diff --git a/changelogs-v2/2026-09/20_5935_改期残留清理用现有读写口即可-前端缺陷-管理后台.md b/changelogs-v2/2026-09/20_5935_改期残留清理用现有读写口即可-前端缺陷-管理后台.md new file mode 100644 index 00000000..4b17bd4d --- /dev/null +++ b/changelogs-v2/2026-09/20_5935_改期残留清理用现有读写口即可-前端缺陷-管理后台.md @@ -0,0 +1,518 @@ +--- +schema: "hl-changelog/v2" +ticket: "5935" +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: "前端 2026-09-20 反馈清单第4项引用了一个不存在的写口 POST .../slots/{slotId}/clear-residue-dates(该端点已随 #7067 去槽位化整体删除,origin/dev-v3 全仓零命中),并要求后端在读侧新增 residueServiceDates 字段。核实:读侧 cells[].rescheduleResidue 已能推导出该日期子集,写侧 DELETE /{assignmentId} 与 POST /batch 两条既有端点已覆盖单日/整批清理,本次零后端代码改动。POST /batch 对已过去的残留日期有硬门禁会拦截,DELETE 是否放行未取证,本文档已如实标注【需确认】。" +updated_at: "2026-09-20" +base: "dev-v3" +--- + +# 车务派单:改期残留清理用现有读写口即可(用法说明,零代码改动) + +> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/` +> **服务**: hl-fleet-service (端口 8082) +> **PR**: 无(零代码改动,未产生新 PR) +> **Issue**: #5935(原始功能单,2026-08-16 已上线并随 #7067 部分退役;本文档是第三次说明) +> **日期**: 2026-09-20 +> **影响范围**: 管理后台车务排车 Step2 弹窗「改期残留清理」交互的前端实现方式;不改后端代码、不改网关路由 + +--- + +## 给前端的一句话结论 + +你要的能力**今天就已经有**:可勾选的残留日期数组不需要后端新增字段,从现有 `candidates` 响应的 `cells[]` 里按 `rescheduleResidue === true` 过滤即可拿到;清理动作也不需要等新接口,逐日清理用 `DELETE /admin/fleet/assignments/{assignmentId}`、整批清理用 `POST /admin/fleet/assignments/batch`(不提交残留日期项即视为清理)。**但你在清单里引用的那个写口 `POST .../slots/{slotId}/clear-residue-dates` 不存在**——它已经随 #7067 去槽位化被整体删除,请改用下面两条现成的写口。 + +--- + +## ⚠️ 关键变化 + +- **本次没有新增、修改或删除任何接口**,后端零代码改动。 +- 前端诉求引用的写口 `POST .../slots/{slotId}/clear-residue-dates` **在 `origin/dev-v3` 全仓零命中**(`clear-residue-dates` / `clearResidueDates` / `cancel-residue` 均零命中)。`AssignmentControllerTest.java:938` 留有明确注释:「#7067:cancel-residue 端点已随槽位概念退役删除」——**「槽位(slot)」这个概念本身已经退役**,基于 `slotId` 的写口不会再出现。 +- 前端要求新增的读侧字段 `residueServiceDates`**不需要新增**:`AssignmentCandidateRespVO.CanonicalSnapshotVO.cells[]`(`AssignmentCandidateRespVO.java:108-150`)里每个 cell 已经逐日携带 `rescheduleResidue`(布尔)+ `serviceDate` + `assignmentId`,前端自己过滤即可得到这个数组。 +- ⚠️ **`POST /batch` 整批清理对"已过去的残留日期"有硬门禁会拦截**(详见「四、契约约束」),前端做整批清理时必须预期这种场景下的 400。逐日清理 `DELETE /{assignmentId}` 是否放行过去日期,本轮**未取证**,标记为【需确认】。 + +--- + +## 一、背景 + +### 前端诉求(原文摘要,仅作背景,不作为下文技术结论的依据) + +前端 2026-09-20 反馈清单第4项:「写口 `POST .../slots/{slotId}/clear-residue-dates` 已就绪,但前端只有 `rescheduleResidue` 布尔徽标,无可勾日期子集,无法实现『选槽位+勾日期』交互」,要求后端在读侧补一个 `residueServiceDates` 日期数组。 + +### 「前端以为的」vs「实际是」 + +| 维度 | 前端以为的 | 实际是(`文件:行` @origin/dev-v3) | +|------|------------|------------------------------------| +| 写口 | `POST .../slots/{slotId}/clear-residue-dates` 已就绪可直接调 | 该端点**不存在**:`clear-residue-dates`/`clearResidueDates`/`cancel-residue` 全仓零命中;`AssignmentControllerTest.java:938`「#7067:cancel-residue 端点已随槽位概念退役删除」。「槽位」概念本身已随 #7067 去槽位化退役,请改用 `DELETE /admin/fleet/assignments/{assignmentId}`(逐日)或 `POST /admin/fleet/assignments/batch`(整批) | +| 读侧字段 | 需要后端新增 `residueServiceDates: string[]` 字段才能拿到可勾选日期子集 | **不需要新增**:`cells[]` 已逐日携带 `rescheduleResidue`(`AssignmentCandidateRespVO.java:146-147`)+ `serviceDate`,前端 `cells.filter(c => c.rescheduleResidue).map(c => c.serviceDate)` 即得;`editableServiceDates`(`AssignmentCandidateRespVO.java:90`)也已把残留日期并入集合(`Step2CanonicalSnapshotService.java:218-227`) | + +### 该问题的历史脉络(同一张单 #5935) + +1. 2026-08-16:`changelogs-v2/2026-08/16_5935_排车按日期清理改期残留-新增接口-管理后台.md` 记录了 **当时新增** 的端点 `POST /admin/fleet/assignments/slots/{slotId}/clear-residue-dates`(commit `1fe44677a`,PR #5985)。 +2. #7067 去槽位化整体重构派单契约,`slotId` 概念被删除,该端点随之整体删除(该 changelog 的 `status_note` 已自我标注:"本端点...已被 #7067 去槽位化整体删除(404),无前端落地对象...残留清理语义由 `POST /batch` 整批 diff 覆盖")。 +3. 2026-09-20:前端仍然引用了这个已退役的端点、并额外要求新增字段——本文档是第三次澄清:**不用等新接口,现有两条写口 + 现有读侧字段已完全覆盖这个场景**。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 查询派单候选资源(含残留标记) | POST | `/admin/fleet/assignments/candidates` | 复用不改 | `canonicalSnapshot.cells[].rescheduleResidue` 已可推导残留日期子集 | +| 2 | 取消派单(逐日清理残留) | DELETE | `/admin/fleet/assignments/{assignmentId}` | 复用不改 | 按 cell 的 `assignmentId` 单日取消,条件释放占用 | +| 3 | 批量创建派单(整批清理残留) | POST | `/admin/fleet/assignments/batch` | 复用不改 | 提交时不带残留日期项,后端按 diff 精确取消 | + +--- + +## 三、接口详情 + +### 1. 查询派单候选资源 `POST /admin/fleet/assignments/candidates` + +**VO**: `AssignmentCandidateReqVO → AssignmentCandidateRespVO` + +#### 使用场景 + +车务打开排车/派单 Step2 弹窗时调用本端点获取 `canonicalSnapshot`;改期后旧日期的逐日派车切片会作为「改期残留」继续出现在 `cells[]` 里(`Step2CanonicalSnapshotService.java:199-227`:完整可编辑服务日 = 需求 `serviceDates` 权威集合 ∪ 在途旧行覆盖的越窗旧日期,注释原文「并入改期残留旧日期:在途行覆盖日期中越出当前需求窗的部分(含全程行展开)」)。前端从返回的 `cells[]` 中筛出 `rescheduleResidue === true` 的项,即得到「待清理的改期残留」候选日期集合,供「选槽位+勾日期」交互勾选。 + +> 本节仅列出与「识别/清理改期残留」直接相关的字段;候选资源查询的完整契约(车辆/司机分页、筛选、常驻关系、自动代入司机等)不在本文档范围。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Body | String(Long) | 否(改派排除自身时必填) | - | 当前订单ID | +| requirementId | Body | String(Long) | 否(改派排除自身时必填) | - | 当前用车需求ID;`requirementId` 缺失或需求上下文不可用时 `canonicalSnapshot` 为 `null`(`AssignmentCandidateRespVO.java:65-68`) | +| startDate | Body | LocalDate | 是 | `@NotNull` | 查询窗口开始日 | +| endDate | Body | LocalDate | 是 | `@NotNull` | 查询窗口结束日 | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| data.canonicalSnapshot | Object | 无 `requirementId` 或上下文不可用时为 `null` | +| data.canonicalSnapshot.editableServiceDates | LocalDate[] | 完整有序可编辑服务日集合;已并入改期残留旧日期(`AssignmentCandidateRespVO.java:90`;并入逻辑 `Step2CanonicalSnapshotService.java:218-227`) | +| data.canonicalSnapshot.cells[] | Array | 每个 RetainedGroupSet × editableServiceDates 笛卡尔积位置的唯一 cell | +| data.canonicalSnapshot.cells[].groupId | String(Long) | 稳定派车组ID(历史行无 assignmentGroupId 时回退 assignmentId) | +| data.canonicalSnapshot.cells[].serviceDate | LocalDate | 服务日期 | +| data.canonicalSnapshot.cells[].assignmentId | String(Long) | 当天逐日派车行ID;全程行/无行/不用车为 `null`;**「取消残留派车按此ID单日取消」**(`AssignmentCandidateRespVO.java:124-126`原文注释) | +| data.canonicalSnapshot.cells[].used | String | `USED`(实际用车) / `UNUSED`(显式不用车) / `null`(无切片行) | +| data.canonicalSnapshot.cells[].rescheduleResidue | Boolean | **前端要的「可勾选残留日期」判据就是这个字段**:`true`=该 cell 服务日期越出当前需求日期窗,属改期后待清理的旧实派车,前端据此挂「改期残留/待清理」徽标(`AssignmentCandidateRespVO.java:146-147`) | +| data.canonicalSnapshot.cells[].canDelete | Boolean | 是否可删除/取消:**改期残留旧行恒 `true`,只能改派或取消释放占用;受只读窗口约束时为 `false`**(`AssignmentCandidateRespVO.java:149-150`原文注释) | +| data.canonicalSnapshot.cells[].readOnly | Boolean | 该 cell 是否只读;只读 cell 不可被批量操作改写 | +| data.canonicalSnapshot.cells[].readOnlyReason | String | 只读原因;可编辑时为 `null` | + +#### 请求示例 + +> 以下示例按 VO 字段与类型构造,非测试服抓包实测。 + +```json +{ + "orderId": "2096412454643802114", + "requirementId": "2096412454643812345", + "startDate": "2026-09-10", + "endDate": "2026-09-15" +} +``` + +#### 响应示例 + +> 按 `AssignmentCandidateRespVO`/`SnapshotCellVO` 字段契约与仓库内测试夹具(`AssignmentControllerTest.java:143-199`)构造,非测试服抓包实测;`cells[1]` 展示一个 `rescheduleResidue=true` 的残留日期。 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "canonicalSnapshot": { + "planGeneration": "9007199254741001", + "snapshotVersion": "1", + "retainedGroupIds": ["9007199254741002"], + "editableServiceDates": ["2026-09-10", "2026-09-11", "2026-09-08"], + "cells": [ + { + "groupId": "9007199254741002", + "serviceDate": "2026-09-10", + "assignmentId": "2096412454488612900", + "used": "USED", + "rescheduleResidue": false, + "canDelete": true, + "readOnly": false, + "readOnlyReason": null + }, + { + "groupId": "9007199254741002", + "serviceDate": "2026-09-08", + "assignmentId": "2096412454488612866", + "used": "USED", + "rescheduleResidue": true, + "canDelete": true, + "readOnly": false, + "readOnlyReason": null + } + ] + } + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +`requirementId` 缺失或需求上下文不可用时 `canonicalSnapshot` 整体为 `null`(不是空对象),前端应据此区分「无残留」和「查不到需求上下文」两种态;没有任何改期残留时 `cells[]` 内所有项 `rescheduleResidue` 恒为 `false`,不会单独返回空数组表示"无残留"。 + +```json +{ "code": 200, "message": "成功", "data": { "canonicalSnapshot": null }, "success": true } +``` + +#### 错误响应 + +```json +{ + "code": 400, + "message": "参数非法: 用车开始日期不能为空", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- `cells[]` 是 RetainedGroupSet × editableServiceDates 的笛卡尔积,**并非只含残留日期**——必须用 `rescheduleResidue === true` 过滤,不能假设整个数组都是残留。 +- `canDelete` 字段的设计意图是「改期残留旧行恒 true」,但受「只读窗口」约束时会翻转为 `false`;前端在渲染可勾选状态时应优先读 `canDelete`,而不是只看 `rescheduleResidue`。 +- 改派场景(排除自身占用)需另传 `excludeAssignmentId`(本文档聚焦残留清理场景,未展开该分支)。 + +--- + +### 2. 取消派单(逐日清理残留) `DELETE /admin/fleet/assignments/{assignmentId}` + +**VO**: `CancelReqVO → CancelRespVO` + +#### 使用场景 + +车务在 Step2 栅格里勾选一个或多个 `rescheduleResidue=true` 的日期后,对每个勾选日期取其 cell 的 `assignmentId`,逐日调用本端点单独取消,条件释放该行占用的车辆/司机。适合「只清理某几天」「部分勾选」场景。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| assignmentId | Path | Long | 是 | - | 取自 `cells[].assignmentId`(残留清理场景下即待取消的那一天) | +| cancelReason | Body | String | 是 | `@NotBlank` | 取消原因 | +| driverNotified | Body | Boolean | 是 | `@NotNull` | 是否已告知司机(人工存证;`true`/`false` 均可取消) | +| notifyNote | Body | String | 否 | - | 告知备注 | +| cutoffDate | Body | LocalDate | 否 | - | 取消生效日;**服务端仅接受当天,空值按当天处理,不支持预约未来取消**(`CancelReqVO.java:35-37`原文注释) | +| evidenceFileIds | Body | List | 否 | 最多20个 | 取消凭证文件ID列表 | +| confirmWithoutEvidence | Body | Boolean | 否 | - | 无凭证时的二次确认标记 | +| requestId | Body | String | 是 | `@NotBlank`,最长64 | 请求幂等ID | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| data.assignmentStatus | String | `canceled`=已取消 | +| data.assignmentGroupId | String(Long) | 派车组ID;历史行无该字段时回退 assignmentId | +| data.assignmentSlotId | String(Long) | 稳定车辆槽位ID(历史字段,随 #7067 去槽位化不再是新契约主键) | +| data.effectiveDate | LocalDate | 实际取消生效日 | +| data.evidenceFileIds | String(Long)[] | 取消凭证文件ID列表 | +| data.confirmedWithoutEvidence | Boolean | 无凭证时是否已二次确认 | +| data.sideEffects.vehicleStatusUpdated | String | 车辆状态更新结果(idle/busy/maint;不涉及车为 null) | +| data.sideEffects.driverStatusUpdated | String | 司机状态更新结果(busy/idle;不涉及司机为 null) | +| data.sideEffects.reconPrepRowsCreated | Integer | 对账预备单创建条数(M1 降级恒 0) | +| data.sideEffects.reconPrepMarkedCanceled | Integer | 对账预备单标记取消条数(M1 降级恒 0) | + +#### 请求示例 + +```json +{ + "cancelReason": "改期残留清理", + "driverNotified": true, + "notifyNote": "已电话通知师傅", + "requestId": "residue-clear-2096412454488612866" +} +``` + +#### 响应示例 + +> 按 `CancelRespVO` 字段契约构造,非测试服抓包实测。 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "assignmentStatus": "canceled", + "assignmentGroupId": "9007199254741002", + "assignmentSlotId": null, + "effectiveDate": "2026-09-20", + "evidenceFileIds": [], + "confirmedWithoutEvidence": null, + "sideEffects": { + "vehicleStatusUpdated": "idle", + "driverStatusUpdated": "idle", + "reconPrepRowsCreated": 0, + "reconPrepMarkedCanceled": 0 + } + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +本端点是单条操作,无「空数据」态;命中不存在的 `assignmentId` 走错误响应(见下)。 + +```json +{ "code": 605009, "message": "派单不存在", "data": null, "success": false } +``` + +#### 错误响应 + +```json +{ + "code": 605028, + "message": "取消生效日不在派单服务日期范围内", + "data": null, + "success": false +} +``` + +其它错误码(`AssignmentController.java:469-471`):400(`cancelReason`/`driverNotified`/`requestId` 请求校验)/ 605009(派单不存在)/ 605020(当前状态不允许取消)/ 605026(无凭证未二次确认)/ 605027(已出发禁止整组取消)。 + +#### 业务边界 + +- 【需确认】**本端点对"服务日期已过去的改期残留行"是否放行取消,本轮未取证**:`CancelReqVO.cutoffDate` 注释写明「服务端仅接受当天,空值按当天处理,不支持预约未来取消」,而 605028 的语义是「取消生效日不在派单服务日期范围内」——若某残留行 `serviceDate` 是 3 天前、`cutoffDate` 按当天处理,"当天"是否落在该行的服务日期范围内、会不会触发 605028,未经实测确认。读侧 `canDelete` 字段的设计意图是「改期残留旧行恒 `true`,受只读窗口约束时 `false`」(`AssignmentCandidateRespVO.java:149-150`),但这是响应快照上的展示态,不等于运行时 DELETE 调用本身在过去日期上必然放行。**若实测发现本端点对已过去日期同样拦截,那才是真正需要后端开一张单的地方**(放开残留行对过去日期的取消)。 +- `assignmentId` 必须取自对应 cell(残留清理场景下即 `rescheduleResidue=true` 的那一项),传错会命中另一天的派车行。 +- `driverNotified` 传 `false` 也允许取消,只是前端需要在未告知司机时给出强提示(后端只做存证,不代为通知)。 + +--- + +### 3. 批量创建派单(整批清理残留) `POST /admin/fleet/assignments/batch` + +**VO**: `BatchCreateAssignmentReqVO → BatchAssignmentWriteRespVO` + +#### 使用场景 + +车务在 Step2 栅格里编辑好完整的新逐日配车方案后一次性提交。若不希望保留某些残留日期,**只需在 `dailyPlan[]` 里不包含那些日期**,后端按(服务日期×车辆)diff 原子重写:新方案未覆盖的现行日期行自动取消(含改期残留行)。适合"整批一次性对齐新方案"场景。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Body | String(Long) | 是 | `@NotNull` | 订单ID | +| requirementId | Body | String(Long) | 是 | `@NotNull` | 当前生效用车需求ID | +| kind | Body | String | 否 | `TRAVEL`/`TRANSFER` | 需求类别,不传按 TRAVEL | +| startDate | Body | LocalDate | 是 | `@NotNull` | 用车开始日期 | +| endDate | Body | LocalDate | 是 | `@NotNull` | 用车结束日期 | +| confirmNoVehicleServiceDates | Body | Boolean | 否 | - | 逐日计划未覆盖全部服务日期时的显式二次确认 | +| sendItinerarySms | Body | Boolean | 否 | 不传按 false | 是否向本批各车师傅发送行程短信 | +| requestId | Body | String | 是 | `@NotBlank`,最长64 | 批次级幂等请求标识 | +| dailyPlan[] | Body | Array | 是 | 最多4000项 | 按行程日的完整配车列表;**要清理的残留日期不要出现在这个数组里** | +| dailyPlan[].serviceDate | Body | LocalDate | 是 | `@NotNull` | 服务日期 | +| dailyPlan[].vehicleId | Body | String(Long) | 是 | `@NotNull` | 车辆ID | +| dailyPlan[].driverId | Body | String(Long) | 是 | `@NotNull` | 司机ID | +| dailyPlan[].assignmentPrice | Body | String(BigDecimal) | 否 | ≥0 | 本车当天实际价格,不传按车型价格日历兜底 | +| dailyPlan[].priceAdjustmentReason | Body | String | 否 | 最长256 | 实际价格与参考价不一致时的调整原因 | +| items / chargeableServiceDates / vehicleFeeWaiverReason / confirmAllServiceDatesFree / holdMode | Body | - | - | **禁止携带** | #7067 已移除的旧字段,携带即 400(`BatchCreateAssignmentReqVO.java:98-124`) | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| data.assignments[] | Array | 按 fleetItemIndex 升序返回的派单结果(历史字段名,结构见 `AssignmentWriteRespVO`) | +| data.finalPlanPublished | Boolean | 本次是否已发布最终方案;`false` 表示排车已落库但接送机未配齐 | +| data.pickupDropoffGate | Object | 接送机门禁状态(要求日与缺口日) | +| data.failedFleetItemIndex | Integer | 直接派定基线失败的车辆槽位序号 | +| data.dailyDifferences[] | Array | 直接派定基线失败的逐日差异 | + +#### 请求示例 + +```json +{ + "orderId": "2096412454643802114", + "requirementId": "2096412454643812345", + "startDate": "2026-09-09", + "endDate": "2026-09-15", + "requestId": "batch-residue-clear-20260920-001", + "dailyPlan": [ + { "serviceDate": "2026-09-10", "vehicleId": "2079857983403024385", "driverId": "2079857983403024387" }, + { "serviceDate": "2026-09-11", "vehicleId": "2079857983403024385", "driverId": "2079857983403024387" } + ] +} +``` + +上例中 `2026-09-09`(残留日期)未出现在 `dailyPlan[]` 里,后端 diff 会把该日现行行自动取消。 + +#### 响应示例 + +> 按 `BatchAssignmentWriteRespVO` 字段契约构造,非测试服抓包实测。 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "assignments": [], + "finalPlanPublished": true, + "pickupDropoffGate": null, + "failedFleetItemIndex": null, + "dailyDifferences": null + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +未配置接送机声明的订单 `pickupDropoffGate` 恒为 `null`;无接送机声明时提交即完成(`finalPlanPublished=true`)。 + +#### 错误响应 + +**本场景最关键的错误**——残留日期已过去时,diff 判定要取消它会被"日期已过去"门禁拦下(`AssignmentService.java:2775-2778` `assertDailyPlanDateMutable`),整个批次连同一并提交的其它合法项一起被拒绝,非部分成功: + +```json +{ + "code": 100001, + "message": "参数非法: 2026-09-09已过去或已完结,不能修改逐日派车配置", + "data": null, + "success": false +} +``` + +其它已知错误码:605062(提交项的服务日期越出当前需求日期窗;已有的窗外在途行不受此码约束,改由整批 diff 精确取消,见 `AssignmentServiceTest.java:1727` 用例)/ 605912(派车快照缺少车型)。 + +#### 业务边界 + +- ⚠️ **改期后延的残留日期多半已经过去,本端点无法清理它们**:`assertDailyPlanDateMutable`(`AssignmentService.java:2775-2778`)对"新方案未覆盖、因此要被 diff 自动取消"的现行行同样做日期校验,命中过去日期即整批 400。前端做"整批清理"操作前必须预判这种场景,遇到 400 应引导车务改用逐日 `DELETE /{assignmentId}`。 +- 旧字段(`items`/`fleetItemIndex`/`used`/`pickupParticipant` 等)携带即 400,不再兼容。 +- 接送机不在本接口配置,走独立的 `PUT /pickup-dropoff-config`。 +- 整批提交是原子操作:校验失败不会部分落库。 + +--- + +## 四、契约约束与正确调用方式 + +> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。 + +### ✅ 正确 / ❌ 错误 用法对照 + +| 场景 | 用法 | +|------|------| +| ✅ 获取可勾选残留日期 | `GET candidates` 响应的 `cells.filter(c => c.rescheduleResidue).map(c => c.serviceDate)` | +| ✅ 逐日清理(含过去日期,未取证是否放行) | `DELETE /admin/fleet/assignments/{cell.assignmentId}` | +| ✅ 整批清理(残留日期均未过去) | `POST /batch`,`dailyPlan[]` 不包含要清理的日期 | +| ❌ 整批清理(残留日期已过去) | 同上 payload → **400(`code=100001`)整批被拒**,不会"跳过过去日期、只清未来的" | +| ❌ 调用文档外的写口 | `POST .../slots/{slotId}/clear-residue-dates` → **该路径不存在**(#7067 已删除,无 `slotId` 概念) | + +### 切换清理方式的必要动作 + +前端在实现"整批清理"按钮时,遇到 `code=100001` 且消息含"已过去或已完结",应识别为"残留日期已过去,本接口无法清理",引导车务改走逐日 `DELETE` 路径(而不是当成普通校验失败重试原样提交)。 + +--- + +## 五、数据库行为 + +本次零改动;以下是既有行为,供前端理解结果落库口径: + +| 清理方式 | 命中行为 | +|----------|----------| +| `DELETE /{assignmentId}` | 该派车行状态置 `canceled`,条件释放车辆/司机占用(`sideEffects` 回真实反算结果) | +| `POST /batch`(diff 自动取消未覆盖的现行行) | `assignmentMapper.cancelActiveRowsByIds(...)` 批量置 `canceled`,`cancel_source` 显式留空(`AssignmentService.java:2796-2802`原文注释「#5924:方案重写不是取消动作,cancel_source不盖章(留NULL)」),取消原因固定写"逐日派车方案调整" | + +两条路径都不会物理删除行,只做状态流转;`assignmentId` 一旦取消不可复用为新方案的匹配键。 + +--- + +## 六、边界行为 + +- 未登录 → 401(网关拦截) +- `assignmentId` 不存在 → `DELETE` 返 605009 +- `requirementId` 缺失或需求上下文不可用 → `candidates` 的 `canonicalSnapshot` 为 `null`,不报错 +- 已出发/已完结的派单 → 605027(整组取消禁止)/「该日已完结不可改派」400(`AssignmentService.java:2714`) +- 老数据兼容:历史行无 `assignmentGroupId` 时,`cells[].groupId` 回退返回 `assignmentId`,对任何真实行恒非空 + +--- + +## 六.5、枚举 / 数据字典 + +### used(`AssignmentCandidateRespVO.SnapshotCellVO.used`) + +**所属字段**: `data.canonicalSnapshot.cells[].used` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `USED` | 实际用车 | 该 cell 当天有实派车辆/司机 | +| `UNUSED` | 显式不用车 | 车务明确标记该日不配车 | +| `null` | 无切片行(未编辑) | 该 group×日尚无任何逐日行 | + +--- + +## 六.6、修改前后对比 + +本文档不涉及接口修改(三条端点均为"复用不改"),无字段级/行为级对比。 + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否(无任何接口变更) +- **前端是否必须同步上线**: 否(后端零改动,前端可按自己节奏切换实现方式) +- **前端 workaround 清理点**: 若前端此前已按"等待新写口/新字段"的假设搭了占位代码或临时禁用了"整批清理"按钮,可撤——改用本文档给出的两条现成端点与现成字段即可实现完整交互 + +--- + +## 七、不影响范围 + +- **仅影响**: 管理后台车务排车 Step2 弹窗「改期残留清理」交互的前端实现方式(用哪个端点、用哪个字段) +- **零影响**: + - 后端代码(本次零改动) + - 数据库 schema + - 网关路由(三条端点均已在网关既有路由内) + - `POST /candidates`、`DELETE /{assignmentId}`、`POST /batch` 的既有契约与行为 + - 其它车务/排车流程(本文档只梳理"改期残留清理"这一个场景) + +--- + +## 八、测试环境已验证 + +本文档不改代码,本轮**未做新的测试服 HTTP 实测**;以下用源码引用替代实测取证,均为 `origin/dev-v3`(HEAD `10efbaddf`,与本地工作树一致)实际存在的代码: + +``` +AssignmentCandidateRespVO.java:90 editableServiceDates 字段存在 ✓ +AssignmentCandidateRespVO.java:108-150 SnapshotCellVO 含 rescheduleResidue/canDelete/assignmentId ✓ +Step2CanonicalSnapshotService.java:218-227 残留日期并入 editableServiceDates 的逻辑 ✓ +AssignmentController.java:75 类级路由 /admin/fleet/assignments ✓ +AssignmentController.java:201-234 POST /batch 端点存在 ✓ +AssignmentController.java:462-477 DELETE /{assignmentId} 端点存在 ✓ +AssignmentControllerTest.java:938 cancel-residue 端点已随 #7067 删除的注释 ✓ +AssignmentService.java:2743-2744,2775-2778 diff 自动取消对过去日期的硬门禁 ✓ +AssignmentServiceTest.java:1727 窗外在途行由 diff 取消而非 605062 拦整批的用例 ✓ +grep clear-residue-dates / clearResidueDates / cancel-residue → 全仓零命中 ✓ +``` + +**【需确认】未取证项**:`DELETE /{assignmentId}` 对服务日期已过去的改期残留行是否放行取消(涉及 605028 与只读窗口判定),需要一次真实测试服调用才能确认,本文档不代为下结论。 + +--- + +## 十、相关文档 + +- 原始功能单及第一版说明:`changelogs-v2/2026-08/16_5935_排车按日期清理改期残留-新增接口-管理后台.md`(已在其 `status_note` 里自我标注端点已随 #7067 退役) +- 退役背景:#7067 去槽位化重构(`06_7067_派单去槽位化按行程日配车-接送机独立配置-修改接口-管理后台.md`) +- 后续计划:若【需确认】项实测发现 `DELETE` 对过去日期同样拦截,需另开工单放开残留行的过去日期取消 + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#5935](https://git.1814.love:8443/wx/HL/issues/5935) +- **PR**: 无(零代码改动,未产生新 PR) + +### 联系人 + +- **后端负责人**: @wx