比较提交
162
次代码提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
3c099dca2f | ||
|
|
2ae5886d43 | ||
|
|
ac06376f48 | ||
|
|
b140269132 | ||
|
|
0d1872c50a | ||
|
|
9fec9cd3da | ||
|
|
7917826ea3 | ||
|
|
a72b685243 | ||
|
|
77725caf28 | ||
|
|
11670fc4f6 | ||
|
|
3fe95b157f | ||
|
|
67b27b8435 | ||
|
|
31f00892a8 | ||
|
|
3173f4f567 | ||
|
|
f017ea044d | ||
|
|
dacf76047a | ||
|
|
eb1fc06e4e | ||
|
|
c624775c5c | ||
|
|
cd6dce1d74 | ||
|
|
6b7c9d9110 | ||
|
|
3493363276 | ||
|
|
8864373064 | ||
|
|
68d34e7e7f | ||
|
|
1faefa4d08 | ||
|
|
2f4e5ad842 | ||
|
|
096d02ccc3 | ||
|
|
b5d1f0add8 | ||
|
|
86e4be1cf0 | ||
|
|
31b640c655 | ||
|
|
e1d4d85e08 | ||
|
|
58fc5437e6 | ||
|
|
1f53bdd6cb | ||
|
|
a817262898 | ||
|
|
31573fcdf5 | ||
|
|
bc682e1a78 | ||
|
|
991bdd62d9 | ||
|
|
9333180fb7 | ||
|
|
a4a559e3c5 | ||
|
|
fb7804dc6a | ||
|
|
52b1d25ebd | ||
|
|
ea83186570 | ||
|
|
170d3b6208 | ||
|
|
3b94480241 | ||
|
|
2e0e840e24 | ||
|
|
01b67cbeac | ||
|
|
71b931226c | ||
|
|
52b50b2a38 | ||
|
|
57322565b2 | ||
|
|
f7a0b9b539 | ||
|
|
f40d83efc5 | ||
|
|
6257bf2df9 | ||
|
|
3b7d1bdc9e | ||
|
|
679b455450 | ||
|
|
69953f5aec | ||
|
|
1e0685a4fe | ||
|
|
450c9a6dfc | ||
|
|
991d9bb641 | ||
|
|
042ee2f396 | ||
|
|
ed20130660 | ||
|
|
1cd35d4c8f | ||
|
|
2590cc25eb | ||
|
|
de05bab5f2 | ||
|
|
7d76ee0cde | ||
|
|
89248fea03 | ||
|
|
8083139dc6 | ||
|
|
e816810210 | ||
|
|
446721c542 | ||
|
|
b4879f96c9 | ||
|
|
e49ef47ce9 | ||
|
|
f4f851bb74 | ||
|
|
017cc5cadc | ||
|
|
211cd7952d | ||
|
|
2e6dac729f | ||
|
|
3cb62cf417 | ||
|
|
5c7fa5dbe6 | ||
|
|
e35ff35c58 | ||
|
|
c021d338b7 | ||
|
|
ec3986cd7e | ||
|
|
c3e6cd38c2 | ||
|
|
248493b0f5 | ||
|
|
228b573705 | ||
|
|
6e40fa96de | ||
|
|
73d6f843ef | ||
|
|
ba8a01befb | ||
|
|
a4f468a732 | ||
|
|
699af1531f | ||
|
|
f4161862c2 | ||
|
|
cb52c7918e | ||
|
|
14482b6a22 | ||
|
|
6c23a7367f | ||
|
|
de960761df | ||
|
|
ee1c4a3167 | ||
|
|
d0d0d87fec | ||
|
|
551d0564d0 | ||
|
|
7c5301db9f | ||
|
|
da2707f63b | ||
|
|
4a3ec16a3d | ||
|
|
ffd0983958 | ||
|
|
f52ef24dc1 | ||
|
|
12a331d7c7 | ||
|
|
86059ce5f3 | ||
|
|
272b23b251 | ||
|
|
54ee10aa8d | ||
|
|
b931167499 | ||
|
|
52d9b49daa | ||
|
|
3ffef86227 | ||
|
|
120aee2b40 | ||
|
|
e38807ccc8 | ||
|
|
bd7f5a5e19 | ||
|
|
85caef620c | ||
|
|
23ef052327 | ||
|
|
f8a161fdf6 | ||
|
|
0e95ebd933 | ||
|
|
07990e1578 | ||
|
|
6cab15bab7 | ||
|
|
cbabad9a44 | ||
|
|
8d5c6943a5 | ||
|
|
b883ce7aa8 | ||
|
|
236cf8ff19 | ||
|
|
e1f5c2b9a2 | ||
|
|
ec0ec9ede1 | ||
|
|
a7b750cede | ||
|
|
736a17f084 | ||
|
|
cdeb340e7c | ||
|
|
86d9356d72 | ||
|
|
c7c8a7299c | ||
|
|
bae6a58183 | ||
|
|
ed0a97326f | ||
|
|
fe3354cce2 | ||
|
|
8e0c18658f | ||
|
|
e64aa1a054 | ||
|
|
e66c76f96b | ||
|
|
3b4e095f26 | ||
|
|
e8fbd19fc5 | ||
|
|
1e2f39e76f | ||
|
|
4d7d0d5e5c | ||
|
|
93d013f1d4 | ||
|
|
9891fee76e | ||
|
|
7a3c0d70b7 | ||
|
|
4877596de1 | ||
|
|
a78b546cce | ||
|
|
f4ed055581 | ||
|
|
614cad68e6 | ||
|
|
12fa3b7489 | ||
|
|
e97a559737 | ||
|
|
1b0bbad5b4 | ||
|
|
7b66c073b9 | ||
|
|
00321aecdb | ||
|
|
00f6bfd4a5 | ||
|
|
2e6884262d | ||
|
|
1208626f69 | ||
|
|
55fe54afa0 | ||
|
|
e492376528 | ||
|
|
67bfaa3993 | ||
|
|
bb53e6f542 | ||
|
|
a808064c73 | ||
|
|
5e517a4503 | ||
|
|
402e6cb45b | ||
|
|
8493c75ab4 | ||
|
|
59470f7b4c | ||
|
|
246e999244 | ||
|
|
92093b7368 |
@@ -44,3 +44,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Validate changelog frontmatter
|
- name: Validate changelog frontmatter
|
||||||
run: npm run check:frontmatter -- --event "$GITHUB_EVENT_PATH"
|
run: npm run check:frontmatter -- --event "$GITHUB_EVENT_PATH"
|
||||||
|
|
||||||
|
- name: Validate changelog path aliases
|
||||||
|
run: npm run check:path-aliases
|
||||||
|
|||||||
+32
-138
@@ -1,85 +1,40 @@
|
|||||||
# 后端 API Changelog 推送与交接指南
|
# 后端 API Changelog 推送说明
|
||||||
|
|
||||||
> 本文可直接发送给后端同事。适用于 `wx/HL` 的管理后台与小程序接口变更。
|
接口发生新增、修改或删除时,在 `hl-api-changelog` 仓库提交一份 changelog。
|
||||||
|
|
||||||
## 一、什么时候必须推送 changelog
|
## 1. 放在哪里
|
||||||
|
|
||||||
以下变化需要 changelog:
|
- 管理后台:`changelogs-v2/YYYY-MM/`
|
||||||
|
- 小程序:`changelogs-v2-mp/YYYY-MM/`
|
||||||
|
|
||||||
- Controller 路径、HTTP 方法或权限边界变化;
|
文件名:
|
||||||
- DTO、VO、BO、Feign 请求或响应字段变化;
|
|
||||||
- 字段必填性、枚举、状态、金额、空值或兼容行为变化;
|
|
||||||
- 新增、修改、废弃或删除管理后台/小程序接口;
|
|
||||||
- 前端或其他调用方需要调整请求、解析或页面行为。
|
|
||||||
|
|
||||||
纯后端内部重构且外部契约完全不变时,可不创建;必须在工单中说明 `frontend_status: not_required` 的判断依据。
|
|
||||||
|
|
||||||
## 二、准备条件
|
|
||||||
|
|
||||||
1. 已有关联的合格 Gitea 工单。
|
|
||||||
2. 已确认目标端:
|
|
||||||
- 管理后台:`changelogs-v2/`
|
|
||||||
- 小程序端:`changelogs-v2-mp/`
|
|
||||||
3. 已确认变更类型:`新增接口`、`修改接口` 或 `删除接口`。
|
|
||||||
4. `D:/work2/hl-ui` 保持只读,不在前端仓库创建配合工单。
|
|
||||||
5. changelog 仓库使用独立任务分支或 worktree,不把其他线程的未跟踪文件一起提交。
|
|
||||||
|
|
||||||
## 三、生成草稿
|
|
||||||
|
|
||||||
预览:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
hl changelog draft 5205 "车务首页汇总状态补全" `
|
|
||||||
--repo D:/work2/HL-v3-worktrees/5205 `
|
|
||||||
--base dev-v3 `
|
|
||||||
--track v3 `
|
|
||||||
--consumer admin `
|
|
||||||
--change-type 修改接口
|
|
||||||
```
|
|
||||||
|
|
||||||
确认目标路径和检测到的 Controller/DTO/VO/Feign 文件后写入:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
hl changelog draft 5205 "车务首页汇总状态补全" `
|
|
||||||
--repo D:/work2/HL-v3-worktrees/5205 `
|
|
||||||
--base dev-v3 `
|
|
||||||
--track v3 `
|
|
||||||
--consumer admin `
|
|
||||||
--change-type 修改接口 `
|
|
||||||
--write
|
|
||||||
```
|
|
||||||
|
|
||||||
`--write` 会自动获取 `changelog` 单写租约。手工创建或修改 changelog 时,应先执行:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
hl resource acquire changelog --ticket 5205 --ttl 1800
|
|
||||||
```
|
|
||||||
|
|
||||||
## 四、文件名
|
|
||||||
|
|
||||||
管理后台:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
changelogs-v2/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除接口}-管理后台.md
|
DD_issue_业务标题-{新增接口|修改接口|删除接口}-{管理后台|小程序端}.md
|
||||||
```
|
```
|
||||||
|
|
||||||
小程序:
|
例如:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
changelogs-v2-mp/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除接口}-小程序端.md
|
changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理后台.md
|
||||||
```
|
```
|
||||||
|
|
||||||
年月日必须使用提交时 `Asia/Shanghai` 的真实日期。状态不得写入文件名,不要增加“前端待处理”“已完成”等额外片段。
|
日期使用提交时的上海日期;不要把“前端待处理”“已完成”等状态写进文件名。
|
||||||
|
|
||||||
## 五、填写 v2 元数据
|
## 2. 写什么
|
||||||
|
|
||||||
|
可以复制仓库根目录的 `CHANGELOG_TEMPLATE.md`,至少写清:
|
||||||
|
|
||||||
|
- 关联的 Issue 和后端 PR;
|
||||||
|
- 接口路径和 HTTP 方法;
|
||||||
|
- 新增、修改或删除的请求/响应字段;
|
||||||
|
- 字段必填性、枚举、状态、空值、金额和兼容规则;
|
||||||
|
- 前端需要做什么;
|
||||||
|
- 后端测试、部署和网关验证结果。
|
||||||
|
|
||||||
|
元数据中:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
|
||||||
schema: "hl-changelog/v2"
|
|
||||||
ticket: "5205"
|
|
||||||
title: "车务首页汇总状态补全"
|
|
||||||
consumer: "admin"
|
|
||||||
change_type: "修改接口"
|
|
||||||
backend_status: "deployed"
|
backend_status: "deployed"
|
||||||
gateway_status: "verified"
|
gateway_status: "verified"
|
||||||
frontend_status: "pending"
|
frontend_status: "pending"
|
||||||
@@ -87,37 +42,15 @@ frontend_owner: ""
|
|||||||
frontend_ref: ""
|
frontend_ref: ""
|
||||||
target_release: ""
|
target_release: ""
|
||||||
verified_at: ""
|
verified_at: ""
|
||||||
status_note: ""
|
|
||||||
updated_at: "2026-07-24"
|
|
||||||
base: "dev-v3"
|
|
||||||
---
|
|
||||||
```
|
```
|
||||||
|
|
||||||
规则:
|
- 需要前端修改:`frontend_status: "pending"`
|
||||||
|
- 不需要前端修改:`frontend_status: "not_required"`
|
||||||
|
- 后端不要代替前端填写 `implemented`、`released` 或 `verified`
|
||||||
|
|
||||||
- 自动草稿从 `backend_status: pending`、`gateway_status: pending` 开始。
|
## 3. 校验
|
||||||
- 后端实际部署完成后才能改为 `backend_status: deployed`。
|
|
||||||
- 经网关验证后填写 `gateway_status: verified`;确实无需网关验证时使用 `not_required`。
|
|
||||||
- 需要前端配合时初始化 `frontend_status: pending`。
|
|
||||||
- 不需要前端修改时使用 `frontend_status: not_required`。
|
|
||||||
- 后端不得代替前端填写 `implemented`、`released` 或 `verified`。
|
|
||||||
|
|
||||||
## 六、正文必须写清
|
在 `hl-api-changelog` 仓库执行:
|
||||||
|
|
||||||
- 关联 Issue 和 PR;
|
|
||||||
- 变更接口清单;
|
|
||||||
- 请求与响应字段;
|
|
||||||
- 枚举、状态、空值、ID 和金额规则;
|
|
||||||
- 老数据和兼容行为;
|
|
||||||
- 前端/调用方需要采取的动作;
|
|
||||||
- 定向测试、网关验证和兼容性证据;
|
|
||||||
- 不影响范围。
|
|
||||||
|
|
||||||
页面展示、列表、汇总、看板、状态标签或颜色变化,还必须在后端工单中准备展示矩阵,明确数据来源、状态范围、空态、颜色和守恒规则。
|
|
||||||
|
|
||||||
## 七、本地校验
|
|
||||||
|
|
||||||
在 changelog 仓库执行:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
npm test
|
npm test
|
||||||
@@ -125,57 +58,18 @@ npm run check:filenames -- --base origin/main --head HEAD
|
|||||||
npm run check:frontmatter -- --base origin/main --head HEAD
|
npm run check:frontmatter -- --base origin/main --head HEAD
|
||||||
```
|
```
|
||||||
|
|
||||||
单文件还可以执行:
|
确保正文没有 `TODO`、`待补充` 或模板占位符。
|
||||||
|
|
||||||
```powershell
|
## 4. 提交和推送
|
||||||
hl changelog lint D:/path/changelog.md
|
|
||||||
```
|
|
||||||
|
|
||||||
发布前 lint 允许前端仍是 `pending`,但要求:
|
只暂存本次 changelog 文件:
|
||||||
|
|
||||||
- `backend_status: deployed`;
|
|
||||||
- `gateway_status` 不再是 `pending`;
|
|
||||||
- 正文不存在 `TODO`、`待补充` 或模板占位符。
|
|
||||||
|
|
||||||
## 八、提交和推送
|
|
||||||
|
|
||||||
只暂存本任务文件,禁止使用会卷入其他线程文件的宽泛命令:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git status --short
|
git status --short
|
||||||
git add -- changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理后台.md
|
git add -- changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理后台.md
|
||||||
git diff --cached --check
|
git diff --cached --check
|
||||||
git commit -m "docs: hand off fleet dashboard contract (#5205)"
|
git commit -m "docs: hand off API contract (#5205)"
|
||||||
git push -u origin <任务分支>
|
git push -u origin <任务分支>
|
||||||
```
|
```
|
||||||
|
|
||||||
随后向 `main` 创建 PR。合并前再次检查上海日期;跨越上海零点且仍未合并时,按贡献规则重命名为当天日期。
|
然后向 `main` 创建 PR。不要提交其他任务的 changelog、`.tmp-*` 文件或任何凭据。
|
||||||
|
|
||||||
不要直接提交:
|
|
||||||
|
|
||||||
- 其他线程的 changelog;
|
|
||||||
- `.tmp-*` 文件;
|
|
||||||
- token、密码、证书、真实隐私数据;
|
|
||||||
- `hl-ui` 代码。
|
|
||||||
|
|
||||||
## 九、回写后端任务
|
|
||||||
|
|
||||||
合并后在后端工单和任务台账记录:
|
|
||||||
|
|
||||||
- changelog 文件路径;
|
|
||||||
- changelog 提交或 PR;
|
|
||||||
- 当前 `frontend_status`;
|
|
||||||
- 后端部署和网关验证证据。
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
hl task update 5205 `
|
|
||||||
--changelog D:/path/changelog.md
|
|
||||||
```
|
|
||||||
|
|
||||||
后端工单可以按后端验收范围关闭;前端继续在同一 changelog 中推进消费状态。
|
|
||||||
|
|
||||||
手工持有租约时,完成后释放:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
hl resource release changelog --ticket 5205
|
|
||||||
```
|
|
||||||
|
|||||||
+12
-1
@@ -30,7 +30,9 @@ changelogs-v2-mp/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除
|
|||||||
检测器读取 `git diff --name-status -z --find-renames` 的结果,只校验本次 diff 新出现的目标路径:
|
检测器读取 `git diff --name-status -z --find-renames` 的结果,只校验本次 diff 新出现的目标路径:
|
||||||
|
|
||||||
- `A`(新增)、`C`(复制)和 `R`(重命名)的目标路径必须通过规则。
|
- `A`(新增)、`C`(复制)和 `R`(重命名)的目标路径必须通过规则。
|
||||||
- `M`(修改历史文件)和 `D`(删除)豁免,不会因存量错误命名阻断。
|
- `M`(修改历史文件)豁免,不会因存量错误命名阻断。
|
||||||
|
- 已发布文件的 `D`(删除)和 `R`(重命名)默认以 `E_PATH_STABILITY` 阻断。确需迁移时,必须在
|
||||||
|
`changelog-path-aliases.json` 登记旧路径到 canonical 的精确关系,并保留可读取的兼容入口。
|
||||||
- 重命名到受控目录时,新目标路径必须使用校验当天的上海日期。
|
- 重命名到受控目录时,新目标路径必须使用校验当天的上海日期。
|
||||||
|
|
||||||
本地校验:
|
本地校验:
|
||||||
@@ -38,6 +40,7 @@ changelogs-v2-mp/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除
|
|||||||
```bash
|
```bash
|
||||||
npm test
|
npm test
|
||||||
npm run check:filenames -- --base origin/main --head HEAD
|
npm run check:filenames -- --base origin/main --head HEAD
|
||||||
|
npm run check:path-aliases
|
||||||
```
|
```
|
||||||
|
|
||||||
生产 CLI 故意不提供 `--date` 或日期环境变量;测试只通过导出的纯函数注入 `Date`。规则失败返回退出码 `1`,Git/事件/参数等基础设施错误返回 `2`。
|
生产 CLI 故意不提供 `--date` 或日期环境变量;测试只通过导出的纯函数注入 `Date`。规则失败返回退出码 `1`,Git/事件/参数等基础设施错误返回 `2`。
|
||||||
@@ -70,12 +73,20 @@ pending → claimed → implemented → released → verified
|
|||||||
- `FRONTEND_CONSUMPTION_STATUS_GUIDE.md`
|
- `FRONTEND_CONSUMPTION_STATUS_GUIDE.md`
|
||||||
- `BACKEND_CHANGELOG_DELIVERY_GUIDE.md`
|
- `BACKEND_CHANGELOG_DELIVERY_GUIDE.md`
|
||||||
|
|
||||||
|
已下发路径是消费契约的一部分,不通过重命名表达状态。历史路径已发生迁移时:
|
||||||
|
|
||||||
|
- `changelog-path-aliases.json` 是机器可识别的唯一映射源;
|
||||||
|
- alias 文件必须保留完整 `hl-changelog/v2` frontmatter,并用 `canonical_path` 指向 canonical;
|
||||||
|
- 前端状态更新使用 `npm run changelog:transition -- <path> <status> ... --write`,命令会同时更新
|
||||||
|
canonical 与全部 alias;对同一状态和证据重复执行不会产生文件变更。
|
||||||
|
|
||||||
本地校验:
|
本地校验:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm test
|
npm test
|
||||||
npm run check:filenames -- --base origin/main --head HEAD
|
npm run check:filenames -- --base origin/main --head HEAD
|
||||||
npm run check:frontmatter -- --base origin/main --head HEAD
|
npm run check:frontmatter -- --base origin/main --head HEAD
|
||||||
|
npm run check:path-aliases
|
||||||
```
|
```
|
||||||
|
|
||||||
## CI 与服务端阻断边界
|
## CI 与服务端阻断边界
|
||||||
|
|||||||
@@ -49,6 +49,30 @@ not_required
|
|||||||
|
|
||||||
## 更新命令
|
## 更新命令
|
||||||
|
|
||||||
|
### 存在历史路径 alias 的文档
|
||||||
|
|
||||||
|
消费线程已经记录的路径不得因文件改名失效。先解析路径:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run changelog:resolve -- "changelogs-v2/2026-07/旧路径.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
状态回写统一使用 alias-aware 命令;传旧路径或 canonical 均会同时更新整组文件:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run changelog:transition -- `
|
||||||
|
"changelogs-v2/2026-07/旧路径.md" implemented `
|
||||||
|
--owner frontend-team `
|
||||||
|
--frontend-ref "mmg/hl-ui@abc1234" `
|
||||||
|
--write
|
||||||
|
```
|
||||||
|
|
||||||
|
相同状态和证据可以重复执行,第二次不会产生文件变更。alias 关系集中记录在
|
||||||
|
`changelog-path-aliases.json`,并由 `npm run check:path-aliases` 校验文件存在性、ticket、
|
||||||
|
canonical 指向及前端状态一致性。
|
||||||
|
|
||||||
|
### 无 alias 的文档
|
||||||
|
|
||||||
领取:
|
领取:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -107,3 +131,4 @@ QA 或产品可以协助更新 `verified_at`,但必须基于实际页面验证
|
|||||||
- `hl-changelog/v1` 继续可读和索引,不强制一次性迁移。
|
- `hl-changelog/v1` 继续可读和索引,不强制一次性迁移。
|
||||||
- 文件名带“前端待处理”不代表真实状态;需要继续流转时补充 v2 元数据。
|
- 文件名带“前端待处理”不代表真实状态;需要继续流转时补充 v2 元数据。
|
||||||
- 不通过重命名表达消费状态,避免破坏文件名校验和历史链接。
|
- 不通过重命名表达消费状态,避免破坏文件名校验和历史链接。
|
||||||
|
- 已被消费的路径如确需规范化,必须先登记 alias、保留兼容入口,并使用 alias-aware 命令同步状态。
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"schema": "hl-changelog-path-aliases/v1",
|
||||||
|
"aliases": []
|
||||||
|
}
|
||||||
+5
-1
@@ -5,7 +5,11 @@ title: "派车按行程日标记车费日期"
|
|||||||
consumer: "admin"
|
consumer: "admin"
|
||||||
backend: "verified"
|
backend: "verified"
|
||||||
gateway: "verified"
|
gateway: "verified"
|
||||||
frontend: "pending"
|
frontend: "implemented"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@5eb8a46bee9a5a101371313e2088decd9ea843f2"
|
||||||
|
updated_at: "2026-07-25T03:03:38.490Z"
|
||||||
base: "dev-v3"
|
base: "dev-v3"
|
||||||
generated: "2026-07-22T18:00:00+08:00"
|
generated: "2026-07-22T18:00:00+08:00"
|
||||||
---
|
---
|
||||||
+6
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@b309f1672f4587d11aa6b8e86d0dd4ba043274d1"
|
||||||
|
updated_at: "2026-07-25T03:11:01.515Z"
|
||||||
|
---
|
||||||
# 【前端待处理·管理后台】#5160 一名司机可绑定多辆常驻车
|
# 【前端待处理·管理后台】#5160 一名司机可绑定多辆常驻车
|
||||||
|
|
||||||
> **服务**: `hl-fleet-service`
|
> **服务**: `hl-fleet-service`
|
||||||
+6
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
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
|
> **服务**: hl-order-service-v3
|
||||||
+5
-1
@@ -5,7 +5,11 @@ title: "用车手动加急与派车看板状态颜色"
|
|||||||
consumer: "admin"
|
consumer: "admin"
|
||||||
backend: "verified"
|
backend: "verified"
|
||||||
gateway: "verified"
|
gateway: "verified"
|
||||||
frontend: "pending"
|
frontend: "implemented"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@d07506cd3aa8a23cb2aa90f891eb853e1b7dd13f"
|
||||||
|
updated_at: "2026-07-25T03:18:55.871Z"
|
||||||
base: "dev-v3"
|
base: "dev-v3"
|
||||||
generated: "2026-07-23T10:00:00+08:00"
|
generated: "2026-07-23T10:00:00+08:00"
|
||||||
---
|
---
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
---
|
---
|
||||||
schema: "hl-changelog/v1"
|
schema: "hl-changelog/v2"
|
||||||
ticket: "5187"
|
ticket: "5187"
|
||||||
title: "多车辆槽位原子批量派车与价格日历带价"
|
title: "多车辆槽位原子批量派车与价格日历带价"
|
||||||
consumer: "admin"
|
consumer: "admin"
|
||||||
backend: "verified"
|
change_type: "新增接口"
|
||||||
gateway: "verified"
|
backend_status: "deployed"
|
||||||
frontend: "pending"
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@6479adf1caf2a5caeea08a24a42bacecbaaabd6a"
|
||||||
|
target_release: "hl-ui/v2.1"
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "前端 v2.1 已实现按 fleetItemIndex 的多槽位选择、批量提交和重复车辆/司机禁选;测试环境 9527 已提供对应源码,尚待登录态页面实操验收。"
|
||||||
|
updated_at: "2026-07-24"
|
||||||
base: "dev-v3"
|
base: "dev-v3"
|
||||||
generated: "2026-07-23T15:38:00+08:00"
|
generated: "2026-07-23T15:38:00+08:00"
|
||||||
---
|
---
|
||||||
@@ -40,7 +47,7 @@ generated: "2026-07-23T15:38:00+08:00"
|
|||||||
- 同一次提交及其网络重试必须复用同一个 `requestId`;用户修改选择后主动再次提交应生成新值。
|
- 同一次提交及其网络重试必须复用同一个 `requestId`;用户修改选择后主动再次提交应生成新值。
|
||||||
- `holdMode=1` 表示排车中等待司机确认,`holdMode=0` 表示直接派定;整批模式必须一致。
|
- `holdMode=1` 表示排车中等待司机确认,`holdMode=0` 表示直接派定;整批模式必须一致。
|
||||||
|
|
||||||
## 二、新增原子批量派单接口
|
## 变更接口
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /admin/fleet/assignments/batch
|
POST /admin/fleet/assignments/batch
|
||||||
@@ -196,6 +203,30 @@ selectedSlots[fleetItemIndex] = {
|
|||||||
- 禁止用 `for` 循环调用旧单派接口;那会在中途失败时留下半批状态。
|
- 禁止用 `for` 循环调用旧单派接口;那会在中途失败时留下半批状态。
|
||||||
- 成功后一次关闭弹窗并刷新看板;不得每成功一辆刷新一次。
|
- 成功后一次关闭弹窗并刷新看板;不得每成功一辆刷新一次。
|
||||||
|
|
||||||
|
#### 当前消费差距
|
||||||
|
|
||||||
|
- `useVehicleDriverPicker.js` 仍只维护一组 `selVehicle/selDriver`。
|
||||||
|
- `AssignModalFooter.vue` 仍只展示一组车辆和司机,并按这一组决定按钮是否可用。
|
||||||
|
- `useAssignFlow.js` 仍只调用 `createAssignment`,没有构造 `items[]`。
|
||||||
|
- `src/api/fleet/board.js` 尚未封装 `POST /fleet/assignments/batch`。
|
||||||
|
|
||||||
|
#### 展示矩阵
|
||||||
|
|
||||||
|
| 场景 | “已选车辆”区域 | 候选/司机联动 | 主操作 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 尚未选择 | 显示 `已选车辆 0/N` 和 N 个待选槽位 | 提示先选择车辆 | 禁用,显示未完成组数 |
|
||||||
|
| 已选一辆 | 槽位 01 显示车牌、车型、司机和移除操作,并成为当前编辑槽位 | 已选车辆标记不可重复;司机只写入当前槽位 | 未完成全部槽位时保持禁用 |
|
||||||
|
| 继续多选 | 新车辆进入下一个待选 `fleetItemIndex`;其他已选槽位保持不变 | 已被其他槽位使用的车辆和司机不可重复选择 | 全部槽位完整后启用 |
|
||||||
|
| 切换槽位 | 高亮当前编辑槽位;允许单独更换车辆、司机和价格 | 候选与司机面板切换到该槽位上下文 | 完整度实时更新 |
|
||||||
|
| 搜索/筛选/翻页 | 已选区域固定可见,集合不丢失 | 只改变候选列表 | 状态保持 |
|
||||||
|
| HOLD 完整 | 显示 `已选择 N/N 辆,司机 N/N` | 每槽位独立司机 | `下一步 · 发送给 N 名司机` |
|
||||||
|
| DIRECT 完整 | 显示 `已选择 N/N 辆,司机 N/N` | 每槽位独立司机 | `直接派定 N 辆车` |
|
||||||
|
| 批量失败 | 保留全部选择;高亮 `failedFleetItemIndex` | 允许修正失败槽位 | 原批次不产生部分成功 |
|
||||||
|
| 批量成功 | 清空选择并关闭弹窗 | 看板只统一刷新一次 | 仅发送一次批量请求 |
|
||||||
|
|
||||||
|
“已选车辆”应作为车辆筛选与候选列表之间持续可见的紧凑区域,不得只在底栏显示最后一辆。
|
||||||
|
选择数量不得超过当前需求的待派车辆槽位数;移除某一槽位不得重排或清空其他槽位。
|
||||||
|
|
||||||
### 3.2 车型价格日历自动带价
|
### 3.2 车型价格日历自动带价
|
||||||
|
|
||||||
候选接口 `vehicles[].protocolPrice` 已返回所选车辆车型在服务开始日的价格日历单价。当前页面
|
候选接口 `vehicles[].protocolPrice` 已返回所选车辆车型在服务开始日的价格日历单价。当前页面
|
||||||
@@ -238,7 +269,7 @@ selectedSlots[fleetItemIndex] = {
|
|||||||
- [ ] 主动筛选“待派车”及重置行为正确。
|
- [ ] 主动筛选“待派车”及重置行为正确。
|
||||||
- [ ] 增加多槽位状态管理、批量请求映射、车型切换带价和默认空筛选的组件/组合式函数测试。
|
- [ ] 增加多槽位状态管理、批量请求映射、车型切换带价和默认空筛选的组件/组合式函数测试。
|
||||||
|
|
||||||
## 六、后端验证证据
|
## 验证证据
|
||||||
|
|
||||||
- `mvn -pl hl-fleet-service spotless:check` 通过。
|
- `mvn -pl hl-fleet-service spotless:check` 通过。
|
||||||
- `AssignmentControllerTest + AssignmentServiceTest`:281 项通过。
|
- `AssignmentControllerTest + AssignmentServiceTest`:281 项通过。
|
||||||
|
|||||||
+8
-1
@@ -5,7 +5,14 @@ title: "用车需求增加独立接机送机选择"
|
|||||||
consumer: "admin"
|
consumer: "admin"
|
||||||
backend: "verified"
|
backend: "verified"
|
||||||
gateway: "verified"
|
gateway: "verified"
|
||||||
frontend: "pending"
|
frontend: "not_required"
|
||||||
|
frontend_status: "not_required"
|
||||||
|
frontend_owner: ""
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "#5236 明确 supersedes #5193;85851ad6 已删除独立开关并改用实时大交通聚合,继续实现会回滚后续契约。"
|
||||||
|
updated_at: "2026-07-27"
|
||||||
base: "dev-v3"
|
base: "dev-v3"
|
||||||
generated: "2026-07-23T17:39:06+08:00"
|
generated: "2026-07-23T17:39:06+08:00"
|
||||||
---
|
---
|
||||||
+6
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
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
|
> 日期:2026-07-24
|
||||||
文件差异内容过多而无法显示
加载差异
@@ -5,7 +5,11 @@ title: "出行人省份与分批大交通关联"
|
|||||||
consumer: "admin"
|
consumer: "admin"
|
||||||
backend: "verified"
|
backend: "verified"
|
||||||
gateway: "verified"
|
gateway: "verified"
|
||||||
frontend: "pending"
|
frontend: "implemented"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@4424375ef9180e69f22a4f5f6b0b80c9ec2062b7"
|
||||||
|
updated_at: "2026-07-24T07:15:16.554Z"
|
||||||
base: "dev-v3"
|
base: "dev-v3"
|
||||||
generated: "2026-07-24T11:58:00+08:00"
|
generated: "2026-07-24T11:58:00+08:00"
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@06f9d4dce58ef64c3e5de96e44754c0793286d06"
|
||||||
|
updated_at: "2026-07-24T07:15:17.169Z"
|
||||||
|
---
|
||||||
# 车务:派单通知预览补齐接送与行程数据
|
# 车务:派单通知预览补齐接送与行程数据
|
||||||
|
|
||||||
> **服务**: hl-fleet-service(8087/8187)
|
> **服务**: hl-fleet-service(8087/8187)
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ title: "车务首页未完成状态独立汇总"
|
|||||||
consumer: "admin"
|
consumer: "admin"
|
||||||
backend: "verified"
|
backend: "verified"
|
||||||
gateway: "verified"
|
gateway: "verified"
|
||||||
frontend: "pending"
|
frontend: "implemented"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@a48846a3e35c06df3aef422e538d5cd198002c2c"
|
||||||
|
updated_at: "2026-07-24T07:15:17.828Z"
|
||||||
base: "dev-v3"
|
base: "dev-v3"
|
||||||
generated: "2026-07-24T14:25:00+08:00"
|
generated: "2026-07-24T14:25:00+08:00"
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
---
|
||||||
|
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: ""
|
||||||
|
path_aliases: "changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-前端待处理-管理后台.md"
|
||||||
|
status_note: "后端与网关已验证;前端 implemented 状态由前端消费线程维护,本次仅迁移 schema。"
|
||||||
|
updated_at: "2026-07-26"
|
||||||
|
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,167 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5253"
|
||||||
|
title: "按车辆记录订单总车费并接入核单"
|
||||||
|
consumer: "multiple"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@e43200d6eede059f82aacfa00de5656b379f8981"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "后端已合入 dev-v3 并部署测试环境;自动计价、手工总价、缺价阻断、部分改派分段、核单实时合计与冻结均已通过网关验证。前端尚未领取,frontend_status 保持 pending。"
|
||||||
|
updated_at: "2026-07-26T04:47:17.025Z"
|
||||||
|
base: "dev-v3"
|
||||||
|
generated: "2026-07-26T08:57:17+08:00"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 按车辆记录订单总车费并接入核单
|
||||||
|
|
||||||
|
车务在每个车辆槽位/派车段记录一个最终总车费;价格日历只提供自动参考,
|
||||||
|
车务可以手工改总价但不回写价格日历。订单核单冻结 Fleet 最终快照并按同一来源计入实际成本。
|
||||||
|
|
||||||
|
## 关联
|
||||||
|
|
||||||
|
- Issue: #5253
|
||||||
|
- 后端分支: `feat/5253-vehicle-total-fee`
|
||||||
|
- Changelog 分支: `docs/5253-vehicle-total-fee`
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
| 表面 | 方法 | 路径 | 变化 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `frontend_api` | `POST` | `/admin/fleet/assignments/candidates` | 入参新增计费服务日;车辆候选新增自动总车费、完整性和缺价日期 |
|
||||||
|
| `frontend_api` | `POST` | `/admin/fleet/assignments` | 入参新增单车最终总车费、调整原因、计费日/免费日口径;响应回显费用快照 |
|
||||||
|
| `frontend_api` | `POST` | `/admin/fleet/assignments/batch` | 每辆车分别提交最终总车费与调整原因,并共享计费日/免费日口径 |
|
||||||
|
| `frontend_api` | `POST` | `/admin/fleet/assignments/:assignmentId/confirm` | 最终确认前可补/改该派车段总车费;响应回显最终费用 |
|
||||||
|
| `frontend_api` | `POST` | `/admin/fleet/assignments/:assignmentId/change` | 改派新段与原车保留段分别记录总车费 |
|
||||||
|
| `frontend_api` | `GET` | `/admin/fleet/board/orders/:orderId` | 每个派车组新增自动参考、最终总价、来源、调整原因、计费/免费日期与冻结态 |
|
||||||
|
| `frontend_api` | `GET` | `/v3/admin/order/:orderId/settlement/vehicle-fees` | 新增核单车辆总车费明细;冻结前读 Fleet,冻结后读 Order 快照 |
|
||||||
|
| `frontend_api` | `POST` | `/v3/admin/order/:orderId/settlement/vehicle-fees/confirm` | 新增确认并冻结车辆总车费 |
|
||||||
|
| `frontend_api` | `POST` | `/v3/admin/order/:orderId/settlement/step6/submit` | 响应新增 `vehicleCost` |
|
||||||
|
| `frontend_api` | `GET` | `/v3/admin/order/:orderId/settlement/summary` | 响应新增 `vehicleCost` |
|
||||||
|
| `internal_feign` / `shared_java` | `GET` | `/internal/fleet/orders/:orderId/vehicle-fees` | 新增 Fleet→Order 的按派车组只读费用快照 |
|
||||||
|
|
||||||
|
路径中的 `:orderId`、`:assignmentId` 表示既有雪花 ID 字符串传输约定。
|
||||||
|
|
||||||
|
## 字段与行为
|
||||||
|
|
||||||
|
### 派车候选与写接口
|
||||||
|
|
||||||
|
- `AssignmentCandidateReqVO` 新增 `chargeableServiceDates`;不传默认全部服务日,
|
||||||
|
空数组表示全部免费。
|
||||||
|
- 候选车辆新增:
|
||||||
|
- `autoVehicleFeeTotal`:价格日历覆盖日期的小计,金额按字符串消费;
|
||||||
|
- `vehicleFeePriceComplete`:是否覆盖全部计费服务日;
|
||||||
|
- `missingVehicleFeeDates`:缺价的计费服务日;
|
||||||
|
- 旧字段 `protocolPrice` 保留但已废弃,新页面不得用它计算总车费。
|
||||||
|
- 创建/批量创建/改派新增 `vehicleFeeTotal`、`vehicleFeeAdjustmentReason`、
|
||||||
|
`chargeableServiceDates`、`vehicleFeeWaiverReason`、
|
||||||
|
`confirmAllServiceDatesFree`。
|
||||||
|
- 部分改派额外新增 `retainedVehicleFeeTotal` 与
|
||||||
|
`retainedVehicleFeeAdjustmentReason`。原车保留段和替换段分别录入,
|
||||||
|
后端不按天数比例拆分。
|
||||||
|
- 最终确认新增 `vehicleFeeTotal` 与 `vehicleFeeAdjustmentReason`。
|
||||||
|
- 创建、改派和确认响应新增 `vehicleFeeAutoTotal`、
|
||||||
|
`vehicleFeeAutoComplete`、`vehicleFeeTotal`、`vehicleFeeSource`、
|
||||||
|
`vehicleFeeAdjustmentReason`;金额字段按字符串消费。
|
||||||
|
|
||||||
|
### 看板详情
|
||||||
|
|
||||||
|
每个派车组新增:
|
||||||
|
|
||||||
|
- `chargeableServiceDates`、`freeServiceDates`、`vehicleFeeWaiverReason`;
|
||||||
|
- `vehicleFeeAutoTotal`、`vehicleFeeAutoComplete`;
|
||||||
|
- `vehicleFeeTotal`、`vehicleFeeSource`(`AUTO`、`MANUAL`、`INCOMPLETE`);
|
||||||
|
- `vehicleFeeAdjustmentReason`、`vehicleFeeFrozen`。
|
||||||
|
|
||||||
|
派车完成后 `vehicleFeeFrozen=true`,不得原地编辑费用;只能走改派形成新的费用段。
|
||||||
|
订单核单完成后禁止继续改派。
|
||||||
|
|
||||||
|
### 核单车辆总车费
|
||||||
|
|
||||||
|
`SettlementVehicleFeesRespVO`:
|
||||||
|
|
||||||
|
- 顶层:`orderId`、`frozen`、`totalVehicleFee`、`items`;
|
||||||
|
- 每项按一个派车组返回车辆、司机、日期、计费/免费日期、自动参考、
|
||||||
|
最终总价、来源、调整审计和 `settlementReady`;
|
||||||
|
- `POST .../confirm` 仅接受全部当前派车组 `settlementReady=true`
|
||||||
|
且最终总车费非空的快照,成功后 `frozen=true`;
|
||||||
|
- 两辆车返回两项并分别计费,`totalVehicleFee` 是各项 `vehicleFeeTotal` 之和;
|
||||||
|
- `vehicleCost` 同步进入核单提交与汇总实际成本,司机费用只保留司机额外费用,
|
||||||
|
避免车辆基础服务费重复计入。
|
||||||
|
|
||||||
|
### 内部契约
|
||||||
|
|
||||||
|
新增共享 DTO `OrderVehicleFeeSnapshotDTO`,包含需求、派车组、稳定车辆槽位、
|
||||||
|
车辆/司机、服务区间、计费/免费日期、自动参考、最终总价、调整审计和
|
||||||
|
`settlementReady`。Order 必须按当前 `requirementId` 精确筛选,
|
||||||
|
不得跨需求或跨改派段合并。
|
||||||
|
|
||||||
|
## 契约影响文件
|
||||||
|
|
||||||
|
- `hl-common/hl-common-core/src/main/java/com/hulalv/common/dto/fleet/OrderVehicleFeeSnapshotDTO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/controller/OrderDriverVehicleInternalController.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/AssignmentCandidateReqVO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/AssignmentCandidateRespVO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/AssignmentWriteRespVO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/BatchCreateAssignmentReqVO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/ChangeAssignmentReqVO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/ChangeAssignmentRespVO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/ConfirmReqVO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/ConfirmRespVO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/assignment/vo/CreateAssignmentReqVO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/board/vo/BoardOrderDetailVO.java`
|
||||||
|
- `hl-fleet-service/src/test/java/com/hulalv/fleet/assignment/controller/OrderDriverVehicleInternalControllerTest.java`
|
||||||
|
- `hl-order-service-v3/src/main/java/com/hulalv/order/fleet/feign/FleetDriverVehicleFeignClient.java`
|
||||||
|
- `hl-order-service-v3/src/main/java/com/hulalv/order/fleet/feign/FleetDriverVehicleFeignFallbackFactory.java`
|
||||||
|
- `hl-order-service-v3/src/main/java/com/hulalv/order/settlement/controller/admin/SettlementController.java`
|
||||||
|
- `hl-order-service-v3/src/main/java/com/hulalv/order/settlement/controller/admin/vo/SettlementSubmitRespVO.java`
|
||||||
|
- `hl-order-service-v3/src/main/java/com/hulalv/order/settlement/controller/admin/vo/SettlementSummaryRespVO.java`
|
||||||
|
- `hl-order-service-v3/src/main/java/com/hulalv/order/settlement/controller/admin/vo/SettlementVehicleFeesRespVO.java`
|
||||||
|
- `hl-order-service-v3/src/test/java/com/hulalv/order/fleet/feign/FleetDriverVehicleFeignContractTest.java`
|
||||||
|
- `hl-order-service-v3/src/test/java/com/hulalv/order/settlement/controller/admin/SettlementControllerTest.java`
|
||||||
|
|
||||||
|
## 前端/调用方动作
|
||||||
|
|
||||||
|
- 派车弹窗按“每辆车一个总车费”展示和提交;不要展示或要求车务填写每日价格。
|
||||||
|
- 默认展示后端 `autoVehicleFeeTotal`。缺价时用
|
||||||
|
`vehicleFeePriceComplete=false` 和 `missingVehicleFeeDates` 提示,
|
||||||
|
车务仍必须填写该车最终总车费及调整原因后才能直接派定/最终确认。
|
||||||
|
- 车务手改只提交 `vehicleFeeTotal`,不得写回车型价格日历。
|
||||||
|
- 收费日/免费日仅作为记录与核单依据;全部免费时必须提交免费原因和二次确认。
|
||||||
|
- 多车订单为每个车辆槽位分别编辑总车费,不提供订单级总价输入框。
|
||||||
|
- 已派定/已完成段按 `vehicleFeeFrozen` 禁用直接编辑,只保留改派入口;
|
||||||
|
已核单订单同时禁用改派。
|
||||||
|
- 核单页先查询 `GET .../vehicle-fees` 展示逐车明细和合计,
|
||||||
|
再调用 `POST .../vehicle-fees/confirm` 冻结;冻结后只读。
|
||||||
|
- 所有雪花 ID 和金额字段按字符串处理,禁止转 JavaScript `number`。
|
||||||
|
- `frontend_status` 保持 `pending`;真实前端领取后使用工作流迁移到 `claimed`。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- Fleet producer:`mvn -pl hl-fleet-service -am verify` 通过;
|
||||||
|
`mvn -pl hl-fleet-service -am spotless:check` 通过。
|
||||||
|
- Order consumer:`mvn -pl hl-order-service-v3 -am verify` 通过。
|
||||||
|
- 契约定向测试:
|
||||||
|
`OrderDriverVehicleInternalControllerTest`、
|
||||||
|
`FleetDriverVehicleFeignContractTest`、
|
||||||
|
`SettlementControllerTest` 通过。
|
||||||
|
- 费用行为覆盖:自动合计、缺价、手工覆盖、全免费、部分改派、
|
||||||
|
派定后冻结、核单冻结、首个计费日只入账一次及并发门禁均有测试。
|
||||||
|
- OpenAPI/oasdiff:`not_configured`;fallback 证据见任务胶囊
|
||||||
|
`openapi-fallback.md`。
|
||||||
|
- producer/consumer 或 Spring Cloud Contract:`not_configured`;
|
||||||
|
fallback 证据见任务胶囊 `consumer-contract-fallback.md`。
|
||||||
|
- 测试部署:Fleet Deploy Panel 任务 `5131f604` 成功,8087/8187
|
||||||
|
双实例健康;补充修复 PR `wx/HL#5261` 已合入 `dev-v3`。
|
||||||
|
- 网关验证:自动计价完整;缺价返回 `605044` 阻断;多车分别保存
|
||||||
|
`1720.00` 与 `1234.56`;部分改派同一稳定槽位拆为原车保留段
|
||||||
|
`900.00` 与新车接替段 `1100.00`,核单实时合计 `2000.00`;
|
||||||
|
核单确认后冻结快照只读。`gateway_status=verified`。
|
||||||
|
- 测试数据:隔离订单已按 `hl-data-cleanup/v1` manifest
|
||||||
|
`5253-cfcc4c8eea2b` 事务清理,全部目标后置计数为 0,价格日历未变化。
|
||||||
|
- 兼容性结论:现有路径均为增量字段;旧 `protocolPrice` 继续返回,
|
||||||
|
但新页面必须改用每车总费用字段。
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5255"
|
||||||
|
title: "已派车恢复改派与行程单入口更正"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@f9251d0ffabf13641630588733d103e00f35e85d"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "后端已部署并完成网关验证;前端需恢复已派车改派按钮,并将看板行程单入口直接复用订单详情现有打印实现。本文件覆盖 #5186 的已派车禁用改派及复制司机 H5 链接口径。"
|
||||||
|
updated_at: "2026-07-26T03:32:07.805Z"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 车务:已派车恢复改派与行程单入口更正
|
||||||
|
|
||||||
|
> **服务**: `hl-fleet-service`
|
||||||
|
>
|
||||||
|
> **工单**: [wx/HL#5255](https://git.1814.love:8443/wx/HL/issues/5255)
|
||||||
|
>
|
||||||
|
> **后端 PR**: [wx/HL#5258](https://git.1814.love:8443/wx/HL/pulls/5258)
|
||||||
|
>
|
||||||
|
> **影响范围**: 车务管理 → 派单看板操作区、既有派车/改派弹窗、行程单打印入口
|
||||||
|
|
||||||
|
## 关键变化
|
||||||
|
|
||||||
|
- 当前有效派单状态为 `assigned` 时,派单看板列表现在返回 `canAssign=true`,并继续下发
|
||||||
|
`CHANGE_ASSIGNMENT`。车辆已经配好后,只要派单尚未进入 `completed/canceled` 终态,车务仍可
|
||||||
|
随时进入既有改派流程,再次更换当前车辆槽位的车辆、司机或两者。
|
||||||
|
- `holding/holding_urgent` 的改派能力不变;`completed/canceled` 继续不可改派。
|
||||||
|
- 看板操作区的行程单入口统一为“打印行程单”,直接复用订单详情已有的
|
||||||
|
`PrintItineraryModal.vue` 和 `getPrintItinerary(orderId)`,不新写打印组件、打印接口或打印数据模型。
|
||||||
|
|
||||||
|
## ⚠️ 对 #5186 的口径更正
|
||||||
|
|
||||||
|
本文件取代
|
||||||
|
`23_5186_排车中订单恢复派车派人入口-修改接口-管理后台.md`
|
||||||
|
中的以下两项旧交接:
|
||||||
|
|
||||||
|
1. #5186 响应矩阵把 `assigned` 与终态一起写成 `canAssign=false`。该口径已失效:
|
||||||
|
`assigned` 现在允许改派,只有 `completed/canceled` 禁止改派。
|
||||||
|
2. #5186 要求看板复制 `currentAssignment.itineraryUrl`,并明确“不复用
|
||||||
|
`PrintItineraryModal.vue`”。该口径已撤销:看板不再把“复制司机 H5 链接”作为本入口的实现,
|
||||||
|
而是直接复用订单详情现有打印代码。
|
||||||
|
|
||||||
|
#5186 中与本次两项更正无关的候选上下文、稳定槽位和待确认流程说明继续有效。本次也不删除后端
|
||||||
|
司机 H5 短链能力;只是看板这个用户入口不再消费它。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 本次口径 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET` | `/admin/fleet/board/orders` | 既有字段语义更正:有效 `assigned` 记录返回 `canAssign=true`,且 `availableActionCodes` 包含 `CHANGE_ASSIGNMENT` |
|
||||||
|
| `POST` | `/admin/fleet/assignments/<assignmentId>/change` | 既有改派接口,路径和请求/响应契约不变;继续按稳定车辆槽位与生效日原子替换 |
|
||||||
|
| `GET` | `/v3/admin/order/<orderId>/print-itinerary` | 订单详情已有打印接口,本次无后端变更;看板直接复用现有前端调用 |
|
||||||
|
|
||||||
|
## 1. 派单看板动作语义
|
||||||
|
|
||||||
|
`GET /admin/fleet/board/orders` 的字段名、类型和必填性均未变化。前端按后端动作字段渲染:
|
||||||
|
|
||||||
|
| `assignmentStatus` | `canAssign` | `availableActionCodes` | 看板主操作 |
|
||||||
|
| --- | ---: | --- | --- |
|
||||||
|
| `unassigned/unassigned_urgent` | `true` | 包含 `ASSIGN` | “派车派人”,进入既有首次派车流程 |
|
||||||
|
| `holding/holding_urgent` | `true` | 包含 `CHANGE_ASSIGNMENT` | “改派”,进入既有改派流程 |
|
||||||
|
| `assigned` | `true` | 包含 `CHANGE_ASSIGNMENT` | “改派”,已配车后仍可再次调整 |
|
||||||
|
| `completed/canceled` | `false` | 不包含 `CHANGE_ASSIGNMENT` | 不显示改派入口 |
|
||||||
|
|
||||||
|
前端不得再把“已有车辆/司机”或 `assignmentStatus === 'assigned'` 当作隐藏改派按钮的条件。
|
||||||
|
入口首先以 `canAssign === true` 判断是否可进入,再以 `availableActionCodes` 区分首次派车或改派。
|
||||||
|
|
||||||
|
## 2. 继续复用既有按槽位改派
|
||||||
|
|
||||||
|
点击“改派”后继续使用当前派车弹窗和现有
|
||||||
|
`POST /admin/fleet/assignments/<assignmentId>/change`:
|
||||||
|
|
||||||
|
- `assignmentId` 取用户选中的 `activeAssignments[]` 派车组/槽位,不得固定取代表项后误改其它车;
|
||||||
|
- 生效日及之后只替换该稳定 `assignmentSlotId` 的逐日切片;
|
||||||
|
- 同一订单其它车辆槽位保持不变;
|
||||||
|
- 可只换车辆、只换司机或同时替换;
|
||||||
|
- 成功后重新拉取看板列表与详情,不缓存旧的 `canAssign`、动作码或派车组数据;
|
||||||
|
- 后端返回 `ORDER_HAS_OTHER_VEHICLES` 等既有警告时继续沿用当前强提示。
|
||||||
|
|
||||||
|
本次没有新增写接口,也没有修改改派请求字段、错误码或事务规则。
|
||||||
|
|
||||||
|
## 3. 行程单入口直接复用订单详情打印代码
|
||||||
|
|
||||||
|
管理后台现有可复用实现位于:
|
||||||
|
|
||||||
|
- `src/views/order-v2/detail/modals/PrintItineraryModal.vue`
|
||||||
|
- `src/api/orderV2.js` 的 `getPrintItinerary(orderId)`
|
||||||
|
- 既有接口 `GET /v3/admin/order/<orderId>/print-itinerary`
|
||||||
|
|
||||||
|
看板按以下方式接入:
|
||||||
|
|
||||||
|
- 操作文案统一为“打印行程单”,点击后把当前订单的字符串 `orderId` 传给现有
|
||||||
|
`PrintItineraryModal`;
|
||||||
|
- 打印预览、加载、错误提示、页面排版和打印动作全部沿用订单详情现有组件;
|
||||||
|
- 可抽取共享挂载点或直接复用组件,但不得复制组件源码形成第二套打印实现;
|
||||||
|
- 不新增 fleet 打印 API,不在前端重新组装行程节点、费用、住宿、大交通或每日行程;
|
||||||
|
- 不调用看板详情去读取 `currentAssignment.itineraryUrl`,不执行剪贴板复制,也不继续使用
|
||||||
|
`ItinerarySendSheet.vue` 承载这个入口;
|
||||||
|
- 无 `orderId` 时不打开弹窗、不提示成功,沿用现有缺少订单上下文的错误态。
|
||||||
|
|
||||||
|
## 前端展示矩阵
|
||||||
|
|
||||||
|
| 页面区域 | 展示内容 | 数据来源 | 包含/排除状态 | 空数据表现 | 标签与颜色 | 守恒规则 |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| 派车看板操作区 | 派车派人 | `availableActionCodes` 含 `ASSIGN` | 包含待派车;排除终态 | 无候选沿用现有提示 | 沿用现有待派状态色 | 只创建目标槽位 |
|
||||||
|
| 派车看板操作区 | 改派 | `availableActionCodes` 含 `CHANGE_ASSIGNMENT` | 包含待确认;排除终态 | 无有效派单不显示 | 沿用现有待确认状态色 | 只替换选中槽位 |
|
||||||
|
| 派车看板操作区 | 改派 | `canAssign=true` 且含 `CHANGE_ASSIGNMENT` | 包含已派车且未完结;排除已完结/已取消 | 无剩余可改服务日时展示既有后端错误 | 沿用现有已派状态色 | 配完仍可再次改派,其他槽位不变 |
|
||||||
|
| 派车看板操作区 | 不显示改派 | 无 `CHANGE_ASSIGNMENT` | 仅已完结/已取消 | 不展示按钮 | 沿用终态灰 | 不恢复终态 |
|
||||||
|
| 派车看板操作区 | 打印行程单 | 订单 `orderId` 与既有打印接口 | 包含可查看订单;排除无订单 ID | 沿用现有打印弹窗加载/错误态 | 沿用现有打印按钮样式 | 只复用一套 `PrintItineraryModal.vue` |
|
||||||
|
|
||||||
|
## 前端处理清单
|
||||||
|
|
||||||
|
- [ ] `assigned + canAssign=true + CHANGE_ASSIGNMENT` 显示“改派”,点击进入既有改派流程。
|
||||||
|
- [ ] `holding/holding_urgent` 继续显示“改派”,首次待派仍显示“派车派人”。
|
||||||
|
- [ ] `completed/canceled` 不显示改派入口。
|
||||||
|
- [ ] 用户选择哪个 `activeAssignments[]` 槽位,就把该槽位的 `assignmentId` 传给既有 change 接口。
|
||||||
|
- [ ] 改派成功后刷新列表与详情,当前槽位更新,其他槽位数量和身份保持不变。
|
||||||
|
- [ ] 将看板行程单操作统一为“打印行程单”,直接复用订单详情
|
||||||
|
`PrintItineraryModal.vue + getPrintItinerary(orderId)`。
|
||||||
|
- [ ] 不新增打印组件/API,不读取或复制 `currentAssignment.itineraryUrl`,不继续使用
|
||||||
|
`ItinerarySendSheet.vue` 实现该入口。
|
||||||
|
- [ ] 覆盖无订单 ID、打印接口失败和打印数据空态,全部沿用既有打印弹窗行为。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端提交:`9ea7d555a078101cbf57d1571d106cd0ec2863af`;PR #5258。
|
||||||
|
- `BoardControllerTest + BoardOrderServiceTest` 共 54 项通过。
|
||||||
|
- `AssignmentServiceTest#change_directFromEffectiveDate_replacesDailySlicesAndWarnsOtherVehicle`
|
||||||
|
通过,验证只替换目标稳定槽位,其它车辆槽位不取消。
|
||||||
|
- `mvn -pl hl-fleet-service spotless:check` 通过。
|
||||||
|
- `mvn -pl hl-fleet-service -am verify` 通过;fleet 207 套件、2,373 项测试,
|
||||||
|
0 失败、0 错误、1 个既有跳过。
|
||||||
|
- 测试环境已滚动部署 `hl-fleet-service`;Nacos `test` 命名空间的 8087、8187 两实例健康。
|
||||||
|
- 测试网关查询 `assigned` 返回 4 条真实记录,全部为
|
||||||
|
`canAssign=true + CHANGE_ASSIGNMENT`,HTTP/code 均为 200。
|
||||||
|
- 测试环境当时没有 `holding/holding_urgent/completed/canceled` 样本;这些状态不冒充网关实测,
|
||||||
|
由已通过的服务状态矩阵与 Controller JSON 测试覆盖。
|
||||||
|
- 网关证据 SHA-256:
|
||||||
|
`1409d22ce9fc2c86101d9f811fef867e0493f177191fb8ac5ee30eb4427185e2`。
|
||||||
|
- OpenAPI/oasdiff:`not_configured`。字段名、类型和 requiredness 未变;仓库没有可复现的
|
||||||
|
Swagger2 → OAS3 导出链且未安装 `oasdiff`,已用 Controller JSON、服务状态矩阵和当前
|
||||||
|
`hl-ui` 消费源码做人工回退核对。
|
||||||
|
- 消费者契约/Spring Cloud Contract:`not_required`。本次没有内部 Feign 或共享 Java DTO 变化。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不修改或部署 `D:/work2/hl-ui`;`frontend_status` 保持 `pending`,页面实现和发布独立流转。
|
||||||
|
- 不删除司机 H5 行程短链、签名 token 或公开行程接口;仅更正看板入口的前端消费方式。
|
||||||
|
- 不新增/删除 API 字段,不改变字段类型、必填性、错误码或雪花 ID 的字符串消费要求。
|
||||||
|
- 不修改首次派车、待确认推进、取消派单、司机确认、资源占用或通知冻结规则。
|
||||||
|
- 不新增 DDL,不迁移、清理或回填数据。
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5257"
|
||||||
|
title: "车务看板按用车需求聚合多车型槽位"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@594dfa44b497b40a1e75c81b3e60abba8deb6137"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: "2026-07-26T11:13:51+08:00"
|
||||||
|
status_note: "后端 PR #5259 已合并为 dev-v3@01bf9c627,并重新部署测试环境及通过网关复验;本契约明确替代 #5216 的按槽位卡片维度。前端尚未认领,需按需求卡消费 assignmentSlots。"
|
||||||
|
updated_at: "2026-07-26T03:40:29.152Z"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 车务看板按用车需求聚合多车型槽位
|
||||||
|
|
||||||
|
## 关联
|
||||||
|
|
||||||
|
- Issue: [wx/HL#5257](https://git.1814.love:8443/wx/HL/issues/5257)
|
||||||
|
- 服务: `hl-fleet-service`
|
||||||
|
- 前端仓库/分支: `mmg/hl-ui` / `v2.1`
|
||||||
|
- 影响范围: 车务管理 → 派车看板列表、需求卡和需求详情
|
||||||
|
- 被本契约替代的旧口径:
|
||||||
|
[#5216 派车看板补充槽位接送路线与就绪摘要](./24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md)
|
||||||
|
中“每张卡对应一个稳定车辆槽位”的维度说明
|
||||||
|
|
||||||
|
## 关键变化
|
||||||
|
|
||||||
|
同一订单的一条当前有效用车需求可能同时需要多种车型、多个车辆槽位。例如
|
||||||
|
`SUV×1 + 商务车×1` 是 **1 条用车需求、合计 2 辆车**,不能显示成 2 条“用车需求”。
|
||||||
|
|
||||||
|
`GET /admin/fleet/board/orders` 的 `data.records[]` 从派车槽位粒度调整为当前
|
||||||
|
active `requirementId` 粒度:
|
||||||
|
|
||||||
|
- 同一 `requirementId` 只返回一条 record。
|
||||||
|
- `requiredVehicles[]` 展示全部车型组及数量。
|
||||||
|
- `assignmentProgress.totalSlots` 展示合计需要的车辆数。
|
||||||
|
- 新增 `assignmentSlots[]`,完整保留逐辆派车身份、状态、车辆和司机。
|
||||||
|
- 原顶层单槽位字段兼容保留,统一表示当前最需要处理的代表槽位。
|
||||||
|
- 状态、车型或司机筛选命中需求内任一槽位时,需求只返回一次。
|
||||||
|
- 后端先按需求聚合,再排序和分页;`total` 与汇总接口不再按槽位重复计数。
|
||||||
|
|
||||||
|
请求参数、路径、HTTP method、响应包络、错误码、数据库和派车执行语义均不变。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /admin/fleet/board/orders
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "成功",
|
||||||
|
"data": {
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"id": "26-5256",
|
||||||
|
"orderId": "2080200000000000000",
|
||||||
|
"requirementId": "2080200000000000900",
|
||||||
|
"requiredVehicles": [
|
||||||
|
{
|
||||||
|
"vehicleType": "suv",
|
||||||
|
"categoryLabel": "SUV",
|
||||||
|
"seats": 5,
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vehicleType": "mpv",
|
||||||
|
"categoryLabel": "商务车",
|
||||||
|
"seats": 7,
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assignmentProgress": {
|
||||||
|
"totalSlots": 2,
|
||||||
|
"unassignedSlots": 2,
|
||||||
|
"holdingSlots": 0,
|
||||||
|
"assignedSlots": 0,
|
||||||
|
"completedSlots": 0,
|
||||||
|
"canceledSlots": 0
|
||||||
|
},
|
||||||
|
"assignmentStatus": "unassigned",
|
||||||
|
"assignmentId": "2080200000000000001",
|
||||||
|
"assignmentSlotId": "2080200000000000101",
|
||||||
|
"fleetItemIndex": 0,
|
||||||
|
"canAssign": true,
|
||||||
|
"canRejectRequirement": true,
|
||||||
|
"assignmentSlots": [
|
||||||
|
{
|
||||||
|
"assignmentId": "2080200000000000001",
|
||||||
|
"assignmentGroupId": "2080200000000000201",
|
||||||
|
"assignmentSlotId": "2080200000000000101",
|
||||||
|
"fleetItemIndex": 0,
|
||||||
|
"slotSummary": {
|
||||||
|
"requiredVehicleType": "suv",
|
||||||
|
"requiredVehicleTypeLabel": "SUV",
|
||||||
|
"requiredSeats": 5
|
||||||
|
},
|
||||||
|
"baseAssignmentStatus": "unassigned",
|
||||||
|
"assignmentStatus": "unassigned",
|
||||||
|
"assignmentStatusLabel": "待派车",
|
||||||
|
"availableActionCodes": ["ASSIGN"],
|
||||||
|
"vehiclePlate": null,
|
||||||
|
"vehicleModel": null,
|
||||||
|
"vehicleSeats": null,
|
||||||
|
"driverName": null,
|
||||||
|
"driverPhone": null,
|
||||||
|
"urgentBadge": null,
|
||||||
|
"canAssign": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assignmentId": "2080200000000000002",
|
||||||
|
"assignmentGroupId": "2080200000000000202",
|
||||||
|
"assignmentSlotId": "2080200000000000102",
|
||||||
|
"fleetItemIndex": 1,
|
||||||
|
"slotSummary": {
|
||||||
|
"requiredVehicleType": "mpv",
|
||||||
|
"requiredVehicleTypeLabel": "商务车",
|
||||||
|
"requiredSeats": 7
|
||||||
|
},
|
||||||
|
"baseAssignmentStatus": "unassigned",
|
||||||
|
"assignmentStatus": "unassigned",
|
||||||
|
"assignmentStatusLabel": "待派车",
|
||||||
|
"availableActionCodes": ["ASSIGN"],
|
||||||
|
"vehiclePlate": null,
|
||||||
|
"vehicleModel": null,
|
||||||
|
"vehicleSeats": null,
|
||||||
|
"driverName": null,
|
||||||
|
"driverPhone": null,
|
||||||
|
"urgentBadge": null,
|
||||||
|
"canAssign": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1,
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 20
|
||||||
|
},
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 新增字段 `assignmentSlots[]`
|
||||||
|
|
||||||
|
所有雪花 ID 必须按字符串消费。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 空值 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `assignmentId` | `String` | 否 | 逐槽位派车/改派操作使用的派单 ID |
|
||||||
|
| `assignmentGroupId` | `String` | 否 | 同一派车组 ID |
|
||||||
|
| `assignmentSlotId` | `String` | 否 | 跨逐日切片稳定的车辆槽位 ID |
|
||||||
|
| `fleetItemIndex` | `Integer` | 否 | 当前需求 `fleet[]` 展开后的槽位序号,0 起 |
|
||||||
|
| `slotSummary` | `Object` | 否 | 该槽位车型、座位、服务范围和整单槽位数 |
|
||||||
|
| `baseAssignmentStatus` | `String` | 否 | 基础落库态 |
|
||||||
|
| `assignmentStatus` | `String` | 否 | 当前有效状态,可含紧急派生态 |
|
||||||
|
| `assignmentStatusLabel` | `String` | 否 | 后端中文状态标签 |
|
||||||
|
| `lifecycleStageCode/lifecycleStageLabel` | `String` | 否 | 该槽位生命周期阶段 |
|
||||||
|
| `currentStep` | `Integer` | 否 | 该槽位当前步骤 |
|
||||||
|
| `availableActionCodes` | `String[]` | 否 | 该槽位可用动作;需求级驳回不在此数组 |
|
||||||
|
| `vehiclePlate/vehicleModel` | `String` | 是 | 已派车辆信息 |
|
||||||
|
| `vehicleSeats` | `Integer` | 是 | 已派车辆座位数 |
|
||||||
|
| `vehicleFleetTeamId` | `String` | 是 | 已派车辆所属车队 ID |
|
||||||
|
| `vehicleFleetTeamName/vehicleFleetTeamType/vehicleFleetTeamSettleType` | `String` | 是 | 已派车辆所属车队信息 |
|
||||||
|
| `driverName` | `String` | 是 | 已派司机姓名 |
|
||||||
|
| `driverPhone` | `String` | 是 | 已脱敏司机手机号 |
|
||||||
|
| `urgentBadge` | `String` | 是 | `T-N` 或 `Nh`;非紧急为 `null` |
|
||||||
|
| `canAssign` | `Boolean` | 否 | 该槽位是否可进入派车/改派流程 |
|
||||||
|
|
||||||
|
## 顶层兼容字段
|
||||||
|
|
||||||
|
以下既有字段没有删除,旧前端继续读取不会报错:
|
||||||
|
|
||||||
|
- `assignmentId/assignmentGroupId/assignmentSlotId/fleetItemIndex`
|
||||||
|
- `slotSummary`
|
||||||
|
- `assignmentStatus/assignmentStatusLabel`
|
||||||
|
- `lifecycleStageCode/lifecycleStageLabel/currentStep/availableActionCodes`
|
||||||
|
- `currentVehicle*`
|
||||||
|
- `currentDriver*`
|
||||||
|
- `urgentBadge/canAssign/canRejectRequirement`
|
||||||
|
|
||||||
|
它们统一指向代表槽位,选择优先级为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
未派 → 排车中 → 已派 → 已完成 → 已取消
|
||||||
|
```
|
||||||
|
|
||||||
|
同级按 `fleetItemIndex/assignmentSlotId/assignmentId` 稳定排序。部分已派时,顶层仍指向
|
||||||
|
未派槽位,保证原“派车派人”入口可继续派下一辆;所有槽位的真值以
|
||||||
|
`assignmentSlots[]` 和 `assignmentProgress` 为准。
|
||||||
|
|
||||||
|
`canRejectRequirement` 与需求级驳回动作只读外层 record。任一槽位已进入
|
||||||
|
`holding/assigned` 时,外层不会错误开放驳回;槽位级 `availableActionCodes` 不包含需求级驳回。
|
||||||
|
|
||||||
|
## 筛选、分页和汇总
|
||||||
|
|
||||||
|
- 状态多选:任一槽位命中即返回该需求一次。
|
||||||
|
- 车型多选:任一需求槽位命中即返回该需求一次,`requiredVehicles[]` 仍保留全部车型。
|
||||||
|
- 司机筛选/关键词:任一槽位司机命中即返回该需求一次。
|
||||||
|
- 混合状态:顶层状态取代表槽位状态;完整状态分布读取 `assignmentProgress` 和
|
||||||
|
`assignmentSlots[]`。
|
||||||
|
- 空态:无当前有效需求或无 fleet 看板候选时返回 `records=[]/total=0`,不按人数合成虚假槽位。
|
||||||
|
- 顺序:先聚合为唯一 requirement record,再确定性排序和分页。
|
||||||
|
- `data.total`:查询范围内唯一 active `requirementId` 数,不是车辆槽位数。
|
||||||
|
- `GET /admin/fleet/board/summary`:状态和今日出团数使用相同需求粒度,不重复计数。
|
||||||
|
|
||||||
|
守恒关系:
|
||||||
|
|
||||||
|
```text
|
||||||
|
assignmentProgress.totalSlots
|
||||||
|
== unassignedSlots
|
||||||
|
+ holdingSlots
|
||||||
|
+ assignedSlots
|
||||||
|
+ completedSlots
|
||||||
|
+ canceledSlots
|
||||||
|
|
||||||
|
assignmentProgress.totalSlots
|
||||||
|
== sum(requiredVehicles[].count)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 前端处理清单
|
||||||
|
|
||||||
|
- [ ] 看板列表对每个 `records[]` 只渲染一张需求卡,不再按
|
||||||
|
`fleetItemIndex/assignmentSlotId` 拆卡,也不得展开 `assignmentSlots[]` 生成额外卡片。
|
||||||
|
- [ ] 列表 row key 优先使用 `requirementId`;仅兼容历史空值时回退 `id/orderId`。
|
||||||
|
- [ ] 需求卡展示 `requiredVehicles[]` 的全部车型组,并显示
|
||||||
|
`assignmentProgress.totalSlots` 为“需要 N 辆车”。
|
||||||
|
- [ ] 需求详情遍历 `assignmentSlots[]` 展示全部车辆槽位、各自状态和已派车辆/司机。
|
||||||
|
- [ ] 顶层按钮可继续使用代表槽位字段;逐辆操作必须使用所选
|
||||||
|
`assignmentSlots[i].assignmentId/fleetItemIndex/assignmentSlotId`,不得复用其他槽位 ID。
|
||||||
|
- [ ] 需求级驳回只使用外层 `canRejectRequirement/availableActionCodes`,不从槽位数组推断。
|
||||||
|
- [ ] 混合状态显示以 `assignmentProgress` 为准,不用顶层单一状态覆盖所有槽位。
|
||||||
|
- [ ] 继续按既有稳定状态 token 映射颜色,不按中文文案判断色值。
|
||||||
|
- [ ] 覆盖单车型×1、多车型各×1、同车型×2、部分已派、全已派、完结/取消和空列表场景。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不修改用车需求 `fleet[]` 的业务语义。
|
||||||
|
- 不合并、删除或改写真实 `fleet_assignment`;派车仍逐辆、逐槽位执行。
|
||||||
|
- 不修改派车、确认、取消、改派、需求驳回接口的请求结构。
|
||||||
|
- 不修改 §7 矩阵派单的车辆槽位维度。
|
||||||
|
- 不修改数据库、网关路由、错误码、车辆/司机占用、保险或费用。
|
||||||
|
- 本交接不代表已修改、发布或验证 `mmg/hl-ui`。
|
||||||
|
|
||||||
|
## 后端验证
|
||||||
|
|
||||||
|
- 精确复现测试:同一 `requirementId` 下 `SUV×1 + 商务车×1` 返回
|
||||||
|
`total=1/records=1`、`requiredVehicles=2` 组、`assignmentSlots=2`、
|
||||||
|
`assignmentProgress.totalSlots=2`。
|
||||||
|
- 同车型×2、部分已派、全已派、混合完结/取消、状态多选、聚合后分页和汇总去重均有自动化覆盖。
|
||||||
|
- 相关定向测试 64 项通过,0 failures/errors。
|
||||||
|
- OpenAPI diff 状态为 `not_configured`:当前环境没有 `oasdiff`,仓库也没有可复现的
|
||||||
|
Swagger 2 → OpenAPI 3 导出链;契约审查保存了 Controller/VO 字段对比和自动化测试
|
||||||
|
作为人工 fallback 证据。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 commit:`fd641651144ab429ec1b371e1902a70bf3e7f025`
|
||||||
|
- 后端 PR:[wx/HL#5259](https://git.1814.love:8443/wx/HL/pulls/5259)
|
||||||
|
- 合并 commit:`dev-v3@01bf9c6274d5c40e00e65b84c5224189b0fd06b8`
|
||||||
|
- 合并后测试部署:现有部署 API 已滚动发布 `dev-v3`,
|
||||||
|
`hl-fleet-service:8087/8187` 均健康;构建、发布成功,部署日志尾部的
|
||||||
|
`ERROR/FATAL/Exception` 命中数为 0。
|
||||||
|
- 网关复现订单 `26-5256`:HTTP/业务码均为 200,`records=1`、`total=1`、
|
||||||
|
`requiredVehicles=SUV×1+MPV×1`、`assignmentSlots=2` 且逐槽位 ID 唯一、
|
||||||
|
`assignmentProgress.totalSlots=2`、状态数量之和为 2;脱敏证据 SHA-256 为
|
||||||
|
`7ba2b05d8bdf335207771c59e089357dbe25d1fe0ac4aa0db1c91f910da563f5`。
|
||||||
|
- fleet 定向测试 64 项通过;`mvn -pl hl-fleet-service spotless:check`
|
||||||
|
611 个文件通过;`mvn -pl hl-fleet-service -am verify` 中 fleet 2386 项测试
|
||||||
|
0 failures/errors、skipped 1,完整 reactor `BUILD SUCCESS`。
|
||||||
|
- OpenAPI diff 为 `not_configured`,已保存 Controller/VO 字段对比、兼容语义、
|
||||||
|
自动化测试和脱敏网关结构作为人工 fallback 证据。
|
||||||
|
- `frontend_status=pending`:本次未修改、部署或验证 `mmg/hl-ui`;
|
||||||
|
页面仍需按“1 条需求卡 + 2 个槽位详情”的新契约完成消费。
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5262"
|
||||||
|
title: "派车逐日车费、只读总价与核单实时接口"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-pi"
|
||||||
|
frontend_ref: "mmg/hl-ui@2dfe8ab40f3db446d0a079d7511986a2d7fdce33"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: "2026-07-26T16:37:00+08:00"
|
||||||
|
path_aliases: "changelogs-v2/2026-07/26_5262_派车逐日车费与核单实时接口-修改接口-前端待处理-管理后台.md"
|
||||||
|
status_note: "后端 PR wx/HL#5266 已合并到 dev-v3(9bfd21de6),Fleet/Order 测试双实例已部署并完成管理端网关与 internal Feign 实测;hl-admin 已在 2dfe8ab40f3db446d0a079d7511986a2d7fdce33 接入逐日车费、只读总价和旧字段移除,verify:changed 通过,尚未发布及页面联调。本契约替代 #5253 的“手工填写每车总价”口径。"
|
||||||
|
updated_at: "2026-07-26"
|
||||||
|
base: "dev-v3"
|
||||||
|
generated: "2026-07-26T16:20:00+08:00"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 派车逐日车费、只读总价与核单实时接口
|
||||||
|
|
||||||
|
## 关联
|
||||||
|
|
||||||
|
- Issue: [wx/HL#5262](https://git.1814.love:8443/wx/HL/issues/5262)
|
||||||
|
- 服务:`hl-fleet-service`、`hl-order-service-v3`
|
||||||
|
- 前端仓库:`mmg/hl-ui`(本文仅交接,不代表已修改前端)
|
||||||
|
- 替代口径:[#5253 按车辆记录订单总车费并接入核单](./26_5253_按车辆记录订单总车费并接入核单-修改接口-管理后台.md)
|
||||||
|
|
||||||
|
## 关键变化
|
||||||
|
|
||||||
|
1. 多日派车按服务日保存 assignment,每个槽位 4 天即 4 条每日记录。
|
||||||
|
2. 价格日历改为提供逐日参考价;车务可覆盖本次派车的某日车费,覆盖值不回写价格日历。
|
||||||
|
3. `vehicleFeeTotal` 改为只读合计,恒等于收费日 `assignmentPrice` 之和。
|
||||||
|
4. 创建、批量派车、确认和改派请求继续兼容解析旧总价字段,但只要传值即返回稳定业务错误,不再接受手工总价。
|
||||||
|
5. 配置车辆不写 Order 核单表;核单后端通过新的 Fleet internal API 实时读取逐日车辆费用。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 变化 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `POST` | `/admin/fleet/assignments/candidates` | 车辆候选新增逐日车费参考 |
|
||||||
|
| `POST` | `/admin/fleet/assignments` | 新增 `dailyVehicleFees`;旧 `vehicleFeeTotal` 禁止传值 |
|
||||||
|
| `POST` | `/admin/fleet/assignments/batch` | 每个最终车辆槽位分别提交 `dailyVehicleFees` |
|
||||||
|
| `POST` | `/admin/fleet/assignments/:assignmentId/change` | 新派车段提交逐日车费;保留段沿用原逐日快照 |
|
||||||
|
| `POST` | `/admin/fleet/assignments/:assignmentId/confirm` | 旧总价字段禁止传值;总价由已保存逐日车费只读计算 |
|
||||||
|
| `GET` | `/admin/fleet/board/orders/:orderId` | 槽位返回 `dailyVehicleFees` 和只读合计 |
|
||||||
|
|
||||||
|
## 请求字段
|
||||||
|
|
||||||
|
`dailyVehicleFees[]`:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `serviceDate` | `LocalDate` | 是 | 本次覆盖的服务日期 |
|
||||||
|
| `price` | `Decimal` | 是 | 本次派车单日车费,最小 `0.00`,最多 2 位小数 |
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
- 未提交覆盖值的收费日使用车型价格日历当天价格。
|
||||||
|
- 收费日缺少日历价格且未提交覆盖值时,后端拒绝最终派车。
|
||||||
|
- 覆盖值与日历参考价不一致时提交 `vehicleFeeAdjustmentReason`。
|
||||||
|
- 免费日期 `assignmentPrice` 固定为 `"0.00"`,不计入总价。
|
||||||
|
- `vehicleFeeTotal`、`retainedVehicleFeeTotal` 以及对应旧调整原因字段不得再由前端提交。
|
||||||
|
|
||||||
|
## 响应字段
|
||||||
|
|
||||||
|
候选、派车写响应和看板槽位新增或统一返回 `dailyVehicleFees[]`:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `serviceDate` | `LocalDate` | 服务日期 |
|
||||||
|
| `chargeable` | `Boolean` | 是否收取车费 |
|
||||||
|
| `calendarPrice` | `Decimal/null` | 车型价格日历参考价;缺价时为空 |
|
||||||
|
| `assignmentPrice` | `Decimal/null` | 本次派车单日车费;收费日必须有值,免费日为 `"0.00"` |
|
||||||
|
| `source` | `String` | `CALENDAR` / `OVERRIDE` / `FREE` / `MISSING` |
|
||||||
|
| `calendarPriceMissing` | `Boolean` | 价格日历是否缺价 |
|
||||||
|
|
||||||
|
`vehicleFeeTotal` 继续返回,但语义变为只读:
|
||||||
|
|
||||||
|
```text
|
||||||
|
vehicleFeeTotal = sum(dailyVehicleFees[chargeable=true].assignmentPrice)
|
||||||
|
```
|
||||||
|
|
||||||
|
金额字段按字符串消费,雪花 ID 继续按字符串消费。
|
||||||
|
|
||||||
|
## 页面展示矩阵
|
||||||
|
|
||||||
|
| 区域 | 展示 | 空态 | 状态/颜色 | 守恒规则 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 收费日期 | 每日显示日历参考价与本次派车价 | 选车前“待计算”;缺价“价格日历缺价” | 参考价中性、覆盖蓝、缺价橙 | 一服务日一条派车记录 |
|
||||||
|
| 最终总车费 | 只读合计,不渲染金额输入框 | 缺价时“价格不完整” | 正常中性、缺价橙 | 等于全部收费日本次派车价之和 |
|
||||||
|
| 已结束行程 | 只允许查看逐日价格和总价 | 不适用 | 只读灰 | 前端禁用与后端拒绝一致 |
|
||||||
|
|
||||||
|
## 前端处理清单
|
||||||
|
|
||||||
|
- [ ] 移除“最终总车费”输入框,改为只读合计。
|
||||||
|
- [ ] 收费日期逐日展示 `calendarPrice`,并允许编辑当前槽位的 `assignmentPrice`。
|
||||||
|
- [ ] 仅把修改后的日期组装为 `dailyVehicleFees`,不调用车型价格日历写接口。
|
||||||
|
- [ ] 使用 `source` 和 `calendarPriceMissing` 展示覆盖与缺价状态。
|
||||||
|
- [ ] 创建、批量派车、确认和改派请求不再传旧总价字段。
|
||||||
|
- [ ] 行程结束后禁用逐日车费、槽位和改派入口。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- OpenAPI/oasdiff:`not_configured`;已完成 Controller/VO 源码比对和 Fleet 接口测试回退证据。
|
||||||
|
- Spring Cloud Contract:`not_configured`;已完成 Fleet producer、Order Feign consumer 和 shared DTO 测试回退证据。
|
||||||
|
- Fleet:`mvn -pl hl-fleet-service -am verify` 与 `spotless:check` 通过。
|
||||||
|
- Order:`mvn -pl hl-order-service-v3 -am verify` 完成,Surefire 零失败并生成可执行 JAR。
|
||||||
|
- 部署:Fleet `630858f8`、Order `63f9ba60` 成功,8087/8187 与 8086/8186 双实例健康。
|
||||||
|
- 网关:管理端订单详情返回 4 条逐日车费及约定的 6 个逐日字段;internal Feign 正向响应严格为顶层 5 个字段、item 12 个字段。
|
||||||
|
- `frontend_status`:`pending`;真实领取后再迁移为 `claimed`。
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5263"
|
||||||
|
title: "最终确认按车选择发送行程短信"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-pi"
|
||||||
|
frontend_ref: "mmg/hl-ui@138136e50c4bbf9931ee020bd28d40977264b64e"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "前端已在 138136e5 接入 HOLD 最终确认逐车短信必选、权威状态查询和 FAILED 受控重试;定向 7 个文件 62 项及 verify:changed 全量验证通过。尚未发布或完成真实页面联调。"
|
||||||
|
updated_at: "2026-07-27"
|
||||||
|
base: "dev-v3"
|
||||||
|
generated: "2026-07-26T17:33:00+08:00"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 最终确认按车选择发送行程短信
|
||||||
|
|
||||||
|
## 关联
|
||||||
|
|
||||||
|
- Issue: [wx/HL#5263](https://git.1814.love:8443/wx/HL/issues/5263)
|
||||||
|
- Changelog PR: [wx/hl-api-changelog#41](https://git.1814.love:8443/wx/hl-api-changelog/pulls/41)
|
||||||
|
- 服务:`hl-fleet-service`、`hl-user-service`
|
||||||
|
- 前端仓库:`mmg/hl-ui`(本文仅交接,不代表已修改前端)
|
||||||
|
- 前置车费契约:[#5262 派车逐日车费、只读总价与核单实时接口](./26_5262_派车逐日车费与核单实时接口-修改接口-前端待处理-管理后台.md)
|
||||||
|
|
||||||
|
## 关键变化
|
||||||
|
|
||||||
|
1. 仅在 HOLD 排车的最终确认阶段,每个车辆组必须显式选择“发送短信”或“不发送短信”,没有默认值。
|
||||||
|
2. 选择发送时,确认事务只落可靠发送意图;短信异步发送失败不会回滚已完成的派车确认。
|
||||||
|
3. 短信只发给该车辆组当前师傅,包含订单摘要、接送摘要和签名行程短链,不包含客户手机号。
|
||||||
|
4. 选择不发送时只完成派车确认,不创建行程短信事件。
|
||||||
|
5. 多车订单逐车独立选择、独立投递、独立查询状态和受控重试。
|
||||||
|
6. 已派定后的订单人数基线复核只能沿用原选择,不允许借复核修改选择或重复发送。
|
||||||
|
7. 直接派车流程不受影响;#5262 已废弃的手工车辆总价入参仍然禁止提交。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 变化 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `POST` | `/admin/fleet/assignments/:assignmentId/confirm` | 请求新增必填 `sendItinerarySms`;响应新增短信选择、事件与状态 |
|
||||||
|
| `GET` | `/admin/fleet/assignments/:assignmentId/itinerary-sms` | 新增单车/车辆组短信审计状态查询 |
|
||||||
|
| `POST` | `/admin/fleet/assignments/:assignmentId/itinerary-sms/retry` | 新增明确失败后的车务受控重试 |
|
||||||
|
|
||||||
|
## 最终确认
|
||||||
|
|
||||||
|
### `POST /admin/fleet/assignments/:assignmentId/confirm`
|
||||||
|
|
||||||
|
#### 请求字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `sendItinerarySms` | `Boolean` | 是 | `true` 发送;`false` 不发送;省略或 `null` 返回参数错误 |
|
||||||
|
| `requestId` | `String` | 是 | 最长 64 字符的幂等请求标识 |
|
||||||
|
| `vehicleFeeTotal` | `Decimal` | 否 | 历史兼容字段;非空即拒绝,最终总车费继续由 #5262 逐日车费只读合计 |
|
||||||
|
| `vehicleFeeAdjustmentReason` | `String` | 否 | 历史兼容字段;非空即拒绝 |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sendItinerarySms": true,
|
||||||
|
"requestId": "fleet-final-confirm-26-8411-car-1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 新增响应字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `sendItinerarySms` | `Boolean` | 本车辆组最终确认时保存的选择 |
|
||||||
|
| `itinerarySmsEventId` | `String/null` | 可靠短信事件 ID;不发送时为空 |
|
||||||
|
| `itinerarySmsStatus` | `String` | 首次确认返回 `PENDING` 或 `NOT_SENT`;已派定复核回显真实状态 |
|
||||||
|
|
||||||
|
## 短信状态
|
||||||
|
|
||||||
|
### `GET /admin/fleet/assignments/:assignmentId/itinerary-sms`
|
||||||
|
|
||||||
|
响应 `data`:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `assignmentId` | `String` | 派单 ID |
|
||||||
|
| `assignmentGroupId` | `String` | 跨服务日车辆组 ID |
|
||||||
|
| `assignmentSlotId` | `String` | 稳定车辆槽位 ID |
|
||||||
|
| `sendItinerarySms` | `Boolean/null` | 未最终确认或历史数据时为 `null` |
|
||||||
|
| `status` | `String` | `NOT_APPLICABLE` / `NOT_SENT` / `PENDING` / `SENT` / `FAILED` / `CANCELED` |
|
||||||
|
| `eventId` | `String/null` | 可靠短信事件 ID |
|
||||||
|
| `retryCount` | `Integer` | 已发生的失败重试次数 |
|
||||||
|
| `canRetry` | `Boolean` | 当前是否允许受控重试 |
|
||||||
|
| `sentAt` | `LocalDateTime/null` | 供应商确认的真实发送时间 |
|
||||||
|
| `lastError` | `String/null` | 已脱敏的最近失败或待对账原因 |
|
||||||
|
|
||||||
|
前端以 `status` 为权威,不得仅凭最终确认接口成功就显示“短信已发送”。
|
||||||
|
|
||||||
|
## 受控重试
|
||||||
|
|
||||||
|
### `POST /admin/fleet/assignments/:assignmentId/itinerary-sms/retry`
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reason": "短信通道配置已恢复,车务确认重发",
|
||||||
|
"requestId": "retry-sms-26-8411-car-1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
- 仅 `FAILED` 且 `canRetry=true` 时展示并调用重试。
|
||||||
|
- `SENT`、`PENDING`、待供应商对账、`CANCELED`、未选择发送时禁止重试。
|
||||||
|
- 重试复用原事件与供应商幂等键,不新建并行短信事件。
|
||||||
|
- 司机或车辆组身份已变化时,旧事件收敛为 `CANCELED`,不得发给旧师傅。
|
||||||
|
|
||||||
|
## 短信与隐私约束
|
||||||
|
|
||||||
|
短信模板参数固定为:
|
||||||
|
|
||||||
|
| 参数 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| `summary` | 脱敏订单摘要 |
|
||||||
|
| `transfer` | 接送摘要 |
|
||||||
|
| `code` | 签名行程短链 |
|
||||||
|
|
||||||
|
短信正文及模板参数不得包含客户手机号。真实联系人信息仅在既有签名行程 H5 中按授权展示。
|
||||||
|
|
||||||
|
## 页面展示矩阵
|
||||||
|
|
||||||
|
| 区域/状态 | 数据源 | 展示 | 空态/禁用 | 颜色 | 守恒规则 |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 最终确认车辆卡片 | 本地待提交选择 | “发送短信”/“不发送短信”二选一 | 未选择时禁止确认并提示必选 | 发送蓝色,不发送中性灰 | 每个车辆组恰好一个选择 |
|
||||||
|
| 确认后状态 | `GET .../itinerary-sms.status` | 待发送/已发送/发送失败/已取消/未发送 | 历史数据为“不适用” | 待发送蓝、已发送绿、失败红、取消灰、未发送中性灰 | 不以确认成功冒充发送成功 |
|
||||||
|
| 失败操作 | `canRetry` | “重试短信” | `canRetry=false` 时隐藏或禁用 | 可重试橙色 | 同一事件串行重试 |
|
||||||
|
| 多车订单 | 每个 `assignmentGroupId` | 每车独立选择与状态 | 不做订单级统一默认 | 各卡片独立 | 一车选择不得覆盖另一车 |
|
||||||
|
|
||||||
|
## 前端处理清单
|
||||||
|
|
||||||
|
- [ ] 最终确认页按车辆组渲染无默认值的短信二选一。
|
||||||
|
- [ ] 未完成选择时不提交确认请求,并展示明确校验提示。
|
||||||
|
- [ ] 确认请求始终显式提交 `sendItinerarySms`,不再依赖后端默认值。
|
||||||
|
- [ ] 确认后通过状态接口展示真实投递状态。
|
||||||
|
- [ ] 仅在 `FAILED && canRetry=true` 时允许填写原因并调用重试。
|
||||||
|
- [ ] 已派定复核回显原选择并保持只读,不提供改选入口。
|
||||||
|
- [ ] 短信状态按展示矩阵处理空态、颜色及多车独立性。
|
||||||
|
- [ ] 确认和复核请求继续不提交 #5262 已废弃的手工总车费字段。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- OpenAPI/oasdiff:`not_configured`;已完成 Controller/VO 源码比对和接口测试回退证据。
|
||||||
|
- 消费者契约:`not_required`;未修改 internal Feign 或共享 Java DTO。
|
||||||
|
- 代码与测试:PR `wx/HL#5270` 已合入 `dev-v3`,merge commit 为 `f0a96c10124c3188e60e1291e7f28d768af50e3a`;`mvn -pl hl-user-service,hl-fleet-service -am test`、`mvn -pl hl-fleet-service spotless:check`、`mvn -pl hl-fleet-service -am verify` 均通过。
|
||||||
|
- 测试部署:`hl-user-service` 与 `hl-fleet-service` 已从合并后的 `dev-v3` 完成双实例滚动部署并通过健康检查。
|
||||||
|
- 网关:显式“不发送”业务验收 11/11 通过;合并后只读复验 3/3 通过,最终状态为 `assigned + NOT_SENT`,无短信事件且不可重试。
|
||||||
|
- `frontend_status`:`pending`;真实领取后再迁移为 `claimed` 并填写 `frontend_owner`。
|
||||||
@@ -395,3 +395,33 @@ GET /admin/fleet/message-templates
|
|||||||
- 车队对账页 Network 必须看到 `/admin/fleet/reconciliation/cars` 和 `/insurance`,默认周期是当前月 `2026-07-01 ~ 2026-07-31`。
|
- 车队对账页 Network 必须看到 `/admin/fleet/reconciliation/cars` 和 `/insurance`,默认周期是当前月 `2026-07-01 ~ 2026-07-31`。
|
||||||
- 车管模板页 Network 必须看到 `/admin/fleet/message-templates`。
|
- 车管模板页 Network 必须看到 `/admin/fleet/message-templates`。
|
||||||
- 车务菜单测试账号必须是车务角色;不要用 `admin`、`adminle`、`wx`、定制师账号验证车务菜单。
|
- 车务菜单测试账号必须是车务角色;不要用 `admin`、`adminle`、`wx`、定制师账号验证车务菜单。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 2026-07-27 补充:矩阵车辆行常驻司机全部误显示“待派司机”
|
||||||
|
|
||||||
|
### 8.1 运行态与源码证据
|
||||||
|
|
||||||
|
- `/fleet/matrix` 的 19 条车辆行全部显示“待派司机”。
|
||||||
|
- 同次页面请求 `GET /admin/fleet/matrix/grid` 返回 200;响应车辆中存在非空
|
||||||
|
`primaryDriverName` 和脱敏 `primaryDriverPhone`,因此不是后端漏返回,也不是全部车辆都未绑定常驻司机。
|
||||||
|
- `useFleetMatrixData.adaptMatrixVehicle()` 已把这两个字段保留到车辆行对象。
|
||||||
|
- `VehicleGantt.primaryDriverOf(v)` 却忽略车辆行字段,只执行
|
||||||
|
`findPrimaryDriver(props.drivers, v.plate)`。
|
||||||
|
- 主矩阵和车辆分窗传入的 `drivers` 均为空列表且没有额外加载动作,所以每辆车都稳定落入
|
||||||
|
“待派司机”空态。
|
||||||
|
|
||||||
|
### 8.2 前端修复口径
|
||||||
|
|
||||||
|
1. 矩阵车辆行展示必须以 `data.vehicles[].primaryDriverName` 为权威来源;非空时直接显示该姓名。
|
||||||
|
2. `primaryDriverPhone` 已由后端脱敏,可按现有设计选择展示,但不得为显示姓名再拉全量司机列表。
|
||||||
|
3. 只有 `primaryDriverName` 为 `null` 或空白时,才显示“待派司机”空态。
|
||||||
|
4. 主矩阵和 `matrix-solo?solo=byVehicle` 必须复用同一解析逻辑,不能一处读取车辆行、一处反查本地司机数组。
|
||||||
|
5. 本问题不涉及后端接口、数据库、派单状态或候选规则变更;不要创建 `mmg/hl-ui` 配合工单,直接消费本 changelog。
|
||||||
|
|
||||||
|
### 8.3 前端验收 checklist
|
||||||
|
|
||||||
|
- [ ] 构造一辆 `primaryDriverName` 非空的车辆,主矩阵车辆行显示接口姓名而不是“待派司机”。
|
||||||
|
- [ ] `primaryDriverName=null` 的车辆仍显示“待派司机”。
|
||||||
|
- [ ] 车辆分窗与主矩阵展示一致。
|
||||||
|
- [ ] 车队、车型、状态筛选及派车占用条不受本次展示修复影响。
|
||||||
|
|||||||
@@ -0,0 +1,390 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5264"
|
||||||
|
title: "移除核单分类手动确认门禁"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-pi"
|
||||||
|
frontend_ref: "c182af23fb724ab6915d75750951b1db0dcc603d"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: "2026-07-28T14:38:26+08:00"
|
||||||
|
status_note: "hl-admin 已移除‘本分类已确认’入口及 allConfirmed/confirmStatus 后续流程门禁;业务提交 c182af23 已在 origin/v2.1 可达,全量 checkpoint 通过"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 【修改接口·管理后台】移除核单分类手动确认门禁 (#5264)
|
||||||
|
|
||||||
|
> **PR**: #5274 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-28 09:42
|
||||||
|
|
||||||
|
## 1. 接口背景
|
||||||
|
|
||||||
|
核单流程不再要求财务在八个核单分类上逐一点击“本分类已确认”。管理后台只需要保存各分类明细;明细完整且可用于报账时,即可生成主报账人报账表。旧分类确认查询和确认接口保留兼容返回,但确认状态不再作为主报账、单团核算、Step6 提交或财务确认的门禁。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||||
|
|---|------|------|------|----------|------|
|
||||||
|
| 1 | 查询原型八个核单分类确认状态 | GET | `/v3/admin/order/:orderId/settlement/category-checks` | 修改接口 | 响应字段保留,但 `allConfirmed` / `confirmStatus` 仅用于兼容展示,不再决定后续流程能否继续 |
|
||||||
|
| 2 | 按最近读取指纹确认单个核单分类 | POST | `/v3/admin/order/:orderId/settlement/category-checks/:category/confirm` | 修改接口 | 标记为废弃兼容;管理后台停止调用并移除“本分类已确认”入口 |
|
||||||
|
| 3 | 生成主报账人报账表 | POST | `/v3/admin/order/:orderId/settlement/reports/reimbursement/generate` | 修改接口 | 生成条件改为核单明细保存完整,不再要求八分类手动确认 |
|
||||||
|
|
||||||
|
## 3. 接口详情
|
||||||
|
|
||||||
|
### 3.1 查询原型八个核单分类确认状态
|
||||||
|
|
||||||
|
- **方法 / 路径**:`GET /v3/admin/order/:orderId/settlement/category-checks`
|
||||||
|
- **使用场景**:旧页面或兼容逻辑读取八分类状态。
|
||||||
|
- **认证**:需要管理后台登录态;房控角色不可访问。
|
||||||
|
- **幂等性**:是,只读查询。
|
||||||
|
- **限流**:无单独接口限流约定。
|
||||||
|
- **接口说明**:字段结构保持不变;`allConfirmed` 和 `items[].confirmStatus` 不再用于判断主报账、单团核算、Step6 或财务确认是否可继续。
|
||||||
|
|
||||||
|
### 3.2 按最近读取指纹确认单个核单分类(废弃兼容)
|
||||||
|
|
||||||
|
- **方法 / 路径**:`POST /v3/admin/order/:orderId/settlement/category-checks/:category/confirm`
|
||||||
|
- **使用场景**:仅兼容旧前端请求;新管理后台不再调用。
|
||||||
|
- **认证**:需要管理后台登录态和财务写权限;房控角色不可访问。
|
||||||
|
- **幂等性**:同一分类、同一 `expectedSourceFingerprint` 重复确认返回当前兼容状态。
|
||||||
|
- **限流**:无单独接口限流约定。
|
||||||
|
- **接口说明**:接口仍校验请求体和分类枚举,但确认投影不再作为后续流程门禁。前端应移除“本分类已确认”按钮、状态卡门禁和基于 `allConfirmed` 的下一步禁用逻辑。
|
||||||
|
|
||||||
|
### 3.3 生成主报账人报账表
|
||||||
|
|
||||||
|
- **方法 / 路径**:`POST /v3/admin/order/:orderId/settlement/reports/reimbursement/generate`
|
||||||
|
- **使用场景**:核单明细保存完整后生成或刷新主报账人报账表。
|
||||||
|
- **认证**:需要管理后台登录态和财务写权限;房控角色不可访问。
|
||||||
|
- **幂等性**:同一来源数据已生成时,可返回当前报账表;来源变化后重新生成。
|
||||||
|
- **限流**:无单独接口限流约定。
|
||||||
|
- **接口说明**:生成门禁改为逐分类明细完整性校验;不再要求先调用八分类确认接口。
|
||||||
|
|
||||||
|
## 4. 接口入参
|
||||||
|
|
||||||
|
### 4.1 路径参数 / Query 参数
|
||||||
|
|
||||||
|
| 接口 | 字段 | 类型 | 必填 | 说明 | 校验规则 |
|
||||||
|
|------|------|------|------|------|----------|
|
||||||
|
| 三个接口共用 | `orderId` | `String` | 是 | 订单 ID,按字符串处理 | 必须为大于 0 的数字 |
|
||||||
|
| 分类确认接口 | `category` | `String` | 是 | 核单分类编码 | 见 §6.1 `SettlementCategory` |
|
||||||
|
|
||||||
|
### 4.2 请求体字段
|
||||||
|
|
||||||
|
#### 4.2.1 `GET /category-checks`
|
||||||
|
|
||||||
|
无请求体。
|
||||||
|
|
||||||
|
#### 4.2.2 `POST /category-checks/:category/confirm`(废弃兼容)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|
||||||
|
|------|------|------|------|----------|
|
||||||
|
| `expectedSourceFingerprint` | `String` | 是 | 最近读取的分类源事实 SHA-256;废弃兼容字段 | 64 位小写十六进制字符串 |
|
||||||
|
| `confirmEmpty` | `Boolean` | 是 | 是否明确确认空分类;废弃兼容字段 | `true` / `false` |
|
||||||
|
|
||||||
|
#### 4.2.3 `POST /reports/reimbursement/generate`
|
||||||
|
|
||||||
|
无请求体。
|
||||||
|
|
||||||
|
## 5. 出参字段
|
||||||
|
|
||||||
|
### 5.1 `SettlementCategoryChecksRespVO`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `orderId` | `String` | 订单 ID |
|
||||||
|
| `allConfirmed` | `Boolean` | 兼容字段;不再作为后续流程门禁 |
|
||||||
|
| `items` | `Array<ItemVO>` | 八个分类状态列表 |
|
||||||
|
|
||||||
|
### 5.2 `SettlementCategoryChecksRespVO.ItemVO`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `category` | `String` | 分类编码,见 §6.1 |
|
||||||
|
| `categoryName` | `String` | 分类中文名 |
|
||||||
|
| `rowCount` | `Integer` | 当前分类明细行数 |
|
||||||
|
| `empty` | `Boolean` | 当前分类是否为空 |
|
||||||
|
| `sourceFingerprint` | `String` | 当前分类源事实指纹 |
|
||||||
|
| `confirmStatus` | `String` | 兼容字段,见 §6.2;不再作为后续流程门禁 |
|
||||||
|
| `confirmedBy` | `String/null` | 兼容字段,确认人 ID |
|
||||||
|
| `confirmedByName` | `String/null` | 兼容字段,确认人姓名 |
|
||||||
|
| `confirmedAt` | `String/null` | 兼容字段,确认时间,格式 `yyyy-MM-dd'T'HH:mm:ss` |
|
||||||
|
|
||||||
|
### 5.3 `SettlementReimbursementReportRespVO`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | `String` | 主报账表 ID |
|
||||||
|
| `orderId` | `String` | 订单 ID |
|
||||||
|
| `reportStatus` | `String` | 报告状态,见 §6.3 |
|
||||||
|
| `sourceFingerprint` | `String` | 报账来源指纹 |
|
||||||
|
| `primaryReporterId` | `String/null` | 主报账人 ID |
|
||||||
|
| `primaryReporterName` | `String/null` | 主报账人姓名 |
|
||||||
|
| `primaryReporterRole` | `String/null` | 主报账人角色 |
|
||||||
|
| `reportVersion` | `Integer` | 报告版本号 |
|
||||||
|
| `driverCollectedTailAmount` | `Decimal` | 司机代收尾款金额 |
|
||||||
|
| `approvedAdvanceAmount` | `Decimal` | 已审批预支金额 |
|
||||||
|
| `reportablePaidCostAmount` | `Decimal` | 可报账已支付成本 |
|
||||||
|
| `reporterNetAmount` | `Decimal` | 报账人净额 |
|
||||||
|
| `primaryReporterCollectedAmount` | `Decimal` | 主报账人已收金额 |
|
||||||
|
| `publicPrepaidAmount` | `Decimal` | 公共预付金额 |
|
||||||
|
| `primaryReporterDueAmount` | `Decimal` | 主报账人应结金额 |
|
||||||
|
| `advanceOutstandingAmount` | `Decimal` | 预支未结金额 |
|
||||||
|
| `reconNetAmount` | `Decimal` | 对账净额 |
|
||||||
|
| `transferDirection` | `String/null` | 转账方向 |
|
||||||
|
| `transferAmount` | `Decimal` | 转账金额 |
|
||||||
|
| `incomeLines` | `Array<Object>` | 收入明细行 |
|
||||||
|
| `expenseLines` | `Array<Object>` | 支出明细行 |
|
||||||
|
| `advanceLines` | `Array<Object>` | 预支明细行 |
|
||||||
|
| `vehicleLines` | `Array<Object>` | 车辆费用明细行 |
|
||||||
|
| `transferStatus` | `String/null` | 转账状态 |
|
||||||
|
| `transferDate` | `String/null` | 转账日期,格式 `yyyy-MM-dd` |
|
||||||
|
| `transferRef` | `String/null` | 转账凭证号 |
|
||||||
|
| `advanceSettledFlag` | `Boolean/null` | 预支是否已结清 |
|
||||||
|
| `signedVoucher` | `Object/null` | 签字凭证信息 |
|
||||||
|
| `generatedBy` | `String/null` | 生成人 ID |
|
||||||
|
| `generatedByName` | `String/null` | 生成人姓名 |
|
||||||
|
| `generatedAt` | `String/null` | 生成时间,格式 `yyyy-MM-dd'T'HH:mm:ss` |
|
||||||
|
| `confirmedBy` | `String/null` | 确认人 ID |
|
||||||
|
| `confirmedByName` | `String/null` | 确认人姓名 |
|
||||||
|
| `confirmedAt` | `String/null` | 确认时间,格式 `yyyy-MM-dd'T'HH:mm:ss` |
|
||||||
|
|
||||||
|
## 6. 枚举 / 数据字典
|
||||||
|
|
||||||
|
### 6.1 `category`(SettlementCategory)
|
||||||
|
|
||||||
|
**所属字段**:路径参数 `category`、响应 `items[].category` | **类型**:`String`
|
||||||
|
|
||||||
|
| 值 | 中文 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| `HOTEL` | 住宿 | 住宿核单明细 |
|
||||||
|
| `TICKET` | 门票/游玩项目 | 门票和游玩项目核单明细 |
|
||||||
|
| `MEAL` | 餐食 | 餐食费用明细 |
|
||||||
|
| `VEHICLE` | 车辆 | 车辆费用明细 |
|
||||||
|
| `GUIDE` | 导游 | 导游费用明细 |
|
||||||
|
| `PHOTOGRAPHER` | 摄影 | 摄影费用明细 |
|
||||||
|
| `OTHER_INCOME` | 其他收入 | 其他收入明细 |
|
||||||
|
| `OTHER_EXPENSE` | 其他支出 | 其他支出明细 |
|
||||||
|
|
||||||
|
### 6.2 `confirmStatus`(兼容状态)
|
||||||
|
|
||||||
|
**所属字段**:`items[].confirmStatus` | **类型**:`String`
|
||||||
|
|
||||||
|
| 值 | 中文 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| `UNCONFIRMED` | 未确认 | 兼容旧确认投影;不再阻止生成主报账表 |
|
||||||
|
| `CONFIRMED` | 已确认 | 兼容旧确认投影;不再作为后续流程门禁 |
|
||||||
|
| `STALE` | 已变化 | 兼容旧确认投影;不再作为后续流程门禁 |
|
||||||
|
|
||||||
|
### 6.3 `reportStatus`(SettlementReportStatus)
|
||||||
|
|
||||||
|
**所属字段**:`reportStatus` | **类型**:`String`
|
||||||
|
|
||||||
|
| 值 | 中文 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| `GENERATED` | 已生成 | 主报账表已生成,尚未确认 |
|
||||||
|
| `CONFIRMED` | 已确认 | 主报账表已确认 |
|
||||||
|
| `STALE` | 来源已变化 | 当前来源指纹与已保存报账表不一致 |
|
||||||
|
|
||||||
|
## 7. 错误码
|
||||||
|
|
||||||
|
| code | 含义 | 触发场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `200` | 成功 | 查询、兼容确认或生成主报账表成功 |
|
||||||
|
| `400` | 请求参数错误 | `orderId` 非法、兼容确认接口缺少请求体、`expectedSourceFingerprint` 不是 64 位小写十六进制、`confirmEmpty` 缺失 |
|
||||||
|
| `404` | 接口或资源不存在 | 路径不存在,或访问不存在的订单 |
|
||||||
|
| `584315` | 核单来源数据已变化,请刷新后重新生成 | 报告来源指纹变化 |
|
||||||
|
| `584317` | 当前报告状态不允许执行该操作 | 当前核单状态不允许生成或确认报告 |
|
||||||
|
| `584319` | 核单存在未知分类或历史迁移数据不完整 | `category` 不是 §6.1 中的值 |
|
||||||
|
| `584320` | 核单分类明细尚未保存完整或数据不可用于报账 | 生成主报账表时,某个分类明细缺必填业务信息或不可用于报账;响应会带具体分类名 |
|
||||||
|
|
||||||
|
## 8. 示例
|
||||||
|
|
||||||
|
### 8.1 典型成功:未逐类确认也可生成主报账表
|
||||||
|
|
||||||
|
**请求**:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v3/admin/order/60001/settlement/reports/reimbursement/generate
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
无请求体。
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "910000000000000001",
|
||||||
|
"orderId": "60001",
|
||||||
|
"reportStatus": "GENERATED",
|
||||||
|
"sourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"primaryReporterId": "11001",
|
||||||
|
"primaryReporterName": "张三",
|
||||||
|
"primaryReporterRole": "GUIDE",
|
||||||
|
"reportVersion": 1,
|
||||||
|
"driverCollectedTailAmount": 0.00,
|
||||||
|
"approvedAdvanceAmount": 2000.00,
|
||||||
|
"reportablePaidCostAmount": 8300.00,
|
||||||
|
"reporterNetAmount": 6300.00,
|
||||||
|
"primaryReporterCollectedAmount": 0.00,
|
||||||
|
"publicPrepaidAmount": 1000.00,
|
||||||
|
"primaryReporterDueAmount": 6300.00,
|
||||||
|
"advanceOutstandingAmount": 0.00,
|
||||||
|
"reconNetAmount": 6300.00,
|
||||||
|
"transferDirection": "PAY_TO_REPORTER",
|
||||||
|
"transferAmount": 6300.00,
|
||||||
|
"incomeLines": [],
|
||||||
|
"expenseLines": [
|
||||||
|
{
|
||||||
|
"category": "HOTEL",
|
||||||
|
"categoryName": "住宿",
|
||||||
|
"amount": 3600.00
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"advanceLines": [],
|
||||||
|
"vehicleLines": [],
|
||||||
|
"transferStatus": "PENDING",
|
||||||
|
"transferDate": null,
|
||||||
|
"transferRef": null,
|
||||||
|
"advanceSettledFlag": false,
|
||||||
|
"signedVoucher": null,
|
||||||
|
"generatedBy": "11",
|
||||||
|
"generatedByName": "旧核单员",
|
||||||
|
"generatedAt": "2026-07-27T10:15:30",
|
||||||
|
"confirmedBy": null,
|
||||||
|
"confirmedByName": null,
|
||||||
|
"confirmedAt": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 边界情况:查询兼容状态仍返回 `allConfirmed=false`
|
||||||
|
|
||||||
|
**请求**:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v3/admin/order/60001/settlement/category-checks
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
无请求体。
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"orderId": "60001",
|
||||||
|
"allConfirmed": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"category": "HOTEL",
|
||||||
|
"categoryName": "住宿",
|
||||||
|
"rowCount": 1,
|
||||||
|
"empty": false,
|
||||||
|
"sourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"confirmStatus": "UNCONFIRMED",
|
||||||
|
"confirmedBy": null,
|
||||||
|
"confirmedByName": null,
|
||||||
|
"confirmedAt": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 业务失败:分类明细未保存完整
|
||||||
|
|
||||||
|
**请求**:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v3/admin/order/60001/settlement/reports/reimbursement/generate
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
无请求体。
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 584320,
|
||||||
|
"msg": "核单分类「住宿」明细尚未保存完整或数据不可用于报账",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 业务边界
|
||||||
|
|
||||||
|
- **适用场景**:管理后台核单流程;分类明细已保存完整后生成主报账人报账表。
|
||||||
|
- **不适用场景**:继续用 `allConfirmed=true` 作为“生成主报账表”“生成单团核算表”“Step6 提交”“财务确认”的前置条件。
|
||||||
|
- **特殊边界**:`POST /category-checks/:category/confirm` 仍可能返回 200,但它只是兼容旧调用,不代表新流程需要或应该调用。
|
||||||
|
- **明细完整性口径**:生成主报账表时,八个分类都必须存在可用于报账的明细快照;缺少分类、金额非法、业务必填项为空或来源数据不可用时返回 `584320`。
|
||||||
|
|
||||||
|
## 10. 修改前后对比
|
||||||
|
|
||||||
|
### 10.1 字段级对比
|
||||||
|
|
||||||
|
| 字段 | 改前 | 改后 |
|
||||||
|
|------|------|------|
|
||||||
|
| `allConfirmed` | 后续流程可能按该字段判断八分类是否已全部确认 | 字段保留兼容,但不再作为后续流程门禁 |
|
||||||
|
| `items[].confirmStatus` | `UNCONFIRMED` / `CONFIRMED` / `STALE` 可能影响页面下一步按钮 | 字段保留兼容,但不再作为后续流程门禁 |
|
||||||
|
| `SettlementCategoryConfirmReqVO.expectedSourceFingerprint` | 分类确认接口必填 | 仍为兼容接口必填;新前端停止调用该接口 |
|
||||||
|
| `SettlementCategoryConfirmReqVO.confirmEmpty` | 分类确认接口必填 | 仍为兼容接口必填;新前端停止调用该接口 |
|
||||||
|
|
||||||
|
### 10.2 行为级对比
|
||||||
|
|
||||||
|
| 行为 | 改前 | 改后 |
|
||||||
|
|------|------|------|
|
||||||
|
| 主报账表生成 | 要求八个分类确认状态全部满足手动确认口径 | 核单明细保存完整即可生成 |
|
||||||
|
| 分类确认按钮 | 前端需要逐分类调用确认接口 | 前端停止调用确认接口,并移除“本分类已确认”入口 |
|
||||||
|
| 单团核算 / Step6 / 财务确认门禁 | 可能间接受八分类确认状态影响 | 不再读取八分类手动确认状态作为门禁 |
|
||||||
|
| 明细不完整时生成主报账表 | 可能表现为八分类未确认或来源变化类提示 | 返回 `584320`,提示具体分类明细未保存完整或不可用于报账 |
|
||||||
|
|
||||||
|
## 11. 影响评估 / 回滚
|
||||||
|
|
||||||
|
### 11.1 影响评估
|
||||||
|
|
||||||
|
- **是否破坏向后兼容**:否。旧查询字段和旧确认接口保留,但确认接口已废弃。
|
||||||
|
- **前端是否必须同步上线**:建议同步。前端应移除“本分类已确认”按钮、`allConfirmed` 门禁和基于 `confirmStatus` 的下一步禁用逻辑。
|
||||||
|
- **影响已有数据**:不要求前端迁移数据;历史确认状态仅作为兼容显示值。
|
||||||
|
|
||||||
|
### 11.2 回滚方案
|
||||||
|
|
||||||
|
- **回滚方式**:如需恢复旧流程,回滚 PR #5274 对应后端变更。
|
||||||
|
- **回滚后清理**:前端若已移除按钮,回滚后需要恢复八分类确认入口和 `allConfirmed` 门禁。
|
||||||
|
|
||||||
|
## 12. 注意事项
|
||||||
|
|
||||||
|
- 管理后台不要再新增对 `POST /category-checks/:category/confirm` 的调用。
|
||||||
|
- 页面上原“本分类已确认”按钮、确认进度提示和 `allConfirmed=false` 禁用下一步的逻辑可以移除。
|
||||||
|
- 查询分类状态接口可继续用于兼容老页面,但不要把 `UNCONFIRMED` 或 `STALE` 解释为主报账表不可生成。
|
||||||
|
- 生成主报账表失败时优先识别 `584320`,它表示需要补齐对应分类明细,而不是要求点击分类确认。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 PR:[#5274](https://git.1814.love:8443/wx/HL/pulls/5274)。
|
||||||
|
- 合并提交:[`8635973e6`](https://git.1814.love:8443/wx/HL/commit/8635973e6)。
|
||||||
|
- 实现提交:[`07ac323a1`](https://git.1814.love:8443/wx/HL/commit/07ac323a1)。
|
||||||
|
- Source frontmatter 已记录后端部署完成、网关验证通过;前端按本交接独立完成消费与验证。
|
||||||
|
|
||||||
|
## 13. 关联 / 联系人
|
||||||
|
|
||||||
|
### 13.1 链接
|
||||||
|
|
||||||
|
- **Issue**: [#5264](https://git.1814.love:8443/wx/HL/issues/5264)
|
||||||
|
- **PR**: [#5274](https://git.1814.love:8443/wx/HL/pulls/5274)
|
||||||
|
- **Merge commit**: [8635973e6](https://git.1814.love:8443/wx/HL/commit/8635973e6)
|
||||||
|
- **Implementation commit**: [07ac323a1](https://git.1814.love:8443/wx/HL/commit/07ac323a1)
|
||||||
|
|
||||||
|
### 13.2 联系人
|
||||||
|
|
||||||
|
- **后端负责人**: @yaosu
|
||||||
|
- **前端对接**: 管理后台前端
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5279"
|
||||||
|
title: "车务派单全链路 E2E 前端缺陷交接"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-admin"
|
||||||
|
frontend_ref: "2ee0988c235c0483a5b51f8cc0deb195d7fd8573"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "本次没有新增或修改后端字段、类型、必填性、路径和错误码;测试环境当前契约已验证。前端需修正动作码、部分日期、HOLD 费用草稿、刷新重置和冲突日期等消费逻辑。"
|
||||||
|
updated_at: "2026-07-27"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 车务:派单全链路 E2E 前端缺陷交接
|
||||||
|
|
||||||
|
> **服务**: `hl-fleet-service`、`hl-order-service-v3`
|
||||||
|
>
|
||||||
|
> **工单**: [wx/HL#5279](https://git.1814.love:8443/wx/HL/issues/5279)
|
||||||
|
>
|
||||||
|
> **前端基线**: `mmg/hl-ui v2.1@5e6718f952ed156c2e71bc38d08c3482e72ba744`
|
||||||
|
>
|
||||||
|
> **影响范围**: 车务管理 → 派单看板、矩阵派单、派车弹窗、派单详情;订单详情 → 用车需求调整
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
这是一份**当前契约消费纠错和前端缺陷交接**,不是后端接口变更:
|
||||||
|
|
||||||
|
- 后端 #5275 已保证部分日期派车只消费目标切片,剩余日期和稳定槽位守恒;
|
||||||
|
- 后端 #5277 已恢复完整已派需求 `canAssign=true + CHANGE_ASSIGNMENT`;
|
||||||
|
- 测试环境通过真实后台 UI 完成 HOLD、DIRECT、司机确认、最终确认、司机拒接、改车、改司机、需求驳回/换版/重派、逐日费用、免费服务日、冲突和重复提交;
|
||||||
|
- 当前剩余阻断均可在最新前端稳定复现,后端不应增加旧动作码别名或重复状态来迁就页面。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
本次不新增接口、字段或错误码,仅纠正以下现有管理后台接口的前端消费:
|
||||||
|
|
||||||
|
| 方法 | 路径 | 当前契约用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET` | `/admin/fleet/board/orders` | 看板代表行、能力字段、稳定槽位和未派切片上下文 |
|
||||||
|
| `GET` | `/admin/fleet/board/orders/<orderId>` | 当前有效派车组、逐日费用和独立生命周期 |
|
||||||
|
| `POST` | `/admin/fleet/assignments/candidates` | 车辆/司机可用窗口与真实冲突明细 |
|
||||||
|
| `POST` | `/admin/fleet/assignments` | HOLD/DIRECT 首次派车 |
|
||||||
|
| `POST` | `/admin/fleet/assignments/<assignmentId>/change` | 按稳定槽位改车、改司机或同时改派 |
|
||||||
|
| `POST` | `/admin/fleet/assignments/<assignmentId>/driver-confirmation` | 登记司机回复 |
|
||||||
|
| `POST` | `/admin/fleet/assignments/<assignmentId>/confirm` | 最终确认执行 |
|
||||||
|
| `POST` | `/admin/fleet/assignments/<assignmentId>/driver-reject` | 司机拒接并退回未派 |
|
||||||
|
| `DELETE` | `/admin/fleet/assignments/<assignmentId>` | 取消派单 |
|
||||||
|
| `POST` | `/admin/fleet/assignments/<assignmentId>/early-complete` | 提前完结 |
|
||||||
|
|
||||||
|
## 一、动作码必须消费后端当前值
|
||||||
|
|
||||||
|
权威字段来自:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /admin/fleet/board/orders
|
||||||
|
GET /admin/fleet/board/orders/<orderId>
|
||||||
|
```
|
||||||
|
|
||||||
|
前端按 `canAssign` 和 `availableActionCodes` 渲染,不按中文状态或本地别名推断。
|
||||||
|
|
||||||
|
| 生命周期 | `currentStep` | 当前动作码 | 正确页面行为 |
|
||||||
|
| --- | ---: | --- | --- |
|
||||||
|
| `unassigned` | 2 | `ASSIGN`, `REJECT_REQUIREMENT` | 派车派人、驳回需求 |
|
||||||
|
| `holding_wait_driver` | 3 | `CHANGE_ASSIGNMENT`, `RECORD_DRIVER_CONFIRMATION`, `DRIVER_REJECT`,出团前/当天另有 `CANCEL_ASSIGNMENT` | 继续派车、司机拒接、改派、取消 |
|
||||||
|
| `driver_confirmed` | 4 | `CHANGE_ASSIGNMENT`, `CONFIRM_EXECUTION`,出团前/当天另有 `CANCEL_ASSIGNMENT` | 恢复第 4 步确认执行;不得只剩改派 |
|
||||||
|
| `assigned` 且未过出团日 | 4 | `CHANGE_ASSIGNMENT`, `CANCEL_ASSIGNMENT` | 改派、取消派单 |
|
||||||
|
| `assigned` 且已过出团日 | 4 | `CHANGE_ASSIGNMENT`, `EARLY_COMPLETE` | 改派、提前完结 |
|
||||||
|
| `completed/canceled` | 终态 | 无可写动作 | 只读 |
|
||||||
|
|
||||||
|
当前前端存在三处精确错位:
|
||||||
|
|
||||||
|
1. 检查 `CANCEL`,而后端下发 `CANCEL_ASSIGNMENT`;
|
||||||
|
2. 检查 `COMPLETE_EARLY`,而后端下发 `EARLY_COMPLETE`;
|
||||||
|
3. `driver_confirmed` 已下发 `CONFIRM_EXECUTION`,但卡片和详情没有恢复确认执行入口。
|
||||||
|
|
||||||
|
`DRIVER_REJECT` 与 `RECORD_DRIVER_CONFIRMATION` 是现行值,继续沿用。不要让后端同时下发新旧两套动作码。
|
||||||
|
|
||||||
|
## 二、部分日期稳定槽必须保留用户点击上下文
|
||||||
|
|
||||||
|
### 复现
|
||||||
|
|
||||||
|
测试单 `26-6263` 的同一 `assignmentSlotId`:
|
||||||
|
|
||||||
|
- 07-30、07-31:已有 HOLD;
|
||||||
|
- 08-01:唯一剩余 `unassigned`;
|
||||||
|
- 看板卡片正确显示“用车 08-01 · 1天”;
|
||||||
|
- 8 月矩阵未派池也只显示 08-01。
|
||||||
|
|
||||||
|
但当前前端:
|
||||||
|
|
||||||
|
- 从看板进入 Step 2 后改成编辑 07-30/07-31 的 active HOLD;
|
||||||
|
- 从矩阵车辆 08-01 空闲格进入时,顶部虽显示“有效服务日期:08-01”,Step 2 却显示 `已选 0/0 天`,继续复用 07-30/07-31 费用并返回 0 个候选。
|
||||||
|
|
||||||
|
### 正确规则
|
||||||
|
|
||||||
|
- 首次派车 `mode=assign` 必须保留用户点击的看板代表行:`assignmentId/assignmentSlotId/fleetItemIndex/startDate/endDate`;详情异步返回后不能被 `currentAssignment` 覆盖;
|
||||||
|
- 矩阵入口以 `entryContext.clickedDate + startDate + endDate` 与订单合法服务段求交集;本例结果固定为 08-01;
|
||||||
|
- `currentAssignment/activeAssignments[]` 只用于改派和确认已有有效派车组,不代表未派切片;
|
||||||
|
- 实际计费服务日为空时,应从已经校验通过的 `assignmentDateRange` 回退生成,不得读取另一 active 组的逐日费用;
|
||||||
|
- 提交仍使用原稳定 `assignmentSlotId`,只消费 08-01,不得重建第二槽位或覆盖 07-30/07-31。
|
||||||
|
|
||||||
|
## 三、HOLD 手工补价不能在司机确认后丢失
|
||||||
|
|
||||||
|
### 复现
|
||||||
|
|
||||||
|
`26-9140` 改派时:
|
||||||
|
|
||||||
|
- 07-30/07-31 使用日历价 860;
|
||||||
|
- 08-01 日历缺价,页面提交覆盖价 860 和调价原因;
|
||||||
|
- `/change` 返回并落库总价 2580;
|
||||||
|
- 派单详情也正确展示 08-01 覆盖价和原因。
|
||||||
|
|
||||||
|
登记司机确认后,Step 4 又从候选价格日历重建费用,08-01 变为“待补价”,总价变为“价格不完整”,`validateVehicleFeeDraft` 阻断最终确认。
|
||||||
|
|
||||||
|
### 正确规则
|
||||||
|
|
||||||
|
- HOLD 已创建后,`activeAssignments[]/currentAssignment` 的 `dailyVehicleFees`、`vehicleFeeTotal`、`vehicleFeeAdjustmentReason`、`chargeableServiceDates` 和 `vehicleFeeWaiverReason` 是当前派车组权威快照;
|
||||||
|
- 候选接口的价格只用于新选车草稿或日历参考,不得覆盖已经保存的 HOLD 费用;
|
||||||
|
- 第 3 步登记司机回复和第 4 步确认执行都不能清空已保存覆盖价;
|
||||||
|
- 详情重拉后应按 `assignmentId/assignmentGroupId` 恢复同一派车组快照。
|
||||||
|
|
||||||
|
## 四、后台刷新不得重置正在编辑的派车草稿
|
||||||
|
|
||||||
|
实测派车弹窗打开后,后台订单刷新会触发整套初始化:
|
||||||
|
|
||||||
|
- 候选接口已返回 19 辆车和 11 名司机,但两个列表被清空,计数/筛选项仍保留;
|
||||||
|
- 用户已切换 DIRECT,提交前又恢复为 HOLD;
|
||||||
|
- 再点一次筛选会重新请求并恢复候选。
|
||||||
|
|
||||||
|
前端应做到:
|
||||||
|
|
||||||
|
- `props.order` 仅因列表/SSE 刷新生成新对象时,不重置当前弹窗;
|
||||||
|
- 只有订单 ID、目标需求 ID、目标稳定槽位、模式或用户主动关闭/重开变化时才重新初始化;
|
||||||
|
- 已编辑车辆、司机、日期、收费日、逐日价、原因、跨常驻确认和 HOLD/DIRECT 模式全部保留;
|
||||||
|
- 若服务端基线真的变化,使用现有 baseline-difference 强提示并让用户决定,不静默改写草稿。
|
||||||
|
|
||||||
|
## 五、多槽 HOLD 必须按槽位推进
|
||||||
|
|
||||||
|
`26-5256` 有两个稳定车辆槽位。槽位 1 登记司机确认后:
|
||||||
|
|
||||||
|
- 详情真值为槽位 1 `driver_confirmed`、槽位 2 `holding_wait_driver`;
|
||||||
|
- 卡片却仍把代表司机显示为“待回复”;
|
||||||
|
- “继续派车”入口消失,槽位 2 无法登记司机回复。
|
||||||
|
|
||||||
|
正确行为:
|
||||||
|
|
||||||
|
- 订单只要任一 active 槽位包含 `RECORD_DRIVER_CONFIRMATION`,卡片和详情保留“继续派车”;
|
||||||
|
- 打开后默认选中最早待回复槽位,也允许切换其它槽位;
|
||||||
|
- 一个槽位确认不得覆盖或隐藏另一个槽位状态;
|
||||||
|
- 订单聚合步骤取最慢槽位,代表卡文案必须明确“代表槽位”,不能冒充全单状态。
|
||||||
|
|
||||||
|
## 六、冲突窗口字段不能混用
|
||||||
|
|
||||||
|
候选响应同时包含:
|
||||||
|
|
||||||
|
| 字段 | 含义 | 页面用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `conflicts[]` | 真正占用冲突;`blocking=true` 的日期禁止选择 | 展示冲突订单与冲突日期 |
|
||||||
|
| `availabilityWindows[]` | 当前查询区间内仍可用的连续窗口 | 仅在资源可用/部分可用提示中展示 |
|
||||||
|
| `availabilityReasonCode` | `AVAILABLE`、`ASSIGNMENT_CONFLICT` 等判定码 | 决定禁用和提示类型 |
|
||||||
|
| `availabilityReasonMessage` | 后端判定文案 | 展示主提示 |
|
||||||
|
|
||||||
|
实测后端冲突是 08-01,`availabilityWindows` 是 08-02..08-04;页面却显示“存在派单冲突 · 08-02 至 08-04”。禁用结果正确,日期解释相反。
|
||||||
|
|
||||||
|
冲突文案应从 `conflicts[].startDate/endDate` 汇总;`availabilityWindows` 不得拼在冲突文案后。
|
||||||
|
|
||||||
|
## 七、提交与刷新反馈
|
||||||
|
|
||||||
|
### 1. 防重复提交
|
||||||
|
|
||||||
|
真实 UI 同一时刻双击 DIRECT 发出两个不同 `requestId`:
|
||||||
|
|
||||||
|
- 第一笔成功并只生成一个有效派车;
|
||||||
|
- 第二笔被后端守恒门禁拒绝,返回 `605033`:`用车需求完成回写处理中,请稍后重试`。
|
||||||
|
|
||||||
|
前端应在第一笔进入提交函数时立即短路后续点击,并在按钮、快捷键和程序触发路径共用同一 `submitting` 门禁。同一草稿的网络重试应复用 requestId,不应每次生成新值。
|
||||||
|
|
||||||
|
### 2. 司机拒接后的详情
|
||||||
|
|
||||||
|
司机拒接成功后,卡片立即变为未派,但当前详情抽屉仍保留旧 HOLD 车辆、费用和动作。关闭重开才正确。成功后需用最新服务端详情整体替换旧对象,不得继续把 mutation 前的行快照合并回来。
|
||||||
|
|
||||||
|
### 3. 车务角色快捷入口
|
||||||
|
|
||||||
|
车务角色下顶部可见“订单列表”,点击却进入未注册 `/housekeeper/orders` 并显示 404。未注册/无权限时不要展示快捷入口;有权限时应确保动态路由已注册再导航。
|
||||||
|
|
||||||
|
## 八、前端验收清单
|
||||||
|
|
||||||
|
- [ ] `CANCEL_ASSIGNMENT` 显示并执行取消;不再检查 `CANCEL`。
|
||||||
|
- [ ] `EARLY_COMPLETE` 显示并执行提前完结;不再检查 `COMPLETE_EARLY`。
|
||||||
|
- [ ] `driver_confirmed + CONFIRM_EXECUTION` 可从卡片和详情恢复第 4 步。
|
||||||
|
- [ ] 多槽 HOLD 任一槽位待回复时保留“继续派车”,逐槽推进且代表状态不串槽。
|
||||||
|
- [ ] `26-6263` 从看板和 8 月矩阵都只编辑/提交 08-01,07-30/07-31 不变。
|
||||||
|
- [ ] 已保存 HOLD 的手工逐日价、调价原因和免费日配置在司机确认、详情刷新、最终确认之间保持不变。
|
||||||
|
- [ ] SSE/列表刷新不清空候选、车辆司机草稿或切换 HOLD/DIRECT 模式。
|
||||||
|
- [ ] 冲突日期取 `conflicts[]`,可用窗口取 `availabilityWindows[]`,两者文案不混用。
|
||||||
|
- [ ] DIRECT/HOLD 第一击后立即禁用所有重复提交路径;同草稿重试复用 requestId。
|
||||||
|
- [ ] 司机拒接后当前卡片和详情同时刷新到 unassigned。
|
||||||
|
- [ ] 车务角色的订单快捷入口不再进入 404。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 `dev-v3@79297c838b62af2426cc83ad0e78cffbd961096b` 已部署,Fleet 8087/8187 两实例健康;
|
||||||
|
- #5277 Fleet reactor 2,427 项通过,0 失败、0 错误、1 跳过;Spotless 通过;
|
||||||
|
- 网关验证:完整 assigned 单槽和三槽均为 `canAssign=true + CHANGE_ASSIGNMENT`;
|
||||||
|
- 真实 UI 写入通过:HOLD、DIRECT、司机确认、最终确认(不发送短信)、司机拒接、只换车、只换司机、同时换车司机、需求驳回、V2/V3 换版重派、收费/免费日、重复提交;
|
||||||
|
- 后端守恒:重复 DIRECT 只生成一笔有效派车;需求 `DONE_ADJUST` 返回 `assignmentDeletedCount=1`,旧资源随后可重新选择;
|
||||||
|
- 部分日期后端真值:07-30/07-31 为 HOLD,08-01 唯一 unassigned,三天共用稳定槽位,无重复/孤儿;
|
||||||
|
- 关键页面截图:`fleet-partial-continuation-broken.png`、`5277-assigned-reassign-restored.png`;
|
||||||
|
- 完整逐步证据由 #5279 工单评论和测试记录保留。
|
||||||
|
|
||||||
|
## 十、契约审查
|
||||||
|
|
||||||
|
- 本文件不对应新的 Controller、VO、字段、类型、必填性、动作码或错误码变更;
|
||||||
|
- OpenAPI/oasdiff:`not_required`,没有新的 `frontend_api` diff;
|
||||||
|
- Consumer Contract:`not_required`,没有 Feign/shared Java 变化;
|
||||||
|
- 当前动作码由 `AssignmentLifecycleResolver` 和现有服务测试确认;部署网关响应与源码一致;
|
||||||
|
- 前端代码审查确认当前仍检查 `CANCEL`、`COMPLETE_EARLY`,且没有消费 `CONFIRM_EXECUTION`。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不修改 `hl-ui`,不在 `mmg/hl-ui` 建工单;本文件 `frontend_status` 保持 `pending`;
|
||||||
|
- 不要求后端新增兼容动作码、重复字段、特殊前端分支或放松资源/幂等守恒;
|
||||||
|
- 不修改派车状态机、计费公式、库存/占用、保险、通知、Outbox 或数据库结构;
|
||||||
|
- 不把前端实现、发布、页面验收纳入后端工单关闭条件。
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5282"
|
||||||
|
title: "矩阵年度月度订单统计"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "新增接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-pi"
|
||||||
|
frontend_ref: "7d0589d6"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "管理后台已由提交 7d0589d6 完成年度月度订单统计消费并通过验证"
|
||||||
|
updated_at: "2026-07-27"
|
||||||
|
base: "dev-v3"
|
||||||
|
generated: "2026-07-27T10:04:56+08:00"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 矩阵年度月度订单统计
|
||||||
|
|
||||||
|
> 后端契约已部署并经测试网关验证;`frontend_status` 独立反映管理端交付状态。
|
||||||
|
|
||||||
|
## 关联
|
||||||
|
|
||||||
|
- Issue: #5282
|
||||||
|
- PR: wx/HL#5286
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
### `GET /admin/fleet/matrix/month-counts`
|
||||||
|
|
||||||
|
一次查询指定年份的矩阵月度订单状态统计,统计口径与 `GET /admin/fleet/matrix/grid` 的 `statusCounts` 一致。
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `year` | query | `Integer` | 是 | 沿用矩阵 `YearMonth` 校验,越界返回 `605010` |
|
||||||
|
| `season` | query | `String` | 否 | 不传或空白时按 `active` 处理 |
|
||||||
|
| `fleetTeamIds` | query | `Long[]` | 否 | 可重复参数;空数组表示不过滤车队 |
|
||||||
|
| `typeKeys` | query | `String[]` | 否 | 可重复参数;空数组表示不过滤车型 |
|
||||||
|
|
||||||
|
响应 `data`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"year": 2026,
|
||||||
|
"months": [
|
||||||
|
{
|
||||||
|
"month": 1,
|
||||||
|
"statusCounts": {
|
||||||
|
"totalAssignments": 0,
|
||||||
|
"unassignedAssignments": 0,
|
||||||
|
"assignedAssignments": 0,
|
||||||
|
"totalOrders": 0,
|
||||||
|
"unassignedOrders": 0,
|
||||||
|
"partialOrders": 0,
|
||||||
|
"assignedOrders": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `months` 固定返回 12 项,按 `month=1..12` 升序;无订单月份不省略,各计数字段为 `0`。
|
||||||
|
- 一条跨月派车记录按实际服务日期覆盖的月份分别计数;已取消、已关闭订单沿用矩阵现有口径排除。
|
||||||
|
- 年份越界沿用矩阵业务错误码 `605010`;缺失必填参数沿用统一参数校验响应。
|
||||||
|
|
||||||
|
## 契约影响文件
|
||||||
|
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/matrix/controller/MatrixController.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/matrix/vo/MatrixMonthCountsReqVO.java`
|
||||||
|
- `hl-fleet-service/src/main/java/com/hulalv/fleet/matrix/vo/MatrixMonthCountsRespVO.java`
|
||||||
|
- `hl-fleet-service/src/test/java/com/hulalv/fleet/matrix/controller/MatrixControllerTest.java`
|
||||||
|
|
||||||
|
## 前端/调用方动作
|
||||||
|
|
||||||
|
- 车务矩阵页面按当前年份、赛季、车队和车型筛选请求本接口。
|
||||||
|
- 月份选择器读取对应月份的 `statusCounts.totalAssignments`;有效零值显示 `0`,请求尚未完成或失败显示 `--`。
|
||||||
|
- 跨年切换时按目标年份加载;可按筛选键缓存结果,筛选变化后重新获取。
|
||||||
|
- 数组查询参数必须使用重复 key(Axios `paramsSerializer` 的 `indexes` 设为 `null`),不要发送带下标的参数名。
|
||||||
|
|
||||||
|
## 兼容性与路由
|
||||||
|
|
||||||
|
- 新增 GET 路径,不修改既有路径、请求参数、响应字段、枚举或错误码,对既有消费者向后兼容。
|
||||||
|
- 网关已有 `Path=/admin/fleet/**` 路由覆盖,无需新增网关配置。
|
||||||
|
- 无 internal Feign 或 shared Java 契约变更。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端定向测试:`MatrixServiceTest,MatrixControllerTest` 共 39 项通过,覆盖固定 12 月零值、跨月、终态排除、筛选、数组绑定、JSON 和与 grid 的统计守恒。
|
||||||
|
- 后端完整门禁:`mvn -pl hl-fleet-service -am verify` 通过(2434 tests,0 failures/errors,1 skipped);Fleet Spotless 626 文件通过。
|
||||||
|
- 测试部署:PR `wx/HL#5286` 合入 `dev-v3`,Deploy Panel 任务 `57a121a0` 成功,8087/8187 双实例健康。
|
||||||
|
- 测试网关:2026 年返回固定 12 项且字段与派单/订单守恒通过;2099 年固定 12 项且全部计数为 `0`。脱敏证据:`C:/Users/Administrator/AppData/Local/hl-workflow/evidence/5282/gateway-month-counts.json`。
|
||||||
|
- DB 地面真相:`not_verified`;现有只读探针的数据源安全守卫拒绝本地地址,未绕过门禁。零值另由 2099 网关实测与 Service 测试覆盖。
|
||||||
|
- 管理端本地实现:全量 Vitest 125 文件 / 1160 项通过;生产构建、ESLint 通过;6 个改动文件 Prettier 检查通过。因前端分支尚未推送/发布,frontmatter 仍保持 `frontend_status: pending`。
|
||||||
|
- OpenAPI diff:`not_configured`。当前仓库仅提供 Swagger 2,且本机未配置 `oasdiff`;已人工核对路径、方法、参数、响应、空态、错误码、网关与消费者,未伪报自动 diff 通过。
|
||||||
|
- 人工契约证据:`C:/Users/Administrator/AppData/Local/hl-workflow/evidence/5282/contract-review.md`。
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5283"
|
||||||
|
title: "车务派车候选响应被订单刷新清空"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-admin"
|
||||||
|
frontend_ref: "2ee0988c235c0483a5b51f8cc0deb195d7fd8573"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "后端候选接口契约与日期冲突口径正常;前端 AssignModal 因同订单对象刷新而重复初始化,清空已成功返回的候选。"
|
||||||
|
updated_at: "2026-07-27"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 车务派车候选响应被订单刷新清空
|
||||||
|
|
||||||
|
> **服务**: `hl-fleet-service`
|
||||||
|
>
|
||||||
|
> **后端工单**: [wx/HL#5283](https://git.1814.love:8443/wx/HL/issues/5283)
|
||||||
|
>
|
||||||
|
> **前端消费端**: `mmg/hl-ui v2.1`
|
||||||
|
>
|
||||||
|
> **影响页面**: 车务管理 → 派车看板/矩阵 → 派车弹窗 `AssignModal`
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
本次不是后端候选过滤或 API 契约缺陷,不新增或修改接口、字段、类型、必填性、错误码及派单状态机:
|
||||||
|
|
||||||
|
- 同一真实请求经测试网关返回 `vehicleTotal=19`,其中 11 辆 `AVAILABLE`,其余车辆因真实日期冲突不可用;
|
||||||
|
- 专用测试车辆在目标日期区间均返回 `AVAILABLE`,车辆、车型、车队和维保状态未造成错误排除;
|
||||||
|
- 页面显示“共 0 辆”发生在响应成功之后:看板刷新/SSE 以新对象替换同一订单,`AssignModal` 再次执行 `resetOptionPages()` 并递增 `requestSeq`,从而清空候选并丢弃已返回结果;
|
||||||
|
- 不得通过放宽后端车辆/车队/车型或冲突过滤来掩盖前端状态重置问题。
|
||||||
|
|
||||||
|
该问题也属于 [#5279 车务派单全链路 E2E 前端缺陷交接](./27_5279_车务派单全链路E2E前端缺陷交接-修改接口-管理后台.md)“后台刷新不得重置正在编辑的派车草稿”的同类消费缺陷;#5283 补充了候选 19/11 的独立网关证据和稳定初始化键要求。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /admin/fleet/assignments/candidates
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <admin-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
接口继续返回车辆、司机两套独立分页候选。前端应消费 `vehicles.records/total` 与 `drivers.records/total`(兼容别名 `list` 仍保留),并以 `available`、`availabilityReasonCode`、`conflicts[]` 和 `availabilityWindows[]` 展示可用性;本次没有后端契约变化。
|
||||||
|
|
||||||
|
## 前端修复口径
|
||||||
|
|
||||||
|
### 1. 以稳定业务身份决定是否重新初始化
|
||||||
|
|
||||||
|
不得仅监听 `props.order` 对象身份。应使用 `initializationIdentity` 或等价稳定键,至少覆盖:
|
||||||
|
|
||||||
|
- `orderId`;
|
||||||
|
- 当前 `requirementId`;
|
||||||
|
- 目标 `assignmentSlotId/fleetItemIndex`;
|
||||||
|
- `assign/change` 模式及必要入口上下文。
|
||||||
|
|
||||||
|
只有上述业务身份真正变化,或用户主动关闭后重新打开弹窗时,才允许执行 `resetOptionPages()` 和重建派车草稿。同一订单仅因列表刷新或 SSE 生成新对象时不得重置。
|
||||||
|
|
||||||
|
### 2. 保留候选与编辑草稿
|
||||||
|
|
||||||
|
同一稳定业务身份下刷新订单对象时,必须保留:
|
||||||
|
|
||||||
|
- 车辆/司机候选 `records/total/page/pageSize`;
|
||||||
|
- 车队、车型、关键字和可用性筛选;
|
||||||
|
- 已选车辆、司机、日期和稳定槽位;
|
||||||
|
- 收费服务日、逐日价格、调价/免费原因;
|
||||||
|
- 跨常驻确认以及 `HOLD/DIRECT` 模式。
|
||||||
|
|
||||||
|
若服务端基线确实变化,继续使用现有 baseline-difference 强提示,由用户决定如何处理;不得静默清空或改写草稿。
|
||||||
|
|
||||||
|
### 3. 保留并发响应保护,但不得误杀当前请求
|
||||||
|
|
||||||
|
继续保留 `requestSeq` 或等价的旧响应隔离机制。只有新业务身份或新查询真正开始时才递增序号;同一订单对象替换不得把已经成功返回的当前候选标记为过期。快速连续刷新时,旧请求不能覆盖新请求,新请求成功结果也不能被无关初始化清空。
|
||||||
|
|
||||||
|
## 前端验收清单
|
||||||
|
|
||||||
|
- [ ] 专用测试订单 `26-4700`、`2026-08-04..2026-08-07`、SUV、3 人请求返回后,页面候选数量与接口一致,不再错误显示 0。
|
||||||
|
- [ ] 相同订单、需求、槽位和模式下,列表/SSE 替换订单对象后,车辆和司机候选仍保留。
|
||||||
|
- [ ] 候选分页总数、筛选条件、已选项和费用草稿不被后台刷新清空。
|
||||||
|
- [ ] 已切换的 `HOLD/DIRECT` 模式不会因同订单刷新恢复默认值。
|
||||||
|
- [ ] 订单、需求、槽位或模式真实变化时仍能正确重新初始化。
|
||||||
|
- [ ] 快速连续刷新时,旧响应不能覆盖新响应,当前成功响应也不会被误判过期。
|
||||||
|
- [ ] Network 记录仍使用现有候选接口,无新增或修改后端请求契约。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 测试网关与 Fleet 双实例同口径响应:`code=200`、车辆总数 19、可用 11、真实冲突 8;
|
||||||
|
- 目标测试车辆均为 `AVAILABLE`,目标日期冲突数为 0;
|
||||||
|
- 后端 worktree 零代码改动,当前候选过滤、日期闭区间冲突和 API 契约保持不变;
|
||||||
|
- 前端 HMR 现场曾出现稳定 `initializationIdentity` 方向的修正,但尚无 `mmg/hl-ui v2.1` 远端提交与发布验收依据,因此本文件保持 `frontend_status: "pending"`。
|
||||||
|
|
||||||
|
## 契约审查
|
||||||
|
|
||||||
|
- Frontend API:现有接口消费纠错,无 Controller/DTO/VO/字段变化,OpenAPI diff 为 `not_required`;
|
||||||
|
- Internal Feign / shared Java:无变化,Consumer Contract 为 `not_required`;
|
||||||
|
- 后端真值由已部署测试环境网关响应和现有 Fleet 源码确认;changelog 发布不代表前端已实现或已发布。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不修改 `hl-ui`,不在 `mmg/hl-ui` 创建工单;
|
||||||
|
- 不修改后端候选过滤、日期冲突、车辆/司机占用、派单状态机、计费、保险、通知或数据库;
|
||||||
|
- 不把前端实现、发布和页面验收冒充为本次后端代码交付。
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5284"
|
||||||
|
title: "建议车辆槽位由车务自由删减并按最终实派方案提交"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "not_required"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-pi"
|
||||||
|
frontend_ref: "d459b946"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "最终实派方案行为已由 #5262 部署;#5284 收口既有接口语义并纠正 #5245 旧前端口径;管理后台已由提交 d459b946 完成槽位自由删减和最终方案 batch 提交并通过验证。"
|
||||||
|
updated_at: "2026-07-27"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 车务:建议车辆槽位由车务自由删减
|
||||||
|
|
||||||
|
> **服务**: `hl-fleet-service`
|
||||||
|
>
|
||||||
|
> **工单**: [wx/HL#5284](https://git.1814.love:8443/wx/HL/issues/5284)
|
||||||
|
>
|
||||||
|
> **影响范围**: 车务管理 → 派单看板 → 订单派车弹窗的车辆槽位编辑与批量提交
|
||||||
|
|
||||||
|
## ⚠️ 关键纠错
|
||||||
|
|
||||||
|
截图中默认出现但没有删除入口的“车辆槽位 1”,是页面依据订单当前用车需求
|
||||||
|
`requiredVehicles[]` 展开的**定制师/订单建议槽位**,不是车务必须保留的最终槽位。
|
||||||
|
|
||||||
|
#5245 曾写“只删除新增且未提交的草稿槽位”。该表述只是在强调不能用本地删除撤销已有派车,
|
||||||
|
但被页面实现成了“建议生成的初始槽位不可删除”。**这个实现口径需要纠正:所有尚未提交的本地槽位都可删除,
|
||||||
|
包括建议生成的初始槽位和车务后加的槽位。最终配几辆、配什么车型由车务决定。**
|
||||||
|
|
||||||
|
边界保持不变:
|
||||||
|
|
||||||
|
- 编辑态允许暂时删至 0 个槽位,并保留“添加车辆槽位”入口;
|
||||||
|
- 最终提交时 `items[]` 仍必须包含 1–20 个完整槽位;
|
||||||
|
- 已经存在的 `holding/assigned/completed` 有效派车不是本地草稿,不能直接删除,继续走取消或改派流程。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
本次不新增 JSON 字段、路径或错误码,只明确现有接口的权威语义:
|
||||||
|
|
||||||
|
| 方法 | 路径 | 当前权威语义 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET` | `/admin/fleet/board/orders` | `requiredVehicles[]` 和 `assignmentProgress.suggestedSlots` 是订单建议,仅作参考 |
|
||||||
|
| `GET` | `/admin/fleet/board/orders/<orderId>` | `suggestedVehicleCount` 是建议数量,`actualVehicleCount` 是车务当前实派数量 |
|
||||||
|
| `POST` | `/admin/fleet/assignments/batch` | `items[]` 是车务本次保留的**完整最终实派方案**,无需覆盖全部建议槽位 |
|
||||||
|
|
||||||
|
## 批量提交契约
|
||||||
|
|
||||||
|
### `items[]` 是完整最终方案
|
||||||
|
|
||||||
|
假设订单建议 2 辆 SUV,页面可以删除两个建议槽位后重新添加 1 个槽位,并提交:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"orderId": "2080000000000000001",
|
||||||
|
"requirementId": "2080000000000000002",
|
||||||
|
"startDate": "2026-08-04",
|
||||||
|
"endDate": "2026-08-07",
|
||||||
|
"holdMode": 1,
|
||||||
|
"requestId": "fleet-final-plan-example",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"fleetItemIndex": 5,
|
||||||
|
"vehicleId": "2080000000000000101",
|
||||||
|
"driverId": "2080000000000000201"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
- `items[]` 数量可以少于、等于或多于建议数量,但必须为 1–20;
|
||||||
|
- `fleetItemIndex` 可沿用建议索引,也可使用未占用的新索引,不要求连续;
|
||||||
|
- 批内 `fleetItemIndex`、车辆和司机各自唯一;
|
||||||
|
- 未被最终方案保留的 `unassigned` 建议占位会退出待派和完成条件;
|
||||||
|
- 若漏传已有 `holding/assigned` 等在途槽位,后端拒绝整批提交并提示先取消,不会静默删除已有派车;
|
||||||
|
- 任一槽位失败仍整批回滚,前端不得循环调用单派接口替代批量提交。
|
||||||
|
|
||||||
|
### 看板建议数与实派数分离
|
||||||
|
|
||||||
|
最终方案提交后可能出现:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"assignmentProgress": {
|
||||||
|
"suggestedSlots": 2,
|
||||||
|
"finalizedByFleet": true,
|
||||||
|
"totalSlots": 1,
|
||||||
|
"unassignedSlots": 0,
|
||||||
|
"holdingSlots": 1,
|
||||||
|
"assignedSlots": 0,
|
||||||
|
"completedSlots": 0,
|
||||||
|
"canceledSlots": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
此时 `suggestedSlots=2` 只保留建议事实,页面进度、完成条件和最终车辆数都按 `totalSlots=1` 计算。
|
||||||
|
|
||||||
|
## 前端展示矩阵
|
||||||
|
|
||||||
|
| 场景 | 数据源 | 页面行为 | 守恒规则 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 订单建议 | `requiredVehicles[]`、`suggestedSlots` | 标注“建议”,只作为车型/座位/数量参考 | 不决定最终槽位数 |
|
||||||
|
| 建议生成的初始槽位 | 前端未提交草稿 | 与新增草稿相同,显示删除入口 | 可见草稿槽位均可删除 |
|
||||||
|
| 车务新增槽位 | 前端未提交草稿 | 显示删除入口 | 可见草稿槽位均可删除 |
|
||||||
|
| 删除到 0 个 | 本地草稿为空 | 展示空态和“添加车辆槽位”,禁用下一步/提交 | 编辑态可为 0,提交态最少 1 |
|
||||||
|
| 最终批量提交 | `items[]` | 只提交车务最终保留的槽位,不补回已删除建议 | 可见最终槽位与 `items[]` 一一对应 |
|
||||||
|
| 已有有效派车 | `activeAssignments[]` | 不显示本地“删除草稿”;使用取消/改派动作 | 不静默撤销 `holding/assigned` |
|
||||||
|
| 最终进度 | `assignmentProgress` | `finalizedByFleet=true` 后按 `totalSlots` 展示 | 状态数量之和等于最终 `totalSlots` |
|
||||||
|
|
||||||
|
## 前端处理清单
|
||||||
|
|
||||||
|
- [ ] 移除 `slot.isNewDraft === true` 对删除按钮的唯一门控;建议生成的未提交初始槽位也必须可删除。
|
||||||
|
- [ ] 删除任一未提交槽位后同步清理该槽位的车辆、司机、逐日车费和跨常驻确认草稿,不能残留参与提交。
|
||||||
|
- [ ] 删除当前槽位后切到相邻槽位;删至 0 个时进入明确空态,不读取已删除槽位的 picker 状态。
|
||||||
|
- [ ] 0 槽位时保留“添加车辆槽位”,并禁用下一步/最终提交;不要向后端发送空 `items[]`。
|
||||||
|
- [ ] 最终 payload 只由当前可见槽位生成;不得按 `requiredVehicles[]` 数量补回已删除建议槽位。
|
||||||
|
- [ ] `requiredVehicles[]`、`suggestedVehicleCount`、`suggestedSlots` 的文案统一为“建议/参考”,不得展示为车务必配数量。
|
||||||
|
- [ ] 已有 `activeAssignments[]` 不走本地草稿删除;继续使用后端下发的取消/改派动作。
|
||||||
|
- [ ] 雪花 ID 继续按字符串消费,`fleetItemIndex` 不要求连续且不得因删除后重排而串到已有稳定槽位。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 本工单不修改 `hl-ui`,不在 `mmg/hl-ui` 建工单;`frontend_status` 保持 `pending`。
|
||||||
|
- 不修改派单状态机、车辆/司机占用、通知、保险、对账、逐日车费、Outbox 或数据库结构。
|
||||||
|
- 不放宽空批次:编辑态可删至 0,不等于后端接受空 `items[]`。
|
||||||
|
- 不允许通过漏传已有有效派车实现静默删除。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端最终方案能力来自已部署的 #5262;#5284 未新增运行时行为,因此 `gateway_status=not_required`。
|
||||||
|
- 定向回归 5 项通过:建议数与最终数可不同、遗漏建议占位不阻塞完成、已有在途槽位漏传阻断、看板建议/实派分离、空 `items[]` 拒绝。
|
||||||
|
- Fleet 全量 `test`:2431 项,0 失败、0 错误、1 跳过。
|
||||||
|
- Fleet 模块 `spotless:check`:通过。
|
||||||
|
- `mvn -pl hl-fleet-service -am verify`:2431 项,0 失败、0 错误、1 跳过,JAR 构建成功。
|
||||||
|
- OpenAPI/oasdiff:`not_configured`;已人工比对路径、字段、类型和必填性,确认只有 Swagger/Javadoc 语义收口。
|
||||||
|
- Consumer Contract:`not_required`;没有 internal Feign 或 shared Java 变化。
|
||||||
|
- 前端状态:`pending`;领取后按 `pending → claimed → implemented → released → verified` 真实流转。
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5292"
|
||||||
|
title: "车务按服务日逐车配置用车、接机与价格"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-pi"
|
||||||
|
frontend_ref: "0a35da1e3b88f9c375d3e79516780e5a66f2b8ad"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: "2026-07-28T14:38:26+08:00"
|
||||||
|
status_note: "hl-admin 已修复可编辑 UNPLANNED/新槽位默认用车、ARRIVAL 参与状态保持及新增槽位候选有界加载;最终提交 0a35da1e 已在 origin/v2.1 可达,全量 checkpoint 通过"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fleet:按服务日逐车配置用车、接机与价格
|
||||||
|
|
||||||
|
> **服务**:`hl-fleet-service`
|
||||||
|
> **后端 PR**:[wx/HL#5296](https://git.1814.love:8443/wx/HL/pulls/5296)
|
||||||
|
> **Issue**:#5292
|
||||||
|
> **日期**:2026-07-27
|
||||||
|
> **影响范围**:管理后台车务看板派车弹窗和订单派车详情
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 关键变化
|
||||||
|
|
||||||
|
新派车页面不再提交“收取车费日期”。派车保存改为提交完整的“服务日期 × 稳定车辆槽位”矩阵,每个单元格独立说明是否用车、车辆、司机、ARRIVAL 接机参与和当天实际价格。
|
||||||
|
|
||||||
|
旧 `items` 输入暂时保留用于滚动发布兼容;旧收费日期字段只能随旧 `items` 使用,`dailyPlan` 模式提交这些字段会被拒绝。
|
||||||
|
|
||||||
|
## 2026-07-28 产品口径补充:默认全部行程用车
|
||||||
|
|
||||||
|
新建派车草稿进入“排车”步骤时,当前需求的**全部可编辑服务日、全部新建稳定车辆槽位均默认勾选“当天用车”**。车务只需要选择车辆、司机和逐日价格;只有主动取消某日勾选或点击“明确不用车”,才表示该日明确不用车。
|
||||||
|
|
||||||
|
### 初始化规则
|
||||||
|
|
||||||
|
1. 后端 `planState=UNPLANNED` 表示尚未形成最终方案,不等于 `NOT_USED`。前端首次把可编辑 `UNPLANNED` 日格转换为草稿时应初始化 `used=true`,复选框默认选中,并显示“待选车辆/司机”或等价规划中状态。
|
||||||
|
2. 新增车辆槽位时,该槽位覆盖的全部可编辑服务日同样默认 `used=true`,不得全部初始化为未勾选。
|
||||||
|
3. 已有 `planFinalized=true` 的 `USED/NOT_USED`、只读日格、已关账日格和已有有效派车必须按服务端事实保留;已经明确保存为 `NOT_USED` 的日期重新打开时仍保持未选中,不得重新默认用车。
|
||||||
|
4. 默认选中只是前端草稿口径,不代表已经完成派车。未选择车辆、司机或逐日价格时仍不得提交,也不得伪造 `planFinalized=true`。
|
||||||
|
5. 用户主动取消“当天用车”时才写入草稿 `used=false/planState=NOT_USED`;某日所有槽位均不用车时继续执行 `confirmNoVehicleServiceDates=true` 二次确认。
|
||||||
|
|
||||||
|
### 当前前端偏差
|
||||||
|
|
||||||
|
前端提交 `7b99fe7d35530073176af1a0376cba7b33503319` 中,`createDailyVehiclePlan()` 在详情提供 `dailyVehiclePlan` 时把所有未最终确认日格按 `UNPLANNED + used=false` 直接用于草稿;`appendDailyVehiclePlanSlot()` 也把新槽位日格初始化为 `used=false`。因此页面出现整段行程“尚未规划”、所有“当天用车”均未选中的状态,与本次明确口径不符。
|
||||||
|
|
||||||
|
前端应区分“服务端基线状态”和“当前编辑草稿默认值”,不能通过把 `UNPLANNED` 改成服务端 `USED` 来伪造最终事实;仅在可编辑的新草稿层默认选中。
|
||||||
|
|
||||||
|
### 前端验收清单
|
||||||
|
|
||||||
|
- [ ] 4 天行程首次进入排车步骤时,4 个可编辑日格的“当天用车”全部默认选中。
|
||||||
|
- [ ] 候选尚未选定时显示待选车辆/司机而非“明确不用车”,且下一步仍因车辆、司机或价格缺失而受阻。
|
||||||
|
- [ ] 新增车辆槽位后,该槽位全部可编辑服务日也默认选中。
|
||||||
|
- [ ] 车务取消其中一天后,仅该日变为明确不用车,其余日期保持选中和已编辑草稿。
|
||||||
|
- [ ] 已保存的 `NOT_USED`、已有有效派车及只读日期重新打开后保持服务端事实,不被默认逻辑覆盖。
|
||||||
|
- [ ] 补充 `createDailyVehiclePlan()`、`appendDailyVehiclePlanSlot()` 和真实挂载 `FleetAssignModal` 的回归测试,覆盖首次进入、增加槽位、重新打开以及主动取消用车。
|
||||||
|
- [ ] 提交请求仍满足:每个 `used=true` 日格具备车辆、司机和价格;全天不用车时带明确二次确认。
|
||||||
|
|
||||||
|
## 2026-07-28 页面阻断:ARRIVAL 接机参与无法选中
|
||||||
|
|
||||||
|
真实页面复验中,服务日 **2026-08-04** 已标记“要求接机”,该日格已勾选“当天用车”、已选车辆和司机、无只读或锁定提示,但点击“参与 ARRIVAL 接机 / 接站”后复选框无法保持选中。该操作发生在派车草稿提交前,属于前端日格交互与状态同步阻断,不是后端保存接口拒绝。
|
||||||
|
|
||||||
|
当前 `origin/v2.1@e744c909` 中:
|
||||||
|
|
||||||
|
- `DailyVehiclePlanMatrix.vue` 的复选框仅发出 `update:pickup(key, checked)`;
|
||||||
|
- `AssignModal.vue` 的 `handleDailyPlanPickupUpdate()` 只将结果写入本地 `dailyPlan`;
|
||||||
|
- 现有 `daily-vehicle-plan-matrix.spec.js` 只断言子组件已发出事件,没有挂载 `FleetAssignModal` 验证父层接收后、候选状态 watcher 运行后以及切换日格后的值是否仍为 `true`。
|
||||||
|
|
||||||
|
因此,子组件事件测试通过不能作为页面可用证据。前端需要从浏览器事件开始逐段核对 `NCheckbox → update:pickup → handleDailyPlanPickupUpdate → dailyPlan → 提交 payload`,找出勾选值被丢弃或覆盖的位置;不得通过跳过逐日方案校验或伪造后端字段规避。
|
||||||
|
|
||||||
|
### 前端验收清单
|
||||||
|
|
||||||
|
- [ ] 对 `pickupRequired=true`、`used=true` 且可编辑的日格,点击后复选框立即选中,并在多轮 `nextTick`、候选状态刷新以及价格编辑后保持选中。
|
||||||
|
- [ ] 切换到其他服务日再返回,接机参与状态仍保留;再次点击可以明确取消。
|
||||||
|
- [ ] 更新只作用于当前 `serviceDate + fleetItemIndex`,不得串改同日其他车辆或其他服务日。
|
||||||
|
- [ ] 将该日改为不用车时自动清除 `pickupParticipant`;重新用车后由车务再次明确选择,不沿用陈旧值。
|
||||||
|
- [ ] 提交前的 `dailyPlan` 以及最终请求体均包含该日格 `pickupParticipant=true`;后端返回成功后详情回显一致。
|
||||||
|
- [ ] 要求接机的服务日至少一辆实际用车标记参与接机;未选择时继续显示既有校验提示,选择后提示收敛。
|
||||||
|
- [ ] 新增 `FleetAssignModal` 父子联动回归测试,覆盖事件接收、watcher 稳定、日格切换和 payload;不能只断言 `DailyVehiclePlanMatrix` 发出了事件。
|
||||||
|
- [ ] 页面 Console 无 `Maximum recursive updates exceeded` 或 `unhandledrejection`,复选框操作不得重新触发无界候选请求。
|
||||||
|
|
||||||
|
在该页面交互修复并完成真实浏览器复验前,`frontend_status` 保持 `claimed`,不得流转为 `implemented/released/verified`。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 1 | 原子提交最终派车方案 | POST | `/admin/fleet/assignments/batch` | 请求体扩展与校验调整 | 新增完整 `dailyPlan`,保留旧 `items` 兼容 |
|
||||||
|
| 2 | 车务看板订单详情 | GET | `/admin/fleet/board/orders/<orderId>` | 响应字段新增 | 返回逐日逐车计划、每车小计和订单车辆总计 |
|
||||||
|
|
||||||
|
## 二、原子提交最终派车方案
|
||||||
|
|
||||||
|
### `POST /admin/fleet/assignments/batch`
|
||||||
|
|
||||||
|
**请求 VO**:`BatchCreateAssignmentReqVO`
|
||||||
|
|
||||||
|
### 新增顶层字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 约束 | 说明 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `dailyPlan` | `DailyPlanItem[]` | 新页面必填 | 与旧 `items` 二选一;最多 4000 项 | 完整“服务日期 × 稳定车辆槽位”矩阵 |
|
||||||
|
| `confirmNoVehicleServiceDates` | `Boolean` | 条件必填 | 某日全部槽位 `used=false` 时必须为 `true` | 全天无需用车二次确认 |
|
||||||
|
|
||||||
|
`orderId`、`requirementId` 和所有 Long ID 继续按 JSON 字符串传输。
|
||||||
|
|
||||||
|
### `DailyPlanItem`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 约束 | 说明 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `fleetItemIndex` | `Integer` | 是 | `>= 0` | 稳定车辆槽位序号 |
|
||||||
|
| `serviceDate` | `String(date)` | 是 | `yyyy-MM-dd`,必须属于当前需求服务日 | 服务日期 |
|
||||||
|
| `used` | `Boolean` | 是 | - | 当天是否实际用车 |
|
||||||
|
| `vehicleId` | `String(Long)` | 条件必填 | `used=true` 必填;不用车必须为空 | 当天车辆 |
|
||||||
|
| `driverId` | `String(Long)` | 条件必填 | `used=true` 必填;不用车必须为空 | 当天司机 |
|
||||||
|
| `pickupParticipant` | `Boolean` | 否 | 仅表示 ARRIVAL 接机/接站;不用车不得为 `true` | 当天是否参与接机 |
|
||||||
|
| `assignmentPrice` | `String(decimal)` | 条件必填 | `used=true` 必填;非负、整数最多 10 位、小数最多 2 位 | 本车当天实际价格,可为 `0.00` |
|
||||||
|
| `priceAdjustmentReason` | `String` | 条件必填 | 最多 256 字;实际价偏离价格日历时必填 | 改价原因 |
|
||||||
|
| `confirmCrossResident` | `Boolean` | 否 | 跨常驻车时按既有规则确认 | 跨常驻车确认 |
|
||||||
|
|
||||||
|
### 正确请求示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"orderId": "789",
|
||||||
|
"orderNo": "26-4165",
|
||||||
|
"requirementId": "790",
|
||||||
|
"startDate": "2026-08-04",
|
||||||
|
"endDate": "2026-08-05",
|
||||||
|
"headcount": 3,
|
||||||
|
"holdMode": 0,
|
||||||
|
"fromEntry": "from-board",
|
||||||
|
"requestId": "daily-plan-26-4165-v1",
|
||||||
|
"confirmNoVehicleServiceDates": false,
|
||||||
|
"dailyPlan": [
|
||||||
|
{
|
||||||
|
"fleetItemIndex": 0,
|
||||||
|
"serviceDate": "2026-08-04",
|
||||||
|
"used": true,
|
||||||
|
"vehicleId": "701",
|
||||||
|
"driverId": "801",
|
||||||
|
"pickupParticipant": true,
|
||||||
|
"assignmentPrice": "800.00",
|
||||||
|
"priceAdjustmentReason": "首日短途优惠"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fleetItemIndex": 1,
|
||||||
|
"serviceDate": "2026-08-04",
|
||||||
|
"used": true,
|
||||||
|
"vehicleId": "702",
|
||||||
|
"driverId": "802",
|
||||||
|
"pickupParticipant": false,
|
||||||
|
"assignmentPrice": "900.00",
|
||||||
|
"priceAdjustmentReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fleetItemIndex": 0,
|
||||||
|
"serviceDate": "2026-08-05",
|
||||||
|
"used": false,
|
||||||
|
"vehicleId": null,
|
||||||
|
"driverId": null,
|
||||||
|
"pickupParticipant": false,
|
||||||
|
"assignmentPrice": null,
|
||||||
|
"priceAdjustmentReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fleetItemIndex": 1,
|
||||||
|
"serviceDate": "2026-08-05",
|
||||||
|
"used": true,
|
||||||
|
"vehicleId": "702",
|
||||||
|
"driverId": "802",
|
||||||
|
"pickupParticipant": false,
|
||||||
|
"assignmentPrice": "900.00",
|
||||||
|
"priceAdjustmentReason": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 提交规则
|
||||||
|
|
||||||
|
- 每个保留槽位必须覆盖当前需求的全部服务日;同一 `fleetItemIndex + serviceDate` 不能重复。
|
||||||
|
- 同一服务日不能重复使用同一车辆或同一司机。
|
||||||
|
- `used=false` 不占用车辆/司机、不投保、不计费;车辆、司机、接机和价格必须为空或 false。
|
||||||
|
- 某日所有槽位均不用车时允许提交,但必须设置 `confirmNoVehicleServiceDates=true`。
|
||||||
|
- 大交通 ARRIVAL 要求平台接机时,当日至少一辆 `used=true` 的车辆必须 `pickupParticipant=true`。
|
||||||
|
- 大交通未要求接机时,仍允许人工标记一辆或多辆使用中的车辆参与接机。
|
||||||
|
- 连续日期使用相同车辆和司机时后端自动合并派车组;逐日价格仍分别冻结。
|
||||||
|
- 过去日期、已完结日期或已关账对账期不能修改。
|
||||||
|
- 新 `dailyPlan` 不得提交 `chargeableServiceDates`、`vehicleFeeWaiverReason`、`confirmAllServiceDatesFree`。
|
||||||
|
|
||||||
|
### 参数错误响应
|
||||||
|
|
||||||
|
本项目参数校验失败沿用 HTTP 200 + 业务 `code=400`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"message": "dailyPlan 与旧 items 必须二选一,dailyPlan 不得提交旧收费日期字段",
|
||||||
|
"data": null,
|
||||||
|
"success": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、车务看板订单详情
|
||||||
|
|
||||||
|
### `GET /admin/fleet/board/orders/<orderId>`
|
||||||
|
|
||||||
|
**响应 VO**:`Result<BoardOrderDetailVO>`
|
||||||
|
|
||||||
|
### 新增响应字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 空值规则 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `dailyVehiclePlan` | `DailyVehiclePlanVO[]` | 无记录返回 `[]` | 按服务日期、槽位序号稳定排序 |
|
||||||
|
| `vehicleFeeSummaries` | `VehicleFeeSummaryVO[]` | 无实际用车返回 `[]` | 按实际车辆汇总小计 |
|
||||||
|
| `vehicleFeeTotal` | `String(decimal)` | 无费用返回 `"0.00"` | 当前需求全部实际用车日总计 |
|
||||||
|
|
||||||
|
### `DailyVehiclePlanVO`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `serviceDate` | `String(date)` | 服务日期 |
|
||||||
|
| `fleetItemIndex` | `Integer` | 稳定槽位序号 |
|
||||||
|
| `assignmentSlotId` | `String(Long)` | 稳定槽位 ID |
|
||||||
|
| `assignmentId` | `String(Long)` | 每日切片 ID;显式不用车也返回占位 ID |
|
||||||
|
| `assignmentGroupId` | `String(Long)` | 当前连续派车组 ID |
|
||||||
|
| `planFinalized` | `Boolean` | 该日格是否已经业务最终确认 |
|
||||||
|
| `planState` | `String` | `UNPLANNED`(建议占位)/ `NOT_USED`(明确不用车)/ `USED`(实际用车) |
|
||||||
|
| `used` | `Boolean` | 当天是否实际用车;必须结合 `planFinalized/planState` 区分未规划与明确不用车 |
|
||||||
|
| `pickupParticipant` | `Boolean` | 当天车辆是否参与 ARRIVAL 接机 |
|
||||||
|
| `pickupRequired` | `Boolean` | 大交通当天是否要求接机 |
|
||||||
|
| `vehicleId` / `driverId` | `String(Long)` | 不用车时为 `null` |
|
||||||
|
| `vehiclePlate` / `vehicleModel` / `driverName` | `String` | 不用车时为 `null` |
|
||||||
|
| `driverPhone` | `String` | 脱敏手机号;不用车时为 `null` |
|
||||||
|
| `calendarPrice` | `String(decimal)` | 价格日历参考价;缺价或不用车时为 `null` |
|
||||||
|
| `assignmentPrice` | `String(decimal)` | 本车当天实际价;不用车时为 `null` |
|
||||||
|
| `priceSource` | `String` | `CALENDAR` / `OVERRIDE` / `MISSING` / `NOT_USED` |
|
||||||
|
| `priceAdjustmentReason` | `String` | 改价原因,无则 `null` |
|
||||||
|
| `assignmentStatus` | `String` | 基础派单状态;显式不用车为 `unassigned` |
|
||||||
|
| `readOnly` | `Boolean` | 过去、已完结或已关账日期为 `true` |
|
||||||
|
| `readOnlyReason` | `String` | `服务日期已过去` / `派单已完结` / `对账期已关账`,可编辑时为 `null` |
|
||||||
|
|
||||||
|
### `VehicleFeeSummaryVO`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `vehicleId` | `String(Long)` | 实际车辆 ID |
|
||||||
|
| `vehiclePlate` | `String` | 车牌 |
|
||||||
|
| `vehicleModel` | `String` | 车型 |
|
||||||
|
| `amount` | `String(decimal)` | 该车辆全部实际用车日小计 |
|
||||||
|
|
||||||
|
金额守恒:`vehicleFeeTotal = sum(vehicleFeeSummaries[].amount) = sum(dailyVehiclePlan[used=true].assignmentPrice)`。
|
||||||
|
|
||||||
|
## 四、前端改造清单
|
||||||
|
|
||||||
|
1. 删除派车弹窗中的“收取车费日期”和免费日期提交逻辑。
|
||||||
|
2. 按服务日渲染稳定车辆槽位矩阵;每格维护 `used`、车辆、司机、ARRIVAL 接机参与和当天实际价格。
|
||||||
|
3. 某日全不用车时显示二次确认,并提交 `confirmNoVehicleServiceDates=true`。
|
||||||
|
4. 默认价格使用详情/报价返回的日历价;修改价格时强制填写原因,允许 `0.00`。
|
||||||
|
5. 详情展示每辆车小计和订单车辆总计;金额以字符串解析,不能用浮点累计。
|
||||||
|
6. `readOnly=true` 的日格禁止编辑,并展示 `readOnlyReason`。
|
||||||
|
7. 新页面只提交 `dailyPlan`,不得同时提交旧 `items` 或收费日期字段。
|
||||||
|
|
||||||
|
## 五、数据库和历史数据
|
||||||
|
|
||||||
|
- `fleet_assignment.daily_vehicle_used`:`1` 表示当天实际用车,`0` 表示明确不用车;滚动发布期间允许 `NULL`,读取侧按车辆/司机事实回退,避免旧节点新写入被误判。
|
||||||
|
- `fleet_assignment.pickup_participant`:`1` 表示当天该车参与 ARRIVAL 接机/接站;滚动发布期间允许 `NULL` 并按 `false` 兼容。
|
||||||
|
- 历史已派日迁移为实际用车;原免费日实际价格迁为 `0.00`;未派占位迁为不用车;历史接机参与默认 `false`。
|
||||||
|
- 全程明确不用车可由 Fleet 以 `vehicleCount=0` 完成需求;Order 端车辆和司机快照均为空。
|
||||||
|
|
||||||
|
## 六、不影响范围
|
||||||
|
|
||||||
|
- 不修改 `hl-ui` 仓库,由本 changelog 交接前端。
|
||||||
|
- 不改变单派接口及旧 `items` 滚动兼容输入。
|
||||||
|
- 不把 DEPARTURE 送机/送站映射到 `pickupParticipant`。
|
||||||
|
- 不改变订单或产品价格日历接口。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 PR #5296 已 squash 合并至 `dev-v3`,`hl-order-service-v3` 与 `hl-fleet-service` 已滚动部署测试环境,双实例健康检查通过。
|
||||||
|
- Fleet 最新 `dev-v3` reactor verify:2452 项测试,0 failures,0 errors,1 skipped;Spotless、Jar、JaCoCo 均成功。
|
||||||
|
- Order 全量:6759 项测试,0 failures,0 errors,29 skipped;零车辆回调生产者/消费者及迁移定向回归通过。
|
||||||
|
- 真实测试网关 `GET /admin/fleet/board/orders/<orderId>`:HTTP 200、业务 code 200;返回 3 条 `dailyVehiclePlan`,包含 `planFinalized`、`planState`、`used`、接机、价格和只读字段;逐车汇总数组及字符串总计存在。
|
||||||
|
- 真实测试网关 `POST /admin/fleet/assignments/batch` 安全负例:`dailyPlan` 携带旧收费日期字段时 HTTP 200、业务 code 400,确认新旧模式互斥;该探针不产生业务写入。
|
||||||
|
- OpenAPI diff 与 Spring Cloud Contract 未配置;已通过源码字段对比、Controller/Service 及 Fleet → Order 双端普通测试提供人工回退证据,未冒充工具通过。
|
||||||
|
|
||||||
|
当前状态:
|
||||||
|
|
||||||
|
- `backend_status: deployed`
|
||||||
|
- `gateway_status: verified`
|
||||||
|
- `frontend_status: claimed`
|
||||||
|
|
||||||
|
关联 Issue:[wx/HL#5292](https://git.1814.love:8443/wx/HL/issues/5292)
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5299"
|
||||||
|
title: "车务矩阵车辆司机预选与常驻标记"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-admin"
|
||||||
|
frontend_ref: "45c713a8b30b13c9b57d102cfcce82885744a7e9"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: "2026-07-28T14:38:26+08:00"
|
||||||
|
status_note: "hl-admin 已修复 FleetAssignModal 递归更新、补齐车辆预选/常驻三态与车辆行实际司机汇总;最终提交 45c713a8 已在 origin/v2.1 可达,checkpoint 通过"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fleet:矩阵车辆司机预选与常驻标记
|
||||||
|
|
||||||
|
> **服务**:`hl-fleet-service`
|
||||||
|
> **Issue**:#5299
|
||||||
|
> **影响页面**:管理后台车务管理 → 矩阵派单
|
||||||
|
> **兼容性**:仅新增响应字段,旧客户端可继续忽略
|
||||||
|
|
||||||
|
## 问题与目标
|
||||||
|
|
||||||
|
当前矩阵车辆行虽然已有常驻司机姓名,但缺少常驻司机稳定 ID;订单甘特条也没有直接返回该派车组的实际车辆、司机及常驻关系。页面因此无法稳定完成以下行为:
|
||||||
|
|
||||||
|
- 从具体车辆上下文发起派单时自动带出车辆和可派常驻司机;
|
||||||
|
- 行头展示车辆常驻司机;
|
||||||
|
- 已排订单条块展示实际执行司机,并区分常驻/临时司机。
|
||||||
|
|
||||||
|
本次后端补齐稳定读契约。前端不得按姓名判断常驻关系,也不得复制相邻订单的临时司机作为新派单默认值。
|
||||||
|
|
||||||
|
## 2026-07-28 前端运行态阻断:`FleetAssignModal` 递归更新
|
||||||
|
|
||||||
|
### 现场结论
|
||||||
|
|
||||||
|
前端实现提交 `bfcfafc69335f289071fe3e6dce74bacb636c55d` 已进入 `v2.1`,但当前测试页面打开逐日派车弹窗并加载车辆、司机候选后稳定出现:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Maximum recursive updates exceeded in component <FleetAssignModal>
|
||||||
|
```
|
||||||
|
|
||||||
|
浏览器 Console 同时记录多次 `unhandledrejection`;Network 中多条 `POST /admin/fleet/assignments/candidates` 均返回 HTTP 200,但车辆、司机区域持续停留在 loading,无法进入下一步。因此本次不是候选接口、日期冲突或后端状态机错误,而是前端实现后的响应式自反馈回归。`frontend_status: "implemented"` 仅表示代码已存在,不代表已发布或页面闭环。
|
||||||
|
|
||||||
|
### 高可信自反馈链
|
||||||
|
|
||||||
|
当前 `AssignModal.vue` 的深度 watcher 同时观察 `candidateEvidence`、`candidateSelectionReady`、`autoSelectedResidentDriverSource` 等候选派生状态,并在回调 `syncActiveSlotSelection()` 中无条件重建 `dailyPlan`、回写当前槽位。`dailyPlan` 又参与计算其他槽位排除 ID、候选可选态和新的 `candidateEvidence`;即使业务值没有变化,新数组/对象身份仍会再次触发同一 watcher,形成“观察候选派生值 → 回写逐日方案 → 候选派生值重新计算 → 再次回写”的闭环。
|
||||||
|
|
||||||
|
前端修复必须同时满足:
|
||||||
|
|
||||||
|
1. `syncActiveSlotSelection()` 先比较当前日格与待写 payload;语义完全相同时直接返回,不创建新 `dailyPlan`/槽位对象。
|
||||||
|
2. watcher 使用稳定原始值或稳定 fingerprint,不深度监听会被自身回写间接失效的派生对象;候选响应、用户选择和槽位同步应有单向边界。
|
||||||
|
3. `updateDailyVehiclePlanCell()` 或等价更新器在无真实字段变化时返回原引用,禁止仅因对象重建触发后续 effect。
|
||||||
|
4. 保留 #5283 的稳定 `initializationIdentity` 与 `requestSeq` 旧响应隔离;不得退回监听 `props.order` 对象身份或通过删除并发保护掩盖循环。
|
||||||
|
5. 正常打开弹窗只允许一次初始候选请求;需要自动常驻司机二次校验时最多再请求一次。状态稳定后不得继续请求,loading 必须收敛。
|
||||||
|
|
||||||
|
### 前端回归验收
|
||||||
|
|
||||||
|
- [ ] 打开订单逐日派车弹窗并取得候选成功响应后,Console 不再出现 `Maximum recursive updates` 或相关 `unhandledrejection`。
|
||||||
|
- [ ] 候选请求数量有明确上界;自动常驻司机场景最多“初始查询 + 携两侧 ID 二次校验”,不存在持续请求。
|
||||||
|
- [ ] 车辆、司机列表结束 loading,接口返回的分页总数与页面一致,可正常进入下一步。
|
||||||
|
- [ ] 已选车辆、司机、逐日费用和跨常驻确认写回一次后保持稳定,多轮 `nextTick` 不再重建相同 `dailyPlan`。
|
||||||
|
- [ ] 相同业务身份的 SSE/列表对象替换仍保留候选与草稿;真实订单、需求、槽位或模式变化时才重新初始化。
|
||||||
|
- [ ] 新增真实挂载 `FleetAssignModal` 的回归测试,模拟候选成功和自动常驻二次校验,断言无未处理 Promise、请求次数有界且草稿稳定;仅做静态源码断言不足以验收。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
### `GET /admin/fleet/matrix/grid`
|
||||||
|
|
||||||
|
`data.vehicles[]` 新增:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 空值规则 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `primaryDriverId` | `String(Long)` | 无常驻司机为 `null` | 车辆主档的权威常驻司机 ID |
|
||||||
|
|
||||||
|
既有 `primaryDriverName`、`primaryDriverPhone` 继续返回;手机号保持脱敏。三个字段共同用于行头展示,常驻判断以 ID 为准。
|
||||||
|
|
||||||
|
`data.vehicles[].assignments[]` 新增:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 空值规则 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `vehicleId` | `String(Long)` | 未派为空 | 该甘特条对应派车组的实际车辆 ID |
|
||||||
|
| `vehiclePlate` | `String` | 未派为空 | 实际车牌 |
|
||||||
|
| `driverId` | `String(Long)` | 未派为空 | 该派车组实际司机 ID |
|
||||||
|
| `driverName` | `String` | 未派为空 | 该派车组实际司机姓名 |
|
||||||
|
| `residentMatch` | `Boolean` | 无实际司机为 `null` | `true`=实际司机是该车常驻司机;`false`=实际司机存在但不是该车常驻司机或车辆无常驻 |
|
||||||
|
|
||||||
|
矩阵继续排除取消切片;同一派车组存在取消日缺口时按有效连续服务段拆成多个甘特条,不得把外包络日期当作司机持续占用。
|
||||||
|
|
||||||
|
## 前端必须调整
|
||||||
|
|
||||||
|
### 1. 从矩阵车辆上下文发起派单
|
||||||
|
|
||||||
|
1. 将所在行 `vehicles[].id` 直接作为当前选择车辆,并在候选请求中传 `selectedVehicleId`。
|
||||||
|
2. 候选接口 `POST /admin/fleet/assignments/candidates` 会返回既有字段 `selectedVehicleResidentDriver`。
|
||||||
|
3. 仅当该快照存在且 `available=true` 时,才把其司机 ID 作为默认司机。
|
||||||
|
4. 无常驻司机、常驻司机冲突或不可派时,车辆仍保持预选,司机保持“待选择”,并展示候选返回的不可用原因。
|
||||||
|
5. 不得取前后相邻订单的实际司机作为新订单默认司机。
|
||||||
|
|
||||||
|
### 2. 矩阵司机展示
|
||||||
|
|
||||||
|
#### 2.1 车辆行司机汇总数据源
|
||||||
|
|
||||||
|
后端契约已经足够,前端不得新增接口或按车牌反查司机:
|
||||||
|
|
||||||
|
- 常驻司机取当前车辆行 `vehicles[].primaryDriverId`、`primaryDriverName`;常驻关系以 ID 为准。
|
||||||
|
- 订单实际司机只取同一车辆行当前响应中的 `vehicles[].assignments[].driverId`、`driverName`;这些是当前矩阵月份和筛选条件已加载的真实派车段。
|
||||||
|
- `assignments[]` 后端已按月内 `startDay` 升序返回。前端按响应数组顺序扫描,以司机首次出现的位置作为其他司机的稳定展示顺序,不按姓名另行排序,也不混入上一月份、上一筛选条件、未派窗口、候选列表或 `drivers` 页面数据。
|
||||||
|
- `residentMatch` 继续用于订单条块的常驻/临时三态标记;车辆行汇总去重使用稳定 `driverId`,不得只按姓名猜测同一人。
|
||||||
|
|
||||||
|
#### 2.2 汇总与展示规则
|
||||||
|
|
||||||
|
1. 若 `primaryDriverId` 非空,先把常驻司机放在结果第一项,显示 `primaryDriverName(常驻)`。ID 存在但姓名异常为空时使用 `姓名未标注(常驻)`,不得输出空白项。
|
||||||
|
2. 随后按 `assignments[]` 当前顺序遍历实际司机:`driverId` 或去空后的 `driverName` 为空则跳过;相同 `driverId` 只保留第一次出现。
|
||||||
|
3. 订单实际司机与 `primaryDriverId` 相同时,不再追加普通姓名,只保留第一项带“(常驻)”标记的展示。
|
||||||
|
4. 其他实际司机按首次出现顺序追加,使用中文逗号 `,` 连接。不得把同一司机跨多个订单或拆分派车段重复展示。
|
||||||
|
5. 无常驻司机但存在订单实际司机时,直接展示实际司机汇总,**不得只显示“无常驻司机”**。
|
||||||
|
6. 只有常驻司机和订单实际司机都不存在时,才显示“无常驻司机”。若行宽不足允许视觉省略,但必须通过 `title`、tooltip 或等价交互查看完整汇总,不得静默丢失司机。
|
||||||
|
|
||||||
|
展示样例:
|
||||||
|
|
||||||
|
| 常驻司机 | 当前行订单司机(按首次出现顺序) | 车辆行展示 |
|
||||||
|
|---|---|---|
|
||||||
|
| 张三 | 张三、李四、王五、李四、赵六 | `张三(常驻),李四,王五,赵六` |
|
||||||
|
| 无 | 李四、王五、李四 | `李四,王五` |
|
||||||
|
| 张三 | 张三、张三 | `张三(常驻)` |
|
||||||
|
| 无 | 空 | `无常驻司机` |
|
||||||
|
|
||||||
|
#### 2.3 订单条块保持既有语义
|
||||||
|
|
||||||
|
已排订单条块继续展示本条 `driverName`:
|
||||||
|
|
||||||
|
- `residentMatch=true`:标记“常驻”。
|
||||||
|
- `residentMatch=false`:标记“临时”。
|
||||||
|
- `residentMatch=null`:显示“待派司机”。
|
||||||
|
|
||||||
|
不要从 `drivers` 页面列表或姓名/车牌文本反推常驻关系;所有 Long ID 继续按字符串比较,禁止转为 JS `Number`。
|
||||||
|
|
||||||
|
#### 2.4 前端验收清单
|
||||||
|
|
||||||
|
- [ ] 截图中车辆无常驻司机但订单已有实际司机时,车辆行显示订单实际司机姓名,不再只显示“无常驻司机”。
|
||||||
|
- [ ] 常驻司机与多名订单司机并存时,展示严格为 `张三(常驻),李四,王五,赵六`,常驻第一且只出现一次。
|
||||||
|
- [ ] 同一实际司机出现在多个订单或多个有效派车段时只展示一次;空 ID、空姓名和未派条块不产生空白分隔项。
|
||||||
|
- [ ] 其他司机顺序跟随当前 `assignments[]` 首次出现顺序;切换月份、车队、车型或状态筛选后按新响应重新计算,不残留旧司机。
|
||||||
|
- [ ] 订单条块的 `driverName/residentMatch` 常驻、临时、待派展示不回归。
|
||||||
|
- [ ] 补充纯汇总函数或 `VehicleGantt` 组件测试,至少覆盖上表四组样例、Long ID 字符串去重和筛选响应替换。
|
||||||
|
- [ ] 真实页面复验行宽溢出场景可查看完整司机列表,Console 无异常,且不增加司机列表或候选接口请求。
|
||||||
|
|
||||||
|
在本节完成并通过真实矩阵页面复验前,`frontend_status` 保持 `claimed`,不得流转为 `implemented/released/verified`。
|
||||||
|
|
||||||
|
### 3. 同步完成 #5292 页面改造
|
||||||
|
|
||||||
|
用户截图仍出现“收取车费日期”,说明当前测试页面仍在使用旧构建或旧逻辑。新页面必须继续执行 #5292:
|
||||||
|
|
||||||
|
- 删除“收取车费日期”及旧免费日期提交逻辑;
|
||||||
|
- 使用 `dailyVehiclePlan` 渲染“服务日期 × 稳定车辆槽位”;
|
||||||
|
- 保存时只提交 `dailyPlan`,不得同时提交旧 `items`、`chargeableServiceDates`、`vehicleFeeWaiverReason` 或 `confirmAllServiceDatesFree`。
|
||||||
|
|
||||||
|
## 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "9007199254740993",
|
||||||
|
"plate": "蒙A-88888",
|
||||||
|
"primaryDriverId": "9007199254740994",
|
||||||
|
"primaryDriverName": "王师傅",
|
||||||
|
"primaryDriverPhone": "138****1234",
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "9007199254740995",
|
||||||
|
"assignmentGroupId": "9007199254740996",
|
||||||
|
"vehicleId": "9007199254740993",
|
||||||
|
"vehiclePlate": "蒙A-88888",
|
||||||
|
"driverId": "9007199254740994",
|
||||||
|
"driverName": "王师傅",
|
||||||
|
"residentMatch": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
所有 Long ID 仍按 JSON 字符串处理,禁止转为 JS `Number`。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不修改派单状态机、车辆/司机占用、保险、对账或常驻关系写入。
|
||||||
|
- 不自动选择不可派司机,不绕过候选接口与最终派单锁内校验。
|
||||||
|
- 不修改 `hl-ui` 仓库;前端消费状态独立流转。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 PR [wx/HL#5300](https://git.1814.love:8443/wx/HL/pulls/5300) 已 squash 合并至 `dev-v3`,合并提交 `fca8cedc6`。
|
||||||
|
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康。
|
||||||
|
- 定向矩阵测试:43 tests,0 failures,0 errors,0 skipped。
|
||||||
|
- Fleet reactor verify:2456 tests,0 failures,0 errors,1 skipped;模块 Spotless、Jar、JaCoCo 成功。
|
||||||
|
- 真实测试网关 `GET /admin/fleet/matrix/grid?year=2026&month=8&season=active`:HTTP/code 200;返回 19 辆车、38 个派车段,新增车辆/司机字段完整,Long ID 均为字符串,`residentMatch` 三态合法,常驻司机手机号全部脱敏,无业务写入。
|
||||||
|
- 网关证据:`D:/work2/HL-v3/.tmp/5299-gateway.json`,SHA-256 `d94ee78fbe52cc193a28e5bf182e1353d2eca9d87824b1c2eb526c62a4fe645d`。
|
||||||
|
- OpenAPI/oasdiff:项目尚未配置可复现 Swagger2→OAS3 与 oasdiff,状态为 `not_configured`;使用源码字段对比、Controller 序列化测试及真实网关响应作为人工回退证据,未冒充工具通过。
|
||||||
|
|
||||||
|
当前状态:后端已部署、网关已验证;前端仅有可达的部分实现 `bfcfafc6`,运行态复验及车辆行司机汇总均未通过,因此 source 状态保持 `claimed`;修复并通过上述页面验收前不得流转为 `implemented`、`released` 或 `verified`。
|
||||||
|
|
||||||
|
关联:#5299、#5292、#5283。
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "4933"
|
||||||
|
title: "矩阵空闲格候选排除车辆日期冲突"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "not_required"
|
||||||
|
frontend_status: "pending"
|
||||||
|
frontend_owner: ""
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "后端月度未派清单契约未变;本次交接前端按所选车辆空闲闭区间过滤候选。"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 车务矩阵:空闲格候选必须排除车辆日期冲突
|
||||||
|
|
||||||
|
> **服务**:`hl-fleet-service`(既有接口,无后端代码变更)
|
||||||
|
>
|
||||||
|
> **Issue**:#4933 的矩阵空闲格直接派单前端实现回归
|
||||||
|
>
|
||||||
|
> **日期**:2026-07-28
|
||||||
|
>
|
||||||
|
> **影响范围**:管理后台车务矩阵点击车辆空闲格后的“选择当前矩阵可派订单”抽屉
|
||||||
|
|
||||||
|
## 关键结论
|
||||||
|
|
||||||
|
`GET /admin/fleet/matrix/unassigned-orders` 的既有语义是返回当前年月及车型筛选下的**全局未派订单池**,不是针对某辆车某段空闲日期计算后的候选集合。前端点击空闲格时已经持有 `vehicleId`、`clickedDate`、`startDate`、`endDate` 和该车辆矩阵占用,但当前抽屉只按“未绑定车辆”过滤,因此会展示完整用车区间不在空闲区间内、甚至与该车已有派车日期重叠的订单。
|
||||||
|
|
||||||
|
本次不要求后端新增字段或改变全局未派池语义。前端必须在打开空闲格抽屉时,根据点击上下文和当前矩阵车辆占用过滤候选;派单提交时仍由后端在资源锁内执行最终冲突重校验。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 后端结构变化 | 前端正确消费方式 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/admin/fleet/matrix/unassigned-orders` | 无;仍传 `year`、`month`、可选 `typeKeys[]`,仍返回当月全局有效未派行 | 仅作为候选数据源;进入某辆车空闲格抽屉前,结合点击上下文和该车矩阵占用做闭区间资格过滤 |
|
||||||
|
| GET | `/admin/fleet/matrix/grid` | 无 | 继续作为车辆、派车段和空闲区间的数据源,不新增前端猜测的返程日、缓冲日或状态 |
|
||||||
|
|
||||||
|
### 前端过滤不变量
|
||||||
|
|
||||||
|
1. 候选订单的全部有效 `serviceDateSegments[]` 必须完整落入所选空闲闭区间 `[startDate, endDate]`;只有后端未提供该数组字段时,才兼容使用订单 `startDate/endDate`。
|
||||||
|
2. 候选完整有效日期与所选车辆当前矩阵占用不得有任一日期重叠;首日、末日及边界同日均按占用处理。
|
||||||
|
3. 不允许为了让候选“可派”而把订单完整区间静默裁成空闲区间交集。候选不满足完整容纳条件时,应直接从抽屉排除。
|
||||||
|
4. 前端过滤不得替代创建或改派接口的后端锁内冲突校验;矩阵快照过期时以后端写侧拒绝为准。
|
||||||
|
5. 取消派车继续按矩阵现有有效性口径排除;不得自行增加返程后缓冲日或改变派车状态语义。
|
||||||
|
|
||||||
|
### 验收矩阵
|
||||||
|
|
||||||
|
以下日期均按闭区间判断:
|
||||||
|
|
||||||
|
| 场景 | 已有占用 | 候选完整区间 | 所选空闲区间 | 预期 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 候选起始日重叠 | 08-03~08-05 | 08-05~08-08 | 08-06~08-31 | 排除 |
|
||||||
|
| 候选结束日重叠 | 08-08~08-10 | 08-05~08-08 | 08-01~08-07 | 排除 |
|
||||||
|
| 候选包含已有占用 | 08-05~08-06 | 08-04~08-07 | 08-01~08-31 | 排除 |
|
||||||
|
| 候选被已有占用包含 | 08-03~08-09 | 08-04~08-07 | 08-01~08-31 | 排除 |
|
||||||
|
| 边界相邻但不重叠 | 08-03~08-05 | 08-06~08-07 | 08-06~08-31 | 保留 |
|
||||||
|
| 完整区间不在空闲区间 | 无额外占用 | 08-04~08-07 | 08-06~08-31 | 排除,不得裁成 08-06~08-07 |
|
||||||
|
| 无冲突且完整容纳 | 08-03~08-05 | 08-29~08-31 | 08-06~08-31 | 保留 |
|
||||||
|
|
||||||
|
## 运行复现
|
||||||
|
|
||||||
|
测试页面 `fleet/matrix` 的 2026-08 月矩阵中:
|
||||||
|
|
||||||
|
- 点击车辆 `蒙C05E05` 的 `2026-08-21` 空闲格;页面明确给出的空闲区间是 `2026-08-06~2026-08-31`。
|
||||||
|
- 抽屉仍显示订单 `26-4700`,完整区间为 `2026-08-04~2026-08-07`。
|
||||||
|
- 同一车辆既有订单 `26-4338` 的逐日用车详情显示 `2026-08-03`、`2026-08-04`、`2026-08-05` 均为已规划用车。
|
||||||
|
- 因而 `26-4700` 既未被空闲区间完整容纳,又在 08-04、08-05 与该车已有占用重叠,必须排除。
|
||||||
|
- 同抽屉订单 `26-5568` 的完整区间为 `2026-08-29~2026-08-31`,应继续保留。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 运行页面已复现当前失败:空闲格抽屉显示 `共 2 单`,其中包含不合格的 `26-4700`。
|
||||||
|
- 运行订单详情已核实车辆 `蒙C05E05` 在 08-03~08-05 的逐日用车事实,不是根据甘特位置猜测日期。
|
||||||
|
- `hl-ui origin/v2.1@e744c9096a6807439b3e4514a7687cf2f058b3fc`:
|
||||||
|
- `useFleetMatrixData.js` 调用月度未派接口时仅传 `year/month/typeKeys`;
|
||||||
|
- `index.vue` 的 `idleAssignableOrders` 仅过滤 `!order.vehicle`;
|
||||||
|
- `matrixAssignmentContext.js` 已构造车辆和空闲区间上下文,但当前只在选中订单后用于派单弹窗。
|
||||||
|
- `wx/HL origin/dev-v3@883fc61a006f6d7e40ca1c8723ffc6d28e37bf75`:`MatrixUnassignedReqVO` 只有 `year/month/typeKeys`,`MatrixService#queryUnassignedOrders` 按月返回全局未派行,当前行为符合既有接口契约。
|
||||||
|
- 前端修复、上述七类单测、发布及页面复核尚未执行,因此 `frontend_status` 保持 `pending`。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不修改后端请求/响应字段、错误码或状态机。
|
||||||
|
- 不修改派单创建、改派的车辆锁、司机锁和闭区间冲突校验。
|
||||||
|
- 不修改全局“打开未派订单窗口”的月度未派池展示;本规则仅用于从具体车辆空闲格进入的候选抽屉。
|
||||||
|
- 不涉及生产或数据库写入。
|
||||||
@@ -0,0 +1,541 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5295"
|
||||||
|
title: "Step3 聚合车辆费用"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-pi"
|
||||||
|
frontend_ref: "0a35da1e3b88f9c375d3e79516780e5a66f2b8ad"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: "2026-07-28T14:38:26+08:00"
|
||||||
|
status_note: "hl-order-service-v3 已部署并通过网关验证;hl-admin 已改用 Step3 GET/PUT 聚合 vehicleFees,旧 GET 仅缺字段时回退,独立确认动作已移除;全量 checkpoint 通过"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 【修改接口·管理后台】Step3 聚合车辆费用 (#5295)
|
||||||
|
|
||||||
|
> **PR**: #5297 | **更新时间**: 2026-07-28 11:26
|
||||||
|
|
||||||
|
## 1. 接口背景
|
||||||
|
|
||||||
|
核单 Step3 页面原来需要分别读取人员费用和车辆总车费,并且车辆费用还有额外确认动作。本次把车辆费用聚合到 Step3 查询和保存响应里:进入 Step3 时同屏拿到人员费用与车辆费用;保存人员费用时,同一次保存会校验车辆费用是否满足核单条件,满足时随响应返回已冻结的车辆费用块。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||||
|
|---|------|------|------|----------|------|
|
||||||
|
| 1 | Step 3 查询人员费用核单明细 | GET | `/v3/admin/order/:orderId/settlement/step3` | 修改接口 | 响应新增 `vehicleFees`,包含车辆费用顶层状态、总金额和逐日明细 |
|
||||||
|
| 2 | Step 3 录人员费用核单明细 | PUT | `/v3/admin/order/:orderId/settlement/step3` | 修改接口 | 保存人员费用时校验车辆费用;满足条件时返回冻结后的 `vehicleFees` |
|
||||||
|
| 3 | 查询核单车辆总车费 | GET | `/v3/admin/order/:orderId/settlement/vehicle-fees` | 保留兼容 | 旧接口仍可用;新 Step3 页面应优先读取 Step3 响应内的 `vehicleFees` |
|
||||||
|
| 4 | 确认并冻结核单车辆总车费 | POST | `/v3/admin/order/:orderId/settlement/vehicle-fees/confirm` | 保留兼容 | 旧接口仍可用;新 Step3 页面应移除单独确认车辆费用的动作 |
|
||||||
|
|
||||||
|
## 3. 接口详情
|
||||||
|
|
||||||
|
### 3.1 GET Step 3 查询人员费用核单明细
|
||||||
|
|
||||||
|
- **方法 + 路径**: `GET /v3/admin/order/:orderId/settlement/step3`
|
||||||
|
- **接口名**: Step 3 查询人员费用核单明细
|
||||||
|
- **使用场景**: 进入核单 Step3 页面时调用,展示人员费用表和车辆费用块。
|
||||||
|
- **认证**: 需要管理后台登录态;无权限或未登录按统一鉴权错误返回。
|
||||||
|
- **幂等性**: 幂等,只读查询。
|
||||||
|
- **限流**: 无接口级特殊限流。
|
||||||
|
- **响应类型**: `Result<SettlementStaffFeesSaveRespVO>`
|
||||||
|
|
||||||
|
### 3.2 PUT Step 3 录人员费用核单明细
|
||||||
|
|
||||||
|
- **方法 + 路径**: `PUT /v3/admin/order/:orderId/settlement/step3`
|
||||||
|
- **接口名**: Step 3 录人员费用核单明细
|
||||||
|
- **使用场景**: 用户保存 Step3 人员费用时调用。保存成功后,响应内同时回传人员费用结果和车辆费用块。
|
||||||
|
- **认证**: 需要管理后台登录态和核单资金写权限。
|
||||||
|
- **幂等性**: 同一 `items` 内容重复提交,人员费用结果保持一致;车辆费用已冻结后再次保存仍返回冻结块。
|
||||||
|
- **限流**: 无接口级特殊限流。
|
||||||
|
- **响应类型**: `Result<SettlementStaffFeesSaveRespVO>`
|
||||||
|
|
||||||
|
## 4. 接口入参
|
||||||
|
|
||||||
|
### 4.1 路径参数 / Query 参数
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 适用接口 | 说明 |
|
||||||
|
|------|------|------|----------|------|
|
||||||
|
| `orderId` | Long/String | 是 | GET、PUT | 订单 ID,必须大于 0;JSON 示例中按字符串展示,避免大整数精度问题 |
|
||||||
|
|
||||||
|
GET 无 Query 参数,无请求体。
|
||||||
|
|
||||||
|
### 4.2 PUT 请求体字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|
||||||
|
|------|------|------|------|----------|
|
||||||
|
| `items` | Array<StaffFeeItem> | 是 | 人员费用行数组,全量替换语义 | 数组字段必须存在;数组元素按下表校验 |
|
||||||
|
| `items[].id` | Long/String | 否 | 已存在行 ID;为空表示新增 | 已有行更新时传 |
|
||||||
|
| `items[].staffRole` | String | 是 | 人员角色 | 仅允许 `LEADER`、`DRIVER`、`GUIDE`、`PHOTOGRAPHER`、`OTHER` |
|
||||||
|
| `items[].staffId` | Long/String | 否 | 关联人员分配 ID | 多人聚合行可为空 |
|
||||||
|
| `items[].detail` | Object | 是 | 按 `staffRole` 区分的明细 JSON | 不能省略;各角色结构见下表 |
|
||||||
|
| `items[].reimburse` | Decimal/String | 否 | 小额报销金额 | 必须大于等于 0;为空按 0 处理 |
|
||||||
|
| `items[].paymentMethod` | String | 否 | 统一付款类型 | 仅允许 `CASH_PAID`、`COMPANY_PAID`、`SIGNED` |
|
||||||
|
| `items[].voucherUrls` | Array<String> | 否 | 人员费用凭证 URL 数组 | 最多 9 个;每个元素必须是 http/https URL,单个最多 1024 字符 |
|
||||||
|
| `items[].settlementConfirmStatus` | String | 否 | 人员费用核单确认状态 | 仅允许 `UNCONFIRMED`、`CONFIRMED` |
|
||||||
|
| `items[].settleStatus` | String | 否 | 辅助人员结算状态 | 仅允许 `PENDING`、`COMPLETED`;主报账人行可为空 |
|
||||||
|
| `items[].settledDate` | String(date) | 否 | 辅助人员结算日期 | 格式 `YYYY-MM-DD` |
|
||||||
|
| `items[].transferRef` | String | 条件必填 | 辅助人员结算转账流水号 | `settleStatus=COMPLETED` 时必填,最多 128 字符 |
|
||||||
|
| `items[].remark` | String | 否 | 备注 | 最多 500 字符 |
|
||||||
|
|
||||||
|
### 4.3 `detail` 字段结构
|
||||||
|
|
||||||
|
| `staffRole` | `detail` 结构 | 必填说明 |
|
||||||
|
|-------------|---------------|----------|
|
||||||
|
| `DRIVER` | `days[]`(`service_date`、`vehicle_brief`、`daily_fee`、`is_used`、`note`),以及 `extra_cost`、`extra_breakdown[]` | `days[]` 必须存在;每个元素必须包含 `service_date`、`daily_fee` |
|
||||||
|
| `GUIDE` | `persons[]`(`name`、`days`、`per_day`、`note`) | `persons[]` 必须存在;每个元素必须包含 `name`、`days`、`per_day` |
|
||||||
|
| `PHOTOGRAPHER` | `persons[]`(`name`、`days`、`per_day`、`note`) | `persons[]` 必须存在;每个元素必须包含 `name`、`days`、`per_day` |
|
||||||
|
| `LEADER` | `days`、`per_day` | `days`、`per_day` 必须存在 |
|
||||||
|
| `OTHER` | `items[]`(`name`、`amount`、`note`) | `items[]` 用于其他人员费用明细 |
|
||||||
|
|
||||||
|
## 5. 出参字段
|
||||||
|
|
||||||
|
### 5.1 统一响应包装
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `code` | Integer | 业务状态码;`200` 表示成功 |
|
||||||
|
| `data` | Object/null | 成功时为 `SettlementStaffFeesSaveRespVO`;失败时通常为 `null` |
|
||||||
|
| `message` | String | 响应消息 |
|
||||||
|
|
||||||
|
### 5.2 `data` 字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `addedIds` | Array<String> | PUT 成功时新增的人员费用行 ID;GET 可为空数组 |
|
||||||
|
| `updatedIds` | Array<String> | PUT 成功时更新的人员费用行 ID;GET 可为空数组 |
|
||||||
|
| `deletedIds` | Array<String> | PUT 成功时软删的人员费用行 ID;GET 可为空数组 |
|
||||||
|
| `totalActualCost` | String(decimal) | 人员费用实际成本合计 |
|
||||||
|
| `items` | Array<StaffFeeRespItem> | 人员费用明细行 |
|
||||||
|
| `vehicleFees` | Object | 本次新增:车辆费用块;无有效车辆需求时仍返回对象,`items=[]`、金额为 `0.00`、`frozen=false` |
|
||||||
|
|
||||||
|
### 5.3 `data.items[]` 人员费用明细
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 人员费用行 ID |
|
||||||
|
| `staffRole` | String | 人员角色:`LEADER`、`DRIVER`、`GUIDE`、`PHOTOGRAPHER`、`OTHER` |
|
||||||
|
| `staffId` | String/null | 关联人员分配 ID |
|
||||||
|
| `staffName` | String/null | 人员姓名快照 |
|
||||||
|
| `totalPlannedCost` | String(decimal) | 计划成本 |
|
||||||
|
| `totalActualCost` | String(decimal) | 实际成本 |
|
||||||
|
| `detail` | Object | 按 `staffRole` 区分的明细 JSON |
|
||||||
|
| `reimburse` | String(decimal) | 小额报销金额 |
|
||||||
|
| `paymentMethod` | String/null | 人员费用付款类型:`CASH_PAID`、`COMPANY_PAID`、`SIGNED` |
|
||||||
|
| `voucherUrls` | Array<String> | 凭证 URL 数组 |
|
||||||
|
| `settlementConfirmStatus` | String | 核单确认状态:`UNCONFIRMED`、`CONFIRMED` |
|
||||||
|
| `settleStatus` | String/null | 辅助人员结算状态:`PENDING`、`COMPLETED`;主报账人行可为空 |
|
||||||
|
| `settledDate` | String(date)/null | 辅助人员结算日期 |
|
||||||
|
| `transferRef` | String/null | 辅助人员结算转账流水号 |
|
||||||
|
| `isPrimaryReporter` | Boolean | 是否主报账人 |
|
||||||
|
| `remark` | String/null | 备注 |
|
||||||
|
|
||||||
|
### 5.4 `data.vehicleFees` 车辆费用顶层
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `orderId` | String | 订单 ID |
|
||||||
|
| `frozen` | Boolean | 车辆费用是否已冻结;PUT 成功冻结后为 `true` |
|
||||||
|
| `requirementId` | String/null | 当前车辆需求 ID;无有效车辆需求时为 `null` |
|
||||||
|
| `settlementReady` | Boolean | 车辆费用是否满足核单条件;为 `false` 时 PUT 可能返回 `584101` |
|
||||||
|
| `totalAmount` | String(decimal) | 车辆费用总金额 |
|
||||||
|
| `totalVehicleFee` | String(decimal) | 车辆费用总金额,兼容旧字段名;前端展示可读取该字段 |
|
||||||
|
| `items` | Array<VehicleFeeItem> | 车辆费用逐日明细;无有效车辆需求时为空数组 |
|
||||||
|
|
||||||
|
### 5.5 `data.vehicleFees.items[]` 车辆费用明细
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `sourceDetailId` | String/null | 逐日费用来源明细 ID;冻结后的旧数据可能为空 |
|
||||||
|
| `serviceDate` | String(date)/null | 逐日服务日期 |
|
||||||
|
| `assignmentGroupId` | String/null | 派车组 ID;逐日来源可为空 |
|
||||||
|
| `assignmentSlotId` | String/null | 派车明细 ID;逐日来源可为空 |
|
||||||
|
| `vehicleId` | String/null | 车辆 ID |
|
||||||
|
| `vehiclePlate` | String/null | 车牌号 |
|
||||||
|
| `vehicleModelId` | String/null | 车型 ID |
|
||||||
|
| `vehicleModelName` | String/null | 车型名称 |
|
||||||
|
| `vehicleModel` | String/null | 车型展示文本,兼容旧字段 |
|
||||||
|
| `driverId` | String/null | 司机 ID |
|
||||||
|
| `driverName` | String/null | 司机姓名 |
|
||||||
|
| `startDate` | String(date)/null | 费用服务开始日期;逐日费用通常等于 `serviceDate` |
|
||||||
|
| `endDate` | String(date)/null | 费用服务结束日期;逐日费用通常等于 `serviceDate` |
|
||||||
|
| `chargeableServiceDates` | Array<String(date)> | 计费服务日期列表 |
|
||||||
|
| `freeServiceDates` | Array<String(date)> | 免费服务日期列表 |
|
||||||
|
| `vehicleFeeWaiverReason` | String/null | 免车费原因 |
|
||||||
|
| `dailyPrice` | String(decimal) | 当日车费 |
|
||||||
|
| `paymentTypeCode` | String | 车辆费用付款类型编码:`CASH_PAID`、`SIGNED`、`COMPANY_PAID` |
|
||||||
|
| `paymentTypeName` | String/null | 车辆费用付款类型名称 |
|
||||||
|
| `amount` | String(decimal) | 本条核单金额 |
|
||||||
|
| `autoVehicleFeeTotal` | String(decimal) | 自动计算车辆费用金额 |
|
||||||
|
| `autoVehicleFeeComplete` | Boolean | 自动计算金额是否完整 |
|
||||||
|
| `vehicleFeeTotal` | String(decimal) | 本条车辆费用金额,兼容旧字段名 |
|
||||||
|
| `vehicleFeeSource` | String | 费用来源:`AUTO` 或 `MANUAL` |
|
||||||
|
| `vehicleFeeAdjustmentReason` | String/null | 手工调整原因 |
|
||||||
|
| `vehicleFeeAdjustedBy` | String/null | 手工调整人 ID |
|
||||||
|
| `vehicleFeeAdjustedAt` | String(datetime)/null | 手工调整时间 |
|
||||||
|
| `settlementReady` | Boolean | 本条车辆费用是否满足核单条件 |
|
||||||
|
|
||||||
|
## 6. 枚举 / 数据字典
|
||||||
|
|
||||||
|
### 6.1 `paymentTypeCode`(车辆费用付款类型)
|
||||||
|
|
||||||
|
**所属字段**: `data.vehicleFees.items[].paymentTypeCode` | **类型**: `String` | **必填**: 是
|
||||||
|
|
||||||
|
| 值 | 中文 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| `CASH_PAID` | 现付 | 车辆费用已由现场现金或等价方式支付 |
|
||||||
|
| `SIGNED` | 签单 | 车辆费用采用签单方式结算 |
|
||||||
|
| `COMPANY_PAID` | 公司支付 | 车辆费用由公司统一支付 |
|
||||||
|
|
||||||
|
### 6.2 `paymentMethod`(人员费用付款类型)
|
||||||
|
|
||||||
|
**所属字段**: `items[].paymentMethod`、`data.items[].paymentMethod` | **类型**: `String` | **必填**: 否
|
||||||
|
|
||||||
|
| 值 | 中文 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| `CASH_PAID` | 现付 | 人员费用已现场支付 |
|
||||||
|
| `COMPANY_PAID` | 公司支付 | 人员费用由公司支付 |
|
||||||
|
| `SIGNED` | 签单 | 人员费用采用签单方式 |
|
||||||
|
|
||||||
|
## 7. 错误码
|
||||||
|
|
||||||
|
| code | 含义 | 触发场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `584100` | 车辆总车费暂时不可用 | GET 或 PUT Step3 读取车辆费用失败,或车辆费用响应与当前订单不匹配 |
|
||||||
|
| `584101` | 存在未完结派车或未确认车辆总车费,暂不能核单 | PUT Step3 保存时,当前车辆费用 `settlementReady=false`,或任一车辆费用明细未满足冻结条件 |
|
||||||
|
| `584102` | 当前用车需求没有可核单的车辆总车费 | PUT Step3 保存时存在有效车辆需求,但车辆费用明细为空 |
|
||||||
|
|
||||||
|
## 8. 示例
|
||||||
|
|
||||||
|
### 8.1 典型成功:GET Step3 返回 9 行车辆费用
|
||||||
|
|
||||||
|
**请求**:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v3/admin/order/2079454953641836546/settlement/step3
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"addedIds": [],
|
||||||
|
"updatedIds": [],
|
||||||
|
"deletedIds": [],
|
||||||
|
"totalActualCost": "3600.00",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "9300000000001",
|
||||||
|
"staffRole": "DRIVER",
|
||||||
|
"staffId": "6800001001",
|
||||||
|
"staffName": "司机A",
|
||||||
|
"totalPlannedCost": "2100.00",
|
||||||
|
"totalActualCost": "2100.00",
|
||||||
|
"detail": {
|
||||||
|
"days": [
|
||||||
|
{
|
||||||
|
"service_date": "2026-07-29",
|
||||||
|
"vehicle_brief": "蒙A12345",
|
||||||
|
"daily_fee": "700.00",
|
||||||
|
"is_used": true,
|
||||||
|
"note": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extra_cost": "0.00",
|
||||||
|
"extra_breakdown": []
|
||||||
|
},
|
||||||
|
"reimburse": "0.00",
|
||||||
|
"paymentMethod": "COMPANY_PAID",
|
||||||
|
"voucherUrls": [],
|
||||||
|
"settlementConfirmStatus": "UNCONFIRMED",
|
||||||
|
"settleStatus": "PENDING",
|
||||||
|
"settledDate": null,
|
||||||
|
"transferRef": null,
|
||||||
|
"isPrimaryReporter": false,
|
||||||
|
"remark": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vehicleFees": {
|
||||||
|
"orderId": "2079454953641836546",
|
||||||
|
"frozen": false,
|
||||||
|
"requirementId": "2079000000000000001",
|
||||||
|
"settlementReady": true,
|
||||||
|
"totalAmount": "6780.00",
|
||||||
|
"totalVehicleFee": "6780.00",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"sourceDetailId": "2080000000000000001",
|
||||||
|
"serviceDate": "2026-07-29",
|
||||||
|
"assignmentGroupId": null,
|
||||||
|
"assignmentSlotId": null,
|
||||||
|
"vehicleId": "300000000000000001",
|
||||||
|
"vehiclePlate": "蒙A12345",
|
||||||
|
"vehicleModelId": "400000000000000001",
|
||||||
|
"vehicleModelName": "商务车",
|
||||||
|
"vehicleModel": "商务车",
|
||||||
|
"driverId": "500000000000000001",
|
||||||
|
"driverName": "宝音德力格尔",
|
||||||
|
"startDate": "2026-07-29",
|
||||||
|
"endDate": "2026-07-29",
|
||||||
|
"chargeableServiceDates": ["2026-07-29"],
|
||||||
|
"freeServiceDates": [],
|
||||||
|
"vehicleFeeWaiverReason": null,
|
||||||
|
"dailyPrice": "700.00",
|
||||||
|
"paymentTypeCode": "COMPANY_PAID",
|
||||||
|
"paymentTypeName": "公司支付",
|
||||||
|
"amount": "700.00",
|
||||||
|
"autoVehicleFeeTotal": "700.00",
|
||||||
|
"autoVehicleFeeComplete": true,
|
||||||
|
"vehicleFeeTotal": "700.00",
|
||||||
|
"vehicleFeeSource": "AUTO",
|
||||||
|
"vehicleFeeAdjustmentReason": null,
|
||||||
|
"vehicleFeeAdjustedBy": null,
|
||||||
|
"vehicleFeeAdjustedAt": null,
|
||||||
|
"settlementReady": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sourceDetailId": "2080000000000000002",
|
||||||
|
"serviceDate": "2026-07-29",
|
||||||
|
"assignmentGroupId": null,
|
||||||
|
"assignmentSlotId": null,
|
||||||
|
"vehicleId": "300000000000000002",
|
||||||
|
"vehiclePlate": "蒙A23456",
|
||||||
|
"vehicleModelId": "400000000000000002",
|
||||||
|
"vehicleModelName": "越野车",
|
||||||
|
"vehicleModel": "越野车",
|
||||||
|
"driverId": "500000000000000002",
|
||||||
|
"driverName": "阿拉坦",
|
||||||
|
"startDate": "2026-07-29",
|
||||||
|
"endDate": "2026-07-29",
|
||||||
|
"chargeableServiceDates": ["2026-07-29"],
|
||||||
|
"freeServiceDates": [],
|
||||||
|
"vehicleFeeWaiverReason": null,
|
||||||
|
"dailyPrice": "700.00",
|
||||||
|
"paymentTypeCode": "SIGNED",
|
||||||
|
"paymentTypeName": "签单",
|
||||||
|
"amount": "700.00",
|
||||||
|
"autoVehicleFeeTotal": "700.00",
|
||||||
|
"autoVehicleFeeComplete": true,
|
||||||
|
"vehicleFeeTotal": "700.00",
|
||||||
|
"vehicleFeeSource": "AUTO",
|
||||||
|
"vehicleFeeAdjustmentReason": null,
|
||||||
|
"vehicleFeeAdjustedBy": null,
|
||||||
|
"vehicleFeeAdjustedAt": null,
|
||||||
|
"settlementReady": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sourceDetailId": "2080000000000000003",
|
||||||
|
"serviceDate": "2026-07-29",
|
||||||
|
"assignmentGroupId": null,
|
||||||
|
"assignmentSlotId": null,
|
||||||
|
"vehicleId": "300000000000000003",
|
||||||
|
"vehiclePlate": "蒙A34567",
|
||||||
|
"vehicleModelId": "400000000000000003",
|
||||||
|
"vehicleModelName": "中巴",
|
||||||
|
"vehicleModel": "中巴",
|
||||||
|
"driverId": "500000000000000003",
|
||||||
|
"driverName": "巴雅尔",
|
||||||
|
"startDate": "2026-07-29",
|
||||||
|
"endDate": "2026-07-29",
|
||||||
|
"chargeableServiceDates": ["2026-07-29"],
|
||||||
|
"freeServiceDates": [],
|
||||||
|
"vehicleFeeWaiverReason": null,
|
||||||
|
"dailyPrice": "860.00",
|
||||||
|
"paymentTypeCode": "CASH_PAID",
|
||||||
|
"paymentTypeName": "现付",
|
||||||
|
"amount": "860.00",
|
||||||
|
"autoVehicleFeeTotal": "860.00",
|
||||||
|
"autoVehicleFeeComplete": true,
|
||||||
|
"vehicleFeeTotal": "860.00",
|
||||||
|
"vehicleFeeSource": "AUTO",
|
||||||
|
"vehicleFeeAdjustmentReason": null,
|
||||||
|
"vehicleFeeAdjustedBy": null,
|
||||||
|
"vehicleFeeAdjustedAt": null,
|
||||||
|
"settlementReady": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:上例只展开 2026-07-29 的 3 行;同一订单还可能继续返回 2026-07-30、2026-07-31 的逐日车辆费用。验收样例中 3 天 x 3 司机共 9 行,合计 `6780.00`。
|
||||||
|
|
||||||
|
### 8.2 边界情况:无有效车辆需求时返回空未冻结块
|
||||||
|
|
||||||
|
**请求**:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v3/admin/order/2079576729147338754/settlement/step3
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"addedIds": [],
|
||||||
|
"updatedIds": [],
|
||||||
|
"deletedIds": [],
|
||||||
|
"totalActualCost": "0.00",
|
||||||
|
"items": [],
|
||||||
|
"vehicleFees": {
|
||||||
|
"orderId": "2079576729147338754",
|
||||||
|
"frozen": false,
|
||||||
|
"requirementId": null,
|
||||||
|
"settlementReady": false,
|
||||||
|
"totalAmount": "0.00",
|
||||||
|
"totalVehicleFee": "0.00",
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 业务失败:PUT 时车辆费用未满足核单条件
|
||||||
|
|
||||||
|
**请求**:
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /v3/admin/order/2079454953641836546/settlement/step3
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "9300000000001",
|
||||||
|
"staffRole": "DRIVER",
|
||||||
|
"staffId": "6800001001",
|
||||||
|
"detail": {
|
||||||
|
"days": [
|
||||||
|
{
|
||||||
|
"service_date": "2026-07-29",
|
||||||
|
"vehicle_brief": "蒙A12345",
|
||||||
|
"daily_fee": "700.00",
|
||||||
|
"is_used": true,
|
||||||
|
"note": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extra_cost": "0.00",
|
||||||
|
"extra_breakdown": []
|
||||||
|
},
|
||||||
|
"reimburse": "0.00",
|
||||||
|
"paymentMethod": "COMPANY_PAID",
|
||||||
|
"voucherUrls": [],
|
||||||
|
"settlementConfirmStatus": "UNCONFIRMED",
|
||||||
|
"settleStatus": "PENDING",
|
||||||
|
"settledDate": null,
|
||||||
|
"transferRef": null,
|
||||||
|
"remark": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 584101,
|
||||||
|
"message": "存在未完结派车或未确认车辆总车费,暂不能核单",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 业务边界
|
||||||
|
|
||||||
|
- **适用场景**: 核单 Step3 页面查询和保存;页面需要同时展示人员费用与车辆费用时,直接使用 Step3 响应。
|
||||||
|
- **车辆费用可为空的场景**: 订单没有有效车辆需求时,GET Step3 返回 `vehicleFees.items=[]`、`frozen=false`、`settlementReady=false`、金额为 `0.00`。
|
||||||
|
- **PUT 保存门禁**: 存在有效车辆需求时,PUT Step3 会校验车辆费用是否满足核单条件;不满足时返回 `584101`,本次人员费用保存不视为成功。
|
||||||
|
- **空明细门禁**: 存在有效车辆需求但没有可核单车辆费用明细时,PUT Step3 返回 `584102`。
|
||||||
|
- **已冻结场景**: 车辆费用已冻结后,GET/PUT Step3 都返回冻结后的 `vehicleFees`。
|
||||||
|
|
||||||
|
## 10. 修改前后对比
|
||||||
|
|
||||||
|
### 10.1 字段级对比
|
||||||
|
|
||||||
|
| 字段 | 改前 | 改后 |
|
||||||
|
|------|------|------|
|
||||||
|
| `data.vehicleFees` | GET/PUT Step3 不返回 | GET/PUT Step3 均返回车辆费用块 |
|
||||||
|
| `data.vehicleFees.frozen` | 无 | 返回车辆费用是否冻结 |
|
||||||
|
| `data.vehicleFees.requirementId` | 无 | 返回当前车辆需求 ID;无有效车辆需求时为 `null` |
|
||||||
|
| `data.vehicleFees.settlementReady` | 无 | 返回车辆费用是否满足核单条件 |
|
||||||
|
| `data.vehicleFees.totalAmount` | 无 | 返回车辆费用总金额 |
|
||||||
|
| `data.vehicleFees.totalVehicleFee` | 无 | 返回车辆费用总金额兼容字段 |
|
||||||
|
| `data.vehicleFees.items[]` | 无 | 返回逐日车辆费用明细 |
|
||||||
|
| `data.vehicleFees.items[].sourceDetailId` | 无 | 返回逐日费用来源明细 ID |
|
||||||
|
| `data.vehicleFees.items[].serviceDate` | 无 | 返回逐日服务日期 |
|
||||||
|
| `data.vehicleFees.items[].dailyPrice` | 无 | 返回当日车费 |
|
||||||
|
| `data.vehicleFees.items[].paymentTypeCode` | 无 | 返回车辆费用付款类型编码 |
|
||||||
|
| `data.vehicleFees.items[].paymentTypeName` | 无 | 返回车辆费用付款类型名称 |
|
||||||
|
| `data.vehicleFees.items[].amount` | 无 | 返回本条核单金额 |
|
||||||
|
| `data.vehicleFees.items[].settlementReady` | 无 | 返回本条车辆费用是否满足核单条件 |
|
||||||
|
|
||||||
|
### 10.2 行为级对比
|
||||||
|
|
||||||
|
| 行为 | 改前 | 改后 |
|
||||||
|
|------|------|------|
|
||||||
|
| 进入 Step3 页面 | 需要单独读取人员费用和车辆费用 | 调用 GET Step3 即可拿到人员费用与车辆费用 |
|
||||||
|
| 保存 Step3 | 只保存人员费用 | 保存人员费用时同步校验车辆费用;满足条件时返回冻结后的车辆费用 |
|
||||||
|
| 车辆费用确认 | 前端可能单独调用 `POST /settlement/vehicle-fees/confirm` | 新 Step3 页面应停止单独调用,移除额外确认动作 |
|
||||||
|
| 旧 GET 车辆费用 | 作为车辆费用主要读取入口 | 保留兼容;新 Step3 页面优先读 `data.vehicleFees` |
|
||||||
|
| 无有效车辆需求 | 需要前端自行处理独立接口空态 | GET Step3 内直接返回空未冻结 `vehicleFees` 块 |
|
||||||
|
|
||||||
|
## 11. 影响评估 / 回滚
|
||||||
|
|
||||||
|
### 11.1 影响评估
|
||||||
|
|
||||||
|
- **是否破坏向后兼容**: 否。Step3 响应新增 `vehicleFees`,旧字段保留;旧车辆费用 GET 和确认 POST 保留兼容。
|
||||||
|
- **前端是否必须同步上线**: 否,但建议管理后台 Step3 页面尽快切到 `data.vehicleFees`,并移除额外车辆费用确认动作。
|
||||||
|
- **旧页面兼容**: 继续调用旧 `GET /v3/admin/order/:orderId/settlement/vehicle-fees` 和 `POST /v3/admin/order/:orderId/settlement/vehicle-fees/confirm` 不会因本次变更直接失效。
|
||||||
|
|
||||||
|
### 11.2 回滚方案
|
||||||
|
|
||||||
|
- **回滚后前端表现**: 如果回滚到旧契约,GET/PUT Step3 不再包含 `data.vehicleFees`;前端需要保留对 `vehicleFees` 缺失的空值兼容。
|
||||||
|
- **前端兼容建议**: 读取 `data.vehicleFees` 前先判空;为空时可降级到旧车辆费用 GET。
|
||||||
|
|
||||||
|
## 12. 注意事项
|
||||||
|
|
||||||
|
- 新 Step3 页面不要再单独调用 `POST /v3/admin/order/:orderId/settlement/vehicle-fees/confirm` 作为额外确认按钮或保存后动作。
|
||||||
|
- 新 Step3 页面读取车辆费用时优先使用 `GET /v3/admin/order/:orderId/settlement/step3` 返回的 `data.vehicleFees`。
|
||||||
|
- `paymentTypeCode` 是车辆费用付款类型字段,枚举值为 `CASH_PAID`、`SIGNED`、`COMPANY_PAID`;不要用人员费用的 `paymentMethod` 去覆盖车辆费用字段。
|
||||||
|
- `totalAmount` 与 `totalVehicleFee` 都表示车辆费用总金额;为兼容旧页面,当前两者应按同一金额展示。
|
||||||
|
- `frozen=false` 不等于接口失败;无有效车辆需求或车辆费用尚未满足核单条件时都可能返回未冻结块。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 PR [wx/HL#5297](https://git.1814.love:8443/wx/HL/pulls/5297) 已合并至 `dev-v3`,合并提交 `5194183f6`。
|
||||||
|
- `dev-v3@ca23f64fc` 执行 `mvn -pl hl-order-service-v3 -am test` 通过。
|
||||||
|
- 测试环境滚动部署任务 `90374dab` 成功;`hl-order-service-v3` 8086/8186 双实例健康。
|
||||||
|
- 经测试网关只读调用 `GET /v3/admin/order/:orderId/settlement/step3`:HTTP 200、业务码 200,响应包含 `vehicleFees`,样本返回 9 条逐日费用明细。
|
||||||
|
- 网关证据:`D:/work2/HL-v3/.tmp/5295-gateway.json`,SHA-256 `dafa5257c4df241e9511c95dba1f689397742133e9f46aefa4701a6d00228ab6`。
|
||||||
|
- OpenAPI/oasdiff:项目未配置可复现的 Swagger 2 到 OAS3 导出与 oasdiff,状态为 `not_configured`;已用 Controller/VO 源码对比、Controller 测试和真实网关响应完成人工回退核对。
|
||||||
|
- `frontend_status` 保持 `pending`;前端真实领取后再迁移为 `claimed` 并填写 `frontend_owner`。
|
||||||
|
|
||||||
|
## 13. 关联 / 联系人
|
||||||
|
|
||||||
|
### 13.1 链接
|
||||||
|
|
||||||
|
- **Issue**: [#5295](https://git.1814.love:8443/wx/HL/issues/5295)
|
||||||
|
- **PR**: [#5297](https://git.1814.love:8443/wx/HL/pulls/5297)
|
||||||
|
- **Merge commit**: [5194183f6](https://git.1814.love:8443/wx/HL/commit/5194183f6)
|
||||||
|
- **Feature commit**: [07d36dd39](https://git.1814.love:8443/wx/HL/commit/07d36dd3989789379388895b86a0954434013108)
|
||||||
|
|
||||||
|
### 13.2 联系人
|
||||||
|
|
||||||
|
- **接口负责人**: @yaosutu
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5301"
|
||||||
|
title: "配车矩阵统一手动加急状态与统计"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-pi"
|
||||||
|
frontend_ref: "e1d0a44a734fefc4f12ff1b71d218df0b265dc62"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: "2026-07-28T14:38:26+08:00"
|
||||||
|
status_note: "hl-admin 已消费人工加急徽章、精确 statuses 筛选与 grid/month 五键统计;业务提交 e1d0a44a 已在 origin/v2.1 可达,checkpoint 通过"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fleet:配车矩阵统一手动加急状态与统计
|
||||||
|
|
||||||
|
> **服务**:`hl-fleet-service`
|
||||||
|
> **Issue**:#5301
|
||||||
|
> **影响页面**:管理后台车务管理 → 配车矩阵
|
||||||
|
> **兼容性**:只新增可选请求参数与响应字段;路径、方法、既有字段、持久化状态不变
|
||||||
|
|
||||||
|
## 问题与目标
|
||||||
|
|
||||||
|
派车看板已经使用当前有效用车需求的 `manualUrgent` 派生人工加急,配车矩阵此前只计算临近出团与 HOLD 超时自动紧急态。同一需求因此可能在看板显示加急、矩阵仍显示普通待派/待确认,矩阵筛选和月份统计也无法精确区分人工加急。
|
||||||
|
|
||||||
|
本次后端统一矩阵全部读视图的有效状态来源,并提供稳定的人工加急字段、中文标签、精确状态筛选和五键计数。人工加急不修改落库派单状态。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
### `GET /admin/fleet/matrix/grid`
|
||||||
|
|
||||||
|
#### 1. 新增请求参数 `statuses`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `statuses` | `String[]` | 否 | 按有效状态精确筛选,多值取并集;非空且至少含一个合法值时优先于旧 `status` |
|
||||||
|
|
||||||
|
合法值:
|
||||||
|
|
||||||
|
- `unassigned`
|
||||||
|
- `unassigned_urgent`
|
||||||
|
- `holding`
|
||||||
|
- `holding_urgent`
|
||||||
|
- `assigned`
|
||||||
|
- `completed`
|
||||||
|
|
||||||
|
兼容规则:
|
||||||
|
|
||||||
|
- 旧 `status=unassigned` 继续同时包含 `unassigned` 与 `unassigned_urgent`;
|
||||||
|
- 旧 `status=assigned` 继续包含 `holding`、`holding_urgent` 与 `assigned`;
|
||||||
|
- `statuses` 全空或全非法时回退旧 `status`;
|
||||||
|
- 未传二者时保持全部展示。
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /admin/fleet/matrix/grid?year=2026&month=8&season=active&statuses=unassigned_urgent&statuses=holding_urgent
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. `data.vehicles[].assignments[]` 新增字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 空值规则 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `manualUrgent` | `Boolean` | 固定 `true/false` | 当前有效用车需求是否人工加急;order-v3 上下文整体不可用时为 `false`,不得前端猜测 |
|
||||||
|
| `assignmentStatusLabel` | `String` | 正常非空 | 后端统一有效状态中文标签,例如“待派车”“待确认”“已派车” |
|
||||||
|
| `urgentBadge` | `String` | 非紧急为空 | 人工加急固定“手动加急”;自动紧急继续返回既有 T-N/HOLD 超时文案 |
|
||||||
|
|
||||||
|
有效状态规则:
|
||||||
|
|
||||||
|
- 基础态 `unassigned` 且 `manualUrgent=true` → `assignmentStatus=unassigned_urgent`;
|
||||||
|
- 基础态 `holding` 且 `manualUrgent=true` → `assignmentStatus=holding_urgent`;
|
||||||
|
- 人工加急优先于临近出团/HOLD 超时自动派生;
|
||||||
|
- `assigned`、`completed` 不因人工加急改变;`canceled` 继续排除;
|
||||||
|
- 只消费当前有效需求上下文,旧需求派单不会继承当前需求的人工加急。
|
||||||
|
|
||||||
|
#### 3. `data.statusCounts.effectiveStatusCounts` 新增固定五键
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"unassigned": 2,
|
||||||
|
"unassigned_urgent": 1,
|
||||||
|
"holding": 3,
|
||||||
|
"holding_urgent": 1,
|
||||||
|
"assigned": 4
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 五个键始终存在,缺类为 `0`;
|
||||||
|
- 每个活跃派车组只进入一个精确状态;
|
||||||
|
- 五键之和恒等于既有 `statusCounts.totalAssignments`;
|
||||||
|
- 既有 `unassignedAssignments`、`assignedAssignments` 和订单级统计保持兼容聚合口径,不删除、不改名。
|
||||||
|
|
||||||
|
### `GET /admin/fleet/matrix/month-counts`
|
||||||
|
|
||||||
|
每月 `statusCounts` 同样新增 `effectiveStatusCounts`:
|
||||||
|
|
||||||
|
- 固定返回 1–12 月,零值月份不省略;
|
||||||
|
- 同样固定五键;
|
||||||
|
- 与相同 `year/month/season/fleetTeamIds/typeKeys` 的 grid 顶部统计守恒;
|
||||||
|
- 年度读取仍为一次批量上下文,不循环产生 N+1。
|
||||||
|
|
||||||
|
## 前端必须调整
|
||||||
|
|
||||||
|
1. 派车条直接展示后端 `assignmentStatusLabel`;不要在前端维护第二套中文状态映射。
|
||||||
|
2. `manualUrgent=true` 且状态为 `unassigned_urgent/holding_urgent` 时,展示 `urgentBadge=手动加急`,颜色与派车看板人工加急保持一致。
|
||||||
|
3. 需要精确状态 Tab/筛选时改传 `statuses[]`;不要用旧 `status=unassigned` 期待只命中普通待派。
|
||||||
|
4. 精确分类数字使用 `statusCounts.effectiveStatusCounts`;既有全部/未派/已派聚合卡片可继续使用旧统计字段。
|
||||||
|
5. 月份切换统计直接使用 `month-counts[].statusCounts.effectiveStatusCounts`,不要前端遍历当前月卡片重算。
|
||||||
|
6. 继续保持 #5299 的实际车辆/司机/常驻标记和 Long ID 字符串处理;禁止转为 JS `Number`。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不修改人工加急写接口或 order-v3 需求状态。
|
||||||
|
- 不修改派单落库状态、车辆/司机占用、费用、保险、对账或常驻关系。
|
||||||
|
- 不新增 Feign、数据库查询或逐订单 N+1。
|
||||||
|
- 不修改 `hl-ui` 仓库;前端消费状态独立流转。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 PR [wx/HL#5304](https://git.1814.love:8443/wx/HL/pulls/5304) 已 squash 合并至 `dev-v3`,合并提交 `ed56bec1f`。
|
||||||
|
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康。
|
||||||
|
- 定向 `MatrixServiceTest,MatrixControllerTest,BoardCandidateSourceTest`:51 tests,0 failures,0 errors,0 skipped;覆盖人工加急、自动回退、精确筛选、五键守恒、当前需求门禁与全局降级。
|
||||||
|
- Fleet reactor verify:2474 tests,0 failures,0 errors,1 skipped;Spotless 628 Java files clean。
|
||||||
|
- 真实测试网关:grid 新字段完整且 `manualUrgent` 全为非空 Boolean;五键固定且和等于 `totalAssignments`;`statuses=assigned` 在旧 `status=unassigned` 同时传入时仍只返回 assigned,证明精确筛选优先;month-counts 固定 12 月且同月五键与 grid 相等;Long ID 为字符串、手机号脱敏、全程无业务写入。
|
||||||
|
- 当前测试矩阵数据没有 `manualUrgent=true` 的活跃派车组,因此真实网关未伪报人工加急正例;正例由 Service/Controller 定向测试覆盖。
|
||||||
|
- 网关证据:`D:/work2/HL-v3/.tmp/5301-gateway.json`,SHA-256 `6ae185282e2194a63168298168888f4c03086916b7ad91268b24ac133b2b363d`。
|
||||||
|
- OpenAPI/oasdiff:项目未配置可复现 Swagger2→OAS3 与 oasdiff,状态为 `not_configured`;使用源码字段对比、Controller/Service 测试及真实网关响应作为人工回退证据。Spring Cloud Contract 为 `not_required`。
|
||||||
|
|
||||||
|
当前状态:后端已部署、网关已验证,前端消费保持 `pending`。
|
||||||
|
|
||||||
|
关联:#5301、#5299。
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5302"
|
||||||
|
title: "派车档期按完整组判定同城衔接"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "not_required"
|
||||||
|
frontend_owner: ""
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "仅修正候选与预校验的既有冲突语义,不新增字段或页面动作;前端继续按后端 available、availabilityReasonCode、conflicts 与 conflict 渲染即可"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fleet:派车档期按完整组判定同城衔接
|
||||||
|
|
||||||
|
> **服务**:`hl-fleet-service`
|
||||||
|
> **Issue**:#5302
|
||||||
|
> **影响页面**:管理后台车务管理 → 派单候选弹窗、保存前预校验
|
||||||
|
> **兼容性**:路径、方法、请求/响应字段和错误码形状不变,仅修正既有业务语义
|
||||||
|
|
||||||
|
## 问题与目标
|
||||||
|
|
||||||
|
派车数据已按服务日期拆成逐日切片。同一辆车或司机已有 D1–D3 多日派车时,旧实现只读取新请求窗口内命中的切片;若新请求只查内部日 D2,D2 会被误当作旧派车组的真实首日/末日,并可能错误套用 R3-EX“真实首尾同城可衔接”,把双重占用误报为可共享。
|
||||||
|
|
||||||
|
本次后端在初筛命中后一次批量补齐同 `assignmentGroupId` 的全部有效切片,再按完整组真实首日、末日及首接客地、末送客地判断冲突。候选咨询、保存前预校验及最终写侧重校验使用同一口径。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
### `POST /admin/fleet/assignments/candidates`
|
||||||
|
|
||||||
|
响应形状不变,以下既有字段的语义修正:
|
||||||
|
|
||||||
|
| 场景 | `available` | `availabilityReasonCode` | `conflicts[].blocking` | `conflicts[].cityJunctionShareCandidate` |
|
||||||
|
|---|---:|---|---:|---:|
|
||||||
|
| 请求日位于已有多日派车组内部 | `false` | `ASSIGNMENT_CONFLICT` | `true` | `false` |
|
||||||
|
| 请求日仅与完整组真实首日或末日相接,且接送城市满足 R3-EX | `true` | `CITY_JUNCTION_SHAREABLE` | `false` | `true` |
|
||||||
|
| 无重叠 | `true` | `AVAILABLE` | - | - |
|
||||||
|
|
||||||
|
内部日冲突的 `conflicts[].startDate/endDate` 返回完整有效派车组范围;`availabilityWindows` 按该真实阻断范围扣减,不再把内部日当成可用窗口。
|
||||||
|
|
||||||
|
车辆和司机分别采用同一口径。仅查询 `holding/assigned` 活跃切片;历史空 `assignmentGroupId` 数据继续按原记录区间判断。
|
||||||
|
|
||||||
|
### `POST /admin/fleet/assignments/precheck`
|
||||||
|
|
||||||
|
响应形状不变:
|
||||||
|
|
||||||
|
- 内部日双重占用返回 `data.conflict=true`;
|
||||||
|
- `data.conflicts[]` 的车辆、司机冲突日期段均为完整有效派车组范围;
|
||||||
|
- 内部日 `cityJunctionShareCandidate=false`;
|
||||||
|
- 真实首尾同城衔接仍按既有 R3-EX 返回非阻断结果。
|
||||||
|
|
||||||
|
最终创建、改派、车务确认及撤销取消恢复仍在资源锁内重新校验,不信任前端咨询结果;本次同步修正这些写侧入口,避免候选正确但最终写入口径不同。
|
||||||
|
|
||||||
|
## 前端处理
|
||||||
|
|
||||||
|
无需修改前端代码或请求参数:
|
||||||
|
|
||||||
|
1. 继续以候选返回的 `available` 和 `availabilityReasonCode` 控制可选状态与原因展示;
|
||||||
|
2. 继续以预校验返回的 `conflict` 决定是否阻止提交;
|
||||||
|
3. 不要在前端自行按单日日期或城市覆盖后端冲突结果;
|
||||||
|
4. 已有 `ASSIGNMENT_CONFLICT`、`CITY_JUNCTION_SHAREABLE` 展示分支可直接消费修正后的数据。
|
||||||
|
|
||||||
|
因此 `frontend_status=not_required`。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不新增或删除 API 字段,不修改 Long ID 字符串、手机号脱敏及错误码契约。
|
||||||
|
- 不修改派单状态机、占用写入、保险、价格、对账或车辆/司机常驻关系。
|
||||||
|
- 不将 `completed/canceled` 切片重新计入活跃占用。
|
||||||
|
- 不修改 `hl-ui` 仓库。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 PR [wx/HL#5303](https://git.1814.love:8443/wx/HL/pulls/5303) 已 squash 合并至 `dev-v3`,合并提交 `d114232ad`。
|
||||||
|
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康。
|
||||||
|
- 定向 `AssignmentServiceTest,AssignmentCandidateServiceTest,FleetAssignmentMapperTest`:342 tests,0 failures,0 errors,0 skipped。
|
||||||
|
- Fleet reactor verify:2466 tests,0 failures,0 errors,1 skipped;Spotless 628 Java files clean。
|
||||||
|
- 真实测试网关选择一个 D1–D4 活跃派车组的内部日 D2 调用候选与预校验:HTTP/code 200;车辆与司机均 `available=false`、`ASSIGNMENT_CONFLICT`、`blocking=true`、`cityJunctionShareCandidate=false`,冲突范围返回完整 D1–D4;全程无业务写入。
|
||||||
|
- 网关证据:`D:/work2/HL-v3/.tmp/5302-gateway.json`,SHA-256 `b482a78d4129998c0ca3a8ee0e9a10ee63dfb25239fe1bd041b17f51043d4524`。
|
||||||
|
- OpenAPI/oasdiff:项目未配置可复现 Swagger2→OAS3 与 oasdiff,状态为 `not_configured`;使用源码对比、定向测试和真实网关响应作为人工回退证据。Spring Cloud Contract 为 `not_required`。
|
||||||
|
|
||||||
|
当前状态:后端已部署、网关已验证,前端无需改造。
|
||||||
|
|
||||||
|
关联:#5302。
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5305"
|
||||||
|
title: "逐日方案重提与完成闭环不变量"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "not_required"
|
||||||
|
frontend_owner: ""
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "仅修正既有逐日派车保存与需求完成事务语义;接口路径、方法、请求响应字段和前端交互均不变"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fleet:逐日方案重提与完成闭环不变量
|
||||||
|
|
||||||
|
> **服务**:`hl-fleet-service`
|
||||||
|
> **Issue**:#5305
|
||||||
|
> **影响页面**:管理后台车务管理 → 逐日逐车派车方案保存
|
||||||
|
> **兼容性**:接口路径、方法、请求/响应字段、错误码和数据库结构不变,仅修正既有事务语义
|
||||||
|
|
||||||
|
## 问题与目标
|
||||||
|
|
||||||
|
逐日派车支持重复提交最终方案。旧实现存在五类边界:全程不用车或没有实际创建命令时会绕过最终订单基线;留车/直派模式变化可能被误判为无变化;混合方案中的历史已完结切片可能因旧冻结标记缺失阻断需求完成;仅修改逐日价格原因不会落库;同槽索引的旧取消版本可能抢占当前稳定槽位。
|
||||||
|
|
||||||
|
本次后端统一修复上述重提、冻结和完成闭环,不新增前端参数或返回字段。
|
||||||
|
|
||||||
|
## 变更接口(既有语义修正)
|
||||||
|
|
||||||
|
### `POST /admin/fleet/assignments/batch`
|
||||||
|
|
||||||
|
请求和响应形状不变:
|
||||||
|
|
||||||
|
1. 包含 `dailyPlan` 的保存会在资源锁内完成写入后、冻结最终方案和生成需求完成 Outbox 前,再次读取当前订单详情、行程和 active 用车需求;日期、人数、行程或需求版本漂移时整批事务回滚。
|
||||||
|
2. 即使全部 `dailyPlan[].used=false`,或本次没有实际创建车辆/司机派单,也执行同一最终基线门禁。
|
||||||
|
3. 重提时同时比较目标 `holdMode`、派单状态、车辆、司机、接机参与、逐日价格和逐日价格调整原因:
|
||||||
|
- `holding → direct` 会按直派目标重新落地;
|
||||||
|
- `assigned → hold` 会按留车目标重新落地;
|
||||||
|
- 原因新增、修改和在不再偏离日历价时清空不再被忽略;偏离日历价时原因必填规则保持不变。
|
||||||
|
4. 历史 `completed` 逐日切片即使来自旧数据、没有 `dispatchPlanFinalized=1`,在同一矩阵已有显式冻结切片时也按不可变的已完成权威日处理;不修改其业务状态,也不会把纯旧版未冻结拓扑误认成新最终方案。
|
||||||
|
5. 同一 `fleetItemIndex` 同时存在旧 `canceled` 版本和当前在途/非取消版本时,稳定槽位优先沿用当前版本,避免重写命中历史槽位。
|
||||||
|
|
||||||
|
所有失败仍沿用现有错误响应结构;事务失败不产生需求完成事件。
|
||||||
|
|
||||||
|
## 前端处理
|
||||||
|
|
||||||
|
无需修改前端代码:
|
||||||
|
|
||||||
|
- 继续提交现有 `dailyPlan`、`holdMode` 和逐日价格原因;
|
||||||
|
- 基线漂移时继续展示后端现有失败提示并刷新后重试;
|
||||||
|
- 不需要新增字段、状态分支或页面组件。
|
||||||
|
|
||||||
|
因此 `frontend_status=not_required`。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不修改 Controller、VO/DTO/BO、Feign 或 shared Java 契约。
|
||||||
|
- 不修改数据库字段、迁移、Long ID 字符串和手机号脱敏规则。
|
||||||
|
- 不改变已完成/已取消业务状态,不放宽历史、完结或关账只读门禁。
|
||||||
|
- 不修改 `hl-ui` 仓库。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 PR `[wx/HL#5306](https://git.1814.love:8443/wx/HL/pulls/5306)` 已 squash 合并至 `dev-v3`,合并提交 ``594d932db``。
|
||||||
|
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康。
|
||||||
|
- 定向 `AssignmentServiceTest,FleetAssignmentMapperTest`:328 tests,0 failures,0 errors,0 skipped。
|
||||||
|
- Fleet reactor verify:2480 tests,0 failures,0 errors,1 skipped;Spotless 628 Java files clean。
|
||||||
|
- 独立 Reviewer 无未解决 P0–P1。
|
||||||
|
- 真实测试网关通过登录和车务矩阵只读查询确认部署后网关、认证及 Fleet 服务链路正常;本次内部事务修正没有可安全构造的写侧正例,核心五类边界由定向测试覆盖,探针不产生业务写入。
|
||||||
|
- 网关证据:`D:/work2/HL-v3/.tmp/5305-gateway.json`,SHA-256 ``4698b3b8b1765715f6dc823664b3d24df2e977d26e6e19da08b5e298c6b6b0f6``。
|
||||||
|
- 契约审查:`not_required`;当前 diff 无 Controller、请求响应模型、Feign 或 shared Java 变化。
|
||||||
|
|
||||||
|
当前状态:后端已部署、网关兼容探针已通过,前端无需改造。
|
||||||
|
|
||||||
|
关联:#5305。
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5307"
|
||||||
|
title: "最终不用车稳定槽看板状态与筛选"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "not_required"
|
||||||
|
frontend_owner: ""
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "后端 PR #5309 已合并并部署测试环境;既有字段的稳定槽聚合与状态筛选语义已修正,前端无需新增字段或改造"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fleet:最终不用车稳定槽看板状态与筛选
|
||||||
|
|
||||||
|
> **服务**:`hl-fleet-service`
|
||||||
|
> **Issue**:#5307
|
||||||
|
> **影响页面**:管理后台车务看板、配车矩阵、未派窗口及当天清单
|
||||||
|
> **兼容性**:接口路径、方法、请求参数和响应字段均不变化;仅修正既有状态、代表资源、统计和筛选语义
|
||||||
|
|
||||||
|
## 问题与目标
|
||||||
|
|
||||||
|
逐日最终方案允许同一稳定车辆槽位部分日期实际用车、其他日期明确不用车。此前最终不用车切片仍以基础状态 `unassigned` 持久化,会覆盖同槽真实已派切片,导致看板误显示待派、代表车辆/司机为空,并使状态筛选和矩阵统计出现幽灵待派。
|
||||||
|
|
||||||
|
本次统一识别 `dispatchPlanFinalized=1,dailyVehicleUsed=0` 的最终不用车事实。该事实不占车辆/司机、不投保、不计费,也不能重新开放普通派车动作。
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
### 车务看板 `/admin/fleet/board/orders`
|
||||||
|
|
||||||
|
- 同一稳定槽存在实际用车日和最终不用车日时,槽位状态、代表车辆和代表司机来自真实用车切片;稳定槽日期范围仍覆盖全部当前有效服务日。
|
||||||
|
- 全日期均最终不用车时,仅在看板只读聚合中归入“最终方案已完成”的既有 `assigned` 分面;数据库业务状态仍保持 `unassigned`,不会伪造派车、取消或完结事件。
|
||||||
|
- 全日期最终不用车记录固定关闭 `canAssign`、`canRejectRequirement`、`availableActionCodes` 和紧急徽章,不开放重复派车或需求驳回。
|
||||||
|
- `assignmentProgress` 按权威稳定槽守恒:混合槽和全最终不用车槽不再计入 `unassignedSlots`。
|
||||||
|
- `status/statuses` 先取得基础态候选超集,输出前再按最终稳定槽状态复筛;`assigned` 与 `unassigned` 互斥筛选不再返回同一需求。
|
||||||
|
- 大候选游标扫描会在 5,000 安全上限前排除普通待派,只保留可能聚合为最终方案完成的 final-unused 候选。
|
||||||
|
|
||||||
|
### 配车矩阵 `/admin/fleet/matrix/*`
|
||||||
|
|
||||||
|
- 最终不用车切片不进入车辆甘特条、顶部待派统计、月份统计、未派窗口、当天待派清单、并行派车或衔接计算。
|
||||||
|
- 混合槽仍展示所有真实用车车辆段及实际司机,不由无车切片覆盖。
|
||||||
|
- 全日期最终不用车不会产生幽灵待派;详情接口原有 `dailyVehiclePlan[].planState=NOT_USED` 事实保持不变。
|
||||||
|
- 普通未最终化 `unassigned`、`holding/assigned/completed`、人工加急五键统计、Long ID 字符串和手机号脱敏均保持原语义。
|
||||||
|
|
||||||
|
## 前端处理
|
||||||
|
|
||||||
|
无需新增字段或修改请求。继续直接消费后端现有:
|
||||||
|
|
||||||
|
- `assignmentStatus`、`assignmentStatusLabel`
|
||||||
|
- `canAssign`、`canRejectRequirement`、`availableActionCodes`
|
||||||
|
- `assignmentProgress.finalizedByFleet`
|
||||||
|
- `assignmentSlots[]`
|
||||||
|
- 矩阵 `statusCounts` 与 `effectiveStatusCounts`
|
||||||
|
- 详情 `dailyVehiclePlan[].planState`
|
||||||
|
|
||||||
|
不要根据基础 `unassigned`、车辆为空或司机为空自行覆盖后端返回的稳定槽状态和操作权限。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不修改派单五态数据库状态机。
|
||||||
|
- 不修改逐日方案写入、车辆/司机占用、费用、保险、对账或完成回调。
|
||||||
|
- 不新增 Feign、数据库查询或逐订单 N+1。
|
||||||
|
- 不修改 `hl-ui`;本次为既有字段语义纠正,前端状态为 `not_required`。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 PR [wx/HL#5309](https://git.1814.love:8443/wx/HL/pulls/5309) 已 squash 合并至 `dev-v3`,合并提交 `ca23f64fc`。
|
||||||
|
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康。
|
||||||
|
- 定向核心:395 tests,0 failures,0 errors;Reviewer 上限回归 `BoardOrderServiceTest` 59 tests 通过。
|
||||||
|
- Fleet reactor verify:2487 tests,0 failures,0 errors,1 skipped;Spotless 629 Java files clean。
|
||||||
|
- 独立 Reviewer 首轮发现 assigned 候选超集可能提前触发 5,000 上限;修复并补充“5,000 普通待派 + 1 最终不用车”游标测试后,终审无 P0–P2。
|
||||||
|
- 真实测试网关只读验证:矩阵精确状态筛选仅返回请求状态、五键统计守恒;看板 assigned/unassigned 返回状态与筛选一致且需求集合互斥;已完成最终方案未进入 unassigned;Long ID 仍为字符串、手机号保持脱敏,全程无业务写入。
|
||||||
|
- 测试环境 API 未暴露内部 final-unused 标记且当前样本未确认存在合法混合槽,因此未伪报真实网关正例;混合槽和全 final-unused 正例由 Service 定向测试覆盖。
|
||||||
|
- 网关证据:`D:/work2/HL-v3/.tmp/5307-gateway.json`,SHA-256 `699363c3a06b0d40205ea477c2a0a67daadc2e1718d08ba1102733e66cf2d505`。
|
||||||
|
- OpenAPI 与 Spring Cloud Contract:`not_required`,因为 Controller、DTO/VO/BO、Feign、shared Java、枚举及错误码形状均未变化。
|
||||||
|
|
||||||
|
当前状态:后端已部署、网关已验证,前端无需改造。
|
||||||
|
|
||||||
|
关联:#5307、#5292、#5301。
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5308"
|
||||||
|
title: "最终派车方案取消恢复代际"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "not_required"
|
||||||
|
frontend_owner: ""
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "仅修正 Fleet 内部最终方案身份、取消恢复与需求完成语义;接口路径、请求响应字段和前端交互均不变"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fleet:最终派车方案取消恢复代际
|
||||||
|
|
||||||
|
> **服务**:`hl-fleet-service`
|
||||||
|
> **Issue**:#5308
|
||||||
|
> **影响页面**:管理后台车务管理 → 逐日派车方案保存、手工取消/恢复、DIRECT/HOLD 改派
|
||||||
|
> **兼容性**:接口路径、方法、请求/响应字段、错误码及业务五态不变;仅修正内部最终方案身份和需求完成闭环
|
||||||
|
|
||||||
|
## 问题与目标
|
||||||
|
|
||||||
|
自定义最终方案完成后,手工取消某个稳定车辆槽位会触发需求重开,恢复后应重新完成。旧实现只有布尔型最终标记,无法区分当前方案、合法改派 tombstone 和后续重提的新方案;多槽部分取消时,剩余槽位可能被误判为新的完整方案,旧取消行也可能在新方案后被错误恢复。
|
||||||
|
|
||||||
|
本次为 Fleet 内部最终派车方案增加持久化代际。代际是数据库内部不透明令牌,不进入 API、完成回调或前端状态。
|
||||||
|
|
||||||
|
## 变更接口(既有语义修正)
|
||||||
|
|
||||||
|
### `POST /admin/fleet/assignments/batch`
|
||||||
|
|
||||||
|
请求和响应形状不变:
|
||||||
|
|
||||||
|
1. 每次最终方案提交会按精确 `assignmentIds` 将全部当前可变切片标记为同一新代,并严格核对实际更新行数;精确重提、仅价格原因重提和 partial daily rewrite 均按新当前代收敛。
|
||||||
|
2. 当前方案按 `stable slotId × serviceDate` 全拓扑校验。多个非空 current generation、当前代外 active 行、孤儿 generation,以及存在退休历史但没有可解析非空 current 代的场景均 fail closed,不再回退为订单建议数量的“看似完整”方案。
|
||||||
|
3. 滚动发布窗口仅兼容“单一非空 current generation + `dispatchPlanFinalized=1` 的 legacy null 行”;这些行按同代完整拓扑校验,不按 generation 数值大小推断先后。
|
||||||
|
|
||||||
|
### 既有取消、恢复与改派接口
|
||||||
|
|
||||||
|
路径和字段均不变:
|
||||||
|
|
||||||
|
- 手工取消保留当前代身份;只恢复一个被取消槽位时仍保持不完整,全部当前代逻辑 key 恢复后才重新生成或重启 `REQUIREMENT_DONE`。
|
||||||
|
- REOPEN 先到时,恢复后按相同 topology fingerprint 重新激活完成事件;恢复先到时,迟到 REOPEN 会因当前拓扑已完整而跳过,不回退需求状态。
|
||||||
|
- DIRECT/HOLD 正常改派的 replacement 继承当前代。合法 canceled tombstone 在存在唯一同代 active replacement 时不污染完整性;HOLD 在确认前仍不完整。
|
||||||
|
- 已退休旧代、已有同代 active replacement 的 tombstone、以及全局失效后仅保留退休代际证据的取消行均禁止恢复。
|
||||||
|
- driver reject、daily rewrite、订单/需求系统取消会退休 current 标记并保留 generation 作为禁止恢复证据;跨 requirement rebind 会清除旧身份,下一次最终提交建立新代。
|
||||||
|
- `completed`、`canceled` 业务状态和历史/完结/关账只读规则不变;generation 不进入完成 topology fingerprint。
|
||||||
|
|
||||||
|
## 数据库迁移
|
||||||
|
|
||||||
|
新增 Fleet 内部 nullable BIGINT:`fleet_assignment.dispatch_plan_generation`。
|
||||||
|
|
||||||
|
- 迁移先把存量 `canceled + dispatch_plan_finalized=1` 的历史改派 tombstone 退休为非 current。
|
||||||
|
- 再仅对非 canceled 的存量 current 行按 requirement 回填兼容 generation。
|
||||||
|
- canceled 历史不猜测代际;无新增索引,读取仍按 `requirement_id` 批量完成。
|
||||||
|
|
||||||
|
## 前端处理
|
||||||
|
|
||||||
|
无需修改前端代码:
|
||||||
|
|
||||||
|
- 继续使用现有逐日方案保存、取消、恢复、DIRECT/HOLD 改派接口和状态字段;
|
||||||
|
- 不新增 `generation` 请求或响应字段,不应在前端推断方案代际;
|
||||||
|
- 状态刷新、错误提示和页面交互保持现状。
|
||||||
|
|
||||||
|
因此 `frontend_status=not_required`,且不修改 `hl-ui`。
|
||||||
|
|
||||||
|
## 不影响范围
|
||||||
|
|
||||||
|
- 不修改 Controller mapping、请求/响应 DTO/VO/BO、Feign、shared Java、枚举或错误码。
|
||||||
|
- 不修改车辆/司机冲突规则、费用、保险、对账或业务五态。
|
||||||
|
- 不按 Snowflake 数值比较代际先后。
|
||||||
|
- 不产生逐槽或逐候选 N+1。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 后端 PR [wx/HL#5312](https://git.1814.love:8443/wx/HL/pulls/5312) 已 squash 合并至 `dev-v3`,合并提交 `883fc61a0`。
|
||||||
|
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康;验证部署任务 `493f862d` 成功。
|
||||||
|
- 定向 `AssignmentServiceTest`:302 tests,0 failures,0 errors,0 skipped。
|
||||||
|
- Fleet reactor verify:2507 tests,0 failures,0 errors,1 skipped;Spotless 629 Java files clean。
|
||||||
|
- 真表 BIGINT 落库读回、全局失效退休证据及迁移先退休 tombstone 再回填 active 的行为测试通过。
|
||||||
|
- 两轮独立 Reviewer 共发现 4 个 P1,均已修复并补充回归测试;无未解决 P0/P1。
|
||||||
|
- 真实测试网关只读验证:矩阵精确状态筛选和统计守恒、看板 assigned/unassigned 互斥、Long ID 字符串及手机号脱敏均保持兼容,全程无业务写入。
|
||||||
|
- 内部 generation 不通过网关暴露,且取消/恢复正例必须产生业务写入,因此未伪造网关正例;完整代际生命周期由 Service、Mapper 真表和迁移测试覆盖。
|
||||||
|
- 网关证据:`D:/work2/HL-v3/.tmp/5308-gateway.json`,SHA-256 `3778da1c8c0d270e3d1da75821e54bfce49ea25bc36f09e8440932ed6e4efa07`。
|
||||||
|
- OpenAPI 与 Spring Cloud Contract:`not_required`,因为无 API/Feign/shared Java 契约形状变化。
|
||||||
|
|
||||||
|
当前状态:后端已合并、部署并通过网关兼容探针,前端无需改造。
|
||||||
|
|
||||||
|
关联:#5308、#5292、#5305。
|
||||||
@@ -0,0 +1,892 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5310"
|
||||||
|
title: "核单其他收支保存即确认"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "pending"
|
||||||
|
frontend_owner: ""
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "其他收入、其他支出保存类接口改为保存即确认;两个独立确认接口已删除,等待管理后台前端停止调用确认接口并处理存量未确认记录重存。"
|
||||||
|
updated_at: "2026-07-28"
|
||||||
|
base: "dev-v3"
|
||||||
|
generated: "2026-07-28T11:54:21+08:00"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 【修改接口·管理后台】核单其他收支保存即确认 (#5310)
|
||||||
|
|
||||||
|
> **PR**: #5313 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-28 11:54
|
||||||
|
|
||||||
|
## 1. 接口背景
|
||||||
|
|
||||||
|
核单「其他收入」「其他支出」不再需要先保存、再单独点确认。保存成功即视为已确认,前端不再调用独立确认接口。
|
||||||
|
|
||||||
|
## 2. 变更清单
|
||||||
|
|
||||||
|
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||||
|
|---|------|------|------|----------|------|
|
||||||
|
| 1 | 新增其他收入并原子创建订单增费 | POST | `/v3/admin/order/{orderId}/settlement/other-incomes` | 修改 | 保存成功后返回 `settlementConfirmStatus=CONFIRMED`、`settlementConfirmStatusName=已确认` |
|
||||||
|
| 2 | 修改其他收入;金额或项目名变化时原子冲销并重建订单增费 | PUT | `/v3/admin/order/{orderId}/settlement/other-incomes/{incomeId}` | 修改 | 保存成功后返回 `settlementConfirmStatus=CONFIRMED`、`settlementConfirmStatusName=已确认` |
|
||||||
|
| 3 | 新增其他支出 | POST | `/v3/admin/order/{orderId}/settlement/other-expenses` | 修改 | 保存成功后返回 `settlementConfirmStatus=CONFIRMED` |
|
||||||
|
| 4 | 修改其他支出 | PUT | `/v3/admin/order/{orderId}/settlement/other-expenses/{settlementId}` | 修改 | 保存成功后返回 `settlementConfirmStatus=CONFIRMED` |
|
||||||
|
| 5 | 批量确认其他收入 | POST | `/v3/admin/order/{orderId}/settlement/other-incomes/confirm` | 删除 | 接口删除;前端不要再调用 |
|
||||||
|
| 6 | 确认其他支出 | POST | `/v3/admin/order/{orderId}/settlement/other-expenses/confirm` | 删除 | 接口删除;前端不要再调用 |
|
||||||
|
|
||||||
|
## 3. 接口详情
|
||||||
|
|
||||||
|
### 3.1 新增其他收入
|
||||||
|
|
||||||
|
- **方法 + 路径**:`POST /v3/admin/order/{orderId}/settlement/other-incomes`
|
||||||
|
- **接口名**:新增其他收入并原子创建订单增费
|
||||||
|
- **使用场景**:在核单其他收入页新增一条其他收入。
|
||||||
|
- **认证**:需要管理后台登录态;需要资金写入权限。
|
||||||
|
- **幂等性**:是;同一订单内 `requestId` 永久唯一,相同 `requestId` 且请求载荷一致时返回同一条记录。
|
||||||
|
- **限流**:无接口专属限流。
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `orderId` | Long | 是 | 订单 ID,必须大于 0 |
|
||||||
|
|
||||||
|
**请求体字段**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|
||||||
|
|------|------|------|------|----------|
|
||||||
|
| `requestId` | String | 是 | 客户端生成的稳定幂等请求 ID,同订单内永久唯一 | 非空,最长 64 字符 |
|
||||||
|
| `incomeDate` | String(date) | 是 | 收入日期,格式 `YYYY-MM-DD` | 非空 |
|
||||||
|
| `projectName` | String | 是 | 项目名称 | 非空,最长 100 字符 |
|
||||||
|
| `projectCategory` | String | 是 | 项目类别 | 非空,最长 64 字符 |
|
||||||
|
| `specification` | String | 否 | 票种/规格 | 最长 100 字符 |
|
||||||
|
| `quantity` | Decimal | 是 | 数量 | >= 0,最多 8 位整数、4 位小数 |
|
||||||
|
| `unitPrice` | Decimal | 是 | 核算单价 | >= 0,最多 8 位整数、2 位小数 |
|
||||||
|
| `settlementAmount` | Decimal | 是 | 核算金额 | > 0,最多 8 位整数、2 位小数;必须等于 `quantity * unitPrice` 四舍五入到 2 位 |
|
||||||
|
| `paymentMethod` | String | 是 | 付款类型 | `CASH_PAID` / `COMPANY_PAID` / `SIGNED` |
|
||||||
|
| `voucherUrls` | String[] | 否 | 凭证 URL 列表 | 最多 9 项;每项最长 1024 字符;必须是 http/https |
|
||||||
|
| `remark` | String | 否 | 备注 | 最长 500 字符 |
|
||||||
|
|
||||||
|
**响应字段:`Result<SettlementOtherIncomeItemRespVO>`**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `code` | Integer | 业务码,成功为 `200` |
|
||||||
|
| `msg` | String | 响应消息 |
|
||||||
|
| `data.id` | String | 其他收入 ID |
|
||||||
|
| `data.requestId` | String | 手工新增幂等请求 ID;自动投影为空 |
|
||||||
|
| `data.incomeDate` | String(date) | 收入日期 |
|
||||||
|
| `data.projectName` | String | 项目名称 |
|
||||||
|
| `data.projectCategory` | String | 项目类别 |
|
||||||
|
| `data.projectCategoryName` | String | 项目类别名称 |
|
||||||
|
| `data.specification` | String | 票种/规格 |
|
||||||
|
| `data.quantity` | Decimal | 数量 |
|
||||||
|
| `data.unitPrice` | Decimal | 核算单价 |
|
||||||
|
| `data.settlementAmount` | Decimal | 核算金额 |
|
||||||
|
| `data.paymentMethod` | String | 付款类型 |
|
||||||
|
| `data.paymentMethodName` | String | 付款类型名称 |
|
||||||
|
| `data.voucherUrls` | String[] | 凭证 URL 列表 |
|
||||||
|
| `data.settlementConfirmStatus` | String | 确认状态;本接口保存成功返回 `CONFIRMED` |
|
||||||
|
| `data.settlementConfirmStatusName` | String | 确认状态名称;本接口保存成功返回 `已确认` |
|
||||||
|
| `data.remark` | String | 备注 |
|
||||||
|
| `data.sourceType` | String | 来源类型 |
|
||||||
|
| `data.sourceTypeName` | String | 来源类型名称 |
|
||||||
|
| `data.sourceId` | String | 来源附加费 ID |
|
||||||
|
|
||||||
|
**错误码**
|
||||||
|
|
||||||
|
| code | 含义 | 触发场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、枚举非法、凭证 URL 非 http/https、金额不等于数量乘单价 |
|
||||||
|
| `584074` | 当前核单状态不允许修改其他收入 | 订单核单状态不是待核单或核单中 |
|
||||||
|
| `584075` | 其他收入关联的附加费来源无效 | 保存后无法得到有效来源记录 |
|
||||||
|
| `584076` | 其他收入核算金额必须等于数量乘以核算单价 | `settlementAmount` 与 `quantity * unitPrice` 不一致 |
|
||||||
|
| `584087` | requestId 已用于另一笔其他收入 | 同订单重复使用 `requestId`,但请求载荷不同 |
|
||||||
|
| `584089` | 核单或结算已完成,资金数据不可再修改 | 订单资金数据已锁定 |
|
||||||
|
|
||||||
|
**示例:典型成功**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v3/admin/order/60001/settlement/other-incomes
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requestId": "oi-20260728-0001",
|
||||||
|
"incomeDate": "2026-07-28",
|
||||||
|
"projectName": "临时加收房差",
|
||||||
|
"projectCategory": "房差",
|
||||||
|
"specification": "双人间",
|
||||||
|
"quantity": 2,
|
||||||
|
"unitPrice": 120.00,
|
||||||
|
"settlementAmount": 240.00,
|
||||||
|
"paymentMethod": "CASH_PAID",
|
||||||
|
"voucherUrls": ["https://cdn.example.com/vouchers/income-1.jpg"],
|
||||||
|
"remark": "现场补收"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "99001",
|
||||||
|
"requestId": "oi-20260728-0001",
|
||||||
|
"incomeDate": "2026-07-28",
|
||||||
|
"projectName": "临时加收房差",
|
||||||
|
"projectCategory": "房差",
|
||||||
|
"projectCategoryName": "房差",
|
||||||
|
"specification": "双人间",
|
||||||
|
"quantity": 2,
|
||||||
|
"unitPrice": 120.00,
|
||||||
|
"settlementAmount": 240.00,
|
||||||
|
"paymentMethod": "CASH_PAID",
|
||||||
|
"paymentMethodName": "现付",
|
||||||
|
"voucherUrls": ["https://cdn.example.com/vouchers/income-1.jpg"],
|
||||||
|
"settlementConfirmStatus": "CONFIRMED",
|
||||||
|
"settlementConfirmStatusName": "已确认",
|
||||||
|
"remark": "现场补收",
|
||||||
|
"sourceType": "ORDER_SURCHARGE",
|
||||||
|
"sourceTypeName": "订单增费",
|
||||||
|
"sourceId": "88001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:边界成功**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requestId": "oi-20260728-0002",
|
||||||
|
"incomeDate": "2026-07-28",
|
||||||
|
"projectName": "其他收入",
|
||||||
|
"projectCategory": "其他",
|
||||||
|
"quantity": 0.0001,
|
||||||
|
"unitPrice": 100.00,
|
||||||
|
"settlementAmount": 0.01,
|
||||||
|
"paymentMethod": "SIGNED",
|
||||||
|
"voucherUrls": [],
|
||||||
|
"remark": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "99002",
|
||||||
|
"requestId": "oi-20260728-0002",
|
||||||
|
"incomeDate": "2026-07-28",
|
||||||
|
"projectName": "其他收入",
|
||||||
|
"projectCategory": "其他",
|
||||||
|
"projectCategoryName": "其他",
|
||||||
|
"specification": null,
|
||||||
|
"quantity": 0.0001,
|
||||||
|
"unitPrice": 100.00,
|
||||||
|
"settlementAmount": 0.01,
|
||||||
|
"paymentMethod": "SIGNED",
|
||||||
|
"paymentMethodName": "签单",
|
||||||
|
"voucherUrls": [],
|
||||||
|
"settlementConfirmStatus": "CONFIRMED",
|
||||||
|
"settlementConfirmStatusName": "已确认",
|
||||||
|
"remark": null,
|
||||||
|
"sourceType": "ORDER_SURCHARGE",
|
||||||
|
"sourceTypeName": "订单增费",
|
||||||
|
"sourceId": "88002"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:业务失败**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requestId": "oi-20260728-0003",
|
||||||
|
"incomeDate": "2026-07-28",
|
||||||
|
"projectName": "加收费用",
|
||||||
|
"projectCategory": "其他",
|
||||||
|
"quantity": 2,
|
||||||
|
"unitPrice": 100.00,
|
||||||
|
"settlementAmount": 199.00,
|
||||||
|
"paymentMethod": "CASH_PAID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 584076,
|
||||||
|
"msg": "其他收入核算金额必须等于数量乘以核算单价",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 修改其他收入
|
||||||
|
|
||||||
|
- **方法 + 路径**:`PUT /v3/admin/order/{orderId}/settlement/other-incomes/{incomeId}`
|
||||||
|
- **接口名**:修改其他收入;金额或项目名变化时原子冲销并重建订单增费
|
||||||
|
- **使用场景**:修改已有其他收入;也用于把存量 `UNCONFIRMED` 记录重新保存为 `CONFIRMED`。
|
||||||
|
- **认证**:需要管理后台登录态;需要资金写入权限。
|
||||||
|
- **幂等性**:否。
|
||||||
|
- **限流**:无接口专属限流。
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `orderId` | Long | 是 | 订单 ID,必须大于 0 |
|
||||||
|
| `incomeId` | Long | 是 | 其他收入 ID,必须大于 0 |
|
||||||
|
|
||||||
|
**请求体字段**
|
||||||
|
|
||||||
|
同 §3.1,但不包含 `requestId`。
|
||||||
|
|
||||||
|
**响应字段**
|
||||||
|
|
||||||
|
同 §3.1;保存成功后 `data.settlementConfirmStatus=CONFIRMED`、`data.settlementConfirmStatusName=已确认`。
|
||||||
|
|
||||||
|
**错误码**
|
||||||
|
|
||||||
|
| code | 含义 | 触发场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、枚举非法、金额不一致 |
|
||||||
|
| `584073` | 其他收入不存在或不属于当前订单 | `incomeId` 不存在或不属于 `orderId` |
|
||||||
|
| `584074` | 当前核单状态不允许修改其他收入 | 订单核单状态不是待核单或核单中 |
|
||||||
|
| `584076` | 其他收入核算金额必须等于数量乘以核算单价 | `settlementAmount` 与 `quantity * unitPrice` 不一致 |
|
||||||
|
| `584089` | 核单或结算已完成,资金数据不可再修改 | 订单资金数据已锁定 |
|
||||||
|
|
||||||
|
**示例:典型成功**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /v3/admin/order/60001/settlement/other-incomes/99001
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"incomeDate": "2026-07-28",
|
||||||
|
"projectName": "临时加收房差",
|
||||||
|
"projectCategory": "房差",
|
||||||
|
"specification": "双人间",
|
||||||
|
"quantity": 2,
|
||||||
|
"unitPrice": 130.00,
|
||||||
|
"settlementAmount": 260.00,
|
||||||
|
"paymentMethod": "COMPANY_PAID",
|
||||||
|
"voucherUrls": ["https://cdn.example.com/vouchers/income-2.jpg"],
|
||||||
|
"remark": "修改金额"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "99001",
|
||||||
|
"requestId": "oi-20260728-0001",
|
||||||
|
"incomeDate": "2026-07-28",
|
||||||
|
"projectName": "临时加收房差",
|
||||||
|
"projectCategory": "房差",
|
||||||
|
"projectCategoryName": "房差",
|
||||||
|
"specification": "双人间",
|
||||||
|
"quantity": 2,
|
||||||
|
"unitPrice": 130.00,
|
||||||
|
"settlementAmount": 260.00,
|
||||||
|
"paymentMethod": "COMPANY_PAID",
|
||||||
|
"paymentMethodName": "公司付款",
|
||||||
|
"voucherUrls": ["https://cdn.example.com/vouchers/income-2.jpg"],
|
||||||
|
"settlementConfirmStatus": "CONFIRMED",
|
||||||
|
"settlementConfirmStatusName": "已确认",
|
||||||
|
"remark": "修改金额",
|
||||||
|
"sourceType": "ORDER_SURCHARGE",
|
||||||
|
"sourceTypeName": "订单增费",
|
||||||
|
"sourceId": "88003"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:边界成功(存量未确认重存)**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"incomeDate": "2026-07-20",
|
||||||
|
"projectName": "历史其他收入",
|
||||||
|
"projectCategory": "其他",
|
||||||
|
"quantity": 1,
|
||||||
|
"unitPrice": 88.00,
|
||||||
|
"settlementAmount": 88.00,
|
||||||
|
"paymentMethod": "CASH_PAID",
|
||||||
|
"voucherUrls": [],
|
||||||
|
"remark": "重存后确认"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "99010",
|
||||||
|
"requestId": "oi-old-001",
|
||||||
|
"incomeDate": "2026-07-20",
|
||||||
|
"projectName": "历史其他收入",
|
||||||
|
"projectCategory": "其他",
|
||||||
|
"projectCategoryName": "其他",
|
||||||
|
"specification": null,
|
||||||
|
"quantity": 1,
|
||||||
|
"unitPrice": 88.00,
|
||||||
|
"settlementAmount": 88.00,
|
||||||
|
"paymentMethod": "CASH_PAID",
|
||||||
|
"paymentMethodName": "现付",
|
||||||
|
"voucherUrls": [],
|
||||||
|
"settlementConfirmStatus": "CONFIRMED",
|
||||||
|
"settlementConfirmStatusName": "已确认",
|
||||||
|
"remark": "重存后确认",
|
||||||
|
"sourceType": "ORDER_SURCHARGE",
|
||||||
|
"sourceTypeName": "订单增费",
|
||||||
|
"sourceId": "88010"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:业务失败**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"incomeDate": "2026-07-28",
|
||||||
|
"projectName": "不存在记录",
|
||||||
|
"projectCategory": "其他",
|
||||||
|
"quantity": 1,
|
||||||
|
"unitPrice": 10.00,
|
||||||
|
"settlementAmount": 10.00,
|
||||||
|
"paymentMethod": "CASH_PAID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 584073,
|
||||||
|
"msg": "其他收入不存在或不属于当前订单",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 新增其他支出
|
||||||
|
|
||||||
|
- **方法 + 路径**:`POST /v3/admin/order/{orderId}/settlement/other-expenses`
|
||||||
|
- **接口名**:新增其他支出
|
||||||
|
- **使用场景**:在核单其他支出页新增一条其他支出。
|
||||||
|
- **认证**:需要管理后台登录态;需要资金写入权限。
|
||||||
|
- **幂等性**:否。
|
||||||
|
- **限流**:无接口专属限流。
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `orderId` | Long | 是 | 订单 ID,必须大于 0 |
|
||||||
|
|
||||||
|
**请求体字段**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|
||||||
|
|------|------|------|------|----------|
|
||||||
|
| `expenseType` | String | 是 | 支出类型 | `FUEL` / `TOLL` / `PARKING` / `RENTAL` / `MAINTENANCE` / `OTHER` |
|
||||||
|
| `projectName` | String | 是 | 项目名称 | 非空,最长 200 字符 |
|
||||||
|
| `expenseDate` | String(date) | 否 | 发生日期,格式 `YYYY-MM-DD` | 可空 |
|
||||||
|
| `actualAmount` | Decimal | 是 | 实际金额 | >= 0,最多 8 位整数、2 位小数 |
|
||||||
|
| `paymentMethod` | String | 是 | 付款类型 | `CASH_PAID` / `COMPANY_PAID` / `SIGNED` |
|
||||||
|
| `voucherUrls` | String[] | 否 | 凭证 URL 列表 | 最多 9 项;每项最长 1024 字符;必须是 http/https |
|
||||||
|
| `remark` | String | 否 | 备注 | 最长 512 字符 |
|
||||||
|
|
||||||
|
**响应字段:`Result<SettlementOtherExpenseRespVO>`**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `code` | Integer | 业务码,成功为 `200` |
|
||||||
|
| `msg` | String | 响应消息 |
|
||||||
|
| `data.id` | String | 其他支出 ID |
|
||||||
|
| `data.expenseType` | String | 支出类型 |
|
||||||
|
| `data.projectName` | String | 项目名称 |
|
||||||
|
| `data.expenseDate` | String(date) | 发生日期 |
|
||||||
|
| `data.actualAmount` | String | 实际金额 |
|
||||||
|
| `data.paymentMethod` | String | 付款类型 |
|
||||||
|
| `data.voucherUrls` | String[] | 凭证 URL 列表 |
|
||||||
|
| `data.settlementConfirmStatus` | String | 确认状态;本接口保存成功返回 `CONFIRMED` |
|
||||||
|
| `data.remark` | String | 备注 |
|
||||||
|
|
||||||
|
**错误码**
|
||||||
|
|
||||||
|
| code | 含义 | 触发场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、枚举非法 |
|
||||||
|
| `584095` | 其他支出字段超出允许范围 | `expenseType` 非法、项目名为空或超长、金额超范围、备注超长 |
|
||||||
|
| `584096` | 付款类型不合法 | `paymentMethod` 不是允许值 |
|
||||||
|
| `584097` | 凭证 URL 格式或数量不合法 | 凭证数量超限、非 http/https、单项超长 |
|
||||||
|
| `584307` | 当前核单状态不允许写入餐食或其他支出 | 订单核单状态不是待核单或核单中 |
|
||||||
|
| `584089` | 核单或结算已完成,资金数据不可再修改 | 订单资金数据已锁定 |
|
||||||
|
|
||||||
|
**示例:典型成功**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v3/admin/order/60001/settlement/other-expenses
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expenseType": "TOLL",
|
||||||
|
"projectName": "过路费",
|
||||||
|
"expenseDate": "2026-07-28",
|
||||||
|
"actualAmount": 50.00,
|
||||||
|
"paymentMethod": "CASH_PAID",
|
||||||
|
"voucherUrls": ["https://cdn.example.com/vouchers/toll.jpg"],
|
||||||
|
"remark": "高速通行费"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "801",
|
||||||
|
"expenseType": "TOLL",
|
||||||
|
"projectName": "过路费",
|
||||||
|
"expenseDate": "2026-07-28",
|
||||||
|
"actualAmount": "50.00",
|
||||||
|
"paymentMethod": "CASH_PAID",
|
||||||
|
"voucherUrls": ["https://cdn.example.com/vouchers/toll.jpg"],
|
||||||
|
"settlementConfirmStatus": "CONFIRMED",
|
||||||
|
"remark": "高速通行费"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:边界成功**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expenseType": "OTHER",
|
||||||
|
"projectName": "0元备注支出",
|
||||||
|
"expenseDate": null,
|
||||||
|
"actualAmount": 0.00,
|
||||||
|
"paymentMethod": "SIGNED",
|
||||||
|
"voucherUrls": [],
|
||||||
|
"remark": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "802",
|
||||||
|
"expenseType": "OTHER",
|
||||||
|
"projectName": "0元备注支出",
|
||||||
|
"expenseDate": null,
|
||||||
|
"actualAmount": "0.00",
|
||||||
|
"paymentMethod": "SIGNED",
|
||||||
|
"voucherUrls": [],
|
||||||
|
"settlementConfirmStatus": "CONFIRMED",
|
||||||
|
"remark": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:业务失败**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expenseType": "BAD_TYPE",
|
||||||
|
"projectName": "非法支出类型",
|
||||||
|
"actualAmount": 10.00,
|
||||||
|
"paymentMethod": "CASH_PAID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 584095,
|
||||||
|
"msg": "其他支出字段超出允许范围",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 修改其他支出
|
||||||
|
|
||||||
|
- **方法 + 路径**:`PUT /v3/admin/order/{orderId}/settlement/other-expenses/{settlementId}`
|
||||||
|
- **接口名**:修改其他支出
|
||||||
|
- **使用场景**:修改已有其他支出;也用于把存量 `UNCONFIRMED` 记录重新保存为 `CONFIRMED`。
|
||||||
|
- **认证**:需要管理后台登录态;需要资金写入权限。
|
||||||
|
- **幂等性**:否。
|
||||||
|
- **限流**:无接口专属限流。
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `orderId` | Long | 是 | 订单 ID,必须大于 0 |
|
||||||
|
| `settlementId` | Long | 是 | 其他支出 ID,必须大于 0 |
|
||||||
|
|
||||||
|
**请求体字段**
|
||||||
|
|
||||||
|
同 §3.3。
|
||||||
|
|
||||||
|
**响应字段**
|
||||||
|
|
||||||
|
同 §3.3;保存成功后 `data.settlementConfirmStatus=CONFIRMED`。
|
||||||
|
|
||||||
|
**错误码**
|
||||||
|
|
||||||
|
| code | 含义 | 触发场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、枚举非法 |
|
||||||
|
| `584091` | 其他支出不存在或不属于当前订单 | `settlementId` 不存在或不属于 `orderId` |
|
||||||
|
| `584095` | 其他支出字段超出允许范围 | `expenseType` 非法、项目名为空或超长、金额超范围、备注超长 |
|
||||||
|
| `584096` | 付款类型不合法 | `paymentMethod` 不是允许值 |
|
||||||
|
| `584097` | 凭证 URL 格式或数量不合法 | 凭证数量超限、非 http/https、单项超长 |
|
||||||
|
| `584307` | 当前核单状态不允许写入餐食或其他支出 | 订单核单状态不是待核单或核单中 |
|
||||||
|
| `584089` | 核单或结算已完成,资金数据不可再修改 | 订单资金数据已锁定 |
|
||||||
|
|
||||||
|
**示例:典型成功**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /v3/admin/order/60001/settlement/other-expenses/801
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expenseType": "PARKING",
|
||||||
|
"projectName": "停车费",
|
||||||
|
"expenseDate": "2026-07-28",
|
||||||
|
"actualAmount": 35.00,
|
||||||
|
"paymentMethod": "COMPANY_PAID",
|
||||||
|
"voucherUrls": ["https://cdn.example.com/vouchers/parking.jpg"],
|
||||||
|
"remark": "改为停车费"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "801",
|
||||||
|
"expenseType": "PARKING",
|
||||||
|
"projectName": "停车费",
|
||||||
|
"expenseDate": "2026-07-28",
|
||||||
|
"actualAmount": "35.00",
|
||||||
|
"paymentMethod": "COMPANY_PAID",
|
||||||
|
"voucherUrls": ["https://cdn.example.com/vouchers/parking.jpg"],
|
||||||
|
"settlementConfirmStatus": "CONFIRMED",
|
||||||
|
"remark": "改为停车费"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:边界成功(存量未确认重存)**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expenseType": "OTHER",
|
||||||
|
"projectName": "历史其他支出",
|
||||||
|
"expenseDate": "2026-07-20",
|
||||||
|
"actualAmount": 1.00,
|
||||||
|
"paymentMethod": "CASH_PAID",
|
||||||
|
"voucherUrls": [],
|
||||||
|
"remark": "重存后确认"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "810",
|
||||||
|
"expenseType": "OTHER",
|
||||||
|
"projectName": "历史其他支出",
|
||||||
|
"expenseDate": "2026-07-20",
|
||||||
|
"actualAmount": "1.00",
|
||||||
|
"paymentMethod": "CASH_PAID",
|
||||||
|
"voucherUrls": [],
|
||||||
|
"settlementConfirmStatus": "CONFIRMED",
|
||||||
|
"remark": "重存后确认"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:业务失败**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expenseType": "TOLL",
|
||||||
|
"projectName": "不存在记录",
|
||||||
|
"expenseDate": "2026-07-28",
|
||||||
|
"actualAmount": 50.00,
|
||||||
|
"paymentMethod": "CASH_PAID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 584091,
|
||||||
|
"msg": "其他支出不存在或不属于当前订单",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 已删除:批量确认其他收入
|
||||||
|
|
||||||
|
- **原方法 + 路径**:`POST /v3/admin/order/{orderId}/settlement/other-incomes/confirm`
|
||||||
|
- **原接口名**:批量确认其他收入
|
||||||
|
- **变更后**:接口已删除;新增/修改其他收入保存成功即确认。
|
||||||
|
- **请求体**:不再支持。旧请求体形如 `{"incomeIds":["99001"]}`,前端不要再发送。
|
||||||
|
- **响应**:不再返回原 `confirmedCount`;调用该路径按不存在接口处理。
|
||||||
|
|
||||||
|
**示例:删除后调用失败**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"incomeIds": ["99001"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"msg": "Not Found",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 已删除:确认其他支出
|
||||||
|
|
||||||
|
- **原方法 + 路径**:`POST /v3/admin/order/{orderId}/settlement/other-expenses/confirm`
|
||||||
|
- **原接口名**:确认其他支出
|
||||||
|
- **变更后**:接口已删除;新增/修改其他支出保存成功即确认。
|
||||||
|
- **请求体**:不再支持。旧接口无请求体。
|
||||||
|
- **响应**:不再返回 `Boolean`;调用该路径按不支持的方法处理。
|
||||||
|
|
||||||
|
**示例:删除后调用失败**
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 405,
|
||||||
|
"msg": "Method Not Allowed",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 接口入参
|
||||||
|
|
||||||
|
各接口入参已在 §3 按接口自包含列出。
|
||||||
|
|
||||||
|
## 5. 出参字段
|
||||||
|
|
||||||
|
各接口出参已在 §3 按接口自包含列出。核心变化是保存类接口的确认状态返回值变为已确认:
|
||||||
|
|
||||||
|
| 接口 | 字段 | 类型 | 当前返回 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| POST/PUT 其他收入 | `data.settlementConfirmStatus` | String | `CONFIRMED` |
|
||||||
|
| POST/PUT 其他收入 | `data.settlementConfirmStatusName` | String | `已确认` |
|
||||||
|
| POST/PUT 其他支出 | `data.settlementConfirmStatus` | String | `CONFIRMED` |
|
||||||
|
|
||||||
|
## 6. 枚举 / 数据字典
|
||||||
|
|
||||||
|
### 6.1 paymentMethod(付款类型)
|
||||||
|
|
||||||
|
**所属字段**:`paymentMethod` | **类型**:`String` | **必填**:是
|
||||||
|
|
||||||
|
| 值 | 中文 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| `CASH_PAID` | 现付 | 现场现金或线下现付 |
|
||||||
|
| `COMPANY_PAID` | 公司付款 | 公司承担或公司支付 |
|
||||||
|
| `SIGNED` | 签单 | 签单结算 |
|
||||||
|
|
||||||
|
### 6.2 settlementConfirmStatus(确认状态)
|
||||||
|
|
||||||
|
**所属字段**:`settlementConfirmStatus` | **类型**:`String` | **必填**:响应字段
|
||||||
|
|
||||||
|
| 值 | 中文 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| `UNCONFIRMED` | 未确认 | 历史存量状态;本次变更后新增/修改保存不再产生该状态 |
|
||||||
|
| `CONFIRMED` | 已确认 | 新增/修改保存成功后的状态 |
|
||||||
|
|
||||||
|
### 6.3 expenseType(其他支出类型)
|
||||||
|
|
||||||
|
**所属字段**:`expenseType` | **类型**:`String` | **必填**:是
|
||||||
|
|
||||||
|
| 值 | 中文 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| `FUEL` | 油费 | 车辆类支出 |
|
||||||
|
| `TOLL` | 过路费 | 车辆类支出 |
|
||||||
|
| `PARKING` | 停车费 | 车辆类支出 |
|
||||||
|
| `RENTAL` | 租车费 | 车辆类支出 |
|
||||||
|
| `MAINTENANCE` | 维修费 | 车辆类支出 |
|
||||||
|
| `OTHER` | 其他 | 非上述类型的其他支出 |
|
||||||
|
|
||||||
|
### 6.4 sourceType(其他收入来源类型)
|
||||||
|
|
||||||
|
**所属字段**:`sourceType` | **类型**:`String` | **必填**:响应字段
|
||||||
|
|
||||||
|
| 值 | 中文 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| `ORDER_SURCHARGE` | 订单增费 | 其他收入关联的订单增费来源 |
|
||||||
|
|
||||||
|
## 7. 错误码
|
||||||
|
|
||||||
|
| code | 含义 | 触发场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `400` | 参数校验失败 | 请求体格式错误、必填缺失、字段长度或格式非法 |
|
||||||
|
| `404` | 接口不存在 | 继续调用已删除的其他收入确认接口 |
|
||||||
|
| `405` | 方法不支持 | 继续调用已删除的其他支出确认接口 |
|
||||||
|
| `584073` | 其他收入不存在或不属于当前订单 | 修改其他收入时 `incomeId` 无效 |
|
||||||
|
| `584074` | 当前核单状态不允许修改其他收入 | 订单核单状态不是待核单或核单中 |
|
||||||
|
| `584075` | 其他收入关联的附加费来源无效 | 其他收入来源无效 |
|
||||||
|
| `584076` | 其他收入核算金额必须等于数量乘以核算单价 | 其他收入金额不一致 |
|
||||||
|
| `584087` | requestId 已用于另一笔其他收入 | 新增其他收入幂等键冲突 |
|
||||||
|
| `584089` | 核单或结算已完成,资金数据不可再修改 | 资金数据已锁定 |
|
||||||
|
| `584091` | 其他支出不存在或不属于当前订单 | 修改其他支出时 `settlementId` 无效 |
|
||||||
|
| `584095` | 其他支出字段超出允许范围 | 支出类型、项目名、金额或备注非法 |
|
||||||
|
| `584096` | 付款类型不合法 | `paymentMethod` 非法 |
|
||||||
|
| `584097` | 凭证 URL 格式或数量不合法 | 凭证 URL 非法 |
|
||||||
|
| `584098` | 餐食或其他支出存在未确认记录 | Step6 前仍有历史未确认餐食或其他支出 |
|
||||||
|
| `584307` | 当前核单状态不允许写入餐食或其他支出 | 订单核单状态不是待核单或核单中 |
|
||||||
|
|
||||||
|
## 8. 示例(3 组:典型 / 边界 / 异常)
|
||||||
|
|
||||||
|
典型成功、边界成功、业务失败示例已按接口内联在 §3.1 至 §3.6。
|
||||||
|
|
||||||
|
## 9. 业务边界
|
||||||
|
|
||||||
|
- **适用场景**:订单核单状态为待核单或核单中,且当前账号具备资金写入权限时,可以新增/修改其他收入和其他支出。
|
||||||
|
- **不适用场景**:核单或结算已完成后,不允许再保存资金数据。
|
||||||
|
- **特殊边界**:历史已存在的 `UNCONFIRMED` 其他收入/其他支出不会因为本次接口变更自动变为 `CONFIRMED`;需要前端对该行发起对应 `PUT` 保存,保存成功后才会返回 `CONFIRMED`。
|
||||||
|
- **删除接口边界**:不要再调用 `/other-incomes/confirm` 和 `/other-expenses/confirm`;保存类接口成功即可完成确认。
|
||||||
|
|
||||||
|
## 10. 修改前后对比
|
||||||
|
|
||||||
|
### 10.1 字段级对比
|
||||||
|
|
||||||
|
| 字段 | 改前 | 改后 |
|
||||||
|
|------|------|------|
|
||||||
|
| 其他收入 `settlementConfirmStatus` | POST/PUT 保存后返回 `UNCONFIRMED`,需再调确认接口变为 `CONFIRMED` | POST/PUT 保存成功直接返回 `CONFIRMED` |
|
||||||
|
| 其他收入 `settlementConfirmStatusName` | POST/PUT 保存后返回 `未确认` | POST/PUT 保存成功返回 `已确认` |
|
||||||
|
| 其他支出 `settlementConfirmStatus` | POST/PUT 保存后返回 `UNCONFIRMED`,需再调确认接口变为 `CONFIRMED` | POST/PUT 保存成功直接返回 `CONFIRMED` |
|
||||||
|
| 其他收入确认响应 `confirmedCount` | `POST /other-incomes/confirm` 返回确认数量 | 接口删除,不再返回 |
|
||||||
|
| 其他支出确认响应 `data` | `POST /other-expenses/confirm` 返回 `true` | 接口删除,不再返回 |
|
||||||
|
|
||||||
|
### 10.2 行为级对比
|
||||||
|
|
||||||
|
| 行为 | 改前 | 改后 |
|
||||||
|
|------|------|------|
|
||||||
|
| 新增其他收入 | 保存后仍是未确认,需要再调用确认接口 | 保存成功即确认 |
|
||||||
|
| 修改其他收入 | 修改后变为未确认,需要再调用确认接口 | 保存成功即确认 |
|
||||||
|
| 新增其他支出 | 保存后仍是未确认,需要再调用确认接口 | 保存成功即确认 |
|
||||||
|
| 修改其他支出 | 修改后变为未确认,需要再调用确认接口 | 保存成功即确认 |
|
||||||
|
| 存量未确认记录 | 可调用独立确认接口批量确认 | 需要逐条用 PUT 重存确认 |
|
||||||
|
| 独立确认按钮 | 调用确认接口 | 不再调用确认接口 |
|
||||||
|
|
||||||
|
## 11. 影响评估 / 回滚
|
||||||
|
|
||||||
|
### 11.1 影响评估
|
||||||
|
|
||||||
|
- **是否破坏向后兼容**:是。两个确认接口删除,继续调用会失败。
|
||||||
|
- **前端是否必须同步上线**:是。需要停止调用已删除确认接口,并以保存接口返回的 `settlementConfirmStatus` 作为确认结果。
|
||||||
|
- **影响已有数据**:历史 `UNCONFIRMED` 其他收入/其他支出不会自动确认;需要通过对应 PUT 保存后确认。
|
||||||
|
|
||||||
|
### 11.2 回滚方案
|
||||||
|
|
||||||
|
- **回滚方式**:如需恢复旧交互,回滚本次接口契约变更对应 PR。
|
||||||
|
- **回滚后清理**:无前端侧额外清理数据。
|
||||||
|
- **回滚耗时**:以后端发布节奏为准。
|
||||||
|
|
||||||
|
## 12. 注意事项
|
||||||
|
|
||||||
|
- 前端保存其他收入/其他支出成功后,不要再追加调用确认接口。
|
||||||
|
- 前端如有独立「确认其他收入」「确认其他支出」按钮或批量确认流程,需要改为保存即确认的交互。
|
||||||
|
- 前端如检测到历史 `UNCONFIRMED` 记录,需要提示用户重新保存该条记录;重存后响应会返回 `CONFIRMED`。
|
||||||
|
- 餐食费用确认接口 `POST /v3/admin/order/{orderId}/settlement/meals/confirm` 本次未删除,不属于本文变更范围。
|
||||||
|
|
||||||
|
## 13. 关联 / 联系人
|
||||||
|
|
||||||
|
### 13.1 链接
|
||||||
|
|
||||||
|
- **Issue**: [#5310](https://git.1814.love:8443/wx/HL/issues/5310)
|
||||||
|
- **PR**: [#5313](https://git.1814.love:8443/wx/HL/pulls/5313)
|
||||||
|
- **Merge commit**: [9b3af7c](https://git.1814.love:8443/wx/HL/commit/9b3af7c8587547fa7b1a0d4282bc4783dff98ea6)
|
||||||
|
|
||||||
|
### 13.2 联系人
|
||||||
|
|
||||||
|
- **后端负责人**: @yst
|
||||||
@@ -5,7 +5,11 @@ title: "车队独立管理及车队字典下线"
|
|||||||
consumer: "admin"
|
consumer: "admin"
|
||||||
backend: "verified"
|
backend: "verified"
|
||||||
gateway: "verified"
|
gateway: "verified"
|
||||||
frontend: "pending"
|
frontend: "implemented"
|
||||||
|
frontend_status: "implemented"
|
||||||
|
frontend_owner: "hl-ui-codex"
|
||||||
|
frontend_ref: "mmg/hl-ui@ac4d5fd6292b13eb504f5393dfe07781800b9233"
|
||||||
|
updated_at: "2026-07-25T01:18:23.686Z"
|
||||||
base: "dev-v3"
|
base: "dev-v3"
|
||||||
generated: "2026-07-22T10:46:00+08:00"
|
generated: "2026-07-22T10:46:00+08:00"
|
||||||
---
|
---
|
||||||
@@ -121,10 +125,30 @@ user-service 在“车务管理”目录下新增子菜单:
|
|||||||
|
|
||||||
## 前端必须修改的范围
|
## 前端必须修改的范围
|
||||||
|
|
||||||
|
### 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`,完成车队分页、新增、编辑、启停和删除:
|
1. 新增 `src/api/fleet/teams.js` 和 `src/views/fleet/teams/index.vue`,完成车队分页、新增、编辑、启停和删除:
|
||||||
- “车队管理”必须显示在“车务管理”目录内,不得作为一级菜单处理。
|
- “车队管理”必须显示在“车务管理”目录内,不得作为一级菜单处理。
|
||||||
|
- 按上面的展示矩阵修正表格列宽和明暗主题样式,不能让“车队名称”列挤占联系人信息区域。
|
||||||
- 仅 `vehicleCount === 0` 时展示/启用删除动作;调用删除接口后刷新列表。
|
- 仅 `vehicleCount === 0` 时展示/启用删除动作;调用删除接口后刷新列表。
|
||||||
- 后端仍会独立校验车辆及未完结司机录入关联,返回 `601107` 时提示“请先完成车辆/司机转移”。
|
- 后端仍会独立校验车辆及未完结司机录入关联,返回 `601107` 时提示“请先完成车辆/司机转移”。
|
||||||
- 编辑历史迁入车队时补齐负责人、负责人电话、付款方式和排序。
|
- 编辑历史迁入车队时补齐负责人、负责人电话、付款方式和排序。
|
||||||
@@ -189,6 +213,8 @@ DELETE FROM sys_dict_type WHERE dict_type = 'fleet_attribution';
|
|||||||
## 验收清单
|
## 验收清单
|
||||||
|
|
||||||
- [ ] 独立车队菜单可分页、新增、编辑、启停,付款方式与资源页选项一致。
|
- [ ] 独立车队菜单可分页、新增、编辑、启停,付款方式与资源页选项一致。
|
||||||
|
- [ ] `/fleet/teams` 在桌面端不再由“车队名称”列制造大段空白,负责人和脱敏电话位于首屏连续阅读区;窄屏按展示矩阵滚动而不是隐藏或挤压关键列。
|
||||||
|
- [ ] `/fleet/teams` 的浅色、暗色模式均使用主题 token,筛选区、表头、数据行、空值、状态标签和操作区在默认/悬停/聚焦/禁用状态下层级清晰。
|
||||||
- [ ] “车队管理”位于“车务管理”目录下;空车队可删除,非空车队删除入口禁用或明确提示后端 `601107`。
|
- [ ] “车队管理”位于“车务管理”目录下;空车队可删除,非空车队删除入口禁用或明确提示后端 `601107`。
|
||||||
- [ ] 历史迁入车队可通过编辑补齐负责人、负责人电话、付款方式和排序,保存时不允许提交空资料。
|
- [ ] 历史迁入车队可通过编辑补齐负责人、负责人电话、付款方式和排序,保存时不允许提交空资料。
|
||||||
- [ ] 车辆新增/编辑/筛选/详情/导入均使用动态车队,不再出现固定三项。
|
- [ ] 车辆新增/编辑/筛选/详情/导入均使用动态车队,不再出现固定三项。
|
||||||
|
|||||||
@@ -135,6 +135,17 @@ POST /admin/fleet/assignments/{assignmentId}/confirm
|
|||||||
- 等待态:“等待司机回复确认”
|
- 等待态:“等待司机回复确认”
|
||||||
- 已登记态:“司机已确认接单,待车务确认执行”
|
- 已登记态:“司机已确认接单,待车务确认执行”
|
||||||
|
|
||||||
|
### 2026-07-24 界面验收补充
|
||||||
|
|
||||||
|
当前“待确认”步骤中,“司机待确认通知 / 模板与预览均来自后端”标题区下方存在明显的
|
||||||
|
大块空白,导致模板选择行和消息预览整体下移。模板标签及消息正文已经正常显示,因此
|
||||||
|
这是前端布局问题,不是后端模板或渲染接口缺少数据。
|
||||||
|
|
||||||
|
- 移除标题区不必要的固定高度、最小高度或空占位,让高度由标题和副标题内容自然撑开。
|
||||||
|
- 标题区与模板选择行保持正常紧凑间距,不要为未来内容预留不可见空白。
|
||||||
|
- 常用桌面分辨率下,标题区底部到模板选择行的垂直空白不应超过 16px。
|
||||||
|
- 本项不新增接口、不调整字段,也不要为修复布局重新维护前端本地模板。
|
||||||
|
|
||||||
## 前端处理清单
|
## 前端处理清单
|
||||||
|
|
||||||
- [ ] 模板列表来自后端 `hold_notify` 模板,默认选中 `isDefault=true`,不再使用前端假模板。
|
- [ ] 模板列表来自后端 `hold_notify` 模板,默认选中 `isDefault=true`,不再使用前端假模板。
|
||||||
@@ -145,6 +156,7 @@ POST /admin/fleet/assignments/{assignmentId}/confirm
|
|||||||
- [ ] 无凭证时仍可成功登记司机确认并执行最终确认。
|
- [ ] 无凭证时仍可成功登记司机确认并执行最终确认。
|
||||||
- [ ] `605025` 时提示“请先登记司机已确认接单”,不提示“缺少凭证”。
|
- [ ] `605025` 时提示“请先登记司机已确认接单”,不提示“缺少凭证”。
|
||||||
- [ ] 页面刷新后能按后端状态恢复待回复/已确认阶段。
|
- [ ] 页面刷新后能按后端状态恢复待回复/已确认阶段。
|
||||||
|
- [ ] 修复“司机待确认通知”标题区异常留白,模板选择与消息预览紧凑衔接。
|
||||||
|
|
||||||
## 验证证据
|
## 验证证据
|
||||||
|
|
||||||
|
|||||||
@@ -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,100 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5254"
|
||||||
|
title: "订单侧已配置车辆补充车型车队服务日期与日单价"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "pending"
|
||||||
|
frontend_owner: ""
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "后端已合入 dev-v3 并部署测试环境;网关已验证新增字段、多车守恒和手机号脱敏。"
|
||||||
|
updated_at: "2026-07-26"
|
||||||
|
base: "dev-v3"
|
||||||
|
generated: "2026-07-26T09:57:21+08:00"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 订单侧已配置车辆补充车型车队服务日期与日单价
|
||||||
|
|
||||||
|
订单详情的已配置车辆补齐车型标题、车队、连续服务日期、服务天数和协议日单价。
|
||||||
|
本记录只表示后端契约交接,`frontend_status` 在真实前端领取前保持 `pending`。
|
||||||
|
|
||||||
|
## 关联
|
||||||
|
|
||||||
|
- Issue: #5254
|
||||||
|
- PR: wx/HL#5260
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 来源 |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/v3/admin/order/:id/itinerary`(`:id` 为订单 ID) | `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`(通过)。
|
||||||
|
- 联合验证:`mvn -pl hl-fleet-service,hl-order-service-v3 -am verify`
|
||||||
|
(BUILD SUCCESS;Fleet 2387 项通过,Spotless 通过;order-v3 定向 81 项通过)。
|
||||||
|
- 测试环境:`hl-fleet-service`、`hl-order-service-v3` 已从 `dev-v3`
|
||||||
|
滚动部署,两实例均健康。
|
||||||
|
- 网关验证:连续 6 次调用 `GET /v3/admin/order/:id/itinerary` 均返回 3 个唯一车辆连续段;
|
||||||
|
新增车型、车队、服务日期、服务天数和计划日单价字段存在,手机号保持脱敏且未返回原始手机号。
|
||||||
|
- 降级路径:Fleet 调用失败或空响应由 `OrderDetailServiceTest` 覆盖,本次未人为中断测试环境 Fleet。
|
||||||
|
- 兼容性结论:仅新增可空响应字段并补齐既有空字段;内部 Feign JSON 双向兼容,
|
||||||
|
不修改方法、路径、参数、必填项、枚举或错误码。
|
||||||
+5
-2
@@ -3,9 +3,12 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test tests/validate-changelog-filenames.test.mjs tests/validate-changelog-frontmatter.test.mjs",
|
"test": "node --test tests/validate-changelog-filenames.test.mjs tests/validate-changelog-frontmatter.test.mjs tests/changelog-path-aliases.test.mjs",
|
||||||
"check:filenames": "node scripts/validate-changelog-filenames.mjs",
|
"check:filenames": "node scripts/validate-changelog-filenames.mjs",
|
||||||
"check:frontmatter": "node scripts/validate-changelog-frontmatter.mjs"
|
"check:frontmatter": "node scripts/validate-changelog-frontmatter.mjs",
|
||||||
|
"check:path-aliases": "node scripts/changelog-path-aliases.mjs --check",
|
||||||
|
"changelog:resolve": "node scripts/changelog-path-aliases.mjs --resolve",
|
||||||
|
"changelog:transition": "node scripts/changelog-path-aliases.mjs --transition"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import {
|
||||||
|
existsSync,
|
||||||
|
readFileSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseFrontmatter,
|
||||||
|
validateFrontendState,
|
||||||
|
validateFrontendTransition,
|
||||||
|
} from './validate-changelog-frontmatter.mjs';
|
||||||
|
|
||||||
|
export const PATH_ALIAS_MANIFEST = 'changelog-path-aliases.json';
|
||||||
|
export const PATH_ALIAS_SCHEMA = 'hl-changelog-path-aliases/v1';
|
||||||
|
|
||||||
|
const CONTROLLED_ROOTS = new Set(['changelogs-v2', 'changelogs-v2-mp']);
|
||||||
|
const FRONTEND_FIELDS = [
|
||||||
|
'frontend_status',
|
||||||
|
'frontend_owner',
|
||||||
|
'frontend_ref',
|
||||||
|
'target_release',
|
||||||
|
'verified_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
function aliasError(code, inputPath, message) {
|
||||||
|
return { code, path: inputPath, message };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeRepoPath(value, label = 'path') {
|
||||||
|
if (typeof value !== 'string' || value.length === 0 || /[\0\r\n]/.test(value)) {
|
||||||
|
throw new Error(`${label} 必须是非空且不含控制字符的 Git 路径`);
|
||||||
|
}
|
||||||
|
if (value.includes('\\') || path.posix.isAbsolute(value) || /^[A-Za-z]:/.test(value)) {
|
||||||
|
throw new Error(`${label} 必须使用仓库内 POSIX 相对路径`);
|
||||||
|
}
|
||||||
|
const normalized = path.posix.normalize(value);
|
||||||
|
if (normalized !== value || normalized === '.' || normalized.startsWith('../')) {
|
||||||
|
throw new Error(`${label} 不能包含 .、.. 或非规范路径片段`);
|
||||||
|
}
|
||||||
|
const [root] = normalized.split('/');
|
||||||
|
if (!CONTROLLED_ROOTS.has(root) || !normalized.endsWith('.md')) {
|
||||||
|
throw new Error(`${label} 必须指向受控 changelog Markdown`);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePathAliasManifest(text) {
|
||||||
|
let value;
|
||||||
|
try {
|
||||||
|
value = JSON.parse(String(text));
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`alias manifest 不是合法 JSON: ${error.message}`);
|
||||||
|
}
|
||||||
|
if (value?.schema !== PATH_ALIAS_SCHEMA || !Array.isArray(value.aliases)) {
|
||||||
|
throw new Error(`alias manifest 必须使用 ${PATH_ALIAS_SCHEMA} 且包含 aliases 数组`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seenAliases = new Set();
|
||||||
|
const aliases = value.aliases.map((entry, index) => {
|
||||||
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
||||||
|
throw new Error(`aliases[${index}] 必须是对象`);
|
||||||
|
}
|
||||||
|
const ticket = String(entry.ticket ?? '');
|
||||||
|
if (!/^[1-9]\d*$/.test(ticket)) {
|
||||||
|
throw new Error(`aliases[${index}].ticket 必须是正整数`);
|
||||||
|
}
|
||||||
|
const alias = normalizeRepoPath(entry.alias, `aliases[${index}].alias`);
|
||||||
|
const canonical = normalizeRepoPath(entry.canonical, `aliases[${index}].canonical`);
|
||||||
|
if (alias === canonical) {
|
||||||
|
throw new Error(`aliases[${index}] 的 alias 与 canonical 不能相同`);
|
||||||
|
}
|
||||||
|
if (seenAliases.has(alias)) {
|
||||||
|
throw new Error(`alias 重复: ${alias}`);
|
||||||
|
}
|
||||||
|
if (typeof entry.reason !== 'string' || entry.reason.trim() === '') {
|
||||||
|
throw new Error(`aliases[${index}].reason 不能为空`);
|
||||||
|
}
|
||||||
|
seenAliases.add(alias);
|
||||||
|
return { ticket, alias, canonical, reason: entry.reason.trim() };
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entry of aliases) {
|
||||||
|
if (seenAliases.has(entry.canonical)) {
|
||||||
|
throw new Error(`暂不允许 alias 链或环: ${entry.alias} -> ${entry.canonical}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { schema: PATH_ALIAS_SCHEMA, aliases };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPathAliasManifest(root = process.cwd()) {
|
||||||
|
const manifestPath = path.join(root, PATH_ALIAS_MANIFEST);
|
||||||
|
if (!existsSync(manifestPath)) {
|
||||||
|
return { schema: PATH_ALIAS_SCHEMA, aliases: [] };
|
||||||
|
}
|
||||||
|
return parsePathAliasManifest(readFileSync(manifestPath, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveChangelogPath(inputPath, manifest) {
|
||||||
|
const normalized = normalizeRepoPath(inputPath);
|
||||||
|
return manifest.aliases.find(({ alias }) => alias === normalized)?.canonical ?? normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function absoluteRepoPath(root, repoPath) {
|
||||||
|
return path.join(root, ...repoPath.split('/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDocument(root, repoPath) {
|
||||||
|
const absolute = absoluteRepoPath(root, repoPath);
|
||||||
|
if (!existsSync(absolute)) {
|
||||||
|
return { error: aliasError('E_ALIAS_MISSING', repoPath, '文件不存在') };
|
||||||
|
}
|
||||||
|
const content = readFileSync(absolute, 'utf8');
|
||||||
|
const { metadata } = parseFrontmatter(content);
|
||||||
|
if (!metadata) {
|
||||||
|
return { error: aliasError('E_ALIAS_FRONTMATTER', repoPath, '缺少 YAML Front Matter') };
|
||||||
|
}
|
||||||
|
return { absolute, content, metadata };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validatePathAliases(root = process.cwd(), manifest = loadPathAliasManifest(root)) {
|
||||||
|
const errors = [];
|
||||||
|
for (const entry of manifest.aliases) {
|
||||||
|
const aliasDocument = readDocument(root, entry.alias);
|
||||||
|
const canonicalDocument = readDocument(root, entry.canonical);
|
||||||
|
if (aliasDocument.error) {
|
||||||
|
errors.push(aliasDocument.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (canonicalDocument.error) {
|
||||||
|
errors.push(canonicalDocument.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [repoPath, metadata] of [
|
||||||
|
[entry.alias, aliasDocument.metadata],
|
||||||
|
[entry.canonical, canonicalDocument.metadata],
|
||||||
|
]) {
|
||||||
|
if (metadata.schema !== 'hl-changelog/v2') {
|
||||||
|
errors.push(aliasError('E_ALIAS_SCHEMA', repoPath, '兼容组文档必须使用 hl-changelog/v2'));
|
||||||
|
}
|
||||||
|
if (metadata.ticket !== entry.ticket) {
|
||||||
|
errors.push(aliasError(
|
||||||
|
'E_ALIAS_TICKET',
|
||||||
|
repoPath,
|
||||||
|
`ticket=${metadata.ticket || '(空)'},manifest ticket=${entry.ticket}`,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (aliasDocument.metadata.canonical_path !== entry.canonical) {
|
||||||
|
errors.push(aliasError(
|
||||||
|
'E_ALIAS_TARGET',
|
||||||
|
entry.alias,
|
||||||
|
`canonical_path 必须等于 ${entry.canonical}`,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for (const field of FRONTEND_FIELDS) {
|
||||||
|
if ((aliasDocument.metadata[field] ?? '') !== (canonicalDocument.metadata[field] ?? '')) {
|
||||||
|
errors.push(aliasError(
|
||||||
|
'E_ALIAS_STATE',
|
||||||
|
entry.alias,
|
||||||
|
`${field} 与 canonical 不一致`,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { checkedCount: manifest.aliases.length, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
function shanghaiDate(now = new Date()) {
|
||||||
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: 'Asia/Shanghai',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
}).formatToParts(now);
|
||||||
|
const values = Object.fromEntries(parts.map(({ type, value }) => [type, value]));
|
||||||
|
return `${values.year}-${values.month}-${values.day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceFrontmatterFields(content, fields) {
|
||||||
|
const normalized = String(content).replaceAll('\r\n', '\n');
|
||||||
|
const end = normalized.indexOf('\n---\n', 4);
|
||||||
|
if (!normalized.startsWith('---\n') || end < 0) {
|
||||||
|
throw new Error('文档缺少可写的 YAML Front Matter');
|
||||||
|
}
|
||||||
|
const lines = normalized.slice(4, end).split('\n');
|
||||||
|
const remaining = new Set(Object.keys(fields));
|
||||||
|
const updated = lines.map((line) => {
|
||||||
|
const separator = line.indexOf(':');
|
||||||
|
if (separator < 0) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
const key = line.slice(0, separator).trim();
|
||||||
|
if (!remaining.has(key)) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
remaining.delete(key);
|
||||||
|
return `${key}: ${JSON.stringify(String(fields[key] ?? ''))}`;
|
||||||
|
});
|
||||||
|
if (remaining.size > 0) {
|
||||||
|
throw new Error(`frontmatter 缺少待更新字段: ${[...remaining].join(', ')}`);
|
||||||
|
}
|
||||||
|
return `---\n${updated.join('\n')}\n---\n${normalized.slice(end + 5)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transitionAliasedFrontendState(
|
||||||
|
root,
|
||||||
|
inputPath,
|
||||||
|
{
|
||||||
|
status,
|
||||||
|
owner,
|
||||||
|
frontendRef,
|
||||||
|
targetRelease,
|
||||||
|
verifiedAt,
|
||||||
|
reason = '',
|
||||||
|
},
|
||||||
|
{ now = new Date(), write = false } = {},
|
||||||
|
) {
|
||||||
|
const manifest = loadPathAliasManifest(root);
|
||||||
|
const before = validatePathAliases(root, manifest);
|
||||||
|
if (before.errors.length > 0) {
|
||||||
|
throw new Error(`alias manifest 校验失败: ${before.errors.map(({ message }) => message).join(';')}`);
|
||||||
|
}
|
||||||
|
const canonical = resolveChangelogPath(inputPath, manifest);
|
||||||
|
const canonicalDocument = readDocument(root, canonical);
|
||||||
|
if (canonicalDocument.error) {
|
||||||
|
throw new Error(`${canonicalDocument.error.path}: ${canonicalDocument.error.message}`);
|
||||||
|
}
|
||||||
|
const current = canonicalDocument.metadata.frontend_status;
|
||||||
|
const transitionErrors = validateFrontendTransition(current, status, reason);
|
||||||
|
if (transitionErrors.length > 0) {
|
||||||
|
throw new Error(transitionErrors.join(';'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
frontend_status: status,
|
||||||
|
frontend_owner: owner ?? canonicalDocument.metadata.frontend_owner ?? '',
|
||||||
|
frontend_ref: frontendRef ?? canonicalDocument.metadata.frontend_ref ?? '',
|
||||||
|
target_release: targetRelease ?? canonicalDocument.metadata.target_release ?? '',
|
||||||
|
verified_at: verifiedAt ?? canonicalDocument.metadata.verified_at ?? '',
|
||||||
|
updated_at: shanghaiDate(now),
|
||||||
|
};
|
||||||
|
const stateErrors = validateFrontendState(fields);
|
||||||
|
if (stateErrors.length > 0) {
|
||||||
|
throw new Error(stateErrors.join(';'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = [
|
||||||
|
canonical,
|
||||||
|
...manifest.aliases
|
||||||
|
.filter((entry) => entry.canonical === canonical)
|
||||||
|
.map((entry) => entry.alias),
|
||||||
|
];
|
||||||
|
const outputs = group.map((repoPath) => {
|
||||||
|
const document = readDocument(root, repoPath);
|
||||||
|
if (document.error) {
|
||||||
|
throw new Error(`${document.error.path}: ${document.error.message}`);
|
||||||
|
}
|
||||||
|
const content = replaceFrontmatterFields(document.content, fields);
|
||||||
|
return {
|
||||||
|
repoPath,
|
||||||
|
absolute: document.absolute,
|
||||||
|
content,
|
||||||
|
changed: content !== document.content.replaceAll('\r\n', '\n'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (write) {
|
||||||
|
for (const output of outputs.filter(({ changed }) => changed)) {
|
||||||
|
writeFileSync(output.absolute, output.content, 'utf8');
|
||||||
|
}
|
||||||
|
const after = validatePathAliases(root, manifest);
|
||||||
|
if (after.errors.length > 0) {
|
||||||
|
throw new Error(`写入后 alias 状态不一致: ${after.errors.map(({ message }) => message).join(';')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
canonical,
|
||||||
|
paths: group,
|
||||||
|
changedPaths: outputs.filter(({ changed }) => changed).map(({ repoPath }) => repoPath),
|
||||||
|
metadata: fields,
|
||||||
|
write,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCli(argv) {
|
||||||
|
if (argv.length === 0 || (argv.length === 1 && argv[0] === '--check')) {
|
||||||
|
return { command: 'check' };
|
||||||
|
}
|
||||||
|
if (argv[0] === '--resolve' && argv.length === 2) {
|
||||||
|
return { command: 'resolve', path: argv[1] };
|
||||||
|
}
|
||||||
|
if (argv[0] !== '--transition') {
|
||||||
|
throw new Error('用法: --check | --resolve <path> | --transition <path> <status> [options]');
|
||||||
|
}
|
||||||
|
if (argv.length < 3) {
|
||||||
|
throw new Error('--transition 需要 path 和 status');
|
||||||
|
}
|
||||||
|
const options = { command: 'transition', path: argv[1], status: argv[2], write: false };
|
||||||
|
const mapping = new Map([
|
||||||
|
['--owner', 'owner'],
|
||||||
|
['--frontend-ref', 'frontendRef'],
|
||||||
|
['--target-release', 'targetRelease'],
|
||||||
|
['--verified-at', 'verifiedAt'],
|
||||||
|
['--reason', 'reason'],
|
||||||
|
]);
|
||||||
|
for (let index = 3; index < argv.length; index += 1) {
|
||||||
|
const flag = argv[index];
|
||||||
|
if (flag === '--write') {
|
||||||
|
options.write = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = mapping.get(flag);
|
||||||
|
const value = argv[index + 1];
|
||||||
|
if (!key || value === undefined) {
|
||||||
|
throw new Error(`不支持或不完整的参数: ${flag}`);
|
||||||
|
}
|
||||||
|
options[key] = value;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function main(argv = process.argv.slice(2), root = process.cwd()) {
|
||||||
|
try {
|
||||||
|
const options = parseCli(argv);
|
||||||
|
const manifest = loadPathAliasManifest(root);
|
||||||
|
if (options.command === 'check') {
|
||||||
|
const result = validatePathAliases(root, manifest);
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
for (const error of result.errors) {
|
||||||
|
console.error(`[${error.code}] ${error.path}: ${error.message}`);
|
||||||
|
}
|
||||||
|
console.error(`FAIL: ${result.errors.length} path alias error(s).`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
console.log(`PASS: validated ${result.checkedCount} changelog path alias(es).`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (options.command === 'resolve') {
|
||||||
|
console.log(resolveChangelogPath(options.path, manifest));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const result = transitionAliasedFrontendState(root, options.path, options, {
|
||||||
|
write: options.write,
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
return 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`ERROR: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCli = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
|
||||||
|
if (isCli) {
|
||||||
|
process.exitCode = main();
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ const CONTROLLED_ROOTS = new Map([
|
|||||||
['changelogs-v2-mp', '小程序端'],
|
['changelogs-v2-mp', '小程序端'],
|
||||||
]);
|
]);
|
||||||
const CHANGE_TYPES = new Set(['新增接口', '修改接口', '删除接口']);
|
const CHANGE_TYPES = new Set(['新增接口', '修改接口', '删除接口']);
|
||||||
|
const PATH_ALIAS_MANIFEST = 'changelog-path-aliases.json';
|
||||||
|
|
||||||
function ruleError(code, path, message, expected) {
|
function ruleError(code, path, message, expected) {
|
||||||
return { code, path, message, expected };
|
return { code, path, message, expected };
|
||||||
@@ -76,6 +77,40 @@ export function collectNewTargetPaths(records) {
|
|||||||
.map(({ targetPath }) => targetPath);
|
.map(({ targetPath }) => targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pathAliasMapFromManifest(value) {
|
||||||
|
if (value?.schema !== 'hl-changelog-path-aliases/v1' || !Array.isArray(value.aliases)) {
|
||||||
|
throw new Error('changelog-path-aliases.json schema 或 aliases 非法');
|
||||||
|
}
|
||||||
|
const aliases = new Map();
|
||||||
|
for (const [index, entry] of value.aliases.entries()) {
|
||||||
|
if (
|
||||||
|
!entry
|
||||||
|
|| typeof entry.alias !== 'string'
|
||||||
|
|| typeof entry.canonical !== 'string'
|
||||||
|
|| entry.alias.length === 0
|
||||||
|
|| entry.canonical.length === 0
|
||||||
|
) {
|
||||||
|
throw new Error(`changelog-path-aliases.json aliases[${index}] 缺少 alias/canonical`);
|
||||||
|
}
|
||||||
|
if (aliases.has(entry.alias)) {
|
||||||
|
throw new Error(`changelog-path-aliases.json alias 重复: ${entry.alias}`);
|
||||||
|
}
|
||||||
|
aliases.set(entry.alias, entry.canonical);
|
||||||
|
}
|
||||||
|
return aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadPathAliasMap(root = process.cwd()) {
|
||||||
|
try {
|
||||||
|
return pathAliasMapFromManifest(JSON.parse(readFileSync(`${root}/${PATH_ALIAS_MANIFEST}`, 'utf8')));
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === 'ENOENT') {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function controlledRootForPath(inputPath) {
|
export function controlledRootForPath(inputPath) {
|
||||||
const candidate = String(inputPath);
|
const candidate = String(inputPath);
|
||||||
for (const root of CONTROLLED_ROOTS.keys()) {
|
for (const root of CONTROLLED_ROOTS.keys()) {
|
||||||
@@ -178,10 +213,32 @@ export function validateChangelogPath(inputPath, now = new Date()) {
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runValidation(records, now = new Date()) {
|
export function runValidation(records, now = new Date(), { pathAliases = new Map() } = {}) {
|
||||||
const targetPaths = collectNewTargetPaths(records);
|
const targetPaths = collectNewTargetPaths(records);
|
||||||
const controlledPaths = targetPaths.filter((targetPath) => controlledRootForPath(targetPath));
|
const controlledPaths = targetPaths.filter((targetPath) => controlledRootForPath(targetPath));
|
||||||
const errors = controlledPaths.flatMap((targetPath) => validateChangelogPath(targetPath, now));
|
const errors = controlledPaths.flatMap((targetPath) => (
|
||||||
|
pathAliases.has(targetPath) ? [] : validateChangelogPath(targetPath, now)
|
||||||
|
));
|
||||||
|
for (const record of records) {
|
||||||
|
const sourcePath = /^R\d{1,3}$/.test(record.status)
|
||||||
|
? record.sourcePath
|
||||||
|
: record.status === 'D' ? record.targetPath : undefined;
|
||||||
|
if (!sourcePath || !controlledRootForPath(sourcePath) || !sourcePath.endsWith('.md')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const expectedTarget = pathAliases.get(sourcePath);
|
||||||
|
const actualTarget = /^R\d{1,3}$/.test(record.status) ? record.targetPath : undefined;
|
||||||
|
if (!expectedTarget || (actualTarget && expectedTarget !== actualTarget)) {
|
||||||
|
errors.push(ruleError(
|
||||||
|
'E_PATH_STABILITY',
|
||||||
|
sourcePath,
|
||||||
|
'已发布 changelog 不得在缺少精确 alias 关系时删除或重命名',
|
||||||
|
actualTarget
|
||||||
|
? `在 ${PATH_ALIAS_MANIFEST} 登记 ${sourcePath} -> ${actualTarget} 并保留兼容入口`
|
||||||
|
: `在 ${PATH_ALIAS_MANIFEST} 登记迁移关系并保留兼容入口`,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
targetCount: targetPaths.length,
|
targetCount: targetPaths.length,
|
||||||
checkedCount: controlledPaths.length,
|
checkedCount: controlledPaths.length,
|
||||||
@@ -294,7 +351,9 @@ export function main(argv = process.argv.slice(2)) {
|
|||||||
try {
|
try {
|
||||||
const options = parseArguments(argv);
|
const options = parseArguments(argv);
|
||||||
const records = parseNameStatusZ(diffFromOptions(options));
|
const records = parseNameStatusZ(diffFromOptions(options));
|
||||||
const result = runValidation(records, new Date());
|
const result = runValidation(records, new Date(), {
|
||||||
|
pathAliases: loadPathAliasMap(),
|
||||||
|
});
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
for (const error of result.errors) {
|
for (const error of result.errors) {
|
||||||
console.error(`[${error.code}] ${error.path}: ${error.message}; expected ${error.expected}`);
|
console.error(`[${error.code}] ${error.path}: ${error.message}; expected ${error.expected}`);
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
loadPathAliasManifest,
|
||||||
|
parsePathAliasManifest,
|
||||||
|
resolveChangelogPath,
|
||||||
|
transitionAliasedFrontendState,
|
||||||
|
validatePathAliases,
|
||||||
|
} from '../scripts/changelog-path-aliases.mjs';
|
||||||
|
import { parseFrontmatter } from '../scripts/validate-changelog-frontmatter.mjs';
|
||||||
|
|
||||||
|
const OLD_5216 = 'changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-前端待处理-管理后台.md';
|
||||||
|
const CURRENT_5216 = 'changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md';
|
||||||
|
|
||||||
|
function document({ canonicalPath = '', status = 'claimed', owner = 'frontend-team', frontendRef = '' } = {}) {
|
||||||
|
const canonicalLine = canonicalPath ? `canonical_path: "${canonicalPath}"\n` : '';
|
||||||
|
return `---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "5216"
|
||||||
|
title: "路径兼容测试"
|
||||||
|
consumer: "admin"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "verified"
|
||||||
|
frontend_status: "${status}"
|
||||||
|
frontend_owner: "${owner}"
|
||||||
|
frontend_ref: "${frontendRef}"
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
${canonicalLine}updated_at: "2026-07-26"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 路径兼容测试
|
||||||
|
|
||||||
|
## 变更接口
|
||||||
|
|
||||||
|
- 无业务接口变化。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
- 自动化测试。
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFixture() {
|
||||||
|
const root = mkdtempSync(path.join(tmpdir(), 'hl-changelog-alias-'));
|
||||||
|
const manifest = {
|
||||||
|
schema: 'hl-changelog-path-aliases/v1',
|
||||||
|
aliases: [{
|
||||||
|
ticket: '5216',
|
||||||
|
alias: OLD_5216,
|
||||||
|
canonical: CURRENT_5216,
|
||||||
|
reason: '测试旧路径兼容',
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
writeFileSync(path.join(root, 'changelog-path-aliases.json'), JSON.stringify(manifest, null, 2));
|
||||||
|
for (const repoPath of [OLD_5216, CURRENT_5216]) {
|
||||||
|
const absolute = path.join(root, ...repoPath.split('/'));
|
||||||
|
mkdirSync(path.dirname(absolute), { recursive: true });
|
||||||
|
writeFileSync(absolute, document({
|
||||||
|
canonicalPath: repoPath === OLD_5216 ? CURRENT_5216 : '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('repository manifest has no dangling aliases after illegal legacy paths are removed', () => {
|
||||||
|
const manifest = loadPathAliasManifest();
|
||||||
|
assert.equal(resolveChangelogPath(OLD_5216, manifest), OLD_5216);
|
||||||
|
assert.equal(resolveChangelogPath(CURRENT_5216, manifest), CURRENT_5216);
|
||||||
|
assert.deepEqual(validatePathAliases().errors, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects unsafe paths, duplicate aliases and alias chains', () => {
|
||||||
|
assert.throws(() => parsePathAliasManifest(JSON.stringify({
|
||||||
|
schema: 'hl-changelog-path-aliases/v1',
|
||||||
|
aliases: [{
|
||||||
|
ticket: '5216',
|
||||||
|
alias: '../outside.md',
|
||||||
|
canonical: CURRENT_5216,
|
||||||
|
reason: 'unsafe',
|
||||||
|
}],
|
||||||
|
})), /POSIX|规范路径/);
|
||||||
|
assert.throws(() => parsePathAliasManifest(JSON.stringify({
|
||||||
|
schema: 'hl-changelog-path-aliases/v1',
|
||||||
|
aliases: [
|
||||||
|
{ ticket: '5216', alias: OLD_5216, canonical: CURRENT_5216, reason: 'one' },
|
||||||
|
{ ticket: '5216', alias: OLD_5216, canonical: CURRENT_5216, reason: 'two' },
|
||||||
|
],
|
||||||
|
})), /重复/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('frontend status transition updates alias group once and is idempotent', () => {
|
||||||
|
const root = createFixture();
|
||||||
|
try {
|
||||||
|
const first = transitionAliasedFrontendState(root, OLD_5216, {
|
||||||
|
status: 'implemented',
|
||||||
|
owner: 'frontend-team',
|
||||||
|
frontendRef: 'mmg/hl-ui@abc1234',
|
||||||
|
}, {
|
||||||
|
now: new Date('2026-07-26T04:00:00Z'),
|
||||||
|
write: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(new Set(first.changedPaths), new Set([OLD_5216, CURRENT_5216]));
|
||||||
|
assert.deepEqual(validatePathAliases(root).errors, []);
|
||||||
|
|
||||||
|
for (const repoPath of [OLD_5216, CURRENT_5216]) {
|
||||||
|
const text = readFileSync(path.join(root, ...repoPath.split('/')), 'utf8');
|
||||||
|
const { metadata } = parseFrontmatter(text);
|
||||||
|
assert.equal(metadata.frontend_status, 'implemented');
|
||||||
|
assert.equal(metadata.frontend_owner, 'frontend-team');
|
||||||
|
assert.equal(metadata.frontend_ref, 'mmg/hl-ui@abc1234');
|
||||||
|
}
|
||||||
|
|
||||||
|
const second = transitionAliasedFrontendState(root, OLD_5216, {
|
||||||
|
status: 'implemented',
|
||||||
|
owner: 'frontend-team',
|
||||||
|
frontendRef: 'mmg/hl-ui@abc1234',
|
||||||
|
}, {
|
||||||
|
now: new Date('2026-07-26T04:00:00Z'),
|
||||||
|
write: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(second.changedPaths, []);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
collectNewTargetPaths,
|
collectNewTargetPaths,
|
||||||
getShanghaiDate,
|
getShanghaiDate,
|
||||||
parseNameStatusZ,
|
parseNameStatusZ,
|
||||||
|
pathAliasMapFromManifest,
|
||||||
runValidation,
|
runValidation,
|
||||||
validateChangelogPath,
|
validateChangelogPath,
|
||||||
} from '../scripts/validate-changelog-filenames.mjs';
|
} from '../scripts/validate-changelog-filenames.mjs';
|
||||||
@@ -114,13 +115,19 @@ test('parses NUL-delimited Chinese and space-containing paths', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('exempts modified and deleted historical bad paths', () => {
|
test('exempts a modified historical bad path but protects a deleted published path', () => {
|
||||||
const records = parseNameStatusZ(Buffer.from(
|
const modified = parseNameStatusZ(Buffer.from(
|
||||||
'M\0changelogs-v2/2026-07/63_历史坏文件.md\0D\0changelogs-v2/2026-07/99_无issue.md\0',
|
'M\0changelogs-v2/2026-07/63_历史坏文件.md\0',
|
||||||
'utf8',
|
'utf8',
|
||||||
));
|
));
|
||||||
assert.deepEqual(collectNewTargetPaths(records), []);
|
assert.deepEqual(collectNewTargetPaths(modified), []);
|
||||||
assert.equal(runValidation(records, SHANGHAI_NOW).errors.length, 0);
|
assert.equal(runValidation(modified, SHANGHAI_NOW).errors.length, 0);
|
||||||
|
|
||||||
|
const deleted = parseNameStatusZ(Buffer.from(
|
||||||
|
'D\0changelogs-v2/2026-07/99_无issue.md\0',
|
||||||
|
'utf8',
|
||||||
|
));
|
||||||
|
assert.ok(runValidation(deleted, SHANGHAI_NOW).errors.some(({ code }) => code === 'E_PATH_STABILITY'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('validates only a copy target path', () => {
|
test('validates only a copy target path', () => {
|
||||||
@@ -128,9 +135,23 @@ test('validates only a copy target path', () => {
|
|||||||
assert.deepEqual(collectNewTargetPaths(records), [validAdmin()]);
|
assert.deepEqual(collectNewTargetPaths(records), [validAdmin()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('accepts a rename from an old bad name to a valid target', () => {
|
test('rejects a rename without a machine-readable compatibility alias', () => {
|
||||||
const records = parseNameStatusZ(Buffer.from(`R100\0changelogs-v2/2026-07/63_old.md\0${validAdmin()}\0`, 'utf8'));
|
const source = 'changelogs-v2/2026-07/63_old.md';
|
||||||
assert.equal(runValidation(records, SHANGHAI_NOW).errors.length, 0);
|
const records = parseNameStatusZ(Buffer.from(`R100\0${source}\0${validAdmin()}\0`, 'utf8'));
|
||||||
|
assert.ok(runValidation(records, SHANGHAI_NOW).errors.some(({ code }) => code === 'E_PATH_STABILITY'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts a rename and a legacy alias path when both are registered', () => {
|
||||||
|
const source = 'changelogs-v2/2026-07/63_old.md';
|
||||||
|
const target = validAdmin();
|
||||||
|
const pathAliases = pathAliasMapFromManifest({
|
||||||
|
schema: 'hl-changelog-path-aliases/v1',
|
||||||
|
aliases: [{ alias: source, canonical: target }],
|
||||||
|
});
|
||||||
|
const rename = parseNameStatusZ(Buffer.from(`R100\0${source}\0${target}\0`, 'utf8'));
|
||||||
|
assert.equal(runValidation(rename, SHANGHAI_NOW, { pathAliases }).errors.length, 0);
|
||||||
|
const compatibilityAddition = parseNameStatusZ(Buffer.from(`A\0${source}\0`, 'utf8'));
|
||||||
|
assert.equal(runValidation(compatibilityAddition, SHANGHAI_NOW, { pathAliases }).errors.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rejects a rename target with an old day', () => {
|
test('rejects a rename target with an old day', () => {
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户