docs(changelog): 团期合同保险面板与逐户出具 #7105
changelog-filename-gate / validate (push) Successful in 2s

新增面板读、手动开、作废重开、批量催签四个端点。

如实标注三条:
1. 出具依赖房/车/导/摄四项 ready,而 hotel_ready 线上无任何写入方(#4132),
   故面板 issuable 恒 false、三个写动作恒返 589548 —— 前端可先接,
   但 #4132 落地前看不到出具效果;面板读与催签不受限制,已实测可用。
2. 出具成功的正向路径未在网关侧实测(该状态在 TEST 造不出来),
   仅单测覆盖;589549 同样未在真实环境命中。
3. 上线必须先部署 hl-user-service 再部署 order-v3,否则写动作全返 589507。

门的口径以 #7248/PR#7249 修正后为准(判四项 ready,不判团期状态)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
jw
2026-09-07 14:34:51 +08:00
共同撰写人 Claude Opus 5
父节点 1c2156fbda
当前提交 347d5ec664
@@ -0,0 +1,471 @@
---
schema: "hl-changelog/v2"
ticket: "7105"
title: "团期合同保险面板与逐户出具"
consumer: "admin"
author: "jw(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "新增团期合同保险面板与逐户手动开、作废重开、批量催签四个端点。⚠️ 出具门依赖房/车/导/摄四项 ready,而其中 hotel_ready 目前线上无任何写入方(#4132 未完成),因此面板 issuable 恒为 false、三个写动作恒返 589548——前端可以先接,但在 #4132 落地前看不到出具效果。面板读与催签不受影响,已实测可用。"
updated_at: "2026-09-07"
base: "dev-v3"
---
# 团期「合同保险」面板:逐户出具、作废重开、批量催签
> **影响范围**:管理后台「团期详情 → 合同保险」Tab。
> 当前状态:后端已部署 TEST 并实测;前端待接入。
> **请先读下面的「当前不可用的部分」再排期。**
## ⚠️ 当前不可用的部分(重要)
出具的开闸条件是**房 / 车 / 导 / 摄四项资源全部就绪**。这四项里:
| 项 | 线上有没有写入方 |
| --- | --- |
| 导游 ready | ✅ 成团时若该团不需要导游即自动置位,需要时由人员配置置位 |
| 摄影 ready | ✅ 同上 |
| 车辆 ready | ✅ 车队服务整团配车完成后回填 |
| **酒店 ready** | ❌ **没有**。回填它的那条链路挂在一个从未被发出的事件上,见 HL#4132 |
结果:**现网任何团期的四项都集不齐**,所以
- 面板的 `issuable` **恒为 false**
- 手动开 / 作废重开 **恒返 589548**
- 团期单的合同与保险**不会自动出具**
**前端可以照本文档接入,但在 HL#4132 落地前,出具相关的按钮点下去只会拿到 589548。**
面板读取与批量催签**不受此限制**,现在就能用。
这一条是产品链路的既有缺口,不是本次交付的缺陷,但它决定了本功能何时真正可用,故写在最前面。
## 一、背景
此前团期没有合同保险的集中视图,管理员要逐户点进订单才能看到谁的合同出了、谁签了、谁的保险还没投;
开错了也只能逐户处理,催签只有单张接口。本次补齐面板与三个批量动作。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
| --- | --- | --- | --- | --- | --- |
| 1 | 合同保险面板 | GET | `/v3/admin/order/group-batch/{groupBatchId}/contracts` | 新增接口 | 汇总计数 + 逐户三态明细;权限码 `group-batch:view` |
| 2 | 手动开合同/保险 | POST | `/v3/admin/order/group-batch/{groupBatchId}/contracts/issue` | 新增接口 | 逐户补开;权限码 `group-batch:contract:issue` |
| 3 | 作废重开 | POST | `/v3/admin/order/group-batch/{groupBatchId}/contracts/reissue` | 新增接口 | 先作废后重开;权限码 `group-batch:contract:issue` |
| 4 | 批量催签合同 | POST | `/v3/admin/order/group-batch/{groupBatchId}/contracts/remind-sign` | 新增接口 | 只对已出未签的户发短信;权限码 `group-batch:contract:issue` |
## 三、接口详情
### 1. 合同保险面板 `GET /v3/admin/order/group-batch/{groupBatchId}/contracts`
**VO**: `GroupBatchContractBoardVO`
#### 使用场景
进入团期详情的「合同保险」Tab 时调用,一次拿到整团的汇总计数与逐户明细。
整页只发 3 次底层查询,不随户数放大。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- | --- |
| groupBatchId | path | Long | 是 | 正整数 | 团期 ID |
#### 出参
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| issuable | Boolean | 当前是否允许出具(四项已配齐)。**现网恒为 false,见文首说明** |
| batchStatus | String | 团期状态 |
| totalCount | Integer | 活跃子订单户数(已取消的户不计) |
| contractIssuedCount | Integer | 合同已出户数 |
| contractSignedCount | Integer | 合同已签户数 |
| insuranceIssuedCount | Integer | 保险已出户数 |
| contractWrongCount | Integer | 合同开错户数 |
| insuranceWrongCount | Integer | 保险开错户数 |
| items[].contractState | String | 三态:NOT_ISSUED / ISSUED / WRONG |
| items[].contractStateText | String | 三态中文名,服务端给出,前端直接显示 |
| items[].awaitingSign | Boolean | 已出未签,即催签的目标 |
| items[].contractUrl | String | 合同文件地址,未出为 null |
| items[].policyNo | String | 保单号,未出为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2096412454643802114/contracts
Authorization: Bearer <token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"batchId": "2096412454643802114",
"issuable": false,
"batchStatus": "RESOURCE_PREPARING",
"totalCount": 55,
"contractIssuedCount": 0,
"contractSignedCount": 0,
"insuranceIssuedCount": 0,
"contractWrongCount": 0,
"insuranceWrongCount": 0,
"items": [
{
"orderId": "2096633951715033089",
"orderNo": "HL20260907001741975",
"contactName": "蒋岚妍",
"contractState": "NOT_ISSUED",
"contractStateText": "未出",
"contractStatus": null,
"contractPlanName": null,
"contractSignedAt": null,
"contractUrl": null,
"awaitingSign": false,
"insuranceState": "NOT_ISSUED",
"insuranceStateText": "未出",
"insuranceStatus": null,
"insuranceProductName": null,
"policyNo": null
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
团期尚无子订单时 `totalCount` 为 0、`items` 为空数组,各计数器均为 0,页面展示空态。
#### 错误响应
```json
{
"code": 589507,
"message": "无操作权限(非团期管理员 / 非本定制师名下)",
"data": null,
"success": false
}
```
#### 业务边界
- **已取消的户不出现在面板里**,也不计入任何计数器。
- 三态不落库,全部由既有的合同状态与保险状态派生;后端给出中文名,前端不要自行映射。
- 读面板走只读权限,不需要出具权限。
### 2. 手动开合同/保险 `POST /v3/admin/order/group-batch/{groupBatchId}/contracts/issue`
**VO**: `GroupBatchContractIssueReqVO`
#### 使用场景
四项资源配齐后,为漏开的户补开合同与旅行意外险。正常情况下系统会自动出具,
本接口是补救通路。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- | --- |
| groupBatchId | path | Long | 是 | 正整数 | 团期 ID |
| target | body | String | 是 | CONTRACT / INSURANCE / BOTH | 本次要开什么 |
| orderIds | body | String[] | 否 | 子订单 ID 列表 | 不传表示整团所有活跃户 |
#### 出参
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| totalCount | Integer | 本次处理户数 |
| successCount | Integer | 成功户数 |
| failCount | Integer | 失败户数 |
| skipCount | Integer | 跳过户数(已出具无需重复开) |
| results[].outcome | String | SUCCESS / FAILED / SKIPPED |
| results[].message | String | 失败或跳过的原因,成功为 null |
#### 请求示例
```json
{
"target": "BOTH",
"orderIds": ["2096633951715033089", "2096633958908264449"]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"totalCount": 2,
"successCount": 1,
"failCount": 1,
"skipCount": 0,
"results": [
{ "orderId": "2096633951715033089", "target": "CONTRACT", "outcome": "SUCCESS", "message": null },
{ "orderId": "2096633958908264449", "target": "INSURANCE", "outcome": "FAILED", "message": "保险下单失败,请稍后重试" }
]
},
"success": true
}
```
#### 空数据 / 降级响应
指定的户都已出具时全部记 SKIPPED,`successCount` 为 0,属正常情况不是错误。
#### 错误响应
```json
{
"code": 589548,
"message": "房/车/导/摄四项配齐后才能出具合同与保险",
"data": null,
"success": false
}
```
#### 业务边界
- **逐户独立事务**:单户失败不回滚已成功的户,也不中断后续户,前端按 `results` 逐行展示。
- **四项未配齐整单拒绝**,返回 589548 且零写入——不是部分成功。
- 五秒内重复提交会被幂等窗口拦下,返回「出具处理中,请勿重复提交」。
- 同一团期的出具动作串行执行,并发点击不会重复开。
### 3. 作废重开 `POST /v3/admin/order/group-batch/{groupBatchId}/contracts/reissue`
**VO**: `GroupBatchContractIssueReqVO`
#### 使用场景
合同方案选错、保险投保信息有误等「开错」情形:先作废原件,再按同一条链路重新开具。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- | --- |
| groupBatchId | path | Long | 是 | 正整数 | 团期 ID |
| target | body | String | 是 | CONTRACT / INSURANCE / BOTH | 本次要重开什么 |
| orderIds | body | String[] | 否 | 子订单 ID 列表 | 不传表示整团所有活跃户 |
| reason | body | String | 否 | 最长 512 字 | 作废原因,写入操作记录 |
#### 出参
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| totalCount | Integer | 本次处理户数 |
| successCount | Integer | 作废并重开成功的户数 |
| failCount | Integer | 失败户数(含作废失败与重开失败,`message` 区分) |
| skipCount | Integer | 跳过户数(该户没有可作废的单据) |
| results[].orderId | String | 子订单 ID |
| results[].target | String | CONTRACT / INSURANCE |
| results[].outcome | String | SUCCESS / FAILED / SKIPPED |
| results[].message | String | 失败或跳过的原因,成功为 null |
#### 请求示例
```json
{
"target": "CONTRACT",
"orderIds": ["2096633951715033089"],
"reason": "合同方案选错"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"totalCount": 1,
"successCount": 1,
"failCount": 0,
"skipCount": 0,
"results": [
{ "orderId": "2096633951715033089", "target": "CONTRACT", "outcome": "SUCCESS", "message": null }
]
},
"success": true
}
```
#### 空数据 / 降级响应
指定的户没有可作废的单据时记 SKIPPED,不产生任何写入。
#### 错误响应
```json
{
"code": 589550,
"message": "无可用合同方案,无法出具合同",
"data": null,
"success": false
}
```
#### 业务边界
- **作废失败不进重开**:该户直接记 FAILED,绝不会出现「原件作废了但新件没开出来」的空档。
- 与「手动开」共用同一道四项配齐门,未配齐同样返 589548。
- 幂等窗口与串行锁同「手动开」。
### 4. 批量催签合同 `POST /v3/admin/order/group-batch/{groupBatchId}/contracts/remind-sign`
**VO**: `GroupBatchContractRemindReqVO`
#### 使用场景
合同已出但客户迟迟不签,管理员一次性给这些户重发签署短信。
**本接口不受四项配齐门限制,现在就能用。**
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- | --- |
| groupBatchId | path | Long | 是 | 正整数 | 团期 ID |
| orderIds | body | String[] | 否 | 子订单 ID 列表 | 不传表示整团所有活跃户 |
#### 出参
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| totalCount | Integer | 本次处理户数 |
| successCount | Integer | 实际发出短信的户数 |
| skipCount | Integer | 跳过户数 |
| results[].message | String | 跳过原因 |
#### 请求示例
```json
{
"orderIds": ["2096633951715033089", "2096633958908264449"]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"totalCount": 2,
"successCount": 0,
"failCount": 0,
"skipCount": 2,
"results": [
{ "orderId": "2096633951715033089", "target": "CONTRACT", "outcome": "SKIPPED", "message": "该户合同未出具或已签署,无需催签" },
{ "orderId": "2096633958908264449", "target": "CONTRACT", "outcome": "SKIPPED", "message": "该户合同未出具或已签署,无需催签" }
]
},
"success": true
}
```
#### 空数据 / 降级响应
无「已出未签」的户时全部 SKIPPED,`successCount` 为 0,不发任何短信。这是正常结果。
#### 错误响应
```json
{
"code": 589507,
"message": "无操作权限(非团期管理员 / 非本定制师名下)",
"data": null,
"success": false
}
```
#### 业务边界
- **只对「已出未签」的户发**:未出具的、已签署的一律跳过并说明原因。
- 单户发送失败不影响其他户,逐户记 FAILED。
- 只要真发出了至少一条,就写一条团期操作记录。
## 四、契约约束与正确调用方式
- **出具按钮的可用性以面板返回的 `issuable` 为准**,不要前端自行按团期状态推断。
- **三个写动作共用一个权限码** `group-batch:contract:issue`,读面板走 `group-batch:view`。
- `orderIds` 不传即整团,传了就只处理这几户;两种用法都返回逐户结果。
- **逐户结果必须逐行展示**:这三个接口都可能部分成功,只看 `code=200` 会误导操作者。
- 三态文案用后端返回的 `contractStateText` / `insuranceStateText`,不要前端自己映射枚举。
- 出具与重开有五秒幂等窗口,前端做防抖即可。
## 五、数据库行为
- 面板读取**零写入**。
- 手动开 / 重开**逐户独立事务**:每户单独提交,失败的户不回滚成功的户。
- 四项未配齐时在任何写操作之前拒绝,**零写入**。
- 重开时先作废后开具,作废失败即停在该户,不会留下「已作废未重开」的中间态。
- 出具、重开、催签各写一条团期操作记录;催签仅在真发出短信时才写。
- 无权限、幂等拦截、参数非法一律零写入。
## 六、边界行为
- 已取消的户不参与任何动作,也不出现在面板。
- 指定的户不属于本团期时按跳过处理,不整单失败。
- 同团期的出具动作串行,避免并发重复开具。
## 六.7、影响评估
- **自动出具链路新增了同一道门**:团期单在四项配齐前不再自动出具合同与保险,
未配齐时静默跳过(不建待办、不发失败通知)。散客单不受影响。
- **上线依赖部署顺序**:`group-batch:contract:issue` 权限码由用户服务的迁移注册并授予
管理员与超级管理员,**必须先部署用户服务再部署订单服务**,否则三个写动作除超级管理员外全返 589507。
- **本功能当前实际不可用**,原因见文首:酒店就绪标志线上无写入方(HL#4132)。
- 面板读与批量催签不受上述限制。
## 七、不影响范围
- 散客单(非团期单)的合同保险出具完全不变。
- 单张合同的出具、签署、作废接口不变,本次只加团期级的批量入口。
- 小程序端不受影响。
- 团期成团、名额调整、流团审批等其他动作不受影响。
## 八、测试环境已验证
TEST 环境已部署用户服务与订单服务并实测:
- 面板读取:55 户的团期返回 55 行逐户明细,计数器与 `issuable` 齐全。
- 空团期读面板:户数 0、明细空数组,不报错。
- 四项未配齐时手动开 → 589548,零写入。
- 四项未配齐时作废重开 → 589548,与手动开同一道门。
- 出具目标非法时同样先被四项配齐门拦下(门先于目标校验)。
- 批量催签指定两户 → 均跳过并给出「该户合同未出具或已签署,无需催签」,零副作用。
- 权限码已生效:读面板通过、三个写动作走到业务校验而非 589507,
说明权限已注册并授予管理员。
### 尚未验证
- **出具成功的正向路径未在网关侧实测**:需要四项配齐的团期,而酒店就绪标志线上无写入方(HL#4132),
该状态在测试环境造不出来。出具成功、逐户独立事务、作废失败不进重开等行为由单元测试覆盖(60 例全绿),
但**未经真实环境验证**,前端联调时请以实际返回为准。
- 出具目标非法的错误码 589549 因被四项配齐门先拦,同样未在真实环境命中。
## 十、相关文档
- 工单:HL#7105
- 合并:HL PR#7243
- 后续修正:HL#7248 / PR#7249——出具门原先按团期状态判定,与「合同全签才能进入物资准备」
这道既有门形成死锁,已改为直接判四项就绪标志。本文档描述的是修正后的口径。
- 阻塞项:HL#4132——团期基准变更事件未发出,导致酒店就绪标志无写入方
## 关联 / 联系人
- 后端:jw
- 前端待接:合同保险 Tab(汇总计数 + 逐户表格 + 三态徽标)、
手动开与作废重开弹窗(目标选择、可选择户、逐户结果展示)、批量催签、
出具类按钮按 `issuable` 置灰并提示原因