# 打印行程单出参增强（notices 自描述 / 景点联系人 / 周几 / 餐布尔）

**接口路径**：GET /v3/admin/order/{id}/print-itinerary
**服务**：hl-order-service-v3
**PR**：[#4683](https://git.1814.love:8443/wx/HL/pulls/4683) | **Issue**：[#4682](https://git.1814.love:8443/wx/HL/issues/4682) | **首上线 PR**：[#4650](https://git.1814.love:8443/wx/HL/pulls/4650) | **合并至**：dev-v3
**变更类型**：破坏性变更（修改接口）

---

## 1. 接口背景

本次是对 PR #4650 首上线的打印行程单接口（出团行程单·Driver Copy）的出参增强，对照前端原型 printItinerary.jsx 补齐 5 项字段变更。**只改出参，入参不变，零 DDL。**

变更核心：
- notices（07 注意事项区块）由固定对象结构改为自描述数组，前端不再需要硬编码三档配色和图标。
- 餐食字段（breakfast / lunch / dinner）由枚举字符串改为布尔值，前端直接判断是否含餐。
- days[] 每日行程增加 week 字段（周几）。
- days[].nodes[] 每个点位增加 description（点位介绍）和 contactPerson / contactPhone（景点联系人，仅 SCENIC 类型有值）。

---

## 2. 变更清单

| 类型 | 字段路径 | 变更说明 |
|------|----------|----------|
| 破坏性变更 | notices | 类型从对象 {forbidden,warning,standard} 改为 NoticeGroupVO[] 自描述数组 |
| 破坏性变更 | days[].breakfast | 类型从 String 枚举改为 Boolean（true=含餐，false=不含） |
| 破坏性变更 | days[].lunch | 同上 |
| 破坏性变更 | days[].dinner | 同上 |
| 新增字段 | days[].week | 周几（由 dayDate 派生；dayDate 为 null 时 week=null） |
| 新增字段 | days[].nodes[].description | 点位介绍（景区等节点有值，可能 null） |
| 新增字段 | days[].nodes[].contactPerson | 景点联系人（仅 SCENIC；其余类型或未维护时为空串） |
| 新增字段 | days[].nodes[].contactPhone | 景点联系电话（同上规则） |

**入参无变化**，**无 DDL**，**无新依赖**。

---

## 3. 接口详情

| 项 | 说明 |
|----|------|
| **方法 + 路径** | GET /v3/admin/order/{id}/print-itinerary |
| **接口名** | 打印行程单（出团行程单·Driver Copy） |
| **功能描述** | 聚合订单 11 个打印区块，供前端渲染 A4 行程单并打印 / 导出 PDF |
| **认证** | 需携带管理后台 JWT（Authorization: Bearer <token>） |
| **权限** | 已登录的管理后台用户，无额外角色限制 |
| **幂等性** | 只读查询，天然幂等 |
| **限流** | 无特殊限流（走网关通用限流） |
| **响应格式** | application/json; charset=utf-8 |
| **包装类型** | Result<PrintItineraryRespVO> |

---

## 4. 接口入参

### 4.1 路径参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | Long（JSON 序列化为 String，防 JS 精度丢失） | 是 | 订单 ID（雪花 ID，前端应作为字符串透传，不要转 number） |

### 4.2 请求体

本接口无请求体（GET 请求）。入参与 PR #4650 首上线版本完全相同，本次未作任何修改。

---

## 5. 出参字段

响应结构：Result<PrintItineraryRespVO>，code=200 时 data 为完整行程单对象。以下列出**本次有变动的字段和结构**（包含字段完整表格），未变动字段（agencyName / transports / customerOverview / feeDetail / hotels / emergencyContacts / refundNotes / handoverChecklist / collectReceipt 等）与首上线 Changelog（changelogs-v2/2026-06/30_4644_打印行程单接口-新增接口-管理后台.md）保持一致，此处不重复列出。

### 5.1 notices（07 注意事项区块）—— 结构破坏性变更

**旧结构**（PR #4650，已废弃）：

```json
{
  "notices": {
    "forbidden": ["..."],
    "warning": ["...", "..."],
    "standard": ["...", "...", "..."]
  }
}
```

**新结构**（本次 PR #4683）：

```json
{
  "notices": [
    {
      "level": "FORBIDDEN",
      "title": "严禁事项 · 违者扣全部车费、永不录用",
      "color": "#DC2626",
      "icon": "🚫",
      "items": ["..."]
    }
  ]
}
```

NoticeGroupVO 每个元素字段：

| 字段名 | 类型 | 说明 |
|--------|------|------|
| level | String | 档位标识：FORBIDDEN / WARNING / STANDARD |
| title | String | 档位标题（含完整描述，可直接渲染为区块标题） |
| color | String | 档位主题色，格式 #RRGGBB |
| icon | String | 档位图标（emoji） |
| items | String[] | 本档说明条目列表 |

**三档固定配置**：

| level | title | color | icon | items 数量 |
|-------|-------|-------|------|-----------|
| FORBIDDEN | 严禁事项 · 违者扣全部车费、永不录用 | #DC2626 | 🚫 | 1 条 |
| WARNING | 警示事项 · 风险由领队、师傅承担 | #D97706 | ⚠️ | 3 条 |
| STANDARD | 流程标准 · 出团必做 | #16A34A | ✅ | 5 条 |

三档内容为后端常量，前端直接渲染 title / color / icon / items 即可，不需要再硬编码配色和图标。内容如需修改需后端发版。

---

### 5.2 days[]（06 每日行程）—— 新增 week，餐食改布尔

days[] 数组完整字段表：

| 字段名 | 类型 | 变更 | 说明 |
|--------|------|------|------|
| dayNumber | Integer | 无变化 | 天序号（1-based） |
| dayDate | String（LocalDate） | 无变化 | 实际日期，格式 YYYY-MM-DD |
| dayTitle | String | 无变化 | 当日标题 |
| week | String 或 null | **本次新增** | 周几（周一~周日；dayDate 为 null 时为 null） |
| description | String 或 null | 无变化 | 当日描述 |
| breakfast | **Boolean** | **类型变更** | true=含早餐（原 HOTEL/SPECIAL/CAMP），false=不含（原 NONE/SELF/null） |
| lunch | **Boolean** | **类型变更** | 同上 |
| dinner | **Boolean** | **类型变更** | 同上 |
| diningRemark | String 或 null | 无变化 | 餐饮备注 |
| gatherPlace | String 或 null | 无变化 | 集合地点（POI JSON 字符串） |
| dismissalPlace | String 或 null | 无变化 | 解散地点（POI JSON 字符串） |
| dailyMileage | String（BigDecimal）或 null | 无变化 | 当日里程（km） |
| nodes | NodeVO[] | 见 5.3 | 当日点位列表，按 sort_order 升序 |

---

### 5.3 days[].nodes[]（06 子项：行程点位）—— 新增 description / contactPerson / contactPhone

nodes[] 完整字段表：

| 字段名 | 类型 | 变更 | 说明 |
|--------|------|------|------|
| nodeName | String | 无变化 | 点位名称（优先取 node 自定义名，回退资源名） |
| nodeType | String | 无变化 | 节点类型枚举值（见 §6） |
| description | String 或 null | **本次新增** | 点位介绍（固化行程 node.description；景点节点通常有值，餐厅等可能 null） |
| startTime | String 或 null | 无变化 | 开始时间，格式 HH:mm |
| timePeriod | String 或 null | 无变化 | 时段（上午 / 下午 / 全天） |
| contactPerson | String | **本次新增** | 景点联系人（仅 nodeType=SCENIC 时通过 resourceId 调 resource-service 取；其余节点类型及未维护景点固定为空串 ""，不为 null） |
| contactPhone | String | **本次新增** | 景点联系电话（同 contactPerson 规则） |
| supplierPhone | String | 无变化 | 供应商电话（node.supplierPhone；无则空串）—— 与 contactPhone 是两个不同字段 |
| remark | String 或 null | 无变化 | 操作备注（作司机 / 导游话术） |

contactPhone vs supplierPhone 区别：
- contactPerson / contactPhone：景点资源的联系人/电话，来自 resource-service getSpotContacts 接口，仅 SCENIC 节点。
- supplierPhone：节点层面手动录入的供应商电话，所有节点类型均有此字段（无值时为空串）。两者独立，前端分开展示。

---

## 6. 枚举 / 数据字典

### 6.1 notices.level（注意事项档位）—— 本次新增

| 枚举值 | 中文含义 | 配色 | 图标 |
|--------|----------|------|------|
| FORBIDDEN | 严禁事项 | #DC2626（红） | 🚫 |
| WARNING | 警示事项 | #D97706（橙） | ⚠️ |
| STANDARD | 流程标准 | #16A34A（绿） | ✅ |

### 6.2 nodeType（行程点位类型）

| 枚举值 | 说明 | contactPerson/contactPhone 是否有值 |
|--------|------|-------------------------------------|
| SCENIC | 景区 | 有（若 resource-service 已维护该景点联系人） |
| RESTAURANT | 餐厅 | 固定空串  |
| ACTIVITY | 活动 | 固定空串  |
| SERVICE | 服务项 | 固定空串  |
| CUSTOM | 自定义 | 固定空串  |

### 6.3 breakfast / lunch / dinner（本次改为 Boolean）

| 旧枚举值（已废弃） | 旧含义 | 新 Boolean 值 |
|--------------------|--------|--------------|
| HOTEL | 酒店早餐 | true |
| SPECIAL | 特色餐 | true |
| CAMP | 营地餐 | true |
| NONE | 不含餐 | false |
| SELF | 自理 | false |
| null / 空 | 未录入 | false |

### 6.4 其余枚举（与首上线相同）

- direction：ARRIVAL（到达）/ DEPARTURE（出发）
- transportType：FLIGHT / TRAIN / SELF_DRIVE
- settleScope：PER_PERSON / PER_TEAM / PER_VEHICLE
- createSource：以字典 order_create_source 为准（OTA / CUSTOMER / B2B / REFERRAL / MINI_PROGRAM 等）

---

## 7. 错误码

与首上线版本相同，本次无新增错误码。

| 错误码 | HTTP 状态 | message | 触发场景 |
|--------|-----------|---------|----------|
| 581007 | 200（Result 业务码） | 订单不存在 | 路径参数 id 对应订单不存在或已软删除 |
| 401 | 401 | Unauthorized | JWT 未携带 / 已过期 |
| 403 | 403 | Forbidden | 无访问权限 |

resource-service getSpotContacts Feign 失败时不报错，contactPerson / contactPhone 降级为空串 ，不影响接口整体返回。

---

## 8. 示例

### 8.1 典型成功

**请求**：

```http
GET /v3/admin/order/1914050000000001/print-itinerary
Authorization: Bearer eyJhbGci...
```

**响应**（含本次全部新增字段）：

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "agencyName": "内蒙古呼籁国际旅行社有限公司",
    "printTime": "2026-07-01T10:00:00",
    "teamNo": "26-0554",
    "productName": "游牧的森林-短途版",
    "dateRange": "2026-07-01→2026-07-04",
    "paxSummary": "2大2小",
    "driverName": "扎西",
    "driverPhone": "13700137001",
    "guideName": "巴特尔",
    "guidePhone": "13700137002",
    "leaderName": "其其格",
    "leaderPhone": "13700137003",
    "consultantName": "王骁",
    "transports": [
      {
        "direction": "ARRIVAL",
        "transportType": "FLIGHT",
        "transportNo": "CA1234",
        "carrier": "中国国航",
        "departStation": "北京首都T2",
        "arriveStation": "海拉尔东山机场",
        "departTime": "2026-07-01T08:00:00",
        "arriveTime": "2026-07-01T10:30:00",
        "selfDrivePeriod": null,
        "selfDriveEta": null,
        "pickupRequired": true,
        "pickupRemark": "T2出口举牌",
        "remark": null
      }
    ],
    "customerOverview": {
      "customerName": "吕思远",
      "customerPhone": "138****0020",
      "adultCount": 2,
      "childCount": 1,
      "youngChildCount": 1,
      "babyCount": 1,
      "tripDays": 4,
      "customerType": null,
      "createSource": "CONSULTANT",
      "customerRemark": "忌海鲜"
    },
    "feeDetail": {
      "items": [
        {"name": "成人包价", "unitPrice": "3380.00", "qty": 2, "subtotal": "6760.00"},
        {"name": "儿童包价", "unitPrice": "1980.00", "qty": 1, "subtotal": "1980.00"}
      ],
      "singleRoomSurcharge": null,
      "grandTotal": "9540.00",
      "onsiteBalance": "3540.00"
    },
    "hotels": [
      {
        "dayNumber": 1,
        "stayDate": "2026-07-01",
        "hotelName": "海拉尔海棠酒店",
        "city": "呼伦贝尔市",
        "roomCategory": "标间",
        "roomCount": 4,
        "contactPerson": "海棠-李经理",
        "contactPhone": "0470-7777777",
        "settleType": "公司付款",
        "paymentMode": "月结",
        "checkInTime": "14:00",
        "checkOutTime": "12:00"
      }
    ],
    "emergencyContacts": [
      {"contactName": "admin", "contactPhone": "13000000001", "role": "房务"}
    ],
    "days": [
      {
        "dayNumber": 1,
        "dayDate": "2026-07-01",
        "dayTitle": "海拉尔接机",
        "week": "周三",
        "description": "接机入住，傍晚自由活动",
        "breakfast": true,
        "lunch": false,
        "dinner": false,
        "diningRemark": null,
        "gatherPlace": null,
        "dismissalPlace": null,
        "dailyMileage": "200.00",
        "nodes": [
          {
            "nodeName": "中俄边境公路（卡线）",
            "nodeType": "SCENIC",
            "description": "呼伦贝尔最美自驾线路，约200公里草原与边境风光",
            "startTime": null,
            "timePeriod": null,
            "contactPerson": "卡线-王警官",
            "contactPhone": "0470-8801234",
            "supplierPhone": "",
            "remark": "提前报备边防，携带身份证"
          },
          {
            "nodeName": "午餐（特色餐）",
            "nodeType": "RESTAURANT",
            "description": null,
            "startTime": "12:00",
            "timePeriod": "上午",
            "contactPerson": "",
            "contactPhone": "",
            "supplierPhone": "0470-88880099",
            "remark": null
          }
        ]
      }
    ],
    "notices": [
      {
        "level": "FORBIDDEN",
        "title": "严禁事项 · 违者扣全部车费、永不录用",
        "color": "#DC2626",
        "icon": "🚫",
        "items": ["严禁向客人推荐自费项目、带客进购物店；违者扣除本行程全部车费，且永不录用"]
      },
      {
        "level": "WARNING",
        "title": "警示事项 · 风险由领队、师傅承担",
        "color": "#D97706",
        "icon": "⚠️",
        "items": ["送站时间不得早于出发时间前 2 小时", "景区半价票差价须由领队自行承担，不得转嫁客人", "行程如有任何变更须经书面确认后方可执行"]
      },
      {
        "level": "STANDARD",
        "title": "流程标准 · 出团必做",
        "color": "#16A34A",
        "icon": "✅",
        "items": ["出发前须核实全程住宿安排", "在出行群内发出行提示及完整大交通信息", "妥善保管签单当日归还公司", "每次入住检查设施拍照留存", "司机出发主动自我介绍"]
      }
    ],
    "refundNotes": [
      {
        "sourceName": "黑山头",
        "intro": "退费说明",
        "items": [
          {
            "title": "单项未骑马",
            "amount": "100",
            "unitLabel": "/人",
            "settleScope": "PER_PERSON",
            "settleScopeLabel": "按人",
            "remark": null,
            "effectiveFrom": null,
            "effectiveTo": null
          }
        ]
      }
    ],
    "handoverChecklist": ["出行群已建立并拉入全部出行人", "签单核对完毕", "车辆及座位已确认", "特殊需求已告知", "代收款已清点"],
    "collectReceipt": {"collectAmount": "3540.00"},
    "remark": null
  },
  "success": true
}
```

---

### 8.2 边界情况（SCENIC 联系人降级为空串 + dayDate 为 null 时 week 为 null + 三餐均 false）

模拟 resource-service Feign 失败导致 contactPerson / contactPhone 降级为空串，以及 dayDate=null 时 week=null：

```json
{
  "code": 200,
  "data": {
    "days": [
      {
        "dayNumber": 1,
        "dayDate": null,
        "dayTitle": "待确认日期",
        "week": null,
        "description": null,
        "breakfast": false,
        "lunch": false,
        "dinner": false,
        "diningRemark": null,
        "gatherPlace": null,
        "dismissalPlace": null,
        "dailyMileage": null,
        "nodes": [
          {
            "nodeName": "呼伦湖景区",
            "nodeType": "SCENIC",
            "description": "呼伦湖，中国第五大湖",
            "startTime": null,
            "timePeriod": null,
            "contactPerson": "",
            "contactPhone": "",
            "supplierPhone": "",
            "remark": null
          }
        ]
      }
    ],
    "notices": [
      {"level": "FORBIDDEN", "title": "严禁事项 · 违者扣全部车费、永不录用", "color": "#DC2626", "icon": "🚫", "items": ["严禁向客人推荐自费项目、带客进购物店；违者扣除本行程全部车费，且永不录用"]},
      {"level": "WARNING", "title": "警示事项 · 风险由领队、师傅承担", "color": "#D97706", "icon": "⚠️", "items": ["送站时间不得早于出发时间前 2 小时", "景区半价票差价须由领队自行承担，不得转嫁客人", "行程如有任何变更须经书面确认后方可执行"]},
      {"level": "STANDARD", "title": "流程标准 · 出团必做", "color": "#16A34A", "icon": "✅", "items": ["出发前须核实全程住宿安排", "在出行群内发出行提示及完整大交通信息", "妥善保管签单当日归还公司", "每次入住检查设施拍照留存", "司机出发主动自我介绍"]}
    ]
  }
}
```

SCENIC 节点的 contactPerson / contactPhone 在 resource-service Feign 失败时降级为空串 "" 而不是 null，前端统一判断 === "" 决定是否显示联系人行。

---

### 8.3 业务失败（订单不存在）

**请求**：

```http
GET /v3/admin/order/9999999999999/print-itinerary
Authorization: Bearer eyJhbGci...
```

**响应**：

```json
{
  "code": 581007,
  "message": "订单不存在",
  "data": null
}
```

---

## 9. 业务边界

### 适用场景

- 订单任意状态均可查询（只读，不限状态）
- 出团前定制师打印行程单，交司机 / 导游 / 领队
- 本次增强后可直接用返回的 title / color / icon 渲染注意事项区块，无需前端硬编码配色

### 不适用场景

- 本接口不替代签单 PDF（GET /v3/admin/order/{id}/sign-voucher）
- 本接口不是小程序端行程查看接口，只给管理后台打印用

### 特殊边界

| 情况 | 行为 |
|------|------|
| dayDate 为 null | week 也为 null，前端按空值处理 |
| nodeType 非 SCENIC | contactPerson / contactPhone 固定为空串 ""（不是 null） |
| SCENIC 节点 resource-service Feign 失败 | contactPerson / contactPhone 降级为空串 ""，接口仍正常 200 返回 |
| 原 breakfast 为 null / SELF / NONE | 新字段值为 false |
| 原 breakfast 为 HOTEL / SPECIAL / CAMP | 新字段值为 true |
| nodes[].description 为 null | 该节点无介绍文字，前端按空值处理（隐藏介绍行） |
| feeDetail Feign 失败 | feeDetail 整体为 null，前端展示暂无（与首上线一致，本次无变化） |

---

## 10. 修改前后对比

### 字段级对比

| 字段路径 | 修改前（PR #4650） | 修改后（本次 PR #4683） |
|----------|-------------------|------------------------|
| notices | { "forbidden": [...], "warning": [...], "standard": [...] } | [{ "level": "FORBIDDEN", "title": "...", "color": "#DC2626", "icon": "🚫", "items": [...] }, ...] |
| days[].breakfast | "HOTEL" / "SPECIAL" / "NONE" / "SELF" / null | true 或 false |
| days[].lunch | 同上 | 同上 |
| days[].dinner | 同上 | 同上 |
| days[].week | 字段不存在 | "周三" 或 null |
| days[].nodes[].description | 字段不存在 | "点位介绍文字" 或 null |
| days[].nodes[].contactPerson | 字段不存在 | "景点联系人" 或 "" |
| days[].nodes[].contactPhone | 字段不存在 | "0470-12345678" 或 "" |

### 行为级对比

| 维度 | 修改前 | 修改后 |
|------|--------|--------|
| notices 渲染 | 前端硬编码三档颜色（红/橙/绿）和图标 | 直接使用返回的 color / icon / title，前端零硬编码 |
| 餐食展示 | 判断字符串枚举（=== "HOTEL" 等） | 直接判断布尔（=== true） |
| 景点联系人 | 无此数据，前端无法展示 | SCENIC 节点自动回填，可展示联系人和电话 |
| 每日行程周几 | 前端需自行从 dayDate 计算 | 后端直接返回 week，前端零计算 |
| 节点介绍 | 无此字段 | 有 description 可展示点位简介 |

---

## 11. 影响评估 / 回滚

### 破坏兼容性

| 字段 | 影响 | 前端必改 |
|------|------|---------|
| notices | 类型从对象改为数组，旧代码 notices.forbidden 将为 undefined | 是：改为遍历 notices 数组，使用各元素的 level / title / color / icon / items |
| breakfast / lunch / dinner | 类型从 String 改为 Boolean，旧代码字符串判断全部失效 | 是：改为布尔判断（=== true） |

### 前端需要同步上线

是，且为破坏性变更——不改前端会出现注意事项区块渲染报错以及餐食图标逻辑失效。

### 回滚方案

- 无 DDL，无数据库变更
- 后端回滚：Revert PR #4683，重新部署 hl-order-service-v3，接口恢复旧结构
- 前端兼容过渡期：可同时判断 Array.isArray(notices)（新结构）vs 非数组（旧结构），两套渲染保留直到后端确认全量上线

---

## 12. 注意事项

1. **notices 破坏性变更是本次最大风险**：旧代码直接访问 notices.forbidden / notices.warning / notices.standard 会报 undefined。前端上线后须全面验证注意事项区块渲染正常。

2. **餐食字段变为布尔，不再区分细类**：如需展示「酒店早餐」/ 「特色餐」等细类，本次接口不支持（只返回是/否含餐）。如有此需求请反馈，另行评估。

3. **contactPerson / contactPhone 返回空串而非 null**：景点联系人未维护时为 "" 而非 null，前端判断用 === "" 不要用 === null。

4. **contactPhone 与 supplierPhone 是两个独立字段**，来源不同，前端按需分别展示（如「景点电话」vs「供应商电话」）。

5. **week 字段 null 安全**：dayDate 有值时 week 一定有值；dayDate 为 null 时 week 一定为 null。

6. **BigDecimal 字段仍为 String**：grandTotal / unitPrice / subtotal / dailyMileage / collectAmount / amount 均序列化为 JSON String，前端直接展示，不要 parseFloat() 转换。

---

## 13. 关联 / 联系人

| 项 | 链接 / 信息 |
|----|------------|
| **Issue** | [#4682 打印行程单出参增强](https://git.1814.love:8443/wx/HL/issues/4682) |
| **PR（本次）** | [#4683 feat(order-v3): 打印行程单出参增强](https://git.1814.love:8443/wx/HL/pulls/4683) |
| **首上线 PR** | [#4650 feat(order-v3): 打印行程单 11 区块聚合接口](https://git.1814.love:8443/wx/HL/pulls/4650) |
| **首上线 Changelog** | changelogs-v2/2026-06/30_4644_打印行程单接口-新增接口-管理后台.md |
| **Merge Commit** | [602f0e68d](https://git.1814.love:8443/wx/HL/commit/602f0e68d) |
| **Feature Commit** | [2fcc42882](https://git.1814.love:8443/wx/HL/commit/2fcc42882) |
| **后端负责人** | yaosutu（腰苏图） |
| **影响服务** | hl-order-service-v3（端口 8086） |
