lzf_0626 076d786bc6 补齐客服转人工工单流:从"只能看"到"能推进"(基线状态机,不自行发明)
## 问题
`svc_handover_ticket` 的 DDL 与状态机在 `docs/02` §7.2 早就定好了
(pending → assigned → processing → resolved → closed,未解决可 cancelled),
但平台**只有 handover:read(只读队列)**:没有任何入口能改状态、assigned_to /
accepted_at / resolved_at / closed_at / resolution 五列**全库 0 非空**,
于是 40 张工单永远停在 pending —— 用户看到的就是"工单全都长一样"。

## 改了什么
后端:
- 新增 `app/service/customer_service_handover_action_service.py`:五个动作
  (分配/接单/解决/关闭/取消),`SELECT ... FOR UPDATE` 锁单后判状态;
  接单允许从 pending 自助接管(同时记受理人);取消不写 closed_at(该列属 closed 状态);
  每次流转写一条 interaction_audit(handover.assigned/accepted/resolved/closed/cancelled);
  非法流转 409、坐席不存在 422、工单不存在 404;回包不含 customer_id/session_id。
- 只读服务保持只读(读侧与写侧是两条边界,单测守着"读侧不许长出写方法"),
  但列表支持 `?status=` 六态筛选、详情补上受理人与流转时间(坐席侧路由信息,非客户数据)。
- `app/api/controllers/admin.py`:五个 action 端点 A049–A053
  (assignments / acceptances / resolutions / closures / cancellations),
  走 `ApiTransactionService.execute_in` —— 幂等记录与业务写入同事务、重复键回放。
- 权限:新增 `handover:write`(9069,只授 admin),已并进种子
  `tools/seed_test_rbac.py`;配套幂等脚本 `tools/grant_handover_write_permission.py`。

前端(管理员工作台 · 转人工工单页):
- 按状态给按钮(待处理→分配/直接接单、已分配→接单、处理中→解决、已解决→关闭、
  未解决都可取消),加了状态筛选与"刷新";摘要弹窗补上受理人与四个时间点、处置结论。
- api-client 注册五个端点;workspace.js 的 api-client 引用与页面自身的 ?v= 一并升版,
  避免浏览器拿旧缓存(旧缓存里没有这些端点)。

冒烟与测试:
- `tools/e2e_smoke_test.py`:B 段建的测试工单由 F 段走完 分配→接单→解决→关闭 收尾
  —— 既不再把测试件堆在 pending 队列里(此前每次冒烟攒一张),又让每次冒烟都覆盖一遍状态机。
  总数 40 → 44 项,实测 44/44 全绿。
- 新增单测 24 条(状态机合法/非法路径、越权、坐席不存在、审计、视图不泄漏客户标识)
  与一条真机集成用例(HTTP 十步 + 数据库侧审计证据 + 自动清理)。
- 读侧那条"详情不得返回 assigned_to"的旧断言按新口径更新,并写清为什么。

## 验证
- `pytest tests/unit tests/contract` → 1489 passed, 2 skipped, 0 failed
- 新增集成用例通过;`tests/integration` 全量跑时
  `test_memory_extraction` / `test_run_cancellation_mysql` 两条偶发红 —— 单独跑都通过,
  是 AGENTS.md 已登记的"常驻 Worker 抢队列"(跑验收前须先停 Worker)
- `tools/portal_api_check.py` → 41 项通过 39、失败 0
- `tools/e2e_smoke_test.py` → 44/44 全通过
- `python tools/check_rbac_seed_consistency.py` → 通过(种子 63 条权限)
- 真机 HTTP 实测:分配→接单→解决→关闭四步 200 且时间戳齐全;取消路径 200 且 closed_at 为空;
  同键重发回放不二次推进;对已关闭工单再分配 409;风控账号处置 403

## 文档
`docs/44-演示流程.md`(场景 4/8 + 命令 + 44 项)、`docs/演示用/后端接口文档`(新增 §11.4b 与
A049–A053)、`docs/演示用/全功能流程-大白话版.md`(工单页签改"读写"+ 已知偏差)、
`AGENTS.md`(9066-9069 号段演进 + 冒烟 44 项)
2026-09-15 00:41:59 +08:00
2026-09-14 01:07:43 +08:00

南方基金智能业务平台

