跳转到主要内容

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/assignassignee_idfield 白名单(consultant_id/doctor_id 服务端选定,非客户端输入直达列名);version CAS
POST /api/tickets/:id/assign-doctorassignee_id同上
POST /api/tickets/:id/transitionto + 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 各节,看板页聚合入口。