diff --git a/changelogs-v2/2026-09/07_7105_团期合同保险面板与逐户出具-新增接口-管理后台.md b/changelogs-v2/2026-09/07_7105_团期合同保险面板与逐户出具-新增接口-管理后台.md new file mode 100644 index 00000000..9833e3b4 --- /dev/null +++ b/changelogs-v2/2026-09/07_7105_团期合同保险面板与逐户出具-新增接口-管理后台.md @@ -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 +``` + +#### 响应示例 + +```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` 置灰并提示原因