2026-09-09 21:55:37 +08:00
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from app.core.contracts import RequestContext
|
2026-09-10 15:55:54 +08:00
|
|
|
from app.core.errors import (
|
|
|
|
|
FeedbackAlreadyExistsError,
|
|
|
|
|
GenericResourceNotFoundError,
|
|
|
|
|
)
|
2026-09-09 21:55:37 +08:00
|
|
|
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(
|
2026-09-10 15:55:54 +08:00
|
|
|
self, session_id: str, context: RequestContext, limit: int, before: int | None = None
|
2026-09-09 21:55:37 +08:00
|
|
|
) -> dict[str, object]:
|
2026-09-10 15:55:54 +08:00
|
|
|
"""消息列表投影;`before` 是文档 §3.8 的游标(记录 ID 边界),已由入口校验。"""
|
|
|
|
|
rows = await self.repository.messages(
|
|
|
|
|
session_id, int(context.user_id), limit, before=before
|
|
|
|
|
)
|
2026-09-09 21:55:37 +08:00
|
|
|
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:
|
2026-09-10 15:55:54 +08:00
|
|
|
raise GenericResourceNotFoundError("消息不存在")
|
2026-09-09 21:55:37 +08:00
|
|
|
if await self.repository.feedback(message_id, int(context.user_id)) is not None:
|
2026-09-10 15:55:54 +08:00
|
|
|
raise FeedbackAlreadyExistsError("消息已反馈")
|
2026-09-09 21:55:37 +08:00
|
|
|
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}}
|
|
|
|
|
|
2026-09-10 15:55:54 +08:00
|
|
|
# 转人工申请(POST /api/v1/conversations/{id}/handover-requests)的唯一实现
|
|
|
|
|
# 在 PublicPlatformService.write("handover", ...):它同一事务写工单 + Outbox
|
|
|
|
|
# 事件 + 审计。此处曾经的第二实现只写审计,会让转人工异步链路断掉,已删除。
|
2026-09-09 21:55:37 +08:00
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
))
|