# mp-service: 保险/合同详情 5 个接口梳理 + 合同接口返回值强类型化

> **服务** hl-mp-service（端口 8085）→ 透传 hl-order-service-v2（端口 8094）
> **PR**: wx/HL#1314 · **Issue**: wx/HL#1312
> **日期**: 2026-04-23
> **影响范围**: C 端「订单详情页」保险详情弹窗、合同详情、保单 PDF 下载

---

## ⚠️ 关键变化

合同相关 3 个接口（`GET /mp/contract/list` / `GET /mp/contract/{id}` / `GET /mp/contract/by-order/{orderId}`）的返回值类型从后端弱类型 `Map<String, Object>` 改为具名 VO（`MpContractVO` / `MpContractDetailVO`）。**JSON 结构与字段名均未改变**，Swagger 上能看到具名 schema 了，前端无需改动；本 changelog 重点是一次性把这 5 个接口契约梳理清楚。

---

## 一、接口清单

| # | 接口 | 方法 | 路径 | 返回类型 |
|---|------|------|------|----------|
| 1 | 订单详情聚合（含保险详情） | GET | `/mp/order/{orderId}/dashboard` | `MpOrderDashboardVO`（取 `.insurance` 字段） |
| 2 | 合同详情（按订单） | GET | `/mp/contract/by-order/{orderId}` | `MpContractVO` |
| 3 | 合同详情（按合同 ID） | GET | `/mp/contract/{id}` | `MpContractDetailVO` |
| 4 | 订单保单合并 PDF | GET | `/mp/insurance/policy-pdf/{orderId}` | `Map<String,Object>`（透传 `PolicyShareVO`） |
| 5 | 单张保单下载 | GET | `/mp/insurance/policy/{insuranceOrderId}/download` | `String`（Base64） |

- 全部需要登录，`userId` 由 mp-service 从 token 中取出后透传给 order-v2
- 仅 5 号需要 `insuranceOrderId`，该 ID 来自订单详情出行人的 `insurancePolicies[].insuranceOrderId`

---

## 二、接口详情

### 1. 保险详情 `GET /mp/order/{orderId}/dashboard`

保险详情没有独立接口，走订单详情聚合 `MpOrderDashboardVO`，取 `.insurance` 字段（`MpInsuranceDetailVO`）。未投保或查询失败时该字段为 `null`（降级，不阻断页面）。

#### 入参

| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `orderId` | Path | Long | ✅ | 订单 ID |

#### 出参 `Result<MpOrderDashboardVO>` — 聚合 VO 全字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `order` | `MpOrderDetailVO` | 订单主体（详见 2026-04-17 订单详情 changelog） |
| `preTripChecklist` | `MpPreTripChecklistVO` | 出发准备清单（降级时 null） |
| `files.contract` | `MpContractSummaryVO` | 合同摘要（无合同 null） |
| `files.invoice` | `MpInvoiceSummaryVO` | 发票摘要（未开票 null） |
| `files.insurancePolicyPdfUrl` | `String` | 保单合并 PDF 下载路径，固定为 `/mp/insurance/policy-pdf/{orderId}` |
| `insurance` | `MpInsuranceDetailVO` | **保险详情（本 changelog 重点字段）** |
| `refund` | `RefundVO` | 退款模块（仅取消/退款中状态有值） |

#### `insurance`（`MpInsuranceDetailVO`）字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `schemeId` | Long | 方案 ID |
| `schemeName` | String | 方案名称 |
| `description` | String | 方案描述 |
| `isOverseas` | Boolean | 是否境外 |
| `totalDays` | Integer | 适用行程天数 |
| `notice` | String | 保险告知状态：`INCLUDED` / `OPTIONAL` / `EXCLUDED` |
| `segments` | `List<CoverageSegment>` | 保障分段列表 |
| `segments[].segmentName` | String | 分段名称 |
| `segments[].dayOffsetStart` | Integer | 起始天 |
| `segments[].dayOffsetEnd` | Integer | 结束天（`-1` = 最后一天） |
| `segments[].productName` | String | 保险产品名称 |
| `segments[].planName` | String | 保险计划名称 |
| `policies` | `List<PolicyItem>` | 投保记录列表（未投保为空数组） |
| `policies[].insuranceOrderId` | Long | 保险订单 ID（用于接口 5 下载） |
| `policies[].policyNo` | String | 保单号 |
| `policies[].productName` | String | 保险产品名称 |
| `policies[].planName` | String | 计划名称 |
| `policies[].premium` | BigDecimal | 保费（元） |
| `policies[].insuredCount` | Integer | 被保人数 |
| `policies[].coverageStartDate` | LocalDate | 保障开始日期 |
| `policies[].coverageEndDate` | LocalDate | 保障结束日期 |
| `policies[].status` | String | 保险状态：`PENDING` / `INSURING` / `INSURED` / `FAILED`（`CANCELLED` 已过滤不返回） |
| `policies[].statusLabel` | String | 状态中文标签 |
| `policies[].insuredPersons` | `List<InsuredPerson>` | 被保人列表 |
| `insuredPersons[].name` | String | 姓名（明文，脱敏由前端处理） |
| `insuredPersons[].idCardType` | String | 证件类型：`ID_CARD` / `PASSPORT` |
| `insuredPersons[].idCardNo` | String | 证件号码（明文） |
| `insuredPersons[].birthday` | LocalDate | 出生日期 |
| `insuredPersons[].gender` | String | `MALE` / `FEMALE` |
| `insuredPersons[].phone` | String | 手机号（明文） |

