# FSD m06 · 预诊断与方案（diagnosis）

LLMS 索引： [llms.txt](/llms.txt)

---

> 本册覆盖代码域 `internal/domain/diagnosis`（无新表——医生输出落 `ticket` 列，状态变更走 `ticket.TransitionInTx` 唯一入口）。全局规则见 [00-总则](/prd/fsd-medilink/00-%E6%80%BB%E5%88%99/) §2。

## m06 功能节目录

| ID | 名称 | 路由/入口 |
|---|---|---|
| F6.01 | 医生工作台（预诊+专家选人 dept 锚） | `GET /api/diagnosis/pending`、`GET /api/diagnosis/experts`、`POST /api/diagnosis/:ticketId` |
| F6.02 | 拒绝与重派 | `POST /api/diagnosis/:ticketId/reject` |
| F6.03 | 患者方案确认 | `GET /pt/plan`、`POST /pt/plan/confirm` |

---

#### F6.01 医生工作台（预诊+专家选人 dept 锚）

> 对应 SRS：F-DIAG-001（专家选人为 M10 扩展，SRS 未细化） ｜ 实现落点：`internal/domain/diagnosis/service.go:47`（PendingList）/`:78`（Submit）/`:229`（ExpertOptions）/`main.go:259-262`（路由） ｜ 操作入口：web `/admin/diagnosis` 医生待诊页（列表→病历预览 Modal→填意见/选专家→提交）

##### 1 功能定义

医生查看 PREDIAGNOSING 工单待诊列表、在线预览病历（复用 F5.04 代理 blob 前端渲染）、按科室锚定查询 PUBLISHED 专家并选人（或手填）、提交预诊意见/方案/推荐科室/预估天数与费用。提交=写 ticket 列（version CAS）→ 流转 PLAN_CONFIRMING → 患者邮件（plan_ready）同事务 → Commit 后三 key SSE。

##### 2 触发条件与前置状态

- `diag:write`（仅 DOCTOR）；工单须处于 PREDIAGNOSING（白名单前置校验+事务内兜底双重）。
- 病历在线预览为纯前端（Modal 内 img/pdf 原生渲染，后端零改动——⚠ DIF-M6 ⑨）。

##### 3 输入与校验

| 字段 | 类型 | 校验 |
|---|---|---|
| prediagnosis_summary | string | 必填语义（医生意见） |
| plan | string | 治疗方案文本 |
| dept_id | int64 | 推荐科室 |
| expert_id | int64 | 0=手填/存量哨兵；>0 时事务内一查两用（校验归属+上架态，取「姓名（职称）」快照） |
| expert | string | 手填推荐专家（'' 允许；ExpertID>0 时被快照覆盖） |
| estimate_days | int | 预估停留天数（0 允许） |
| estimate_amount | float64 | 预估总费用（**0=未填哨兵 → 定金回落固定额**，`payment.DepositCents` 消费） |
| estimate_currency | string | 缺省服务端兜底 USD |

`GET /api/diagnosis/experts?dept_id=`：dept_id<=0 → 空列表；仅 `status='PUBLISHED'` 专家，`ORDER BY sort,id`。

##### 4 处理流程

```mermaid
sequenceDiagram
    participant DW as web 医生工作台
    participant H as diagnosis.Handler
    participant S as diagnosis.Service
    participant DB as MySQL
    participant OS as MinIO
    DW->>H: GET /api/diagnosis/pending
    H-->>DW: PREDIAGNOSING 工单列表（患者名/主诉/期望城市）
    DW->>H: GET /api/documents/:id/download（病历预览）
    H->>OS: store.Get → blob 代理
    DW->>H: GET /api/diagnosis/experts?dept_id=
    H-->>DW: PUBLISHED 专家选项（id/name/title/specialty）
    DW->>H: POST /api/diagnosis/:ticketId {意见, dept_id, expert_id, 预估…}
    H->>S: Submit
    alt expert_id>0
        S->>DB: SELECT 专家（404 ErrExpertNotFound / 归属或非 PUBLISHED 409）
        S->>S: ResolveExpertSnapshot 快照「姓名（职称）」；hospital_id 回填专家院区
    end
    S->>DB: WithTx: UPDATE ticket 列集（version CAS）→ TransitionInTx(PREDIAGNOSING→PLAN_CONFIRMING) → outbox(plan_ready)
    S->>S: Commit 后三 key SSE
    H-->>DW: 204
```

##### 5 输出与结果状态

待诊列表/专家选项 JSON；提交 204；患者收 plan_ready 邮件 + /pt/plan 可见方案（F6.03）。

##### 6 状态流转

`PREDIAGNOSING → PLAN_CONFIRMING`（A 轴正向边）；`ticket.hospital_id` 自 M4 建单以来**首次有写路径**（选专家=确认院区，工单已绑院且与专家归属不符 → 409——`ValidateExpertSelection` 纯函数）。

##### 7 边界与异常

| 场景 | 行为 |
|---|---|
| 专家不存在 | 404 `ErrExpertNotFound`（⚠ DIF-M10 ③ M10 核查 A3 收口：原 400 改 404 与 ErrOrderNotFound 同形，mapErr 同映） |
| 专家非 PUBLISHED / 归属院区与工单不符 | 409（ValidateExpertSelection） |
| 工单非 PREDIAGNOSING | 409 ErrInvalidTransition（白名单前置） |
| version 竞态 | 409（写列 CAS 命中 0 行） |
| 专家快照 | 不可变——专家改名/下架不回写历史方案（医疗记录语义，⚠ DIF-M10 ④） |
| estimate_amount=0 | 定金回落 config 固定额（M6 单测断言 200.0 兼容证明） |

