docs(changelog): #2586 合同/保险模块 TODO 接通 — 从占位 → 完整可用

v3 contract / insurance 三 PR 累计变更(#2590 + #2591 + #2603)。
前端关键变化 4 点:
1. 合同/保险全部接口从占位/抛错转为真实可用
2. POST /v3/admin/insurance/purchase 新增订单状态+支付状态双重校验
3. POST /v3/admin/order/{id}/transition CONFIRM 异步触发自动签约/投保
4. 出行人变更触发已签约订单作废重做(状态白名单过滤)

新增字段:ContractScheme.defaultDestination / defaultDepartureCity
新增错误码:540022 ORDER_NOT_CONFIRMED_FOR_INSURANCE / 510206 合同方案未配置模板
新增异步事件:ChecklistConfirmedEvent / TravelerChangedEvent

测试服真测通过:单测 1096 全绿 + 9443 网关 + 真 admin token + P0-1 正反例齐全。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot 2026-05-19 16:16:23 +08:00
父节点 b2618f97e8
当前提交 1e141e445c

查看文件

@ -0,0 +1,209 @@
# 合同/保险模块 TODO 接通 — 从占位 → 完整可用 (工单 #2586)
> **存放目录**: 二期(v3, `order-v3` 标签)→ `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8084 / 测试服 9443 网关 + `/v3/admin/...`)
> **PR**: #2590 (主体) + #2591 (CR 跟进) + #2603 (P2/P3 + Publisher 接入)
> **Issue**: #2586
> **日期**: 2026-05-19
> **影响范围**: 管理后台「合同」「保险」两大模块全部接口 + 「订单状态机 CONFIRM 转换」副作用
---
## ⚠️ 关键变化(前端必看 4 点)
### 1. 合同 / 保险模块从「占位返空 / 抛异常」转为「完整可用」(行为变化,非破坏性)
PR #2590 之前合同 / 保险服务的所有写接口都返 `UnsupportedOperationException` 占位,查询接口返空列表 + `log.warn TODO`。本次合并后**全部真实可用**
- `POST /v3/admin/contract/create-by-scheme` 合同按方案创建 — 真实创建并接入第三方合同平台12301 / 腾讯电子签)
- `POST /v3/admin/insurance/purchase` 手动投保 — 真实写 `insurance_order` + 调宝游 API
- `POST /v3/admin/insurance/auto-purchase` / `auto-purchase-by-scheme` 自动投保 — 真实链路
- `POST /v3/admin/contract/resend-sign-sms` 重发签署短信 — 真实做用户归属校验
- `POST /v3/admin/contract/void/{id}` 作废 — 真实状态流转
- 列表 / 详情 / scheme 列表 — 全部返真实数据
**前端可解除**任何针对这些接口的"待后端实现"占位提示,按真实响应处理。
### 2. `POST /v3/admin/insurance/purchase` 新增订单状态校验(业务规则收紧)
只允许以下组合的订单投保:
| 维度 | 允许值 |
|------|-------|
| `orderStatus` | `PENDING_DEPARTURE` / `PENDING_BALANCE` / `TRAVELLING` |
| `payStatus` | `DEPOSIT_PAID` / `FULLY_PAID` |
任一不符 → 返业务码 `540022` `ORDER_NOT_CONFIRMED_FOR_INSURANCE` 文案 `"请先确认订单后再配置保险(当前订单状态不允许投保)"`
**前端处理建议**
- 投保按钮的可点击态可加前置判断(不必依赖后端报错才提示)
- 若直接拦不到,把 540022 错误码处理成 toast 弹该文案即可
### 3. `POST /v3/admin/order/{id}/transition` event=CONFIRM 自动触发合同 / 保险创建(异步)
订单状态机 `CUSTOMIZING → PENDING_DEPARTURE` 的 CONFIRM 转换会**异步**触发:
- 自动创建合同(前提:`contract_scheme.status=ACTIVE``contract_template_code` 配置过)
- 自动投保(前提:`insurance_scheme.status=ACTIVE`
接口同步返回 `triggeredEvents: ["ASYNC_CONTRACT_GENERATE", "ASYNC_INSURANCE_ISSUE"]`,**真正写库发生在事务 commit 后异步线程**`@TransactionalEventListener(AFTER_COMMIT)`)。
**前端表现**
- CONFIRM 转换 200 返回后,不要立刻显示「合同已生成 / 保险已购买」(异步还没跑完)
- 建议 transition 200 后 toast 提示「订单已确认,合同/保险将在 1 分钟内自动生成」,然后等用户主动刷新合同列表 / 保险列表看到记录
- 自动签约失败时合同/保险域有内部紧急待办 + 通知中心推送(通知中心走 NotificationDispatcher,渠道在 `notification_event_config` 配)
### 4. 出行人变更触发合同 / 保险作废+重做(已签约订单)
`TravelerService` 的 5 个写方法(`batchEdit` / `add` / `delete` / `addByInternal` / `updateByInternal`)末尾按状态白名单异步发布 `TravelerChangedEvent`
| 订单 orderStatus | 是否触发作废重做 |
|---|---|
| `PENDING_DEPARTURE` / `PENDING_BALANCE` / `TRAVELLING` | ✅ 触发:作废旧合同 + 重新签约;作废旧保单 + 重投 |
| `CUSTOMIZING` / `PENDING_PAY` / `PENDING_COMPLETE` 等未签约 | ❌ 不触发(避免事件风暴) |
| `REVIEWING` / `SETTLED` / `CANCELLED` | ❌ 不触发 |
**前端表现**:已签约订单编辑出行人后,合同 / 保险记录会自动作废并重新生成(异步),用户应感知到合同 PDF / 保单号变化。建议在出行人编辑成功后给一个 toast「已签约订单编辑出行人将触发合同/保险重新生成」。
---
## 一、背景
订单 v3 的 contract / insurance 域骨架在 yst 早期 PR 已从 v2 搬运,但接通订单核心域(`OrderInfoMapper` / `OrderTravelerMapper`)的位置全部以 `TODO(PR-order-core)` 占位,方法体抛 `UnsupportedOperationException` 或返 `Collections.emptyList()`
PR #2552-2554 出行人三连完成后,`OrderInfoMapper` / `OrderTravelerMapper` 已在 v3 落位,本工单收尾 — 把 contract / insurance 完整对接、激活 publisher、补全单测、修复 CR 三轮深审找出的 3 P0 + 5 P1 + 10 P2/P3 + 3 P3-NEW。
---
## 二、变更接口清单
### 合同模块admin
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|---------|
| 1 | 合同方案列表(启用) | GET | `/v3/admin/contract/scheme/list` | 行为变化(返真实数据) |
| 2 | 合同列表 | GET | `/v3/admin/contract/list` | 行为变化 |
| 3 | 按方案创建合同 | POST | `/v3/admin/contract/create-by-scheme` | 行为变化(真实写库 + 第三方调用) |
| 4 | 重发签署短信 | POST | `/v3/admin/contract/resend-sign-sms` | 行为变化(接入用户归属校验) |
| 5 | 作废合同 | POST | `/v3/admin/contract/void/{contractId}` | 行为变化 |
| 6 | 合同方案 CRUD | POST | `/v3/admin/contract/scheme/*` | 新增 `defaultDestination` / `defaultDepartureCity` 字段 |
### 合同模块mp 内部)
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|---------|
| 7 | 用户合同列表 | GET | `/internal/mp/contract/list` | 行为变化(按 userId 反查 orderIds |
| 8 | 用户合同详情 | GET | `/internal/mp/contract/detail/{contractId}` | 行为变化(接入归属校验) |
### 保险模块admin
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|---------|
| 9 | 保险方案列表(启用) | GET | `/v3/admin/insurance/scheme/list` | 行为变化 |
| 10 | 保单列表 | GET | `/v3/admin/insurance/orders` | 行为变化 |
| 11 | 手动投保 | POST | `/v3/admin/insurance/purchase` | 行为变化 + **状态校验新增** |
| 12 | 自动投保 | POST | `/v3/admin/insurance/auto-purchase` | 行为变化 |
| 13 | 按方案自动投保 | POST | `/v3/admin/insurance/auto-purchase-by-scheme` | 行为变化 |
| 14 | 取消保险 | POST | `/v3/admin/insurance/cancel` | 行为变化 |
| 15 | 投保预览 | POST | `/v3/admin/insurance/preview-apply` | 行为变化 |
### 订单状态机副作用
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|---------|
| 16 | 订单状态流转 | POST | `/v3/admin/order/{id}/transition` | **CONFIRM 事件副作用扩展**:异步触发自动签约/投保 |
| 17 | 出行人 batchEdit | POST | `/v3/admin/order/{id}/traveler/batch-edit` | 已签约阶段触发作废重做 |
| 18 | 出行人 add | POST | `/v3/admin/order/{id}/traveler/add` | 同上 |
| 19 | 出行人 delete | POST | `/v3/admin/order/{id}/traveler/delete/{travelerId}` | 同上 |
---
## 三、字段变更(合同方案)
`ContractScheme` 实体新增 2 个字段(对应表 `contract_scheme`
| 字段名 | 类型 | 含义 | 业务规则 |
|--------|------|------|---------|
| `defaultDestination` | varchar(64) NULL | 方案默认目的地 | createByScheme 时取值优先级scheme.defaultDestination → productName 关键字提取(呼伦贝尔/阿尔山/新疆/内蒙古/青海/西藏等) → 全局兜底「内蒙古呼伦贝尔」 |
| `defaultDepartureCity` | varchar(64) NULL | 方案默认出发地 | 同上,全局兜底「呼和浩特」 |
**前端影响**admin 合同方案管理表单加 2 个文本输入框(选填)。
---
## 四、错误码新增
| 业务码 | 文案 | 触发条件 |
|--------|------|---------|
| `540022` | 请先确认订单后再配置保险(当前订单状态不允许投保) | 调 `/v3/admin/insurance/purchase` 时订单状态 / 支付状态不符合白名单 |
| `510206` | 合同方案未配置模板 | 调 `/v3/admin/contract/create-by-scheme``scheme.contractTemplateCode = null` |
其他既有错误码不变。
---
## 五、Publisher 异步链路
### `ChecklistConfirmedEvent`
- **发布点**`OrderService.transition()` 守卫 `from=CUSTOMIZING && to=PENDING_DEPARTURE && event=CONFIRM`
- **携带**`orderId / orderNo / operatorId / operatorType (ADMIN/SYSTEM)`
- **operatorId 来源**`AdminContextUtil.getAdminId()`(请求上下文)→ 无则 null + SYSTEM
- **监听者**`ContractEventListener` + `InsuranceEventListener` 都接 `@TransactionalEventListener(AFTER_COMMIT, fallbackExecution=true)`
- **下游行为**listener 走 `contract.createByScheme` / `insurance.autoPurchase`,失败有紧急待办 + 通知中心降级通知
### `TravelerChangedEvent`
- **发布点**`TravelerService` 5 个写方法末尾
- **守卫**:仅在订单 `orderStatus ∈ {PENDING_DEPARTURE, PENDING_BALANCE, TRAVELLING}` 时发布
- **携带**`orderId / orderNo / changeType (ADD/MODIFY/REMOVE) / affectedTravelerIds`
- **下游行为**listener 走 `contract.invalidateByOrderId + createByScheme`(作废重签)、`insurance.cancelByOrderId + autoPurchase`(退旧重投)
---
## 六、数据库迁移
| 文件 | 内容 |
|------|------|
| `V20260519_004__create_order_todo.sql` | 新增 `order_todo` 表(订单内部待办,自动签约/投保失败时生成紧急待办) |
| `V20260519_005__contract_scheme_add_default_destination.sql` | `contract_scheme``default_destination` + `default_departure_city` 两列 |
---
## 七、测试服真测结论
### 部署
- 测试服 v3 已部署 dev-v3 最新 HEAD `af7dd70` 双实例 8086+8186
- Flyway 应用 V20260519_004 + V20260519_005 成功
- Nacos test namespace 注册健康,9443 网关路由通
### API 真测test_admin token 通过 trusted device 通路)
| 接口 | 结果 |
|------|------|
| GET 合同 scheme list / 合同 list / 保险 scheme list / 保单 list | ✅ 200 真实数据 |
| POST insurance.purchase CUSTOMIZING+UNPAID | ✅ 抛 540022 |
| POST insurance.purchase PENDING_DEPARTURE+UNPAID | ✅ 抛 540022**证明 pay_status 单独校验起作用** |
| POST insurance.purchase PENDING_DEPARTURE+DEPOSIT_PAID | ✅ 状态校验过,进入 planId 校验阶段 |
| POST order.transition CONFIRM | ✅ 200,triggeredEvents 含 ASYNC_CONTRACT_GENERATE + ASYNC_INSURANCE_ISSUE,listener 真收到(业务数据 scheme 缺所以 fail-soft |
### 单测
- `mvn -pl hl-order-service-v3 test`**Tests run 1096 / Failures 0 / Errors 0**(基线 1069 → +27
---
## 八、已知限制(非缺陷)
1. **测试服当前 active contract_scheme 都没填 `contract_template_code`**,所以 transition CONFIRM 异步自动签约会 short-circuit「无可用合同方案」并发紧急待办。需要在 admin 后台进 `/v3/admin/contract/scheme/*` 表单配置 `contract_template_code` 后才能完整测端到端。
2. **测试服当前没有 active `insurance_scheme`**,同上。
3. 自动签约时按 v3 当前 list 第一个 ACTIVE 方案挑选(暂无 mchId/productId 路由),后续多公司多产品场景需要在 `ContractScheme``mchId` / `productId` 字段后过滤。
---
## 九、关联
- 主 PR#2590 (https://git.1814.love:8443/wx/HL/pulls/2590)
- CR 跟进:#2591
- 最终零遗留:#2603
- 工单:#2586
---
**前端 mmg**:请同步检查投保 / 合同管理 / 出行人编辑 / 订单确认 4 个页面的 UI 提示逻辑,重点是「投保按钮置灰条件」+「订单确认后异步合同/保险生成提示」+「已签约后编辑出行人的告知 toast」。如有疑问随时找。