本项目是一个基于 FastAPI 和 MVC+S 架构的金融业务 Agent 平台,当前包含以下三项核心业务:

  1. 场外基金申购/赎回运营流程
  2. 产品推介材料与宣传海报生成
  3. 金融自然语言转 SQL(NL2SQL)查询

项目中的场外基金运营、产品推介材料与场内基金模拟交易相互隔离。场外业务使用独立的 offsite_fund_* 数据表和接口,不写入场内交易表。

一、技术栈

  • Python 3.13
  • FastAPI、Uvicorn
  • SQLAlchemy 2.x、Alembic
  • MySQL、Redis
  • Pydantic v2
  • JWT + RBAC
  • python-pptx、OpenPyXL、Pillow、Matplotlib
  • 可选:阿里云 DocMind/OCR、DeepSeek、SMTP、Milvus、Neo4j

二、项目结构

group_fqcd_jr/
├── app/
│   ├── api/controllers/             # HTTP 路由和请求参数校验
│   ├── api/schemas/                # 请求/响应模型
│   ├── core/                       # 业务契约、配置和公共规则
│   ├── model/                      # SQLAlchemy 数据模型
│   ├── service/                    # 业务编排、规则和数据访问
│   ├── infrastructure/             # 数据库、缓存和外部基础设施适配
│   └── worker/                     # 异步 Worker
├── data/                            # 测试与演示数据集
├── docs/                            # 架构、接口、数据库和业务说明
├── tests/                           # 单元、契约、集成和环境测试
├── tools/                           # 迁移、种子、检查和测试工具
├── alembic/                         # 数据库迁移
├── hq.py                            # 南方基金行情数据适配模块
├── nl2sql_yc.py                     # 金融 NL2SQL 兼容入口和查询逻辑
├── .env.example                     # 环境变量模板
└── README.md

三、环境准备

项目要求 Python >=3.13,<3.14。建议使用独立虚拟环境:

conda create -n jr_py313 python=3.13
conda activate jr_py313
pip install -r requirements.txt

复制配置文件:

Copy-Item .env.example .env

然后根据本机环境修改 .env,至少配置:

MYSQL_DSN
JWT_PRIVATE_KEY_PATH
JWT_PUBLIC_KEY_PATH

.env、JWT 私钥、模型密钥和真实外部服务凭据禁止提交到 Git。

四、启动服务

4.1 数据库迁移

请确保 MySQL 已启动,并且 .env 中的数据库连接可用:

alembic upgrade head
python tools/audit_schema.py

4.2 启动 HTTP 服务

python -m uvicorn app.main:app --host 127.0.0.1 --port 8099

服务地址:

http://127.0.0.1:8099

OpenAPI 文档:

http://127.0.0.1:8099/docs

4.3 启动 Worker

需要异步处理 Agent 运行或场外邮件时,另开终端:

python -m app.worker

Worker 是否启用场外收信,取决于:

OFFSITE_MAIL_WORKER_ENABLED
OFFSITE_IMAP_ENABLED
OFFSITE_WORKER_USER_ID

五、功能一:场外基金申购/赎回

5.1 功能说明

场外流程面向运营人员,主要处理:

邮件接收
→ 附件保存
→ OCR/文档识别
→ 结构化字段提取
→ 申购/赎回规则计算
→ NL2SQL 查询资金和持仓数据
→ 运营人员确认
→ 创建通知
→ 发送或 dry-run
→ 审计和统计

支持的单据类型:

  • subscription:申购
  • redemption:赎回
  • summary:汇总材料
  • other:其他材料

系统保留原始邮件、附件、识别结果、查询记录、规则结果、人工修正记录和审计记录。 人工修正不会覆盖 Agent 原始识别值。

5.2 主要接口

基础路径:

