补齐客服转人工工单流:从"只能看"到"能推进"(基线状态机,不自行发明)

## 问题
`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 项)
This commit is contained in:
2026-09-15 00:41:59 +08:00
parent ed59e93b53
commit 076d786bc6
17 changed files with 1413 additions and 28 deletions
+9 -4
View File
@@ -99,7 +99,7 @@ python tools/e2e_smoke_test.py --read-only # 业务链路:登录→下单
python tools/portal_api_check.py # 接口契约:按前端的方式核对每个端点的状态码与字段
```
看最后一行:**40/40** 与 **41 项全通过**才开始演示(用例数会随开发增减,
看最后一行:**44/44** 与 **41 项全通过**才开始演示(用例数会随开发增减,
判断健康的准则是 **0 FAIL**,不是绝对条数)。有 FAIL 就按 §4 排查。
> 两者的分工:`e2e_smoke_test.py` 回答"**这条业务走得通吗**",
@@ -206,11 +206,16 @@ Invoke-RestMethod -Uri http://127.0.0.1:8000/api/v1/users/me/orders -Method Post
| | |
|---|---|
| **操作** | 客户页右下角浮窗 → 问「基金定投是什么」→ 点「转人工客服」 |
| **预期** | 先得到知识库答案;转人工返回 `202`,工单进入管理员队列 |
| **预期** | 先得到知识库答案;转人工返回 `202`,工单进入管理员队列(**新建的工单状态是 `pending`**) |
**这体现什么**:客户**主动**请求人工与"答不上来"是两条不同路径 —— 前者建工单,
后者按既定口径引导拨打客服热线(**不建工单**,符合我们"答不了就转人工、不硬答"的原则)。
> ✅ **这张工单现在可以在管理员工作台里被真正处置掉**(2026-09-14 补齐):
> 分配 → 接单 → 解决 → 关闭(未解决可取消),每一步都写审计。
> 演完场景 8 时顺手走一遍,就能把"客户请求人工 → 坐席接管 → 结案归档"讲完整。
> 状态机与权限见 §场景 8 的表格。
### 场景 5 · 风控预警处置(2 min)⭐ 重点
| | |
@@ -299,7 +304,7 @@ Invoke-RestMethod -Uri http://127.0.0.1:8000/api/v1/users/me/orders -Method Post
| 角色与权限 | 五个角色的权限数(**2026-09-14 实测**):admin **62** / advisor **31** / customer **26** / risk_operator **10** / operator **7**;还能按用户 ID 查"服务端实际解析出来的角色与数据范围" |
| 配置与模型 | 工具白名单、提示词**走发布状态机**(草稿 → 待审核 → 已审核 → 生效,可回滚;被取代的版本落 superseded),不是改配置文件;模型路由规则也在这里 |
| 审计记录 | 每一次权限判定、工具调用都有记录;没有"看敏感详情"的权限时,详情由服务端**脱敏** |
| 转人工工单 | 场景 4 建的工单在这里(**当前库里 40 条**);**不返回客户标识与原始对话**(二次脱敏) |
| 转人工工单 | 场景 4 建的工单在这里(**当前库里 40+ 张,多数是冒烟测试件**);只回**二次脱敏**摘要,不返回客户标识与原始对话。**可以处置**:分配 → 接单 → 解决 → 关闭(未解决可取消),按状态给按钮、可按状态筛 |
| 画像候选 | 客服从对话里提炼的画像候选,批准后才进正式记忆 |
| 投顾复核 | 待审的投顾交付物(推荐方案 + 投资方案书)→ 审核通过 / 驳回 / 发布给客户 |
| 知识库 | 上传文档(faq / product / policy)→ 自动切分入库并投向量同步;**客服就是靠它作答**(当前 active 知识 24 份、知识元数据 199 行) |
@@ -421,7 +426,7 @@ python tools/e2e_smoke_test.py
python tools/seed_demo_data.py # 准备演示数据(10 步,首次/换机器)
python tools/sync_market_prices.py # 刷行情(15 分钟有效期,下单 503 时补跑)
powershell -ExecutionPolicy Bypass -File start.ps1 # 起 API + Worker(自动刷行情)
python tools/e2e_smoke_test.py # 全量体检(6 条线 40 项)
python tools/e2e_smoke_test.py # 全量体检(6 条线 44 项,含工单处置闭环)
python tools/e2e_smoke_test.py --read-only # 只读体检,不动数据
python tools/check_today_profit_loss.py # 「今日盈亏」纯 SQL 复算并与接口逐只比对
python tools/portal_api_check.py # 接口契约体检(按前端的方式调,41 项)