# 合同/保险镜像状态"无记录"时从 `"NONE"` 改为 `null`

- **变更类型**：修改接口
- **端类型**：管理后台
- **日期**：2026-06-18
- **Issue**：[#3953](https://git.1814.love:8443/wx/HL/issues/3953)
- **PR**：[#3956](https://git.1814.love:8443/wx/HL/pulls/3956)
- **Commit**：[dc21950a9](https://git.1814.love:8443/wx/HL/commit/dc21950a9)（feature）/ [41cfd5ad8](https://git.1814.love:8443/wx/HL/commit/41cfd5ad8)（merge）
- **后端负责人**：腰苏图

---
## ① 接口背景

order-v3 主表 `order_main` 中有两个镜像状态列：`contract_status`（合同状态）和 `insurance_status`（保险状态）。

以往在订单**尚无合同**或**尚无保险**时，这两列的值以字符串 `"NONE"` 写入并通过接口返回给前端。此次统一语义：「无记录」状态改为返回 `null`，不再返回字符串 `"NONE"`。

涉及枚举值（有进展时不变）：
- 合同：`GENERATING` / `GENERATED` / `SIGNED` / `VOIDED` / `RESIGNING`
- 保险：`INSURING` / `INSURED` / `CANCELLED` / `FAILED`

---
## ② 变更清单

| # | 接口 | 字段路径 | 变更 |
|---|------|----------|------|
| 1 | `GET /v3/admin/order/{id}` | `main.contractStatus` | 无合同时：`"NONE"` → `null` |
| 2 | `GET /v3/admin/order/{id}` | `main.insuranceStatus` | 无保险时：`"NONE"` → `null` |
| 3 | `GET /v3/admin/order/{id}/contract-insurance` | `contract.contractStatus` | 无合同时：`"NONE"` → `null` |
| 4 | `GET /v3/admin/order/{id}/contract-insurance` | `insurance.insuranceStatus` | 无保险时：`"NONE"` → `null` |
| 5 | `GET /v3/admin/order/group-batch/{groupBatchId}/orders` | `[*].contractStatus` | 无合同时：`"NONE"` → `null` |
| 6 | `GET /v3/admin/order/group-batch/{groupBatchId}/orders` | `[*].insuranceStatus` | 无保险时：`"NONE"` → `null` |

> 订单列表 `GET /v3/admin/order/list` 返回的 `OrderListItemRespVO` **不含**这两个字段，不受影响。

---
## ③ 接口详情

### 接口一：订单详情（9 Tab 聚合）

| 项 | 值 |
|---|---|
| 方法 + 路径 | `GET /v3/admin/order/{id}` |
| 接口名 | 订单详情（9 Tab 聚合） |
| 认证 | 需要 JWT（管理后台 token） |
| 幂等性 | 只读，天然幂等 |
| 限流 | 无特殊限流 |

### 接口二：合同保险 Tab

| 项 | 值 |
|---|---|
| 方法 + 路径 | `GET /v3/admin/order/{id}/contract-insurance` |
| 接口名 | 订单合同保险 Tab |
| 认证 | 需要 JWT（管理后台 token） |
| 幂等性 | 只读，天然幂等 |
| 限流 | 无特殊限流 |

### 接口三：团期子订单列表

| 项 | 值 |
|---|---|
| 方法 + 路径 | `GET /v3/admin/order/group-batch/{groupBatchId}/orders` |
| 接口名 | 团期子订单列表 |
| 认证 | 需要 JWT（管理后台 token） |
| 幂等性 | 只读，天然幂等 |
| 限流 | 无特殊限流 |

---
## ④ 接口入参

三个接口均为路径参数，本次变更不影响入参。

| 参数 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | Path | Long | 是 | 订单 ID（接口一、二） |
| `groupBatchId` | Path | Long | 是 | 团期 ID（接口三） |

---

## ⑤ 出参字段（受影响字段）

### 接口一 `GET /v3/admin/order/{id}` — `main` 节点

| 字段路径 | 类型 | 说明 |
|----------|------|------|
| `main.contractStatus` | `String \| null` | 合同状态。**无合同时返回 `null`**（原来返回 `"NONE"`）；有合同时为枚举值，见 § ⑥ |
| `main.insuranceStatus` | `String \| null` | 保险状态。**无保险时返回 `null`**（原来返回 `"NONE"`）；有保险时为枚举值，见 § ⑥ |

### 接口二 `GET /v3/admin/order/{id}/contract-insurance`

| 字段路径 | 类型 | 说明 |
|----------|------|------|
| `contract.contractStatus` | `String \| null` | 合同状态。**无合同时返回 `null`** |
| `insurance.insuranceStatus` | `String \| null` | 保险状态。**无保险时返回 `null`** |

其余字段（`contractSignedAt`、`contractFileUrl`、`events`、`insurancePolicyNo`、`insurancePremium` 等）在无合同/无保险时仍返回 `null`，无变化。

### 接口三 `GET /v3/admin/order/group-batch/{groupBatchId}/orders` — 列表项

| 字段路径 | 类型 | 说明 |
|----------|------|------|
| `[*].contractStatus` | `String \| null` | 合同状态。**无合同时返回 `null`** |
| `[*].insuranceStatus` | `String \| null` | 保险状态。**无保险时返回 `null`** |

---
## ⑥ 枚举 / 数据字典

### contractStatus 合同状态

| 枚举值 | 含义 | 备注 |
|--------|------|------|
| `null` | 无合同记录 | **本次新值**（原来是 `"NONE"`） |
| `GENERATING` | 合同生成中 | |
| `GENERATED` | 合同已生成（待签署） | |
| `SIGNED` | 已签署 | |
| `VOIDED` | 已作废 | |
| `RESIGNING` | 重新签署中 | |

### insuranceStatus 保险状态

| 枚举值 | 含义 | 备注 |
|--------|------|------|
| `null` | 无保险记录 | **本次新值**（原来是 `"NONE"`） |
| `INSURING` | 出单中 | |
| `INSURED` | 已投保 | |
| `CANCELLED` | 已取消 | |
| `FAILED` | 出单失败 | |

---
## ⑦ 错误码

本次变更不新增错误码。以下为接口既有错误码：

| 错误码 | HTTP 状态 | 说明 |
|--------|-----------|------|
| `587000` | 400 | 订单不存在 |

---

## ⑧ 示例

### 8.1 典型成功——订单已签约并已投保

**请求**
```http
GET /v3/admin/order/2000000001
Authorization: Bearer <token>
```

**响应（main 节点节选）**
```json
{
  "code": 200,
  "data": {
    "main": {
      "id": "2000000001",
      "orderNo": "HL20260601001",
      "orderStatus": "ON_TRIP",
      "contractStatus": "SIGNED",
      "insuranceStatus": "INSURED",
      "refundStatus": null,
      "hasRefund": false
    }
  }
}
```

**合同保险 Tab（`GET /v3/admin/order/2000000001/contract-insurance`）**
```json
{
  "code": 200,
  "data": {
    "contract": {
      "contractStatus": "SIGNED",
      "contractSignedAt": "2026-06-01T10:30:00",
      "contractFileUrl": "https://oss.hulalv.com/contract/HL20260601001.pdf",
      "events": [
        { "eventType": "GENERATE", "occurredAt": "2026-06-01T10:00:00" },
        { "eventType": "SIGN", "occurredAt": "2026-06-01T10:30:00" }
      ]
    },
    "insurance": {
      "insuranceStatus": "INSURED",
      "insurancePolicyNo": "PICC2026060100123",
      "insurancePremium": 88.00,
      "events": [
        { "eventType": "ISSUE", "occurredAt": "2026-06-01T10:35:00" }
      ]
    }
  }
}
```

---
### 8.2 边界情况——订单刚创建，尚无合同和保险

**响应（main 节点节选）**
```json
{
  "code": 200,
  "data": {
    "main": {
      "id": "2000000002",
      "orderNo": "HL20260602001",
      "orderStatus": "CUSTOMIZING",
      "contractStatus": null,
      "insuranceStatus": null,
      "hasRefund": false
    }
  }
}
```

**合同保险 Tab**
```json
{
  "code": 200,
  "data": {
    "contract": {
      "contractStatus": null,
      "contractSignedAt": null,
      "contractFileUrl": null,
      "events": []
    },
    "insurance": {
      "insuranceStatus": null,
      "insurancePolicyNo": null,
      "insurancePremium": null,
      "events": []
    }
  }
}
```

**团期子订单列表（`GET /v3/admin/order/group-batch/3000000001/orders`，无合同无保险的子订单项节选）**
```json
{
  "code": 200,
  "data": [
    {
      "orderId": "2000000002",
      "orderNo": "HL20260602001",
      "contractStatus": null,
      "insuranceStatus": null,
      "payStatus": "UNPAID"
    }
  ]
}
```

### 8.3 业务失败——订单 ID 不存在

**请求**
```http
GET /v3/admin/order/9999999999
```

**响应**
```json
{
  "code": 587000,
  "msg": "订单不存在"
}
```

---
## ⑨ 业务边界

**适用**：
- 创建后尚未触发签约的订单：`contractStatus` 返回 `null`
- 创建后尚未触发投保的订单：`insuranceStatus` 返回 `null`
- 合同被作废后重新签署流程未开始的中间态仍走枚举值（`VOIDED` → `RESIGNING`），不返回 `null`

**不适用（不返回 null）**：
- 合同/保险流程一旦触发（任意枚举值），即使中途失败（`FAILED`）也返回枚举值而非 `null`

**特殊边界**：
- 若历史订单数据库列值为 `"NONE"`（迁移前旧数据），后端读取时统一转换为 `null` 再返回，前端不会收到字符串 `"NONE"`

---

## ⑩ 修改前后对比

### 字段级对比

| 字段 | 无合同/无保险时修改前 | 无合同/无保险时修改后 |
|------|---------------------|---------------------|
| `contractStatus` | `"NONE"` | `null` |
| `insuranceStatus` | `"NONE"` | `null` |

三个接口均适用此对比。有合同/有保险时枚举值**不变**。

### 行为级对比

| 场景 | 修改前 | 修改后 |
|------|--------|--------|
| 新建订单查详情 | `contractStatus: "NONE"` | `contractStatus: null` |
| 查合同保险 Tab | `contract.contractStatus: "NONE"` | `contract.contractStatus: null` |
| 团期子订单列表 | `contractStatus: "NONE"` | `contractStatus: null` |

---

## ⑪ 影响评估 / 回滚

### 破坏兼容

**破坏性变更**。若前端代码以 `contractStatus === "NONE"` 或 `insuranceStatus === "NONE"` 判断「无状态」，**需改为 `contractStatus == null`**（或 `!contractStatus`），否则判断会失效。

### 前端同步要求

前端需在本次后端上线前（或同时）完成如下改动：

1. 所有判断 `contractStatus === "NONE"` 的地方改为 `contractStatus == null`（或 `!contractStatus`）
2. 所有判断 `insuranceStatus === "NONE"` 的地方改为 `insuranceStatus == null`（或 `!insuranceStatus`）
3. 合同 Tab 徽标逻辑：判断是否显示「无合同」状态从 `=== "NONE"` 改为 `=== null`
4. 保险 Tab 徽标逻辑：同上

### 回滚方案

后端可在镜像写入层回退 `null → "NONE"` 兜底逻辑，重新发布即生效，无需 DDL。

---

## ⑫ 注意事项

1. **三个接口同时生效**：`GET /v3/admin/order/{id}`、`GET /v3/admin/order/{id}/contract-insurance`、`GET /v3/admin/order/group-batch/{groupBatchId}/orders` 同批上线，需一次性适配，不存在分步过渡。
2. **历史数据已处理**：后端上线时同步处理库中存量 `"NONE"` 值，前端永远不会收到字符串 `"NONE"`，无需做兼容两个值的过渡逻辑。
3. **列表接口不受影响**：`GET /v3/admin/order/list` 返回的列表项 VO 不含 `contractStatus` / `insuranceStatus`，无需处理。
4. **枚举值语义不变**：除 `"NONE"` 变为 `null` 外，其余枚举值含义和拼写完全不变。

---

## ⑬ 关联 / 联系人

| 项 | 链接 |
|---|------|
| Issue | [#3953 合同/保险镜像状态统一 NONE→null](https://git.1814.love:8443/wx/HL/issues/3953) |
| PR | [#3956 refactor(order-v3): 合同/保险镜像状态统一 NONE→null](https://git.1814.love:8443/wx/HL/pulls/3956) |
| Feature Commit | [dc21950a9](https://git.1814.love:8443/wx/HL/commit/dc21950a9) |
| Merge Commit | [41cfd5ad8](https://git.1814.love:8443/wx/HL/commit/41cfd5ad8) |
| 后端负责人 | 腰苏图（yaosutu） |
