-
Notifications
You must be signed in to change notification settings - Fork 0
rebase: a basic version 0.2 #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
932ed02
aba5143
fae5fde
fa87047
3e10bfa
f29be3f
4a8cbbf
0b47ab3
a8f26dd
09216e7
c3ed0fe
d1defa6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,16 +2,20 @@ | |
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import shutil | ||
| from pathlib import Path | ||
| from typing import Final | ||
| from uuid import uuid4 | ||
|
|
||
| from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status | ||
| from sqlalchemy import delete, select | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from app.core.config import get_settings | ||
| from app.core.security import hash_password, verify_password | ||
| from app.db.repository import UserRepository | ||
| from app.models.uploaded_paper import UploadedPaper | ||
| from app.models.parsed_paper_cache import ParsedPaperCache | ||
| from app.db.session import get_db | ||
| from app.dependencies.auth import get_current_user | ||
| from app.schemas import user as user_schema | ||
|
|
@@ -45,6 +49,16 @@ def _remove_avatar_file(avatar_url: str | None) -> None: | |
| pass | ||
|
|
||
|
|
||
| def _remove_path_safely(target: Path) -> None: | ||
| try: | ||
| if target.is_file() or target.is_symlink(): | ||
| target.unlink() | ||
| elif target.is_dir(): | ||
| shutil.rmtree(target) | ||
| except OSError: | ||
| pass | ||
|
|
||
|
|
||
| @router.post( | ||
| "", | ||
| response_model=user_schema.UserRead, | ||
|
|
@@ -179,13 +193,35 @@ async def delete_account( | |
| current_user=Depends(get_current_user), | ||
| db: AsyncSession = Depends(get_db), | ||
| ): | ||
| """Soft-delete the user account by marking it inactive.""" | ||
| """Hard delete user and all related data/files so the email can be reused.""" | ||
|
|
||
| # Collect uploaded papers before DB deletion (to remove files/cache/parsed dirs) | ||
| result = await db.execute( | ||
| select(UploadedPaper.id, UploadedPaper.stored_filename, UploadedPaper.file_hash).where( | ||
| UploadedPaper.user_id == current_user.id | ||
| ) | ||
| ) | ||
| uploads = list(result.all()) | ||
|
|
||
| # Remove physical files and parsed outputs | ||
| for paper_id, stored_filename, file_hash in uploads: | ||
| upload_path = settings.media_path / "uploads" / stored_filename | ||
| parse_dir = settings.media_path / "parsed" / f"paper_{paper_id}" | ||
| _remove_path_safely(upload_path) | ||
| _remove_path_safely(parse_dir) | ||
|
|
||
| if file_hash: | ||
| await db.execute(delete(ParsedPaperCache).where(ParsedPaperCache.file_hash == file_hash)) | ||
|
|
||
| # Remove avatar if under media | ||
| _remove_avatar_file(getattr(current_user, "avatar_url", None)) | ||
|
|
||
| repo = UserRepository(db) | ||
| await repo.update(current_user, {"is_active": False}) | ||
| # 删除用户前先删除上传记录,避免 ORM 删除流程尝试将 user_id 置空导致约束错误 | ||
| await db.execute(delete(UploadedPaper).where(UploadedPaper.user_id == current_user.id)) | ||
|
|
||
| # Finally delete the user (FK cascades will clean remaining dependencies) | ||
| await db.delete(current_user) | ||
|
|
||
| await db.commit() | ||
|
|
||
| return {"message": "账户已成功注销"} | ||
| return {"message": "账户已彻底删除,可使用该邮箱重新注册"} | ||
|
Comment on lines
+196
to
+227
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,13 +26,14 @@ async def create_conversation(self, user_id: int, data: ConversationCreate) -> C | |
| user_id=user_id, | ||
| title=data.title, | ||
| category=data.category or "search", | ||
| paper_id=getattr(data, "paper_id", None), | ||
| ) | ||
| self.db.add(conversation) | ||
| await self.db.commit() | ||
| await self.db.refresh(conversation) | ||
| return conversation | ||
|
|
||
| async def get_conversation(self, conversation_id: int, user_id: int) -> Optional[Conversation]: | ||
| async def get_conversation(self, conversation_id: int, user_id: int, *, paper_id: int | None = None) -> Optional[Conversation]: | ||
| """获取特定对话(含消息)""" | ||
| stmt = ( | ||
| select(Conversation) | ||
|
|
@@ -43,6 +44,8 @@ async def get_conversation(self, conversation_id: int, user_id: int) -> Optional | |
| ) | ||
| .options(selectinload(Conversation.messages)) | ||
| ) | ||
| if paper_id is not None: | ||
| stmt = stmt.where(Conversation.paper_id == paper_id) | ||
|
Comment on lines
+47
to
+48
|
||
| result = await self.db.execute(stmt) | ||
| return result.scalar_one_or_none() | ||
|
|
||
|
|
@@ -111,6 +114,28 @@ async def delete_conversation(self, conversation_id: int, user_id: int) -> bool: | |
| await self.db.commit() | ||
| return True | ||
|
|
||
| async def delete_conversations_for_paper(self, user_id: int, paper_id: int) -> int: | ||
| """软删除绑定到指定文档的阅读类对话,返回删除数量""" | ||
| stmt = ( | ||
| select(Conversation) | ||
| .where( | ||
| Conversation.user_id == user_id, | ||
| Conversation.paper_id == paper_id, | ||
| Conversation.category == "reading", | ||
| Conversation.is_deleted == False, | ||
| ) | ||
| .options(selectinload(Conversation.messages)) | ||
| ) | ||
| result = await self.db.execute(stmt) | ||
| conversations = result.scalars().all() | ||
| deleted = 0 | ||
| for conv in conversations: | ||
| conv.is_deleted = True | ||
| deleted += 1 | ||
| if deleted: | ||
| await self.db.commit() | ||
| return deleted | ||
|
|
||
| async def add_message( | ||
| self, | ||
| conversation_id: int, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable resolution is normalized to lowercase and then compared against None and string literals. However, if conflict_resolution is an empty string after stripping, the expression resolves to None due to "or None". This logic could be clearer. Consider: resolution = conflict_resolution.strip().lower() if conflict_resolution and conflict_resolution.strip() else None