##### 8 权限与数据规则

`diag:write`（DOCTOR 专属）；医生选人端点同挂 `diag:write`（14 码矩阵不扩——⚠ DIF-M10 ⑤ 权限面不蔓延）；dept 锚设计根据=ticket.hospital_id 预诊前恒 0 不可作 JOIN 锚（M10 核查 B3/B5）。

---

#### F6.02 拒绝与重派

> 对应 SRS：F-TICK-002（「医生确认/拒绝」，M6 裁决 4 补齐） ｜ 实现落点：`internal/domain/diagnosis/service.go:154`（Reject）/`main.go:262`（路由） ｜ 操作入口：web `/admin/diagnosis` 待诊详情「拒绝」按钮

##### 1 功能定义

医生拒绝预诊请求：状态机**回退边 3**（PREDIAGNOSING→PENDING_ASSIGN）+ INBOX 通知顾问重新分配（`doctor_rejected` 模板，同事务）。零写路径（拒绝只流转不写列）。

##### 2 触发条件与前置状态

`diag:write`；工单须 PREDIAGNOSING；reason 必填。

##### 3 输入与校验

| 字段 | 类型 | 校验 |
|---|---|---|
| reason | string | 必填（400）；拼入 op_log reason「医生拒绝预诊请求：+reason」 |

##### 4 处理流程

```mermaid
sequenceDiagram
    participant DW as web 医生工作台
    participant H as diagnosis.Handler
    participant S as diagnosis.Service
    participant DB as MySQL
    DW->>H: POST /api/diagnosis/:ticketId/reject {reason}
    H->>S: Reject
    S->>DB: WithTx: TransitionInTx(PREDIAGNOSING→PENDING_ASSIGN) + outbox(INBOX doctor_rejected → role:CONSULTANT)
    S->>S: Commit 后三 key SSE
    H-->>DW: 204
```

##### 5 输出与结果状态

204；顾问 INBOX 收到重派待办（payload={ticket_no, reason}）；工单回到待分配。

##### 6 状态流转

回退边 3（白名单内唯一由 DOCTOR 触发的回退边）；lifecycle from/to 记录回退轨迹。

##### 7 边界与异常

| 场景 | 行为 |
|---|---|
| 工单非 PREDIAGNOSING | 409（白名单前置） |
| reason 空 | 400 |
| version 竞态 | 409 |

##### 8 权限与数据规则

`diag:write`；INBOX recipient=`role:CONSULTANT`（角色谓词待办）。

---

#### F6.03 患者方案确认

> 对应 SRS：F-DIAG-002 ｜ 实现落点：`internal/domain/diagnosis/service.go:190`（ConfirmByPatient）/`:260`（PatientPlan）/`main.go:263-264`（路由） ｜ 操作入口：app H5 `#/pages/progress` 方案卡片（查看→确认）

##### 1 功能定义

患者在确认前查看方案（/pt/plan——F-DIAG-002「确认方案前看方案」语义闭环，DIF-M7 ⑤ 补齐）、确认后进入支付（PLAN_CONFIRMING→PENDING_PAYMENT，「进入支付」，支付本身 M6 接续）。

##### 2 触发条件与前置状态

pt JWT；本人最新工单（直查口径 `WHERE patient_id=? ORDER BY id DESC LIMIT 1`——越权不可能 claims 即身份，⚠ DIF-M6 ⑤）；确认须 PLAN_CONFIRMING。

##### 3 输入与校验

| 端点 | 输入 | 校验 |
|---|---|---|
| GET /pt/plan | 无 | 无工单 404；预诊前字段零值/nil（前端按 status 渲染） |
| POST /pt/plan/confirm | 无 | TransitionInTx 白名单（非 PLAN_CONFIRMING 409） |

##### 4 处理流程

```mermaid
sequenceDiagram
    participant A as app 方案页
    participant H as diagnosis.Handler
    participant S as diagnosis.Service
    participant DB as MySQL
    A->>H: GET /pt/plan
    H->>S: PatientPlan（直查本人最新工单）
    H-->>A: 200 {ticket_no, status, prediagnosis_summary, plan, expert, estimate_*}
    A->>H: POST /pt/plan/confirm
    H->>S: ConfirmByPatient
    S->>DB: WithTx: TransitionInTx(PLAN_CONFIRMING→PENDING_PAYMENT)
    S->>S: Commit 后三 key SSE
    H-->>A: 204（前端引导创建支付单 F7.01）
```

##### 5 输出与结果状态

方案视图 JSON（含专家快照/预估天数/费用/币种）；确认 204；SSE ticket 事件双端刷新。

##### 6 状态流转

`PLAN_CONFIRMING → PENDING_PAYMENT`（A 轴正向边；actor=PATIENT id，op_log reason「患者在线确认诊疗方案」）。

##### 7 边界与异常

| 场景 | 行为 |
|---|---|
| 无工单 | 404 |
| 非 PLAN_CONFIRMING（重复确认/未到阶段） | 409 |
| 回退边 1 触发后（支付失败重谈） | 工单回 PLAN_CONFIRMING，患者可再次确认 |

##### 8 权限与数据规则

pt JWT + patient_id 直查（不走权限码）；方案文本 prediagnosis_summary/plan 为 NULL 豁免列（「尚未发生」语义）。

---

反链：

- [功能规格说明书(FSD)](/prd/fsd-medilink/)
- [MediLink Global 功能规格说明书（FSD）v1.0 · 总则](/prd/fsd-medilink/00-%E6%80%BB%E5%88%99/)
