diff --git a/changelogs-v2/2026-09/21_8068_车务手动软清派车行记录留痕-修改接口-管理后台.md b/changelogs-v2/2026-09/21_8068_车务手动软清派车行记录留痕-修改接口-管理后台.md new file mode 100644 index 00000000..b046d23c --- /dev/null +++ b/changelogs-v2/2026-09/21_8068_车务手动软清派车行记录留痕-修改接口-管理后台.md @@ -0,0 +1,414 @@ +--- +schema: "hl-changelog/v2" +ticket: "8068" +title: "派单操作时间线新增软清记录留痕" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "" +updated_at: "2026-09-21" +base: "dev-v3" +--- + +# fleet: 派单操作时间线新增软清记录留痕 + +> **存放目录**: `changelogs-v2/2026-09/` +> +> **服务**: hl-fleet-service (端口 8003) +> **PR**: #8113 +> **Issue**: #8068 +> **日期**: 2026-09-21 +> **影响范围**: 车务手动派车清空链路;派单操作时间线接口返回的操作记录 + +--- + +## ⚠️ 关键变化 + +车务手动软清派车行(清空所选行的车辆/司机),以前在 `fleet_assignment` 表直接清空不留痕迹。现已补齐写口留痕:`fleet_assignment_operation_log` 新增 `soft_cleared` 操作类型。 + +**前端影响**:派单看板订单卡片「查看日志」时间线会新增这类记录。如果前端按 `operation_type` 做了白名单过滤,`soft_cleared` 需要加进去——不加会静默漏渲染。 + +--- + +## 一、背景 + +软清是车务人员在派车界面一次性清空多个选中行的车辆/司机字段,状态置回「待改派」。此前的实现直接 UPDATE 字段不写日志,导致事后无法查证「是谁、什么时候、清掉了哪个司机/哪台车」(工单 #8051 #8068 提及)。 + +本次补齐留痕机制:每次软清都在 `fleet_assignment_operation_log` 写一行,记录被清空前的身份快照及操作范围。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 派单操作时间线 | GET | `/admin/fleet/orders/{orderId}/operation-log` | 响应新增操作类型 | 新增 `soft_cleared` 枚举值 + `detail_json` 字段扩展 | + +--- + +## 三、接口详情 + +### 1. 派单操作时间线 `GET /admin/fleet/orders/{orderId}/operation-log` + +**VO**: `FleetOperationLogItemVO → FleetOrderOperationLogRespVO` + +#### 使用场景 + +车务在派车看板订单卡片内点「查看日志」,实时呈现该订单全部派车操作的不可变时间线。新操作类型 `soft_cleared` 会在此时间线中出现。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Path | Long | ✅ | 雪花 ID | 订单 ID | +| page | Query | Integer | ❌ | 默认 1,上限 100000 | 分页页码 | +| pageSize | Query | Integer | ❌ | 默认 50,上限 200 | 每页条数 | +| sortBy | Query | String | ❌ | 支持 `time,asc` | 排序字段,默认 create_time 倒序 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| data.records | List | 当前页操作日志行 | +| data.records[].id | String | 日志行 ID(雪花号,以字符串返回防精度丢失) | +| data.records[].time | LocalDateTime | 操作时间(ISO 8601 格式) | +| data.records[].opType | String | 操作类型英文枚举值,**含新增的 `soft_cleared`** | +| data.records[].opTypeLabel | String | 操作类型中文标签(由后端从枚举翻译) | +| data.records[].summary | String | 可读摘要(后端拼接:操作人 + 动作 + 前后值对比) | +| data.records[].operatorName | String | 操作人(企微名优先,无则用户名,系统动作显示「系统」) | +| data.records[].effectiveDate | LocalDate | 生效日期(按天改派等动作有值,无则 null) | +| data.records[].detailJson | String | 明细 JSON,格式见下节 | +| data.total | Long | 总条数 | +| data.page | Integer | 当前页码 | +| data.pageSize | Integer | 本页条数 | + +#### 新增字段详情 + +**`opType` 新增枚举值** + +| 值 | 中文标签 | 触发场景 | +|----|----------|---------| +| `soft_cleared` | 清空司机/车辆 | 车务在派车界面手动清空选中行的车辆/司机字段 | + +**`detailJson` 字段(当 `opType="soft_cleared"` 时)** + +响应的 `detailJson` 是字符串化的 JSON,结构如下(示例已脱敏保留字段结构): + +```json +{ + "scope": "GROUP", + "assignmentGroupId": "360380081354444801", + "statusBefore": "assigned", + "clearedRowCount": 1, + "vehicleIdBefore": "2085539421276286978", + "driverIdBefore": "2067084362829979650", + "vehiclePlateSnapshot": "蒙A-E2E99", + "driverNameSnapshot": "王信", + "clearedRows": [ + { + "assignmentId": "2101990265092984833", + "serviceDate": "2026-12-22", + "vehicleIdBefore": "2085539421276286978", + "vehiclePlateBefore": "蒙A-E2E99", + "driverIdBefore": "2067084362829979650", + "driverNameBefore": "王信" + } + ] +} +``` + +字段说明: + +| 字段 | 类型 | 说明 | +|------|------|------| +| scope | String | 清空范围,`GROUP` 表示按派车组(团期)整组清空 | +| assignmentGroupId | String | 派车组 ID(字符串格式,防雪花 ID 过 JS 掉精度) | +| statusBefore | String | 清空前状态,通常为 `assigned`(已派车) | +| clearedRowCount | Integer | 本次清空涉及的派车行数 | +| vehicleIdBefore | String | **清空前的车辆 ID**(19 位雪花号,字符串返回) | +| driverIdBefore | String | **清空前的司机 ID**(19 位雪花号,字符串返回) | +| vehiclePlateSnapshot | String | 清空前的车牌号(可读值快照,用于日志展示) | +| driverNameSnapshot | String | 清空前的司机名(可读值快照,用于日志展示) | +| clearedRows | Array | 本次清空涉及的所有派车行详情,每行包含 assignmentId / serviceDate / 清空前的车辆/司机 ID 与车牌/司机名 | + +**关键说明:所有 ID 字段都以字符串返回** + +派车 ID、司机 ID、车辆 ID 等均采用字符串格式。**这很重要**:JavaScript 的 `Number` 类型只能精确表示到 16 位数字,而雪花 ID 是 19 位。若以数字解析,末位会被四舍五入,导致 ID 失真且无任何错误提示。 + +#### 请求示例 + +```http +GET /admin/fleet/orders/2085539421276286978/operation-log?page=1&pageSize=50 HTTP/1.1 +Host: {后台域名} +Authorization: Bearer {token} +Accept: application/json +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "records": [ + { + "id": "2101990265092984834", + "time": "2026-09-21T18:45:32", + "opType": "soft_cleared", + "opTypeLabel": "清空司机/车辆", + "summary": "车务 王信 清空司机/车辆:蒙A-E2E99王信,派车行 1 行待改派", + "operatorName": "王信", + "effectiveDate": null, + "detailJson": "{\"scope\":\"GROUP\",\"assignmentGroupId\":\"360380081354444801\",\"statusBefore\":\"assigned\",\"clearedRowCount\":1,\"vehicleIdBefore\":\"2085539421276286978\",\"driverIdBefore\":\"2067084362829979650\",\"vehiclePlateSnapshot\":\"蒙A-E2E99\",\"driverNameSnapshot\":\"王信\",\"clearedRows\":[{\"assignmentId\":\"2101990265092984833\",\"serviceDate\":\"2026-12-22\",\"vehicleIdBefore\":\"2085539421276286978\",\"vehiclePlateBefore\":\"蒙A-E2E99\",\"driverIdBefore\":\"2067084362829979650\",\"driverNameBefore\":\"王信\"}]}", + "changeDetail": null + }, + { + "id": "2101990265092984833", + "time": "2026-09-21T18:45:31", + "opType": "assignment_created", + "opTypeLabel": "新建派单", + "summary": "系统 新建派单:蒙A-E2E99王信,生效日 12/22", + "operatorName": "系统", + "effectiveDate": "2026-12-22", + "detailJson": null, + "changeDetail": null + } + ], + "total": 2, + "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 +- **排序**:默认按 create_time 倒序(新操作在前);支持 `sortBy=time,asc` 升序 +- **ID 精度**:所有雪花号均以字符串返回,前端切勿转为 Number 类型 +- **历史数据兼容**:该接口是只读时间线,查询时包含所有 `operation_type` 值(包括本次新增的 `soft_cleared`) + +--- + +## 四、契约约束与正确调用方式 + +### ✅ 正确 / ❌ 错误 payload 对照 + +此接口为只读 GET,无请求体。正确调用示例: + +| 场景 | URL | +|------|-----| +| ✅ 第一页,默认排序 | `GET /admin/fleet/orders/2085539421276286978/operation-log?page=1&pageSize=50` | +| ✅ 升序时间线 | `GET /admin/fleet/orders/2085539421276286978/operation-log?sortBy=time,asc&pageSize=50` | +| ✅ 处理 detailJson 字符串 | `JSON.parse(record.detailJson)` 转为对象后访问字段 | +| ❌ ID 作为 Number | `parseInt(record.id)` 会导致末位精度丢失 | +| ❌ 分页超过上限 | `pageSize=500` → 400 Bad Request | + +### 处理 detailJson 的正确方式 + +`detailJson` 字段值本身是 JSON 字符串(因为在 SQL 层 `detail_json` 列是 text 类型,序列化到 JSON 响应时被转义成字符串)。前端需要先 parse 再访问: + +```javascript +// ❌ 错误:直接访问 +const vehicleId = record.detailJson.vehicleIdBefore; // undefined + +// ✅ 正确:先 parse +const detail = JSON.parse(record.detailJson); +const vehicleId = detail.vehicleIdBefore; // "2085539421276286978" + +// ✅ 保持字符串,不转 Number +const id = detail.vehicleIdBefore; // String,保持精度 +// ❌ 禁止转数字 +const id = Number(detail.vehicleIdBefore); // 末位被四舍五入 +``` + +--- + +## 五、数据库行为 + +| 场景 | 数据库表 | 操作 | +|------|----------|------| +| 软清执行 | `fleet_assignment` | UPDATE `driver_id = null, vehicle_id = null` 等字段 | +| 软清写口 | `fleet_assignment_operation_log` | INSERT 一行,`operation_type = 'soft_cleared'`,`detail_json` 记录清空前的身份 | + +--- + +## 六、边界行为 + +- **订单不存在** → 404 +- **分页参数超范围** → 400(pageSize > 200 或 page > 100000) +- **无鉴权** → 401(网关拦截) +- **权限不足** → 403(非车务/管理员) +- **sortBy 值非法** → 忽略,使用默认倒序 +- **历史数据**:该接口返回的是操作日志的完整历史,不因本次变更而改变已有记录;本次新增的 `soft_cleared` 仅出现在 2026-09-21 18:43 之后发生的清空操作 + +--- + +## 六.5、枚举 / 数据字典 + +### operation_type 枚举(`AssignmentOperationTypeEnum`) + +**所属字段**: `FleetOperationLogItemVO.opType` | **类型**: `String` + +新增值: + +| 值 | 中文标签 | 说明 | +|----|----------|------| +| `soft_cleared` | 清空司机/车辆 | 车务手动清空派车行的车辆/司机字段,状态置回待改派 | + +现有值(不含全列表,仅示例): + +| 值 | 中文标签 | 说明 | +|----|----------|------| +| `assignment_created` | 新建派单 | 订单新建派单时 | +| `change_completed` | 修改派单完成 | 改派操作完成 | +| `share_release_cleared` | 共用关系解除释放占用 | 共用关系解除时,共用组成员释放占用(#8061 新增) | +| `confirmed` | 确认执行 | 司机确认执行派单 | +| `completed` | 完结派单 | 派单完成 | + +--- + +## 六.6、修改前后对比 + +### 响应字段级对比 + +| 字段 | 改前 | 改后 | 备注 | +|------|------|------|------| +| `records[].opType` | 不含 `soft_cleared` | 新增 `soft_cleared` | 前端若按白名单过滤需要加入 | +| `records[].detailJson` | 已有其他操作类型的内容 | 新增 `soft_cleared` 时的 detail_json 结构 | 当 opType 为 `soft_cleared` 时,detailJson 包含 scope/statusBefore/vehicleIdBefore 等字段 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 软清留痕 | 派车行直接清空,无操作日志 | 调用软清写口时同步在 `fleet_assignment_operation_log` 写一行,记录被清空前的车/司机 | +| 时间线查询 | 软清操作不可见 | 软清操作以 `soft_cleared` 行显示在时间线中 | +| 事后取证 | 无法查证谁清了、清掉了谁 | `detail_json` 中记录清空前的 vehicleId/driverId 快照,支持事后追溯 | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否 + - 新增操作类型 `soft_cleared` 不影响既有操作类型的解析 + - 响应字段无删除,仅新增返回内容 + +- **前端是否必须同步上线**: 否(但需适配白名单过滤) + - 后端接口变更无必须的前端代码改动 + - **但是**:如果前端在渲染时间线时对 `operation_type` 做了白名单过滤,白名单中**必须加入 `soft_cleared`**,否则该操作会静默漏渲染 + +- **前端 workaround 清理点**: + - 若有硬编码的 operation_type 白名单(例如 `['assignment_created', 'change_completed', ...]`),需补充 `'soft_cleared'` + - 若用了枚举常量或字典,确保后台下发的字典已包含 `soft_cleared` 标签 + +--- + +## 七、不影响范围 + +- **仅影响**:派单操作时间线接口 `/admin/fleet/orders/{orderId}/operation-log` 的响应内容 +- **零影响**: + - 派车创建/改派/确认等业务流程 + - 订单详情接口 + - 派车行状态字段(清空行为本身不变,仍是直接 UPDATE 派车表) + - 其他模块的操作日志接口(如房务) + - 前端派车列表/派车详情等其他功能模块 + +--- + +## 八、测试环境已验证 + +测试服 `hl-fleet-service` 版本 `dev-v3@9f6443de4`(2026-09-21 18:46 部署,STATE=ok) + +实测验证(订单 `2085539421276286978`、派车行已清空): + +``` +GET /admin/fleet/orders/2085539421276286978/operation-log?page=1&pageSize=50 + ↓ +200 OK + +响应中 records[0]: + { + "opType": "soft_cleared", + "opTypeLabel": "清空司机/车辆", + "operatorName": "王信", + "time": "2026-09-21T18:45:32", + "detailJson": "{\"scope\":\"GROUP\",\"assignmentGroupId\":\"360380081354444801\",\"statusBefore\":\"assigned\",\"vehicleIdBefore\":\"2085539421276286978\",\"driverIdBefore\":\"2067084362829979650\",\"vehiclePlateSnapshot\":\"蒙A-E2E99\",\"driverNameSnapshot\":\"王信\", ...}", + ... + } + ✓ opType 正确为 soft_cleared + ✓ detailJson 包含清空前的 vehicleId/driverId(字符串格式) + ✓ 快照字段 vehiclePlateSnapshot/driverNameSnapshot 可读 +``` + +--- + +## 九、特别说明:共用关系解除时的双行记录 + +共用关系解除(#8061)会在同一次操作中产生**两条连续的操作日志**: + +1. **`share_release_cleared`** — 由共用关系解除的调用点写,答「哪个共用关系、解除原因、采用何种幸存者策略」 + - `detail_json` 含 `shareGroupId` / `releaseReason` / `survivorPolicy` / `vehicleIdBefore` 等字段 + +2. **`soft_cleared`** — 由软清写口本身写,答「派车行被清了、清前是什么」(本次新增) + - `detail_json` 含 `scope` / `statusBefore` / `vehicleIdBefore` / `driverIdBefore` / `clearedRows` 等字段 + +**这不是重复数据**——两行记的是同一次操作的不同侧面,各自的 `detail_json` 内容完全不同,都有独特的业务含义。前端在渲染时间线时需要正确识别两种类型,包括在任何过滤/搜索逻辑中都要同时考虑这两个 operation_type。 + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#8068](https://git.1814.love:8443/wx/HL/issues/8068) +- 关联 PR: [wx/HL#8113](https://git.1814.love:8443/wx/HL/pulls/8113) +- 相关工单: [#8061](https://git.1814.love:8443/wx/HL/issues/8061)(共用关系解除),[#8051](https://git.1814.love:8443/wx/HL/issues/8051)(库内取证需求) + +--- + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#8068](https://git.1814.love:8443/wx/HL/issues/8068) +- **PR**: [#8113](https://git.1814.love:8443/wx/HL/pulls/8113) +- **Merge commit**: [9f6443de4](https://git.1814.love:8443/wx/HL/commit/9f6443de4) + +### 联系人 + +- **后端负责人**: @wx