99 lines
4.0 KiB
Python
99 lines
4.0 KiB
Python
"""投后服务与陪伴:回访留痕 + 合规话术库 + 触达日志 + AI 话术(代理 Agent)。
|
|||
|
|
|
||
|
|
PRD §4.4:回访对话不经过投顾Agent,人工录入仅留痕,不更新记忆单元;
|
||
|
|
AI 话术仅作参考,正式发送走工作台消息模块。
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from model.advisor_visit_record import AdvisorVisitRecord
|
||
|
|
from model.sys_user import SysUser
|
||
|
|
from repositories.advisor_visit_record import AdvisorVisitRecordRepo
|
||
|
|
from repositories.sys_message import SysMessageRepo
|
||
|
|
from schemas.advisor import VisitCreateReq
|
||
|
|
from service.advisor.permissions import ensure_customer_owned
|
||
|
|
from utils.exceptions import ParamError
|
||
|
|
|
||
|
|
# 合规话术库(内置,标准化投教/市场解读/调仓沟通)。
|
||
|
|
# 假设:V1.0 内置常量,后续可迁移 sys_config 运营化;内容不含承诺收益等敏感词。
|
||
|
|
TALK_TEMPLATES = [
|
||
|
|
{"scene": "市场波动安抚", "content": "市场短期波动属正常现象,请结合自身风险承受能力理性看待,勿因短期涨跌追涨杀跌。"},
|
||
|
|
{"scene": "调仓沟通", "content": "本次调整旨在使组合回归目标配置,不构成任何收益承诺,请结合自身情况审慎判断。"},
|
||
|
|
{"scene": "风险测评到期提醒", "content": "您的风险测评即将到期,为保障适当性匹配,请及时完成最新测评。"},
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def _visit_item(v: AdvisorVisitRecord) -> dict:
|
||
|
|
return {
|
||
|
|
"id": v.id,
|
||
|
|
"customer_id": v.customer_id,
|
||
|
|
"advisor_id": v.advisor_id,
|
||
|
|
"visit_type": v.visit_type,
|
||
|
|
"visit_time": v.visit_time.isoformat(),
|
||
|
|
"summary": v.summary,
|
||
|
|
"audio_url": v.audio_url,
|
||
|
|
"create_time": v.create_time.isoformat() if v.create_time else None,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
async def list_visits(
|
||
|
|
db: AsyncSession, user: SysUser, *, customer_id: int | None = None,
|
||
|
|
page: int = 1, page_size: int = 20,
|
||
|
|
) -> dict:
|
||
|
|
if customer_id is not None:
|
||
|
|
await ensure_customer_owned(db, user.id, customer_id)
|
||
|
|
repo = AdvisorVisitRecordRepo(db)
|
||
|
|
items = await repo.list_by_advisor(
|
||
|
|
advisor_id=user.id, customer_id=customer_id,
|
||
|
|
limit=page_size, offset=(page - 1) * page_size,
|
||
|
|
)
|
||
|
|
total = await repo.count_by_advisor(advisor_id=user.id, customer_id=customer_id)
|
||
|
|
return {"total": total, "page": page, "page_size": page_size, "items": [_visit_item(v) for v in items]}
|
||
|
|
|
||
|
|
|
||
|
|
async def create_visit(db: AsyncSession, user: SysUser, req: VisitCreateReq) -> dict:
|
||
|
|
await ensure_customer_owned(db, user.id, req.customer_id)
|
||
|
|
record = AdvisorVisitRecord(
|
||
|
|
customer_id=req.customer_id,
|
||
|
|
advisor_id=user.id,
|
||
|
|
visit_type=req.visit_type,
|
||
|
|
visit_time=req.visit_time,
|
||
|
|
summary=req.summary,
|
||
|
|
audio_url=req.audio_url,
|
||
|
|
)
|
||
|
|
record = await AdvisorVisitRecordRepo(db).add(record)
|
||
|
|
return {"visit_id": record.id}
|
||
|
|
|
||
|
|
|
||
|
|
def list_talk_templates() -> list[dict]:
|
||
|
|
"""合规话术库(内置,投顾参考;不自动发送)。"""
|
||
|
|
return TALK_TEMPLATES
|
||
|
|
|
||
|
|
|
||
|
|
async def list_touch_logs(
|
||
|
|
db: AsyncSession, user: SysUser, customer_id: int, *, page: int = 1, page_size: int = 20
|
||
|
|
) -> dict:
|
||
|
|
"""触达留痕:站内信(发送触达)+ 回访记录(人工沟通)聚合。
|
||
|
|
|
||
|
|
假设:V1.0 触达通道仅站内信与回访;短信/企微/电话统一落在回访记录。
|
||
|
|
"""
|
||
|
|
await ensure_customer_owned(db, user.id, customer_id)
|
||
|
|
messages = await SysMessageRepo(db).list_by_user(
|
||
|
|
customer_id, limit=page_size, offset=(page - 1) * page_size
|
||
|
|
)
|
||
|
|
visits = await AdvisorVisitRecordRepo(db).list_by_advisor(
|
||
|
|
advisor_id=user.id, customer_id=customer_id,
|
||
|
|
limit=page_size, offset=(page - 1) * page_size,
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"messages": [
|
||
|
|
{
|
||
|
|
"id": m.id, "msg_type": m.msg_type, "title": m.title,
|
||
|
|
"content": m.content, "create_time": m.create_time.isoformat() if m.create_time else None,
|
||
|
|
}
|
||
|
|
for m in messages
|
||
|
|
],
|
||
|
|
"visits": [_visit_item(v) for v in visits],
|
||
|
|
}
|