# 【修改接口·管理后台】订单详情「合同·保险」Tab:无合同/无保险时子对象返 null (#4460) > **PR**: #4461 | **服务**: hl-order-service-v3 | **更新时间**: 2026-06-26 ## 1. 接口背景 订单详情「合同·保险」Tab 懒加载接口,在订单**从未签约 / 从未投保**时,原本仍返回一个字段全为 `null` 的 `contract` / `insurance` 壳对象,前端要判断"这单到底有没有合同/保险"只能逐字段判空,体验差且容易误判。 本次改为:合同、保险**各自独立**判断是否存在,不存在则整个子对象直接返 `null`,前端一层判断即可。 ## 2. 变更清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 合同·保险 Tab | GET | `/v3/admin/order/{id}/contract-insurance` | 修改接口 | 无合同时 `contract` 返 null、无保险时 `insurance` 返 null(原为字段全 null 的壳对象) | ## 3. 接口详情 ### 3.1 获取订单合同·保险 Tab 数据 - **使用场景**:管理后台订单详情页,点开「合同·保险」Tab 时懒加载 - **认证**:需要 JWT(管理后台登录态) - **幂等性**:是(纯查询) - **限流**:无 ## 4. 接口入参 ### 4.1 路径参数 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `id` | String | ✅ | 订单 ID(雪花 ID,字符串传递) | ### 4.2 请求体字段 无(GET 请求)。 ## 5. 出参(响应) ### 5.1 顶层结构 | 字段 | 类型 | 说明 | |------|------|------| | `contract` | Object \| null | 合同信息;该订单**从未签约**时为 `null` | | `insurance` | Object \| null | 保险信息;该订单**从未投保**时为 `null` | > **判定口径**: > - `contract` 为 null:合同状态为空 **且** 无合同方案头 **且** 无合同事件 > - `insurance` 为 null:保险聚合状态为空 **且** 无保单 > 三者/两者任一非空即视为"存在",照常返回完整对象。 ### 5.2 `contract` 对象字段(存在时) | 字段 | 类型 | 说明 | |------|------|------| | `contractStatus` | String | 合同状态,见 §6.1 | | `contractSignedAt` | String | 签署时间(`yyyy-MM-dd HH:mm:ss`),未签为 null | | `contractFileUrl` | String | 合同文件地址,未生成为 null | | `contractSchemeName` | String | 合同方案名 | | `signerName` | String | 签署人 | | `signMethod` | String | 签署方式 | | `events` | Array | 合同流程事件(固定 5 步模板),见 §5.4 | ### 5.3 `insurance` 对象字段(存在时) | 字段 | 类型 | 说明 | |------|------|------| | `insuranceStatus` | String | 保险聚合状态,见 §6.2 | | `insuranceStatusName` | String | 保险状态中文名 | | `totalPremium` | Number | 总保费 | | `policyCount` | Number | 保单数量 | | `policies` | Array | 逐张保单列表(存在时;可能为空数组) | ### 5.4 `events` / `policies[].events` 事件字段 | 字段 | 类型 | 说明 | |------|------|------| | `eventType` / `stepCode` | String | 步骤码 | | `eventName` | String | 步骤名 | | `actorType` | String | 操作方类型,见 §6.3 | | `actorTypeName` | String | 操作方中文名 | | `actorLabel` | String | 操作方标签 | | `statusLabel` | String | 状态标签(如 已完成/进行中/待处理) | | `description` | String | 描述 | | `occurredAt` | String | 发生时间 | ## 6. 枚举 / 数据字典 ### 6.1 contractStatus(合同状态) **所属字段**:`contract.contractStatus` | **类型**:`String` | 值 | 中文 | 说明 | |----|------|------| | `SIGNED` | 已签约 | 合同已签署完成 | | 其他业务态 | — | 按合同流程取值(生成/报备/作废等) | ### 6.2 insuranceStatus(保险聚合状态) **所属字段**:`insurance.insuranceStatus` | **类型**:`String` | 值 | 中文(insuranceStatusName) | 说明 | |----|------|------| | `INSURED` | 已出单 | — | | `INSURING` | 出单中 | — | | `CANCELLED` | 已取消 | — | | `FAILED` | 出单失败 | — | ### 6.3 actorType(操作方类型) **所属字段**:`events[].actorType` / `policies[].events[].actorType` | **类型**:`String` | 值 | 中文(actorTypeName) | 说明 | |----|------|------| | `SYSTEM` | 系统 | — | | `THIRD_PARTY` | 第三方 | 如电子合同平台 / 保险公司 | | `CUSTOMER` | 客户 | — | ## 7. 错误码 | code | 含义 | 触发场景 | |------|------|----------| | `581007` | 订单不存在 | 路径 `id` 对应订单不存在 | ## 8. 示例 ### 8.1 典型成功(无合同无保险订单) **请求**: ``` GET /v3/admin/order/2070039058075017217/contract-insurance Authorization: Bearer 无请求体 ``` **响应**: ```json { "code": 200, "message": "成功", "data": { "contract": null, "insurance": null }, "success": true } ``` ### 8.2 边界(有合同·有保险订单) **场景说明**:订单已签约(SIGNED)且已投保,两子对象照常返回完整结构。 **响应**: ```json { "code": 200, "message": "成功", "data": { "contract": { "contractStatus": "SIGNED", "contractSignedAt": "2026-04-18 17:12:00", "contractFileUrl": "https://oss.example.com/contract/xxx.pdf", "contractSchemeName": "标准国内单团签约方案", "signerName": "张三", "signMethod": "电子签约", "events": [ { "stepCode": "GENERATE", "eventName": "生成合同", "actorType": "SYSTEM", "actorTypeName": "系统", "statusLabel": "已完成", "occurredAt": "2026-04-18 17:10:00" } ] }, "insurance": { "insuranceStatus": "INSURED", "insuranceStatusName": "已出单", "totalPremium": 88.00, "policyCount": 1, "policies": [ { "policyNo": "PICC-001", "status": "INSURED", "statusName": "已出单", "events": [] } ] } }, "success": true } ``` ### 8.3 业务失败(订单不存在) **请求**: ``` GET /v3/admin/order/88888888/contract-insurance Authorization: Bearer ``` **响应**: ```json { "code": 581007, "message": "订单不存在", "success": false } ``` ## 9. 业务边界 - ✅ **返完整对象**:合同侧状态/方案头/事件任一存在 → `contract` 完整返回;保险侧聚合状态或保单任一存在 → `insurance` 完整返回 - ⚠️ **返 null**:从未签约 → `contract` 整体为 `null`;从未投保 → `insurance` 整体为 `null` - 两者相互独立:可能出现「有合同无保险」(`contract` 非 null、`insurance` 为 null)或反之 ## 10. 修改前后对比 ### 10.1 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | `data.contract`(无合同时) | 字段全为 null 的对象 `{contractStatus:null, ..., events:[]}` | `null` | | `data.insurance`(无保险时) | 字段全为 null 的对象 `{insuranceStatus:null, ..., policyCount:0, policies:[]}` | `null` | ### 10.2 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 前端判断有无合同/保险 | 逐字段判空(如 `contract.contractStatus == null && ...`) | 一层判断 `if (data.contract)` / `if (data.insurance)` | ## 11. 影响评估 / 回滚 ### 11.1 影响评估 - **是否破坏向后兼容**:是(无合同/无保险订单的 `contract`/`insurance` 由对象变为 null,前端需调整判空逻辑) - **前端是否必须同步上线**:是(原逐字段判空逻辑需改为判子对象是否为 null) ### 11.2 回滚方案 - **回滚方式**:revert PR #4461 - **回滚耗时**:预计 5 分钟(重新部署) ## 12. 注意事项 - 前端 workaround 清理点:原先「逐字段判空来识别是否有合同/保险」的逻辑可改为「判 `data.contract` / `data.insurance` 是否为 null」 - 「有合同·有保险」「有合同·无保险」「无合同·有保险」「无合同·无保险」四种组合均需正确处理 ## 13. 关联 / 联系人 ### 13.1 链接 - **Issue**: [#4460](https://git.1814.love:8443/wx/HL/issues/4460) - **PR**: [#4461](https://git.1814.love:8443/wx/HL/pulls/4461) - **Merge commit**: [8ae5d58](https://git.1814.love:8443/wx/HL/commit/8ae5d5819268a01910a8ec93908489d728ad7005) ### 13.2 联系人 - **后端负责人**: @yst - **前端对接(管理后台)**: —