---

### 2. 合同详情（按订单） `GET /mp/contract/by-order/{orderId}`

返回订单关联的**最新**有效合同（非作废），一般用于订单详情页的"合同"入口。

#### 入参

| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `orderId` | Path | Long | ✅ | 订单 ID |

#### 出参 `Result<MpContractVO>`

| 字段 | 类型 | 说明 |
|------|------|------|
| `contractId` | Long | 合同 ID |
| `orderId` | Long | 订单 ID |
| `templateCode` | String | 模板编码 |
| `templateName` | String | 模板名称 |
| `contractNumber` | String | 合同编号 |
| `platform` | String | 签约平台 |
| `contractType` | String | 字典 `contract_type`：`TOUR` / `INSURANCE` |
| `contractTypeLabel` | String | 合同类型标签 |
| `mode` | String | 字典 `contract_mode`：`ONLINE` / `OFFLINE` |
| `modeLabel` | String | 签约模式标签 |
| `status` | String | 字典 `contract_status` |
| `statusLabel` | String | 合同状态标签 |
| `signUrl` | String | 签署 URL |
| `qrCodeUrl` | String | 签署二维码 URL |
| `fileUrl` | String | 合同文件 URL |
| `agencyCode` | String | 旅行社编号 |
| `travelAgencyName` | String | 旅行社名称 |
| `destination` | String | 目的地 |
| `departureDate` | LocalDate | 出发日期 |
| `returnDate` | LocalDate | 返回日期 |
| `totalAmount` | BigDecimal | 合同总金额 |
| `touristCount` | Integer | 出行人数 |
| `contactName` | String | 联系人姓名（明文，脱敏由前端处理） |
| `contactPhone` | String | 联系人电话（明文，脱敏由前端处理） |
| `createTime` | LocalDateTime | 创建时间 |

无合同时 `data` 为 `null`。

---

### 3. 合同详情（按合同 ID） `GET /mp/contract/{id}`

按合同 ID 查完整详情，返回合同基本信息 + 出行人签署详情 + 状态变更日志。

#### 入参

| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | Path | Long | ✅ | 合同 ID |

#### 出参 `Result<MpContractDetailVO>`

`MpContractDetailVO` 继承 `MpContractVO`（字段见接口 2），额外字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| `supplementaryClause` | String | 补充约定内容 |
| `travelers` | `List<TravelerInfo>` | 出行人列表 |
| `travelers[].travelerId` | Long | 出行人 ID |
| `travelers[].name` | String | 姓名（明文） |
| `travelers[].idCardType` | String | `ID_CARD` / `PASSPORT` |
| `travelers[].idCardNo` | String | 证件号码（明文） |
| `travelers[].phone` | String | 手机号（明文） |
| `travelers[].isSigner` | Boolean | 是否签署人 |
| `statusLogs` | `List<MpContractStatusLogVO>` | 状态变更日志 |
| `statusLogs[].logId` | Long | 日志 ID |
| `statusLogs[].contractId` | Long | 合同 ID |
| `statusLogs[].oldStatus` | String | 旧状态 |
| `statusLogs[].newStatus` | String | 新状态 |
| `statusLogs[].source` | String | 变更来源：`CALLBACK` / `POLLING` / `MANUAL` |
| `statusLogs[].rawPayload` | String | 原始载荷（仅调试用） |
| `statusLogs[].createTime` | LocalDateTime | 创建时间 |

权限：合同 `userId` 必须与登录用户一致，否则 403。

---

### 4. 订单保单合并 PDF `GET /mp/insurance/policy-pdf/{orderId}`

合并订单下所有出行人的有效保单为一个 PDF，返回 Base64。

#### 入参

| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `orderId` | Path | Long | ✅ | 订单 ID |

#### 出参 `Result<Map<String,Object>>`（BFF 当前透传 order-v2 `PolicyShareVO`）

| 字段 | 类型 | 说明 |
|------|------|------|
| `pdfBase64` | String | 合并后的保单 PDF（Base64 编码） |
| `personName` | String | 出行人姓名（订单范围分享时一般为空/null） |
| `policyCount` | Integer | 成功合并的保单数量 |
| `failedCount` | Integer | 下载失败的保单数量 |

