docs(changelog): #5935 改期残留清理——前端要的能力已具备,用现有读写口即可
changelog-filename-gate / validate (push) Failing after 2s
changelog-filename-gate / validate (push) Failing after 2s
前端 2026-09-20 待办清单第 4 项要求读侧补 residueServiceDates 日期数组,
并称写口 POST .../slots/{slotId}/clear-residue-dates 已就绪。两条都不成立:
- 那个写口在 origin/dev-v3 上零命中——「槽位(slot)」概念已随 #7067 退役,
基于 slotId 的写口不会再有;
- 读侧能力早就有,只是不叫 residueServiceDates:candidates 响应的 cells[]
逐日带 rescheduleResidue + serviceDate + assignmentId,
另有 editableServiceDates 已并入残留日期。
故本件是用法说明,后端零代码改动:逐日清理走 DELETE /assignments/{id},
整批清理走 POST /batch 不提交残留项(diff 会精确取消)。
已写明真正的写侧边界——POST /batch 有「已过去日期不可改」硬门禁,
而改期后延场景的残留日多半已过去。
DELETE 路径对已过去日期是否放行未取证,如实标【需确认】;
若实测也挡住,那才需要另开工单放开残留行的过去日期取消。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -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<Long> | 否 | 最多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
|
||||
在新工单中引用
屏蔽一个用户