Files
group_fqcd_jr/app/service/conversation_service.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

92 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 边界),已由入口校验。
取 `limit + 1` 行来判断"还有没有更旧的":只看"取满没取满"会把恰好等于 limit 的
最后一页说成还有下一页。`next_cursor` 就是本页最后一条的 `message_id` ——
游标语义是"取更旧的一页",所以它天然可续(集成测试正是这么翻页的)。
返回的是内部结构 `{items, next_cursor, has_more}`,由 Controller 用
`list_envelope` 拆成 `docs/05` §3.3 要求的 `{data, meta}`;此前这里直接返回
`{"data": [...]}`,成功响应因此**完全没有 `meta.trace_id`**。
"""
rows = await self.repository.messages(
session_id, int(context.user_id), limit + 1, before=before
)
has_more = len(rows) > limit
page_rows = rows[:limit]
return {
"items": [
{
"message_id": str(row.id),
"role": row.role,
"content": row.content,
"created_at": row.created_at.isoformat() + "Z",
}
for row in page_rows
],
"next_cursor": str(page_rows[-1].id) if has_more and page_rows else None,
"has_more": has_more,
}
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})
# 只返回业务数据;§3.3 的信封由 Controller 套(此前这里自带 {"data": ...},
# 于是成功响应没有 meta.trace_id)。
return {"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),
))