---

### 5. 单张保单下载 `GET /mp/insurance/policy/{insuranceOrderId}/download`

下载指定保险订单的单张保单 PDF，返回 Base64 字符串。`insuranceOrderId` 从接口 1 的 `insurance.policies[].insuranceOrderId` 取。

#### 入参

| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `insuranceOrderId` | Path | Long | ✅ | 保险订单 ID（非订单 ID） |

#### 出参 `Result<String>`

| 字段 | 类型 | 说明 |
|------|------|------|
| `data` | String | 保单 PDF（Base64 编码） |

---

## 三、响应示例

### 接口 2 `GET /mp/contract/by-order/{orderId}`

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "contractId": 9001,
    "orderId": 20439,
    "templateCode": "TOUR_STANDARD",
    "templateName": "旅游服务合同（标准版）",
    "contractNumber": "HL-2026-0423-0001",
    "platform": "fadada",
    "contractType": "TOUR",
    "contractTypeLabel": "旅游合同",
    "mode": "ONLINE",
    "modeLabel": "线上签署",
    "status": "SIGNED",
    "statusLabel": "已签署",
    "signUrl": "https://sign.fadada.com/xxx",
    "qrCodeUrl": "https://cdn.1814.love/qr-9001.png",
    "fileUrl": "https://cdn.1814.love/contract-9001.pdf",
    "agencyCode": "HULAL001",
    "travelAgencyName": "呼籁旅行",
    "destination": "呼伦贝尔",
    "departureDate": "2026-07-01",
    "returnDate": "2026-07-07",
    "totalAmount": "12600.00",
    "touristCount": 4,
    "contactName": "张三",
    "contactPhone": "13800138000",
    "createTime": "2026-04-20 10:30:00"
  },
  "success": true
}
```

### 接口 4 `GET /mp/insurance/policy-pdf/{orderId}`

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "pdfBase64": "JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeX...（截断）",
    "personName": null,
    "policyCount": 4,
    "failedCount": 0
  },
  "success": true
}
```

---

## 四、边界行为

- 未登录 → 401（网关拦截）
- 订单/合同不属于当前 `userId` → 403
- 订单无合同（接口 2）→ `data: null`
- 订单未投保（接口 1 的 `insurance` 字段）→ `insurance: null`，不阻断页面
- 保单 PDF 下载失败（接口 4）→ `failedCount > 0`，`pdfBase64` 仍返回已合并部分；下游整体失败时 BFF 返 `500`
- 合同服务整体不可用（接口 2/3）→ BFF 返 `500 合同服务不可用，请稍后重试`

---

## 五、不影响范围

- **仅影响**：C 端订单详情页的保险详情弹窗 / 合同详情 / 保单 PDF 下载入口
- **零影响**：
  - 下单、支付、退款、发票所有接口
  - 合同列表 `GET /mp/contract/list`（未列出，字段同接口 2 `MpContractVO`）
  - 管理后台合同/保险所有接口（不走 `/mp/` 前缀）
  - JSON 字段名与取值逻辑（本次仅后端 VO 强类型化）

---

## 六、相关历史 PR

| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| wx/HL#1314 | wx/HL#1312 | 合同接口返回值从 Map 改为强类型 VO（本 PR） | ✅ 最新 |
| 2026-04-17 `mp-order-detail-refactor` | - | 订单详情聚合 VO `MpOrderDashboardVO` | ✅ 有效 |

---

## 七、代码位置

- Controller
  - `hl-mp-service` · `com.hulalv.mp.controller.MpOrderController#getOrderDashboard`
  - `hl-mp-service` · `com.hulalv.mp.controller.MpContractController`
  - `hl-mp-service` · `com.hulalv.mp.controller.MpInsuranceController`
- VO
  - `hl-mp-service` · `com.hulalv.mp.vo.MpOrderDashboardVO`
  - `hl-mp-service` · `com.hulalv.mp.vo.MpInsuranceDetailVO`
  - `hl-mp-service` · `com.hulalv.mp.vo.MpContractVO` / `MpContractDetailVO` / `MpContractStatusLogVO`
- 下游
  - `hl-order-service-v2` · `com.hulalv.contract.controller.internal.InternalMpContractController`
  - `hl-order-service-v2` · `com.hulalv.insurance.controller.internal.InternalInsuranceController`
  - `hl-order-service-v2` · `com.hulalv.insurance.vo.PolicyShareVO`

---

## 八、相关文档

- 关联 Issue: [wx/HL#1312](https://git.1814.love:8443/wx/HL/issues/1312)
- 关联 PR: [wx/HL#1314](https://git.1814.love:8443/wx/HL/pulls/1314)
- 订单详情聚合 VO 参考: `changelogs/2026-04/2026-04-17_order-v2_mp-order-detail-refactor.md`
