补齐客服转人工工单流:从"只能看"到"能推进"(基线状态机,不自行发明)
## 问题 `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:
+9
-4
@@ -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 项)
|
||||
|
||||
@@ -467,7 +467,7 @@
|
||||
| **角色与权限** | 看角色清单(角色名/代码/权限数/用户数/状态);输入一个用户 ID,看**服务端实际解析出来的**角色、权限、数据范围 | 只读 |
|
||||
| **配置与模型** | 平台里**唯一能改配置**的地方:新建发布版本 → 校验 → 审核 → 激活;管平台配置项(命名空间:工具白名单 `agent_tools`、记忆 `memory`、关系 `relationship`、运行时 `runtime`、行情 `fund_market`)与模型路由规则 | 读写 |
|
||||
| **审计记录** | 最近 20 条审计(动作、主体、入口、时间);没有"看敏感详情"的权限时,详情由服务端**脱敏** | 只读 |
|
||||
| **转人工工单** | 看客服转人工的工单与**二次脱敏**的会话摘要 | 只读 |
|
||||
| **转人工工单** | 看客服转人工的工单与**二次脱敏**的会话摘要;**并且能处置**:分配 → 接单 → 解决 → 关闭(未解决可取消),可按状态筛,每一步都写审计 | 读写 |
|
||||
| **画像候选** | 客户对话里提炼出的画像候选,批准后才进正式记忆(批准/驳回都要填意见) | 读写 |
|
||||
| **投顾复核** | 待审的投顾交付物(推荐方案 + 投资方案书)→ 审核通过 / 驳回 / 发布给客户 | 读写 |
|
||||
| **知识库** | 上传文档(faq / product / policy)→ 自动切分入库并投向量同步;**客服就是靠它作答**;也能让某份知识失效 | 读写 |
|
||||
@@ -707,3 +707,4 @@ python tools/portal_api_check.py # 接口契约体检(按
|
||||
| 投顾"已发布交付物"空态写"需管理员审核" | 文案没跟进"投顾可自助审核发布"这次改动 |
|
||||
| 管理员配置发布没有"驳回/回滚"按钮、模型端点页是只读的、审计页没有筛选框 | 后端接口都有,前端未接 |
|
||||
| 客户「资金流水」页顶部导航不高亮 | 导航配置里该页的 `active` 是空串 |
|
||||
| 转人工工单队列里一堆一模一样的"端到端冒烟"单子 | 冒烟脚本每跑一次建一张。**2026-09-14 起它自己会走完处置闭环收尾**(分配→接单→解决→关闭),不再堆积;历史遗留的那些可以手动处置掉 |
|
||||
|
||||
@@ -1890,8 +1890,13 @@ model_failure | system_busy | clarification
|
||||
| 编号 | 端点 | 用途 | 权限 |
|
||||
|---|---|---|---|
|
||||
| **A033** | `GET /api/v1/admin/audit-records` | 审计查询(`limit` 1–100 默认 20 + `cursor`) | `audit:read`(无 `audit:read-sensitive` 则 `detail` 被涂成 `{"redacted": true}`) |
|
||||
| — | `GET /api/v1/admin/customer-service/handover-tickets` | 客服待转人工队列(只读) | 管理员 |
|
||||
| — | `GET .../handover-tickets/{ticket_no}` | 单工单脱敏摘要 | 管理员 |
|
||||
| — | `GET /api/v1/admin/customer-service/handover-tickets` | 客服转人工队列(只读);**支持 `?status=` 六态筛选** | `handover:read` + 管理员 |
|
||||
| — | `GET .../handover-tickets/{ticket_no}` | 单工单脱敏摘要 + 流转进度 | 同上 |
|
||||
| **A049** | `POST .../handover-tickets/{ticket_no}/assignments` | 分配工单(`pending → assigned`),体 `{assignee_id}` | `handover:write` + 管理员;幂等 |
|
||||
| **A050** | `POST .../handover-tickets/{ticket_no}/acceptances` | 接单(`pending` 自助接管 / `assigned → processing`),无体 | 同上 |
|
||||
| **A051** | `POST .../handover-tickets/{ticket_no}/resolutions` | 解决(`processing → resolved`),体 `{resolution}` ≥2 字 | 同上 |
|
||||
| **A052** | `POST .../handover-tickets/{ticket_no}/closures` | 关闭(`resolved → closed`),体 `{note}` 可空 | 同上 |
|
||||
| **A053** | `POST .../handover-tickets/{ticket_no}/cancellations` | 取消(`pending\|assigned\|processing → cancelled`),体 `{reason}` ≥2 字 | 同上 |
|
||||
| **A039** | `GET /api/v1/admin/customer-profile-candidates` | 待处理画像候选(`limit` 默认 20) | `memory:candidate:review`(`admin=True`) |
|
||||
| **A040** | `POST /api/v1/admin/customer-profile-candidates/{candidate_id}/reviews` | 批准/驳回候选 | 同上;**成功 200** |
|
||||
| **A041** | `POST /api/v1/admin/advisor/asset-allocation-backtests` | 资产配置回测 | `asset-allocation:backtest`;**201**;幂等 |
|
||||
@@ -1908,6 +1913,32 @@ model_failure | system_busy | clarification
|
||||
|
||||
**A040 批准时会处理同键旧正式记忆**(`conflict_type="candidate_promoted"`)。
|
||||
|
||||
### 11.4b 转人工工单处置(A049–A053)—— 2026-09-14 补齐
|
||||
|
||||
状态机照基线 `docs/02-数据库建表设计.md` §7.2 与 `docs/03` §客服转人工(**没有自行发明**):
|
||||
|
||||
```text
|
||||
pending -> assigned -> processing -> resolved -> closed
|
||||
| | |
|
||||
`----------+-------------+-> cancelled
|
||||
```
|
||||
|
||||
| 项 | 口径 |
|
||||
|---|---|
|
||||
| **权限** | 读 `handover:read`(9046,原本就有);**处置要 `handover:write`(9069,本次新增)**,且两者都要求 `admin`/`super_admin` 角色 |
|
||||
| **幂等** | 五个端点**都要求 `Idempotency-Key`**,且幂等范围包含被折叠的路径参数(同一把键用在两张不同工单上算两次操作) |
|
||||
| **审计** | 每次流转写一条 `interaction_audit`:`handover.assigned` / `handover.accepted` / `handover.resolved` / `handover.closed` / `handover.cancelled`,`detail` 里带 `ticket_no`、`from_status`、`to_status` |
|
||||
| **并发** | 动作先 `SELECT ... FOR UPDATE` 锁工单再判状态:两个人同时点,后到的会看到已变化的状态并拿到 409 |
|
||||
| **非法流转** | **409**(平台唯一的 409 码是 `RUN_NOT_CANCELLABLE`,真正原因在 `message` 里,例如"当前状态为closed,只有 pending 的工单可以分配") |
|
||||
| **接单人不存在** | **422** `AGENT_INPUT_INVALID`(先查 `sys_user`,不让数据库外键以 500 冒出) |
|
||||
| **响应字段** | `ticket_no`、`status`、`priority`、`assigned_to`、`assigned_at`、`accepted_at`、`resolved_at`、`closed_at`、`resolution`、`updated_at`;**不含 `customer_id`/`session_id`** |
|
||||
| **两个刻意口径** | ① `accept` 允许从 `pending` 直接接管(同时把 `assigned_to` 记成接单人);② `cancel` **不写 `closed_at`**(该列属 `closed` 状态),取消原因写进 `resolution`("已取消:…") |
|
||||
|
||||
列表端点新增 `?status=`(六态之一,**与数据库 CHECK 逐字对齐**;传别的值直接 **422**,
|
||||
不会漏到数据库变成 500)。
|
||||
|
||||
---
|
||||
|
||||
### 11.5 RBAC 只读查询(A035–A038)
|
||||
|
||||
`app/api/controllers/rbac.py`,前缀 `/api/v1/admin`。**四个端点全部只读,且不写审计。**
|
||||
|
||||
Reference in New Issue
Block a user