from datetime import UTC, datetime from uuid import uuid4 from sqlalchemy.ext.asyncio import AsyncSession from app.core.contracts import RequestContext from app.core.errors import ( FeedbackAlreadyExistsError, GenericResourceNotFoundError, ) from app.model.audit import InteractionAudit from app.model.conversation import ConversationFeedback from app.repository.conversation_repository import ConversationRepository from app.service.authorization_service import AuthorizationService class ConversationService: def __init__(self, session: AsyncSession) -> None: self.session = session self.repository = ConversationRepository(session) async def messages( self, session_id: str, context: RequestContext, limit: int, before: int | None = None ) -> dict[str, object]: """消息列表投影;`before` 是文档 §3.8 的游标(记录 ID 边界),已由入口校验。""" rows = await self.repository.messages( session_id, int(context.user_id), limit, before=before ) return {"data": [{"message_id": str(row.id), "role": row.role, "content": row.content, "created_at": row.created_at.isoformat() + "Z"} for row in rows]} async def feedback( self, message_id: int, context: RequestContext, rating: int, feedback_type: str | None, feedback_content: str | None, ) -> dict[str, object]: await AuthorizationService.require(context, "conversation:feedback") async with self.session.begin(): message = await self.repository.message(message_id, int(context.user_id)) if message is None: raise GenericResourceNotFoundError("消息不存在") if await self.repository.feedback(message_id, int(context.user_id)) is not None: raise FeedbackAlreadyExistsError("消息已反馈") now = datetime.now(UTC).replace(tzinfo=None) feedback = ConversationFeedback( feedback_no=f"fb-{uuid4().hex[:24]}", session_id=message.session_id, message_id=message_id, customer_id=int(context.user_id), rating=rating, feedback_type=feedback_type, feedback_content=feedback_content, status="open", created_at=now, updated_at=now, ) self.session.add(feedback) self._audit(context, message.session_id, "conversation.feedback_created", {"feedback_no": feedback.feedback_no}) return {"data": {"feedback_no": feedback.feedback_no, "status": feedback.status}} # 转人工申请(POST /api/v1/conversations/{id}/handover-requests)的唯一实现 # 在 PublicPlatformService.write("handover", ...):它同一事务写工单 + Outbox # 事件 + 审计。此处曾经的第二实现只写审计,会让转人工异步链路断掉,已删除。 def _audit( self, context: RequestContext, session_id: str, action: str, detail: dict[str, object] ) -> None: self.session.add(InteractionAudit( actor_type="user", actor_id=int(context.user_id), session_id=session_id, portal=context.portal, action_type=action, detail={**detail, "trace_id": context.trace_id}, created_at=datetime.now(UTC).replace(tzinfo=None), ))