/api/v1/offsite-fund
方法 路径 用途
GET /mails 分页查询场外邮件
GET /mails/{mail_id} 查看邮件详情
POST /mails/{mail_id}/deletions 软删除邮件
GET /mails/{mail_id}/recognition-fields 查看 OCR 识别字段
PUT /mails/{mail_id}/recognition-fields 保存 OCR 字段人工修正
GET /documents/{task_id}/nl2sql-fields 查看 NL2SQL 返回字段
PUT /documents/{task_id}/nl2sql-fields 保存 NL2SQL 字段人工修正
GET /documents/{task_id}/rule-results 查看规则判定结果
POST /documents/{task_id}/rule-results/recalculations 使用已有结果重新判定
GET /mailbox-status 查看收件游标状态
POST /mailbox-status/recoveries 恢复被阻塞的收件游标
GET /attachments/{attachment_id}/file 预览或下载原始附件
POST /documents/{task_id}/confirmations 运营人员确认单据
POST /documents/{task_id}/recognition-retries 重试识别异常单据
POST /documents/{task_id}/notifications 创建运营通知
POST /notifications/{notification_id}/send 发送通知
POST /settlement-statistics/recalculate 重算结算统计

触发场外单据 NL2SQL 核对的接口为:

POST /api/tasks/{task_id}/trigger-agent-nl2sql

5.3 权限与安全

  • 读取权限:offsite:read
  • 写入权限:offsite:write
  • 确认权限:offsite:confirm
  • NL2SQL 权限:offsite:nl2sql
  • 通知权限:offsite:notify
  • 请求中的 operator_id 必须与 JWT 身份一致
  • 所有写操作都进行权限校验、操作人校验、审计和状态流转校验
  • 通知发送支持幂等,SMTP 默认关闭并使用 dry-run

六、功能二:产品推介材料与宣传海报生成

6.1 功能说明

该模块用于新产品的结构化资料整理和宣传材料生成。当前生成链路为:

创建任务
→ 填写产品、经理、团队、策略、费用、业绩和风险资料
→ 上传经理照片、业绩数据、来源证据或模板
→ 合规检查
→ 生成 PPTX、宣传海报和可选 PDF
→ 审核
→ 发送给指定投顾

支持的输入附件:

  • manager_photo:基金经理照片
  • performance_data:CSV/XLSX 业绩数据
  • source_evidence:来源证据
  • template_file:材料模板

支持的输出格式:

  • pptx
  • poster
  • pdf,需要配置 LibreOffice/soffice 等转换器

模块会执行风险披露、费用结构、业绩数据、输入完整性和生成文本合规检查。 合规阻断时返回具体 findings 和补齐建议,不继续生成材料。

6.2 主要接口

基础路径:

/api/v1/fund-promotion-materials
方法 路径 用途
POST / 创建推介材料任务
PUT /{task_no}/inputs 保存结构化输入资料
POST /{task_no}/attachments 上传照片、业绩文件或证据
POST /{task_no}/generations 生成推介材料
GET /{task_no}/compliance-checks 查看合规检查结果
GET /{task_no} 查询任务和已审核材料
POST /{task_no}/reviews 审核、驳回或要求修改
POST /{task_no}/deliveries 发送给投顾

创建任务和写入操作必须提供请求头:

Idempotency-Key: <唯一请求键>

6.3 权限与状态

主要权限:

  • promotion:write
  • promotion:read
  • promotion:review
  • promotion:deliver

材料任务状态包括:

draft
→ input_ready
→ generating
→ generated
→ pending_review
→ approved
→ sent

异常状态包括:

compliance_failed
rejected
failed

生成的文件默认保存到:

data/promotion_materials/

七、功能三:金融 NL2SQL

7.1 功能说明

NL2SQL 将自然语言问题转换为经过权限、业务域、表白名单和只读校验的 SQL。 业务 Agent 不直接连接数据库,而是通过公共只读工具调用:

业务 Agent
→ BaseAgent.call_tool
→ ToolExecutor
→ query_financial_data
→ 只读金融数据表

工具名称:

query_financial_data

权限码:

financial:nl2sql:read

7.2 支持范围

当前纳入查询范围的主要表:

sys_customer_assignment
fin_customer_profile
fin_risk_assessment
fin_product
fin_fee_rule
fin_market_price
fin_nav_history
fin_holding
fin_transaction
fin_sim_order
fin_sim_account
fin_cash_ledger
client_facing_content

支持:

  • 基金净值、行情和历史收益查询
  • 费率和费用查询
  • 客户风险测评和画像查询
  • 持仓、交易、订单、账户和资金流水查询
  • 历史范围查询
  • 最多跨三个业务域查询
  • 结果行数限制,默认 50,最大 200
  • SQL dry-run

