From 1e1f9e86e6e60819d14556026ad1c98e852d2bae Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Tue, 22 Sep 2026 05:03:26 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#8114=20=E5=8F=B8=E6=9C=BA?= =?UTF-8?q?=E6=8B=92=E6=8E=A5/=E9=80=80=E5=9B=9E=E5=BE=85=E6=B4=BE?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=95=99=E7=97=95=EF=BC=88fleet=20=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E6=8E=A5=E5=8F=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 fleet_assignment_operation_log 的 driver_rejected 操作类型, detail_json 以锚点行 + clearedRows[] 形状记录清空前的车与司机身份。 前端若对 operation_type 做白名单过滤需加入该取值,否则静默漏渲染。 Refs wx/HL#8114 Co-Authored-By: Claude Opus 5 (1M context) --- ...机拒接退回待派记录留痕-修改接口-管理后台.md | 420 ++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 changelogs-v2/2026-09/22_8114_司机拒接退回待派记录留痕-修改接口-管理后台.md diff --git a/changelogs-v2/2026-09/22_8114_司机拒接退回待派记录留痕-修改接口-管理后台.md b/changelogs-v2/2026-09/22_8114_司机拒接退回待派记录留痕-修改接口-管理后台.md new file mode 100644 index 00000000..c02c50b8 --- /dev/null +++ b/changelogs-v2/2026-09/22_8114_司机拒接退回待派记录留痕-修改接口-管理后台.md @@ -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` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data.records | List | 当前页操作日志行 | +| 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