--- schema: "hl-changelog/v2" ticket: "7105" title: "团期合同保险面板与逐户出具" consumer: "admin" author: "jw(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "838e77da" target_release: "" verified_at: "2026-09-08" status_note: "新增团期合同保险面板与逐户手动开/作废重开/批量催签四端点,后端已部署 TEST 并实测。注意:出具门依赖房车导摄四项 ready,而 hotel_ready 线上无写入方(#4132 未完成),故面板 issuable 恒 false、手动开/作废重开恒 589548;面板读与批量催签不受影响现在就能用。前端待接:团期详情新增合同保险 Tab(汇总计数+逐户表格+三态徽标用后端 contractStateText/insuranceStateText 不自映射),手动开/作废重开弹窗(target+可选户+逐户结果),批量催签,出具类按钮按 issuable 置灰;权限读 group-batch:view、写 group-batch:contract:issue(需先部署用户服务)。决策:整个 Tab 入 backlog 排在 #7067 U3-U7 闭环后统一汇总审派发,接入时按 issuable 置灰即可、不因 #4132 单独挂起。本条保持 pending。" 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` 置灰并提示原因