比较提交

...
作者 SHA1 备注 提交日期
wx 7a3c0d70b7 docs: record deployment evidence (#5236)
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-07-24 18:45:34 +08:00
wx 4877596de1 docs: hand off API contract (#5236)
changelog-filename-gate / validate (pull_request) Failing after 1s
2026-07-24 18:24:27 +08:00
Mimingguang a78b546cce chore(changelog): 标记前端已实现 #5203
changelog-filename-gate / validate (push) Failing after 2s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/24_5203_出行人手机号订单级门禁-修改接口-管理后台.md
2026-07-24 17:57:22 +08:00
Mimingguang f4ed055581 chore(changelog): 标记前端已领取 #5203
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/24_5203_出行人手机号订单级门禁-修改接口-管理后台.md
2026-07-24 17:48:11 +08:00
yaosutu 614cad68e6 通知小程序出行人资料完整度语义变更
changelog-filename-gate / validate (push) Successful in 2s
2026-07-24 17:47:04 +08:00
yaosutu 12fa3b7489 通知管理后台出行人手机号订单级门禁变更
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 17:39:10 +08:00
API Changelog Bot e97a559737 docs(changelog): reconcile frontend status for #5187
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 17:36:08 +08:00
Mimingguang 1b0bbad5b4 chore(changelog): 标记前端已实现 #5226
changelog-filename-gate / validate (push) Failing after 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/24_5226_用车需求提交后实时刷新派单看板-修改接口-管理后台.md
2026-07-24 17:29:05 +08:00
Mimingguang 7b66c073b9 chore(changelog): 标记前端已领取 #5226
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/24_5226_用车需求提交后实时刷新派单看板-修改接口-管理后台.md
2026-07-24 17:18:01 +08:00
API Changelog Bot 00321aecdb docs(changelog): keep fleet board frontend pending (#5226)
changelog-filename-gate / validate (push) Successful in 2s
2026-07-24 17:16:16 +08:00
API Changelog Bot 00f6bfd4a5 docs(changelog): hand off fleet board SSE refresh (#5226)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 17:15:08 +08:00
Mimingguang 2e6884262d chore(changelog): 标记前端已实现 #5187
changelog-filename-gate / validate (push) Failing after 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/23_5187_多车辆槽位原子批量派车与价格日历带价_前端待处理-新增接口-管理后台.md
2026-07-24 16:55:58 +08:00
Mimingguang 1208626f69 chore(changelog): 标记前端已领取 #5187
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/23_5187_多车辆槽位原子批量派车与价格日历带价_前端待处理-新增接口-管理后台.md
2026-07-24 16:50:06 +08:00
API Changelog Bot 55fe54afa0 docs(changelog): clarify multi-vehicle frontend handoff (#5187)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 16:40:42 +08:00
wx e492376528 docs(api): migrate #5216 handoff to schema v2
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 15:48:39 +08:00
wx 67bfaa3993 Merge pull request 'docs: 精简后端 changelog 推送说明' (#26) from docs/simplify-backend-changelog-guide into main
changelog-filename-gate / validate (push) Successful in 2s
2026-07-24 15:38:16 +08:00
wx bb53e6f542 docs: simplify backend changelog guide
changelog-filename-gate / validate (pull_request) Successful in 1s
2026-07-24 15:37:42 +08:00
Mimingguang a808064c73 chore(changelog): 标记前端已实现 #5216
changelog-filename-gate / validate (push) Successful in 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-前端待处理-管理后台.md
2026-07-24 15:25:59 +08:00
wx 5e517a4503 Merge pull request 'feat: 增加 changelog 前端消费状态与交接指南 (#5218)' (#25) from feat/5218-consumption-status into main
changelog-filename-gate / validate (push) Successful in 2s
2026-07-24 15:24:14 +08:00
wx 6cbe22f40a feat: track frontend changelog consumption (#5218)
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-07-24 15:22:10 +08:00
Mimingguang 402e6cb45b chore(changelog): 标记前端已领取 #5216
changelog-filename-gate / validate (push) Successful in 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/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-前端待处理-管理后台.md
2026-07-24 15:17:30 +08:00
Mimingguang 8493c75ab4 chore(changelog): 标记前端已实现 #5215
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/24_5215_车务首页未完成状态独立汇总-修改接口-管理后台.md
2026-07-24 15:15:18 +08:00
Mimingguang 59470f7b4c chore(changelog): 标记前端已实现 #5211
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/24_5211_派单通知预览补齐接送与行程数据-修改接口-管理后台.md
2026-07-24 15:15:17 +08:00
Mimingguang 246e999244 chore(changelog): 标记前端已实现 #5209
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/24_5209_出行人省份与分批大交通关联-修改接口-管理后台.md
2026-07-24 15:15:16 +08:00
共修改 18 个文件,包含 2300 行新增和 17 行删除
@@ -41,3 +41,6 @@ jobs:
- name: Validate new changelog filenames - name: Validate new changelog filenames
run: npm run check:filenames -- --event "$GITHUB_EVENT_PATH" run: npm run check:filenames -- --event "$GITHUB_EVENT_PATH"
- name: Validate changelog frontmatter
run: npm run check:frontmatter -- --event "$GITHUB_EVENT_PATH"
+75
查看文件
@@ -0,0 +1,75 @@
# 后端 API Changelog 推送说明
接口发生新增、修改或删除时,在 `hl-api-changelog` 仓库提交一份 changelog。
## 1. 放在哪里
- 管理后台:`changelogs-v2/YYYY-MM/`
- 小程序:`changelogs-v2-mp/YYYY-MM/`
文件名:
```text
DD_issue_业务标题-{新增接口|修改接口|删除接口}-{管理后台|小程序端}.md
```
例如:
```text
changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理后台.md
```
日期使用提交时的上海日期;不要把“前端待处理”“已完成”等状态写进文件名。
## 2. 写什么
可以复制仓库根目录的 `CHANGELOG_TEMPLATE.md`,至少写清:
- 关联的 Issue 和后端 PR;
- 接口路径和 HTTP 方法;
- 新增、修改或删除的请求/响应字段;
- 字段必填性、枚举、状态、空值、金额和兼容规则;
- 前端需要做什么;
- 后端测试、部署和网关验证结果。
元数据中:
```yaml
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
```
- 需要前端修改:`frontend_status: "pending"`
- 不需要前端修改:`frontend_status: "not_required"`
- 后端不要代替前端填写 `implemented`、`released` 或 `verified`
## 3. 校验
在 `hl-api-changelog` 仓库执行:
```powershell
npm test
npm run check:filenames -- --base origin/main --head HEAD
npm run check:frontmatter -- --base origin/main --head HEAD
```
确保正文没有 `TODO`、`待补充` 或模板占位符。
## 4. 提交和推送
只暂存本次 changelog 文件:
```powershell
git status --short
git add -- changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理后台.md
git diff --cached --check
git commit -m "docs: hand off API contract (#5205)"
git push -u origin <任务分支>
```
然后向 `main` 创建 PR。不要提交其他任务的 changelog、`.tmp-*` 文件或任何凭据。
+18
查看文件
@@ -1,3 +1,21 @@
---
schema: "hl-changelog/v2"
ticket: "{issue-no}"
title: "{一句话概括变化}"
consumer: "{admin|mp|internal|multiple}"
change_type: "{新增接口|修改接口|删除接口}"
backend_status: "pending"
gateway_status: "pending"
frontend_status: "{pending|not_required}"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "YYYY-MM-DD"
base: "{dev|dev-v3}"
---
# {模块名}: {一句话概括变化} # {模块名}: {一句话概括变化}
> **存放目录**: > **存放目录**:
+36
查看文件
@@ -42,6 +42,42 @@ npm run check:filenames -- --base origin/main --head HEAD
生产 CLI 故意不提供 `--date` 或日期环境变量;测试只通过导出的纯函数注入 `Date`。规则失败返回退出码 `1`,Git/事件/参数等基础设施错误返回 `2`。 生产 CLI 故意不提供 `--date` 或日期环境变量;测试只通过导出的纯函数注入 `Date`。规则失败返回退出码 `1`,Git/事件/参数等基础设施错误返回 `2`。
## 前端消费状态
新增二期 changelog 必须使用 `hl-changelog/v2` YAML Front Matter。状态只写在元数据中,不写入文件名:
```yaml
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
updated_at: "2026-07-24"
```
前端状态正常流转为:
```text
pending → claimed → implemented → released → verified
```
不需要前端修改时使用 `not_required`。字段一致性、必填证据和新增文档 frontmatter 由 `check:frontmatter` 校验。
完整职责和命令见:
- `FRONTEND_CONSUMPTION_STATUS_GUIDE.md`
- `BACKEND_CHANGELOG_DELIVERY_GUIDE.md`
本地校验:
```bash
npm test
npm run check:filenames -- --base origin/main --head HEAD
npm run check:frontmatter -- --base origin/main --head HEAD
```
## CI 与服务端阻断边界 ## CI 与服务端阻断边界
Gitea Actions 会在指向 `main` 的 PR 和 `main` 的 push 上运行回归测试与文件名检测。该 workflow 是检测器: Gitea Actions 会在指向 `main` 的 PR 和 `main` 的 push 上运行回归测试与文件名检测。该 workflow 是检测器:
+109
查看文件
@@ -0,0 +1,109 @@
# API Changelog 前端消费状态协作说明
## 可直接转发给前端的通知
API changelog 从 `hl-changelog/v2` 开始记录前端消费进度。后端交接时会填写:
```yaml
backend_status: "deployed"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
```
请前端在领取、实现、发布和页面验证时更新对应状态,并填写可追溯的前端 PR、提交或发布版本。
这不会把前端工作纳入后端工单验收,也不要求在 `hl-ui` 创建配合工单。它只用于区分:
- 后端接口是否已经部署并验证;
- 前端是否已经领取;
- 前端代码是否已经实现;
- 页面是否已经发布并验证。
只有 `frontend_status: "verified"` 才表示用户页面形成完整闭环。
## 状态流转
```text
pending → claimed → implemented → released → verified
```
不需要前端修改时:
```text
not_required
```
| 状态 | 含义 | 必填证据 |
|---|---|---|
| `not_required` | 不需要前端修改 | 不填写前端负责人、引用和版本 |
| `pending` | 等待前端领取 | 无 |
| `claimed` | 前端已领取 | `frontend_owner` |
| `implemented` | 前端代码已实现 | `frontend_owner`、`frontend_ref` |
| `released` | 已发布 | 再填写 `target_release` |
| `verified` | 页面已验证 | 再填写 `verified_at` |
跨级迁移会被自动校验拒绝。状态回退或改为/取消 `not_required` 时必须填写原因。
## 更新命令
领取:
```powershell
hl changelog transition 5205 D:/path/changelog.md claimed `
--owner frontend-team --write
```
实现:
```powershell
hl changelog transition 5205 D:/path/changelog.md implemented `
--owner frontend-team `
--frontend-ref "mmg/hl-ui@abc1234" `
--write
```
发布:
```powershell
hl changelog transition 5205 D:/path/changelog.md released `
--target-release "test-2026.07.24" `
--write
```
验证:
```powershell
hl changelog transition 5205 D:/path/changelog.md verified `
--verified-at "2026-07-24" `
--write
```
命令默认只预览;只有 `--write` 才修改文件。写入命令会自动获取 `changelog` 单写租约。
## 职责边界
后端负责:
- 完成后端测试、部署和网关验证;
- 初始化 v2 元数据;
- 需要前端时设置 `pending`,不需要时设置 `not_required`;
- 不替前端填写 `implemented`、`released` 或 `verified`。
前端负责:
- 领取时填写负责人;
- 实现后填写前端引用;
- 发布后填写目标版本或环境;
- 页面验证后填写验证日期。
QA 或产品可以协助更新 `verified_at`,但必须基于实际页面验证,不能只根据接口成功或代码已合并标记完成。
## 存量文档
- 新 changelog 全部使用 `hl-changelog/v2`。
- `hl-changelog/v1` 继续可读和索引,不强制一次性迁移。
- 文件名带“前端待处理”不代表真实状态;需要继续流转时补充 v2 元数据。
- 不通过重命名表达消费状态,避免破坏文件名校验和历史链接。
@@ -1,11 +1,18 @@
--- ---
schema: "hl-changelog/v1" schema: "hl-changelog/v2"
ticket: "5187" ticket: "5187"
title: "多车辆槽位原子批量派车与价格日历带价" title: "多车辆槽位原子批量派车与价格日历带价"
consumer: "admin" consumer: "admin"
backend: "verified" change_type: "新增接口"
gateway: "verified" backend_status: "deployed"
frontend: "pending" gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@6479adf1caf2a5caeea08a24a42bacecbaaabd6a"
target_release: "hl-ui/v2.1"
verified_at: ""
status_note: "前端 v2.1 已实现按 fleetItemIndex 的多槽位选择、批量提交和重复车辆/司机禁选;测试环境 9527 已提供对应源码,尚待登录态页面实操验收。"
updated_at: "2026-07-24"
base: "dev-v3" base: "dev-v3"
generated: "2026-07-23T15:38:00+08:00" generated: "2026-07-23T15:38:00+08:00"
--- ---
@@ -40,7 +47,7 @@ generated: "2026-07-23T15:38:00+08:00"
- 同一次提交及其网络重试必须复用同一个 `requestId`;用户修改选择后主动再次提交应生成新值。 - 同一次提交及其网络重试必须复用同一个 `requestId`;用户修改选择后主动再次提交应生成新值。
- `holdMode=1` 表示排车中等待司机确认,`holdMode=0` 表示直接派定;整批模式必须一致。 - `holdMode=1` 表示排车中等待司机确认,`holdMode=0` 表示直接派定;整批模式必须一致。
## 二、新增原子批量派单接口 ## 变更接口
```http ```http
POST /admin/fleet/assignments/batch POST /admin/fleet/assignments/batch
@@ -196,6 +203,30 @@ selectedSlots[fleetItemIndex] = {
- 禁止用 `for` 循环调用旧单派接口;那会在中途失败时留下半批状态。 - 禁止用 `for` 循环调用旧单派接口;那会在中途失败时留下半批状态。
- 成功后一次关闭弹窗并刷新看板;不得每成功一辆刷新一次。 - 成功后一次关闭弹窗并刷新看板;不得每成功一辆刷新一次。
#### 当前消费差距
- `useVehicleDriverPicker.js` 仍只维护一组 `selVehicle/selDriver`。
- `AssignModalFooter.vue` 仍只展示一组车辆和司机,并按这一组决定按钮是否可用。
- `useAssignFlow.js` 仍只调用 `createAssignment`,没有构造 `items[]`。
- `src/api/fleet/board.js` 尚未封装 `POST /fleet/assignments/batch`。
#### 展示矩阵
| 场景 | “已选车辆”区域 | 候选/司机联动 | 主操作 |
| --- | --- | --- | --- |
| 尚未选择 | 显示 `已选车辆 0/N` 和 N 个待选槽位 | 提示先选择车辆 | 禁用,显示未完成组数 |
| 已选一辆 | 槽位 01 显示车牌、车型、司机和移除操作,并成为当前编辑槽位 | 已选车辆标记不可重复;司机只写入当前槽位 | 未完成全部槽位时保持禁用 |
| 继续多选 | 新车辆进入下一个待选 `fleetItemIndex`;其他已选槽位保持不变 | 已被其他槽位使用的车辆和司机不可重复选择 | 全部槽位完整后启用 |
| 切换槽位 | 高亮当前编辑槽位;允许单独更换车辆、司机和价格 | 候选与司机面板切换到该槽位上下文 | 完整度实时更新 |
| 搜索/筛选/翻页 | 已选区域固定可见,集合不丢失 | 只改变候选列表 | 状态保持 |
| HOLD 完整 | 显示 `已选择 N/N 辆,司机 N/N` | 每槽位独立司机 | `下一步 · 发送给 N 名司机` |
| DIRECT 完整 | 显示 `已选择 N/N 辆,司机 N/N` | 每槽位独立司机 | `直接派定 N 辆车` |
| 批量失败 | 保留全部选择;高亮 `failedFleetItemIndex` | 允许修正失败槽位 | 原批次不产生部分成功 |
| 批量成功 | 清空选择并关闭弹窗 | 看板只统一刷新一次 | 仅发送一次批量请求 |
“已选车辆”应作为车辆筛选与候选列表之间持续可见的紧凑区域,不得只在底栏显示最后一辆。
选择数量不得超过当前需求的待派车辆槽位数;移除某一槽位不得重排或清空其他槽位。
### 3.2 车型价格日历自动带价 ### 3.2 车型价格日历自动带价
候选接口 `vehicles[].protocolPrice` 已返回所选车辆车型在服务开始日的价格日历单价。当前页面 候选接口 `vehicles[].protocolPrice` 已返回所选车辆车型在服务开始日的价格日历单价。当前页面
@@ -238,7 +269,7 @@ selectedSlots[fleetItemIndex] = {
- [ ] 主动筛选“待派车”及重置行为正确。 - [ ] 主动筛选“待派车”及重置行为正确。
- [ ] 增加多槽位状态管理、批量请求映射、车型切换带价和默认空筛选的组件/组合式函数测试。 - [ ] 增加多槽位状态管理、批量请求映射、车型切换带价和默认空筛选的组件/组合式函数测试。
## 六、后端验证证据 ## 验证证据
- `mvn -pl hl-fleet-service spotless:check` 通过。 - `mvn -pl hl-fleet-service spotless:check` 通过。
- `AssignmentControllerTest + AssignmentServiceTest`:281 项通过。 - `AssignmentControllerTest + AssignmentServiceTest`:281 项通过。
@@ -5,7 +5,11 @@ title: "出行人省份与分批大交通关联"
consumer: "admin" consumer: "admin"
backend: "verified" backend: "verified"
gateway: "verified" gateway: "verified"
frontend: "pending" frontend: "implemented"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@4424375ef9180e69f22a4f5f6b0b80c9ec2062b7"
updated_at: "2026-07-24T07:15:16.554Z"
base: "dev-v3" base: "dev-v3"
generated: "2026-07-24T11:58:00+08:00" generated: "2026-07-24T11:58:00+08:00"
--- ---
@@ -1,3 +1,9 @@
---
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@06f9d4dce58ef64c3e5de96e44754c0793286d06"
updated_at: "2026-07-24T07:15:17.169Z"
---
# 车务:派单通知预览补齐接送与行程数据 # 车务:派单通知预览补齐接送与行程数据
> **服务**: hl-fleet-service(8087/8187) > **服务**: hl-fleet-service(8087/8187)
@@ -5,7 +5,11 @@ title: "车务首页未完成状态独立汇总"
consumer: "admin" consumer: "admin"
backend: "verified" backend: "verified"
gateway: "verified" gateway: "verified"
frontend: "pending" frontend: "implemented"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@a48846a3e35c06df3aef422e538d5cd198002c2c"
updated_at: "2026-07-24T07:15:17.828Z"
base: "dev-v3" base: "dev-v3"
generated: "2026-07-24T14:25:00+08:00" generated: "2026-07-24T14:25:00+08:00"
--- ---
@@ -1,11 +1,18 @@
--- ---
schema: "hl-changelog/v1" schema: "hl-changelog/v2"
ticket: "5216" ticket: "5216"
title: "派车看板补充槽位接送路线与就绪摘要" title: "派车看板补充槽位接送路线与就绪摘要"
consumer: "admin" consumer: "admin"
backend: "verified" change_type: "修改接口"
gateway: "verified" backend_status: "deployed"
frontend: "pending" gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@41f307090eccfdf3d06deabce8bc4f3d2be9a99a"
target_release: "hl-ui/v2.1"
verified_at: ""
status_note: "后端与网关已验证;前端 implemented 状态由前端消费线程维护,本次仅迁移 schema。"
updated_at: "2026-07-24"
base: "dev-v3" base: "dev-v3"
generated: "2026-07-24T14:24:00+08:00" generated: "2026-07-24T14:24:00+08:00"
--- ---
@@ -0,0 +1,97 @@
---
schema: "hl-changelog/v2"
ticket: "5226"
title: "用车需求提交后实时刷新派单看板"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@716d5e81311628f42d2ac1945089755b4264d47e"
target_release: "hl-ui/v2.1"
verified_at: ""
status_note: "2026-07-24T17:12:49+08:00 后端已部署且网关 SSE 契约已验证;管理台仍待消费 fleet-board-changed,前端状态保持 pending。"
updated_at: "2026-07-24T09:29:05.220Z"
base: "origin/dev-v3"
generated: "2026-07-24T16:41:16+08:00"
---
# 用车需求提交后实时刷新派单看板
> 自动草稿不会代表已验证;完成实际测试后再更新 frontmatter。
## 关联
- Issue: [wx/HL#5226](https://git.1814.love:8443/wx/HL/issues/5226)
- PR: [wx/HL#5232](https://git.1814.love:8443/wx/HL/pulls/5232)
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| `POST` | `/internal/notification/fleet-board/broadcast` | Fleet 事务提交后调用 user-service 的内部广播端点 |
| `GET` | `/ws/admin-msg/stream` | 既有 SSE 流新增命名事件 `fleet-board-changed` |
## 契约影响文件
- `hl-fleet-service/src/main/java/com/hulalv/fleet/board/port/vo/FleetBoardChangedFeignReqVO.java`
- `hl-user-service/src/main/java/com/hulalv/user/notification/sse/vo/FleetBoardChangedReqVO.java`
## 前端/调用方动作
管理台继续复用现有 `/ws/admin-msg/stream` 连接,不新建第二条 EventSource。全局 SSE
组合式函数新增命名事件监听:
```js
eventSource.addEventListener('fleet-board-changed', onFleetBoardChanged)
```
事件数据示例:
```json
{
"type": "FLEET_BOARD",
"targetRoleKey": "VEHICLE_MANAGER",
"orderId": "2079000000000000001",
"requirementId": "2079000000000000101"
}
```
- 该事件是“看板数据已失效”信令,不承载订单行数据;收到后重新查询当前看板。
- 只刷新 `getBoardSummary` 和当前页 `getBoardOrders`,保留状态、日期、车型、关键词、页码和展开状态。
- 事件可能短时间连续到达,必须合并刷新并避免并发请求覆盖;不得每个事件各发一组请求。
- `orderId/requirementId` 只用于定位和诊断,按 String 保存,不能转为 Number。
- 页面不可见时先标记 dirty,恢复可见或 SSE 重连成功后刷新一次。
- 刷新失败保留现有列表,不清空页面;沿用现有错误提示与下一次事件重试。
### 展示矩阵
| 场景 | 汇总卡 | 看板列表 | 筛选/页码 | 请求策略 |
| --- | --- | --- | --- | --- |
| 页面可见,收到一次事件 | 重新查询 | 重新查询当前页 | 完整保留 | 合并为一轮刷新 |
| 短时间收到多次事件 | 最终值更新一次 | 最终值更新一次 | 完整保留 | debounce/coalesce,禁止并发覆盖 |
| 刷新进行中又收到事件 | 当前请求完成后再补一次 | 同左 | 完整保留 | 最多保留一个 pending refresh |
| 页面隐藏时收到事件 | 暂不请求 | 暂不请求 | 完整保留 | 标记 dirty,恢复可见后刷新一次 |
| SSE 重连成功 | 重新查询 | 重新查询当前页 | 完整保留 | 主动补偿一次,覆盖断线窗口 |
| 查询失败 | 保留旧值 | 保留旧列表 | 完整保留 | 展示既有错误提示,等待重试 |
| 非车务当前角色 | 不收到事件 | 不刷新 | 不变 | 后端仅投递 `VEHICLE_MANAGER` |
## 验证证据
- Fleet 定向测试:254 项通过,0 failure / 0 error。
- User 定向测试:40 项通过,0 failure / 0 error。
- `mvn -f hl-fleet-service/pom.xml spotless:check` 通过。
- `mvn -pl hl-fleet-service -am verify` 通过。
- `mvn -pl hl-user-service -am verify` 通过。
- 事务语义:只有用车需求展开事务 `AFTER_COMMIT` 才广播;回滚不发事件,广播失败不阻断主流程。
- 路由语义:事件名固定为 `fleet-board-changed`,仅投递当前角色为 `VEHICLE_MANAGER` 的连接。
- 测试环境部署:`hl-user-service` 任务 `6d567b7f`、`hl-fleet-service` 任务 `561b1051`
均成功,两个滚动实例分别恢复健康。
- 网关/SSE:`GET /ws/admin-msg/stream` 返回 HTTP 200 和 `text/event-stream`;
`8081/8181` 两实例内部广播均返回业务码 200,未带内部令牌返回 403。
- 实际事件:当前角色为 `VEHICLE_MANAGER` 的连接收到 `fleet-board-changed`,
`type=FLEET_BOARD`,`targetRoleKey=VEHICLE_MANAGER`,订单与需求 ID 按 String 到达。
- 脱敏证据:`5226-gateway-sse.json`,SHA-256
`00f3a2d441e9993ea7df706924fa792497170f1d4efee7ca190f741ca5844936`。
- 兼容性结论:既有 SSE 事件和看板查询接口不变;未消费新命名事件的前端保持原行为。
@@ -0,0 +1,122 @@
---
schema: "hl-changelog/v2"
ticket: "5236"
title: "用车接送改由大交通默认驱动"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端 PR #5241 已合并至 dev-v3(9578f78d5),order/fleet 已部署测试环境(b57ce915/0da3b9e6),双实例 internal 契约与网关汇总/列表/详情已验证;前端仍为 pending,待删除车辆接送开关并改用大交通摘要。"
updated_at: "2026-07-24"
base: "dev-v3"
---
# 用车接送改由大交通默认驱动
## 关联
- Issue: [wx/HL#5236](https://git.1814.love:8443/wx/HL/issues/5236)
- Backend PR: [wx/HL#5241](https://git.1814.love:8443/wx/HL/pulls/5241)
- Supersedes: [wx/HL#5193](https://git.1814.love:8443/wx/HL/issues/5193) 中“用车需求独立决定接送”的业务口径
- 服务: `hl-order-service-v3`、`hl-fleet-service`
- 前端仓库/分支: `mmg/hl-ui` / `v2.1`
## 关键变化
车辆安排不再让定制师重复选择“是否需要接机/接站”和“是否需要送机/送站”。
接送结论由订单当前大交通批次直接决定:
- `ARRIVAL` 批次聚合接机/接站。
- `DEPARTURE` 批次聚合送机/送站。
- 同方向任一批 `pickupRequired=true`,该方向为需要接送。
- 同方向全部批次均为 `false`,该方向为客人自理。
- 没有该方向批次时返回 `null`,表示未知。
- 新建大交通未传 `pickupRequired` 时默认保存为 `true`;显式 `false` 保持客人自理。
用车需求和订单调整中的 `pickupRequired`、`dropoffRequired` 字段暂不删除,继续兼容旧请求和回显,
但不再覆盖实时大交通结论。
## 变更接口
| 方法 | 路径 | 变化 |
|---|---|---|
| `POST` | `/v3/admin/order/:id/transport-plan/add` | 新增大交通未传 `pickupRequired` 时默认 `true` |
| `POST` | `/v3/admin/order/:id/transport-plan/batch` | 批量替换中每个未传值的批次默认 `true` |
| `POST` | `/v3/admin/order/:id/transport-plan/:planId/edit` | 未传该字段时保留原值;显式值正常覆盖 |
| `PUT` | `/v3/admin/order/:id/vehicle-requirement` | 两个接送字段改为兼容字段,不再是权威来源 |
| `GET` | `/v3/admin/order/:id/adjustment/snapshot?scope=VEHICLE_REQ` | 继续通过 `vehicleTransportSummary` 返回大交通批次摘要 |
| `POST` | `/v3/admin/order/:id/adjustment/submit` | `updates.vehicleRequirement` 中两个接送字段仅兼容接收 |
| `GET` | `/admin/fleet/board/orders` | 卡片接送就绪状态改为按实时大交通方向聚合 |
| `GET` | `/admin/fleet/board/orders/:orderId` | `transport.pickupRequired/dropoffRequired` 只取实时大交通聚合 |
小程序内部大交通新增与批量接口使用相同默认规则,但本 changelog 的前端处理范围仅为管理后台。
## 字段语义
### 大交通请求 `pickupRequired`
| 场景 | 入参 | 保存结果 |
|---|---|---|
| 新增单批/批量批次未传 | 字段省略或 `null` | `true` |
| 新增单批/批量批次显式自理 | `false` | `false` |
| 编辑既有批次未传 | 字段省略或 `null` | 保留原值 |
| 编辑既有批次显式修改 | `true` / `false` | 按提交值覆盖 |
数据库列仍为 `TINYINT(1) NOT NULL`,仅把新记录的数据库默认值从 `0` 改为 `1`,不回填或改写历史行。
### 派单详情响应
| 字段 | 类型 | 空值 | 说明 |
|---|---|---|---|
| `transport.pickupRequired` | `Boolean` | 无 ARRIVAL 批次时为 `null` | ARRIVAL 批次聚合 |
| `transport.dropoffRequired` | `Boolean` | 无 DEPARTURE 批次时为 `null` | DEPARTURE 批次聚合 |
| `transport.arrive/depart` | `Object/null` | 对应整团批次不存在时为 `null` | 到达/返程整团大交通 |
| `transport.batches[]` | `Object[]` | 无分批时为空数组 | 分批大交通,保留方向、时间、站点和关联出行人 |
## 前端处理
1. 删除“调整订单 → 车辆安排”中的“是否需要接机/接站”和“是否需要送机/送站”两个开关。
2. 提交用车需求或订单调整时,不再主动提交 `pickupRequired`、`dropoffRequired`。
3. 车辆安排页直接展示 `vehicleTransportSummary.arrivals[]` 与 `departures[]`;继续使用其中的
`direction`、`time`、`station`、`transportNo`、`pickupRequired`、`pickupRemark` 和
`travelerNames[]`。
4. 派单看板和详情不得回退到 `vehicleRequirement.pickupRequired/dropoffRequired`;
使用看板接送摘要与详情 `transport.pickupRequired/dropoffRequired`。
5. 雪花 ID 仍按字符串处理,本次没有字段删除、类型变化或新增错误码。
## 展示矩阵
| 大交通场景 | 接机/接站 | 送机/送站 | 页面展示 |
|---|---:|---:|---|
| ARRIVAL 任一批需要,DEPARTURE 全部自理 | `true` | `false` | 分方向显示“平台接 / 客人自理” |
| ARRIVAL 全部自理,DEPARTURE 任一批需要 | `false` | `true` | 分方向显示“客人自理 / 平台送” |
| 同方向多批混合 | `true` | 按返程批次聚合 | 明细保留每个批次及关联出行人 |
| 只有 ARRIVAL | 按到达批次聚合 | `null` | 返程显示未提供,不回退旧用车需求 |
| 只有 DEPARTURE | `null` | 按返程批次聚合 | 到达显示未提供,不回退旧用车需求 |
| 完全无大交通 | `null` | `null` | 显示“暂无接送机时间” |
## 验证证据
- Order 定向测试 57 项通过。
- Fleet `BoardOrderServiceTest` 50 项通过。
- 调整/需求/出行人兼容链路 357 项通过。
- 调整快照完整字段断言 `AdjustmentServiceTest` 10 项通过。
- `mvn -pl hl-order-service-v3 -am verify` 通过。
- Order 模块 Surefire 汇总 6646 项,0 失败、0 错误、28 跳过。
- `mvn -pl hl-fleet-service -am verify` 通过:Fleet 模块 2361 项,0 失败、0 错误、1 跳过。
- Fleet `spotless:check` 与 `git diff --check` 通过。
- OpenAPI/oasdiff: `not_configured`,使用源码字段/语义比对和测试作为 fallback。
- Spring Cloud Contract: `not_configured`,使用 order-v3 生产者与 Fleet 消费者测试作为 fallback。
- 后端 PR #5241 已合并,merge commit 为 `9578f78d5f0241db502d94b22283cbff0a351c53`。
- 测试环境部署任务:order `b57ce915`、fleet `0da3b9e6`。
- `order_transport_plan.pickup_required` 已验证为 `TINYINT(1) NOT NULL DEFAULT 1`,Flyway
`20260724.001` 执行成功。
- order `8086/8186` 均通过 `/v3/internal/order/orders/:orderId/transport` 与批量看板上下文实测;
`true/false/null` 三态及 ARRIVAL/DEPARTURE 分方向聚合符合字段语义。
- 测试网关 `/admin/fleet/board/summary`、`/orders`、`/orders/:orderId` 均返回成功;
详情连续 4 次通过,运行时证据为 `D:/work2/hl-workflow/.tmp/5236-gateway-evidence.json`。
@@ -0,0 +1,343 @@
# 🔧 出行人批量编辑:资料完整度与完成统计统一为订单级手机号语义(#5203)
> **PR**: [#5210](https://git.1814.love:8443/wx/HL/pulls/5210)
> **Issue**: [#5203](https://git.1814.love:8443/wx/HL/issues/5203)
> **日期**: 2026-07-24
> **消费端**: 一期小程序 MP BFF
> **接口**: `POST /mp/v3/order/{id}/traveler/batch-edit`
## 1. 变更背景
出行人资料完整度与订单签约、确认门禁此前存在两套手机号口径:逐人完成状态可能要求每名成人都有手机号,但订单门禁只要求整单至少一名出行人有手机号。
本次统一为:
- 单名出行人的资料完整度只检查 `name`、`gender`、`birthday`、`idType`、`idNo` 五项;
- `phone` 不再影响该出行人的完成状态;
- 订单整体仍必须至少有一名出行人填写手机号;
- 接口字段名、类型和层级不变,但 `completedCount`、`pendingCount`、`allCompleted` 的统计结果可能变化。
## 2. 变更接口
| 接口 | 方法 | 路径 | 变更类型 |
|---|---|---|---|
| 客户批量补全出行人 | POST | `/mp/v3/order/{id}/traveler/batch-edit` | 响应字段语义修改 |
## 3. 完整接口契约
### 3.1 调用约束
| 项目 | 契约 |
|---|---|
| 认证 | 需要小程序登录态 |
| 可编辑订单状态 | `PENDING_PAY`(待支付)、`CUSTOMIZING`(定制中) |
| 订单归属 | 只能编辑当前登录用户自己的订单 |
| 幂等 | 同一订单 3 秒内重复提交返回 `100502` |
| 批量上限 | 每次 1~30 名出行人 |
| 写入语义 | `id=null` 为新增,`id` 非空为更新;未出现在数组中的已有出行人不会被删除 |
### 3.2 路径参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | Long | 是 | 订单 ID;建议以字符串形式传递,避免大整数精度丢失 |
### 3.3 请求体
请求类型:`TravelerBatchEditReqVO`
| 字段 | 类型 | 必填 | 约束与语义 |
|---|---|---|---|
| `travelers` | `TravelerEditItem[]` | 是 | 1~30 项 |
| `travelers[].id` | Long / null | 否 | `null` 表示新增;非空表示更新,且必须属于路径中的订单 |
| `travelers[].name` | String / null | 条件必填 | 新增项的 `name`、`idType`、`idNo` 至少一项非空;有值时长度 2~30,只允许中文、英文、中点 `·`、连字符 `-`、空格 |
| `travelers[].gender` | String / null | 否 | `0`、`1`、`2`;该字段为空时资料状态为 `PENDING` |
| `travelers[].birthday` | String | 是 | `yyyy-MM-dd`,不得晚于当天;用于派生出行人类型 |
| `travelers[].idType` | String / null | 条件必填 | 取值见第 4 节;与 `idNo` 配套 |
| `travelers[].idNo` | String / null | 条件必填 | `ID_CARD` 为 18 位数字或末位 `X/x`;其他证件为 5~30 位字母、数字或连字符 |
| `travelers[].nationality` | String / null | 否 | 允许不传或传 `null`,不允许显式传空字符串 |
| `travelers[].race` | String / null | 否 | 允许不传或传 `null`,不允许显式传空字符串 |
| `travelers[].phone` | String / null | 否 | 有值时必须为 11 位数字;更新时 `null` 表示保留原值,空字符串表示清空 |
| `travelers[].emergencyContact` | String / null | 否 | 出行人级紧急联系人姓名 |
| `travelers[].emergencyPhone` | String / null | 否 | 有值时必须为 11 位数字 |
| `travelers[].roomGroupNo` | Integer / null | 否 | 最小为 1,最大不超过订单声明总人数 |
更新已有出行人时,除必填的 `birthday` 外,其他可选字段传 `null` 表示保留原值。
### 3.4 响应
响应类型:`Result<TravelerBatchEditRespVO>`
统一响应字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | Integer | `200` 表示成功,其他值见第 5 节 |
| `message` | String | 响应文案 |
| `data` | Object / null | 成功时为批量编辑统计,失败时通常为 `null` |
| `traceId` | String / null | 链路追踪 ID,可能为空 |
| `success` | Boolean | `code == 200` 时为 `true` |
`data` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `createdCount` | Integer | 本次请求中 `id=null` 的新增数量 |
| `updatedCount` | Integer | 本次请求中 `id` 非空的更新数量 |
| `completedCount` | Integer | 操作完成后,订单内五项资料均完整的出行人总数 |
| `pendingCount` | Integer | 操作完成后,订单内五项资料仍有缺失的出行人总数 |
| `allCompleted` | Boolean | 同时满足“订单声明人数大于 0、实际人数等于声明人数、`pendingCount=0`、整单至少一名出行人有手机号”时为 `true` |
五项资料指:`name`、`gender`、`birthday`、`idType`、`idNo`。手机号不计入单名出行人的完成状态,但仍计入 `allCompleted` 的订单级门禁。
## 4. 枚举与数据字典
### 4.1 `gender`
| 值 | 中文 | 完整度语义 |
|---|---|---|
| `0` | 未知 | 有值,满足 `gender` 完整度 |
| `1` | 男 | 有值,满足 `gender` 完整度 |
| `2` | 女 | 有值,满足 `gender` 完整度 |
### 4.2 资料完成状态
该状态不单独出现在本接口响应中,但直接决定 `completedCount` 和 `pendingCount`。
| 值 | 中文 | 判定 |
|---|---|---|
| `COMPLETED` | 已完善 | `name/gender/birthday/idType/idNo` 五项全部非空 |
| `PENDING` | 待完善 | 上述五项任一为空 |
### 4.3 `idType`
| 值 | 中文 |
|---|---|
| `ID_CARD` | 身份证 |
| `PASSPORT` | 护照 |
| `HK_MACAU_PASS` | 港澳通行证 |
| `HONGKONG_RESIDENT_PASS` | 回乡证 |
| `TAIWAN_PASS` | 台湾通行证 |
| `MILITARY_ID` | 军官证 |
| `OTHER` | 其他 |
### 4.4 出行人类型
出行人类型不由请求体传入,而是根据 `birthday` 自动派生,并用于校验订单各类型人数配额。
| 年龄 | 值 | 中文 |
|---|---|---|
| 未满 2 周岁 | `BABY` | 幼童 |
| 2~6 周岁 | `YOUNG_CHILD` | 小童 |
| 7~17 周岁 | `CHILD` | 儿童 |
| 18 周岁及以上 | `ADULT` | 成人 |
## 5. 错误码
| code | message / 含义 | 触发场景 |
|---|---|---|
| `401` | 未认证 | 未携带有效小程序登录态 |
| `500` | 出行人服务不可用,请稍后重试 | BFF 无法调用出行人服务 |
| `100001` | 参数非法 | `travelers` 为空、超过 30 项、缺少 `birthday`、日期格式错误等请求校验失败 |
| `100502` | 出行人补全处理中,请勿重复提交 | 同一订单 3 秒内重复提交 |
| `100503` | 资源被占用,请稍后重试 | 同一订单存在并发写入且未能取得操作权 |
| `100701` | 姓名长度异常(2-30字符) | 非空姓名长度不在 2~30 字符 |
| `100702` | 姓名含非法字符 | 姓名包含允许字符集之外的内容 |
| `100703` | 姓名含敏感词 | 姓名命中敏感词 |
| `100704` | 姓名格式不正确 | 同一字符连续重复 5 次及以上 |
| `581101` | 12301 必报字段缺失(国籍 / 民族不能为空字符串) | `nationality` 或 `race` 显式传空字符串 |
| `581102` | 订单不存在,无法编辑出行人 | 处理过程中订单不存在 |
| `581103` | 性别编码不合法(应为 1=男/2=女/0=未知) | 非空 `gender` 不在 `0/1/2` |
| `581104` | 同住分组号超出订单家庭数上限 | `roomGroupNo < 1` 或超过订单声明总人数 |
| `581110` | 出行人 ID 不属于该订单 | 更新项的 `id` 不属于路径订单 |
| `581111` | 已签电子合同后禁止修改证件号 | 已签约记录尝试修改 `idNo` |
| `581112` | 证件号格式不合法,请检查证件类型与号码是否匹配 | `idType` 非法或 `idNo` 格式不匹配 |
| `581113` | 手机号格式非法(应为 11 位数字) | 非空 `phone` 或 `emergencyPhone` 不是 11 位数字 |
| `581114` | 出生日期不能晚于今天 | `birthday` 为未来日期 |
| `581118` | 新增出行人缺少必填字段 | 新增项的 `name/idType/idNo` 全部为空 |
| `581119` | 出行人证件号重复 | 同一请求或订单内出现重复证件号 |
| `581122` | 订单不属于当前用户 | 订单不存在或不属于当前登录用户 |
| `581145` | 订单已确认,出行人信息不可再经小程序修改,如需变更请联系定制师 | 订单状态不在 `PENDING_PAY/CUSTOMIZING` 白名单 |
| `581149` | 出行人类型人数超出订单人数配置 | 根据生日派生后的某类出行人数超过订单声明配额 |
## 6. 示例
### 6.1 典型成功:两人五项完整,仅一人有手机号
**请求**
```http
POST /mp/v3/order/2079576729147338754/traveler/batch-edit
Authorization: Bearer <mp-token>
Content-Type: application/json
```
```json
{
"travelers": [
{
"id": "2079576729147338801",
"name": "张三",
"gender": "1",
"birthday": "1990-01-01",
"idType": "PASSPORT",
"idNo": "P1234567",
"nationality": "中国",
"race": "汉族",
"phone": "13800138000",
"emergencyContact": null,
"emergencyPhone": null,
"roomGroupNo": 1
},
{
"id": "2079576729147338802",
"name": "李四",
"gender": "2",
"birthday": "1992-02-02",
"idType": "PASSPORT",
"idNo": "P7654321",
"nationality": "中国",
"race": "汉族",
"phone": "",
"emergencyContact": null,
"emergencyPhone": null,
"roomGroupNo": 1
}
]
}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"createdCount": 0,
"updatedCount": 2,
"completedCount": 2,
"pendingCount": 0,
"allCompleted": true
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
### 6.2 边界成功:五项全部完整,但整单没有手机号
假设订单声明人数和实际人数均为 2,且请求将最后一部手机号清空。
**请求**
```http
POST /mp/v3/order/2079576729147338754/traveler/batch-edit
Authorization: Bearer <mp-token>
Content-Type: application/json
```
```json
{
"travelers": [
{
"id": "2079576729147338801",
"name": "张三",
"gender": "1",
"birthday": "1990-01-01",
"idType": "PASSPORT",
"idNo": "P1234567",
"phone": ""
},
{
"id": "2079576729147338802",
"name": "李四",
"gender": "2",
"birthday": "1992-02-02",
"idType": "PASSPORT",
"idNo": "P7654321",
"phone": ""
}
]
}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"createdCount": 0,
"updatedCount": 2,
"completedCount": 2,
"pendingCount": 0,
"allCompleted": false
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
这里 `completedCount=2` 表示两人的五项资料都完整;`allCompleted=false` 表示订单级“至少一名出行人有手机号”门禁未满足。
### 6.3 业务失败:订单已确认
**请求**
```http
POST /mp/v3/order/2079576729147338754/traveler/batch-edit
Authorization: Bearer <mp-token>
Content-Type: application/json
```
```json
{
"travelers": [
{
"id": "2079576729147338801",
"name": "张三",
"gender": "1",
"birthday": "1990-01-01",
"idType": "PASSPORT",
"idNo": "P1234567",
"phone": "13800138000"
}
]
}
```
**响应**
```json
{
"code": 581145,
"message": "订单已确认,出行人信息不可再经小程序修改,如需变更请联系定制师",
"data": null,
"traceId": "a1b2c3d4-e5f6-7890",
"success": false
}
```
## 7. 修改前后对比
| 场景 | 修改前 | 修改后 |
|---|---|---|
| 单名成人五项完整但本人无手机号 | 可能计入 `pendingCount` | 计入 `completedCount` |
| 同行人已有手机号 | 仍可能要求每名成人各自填写 | 整单手机号门禁已满足 |
| 五项完整且整单无手机号 | 逐人完成状态与手机号门禁混合 | `completedCount` 可等于实际人数,但 `allCompleted=false` |
| 请求 / 响应结构 | 现有字段 | 不变 |
## 8. 消费注意事项
- 不要按“每名成人必须有手机号”在本地重算资料完成状态。
- `completedCount` 和 `pendingCount` 是操作后订单内的总量,不是本次请求中发生状态变化的行数。
- 判断本接口是否已满足整单补全条件,以响应 `allCompleted` 为准;它已同时包含人数、五项资料和订单级手机号门禁。
- `phone=null` 在更新场景表示保留原值;需要清空手机号时传空字符串。
## 9. 关联
- **Issue**: [#5203](https://git.1814.love:8443/wx/HL/issues/5203)
- **PR**: [#5210](https://git.1814.love:8443/wx/HL/pulls/5210)
- **Merge commit**: [88d0aec8375b56b5b8141984645a6998f8a42609](https://git.1814.love:8443/wx/HL/commit/88d0aec8375b56b5b8141984645a6998f8a42609)
+3 -2
查看文件
@@ -3,8 +3,9 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "node --test tests/validate-changelog-filenames.test.mjs", "test": "node --test tests/validate-changelog-filenames.test.mjs tests/validate-changelog-frontmatter.test.mjs",
"check:filenames": "node scripts/validate-changelog-filenames.mjs" "check:filenames": "node scripts/validate-changelog-filenames.mjs",
"check:frontmatter": "node scripts/validate-changelog-frontmatter.mjs"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
+3 -3
查看文件
@@ -76,7 +76,7 @@ export function collectNewTargetPaths(records) {
.map(({ targetPath }) => targetPath); .map(({ targetPath }) => targetPath);
} }
function controlledRootForPath(inputPath) { export function controlledRootForPath(inputPath) {
const candidate = String(inputPath); const candidate = String(inputPath);
for (const root of CONTROLLED_ROOTS.keys()) { for (const root of CONTROLLED_ROOTS.keys()) {
if (candidate === root || candidate.startsWith(`${root}/`) || candidate.startsWith(`${root}\\`)) { if (candidate === root || candidate.startsWith(`${root}/`) || candidate.startsWith(`${root}\\`)) {
@@ -190,7 +190,7 @@ export function runValidation(records, now = new Date()) {
}; };
} }
function parseArguments(argv) { export function parseArguments(argv) {
const options = {}; const options = {};
for (let index = 0; index < argv.length; index += 2) { for (let index = 0; index < argv.length; index += 2) {
const flag = argv[index]; const flag = argv[index];
@@ -257,7 +257,7 @@ function gitDiff(revisions) {
}); });
} }
function diffFromOptions(options) { export function diffFromOptions(options) {
if (!options.event) { if (!options.event) {
const base = resolveCommitRef(options.base, 'base ref'); const base = resolveCommitRef(options.base, 'base ref');
const head = resolveCommitRef(options.head, 'head ref'); const head = resolveCommitRef(options.head, 'head ref');
+265
查看文件
@@ -0,0 +1,265 @@
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import {
controlledRootForPath,
diffFromOptions,
parseArguments,
parseNameStatusZ,
} from './validate-changelog-filenames.mjs';
const FRONTEND_STATUSES = new Set([
'not_required',
'pending',
'claimed',
'implemented',
'released',
'verified',
]);
const BACKEND_STATUSES = new Set(['pending', 'tested', 'deployed']);
const GATEWAY_STATUSES = new Set(['pending', 'verified', 'not_required']);
const CONSUMERS = new Set(['admin', 'mp', 'internal', 'multiple']);
const CHANGE_TYPES = new Set(['新增接口', '修改接口', '删除接口']);
const REQUIRED_KEYS = [
'schema',
'ticket',
'title',
'consumer',
'change_type',
'backend_status',
'gateway_status',
'frontend_status',
'frontend_owner',
'frontend_ref',
'target_release',
'verified_at',
'updated_at',
'base',
];
function ruleError(code, file, message) {
return { code, path: file, message };
}
function isIsoDate(value) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value ?? '');
if (!match) {
return false;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const parsed = new Date(Date.UTC(year, month - 1, day));
return parsed.getUTCFullYear() === year
&& parsed.getUTCMonth() === month - 1
&& parsed.getUTCDate() === day;
}
function isIsoDateOrTime(value) {
if (isIsoDate(value)) {
return true;
}
return /^\d{4}-\d{2}-\d{2}T/.test(value ?? '')
&& Number.isFinite(Date.parse(value));
}
export function parseFrontmatter(text) {
const value = String(text ?? '').replaceAll('\r\n', '\n');
if (!value.startsWith('---\n')) {
return { metadata: undefined, body: value };
}
const end = value.indexOf('\n---\n', 4);
if (end < 0) {
return { metadata: undefined, body: value };
}
const metadata = {};
for (const line of value.slice(4, end).split('\n')) {
const separator = line.indexOf(':');
if (separator < 0) {
continue;
}
const key = line.slice(0, separator).trim();
let fieldValue = line.slice(separator + 1).trim();
if (
(fieldValue.startsWith('"') && fieldValue.endsWith('"'))
|| (fieldValue.startsWith("'") && fieldValue.endsWith("'"))
) {
fieldValue = fieldValue.slice(1, -1);
}
metadata[key] = fieldValue;
}
return { metadata, body: value.slice(end + 5) };
}
export function validateFrontendState(metadata) {
const errors = [];
const status = metadata.frontend_status;
const owner = metadata.frontend_owner?.trim() ?? '';
const reference = metadata.frontend_ref?.trim() ?? '';
const release = metadata.target_release?.trim() ?? '';
const verifiedAt = metadata.verified_at?.trim() ?? '';
if (!FRONTEND_STATUSES.has(status)) {
return [`frontend_status 非法: ${status || '(空)'}`];
}
if (['claimed', 'implemented', 'released', 'verified'].includes(status) && !owner) {
errors.push(`${status} 必须填写 frontend_owner`);
}
if (['implemented', 'released', 'verified'].includes(status) && !reference) {
errors.push(`${status} 必须填写 frontend_ref`);
}
if (['released', 'verified'].includes(status) && !release) {
errors.push(`${status} 必须填写 target_release`);
}
if (status === 'verified' && !verifiedAt) {
errors.push('verified 必须填写 verified_at');
}
if (verifiedAt && !isIsoDateOrTime(verifiedAt)) {
errors.push('verified_at 必须是 ISO 日期或时间');
}
if (status === 'not_required' && [owner, reference, release, verifiedAt].some(Boolean)) {
errors.push('not_required 不得保留前端负责人、引用、版本或验证时间');
}
return errors;
}
export function validateFrontendTransition(current, target, reason = '') {
if (!FRONTEND_STATUSES.has(current) || !FRONTEND_STATUSES.has(target)) {
return ['frontend_status 非法'];
}
if (current === target) {
return [];
}
if (current === 'not_required' || target === 'not_required') {
return reason.trim() ? [] : ['涉及 not_required 的迁移必须填写原因'];
}
const order = ['pending', 'claimed', 'implemented', 'released', 'verified'];
const currentIndex = order.indexOf(current);
const targetIndex = order.indexOf(target);
if (targetIndex === currentIndex + 1) {
return [];
}
if (targetIndex < currentIndex) {
return reason.trim() ? [] : ['状态回退必须填写原因'];
}
return [`禁止跨级迁移: ${current} -> ${target}`];
}
export function validateV2Document(file, text, { requireV2 = false } = {}) {
const { metadata, body } = parseFrontmatter(text);
if (!metadata) {
return requireV2 ? [ruleError('E_FRONTMATTER', file, '新增 changelog 缺少 YAML Front Matter')] : [];
}
if (metadata.schema !== 'hl-changelog/v2') {
return requireV2
? [ruleError('E_SCHEMA', file, `新增 changelog 必须使用 hl-changelog/v2,当前为 ${metadata.schema || '(空)'}`)]
: [];
}
const errors = [];
for (const key of REQUIRED_KEYS) {
if (!(key in metadata)) {
errors.push(ruleError('E_REQUIRED', file, `frontmatter 缺少 ${key}`));
}
}
for (const key of ['ticket', 'title', 'consumer', 'change_type', 'backend_status', 'gateway_status', 'frontend_status', 'updated_at', 'base']) {
if (!metadata[key]?.trim()) {
errors.push(ruleError('E_REQUIRED', file, `${key} 不能为空`));
}
}
if (!CHANGE_TYPES.has(metadata.change_type)) {
errors.push(ruleError('E_CHANGE_TYPE', file, `change_type 非法: ${metadata.change_type || '(空)'}`));
}
if (!CONSUMERS.has(metadata.consumer)) {
errors.push(ruleError('E_CONSUMER', file, `consumer 非法: ${metadata.consumer || '(空)'}`));
}
if (!BACKEND_STATUSES.has(metadata.backend_status)) {
errors.push(ruleError('E_BACKEND_STATUS', file, `backend_status 非法: ${metadata.backend_status || '(空)'}`));
}
if (!GATEWAY_STATUSES.has(metadata.gateway_status)) {
errors.push(ruleError('E_GATEWAY_STATUS', file, `gateway_status 非法: ${metadata.gateway_status || '(空)'}`));
}
if (metadata.backend_status !== 'deployed') {
errors.push(ruleError('E_BACKEND_PENDING', file, '发布的 changelog 必须是 backend_status=deployed'));
}
if (metadata.gateway_status === 'pending') {
errors.push(ruleError('E_GATEWAY_PENDING', file, '发布的 changelog 不能保留 gateway_status=pending'));
}
for (const message of validateFrontendState(metadata)) {
errors.push(ruleError('E_FRONTEND_STATE', file, message));
}
if (metadata.consumer === 'internal' && metadata.frontend_status !== 'not_required') {
errors.push(ruleError('E_FRONTEND_STATE', file, 'internal consumer 必须使用 frontend_status=not_required'));
}
if (!isIsoDate(metadata.updated_at)) {
errors.push(ruleError('E_UPDATED_AT', file, 'updated_at 必须是真实的 YYYY-MM-DD 日期'));
}
const filename = path.posix.basename(file);
const issue = /^\d{2}_([1-9]\d*)_/.exec(filename)?.[1];
if (issue && metadata.ticket !== issue) {
errors.push(ruleError('E_TICKET_MISMATCH', file, `ticket=${metadata.ticket} 与文件名 Issue=${issue} 不一致`));
}
const filenameType = /-(新增接口|修改接口|删除接口)-(?:管理后台|小程序端)\.md$/.exec(filename)?.[1];
if (filenameType && metadata.change_type !== filenameType) {
errors.push(ruleError('E_TYPE_MISMATCH', file, `change_type=${metadata.change_type} 与文件名=${filenameType} 不一致`));
}
if (/\{[^{}\n]+\}|\bTODO\b|待补充/i.test(body)) {
errors.push(ruleError('E_PLACEHOLDER', file, '正文仍有 TODO、待补充或模板占位符'));
}
if (!body.includes('## 变更接口') || !body.includes('## 验证证据')) {
errors.push(ruleError('E_SECTIONS', file, '正文缺少“变更接口”或“验证证据”章节'));
}
return errors;
}
export function collectChangedDocuments(records) {
return records
.filter(({ status }) => status !== 'D')
.map((record) => ({
path: record.targetPath,
isNew: record.status === 'A' || /^C\d{1,3}$/.test(record.status) || /^R\d{1,3}$/.test(record.status),
}))
.filter(({ path: file }) => controlledRootForPath(file) && file.endsWith('.md'));
}
export function runFrontmatterValidation(records, root = process.cwd()) {
const documents = collectChangedDocuments(records);
const errors = [];
for (const document of documents) {
let text;
try {
text = readFileSync(path.join(root, ...document.path.split('/')), 'utf8');
} catch (error) {
errors.push(ruleError('E_READ', document.path, `无法读取文件: ${error.message}`));
continue;
}
errors.push(...validateV2Document(document.path, text, { requireV2: document.isNew }));
}
return { checkedCount: documents.length, errors };
}
export function main(argv = process.argv.slice(2)) {
try {
const options = parseArguments(argv);
const records = parseNameStatusZ(diffFromOptions(options));
const result = runFrontmatterValidation(records);
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} frontmatter error(s) in ${result.checkedCount} changelog file(s).`);
return 1;
}
console.log(`PASS: validated frontmatter for ${result.checkedCount} changed changelog file(s).`);
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();
}
@@ -0,0 +1,134 @@
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
parseFrontmatter,
runFrontmatterValidation,
validateFrontendState,
validateFrontendTransition,
validateV2Document,
} from '../scripts/validate-changelog-frontmatter.mjs';
const FILE = 'changelogs-v2/2026-07/24_5218_工作流治理-修改接口-管理后台.md';
function metadata(overrides = {}) {
return {
schema: 'hl-changelog/v2',
ticket: '5218',
title: '工作流治理',
consumer: 'admin',
change_type: '修改接口',
backend_status: 'deployed',
gateway_status: 'verified',
frontend_status: 'pending',
frontend_owner: '',
frontend_ref: '',
target_release: '',
verified_at: '',
status_note: '',
updated_at: '2026-07-24',
base: 'dev-v3',
...overrides,
};
}
function document(overrides = {}) {
const fields = metadata(overrides);
const frontmatter = Object.entries(fields)
.map(([key, value]) => `${key}: "${value}"`)
.join('\n');
return `---\n${frontmatter}\n---\n\n# 工作流治理\n\n## 变更接口\n\n- 无业务接口变化。\n\n## 验证证据\n\n- 自动化测试通过。\n`;
}
test('parses quoted flat YAML frontmatter', () => {
const parsed = parseFrontmatter(document());
assert.equal(parsed.metadata.schema, 'hl-changelog/v2');
assert.equal(parsed.metadata.frontend_status, 'pending');
});
test('accepts a complete v2 handoff with pending frontend consumption', () => {
assert.deepEqual(validateV2Document(FILE, document(), { requireV2: true }), []);
});
test('rejects backend and gateway pending at publication', () => {
const errors = validateV2Document(
FILE,
document({ backend_status: 'pending', gateway_status: 'pending' }),
{ requireV2: true },
);
assert.ok(errors.some(({ code }) => code === 'E_BACKEND_PENDING'));
assert.ok(errors.some(({ code }) => code === 'E_GATEWAY_PENDING'));
});
test('requires frontend evidence as status advances', () => {
assert.deepEqual(
validateFrontendState(metadata({ frontend_status: 'claimed' })),
['claimed 必须填写 frontend_owner'],
);
assert.deepEqual(
validateFrontendState(metadata({
frontend_status: 'implemented',
frontend_owner: 'frontend-team',
frontend_ref: 'mmg/hl-ui@abc1234',
})),
[],
);
assert.ok(
validateFrontendState(metadata({
frontend_status: 'verified',
frontend_owner: 'frontend-team',
frontend_ref: 'mmg/hl-ui@abc1234',
target_release: 'prod-2026.07.24',
verified_at: '2026-02-30',
})).includes('verified_at 必须是 ISO 日期或时间'),
);
});
test('rejects skipped transitions and requires a rollback reason', () => {
assert.ok(validateFrontendTransition('pending', 'implemented').length > 0);
assert.deepEqual(validateFrontendTransition('pending', 'claimed'), []);
assert.ok(validateFrontendTransition('released', 'implemented').length > 0);
assert.deepEqual(
validateFrontendTransition('released', 'implemented', '测试发布已回滚'),
[],
);
});
test('new changelog requires v2 while a modified legacy file remains compatible', () => {
const root = mkdtempSync(path.join(tmpdir(), 'hl-frontmatter-'));
try {
const file = path.join(root, ...FILE.split('/'));
mkdirSync(path.dirname(file), { recursive: true });
writeFileSync(file, '# legacy\n');
const newResult = runFrontmatterValidation([{ status: 'A', targetPath: FILE }], root);
assert.ok(newResult.errors.some(({ code }) => code === 'E_FRONTMATTER'));
const modifiedResult = runFrontmatterValidation([{ status: 'M', targetPath: FILE }], root);
assert.deepEqual(modifiedResult.errors, []);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('detects metadata and filename mismatches', () => {
const errors = validateV2Document(
FILE,
document({ ticket: '9999', change_type: '新增接口' }),
{ requireV2: true },
);
assert.ok(errors.some(({ code }) => code === 'E_TICKET_MISMATCH'));
assert.ok(errors.some(({ code }) => code === 'E_TYPE_MISMATCH'));
});
test('rejects unsupported consumers and impossible dates', () => {
const errors = validateV2Document(
FILE,
document({ consumer: 'browser', updated_at: '2026-02-30' }),
{ requireV2: true },
);
assert.ok(errors.some(({ code }) => code === 'E_CONSUMER'));
assert.ok(errors.some(({ code }) => code === 'E_UPDATED_AT'));
});