FSD m05 · 工单与看板(ticket)
本册覆盖代码域
internal/domain/ticket的 B 端面(直传三步/注册链部分在 m04)。全局规则见 00-总则 §2(A 轴状态机 §2.2、AST 守卫 §2.3)。
m05 功能节目录
| ID | 名称 | 路由/入口 |
|---|---|---|
| F5.01 | 工单自动生成 | (F4.01 事务链内 CreateInTx) |
| F5.02 | 工单分配与流转 | POST /api/tickets/:id/assign(-doctor)、POST /api/tickets/:id/transition |
| F5.03 | 状态机 transition() 与三条回退边 | (F5.02 内核;守卫测试锁定) |
| F5.04 | 病历下载与版本组读 | GET /api/documents/:id/download、GET /api/documents/:id/versions |
| F5.05 | 运营看板(聚合+三超时+对账 tab) | GET /api/board |
F5.01 工单自动生成
对应 SRS:F-TICK-001 | 实现落点:
internal/domain/ticket/service.go:236(CreateInTx,F4.01 注册事务链内调用) | 操作入口:—(系统自动,随注册发生)
1 功能定义
注册事务内自动创建工单:INSERT(status=CREATED, care_stage=NONE)→ 事务内回填 ticket_no='T'+yyyyMMdd+LPAD(id,4,'0')(INSERT(’’)→UPDATE,非状态列豁免申报,uk 兜底唯一)→ lifecycle(‘CREATED’) + op_log(SYSTEM register) 同事务(迁移-日志同路径)。
2 触发条件与前置状态
F4.01 注册事务链;无人工触发路径(MVP 一人一单)。
3 输入与校验
patient_id / expect_city / expect_window / chief_complaint(来自注册表单);其余列哨兵默认(consultant_id=0/doctor_id=0/hospital_id=0/dept_id=0——M10 起hospital_id 在专家确认时回填,F6.01)。
4 处理流程
见 F4.01 §4 时序图开单段;CreateInTx 内部三步(INSERT → 回填 ticket_no → 双日志)。
5 输出与结果状态
ticket 行(CREATED/NONE/version=1)+ ticket_no 唯一编号;响应透出 {id, ticket_no}。
6 状态流转
→ CREATED(lifecycle event=‘CREATED’);后续 PENDING_ASSIGN 由 F5.02/F6.01 流转。
7 边界与异常
| 场景 | 行为 |
|---|---|
| 同日多单 | ticket_no 序号=LPAD(id)——id 自增无计数器状态(uk 兜底) |
| 回填失败 | 事务回滚(整链) |
| patient_id 关联 | 0 哨兵不可能(注册事务内已有 uid) |
8 权限与数据规则
系统触发(actor=SYSTEM);lifecycle ActorID=patient_id。
F5.02 工单分配与流转
对应 SRS:F-TICK-002 | 实现落点:
internal/domain/ticket/service_bo.go:171(Assign)/:216(Transition)/:244-268(NotifyTransition/pushTransition)/handler_bo.go:77-136| 操作入口:web/admin/tickets列表→详情(分配顾问/分配医生/流转按钮)
1 功能定义
顾问分配工单(给顾问改派/给医生)与手动流转状态;每次流转 Commit 后三 key SSE 同推(consultant:all 列表刷新 / ticket:<id> 详情 / s:<sessID> 患者流)。
2 触发条件与前置状态
- 分配/流转:
ticket:assign/ticket:transition(CONSULTANT 持有;DOCTOR 均无——只读+写预诊)。 - 流转前置:当前状态在白名单内(F5.03);reason 必填。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| POST /api/tickets/:id/assign | assignee_id | field 白名单(consultant_id/doctor_id 服务端选定,非客户端输入直达列名);version CAS |
| POST /api/tickets/:id/assign-doctor | assignee_id | 同上 |
| POST /api/tickets/:id/transition | to + reason | 白名单校验(ErrInvalidTransition 409);reason 必填 400;status 谓词 CAS |
4 处理流程
5 输出与结果状态
分配 204;流转 200 返回更新后工单;SSE ticket 事件 {id, ticket_no, status, care_stage, updated_at, version}(web/app 前端注册表消费——SSE 事件注册表守卫对齐)。
6 状态流转
A 轴全图见总则 §2.2;分配列(consultant_id/doctor_id)非状态机列——version CAS 申报豁免(沿 BindPatientInTx 惯例),lifecycle event=ASSIGN_CONSULTANT/ASSIGN_DOCTOR(from/to 填旧/新 assignee id 串,可回放)。
7 边界与异常
| 场景 | 行为 |
|---|---|
| 并发流转(from 漂移) | 409 ErrVersionConflict |
| 白名单外迁移 | 409 ErrInvalidTransition |
| 推送失败 | 仅 warn——SSE 是通知不是事实源,客户端重连 resync;三 key 相互独立不短路(M5 核查 A3:StaffKey 失败不得吞掉 TicketKey) |
| > ⚠ DIF-F5-1(观察项) | 分配不校验 assignee 存在性/角色:现状未校验(id 由前端用户列表提供,service_bo.go:171 仅 version CAS 写列)——登记 99-附录 B 观察项 |
8 权限与数据规则
ticket:assign/ticket:transition 路由级;分配与流转均双日志同事务(W4 补齐——M5 曾遗留分配无审计缺口)。
F5.03 状态机 transition() 与三条回退边
对应 SRS:F-TICK-002(状态机机制,SRS 只述 8 状态未述回退) | 实现落点:
internal/domain/ticket/pure.go:92(ticketTransitions)/:103(CanTransition)/service.go:267(TransitionInTx)/guard/ast_status_guard_test.go(AST 守卫) | 操作入口:—(机制层;实际触发见 F5.02/F6.02/F7.02)
1 功能定义
状态机唯一权威的代码化:白名单 map(8 状态 7 出边 + 3 回退边)→ CanTransition 纯函数 → TransitionInTx 封装(白名单前置→status 谓词 CAS→lifecycle+op_log 同事务)。三条回退边是设计补充(SRS 未定义):边 1 支付失败重谈(PENDING_PAYMENT→PLAN_CONFIRMING)、边 2 改方案(PENDING_DEPARTURE→PLAN_CONFIRMING)、边 3 医生拒绝重派(PREDIAGNOSING→PENDING_ASSIGN)。
2 触发条件与前置状态
一切 ticket.status 变更的唯一合法路径;绕过即 AST 守卫测试红(guard/ast_status_guard_test.go:状态列字符串字面量只准出现在白名单函数)。
3 输入与校验
(ticketID, from, to, reason, actorID, actorRole);from 漂移(并发)→ ErrVersionConflict。
4 处理流程
5 输出与结果状态
状态列已迁移、version+1、双日志落库;守卫测试断言 ticketTransitions 与 8 状态+3 回退边逐一对应(防静默增删)。
6 状态流转
即本节本体(总则 §2.2 A 轴图);回退边 1 由支付回调驱动(F7.02)、边 3 由医生拒绝驱动(F6.02)、边 2 手动触发权限待产品确认(tech-design §15 风险 3——UI 不出按钮,白名单内 API 可达)。
7 边界与异常
| 场景 | 行为 |
|---|---|
| COMPLETED 迁出 | 409(终态无出边) |
| 同状态自迁 | 409(白名单无自环) |
| AST 守卫边界 | 动态拼接 SQL 扫不到——守卫是兜底不是完备证明,拼接绕过须 review 把关(⚠ DIF-M5 ② 申报) |
8 权限与数据规则
reason 必填(关键操作审计纪律);payment/trip/user/hospital 族状态机同构封装(TransitionOrderInTx 等 8 白名单函数,见总则 §2.3-2)。
F5.04 病历下载与版本组读
对应 SRS:F-CARE-002(版本管理读闭环,就医前部分) | 实现落点:
internal/domain/ticket/service_bo.go:271(DocumentFile)/:307(PatientDocument)/:347(DocumentVersions)/handler_bo.go:137-180| 操作入口:web 工单详情文档列表(下载/版本);app 进度页文档下载
1 功能定义
B 端代理下载病历/阶段资料(服务端 blob 转发——<a> 带不了 Authorization)、查询同组版本历史;患者端下载走归属谓词双轨(已归属按 ticket.patient_id;未归属按绑定会话 gsid 锚)。
2 触发条件与前置状态
- B 端:
ticket:read;viewer 须为该工单顾问/医生/ADMIN(否则 403 ErrForbidden)。 - 患者端:pt JWT;归属谓词双轨(
PatientDocument)。
3 输入与校验
:docId path;未归属文档(ticket_id=0)B 端不可见(404——guest 所有权锚阶段)。
4 处理流程
5 输出与结果状态
文件 blob(Content-Type=实际 mime);版本端点返回同组全版本(version_no 降序;doc_group=0 历史散件返回单行自身)。
6 状态流转
无状态变更(读路径);版本替换的写路径在 F8.02。
7 边界与异常
| 场景 | 行为 |
|---|---|
| 未归属文档 B 端访问 | 404(不可见性——不暴露 guest 阶段产物) |
| 跨工单访问 | 403 |
| 患者未绑定会话查未归属文档 | 404(无法证明所有权) |
| 对象已删(GDPR 后) | 500/404 视存储实现(文档行已随抹除链处理) |
8 权限与数据规则
ticket:read(B 端)/ pt JWT 谓词(患者);≤20MB 内存载(MVP 量级);患者端下载 M8 兑现(DIF-M7 ⑦)。
F5.05 运营看板(聚合+三超时+对账 tab)
对应 SRS:F-ADMIN-003(工单监控:总览看板、超时提醒、搜索筛选) | 实现落点:
internal/domain/ticket/service_bo.go:415(Board)/pure.go:137-154(BoardThresholds/IsTimedOut)/main.go:150-154(config 注入) | 操作入口:web/admin/board运营看板页
1 功能定义
单端点被动展示聚合(裁决 8——无主动通知/定时器):status/care_stage 两条 GROUP BY 计数 + 三个等待态(PENDING_ASSIGN/PREDIAGNOSING/PENDING_PAYMENT)超时工单表(阈值 config board.* 三键注入,内存 IsTimedOut 纯函数过滤)。
2 触发条件与前置状态
ops:read(CONSULTANT+ADMIN);页面刷新触发(被动展示口径)。
3 输入与校验
无入参;阈值来自 config(timeout_pending_assign_hours / timeout_prediagnosing_hours / timeout_pending_payment_hours)。
4 处理流程
5 输出与结果状态
{status_counts: {CREATED: n,...}, stage_counts: {...}, timeout_tickets: [{id, ticket_no, status, care_stage, patient_name, updated_at, hours_in_status}]}。
6 状态流转
只读聚合;不建看板物化表(单日百级工单实时聚合足够——tech-design §5.1 裁决)。
7 边界与异常
| 场景 | 行为 |
|---|---|
| 超时阈值未配 | 零值阈值=永超时为假(IsTimedOut 语义——config 缺省值兜底) |
| 工单量大 | 等待态全量拉取内存过滤——MVP 量级充分,量级证明需要再物化 |
| 搜索筛选 | 列表页(F5.02 List)承担 status/city 筛选;看板只做总览 |
8 权限与数据规则
ops:read;对账 tab(支付残留单 GET /api/payments/pending + FAILED 邮件 GET /api/notifications/failed)在 m07/m10 各节,看板页聚合入口。