# 【前端对接·管理后台】车务需求级派单完成回调与滚动发布契约

> Issue: [wx/HL#4935](https://git.1814.love:8443/wx/HL/issues/4935)
>
> PR: [wx/HL#4994](https://git.1814.love:8443/wx/HL/pulls/4994)、[wx/HL#5009](https://git.1814.love:8443/wx/HL/pulls/5009)
>
> 服务: `hl-fleet-service` / `hl-order-service-v3`
>
> 日期: 2026-07-16
>
> 影响范围: 车务派单完成、用车需求驳回、订单资源状态、看板刷新与部署兼容

## 一、关键纠正

此前链路可能在单个日期或单辆车派定后提前把整个用车需求写成 `DONE`。本次改为：

- 一个用车需求只做一次最终完成回调。
- 只有全部服务日期、全部车型项均已生成有效派单，并且每条逐日配置同时绑定车辆和司机，后端才允许整个需求完成。
- 前端不得根据“某一天已派”“某一辆车已派”自行把需求或订单资源节点标成完成。
- 前端继续直接使用看板/详情接口返回的状态、文案和能力字段，不维护独立状态映射。

## 二、前端接口结论

本次不新增前端调用接口，管理后台继续使用：

| 接口 | 方法 | 路径 | 前端用途 |
|---|---|---|---|
| 派单看板汇总 | GET | `/admin/fleet/board/summary` | 状态选项、文案、数量 |
| 派单看板列表 | GET | `/admin/fleet/board/orders` | 分页卡片、状态与能力字段 |
| 派单看板详情 | GET | `/admin/fleet/board/orders/{orderId}` | 当前需求、逐日行程、当前派单 |
| 派单时间线 | GET | `/admin/fleet/board/orders/{orderId}/timeline` | 已发生操作记录 |

前端处理规则：

1. 状态筛选使用 `summary.statusOptions`，卡片文案使用 `assignmentStatusLabel`。
2. 派车入口只看 `canAssign`，驳回入口只看 `canRejectRequirement`。
3. 派单、驳回或重试成功后重新请求汇总、列表和当前详情，不能只在本地改一张卡片。
4. 同一需求仍有未完成日期或其他车辆项时，后端保持进行中；前端不得提前展示“已完成”。
5. 后端部署开关关闭期间，需求级完成/驳回事件会保留待重放；前端不需要轮询内部 Outbox，也不得调用内部回调。

## 三、管理后台响应示例

### 3.1 仍有未完成配置

```json
{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "assignmentStatus": "holding",
    "assignmentStatusLabel": "排车中",
    "canAssign": true,
    "canRejectRequirement": false,
    "currentAssignment": {
      "requirementId": "2075001000000000001"
    }
  }
}
```

该响应只表示需求仍在处理，不能因 `currentAssignment` 非空推断整个需求已完成。

### 3.2 整个需求完成后

```json
{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": {
    "assignmentStatus": "assigned",
    "assignmentStatusLabel": "已派车",
    "canAssign": false,
    "canRejectRequirement": false
  }
}
```

实际字段以看板接口当前 OpenAPI 为准；状态中文和能力判断均由后端返回。

## 四、内部回调契约

> 本节供后端与 QA 验收。以下 `/v3/internal/**` 接口不经过管理后台，不配置公网网关路由，前端禁止调用。

### 4.1 最终完成回调

```http
POST /v3/internal/order/vehicle-assignment/callback
Content-Type: application/json
```

请求示例：

```json
{
  "orderId": "2074746808742928386",
  "requirementId": "2075001000000000001",
  "vehicleId": "2076001000000000001",
  "vehicleType": "suv",
  "vehicleCount": 1,
  "licensePlate": "蒙A12345",
  "brand": "丰田汉兰达",
  "seats": 7,
  "plannedDailyFee": "1300.00",
  "dailyFeeSource": "PRICE_CALENDAR",
  "driverStaffId": "2077001000000000001",
  "driverName": "测试司机",
  "driverPhone": "13800000000",
  "topologyFingerprint": "<64位 SHA-256 摘要>",
  "remark": "需求级最终派单快照"
}
```

成功响应：

```json
{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": null
}
```

新版 Fleet 必填字段：`orderId`、`requirementId`、`vehicleId`、`vehicleType`、`vehicleCount`、`topologyFingerprint`。其中 `topologyFingerprint` 是 Fleet 根据该需求全部有效逐日派单生成的 64 位 SHA-256 摘要；其余快照字段允许为空，后端不得伪造车牌、品牌、座位、价格或司机信息。

`vehicleId`、车牌和司机字段是稳定排序后的代表派单，`vehicleCount` 是该需求实际车辆组总数。完整逐日、多车辆和多司机拓扑仍以 Fleet 派单明细为准，不能从该轻量快照反推完整派车表。

滚动发布期间，旧版 Fleet 不传 `topologyFingerprint` 时，Order-v3 会根据完整回调快照生成 `legacy:` 前缀摘要并持久化。该兼容仅用于先升级 Order-v3、后升级 Fleet 的过渡期；新版 Fleet 仍必须发送摘要。

### 4.2 轻量进度回写

```http
POST /v3/internal/order/orders/{orderId}/requirement/vehicle/status?requirementId={requirementId}&status=PROCESSING
```

该接口只允许 `PROCESSING`。`DONE` 必须走最终完成回调并冻结快照。

### 4.3 驳回回写

```http
POST /v3/internal/order/orders/{orderId}/requirement/vehicle/reject
Content-Type: application/json
```

请求示例：

```json
{
  "requirementId": "2075001000000000001",
  "returnRemark": "车型需求不完整，请定制师补充",
  "operatorId": "2078001000000000001"
}
```

只有当前需求不存在 `holding/assigned` 有效派单时才允许驳回。

## 五、状态、幂等和错误分支

| 场景 | 结果 | 副作用 |
|---|---|---|
| `PENDING/PROCESSING` 且无快照 | 原子写快照并完成需求 | 同事务写需求 `DONE`、订单车辆状态 `DONE`、待办/日志并尝试推进订单 |
| `DONE` 且摘要相同 | 幂等成功 | 不加需求写锁、不更新 `update_time`，不重复同步司机、待办、日志或推进订单 |
| `DONE` 且摘要变化 | 刷新轻量快照 | 只更新同一需求快照和司机信息，不重复推进订单、待办或时间线 |
| 旧版回调未传摘要 | 兼容成功 | Order-v3 生成稳定 `legacy:` 摘要；相同旧请求重放仍为零写入 |
| `DONE` 但无快照 | 返回 `582081` | 禁止补造快照，禁止继续副作用 |
| active 状态已有快照 | 返回 `582082` | 禁止重复回写 |
| 需求不存在或失效 | 返回 `582080` | 无写入 |
| 非法状态流转 | 返回 `582083` | 无写入 |
| 订单/需求已取消 | 跳过 | 不写完成快照，不推进订单 |

并发与重放需区分：同一摘要在 5 秒互斥窗口外再次提交时返回成功且数据库零写入；互斥窗口内的并发重复请求返回可识别冲突 `100502`，同样不得重复写快照、待办、流水或推进订单。前端遇到该冲突应刷新当前需求状态，不得自行补写完成状态。

错误响应示例：

```json
{
  "code": 582081,
  "message": "用车需求状态不允许回写配车",
  "success": false,
  "data": null
}
```

## 六、滚动发布与回滚

1. 先部署全部 `hl-order-service-v3` 实例并确认 Flyway 成功。
2. 保持 `FLEET_REQUIREMENT_LIFECYCLE_ENABLED=false`，再部署全部 `hl-fleet-service` 实例。
3. 确认 Fleet Flyway、健康与普通 Outbox 消费正常后，再启用开关。
4. 开关关闭时，需求级 Outbox 事件不会占用普通事件扫描窗口，也不会被丢弃；开启后继续重放。
5. 异常时先关闭开关，再回滚服务制品；兼容字段和历史 Outbox 不做破坏性回滚。

## 七、前端必须处理

1. 不新增内部回调请求，不把内部错误码做成独立前端流程。
2. 不按每日派单行数或单车派定结果推导需求完成。
3. 继续使用后端返回的 `statusOptions`、`assignmentStatusLabel`、`canAssign`、`canRejectRequirement`。
4. 操作成功后刷新服务端状态；并发处理中若能力字段变化，以最新接口响应为准。
5. 不修改既有分页、团号、联系人、定制师、逐日行程和大交通字段接法；这些仍以 `57_4882` 文档为准。

## 八、不影响范围

- 不修改 `hl-ui`，本文件仅做后端契约告知。
- 不新增管理后台分页或不分页接口。
- 不改变车型大类、司机占一座、司机险只计车队成本等既有口径。
- 不处理团期配车。

## 九、验证状态

### 9.1 合并前代码验证

```text
hl-order-service-v3 targeted: 198 tests，0 failures，0 errors，0 skipped
hl-order-service-v3 full verify: 5582 tests，0 failures，0 errors，15 skipped
hl-fleet-service targeted: 226 tests，0 failures，0 errors，0 skipped
hl-fleet-service full verify: 1721 tests，0 failures，0 errors，0 skipped
独立终审: P0=0，P1=0，P2=0
```

### 9.2 测试环境部署

- `hl-order-service-v3` Deploy Panel 任务 `29b541d6` 成功；`8086/8186` 双实例均启动并监听。
- `hl-fleet-service` Deploy Panel 任务 `aaee8d01` 成功；`8087/8187` 双实例均启动并监听。
- Nacos 已启用 `fleet.assign.requirement-lifecycle-enabled=true` 与 `fleet.feign.writeback.enabled=true`。
- 发布顺序按“Order-v3 全实例 -> Fleet 全实例 -> 开启需求级生命周期开关”执行，未跨过滚动发布护栏。

### 9.3 真实 API 与数据验收

使用独立车务账号和真实测试订单完成 DIRECT、HOLD、取消后迟到回调、同摘要重放、摘要变化刷新、非法参数及失效需求分支验收；未使用 `admin`、`wx` 或 Mock 数据。

公网网关 `https://api.test.1814.love:9443` 最终验证：

| 请求 | 结果 |
|---|---|
| 车务账号登录 | HTTP 200 |
| `GET /admin/fleet/board/summary` | HTTP 200，状态码/文案/数量由后端返回 |
| `GET /admin/fleet/board/orders?status=assigned&orderNo=...` | HTTP 200，精准返回 1 条 |
| `GET /admin/fleet/board/orders/{orderId}` | HTTP 200，返回逐日行程、车型诉求、司机确认凭证及当前派单 |
| `GET /admin/fleet/board/orders/{orderId}/timeline` | HTTP 200，返回完整操作时间线 |

HOLD 模式真实终态校验：

```json
{
  "requirementStatus": "DONE",
  "vehicleControlStatus": "DONE",
  "hasFleetAssigned": true,
  "snapshotCount": 1,
  "activeDailySlices": 6,
  "activeDailySliceStatus": "assigned",
  "driverConfirmationEvidenceCount": 1,
  "completionOutboxStatus": "SUCCESS"
}
```

取消订单迟到回调保持 `hasFleetAssigned=false`、快照数为 0、有效逐日派单数为 0；相同拓扑摘要在互斥窗口外重放返回 HTTP 200 且不重复推进待办、流水或订单状态，窗口内并发重复返回 `100502` 且无重复副作用。

### 9.4 OpenAPI 与日志

- Order-v3 OpenAPI 已公开内部最终回调及 `VehicleAssignmentCallbackReqVO` 的 6 个必填字段。
- Fleet OpenAPI 已公开创建、预检、取消、改派、最终确认、司机确认/拒绝、提前结束、需求驳回与撤销取消等 10 个生命周期接口。
- 2026-07-16 22:22 后四个目标实例均无 `ERROR` 级日志；目标订单与需求在四实例中均为 0 条 WARN/ERROR，日志可见司机确认、最终回调成功和 Order-v3 快照刷新。
- 测试环境另有保险 PDF 缺失与历史脏订单降级 WARN，未关联本次目标订单，不作为本契约成功响应的一部分。

Issue #4935 的代码、部署、网关 API、MySQL 终态和服务日志证据均已补齐，可按后端验收清单关单。

## 十、相关文档

- 当前看板字段、分页、统计、行程与保险：`57_4882_车务派单看板当前订单字段统计行程与保险事务收口-管理后台.md`
- 车务提需求与派单看板：`53_4871_车务提需求派单看板闭环契约-管理后台.md`
- 团号与定制师筛选：`54_4876_派单看板团号与定制师下拉筛选-管理后台.md`
- 后端最终回调契约：`hl-backend-changelog/changelogs/2026-07/14_1022_order-v3_vehicle-assignment-callback-contract.md`
