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