# 打印行程单接口（出团行程单·Driver Copy）

**接口路径**：`GET /v3/admin/order/{id}/print-itinerary`
**服务**：hl-order-service-v3
**PR**：[#4650](https://git.1814.love:8443/wx/HL/pulls/4650) ｜ **Issue**：[#4644](https://git.1814.love:8443/wx/HL/issues/4644) ｜ **合并至**：dev-v3
**变更类型**：✨ 新增接口

---

## 1. 接口背景

出团前，管理后台定制师（或运营人员）需打印一份完整的出团行程单（俗称"Driver Copy"）交给司机/导游/领队，供出行全程参考。本次在订单详情页新增该打印端点，后端聚合 **11 个功能区块**（大交通、客人信息、费用明细、全程住宿、应急联系人、每日行程、注意事项、退费规则、交接确认、代收款回执、备注）一次性返回，前端按 A4 纵向布局渲染并提供打印/PDF 下载入口。

同源先例：`GET /v3/admin/order/{id}/sign-voucher`（签单 PDF）。

---

## 2. 变更清单

| 类型 | 接口 | 说明 |
|------|------|------|
| ✨ 新增 | `GET /v3/admin/order/{id}/print-itinerary` | 打印行程单 11 区块全量聚合接口 |

---

## 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 请求）。

---

## 5. 出参字段

响应结构：`Result<PrintItineraryRespVO>`，`code=200` 时 `data` 为完整行程单对象。

### 5.1 根字段（header 区块 + 顶层区块引用）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `agencyName` | String | 本社名称，如「呼籁旅行有限公司」 |
| `printTime` | String | 打印时间，ISO-8601 格式，如 `2026-06-29T10:30:00` |
| `teamNo` | String | 团号，如 `HLA2026060001` |
| `productName` | String | 产品名 |
| `dateRange` | String | 出行日期范围，格式 `YYYY-MM-DD→YYYY-MM-DD` |
| `paxSummary` | String | 人数摘要，如 `2大1小` |
| `driverName` | String | 司机姓名；未配置则为空串 |
| `driverPhone` | String | 司机电话（内部明文，仅内网打印场景）；未配置则为空串 |
| `guideName` | String | 导游姓名；未配置则为空串 |
| `guidePhone` | String | 导游电话；未配置则为空串 |
| `leaderName` | String | 领队姓名；未配置则为空串 |
| `leaderPhone` | String | 领队电话；未配置则为空串 |
| `consultantName` | String | 定制师 / 经办人姓名 |
| `transports` | `TransportVO[]` | 01 区块：大交通批次列表，按方向升序（ARRIVAL 在前） |
| `customerOverview` | `CustomerOverviewVO` | 02 区块：客人信息速览 |
| `feeDetail` | `FeeDetailVO` 或 `null` | 03 区块：费用明细；product-v2 Feign 失败时为 `null`，前端应降级展示「暂无」 |
| `hotels` | `HotelVO[]` | 04 区块：全程住宿逐晚列表 |
| `emergencyContacts` | `EmergencyContactVO[]` | 05 区块：应急兜底联系人（配房房务 claimer） |
| `days` | `DayVO[]` | 06 区块：每日行程（来自固化行程表） |
| `notices` | `NoticesVO` | 07 区块：注意事项（固定文案，见 §8 示例） |
| `refundNotes` | `RefundNoteGroup[]` | 08 区块：退费规则（来自产品快照）；无退费说明时为空数组 |
| `handoverChecklist` | `String[]` | 09 区块：师傅领队交接确认 checklist（固定条目，前端在每项右侧留签字栏） |
| `collectReceipt` | `CollectReceiptVO` | 10 区块：代收款签收回执 |
| `remark` | String 或 `null` | 11 区块：备注（来自订单内部备注字段 `consultantRemark`） |

---

### 5.2 TransportVO（01 大交通批次）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `direction` | String | 方向枚举值：`ARRIVAL`（到达）/ `DEPARTURE`（出发） |
| `transportType` | String | 交通方式枚举值：`FLIGHT`（航班）/ `TRAIN`（火车）/ `SELF_DRIVE`（自驾） |
| `transportNo` | String | 航班号 / 车次号；`SELF_DRIVE` 时为空串 |
| `carrier` | String | 航司 / 铁路公司名称 |
| `departStation` | String | 出发站 / 机场全称 |
| `arriveStation` | String | 到达站 / 机场全称 |
| `departTime` | String（LocalDateTime） | 出发时间，格式 `YYYY-MM-DDTHH:mm:ss` |
| `arriveTime` | String（LocalDateTime） | 到达时间，格式 `YYYY-MM-DDTHH:mm:ss` |
| `selfDrivePeriod` | String 或 `null` | 自驾时段，仅 `SELF_DRIVE` 时有值：`MORNING` / `AFTERNOON` / `EVENING` |
| `selfDriveEta` | String（LocalDateTime）或 `null` | 自驾预计到达时间，仅 `SELF_DRIVE` 时有值 |
| `pickupRequired` | Boolean | 是否需要平台接送 |
| `pickupRemark` | String 或 `null` | 接送备注 |
| `remark` | String 或 `null` | 本段交通备注 |

---

### 5.3 CustomerOverviewVO（02 客人信息速览）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `customerName` | String | 主联系人姓名 |
| `customerPhone` | String | 主联系人手机（已脱敏，格式 `138****5678`） |
| `adultCount` | Integer | 成人数 |
| `childCount` | Integer | 儿童数（6–12 岁） |
| `youngChildCount` | Integer | 幼童数（2–6 岁） |
| `babyCount` | Integer | 婴儿数（0–2 岁） |
| `tripDays` | Integer | 行程天数 |
| `customerType` | String 或 `null` | 客人类型（运营字段，当前后端恒为 `null`，前端按空值处理） |
| `createSource` | String | 客源枚举值（见 §6 枚举表） |
| `customerRemark` | String 或 `null` | 客户备注（特殊需求 / 忌口 / 纪念日等） |

---

### 5.4 FeeDetailVO（03 费用明细）

**整体可为 `null`**（product-v2 Feign 失败时降级），前端应展示「暂无」而不报错。

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `items` | `FeeLineItemVO[]` | 费用行项列表（仅含数量 > 0 的人群类型） |
| `singleRoomSurcharge` | String（BigDecimal）或 `null` | 单房差加价；无单间需求时为 `null` |
| `grandTotal` | String（BigDecimal） | 合计金额 |
| `onsiteBalance` | String（BigDecimal）或 `null` | 代收款（现场实收尾款）；未录入时为 `null` |

**FeeLineItemVO（费用行项）**：

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `name` | String | 行项名称，如 `成人包价` / `儿童包价` / `幼童包价` / `婴儿包价` |
| `unitPrice` | String（BigDecimal） | 单价 |
| `qty` | Integer | 数量 |
| `subtotal` | String（BigDecimal） | 小计（= 单价 × 数量） |

> 注意：BigDecimal 字段均序列化为 **JSON String**（防 JS 精度丢失），前端渲染时直接使用，**不需要** `parseFloat()` 转换。

---

### 5.5 HotelVO（04 全程住宿，逐晚）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `dayNumber` | Integer | 天序号（1-based，第一晚 = 1） |
| `stayDate` | String（LocalDate） | 入住日期，格式 `YYYY-MM-DD` |
| `hotelName` | String | 酒店名称 |
| `city` | String | 所在城市（通过 resource-service Feign 取；Feign 失败时为空串） |
| `roomCategory` | String | 房型（中文，查字典 `room_category`；未命中则回退 `其他房型`） |
| `roomCount` | Integer | 房间数 |
| `contactPerson` | String | 酒店联系人（Feign 取；失败时为空串） |
| `contactPhone` | String | 联系电话（Feign 取；失败时为空串） |
| `settleType` | String | 结算方式（中文，查字典 `settle_type`；失败时为空串） |
| `paymentMode` | String | 付款方式（中文，查字典 `payment_mode`；失败时为空串） |
| `checkInTime` | String 或 `null` | 入住时间，格式 `HH:mm` |
| `checkOutTime` | String 或 `null` | 退房时间，格式 `HH:mm` |

---

### 5.6 EmergencyContactVO（05 应急兜底联系人）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `contactName` | String | 姓名（来自配房 claimer_name 快照） |
| `contactPhone` | String | 电话（通过 user-service Feign 取 admin_user.mobile；失败或未维护时为空串） |
| `role` | String | 角色说明，固定为 `房务` |

---

### 5.7 DayVO（06 每日行程）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `dayNumber` | Integer | 天序号（1-based） |
| `dayDate` | String（LocalDate） | 实际日期，格式 `YYYY-MM-DD` |
| `dayTitle` | String | 当日标题 |
| `description` | String 或 `null` | 当日描述 |
| `breakfast` | String 或 `null` | 早餐安排枚举值（见 §6） |
| `lunch` | String 或 `null` | 午餐安排枚举值 |
| `dinner` | String 或 `null` | 晚餐安排枚举值 |
| `diningRemark` | String 或 `null` | 餐饮备注 |
| `gatherPlace` | String 或 `null` | 集合地点（POI JSON 字符串，前端按需解析展示名称） |
| `dismissalPlace` | String 或 `null` | 解散地点（POI JSON 字符串） |
| `dailyMileage` | String（BigDecimal）或 `null` | 当日里程（km） |
| `nodes` | `NodeVO[]` | 当日点位列表，按 sort_order 升序 |

**NodeVO（06 子项：行程点位）**：

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `nodeName` | String | 点位名称（优先取 node 自定义名，回退资源名） |
| `nodeType` | String | 节点类型枚举值（见 §6） |
| `startTime` | String 或 `null` | 开始时间，格式 `HH:mm` |
| `timePeriod` | String 或 `null` | 时段（`上午` / `下午` / `全天`） |
| `supplierPhone` | String 或 `null` | 供应商电话；无则空串 |
| `remark` | String 或 `null` | 操作备注（作司机 / 导游话术） |

---

### 5.8 NoticesVO（07 注意事项）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `forbidden` | `String[]` | 严禁项目（固定文案，见 §8 典型示例） |
| `warning` | `String[]` | 警示项目（固定文案） |
| `standard` | `String[]` | 流程标准项目（固定文案） |

> 三组均为后端硬编码固定文案，每次响应内容相同，前端直接渲染即可。

---

### 5.9 RefundNoteGroup（08 退费规则）

`refundNotes` 为 `RefundNoteGroup[]`，每个元素对应一个行程节点的退费说明。

**RefundNoteGroup**：

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `sourceName` | String | 来源点位名称，如 `呼伦湖景区` |
| `intro` | String 或 `null` | 退费说明总体备注 |
| `items` | `RefundItem[]` | 退费明细列表 |

**RefundItem**：

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `title` | String | 展示标题，如 `成人未参加` |
| `amount` | String（BigDecimal） | 退费金额（赠送项目为 `0`） |
| `unitLabel` | String | 展示单位文案，如 `/人` / `/团` / `/辆` |
| `settleScope` | String | 结算粒度枚举值（见 §6） |
| `settleScopeLabel` | String | 结算粒度中文，如 `按人` |
| `remark` | String 或 `null` | 备注 |
| `effectiveFrom` | String（LocalDate）或 `null` | 规则生效起日；`null` 表示无限制 |
| `effectiveTo` | String（LocalDate）或 `null` | 规则生效止日；`null` 表示无限制 |

---

### 5.10 handoverChecklist（09 师傅领队交接确认）

`String[]`，后端返回以下 5 条固定条目，前端在每项右侧留签字栏：

1. `出行群已建立并拉入全部出行人`
2. `签单核对完毕（日期/人数/酒店/景点无误）`
3. `车辆及座位安排已确认`
4. `特殊需求已告知（忌口/纪念日/老幼残）`
5. `代收款金额已当面清点确认`

---

### 5.11 CollectReceiptVO（10 代收款签收回执）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `collectAmount` | String（BigDecimal）或 `null` | 代收金额（来自 `order_main.onsiteBalance`）；未录入时为 `null` |

---

## 6. 枚举 / 数据字典

### 6.1 direction（大交通方向）

| 枚举值 | 说明 |
|--------|------|
| `ARRIVAL` | 到达（进藏 / 进目的地） |
| `DEPARTURE` | 出发（离开目的地返程） |

### 6.2 transportType（交通方式）

| 枚举值 | 说明 |
|--------|------|
| `FLIGHT` | 航班 |
| `TRAIN` | 火车 |
| `SELF_DRIVE` | 自驾 |

### 6.3 selfDrivePeriod（自驾时段，仅 SELF_DRIVE）

| 枚举值 | 说明 |
|--------|------|
| `MORNING` | 上午 |
| `AFTERNOON` | 下午 |
| `EVENING` | 晚上 |

### 6.4 createSource（客源渠道）

| 枚举值 | 常见含义（展示名由字典 order_create_source 翻译） |
|--------|------------------------------------------------|
| `OTA` | 马蜂窝 / 携程等平台 |
| `CUSTOMER` | 老客户 / 回头客 |
| `B2B` | 同业 |
| `REFERRAL` | 转介绍 |
| `MINI_PROGRAM` | 小程序直询 |

> 字典值以实际 order_create_source 字典表为准，可能随运营配置增减。

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

| 枚举值 | 说明 |
|--------|------|
| `SCENIC` | 景区 |
| `RESTAURANT` | 餐厅 |
| `ACTIVITY` | 活动 |
| `SERVICE` | 服务项 |
| `CUSTOM` | 自定义 |

### 6.6 餐食安排（breakfast / lunch / dinner）

| 枚举值 | 说明 |
|--------|------|
| `HOTEL` | 酒店早餐 / 含在住宿中 |
| `CAMP` | 营地餐 |
| `SPECIAL` | 特色餐（安排饭店） |
| `SELF` | 自理 |

### 6.7 settleScope（退费结算粒度）

| 枚举值 | 说明 |
|--------|------|
| `PER_PERSON` | 按人结算（`/人`） |
| `PER_TEAM` | 按团结算（`/团`） |
| `PER_VEHICLE` | 按车结算（`/辆`） |

### 6.8 字典翻译字段

以下字段值来自数据字典，前端**不需要**自己翻译枚举，后端已翻译为中文：

| 字段 | 字典表 key | 说明 |
|------|-----------|------|
| `hotels[].roomCategory` | `room_category` | 房型中文名；未命中回退「其他房型」 |
| `hotels[].settleType` | `settle_type` | 结算方式中文 |
| `hotels[].paymentMode` | `payment_mode` | 付款方式中文 |

---

## 7. 错误码

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

> 其余 Feign 软依赖（product-v2 费用 / resource 城市 / user 应急电话）失败时**不报错**，对应字段降级为 `null` / 空串，不影响接口正常返回。

---

## 8. 示例

### 8.1 典型成功（完整 2 大 1 小 7 日游订单）

**请求**：

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

**响应**（节选关键区块，实际字段更多）：

```json
{
  "code": 200,
  "data": {
    "agencyName": "呼籁旅行有限公司",
    "printTime": "2026-06-30T09:15:00",
    "teamNo": "HLA2026070001",
    "productName": "呼伦贝尔7日私家定制游",
    "dateRange": "2026-07-01→2026-07-07",
    "paxSummary": "2大1小",
    "driverName": "全广存",
    "driverPhone": "18614805444",
    "guideName": "",
    "guidePhone": "",
    "leaderName": "扎西",
    "leaderPhone": "13812345678",
    "consultantName": "张定制",
    "transports": [
      {
        "direction": "ARRIVAL",
        "transportType": "FLIGHT",
        "transportNo": "CA4167",
        "carrier": "中国国航",
        "departStation": "北京首都国际机场T3",
        "arriveStation": "海拉尔东山机场",
        "departTime": "2026-07-01T08:00:00",
        "arriveTime": "2026-07-01T11:30:00",
        "selfDrivePeriod": null,
        "selfDriveEta": null,
        "pickupRequired": true,
        "pickupRemark": "到达T2出口等候",
        "remark": null
      }
    ],
    "customerOverview": {
      "customerName": "张三",
      "customerPhone": "138****5678",
      "adultCount": 2,
      "childCount": 1,
      "youngChildCount": 0,
      "babyCount": 0,
      "tripDays": 7,
      "customerType": null,
      "createSource": "CUSTOMER",
      "customerRemark": "小孩对海鲜过敏，全程忌口"
    },
    "feeDetail": {
      "items": [
        {"name": "成人包价", "unitPrice": "4500.00", "qty": 2, "subtotal": "9000.00"},
        {"name": "儿童包价", "unitPrice": "3000.00", "qty": 1, "subtotal": "3000.00"}
      ],
      "singleRoomSurcharge": null,
      "grandTotal": "12000.00",
      "onsiteBalance": "5000.00"
    },
    "hotels": [{
      "dayNumber": 1,
      "stayDate": "2026-07-01",
      "hotelName": "伯爵大酒店",
      "city": "海拉尔",
      "roomCategory": "标准双床间",
      "roomCount": 2,
      "contactPerson": "张经理",
      "contactPhone": "0470-12345678",
      "settleType": "签单",
      "paymentMode": "月结",
      "checkInTime": "14:00",
      "checkOutTime": "12:00"
    }],
    "emergencyContacts": [{"contactName": "李房务", "contactPhone": "13900000001", "role": "房务"}],
    "days": [{
      "dayNumber": 1,
      "dayDate": "2026-07-01",
      "dayTitle": "抵达海拉尔·入住休整",
      "description": "抵达海拉尔后接机，送至酒店休整",
      "breakfast": null,
      "lunch": "SELF",
      "dinner": "SPECIAL",
      "diningRemark": "晚餐安排烤全羊",
      "gatherPlace": null,
      "dismissalPlace": null,
      "dailyMileage": "45.00",
      "nodes": [{
        "nodeName": "海拉尔东山机场",
        "nodeType": "SERVICE",
        "startTime": "11:30",
        "timePeriod": "上午",
        "supplierPhone": "0470-88880001",
        "remark": "接机后直接送酒店，行李搬运到房间"
      }]
    }],
    "notices": {
      "forbidden": ["严禁向客人推荐自费项目、带客进购物店；违者扣除本行程全部车费，且永不录用"],
      "warning": ["送站时间不得早于出发时间前 2 小时", "景区半价票差价须由领队自行承担，不得转嫁客人", "行程如有任何变更须经书面确认后方可执行"],
      "standard": ["出发前须核实全程住宿安排（联系酒店确认房态）", "在出行群内发出行提示及完整大交通信息", "妥善保管签单，行程结束后当日归还公司", "每次入住后须检查房间设施并拍照留存", "司机出发前主动向客人自我介绍并说明全程安排"]
    },
    "refundNotes": [{
      "sourceName": "呼伦湖景区",
      "intro": "苔藓步道为赠送项目，不退费",
      "items": [{"title": "成人未参加", "amount": "44.00", "unitLabel": "/人", "settleScope": "PER_PERSON", "settleScopeLabel": "按人", "remark": null, "effectiveFrom": null, "effectiveTo": null}]
    }],
    "handoverChecklist": ["出行群已建立并拉入全部出行人", "签单核对完毕（日期/人数/酒店/景点无误）", "车辆及座位安排已确认", "特殊需求已告知（忌口/纪念日/老幼残）", "代收款金额已当面清点确认"],
    "collectReceipt": {"collectAmount": "5000.00"},
    "remark": "客人全程需要轮椅通道，联系景区提前安排"
  }
}
```

---

### 8.2 边界情况（人员未配置 + feeDetail 降级 + 无退费规则）

模拟 product-v2 Feign 失败时 `feeDetail` 返回 `null`，司机/导游/领队均未配置，`refundNotes` 为空数组：

```json
{
  "code": 200,
  "data": {
    "agencyName": "呼籁旅行有限公司",
    "printTime": "2026-06-30T09:16:00",
    "teamNo": "HLA2026070002",
    "productName": "张家界3日游",
    "dateRange": "2026-07-10→2026-07-12",
    "paxSummary": "1大",
    "driverName": "",
    "driverPhone": "",
    "guideName": "",
    "guidePhone": "",
    "leaderName": "",
    "leaderPhone": "",
    "consultantName": "李运营",
    "transports": [],
    "customerOverview": {
      "customerName": "王五",
      "customerPhone": "139****0000",
      "adultCount": 1,
      "childCount": 0,
      "youngChildCount": 0,
      "babyCount": 0,
      "tripDays": 3,
      "customerType": null,
      "createSource": "OTA",
      "customerRemark": null
    },
    "feeDetail": null,
    "hotels": [],
    "emergencyContacts": [],
    "days": [],
    "notices": {
      "forbidden": ["严禁向客人推荐自费项目、带客进购物店；违者扣除本行程全部车费，且永不录用"],
      "warning": ["送站时间不得早于出发时间前 2 小时", "景区半价票差价须由领队自行承担，不得转嫁客人", "行程如有任何变更须经书面确认后方可执行"],
      "standard": ["出发前须核实全程住宿安排（联系酒店确认房态）", "在出行群内发出行提示及完整大交通信息", "妥善保管签单，行程结束后当日归还公司", "每次入住后须检查房间设施并拍照留存", "司机出发前主动向客人自我介绍并说明全程安排"]
    },
    "refundNotes": [],
    "handoverChecklist": ["出行群已建立并拉入全部出行人", "签单核对完毕（日期/人数/酒店/景点无误）", "车辆及座位安排已确认", "特殊需求已告知（忌口/纪念日/老幼残）", "代收款金额已当面清点确认"],
    "collectReceipt": {"collectAmount": null},
    "remark": null
  }
}
```

> **前端处理建议**：
> - `feeDetail === null` 时，整个费用区块显示「暂无（系统获取失败）」
> - 人员字段（`driverName` 等）为空串时，对应展示行可隐藏或显示「-」
> - `collectReceipt.collectAmount === null` 时，代收款回执区块显示「代收款金额待确认」

---

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

**请求**：

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

**响应**：

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

---

## 9. 业务边界

### 适用场景

- 订单状态为任意状态均可查询（只读，不限状态）
- 出团前 1–3 天由定制师打印交出行人员
- 支持 A4 纵向排版，前端按 A4 规格渲染后提供打印 / PDF 导出

### 不适用场景

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

### 特殊边界

| 情况 | 行为 |
|------|------|
| 订单行程未固化（`days` 为空） | 正常返回，`days` 为空数组 |
| product-v2 Feign 超时 / 熔断 | `feeDetail` 降级为 `null`，接口正常 200 返回 |
| resource-service Feign 失败 | `hotels[].city` / `contactPerson` / `contactPhone` 降级为空串 |
| user-service Feign 失败 | `emergencyContacts[].contactPhone` 降级为空串 |
| 费用快照与订单当前金额不一致 | 以产品快照（`feeDetail`）为准，非实时联动；若前端需要实时金额，读订单金额字段 |

---

## 10. 修改前后对比

本次为**新增接口**，无历史版本，跳过此节。

---

## 11. 影响评估 / 回滚

本次为**新增接口**（纯加量），不影响任何已有接口。

- **前端是否需要同步上线**：是（仅新增打印入口，不强制）
- **回滚方案**：后端可随时下架接口，前端调用报 404 时隐藏打印按钮即可
- **破坏兼容性**：无

---

## 12. 注意事项

1. **BigDecimal 均为 String**：`grandTotal` / `unitPrice` / `subtotal` / `singleRoomSurcharge` / `onsiteBalance` / `dailyMileage` / `collectAmount` / `amount` 均序列化为 JSON String，前端**直接展示**即可，不需要 `parseFloat` 转换。

2. **feeDetail 可为 null**：费用区块来自 product-v2 Feign，不保证一定有值，前端**必须**做 null 判断，展示兜底文案「暂无」。

3. **电话号码三级降级**：
   - 司机/导游/领队电话：来自订单人员分配，未分配时为空串（不为 null）
   - 酒店联系人/电话：来自 resource-service Feign，失败时为空串
   - 应急联系人电话：来自 user-service Feign 取 admin_user.mobile，失败时为空串
   - 三种情况均不报错，前端按空串处理即可（显示「-」或隐藏）

4. **主联系人手机已脱敏**：`customerOverview.customerPhone` 格式为 `138****5678`，用于打印展示，安全无需二次处理。

5. **注意事项固定文案**：`notices` 三组内容为后端常量（`PrintItineraryConstants`），不走数据库，修改文案需后端发版。

6. **费用与实际金额可能不同步**：`feeDetail` 来自产品快照，当产品价格调整但订单快照未更新时，打印金额可能与最新报价不一致，属预期行为（以签单时快照为准）。

---

## 13. 关联 / 联系人

| 项 | 链接 / 信息 |
|----|------------|
| **Issue** | [#4644 打印行程单接口（出团行程单·Driver Copy）](https://git.1814.love:8443/wx/HL/issues/4644) |
| **PR** | [#4650 feat(order-v3): 打印行程单 11 区块聚合接口](https://git.1814.love:8443/wx/HL/pulls/4650) |
| **Merge Commit** | [8e2b02f56](https://git.1814.love:8443/wx/HL/commit/8e2b02f56) |
| **Feature Commit** | [187169daf](https://git.1814.love:8443/wx/HL/commit/187169daf) |
| **后端负责人** | yaosutu |
| **影响服务** | hl-order-service-v3（端口 8086） |
