feat:新增投顾工作台相关接口
This commit is contained in:
@@ -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)
|
||||
@@ -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
|
||||
@@ -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"'},
|
||||
)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user