From 736a6653de9b563b376209f51764d604f28c9a3f Mon Sep 17 00:00:00 2001 From: kx_guo Date: Sun, 13 Sep 2026 17:36:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8E=86=E5=8F=B2=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E5=88=A0=E9=99=A4=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/conversations.py | 10 ++++++++++ app/repositories/conversations.py | 10 ++++++++++ app/services/conversations.py | 18 ++++++++++++++++++ tests/test_conversation_delete.py | 19 +++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 tests/test_conversation_delete.py diff --git a/app/api/v1/conversations.py b/app/api/v1/conversations.py index e311284..862a3fa 100644 --- a/app/api/v1/conversations.py +++ b/app/api/v1/conversations.py @@ -57,6 +57,16 @@ async def get_conversation( ) +@router.delete("/{conversation_id}", response_model=ApiResponse[None]) +async def delete_conversation( + conversation_id: int, + session: DatabaseSession, + current_user: Annotated[UserAccount, Depends(require_permission("risk.assess", operate=True))], +) -> ApiResponse[None]: + await ConversationService(session).delete(conversation_id, current_user) + return ApiResponse(data=None, message="对话已删除") + + @router.post("/{conversation_id}/messages", response_model=ApiResponse[ConversationData]) async def send_conversation_message( conversation_id: int, diff --git a/app/repositories/conversations.py b/app/repositories/conversations.py index 757d4d0..24608a1 100644 --- a/app/repositories/conversations.py +++ b/app/repositories/conversations.py @@ -71,3 +71,13 @@ class ConversationRepository: .options(selectinload(Conversation.messages)) ) return await self.session.scalar(statement) + + async def delete_for_user(self, conversation_id: int, user_id: int) -> bool: + """Delete a conversation only when it belongs to the requesting user.""" + + conversation = await self.get_for_user(conversation_id, user_id) + if conversation is None: + return False + await self.session.delete(conversation) + await self.session.flush() + return True diff --git a/app/services/conversations.py b/app/services/conversations.py index 30f1138..1ade4fe 100644 --- a/app/services/conversations.py +++ b/app/services/conversations.py @@ -128,6 +128,24 @@ class ConversationService: raise AppException("对话创建失败", code=500, status_code=500) return self._data(persisted, messages=[], total=0, offset=0) + async def delete(self, conversation_id: int, user: UserAccount) -> None: + """Delete one user-owned conversation and retain an audit trail.""" + + conversation = await self.repository.get_for_user(conversation_id, user.id) + if conversation is None: + raise AppException("对话不存在或无权访问", code=404, status_code=404) + self.session.add( + OperationAudit( + actor_user_id=user.id, + actor_emp_no=user.emp_no, + action="conversation.delete", + resource_type="conversation", + resource_id=str(conversation_id), + audit_metadata={"title": conversation.title[:120]}, + ) + ) + await self.repository.delete_for_user(conversation_id, user.id) + async def send( self, conversation_id: int, diff --git a/tests/test_conversation_delete.py b/tests/test_conversation_delete.py new file mode 100644 index 0000000..ac74098 --- /dev/null +++ b/tests/test_conversation_delete.py @@ -0,0 +1,19 @@ +import pytest + +from app.models.entities import Conversation, UserAccount +from app.services.conversations import ConversationService + + +@pytest.mark.asyncio +async def test_delete_conversation_rejects_conversation_owned_by_another_user() -> None: + service = object.__new__(ConversationService) + + class Repository: + async def get_for_user(self, conversation_id, user_id): + return None + + service.repository = Repository() + service.session = object() + with pytest.raises(Exception) as exc_info: + await service.delete(10, UserAccount(id=2, emp_no="u2", name="用户", role="user")) + assert getattr(exc_info.value, "status_code", None) == 404