比较提交

...
作者 SHA1 备注 提交日期
Mimingguang de05bab5f2 chore(changelog): 标记前端已实现 #5263
changelog-filename-gate / validate (push) Successful in 1s
修改原因:管理后台已经完成最终确认逐车短信选择、真实状态查询和失败受控重试,需要同步前端消费进度与可追溯提交。

修改内容:将 #5263 从 pending 经 claimed 合法迁移为 implemented,登记 hl-ui-pi 与前端提交 138136e5,清理误填的 verified_at,并注明尚未发布和真实页面联调。

实际验证:前端定向 7 个 Vitest 文件 62 项通过,verify:changed 全量通过;changelog 46 项测试、path alias 校验和 git diff --check 通过。

Changelog: changelogs-v2/2026-07/26_5263_最终确认按车选择发送行程短信-修改接口-管理后台.md
2026-07-27 00:27:09 +08:00
Mimingguang 7d76ee0cde chore(changelog): 标记 #5193 被后续契约取代
changelog-filename-gate / validate (push) Successful in 2s
修改原因:#5236 已明确 supersedes #5193,并由前端提交 85851ad6 删除独立接送开关、停止提交兼容字段,继续执行旧契约会回滚现行大交通聚合口径。

修改内容:将 #5193 的前端消费状态从 claimed 收口为 not_required,清理旧负责人并补齐新版消费元数据和取代原因。

实际验证:相关前端 5 个 Vitest 文件共 50 项通过;changelog 仓库 46 项测试通过;path alias 校验与 git diff --check 通过。

