比较提交

...
作者 SHA1 备注 提交日期
wx de960761df docs(changelog): hand off configured vehicle details (#5254)
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-07-26 09:58:39 +08:00
Mimingguang ee1c4a3167 chore(changelog): 标记前端已实现 #5245
changelog-filename-gate / validate (push) Failing after 2s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/25_5245_行程短链预览与同槽位改派解析-修改接口-管理后台.md
2026-07-26 09:23:05 +08:00
wx 7c5301db9f Merge pull request 'docs: correct frontend slot acceptance for #5245' (#33) from docs/5245-add-vehicle-slot-correction into main
changelog-filename-gate / validate (push) Successful in 2s
2026-07-25 19:42:15 +08:00
wx da2707f63b docs: correct frontend slot acceptance for #5245
changelog-filename-gate / validate (pull_request) Successful in 1s
2026-07-25 19:40:47 +08:00
Mimingguang 4a3ec16a3d chore(changelog): 标记前端已实现 #5245
changelog-filename-gate / validate (push) Failing after 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/25_5245_行程短链预览与同槽位改派解析-修改接口-管理后台.md
2026-07-25 18:21:57 +08:00
wx ffd0983958 Merge pull request #32 from docs/5245-preview-shortlink-release-evidence
changelog-filename-gate / validate (push) Successful in 1s
docs(changelog): 补充 #5245 短链交付证据
2026-07-25 18:04:24 +08:00
wx f52ef24dc1 docs(changelog): 补充5245短链交付证据
changelog-filename-gate / validate (pull_request) Successful in 1s
2026-07-25 18:03:01 +08:00
Mimingguang 12a331d7c7 chore(changelog): 标记前端已实现 #5219
changelog-filename-gate / validate (push) Successful in 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/25_5219_核单原型流迁移-修改接口-管理后台.md
2026-07-25 16:37:46 +08:00
Mimingguang 86059ce5f3 chore(changelog): 标记前端已领取 #5219
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/25_5219_核单原型流迁移-修改接口-管理后台.md
2026-07-25 16:21:47 +08:00
yaosutu 272b23b251 新增核单原型流管理后台接口变更说明
changelog-filename-gate / validate (push) Failing after 1s
2026-07-25 16:12:38 +08:00
Mimingguang 54ee10aa8d chore(changelog): 标记前端已实现 #5245
changelog-filename-gate / validate (push) Failing after 2s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/25_5245_行程短链预览与同槽位改派解析-修改接口-管理后台.md
2026-07-25 12:21:27 +08:00
Mimingguang b931167499 chore(changelog): 标记前端已领取 #5245
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/25_5245_行程短链预览与同槽位改派解析-修改接口-管理后台.md
2026-07-25 12:10:00 +08:00
wx 52d9b49daa Merge pull request #31: docs(fleet) #5245 changelog handoff
changelog-filename-gate / validate (push) Failing after 1s
Correct the stable short-link preview contract and document multi-vehicle/multi-driver frontend consumption.
2026-07-25 12:02:03 +08:00
wx 3ffef86227 docs(fleet): correct shortlink preview contract (#5245)
changelog-filename-gate / validate (pull_request) Failing after 1s
2026-07-25 11:55:50 +08:00
Mimingguang 120aee2b40 chore(changelog): 标记前端已实现 #5238
changelog-filename-gate / validate (push) Successful in 2s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/25_5238_核单门票来源类型统一-修改接口-管理后台.md
2026-07-25 11:42:03 +08:00
Mimingguang e38807ccc8 chore(changelog): 标记前端已领取 #5238
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/25_5238_核单门票来源类型统一-修改接口-管理后台.md
2026-07-25 11:37:16 +08:00
Mimingguang bd7f5a5e19 chore(changelog): 标记前端已实现 #5237
changelog-filename-gate / validate (push) Has been cancelled
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/25_5237_酒店候选补齐房型结算价-修改接口-管理后台.md
2026-07-25 11:37:11 +08:00
Mimingguang 85caef620c chore(changelog): 标记前端已实现 #5216
changelog-filename-gate / validate (push) Failing after 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md
2026-07-25 11:29:38 +08:00
Mimingguang 23ef052327 chore(changelog): 标记前端已实现 #5202
changelog-filename-gate / validate (push) Successful in 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/24_5202_调整订单行程节点时间-修改接口-管理后台.md
2026-07-25 11:25:59 +08:00
Mimingguang f8a161fdf6 chore(changelog): 标记前端已领取 #5202
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_5202_调整订单行程节点时间-修改接口-管理后台.md
2026-07-25 11:21:56 +08:00
Mimingguang 0e95ebd933 chore(changelog): 标记前端已领取 #5193
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/23_5193_用车需求增加独立接机送机选择-修改接口-管理后台.md
2026-07-25 11:19:01 +08:00
Mimingguang 07990e1578 chore(changelog): 标记前端已实现 #5178
changelog-filename-gate / validate (push) Successful in 2s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/23_5178_用车手动加急与派车看板状态颜色-新增接口-管理后台.md
2026-07-25 11:18:56 +08:00
Mimingguang 6cab15bab7 chore(changelog): 标记前端已领取 #5178
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/23_5178_用车手动加急与派车看板状态颜色-新增接口-管理后台.md
2026-07-25 11:14:57 +08:00
Mimingguang cbabad9a44 chore(changelog): 标记前端已实现 #5176
changelog-filename-gate / validate (push) Successful in 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/23_5176_房务配房彻底移除成交价历史字段-修改接口-管理后台.md
2026-07-25 11:14:53 +08:00
Mimingguang 8d5c6943a5 chore(changelog): 标记前端已领取 #5176
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/23_5176_房务配房彻底移除成交价历史字段-修改接口-管理后台.md
2026-07-25 11:11:05 +08:00
Mimingguang b883ce7aa8 chore(changelog): 标记前端已实现 #5160
changelog-filename-gate / validate (push) Successful in 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/22_5160_一名司机多辆常驻车-修改接口-管理后台.md
2026-07-25 11:11:01 +08:00
Mimingguang 236cf8ff19 chore(changelog): 标记前端已领取 #5160
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/22_5160_一名司机多辆常驻车-修改接口-管理后台.md
2026-07-25 11:03:43 +08:00
Mimingguang e1f5c2b9a2 chore(changelog): 标记前端已实现 #5158
changelog-filename-gate / validate (push) Successful in 2s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/22_5158_派车按行程日标记车费日期-修改接口-管理后台.md
2026-07-25 11:03:38 +08:00
yaosutu ec0ec9ede1 补充酒店候选房型结算价前端契约
changelog-filename-gate / validate (push) Failing after 1s
2026-07-25 11:01:19 +08:00
Mimingguang a7b750cede chore(changelog): 标记前端已领取 #5158
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/22_5158_派车按行程日标记车费日期-修改接口-管理后台.md
2026-07-25 10:59:34 +08:00
yaosutu 736a17f084 修正 changelog 文件名并新增核单门票来源通知
changelog-filename-gate / validate (push) Failing after 1s
2026-07-25 10:57:14 +08:00
API Changelog Bot cdeb340e7c docs(fleet): hand off itinerary shortlink contract (#5245)
changelog-filename-gate / validate (pull_request) Failing after 1s
2026-07-25 09:51:02 +08:00
Mimingguang 86d9356d72 chore(changelog): 标记前端已实现 #5244
changelog-filename-gate / validate (push) Failing after 2s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/25_5244_派单详情分别返回接送说明与通用备注-修改接口-管理后台.md
2026-07-25 09:24:40 +08:00
Mimingguang c7c8a7299c chore(changelog): 标记前端已领取 #5244
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/25_5244_派单详情分别返回接送说明与通用备注-修改接口-管理后台.md
2026-07-25 09:20:58 +08:00
Mimingguang bae6a58183 chore(changelog): 标记前端已实现 #5131
changelog-filename-gate / validate (push) Successful in 1s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/5131-fleet-team-management.md
2026-07-25 09:18:24 +08:00
wx ed0a97326f Merge pull request 'docs(fleet): 交接派单详情接送说明与通用备注 (#5244)' (#30) from docs/5244-fleet-pickup-remark into main
changelog-filename-gate / validate (push) Successful in 1s
2026-07-25 09:14:11 +08:00
API Changelog Bot fe3354cce2 docs(fleet): hand off pickup remark fields (#5244)
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-07-25 09:13:16 +08:00
wx 8e0c18658f Merge pull request 'docs: 交接派单详情接送说明与备注契约 (#5244)' (#29) from docs/5244-fleet-pickup-remark into main
changelog-filename-gate / validate (push) Successful in 2s
2026-07-25 09:11:20 +08:00
Mimingguang e64aa1a054 chore(changelog): 标记前端已领取 #5131
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/5131-fleet-team-management.md
2026-07-25 09:10:49 +08:00
API Changelog Bot e66c76f96b docs: hand off API contract (#5244)
changelog-filename-gate / validate (pull_request) Successful in 2s
2026-07-25 09:09:32 +08:00
API Changelog Bot 3b4e095f26 docs(changelog): report driver confirmation header spacing (#5146)
changelog-filename-gate / validate (push) Successful in 2s
2026-07-24 19:19:30 +08:00
wx e8fbd19fc5 docs: 告知车队管理列宽与暗色模式问题 (#5131)
changelog-filename-gate / validate (push) Successful in 1s
补充 /fleet/teams 列宽、响应式和暗色主题展示矩阵及验收标准。
2026-07-24 19:09:06 +08:00
API Changelog Bot 1e2f39e76f docs: report fleet team layout and dark mode (#5131)
changelog-filename-gate / validate (pull_request) Successful in 1s
2026-07-24 19:06:59 +08:00
Mimingguang 4d7d0d5e5c chore(changelog): 标记前端已实现 #5236
changelog-filename-gate / validate (push) Failing after 2s
修改原因:hl-ui changelog loop 需要把前端消费进度回写到契约源,避免领取、实现状态与实际交付脱节。

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

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

Frontend-Status: tools/mcp-api-sync/.changelog-repo/changelogs-v2/2026-07/24_5236_用车接送改由大交通默认驱动-修改接口-管理后台.md
2026-07-24 18:59:50 +08:00
Mimingguang 93d013f1d4 chore(changelog): 标记前端已领取 #5236
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_5236_用车接送改由大交通默认驱动-修改接口-管理后台.md
2026-07-24 18:48:28 +08:00
wx 9891fee76e Merge pull request #27 from docs/5236-transport-driven-transfer
changelog-filename-gate / validate (push) Successful in 1s
docs: publish transport-driven transfer handoff (#5236)
2026-07-24 18:46:30 +08:00
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
API Changelog Bot 92093b7368 docs(api): hand off fleet board decision summary (#5216)
changelog-filename-gate / validate (push) Failing after 1s
2026-07-24 15:12:42 +08:00
API Changelog Bot ba9b41bd9e docs: hand off dashboard status cards (#5215)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 14:26:12 +08:00
API Changelog Bot fe4bdd1e2d docs: hand off traveler transport linkage (#5209)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 12:00:15 +08:00
API Changelog Bot 40e28d2d4e docs(fleet): 交接派单通知预览修正 (#5211)
changelog-filename-gate / validate (push) Successful in 2s
2026-07-24 11:59:02 +08:00
API Changelog Bot 92b53da54d docs(order): hand off vehicle rejection history (#5200)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 11:10:25 +08:00
API Changelog Bot 4f93f147be docs: hand off fleet dashboard display contract (#5205)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 11:01:15 +08:00
API Changelog Bot cfc9c7b8db docs: 清理节点时间契约格式 (#5202)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 10:28:59 +08:00
API Changelog Bot 45f5dc4a06 docs: 交接调整订单节点时间契约 (#5202)
changelog-filename-gate / validate (push) Failing after 1s
2026-07-24 10:28:43 +08:00
API Changelog Bot ad33e901ab docs(fleet): record merged dashboard deployment (#5199)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-24 10:14:16 +08:00
API Changelog Bot c76d5f72c7 docs(fleet): hand off dashboard order summary fix (#5199)
changelog-filename-gate / validate (push) Successful in 2s
2026-07-24 10:07:10 +08:00
API Changelog Bot e8b8eea8f7 docs(order): record transfer option verification
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 18:07:44 +08:00
API Changelog Bot 33a34f1e3d docs: 交接多车待确认与按槽位改派 (#5194)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 18:05:30 +08:00
API Changelog Bot 4f94800a05 docs(order): document transfer requirement options
changelog-filename-gate / validate (push) Failing after 2s
2026-07-23 17:41:02 +08:00
API Changelog Bot 9b32c2e2e0 docs(fleet): specify mobile itinerary link copy flow
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 17:06:15 +08:00
API Changelog Bot 8737f63a54 docs(fleet): reuse canonical itinerary print flow
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 17:01:18 +08:00
API Changelog Bot acb7c040bc docs(fleet): clarify pending dispatch UI fixes
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 16:49:38 +08:00
yaosutu 8288bfcc7f 补充Step2票种规格字典与默认值说明
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 16:43:36 +08:00
API Changelog Bot 199dad384e docs(fleet): hand off reassign step state (#5186)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 16:30:05 +08:00
API Changelog Bot 171be62be5 docs(fleet): verify reassign context handoff (#5186)
changelog-filename-gate / validate (push) Successful in 2s
2026-07-23 16:23:50 +08:00
API Changelog Bot 6b29df3f63 docs: use changelog as sole frontend handoff
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 16:12:29 +08:00
API Changelog Bot e9a625d8fb docs(fleet): hand off reassign candidate context (#5186)
changelog-filename-gate / validate (push) Successful in 2s
2026-07-23 16:11:59 +08:00
API Changelog Bot 710ceba840 docs: add atomic batch gateway evidence
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 16:05:20 +08:00
API Changelog Bot 8f6c7678dd docs: record fleet batch gateway verification
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 15:51:31 +08:00
API Changelog Bot b279ae7eeb docs(fleet): hand off atomic batch assignment (#5187)
changelog-filename-gate / validate (push) Successful in 2s
2026-07-23 15:41:16 +08:00
API Changelog Bot 3740020603 docs(fleet): hand off holding reassignment contract (#5186)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 15:27:19 +08:00
yaosutu 1e7c98f270 新增酒店房型轻量下拉接口说明
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 14:07:27 +08:00
API Changelog Bot 2e94a6387c docs: 补充车务看板聊天首次打开与实时角标回归 #5180
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 11:40:58 +08:00
API Changelog Bot f7ebc9c6f9 docs(fleet): reopen matrix team number display (#5156)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 11:36:53 +08:00
API Changelog Bot 4eb1e9db52 docs(workflow): 记录 Actions 外部依赖约束 (#24)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 11:08:57 +08:00
API Changelog Bot 8d6c2f3375 fix(workflow): 移除外部 Actions 仓库依赖 (#24)
changelog-filename-gate / validate (push) Successful in 1s
2026-07-23 11:07:41 +08:00
API Changelog Bot e5b0d7d7e7 fix(workflow): 恢复 Actions 执行器并支持手动触发 (#24)
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 11:04:57 +08:00
API Changelog Bot c8557f50b5 docs(frontend): add verified rollout evidence for #5178
changelog-filename-gate / validate (push) Failing after 1m31s
2026-07-23 10:28:35 +08:00
API Changelog Bot 27de6f8837 docs: normalize fleet unread handoff markdown
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 10:22:36 +08:00
API Changelog Bot bdb6ac7e92 docs: hand off fleet personal unread badge #5180
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 10:22:16 +08:00
API Changelog Bot 5163692a3f docs(frontend): link backend PR for #5178
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 10:17:56 +08:00
API Changelog Bot b5b18f01d7 docs(frontend): hand off vehicle urgency contract (#5178)
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 10:15:07 +08:00
yaosutu c352c72060 再次通知前端核单Step1全量保存约定
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 10:08:39 +08:00
yaosutu fc2ebf84ac 补充核单Step1数据来源提交约定
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 09:55:07 +08:00
API Changelog Bot 94e3aa4525 chore: keep local temporary user file untracked
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 09:32:26 +08:00
API Changelog Bot 3847d19b20 docs: add dated fleet chat frontend handoff
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 09:32:15 +08:00
API Changelog Bot 3e710dee3c docs: hand off fleet chat UI requirements
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 09:25:20 +08:00
yst bf6e7636ac fix(workflow): 增加 Changelog 文件名检测器 (#24)
changelog-filename-gate / validate (push) Has been cancelled
2026-07-23 09:24:42 +08:00
API Changelog Bot e1bc127abc docs(order-v3): hand off house price contract #5176 2026-07-23 09:22:03 +08:00
API Changelog Bot 8f08de40b6 docs(admin): 通知派车按日车费日期契约 (#5158) 2026-07-22 18:30:22 +08:00
API Changelog Bot ab510d7e62 docs(fleet): hand off multiple resident vehicles (#5160) 2026-07-22 17:54:27 +08:00
API Changelog Bot c3a5b2cd99 docs: 补充车队菜单层级与删除契约 (#5131) 2026-07-22 17:47:19 +08:00
API Changelog Bot 033d60adc9 docs(fleet): expand candidate handoff (#5156) 2026-07-22 17:16:38 +08:00
API Changelog Bot 00365d3d26 docs(fleet): hand off driver confirmation changes (#5146) 2026-07-22 17:06:05 +08:00
API Changelog Bot 6a8f6d870b docs(fleet): hand off team number display (#5156) 2026-07-22 16:57:33 +08:00
wx df88c7256f Merge pull request 'docs(api): 回写派单详情网关验收' (#23) from docs/5149-fleet-detail-team-product into main 2026-07-22 16:35:17 +08:00
API Changelog Bot 37e951eb49 docs(api): record fleet detail gateway verification 2026-07-22 16:34:52 +08:00
wx 14f6349b96 Merge pull request 'docs(api): 通知派单详情团号与产品类型契约' (#22) from docs/5149-fleet-detail-team-product into main 2026-07-22 16:24:10 +08:00
API Changelog Bot 22a51bdd09 docs(api): notify fleet team and product type fields 2026-07-22 16:23:43 +08:00
wx 7f0b279d93 Merge pull request 'docs: 房务订单详情补充团号契约 (#5150)' (#21) from docs/5150-house-detail-team-no into main 2026-07-22 16:02:50 +08:00
API Changelog Bot 1eb4246e3a docs: hand off house detail team number (#5150) 2026-07-22 16:02:04 +08:00
wx fd02530c35 Merge pull request 'docs(api): 回写常驻司机网关验收' (#20) from docs/5145-selected-vehicle-resident-driver into main 2026-07-22 15:42:39 +08:00
API Changelog Bot 21b5b5cf01 docs(api): record resident driver gateway verification 2026-07-22 15:42:06 +08:00
wx e2b305a708 Merge pull request 'docs(api): 通知选车后常驻司机默认配对契约' (#19) from docs/5145-selected-vehicle-resident-driver into main 2026-07-22 15:29:24 +08:00
API Changelog Bot f75c679c9d docs(api): notify resident driver auto-selection contract 2026-07-22 15:28:17 +08:00
wx 8c44c113f3 Merge pull request 'docs(api): 需求不匹配改用显式标签' (#18) from docs/5139-mismatch-tag into main 2026-07-22 14:49:22 +08:00
API Changelog Bot 938ca1702f docs(api): replace mismatch outline with tags for 5139 2026-07-22 14:49:02 +08:00
API Changelog Bot bb3034ac3d docs: 补充司机编辑在线投保前端要求 2026-07-22 14:44:54 +08:00
wx 3fc70f0cf4 Merge pull request 'docs(api): 明确 5139 司机筛选按原型平铺' (#17) from docs/5139-driver-filter-prototype into main 2026-07-22 14:43:38 +08:00
API Changelog Bot a20540e6ee docs(api): require prototype driver filters for 5139 2026-07-22 14:43:22 +08:00
wx aa6f8b0897 Merge pull request 'docs(api): 交接派单司机保险保障状态' (#16) from docs/5141-driver-insurance-candidate into main 2026-07-22 14:37:14 +08:00
API Changelog Bot 60be944703 docs(api): publish driver insurance candidate contract (#5141) 2026-07-22 14:36:57 +08:00
wx 9bb8004bb1 Merge pull request 'docs(api): 补充 5139 目标前端标记' (#15) from docs/5139-target-frontend into main 2026-07-22 13:54:16 +08:00
API Changelog Bot a9f9a4a783 docs(api): mark target frontend for 5139 2026-07-22 13:52:24 +08:00
wx a31b427897 docs(api): 发布派单候选筛选与分页契约 (#14)
关联 wx/HL#5139
2026-07-22 13:42:52 +08:00
API Changelog Bot b676873eec docs(api): publish assignment candidate contract (#5139) 2026-07-22 13:42:26 +08:00
API Changelog Bot f1b8cdf123 docs: 回填 5132 订单标签网关证据 2026-07-22 12:08:41 +08:00
wx 58c7675b2f Merge PR #13: 车队独立管理前端联调契约
关联 wx/HL#5131
2026-07-22 12:02:27 +08:00
API Changelog Bot 1a319196ed docs(api): record fleet team test deployment (#5131) 2026-07-22 11:58:38 +08:00
API Changelog Bot 2049120145 docs: 告知前端使用 5132 真实订单标签 2026-07-22 11:56:48 +08:00
API Changelog Bot 42fff93c72 docs(api): document fleet team management (#5131) 2026-07-22 11:46:17 +08:00
API Changelog Bot f88592504e docs: 调整 5132 出行人年龄展示文案 2026-07-22 11:30:12 +08:00
API Changelog Bot f8b70ec7a8 docs: 回填 5132 网关验证证据 2026-07-22 11:17:41 +08:00
API Changelog Bot d8cb0d6128 docs: 补充 5132 详细出行人契约 2026-07-22 11:07:42 +08:00
API Changelog Bot f96f5302af docs: 关联车务详情工单 5132 2026-07-22 10:44:20 +08:00
API Changelog Bot 02c4ef0552 docs: 标记车务详情任务无工单 2026-07-22 10:40:33 +08:00
API Changelog Bot 142f81ffb3 docs: 补齐车务详情变更单元数据 2026-07-22 10:36:31 +08:00
API Changelog Bot c0d07a0db4 docs: 补充车务派单详情前端契约 2026-07-22 10:33:02 +08:00
API Changelog Bot d8355c3c57 docs: 将景区季节清空通知移至一期 #5123 2026-07-21 21:17:34 +08:00
API Changelog Bot abe546707c docs: 补充景区季节清空前端联调说明 #5123 2026-07-21 20:07:06 +08:00
API Changelog Bot 9c4d8eab39 docs(fleet): 通知前端接入车务派单SSE 2026-07-21 18:22:27 +08:00
API Changelog Bot c53a6d6875 docs(house): 通知前端清理最终确认残留遮罩 2026-07-21 16:53:30 +08:00
yaosutu 33d0999a68 纠正对公转账代收人契约说明 2026-07-21 16:40:57 +08:00
yaosutu 39acbeb0e8 通知前端修正调整订单尾款取值 2026-07-21 15:03:48 +08:00
API Changelog Bot 12f5dffb07 docs(house): 通知前端区分作废配房 2026-07-21 14:14:19 +08:00
API Changelog Bot 0910cbae7c docs(house): 交接改期旧配房人工清理 2026-07-21 13:16:33 +08:00
API Changelog Bot 27c7442900 docs(fleet): 说明矩阵有效服务日期段 2026-07-21 12:23:35 +08:00
API Changelog Bot 9a6edddd38 docs(fleet): 校正矩阵重叠异常示例 2026-07-21 12:15:07 +08:00
API Changelog Bot 1076c68896 docs(fleet): 补充矩阵重叠异常接口契约 2026-07-21 11:35:19 +08:00
API Changelog Bot 5951c3023b fix: 补充矩阵取消过滤与历史展示要求 2026-07-21 10:51:42 +08:00
API Changelog Bot 5b59232713 docs: 通知管理后台改用房务作废筛选 2026-07-21 09:47:01 +08:00
API Changelog Bot a93d1f7fe6 fix: 通知前端修正车务矩阵图例与直接派单 2026-07-21 09:36:44 +08:00
API Changelog Bot 642bed1e69 docs: 通知管理后台接入作废配房快照 2026-07-20 21:12:11 +08:00
API Changelog Bot 9ff8eb4c8c docs: 告知管理后台处理改期旧配房 2026-07-20 18:55:55 +08:00
API Changelog Bot fcb002f662 docs: 要求订单详情增加作废记录入口 2026-07-20 17:43:23 +08:00
API Changelog Bot 364828e5ee docs: 补充房务详情错误路由Network证据 2026-07-20 17:36:53 +08:00
API Changelog Bot 3ad9f1ec85 docs: 更正房务历史详情接口路径 2026-07-20 17:25:32 +08:00
API Changelog Bot da06fa538c docs: 通知管理后台处理作废房务需求只读详情 2026-07-20 17:07:57 +08:00
API Changelog Bot 2608ed2bfa docs: 通知管理后台按总人数展示房务调整 2026-07-20 16:40:12 +08:00
API Changelog Bot fe6b35d235 docs: 移除前端通知本地源码路径 2026-07-20 15:07:30 +08:00
API Changelog Bot e8b3414967 docs: 标明房务按钮问题归属管理后台 2026-07-20 15:06:04 +08:00
API Changelog Bot bf138d29d3 docs: 通知完成态配房按钮被隐藏 2026-07-20 14:27:22 +08:00
API Changelog Bot 319cc684ac docs: 通知房务候选酒店被错误限定 2026-07-20 09:33:16 +08:00
wx 584f0b76ed docs: 通知车务派单可靠通知与取消重派契约 2026-07-19 19:34:19 +08:00
API Changelog Bot ea6f5bacb1 docs: 补充订单详情混合房型契约 2026-07-19 18:35:13 +08:00
API Changelog Bot 317e71ef52 docs: 通知房务混合房型逐行契约 2026-07-19 18:01:18 +08:00
API Changelog Bot c3f0ebd9fd docs: 通知住宿需求允许房型待定 2026-07-19 17:46:18 +08:00
API Changelog Bot 7332ac282a docs: 通知调整记录新增出行人ID 2026-07-19 17:02:16 +08:00
API Changelog Bot 62ab4cdb2c docs: 补充房务调整历史契约字段 2026-07-19 15:56:39 +08:00
wx eb96028029 docs: 补充管理后台 SSE 鉴权与重连修复通知 2026-07-19 15:40:59 +08:00
API Changelog Bot 7c7914bd34 docs: 补充房务 bug 修复交接清单 2026-07-19 14:59:47 +08:00
yaosutu bb01b978b8 修正核团出行人年龄变更验证证据 2026-07-19 11:24:21 +08:00
wx 7a90d75b5a docs: 补充核团出行人出生日期年龄契约 (#5068) 2026-07-19 11:06:48 +08:00
wx e94e8d7e6e docs: 补充核团核算状态契约 (#5066) 2026-07-19 10:42:52 +08:00
API Changelog Bot 493b62dd8c docs: 删除核团详情司机车辆通知 (#5037) 2026-07-19 09:19:02 +08:00
wx 8bb947ea99 Merge PR #12: docs(api): #4938 车务基线差异前端契约
关联 wx/HL#4938、代码 PR wx/HL#5065;不包含内部补偿接口。
2026-07-19 02:24:27 +08:00
API Changelog Bot 07dbd2b595 docs(api): 补充 #4938 车务基线差异契约 2026-07-19 02:20:37 +08:00
API Changelog Bot 2bc7571953 docs(dashboard): 补充统计口径与金额字符串契约 (#5062) 2026-07-18 23:07:58 +08:00
API Changelog Bot d197a6c3aa docs(order-v3): 分离5037前后端契约边界 2026-07-18 22:07:36 +08:00
yaosutu 5e2c374ab1 新增核团核算列表详情接口变更说明 2026-07-18 18:03:32 +08:00
API Changelog Bot cb2bbc7d46 docs(house): 交接待最终确认派生待办 2026-07-18 18:02:42 +08:00
yaosutu ce5bac10e6 补充核单Step1住宿字段前端变更通知 2026-07-18 16:53:16 +08:00
yaosutu 74461331cb 推送核单Step2景区游玩项目字段前端变更 2026-07-18 16:42:29 +08:00
API Changelog Bot 575111baee docs(order): 发布核团详情司机车辆契约 (#5037) 2026-07-18 16:06:53 +08:00
yaosutu 4a88454172 新增预支审批列表接口变更说明 2026-07-18 15:47:44 +08:00
yaosutu cb3c557702 新增核单操作日志查询接口变更说明 2026-07-18 15:22:45 +08:00
API Changelog Bot a9fafb8a14 docs(fleet): 发布车务读模型前端契约 2026-07-18 11:43:16 +08:00
yaosutu e3c0209f5b 推送费用明细已收款拆分接口变更通知 2026-07-18 10:13:12 +08:00
API Changelog Bot 8319198461 docs(grassland): 记录排序与MP4修复正式发布 2026-07-17 12:00:01 +08:00
API Changelog Bot 1e47fb9069 docs(fleet): 补全 #4935 派单回调验收契约 2026-07-16 23:26:23 +08:00
API Changelog Bot 3d59972940 docs: 告知草原指南MP4物理交错修复 2026-07-16 21:27:02 +08:00
API Changelog Bot 88fb95dabb docs: 更新4907房务最终确认契约与验证证据 2026-07-16 18:53:19 +08:00
API Changelog Bot e6b9c86b0e docs: 更正草原指南倍速播放缓冲死锁 2026-07-16 16:58:06 +08:00
API Changelog Bot f852024d4a docs: 告知草原指南1x与2x播放缓冲优化 2026-07-16 16:31:51 +08:00
API Changelog Bot 98740244ca docs: 告知草原指南排序权重正序 #5011 2026-07-16 15:22:53 +08:00
API Changelog Bot da7c3808c9 docs: 标记草原指南免登录接口已正式发布 #5005 2026-07-16 14:26:59 +08:00
API Changelog Bot e8ce58c503 docs: 告知草原指南免登录只读接口 #5005 2026-07-16 10:20:43 +08:00
wx 86b3f7c799 Merge pull request 'docs(草原指南): 通知今日 hl-ui 改动已全部回退' (#11) from docs/release-grassland-folder-scope-prod-20260715 into main 2026-07-15 15:38:03 +08:00
API Changelog Bot d5941cb36c docs(草原指南): 通知今日前端改动已全部回退 2026-07-15 15:35:44 +08:00
API Changelog Bot f49d448d1d docs(fleet): 补充需求级派单完成回调契约 2026-07-15 14:52:30 +08:00
wx 41b7152d8d Merge pull request 'docs: 更正草原指南视频素材查询范围' (#10) from docs/grassland-video-current-folder-only into main 2026-07-15 14:39:19 +08:00
共修改 101 个文件,包含 18067 行新增和 19 行删除
@@ -0,0 +1,46 @@
name: changelog-filename-gate
on:
workflow_dispatch:
pull_request:
branches:
- main
types:
- opened
- reopened
- synchronize
- edited
push:
branches:
- main
jobs:
validate:
name: validate
runs-on: ubuntu-latest
steps:
# Keep the gate self-contained: the test environment cannot reliably clone GitHub Actions repositories.
- name: Checkout full history
run: |
git init -q .
git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git fetch --no-tags --prune origin \
"+refs/heads/*:refs/remotes/origin/*" \
"+refs/pull/*/head:refs/remotes/pull/*/head" \
"+refs/pull/*/merge:refs/remotes/pull/*/merge"
git checkout --detach "$GITHUB_SHA"
- name: Verify Node.js runtime
run: |
node --version
npm --version
node -e "if (Number(process.versions.node.split('.')[0]) < 20) process.exit(1)"
- name: Run regression tests
run: npm test
- name: Validate new changelog filenames
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}"
---
# {模块名}: {一句话概括变化}
> **存放目录**:
+89
查看文件
@@ -0,0 +1,89 @@
# Changelog 贡献规则
## 二期文件名
`/v3/admin/*` 接口写入 `changelogs-v2/`:
```text
changelogs-v2/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除接口}-管理后台.md
```
`/v3/mp/*` 接口写入 `changelogs-v2-mp/`:
```text
changelogs-v2-mp/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除接口}-小程序端.md
```
其中:
- `YYYY-MM` 和 `DD` 必须是校验运行时 `Asia/Shanghai` 的真实年月日,且均须补齐两位。跨越上海零点后仍未合并的 PR,需要把新文件重命名为当天日期。
- `issue` 必须是不带 `#` 的十进制正整数,不允许 `0`、负数或前缀符号。
- 业务标题不能为空。
- 变更类型只能是 `新增接口`、`修改接口` 或 `删除接口`。
- 端类型由目录唯一决定:`changelogs-v2/` 固定为 `管理后台`,`changelogs-v2-mp/` 固定为 `小程序端`。
- 同一改动同时影响 `/v3/admin/*` 和 `/v3/mp/*` 时,应按目录拆成两份。
一期 `changelogs/` 沿用现行格式,不套用上述强制模板。
## 校验范围
检测器读取 `git diff --name-status -z --find-renames` 的结果,只校验本次 diff 新出现的目标路径:
- `A`(新增)、`C`(复制)和 `R`(重命名)的目标路径必须通过规则。
- `M`(修改历史文件)和 `D`(删除)豁免,不会因存量错误命名阻断。
- 重命名到受控目录时,新目标路径必须使用校验当天的上海日期。
本地校验:
```bash
npm test
npm run check:filenames -- --base origin/main --head HEAD
```
生产 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 与服务端阻断边界
Gitea Actions 会在指向 `main` 的 PR 和 `main` 的 push 上运行回归测试与文件名检测。该 workflow 是检测器:
- 在 `main` 未开启分支保护和 required status 时,失败状态不能硬性阻止合并。
- `push` 事件发生在写入之后,只能检测/报警,不能撤销直推。
- 只有管理员另行保护 `main`、关闭直推,并在 workflow 首次成功运行后,从 Gitea 最近上报的 status context 列表中选择实际值作为 required status,才能宣称服务端 hard gate 已激活。激活记录必须保存首次运行链接和 status API/分支保护回读证据;不得预设 job id/name `validate` 就是 Gitea 实际上报的 context。
因此,本仓库文件交付的准确表述是:**detector 已安装;在 `main` 未保护时,服务端 hard gate 尚未激活。**
+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 元数据。
- 不通过重命名表达消费状态,避免破坏文件名校验和历史链接。
@@ -44,3 +44,4 @@ body:`{"orderId": 2071784830965620737}`(必填;可选 `peer`)
- 车务发消息→定制师收到(unread+1);车务读→团队共享水位推进(任一车务读全体清零)。
- 单测 user-service 3099 绿(房务基线零回归)。
- **看板列表红点(PR #4698)实测**:`/admin/fleet/board/orders` 74 单每行含 `orderId`(真数字雪花)+ `unreadMessageCount`;定制师向某单广播一条→重查该行 `unreadMessageCount=1`(团队未读联动看板);user-service 软降级路径不阻断列表。fleet-service 全量 1307 测试绿、ArchTest 11/11。
@@ -197,16 +197,7 @@ GET /v3/admin/order/2075415307597357058/payment/manual-receipt/options
"channel": "BANK_TRANSFER",
"channelText": "银行转账",
"allowedPayTypes": ["DEPOSIT", "FULL"],
"collectors": [
{
"collectorType": "COMPANY_ACCOUNT",
"collectorId": null,
"collectorName": "公司账户",
"collectorRole": "COMPANY_ACCOUNT",
"collectorRoleText": "公司账户",
"defaultSelected": true
}
]
"collectors": []
},
{
"channel": "DRIVER_CASH",
@@ -0,0 +1,322 @@
# 【修改接口·管理后台】费用明细已收款拆分 (#5022)
> **PR**: #5025 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-18 00:00
## 1. 接口背景
管理后台订单费用明细需要区分展示已收订金、已收尾款、已收全款。原接口只返回 `paidAmount` 已收总额,无法直接区分不同收款类型。本次在订单费用接口响应 `data` 内新增 3 个拆分金额字段,`paidAmount` 仍表示已收总额。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 订单费用信息 | GET | `/v3/admin/order/{id}/finance` | 修改接口 | 响应 `data` 新增 `depositPaidAmount`、`balancePaidAmount`、`fullPaidAmount` |
## 3. 接口详情
### 3.1 订单费用信息
- **使用场景**: 查询单个订单的费用汇总、优惠、加价、退款、线上支付交易明细。
- **认证**: 需要管理后台登录态 JWT。
- **幂等性**: 查询接口,幂等。
- **限流**: 无新增限流规则。
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | String | 是 | 订单 ID,路径参数。示例:`2077233785174179841` |
### 4.2 请求体字段
GET 请求,无请求体。
## 5. 出参字段
### 5.1 顶层响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码,`200` 表示成功 |
| `message` | String | 响应消息 |
| `data` | Object | 订单费用信息 |
| `traceId` | String / null | 链路追踪 ID,可能为 `null` |
| `success` | Boolean | 请求是否成功 |
### 5.2 data 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `totalAmount` | String | 订单总金额,金额字符串,单位元 |
| `payableAmount` | String | 应付金额,金额字符串,单位元 |
| `paidAmount` | String | 已收总额,包含成功线上收款和未撤销线下收款 |
| `depositPaidAmount` | String | 新增。实际已收订金金额,成功线上订金 + 未撤销线下订金 |
| `balancePaidAmount` | String | 新增。实际已收尾款金额,成功线上尾款 + 未撤销线下尾款 |
| `fullPaidAmount` | String | 新增。实际已收全款金额,成功线上全款 + 未撤销线下全款 |
| `balanceAmount` | String | 待收余额,金额字符串,单位元 |
| `discountAmount` | String | 优惠总额,金额字符串,单位元 |
| `surchargeAmount` | String | 加价总额,金额字符串,单位元 |
| `refundAmount` | String | 已退金额,金额字符串,单位元 |
| `payments` | Array | 线上支付交易明细;仍只表示线上交易,不包含线下收款明细 |
| `discounts` | Array | 优惠明细 |
| `surcharges` | Array | 加价明细 |
### 5.3 payments 字段项
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 支付交易 ID |
| `paymentNo` | String | 支付流水号 |
| `amount` | String | 支付金额,单位元 |
| `paymentType` | String | 支付类型 |
| `status` | String | 支付状态 |
| `paidAt` | String / null | 支付成功时间,格式 `yyyy-MM-dd HH:mm:ss` |
> 本次未改变 `payments` 语义:它仍只表示线上支付交易明细。线下收款明细仍通过 `GET /v3/admin/order/{orderId}/payment/manual-receipt` 获取;线下金额已聚合进 `depositPaidAmount`、`balancePaidAmount`、`fullPaidAmount` 和 `paidAmount`。
### 5.4 discounts 字段项
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 优惠明细 ID |
| `name` | String | 优惠名称 |
| `amount` | String | 优惠金额,单位元 |
| `type` | String | 优惠类型 |
| `source` | String | 优惠来源 |
| `createdAt` | String | 创建时间,格式 `yyyy-MM-dd HH:mm:ss` |
### 5.5 surcharges 字段项
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 加价明细 ID |
| `name` | String | 加价名称 |
| `amount` | String | 加价金额,单位元 |
| `type` | String | 加价类型 |
| `createdAt` | String | 创建时间,格式 `yyyy-MM-dd HH:mm:ss` |
## 6. 枚举 / 数据字典
### 6.1 paymentType
**所属字段**: `payments[].paymentType` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `DEPOSIT` | 订金 | 订金支付 |
| `BALANCE` | 尾款 | 尾款支付 |
| `FULL` | 全款 | 全款支付 |
### 6.2 status
**所属字段**: `payments[].status` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `SUCCESS` | 支付成功 | 计入对应已收金额 |
| `PENDING` | 待支付 | 不计入对应已收金额 |
| `CLOSED` | 已关闭 | 不计入对应已收金额 |
| `FAILED` | 支付失败 | 不计入对应已收金额 |
### 6.3 type
**所属字段**: `discounts[].type`、`surcharges[].type` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `EARLY_BIRD` | 早鸟优惠 | 早鸟规则产生的优惠 |
| `MANUAL` | 手工调整 | 人工录入的优惠或加价 |
| `OTHER` | 其他 | 其他类型 |
### 6.4 source
**所属字段**: `discounts[].source` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `EARLY_BIRD_PLAN` | 早鸟方案 | 来源于早鸟优惠方案 |
| `MANUAL` | 手工录入 | 来源于人工录入 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 订单费用信息查询成功 |
| `401` | 未登录或登录失效 | 未携带有效管理后台 JWT |
| `403` | 无权限 | 当前账号无权访问该订单费用信息 |
| `404` | 订单不存在 | 路径参数 `id` 对应订单不存在 |
| `500` | 系统异常 | 服务端处理异常 |
## 8. 示例
### 8.1 典型成功
**请求**:
```http
GET /v3/admin/order/2077233785174179841/finance HTTP/1.1
Authorization: Bearer <token>
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"totalAmount": "3105.00",
"payableAmount": "2955.00",
"paidAmount": "2500.00",
"depositPaidAmount": "2000.00",
"balancePaidAmount": "500.00",
"fullPaidAmount": "0",
"balanceAmount": "455.00",
"discountAmount": "150.00",
"surchargeAmount": "0.00",
"refundAmount": "0.00",
"payments": [],
"discounts": [
{
"id": "2077233785199345666",
"name": "早鸟优惠:早鸟-小团减150(适用人群:成人/儿童/小童)",
"amount": "150.00",
"type": "EARLY_BIRD",
"source": "EARLY_BIRD_PLAN",
"createdAt": "2026-07-15 11:28:22"
}
],
"surcharges": []
},
"traceId": null,
"success": true
}
```
### 8.2 边界情况
**场景说明**: 订单暂无任何成功线上收款和未撤销线下收款时,所有已收拆分金额均返回 0 金额;明细数组可为空数组。
**请求**:
```http
GET /v3/admin/order/2077233785174179841/finance HTTP/1.1
Authorization: Bearer <token>
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"totalAmount": "3105.00",
"payableAmount": "2955.00",
"paidAmount": "0",
"depositPaidAmount": "0",
"balancePaidAmount": "0",
"fullPaidAmount": "0",
"balanceAmount": "2955.00",
"discountAmount": "150.00",
"surchargeAmount": "0.00",
"refundAmount": "0.00",
"payments": [],
"discounts": [],
"surcharges": []
},
"traceId": null,
"success": true
}
```
### 8.3 业务失败
**场景说明**: 订单 ID 不存在。
**请求**:
```http
GET /v3/admin/order/9999999999999999999/finance HTTP/1.1
Authorization: Bearer <token>
```
无请求体。
**响应**:
```json
{
"code": 404,
"message": "订单不存在",
"data": null,
"traceId": null,
"success": false
}
```
## 9. 业务边界
- **适用场景**: 管理后台查询订单费用明细时调用,订单存在且当前账号有访问权限。
- **不适用场景**: 用该接口获取线下收款明细列表;线下收款明细仍由 `GET /v3/admin/order/{orderId}/payment/manual-receipt` 返回。
- **特殊边界**: `payments` 为空不代表订单没有已收金额;可能存在未撤销线下收款,已聚合到本次新增的拆分金额和 `paidAmount`。
- **金额口径**: `paidAmount` 为已收总额;`depositPaidAmount`、`balancePaidAmount`、`fullPaidAmount` 为按收款类型拆分后的已收金额。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `data.depositPaidAmount` | 不返回 | 返回实际已收订金金额 |
| `data.balancePaidAmount` | 不返回 | 返回实际已收尾款金额 |
| `data.fullPaidAmount` | 不返回 | 返回实际已收全款金额 |
| `data.paidAmount` | 返回已收总额 | 继续返回已收总额,语义不变 |
| `data.payments` | 返回线上支付交易明细 | 继续只返回线上支付交易明细,语义不变 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 已收金额拆分 | 只能读取 `paidAmount` 总额 | 可读取订金、尾款、全款 3 类已收金额 |
| 线下收款聚合 | `paidAmount` 中包含线下收款,无法按类型拆分 | 线下收款按类型聚合进新增拆分字段和 `paidAmount` |
| 线上支付明细 | `payments` 表示线上支付交易明细 | 保持不变,仍不包含线下收款明细 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**: 否。本次只新增响应字段,未删除或改名已有字段。
- **前端是否必须同步上线**: 否。旧前端继续读取 `paidAmount` 不受影响;需要区分已收订金、已收尾款、已收全款时读取新增字段。
- **影响已有数据**: 否。历史订单按成功线上收款和未撤销线下收款聚合返回。
### 11.2 回滚方案
- **回滚方式**: 回滚 PR #5025 后,接口不再返回 3 个新增字段。
- **回滚后兼容**: 只依赖 `paidAmount` 的旧逻辑不受影响;依赖新增字段的消费方需要兼容字段缺失。
- **回滚后清理**: 无需清理前端侧数据。
## 12. 注意事项
- `depositPaidAmount`、`balancePaidAmount`、`fullPaidAmount` 是金额字符串,单位元。
- 金额为 0 时可能返回 `"0"` 或 `"0.00"`,消费方不要依赖固定小数位判断金额语义。
- `payments` 为空时仍可能存在已收金额,因为线下收款不进入 `payments`。
- 线下收款明细列表仍由 `GET /v3/admin/order/{orderId}/payment/manual-receipt` 获取。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5022](https://git.1814.love:8443/wx/HL/issues/5022)
- **PR**: [#5025](https://git.1814.love:8443/wx/HL/pulls/5025)
- **Merge commit**: [aeb2d6d24](https://git.1814.love:8443/wx/HL/commit/aeb2d6d248fcc0fe49a540bfb3b864f06bc00123)
### 13.2 联系人
- **后端负责人**: 腰苏图
@@ -0,0 +1,261 @@
# 【新增接口·管理后台】核单操作日志查询 (#5038)
> **PR**: [#5042](https://git.1814.love:8443/wx/HL/pulls/5042) | **服务**: `hl-order-service-v3` | **更新时间**: 2026-07-18 15:30
## 1. 接口背景
核单页新增独立操作日志页签,用于查看当前订单在核单流程中的关键写入动作,包括住宿核单、门票/活动核单、人员费用、补助、返还记录、主报账对账、提交核单和财务确认。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 查询核单操作日志 | GET | `/v3/admin/order/{orderId}/settlement/logs` | 新增接口 | 按订单分页返回核单专用操作日志 |
## 3. 接口详情
### 3.1 查询核单操作日志
- **使用场景**: 订单详情核单页签内展示核单操作历史。
- **认证**: 需要管理后台 JWT。
- **幂等性**: 是。GET 查询不产生写入。
- **排序**: 按 `operatedAt` 倒序;同一时间按 `id` 倒序。
- **请求体**: 无。
## 4. 接口入参
### 4.1 路径参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `orderId` | string | 是 | 订单 ID。后端按 64 位整数处理,前端按字符串保存和传递。 |
### 4.2 Query 参数
| 字段 | 类型 | 必填 | 默认值 | 校验规则 | 说明 |
|---|---|---|---|---|---|
| `page` | number | 否 | `1` | 最小 `1` | 当前页码。 |
| `pageSize` | number | 否 | `20` | `1` 到 `100` | 每页条数。 |
## 5. 出参
接口返回 `Result<PageResult<RecordVO>>`。
### 5.1 顶层响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | number | 状态码,成功为 `200`。 |
| `message` | string | 响应消息,成功为 `成功`。 |
| `data` | object | 分页数据。 |
| `traceId` | string \| null | 链路追踪 ID。 |
| `success` | boolean | 是否成功。 |
### 5.2 `data` 字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `records` | array | 操作日志记录列表。无日志时为空数组。 |
| `total` | number | 总记录数。 |
| `page` | number | 当前页码。 |
| `pageSize` | number | 每页条数。 |
### 5.3 `records[]` 字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 日志 ID。 |
| `orderId` | string | 订单 ID。 |
| `operationType` | string | 操作类型编码,见第 6 节。 |
| `operationTypeName` | string | 操作类型中文名。 |
| `operationObject` | string | 操作对象编码,见第 6 节。 |
| `operationObjectName` | string | 操作对象中文名。 |
| `content` | string | 操作内容。 |
| `operatorType` | string | 操作人类型,见第 6 节。 |
| `operatorId` | string \| null | 操作人 ID。系统自动操作时可为 `null`。 |
| `operatorName` | string | 操作人名称。 |
| `operatedAt` | string | 操作时间,格式示例 `2026-07-18 15:15:12`。 |
| `beforeSnapshot` | object \| array \| null | 改动前快照。结构随操作对象变化。 |
| `afterSnapshot` | object \| array \| null | 改动后快照。结构随操作对象变化。 |
| `changeItems` | array | 改动项列表。无差异时为空数组。 |
### 5.4 `changeItems[]` 字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `field` | string | 改动字段。非对象快照整体变化时为 `snapshot`。 |
| `fieldName` | string | 改动字段展示名。当前与 `field` 同值;整体变化时为 `整体快照`。 |
| `beforeValue` | any | 改动前值。 |
| `afterValue` | any | 改动后值。 |
## 6. 枚举 / 数据字典
### 6.1 `operationType`
| 值 | 中文 | 说明 |
|---|---|---|
| `SAVE_STEP1` | 保存住宿核单 | 保存 Step1 住宿核单明细时生成。 |
| `SAVE_STEP2` | 保存门票/活动核单 | 保存 Step2 门票/活动核单明细时生成。 |
| `SAVE_STEP3` | 保存人员费用 | 保存 Step3 人员费用核单时生成。 |
| `SAVE_STEP4` | 保存补助 | 保存 Step4 补助时生成。 |
| `ADD_REFUND` | 新增返还记录 | 新增 Step5 返还记录时生成;终止行程自动生成返还记录也使用该类型。 |
| `DELETE_REFUND` | 删除返还记录 | 删除 Step5 返还记录时生成。 |
| `SAVE_RECON` | 保存主报账对账 | 保存主报账对账信息时生成。 |
| `SUBMIT` | 提交核单 | Step6 提交核单时生成。 |
| `CONFIRM` | 财务确认 | 财务确认结算时生成。 |
### 6.2 `operationObject`
| 值 | 中文 | 说明 |
|---|---|---|
| `HOTEL` | 住宿核单 | 住宿核单相关操作对象。 |
| `TICKET` | 门票/活动核单 | 门票或活动核单相关操作对象。 |
| `STAFF_FEES` | 人员费用 | 司机、导游、摄影或其他人员费用。 |
| `SUBSIDY` | 补助 | 补助核单相关操作对象。 |
| `REFUND` | 返还记录 | 返还记录相关操作对象。 |
| `RECON` | 主报账对账 | 主报账对账相关操作对象。 |
| `SETTLEMENT` | 核单结算 | 提交核单或财务确认等整体结算操作对象。 |
### 6.3 `operatorType`
| 值 | 中文 | 说明 |
|---|---|---|
| `ADMIN` | 管理后台用户 | 管理后台人工操作。 |
| `SYSTEM` | 系统自动 | 定时任务或内部流程自动触发。 |
| `USER` | C 端用户 | 当前核单日志一般不使用,保留统一操作人类型。 |
| `MQ` | 消息回调 | 当前核单日志一般不使用,保留统一操作人类型。 |
## 7. 错误码
| code | 含义 | 触发场景 |
|---|---|---|
| `200` | 成功 | 查询成功,含无日志空列表。 |
| `400` | 参数校验失败 | `page < 1`、`pageSize < 1`、`pageSize > 100`,或 `orderId` 无法解析为整数。 |
| `401` | 未认证 | 未携带有效管理后台 JWT。 |
## 8. 示例
### 8.1 典型成功
**请求**
```http
GET /v3/admin/order/2077233855281971202/settlement/logs?page=1&pageSize=20
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "2078378034477375490",
"orderId": "2077233855281971202",
"operationType": "SAVE_STEP4",
"operationTypeName": "保存补助",
"operationObject": "SUBSIDY",
"operationObjectName": "补助",
"content": "保存补助",
"operatorType": "ADMIN",
"operatorId": "1001",
"operatorName": "admin",
"operatedAt": "2026-07-18 15:15:12",
"beforeSnapshot": [],
"afterSnapshot": [],
"changeItems": []
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"traceId": null,
"success": true
}
```
### 8.2 边界:暂无日志
**请求**
```http
GET /v3/admin/order/2076236236812345346/settlement/logs?page=1&pageSize=20
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
},
"traceId": null,
"success": true
}
```
### 8.3 异常:未登录
**请求**
```http
GET /v3/admin/order/2077233855281971202/settlement/logs?page=1&pageSize=20
```
**响应**
```json
{
"code": 401,
"message": "缺少有效 Authorization 头",
"data": null,
"traceId": null,
"success": false
}
```
## 9. 业务边界
- 该接口只查询核单专用操作日志,不返回订单状态日志、支付流水、调整订单记录或房车操作日志。
- 无核单日志时返回空分页,不视为异常。
- `beforeSnapshot`、`afterSnapshot` 的内部字段随操作对象变化,前端应把它们作为 JSON 快照展示或按对象类型做兼容解析。
- `changeItems` 只表达快照层面的差异;数组类快照整体变化时可能只返回一条 `field=snapshot` 的整体改动项。
- 查询接口本身不会生成日志;日志由对应核单写入动作生成。
## 10. 修改前后对比
新增接口,无历史接口可对比。
## 11. 影响评估 / 回滚
- **是否破坏向后兼容**: 否,新增接口。
- **前端是否必须同步上线**: 否。不接入该接口时只是不展示核单操作日志页签。
## 12. 注意事项
- 前端展示长整型 ID 时按字符串处理,避免精度丢失。
- 前端不要根据 `operationTypeName` 或 `operationObjectName` 反推状态;需要判断类型时使用编码字段。
- 空列表是合法状态,适用于未开始核单或日志功能上线前的历史订单。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5038](https://git.1814.love:8443/wx/HL/issues/5038)
- **PR**: [#5042](https://git.1814.love:8443/wx/HL/pulls/5042)
- **Merge commit**: [176405d](https://git.1814.love:8443/wx/HL/commit/176405d489df22a184e848df88a64fe104a6672f)
### 13.2 联系人
- **后端负责人**: @yaosutu
- **前端对接**: 管理后台前端
@@ -0,0 +1,319 @@
# 【新增接口·管理后台】预支审批列表接口 (#5041)
> **PR**: #5044 / #5046 | **服务**: hl-order-service-v3 + hl-user-service | **更新时间**: 2026-07-18 15:40
## 1. 接口背景
管理后台需要在财务菜单下独立查看待审批、已通过、已驳回的订单预支记录。此前预支记录只能从订单详情上下文查看,财务人员缺少全局审批列表入口。
本次新增全局分页查询接口,并新增菜单入口:
- 菜单目录:`财务管理`
- 菜单名称:`预支审批`
- 菜单路由:`advance-approvals`
- 前端组件:`finance/AdvanceApprovalList`
- 列表权限:`order:advance-approval:page`
- 通过按钮权限:`order:advance-approval:approve`
- 驳回按钮权限:`order:advance-approval:reject`
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 分页查询预支审批列表 | GET | `/v3/admin/order/advance-approvals/page` | 新增接口 | 按审批状态分页查询预支记录,并返回订单摘要字段 |
## 3. 接口详情
### 3.1 分页查询预支审批列表
- **使用场景**:财务人员进入“财务管理 / 预支审批”页面时查询预支审批列表。
- **认证**:需要管理后台 JWT。
- **权限点**:`order:advance-approval:page`。
- **幂等性**:是。该接口只读,不修改数据。
- **排序**:按 `submittedAt` 倒序,其次按 `createTime` 倒序,再按 `id` 倒序。
## 4. 接口入参
### 4.1 Query 参数
| 字段 | 类型 | 必填 | 默认值 | 说明 | 校验规则 |
|------|------|------|--------|------|----------|
| `page` | Integer | 否 | `1` | 页码 | 最小值 `1` |
| `pageSize` | Integer | 否 | `20` | 每页条数 | 最小值 `1`,最大值 `100` |
| `status` | String | 否 | `SUBMITTED` | 审批状态 | 可传 `SUBMITTED` / `APPROVED` / `REJECTED`;大小写不敏感,后端会转大写 |
| `orderId` | String | 否 | 无 | 订单 ID 精确筛选 | 雪花 ID,前端按字符串处理 |
| `keyword` | String | 否 | 无 | 订单关键字 | 模糊匹配订单号、团号、产品名 |
| `payeeName` | String | 否 | 无 | 收款人姓名 | 模糊匹配 |
| `createdByName` | String | 否 | 无 | 申请人姓名 | 模糊匹配 |
| `submittedAtFrom` | String | 否 | 无 | 提交时间开始 | 格式 `yyyy-MM-dd'T'HH:mm:ss` |
| `submittedAtTo` | String | 否 | 无 | 提交时间结束 | 格式 `yyyy-MM-dd'T'HH:mm:ss` |
### 4.2 请求体
无请求体。
## 5. 出参
### 5.1 响应结构
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
},
"traceId": "可选链路追踪ID",
"success": true
}
```
### 5.2 `data` 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `records` | Array | 当前页记录列表 |
| `total` | Integer | 总记录数 |
| `page` | Integer | 当前页码 |
| `pageSize` | Integer | 每页条数 |
### 5.3 `records[]` 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 预支 ID |
| `orderId` | String | 订单 ID |
| `payeeStaffId` | String | 收款人对应的订单人员分配 ID |
| `payeeName` | String | 收款人姓名 |
| `payeeRole` | String | 收款人角色编码 |
| `payeeRoleText` | String | 收款人角色中文 |
| `advanceType` | String | 预支类型 |
| `amount` | Number | 预支金额 |
| `purpose` | String | 用途说明 |
| `voucherUrl` | String | 凭证 URL,可为空 |
| `status` | String | 预支审批状态编码 |
| `statusText` | String | 预支审批状态中文 |
| `rejectReason` | String | 驳回原因,仅驳回记录通常有值 |
| `createdByName` | String | 申请人姓名 |
| `createTime` | String | 创建时间,格式为 ISO 日期时间 |
| `submittedAt` | String | 提交审批时间,格式为 ISO 日期时间 |
| `approvedAt` | String | 审批时间,通过或驳回后有值 |
| `approvedBy` | String | 审批人姓名 |
| `orderNo` | String | 订单号 |
| `teamNo` | String | 团号 |
| `productName` | String | 产品名称 |
| `departDate` | String | 出发日期,格式 `yyyy-MM-dd` |
| `returnDate` | String | 返程日期,格式 `yyyy-MM-dd` |
| `consultantName` | String | 定制师姓名 |
| `orderStatus` | String | 订单状态编码 |
| `orderStatusName` | String | 订单状态中文 |
| `flowStatus` | String | 流程状态编码 |
| `flowStatusName` | String | 流程状态中文 |
| `payStatus` | String | 支付状态编码 |
| `payStatusName` | String | 支付状态中文 |
| `settlementStatus` | String | 结算状态编码 |
| `settlementStatusName` | String | 结算状态中文 |
| `orderAmount` | String | 订单应收金额 |
| `paidAmount` | String | 订单已收金额 |
## 6. 枚举 / 数据字典
### 6.1 `status` / `records[].status`:预支审批状态
| 值 | 中文 | 说明 |
|----|------|------|
| `SUBMITTED` | 待审批 | 创建预支后进入待审批状态;默认查询此状态 |
| `APPROVED` | 已通过 | 财务审批通过 |
| `REJECTED` | 已驳回 | 财务审批驳回,通常带 `rejectReason` |
### 6.2 `records[].payeeRole`:收款人角色
| 值 | 中文 | 说明 |
|----|------|------|
| `DRIVER` | 司机 | 司机人员 |
| `LEADER` | 导游 | 导游人员 |
| `PHOTOGRAPHER` | 摄影师 | 摄影人员 |
| `OTHER` | 其他 | 其他人员 |
### 6.3 订单状态类字段
`orderStatus`、`flowStatus`、`payStatus`、`settlementStatus` 返回系统内已有状态编码;对应中文展示优先使用同记录里的 `orderStatusName`、`flowStatusName`、`payStatusName`、`settlementStatusName`,前端不需要硬编码中文。
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询成功 |
| `585005` | 预支当前状态不允许此操作 | `status` 传入值不在 `SUBMITTED` / `APPROVED` / `REJECTED` 内 |
| `400` | 参数校验失败 | `page < 1`、`pageSize < 1`、`pageSize > 100` 或日期格式不符合要求 |
| `401` | 未认证 | 未携带有效管理后台 JWT |
## 8. 示例
### 8.1 典型成功:查询待审批列表
**请求**:
```http
GET /v3/admin/order/advance-approvals/page?page=1&pageSize=10&status=SUBMITTED HTTP/1.1
Authorization: Bearer {adminToken}
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "2077000000000000001",
"orderId": "2076000000000000001",
"payeeStaffId": "2076000000000000101",
"payeeName": "张三",
"payeeRole": "DRIVER",
"payeeRoleText": "司机",
"advanceType": "ACCOMMODATION_DEPOSIT",
"amount": 500.00,
"purpose": "住宿押金",
"voucherUrl": "https://example.test/voucher/advance-001.jpg",
"status": "SUBMITTED",
"statusText": "待审批",
"rejectReason": null,
"createdByName": "腰苏图",
"createTime": "2026-07-18T10:20:30",
"submittedAt": "2026-07-18T10:20:30",
"approvedAt": null,
"approvedBy": null,
"orderNo": "HL202607180001",
"teamNo": "T202607180001",
"productName": "草原亲子 3 日游",
"departDate": "2026-07-21",
"returnDate": "2026-07-23",
"consultantName": "腰苏图",
"orderStatus": "PENDING_DEPARTURE",
"orderStatusName": "待出行",
"flowStatus": "PENDING_DEPARTURE",
"flowStatusName": "待出行",
"payStatus": "PAID",
"payStatusName": "已支付",
"settlementStatus": "NONE",
"settlementStatusName": "未核单",
"orderAmount": "3600.00",
"paidAmount": "3600.00"
}
],
"total": 1,
"page": 1,
"pageSize": 10
},
"success": true
}
```
### 8.2 边界情况:无记录
**请求**:
```http
GET /v3/admin/order/advance-approvals/page?page=1&pageSize=20&status=APPROVED&keyword=NO_MATCH_KEYWORD HTTP/1.1
Authorization: Bearer {adminToken}
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
},
"success": true
}
```
### 8.3 异常情况:非法审批状态
**请求**:
```http
GET /v3/admin/order/advance-approvals/page?page=1&pageSize=10&status=BAD_STATUS HTTP/1.1
Authorization: Bearer {adminToken}
```
无请求体。
**响应**:
```json
{
"code": 585005,
"message": "预支当前状态不允许此操作",
"data": null,
"success": false
}
```
## 9. 业务边界
- `status` 不传时默认查 `SUBMITTED`。
- `status` 支持小写或混合大小写,后端统一转大写后校验。
- `keyword` 只匹配订单号、团号、产品名。
- `payeeName` 只匹配收款人姓名。
- `createdByName` 只匹配申请人姓名。
- `submittedAtFrom` 与 `submittedAtTo` 都是闭区间过滤条件。
- 金额字段中,预支金额 `amount` 为数值;订单金额 `orderAmount`、`paidAmount` 为字符串,前端按字符串展示或转高精度数值处理。
- ID 类字段均按字符串处理,避免 JS 数字精度问题。
## 10. 修改前后对比
新增接口,无旧接口对比。
## 11. 影响评估 / 回滚
新增接口和新增菜单入口,不破坏已有接口契约。
- **是否破坏向后兼容**:否。
- **前端是否必须同步上线**:否;未接入该页面时不影响原订单详情预支能力。
- **回滚影响**:回滚后“财务管理 / 预支审批”菜单和列表接口不可用。
## 12. 注意事项
- 前端页面应挂到 `财务管理 / 预支审批`。
- 查询接口只负责列表展示,不执行审批动作。
- 审批通过、驳回按钮权限已经随菜单一起下发,按钮可以按权限点控制展示或禁用。
- 当前菜单按钮节点 `visible=false`,用于权限控制,不作为侧边栏可见菜单展示。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5041](https://git.1814.love:8443/wx/HL/issues/5041)
- **PR**: [#5044](https://git.1814.love:8443/wx/HL/pulls/5044)
- **部署修复 Issue**: [#5045](https://git.1814.love:8443/wx/HL/issues/5045)
- **部署修复 PR**: [#5046](https://git.1814.love:8443/wx/HL/pulls/5046)
- **Merge commit**: [b1ac11c03](https://git.1814.love:8443/wx/HL/commit/b1ac11c030a56a94fd8623f44b5a630fb41f2da9)
### 13.2 验证记录
- 测试服 `hl-user-service` 8081/8181 双实例部署成功。
- 测试服 `hl-order-service-v3` 8086/8186 双实例部署成功。
- 网关实调 `GET /v3/admin/order/advance-approvals/page?page=1&pageSize=10&status=SUBMITTED` 返回 `code=200`、`records=1`。
- 网关实调非法 `status=BAD_STATUS` 返回 `code=585005`。
- 网关实调 `/admin/menu/my` 已返回“预支审批”菜单和通过/驳回按钮权限。
### 13.3 联系人
- **后端负责人**: @yst
@@ -0,0 +1,397 @@
# 【修改接口·管理后台】核单 Step1 住宿成本字段 (#5043)
> **PR**: #5047 / #5162 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-23 09:53
## 1. 接口背景
核单 Step1 住宿成本明细需要对齐原型里的酒店资源、房型资源、核算单价、来源和确认状态展示。现有接口保留原路径,在 `GET/PUT /v3/admin/order/{orderId}/settlement/step1` 上做兼容增强。
### 1.1 2026-07-23 前端对接补充(数据来源提交约定)
`sourceType` 只用于区分住宿明细的数据来源,不决定行是否可编辑。行的可编辑性继续由 `settlementConfirmStatus` 控制,本次不新增 `deletable` 或 `sourceFieldsEditable` 字段。
| 数据来源 | GET 返回 / PUT 回传的 `sourceType` | PUT 回传的 `sourceId` | PUT 回传的 `hotelAssignmentId` |
|----------|------------------------------------------|----------------------------|---------------------------------------|
| 后台配房自动行 | `HOUSE_ASSIGNMENT` | 原样回传 GET 返回的配房记录 ID | 原样回传 GET 返回的配房记录 ID |
| 前端手动新增行 | `MANUAL` | `null` | `null` |
`sourceType` 当前仍为非必填字段:未传时,后端可根据 `hotelAssignmentId` 推断数据来源。为了稳定保留来源信息,前端保存时应按上表显式回传。
**后台配房自动行 PUT 关键字段**:
```json
{
"sourceType": "HOUSE_ASSIGNMENT",
"sourceId": "2077233886282088401",
"hotelAssignmentId": "2077233886282088401",
"settlementConfirmStatus": "UNCONFIRMED"
}
```
**前端手动新增行 PUT 关键字段**:
```json
{
"sourceType": "MANUAL",
"sourceId": null,
"hotelAssignmentId": null,
"settlementConfirmStatus": "UNCONFIRMED"
}
```
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查询住宿核算明细 | GET | `/v3/admin/order/{orderId}/settlement/step1` | 修改接口 | `HotelItemVO` 出参新增 10 个字段 |
| 2 | 保存住宿核算明细 | PUT | `/v3/admin/order/{orderId}/settlement/step1` | 修改接口 | `HotelItemVO` 入参支持保存酒店/房型资源、单价、来源和确认状态字段 |
## 3. 接口详情
### 3.1 查询住宿核算明细
- **使用场景**:进入核单 Step1 住宿页签时查询住宿成本明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:幂等,只读查询。
- **响应结构**:`data` 为 `HotelItemVO[]` 数组。
### 3.2 保存住宿核算明细
- **使用场景**:保存核单 Step1 住宿成本明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:全量替换保存;同一份 `items` 重复提交后,以最后一次提交为准。
- **请求体兼容**:推荐使用 `{ "items": [...] }`;历史数组 body `[...]` 仍兼容。
## 4. 接口入参
### 4.1 路径参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | string | 是 | 订单 ID,长整型字符串 |
### 4.2 PUT 请求体字段
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `items` | array | 是 | 住宿成本明细行数组,全量替换保存 | 不允许为 `null` |
| `items[].id` | string | 否 | 已存在行 ID;全量替换保存时可不传 | 长整型字符串 |
| `items[].hotelAssignmentId` | string/null | 否 | 配房 assignment ID;后台配房自动行应原样回传,手动行传 `null` | 长整型字符串或 `null` |
| `items[].hotelId` | string/null | 否 | 酒店资源 ID;本次新增 | 长整型字符串或 `null` |
| `items[].roomTypeId` | string/null | 否 | 房型资源 ID;本次新增 | 长整型字符串或 `null` |
| `items[].stayDate` | string | 是 | 入住日期 | `yyyy-MM-dd`,不能早于订单出发日 |
| `items[].hotelName` | string | 是 | 酒店名称 | 1-200 字符 |
| `items[].roomType` | string/null | 否 | 房型分类或旧展示字段 | 最大 64 字符 |
| `items[].roomTypeName` | string/null | 否 | 房型/规格名称;本次新增 | 最大 64 字符 |
| `items[].roomCount` | integer | 是 | 总间数 | 正整数 |
| `items[].unitPrice` | number/null | 否 | 核算单价,单位元/间夜;本次新增;不传时按 `actualCost / roomCount` 降级计算 | `>= 0` |
| `items[].plannedCost` | number | 是 | 计划成本,单位元 | `>= 0` |
| `items[].actualCost` | number | 是 | 实际成本,单位元 | `>= 0` |
| `items[].paymentMethod` | string | 否 | 付款方式;与 `settleType` 二选一 | `SIGNED` / `COMPANY_PAID` / `CASH_PAID` |
| `items[].paymentMethodName` | string | 否 | 付款方式中文名,仅展示字段;保存时可不传;本次新增 | 最大 32 字符 |
| `items[].settleType` | string | 否 | 配房结算类型;与 `paymentMethod` 二选一 | `cash` / `sign` / `company` |
| `items[].sourceType` | string | 否 | 仅标识数据来源;后台配房行回传 `HOUSE_ASSIGNMENT`,手动行传 `MANUAL`;不传时后端按 `hotelAssignmentId` 推断 | `HOUSE_ASSIGNMENT` / `MANUAL` / `TEMPLATE` |
| `items[].sourceTypeName` | string | 否 | 来源类型中文名,仅展示字段;保存时可不传;本次新增 | 最大 32 字符 |
| `items[].sourceId` | string/null | 否 | 来源业务 ID;后台配房自动行应原样回传配房记录 ID,手动行传 `null` | 长整型字符串或 `null` |
| `items[].settlementConfirmStatus` | string | 否 | 核单确认状态;本次新增;不传默认 `CONFIRMED` | `UNCONFIRMED` / `CONFIRMED` |
| `items[].settlementConfirmStatusName` | string | 否 | 核单确认状态中文名,仅展示字段;保存时可不传;本次新增 | 最大 32 字符 |
| `items[].remark` | string/null | 否 | 备注 | 最大 500 字符 |
| `items[].voucherUrls` | array | 否 | 凭证图片 URL 数组 | 字符串数组 |
## 5. 出参
### 5.1 GET 响应字段:`HotelItemVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string/null | 核单住宿明细行 ID;首次派生未保存的行可为 `null` |
| `hotelAssignmentId` | string/null | 配房 assignment ID;手工行可为 `null` |
| `hotelId` | string/null | 酒店资源 ID;本次新增 |
| `roomTypeId` | string/null | 房型资源 ID;本次新增 |
| `stayDate` | string | 入住日期,`yyyy-MM-dd` |
| `hotelName` | string | 酒店名称 |
| `roomType` | string/null | 房型分类或旧展示字段 |
| `roomTypeName` | string/null | 房型/规格名称;本次新增 |
| `roomCount` | integer | 总间数 |
| `unitPrice` | number/null | 核算单价,单位元/间夜;本次新增 |
| `plannedCost` | number | 计划成本,单位元 |
| `actualCost` | number | 实际成本,单位元 |
| `paymentMethod` | string | 付款方式 |
| `paymentMethodName` | string/null | 付款方式中文名;本次新增 |
| `settleType` | string/null | 配房结算类型;保存草稿后可能为空 |
| `sourceType` | string | 来源类型;本次新增 |
| `sourceTypeName` | string/null | 来源类型中文名;本次新增 |
| `sourceId` | string/null | 来源业务 ID;本次新增 |
| `settlementConfirmStatus` | string | 核单确认状态;本次新增 |
| `settlementConfirmStatusName` | string/null | 核单确认状态中文名;本次新增 |
| `remark` | string/null | 备注 |
| `voucherUrls` | array | 凭证图片 URL 数组 |
### 5.2 PUT 响应字段:`SettlementHotelSaveRespVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `addedIds` | string[] | 本次保存新增的核单住宿明细行 ID 列表 |
| `updatedIds` | string[] | 本次保存更新的核单住宿明细行 ID 列表;当前全量替换语义下通常为空数组 |
| `deletedIds` | string[] | 本次保存删除的核单住宿明细行 ID 列表;当前返回通常为空数组 |
| `totalActualCost` | string | 保存后 Step1 实际成本合计,单位元 |
## 6. 枚举 / 数据字典
### 6.1 `paymentMethod`
**所属字段**:`items[].paymentMethod`、`data[].paymentMethod` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `SIGNED` | 签单 | 现场签单 |
| `COMPANY_PAID` | 公司付款 | 公司统一付款 |
| `CASH_PAID` | 现付 | 现场现金或线下现付 |
### 6.2 `settleType`
**所属字段**:`items[].settleType`、`data[].settleType` | **类型**:String | **必填**:否
| 值 | 中文 | 映射后的 `paymentMethod` |
|----|------|--------------------------|
| `cash` | 现付 | `CASH_PAID` |
| `sign` | 签单 | `SIGNED` |
| `company` | 公司付款 | `COMPANY_PAID` |
### 6.3 `sourceType`
**所属字段**:`items[].sourceType`、`data[].sourceType` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `HOUSE_ASSIGNMENT` | 配房结果 | 来自房务配房结果 |
| `MANUAL` | 手工 | 核单手工补充住宿行 |
| `TEMPLATE` | 模板 | 模板来源住宿行,当前预留 |
### 6.4 `settlementConfirmStatus`
**所属字段**:`items[].settlementConfirmStatus`、`data[].settlementConfirmStatus` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `UNCONFIRMED` | 未确认 | 核单住宿明细未确认 |
| `CONFIRMED` | 已确认 | 核单住宿明细已确认;不传时默认该值 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `584002` | 当前核单状态不允许录住宿核单 | PUT 保存时,订单不是「待核单」或「核单中」 |
| `584008` | 订单缺出发日期,无法派生 dayNumber | PUT 保存时订单出发日期为空 |
| `584009` | `stayDate` 早于订单出发日期 | PUT 保存时日期越界 |
| `584062` | 临时行必须指定付款方式 | `paymentMethod` 和 `settleType` 都为空 |
| `584064` | 配房记录 `settleType` 字典值非法 | `settleType` 不是 `cash/sign/company` |
| `100001` | 参数非法 | 字段格式不符合校验,例如枚举值不在允许范围内、金额小于 0 |
## 8. 示例
### 8.1 典型成功:GET 查询
**请求**
```http
GET /v3/admin/order/2077233855281971202/settlement/step1
Authorization: Bearer {token}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": [
{
"id": "2077328317421187001",
"hotelAssignmentId": "2077233886282088401",
"hotelId": "50001",
"roomTypeId": "51001",
"stayDate": "2026-07-18",
"hotelName": "海拉尔海棠酒店",
"roomType": "STANDARD",
"roomTypeName": "精品标间",
"roomCount": 2,
"unitPrice": 440.00,
"plannedCost": 880.00,
"actualCost": 880.00,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"settleType": "cash",
"sourceType": "HOUSE_ASSIGNMENT",
"sourceTypeName": "配房结果",
"sourceId": "2077233886282088401",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "已核对",
"voucherUrls": []
}
]
}
```
### 8.2 边界成功:PUT 保存手工住宿行
**请求**
```http
PUT /v3/admin/order/2077233855281971202/settlement/step1
Authorization: Bearer {token}
Content-Type: application/json
```
```json
{
"items": [
{
"hotelAssignmentId": null,
"hotelId": null,
"roomTypeId": null,
"stayDate": "2026-07-18",
"hotelName": "临时补充酒店",
"roomType": "STANDARD",
"roomTypeName": "标准间",
"roomCount": 1,
"unitPrice": 300.00,
"plannedCost": 300.00,
"actualCost": 300.00,
"paymentMethod": "COMPANY_PAID",
"sourceType": "MANUAL",
"sourceId": null,
"settlementConfirmStatus": "UNCONFIRMED",
"voucherUrls": [],
"remark": "核单临时补充"
}
]
}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"addedIds": ["2078398065684692001"],
"updatedIds": [],
"deletedIds": [],
"totalActualCost": "300.00"
}
}
```
### 8.3 业务失败:缺付款方式
**请求**
```http
PUT /v3/admin/order/2077233855281971202/settlement/step1
Authorization: Bearer {token}
Content-Type: application/json
```
```json
{
"items": [
{
"hotelAssignmentId": null,
"stayDate": "2026-07-18",
"hotelName": "临时补充酒店",
"roomType": "STANDARD",
"roomCount": 1,
"plannedCost": 300.00,
"actualCost": 300.00
}
]
}
```
**响应**
```json
{
"code": 584062,
"message": "临时行(无配房关联)必须指定 paymentMethod",
"data": null,
"success": false
}
```
## 9. 业务边界
- `PUT` 是全量替换保存;前端保存时应提交页面当前完整明细列表。
- GET 返回的后台配房自动行,PUT 保存时应原样回传 `sourceType=HOUSE_ASSIGNMENT`、`sourceId` 和 `hotelAssignmentId`,两个 ID 都是对应配房记录 ID。
- 前端手动新增行,PUT 保存时传 `sourceType=MANUAL`、`sourceId=null`、`hotelAssignmentId=null`。
- `sourceType` 不传时,`hotelAssignmentId` 有值默认 `HOUSE_ASSIGNMENT`,否则默认 `MANUAL`。
- `sourceType` 只区分数据来源,不决定可编辑性;可编辑性继续由 `settlementConfirmStatus` 控制。
- `sourceId` 不传且 `sourceType=HOUSE_ASSIGNMENT` 时,默认使用 `hotelAssignmentId`;其他来源可为 `null`。
- `unitPrice` 不传且 `roomCount > 0`、`actualCost` 有值时,返回时会按 `actualCost / roomCount` 保留 2 位小数。
- `settlementConfirmStatus` 不传时默认 `CONFIRMED`。
- `paymentMethod` 与 `settleType` 二选一;`paymentMethod` 优先,`settleType` 会映射成 `paymentMethod`。
- 订单核单状态必须是「待核单」或「核单中」才允许保存 Step1。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 修改前 | 修改后 |
|------|--------|--------|
| `hotelId` | 无 | 新增,酒店资源 ID |
| `roomTypeId` | 无 | 新增,房型资源 ID |
| `roomTypeName` | 无 | 新增,房型/规格名称 |
| `unitPrice` | 无 | 新增,核算单价 |
| `paymentMethodName` | 无 | 新增,付款方式中文名 |
| `sourceType` | 无 | 新增,来源类型 |
| `sourceTypeName` | 无 | 新增,来源类型中文名 |
| `sourceId` | 无 | 新增,来源业务 ID |
| `settlementConfirmStatus` | 无 | 新增,核单确认状态 |
| `settlementConfirmStatusName` | 无 | 新增,核单确认状态中文名 |
### 10.2 行为级对比
| 行为 | 修改前 | 修改后 |
|------|--------|--------|
| 住宿来源展示 | 只能通过 `hotelAssignmentId` 粗略判断 | 返回 `sourceType/sourceTypeName/sourceId` |
| 单价展示 | 前端只能根据总价和间数自行推算 | 返回 `unitPrice`,缺失时后端按实际成本和间数降级计算 |
| 确认状态展示 | 无独立字段 | 返回 `settlementConfirmStatus/settlementConfirmStatusName` |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:否。新增字段为兼容性新增,旧字段保留。
- **前端是否必须同步上线**:否。旧页面可继续按原字段展示;需要原型新增列时读取新增字段。
- **影响已有数据**:历史行新增字段可能为 `null`,前端需要保留空值展示逻辑。
### 11.2 回滚方案
- 回滚接口代码后,前端不要再依赖本次新增字段。
- 如果页面已使用新增列,回滚期间新增列需要降级为空态展示。
## 12. 注意事项
- `paymentMethodName`、`sourceTypeName`、`settlementConfirmStatusName` 都是展示字段,保存时可不传。
- `roomType` 是旧字段,`roomTypeName` 是本次新增的房型/规格名称;两者可能同时存在。
- `settlementConfirmStatus` 是核单明细确认状态,与房务配房确认状态不是同一个字段。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5043](https://git.1814.love:8443/wx/HL/issues/5043)
- **PR**: [#5047](https://git.1814.love:8443/wx/HL/pulls/5047)
- **Merge commit**: [0a9e83b39](https://git.1814.love:8443/wx/HL/commit/0a9e83b390d135c05b43bc18afe1b190329bf85c)
- **对接补充 Issue**: [#5157](https://git.1814.love:8443/wx/HL/issues/5157)
- **对接补充 PR**: [#5162](https://git.1814.love:8443/wx/HL/pulls/5162)
- **对接补充 Merge commit**: [bc5669fd5](https://git.1814.love:8443/wx/HL/commit/bc5669fd5)
### 13.2 联系人
- **后端负责人**: @yaosutu
@@ -0,0 +1,306 @@
# 【修改接口·管理后台】核单 Step2 景区游玩项目字段 (#5049)
> **PR**: #5051 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-18 16:40
## 1. 接口背景
核单 Step2 的景区/游玩项目核算明细需要展示来源、行程天数、规格/票型、销售单价、销售小计和付款方式中文名。现有接口保留原路径,在原 `GET/PUT /v3/admin/order/{orderId}/settlement/step2` 上做兼容增强。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查询门票/游玩项目核算明细 | GET | `/v3/admin/order/{orderId}/settlement/step2` | 修改接口 | `TicketItemVO` 出参新增 6 个字段 |
| 2 | 保存门票/游玩项目核算明细 | PUT | `/v3/admin/order/{orderId}/settlement/step2` | 修改接口 | `sourceType` 新增 `CUSTOM_ASSIGNMENT`,入参支持保存规格、销售单价、销售小计 |
## 3. 接口详情
### 3.1 查询门票/游玩项目核算明细
- **使用场景**:进入核单 Step2 景区/游玩项目页签时查询明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:幂等,只读查询。
- **响应结构**:`data` 为 `TicketItemVO[]` 数组。
### 3.2 保存门票/游玩项目核算明细
- **使用场景**:保存核单 Step2 景区/游玩项目核算明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:全量替换保存;同一份 `items` 重复提交后,以最后一次提交为准。
- **请求体兼容**:推荐使用 `{ "items": [...] }`;历史数组 body `[...]` 仍兼容。
## 4. 接口入参
### 4.1 路径参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | string | 是 | 订单 ID,长整型字符串 |
### 4.2 PUT 请求体字段
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `items` | array | 是 | 门票/游玩项目明细行数组,全量替换保存 | 不允许为 `null` |
| `items[].id` | string | 否 | 已存在行 ID;全量替换保存时可不传 | 长整型字符串 |
| `items[].sourceType` | string | 是 | 来源类型 | `SCENIC_ASSIGNMENT` / `ACTIVITY_ASSIGNMENT` / `CUSTOM_ASSIGNMENT` |
| `items[].sourceTypeName` | string | 否 | 来源类型中文名,仅展示字段;保存时可不传 | 最大 32 字符 |
| `items[].scenicAssignmentId` | string | 否 | 来源 assignment ID;手工项目传 `null` | 长整型字符串或 `null` |
| `items[].dayNumber` | integer | 否 | 行程第几天;保存时以后端根据 `dayDate` 计算后的值为准 | 从 1 开始 |
| `items[].dayDate` | string | 是 | 行程日期 | `yyyy-MM-dd`,不能早于订单出发日 |
| `items[].scenicName` | string | 是 | 景区/游玩项目名称 | 1-200 字符 |
| `items[].specName` | string | 否 | 规格/票型名称 | 最大 128 字符 |
| `items[].ticketCount` | integer | 是 | 实际购票数量 | 建议非负整数 |
| `items[].ticketUnitPrice` | number | 否 | 参考成本单价,单位元 | 小数 |
| `items[].sellPrice` | number | 否 | 客户成交单价,单位元 | `>= 0` |
| `items[].totalAmount` | number | 否 | 客户成交小计,单位元;为空且有 `sellPrice` 时后端按 `sellPrice * ticketCount` 降级计算 | `>= 0` |
| `items[].plannedCost` | number | 是 | 计划成本,单位元 | `>= 0` |
| `items[].actualCost` | number | 是 | 实际成本,单位元 | `>= 0` |
| `items[].paymentMethod` | string | 否 | 付款方式;为空时默认 `COMPANY_PAID` | `SIGNED` / `COMPANY_PAID` / `CASH_PAID` |
| `items[].paymentMethodName` | string | 否 | 付款方式中文名,仅展示字段;保存时可不传 | 最大 32 字符 |
| `items[].voucherUrls` | array | 否 | 凭证图片 URL 数组 | 字符串数组 |
| `items[].remark` | string | 否 | 备注 | 最大 500 字符 |
## 5. 出参
### 5.1 GET 响应字段:`TicketItemVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 核单明细行 ID |
| `sourceType` | string | 来源类型 |
| `sourceTypeName` | string | 来源类型中文名;本次新增 |
| `scenicAssignmentId` | string/null | 来源 assignment ID;手工项目为 `null` |
| `dayNumber` | integer/null | 行程第几天;本次新增 |
| `dayDate` | string | 行程日期,`yyyy-MM-dd` |
| `scenicName` | string | 景区/游玩项目名称 |
| `specName` | string/null | 规格/票型名称;本次新增 |
| `ticketCount` | integer | 实际购票数量 |
| `ticketUnitPrice` | number/null | 参考成本单价 |
| `sellPrice` | number/null | 客户成交单价;本次新增 |
| `totalAmount` | number/null | 客户成交小计;本次新增 |
| `plannedCost` | number | 计划成本 |
| `actualCost` | number | 实际成本 |
| `paymentMethod` | string | 付款方式 |
| `paymentMethodName` | string/null | 付款方式中文名;本次新增 |
| `voucherUrls` | array | 凭证图片 URL 数组 |
| `remark` | string/null | 备注 |
### 5.2 PUT 响应字段:`SettlementTicketSaveRespVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `addedIds` | string[] | 本次保存新增的核单明细行 ID 列表 |
| `updatedIds` | string[] | 本次保存更新的核单明细行 ID 列表;当前全量替换语义下通常为空数组 |
| `deletedIds` | string[] | 本次保存删除的核单明细行 ID 列表;当前返回通常为空数组 |
| `totalActualCost` | string | 保存后 Step2 实际成本合计,单位元 |
## 6. 枚举 / 数据字典
### 6.1 `sourceType`
**所属字段**:`items[].sourceType`、`data[].sourceType` | **类型**:String | **必填**:是
| 值 | 中文 | 说明 |
|----|------|------|
| `SCENIC_ASSIGNMENT` | 景区 | 景区来源行 |
| `ACTIVITY_ASSIGNMENT` | 游玩项目 | 游玩项目来源行 |
| `CUSTOM_ASSIGNMENT` | 手工项目 | 本次新增;核单手工补充行,`scenicAssignmentId` 可为 `null` |
### 6.2 `paymentMethod`
**所属字段**:`items[].paymentMethod`、`data[].paymentMethod` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `SIGNED` | 签单 | 现场签单 |
| `COMPANY_PAID` | 公司付款 | 公司统一付款;未传 `paymentMethod` 时默认该值 |
| `CASH_PAID` | 现付 | 现场现金/线下现付 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `584011` | 当前核单状态不允许录门票核单 | PUT 保存时,订单不是「待核单」或「核单中」 |
| `100001` | 参数非法 | 字段格式不符合校验,例如 `sourceType` 不在允许枚举内、金额小于 0 |
## 8. 示例
### 8.1 典型成功:GET 查询
**请求**
```http
GET /v3/admin/order/2077233855281971202/settlement/step2
Authorization: Bearer {token}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": [
{
"id": "2077328317421187073",
"sourceType": "SCENIC_ASSIGNMENT",
"sourceTypeName": "景区",
"scenicAssignmentId": "2077233886282088450",
"dayNumber": 5,
"dayDate": "2026-07-18",
"scenicName": "呼和诺尔草原旅游区",
"specName": null,
"ticketCount": 1,
"ticketUnitPrice": 59.00,
"sellPrice": null,
"totalAmount": null,
"plannedCost": 59.00,
"actualCost": 59.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": [],
"remark": null
}
]
}
```
### 8.2 边界成功:PUT 保存手工项目
**请求**
```http
PUT /v3/admin/order/2077233855281971202/settlement/step2
Authorization: Bearer {token}
Content-Type: application/json
```
```json
{
"items": [
{
"sourceType": "CUSTOM_ASSIGNMENT",
"scenicAssignmentId": null,
"dayDate": "2026-07-18",
"scenicName": "临时补充游玩项目",
"specName": "成人票",
"ticketCount": 2,
"ticketUnitPrice": 12.34,
"sellPrice": 56.78,
"totalAmount": 113.56,
"plannedCost": 24.68,
"actualCost": 24.68,
"paymentMethod": "COMPANY_PAID",
"voucherUrls": [],
"remark": "核单临时补充"
}
]
}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"addedIds": ["2078398065684692994"],
"updatedIds": [],
"deletedIds": [],
"totalActualCost": "24.68"
}
}
```
### 8.3 业务失败:已核单订单禁止保存
**请求**
```http
PUT /v3/admin/order/2077233886248534018/settlement/step2
Authorization: Bearer {token}
Content-Type: application/json
```
```json
{
"items": []
}
```
**响应**
```json
{
"code": 584011,
"message": "当前核单状态为「已核单」,不允许录门票核单,必须为「待核单」或「核单中」",
"data": null,
"success": false
}
```
## 9. 业务边界
- `PUT` 是全量替换保存;前端保存时应提交页面当前完整明细列表。
- `CUSTOM_ASSIGNMENT` 表示核单手工补充项目,`scenicAssignmentId` 可以为 `null`。
- `dayNumber` 保存时以后端根据 `dayDate` 和订单出发日计算的结果为准。
- `totalAmount` 为空且 `sellPrice` 有值时,后端会按 `sellPrice * ticketCount` 降级计算。
- 未传 `paymentMethod` 时,后端默认使用 `COMPANY_PAID`。
- 订单核单状态必须是「待核单」或「核单中」才允许保存 Step2。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 修改前 | 修改后 |
|------|--------|--------|
| `sourceType` | `SCENIC_ASSIGNMENT` / `ACTIVITY_ASSIGNMENT` | 新增 `CUSTOM_ASSIGNMENT` |
| `sourceTypeName` | 无 | 新增,返回来源中文名 |
| `dayNumber` | 无 | 新增,返回行程第几天 |
| `specName` | 无 | 新增,返回/保存规格或票型名称 |
| `sellPrice` | 无 | 新增,返回/保存客户成交单价 |
| `totalAmount` | 无 | 新增,返回/保存客户成交小计 |
| `paymentMethodName` | 无 | 新增,返回付款方式中文名 |
### 10.2 行为级对比
| 行为 | 修改前 | 修改后 |
|------|--------|--------|
| 手工补充项目来源 | 只能用既有来源类型兜底表达 | 可明确传 `CUSTOM_ASSIGNMENT` |
| 手工项目 assignment ID | 前端容易误以为必须有来源 ID | `CUSTOM_ASSIGNMENT` 下 `scenicAssignmentId` 可为 `null` |
| 销售金额展示 | 只能展示成本字段 | 可展示 `sellPrice` / `totalAmount` |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:否。新增字段为兼容性新增;旧字段继续保留。
- **前端是否必须同步上线**:否。旧页面可继续按原字段展示;需要原型新增列时再读取新字段。
- **影响已有数据**:不需要前端做数据迁移;历史行新字段可能为 `null`。
### 11.2 回滚方案
- 回滚接口代码后,前端不要再依赖 `CUSTOM_ASSIGNMENT` 和新增字段。
- 如已保存手工项目,回滚前应确认旧版本是否能识别该来源类型。
## 12. 注意事项
- 前端不要把 `sourceTypeName`、`paymentMethodName` 当作提交必填项;它们是展示字段。
- 前端保存时建议保留并回传用户编辑后的 `specName`、`sellPrice`、`totalAmount`。
- 已核单订单保存 Step2 会返回 `584011`,这不是接口异常。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5049](https://git.1814.love:8443/wx/HL/issues/5049)
- **PR**: [#5051](https://git.1814.love:8443/wx/HL/pulls/5051)
- **Merge commit**: [d60324fde](https://git.1814.love:8443/wx/HL/commit/d60324fde228b50c7ba70d1f39a641e851dc351d)
### 13.2 联系人
- **后端负责人**: @yst
@@ -0,0 +1,752 @@
# 【新增/修改接口·管理后台】核团核算列表与详情聚合 (#5055)
> **PR**: [#5058](https://git.1814.love:8443/wx/HL/pulls/5058) | **服务**: `hl-order-service-v3` | **更新时间**: 2026-07-18 18:20
## 1. 接口背景
核团核算页面需要一个常规产品订单入口列表,并且详情页需要一次性拿到订单信息、出行人、司机车辆、应收构成、线上支付和线下收款记录。此前详情接口字段不完整,前端需要自行合并多个接口;本次把核团入口和详情聚合契约收敛到订单服务接口。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 核团核算任务列表 | GET | `/v3/admin/order-settlement/tasks` | 新增接口 | 查询常规 CORE 产品、无团期批次、已完成订单的核团任务列表 |
| 2 | 查询核团详情 | GET | `/v3/admin/order/{orderId}/settlement/return-detail` | 修改接口 | 扩展订单信息、出行人中文枚举、司机车辆集合、应收汇总/明细、支付+线下收款合并记录 |
## 3. 接口详情
### 3.1 核团核算任务列表
- **使用场景**: 核团核算页的常规产品列表。
- **认证**: 需要管理后台 JWT。
- **幂等性**: 是,只读查询。
- **请求体**: 无。
- **响应结构**: `Result<PageResult<SettlementTaskRespVO>>`。
#### 3.1.1 Query 入参
| 字段 | 类型 | 必填 | 默认值 | 校验规则 | 说明 |
|---|---|---|---|---|---|
| `page` | number | 否 | `1` | 最小 `1` | 当前页码 |
| `pageSize` | number | 否 | `20` | `1` 到 `100` | 每页条数 |
| `keyword` | string | 否 | - | - | 关键词,按订单号、团号、产品名模糊查询 |
| `departureDateFrom` | string | 否 | - | `yyyy-MM-dd` | 出发日期开始 |
| `departureDateTo` | string | 否 | - | `yyyy-MM-dd` | 出发日期结束 |
| `settlementStatus` | string | 否 | - | `NONE` / `PENDING` / `COMPLETED` | 核算状态筛选 |
#### 3.1.2 响应字段
顶层统一响应:
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | number | 成功为 `200` |
| `message` | string | 成功为 `成功` |
| `data` | object | 分页数据 |
| `traceId` | string/null | 链路追踪 ID |
| `success` | boolean | 是否成功 |
`data` 分页字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `records` | array | 核团任务行列表,空结果返回 `[]` |
| `total` | number | 总记录数 |
| `page` | number | 当前页码 |
| `pageSize` | number | 每页条数 |
`records[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderId` | string | 订单 ID |
| `orderNo` | string | 订单号 |
| `teamNo` | string/null | 团号 |
| `productName` | string | 产品名称 |
| `departureDate` | string/null | 出发日期,格式 `yyyy-MM-dd` |
| `returnDate` | string/null | 返团日期,格式 `yyyy-MM-dd` |
| `peopleCount` | number | 出行人总数 |
| `peopleSummary` | string | 人数文案,如 `2成人2儿童`、`2成人1婴儿`、`0人` |
| `systemBalanceAmount` | number | 系统计算待收尾款 |
| `settlementStatus` | string | 核算状态 |
| `settlementStatusName` | string | 核算状态中文名 |
列表不返回 `routeName`、`driverName`、`vehiclePlateNo`。
#### 3.1.3 示例
典型成功:
**请求**
```http
GET /v3/admin/order-settlement/tasks?page=1&pageSize=10&settlementStatus=COMPLETED
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"orderId": "2077233886248534018",
"orderNo": "HL202607180001",
"teamNo": "T20260718001",
"productName": "呼伦贝尔草原 5 日游",
"departureDate": "2026-07-20",
"returnDate": "2026-07-24",
"peopleCount": 3,
"peopleSummary": "2成人1婴儿",
"systemBalanceAmount": 0.00,
"settlementStatus": "COMPLETED",
"settlementStatusName": "已结算"
}
],
"total": 12,
"page": 1,
"pageSize": 10
},
"traceId": null,
"success": true
}
```
空结果:
**请求**
```http
GET /v3/admin/order-settlement/tasks?page=1&pageSize=10&keyword=不存在的订单
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 10
},
"traceId": null,
"success": true
}
```
### 3.2 查询核团详情
- **使用场景**: 点击核团任务后进入返团核算详情页。
- **认证**: 需要管理后台 JWT。
- **幂等性**: 是,只读查询。
- **请求体**: 无。
- **响应结构**: `Result<SettlementReturnDetailRespVO>`。
#### 3.2.1 路径入参
| 字段 | 类型 | 必填 | 校验规则 | 说明 |
|---|---|---|---|---|
| `orderId` | string | 是 | 长整型字符串,必须大于 `0` | 订单 ID |
#### 3.2.2 响应字段
`data` 顶层字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderInfo` | object/null | 订单信息;取消订单可能为空 |
| `travelers` | array | 出行人列表,敏感字段已脱敏 |
| `driverVehicles` | array | 当前有效司机车辆连续服务区间;无有效派车返回 `[]` |
| `receivableSummary` | object/null | 应收汇总 |
| `receivableItems` | array | 应收计算明细 |
| `collectionSummary` | object/null | 收款汇总 |
| `collectionRecords` | array | 线上支付和线下收款合并记录 |
`orderInfo` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderId` | string | 订单 ID |
| `orderNo` | string | 订单号 |
| `teamNo` | string/null | 团号 |
| `productName` | string | 产品名称 |
| `departureDate` | string/null | 出发日期,格式 `yyyy-MM-dd` |
| `returnDate` | string/null | 返团日期,格式 `yyyy-MM-dd` |
| `peopleCount` | number | 出行人总数 |
| `adultCount` | number | 成人数 |
| `childCount` | number | 儿童数 |
| `youngChildCount` | number | 幼童数 |
| `babyCount` | number | 婴儿数 |
| `peopleSummary` | string | 人数文案 |
| `consultantId` | string/null | 定制师 ID |
| `consultantName` | string/null | 定制师姓名 |
| `houseStaffId` | string/null | 房务人员 ID |
| `houseStaffName` | string/null | 房务人员姓名 |
| `fleetStaffId` | string/null | 车务人员 ID |
| `fleetStaffName` | string/null | 车务人员姓名 |
| `settlementStatus` | string | 核算状态 |
| `settlementStatusName` | string | 核算状态中文名 |
`travelers[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `travelerId` | string | 出行人 ID |
| `travelerName` | string | 出行人姓名 |
| `travelerType` | string | 出行人类型 |
| `travelerTypeName` | string | 出行人类型中文名 |
| `idType` | string/null | 证件类型 |
| `idTypeName` | string/null | 证件类型中文名 |
| `phone` | string/null | 脱敏手机号 |
| `idCardNo` | string/null | 脱敏证件号 |
`driverVehicles[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `driverId` | string/null | 司机 ID |
| `driverName` | string/null | 司机姓名 |
| `driverPhone` | string/null | 脱敏司机手机号 |
| `vehicleId` | string/null | 车辆 ID |
| `vehiclePlateNo` | string/null | 车牌号 |
| `vehicleModelName` | string/null | 车型名称 |
| `seatCount` | number/null | 座位数 |
| `startDate` | string | 连续服务开始日期,格式 `yyyy-MM-dd` |
| `endDate` | string | 连续服务结束日期,格式 `yyyy-MM-dd` |
`receivableSummary` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderAmount` | number | 订单基础金额 |
| `surchargeAmount` | number | 附加费金额 |
| `discountAmount` | number | 优惠金额 |
| `payableAmount` | number | 应收总额 |
| `formulaText` | string | 应收总额公式文案,固定为 `订单金额 + 附加费 - 优惠 = 应收总额` |
`receivableItems[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `sourceRecordId` | string/null | 来源记录 ID |
| `itemType` | string | 应收项类型 |
| `itemTypeName` | string | 应收项类型中文名 |
| `itemCode` | string/null | 应收项编码 |
| `itemName` | string/null | 应收项名称 |
| `direction` | string | 方向,`ADD` 增加应收,`DEDUCT` 减少应收 |
| `amount` | number | 金额 |
`collectionSummary` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `payableAmount` | number | 应收总额 |
| `paidAmount` | number | 累计已收 |
| `refundedAmount` | number | 累计已退 |
| `netPaidAmount` | number | 净已收,等于已收减已退 |
| `balanceAmount` | number | 待收尾款 |
| `depositPaidAmount` | number | 已收订金 |
| `balancePaidAmount` | number | 已收尾款 |
| `fullPaidAmount` | number | 已收全款 |
`collectionRecords[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `recordId` | string | 收款记录 ID |
| `recordType` | string | 记录类型,线上支付或线下收款 |
| `recordTypeName` | string | 记录类型中文名 |
| `payType` | string/null | 款项类型 |
| `payTypeName` | string/null | 款项类型中文名 |
| `channel` | string/null | 支付或收款渠道 |
| `channelName` | string/null | 支付或收款渠道中文名 |
| `receiptMethod` | string/null | 线下收款方式;在线支付和对公转账可为空 |
| `receiptMethodName` | string/null | 线下收款方式中文名 |
| `amount` | number | 金额 |
| `collectedAt` | string/null | 收款时间,格式 `yyyy-MM-dd HH:mm:ss` 或 ISO 时间字符串 |
| `status` | string/null | 收款记录状态 |
| `statusName` | string/null | 收款记录状态中文名 |
| `collectorName` | string/null | 代收人姓名 |
| `operatorName` | string/null | 登记人姓名 |
| `thirdPartyNoMasked` | string/null | 脱敏第三方交易号 |
| `transferRef` | string/null | 转账流水号 |
| `voucherUrls` | string[]/null | 凭证图片 URL |
| `remark` | string/null | 备注 |
| `includedInPaidAmount` | boolean | 是否计入已收金额 |
详情接口不返回 `needsVehicle`、`vehicleControlStatus`、`vehicleControlStatusName`。
#### 3.2.3 示例
典型成功:
**请求**
```http
GET /v3/admin/order/2077233886248534018/settlement/return-detail
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderInfo": {
"orderId": "2077233886248534018",
"orderNo": "HL202607180001",
"teamNo": "T20260718001",
"productName": "呼伦贝尔草原 5 日游",
"departureDate": "2026-07-20",
"returnDate": "2026-07-24",
"peopleCount": 3,
"adultCount": 2,
"childCount": 0,
"youngChildCount": 0,
"babyCount": 1,
"peopleSummary": "2成人1婴儿",
"consultantId": "1001",
"consultantName": "admin",
"houseStaffId": "20001",
"houseStaffName": "房务A",
"fleetStaffId": "30001",
"fleetStaffName": "车务A",
"settlementStatus": "COMPLETED",
"settlementStatusName": "已结算"
},
"travelers": [
{
"travelerId": "2077233886248535001",
"travelerName": "张三",
"travelerType": "ADULT",
"travelerTypeName": "成人",
"idType": "ID_CARD",
"idTypeName": "身份证",
"phone": "138****0000",
"idCardNo": "150***********1234"
}
],
"driverVehicles": [
{
"driverId": "2078304714008522754",
"driverName": "李师傅",
"driverPhone": "176****3787",
"vehicleId": "2065329514644152321",
"vehiclePlateNo": "蒙C01E01",
"vehicleModelName": "丰田埃尔法",
"seatCount": 7,
"startDate": "2026-07-20",
"endDate": "2026-07-24"
}
],
"receivableSummary": {
"orderAmount": 6000.00,
"surchargeAmount": 200.00,
"discountAmount": 90.00,
"payableAmount": 6110.00,
"formulaText": "订单金额 + 附加费 - 优惠 = 应收总额"
},
"receivableItems": [
{
"sourceRecordId": "2077233886248534018",
"itemType": "BASE_ORDER",
"itemTypeName": "订单基础应收",
"itemCode": "ORDER_AMOUNT",
"itemName": "呼伦贝尔草原 5 日游",
"direction": "ADD",
"amount": 6000.00
},
{
"sourceRecordId": "2077233886248536001",
"itemType": "SURCHARGE",
"itemTypeName": "附加费",
"itemCode": "SINGLE_ROOM",
"itemName": "单房差",
"direction": "ADD",
"amount": 200.00
},
{
"sourceRecordId": "2077233886248537001",
"itemType": "DISCOUNT",
"itemTypeName": "优惠",
"itemCode": "PROMOTION",
"itemName": "活动优惠",
"direction": "DEDUCT",
"amount": 90.00
}
],
"collectionSummary": {
"payableAmount": 6110.00,
"paidAmount": 6110.00,
"refundedAmount": 0.00,
"netPaidAmount": 6110.00,
"balanceAmount": 0.00,
"depositPaidAmount": 1000.00,
"balancePaidAmount": 5110.00,
"fullPaidAmount": 0.00
},
"collectionRecords": [
{
"recordId": "2077233886248538001",
"recordType": "ONLINE_PAYMENT",
"recordTypeName": "在线支付",
"payType": "DEPOSIT",
"payTypeName": "订金",
"channel": "WECHAT",
"channelName": "微信支付",
"receiptMethod": null,
"receiptMethodName": null,
"amount": 1000.00,
"collectedAt": "2026-07-10 10:30:00",
"status": "SUCCEEDED",
"statusName": "支付成功",
"collectorName": null,
"operatorName": null,
"thirdPartyNoMasked": "4200****0001",
"transferRef": null,
"voucherUrls": null,
"remark": null,
"includedInPaidAmount": true
},
{
"recordId": "2077233886248539001",
"recordType": "MANUAL_RECEIPT",
"recordTypeName": "线下收款",
"payType": "BALANCE",
"payTypeName": "尾款",
"channel": "DRIVER_CASH",
"channelName": "报账人收款",
"receiptMethod": "WECHAT_TRANSFER",
"receiptMethodName": "微信转账",
"amount": 5110.00,
"collectedAt": "2026-07-20 18:30:00",
"status": "CONFIRMED",
"statusName": "已确认",
"collectorName": "李师傅",
"operatorName": "admin",
"thirdPartyNoMasked": null,
"transferRef": null,
"voucherUrls": [
"https://oss.example.com/receipt/a.jpg"
],
"remark": "现场收尾款",
"includedInPaidAmount": true
}
]
},
"traceId": null,
"success": true
}
```
边界成功:无司机车辆、无收款记录。
**请求**
```http
GET /v3/admin/order/2077233886248534019/settlement/return-detail
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderInfo": {
"orderId": "2077233886248534019",
"orderNo": "HL202607180002",
"teamNo": null,
"productName": "呼伦贝尔草原 5 日游",
"departureDate": "2026-07-20",
"returnDate": "2026-07-24",
"peopleCount": 1,
"adultCount": 1,
"childCount": 0,
"youngChildCount": 0,
"babyCount": 0,
"peopleSummary": "1成人",
"consultantId": "1001",
"consultantName": "admin",
"houseStaffId": null,
"houseStaffName": null,
"fleetStaffId": null,
"fleetStaffName": null,
"settlementStatus": "NONE",
"settlementStatusName": "未结算"
},
"travelers": [],
"driverVehicles": [],
"receivableSummary": {
"orderAmount": 3000.00,
"surchargeAmount": 0.00,
"discountAmount": 0.00,
"payableAmount": 3000.00,
"formulaText": "订单金额 + 附加费 - 优惠 = 应收总额"
},
"receivableItems": [
{
"sourceRecordId": "2077233886248534019",
"itemType": "BASE_ORDER",
"itemTypeName": "订单基础应收",
"itemCode": "ORDER_AMOUNT",
"itemName": "呼伦贝尔草原 5 日游",
"direction": "ADD",
"amount": 3000.00
}
],
"collectionSummary": {
"payableAmount": 3000.00,
"paidAmount": 0.00,
"refundedAmount": 0.00,
"netPaidAmount": 0.00,
"balanceAmount": 3000.00,
"depositPaidAmount": 0.00,
"balancePaidAmount": 0.00,
"fullPaidAmount": 0.00
},
"collectionRecords": []
},
"traceId": null,
"success": true
}
```
业务失败:订单不存在。
**请求**
```http
GET /v3/admin/order/999999999999999999/settlement/return-detail
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 581007,
"message": "订单不存在",
"data": null,
"traceId": null,
"success": false
}
```
## 4. 入参汇总
| 接口 | 入参位置 | 字段 |
|---|---|---|
| `GET /v3/admin/order-settlement/tasks` | Query | `page`、`pageSize`、`keyword`、`departureDateFrom`、`departureDateTo`、`settlementStatus` |
| `GET /v3/admin/order/{orderId}/settlement/return-detail` | Path | `orderId` |
## 5. 出参汇总
| 接口 | 出参根结构 | 主要字段 |
|---|---|---|
| `GET /v3/admin/order-settlement/tasks` | `PageResult<SettlementTaskRespVO>` | `records[]`、`total`、`page`、`pageSize` |
| `GET /v3/admin/order/{orderId}/settlement/return-detail` | `SettlementReturnDetailRespVO` | `orderInfo`、`travelers`、`driverVehicles`、`receivableSummary`、`receivableItems`、`collectionSummary`、`collectionRecords` |
## 6. 枚举 / 数据字典
### 6.1 `settlementStatus`
**所属字段**: `settlementStatus`、`records[].settlementStatus`、`orderInfo.settlementStatus` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `NONE` | 未结算 | 初始态,尚未提交核单 |
| `PENDING` | 待财务复核 | 已提交核单,等待财务复核 |
| `COMPLETED` | 已结算 | 财务复核已完成 |
### 6.2 `travelerType`
**所属字段**: `travelers[].travelerType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `ADULT` | 成人 | 成人出行人 |
| `CHILD` | 儿童 | 儿童出行人 |
| `YOUNG_CHILD` | 幼童 | 幼童出行人 |
| `BABY` | 婴儿 | 婴儿出行人 |
### 6.3 `idType`
**所属字段**: `travelers[].idType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `ID_CARD` | 身份证 | 居民身份证 |
| `PASSPORT` | 护照 | 护照 |
| `BIRTH_CERT` | 出生证明 | 出生医学证明 |
### 6.4 `itemType`
**所属字段**: `receivableItems[].itemType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `BASE_ORDER` | 订单基础应收 | 订单基础金额 |
| `SURCHARGE` | 附加费 | 附加费用,增加应收 |
| `DISCOUNT` | 优惠 | 优惠项目,减少应收 |
### 6.5 `direction`
**所属字段**: `receivableItems[].direction` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `ADD` | 增加 | 计入应收增加项 |
| `DEDUCT` | 扣减 | 计入应收扣减项 |
### 6.6 `recordType`
**所属字段**: `collectionRecords[].recordType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `ONLINE_PAYMENT` | 在线支付 | 线上支付流水 |
| `MANUAL_RECEIPT` | 线下收款 | 管理后台登记的线下收款 |
### 6.7 `payType`
**所属字段**: `collectionRecords[].payType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `DEPOSIT` | 订金 | 订金 |
| `FULL` | 全款 | 全款 |
| `BALANCE` | 尾款 | 尾款 |
### 6.8 `channel`
**所属字段**: `collectionRecords[].channel` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `WECHAT` | 微信支付 | 在线微信支付 |
| `ALIPAY` | 支付宝 | 在线支付宝支付 |
| `OFFLINE_TRANSFER` | 线下转账 | 线下转账渠道 |
| `DRIVER_CASH` | 报账人收款 | 报账人代收 |
| `BANK_TRANSFER` | 对公转账 | 对公银行转账 |
| `CONSULTANT_COLLECTION` | 定制师代收 | 定制师代收 |
### 6.9 `receiptMethod`
**所属字段**: `collectionRecords[].receiptMethod` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `WECHAT_TRANSFER` | 微信转账 | 线下微信转账 |
| `CASH` | 现金收款 | 现金收款 |
### 6.10 `collectionRecords[].status`
**所属字段**: `collectionRecords[].status` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `SUCCEEDED` | 支付成功 | 成功在线支付,计入已收 |
| `PENDING` | 待支付 | 在线支付待支付,不计入已收 |
| `CLOSED` | 已关闭 | 在线支付已关闭,不计入已收 |
| `REFUNDED` | 已退款 | 在线支付已退款 |
| `CONFIRMED` | 已确认 | 线下收款已确认,计入已收 |
| `VOIDED` | 已撤销 | 线下收款已撤销,不计入已收 |
## 7. 错误码
| code | 含义 | 触发场景 |
|---|---|---|
| `200` | 成功 | 查询成功 |
| `400` | 参数校验失败 | `page < 1`、`pageSize > 100`、`orderId <= 0`、日期格式非法、`settlementStatus` 非法 |
| `401` | 未认证 | 未携带有效管理后台 JWT |
| `581007` | 订单不存在 | 详情接口查询不存在的订单 |
| `581045` | 房务角色无权查看订单详情,房务仅可配房 | 房务管理员或房务组长访问列表或详情 |
| `584072` | 车务司机车辆信息暂时不可用,请稍后重试 | 详情接口读取司机车辆信息不可用 |
## 8. 示例
示例已按接口放在 `3.1.3` 和 `3.2.3`:列表接口包含典型成功和空结果;详情接口包含典型成功、无司机车辆/无收款记录边界成功、订单不存在业务失败。
## 9. 业务边界
- 列表只包含常规 CORE 产品、无团期批次、已完成订单;团期、小蒙马、导游/摄影团队独立列表不在本接口范围。
- 列表按返团日期倒序、订单 ID 倒序返回。
- 详情接口取消订单返回成功响应,但 `orderInfo`、汇总对象可为空,数组字段为空数组。
- 详情中的手机号、身份证号、第三方交易号、司机手机号均为脱敏值。
- `collectionRecords` 已合并在线支付和线下收款,前端不需要再把支付记录接口与线下收款接口自行合并。
- `receivableSummary.payableAmount` 是应收总额,计算项通过 `receivableItems` 返回。
## 10. 修改前后对比
### 10.1 列表接口
| 项 | 修改前 | 修改后 |
|---|---|---|
| 常规产品核团列表 | 无专用接口 | 新增 `GET /v3/admin/order-settlement/tasks` |
| 人数文案 | 无 | 返回 `peopleSummary` |
| 核算状态中文 | 无 | 返回 `settlementStatusName` |
| 司机/车牌 | 不适用 | 列表不返回司机和车牌字段 |
### 10.2 详情接口
| 字段/结构 | 修改前 | 修改后 |
|---|---|---|
| `orderInfo.peopleSummary` | 无 | 新增 |
| `orderInfo.adultCount/childCount/youngChildCount/babyCount` | 无 | 新增 |
| `orderInfo.consultantId/consultantName` | 无 | 新增 |
| `orderInfo.houseStaffId/houseStaffName` | 无 | 新增 |
| `orderInfo.fleetStaffId/fleetStaffName` | 无 | 新增 |
| `orderInfo.settlementStatusName` | 无 | 新增 |
| `travelers[].travelerTypeName` | 无 | 新增 |
| `travelers[].idTypeName` | 无 | 新增 |
| `driverVehicles` | 无 | 新增司机车辆集合 |
| `receivableSummary` | 不完整 | 新增订单金额、附加费、优惠、应收总额、公式文案 |
| `receivableItems` | 无 | 新增应收计算明细 |
| `collectionSummary` | 不完整 | 新增已收/已退/净已收/待收/订金/尾款/全款汇总 |
| `collectionRecords` | 无 | 新增在线支付+线下收款合并记录 |
| `needsVehicle/vehicleControlStatus/vehicleControlStatusName` | 可能需要前端关注 | 本接口不返回 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**: 否。列表为新增接口;详情为新增出参字段和新增集合结构。
- **前端是否必须同步上线**: 建议同步。核团列表页应改用新增列表接口;详情页可直接使用新增聚合字段,减少前端合并接口逻辑。
- **影响已有数据**: 不需要数据迁移。
### 11.2 回滚方案
- **回滚方式**: 回滚 PR #5058。
- **回滚后前端影响**: 新增列表接口不可用,详情新增字段消失;前端需要回退到原有多接口合并方案或旧页面逻辑。
## 12. 注意事项
- `orderId`、`travelerId`、`driverId`、`vehicleId` 等长整型 ID 均按字符串处理,避免 JS 精度丢失。
- 列表不要展示司机和车牌;司机车辆只在详情的 `driverVehicles` 中展示。
- 详情里的线下收款和在线支付已经按统一记录结构返回,`recordType` 用于区分来源。
- 金额字段单位均为元。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5055](https://git.1814.love:8443/wx/HL/issues/5055)
- **PR**: [#5058](https://git.1814.love:8443/wx/HL/pulls/5058)
- **Merge commit**: [d916901f8](https://git.1814.love:8443/wx/HL/commit/d916901f8)
### 13.2 联系人
- **后端负责人**: @yst
- **需求确认**: @yaosutu
@@ -0,0 +1,255 @@
# 🔧【消费方式纠正·管理后台】调整订单尾款显示纠正(#5116)
> **接口**:`GET /v3/admin/order/{id}/adjustment/snapshot`
> **服务**:`hl-order-service-v3`
> **更新时间**:2026-07-21
> **重要说明**:**后端接口契约、字段和金额计算均未变;本通知仅要求管理后台纠正字段取值,前端必须同步处理。**
## 1. 接口背景
管理后台订单详情与“调整订单”弹窗对同一订单展示了不同的待收尾款。订单存在 150.00 元优惠时:
- 订单详情展示待收尾款 `14350.00`;
- 调整快照实际返回 `data.basic.balanceAmount = "14350.00"`;
- 调整弹窗却展示 `14500.00`。
错误值恰好等于 `16000.00 - 1500.00 = 14500.00`,说明弹窗使用订单基价减已付金额自行计算,遗漏了 `150.00` 优惠。后端快照已经返回包含优惠、附加费、实付及退款口径的最终尾款,前端不应再次计算。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 调整订单预填快照查询 | GET | `/v3/admin/order/{id}/adjustment/snapshot` | 前端消费方式纠正 | 弹窗尾款直接读取 `data.basic.balanceAmount`;后端接口无变更 |
本次没有新增、删除或重命名任何请求字段、响应字段、枚举值或错误码。
## 3. 接口详情
### 3.1 调整订单预填快照查询
- **使用场景**:打开管理后台“调整订单”弹窗时,获取当前订单的基础金额及所选子领域快照。
- **认证**:管理后台登录态。
- **幂等性**:是;只读查询。
- **限流**:无本接口专属限流约定。
- **尾款取值**:直接读取 `data.basic.balanceAmount`。
- **禁止用法**:不要使用 `orderAmount - paidAmount`、`订单总额 - 已付订金`等公式自行计算尾款。
`snapshot` 响应中没有供前端重算尾款使用的 `paidAmount` 字段;`balanceAmount` 已是后端统一金额口径下的最终结果。
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 位置 | 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|:---:|------|----------|
| Path | `id` | Long | 是 | 订单 ID | 必须是存在且当前账号可访问的订单 ID;前端按字符串传递,避免 JavaScript 大整数精度丢失 |
| Query | `scope` | String | 否 | 限定返回子领域;多个值用英文逗号分隔 | 不传返回全部子领域;合法值见第 6 节 |
### 4.2 请求体
GET 请求无请求体。本次请求参数没有变化。
## 5. 出参(响应)
### 5.1 响应包装
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码;`200` 表示成功 |
| `message` | String | 结果说明;失败时为错误信息 |
| `data` | Object / null | 成功时为调整快照;失败时为 `null` |
| `data.basic` | Object | 订单基础信息;无论 `scope` 取何合法值均返回 |
### 5.2 `data.basic` 本问题涉及的金额字段
| 字段 | JSON 类型 | 语义 | 示例值 |
|------|-----------|------|--------|
| `orderAmount` | String | 订单基价,不等同于优惠后的应收金额 | `"16000.00"` |
| `surchargeAmount` | String | 已有附加费合计 | `"0.00"` |
| `discountAmount` | String | 已有优惠合计 | `"150.00"` |
| `receivableAmount` | String | 应收总额,口径为 `max(0, orderAmount + surchargeAmount - discountAmount)` | `"15850.00"` |
| `balanceAmount` | String | 待收尾款;已综合应收、净已付和退款口径,前端直接展示 | `"14350.00"` |
金额字段均为十进制金额字符串。前端可按金额组件的统一规则格式化显示,但不得从其他字段重新推导 `balanceAmount`。
本问题订单的金额核对:
```text
应收金额 = 16000.00 + 0.00 - 150.00 = 15850.00
待收尾款 = 15850.00 - 1500.00 = 14350.00
错误展示 = 16000.00 - 1500.00 = 14500.00(漏减优惠 150.00)
```
## 6. 枚举 / 数据字典
### 6.1 `scope`(调整快照子领域)
**所属字段**:Query 参数 `scope`|**类型**:String|**必填**:否|**本次变化**:无
| 值 | 中文 | 说明 |
|----|------|------|
| `BASIC` | 基础信息 | 仅请求基础视图;`basic` 本身始终返回 |
| `PEOPLE` | 出行人 | 返回出行人子领域,同时返回 `basic` |
| `SCHEDULE` | 改期 | 返回日期/天数子领域,同时返回 `basic` |
| `ITINERARY` | 行程 | 返回行程子领域,同时返回 `basic` |
| `HOTEL_REQ` | 住宿需求 | 返回住宿需求子领域,同时返回 `basic` |
| `VEHICLE_REQ` | 用车需求 | 返回用车需求子领域,同时返回 `basic` |
| `FEE` | 费用兼容值 | 不返回独立费用列表;金额统一读取 `basic` |
多个子领域可用英文逗号连接,例如 `PEOPLE,SCHEDULE`。尾款展示只依赖始终返回的 `basic.balanceAmount`,无需为了尾款额外指定 `scope`。
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 快照查询成功 |
| `581007` | 订单不存在 | `id` 对应订单不存在 |
| `587003` | scope 枚举值非法 | `scope` 中任一值不在第 6 节合法值范围内 |
本次未新增或修改错误码。登录失效、无访问权限等通用网关错误沿用管理后台现有统一处理。
## 8. 示例(典型 / 边界 / 异常)
### 8.1 典型成功:订单存在优惠
**请求**:
```http
GET /v3/admin/order/2079454953641836546/adjustment/snapshot?scope=BASIC
Authorization: Bearer <管理后台登录凭证>
无请求体
```
**响应**:
```json
{
"code": 200,
"message": "success",
"data": {
"basic": {
"orderAmount": "16000.00",
"surchargeAmount": "0.00",
"discountAmount": "150.00",
"receivableAmount": "15850.00",
"balanceAmount": "14350.00"
}
}
}
```
前端底部“尾款”应展示 `14350.00`,取值路径为 `data.basic.balanceAmount`。
### 8.2 边界情况:无优惠、无附加费
**场景说明**:优惠和附加费均为 0 时,错误公式可能碰巧得到相同结果,仍必须读取 `balanceAmount`,不可据此保留自行计算逻辑。
**请求**:
```http
GET /v3/admin/order/2079454953641836546/adjustment/snapshot?scope=BASIC
Authorization: Bearer <管理后台登录凭证>
无请求体
```
**响应示例**:
```json
{
"code": 200,
"message": "success",
"data": {
"basic": {
"orderAmount": "16000.00",
"surchargeAmount": "0.00",
"discountAmount": "0.00",
"receivableAmount": "16000.00",
"balanceAmount": "14500.00"
}
}
}
```
### 8.3 业务失败:非法 scope
**请求**:
```http
GET /v3/admin/order/2079454953641836546/adjustment/snapshot?scope=UNKNOWN
Authorization: Bearer <管理后台登录凭证>
无请求体
```
**响应**:
```json
{
"code": 587003,
"message": "scope 枚举值非法",
"data": null
}
```
## 9. 业务边界
- ✅ `basic` 对所有合法 `scope` 始终返回,尾款统一读取 `data.basic.balanceAmount`。
- ✅ 优惠、附加费、实付和退款等金额因素由后端统一计入口径;前端无需也不应复算。
- ✅ `discountAmount = "0.00"` 时仍按同一路径读取尾款,避免代码按“有无优惠”产生两个分支。
- ✅ 取消订单的 `receivableAmount` 和 `balanceAmount` 为 `"0.00"`,前端按返回值展示。
- ⚠️ 金额是字符串;不得先转为 JavaScript `Number` 后自行进行财务运算。
- ❌ 不要把 `orderAmount` 当成应收金额或待收尾款。
## 10. 修改前后对比
### 10.1 字段级对比
| 项目 | 修改前 | 修改后 |
|------|--------|--------|
| 后端请求字段 | 现有契约 | **不变** |
| 后端响应字段 | 已返回 `basic.balanceAmount` | **不变** |
| 后端枚举 / 错误码 | 现有契约 | **不变** |
| 前端尾款取值 | 疑似用 `orderAmount - paidAmount` 自行计算 | 直接读取 `data.basic.balanceAmount` |
### 10.2 行为级对比
| 场景 | 修改前 | 修改后 |
|------|--------|--------|
| 存在 150.00 元优惠 | 弹窗显示 `14500.00`,比正确金额多 150.00 | 弹窗显示后端返回的 `14350.00` |
| 无优惠 | 可能因错误公式碰巧显示正确 | 始终按统一字段展示 |
| 存在附加费或退款口径 | 自行计算可能继续出现偏差 | 由后端统一口径的 `balanceAmount` 保证一致 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:否;后端契约无变化。
- **前端是否必须同步上线**:是;当前调整弹窗已展示错误尾款。
- **影响范围**:管理后台“调整订单”弹窗底部尾款展示;订单详情页无需调整。
### 11.2 回滚说明
- 本通知没有后端变更,不涉及后端回滚。
- 前端若回滚本次取值纠正,会恢复错误展示,因此不建议回滚;需要紧急处理时应暂时隐藏尾款展示,不应恢复自行计算。
## 12. 注意事项
- 删除或停用弹窗内“订单总额减已付金额”的尾款计算逻辑。
- 尾款唯一取值路径为 `snapshot.data.basic.balanceAmount`;若前端请求封装已解包 `data`,则取 `snapshot.basic.balanceAmount`。
- 不要使用 `orderAmount`、`receivableAmount` 与其他页面缓存的已付金额拼接计算尾款。
- 建议增加至少两条前端回归用例:存在优惠时尾款一致;存在附加费时尾款一致。
- **后端契约未变、后端无需修改;本通知是现存前端消费问题的纠正通知。**
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**:[#5116](https://git.1814.love:8443/wx/HL/issues/5116)
- **后端 PR**:无(后端无需改动)
- **后端 commit**:无(后端无需改动)
### 13.2 联系人
- **负责人**:@yst
@@ -0,0 +1,411 @@
# 📝【契约纠正·管理后台】对公转账不需要代收人 (#5120)
> **变更性质**:现有接口契约澄清 + 历史文档示例纠错|**端类型**:管理后台|**更新日期**:2026-07-21
>
> 本次没有发布新的后端字段、枚举或行为变更;下文说明接口已有的稳定契约。
## 1. 接口背景
管理后台在“登记线下收款”中选择“对公转账”后仍显示“代收人”,与当前接口契约不一致。对公转账不由某位员工代收,只需填写转账流水号;代收人仅在“报账人收款”渠道下需要选择。
2026-07-10 的历史通知曾在示例中给 `BANK_TRANSFER.collectors` 放入“公司账户”对象,该示例与实际响应不符,本次一并纠正为空数组。
## 2. 变更清单
| # | 方法 | 路径 | 通知类型 | 说明 |
|---|---|---|---|---|
| 1 | GET | `/v3/admin/order/{orderId}/payment/manual-receipt/options` | 契约澄清 | `BANK_TRANSFER.collectors` 始终为 `[]`;各渠道使用各自的候选项 |
| 2 | POST | `/v3/admin/order/{orderId}/payment/manual-receipt` | 契约澄清 | `collectorStaffId` 仅对 `DRIVER_CASH` 条件必填;`BANK_TRANSFER` 条件必填 `transferRef` |
| 3 | 文档 | `2026-07/10_4884_线下收款代收人-修改接口-管理后台.md` | 示例纠错 | 将对公转账的错误 `collectors` 对象改为 `[]` |
## 3. 接口详情
### 3.1 查询线下收款选项
- **方法与路径**:`GET /v3/admin/order/{orderId}/payment/manual-receipt/options`
- **使用场景**:打开登记线下收款表单时,查询当前订单可用的渠道、款项类型、代收人和收款方式。
- **认证**:需要管理后台 JWT。
- **幂等性**:幂等,只读查询。
- **限流**:无接口专属限流规则。
### 3.2 登记线下收款
- **方法与路径**:`POST /v3/admin/order/{orderId}/payment/manual-receipt`
- **使用场景**:按 options 当前返回的可用渠道和款项类型登记一笔线下收款。
- **认证**:需要管理后台 JWT。
- **幂等性**:非幂等,每次成功请求会新增一条收款记录。
- **限流**:无接口专属限流规则。
## 4. 接口入参
### 4.1 路径参数(两个接口通用)
| 字段 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `orderId` | Path | String(Long) | 是 | 订单 ID,按字符串处理 |
GET 接口无 Query 参数、无请求体。
### 4.2 POST 请求体
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|---|---|---|---|---|
| `channel` | String | 是 | 收款渠道 | `DRIVER_CASH` / `BANK_TRANSFER` / `CONSULTANT_COLLECTION` |
| `payType` | String | 是 | 款项类型 | 必须取 options 中当前渠道的 `allowedPayTypes` |
| `amount` | Decimal | 是 | 收款金额 | 最小 `0.01`,不能超过当前可收余额 |
| `receivedAt` | String(LocalDateTime) | 否 | 收款时间 | `yyyy-MM-dd'T'HH:mm:ss`;不传默认当前时间 |
| `transferRef` | String | 条件必填 | 对公转账流水号 | `BANK_TRANSFER` 必填,其他渠道不使用 |
| `receiptMethod` | String | 否 | 收款方式 | 取当前渠道 `receiptMethods[].value`;`BANK_TRANSFER` 为空 |
| `collectorStaffId` | String(Long) | 条件必填 | 代收人 assignmentId | **仅 `DRIVER_CASH` 必填**,且必须取当前渠道 `collectors[].collectorId` |
| `collectorType` | String | 否 | 实际代收人类型 | 不传时按 `channel` 推导;如传入,必须与渠道匹配 |
| `voucherUrls` | Array<String> | 否 | 凭证图片 URL 列表 | 可为空数组或不传 |
| `remark` | String | 否 | 备注 | 最长 500 字 |
### 4.3 渠道联动必填矩阵
| `channel` | `collectorStaffId` | `collectorType` | `transferRef` | 代收人规则 |
|---|---|---|---|---|
| `BANK_TRANSFER` | 不需要;误传也不作为员工代收人处理 | 可不传;如传只能为 `COMPANY_ACCOUNT` | **必填** | 不选择任何员工,公司账户是收款归属而非代收人候选项 |
| `CONSULTANT_COLLECTION` | 不需要 | 可不传;如传只能为 `CONSULTANT` | 不需要 | 使用订单定制师,不使用员工选择器 |
| `DRIVER_CASH` | **必填** | 可不传;如传只能为 `ORDER_STAFF` | 不需要 | 仅能选当前订单 options 返回的有效报账人 |
## 5. 出参(响应)
### 5.1 通用响应包装
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | Integer | `200` 表示成功,其他值为业务错误码 |
| `message` | String | 结果或错误说明 |
| `data` | Object/null | 业务数据;失败时通常为 `null` |
| `success` | Boolean | 是否成功 |
### 5.2 GET options 的 `data`
| 字段 | 类型 | 说明 |
|---|---|---|
| `channels` | Array<ChannelOption> | 当前订单的线下收款渠道列表 |
| `channels[].channel` | String | 渠道枚举值 |
| `channels[].channelText` | String | 渠道展示文案 |
| `channels[].allowedPayTypes` | Array<String> | 当前订单状态下该渠道允许的款项类型;以本次响应为准 |
| `channels[].disabled` | Boolean | `true` 表示当前不可提交该渠道 |
| `channels[].disabledReason` | String/null | 禁用原因;可用时为 `null` |
| `channels[].collectors` | Array<CollectorOption> | **该渠道自己的代收人候选列表**;`BANK_TRANSFER` 为 `[]` |
| `channels[].receiptMethods` | Array<OptionItem> | 该渠道可选收款方式;`BANK_TRANSFER` 为 `[]` |
| `collectors[].collectorType` | String | 代收人类型 |
| `collectors[].collectorId` | String(Long) | `ORDER_STAFF` 为 assignmentId,`CONSULTANT` 为管理员 ID |
| `collectors[].collectorName` | String | 代收人姓名 |
| `collectors[].collectorRole` | String | 代收人角色值 |
| `collectors[].collectorRoleText` | String | 代收人角色文案 |
| `collectors[].defaultSelected` | Boolean | 是否默认选中 |
| `receiptMethods[].value` | String | 收款方式值 |
| `receiptMethods[].label` | String | 收款方式文案 |
| `receiptMethods[].defaultSelected` | Boolean | 是否默认选中 |
### 5.3 POST 的 `data`
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | String(Long) | 收款凭据 ID |
| `orderId` | String(Long) | 订单 ID |
| `channel` / `channelLabel` | String | 收款渠道值 / 文案 |
| `payType` / `payTypeLabel` | String | 款项类型值 / 文案 |
| `amount` | Decimal | 本次收款金额 |
| `receivedAt` | String(LocalDateTime) | 收款时间 |
| `collectorStaffId` / `collectorStaffName` | String(Long)/String/null | 仅 `DRIVER_CASH` 有值 |
| `collectorType` | String | 实际代收人类型 |
| `collectorAdminId` | String(Long)/null | `CONSULTANT_COLLECTION` 为定制师管理员 ID |
| `collectorName` / `collectorRole` | String | 代收归属快照名称 / 角色 |
| `transferRef` | String/null | 对公转账流水号,仅 `BANK_TRANSFER` 有值 |
| `receiptMethod` / `receiptMethodLabel` | String/null | 收款方式值 / 文案;`BANK_TRANSFER` 为空 |
| `voucherUrls` | Array<String> | 凭证图片 URL 列表 |
| `remark` | String/null | 备注 |
| `operatorName` | String | 登记人姓名 |
| `createTime` | String(LocalDateTime) | 登记时间 |
| `voided` | Boolean | 是否已撤销;新登记为 `false` |
| `voidedByName` / `voidedAt` / `voidReason` | String/null | 撤销信息;新登记时为 `null` |
| `paidAmountAfter` | Decimal | 登记后订单累计已付金额 |
| `payStatusAfter` | String | 登记后订单支付状态 |
## 6. 枚举 / 数据字典
### 6.1 `channel`
**所属字段**:`channel` / `channels[].channel`|**类型**:String
| 值 | 中文 | 说明 |
|---|---|---|
| `BANK_TRANSFER` | 对公转账 | 无员工代收人,必须填 `transferRef` |
| `CONSULTANT_COLLECTION` | 定制师代收 | 使用订单定制师,不传 `collectorStaffId` |
| `DRIVER_CASH` | 报账人收款 | 仅允许尾款,必须从本渠道 `collectors` 选择代收人 |
### 6.2 `payType`
**所属字段**:`payType` / `channels[].allowedPayTypes[]`|**类型**:String
| 值 | 中文 | 说明 |
|---|---|---|
| `DEPOSIT` | 订金 | 是否可登记以 options 当前返回为准 |
| `FULL` | 全款 | 是否可登记以 options 当前返回为准 |
| `BALANCE` | 尾款 | 是否可登记以 options 当前返回为准;`DRIVER_CASH` 只允许此值 |
> `BANK_TRANSFER` 的通用契约可支持 `DEPOSIT` / `FULL` / `BALANCE`,但具体订单当次能提交哪些值,必须以 options 的 `allowedPayTypes` 为准,不要将某个实例的 `BALANCE` 硬编码为全局规则。
### 6.3 `collectorType`
**所属字段**:`collectorType` / `collectors[].collectorType`|**类型**:String
| 值 | 中文 | 匹配渠道 |
|---|---|---|
| `COMPANY_ACCOUNT` | 公司账户 | `BANK_TRANSFER` |
| `CONSULTANT` | 定制师 | `CONSULTANT_COLLECTION` |
| `ORDER_STAFF` | 订单工作人员 | `DRIVER_CASH` |
### 6.4 `receiptMethod`
**所属字段**:`receiptMethod` / `receiptMethods[].value`|**类型**:String
| 值 | 中文 | 说明 |
|---|---|---|
| `WECHAT_TRANSFER` | 微信转账 | 人员代收渠道的当前默认字典值 |
| `CASH` | 现金收款 | 人员代收渠道的当前默认字典值 |
`BANK_TRANSFER.receiptMethods=[]`;该字典可扩展,实际可选值以 options 当次返回为准。
### 6.5 `payStatusAfter`
**所属字段**:POST 响应 `payStatusAfter`|**类型**:String
| 值 | 中文 | 说明 |
|---|---|---|
| `UNPAID` | 未付款 | 尚未完成有效收款 |
| `DEPOSIT_PAID` | 已付订金 | 订金已收 |
| `FULLY_PAID` | 已付全款 | 应收金额已收齐 |
## 7. 错误码
| code | message / 含义 | 触发场景 |
|---|---|---|
| `520011` | 支付类型无效或与订单状态不匹配 | `payType` 不在当前 options 允许范围内 |
| `520401` | 收款渠道非法 | `channel` 不在三个渠道枚举中 |
| `520402` | 对公转账渠道必须填写转账流水号 | `BANK_TRANSFER` 未传 `transferRef` |
| `520403` | 报账人收款渠道必须指定代收人 | `DRIVER_CASH` 未传 `collectorStaffId` |
| `520404` | 代收人不属于本订单人员 | `collectorStaffId` 不是本订单有效人员 |
| `520407` | 订单已取消,不允许登记线下收款 | 已取消订单提交 POST |
| `520408` | 收款金额必须大于 0 | `amount < 0.01` |
| `520409` | 线下收款代收人类型非法 | `collectorType` 与 `channel` 不匹配 |
| `520410` | 报账人收款只能登记尾款 | `DRIVER_CASH` 提交 `DEPOSIT` 或 `FULL` |
| `520411` | 报账人收款必须选择本订单报账人 | 选中的订单人员不是报账人 |
| `520412` | 订单没有可用定制师,不能登记定制师代收 | `CONSULTANT_COLLECTION` 无可用定制师 |
| `520413` | 本次收款金额超过当前可收余额 | `amount` 大于当前可收金额 |
## 8. 示例(典型 + 边界 + 异常)
### 8.1 典型:查询选项,对公转账无代收人
**请求**:
```http
GET /v3/admin/order/2079454953641836546/payment/manual-receipt/options
Authorization: Bearer <admin-jwt>
无请求体
```
**响应**:
```json
{
"code": 200,
"message": "操作成功",
"data": {
"channels": [
{
"channel": "CONSULTANT_COLLECTION",
"channelText": "定制师代收",
"allowedPayTypes": ["BALANCE"],
"disabled": false,
"disabledReason": null,
"collectors": [
{
"collectorType": "CONSULTANT",
"collectorId": "2037350531801993218",
"collectorName": "张三",
"collectorRole": "CONSULTANT",
"collectorRoleText": "定制师",
"defaultSelected": true
}
],
"receiptMethods": [
{"value": "WECHAT_TRANSFER", "label": "微信转账", "defaultSelected": true},
{"value": "CASH", "label": "现金收款", "defaultSelected": false}
]
},
{
"channel": "BANK_TRANSFER",
"channelText": "对公转账",
"allowedPayTypes": ["BALANCE"],
"disabled": false,
"disabledReason": null,
"collectors": [],
"receiptMethods": []
},
{
"channel": "DRIVER_CASH",
"channelText": "报账人收款",
"allowedPayTypes": [],
"disabled": true,
"disabledReason": "本订单暂无可代收报账人",
"collectors": [],
"receiptMethods": [
{"value": "WECHAT_TRANSFER", "label": "微信转账", "defaultSelected": true},
{"value": "CASH", "label": "现金收款", "defaultSelected": false}
]
}
]
},
"success": true
}
```
### 8.2 边界:对公转账不传代收人
**场景说明**:`collectorStaffId` 和 `collectorType` 都不传;仅提交 options 当前允许的款项类型与对公转账流水号。
**请求**:
```http
POST /v3/admin/order/2079454953641836546/payment/manual-receipt
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{
"channel": "BANK_TRANSFER",
"payType": "BALANCE",
"amount": 100.00,
"transferRef": "BANK202607210001",
"voucherUrls": [],
"remark": "客户对公转账"
}
```
**响应**:
```json
{
"code": 200,
"message": "操作成功",
"data": {
"id": "2079600000000000001",
"orderId": "2079454953641836546",
"channel": "BANK_TRANSFER",
"channelLabel": "对公转账",
"payType": "BALANCE",
"payTypeLabel": "尾款",
"amount": 100.00,
"receivedAt": "2026-07-21T15:30:00",
"collectorStaffId": null,
"collectorStaffName": null,
"collectorType": "COMPANY_ACCOUNT",
"collectorAdminId": null,
"collectorName": "公司账户",
"collectorRole": "COMPANY_ACCOUNT",
"transferRef": "BANK202607210001",
"receiptMethod": null,
"receiptMethodLabel": null,
"voucherUrls": [],
"remark": "客户对公转账",
"operatorName": "管理员",
"createTime": "2026-07-21T15:30:00",
"voided": false,
"voidedByName": null,
"voidedAt": null,
"voidReason": null,
"paidAmountAfter": 1600.00,
"payStatusAfter": "DEPOSIT_PAID"
},
"success": true
}
```
### 8.3 异常:报账人收款未选择代收人
**请求**:
```http
POST /v3/admin/order/2079454953641836546/payment/manual-receipt
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{
"channel": "DRIVER_CASH",
"payType": "BALANCE",
"amount": 100.00,
"receiptMethod": "CASH"
}
```
**响应**:
```json
{
"code": 520403,
"message": "报账人收款渠道必须指定代收人",
"data": null,
"success": false
}
```
## 9. 业务边界
- `collectors` 是每个 channel 自己的候选列表,不是所有渠道共用的必选列表。
- `BANK_TRANSFER`:`collectors=[]`,不传 `collectorStaffId`,必须传 `transferRef`;即使误传 `collectorStaffId`,响应中员工代收人 ID 仍为空。
- `CONSULTANT_COLLECTION`:不要提交 `collectorStaffId`;当订单没有可用定制师时,渠道禁用。
- `DRIVER_CASH`:仅允许 `BALANCE`,必须传当前订单有效报账人的 assignmentId。
- options 的 `allowedPayTypes` 随订单状态和可收余额变化。待支付订单中,对公转账/定制师代收可返回 `DEPOSIT`、`FULL`;非待支付且仍有可收余额时可返回 `BALANCE`。
- 渠道 `disabled=true` 或 `allowedPayTypes=[]` 时,当前不可提交该渠道。
- 已取消订单、无可收余额的订单不能登记线下收款。
## 10. 修改前后对比
> 本节对比的是“错误理解 / 错误文档示例”与“正确的现有契约”,不表示后端今日发布了新的接口变更。
| 项目 | 错误理解 / 历史错误示例 | 正确契约 |
|---|---|---|
| 对公转账的代收人候选 | `BANK_TRANSFER.collectors` 含“公司账户”对象 | `BANK_TRANSFER.collectors=[]` |
| `collectorStaffId` 字段 | 所有渠道都要选代收人,或该字段已从后端删除 | 字段仍保留,**仅 `DRIVER_CASH` 条件必填** |
| 对公转账必填项 | 代收人 | `transferRef` 转账流水号 |
| 定制师代收 | 复用员工代收人选择器并传 `collectorStaffId` | 不要传 `collectorStaffId`,使用订单定制师 |
| 对公转账款项类型 | 固定只能是某一种款项 | 以 options 当前返回的 `allowedPayTypes` 为准 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:否。后端字段、枚举和行为没有变更。
- **前端是否必须同步上线**:是。已有页面在 `BANK_TRANSFER` 下显示代收人,需要按正确契约纠正。
### 11.2 回滚说明
- 本次仅修正通知文档,不涉及后端接口回滚。
- 若前端回滚渠道联动修正,对公转账将再次错误显示代收人。
## 12. 注意事项
- 选中 `BANK_TRANSFER` 时,隐藏代收人选择器,并清空从其他渠道切换前残留的 `collectorStaffId`。
- 选中 `BANK_TRANSFER` 时,显示并校验 `transferRef`,不要根据统一响应结构中“存在 `collectors` 字段”就认定代收人必选。
- 仅 `DRIVER_CASH` 把 `collectorStaffId` 设为必填,候选项取当前 channel 的 `collectors`。
- `CONSULTANT_COLLECTION` 不要复用 `DRIVER_CASH` 的员工代收人校验。
- 不要把测试订单中 `BANK_TRANSFER.allowedPayTypes=["BALANCE"]` 固化为全局规则;每次均以 options 返回为准。
## 13. 关联 / 联系人
### 13.1 关联
- **Issue**:[#5120](https://git.1814.love:8443/wx/HL/issues/5120)
- **后端 PR**:无(本次无后端代码变更)
- **后端 commit**:无(本次无后端代码变更)
### 13.2 联系人
- **后端负责人**:腰苏图
@@ -0,0 +1,178 @@
---
schema: "hl-changelog/v1"
ticket: "5158"
title: "派车按行程日标记车费日期"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "implemented"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@5eb8a46bee9a5a101371313e2088decd9ea843f2"
updated_at: "2026-07-25T03:03:38.490Z"
base: "dev-v3"
generated: "2026-07-22T18:00:00+08:00"
---
# 【修改接口·前端待处理·管理后台】派车按行程日标记车费日期
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-fleet-service
>
> **后端 PR**: [wx/HL#5166](https://git.1814.love:8443/wx/HL/pulls/5166)、[wx/HL#5167](https://git.1814.love:8443/wx/HL/pulls/5167)
>
> **工单**: [wx/HL#5158](https://git.1814.love:8443/wx/HL/issues/5158)
>
> **影响范围**: 管理后台派单创建、修改派单、派单详情及待司机确认通知预览
## 关键变化
派单的“服务日”和“收取车费日”现在是两个不同概念。未勾选车费的日期仍然是正常派车服务日,继续占用司机和车辆并按原规则处理保险,只把当天车费记为 `0`;不要把未勾选日期当成取消派车。
历史派单及未传新字段的调用均按“全部服务日收取车费”处理,不改变旧数据金额。
## 变更接口
| 方法 | 路径 | 变更类型 | 说明 |
| --- | --- | --- | --- |
| POST | `/admin/fleet/assignments` | 请求新增字段 | 创建排车/直接派车时冻结计费服务日 |
| POST | `/admin/fleet/assignments/{assignmentId}/change` | 请求能力扩展 | 支持不换车、不换司机,仅修改已确认派单的计费日 |
| GET | `/admin/fleet/board/orders/{orderId}` | 响应新增字段 | 当前有效派车组返回计费日、免费服务日及说明 |
| POST | `/admin/fleet/message-templates/{templateId}/render` | 请求与模板变量新增 | 预览计费安排;默认待确认模板已增加车费安排 |
## 创建派单
`POST /admin/fleet/assignments` 的原字段不变,新增:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `chargeableServiceDates` | `LocalDate[]/null` | 否 | 收取车费的服务日期;不传或 `null` 表示全部服务日,空数组表示全部免费 |
| `vehicleFeeWaiverReason` | `String/null` | 条件必填 | 免费服务日说明,最长 256 字;全部免费时必填 |
| `confirmAllServiceDatesFree` | `Boolean/null` | 条件必填 | `chargeableServiceDates=[]` 时必须显式传 `true` |
部分日期计费示例:
```json
{
"orderId": "2046400000000000001",
"startDate": "2026-07-29",
"endDate": "2026-07-31",
"vehicleId": "2046400000000000101",
"driverId": "2046400000000000201",
"holdMode": 1,
"chargeableServiceDates": ["2026-07-30"],
"vehicleFeeWaiverReason": "首尾接送已包含在团费中",
"requestId": "dispatch-5158-example"
}
```
全部免费示例:
```json
{
"chargeableServiceDates": [],
"vehicleFeeWaiverReason": "本团车费由合作方统一结算",
"confirmAllServiceDatesFree": true
}
```
校验规则:
- 所有日期必须属于本次派车组的服务日期,否则返回参数错误。
- 空数组但未二次确认,返回“全部服务日免费时必须二次确认”。
- 空数组但未填写说明,返回“全部服务日免费时必须填写原因”。
- `null` 与不传保持兼容,默认所有服务日计费。
## 修改已确认派单的计费日
`POST /admin/fleet/assignments/{assignmentId}/change` 复用同名三个字段。仅调整车费时可以不传 `newVehicleId/newDriverId`,但必须:
- `holdMode=0`;
- `effectiveDate` 指定修改生效日;
- `reason` 填写本次修改原因;
- `chargeableServiceDates` 表示从 `effectiveDate` 起目标切片中仍收车费的日期,不能包含生效日前日期。
```json
{
"effectiveDate": "2026-07-29",
"holdMode": 0,
"chargeableServiceDates": ["2026-07-30"],
"vehicleFeeWaiverReason": "首尾接送已包含在团费中",
"reason": "按实际结算范围调整",
"requestId": "change-fee-5158-example"
}
```
已确认派单会原地更新计费标记并写操作审计,不取消/重建派单,不改变司机、车辆、占用和保险。对应月份已经关账时返回 `605600`,前端应提示先由有权限人员重开账期。
## 派单详情新增字段
`GET /admin/fleet/board/orders/{orderId}` 的 `data.currentAssignment` 与 `data.activeAssignments[]` 新增:
```json
{
"chargeableServiceDates": ["2026-07-30"],
"freeServiceDates": ["2026-07-29", "2026-07-31"],
"vehicleFeeWaiverReason": "首尾接送已包含在团费中"
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `chargeableServiceDates` | `LocalDate[]` | 收取车费的服务日,按日期升序 |
| `freeServiceDates` | `LocalDate[]` | 仍提供车辆服务但车费为 0 的日期,按日期升序 |
| `vehicleFeeWaiverReason` | `String/null` | 免费服务日说明 |
## 待确认通知与模板预览
`POST /admin/fleet/message-templates/{templateId}/render` 请求新增:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `serviceDates` | `LocalDate[]/null` | 本次派车的实际服务日期;不传时按订单起止日生成 |
| `chargeableServiceDates` | `LocalDate[]/null` | 预览中的计费日期;不传表示全部计费 |
| `vehicleFeeWaiverReason` | `String/null` | 免费服务日说明 |
新增模板变量:
| 变量 | 示例 |
| --- | --- |
| `{{assignment.vehicleFeeSummary}}` | `收取车费:7月30日;免费服务日:7月29日、7月31日` |
| `{{assignment.chargeableServiceDates}}` | `7月30日` |
| `{{assignment.freeServiceDates}}` | `7月29日、7月31日` |
| `{{assignment.vehicleFeeWaiverReason}}` | `首尾接送已包含在团费中` |
全部免费时摘要固定为“本团服务日均不计车费”,全部计费时为“全部服务日收取车费”。派单待确认通知会读取落库后的逐日切片并冻结正文;运营修改模板后不会追改已生成的通知。
系统默认“排车待确认”模板已增加“车费安排”和“免车费说明”。运营自行修改过的默认模板不会被数据库迁移覆盖,如需显示新内容,应在车管模板页面自行加入上述变量。
## 前端处理清单
- [ ] 派单页用订单实际行程服务日期生成多选项,默认全选,并明确标题为“收取车费日期”。
- [ ] 未选日期标为“免费服务日(仍派车、仍占用、仍按规则投保)”,不得触发取消派车逻辑。
- [ ] 全部取消勾选时显示二次确认并要求填写免费原因,提交 `confirmAllServiceDatesFree=true`。
- [ ] 修改已确认派单时调用既有 `/change` 接口,不直接复用创建接口;提交 `holdMode=0`、`reason` 和完整目标日期集合。
- [ ] 派单详情读取 `chargeableServiceDates/freeServiceDates` 回显,不根据车费金额反推。
- [ ] 待确认短信预览把同一组 `serviceDates/chargeableServiceDates/vehicleFeeWaiverReason` 传给模板渲染接口。
- [ ] 雪花 ID 继续按字符串处理,日期继续使用 `yyyy-MM-dd`。
## 验证证据
- 创建派单默认全计费、部分计费及全部免费二次确认均有单元测试。
- 免费服务日仅车费归零,保险成本字段与派车占用保持不变。
- 已确认派单变更写可靠 Outbox,失败可重试;已关账期在变更落库前拦截。
- 待司机确认通知从实际逐日派车切片生成并冻结计费摘要。
- `FleetServiceApplicationTest` 以 H2 真 Flyway 执行 65 条迁移通过;`spotless:check` 与 fleet `verify` 通过。
- 测试环境部署任务 `784326bb` 成功,`hl-fleet-service` 8087/8187 双实例健康。
- 以 `admin` 车务身份经测试网关实测模板列表、部分计费预览、全部免费预览、看板列表与详情,HTTP/业务码均为 200。
- 部分计费正文实际包含“收取车费:7月30日;免费服务日:7月29日、7月31日”;全部免费正文实际包含“本团服务日均不计车费”和免费原因。
- 看板详情 `currentAssignment` 已实际返回 `chargeableServiceDates/freeServiceDates/vehicleFeeWaiverReason` 契约字段。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`;前端按“前端处理清单”接入即可。
@@ -0,0 +1,130 @@
---
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@b309f1672f4587d11aa6b8e86d0dd4ba043274d1"
updated_at: "2026-07-25T03:11:01.515Z"
---
# 【前端待处理·管理后台】#5160 一名司机可绑定多辆常驻车
> **服务**: `hl-fleet-service`
> **Issue**: [wx/HL#5160](https://git.1814.love:8443/wx/HL/issues/5160)
> **PR**: [wx/HL#5164](https://git.1814.love:8443/wx/HL/pulls/5164)
> **日期**: 2026-07-22
> **影响范围**: 管理后台司机档案、车辆档案与车务派单候选
---
## 关键变化
- 常驻关系调整为“一辆车至多一名常驻司机,一名司机可常驻多辆车”。常驻关系仍以 `fleet_vehicle.primary_driver_id` 为权威,与实际派单关系相互独立。
- 新增司机侧常驻车辆全集替换接口;空数组表示全部解绑。接口会先校验全部目标车辆,任一车辆已被其他司机占用时整单失败,不产生部分写入。
- 旧单车接口和旧单车响应字段继续保留。旧接口等价于把全集替换为单元素或空集合;旧响应字段固定取按车辆 ID 升序后的第一辆。
- `onlyResidentUnbound` 司机筛选参数兼容保留但不再生效,因为司机不再存在“已被一辆车占用”的状态。车辆侧“仅无常驻司机车辆”筛选仍有效。
## 接口清单
| # | 方法 | 路径 | 变更 |
|---|---|---|---|
| 1 | `PUT` | `/admin/fleet/drivers/{driverId}/resident-vehicles` | 新增:全量替换司机常驻车辆集合 |
| 2 | `PUT` | `/admin/fleet/drivers/{driverId}/resident-vehicle` | 保留:旧单车契约,内部按全集替换执行 |
| 3 | `GET` | `/admin/fleet/drivers/{driverId}` | 新增 `residentVehicles[]` |
| 4 | `GET` | `/admin/fleet/drivers` | 列表项新增 `residentVehicles[]`;`onlyResidentUnbound` 废弃 |
| 5 | `POST` | `/admin/fleet/assignments/candidates` | 司机候选与已选司机回显新增完整常驻车辆集合 |
| 6 | 车辆新增/编辑/导入 | 既有车辆档案接口 | 同一司机已常驻其他车辆时不再返回 `605022` |
## 1. 全量替换常驻车辆
`PUT /admin/fleet/drivers/{driverId}/resident-vehicles`
请求体:
```json
{
"vehicleIds": [
"2079857985374363650",
"2079857985697308674",
"2079857985848320002"
]
}
```
| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `vehicleIds` | `string[]` | ✅ | 最多 100 个 | 目标全集;`[]` 表示全部解绑;雪花 ID 必须按字符串处理 |
成功响应:
```json
{ "code": 200, "message": "成功", "data": null, "success": true }
```
错误行为:
- 司机不存在:`600205`
- 任一车辆不存在:`600110`
- 任一目标车辆属于其他常驻司机:`605023`
- 缺少 `vehicleIds` 或超过 100 个:`400`
## 2. 司机详情与列表
详情和分页列表项新增:
```json
{
"residentVehiclePlate": "蒙A-T1557",
"residentVehicles": [
{
"vehicleId": "2079857985374363650",
"plate": "蒙A-T1557",
"modelName": "丰田普拉多",
"vehicleTypeId": "..."
},
{
"vehicleId": "2079857985697308674",
"plate": "蒙A-U1557",
"modelName": "丰田汉兰达",
"vehicleTypeId": "..."
}
]
}
```
`residentVehicles` 恒按 `vehicleId` 升序;无常驻车辆时为 `[]`。兼容字段 `residentVehiclePlate` 取第一项车牌,无数据时为 `null`。
## 3. 派单候选
`POST /admin/fleet/assignments/candidates`:
- `data.drivers.records[].residentVehicles[]` 新增全部 `{ vehicleId, plate }`;旧 `residentVehicleId` / `residentVehiclePlate` 取第一项。
- `data.selectedDriverResidentVehicles[]` 新增已选司机的全部车辆候选快照;旧 `selectedDriverResidentVehicle` 取第一项。
- 所选车辆命中司机常驻集合中的任意一辆,均视为常驻匹配,不触发跨常驻确认。
- `driverKeyword` 可命中该司机任意常驻车牌。
## 不影响范围
- 每辆车仍只有一个 `primaryDriverId`,车辆侧选择常驻司机仍是单选。
- 实际订单派车不修改常驻关系;跨常驻车辆派单规则继续有效。
- 旧接口、旧单车字段与 H5 单车续签契约继续可用。
- 本次只提供后端契约,不直接修改 `hl-ui`。
## 验收清单
- [ ] 司机编辑可提交多辆车辆的 `vehicleIds` 全集,保存后详情回显相同集合。
- [ ] 空数组可全部解绑;重复提交相同集合幂等成功。
- [ ] 目标车辆被其他司机占用时整单失败,原绑定保持不变。
- [ ] 车辆档案可把同一司机设为多辆车的常驻司机。
- [ ] 派单选择司机的任一常驻车辆都显示常驻匹配,其他车辆仍按跨常驻规则提示。
- [ ] 所有雪花 ID 保持字符串处理。
## 测试环境验证
2026-07-22 经测试网关 `https://api.test.1814.love:9443` 验证:
- `PUT /admin/fleet/drivers/2079857983403024385/resident-vehicles` 提交 3 个车辆 ID → `code=200`。
- `GET /admin/fleet/drivers/2079857983403024385` → `residentVehicles` 按 ID 升序返回 3 项,兼容字段 `residentVehiclePlate=蒙A-T1557`。
- 分别读取 3 辆车辆详情,`primaryDriverId` 均为 `2079857983403024385`,`primaryDriverName=朝鲁门`:
- `蒙A-T1557` / 丰田普拉多
- `蒙A-U1557` / 丰田汉兰达
- `蒙A-V1557` / 丰田兰德酷路泽
- 测试环境部署任务:`1485774a`,`hl-fleet-service` 从 `dev-v3` 部署成功。
- 后端验证:定向 527 个测试通过;`spotless:check` 通过;最新 `dev-v3` 基线执行 `mvn -pl hl-fleet-service -am verify` 通过。
@@ -0,0 +1,10 @@
# 车务订单聊天前端入口与未读提醒
## 前端交接(截图反馈)
- 派单看板订单卡接入 `unreadMessageCount`:大于 0 显示消息红点,超过 9 显示 `9+`。
- 派单看板订单卡增加「联系定制师」按钮,使用该行 `orderId` 调用 `POST /admin/message/chat/open-fleet`,不要使用展示号 `id`。
- 订单详情弹窗增加「联系定制师」按钮,复用房务聊天抽屉和同一 `open-fleet` 接口;打开会话后自动标记已读。
- 右上角消息提醒沿用 `/admin/message/unread-count` 初始拉取 + SSE 刷新,和房务保持一致。
详细接口契约见同目录 `02_4689_车务订单聊天_定制师车务团队_新接口_管理后台.md`。
@@ -0,0 +1,119 @@
---
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@28a4a78888777a50b11c37a69d4bb42d43d0e552"
updated_at: "2026-07-25T03:14:52.995Z"
---
# 房务配房价格模型收口为协议价与结算价
> **服务**: hl-order-service-v3
> **Issue**: [wx/HL#5176](https://git.1814.love:8443/wx/HL/issues/5176)
> **PR**: [wx/HL#5179](https://git.1814.love:8443/wx/HL/pulls/5179)
> **日期**: 2026-07-23
> **影响范围**: 管理后台 · 房务配房提交/修改/详情、订单行程住宿回配展示
---
## 关键变化
房务配房价格只保留两个权威字段:
| 字段 | 含义 |
|------|------|
| `protoPrice` | 协议价快照,元/间·晚 |
| `settlementPrice` | 结算价快照,元/间·晚 |
历史 `sellPrice` 已从房务请求、响应、持久化、快照和内部契约中删除。订单行程住宿回配对象同时删除为兼容历史展示而重复返回的 `unitPrice`、`plannedCost`、`protocolPrice`;前端不得继续提交、读取或回退这些字段。
本变更仅清理房务配房链路,不影响行程节点、结算票等其他业务域中的同名价格字段。
---
## 变更接口
| 接口 | 方法 | 路径 | 变更 |
|------|------|------|------|
| 提交配房方案 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/assignments` | 入参项删除 `sellPrice` |
| 修改单条配房 | PUT | `/v3/admin/order/assignments/{assignmentId}` | 入参删除 `sellPrice` |
| 房务订单详情 | GET | `/admin/house/orders/{orderId}` | `itinerary[].assignments[]` 删除 `sellPrice` |
| 订单行程详情 | GET | `/v3/admin/order/{orderId}/itinerary` | `hotelGroup.assignments[]` 删除重复价格字段,改为双价 |
### 配房提交/修改入参
只提交:
```json
{
"protoPrice": 588.00,
"settlementPrice": 688.00
}
```
不要再提交:
```json
{
"sellPrice": 688.00
}
```
### 房务详情配房项
`data.itinerary[].assignments[]` 当前价格字段:
```json
{
"protoPrice": "588.00",
"settlementPrice": "688.00"
}
```
已删除:`sellPrice`。
### 订单行程住宿回配项
`data.hotelGroup.assignments[]` 当前价格字段:
```json
{
"protoPrice": "588.00",
"settlementPrice": "688.00"
}
```
已删除:
- `sellPrice`
- `unitPrice`
- `plannedCost`
- `protocolPrice`
前端价格列直接读取 `settlementPrice`;需要同时展示成本参考时读取 `protoPrice`。不要为兼容旧页面在本地重新合成 `unitPrice`、`plannedCost` 或 `sellPrice`。
---
## 金额口径
- 房务配房、房务详情和订单行程详情均直接返回落库快照,不按当前资源价格日历重算。
- 终止退款住宿单价改为从房务只读契约读取 `settlementPrice`;对外退款明细字段名仍为 `dealPrice`。
- 签单住宿成本仍读取 `protoPrice`。
- 核单住宿计划/实际成本继续按 `settlementPrice × roomCount` 派生,对外核单字段不在本次删除范围。
---
## 前端处理
1. 删除所有房务配房请求中的 `sellPrice`。
2. 房务详情和订单行程住宿价格统一展示 `settlementPrice`。
3. 协议价展示读取 `protoPrice`。
4. 删除对 `sellPrice`、`unitPrice`、`plannedCost`、`protocolPrice` 的兼容读取与回退逻辑。
5. 若接口返回的 `protoPrice` 或 `settlementPrice` 为 `null`,按统一空值样式展示,不用另一字段伪造。
---
## 后端验证
- 房务提交、修改、详情、历史快照、内部聚合、订单详情、退款和核单定向测试通过。
- 订单详情集成测试确认仅返回 `protoPrice`、`settlementPrice`,旧价格字段不存在。
- Flyway 新增迁移,精确删除 `house_hotel_assignment.sell_price` 与 `house_requirement_assignment_snapshot.sell_price`;其他业务域同名列不受影响。
- `D:/work2/hl-ui` 未修改,前端适配由管理后台项目单独处理。
@@ -0,0 +1,156 @@
---
schema: "hl-changelog/v1"
ticket: "5178"
title: "用车手动加急与派车看板状态颜色"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "implemented"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@d07506cd3aa8a23cb2aa90f891eb853e1b7dd13f"
updated_at: "2026-07-25T03:18:55.871Z"
base: "dev-v3"
generated: "2026-07-23T10:00:00+08:00"
---
# 【新增接口·前端待处理·管理后台】用车手动加急与派车看板状态颜色
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-order-service-v3、hl-fleet-service
>
> **后端 PR**: [wx/HL#5181](https://git.1814.love:8443/wx/HL/pulls/5181)
>
> **工单**: [wx/HL#5178](https://git.1814.love:8443/wx/HL/issues/5178)
>
> **影响范围**: 订单详情“行程安排”用车卡片、车务派车看板列表及派单详情
## 业务口径
- 用车需求的“加急/取消加急”与房务需求保持一致:只改变优先级和页面提示,不修改需求状态、派单状态、司机车辆占用、保险或费用。
- 手动加急优先于临近出团、等待回复等自动紧急规则;取消后由后端恢复现有自动紧急度。
- 前端不得根据日期自行推断是否加急,也不得乐观修改本地状态;操作成功后重新拉取订单详情和派车看板。
- 已完成或已失效需求不能加急。按钮按后端允许状态 `PENDING`、`PROCESSING` 显示。
## 变更接口
| 方法 | 路径 | 权限 | 说明 |
| --- | --- | --- | --- |
| POST | `/v3/admin/order/vehicle-requirement/{requirementId}/urgent` | 本单定制师或超级管理员 | 手动加急;重复调用幂等 |
| POST | `/v3/admin/order/vehicle-requirement/{requirementId}/urgent/cancel` | 本单定制师或超级管理员 | 取消手动加急;重复调用幂等 |
请求无 Body,成功统一返回:
```json
{
"code": 200,
"data": null,
"message": "成功",
"success": true
}
```
需要明确处理的业务错误:
| code | 含义 | 建议提示 |
| --- | --- | --- |
| `582084` | 当前账号不是本单定制师且不是超级管理员 | 仅本单定制师可操作用车加急 |
| `582087` | 需求已完成、已失效或状态已变化 | 当前用车需求状态已变化,请刷新后重试 |
## 订单行程接口新增字段
`GET /v3/admin/order/{id}/itinerary` 的 `data.vehicleGroup.requirement` 新增:
```json
{
"requirementId": "2046400000000000001",
"status": "PENDING",
"manualUrgent": true
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `manualUrgent` | `Boolean` | `true` 显示“取消加急”,`false` 显示“加急” |
订单详情“行程安排 → 用车安排”卡片应复用房务卡片的交互:
- 当前有效需求存在且状态为 `PENDING/PROCESSING` 时,在“联系车务”旁显示“加急”或“取消加急”。
- 点击后调用对应接口;成功后重拉订单详情,并让派车看板列表失效/刷新。
- 提交中禁用按钮,防止连续点击;接口错误显示后端业务文案。
## 派车看板列表契约
`GET /admin/fleet/board/orders` 每条 `data.records[]` 新增/统一返回:
```json
{
"assignmentStatus": "unassigned_urgent",
"assignmentStatusLabel": "待派车",
"manualUrgent": true,
"urgentBadge": "手动加急"
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `assignmentStatus` | `String` | 后端计算后的权威状态码 |
| `assignmentStatusLabel` | `String` | 后端统一中文状态文案,页面直接展示 |
| `manualUrgent` | `Boolean` | 是否由定制师手动加急 |
| `urgentBadge` | `String/null` | 手动加急固定为“手动加急”;自动加急返回既有 T-N 文案 |
同一状态内,后端已把手动加急订单排在自动加急和普通订单之前,前端不需要再次排序。
## 派单详情新增字段
`GET /admin/fleet/board/orders/{orderId}` 新增:
- `data.manualUrgent`
- `data.currentAssignment.manualUrgent`
- `data.activeAssignments[].manualUrgent`
当前派单的 `assignmentStatus/assignmentStatusLabel/urgentBadge` 同样是后端计算后的有效状态,详情页不得自行按日期覆盖。
## 页面颜色映射
派车看板卡片参照房务卡片形成清晰的状态色,优先使用现有设计 Token;不要只给状态标签上色。
| 判定顺序 | 卡片语义 | 推荐底色 / 边框 | 标签 |
| --- | --- | --- | --- |
| `manualUrgent=true` | 定制师手动加急 | 淡红 `#FFF1F0` / 红 `#FF4D4F` | `urgentBadge` |
| `assignmentStatus=unassigned_urgent/holding_urgent` | 系统自动加急 | 淡橙 `#FFF7E6` / 橙 `#FA8C16` | `urgentBadge` |
| `assignmentStatus=unassigned` | 待派车 | 淡黄 `#FFFBE6` / 黄 `#FAAD14` | `assignmentStatusLabel` |
| `assignmentStatus=holding` | 待司机/车务确认 | 淡蓝 `#E6F4FF` / 蓝 `#1677FF` | `assignmentStatusLabel` |
| `assignmentStatus=assigned` | 已确认执行 | 淡绿 `#F6FFED` / 绿 `#52C41A` | `assignmentStatusLabel` |
| 完成、取消等终态 | 已结束 | 灰 `#F5F5F5` / 灰 `#BFBFBF` | `assignmentStatusLabel` |
样式优先级必须是 `manualUrgent` > 自动紧急状态 > 普通状态。卡片左侧强调边、背景和状态标签应同步变化,效果与房务“已回配/待处理”卡片一致。
## 前端处理清单
- [ ] `VehicleArrangeCard.vue` 在“联系车务”旁增加“加急/取消加急”,交互和按钮状态复用 `RoomArrangeCard.vue`。
- [ ] `ArrangementTab.vue` 和订单详情父组件透传 `urgent/cancel-urgent` 事件,新增用车加急 API 方法。
- [ ] 操作成功后重新拉取订单详情和派车看板,不在前端直接翻转 `manualUrgent`。
- [ ] 派车看板卡片按上表给整卡着色,优先读取 `manualUrgent`,状态文案读取 `assignmentStatusLabel`。
- [ ] 手动加急与自动加急用不同颜色和标签,不显示英文状态码。
- [ ] 派单详情读取顶层及当前/有效派单的 `manualUrgent`,不根据出团日期反推。
- [ ] 雪花 ID 继续按字符串处理。
## 验证证据
- 用车加急/取消加急的权限、状态门控和幂等覆盖单元测试。
- order-v3 订单详情与 order-v3 → fleet 共享契约均覆盖 `manualUrgent`。
- fleet 有效状态、排序、列表和详情映射覆盖手动加急优先规则。
- order-v3 与 fleet 全量 `verify` 通过,fleet 同时通过 Spotless 门禁。
- 测试环境滚动部署成功:order-v3 任务 `126a59e4`,8086/8186 双实例健康;fleet 任务 `a21c7f2e`,8087/8187 双实例健康。
- 以 `wx` 定制师经网关加急,行程接口实际返回 `manualUrgent=true`;以 `admin` 车务读取看板,实际返回 `unassigned_urgent`、`待派车`、`手动加急`,派单详情顶层也返回 `manualUrgent=true`。
- 验收结束已由 `wx` 取消加急并复查,看板恢复 `manualUrgent=false`、`unassigned`、`urgentBadge=null`,未遗留测试状态。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`;前端按“前端处理清单”接入即可。
@@ -0,0 +1,145 @@
# 订单详情「联系车务」独立未读红点
> 日期:2026-07-23
> 工单:HL #5180
> 影响范围:管理后台订单详情 → 行程安排 → 用车安排;聊天 SSE 实时状态
> 状态:前端待处理
## 1. 问题与口径
车务通过 `FLEET:{orderId}` 会话给定制师发送消息后,顶部全局铃铛能显示未读,但当前订单「联系车务」按钮没有订单维度红点。房务按钮已有同款能力,本次车务必须复用相同的角标样式、实时刷新和已读清零交互。
房务与车务未读是两个独立业务会话,禁止继续共用一个字段:
- `unreadMessageCount`:当前登录定制师在本订单 `HOUSE:{orderId}` 会话的未读数,只供「联系房务」使用。
- `fleetUnreadMessageCount`:当前登录定制师在本订单 `FLEET:{orderId}` 会话的未读数,只供「联系车务」使用。
- 顶部铃铛全局未读只能说明“存在未读”,不能作为当前订单按钮是否亮红点的判断依据。
## 2. 接口变更
```http
GET /v3/admin/order/{id}/itinerary
```
响应新增:
```json
{
"unreadMessageCount": 0,
"fleetUnreadMessageCount": 2,
"canContactFleet": true,
"contactFleetDisabledReason": null
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `unreadMessageCount` | Integer | HOUSE 订单会话未读;既有字段,语义不变 |
| `fleetUnreadMessageCount` | Integer | FLEET 订单会话未读;新增字段;无会话、未登录或软依赖降级时为 `0` |
两个字段必须分别消费,不能用 `fleetUnreadMessageCount || unreadMessageCount` 一类兜底混用,否则房务消息会错误点亮车务按钮。
后端内部取数同时增加可选 `unreadScope`:
- FLEET 不传或传 `TEAM`:保持车务看板既有团队共享未读口径。
- FLEET 传 `PERSONAL`:按指定 `adminId` 返回车务发给该定制师的个人未读;订单详情的 `fleetUnreadMessageCount` 使用此口径。
- HOUSE:仍按成员行个人未读统计,行为不变。
该字段属于 order-v3 → user-service 内部契约,管理后台无需直接传递。
## 3. 前端实现要求
### 3.1 按钮角标
`VehicleArrangeCard.vue` 的「联系车务」按钮按 `RoomArrangeCard.vue` 原样复用 `NBadge`:
- `fleetUnreadMessageCount > 0` 时显示红色数字角标。
- `max=99`,`0` 自动隐藏。
- 按钮禁用时仍可保留未读提示,不能因为 `canContactFleet=false` 静默吞掉既有会话未读;是否允许重新开会话继续遵守后端门控。
- 样式、偏移、尺寸与「联系房务」保持一致,不新增另一套红点 CSS。
`v3Adapter.js` 需把行程接口的 `fleetUnreadMessageCount` 映射为独立本地字段(建议 `itineraryFleetUnreadCount`),不要覆盖现有 `itineraryUnreadCount`。
### 3.2 SSE 实时刷新
订单详情现有 `lastChatSignal` 监听只匹配 `HOUSE:{orderId}`。需要同时支持:
```text
HOUSE:{orderId} -> 刷新联系房务角标
FLEET:{orderId} -> 刷新联系车务角标
```
收到当前订单的 `FLEET:{orderId}` `im-chat` / `im-chat-read` 信令后,轻量重拉行程接口并只合并 `fleetUnreadMessageCount`;不能整页闪骨架屏,也不能把其他订单的全局未读数套到当前订单。
### 3.3 已读清零与竞态
- 打开「联系车务」并收到聊天抽屉 `read` 事件后,立即把当前订单车务角标本地清零。
- 房务已读只清 HOUSE 字段,车务已读只清 FLEET 字段,互不影响。
- 复用房务现有 `chatReadEpoch`(或等价版本号)防竞态:已读期间较早发出的刷新请求返回时,不得把旧未读数重新覆盖成红点。
- 切换订单时按新 `orderId` 重算,不能沿用上一个订单角标。
## 4. 验收场景
- [ ] 当前订单无未读时,「联系车务」不显示角标。
- [ ] 车务给本单定制师发送 1 条消息后,不刷新页面,顶部铃铛和本单「联系车务」都立即显示红点/数字 `1`。
- [ ] 当前订单无车务未读、其他订单有车务未读时,顶部铃铛可亮,但当前订单「联系车务」不亮。
- [ ] 当前订单只有房务未读时,只点亮「联系房务」,不得点亮「联系车务」。
- [ ] 打开本单车务会话并读完后,「联系车务」角标立即消失,顶部铃铛与站内信列表同步收敛。
- [ ] 已读操作与 SSE 刷新并发时,旧请求不会让已清零红点复现。
- [ ] 角标样式、最大数字和按钮布局与房务模块一致。
## 5. 兼容性
新增响应字段为加性变更。未接入新字段的旧前端行为不变;前端接入后仍使用既有 `POST /admin/message/chat/open-fleet` 打开会话,`orderId` 必须使用数字雪花字符串,不能使用 `HL...` 展示号。
## 6. 2026-07-23 车务看板回归补充
本节针对车务管理员在「派单看板」点击「联系定制师」的反向会话入口。它使用车务团队
`TEAM` 未读口径,不得复用定制师订单详情的 `PERSONAL` 字段。
### 6.1 首次点击必须立即打开真实会话
当前 `FleetBoard` 在首次点击时同一轮设置 `chatOrder` 和 `chatOpen=true`,随后通过
`v-if="chatOrder"` 首次挂载 `ChatDrawer`。`ChatDrawer` 对 `props.show` 的 watcher 没有
立即执行,因此组件以 `show=true` 首次挂载时不会调用 `open-fleet`,只显示默认「对端」
和空线程;关闭后第二次发生 `false -> true` 才会正常调用接口。
前端需要修复该生命周期缺口:
- `ChatDrawer` 首次挂载且 `show=true` 时必须执行一次 `openFlow()`;建议在现有合并 watcher
保留 `flush: 'post'` 并增加 `immediate: true`,或采用等价的挂载处理。
- 首次点击只能调用一次 `POST /admin/message/chat/open-fleet`,不能因 `show/bizId` 同轮变化
重复打开或让后一个请求取消前一个请求。
- 首次接口响应后立即展示真实 `peerName/peerRoleLabel/thread`;加载完成前保持 loading,
不能先落成可交互的「对端」空会话。
- `show=false` 首次挂载不得调用打开接口;之后每次 `false -> true` 仍只调用一次。
### 6.2 车务看板未读角标必须实时刷新
车务看板列表、密集视图和详情抽屉虽然已经消费 `unreadMessageCount`,但当前只订阅
`useFleetDispatchRefresh` 的派车业务信令,没有订阅聊天总线 `lastChatSignal`。因此定制师
发来 `FLEET:{orderId}` 新消息后只能整页刷新才出现角标。
前端需要按房务模块的方式补齐:
- 监听全局 `lastChatSignal`,仅处理当前页订单的 `FLEET:{orderId}` `im-chat/im-chat-read`
信令;其他模块和其他订单不得误刷新角标。
- 信令只表示“数据变化”,不能把顶部铃铛的合并未读数直接写进订单。应轻量重拉当前筛选/
分页的车务看板订单接口,并只合并对应行的 `unreadMessageCount`。
- 轻量刷新不得切换全页 loading、闪白、重置筛选、分页或滚动位置;短时间连续信令需要合并。
- 列表 `orders`、当前 `activeOrder`、当前 `chatOrder` 的同一订单计数必须一起收敛。
- 打开会话收到 `read` 后立即本地清零,并使用读态纪元/刷新序号防止较早发出的异步刷新把
旧未读数重新覆盖回来。
- 聊天抽屉正在打开当前会话时由 `ChatDrawer` 实时拉消息并标已读;看板 watcher 不得与其
抢写非零计数。
### 6.3 回归验收
- [ ] 清缓存后首次点击任一订单「联系定制师」,只发起一次 `open-fleet`,直接显示真实定制师及历史消息,不出现「对端」空会话。
- [ ] 关闭再打开同一订单,行为与首次一致,不依赖“点第二次才正常”。
- [ ] 定制师给该订单发送 1 条新消息后,车务不刷新页面即可在对应订单按钮看到房务同款红色数字角标。
- [ ] 新消息只更新对应订单;其他订单、HOUSE 会话和顶部全局未读不得误点亮该按钮。
- [ ] 实时更新角标时页面不闪 loading,筛选、分页、滚动位置保持不变。
- [ ] 打开会话后角标立即清零;异步刷新晚返回也不会让红点复现。
- [ ] 列表视图、密集视图、订单详情抽屉三处计数一致。
- [ ] 增加组件测试:`ChatDrawer(show=true)` 首挂打开一次、`show=false` 首挂不打开、聊天信令轻量更新/已读竞态不复亮。
@@ -0,0 +1,425 @@
# 【🔧 修改接口·管理后台】Step2 票种规格默认值(#5185)
> **PR**: #5191 | **更新时间**: 2026-07-23
## 1. 接口背景
Step2 门票/游玩项目明细原先可能返回或保存空的 `specName`,前端无法稳定展示票种/规格。现在查询和保存统一补齐“成人票”默认值,同时保留已有的非空规格。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | Step2 查询门票核单明细 | GET | `/v3/admin/order/{orderId}/settlement/step2` | 修改接口 | 无明确规格的明细统一返回 `specName=成人票` |
| 2 | Step2 保存门票核单明细 | PUT | `/v3/admin/order/{orderId}/settlement/step2` | 修改接口 | `specName` 为 `null`、空串或纯空白时按“成人票”保存 |
## 3. 接口详情
### 3.1 Step2 查询门票核单明细
- **使用场景**:进入或刷新核单 Step2 时查询门票/游玩项目明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:是,只读查询。
- **限流**:无接口专属限流约定。
- **默认语义**:自动生成且无明确规格、或已有明细规格为空时,`specName` 返回“成人票”。
- **保留语义**:已有非空规格原样返回,例如“骑马体验”。
### 3.2 Step2 保存门票核单明细
- **使用场景**:全量保存 Step2 门票/游玩项目明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:业务数据为全量替换语义;重复提交相同明细得到相同业务内容,行 ID 可能重新生成。
- **限流**:无接口专属限流约定。
- **默认语义**:`items[].specName` 为 `null`、`""` 或纯空白时,保存并回读为“成人票”。
- **保留语义**:非空规格原样保存,例如“骑马体验”不会被替换。
## 4. 接口入参
### 4.1 路径参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | Long / String | 是 | 订单 ID,必须大于 `0`;19 位 ID 建议按字符串拼入路径 |
GET 无 Query 参数、无请求体。
### 4.2 PUT 请求体
推荐使用对象形式;接口同时兼容直接提交明细数组。
```json
{
"items": []
}
```
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `items` | Array | 是 | 门票/游玩项目明细,全量替换 | 可为空数组;空数组表示清空已保存草稿 |
### 4.3 `items[]` 字段
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `id` | Long / String | 否 | 已存在行 ID;新增或自动生成行可为空 | 19 位 ID 建议使用字符串 |
| `sourceType` | String | 是 | 来源类型 | `SCENIC_ASSIGNMENT`、`ACTIVITY_ASSIGNMENT`、`CUSTOM_ASSIGNMENT` |
| `sourceTypeName` | String | 否 | 来源类型中文名 | 最长 32 字符 |
| `scenicAssignmentId` | Long / String | 否 | 来源记录 ID;手动补充行为空 | 19 位 ID 建议使用字符串 |
| `dayNumber` | Integer | 否 | 行程第几天,从 `1` 开始;保存时按 `dayDate` 计算 | 无需前端计算 |
| `dayDate` | String | 是 | 项目日期 | `yyyy-MM-dd`,不得早于订单出发日期 |
| `scenicName` | String | 是 | 景区或游玩项目名称 | 非空,最长 200 字符 |
| `specName` | String | 否 | 票种/规格名称 | 最长 128 字符;`null`、空串、纯空白统一为“成人票” |
| `ticketCount` | Integer | 是 | 实际购票数量 | 套餐含项目可填 `0` |
| `ticketUnitPrice` | Decimal | 否 | 参考单价,单位元 | 自费项目可填;包价项目可为 `null` |
| `sellPrice` | Decimal | 否 | 客户成交单价,单位元 | 大于等于 `0` |
| `totalAmount` | Decimal | 否 | 客户成交小计,单位元 | 大于等于 `0`;为空时按 `sellPrice × ticketCount` 计算 |
| `plannedCost` | Decimal | 是 | 计划成本,单位元 | 大于等于 `0` |
| `actualCost` | Decimal | 是 | 实际成本,单位元 | 大于等于 `0` |
| `paymentMethod` | String | 否 | 付款方式 | `SIGNED`、`COMPANY_PAID`、`CASH_PAID`;为空时为 `COMPANY_PAID` |
| `paymentMethodName` | String | 否 | 付款方式中文名 | 最长 32 字符 |
| `voucherUrls` | Array\<String> | 否 | 凭证图片 URL 列表 | 可为空数组 |
| `remark` | String | 否 | 备注 | 最长 500 字符 |
## 5. 出参(响应)
### 5.1 统一响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码,成功为 `200` |
| `message` | String | 响应消息,成功为“成功” |
| `data` | Object / Array | GET 为明细数组,PUT 为保存结果对象 |
| `traceId` | String / null | 链路追踪 ID |
| `success` | Boolean | `code=200` 时为 `true` |
### 5.2 GET `data[]`
| 字段 | 类型 | 可为空 | 说明 |
|------|------|--------|------|
| `id` | String / null | 是 | 已保存的 19 位行 ID 按字符串返回;未保存的自动生成行可为 `null` |
| `sourceType` | String | 否 | 来源类型,取值见 §6.2 |
| `sourceTypeName` | String | 是 | 来源类型中文名 |
| `scenicAssignmentId` | String / null | 是 | 19 位来源记录 ID 按字符串返回;手动补充行为空 |
| `dayNumber` | Integer | 是 | 根据项目日期与订单行程计算的天序 |
| `dayDate` | String | 否 | 项目日期,格式为 `yyyy-MM-dd` |
| `scenicName` | String | 否 | 景区或游玩项目名称 |
| `specName` | String | 否 | 无明确规格时返回“成人票”;已有非空规格原样返回 |
| `ticketCount` | Integer | 否 | 实际购票数量 |
| `ticketUnitPrice` | Decimal | 是 | 参考单价,单位元 |
| `sellPrice` | Decimal | 是 | 客户成交单价,单位元 |
| `totalAmount` | Decimal | 是 | 客户成交小计,单位元 |
| `plannedCost` | Decimal | 否 | 计划成本,单位元 |
| `actualCost` | Decimal | 否 | 实际成本,单位元 |
| `paymentMethod` | String | 是 | 付款方式,取值见 §6.3 |
| `paymentMethodName` | String | 是 | 付款方式中文名 |
| `voucherUrls` | Array\<String> | 是 | 凭证图片 URL 列表 |
| `remark` | String | 是 | 备注 |
### 5.3 PUT `data`
| 字段 | 类型 | 说明 |
|------|------|------|
| `addedIds` | Array\<String> | 本次新增行 ID 列表 |
| `updatedIds` | Array\<String> | 本次更新行 ID 列表 |
| `deletedIds` | Array\<String> | 本次删除行 ID 列表 |
| `totalActualCost` | String | 保存后实际成本合计,单位元 |
## 6. 枚举 / 数据字典
### 6.1 `specName`(数据字典 `settlement_ticket_spec`)
**所属字段**:`items[].specName` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `成人票` | 成人票 | 当前默认票种/规格;字典接口返回的 `dictValue` |
加载选项使用:
```http
GET /admin/dict/data/settlement_ticket_spec
```
展示使用字典项 `dictLabel`,提交使用 `dictValue`。本次没有新增 `specCode` 字段。
### 6.2 `sourceType`
**所属字段**:`items[].sourceType` | **类型**:String | **必填**:是
| 值 | 中文 | 说明 |
|----|------|------|
| `SCENIC_ASSIGNMENT` | 景区 | 来源于景区项目 |
| `ACTIVITY_ASSIGNMENT` | 游玩项目 | 来源于游玩项目 |
| `CUSTOM_ASSIGNMENT` | 手动补充 | 核单时手动新增 |
### 6.3 `paymentMethod`
**所属字段**:`items[].paymentMethod` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `SIGNED` | 签单 | 供应商签单 |
| `COMPANY_PAID` | 公司付款 | 未传付款方式时的默认值 |
| `CASH_PAID` | 现付 | 现场付款,可附凭证 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询或保存成功 |
| `400` | 请求参数校验失败 | 必填字段为空、枚举值不合法、金额为负数、字段超长或日期格式错误 |
| `401` | 未认证或认证失效 | 未携带有效管理后台 JWT |
| `584011` | 当前核单状态不允许录门票核单 | PUT 时订单核单状态不是“待核单”或“核单中” |
| `584017` | 订单缺出发日期 | PUT 时无法根据 `dayDate` 计算 `dayNumber` |
| `584018` | 项目日期早于订单出发日期 | PUT 的 `items[].dayDate` 早于订单出发日期 |
| `500` | 系统异常 | 查询或保存过程发生未预期异常 |
## 8. 示例
### 8.1 典型成功:查询自动生成明细
**请求**:
```http
GET /v3/admin/order/2079454953641836546/settlement/step2
Authorization: Bearer <admin-token>
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": [
{
"id": null,
"sourceType": "SCENIC_ASSIGNMENT",
"sourceTypeName": "景区",
"scenicAssignmentId": "2079454953641837001",
"dayNumber": 1,
"dayDate": "2026-07-21",
"scenicName": "示例景区",
"specName": "成人票",
"ticketCount": 2,
"ticketUnitPrice": 100.00,
"sellPrice": 120.00,
"totalAmount": 240.00,
"plannedCost": 200.00,
"actualCost": 200.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": [],
"remark": null
}
],
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
### 8.2 典型成功:提交字典选中的“成人票”
**请求**:
```http
PUT /v3/admin/order/2079454953641836546/settlement/step2
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"items": [
{
"id": null,
"sourceType": "SCENIC_ASSIGNMENT",
"sourceTypeName": "景区",
"scenicAssignmentId": "2079454953641837001",
"dayNumber": 1,
"dayDate": "2026-07-21",
"scenicName": "示例景区",
"specName": "成人票",
"ticketCount": 2,
"ticketUnitPrice": 100.00,
"sellPrice": 120.00,
"totalAmount": 240.00,
"plannedCost": 200.00,
"actualCost": 200.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": [],
"remark": null
}
]
}
```
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"addedIds": ["2079454953641840001"],
"updatedIds": [],
"deletedIds": [],
"totalActualCost": "200.00"
},
"traceId": "b2c3d4e5-f6a7-8901",
"success": true
}
```
### 8.3 边界情况:空规格归一为“成人票”
**保存请求**:
```http
PUT /v3/admin/order/2079454953641836546/settlement/step2
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"items": [
{
"sourceType": "ACTIVITY_ASSIGNMENT",
"sourceTypeName": "游玩项目",
"scenicAssignmentId": "2079454953641837002",
"dayDate": "2026-07-22",
"scenicName": "示例游玩项目",
"specName": null,
"ticketCount": 2,
"ticketUnitPrice": null,
"sellPrice": 0,
"totalAmount": 0,
"plannedCost": 0,
"actualCost": 0,
"paymentMethod": "COMPANY_PAID",
"voucherUrls": [],
"remark": null
}
]
}
```
**保存响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"addedIds": ["2079454953641840002"],
"updatedIds": [],
"deletedIds": [],
"totalActualCost": "0"
},
"traceId": "c3d4e5f6-a7b8-9012",
"success": true
}
```
随后 GET 回读时,该行的关键字段为:
```json
{
"scenicName": "示例游玩项目",
"specName": "成人票"
}
```
### 8.4 业务失败:非法来源类型
**请求**:
```http
PUT /v3/admin/order/2079454953641836546/settlement/step2
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"items": [
{
"sourceType": "UNKNOWN",
"dayDate": "2026-07-21",
"scenicName": "示例项目",
"specName": "成人票",
"ticketCount": 1,
"plannedCost": 0,
"actualCost": 0
}
]
}
```
**响应**:
```json
{
"code": 400,
"message": "sourceType 必须是 SCENIC_ASSIGNMENT / ACTIVITY_ASSIGNMENT / CUSTOM_ASSIGNMENT 之一",
"data": null,
"traceId": "d4e5f6a7-b8c9-0123",
"success": false
}
```
## 9. 业务边界
- GET:自动生成明细没有明确规格时,返回 `specName=成人票`。
- GET:历史已保存明细的 `specName` 为 `null`、空串或纯空白时,也返回“成人票”。
- GET/PUT:已有非空规格保持不变,例如“骑马体验”不会被覆盖。
- PUT:`specName` 最大 128 字符;空值会归一为“成人票”而不是报错。
- PUT:`items` 为全量数据;遗漏的旧明细不会继续保留。
- PUT:仅订单核单状态为“待核单”或“核单中”时允许保存。
- 前端从 `settlement_ticket_spec` 字典读取选项,展示 `dictLabel`,提交 `dictValue` 到 `specName`。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `items[].specName` | String,可返回或保存为空 | String;查询和保存的空值统一为“成人票” |
| `items[].specCode` | 不存在 | 仍不存在,本次未新增 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 自动生成明细没有明确规格 | `specName` 可能为空或与项目名重复 | `specName=成人票` |
| 保存 `specName=null`、空串或纯空白 | 可能按空值保存和回显 | 保存、回读均为“成人票” |
| 保存非空自定义规格 | 原样保存 | 仍原样保存 |
| 接口数量 | GET、PUT 两个既有接口 | 不变,没有新增 Step2 接口 |
## 11. 影响评估
- **是否破坏向后兼容**:否,接口路径、请求结构和响应字段均未改变;只收紧了空规格的返回语义。
- **前端是否必须同步上线**:否;前端可逐步接入字典下拉,未接入时也会收到稳定的“成人票”默认值。
## 12. 注意事项
- 前端如有 `specName || "成人票"` 的临时兜底,可在确认接口已覆盖当前环境后移除。
- 不要新增或提交 `specCode`;当前契约只使用 `specName`。
- 选择字典项后提交 `dictValue`,不要提交 `dictLabel` 以外的展示元数据或 `dictDataId`。
- 自定义非空规格可以继续提交,接口不会强制替换为字典当前默认项。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5185](https://git.1814.love:8443/wx/HL/issues/5185)
- **PR**: [#5191](https://git.1814.love:8443/wx/HL/pulls/5191)
- **Merge commit**: [03ab19b46](https://git.1814.love:8443/wx/HL/commit/03ab19b463be4b00f92848cb348ce6158918e041)
### 13.2 联系人
- **后端负责人**: @yst
@@ -0,0 +1,207 @@
---
schema: "hl-changelog/v1"
ticket: "5186"
title: "排车中订单恢复派车派人入口并补齐改派上下文"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-23T16:10:00+08:00"
---
# Fleet:排车中订单恢复“派车派人”入口
> **服务**: hl-fleet-service
> **Issue**: #5186
> **日期**: 2026-07-23
> **影响范围**: 管理后台车务派单看板及订单派车流程
---
## ⚠️ 关键变化
排车状态为 `holding` 或 `holding_urgent` 时,订单并非不可操作:后端现在返回 `canAssign=true`,并在 `availableActionCodes` 中下发 `CHANGE_ASSIGNMENT`,允许车务继续进入“派车派人”流程调整司机或车辆。
2026-07-23 契约修订:改派候选查询需要用 `orderId + requirementId + fleetItemIndex` 精确定位当前订单的当前用车需求槽位。看板列表、详情顶层和详情内每个有效派车组现均稳定返回字符串形式的 `requirementId`,前端直接透传,不自行推导。
## 变更接口
| 接口 | 方法 | 路径 | 变更类型 |
|------|------|------|----------|
| 派单看板列表 | GET | `/admin/fleet/board/orders` | 响应字段取值扩展、新增字段 |
| 派单看板详情 | GET | `/admin/fleet/board/orders/:orderId` | 新增字段 |
请求参数和写接口路径均未改变。
## 二、响应契约
`records[]` 中以下字段按服务端返回值处理:
| `assignmentStatus` | `canAssign` | `availableActionCodes` | 前端行为 |
|---|---:|---|---|
| `unassigned` | `true` | 包含 `ASSIGN` | 展示“派车派人”,提交既有创建派单接口 |
| `unassigned_urgent` | `true` | 包含 `ASSIGN` | 展示“派车派人”,提交既有创建派单接口 |
| `holding` | `true` | 包含 `CHANGE_ASSIGNMENT` | 展示“改派”,提交既有 `changeAssignment` 改派接口 |
| `holding_urgent` | `true` | 包含 `CHANGE_ASSIGNMENT` | 展示“改派”,提交既有 `changeAssignment` 改派接口 |
| `assigned` / `completed` / `canceled` | `false` | 不包含上述可派动作 | 不展示入口 |
前端不要再用 `assignmentStatus === 'unassigned'` 自行推断入口,也不要因为订单已有司机或车辆就隐藏按钮。入口以 `canAssign === true` 为第一判断,具体提交模式以 `availableActionCodes` 为准。
排车中进入流程属于调整当前有效派单,不是新增第二条有效派单;继续复用现有改派请求、基线差异提示、司机车辆档期冲突提示和刷新逻辑。
### 改派候选上下文
| 响应位置 | 新增字段 | 类型 | 用途 |
|---|---|---|---|
| 列表 `data.records[]` | `requirementId` | `string` | 当前卡片所属用车需求 ID |
| 详情 `data` | `requirementId` | `string` | 当前有效用车需求 ID |
| 详情 `data.currentAssignment` | `requirementId` | `string` | 当前派车组所属用车需求 ID |
| 详情 `data.activeAssignments[]` | `requirementId` | `string` | 每个有效派车组所属用车需求 ID |
进入改派候选查询时:
- `orderId` 取列表返回的数字订单 ID;
- `requirementId` 优先取详情顶层同名字段,按具体派车组操作时可取该组的同名字段;
- `fleetItemIndex` 取当前卡片或当前派车组字段;
- 三者必须原样透传给候选接口,不得使用团号、订单号或数组位置替代;
- `orderId`、`requirementId` 均按字符串处理,避免 JavaScript 大整数精度丢失。
当前 `v2.1` 候选请求组装已经读取 `order.requirementId`;后端部署后,从列表进入并合并详情时会获得该字段,无需前端猜测需求 ID。
### 排车中入口与向导状态
测试环境现状仍有一处前端状态错位:看板卡片已经显示“排车中”,点击“派车派人”后虽然按 `CHANGE_ASSIGNMENT` 进入 `reassign` 模式,但 `resolveAssignFlowRestoreState(order, mode)` 对所有非 `confirmHold` 模式固定返回 `step: 1`,导致向导错误高亮“订单详情”,底部也显示“下一步 · 排车”。
前端需要统一按后端状态和动作码恢复入口语义:
- `assignmentStatus=holding/holding_urgent` 且动作码包含 `CHANGE_ASSIGNMENT` 时,卡片按钮文案显示“改派”,不要继续显示“派车派人”;
- 从该入口打开时保持 `mode=reassign`,向导直接进入第 2 步“排车”,第 1 步“订单详情”显示已完成;
- 第 2 步带出当前司机、车辆,允许只更换其中一项;提交继续调用既有 `changeAssignment`,不得新增第二条有效派单;
- “司机已确认/查看待确认”入口仍使用 `confirmHold` 并恢复第 3 或第 4 步,不能被本次改派逻辑影响;
- 首次待派车订单仍从第 1 步开始,按钮仍为“派车派人”。
以上仅是前端状态机和展示文案调整,后端不新增接口或字段。
### `holding` 的用户可见状态文案
`holding` 是后端技术状态码,表示车辆和司机已经锁定、派单通知已经发出,当前正在等待司机回复。面向车务人员时不能继续显示“排车中”,应统一显示为“待确认”:
- 看板卡片主状态:`holding` 显示“待确认”,`holding_urgent` 显示“待确认即将超时”;
- 状态筛选、数量汇总和图例使用同一套“待确认”文案;
- 卡片上的司机回执徽标可显示“待回复”,用于补充说明,不能与主状态“排车中”形成两个不同口径;
- 技术值仍保持 `holding/holding_urgent`,接口请求参数、状态判断、颜色和改派动作码均不改变;
- 首次尚未锁定车辆和司机的 `unassigned` 继续显示“待派车”,确认完成后的 `assigned` 继续显示“已派车”。
当前 `v2.1` 的 `ORDER_STATUS_META`、`FILTER_STATUS_OPTIONS` 以及后端 `assignmentStatusLabel` 仍含“排车中”旧文案。管理后台应以本节用户口径覆盖展示;如直接消费后端 label,前端需按状态码归一,避免同页出现“排车中”和“待确认”两套名称。
### 待确认订单的“继续派车”入口
待确认订单必须同时保留“继续派车”和“改派”两个入口:
- `availableActionCodes` 包含 `RECORD_DRIVER_CONFIRMATION` 时显示主按钮“继续派车”,点击复用现有 `onConfirmHold(order)`,以 `mode=confirmHold` 打开派单弹窗;
- `confirmHold` 根据后端 `stageCode/driverConfirmedAt` 恢复流程:等待司机回复时进入第 3 步“待确认”,已登记司机确认时进入第 4 步“确认执行”;
- `availableActionCodes` 包含 `CHANGE_ASSIGNMENT` 时另行显示“改派”,点击进入第 2 步重新选择车辆或司机;
- 两个按钮不得互相替代:“继续派车”推进当前有效派单,“改派”修改当前有效派单;
- “复制行程单链接”是独立只读能力,复制后端为当前有效派单签发的司机 H5 链接,不参与派单状态流转。
当前 `v2.1@6e6a11bf` 的 `resolveBoardRowActions()` 仍检查已经废弃的 `CONFIRM` 动作码,而后端生命周期实际下发 `RECORD_DRIVER_CONFIRMATION`,因此截图中“继续派车/司机已确认”按钮没有渲染。前端改为消费真实动作码即可,无需后端增加兼容别名。
### 看板复制司机 H5 行程单链接
看板不再维护独立的“发行程单”抽屉,也不在前端模拟发送成功。这里不是打开订单详情的“打印行程单”,而是把后端已经为当前有效派单签发的 H5 链接复制到剪贴板,车务再通过微信等渠道发给司机。司机可在手机浏览器中独立打开,不需要登录管理后台。
- 将看板按钮文案由“发行程单”改为“复制行程单链接”,事件名同步改为 `copy-itinerary-link`,避免继续表达成“发送”;
- 点击时先调用既有看板详情接口 `GET /admin/fleet/board/orders/{orderId}`,不要调用订单打印接口;
- 默认复制 `currentAssignment.itineraryUrl`;如果入口明确针对某一条有效派单,则复制对应 `activeAssignments[].itineraryUrl`;
- 链接必须直接使用后端返回值,前端不得自行拼接 H5 地址、token、订单 ID 或派单 ID;
- 优先使用 `navigator.clipboard.writeText(itineraryUrl)`;当前管理后台可能运行在 HTTP 内网环境,必须同时提供临时 `textarea + document.execCommand('copy')` 降级实现;
- 复制成功提示“行程单链接已复制,可发送给司机”;
- `itineraryUrl` 为空时不得复制或提示成功,应提示“行程单链接暂不可用,请确认已派车且 H5 配置正常”;
- 删除/停用看板自己的 `ItinerarySendSheet.vue`、消息模板和本地 `message.success('已发送行程单')` 假流程;
- 不复用 `PrintItineraryModal.vue`,不调用 `GET /v3/admin/order/{orderId}/print-itinerary`;订单详情的“打印行程单”继续作为面向车务的独立打印能力保留;
- 链接包含签名 token,前端日志、埋点和错误提示不得记录或展示完整 URL;
- 改派成功后必须重新拉取详情并使用新派单的 `itineraryUrl`,不能继续缓存或复制旧派单链接。
后端链接已绑定当前订单与具体派单,有效期至行程结束后 7 天;公开 H5 接口会校验签名、有效期和派单归属,并实时读取行程数据。复制动作本身不改变派单状态,也不产生“已发送”记录。
建议前端按以下逻辑落地(函数名可按现有工程调整):
```ts
async function onCopyItineraryLink(order: BoardOrder) {
const orderId = resolveBoardOrderId(order)
const detail = await getBoardOrderDetail(orderId)
const itineraryUrl = detail.currentAssignment?.itineraryUrl?.trim()
if (!itineraryUrl) {
message.error('行程单链接暂不可用,请确认已派车且 H5 配置正常')
return
}
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(itineraryUrl)
} else {
copyTextByTextarea(itineraryUrl)
}
message.success('行程单链接已复制,可发送给司机')
} catch {
message.error('复制失败,请稍后重试')
}
}
function copyTextByTextarea(text: string) {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.setAttribute('readonly', '')
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
const copied = document.execCommand('copy')
document.body.removeChild(textarea)
if (!copied) throw new Error('copy failed')
}
```
## 三、不影响范围
- `canRejectRequirement` 仍只在未派阶段可能为 `true`;排车中不得重新开放“驳回用车需求”。
- 已派车、已完成、已取消状态不会因本次变更开放派车入口。
- 无数据库、Redis、MQ、候选请求字段或错误码变更。
## 四、前端自测清单
- [ ] `holding` 订单显示“改派”按钮,点击后带出当前司机、车辆并进入调整流程。
- [ ] `holding_urgent` 同样显示“改派”并进入调整流程。
- [ ] `holding/holding_urgent + CHANGE_ASSIGNMENT` 卡片按钮显示“改派”,打开后直接高亮第 2 步“排车”,第 1 步为已完成。
- [ ] 首次待派车仍从第 1 步开始;`confirmHold` 仍恢复第 3/4 步,三种入口互不串态。
- [ ] `holding/holding_urgent` 在卡片、筛选、汇总和图例统一显示“待确认/待确认即将超时”,页面不再出现“排车中”旧文案。
- [ ] 技术状态值仍为 `holding`,改派、司机确认和超时判断不因文案变化而改变。
- [ ] 待确认订单在 `RECORD_DRIVER_CONFIRMATION` 可用时显示“继续派车”,点击以 `confirmHold` 恢复第 3/4 步。
- [ ] “继续派车”和“改派”同时存在且职责分离;前端不再检查不存在的 `CONFIRM` 动作码。
- [ ] 待确认或已派车且存在当前有效派单时,看板显示“复制行程单链接”。
- [ ] 点击后通过看板详情取得并复制 `currentAssignment.itineraryUrl`,不调用订单打印接口、不在前端拼接链接。
- [ ] HTTPS/localhost 使用 Clipboard API,HTTP 内网环境可通过降级方案正常复制。
- [ ] 复制成功提示“行程单链接已复制,可发送给司机”;链接为空或复制失败时给出明确错误且不误报成功。
- [ ] 复制出的链接可由司机在手机浏览器中独立打开,无需登录管理后台,并展示当前派单的实时行程。
- [ ] 看板不再使用 `ItinerarySendSheet` 或模拟“已发送行程单”,改派后不会复制旧派单链接。
- [ ] 打开排车步骤时,候选请求同时携带字符串 `orderId`、`requirementId` 和当前 `fleetItemIndex`,不再出现“改派候选查询必须携带当前订单ID、用车需求ID和车型项索引”。
- [ ] 提交时调用既有 `changeAssignment`,不调用创建派单接口。
- [ ] `assigned`、`completed`、`canceled` 不误显示入口。
- [ ] 调整成功后刷新看板,页面只保留一组当前有效派单。
## 验证证据
- 后端提交:`aaeb01242`;PR:[wx/HL#5190](https://git.1814.love:8443/wx/HL/pulls/5190),已合并 `dev-v3`。
- 状态矩阵、生命周期动作、改派上下文及排车中改派保护已有定向测试覆盖;`BoardOrderServiceTest` 与 `BoardControllerTest` 共 49 项通过。
- `mvn -f hl-fleet-service/pom.xml spotless:check` 通过。
- `mvn -pl hl-fleet-service -am verify` 通过。
- 测试环境 Fleet 滚动部署任务 `3458b3ab` 成功,8087、8187 两实例健康。
- 网关按团号 `26-7042` 验证:列表与详情 HTTP/code 200,`requirementId` 在列表、详情顶层、`currentAssignment` 和 `activeAssignments[]` 均存在;携带 `orderId + requirementId + fleetItemIndex + excludeAssignmentId` 调用候选接口 HTTP/code 200,返回 19 辆车、19 名司机候选。
- 当前前端 `v2.1` 已从 `order.requirementId` 组装候选请求;但截至 `v2.1@6e6a11bf`,`resolveAssignFlowRestoreState` 对 `reassign` 仍固定恢复第 1 步,且排车中入口文案仍为“派车派人”,需要按上方状态规则调整。
## 六、相关文档
- [wx/HL#5186](https://git.1814.love:8443/wx/HL/issues/5186)
@@ -0,0 +1,292 @@
---
schema: "hl-changelog/v2"
ticket: "5187"
title: "多车辆槽位原子批量派车与价格日历带价"
consumer: "admin"
change_type: "新增接口"
backend_status: "deployed"
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"
generated: "2026-07-23T15:38:00+08:00"
---
# 【新增接口·前端待处理·管理后台】多车辆槽位原子批量派车与价格日历带价
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 页面:车务管理 → 派车看板 → 派车派人弹窗、派单详情
- 前端交接:仅以本 `hl-api-changelog` 文档为准,不另建前端仓库工单。
- 小程序:无需处理
> **后端工单**: [wx/HL#5187](https://git.1814.love:8443/wx/HL/issues/5187)
>
> **后端 PR**: [wx/HL#5189](https://git.1814.love:8443/wx/HL/pulls/5189)
>
> **兼容性**: 既有单槽位 `POST /admin/fleet/assignments` 不变;多车订单必须改用本次批量接口,
> 前端不得循环调用单派接口。
## 一、业务口径
一条用车需求可能展开出多个车辆槽位。派车弹窗应按 `fleetItemIndex` 维护多组
“车辆 + 司机 + 协议价”,允许一次选择多辆车并一次提交。整批任一槽位失败时不得留下前面
已成功、后面失败的半批派单。
- 同一批内 `fleetItemIndex`、`vehicleId`、`driverId` 分别不可重复。
- 前端只允许选择当前需求实际展开出的待派槽位,不得自行增加超过需求数量的车辆。
- 雪花 ID 全程按字符串保存和提交。
- 同一次提交及其网络重试必须复用同一个 `requestId`;用户修改选择后主动再次提交应生成新值。
- `holdMode=1` 表示排车中等待司机确认,`holdMode=0` 表示直接派定;整批模式必须一致。
## 变更接口
```http
POST /admin/fleet/assignments/batch
Content-Type: application/json
```
请求示例:
```json
{
"orderId": "2046800000000000001",
"orderNo": "26-4165",
"requirementId": "2046800000000000101",
"startDate": "2026-07-28",
"endDate": "2026-07-30",
"pickupAt": "海拉尔",
"dropoffAt": "满洲里",
"headcount": 8,
"chargeableServiceDates": [
"2026-07-28",
"2026-07-29",
"2026-07-30"
],
"holdMode": 1,
"skipCityJunctionException": false,
"fromEntry": "from-board",
"requestId": "fleet-batch-7fe5c3a8",
"items": [
{
"fleetItemIndex": 0,
"vehicleId": "2046800000000000201",
"driverId": "2046800000000000301",
"protocolPrice": "520.00",
"confirmCrossResident": false
},
{
"fleetItemIndex": 1,
"vehicleId": "2046800000000000202",
"driverId": "2046800000000000302",
"protocolPrice": "860.00",
"confirmCrossResident": false
}
]
}
```
### 公共字段
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | ---: | --- |
| `orderId` | String(Long) | 是 | 订单 ID |
| `orderNo` | String | 否 | 订单号冗余 |
| `requirementId` | String(Long) | 是 | 当前生效用车需求 ID |
| `startDate` / `endDate` | LocalDate | 是 | 整批服务日期闭区间 |
| `pickupAt` / `dropoffAt` | String | 否 | 接送地 |
| `headcount` | Integer | 否 | 乘客人数 |
| `chargeableServiceDates` | LocalDate[] | 否 | 不传=全部计费;空数组=全部免费 |
| `vehicleFeeWaiverReason` | String | 条件必填 | 存在免费服务日时填写 |
| `confirmAllServiceDatesFree` | Boolean | 条件必填 | 全部免费时必须为 `true` |
| `holdMode` | Integer | 是 | `1=排车中`,`0=直接派定` |
| `skipCityJunctionException` | Boolean | 否 | 与单派接口同义 |
| `fromEntry` | String | 否 | 操作来源 |
| `requestId` | String | 是 | 批次幂等键,最大 64 字符 |
| `items` | Object[] | 是 | 1-20 个车辆槽位 |
### `items[]`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | ---: | --- |
| `fleetItemIndex` | Integer | 是 | 当前需求展开后的槽位序号,0 起 |
| `vehicleId` | String(Long) | 是 | 所选车辆 ID,批内不可重复 |
| `driverId` | String(Long) | 是 | 所选司机 ID,批内不可重复 |
| `protocolPrice` | String(BigDecimal) | 否 | 元/车天;不传时后端按车型价格日历兜底 |
| `messageTemplateId` | String(Long) | 否 | `holdMode=1` 的通知模板 |
| `customBody` | String | 否 | `holdMode=1` 的本次自定义通知正文 |
| `confirmCrossResident` | Boolean | 否 | 跨常驻车辆组合的显式确认 |
成功响应按 `fleetItemIndex` 升序返回:
```json
{
"code": 200,
"data": {
"assignments": [
{
"fleetItemIndex": 0,
"assignment": {
"id": "2046800000000000401",
"assignmentGroupId": "2046800000000000501",
"assignmentSlotId": "2046800000000000601",
"assignmentStatus": "holding",
"stageCode": "holding_wait_driver",
"stageLabel": "排车中·等待司机确认",
"currentStep": 2,
"skippedStepCodes": [],
"protocolPrice": "520.00",
"holdSentAt": null,
"confirmedAt": null,
"sideEffects": null,
"dailyDifferences": null
}
}
],
"failedFleetItemIndex": null,
"dailyDifferences": null
},
"message": "成功",
"success": true
}
```
直接派定发生订单/行程/需求冻结基线不一致时返回既有业务码 `605041`,并额外指出失败槽位:
```json
{
"code": 605041,
"data": {
"assignments": [],
"failedFleetItemIndex": 1,
"dailyDifferences": [
{
"serviceDate": "2026-07-29",
"differenceType": "CAPACITY_INSUFFICIENT",
"message": "逐日车辆可用座位不足"
}
]
}
}
```
无论返回哪一种失败,整批均不产生部分成功数据。前端失败后保留用户当前选择并展示后端文案;
`605041` 可同时高亮 `failedFleetItemIndex` 对应槽位及逐日差异。
## 三、派车弹窗前端修改
### 3.1 多车辆选择
当前实现只有全局单值 `selVehicle/selDriver`,再次选择会覆盖上一辆车。需改成按
`fleetItemIndex` 保存的槽位数组或 Map:
```text
selectedSlots[fleetItemIndex] = {
vehicle,
driver,
protocolPrice,
confirmCrossResident
}
```
- 点击某个候选车辆只修改当前待选槽位,不清空其他已选槽位。
- 已选摘要、取消车辆、司机选择和常驻组合提示均必须作用于对应槽位。
- 提交前校验所有本次待派槽位都有车辆和司机,然后一次调用批量接口。
- 禁止用 `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 车型价格日历自动带价
候选接口 `vehicles[].protocolPrice` 已返回所选车辆车型在服务开始日的价格日历单价。当前页面
只从订单级 `props.order.protocolPrice` 初始化输入框,导致价格日历明明有值仍显示空。
- 选中车辆时,把该车辆的 `protocolPrice` 写入对应槽位价格框。
- 每辆车独立显示、独立可编辑,提交到 `items[].protocolPrice`。
- 切换车辆时改为新车辆的价格日历值;不能沿用上一辆车的价格。
- 候选值为空时输入框可留空,后端仍会在最终保存时按所选车辆车型 + `startDate` 再兜底一次。
- 不得把一个全局价格复制给所有不同车型。
### 3.3 联系定制师
派车看板卡片已有“联系定制师”,派单详情第 1 步和后续派车弹窗也应与房务详情保持一致:
- 在详情可见区域补“联系定制师”按钮,复用现有 `open-fleet` 会话流程。
- 订单 ID 使用数字雪花字符串,不能传 `HL...` 展示号或团号。
- 按钮位置、图标、禁用态、加载态和聊天抽屉交互复用房务模块,不另做一套样式。
- 首次打开真实会话和实时未读角标仍按
[#5180 前端交接](./23_5180_订单详情联系车务独立未读红点-修改接口-管理后台.md)处理。
## 四、派车看板默认状态筛选
这是前端初始化逻辑修复,不需要后端接口变更:
- `statusSel` 初始值必须为 `[]`,页面首次进入状态框显示空/不限。
- 首次列表请求不得携带 `statuses=unassigned`,默认展示全部状态。
- 点击“重置”后的值和首次进入完全一致。
- 用户主动选择“待派车”后才传对应状态;刷新筛选结果时不得偷偷恢复默认待派车。
## 五、前端验收清单
- [ ] 一条需求展开 2 个车辆槽位时,可同时选择 2 辆不同车辆和 2 名不同司机,第一辆不会被第二辆覆盖。
- [ ] 提交只发送 1 次 `/admin/fleet/assignments/batch`,不循环调用旧单派接口。
- [ ] 第二槽位失败时页面提示失败,刷新后两个槽位都没有半批残留。
- [ ] 价格日历有值时,选择每辆车后各自价格框立即带出对应 `protocolPrice`。
- [ ] 修改某辆车价格只影响该槽位,成功响应按槽位回显冻结价格。
- [ ] 派单详情第 1 步和派车流程均能直接“联系定制师”,交互与房务一致。
- [ ] 派车看板首次进入状态筛选为空,首次请求不传 `statuses`,默认可见全部状态。
- [ ] 主动筛选“待派车”及重置行为正确。
- [ ] 增加多槽位状态管理、批量请求映射、车型切换带价和默认空筛选的组件/组合式函数测试。
## 验证证据
- `mvn -pl hl-fleet-service spotless:check` 通过。
- `AssignmentControllerTest + AssignmentServiceTest`:281 项通过。
- `mvn -pl hl-fleet-service -am verify` 通过:fleet 2334 项,0 failure / 0 error,1 skipped。
- 批量成功、空明细校验、重复槽位校验、字符串雪花 ID、直接派定基线差异及事务/幂等注解均有测试覆盖。
- 测试环境部署任务 `28f9048a` 成功,`hl-fleet-service` 两个滚动实例均恢复健康。
- 经测试环境网关验证:
- 派车看板列表请求返回 HTTP 200 / 业务码 200。
- 批量接口空明细返回业务码 400,文案为“派单车辆槽位不能为空”。
- 批量接口重复 `fleetItemIndex` 返回业务码 100001,且未产生写入。
- 自建并标记测试订单,使用 SUV + MPV 两个槽位执行失败探针:第一槽位合法、第二槽位车辆不存在,
接口返回 605001;随后详情仍为 0 个有效派单,证明第一槽位及副作用意图随整批回滚。
- 同一测试订单使用两个合法槽位执行成功探针:接口返回 200,结果按
`fleetItemIndex=[0,1]` 排序,价格快照分别为 `700.00`、`860.00`,详情恰有 2 个
`assigned` 派单。
- 使用相同 `requestId` 重放返回业务码 100502;重放后详情仍恰有 2 个有效派单,无重复写入。
- 验收后已通过订单取消 API 精确清理自建测试订单,订单状态为 `CANCELLED`,详情有效派单恢复为 0。
- 部署后 fleet 服务与网关日志未发现 ERROR;重复槽位探针只产生预期的业务校验 WARN。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,184 @@
---
schema: "hl-changelog/v1"
ticket: "5193"
title: "用车需求增加独立接机送机选择"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "claimed"
frontend_status: "claimed"
frontend_owner: "hl-ui-codex"
frontend_ref: ""
updated_at: "2026-07-25T03:19:00.677Z"
base: "dev-v3"
generated: "2026-07-23T17:39:06+08:00"
---
# 【修改接口·前端待处理·管理后台】用车需求增加独立接机送机选择
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-order-service-v3、hl-fleet-service
>
> **工单**: [wx/HL#5193](https://git.1814.love:8443/wx/HL/issues/5193)
>
> **影响范围**: 提交/调整用车需求、订单详情用车摘要、车务派单详情
## 业务口径
“是否需要平台接送”拆分为两个相互独立的业务选择:
- `pickupRequired`:是否需要平台接机/接站;
- `dropoffRequired`:是否需要平台送机/送站。
提交用车需求时两个选项默认都选中,即默认都为 `true`。定制师可以分别取消,支持四种组合。接机与送机选择是用车需求本身的明确口径,不从航班、站点、接送时间或大交通批次推断。
## 一、提交/调整用车需求
### 1. 直接提交或修改
`PUT /v3/admin/order/{id}/vehicle-requirement`
请求体新增:
```json
{
"fleet": [
{
"vehicleType": "SUV",
"seats": 7,
"count": 1
}
],
"pickupRequired": true,
"dropoffRequired": true,
"specialTags": ["中文司机"],
"remark": "司机会蒙语"
}
```
### 2. 调整订单
`POST /v3/admin/order/{id}/adjustment/submit`
`updates.vehicleRequirement` 新增相同字段:
```json
{
"updates": {
"vehicleRequirement": {
"fleet": [
{
"vehicleType": "SUV",
"seats": 7,
"count": 1
}
],
"pickupRequired": false,
"dropoffRequired": true,
"specialTags": [],
"remark": ""
}
}
}
```
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `pickupRequired` | `Boolean` | 前端应显式提交 | `true` 需要接机/接站,`false` 不需要 |
| `dropoffRequired` | `Boolean` | 前端应显式提交 | `true` 需要送机/送站,`false` 不需要 |
兼容规则:
- 首次提交缺少字段时,后端按 `true` 保存;
- 修改或调整既有需求时缺少字段,后端继承当前有效需求的原值;
- 前端不要依赖兼容兜底,提交时始终显式发送两个字段。
## 二、前端表单
在“车辆安排/提交用车需求”表单中增加两个独立开关或复选框:
- 是否需要接机/接站;
- 是否需要送机/送站。
交互要求:
- 新建用车需求时两个选项默认选中;
- 编辑或调整时以接口回显值为准,不要每次强制重置为选中;
- 两个选项均可独立取消,不做互斥或联动;
- 不根据有没有大交通、航班时间或站点决定勾选状态。
## 三、回显接口
以下响应均新增 `pickupRequired` 和 `dropoffRequired`,用于无损回显:
- 用车需求提交/修改响应;
- `GET /v3/admin/order/{id}/adjustment/snapshot` 的 `data.vehicleRequirement`;
- `GET /v3/admin/order/{id}/itinerary` 的 `data.vehicleGroup.requirement`;
- order-v3 内部用车需求契约。
示例:
```json
{
"pickupRequired": false,
"dropoffRequired": true
}
```
## 四、车务派单详情
`GET /admin/fleet/board/orders/{orderId}` 的 `data.transport` 新增/明确返回:
```json
{
"pickupRequired": false,
"dropoffRequired": true
}
```
这两个值来自订单当前有效用车需求,是车务详情页展示的权威值。前端不得再用抵达/返程班次、站点、接送时间或批次记录推断。
页面分别显示:
| 字段值 | 接机标签 | 送机标签 |
| --- | --- | --- |
| `true` | 需要平台接机 | 需要平台送机 |
| `false` | 无需平台接机 | 无需平台送机 |
不要再合并显示单个“需要平台接送”标签。滚动部署期间若字段暂为 `null`,可以暂不显示对应标签;部署完成后现有历史需求会按默认值返回 `true`。
## 五、前端处理清单
- [ ] 提交用车需求表单增加“是否需要接机/接站”和“是否需要送机/送站”。
- [ ] 新建时两个选项默认选中,提交时显式发送两个 Boolean。
- [ ] 调整订单预填读取 `vehicleRequirement.pickupRequired/dropoffRequired`,不覆盖既有值。
- [ ] 订单详情用车需求摘要按两个字段分别展示。
- [ ] 车务派单详情按 `transport.pickupRequired/dropoffRequired` 分别展示接机、送机标签。
- [ ] 不再展示单个“需要平台接送”,不根据大交通信息反推。
- [ ] 覆盖需要/不需要的四种组合及旧需求默认双 `true`。
## 六、不影响范围
- 大交通录入中的批次级 `pickupRequired` 继续表示该批次自身是否接送,本次不修改其录入和计算逻辑。
- 接送班次、站点、时间和备注字段结构不变。
- 车型、座位数、数量、特殊诉求和备注提交结构不变。
## 七、验证证据
- 后端提交:`2112396b1`;PR:[wx/HL#5197](https://git.1814.love:8443/wx/HL/pulls/5197),已合并 `dev-v3`(合并提交 `f9e05fbe4`)。
- 用车需求默认值、显式选择、版本继承、调整透传、订单详情回显及 order-v3 → fleet 契约均有单元测试覆盖。
- order-v3 定向测试 311 项、fleet board 定向测试 49 项通过。
- `mvn -pl hl-order-service-v3 -am verify`、`mvn -pl hl-fleet-service -am verify` 和 fleet `spotless:check` 全部通过。
- 测试环境滚动部署成功:order-v3 任务 `af6ff925`,8086/8186 两实例健康;fleet 任务 `e11bca29`,8087/8187 两实例健康。
- 以 `admin` 车务经网关查询团号 `26-8550` 的派车详情,HTTP/code 200,`transport.pickupRequired=true`、`transport.dropoffRequired=true`。
- 以 `wx` 定制师经网关查询同订单调整预填,HTTP/code 200,`vehicleRequirement.pickupRequired=true`、`vehicleRequirement.dropoffRequired=true`。
- 测试库只读核验:两列均为 `NOT NULL DEFAULT 1`,该历史活动需求已迁移为 `1/1`。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`;前端按“前端处理清单”接入即可。
@@ -0,0 +1,230 @@
---
schema: "hl-changelog/v1"
ticket: "5194"
title: "待确认详情补全多车多司机与按槽位改派"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-23T18:02:00+08:00"
---
# 【修改接口·前端待处理·管理后台】待确认详情补全多车多司机与按槽位改派
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-fleet-service
>
> **工单**: [wx/HL#5194](https://git.1814.love:8443/wx/HL/issues/5194)
>
> **后端 PR**: [wx/HL#5196](https://git.1814.love:8443/wx/HL/pulls/5196)
>
> **影响范围**: 派车看板状态、派单弹窗改派、司机待确认页
## 关键业务口径
1. `holding` 落库状态不变,但等待司机真实回复时,页面展示文案统一为“待确认”,不得再显示“排车中”。
2. 一张订单可以同时存在多个有效车辆槽位,每个槽位有独立车辆、司机和确认阶段。前端必须遍历 `activeAssignments`,不能只读兼容字段 `currentAssignment`。
3. 改派以选中的稳定槽位为单位。两辆车中只改一辆时,只提交目标槽位对应的 `activeAssignments[i].id`;其他槽位不取消、不重建、不改变。
4. 改派界面必须先展示历史车辆/司机,并要求车务人员在界面中明确清除目标槽位的旧选择后才能选择新车/新司机。
5. “清除旧选择”只修改前端草稿状态,**不得先调用取消派单接口**。最终一次调用 `change` 原子替换;用户关闭弹窗时后端原派单保持不变。
6. 多车待确认页按槽位分别登记司机回复。只要任一有效 HOLD 槽位未收到司机回复,订单顶部整体步骤仍停在“待确认”。
## 一、派单详情
```http
GET /admin/fleet/board/orders/{orderId}
```
### `activeAssignments[]` 完整字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | `String` | 当前有效派车组锚点 ID;改派、登记司机确认、最终确认均使用该值 |
| `assignmentGroupId` | `String` | 当前派车组 ID;改派后会生成新值 |
| `assignmentSlotId` | `String` | 稳定车辆槽位 ID;同一槽位改派前后保持不变 |
| `fleetItemIndex` | `Integer` | 用车需求项序号 |
| `requiredVehicleType` | `String` | 需求车型 |
| `requiredSeats` | `Integer` | 需求座位数 |
| `vehicleId` | `String` | 当前车辆 ID |
| `vehiclePlate` | `String` | 当前车牌 |
| `vehicleModel` | `String` | 当前车型;优先派车冻结快照,缺失时回填车辆档案 |
| `vehicleSeats` | `Integer` | 当前车辆座位数 |
| `vehicleFleetTeamId` | `String` | 当前车辆所属车队 ID |
| `vehicleFleetTeamName` | `String` | 当前车辆所属车队名称 |
| `driverId` | `String` | 当前司机 ID |
| `driverName` | `String` | 当前司机姓名 |
| `driverPhone` | `String` | 当前司机脱敏手机号,例如 `138****1234` |
| `assignmentStatus` | `String` | 派生态 |
| `assignmentStatusLabel` | `String` | 后端统一展示文案 |
| `lifecycleStageCode` | `String` | 当前槽位生命周期阶段 |
| `lifecycleStageLabel` | `String` | 当前槽位阶段文案 |
| `currentStep` | `Integer` | 当前槽位所在步骤 |
| `availableActionCodes` | `String[]` | 当前槽位允许操作 |
| `driverConfirmedAt` | `LocalDateTime/null` | 本槽位司机确认时间 |
示例(字段已脱敏):
```json
{
"currentAssignment": {
"id": "2079502431745396738",
"assignmentSlotId": "2079502431745396738"
},
"activeAssignments": [
{
"id": "2079502431745396738",
"assignmentGroupId": "2079502431745396738",
"assignmentSlotId": "2079502431745396738",
"fleetItemIndex": 0,
"vehicleId": "2079857985374363650",
"vehiclePlate": "蒙A-T1557",
"vehicleModel": "丰田普拉多",
"vehicleSeats": 7,
"vehicleFleetTeamId": "2079857981112934401",
"vehicleFleetTeamName": "合作车队A",
"driverId": "2079857983403024385",
"driverName": "司机姓名",
"driverPhone": "199****1557",
"baseAssignmentStatus": "holding",
"assignmentStatus": "holding",
"assignmentStatusLabel": "待确认",
"lifecycleStageCode": "holding_wait_driver",
"lifecycleStageLabel": "待确认",
"currentStep": 3
}
]
}
```
### 兼容与聚合规则
- `currentAssignment` 仍返回“最新有效派车组”,仅用于兼容旧版单车页面;新页面不得据此判断订单只有一辆车。
- `activeAssignments` 只含有效 `holding/assigned` 派车组,按需求项、服务日期、派车组 ID 稳定排序。
- `progressSteps` 是订单整体步骤,多车时按最慢有效槽位聚合。
- 每辆车的实际阶段以对应 `activeAssignments[i].lifecycleStageCode/currentStep` 为准。
- 老异常数据若车辆档案或司机电话确实缺失,对应字段可能为 `null`;页面显示 `-`,不得导致整页报错。
## 二、按目标槽位改派
```http
POST /admin/fleet/assignments/{assignmentId}/change
```
`assignmentId` 必须使用车务人员选中的 `activeAssignments[i].id`,不要使用订单 ID,也不要默认使用 `currentAssignment.id`。
请求示例:
```json
{
"effectiveDate": "2026-07-29",
"newVehicleId": "2079857985374363999",
"newDriverId": "2079857983403024999",
"holdMode": 1,
"messageTemplateId": "2073978002412105729",
"protocolPrice": 700.00,
"reason": "替换第 2 个车辆槽位",
"requestId": "change-slot-20260723-001"
}
```
原子替换成功响应会明确返回稳定槽位和其他未改车辆:
```json
{
"code": 200,
"data": {
"assignmentId": "新的有效派单锚点ID",
"assignmentSlotId": "改派前后不变的槽位ID",
"previousAssignmentGroupId": "被替换的旧派车组ID",
"newAssignmentGroupId": "新派车组ID",
"assignmentStatus": "holding",
"effectiveDate": "2026-07-29",
"affectedDays": 3,
"otherVehicleCount": 1,
"warningCode": "ORDER_HAS_OTHER_VEHICLES",
"otherVehicles": [
{
"assignmentSlotId": "未改车辆槽位ID",
"vehiclePlate": "蒙A-U1557",
"driverName": "另一位司机",
"startDate": "2026-07-29",
"endDate": "2026-07-31"
}
]
},
"success": true
}
```
### 改派页面正确流程
1. 打开改派时用 `activeAssignments` 渲染全部现有槽位卡片,显示车牌、车型、座位、司机姓名、脱敏电话。
2. 用户先选择要替换的槽位;两车订单不得自动选“最新一辆”代替用户决定。
3. 目标槽位显示“清除当前车辆/司机”。用户明确点击后,只清空本地候选草稿并解锁新车/新司机选择。
4. 未清除目标槽位前禁用候选选择和提交;其他槽位仍只读展示,不跟随清空。
5. 提交时只调用一次目标 `id` 的 `change`。禁止先 `DELETE /assignments/{id}`,也禁止先调用 `driver-reject`。
6. 成功后重新请求订单详情,用新的 `activeAssignments` 替换页面状态;不要在前端自行拼接新旧派车组。
7. `warningCode=ORDER_HAS_OTHER_VEHICLES` 是“还有其他车辆保持不变”的强提示,不是失败,不得继续批量改派其他槽位。
## 三、多司机待确认页
司机待确认页必须遍历 `activeAssignments`,每个槽位至少显示:
- 车型、车牌、座位数;
- 司机姓名、脱敏手机号;
- 当前阶段文案;
- 本槽位的通知模板预览、司机回复摘要、确认凭证和操作按钮。
每名司机独立调用:
```http
POST /admin/fleet/assignments/{activeAssignments[i].id}/driver-confirmation
POST /admin/fleet/assignments/{activeAssignments[i].id}/confirm
```
不得因为其中一名司机已确认,就把其他仍待回复的司机一起标记为已确认。
## 四、模板渲染的 `canceled` 处理
测试网关已验证真实 `hold_notify` 模板列表和 `render` 接口均为 200,订单 26-7042 的模板正文可正常渲染。页面出现原样英文 `canceled` 是前端取消旧请求被当成业务错误展示,不是后端模板错误。
前端必须:
1. 模板、车辆或司机快速切换时允许取消旧 render 请求。
2. 对 Axios `CanceledError`、`ERR_CANCELED` 或项目统一的取消请求判定静默处理,不弹错误条、不清空最后一次成功预览。
3. 只采纳最新一次请求的响应;较早请求即使后返回也不得覆盖新预览。
4. 真正的 HTTP/业务错误才显示“模板加载失败”,并保留“重试加载”。
5. 禁止把异常对象的 `message`(例如 `canceled`)直接展示给用户。
## 五、前端处理清单
- [ ] 看板卡片对 `holding_wait_driver` 展示“待确认”,不再硬编码“排车中”。
- [ ] 派单详情、改派和待确认页全部遍历 `activeAssignments`,不再只读 `currentAssignment`。
- [ ] 每个槽位显示车型、车牌、座位、司机姓名和脱敏手机号。
- [ ] 多车顶部步骤使用后端 `progressSteps`,每车状态使用本项生命周期字段。
- [ ] 改派前展示全部现有槽位,由车务人员明确选择目标槽位。
- [ ] 目标槽位必须先在界面中手动清除旧选择,才允许重新选择车辆/司机。
- [ ] 清除操作仅修改前端草稿,不调用取消或退回接口;最终只调用一次目标槽位的 `change`。
- [ ] 两车只换一车时,另一槽位保持原样;成功后重新拉取详情。
- [ ] 每名司机分别登记确认和最终确认,不能用一个槽位状态覆盖全部司机。
- [ ] render 请求取消时静默处理,不再显示原样英文 `canceled`。
## 六、验证证据
- 后端:fleet 及依赖模块全量 `verify` 成功,2,336 个测试 0 失败、1 个既有跳过;`spotless:check` 通过。
- 按槽位改派测试:验证替换目标槽位时不会调用其他槽位的取消语句。
- 多车测试:验证完整返回两个稳定槽位及车辆/司机字段;任一司机未回复时整体仍停在待确认。
- 部署:测试环境部署任务 `6679d69c` 成功,8087/8187 双实例滚动发布并通过健康检查。
- 网关:订单 26-7042 返回 `assignmentStatus=holding`、`assignmentStatusLabel=待确认`、`lifecycleStageCode=holding_wait_driver`、`lifecycleStageLabel=待确认`。
- 网关:订单 26-7042 的 `activeAssignments` 已返回稳定槽位、车型、座位、司机和脱敏手机号;真实模板 render 正文长度 240,未出现后端 `canceled` 错误。
- 当前测试环境看板前 99 个唯一订单没有多有效派车样本,因此多车网关展示不能靠现存业务数据复验,后端多车契约由自动化测试覆盖。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,110 @@
---
schema: "hl-changelog/v1"
ticket: "5199"
title: "车务首页订单去重与字段补全"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-24T09:46:00+08:00"
---
# 【修改接口·前端待处理·管理后台】车务首页订单去重与字段补全
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-fleet-service、hl-user-service
>
> **工单**: [wx/HL#5199](https://git.1814.love:8443/wx/HL/issues/5199)
>
> **影响范围**: 车务角色首页的待安排车辆统计、即将用车订单表格和操作列
## 关键变化
`GET /admin/profile/dashboard?period=today` 在当前角色为 `VEHICLE_MANAGER` 时,`data.upcomingTrips` 的口径调整为:
- 只返回近 7 天仍存在未完成派车槽位的订单,未完成态为 `unassigned/unassigned_urgent/holding/holding_urgent`;
- 同一订单的多个车型、车辆或派车槽位按 `orderId` 合并为一行;
- 返回符合条件的完整订单集合,不再固定截断为 5 条;
- 全部派车槽位均已进入 `assigned` 的订单不再出现在待处理列表。
派车看板 `/admin/fleet/board/orders` 仍保持派车槽位维度和原分页规则,本次不修改。
## 响应字段
`data.upcomingTrips[]` 完整结构:
```json
{
"orderId": "70123456789",
"assignmentId": "80123456789",
"orderNo": "HL202607010001",
"teamNo": "26-7218",
"productName": "呼伦贝尔 6 日",
"customerName": "张先生",
"contactName": "张先生",
"plannerName": "苏日娜",
"consultantName": "苏日娜",
"consultantDisplayName": "苏日娜",
"departureDate": "2026-07-10",
"endDate": "2026-07-15",
"headcount": 4,
"status": "unassigned_urgent",
"statusLabel": "待派车",
"urgentBadge": "T-1",
"canAssign": true,
"canRejectRequirement": true
}
```
本次补齐的字段:
| 字段 | 类型 | 页面用途 |
| --- | --- | --- |
| `teamNo` | `String/null` | 团号列 |
| `contactName` | `String/null` | 联系人列,当前与 `customerName` 同值 |
| `plannerName` | `String/null` | 定制师兼容字段 |
| `consultantName` | `String/null` | 定制师兼容字段 |
| `consultantDisplayName` | `String/null` | 定制师优先展示字段 |
| `canAssign` | `Boolean` | 是否显示派车/调整入口 |
| `canRejectRequirement` | `Boolean` | 是否显示驳回需求入口 |
`orderId` 和 `assignmentId` 继续按字符串处理,不得转为 JavaScript `Number`。同订单存在多个未完成派车槽位时,`assignmentId/status/statusLabel/urgentBadge/操作权限` 来自后端排序最靠前的代表槽位。
## 前端处理清单
- [ ] 表格行以 `orderId` 为订单级唯一键,不按 `assignmentId` 重复渲染。
- [ ] 不在前端截取前 5 条,也不再自行做派车槽位去重。
- [ ] 团号列读取 `teamNo`。
- [ ] 定制师列优先读取 `consultantDisplayName`,为空时可回退 `consultantName/plannerName`。
- [ ] 操作列按 `canAssign/canRejectRequirement` 显示已有操作入口,不根据中文状态文案推断。
- [ ] 覆盖同订单多车型、多车、超过 5 个未完成订单、部分槽位已派和全部槽位已派场景。
## 不影响范围
- 不修改 `pendingArrangeVehicle` 字段名。
- 不修改派车看板、矩阵派单、车辆列表或司机列表的分页和展示维度。
- 不新增前端路由、权限码或请求参数。
## 后端验证
- 后端 PR:[wx/HL#5201](https://git.1814.love:8443/wx/HL/pulls/5201)。
- `FleetDashboardSummaryServiceTest` 覆盖 8 条派车槽位聚合为 6 个未完成订单、重复订单去重和 `assigned` 过滤。
- `LogisticsDashboardServiceTest` 覆盖团号、联系人、定制师与操作权限字段透传及 Feign 降级空态。
- `hl-fleet-service` 测试环境滚动部署任务 `0944b306` 成功,8087/8187 双实例健康。
- `hl-user-service` 测试环境滚动部署任务 `e306a054` 成功,8081/8181 双实例健康。
- PR 合并后以 `dev-v3` 再次滚动部署,fleet 任务 `d914f3fb`、user 任务 `3fde7f12` 均成功,
部署仓库 HEAD 包含合并提交 `bab22a096d`。
- 经网关请求 `GET /admin/profile/dashboard?period=today` 返回 HTTP 200、业务码 200;`upcomingTrips`
与相同日期和状态条件下的派单看板订单集合一致,共 4 个唯一订单,重复订单数 0,无已完成订单混入。
- `teamNo/contactName/plannerName/consultantName/consultantDisplayName/canAssign/canRejectRequirement`
在 4 条记录中均存在且均有有效值;user-service 与 gateway 近期日志未发现本次调用异常。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,150 @@
---
schema: "hl-changelog/v1"
ticket: "5200"
title: "用车需求驳回历史与重新提交"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-24T11:05:00+08:00"
---
# 【修改接口·前端待处理·管理后台】用车需求驳回历史与重新提交
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-order-service-v3、hl-fleet-service
>
> **工单**: [wx/HL#5200](https://git.1814.love:8443/wx/HL/issues/5200)
>
> **影响范围**: 订单详情行程安排中的用车需求、驳回后的重新提交
## 业务口径
- 用车需求被车务驳回后,旧版本终态失活并作为只读历史保留,不是“已回配”。
- 驳回后没有当前 active 用车需求,订单详情允许定制师重新提交。
- 重新提交创建新的 active 版本并重新进入车务流程,不复活或覆盖旧版本。
- 新旧版本可以同屏展示:当前版本保留原有操作,历史版本只读。
## 一、订单详情行程安排
接口:
```http
GET /v3/admin/order/{orderId}/itinerary
```
响应 `data` 新增:
```json
{
"vehicleGroup": null,
"vehicleHistory": [
{
"requirementId": "2080186927616606210",
"version": 1,
"status": "REJECTED_TO_CONSULTANT",
"isActive": false,
"submittedAt": "2026-07-23 15:03:06",
"returnedAt": "2026-07-24 09:20:00",
"returnRemark": "当地无合适车辆",
"vehicleTypeSummary": "suv×1",
"specialTags": ["儿童安全座椅", "大行李空间"],
"pickupRequired": true,
"dropoffRequired": true,
"remark": "原需求备注"
}
],
"canContactFleet": false,
"contactFleetDisabledReason": "请先提交有效用车需求后再联系车务"
}
```
### `vehicleHistory` 字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `requirementId` | `String` | 历史需求 ID |
| `version` | `Integer` | 版本号,列表按版本倒序 |
| `status` | `String` | 驳回场景为 `REJECTED_TO_CONSULTANT` 或 `REJECTED_TO_ADMIN` |
| `isActive` | `Boolean` | 历史项固定为 `false` |
| `submittedAt` | `LocalDateTime` | 原需求提交时间 |
| `returnedAt` | `LocalDateTime/null` | 驳回时间 |
| `returnRemark` | `String/null` | 驳回原因 |
| 其余摘要字段 | 与 `vehicleGroup.requirement` 相同 | 车型、座位、接送、特殊诉求和备注等原需求快照 |
边界行为:
- 从未提交用车需求:`vehicleGroup=null`、`vehicleHistory=[]`。
- 已驳回且尚未重提:`vehicleGroup=null`、`vehicleHistory` 包含驳回历史。
- 已重新提交:`vehicleGroup.requirement` 是新 active 版本,`vehicleHistory` 仍包含旧版本。
- 历史列表还可能包含被正常重版替换的失活版本,前端按 `status` 决定是否展示“已驳回”。
## 二、重新提交
继续使用现有接口:
```http
PUT /v3/admin/order/{orderId}/vehicle-requirement
```
驳回后提交的返回语义:
```json
{
"code": 200,
"data": {
"version": 2,
"isActive": true,
"status": "PENDING",
"branchTaken": "INIT_SUBMIT"
}
}
```
版本规则:
- 新版本号 = 历史最高版本号 + 1;
- 新版本为当前 active 需求;
- 旧驳回版本继续留在 `vehicleHistory`;
- 仅新版本进入车务看板和派单流程。
## 三、前端处理清单
- [ ] 订单详情用车卡片固定支持“历史需求”只读区域,不论当前 active 需求是否存在。
- [ ] 历史项展示版本、原需求内容、驳回状态、`returnedAt` 和 `returnRemark`。
- [ ] 历史项不得显示修改、联系车务、派车或“已回配”等当前需求操作/文案。
- [ ] `vehicleGroup=null` 且 `vehicleHistory` 非空时,继续显示“提交用车需求”入口。
- [ ] 有新 `vehicleGroup.requirement` 时,同时展示当前需求和旧历史,历史项不覆盖当前状态。
- [ ] 不要把 `vehicleHistory` 项映射成当前 `vehicleGroup.requirement`。
- [ ] 覆盖驳回未重提、驳回后重提、存在多个历史版本三个场景。
## 四、兼容说明
现有前端在 `vehicleGroup=null` 时已能进入“提交用车需求”空态,因此后端部署后不会再把驳回需求误显示成“已回配”。新增历史区域需要前端按上方清单接入;未接入时只是暂不展示历史内容,不影响重新提交。
## 五、后端验证
- 驳回 CAS 原子更新 `status`、`is_active=false`、驳回原因/时间并清空接单人。
- Fleet Outbox 按“订单需求驳回成功 → 取消未派占位”的顺序执行;重放已失活驳回历史时幂等成功。
- 无 active 需求时按历史最高版本递增,兼容旧 active rejected 数据。
- 订单/Fleet 相关定向测试累计 658 项通过。
- `mvn -pl hl-order-service-v3 -am verify`、`mvn -pl hl-fleet-service -am verify` 和 fleet `spotless:check` 全部通过。
- 后端 PR [wx/HL#5206](https://git.1814.love:8443/wx/HL/pulls/5206) 已合并到
`dev-v3@c13a035d0`;测试环境滚动部署任务 `a2a5a9fa` 成功,8086/8186 两实例健康。
- 网关以 `wx` 验证订单 `26-9919`:驳回迁移后 `vehicleGroup=null`,
`vehicleHistory` 返回 v1、`REJECTED_TO_CONSULTANT`、`isActive=false` 及原驳回原因;
重新提交返回 v2、`PENDING`,再次查询同时保留 v1 历史和 v2 当前需求。
- 测试库核对:v1 已失活且保留,v2 为唯一 active;Fleet 为 v2 生成 6 条
`unassigned` 日切片,旧 v1 的 6 条派单保持 `canceled`。
- 异常团号 `26-4165` 的 3 条旧派单均已取消并软删除,0 条可见、0 条在途;
对应失败 Outbox 已进入 `QUARANTINED`,网关看板按团号搜索返回 0 条。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,136 @@
---
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@b619849eb3c71f2e466dec4539f577213c060db0"
updated_at: "2026-07-25T03:25:59.312Z"
---
# 调整订单行程节点时间回显与修改(修改接口)
> 日期:2026-07-24
> 工单:#5202
> 服务:`hl-order-service-v3`
> 前端状态:待处理(`D:/work2/hl-ui` 本次未修改)
## 结论
产品行程节点的两类时间在下单时均已固化到订单行程节点:
- `startTime`:精确开始时间,格式 `HH:mm`。
- `timePeriod`:时间说明,值来自 `itinerary_time_period` 字典,例如 `MORNING`。
本次补齐“调整订单 → 行程”中的完整读写契约:
- 快照逐节点返回 `startTime`、`timePeriod`。
- 调整提交可只修改时间,不要求同时改价或改数量。
- 新增节点也可携带两类时间。
- 两字段可分别存在,不强制互斥。
- 旧订单或未设置时间的节点返回 `null`。
## 涉及接口
| 方法 | 路径 | 变化 |
|---|---|---|
| GET | `/v3/admin/order/{orderId}/adjustment/snapshot` | `data.itinerary.days[].nodes[]` 补齐 `timePeriod`,保留已有 `startTime` |
| POST | `/v3/admin/order/{orderId}/adjustment/submit` | `updates.itinerary.days[].nodes[]` 支持提交 `startTime`、`timePeriod` |
## 快照出参
```json
{
"data": {
"itinerary": {
"days": [
{
"id": "8001",
"dayNumber": 2,
"nodes": [
{
"id": "9001",
"title": "呼和诺尔草原旅游区",
"startTime": "05:05",
"timePeriod": null
},
{
"id": "9002",
"title": "额尔古纳湿地漂流",
"startTime": null,
"timePeriod": "EARLY_MORNING"
}
]
}
]
}
}
}
```
`timePeriod` 返回字典值,展示文字请使用现有 `itinerary_time_period` 字典翻译,不要在页面硬编码中文。
## 提交语义
节点时间沿用 patch 语义:
| 入参状态 | 含义 |
|---|---|
| 字段省略或传 `null` | 不修改该字段 |
| `startTime: "09:05"` | 设置精确开始时间 |
| `timePeriod: "MORNING"` | 设置时间说明 |
| `startTime: ""` | 清空精确开始时间,后端落库为 `NULL` |
| `timePeriod: ""` | 清空时间说明,后端落库为 `NULL` |
仅修改时间时的请求示例:
```json
{
"updates": {
"itinerary": {
"days": [
{
"id": "8001",
"dayNumber": 2,
"nodes": [
{
"id": "9001",
"startTime": "06:30"
},
{
"id": "9002",
"timePeriod": "AFTERNOON"
}
]
}
]
}
}
}
```
清空示例:
```json
{
"id": "9001",
"startTime": "",
"timePeriod": ""
}
```
非空 `startTime` 必须是 24 小时制 `HH:mm`,例如 `09:05`;`9:05`、`24:00` 均不合法。
## 管理后台处理清单
当前 `FunItemAdjustModal.vue` 已从快照读取 `startTime`,但尚未渲染、参与 diff 或写入提交体;`timePeriod` 还未映射。需补:
- 节点草稿与基线同时保存 `startTime`、`timePeriod`。
- 节点行增加精确时间选择器和 `itinerary_time_period` 字典下拉,并回显现有值。
- `funItemsChanged` 与节点 diff 同时比较两字段;只有时间变化也要生成节点 patch。
- `buildItineraryDays()` 对变化字段按需提交,未变化字段省略。
- 用户清空后,提交体将该字段从前端 `null` 转为 `""`;不要直接传 `null`,否则后端按“不修改”处理。
- 保留两字段可同时设置的能力,不在前端强制互斥。
## 兼容性
- 新增出参字段兼容旧调用方。
- `startTime` 原字段保持不变。
- 未设置时间的历史数据返回 `null`,前端按空态展示。
- 本次不改订单金额、节点价格、数量、顺序和资源绑定逻辑。
@@ -0,0 +1,105 @@
---
schema: "hl-changelog/v1"
ticket: "5205"
title: "车务首页汇总与看板人员类型展示"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-24T10:45:00+08:00"
---
# 【修改接口·前端待处理·管理后台】车务首页汇总与看板人员类型展示
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-fleet-service、hl-user-service
>
> **工单**: [wx/HL#5205](https://git.1814.love:8443/wx/HL/issues/5205)
>
> **影响范围**: 车务首页待安排车辆卡片、即将用车订单状态标签、派单看板人数摘要
## 1. 首页待安排车辆汇总
`GET /admin/profile/dashboard?period=today` 的响应结构不变,`data.pendingArrangeVehicle`
调整为 `data.upcomingTrips` 中订单级唯一的有效未完成配车订单总数。
计入口径:
- `unassigned`、`unassigned_urgent`:待派车;
- `holding`、`holding_urgent`:待确认。
不计入口径:
- `assigned`:已完成车辆配置;
- `canceled`、`completed`:已取消或已完结,不属于有效待处理订单。
页面不得只统计 `unassigned`,也不得自行按派车槽位累加。当前测试环境验收样例为
3 个待派车加 1 个待确认,`pendingArrangeVehicle` 与列表徽标都应显示 4。
## 2. 首页状态标签颜色
颜色按稳定状态码映射,不按中文 `statusLabel` 判断:
| `upcomingTrips[].status` | 标签 | 建议语义色 |
| --- | --- | --- |
| `unassigned` / `unassigned_urgent` | 待派车 | warning / 橙色 |
| `holding` / `holding_urgent` | 待确认 | processing / 蓝色 |
紧急程度继续使用 `urgentBadge` 单独表达,不要通过把全部状态渲染成橙色来表示紧急。
## 3. 派单看板人员类型
`GET /admin/fleet/board/orders` 的 `data.records[]` 已返回真实订单人数构成:
```json
{
"headcount": 5,
"adultCount": 2,
"childCount": 1,
"youngChildCount": 1,
"babyCount": 1
}
```
看板卡片不要只展示 `5人`,应展示人员类型构成,例如:
```text
成人2 · 儿童1 · 幼童1 · 婴儿1
```
展示规则:
- 四类字段均为订单真实数据,不得按客户名、标签、总人数或订单 ID 推测;
- 数值为 0 的类型可省略;
- 四类字段全部为 `null` 时才兼容回退 `headcount + "人"`;
- 四类人数合计与 `headcount` 不一致时保留后端原值,并上报数据异常,不在前端静默改数。
## 前端处理清单
- [ ] 首页待安排车辆卡片直接展示后端 `pendingArrangeVehicle`,不再自行只统计待派车状态。
- [ ] 首页列表徽标与 `upcomingTrips.length` 保持一致。
- [ ] 待派车使用橙色,待确认使用蓝色;颜色映射使用状态码。
- [ ] 派单看板用四类人数构成替换单一总人数文案,零值类型省略。
- [ ] 覆盖 3 个待派车 + 1 个待确认、紧急派生态、四类人数混合和全零/空值兼容场景。
## 后端验证
- `FleetDashboardSummaryServiceTest` 覆盖待派车与待确认共同汇总、订单级去重及完成态排除。
- 分支 `fix/5205-fleet-dashboard-summary` 已通过测试环境滚动部署任务 `f64ec5ef`,
`hl-fleet-service` 的 8087/8187 双实例健康。
- 2026-07-24 经测试网关验证:
`pendingArrangeVehicle=4`、`upcomingTrips.length=4`、唯一订单数为 4、重复数为 0,
状态分布为 `unassigned:3`、`holding:1`。
- `GET /admin/fleet/board/orders` 返回 9 条记录,9 条均具有非空的
`adultCount/childCount/youngChildCount/babyCount`。
- fleet、user 与 gateway 日志未发现本次 dashboard 请求相关异常。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,129 @@
---
schema: "hl-changelog/v1"
ticket: "5209"
title: "出行人省份与分批大交通关联"
consumer: "admin"
backend: "verified"
gateway: "verified"
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"
generated: "2026-07-24T11:58:00+08:00"
---
# 【修改接口·前端待处理·管理后台】出行人省份与分批大交通关联
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-order-service-v3、hl-fleet-service
>
> **工单**: [wx/HL#5209](https://git.1814.love:8443/wx/HL/issues/5209)
>
> **PR**: [wx/HL#5212](https://git.1814.love:8443/wx/HL/pulls/5212)
>
> **影响范围**: 车务派单详情的出行人省份标签、大交通与出行人关联、分批抵达/离开展示
## 一、接口变化
`GET /admin/fleet/board/orders/{orderId}`
### 1. 出行人
`data.travelers[]` 补充省级行政区,并继续返回稳定的大交通计划 ID:
```json
{
"travelerId": "3001",
"nameMasked": "张**",
"idNoMasked": "320***********108X",
"idProvinceCode": "32",
"idProvinceName": "江苏省",
"transportPlanIds": ["4001", "4003"]
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `idProvinceCode` | `String \| null` | 身份证省级行政区代码,例如 `32` |
| `idProvinceName` | `String \| null` | 身份证省级行政区名称,例如 `江苏省` |
| `transportPlanIds` | `String[]` | 该出行人关联的全部大交通计划 ID |
省份仅对结构、生日段和省级前缀均可识别的 18 位大陆身份证派生。护照、其他证件、空值或无法识别的身份证返回 `null`;接口不会新增身份证明文。
### 2. 大交通单段和批次
`data.transport.arrive`、`data.transport.depart` 及 `data.transport.batches[]` 统一补充:
```json
{
"planId": "4001",
"direction": "ARRIVAL",
"travelerIds": ["3001"],
"transportNo": "CA1234",
"time": "2026-07-29T11:10:00",
"station": "满洲里西郊机场",
"pickupRequired": true
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `planId` | `String` | 大交通计划 ID |
| `direction` | `String \| null` | `ARRIVAL` 抵达、`DEPARTURE` 离开;极少量历史异常数据可能为 `null` |
| `travelerIds` | `String[]` | 本段或本批关联的出行人 ID |
| `pickupRequired` | `Boolean \| null` | 本段或本批是否需要平台接送 |
一起抵达/离开的首段仍放在 `arrive` 或 `depart`。同方向存在更多批次时,后续批次保留在 `batches[]`,不会合并为单个时间或站点。
## 二、稳定关联算法
前端必须按 ID 关联,不再按姓名关联:
1. 将 `travelers[]` 按 `String(travelerId)` 建立索引。
2. 将非空的 `transport.arrive`、`transport.depart` 与 `transport.batches[]` 合并为大交通段列表。
3. 对每个大交通段遍历 `travelerIds[]`,按字符串 ID 查找对应出行人。
4. 需要反向查询时,用出行人的 `transportPlanIds[]` 匹配各段 `planId`。
所有雪花 ID 都按 JSON 字符串返回。不要转换为 JavaScript `Number`,避免精度丢失。
`travelerNames` 仅为旧页面兼容展示字段,不是关联键。姓名可能重复、脱敏或变化,不得用于匹配。
## 三、页面处理
- 出行人卡片在 `idProvinceName` 非空时显示省份标签;为空时不显示占位标签。
- 大交通区域按 `direction` 区分抵达和离开,不根据数组位置猜方向。
- 每个批次独立展示时间、站点、班次、接送要求和对应出行人。
- 同方向多个批次不得覆盖、去重或压缩为一个批次。
- `travelerIds` 为空时显示该交通段,但不要按姓名猜测关联人。
- `direction=null` 的历史记录可显示为“方向待完善”,不要默认当作离开。
## 四、前端处理清单
- [ ] 出行人卡片读取 `idProvinceName` 并按空值规则显示省份标签。
- [ ] 按字符串 `travelerId/planId` 建立双向关联,不转换为 `Number`。
- [ ] 同时处理 `transport.arrive`、`transport.depart` 和全部 `transport.batches[]`。
- [ ] 按 `direction` 区分抵达/离开,并支持同方向多个批次。
- [ ] 每个大交通段展示其 `travelerIds[]` 对应的出行人。
- [ ] 不使用 `travelerNames`、脱敏姓名或数组位置作为关联依据。
- [ ] 覆盖单批、多批、无关联人、无大交通、省份为空和 `direction=null` 场景。
## 五、验证证据
- 后端提交:`45b631cd0`;PR:[wx/HL#5212](https://git.1814.love:8443/wx/HL/pulls/5212)。
- `mvn -pl hl-order-service-v3,hl-fleet-service -am verify`、fleet `spotless:check` 和定向测试全部通过。
- 测试环境分支部署成功:order task `e1fc0581`、fleet task `d4cea89b`,四个实例健康。
- 经网关遍历 11 条看板订单,详情成功 11/11;30 名出行人中 24 名返回可识别省份。
- 9 个真实大交通段共验证 28 组双向 ID 关联,所有 ID 均为 JSON 字符串,未出现身份证明文字段。
- 临时构造 3 个返程批次验证 `depart + batches[2]` 后已通过业务 API 完整清理,临时批次残留 0。
- order、fleet、gateway 两实例自部署起均无目标 ERROR/Exception。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`;前端按“前端处理清单”接入即可。
@@ -0,0 +1,84 @@
---
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)
> **PR**: #5213
> **Issue**: #5211
> **日期**: 2026-07-24
> **影响范围**: 管理后台派车弹窗“排车待确认”通知预览
---
## ⚠️ 关键变化
原预览接口只读取订单主表基础字段,导致已有大交通和行程的订单仍把“接团、送团、行程链接、有效期”渲染为空。现在预览与 HOLD 实际发送共用接送口径,并在不创建派单、不写短链记录的前提下补齐行程长链和有效期。
## 一、变更接口
| 接口 | 方法 | 路径 | 变更类型 |
|------|------|------|----------|
| 微信通知模板预览 | POST | `/admin/fleet/message-templates/{templateId}/render` | 响应内容修正,结构不变 |
### 入参
请求体字段、类型和必填规则均不变:
```json
{
"orderId": "订单雪花 ID",
"vehicleId": "车辆雪花 ID",
"driverId": "司机雪花 ID",
"serviceDates": ["2026-07-29", "2026-07-30", "2026-07-31"],
"chargeableServiceDates": ["2026-07-29", "2026-07-30", "2026-07-31"],
"vehicleFeeWaiverReason": null
}
```
### 出参
`MessageTemplateRenderRespVO` 结构仍为:
| 字段 | 类型 | 说明 |
|------|------|------|
| `renderedBody` | String | 已替换变量的完整通知正文 |
| `variablesUsed` | String[] | 模板实际引用的变量 key |
本次修正以下既有变量在 `renderedBody` 中的取值:
| 变量 | 新口径 |
|------|--------|
| `order.pickupInfo` | 从订单到达方向大交通格式化;无需平台接送或资料缺失时输出明确文案 |
| `order.dropoffInfo` | 从订单返程方向大交通格式化;单方向缺失不再留空 |
| `itinerary.url` | 使用 `orderId + 行程结束日` 无副作用现签订单级长链;签发失败时输出明确不可用文案 |
| `itinerary.expireAt` | 与行程链接签发口径同源计算;无法计算时输出“待确认” |
## 二、前端调用约束
- 前端无需新增请求字段,也无需自行拼接接送或行程文案。
- 继续直接展示后端返回的 `renderedBody`。
- 预览中的行程链接不会提前创建派单、派单短链或通知记录;真实发送仍由 HOLD 冻结链路生成稳定短链。
- 数据不可用时后端返回明确降级文案,前端不要再把这些文案转换为空串。
## 三、不影响范围
- 不修改模板、派单和通知接口的 JSON 结构。
- 不修改订单、大交通或行程数据。
- 不改变 HOLD 通知冻结、Outbox 投递和真实短链幂等规则。
- 不涉及 `hl-ui` 代码修改。
## 四、验证
- 定向测试:`MessageTemplateRenderServiceTest` + `AssignmentHoldNotificationSnapshotFactoryTest`,22/22 通过。
- 全量验证:`mvn -pl hl-fleet-service -am verify`,14 个 reactor 模块通过。
- Spotless:598 files clean。
- 测试环境网关验收将在 PR 合并并部署后补录到 Issue #5211。
## 五、相关文档
- [后端 Issue #5211](https://git.1814.love:8443/wx/HL/issues/5211)
- [后端 PR #5213](https://git.1814.love:8443/wx/HL/pulls/5213)
@@ -0,0 +1,121 @@
---
schema: "hl-changelog/v1"
ticket: "5215"
title: "车务首页未完成状态独立汇总"
consumer: "admin"
backend: "verified"
gateway: "verified"
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"
generated: "2026-07-24T14:25:00+08:00"
---
# 【修改接口·前端待处理·管理后台】车务首页未完成状态独立汇总
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-fleet-service、hl-user-service
>
> **工单**: [wx/HL#5215](https://git.1814.love:8443/wx/HL/issues/5215)
>
> **影响范围**: 车务首页待处理汇总卡片
## 接口变更
`GET /admin/profile/dashboard?period=today` 在保留 `data.pendingArrangeVehicle`
总数的基础上,新增固定顺序的 `data.pendingStatusCards`:
```json
{
"code": 200,
"data": {
"pendingArrangeVehicle": 5,
"pendingStatusCards": [
{
"status": "unassigned",
"statusLabel": "待派车",
"count": 4
},
{
"status": "holding",
"statusLabel": "待确认",
"count": 1
}
]
}
}
```
字段口径:
| 字段 | 含义 |
| --- | --- |
| `pendingArrangeVehicle` | 近 7 天订单级唯一的全部未完成配车订单数 |
| `pendingStatusCards[].status` | 稳定状态键,固定为 `unassigned`、`holding` |
| `pendingStatusCards[].statusLabel` | 后端中文标签,分别为“待派车”“待确认” |
| `pendingStatusCards[].count` | 对应基础状态的订单级唯一数量 |
`unassigned_urgent` 归入 `unassigned`,`holding_urgent` 归入 `holding`。
`assigned`(配置完成)、`canceled`、`completed` 不进入状态卡。接口始终按
`unassigned`、`holding` 顺序返回两项;即使数量为 0 也不省略。
守恒关系:
```text
sum(pendingStatusCards[].count)
== pendingArrangeVehicle
== upcomingTrips.length
```
## 前端展示
首页汇总区应展示三张独立卡片:
| 卡片 | 数据源 | 建议语义色 |
| --- | --- | --- |
| 待安排车辆 | `pendingArrangeVehicle` | danger / 红色 |
| 待派车 | `pendingStatusCards[status=unassigned].count` | warning / 橙色 |
| 待确认 | `pendingStatusCards[status=holding].count` | processing / 蓝色 |
实现要求:
- 使用 `pendingStatusCards` 循环渲染状态卡,并以 `status` 作为稳定 key 和颜色映射依据;
- 保留现有“待安排车辆”总卡,不要用状态卡替换总数;
- 不要从 `upcomingTrips` 或派车槽位在前端重新汇总;
- 数量为 0 时仍展示对应状态卡,避免布局和状态口径随数据变化;
- 标签优先展示后端 `statusLabel`,颜色不得按中文文案判断。
## 前端处理清单
- [ ] 首页汇总区展示“待安排车辆、待派车、待确认”三张卡片。
- [ ] 待安排车辆使用红色、待派车使用橙色、待确认使用蓝色。
- [ ] 状态卡以 `status` 为 key,直接使用后端 `count`,不在前端重新统计。
- [ ] 覆盖 `5/4/1`、两个状态均为 0、单个状态为 0 的展示场景。
- [ ] 保持现有即将用车订单表格与操作逻辑不变。
## 后端验证
- `FleetDashboardSummaryServiceTest` 覆盖 `unassigned=4`、`holding=1`、
紧急派生态归并、订单去重、配置完成排除及零值场景。
- `LogisticsDashboardServiceTest` 与 Feign fallback 测试覆盖完整透传、
滚动部署兼容和固定两项零值降级。
- `mvn -pl hl-user-service,hl-fleet-service -am verify` 已通过。
- 分支 `fix/5215-fleet-dashboard-status-cards` 已按 fleet、user 顺序部署,
四个服务实例均健康。
- 2026-07-24 经测试网关验证:
`pendingArrangeVehicle=5`、`unassigned=4`、`holding=1`、
`upcomingTrips.length=5`。
- fleet、user 与 gateway 部署完成窗口内 ERROR 级别日志均为 0,
未发现 dashboard 目标异常。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,198 @@
---
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: ""
status_note: "后端与网关已验证;前端 implemented 状态由前端消费线程维护,本次仅迁移 schema。"
updated_at: "2026-07-25T03:29:38.101Z"
base: "dev-v3"
generated: "2026-07-24T14:24:00+08:00"
---
# 【修改接口·前端待处理·管理后台】派车看板补充槽位接送路线与就绪摘要
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-order-service-v3、hl-fleet-service
>
> **工单**: [wx/HL#5216](https://git.1814.love:8443/wx/HL/issues/5216)
>
> **影响范围**: 车务管理 → 派车看板卡片
## 业务口径
派车看板卡片本身应足够车务完成日常派车判断,详情页只用于查看更深信息。每张卡对应一个稳定车辆槽位,
同时显示当前需求全部槽位的派车进度、该槽位服务范围、接送批次、路线和资料就绪风险。
- 看板仍按车辆槽位维度返回,不改为订单维度。
- 只统计订单当前有效用车需求,不混入已驳回、已失活或旧版本需求。
- 行程或大交通缺失只作风险提示,`readiness.blocksAssignment` 固定为 `false`,不改变 `canAssign`。
- 卡片接送摘要不返回出行人、接送备注或大交通自由文本备注。
## 变更接口
```http
GET /admin/fleet/board/orders
```
请求参数、筛选、排序、分页和 `records[]` 维度不变;每条 `records[]` 新增以下字段:
```json
{
"assignmentSlotId": "2080200000000000001",
"slotSummary": {
"assignmentSlotId": "2080200000000000001",
"slotIndex": 2,
"totalSlots": 3,
"serviceStartDate": "2026-07-29",
"serviceEndDate": "2026-07-31",
"serviceDays": 3,
"requiredVehicleType": "mpv",
"requiredVehicleTypeLabel": "商务车",
"requiredSeats": 7
},
"assignmentProgress": {
"totalSlots": 3,
"unassignedSlots": 1,
"holdingSlots": 1,
"assignedSlots": 1,
"completedSlots": 0,
"canceledSlots": 0
},
"pickupSummary": {
"required": true,
"statusCode": "PARTIAL",
"statusLabel": "接客信息部分缺失",
"batchCount": 2,
"readyBatchCount": 1,
"transportNos": ["MU8345", "K7091"],
"earliestTime": "2026-07-29T10:30:00",
"latestTime": "2026-07-29T15:20:00",
"stations": ["海拉尔机场"]
},
"dropoffSummary": {
"required": true,
"statusCode": "READY",
"statusLabel": "送客信息已齐",
"batchCount": 1,
"readyBatchCount": 1,
"transportNos": ["CA1234"],
"earliestTime": "2026-07-31T18:00:00",
"latestTime": "2026-07-31T18:00:00",
"stations": ["海拉尔站"]
},
"routeSummary": "海拉尔区 → 额尔古纳市 → 满洲里市",
"daysUntilDeparture": 5,
"readiness": {
"statusCode": "PARTIAL",
"statusLabel": "部分信息待补",
"itineraryStatusCode": "READY",
"itineraryStatusLabel": "行程已完整",
"itineraryDayCount": 3,
"itineraryExpectedDayCount": 3,
"missingItemCodes": ["PICKUP_TRANSFER"],
"missingItemLabels": ["接客信息"],
"blocksAssignment": false
}
}
```
### 当前槽位 `slotSummary`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `assignmentSlotId` | `String` | 稳定车辆槽位 ID,按雪花 ID 字符串处理 |
| `slotIndex` | `Integer/null` | 当前槽位序号,**从 1 开始** |
| `totalSlots` | `Integer` | 当前有效需求车辆槽位总数 |
| `serviceStartDate/serviceEndDate` | `LocalDate/null` | 当前槽位实际服务范围 |
| `serviceDays` | `Integer/null` | 服务范围闭区间天数 |
| `requiredVehicleType` | `String/null` | 当前槽位车型规范编码 |
| `requiredVehicleTypeLabel` | `String` | 当前槽位车型中文标签 |
| `requiredSeats` | `Integer/null` | 当前槽位要求座位数 |
不要用既有整单 `requiredVehicles[]` 的数组位置猜当前卡片车型;当前卡片只读取 `slotSummary`。
### 整单进度 `assignmentProgress`
`assignmentProgress` 基于当前有效需求的全部稳定槽位计算,不受本次列表状态、车型、日期或关键词筛选影响。
前端可直接展示“3 车:待派 1 / 排车中 1 / 已派 1”,不要用当前页 `records[]` 自行计数。
### 接送摘要 `pickupSummary/dropoffSummary`
| `statusCode` | 含义 |
| --- | --- |
| `READY` | 所有批次时间和站点均完整 |
| `PARTIAL` | 至少一个批次完整,但仍有批次缺时间或站点 |
| `MISSING` | 当前要求该方向接送,但没有完整批次 |
| `NOT_REQUIRED` | 当前用车需求明确不要求该方向接送 |
| `SOURCE_UNAVAILABLE` | order-v3 暂不可用,不能把它显示成“无需接送”或“资料已齐” |
`pickupAt/dropoffAt` 兼容字段继续保留;订单实时上下文可用时,优先回填对应方向第一个有效站点。
### 行程与就绪度
- `routeSummary` 按行程天顺序生成,并压缩连续重复地点;无地点时为 `null`。
- `daysUntilDeparture` 是服务端当前日期到出团日的自然日数;负数表示已出团。
- `readiness.statusCode` 为 `READY/PARTIAL/MISSING/SOURCE_UNAVAILABLE`。
- `missingItemCodes` 当前可能包含:
- `ORDER_CONTEXT`:订单实时信息不可用;
- `ITINERARY_DAYS`:逐日行程缺失、天数不完整或日期仍是旧档期;
- `ROUTE_SUMMARY`:行程天没有可用地点;
- `PICKUP_TRANSFER`:要求接客但资料不完整;
- `DROPOFF_TRANSFER`:要求送客但资料不完整。
页面使用后端 `statusLabel/missingItemLabels` 展示中文,不自行翻译状态码。
## 前端处理清单
- [ ] 卡片主信息区展示“第 `slotIndex/totalSlots` 车”、车型标签、座位数和槽位服务日期。
- [ ] 展示 `assignmentProgress` 整单进度,不按当前页或筛选后记录重新计算。
- [ ] 分别展示接客和送客摘要;多批次显示批次数、班次/车次、时间范围和站点。
- [ ] `NOT_REQUIRED` 显示“无需接客/无需送客”,`SOURCE_UNAVAILABLE` 显示“信息暂不可用”。
- [ ] 展示 `routeSummary`、`daysUntilDeparture` 和 `readiness` 风险提示。
- [ ] 资料缺失时不得禁用派车按钮;操作能力继续只读 `canAssign/availableActionCodes`。
- [ ] 不在卡片展示出行人、接送备注、大交通备注等敏感或自由文本信息。
- [ ] `assignmentSlotId/assignmentId/assignmentGroupId/requirementId/orderId` 均按字符串处理。
- [ ] 覆盖单车、多车、部分已派、多批次接送、无需接送、资料缺失和下游降级场景。
## 不影响范围
- 不修改派车看板请求参数、分页、筛选、排序和操作接口。
- 不修改 `canAssign/canRejectRequirement/availableActionCodes` 计算。
- 不修改派单详情、矩阵派单、司机车辆占用、保险和费用。
- 不迁移数据库,不写入订单、行程、大交通或派单数据。
## 验证证据
- 后端提交:`e73740c57caf295cac96d964e540279f581f1fb0`;PR:
[wx/HL#5221](https://git.1814.love:8443/wx/HL/pulls/5221)。
- `mvn -pl hl-fleet-service -am verify` 通过:2354 tests,0 failures/errors,skipped 1;
Spotless 603 files clean。
- `OrderFleetProviderServiceTest` 33 项、`BoardOrderServiceTest` 48 项及
`AssignmentServiceTest` 稳定槽位聚合测试均通过。
- `mvn -pl hl-order-service-v3 -am verify` 共执行 6643 tests,其中 6642 项通过;
唯一错误是上游 `SettlementFinancialChecksMigrationTest` 在当前环境无法发现 Docker,
与本次接口变更无关。Issue #5216 的对应验收项因此仍保持未勾选。
- 功能分支部署任务:order-v3 `d0e26c90`、fleet `a4776dfa`,均成功完成双实例滚动部署。
- 经测试网关实测 `GET /admin/fleet/board/orders?page=1&pageSize=20`:HTTP 200,返回 3 条真实记录;
8 个新增字段在 3 条记录中全部存在且非空,观测到就绪状态 `PARTIAL`,接送状态
`READY/MISSING`。
- Nacos 实测:`hl-order-service-v3` 8086/8186、`hl-fleet-service` 8087/8187 均为 2/2 健康;
四个新实例均有正常启动记录,启动后的运行日志未发现新增 ERROR、FATAL、Exception 或
`Caused by`。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`;前端按“前端处理清单”接入即可。
@@ -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: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@85851ad68d427e161d9342525af4567c1d108d5f"
target_release: ""
verified_at: ""
status_note: "后端 PR #5241 已合并至 dev-v3(9578f78d5),order/fleet 已部署测试环境(b57ce915/0da3b9e6),双实例 internal 契约与网关汇总/列表/详情已验证;前端仍为 pending,待删除车辆接送开关并改用大交通摘要。"
updated_at: "2026-07-24T10:59:50.144Z"
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,427 @@
---
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@adff10ea74198f4e89a1488e631463bedbcd4eea"
updated_at: "2026-07-25T03:37:10.934Z"
---
# 【修改接口·管理后台】酒店候选补齐房型结算价 (#5237)
> **PR**: #5240 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-25 10:58
## 1. 接口背景
管理后台酒店候选列表原来只在候选酒店顶层返回 `protoPrice`,前端无法确认这个价格来自哪个真实房型,也拿不到同一房型同一天的结算价。配房时如果只看房型列表或自行匹配最低价,容易把协议价和结算价口径拆到不同房型。
本次在候选酒店顶层补齐:
- `protoPriceRoomTypeId`:产生顶层 `protoPrice` 的真实房型 ID。
- `settlementPrice`:与 `protoPriceRoomTypeId` 同一房型、同一天的结算价。
顶层 `protoPrice`、`protoPriceRoomTypeId`、`settlementPrice` 是同一代表房型口径。未维护结算价时 `settlementPrice = null`,不会用协议价兜底。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查询酒店候选(4 场景统一入口) | GET | `/v3/admin/hotel-candidates` | 修改接口 | 候选酒店项新增 `protoPriceRoomTypeId`、`settlementPrice` 两个出参字段;入参不变。 |
## 3. 接口详情
### 3.1 查询酒店候选(4 场景统一入口)
- **使用场景**:管理后台在订单维度查询某一晚的候选酒店,用于配房选酒店、回显当前已配酒店、按产品池/定制师点名/资源库候选排序。
- **认证**:需要管理后台 JWT。
- **幂等性**:只读查询,幂等。
- **限流**:无接口级特殊限流;受网关与服务通用限流策略约束。
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | String | 是 | 订单 ID。后端 Long,JSON/Query 建议按字符串传,避免长 ID 精度问题。 |
| `dayNumber` | Integer | 否 | 第几天,从 1 开始;用于推算 `stayDate = departDate + dayNumber - 1`。最小值 1。 |
| `stayDate` | String | 否 | 入住日期,格式 `yyyy-MM-dd`;直接指定时优先于 `dayNumber` 推算。 |
| `city` | String | 否 | 城市代码或城市名;未传且非关键词模式时默认不按城市限制。 |
| `keyword` | String | 否 | 关键词;非空时跨城/省匹配酒店名、城市、省份、地址,此时 `city` 可不传。 |
| `limit` | Integer | 否 | 返回候选条数上限,默认 30,最小 1,最大 50。 |
| `roomCategory` | String | 否 | 房型字典 code。 |
| `roomCount` | Integer | 否 | 需要的房间数;最小 1。 |
| `preferredHotelId` | String | 否 | 定制师指定的优先酒店 ID。后端 Long,建议字符串传。 |
| `requirementId` | String | 否 | 用房需求 ID;传入后将该需求 days JSON 中当前天的酒店候选作为定制师指定候选。后端 Long,建议字符串传。 |
### 4.2 请求体字段
GET 接口无请求体。
## 5. 出参字段
统一响应结构:
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码,成功为 `200`。 |
| `message` | String | 响应消息,成功为 `成功`。 |
| `data` | Object | 酒店候选查询出参。 |
| `traceId` | String | 链路追踪 ID,可能为空。 |
| `success` | Boolean | `code == 200` 时为 `true`。 |
`data` 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `stayDate` | String | 入住日期,格式 `yyyy-MM-dd`。 |
| `city` | String / null | 本次查询使用的城市;关键词模式或默认不限城市时可为 `null`。 |
| `productType` | String | 产品类型:`CORE` / `GROUP` / `CUSTOM`。 |
| `candidates` | Array | 候选酒店列表,已按产品类型分流排序。 |
`data.candidates[]` 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `hotelId` | String | 酒店 ID。 |
| `hotelName` | String | 酒店名称。 |
| `level` | String / null | 酒店等级。 |
| `form` | String / null | 住宿形态。 |
| `address` | String / null | 地址。 |
| `tags` | Array<String> | 运营标签;无标签时为空数组或 `null`。 |
| `contactPerson` | String / null | 联系人。 |
| `contactWechat` | String / null | 联系微信。 |
| `settleType` | String / null | 结算类型,取值见 §6.1。 |
| `city` | String / null | 酒店所在城市。 |
| `district` | String / null | 酒店所在区/县。 |
| `roomTypes` | Array | 该酒店当日真实房型列表;无房型数据时为空数组。 |
| `protoPrice` | String / null | 代表房型协议价。与 `protoPriceRoomTypeId`、顶层 `settlementPrice` 同一房型同一天。 |
| `protoPriceRoomTypeId` | String / null | 产生顶层 `protoPrice` 的真实房型 ID。无有效可售协议价时为 `null`。 |
| `settlementPrice` | String / null | 与 `protoPriceRoomTypeId` 同一房型、同一天的结算价。未维护时为 `null`,不会用 `protoPrice` 兜底。 |
| `todayAvailable` | Integer / null | 今日全房型可用房数合计。 |
| `availFreshness` | String / null | 可用数数据时效:`fresh` / `stale` / `never_checked`。 |
| `lastCheckedAt` | String / null | 最近一次核房时间,格式 `yyyy-MM-dd'T'HH:mm:ss`。 |
| `matchedRoomTypeAvailable` | Integer / null | 匹配房型今日可用数。 |
| `matchedRoomTypeId` | String / null | 匹配的房型 ID。 |
| `matchedRoomTypeLabel` | String / null | 匹配的房型中文。 |
| `quickPickEnabled` | Boolean / null | 是否支持快速配房。 |
| `quickPickDisabledReason` | String / null | 置灰原因。 |
| `isPoolMatch` | Boolean / null | 是否产品池内。 |
| `poolMatchBadge` | Object / null | 产品池内徽章。 |
| `isConsultantRecommended` | Boolean / null | 是否被定制师点名。 |
| `consultantRecommendBadge` | Object / null | 定制师点名徽章。 |
| `historyMatchScore` | Number / null | 历史匹配度,范围 0-1。 |
| `score` | Number / null | 排序分数。 |
| `recommendation` | String / null | 推荐理由。 |
| `recommended` | Boolean / null | 是否为推荐候选。 |
| `recommendSource` | String / null | 推荐来源,见 §6.4。 |
| `historyScoreStub` | Boolean / null | 历史命中分数是否为 stub。 |
| `isCurrentlyAssigned` | Boolean / null | 是否为本天当前已配酒店。 |
| `assignedRoomTypeId` | String / null | 本天当前已配的房型 ID;`isCurrentlyAssigned=true` 时可用于预填原房型。 |
`data.candidates[].roomTypes[]` 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `roomTypeId` | String | 房型 ID。 |
| `name` | String / null | 房型名称。 |
| `roomCategory` | String / null | 房型分类字典值。 |
| `bedType` | String / null | 床型,已按字典尽量翻译;字典缺失时可回退为 code。 |
| `maxOccupancy` | Integer / null | 最大入住人数。 |
| `available` | Integer / null | 今日可用房数;`unlimited=true` 时为 `null`,语义为不限。 |
| `unlimited` | Boolean | 是否不限库存。 |
| `stock` | Integer / null | 当前可用房;`unlimited=true` 时为 `null`。 |
| `protocolPrice` | String / null | 该房型当日协议价。 |
| `settlementPrice` | String / null | 该房型当日结算价。 |
| `basePrice` | String / null | 标价/挂牌价。 |
| `inventoryStatus` | String | 库存状态,见 §6.2。 |
徽章对象字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `label` | String | 中文徽章文字。 |
| `color` | String | 徽章色,见 §6.5。 |
| `tooltip` | String | 悬浮提示。 |
## 6. 枚举 / 数据字典
### 6.1 `settleType`
**所属字段**:`data.candidates[].settleType` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `cash` | 现付 | 到店或线下现金类结算。 |
| `sign` | 签单 | 供应商签单结算。 |
| `company` | 公司付 | 公司统一付款结算。 |
### 6.2 `inventoryStatus`
**所属字段**:`data.candidates[].roomTypes[].inventoryStatus` | **类型**:String | **必填**:是
| 值 | 中文 | 说明 |
|----|------|------|
| `AVAILABLE` | 可售 | 有余量,或 `unlimited=true` 不限库存。 |
| `FULL` | 满房 | 有日历记录,但库存为 0。 |
| `CLOSED` | 未开放 | 无该日价格日历记录。 |
### 6.3 `availFreshness`
**所属字段**:`data.candidates[].availFreshness` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `fresh` | 最新 | 可用于快速配房判断。 |
| `stale` | 过期 | 核房数据过期。 |
| `never_checked` | 从未核房 | 无可用核房数据。 |
### 6.4 `recommendSource`
**所属字段**:`data.candidates[].recommendSource` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `PRODUCT_POOL` | 产品池 | 来自产品池候选。 |
| `CONSULTANT` | 定制师点名 | 来自定制师指定候选。 |
| `RESOURCE_LIB` | 资源库 | 来自资源库候选。 |
### 6.5 `Badge.color`
**所属字段**:`poolMatchBadge.color` / `consultantRecommendBadge.color` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `blue` | 蓝色 | 普通推荐或池内标识。 |
| `gold` | 金色 | 高优先级推荐标识。 |
| `gray` | 灰色 | 弱提示标识。 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询成功。 |
| `400` | 参数错误 | `orderId` 为空、`dayNumber < 1`、`limit` 超出 1-50、`roomCount < 1`、日期格式不是 `yyyy-MM-dd` 等参数绑定或校验失败。 |
| `401` | 未认证 | JWT 缺失或无效。 |
| `403` | 无权限 | 当前账号无权访问该管理后台接口或订单数据。 |
| `581007` | 订单不存在 | `orderId` 对应订单不存在。 |
| `500` | 服务内部错误 | 非预期异常。 |
## 8. 示例(3 组:典型 / 边界 / 异常)
### 8.1 典型成功
**请求**:
```http
GET /v3/admin/hotel-candidates?orderId=100001&dayNumber=1&stayDate=2026-07-25&limit=30&roomCount=2 HTTP/1.1
Authorization: Bearer <admin-jwt>
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"stayDate": "2026-07-25",
"city": null,
"productType": "CORE",
"candidates": [
{
"hotelId": "2023714929877450753",
"hotelName": "测试酒店",
"level": "舒适型",
"form": "HOTEL",
"address": "呼伦贝尔市海拉尔区测试路 1 号",
"tags": ["协议酒店"],
"contactPerson": "张经理",
"contactWechat": "hotel_mgr",
"settleType": "sign",
"city": "呼伦贝尔市",
"district": "海拉尔区",
"protoPrice": "280.00",
"protoPriceRoomTypeId": "2023727403196502017",
"settlementPrice": "279.00",
"todayAvailable": 7,
"availFreshness": "fresh",
"lastCheckedAt": null,
"matchedRoomTypeAvailable": 7,
"matchedRoomTypeId": "2023727403196502017",
"matchedRoomTypeLabel": "豪华大床房",
"quickPickEnabled": true,
"quickPickDisabledReason": null,
"isPoolMatch": true,
"poolMatchBadge": {
"label": "产品池内",
"color": "blue",
"tooltip": "本酒店在产品池内,优先推荐"
},
"isConsultantRecommended": false,
"consultantRecommendBadge": null,
"historyMatchScore": 0.85,
"score": 1185.0,
"recommendation": "池内 · 历史合作 8 单成功率 95%",
"recommended": true,
"recommendSource": "PRODUCT_POOL",
"historyScoreStub": true,
"isCurrentlyAssigned": false,
"assignedRoomTypeId": null,
"roomTypes": [
{
"roomTypeId": "2023727403196502017",
"name": "豪华大床房",
"roomCategory": "KING",
"bedType": "大床",
"maxOccupancy": 2,
"available": 7,
"unlimited": false,
"stock": 7,
"protocolPrice": "280.00",
"settlementPrice": "279.00",
"basePrice": "568.00",
"inventoryStatus": "AVAILABLE"
}
]
}
]
},
"traceId": "trace-20260725-0001",
"success": true
}
```
### 8.2 边界情况
**场景说明**:代表房型有协议价但未维护结算价,顶层 `settlementPrice` 返回 `null`,不使用 `protoPrice` 兜底。
**请求**:
```http
GET /v3/admin/hotel-candidates?orderId=100001&stayDate=2026-07-25&keyword=%E6%B5%B7%E6%8B%89%E5%B0%94&limit=1 HTTP/1.1
Authorization: Bearer <admin-jwt>
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"stayDate": "2026-07-25",
"city": null,
"productType": "CUSTOM",
"candidates": [
{
"hotelId": "2023714929877450753",
"hotelName": "测试酒店",
"settleType": "cash",
"protoPrice": "280.00",
"protoPriceRoomTypeId": "2023727403196502017",
"settlementPrice": null,
"roomTypes": [
{
"roomTypeId": "2023727403196502017",
"name": "豪华大床房",
"available": 7,
"unlimited": false,
"protocolPrice": "280.00",
"settlementPrice": null,
"basePrice": "568.00",
"inventoryStatus": "AVAILABLE"
}
],
"quickPickEnabled": true,
"recommended": true,
"recommendSource": "RESOURCE_LIB"
}
]
},
"traceId": "trace-20260725-0002",
"success": true
}
```
### 8.3 业务失败(异常)
**场景说明**:`orderId` 未传,触发参数校验失败。
**请求**:
```http
GET /v3/admin/hotel-candidates?stayDate=2026-07-25 HTTP/1.1
Authorization: Bearer <admin-jwt>
```
无请求体。
**响应**:
```json
{
"code": 400,
"message": "orderId 不能为空",
"data": null,
"traceId": "trace-20260725-0003",
"success": false
}
```
## 9. 业务边界
- **适用场景**:管理后台按订单和入住日查询酒店候选;`stayDate` 可直接传,也可通过 `dayNumber` 和订单出发日推算。
- **不适用场景**:不用于前端直接查询内部资源服务;本文只描述管理后台 `/v3/admin/hotel-candidates`。
- **特殊边界**:顶层 `protoPrice`、`protoPriceRoomTypeId`、`settlementPrice` 必须按同一代表房型理解;`settlementPrice = null` 表示该代表房型当天未维护结算价。
- **特殊边界**:`roomTypes[].settlementPrice` 是每个房型自己的当日结算价;顶层 `settlementPrice` 只对应 `protoPriceRoomTypeId` 指向的代表房型。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `data.candidates[].protoPriceRoomTypeId` | 不返回 | 返回产生顶层 `protoPrice` 的真实房型 ID;无有效可售协议价为 `null`。 |
| `data.candidates[].settlementPrice` | 不返回 | 返回与 `protoPriceRoomTypeId` 同一房型、同一天的结算价;未维护为 `null`。 |
| `data.candidates[].protoPrice` | 已返回,但无法判断来自哪个房型 | 仍返回原字段,并与新增的 `protoPriceRoomTypeId`、顶层 `settlementPrice` 组成同一代表房型口径。 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 候选酒店顶层价格展示 | 只能拿到代表协议价 `protoPrice`。 | 可同时拿到代表协议价、代表房型 ID、该代表房型结算价。 |
| 结算价为空 | 顶层没有结算价字段。 | 顶层 `settlementPrice` 返回 `null`;不使用 `protoPrice` 兜底。 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:否。只新增出参字段,已有字段名、类型、入参不变。
- **前端是否必须同步上线**:否。老前端可忽略新增字段;需要展示或回填结算价的页面可读取新增字段。
- **影响已有数据**:无数据迁移要求;历史未维护结算价的房型按 `settlementPrice = null` 返回。
### 11.2 回滚方案
- **回滚方式**:回滚 PR #5240 后,顶层新增字段不再返回。
- **回滚后清理**:无前端数据清理要求。
- **回滚耗时**:按常规服务回滚流程处理。
## 12. 注意事项
- 前端读取顶层 `settlementPrice` 时,不要把 `null` 当作 `protoPrice`;`null` 表示未维护结算价。
- 如需定位价格来自哪个房型,使用顶层 `protoPriceRoomTypeId` 去匹配 `roomTypes[].roomTypeId`。
- 金额和长 ID 在响应 JSON 中按字符串处理,例如 `"280.00"`、`"2023727403196502017"`。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5237](https://git.1814.love:8443/wx/HL/issues/5237)
- **PR**: [#5240](https://git.1814.love:8443/wx/HL/pulls/5240)
- **Merge commit**: [dc6e2ef](https://git.1814.love:8443/wx/HL/commit/dc6e2ef6c2b49bd503353814f85723566d4413c6)
### 13.2 联系人
- **后端负责人**: @yst
@@ -0,0 +1,388 @@
---
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@5a155c42395a7abd66c78789b225d6af86bb7fbd"
updated_at: "2026-07-25T03:42:03.625Z"
---
# 【修改接口·管理后台】核单门票来源类型统一 (#5238)
> **PR**: #5242 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-25 10:03
## 1. 接口背景
核单 Step2 门票/游玩项目页签中,手工补充的门票行此前在查询出参中使用 `CUSTOM_ASSIGNMENT`。为避免前端按不同 Tab 或来源类型做额外分支,本次将查询出参的手工门票来源统一为 `MANUAL`,中文名统一为 `手工项目`;保存接口同步允许直接提交 `MANUAL`。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | Step 2 查询门票核单明细 | GET | `/v3/admin/order/{orderId}/settlement/step2` | 修改接口 | 手工/自定义门票行的 `sourceType` 统一返回 `MANUAL`,`sourceTypeName` 返回 `手工项目` |
| 2 | Step 2 录门票核单明细 | PUT | `/v3/admin/order/{orderId}/settlement/step2` | 修改接口 | `items[].sourceType` 新增允许 `MANUAL`;旧 `CUSTOM_ASSIGNMENT` 入参继续兼容 |
## 3. 接口详情
### 3.1 Step 2 查询门票核单明细
- **方法**:GET
- **路径**:`/v3/admin/order/{orderId}/settlement/step2`
- **接口名**:`listTicket`
- **ApiOperation**:Step 2 查询门票核单明细
- **使用场景**:进入核单 Step2 门票/游玩项目页签,或保存成功后回读页面明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:幂等,只读查询。
- **限流**:无单接口额外限流。
- **响应结构**:`data` 为 `TicketItemVO[]`。
### 3.2 Step 2 录门票核单明细
- **方法**:PUT
- **路径**:`/v3/admin/order/{orderId}/settlement/step2`
- **接口名**:`saveTicket`
- **ApiOperation**:Step 2 录门票核单明细
- **使用场景**:保存核单 Step2 门票/游玩项目明细,包含派生门票行和手工补充门票行。
- **认证**:需要管理后台 JWT。
- **幂等性**:全量替换保存;同一份 `items` 重复提交后,以最后一次提交结果为准。
- **限流**:无单接口额外限流。
- **请求体兼容**:推荐使用 `{ "items": [...] }`;历史数组 body `[...]` 仍兼容。
- **响应结构**:`data` 为 `SettlementTicketSaveRespVO`。
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 接口 | 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| GET / PUT | `orderId` | string | 是 | 订单 ID,长整型字符串 |
两个接口均无 Query 参数。
### 4.2 GET 请求体字段
GET 无请求体。
### 4.3 PUT 请求体字段
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `items` | array | 是 | 门票/游玩项目明细行数组,全量替换保存 | 不允许为 `null` |
| `items[].id` | string | 否 | 已存在行 ID;新增行可不传 | 长整型字符串 |
| `items[].sourceType` | string | 是 | 来源类型;手工门票推荐传 `MANUAL` | `SCENIC_ASSIGNMENT` / `ACTIVITY_ASSIGNMENT` / `MANUAL` / `CUSTOM_ASSIGNMENT` |
| `items[].sourceTypeName` | string | 否 | 来源类型中文名,仅展示字段;保存时可不传 | 最大 32 字符 |
| `items[].scenicAssignmentId` | string/null | 否 | 来源 assignment ID;手工项目传 `null` | 长整型字符串或 `null` |
| `items[].dayNumber` | integer/null | 否 | 行程第几天;保存后以回读值为准 | 从 1 开始 |
| `items[].dayDate` | string | 是 | 行程日期 | `yyyy-MM-dd` |
| `items[].scenicName` | string | 是 | 景区/游玩项目名称 | 1-200 字符 |
| `items[].specName` | string/null | 否 | 规格/票型名称 | 最大 128 字符 |
| `items[].ticketCount` | integer | 是 | 实际购票数量;套餐含门票但无额外成本时可填 0 | 整数 |
| `items[].ticketUnitPrice` | number/null | 否 | 参考成本单价,单位元 | 小数 |
| `items[].sellPrice` | number/null | 否 | 客户成交单价,单位元 | `>= 0` |
| `items[].totalAmount` | number/null | 否 | 客户成交小计,单位元 | `>= 0` |
| `items[].plannedCost` | number | 是 | 计划成本,单位元 | `>= 0` |
| `items[].actualCost` | number | 是 | 实际成本,单位元 | `>= 0` |
| `items[].paymentMethod` | string | 否 | 付款方式;不传时按公司付款处理 | `SIGNED` / `COMPANY_PAID` / `CASH_PAID` |
| `items[].paymentMethodName` | string | 否 | 付款方式中文名,仅展示字段;保存时可不传 | 最大 32 字符 |
| `items[].voucherUrls` | array | 否 | 凭证图片 URL 数组 | 字符串数组 |
| `items[].remark` | string/null | 否 | 备注 | 最大 500 字符 |
## 5. 出参字段
### 5.1 GET 响应字段:`TicketItemVO[]`
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | integer | 业务状态码,成功为 `200` |
| `message` | string | 响应消息 |
| `success` | boolean | 是否成功 |
| `data` | array | 门票/游玩项目明细行数组 |
| `data[].id` | string/null | 核单明细行 ID;未持久化派生行可能为 `null` |
| `data[].sourceType` | string | 来源类型;手工/自定义门票行本次统一返回 `MANUAL` |
| `data[].sourceTypeName` | string/null | 来源类型中文名;`MANUAL` 返回 `手工项目` |
| `data[].scenicAssignmentId` | string/null | 来源 assignment ID;手工项目为 `null` |
| `data[].dayNumber` | integer/null | 行程第几天 |
| `data[].dayDate` | string | 行程日期,`yyyy-MM-dd` |
| `data[].scenicName` | string | 景区/游玩项目名称 |
| `data[].specName` | string/null | 规格/票型名称 |
| `data[].ticketCount` | integer | 实际购票数量 |
| `data[].ticketUnitPrice` | number/null | 参考成本单价,单位元 |
| `data[].sellPrice` | number/null | 客户成交单价,单位元 |
| `data[].totalAmount` | number/null | 客户成交小计,单位元 |
| `data[].plannedCost` | number | 计划成本,单位元 |
| `data[].actualCost` | number | 实际成本,单位元 |
| `data[].paymentMethod` | string/null | 付款方式 |
| `data[].paymentMethodName` | string/null | 付款方式中文名 |
| `data[].voucherUrls` | array | 凭证图片 URL 数组 |
| `data[].remark` | string/null | 备注 |
### 5.2 PUT 响应字段:`SettlementTicketSaveRespVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | integer | 业务状态码,成功为 `200` |
| `message` | string | 响应消息 |
| `success` | boolean | 是否成功 |
| `data.addedIds` | string[] | 本次保存新增的核单明细行 ID 列表 |
| `data.updatedIds` | string[] | 本次保存更新的核单明细行 ID 列表;当前全量替换语义下通常为空数组 |
| `data.deletedIds` | string[] | 本次保存删除的核单明细行 ID 列表;当前返回通常为空数组 |
| `data.totalActualCost` | string | 保存后 Step2 实际成本合计,单位元 |
## 6. 枚举 / 数据字典
### 6.1 `sourceType`
**所属字段**:`items[].sourceType`、`data[].sourceType` | **类型**:String | **PUT 必填**:是 | **GET 必返**:是
| 值 | 中文 | 说明 |
|----|------|------|
| `SCENIC_ASSIGNMENT` | 景区 | 景区派生来源行;查询和保存语义不变 |
| `ACTIVITY_ASSIGNMENT` | 游玩项目 | 游玩项目派生来源行;查询和保存语义不变 |
| `MANUAL` | 手工项目 | 本次推荐值;查询手工/自定义门票行统一返回该值,保存接口也允许提交该值 |
| `CUSTOM_ASSIGNMENT` | 手工项目(旧入参兼容) | 仅用于兼容旧保存请求;查询响应不再返回该值 |
### 6.2 `sourceTypeName`
**所属字段**:`items[].sourceTypeName`、`data[].sourceTypeName` | **类型**:String | **必填**:否
| sourceType | sourceTypeName | 说明 |
|------------|----------------|------|
| `SCENIC_ASSIGNMENT` | `景区` | 景区派生来源行 |
| `ACTIVITY_ASSIGNMENT` | `游玩项目` | 游玩项目派生来源行 |
| `MANUAL` | `手工项目` | 手工/自定义门票行统一展示名 |
| `CUSTOM_ASSIGNMENT` | `手工项目` | 旧保存请求兼容;保存成功后回读为 `MANUAL` / `手工项目` |
| `null` / 未知值 | `null` | 查询行为不变,不新增兜底文案 |
### 6.3 `paymentMethod`
**所属字段**:`items[].paymentMethod`、`data[].paymentMethod` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `SIGNED` | 签单 | 现场签单 |
| `COMPANY_PAID` | 公司付款 | 公司统一付款;未传 `paymentMethod` 时按该值处理 |
| `CASH_PAID` | 现付 | 现场现金/线下现付 |
## 7. 错误码
| HTTP 状态 / code | 含义 | 触发场景 |
|------------------|------|----------|
| `200` / `200` | 成功 | GET 查询成功或 PUT 保存成功 |
| `200` / `401` | 未授权 | 缺少有效的管理后台 `Authorization` 头 |
| `400` / `400` | 请求参数非法 | `sourceType` 不在 `SCENIC_ASSIGNMENT` / `ACTIVITY_ASSIGNMENT` / `MANUAL` / `CUSTOM_ASSIGNMENT` 内,或请求体结构不符合要求 |
| `200` / `584011` | 当前核单状态不允许录门票核单 | PUT 保存时订单不是可录门票核单的状态 |
### 7.1 错误结构
```json
{
"code": 400,
"message": "sourceType 必须是 SCENIC_ASSIGNMENT / ACTIVITY_ASSIGNMENT / MANUAL / CUSTOM_ASSIGNMENT 之一",
"data": null,
"success": false
}
```
## 8. 示例(3 组:典型 / 边界 / 异常)
### 8.1 典型成功:GET 返回手工项目为 MANUAL
**请求**:
```http
GET /v3/admin/order/2079576729147338754/settlement/step2
Authorization: Bearer {token}
```
GET 无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": [
{
"id": "2080186487600025601",
"sourceType": "MANUAL",
"sourceTypeName": "手工项目",
"scenicAssignmentId": null,
"dayNumber": 2,
"dayDate": "2026-07-22",
"scenicName": "临时补充门票",
"specName": "成人票",
"ticketCount": 2,
"ticketUnitPrice": 30.00,
"sellPrice": 50.00,
"totalAmount": 100.00,
"plannedCost": 60.00,
"actualCost": 60.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": [],
"remark": "现场补充"
}
]
}
```
### 8.2 边界成功:查询结果原样 PUT
**场景说明**:前端可把 GET 回来的 `MANUAL` 行原样放入 `items` 后提交;保存成功后再次 GET 仍返回 `MANUAL` / `手工项目`。
**请求**:
```http
PUT /v3/admin/order/2079576729147338754/settlement/step2
Authorization: Bearer {token}
Content-Type: application/json
```
```json
{
"items": [
{
"id": "2080186487600025601",
"sourceType": "MANUAL",
"sourceTypeName": "手工项目",
"scenicAssignmentId": null,
"dayNumber": 2,
"dayDate": "2026-07-22",
"scenicName": "临时补充门票",
"specName": "成人票",
"ticketCount": 2,
"ticketUnitPrice": 30.00,
"sellPrice": 50.00,
"totalAmount": 100.00,
"plannedCost": 60.00,
"actualCost": 60.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": [],
"remark": "现场补充"
}
]
}
```
**响应**:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"addedIds": ["2080186500000000001"],
"updatedIds": [],
"deletedIds": [],
"totalActualCost": "60.00"
}
}
```
### 8.3 业务失败:非法 sourceType
**场景说明**:`items[].sourceType` 传入未定义值时仍按参数非法处理。
**请求**:
```http
PUT /v3/admin/order/2079576729147338754/settlement/step2
Authorization: Bearer {token}
Content-Type: application/json
```
```json
{
"items": [
{
"sourceType": "TAB_MANUAL",
"scenicAssignmentId": null,
"dayDate": "2026-07-22",
"scenicName": "临时补充门票",
"specName": "成人票",
"ticketCount": 1,
"ticketUnitPrice": 0,
"sellPrice": 0,
"totalAmount": 0,
"plannedCost": 0,
"actualCost": 0,
"paymentMethod": "COMPANY_PAID",
"voucherUrls": [],
"remark": null
}
]
}
```
**响应**:
```json
{
"code": 400,
"message": "sourceType 必须是 SCENIC_ASSIGNMENT / ACTIVITY_ASSIGNMENT / MANUAL / CUSTOM_ASSIGNMENT 之一",
"data": null,
"success": false
}
```
## 9. 业务边界
- **适用场景**:核单 Step2 门票/游玩项目页签查询、保存门票明细时使用。
- **手工项目保存**:新增或编辑手工门票行时,`items[].sourceType` 推荐传 `MANUAL`,`scenicAssignmentId` 可传 `null`。
- **旧入参兼容**:旧页面继续传 `CUSTOM_ASSIGNMENT` 仍可保存;保存成功后再次查询会返回 `MANUAL`。
- **查询结果原样提交**:GET 返回的 `MANUAL` 行可原样进入 PUT 的 `items`。
- **未变化范围**:`SCENIC_ASSIGNMENT`、`ACTIVITY_ASSIGNMENT` 的查询和保存语义不变;`null` / 未知来源的查询兜底行为不变。
- **不适用场景**:人员费用、住宿、餐食、其他支出接口没有本次契约变化。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 修改前 | 修改后 |
|------|--------|--------|
| GET `data[].sourceType` | 手工/自定义门票行返回 `CUSTOM_ASSIGNMENT` | 手工/自定义门票行统一返回 `MANUAL` |
| GET `data[].sourceTypeName` | 手工/自定义门票行可能按旧来源展示 | 手工/自定义门票行统一返回 `手工项目` |
| PUT `items[].sourceType` | 允许 `SCENIC_ASSIGNMENT` / `ACTIVITY_ASSIGNMENT` / `CUSTOM_ASSIGNMENT` | 允许 `SCENIC_ASSIGNMENT` / `ACTIVITY_ASSIGNMENT` / `MANUAL` / `CUSTOM_ASSIGNMENT` |
### 10.2 行为级对比
| 行为 | 修改前 | 修改后 |
|------|--------|--------|
| 查询手工门票行 | 前端需要识别 `CUSTOM_ASSIGNMENT` | 前端按 `MANUAL` 识别手工项目 |
| 保存手工门票行 | 前端需要把手工 Tab 转成 `CUSTOM_ASSIGNMENT` | 前端可直接提交 `MANUAL` |
| 查询结果原样保存 | GET 的旧来源值与页面手工 Tab 值可能不一致 | GET 结果可原样 PUT |
| 旧请求兼容 | 旧 `CUSTOM_ASSIGNMENT` 入参可保存 | 继续可保存,回读统一为 `MANUAL` |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:否。PUT 继续兼容旧 `CUSTOM_ASSIGNMENT` 入参;GET 只统一手工门票来源的展示值。
- **前端是否必须同步上线**:否。旧保存请求仍可用;但前端可清理 `MANUAL` 与 `CUSTOM_ASSIGNMENT` 互转逻辑。
- **影响已有数据**:不需要前端处理历史数据;页面以后端返回的 `MANUAL` 为准。
### 11.2 回滚方案
- 如接口回滚,前端需恢复兼容 GET 返回 `CUSTOM_ASSIGNMENT` 的判断。
- 回滚后不要把 GET 查询结果中的 `sourceType` 假定为一定可原样提交。
## 12. 注意事项
- 前端不要再按 Tab 名称把手工项目强制转换成 `CUSTOM_ASSIGNMENT`;新增手工行可以直接传 `MANUAL`。
- 前端如有 `sourceType === "CUSTOM_ASSIGNMENT"` 才展示手工项目的判断,需要同步兼容或改为判断 `MANUAL`。
- `CUSTOM_ASSIGNMENT` 仅作为旧保存请求兼容值保留,不应再作为新页面查询展示值。
- `sourceTypeName` 是展示字段,保存时可不传;保存后以再次查询结果为准。
- 非法 `sourceType` 仍会返回参数非法,不新增兜底保存。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5238](https://git.1814.love:8443/wx/HL/issues/5238)
- **PR**: [#5242](https://git.1814.love:8443/wx/HL/pulls/5242)
- **Merge commit**: [bfb28a258](https://git.1814.love:8443/wx/HL/commit/bfb28a258)
### 13.2 联系人
- **后端负责人**: @yst
@@ -0,0 +1,139 @@
---
schema: "hl-changelog/v2"
ticket: "5244"
title: "派单详情分别返回接送说明与通用备注"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@ede7d025e2d6d0d90e570d0bfa5d90f588431842"
target_release: ""
verified_at: ""
status_note: "后端 PR #5246 已合并并部署;前端需在派单 Step1 大交通卡片分别渲染两个字段。"
updated_at: "2026-07-25T01:24:40.528Z"
base: "dev-v3"
generated: "2026-07-25T09:05:18+08:00"
---
# 车务派单详情:分别返回接送说明与通用备注
> **服务**: `hl-order-service-v3`、`hl-fleet-service`
>
> **工单**: [wx/HL#5244](https://git.1814.love:8443/wx/HL/issues/5244)
>
> **后端 PR**: [wx/HL#5246](https://git.1814.love:8443/wx/HL/pulls/5246)
>
> **影响范围**: 车务管理 → 派车看板 → 派单弹窗 Step1 → 大交通
## 业务口径
`pickupRemark` 与 `remark` 是两个独立字段,不得合并、互相覆盖或只取其中一个:
- `pickupRemark`:接机/送机说明;ARRIVAL 展示为“接机说明”,DEPARTURE 展示为“送机说明”。
- `remark`:大交通通用备注,展示为“备注”。
- 整团 `arrive/depart` 与分批 `batches[]` 使用同一字段口径。
- 任一字段为 `null` 或空白时,只隐藏该字段对应的展示行,不影响另一字段。
## 变更接口
### 管理后台
```http
GET /admin/fleet/board/orders/:orderId
```
`data.transport.arrive`、`data.transport.depart` 与 `data.transport.batches[]` 均包含:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `pickupRemark` | `String/null` | 否 | 接机/送机说明 |
| `remark` | `String/null` | 否 | 大交通通用备注;既有字段继续保留 |
响应示例:
```json
{
"code": 200,
"data": {
"transport": {
"arrive": {
"direction": "ARRIVAL",
"pickupRemark": "到达出口举牌接机",
"remark": "航班可能延误"
},
"depart": {
"direction": "DEPARTURE",
"pickupRemark": "提前三小时送机",
"remark": "请再次确认航站楼"
},
"batches": [
{
"direction": "ARRIVAL",
"pickupRemark": "分批接机说明",
"remark": "分批通用备注"
}
]
}
}
}
```
### 内部契约
```http
GET /v3/internal/order/orders/:orderId/fleet-detail-context
```
order-v3 → fleet 的共享 `OrderTransportForFleetDTO` 在整团段与分批段均独立传递
`pickupRemark`、`remark`。这是兼容性增量:路径、HTTP 方法、既有字段、枚举、错误码及
`pickupRequired` 三态口径均不变。
## 前端展示矩阵
| 方向/模式 | `pickupRemark` | `remark` | 页面展示 |
| --- | --- | --- | --- |
| ARRIVAL,整团或分批 | 有 | 有 | 分别显示“接机说明”和“备注” |
| DEPARTURE,整团或分批 | 有 | 有 | 分别显示“送机说明”和“备注” |
| 任一方向 | 有 | 空 | 只显示接机/送机说明 |
| 任一方向 | 空 | 有 | 只显示备注 |
| 任一方向 | 空 | 空 | 两行均不显示 |
前端不得根据 `pickupRequired` 推导说明文本,也不得用一个字段回填另一个字段。
## 前端处理清单
- [ ] 派单弹窗 Step1 大交通卡片读取 `pickupRemark`,按方向显示“接机说明”或“送机说明”。
- [ ] 通用备注继续读取 `remark`,与接机/送机说明分行展示。
- [ ] 同时覆盖 `arrive`、`depart`、`batches[]`。
- [ ] 对 `null`、空字符串和纯空白字符串使用单字段空态规则。
- [ ] 不显示 `travelerIds` 等内部关联字段;既有出行人脱敏规则不变。
## 契约验证状态
- OpenAPI/oasdiff:`not_configured`。项目当前未配置稳定 Swagger2 → OAS3 导出与 oasdiff 基线。
- 消费者契约/Spring Cloud Contract:`not_configured`。项目当前未配置 SCC。
- fallback:源码与 Codemap 影响比对、order-v3 生产者测试、fleet 消费者/Controller 测试以及完整 reactor 验证。
- 本次没有临时安装 oasdiff 或 Spring Cloud Contract 依赖。
## 验证证据
- 合并提交:`ca3c5c7310ddc142398382644a40ab57d951248e`。
- 定向生产者/消费者测试:88 项通过。
- 影响范围测试:25 个 reactor 模块全部通过。
- Fleet 完整验证:2361 项测试,0 失败、0 错误、1 跳过;Spotless 606 个 Java 文件通过。
- 测试部署:
- order-v3 任务 `a5916436`,8086/8186 双实例成功;
- fleet 任务 `a198456e`,8087/8187 双实例成功。
- 部署面板与 Nacos 均确认两个服务 2/2 running、healthy、enabled;部署后日志新增错误匹配为 0。
- 经测试网关验证真实团单:列表与详情 HTTP/业务码均为 200,`relatedDetailReady=true`;
ARRIVAL、DEPARTURE 均同时返回非空且取值不同的 `pickupRemark`、`remark`。
## 不影响范围
- 不修改 `D:/work2/hl-ui`。
- 不修改大交通录入、接送默认值、接送需求聚合、派车状态机或历史数据。
- 不新增 DDL,不清理、不回填存量大交通备注。
> 后端与网关已验证;`frontend_status: pending` 表示等待前端真实领取,不代表页面已实现、发布或验证。
@@ -0,0 +1,218 @@
---
schema: "hl-changelog/v2"
ticket: "5245"
title: "行程短链预览与同槽位改派解析"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@cd8aff9b0c6499a1dee1b9c3ca00ddbceb4b5aed"
target_release: ""
verified_at: ""
status_note: "后端已部署并完成网关验证;用户验收发现排车页缺少新增车辆槽位入口,前端已退回 claimed 继续修复。"
updated_at: "2026-07-26T01:23:05.110Z"
base: "dev-v3"
---
# 车务:行程短链预览与同槽位改派解析
> **服务**: `hl-fleet-service`
>
> **工单**: [wx/HL#5245](https://git.1814.love:8443/wx/HL/issues/5245)
>
> **后端 PR**: [wx/HL#5249](https://git.1814.love:8443/wx/HL/pulls/5249)、
> [wx/HL#5250](https://git.1814.love:8443/wx/HL/pulls/5250)
>
> **影响范围**: 车务管理 → 派车弹窗通知预览、车辆/司机批量选择、派单详情
## 关键变化
- 通知模板预览中的 `itinerary.url` 会为当前派车组即时创建或复用稳定短链,
例如 `https://hr.example.com/s/Dabc1234`,不再把完整 HMAC token URL 或“派车后生成”占位文案放进预览正文。
- 既有短链和完整 token 长链在原派车组失效后,只允许解析到同一订单、同一
`assignmentSlotId` 的唯一当前有效派车组;跨订单、跨槽位、无有效派单或同槽位存在多个
active 派车组时继续返回 `605308`。
- 批量派单和详情多司机字段是既有契约,本次明确前端消费口径:一次提交 `items[]`,详情展示
`activeAssignments[]`,不得只处理兼容代表字段 `currentAssignment`。
- 排车页必须提供“+ 添加车辆槽位”入口。新增槽位不是替换“车辆槽位 1”,而是追加一个可独立
选择车辆和司机的草稿槽位;多个槽位统一映射为批量派单 `items[]`。
## 变更接口
| 方法 | 路径 | 本次口径 |
| --- | --- | --- |
| `POST` | `/admin/fleet/message-templates/<templateId>/render` | 请求新增可选 `assignmentGroupId`;有效派车组即时创建/复用稳定短链;旧前端未传时仅在订单、车辆、司机唯一定位一个 active 组时兼容 |
| `GET` | `/app/h5/s/<code>` | 继续生成短时 token 并重定向;同槽位改派后的解析由行程接口完成 |
| `GET` | `/app/h5/itinerary/<token>` | 原组失效后仅回退同订单、同稳定槽位的唯一 active 派车组 |
| `POST` | `/admin/fleet/assignments/batch` | 既有:按 `items[]` 一次提交多个车辆/司机槽位 |
| `GET` | `/admin/fleet/board/orders/<orderId>` | 既有:按 `activeAssignments[]` 返回全部当前有效派车组 |
## 1. 通知模板预览
```http
POST /admin/fleet/message-templates/<templateId>/render
```
请求新增可选字段 `assignmentGroupId`,响应结构不变。前端在预览包含
`itinerary.url` 或 `itinerary.code` 的模板时,应传入当前派车组 ID;后端仅为兼容旧前端,
在 `orderId` + `vehicleId` + `driverId` 唯一定位一个 active 派车组时允许省略:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `orderId` | `string` | 是 | 订单雪花 ID |
| `vehicleId` | `string` | 是 | 当前派车组车辆雪花 ID |
| `driverId` | `string` | 是 | 当前派车组司机雪花 ID |
| `assignmentGroupId` | `string` | 行程预览时强烈建议 | 派车组雪花 ID;取自批量派单响应,多车多司机场景必须按槽位传入 |
派车组有效时,预览会即时创建或复用该组短链:
```yaml
code: 200
data:
renderedBody: "请查看行程:https://hr.example.com/s/Dabc1234"
variablesUsed:
- "itinerary.url"
```
未传 `assignmentGroupId` 且订单、车辆、司机无法唯一定位 active 派车组,或显式派车组无效时,
`itinerary.url` 使用“行程链接暂不可用,请联系车务确认”,`itinerary.code` 为空字符串。
短链配置、注册或数据库失败时接口直接返回错误,不静默降级为占位文案;任何场景都不会回退或
暴露完整 HMAC URL。同一派车组通过显式 ID 或兼容定位重复预览、发送、重试时复用同一短链。
## 2. 同稳定槽位改派后的旧链接
短链先通过 `/app/h5/s/<code>` 重定向到短时 token;短链与直接保存的完整 token 最终都进入
`/app/h5/itinerary/<token>`,因此使用同一组回退规则:
| token 原派单与当前派单 | 结果 |
| --- | --- |
| 原派车组仍有 `holding` / `assigned` 服务日 | 使用原派车组当前 active 视图 |
| 原组失效,同 `orderId` + 同 `assignmentSlotId` 恰有一个 active 组 | 使用当前改派组 |
| 仅有其他订单或其他槽位的 active 组 | `605308` |
| 同槽位无 active 组 | `605308` |
| 同槽位存在多个 active 组 | `605308`,失败封闭 |
本次不改变 token 签名、有效期、短链 code 结构或错误码。
## 3. 前端多车辆/多司机消费
### 批量派单
```http
POST /admin/fleet/assignments/batch
```
每个已选车辆槽位生成一个 `items[]` 元素,所有槽位一次提交:
```yaml
orderId: "2080000000000000001"
requirementId: "2080000000000000002"
startDate: "2026-07-29"
endDate: "2026-07-31"
holdMode: 1
requestId: "assign-2080000000000000001-v1"
items:
- fleetItemIndex: 0
vehicleId: "2080000000000000101"
driverId: "2080000000000000201"
- fleetItemIndex: 1
vehicleId: "2080000000000000102"
driverId: "2080000000000000202"
```
- `fleetItemIndex` 从 0 开始,对应需求展开后的稳定车辆槽位。
- `vehicleId`、`driverId` 必填;雪花 ID 全程按字符串处理。
- `protocolPrice`、`messageTemplateId`、`customBody`、`confirmCrossResident` 是单槽位可选字段。
- 前端维护可编辑槽位列表。初始槽位来自当前有效派车组或订单用车需求;点击
“+ 添加车辆槽位”后追加一个空白草稿槽位,不得覆盖或复用既有槽位。
- 每个草稿槽位独立选择一辆车和一名司机;未提交的新槽位允许删除,已有
`holding` / `assigned` 槽位不得被“删除草稿”操作静默撤销。
- 进入下一步前校验所有可提交槽位均已选择车辆和司机,并为每个槽位生成唯一
`fleetItemIndex`。页面可见槽位数必须等于本次提交的 `items[]` 数量。
- 不得为每辆车循环调用单条 `POST /admin/fleet/assignments` 代替批量接口。
- 批量响应按 `data.assignments[].assignment.assignmentGroupId` 返回各槽位派车组 ID;
前端逐项调用模板预览时传入对应 `assignmentGroupId`,不得只预览代表项。
### 派单详情
```http
GET /admin/fleet/board/orders/<orderId>
```
按 `data.activeAssignments[]` 渲染每个有效派车组,至少消费:
| 字段 | 用途 |
| --- | --- |
| `assignmentGroupId` | 派车组稳定展示 key |
| `assignmentSlotId` | 同一需求车辆槽位的稳定身份 |
| `fleetItemIndex` | 槽位顺序 |
| `vehicleId` / `vehiclePlate` / `vehicleModel` | 车辆展示 |
| `driverId` / `driverName` / `driverPhone` | 司机展示;电话已脱敏 |
| `assignmentStatus` / `assignmentStatusLabel` | 当前有效状态 |
| `lifecycleStageCode` | 生命周期阶段 |
`currentAssignment` 仅为兼容代表项,不能用来判断订单只有一辆车或只展示一名司机。
`activeAssignments` 无数据时使用空列表空态,不复制代表项凑数。
## 前端展示矩阵
| 场景 | 数据源 | 页面行为 |
| --- | --- | --- |
| 通知预览传入有效派车组 | `assignmentGroupId` + `renderedBody` 中的 `itinerary.url` | 即时创建或复用并展示稳定短链 |
| 旧前端未传派车组但订单、车辆、司机唯一定位 | `orderId` + `vehicleId` + `driverId` | 兼容定位并返回同一稳定短链 |
| 派车组缺失、无效或定位不唯一 | “行程链接暂不可用,请联系车务确认” | 展示不可用态,不把文案当可发送链接 |
| 已有车辆槽位 | `activeAssignments[]` 或当前排车草稿 | 按稳定槽位逐项展示;允许重选当前槽位的车辆或司机 |
| 新增车辆槽位 | 前端草稿槽位列表 | 展示“+ 添加车辆槽位”;每次点击只追加一个空白槽位,不替换已有槽位 |
| 新增槽位未选完整 | 草稿槽位的 `vehicleId` / `driverId` | 槽位显示未完成警示,禁用“下一步”;不生成可发送通知 |
| 删除未提交槽位 | 前端草稿槽位列表 | 只删除新增且未提交的草稿槽位,不撤销已有有效派车组 |
| 一单多个车辆槽位 | `items[]` | 每个槽位各选一辆车和一名司机,一次批量提交;可见槽位数与 `items[]` 数量守恒 |
| 详情有多个 active 派车组 | `activeAssignments[]` | 按槽位逐项展示车辆、司机、脱敏电话和状态 |
| 详情无 active 派车组 | `activeAssignments=[]` | 展示无有效派单空态 |
## 前端处理清单
- [ ] 排车页提供“+ 添加车辆槽位”入口,允许连续新增多个草稿槽位,不得只重选“车辆槽位 1”。
- [ ] 每个新增槽位分别选择一辆车和一名司机,并支持删除未提交的草稿槽位。
- [ ] “下一步”前校验所有槽位,按页面槽位顺序生成唯一 `fleetItemIndex`,可见槽位与
`items[]` 一一对应。
- [ ] 统一提交 `POST /admin/fleet/assignments/batch` 的 `items[]`,保留批次级 `requestId`。
- [ ] 批量派单响应逐项保存 `assignmentGroupId`;通知预览传入当前槽位的
`orderId`、`vehicleId`、`driverId`、`assignmentGroupId`,只把真实短链视为可发送链接。
- [ ] 派单详情按 `activeAssignments[]` 展示全部车辆/司机,不只读 `currentAssignment`。
- [ ] 司机电话使用后端脱敏值,雪花 ID 始终按字符串处理。
- [ ] 覆盖无 active、多 active、短链不可用等空态/失败封闭场景。
## 前端验收反馈
- 2026-07-25 用户页面验收:排车页仅显示“车辆槽位 1”,只能在该槽位内重选车辆或司机,
无法新增第二个槽位;当前前端提交不满足多车辆、多司机批量派单要求。
- 状态因此由 `implemented` 回退为 `claimed`。前端完成新增槽位、逐槽位选择和批量提交后,
应填写新的 `frontend_ref` 再迁移为 `implemented`。
## 验证证据
- OpenAPI/oasdiff:`not_configured`。项目未配置可复现的 Swagger2 → OAS3 导出与 oasdiff 基线;
本次使用源码语义比对、Controller/Service 定向测试与测试网关证据兜底。
- 消费者契约/Spring Cloud Contract:`not_required`。本次没有内部 Feign 或共享 Java DTO 变化。
- 后端定向测试:41 项通过,0 失败、0 错误、0 跳过。
- Fleet Spotless:606 个 Java 文件检查通过。
- 完整 reactor `verify`:3209 项测试,0 失败、0 错误、1 跳过;其中 fleet 2373 项,
0 失败、0 错误、1 跳过。
- 后端 PR #5249 合并提交:`433ef238f09eba2258c996093b1d8cb2309a8e83`。
- 后端 PR #5250 合并提交:`d939995bd266f11076eb79ea183e37a968e01afc`。
- 测试部署任务:`8eae87b2`;`hl-fleet-service` 的 `8187`、`8087` 两实例均健康。
- 测试网关已验证:显式 `assignmentGroupId` 与唯一兼容定位返回同一 7 位短码;
重复预览保持稳定,短链 302、H5 JSON 与 HTML 均成功;失效组返回 `605308`,
篡改签名返回 `605306`。脱敏证据已回写工单 #5245。
## 不影响范围
- 不修改或部署 `D:/work2/hl-ui`。
- 除模板预览请求新增可选 `assignmentGroupId` 外,不删除 API 字段,不改变既有字段类型、
必填性或枚举;模板预览响应结构不变。预览在命中有效派车组时会幂等写入短链记录。
- 不修改批量派单事务、价格、跨常驻确认、保险或通知冻结规则。
- 不新增 DDL,不清理、不回填存量数据。
> `frontend_status: claimed` 表示前端已领取但仍需修复“新增车辆槽位”;尚未形成可验收的完整实现。
@@ -0,0 +1,238 @@
---
schema: "hl-changelog/v1"
ticket: "5131"
title: "车队独立管理及车队字典下线"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "implemented"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@ac4d5fd6292b13eb504f5393dfe07781800b9233"
updated_at: "2026-07-25T01:18:23.686Z"
base: "dev-v3"
generated: "2026-07-22T10:46:00+08:00"
---
# 【新增接口·修改接口·前端需联调·管理后台/H5】车队独立管理及车队字典下线
> **服务**: hl-fleet-service + hl-user-service
> **日期**: 2026-07-22
> **工单**: #5131
> **影响范围**: 车队管理、车辆档案、司机 H5、自带车审核、派车候选、矩阵、车队对账
## 关键变化
`fleet_attribution` 不再是车队数据源。后端新增 `fleet_team` 主数据,统一维护:
- `teamName`:车队名称。
- `teamType`:`SELF_OPERATED` 自有 / `COOPERATIVE` 合作。
- `leaderName`、`leaderPhone`:负责人及电话;列表电话脱敏,详情返回编辑原值。
- `settleType`:直接复用资源付款方式 `resource_settle_type`,当前值为 `cash` / `sign` / `company`。
- `status`:`ACTIVE` / `DISABLED`。
车辆及相关链路以 `fleetTeamId` 为权威关联。旧 `fleet` 稳定编码仅在客户端切换期保留兼容,不得再用于生成选项或写死 `own/coopA/coopB`。
## 变更接口
### 车队管理
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/admin/fleet/teams/page` | 分页;支持 `keyword/teamType/status/settleType` |
| GET | `/admin/fleet/teams/options` | 有效车队下拉;编辑存量时可传 `includeDisabledId` 回显当前停用车队 |
| GET | `/admin/fleet/teams/:fleetTeamId` | 详情;负责人电话返回原值供编辑 |
| POST | `/admin/fleet/teams` | 新增 |
| PUT | `/admin/fleet/teams/:fleetTeamId` | 编辑 |
| DELETE | `/admin/fleet/teams/:fleetTeamId` | 删除;仅名下无车辆且无未完结司机自助录入时允许 |
| POST | `/admin/fleet/teams/:fleetTeamId/disable` | 停用;仍有在役车辆返回 `601103` |
| POST | `/admin/fleet/teams/:fleetTeamId/enable` | 启用 |
保存请求:
```json
{
"teamName": "合作车队一队",
"teamType": "COOPERATIVE",
"leaderName": "张三",
"leaderPhone": "13800138000",
"settleType": "sign",
"sortOrder": 20,
"remark": "旺季合作车队"
}
```
下拉响应项:
```json
{
"fleetTeamId": "2080000000000000001",
"teamCode": "ft_fsq1ab23cd",
"teamName": "合作车队一队",
"teamType": "COOPERATIVE",
"settleType": "sign",
"status": "ACTIVE"
}
```
雪花 ID 一律按字符串处理,禁止 `Number()` / `parseInt()`。
## 修改接口
### 车辆档案
- `POST /admin/fleet/vehicles`、`PUT /admin/fleet/vehicles/:id`:新增 `fleetTeamId`,新前端必传。
- `GET /admin/fleet/vehicles/page`:新增筛选参数 `fleetTeamId`;列表项新增 `fleetTeamId/fleetTeamName/fleetType/settleType`。
- `GET /admin/fleet/vehicles/:id`:详情新增同上字段。
- 车辆导入模板把车队列改为“车队名称”,填写独立车队管理中的有效名称;历史表头和稳定编码仍兼容。
### 司机 H5 与审核
- `GET /app/h5/driver-onboard/init`:链接可编辑时新增 `fleetTeamOptions[]`,只包含 `fleetTeamId/teamName/teamType`,不暴露负责人和结算资料;续签会额外包含当前已停用车队用于原值回显。
- `SubmitVehicleVO`、续签常驻车回显新增 `fleetTeamId`。
- 待审核详情 `vehicle`、审核通过请求 `ownVehicle` 新增 `fleetTeamId`。
- H5 和管理端都必须提交 ID;旧 `fleet` 仅兼容已打开的旧页面。
### 派车候选与矩阵
- 派车车辆候选新增 `fleetTeamId/fleetTeamName/fleetType/settleType`。
- `GET /admin/fleet/board/orders` 已派车辆新增 `currentVehicleFleetTeamId/currentVehicleFleetTeamName/currentVehicleFleetTeamType/currentVehicleFleetTeamSettleType`。
- `GET /admin/fleet/matrix/grid` 新增 `fleetTeamIds[]`;`fleets[]` 废弃。
- 矩阵车辆行新增 `fleetTeamId/fleetTeamName/fleetType/settleType`。
- 响应新增 `fleetTeamCounts[]`,每项包含 `fleetTeamId/teamName/count`;`fleetCount` 仅过渡兼容。
### 对账
- 车费车队分组新增 `fleetTeamId/fleetType/settleType`,名称使用对账快照。
- `GET /admin/fleet/reconciliation/cars` 与 CSV 导出新增 `fleetTeamIds[]`;传入后优先于旧 `fleets[]`。
- 保险车队分组新增 `fleetTeamId/fleetName/fleetType/settleType`。
- 实际结算保存新增 `fleetTeamId`;旧 `fleet` 废弃。
- 后端按车队类型派生 `OWN_COST/COOP_QUOTE`,不再把 `own` 当特殊业务编码。
历史字典迁入的车队可能没有负责人资料。新增、编辑请求中的 `leaderName`、`leaderPhone`、
`settleType`、`sortOrder` 均为必填;前端编辑存量车队时必须提示车务人员补录真实资料,禁止用占位姓名或虚假电话自动填充。
## 独立菜单与权限
user-service 在“车务管理”目录下新增子菜单:
- 路由:`/fleet/teams`
- 组件:`fleet/teams/index`
- 权限:`fleet:team:list`、`fleet:team:create`、`fleet:team:update`、`fleet:team:status`、`fleet:team:delete`
- 默认角色:`SUPER_ADMIN`、`ADMIN`、`VEHICLE_MANAGER`
前端必须新增对应组件,否则菜单发布后会出现空路由。
## 前端必须修改的范围
### 2026-07-24 页面复测反馈:列表列宽与暗色模式
测试环境 `/fleet/teams` 页面已经能展示负责人和脱敏电话,但当前样式仍需前端修正,本反馈不涉及后端接口或字段变化:
1. 表格列宽分配失衡。“车队名称”列占用过多空白,把“负责人 / 负责人电话”等核心联系人信息推到页面右侧,首屏信息密度过低。
2. 暗色模式不能只替换页面背景。当前筛选区、表头、行分隔线、空值、状态标签和操作区的层级与对比度不足,部分边界难以辨认。
3. 样式必须复用项目主题 token;禁止在本页写死仅适用于浅色模式的背景色、文字色或边框色。负责人电话仍只展示接口返回的脱敏值,样式调整不得绕过脱敏。
#### 展示矩阵
| 视口 / 主题 | 车队名称 | 负责人 / 负责人电话 | 其他列 | 验收表现 |
| --- | --- | --- | --- | --- |
| `>= 1440px`,浅色 | 弹性列,限制最大占比;超长省略并可查看完整名称 | 建议分别保留约 `120px / 140px`,左对齐 | 类型、付款方式、数量、排序、状态和操作按内容定宽 | 联系人紧邻业务字段,首屏无大段无意义空白 |
| `>= 1440px`,暗色 | 同浅色列宽规则 | 同浅色列宽规则 | 使用暗色主题 token | 页面、筛选区、表头、数据行、状态标签和操作区层级清楚 |
| `1024px - 1439px`,浅色/暗色 | 优先收缩并省略,不能无限占宽 | 不压缩为空或挤出主要阅读区 | 保留操作列可用宽度 | 联系人信息仍可直接阅读 |
| `< 1024px`,浅色/暗色 | 设置表格最小宽度 | 保持可读宽度 | 允许横向滚动 | 不通过隐藏关键列或强行挤压完成适配 |
空负责人和空电话统一显示 `—`。联系人文本左对齐;车辆数、排序、状态和操作居中。浅色与暗色模式都必须覆盖默认、悬停、聚焦、禁用和空数据状态;普通文本与背景建议至少达到 `4.5:1` 对比度,控件边界和状态提示应清晰可辨。
### 管理后台
1. 新增 `src/api/fleet/teams.js` 和 `src/views/fleet/teams/index.vue`,完成车队分页、新增、编辑、启停和删除:
- “车队管理”必须显示在“车务管理”目录内,不得作为一级菜单处理。
- 按上面的展示矩阵修正表格列宽和明暗主题样式,不能让“车队名称”列挤占联系人信息区域。
- 仅 `vehicleCount === 0` 时展示/启用删除动作;调用删除接口后刷新列表。
- 后端仍会独立校验车辆及未完结司机录入关联,返回 `601107` 时提示“请先完成车辆/司机转移”。
- 编辑历史迁入车队时补齐负责人、负责人电话、付款方式和排序。
2. 车辆档案:
- `src/views/fleet/vehicles/index.vue`
- `src/views/fleet/vehicles/components/VehicleEditModal.vue`
- `src/api/fleet/vehicles.js`
使用 `/admin/fleet/teams/options`,表单和筛选绑定 `fleetTeamId`,展示 `fleetTeamName`。
3. 自带车审核和车辆选择:
- `src/views/fleet/drivers/pending/index.vue`
- `src/views/fleet/drivers/components/VehiclePickerModal.vue`
- `src/api/fleet/drivers.js`
不再读取 `fleet_attribution`。
4. 派车看板、矩阵和共享甘特:删除 `own/coopA/coopB` 固定数组和固定颜色映射,按 API 返回的 ID/名称动态分组。涉及:
- `src/views/fleet/board/composables/useVehicleDriverPicker.js`
- `src/views/fleet/board/components/VehiclePickerList.vue`
- `src/views/fleet/matrix/**`
- `src/views/fleet/_shared/fleetDisplay.js`
- `src/views/fleet/_shared/gantt/**`
5. 车队对账:`src/views/fleet/recon/**` 删除三车队固定循环、固定展开状态和固定 CSV 顺序;实际结算提交 `fleetTeamId`。
动态车队颜色可由 `fleetTeamId` 做稳定哈希映射,但不得用数组下标产生每次刷新变化的颜色。
### 司机 H5
以下文件把硬编码 `<option value="own/coopA/coopB">` 改为初始化响应的 `fleetTeamOptions`,提交 `fleetTeamId`:
- `src/views/h5/driver-intake/DriverIntakeForm.vue`
- `src/views/h5/driver-intake/composables/useIntakeForm.js`
- `src/views/h5/driver-intake/composables/useRenewPrefill.js`
- `src/views/h5/driver-intake/steps/StepVehicleReg.vue`
- `src/views/h5/driver-intake/steps/RenewUpdate.vue`
## 删除字典与发布顺序
user-service 迁移会精确删除:
```sql
DELETE FROM sys_dict_data WHERE dict_type = 'fleet_attribution';
DELETE FROM sys_dict_type WHERE dict_type = 'fleet_attribution';
```
必须按以下顺序发布,禁止先删字典:
1. 发布 `hl-fleet-service`,完成 `fleet_team` 建表、存量回填和兼容接口上线。
2. 发布已完成本清单的 `hl-ui`,确认车辆、审核、H5、矩阵和对账不再读取该字典。
3. 最后发布 `hl-user-service`,新增独立菜单并删除字典。
若环境中曾在字典里新增但从未被车辆、待审核或对账引用的车队,发布前需先在独立车队管理中补建;迁移会自动收集所有已有业务引用编码,但不会跨服务读取未使用的字典配置。
## 兼容与业务规则
- 车队名称唯一;内部 `teamCode` 创建后不可修改。
- 车队已关联车辆后不能切换自有/合作类型,防止历史结算语义漂移。
- 停用车队不出现在普通下拉;存量车辆编辑可回显当前停用车队,但不能切入其他停用车队。
- 车队下仍有 `ACTIVE` 车辆时禁止停用,须先转移或停用车辆。
- 车队只有在名下无车辆、无未完结司机自助录入时才能删除;正式司机通过常驻车辆归属,车辆未转移时删除同样会被拒绝(`601107`)。
- 停用车队的存量车辆不得恢复在役,也不会进入派车候选或矩阵。
- 对账保存车队名称、类型和付款方式快照,后续改主档不修改历史账期。
- 负责人电话属于敏感信息,列表只展示脱敏值,不得写日志或进入前端埋点。
## 验收清单
- [ ] 独立车队菜单可分页、新增、编辑、启停,付款方式与资源页选项一致。
- [ ] `/fleet/teams` 在桌面端不再由“车队名称”列制造大段空白,负责人和脱敏电话位于首屏连续阅读区;窄屏按展示矩阵滚动而不是隐藏或挤压关键列。
- [ ] `/fleet/teams` 的浅色、暗色模式均使用主题 token,筛选区、表头、数据行、空值、状态标签和操作区在默认/悬停/聚焦/禁用状态下层级清晰。
- [ ] “车队管理”位于“车务管理”目录下;空车队可删除,非空车队删除入口禁用或明确提示后端 `601107`。
- [ ] 历史迁入车队可通过编辑补齐负责人、负责人电话、付款方式和排序,保存时不允许提交空资料。
- [ ] 车辆新增/编辑/筛选/详情/导入均使用动态车队,不再出现固定三项。
- [ ] 司机 H5 新招、续签和管理端自带车审核均可选择动态车队并正确回显。
- [ ] 派车候选、矩阵、甘特和对账能展示任意新增车队,颜色和分组稳定。
- [ ] 全前端搜索不到 `fleet_attribution` 运行时读取,也没有业务代码写死 `own/coopA/coopB` 车队集合。
- [ ] 按发布顺序上线后,删除字典不会导致下拉为空、标签显示编码或请求失败。
- [ ] 雪花 ID 全程按字符串处理,负责人电话未出现在日志、埋点或列表明文。
## 验证证据
- `mvn -pl hl-fleet-service -am -DskipTests compile`:通过。
- 受影响链路 12 个测试类定向执行:388 项通过,0 failure,0 error。
- user-service 菜单迁移审计:1 项通过,0 failure,0 error。
- `mvn -pl hl-user-service,hl-fleet-service -am test`:通过。
- `mvn -pl hl-fleet-service -am verify`:通过;fleet 绑定的 `spotless:check` 同步通过。
- 测试环境已部署 `hl-fleet-service@feat/fleet-team-management`,8087/8187 双实例健康。
- 测试网关只读实测:车队分页、有效车队下拉、车辆分页均 HTTP/业务码 200;动态车队字段齐全,负责人电话列表脱敏。
- 前端页面联调及 `hl-user-service` 菜单/删字典迁移:待前端完成动态车队与独立菜单页面后按发布顺序执行。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -1,6 +1,6 @@
# 【行为变更·管理后台】订单调整保留配房与房务驳回定制师待办(#4907)
> 2026-07-14 最终状态:#4907 后端链路已完成并关闭;最终全量复测与前端接入总览见 `57_房务全量API复测与前端最终接入核对-管理后台.md`。前端页面验收属于独立交付,不作为后端工单关单门禁。
> 2026-07-16 最终后端状态:零配房最终确认动作契约已由 PR #5014 补齐并合入 `dev-v3`。最新代码、部署、网关 API、DB/库存和日志证据均通过;前端页面实现属于独立交付,不作为后端工单关单门禁。
> 服务:`hl-order-service-v3`
>
@@ -8,9 +8,9 @@
>
> 接口结构:新增“单条配房晚次与资源原子调整”接口;其余沿用订单调整、房务详情、最终确认和房务驳回现有接口
>
> 后端状态:PR [#4915](https://git.1814.love:8443/wx/HL/pulls/4915)、[#4925](https://git.1814.love:8443/wx/HL/pulls/4925)、[#4926](https://git.1814.love:8443/wx/HL/pulls/4926)、[#4928](https://git.1814.love:8443/wx/HL/pulls/4928)、[#4931](https://git.1814.love:8443/wx/HL/pulls/4931) 已合并;最新测试环境部署任务 `eb216570` 成功,`8086/8186` 双实例 UP
> 后端状态:既有 PR [#4915](https://git.1814.love:8443/wx/HL/pulls/4915)、[#4925](https://git.1814.love:8443/wx/HL/pulls/4925)、[#4926](https://git.1814.love:8443/wx/HL/pulls/4926)、[#4928](https://git.1814.love:8443/wx/HL/pulls/4928)、[#4931](https://git.1814.love:8443/wx/HL/pulls/4931) 与最新 PR [#5014](https://git.1814.love:8443/wx/HL/pulls/5014) 均已合并;最终验证基线 `dev-v3@d54435af7`,测试环境部署任务 `86d9bf11` 成功,`8086/8186` 双实例 UP
>
> 联调证据:2026-07-12 最终网关四场景探针通过,覆盖连续改需求、跨晚次原子移动、最终确认、供应商驳回、订单取消、库存迁移与库存不足补偿;报告 `D:/work2/HL-v3/.tmp/house-adjustment-flow-probe-20260712-212622.json` 为 `ok=true`
> 联调证据:2026-07-16 部署后重新使用隔离订单执行接口面、缺口流程、订单日志和调整闭环四组探针,全部通过;最终调整报告 `D:/work2/HL-v3/.tmp/house-adjustment-flow-probe-20260716-184129.json` 为 `ok=true`
## 0. 2026-07-12 追加:返工标签唯一口径与前端未完成项
@@ -202,6 +202,29 @@ POST /admin/house/assignments/requirements/{requirementId}/finalize
前端收到该错误后保留当前配房数据,提示房务逐日处理;不得清空页面状态或隐藏超出新行程的旧配房。
### 2.1 零配房动作契约
详情接口与最终确认写接口现已使用同一业务口径:
- 当前生效需求由当前房务持有。
- `houseStatus=CLAIMING`。
- 没有任何配房记录。
- 没有未闭环询房。
满足以上条件时,房务详情返回:
```json
{
"actions": {
"canFinalize": {
"enabled": true
}
}
}
```
以下场景仍保持禁用:存在部分配房、存在未闭环询房、非当前持有人、非当前生效需求或需求已经完成。最终确认成功后必须重新加载详情和待办;后端会关闭相关待办且不会创建配房或变更库存。
## 3. 房务驳回后的定制师待办
```http
@@ -247,10 +270,14 @@ Content-Type: application/json
## 5. 后端验证证据
- PR:`wx/HL#4910/#4911/#4913/#4915/#4925/#4926/#4928/#4931`
- 最新测试环境部署任务:`eb216570`,`8086/8186` 双实例均 UP
- 定向测试:276 项通过;模块全量 5523 项仅复现 clean baseline 的 3 失败 + 2 错误,无新增回归
- 最终网关全流程报告:`D:/work2/HL-v3/.tmp/house-adjustment-flow-probe-20260712-212622.json`,四场景全部 `ok=true`
- 实测通过:增晚、减晚、人数变化、连续调整新旧待办替代、改期+增晚同次提交、酒店/房型/房间数替换、入住日期对齐、人工清空、零配房最终确认、供应商驳回、订单取消、库存成功迁移、库存不足整单回滚
- 返工标签:`REQUIREMENT_ADJUSTED` 压制同需求 `PENDING_ARRANGE`;最终确认、供应商驳回、订单取消后统计均从 1 回到 0
- PR:`wx/HL#4910/#4911/#4913/#4915/#4925/#4926/#4928/#4931/#5014`
- 最终验证基线:`dev-v3@d54435af7`
- 模块全量:5609 项测试,0 failure,0 error,15 skipped,`BUILD SUCCESS`
- 最新测试环境部署任务:`86d9bf11`,`8086/8186` 双实例均 UP
- 网关接口面:68/68 通过,报告 `D:/work2/HL-v3/.tmp/house-api-surface-probe-20260716-182908.json`
- 缺口流程:3/3 通过,报告 `D:/work2/HL-v3/.tmp/house-api-gap-flow-probe-20260716-183121.json`
- 订单日志:3/3 通过,报告 `D:/work2/HL-v3/.tmp/house-order-log-flow-probe-20260716-183224.json`
- 调整闭环:4/4 通过,报告 `D:/work2/HL-v3/.tmp/house-adjustment-flow-probe-20260716-184129.json`
- 实测通过:增晚、减晚、人数变化、连续调整新旧待办替代、改期+增晚、跨晚次原子调整、酒店/房型/房间数替换、入住日期对齐、人工清空、零配房最终确认、供应商驳回、订单取消、库存成功迁移、库存不足整单回滚
- 返工标签:`REQUIREMENT_ADJUSTED` 压制同需求 `PENDING_ARRANGE`;最终确认、供应商驳回、订单取消后返工待办均正确关闭
@@ -0,0 +1,273 @@
# 【前端对接·管理后台】车务需求级派单完成回调与滚动发布契约
> Issue: [wx/HL#4935](https://git.1814.love:8443/wx/HL/issues/4935)
>
> PR: [wx/HL#4994](https://git.1814.love:8443/wx/HL/pulls/4994)、[wx/HL#5009](https://git.1814.love:8443/wx/HL/pulls/5009)
>
> 服务: `hl-fleet-service` / `hl-order-service-v3`
>
> 日期: 2026-07-16
>
> 影响范围: 车务派单完成、用车需求驳回、订单资源状态、看板刷新与部署兼容
## 一、关键纠正
此前链路可能在单个日期或单辆车派定后提前把整个用车需求写成 `DONE`。本次改为:
- 一个用车需求只做一次最终完成回调。
- 只有全部服务日期、全部车型项均已生成有效派单,并且每条逐日配置同时绑定车辆和司机,后端才允许整个需求完成。
- 前端不得根据“某一天已派”“某一辆车已派”自行把需求或订单资源节点标成完成。
- 前端继续直接使用看板/详情接口返回的状态、文案和能力字段,不维护独立状态映射。
## 二、前端接口结论
本次不新增前端调用接口,管理后台继续使用:
| 接口 | 方法 | 路径 | 前端用途 |
|---|---|---|---|
| 派单看板汇总 | GET | `/admin/fleet/board/summary` | 状态选项、文案、数量 |
| 派单看板列表 | GET | `/admin/fleet/board/orders` | 分页卡片、状态与能力字段 |
| 派单看板详情 | GET | `/admin/fleet/board/orders/{orderId}` | 当前需求、逐日行程、当前派单 |
| 派单时间线 | GET | `/admin/fleet/board/orders/{orderId}/timeline` | 已发生操作记录 |
前端处理规则:
1. 状态筛选使用 `summary.statusOptions`,卡片文案使用 `assignmentStatusLabel`。
2. 派车入口只看 `canAssign`,驳回入口只看 `canRejectRequirement`。
3. 派单、驳回或重试成功后重新请求汇总、列表和当前详情,不能只在本地改一张卡片。
4. 同一需求仍有未完成日期或其他车辆项时,后端保持进行中;前端不得提前展示“已完成”。
5. 后端部署开关关闭期间,需求级完成/驳回事件会保留待重放;前端不需要轮询内部 Outbox,也不得调用内部回调。
## 三、管理后台响应示例
### 3.1 仍有未完成配置
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"assignmentStatus": "holding",
"assignmentStatusLabel": "排车中",
"canAssign": true,
"canRejectRequirement": false,
"currentAssignment": {
"requirementId": "2075001000000000001"
}
}
}
```
该响应只表示需求仍在处理,不能因 `currentAssignment` 非空推断整个需求已完成。
### 3.2 整个需求完成后
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"assignmentStatus": "assigned",
"assignmentStatusLabel": "已派车",
"canAssign": false,
"canRejectRequirement": false
}
}
```
实际字段以看板接口当前 OpenAPI 为准;状态中文和能力判断均由后端返回。
## 四、内部回调契约
> 本节供后端与 QA 验收。以下 `/v3/internal/**` 接口不经过管理后台,不配置公网网关路由,前端禁止调用。
### 4.1 最终完成回调
```http
POST /v3/internal/order/vehicle-assignment/callback
Content-Type: application/json
```
请求示例:
```json
{
"orderId": "2074746808742928386",
"requirementId": "2075001000000000001",
"vehicleId": "2076001000000000001",
"vehicleType": "suv",
"vehicleCount": 1,
"licensePlate": "蒙A12345",
"brand": "丰田汉兰达",
"seats": 7,
"plannedDailyFee": "1300.00",
"dailyFeeSource": "PRICE_CALENDAR",
"driverStaffId": "2077001000000000001",
"driverName": "测试司机",
"driverPhone": "13800000000",
"topologyFingerprint": "<64位 SHA-256 摘要>",
"remark": "需求级最终派单快照"
}
```
成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": null
}
```
新版 Fleet 必填字段:`orderId`、`requirementId`、`vehicleId`、`vehicleType`、`vehicleCount`、`topologyFingerprint`。其中 `topologyFingerprint` 是 Fleet 根据该需求全部有效逐日派单生成的 64 位 SHA-256 摘要;其余快照字段允许为空,后端不得伪造车牌、品牌、座位、价格或司机信息。
`vehicleId`、车牌和司机字段是稳定排序后的代表派单,`vehicleCount` 是该需求实际车辆组总数。完整逐日、多车辆和多司机拓扑仍以 Fleet 派单明细为准,不能从该轻量快照反推完整派车表。
滚动发布期间,旧版 Fleet 不传 `topologyFingerprint` 时,Order-v3 会根据完整回调快照生成 `legacy:` 前缀摘要并持久化。该兼容仅用于先升级 Order-v3、后升级 Fleet 的过渡期;新版 Fleet 仍必须发送摘要。
### 4.2 轻量进度回写
```http
POST /v3/internal/order/orders/{orderId}/requirement/vehicle/status?requirementId={requirementId}&status=PROCESSING
```
该接口只允许 `PROCESSING`。`DONE` 必须走最终完成回调并冻结快照。
### 4.3 驳回回写
```http
POST /v3/internal/order/orders/{orderId}/requirement/vehicle/reject
Content-Type: application/json
```
请求示例:
```json
{
"requirementId": "2075001000000000001",
"returnRemark": "车型需求不完整,请定制师补充",
"operatorId": "2078001000000000001"
}
```
只有当前需求不存在 `holding/assigned` 有效派单时才允许驳回。
## 五、状态、幂等和错误分支
| 场景 | 结果 | 副作用 |
|---|---|---|
| `PENDING/PROCESSING` 且无快照 | 原子写快照并完成需求 | 同事务写需求 `DONE`、订单车辆状态 `DONE`、待办/日志并尝试推进订单 |
| `DONE` 且摘要相同 | 幂等成功 | 不加需求写锁、不更新 `update_time`,不重复同步司机、待办、日志或推进订单 |
| `DONE` 且摘要变化 | 刷新轻量快照 | 只更新同一需求快照和司机信息,不重复推进订单、待办或时间线 |
| 旧版回调未传摘要 | 兼容成功 | Order-v3 生成稳定 `legacy:` 摘要;相同旧请求重放仍为零写入 |
| `DONE` 但无快照 | 返回 `582081` | 禁止补造快照,禁止继续副作用 |
| active 状态已有快照 | 返回 `582082` | 禁止重复回写 |
| 需求不存在或失效 | 返回 `582080` | 无写入 |
| 非法状态流转 | 返回 `582083` | 无写入 |
| 订单/需求已取消 | 跳过 | 不写完成快照,不推进订单 |
并发与重放需区分:同一摘要在 5 秒互斥窗口外再次提交时返回成功且数据库零写入;互斥窗口内的并发重复请求返回可识别冲突 `100502`,同样不得重复写快照、待办、流水或推进订单。前端遇到该冲突应刷新当前需求状态,不得自行补写完成状态。
错误响应示例:
```json
{
"code": 582081,
"message": "用车需求状态不允许回写配车",
"success": false,
"data": null
}
```
## 六、滚动发布与回滚
1. 先部署全部 `hl-order-service-v3` 实例并确认 Flyway 成功。
2. 保持 `FLEET_REQUIREMENT_LIFECYCLE_ENABLED=false`,再部署全部 `hl-fleet-service` 实例。
3. 确认 Fleet Flyway、健康与普通 Outbox 消费正常后,再启用开关。
4. 开关关闭时,需求级 Outbox 事件不会占用普通事件扫描窗口,也不会被丢弃;开启后继续重放。
5. 异常时先关闭开关,再回滚服务制品;兼容字段和历史 Outbox 不做破坏性回滚。
## 七、前端必须处理
1. 不新增内部回调请求,不把内部错误码做成独立前端流程。
2. 不按每日派单行数或单车派定结果推导需求完成。
3. 继续使用后端返回的 `statusOptions`、`assignmentStatusLabel`、`canAssign`、`canRejectRequirement`。
4. 操作成功后刷新服务端状态;并发处理中若能力字段变化,以最新接口响应为准。
5. 不修改既有分页、团号、联系人、定制师、逐日行程和大交通字段接法;这些仍以 `57_4882` 文档为准。
## 八、不影响范围
- 不修改 `hl-ui`,本文件仅做后端契约告知。
- 不新增管理后台分页或不分页接口。
- 不改变车型大类、司机占一座、司机险只计车队成本等既有口径。
- 不处理团期配车。
## 九、验证状态
### 9.1 合并前代码验证
```text
hl-order-service-v3 targeted: 198 tests,0 failures,0 errors,0 skipped
hl-order-service-v3 full verify: 5582 tests,0 failures,0 errors,15 skipped
hl-fleet-service targeted: 226 tests,0 failures,0 errors,0 skipped
hl-fleet-service full verify: 1721 tests,0 failures,0 errors,0 skipped
独立终审: P0=0,P1=0,P2=0
```
### 9.2 测试环境部署
- `hl-order-service-v3` Deploy Panel 任务 `29b541d6` 成功;`8086/8186` 双实例均启动并监听。
- `hl-fleet-service` Deploy Panel 任务 `aaee8d01` 成功;`8087/8187` 双实例均启动并监听。
- Nacos 已启用 `fleet.assign.requirement-lifecycle-enabled=true` 与 `fleet.feign.writeback.enabled=true`。
- 发布顺序按“Order-v3 全实例 -> Fleet 全实例 -> 开启需求级生命周期开关”执行,未跨过滚动发布护栏。
### 9.3 真实 API 与数据验收
使用独立车务账号和真实测试订单完成 DIRECT、HOLD、取消后迟到回调、同摘要重放、摘要变化刷新、非法参数及失效需求分支验收;未使用 `admin`、`wx` 或 Mock 数据。
公网网关 `https://api.test.1814.love:9443` 最终验证:
| 请求 | 结果 |
|---|---|
| 车务账号登录 | HTTP 200 |
| `GET /admin/fleet/board/summary` | HTTP 200,状态码/文案/数量由后端返回 |
| `GET /admin/fleet/board/orders?status=assigned&orderNo=...` | HTTP 200,精准返回 1 条 |
| `GET /admin/fleet/board/orders/{orderId}` | HTTP 200,返回逐日行程、车型诉求、司机确认凭证及当前派单 |
| `GET /admin/fleet/board/orders/{orderId}/timeline` | HTTP 200,返回完整操作时间线 |
HOLD 模式真实终态校验:
```json
{
"requirementStatus": "DONE",
"vehicleControlStatus": "DONE",
"hasFleetAssigned": true,
"snapshotCount": 1,
"activeDailySlices": 6,
"activeDailySliceStatus": "assigned",
"driverConfirmationEvidenceCount": 1,
"completionOutboxStatus": "SUCCESS"
}
```
取消订单迟到回调保持 `hasFleetAssigned=false`、快照数为 0、有效逐日派单数为 0;相同拓扑摘要在互斥窗口外重放返回 HTTP 200 且不重复推进待办、流水或订单状态,窗口内并发重复返回 `100502` 且无重复副作用。
### 9.4 OpenAPI 与日志
- Order-v3 OpenAPI 已公开内部最终回调及 `VehicleAssignmentCallbackReqVO` 的 6 个必填字段。
- Fleet OpenAPI 已公开创建、预检、取消、改派、最终确认、司机确认/拒绝、提前结束、需求驳回与撤销取消等 10 个生命周期接口。
- 2026-07-16 22:22 后四个目标实例均无 `ERROR` 级日志;目标订单与需求在四实例中均为 0 条 WARN/ERROR,日志可见司机确认、最终回调成功和 Order-v3 快照刷新。
- 测试环境另有保险 PDF 缺失与历史脏订单降级 WARN,未关联本次目标订单,不作为本契约成功响应的一部分。
Issue #4935 的代码、部署、网关 API、MySQL 终态和服务日志证据均已补齐,可按后端验收清单关单。
## 十、相关文档
- 当前看板字段、分页、统计、行程与保险:`57_4882_车务派单看板当前订单字段统计行程与保险事务收口-管理后台.md`
- 车务提需求与派单看板:`53_4871_车务提需求派单看板闭环契约-管理后台.md`
- 团号与定制师筛选:`54_4876_派单看板团号与定制师下拉筛选-管理后台.md`
- 后端最终回调契约:`hl-backend-changelog/changelogs/2026-07/14_1022_order-v3_vehicle-assignment-callback-contract.md`
@@ -0,0 +1,775 @@
# 【前端对接·管理后台】车务看板、详情、候选与矩阵读模型统一
> Issue: [wx/HL#4936](https://git.1814.love:8443/wx/HL/issues/4936)
>
> PR: [wx/HL#5031](https://git.1814.love:8443/wx/HL/pulls/5031)、[wx/HL#5032](https://git.1814.love:8443/wx/HL/pulls/5032)、[wx/HL#5034](https://git.1814.love:8443/wx/HL/pulls/5034)
>
> 服务: `hl-fleet-service` / `hl-order-service-v3`
>
> 日期: 2026-07-18
>
> 影响范围: 车务派单看板汇总与列表、派单详情、车辆/司机候选、矩阵月视图、相邻订单衔接风险
## 一、对接结论
1. 派单看板继续使用分页接口,`pageSize` 最大 100;没有新增“不分页全量接口”。
2. `/summary` 与 `/orders` 共用日期、车型、司机、联系人、团号、定制师和 `keyword` 筛选;汇总忽略 `status/statuses/page/pageSize`,返回同一筛选范围内的全部状态分面。`pendingCount/pendingUrgentCount/todayDepartCount/holdingTimeoutCount` 同样随这些订单筛选变化;`idleVehicleCount/idleDriverCount` 是不随订单筛选变化的全局资源指标。
3. 派单列表、详情和矩阵均以**当前订单 + 当前有效用车需求 + 当前有效派车组**为准,历史需求和历史派单不能覆盖当前数据。
4. 详情一次返回逐日行程、大交通、当前需求、全部有效派车组、生命周期、凭证和操作记录。
5. 候选车辆和司机分别分页,允许先选车或先选司机;返回完整闭区间可用时间窗、结构化可用性原因、冲突和常驻关系。
6. 矩阵按整月查询,但同一跨月派车组先补齐完整组再裁剪显示;不会因只查到月内一天而丢失真实起止日期。
7. 矩阵相邻订单衔接风险由后端返回 `status/statusLabel/style/reasonCode/reasonMessage`,前端不得自行根据颜色或时间重新推导。
8. **大交通允许不填写。** 无大交通时仍可提交用车需求、查询候选、预检和派车;前端只能显示提示,不得禁用派车按钮。
9. 所有雪花 ID 均按字符串处理,禁止转为 JavaScript `Number`。
10. 本次未修改 `hl-ui`,前端只按本文完成接口对接。
## 二、接口清单
| # | 接口 | 方法 | 路径 | 用途 |
|---|---|---|---|---|
| 1 | 看板汇总 | GET | `/admin/fleet/board/summary` | 同筛选状态计数、急单数、资源数、定制师选项 |
| 2 | 看板列表 | GET | `/admin/fleet/board/orders` | 分页卡片、统一筛选、当前状态和操作能力 |
| 3 | 看板详情 | GET | `/admin/fleet/board/orders/{orderId}` | 当前订单、当前需求、行程、大交通、派车组和日志 |
| 4 | 出行人脱敏列表 | GET | `/admin/fleet/board/orders/{orderId}/travelers` | 默认脱敏查看出行人 |
| 5 | 出行人明文查询 | POST | `/admin/fleet/board/orders/{orderId}/travelers/plain` | 有权限且有审计理由时查看明文 |
| 6 | 派单候选 | POST | `/admin/fleet/assignments/candidates` | 车辆和司机独立分页、冲突、可用时间窗、常驻关系 |
| 7 | 矩阵月视图 | GET | `/admin/fleet/matrix/grid` | 车辆月历、派车段、并行车辆、大交通和衔接风险 |
## 三、看板汇总与列表公共筛选
### 3.1 查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `statuses` | `string[]` | 否 | 多状态任一命中;支持重复 query 参数或逗号分隔。 |
| `status` | `string` | 否 | 单状态/逗号分隔别名,与 `statuses` 合并。 |
| `startDayFrom` | `date` | 否 | 日期区间起,与当前行程闭区间做重叠匹配。 |
| `startDate` | `date` | 否 | `startDayFrom` 别名;前者未传时生效。 |
| `startDayTo` | `date` | 否 | 日期区间止,与当前行程闭区间做重叠匹配。 |
| `endDate` | `date` | 否 | `startDayTo` 别名;前者未传时生效。 |
| `vehicleTypeKeys` | `string[]` | 否 | 车型大类:`suv/mpv/bus/sedan`,任一命中。 |
| `typeKeys` | `string[]` | 否 | `vehicleTypeKeys` 别名。 |
| `driverName` | `string` | 否 | 当前司机姓名模糊匹配。 |
| `keyword` | `string` | 否 | 司机、联系人/客户、团号、订单号、当前负责定制师展示名任一包含即命中。定制师展示名为企业微信昵称优先、用户名兜底;order-v3 降级时回退派单快照,只匹配后端最终解析出的一个展示名。 |
| `contactName` | `string` | 否 | 联系人/客户名模糊匹配。 |
| `contactKeyword` | `string` | 否 | `contactName` 别名。 |
| `teamNo` | `string` | 否 | 团号包含匹配,例如 `7218` 可命中 `26-7218`。 |
| `consultantId` | `string` | 否 | 当前负责定制师管理员 ID 精确匹配。 |
| `plannerName` | `string` | 否 | 定制师显示名模糊匹配兼容参数。 |
| `consultantName` | `string` | 否 | `plannerName` 别名。 |
| `variant` | `string` | 否 | `list` 默认;`grid` 为兼容值,其他值返回参数错误。 |
| `page` | `int` | 列表否 | 默认 1;汇总忽略。 |
| `pageSize` | `int` | 列表否 | 默认 20、最大 100;汇总忽略。 |
状态值:
```text
unassigned / unassigned_urgent / holding / holding_urgent /
assigned / change_requested / completed / canceled
```
状态含义由后端 `statusOptions` 返回。`holding` 是“车务已排车、司机尚未完成确认链路”,不是“车务正在浏览详情”。
### 3.2 汇总请求示例
```http
GET /admin/fleet/board/summary?startDate=2026-07-01&endDate=2026-07-31&typeKeys=suv&keyword=王
Authorization: Bearer <fleet-manager-token>
```
### 3.3 汇总响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"pendingCount": 3,
"pendingUrgentCount": 1,
"todayDepartCount": 1,
"idleVehicleCount": 9,
"idleDriverCount": 5,
"holdingTimeoutCount": 1,
"statusCounts": {
"unassigned": 3,
"holding": 1,
"assigned": 2,
"changeRequested": 0,
"completed": 4,
"canceled": 1,
"unassignedUrgent": 1,
"holdingUrgent": 1
},
"statusOptions": [
{
"value": "unassigned",
"label": "待派车",
"count": 3,
"urgentCount": 1
},
{
"value": "holding",
"label": "排车中",
"count": 1,
"urgentCount": 1
},
{
"value": "assigned",
"label": "已派车",
"count": 2,
"urgentCount": 0
}
],
"consultantOptions": [
{
"value": "2000000000000000001",
"label": "企业微信昵称"
}
]
}
}
```
一致性规则:
```text
同一组非状态筛选条件下:
summary.statusOptions[value=X].count
== orders?statuses=X 返回的 data.total
```
`idleVehicleCount/idleDriverCount` 是当前物理资源指标,不受订单文字筛选影响;其他订单状态计数使用同一筛选后的记录集。
### 3.4 列表请求示例
```http
GET /admin/fleet/board/orders?page=1&pageSize=20&statuses=unassigned,holding&keyword=7218&consultantId=2000000000000000001
Authorization: Bearer <fleet-manager-token>
```
### 3.5 列表响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"page": 1,
"pageSize": 20,
"total": 1,
"records": [
{
"id": "HL202607180001",
"orderNo": "HL202607180001",
"orderId": "2000000000000000101",
"teamNo": "26-7218",
"assignmentId": "2000000000000000201",
"assignmentGroupId": "2000000000000000201",
"fleetItemIndex": 0,
"customerName": "测试联系人",
"contactName": "测试联系人",
"productName": "测试产品",
"headcount": 4,
"adultCount": 3,
"childCount": 1,
"youngChildCount": 0,
"babyCount": 0,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"days": 3,
"pickupAt": null,
"dropoffAt": null,
"isHailarPickup": false,
"isHailarDropoff": false,
"consultantId": "2000000000000000001",
"plannerName": "企业微信昵称",
"consultantName": "企业微信昵称",
"consultantDisplayName": "企业微信昵称",
"specialTags": ["中文司机", "大行李空间"],
"requirementRemark": "无大交通,按行程安排车辆",
"requiredVehicles": [
{
"vehicleType": "suv",
"categoryLabel": "SUV系列",
"seats": 7,
"count": 1
}
],
"assignmentStatus": "unassigned",
"assignmentStatusLabel": "待派车",
"lifecycleStageCode": "requirement_pending",
"lifecycleStageLabel": "待车务派车",
"currentStep": 1,
"availableActionCodes": ["ASSIGN", "REJECT_REQUIREMENT"],
"urgentBadge": null,
"canAssign": true,
"canRejectRequirement": true
}
]
}
}
```
### 3.6 列表字段绑定规则
| 字段 | 前端规则 |
|---|---|
| `teamNo` | 展示当前团号;空值不回退拼造。 |
| `contactName` | 卡片联系人。 |
| `consultantDisplayName` | 定制师展示名,企业微信昵称优先、用户名兜底。 |
| `startDate/endDate/days` | 当前订单档期,闭区间含首尾。 |
| `specialTags/requirementRemark` | 当前有效用车需求,不得混入历史需求。 |
| `assignmentStatusLabel/lifecycleStageLabel` | 直接展示,前端不维护独立中文映射。 |
| `availableActionCodes/canAssign/canRejectRequirement` | 决定操作入口;急单样式不得隐藏按钮。 |
列表按派车组聚合;底层一天一条派车切片不会把同一派车组重复成多张卡片。紧急待处理在前、普通进行中次之、终态沉底,同优先级以稳定 ID 兜底;前端不得二次排序。
## 四、派单详情
### 4.1 请求
```http
GET /admin/fleet/board/orders/2000000000000000101
Authorization: Bearer <fleet-manager-token>
```
路径参数是数字订单 ID,按字符串传递,不是订单号。
### 4.2 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"id": "HL202607180001",
"orderNo": "HL202607180001",
"teamNo": "26-7218",
"customerName": "测试联系人",
"headcount": 4,
"adultCount": 3,
"childCount": 1,
"youngChildCount": 0,
"babyCount": 0,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"pickupAt": null,
"dropoffAt": null,
"productName": "测试产品",
"consultantId": "2000000000000000001",
"plannerName": "企业微信昵称",
"consultantName": "企业微信昵称",
"consultantDisplayName": "企业微信昵称",
"specialTags": ["中文司机", "大行李空间"],
"requirementRemark": "无大交通,按行程安排车辆",
"itinerary": {
"theme": "草原三日",
"route": "海拉尔 → 额尔古纳 → 满洲里",
"days": [
{
"dayNumber": 1,
"date": "2026-07-20",
"title": "抵达海拉尔",
"detail": "市区行程"
},
{
"dayNumber": 2,
"date": "2026-07-21",
"title": "额尔古纳",
"detail": "草原行程"
},
{
"dayNumber": 3,
"date": "2026-07-22",
"title": "满洲里",
"detail": "返程"
}
]
},
"transport": {
"transferTimeHint": "暂无接送机时间",
"arrive": null,
"depart": null,
"batches": [],
"pickupRequired": null
},
"currentAssignment": {
"id": "2000000000000000201",
"assignmentGroupId": "2000000000000000201",
"fleetItemIndex": 0,
"requiredVehicleType": "suv",
"requiredSeats": 7,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"vehicleId": "2000000000000000301",
"vehiclePlate": "蒙A·TEST1",
"driverId": "2000000000000000401",
"driverName": "测试司机",
"baseAssignmentStatus": "assigned",
"assignmentStatus": "assigned",
"lifecycleStageCode": "confirmed",
"lifecycleStageLabel": "已确认执行",
"currentStep": 4,
"availableActionCodes": ["CANCEL", "CHANGE_DRIVER", "COMPLETE_EARLY"],
"protocolPrice": "1300.00",
"driverConfirmationEvidenceFileIds": []
},
"activeAssignments": [
{
"assignmentGroupId": "2000000000000000201",
"fleetItemIndex": 0,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"vehiclePlate": "蒙A·TEST1",
"driverName": "测试司机",
"assignmentStatus": "assigned"
}
],
"operationLog": {
"records": [],
"total": 0,
"summary": {"totalCount": 0}
},
"relatedDetailReady": true
}
}
```
### 4.3 当前需求和派车组隔离
- `currentAssignment` 是最新一个有效派车组的兼容字段。
- `activeAssignments` 才是当前需求下全部有效派车组;一单多车时必须渲染完整数组。
- 历史需求、已取消组和旧订单快照不能进入当前需求详情。
- `baseAssignmentStatus` 是落库基础态;`assignmentStatus` 是当前有效状态,前端展示后者。
### 4.4 逐日行程规则
- `itinerary.days` 来自 `order_itinerary_day`,一天一条事实数据。
- 返回日期必须位于当前 `[startDate,endDate]`,按 `dayNumber` 稳定排序。
- 改期后按当前订单档期对齐;越界、旧版本和非法日序数据不返回。
- 无有效行程时返回 `days=[]`,前端显示空态,不得生成假行程。
### 4.5 大交通规则
有数据时:
- 到达接客使用大交通 `arriveTime`。
- 返程送客使用大交通 `departTime`。
- 分批接送完整返回 `batches`。
无数据时固定返回:
```json
{
"transport": {
"transferTimeHint": "暂无接送机时间",
"arrive": null,
"depart": null,
"batches": [],
"pickupRequired": null
}
}
```
**空大交通是正常业务状态,不是参数错误,也不是派车阻断条件。** `pickupAt/dropoffAt` 同样允许为 `null`。
## 五、车辆与司机候选
### 5.1 请求
```http
POST /admin/fleet/assignments/candidates
Authorization: Bearer <fleet-manager-token>
Content-Type: application/json
```
```json
{
"orderId": "2000000000000000101",
"requirementId": "2000000000000000501",
"fleetItemIndex": 0,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"headcount": 4,
"selectedVehicleId": null,
"selectedDriverId": null,
"excludeAssignmentId": null,
"vehicleKeyword": "GL8",
"driverKeyword": "张",
"vehiclePage": 1,
"vehiclePageSize": 20,
"driverPage": 1,
"driverPageSize": 20
}
```
`pickupAt/dropoffAt` 未出现是合法请求;有城市信息时可额外传入,供城市衔接判断使用。
### 5.2 请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `orderId` | `string` | 改派时是 | 当前订单 ID。 |
| `requirementId` | `string` | 改派时是 | 当前有效用车需求 ID。 |
| `fleetItemIndex` | `int` | 否 | 当前车型项序号,从 0 开始。 |
| `startDate/endDate` | `date` | 是 | 请求用车闭区间。 |
| `pickupAt/dropoffAt` | `string` | 否 | 城市衔接辅助信息;空值不阻断候选和派车。 |
| `headcount` | `int` | 否 | 乘客人数,不含司机。车辆乘客容量=`seats-1`。 |
| `selectedVehicleId` | `string` | 否 | 已选车辆,支持先选车。 |
| `selectedDriverId` | `string` | 否 | 已选司机,支持先选司机。 |
| `excludeAssignmentId` | `string` | 否 | 改派时排除当前派单,且必须属于当前订单和需求。 |
| `vehicleKeyword` | `string` | 否 | 车牌或车型。 |
| `driverKeyword` | `string` | 否 | 司机姓名或完整手机号。 |
| `vehiclePage/driverPage` | `int` | 是 | 各自分页页码,默认 1。 |
| `vehiclePageSize/driverPageSize` | `int` | 是 | 各自每页条数,最大 100。 |
### 5.3 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"vehicles": {
"records": [
{
"vehicleId": "2000000000000000301",
"plate": "蒙A·TEST1",
"modelName": "测试车型",
"seats": 7,
"passengerCapacity": 6,
"seatsEnough": true,
"fleet": "own",
"selected": false,
"available": true,
"availabilityReasonCode": "AVAILABLE",
"availabilityReasonMessage": "所选服务日期内可用",
"availabilityWindows": [
{"startDate": "2026-07-20", "endDate": "2026-07-22"}
],
"residentMatch": false,
"crossResident": false,
"requiresCrossResidentConfirmation": false,
"relationMessage": null,
"conflicts": []
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"drivers": {
"records": [
{
"driverId": "2000000000000000401",
"name": "测试司机",
"maskedPhone": "138****0000",
"years": 8,
"season": "active",
"completedOrderCount": 12,
"rating": 5.0,
"ratingDefaulted": true,
"selected": false,
"available": true,
"availabilityReasonCode": "CITY_JUNCTION_SHAREABLE",
"availabilityReasonMessage": "仅存在可衔接的同城边界占用",
"availabilityWindows": [
{"startDate": "2026-07-20", "endDate": "2026-07-22"}
],
"residentMatch": false,
"crossResident": false,
"requiresCrossResidentConfirmation": false,
"conflicts": []
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"selectedRelation": null
}
}
```
### 5.4 候选判定规则
| `availabilityReasonCode` | 含义 | `available` |
|---|---|---|
| `AVAILABLE` | 整个请求闭区间无阻塞占用 | `true` |
| `CITY_JUNCTION_SHAREABLE` | 只有满足规则的城市边界衔接 | `true` |
| `ASSIGNMENT_CONFLICT` | 请求区间内存在阻塞派单 | `false` |
- `availabilityWindows` 是请求区间内的实际可用**闭区间**,冲突会切分时间窗。
- 车辆和司机都必须覆盖完整请求区间才可直接选中。
- 司机无评价时返回 `rating=5.0` 且 `ratingDefaulted=true`;有真实评价时为 `false`。
- 车辆总座位数包含司机,`passengerCapacity=seats-1`;前端不得把司机座位再次给乘客。
- `selectedRelation` 在车辆和司机都已选时返回常驻关系。跨常驻组合必须显示后端提示并显式确认。
## 六、矩阵月视图
### 6.1 请求
```http
GET /admin/fleet/matrix/grid?year=2026&month=7&season=active&fleets=own,coopA&typeKeys=suv&status=all
Authorization: Bearer <fleet-manager-token>
```
### 6.2 请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `year` | `int` | 是 | 查询年份。 |
| `month` | `int` | 是 | 1-12,越界返回车务月份错误。 |
| `season` | `string` | 否 | `active` 默认;也支持 `pending/archived/blacklist`。 |
| `fleets` | `string[]` | 否 | `own/coopA/coopB` 多选。 |
| `typeKeys` | `string[]` | 否 | `suv/mpv/bus/sedan` 多选。 |
| `status` | `string` | 否 | `all` 默认、`unassigned`、`assigned`。 |
### 6.3 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"year": 2026,
"month": 7,
"daysInMonth": 31,
"todayDay": 18,
"weekendDays": [4, 5, 11, 12, 18, 19, 25, 26],
"fleetCount": {"own": 5, "coopA": 4, "coopB": 3},
"statusCounts": {
"totalAssignments": 21,
"unassignedAssignments": 10,
"assignedAssignments": 11,
"totalOrders": 16,
"unassignedOrders": 5,
"partialOrders": 2,
"assignedOrders": 11
},
"unassignedWindowCount": 5,
"vehicles": [
{
"id": "2000000000000000301",
"plate": "蒙A·TEST1",
"modelName": "测试车型",
"seats": 7,
"fleet": "own",
"primaryDriverName": "测试司机",
"primaryDriverPhone": "138****0000",
"assignments": [
{
"id": "2000000000000000201",
"assignmentGroupId": "2000000000000000201",
"orderNumericId": "2000000000000000101",
"orderNo": "HL202607180001",
"teamNo": "26-7218",
"consultantId": "2000000000000000001",
"consultantName": "企业微信昵称",
"customerName": "测试联系人",
"headcount": 4,
"adultCount": 3,
"childCount": 1,
"startDay": 20,
"endDay": 22,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"clippedHead": false,
"clippedTail": false,
"vehicleCategory": "suv",
"categoryLabel": "SUV系列",
"assignmentStatus": "assigned",
"protocolPrice": "1300.00",
"vehicleSpecialTags": ["中文司机"],
"vehicleRequirementRemark": "按行程安排",
"pickupTransports": [],
"dropoffTransports": [],
"parallelAssignments": [
{
"assignmentGroupId": "2000000000000000201",
"fleetItemIndex": 0,
"vehicleCategory": "suv",
"categoryLabel": "SUV系列",
"requiredSeats": 7,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"vehicleId": "2000000000000000301",
"vehiclePlate": "蒙A·TEST1",
"driverId": "2000000000000000401",
"driverName": "测试司机",
"assignmentStatus": "assigned",
"protocolPrice": "1300.00"
}
]
}
],
"connections": [
{
"fromAssignmentGroupId": "2000000000000000201",
"toAssignmentGroupId": "2000000000000000202",
"fromEndDate": "2026-07-22",
"toStartDate": "2026-07-23",
"previousDepartureTime": null,
"nextArrivalTime": null,
"previousCity": null,
"nextCity": null,
"connectionMinutes": null,
"thresholdMinutes": 120,
"status": "MISSING_TIME",
"statusLabel": "缺少接送时间",
"style": "RED_DASHED",
"reasonCode": "PREVIOUS_REQUIRED_DEPARTURE_MISSING",
"reasonMessage": "前一订单缺少必需送客批次"
}
]
}
]
}
}
```
### 6.4 派单统计口径
- `totalAssignments` 是派车行数,不是订单数。
- `totalOrders` 是去重订单数。
- 一单同时存在已派和未派项时计入 `partialOrders`,也计入 `unassignedOrders`。
- `unassignedWindowCount` 是含未派项的去重订单数,不等于未派逐日切片条数。
- 跨月派车组使用完整原始 `startDate/endDate`,仅 `startDay/endDay` 裁剪到当前月;`clippedHead/clippedTail` 告诉前端是否跨月续接。
### 6.5 相邻订单衔接四态
| `status` | `style` | 含义 |
|---|---|---|
| `MISSING_TIME` | `RED_DASHED` | 缺少必需大交通、时刻或城市,无法完成衔接判断。 |
| `DIFFERENT_CITY` | `DARK_RED` | 前后订单城市不同。 |
| `SAME_CITY_TOO_SHORT` | `LIGHT_RED` | 同城但间隔小于配置阈值。 |
| `SAME_CITY_OK` | `GREEN` | 同城且间隔达到配置阈值。 |
原因码:
```text
PREVIOUS_REQUIRED_DEPARTURE_MISSING
NEXT_REQUIRED_ARRIVAL_MISSING
PREVIOUS_DEPARTURE_TIME_MISSING
NEXT_ARRIVAL_TIME_MISSING
PREVIOUS_DEPARTURE_CITY_MISSING
NEXT_ARRIVAL_CITY_MISSING
DIFFERENT_CITY
SAME_CITY_INTERVAL_TOO_SHORT
SAME_CITY_INTERVAL_SUFFICIENT
```
同城最小衔接阈值读取 Nacos 配置,默认 120 分钟;恰好等于阈值属于 `SAME_CITY_OK`。
注意:`MISSING_TIME` 是矩阵风险提示,不表示订单不能派车。用户未填写大交通时仍允许完成派车。
## 七、出行人权限与审计
### 7.1 脱敏列表
```http
GET /admin/fleet/board/orders/2000000000000000101/travelers
Authorization: Bearer <fleet-manager-token>
```
默认返回姓名、年龄类型和脱敏证件/手机号;前端日常派车只使用此接口。
### 7.2 明文查询
```http
POST /admin/fleet/board/orders/2000000000000000101/travelers/plain
Authorization: Bearer <fleet-manager-token>
Content-Type: application/json
```
```json
{
"reason": "司机出发前核对接客人信息"
}
```
明文接口必须经过权限校验并记录操作人、订单、理由和时间。前端不得缓存、日志打印或二次持久化明文个人信息。
## 八、前端必须处理
1. 看板使用分页接口,分页器读取 `data.total/page/pageSize`。
2. 状态项、状态中文、急单数读取 `summary.statusOptions`,不维护独立枚举和独立计数。
3. 汇总请求必须携带与列表相同的非状态筛选;不要把当前状态筛选传成汇总统计范围。
4. 卡片展示 `teamNo/contactName/headcount/consultantDisplayName/startDate/endDate/specialTags/requirementRemark`。
5. 定制师选择值传 `consultantId`;显示使用后端返回的企业微信优先名称。
6. 统一文本框传 `keyword`,可同时匹配司机、联系人、团号、订单号和当前定制师展示名;定制师按企业微信昵称优先、用户名兜底,前端不得自行并行匹配多个名称别名。
7. 操作按钮使用 `availableActionCodes/canAssign/canRejectRequirement`,不能按颜色或前端状态猜测。
8. 详情多车读取 `activeAssignments`;`currentAssignment` 只是兼容的最新一组。
9. `transport.transferTimeHint="暂无接送机时间"` 时显示提示,但保持派车入口可用。
10. 候选资源分别读取 `vehicles/drivers` 分页;显示后端原因和常驻关系提示。
11. 矩阵直接使用 `connections[].status/style/reasonCode/reasonMessage`;大红、淡红、虚线红和绿色语义不能自行交换。
12. 所有雪花 ID 当字符串处理。
## 九、兼容性与不影响范围
- 保留 `status/startDate/endDate/typeKeys/contactKeyword/consultantName` 等兼容别名。
- 不新增前端内部接口,不改变现有派车写接口。
- 不要求填写大交通,也不把空大交通改成校验错误。
- 不改变“一天一条派车切片”的数据库事实模型;本轮只修正读模型聚合。
- 不处理团期配车。
- 不修改 `hl-ui`。
## 十、验证证据
### 10.1 代码与测试
```text
hl-fleet-service 定向测试:253/253 通过
hl-fleet-service 全量 verify:1799/1799 通过
hl-order-service-v3 相关契约测试:30/30 通过
独立代码评审:无 P0/P1 阻断项
OpenAPI 说明定向校验:spotless:check + compile 通过
```
Order-v3 全量测试中 4 项环境/基线失败已在同提交干净基线复现:3 项为 H2 缺少 `payment_manual_receipt`,1 项为既有本地缓存架构门禁;不由本次车务改动引入。
### 10.2 部署
```text
hl-order-service-v3:Deploy Panel 任务 22ec81e8,8086/8186 双实例成功
hl-fleet-service:Deploy Panel 任务 68487f94,8087/8187 双实例成功
hl-fleet-service OpenAPI 口径补充:Deploy Panel 任务 249594e3,8087/8187 双实例成功
```
部署后已通过网关读取车务 OpenAPI,确认线上文档明确区分“同一订单筛选范围内的状态计数”与“不随订单筛选变化的全局空闲资源数”,并包含统一关键词对当前定制师展示名的匹配规则。
### 10.3 真实网关 API
使用独立车务和定制师测试账号,经 `https://api.test.1814.love:9443` 完成真实订单全流程回归;未使用 `admin`、`wx` 或 Mock 数据。
```text
最终回归:161/161 通过,失败 0
覆盖:看板汇总/列表/详情、统一筛选、定制师、脱敏/明文出行人、候选车辆/司机、
常驻与跨常驻、预检、派车、司机确认、取消/恢复/改派/提前完结、矩阵、价格、
车辆/司机、对账、模板,以及无大交通订单完整派车链路。
```
无大交通真实场景额外断言:
```text
- 新建真实订单并补齐 2 名出行人
- 不创建任何大交通计划
- 成功提交有效用车需求
- 详情:arrive=null、depart=null、batches=[]、transferTimeHint=暂无接送机时间
- 候选查询成功,pickupAt/dropoffAt 均省略
- 派车预检成功,conflict=false
- 直派成功并进入已派车状态
- DB:派车组逐日 6 条,pickup_at/dropoff_at 6 条均为空
```
## 十一、相关文档
- 看板字段、统计、行程和保险事务:`57_4882_车务派单看板当前订单字段统计行程与保险事务收口-管理后台.md`
- 需求级最终完成回调:`59_4935_车务需求级派单完成回调与滚动发布契约-管理后台.md`
- 提需求和派单基础契约:`53_4871_车务提需求派单看板闭环契约-管理后台.md`
- 团号与定制师筛选:`54_4876_派单看板团号与定制师下拉筛选-管理后台.md`
@@ -0,0 +1,213 @@
# 【前端对接·管理后台】房务待办新增“待最终确认”派生项
> Issue: [wx/HL#5053](https://git.1814.love:8443/wx/HL/issues/5053)
>
> PR: [wx/HL#5059](https://git.1814.love:8443/wx/HL/pulls/5059)
>
> 合并提交: `989318c3925b`
>
> 服务: `hl-order-service-v3` / `hl-user-service`
>
> 日期: 2026-07-18
>
> 影响范围: 房务“待处理”列表、待办类型筛选、订单详情跳转、房务工作台待办统计
## 一、对接结论
1. 房务“待处理”页的唯一主数据源仍是 `GET /v3/admin/order/todos`,不要改用任何 `my-claims` 接单列表接口。`my-claims` 不返回完整的待办类型聚合,不能替代待办接口。
2. 待办接口新增可选类型 `PENDING_FINALIZE`,中文标签为“待最终确认”。它是查询时派生的虚拟待办,不落 `house_todo`,因此 `derived=true`、标签级 `todoId=null`。
3. 当前房务持有的 active 住宿需求处于 `status=PROCESSING`、`houseStatus=PENDING_FINALIZE` 时,接口返回该派生项;最终确认后需求进入 `DONE/CONFIRMED`,该项从列表、筛选结果和统计中自然消失。
4. `list[].todoTypes[]` 是一订单多标签的权威数据。点击 `PENDING_FINALIZE` 标签时必须使用该标签自己的 `requirementId`,不能依赖聚合行顶层 `requirementId`。
5. 派生项不可调用待办 `RESOLVE`。用户应打开 `OrderDetailModal` 完成“最终确认”,成功后刷新待办列表与工作台仪表盘。
6. 房务工作台 `GET /admin/profile/dashboard` 的 `todoSummary` 同步新增大写键 `PENDING_FINALIZE`。
7. 本次没有修改 `hl-ui`;下文列出的现有前端筛选和标签级参数问题需由前端处理。
## 二、待办接口变化
### 2.1 请求
```http
GET /v3/admin/order/todos?scope=mine&status=OPEN&page=1&pageSize=20
Authorization: Bearer <room-manager-token>
```
只看“待最终确认”时:
```http
GET /v3/admin/order/todos?scope=mine&status=OPEN&todoType=PENDING_FINALIZE&page=1&pageSize=20
Authorization: Bearer <room-manager-token>
```
查询参数名是 `todoType`,不是 `type`。`todoType` 支持逗号分隔多选。
### 2.2 响应示例
```json
{
"code": 200,
"data": {
"list": [
{
"id": null,
"orderId": "2000000000000000001",
"orderNo": "HL202607180001",
"todoType": "PENDING_FINALIZE",
"todoTypeLabel": "待最终确认",
"title": "待最终确认",
"urgency": "normal",
"status": "OPEN",
"derived": true,
"requirementId": "2000000000000000101",
"orderTodoCount": 1,
"todoTypes": [
{
"typeCode": "PENDING_FINALIZE",
"typeLabel": "待最终确认",
"urgency": "normal",
"count": 1,
"derived": true,
"todoId": null,
"requirementId": "2000000000000000101",
"unreadCount": null
}
]
}
],
"total": 1,
"stats": {
"PENDING_FINALIZE": 1
}
}
}
```
字段规则:
| 字段 | 前端规则 |
|---|---|
| `list[].todoTypes[]` | 一订单多待办类型的权威标签数组,必须完整渲染。 |
| `todoTypes[].typeCode` | 新增可选值 `PENDING_FINALIZE`。 |
| `todoTypes[].typeLabel` | 直接展示后端中文“待最终确认”。 |
| `todoTypes[].derived` | `true` 表示虚拟待办,由源业务状态自然消失。 |
| `todoTypes[].todoId` | `PENDING_FINALIZE` 固定为 `null`,禁止调用 `RESOLVE`。 |
| `todoTypes[].requirementId` | 打开该标签对应房务详情时使用;雪花 ID 按字符串透传。 |
| `list[].requirementId` | 只兼容聚合行主标签;一行多标签时不能替代标签级字段。 |
| `stats.PENDING_FINALIZE` | 当前 scope 内“待最终确认”需求数;无数据也返回 `0`。 |
`stats` 是当前 scope 的完整分类计数,不因本次 `todoType` facet 收窄;因此筛选结果 `total` 可以是 `1`,同时其他统计槽仍保留其真实值。
### 2.3 生命周期
```text
当前房务 + active requirement
status=PROCESSING + houseStatus=PENDING_FINALIZE
→ /todos 出现 PENDING_FINALIZE 派生标签
→ 用户从该标签进入订单详情并执行最终确认
→ status=DONE + houseStatus=CONFIRMED
→ /todos、todoType facet、stats 和 dashboard 中该项均消失/归零
```
该链路不创建 `house_todo` 记录,也不改变既有持久化待办的 RESOLVE 语义。
## 三、房务工作台统计变化
房务角色调用:
```http
GET /admin/profile/dashboard?period=today
Authorization: Bearer <room-manager-token>
```
`data.todoSummary` 新增:
```json
{
"total": 4,
"PENDING_FINALIZE": 1
}
```
- 键名固定为大写 `PENDING_FINALIZE`,与待办接口 `stats`、`todoTypes[].typeCode` 共用同一常量。
- 最终确认后该值归零;前端刷新列表时应同时刷新工作台数据或使对应查询缓存失效。
## 四、前端必须处理的现有问题
### 4.1 筛选器混用了“房务状态”和“待办类型”
当前 `src/views/housekeeper/todos/index.vue:220-227` 把以下值放在同一个 `STATUS_OPTIONS` 中:
```text
HOTEL_REPLY_TIMEOUT / PENDING_ARRANGE /
CLAIMING / IN_INQUIRY / PENDING_FINALIZE / EXCEPTION
```
但同文件 `:370` 固定请求 `status=OPEN`,`:374` 又把所有非“全部”选项都作为 `todoType`,最终由 `:386` 请求待办接口。
- `HOTEL_REPLY_TIMEOUT`、`PENDING_ARRANGE`、`PENDING_FINALIZE` 是合法 `HouseTodoType`。
- `CLAIMING`、`EXCEPTION` 是房务业务状态,不是待办类型,作为 `todoType` 请求会得到空结果。
- `IN_INQUIRY` 已不再是顶层房务状态,也不是待办类型。
前端应删除这三个无效 `todoType` 选项;如产品确实需要按房务状态筛选,应另行使用声明支持该参数的数据源,不能继续混传给 `/todos.todoType`。
同时,`src/api/housekeeper/todos.js:53` 的注释仍写 `params.type`,实际页面和后端均使用 `params.todoType`;请同步修正文档注释,API 调用本身仍是 `:66-67` 的 `/v3/admin/order/todos`。
### 4.2 标签级 `requirementId` 在映射时丢失
当前 `src/views/housekeeper/todos/index.vue:290-313` 把 `todoTypes[]` 映射成 `reasonTags` 时只保留了 `typeCode/label/derived`,未保留标签自己的 `requirementId`;`:267` 和 `:453` 只使用聚合行顶层 `requirementId`。
一订单多标签时,顶层字段属于“主标签”,不保证就是用户点击的 `PENDING_FINALIZE` 标签。建议保留标签字段并按点击项分发:
```js
const pendingFinalizeTag = row.todoTypes.find(
(item) => item.typeCode === 'PENDING_FINALIZE'
)
openOrderDetail({
orderId: row.orderId,
requirementId: pendingFinalizeTag?.requirementId,
claimScope: 'mine',
})
```
实际组件仍应复用现有 `OrderDetailModal`,传入:
```vue
<OrderDetailModal
:order-id="orderId"
:requirement-id="requirementId"
claim-scope="mine"
/>
```
所有 ID 均按字符串处理,禁止转为 JavaScript `Number`。
### 4.3 完成动作
`PENDING_FINALIZE` 的处理入口是订单详情内“最终确认”,不是待办 `RESOLVE`:
1. 从点击标签取得 `orderId + todoTypes[].requirementId`。
2. 以 `claimScope=mine` 打开 `OrderDetailModal`。
3. 用户执行一次“最终确认”。
4. 成功后刷新 `/v3/admin/order/todos` 和 `/admin/profile/dashboard`。
5. 列表行、筛选计数和工作台徽章应同步消失/归零。
## 五、已完成的后端与测试环境验证
- PR #5059 已合并到 `dev-v3`,合并提交为 `989318c3925b`。
- `hl-order-service-v3` TEST 部署任务 `fd8522fe` 成功,`8086/8186` 双实例健康。
- `hl-user-service` TEST 部署任务 `372a9f3e` 成功,`8081/8181` 双实例健康。
- 网关 API 实测:进入待最终确认前 `PENDING_FINALIZE=0`;测试需求进入 `PROCESSING/PENDING_FINALIZE` 后,列表、facet、`stats` 和 dashboard 均为 `1`;最终确认后均恢复为 `0`。
- TEST DB 对账:派生项出现时没有新增 `house_todo(PENDING_FINALIZE)`;最终确认后需求为 `DONE/CONFIRMED`,两晚配房仍为 `CONFIRMED`,房务归属和配房数据均保留。
- 真实调试 Chrome 已验证“待最终确认”标签可见、筛选只剩目标订单、最终确认后列表空态;浏览器验收仅作为前端对接参考,不改变上述接口契约。
## 六、前端验收清单
- [ ] “待处理”页仅以 `GET /v3/admin/order/todos` 为主数据源。
- [ ] `STATUS_OPTIONS` 不再把 `CLAIMING/IN_INQUIRY/EXCEPTION` 作为 `todoType` 发送。
- [ ] 使用 `todoType=PENDING_FINALIZE` 可筛出“待最终确认”订单。
- [ ] 完整渲染 `list[].todoTypes[]`,显示后端 `typeLabel`。
- [ ] 映射和点击事件保留 `todoTypes[].requirementId`。
- [ ] 以 `orderId + 标签级 requirementId + claimScope=mine` 打开 `OrderDetailModal`。
- [ ] 派生标签不调用待办 `RESOLVE`。
- [ ] 最终确认成功后刷新待办列表和 dashboard,标签与计数同步消失。
- [ ] 所有雪花 ID 均按字符串透传。
@@ -0,0 +1,510 @@
# 【#4938 前端对接·管理后台】车务创建、修改与最终确认返回订单调整基线差异
> Issue: [wx/HL#4938](https://git.1814.love:8443/wx/HL/issues/4938)
>
> PR: [wx/HL#5065](https://git.1814.love:8443/wx/HL/pulls/5065)
>
> 服务: `hl-fleet-service`
>
> 日期: 2026-07-18
>
> 影响范围: 车务直接派车、直接改派、`holding → assigned` 最终确认,以及订单调整后的日期、人数、车辆容量差异处理
>
> 状态: 后端已合并并部署测试环境;待管理后台按本文完成页面联调
## 一、前端对接结论
以下三个既有管理后台接口现在共用业务码 `605041` 返回结构化逐日差异:
| 操作 | 接口 | 触发 605041 的条件 |
|---|---|---|
| 创建派单 | `POST /admin/fleet/assignments` | `holdMode=0` 直接派车时最终基线不一致 |
| 修改派单 | `POST /admin/fleet/assignments/{assignmentId}/change` | `holdMode=0` 直接改派时最终基线不一致 |
| 最终确认 | `POST /admin/fleet/assignments/{assignmentId}/confirm` | 订单、需求、行程、逐日派单、人数或容量基线不一致 |
前端必须遵守以下判断:
1. 三个接口的 `605041` 当前均返回 HTTP 200,但统一响应体为 `code=605041`、`success=false`,不能只判断 HTTP 状态。
2. `605041` 的 `data` 不为空:
- create 返回 `AssignmentWriteRespVO`,保证 `data.dailyDifferences` 可读;
- change 返回 `ChangeAssignmentRespVO`,保证 `data.dailyDifferences` 可读;
- confirm 返回 `ConfirmRespVO`,保证 `data.confirmed=false` 和 `data.dailyDifferences` 可读。
3. `605041` 不会提交派单及其关联业务状态写入:
- create 不会新增或激活派单;
- change 不会取消旧派单,也不会生成可用的新派车版本;
- confirm 不会推进派单状态或确认时间;
- 三者都不会触发车辆/司机占用、保险、对账或订单派定结果变化。
- change 仍会按既有设计在独立事务保留一条 `CHANGE_FAILED` 操作审计;它不是有效派单版本,也不表示业务写入成功。
4. 收到 `605041` 后保持操作前页面状态,展示逐日差异,并重新拉取最新订单和车务详情。
5. 所有派单、车辆槽位、派车组、订单、车辆和司机雪花 ID 均按 JSON String 发送和读取,禁止转为 JavaScript `Number`。金额字段也按 String 读取。
## 二、605041 公共响应契约
### 2.1 统一响应外层
```json
{
"code": 605041,
"message": "订单行程或用车派单已变化,请按逐日差异处理后重试",
"data": {
"dailyDifferences": [
{
"serviceDate": "2026-07-22",
"differenceType": "CAPACITY_INSUFFICIENT",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": 5,
"capacityGap": 3,
"message": "车辆载客量不足,已按每车司机占一座计算"
}
]
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938f09a001",
"success": false
}
```
注意:
- `data` 的完整类型取决于调用的是 create、change 还是 confirm,不能跨接口复用成功响应模型。
- 除本文件明确保证的失败字段外,其余成功态字段在 `605041` 时为空,前端不得用它们推断写入结果。
- `traceId` 用于反馈和日志定位,不参与业务判断。
### 2.2 `dailyDifferences[]`
| 字段 | 类型 | 说明 |
|---|---|---|
| `serviceDate` | String/null | 发生差异的服务日,格式 `yyyy-MM-dd`;无法定位到单日时为空 |
| `differenceType` | String | 差异类型,取值见下表 |
| `assignmentId` | String/null | 可定位到具体派单时返回;仅用于定位差异,不代表本次写入成功 |
| `assignmentSlotId` | String/null | 可定位到稳定车辆槽位时返回 |
| `passengerCount` | Integer/null | 当前比对使用的乘客人数,不含司机 |
| `passengerCapacity` | Integer/null | 当日车辆合计可载客人数,每辆车已扣除司机一座 |
| `capacityGap` | Integer/null | 缺少座位数,等于 `passengerCount - passengerCapacity` |
| `message` | String | 后端生成的差异说明,可直接辅助展示 |
差异类型:
| `differenceType` | 含义 |
|---|---|
| `REQUIREMENT_VERSION_MISMATCH` | 当前生效用车需求已变化,或订单/需求已不可继续派单 |
| `ORDER_DATE_MISMATCH` | 订单当前日期与用车需求冻结日期不一致 |
| `ITINERARY_DATE_MISMATCH` | 逐日行程日期与用车需求冻结日期不一致,或缺少逐日行程 |
| `ASSIGNMENT_DATE_MISSING` | 某服务日缺少有效派单或车辆槽位 |
| `ASSIGNMENT_DATE_EXTRA` | 派单仍包含已不属于当前需求的服务日 |
| `HEADCOUNT_BASELINE_MISMATCH` | 订单当前人数、需求冻结人数或派单人数快照不一致 |
| `CAPACITY_INSUFFICIENT` | 当日所有车辆合计载客量不足 |
`dailyDifferences` 可能同时包含多种类型、多条服务日记录。前端应遍历数组展示,不得只取第一条,也不得自行重算人数或车辆容量。
## 三、创建派单 create
### 3.1 请求
```http
POST /admin/fleet/assignments
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"orderId": "2078001000000000101",
"orderNo": "26-0719",
"requirementId": "2078001000000000201",
"fleetItemIndex": 1,
"vehicleId": "2078001000000000301",
"driverId": "2078001000000000401",
"startDate": "2026-07-21",
"endDate": "2026-07-23",
"pickupAt": "海拉尔",
"dropoffAt": "满洲里",
"headcount": 8,
"protocolPrice": "1300.00",
"holdMode": 0,
"fromEntry": "from-board",
"skipCityJunctionException": false,
"strictSeats": true,
"confirmCrossResident": false,
"requestId": "fleet-create-4938-20260719-001"
}
```
`605041` 只适用于 `holdMode=0`。原有 `holdMode=1` 排车锁定流程不执行本次最终基线门禁。
### 3.2 holdMode=0 成功响应
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2078001000000000501",
"assignmentGroupId": "2078001000000000601",
"assignmentSlotId": "2078001000000000701",
"assignmentStatus": "assigned",
"stageCode": "assigned",
"stageLabel": "已派车",
"currentStep": 4,
"skippedStepCodes": [
"DRIVER_CONFIRMATION",
"DRIVER_CONFIRMATION_EVIDENCE"
],
"protocolPrice": "1300.00",
"holdSentAt": null,
"confirmedAt": "2026-07-19 14:20:00",
"sideEffects": {
"vehicleStatusUpdated": "busy",
"driverStatusUpdated": "busy",
"reconPrepRowsCreated": 0,
"reconPrepMarkedCanceled": null
},
"dailyDifferences": null
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938c200001",
"success": true
}
```
仅在 `code=200` 时把新派单加入页面;`id`、`assignmentGroupId`、`assignmentSlotId` 均按 String 保存。
### 3.3 605041 失败响应
```json
{
"code": 605041,
"message": "订单行程或用车派单已变化,请按逐日差异处理后重试",
"data": {
"id": null,
"assignmentGroupId": null,
"assignmentSlotId": null,
"assignmentStatus": null,
"stageCode": null,
"stageLabel": null,
"currentStep": null,
"skippedStepCodes": null,
"protocolPrice": null,
"holdSentAt": null,
"confirmedAt": null,
"sideEffects": null,
"dailyDifferences": [
{
"serviceDate": "2026-07-22",
"differenceType": "CAPACITY_INSUFFICIENT",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": 5,
"capacityGap": 3,
"message": "车辆载客量不足,已按每车司机占一座计算"
}
]
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938c605041",
"success": false
}
```
此时不得把临时响应内容加入派单列表,不得本地占用车辆/司机;刷新后仍以服务端最新详情为准。
## 四、修改派单 change
### 4.1 请求
```http
POST /admin/fleet/assignments/2078001000000000501/change
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"effectiveDate": "2026-07-22",
"newVehicleId": "2078001000000000302",
"newDriverId": "2078001000000000402",
"holdMode": 0,
"protocolPrice": "1688.00",
"confirmCrossResident": false,
"reason": "订单调整后更换车辆和司机",
"requestId": "fleet-change-4938-20260719-001"
}
```
`newVehicleId`、`newDriverId` 至少传一个。`605041` 只适用于 `holdMode=0`;`holdMode=1` 仍按排车待司机确认流程处理。
### 4.2 holdMode=0 成功响应
```json
{
"code": 200,
"message": "成功",
"data": {
"assignmentId": "2078001000000000502",
"assignmentSlotId": "2078001000000000701",
"previousAssignmentGroupId": "2078001000000000601",
"newAssignmentGroupId": "2078001000000000602",
"assignmentStatus": "assigned",
"effectiveDate": "2026-07-22",
"affectedDays": 2,
"protocolPrice": "1688.00",
"otherVehicleCount": 1,
"warningCode": "ORDER_HAS_OTHER_VEHICLES",
"warningMessage": "该订单另有1个车辆槽位,当前仅修改本车辆,请核对其它车辆安排",
"otherVehicles": [
{
"assignmentSlotId": "2078001000000000702",
"vehiclePlate": "蒙B-66666",
"driverName": "李师傅",
"startDate": "2026-07-21",
"endDate": "2026-07-23"
}
],
"dailyDifferences": null
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938a200001",
"success": true
}
```
change 成功响应没有 `confirmed` 和 `sideEffects` 字段。只有 `code=200` 时才能用新派车组替换页面中的旧版本;`warningCode=ORDER_HAS_OTHER_VEHICLES` 时继续保留既有强提示。
### 4.3 605041 失败响应
```json
{
"code": 605041,
"message": "订单行程或用车派单已变化,请按逐日差异处理后重试",
"data": {
"assignmentId": null,
"assignmentSlotId": null,
"previousAssignmentGroupId": null,
"newAssignmentGroupId": null,
"assignmentStatus": null,
"effectiveDate": null,
"affectedDays": null,
"protocolPrice": null,
"otherVehicleCount": null,
"warningCode": null,
"warningMessage": null,
"otherVehicles": null,
"dailyDifferences": [
{
"serviceDate": null,
"differenceType": "HEADCOUNT_BASELINE_MISMATCH",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": null,
"capacityGap": null,
"message": "订单当前人数与用车需求冻结人数不一致"
},
{
"serviceDate": "2026-07-22",
"differenceType": "CAPACITY_INSUFFICIENT",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": 5,
"capacityGap": 3,
"message": "车辆载客量不足,已按每车司机占一座计算"
}
]
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938a605041",
"success": false
}
```
此时旧派单和原派车组仍是有效业务状态。后端可能新增一条 `CHANGE_FAILED` 操作审计,但不会生成可用的新派车版本。不得用 `dailyDifferences[].assignmentId` 替换页面主键,也不得本地切换车辆、司机或状态。
## 五、最终确认 confirm
### 5.1 请求
```http
POST /admin/fleet/assignments/2078001000000000501/confirm
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"requestId": "fleet-final-confirm-4938-20260719-001"
}
```
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `assignmentId` | Path | String | 是 | 有效派单 ID | 当前派车组中的任一派单 ID |
| `requestId` | Body | String | 是 | 非空,最长 64 | 最终确认幂等标识 |
### 5.2 成功响应
```json
{
"code": 200,
"message": "成功",
"data": {
"confirmed": true,
"assignmentStatus": "assigned",
"stageCode": "assigned",
"stageLabel": "已派车",
"currentStep": 4,
"assignmentGroupId": "2078001000000000601",
"confirmedAt": "2026-07-19 14:30:00",
"itineraryUrl": "https://h5.example.com/#/itinerary/<signed-token>",
"sideEffects": {
"vehicleStatusUpdated": "busy",
"driverStatusUpdated": "busy",
"reconPrepRowsCreated": 0,
"reconPrepMarkedCanceled": null
},
"dailyDifferences": null
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938f200001",
"success": true
}
```
`itineraryUrl` 签发配置不可用时允许为空,不影响确认成功。前端只有在 `code=200 && data.confirmed===true` 时展示最终确认成功,并刷新派单详情、看板列表和汇总。
### 5.3 日期、人数与容量同时存在差异
```json
{
"code": 605041,
"message": "订单行程或用车派单已变化,请按逐日差异处理后重试",
"data": {
"confirmed": false,
"assignmentStatus": null,
"stageCode": null,
"stageLabel": null,
"currentStep": null,
"assignmentGroupId": null,
"confirmedAt": null,
"itineraryUrl": null,
"sideEffects": null,
"dailyDifferences": [
{
"serviceDate": "2026-07-21",
"differenceType": "ORDER_DATE_MISMATCH",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": null,
"passengerCapacity": null,
"capacityGap": null,
"message": "订单当前日期与用车需求冻结日期不一致"
},
{
"serviceDate": null,
"differenceType": "HEADCOUNT_BASELINE_MISMATCH",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": null,
"capacityGap": null,
"message": "订单当前人数与用车需求冻结人数不一致"
},
{
"serviceDate": "2026-07-22",
"differenceType": "CAPACITY_INSUFFICIENT",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": 5,
"capacityGap": 3,
"message": "车辆载客量不足,已按每车司机占一座计算"
}
]
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938f605041",
"success": false
}
```
### 5.4 缺少某日车辆槽位
```json
{
"code": 605041,
"message": "订单行程或用车派单已变化,请按逐日差异处理后重试",
"data": {
"confirmed": false,
"assignmentStatus": null,
"stageCode": null,
"stageLabel": null,
"currentStep": null,
"assignmentGroupId": null,
"confirmedAt": null,
"itineraryUrl": null,
"sideEffects": null,
"dailyDifferences": [
{
"serviceDate": "2026-07-23",
"differenceType": "ASSIGNMENT_DATE_MISSING",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": null,
"passengerCapacity": null,
"capacityGap": null,
"message": "该服务日缺少第2个车辆槽位派单"
}
]
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938f605042",
"success": false
}
```
## 六、前端统一处理顺序
1. 先判断业务 `code`,再读取端点自己的 `data`:
- `code=200`:按对应成功模型处理;
- `code=605041`:按对应失败模型读取 `dailyDifferences`;
- 其他业务码:继续走既有错误处理。
2. `605041` 时不要乐观更新:
- create 不新增本地派单;
- change 不替换旧派单;
- confirm 不改为 `assigned` 或“已确认”。
3. 展示顶层 `message`,并按 `dailyDifferences` 列出服务日、差异类型、人数、容量和缺口;字段为空时隐藏对应展示项。
4. 重新请求最新订单和车务详情,避免继续使用操作前缓存。
5. 用户修正订单日期、行程、人数或派单后,生成新的 `requestId` 再提交新动作;仅在同一次动作网络结果不确定时复用原 `requestId`。
## 七、其他直接错误分支
| 业务码 | 常见场景 | 前端处理 |
|---|---|---|
| `605009` | 派单不存在 | 刷新详情和看板,停止操作旧记录 |
| `605020` | 当前状态不允许操作 | 刷新最新生命周期与操作能力 |
| `605025` | 最终确认前缺少司机确认或有效凭证 | 引导先完成司机确认与凭证登记 |
| `605001` / `605003` | 车辆或司机档期冲突 | 展示后端错误并重新选车/司机 |
| `605036` | 跨常驻车未显式确认 | 二次提示后携带 `confirmCrossResident=true` 重试 |
| `605041` | 订单、需求、行程、逐日派单、人数或容量基线不一致 | 使用当前端点的 `data.dailyDifferences` 展示并处理 |
请求字段为空、格式不正确或超过长度限制时走统一参数校验错误,前端应在发请求前完成同样约束。
## 八、不影响范围
- 不新增管理后台接口,三个接口的请求字段结构保持不变。
- `holdMode=1` 的排车锁定、司机确认与凭证登记流程保持不变。
- 不改变取消、司机拒接、驳回需求、撤销取消和提前完结的前端调用契约。
- 不要求前端计算订单人数、逐日服务日期或车辆载客量,这些均由后端权威校验并返回差异。
- 本文件只描述管理后台直接消费的 HTTP 契约,不包含服务间调用或发布实现细节。
## 九、验收状态与待补证据
已完成:
- 当前 worktree 中 create、change、confirm Controller 的 `605041` 强类型 `data` 静态核对。
- `AssignmentWriteRespVO`、`ChangeAssignmentRespVO`、`ConfirmRespVO` 与 `dailyDifferences` 字段静态核对。
- 三条失败路径不提交派单及其关联业务状态变化的源码顺序核对。
- 后端 PR #5065 已合并至 `dev-v3`;Fleet 双实例 `8087/8187` 已完成滚动部署并通过健康/Nacos 验证。
- 最终源码指纹下 Fleet `verify` 1909/1909、Order-v3 受影响回归 438/438、User Quartz 桥接 5/5 均通过。
前端联调仍需补充:
- 三个接口在测试环境 OpenAPI 中的请求/响应模型截图或导出差异。
- 经网关分别取得 create、change、confirm 的成功响应和 `605041` 真实响应,记录 HTTP 状态、业务码、`traceId` 与完整 `data`。
- 对 `605041` 前后做测试业务数据对照,确认派单版本/状态、车辆司机占用、保险、对账和订单派定结果未发生变化。
- 管理后台页面联调证据:差异列表展示、空字段处理、刷新行为、禁止乐观更新,以及修正后重新提交成功。
@@ -0,0 +1,83 @@
# 【修改接口·管理后台】订单工作台统计口径与金额字符串收口(#5062)
> Issue: [wx/HL#5062](https://git.1814.love:8443/wx/HL/issues/5062)
>
> 服务: `hl-user-service`、`hl-order-service-v3`
>
> 日期: 2026-07-18
>
> 影响入口: `GET /admin/profile/dashboard?period={today|week|month}`
## 一、前端结论
1. 路径、请求参数和角色分流不变,不需要新增接口调用。
2. GMV/收入改按真实收款时间归属:线上只统计成功支付,线下只统计未撤销收款;不再按订单创建时间归属订单累计实付。
3. 退款改按真实成功退款时间归属;财务近 30 天趋势返回真实每日收入和退款。
4. 所有金额字段固定按 JSON String 处理;比例 `gmvDiffRate` 仍为 JSON Number。
5. 雪花 ID(例如排行 `adminId`、即将出行 `orderId`)固定按 JSON String 处理,禁止转换为 JavaScript `Number`。
6. 排行订单数为期间发生有效收款的订单去重数,同一订单多笔收款只计一单、金额全部累加。
7. 权威统计源不可用时接口失败关闭,不会用部分成功数据或全零数据伪装成功。
## 二、受影响字段
### 2.1 ADMIN / CUSTOMIZER 工作台
| 字段 | JSON 类型 | 说明 |
|---|---|---|
| `overview.gmv` | String | 当前 period 内真实收款金额 |
| `overview.gmvDiffRate` | Number | 与上一等长期间相比的变化比例 |
| `trend[].gmv` | String | 对应日期的真实收款金额 |
| `ranking[].adminId` | String | 定制师雪花 ID |
| `ranking[].gmv` | String | 对应定制师期间真实收款金额 |
| `ranking[].orderCount` | Number | 发生有效收款的去重订单数 |
| `ranking[].avatar` | String/null | 定制师头像;用户信息降级时允许为空 |
| `upcomingTrips[].orderId` | String | 订单雪花 ID |
### 2.2 FINANCE 工作台
| 字段 | JSON 类型 | 说明 |
|---|---|---|
| `periodIncome` | String | 当前 period 内真实收入 |
| `periodRefund` | String | 当前 period 内成功退款 |
| `monthIncome` | String | 自然月真实收入 |
| `monthRefund` | String | 自然月成功退款 |
| `financeTrend[].date` | String | 日期,`yyyy-MM-dd` |
| `financeTrend[].income` | String | 当日真实收入 |
| `financeTrend[].refund` | String | 当日成功退款 |
## 三、响应片段
```json
{
"code": 200,
"success": true,
"data": {
"role": "FINANCE",
"periodIncome": "128000.00",
"periodRefund": "5600.00",
"monthIncome": "328000.00",
"monthRefund": "8600.00",
"financeTrend": [
{
"date": "2026-07-18",
"income": "12000.00",
"refund": "600.00"
}
]
}
}
```
## 四、前端检查清单
- [ ] 金额展示使用字符串格式化,不执行 `Number(amount)`。
- [ ] `gmvDiffRate` 继续按 Number 计算百分比。
- [ ] 所有 Long ID 保持字符串透传到路由和请求参数。
- [ ] 不再用订单创建日解释趋势 GMV;趋势日期是支付/收款发生日。
- [ ] 财务趋势同时渲染 `income` 与 `refund`,空日后端返回 `"0.00"`。
- [ ] 接口业务失败时展示重试,不把缺失统计源当作全零成功。
## 五、后端验证
- Dashboard、User 聚合、支付/退款 Feign、序列化与失败关闭相关测试已通过。
- Issue #5062 最终五模块全量测试:14,518 个测试,0 失败、0 错误;Fleet Reactor verify:1,824 个测试,0 失败、0 错误、0 跳过。
@@ -0,0 +1,73 @@
# 房务 bug房务:管理后台修复清单
## 来源
桌面文件:`bug房务.docx`。
## 后端当前状态
`dev-v3` 已包含房务需求版本、改期平移、库存原子迁移、增减晚次、人数变化、作废需求保护、返工待办互斥和最终确认动作契约修复。TEST 回归数据由 Codex 生成,5 条王骁订单已进入 `PENDING / PENDING_CLAIM` 抢单池。
## 管理后台必须修复
### 1. 酒店多房型展示
当定制师选择同一酒店的多个房型时,房务卡片必须按 `hotelId + roomTypeId` 展开,禁止把多个房型合并为一个房型后叠加房间数。展示的房型名称、房间数、价格和晚次必须与需求明细逐项对应。
### 2. 待办标签颜色
按后端 `todoType` 使用统一颜色:待配房、需求变更重配、待最终确认、酒店超时、异常/取消必须视觉可区分;不能只显示文字而丢失优先级。
### 3. 指定酒店但不指定房型
酒店已指定、房型为空时,仍应允许进入房务流程;候选列表限定指定酒店,房型由房务选择。不能把“房型为空”误判为需求无效。
### 4. 最终确认后修改
已最终确认订单进入详情后,仍需显示“修改/替换酒店、调整房型、修改房间数、清空配房”入口。操作前调用重新询房/重开接口,成功后刷新详情;不能在前端用 `m.finalized` 直接隐藏或禁用所有修改入口。
重点文件:`src/views/housekeeper/components/OrderDetailModal.vue` 中 `canClearAssignments`、修改入口和 `ensureRequirementEditable` 的状态判断必须统一。
### 5. 作废需求展示
作废需求必须展示:
- 作废状态和红色视觉标记
- 准确易懂的作废原因,例如“定制师修改住宿需求,原房务需求已作废”
- 仅保留“查看”操作
- 隐藏领取、配房、替换、清空、确认、最终确认等所有写操作
### 6. 改出发日期
改期后页面必须展示新日期;当前有效晚次的配房日期由后端按 `dayNumber` 对齐。原日期库存先恢复,新日期库存全部预占成功后才提交;失败时订单、配房和库存保持原状。页面必须展示“已改期,配房需按新日期重新确认”的说明。
### 7. 增加出行人数
订单调整摘要和房务详情必须显示“增加 X 人”,同时展示调整前人数、调整后人数和新增出行人;既有酒店、房型、房间数和库存字段不得丢失。
### 8. 增加行程天数
必须展示“新增第 N 晚住宿,新增日期待配房”;原日期配房保留,新增晚次为空白候选,未完成新增晚次时禁止最终确认。
## 验收订单
使用以下 5 条 TEST 订单逐项验证:
- `HL20260719092421973`
- `HL20260719092425403`
- `HL20260719092428569`
- `HL20260719092431853`
- `HL20260719092435001`
每条订单均为定制师“王骁”,已模拟支付、补全出行人并提交住宿需求。
## 验收要求
必须同时提供:
1. 房务首页截图
2. 待办列表截图
3. 订单详情截图
4. 改期/增人/增晚前后对比截图
5. 浏览器 Network 请求确认只提交 `dayNumber`,不由前端提交 `stayDate`
6. 最终确认后订单从待办和工作台消失
@@ -0,0 +1,244 @@
# 【修改接口·管理后台】核团核算状态改用 `review_status`(#5066)
> **PR**: [#5067](https://git.1814.love:8443/wx/HL/pulls/5067) | **服务**: `hl-order-service-v3` | **更新时间**: 2026-07-19 10:22
## 1. 关键变化
> ⚠️ 两个接口的字段名 `settlementStatus` / `settlementStatusName` 均保持不变,但字段的数据来源、可选枚举和业务语义已经变化。前端不得继续复用财务结算状态字典。
- 核团核算状态的数据来源由 `order_main.settlement_status` 改为 `order_main.review_status`。
- 页面状态统一为:
- `PENDING`:待核算
- `IN_PROGRESS`:核算中
- `COMPLETED`:已完成
- 列表查询参数名仍为 `settlementStatus`,但合法值改为 `PENDING / IN_PROGRESS / COMPLETED`。
- 旧值 `NONE` 不再是合法查询参数;历史 `review_status = NONE / NULL` 的订单统一投影为 `PENDING / 待核算`。
- 本次仅调整核团页面的查询和返回投影,不修改财务复核及结算完成所使用的 `settlement_status`。
## 2. 变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 核团核算任务列表 | GET | `/v3/admin/order-settlement/tasks` | 修改接口 | 筛选和返回状态改用 `review_status`;参数名 `settlementStatus` 保持不变 |
| 2 | 查询核团详情 | GET | `/v3/admin/order/{orderId}/settlement/return-detail` | 修改接口 | `orderInfo` 中的核算状态改用 `review_status` 投影 |
## 3. 接口详情
### 3.1 核团核算任务列表
`GET /v3/admin/order-settlement/tasks`
- **认证**:需要管理后台 JWT。
- **幂等性**:是,只读查询。
- **请求体**:无。
- **响应结构**:`Result<PageResult<SettlementTaskRespVO>>`。
- 分页、关键词、出发日期筛选和列表范围均保持不变。
#### Query 入参
| 字段 | 类型 | 必填 | 合法值 | 说明 |
|---|---|---|---|---|
| `settlementStatus` | string | 否 | `PENDING` / `IN_PROGRESS` / `COMPLETED` | 核团页面核算状态;字段名保留,实际筛选 `order_main.review_status` |
其他 Query 参数保持不变:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | number | 否 | 当前页码,默认 `1` |
| `pageSize` | number | 否 | 每页条数,默认 `20`,范围 `1` 到 `100` |
| `keyword` | string | 否 | 按订单号、团号、产品名模糊查询 |
| `departureDateFrom` | string | 否 | 出发日期开始,格式 `yyyy-MM-dd` |
| `departureDateTo` | string | 否 | 出发日期结束,格式 `yyyy-MM-dd` |
#### 受影响的响应字段
| 字段 | JSON 类型 | 修改后说明 |
|---|---|---|
| `data.records[].settlementStatus` | string | 核团核算状态:`PENDING / IN_PROGRESS / COMPLETED` |
| `data.records[].settlementStatusName` | string | 页面文案:`待核算 / 核算中 / 已完成` |
列表中的其他字段、分页结构和排序规则均保持不变。
#### 请求与响应示例
**请求**
```http
GET /v3/admin/order-settlement/tasks?page=1&pageSize=10&settlementStatus=IN_PROGRESS
Authorization: Bearer <JWT>
```
**响应片段**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"orderId": "2077233886248534018",
"orderNo": "HL202607180001",
"teamNo": "T20260718001",
"productName": "呼伦贝尔草原 5 日游",
"departureDate": "2026-07-20",
"returnDate": "2026-07-24",
"peopleCount": 3,
"peopleSummary": "2成人1婴儿",
"systemBalanceAmount": 0.00,
"settlementStatus": "IN_PROGRESS",
"settlementStatusName": "核算中"
}
],
"total": 1,
"page": 1,
"pageSize": 10
},
"traceId": null,
"success": true
}
```
### 3.2 查询核团详情
`GET /v3/admin/order/{orderId}/settlement/return-detail`
- **认证**:需要管理后台 JWT。
- **幂等性**:是,只读查询。
- **请求体**:无。
- **路径参数和响应整体结构保持不变。**
#### 受影响的响应字段
| 字段 | JSON 类型 | 修改后说明 |
|---|---|---|
| `data.orderInfo.settlementStatus` | string | 核团核算状态:`PENDING / IN_PROGRESS / COMPLETED`,来源为 `review_status` |
| `data.orderInfo.settlementStatusName` | string | 页面文案:`待核算 / 核算中 / 已完成` |
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"orderInfo": {
"orderId": "2077233886248534018",
"orderNo": "HL202607180001",
"settlementStatus": "COMPLETED",
"settlementStatusName": "已完成"
},
"travelers": [],
"driverVehicles": [],
"receivableItems": [],
"collectionRecords": []
},
"traceId": null,
"success": true
}
```
> 示例仅展示本次相关字段;详情接口原有的订单信息、出行人、司机车辆、应收和收款字段均保持不变。
## 4. 枚举与状态映射
| `settlementStatus` | `settlementStatusName` | 核团页面语义 |
|---|---|---|
| `PENDING` | 待核算 | 尚未开始核算 |
| `IN_PROGRESS` | 核算中 | 已开始录入或处理核算数据 |
| `COMPLETED` | 已完成 | 核单已经提交完成 |
### 历史数据兼容
| `order_main.review_status` 实际值 | 接口返回 `settlementStatus` | 接口返回 `settlementStatusName` |
|---|---|---|
| `NULL`、空值或 `NONE` | `PENDING` | 待核算 |
| `PENDING` | `PENDING` | 待核算 |
| `IN_PROGRESS` | `IN_PROGRESS` | 核算中 |
| `COMPLETED` | `COMPLETED` | 已完成 |
### 列表筛选规则
| Query 参数 | 后端筛选行为 |
|---|---|
| 不传 `settlementStatus` | 不追加核算状态过滤,返回符合其他条件的任务 |
| `PENDING` | 匹配 `review_status = PENDING / NONE / NULL`,兼容历史订单 |
| `IN_PROGRESS` | 精确匹配 `review_status = IN_PROGRESS` |
| `COMPLETED` | 精确匹配 `review_status = COMPLETED` |
| `NONE` 或其他值 | 参数校验失败,HTTP 200、业务码 `400` |
## 5. 修改前后对比
| 项目 | 修改前 | 修改后 |
|---|---|---|
| 接口字段名 | `settlementStatus` / `settlementStatusName` | 保持不变 |
| 状态数据源 | 财务结算态 `settlement_status` | 核团核算流程态 `review_status` |
| 查询参数枚举 | `NONE / PENDING / COMPLETED` | `PENDING / IN_PROGRESS / COMPLETED` |
| `PENDING` 文案/语义 | 待财务复核 | 待核算 |
| `COMPLETED` 文案/语义 | 已结算 | 已完成 |
| 处理中状态 | 无独立值 | 新增 `IN_PROGRESS / 核算中` |
| 历史 `NONE / NULL` 返回值 | `NONE / 未结算` | 归一为 `PENDING / 待核算` |
## 6. 前端适配清单
- [ ] 核团状态下拉改为 `PENDING / IN_PROGRESS / COMPLETED`。
- [ ] 下拉文案依次使用“待核算 / 核算中 / 已完成”。
- [ ] 删除核团页面向接口传递 `NONE` 的逻辑。
- [ ] 不修改 Query 参数名,继续传 `settlementStatus`。
- [ ] 不修改响应字段名,继续读取 `settlementStatus` 和 `settlementStatusName`。
- [ ] 不再复用财务结算状态字典解释这两个核团接口。
- [ ] 若前端自行维护状态文案,必须同步更新;优先使用后端返回的 `settlementStatusName`。
- [ ] 对历史未开始核算的订单统一按 `PENDING / 待核算` 展示。
## 7. 错误与边界行为
| 场景 | 行为 |
|---|---|
| 未传 `settlementStatus` | 正常查询,不按核算状态过滤 |
| 传 `settlementStatus=NONE` | 参数校验失败,HTTP 200、业务码 `400` |
| 传其他非法状态 | 参数校验失败,HTTP 200、业务码 `400` |
| 历史 `review_status=NONE/NULL` | 列表和详情均返回 `PENDING / 待核算` |
| 房务角色访问 | 保持原权限规则,不因本次变更放开 |
| 订单不存在 | 详情接口保持原订单不存在错误 |
| 空列表 | 返回成功响应,`records=[]` |
## 8. 不影响范围
- 财务复核和财务结算完成仍使用 `order_main.settlement_status`。
- 核单提交后写入 `settlement_status=PENDING`、财务确认后写入 `settlement_status=COMPLETED` 的流程不变。
- 两个接口的 URL、HTTP 方法、认证方式、分页结构和其他字段均不变。
- 不涉及数据库表结构或数据迁移。
- 不影响核团详情中的出行人、司机车辆、应收明细和收款明细契约。
- 不影响其他财务页面对 `settlementStatus` 的既有使用;本次语义仅适用于本文列出的两个核团接口。
## 9. 影响评估与回滚
- **字段结构是否破坏兼容**:否,字段名和 JSON 类型不变。
- **业务语义是否变化**:是,同名字段的数据来源、枚举和中文含义均发生变化。
- **前端是否需要同步适配**:是,核团状态下拉和本地状态字典必须同步。
- **是否影响已有数据**:不改写已有数据;读取时兼容历史 `NONE / NULL`。
- 回滚 PR #5067 后,两个接口会重新使用旧的财务结算状态语义。
- 因 `PENDING`、`COMPLETED` 是同名但不同含义的值,前后端版本回滚必须同步,不能仅根据字段是否存在判断版本。
- 无数据库迁移,无需清理或恢复数据。
## 10. 后端验证与发布状态
- PR #5067 原定向测试:48 tests,0 failures,0 errors。
- 与审计分支融合后的核团状态/快照/Feign 定向测试:121 tests,0 failures,0 errors。
- 融合后的 `hl-order-service-v3` 全量测试:5,797 tests,0 failures,0 errors,15 条件跳过。
- PR #5067 已于 2026-07-19 10:22 合并到 `dev-v3`。
- 本文未取得测试环境部署或网关真实接口调用证据;合并完成不等同于测试环境已经生效。
## 11. 历史契约说明
| 文档/PR | 说明 | 当前有效性 |
|---|---|---|
| Changelog `18_5055_核团核算列表详情-修改接口-管理后台.md` / PR #5058 | 首次交付核团列表与详情聚合接口 | 接口结构及非状态字段仍有效 |
| 上述文档中的 `settlementStatus` 枚举和示例 | 使用 `NONE / PENDING / COMPLETED` 及“未结算 / 待财务复核 / 已结算” | 已被本文纠正,不再作为核团页面契约 |
| PR #5067 / Issue #5066 | 核团核算状态改用 `review_status` | 当前最新契约 |
## 12. 关联链接
- **Issue**: [#5066](https://git.1814.love:8443/wx/HL/issues/5066)
- **PR**: [#5067](https://git.1814.love:8443/wx/HL/pulls/5067)
- **Merge commit**: [f60241f3d](https://git.1814.love:8443/wx/HL/commit/f60241f3d6fdb2f36c091dc93d0232f2dcfe4775)
@@ -0,0 +1,30 @@
# 房务契约补充:作废与订单调整历史字段
前端反馈“作废、人数、改期、增晚缺少明确出参”。现按当前后端实现明确如下,禁止按页面文案猜测:
## 房务详情/需求历史
接口:`GET /admin/house/orders/{orderId}`、`GET /admin/house/orders/{orderId}/requirement-history`
- `requirement.recentHistory[].status`:`PENDING`、`PROCESSING`、`DONE`、`REJECTED_TO_CONSULTANT`、`REJECTED_TO_ADMIN`、`SUPERSEDED`。
- `requirement.recentHistory[].returnReason`:退回/驳回原因;未退回为 `null`。
- `requirement.recentHistory[].returnedBy`、`returnedAt`:退回操作人和时间;未退回为 `null`。
- 作废订单本身使用 `order.status` 及 `statusLabel`;房务流程使用 `progress.houseStatus` 及 `houseStatusLabel`,不能把二者混用。
## 人数、改期、增晚的调整记录
接口:`GET /v3/admin/order/{orderId}/adjustment-record`
`items[].type` 是稳定枚举,`label/before/after` 已由后端生成,前端直接展示:
- `HEADCOUNT`:人数变化;`before/after` 为调整前后人数摘要。
- `DEPART_DATE`:改期;`before/after` 为旧/新出发日期。
- `TRIP_DAYS`:增减行程天数;`before/after` 为旧/新“X天Y晚”摘要。
- `TRAVELER_EDIT`:仅出行人资料字段编辑,不代表人数变化。
- `HOTEL_REQ`:住宿需求调整。
调整记录同时返回 `occurredAt`、`changeCount`、`statusNote`、`balanceBefore`、`balanceAfter`。新增出行人明细不从房务详情猜测,使用既有订单出行人接口;`HEADCOUNT` 记录用于展示人数前后变化。
## 验收说明
当前后端测试环境已有登录态,后续验收使用仓库 CDP 调试浏览器和 Network 证据;不得因普通浏览器无登录态改用插件或猜测实现。
@@ -0,0 +1,86 @@
# 【修改接口·管理后台】核团详情出行人补充出生日期和年龄(#5068)
> **Issue**: [#5068](https://git.1814.love:8443/wx/HL/issues/5068) | **PR**: [#5069](https://git.1814.love:8443/wx/HL/pulls/5069) | **服务**: `hl-order-service-v3` | **更新时间**: 2026-07-19 11:23
## 1. 关键变化
- 核团详情 `travelers[]` 新增可空字段 `birthday` 和 `age`。
- `birthday` 为出行人出生日期,格式 `yyyy-MM-dd`。
- `age` 为按订单出发日期计算的周岁。
- 出生日期为空、订单出发日期为空,或出生日期晚于出发日期时,`age` 返回 `null`。
- 出行人手机号和证件号继续沿用原有脱敏规则。
## 2. 受影响接口
`GET /v3/admin/order/{orderId}/settlement/return-detail`
- HTTP 方法、URL、认证、路径参数及响应整体结构均不变。
- 本次只增加 `data.travelers[]` 的响应字段,不增加请求参数。
## 3. 新增响应字段
| 字段 | JSON 类型 | 是否可空 | 说明 |
|---|---|---|---|
| `data.travelers[].birthday` | string | 是 | 出生日期,格式 `yyyy-MM-dd` |
| `data.travelers[].age` | number | 是 | 以订单出发日期为基准计算的周岁 |
### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"travelers": [
{
"travelerId": "71001",
"travelerName": "张三",
"travelerType": "ADULT",
"travelerTypeName": "成人",
"birthday": "1990-07-20",
"age": 36,
"idType": "ID_CARD",
"idTypeName": "身份证",
"phone": "138****1234",
"idCardNo": "110***********1234"
}
]
},
"success": true
}
```
> 示例仅展示本次相关结构;核团详情中的订单、司机车辆、应收和收款等字段保持不变。
## 4. 年龄计算与空值边界
| 场景 | `birthday` | `age` |
|---|---|---|
| 出生日期和订单出发日期均有效 | 返回出生日期 | 返回两个日期之间的完整周岁 |
| 出生日期为空 | `null` | `null` |
| 订单出发日期为空 | 返回出生日期 | `null` |
| 出生日期晚于订单出发日期 | 返回出生日期 | `null` |
当前已合并实现不会在订单出发日期缺失时改用服务器当前日期。Issue #5068 初始描述中的“按当前日期兜底”尚未进入代码;若业务仍需要该口径,应另行变更后端实现和本通知。
## 5. 前端适配清单
- [ ] 在核团详情出行人列表展示 `birthday` 和 `age`。
- [ ] 对两个字段均做 `null` 兼容,不拼接 `null岁` 或展示无效日期。
- [ ] 年龄直接使用后端返回值,不在浏览器端按当前日期重新计算。
- [ ] 继续使用现有脱敏后的 `phone` 和 `idCardNo`,不要尝试恢复明文。
- [ ] 不改变接口 URL、请求参数和其他响应字段的解析逻辑。
## 6. 兼容性与发布边界
- 新增字段对忽略未知 JSON 字段的旧客户端向后兼容。
- 字段为可空值,前端不能把 `birthday` 或 `age` 设为必填。
- PR #5069 已于 2026-07-19 10:47 合并到 `dev-v3`,合并提交为 `1da9389fbbc567dfd8b98703a6b9ebbfb2ea1d69`。
- 测试环境公网网关已验证新字段返回,见下方验证证据。
## 7. 验证证据
- 后端定向测试:`mvn -pl hl-order-service-v3 -am -DfailIfNoTests=false -Dtest=SettlementReturnDetailQueryServiceTest,SettlementControllerTest test`。
- 测试环境公网网关验证:`GET https://web.test.1814.love:9443/v3/admin/order/2077233855281971202/settlement/return-detail` 连续 6 次返回 `code=200`。
- 实测订单出发日为 `2026-07-13`,首位出行人 `birthday=1991-05-27`,接口返回 `age=35`,与按订单出发日计算的周岁一致。
- 实测响应中 `phone`、`idCardNo` 仍为脱敏值。
@@ -0,0 +1,19 @@
# #5074 调整记录新增本次新增出行人 ID
接口:`GET /v3/admin/order/{orderId}/adjustment-record`
每条调整记录新增:
```json
{
"addedTravelerIds": ["2078739881671921666", "2078739895240560641"]
}
```
- 类型:`string[]`,雪花 ID 必须按字符串处理。
- 含义:仅包含该次订单调整事务实际新增的出行人 ID。
- 多人同时新增:完整返回全部新增 ID;数组顺序不承载业务语义。
- 仅编辑、仅删除、未涉及出行人或旧历史记录:返回 `[]`。
- 前端将当前出行人列表中的 `id` 与 `addedTravelerIds` 精确匹配后标记“本次新增”;禁止按列表位置或 ID 大小推断。
后端 Issue:`wx/HL#5074`;PR:`wx/HL#5075`。
@@ -0,0 +1,13 @@
# #5078 住宿需求允许指定酒店暂不指定房型
影响接口:住宿需求首次提交及订单调整提交。
业务规则调整:
- 允许候选酒店 `hotelId` 有值,同时房型行 `roomTypeId=null`、`roomCategory=null`。
- 此时 `roomCount` 仍必须为正数,用于表达“酒店已指定,具体房型由房务后续确认”。
- 后端不会伪造房型 ID 或协议价;房务配房时再选择该酒店的真实房型。
- 指定真实房型时继续按原契约传 `roomTypeId`;完整房型、多房型、未指定酒店场景不变。
- `roomCount` 为 0、负数或缺失时仍按非法房数拒绝。
后端 Issue:`wx/HL#5078`;PR:`wx/HL#5079`。
@@ -0,0 +1,160 @@
# 【前端待处理·管理后台】管理后台消息 SSE 鉴权重连与旧会话恢复
> **模块**:管理后台全局消息 / 在线状态 / 聊天信令 | **服务**:`hl-gateway` + `hl-user-service`<br>
> **类型**:前端待处理 + 联调告知 | **更新时间**:2026-07-19<br>
> **影响范围**:管理后台全局 SSE 连接、顶部未读角标、聊天、在线状态与抢单池信令<br>
> **状态**:后端已完成根因定位;前端尚未修复;接口契约未变
## 1. 结论与处理优先级
> ⚠️ 2026-07-19 测试环境启用 SSE 连接角色一致性校验后,发布前已签发且仍在有效期内的旧登录会话可能缺少当前角色标记。此时网关能够识别 access token,但用户服务会拒绝建立 SSE,前端当前实现会持续使用同一登录会话无限重连。
- 接口 URL、HTTP 方法、事件结构均未修改。
- 这不是 `token` Query 参数名写错;当前前端 URL 拼接方式与网关读取方式一致。
- **用户立即恢复方式**:退出当前账号,重新登录并选择当前角色,再建立 SSE。
- **前端必须处理**:Token/角色变化时主动重建连接、限制连续失败重试、给出重新登录提示,并消除默认 `message` 事件的重复注册。
- 本次现象包含后端发布前旧会话兼容问题;前端改造用于正确管理连接生命周期和避免无限重试,不代表把后端兼容责任转移给前端。
## 2. 当前接口契约
```http
GET /ws/admin-msg/stream?token=<accessToken>
Accept: text/event-stream
```
- 认证:管理后台 access token。
- 当前使用原生 `EventSource`,浏览器 API 不能自定义 `Authorization` Header,因此现有实现通过 Query 参数传递 token。
- `token` 必须使用当前 Store 中的 access token,并通过 `encodeURIComponent` 做 URL 编码。
- 成功建连后,请求应长期保持 `Pending`,响应类型为 `text/event-stream`。
- 首个握手事件:
```text
event: connected
data: ok
```
- 后续仍沿用现有具名事件,包括 `unread-count`、`im-chat`、`im-chat-read`、`presence` 和 `grab-pool-changed`;本次没有修改事件数据结构。
## 3. 已确认的问题链路
### 3.1 旧登录会话与新角色标记不兼容
测试环境运行链路已确认:
1. 网关可以从 `?token=` 读取并校验管理后台 JWT。
2. 网关向用户服务转发可信的管理员身份及角色信息。
3. 用户服务在下发任何 SSE 数据前校验“连接角色是否仍为当前登录角色”。
4. 发布前签发的旧登录会话没有初始化新角色标记时,校验按安全策略失败并关闭连接。
5. 新登录或重新选择角色会重新写入角色标记,因此重新登录后可恢复。
该校验采用 fail-closed(失败时拒绝)策略,目的是避免角色切换后旧 Token 继续接收不属于当前角色的消息。
### 3.2 前端当前会无限重试同一失败会话
当前 `src/composables/useAdminMessageSSE.js` 在 `EventSource.onerror` 后执行关闭和指数退避,但没有连续失败上限,也没有触发重新登录或鉴权恢复流程。
原生 `EventSource.onerror` 不暴露 HTTP 状态码和响应正文,前端不能仅凭 `onerror` 精确区分 401/403、服务异常和临时断网。因此不能把所有错误都直接判定为 Token 失效,但必须限制无休止重连。
### 3.3 默认 `message` 事件被重复注册
当前实现同时注册:
```js
es.onmessage = handleMessage
es.addEventListener('message', handleMessage)
```
两种写法都会监听默认 `message` 事件,并不是互斥兜底。后端发送默认 `message` 时,同一数据可能被处理两次,必须只保留一种注册方式。
## 4. 用户立即恢复步骤
1. 关闭当前页面产生的旧 SSE 连接。
2. 正常退出管理后台。
3. 重新登录,并重新选择当前需要使用的角色。
4. 进入主布局后重新建立 `/ws/admin-msg/stream`。
5. 在浏览器 Network 中确认请求保持 `Pending`,并收到一次 `connected` 事件。
不要通过手工复制、修改或在地址栏粘贴完整 Token 的方式恢复连接。
## 5. 【前端·管理后台】适配清单
### 5.1 让 SSE 生命周期跟随登录凭证和角色
- [ ] 监听 `userStore.token` 变化;值变化时先关闭旧 `EventSource`,再使用最新 Token 建立唯一的新连接。
- [ ] 角色切换成功并更新 Token 后,立即重建 SSE,不等待旧连接自行报错。
- [ ] 登出、主布局卸载或 Token 被清空时,关闭连接、清理重连定时器并禁止再次拉起。
- [ ] 保证全局最多只有一个管理后台消息 SSE 实例,避免布局重复挂载造成多连接。
- [ ] 重建连接时始终从 Store 现取 Token,不缓存旧登录会话中的 Token 字符串。
### 5.2 限制连续失败,避免无限重连
- [ ] 保留指数退避和最大间隔,但增加“连续失败次数/总时长”上限。
- [ ] **仅在收到后端 `connected` 事件后**清零连续失败计数;`EventSource.onopen` 不能作为鉴权成功依据,也不能清零计数。
- [ ] 达到上限后停止自动重试,并显示中性、可操作的提示,例如“消息连接连续失败,请检查网络或重新登录”。
- [ ] 用户完成重新登录、Token 刷新、角色切换或主动点击重试后,才开启新一轮连接。
- [ ] 临时断网恢复后仍允许重连;可结合 `online` 事件或显式重试入口恢复,而不是永久静默失效。
> 注意:由于原生 `EventSource` 无法在 `onerror` 中读取响应状态,前端不要根据一次 `onerror` 立即清空登录态。需要使用连续失败阈值,并结合普通鉴权接口结果或既有 Token 刷新状态判断。
### 5.3 消除重复消息处理
- [ ] `es.onmessage` 与 `es.addEventListener('message', ...)` 只保留一种。
- [ ] `connected`、`unread-count`、`im-chat`、`im-chat-read`、`presence`、`grab-pool-changed` 等具名事件继续分别注册。
- [ ] 验证单条默认 `message`、聊天信令和未读数信令都只被业务层消费一次。
### 5.4 失败信息与联调反馈
- [ ] 前端提示中不要展示 Token、完整 SSE URL、Cookie 或管理员标识。
- [ ] 如重新登录后仍失败,只反馈发生时间、页面、错误 `message` 和 `X-Trace-Id`。
- [ ] 若 Network 原始响应确实为“未提供有效的Token”,请附 `X-Trace-Id` 交后端继续检查路由/拦截器链;不要附 Token。
## 6. 验收场景
| 场景 | 期望结果 |
|---|---|
| 重新登录后首次进入主布局 | 只建立 1 条 SSE;请求保持 `Pending`;收到 1 次 `connected` |
| access token 刷新 | 旧连接关闭,使用新 Token 只重建 1 次 |
| 切换管理后台角色 | 旧角色连接立即关闭;新角色 Token 建立新连接;不接收旧角色后续数据 |
| 发布前旧会话无法建连 | 退避重试达到阈值后停止,并明确提示重新登录;不无限刷请求 |
| 只触发 `onopen`、未收到 `connected`、随后触发 `onerror` | 仍累计连续失败次数,不得被 `onopen` 反复清零 |
| 临时断网后恢复 | 在受控退避或用户重试后恢复连接,不产生并发 SSE |
| 收到默认 `message` | 同一事件只处理 1 次 |
| 正常登出 | SSE 和重连定时器均被清理,退出页不再发起连接 |
| 重新登录后仍失败 | 联调材料仅包含时间、页面、错误消息、`X-Trace-Id`,不包含 Token |
## 7. 后端状态与边界
- 当前接口路径、Query 参数名和 SSE 事件结构未变,不需要前端调整数据模型。
- 新登录/角色切换链路会写入当前角色标记,重新登录是当前可用的恢复手段。
- 发布前旧会话没有迁移标记是本次问题的触发条件;后端尚未交付旧会话兼容补丁。
- 角色一致性校验必须保留,不能为了兼容旧会话而允许旧角色 Token 接收消息。
- 若后续改为一次性 SSE Ticket、Fetch Streaming 或其他不在 URL 中携带 access token 的方案,将另发接口契约,不在本次前端适配范围内。
## 8. 安全要求
- 禁止把完整 Token、带 Token 的完整 SSE URL、Cookie 或真实管理员信息写入 Issue、PR、Changelog、日志和截图。
- Token 一旦通过聊天、工单或截图暴露,应立即停止使用和传播,通知后端/运维按当前鉴权策略显式吊销或拒绝该旧 Token,并验证它已无法访问;随后重新登录获取新 Token。
- 重新登录只是恢复 SSE 和获取新 Token,不等于旧 JWT 已自动吊销;尤其在仅校验 JWT 签名的环境中,必须单独完成旧 Token 的失效处置。
- 不得在前端代码中硬编码 Token,也不得把 Token 写入错误上报或埋点参数。
## 9. 影响范围
| 文件/能力 | 说明 |
|---|---|
| `src/composables/useAdminMessageSSE.js` | 连接、重连、事件监听和清理逻辑 |
| `src/layouts/BasicLayout.vue` | 主布局挂载、登出和 SSE 生命周期 |
| 角色切换流程 | Token 更新后主动重建 SSE |
| 顶部未读角标、聊天、在线状态、抢单池信令 | 共用同一 SSE,需防止连接缺失或事件重复消费 |
## 10. 发布说明
- 本文是前端联调和修复通知,不代表已修改或发布前端代码。
- 本文没有包含任何真实 Token、管理员 ID、Cookie 或其他敏感信息。
- 前端完成后应在 `mmg/hl-ui` 走自身 Issue、分支、PR、测试和发布流程。
## 11. 相关历史契约
| 文档 | 当前说明 |
|---|---|
| [内部员工站内信收件箱 + SSE 实时推送](../2026-06/04_3414_内部员工站内信收件箱-SSE实时推送-管理后台.md) | SSE 路径、Query 鉴权和事件契约仍有效;其中“断线自动重连”的建议被本文补充为有上限的受控重连,鉴权持续失败时不得无限请求 |
| [切角色 / 刷新令牌原子保存](../2026-06/38_4529_切角色与刷新token原子保存_前端必改-管理后台.md) | `token` 与 `refreshToken` 原子保存要求仍有效;保存新 access token 后还必须关闭旧 SSE 并主动重建 |
@@ -0,0 +1,49 @@
# 房务详情混合房型逐行出参(Issue #5080)
## 背景
同一晚存在多个房型时,旧兼容标量会把 `rooms[]` 的首行房型与所有房数相加,导致“标间 1 + 大床房 1”被错误展示为“标间 2”。
## 接口
`GET /admin/house/orders/{orderId}`
`GET /v3/admin/order/{orderId}`(订单详情中的住宿需求摘要)
## 新增字段
`data.itinerary[].expectedRooms[]`:当天逐房型预期房间列表,混合房型展示和业务判断以此字段为准。
```json
{
"expectedRoom": {
"roomCategory": null,
"roomCategoryLabel": null,
"roomCount": 2
},
"expectedRooms": [
{ "roomCategory": "STANDARD", "roomCategoryLabel": "标间", "roomCount": 1 },
{ "roomCategory": "KING", "roomCategoryLabel": "大床房", "roomCount": 1 }
]
}
```
## 兼容规则
- 单一房型:`expectedRoom` 继续返回原标量,`expectedRooms[]` 同时提供逐行数据。
- 混合房型:`expectedRoom.roomCategory` 与 `roomCategoryLabel` 返回 `null`,防止形成“首房型 × 总房数”的错误含义;`roomCount` 仍为总间数。
- 未指定房型:房型字段保持 `null`,房数按需求返回。
- `requirement.current.days[].segments[].candidates[].rooms[]` 仍是候选酒店房型行的权威明细。
## 前端适配要求
1. 房务详情及“选择酒店”弹窗不得再用 `days[].roomCategory` 或首个酒店 `roomCategory` 表示混合房型。
2. 标题按 `expectedRooms[]` 渲染,例如“标间 1 间 + 大床房 1 间”。
3. 候选房型筛选与默认数量应逐条读取 `expectedRooms[]`;不得以首行房型套用总间数。
4. 兼容后端尚未部署时,可从 `segments[].candidates[0].rooms[]` 读取同等权威明细,但不得猜测列表顺序。
## 订单详情补充(Issue #5082)
- `hotelRequirement.days[].hotels[]` 与 `segments[]` 的兼容 `roomCategory/roomCategoryLabel` 仅在对应 `rooms[]` 全部属于同一房型大类时返回。
- 混合房型时,上述兼容房型字段返回 `null`,`roomCount` 仍返回总间数;页面标题必须由 `rooms[]` 逐行生成。
- 这可避免“标间 1 + 大床房 1”被标题错误展示为“标间 2”。
@@ -0,0 +1,370 @@
# 【前端对接·管理后台】车务派单可靠通知、取消后重派与发送状态契约
> Issue: [wx/HL#4933](https://git.1814.love:8443/wx/HL/issues/4933)
>
> PR: [wx/HL#5073](https://git.1814.love:8443/wx/HL/pulls/5073)、[wx/HL#5084](https://git.1814.love:8443/wx/HL/pulls/5084)
>
> 服务: `hl-fleet-service` / `hl-user-service` / `hl-order-service-v3` / `hl-gateway`
>
> 日期: 2026-07-19
>
> 影响范围: 派单/改派弹窗、派单详情操作记录、通知发送日志、订单详情推送记录、取消后重新派车
## 一、前端结论
- `holdMode=1` 的创建派单和改派现在会冻结本次通知模板与正文,并由后端异步执行可靠短信发送。
- 创建 HOLD 成功只表示派单和通知意图已落库;首次响应中的 `holdSentAt` 固定为 `null`。只有供应商真实受理后,派单详情的 `currentAssignment.holdSentAt` 才会回显发送时间。
- 通知日志 `status` 已从旧的少量状态扩展为 `0~6`。前端必须展示“投递中、结果不确定、授权撤销”,不得把它们归并成发送成功或失败。
- 取消派单成功后,后端会可靠地把当前生效用车需求重新打开,允许再次派车;该过程为最终一致。前端刷新看板和详情,并以最新 `canAssign`/当前需求状态决定是否开放重派,不调用内部重开接口。
- 订单详情推送记录的归一化状态枚举已调整,前端需要同步新枚举。
- `/internal/**`、`/v3/internal/**` 均为服务间接口,经网关调用返回业务码 `403`;任何 Web/小程序代码都不得调用。
## 二、前端可调用接口
| 接口 | 方法 | 路径 | 本轮变化 |
|---|---|---|---|
| 创建派单 | POST | `/admin/fleet/assignments` | 新增 `messageTemplateId/customBody`;明确 `holdSentAt` 语义 |
| 修改派单 | POST | `/admin/fleet/assignments/{assignmentId}/change` | HOLD 改派新增 `messageTemplateId/customBody` |
| 派单看板详情 | GET | `/admin/fleet/board/orders/{orderId}` | 回显真实 `holdSentAt`;操作记录补齐取消/退保完整时间线 |
| 派单看板汇总 | GET | `/admin/fleet/board/summary` | 取消后刷新当前状态与能力字段 |
| 派单看板列表 | GET | `/admin/fleet/board/orders` | 取消后刷新当前状态与能力字段 |
| 通知发送日志 | GET | `/admin/notification/logs` | 状态扩展为 `0~6`,新增可靠投递审计字段 |
| 通知发送统计 | GET | `/admin/notification/logs/stats` | 新增跳过、投递中、不确定、撤销等统计 |
| 人工核对可靠短信 | PUT | `/admin/notification/logs/{id}/resolve-reliable` | 新增,仅专用权限可用 |
| 订单详情推送记录 | GET | `/v3/admin/order/{id}/push-records` | 归一化状态枚举调整 |
## 三、创建/修改 HOLD 派单
### 3.1 请求字段
两个写接口新增相同的可选字段:
| 字段 | 类型 | 规则 |
|---|---|---|
| `messageTemplateId` | string | HOLD 通知模板 ID;可空,空时使用 `hold_notify` 默认模板;`holdMode=0` 时忽略 |
| `customBody` | string | 本次通知自定义正文;可空,最大 4000 字符;只冻结本次内容,不回写模板 |
所有雪花 ID 继续按字符串传递和保存,禁止转为 JavaScript `Number`。
创建 HOLD 请求示例:
```http
POST /admin/fleet/assignments
Content-Type: application/json
Authorization: Bearer <fleet-admin-token>
```
```json
{
"orderId": "2074746808742928386",
"requirementId": "2075001000000000001",
"vehicleId": "2076001000000000001",
"driverId": "2077001000000000001",
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"headcount": 4,
"holdMode": 1,
"messageTemplateId": "20260706000101",
"customBody": "王师傅您好,26-7218 团 7 月 20 日待确认。",
"fromEntry": "from-board",
"requestId": "hold-2074746808742928386-001"
}
```
修改为 HOLD 请求示例:
```http
POST /admin/fleet/assignments/2078001000000000001/change
Content-Type: application/json
Authorization: Bearer <fleet-admin-token>
```
```json
{
"effectiveDate": "2026-07-21",
"newVehicleId": "2076001000000000002",
"newDriverId": "2077001000000000002",
"holdMode": 1,
"messageTemplateId": "20260706000101",
"customBody": "李师傅您好,本团 7 月 21 日起调整由您服务,请确认。",
"reason": "原司机临时无法执行",
"requestId": "change-2078001000000000001-001"
}
```
### 3.2 创建响应与 `holdSentAt`
HOLD 创建成功响应示例:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"id": "2078001000000000001",
"assignmentGroupId": "2078001000000000001",
"assignmentSlotId": "2078001000000000001",
"assignmentStatus": "holding",
"stageCode": "holding_wait_driver",
"stageLabel": "排车中·等待司机确认",
"currentStep": 3,
"skippedStepCodes": [],
"protocolPrice": "1300.00",
"holdSentAt": null,
"confirmedAt": null,
"sideEffects": null,
"dailyDifferences": []
}
}
```
前端处理规则:
1. `code=200` 且 `assignmentStatus=holding` 后立即关闭重复提交入口,并刷新详情。
2. `holdSentAt=null` 不是接口失败,也不能显示“短信已发送”;应显示“通知处理中/等待发送结果”。
3. 后续读取 `GET /admin/fleet/board/orders/{orderId}`,仅当 `currentAssignment.holdSentAt` 非空时显示真实发送时间。
4. 模板缺失、供应商失败或结果不确定时,派单仍保持 `holding`,前端通过通知日志查看真实状态,不自行改派单状态。
## 四、通知发送日志状态
### 4.1 状态枚举
`GET /admin/notification/logs` 的请求筛选参数和响应字段 `status` 统一使用:
| status | 含义 | 前端展示建议 |
|---:|---|---|
| 0 | 发送成功,供应商明确受理 | 成功 |
| 1 | 明确失败 | 失败 |
| 2 | 无收件人 | 已跳过·无收件人 |
| 3 | 无模板 | 已跳过·无模板 |
| 4 | 投递中 | 投递中 |
| 5 | 结果不确定 | 待核对 |
| 6 | 授权撤销 | 已撤销 |
前端不得把 `4/5/6` 计入成功或失败。状态 `5` 也不能自动重发,避免供应商实际已发送时重复通知司机。
单条日志新增字段:
```json
{
"id": 2080001000000000001,
"eventCode": "FLEET_DISPATCH_CREATED",
"channel": "SMS",
"bizId": "2078001000000000001",
"bizType": "FLEET_ASSIGNMENT_HOLD",
"status": 5,
"latestProviderAttemptAt": "2026-06-19T10:00:00",
"providerSentAt": null,
"resultTime": null,
"manualResolvedAt": null,
"manualResolvedBy": null,
"manualResolutionReason": null
}
```
新增统计字段:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"totalToday": 20,
"successToday": 12,
"failToday": 2,
"skippedToday": 3,
"dispatchingToday": 1,
"unknownToday": 1,
"canceledToday": 1,
"terminalAttemptToday": 14,
"successRate": 85.71,
"channelStats": []
}
}
```
`successRate` 的分母是 `terminalAttemptToday = successToday + failToday`,前端不要再用 `totalToday` 自行计算。
## 五、人工核对结果不确定短信
该入口只处理超过供应商 29 天查询窗口、仍为 `status=5` 的车务可靠短信,并要求 `NOTIFICATION_RELIABLE_RESOLVE` 专用权限。当前后端只授予 `SUPER_ADMIN`;普通管理员即使手工构造请求也会被拒绝。
确认已发送:
```http
PUT /admin/notification/logs/2080001000000000001/resolve-reliable
Content-Type: application/json
Authorization: Bearer <super-admin-token>
```
```json
{
"resolution": "SUCCESS",
"reason": "阿里云控制台发送记录核对,工单 SMS-20260719-001",
"externalMessageId": "SMS-20260719-001",
"providerSentAt": "2026-06-19T10:00:30"
}
```
确认未发送:
```json
{
"resolution": "NOT_SENT",
"reason": "阿里云控制台未查到对应发送记录",
"externalMessageId": null,
"providerSentAt": null
}
```
成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": null
}
```
处理规则:
- `SUCCESS` 必须传 `externalMessageId` 和 `providerSentAt`;事实时间必须位于最近一次供应商尝试时间前后 5 分钟内。
- `NOT_SENT` 不得传 `providerSentAt`。
- 请求返回 `100001` 表示参数或证据时间不合法;返回 `100003` 表示无权限、日志不符合人工核对条件或状态已变化。
- 操作成功后刷新当前日志行和统计;不要在前端直接篡改状态。
## 六、订单详情推送记录状态
`GET /v3/admin/order/{id}/push-records` 的 `records[].status` 改为:
| status | 含义 |
|---|---|
| `SENT` | 供应商明确受理 |
| `FAILED` | 明确失败 |
| `SKIPPED_NO_RECIPIENT` | 无收件人 |
| `SKIPPED_NO_TEMPLATE` | 无模板 |
| `DISPATCHING` | 投递中 |
| `UNKNOWN` | 结果不确定 |
| `CANCELED` | 授权已撤销 |
| `UNRECOGNIZED` | 未识别的存量状态 |
响应示例:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"total": 1,
"records": [
{
"id": 2080001000000000001,
"eventCode": "FLEET_DISPATCH_CREATED",
"channel": "SMS",
"channelName": "短信",
"kind": "sms",
"target": "王师傅",
"status": "UNKNOWN",
"statusName": "结果不确定",
"rawStatus": 5,
"failReason": null,
"bizId": "2078001000000000001",
"bizType": "FLEET_ASSIGNMENT_HOLD",
"sentAt": "2026-07-19T10:00:00"
}
],
"summary": {
"all": 1,
"sms": 1,
"miniapp": 0,
"officialAccount": 0,
"inapp": 0,
"internal": 0,
"wework": 0,
"other": 0,
"failed": 0
}
}
}
```
`summary.failed` 只统计 `rawStatus=1`,不包含 `UNKNOWN/DISPATCHING/CANCELED`。
## 七、取消后重新派车
前端仍调用既有接口取消:
```http
DELETE /admin/fleet/assignments/{assignmentId}
```
成功后的正确流程:
1. 接受取消响应中的 `assignmentStatus=canceled`。
2. 重新请求 `/admin/fleet/board/summary`、`/admin/fleet/board/orders` 和 `/admin/fleet/board/orders/{orderId}`。
3. 后端完成需求重开后,当前订单重新出现可派状态;按钮只看最新响应的 `canAssign`,不要本地强制改为可派。
4. 如果首次刷新仍未开放重派,保持处理中并短暂重试刷新;不要调用 `/v3/internal/order/**`,也不要让用户重复取消。
5. 重新派车成功后再次刷新服务端状态,不能沿用已取消派单的 `assignmentId`。
派单详情 `operationLog.records[]` 会保留不可变取消时间线,新增/强化的 `opType` 包括:
- `cancel_requested`
- `driver_notification_recorded`
- `cancel_evidence_recorded`
- `insurance_refund_pending`
- `insurance_refund_succeeded`
- `insurance_refund_failed`
- `cancel_completed`
- `cancel_restored`
- `cancel_failed`
前端优先展示后端返回的 `opTypeLabel`、`operationStatusLabel` 和 `summary`,不要另维护中文文案。`operationStatus` 允许 `pending/succeeded/failed`。
## 八、网关 internal 边界
下列路径全部禁止客户端调用:
```text
/internal
/internal/**
/v3/internal
/v3/internal/**
```
网关按项目协议返回 HTTP 200,但响应体为:
```json
{
"code": 403,
"message": "接口不可访问",
"success": false,
"data": null
}
```
请前端全仓检查是否仍有 `/v3/internal/mp/**` 等历史调用;如存在,不要自行改成另一个 internal 地址,应反馈后端补正式 BFF/admin 契约。
## 九、前端待处理清单
- [ ] 派单/改派弹窗在 HOLD 模式支持 `messageTemplateId/customBody`,DIRECT 模式不提交或忽略这两个字段。
- [ ] HOLD 创建成功时把 `holdSentAt=null` 展示为处理中,不显示“已发送”。
- [ ] 通知日志筛选、标签和统计适配 `0~6` 状态及新增字段。
- [ ] 仅对具备专用权限的账号展示“人工核对可靠短信”入口,并实现 `SUCCESS/NOT_SENT` 两种表单校验。
- [ ] 订单详情推送记录适配新的归一化状态枚举。
- [ ] 取消派单后刷新服务端状态,以 `canAssign` 控制重新派车入口。
- [ ] 确认前端不存在任何 `/internal/**` 或 `/v3/internal/**` 调用。
- [ ] 所有雪花 ID 保持字符串。
## 十、后端交付与测试环境状态
- 后端 PR #5073、#5084 已合并到 `dev-v3`。
- `hl-order-service-v3`、`hl-fleet-service`、`hl-gateway` 已按顺序部署 TEST,双实例健康;当前 OpenAPI 已公开本文全部管理端接口。
- 已用真实测试订单完成 DIRECT、取消、需求重开、再次 DIRECT、司机同步和退保时间线验收。
- TEST 当前 `hold_notify` 短信模板仍是占位配置,真实 HOLD 短信会失败关闭,`holdSentAt` 保持 `null`;这是环境配置阻塞,不应由前端伪造成发送成功。
- 本文件只做契约交接,不修改 `hl-ui`。
@@ -0,0 +1,50 @@
# 房务选择酒店误传 preferredHotelId 导致只显示 1 家(前端待处理)
## 现象
订单 `2078739922130243586` 第 1 晚打开“选择酒店”弹窗,只显示定制师指定的“呼伦贝尔香格里拉大酒店”,分页显示“共 1 条”,页面提示“已限定定制师指定酒店”。
## 已确认原因
`192.168.100.160:9527` 当前 Vite 服务实际返回的 `PickHotelModal.vue` 仍在候选请求中传递:
```js
preferredHotelId:
on.specifiedHotelIds?.length === 1 ? String(on.specifiedHotelIds[0]) : undefined
```
该参数会要求后端按指定酒店过滤,因此响应只剩 1 家。这与当前产品意图“定制师指定酒店置顶并标记,房务仍可选择其他酒店”冲突。
当前 `D:/work2/hl-ui` 源码已经不再传该参数,说明 `192.168.100.160:9527` 运行的是未同步的工作树或旧代码。
## 接口证据
接口:`GET /v3/admin/hotel-candidates`
公共参数:
- `orderId=2078739922130243586`
- `dayNumber=1`
- `stayDate=2026-07-22`
- `roomCount=2`
- `limit=50`
结果:
- 不传 `preferredHotelId`、无关键词:返回 21 家;香格里拉为 `isConsultantRecommended=true` 且排第 1。
- 不传 `preferredHotelId`、`keyword=满洲里`:返回 3 家,包括香格里拉、满洲里凯旋大酒店、满洲里饭店(百年俄式)。
- 当前截图环境传入唯一 `preferredHotelId`:只返回指定酒店 1 家。
## 前端处理要求
1. 房务候选请求不得传 `preferredHotelId`,无论 `specifiedHotelIds` 是 1 个还是多个。
2. `specifiedHotelIds` 仅用于页面提示;推荐标记以接口 `isConsultantRecommended` 为准。
3. 不输入关键词时展示后端返回的全部候选;跨城搜索继续使用 `keyword`。
4. 确认 `192.168.100.160:9527` 的 Vite 进程工作目录与 `D:/work2/hl-ui` 当前目标分支一致,重启 Vite 后清除模块缓存并复测。
## 验收
- 打开本订单第 1 晚选择酒店,不输入关键词时不再显示“共 1 条”,可看到其他酒店。
- 搜索“满洲里”返回 3 家。
- 香格里拉仍显示“定制师推荐”,但不会阻止选择其他酒店。
- Network 中 `/v3/admin/hotel-candidates` 请求不含 `preferredHotelId`。
@@ -0,0 +1,66 @@
# 房务最终确认后修改配房按钮被旧前端隐藏(前端待处理)
## 目标前端
- **端类型:管理后台(Web)**
- **目标仓库:`mmg/hl-ui`**
- **仓库地址:`https://git.1814.love:8443/mmg/hl-ui.git`**
- **前端本地测试环境:`http://192.168.100.160:9527`**
- **小程序:无需处理**
本通知应由管理后台前端负责人在 `mmg/hl-ui` 处理,不属于后端仓库 `wx/HL`,也不属于小程序前端。
## 现象
订单 `HL20260719151313174` 已最终确认、配房进度 `2/2`,房务详情配房行程只显示“询房”按钮;既有配房行未显示“替换”“改协议价”“移除”等修改入口。
业务要求:最终确认后房务仍可修改配房。发起修改时先将住宿需求从完成态解冻回配房中,再执行替换、移除、改价等操作。
## 已确认原因
`192.168.100.160:9527` 当前 Vite 服务实际返回的 `OrderDetailModal.vue` 仍包含旧门槛:
```js
const canMutateRequirement = computed(
() =>
canEditHouseOrder.value &&
!requirementReadOnly.value &&
(merged.value?.finalized !== true || merged.value?.reopenAction?.enabled === true)
)
```
后端详情当前有意将 `reopenAction` 设为 disabled,不再把“回配”作为单独按钮;后端各直接编辑入口会调用 `reopenIfFinalizedForDirectEdit()` 自动解冻。因此旧前端条件在 `finalized=true` 时恒为 false,连真正的修改按钮也全部隐藏。
当前 `D:/work2/hl-ui` 源码已经改为:
```js
const canMutateRequirement = computed(
() => canEditHouseOrder.value && !requirementReadOnly.value
)
```
并由 `ensureRequirementEditable(reqId)` 在写操作前调用 `reopenRequirement(reqId)`,与后端自动解冻语义一致。
## 前端处理要求
1. 同步当前 `D:/work2/hl-ui` 正确实现到 `192.168.100.160:9527` 实际运行工作树,重启 Vite 服务。
2. `canMutateRequirement` 不得用 `finalized` 或 `reopenAction.enabled` 隐藏配房修改入口。
3. 最终确认后,只要订单属于当前房务、需求仍生效且未驳回/作废,应继续显示:
- 当晚“替换”;
- 已确认配房行“改协议价”;
- 已确认配房行“移除”。
4. 写操作前沿用 `ensureRequirementEditable()`;不得要求用户先点击一个独立“回配”按钮。
5. 驳回需求、作废需求、非本人订单、组长只读入口仍保持只读,不得放宽权限边界。
## 后端依据
- `HouseAssignmentService.reopenIfFinalizedForDirectEdit()`:最终确认后的直接编辑自动解冻。
- 替换、移除、改协议价等多个写入口均已调用该方法。
- `HouseDetailAggregator` 不暴露独立 reopen action 属预期行为,不需要后端恢复该按钮。
## 验收
- 打开订单 `HL20260719151313174`,完成态仍可看到“替换”“改协议价”“移除”。
- 点击修改后 Network 先出现 reopen 或对应写接口自动解冻,操作成功,房务状态回到配房中。
- 重新配房并逐日确认后,可再次最终确认。
- 非本人、驳回、作废及只读入口仍不显示写操作。
@@ -0,0 +1,45 @@
# 房务调整提醒按总人数展示(修改接口)
## 目标前端
- **端类型:管理后台(Web)**
- **目标仓库:`mmg/hl-ui`**
- **仓库地址:`https://git.1814.love:8443/mmg/hl-ui.git`**
- **联调/验收环境:`http://192.168.100.160:9527`**
- **小程序:无需处理**
## 背景
订单调整删除一名出行人后,房务端“订单调整提醒”曾显示人员类型变化,例如“儿童人数 2 → 1”。房务只需要核对订单总人数,因此后端统一调整为“总人数 4 → 3”。
## 接口语义变更
涉及调整记录及房务详情中复用的 `changeItems`:
```json
{
"type": "HEADCOUNT",
"label": "总人数",
"before": "4",
"after": "3"
}
```
- 总人数发生变化时,只返回一条 `HEADCOUNT`,`label` 固定为 `总人数`。
- 不再按成人、儿童、小童、婴儿分别返回多条 `HEADCOUNT`。
- 人员类型变化但总人数不变时,不返回 `HEADCOUNT`。
- 出行人明细及 `addedTravelerIds` 契约不变。
## 管理后台处理要求
1. 房务“订单调整提醒”直接展示 `label + before → after`,不得自行按人员类型重新计算。
2. 不要依赖旧的“成人人数/儿童人数/小童人数/婴儿人数”标签。
3. 历史调整记录仍可能保留旧标签,前端需要兼容只读展示;新记录按“总人数”展示。
## 验收
- 订单出行人由 4 人删除 1 人后,房务提醒显示“总人数 4 → 3”。
- 页面不显示“儿童人数 2 → 1”等人员类型变化。
- 总人数不变时不出现人数调整提醒。
后端关联:`wx/HL#5090`、PR `wx/HL#5091`。
@@ -0,0 +1,127 @@
# 作废房务需求只读与历史详情(修改接口)
## 目标前端
- **端类型:管理后台(Web)**
- **目标仓库:`mmg/hl-ui`**
- **仓库地址:`https://git.1814.love:8443/mmg/hl-ui.git`**
- **联调/验收环境:`http://192.168.100.160:9527`**
- **小程序:无需处理**
## 业务硬规则
作废房务需求只能查看。页面不得提供联系房务、联系定制师、转单、配房、询房、替换、移除、改价、清空配房、驳回、最终确认等任何业务操作。
## 问题与原因
同一订单调整后会保留旧的失活需求并生成新的生效需求。此前列表虽返回 `voided=true`,但前端未标红、未展示原因;点击旧行又只按 `orderId` 请求详情,导致打开当前生效需求,出现旧记录与当前配房串版。
## 接口变更
### 1. 我的房务订单列表
`GET /v3/admin/order/grab-pool/my-claims/hotel`
作废行新增/明确字段:
```json
{
"id": "2078779808241668097",
"orderId": "2078739922130243586",
"requirementVersion": 2,
"voided": true,
"voidReason": "订单调整生成新版本,原需求已作废",
"voidedAt": "2026-07-20T16:52:29",
"primaryAction": {
"type": "VIEW",
"url": "/admin/order/2078739922130243586/arrange?requirementId=2078779808241668097"
}
}
```
注意:列表字段 `id` 就是本行的房型需求 ID,打开详情时必须连同该 ID 传给详情接口,不能只传 `orderId`。
### 2. 房务详情支持指定历史需求
`GET /admin/house/orders/{orderId}?requirementId={requirementId}`
该接口使用既有房务详情命名空间 `/admin/house`,请求时必须沿用 API 模块的绝对路径配置,不得自行添加 `/v3`。错误请求 `/v3/admin/house/orders/{orderId}` 会返回“接口不存在”。
### 2026-07-20 本地测试环境 Network 复核
`http://192.168.100.160:9527` 点击作废行“查看”时实际发出:
```text
错误:GET /v3/admin/house/orders/2078739922130243586?requirementId=2078779808241668097
正确:GET /admin/house/orders/2078739922130243586?requirementId=2078779808241668097
```
同一弹窗的需求历史请求已经使用正确命名空间:
```text
GET /admin/house/orders/2078739922130243586/requirement-history
```
因此请检查详情 API 方法是否误传 `baseURL: '/v3'`、V3 request config 或再次拼接 `/v3`。只修改详情请求,`operation-log` 仍按它自己的既有 `/v3/admin/house/...` 契约处理,不得全局替换。
- 不传 `requirementId`:保持原行为,返回当前生效需求。
- 传 `requirementId`:精确返回该订单的指定历史需求;ID 不属于该订单时返回业务错误。
- 作废历史需求不会混入当前需求的配房数据。
详情新增顶层字段:
```json
{
"viewedRequirementId": "2078779808241668097",
"historicalRequirement": true,
"voided": true,
"voidReason": "订单调整生成新版本,原需求已作废",
"voidedAt": "2026-07-20T16:52:29"
}
```
`requirement.history[]` 同步增加 `requirementId`、`voided`、`voidReason`、`voidedAt`。
历史作废详情中:
- `actions` 下全部动作的 `enabled=false`;
- `permissions.canEdit=false`;
- `permissions.canSendMessage=false`;
- `requirement.actions.canSendMessage=false`,其他写动作同样为 `false`;
- `permissions.canViewMessage=true` 只代表允许查看既有留言,不代表可回复。
## 管理后台处理要求
1. `voided=true` 的列表行和详情必须使用明确的红色作废样式,并展示“已作废”、`voidReason` 和作废时间。
2. 作废列表行只能显示“查看”;不得显示“更多”菜单或任何联系、流转、配房按钮。
3. 点击作废行必须携带本行 `id` 作为 `requirementId` 请求详情,不得复用当前有效需求详情。
4. 详情只要 `voided=true` 或 `historicalRequirement=true`,前端必须再次强制只读并隐藏全部业务操作,不能只依赖某一个按钮字段。
5. 人数调整提醒直接展示后端 `changeItems`;按通知 70,人数仅显示“总人数 4 → 3”,不显示成人/儿童等具体人员类型变化。
### 当前订单详情增加“作废记录”入口
在当前有效订单的房务详情中增加按钮:`作废记录(N)`,让房务不必返回列表寻找红色卡片。
- `N` 为该订单历史需求中 `voided=true` 的数量;没有作废记录时可隐藏按钮或显示禁用的 `作废记录(0)`。
- 按钮建议放在详情标题区或需求信息区,与普通业务写操作分开,避免误认为可以恢复作废需求。
- 点击后打开只读抽屉/弹窗,列出该订单全部作废需求,至少展示:需求版本、提交/作废时间、作废原因、原状态。
- 列表数据可使用详情响应的 `requirement.history[]`,按 `voided=true` 过滤;每项必须使用自身 `requirementId`。
- 点击某条“查看详情”时调用:
```text
GET /admin/house/orders/{orderId}?requirementId={该条requirementId}
```
- 历史详情继续执行严格只读规则,只能关闭/返回,不能联系、转单、配房、清空、驳回、最终确认或执行其他业务操作。
- 作废记录列表按 `voidedAt DESC` 展示,最新作废记录在前;本入口不改变“我的订单”主列表中作废卡片统一置底的规则。
## 验收
- 同一订单的作废旧行与当前有效行能明确区分,旧行标红并显示原因。
- 旧行仅有“查看”,不存在任何写操作或联系操作。
- 打开旧行后 `viewedRequirementId` 等于该行 `id`,内容为旧需求快照,不出现当前配房。
- 作废详情仅可阅读,所有动作均隐藏或禁用。
- 删除一名出行人后,调整提醒显示“总人数 4 → 3”。
- 当前有效订单详情显示“作废记录(1)”;点击可看到该订单的作废需求列表,并能打开对应只读历史详情。
后端关联:`wx/HL#5092`。
@@ -0,0 +1,70 @@
# 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`(`https://git.1814.love:8443/mmg/hl-ui.git`)
- 联调/验收环境:`http://192.168.100.160:9527`
- 小程序、H5 及其他前端:无需处理
# 变更背景
订单改出发日期后,原入住日期已经配置的酒店不能静默平移到新日期。房务需要明确核对并逐条删除旧配房;旧配房未清完时禁止最终确认。
# 详情接口新增字段
`GET /admin/house/orders/{orderId}` 顶层新增:
```json
{
"pendingRescheduleAssignments": [
{
"assignmentId": "99001",
"originalStayDate": "2026-07-22",
"hotelId": "8001",
"hotelName": "示例酒店",
"roomTypeId": "9001",
"roomTypeName": "普通标间",
"roomCategory": "STANDARD",
"roomCategoryLabel": "标间",
"roomCount": 2,
"assignmentStage": "FINAL_CONFIRMED",
"assignmentStageLabel": "最终确认",
"deleteEndpoint": "DELETE /v3/admin/order/assignments/99001"
}
]
}
```
`assignmentStage` 枚举:
| 值 | 中文 | 含义 |
| --- | --- | --- |
| `UNCONFIRMED` | 未单日确认 | 改期前仍处于询房/候选阶段 |
| `DAY_CONFIRMED` | 单日确认 | 改期前已完成该日确认,但原需求未最终确认 |
| `FINAL_CONFIRMED` | 最终确认 | 改期前所属住宿需求已经最终确认 |
数组为空表示没有改期旧配房待清理。旧配房不会再出现在当前 `itinerary[].assignments`,也不计入当前配房进度。
# 前端交互要求
1. 在“订单调整提醒”的改期记录下展示 `pendingRescheduleAssignments`,每行至少显示:原日期、酒店、房型、数量、配房步骤。
2. 每行提供“删除旧配房”,调用返回的 `deleteEndpoint`;成功后重新拉取详情。
3. 只要数组非空,不允许用户最终确认,并显示后端 `actions.canFinalize.disabledReason`。
4. 数组清空后再按后端 `actions.canFinalize.enabled` 决定按钮状态,禁止前端自行推断。
5. 删除仍可能因领取归属、房务写权限、并发修改或库存释放链路失败而报错,直接展示后端消息并刷新详情。
# 最终确认写口门禁
`POST /admin/house/assignments/requirements/{requirementId}/finalize`
若仍有旧日期配房,返回业务错误:
- code:`808183`
- message:`改期前旧日期配房尚未清理,请逐条删除后再最终确认`
该门禁由后端强制执行,前端禁用按钮仅用于交互提示。
# 兼容说明
- 字段为 additive;旧页面忽略新增字段不会影响反序列化。
- `roomTypeName` 在资源服务降级时可为空,前端回退 `roomCategoryLabel`。
- `assignmentId`、`hotelId`、`roomTypeId` 按字符串处理,禁止转 JavaScript `number`。
@@ -0,0 +1,64 @@
# 作废需求详情冻结作废时配房快照
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`(`https://git.1814.love:8443/mmg/hl-ui.git`)
- 联调/验收环境:`http://192.168.100.160:9527`
- 小程序、H5 及其他前端:无需处理
## 业务规则
订单调整生成新住宿需求时,旧需求详情必须展示“该需求作废当时”的配房事实,不能复用当前订单的新日期、新行程地点或当前配房。历史数据严格只读,不能恢复或执行任何业务操作。
## 接口
```text
GET /admin/house/orders/{orderId}?requirementId={作废需求ID}
```
历史详情的 `itinerary[].assignments[]` 明确返回冻结字段:
```json
{
"dayNumber": 1,
"stayDate": "2026-07-28",
"assignments": [
{
"assignmentId": "2079188886726098945",
"hotelId": "2023714929877450753",
"hotelName": "呼伦贝尔香格里拉大酒店",
"roomTypeId": "2023727403196502017",
"roomTypeName": "普通标间",
"roomCategory": "STANDARD",
"confirmStatus": "CONFIRMED",
"confirmStatusLabel": "已确认",
"roomCount": 1,
"protoPrice": "280.00",
"settlementPrice": "279.00",
"settleType": "sign",
"sellPrice": "280.00",
"deductInventory": false
}
]
}
```
`stayDate`、酒店、房型、数量、价格、支付方式、库存口径和确认状态均来自作废时快照。后续删除/修改当前配房、资源酒店改名或价格调整,不影响历史详情。
## 管理后台处理要求
1. 作废详情按 `itinerary[]` 展示旧日期;每条配房至少显示酒店、`roomTypeName`、`roomCount` 和 `confirmStatusLabel`。
2. 房型名称优先使用 `roomTypeName`;部署前没有可信名称快照的旧数据才允许回退 `roomCategoryLabel`,不得按 ID 或列表位置猜测。
3. `historicalRequirement=true` 或 `voided=true` 时保持严格只读:仅允许查看、关闭、查看车务;不得出现联系、转单、配房、删除、清空、驳回、最终确认等房务写操作。
4. 禁止用当前订单出发日期推算历史 `stayDate`,禁止调用当前资源结果覆盖后端返回的历史酒店/房型快照。
5. ID 字段按字符串处理,禁止转换为 JavaScript `number`。
## 兼容与验收证据
- 变更为 additive,当前生效需求的接口结构不变。
- 测试订单:`HL20260719151313174`,`orderId=2078739922130243586`。
- 作废需求:`requirementId=2079181505287897090`。
- 测试环境返回 3 晚旧配房:2026-07-28/29/30,酒店“呼伦贝尔香格里拉大酒店”,房型“普通标间”,数量 1/2/3,状态均为“已确认”。
- 浏览器验收使用仓库 CDP 脚本完成,页面无 console error 或 failed request。
@@ -0,0 +1,133 @@
# 【前端待处理·管理后台】#4933 车务矩阵图例与空闲格直接派单
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 仓库地址:<https://git.1814.love:8443/mmg/hl-ui.git>
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理,本问题仅涉及管理后台车务矩阵页面
- H5:无需处理,本问题仅涉及管理后台车务矩阵页面
## 问题与结论
2026-07-21 在车务管理员角色访问 `/fleet/matrix` 时确认两处前端缺陷:
1. 页面图例仍显示“绿 海拉尔接/送机、橙 外地接/送机”,把地域误当成衔接状态;这与 #4933 已确认的后端语义不一致。
2. 点击车辆空闲格只弹出“请先从未派订单池拖拽或在订单详情发起派单”,阻断了矩阵页直接派单。矩阵页已有 `AssignModal` 和车辆预选能力,应直接进入本页派单流程。
3. 订单详情“历史操作”把保险退保结果的内部 JSON 原样拼进业务时间线,业务人员无法阅读。
4. 矩阵只解释衔接标记,未解释蓝灰色派车占用条;同车出现重叠时也没有冲突语义。后端 #5109 将统一过滤取消记录,前端仍需区分普通占用与有效派车重叠异常。
后端 #5109 已统一矩阵统计与占用条口径,并新增有效派车重叠异常字段。前端必须消费后端下发的衔接状态和异常信息,不能按“海拉尔/外地”或占用条颜色自行推导。
## 图例适配要求
图例按 `connections[].status/style` 固定展示以下四种业务语义:
| `status` | `style` | 图例文案 | 展示要求 |
| --- | --- | --- | --- |
| `SAME_CITY_OK` | `GREEN` | 同城衔接正常 | 绿色实线/标记 |
| `DIFFERENT_CITY` | `DARK_RED` | 不同城 | 深红色实线/强提醒 |
| `SAME_CITY_TOO_SHORT` | `LIGHT_RED` | 同城间隔不足 | 淡红色实线/提醒 |
| `MISSING_TIME` | `RED_DASHED` | 缺少接送信息 | 红色虚线 |
- 删除“海拉尔接/送机”“外地接/送机”两项旧图例。
- 图例颜色、矩阵连线/标记和悬浮详情必须使用同一份状态映射。
- 悬浮详情优先展示后端 `label`、`reasonText`、`intervalMinutes`、`minIntervalMinutes`;不得覆盖后端文案或重新计算状态。
图例应分成两组,避免混淆:
- 派车占用:说明订单占用条的基础颜色、边框及文字含义。
- 订单衔接:继续展示上述四种后端衔接状态。
若后端返回有效派车重叠异常标记,必须使用独立冲突样式和明确文案,不能复用普通占用色,也不能把两条记录静默叠放。
## 历史操作展示要求
- 时间线默认只展示 `opTypeLabel`、`operationStatusLabel`、`summary`、操作时间和业务操作人。
- `detailJson` 仅供诊断或折叠的技术明细使用,不得直接拼接到 `content`,不得默认展示 JSON。
- 保险退保成功示例应展示为“司机保险退保成功 / 已完成 / 共 1 个服务日,线上成功 1,线下完成 0”,不展示 `resolution`、`refundResult`、日期数组等内部字段名。
- `canceled` 且有效派车组为 0 的订单可在详情中查看取消时间线,但不能在矩阵中继续绘制占用条。
## 空闲格直接派单要求
用户点击车辆某日的空闲格后,应在当前矩阵页面完成派单:
1. 打开未派订单选择层(或复用现有选择组件),只列出当前筛选范围内可派订单。
2. 选中订单后打开现有 `AssignModal`。
3. 自动预选被点击车辆,并将点击日期带入派单日期上下文;仍允许用户在弹窗内调整司机、车辆和合法日期范围。
4. 按现有候选、预校验和创建派单接口完成校验与提交,不能绕过冲突、容量、常驻错配确认等后端门禁。
5. 成功后关闭弹窗并刷新矩阵、统计和未派订单数量;失败时保留用户已填内容并展示后端错误。
6. 当确实没有可派订单时才显示空态“暂无可派订单”,不得再提示用户去订单详情发起派单。
建议直接修正当前 `@idle-click="onIdleClick"` 分支:现实现只调用 `message.info`,但同页已经挂载 `AssignModal`、`activeOrder`、`preselectVehicle` 和 `assignMode`,应复用现有派单链路。
## 接口证据
矩阵响应已提供后端判定结果,核心字段位于车辆相邻订单衔接集合 `connections`:
```json
{
"status": "SAME_CITY_OK",
"label": "同城衔接正常",
"style": "GREEN",
"reasonCode": "SAME_CITY_INTERVAL_SUFFICIENT",
"reasonText": "同城前后订单衔接间隔满足配置阈值",
"intervalMinutes": 180,
"minIntervalMinutes": 120
}
```
后端允许值:
- `status`:`MISSING_TIME`、`DIFFERENT_CITY`、`SAME_CITY_TOO_SHORT`、`SAME_CITY_OK`
- `style`:`RED_DASHED`、`DARK_RED`、`LIGHT_RED`、`GREEN`
每辆车新增 `overlaps`,仅在两个不同的有效派车组日期重叠时返回;无异常时固定为空数组:
```json
{
"id": "100",
"assignments": [],
"overlaps": [
{
"firstAssignmentGroupId": "1001",
"secondAssignmentGroupId": "1002",
"overlapStartDate": "2026-05-03",
"overlapEndDate": "2026-05-04",
"code": "ACTIVE_ASSIGNMENT_OVERLAP",
"message": "同一车辆存在有效派车日期重叠"
}
]
}
```
- `canceled` 派车切片不进入 `assignments`、顶部统计、衔接计算或 `overlaps`。
- 同一派车组的有效日期被取消日切断时,`assignments` 返回两个不连续日期段,但顶部仍按一个派车组计数。
- `unassigned-orders` 条目及 `parallelAssignments[]` 新增 `serviceDateSegments[]`,每项包含 `startDate/endDate`;存在取消日期缺口时返回多个连续有效段。旧 `startDate/endDate` 仅表示该组总体边界,前端绘制或判断逐日有效性必须以 `serviceDateSegments` 为准。
- `overlaps` 是明确的数据异常,不是新的普通占用颜色;前端应显示独立冲突提示并允许定位涉及的两个派车组。
派单继续复用现有车务候选、预校验和创建派单接口,不新增接口。
## 验收清单
- [ ] `/fleet/matrix` 图例只展示四种后端衔接语义,不再出现地域型图例。
- [ ] 构造四种 `status/style` 数据,图例、矩阵标记和悬浮说明三者一致。
- [ ] 点击任意车辆空闲格可在当前页面选择未派订单并打开派单弹窗。
- [ ] 派单弹窗自动预选点击车辆及日期上下文。
- [ ] 正常派单成功后矩阵、统计与未派数刷新。
- [ ] 冲突或校验失败时展示后端错误且不产生半成品派单。
- [ ] 无可派订单时展示空态,不再引导去订单详情。
- [ ] 图例分别说明“派车占用”和“订单衔接”,普通占用、衔接标记与重叠异常不会混淆。
- [ ] 历史操作默认不显示或拼接 `detailJson`,保险退保记录使用中文业务摘要。
- [ ] 已取消且有效派车组为 0 的订单只保留详情历史,不绘制矩阵占用条。
- [ ] `vehicles[].overlaps[]` 非空时展示明确重叠异常;为空时不显示冲突样式。
- [ ] 在 <http://192.168.100.160:9527> 以车务管理员角色完成页面、Network/API 响应和截图验收。
## 现场证据
- 页面:`/fleet/matrix`
- 角色:车务管理员
- 现象:错误地域图例;点击车辆空闲格连续出现阻断性提示
- 现场截图:由 #4933 验收反馈于 2026-07-21 提供
- 当前调试 Chrome 登录态已失效,自动复核跳转登录页;修复后需在上述固定验收环境重新登录并完成验收清单。
@@ -0,0 +1,68 @@
# 目标前端
- 端类型:管理后台(Web)
- 前端仓库:`mmg/hl-ui`(`https://git.1814.love:8443/mmg/hl-ui.git`)
- 联调环境:`http://192.168.100.160:9527`
- 其他前端:不需要处理
# 变更目标
房务管理员“订单列表”的第五个状态筛选由“异常”替换为“作废”。该筛选必须走后端分页,不能只过滤当前页。
# 接口变更
## 我的接单
`GET /v3/admin/order/grab-pool/my-claims/hotel`
新增查询参数取值:
```text
status=voided
```
语义:仅返回当前房务曾领取、后因订单调整生成新版本而作废的旧住宿需求。
响应保持原结构:
- `list`:本页作废需求;每行 `voided=true`。
- `total`:全部命中作废需求数,用于服务端分页。
- `stats.voided`:当前房务全部作废需求计数,不随当前状态筛选收窄。
- `voidReason`、`voidedAt`:作废原因和时间。
- `primaryAction.code=VIEW`:只读查看,不提供配房、最终确认、清空配房、转单等写操作。
现有 `status=exception` 契约仍保留给其他业务入口,语义不变;本页面不再把它作为第五个筛选项展示。
# 管理后台适配要求
房务管理员“订单列表”顶部筛选固定为:
1. 全部
2. 配房中
3. 待确认
4. 已完成
5. 作废
第五项适配:
- 文案由“异常”改为“作废”。
- value 由 `exception` 改为 `voided`。
- 数量徽标读取 `stats.voided`,不要继续读取 `stats.exception`。
- 点击后请求 `status=voided`,列表和 `total` 直接使用接口结果,禁止前端当前页二次筛选。
- 作废卡片保持红色只读样式,显示作废原因和作废时间。
- “查看”必须携带该行自己的住宿需求 ID 作为 `requirementId`,进入作废历史详情。
# 验收标准
- 在 `http://192.168.100.160:9527/housekeeper/orders` 不再显示“异常”筛选,显示“作废”。
- “作废”徽标数量等于接口 `stats.voided`。
- 点击“作废”后 Network 请求包含 `status=voided`。
- 多页作废数据的 `total`、分页与列表一致,不发生只过滤当前页的问题。
- 作废列表仅包含 `voided=true` 的历史需求;正常配房中的当前需求不混入。
- 作废行只保留“查看”,详情为只读状态。
# 后端交付
- 后端 Issue:`wx/HL#5103`
- 后端分支:`fix/5103-house-voided-filter`
- 合入并部署测试环境后,前端再进行 Network 与页面验收。
@@ -0,0 +1,107 @@
# 房务改期旧配房人工清理
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`(`https://git.1814.love:8443/mmg/hl-ui.git`)
- 联调/验收环境:`http://192.168.100.160:9527`
- 其他前端:小程序、H5 无需处理
> 服务:`hl-order-service-v3`(8086)
> PR:#5097
> 日期:2026-07-21
> 影响范围:房务订单详情弹窗的改期后旧配房清理和重新配房流程
---
## 关键变化
订单改期后,既有配房不再自动迁移到新日期,也不会自动变为新需求的可确认候选。后端将这些配房标记为待人工删除;房务必须逐条删除旧配房,再按新行程重新提交并确认配房。
当前后端已返回待清理数据和删除接口,但管理后台尚未消费该字段,导致页面无法完成该流程。
---
## 接口清单
| 接口 | 方法 | 路径 | 变更 | 用途 |
|------|------|------|------|------|
| 房务订单详情 | GET | `/admin/house/orders/{orderId}` | 响应新增字段 | 返回改期后待删除旧配房 |
| 删除配房 | DELETE | `/v3/admin/order/assignments/{assignmentId}` | 既有接口 | 删除一条待清理旧配房 |
---
## 房务订单详情
### `GET /admin/house/orders/{orderId}`
响应 `data` 新增 `pendingRescheduleAssignments`:
| 字段 | 类型 | 说明 |
|------|------|------|
| `assignmentId` | String | 待删除配房 ID |
| `originalStayDate` | String | 原入住日期,格式 `yyyy-MM-dd` |
| `hotelId` | String | 原酒店 ID |
| `hotelName` | String | 原酒店名称快照 |
| `roomTypeId` | String | 原房型 ID |
| `roomTypeName` | String | 原房型名称,资源不可用时可为空 |
| `roomCategory` | String | 房型字典 code |
| `roomCategoryLabel` | String | 房型中文 |
| `roomCount` | Integer | 房间数量 |
| `assignmentStage` | String | `UNCONFIRMED`、`DAY_CONFIRMED`、`FINAL_CONFIRMED` |
| `assignmentStageLabel` | String | 未单日确认、单日确认、最终确认 |
| `deleteEndpoint` | String | 本条配房的删除接口 |
无待清理配房时该字段返回空数组。
```json
{
"code": 0,
"data": {
"pendingRescheduleAssignments": [
{
"assignmentId": "2079430000000000001",
"originalStayDate": "2026-07-23",
"hotelId": "2001",
"hotelName": "旧日期酒店",
"roomTypeId": "3001",
"roomTypeName": "标准大床房",
"roomCategory": "STANDARD",
"roomCategoryLabel": "标准间",
"roomCount": 2,
"assignmentStage": "DAY_CONFIRMED",
"assignmentStageLabel": "单日确认",
"deleteEndpoint": "DELETE /v3/admin/order/assignments/2079430000000000001"
}
]
}
}
```
---
## 前端处理规则
1. `pendingRescheduleAssignments` 非空时,在房务订单详情中显示“改期前待清理配房”区域,逐条展示原入住日期、酒店、房型、房间数和配房阶段。
2. 每条记录使用其 `deleteEndpoint` 调用删除配房接口;删除成功后重新获取订单详情。
3. 待清理列表非空时,禁用最终确认,并提示“改期前旧日期配房尚未清理,请逐条删除”。
4. 删除完成后,由房务按当前行程重新提交配房候选,再执行单日确认。不得直接对旧配房调用确认接口。
5. `assignmentStage` 仅作展示,不可通过编辑旧配房绕过删除步骤。
---
## 边界行为
- 直接确认旧配房不会产生候选,接口将返回 `808118`“该天无可确认的询房中候选”。
- 旧配房未清理时,最终确认返回 `808183`,不得绕过。
- 删除后重新配房仍沿用现有提交和单日确认接口,无新增请求体字段。
## 不影响范围
- 抢单池、领取、转单和释放流程不变。
- 小程序、H5 无需适配。
- 非改期订单的详情和配房流程不变。
## 相关文档
- PR:[#5097](https://git.1814.love:8443/wx/HL/pulls/5097)
@@ -0,0 +1,73 @@
# 房务作废配房视觉区分
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`(`https://git.1814.love:8443/mmg/hl-ui.git`)
- 联调/验收环境:`http://192.168.100.160:9527`
- 其他前端:小程序、H5 无需处理
> 服务:`hl-order-service-v3`(8086)
> 关联 PR:#5097
> 日期:2026-07-21
> 影响范围:房务订单详情中的作废需求及配房快照展示
---
## 关键问题
当前管理后台打开作废住宿需求时,顶部已提示“只读查看”和“已作废”,但“配房行程”仍使用绿色背景以及“已确认/已完成”标签。该视觉语义会让房务误认为这些配房仍然有效。
作废需求中的配房数据是历史快照,仅用于追溯,不能复用当前有效配房的成功态样式。
---
## 前端处理要求
1. 当详情响应表明当前查看的住宿需求已作废时,配房行程内所有快照行统一进入“作废历史”展示态。
2. 行背景使用浅红色警示背景,边框和状态标签使用红色语义;保证文字对比度,不使用高饱和纯红大面积填充。
3. 原绿色“已确认”和“已完成”标签统一替换为红色“已作废”。原确认阶段可作为次要文字展示,例如“作废前:已最终确认”,不得继续作为当前状态标签。
4. 酒店、房型、入住日期、房间数、价格和支付方式继续展示,数据来源保持后端作废快照,不读取当前酒店或房型主数据覆盖快照。
5. 作废详情必须保持全只读:隐藏或禁用配房、改单、删除、确认、最终确认等写操作。
6. 非作废需求继续沿用现有绿色确认/完成样式,不得受影响。
---
## 推荐展示层级
- 需求级:顶部保留红色“已作废”状态和只读原因。
- 配房行级:浅红背景 + 红色“已作废”标签。
- 历史阶段:灰色次要文案“作废前:未确认 / 单日确认 / 最终确认”。
- 操作区:不出现任何可写按钮。
---
## 验收场景
| 场景 | 预期 |
|------|------|
| 作废需求存在两晚已确认配房快照 | 两晚均显示浅红背景和“已作废”,不显示绿色“已完成” |
| 作废前处于单日确认 | 主状态“已作废”,次要信息可显示“作废前:单日确认” |
| 作废前已最终确认 | 主状态“已作废”,次要信息可显示“作废前:最终确认” |
| 作废快照中的房型已被资源侧修改/删除 | 仍展示作废时冻结的房型名称 |
| 打开作废详情 | 只能查看,不存在可触发写接口的按钮 |
| 打开当前有效需求 | 原绿色确认/完成样式不变 |
---
## 接口依据
- 详情:`GET /admin/house/orders/{orderId}?requirementId={voidedRequirementId}`
- 后端已返回作废需求状态、只读原因及作废时配房快照。
- 本通知不要求新增或修改后端接口。
## 不影响范围
- 选单池、我的订单筛选和抢单流程不变。
- 当前有效需求的配房、确认和最终确认流程不变。
- 小程序、H5 无需处理。
## 相关文档
- 后端 PR:[#5097](https://git.1814.love:8443/wx/HL/pulls/5097)
- 改期旧配房人工清理:`changelogs-v2/2026-07/75_5097_改期旧配房人工清理-管理后台.md`
@@ -0,0 +1,51 @@
# 房务最终确认成功后残留全屏遮罩(前端待处理)
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`(`https://git.1814.love:8443/mmg/hl-ui.git`)
- 联调/验收环境:`http://192.168.100.160:9527`
- 其他前端:小程序、H5 无需处理
本问题属于管理后台交互状态清理,不要求修改后端接口。
## 现象
房务管理员在订单详情完成最后一晚“单日确认”后点击“最终确认”,业务操作成功,列表状态也已更新为“已完成”,但详情抽屉关闭后页面仍残留覆盖整个视口的 `.n-modal-mask`。
遮罩使列表变暗并拦截鼠标操作,页面上没有可见对话框或关闭按钮;按 `Escape` 后遮罩消失,页面恢复操作。
## 复现记录
- 测试订单:`HL20260721142957939`
- 操作角色:房务管理员
- 操作步骤:选单 -> 两晚配房 -> 两晚单日确认 -> 最终确认
- 配房口径:第 1 晚扣系统库存,第 2 晚不扣系统库存
- 页面结果:最终确认成功,列表显示“已完成”和“已配 2 / 共 2 晚”,但全屏遮罩残留
- 运行观察:成功后观察 3 秒,无 console error 或 failed request
- 截图证据:`D:/work2/HL-v3/.tmp/house-final-200-browser-flow-final-transient-empty.png`
按 `Escape` 清除遮罩后重新打开该订单,详情正常显示 `2/2`、两晚酒店与库存口径,说明后端状态和配房数据均已正确落库,问题集中在前端弹层/抽屉的关闭清理。
## 前端处理要求
1. 最终确认成功后,关闭确认对话框和订单详情抽屉时同步卸载对应 teleport/modal 容器,不能遗留可见或可交互的 `.n-modal-mask`。
2. 无论最终确认请求成功、业务失败、网络异常或用户取消,都必须在结束路径中恢复页面滚动和 pointer events。
3. 不得依赖用户按 `Escape`、刷新页面或重新进入菜单恢复操作。
4. 避免用全局删除所有遮罩的方式修复;只清理本次最终确认流程拥有的弹层状态,不能影响站内信、全局搜索等其他弹层。
5. 最终确认成功后若自动关闭详情,列表状态与统计应正常刷新;若保留详情,则应直接展示正确的完成态 `2/2` 数据。
## 验收
| 场景 | 预期 |
| --- | --- |
| 正常完成最终确认 | 成功提示后无残留遮罩,列表可立即点击、筛选和滚动 |
| 最终确认业务失败 | 错误提示可关闭,原详情仍可操作,无遮罩残留 |
| 最终确认网络失败 | loading 结束,页面恢复交互,可重试 |
| 用户取消最终确认 | 对话框关闭,详情与列表交互正常 |
| 成功后重开订单 | 显示“已完成”、正确配房进度及全部配房行 |
## 接口边界
- 最终确认沿用现有房务接口,无需新增字段或修改响应结构。
- 本次浏览器复测确认成功后的列表和详情数据正确,不创建 `wx/HL` 后端 Issue。
@@ -0,0 +1,150 @@
# 【前端待处理·管理后台】车务派车看板与矩阵 SSE 实时刷新
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 仓库地址:<https://git.1814.love:8443/mmg/hl-ui.git>
- 联调/验收环境:<http://192.168.100.160:9527>
- 目标角色:当前登录角色为 `VEHICLE_MANAGER`(车务管理员)
- 小程序、司机 H5:无需处理
## 问题与后端结论
2026-07-21 复现:订单详情新增用车需求后,fleet-service 已生成未派车占位,但已打开的派车看板和矩阵派单不会实时刷新;多个车务管理员同时在线时,其他人的页面也无法感知变化。
根因是原管理后台 SSE 只接入消息、聊天、在线状态和房务抢单池信令,没有车务派单数据变更事件,也没有看板/矩阵的刷新订阅。
后端已补充以下链路:
1. fleet-service 在用车需求展开事务真正提交后发布 `REQUIREMENT_EXPANDED` 事件,避免页面刷新早于未派占位落库。
2. fleet-service 调 user-service 内部广播接口。
3. user-service 经 Redis Pub/Sub 把信令分发到所有实例。
4. 每个实例只向当前连接角色为 `VEHICLE_MANAGER` 的 SSE 连接发送 `fleet-dispatch-changed`。
5. 信令不绑定单个 `adminId`,因此多个车务管理员、多个浏览器标签和多个 user-service Pod 均可收到。
> 后端代码和定向测试已完成;测试环境是否已部署须以前后端发布记录为准。前端不得在后端未部署时把“收不到新事件”误判为页面监听实现失败。
## SSE 契约
继续复用现有管理后台 SSE 连接,不新增浏览器请求接口:
```http
GET /ws/admin-msg/stream?token=<accessToken>
Accept: text/event-stream
```
新增具名事件:
```text
event: fleet-dispatch-changed
data: {"type":"FLEET_DISPATCH","targetRoleKey":"VEHICLE_MANAGER","fleetEvent":"REQUIREMENT_EXPANDED","orderId":"2079494135466643457","requirementId":"..."}
```
字段说明:
| 字段 | 类型 | 当前值/说明 |
| --- | --- | --- |
| `type` | String | 固定 `FLEET_DISPATCH` |
| `targetRoleKey` | String | 固定 `VEHICLE_MANAGER` |
| `fleetEvent` | String | 当前为 `REQUIREMENT_EXPANDED` |
| `orderId` | String/Long | 变更涉及的订单 ID;仅作定位提示,按字符串处理 |
| `requirementId` | String/Long | 变更涉及的用车需求 ID;仅作定位提示,按字符串处理 |
- 前端不得对雪花 ID 使用 `Number()`;如需比较,统一 `String(value)` 后比较。
- 本事件是“数据已变化”的轻量信令,不携带看板或矩阵业务正文。
- 收到事件后必须重拉现有权威查询接口,不能根据信令自行拼装订单、派车组或矩阵占用条。
- 后端只向当前角色为 `VEHICLE_MANAGER` 的连接投递。`SUPER_ADMIN` 只有切换并以车务管理员当前角色重新建立 SSE 后才会收到。
## 前端接入要求
### 1. 全局 SSE 接收
在 `src/composables/useAdminMessageSSE.js` 增加具名事件监听:
```js
es.addEventListener('fleet-dispatch-changed', handleFleetDispatchSignal)
```
解析 JSON 后转入独立的车务派单信令总线。不要复用聊天信令或房务 `lastGrabPoolSignal`,避免模块语义互相污染。
建议在现有总线文件中新增:
```js
export const lastFleetDispatchSignal = ref(null)
export function pushFleetDispatchSignal(signal) {
if (!signal) return
lastFleetDispatchSignal.value = { ...signal, _seq: Date.now() }
}
```
连续事件必须保证每次都能触发订阅;实现可沿用现有 `_gseq` 自增模式,不强制使用 `Date.now()`。
### 2. 派车看板刷新
目标页面:`src/views/fleet/board/index.vue`
- 订阅 `lastFleetDispatchSignal`。
- 页面处于挂载状态并收到 `REQUIREMENT_EXPANDED` 后调用现有 `fetchBoard()`。
- 保留当前筛选条件、分页/视图模式和搜索输入,不得重置用户工作区。
- 多条短时间信令可做 100~300ms 合并刷新,避免重复并发请求。
- 沿用现有请求序号/取消机制,迟到响应不得覆盖较新的看板数据。
### 3. 矩阵派单刷新
目标页面:
- `src/views/fleet/matrix/index.vue`
- `src/views/fleet/matrix/solo/index.vue`(如独立挂载数据上下文)
- `src/views/fleet/matrix/composables/useFleetMatrixData.js`
处理要求:
- 收到信令后调用现有 `fetchMatrix(requestFilters.value)` 或等价的当前筛选刷新入口。
- 同步刷新未派订单池、顶部统计和矩阵占用数据;不能只刷新车辆行而保留旧未派数量。
- 保留当前年月、车队、车型和其他筛选条件。
- 若派单弹窗正在提交,不得关闭弹窗或清空用户输入;提交结束后以最后一次权威查询结果收敛页面。
- 矩阵分窗复用主页面数据组件时只订阅一次,避免同一事件发起重复请求。
### 4. 断线重连对账
Redis Pub/Sub 和 SSE 均不提供历史事件重放。断线期间可能漏过 `fleet-dispatch-changed`,因此:
- SSE 重新收到 `connected` 后,若派车看板或矩阵当前已打开,应主动重拉一次当前页面数据。
- 不要仅依赖实时事件维持页面正确性。
- 仍按现有 SSE 生命周期要求保证全局只有一条连接,不得为看板和矩阵各自新建 `EventSource`。
## 不影响范围
- 不修改派车看板、矩阵派单现有查询接口及响应结构。
- 不修改现有 `message`、`unread-count`、`im-chat`、`im-chat-read`、`presence`、`grab-pool-changed` 事件。
- 不要求前端调用 `/internal/sse/fleet-dispatch/broadcast`;该路径仅供服务间 Feign 使用,管理后台不得直接访问。
- 本次只覆盖用车需求展开后实时刷新。后续其他派单动作如扩展新的 `fleetEvent`,将另行补充契约。
## 验收清单
- [ ] 以车务管理员 A 打开 `/fleet/board`,车务管理员 B 或定制师新增用车需求后,A 的看板无需手动刷新即可出现新未派订单。
- [ ] 以车务管理员 A 打开 `/fleet/matrix`,新增用车需求后未派订单池、顶部统计和矩阵数据自动更新。
- [ ] 两个不同车务管理员同时在线并分别打开看板/矩阵,两边均收到同一变更并刷新。
- [ ] 同一车务管理员两个浏览器标签同时在线,两个标签均能刷新且互不关闭 SSE。
- [ ] 当前角色为 `SUPER_ADMIN` 且未切换车务角色时不接收本事件。
- [ ] `SUPER_ADMIN` 切换为 `VEHICLE_MANAGER` 并重建 SSE 后可以接收。
- [ ] 收到信令时保留看板和矩阵当前筛选条件,不跳回默认月份或清空搜索项。
- [ ] 短时间连续提交用车需求不会产生请求风暴或旧响应覆盖新数据。
- [ ] SSE 断线期间新增需求,连接恢复并收到 `connected` 后页面主动对账并显示最新数据。
- [ ] Network 中只存在一条 `/ws/admin-msg/stream` 长连接,没有为车务页面新增独立 SSE。
## 后端验证记录
- fleet-service:`AssignmentServiceTest` 239 项通过。
- fleet-service:车务派单 AFTER_COMMIT 通知测试 2 项通过。
- user-service:`AdminSseServiceTest` 37 项通过,覆盖两个车务同时接收、非车务角色隔离。
- user-service:车务广播与内部接口测试 3 项通过。
- `hl-fleet-service`、`hl-user-service` 模块级 `mvn -DskipTests package` 均通过。
## 发布说明
- 本文是前端接入与联调通知,不代表已修改或发布 `mmg/hl-ui`。
- 前端完成后应在 `mmg/hl-ui` 走自身 Issue、分支、PR、测试和发布流程。
- 联调材料不得包含 access token、带 token 的完整 SSE URL、Cookie 或真实管理员身份信息。
@@ -0,0 +1,270 @@
---
schema: "hl-changelog/v1"
ticket: "5132"
title: "车务派单详情补全产品、行程节点、出行人与大交通"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-22T10:35:00+08:00"
---
# 【修改接口·前端待处理·管理后台】车务派单详情补全产品、行程节点与出行人
> **服务**: hl-order-service-v3 + hl-fleet-service
> **日期**: 2026-07-22
> **工单**: #5132
> **影响范围**: 管理后台车务管理 / 派车看板 / 派单弹窗 Step1「订单详情」
---
## 关键变化
派单弹窗 Step1 不能再只展示人数、日期和每日一句简介。`GET /admin/fleet/board/orders/{orderId}` 现一次返回:
- `productName`:订单产品名(原字段,前端本次必须展示)。
- `tags[]`:订单在 `order_tag` 中真实挂载的标签名称与颜色;无标签返回 `[]`。
- `itinerary.days[].nodes[]`:每日真实行程节点,含开始时间、时段、时长、名称和简介。
- `travelers[]`:出行人脱敏基本信息,不含生日和任何明文字段。
- `transport`:抵达、返程及分批大交通信息(原字段,前端本次必须完整展示时间和班次,不能只显示站点)。
行程数据仍以订单当前 `order_itinerary_day` 和 `order_itinerary_node` 为权威源,禁止从产品模板反推。
---
## 变更接口
```http
GET /admin/fleet/board/orders/{orderId}
```
响应 VO:`BoardOrderDetailVO`
### 新增字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `tags` | Array | 真实订单标签;无标签返回 `[]` |
| `tags[].tagId` | String | 标签雪花 ID,必须按字符串处理 |
| `tags[].name` | String/null | 标签名称 |
| `tags[].color` | String/null | 标签颜色,如 `#52C41A` |
| `itinerary.days[].nodes` | Array | 当日节点,按 `sortOrder` 升序;无节点返回 `[]` |
| `itinerary.days[].nodes[].nodeId` | String | 节点雪花 ID,必须按字符串处理 |
| `itinerary.days[].nodes[].nodeType` | String/null | 节点类型,如 `SCENIC`、`RESTAURANT`、`ACTIVITY`、`SERVICE`、`CUSTOM` |
| `itinerary.days[].nodes[].nodeName` | String/null | 节点展示名;节点名为空时后端回退资源名 |
| `itinerary.days[].nodes[].startTime` | String/null | 开始时间,格式 `HH:mm` |
| `itinerary.days[].nodes[].timePeriod` | String/null | 时段,如上午、下午、全天 |
| `itinerary.days[].nodes[].durationMinutes` | Number/null | 时长,单位分钟 |
| `itinerary.days[].nodes[].description` | String/null | 节点简介 |
| `itinerary.days[].nodes[].sortOrder` | Number/null | 同天排序 |
| `travelers` | Array | 出行人脱敏基本信息;无出行人或下游降级时返回 `[]` |
| `travelers[].travelerId` | String | 出行人雪花 ID,必须按字符串处理 |
| `travelers[].travelerType` | String/null | `ADULT` / `CHILD` / `YOUNG_CHILD` / `BABY` |
| `travelers[].travelerTypeName` | String/null | 人员类型中文名,如“成人”“儿童” |
| `travelers[].nameMasked` | String/null | 脱敏姓名 |
| `travelers[].gender` | String/null | 性别字典值 |
| `travelers[].genderName` | String/null | 性别中文名 |
| `travelers[].ageAtDeparture` | Number/null | 按订单出发日计算的周岁 |
| `travelers[].idType` | String/null | 证件类型字典值 |
| `travelers[].idTypeName` | String/null | 证件类型中文名 |
| `travelers[].idNoMasked` | String/null | 脱敏证件号 |
| `travelers[].phoneMasked` | String/null | 脱敏手机号 |
| `travelers[].nationality` | String/null | 国籍 |
| `travelers[].race` | String/null | 民族 |
| `travelers[].emergencyContactMasked` | String/null | 脱敏紧急联系人姓名 |
| `travelers[].emergencyPhoneMasked` | String/null | 脱敏紧急联系人电话 |
| `travelers[].roomGroupNo` | Number/null | 同住分组号 |
| `travelers[].transportPlanIds` | String[] | 关联大交通批次 ID,必须按字符串处理 |
| `travelers[].profileStatus` | String/null | 资料状态:`PENDING` / `COMPLETED` |
| `travelers[].profileStatusName` | String/null | 资料状态中文名 |
`productName` 是已有字段,结构不变;本次页面必须消费,不再只保存在 `normalizeBoardOrder().product` 而不展示。
### 已有但本次必须完整展示的大交通字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `transport.arrive` | Object/null | 抵达接团段 |
| `transport.arrive.transportNo` | String/null | 抵达航班号/车次号 |
| `transport.arrive.time` | String/null | 抵达时间,ISO `LocalDateTime` |
| `transport.arrive.station` | String/null | 抵达机场/车站 |
| `transport.arrive.remark` | String/null | 抵达备注 |
| `transport.depart` | Object/null | 返程送站段 |
| `transport.depart.transportNo` | String/null | 返程航班号/车次号 |
| `transport.depart.time` | String/null | 返程时间,ISO `LocalDateTime` |
| `transport.depart.station` | String/null | 返程机场/车站 |
| `transport.depart.remark` | String/null | 返程备注 |
| `transport.batches` | Array | 分批接送列表 |
| `transport.batches[].travelerNames` | String/null | 本批出行人姓名摘要 |
| `transport.batches[].transportNo` | String/null | 本批航班号/车次号 |
| `transport.batches[].time` | String/null | 本批抵达/返程时间 |
| `transport.batches[].station` | String/null | 本批机场/车站 |
| `transport.transferTimeHint` | String/null | 无任何大交通时间时的后端提示,当前为“暂无接送机时间” |
| `transport.pickupRequired` | Boolean/null | 是否需要平台派车接送 |
### 响应示例
```json
{
"code": 200,
"data": {
"orderNo": "HL20260721171011648",
"productName": "草原亲子三日游",
"tags": [
{
"tagId": "9001",
"name": "亲子家庭",
"color": "#52C41A"
}
],
"itinerary": {
"theme": "草原亲子三日游",
"route": null,
"days": [
{
"dayNumber": 1,
"date": "2026-07-29",
"title": "接机",
"detail": "抵达后入住酒店",
"nodes": [
{
"nodeId": "2001",
"nodeType": "SERVICE",
"nodeName": "海拉尔机场接机",
"startTime": "10:30",
"timePeriod": "上午",
"durationMinutes": 60,
"description": "司机举牌接机",
"sortOrder": 1
}
]
}
]
},
"travelers": [
{
"travelerId": "3001",
"travelerType": "ADULT",
"travelerTypeName": "成人",
"nameMasked": "孔**",
"gender": "2",
"genderName": "女",
"ageAtDeparture": 35,
"idType": "ID_CARD",
"idTypeName": "身份证",
"idNoMasked": "150***********1234",
"phoneMasked": "138****1234",
"nationality": "中国",
"race": "蒙古族",
"emergencyContactMasked": "王*",
"emergencyPhoneMasked": "139****5678",
"roomGroupNo": 1,
"transportPlanIds": ["4001"],
"profileStatus": "COMPLETED",
"profileStatusName": "已完善"
}
],
"transport": {
"transferTimeHint": null,
"arrive": {
"transportNo": "CA1234",
"time": "2026-07-29T10:30:00",
"station": "海拉尔东山国际机场",
"remark": "T2 出口举牌接机"
},
"depart": {
"transportNo": "CA5678",
"time": "2026-07-31T17:20:00",
"station": "海拉尔东山国际机场",
"remark": "提前 2 小时送达"
},
"batches": [],
"pickupRequired": true
}
},
"success": true
}
```
---
## 前端页面调整要求
目标文件:`src/views/fleet/board/components/Step1OrderDetail.vue`。
1. 顶部订单摘要展示产品名,读取 `order.productName || order.product`;产品名为空才显示 `—`。
- 当前 `Step1OrderDetail.vue` 中的 `order.bookingType || '企业包车'` 是硬编码占位,不是订单标签,必须删除。
- 该位置改为遍历详情响应 `tags[]`,使用 `name` 作为文案、`color` 作为颜色;`tags=[]` 时不显示标签,也不回退“企业包车”。
2. 在顶部订单摘要下增加“大交通”信息卡,抵达与返程分栏展示 `transportNo + time + station + remark`:
- 时间使用完整月日和时分,不只展示日期。
- `arrive`、`depart` 独立判空,只有一段时仍正常展示该段。
- `batches[]` 非空时增加“分批接送”,展示本批出行人、班次、时间和站点。
- 无任何时间时展示 `transport.transferTimeHint`,不得伪造航班或时间。
- 当前顶部“接送”统计可保留站点摘要,但不能替代大交通详情卡。
3. 左侧“每日安排”保留日标题和 `detail`,并在每一天下面渲染 `nodes[]`:
- 时间优先显示 `startTime`,为空时显示 `timePeriod`,两者都有可组合展示。
- `startTime` 和 `timePeriod` 都为空时显示“时间待定”,不得根据节点顺序或描述猜测具体时间。
- 主文案显示 `nodeName`。
- `durationMinutes` 有值时显示易读时长。
- `description` 有值且与日简介不重复时显示节点简介。
4. 右侧新增“出行人信息”区,默认展示脱敏姓名、人员类型、性别、年龄、国籍/民族、脱敏手机号和资料状态;证件、同住分组、关联大交通批次及紧急联系人可在行内展开或次要信息区展示。
- 年龄文案使用自然表达“年龄 29 岁”,不要显示成“出发时 29岁”。
- `ageAtDeparture` 的业务口径仍是按订单出发日计算;如需说明,将“按出发日计算”放在字段提示或帮助文案中,不与年龄值拼成标签。
5. 禁止为了展示此页面调用明文接口 `POST /admin/fleet/board/orders/{orderId}/travelers/plain`。Step1 只使用详情响应中的脱敏 `travelers[]`。
6. 空态明确:无节点显示“暂无行程节点”,无出行人显示“暂未填写出行人信息”;不得生成模拟节点或模拟出行人。
7. 雪花 ID 禁止 `Number()` / `parseInt()`,统一按字符串处理。
8. 用车备注与通用特殊诉求必须按字段来源分区展示,不能混在同一个“特殊要求”警示框:
- `requirementRemark` 来源于定制师在“调整订单 → 车辆安排 → 备注/其他诉求”填写的自由文本,应显示在“定制师备注”或更准确的“用车备注”卡片中;例如“司机会蒙语”。
- `specialTags[]` 来源于“通用特殊诉求”的多选标签,只在“特殊要求”区域展示标签;例如“儿童安全座椅”“大行李空间”“中文司机”。
- `plannerNote` 是订单级定制师备注,与 `requirementRemark` 不是同一字段;两者同时存在时分行展示并标明来源,不得互相覆盖。
- `requirements` 仅作为历史订单兼容文本;当 `requirementRemark` 或 `specialTags[]` 已有值时,不得把它们重复拼入 `requirements`。
推荐布局:顶部摘要下放横向“大交通”卡;左栏继续承载逐日节点时间线;右栏顺序为“出行人信息 → 客人留言 → 定制师/用车备注 → 特殊要求 → 操作记录”。
---
## 兼容与降级
- 仅新增响应字段,不修改请求参数,不影响旧调用方。
- 历史订单无订单标签时 `tags=[]`,禁止使用产品类型、预订类型或固定文案冒充订单标签。
- 历史行程没有节点时 `nodes=[]`,每日标题和简介仍照常返回。
- order-v3 聚合上下文失败并回退 fleet 本地快照时,`relatedDetailReady=false`,`travelers=[]`,行程节点不可用;前端显示真实空态。
- 原独立脱敏接口 `GET /admin/fleet/board/orders/{orderId}/travelers` 保留兼容,但此页面无需再发第二次请求。
- 不返回 `birthday`、明文姓名、明文证件号、明文手机号或明文紧急联系人。
---
## 验收清单
- [ ] 顶部可看到订单产品名。
- [ ] 顶部只展示 `tags[]` 中的真实订单标签;无标签时不显示,“企业包车”硬编码已删除。
- [ ] 大交通卡分别展示抵达/返程的班次、完整时间、站点和备注。
- [ ] 有分批接送时展示每批出行人、班次、时间和站点;无大交通时间时展示真实空态。
- [ ] 每日安排按节点顺序展示时间、节点名、时长和简介。
- [ ] 节点无 `startTime` 时可回退显示 `timePeriod`,不会出现 `undefined`。
- [ ] 右侧可看到全部出行人的脱敏基本信息。
- [ ] 每位出行人以“年龄 N 岁”的自然文案展示年龄,并可查看人员类型、性别、证件、国籍/民族、同住分组、关联大交通及资料状态。
- [ ] 无节点/无出行人时展示真实空态,不生成模拟数据。
- [ ] 页面 Network 只需现有详情请求,不调用出行人明文接口。
- [ ] “备注/其他诉求”读取 `requirementRemark` 并显示在定制师/用车备注卡片;“特殊要求”只展示 `specialTags[]`,不会再把备注文本放入警示框。
- [ ] `plannerNote` 与 `requirementRemark` 同时存在时分别展示且不覆盖,历史 `requirements` 不造成重复文案。
- [ ] 现有留言、特殊要求、步骤条和操作记录不受影响。
---
## 验证证据
- `ItineraryServiceTest`:覆盖节点名称、开始时间、时段、时长、简介和排序装配。
- `OrderFleetProviderServiceTest`:覆盖节点随当前订单日期对齐且出行人脱敏进入聚合上下文。
- `BoardOrderServiceTest`:覆盖 shared DTO 到管理端 VO 的节点和出行人映射。
- `BoardControllerTest`:覆盖 `productName`、节点时间、String ID 与脱敏出行人的 JSON 契约。
- `OrderFleetProviderServiceTest`、`BoardOrderServiceTest` 与 `BoardControllerTest`:覆盖 `order_tag` 名称/颜色进入详情响应,标签 ID 按字符串序列化。
- 测试环境网关实测订单 `HL20260721171011648`:HTTP 200,返回 3 个行程日、14 个真实节点和 5 位出行人;5 位出行人均返回 `ageAtDeparture`,且未出现生日、明文姓名、明文证件号或明文手机号。
- 同一实测订单返回 2 个真实订单标签“自动化测试”“房务需求”,均包含颜色,`tagId` 均为字符串;响应不含 `bookingType`,前端无需也不得使用“企业包车”等硬编码兜底。
- 同一实测订单已返回抵达大交通的班次、抵达时间和站点;该订单无返程段、无分批接送,接口按真实数据返回空值或空数组。
- 该订单 14 个节点的 `startTime` 与 `timePeriod` 在订单行程源数据中均为空,接口如实返回 `null`;前端须展示“时间待定”,若要显示具体钟点需先补录订单行程节点时间。
- 测试环境团号 `26-7042` 的调整单中,“备注/其他诉求”为“司机会蒙语”;详情接口已分别返回 `requirementRemark` 和 `specialTags[]`。截至 `v2.1@6e6a11bf`,`Step1OrderDetail.vue` 仍使用 `requirementRemark || requirements` 渲染“特殊要求”警示框,并仅用 `plannerNote` 渲染定制师留言,造成截图中的字段串位;按上方规则调整前端展示即可,后端无需新增字段。
- 网关证据已由 `hl task` 登记,SHA-256:`8ff09cc804fb8d72fde6df3f338125b725c857d2c098b59f6b222f460da844a3`。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,183 @@
---
schema: "hl-changelog/v1"
ticket: "5139"
title: "车务派单候选筛选、分页与任意车辆选择"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-22T13:25:00+08:00"
---
# 【修改接口·前端待处理·管理后台】车务派单候选筛选、分页与任意车辆选择
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 仓库地址:<https://git.1814.love:8443/mmg/hl-ui.git>
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-fleet-service
> **日期**: 2026-07-22
> **工单**: #5139
> **影响范围**: 订单派车弹窗的车辆候选、司机候选与最终派单校验
## 关键变化
`POST /admin/fleet/assignments/candidates` 继续同时返回车辆和司机,但两侧必须按各自分页参数渲染。后端新增动态车队/车型筛选、车型需求匹配、协议参考价、车辆常驻司机、司机历史统计,以及“先选司机时回显常驻车”的契约。
车型或座位不符合订单需求时,车辆仍允许选择;只有真实档期冲突或资源不可用才禁止。车辆选中态不是强制单选,前端再次点击已选车辆时可把 `selectedVehicleId` 清为 `null` 后重新查询。
## 变更接口
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/admin/fleet/assignments/candidates` | 车辆、司机独立筛选和分页;任一侧可先选 |
| POST | `/admin/fleet/assignments/precheck` | 车型/座位不匹配只返回 warning |
| POST | `/admin/fleet/assignments` | `strictSeats` 历史字段不再阻断任意车辆派单 |
## 候选查询入参
在原请求基础上新增或明确以下字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `selectedVehicleId` | String/null | 否 | 当前已选车辆;传 `null` 表示取消车辆选择 |
| `selectedDriverId` | String/null | 否 | 当前已选司机;可在未选车辆时先传 |
| `fleetTeamId` | String/null | 否 | 独立车队主数据 ID;空为全部 |
| `vehicleTypeId` | String/null | 否 | 车型大类 ID;空为全部 |
| `requiredVehicleType` | String/null | 否 | 订单需求车型大类 key,只影响匹配标记,不限制选择 |
| `vehiclePage` / `vehiclePageSize` | Integer | 是 | 车辆独立分页,页大小 1~100 |
| `driverPage` / `driverPageSize` | Integer | 是 | 司机独立分页,页大小 1~100 |
| `driverAvailability` | String | 否 | `ALL` / `AVAILABLE`,接口默认 `ALL`;管理后台按原型首屏显式传 `AVAILABLE` |
| `driverSort` | String | 否 | `SMART` / `RATING` / `YEARS` / `RECENT_ORDER`,默认 `SMART` |
雪花 ID 一律按字符串保存和提交,禁止 `Number()`、`parseInt()`。
取消车辆但保留司机的请求示例:
```json
{
"orderId": "2080000000000000001",
"requirementId": "2080000000000000101",
"fleetItemIndex": 0,
"startDate": "2026-07-29",
"endDate": "2026-07-31",
"headcount": 5,
"requiredVehicleType": "suv",
"selectedVehicleId": null,
"selectedDriverId": "2080000000000000201",
"vehiclePage": 1,
"vehiclePageSize": 10,
"driverPage": 1,
"driverPageSize": 10
}
```
## 响应结构
### 独立分页
`data.vehicles` 和 `data.drivers` 均返回:
```json
{
"records": [],
"list": [],
"total": 106,
"page": 1,
"pageSize": 10
}
```
`records` 与 `list` 内容相同,前端统一使用 `records`。切换车辆筛选只重置 `vehiclePage`,切换司机筛选只重置 `driverPage`,不要一次性把所有候选渲染成长列表。
### 动态筛选项
- `fleetTeamFacets[]`: `fleetTeamId/fleetTeamName/fleetType/count`。
- `vehicleTypeFacets[]`: `vehicleTypeId/vehicleTypeKey/vehicleTypeName/count`。
- 数量按当前车辆关键词统计;“全部”数量可按 facet 求和或使用 `vehicles.total`。
- 不再写死“自有车队/合作车队 A/合作车队 B”或固定车型数组。
### 车辆候选新增字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `vehicleTypeId` | String/null | 车型大类 ID |
| `vehicleTypeKey` / `vehicleTypeName` | String/null | 车型大类编码和名称 |
| `fleetTeamId/fleetTeamName/fleetType` | String/null | 动态车队信息 |
| `primaryDriverId/primaryDriverName` | String/null | 常驻司机;为空显示“无常驻” |
| `protocolPrice` | Decimal/null | 用车开始日价格日历协议参考价;为空显示“未设价” |
| `passengerCapacity` | Integer | 载客数,已扣除司机座 |
| `seatsEnough` | Boolean | 座位是否满足人数,仅用于提示 |
| `requirementMatched` | Boolean | 车型和座位是否均符合需求;`false` 只做醒目标记 |
| `available` | Boolean | 是否可选的权威值;真实档期冲突时为 `false` |
| `selected` | Boolean | 是否为当前已选车辆 |
前端禁用判断只使用 `available === false`。禁止用 `requirementMatched === false`、`seatsEnough === false` 或车型不一致禁用车辆;这些情况应显示“需求不匹配/座位不足”提示,但允许车务选中。
需求不匹配必须使用车辆卡片内的显式标签,不能再以黄色外框作为主要提示:
- `requirementMatched === false`:在车辆名称/状态附近显示橙色 `需求不匹配` 标签。
- `seatsEnough === false`:额外显示红色或橙红色 `座位不足` 标签。
- 移除需求不匹配专用黄色外框;边框只保留选中态、档期冲突等已有交互语义,避免颜色含义不明。
- 标签只负责提醒,不改变 `available`、点击选择或最终派单规则。
### 先选司机与取消车辆
- 仅传 `selectedDriverId` 时,`selectedDriverResidentVehicle` 返回该司机常驻车的完整车辆候选;司机无常驻车时为 `null`。
- 常驻车即使不在当前车队、车型筛选页内,也会通过该独立字段返回,前端可置顶或单独提示。
- 再次点击已选车辆时,前端清空本地车辆 ID,并以 `selectedVehicleId: null` 查询;保留 `selectedDriverId` 时常驻车提示仍存在。
- 同时选定跨常驻车组合时,沿用 `selectedRelation.requiresConfirmation` 和候选项 `requiresCrossResidentConfirmation` 的确认流程。
### 司机候选统计
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `completedOrderCount` | Integer | 司机跨赛季历史完单量,按派车组去重 |
| `lastOrderAt` | Date/null | 最近完单日期 |
| `rating` | Decimal/null | 真实平均评分;无评价时为 `null` |
| `hasRating` | Boolean | 是否存在真实评分 |
| `residentVehicleId/residentVehiclePlate` | String/null | 司机常驻车辆 |
`hasRating=false` 时显示“暂无评价”,不要展示星标和 `0.0/5.0`;不得再用固定 `5.0` 兜底。单量为司机历史累计,不按赛季清零。
### 原型一致性:司机筛选控件
司机筛选必须按原型平铺展示,不能用两个下拉框折叠选项。平铺按钮让车务一眼看到当前范围和全部排序方式,并可单击切换:
- 范围:`仅空闲`(`AVAILABLE`,首屏默认选中)、`全部`(`ALL`)。
- 排序:`智能推荐`(`SMART`,首屏默认选中)、`评分`(`RATING`)、`驾龄`(`YEARS`)、`最近接单`(`RECENT_ORDER`)。
- 切换范围或排序时只把 `driverPage` 重置为 1,不重置车辆筛选、车辆页码或已选车辆。
- “全部司机/智能排序”两个 `NSelect` 不视为原型等价实现;验收以按钮全部可见、选中态明确为准。
## 最终派单规则
- 车型或座位不匹配:候选项仍可选,预检返回 warning,最终派单不阻断。
- 档期冲突、车辆/司机不可用、黑名单或跨常驻未确认:仍按现有业务守卫阻断。
- `strictSeats` 为历史兼容字段,可不再提交;即使提交 `true` 也不会把座位不足变成阻断。
## 前端处理清单
- [ ] 车辆和司机列表分别接 `records/total/page/pageSize` 并增加独立分页控件。
- [ ] 车队和车型筛选使用 `fleetTeamFacets/vehicleTypeFacets` 动态渲染及计数。
- [ ] 车辆行展示车型、常驻司机、协议参考价;需求不匹配改用卡片内显式标签并移除黄色外框,座位不足追加独立标签,均不禁选。
- [ ] 支持再次点击已选车辆取消选择,并传 `selectedVehicleId: null`。
- [ ] 支持先选司机,并展示/置顶 `selectedDriverResidentVehicle`。
- [ ] 司机范围与排序按原型平铺为 2+4 个按钮,默认“仅空闲 + 智能推荐”,不得折叠成两个下拉框。
- [ ] 司机无评价显示“暂无评价”,不伪造 `5.0` 或 `0.0`;完成单量读取 `completedOrderCount`。
- [ ] 雪花 ID 全程按字符串处理。
## 验证证据
- PR [wx/HL#5140](https://git.1814.love:8443/wx/HL/pulls/5140) 已合并到 `dev-v3`。
- 派单候选、派单服务、司机统计、可靠投影、价格日历和迁移审计定向测试全部通过。
- `spotless:check` 与 `mvn -pl hl-fleet-service -am verify` 通过。
- 测试环境 `hl-fleet-service` 8087/8187 双实例滚动部署健康。
- 测试网关实测 HTTP/业务码 200:车辆和司机独立分页一致,返回 3 个动态车队、4 个车型大类;协议价非空,车型不匹配车辆仍可选;车辆可清空,先选司机可返回常驻车。
- 测试库只读核验:`V20260722.002` 已成功执行,候选 `completedOrderCount/lastOrderAt` 与 `fleet_driver` 投影一致,无评分司机返回 `rating=null`。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,114 @@
---
schema: "hl-changelog/v1"
ticket: "5141"
title: "车务派单司机保险类型与行程保障状态"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-22T14:07:00+08:00"
---
# 【修改接口·前端待处理·管理后台】车务派单司机保险类型与行程保障状态
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 仓库地址:<https://git.1814.love:8443/mmg/hl-ui.git>
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
## 变更接口
`POST /admin/fleet/assignments/candidates` 的 `data.drivers.records[]` 新增司机保险字段。数据直接来自司机档案,并按本次请求的 `startDate/endDate` 判断全年保险是否完整覆盖行程。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `insuranceType` | String | `annual` 全年保险、`perTrip` 按行程投保、`none` 无保险 |
| `insuranceTypeLabel` | String | `全年保险`、`按行程投保`、`无保险` |
| `insuranceAnnualStart` | Date/null | 全年保险起始日;非 `annual` 为空 |
| `insuranceAnnualEnd` | Date/null | 全年保险到期日;非 `annual` 为空 |
| `insuranceCoverageStatus` | String | 本次行程保障状态,枚举见下表 |
| `insuranceCoverageMessage` | String | 后端生成的中文提示,可直接展示 |
| `insuranceCovered` | Boolean | 仅全年保险完整覆盖本次服务日期时为 `true` |
## 保障状态
| `insuranceCoverageStatus` | `insuranceCoverageMessage` | 含义 |
| --- | --- | --- |
| `ANNUAL_COVERED` | 全年保险已覆盖 | 年保起止日完整覆盖本次行程 |
| `ANNUAL_NOT_COVERED` | 全年保险不覆盖本行程 | 年保缺日期、未生效、已过期或仅覆盖部分行程 |
| `PER_TRIP_REQUIRED` | 待按行程投保 | 司机配置为按行程投保,候选阶段尚不代表已经出单 |
| `UNINSURED` | 无保险 | 司机档案明确为无保险 |
| `UNKNOWN` | 保险状态未知 | 存量异常值兜底,不能当作已保障 |
## 前端展示规则
- 在司机卡片姓名或驾龄附近展示保险徽标,文案优先使用 `insuranceCoverageMessage`。
- `ANNUAL_COVERED` 可用绿色;`PER_TRIP_REQUIRED` 用橙色;`ANNUAL_NOT_COVERED/UNINSURED/UNKNOWN` 用红色或醒目警示色。
- 全年保险可在悬浮提示或次级文案展示 `insuranceAnnualStart ~ insuranceAnnualEnd`。
- 保险状态只用于车务判断和提示,不影响司机候选的 `available`,不得因为未投保或待按行程投保禁用司机。
- 不要只根据 `insuranceType=annual` 显示“已保障”,必须以 `insuranceCoverageStatus` 或 `insuranceCovered` 为准。
## 前端处理清单
- [ ] 司机候选卡片展示保险保障徽标。
- [ ] 区分全年已覆盖、全年未覆盖、待按行程投保、无保险及未知状态。
- [ ] 年保可查看保障起止日,且不把过期或部分覆盖年保展示为已保障。
- [ ] 保险状态不改变司机可选性,候选禁用仍只依据 `available === false`。
## 编辑司机:无保单时直接线上投保
司机编辑抽屉选择“全年保险”后,如果“关联保游网保单”没有可选数据,不应只展示空下拉。需要在当前抽屉提供“立即投保”入口,复用保险订单页“投保下单 → 司机”的线上真实投保逻辑。
目标文件:
- `src/views/fleet/drivers/components/DriverEditModal.vue`
- 可复用 `src/views/insurance/orders/index.vue` 中的司机投保表单和 `src/api/fleet/drivers.js` 的 `purchaseDriverInsurance`。
交互要求:
1. 无可关联保单时显示“暂无可关联保单”,并提供“立即投保”按钮。
2. 点击后填写保险计划、保障开始、保障结束和可选备注;表单行为与保险订单页的司机投保一致。
3. 用户点击“确认投保”后才发起真实线上投保;仅切换到“全年保险”不得自动出单。
4. 投保请求必须传 `bindAnnual: true`。受理成功后,后端会自动把新保单绑定为司机档案的全年保险。
5. 成功后重新加载司机保单列表和司机详情,回显新 `insuranceOrderId`、保单状态及保障起止;`INSURING` 时显示“出单中”,不能要求用户重复投保。
6. 保留“手工录入线下保单”作为独立兜底路径,文案和操作不得与线上投保混用。
调用示例:
```http
POST /admin/fleet/drivers/{driverId}/insurance/purchase
```
```json
{
"planId": "2080000000000000001",
"coverageStartDate": "2026-07-23",
"coverageEndDate": "2027-07-22",
"bindAnnual": true,
"remark": "司机全年保险"
}
```
`driverId`、`planId` 和响应中的 `insuranceOrderId` 均为雪花 ID,前端必须按字符串透传。保险计划继续使用 `GET /admin/fleet/drivers/insurance/plan-options`。
异常处理沿用保险订单页:全局展示后端错误文案;若返回 `600206`,表示可能已经出单但档案绑定失败,必须关闭投保弹窗并刷新保单列表,提示用户勿重复投保。
追加验收项:
- [ ] 编辑司机选择全年保险且无已有保单时,可在当前抽屉发起线上真实投保。
- [ ] 请求携带 `bindAnnual: true`,投保受理后司机档案自动回显全年保险,无需先保存再关联。
- [ ] 出单中、已承保和 `600206` 场景均不会诱导用户重复投保。
- [ ] 线上投保与手工录入线下保单入口、文案和数据来源清晰分离。
## 验证证据
- 后端 PR [wx/HL#5143](https://git.1814.love:8443/wx/HL/pulls/5143) 已合并到 `dev-v3`。
- 派单候选与司机域定向测试共 148 项通过。
- fleet `spotless:check` 与 `mvn -pl hl-fleet-service -am verify` 通过。
- 测试网关真实返回 21 名司机,覆盖全年已覆盖、全年未覆盖、待按行程投保和无保险四类结果;响应与测试库司机保险档案逐条一致,19 名警示状态司机仍可选择。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,161 @@
---
schema: "hl-changelog/v1"
ticket: "5145"
title: "选车后常驻司机默认配对"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-22T15:30:00+08:00"
---
# 【修改接口·前端待处理·管理后台】选车后常驻司机默认配对
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 仓库地址:<https://git.1814.love:8443/mmg/hl-ui.git>
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-fleet-service
>
> **后端 PR**: [wx/HL#5148](https://git.1814.love:8443/wx/HL/pulls/5148)
>
> **工单**: [wx/HL#5145](https://git.1814.love:8443/wx/HL/issues/5145)
>
> **日期**: 2026-07-22
>
> **影响范围**: 管理后台订单派车弹窗的车辆/司机联动选择
## 关键变化
`POST /admin/fleet/assignments/candidates` 的响应新增 `data.selectedVehicleResidentDriver`。前端选中车辆后,可直接取得该车常驻司机的完整候选快照并按档期决定是否自动选中,不再依赖当前司机页中能否找到该司机。
这个独立快照不受司机关键词、司机分页、`driverAvailability=AVAILABLE` 或排序条件影响;没有有效常驻司机时为 `null`。
## 变更接口
| 方法 | 路径 | 变更类型 | 说明 |
| --- | --- | --- | --- |
| POST | `/admin/fleet/assignments/candidates` | 响应新增字段 | 返回已选车辆的常驻司机候选快照 |
请求时继续传当前选中车辆:
```json
{
"orderId": "2080000000000000001",
"requirementId": "2080000000000000101",
"fleetItemIndex": 0,
"startDate": "2026-07-29",
"endDate": "2026-07-31",
"selectedVehicleId": "2080000000000000201",
"selectedDriverId": null,
"driverKeyword": "不会命中常驻司机的关键词",
"driverAvailability": "AVAILABLE",
"driverPage": 3,
"driverPageSize": 10
}
```
响应新增字段示例:
```json
{
"data": {
"selectedVehicleResidentDriver": {
"driverId": "2080000000000000301",
"name": "常驻司机",
"maskedPhone": "135****5001",
"available": true,
"availabilityReasonCode": "AVAILABLE",
"availabilityReasonMessage": "所选服务日期内可用",
"availabilityWindows": [
{ "startDate": "2026-07-29", "endDate": "2026-07-31" }
],
"insuranceCoverageStatus": "ANNUAL_COVERED",
"insuranceCoverageMessage": "全年保险已覆盖",
"insuranceCovered": true,
"residentVehicleId": "2080000000000000201",
"residentVehiclePlate": "蒙A-示例",
"completedOrderCount": 12,
"rating": null,
"hasRating": false,
"conflicts": []
}
}
}
```
冲突时该字段仍返回,不会被 `driverAvailability=AVAILABLE` 过滤:
```json
{
"data": {
"selectedVehicleResidentDriver": {
"driverId": "2080000000000000301",
"available": false,
"availabilityReasonCode": "ASSIGNMENT_CONFLICT",
"availabilityReasonMessage": "所选服务日期内存在派单冲突",
"availabilityWindows": [],
"conflicts": [
{
"startDate": "2026-07-30",
"endDate": "2026-07-31",
"blocking": true,
"reasonCode": "ASSIGNMENT_CONFLICT"
}
]
}
}
}
```
雪花 ID 继续按字符串处理,禁止 `Number()` 或 `parseInt()`。
## 前端交互口径
### 选车后默认常驻司机
- 用户选中车辆后重新请求候选接口,并读取 `selectedVehicleResidentDriver`。
- 字段非空且 `available === true`:默认选中该司机,并记录本次司机选择来源为“车辆常驻司机自动选中”。
- 字段非空且 `available === false`:不要自动选中;在车辆/司机联动区域显示醒目的 `常驻司机档期冲突` 标签,可补充 `availabilityReasonMessage`。
- 字段为 `null`:该车辆没有有效常驻司机,不自动选择司机。
- 不要在 `drivers.records` 中二次查找常驻司机;它可能因关键词、分页或“仅空闲”条件不在当前列表。
### 更换自动选中的常驻司机
- 只有当前司机是本次选车后自动选中的常驻司机时,用户点击其他司机才弹二次确认。
- 推荐文案:`该车辆已默认匹配常驻司机「{name}」,确认更换为「{newName}」吗?`
- 点击取消:保留原常驻司机,不更新本地 `selectedDriverId`,也不要以新司机重新查询接口。
- 点击确认:替换为新司机,再以新 `selectedDriverId` 查询候选接口。
- 常驻司机因档期冲突未自动选中时,用户选择其他司机不需要这次二次确认。
- 用户主动选择其他司机后的跨常驻关系,仍按已有 `selectedRelation.requiresConfirmation` 做最终派单确认;两种确认不可合并。
### 车辆取消与切换
- 再次点击已选车辆取消选择时,同时清除“自动常驻司机”来源标记;是否保留司机沿用当前页面既有取消车辆口径。
- 切换到另一辆车后,以上规则按新响应重新执行;不得沿用上一辆车的常驻司机快照。
## 前端处理清单
- [ ] 接入 `selectedVehicleResidentDriver`,不依赖司机当前分页定位常驻司机。
- [ ] 常驻司机档期可用时默认选中,并记录自动选择来源。
- [ ] 常驻司机冲突时不自动选中,展示 `常驻司机档期冲突` 标签。
- [ ] 更换自动选中的常驻司机时增加二次确认;取消不产生瞬时切换或接口重查。
- [ ] 保留已有跨常驻最终派单确认,两种确认分别处理。
- [ ] 雪花 ID 全程按字符串处理。
## 验证证据
- 定向测试覆盖:常驻司机在司机关键词/分页/仅空闲筛选之外仍返回;档期冲突仍返回;车辆无常驻司机返回 `null`。
- `mvn -pl hl-fleet-service -am test` 通过。
- `mvn -pl hl-fleet-service spotless:check` 通过。
- `mvn -pl hl-fleet-service -am verify` 通过。
- `hl-fleet-service` 已从 `dev-v3` 滚动部署测试环境,8087/8187 双实例健康。
- 测试网关实测 HTTP/业务码 200:常驻司机快照在司机关键词不命中、司机页为空和 `AVAILABLE` 筛选下仍返回,司机 ID 与车辆 `primaryDriverId` 一致;无常驻司机车辆返回 `null`。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,169 @@
---
schema: "hl-changelog/v1"
ticket: "5146"
title: "司机待确认通知与可选确认凭证"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-22T15:20:00+08:00"
---
# 【修改接口·前端待处理·管理后台】司机待确认通知与可选确认凭证
## 业务变化
派单待司机确认不再使用内部订单号和“模拟司机回复”。系统默认微信正文改为司机可直接判断是否接单的订车单,展示团号、服务日期、人数及人员类型构成、车辆、接送信息、客户备注和行程详情。
司机通过微信或电话真实回复后,由车务人员在页面登记“司机已确认接单”。确认凭证用于上传微信截图等佐证,改为选填,不上传也能登记司机确认并继续最终确认执行。
## 默认通知正文
```text
呼伦旅行—订车单
大含服务品质包,已包含接送机/站费用。不得与客人同餐;纯玩、无购物、无自费。请认真完成服务群内容。
师傅您好,请确认以下订车信息:
团号:26-0503
日期:2026-07-29 至 2026-07-31
人数:4人(成人2、幼童1、婴儿1)
车辆:蒙B-34567 丰田汉兰达(7座)
接团:CA1234 2026-07-29 10:40 海拉尔东山国际机场
送团:G529 2026-07-31 16:20 海拉尔站
备注:草原沙漠精华6日
行程详情:https://示例短链(有效期至 2026-07-31 23:59)
请确认是否接单。
```
- 不展示内部订单号。
- 不展示客户姓名。
- 人员构成仅展示数量大于 0 的类型,例如“成人2、儿童1、婴儿1”。
- 运营已人工修改过的自定义模板不会被迁移覆盖。
## 变更接口
### 登记司机已确认
```http
POST /admin/fleet/assignments/{assignmentId}/driver-confirmation
```
请求示例(无凭证):
```json
{
"requestId": "driver-confirm-20260722-001",
"driverReplyNote": "司机微信回复已确认接单"
}
```
请求示例(有凭证):
```json
{
"requestId": "driver-confirm-20260722-002",
"driverReplyNote": "司机微信回复已确认接单",
"evidenceFileIds": ["1934567890123456701"]
}
```
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `requestId` | 是 | 幂等标识,最长 64 字符;每次真实提交生成并在重试时保持不变 |
| `driverReplyNote` | 否 | 司机回复原话或摘要,最长 512 字符 |
| `evidenceFileIds` | 否 | 已上传文件的 fileId,最多 10 个;空数组、不传均允许 |
成功响应仍保持派单落库状态 `holding`,但返回:
```json
{
"code": 200,
"data": {
"assignmentStatus": "holding",
"stageCode": "driver_confirmed",
"stageLabel": "司机已确认·待车务确认执行",
"currentStep": 4,
"assignmentGroupId": "1934567890123456790",
"driverConfirmedAt": "2026-07-22T15:20:00",
"evidenceFileIds": []
},
"success": true
}
```
### 最终确认执行
登记司机确认成功后,再调用:
```http
POST /admin/fleet/assignments/{assignmentId}/confirm
```
```json
{
"requestId": "fleet-final-confirm-20260722-001"
}
```
- 最终确认不再要求存在凭证。
- 未调用 `driver-confirmation` 就直接最终确认,返回业务错误 `605025`,文案“请先登记司机已确认接单”。
- `assignmentId`、`assignmentGroupId`、`evidenceFileIds[]` 均按字符串处理。
## 前端页面调整要求
目标区域:派单弹窗“待确认”步骤,当前实现位于 `src/views/fleet/board/components/Step3DriverConfirm.vue` 及其父级流程。
1. 删除 `useDispatchMessage.js` 中 `messageTemplates` 三条本地假模板和 `buildMessage()` 拼接正文,不再使用 `standard/sched/itin` 这类前端自造 ID。
2. 弹窗打开时调用 `GET /admin/fleet/message-templates?templateType=hold_notify` 加载真实车管模板;默认选中后端返回的 `isDefault=true` 模板。
3. 选择订单、车辆、司机或模板后,调用 `POST /admin/fleet/message-templates/{templateId}/render`,传 `orderId/vehicleId/driverId`,以响应 `renderedBody` 作为消息预览。
4. 创建 HOLD 派单时传真实雪花 `messageTemplateId`。用户未编辑正文时不要传 `customBody`;用户确实改过本次正文时才传 `customBody`。
5. 删除“对方正在输入…”和“模拟·师傅回复确认”,不得用本地布尔值伪造司机回复。
6. 改为明确操作“登记司机已确认”,可同时填写可选的“司机回复摘要”。
7. 增加“上传确认凭证(选填)”,复用现有文件上传能力;上传成功后只传 fileId,不传 URL。
8. 点击登记时调用 `driver-confirmation`;只有响应成功且 `stageCode=driver_confirmed` 后,才允许进入最终确认执行。
9. 没有上传凭证时不得禁用登记按钮,也不得阻止下一步;`evidenceFileIds` 可省略或传 `[]`。
10. 最终确认调用 `confirm` 时只传新的 `requestId`,不要再传 `driverReplyNote`。
11. 页面刷新后以详情返回的 `driverConfirmedAt`/阶段信息恢复状态,不使用前端临时模拟状态。
现有 API 文件 `src/api/fleet/message-template.js` 已封装模板列表和渲染接口,应直接复用。页面显示的模板名、正文和最终创建请求必须来自同一个后端模板,禁止再次在前端维护一套同名文案。
推荐文案:
- 操作按钮:“登记司机已确认”
- 上传项:“确认凭证(选填)”
- 等待态:“等待司机回复确认”
- 已登记态:“司机已确认接单,待车务确认执行”
### 2026-07-24 界面验收补充
当前“待确认”步骤中,“司机待确认通知 / 模板与预览均来自后端”标题区下方存在明显的
大块空白,导致模板选择行和消息预览整体下移。模板标签及消息正文已经正常显示,因此
这是前端布局问题,不是后端模板或渲染接口缺少数据。
- 移除标题区不必要的固定高度、最小高度或空占位,让高度由标题和副标题内容自然撑开。
- 标题区与模板选择行保持正常紧凑间距,不要为未来内容预留不可见空白。
- 常用桌面分辨率下,标题区底部到模板选择行的垂直空白不应超过 16px。
- 本项不新增接口、不调整字段,也不要为修复布局重新维护前端本地模板。
## 前端处理清单
- [ ] 模板列表来自后端 `hold_notify` 模板,默认选中 `isDefault=true`,不再使用前端假模板。
- [ ] 消息预览调用后端 `render`,HOLD 创建传同一个真实 `messageTemplateId`,确保预览与发送一致。
- [ ] 删除模拟司机回复入口及伪造回复气泡。
- [ ] 增加真实“登记司机已确认”操作并调用 `driver-confirmation`。
- [ ] 支持上传最多 10 个确认凭证,明确标记为选填。
- [ ] 无凭证时仍可成功登记司机确认并执行最终确认。
- [ ] `605025` 时提示“请先登记司机已确认接单”,不提示“缺少凭证”。
- [ ] 页面刷新后能按后端状态恢复待回复/已确认阶段。
- [ ] 修复“司机待确认通知”标题区异常留白,模板选择与消息预览紧凑衔接。
## 验证证据
- 后端:`hl-order-service-v3` clean verify、shared/Feign 生产者与消费者测试、fleet 定向测试、`spotless:check` 和 fleet `verify` 通过。
- 部署:`hl-order-service-v3` 的 8086/8186、`hl-fleet-service` 的 8087/8187 两组测试实例均滚动发布成功并通过健康检查。
- 网关:使用 `admin / VEHICLE_MANAGER` 经 `https://api.test.1814.love:9443` 验证模板列表、车务看板、车辆列表、司机列表和模板 render,HTTP 均为 200。
- 默认 `hold_notify` 模板已按真实动态模板 ID 升级;固定服务规则、新变量、内部订单号/客户变量移除及渲染后无未解析变量均通过断言。
- 扩大聚合测试排除了三个与本次无关的既有失败:`CrossSchemaMigrationAuditTest`、`InternalUserControllerTest2`、`MpOrderDetailJsonSampleTest`;本次受影响的 order-v3/fleet 验证未排除。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,85 @@
# 房务订单详情:订单概要补充团号
> **服务**: hl-order-service-v3
> **PR**: [wx/HL#5151](https://git.1814.love:8443/wx/HL/pulls/5151)
> **Issue**: [wx/HL#5150](https://git.1814.love:8443/wx/HL/issues/5150)
> **日期**: 2026-07-22
> **影响范围**: 管理后台 · 房务订单详情弹窗标题/订单概要
---
## 关键变化
房务订单详情响应的 `data.order` 新增 `teamNo`。前端可直接显示订单当前团号;尚未生成团号时字段为 `null`,不要以订单号或其他值拼造团号。
---
## 变更接口
| 接口 | 方法 | 路径 | 变更类型 |
|------|------|------|----------|
| 房务订单详情 | GET | `/admin/house/orders/{orderId}` | 响应字段扩展 |
### 出参 `Result<HouseOrderDetailRespVO>`
| 字段路径 | 类型 | 是否新增 | 说明 |
|----------|------|----------|------|
| `data.order.teamNo` | `string/null` | 是 | 当前订单团号,取自 `order_main.team_no`;订金支付后生成,未生成时为 `null` |
有团号响应片段:
```json
{
"code": 200,
"data": {
"order": {
"orderId": "2044321098765432100",
"orderNo": "HL20260721171011648",
"teamNo": "26-0518",
"productName": "孔知悦"
}
},
"success": true
}
```
尚未生成团号时:
```json
{
"code": 200,
"data": {
"order": {
"orderId": "2044321098765432100",
"orderNo": "HL20260721171011648",
"teamNo": null,
"productName": "孔知悦"
}
},
"success": true
}
```
---
## 前端处理
1. 房务订单详情弹窗标题建议按“订单详情 · 订单号 · 团号 · 产品名”展示。
2. 团号读取 `data.order.teamNo`;有值时显示,无值时隐藏团号片段或显示统一空值占位。
3. 不要用 `orderNo` 回退为团号,也不要从列表缓存或历史快照读取团号。
---
## 边界与不影响范围
- 本次仅新增只读响应字段,不修改入参、状态机、房务权限和配房流程。
- 现有响应字段保持兼容。
- 无团号的存量订单正常返回 `teamNo: null`,无需数据迁移。
- `D:/work2/hl-ui` 未修改,前端适配由管理后台项目单独处理。
---
## 后端验证
- `HouseDetailAggregatorTest` 覆盖有团号、未生成团号两种场景。
- 定向测试结果:77 tests passed。
@@ -0,0 +1,111 @@
---
schema: "hl-changelog/v1"
ticket: "5149"
title: "派单详情补充团号与产品类型"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-22T16:22:00+08:00"
---
# 【修改接口·前端待处理·管理后台】派单详情补充团号与产品类型
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 仓库地址:<https://git.1814.love:8443/mmg/hl-ui.git>
- 目标分支:`v2.1`
- 联调/验收环境:<http://192.168.100.160:9527>
- 小程序:无需处理
> **服务**: hl-order-service-v3、hl-fleet-service
>
> **后端 PR**: [wx/HL#5154](https://git.1814.love:8443/wx/HL/pulls/5154)
>
> **工单**: [wx/HL#5149](https://git.1814.love:8443/wx/HL/issues/5149)
>
> **影响范围**: 管理后台订单派车弹窗 Step1 订单详情
## 关键变化
`GET /admin/fleet/board/orders/{orderId}` 已有 `teamNo`,但前端当前把 `orderNo` 显示在标题和详情区,造成订单号被误认为团号。本次新增产品类型枚举值和中文名,前端必须改用 `teamNo` 展示团号。
## 变更接口
| 方法 | 路径 | 变更类型 | 说明 |
| --- | --- | --- | --- |
| GET | `/admin/fleet/board/orders/{orderId}` | 响应新增字段 | 新增 `productType/productTypeName`,继续返回 `teamNo` |
响应关键字段:
```json
{
"code": 200,
"data": {
"orderNo": "HL20260721171011648",
"teamNo": "26-0503",
"customerName": "孔知悦",
"productName": "测试核心产品-多档-固定比例",
"productType": "CORE",
"productTypeName": "核心产品",
"relatedDetailReady": true
}
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `orderNo` | String/null | 订单号,仅保留业务查询和审计用途,不再作为弹窗团号展示 |
| `teamNo` | String/null | 团号,弹窗标题与详情区的权威展示字段 |
| `productType` | String/null | 产品类型枚举:`CORE/ROUTE/CUSTOM/GROUP` |
| `productTypeName` | String/null | `product_type` 数据字典中文名,页面优先展示该字段 |
order 服务不可用、`relatedDetailReady=false` 时,新产品类型字段可能为 `null`,前端显示 `--`,不要从产品名称猜测类型。
## 前端展示口径
### 弹窗标题
当前:
```text
派单 · {orderNo} · {customerName}
```
改为:
```text
派单 · {teamNo || '--'} · {customerName}
```
- 标题中不再展示 `orderNo`。
- `teamNo` 为空时显示 `--`,不得回退为订单号,以免继续混淆两个业务编号。
### 订单详情区
- 在订单基础信息中明确增加 `团号:{teamNo || '--'}`。
- 增加 `产品类型:{productTypeName || productType || '--'}`。
- 产品名称继续读取 `productName`,与产品类型分开显示。
- 图中顶部原 `HL202607...` 订单号位置改为团号;不要在同一区域重复显示订单号。
## 前端处理清单
- [ ] 弹窗标题将 `orderNo` 替换为 `teamNo`,空值显示 `--`。
- [ ] 订单详情区新增或修正“团号”字段,读取 `teamNo`。
- [ ] 订单详情区展示“产品类型”,优先读取 `productTypeName`,枚举值作为降级。
- [ ] 产品名称与产品类型保持两个独立字段,不从名称推断类型。
- [ ] 雪花 ID 继续按字符串处理。
## 验证证据
- order→fleet 共享 DTO 生产者/消费者定向测试通过。
- `mvn -pl hl-order-service-v3,hl-fleet-service -am test` 通过。
- `mvn -pl hl-fleet-service spotless:check` 通过。
- `mvn -pl hl-order-service-v3,hl-fleet-service -am verify` 通过。
- `hl-order-service-v3` 8086/8186 与 `hl-fleet-service` 8087/8187 已从 `dev-v3` 滚动部署并保持健康。
- 测试网关对截图订单实测 HTTP/业务码 200:`teamNo` 非空,`productType=CORE`,`productTypeName` 非空,且 `orderNo` 与 `teamNo` 为不同编号。
> 本文是前端接入通知,不代表已修改或发布 `mmg/hl-ui`。
@@ -0,0 +1,153 @@
# 【前端待处理·管理后台】#5156 车务团号、中文状态与常驻关系筛选
> **服务**: `hl-fleet-service`
> **Issue**: [wx/HL#5156](https://git.1814.love:8443/wx/HL/issues/5156)
> **日期**: 2026-07-22
> **影响范围**: 管理后台车务派单看板、订单详情、车务矩阵、派单候选弹窗
---
## 关键结论
1. 看板与矩阵的可见订单主标识统一使用完整团号:
```js
const visibleOrderCode = teamNo?.trim() || orderNo?.trim() || '—'
```
该规则只替换可见文案,不得用 `teamNo` 替换 `id`、`orderNumericId`、`assignmentId` 或 `assignmentGroupId`,不得改变行键、路由和派单接口入参。
**2026-07-23 漏验收补充**:矩阵“已派订单占用条”的正文必须直接显示
`visibleOrderCode`,不能只把团号放在鼠标悬浮 `title` 中。当前页面正文仍是
“人员构成 · 路线”,车务无法在矩阵中直接识别团号。建议正文按
“团号 · 人员构成 · 路线”排列;空间不足时优先保留完整团号,人员构成和路线可
省略或截断,悬浮提示继续展示完整信息。
2. 订单详情 `activeAssignments[]` 新增 `assignmentStatusLabel`。页面只展示中文标签,例如 `assignmentStatus=assigned` 对应 `assignmentStatusLabel=已派车`;`assignmentStatus` 继续用于程序判断,不直接显示英文状态码。
3. 派单候选支持双向常驻关系展示和筛选:
- 车辆候选有常驻司机时,展示 `primaryDriverName` 和 `primaryDriverMaskedPhone`;`vehicleKeyword` 支持车牌、车型、常驻司机姓名或 11 位完整手机号。
- 司机候选有常驻车辆时,展示 `residentVehiclePlate`;`driverKeyword` 支持司机姓名、11 位完整手机号或常驻车牌。
- 完整手机号只作为精确筛选入参,响应仍只返回脱敏手机号。
## 接口契约
### 1. 看板、详情与矩阵团号
| 页面场景 | 接口 | 团号字段 |
| --- | --- | --- |
| 看板列表 | `GET /admin/fleet/board/orders` | `data.records[].teamNo` |
| 看板详情 | `GET /admin/fleet/board/orders/{orderId}` | `data.teamNo` |
| 矩阵已派占用 | `GET /admin/fleet/matrix/grid` | `data.vehicles[].assignments[].teamNo` |
| 矩阵未派清单 | `GET /admin/fleet/matrix/unassigned-orders` | `data[].teamNo` |
| 矩阵单日清单 | `GET /admin/fleet/matrix/day-orders` | `data[].teamNo` |
前端处理位置:
- `src/views/fleet/board/index.vue`
- `src/views/fleet/board/components/OrderRowList.vue`
- `src/views/fleet/board/components/OrderDrawer.vue`
- `src/views/fleet/matrix/composables/useFleetMatrixData.js`
- `src/views/fleet/_shared/gantt/components/OrderGantt.vue`
- `src/views/fleet/_shared/gantt/components/OrderBar.vue`
- `src/views/fleet/_shared/gantt/components/VehicleGantt.vue`
- `src/views/fleet/_shared/gantt/components/UnassignedPool.vue`
- `src/views/fleet/matrix/components/DayListModal.vue`
### 2. 订单详情中文状态
`GET /admin/fleet/board/orders/{orderId}` 的每个 `data.activeAssignments[]` 新增:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `assignmentStatus` | `string` | 稳定状态码,供逻辑判断 |
| `assignmentStatusLabel` | `string` | 后端统一解析的中文状态标签,供页面展示 |
```json
{
"assignmentStatus": "assigned",
"assignmentStatusLabel": "已派车"
}
```
`OrderDrawer.vue` 中有效派车组右侧标签改为 `assignment.assignmentStatusLabel || '—'`,不要再以 `assignmentStatus` 作为可见兜底。
### 3. 派单候选常驻关系
接口:`POST /admin/fleet/assignments/candidates`
请求关键词:
| 字段 | 新口径 |
| --- | --- |
| `vehicleKeyword` | 车牌/车型包含匹配;常驻司机姓名包含匹配;常驻司机 11 位完整手机号精确匹配 |
| `driverKeyword` | 司机姓名包含匹配;司机 11 位完整手机号精确匹配;常驻车牌包含匹配 |
车辆候选 `data.vehicles.records[]`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `primaryDriverId` | `string \| null` | 常驻司机 ID |
| `primaryDriverName` | `string \| null` | 常驻司机姓名 |
| `primaryDriverMaskedPhone` | `string \| null` | 常驻司机脱敏手机号 |
司机候选 `data.drivers.records[]` 继续返回:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `residentVehicleId` | `string \| null` | 常驻车辆 ID |
| `residentVehiclePlate` | `string \| null` | 常驻车牌 |
前端处理要求:
- `VehiclePickerList.vue`:有常驻司机时显示“常驻司机:姓名 脱敏手机号”,无常驻时显示“无常驻”;搜索提示改为“搜索车牌/车型/常驻司机姓名/完整手机号”。
- `DriverPickerList.vue`:保留现有“常驻 {residentVehiclePlate}”展示;搜索提示改为“搜索姓名/完整手机号/常驻车牌”。
- `useVehicleDriverPicker.js`:关键词变化后分别重置对应页码为 1,并将原值传给 `vehicleKeyword` / `driverKeyword`;不在前端对当前页二次过滤。
- 所有雪花 ID 保持字符串处理。
## 示例
```json
{
"vehicles": {
"records": [
{
"vehicleId": "2000000000000000101",
"plate": "蒙A77777",
"primaryDriverId": "2000000000000000201",
"primaryDriverName": "张师傅",
"primaryDriverMaskedPhone": "138****5678"
}
]
},
"drivers": {
"records": [
{
"driverId": "2000000000000000201",
"name": "张师傅",
"maskedPhone": "138****5678",
"residentVehicleId": "2000000000000000101",
"residentVehiclePlate": "蒙A77777"
}
]
}
}
```
## 验收清单
- [ ] 看板卡片、详情标题和矩阵各订单入口优先显示完整 `teamNo`,空值回退 `orderNo`。
- [ ] 矩阵已派占用条正文直接显示 `teamNo || orderNo || '—'`;不能只在悬浮提示中显示。窄占用条优先保留完整团号,其次才是人员构成和路线。
- [ ] 有效派车组状态只显示 `assignmentStatusLabel` 中文文案,不再出现 `assigned` 等英文状态码。
- [ ] 有常驻司机的车辆显示姓名和脱敏手机号;可按姓名或完整手机号筛到对应车辆。
- [ ] 有常驻车辆的司机显示常驻车牌;可按常驻车牌筛到对应司机。
- [ ] 车牌/车型搜索和司机姓名/完整手机号搜索保持有效,关键词变化后分页正确重置。
- [ ] 团号展示不改变详情、拖拽、派单、改派、取消等操作使用的稳定 ID。
- [ ] 前端单元测试覆盖已派占用条正文团号、团号回退、中文状态、双向常驻关系显示与筛选参数。
## 后端核查证据
- `BoardOrderServiceTest` 覆盖 `assigned -> 已派车`。
- `AssignmentCandidateServiceTest` 覆盖车辆按常驻司机姓名/完整手机号筛选、司机按常驻车牌筛选及双向关系回显。
- `DriverServiceTest` 覆盖指定常驻司机集合内的姓名/完整手机号查询,明文手机号不离开司机域。
@@ -0,0 +1,41 @@
# 草原指南管理:今日前端文件夹选择改动已全部回退
> **日期**:2026-07-15
> **影响范围**:`hl-ui` 草原指南视频素材选择器
> **当前状态**:今天由 `wx` 合入的相关前端代码已全部撤销
---
## 一、回退结论
今天合入 `hl-ui` 的草原指南素材文件夹选择改动不再作为当前实现,相关 PR 已通过纯 Git revert 全部撤销:
- `v2.1`:原 PR #7、#8、#9 已由回退 PR [wx/hl-ui#12](https://git.1814.love:8443/wx/hl-ui/pulls/12) 撤销。
- `master`:原 PR #10 已由回退 PR [wx/hl-ui#11](https://git.1814.love:8443/wx/hl-ui/pulls/11) 撤销。
回退后未新增任何替代前端实现。
## 二、当前代码与部署基线
| 分支 / 环境 | 当前版本 | 回退校验 |
|---|---|---|
| `v2.1` / 测试环境 | `30b3ee1f`,任务 `ca6a7df0` | 代码树与改动前 `797beda4` 完全一致 |
| `master` / 正式环境 | `f97a7ccb`,deploy `271` | 代码树与改动前 `4d305ca4` 完全一致 |
测试与正式环境的 `VideoEditDrawer` 静态资源均已复核,不再包含本次新增的 `allow-sub-category-select` 或 `include-descendants` 配置。
## 三、对早期记录的更正
以下两份记录仅保留为历史过程,不代表当前 `hl-ui` 代码状态:
- `15_fix_grassland_guide_video_folder_selector_visibility.md`
- `15_fix_grassland_guide_video_current_folder_scope.md`
前端不得再按上述两份记录继续实现或判断当前页面能力;如后续重新启动该需求,需要重新确认业务范围并另开前端任务。
## 四、后端接口边界
- `GET /admin/material/list` 的 `includeDescendants` 后端可选能力仍保留。
- 默认不传或传 `false` 时只查当前分类节点;显式传 `true` 时查询所选节点及其后代。
- 当前 `hl-ui` 回退后不消费本次新增的文件夹选择能力。
- 本次前端回退不修改后端接口、响应结构、数据库、字典、菜单或 Nacos 配置。
@@ -0,0 +1,20 @@
# 草原指南管理端免登录详情接口
- 工单:`wx/HL#5005`
- 测试环境:已部署 `dev-v3`
- 正式环境:已部署 `main@59e2b20c`
- 鉴权:不需要登录
- 数据范围:只允许查询 `PUBLISHED`
## 接口
`GET /admin/grassland-guide/public/videos/{videoId}`
返回列表字段,并增加:`videoMaterialId`、`videoUrl`、
`customCoverMaterialId`、`contentHtml`、`contentImageMaterialIds`、
`createdBy`、`updatedBy`、`createdAt`、`updatedAt`。
说明:管理端免登录预览不受 `loginRequired` 限制;该字段只表达小程序用户是否需要登录。
草稿、下架、删除或不存在的视频统一按不存在处理。
测试网关实测:HTTP 200,业务码 `200`。
@@ -0,0 +1,27 @@
# 草原指南管理端免登录列表接口
- 工单:`wx/HL#5005`
- 测试环境:已部署 `dev-v3`
- 正式环境:已部署 `main@59e2b20c`
- 鉴权:不需要登录
- 数据范围:仅返回 `PUBLISHED`,不会返回草稿、下架或已删除数据
## 接口
`GET /admin/grassland-guide/public/videos`
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| keyword | string | 否 | - | 匹配标题或简介 |
| featured | boolean | 否 | - | 是否精选 |
| page | integer | 否 | 1 | 最小 1 |
| pageSize | integer | 否 | 20 | 1~100 |
`data` 为分页对象:`records`、`total`、`page`、`pageSize`。列表项包含:
`videoId`、`title`、`summary`、`effectiveCoverUrl`、`coverSource`、
`durationSeconds`、`featured`、`loginRequired`、`sortWeight`、`status`、
`publishTime`、`linkedProductId`。
测试网关实测:HTTP 200,业务码 `200`。
@@ -0,0 +1,26 @@
# 草原指南管理端免登录播放接口
- 工单:`wx/HL#5005`
- 测试环境:已部署 `dev-v3`
- 正式环境:已部署 `main@59e2b20c`
- 鉴权:不需要登录
- 数据范围:只允许播放 `PUBLISHED`
## 接口
`GET /admin/grassland-guide/public/videos/{videoId}/play`
`data` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| videoId | string | 视频业务 ID |
| title | string | 标题 |
| videoUrl | string | OSS MP4 直出地址 |
| durationSeconds | integer | 素材自动解析的时长(秒) |
| effectiveCoverUrl | string | 自定义封面或 OSS 自动截帧封面 |
该接口是播放最小响应,不返回正文、创建人或操作信息。管理端免登录预览不受
`loginRequired` 限制;小程序端仍按该字段执行登录鉴权。
测试网关实测:HTTP 200,业务码 `200`,真实素材返回播放地址与时长。
@@ -0,0 +1,23 @@
# 草原指南管理端免登录推荐接口
- 工单:`wx/HL#5005`
- 测试环境:已部署 `dev-v3`
- 正式环境:已部署 `main@59e2b20c`
- 鉴权:不需要登录
- 数据范围:源视频和推荐结果都必须是 `PUBLISHED`,推荐结果自动排除自身
## 接口
`GET /admin/grassland-guide/public/videos/{videoId}/recommendations`
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| page | integer | 否 | 1 | 最小 1 |
| pageSize | integer | 否 | 20 | 1~100 |
`data` 为分页对象:`records`、`total`、`page`、`pageSize`;`records` 字段与列表接口一致。
推荐继续使用后端推荐算法,相关性不足时随机兜底,不需要前端拼装。
测试网关实测:HTTP 200,业务码 `200`。
@@ -0,0 +1,17 @@
# 草原指南管理列表按排序权重正序
- 工单:`wx/HL#5011`
- 测试环境:已部署 `dev-v3`
- 接口:`GET /admin/grassland-guide/videos`
## 变更
列表排序改为:
1. `sortWeight ASC`
2. `publishTime DESC`
3. `videoId DESC`
排序权重数字越小越靠前,`0` 排在所有正数权重之前。接口字段和请求参数没有变化,前端不需要改传参。
测试环境已用 57 条真实数据验证,权重顺序为 `0, 0, 0, 1, 100, 200...`。
@@ -0,0 +1,98 @@
# 草原指南 MP4 物理交错修复与测试数据回填
> 日期:2026-07-16
>
> 后端 Issue:[HL #5016](https://git.1814.love:8443/wx/HL/issues/5016)
>
> 后端 PR:[HL #5019](https://git.1814.love:8443/wx/HL/pulls/5019)
>
> 测试分支:`dev-v3`
>
> 影响服务:`hl-user-service`
>
> 本次不修改前端仓库,不新增或修改接口字段
## 1. 问题结论
草原指南真实 4K MP4 在 `1x`、`2x` 播放时出现固定位置反复缓冲。浏览器 Network 中同一文件产生大量被取消并重新发起的 `206 Partial Content` 请求,实际传输量可以超过文件本身体积。
该问题包含两个独立因素:
1. 历史 MP4 虽然已经把 `moov` 移到 `mdat` 前面,但音频、视频 Sample 在 `mdat` 中没有按时间物理交错。播放到同一时间点时,浏览器需要在文件相距很远的位置来回读取音视频数据,造成 Range 请求抖动、取消和重复下载。
2. 原片本身约 `357204588` 字节、`71` 秒,平均码率约 `40.25 Mbps`。2026-07-16 当前诊断链路连续读取 OSS Range 仅约 `0.52–1.44 MB/s`(约 `4.14–11.50 Mbps`),低于 `1x` 所需的约 `5.03 MB/s`,因此修复文件结构后仍可能因实际网络吞吐不足发生正常缓冲。
前端缓存策略不能补足长期吞吐缺口。`preload="auto"` 只能改善起播等待,不能让 `10 Mbps` 链路持续播放 `40 Mbps` 原片。
## 2. 后端修复
草原指南视频上传确认阶段新增 MP4 无损重封装:
- 不重新编码,不改变 H.264/AAC Sample。
- 不降低分辨率、帧率或码率。
- 保持原视频、音频轨的 Sample 数量、Sample 总字节数和时长。
- 将 `ftyp`、`moov` 放在文件前部。
- 以约 2 秒为窗口重新排列音频、视频 Chunk,使相同时间点的数据物理相邻。
- 重封装前后执行媒体指纹校验;不一致时拒绝替换原对象。
- 新对象使用 `.progressive-...mp4` Key,成功后再以 CAS 更新文件记录;旧对象暂不删除,便于回滚。
- 单个 Pod 同时只执行一个重封装任务,并检查临时磁盘空间,避免大文件并发耗尽磁盘。
接口路径和请求体保持不变:
```http
POST /admin/material/upload/confirm
```
前端仍然只提交原有 `materialId`、`description` 和 `tagIds`,不需要增加重封装参数。
## 3. 历史测试数据回填
已对测试环境问题视频执行一次性回填:
| 项目 | 值 |
| --- | --- |
| 草原指南视频 ID | `2076910159484928002` |
| 素材 ID | `2076909130286612482` |
| 文件 ID | `2076909128663379970` |
| 文件大小 | `357204588` 字节 |
| 新时长 | `71` 秒 |
| 新文件标识 | OSS Key 包含 `.progressive-2076909128663379970-` |
回填结果:
- `file_info` 已切换为新 `.progressive-...mp4`,状态为 `ACTIVE / OSS_MP4_READY_V1`。
- `material.oss_url`、自动封面和时长已切换。
- `grassland_guide_video` 自动封面和时长已同步,业务记录仍为 `PUBLISHED`。
- 资源服务 `8082/8182` 与网关 `8080` 的匿名详情、播放接口均返回新地址。
- OSS `HEAD` 返回 `200 video/mp4`、`Content-Length: 357204588`、`Accept-Ranges: bytes`。
- `Range: bytes=0-1048575` 返回 `206` 和正确的 `Content-Range`。
## 4. 部署与验证证据
- 修复提交:`4529da5e97fea21022ca700390a146944608eadc`
- `dev-v3` 合并提交:`65c63a68d903370d07dd80ffea62ac58ccfb31c8`
- `hl-user-service` 测试环境滚动部署任务:`f4d670ba`
- 两实例 `8081/8181` 均启动健康。
- `hl-user-service` 测试:`3226` 个测试,`0` 失败,`0` 错误,`6` 跳过。
- 真实 357 MB 文件无损重封装测试通过;重封装前后轨道 Sample 数、Sample 字节数和时长一致。
- 原文件约 44 秒处的音频、视频数据物理距离约 `185.7 MB`;重封装后缩短到约 `69.7 KB`。
## 5. 仍需处理的基础设施边界
本次后端修复解决“文件内部排列导致重复 Range 请求”的问题,但不能提高用户到 OSS 的实时带宽。
在“不降低码率、不生成低清档”的前提下:
- `1x` 需要链路持续高于约 `40.25 Mbps`,还应预留网络波动余量。
- `2x` 平均需要约 `80.50 Mbps`,短时峰值可能更高。
- 当前测试桶传输加速域名请求返回 `400`,未形成可用的加速播放链路。
- 若目标用户链路长期低于原片码率,只能选择 OSS 前置 CDN/边缘缓存、开通并验证 OSS 传输加速,或让用户在播放前基本下载完整文件;单纯调整浏览器缓冲逻辑无法解决。
接入 CDN 或 OSS 传输加速会改变基础设施和费用,须单独确认后实施。不得为规避该问题把流量代理到 Java 服务,也不得把几百 MB 文件整体读入服务内存。
## 6. 大文件上传确认超时提醒
本次真实文件在测试环境完成下载、重封装、上传共耗时约 `410` 秒,超过当前网关约 `60` 秒的请求超时。
- 服务端任务能够继续完成,但前端可能先收到 `504`。
- 在异步媒体处理改造完成前,前端不得因单次 `504` 立即重复提交或重复上传,应重新查询素材状态。
- 正式发布前应单独改造为异步处理状态机,或提供明确的后台任务查询接口;不建议简单把网关超时提高到数分钟。
@@ -0,0 +1,19 @@
# 草原指南小程序列表按排序权重正序
- 工单:`wx/HL#5011`
- 测试环境:已部署 `dev-v3`
- 影响接口:
- `GET /mp/grassland-guide/home`
- `GET /mp/grassland-guide/videos`
## 变更
普通视频分页列表排序改为:
1. `sortWeight ASC`
2. `publishTime DESC`
3. `videoId DESC`
排序权重数字越小越靠前,`0` 排在所有正数权重之前。首页精选区原本就是小权重优先,规则保持不变;相关推荐仍使用推荐算法,不改为纯权重排序。
接口字段和请求参数没有变化。
@@ -0,0 +1,108 @@
# 草原指南切换 2x 后永久缓冲更正
> 日期:2026-07-16
>
> 前端仓库:`mmg/hl-ui`
>
> 影响组件:`src/views/h5/grassland-guide/components/GuideVideoPlayer.vue`
>
> 影响辅助逻辑:`src/views/h5/grassland-guide/components/playbackBuffer.js`
>
> 本次后端接口、字段、OSS 地址和视频文件均无变化
## 1. 现象
视频在 `1x` 已经开始播放后切换到 `2x`,页面停在“正在缓冲”,即使等待较长时间也不能恢复。
Network 中可以看到同一个 MP4 存在多条大小不同的 `206 Partial Content` 请求。这是 Chrome 原生媒体加载器根据 MP4 元数据、当前播放位置和缓存状态发起的 HTTP Range 请求,Range 大小不固定是正常行为,不能据此判断 OSS 分片异常。
## 2. 已确认根因
线上播放器当前调用链为:
```text
切换 2x
→ 设置 video.playbackRate = 2
→ evaluateBuffer({ initial: true })
→ 要求 bufferAhead >= 15 秒
→ 未达到阈值
→ pauseForBuffering()
→ video.pause()
```
播放器暂停后,浏览器可以降低甚至停止后续媒体预取。当前代码又只依赖 `progress`、`canplay` 等媒体事件重新执行 `evaluateBuffer()`,没有保证这些事件一定继续产生,因此可能永远达不到 `15` 秒阈值,形成状态机死锁。
同类问题还存在于:
- 首次播放前等待固定 `8` 秒。
- `timeupdate` 检测到低水位后主动 `pause()`。
- `waiting`/`stalled` 事件再次主动 `pause()`。
这些逻辑把浏览器原生的“缺数据时等待并继续下载”变成了应用层“暂停后等待浏览器继续下载”,两者行为并不等价。
## 3. 必须修改的前端逻辑
### 3.1 播放与切换倍速
```js
async function play() {
playbackRequested.value = true
await videoRef.value?.play()
}
function togglePlaybackRate() {
const video = videoRef.value
if (!video) return
playbackRate.value = playbackRate.value === 1 ? 2 : 1
video.playbackRate = playbackRate.value
// 禁止在这里 pause()
// 禁止在这里重新执行固定秒数的初始缓冲门槛
}
```
### 3.2 缓冲状态由原生事件驱动
```js
function handleWaiting() {
if (!playbackRequested.value) return
buffering.value = true
recordPlaybackEvent('waiting')
}
function handleStalled() {
const video = videoRef.value
if (playbackRequested.value && video?.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
buffering.value = true
}
recordPlaybackEvent('stalled')
}
function handlePlaying() {
buffering.value = false
recordPlaybackEvent('playing')
}
```
事件处理器内不得调用 `video.pause()`。只要用户没有主动暂停,就保留原生播放意图,让浏览器在缺数据时自动等待、继续 Range 取流,并在数据恢复后自行继续播放。
### 3.3 删除主动低水位暂停
- 删除 `pauseForBuffering()` 对自动缓冲流程的使用。
- `handleTimeUpdate()` 只同步时间、记录 `bufferAhead` 和掉帧指标,不得检测低水位后暂停。
- `evaluateBuffer()` 不再阻塞首次播放或倍速切换;如保留该方法,只能用于诊断,不能控制 `play/pause`。
- 删除 `BUFFER_POLICIES` 中作为播放硬门槛的 `initial/low/resume`,避免后续重新引入死锁。
## 4. 验收要求
- [ ] `1x` 点击播放后能直接进入原生播放流程,不等待固定 `8` 秒。
- [ ] 播放中切换 `2x` 不触发 `pause` 事件。
- [ ] 切换 `2x` 后,即使触发 `waiting`,后续 Range 请求仍继续。
- [ ] 数据恢复后触发 `playing`,缓冲遮罩自动消失,播放继续。
- [ ] `1x ↔ 2x` 连续切换 10 次,不出现永久缓冲。
- [ ] 拖动进度后仍可重新播放,用户主动暂停不会被自动恢复。
- [ ] Console 中不再出现 `ratechange → initial-buffering → pause` 的调用序列。
- [ ] 使用 DevTools 测试真实用户表现时关闭 `Disable cache`;需要模拟弱网时单独选择网络限速,不把禁用缓存结果当作正常生产表现。
修复状态机后,若 `1x` 或 `2x` 仍出现能够自行恢复的短时 `waiting`,再根据 `bufferAhead`、实际下载速度和掉帧数判断是用户网络还是设备解码能力问题。播放器逻辑修复不能提高用户带宽;需要跨地区稳定承载原始 4K 高码率视频时,应另行评估 OSS 前置 CDN Range 缓存。
@@ -0,0 +1,15 @@
# 草原指南管理端免登录列表按排序权重正序
- 工单:`wx/HL#5011`
- 测试环境:已部署 `dev-v3`
- 接口:`GET /admin/grassland-guide/public/videos`
## 变更
列表排序改为:
1. `sortWeight ASC`
2. `publishTime DESC`
3. `videoId DESC`
排序权重数字越小越靠前,`0` 排在所有正数权重之前。接口字段、分页方式和免登录规则没有变化。
@@ -0,0 +1,128 @@
# 草原指南 4K 原片播放缓冲优化通知
> 日期:2026-07-16
>
> 前端仓库:`mmg/hl-ui`
>
> 影响页面:草原指南 H5 详情/播放页
>
> 本次后端接口、字段和 OSS 地址均无变化
> [!IMPORTANT]
> 2026-07-16 线上验证发现:原通知中的“主动暂停并等待固定缓冲秒数”会与浏览器原生媒体加载策略形成死锁,已经撤销。前端必须按独立更正通知
> [`16_fix_grassland_guide_playback_buffer_deadlock.md`](./16_fix_grassland_guide_playback_buffer_deadlock.md)
> 处理,不得再以 `8/15` 秒阈值阻塞播放或切换倍速。
## 1. 问题与诊断结论
草原指南详情直接播放后端返回的 OSS 原始 MP4。正式环境真实 4K 样本的媒体参数为:
- 文件约 `451.74 MiB`,时长约 `95` 秒。
- 分辨率 `3812 × 2160`,`60 fps`。
- H.264 Main Profile,Level `5.2`。
- 平均码率约 `40.20 Mbps`。
- `1x` 播放时短时峰值约 `72.98 Mbps`。
- `2x` 播放时平均网络消耗约 `80.40 Mbps`;短时峰值约 `142.17 Mbps`。
正式 OSS 已支持 HTTP Range,请求返回 `206 Partial Content`。同一诊断环境连续读取 OSS Range 的平均速度约 `230.52 Mbps`,高于该视频 `2x` 播放的短时峰值。因此目前没有证据表明 Java 服务或 OSS Range 能力是主要瓶颈;但该结果不代表每个用户到 OSS 的实时链路都能达到相同速度。
现已确认 `1x` 也会偶发卡顿。结合 `1x` 接近 `73 Mbps` 的短时峰值,更可能是用户链路瞬时波动与浏览器前向缓冲较浅共同造成:即使平均网速高于视频平均码率,只要短时间下载速度低于瞬时消耗速度,缓冲仍可能耗尽。因此首次 `1x` 播放和卡顿恢复也必须执行缓冲水位保护,不能只处理 `2x`。
此外,`4K 60 fps` 在 `2x` 下相当于设备需要承担接近 `120 fps` 的解码节奏。部分设备即使网络充足,也可能因硬件解码能力不足出现掉帧;前端需要把“等待网络缓冲”和“设备解码掉帧”分别记录。
## 2. 前端处理要求
### 2.1 保持原生 Range 播放
- 继续直接使用接口返回的 `videoUrl` 作为 `<video src>`。
- 设置 `preload="auto"`,允许浏览器提前加载媒体数据。
- 不要使用 `fetch`/`axios` 把完整 MP4 下载为 Blob 后再播放,避免一次性占用数百 MiB 内存并破坏原生 Range 调度。
- 不要改写 OSS 域名,不转成 HLS/M3U8,不接直播播放器。
- 本轮不降低码率、不转码、不新增清晰度档位。
### 2.2 计算真实前向缓冲
不能只读取 `video.buffered.end(video.buffered.length - 1)`。应找到包含 `currentTime` 的 buffered 区间,再计算:
```js
function getBufferAhead(video) {
const currentTime = video.currentTime
for (let index = 0; index < video.buffered.length; index += 1) {
const start = video.buffered.start(index)
const end = video.buffered.end(index)
if (currentTime >= start && currentTime <= end) {
return Math.max(0, end - currentTime)
}
}
return 0
}
```
### 2.3 `1x` 与倍速播放缓冲策略
- 用户点击播放时直接调用原生 `video.play()`,不得先暂停等待固定缓冲秒数。
- 用户切换 `2x` 时只设置 `video.playbackRate = 2`,不得调用 `pause()`,也不得重新执行“初始缓冲门槛”。
- `waiting` 事件只负责显示“正在缓冲”和记录诊断;不得在事件处理器内再次调用 `pause()`。
- 保持原生播放请求后,浏览器会继续 Range 取流;数据恢复后通过 `playing` 事件关闭缓冲提示。
- `stalled` 用于记录网络加载停滞;仅当仍有播放意图且 `readyState < HTMLMediaElement.HAVE_FUTURE_DATA` 时显示缓冲提示,不主动暂停。
- `timeupdate` 只采集缓冲指标,不得因低于人为水位而主动暂停。
- 用户主动暂停、拖动进度或离开页面时清除缓冲提示,避免与真实播放意图混淆。
- `preload="auto"` 只是浏览器提示,不能强制浏览器预取指定秒数。
`video.buffered` 用于诊断和观测,不再作为阻塞 `play()` 或切换倍速的硬门槛。简单的原生 `<video>` 无法保证“暂停后一定继续预取到 N 秒”;若业务将来要求确定性分片缓冲,需要另行评估 MSE/HLS 或 CDN,不应在原生 MP4 播放器中模拟。
### 2.4 记录卡顿证据
至少监听并记录以下事件和状态:
- 事件:`loadstart`、`loadedmetadata`、`canplay`、`canplaythrough`、`progress`、`waiting`、`stalled`、`playing`、`seeking`、`seeked`、`error`。
- 状态:`currentTime`、`playbackRate`、前向缓冲秒数、`readyState`、`networkState`。
- 浏览器支持时记录 `getVideoPlaybackQuality()` 的 `droppedVideoFrames` 和 `totalVideoFrames`。
日志不得记录完整带签名 URL、Token 或其他凭证。建议只记录 `videoId`、事件时间和上述播放指标。
判断口径:
- 出现 `waiting`/`stalled`,同时前向缓冲接近 `0`:网络或缓冲调度不足。
- 前向缓冲充足、没有 `waiting`,但 `droppedVideoFrames` 持续上升:设备解码能力不足。
## 3. 接口契约
播放接口保持不变:
```http
GET /admin/grassland-guide/public/videos/{videoId}/play
```
前端继续使用:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `videoId` | string | 视频业务 ID |
| `videoUrl` | string | OSS 原始 MP4 地址,直接交给原生 `<video>` |
| `durationSeconds` | integer | 后端从素材解析的时长 |
| `effectiveCoverUrl` | string | 生效封面 |
雪花 ID 必须按字符串处理。本次不增加缓冲、码率或清晰度字段,前端不得等待后端返回这些字段后才处理播放。
相关接口说明见:
- [`16_feat_grassland_guide_public_play.md`](./16_feat_grassland_guide_public_play.md)
- [`12_feat_grassland_guide_admin_mp_oss.md`](./12_feat_grassland_guide_admin_mp_oss.md)
## 4. 前端验收清单
- [ ] `<video>` 使用接口原始 `videoUrl`,并设置 `preload="auto"`。
- [ ] 没有将完整 MP4 下载为 Blob。
- [ ] 首次 `1x` 播放不受固定缓冲秒数阻塞。
- [ ] 切换 `2x` 不调用 `pause()`,不等待 `15` 秒缓冲。
- [ ] `waiting`/`stalled` 只更新提示与日志,不主动暂停;`playing` 能可靠关闭提示。
- [ ] 用户暂停、拖动和离开页面时不会被自动恢复播放。
- [ ] 能区分并记录缓冲耗尽与设备解码掉帧。
- [ ] 使用正式 4K 样本分别连续验证 `1x` 和 `2x`,记录等待次数、累计等待时长、最小前向缓冲和掉帧数。
- [ ] Chrome 桌面端和目标移动设备均完成验证。
若完成上述缓冲策略后,多个地区和设备仍普遍出现“前向缓冲耗尽”,再单独评估 OSS 前置 CDN Range 缓存或 OSS 传输加速;该基础设施调整会新增费用,不属于本次前端通知范围。
@@ -0,0 +1,34 @@
# 草原指南 MP4 重封装时间轴修复正式发布
> 日期:2026-07-17
>
> 后端 Issue:[HL #5021](https://git.1814.love:8443/wx/HL/issues/5021)
>
> 后端 PR:[HL #5026](https://git.1814.love:8443/wx/HL/pulls/5026)
>
> 正式分支:`main`
>
> 影响服务:`hl-user-service`
## 问题与修复
旧版 MP4 无损重封装虽然保持了 Sample 数据,但 `mvhd/tkhd/elst` 使用了不一致的时间单位,浏览器可能把完整视频识别成数秒并提前触发 `ended`。
后端现已统一 presentation timeline 的 movie timescale,并在替换 OSS 对象前校验 `mvhd/tkhd/elst/mdhd/stts` 一致性。视频仍保持原分辨率、帧率、编码和码率,不经过 Java 服务代理播放流量。
接口路径、请求体及响应字段均保持不变,前端无需适配新字段。
## 验证结果
- MP4 核心测试:30/30 通过
- `FileServiceTest` 与 `OssServiceTest`:141/141 通过
- Maven package:`BUILD SUCCESS`
- 测试环境真实媒体 1x/2x 播放已验收不卡顿
- 正式部署任务:`#283`
- 发布提交/镜像:`009d5ffe`
- `hl-user-service`:`2/2 Ready`
- 正式列表、详情、推荐、播放接口:HTTP/业务码均为 200
- 正式真实 MP4:`Content-Length: 592945035`、`Accept-Ranges: bytes`
- `Range: bytes=0-1023`:返回 206,`Content-Range: bytes 0-1023/592945035`
本次未修改或部署 `hl-ui`,无数据库结构及 Nacos 配置变更。
@@ -0,0 +1,25 @@
# 草原指南排序权重正序规则正式发布
> 日期:2026-07-17
>
> 后端 PR:[HL #5026](https://git.1814.love:8443/wx/HL/pulls/5026)
>
> 正式分支:`main`
>
> 影响服务:`hl-resource-service`
## 变更说明
草原指南列表排序权重统一按正序处理:数值越小越靠前,`0` 位于 `1` 之前。
本次不新增或删除接口,不修改请求参数与响应字段。前端继续使用原有列表接口,不需要自行反转结果。
## 正式验证
- 正式部署任务:`#282`
- 发布提交/镜像:`009d5ffe`
- `hl-resource-service`:`2/2 Ready`
- 匿名正式列表接口:HTTP 200、业务码 200
- 正式数据首屏排序权重:`0,1,2,3,4,5,6,7,8,9`
本次未修改或部署 `hl-ui`,无数据库结构及 Nacos 配置变更。
@@ -0,0 +1,306 @@
# ✨ 酒店与房型轻量下拉接口(#5183)
> **PR**: [#5184](https://git.1814.love:8443/wx/HL/pulls/5184)
> **服务**: hl-resource-service
> **更新时间**: 2026-07-23 14:05
## 1. 接口背景
管理后台在核单 Step1 手动新增住宿项时,需要异步搜索可用酒店,并在选定酒店后加载该酒店当前可用的房型。新增两个轻量接口,避免下拉框拉取完整资源详情或在前端过滤禁用数据。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 酒店轻量下拉分页 | GET | `/admin/hotel/options` | 新增接口 | 按酒店名称异步分页搜索,仅返回启用酒店 |
| 2 | 启用房型轻量下拉 | GET | `/admin/hotel/{hotelId}/room-type-options` | 新增接口 | 返回指定酒店下的启用房型 |
## 3. 接口详情
### 3.1 酒店轻量下拉分页
- **使用场景**:酒店下拉框首次打开或用户输入关键词时调用。
- **认证**:需要管理后台 JWT,使用 `Authorization: Bearer <token>`。
- **幂等性**:幂等,只读查询。
- **响应类型**:`Result<PageResult<HotelSimpleRespVO>>`。
#### Query 入参
| 字段 | 类型 | 必填 | 默认值 | 校验规则 | 说明 |
|------|------|------|--------|----------|------|
| `keyword` | String | 否 | `null` | 最长 100 个字符 | 酒店名称模糊匹配;空字符串或仅空白字符等同未传 |
| `page` | Integer | 否 | `1` | 最小值 1 | 当前页码 |
| `pageSize` | Integer | 否 | `20` | 1~100 | 每页条数 |
无请求体。
#### 完整出参
| 字段 | 类型 | 可空 | 说明 |
|------|------|------|------|
| `code` | Integer | 否 | 业务状态码;成功为 `200` |
| `message` | String | 否 | 响应消息;成功为 `成功` |
| `success` | Boolean | 否 | `code == 200` 时为 `true` |
| `traceId` | String | 是 | 链路追踪 ID |
| `data` | Object | 否 | 分页数据 |
| `data.records` | Array | 否 | 本页酒店列表;无匹配项时为空数组 |
| `data.records[].hotelId` | String | 否 | 酒店 ID;必须按字符串保存和传递 |
| `data.records[].hotelName` | String | 否 | 酒店名称 |
| `data.total` | Integer | 否 | 符合条件的总记录数 |
| `data.page` | Integer | 否 | 当前页码 |
| `data.pageSize` | Integer | 否 | 每页条数 |
#### 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询完成,包括没有匹配数据 |
| `400` | 参数校验失败 | `page < 1`、`pageSize` 不在 1~100,或 `keyword` 超过 100 个字符 |
#### 业务边界
- 只返回启用状态的酒店,禁用酒店不会出现在任何分页结果中。
- `keyword` 仅对酒店名称做模糊匹配。
- 未传 `keyword` 时分页查询全部启用酒店。
- 页码超过最后一页时,`records` 返回空数组,`total` 仍是符合条件的总记录数。
- 结果按酒店名称、酒店 ID 升序排列。
#### 典型成功示例
**请求**
```http
GET /admin/hotel/options?keyword=伯爵&page=1&pageSize=20
Authorization: Bearer <token>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"hotelId": "9007199254740993",
"hotelName": "伯爵酒店"
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
#### 边界示例:无匹配数据
**请求**
```http
GET /admin/hotel/options?keyword=不存在的酒店&page=1&pageSize=20
Authorization: Bearer <token>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
},
"traceId": "b2c3d4e5-f6a7-8901",
"success": true
}
```
#### 异常示例:每页条数越界
**请求**
```http
GET /admin/hotel/options?page=1&pageSize=101
Authorization: Bearer <token>
```
无请求体。
**响应**
```json
{
"code": 400,
"message": "每页条数最大为100",
"data": null,
"traceId": "c3d4e5f6-a7b8-9012",
"success": false
}
```
### 3.2 启用房型轻量下拉
- **使用场景**:用户从酒店下拉框选定酒店后,加载该酒店可选择的房型。
- **认证**:需要管理后台 JWT,使用 `Authorization: Bearer <token>`。
- **幂等性**:幂等,只读查询。
- **响应类型**:`Result<List<RoomTypeOptionRespVO>>`。
#### Path 入参
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `hotelId` | String | 是 | 已选酒店 ID;URL 中按十进制数字字符串传递 |
无 Query 参数,无请求体。
#### 完整出参
| 字段 | 类型 | 可空 | 说明 |
|------|------|------|------|
| `code` | Integer | 否 | 业务状态码;成功为 `200` |
| `message` | String | 否 | 响应消息;成功为 `成功` |
| `success` | Boolean | 否 | `code == 200` 时为 `true` |
| `traceId` | String | 是 | 链路追踪 ID |
| `data` | Array | 否 | 启用房型列表;没有启用房型时为空数组 |
| `data[].roomTypeId` | String | 否 | 房型 ID;必须按字符串保存和传递 |
| `data[].roomTypeName` | String | 否 | 房型名称 |
| `data[].roomCategory` | String | 否 | 房型分类,取值见第 4 节 |
#### 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询完成,包括酒店不存在或没有启用房型 |
| `400` | 参数类型错误 | `hotelId` 不是可解析的十进制数字字符串 |
#### 业务边界
- 只返回 `hotelId` 对应酒店下的启用房型,禁用房型不会返回。
- 酒店不存在、酒店下没有房型或没有启用房型时,均返回成功和空数组。
- 结果按房型排序值、房型 ID 升序排列。
- 该接口不分页,应在酒店选择变化后重新请求,不能继续使用上一个酒店的房型结果。
#### 典型成功示例
**请求**
```http
GET /admin/hotel/9007199254740993/room-type-options
Authorization: Bearer <token>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": [
{
"roomTypeId": "9007199254741993",
"roomTypeName": "标准间",
"roomCategory": "STANDARD"
},
{
"roomTypeId": "9007199254741994",
"roomTypeName": "家庭房",
"roomCategory": "FAMILY"
}
],
"traceId": "d4e5f6a7-b8c9-0123",
"success": true
}
```
#### 边界示例:没有启用房型
**请求**
```http
GET /admin/hotel/9007199254740999/room-type-options
Authorization: Bearer <token>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": [],
"traceId": "e5f6a7b8-c9d0-1234",
"success": true
}
```
#### 异常示例:酒店 ID 类型错误
**请求**
```http
GET /admin/hotel/not-a-number/room-type-options
Authorization: Bearer <token>
```
无请求体。
**响应**
```json
{
"code": 400,
"message": "参数类型错误: hotelId='not-a-number'(需要 Long 类型)",
"data": null,
"traceId": "f6a7b8c9-d0e1-2345",
"success": false
}
```
## 4. 枚举 / 数据字典
### 4.1 `roomCategory`(字典类型 `room_category`)
**所属字段**:`RoomTypeOptionRespVO.roomCategory`
**类型**:String
**必填**:是
| 值 | 中文 | 说明 |
|----|------|------|
| `STANDARD` | 标间 | 标准房型 |
| `SINGLE` | 单人间 | 单人房型 |
| `TWIN` | 双床房 | 双床房型 |
| `QUEEN` | 大床房 | 大床房型 |
| `KING` | 豪华大床 | 豪华大床房型 |
| `SUITE` | 套房 | 套房房型 |
| `FAMILY` | 家庭房 | 家庭房型 |
| `YURT` | 蒙古包 | 蒙古包房型 |
| `SPECIAL` | 特色房 | 特色房型 |
| `PARENT_CHILD` | 亲子房 | 亲子房型 |
## 5. 影响评估
- 两个接口均为新增,只读接口,不破坏现有接口兼容性。
- 前端可按需接入;不要求与后端同步上线。
- 所有酒店 ID、房型 ID 都按 `String` 处理,不要转换为 JavaScript `Number`。
## 6. 关联
- **Issue**: [#5183](https://git.1814.love:8443/wx/HL/issues/5183)
- **PR**: [#5184](https://git.1814.love:8443/wx/HL/pulls/5184)
- **Merge commit**: [0163a7a03](https://git.1814.love:8443/wx/HL/commit/0163a7a0308344691f2379e154c40069fd8d34ba)
- **后端负责人**: @yaosutu
@@ -0,0 +1,219 @@
# 【🔧 修改接口·管理后台】Step2 票种规格字典(#5185)
> **PR**: #5191 | **更新时间**: 2026-07-23
## 1. 接口背景
Step2 门票/游玩项目需要统一的票种/规格选项。管理后台现在可以按字典类型加载启用选项,首个可用选项为“成人票”,避免前端写死选项文案。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 按类型查询字典数据 | GET | `/admin/dict/data/settlement_ticket_spec` | 修改接口 | 新增 `settlement_ticket_spec` 字典类型及启用项“成人票” |
## 3. 接口详情
### 3.1 按类型查询 Step2 票种规格
- **使用场景**:加载 Step2 门票/游玩项目的票种/规格下拉选项。
- **认证**:需要管理后台 JWT。
- **幂等性**:是,只读查询。
- **限流**:无接口专属限流约定。
- **排序**:按 `sortOrder` 升序,同一排序号再按 `dictDataId` 升序。
- **过滤**:仅返回 `status=ACTIVE` 的字典项。
## 4. 接口入参
### 4.1 路径参数
| 字段 | 类型 | 必填 | 固定值 | 说明 |
|------|------|------|--------|------|
| `dictType` | String | 是 | `settlement_ticket_spec` | Step2 票种/规格字典类型编码 |
### 4.2 Query 参数与请求体
无 Query 参数,无请求体。
## 5. 出参(响应)
### 5.1 统一响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码,成功为 `200` |
| `message` | String | 响应消息,成功为“成功” |
| `data` | Array | 启用的字典项列表;无匹配项时为 `[]` |
| `traceId` | String / null | 链路追踪 ID |
| `success` | Boolean | `code=200` 时为 `true` |
### 5.2 `data[]` 字典项字段
| 字段 | 类型 | 可为空 | 说明 |
|------|------|--------|------|
| `dictDataId` | Long | 否 | 字典数据 ID |
| `dictType` | String | 否 | 字典类型编码,本接口固定为 `settlement_ticket_spec` |
| `dictLabel` | String | 否 | 展示文案 |
| `dictValue` | String | 否 | 提交值;选择后写入 Step2 `items[].specName` |
| `icon` | String | 是 | 图标,本字典项当前为 `null` |
| `color` | String | 是 | 展示色值,本字典项当前为 `null` |
| `sortOrder` | Integer | 否 | 排序号,越小越靠前 |
| `status` | String | 否 | 字典项状态 |
| `remark` | String | 是 | 字典项说明 |
| `createdAt` | String | 是 | 创建时间,格式为 `yyyy-MM-dd HH:mm:ss` |
| `updatedAt` | String | 是 | 更新时间,格式为 `yyyy-MM-dd HH:mm:ss` |
## 6. 枚举 / 数据字典
### 6.1 `settlement_ticket_spec`
**展示字段**:`dictLabel` | **提交字段**:`dictValue` | **提交目标**:Step2 `items[].specName`
| `dictValue` | `dictLabel` | `sortOrder` | `status` | 说明 |
|-------------|-------------|-------------|----------|------|
| `成人票` | 成人票 | `10` | `ACTIVE` | Step2 默认票种/规格 |
### 6.2 `status`
| 值 | 中文 | 是否由本接口返回 |
|----|------|------------------|
| `ACTIVE` | 启用 | 是 |
| `INACTIVE` | 禁用 | 否;查询接口会过滤禁用项 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询成功;没有匹配项时 `data=[]` |
| `401` | 未认证或认证失效 | 未携带有效管理后台 JWT |
| `500` | 系统异常 | 查询过程发生未预期异常 |
## 8. 示例
### 8.1 典型成功
**请求**:
```http
GET /admin/dict/data/settlement_ticket_spec
Authorization: Bearer <admin-token>
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": [
{
"dictDataId": 101411,
"dictType": "settlement_ticket_spec",
"dictLabel": "成人票",
"dictValue": "成人票",
"icon": null,
"color": null,
"sortOrder": 10,
"status": "ACTIVE",
"remark": "Step2 默认票种/规格",
"createdAt": "2026-07-23 10:00:00",
"updatedAt": "2026-07-23 10:00:00"
}
],
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
### 8.2 边界情况:不存在的字典类型
**请求**:
```http
GET /admin/dict/data/not_exists
Authorization: Bearer <admin-token>
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": [],
"traceId": "b2c3d4e5-f6a7-8901",
"success": true
}
```
### 8.3 业务失败:未认证
**请求**:
```http
GET /admin/dict/data/settlement_ticket_spec
```
无请求体,且未携带 `Authorization`。
**响应**:
```json
{
"code": 401,
"message": "未认证或登录已失效",
"data": null,
"traceId": "c3d4e5f6-a7b8-9012",
"success": false
}
```
## 9. 业务边界
- 字典接口只返回启用项;禁用项不会出现在下拉列表中。
- 前端展示 `dictLabel`,并将选中项的 `dictValue` 原样提交到 Step2 `items[].specName`。
- 当前首个字典值为“成人票”;后续新增启用项时,接口会按排序规则一并返回。
- 字典为单层平铺列表,不包含父子层级。
## 10. 修改前后对比
### 10.1 字段级对比
| 项目 | 改前 | 改后 |
|------|------|------|
| 响应结构 | `Result<List<SysDictDataRespVO>>` | 不变 |
| Step2 票种规格字典类型 | 无 `settlement_ticket_spec` 可用项 | 返回启用项“成人票” |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 加载 Step2 票种/规格选项 | 无专用字典数据 | 可查询 `settlement_ticket_spec` |
| 前端选项值 | 需要自行维护 | 使用接口返回的 `dictValue` |
## 11. 影响评估
- **是否破坏向后兼容**:否,接口路径和响应结构未改变。
- **前端是否必须同步上线**:否;接入后可使用动态字典,旧逻辑不会因本次新增字典项而报错。
## 12. 注意事项
- 不要把“成人票”选项数组硬编码在前端;应按需调用本接口。
- 展示使用 `dictLabel`,保存使用 `dictValue`,不要提交 `dictDataId`。
- Step2 仍使用原有 `specName` 字段,没有新增 `specCode`。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5185](https://git.1814.love:8443/wx/HL/issues/5185)
- **PR**: [#5191](https://git.1814.love:8443/wx/HL/pulls/5191)
- **Merge commit**: [03ab19b46](https://git.1814.love:8443/wx/HL/commit/03ab19b463be4b00f92848cb348ce6158918e041)
### 13.2 联系人
- **后端负责人**: @yst
@@ -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)
@@ -0,0 +1,111 @@
# 【前端待处理·管理后台】景区季节全量清空与标签同步(#5123)
## 目标前端
- 端类型:管理后台(Web)
- 目标仓库:`mmg/hl-ui`
- 目标分支:按前端仓库当前发布流程执行
- 后端工单:`wx/HL #5123`
- 小程序:无需改页面,但必须参与接口与缓存回退验收
## 问题与根因
`src/views/resource/scenic/SeasonDrawer.vue` 的 `handleSave()` 目前只在
`isSeasonConfigured(form)` 为 true 时调用 PUT。已存在的季节被全部清空后,
该判断变为 false,前端既不发 PUT,也没有调用后端已有 DELETE,数据库旧记录仍在,
因此重新打开抽屉会回显旧内容,页签“已配置”和景区列表季节标签也不会消失。
后端同时修复了可空字段写入 null、季节素材引用解绑,以及管理后台/小程序相关缓存依赖失效。
前端不能继续用“不发请求”表达删除已存在季节。
## 接口契约
### 查询季节列表
```http
GET /admin/scenic/spot/{scenicId}/seasons
```
### 保存仍有内容的季节
```http
PUT /admin/scenic/spot/{scenicId}/season/{seasonType}
Content-Type: application/json
```
### 删除已全量清空的季节
```http
DELETE /admin/scenic/spot/{scenicId}/season/{seasonType}
```
`seasonType` 取 `spring`、`summer`、`autumn`、`winter`。DELETE 无请求体,沿用现有管理后台鉴权。
## 前端改动要求
### 1. API 封装
在 `src/api/scenic.js` 新增并导出删除方法,例如:
```js
export function deleteScenicSeason(scenicId, seasonType) {
return http.delete(`/scenic/spot/${scenicId}/season/${seasonType}`)
}
```
### 2. 记录初始已配置季节
`SeasonDrawer.vue` 每次打开并成功加载季节列表后,记录后端实际返回过的 `seasonType` 集合。
- 加载前清空该集合,避免切换景区时串数据。
- 只以后端列表是否存在记录作为“初始已配置”依据,不要用当前编辑中的
`isSeasonConfigured()` 反推。
- 查询失败时不得把未知状态当成“从未配置”;应阻止保存或保留错误状态,避免误判删除。
### 3. 保存判定
遍历四季时按以下规则处理:
| 初始状态 | 当前表单 | 请求 |
| --- | --- | --- |
| 不存在 | 全空 | 不请求 |
| 不存在 | 有内容 | PUT |
| 已存在 | 有内容 | PUT |
| 已存在 | 全空 | DELETE |
所有文本字段都按 `trim()` 后判断是否为空;素材按有效 `id` 判断,空壳对象不能让季节继续显示为已配置。
同一次保存中任一季节请求失败时,不得关闭抽屉或显示“全部保存成功”;应保留编辑内容并明确提示失败季节。
全部请求成功后重新 GET 季节列表,再触发父级景区列表刷新,确保以下状态以服务端结果收敛:
- 当前抽屉内容不再回显已删除季节。
- 对应页签“已配置”标记消失。
- 景区列表对应季节标签消失。
- 其他季节和景区基础信息不变。
## 小程序链路说明
小程序产品详情会经 product-service 和 resource-service 读取季节亮点及季节媒体。
后端已为实时产品详情、订单快照、mp-service 产品聚合和景区详情缓存补齐
`table:scenic_season` / `table:scenic_spot` 依赖。
季节删除后,小程序不得继续显示旧 `seasonHighlights`、封面、轮播图或视频;未命中季节时应回退景区本体媒体和节点快照描述。前端管理后台无需主动清小程序 Redis,也不得新增清缓存接口。
## 验收清单
- [ ] 仅配置描述和亮点的季节,两项全部清空并保存后,重新打开不再回显。
- [ ] 对应页签“已配置”标记消失。
- [ ] 保存成功并刷新景区列表后,对应季节标签消失。
- [ ] 保留其他内容时,可单独清空描述、亮点、封面、轮播图和视频。
- [ ] 清空一个季节不影响其他季节及景区基础信息。
- [ ] 从未配置且仍为空的季节不发 PUT 或 DELETE。
- [ ] 查询季节列表失败时不会误发 DELETE。
- [ ] 部分请求失败时抽屉保留,且不会提示全部成功。
- [ ] 小程序产品详情不再返回已删除季节的亮点或媒体。
- [ ] 小程序在季节未命中时正确回退景区本体媒体和节点快照描述。
## 发布说明
- 本文是前端修复与联调通知,不代表已修改或发布 `mmg/hl-ui`。
- 前端完成后须创建并指派自身工单,走分支、PR、测试和发布流程,并关联 `wx/HL #5123`。
- 测试环境验收必须通过网关使用真实管理员鉴权完成 PUT、DELETE、GET 回读;记录不得包含 token、Cookie 或真实隐私数据。
@@ -0,0 +1,94 @@
---
schema: "hl-changelog/v2"
ticket: "5254"
title: "订单侧已配置车辆补充车型车队服务日期与日单价"
consumer: "admin"
change_type: "修改接口"
backend_status: "pending"
gateway_status: "pending"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-07-26"
base: "dev-v3"
generated: "2026-07-26T09:57:21+08:00"
---
# 订单侧已配置车辆补充车型车队服务日期与日单价
订单详情的已配置车辆补齐车型标题、车队、连续服务日期、服务天数和协议日单价。
本记录只表示后端契约交接,`frontend_status` 在真实前端领取前保持 `pending`。
## 关联
- Issue: #5254
- PR: 待补充
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| GET | `/v3/admin/order/{id}/itinerary` | `data.vehicleGroup.assignments[]` |
### `data.vehicleGroup.assignments[]`
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `brand` | string | 否 | 兼容字段;Fleet 实时数据下回填车辆车型名称,前端标题可按 `brand \|\| vehicleType \|\| '—'` 展示 |
| `fleetTeamId` | string | 否 | 车辆所属车队 ID;雪花 ID 按字符串返回 |
| `fleetTeamName` | string | 否 | 车辆所属车队名称;归档车辆或历史快照无法补齐时为空 |
| `startDate` | string(`yyyy-MM-dd`) | 否 | 车辆/司机连续服务段开始日 |
| `endDate` | string(`yyyy-MM-dd`) | 否 | 车辆/司机连续服务段结束日 |
| `serviceDays` | integer | 否 | 连续服务天数,首尾日期均计入 |
| `plannedDailyFee` | string(decimal) | 否 | 协议日单价;连续段内每日协议价不一致或无价格时为空,不得按 0 元展示 |
既有 `vehicleType`、`licensePlate`、`seats`、`driverName` 和
`driverPhoneMasked` 继续返回;手机号保持脱敏。
### 内部 Feign/shared Java
`OrderDriverVehicleCandidateDTO` 新增可空字段:
- `fleetTeamId: Long`(JSON 字符串)
- `fleetTeamName: String`
- `protocolPrice: BigDecimal`(JSON 字符串)
既有 `startDate`、`endDate` 本次开始映射到订单侧公开响应。新旧 Fleet/Order
可滚动部署:旧消费者忽略新增字段,新消费者读取旧生产者时新增字段为空。
## 契约影响文件
- `hl-common/hl-common-core/src/main/java/com/hulalv/common/dto/fleet/OrderDriverVehicleCandidateDTO.java`
- `hl-order-service-v3/src/main/java/com/hulalv/order/core/controller/admin/vo/detail/ItineraryVO.java`
- `hl-order-service-v3/src/test/java/com/hulalv/order/core/controller/admin/OrderControllerTest.java`
- `hl-order-service-v3/src/test/java/com/hulalv/order/fleet/feign/FleetDriverVehicleFeignContractTest.java`
## 前端/调用方动作
- `src/views/order-v2/detail/_shared/v3Adapter.js` 映射新增字段:
`fleetTeamName`、`startDate`、`endDate`、`serviceDays`、`plannedDailyFee`。
- 车型标题使用 `brand || vehicleType || '—'`;`brand === vehicleType` 时不要重复展示同一车型。
- “用车安排”摘要展示车型、车牌、座位、司机、脱敏手机号和服务日期。
- “已配置车辆”弹窗展示车型、车牌、座位、所属车队、司机、脱敏手机号、
服务起止日期、服务天数和协议日单价。
- `plannedDailyFee == null` 时显示 `—`,不得显示 0 元;无车辆时保持“暂无已配车”。
- 多车/改派连续段按接口数组逐条渲染,不按车牌或司机姓名自行去重。
## 验证证据
- Fleet 定向测试:
`mvn -pl hl-fleet-service -am -Dtest=OrderDriverVehicleQueryServiceTest,AssignmentConverterTest -Dsurefire.failIfNoSpecifiedTests=false test`
(31 项通过)。
- Order 消费者与内部契约:
`mvn -pl hl-order-service-v3 -am -Dtest=OrderDetailServiceTest,FleetDriverVehicleFeignContractTest -Dsurefire.failIfNoSpecifiedTests=false test`
(79 项通过)。
- 前端 API 序列化:
`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`(通过)。
- 网关验证:待补充
- 兼容性结论:仅新增可空响应字段并补齐既有空字段;内部 Feign JSON 双向兼容,
不修改方法、路径、参数、必填项、枚举或错误码。
+13
查看文件
@@ -0,0 +1,13 @@
{
"name": "hl-api-changelog-quality-gates",
"private": true,
"type": "module",
"scripts": {
"test": "node --test tests/validate-changelog-filenames.test.mjs tests/validate-changelog-frontmatter.test.mjs",
"check:filenames": "node scripts/validate-changelog-filenames.mjs",
"check:frontmatter": "node scripts/validate-changelog-frontmatter.mjs"
},
"engines": {
"node": ">=20"
}
}
+316
查看文件
@@ -0,0 +1,316 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
const CONTROLLED_ROOTS = new Map([
['changelogs-v2', '管理后台'],
['changelogs-v2-mp', '小程序端'],
]);
const CHANGE_TYPES = new Set(['新增接口', '修改接口', '删除接口']);
function ruleError(code, path, message, expected) {
return { code, path, message, expected };
}
export function getShanghaiDate(now = new Date()) {
if (!(now instanceof Date) || Number.isNaN(now.getTime())) {
throw new TypeError('The validation clock must be a valid 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]));
if (!/^\d{4}$/.test(values.year) || !/^\d{2}$/.test(values.month) || !/^\d{2}$/.test(values.day)) {
throw new Error('Unable to derive the current Asia/Shanghai calendar date.');
}
return {
yearMonth: `${values.year}-${values.month}`,
day: values.day,
isoDate: `${values.year}-${values.month}-${values.day}`,
};
}
export function parseNameStatusZ(raw) {
const text = Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw ?? '');
const tokens = text.split('\0');
if (tokens.at(-1) === '') {
tokens.pop();
}
const records = [];
for (let index = 0; index < tokens.length;) {
const status = tokens[index++];
if (!status) {
throw new Error('Malformed git diff: an empty status token was found.');
}
if (/^[RC]\d{1,3}$/.test(status)) {
const sourcePath = tokens[index++];
const targetPath = tokens[index++];
if (sourcePath === undefined || targetPath === undefined) {
throw new Error(`Malformed git diff: ${status} must contain source and target paths.`);
}
records.push({ status, sourcePath, targetPath });
continue;
}
const targetPath = tokens[index++];
if (targetPath === undefined) {
throw new Error(`Malformed git diff: ${status} is missing its path.`);
}
records.push({ status, targetPath });
}
return records;
}
export function collectNewTargetPaths(records) {
return records
.filter(({ status }) => status === 'A' || /^C\d{1,3}$/.test(status) || /^R\d{1,3}$/.test(status))
.map(({ targetPath }) => targetPath);
}
export function controlledRootForPath(inputPath) {
const candidate = String(inputPath);
for (const root of CONTROLLED_ROOTS.keys()) {
if (candidate === root || candidate.startsWith(`${root}/`) || candidate.startsWith(`${root}\\`)) {
return root;
}
}
return undefined;
}
export function validateChangelogPath(inputPath, now = new Date()) {
const changelogPath = String(inputPath);
const root = controlledRootForPath(changelogPath);
if (!root) {
return [];
}
const expectedClient = CONTROLLED_ROOTS.get(root);
if (changelogPath.includes('\\')) {
return [ruleError(
'E_STRUCTURE',
changelogPath,
'受控 Git 路径包含反斜杠;反斜杠是文件名字节,不是路径分隔符',
`${root}/YYYY-MM/DD_issue_标题-{新增接口|修改接口|删除接口}-${expectedClient}.md`,
)];
}
const expectedDate = getShanghaiDate(now);
const errors = [];
const segments = changelogPath.split('/');
if (segments.length !== 3 || !segments[2].endsWith('.md')) {
return [ruleError(
'E_STRUCTURE',
changelogPath,
'路径层级或扩展名不符合规则',
`${root}/YYYY-MM/DD_issue_标题-{新增接口|修改接口|删除接口}-${expectedClient}.md`,
)];
}
const [, yearMonth, filename] = segments;
if (yearMonth !== expectedDate.yearMonth) {
errors.push(ruleError(
'E_MONTH',
changelogPath,
`月目录为 ${yearMonth}`,
`Asia/Shanghai 当天的 ${expectedDate.yearMonth}`,
));
}
const stem = filename.slice(0, -3);
const clientSeparator = stem.lastIndexOf('-');
const actualClient = clientSeparator >= 0 ? stem.slice(clientSeparator + 1) : '';
const beforeClient = clientSeparator >= 0 ? stem.slice(0, clientSeparator) : stem;
if (actualClient !== expectedClient) {
errors.push(ruleError(
'E_CLIENT',
changelogPath,
`端类型为 ${actualClient || '(缺失)'}`,
expectedClient,
));
}
const typeSeparator = beforeClient.lastIndexOf('-');
const actualType = typeSeparator >= 0 ? beforeClient.slice(typeSeparator + 1) : '';
const prefix = typeSeparator >= 0 ? beforeClient.slice(0, typeSeparator) : beforeClient;
if (!CHANGE_TYPES.has(actualType)) {
errors.push(ruleError(
'E_CHANGE_TYPE',
changelogPath,
`变更类型为 ${actualType || '(缺失)'}`,
'新增接口 / 修改接口 / 删除接口',
));
}
const prefixMatch = /^(\d{2})_([^_]*)_(.*)$/.exec(prefix);
if (!prefixMatch) {
const dayMatch = /^(\d{2})_/.exec(prefix);
if (dayMatch && dayMatch[1] !== expectedDate.day) {
errors.push(ruleError('E_DAY', changelogPath, `日前缀为 ${dayMatch[1]}`, expectedDate.day));
}
errors.push(ruleError(
'E_ISSUE',
changelogPath,
'文件名缺少可识别的正整数 Issue 号',
'DD_[1-9][0-9]*_标题',
));
return errors;
}
const [, day, issue, title] = prefixMatch;
if (day !== expectedDate.day) {
errors.push(ruleError('E_DAY', changelogPath, `日前缀为 ${day}`, expectedDate.day));
}
if (!/^[1-9]\d*$/.test(issue)) {
errors.push(ruleError('E_ISSUE', changelogPath, `Issue 号为 ${issue || '(缺失)'}`, '不带 # 的十进制正整数'));
}
if (title.trim() === '') {
errors.push(ruleError('E_TITLE', changelogPath, '业务标题为空', '非空业务标题'));
}
return errors;
}
export function runValidation(records, now = new Date()) {
const targetPaths = collectNewTargetPaths(records);
const controlledPaths = targetPaths.filter((targetPath) => controlledRootForPath(targetPath));
const errors = controlledPaths.flatMap((targetPath) => validateChangelogPath(targetPath, now));
return {
targetCount: targetPaths.length,
checkedCount: controlledPaths.length,
errors,
expectedDate: getShanghaiDate(now),
};
}
export function parseArguments(argv) {
const options = {};
for (let index = 0; index < argv.length; index += 2) {
const flag = argv[index];
const value = argv[index + 1];
if (!['--event', '--base', '--head'].includes(flag) || value === undefined) {
throw new Error(`Unsupported or incomplete argument: ${flag ?? '(missing)'}`);
}
options[flag.slice(2)] = value;
}
if (options.event && (options.base || options.head)) {
throw new Error('--event cannot be combined with --base/--head.');
}
if (!options.event && (!options.base || !options.head)) {
throw new Error('Use --event <path> or both --base <ref> --head <ref>.');
}
return options;
}
function runGit(args, options = {}) {
return execFileSync('git', args, {
...options,
stdio: ['pipe', 'pipe', 'pipe'],
});
}
function resolveCommitRef(ref, label) {
if (typeof ref !== 'string' || ref.length === 0 || /[\0\r\n]/.test(ref)) {
throw new Error(`${label} must be a non-empty Git ref without control characters.`);
}
let resolved;
try {
resolved = runGit(['rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`], { encoding: 'utf8' }).trim();
} catch {
throw new Error(`${label} does not resolve to an existing commit.`);
}
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(resolved)) {
throw new Error(`${label} resolved to an unexpected object ID.`);
}
return resolved;
}
function resolveEventCommitSha(sha, label) {
if (typeof sha !== 'string' || !/^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/.test(sha)) {
throw new Error(`${label} must be a full 40- or 64-character hexadecimal commit SHA.`);
}
return resolveCommitRef(sha.toLowerCase(), label);
}
function emptyTreeObjectId() {
const objectId = runGit(['hash-object', '-t', 'tree', '--stdin'], {
input: Buffer.alloc(0),
encoding: 'utf8',
}).trim();
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(objectId)) {
throw new Error('Git returned an unexpected empty-tree object ID.');
}
return objectId;
}
function gitDiff(revisions) {
return runGit(['diff', '--name-status', '-z', '--find-renames', ...revisions, '--'], {
encoding: 'buffer',
maxBuffer: 64 * 1024 * 1024,
});
}
export function diffFromOptions(options) {
if (!options.event) {
const base = resolveCommitRef(options.base, 'base ref');
const head = resolveCommitRef(options.head, 'head ref');
return gitDiff([`${base}...${head}`]);
}
const event = JSON.parse(readFileSync(options.event, 'utf8'));
if (event.pull_request) {
const base = event.pull_request.base?.sha;
const head = event.pull_request.head?.sha;
if (!base || !head) {
throw new Error('The pull_request event is missing base/head SHA values.');
}
const resolvedBase = resolveEventCommitSha(base, 'pull_request base SHA');
const resolvedHead = resolveEventCommitSha(head, 'pull_request head SHA');
return gitDiff([`${resolvedBase}...${resolvedHead}`]);
}
if (event.before && event.after) {
if (typeof event.before !== 'string' || !/^(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})$/.test(event.before)) {
throw new Error('push before SHA must be a full 40- or 64-character hexadecimal value.');
}
const before = /^0+$/.test(event.before)
? emptyTreeObjectId()
: resolveEventCommitSha(event.before, 'push before SHA');
const after = resolveEventCommitSha(event.after, 'push after SHA');
return gitDiff([before, after]);
}
throw new Error('Unsupported event payload: expected pull_request or push before/after SHA values.');
}
export function main(argv = process.argv.slice(2)) {
try {
const options = parseArguments(argv);
const records = parseNameStatusZ(diffFromOptions(options));
const result = runValidation(records, new Date());
if (result.errors.length > 0) {
for (const error of result.errors) {
console.error(`[${error.code}] ${error.path}: ${error.message}; expected ${error.expected}`);
}
console.error(`FAIL: found ${new Set(result.errors.map(({ path }) => path)).size} invalid changelog path(s) among ${result.checkedCount} controlled changelog path(s).`);
return 1;
}
console.log(`PASS: validated ${result.checkedCount} controlled changelog path(s) from ${result.targetCount} new target path(s); expected Shanghai date ${result.expectedDate.isoDate}.`);
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();
}
+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();
}
+293
查看文件
@@ -0,0 +1,293 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { existsSync, mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import {
collectNewTargetPaths,
getShanghaiDate,
parseNameStatusZ,
runValidation,
validateChangelogPath,
} from '../scripts/validate-changelog-filenames.mjs';
const SHANGHAI_NOW = new Date('2026-07-22T04:00:00.000Z');
const CLI_PATH = fileURLToPath(new URL('../scripts/validate-changelog-filenames.mjs', import.meta.url));
function validAdmin({ day = '22', month = '2026-07', issue = '5161', title = '文件名校验', type = '新增接口', client = '管理后台' } = {}) {
return `changelogs-v2/${month}/${day}_${issue}_${title}-${type}-${client}.md`;
}
function validMp({ day = '22', month = '2026-07', issue = '5161', title = '文件名校验', type = '修改接口', client = '小程序端' } = {}) {
return `changelogs-v2-mp/${month}/${day}_${issue}_${title}-${type}-${client}.md`;
}
function codes(file, now = SHANGHAI_NOW) {
return validateChangelogPath(file, now).map((error) => error.code);
}
test('accepts a valid v2 admin path', () => {
assert.deepEqual(validateChangelogPath(validAdmin(), SHANGHAI_NOW), []);
});
test('accepts a valid v2-mp path', () => {
assert.deepEqual(validateChangelogPath(validMp(), SHANGHAI_NOW), []);
});
test('uses Asia/Shanghai when UTC is still the previous day', () => {
const now = new Date('2026-07-21T16:05:00.000Z');
assert.equal(getShanghaiDate(now).isoDate, '2026-07-22');
assert.deepEqual(validateChangelogPath(validAdmin(), now), []);
});
test('accepts the real leap day', () => {
const now = new Date('2024-02-29T04:00:00.000Z');
assert.deepEqual(validateChangelogPath(validAdmin({ month: '2024-02', day: '29' }), now), []);
});
test('rejects a wrong day', () => {
assert.ok(codes(validAdmin({ day: '21' })).includes('E_DAY'));
});
test('rejects a wrong month directory', () => {
assert.ok(codes(validAdmin({ month: '2026-06' })).includes('E_MONTH'));
});
test('rejects a day greater than 31', () => {
assert.ok(codes(validAdmin({ day: '63' })).includes('E_DAY'));
});
test('rejects a missing issue number', () => {
assert.ok(codes('changelogs-v2/2026-07/22_标题-新增接口-管理后台.md').includes('E_ISSUE'));
});
test('rejects issue zero', () => {
assert.ok(codes(validAdmin({ issue: '0' })).includes('E_ISSUE'));
});
test('rejects a hash-prefixed issue', () => {
assert.ok(codes(validAdmin({ issue: '#5161' })).includes('E_ISSUE'));
});
test('rejects a non-numeric issue', () => {
assert.ok(codes(validAdmin({ issue: 'ABC' })).includes('E_ISSUE'));
});
test('rejects an unsupported change type', () => {
assert.ok(codes(validAdmin({ type: '新增字段' })).includes('E_CHANGE_TYPE'));
});
test('rejects the mini-program client in v2', () => {
assert.ok(codes(validAdmin({ client: '小程序端' })).includes('E_CLIENT'));
});
test('rejects the admin client in v2-mp', () => {
assert.ok(codes(validMp({ client: '管理后台' })).includes('E_CLIENT'));
});
test('rejects an empty title', () => {
assert.ok(codes(validAdmin({ title: '' })).includes('E_TITLE'));
});
test('rejects a backslash inside a controlled physical path', () => {
const physicalPath = 'changelogs-v2/2026-07\\22_5161_绕过-新增接口-管理后台.md';
assert.ok(codes(physicalPath).includes('E_STRUCTURE'));
});
test('rejects a root-level backslash pseudo path', () => {
const physicalPath = 'changelogs-v2\\2026-07\\22_5161_绕过-新增接口-管理后台.md';
assert.ok(codes(physicalPath).includes('E_STRUCTURE'));
});
test('exempts phase-one changelogs', () => {
assert.deepEqual(validateChangelogPath('changelogs/2026-07/99_no_issue_anything.md', SHANGHAI_NOW), []);
});
test('parses NUL-delimited Chinese and space-containing paths', () => {
const raw = Buffer.from(`A\0${validAdmin({ title: '中文 空格' })}\0M\0old file.md\0`, 'utf8');
assert.deepEqual(parseNameStatusZ(raw), [
{ status: 'A', targetPath: validAdmin({ title: '中文 空格' }) },
{ status: 'M', targetPath: 'old file.md' },
]);
});
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',
'utf8',
));
assert.deepEqual(collectNewTargetPaths(records), []);
assert.equal(runValidation(records, SHANGHAI_NOW).errors.length, 0);
});
test('validates only a copy target path', () => {
const records = parseNameStatusZ(Buffer.from(`C100\0old.md\0${validAdmin()}\0`, 'utf8'));
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 target with an old day', () => {
const target = validAdmin({ day: '21' });
const records = parseNameStatusZ(Buffer.from(`R100\0changelogs-v2/2026-07/63_old.md\0${target}\0`, 'utf8'));
assert.ok(runValidation(records, SHANGHAI_NOW).errors.some((error) => error.code === 'E_DAY'));
});
test('aggregates all invalid paths instead of stopping at the first', () => {
const records = [
{ status: 'A', targetPath: validAdmin() },
{ status: 'A', targetPath: validMp() },
{ status: 'A', targetPath: validAdmin({ day: '21' }) },
{ status: 'A', targetPath: validMp({ issue: '0' }) },
];
const result = runValidation(records, SHANGHAI_NOW);
assert.equal(new Set(result.errors.map((error) => error.path)).size, 2);
assert.equal(result.checkedCount, 4);
});
test('real git diff ignores historical modifications and validates an added path', () => {
const repo = mkdtempSync(path.join(tmpdir(), 'hl-changelog-gate-'));
try {
execFileSync('git', ['init', '-q'], { cwd: repo });
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: repo });
execFileSync('git', ['config', 'user.name', 'Gate Test'], { cwd: repo });
execFileSync('git', ['config', 'core.autocrlf', 'false'], { cwd: repo });
const oldPath = path.join(repo, 'changelogs-v2', '2026-07', '63_old.md');
mkdirSync(path.dirname(oldPath), { recursive: true });
writeFileSync(oldPath, 'old\n');
execFileSync('git', ['add', '.'], { cwd: repo });
execFileSync('git', ['commit', '-qm', 'base'], { cwd: repo });
const base = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim();
writeFileSync(oldPath, 'changed\n');
const addedPath = path.join(repo, ...validAdmin().split('/'));
writeFileSync(addedPath, 'new\n');
execFileSync('git', ['add', '.'], { cwd: repo });
execFileSync('git', ['commit', '-qm', 'head'], { cwd: repo });
const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim();
const raw = execFileSync('git', ['diff', '--name-status', '-z', '--find-renames', base, head], { cwd: repo });
const result = runValidation(parseNameStatusZ(raw), SHANGHAI_NOW);
assert.equal(result.checkedCount, 1);
assert.equal(result.errors.length, 0);
} finally {
rmSync(repo, { recursive: true, force: true });
}
});
test('production CLI selects PR/push revisions safely and preserves exit-code semantics', async (t) => {
const repo = mkdtempSync(path.join(tmpdir(), 'hl-changelog-cli-'));
const eventPath = path.join(repo, 'event.json');
function git(...args) {
return execFileSync('git', args, { cwd: repo, encoding: 'utf8' }).trim();
}
function runCli(args) {
return execFileSync(process.execPath, [CLI_PATH, ...args], {
cwd: repo,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
}
function runCliResult(args) {
try {
return { status: 0, stdout: runCli(args), stderr: '' };
} catch (error) {
return {
status: error.status,
stdout: String(error.stdout ?? ''),
stderr: String(error.stderr ?? ''),
};
}
}
function writeEvent(value) {
writeFileSync(eventPath, typeof value === 'string' ? value : JSON.stringify(value));
}
try {
git('init', '-q');
git('config', 'user.email', 'test@example.com');
git('config', 'user.name', 'Gate Test');
git('config', 'core.autocrlf', 'false');
writeFileSync(path.join(repo, 'README.md'), 'base\n');
git('add', '.');
git('commit', '-qm', 'base');
const base = git('rev-parse', 'HEAD');
const today = getShanghaiDate(new Date());
const validPath = validAdmin({ month: today.yearMonth, day: today.day });
const addedPath = path.join(repo, ...validPath.split('/'));
mkdirSync(path.dirname(addedPath), { recursive: true });
writeFileSync(addedPath, 'valid\n');
git('add', '.');
git('commit', '-qm', 'valid');
const validHead = git('rev-parse', 'HEAD');
const wrongDay = today.day === '01' ? '02' : '01';
const invalidPath = validAdmin({ month: today.yearMonth, day: wrongDay, issue: '5162' });
writeFileSync(path.join(repo, ...invalidPath.split('/')), 'invalid\n');
git('add', '.');
git('commit', '-qm', 'invalid');
const invalidHead = git('rev-parse', 'HEAD');
await t.test('PR event uses base...head and returns 0 for a legal addition', () => {
writeEvent({ pull_request: { base: { sha: base }, head: { sha: validHead } } });
const result = runCliResult(['--event', eventPath]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /validated 1 controlled changelog path/);
});
await t.test('push event uses before/after and returns 0 for a legal addition', () => {
writeEvent({ before: base, after: validHead });
const result = runCliResult(['--event', eventPath]);
assert.equal(result.status, 0, result.stderr);
});
await t.test('new-branch push uses the empty tree', () => {
writeEvent({ before: '0'.repeat(base.length), after: validHead });
const result = runCliResult(['--event', eventPath]);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /validated 1 controlled changelog path/);
});
await t.test('rule failure returns 1', () => {
writeEvent({ pull_request: { base: { sha: validHead }, head: { sha: invalidHead } } });
const result = runCliResult(['--event', eventPath]);
assert.equal(result.status, 1);
assert.match(result.stderr, /\[E_DAY\]/);
});
await t.test('missing event fields return 2', () => {
writeEvent({ pull_request: { base: { sha: base }, head: {} } });
assert.equal(runCliResult(['--event', eventPath]).status, 2);
});
await t.test('invalid JSON returns 2', () => {
writeEvent('{not-json');
assert.equal(runCliResult(['--event', eventPath]).status, 2);
});
await t.test('nonexistent event commit returns 2', () => {
writeEvent({ before: base, after: 'f'.repeat(base.length) });
assert.equal(runCliResult(['--event', eventPath]).status, 2);
});
await t.test('a Git option cannot be injected as a local ref', () => {
const result = runCliResult(['--base', '--output=gate-review', '--head', 'HEAD']);
assert.equal(result.status, 2);
assert.equal(existsSync(path.join(repo, 'gate-review')), false);
assert.equal(readdirSync(repo).some((name) => name.startsWith('gate-review')), false);
});
} finally {
rmSync(repo, { recursive: true, force: true });
}
});

某些文件未显示,因为此 diff 中更改的文件太多 显示更多