# 【前端对接·管理后台】车务司机险自动投退保闭环与保险菜单契约

> **Issue**: [wx/HL#4760](https://git.1814.love:8443/wx/HL/issues/4760)
> **服务**: hl-fleet-service + hl-order-service-v3 insurance Feign
> **日期**: 2026-07-06
> **影响范围**: 车务管理 / 保险菜单 / 派单确认 / 取消 / 提前完结 / 车队对账

---

## 1. 结论

- 车务保险菜单只展示司机险任务，不展示游客保险、订单保险、客户退款保险明细。
- `insurance.type=perTrip` 的司机在派单确认进入 `assigned` 后，后端按 `driverId + serviceDate` 自动一日一保投保。
- `insurance.type=annual` 的司机不会自动按单买险，但派单确认时后端会逐服务日校验年保覆盖；年保到期或覆盖不足会生成车务保险待办。
- 同一司机同一服务日只能有一份司机险；前后订单相接同日不重复买。
- 取消/提前完结后，只退该司机该服务日已无其它 active 派单占用的单日险。
- 司机险只进入车队/司机成本，不进入客户退款、订单收款、游客保险或合同保险。
- 自动投退保失败不阻断派单主流程，但必须进入车务保险任务待处理。

---

## 2. 角色与菜单

| 角色 | 行为 |
|------|------|
| `VEHICLE_MANAGER` / 车务角色 | 可访问 `/admin/fleet/insurance/**`，可查看任务、重试、标记线下完成、忽略。测试服独立账号：`fleet_mgr_4760` |
| `CUSTOMIZER` / 定制师 | 不应展示车务保险菜单；若访问 `/admin/fleet/**` 按角色守卫返回 `403`。测试服独立账号：`designer_4760` |

前端不要复用订单保险页面的数据源。车务保险菜单只调本文件接口。

---

## 3. 接口一：司机险任务/流水分页

```http
GET /admin/fleet/insurance/tasks
```

### 3.1 Query 参数

| 参数 | 类型 | 必填 | 示例 | 说明 |
|------|------|------|------|------|
| `page` | number | 否 | `1` | 页码 |
| `pageSize` | number | 否 | `20` | 每页条数 |
| `pendingOnly` | boolean | 否 | `true` | `true` 只查 `PENDING/PROCESSING`；不传查全部 |
| `taskType` | string | 否 | `PURCHASE` | `PURCHASE` 投保 / `REFUND` 退保 |
| `taskStatus` | string | 否 | `PENDING` | `PENDING/PROCESSING/SUCCESS/RESOLVED/IGNORED` |
| `driverId` | string | 否 | `188800000000000001` | 司机 ID |
| `assignmentId` | string | 否 | `194000000000000001` | 派单 ID |
| `orderId` | string | 否 | `193900000000000001` | 订单 ID |
| `serviceDateFrom` | string | 否 | `2026-07-01` | 服务日起，`yyyy-MM-dd` |
| `serviceDateTo` | string | 否 | `2026-07-31` | 服务日止，不能早于起始日 |

### 3.2 请求例子

```http
GET /admin/fleet/insurance/tasks?pendingOnly=true&taskType=PURCHASE&serviceDateFrom=2026-07-01&serviceDateTo=2026-07-31&page=1&pageSize=20
Authorization: Bearer <admin-token>
```

### 3.3 响应例子

```json
{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "page": 1,
    "pageSize": 20,
    "total": 2,
    "records": [
      {
        "taskId": "194100000000000001",
        "bizKey": "PURCHASE:188800000000000001:2026-07-06",
        "taskType": "PURCHASE",
        "taskTypeLabel": "投保",
        "taskStatus": "PENDING",
        "taskStatusLabel": "待处理",
        "driverId": "188800000000000001",
        "driverName": "王师傅",
        "assignmentId": "194000000000000001",
        "orderId": "193900000000000001",
        "orderNo": "HL20260705001",
        "insuranceOrderId": null,
        "policyNo": "",
        "serviceDate": "2026-07-06",
        "coverageStartDate": "2026-07-06",
        "coverageEndDate": "2026-07-06",
        "premiumAmount": "0.00",
        "source": "BAOYOU",
        "sourceLabel": "保游网",
        "errorCode": "540007",
        "errorMessage": "保险计划没有匹配该保障天数的费率",
        "suggestedAction": "保险计划没有匹配该保障天数的费率，请维护费率后重试",
        "retryCount": 1,
        "proofUrls": [],
        "handledBy": null,
        "handledAt": null,
        "handleRemark": "",
        "createTime": "2026-07-06T09:10:00",
        "updateTime": "2026-07-06T09:12:00"
      },
      {
        "taskId": "194100000000000002",
        "bizKey": "PURCHASE:188800000000000001:2026-07-07",
        "taskType": "PURCHASE",
        "taskTypeLabel": "投保",
        "taskStatus": "SUCCESS",
        "taskStatusLabel": "已成功",
        "driverId": "188800000000000001",
        "driverName": "王师傅",
        "assignmentId": "194000000000000002",
        "orderId": "193900000000000002",
        "orderNo": "HL20260705002",
        "insuranceOrderId": "194200000000000001",
        "policyNo": "BY-20260707001",
        "serviceDate": "2026-07-07",
        "coverageStartDate": "2026-07-07",
        "coverageEndDate": "2026-07-07",
        "premiumAmount": "18.00",
        "source": "BAOYOU",
        "sourceLabel": "保游网",
        "errorCode": "",
        "errorMessage": "",
        "suggestedAction": "",
        "retryCount": 0,
        "proofUrls": [],
        "handledBy": "0",
        "handledAt": "2026-07-06T09:10:05",
        "handleRemark": "",
        "createTime": "2026-07-06T09:10:05",
        "updateTime": "2026-07-06T09:10:05"
      }
    ]
  }
}
```

### 3.4 字段说明

| 字段 | 说明 |
|------|------|
| `bizKey` | 后端幂等键，前端只展示/排查，不要拼业务逻辑 |
| `taskType` | `PURCHASE` 投保，`REFUND` 退保 |
| `taskStatus` | `PENDING` 待处理，`PROCESSING` 处理中，`SUCCESS` 线上成功，`RESOLVED` 线下解决，`IGNORED` 已忽略 |
| `premiumAmount` | 真实保费/退回金额，字符串；前端不要转 number 做精度运算 |
| `source` | `BAOYOU` 保游网，`OFFLINE` 线下处理 |
| `proofUrls` | 线下处理凭证 URL；线上成功通常为空数组 |
| `handledBy=0` | 后端自动成功流水，不是人工账号 |

---

## 4. 接口二：重试任务

```http
POST /admin/fleet/insurance/tasks/{taskId}/retry
```

### 4.1 请求例子

```http
POST /admin/fleet/insurance/tasks/194100000000000001/retry
Authorization: Bearer <admin-token>
```

无 body。

### 4.2 响应例子

```json
{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "taskId": "194100000000000001",
    "taskStatus": "SUCCESS",
    "taskStatusLabel": "已成功"
  }
}
```

### 4.3 前端处理

- 点击后按钮加 loading，接口有幂等保护，不要连续提交。
- 成功后刷新分页列表。
- 如果仍返回 `PENDING`，说明重试失败但已更新错误信息和 `retryCount`，前端展示最新 `errorMessage/suggestedAction`。

---

## 5. 接口三：标记线下完成

```http
POST /admin/fleet/insurance/tasks/{taskId}/offline-done
```

### 5.1 请求参数

| 字段 | 类型 | 必填 | 示例 | 说明 |
|------|------|------|------|------|
| `policyNo` | string | 是 | `OFFLINE-20260701001` | 真实保单号 / 退保凭证号 |
| `premiumAmount` | decimal string | 是 | `"18.00"` | 真实保费金额；退保填实际退回/冲减金额 |
| `proofUrls` | string[] | 是 | `["https://...jpg"]` | 至少 1 个凭证 URL |
| `remark` | string | 否 | `保司后台线下补投` | 处理备注 |

### 5.2 请求例子

```json
{
  "policyNo": "OFFLINE-20260701001",
  "premiumAmount": "18.00",
  "proofUrls": [
    "https://oss.test.1814.love/fleet/insurance/OFFLINE-20260701001.jpg"
  ],
  "remark": "保司后台线下补投，截图已上传"
}
```

### 5.3 响应例子

```json
{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "taskId": "194100000000000001",
    "taskStatus": "RESOLVED",
    "taskStatusLabel": "已解决"
  }
}
```

### 5.4 校验

- `policyNo` 为空：参数校验失败。
- `premiumAmount` 为空或负数：参数校验失败。
- `proofUrls` 为空数组或不传：返回 `code=400`，`message=线下凭证不能为空`。

---

## 6. 接口四：忽略任务

```http
POST /admin/fleet/insurance/tasks/{taskId}/ignore
```

### 6.1 请求例子

```json
{
  "remark": "重复人工记录，保单已由另一任务处理"
}
```

### 6.2 响应例子

```json
{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "taskId": "194100000000000001",
    "taskStatus": "IGNORED",
    "taskStatusLabel": "已忽略"
  }
}
```

---

## 7. 自动触发场景

| 场景 | 后端行为 | 前端关注 |
|------|----------|----------|
| 派单确认 `holding -> assigned` | perTrip 司机按服务日逐日投保 | 确认派单成功不代表保险一定成功，需看保险任务 |
| 派单确认时司机配置年保且服务日全部被 `annualStart~annualEnd` 覆盖 | 不买按单险，不生成待办 | 保险成本按年保摊销进车队成本 |
| 派单确认时司机配置年保但部分服务日不被覆盖 | 对未覆盖服务日生成 `PENDING` 投保待办，错误码 `540033` | 提示车务续保/修正年保起止，或改按行程险后重试 |
| 司机年保为手工/线下来源且与线上保单不一致 | 生成 `PENDING` 核对待办，错误码 `540034` | 提示上传线下凭证或修正保单信息 |
| 已有线上保单覆盖同司机同日 | 不重复购买，写 `SUCCESS` 幂等流水 | 不要提示重复投保 |
| 已有手工/年保覆盖同司机同日 | 不买按单险，写 `PENDING` 提醒核对 | 展示 `suggestedAction` |
| 派单取消 | 只退该司机该服务日无其它 active 派单占用的单日险 | 同日仍有其它订单时不会退 |
| 提前完结 | 退新 `endDate` 之后不再使用的单日险 | 定时完结通常无动作 |
| 撤销取消恢复为 assigned | 如果之前已退险，恢复后补买 | 前端刷新任务列表 |

---

## 8. 前端不要做的事

- 不要用大交通日期推导司机险保障日期；司机险保障日期以派单服务日为准。
- 不要把司机险退保结果参与客户退款计算。
- 不要把 `premiumAmount` 转成 JS number 再计算。
- 不要在前端拼重试业务参数，重试只传 `taskId`。
- 不要在车务保险菜单展示订单游客险。
- 不要把 `SUCCESS` 自动流水当成待办提示；待办只看 `PENDING/PROCESSING`。
- 不要只看司机保险类型是 `annual` 就认为无需处理；必须以后端任务状态为准。年保到期、行程增加一天、换司机后都会重新按服务日校验。

---

## 9. 错误码提示

| code | 场景 | 前端文案建议 |
|------|------|--------------|
| `100001` | 参数非法、任务缺上下文、服务日区间反了 | 展示后端 message |
| `100502` | 幂等保护，重复提交 | “正在处理，请勿重复提交” |
| `540007` | 保险计划缺费率 | “请维护保险计划费率后重试” |
| `540032` | 同司机服务日已有保单覆盖 | “已有保单覆盖，请核对后处理” |
| `540033` | 司机配置年保但服务日不在年保起止内 | “司机年保未覆盖当前服务日，请续保/修正年保后重试，或改按行程险处理” |
| `540034` | 手工年保/线下保单与线上覆盖信息不一致 | “请核对线下保单并上传凭证，确认后标记线下完成” |
| `605601` | 保险服务不可用 | “保险服务暂不可用，请稍后重试或线下处理” |

---

## 10. 测试要求

前端联调不要只测接口 200。至少覆盖：

- 新建订单 -> 补出行人 -> 补大交通 -> 提用车需求 -> 车务派单确认 -> 保险任务生成。
- 同一司机同一服务日多订单，不重复买保险。
- 取消派单时，同司机同日还有其它 active 派单则不退保。
- 年保司机在服务期内不买按单险；服务日超过年保到期日时生成 `540033` 待办。
- 年保续保后重试 `540033` 任务，应变为 `SUCCESS`，且不能产生重复按单险。
- 投保失败后重试成功。
- 线下完成必须上传凭证。
- 定制师角色不可见车务保险菜单。
