2727 ParsedPaperCacheRepository ,
2828 UploadedPaperRepository ,
2929)
30+ from app .db .note_repository import NoteRepository
3031from app .db .conversation_repository import ConversationRepository
3132from app .db .session import get_db
3233from app .dependencies .auth import get_current_user
4647 ensure_unique_storage_name ,
4748 normalize_original_filename ,
4849 sanitize_storage_filename ,
50+ build_storage_name_with_email ,
4951)
5052
5153if TYPE_CHECKING :
@@ -151,6 +153,8 @@ async def list_uploaded_papers(
151153async def upload_paper (
152154 file : UploadFile = File (..., description = "需要上传的 PDF 文件" ),
153155 folder_id : int | None = Form (None , description = "文件夹 ID,不填则保存在未分类" ),
156+ conflict_resolution : str | None = Form (None , description = "冲突处理方式:overwrite 或 rename" ),
157+ new_filename : str | None = Form (None , description = "当 conflict_resolution=rename 时的新文件名" ),
154158 current_user = Depends (get_current_user ),
155159 db : AsyncSession = Depends (get_db ),
156160) -> upload_schema .UploadedPaperRead :
@@ -170,42 +174,108 @@ async def upload_paper(
170174 )
171175
172176 cleaned_bytes = raw_bytes [:MAX_UPLOAD_BYTES ]
173- file_hash = _calculate_file_hash (cleaned_bytes )
174-
175- original_display_name = normalize_original_filename (file .filename )
176- storage_candidate = sanitize_storage_filename (original_display_name )
177- stored_filename , destination = ensure_unique_storage_name (UPLOAD_DIR , storage_candidate )
178-
179- await asyncio .to_thread (destination .write_bytes , cleaned_bytes )
180177
181- relative_url = f"/media/uploads/{ stored_filename } "
182-
183- folder_repo = LibraryFolderRepository (db )
184178 repo = UploadedPaperRepository (db )
179+ folder_repo = LibraryFolderRepository (db )
185180 resolved_folder_id = await _ensure_folder_access (
186181 folder_repo ,
187182 folder_id = folder_id if folder_id and folder_id > 0 else None ,
188183 user_id = current_user .id ,
189184 )
185+
186+ target_display_name = normalize_original_filename (file .filename )
187+ existing = await repo .get_by_original_name (current_user .id , target_display_name )
188+
189+ # 解析冲突处理策略
190+ resolution = (conflict_resolution or "" ).strip ().lower () or None
191+ if existing and not resolution :
192+ raise HTTPException (
193+ status_code = status .HTTP_409_CONFLICT ,
194+ detail = {
195+ "message" : "当前用户已存在同名文件" ,
196+ "conflict" : True ,
197+ "filename" : target_display_name ,
198+ "options" : ["overwrite" , "rename" ],
199+ },
200+ )
201+
202+ if resolution == "rename" :
203+ if not new_filename :
204+ raise HTTPException (status_code = status .HTTP_400_BAD_REQUEST , detail = "重命名上传时必须提供新文件名" )
205+ target_display_name = normalize_original_filename (new_filename )
206+ existing = await repo .get_by_original_name (current_user .id , target_display_name )
207+ if existing :
208+ raise HTTPException (
209+ status_code = status .HTTP_409_CONFLICT ,
210+ detail = {
211+ "message" : "新的文件名仍然存在冲突,请更换名称" ,
212+ "conflict" : True ,
213+ "filename" : target_display_name ,
214+ "options" : ["overwrite" , "rename" ],
215+ },
216+ )
217+ elif resolution not in {None , "overwrite" }:
218+ raise HTTPException (status_code = status .HTTP_400_BAD_REQUEST , detail = "无效的冲突处理方式" )
219+
220+ if existing and resolution == "overwrite" :
221+ stored_filename = existing .stored_filename
222+ destination = (UPLOAD_DIR / stored_filename ).resolve ()
223+ relative_url = existing .file_url or f"/media/uploads/{ stored_filename } "
224+ # 先删除旧文件,再写入新内容,保持物理名不变
225+ try :
226+ if destination .exists ():
227+ await asyncio .to_thread (destination .unlink )
228+ except Exception :
229+ logger .warning ("Failed to remove existing file before overwrite: %s" , destination )
230+ else :
231+ storage_candidate = build_storage_name_with_email (target_display_name , current_user .email )
232+ storage_candidate = sanitize_storage_filename (storage_candidate )
233+ stored_filename , destination = ensure_unique_storage_name (UPLOAD_DIR , storage_candidate )
234+ relative_url = f"/media/uploads/{ stored_filename } "
235+
236+ await asyncio .to_thread (destination .write_bytes , cleaned_bytes )
237+ file_hash = _calculate_file_hash (cleaned_bytes )
238+
190239 metadata_json : dict | None = None
191240 try :
192- metadata_json = await extract_pdf_metadata_async (destination , original_display_name )
241+ metadata_json = await extract_pdf_metadata_async (destination , target_display_name )
193242 except Exception as exc : # pragma: no cover - best effort only
194243 logger .warning ("Failed to extract metadata for uploaded PDF: %s" , exc )
195244 metadata_json = None
196245
197246 try :
198- record = await repo .create (
199- user_id = current_user .id ,
200- stored_filename = stored_filename ,
201- original_filename = original_display_name ,
202- content_type = file .content_type or "application/pdf" ,
203- file_size = len (cleaned_bytes ),
204- file_url = relative_url ,
205- file_hash = file_hash ,
206- folder_id = resolved_folder_id ,
207- metadata_json = metadata_json ,
208- )
247+ if existing and resolution == "overwrite" :
248+ await repo .purge_cached_artifacts (existing )
249+
250+ conv_repo = ConversationRepository (db )
251+ note_repo = NoteRepository (db )
252+ await conv_repo .delete_conversations_for_paper (current_user .id , existing .id )
253+ await note_repo .detach_uploaded_paper (current_user .id , existing .id )
254+
255+ # 更新记录为新的文件
256+ await repo .update_file_fields (
257+ existing ,
258+ stored_filename = stored_filename ,
259+ file_url = relative_url ,
260+ file_size = len (cleaned_bytes ),
261+ file_hash = file_hash ,
262+ content_type = file .content_type or "application/pdf" ,
263+ )
264+ await repo .update_metadata (existing , metadata_json )
265+ record = existing
266+ else :
267+ # 新建记录(无冲突或重命名)
268+ record = await repo .create (
269+ user_id = current_user .id ,
270+ stored_filename = stored_filename ,
271+ original_filename = target_display_name ,
272+ content_type = file .content_type or "application/pdf" ,
273+ file_size = len (cleaned_bytes ),
274+ file_url = relative_url ,
275+ file_hash = file_hash ,
276+ folder_id = resolved_folder_id ,
277+ metadata_json = metadata_json ,
278+ )
209279 await db .commit ()
210280 except Exception :
211281 await db .rollback ()
@@ -834,21 +904,29 @@ async def _handle_paper_qa(
834904 conversation_id = request .conversation_id
835905
836906 if conversation_id :
837- # 验证对话存在且属于当前用户,并属于智能阅读
907+ # 验证对话存在且属于当前用户,并属于智能阅读,且绑定到该文档
838908 conversation = await conv_repo .get_conversation (conversation_id , current_user .id )
839909 if not conversation or conversation .category != "reading" :
840910 raise HTTPException (
841911 status_code = status .HTTP_404_NOT_FOUND ,
842912 detail = "Conversation not found or access denied"
843913 )
914+ if conversation .paper_id not in (None , request .paper_id ):
915+ raise HTTPException (
916+ status_code = status .HTTP_404_NOT_FOUND ,
917+ detail = "Conversation not found for this paper" ,
918+ )
919+ if conversation .paper_id is None :
920+ conversation .paper_id = request .paper_id
921+ await db .flush ()
844922 # 获取历史消息
845923 history_messages = await conv_repo .get_conversation_messages (conversation_id , current_user .id )
846924 else :
847- # 创建新对话
925+ # 创建新对话并绑定该文档
848926 paper_title = parse_result .get ("metadata" , {}).get ("title" , "未命名文档" )
849927 conversation = await conv_repo .create_conversation (
850928 current_user .id ,
851- ConversationCreate (title = f"关于《{ paper_title } 》的讨论" , category = "reading" )
929+ ConversationCreate (title = f"关于《{ paper_title } 》的讨论" , category = "reading" , paper_id = request . paper_id )
852930 )
853931 conversation_id = conversation .id
854932 history_messages = []
0 commit comments