文件
hl-api-changelog/changelogs-v2/2026-09/26_8371_改期残留行单行取消与累计凭证回显-修改接口-管理后台.md
T
Mimingguang 1a07718bd1
changelog-filename-gate / validate (push) Failing after 1s
chore(changelog): #8371 前端回写 verified(mmg,25d0469c,v2.1)
2026-09-26 19:30:33 +08:00

275 行
19 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "8371"
title: "取消派单:改期残留行单行取消,evidenceFileIds 改为按组累计"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "mmg"
frontend_ref: "25d0469ccf12fc0fb2e359a8111e4acd133e6803"
target_release: "v2.1"
verified_at: "2026-09-26"
status_note: "PR #8380 已合并 dev-v3(commit c2bc5c3d4),测试服 hl-fleet-service 已部署该 commit(本单不改 order-v3),已在测试服完成 6 步全链路真实网关调用验证:残留行单行取消(不再命中 605027)、非残留组取消保持原逻辑、批量重提窗内行、需求确认成功。前端接入本次不需要自行判断「该行是否残留」——后端已在服务端自动判定并豁免日期门;唯一需要适配的是 CancelRespVO.evidenceFileIds 语义变化,见下文「六.6」。 前端 2026-09-26 已交付(hl-admin 25d0469c):isCanceledAssignmentResponse 认 canceled/exception 两态(通用入口取消在途残留行不再误报失败);残留清理入口注释与 cancelAssignment JSDoc 按本单订正(605710 透 message、evidenceFileIds 累计语义禁拼接——三调用点实证零消费响应值);sync-log 挂起项「DELETE 过去日期放行待确认」销项。"
updated_at: "2026-09-26"
base: "dev-v3"
---
# 取消派单:改期残留行单行取消,evidenceFileIds 改为按组累计
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
>
> **服务**: hl-fleet-service
> **PR**: #8380
> **Issue**: #8371
> **日期**: 2026-09-26
> **影响范围**: 管理后台车务派单「取消派车」弹窗与取消成功后的凭证展示
---
## ⚠️ 关键变化
`DELETE /admin/fleet/assignments/{assignmentId}` 取消派车接口两处行为改动(路径、方法、入参字段不变):
1. **改期残留行单行取消**(`AssignmentController.java:464-481`、`AssignmentService.java:5308-5419`):订单改期后,落在「该行 `requirementId` 所指当前用车需求日期窗口」之外的旧逐日派车行(称「改期残留行」),调用本接口取消时现改为**只取消这一行**,不再连带同派车组内其他行。同时豁免 605027(已出发禁止整组取消)、605047(行程已结束只读)、605028(生效日不在派单服务日期范围内)三道日期门;`effectiveDate` 恒为今天,入参 `cutoffDate` 对残留行不生效。**非残留行(含全程行)行为不变,仍按派车组整组处理**。
- 残留行判定依赖实时查询 order-v3 侧的用车需求日期窗(`orderQueryFacade.getFleetDetailContextStrict`),查询失败时**整笔取消 fail-closed,一行都不取消**,返回新错误码 605710「订单服务不可用:{0}」。
2. **`CancelRespVO.evidenceFileIds` 语义变更**(`CancelRespVO.java:35`):改前是本次请求入参携带的凭证 ID 原样返回;改后是**该派车组自创建以来累计的全部取消凭证 ID**(追加式只增不丢,旧的在前新的在后),本次未传凭证时返回空数组,不代表没有历史凭证——历史凭证不会因为本次没传就消失。
另有一个非接口改动:夜间自动完结定时任务(`AssignmentCompleteJob` → `AssignmentService.listScheduleCompletableBefore`)现在会跳过判定为改期残留行的 `assigned` 状态派车,不再把它们自动置 `completed`;残留行会一直停在原状态,直到车务手工调用本接口取消。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 取消派单 | DELETE | `/admin/fleet/assignments/{assignmentId}` | 行为变更(出参字段语义变更,字段本身不变) | 改期残留行单行取消并豁免三道日期门;`evidenceFileIds` 改为按组累计 |
---
## 三、接口详情
### 1. 取消派单 `DELETE /admin/fleet/assignments/{assignmentId}`
**VO**: `CancelReqVO → CancelRespVO`
#### 使用场景
管理后台车务派单管理页面,对某一行派车点击「取消」调用本接口,按条件释放该行占用的车辆/司机资源。目标行若被判定为「改期残留行」(服务日期落在当前用车需求日期窗之外),只取消这一行,不影响同派车组内窗口以内的其它日期行;否则仍按整组处理。
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| assignmentId | Path | Long | 是 | 派单须存在,否则 605009 | 待取消的派单行 ID |
| cancelReason | Body | String | 是 | `@NotBlank` | 取消原因 |
| driverNotified | Body | Boolean | 是 | `@NotNull` | 是否已告知司机(人工存证,true/false 均可取消) |
| notifyNote | Body | String | 否 | - | 告知方式/摘要备注 |
| cutoffDate | Body | LocalDate | 否 | 服务端仅接受当天,空值按当天处理 | 取消生效日;**改期残留行本参数不生效,effectiveDate 恒为今天** |
| evidenceFileIds | Body | Array\<Long\> | 否 | `@Size` 最多 20 个 | 本次新增的取消凭证文件 ID |
| confirmWithoutEvidence | Body | Boolean | 否 | - | 无凭证时的二次确认标记 |
| requestId | Body | String | 是 | `@NotBlank`,最长 64 | 幂等请求 ID |
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data.assignmentStatus | String | 取消后落库状态:来源行是 `unassigned` 落 `canceled`;来源行是 `assigned`/`holding`(在途)落 `exception`(与通用取消 #6152 同口径,非残留身份特有) |
| data.assignmentGroupId | String(Long) | 派车组 ID;历史行无该字段时回退 assignmentId |
| data.assignmentSlotId | String(Long) | 稳定车辆槽位 ID(历史字段) |
| 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 降级可能为 `null` 或 0) |
| data.sideEffects.reconPrepMarkedCanceled | Integer | 对账预备单标记取消条数(M1 降级恒 0) |
#### 请求示例
> 测试服网关实测原文,目标行为改期残留行,服务日期已过去。
```json
{
"cancelReason": "8371-verify-residue-delete",
"driverNotified": true,
"notifyNote": "8371-verify-residue-delete",
"confirmWithoutEvidence": true,
"requestId": "8371-verify-delete-1"
}
```
#### 响应示例
> 测试服网关实测原文。取消前该行处于 `assigned`,故落库为 `exception` 而非 `canceled`;`reconPrepRowsCreated` 实测返回 `null`,前端勿据它做判断。
```json
{
"code": 200,
"message": "成功",
"data": {
"assignmentStatus": "exception",
"assignmentGroupId": "362105579390504961",
"assignmentSlotId": "362105579390504961",
"effectiveDate": "2026-09-26",
"evidenceFileIds": [],
"confirmedWithoutEvidence": true,
"sideEffects": {
"vehicleStatusUpdated": "busy",
"driverStatusUpdated": "busy",
"reconPrepRowsCreated": null,
"reconPrepMarkedCanceled": 0
}
},
"success": true
}
```
#### 空数据 / 降级响应
本端点是单条写操作,无空数据态。目标行判定改期残留行失败(依赖的 order-v3 查询不可达)时,不返回部分结果、不猜测是否残留,一行都不取消,直接走错误响应(见下方 605710)。
```json
{ "code": 605710, "message": "订单服务不可用:取改期残留判定所需的用车需求日期窗失败", "success": false }
```
#### 错误响应
```json
{ "code": 605009, "message": "派单不存在", "success": false }
```
```json
{ "code": 605026, "message": "未上传取消凭证,请二次确认后再取消", "success": false }
```
```json
{ "code": 605027, "message": "行程已出发,不能整组取消,请使用修改派单", "success": false }
```
**其它错误码**:400(`cancelReason`/`driverNotified`/`requestId` 请求校验)/ 605020「当前派单状态不允许此操作」/ 605028「生效日期不在派单服务日期范围内」/ 605047「行程已结束,派车信息只读,不能修改或改派」(`AssignmentService.java:5410` `assertTripMutable`)。**605027/605047/605028 三码仅在目标行判定为非改期残留行时才可能命中**;判定为改期残留行时豁免这三码,改走单行取消分支。
#### 业务边界
- **改期残留行判定**(`AssignmentDailyPlanFacts.isRescheduleResidue`,经 `AssignmentService.resolveCancelResidueWindow`/`isCancelResidueRow` 调用):仅逐日行(非全程行)参与判定;判定要在加锁前实时查询 order-v3 侧当前用车需求的日期窗,查询失败直接抛 605710,不对残留身份做任何假设,不取消任何行。
- **单行取消**:残留行确认后,只取消这一行;同派车组内落在窗口以内的其它日期行不受影响、不校验、不触发状态机。
- **整组取消**:非残留行维持原逻辑——取消会按派车组整体处理(`resolveEffectiveGroupRows` 圈定的全部可取消行),605027/605047/605028 三道日期门原样校验,`effectiveDate` 仍取 `cutoffDate`(默认今天,只接受今天)。
- **落库状态并非恒为 `canceled`**:来源行状态是 `unassigned` 时落 `canceled`;是 `assigned`/`holding`(在途)时落 `exception`(#6152 既有分流规则,不因残留身份而不同)。
- **凭证累积**:命中残留分支时同组会分多次调用逐行取消,若整组替换式返回本次入参会丢掉前几次的凭证;因此 `evidenceFileIds` 统一改为按组读取累计清单(`assignmentEvidenceService.listCancellationFileIds(groupId)`),前端应直接以响应值为准,不要自行拼接历史值。
- **操作日志**:仅命中残留分支时,`assignmentOperationLog` 的 `CANCEL_REQUESTED` 记录 `detailJson` 会带 `"scope":"RESIDUE_ROW"`;非残留(整组)分支不写这个键(不是写 `"GROUP"`,而是完全没有该字段)。可通过 `GET /admin/fleet/board/orders/{orderId}` 的 `operationLog[].detailJson` 读取核对。
- **司机通知**:`driverNotified=false` 仍允许取消,仅作人工存证,后端不代为通知。
- **幂等性**:`requestId` 走 `@Idempotent`,10 秒窗口内重复提交被拦截(提示「派单取消处理中,请勿重复提交」)。
---
## 四、契约约束与正确调用方式
### 残留行判定是服务端自动完成的,前端不需要预判
前端调用取消接口时无需先判断目标行是否为「改期残留行」——服务端在处理请求时会自动查询、自动分流。前端只需按现有交互正常收集 `cancelReason`/`driverNotified`/`requestId` 等必填参数发起请求:
- 命中残留行分支时,响应正常返回 200,`effectiveDate` 恒为今天,同派车组内其它行不受影响;前端无需额外处理。
- 判定依赖的 order-v3 查询失败时,返回 605710,此时**一整行都没有取消**,前端应按普通失败提示处理并允许重试,不需要区分"是不是残留行"。
### evidenceFileIds 的正确使用方式
本次请求**携带凭证**时,`CancelRespVO.evidenceFileIds` 返回该派车组截至本次累计的全部取消凭证 ID,旧在前新在后(而不是本次请求入参那几个;`AssignmentService.java:5526-5527`)。前端展示「该派车组已上传哪些取消凭证」时,应直接使用响应值整体覆盖式渲染,不要再把它与本地缓存的历史值做拼接,否则会出现重复 ID。
若本次请求未携带任何凭证(`evidenceFileIds` 为空、以 `confirmWithoutEvidence=true` 放行 605026),响应的 `evidenceFileIds` 为空数组——这不代表该派车组没有历史凭证,只是本次响应未重新拉取累计列表(`hasEvidence` 为 false 分支不会调用 `listCancellationFileIds`)。因此前端不要用空数组覆盖已展示的历史凭证;需要展示累计凭证时,以最近一次**携带凭证**的取消响应为准。
---
## 五、数据库行为
- **`fleet_assignment`**:目标行 `assignment_status` 更新——来源行是 `unassigned` 写 `canceled`,是 `assigned`/`holding`(在途)写 `exception`(#6152 既有分流规则,不因是否残留行而不同);`updated_by` 显式落取消操作人。无表结构变更、无新增列。
- **`fleet_assignment_evidence`**:`hasEvidence=true` 时追加新行(`assignmentEvidenceService.appendCancellationEvidenceValidated`),不删除、不覆盖已有记录。
- **`fleet_assignment_operation_log`**:追加一条 `CANCEL_REQUESTED` 记录;`detail_json` 命中残留行分支时带 `"scope":"RESIDUE_ROW"`,非残留(整组)分支**不写 `scope` 键**(不是写 `"GROUP"`)。`hasEvidence=true` 时另追加一条 `CANCEL_EVIDENCE_RECORDED` 记录。
- **无新增表、无新增列**:改期残留行的判定结果本身不落库,每次取消请求都会重新实时查询 order-v3。
---
## 六、边界行为
- **未登录** → 401(网关拦截)。
- **派单不存在** → 605009。
- **状态不允许**(如已取消、已完结) → 605020。
- **order-v3 不可达** → 605710 fail-closed,一行都不取消,不对残留身份做任何猜测。
- **无凭证未二次确认** → 605026。
- **非残留行已出发** → 605027;**非残留行程已结束** → 605047;**非残留行生效日不在服务日期范围内** → 605028——这三码只在非残留分支可能命中,残留行分支豁免。
---
## 六.6、修改前后对比
### 行为对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 取消改期残留行 | 按整组处理,605027/605047/605028 三道日期门正常校验,同组内已出发的其它行会导致整组取消被拒 | **新增单行分支**:只取消该行本身,豁免三道日期门,`effectiveDate` 固定今天;同组其它行不受影响 |
| order-v3 查询失败时的处理 | 不涉及(改前无此依赖) | 新增 fail-closed:605710,一行都不取消 |
| 夜间自动完结 | 残留行与其它行一样,到期后被 `AssignmentCompleteJob` 自动置 `completed` | 残留行被跳过,停留在原状态直到车务手工调用本接口取消 |
### 出参字段对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `evidenceFileIds` | 本次请求入参携带的凭证 ID 原样返回 | **语义变更**:该派车组自创建以来累计的全部取消凭证 ID(`hasEvidence=true` 时返回累计列表,为 false 时返回空数组) |
---
## 六.7、影响评估
- **是否破坏向后兼容**:否。改期残留行的单行取消是新增的判定分支,非残留行(含全程行)的整组取消逻辑、三道日期门校验逐字不变。`evidenceFileIds` 字段名、类型(`Array<Long>`/序列化为字符串数组)均未变,只是同一个字段返回的内容集合变了。
- **前端是否必须同步上线**:否。前端不做任何改动,取消接口对非残留行的调用行为与改前完全一致;对残留行的调用会自动享受到"不再报 605027/605047/605028"的效果,不需要前端识别或适配。**唯一需要前端注意的**:如果前端此前把 `evidenceFileIds` 当作"本次上传的文件"做本地拼接展示,接入后应改为直接使用响应值整体替换,否则会展示出重复的凭证 ID。
- **覆盖边界**:本次改动不影响 `POST /admin/fleet/assignments/batch`、`POST /admin/fleet/assignments/requirements/{requirementId}/confirm` 等相邻写口自身的契约,只是这些写口在残留行被移出组后不再对该组报改前的连带错误(100001/605055),详见「八、测试环境已验证」步骤 4/6 的实测记录。
---
## 七、不影响范围
- **仅影响**:`DELETE /admin/fleet/assignments/{assignmentId}` 对改期残留行的判定分支,以及 `evidenceFileIds` 的取值口径。
- **零影响**:
- 非残留行(含全程行)的整组取消规则,三道日期门校验逐字不变。
- 派车创建、批量重提、需求确认等其它写口自身的入参/出参契约。
- 车辆/司机占用释放的条件判断逻辑(`sideEffects` 字段结构不变)。
- #6152 既定的取消状态分流规则(`unassigned`→`canceled`,`assigned`/`holding`→`exception`)。
---
## 八、测试环境已验证
部署:PR #8380 已合并 `dev-v3`(`c2bc5c3d4`),测试服 hl-fleet-service 已部署该 commit;本单不改 order-v3。
以下 6 步为同一夹具的全链路真实网关调用(夹具:`orderId=2103714435619303425`,改期后需求窗 2026-10-02~10-04,派车组 3 行,其中 2026-09-20 一行落在窗外):
1. **改前状态**:`GET /admin/fleet/board/orders/{orderId}` 确认三行初始状态均为 `assigned`。
2. **DELETE 残留行**:对 2026-09-20 这一行发起取消,改前同一请求体命中 605027(拒绝整组取消);改后单行取消直接成功,返回 `assignmentStatus="exception"`(该行取消前是 `assigned`,按 #6152 分流落 `exception`)、`evidenceFileIds=[]`。
3. **复读验证**:窗内两行逐字段(vehicleId/driverId/assignmentPrice/priceSource/assignmentStatus 等)与改前基线完全一致,未受影响;操作日志新增一条 `detailJson` 含 `"scope":"RESIDUE_ROW"` 的记录。
4. **批量重提窗内行**:`POST /admin/fleet/assignments/batch` 提交窗内两行,改前同一请求体命中 100001("已过去或已完结,不能修改逐日派车配置");改后不再报错(残留行已单独取消,不再纳入本次提交的校验范围)。
5. **取新 generation**:复读 `dispatchPlanGeneration` 供确认步骤使用。
6. **需求确认**:`POST /admin/fleet/assignments/requirements/{requirementId}/confirm` 成功,改前基线在此步命中 605055("派车方案已变化,请刷新后重试"),改后成功确认。
全链路(DELETE 残留行 → 批量重提窗内行 → 需求确认)打通,改前基线记录的三处拒绝(605027/100001/605055)在改后代码下不再复现。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8371](https://git.1814.love/wx/HL/issues/8371)
- 关联 PR: [wx/HL#8380](https://git.1814.love/wx/HL/pulls/8380)
- 上游工单: #5935(改期残留清理,见 `20_5935_改期残留清理用现有读写口即可-前端缺陷-管理后台.md` 内 9 处订正指针)
## 关联 / 联系人
### 链接
- **Issue**: [#8371](https://git.1814.love/wx/HL/issues/8371)
- **PR**: [#8380](https://git.1814.love/wx/HL/pulls/8380)
- **Merge commit**: [c2bc5c3d4](https://git.1814.love/wx/HL/commit/c2bc5c3d4)
### 联系人
- **后端负责人**: @wx
- **前端负责人**: @mmg