Changelog: changelogs-v2/2026-07/23_5193_用车需求增加独立接机送机选择-修改接口-管理后台.md
2026-07-27 00:04:57 +08:00
wx 89248fea03 Merge pull request 'docs: #5263 最终确认短信契约交接' (#41)
changelog-filename-gate / validate (push) Successful in 2s
2026-07-26 20:59:57 +08:00
wx 8083139dc6 docs: mark #5263 backend deployed and gateway verified
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-07-26 20:49:36 +08:00
wx e816810210 docs(changelog): hand off final-confirm itinerary SMS (#5263) 2026-07-26 20:29:00 +08:00
Mimingguang 446721c542 chore(changelog): 标记前端已实现 #5262
changelog-filename-gate / validate (push) Successful in 1s
修改原因:hl-admin 已完成派车逐日车费契约消费,需要回写可追溯的前端实现证据。

修改内容:将 #5262 canonical 与稳定路径 alias 同步迁移为 implemented,登记 hl-ui-pi 和 mmg/hl-ui@2dfe8ab40f3db446d0a079d7511986a2d7fdce33,并注明尚未发布和页面联调。

实际验证:hl-admin 的 pnpm verify:changed --no-cache 全部通过;source git diff --check 通过,alias 与 canonical 状态及证据保持一致。

对应 changelog:changelogs-v2/2026-07/26_5262_派车逐日车费与核单实时接口-修改接口-管理后台.md
2026-07-26 18:48:16 +08:00
Mimingguang b4879f96c9 fix(changelog): 同步 #5262 canonical 领取状态
changelog-filename-gate / validate (push) Successful in 2s
修改原因:#5262 在领取期间新增了稳定路径 alias,首次回写只更新了兼容入口,canonical 仍为 pending,违反 alias 状态一致性。

修改内容:将 canonical 文档同步为 claimed,登记 hl-ui-pi 负责人并修正状态说明;兼容入口保持已领取状态。

实际验证:git diff --check 通过,alias 与 canonical 的 frontend_status、frontend_owner 已一致。

对应 changelog:changelogs-v2/2026-07/26_5262_派车逐日车费与核单实时接口-修改接口-管理后台.md
2026-07-26 18:47:22 +08:00
Mimingguang e49ef47ce9 chore(changelog): 标记前端已领取 #5262
changelog-filename-gate / validate (push) Failing after 1s
修改原因:Pi 主会话 changelog loop 已领取派车逐日车费契约,需要同步前端消费负责人。

修改内容:将 #5262 frontend_status 从 pending 迁移为 claimed,并登记 frontend_owner 为 hl-ui-pi。

实际验证:git diff --check 通过,前端实现由 hl-admin 主仓库任务继续完成。

对应 changelog:changelogs-v2/2026-07/26_5262_派车逐日车费与核单实时接口-修改接口-前端待处理-管理后台.md
2026-07-26 18:45:57 +08:00
wx f4f851bb74 Merge pull request 'fix(changelog): 修复 #5262 文件名校验红叉' (#42)
changelog-filename-gate / validate (push) Successful in 2s
修复 #5262 changelog 文件名门禁,并保留旧路径 alias。
2026-07-26 18:20:24 +08:00
wx 017cc5cadc fix(changelog): repair #5262 filename gate
changelog-filename-gate / validate (pull_request) Successful in 1s
2026-07-26 18:19:03 +08:00
wx 211cd7952d Merge pull request 'docs(changelog): 交接派车逐日车费契约 (#5262)' (#40) from feat/5262-fleet-fee-changelog into main
changelog-filename-gate / validate (push) Failing after 1s
2026-07-26 16:41:14 +08:00
wx 2e6dac729f docs(changelog): hand off fleet daily fee contract (#5262)
changelog-filename-gate / validate (pull_request) Failing after 2s
2026-07-26 16:39:32 +08:00
wx 3cb62cf417 Merge pull request 'fix: restore stable changelog path aliases for #5252' (#39) from fix/5252-changelog-path-alias into main
changelog-filename-gate / validate (push) Successful in 2s
2026-07-26 15:06:59 +08:00
wx 5c7fa5dbe6 fix(changelog): restore stable path aliases for #5252
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-07-26 15:02:13 +08:00
Mimingguang e35ff35c58 chore(changelog): 标记前端已实现 #5253
changelog-filename-gate / validate (push) Failing after 2s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

修改内容:将 frontend_status 与已有 legacy frontend 同步为 implemented,记录负责人 hl-ui-codex,并关联 mmg/hl-ui@e43200d6eede059f82aacfa00de5656b379f8981;发布和验收字段保持不变。

实际验证:回写器已校验目标文件、状态单调性、提交范围和 Front Matter 内容,提交只包含当前 changelog。

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/26_5253_按车辆记录订单总车费并接入核单-修改接口-管理后台.md
2026-07-26 12:47:17 +08:00
Mimingguang c021d338b7 chore(changelog): 标记前端已领取 #5253
changelog-filename-gate / validate (push) Failing after 2s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

修改内容:将 frontend_status 与已有 legacy frontend 同步为 claimed,记录负责人 hl-ui-codex,实现引用保持为空;发布和验收字段保持不变。

实际验证:回写器已校验目标文件、状态单调性、提交范围和 Front Matter 内容,提交只包含当前 changelog。

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/26_5253_按车辆记录订单总车费并接入核单-修改接口-管理后台.md
2026-07-26 12:24:47 +08:00
wx ec3986cd7e Merge pull request 'docs: #5253 按车辆记录订单总车费并接入核单' (#34) from docs/5253-vehicle-total-fee into main
changelog-filename-gate / validate (push) Successful in 2s
2026-07-26 12:21:06 +08:00
API Changelog Bot c3e6cd38c2 docs(changelog): mark #5253 deployed and gateway verified
changelog-filename-gate / validate (pull_request) Successful in 1s
2026-07-26 12:20:15 +08:00
Mimingguang 248493b0f5 chore(changelog): 标记前端已实现 #5257
changelog-filename-gate / validate (push) Failing after 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

修改内容:将 frontend_status 与已有 legacy frontend 同步为 implemented,记录负责人 hl-ui-codex,并关联 mmg/hl-ui@594dfa44b497b40a1e75c81b3e60abba8deb6137;发布和验收字段保持不变。

实际验证:回写器已校验目标文件、状态单调性、提交范围和 Front Matter 内容,提交只包含当前 changelog。

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/26_5257_车务看板按用车需求聚合多车型槽位-修改接口-管理后台.md
2026-07-26 11:40:29 +08:00
wx 228b573705 Merge pull request 'docs: 标记 #5254 后端与网关已验证' (#38) from chore/5254-backend-verified into main
changelog-filename-gate / validate (push) Successful in 2s
2026-07-26 11:36:38 +08:00
wx 6e40fa96de docs: mark backend delivery verified (#5254)
changelog-filename-gate / validate (pull_request) Successful in 1s
2026-07-26 11:36:23 +08:00
Mimingguang 73d6f843ef chore(changelog): 标记前端已领取 #5257
changelog-filename-gate / validate (push) Failing after 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

修改内容:将 frontend_status 与已有 legacy frontend 同步为 claimed,记录负责人 hl-ui-codex,实现引用保持为空;发布和验收字段保持不变。

实际验证:回写器已校验目标文件、状态单调性、提交范围和 Front Matter 内容,提交只包含当前 changelog。

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/26_5257_车务看板按用车需求聚合多车型槽位-修改接口-管理后台.md
2026-07-26 11:32:10 +08:00
Mimingguang ba8a01befb chore(changelog): 标记前端已实现 #5255
changelog-filename-gate / validate (push) Has been cancelled
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

修改内容:将 frontend_status 与已有 legacy frontend 同步为 implemented,记录负责人 hl-ui-codex,并关联 mmg/hl-ui@f9251d0ffabf13641630588733d103e00f35e85d;发布和验收字段保持不变。

实际验证:回写器已校验目标文件、状态单调性、提交范围和 Front Matter 内容,提交只包含当前 changelog。

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/26_5255_已派车恢复改派与行程单入口更正-修改接口-管理后台.md
2026-07-26 11:32:08 +08:00
Mimingguang a4f468a732 chore(changelog): 标记前端已领取 #5255
changelog-filename-gate / validate (push) Failing after 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

修改内容:将 frontend_status 与已有 legacy frontend 同步为 claimed,记录负责人 hl-ui-codex,实现引用保持为空;发布和验收字段保持不变。

实际验证:回写器已校验目标文件、状态单调性、提交范围和 Front Matter 内容,提交只包含当前 changelog。

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/26_5255_已派车恢复改派与行程单入口更正-修改接口-管理后台.md
2026-07-26 11:24:39 +08:00
wx 699af1531f Merge pull request 'docs(api): 更正车务改派与行程单入口 (#5255)' (#37) from changelog/5255-fleet-reassign-print-handoff into main
changelog-filename-gate / validate (push) Successful in 1s
2026-07-26 11:22:27 +08:00
wx f4161862c2 docs(api): 更正车务改派与行程单入口 (#5255)
changelog-filename-gate / validate (pull_request) Successful in 1s
2026-07-26 11:21:03 +08:00
wx cb52c7918e Merge pull request 'docs(api): 交接车务看板需求聚合契约 (#5257)' (#36) from docs/5257-fleet-requirement-card into main
changelog-filename-gate / validate (push) Successful in 2s
2026-07-26 11:15:37 +08:00
API Changelog Bot 14482b6a22 docs(api): hand off fleet requirement aggregation (#5257)
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-07-26 11:14:42 +08:00
wx 6c23a7367f Merge PR #35: 交接订单侧已配置车辆详情字段
changelog-filename-gate / validate (push) Successful in 2s
关联 wx/HL#5254;frontend_status 保持 pending。
2026-07-26 10:01:19 +08:00
API Changelog Bot d0d0d87fec docs(changelog): remove publication placeholders (#5253)
changelog-filename-gate / validate (pull_request) Failing after 1s
2026-07-26 09:02:48 +08:00
API Changelog Bot 551d0564d0 docs(changelog): hand off vehicle total fee contract (#5253)
changelog-filename-gate / validate (pull_request) Failing after 1s
2026-07-26 09:00:38 +08:00
共修改 19 个文件,包含 1609 行新增和 25 行删除
@@ -44,3 +44,6 @@ jobs:
- name: Validate changelog frontmatter
run: npm run check:frontmatter -- --event "$GITHUB_EVENT_PATH"
- name: Validate changelog path aliases
run: npm run check:path-aliases
+12 -1
查看文件
@@ -30,7 +30,9 @@ changelogs-v2-mp/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除
检测器读取 `git diff --name-status -z --find-renames` 的结果,只校验本次 diff 新出现的目标路径:
- `A`(新增)、`C`(复制)和 `R`(重命名)的目标路径必须通过规则。
- `M`(修改历史文件)和 `D`(删除)豁免,不会因存量错误命名阻断。
- `M`(修改历史文件)豁免,不会因存量错误命名阻断。
- 已发布文件的 `D`(删除)和 `R`(重命名)默认以 `E_PATH_STABILITY` 阻断。确需迁移时,必须在
`changelog-path-aliases.json` 登记旧路径到 canonical 的精确关系,并保留可读取的兼容入口。
- 重命名到受控目录时,新目标路径必须使用校验当天的上海日期。
本地校验:
@@ -38,6 +40,7 @@ changelogs-v2-mp/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除
```bash
npm test
npm run check:filenames -- --base origin/main --head HEAD
npm run check:path-aliases
```
生产 CLI 故意不提供 `--date` 或日期环境变量;测试只通过导出的纯函数注入 `Date`。规则失败返回退出码 `1`,Git/事件/参数等基础设施错误返回 `2`。
@@ -70,12 +73,20 @@ pending → claimed → implemented → released → verified
- `FRONTEND_CONSUMPTION_STATUS_GUIDE.md`
- `BACKEND_CHANGELOG_DELIVERY_GUIDE.md`
已下发路径是消费契约的一部分,不通过重命名表达状态。历史路径已发生迁移时:
- `changelog-path-aliases.json` 是机器可识别的唯一映射源;
- alias 文件必须保留完整 `hl-changelog/v2` frontmatter,并用 `canonical_path` 指向 canonical;
- 前端状态更新使用 `npm run changelog:transition -- <path> <status> ... --write`,命令会同时更新
canonical 与全部 alias;对同一状态和证据重复执行不会产生文件变更。
本地校验:
```bash
npm test
npm run check:filenames -- --base origin/main --head HEAD
npm run check:frontmatter -- --base origin/main --head HEAD
npm run check:path-aliases
```
## CI 与服务端阻断边界
+25
查看文件
@@ -49,6 +49,30 @@ not_required
## 更新命令
### 存在历史路径 alias 的文档
消费线程已经记录的路径不得因文件改名失效。先解析路径:
```powershell
npm run changelog:resolve -- "changelogs-v2/2026-07/旧路径.md"
```
状态回写统一使用 alias-aware 命令;传旧路径或 canonical 均会同时更新整组文件:
```powershell
npm run changelog:transition -- `
"changelogs-v2/2026-07/旧路径.md" implemented `
--owner frontend-team `
--frontend-ref "mmg/hl-ui@abc1234" `
--write
```
相同状态和证据可以重复执行,第二次不会产生文件变更。alias 关系集中记录在
`changelog-path-aliases.json`,并由 `npm run check:path-aliases` 校验文件存在性、ticket、
canonical 指向及前端状态一致性。
### 无 alias 的文档
领取:
```powershell
@@ -107,3 +131,4 @@ QA 或产品可以协助更新 `verified_at`,但必须基于实际页面验证
- `hl-changelog/v1` 继续可读和索引,不强制一次性迁移。
- 文件名带“前端待处理”不代表真实状态;需要继续流转时补充 v2 元数据。
- 不通过重命名表达消费状态,避免破坏文件名校验和历史链接。
- 已被消费的路径如确需规范化,必须先登记 alias、保留兼容入口,并使用 alias-aware 命令同步状态。
+17
查看文件
@@ -0,0 +1,17 @@
{
"schema": "hl-changelog-path-aliases/v1",
"aliases": [
{
"ticket": "5216",
"alias": "changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-前端待处理-管理后台.md",
"canonical": "changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md",
"reason": "兼容前端消费线程在文件重命名前持久化的稳定路径"
},
{
"ticket": "5262",
"alias": "changelogs-v2/2026-07/26_5262_派车逐日车费与核单实时接口-修改接口-前端待处理-管理后台.md",
"canonical": "changelogs-v2/2026-07/26_5262_派车逐日车费与核单实时接口-修改接口-管理后台.md",
"reason": "修复首次交接文件名包含前端状态导致的 CI 失败,并保留已发布路径兼容入口"
}
]
}
@@ -5,11 +5,14 @@ title: "用车需求增加独立接机送机选择"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "claimed"
frontend_status: "claimed"
frontend_owner: "hl-ui-codex"
frontend: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
updated_at: "2026-07-25T03:19:00.677Z"
target_release: ""
verified_at: ""
status_note: "#5236 明确 supersedes #5193;85851ad6 已删除独立开关并改用实时大交通聚合,继续实现会回滚后续契约。"
updated_at: "2026-07-27"
base: "dev-v3"
generated: "2026-07-23T17:39:06+08:00"
---
@@ -0,0 +1,33 @@
---
schema: "hl-changelog/v2"
ticket: "5216"
title: "派车看板补充槽位接送路线与就绪摘要"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@cd493f83a7881401552494fc5a90fbb87395131b"
target_release: "hl-ui/v2.1"
verified_at: ""
canonical_path: "changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md"
status_note: "兼容 #5216 在文件重命名前已被前端消费线程持久化的稳定路径;状态更新必须通过 alias-aware transition 同步 canonical。"
updated_at: "2026-07-26"
base: "dev-v3"
---
# 【路径兼容·管理后台】派车看板补充槽位接送路线与就绪摘要
本文件是 #5216 已下发旧路径的兼容入口。完整接口说明以
`changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md`
为 canonical;不得删除或再次重命名本文件。
## 变更接口
- 不新增或修改业务接口;本文件仅恢复前端状态回写所需的稳定路径。
## 验证证据
- `changelog-path-aliases.json` 记录旧路径与 canonical 的机器可识别关系。
- alias 校验与状态同步测试覆盖读取、同步写入和重复写入。
@@ -11,8 +11,9 @@ frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@cd493f83a7881401552494fc5a90fbb87395131b"
target_release: "hl-ui/v2.1"
verified_at: ""
path_aliases: "changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-前端待处理-管理后台.md"
status_note: "后端与网关已验证;前端 implemented 状态由前端消费线程维护,本次仅迁移 schema。"
updated_at: "2026-07-25T03:29:38.101Z"
updated_at: "2026-07-26"
base: "dev-v3"
generated: "2026-07-24T14:24:00+08:00"
---
@@ -0,0 +1,167 @@
---
schema: "hl-changelog/v2"
ticket: "5253"
title: "按车辆记录订单总车费并接入核单"
consumer: "multiple"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@e43200d6eede059f82aacfa00de5656b379f8981"
target_release: ""
verified_at: ""
status_note: "后端已合入 dev-v3 并部署测试环境;自动计价、手工总价、缺价阻断、部分改派分段、核单实时合计与冻结均已通过网关验证。前端尚未领取,frontend_status 保持 pending。"
updated_at: "2026-07-26T04:47:17.025Z"
base: "dev-v3"
generated: "2026-07-26T08:57:17+08:00"
---
# 按车辆记录订单总车费并接入核单
车务在每个车辆槽位/派车段记录一个最终总车费;价格日历只提供自动参考,
车务可以手工改总价但不回写价格日历。订单核单冻结 Fleet 最终快照并按同一来源计入实际成本。
## 关联
- Issue: #5253
- 后端分支: `feat/5253-vehicle-total-fee`
- Changelog 分支: `docs/5253-vehicle-total-fee`
## 变更接口
| 表面 | 方法 | 路径 | 变化 |
|---|---|---|---|
| `frontend_api` | `POST` | `/admin/fleet/assignments/candidates` | 入参新增计费服务日;车辆候选新增自动总车费、完整性和缺价日期 |
| `frontend_api` | `POST` | `/admin/fleet/assignments` | 入参新增单车最终总车费、调整原因、计费日/免费日口径;响应回显费用快照 |
| `frontend_api` | `POST` | `/admin/fleet/assignments/batch` | 每辆车分别提交最终总车费与调整原因,并共享计费日/免费日口径 |
| `frontend_api` | `POST` | `/admin/fleet/assignments/:assignmentId/confirm` | 最终确认前可补/改该派车段总车费;响应回显最终费用 |
| `frontend_api` | `POST` | `/admin/fleet/assignments/:assignmentId/change` | 改派新段与原车保留段分别记录总车费 |
| `frontend_api` | `GET` | `/admin/fleet/board/orders/:orderId` | 每个派车组新增自动参考、最终总价、来源、调整原因、计费/免费日期与冻结态 |
| `frontend_api` | `GET` | `/v3/admin/order/:orderId/settlement/vehicle-fees` | 新增核单车辆总车费明细;冻结前读 Fleet,冻结后读 Order 快照 |
| `frontend_api` | `POST` | `/v3/admin/order/:orderId/settlement/vehicle-fees/confirm` | 新增确认并冻结车辆总车费 |
| `frontend_api` | `POST` | `/v3/admin/order/:orderId/settlement/step6/submit` | 响应新增 `vehicleCost` |
| `frontend_api` | `GET` | `/v3/admin/order/:orderId/settlement/summary` | 响应新增 `vehicleCost` |
| `internal_feign` / `shared_java` | `GET` | `/internal/fleet/orders/:orderId/vehicle-fees` | 新增 Fleet→Order 的按派车组只读费用快照 |
路径中的 `:orderId`、`:assignmentId` 表示既有雪花 ID 字符串传输约定。
## 字段与行为
### 派车候选与写接口
- `AssignmentCandidateReqVO` 新增 `chargeableServiceDates`;不传默认全部服务日,
空数组表示全部免费。
- 候选车辆新增:
- `autoVehicleFeeTotal`:价格日历覆盖日期的小计,金额按字符串消费;
- `vehicleFeePriceComplete`:是否覆盖全部计费服务日;
- `missingVehicleFeeDates`:缺价的计费服务日;
- 旧字段 `protocolPrice` 保留但已废弃,新页面不得用它计算总车费。
- 创建/批量创建/改派新增 `vehicleFeeTotal`、`vehicleFeeAdjustmentReason`、
`chargeableServiceDates`、`vehicleFeeWaiverReason`、
`confirmAllServiceDatesFree`。
- 部分改派额外新增 `retainedVehicleFeeTotal` 与
`retainedVehicleFeeAdjustmentReason`。原车保留段和替换段分别录入,
后端不按天数比例拆分。
- 最终确认新增 `vehicleFeeTotal` 与 `vehicleFeeAdjustmentReason`。
- 创建、改派和确认响应新增 `vehicleFeeAutoTotal`、
`vehicleFeeAutoComplete`、`vehicleFeeTotal`、`vehicleFeeSource`、
`vehicleFeeAdjustmentReason`;金额字段按字符串消费。
### 看板详情
每个派车组新增:
- `chargeableServiceDates`、`freeServiceDates`、`vehicleFeeWaiverReason`;
- `vehicleFeeAutoTotal`、`vehicleFeeAutoComplete`;
- `vehicleFeeTotal`、`vehicleFeeSource`(`AUTO`、`MANUAL`、`INCOMPLETE`);
- `vehicleFeeAdjustmentReason`、`vehicleFeeFrozen`。
派车完成后 `vehicleFeeFrozen=true`,不得原地编辑费用;只能走改派形成新的费用段。
订单核单完成后禁止继续改派。
### 核单车辆总车费
`SettlementVehicleFeesRespVO`:
- 顶层:`orderId`、`frozen`、`totalVehicleFee`、`items`;
- 每项按一个派车组返回车辆、司机、日期、计费/免费日期、自动参考、
最终总价、来源、调整审计和 `settlementReady`;
- `POST .../confirm` 仅接受全部当前派车组 `settlementReady=true`
且最终总车费非空的快照,成功后 `frozen=true`;
- 两辆车返回两项并分别计费,`totalVehicleFee` 是各项 `vehicleFeeTotal` 之和;
- `vehicleCost` 同步进入核单提交与汇总实际成本,司机费用只保留司机额外费用,
避免车辆基础服务费重复计入。
### 内部契约
新增共享 DTO `OrderVehicleFeeSnapshotDTO`,包含需求、派车组、稳定车辆槽位、
车辆/司机、服务区间、计费/免费日期、自动参考、最终总价、调整审计和
`settlementReady`。Order 必须按当前 `requirementId` 精确筛选,
不得跨需求或跨改派段合并。
## 契约影响文件
- `hl-common/hl-common-core/src/main/java/com/hulalv/common/dto/fleet/OrderVehicleFeeSnapshotDTO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/controller/OrderDriverVehicleInternalController.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/AssignmentCandidateReqVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/AssignmentCandidateRespVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/AssignmentWriteRespVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/BatchCreateAssignmentReqVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/ChangeAssignmentReqVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/ChangeAssignmentRespVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/ConfirmReqVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/ConfirmRespVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/CreateAssignmentReqVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/board/vo/BoardOrderDetailVO.java`
- `hl-fleet-service/src/test/java/com/hulalv/fleet/assignment/controller/OrderDriverVehicleInternalControllerTest.java`
- `hl-order-service-v3/src/main/java/com/hulalv/order/fleet/feign/FleetDriverVehicleFeignClient.java`
- `hl-order-service-v3/src/main/java/com/hulalv/order/fleet/feign/FleetDriverVehicleFeignFallbackFactory.java`
- `hl-order-service-v3/src/main/java/com/hulalv/order/settlement/controller/admin/SettlementController.java`
- `hl-order-service-v3/src/main/java/com/hulalv/order/settlement/controller/admin/vo/SettlementSubmitRespVO.java`
- `hl-order-service-v3/src/main/java/com/hulalv/order/settlement/controller/admin/vo/SettlementSummaryRespVO.java`
- `hl-order-service-v3/src/main/java/com/hulalv/order/settlement/controller/admin/vo/SettlementVehicleFeesRespVO.java`
- `hl-order-service-v3/src/test/java/com/hulalv/order/fleet/feign/FleetDriverVehicleFeignContractTest.java`
- `hl-order-service-v3/src/test/java/com/hulalv/order/settlement/controller/admin/SettlementControllerTest.java`
## 前端/调用方动作
- 派车弹窗按“每辆车一个总车费”展示和提交;不要展示或要求车务填写每日价格。
- 默认展示后端 `autoVehicleFeeTotal`。缺价时用
`vehicleFeePriceComplete=false` 和 `missingVehicleFeeDates` 提示,
车务仍必须填写该车最终总车费及调整原因后才能直接派定/最终确认。
- 车务手改只提交 `vehicleFeeTotal`,不得写回车型价格日历。
- 收费日/免费日仅作为记录与核单依据;全部免费时必须提交免费原因和二次确认。
- 多车订单为每个车辆槽位分别编辑总车费,不提供订单级总价输入框。
- 已派定/已完成段按 `vehicleFeeFrozen` 禁用直接编辑,只保留改派入口;
已核单订单同时禁用改派。
- 核单页先查询 `GET .../vehicle-fees` 展示逐车明细和合计,
再调用 `POST .../vehicle-fees/confirm` 冻结;冻结后只读。
- 所有雪花 ID 和金额字段按字符串处理,禁止转 JavaScript `number`。
- `frontend_status` 保持 `pending`;真实前端领取后使用工作流迁移到 `claimed`。
## 验证证据
- Fleet producer:`mvn -pl hl-fleet-service -am verify` 通过;
`mvn -pl hl-fleet-service -am spotless:check` 通过。
- Order consumer:`mvn -pl hl-order-service-v3 -am verify` 通过。
- 契约定向测试:
`OrderDriverVehicleInternalControllerTest`、
`FleetDriverVehicleFeignContractTest`、
`SettlementControllerTest` 通过。
- 费用行为覆盖:自动合计、缺价、手工覆盖、全免费、部分改派、
派定后冻结、核单冻结、首个计费日只入账一次及并发门禁均有测试。
- OpenAPI/oasdiff:`not_configured`;fallback 证据见任务胶囊
`openapi-fallback.md`。
- producer/consumer 或 Spring Cloud Contract:`not_configured`;
fallback 证据见任务胶囊 `consumer-contract-fallback.md`。
- 测试部署:Fleet Deploy Panel 任务 `5131f604` 成功,8087/8187
双实例健康;补充修复 PR `wx/HL#5261` 已合入 `dev-v3`。
- 网关验证:自动计价完整;缺价返回 `605044` 阻断;多车分别保存
`1720.00` 与 `1234.56`;部分改派同一稳定槽位拆为原车保留段
`900.00` 与新车接替段 `1100.00`,核单实时合计 `2000.00`;
核单确认后冻结快照只读。`gateway_status=verified`。
- 测试数据:隔离订单已按 `hl-data-cleanup/v1` manifest
`5253-cfcc4c8eea2b` 事务清理,全部目标后置计数为 0,价格日历未变化。
- 兼容性结论:现有路径均为增量字段;旧 `protocolPrice` 继续返回,
但新页面必须改用每车总费用字段。
@@ -0,0 +1,158 @@
---
schema: "hl-changelog/v2"
ticket: "5255"
title: "已派车恢复改派与行程单入口更正"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@f9251d0ffabf13641630588733d103e00f35e85d"
target_release: ""
verified_at: ""
status_note: "后端已部署并完成网关验证;前端需恢复已派车改派按钮,并将看板行程单入口直接复用订单详情现有打印实现。本文件覆盖 #5186 的已派车禁用改派及复制司机 H5 链接口径。"
updated_at: "2026-07-26T03:32:07.805Z"
base: "dev-v3"
---
# 车务:已派车恢复改派与行程单入口更正
> **服务**: `hl-fleet-service`
>
> **工单**: [wx/HL#5255](https://git.1814.love:8443/wx/HL/issues/5255)
>
> **后端 PR**: [wx/HL#5258](https://git.1814.love:8443/wx/HL/pulls/5258)
>
> **影响范围**: 车务管理 → 派单看板操作区、既有派车/改派弹窗、行程单打印入口
## 关键变化
- 当前有效派单状态为 `assigned` 时,派单看板列表现在返回 `canAssign=true`,并继续下发
`CHANGE_ASSIGNMENT`。车辆已经配好后,只要派单尚未进入 `completed/canceled` 终态,车务仍可
随时进入既有改派流程,再次更换当前车辆槽位的车辆、司机或两者。
- `holding/holding_urgent` 的改派能力不变;`completed/canceled` 继续不可改派。
- 看板操作区的行程单入口统一为“打印行程单”,直接复用订单详情已有的
`PrintItineraryModal.vue` 和 `getPrintItinerary(orderId)`,不新写打印组件、打印接口或打印数据模型。
## ⚠️ 对 #5186 的口径更正
本文件取代
`23_5186_排车中订单恢复派车派人入口-修改接口-管理后台.md`
中的以下两项旧交接:
1. #5186 响应矩阵把 `assigned` 与终态一起写成 `canAssign=false`。该口径已失效:
`assigned` 现在允许改派,只有 `completed/canceled` 禁止改派。
2. #5186 要求看板复制 `currentAssignment.itineraryUrl`,并明确“不复用
`PrintItineraryModal.vue`”。该口径已撤销:看板不再把“复制司机 H5 链接”作为本入口的实现,
而是直接复用订单详情现有打印代码。
#5186 中与本次两项更正无关的候选上下文、稳定槽位和待确认流程说明继续有效。本次也不删除后端
司机 H5 短链能力;只是看板这个用户入口不再消费它。
## 变更接口
| 方法 | 路径 | 本次口径 |
| --- | --- | --- |
| `GET` | `/admin/fleet/board/orders` | 既有字段语义更正:有效 `assigned` 记录返回 `canAssign=true`,且 `availableActionCodes` 包含 `CHANGE_ASSIGNMENT` |
| `POST` | `/admin/fleet/assignments/<assignmentId>/change` | 既有改派接口,路径和请求/响应契约不变;继续按稳定车辆槽位与生效日原子替换 |
| `GET` | `/v3/admin/order/<orderId>/print-itinerary` | 订单详情已有打印接口,本次无后端变更;看板直接复用现有前端调用 |
## 1. 派单看板动作语义
`GET /admin/fleet/board/orders` 的字段名、类型和必填性均未变化。前端按后端动作字段渲染:
| `assignmentStatus` | `canAssign` | `availableActionCodes` | 看板主操作 |
| --- | ---: | --- | --- |
| `unassigned/unassigned_urgent` | `true` | 包含 `ASSIGN` | “派车派人”,进入既有首次派车流程 |
| `holding/holding_urgent` | `true` | 包含 `CHANGE_ASSIGNMENT` | “改派”,进入既有改派流程 |
| `assigned` | `true` | 包含 `CHANGE_ASSIGNMENT` | “改派”,已配车后仍可再次调整 |
| `completed/canceled` | `false` | 不包含 `CHANGE_ASSIGNMENT` | 不显示改派入口 |
前端不得再把“已有车辆/司机”或 `assignmentStatus === 'assigned'` 当作隐藏改派按钮的条件。
入口首先以 `canAssign === true` 判断是否可进入,再以 `availableActionCodes` 区分首次派车或改派。
## 2. 继续复用既有按槽位改派
点击“改派”后继续使用当前派车弹窗和现有
`POST /admin/fleet/assignments/<assignmentId>/change`:
- `assignmentId` 取用户选中的 `activeAssignments[]` 派车组/槽位,不得固定取代表项后误改其它车;
- 生效日及之后只替换该稳定 `assignmentSlotId` 的逐日切片;
- 同一订单其它车辆槽位保持不变;
- 可只换车辆、只换司机或同时替换;
- 成功后重新拉取看板列表与详情,不缓存旧的 `canAssign`、动作码或派车组数据;
- 后端返回 `ORDER_HAS_OTHER_VEHICLES` 等既有警告时继续沿用当前强提示。
本次没有新增写接口,也没有修改改派请求字段、错误码或事务规则。
## 3. 行程单入口直接复用订单详情打印代码
管理后台现有可复用实现位于:
- `src/views/order-v2/detail/modals/PrintItineraryModal.vue`
- `src/api/orderV2.js` 的 `getPrintItinerary(orderId)`
- 既有接口 `GET /v3/admin/order/<orderId>/print-itinerary`
看板按以下方式接入:
- 操作文案统一为“打印行程单”,点击后把当前订单的字符串 `orderId` 传给现有
`PrintItineraryModal`;
- 打印预览、加载、错误提示、页面排版和打印动作全部沿用订单详情现有组件;
- 可抽取共享挂载点或直接复用组件,但不得复制组件源码形成第二套打印实现;
- 不新增 fleet 打印 API,不在前端重新组装行程节点、费用、住宿、大交通或每日行程;
- 不调用看板详情去读取 `currentAssignment.itineraryUrl`,不执行剪贴板复制,也不继续使用
`ItinerarySendSheet.vue` 承载这个入口;
- 无 `orderId` 时不打开弹窗、不提示成功,沿用现有缺少订单上下文的错误态。
## 前端展示矩阵
| 页面区域 | 展示内容 | 数据来源 | 包含/排除状态 | 空数据表现 | 标签与颜色 | 守恒规则 |
| --- | --- | --- | --- | --- | --- | --- |
| 派车看板操作区 | 派车派人 | `availableActionCodes` 含 `ASSIGN` | 包含待派车;排除终态 | 无候选沿用现有提示 | 沿用现有待派状态色 | 只创建目标槽位 |
| 派车看板操作区 | 改派 | `availableActionCodes` 含 `CHANGE_ASSIGNMENT` | 包含待确认;排除终态 | 无有效派单不显示 | 沿用现有待确认状态色 | 只替换选中槽位 |
| 派车看板操作区 | 改派 | `canAssign=true` 且含 `CHANGE_ASSIGNMENT` | 包含已派车且未完结;排除已完结/已取消 | 无剩余可改服务日时展示既有后端错误 | 沿用现有已派状态色 | 配完仍可再次改派,其他槽位不变 |
| 派车看板操作区 | 不显示改派 | 无 `CHANGE_ASSIGNMENT` | 仅已完结/已取消 | 不展示按钮 | 沿用终态灰 | 不恢复终态 |
| 派车看板操作区 | 打印行程单 | 订单 `orderId` 与既有打印接口 | 包含可查看订单;排除无订单 ID | 沿用现有打印弹窗加载/错误态 | 沿用现有打印按钮样式 | 只复用一套 `PrintItineraryModal.vue` |
## 前端处理清单
- [ ] `assigned + canAssign=true + CHANGE_ASSIGNMENT` 显示“改派”,点击进入既有改派流程。
- [ ] `holding/holding_urgent` 继续显示“改派”,首次待派仍显示“派车派人”。
- [ ] `completed/canceled` 不显示改派入口。
- [ ] 用户选择哪个 `activeAssignments[]` 槽位,就把该槽位的 `assignmentId` 传给既有 change 接口。
- [ ] 改派成功后刷新列表与详情,当前槽位更新,其他槽位数量和身份保持不变。
- [ ] 将看板行程单操作统一为“打印行程单”,直接复用订单详情
`PrintItineraryModal.vue + getPrintItinerary(orderId)`。
- [ ] 不新增打印组件/API,不读取或复制 `currentAssignment.itineraryUrl`,不继续使用
`ItinerarySendSheet.vue` 实现该入口。
- [ ] 覆盖无订单 ID、打印接口失败和打印数据空态,全部沿用既有打印弹窗行为。
## 验证证据
- 后端提交:`9ea7d555a078101cbf57d1571d106cd0ec2863af`;PR #5258。
- `BoardControllerTest + BoardOrderServiceTest` 共 54 项通过。
- `AssignmentServiceTest#change_directFromEffectiveDate_replacesDailySlicesAndWarnsOtherVehicle`
通过,验证只替换目标稳定槽位,其它车辆槽位不取消。
- `mvn -pl hl-fleet-service spotless:check` 通过。
- `mvn -pl hl-fleet-service -am verify` 通过;fleet 207 套件、2,373 项测试,
0 失败、0 错误、1 个既有跳过。
- 测试环境已滚动部署 `hl-fleet-service`;Nacos `test` 命名空间的 8087、8187 两实例健康。
- 测试网关查询 `assigned` 返回 4 条真实记录,全部为
`canAssign=true + CHANGE_ASSIGNMENT`,HTTP/code 均为 200。
- 测试环境当时没有 `holding/holding_urgent/completed/canceled` 样本;这些状态不冒充网关实测,
由已通过的服务状态矩阵与 Controller JSON 测试覆盖。
- 网关证据 SHA-256:
`1409d22ce9fc2c86101d9f811fef867e0493f177191fb8ac5ee30eb4427185e2`。
- OpenAPI/oasdiff:`not_configured`。字段名、类型和 requiredness 未变;仓库没有可复现的
Swagger2 → OAS3 导出链且未安装 `oasdiff`,已用 Controller JSON、服务状态矩阵和当前
`hl-ui` 消费源码做人工回退核对。
- 消费者契约/Spring Cloud Contract:`not_required`。本次没有内部 Feign 或共享 Java DTO 变化。
## 不影响范围
- 不修改或部署 `D:/work2/hl-ui`;`frontend_status` 保持 `pending`,页面实现和发布独立流转。
- 不删除司机 H5 行程短链、签名 token 或公开行程接口;仅更正看板入口的前端消费方式。
- 不新增/删除 API 字段,不改变字段类型、必填性、错误码或雪花 ID 的字符串消费要求。
- 不修改首次派车、待确认推进、取消派单、司机确认、资源占用或通知冻结规则。
- 不新增 DDL,不迁移、清理或回填数据。
@@ -0,0 +1,281 @@
---
schema: "hl-changelog/v2"
ticket: "5257"
title: "车务看板按用车需求聚合多车型槽位"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@594dfa44b497b40a1e75c81b3e60abba8deb6137"
target_release: ""
verified_at: "2026-07-26T11:13:51+08:00"
status_note: "后端 PR #5259 已合并为 dev-v3@01bf9c627,并重新部署测试环境及通过网关复验;本契约明确替代 #5216 的按槽位卡片维度。前端尚未认领,需按需求卡消费 assignmentSlots。"
updated_at: "2026-07-26T03:40:29.152Z"
base: "dev-v3"
---
# 车务看板按用车需求聚合多车型槽位
## 关联
- Issue: [wx/HL#5257](https://git.1814.love:8443/wx/HL/issues/5257)
- 服务: `hl-fleet-service`
- 前端仓库/分支: `mmg/hl-ui` / `v2.1`
- 影响范围: 车务管理 → 派车看板列表、需求卡和需求详情
- 被本契约替代的旧口径:
[#5216 派车看板补充槽位接送路线与就绪摘要](./24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md)
中“每张卡对应一个稳定车辆槽位”的维度说明
## 关键变化
同一订单的一条当前有效用车需求可能同时需要多种车型、多个车辆槽位。例如
`SUV×1 + 商务车×1` 是 **1 条用车需求、合计 2 辆车**,不能显示成 2 条“用车需求”。
`GET /admin/fleet/board/orders` 的 `data.records[]` 从派车槽位粒度调整为当前
active `requirementId` 粒度:
- 同一 `requirementId` 只返回一条 record。
- `requiredVehicles[]` 展示全部车型组及数量。
- `assignmentProgress.totalSlots` 展示合计需要的车辆数。
- 新增 `assignmentSlots[]`,完整保留逐辆派车身份、状态、车辆和司机。
- 原顶层单槽位字段兼容保留,统一表示当前最需要处理的代表槽位。
- 状态、车型或司机筛选命中需求内任一槽位时,需求只返回一次。
- 后端先按需求聚合,再排序和分页;`total` 与汇总接口不再按槽位重复计数。
请求参数、路径、HTTP method、响应包络、错误码、数据库和派车执行语义均不变。
## 变更接口
```http
GET /admin/fleet/board/orders
```
### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "26-5256",
"orderId": "2080200000000000000",
"requirementId": "2080200000000000900",
"requiredVehicles": [
{
"vehicleType": "suv",
"categoryLabel": "SUV",
"seats": 5,
"count": 1
},
{
"vehicleType": "mpv",
"categoryLabel": "商务车",
"seats": 7,
"count": 1
}
],
"assignmentProgress": {
"totalSlots": 2,
"unassignedSlots": 2,
"holdingSlots": 0,
"assignedSlots": 0,
"completedSlots": 0,
"canceledSlots": 0
},
"assignmentStatus": "unassigned",
"assignmentId": "2080200000000000001",
"assignmentSlotId": "2080200000000000101",
"fleetItemIndex": 0,
"canAssign": true,
"canRejectRequirement": true,
"assignmentSlots": [
{
"assignmentId": "2080200000000000001",
"assignmentGroupId": "2080200000000000201",
"assignmentSlotId": "2080200000000000101",
"fleetItemIndex": 0,
"slotSummary": {
"requiredVehicleType": "suv",
"requiredVehicleTypeLabel": "SUV",
"requiredSeats": 5
},
"baseAssignmentStatus": "unassigned",
"assignmentStatus": "unassigned",
"assignmentStatusLabel": "待派车",
"availableActionCodes": ["ASSIGN"],
"vehiclePlate": null,
"vehicleModel": null,
"vehicleSeats": null,
"driverName": null,
"driverPhone": null,
"urgentBadge": null,
"canAssign": true
},
{
"assignmentId": "2080200000000000002",
"assignmentGroupId": "2080200000000000202",
"assignmentSlotId": "2080200000000000102",
"fleetItemIndex": 1,
"slotSummary": {
"requiredVehicleType": "mpv",
"requiredVehicleTypeLabel": "商务车",
"requiredSeats": 7
},
"baseAssignmentStatus": "unassigned",
"assignmentStatus": "unassigned",
"assignmentStatusLabel": "待派车",
"availableActionCodes": ["ASSIGN"],
"vehiclePlate": null,
"vehicleModel": null,
"vehicleSeats": null,
"driverName": null,
"driverPhone": null,
"urgentBadge": null,
"canAssign": true
}
]
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"success": true
}
```
## 新增字段 `assignmentSlots[]`
所有雪花 ID 必须按字符串消费。
| 字段 | 类型 | 空值 | 说明 |
|---|---|---|---|
| `assignmentId` | `String` | 否 | 逐槽位派车/改派操作使用的派单 ID |
| `assignmentGroupId` | `String` | 否 | 同一派车组 ID |
| `assignmentSlotId` | `String` | 否 | 跨逐日切片稳定的车辆槽位 ID |
| `fleetItemIndex` | `Integer` | 否 | 当前需求 `fleet[]` 展开后的槽位序号,0 起 |
| `slotSummary` | `Object` | 否 | 该槽位车型、座位、服务范围和整单槽位数 |
| `baseAssignmentStatus` | `String` | 否 | 基础落库态 |
| `assignmentStatus` | `String` | 否 | 当前有效状态,可含紧急派生态 |
| `assignmentStatusLabel` | `String` | 否 | 后端中文状态标签 |
| `lifecycleStageCode/lifecycleStageLabel` | `String` | 否 | 该槽位生命周期阶段 |
| `currentStep` | `Integer` | 否 | 该槽位当前步骤 |
| `availableActionCodes` | `String[]` | 否 | 该槽位可用动作;需求级驳回不在此数组 |
| `vehiclePlate/vehicleModel` | `String` | 是 | 已派车辆信息 |
| `vehicleSeats` | `Integer` | 是 | 已派车辆座位数 |
| `vehicleFleetTeamId` | `String` | 是 | 已派车辆所属车队 ID |
| `vehicleFleetTeamName/vehicleFleetTeamType/vehicleFleetTeamSettleType` | `String` | 是 | 已派车辆所属车队信息 |
| `driverName` | `String` | 是 | 已派司机姓名 |
| `driverPhone` | `String` | 是 | 已脱敏司机手机号 |
| `urgentBadge` | `String` | 是 | `T-N` 或 `Nh`;非紧急为 `null` |
| `canAssign` | `Boolean` | 否 | 该槽位是否可进入派车/改派流程 |
## 顶层兼容字段
以下既有字段没有删除,旧前端继续读取不会报错:
- `assignmentId/assignmentGroupId/assignmentSlotId/fleetItemIndex`
- `slotSummary`
- `assignmentStatus/assignmentStatusLabel`
- `lifecycleStageCode/lifecycleStageLabel/currentStep/availableActionCodes`
- `currentVehicle*`
- `currentDriver*`
- `urgentBadge/canAssign/canRejectRequirement`
它们统一指向代表槽位,选择优先级为:
```text
未派 → 排车中 → 已派 → 已完成 → 已取消
```
同级按 `fleetItemIndex/assignmentSlotId/assignmentId` 稳定排序。部分已派时,顶层仍指向
未派槽位,保证原“派车派人”入口可继续派下一辆;所有槽位的真值以
`assignmentSlots[]` 和 `assignmentProgress` 为准。
`canRejectRequirement` 与需求级驳回动作只读外层 record。任一槽位已进入
`holding/assigned` 时,外层不会错误开放驳回;槽位级 `availableActionCodes` 不包含需求级驳回。
## 筛选、分页和汇总
- 状态多选:任一槽位命中即返回该需求一次。
- 车型多选:任一需求槽位命中即返回该需求一次,`requiredVehicles[]` 仍保留全部车型。
- 司机筛选/关键词:任一槽位司机命中即返回该需求一次。
- 混合状态:顶层状态取代表槽位状态;完整状态分布读取 `assignmentProgress` 和
`assignmentSlots[]`。
- 空态:无当前有效需求或无 fleet 看板候选时返回 `records=[]/total=0`,不按人数合成虚假槽位。
- 顺序:先聚合为唯一 requirement record,再确定性排序和分页。
- `data.total`:查询范围内唯一 active `requirementId` 数,不是车辆槽位数。
- `GET /admin/fleet/board/summary`:状态和今日出团数使用相同需求粒度,不重复计数。
守恒关系:
```text
assignmentProgress.totalSlots
== unassignedSlots
+ holdingSlots
+ assignedSlots
+ completedSlots
+ canceledSlots
assignmentProgress.totalSlots
== sum(requiredVehicles[].count)
```
## 前端处理清单
- [ ] 看板列表对每个 `records[]` 只渲染一张需求卡,不再按
`fleetItemIndex/assignmentSlotId` 拆卡,也不得展开 `assignmentSlots[]` 生成额外卡片。
- [ ] 列表 row key 优先使用 `requirementId`;仅兼容历史空值时回退 `id/orderId`。
- [ ] 需求卡展示 `requiredVehicles[]` 的全部车型组,并显示
`assignmentProgress.totalSlots` 为“需要 N 辆车”。
- [ ] 需求详情遍历 `assignmentSlots[]` 展示全部车辆槽位、各自状态和已派车辆/司机。
- [ ] 顶层按钮可继续使用代表槽位字段;逐辆操作必须使用所选
`assignmentSlots[i].assignmentId/fleetItemIndex/assignmentSlotId`,不得复用其他槽位 ID。
- [ ] 需求级驳回只使用外层 `canRejectRequirement/availableActionCodes`,不从槽位数组推断。
- [ ] 混合状态显示以 `assignmentProgress` 为准,不用顶层单一状态覆盖所有槽位。
- [ ] 继续按既有稳定状态 token 映射颜色,不按中文文案判断色值。
- [ ] 覆盖单车型×1、多车型各×1、同车型×2、部分已派、全已派、完结/取消和空列表场景。
## 不影响范围
- 不修改用车需求 `fleet[]` 的业务语义。
- 不合并、删除或改写真实 `fleet_assignment`;派车仍逐辆、逐槽位执行。
- 不修改派车、确认、取消、改派、需求驳回接口的请求结构。
- 不修改 §7 矩阵派单的车辆槽位维度。
- 不修改数据库、网关路由、错误码、车辆/司机占用、保险或费用。
- 本交接不代表已修改、发布或验证 `mmg/hl-ui`。
## 后端验证
- 精确复现测试:同一 `requirementId` 下 `SUV×1 + 商务车×1` 返回
`total=1/records=1`、`requiredVehicles=2` 组、`assignmentSlots=2`、
`assignmentProgress.totalSlots=2`。
- 同车型×2、部分已派、全已派、混合完结/取消、状态多选、聚合后分页和汇总去重均有自动化覆盖。
- 相关定向测试 64 项通过,0 failures/errors。
- OpenAPI diff 状态为 `not_configured`:当前环境没有 `oasdiff`,仓库也没有可复现的
Swagger 2 → OpenAPI 3 导出链;契约审查保存了 Controller/VO 字段对比和自动化测试
作为人工 fallback 证据。
## 验证证据
- 后端 commit:`fd641651144ab429ec1b371e1902a70bf3e7f025`
- 后端 PR:[wx/HL#5259](https://git.1814.love:8443/wx/HL/pulls/5259)
- 合并 commit:`dev-v3@01bf9c6274d5c40e00e65b84c5224189b0fd06b8`
- 合并后测试部署:现有部署 API 已滚动发布 `dev-v3`,
`hl-fleet-service:8087/8187` 均健康;构建、发布成功,部署日志尾部的
`ERROR/FATAL/Exception` 命中数为 0。
- 网关复现订单 `26-5256`:HTTP/业务码均为 200,`records=1`、`total=1`、
`requiredVehicles=SUV×1+MPV×1`、`assignmentSlots=2` 且逐槽位 ID 唯一、
`assignmentProgress.totalSlots=2`、状态数量之和为 2;脱敏证据 SHA-256 为
`7ba2b05d8bdf335207771c59e089357dbe25d1fe0ac4aa0db1c91f910da563f5`。
- fleet 定向测试 64 项通过;`mvn -pl hl-fleet-service spotless:check`
611 个文件通过;`mvn -pl hl-fleet-service -am verify` 中 fleet 2386 项测试
0 failures/errors、skipped 1,完整 reactor `BUILD SUCCESS`。
- OpenAPI diff 为 `not_configured`,已保存 Controller/VO 字段对比、兼容语义、
自动化测试和脱敏网关结构作为人工 fallback 证据。
- `frontend_status=pending`:本次未修改、部署或验证 `mmg/hl-ui`;
页面仍需按“1 条需求卡 + 2 个槽位详情”的新契约完成消费。
@@ -0,0 +1,33 @@
---
schema: "hl-changelog/v2"
ticket: "5262"
title: "派车逐日车费、只读总价与核单实时接口"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "mmg/hl-ui@2dfe8ab40f3db446d0a079d7511986a2d7fdce33"
target_release: ""
verified_at: "2026-07-26T16:37:00+08:00"
canonical_path: "changelogs-v2/2026-07/26_5262_派车逐日车费与核单实时接口-修改接口-管理后台.md"
status_note: "兼容 #5262 首次交接时已发布的稳定路径;状态更新必须通过 alias-aware transition 同步 canonical。"
updated_at: "2026-07-26"
base: "dev-v3"
---
# 【路径兼容·管理后台】派车逐日车费、只读总价与核单实时接口
本文件是 #5262 首次交接路径的兼容入口。完整接口说明以
`changelogs-v2/2026-07/26_5262_派车逐日车费与核单实时接口-修改接口-管理后台.md`
为 canonical;不得删除或再次重命名本文件。
## 变更接口
- 不新增或修改业务接口;本文件仅保留已下发路径,并把消费方引导到 canonical 文档。
## 验证证据
- `changelog-path-aliases.json` 记录旧路径与 canonical 的机器可识别关系。
- alias 校验确保前端状态字段与 canonical 同步。
@@ -0,0 +1,112 @@
---
schema: "hl-changelog/v2"
ticket: "5262"
title: "派车逐日车费、只读总价与核单实时接口"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "mmg/hl-ui@2dfe8ab40f3db446d0a079d7511986a2d7fdce33"
target_release: ""
verified_at: "2026-07-26T16:37:00+08:00"
path_aliases: "changelogs-v2/2026-07/26_5262_派车逐日车费与核单实时接口-修改接口-前端待处理-管理后台.md"
status_note: "后端 PR wx/HL#5266 已合并到 dev-v3(9bfd21de6),Fleet/Order 测试双实例已部署并完成管理端网关与 internal Feign 实测;hl-admin 已在 2dfe8ab40f3db446d0a079d7511986a2d7fdce33 接入逐日车费、只读总价和旧字段移除,verify:changed 通过,尚未发布及页面联调。本契约替代 #5253 的“手工填写每车总价”口径。"
updated_at: "2026-07-26"
base: "dev-v3"
generated: "2026-07-26T16:20:00+08:00"
---
# 派车逐日车费、只读总价与核单实时接口
## 关联
- Issue: [wx/HL#5262](https://git.1814.love:8443/wx/HL/issues/5262)
- 服务:`hl-fleet-service`、`hl-order-service-v3`
- 前端仓库:`mmg/hl-ui`(本文仅交接,不代表已修改前端)
- 替代口径:[#5253 按车辆记录订单总车费并接入核单](./26_5253_按车辆记录订单总车费并接入核单-修改接口-管理后台.md)
## 关键变化
1. 多日派车按服务日保存 assignment,每个槽位 4 天即 4 条每日记录。
2. 价格日历改为提供逐日参考价;车务可覆盖本次派车的某日车费,覆盖值不回写价格日历。
3. `vehicleFeeTotal` 改为只读合计,恒等于收费日 `assignmentPrice` 之和。
4. 创建、批量派车、确认和改派请求继续兼容解析旧总价字段,但只要传值即返回稳定业务错误,不再接受手工总价。
5. 配置车辆不写 Order 核单表;核单后端通过新的 Fleet internal API 实时读取逐日车辆费用。
## 变更接口
| 方法 | 路径 | 变化 |
| --- | --- | --- |
| `POST` | `/admin/fleet/assignments/candidates` | 车辆候选新增逐日车费参考 |
| `POST` | `/admin/fleet/assignments` | 新增 `dailyVehicleFees`;旧 `vehicleFeeTotal` 禁止传值 |
| `POST` | `/admin/fleet/assignments/batch` | 每个最终车辆槽位分别提交 `dailyVehicleFees` |
| `POST` | `/admin/fleet/assignments/:assignmentId/change` | 新派车段提交逐日车费;保留段沿用原逐日快照 |
| `POST` | `/admin/fleet/assignments/:assignmentId/confirm` | 旧总价字段禁止传值;总价由已保存逐日车费只读计算 |
| `GET` | `/admin/fleet/board/orders/:orderId` | 槽位返回 `dailyVehicleFees` 和只读合计 |
## 请求字段
`dailyVehicleFees[]`:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `serviceDate` | `LocalDate` | 是 | 本次覆盖的服务日期 |
| `price` | `Decimal` | 是 | 本次派车单日车费,最小 `0.00`,最多 2 位小数 |
规则:
- 未提交覆盖值的收费日使用车型价格日历当天价格。
- 收费日缺少日历价格且未提交覆盖值时,后端拒绝最终派车。
- 覆盖值与日历参考价不一致时提交 `vehicleFeeAdjustmentReason`。
- 免费日期 `assignmentPrice` 固定为 `"0.00"`,不计入总价。
- `vehicleFeeTotal`、`retainedVehicleFeeTotal` 以及对应旧调整原因字段不得再由前端提交。
## 响应字段
候选、派车写响应和看板槽位新增或统一返回 `dailyVehicleFees[]`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `serviceDate` | `LocalDate` | 服务日期 |
| `chargeable` | `Boolean` | 是否收取车费 |
| `calendarPrice` | `Decimal/null` | 车型价格日历参考价;缺价时为空 |
| `assignmentPrice` | `Decimal/null` | 本次派车单日车费;收费日必须有值,免费日为 `"0.00"` |
| `source` | `String` | `CALENDAR` / `OVERRIDE` / `FREE` / `MISSING` |
| `calendarPriceMissing` | `Boolean` | 价格日历是否缺价 |
`vehicleFeeTotal` 继续返回,但语义变为只读:
```text
vehicleFeeTotal = sum(dailyVehicleFees[chargeable=true].assignmentPrice)
```
金额字段按字符串消费,雪花 ID 继续按字符串消费。
## 页面展示矩阵
| 区域 | 展示 | 空态 | 状态/颜色 | 守恒规则 |
| --- | --- | --- | --- | --- |
| 收费日期 | 每日显示日历参考价与本次派车价 | 选车前“待计算”;缺价“价格日历缺价” | 参考价中性、覆盖蓝、缺价橙 | 一服务日一条派车记录 |
| 最终总车费 | 只读合计,不渲染金额输入框 | 缺价时“价格不完整” | 正常中性、缺价橙 | 等于全部收费日本次派车价之和 |
| 已结束行程 | 只允许查看逐日价格和总价 | 不适用 | 只读灰 | 前端禁用与后端拒绝一致 |
## 前端处理清单
- [ ] 移除“最终总车费”输入框,改为只读合计。
- [ ] 收费日期逐日展示 `calendarPrice`,并允许编辑当前槽位的 `assignmentPrice`。
- [ ] 仅把修改后的日期组装为 `dailyVehicleFees`,不调用车型价格日历写接口。
- [ ] 使用 `source` 和 `calendarPriceMissing` 展示覆盖与缺价状态。
- [ ] 创建、批量派车、确认和改派请求不再传旧总价字段。
- [ ] 行程结束后禁用逐日车费、槽位和改派入口。
## 验证证据
- OpenAPI/oasdiff:`not_configured`;已完成 Controller/VO 源码比对和 Fleet 接口测试回退证据。
- Spring Cloud Contract:`not_configured`;已完成 Fleet producer、Order Feign consumer 和 shared DTO 测试回退证据。
- Fleet:`mvn -pl hl-fleet-service -am verify` 与 `spotless:check` 通过。
- Order:`mvn -pl hl-order-service-v3 -am verify` 完成,Surefire 零失败并生成可执行 JAR。
- 部署:Fleet `630858f8`、Order `63f9ba60` 成功,8087/8187 与 8086/8186 双实例健康。
- 网关:管理端订单详情返回 4 条逐日车费及约定的 6 个逐日字段;internal Feign 正向响应严格为顶层 5 个字段、item 12 个字段。
- `frontend_status`:`pending`;真实领取后再迁移为 `claimed`。
@@ -0,0 +1,158 @@
---
schema: "hl-changelog/v2"
ticket: "5263"
title: "最终确认按车选择发送行程短信"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "mmg/hl-ui@138136e50c4bbf9931ee020bd28d40977264b64e"
target_release: ""
verified_at: ""
status_note: "前端已在 138136e5 接入 HOLD 最终确认逐车短信必选、权威状态查询和 FAILED 受控重试;定向 7 个文件 62 项及 verify:changed 全量验证通过。尚未发布或完成真实页面联调。"
updated_at: "2026-07-27"
base: "dev-v3"
generated: "2026-07-26T17:33:00+08:00"
---
# 最终确认按车选择发送行程短信
## 关联
- Issue: [wx/HL#5263](https://git.1814.love:8443/wx/HL/issues/5263)
- Changelog PR: [wx/hl-api-changelog#41](https://git.1814.love:8443/wx/hl-api-changelog/pulls/41)
- 服务:`hl-fleet-service`、`hl-user-service`
- 前端仓库:`mmg/hl-ui`(本文仅交接,不代表已修改前端)
- 前置车费契约:[#5262 派车逐日车费、只读总价与核单实时接口](./26_5262_派车逐日车费与核单实时接口-修改接口-前端待处理-管理后台.md)
## 关键变化
1. 仅在 HOLD 排车的最终确认阶段,每个车辆组必须显式选择“发送短信”或“不发送短信”,没有默认值。
2. 选择发送时,确认事务只落可靠发送意图;短信异步发送失败不会回滚已完成的派车确认。
3. 短信只发给该车辆组当前师傅,包含订单摘要、接送摘要和签名行程短链,不包含客户手机号。
4. 选择不发送时只完成派车确认,不创建行程短信事件。
5. 多车订单逐车独立选择、独立投递、独立查询状态和受控重试。
6. 已派定后的订单人数基线复核只能沿用原选择,不允许借复核修改选择或重复发送。
7. 直接派车流程不受影响;#5262 已废弃的手工车辆总价入参仍然禁止提交。
## 变更接口
| 方法 | 路径 | 变化 |
| --- | --- | --- |
| `POST` | `/admin/fleet/assignments/:assignmentId/confirm` | 请求新增必填 `sendItinerarySms`;响应新增短信选择、事件与状态 |
| `GET` | `/admin/fleet/assignments/:assignmentId/itinerary-sms` | 新增单车/车辆组短信审计状态查询 |
| `POST` | `/admin/fleet/assignments/:assignmentId/itinerary-sms/retry` | 新增明确失败后的车务受控重试 |
## 最终确认
### `POST /admin/fleet/assignments/:assignmentId/confirm`
#### 请求字段
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `sendItinerarySms` | `Boolean` | 是 | `true` 发送;`false` 不发送;省略或 `null` 返回参数错误 |
| `requestId` | `String` | 是 | 最长 64 字符的幂等请求标识 |
| `vehicleFeeTotal` | `Decimal` | 否 | 历史兼容字段;非空即拒绝,最终总车费继续由 #5262 逐日车费只读合计 |
| `vehicleFeeAdjustmentReason` | `String` | 否 | 历史兼容字段;非空即拒绝 |
请求示例:
```json
{
"sendItinerarySms": true,
"requestId": "fleet-final-confirm-26-8411-car-1"
}
```
#### 新增响应字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `sendItinerarySms` | `Boolean` | 本车辆组最终确认时保存的选择 |
| `itinerarySmsEventId` | `String/null` | 可靠短信事件 ID;不发送时为空 |
| `itinerarySmsStatus` | `String` | 首次确认返回 `PENDING` 或 `NOT_SENT`;已派定复核回显真实状态 |
## 短信状态
### `GET /admin/fleet/assignments/:assignmentId/itinerary-sms`
响应 `data`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `assignmentId` | `String` | 派单 ID |
| `assignmentGroupId` | `String` | 跨服务日车辆组 ID |
| `assignmentSlotId` | `String` | 稳定车辆槽位 ID |
| `sendItinerarySms` | `Boolean/null` | 未最终确认或历史数据时为 `null` |
| `status` | `String` | `NOT_APPLICABLE` / `NOT_SENT` / `PENDING` / `SENT` / `FAILED` / `CANCELED` |
| `eventId` | `String/null` | 可靠短信事件 ID |
| `retryCount` | `Integer` | 已发生的失败重试次数 |
| `canRetry` | `Boolean` | 当前是否允许受控重试 |
| `sentAt` | `LocalDateTime/null` | 供应商确认的真实发送时间 |
| `lastError` | `String/null` | 已脱敏的最近失败或待对账原因 |
前端以 `status` 为权威,不得仅凭最终确认接口成功就显示“短信已发送”。
## 受控重试
### `POST /admin/fleet/assignments/:assignmentId/itinerary-sms/retry`
请求:
```json
{
"reason": "短信通道配置已恢复,车务确认重发",
"requestId": "retry-sms-26-8411-car-1"
}
```
规则:
- 仅 `FAILED` 且 `canRetry=true` 时展示并调用重试。
- `SENT`、`PENDING`、待供应商对账、`CANCELED`、未选择发送时禁止重试。
- 重试复用原事件与供应商幂等键,不新建并行短信事件。
- 司机或车辆组身份已变化时,旧事件收敛为 `CANCELED`,不得发给旧师傅。
## 短信与隐私约束
短信模板参数固定为:
| 参数 | 内容 |
| --- | --- |
| `summary` | 脱敏订单摘要 |
| `transfer` | 接送摘要 |
| `code` | 签名行程短链 |
短信正文及模板参数不得包含客户手机号。真实联系人信息仅在既有签名行程 H5 中按授权展示。
## 页面展示矩阵
| 区域/状态 | 数据源 | 展示 | 空态/禁用 | 颜色 | 守恒规则 |
| --- | --- | --- | --- | --- | --- |
| 最终确认车辆卡片 | 本地待提交选择 | “发送短信”/“不发送短信”二选一 | 未选择时禁止确认并提示必选 | 发送蓝色,不发送中性灰 | 每个车辆组恰好一个选择 |
| 确认后状态 | `GET .../itinerary-sms.status` | 待发送/已发送/发送失败/已取消/未发送 | 历史数据为“不适用” | 待发送蓝、已发送绿、失败红、取消灰、未发送中性灰 | 不以确认成功冒充发送成功 |
| 失败操作 | `canRetry` | “重试短信” | `canRetry=false` 时隐藏或禁用 | 可重试橙色 | 同一事件串行重试 |
| 多车订单 | 每个 `assignmentGroupId` | 每车独立选择与状态 | 不做订单级统一默认 | 各卡片独立 | 一车选择不得覆盖另一车 |
## 前端处理清单
- [ ] 最终确认页按车辆组渲染无默认值的短信二选一。
- [ ] 未完成选择时不提交确认请求,并展示明确校验提示。
- [ ] 确认请求始终显式提交 `sendItinerarySms`,不再依赖后端默认值。
- [ ] 确认后通过状态接口展示真实投递状态。
- [ ] 仅在 `FAILED && canRetry=true` 时允许填写原因并调用重试。
- [ ] 已派定复核回显原选择并保持只读,不提供改选入口。
- [ ] 短信状态按展示矩阵处理空态、颜色及多车独立性。
- [ ] 确认和复核请求继续不提交 #5262 已废弃的手工总车费字段。
## 验证证据
- OpenAPI/oasdiff:`not_configured`;已完成 Controller/VO 源码比对和接口测试回退证据。
- 消费者契约:`not_required`;未修改 internal Feign 或共享 Java DTO。
- 代码与测试:PR `wx/HL#5270` 已合入 `dev-v3`,merge commit 为 `f0a96c10124c3188e60e1291e7f28d768af50e3a`;`mvn -pl hl-user-service,hl-fleet-service -am test`、`mvn -pl hl-fleet-service spotless:check`、`mvn -pl hl-fleet-service -am verify` 均通过。
- 测试部署:`hl-user-service` 与 `hl-fleet-service` 已从合并后的 `dev-v3` 完成双实例滚动部署并通过健康检查。
- 网关:显式“不发送”业务验收 11/11 通过;合并后只读复验 3/3 通过,最终状态为 `assigned + NOT_SENT`,无短信事件且不可重试。
- `frontend_status`:`pending`;真实领取后再迁移为 `claimed` 并填写 `frontend_owner`。
@@ -4,14 +4,14 @@ ticket: "5254"
title: "订单侧已配置车辆补充车型车队服务日期与日单价"
consumer: "admin"
change_type: "修改接口"
backend_status: "pending"
gateway_status: "pending"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
status_note: "后端已合入 dev-v3 并部署测试环境;网关已验证新增字段、多车守恒和手机号脱敏。"
updated_at: "2026-07-26"
base: "dev-v3"
generated: "2026-07-26T09:57:21+08:00"
@@ -25,13 +25,13 @@ generated: "2026-07-26T09:57:21+08:00"
## 关联
- Issue: #5254
- PR: 待补充
- PR: wx/HL#5260
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| GET | `/v3/admin/order/{id}/itinerary` | `data.vehicleGroup.assignments[]` |
| GET | `/v3/admin/order/:id/itinerary`(`:id` 为订单 ID) | `data.vehicleGroup.assignments[]` |
### `data.vehicleGroup.assignments[]`
@@ -89,6 +89,12 @@ generated: "2026-07-26T09:57:21+08:00"
`mvn -pl hl-order-service-v3 -am -Dtest=OrderControllerTest#getItinerary_validId_returns200 -Dsurefire.failIfNoSpecifiedTests=false test`
(1 项通过)。
- Fleet Spotless:`mvn -pl hl-fleet-service spotless:check`(通过)。
- 网关验证:待补充
- 联合验证:`mvn -pl hl-fleet-service,hl-order-service-v3 -am verify`
(BUILD SUCCESS;Fleet 2387 项通过,Spotless 通过;order-v3 定向 81 项通过)。
- 测试环境:`hl-fleet-service`、`hl-order-service-v3` 已从 `dev-v3`
滚动部署,两实例均健康。
- 网关验证:连续 6 次调用 `GET /v3/admin/order/:id/itinerary` 均返回 3 个唯一车辆连续段;
新增车型、车队、服务日期、服务天数和计划日单价字段存在,手机号保持脱敏且未返回原始手机号。
- 降级路径:Fleet 调用失败或空响应由 `OrderDetailServiceTest` 覆盖,本次未人为中断测试环境 Fleet。
- 兼容性结论:仅新增可空响应字段并补齐既有空字段;内部 Feign JSON 双向兼容,
不修改方法、路径、参数、必填项、枚举或错误码。
+5 -2
查看文件
@@ -3,9 +3,12 @@
"private": true,
"type": "module",
"scripts": {
"test": "node --test tests/validate-changelog-filenames.test.mjs tests/validate-changelog-frontmatter.test.mjs",
"test": "node --test tests/validate-changelog-filenames.test.mjs tests/validate-changelog-frontmatter.test.mjs tests/changelog-path-aliases.test.mjs",
"check:filenames": "node scripts/validate-changelog-filenames.mjs",
"check:frontmatter": "node scripts/validate-changelog-frontmatter.mjs"
"check:frontmatter": "node scripts/validate-changelog-frontmatter.mjs",
"check:path-aliases": "node scripts/changelog-path-aliases.mjs --check",
"changelog:resolve": "node scripts/changelog-path-aliases.mjs --resolve",
"changelog:transition": "node scripts/changelog-path-aliases.mjs --transition"
},
"engines": {
"node": ">=20"
+361
查看文件
@@ -0,0 +1,361 @@
#!/usr/bin/env node
import {
existsSync,
readFileSync,
writeFileSync,
} from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import {
parseFrontmatter,
validateFrontendState,
validateFrontendTransition,
} from './validate-changelog-frontmatter.mjs';
export const PATH_ALIAS_MANIFEST = 'changelog-path-aliases.json';
export const PATH_ALIAS_SCHEMA = 'hl-changelog-path-aliases/v1';
const CONTROLLED_ROOTS = new Set(['changelogs-v2', 'changelogs-v2-mp']);
const FRONTEND_FIELDS = [
'frontend_status',
'frontend_owner',
'frontend_ref',
'target_release',
'verified_at',
];
function aliasError(code, inputPath, message) {
return { code, path: inputPath, message };
}
export function normalizeRepoPath(value, label = 'path') {
if (typeof value !== 'string' || value.length === 0 || /[\0\r\n]/.test(value)) {
throw new Error(`${label} 必须是非空且不含控制字符的 Git 路径`);
}
if (value.includes('\\') || path.posix.isAbsolute(value) || /^[A-Za-z]:/.test(value)) {
throw new Error(`${label} 必须使用仓库内 POSIX 相对路径`);
}
const normalized = path.posix.normalize(value);
if (normalized !== value || normalized === '.' || normalized.startsWith('../')) {
throw new Error(`${label} 不能包含 .、.. 或非规范路径片段`);
}
const [root] = normalized.split('/');
if (!CONTROLLED_ROOTS.has(root) || !normalized.endsWith('.md')) {
throw new Error(`${label} 必须指向受控 changelog Markdown`);
}
return normalized;
}
export function parsePathAliasManifest(text) {
let value;
try {
value = JSON.parse(String(text));
} catch (error) {
throw new Error(`alias manifest 不是合法 JSON: ${error.message}`);
}
if (value?.schema !== PATH_ALIAS_SCHEMA || !Array.isArray(value.aliases)) {
throw new Error(`alias manifest 必须使用 ${PATH_ALIAS_SCHEMA} 且包含 aliases 数组`);
}
const seenAliases = new Set();
const aliases = value.aliases.map((entry, index) => {
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
throw new Error(`aliases[${index}] 必须是对象`);
}
const ticket = String(entry.ticket ?? '');
if (!/^[1-9]\d*$/.test(ticket)) {
throw new Error(`aliases[${index}].ticket 必须是正整数`);
}
const alias = normalizeRepoPath(entry.alias, `aliases[${index}].alias`);
const canonical = normalizeRepoPath(entry.canonical, `aliases[${index}].canonical`);
if (alias === canonical) {
throw new Error(`aliases[${index}] 的 alias 与 canonical 不能相同`);
}
if (seenAliases.has(alias)) {
throw new Error(`alias 重复: ${alias}`);
}
if (typeof entry.reason !== 'string' || entry.reason.trim() === '') {
throw new Error(`aliases[${index}].reason 不能为空`);
}
seenAliases.add(alias);
return { ticket, alias, canonical, reason: entry.reason.trim() };
});
for (const entry of aliases) {
if (seenAliases.has(entry.canonical)) {
throw new Error(`暂不允许 alias 链或环: ${entry.alias} -> ${entry.canonical}`);
}
}
return { schema: PATH_ALIAS_SCHEMA, aliases };
}
export function loadPathAliasManifest(root = process.cwd()) {
const manifestPath = path.join(root, PATH_ALIAS_MANIFEST);
if (!existsSync(manifestPath)) {
return { schema: PATH_ALIAS_SCHEMA, aliases: [] };
}
return parsePathAliasManifest(readFileSync(manifestPath, 'utf8'));
}
export function resolveChangelogPath(inputPath, manifest) {
const normalized = normalizeRepoPath(inputPath);
return manifest.aliases.find(({ alias }) => alias === normalized)?.canonical ?? normalized;
}
function absoluteRepoPath(root, repoPath) {
return path.join(root, ...repoPath.split('/'));
}
function readDocument(root, repoPath) {
const absolute = absoluteRepoPath(root, repoPath);
if (!existsSync(absolute)) {
return { error: aliasError('E_ALIAS_MISSING', repoPath, '文件不存在') };
}
const content = readFileSync(absolute, 'utf8');
const { metadata } = parseFrontmatter(content);
if (!metadata) {
return { error: aliasError('E_ALIAS_FRONTMATTER', repoPath, '缺少 YAML Front Matter') };
}
return { absolute, content, metadata };
}
export function validatePathAliases(root = process.cwd(), manifest = loadPathAliasManifest(root)) {
const errors = [];
for (const entry of manifest.aliases) {
const aliasDocument = readDocument(root, entry.alias);
const canonicalDocument = readDocument(root, entry.canonical);
if (aliasDocument.error) {
errors.push(aliasDocument.error);
continue;
}
if (canonicalDocument.error) {
errors.push(canonicalDocument.error);
continue;
}
for (const [repoPath, metadata] of [
[entry.alias, aliasDocument.metadata],
[entry.canonical, canonicalDocument.metadata],
]) {
if (metadata.schema !== 'hl-changelog/v2') {
errors.push(aliasError('E_ALIAS_SCHEMA', repoPath, '兼容组文档必须使用 hl-changelog/v2'));
}
if (metadata.ticket !== entry.ticket) {
errors.push(aliasError(
'E_ALIAS_TICKET',
repoPath,
`ticket=${metadata.ticket || '(空)'},manifest ticket=${entry.ticket}`,
));
}
}
if (aliasDocument.metadata.canonical_path !== entry.canonical) {
errors.push(aliasError(
'E_ALIAS_TARGET',
entry.alias,
`canonical_path 必须等于 ${entry.canonical}`,
));
}
for (const field of FRONTEND_FIELDS) {
if ((aliasDocument.metadata[field] ?? '') !== (canonicalDocument.metadata[field] ?? '')) {
errors.push(aliasError(
'E_ALIAS_STATE',
entry.alias,
`${field} 与 canonical 不一致`,
));
}
}
}
return { checkedCount: manifest.aliases.length, errors };
}
function shanghaiDate(now = new Date()) {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(now);
const values = Object.fromEntries(parts.map(({ type, value }) => [type, value]));
return `${values.year}-${values.month}-${values.day}`;
}
function replaceFrontmatterFields(content, fields) {
const normalized = String(content).replaceAll('\r\n', '\n');
const end = normalized.indexOf('\n---\n', 4);
if (!normalized.startsWith('---\n') || end < 0) {
throw new Error('文档缺少可写的 YAML Front Matter');
}
const lines = normalized.slice(4, end).split('\n');
const remaining = new Set(Object.keys(fields));
const updated = lines.map((line) => {
const separator = line.indexOf(':');
if (separator < 0) {
return line;
}
const key = line.slice(0, separator).trim();
if (!remaining.has(key)) {
return line;
}
remaining.delete(key);
return `${key}: ${JSON.stringify(String(fields[key] ?? ''))}`;
});
if (remaining.size > 0) {
throw new Error(`frontmatter 缺少待更新字段: ${[...remaining].join(', ')}`);
}
return `---\n${updated.join('\n')}\n---\n${normalized.slice(end + 5)}`;
}
export function transitionAliasedFrontendState(
root,
inputPath,
{
status,
owner,
frontendRef,
targetRelease,
verifiedAt,
reason = '',
},
{ now = new Date(), write = false } = {},
) {
const manifest = loadPathAliasManifest(root);
const before = validatePathAliases(root, manifest);
if (before.errors.length > 0) {
throw new Error(`alias manifest 校验失败: ${before.errors.map(({ message }) => message).join(';')}`);
}
const canonical = resolveChangelogPath(inputPath, manifest);
const canonicalDocument = readDocument(root, canonical);
if (canonicalDocument.error) {
throw new Error(`${canonicalDocument.error.path}: ${canonicalDocument.error.message}`);
}
const current = canonicalDocument.metadata.frontend_status;
const transitionErrors = validateFrontendTransition(current, status, reason);
if (transitionErrors.length > 0) {
throw new Error(transitionErrors.join(';'));
}
const fields = {
frontend_status: status,
frontend_owner: owner ?? canonicalDocument.metadata.frontend_owner ?? '',
frontend_ref: frontendRef ?? canonicalDocument.metadata.frontend_ref ?? '',
target_release: targetRelease ?? canonicalDocument.metadata.target_release ?? '',
verified_at: verifiedAt ?? canonicalDocument.metadata.verified_at ?? '',
updated_at: shanghaiDate(now),
};
const stateErrors = validateFrontendState(fields);
if (stateErrors.length > 0) {
throw new Error(stateErrors.join(';'));
}
const group = [
canonical,
...manifest.aliases
.filter((entry) => entry.canonical === canonical)
.map((entry) => entry.alias),
];
const outputs = group.map((repoPath) => {
const document = readDocument(root, repoPath);
if (document.error) {
throw new Error(`${document.error.path}: ${document.error.message}`);
}
const content = replaceFrontmatterFields(document.content, fields);
return {
repoPath,
absolute: document.absolute,
content,
changed: content !== document.content.replaceAll('\r\n', '\n'),
};
});
if (write) {
for (const output of outputs.filter(({ changed }) => changed)) {
writeFileSync(output.absolute, output.content, 'utf8');
}
const after = validatePathAliases(root, manifest);
if (after.errors.length > 0) {
throw new Error(`写入后 alias 状态不一致: ${after.errors.map(({ message }) => message).join(';')}`);
}
}
return {
canonical,
paths: group,
changedPaths: outputs.filter(({ changed }) => changed).map(({ repoPath }) => repoPath),
metadata: fields,
write,
};
}
function parseCli(argv) {
if (argv.length === 0 || (argv.length === 1 && argv[0] === '--check')) {
return { command: 'check' };
}
if (argv[0] === '--resolve' && argv.length === 2) {
return { command: 'resolve', path: argv[1] };
}
if (argv[0] !== '--transition') {
throw new Error('用法: --check | --resolve <path> | --transition <path> <status> [options]');
}
if (argv.length < 3) {
throw new Error('--transition 需要 path 和 status');
}
const options = { command: 'transition', path: argv[1], status: argv[2], write: false };
const mapping = new Map([
['--owner', 'owner'],
['--frontend-ref', 'frontendRef'],
['--target-release', 'targetRelease'],
['--verified-at', 'verifiedAt'],
['--reason', 'reason'],
]);
for (let index = 3; index < argv.length; index += 1) {
const flag = argv[index];
if (flag === '--write') {
options.write = true;
continue;
}
const key = mapping.get(flag);
const value = argv[index + 1];
if (!key || value === undefined) {
throw new Error(`不支持或不完整的参数: ${flag}`);
}
options[key] = value;
index += 1;
}
return options;
}
export function main(argv = process.argv.slice(2), root = process.cwd()) {
try {
const options = parseCli(argv);
const manifest = loadPathAliasManifest(root);
if (options.command === 'check') {
const result = validatePathAliases(root, manifest);
if (result.errors.length > 0) {
for (const error of result.errors) {
console.error(`[${error.code}] ${error.path}: ${error.message}`);
}
console.error(`FAIL: ${result.errors.length} path alias error(s).`);
return 1;
}
console.log(`PASS: validated ${result.checkedCount} changelog path alias(es).`);
return 0;
}
if (options.command === 'resolve') {
console.log(resolveChangelogPath(options.path, manifest));
return 0;
}
const result = transitionAliasedFrontendState(root, options.path, options, {
write: options.write,
});
console.log(JSON.stringify(result, null, 2));
return 0;
} catch (error) {
console.error(`ERROR: ${error instanceof Error ? error.message : String(error)}`);
return 2;
}
}
const isCli = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
if (isCli) {
process.exitCode = main();
}
+62 -3
查看文件
@@ -9,6 +9,7 @@ const CONTROLLED_ROOTS = new Map([
['changelogs-v2-mp', '小程序端'],
]);
const CHANGE_TYPES = new Set(['新增接口', '修改接口', '删除接口']);
const PATH_ALIAS_MANIFEST = 'changelog-path-aliases.json';
function ruleError(code, path, message, expected) {
return { code, path, message, expected };
@@ -76,6 +77,40 @@ export function collectNewTargetPaths(records) {
.map(({ targetPath }) => targetPath);
}
export function pathAliasMapFromManifest(value) {
if (value?.schema !== 'hl-changelog-path-aliases/v1' || !Array.isArray(value.aliases)) {
throw new Error('changelog-path-aliases.json schema 或 aliases 非法');
}
const aliases = new Map();
for (const [index, entry] of value.aliases.entries()) {
if (
!entry
|| typeof entry.alias !== 'string'
|| typeof entry.canonical !== 'string'
|| entry.alias.length === 0
|| entry.canonical.length === 0
) {
throw new Error(`changelog-path-aliases.json aliases[${index}] 缺少 alias/canonical`);
}
if (aliases.has(entry.alias)) {
throw new Error(`changelog-path-aliases.json alias 重复: ${entry.alias}`);
}
aliases.set(entry.alias, entry.canonical);
}
return aliases;
}
export function loadPathAliasMap(root = process.cwd()) {
try {
return pathAliasMapFromManifest(JSON.parse(readFileSync(`${root}/${PATH_ALIAS_MANIFEST}`, 'utf8')));
} catch (error) {
if (error?.code === 'ENOENT') {
return new Map();
}
throw error;
}
}
export function controlledRootForPath(inputPath) {
const candidate = String(inputPath);
for (const root of CONTROLLED_ROOTS.keys()) {
@@ -178,10 +213,32 @@ export function validateChangelogPath(inputPath, now = new Date()) {
return errors;
}
export function runValidation(records, now = new Date()) {
export function runValidation(records, now = new Date(), { pathAliases = new Map() } = {}) {
const targetPaths = collectNewTargetPaths(records);
const controlledPaths = targetPaths.filter((targetPath) => controlledRootForPath(targetPath));
const errors = controlledPaths.flatMap((targetPath) => validateChangelogPath(targetPath, now));
const errors = controlledPaths.flatMap((targetPath) => (
pathAliases.has(targetPath) ? [] : validateChangelogPath(targetPath, now)
));
for (const record of records) {
const sourcePath = /^R\d{1,3}$/.test(record.status)
? record.sourcePath
: record.status === 'D' ? record.targetPath : undefined;
if (!sourcePath || !controlledRootForPath(sourcePath) || !sourcePath.endsWith('.md')) {
continue;
}
const expectedTarget = pathAliases.get(sourcePath);
const actualTarget = /^R\d{1,3}$/.test(record.status) ? record.targetPath : undefined;
if (!expectedTarget || (actualTarget && expectedTarget !== actualTarget)) {
errors.push(ruleError(
'E_PATH_STABILITY',
sourcePath,
'已发布 changelog 不得在缺少精确 alias 关系时删除或重命名',
actualTarget
? `在 ${PATH_ALIAS_MANIFEST} 登记 ${sourcePath} -> ${actualTarget} 并保留兼容入口`
: `在 ${PATH_ALIAS_MANIFEST} 登记迁移关系并保留兼容入口`,
));
}
}
return {
targetCount: targetPaths.length,
checkedCount: controlledPaths.length,
@@ -294,7 +351,9 @@ export function main(argv = process.argv.slice(2)) {
try {
const options = parseArguments(argv);
const records = parseNameStatusZ(diffFromOptions(options));
const result = runValidation(records, new Date());
const result = runValidation(records, new Date(), {
pathAliases: loadPathAliasMap(),
});
if (result.errors.length > 0) {
for (const error of result.errors) {
console.error(`[${error.code}] ${error.path}: ${error.message}; expected ${error.expected}`);
+132
查看文件
@@ -0,0 +1,132 @@
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
loadPathAliasManifest,
parsePathAliasManifest,
resolveChangelogPath,
transitionAliasedFrontendState,
validatePathAliases,
} from '../scripts/changelog-path-aliases.mjs';
import { parseFrontmatter } from '../scripts/validate-changelog-frontmatter.mjs';
const OLD_5216 = 'changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-前端待处理-管理后台.md';
const CURRENT_5216 = 'changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md';
function document({ canonicalPath = '', status = 'claimed', owner = 'frontend-team', frontendRef = '' } = {}) {
const canonicalLine = canonicalPath ? `canonical_path: "${canonicalPath}"\n` : '';
return `---
schema: "hl-changelog/v2"
ticket: "5216"
title: "路径兼容测试"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "${status}"
frontend_owner: "${owner}"
frontend_ref: "${frontendRef}"
target_release: ""
verified_at: ""
${canonicalLine}updated_at: "2026-07-26"
base: "dev-v3"
---
# 路径兼容测试
## 变更接口
- 无业务接口变化。
## 验证证据
- 自动化测试。
`;
}
function createFixture() {
const root = mkdtempSync(path.join(tmpdir(), 'hl-changelog-alias-'));
const manifest = {
schema: 'hl-changelog-path-aliases/v1',
aliases: [{
ticket: '5216',
alias: OLD_5216,
canonical: CURRENT_5216,
reason: '测试旧路径兼容',
}],
};
writeFileSync(path.join(root, 'changelog-path-aliases.json'), JSON.stringify(manifest, null, 2));
for (const repoPath of [OLD_5216, CURRENT_5216]) {
const absolute = path.join(root, ...repoPath.split('/'));
mkdirSync(path.dirname(absolute), { recursive: true });
writeFileSync(absolute, document({
canonicalPath: repoPath === OLD_5216 ? CURRENT_5216 : '',
}));
}
return root;
}
test('repository #5216 compatibility entry resolves to its canonical document', () => {
const manifest = loadPathAliasManifest();
assert.equal(resolveChangelogPath(OLD_5216, manifest), CURRENT_5216);
assert.equal(resolveChangelogPath(CURRENT_5216, manifest), CURRENT_5216);
assert.deepEqual(validatePathAliases().errors, []);
});
test('rejects unsafe paths, duplicate aliases and alias chains', () => {
assert.throws(() => parsePathAliasManifest(JSON.stringify({
schema: 'hl-changelog-path-aliases/v1',
aliases: [{
ticket: '5216',
alias: '../outside.md',
canonical: CURRENT_5216,
reason: 'unsafe',
}],
})), /POSIX|规范路径/);
assert.throws(() => parsePathAliasManifest(JSON.stringify({
schema: 'hl-changelog-path-aliases/v1',
aliases: [
{ ticket: '5216', alias: OLD_5216, canonical: CURRENT_5216, reason: 'one' },
{ ticket: '5216', alias: OLD_5216, canonical: CURRENT_5216, reason: 'two' },
],
})), /重复/);
});
test('frontend status transition updates alias group once and is idempotent', () => {
const root = createFixture();
try {
const first = transitionAliasedFrontendState(root, OLD_5216, {
status: 'implemented',
owner: 'frontend-team',
frontendRef: 'mmg/hl-ui@abc1234',
}, {
now: new Date('2026-07-26T04:00:00Z'),
write: true,
});
assert.deepEqual(new Set(first.changedPaths), new Set([OLD_5216, CURRENT_5216]));
assert.deepEqual(validatePathAliases(root).errors, []);
for (const repoPath of [OLD_5216, CURRENT_5216]) {
const text = readFileSync(path.join(root, ...repoPath.split('/')), 'utf8');
const { metadata } = parseFrontmatter(text);
assert.equal(metadata.frontend_status, 'implemented');
assert.equal(metadata.frontend_owner, 'frontend-team');
assert.equal(metadata.frontend_ref, 'mmg/hl-ui@abc1234');
}
const second = transitionAliasedFrontendState(root, OLD_5216, {
status: 'implemented',
owner: 'frontend-team',
frontendRef: 'mmg/hl-ui@abc1234',
}, {
now: new Date('2026-07-26T04:00:00Z'),
write: true,
});
assert.deepEqual(second.changedPaths, []);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
+29 -8
查看文件
@@ -10,6 +10,7 @@ import {
collectNewTargetPaths,
getShanghaiDate,
parseNameStatusZ,
pathAliasMapFromManifest,
runValidation,
validateChangelogPath,
} from '../scripts/validate-changelog-filenames.mjs';
@@ -114,13 +115,19 @@ test('parses NUL-delimited Chinese and space-containing paths', () => {
]);
});
test('exempts modified and deleted historical bad paths', () => {
const records = parseNameStatusZ(Buffer.from(
'M\0changelogs-v2/2026-07/63_历史坏文件.md\0D\0changelogs-v2/2026-07/99_无issue.md\0',
test('exempts a modified historical bad path but protects a deleted published path', () => {
const modified = parseNameStatusZ(Buffer.from(
'M\0changelogs-v2/2026-07/63_历史坏文件.md\0',
'utf8',
));
assert.deepEqual(collectNewTargetPaths(records), []);
assert.equal(runValidation(records, SHANGHAI_NOW).errors.length, 0);
assert.deepEqual(collectNewTargetPaths(modified), []);
assert.equal(runValidation(modified, SHANGHAI_NOW).errors.length, 0);
const deleted = parseNameStatusZ(Buffer.from(
'D\0changelogs-v2/2026-07/99_无issue.md\0',
'utf8',
));
assert.ok(runValidation(deleted, SHANGHAI_NOW).errors.some(({ code }) => code === 'E_PATH_STABILITY'));
});
test('validates only a copy target path', () => {
@@ -128,9 +135,23 @@ test('validates only a copy target path', () => {
assert.deepEqual(collectNewTargetPaths(records), [validAdmin()]);
});
test('accepts a rename from an old bad name to a valid target', () => {
const records = parseNameStatusZ(Buffer.from(`R100\0changelogs-v2/2026-07/63_old.md\0${validAdmin()}\0`, 'utf8'));
assert.equal(runValidation(records, SHANGHAI_NOW).errors.length, 0);
test('rejects a rename without a machine-readable compatibility alias', () => {
const source = 'changelogs-v2/2026-07/63_old.md';
const records = parseNameStatusZ(Buffer.from(`R100\0${source}\0${validAdmin()}\0`, 'utf8'));
assert.ok(runValidation(records, SHANGHAI_NOW).errors.some(({ code }) => code === 'E_PATH_STABILITY'));
});
test('accepts a rename and a legacy alias path when both are registered', () => {
const source = 'changelogs-v2/2026-07/63_old.md';
const target = validAdmin();
const pathAliases = pathAliasMapFromManifest({
schema: 'hl-changelog-path-aliases/v1',
aliases: [{ alias: source, canonical: target }],
});
const rename = parseNameStatusZ(Buffer.from(`R100\0${source}\0${target}\0`, 'utf8'));
assert.equal(runValidation(rename, SHANGHAI_NOW, { pathAliases }).errors.length, 0);
const compatibilityAddition = parseNameStatusZ(Buffer.from(`A\0${source}\0`, 'utf8'));
assert.equal(runValidation(compatibilityAddition, SHANGHAI_NOW, { pathAliases }).errors.length, 0);
});
test('rejects a rename target with an old day', () => {