比较提交
61
次代码提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
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 |
@@ -44,3 +44,6 @@ jobs:
|
||||
|
||||
- name: Validate changelog frontmatter
|
||||
run: npm run check:frontmatter -- --event "$GITHUB_EVENT_PATH"
|
||||
|
||||
- name: Validate changelog path aliases
|
||||
run: npm run check:path-aliases
|
||||
|
||||
+12
-1
@@ -30,7 +30,9 @@ changelogs-v2-mp/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除
|
||||
检测器读取 `git diff --name-status -z --find-renames` 的结果,只校验本次 diff 新出现的目标路径:
|
||||
|
||||
- `A`(新增)、`C`(复制)和 `R`(重命名)的目标路径必须通过规则。
|
||||
- `M`(修改历史文件)和 `D`(删除)豁免,不会因存量错误命名阻断。
|
||||
- `M`(修改历史文件)豁免,不会因存量错误命名阻断。
|
||||
- 已发布文件的 `D`(删除)和 `R`(重命名)默认以 `E_PATH_STABILITY` 阻断。确需迁移时,必须在
|
||||
`changelog-path-aliases.json` 登记旧路径到 canonical 的精确关系,并保留可读取的兼容入口。
|
||||
- 重命名到受控目录时,新目标路径必须使用校验当天的上海日期。
|
||||
|
||||
本地校验:
|
||||
@@ -38,6 +40,7 @@ changelogs-v2-mp/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除
|
||||
```bash
|
||||
npm test
|
||||
npm run check:filenames -- --base origin/main --head HEAD
|
||||
npm run check:path-aliases
|
||||
```
|
||||
|
||||
生产 CLI 故意不提供 `--date` 或日期环境变量;测试只通过导出的纯函数注入 `Date`。规则失败返回退出码 `1`,Git/事件/参数等基础设施错误返回 `2`。
|
||||
@@ -70,12 +73,20 @@ pending → claimed → implemented → released → verified
|
||||
- `FRONTEND_CONSUMPTION_STATUS_GUIDE.md`
|
||||
- `BACKEND_CHANGELOG_DELIVERY_GUIDE.md`
|
||||
|
||||
已下发路径是消费契约的一部分,不通过重命名表达状态。历史路径已发生迁移时:
|
||||
|
||||
- `changelog-path-aliases.json` 是机器可识别的唯一映射源;
|
||||
- alias 文件必须保留完整 `hl-changelog/v2` frontmatter,并用 `canonical_path` 指向 canonical;
|
||||
- 前端状态更新使用 `npm run changelog:transition -- <path> <status> ... --write`,命令会同时更新
|
||||
canonical 与全部 alias;对同一状态和证据重复执行不会产生文件变更。
|
||||
|
||||
本地校验:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run check:filenames -- --base origin/main --head HEAD
|
||||
npm run check:frontmatter -- --base origin/main --head HEAD
|
||||
npm run check:path-aliases
|
||||
```
|
||||
|
||||
## CI 与服务端阻断边界
|
||||
|
||||
@@ -49,6 +49,30 @@ not_required
|
||||
|
||||
## 更新命令
|
||||
|
||||
### 存在历史路径 alias 的文档
|
||||
|
||||
消费线程已经记录的路径不得因文件改名失效。先解析路径:
|
||||
|
||||
```powershell
|
||||
npm run changelog:resolve -- "changelogs-v2/2026-07/旧路径.md"
|
||||
```
|
||||
|
||||
状态回写统一使用 alias-aware 命令;传旧路径或 canonical 均会同时更新整组文件:
|
||||
|
||||
```powershell
|
||||
npm run changelog:transition -- `
|
||||
"changelogs-v2/2026-07/旧路径.md" implemented `
|
||||
--owner frontend-team `
|
||||
--frontend-ref "mmg/hl-ui@abc1234" `
|
||||
--write
|
||||
```
|
||||
|
||||
相同状态和证据可以重复执行,第二次不会产生文件变更。alias 关系集中记录在
|
||||
`changelog-path-aliases.json`,并由 `npm run check:path-aliases` 校验文件存在性、ticket、
|
||||
canonical 指向及前端状态一致性。
|
||||
|
||||
### 无 alias 的文档
|
||||
|
||||
领取:
|
||||
|
||||
```powershell
|
||||
@@ -107,3 +131,4 @@ QA 或产品可以协助更新 `verified_at`,但必须基于实际页面验证
|
||||
- `hl-changelog/v1` 继续可读和索引,不强制一次性迁移。
|
||||
- 文件名带“前端待处理”不代表真实状态;需要继续流转时补充 v2 元数据。
|
||||
- 不通过重命名表达消费状态,避免破坏文件名校验和历史链接。
|
||||
- 已被消费的路径如确需规范化,必须先登记 alias、保留兼容入口,并使用 alias-aware 命令同步状态。
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"schema": "hl-changelog-path-aliases/v1",
|
||||
"aliases": [
|
||||
{
|
||||
"ticket": "5216",
|
||||
"alias": "changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-前端待处理-管理后台.md",
|
||||
"canonical": "changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md",
|
||||
"reason": "兼容前端消费线程在文件重命名前持久化的稳定路径"
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
-1
@@ -5,7 +5,11 @@ title: "派车按行程日标记车费日期"
|
||||
consumer: "admin"
|
||||
backend: "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"
|
||||
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 一名司机可绑定多辆常驻车
|
||||
|
||||
> **服务**: `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
|
||||
+5
-1
@@ -5,7 +5,11 @@ title: "用车手动加急与派车看板状态颜色"
|
||||
consumer: "admin"
|
||||
backend: "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"
|
||||
generated: "2026-07-23T10:00:00+08:00"
|
||||
---
|
||||
+5
-1
@@ -5,7 +5,11 @@ title: "用车需求增加独立接机送机选择"
|
||||
consumer: "admin"
|
||||
backend: "verified"
|
||||
gateway: "verified"
|
||||
frontend: "pending"
|
||||
frontend: "claimed"
|
||||
frontend_status: "claimed"
|
||||
frontend_owner: "hl-ui-codex"
|
||||
frontend_ref: ""
|
||||
updated_at: "2026-07-25T03:19:00.677Z"
|
||||
base: "dev-v3"
|
||||
generated: "2026-07-23T17:39:06+08:00"
|
||||
---
|
||||
+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
|
||||
@@ -8,191 +8,26 @@ backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "implemented"
|
||||
frontend_owner: "hl-ui-codex"
|
||||
frontend_ref: "mmg/hl-ui@41f307090eccfdf3d06deabce8bc4f3d2be9a99a"
|
||||
frontend_ref: "mmg/hl-ui@cd493f83a7881401552494fc5a90fbb87395131b"
|
||||
target_release: "hl-ui/v2.1"
|
||||
verified_at: ""
|
||||
status_note: "后端与网关已验证;前端 implemented 状态由前端消费线程维护,本次仅迁移 schema。"
|
||||
updated_at: "2026-07-24"
|
||||
canonical_path: "changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md"
|
||||
status_note: "兼容 #5216 在文件重命名前已被前端消费线程持久化的稳定路径;状态更新必须通过 alias-aware transition 同步 canonical。"
|
||||
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`。
|
||||
- 卡片接送摘要不返回出行人、接送备注或大交通自由文本备注。
|
||||
本文件是 #5216 已下发旧路径的兼容入口。完整接口说明以
|
||||
`changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md`
|
||||
为 canonical;不得删除或再次重命名本文件。
|
||||
|
||||
## 变更接口
|
||||
|
||||
```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`;前端按“前端处理清单”接入即可。
|
||||
- `changelog-path-aliases.json` 记录旧路径与 canonical 的机器可识别关系。
|
||||
- alias 校验与状态同步测试覆盖读取、同步写入和重复写入。
|
||||
|
||||
@@ -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,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 个槽位详情”的新契约完成消费。
|
||||
@@ -5,7 +5,11 @@ title: "车队独立管理及车队字典下线"
|
||||
consumer: "admin"
|
||||
backend: "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"
|
||||
generated: "2026-07-22T10:46:00+08:00"
|
||||
---
|
||||
|
||||
@@ -135,6 +135,17 @@ POST /admin/fleet/assignments/{assignmentId}/confirm
|
||||
- 等待态:“等待司机回复确认”
|
||||
- 已登记态:“司机已确认接单,待车务确认执行”
|
||||
|
||||
### 2026-07-24 界面验收补充
|
||||
|
||||
当前“待确认”步骤中,“司机待确认通知 / 模板与预览均来自后端”标题区下方存在明显的
|
||||
大块空白,导致模板选择行和消息预览整体下移。模板标签及消息正文已经正常显示,因此
|
||||
这是前端布局问题,不是后端模板或渲染接口缺少数据。
|
||||
|
||||
- 移除标题区不必要的固定高度、最小高度或空占位,让高度由标题和副标题内容自然撑开。
|
||||
- 标题区与模板选择行保持正常紧凑间距,不要为未来内容预留不可见空白。
|
||||
- 常用桌面分辨率下,标题区底部到模板选择行的垂直空白不应超过 16px。
|
||||
- 本项不新增接口、不调整字段,也不要为修复布局重新维护前端本地模板。
|
||||
|
||||
## 前端处理清单
|
||||
|
||||
- [ ] 模板列表来自后端 `hold_notify` 模板,默认选中 `isDefault=true`,不再使用前端假模板。
|
||||
@@ -145,6 +156,7 @@ POST /admin/fleet/assignments/{assignmentId}/confirm
|
||||
- [ ] 无凭证时仍可成功登记司机确认并执行最终确认。
|
||||
- [ ] `605025` 时提示“请先登记司机已确认接单”,不提示“缺少凭证”。
|
||||
- [ ] 页面刷新后能按后端状态恢复待回复/已确认阶段。
|
||||
- [ ] 修复“司机待确认通知”标题区异常留白,模板选择与消息预览紧凑衔接。
|
||||
|
||||
## 验证证据
|
||||
|
||||
|
||||
@@ -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,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "node --test tests/validate-changelog-filenames.test.mjs tests/validate-changelog-frontmatter.test.mjs",
|
||||
"test": "node --test tests/validate-changelog-filenames.test.mjs tests/validate-changelog-frontmatter.test.mjs tests/changelog-path-aliases.test.mjs",
|
||||
"check:filenames": "node scripts/validate-changelog-filenames.mjs",
|
||||
"check:frontmatter": "node scripts/validate-changelog-frontmatter.mjs"
|
||||
"check:frontmatter": "node scripts/validate-changelog-frontmatter.mjs",
|
||||
"check:path-aliases": "node scripts/changelog-path-aliases.mjs --check",
|
||||
"changelog:resolve": "node scripts/changelog-path-aliases.mjs --resolve",
|
||||
"changelog:transition": "node scripts/changelog-path-aliases.mjs --transition"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
||||
@@ -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', '小程序端'],
|
||||
]);
|
||||
const CHANGE_TYPES = new Set(['新增接口', '修改接口', '删除接口']);
|
||||
const PATH_ALIAS_MANIFEST = 'changelog-path-aliases.json';
|
||||
|
||||
function ruleError(code, path, message, expected) {
|
||||
return { code, path, message, expected };
|
||||
@@ -76,6 +77,40 @@ export function collectNewTargetPaths(records) {
|
||||
.map(({ targetPath }) => targetPath);
|
||||
}
|
||||
|
||||
export function pathAliasMapFromManifest(value) {
|
||||
if (value?.schema !== 'hl-changelog-path-aliases/v1' || !Array.isArray(value.aliases)) {
|
||||
throw new Error('changelog-path-aliases.json schema 或 aliases 非法');
|
||||
}
|
||||
const aliases = new Map();
|
||||
for (const [index, entry] of value.aliases.entries()) {
|
||||
if (
|
||||
!entry
|
||||
|| typeof entry.alias !== 'string'
|
||||
|| typeof entry.canonical !== 'string'
|
||||
|| entry.alias.length === 0
|
||||
|| entry.canonical.length === 0
|
||||
) {
|
||||
throw new Error(`changelog-path-aliases.json aliases[${index}] 缺少 alias/canonical`);
|
||||
}
|
||||
if (aliases.has(entry.alias)) {
|
||||
throw new Error(`changelog-path-aliases.json alias 重复: ${entry.alias}`);
|
||||
}
|
||||
aliases.set(entry.alias, entry.canonical);
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
export function loadPathAliasMap(root = process.cwd()) {
|
||||
try {
|
||||
return pathAliasMapFromManifest(JSON.parse(readFileSync(`${root}/${PATH_ALIAS_MANIFEST}`, 'utf8')));
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return new Map();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function controlledRootForPath(inputPath) {
|
||||
const candidate = String(inputPath);
|
||||
for (const root of CONTROLLED_ROOTS.keys()) {
|
||||
@@ -178,10 +213,32 @@ export function validateChangelogPath(inputPath, now = new Date()) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function runValidation(records, now = new Date()) {
|
||||
export function runValidation(records, now = new Date(), { pathAliases = new Map() } = {}) {
|
||||
const targetPaths = collectNewTargetPaths(records);
|
||||
const controlledPaths = targetPaths.filter((targetPath) => controlledRootForPath(targetPath));
|
||||
const errors = controlledPaths.flatMap((targetPath) => validateChangelogPath(targetPath, now));
|
||||
const errors = controlledPaths.flatMap((targetPath) => (
|
||||
pathAliases.has(targetPath) ? [] : validateChangelogPath(targetPath, now)
|
||||
));
|
||||
for (const record of records) {
|
||||
const sourcePath = /^R\d{1,3}$/.test(record.status)
|
||||
? record.sourcePath
|
||||
: record.status === 'D' ? record.targetPath : undefined;
|
||||
if (!sourcePath || !controlledRootForPath(sourcePath) || !sourcePath.endsWith('.md')) {
|
||||
continue;
|
||||
}
|
||||
const expectedTarget = pathAliases.get(sourcePath);
|
||||
const actualTarget = /^R\d{1,3}$/.test(record.status) ? record.targetPath : undefined;
|
||||
if (!expectedTarget || (actualTarget && expectedTarget !== actualTarget)) {
|
||||
errors.push(ruleError(
|
||||
'E_PATH_STABILITY',
|
||||
sourcePath,
|
||||
'已发布 changelog 不得在缺少精确 alias 关系时删除或重命名',
|
||||
actualTarget
|
||||
? `在 ${PATH_ALIAS_MANIFEST} 登记 ${sourcePath} -> ${actualTarget} 并保留兼容入口`
|
||||
: `在 ${PATH_ALIAS_MANIFEST} 登记迁移关系并保留兼容入口`,
|
||||
));
|
||||
}
|
||||
}
|
||||
return {
|
||||
targetCount: targetPaths.length,
|
||||
checkedCount: controlledPaths.length,
|
||||
@@ -294,7 +351,9 @@ export function main(argv = process.argv.slice(2)) {
|
||||
try {
|
||||
const options = parseArguments(argv);
|
||||
const records = parseNameStatusZ(diffFromOptions(options));
|
||||
const result = runValidation(records, new Date());
|
||||
const result = runValidation(records, new Date(), {
|
||||
pathAliases: loadPathAliasMap(),
|
||||
});
|
||||
if (result.errors.length > 0) {
|
||||
for (const error of result.errors) {
|
||||
console.error(`[${error.code}] ${error.path}: ${error.message}; expected ${error.expected}`);
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
loadPathAliasManifest,
|
||||
parsePathAliasManifest,
|
||||
resolveChangelogPath,
|
||||
transitionAliasedFrontendState,
|
||||
validatePathAliases,
|
||||
} from '../scripts/changelog-path-aliases.mjs';
|
||||
import { parseFrontmatter } from '../scripts/validate-changelog-frontmatter.mjs';
|
||||
|
||||
const OLD_5216 = 'changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-前端待处理-管理后台.md';
|
||||
const CURRENT_5216 = 'changelogs-v2/2026-07/24_5216_派车看板补充槽位接送路线与就绪摘要-修改接口-管理后台.md';
|
||||
|
||||
function document({ canonicalPath = '', status = 'claimed', owner = 'frontend-team', frontendRef = '' } = {}) {
|
||||
const canonicalLine = canonicalPath ? `canonical_path: "${canonicalPath}"\n` : '';
|
||||
return `---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "5216"
|
||||
title: "路径兼容测试"
|
||||
consumer: "admin"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "${status}"
|
||||
frontend_owner: "${owner}"
|
||||
frontend_ref: "${frontendRef}"
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
${canonicalLine}updated_at: "2026-07-26"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 路径兼容测试
|
||||
|
||||
## 变更接口
|
||||
|
||||
- 无业务接口变化。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- 自动化测试。
|
||||
`;
|
||||
}
|
||||
|
||||
function createFixture() {
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'hl-changelog-alias-'));
|
||||
const manifest = {
|
||||
schema: 'hl-changelog-path-aliases/v1',
|
||||
aliases: [{
|
||||
ticket: '5216',
|
||||
alias: OLD_5216,
|
||||
canonical: CURRENT_5216,
|
||||
reason: '测试旧路径兼容',
|
||||
}],
|
||||
};
|
||||
writeFileSync(path.join(root, 'changelog-path-aliases.json'), JSON.stringify(manifest, null, 2));
|
||||
for (const repoPath of [OLD_5216, CURRENT_5216]) {
|
||||
const absolute = path.join(root, ...repoPath.split('/'));
|
||||
mkdirSync(path.dirname(absolute), { recursive: true });
|
||||
writeFileSync(absolute, document({
|
||||
canonicalPath: repoPath === OLD_5216 ? CURRENT_5216 : '',
|
||||
}));
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
test('repository #5216 compatibility entry resolves to its canonical document', () => {
|
||||
const manifest = loadPathAliasManifest();
|
||||
assert.equal(resolveChangelogPath(OLD_5216, manifest), CURRENT_5216);
|
||||
assert.equal(resolveChangelogPath(CURRENT_5216, manifest), CURRENT_5216);
|
||||
assert.deepEqual(validatePathAliases().errors, []);
|
||||
});
|
||||
|
||||
test('rejects unsafe paths, duplicate aliases and alias chains', () => {
|
||||
assert.throws(() => parsePathAliasManifest(JSON.stringify({
|
||||
schema: 'hl-changelog-path-aliases/v1',
|
||||
aliases: [{
|
||||
ticket: '5216',
|
||||
alias: '../outside.md',
|
||||
canonical: CURRENT_5216,
|
||||
reason: 'unsafe',
|
||||
}],
|
||||
})), /POSIX|规范路径/);
|
||||
assert.throws(() => parsePathAliasManifest(JSON.stringify({
|
||||
schema: 'hl-changelog-path-aliases/v1',
|
||||
aliases: [
|
||||
{ ticket: '5216', alias: OLD_5216, canonical: CURRENT_5216, reason: 'one' },
|
||||
{ ticket: '5216', alias: OLD_5216, canonical: CURRENT_5216, reason: 'two' },
|
||||
],
|
||||
})), /重复/);
|
||||
});
|
||||
|
||||
test('frontend status transition updates alias group once and is idempotent', () => {
|
||||
const root = createFixture();
|
||||
try {
|
||||
const first = transitionAliasedFrontendState(root, OLD_5216, {
|
||||
status: 'implemented',
|
||||
owner: 'frontend-team',
|
||||
frontendRef: 'mmg/hl-ui@abc1234',
|
||||
}, {
|
||||
now: new Date('2026-07-26T04:00:00Z'),
|
||||
write: true,
|
||||
});
|
||||
assert.deepEqual(new Set(first.changedPaths), new Set([OLD_5216, CURRENT_5216]));
|
||||
assert.deepEqual(validatePathAliases(root).errors, []);
|
||||
|
||||
for (const repoPath of [OLD_5216, CURRENT_5216]) {
|
||||
const text = readFileSync(path.join(root, ...repoPath.split('/')), 'utf8');
|
||||
const { metadata } = parseFrontmatter(text);
|
||||
assert.equal(metadata.frontend_status, 'implemented');
|
||||
assert.equal(metadata.frontend_owner, 'frontend-team');
|
||||
assert.equal(metadata.frontend_ref, 'mmg/hl-ui@abc1234');
|
||||
}
|
||||
|
||||
const second = transitionAliasedFrontendState(root, OLD_5216, {
|
||||
status: 'implemented',
|
||||
owner: 'frontend-team',
|
||||
frontendRef: 'mmg/hl-ui@abc1234',
|
||||
}, {
|
||||
now: new Date('2026-07-26T04:00:00Z'),
|
||||
write: true,
|
||||
});
|
||||
assert.deepEqual(second.changedPaths, []);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
collectNewTargetPaths,
|
||||
getShanghaiDate,
|
||||
parseNameStatusZ,
|
||||
pathAliasMapFromManifest,
|
||||
runValidation,
|
||||
validateChangelogPath,
|
||||
} from '../scripts/validate-changelog-filenames.mjs';
|
||||
@@ -114,13 +115,19 @@ test('parses NUL-delimited Chinese and space-containing paths', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('exempts modified and deleted historical bad paths', () => {
|
||||
const records = parseNameStatusZ(Buffer.from(
|
||||
'M\0changelogs-v2/2026-07/63_历史坏文件.md\0D\0changelogs-v2/2026-07/99_无issue.md\0',
|
||||
test('exempts a modified historical bad path but protects a deleted published path', () => {
|
||||
const modified = parseNameStatusZ(Buffer.from(
|
||||
'M\0changelogs-v2/2026-07/63_历史坏文件.md\0',
|
||||
'utf8',
|
||||
));
|
||||
assert.deepEqual(collectNewTargetPaths(records), []);
|
||||
assert.equal(runValidation(records, SHANGHAI_NOW).errors.length, 0);
|
||||
assert.deepEqual(collectNewTargetPaths(modified), []);
|
||||
assert.equal(runValidation(modified, SHANGHAI_NOW).errors.length, 0);
|
||||
|
||||
const deleted = parseNameStatusZ(Buffer.from(
|
||||
'D\0changelogs-v2/2026-07/99_无issue.md\0',
|
||||
'utf8',
|
||||
));
|
||||
assert.ok(runValidation(deleted, SHANGHAI_NOW).errors.some(({ code }) => code === 'E_PATH_STABILITY'));
|
||||
});
|
||||
|
||||
test('validates only a copy target path', () => {
|
||||
@@ -128,9 +135,23 @@ test('validates only a copy target path', () => {
|
||||
assert.deepEqual(collectNewTargetPaths(records), [validAdmin()]);
|
||||
});
|
||||
|
||||
test('accepts a rename from an old bad name to a valid target', () => {
|
||||
const records = parseNameStatusZ(Buffer.from(`R100\0changelogs-v2/2026-07/63_old.md\0${validAdmin()}\0`, 'utf8'));
|
||||
assert.equal(runValidation(records, SHANGHAI_NOW).errors.length, 0);
|
||||
test('rejects a rename without a machine-readable compatibility alias', () => {
|
||||
const source = 'changelogs-v2/2026-07/63_old.md';
|
||||
const records = parseNameStatusZ(Buffer.from(`R100\0${source}\0${validAdmin()}\0`, 'utf8'));
|
||||
assert.ok(runValidation(records, SHANGHAI_NOW).errors.some(({ code }) => code === 'E_PATH_STABILITY'));
|
||||
});
|
||||
|
||||
test('accepts a rename and a legacy alias path when both are registered', () => {
|
||||
const source = 'changelogs-v2/2026-07/63_old.md';
|
||||
const target = validAdmin();
|
||||
const pathAliases = pathAliasMapFromManifest({
|
||||
schema: 'hl-changelog-path-aliases/v1',
|
||||
aliases: [{ alias: source, canonical: target }],
|
||||
});
|
||||
const rename = parseNameStatusZ(Buffer.from(`R100\0${source}\0${target}\0`, 'utf8'));
|
||||
assert.equal(runValidation(rename, SHANGHAI_NOW, { pathAliases }).errors.length, 0);
|
||||
const compatibilityAddition = parseNameStatusZ(Buffer.from(`A\0${source}\0`, 'utf8'));
|
||||
assert.equal(runValidation(compatibilityAddition, SHANGHAI_NOW, { pathAliases }).errors.length, 0);
|
||||
});
|
||||
|
||||
test('rejects a rename target with an old day', () => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户