docs(changelog): #8114 司机拒接/退回待派记录留痕(fleet 修改接口)
changelog-filename-gate / validate (push) Failing after 2s

新增 fleet_assignment_operation_log 的 driver_rejected 操作类型,
detail_json 以锚点行 + clearedRows[] 形状记录清空前的车与司机身份。
前端若对 operation_type 做白名单过滤需加入该取值,否则静默漏渲染。

Refs wx/HL#8114

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot
2026-09-22 05:03:26 +08:00
共同撰写人 Claude Opus 5
父节点 71ad3dbfcd
当前提交 1e1f9e86e6
@@ -0,0 +1,420 @@
---
schema: "hl-changelog/v2"
ticket: "8114"
title: "派单操作时间线新增司机拒接记录留痕"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "backend_status=deployed: hl-fleet-service 测试服部署 sha=10ed12133,与本单 PR #8139 的合并提交完全一致,STATE=ok(该结论由管理者侧执行 deploy-status.sh 核验后转达,本会话未持有目标机器 SSH 权限、未亲自运行该脚本,如实披露)。gateway_status=not_required 特指网关路由层:driver-reject 写口与 operation-log 读口均为存量路由,本单未新增/改动任何 hl-gateway 路由配置;本会话对 driver-reject 写口做过真实网关实测(见八节,2026-09-22 04:00 前后),命中的是「非 holding 状态」守卫分支(605020),该守卫早于本单存在、与本单新增代码不重叠;受限于测试环境仅有 #5827 后新建的 assigned 态数据、没有存量 holding 派单,未能网关实测出「拒接成功→写入 driver_rejected→读侧可见」这条正向链路,该正向链路的验证依据是 AC-1/AC-2 的真库集成测试(H2 MODE=MySQL + 真 Flyway + 真 MyBatis + 真 AssignmentService Bean),非网关活测,已在三/八节如实注明并附逐字 dump。frontend_status=pending: 未获得 mmg 对 operation_type 白名单现状的新鲜核验,不认定 not_required。"
updated_at: "2026-09-22"
base: "dev-v3"
---
# fleet: 派单操作时间线新增司机拒接记录留痕
> **存放目录**: `changelogs-v2/2026-09/`
>
> **服务**: hl-fleet-service (端口 8003)
> **PR**: #8139
> **Issue**: #8114
> **日期**: 2026-09-22
> **影响范围**: 司机拒接/车务退回待派链路;派单操作时间线接口返回的操作记录
---
## ⚠️ 关键变化
司机拒接/车务退回待派(`POST /admin/fleet/assignments/{assignmentId}/driver-reject`,仅 `holding` 状态可调用,成功后派单回到 `unassigned`)以前在 `fleet_assignment` 表清空车辆/司机字段后不留任何痕迹。现已补齐写口留痕:`fleet_assignment_operation_log` 新增 `driver_rejected` 操作类型——与 #8068 已上线的 `soft_cleared` 是两个独立取值,不共用。
**前端影响**:派单看板订单卡片「查看日志」时间线会新增这类记录。如果前端按 `operation_type` 做了白名单过滤,`driver_rejected` 需要加进去——不加会静默漏渲染。
**覆盖边界**:`driver_rejected` 只在司机拒接/退回待派**成功执行**时写入,而该写口只接受 `holding` 状态的派单(组)。自 #5827 起,新建派单提交即派定,落库恒为 `assigned`(`holding` 仅剩发版前的存量数据)——因此在测试环境用新建的派单去调 `driver-reject` 会拿到 `605020`(本会话已实测复现,见八节),不会触发这条新留痕;能触发它的只有 #5827 发版前遗留、目前仍停留在 `holding` 的存量派单(组)。
---
## 一、背景
`fleet_assignment` 的 `vehicle_id` / `driver_id` 会因两类写口被清空:车务手动软清(#8068,已补齐留痕)与司机拒接/车务退回待派(本单)。此前拒接写口(`AssignmentService#doDriverRejectInLock`,覆盖 `updateDriverReject` 单派与 `updateDriverRejectGroup` 组派两条路径)在 CAS 清空成功后不写任何操作日志,事后无法查证「是谁拒的、拒掉的是哪位司机哪台车」——与 #8068 描述的是同一个洞,只是入口不同(工单 #8051 #8068 #8114)。
本次补齐留痕机制:每次拒接成功都在 `fleet_assignment_operation_log` 写一行 `operation_type = driver_rejected`,记录被清空前的身份快照及操作范围,形状与 #8068 的 `soft_cleared` 一致(锚点行 + `clearedRows[]`),但两者是**两个独立的枚举取值**——读侧 `AssignmentOperationLogQueryService` 没有 `operation_type` 白名单,共用会让「车务主动清空」与「司机拒接退回」在时间线上无法区分,而两者的责任归属不同。
触发本次留痕的写口端点本身**未变**:`POST /admin/fleet/assignments/{assignmentId}/driver-reject`(单派/组派共用同一端点,按当前派单是否属于某个派车组自动分流),请求体/响应结构/错误码均未调整。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 派单操作时间线 | GET | `/admin/fleet/orders/{orderId}/operation-log` | 响应新增操作类型 | 新增 `driver_rejected` 枚举值 + `detail_json` 字段扩展 |
---
## 三、接口详情
### 1. 派单操作时间线 `GET /admin/fleet/orders/{orderId}/operation-log`
**VO**: `FleetOperationLogItemVO → FleetOrderOperationLogRespVO`
#### 使用场景
车务在派车看板订单卡片内点「查看日志」,实时呈现该订单全部派车操作的不可变时间线。新操作类型 `driver_rejected` 会在司机拒接/车务退回待派成功执行后出现在此时间线中。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | ✅ | 雪花 ID | 订单 ID |
| page | Query | Long | ❌ | 默认 1,上限 100000 | 分页页码 |
| pageSize | Query | Long | ❌ | 默认 50,上限 200 | 每页条数 |
| keyword | Query | String | ❌ | 最长 32 | 按操作人/摘要模糊匹配 |
| sortBy | Query | String | ❌ | 如 `time,desc` / `time,asc` | 排序字段,默认按 create_time 倒序 |
| startDate / endDate | Query | LocalDateTime | ❌ | ISO 格式 `yyyy-MM-dd'T'HH:mm:ss` | 时间范围过滤 |
#### 出参 `Result<FleetOrderOperationLogRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| data.records | List<FleetOperationLogItemVO> | 当前页操作日志行 |
| data.records[].id | String | 日志行 ID(雪花号,以字符串返回防精度丢失) |
| data.records[].time | LocalDateTime | 操作时间 |
| data.records[].opType | String | 操作类型英文枚举值,**含新增的 `driver_rejected`** |
| data.records[].opTypeLabel | String | 操作类型中文标签(由后端从枚举翻译,`driver_rejected` → "司机拒接/退回待派") |
| data.records[].summary | String | 可读摘要(固定文案:"司机拒接/退回待派,清空车与司机,派车行置待选择") |
| data.records[].operatorName | String | 操作人(企微名优先,无则用户名,系统动作显示"系统") |
| data.records[].effectiveDate | LocalDate | 生效日期(取锚点行的 serviceDate) |
| data.records[].detailJson | String | 明细 JSON(字符串化),格式见下节 |
| data.total | Long | 总条数 |
| data.page | Integer | 当前页码 |
| data.pageSize | Integer | 本页条数 |
#### 新增字段详情
**`opType` 新增枚举值**
| 值 | 中文标签 | 触发场景 |
|----|----------|---------|
| `driver_rejected` | 司机拒接/退回待派 | 司机拒接本次排车锁定,或车务确认锁定无效,调用 `driver-reject` 端点成功清空车辆/司机字段 |
**`detailJson` 字段(当 `opType="driver_rejected"` 时)**
响应的 `detailJson` 是字符串化的 JSON。字段与顺序如下(取自真实集成测试落库结果,逐字未改;单派场景,`DriverRejectWritebackTraceIntegrationTest` AC-1):
```json
{"scope":"SINGLE","assignmentGroupId":"8114100001","statusBefore":"holding","rejectReason":"司机临时车辆抛锚,退回待派","clearedRowCount":1,"vehicleIdBefore":"8114300001","driverIdBefore":"8114400001","vehiclePlateSnapshot":"蒙A-81141","driverNameSnapshot":"拒接司机甲","clearedRows":[{"assignmentId":"8114100001","serviceDate":"2031-07-03","vehicleIdBefore":"8114300001","vehiclePlateBefore":"蒙A-81141","driverIdBefore":"8114400001","driverNameBefore":"拒接司机甲"}]}
```
组派场景(一次拒接影响整组多行,AC-2):
```json
{"scope":"GROUP","assignmentGroupId":"8114200001","statusBefore":"holding","rejectReason":"司机拒接整组行程,退回待派","clearedRowCount":2,"vehicleIdBefore":"8114300011","driverIdBefore":"8114400011","vehiclePlateSnapshot":"蒙A-81142","driverNameSnapshot":"拒接司机乙","clearedRows":[{"assignmentId":"8114100011","serviceDate":"2031-07-10","vehicleIdBefore":"8114300011","vehiclePlateBefore":"蒙A-81142","driverIdBefore":"8114400011","driverNameBefore":"拒接司机乙"},{"assignmentId":"8114100012","serviceDate":"2031-07-11","vehicleIdBefore":"8114300012","vehiclePlateBefore":"蒙A-81143","driverIdBefore":"8114400011","driverNameBefore":"拒接司机乙"}]}
```
字段说明(`LinkedHashMap` 插入顺序,来自 `AssignmentService#clearedIdentitySnapshot`):
| 字段 | 类型 | 说明 |
|------|------|------|
| scope | String | 拒接范围,`SINGLE` 单派、`GROUP` 按派车组整组拒接 |
| assignmentGroupId | String | 派车组 ID(字符串格式;见下方「关键说明 1」的类型对照) |
| statusBefore | String | 清空前状态,恒为 `holding`(拒接只放行 `holding → unassigned` 这一种迁移,非 `holding` 一律 605020,不写日志) |
| rejectReason | String | 拒接/退回原因,与请求体 `rejectReason` 同一份取值(仅存证,不参与任何判定) |
| clearedRowCount | Integer | 本次清空涉及的派车行数(单派恒为 1,组派为组内行数) |
| vehicleIdBefore | String | 清空前的车辆 ID(锚点行,字符串返回) |
| driverIdBefore | String | 清空前的司机 ID(锚点行,字符串返回) |
| vehiclePlateSnapshot | String | 清空前的车牌号(可读值快照) |
| driverNameSnapshot | String | 清空前的司机名(可读值快照) |
| clearedRows | Array | 本次实际被清空的每一行明细:`assignmentId` / `serviceDate` / `vehicleIdBefore` / `vehiclePlateBefore` / `driverIdBefore` / `driverNameBefore`(组派时逐切片各自的车/司机可能不同,不是锚点行的复制,见下方「关键说明 2」) |
**关键说明 1:所有 ID 字段都以字符串返回,但 DB 列类型不代表 JSON 字段类型**
`detail_json` 里的 `vehicleIdBefore` / `driverIdBefore` / `assignmentId` / `clearedRows[].assignmentId` 等 ID,均由后端 `AssignmentService#idText(Object)` 转换为字符串后再落 JSON(javadoc 原文:「可空 ID 转字符串(雪花 ID 落 JSON 防 JS 精度丢失;null 原样保留)」)。雪花 ID 是 19 位数字,JavaScript 的 `Number` 只能精确表示到 16 位,若以数字解析会在末位静默丢精度、无任何报错提示。
⚠️ **不要把 DB 列类型和 JSON 字段类型混为一谈**:`fleet_assignment_operation_log` 表的 `assignment_group_id` 列在数据库里是 `BIGINT NOT NULL`(这一列是这一行日志自身的分组归属,落值就是数字,不在 `detail_json` 里);而 `detail_json` 内部的 `assignmentGroupId` 键是经 `idText(...)` 转换后的字符串——两者字段名相近,是两个不同的东西。前端通过这个 JSON 接口只会消费到后者(字符串),不会直接看到 DB 列。
**关键说明 2:组拒接只写一行——锚点 + `clearedRows[]`,不是逐槽位各写一条**
一次组派拒接会清空组内全部行(上面示例是 2 行),但 `fleet_assignment_operation_log` 只 INSERT **一行**:锚点行(组内第一行)写 `operation_type=driver_rejected`,组内其余被清空的行不会各自再产生一条独立的日志行,它们清空前的身份只出现在这一行的 `detail_json.clearedRows[]` 数组里。
实测依据(AC-2):把查询条件放宽成只按 `order_id` 扫描整张表(不加 `assignment_id` 过滤、不加 `operation_type` 过滤),针对一次影响 2 行的组拒接,整单也只返回 **1 行**日志(`assignment_id` 为锚点行 `8114100011`),组内另一行 `8114100012` 名下没有独立的日志行。⇒ 前端如果按 `assignmentId` 去时间线表里找某一行派车行「自己的」拒接记录,组内非锚点行是找不到的,需要改为解析锚点行的 `clearedRows[]`。
**关键说明 3:若按 `operation_type` 白名单过滤,需要加入新取值**
时间线读侧(`AssignmentOperationLogQueryService`)没有 `operation_type` 白名单,新取值会自动随查询结果返回。但**如果前端渲染时自行对 `operation_type` 做了白名单过滤,必须把 `driver_rejected` 加入白名单**,否则这条记录会被过滤掉、静默漏渲染(不报错、不提示)。
**关键说明 4:新记录只出现在存量 `holding` 派单(组)被拒接时**
产生 `driver_rejected` 记录的前提是 `driver-reject` 写口调用**成功**,而该写口只接受 `holding` 状态的派单(组)——`FleetAssignmentMapper.updateDriverReject` / `updateDriverRejectGroup` 均带 `.eq(assignment_status, holding)` 的 CAS 条件,非 `holding` 一律返回 `605020`、不写任何日志。自 #5827 起新建派单提交即派定,落库恒为 `assigned`(`holding` 仅剩发版前的存量数据,`CreateAssignmentCommand` javadoc 原文:"落库目标态:#5827 起提交即派定,最终态恒 assigned(holding 只是同事务内的瞬时中间态)")。⇒ 在测试环境用新建的派单调用 `driver-reject` 会拿到 `605020`(本会话已实测复现,详见八节的真实网关响应),不会产生 `driver_rejected` 记录;要看到这条新记录的渲染效果,需要用 #5827 发版前遗留、目前仍处于 `holding` 的存量派单(组),或等真实司机拒接场景产生新数据——这是该端点自身的既有前置条件,不是本次改动新增的限制。
#### 请求示例
```http
GET /admin/fleet/orders/8114000001/operation-log?page=1&pageSize=50 HTTP/1.1
Host: {后台域名}
Authorization: Bearer {token}
Accept: application/json
```
#### 响应示例
以下 `records[0]` 按 `FleetOperationLogItemVO` 字段映射规则,从 AC-1 集成测试真实落库结果重新组装(`detailJson` 内容逐字符取自该次真库 IT 结果;`id`/`time`/`effectiveDate` 取自同一行的 `operation_id`/`create_time`/`effective_date`;`operatorName` 为该 IT 用例夹具下的 `actor_name` 原值「系统」——集成测试未经过网关鉴权链路、没有设置 `operator_id`,真实生产场景下这里通常是执行拒接操作的车务人员姓名,机制与其他写口的 `operatorName` 完全一致,本单未改动。整份响应信封本身未经网关活捕获,参见八节的受限说明):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"records": [
{
"id": "2102121618417971202",
"time": "2026-09-22T03:43:44",
"opType": "driver_rejected",
"opTypeLabel": "司机拒接/退回待派",
"summary": "司机拒接/退回待派,清空车与司机,派车行置待选择",
"operatorName": "系统",
"effectiveDate": "2031-07-03",
"detailJson": "{\"scope\":\"SINGLE\",\"assignmentGroupId\":\"8114100001\",\"statusBefore\":\"holding\",\"rejectReason\":\"司机临时车辆抛锚,退回待派\",\"clearedRowCount\":1,\"vehicleIdBefore\":\"8114300001\",\"driverIdBefore\":\"8114400001\",\"vehiclePlateSnapshot\":\"蒙A-81141\",\"driverNameSnapshot\":\"拒接司机甲\",\"clearedRows\":[{\"assignmentId\":\"8114100001\",\"serviceDate\":\"2031-07-03\",\"vehicleIdBefore\":\"8114300001\",\"vehiclePlateBefore\":\"蒙A-81141\",\"driverIdBefore\":\"8114400001\",\"driverNameBefore\":\"拒接司机甲\"}]}",
"changeDetail": null
}
],
"total": 1,
"page": 1,
"pageSize": 50
}
}
```
#### 空数据 / 降级响应
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 50
}
}
```
#### 错误响应
```json
{
"code": 404,
"message": "订单不存在",
"success": false,
"data": null
}
```
#### 业务边界
- **权限**:网关 `/admin/fleet/**` 统一鉴权,车务/管理员可访问
- **分页上限**:pageSize 最高 200,page 最高 100000
- **ID 精度**:所有雪花号均以字符串返回,前端切勿转为 Number 类型
- **operation_type 白名单**:若前端自行维护白名单过滤时间线渲染,必须加入 `driver_rejected`,否则静默漏渲染
- **组拒接留痕形状**:一次组拒接只在 `fleet_assignment_operation_log` 里产生一行(锚点行),组内其余行的清空前身份只存在于该行 `detail_json.clearedRows[]` 数组内,不能按 `assignmentId` 直接从时间线表里查到独立行
- **可达性边界**:`driver_rejected` 只在拒接 `holding` 状态的存量派单(组)成功时产生;测试环境新建的派单调用 `driver-reject` 恒得 `605020`,不会产生这条记录
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
此接口为只读 GET,无请求体。正确调用示例:
| 场景 | URL |
|------|-----|
| ✅ 第一页,默认排序 | `GET /admin/fleet/orders/{orderId}/operation-log?page=1&pageSize=50` |
| ✅ 处理 detailJson 字符串 | `JSON.parse(record.detailJson)` 转为对象后访问字段 |
| ❌ ID 作为 Number | `parseInt(record.id)` / `Number(detail.vehicleIdBefore)` 会导致末位精度丢失 |
| ❌ 按 assignmentId 直查组内非锚点行的独立日志 | 组内非锚点行没有独立日志行,需解析锚点行的 `clearedRows[]` |
### 处理 detailJson 的正确方式
```javascript
// ✅ 正确:先 parse,再按需读取 driver_rejected 专属字段
const detail = JSON.parse(record.detailJson);
if (record.opType === 'driver_rejected') {
const rejectReason = detail.rejectReason; // 拒接原因
const clearedRows = detail.clearedRows; // 组内逐行明细,SINGLE 场景长度恒为 1
const vehicleId = detail.vehicleIdBefore; // String,保持精度,不转 Number
}
// ❌ 禁止转数字
const id = Number(detail.vehicleIdBefore); // 末位被四舍五入
```
### operation_type 白名单排查清单(若前端有)
- [ ] 渲染时间线的组件是否存在 `operation_type` 白名单/枚举映射?
- [ ] 若存在,是否已加入 `driver_rejected` → "司机拒接/退回待派"?
- [ ] 图标/颜色映射表是否需要为 `driver_rejected` 配一个默认展示(未配置时不应崩溃或空白)?
---
## 五、数据库行为
| 场景 | 数据库表 | 操作 |
|------|----------|------|
| 司机拒接(单派)执行成功 | `fleet_assignment` | UPDATE `vehicle_id = null, driver_id = null` 等字段(CAS 条件 `assignment_status = holding`) |
| 司机拒接(组派)执行成功 | `fleet_assignment` | 按 `assignment_group_id` 批量 UPDATE 组内多行,同上 CAS 条件 |
| 拒接留痕写口 | `fleet_assignment_operation_log` | INSERT 一行,`operation_type = 'driver_rejected'`,`detail_json` 记录清空前的身份(锚点行 1 条,覆盖整组) |
---
## 六、边界行为
- **派单不存在** → `605009`
- **非 `holding` 状态** → `605020`(不允许退回待派;自 #5827 起新建派单落库恒为 `assigned`,`holding` 仅剩发版前的存量数据——对新建派单调用会必得 `605020`,这不是缺陷,见「关键说明 4」)
- **`rejectReason` 为空** → `400001`
- **旧 HOLD 通知结果正在确认中** → `605042`(可稍后重试;此次不改变派单状态、不清空车辆/司机身份、不改变资源占用,也不写留痕)
- **无鉴权** → `401`(网关拦截);**权限不足**(非车务/管理员)→ `403`
- **历史数据**:本次新增的 `driver_rejected` 仅出现在部署后发生的拒接成功操作中,不影响此前已有的操作日志记录
---
## 六.5、枚举 / 数据字典
### operation_type 枚举(`AssignmentOperationTypeEnum`)
**所属字段**: `FleetOperationLogItemVO.opType` | **类型**: `String`
新增值:
| 值 | 中文标签 | 说明 |
|----|----------|------|
| `driver_rejected` | 司机拒接/退回待派 | 司机拒接锁定或车务确认锁定无效,调用 `driver-reject` 成功后清空派车行的车辆/司机字段 |
现有值(不含全列表,仅示例,与 `driver_rejected` 语义最接近的一项一并列出对照):
| 值 | 中文标签 | 说明 |
|----|----------|------|
| `assignment_created` | 新建派单 | 订单新建派单时 |
| `change_completed` | 修改派单完成 | 改派操作完成 |
| `soft_cleared` | 清空司机/车辆 | 车务手动清空派车行的车辆/司机字段(#8068,与 `driver_rejected` 是同形状但独立的取值,不共用) |
| `confirmed` | 确认执行 | 司机确认执行派单 |
| `completed` | 完结派单 | 派单完成 |
---
## 六.6、修改前后对比
### 响应字段级对比
| 字段 | 改前 | 改后 | 备注 |
|------|------|------|------|
| `records[].opType` | 不含 `driver_rejected` | 新增 `driver_rejected` | 前端若按白名单过滤需要加入 |
| `records[].detailJson` | 已有其他操作类型的内容 | 新增 `opType="driver_rejected"` 时的结构 | 含 `rejectReason` 等专属字段,见三节 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 司机拒接/退回待派留痕 | 派车行 `vehicle_id`/`driver_id` 直接清空,无操作日志 | 调用 `driver-reject` 成功后同事务在 `fleet_assignment_operation_log` 写一行,记录清空前的车/司机 |
| 时间线查询 | 拒接操作不可见 | 拒接操作以 `driver_rejected` 行显示在时间线中 |
| 事后取证 | 无法查证谁拒的、拒掉了谁 | `detail_json` 中记录清空前的 vehicleId/driverId 等快照,支持事后追溯 |
---
## 六.7、影响评估
- **是否破坏向后兼容**:否
- 新增操作类型 `driver_rejected` 不影响既有类型的解析
- 响应字段无删除,仅新增返回内容(仅当 `opType=driver_rejected` 时出现新的 `detail_json` 结构)
- **前端是否必须同步上线**:否(但需适配白名单过滤)
- 后端接口变更无必须的前端代码改动
- **但是**:如果前端渲染时间线时对 `operation_type` 做了白名单过滤,白名单中**必须加入 `driver_rejected`**,否则该操作会静默漏渲染
- **前端 workaround 清理点**:
- 若有硬编码的 `operation_type` 白名单,需补充 `'driver_rejected'`
- 若用了枚举常量或字典,确保下发的字典已包含 `driver_rejected` 标签
- **覆盖边界(前端自测数据来源受限,需知悉)**:
- `driver_rejected` 只在「存量 `holding` 派单(组)」被拒接成功时产生。自 #5827 起,新建派单提交即派定,落库恒为 `assigned`——因此**对着测试环境新建的派单调用 `driver-reject` 会返回 `605020`,不会产生这条新记录**;能触发它的只有 #5827 发版前遗留、目前仍处于 `holding` 的存量派单(组)。
- 前端要验证 `driver_rejected` 的渲染效果,需要去找这类存量数据(或等真实司机拒接场景产生新数据),而不是自己新建一条派单来复现——这是该端点自身的既有前置条件(`holding` 状态限定,非本单引入),不是本次改动新增的限制。
---
## 七、不影响范围
- **仅影响**:派单操作时间线接口 `/admin/fleet/orders/{orderId}/operation-log` 的响应内容
- **零影响**:
- `driver-reject` 端点自身的请求体/响应结构/错误码(一律未改)
- `updateDriverReject` / `updateDriverRejectGroup` 两个 Mapper default 方法(一行未改)
- 派车创建/改派/确认等其他业务流程
- 订单详情接口
- 其他模块的操作日志接口(如房务)
- 前端派车列表/派车详情等其他功能模块
---
## 八、测试环境已验证
**后端部署**:`hl-fleet-service` 测试服部署 sha `10ed12133`,与本单 PR #8139 的合并提交完全一致,`STATE=ok`。⚠️ 该结论由管理者侧执行 `deploy-status.sh` 核验后转达,本会话未持有目标机器的 SSH 访问权限、未亲自运行该脚本,如实披露。
**网关实测(本会话 2026-09-22 04:00 前后,真实发起,非构造)**:
对一条刚通过 `POST /admin/fleet/assignments/batch` 新建、状态为 `assigned` 的派单(`assignmentId=2102125764705181697`,04:00:12 创建)调用拒接端点:
```
POST /admin/fleet/assignments/2102125764705181697/driver-reject
↓
HTTP 200,code=605020,msg=当前派单状态不允许此操作
```
这与「关键说明 4」描述的可达性边界完全吻合:新建派单落库恒为 `assigned`,非 `holding` 一律 `605020`。同时执行了两组反例校验:
```
POST /admin/fleet/assignments/2102125764705181697999/driver-reject(ID 格式非法)
↓ HTTP 200,code=400,msg=参数 assignmentId 格式错误,请检查后重试
POST /admin/fleet/assignments/2102125764705181698/driver-reject(格式合法但不存在)
↓ HTTP 200,code=605009,msg=派单不存在
```
调用前后对该派单行与 `fleet_assignment_operation_log` 的 SELECT 复核:拒接调用前后该行 `vehicle_id`/`driver_id`/`assignment_status` 均无变化(`assigned`,`2085539421276286978`/`2065272150012444674`),`operation_type='driver_rejected'` 的行数前后均为 0——确认失败调用没有副作用、也没有写出留痕,符合预期。
**受限说明**:本会话测试环境内没有可用的存量 `holding` 派单(组),因此上面这组网关实测只覆盖了 `driver-reject` 的失败分支,**没有**现场网关实测出「拒接成功 → 写入 `driver_rejected` → 时间线读侧可见」这条正向链路(该限制本身正是「关键说明 4」所述的可达性边界,不是本次验证的疏漏)。正向链路的验证依据是集成测试真库取证(见下):
用例:`DriverRejectWritebackTraceIntegrationTest`(`test` profile 的 H2 `MODE=MySQL` + 真 Flyway DDL + 真 MyBatis 生成 SQL + 真 `AssignmentService` Bean,非 MySQL 8 容器)。
- 单派(AC-1):拒接前该行 `vehicle_id=8114300001, driver_id=8114400001, assignment_status=holding`;拒接后同一行 `vehicle_id=null, driver_id=null, assignment_status=unassigned`;同时写出一行 `fleet_assignment_operation_log`(`operation_id=2102121618417971202, operation_type=driver_rejected`),其 `detail_json.vehicleIdBefore="8114300001"` / `.driverIdBefore="8114400001"` 与清空前的 DB 值一致——排除了「UPDATE 之后再 reload 导致全 null」这一最强反例。
- 组派(AC-2):两个切片清空前分别持有不同的车/司机(`8114300011/8114400011` 与 `8114300012/8114400011`),拒接后按 `order_id` 全表扫描整单只返回 **1 行**日志(锚点 `assignment_id=8114100011`),第二个切片 `8114100012` 名下无独立日志行,两切片各自的清空前身份分别体现在锚点行 `detail_json.clearedRows[0]` 与 `[1]` 里——确认了「关键说明 2」所述的锚点+`clearedRows[]`留痕形状。
三节的响应示例即按上述 AC-1 真库 IT 结果、按 `FleetOperationLogItemVO` 字段映射规则重新组装(非网关活捕获,已在三节标注)。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8114](https://git.1814.love:8443/wx/HL/issues/8114)
- 关联 PR: [wx/HL#8139](https://git.1814.love:8443/wx/HL/pulls/8139)
- 相关工单: [#8068](https://git.1814.love:8443/wx/HL/issues/8068)(软清留痕,同形状参照实现),[#5827](https://git.1814.love:8443/wx/HL/issues/5827)(取消司机确认环节,holding 状态自此变为遗留态)
---
## 关联 / 联系人
### 链接
- **Issue**: [#8114](https://git.1814.love:8443/wx/HL/issues/8114)
- **PR**: [#8139](https://git.1814.love:8443/wx/HL/pulls/8139)
- **Merge commit**: [10ed12133](https://git.1814.love:8443/wx/HL/commit/10ed12133)
### 联系人
- **后端负责人**: @wx