feat:新增投顾工作台相关接口

This commit is contained in:
2026-09-12 20:42:33 +08:00
parent b95fb5d4f7
commit 639a10e10a
50 changed files with 3434 additions and 14 deletions
+14
View File
@@ -0,0 +1,14 @@
"""投顾工作台路由聚合(前缀 /api/advisor,在 api/router.py 以 /api 挂载)。"""
from fastapi import APIRouter
from api.advisor import audit, customers, dashboard, diagnosis, drafts, report, todos, visits
router = APIRouter(prefix="/advisor", tags=["投顾工作台"])
router.include_router(dashboard.router)
router.include_router(customers.router)
router.include_router(diagnosis.router)
router.include_router(drafts.router)
router.include_router(todos.router)
router.include_router(visits.router)
router.include_router(audit.router)
router.include_router(report.router)
+13
View File
@@ -0,0 +1,13 @@
"""路由层透传信息提取:投顾 JWT 与 trace_id(供 Agent 客户端透传)。"""
from __future__ import annotations
from fastapi import Request
from utils.request_id import get_request_id, new_request_id
def extract_auth(request: Request) -> tuple[str, str]:
"""返回 (Authorization 头原值, trace_id);trace_id 优先透传,无则生成。"""
auth = request.headers.get("Authorization", "")
trace_id = request.headers.get("X-Trace-Id") or get_request_id() or new_request_id()
return auth, trace_id
+52
View File
@@ -0,0 +1,52 @@
"""审计台账路由(业务在 service/advisor/audit.py)。导出为 CSV 下载。"""
from datetime import datetime
from fastapi import APIRouter, Depends, Query
from fastapi.responses import Response
from sqlalchemy.ext.asyncio import AsyncSession
from api.deps import require_advisor
from config.deps import get_db
from model.sys_user import SysUser
from service.advisor import audit as audit_service
from utils.response import success
router = APIRouter()
@router.get("/audit/ledger", summary="本人审计台账筛选")
async def ledger(
action: str | None = Query(None, max_length=64),
keyword: str | None = Query(None, max_length=128),
start: datetime | None = Query(None),
end: datetime | None = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(
await audit_service.list_ledger(
db, user, action=action, keyword=keyword, start=start, end=end,
page=page, page_size=page_size,
)
)
@router.get("/audit/export", summary="导出本人审计台账(CSV)")
async def export(
action: str | None = Query(None, max_length=64),
keyword: str | None = Query(None, max_length=128),
start: datetime | None = Query(None),
end: datetime | None = Query(None),
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
csv_text = await audit_service.export_ledger(
db, user, action=action, keyword=keyword, start=start, end=end
)
return Response(
content=csv_text,
media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": 'attachment; filename="advisor_audit.csv"'},
)
+72
View File
@@ -0,0 +1,72 @@
"""客户 360 路由(业务在 service/advisor/customers.py,路由只做编排)。"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from api.deps import require_advisor
from config.deps import get_db
from model.sys_user import SysUser
from schemas.advisor import RelationReq
from service.advisor import customers as customers_service
from utils.response import success
router = APIRouter()
@router.get("/customers", summary="名下客户列表(只读 + 脱敏)")
async def list_customers(
status: str | None = Query(None, max_length=16),
keyword: str | None = Query(None, max_length=64),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(
await customers_service.list_customers(
db, user, status=status, keyword=keyword, page=page, page_size=page_size
)
)
@router.get("/customers/{customer_id}", summary="客户详情(unmask=true 查看完整手机号并留痕)")
async def get_customer(
customer_id: int,
unmask: bool = Query(False),
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(await customers_service.get_customer(db, user, customer_id, unmask=unmask))
@router.get("/customers/{customer_id}/holdings", summary="客户持仓(只读)")
async def get_holdings(
customer_id: int,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(await customers_service.get_customer_holdings(db, user, customer_id))
@router.get("/customers/{customer_id}/reports", summary="客户历史建议报告")
async def get_reports(
customer_id: int,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(
await customers_service.get_customer_reports(
db, user, customer_id, page=page, page_size=page_size
)
)
@router.post("/customers/{customer_id}/relation", summary="签约/结束服务(工作台唯一写入方)")
async def update_relation(
customer_id: int,
req: RelationReq,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(await customers_service.update_relation(db, user, customer_id, req))
+19
View File
@@ -0,0 +1,19 @@
"""驾驶舱路由(业务在 service/advisor/dashboard.py,路由只做编排)。"""
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from api.deps import require_advisor
from config.deps import get_db
from model.sys_user import SysUser
from service.advisor import dashboard as dashboard_service
from utils.response import success
router = APIRouter()
@router.get("/dashboard", summary="首页数据与待办聚合")
async def dashboard(
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(await dashboard_service.get_dashboard(db, user))
+46
View File
@@ -0,0 +1,46 @@
"""资产配置与组合诊断路由(业务在 service/advisor/diagnosis.py,路由只做编排)。"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from api.deps import require_advisor
from config.deps import get_db
from model.sys_user import SysUser
from service.advisor import diagnosis as diagnosis_service
from utils.response import success
router = APIRouter()
@router.get("/diagnosis/{customer_id}", summary="持仓诊断(分布 + 集中度,只读)")
async def diagnose(
customer_id: int,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(await diagnosis_service.diagnose(db, user, customer_id))
@router.get("/strategies", summary="标准策略库(只读)")
async def strategies(
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(await diagnosis_service.list_strategies(db))
@router.get("/funds", summary="白名单产品筛选")
async def funds(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
keyword: str | None = Query(None, max_length=64),
product_type: str | None = Query(None, max_length=32),
risk_level: str | None = Query(None, max_length=8),
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(
await diagnosis_service.list_funds(
db, page=page, page_size=page_size, keyword=keyword,
product_type=product_type, risk_level=risk_level,
)
)
+99
View File
@@ -0,0 +1,99 @@
"""草稿/调仓/发送路由(业务在 service/advisor/drafts.py,路由只做编排 + 透传 JWT/trace_id)。"""
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy.ext.asyncio import AsyncSession
from api.advisor._auth import extract_auth
from api.deps import require_advisor
from config.deps import get_db
from model.sys_user import SysUser
from schemas.advisor import DraftSaveReq, RebalanceRunReq, TalkScriptReq
from service.advisor import drafts as drafts_service
from utils.response import success
router = APIRouter()
@router.get("/drafts", summary="草稿列表(代理 Agent draft/list)")
async def list_drafts(
request: Request,
customer_id: int | None = Query(None),
status: str | None = Query(None, max_length=16),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
auth, trace_id = extract_auth(request)
return success(
await drafts_service.list_drafts(
db, user, auth_header=auth, trace_id=trace_id,
customer_id=customer_id, status=status, page=page, page_size=page_size,
)
)
@router.get("/drafts/{draft_id}", summary="草稿详情(代理 Agent draft/{id})")
async def get_draft(
draft_id: str,
request: Request,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
auth, trace_id = extract_auth(request)
return success(await drafts_service.get_draft(db, user, auth_header=auth, trace_id=trace_id, draft_id=draft_id))
@router.put("/drafts/{draft_id}/save", summary="编辑保存(代理 Agent + 本地镜像快照)")
async def save_draft(
draft_id: str,
req: DraftSaveReq,
request: Request,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
auth, trace_id = extract_auth(request)
return success(await drafts_service.save_draft(db, user, auth_header=auth, trace_id=trace_id, draft_id=draft_id, req=req))
@router.post("/drafts/{draft_id}/discard", summary="废弃草稿(回写 Agent)")
async def discard_draft(
draft_id: str,
request: Request,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
auth, trace_id = extract_auth(request)
return success(await drafts_service.discard_draft(db, user, auth_header=auth, trace_id=trace_id, draft_id=draft_id))
@router.post("/drafts/{draft_id}/send", summary="发送终审 + 手动发送(工作台本地)")
async def send_draft(
draft_id: str,
request: Request,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
auth, trace_id = extract_auth(request)
return success(await drafts_service.send_draft(db, user, auth_header=auth, trace_id=trace_id, draft_id=draft_id))
@router.post("/rebalance/run", summary="手动触发调仓再平衡(异步受理)")
async def run_rebalance(
req: RebalanceRunReq,
request: Request,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
auth, trace_id = extract_auth(request)
return success(await drafts_service.run_rebalance(db, user, auth_header=auth, trace_id=trace_id, req=req))
@router.post("/talk-script", summary="生成沟通话术草稿(同步,超时降级)")
async def talk_script(
req: TalkScriptReq,
request: Request,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
auth, trace_id = extract_auth(request)
return success(await drafts_service.generate_talk_script(db, user, auth_header=auth, trace_id=trace_id, req=req))
+19
View File
@@ -0,0 +1,19 @@
"""个人绩效报表路由(业务在 service/advisor/report.py)。"""
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from api.deps import require_advisor
from config.deps import get_db
from model.sys_user import SysUser
from service.advisor import report as report_service
from utils.response import success
router = APIRouter()
@router.get("/report/personal", summary="个人绩效统计")
async def personal_report(
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(await report_service.personal_report(db, user))
+38
View File
@@ -0,0 +1,38 @@
"""投顾待办路由(业务在 service/advisor/todos.py,路由只做编排)。"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from api.deps import require_advisor
from config.deps import get_db
from model.sys_user import SysUser
from schemas.advisor import TodoHandleReq
from service.advisor import todos as todos_service
from utils.response import success
router = APIRouter()
@router.get("/todos", summary="本人待办列表")
async def list_todos(
status: str | None = Query(None, max_length=16),
todo_type: str | None = Query(None, max_length=32),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(
await todos_service.list_todos(
db, user, status=status, todo_type=todo_type, page=page, page_size=page_size
)
)
@router.post("/todos/{todo_id}/handle", summary="处理待办(process/done)")
async def handle_todo(
todo_id: int,
req: TodoHandleReq,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(await todos_service.handle_todo(db, user, todo_id, req))
+56
View File
@@ -0,0 +1,56 @@
"""投后服务路由(回访/话术库/触达日志,业务在 service/advisor/visits.py)。"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from api.deps import require_advisor
from config.deps import get_db
from model.sys_user import SysUser
from schemas.advisor import VisitCreateReq
from service.advisor import visits as visits_service
from utils.response import success
router = APIRouter()
@router.get("/visits", summary="回访记录列表")
async def list_visits(
customer_id: int | None = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(
await visits_service.list_visits(
db, user, customer_id=customer_id, page=page, page_size=page_size
)
)
@router.post("/visits", summary="新增回访留痕(不触发 Agent 记忆抽取)")
async def create_visit(
req: VisitCreateReq,
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(await visits_service.create_visit(db, user, req))
@router.get("/talk-templates", summary="合规话术库(内置,投顾参考)")
async def talk_templates(user: SysUser = Depends(require_advisor)):
return success(visits_service.list_talk_templates())
@router.get("/touch-logs", summary="触达留痕(站内信 + 回访)")
async def touch_logs(
customer_id: int = Query(...),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user: SysUser = Depends(require_advisor),
db: AsyncSession = Depends(get_db),
):
return success(
await visits_service.list_touch_logs(
db, user, customer_id, page=page, page_size=page_size
)
)