不支持或会被拒绝:

  • 非白名单表
  • 非 SELECT 查询
  • 未通过客户数据范围校验的查询
  • 当前快照表的历史时点查询
  • 模糊且未确认查询范围、指标口径或时间条件的问题

7.3 调用示例

result = await self.call_tool(
    "query_financial_data",
    {
        "question": "查询基金代码 000001 最近 30 天的净值",
        "dry_run": False,
        "limit": 50,
    },
    intent="financial_query",
    context=context,
)

模糊问题会返回:

{
  "status": "need_confirmation",
  "message": "请确认查询范围、指标口径和时间条件。"
}

工具结果包含查询计划、SQL、参数、权限检查和执行摘要,并进入统一审计和运行持久化链路。

八、测试数据集

测试数据位于:

data/

当前数据集概况:

类型 数量 用途
.eml 13 场外邮件及附件识别
.csv / .xlsx 8 推介材料业绩数据
.png / .jpg 35 经理照片、宣传图和测试图片
.pptx 10 推介材料结构检查
.pdf 9 PDF 文件有效性检查

测试数据只用于本地测试和演示,不应直接作为生产业务数据导入。

九、测试与代码检查

9.1 运行完整测试

普通单元和契约测试:

python -m pytest tests/unit tests/contract -q -p no:cacheprovider

使用独立 MySQL 测试库并执行测试数据隔离:

python -m tools.run_tests_on_test_db tests -p no:cacheprovider

9.2 运行三项功能定向测试

python -m pytest `
  tests/integration/test_offsite_fund_api.py `
  tests/integration/test_offsite_nl2sql_fields.py `
  tests/integration/test_offsite_notification_send.py `
  tests/integration/test_promotion_material_api.py `
  tests/unit/service/test_suitability_service.py `
  tests/contract/test_financial_nl2sql_tool_contract.py `
  -q -p no:cacheprovider

9.3 代码质量检查

python -m ruff check app tests tools alembic
python -m mypy app
git diff --check

9.4 测试基线

最近一次项目回归记录:

关键三项功能定向测试:85 passed
全量自动化测试:559 passed, 1 skipped

Redis、真实 IMAP/OCR/DeepSeek/SMTP、Neo4j、Milvus 和浏览器 E2E 是否能执行, 取决于本机服务和凭据配置。外部服务关闭或不可用时,测试使用本地数据集、Mock 或 dry-run。

十、配置开关

场外邮件和外部识别:

OFFSITE_IMAP_ENABLED=false
OFFSITE_MAIL_WORKER_ENABLED=false
OFFSITE_OCR_ENABLED=false
OFFSITE_DEEPSEEK_ENABLED=false
OFFSITE_SMTP_ENABLED=false
OFFSITE_SMTP_DRY_RUN=true

产品推介材料:

PROMOTION_MATERIAL_STORAGE_DIR=data/promotion_materials
PROMOTION_PDF_ENABLED=false
PROMOTION_PDF_CONVERTER_PATH=

启用真实服务前,请先完成网络、凭据、权限、数据脱敏和回滚方案确认。

十一、安全与开发约束

  • 所有业务请求都应经过统一 JWT、RBAC、客户范围和审计链路。
  • 不在业务 Agent 中自行读取密钥、直接连接数据库或绕过 ToolExecutor。
  • 场外业务不能写入场内交易表。
  • 数据库结构只能通过 Alembic 迁移修改。
  • 不删除、重命名或复用已有表和字段;历史结构不足时新增字段、新表或兼容读写。
  • 所有写接口应设计幂等键、状态机和重复提交保护。
  • 真实外部服务默认关闭,测试优先使用本地数据、Mock 和 dry-run。

十二、相关文档

  • docs/MVC/MVC架构.md:项目架构和目录职责
  • docs/00-新数据库基线设计.md:数据库业务基线
  • docs/05-接口文档.md:接口权威说明
  • docs/14-Agent组员统一接入说明书.md:Agent 接入规范
  • docs/15-金融NL2SQL工具接入说明.md:NL2SQL 工具规范
  • product_promotion_to_do_list.md:推介材料模块开发记录
  • 测试报告/2026-09-12.md:三项功能数据集测试报告
S
Description
番茄炒蛋组
Readme
18 MiB
Languages
Python 99.9%