# FSD m09 · 出行协助（trip）

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

---

> 本册覆盖代码域 `internal/domain/trip`（单表 `trip_record`，detail JSON 列承载酒店/交通字段差异）。全局规则见 [00-总则](/prd/fsd-medilink/00-%E6%80%BB%E5%88%99/) §2（trip 状态机 §2.2）。

## m09 功能节目录

| ID | 名称 | 路由/入口 |
|---|---|---|
| F9.01 | 酒店安排 | `POST /api/trips`（kind=HOTEL）、`POST /api/trips/:id/transition` |
| F9.02 | 交通安排 | `POST /api/trips`（kind=TRANSPORT）、`POST /api/trips/:id/transition` |
| F9.03 | 患者只读拉取 | `GET /pt/trips` |

---

#### F9.01 酒店安排

> 对应 SRS：F-TRIP-001 ｜ 实现落点：`internal/domain/trip/service.go:60`（Create）/`Transition`（tripTransitions 白名单）/`main.go:319-321`（路由） ｜ 操作入口：web 工单详情出行协助区「新增记录」（顾问）

##### 1 功能定义

顾问为工单创建酒店安排记录（推荐合作酒店/距离价格翻译服务说明/预订需求/状态记录）：`kind='HOTEL'`，字段差异进 `detail` JSON 列（服务端透传不校验内部结构——SRS §7「首期人工协助，系统仅做记录，不自动对接 OTA」）。状态机 PLANNED→BOOKED→COMPLETED/CANCELLED。

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

`trip:write`（CONSULTANT）；工单存在（防挂空单——Create 归属校验 404）。

##### 3 输入与校验

| 字段 | 类型 | 校验 |
|---|---|---|
| ticket_id | int64 | 工单存在性（404 ErrTicketNotFound） |
| kind | enum | HOTEL / TRANSPORT（值域见 entity； kind 白名单校验） |
| title | string | 记录标题 |
| scheduled_at | datetime 可空 | 预定时间 |
| detail | JSON | 明细对象（服务端不校验内部结构——零字段级查询 MVP） |
| note | string 可空 | 备注 |

JSONBytes 类型：Scan/Marshal 双实现（json.RawMessage 无 sql.Scanner——活体 SELECT * 即 500 的「双处登记」教训型修正，⚠ DIF-M8 ⑧）。

##### 4 处理流程

```mermaid
sequenceDiagram
    participant W as web 工单详情
    participant H as trip.Handler
    participant S as trip.Service
    participant DB as MySQL
    W->>H: POST /api/trips {ticket_id, kind=HOTEL, title, detail…}
    H->>S: Create
    S->>DB: SELECT 工单（不存在 → 404）
    S->>DB: WithTx: INSERT trip_record(PLANNED) + lifecycle(CREATED) + op_log
    H-->>W: 201 记录 JSON
    W->>H: POST /api/trips/:id/transition {to=BOOKED, reason}
    H->>S: Transition（白名单前置→status 谓词 CAS→双日志同事务）
    H-->>W: 200 / 409
```

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

记录 JSON（id/ticket_id/kind/title/status/scheduled_at/detail/note/version…——json tag snake_case，DIF-M8 ⑧ 整形）；患者端 /pt/trips 可见（F9.03）。

##### 6 状态流转

`PLANNED → {BOOKED, CANCELLED}`；`BOOKED → {COMPLETED, CANCELLED}`；CANCELLED/COMPLETED 终态不可迁（tripTransitions 白名单+守卫测试锁定）；lifecycle entity_type='trip'（无专属 trip_log 表——双日志轨兑现六原则，DIF-M7 ①）。

##### 7 边界与异常

| 场景 | 行为 |
|---|---|
| 工单不存在 | 404 |
| 白名单外流转（如 PLANNED→COMPLETED） | 409 ErrInvalidTripTransition |
| 并发流转 | 409（status 谓词 CAS） |
| OTA 对接 | 不做（SRS §7 明示人工协助——枚举与 detail 结构为未来演进留位） |

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

`trip:write` 创建与流转；`ticket:read` 工单聚合读（web 详情）；detail JSON 原生存储无注入面（JSONBytes 序列化边界）。

---

#### F9.02 交通安排

> 对应 SRS：F-TRIP-002 ｜ 实现落点：同 F9.01（`kind='TRANSPORT'` 同一 Create/Transition 路径） ｜ 操作入口：web 工单详情出行协助区「新增记录」

##### 1 功能定义

顾问创建交通安排记录（机场接送/就医期间交通/信息同步）：与 F9.01 完全同构，仅 `kind='TRANSPORT'`；detail JSON 承载航班号/接送点等字段差异。

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

同 F9.01（`trip:write` + 工单存在）。

##### 3 输入与校验

同 F9.01（kind 值域差异）。

##### 4 处理流程

同 F9.01 §4。

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

同 F9.01；工单时间线出行节点内嵌展示。

##### 6 状态流转

同 F9.01。

##### 7 边界与异常

同 F9.01（kind 与状态机正交——状态机不区分酒店/交通）。

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

同 F9.01。

---

#### F9.03 患者只读拉取（/pt/trips）

> 对应 SRS：F-TRIP（患者可见信息同步） ｜ 实现落点：`internal/domain/trip/service.go:113`（ListByPatient）/`main.go:322`（路由） ｜ 操作入口：app H5 `#/pages/progress` 出行信息区

##### 1 功能定义

患者拉取本人最新工单的出行协助记录（只读）：直查口径沿 DIF-M6 ⑤（patient_id→最新工单→ListByTicket）。**无 SSE 推送**（患者拉取式——DIF-M7 ⑦ 明确不做清单：SSE 注册表守卫不扩面）。

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

pt JWT；无工单返回空列表（非 404——进度页语义）。

##### 3 输入与校验

无入参（patient_id=claims.Sub）。

##### 4 处理流程

```mermaid
flowchart LR
    A["GET /pt/trips"] --> B["SELECT 最新工单 WHERE patient_id"]
    B -->|无工单| C["空列表（非 404）"]
    B -->|有| D["SELECT * FROM trip_record WHERE ticket_id ORDER BY id"]
    D --> E["记录数组（只读）"]
```

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

记录数组（同 F9.01 JSON 形态）。

##### 6 状态流转

只读；患者不可创建/流转出行记录。

##### 7 边界与异常

| 场景 | 行为 |
|---|---|
| 无工单 | 200 空列表 |
| SSE 实时性 | 无——打开进度页时拉取（拉取式口径） |
| 多工单 | 最新一单的记录（一人一单 MVP 语义） |

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

pt JWT + patient_id 直查（不走权限码）；记录全量可见（无字段级脱敏——出行信息非敏感 PII）。

---

反链：

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