docs(changelog): #8301 车务看板带日期筛选时单条 requirement_id 为空的派车行不再清空整个日期窗
changelog-filename-gate / validate (push) Failing after 2s

这个提交包含在:
API Changelog Bot
2026-09-24 10:57:15 +08:00
父节点 f872d5204a
当前提交 e28c94c18b
@@ -0,0 +1,293 @@
---
schema: "hl-changelog/v2"
ticket: "8301"
title: "车务看板带日期筛选时,单条 requirement_id 为空的派车行不再清空整个日期窗"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-09-24"
base: "dev-v3"
---
# fleet: 看板日期筛选分支的脏行失败关闭收窄为跳过单行
> **存放目录**: `changelogs-v2/2026-09/`
>
> **服务**: hl-fleet-service
> **PR**: #8310
> **Issue**: #8301
> **日期**: 2026-09-24
> **影响范围**: 管理后台「车务看板」订单列表/网格视图(带日期筛选时)
---
## ⚠️ 关键变化
**带日期筛选时,单条脏数据不再清空整个日期窗。** 此前只要日期窗里存在任意一条 `requirement_id` 为空的派车行,`GET /admin/fleet/board/orders`(带 `startDate/endDate` 或 `startDayFrom/startDayTo`)就整页返回 `records=[]`、`total=0`、`code=200`,**不报错**——页面上看到的是「这几天没有订单」而不是任何异常。现在这条行被**跳过并记 WARN**(WARN 里带 `orderId`/`assignmentId`/`assignmentGroupId`),同一日期窗内其余订单**照常返回**,与不带日期筛选时的既有口径一致。
**接口契约零变化**:路径、参数、字段、类型、必填性、枚举、错误码全部不变,只修正日期分支的行为。
---
## 一、背景
车务看板带日期筛选时走一条独立的「当前主单日期候选」扫描分支。该分支原本把四类候选行异常合并成一个失败关闭条件,其中一类是「派车行的 `requirement_id` 为空」——这是历史数据迁移、手工修数或未来写路径缺陷可能留下的占位行。这类行本身匹配不上任何用车需求,在不带日期的路径里只是被跳过并记一条汇总 WARN;但在日期分支里,它会让**同页全部订单**的候选一起作废,表现为整窗静默返回空。
本次把该条件按成因拆开:真正影响槽位聚合正确性的三类结构性畸形(行对象缺失、主单 ID 缺失、派车组 ID 与派车 ID 同时缺失)以及重复槽位键**仍然失败关闭**,只是各自补一条带定位信息的 WARN;`requirement_id` 为空的行改为逐行剔除并记 WARN。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 看板订单列表 | GET | `/admin/fleet/board/orders` | 行为变更(契约不变) | 带日期筛选时,`requirement_id` 为空的派车行由「整窗失败关闭」改为「跳过该行 + WARN」 |
---
## 三、接口详情
### 1. 看板订单列表 `GET /admin/fleet/board/orders`
**VO**: `BoardOrderPageReqVO → BoardOrderPageRespVO`(`records: BoardOrderRecordVO[]`)
#### 使用场景
管理后台「车务看板」订单列表/网格视图(`variant=list|grid`)。车务人员按状态、日期、团号、车型等条件筛出待派/已派订单卡片,逐卡执行改派、派车、查看行程等操作。本次改动只影响**带日期筛选**时该列表返回的记录集合。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| startDate | Query | LocalDate | ❌ | `yyyy-MM-dd` | 行程日期窗起(`startDayFrom` 的兼容别名) |
| endDate | Query | LocalDate | ❌ | `yyyy-MM-dd` | 行程日期窗止(`startDayTo` 的兼容别名) |
| startDayFrom | Query | LocalDate | ❌ | `yyyy-MM-dd` | 行程日期窗起;为空时回落到 `startDate` |
| startDayTo | Query | LocalDate | ❌ | `yyyy-MM-dd` | 行程日期窗止;为空时回落到 `endDate` |
| statuses / status | Query | String[] / String | ❌ | 枚举:unassigned/unassigned_urgent/holding/holding_urgent/assigned/canceled/completed | 状态筛选,可多选;`status` 为单值兼容别名 |
| vehicleTypeKeys / typeKeys | Query | String[] | ❌ | 车型大类字典值 | 车型多选筛选 |
| keyword / driverName / contactName / teamNo / plannerName | Query | String | ❌ | - | 关键字、司机、联系人、团号、定制师等模糊搜索 |
| groupBatchId | Query | Long | ❌ | 等值匹配 | 运营团期 ID |
| consultantId | Query | Long | ❌ | - | 定制师 ID 精确筛选 |
| variant | Query | String | ❌ | `list`/`grid` | 视图口径 |
| page(或 pageNo) | Query | Integer | ❌ | ≥1,缺省 1 | 页码 |
| pageSize | Query | Integer | ❌ | 1-100 | 每页条数 |
> 本单不新增、不删除、不改任何参数的类型与必填性;上表只为把「受影响的日期参数」放进完整上下文。
#### 出参 `Result<BoardOrderPageRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| records | BoardOrderRecordVO[] | 本页看板卡片;本次改动只影响带日期筛选时该数组的内容 |
| total | Long | 命中总数;带日期筛选时不再被单条脏行清零 |
| page | Integer | 当前页码 |
| pageSize | Integer | 每页条数 |
| records[].id | Long | 派车行/虚拟条目主键 |
| records[].orderId / orderNo / teamNo | Long / String / String | 子订单 ID、订单编号、团号 |
| records[].assignmentId / assignmentGroupId / requirementId | Long | 派车行 ID、派车分组 ID、当前用车需求 ID |
| records[].assignmentStatus | String | 派单状态码 |
| records[].virtualPending | Boolean | 虚拟待派卡标记(真实记录恒为 `false`) |
| records[].groupVehicleCovered | Boolean | 团车整段接管标记(详见 #8235 交接件) |
| records[].urgentBadge / canAssign / dispatchReadOnly | String / Boolean / Boolean | 加急标签、可派车、只读 |
| records[].startDate / endDate | LocalDate | 行程起止日期 |
| records[].currentVehiclePlate / currentDriverName | String | 当前车辆车牌 / 司机姓名 |
#### 请求示例
```http
GET /admin/fleet/board/orders?startDate=2026-11-01&endDate=2026-11-10&variant=list&pageNo=1&pageSize=20
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": 7330843067599549,
"orderId": 2101014943313670001,
"orderNo": "HL20261106000001",
"teamNo": null,
"assignmentId": 7330843067599549,
"assignmentGroupId": 7330843067599549,
"requirementId": 2101014943313670100,
"assignmentStatus": "holding",
"virtualPending": false,
"groupVehicleCovered": null,
"urgentBadge": null,
"canAssign": true,
"dispatchReadOnly": false,
"startDate": "2026-11-06",
"endDate": "2026-11-07",
"currentVehiclePlate": null,
"currentDriverName": null
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"success": true
}
```
#### 空数据 / 降级响应
无匹配记录时返回空数组与 `total=0`,不报错:
```json
{
"code": 200,
"message": "成功",
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
"success": true
}
```
带日期筛选时的空列表**仍不等同于**「该日期窗确实无单」:日期候选扫描取数失败、结果集缺失、候选行结构畸形(行对象缺失 / 主单 ID 缺失 / 派车组 ID 与派车 ID 同时为空)或槽位键重复时,本页仍按既有 fail-closed 口径整体返回空,但每一种成因现在都会在 `hl-fleet-service` 日志里留下一条可区分的 WARN。不带日期筛选的查询不走这条分支。
#### 错误响应
```json
{
"code": 401,
"message": "未登录或登录已过期",
"success": false,
"data": null
}
```
#### 业务边界
- 只影响**带日期筛选**的分支。不带日期的查询、车辆矩阵接口、看板汇总接口的口径与本次改动前完全一致。
- `requirement_id` 为空的派车行会被剔除:该行原本在不带日期的路径里也匹配不上任何用车需求、不可能出现在看板上,因此剔除它不会让**本该展示**的卡片消失。
- 若某订单在日期窗内**只有**这一条脏行,该订单在带日期的视图里看不到——这与它不带日期时的既有表现一致;让它可见需要的是数据修复,不是看板放行。
- 日期候选结果集本身取到空(无订单上下文)时不查派车行,正常返回空页。
- 鉴权:管理后台登录态,未登录 401(网关拦截)。
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误用法对照
| 场景 | 说明 |
|------|------|
| ✅ 直接使用 `total` 与 `records` | 结构与字段均未变,前端无需做任何解析适配 |
| ✅ 把带日期筛选返回的空列表理解为「结构性异常或确实无单」 | 本次改动后,单条 `requirement_id` 为空的脏行不再制造这种空结果;剩余会制造空结果的是结构性畸形(行缺失、主单 ID 缺失、槽位键缺失或重复),均会在服务端留 WARN |
| ❌ 为「带日期筛选可能返回空」保留前端兜底或本地缓存回填 | 本次改动后不再是必需;保留也不会出错,但会把真实的结构性畸形空结果一并掩盖 |
| ❌ 依赖后端 WARN 的具体文案做前端逻辑 | WARN 只面向排障,文案不是契约 |
### 切换状态时的必要动作
无。本接口是只读 GET,跳过脏行的判定完全由后端在读取时完成,不接受前端传参覆盖,也不需要前端在请求前做任何字段准备。
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 无匹配记录 → 200 + 空数组、`total=0`,不报错
- 日期候选行含 `requirement_id` 为空的行 → 200,该行被跳过并记 WARN,其余订单照常返回(本次改动)
- 日期候选行结构畸形(行对象缺失 / 主单 ID 缺失 / 派车组 ID 与派车 ID 同时缺失)或槽位键重复 → 200 + 整页空(既有 fail-closed 口径保留),并新增带定位信息的 WARN
- order-v3 日期候选页取数失败、超过单次一致性快照上限或上下文畸形 → 200 + 整页空(既有 fail-closed 口径保留)
- 虚拟待派候选层不可用(开关关闭或远端候选不可用)→ 只丢虚拟条目,真实派车行照常返回
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 | 变化说明 |
|------|------|------|----------|
| 请求 / 响应字段 | 无变化 | 无变化 | 本次为纯行为修正,不新增、不删除、不改类型 |
### 行为级对比
| 行为 | 改前 | 改后 | 影响 |
|------|------|------|------|
| 日期窗内某订单存在一条 `requirement_id` 为空的派车行 | **整窗**返回 `records=[]`、`total=0`、`code=200`,服务端无日志 | 只**跳过该行**并记 WARN,同窗其余订单正常返回 | 带日期筛选的列表不再被单条脏数据清空 |
| 同一数据**不带日期**查询 | 该行被跳过 + 一条汇总 WARN | 不变 | 无 |
| 日期候选出现重复槽位键 | 整页失败关闭,无日志 | 整页失败关闭(不变),新增带重复键样本的 WARN | 排障可定位 |
| 日期候选行结构畸形(行缺失 / 主单 ID 缺失 / 槽位键全缺) | 整页失败关闭,无日志 | 整页失败关闭(不变),新增带成因与定位字段的 WARN | 排障可定位 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否。请求参数、响应结构、字段类型与错误码全部不变,仅带日期筛选时的记录集合与 `total` 不再被单条脏数据清零。
- **前端是否必须同步上线**: 否。前端无需修改即可受益。
- **前端 workaround 清理点**: 若前端此前针对「带日期筛选偶发返回空」做过兜底或本地回填,可以在确认后移除;保留不影响功能。
---
## 七、不影响范围
- **仅影响**:管理后台「车务看板」订单列表/网格视图(`/admin/fleet/board/orders`)在**带日期筛选**时的记录集合与 `total`。
- **零影响**:
- 不带日期筛选的看板订单列表口径
- 车辆矩阵未派清单、网格与月度统计(`/admin/fleet/matrix/**`,不走日期候选扫描分支)
- 看板汇总与统计接口
- 派车、改派、确认等写接口
- 用车需求与派车行的数据结构、状态机与任何表结构
---
## 八、测试环境已验证
测试服部署(`deploy:test` 单写租约):`hl-fleet-service` 2026-09-24 10:49:54 滚动部署到 `dev-v3 @ cc70c9ba3`(该提交是本次 squash 合并 `48c755fd1` 的后代),实例 8187 / 8087 均 UP 且健康检查通过。
网关实测(`https://api.test.1814.love`,管理后台登录态切到车务角色):
| # | 请求 | 修复前 `total` | 修复后 `total` |
|---|------|----------------|----------------|
| 1 | `GET /admin/fleet/board/orders`(不带日期) | 243 | 243(对照组,未变)✓ |
| 2 | `...&startDate=2026-11-01&endDate=2026-11-10`(窗内含脏行) | **0** | **8** ✓ |
| 3 | `...&startDayFrom=2026-11-01&startDayTo=2026-11-10`(同窗别名参数) | **0** | **8** ✓ |
| 4 | `...&startDate=2026-09-24&endDate=2026-11-04`(窗内不含脏行) | 71 | 71(对照组,未变)✓ |
| 5 | `...&startDate=2026-11-06&endDate=2026-11-06`(脏行当天) | **0** | **6** ✓ |
| 6 | `...&startDate=2026-11-05&endDate=2026-11-07` | **0** | **6** ✓ |
服务端逐行 WARN(两个实例各两条,与上表实测同刻,原始行):
```text
2026-09-24 10:50:56.063 [http-nio-8087-exec-1] WARN c.h.f.b.service.BoardCandidateSource - 车务看板列表日期候选剔除无用车需求ID的派单行(本行不参与候选): orderId=2101014943313670146 assignmentId=7330843067599548 assignmentGroupId=7330843067599548
2026-09-24 10:50:56.556 [http-nio-8187-exec-2] WARN c.h.f.b.service.BoardCandidateSource - 车务看板列表日期候选剔除无用车需求ID的派单行(本行不参与候选): orderId=2101014943313670146 assignmentId=7330843067599548 assignmentGroupId=7330843067599548
```
那条脏行(`assignment_id=7330843067599548`,`order_id=2101014943313670146`,行程日期 2026-11-06)本身不再出现在任何日期窗结果里——它匹配不上任何用车需求,本就无法在看板渲染;关键变化是它不再让同窗其余订单一起消失。
本地验证:定向 42/42 绿、看板与矩阵影响面回归 379/379 绿、`spotless:check` 与 `mvn -o -pl hl-fleet-service -am verify` 均 BUILD SUCCESS(该模块 4766 个用例 0 失败 0 错误 6 跳过)。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8301](https://git.1814.love/wx/HL/issues/8301)
- 关联 PR: [wx/HL#8310](https://git.1814.love/wx/HL/pulls/8310)
- 同一接口的上一份交接件(#8235,看板订单记录新增 `groupVehicleCovered`),其中「带日期筛选的既有 fail-closed 口径」一条由本文件取代
## 关联 / 联系人
### 链接
- **Issue**: [#8301](https://git.1814.love/wx/HL/issues/8301)
- **PR**: [#8310](https://git.1814.love/wx/HL/pulls/8310)
- **Merge commit**: [48c755fd1](https://git.1814.love/wx/HL/commit/48c755fd1feff715f9ec946b55f77207f23dc942)
### 联系人
- **后端负责人**: @wx
- **前端负责人**: @mmg