比较提交

..

没有共同的提交。main 和 docs/5236-transport-driven-transfer 的历史完全不同。

共有 234 个文件被更改,包括 40 次插入38424 次删除

2
.gitattributes vendored
查看文件

@ -1,2 +0,0 @@
.githooks/* text eol=lf
scripts/*.mjs text eol=lf

查看文件

@ -44,6 +44,3 @@ 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

查看文件

@ -1,25 +0,0 @@
#!/bin/sh
# changelog 发布门禁(推送前强制校验)
# 启用(每台机一次): git config core.hooksPath .githooks
# 拦截目标: 接口类 changelog 未部署测试服(backend_status != deployed)就推送给前端,
# 以及文件名/frontmatter 结构违规。规则实现见 scripts/validate-changelog-*.mjs。
zero=0000000000000000000000000000000000000000
status=0
while read local_ref local_sha remote_ref remote_sha; do
# 删除远端分支的推送没有本地内容可校验
[ "$local_sha" = "$zero" ] && continue
if [ "$remote_sha" = "$zero" ]; then
base=$(git rev-parse --verify origin/main 2>/dev/null) || continue
else
base=$remote_sha
fi
[ "$base" = "$local_sha" ] && continue
node scripts/validate-changelog-filenames.mjs --base "$base" --head "$local_sha" || status=1
node scripts/validate-changelog-frontmatter.mjs --base "$base" --head "$local_sha" || status=1
done
if [ "$status" -ne 0 ]; then
echo "" >&2
echo "推送被 changelog 发布门禁拦截:接口类条目必须测试服已部署+实测(backend_status=deployed)后才能推送给前端。" >&2
echo "修正文件后重试;规则详见 BACKEND_CHANGELOG_DELIVERY_GUIDE.md §2.1。" >&2
fi
exit $status

1
.gitignore vendored
查看文件

@ -1 +0,0 @@
.tmp-user-*

查看文件

@ -10,11 +10,9 @@
文件名:
```text
DD_issue_业务标题-{新增接口|修改接口|删除接口|修复|前端缺陷|前端优化|前端修复}-{管理后台|小程序端}.md
DD_issue_业务标题-{新增接口|修改接口|删除接口}-{管理后台|小程序端}.md
```
纯前端条目无后端工单issue 段写字面量 `frontend`,如 `10_frontend_标题-前端缺陷-管理后台.md`
例如:
```text
@ -50,39 +48,6 @@ verified_at: ""
- 不需要前端修改:`frontend_status: "not_required"`
- 后端不要代替前端填写 `implemented``released``verified`
## 2.1 发布门禁硬规则,2026-08-10 wx 定)
**给前端推送的 changelog,内容必须是测试环境已经存在、可实测到的。**
- 接口类条目(新增接口/修改接口/删除接口推送前必须走完「PR 合并 → 部署测试服 → 测试服真实 API 验证」,frontmatter 必须 `backend_status: "deployed"`,并在正文「验证证据」章节贴实测结果。
- `backend_status``merged` / `pending` / `implemented` 等未部署状态的条目**禁止 push**(校验规则 E_BACKEND_PENDING 会拦)。「先给前端契约、部署随后」的预告式推送一律禁止——前端拿到 changelog 会立刻联调,接口不在等于空耗与误判。
- 纯前端条目(前端缺陷/前端优化/前端修复):`backend_status: "not_required"`,change_type 用对应前端类型;`frontend_status: "not_required"` 时不得残留 frontend_owner / frontend_ref / target_release / verified_at。
- 背景2026-08-06~08-07 三条未部署即推送的条目(#5599/#5567/#5633导致前端在测试环境验不到字段2026-08-10 投诉属实);当时仓库 CI 因校验规则假阳性长期常红被忽略,规则已于 2026-08-10 修正(前端条目类型合法化、`{orderId}` 路径参数不再误判为占位符),此后 **CI 红 = 真违规,必须当场修复回填**
**推送校验(强制)**
- 推荐一次性启用本地钩子,之后 push 自动拦截:`git config core.hooksPath .githooks`
- 未启用钩子则每次 push 前手动跑 §3 的两条校验命令,红了不许推。
- 仓库 CIchangelog-filename-gate对每次 push 复检;push 后请回看 Gitea Actions 状态,红 X 必须当场处理。
## 2.5 写作方法论(对齐 yst 团队 changelog-conventions SKILL,2026-08-04 起执行)
**受众优先**:触达 `/admin/*` `/mp/*` `/v3/admin/*` `/v3/mp/*` 等对外前缀的改动**一律**写前端 changelog,哪怕"前端代码零改动"(前端 AI 可能有 workaround 需清理信号)。`/v3/internal/*` Feign 接口**必须拆出去**单独走后端 changelog,不许和 admin/mp 接口塞同一份(反例:# traveler 11 接口事故)。
**自包含**:禁止"详见 Knife4j / Swagger / 同目录 xx.md"。所有请求参数表、响应字段表、枚举值(值+中文+说明)、错误码、完整 JSON 示例必须内联——消费方 AI 没有内部文档权限。
**消费方语言**:写"下拉框去掉草稿选项",不写"status 字段 ApiModelProperty 注解更新";值变了用 `原来 → 现在` 表格,不写散文。
**示例要求**:每个接口至少 1 组「典型成功」示例(请求+响应完整 JSON;修改类接口建议补「边界」「异常」共 3 组。GET 示例也要写全 URL + Authorization 头 + 注明"无请求体"。
**不写后端实现**:禁止出现 DB 表/字段名、雪花 ID 序列化细节、Nacos 配置拼接、端口/重启/回滚耗时等后端实现与运维内容(后端运维信息写后端 changelog。"任何一行拿掉后接口契约仍成立,就该删"。
**emoji 分类(标题用)**:⚠️ 破坏性变更 / ✨ 新增 / 🔧 行为变更 / 📝 仅文档。
**commit message 用中文**`新增退款政策字段(产品详情接口)`,不用英文。
**多接口 changelog≥3 接口)**:按接口分小节,每个接口自含「使用场景/入参/出参/错误码/业务边界/示例」,不把多接口入参混到一张大表。
## 3. 校验
`hl-api-changelog` 仓库执行:
@ -97,36 +62,14 @@ npm run check:frontmatter -- --base origin/main --head HEAD
## 4. 提交和推送
只暂存本次 changelog 文件,**直接 commit main**(不建分支/PR
只暂存本次 changelog 文件:
```powershell
git status --short
git add -- changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理后台.md
git diff --cached --check
git commit -m "docs: hand off API contract (#5205)"
git push origin main
git push -u origin <任务分支>
```
不要提交其他任务的 changelog、`.tmp-*` 文件或任何凭据。
## 5. author 字段(必填)
所有 changelog frontmatter 必须包含 `author` 字段,格式为推送者登录名 + `(GIT)` 后缀:
```yaml
author: "wx(GIT)" # wx 推送写 wx(GIT);yst 推送写 yst(GIT);以此类推
```
谁 push 到 main 就写谁,多会话并行时用于追溯该条 changelog 的推送人。新写文件必须带;修改旧文件时顺手补上。
**联系人章节(模仿 yst 格式,2026-08-04 wx 定)**:除 frontmatter `author` 字段外,正文末尾"关联 / 联系人"章节必须标注后端负责人,格式与 yst 的 changelog 一致:
```markdown
## 关联 / 联系人
### 联系人
- **后端负责人**: @wx
```
与 frontmatter `author` 字段同源wx 负责写 `@wx`,yst 负责写 `@yst`。不要在正文开头加"作者"行(已废弃)。模板已含此章节(见 CHANGELOG_TEMPLATE.md。修改他人 changelog 时不要改联系人。
然后向 `main` 创建 PR。不要提交其他任务的 changelog、`.tmp-*` 文件或任何凭据。

查看文件

@ -3,7 +3,6 @@ schema: "hl-changelog/v2"
ticket: "{issue-no}"
title: "{一句话概括变化}"
consumer: "{admin|mp|internal|multiple}"
author: "{推送者登录名}(GIT)" # 如 wx(GIT)/yst(GIT),谁 push 到 main 就写谁
change_type: "{新增接口|修改接口|删除接口}"
backend_status: "pending"
gateway_status: "pending"
@ -154,36 +153,6 @@ base: "{dev|dev-v3}"
---
## 六.5、枚举 / 数据字典(接口出现枚举时必写)
每个枚举单独一个子节,不混表。字段+枚举类对应关系写在子节开头。
### {字段名}{枚举类全限定名}
**所属字段**: `{ReqVO/RespVO 字段名}` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `VALUE_A` | 中文名 | 触发条件/含义 |
## 六.6、修改前后对比(修改/删除类接口必写,新增跳过)
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
## 六.7、影响评估(修改/删除类必写)
- **是否破坏向后兼容**: 是 / 否
- **前端是否必须同步上线**: 是 / 否
- **前端 workaround 清理点**: {如"老前端按比例硬编码算定金的逻辑可撤",无则写"无"}
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- **仅影响**: 管理后台 X 表单
@ -224,15 +193,3 @@ POST /mp/product/{id}/quote → 200 + 报价成功 ✓
- 关联 Issue: [wx/HL#{issue}](https://git.1814.love:8443/wx/HL/issues/{issue})
- 关联 PR: [wx/HL#{pr}](https://git.1814.love:8443/wx/HL/pulls/{pr})
- 后续计划: 见 `{另一条 changelog 路径}`
## 关联 / 联系人
### 链接
- **Issue**: [#{issue-no}](https://git.1814.love:8443/wx/HL/issues/{issue-no})
- **PR**: [#{pr-no}](https://git.1814.love:8443/wx/HL/pulls/{pr-no})
- **Merge commit**: [{merge-sha}](https://git.1814.love:8443/wx/HL/commit/{merge-sha})(合并后回填)
### 联系人
- **后端负责人**: @{推送者登录名}

查看文件

@ -4,8 +4,6 @@
`/v3/admin/*` 接口写入 `changelogs-v2/`
项目例外:`/admin/fleet/*` 虽无 `/v3` 前缀,但由二期车管管理后台消费,同样写入 `changelogs-v2/`
```text
changelogs-v2/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除接口}-管理后台.md
```
@ -32,9 +30,7 @@ changelogs-v2-mp/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除
检测器读取 `git diff --name-status -z --find-renames` 的结果,只校验本次 diff 新出现的目标路径:
- `A`(新增)、`C`(复制)和 `R`(重命名)的目标路径必须通过规则。
- `M`(修改历史文件)豁免,不会因存量错误命名阻断。
- 已发布文件的 `D`(删除)和 `R`(重命名)默认以 `E_PATH_STABILITY` 阻断。确需迁移时,必须在
`changelog-path-aliases.json` 登记旧路径到 canonical 的精确关系,并保留可读取的兼容入口。
- `M`(修改历史文件)和 `D`(删除)豁免,不会因存量错误命名阻断。
- 重命名到受控目录时,新目标路径必须使用校验当天的上海日期。
本地校验:
@ -42,7 +38,6 @@ 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`
@ -75,20 +70,12 @@ 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,30 +49,6 @@ 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
@ -131,4 +107,3 @@ QA 或产品可以协助更新 `verified_at`,但必须基于实际页面验证
- `hl-changelog/v1` 继续可读和索引,不强制一次性迁移。
- 文件名带“前端待处理”不代表真实状态;需要继续流转时补充 v2 元数据。
- 不通过重命名表达消费状态,避免破坏文件名校验和历史链接。
- 已被消费的路径如确需规范化,必须先登记 alias、保留兼容入口,并使用 alias-aware 命令同步状态。

查看文件

@ -1,4 +0,0 @@
{
"schema": "hl-changelog-path-aliases/v1",
"aliases": []
}

查看文件

@ -8,12 +8,11 @@ backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@cd493f83a7881401552494fc5a90fbb87395131b"
frontend_ref: "mmg/hl-ui@41f307090eccfdf3d06deabce8bc4f3d2be9a99a"
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"
updated_at: "2026-07-24"
base: "dev-v3"
generated: "2026-07-24T14:24:00+08:00"
---

查看文件

@ -6,13 +6,13 @@ consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@85851ad68d427e161d9342525af4567c1d108d5f"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端 PR #5241 已合并至 dev-v39578f78d5,order/fleet 已部署测试环境b57ce915/0da3b9e6,双实例 internal 契约与网关汇总/列表/详情已验证;前端仍为 pending,待删除车辆接送开关并改用大交通摘要。"
updated_at: "2026-07-24T10:59:50.144Z"
updated_at: "2026-07-24"
base: "dev-v3"
---

查看文件

@ -1,427 +0,0 @@
---
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

查看文件

@ -1,388 +0,0 @@
---
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

查看文件

@ -1,139 +0,0 @@
---
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` 表示等待前端真实领取,不代表页面已实现、发布或验证。

查看文件

@ -1,218 +0,0 @@
---
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 Spotless606 个 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` 表示前端已领取但仍需修复“新增车辆槽位”;尚未形成可验收的完整实现。

查看文件

@ -1,167 +0,0 @@
---
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` 继续返回,
但新页面必须改用每车总费用字段。

查看文件

@ -1,158 +0,0 @@
---
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,不迁移、清理或回填数据。

查看文件

@ -1,281 +0,0 @@
---
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 个槽位详情”的新契约完成消费。

查看文件

@ -1,112 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5262"
title: "派车逐日车费、只读总价与核单实时接口"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "mmg/hl-ui@2dfe8ab40f3db446d0a079d7511986a2d7fdce33"
target_release: ""
verified_at: "2026-07-26T16:37:00+08:00"
path_aliases: "changelogs-v2/2026-07/26_5262_派车逐日车费与核单实时接口-修改接口-前端待处理-管理后台.md"
status_note: "后端 PR wx/HL#5266 已合并到 dev-v39bfd21de6,Fleet/Order 测试双实例已部署并完成管理端网关与 internal Feign 实测;hl-admin 已在 2dfe8ab40f3db446d0a079d7511986a2d7fdce33 接入逐日车费、只读总价和旧字段移除,verify:changed 通过,尚未发布及页面联调。本契约替代 #5253 的“手工填写每车总价”口径。"
updated_at: "2026-07-26"
base: "dev-v3"
generated: "2026-07-26T16:20:00+08:00"
---
# 派车逐日车费、只读总价与核单实时接口
## 关联
- Issue: [wx/HL#5262](https://git.1814.love:8443/wx/HL/issues/5262)
- 服务:`hl-fleet-service``hl-order-service-v3`
- 前端仓库:`mmg/hl-ui`(本文仅交接,不代表已修改前端)
- 替代口径:[#5253 按车辆记录订单总车费并接入核单](./26_5253_按车辆记录订单总车费并接入核单-修改接口-管理后台.md)
## 关键变化
1. 多日派车按服务日保存 assignment,每个槽位 4 天即 4 条每日记录。
2. 价格日历改为提供逐日参考价;车务可覆盖本次派车的某日车费,覆盖值不回写价格日历。
3. `vehicleFeeTotal` 改为只读合计,恒等于收费日 `assignmentPrice` 之和。
4. 创建、批量派车、确认和改派请求继续兼容解析旧总价字段,但只要传值即返回稳定业务错误,不再接受手工总价。
5. 配置车辆不写 Order 核单表;核单后端通过新的 Fleet internal API 实时读取逐日车辆费用。
## 变更接口
| 方法 | 路径 | 变化 |
| --- | --- | --- |
| `POST` | `/admin/fleet/assignments/candidates` | 车辆候选新增逐日车费参考 |
| `POST` | `/admin/fleet/assignments` | 新增 `dailyVehicleFees`;旧 `vehicleFeeTotal` 禁止传值 |
| `POST` | `/admin/fleet/assignments/batch` | 每个最终车辆槽位分别提交 `dailyVehicleFees` |
| `POST` | `/admin/fleet/assignments/:assignmentId/change` | 新派车段提交逐日车费;保留段沿用原逐日快照 |
| `POST` | `/admin/fleet/assignments/:assignmentId/confirm` | 旧总价字段禁止传值;总价由已保存逐日车费只读计算 |
| `GET` | `/admin/fleet/board/orders/:orderId` | 槽位返回 `dailyVehicleFees` 和只读合计 |
## 请求字段
`dailyVehicleFees[]`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `serviceDate` | `LocalDate` | 是 | 本次覆盖的服务日期 |
| `price` | `Decimal` | 是 | 本次派车单日车费,最小 `0.00`,最多 2 位小数 |
规则:
- 未提交覆盖值的收费日使用车型价格日历当天价格。
- 收费日缺少日历价格且未提交覆盖值时,后端拒绝最终派车。
- 覆盖值与日历参考价不一致时提交 `vehicleFeeAdjustmentReason`
- 免费日期 `assignmentPrice` 固定为 `"0.00"`,不计入总价。
- `vehicleFeeTotal``retainedVehicleFeeTotal` 以及对应旧调整原因字段不得再由前端提交。
## 响应字段
候选、派车写响应和看板槽位新增或统一返回 `dailyVehicleFees[]`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `serviceDate` | `LocalDate` | 服务日期 |
| `chargeable` | `Boolean` | 是否收取车费 |
| `calendarPrice` | `Decimal/null` | 车型价格日历参考价;缺价时为空 |
| `assignmentPrice` | `Decimal/null` | 本次派车单日车费;收费日必须有值,免费日为 `"0.00"` |
| `source` | `String` | `CALENDAR` / `OVERRIDE` / `FREE` / `MISSING` |
| `calendarPriceMissing` | `Boolean` | 价格日历是否缺价 |
`vehicleFeeTotal` 继续返回,但语义变为只读:
```text
vehicleFeeTotal = sum(dailyVehicleFees[chargeable=true].assignmentPrice)
```
金额字段按字符串消费,雪花 ID 继续按字符串消费。
## 页面展示矩阵
| 区域 | 展示 | 空态 | 状态/颜色 | 守恒规则 |
| --- | --- | --- | --- | --- |
| 收费日期 | 每日显示日历参考价与本次派车价 | 选车前“待计算”;缺价“价格日历缺价” | 参考价中性、覆盖蓝、缺价橙 | 一服务日一条派车记录 |
| 最终总车费 | 只读合计,不渲染金额输入框 | 缺价时“价格不完整” | 正常中性、缺价橙 | 等于全部收费日本次派车价之和 |
| 已结束行程 | 只允许查看逐日价格和总价 | 不适用 | 只读灰 | 前端禁用与后端拒绝一致 |
## 前端处理清单
- [ ] 移除“最终总车费”输入框,改为只读合计。
- [ ] 收费日期逐日展示 `calendarPrice`,并允许编辑当前槽位的 `assignmentPrice`
- [ ] 仅把修改后的日期组装为 `dailyVehicleFees`,不调用车型价格日历写接口。
- [ ] 使用 `source``calendarPriceMissing` 展示覆盖与缺价状态。
- [ ] 创建、批量派车、确认和改派请求不再传旧总价字段。
- [ ] 行程结束后禁用逐日车费、槽位和改派入口。
## 验证证据
- OpenAPI/oasdiff`not_configured`;已完成 Controller/VO 源码比对和 Fleet 接口测试回退证据。
- Spring Cloud Contract`not_configured`;已完成 Fleet producer、Order Feign consumer 和 shared DTO 测试回退证据。
- Fleet`mvn -pl hl-fleet-service -am verify``spotless:check` 通过。
- Order`mvn -pl hl-order-service-v3 -am verify` 完成,Surefire 零失败并生成可执行 JAR。
- 部署Fleet `630858f8`、Order `63f9ba60` 成功,8087/8187 与 8086/8186 双实例健康。
- 网关:管理端订单详情返回 4 条逐日车费及约定的 6 个逐日字段;internal Feign 正向响应严格为顶层 5 个字段、item 12 个字段。
- `frontend_status``pending`;真实领取后再迁移为 `claimed`

查看文件

@ -1,158 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5263"
title: "最终确认按车选择发送行程短信"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "mmg/hl-ui@138136e50c4bbf9931ee020bd28d40977264b64e"
target_release: ""
verified_at: ""
status_note: "前端已在 138136e5 接入 HOLD 最终确认逐车短信必选、权威状态查询和 FAILED 受控重试;定向 7 个文件 62 项及 verify:changed 全量验证通过。尚未发布或完成真实页面联调。"
updated_at: "2026-07-27"
base: "dev-v3"
generated: "2026-07-26T17:33:00+08:00"
---
# 最终确认按车选择发送行程短信
## 关联
- Issue: [wx/HL#5263](https://git.1814.love:8443/wx/HL/issues/5263)
- Changelog PR: [wx/hl-api-changelog#41](https://git.1814.love:8443/wx/hl-api-changelog/pulls/41)
- 服务:`hl-fleet-service``hl-user-service`
- 前端仓库:`mmg/hl-ui`(本文仅交接,不代表已修改前端)
- 前置车费契约:[#5262 派车逐日车费、只读总价与核单实时接口](./26_5262_派车逐日车费与核单实时接口-修改接口-前端待处理-管理后台.md)
## 关键变化
1. 仅在 HOLD 排车的最终确认阶段,每个车辆组必须显式选择“发送短信”或“不发送短信”,没有默认值。
2. 选择发送时,确认事务只落可靠发送意图;短信异步发送失败不会回滚已完成的派车确认。
3. 短信只发给该车辆组当前师傅,包含订单摘要、接送摘要和签名行程短链,不包含客户手机号。
4. 选择不发送时只完成派车确认,不创建行程短信事件。
5. 多车订单逐车独立选择、独立投递、独立查询状态和受控重试。
6. 已派定后的订单人数基线复核只能沿用原选择,不允许借复核修改选择或重复发送。
7. 直接派车流程不受影响;#5262 已废弃的手工车辆总价入参仍然禁止提交。
## 变更接口
| 方法 | 路径 | 变化 |
| --- | --- | --- |
| `POST` | `/admin/fleet/assignments/:assignmentId/confirm` | 请求新增必填 `sendItinerarySms`;响应新增短信选择、事件与状态 |
| `GET` | `/admin/fleet/assignments/:assignmentId/itinerary-sms` | 新增单车/车辆组短信审计状态查询 |
| `POST` | `/admin/fleet/assignments/:assignmentId/itinerary-sms/retry` | 新增明确失败后的车务受控重试 |
## 最终确认
### `POST /admin/fleet/assignments/:assignmentId/confirm`
#### 请求字段
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `sendItinerarySms` | `Boolean` | 是 | `true` 发送;`false` 不发送;省略或 `null` 返回参数错误 |
| `requestId` | `String` | 是 | 最长 64 字符的幂等请求标识 |
| `vehicleFeeTotal` | `Decimal` | 否 | 历史兼容字段;非空即拒绝,最终总车费继续由 #5262 逐日车费只读合计 |
| `vehicleFeeAdjustmentReason` | `String` | 否 | 历史兼容字段;非空即拒绝 |
请求示例:
```json
{
"sendItinerarySms": true,
"requestId": "fleet-final-confirm-26-8411-car-1"
}
```
#### 新增响应字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `sendItinerarySms` | `Boolean` | 本车辆组最终确认时保存的选择 |
| `itinerarySmsEventId` | `String/null` | 可靠短信事件 ID;不发送时为空 |
| `itinerarySmsStatus` | `String` | 首次确认返回 `PENDING``NOT_SENT`;已派定复核回显真实状态 |
## 短信状态
### `GET /admin/fleet/assignments/:assignmentId/itinerary-sms`
响应 `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `assignmentId` | `String` | 派单 ID |
| `assignmentGroupId` | `String` | 跨服务日车辆组 ID |
| `assignmentSlotId` | `String` | 稳定车辆槽位 ID |
| `sendItinerarySms` | `Boolean/null` | 未最终确认或历史数据时为 `null` |
| `status` | `String` | `NOT_APPLICABLE` / `NOT_SENT` / `PENDING` / `SENT` / `FAILED` / `CANCELED` |
| `eventId` | `String/null` | 可靠短信事件 ID |
| `retryCount` | `Integer` | 已发生的失败重试次数 |
| `canRetry` | `Boolean` | 当前是否允许受控重试 |
| `sentAt` | `LocalDateTime/null` | 供应商确认的真实发送时间 |
| `lastError` | `String/null` | 已脱敏的最近失败或待对账原因 |
前端以 `status` 为权威,不得仅凭最终确认接口成功就显示“短信已发送”。
## 受控重试
### `POST /admin/fleet/assignments/:assignmentId/itinerary-sms/retry`
请求:
```json
{
"reason": "短信通道配置已恢复,车务确认重发",
"requestId": "retry-sms-26-8411-car-1"
}
```
规则:
- 仅 `FAILED``canRetry=true` 时展示并调用重试。
- `SENT``PENDING`、待供应商对账、`CANCELED`、未选择发送时禁止重试。
- 重试复用原事件与供应商幂等键,不新建并行短信事件。
- 司机或车辆组身份已变化时,旧事件收敛为 `CANCELED`,不得发给旧师傅。
## 短信与隐私约束
短信模板参数固定为:
| 参数 | 内容 |
| --- | --- |
| `summary` | 脱敏订单摘要 |
| `transfer` | 接送摘要 |
| `code` | 签名行程短链 |
短信正文及模板参数不得包含客户手机号。真实联系人信息仅在既有签名行程 H5 中按授权展示。
## 页面展示矩阵
| 区域/状态 | 数据源 | 展示 | 空态/禁用 | 颜色 | 守恒规则 |
| --- | --- | --- | --- | --- | --- |
| 最终确认车辆卡片 | 本地待提交选择 | “发送短信”/“不发送短信”二选一 | 未选择时禁止确认并提示必选 | 发送蓝色,不发送中性灰 | 每个车辆组恰好一个选择 |
| 确认后状态 | `GET .../itinerary-sms.status` | 待发送/已发送/发送失败/已取消/未发送 | 历史数据为“不适用” | 待发送蓝、已发送绿、失败红、取消灰、未发送中性灰 | 不以确认成功冒充发送成功 |
| 失败操作 | `canRetry` | “重试短信” | `canRetry=false` 时隐藏或禁用 | 可重试橙色 | 同一事件串行重试 |
| 多车订单 | 每个 `assignmentGroupId` | 每车独立选择与状态 | 不做订单级统一默认 | 各卡片独立 | 一车选择不得覆盖另一车 |
## 前端处理清单
- [ ] 最终确认页按车辆组渲染无默认值的短信二选一。
- [ ] 未完成选择时不提交确认请求,并展示明确校验提示。
- [ ] 确认请求始终显式提交 `sendItinerarySms`,不再依赖后端默认值。
- [ ] 确认后通过状态接口展示真实投递状态。
- [ ] 仅在 `FAILED && canRetry=true` 时允许填写原因并调用重试。
- [ ] 已派定复核回显原选择并保持只读,不提供改选入口。
- [ ] 短信状态按展示矩阵处理空态、颜色及多车独立性。
- [ ] 确认和复核请求继续不提交 #5262 已废弃的手工总车费字段。
## 验证证据
- OpenAPI/oasdiff`not_configured`;已完成 Controller/VO 源码比对和接口测试回退证据。
- 消费者契约:`not_required`;未修改 internal Feign 或共享 Java DTO。
- 代码与测试PR `wx/HL#5270` 已合入 `dev-v3`,merge commit 为 `f0a96c10124c3188e60e1291e7f28d768af50e3a``mvn -pl hl-user-service,hl-fleet-service -am test``mvn -pl hl-fleet-service spotless:check``mvn -pl hl-fleet-service -am verify` 均通过。
- 测试部署:`hl-user-service``hl-fleet-service` 已从合并后的 `dev-v3` 完成双实例滚动部署并通过健康检查。
- 网关:显式“不发送”业务验收 11/11 通过;合并后只读复验 3/3 通过,最终状态为 `assigned + NOT_SENT`,无短信事件且不可重试。
- `frontend_status``pending`;真实领取后再迁移为 `claimed` 并填写 `frontend_owner`

查看文件

@ -395,33 +395,3 @@ GET /admin/fleet/message-templates
- 车队对账页 Network 必须看到 `/admin/fleet/reconciliation/cars``/insurance`,默认周期是当前月 `2026-07-01 ~ 2026-07-31`
- 车管模板页 Network 必须看到 `/admin/fleet/message-templates`
- 车务菜单测试账号必须是车务角色;不要用 `admin``adminle``wx`、定制师账号验证车务菜单。
---
## 8. 2026-07-27 补充:矩阵车辆行常驻司机全部误显示“待派司机”
### 8.1 运行态与源码证据
- `/fleet/matrix` 的 19 条车辆行全部显示“待派司机”。
- 同次页面请求 `GET /admin/fleet/matrix/grid` 返回 200;响应车辆中存在非空
`primaryDriverName` 和脱敏 `primaryDriverPhone`,因此不是后端漏返回,也不是全部车辆都未绑定常驻司机。
- `useFleetMatrixData.adaptMatrixVehicle()` 已把这两个字段保留到车辆行对象。
- `VehicleGantt.primaryDriverOf(v)` 却忽略车辆行字段,只执行
`findPrimaryDriver(props.drivers, v.plate)`
- 主矩阵和车辆分窗传入的 `drivers` 均为空列表且没有额外加载动作,所以每辆车都稳定落入
“待派司机”空态。
### 8.2 前端修复口径
1. 矩阵车辆行展示必须以 `data.vehicles[].primaryDriverName` 为权威来源;非空时直接显示该姓名。
2. `primaryDriverPhone` 已由后端脱敏,可按现有设计选择展示,但不得为显示姓名再拉全量司机列表。
3. 只有 `primaryDriverName``null` 或空白时,才显示“待派司机”空态。
4. 主矩阵和 `matrix-solo?solo=byVehicle` 必须复用同一解析逻辑,不能一处读取车辆行、一处反查本地司机数组。
5. 本问题不涉及后端接口、数据库、派单状态或候选规则变更;不要创建 `mmg/hl-ui` 配合工单,直接消费本 changelog。
### 8.3 前端验收 checklist
- [ ] 构造一辆 `primaryDriverName` 非空的车辆,主矩阵车辆行显示接口姓名而不是“待派司机”。
- [ ] `primaryDriverName=null` 的车辆仍显示“待派司机”。
- [ ] 车辆分窗与主矩阵展示一致。
- [ ] 车队、车型、状态筛选及派车占用条不受本次展示修复影响。

查看文件

@ -1,390 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5264"
title: "移除核单分类手动确认门禁"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "c182af23fb724ab6915d75750951b1db0dcc603d"
target_release: ""
verified_at: "2026-07-28T14:38:26+08:00"
status_note: "hl-admin 已移除‘本分类已确认’入口及 allConfirmed/confirmStatus 后续流程门禁;业务提交 c182af23 已在 origin/v2.1 可达,全量 checkpoint 通过"
updated_at: "2026-07-28"
base: "dev-v3"
---
# 【修改接口·管理后台】移除核单分类手动确认门禁 (#5264)
> **PR**: #5274 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-28 09:42
## 1. 接口背景
核单流程不再要求财务在八个核单分类上逐一点击“本分类已确认”。管理后台只需要保存各分类明细;明细完整且可用于报账时,即可生成主报账人报账表。旧分类确认查询和确认接口保留兼容返回,但确认状态不再作为主报账、单团核算、Step6 提交或财务确认的门禁。
## 变更接口
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查询原型八个核单分类确认状态 | GET | `/v3/admin/order/:orderId/settlement/category-checks` | 修改接口 | 响应字段保留,但 `allConfirmed` / `confirmStatus` 仅用于兼容展示,不再决定后续流程能否继续 |
| 2 | 按最近读取指纹确认单个核单分类 | POST | `/v3/admin/order/:orderId/settlement/category-checks/:category/confirm` | 修改接口 | 标记为废弃兼容;管理后台停止调用并移除“本分类已确认”入口 |
| 3 | 生成主报账人报账表 | POST | `/v3/admin/order/:orderId/settlement/reports/reimbursement/generate` | 修改接口 | 生成条件改为核单明细保存完整,不再要求八分类手动确认 |
## 3. 接口详情
### 3.1 查询原型八个核单分类确认状态
- **方法 / 路径**`GET /v3/admin/order/:orderId/settlement/category-checks`
- **使用场景**:旧页面或兼容逻辑读取八分类状态。
- **认证**:需要管理后台登录态;房控角色不可访问。
- **幂等性**:是,只读查询。
- **限流**:无单独接口限流约定。
- **接口说明**:字段结构保持不变;`allConfirmed``items[].confirmStatus` 不再用于判断主报账、单团核算、Step6 或财务确认是否可继续。
### 3.2 按最近读取指纹确认单个核单分类(废弃兼容)
- **方法 / 路径**`POST /v3/admin/order/:orderId/settlement/category-checks/:category/confirm`
- **使用场景**:仅兼容旧前端请求;新管理后台不再调用。
- **认证**:需要管理后台登录态和财务写权限;房控角色不可访问。
- **幂等性**:同一分类、同一 `expectedSourceFingerprint` 重复确认返回当前兼容状态。
- **限流**:无单独接口限流约定。
- **接口说明**:接口仍校验请求体和分类枚举,但确认投影不再作为后续流程门禁。前端应移除“本分类已确认”按钮、状态卡门禁和基于 `allConfirmed` 的下一步禁用逻辑。
### 3.3 生成主报账人报账表
- **方法 / 路径**`POST /v3/admin/order/:orderId/settlement/reports/reimbursement/generate`
- **使用场景**:核单明细保存完整后生成或刷新主报账人报账表。
- **认证**:需要管理后台登录态和财务写权限;房控角色不可访问。
- **幂等性**:同一来源数据已生成时,可返回当前报账表;来源变化后重新生成。
- **限流**:无单独接口限流约定。
- **接口说明**:生成门禁改为逐分类明细完整性校验;不再要求先调用八分类确认接口。
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 接口 | 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|------|----------|
| 三个接口共用 | `orderId` | `String` | 是 | 订单 ID,按字符串处理 | 必须为大于 0 的数字 |
| 分类确认接口 | `category` | `String` | 是 | 核单分类编码 | 见 §6.1 `SettlementCategory` |
### 4.2 请求体字段
#### 4.2.1 `GET /category-checks`
无请求体。
#### 4.2.2 `POST /category-checks/:category/confirm`(废弃兼容)
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `expectedSourceFingerprint` | `String` | 是 | 最近读取的分类源事实 SHA-256;废弃兼容字段 | 64 位小写十六进制字符串 |
| `confirmEmpty` | `Boolean` | 是 | 是否明确确认空分类;废弃兼容字段 | `true` / `false` |
#### 4.2.3 `POST /reports/reimbursement/generate`
无请求体。
## 5. 出参字段
### 5.1 `SettlementCategoryChecksRespVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `orderId` | `String` | 订单 ID |
| `allConfirmed` | `Boolean` | 兼容字段;不再作为后续流程门禁 |
| `items` | `Array<ItemVO>` | 八个分类状态列表 |
### 5.2 `SettlementCategoryChecksRespVO.ItemVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `category` | `String` | 分类编码,见 §6.1 |
| `categoryName` | `String` | 分类中文名 |
| `rowCount` | `Integer` | 当前分类明细行数 |
| `empty` | `Boolean` | 当前分类是否为空 |
| `sourceFingerprint` | `String` | 当前分类源事实指纹 |
| `confirmStatus` | `String` | 兼容字段,见 §6.2;不再作为后续流程门禁 |
| `confirmedBy` | `String/null` | 兼容字段,确认人 ID |
| `confirmedByName` | `String/null` | 兼容字段,确认人姓名 |
| `confirmedAt` | `String/null` | 兼容字段,确认时间,格式 `yyyy-MM-dd'T'HH:mm:ss` |
### 5.3 `SettlementReimbursementReportRespVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | `String` | 主报账表 ID |
| `orderId` | `String` | 订单 ID |
| `reportStatus` | `String` | 报告状态,见 §6.3 |
| `sourceFingerprint` | `String` | 报账来源指纹 |
| `primaryReporterId` | `String/null` | 主报账人 ID |
| `primaryReporterName` | `String/null` | 主报账人姓名 |
| `primaryReporterRole` | `String/null` | 主报账人角色 |
| `reportVersion` | `Integer` | 报告版本号 |
| `driverCollectedTailAmount` | `Decimal` | 司机代收尾款金额 |
| `approvedAdvanceAmount` | `Decimal` | 已审批预支金额 |
| `reportablePaidCostAmount` | `Decimal` | 可报账已支付成本 |
| `reporterNetAmount` | `Decimal` | 报账人净额 |
| `primaryReporterCollectedAmount` | `Decimal` | 主报账人已收金额 |
| `publicPrepaidAmount` | `Decimal` | 公共预付金额 |
| `primaryReporterDueAmount` | `Decimal` | 主报账人应结金额 |
| `advanceOutstandingAmount` | `Decimal` | 预支未结金额 |
| `reconNetAmount` | `Decimal` | 对账净额 |
| `transferDirection` | `String/null` | 转账方向 |
| `transferAmount` | `Decimal` | 转账金额 |
| `incomeLines` | `Array<Object>` | 收入明细行 |
| `expenseLines` | `Array<Object>` | 支出明细行 |
| `advanceLines` | `Array<Object>` | 预支明细行 |
| `vehicleLines` | `Array<Object>` | 车辆费用明细行 |
| `transferStatus` | `String/null` | 转账状态 |
| `transferDate` | `String/null` | 转账日期,格式 `yyyy-MM-dd` |
| `transferRef` | `String/null` | 转账凭证号 |
| `advanceSettledFlag` | `Boolean/null` | 预支是否已结清 |
| `signedVoucher` | `Object/null` | 签字凭证信息 |
| `generatedBy` | `String/null` | 生成人 ID |
| `generatedByName` | `String/null` | 生成人姓名 |
| `generatedAt` | `String/null` | 生成时间,格式 `yyyy-MM-dd'T'HH:mm:ss` |
| `confirmedBy` | `String/null` | 确认人 ID |
| `confirmedByName` | `String/null` | 确认人姓名 |
| `confirmedAt` | `String/null` | 确认时间,格式 `yyyy-MM-dd'T'HH:mm:ss` |
## 6. 枚举 / 数据字典
### 6.1 `category`SettlementCategory
**所属字段**:路径参数 `category`、响应 `items[].category` | **类型**`String`
| 值 | 中文 | 说明 |
|----|------|------|
| `HOTEL` | 住宿 | 住宿核单明细 |
| `TICKET` | 门票/游玩项目 | 门票和游玩项目核单明细 |
| `MEAL` | 餐食 | 餐食费用明细 |
| `VEHICLE` | 车辆 | 车辆费用明细 |
| `GUIDE` | 导游 | 导游费用明细 |
| `PHOTOGRAPHER` | 摄影 | 摄影费用明细 |
| `OTHER_INCOME` | 其他收入 | 其他收入明细 |
| `OTHER_EXPENSE` | 其他支出 | 其他支出明细 |
### 6.2 `confirmStatus`(兼容状态)
**所属字段**`items[].confirmStatus` | **类型**`String`
| 值 | 中文 | 说明 |
|----|------|------|
| `UNCONFIRMED` | 未确认 | 兼容旧确认投影;不再阻止生成主报账表 |
| `CONFIRMED` | 已确认 | 兼容旧确认投影;不再作为后续流程门禁 |
| `STALE` | 已变化 | 兼容旧确认投影;不再作为后续流程门禁 |
### 6.3 `reportStatus`SettlementReportStatus
**所属字段**`reportStatus` | **类型**`String`
| 值 | 中文 | 说明 |
|----|------|------|
| `GENERATED` | 已生成 | 主报账表已生成,尚未确认 |
| `CONFIRMED` | 已确认 | 主报账表已确认 |
| `STALE` | 来源已变化 | 当前来源指纹与已保存报账表不一致 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询、兼容确认或生成主报账表成功 |
| `400` | 请求参数错误 | `orderId` 非法、兼容确认接口缺少请求体、`expectedSourceFingerprint` 不是 64 位小写十六进制、`confirmEmpty` 缺失 |
| `404` | 接口或资源不存在 | 路径不存在,或访问不存在的订单 |
| `584315` | 核单来源数据已变化,请刷新后重新生成 | 报告来源指纹变化 |
| `584317` | 当前报告状态不允许执行该操作 | 当前核单状态不允许生成或确认报告 |
| `584319` | 核单存在未知分类或历史迁移数据不完整 | `category` 不是 §6.1 中的值 |
| `584320` | 核单分类明细尚未保存完整或数据不可用于报账 | 生成主报账表时,某个分类明细缺必填业务信息或不可用于报账;响应会带具体分类名 |
## 8. 示例
### 8.1 典型成功:未逐类确认也可生成主报账表
**请求**
```http
POST /v3/admin/order/60001/settlement/reports/reimbursement/generate
Authorization: Bearer <token>
```
无请求体。
**响应**
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "910000000000000001",
"orderId": "60001",
"reportStatus": "GENERATED",
"sourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"primaryReporterId": "11001",
"primaryReporterName": "张三",
"primaryReporterRole": "GUIDE",
"reportVersion": 1,
"driverCollectedTailAmount": 0.00,
"approvedAdvanceAmount": 2000.00,
"reportablePaidCostAmount": 8300.00,
"reporterNetAmount": 6300.00,
"primaryReporterCollectedAmount": 0.00,
"publicPrepaidAmount": 1000.00,
"primaryReporterDueAmount": 6300.00,
"advanceOutstandingAmount": 0.00,
"reconNetAmount": 6300.00,
"transferDirection": "PAY_TO_REPORTER",
"transferAmount": 6300.00,
"incomeLines": [],
"expenseLines": [
{
"category": "HOTEL",
"categoryName": "住宿",
"amount": 3600.00
}
],
"advanceLines": [],
"vehicleLines": [],
"transferStatus": "PENDING",
"transferDate": null,
"transferRef": null,
"advanceSettledFlag": false,
"signedVoucher": null,
"generatedBy": "11",
"generatedByName": "旧核单员",
"generatedAt": "2026-07-27T10:15:30",
"confirmedBy": null,
"confirmedByName": null,
"confirmedAt": null
}
}
```
### 8.2 边界情况:查询兼容状态仍返回 `allConfirmed=false`
**请求**
```http
GET /v3/admin/order/60001/settlement/category-checks
Authorization: Bearer <token>
```
无请求体。
**响应**
```json
{
"code": 200,
"msg": "success",
"data": {
"orderId": "60001",
"allConfirmed": false,
"items": [
{
"category": "HOTEL",
"categoryName": "住宿",
"rowCount": 1,
"empty": false,
"sourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"confirmStatus": "UNCONFIRMED",
"confirmedBy": null,
"confirmedByName": null,
"confirmedAt": null
}
]
}
}
```
### 8.3 业务失败:分类明细未保存完整
**请求**
```http
POST /v3/admin/order/60001/settlement/reports/reimbursement/generate
Authorization: Bearer <token>
```
无请求体。
**响应**
```json
{
"code": 584320,
"msg": "核单分类「住宿」明细尚未保存完整或数据不可用于报账",
"data": null
}
```
## 9. 业务边界
- **适用场景**:管理后台核单流程;分类明细已保存完整后生成主报账人报账表。
- **不适用场景**:继续用 `allConfirmed=true` 作为“生成主报账表”“生成单团核算表”“Step6 提交”“财务确认”的前置条件。
- **特殊边界**`POST /category-checks/:category/confirm` 仍可能返回 200,但它只是兼容旧调用,不代表新流程需要或应该调用。
- **明细完整性口径**:生成主报账表时,八个分类都必须存在可用于报账的明细快照;缺少分类、金额非法、业务必填项为空或来源数据不可用时返回 `584320`
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `allConfirmed` | 后续流程可能按该字段判断八分类是否已全部确认 | 字段保留兼容,但不再作为后续流程门禁 |
| `items[].confirmStatus` | `UNCONFIRMED` / `CONFIRMED` / `STALE` 可能影响页面下一步按钮 | 字段保留兼容,但不再作为后续流程门禁 |
| `SettlementCategoryConfirmReqVO.expectedSourceFingerprint` | 分类确认接口必填 | 仍为兼容接口必填;新前端停止调用该接口 |
| `SettlementCategoryConfirmReqVO.confirmEmpty` | 分类确认接口必填 | 仍为兼容接口必填;新前端停止调用该接口 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 主报账表生成 | 要求八个分类确认状态全部满足手动确认口径 | 核单明细保存完整即可生成 |
| 分类确认按钮 | 前端需要逐分类调用确认接口 | 前端停止调用确认接口,并移除“本分类已确认”入口 |
| 单团核算 / Step6 / 财务确认门禁 | 可能间接受八分类确认状态影响 | 不再读取八分类手动确认状态作为门禁 |
| 明细不完整时生成主报账表 | 可能表现为八分类未确认或来源变化类提示 | 返回 `584320`,提示具体分类明细未保存完整或不可用于报账 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:否。旧查询字段和旧确认接口保留,但确认接口已废弃。
- **前端是否必须同步上线**:建议同步。前端应移除“本分类已确认”按钮、`allConfirmed` 门禁和基于 `confirmStatus` 的下一步禁用逻辑。
- **影响已有数据**:不要求前端迁移数据;历史确认状态仅作为兼容显示值。
### 11.2 回滚方案
- **回滚方式**:如需恢复旧流程,回滚 PR #5274 对应后端变更。
- **回滚后清理**:前端若已移除按钮,回滚后需要恢复八分类确认入口和 `allConfirmed` 门禁。
## 12. 注意事项
- 管理后台不要再新增对 `POST /category-checks/:category/confirm` 的调用。
- 页面上原“本分类已确认”按钮、确认进度提示和 `allConfirmed=false` 禁用下一步的逻辑可以移除。
- 查询分类状态接口可继续用于兼容老页面,但不要把 `UNCONFIRMED``STALE` 解释为主报账表不可生成。
- 生成主报账表失败时优先识别 `584320`,它表示需要补齐对应分类明细,而不是要求点击分类确认。
## 验证证据
- 后端 PR[#5274](https://git.1814.love:8443/wx/HL/pulls/5274)。
- 合并提交:[`8635973e6`](https://git.1814.love:8443/wx/HL/commit/8635973e6)。
- 实现提交:[`07ac323a1`](https://git.1814.love:8443/wx/HL/commit/07ac323a1)。
- Source frontmatter 已记录后端部署完成、网关验证通过;前端按本交接独立完成消费与验证。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5264](https://git.1814.love:8443/wx/HL/issues/5264)
- **PR**: [#5274](https://git.1814.love:8443/wx/HL/pulls/5274)
- **Merge commit**: [8635973e6](https://git.1814.love:8443/wx/HL/commit/8635973e6)
- **Implementation commit**: [07ac323a1](https://git.1814.love:8443/wx/HL/commit/07ac323a1)
### 13.2 联系人
- **后端负责人**: @yaosu
- **前端对接**: 管理后台前端

查看文件

@ -1,229 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5279"
title: "车务派单全链路 E2E 前端缺陷交接"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin"
frontend_ref: "2ee0988c235c0483a5b51f8cc0deb195d7fd8573"
target_release: ""
verified_at: ""
status_note: "本次没有新增或修改后端字段、类型、必填性、路径和错误码;测试环境当前契约已验证。前端需修正动作码、部分日期、HOLD 费用草稿、刷新重置和冲突日期等消费逻辑。"
updated_at: "2026-07-27"
base: "dev-v3"
---
# 车务:派单全链路 E2E 前端缺陷交接
> **服务**: `hl-fleet-service``hl-order-service-v3`
>
> **工单**: [wx/HL#5279](https://git.1814.love:8443/wx/HL/issues/5279)
>
> **前端基线**: `mmg/hl-ui v2.1@5e6718f952ed156c2e71bc38d08c3482e72ba744`
>
> **影响范围**: 车务管理 → 派单看板、矩阵派单、派车弹窗、派单详情;订单详情 → 用车需求调整
## 结论
这是一份**当前契约消费纠错和前端缺陷交接**,不是后端接口变更:
- 后端 #5275 已保证部分日期派车只消费目标切片,剩余日期和稳定槽位守恒;
- 后端 #5277 已恢复完整已派需求 `canAssign=true + CHANGE_ASSIGNMENT`
- 测试环境通过真实后台 UI 完成 HOLD、DIRECT、司机确认、最终确认、司机拒接、改车、改司机、需求驳回/换版/重派、逐日费用、免费服务日、冲突和重复提交;
- 当前剩余阻断均可在最新前端稳定复现,后端不应增加旧动作码别名或重复状态来迁就页面。
## 变更接口
本次不新增接口、字段或错误码,仅纠正以下现有管理后台接口的前端消费:
| 方法 | 路径 | 当前契约用途 |
| --- | --- | --- |
| `GET` | `/admin/fleet/board/orders` | 看板代表行、能力字段、稳定槽位和未派切片上下文 |
| `GET` | `/admin/fleet/board/orders/<orderId>` | 当前有效派车组、逐日费用和独立生命周期 |
| `POST` | `/admin/fleet/assignments/candidates` | 车辆/司机可用窗口与真实冲突明细 |
| `POST` | `/admin/fleet/assignments` | HOLD/DIRECT 首次派车 |
| `POST` | `/admin/fleet/assignments/<assignmentId>/change` | 按稳定槽位改车、改司机或同时改派 |
| `POST` | `/admin/fleet/assignments/<assignmentId>/driver-confirmation` | 登记司机回复 |
| `POST` | `/admin/fleet/assignments/<assignmentId>/confirm` | 最终确认执行 |
| `POST` | `/admin/fleet/assignments/<assignmentId>/driver-reject` | 司机拒接并退回未派 |
| `DELETE` | `/admin/fleet/assignments/<assignmentId>` | 取消派单 |
| `POST` | `/admin/fleet/assignments/<assignmentId>/early-complete` | 提前完结 |
## 一、动作码必须消费后端当前值
权威字段来自:
```http
GET /admin/fleet/board/orders
GET /admin/fleet/board/orders/<orderId>
```
前端按 `canAssign``availableActionCodes` 渲染,不按中文状态或本地别名推断。
| 生命周期 | `currentStep` | 当前动作码 | 正确页面行为 |
| --- | ---: | --- | --- |
| `unassigned` | 2 | `ASSIGN`, `REJECT_REQUIREMENT` | 派车派人、驳回需求 |
| `holding_wait_driver` | 3 | `CHANGE_ASSIGNMENT`, `RECORD_DRIVER_CONFIRMATION`, `DRIVER_REJECT`,出团前/当天另有 `CANCEL_ASSIGNMENT` | 继续派车、司机拒接、改派、取消 |
| `driver_confirmed` | 4 | `CHANGE_ASSIGNMENT`, `CONFIRM_EXECUTION`,出团前/当天另有 `CANCEL_ASSIGNMENT` | 恢复第 4 步确认执行;不得只剩改派 |
| `assigned` 且未过出团日 | 4 | `CHANGE_ASSIGNMENT`, `CANCEL_ASSIGNMENT` | 改派、取消派单 |
| `assigned` 且已过出团日 | 4 | `CHANGE_ASSIGNMENT`, `EARLY_COMPLETE` | 改派、提前完结 |
| `completed/canceled` | 终态 | 无可写动作 | 只读 |
当前前端存在三处精确错位:
1. 检查 `CANCEL`,而后端下发 `CANCEL_ASSIGNMENT`
2. 检查 `COMPLETE_EARLY`,而后端下发 `EARLY_COMPLETE`
3. `driver_confirmed` 已下发 `CONFIRM_EXECUTION`,但卡片和详情没有恢复确认执行入口。
`DRIVER_REJECT``RECORD_DRIVER_CONFIRMATION` 是现行值,继续沿用。不要让后端同时下发新旧两套动作码。
## 二、部分日期稳定槽必须保留用户点击上下文
### 复现
测试单 `26-6263` 的同一 `assignmentSlotId`
- 07-30、07-31已有 HOLD;
- 08-01唯一剩余 `unassigned`
- 看板卡片正确显示“用车 08-01 · 1天”;
- 8 月矩阵未派池也只显示 08-01。
但当前前端:
- 从看板进入 Step 2 后改成编辑 07-30/07-31 的 active HOLD;
- 从矩阵车辆 08-01 空闲格进入时,顶部虽显示“有效服务日期08-01”,Step 2 却显示 `已选 0/0 天`,继续复用 07-30/07-31 费用并返回 0 个候选。
### 正确规则
- 首次派车 `mode=assign` 必须保留用户点击的看板代表行:`assignmentId/assignmentSlotId/fleetItemIndex/startDate/endDate`;详情异步返回后不能被 `currentAssignment` 覆盖;
- 矩阵入口以 `entryContext.clickedDate + startDate + endDate` 与订单合法服务段求交集;本例结果固定为 08-01;
- `currentAssignment/activeAssignments[]` 只用于改派和确认已有有效派车组,不代表未派切片;
- 实际计费服务日为空时,应从已经校验通过的 `assignmentDateRange` 回退生成,不得读取另一 active 组的逐日费用;
- 提交仍使用原稳定 `assignmentSlotId`,只消费 08-01,不得重建第二槽位或覆盖 07-30/07-31。
## 三、HOLD 手工补价不能在司机确认后丢失
### 复现
`26-9140` 改派时:
- 07-30/07-31 使用日历价 860;
- 08-01 日历缺价,页面提交覆盖价 860 和调价原因;
- `/change` 返回并落库总价 2580;
- 派单详情也正确展示 08-01 覆盖价和原因。
登记司机确认后,Step 4 又从候选价格日历重建费用,08-01 变为“待补价”,总价变为“价格不完整”,`validateVehicleFeeDraft` 阻断最终确认。
### 正确规则
- HOLD 已创建后,`activeAssignments[]/currentAssignment``dailyVehicleFees``vehicleFeeTotal``vehicleFeeAdjustmentReason``chargeableServiceDates``vehicleFeeWaiverReason` 是当前派车组权威快照;
- 候选接口的价格只用于新选车草稿或日历参考,不得覆盖已经保存的 HOLD 费用;
- 第 3 步登记司机回复和第 4 步确认执行都不能清空已保存覆盖价;
- 详情重拉后应按 `assignmentId/assignmentGroupId` 恢复同一派车组快照。
## 四、后台刷新不得重置正在编辑的派车草稿
实测派车弹窗打开后,后台订单刷新会触发整套初始化:
- 候选接口已返回 19 辆车和 11 名司机,但两个列表被清空,计数/筛选项仍保留;
- 用户已切换 DIRECT,提交前又恢复为 HOLD;
- 再点一次筛选会重新请求并恢复候选。
前端应做到:
- `props.order` 仅因列表/SSE 刷新生成新对象时,不重置当前弹窗;
- 只有订单 ID、目标需求 ID、目标稳定槽位、模式或用户主动关闭/重开变化时才重新初始化;
- 已编辑车辆、司机、日期、收费日、逐日价、原因、跨常驻确认和 HOLD/DIRECT 模式全部保留;
- 若服务端基线真的变化,使用现有 baseline-difference 强提示并让用户决定,不静默改写草稿。
## 五、多槽 HOLD 必须按槽位推进
`26-5256` 有两个稳定车辆槽位。槽位 1 登记司机确认后:
- 详情真值为槽位 1 `driver_confirmed`、槽位 2 `holding_wait_driver`
- 卡片却仍把代表司机显示为“待回复”;
- “继续派车”入口消失,槽位 2 无法登记司机回复。
正确行为:
- 订单只要任一 active 槽位包含 `RECORD_DRIVER_CONFIRMATION`,卡片和详情保留“继续派车”;
- 打开后默认选中最早待回复槽位,也允许切换其它槽位;
- 一个槽位确认不得覆盖或隐藏另一个槽位状态;
- 订单聚合步骤取最慢槽位,代表卡文案必须明确“代表槽位”,不能冒充全单状态。
## 六、冲突窗口字段不能混用
候选响应同时包含:
| 字段 | 含义 | 页面用途 |
| --- | --- | --- |
| `conflicts[]` | 真正占用冲突;`blocking=true` 的日期禁止选择 | 展示冲突订单与冲突日期 |
| `availabilityWindows[]` | 当前查询区间内仍可用的连续窗口 | 仅在资源可用/部分可用提示中展示 |
| `availabilityReasonCode` | `AVAILABLE``ASSIGNMENT_CONFLICT` 等判定码 | 决定禁用和提示类型 |
| `availabilityReasonMessage` | 后端判定文案 | 展示主提示 |
实测后端冲突是 08-01,`availabilityWindows` 是 08-02..08-04;页面却显示“存在派单冲突 · 08-02 至 08-04”。禁用结果正确,日期解释相反。
冲突文案应从 `conflicts[].startDate/endDate` 汇总;`availabilityWindows` 不得拼在冲突文案后。
## 七、提交与刷新反馈
### 1. 防重复提交
真实 UI 同一时刻双击 DIRECT 发出两个不同 `requestId`
- 第一笔成功并只生成一个有效派车;
- 第二笔被后端守恒门禁拒绝,返回 `605033``用车需求完成回写处理中,请稍后重试`
前端应在第一笔进入提交函数时立即短路后续点击,并在按钮、快捷键和程序触发路径共用同一 `submitting` 门禁。同一草稿的网络重试应复用 requestId,不应每次生成新值。
### 2. 司机拒接后的详情
司机拒接成功后,卡片立即变为未派,但当前详情抽屉仍保留旧 HOLD 车辆、费用和动作。关闭重开才正确。成功后需用最新服务端详情整体替换旧对象,不得继续把 mutation 前的行快照合并回来。
### 3. 车务角色快捷入口
车务角色下顶部可见“订单列表”,点击却进入未注册 `/housekeeper/orders` 并显示 404。未注册/无权限时不要展示快捷入口;有权限时应确保动态路由已注册再导航。
## 八、前端验收清单
- [ ] `CANCEL_ASSIGNMENT` 显示并执行取消;不再检查 `CANCEL`
- [ ] `EARLY_COMPLETE` 显示并执行提前完结;不再检查 `COMPLETE_EARLY`
- [ ] `driver_confirmed + CONFIRM_EXECUTION` 可从卡片和详情恢复第 4 步。
- [ ] 多槽 HOLD 任一槽位待回复时保留“继续派车”,逐槽推进且代表状态不串槽。
- [ ] `26-6263` 从看板和 8 月矩阵都只编辑/提交 08-01,07-30/07-31 不变。
- [ ] 已保存 HOLD 的手工逐日价、调价原因和免费日配置在司机确认、详情刷新、最终确认之间保持不变。
- [ ] SSE/列表刷新不清空候选、车辆司机草稿或切换 HOLD/DIRECT 模式。
- [ ] 冲突日期取 `conflicts[]`,可用窗口取 `availabilityWindows[]`,两者文案不混用。
- [ ] DIRECT/HOLD 第一击后立即禁用所有重复提交路径;同草稿重试复用 requestId。
- [ ] 司机拒接后当前卡片和详情同时刷新到 unassigned。
- [ ] 车务角色的订单快捷入口不再进入 404。
## 验证证据
- 后端 `dev-v3@79297c838b62af2426cc83ad0e78cffbd961096b` 已部署,Fleet 8087/8187 两实例健康;
- #5277 Fleet reactor 2,427 项通过,0 失败、0 错误、1 跳过;Spotless 通过;
- 网关验证:完整 assigned 单槽和三槽均为 `canAssign=true + CHANGE_ASSIGNMENT`
- 真实 UI 写入通过HOLD、DIRECT、司机确认、最终确认不发送短信、司机拒接、只换车、只换司机、同时换车司机、需求驳回、V2/V3 换版重派、收费/免费日、重复提交;
- 后端守恒:重复 DIRECT 只生成一笔有效派车;需求 `DONE_ADJUST` 返回 `assignmentDeletedCount=1`,旧资源随后可重新选择;
- 部分日期后端真值07-30/07-31 为 HOLD,08-01 唯一 unassigned,三天共用稳定槽位,无重复/孤儿;
- 关键页面截图:`fleet-partial-continuation-broken.png``5277-assigned-reassign-restored.png`
- 完整逐步证据由 #5279 工单评论和测试记录保留。
## 十、契约审查
- 本文件不对应新的 Controller、VO、字段、类型、必填性、动作码或错误码变更;
- OpenAPI/oasdiff`not_required`,没有新的 `frontend_api` diff;
- Consumer Contract`not_required`,没有 Feign/shared Java 变化;
- 当前动作码由 `AssignmentLifecycleResolver` 和现有服务测试确认;部署网关响应与源码一致;
- 前端代码审查确认当前仍检查 `CANCEL``COMPLETE_EARLY`,且没有消费 `CONFIRM_EXECUTION`
## 不影响范围
- 不修改 `hl-ui`,不在 `mmg/hl-ui` 建工单;本文件 `frontend_status` 保持 `pending`
- 不要求后端新增兼容动作码、重复字段、特殊前端分支或放松资源/幂等守恒;
- 不修改派车状态机、计费公式、库存/占用、保险、通知、Outbox 或数据库结构;
- 不把前端实现、发布、页面验收纳入后端工单关闭条件。

查看文件

@ -1,99 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5282"
title: "矩阵年度月度订单统计"
consumer: "admin"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "7d0589d6"
target_release: ""
verified_at: ""
status_note: "管理后台已由提交 7d0589d6 完成年度月度订单统计消费并通过验证"
updated_at: "2026-07-27"
base: "dev-v3"
generated: "2026-07-27T10:04:56+08:00"
---
# 矩阵年度月度订单统计
> 后端契约已部署并经测试网关验证;`frontend_status` 独立反映管理端交付状态。
## 关联
- Issue: #5282
- PR: wx/HL#5286
## 变更接口
### `GET /admin/fleet/matrix/month-counts`
一次查询指定年份的矩阵月度订单状态统计,统计口径与 `GET /admin/fleet/matrix/grid``statusCounts` 一致。
请求参数:
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `year` | query | `Integer` | 是 | 沿用矩阵 `YearMonth` 校验,越界返回 `605010` |
| `season` | query | `String` | 否 | 不传或空白时按 `active` 处理 |
| `fleetTeamIds` | query | `Long[]` | 否 | 可重复参数;空数组表示不过滤车队 |
| `typeKeys` | query | `String[]` | 否 | 可重复参数;空数组表示不过滤车型 |
响应 `data`
```json
{
"year": 2026,
"months": [
{
"month": 1,
"statusCounts": {
"totalAssignments": 0,
"unassignedAssignments": 0,
"assignedAssignments": 0,
"totalOrders": 0,
"unassignedOrders": 0,
"partialOrders": 0,
"assignedOrders": 0
}
}
]
}
```
- `months` 固定返回 12 项,按 `month=1..12` 升序;无订单月份不省略,各计数字段为 `0`
- 一条跨月派车记录按实际服务日期覆盖的月份分别计数;已取消、已关闭订单沿用矩阵现有口径排除。
- 年份越界沿用矩阵业务错误码 `605010`;缺失必填参数沿用统一参数校验响应。
## 契约影响文件
- `hl-fleet-service/src/main/java/com/hulalv/fleet/matrix/controller/MatrixController.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/matrix/vo/MatrixMonthCountsReqVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/matrix/vo/MatrixMonthCountsRespVO.java`
- `hl-fleet-service/src/test/java/com/hulalv/fleet/matrix/controller/MatrixControllerTest.java`
## 前端/调用方动作
- 车务矩阵页面按当前年份、赛季、车队和车型筛选请求本接口。
- 月份选择器读取对应月份的 `statusCounts.totalAssignments`;有效零值显示 `0`,请求尚未完成或失败显示 `--`
- 跨年切换时按目标年份加载;可按筛选键缓存结果,筛选变化后重新获取。
- 数组查询参数必须使用重复 keyAxios `paramsSerializer``indexes` 设为 `null`),不要发送带下标的参数名。
## 兼容性与路由
- 新增 GET 路径,不修改既有路径、请求参数、响应字段、枚举或错误码,对既有消费者向后兼容。
- 网关已有 `Path=/admin/fleet/**` 路由覆盖,无需新增网关配置。
- 无 internal Feign 或 shared Java 契约变更。
## 验证证据
- 后端定向测试:`MatrixServiceTest,MatrixControllerTest` 共 39 项通过,覆盖固定 12 月零值、跨月、终态排除、筛选、数组绑定、JSON 和与 grid 的统计守恒。
- 后端完整门禁:`mvn -pl hl-fleet-service -am verify` 通过2434 tests,0 failures/errors,1 skipped;Fleet Spotless 626 文件通过。
- 测试部署PR `wx/HL#5286` 合入 `dev-v3`,Deploy Panel 任务 `57a121a0` 成功,8087/8187 双实例健康。
- 测试网关2026 年返回固定 12 项且字段与派单/订单守恒通过;2099 年固定 12 项且全部计数为 `0`。脱敏证据:`C:/Users/Administrator/AppData/Local/hl-workflow/evidence/5282/gateway-month-counts.json`
- DB 地面真相:`not_verified`;现有只读探针的数据源安全守卫拒绝本地地址,未绕过门禁。零值另由 2099 网关实测与 Service 测试覆盖。
- 管理端本地实现:全量 Vitest 125 文件 / 1160 项通过;生产构建、ESLint 通过;6 个改动文件 Prettier 检查通过。因前端分支尚未推送/发布,frontmatter 仍保持 `frontend_status: pending`
- OpenAPI diff`not_configured`。当前仓库仅提供 Swagger 2,且本机未配置 `oasdiff`;已人工核对路径、方法、参数、响应、空态、错误码、网关与消费者,未伪报自动 diff 通过。
- 人工契约证据:`C:/Users/Administrator/AppData/Local/hl-workflow/evidence/5282/contract-review.md`

查看文件

@ -1,106 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5283"
title: "车务派车候选响应被订单刷新清空"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin"
frontend_ref: "2ee0988c235c0483a5b51f8cc0deb195d7fd8573"
target_release: ""
verified_at: ""
status_note: "后端候选接口契约与日期冲突口径正常;前端 AssignModal 因同订单对象刷新而重复初始化,清空已成功返回的候选。"
updated_at: "2026-07-27"
base: "dev-v3"
---
# 车务派车候选响应被订单刷新清空
> **服务**: `hl-fleet-service`
>
> **后端工单**: [wx/HL#5283](https://git.1814.love:8443/wx/HL/issues/5283)
>
> **前端消费端**: `mmg/hl-ui v2.1`
>
> **影响页面**: 车务管理 → 派车看板/矩阵 → 派车弹窗 `AssignModal`
## 结论
本次不是后端候选过滤或 API 契约缺陷,不新增或修改接口、字段、类型、必填性、错误码及派单状态机:
- 同一真实请求经测试网关返回 `vehicleTotal=19`,其中 11 辆 `AVAILABLE`,其余车辆因真实日期冲突不可用;
- 专用测试车辆在目标日期区间均返回 `AVAILABLE`,车辆、车型、车队和维保状态未造成错误排除;
- 页面显示“共 0 辆”发生在响应成功之后:看板刷新/SSE 以新对象替换同一订单,`AssignModal` 再次执行 `resetOptionPages()` 并递增 `requestSeq`,从而清空候选并丢弃已返回结果;
- 不得通过放宽后端车辆/车队/车型或冲突过滤来掩盖前端状态重置问题。
该问题也属于 [#5279 车务派单全链路 E2E 前端缺陷交接](./27_5279_车务派单全链路E2E前端缺陷交接-修改接口-管理后台.md)“后台刷新不得重置正在编辑的派车草稿”的同类消费缺陷;#5283 补充了候选 19/11 的独立网关证据和稳定初始化键要求。
## 变更接口
```http
POST /admin/fleet/assignments/candidates
Content-Type: application/json
Authorization: Bearer <admin-token>
```
接口继续返回车辆、司机两套独立分页候选。前端应消费 `vehicles.records/total``drivers.records/total`(兼容别名 `list` 仍保留),并以 `available``availabilityReasonCode``conflicts[]``availabilityWindows[]` 展示可用性;本次没有后端契约变化。
## 前端修复口径
### 1. 以稳定业务身份决定是否重新初始化
不得仅监听 `props.order` 对象身份。应使用 `initializationIdentity` 或等价稳定键,至少覆盖:
- `orderId`
- 当前 `requirementId`
- 目标 `assignmentSlotId/fleetItemIndex`
- `assign/change` 模式及必要入口上下文。
只有上述业务身份真正变化,或用户主动关闭后重新打开弹窗时,才允许执行 `resetOptionPages()` 和重建派车草稿。同一订单仅因列表刷新或 SSE 生成新对象时不得重置。
### 2. 保留候选与编辑草稿
同一稳定业务身份下刷新订单对象时,必须保留:
- 车辆/司机候选 `records/total/page/pageSize`
- 车队、车型、关键字和可用性筛选;
- 已选车辆、司机、日期和稳定槽位;
- 收费服务日、逐日价格、调价/免费原因;
- 跨常驻确认以及 `HOLD/DIRECT` 模式。
若服务端基线确实变化,继续使用现有 baseline-difference 强提示,由用户决定如何处理;不得静默清空或改写草稿。
### 3. 保留并发响应保护,但不得误杀当前请求
继续保留 `requestSeq` 或等价的旧响应隔离机制。只有新业务身份或新查询真正开始时才递增序号;同一订单对象替换不得把已经成功返回的当前候选标记为过期。快速连续刷新时,旧请求不能覆盖新请求,新请求成功结果也不能被无关初始化清空。
## 前端验收清单
- [ ] 专用测试订单 `26-4700``2026-08-04..2026-08-07`、SUV、3 人请求返回后,页面候选数量与接口一致,不再错误显示 0。
- [ ] 相同订单、需求、槽位和模式下,列表/SSE 替换订单对象后,车辆和司机候选仍保留。
- [ ] 候选分页总数、筛选条件、已选项和费用草稿不被后台刷新清空。
- [ ] 已切换的 `HOLD/DIRECT` 模式不会因同订单刷新恢复默认值。
- [ ] 订单、需求、槽位或模式真实变化时仍能正确重新初始化。
- [ ] 快速连续刷新时,旧响应不能覆盖新响应,当前成功响应也不会被误判过期。
- [ ] Network 记录仍使用现有候选接口,无新增或修改后端请求契约。
## 验证证据
- 测试网关与 Fleet 双实例同口径响应:`code=200`、车辆总数 19、可用 11、真实冲突 8;
- 目标测试车辆均为 `AVAILABLE`,目标日期冲突数为 0;
- 后端 worktree 零代码改动,当前候选过滤、日期闭区间冲突和 API 契约保持不变;
- 前端 HMR 现场曾出现稳定 `initializationIdentity` 方向的修正,但尚无 `mmg/hl-ui v2.1` 远端提交与发布验收依据,因此本文件保持 `frontend_status: "pending"`
## 契约审查
- Frontend API现有接口消费纠错,无 Controller/DTO/VO/字段变化,OpenAPI diff 为 `not_required`
- Internal Feign / shared Java无变化,Consumer Contract 为 `not_required`
- 后端真值由已部署测试环境网关响应和现有 Fleet 源码确认;changelog 发布不代表前端已实现或已发布。
## 不影响范围
- 不修改 `hl-ui`,不在 `mmg/hl-ui` 创建工单;
- 不修改后端候选过滤、日期冲突、车辆/司机占用、派单状态机、计费、保险、通知或数据库;
- 不把前端实现、发布和页面验收冒充为本次后端代码交付。

查看文件

@ -1,145 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5284"
title: "建议车辆槽位由车务自由删减并按最终实派方案提交"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "d459b946"
target_release: ""
verified_at: ""
status_note: "最终实派方案行为已由 #5262 部署;#5284 收口既有接口语义并纠正 #5245 旧前端口径;管理后台已由提交 d459b946 完成槽位自由删减和最终方案 batch 提交并通过验证。"
updated_at: "2026-07-27"
base: "dev-v3"
---
# 车务:建议车辆槽位由车务自由删减
> **服务**: `hl-fleet-service`
>
> **工单**: [wx/HL#5284](https://git.1814.love:8443/wx/HL/issues/5284)
>
> **影响范围**: 车务管理 → 派单看板 → 订单派车弹窗的车辆槽位编辑与批量提交
## ⚠️ 关键纠错
截图中默认出现但没有删除入口的“车辆槽位 1”,是页面依据订单当前用车需求
`requiredVehicles[]` 展开的**定制师/订单建议槽位**,不是车务必须保留的最终槽位。
#5245 曾写“只删除新增且未提交的草稿槽位”。该表述只是在强调不能用本地删除撤销已有派车,
但被页面实现成了“建议生成的初始槽位不可删除”。**这个实现口径需要纠正:所有尚未提交的本地槽位都可删除,
包括建议生成的初始槽位和车务后加的槽位。最终配几辆、配什么车型由车务决定。**
边界保持不变:
- 编辑态允许暂时删至 0 个槽位,并保留“添加车辆槽位”入口;
- 最终提交时 `items[]` 仍必须包含 1–20 个完整槽位;
- 已经存在的 `holding/assigned/completed` 有效派车不是本地草稿,不能直接删除,继续走取消或改派流程。
## 变更接口
本次不新增 JSON 字段、路径或错误码,只明确现有接口的权威语义:
| 方法 | 路径 | 当前权威语义 |
| --- | --- | --- |
| `GET` | `/admin/fleet/board/orders` | `requiredVehicles[]``assignmentProgress.suggestedSlots` 是订单建议,仅作参考 |
| `GET` | `/admin/fleet/board/orders/<orderId>` | `suggestedVehicleCount` 是建议数量,`actualVehicleCount` 是车务当前实派数量 |
| `POST` | `/admin/fleet/assignments/batch` | `items[]` 是车务本次保留的**完整最终实派方案**,无需覆盖全部建议槽位 |
## 批量提交契约
### `items[]` 是完整最终方案
假设订单建议 2 辆 SUV,页面可以删除两个建议槽位后重新添加 1 个槽位,并提交:
```json
{
"orderId": "2080000000000000001",
"requirementId": "2080000000000000002",
"startDate": "2026-08-04",
"endDate": "2026-08-07",
"holdMode": 1,
"requestId": "fleet-final-plan-example",
"items": [
{
"fleetItemIndex": 5,
"vehicleId": "2080000000000000101",
"driverId": "2080000000000000201"
}
]
}
```
规则:
- `items[]` 数量可以少于、等于或多于建议数量,但必须为 1–20;
- `fleetItemIndex` 可沿用建议索引,也可使用未占用的新索引,不要求连续;
- 批内 `fleetItemIndex`、车辆和司机各自唯一;
- 未被最终方案保留的 `unassigned` 建议占位会退出待派和完成条件;
- 若漏传已有 `holding/assigned` 等在途槽位,后端拒绝整批提交并提示先取消,不会静默删除已有派车;
- 任一槽位失败仍整批回滚,前端不得循环调用单派接口替代批量提交。
### 看板建议数与实派数分离
最终方案提交后可能出现:
```json
{
"assignmentProgress": {
"suggestedSlots": 2,
"finalizedByFleet": true,
"totalSlots": 1,
"unassignedSlots": 0,
"holdingSlots": 1,
"assignedSlots": 0,
"completedSlots": 0,
"canceledSlots": 0
}
}
```
此时 `suggestedSlots=2` 只保留建议事实,页面进度、完成条件和最终车辆数都按 `totalSlots=1` 计算。
## 前端展示矩阵
| 场景 | 数据源 | 页面行为 | 守恒规则 |
| --- | --- | --- | --- |
| 订单建议 | `requiredVehicles[]``suggestedSlots` | 标注“建议”,只作为车型/座位/数量参考 | 不决定最终槽位数 |
| 建议生成的初始槽位 | 前端未提交草稿 | 与新增草稿相同,显示删除入口 | 可见草稿槽位均可删除 |
| 车务新增槽位 | 前端未提交草稿 | 显示删除入口 | 可见草稿槽位均可删除 |
| 删除到 0 个 | 本地草稿为空 | 展示空态和“添加车辆槽位”,禁用下一步/提交 | 编辑态可为 0,提交态最少 1 |
| 最终批量提交 | `items[]` | 只提交车务最终保留的槽位,不补回已删除建议 | 可见最终槽位与 `items[]` 一一对应 |
| 已有有效派车 | `activeAssignments[]` | 不显示本地“删除草稿”;使用取消/改派动作 | 不静默撤销 `holding/assigned` |
| 最终进度 | `assignmentProgress` | `finalizedByFleet=true` 后按 `totalSlots` 展示 | 状态数量之和等于最终 `totalSlots` |
## 前端处理清单
- [ ] 移除 `slot.isNewDraft === true` 对删除按钮的唯一门控;建议生成的未提交初始槽位也必须可删除。
- [ ] 删除任一未提交槽位后同步清理该槽位的车辆、司机、逐日车费和跨常驻确认草稿,不能残留参与提交。
- [ ] 删除当前槽位后切到相邻槽位;删至 0 个时进入明确空态,不读取已删除槽位的 picker 状态。
- [ ] 0 槽位时保留“添加车辆槽位”,并禁用下一步/最终提交;不要向后端发送空 `items[]`
- [ ] 最终 payload 只由当前可见槽位生成;不得按 `requiredVehicles[]` 数量补回已删除建议槽位。
- [ ] `requiredVehicles[]``suggestedVehicleCount``suggestedSlots` 的文案统一为“建议/参考”,不得展示为车务必配数量。
- [ ] 已有 `activeAssignments[]` 不走本地草稿删除;继续使用后端下发的取消/改派动作。
- [ ] 雪花 ID 继续按字符串消费,`fleetItemIndex` 不要求连续且不得因删除后重排而串到已有稳定槽位。
## 不影响范围
- 本工单不修改 `hl-ui`,不在 `mmg/hl-ui` 建工单;`frontend_status` 保持 `pending`
- 不修改派单状态机、车辆/司机占用、通知、保险、对账、逐日车费、Outbox 或数据库结构。
- 不放宽空批次:编辑态可删至 0,不等于后端接受空 `items[]`
- 不允许通过漏传已有有效派车实现静默删除。
## 验证证据
- 后端最终方案能力来自已部署的 #5262#5284 未新增运行时行为,因此 `gateway_status=not_required`
- 定向回归 5 项通过:建议数与最终数可不同、遗漏建议占位不阻塞完成、已有在途槽位漏传阻断、看板建议/实派分离、空 `items[]` 拒绝。
- Fleet 全量 `test`2431 项,0 失败、0 错误、1 跳过。
- Fleet 模块 `spotless:check`:通过。
- `mvn -pl hl-fleet-service -am verify`2431 项,0 失败、0 错误、1 跳过,JAR 构建成功。
- OpenAPI/oasdiff`not_configured`;已人工比对路径、字段、类型和必填性,确认只有 Swagger/Javadoc 语义收口。
- Consumer Contract`not_required`;没有 internal Feign 或 shared Java 变化。
- 前端状态:`pending`;领取后按 `pending → claimed → implemented → released → verified` 真实流转。

查看文件

@ -1,297 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5292"
title: "车务按服务日逐车配置用车、接机与价格"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "0a35da1e3b88f9c375d3e79516780e5a66f2b8ad"
target_release: ""
verified_at: "2026-07-28T14:38:26+08:00"
status_note: "hl-admin 已修复可编辑 UNPLANNED/新槽位默认用车、ARRIVAL 参与状态保持及新增槽位候选有界加载;最终提交 0a35da1e 已在 origin/v2.1 可达,全量 checkpoint 通过"
updated_at: "2026-07-28"
base: "dev-v3"
---
# Fleet按服务日逐车配置用车、接机与价格
> **服务**`hl-fleet-service`
> **后端 PR**[wx/HL#5296](https://git.1814.love:8443/wx/HL/pulls/5296)
> **Issue**#5292
> **日期**2026-07-27
> **影响范围**:管理后台车务看板派车弹窗和订单派车详情
---
## ⚠️ 关键变化
新派车页面不再提交“收取车费日期”。派车保存改为提交完整的“服务日期 × 稳定车辆槽位”矩阵,每个单元格独立说明是否用车、车辆、司机、ARRIVAL 接机参与和当天实际价格。
`items` 输入暂时保留用于滚动发布兼容;旧收费日期字段只能随旧 `items` 使用,`dailyPlan` 模式提交这些字段会被拒绝。
## 2026-07-28 产品口径补充:默认全部行程用车
新建派车草稿进入“排车”步骤时,当前需求的**全部可编辑服务日、全部新建稳定车辆槽位均默认勾选“当天用车”**。车务只需要选择车辆、司机和逐日价格;只有主动取消某日勾选或点击“明确不用车”,才表示该日明确不用车。
### 初始化规则
1. 后端 `planState=UNPLANNED` 表示尚未形成最终方案,不等于 `NOT_USED`。前端首次把可编辑 `UNPLANNED` 日格转换为草稿时应初始化 `used=true`,复选框默认选中,并显示“待选车辆/司机”或等价规划中状态。
2. 新增车辆槽位时,该槽位覆盖的全部可编辑服务日同样默认 `used=true`,不得全部初始化为未勾选。
3. 已有 `planFinalized=true``USED/NOT_USED`、只读日格、已关账日格和已有有效派车必须按服务端事实保留;已经明确保存为 `NOT_USED` 的日期重新打开时仍保持未选中,不得重新默认用车。
4. 默认选中只是前端草稿口径,不代表已经完成派车。未选择车辆、司机或逐日价格时仍不得提交,也不得伪造 `planFinalized=true`
5. 用户主动取消“当天用车”时才写入草稿 `used=false/planState=NOT_USED`;某日所有槽位均不用车时继续执行 `confirmNoVehicleServiceDates=true` 二次确认。
### 当前前端偏差
前端提交 `7b99fe7d35530073176af1a0376cba7b33503319` 中,`createDailyVehiclePlan()` 在详情提供 `dailyVehiclePlan` 时把所有未最终确认日格按 `UNPLANNED + used=false` 直接用于草稿;`appendDailyVehiclePlanSlot()` 也把新槽位日格初始化为 `used=false`。因此页面出现整段行程“尚未规划”、所有“当天用车”均未选中的状态,与本次明确口径不符。
前端应区分“服务端基线状态”和“当前编辑草稿默认值”,不能通过把 `UNPLANNED` 改成服务端 `USED` 来伪造最终事实;仅在可编辑的新草稿层默认选中。
### 前端验收清单
- [ ] 4 天行程首次进入排车步骤时,4 个可编辑日格的“当天用车”全部默认选中。
- [ ] 候选尚未选定时显示待选车辆/司机而非“明确不用车”,且下一步仍因车辆、司机或价格缺失而受阻。
- [ ] 新增车辆槽位后,该槽位全部可编辑服务日也默认选中。
- [ ] 车务取消其中一天后,仅该日变为明确不用车,其余日期保持选中和已编辑草稿。
- [ ] 已保存的 `NOT_USED`、已有有效派车及只读日期重新打开后保持服务端事实,不被默认逻辑覆盖。
- [ ] 补充 `createDailyVehiclePlan()``appendDailyVehiclePlanSlot()` 和真实挂载 `FleetAssignModal` 的回归测试,覆盖首次进入、增加槽位、重新打开以及主动取消用车。
- [ ] 提交请求仍满足:每个 `used=true` 日格具备车辆、司机和价格;全天不用车时带明确二次确认。
## 2026-07-28 页面阻断ARRIVAL 接机参与无法选中
真实页面复验中,服务日 **2026-08-04** 已标记“要求接机”,该日格已勾选“当天用车”、已选车辆和司机、无只读或锁定提示,但点击“参与 ARRIVAL 接机 / 接站”后复选框无法保持选中。该操作发生在派车草稿提交前,属于前端日格交互与状态同步阻断,不是后端保存接口拒绝。
当前 `origin/v2.1@e744c909` 中:
- `DailyVehiclePlanMatrix.vue` 的复选框仅发出 `update:pickup(key, checked)`
- `AssignModal.vue``handleDailyPlanPickupUpdate()` 只将结果写入本地 `dailyPlan`
- 现有 `daily-vehicle-plan-matrix.spec.js` 只断言子组件已发出事件,没有挂载 `FleetAssignModal` 验证父层接收后、候选状态 watcher 运行后以及切换日格后的值是否仍为 `true`
因此,子组件事件测试通过不能作为页面可用证据。前端需要从浏览器事件开始逐段核对 `NCheckbox → update:pickup → handleDailyPlanPickupUpdate → dailyPlan → 提交 payload`,找出勾选值被丢弃或覆盖的位置;不得通过跳过逐日方案校验或伪造后端字段规避。
### 前端验收清单
- [ ] 对 `pickupRequired=true``used=true` 且可编辑的日格,点击后复选框立即选中,并在多轮 `nextTick`、候选状态刷新以及价格编辑后保持选中。
- [ ] 切换到其他服务日再返回,接机参与状态仍保留;再次点击可以明确取消。
- [ ] 更新只作用于当前 `serviceDate + fleetItemIndex`,不得串改同日其他车辆或其他服务日。
- [ ] 将该日改为不用车时自动清除 `pickupParticipant`;重新用车后由车务再次明确选择,不沿用陈旧值。
- [ ] 提交前的 `dailyPlan` 以及最终请求体均包含该日格 `pickupParticipant=true`;后端返回成功后详情回显一致。
- [ ] 要求接机的服务日至少一辆实际用车标记参与接机;未选择时继续显示既有校验提示,选择后提示收敛。
- [ ] 新增 `FleetAssignModal` 父子联动回归测试,覆盖事件接收、watcher 稳定、日格切换和 payload;不能只断言 `DailyVehiclePlanMatrix` 发出了事件。
- [ ] 页面 Console 无 `Maximum recursive updates exceeded``unhandledrejection`,复选框操作不得重新触发无界候选请求。
在该页面交互修复并完成真实浏览器复验前,`frontend_status` 保持 `claimed`,不得流转为 `implemented/released/verified`
## 变更接口
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 原子提交最终派车方案 | POST | `/admin/fleet/assignments/batch` | 请求体扩展与校验调整 | 新增完整 `dailyPlan`,保留旧 `items` 兼容 |
| 2 | 车务看板订单详情 | GET | `/admin/fleet/board/orders/<orderId>` | 响应字段新增 | 返回逐日逐车计划、每车小计和订单车辆总计 |
## 二、原子提交最终派车方案
### `POST /admin/fleet/assignments/batch`
**请求 VO**`BatchCreateAssignmentReqVO`
### 新增顶层字段
| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `dailyPlan` | `DailyPlanItem[]` | 新页面必填 | 与旧 `items` 二选一;最多 4000 项 | 完整“服务日期 × 稳定车辆槽位”矩阵 |
| `confirmNoVehicleServiceDates` | `Boolean` | 条件必填 | 某日全部槽位 `used=false` 时必须为 `true` | 全天无需用车二次确认 |
`orderId``requirementId` 和所有 Long ID 继续按 JSON 字符串传输。
### `DailyPlanItem`
| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| `fleetItemIndex` | `Integer` | 是 | `>= 0` | 稳定车辆槽位序号 |
| `serviceDate` | `String(date)` | 是 | `yyyy-MM-dd`,必须属于当前需求服务日 | 服务日期 |
| `used` | `Boolean` | 是 | - | 当天是否实际用车 |
| `vehicleId` | `String(Long)` | 条件必填 | `used=true` 必填;不用车必须为空 | 当天车辆 |
| `driverId` | `String(Long)` | 条件必填 | `used=true` 必填;不用车必须为空 | 当天司机 |
| `pickupParticipant` | `Boolean` | 否 | 仅表示 ARRIVAL 接机/接站;不用车不得为 `true` | 当天是否参与接机 |
| `assignmentPrice` | `String(decimal)` | 条件必填 | `used=true` 必填;非负、整数最多 10 位、小数最多 2 位 | 本车当天实际价格,可为 `0.00` |
| `priceAdjustmentReason` | `String` | 条件必填 | 最多 256 字;实际价偏离价格日历时必填 | 改价原因 |
| `confirmCrossResident` | `Boolean` | 否 | 跨常驻车时按既有规则确认 | 跨常驻车确认 |
### 正确请求示例
```json
{
"orderId": "789",
"orderNo": "26-4165",
"requirementId": "790",
"startDate": "2026-08-04",
"endDate": "2026-08-05",
"headcount": 3,
"holdMode": 0,
"fromEntry": "from-board",
"requestId": "daily-plan-26-4165-v1",
"confirmNoVehicleServiceDates": false,
"dailyPlan": [
{
"fleetItemIndex": 0,
"serviceDate": "2026-08-04",
"used": true,
"vehicleId": "701",
"driverId": "801",
"pickupParticipant": true,
"assignmentPrice": "800.00",
"priceAdjustmentReason": "首日短途优惠"
},
{
"fleetItemIndex": 1,
"serviceDate": "2026-08-04",
"used": true,
"vehicleId": "702",
"driverId": "802",
"pickupParticipant": false,
"assignmentPrice": "900.00",
"priceAdjustmentReason": null
},
{
"fleetItemIndex": 0,
"serviceDate": "2026-08-05",
"used": false,
"vehicleId": null,
"driverId": null,
"pickupParticipant": false,
"assignmentPrice": null,
"priceAdjustmentReason": null
},
{
"fleetItemIndex": 1,
"serviceDate": "2026-08-05",
"used": true,
"vehicleId": "702",
"driverId": "802",
"pickupParticipant": false,
"assignmentPrice": "900.00",
"priceAdjustmentReason": null
}
]
}
```
### 提交规则
- 每个保留槽位必须覆盖当前需求的全部服务日;同一 `fleetItemIndex + serviceDate` 不能重复。
- 同一服务日不能重复使用同一车辆或同一司机。
- `used=false` 不占用车辆/司机、不投保、不计费;车辆、司机、接机和价格必须为空或 false。
- 某日所有槽位均不用车时允许提交,但必须设置 `confirmNoVehicleServiceDates=true`
- 大交通 ARRIVAL 要求平台接机时,当日至少一辆 `used=true` 的车辆必须 `pickupParticipant=true`
- 大交通未要求接机时,仍允许人工标记一辆或多辆使用中的车辆参与接机。
- 连续日期使用相同车辆和司机时后端自动合并派车组;逐日价格仍分别冻结。
- 过去日期、已完结日期或已关账对账期不能修改。
- 新 `dailyPlan` 不得提交 `chargeableServiceDates``vehicleFeeWaiverReason``confirmAllServiceDatesFree`
### 参数错误响应
本项目参数校验失败沿用 HTTP 200 + 业务 `code=400`
```json
{
"code": 400,
"message": "dailyPlan 与旧 items 必须二选一,dailyPlan 不得提交旧收费日期字段",
"data": null,
"success": false
}
```
## 三、车务看板订单详情
### `GET /admin/fleet/board/orders/<orderId>`
**响应 VO**`Result<BoardOrderDetailVO>`
### 新增响应字段
| 字段 | 类型 | 空值规则 | 说明 |
|---|---|---|---|
| `dailyVehiclePlan` | `DailyVehiclePlanVO[]` | 无记录返回 `[]` | 按服务日期、槽位序号稳定排序 |
| `vehicleFeeSummaries` | `VehicleFeeSummaryVO[]` | 无实际用车返回 `[]` | 按实际车辆汇总小计 |
| `vehicleFeeTotal` | `String(decimal)` | 无费用返回 `"0.00"` | 当前需求全部实际用车日总计 |
### `DailyVehiclePlanVO`
| 字段 | 类型 | 说明 |
|---|---|---|
| `serviceDate` | `String(date)` | 服务日期 |
| `fleetItemIndex` | `Integer` | 稳定槽位序号 |
| `assignmentSlotId` | `String(Long)` | 稳定槽位 ID |
| `assignmentId` | `String(Long)` | 每日切片 ID;显式不用车也返回占位 ID |
| `assignmentGroupId` | `String(Long)` | 当前连续派车组 ID |
| `planFinalized` | `Boolean` | 该日格是否已经业务最终确认 |
| `planState` | `String` | `UNPLANNED`(建议占位)/ `NOT_USED`(明确不用车)/ `USED`(实际用车) |
| `used` | `Boolean` | 当天是否实际用车;必须结合 `planFinalized/planState` 区分未规划与明确不用车 |
| `pickupParticipant` | `Boolean` | 当天车辆是否参与 ARRIVAL 接机 |
| `pickupRequired` | `Boolean` | 大交通当天是否要求接机 |
| `vehicleId` / `driverId` | `String(Long)` | 不用车时为 `null` |
| `vehiclePlate` / `vehicleModel` / `driverName` | `String` | 不用车时为 `null` |
| `driverPhone` | `String` | 脱敏手机号;不用车时为 `null` |
| `calendarPrice` | `String(decimal)` | 价格日历参考价;缺价或不用车时为 `null` |
| `assignmentPrice` | `String(decimal)` | 本车当天实际价;不用车时为 `null` |
| `priceSource` | `String` | `CALENDAR` / `OVERRIDE` / `MISSING` / `NOT_USED` |
| `priceAdjustmentReason` | `String` | 改价原因,无则 `null` |
| `assignmentStatus` | `String` | 基础派单状态;显式不用车为 `unassigned` |
| `readOnly` | `Boolean` | 过去、已完结或已关账日期为 `true` |
| `readOnlyReason` | `String` | `服务日期已过去` / `派单已完结` / `对账期已关账`,可编辑时为 `null` |
### `VehicleFeeSummaryVO`
| 字段 | 类型 | 说明 |
|---|---|---|
| `vehicleId` | `String(Long)` | 实际车辆 ID |
| `vehiclePlate` | `String` | 车牌 |
| `vehicleModel` | `String` | 车型 |
| `amount` | `String(decimal)` | 该车辆全部实际用车日小计 |
金额守恒:`vehicleFeeTotal = sum(vehicleFeeSummaries[].amount) = sum(dailyVehiclePlan[used=true].assignmentPrice)`
## 四、前端改造清单
1. 删除派车弹窗中的“收取车费日期”和免费日期提交逻辑。
2. 按服务日渲染稳定车辆槽位矩阵;每格维护 `used`、车辆、司机、ARRIVAL 接机参与和当天实际价格。
3. 某日全不用车时显示二次确认,并提交 `confirmNoVehicleServiceDates=true`
4. 默认价格使用详情/报价返回的日历价;修改价格时强制填写原因,允许 `0.00`
5. 详情展示每辆车小计和订单车辆总计;金额以字符串解析,不能用浮点累计。
6. `readOnly=true` 的日格禁止编辑,并展示 `readOnlyReason`
7. 新页面只提交 `dailyPlan`,不得同时提交旧 `items` 或收费日期字段。
## 五、数据库和历史数据
- `fleet_assignment.daily_vehicle_used``1` 表示当天实际用车,`0` 表示明确不用车;滚动发布期间允许 `NULL`,读取侧按车辆/司机事实回退,避免旧节点新写入被误判。
- `fleet_assignment.pickup_participant``1` 表示当天该车参与 ARRIVAL 接机/接站;滚动发布期间允许 `NULL` 并按 `false` 兼容。
- 历史已派日迁移为实际用车;原免费日实际价格迁为 `0.00`;未派占位迁为不用车;历史接机参与默认 `false`
- 全程明确不用车可由 Fleet 以 `vehicleCount=0` 完成需求;Order 端车辆和司机快照均为空。
## 六、不影响范围
- 不修改 `hl-ui` 仓库,由本 changelog 交接前端。
- 不改变单派接口及旧 `items` 滚动兼容输入。
- 不把 DEPARTURE 送机/送站映射到 `pickupParticipant`
- 不改变订单或产品价格日历接口。
## 验证证据
- 后端 PR #5296 已 squash 合并至 `dev-v3``hl-order-service-v3``hl-fleet-service` 已滚动部署测试环境,双实例健康检查通过。
- Fleet 最新 `dev-v3` reactor verify2452 项测试,0 failures,0 errors,1 skipped;Spotless、Jar、JaCoCo 均成功。
- Order 全量6759 项测试,0 failures,0 errors,29 skipped;零车辆回调生产者/消费者及迁移定向回归通过。
- 真实测试网关 `GET /admin/fleet/board/orders/<orderId>`HTTP 200、业务 code 200;返回 3 条 `dailyVehiclePlan`,包含 `planFinalized``planState``used`、接机、价格和只读字段;逐车汇总数组及字符串总计存在。
- 真实测试网关 `POST /admin/fleet/assignments/batch` 安全负例:`dailyPlan` 携带旧收费日期字段时 HTTP 200、业务 code 400,确认新旧模式互斥;该探针不产生业务写入。
- OpenAPI diff 与 Spring Cloud Contract 未配置;已通过源码字段对比、Controller/Service 及 Fleet → Order 双端普通测试提供人工回退证据,未冒充工具通过。
当前状态:
- `backend_status: deployed`
- `gateway_status: verified`
- `frontend_status: claimed`
关联 Issue[wx/HL#5292](https://git.1814.love:8443/wx/HL/issues/5292)

查看文件

@ -1,205 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5299"
title: "车务矩阵车辆司机预选与常驻标记"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-admin"
frontend_ref: "45c713a8b30b13c9b57d102cfcce82885744a7e9"
target_release: ""
verified_at: "2026-07-28T14:38:26+08:00"
status_note: "hl-admin 已修复 FleetAssignModal 递归更新、补齐车辆预选/常驻三态与车辆行实际司机汇总;最终提交 45c713a8 已在 origin/v2.1 可达,checkpoint 通过"
updated_at: "2026-07-28"
base: "dev-v3"
---
# Fleet矩阵车辆司机预选与常驻标记
> **服务**`hl-fleet-service`
> **Issue**#5299
> **影响页面**:管理后台车务管理 → 矩阵派单
> **兼容性**:仅新增响应字段,旧客户端可继续忽略
## 问题与目标
当前矩阵车辆行虽然已有常驻司机姓名,但缺少常驻司机稳定 ID;订单甘特条也没有直接返回该派车组的实际车辆、司机及常驻关系。页面因此无法稳定完成以下行为
- 从具体车辆上下文发起派单时自动带出车辆和可派常驻司机;
- 行头展示车辆常驻司机;
- 已排订单条块展示实际执行司机,并区分常驻/临时司机。
本次后端补齐稳定读契约。前端不得按姓名判断常驻关系,也不得复制相邻订单的临时司机作为新派单默认值。
## 2026-07-28 前端运行态阻断:`FleetAssignModal` 递归更新
### 现场结论
前端实现提交 `bfcfafc69335f289071fe3e6dce74bacb636c55d` 已进入 `v2.1`,但当前测试页面打开逐日派车弹窗并加载车辆、司机候选后稳定出现:
```text
Maximum recursive updates exceeded in component <FleetAssignModal>
```
浏览器 Console 同时记录多次 `unhandledrejection`;Network 中多条 `POST /admin/fleet/assignments/candidates` 均返回 HTTP 200,但车辆、司机区域持续停留在 loading,无法进入下一步。因此本次不是候选接口、日期冲突或后端状态机错误,而是前端实现后的响应式自反馈回归。`frontend_status: "implemented"` 仅表示代码已存在,不代表已发布或页面闭环。
### 高可信自反馈链
当前 `AssignModal.vue` 的深度 watcher 同时观察 `candidateEvidence``candidateSelectionReady``autoSelectedResidentDriverSource` 等候选派生状态,并在回调 `syncActiveSlotSelection()` 中无条件重建 `dailyPlan`、回写当前槽位。`dailyPlan` 又参与计算其他槽位排除 ID、候选可选态和新的 `candidateEvidence`;即使业务值没有变化,新数组/对象身份仍会再次触发同一 watcher,形成“观察候选派生值 → 回写逐日方案 → 候选派生值重新计算 → 再次回写”的闭环。
前端修复必须同时满足:
1. `syncActiveSlotSelection()` 先比较当前日格与待写 payload;语义完全相同时直接返回,不创建新 `dailyPlan`/槽位对象。
2. watcher 使用稳定原始值或稳定 fingerprint,不深度监听会被自身回写间接失效的派生对象;候选响应、用户选择和槽位同步应有单向边界。
3. `updateDailyVehiclePlanCell()` 或等价更新器在无真实字段变化时返回原引用,禁止仅因对象重建触发后续 effect。
4. 保留 #5283 的稳定 `initializationIdentity``requestSeq` 旧响应隔离;不得退回监听 `props.order` 对象身份或通过删除并发保护掩盖循环。
5. 正常打开弹窗只允许一次初始候选请求;需要自动常驻司机二次校验时最多再请求一次。状态稳定后不得继续请求,loading 必须收敛。
### 前端回归验收
- [ ] 打开订单逐日派车弹窗并取得候选成功响应后,Console 不再出现 `Maximum recursive updates` 或相关 `unhandledrejection`
- [ ] 候选请求数量有明确上界;自动常驻司机场景最多“初始查询 + 携两侧 ID 二次校验”,不存在持续请求。
- [ ] 车辆、司机列表结束 loading,接口返回的分页总数与页面一致,可正常进入下一步。
- [ ] 已选车辆、司机、逐日费用和跨常驻确认写回一次后保持稳定,多轮 `nextTick` 不再重建相同 `dailyPlan`
- [ ] 相同业务身份的 SSE/列表对象替换仍保留候选与草稿;真实订单、需求、槽位或模式变化时才重新初始化。
- [ ] 新增真实挂载 `FleetAssignModal` 的回归测试,模拟候选成功和自动常驻二次校验,断言无未处理 Promise、请求次数有界且草稿稳定;仅做静态源码断言不足以验收。
## 变更接口
### `GET /admin/fleet/matrix/grid`
`data.vehicles[]` 新增:
| 字段 | 类型 | 空值规则 | 说明 |
|---|---|---|---|
| `primaryDriverId` | `String(Long)` | 无常驻司机为 `null` | 车辆主档的权威常驻司机 ID |
既有 `primaryDriverName``primaryDriverPhone` 继续返回;手机号保持脱敏。三个字段共同用于行头展示,常驻判断以 ID 为准。
`data.vehicles[].assignments[]` 新增:
| 字段 | 类型 | 空值规则 | 说明 |
|---|---|---|---|
| `vehicleId` | `String(Long)` | 未派为空 | 该甘特条对应派车组的实际车辆 ID |
| `vehiclePlate` | `String` | 未派为空 | 实际车牌 |
| `driverId` | `String(Long)` | 未派为空 | 该派车组实际司机 ID |
| `driverName` | `String` | 未派为空 | 该派车组实际司机姓名 |
| `residentMatch` | `Boolean` | 无实际司机为 `null` | `true`=实际司机是该车常驻司机;`false`=实际司机存在但不是该车常驻司机或车辆无常驻 |
矩阵继续排除取消切片;同一派车组存在取消日缺口时按有效连续服务段拆成多个甘特条,不得把外包络日期当作司机持续占用。
## 前端必须调整
### 1. 从矩阵车辆上下文发起派单
1. 将所在行 `vehicles[].id` 直接作为当前选择车辆,并在候选请求中传 `selectedVehicleId`
2. 候选接口 `POST /admin/fleet/assignments/candidates` 会返回既有字段 `selectedVehicleResidentDriver`
3. 仅当该快照存在且 `available=true` 时,才把其司机 ID 作为默认司机。
4. 无常驻司机、常驻司机冲突或不可派时,车辆仍保持预选,司机保持“待选择”,并展示候选返回的不可用原因。
5. 不得取前后相邻订单的实际司机作为新订单默认司机。
### 2. 矩阵司机展示
#### 2.1 车辆行司机汇总数据源
后端契约已经足够,前端不得新增接口或按车牌反查司机:
- 常驻司机取当前车辆行 `vehicles[].primaryDriverId``primaryDriverName`;常驻关系以 ID 为准。
- 订单实际司机只取同一车辆行当前响应中的 `vehicles[].assignments[].driverId``driverName`;这些是当前矩阵月份和筛选条件已加载的真实派车段。
- `assignments[]` 后端已按月内 `startDay` 升序返回。前端按响应数组顺序扫描,以司机首次出现的位置作为其他司机的稳定展示顺序,不按姓名另行排序,也不混入上一月份、上一筛选条件、未派窗口、候选列表或 `drivers` 页面数据。
- `residentMatch` 继续用于订单条块的常驻/临时三态标记;车辆行汇总去重使用稳定 `driverId`,不得只按姓名猜测同一人。
#### 2.2 汇总与展示规则
1. 若 `primaryDriverId` 非空,先把常驻司机放在结果第一项,显示 `primaryDriverName常驻`。ID 存在但姓名异常为空时使用 `姓名未标注(常驻)`,不得输出空白项。
2. 随后按 `assignments[]` 当前顺序遍历实际司机:`driverId` 或去空后的 `driverName` 为空则跳过;相同 `driverId` 只保留第一次出现。
3. 订单实际司机与 `primaryDriverId` 相同时,不再追加普通姓名,只保留第一项带“(常驻)”标记的展示。
4. 其他实际司机按首次出现顺序追加,使用中文逗号 `,` 连接。不得把同一司机跨多个订单或拆分派车段重复展示。
5. 无常驻司机但存在订单实际司机时,直接展示实际司机汇总,**不得只显示“无常驻司机”**。
6. 只有常驻司机和订单实际司机都不存在时,才显示“无常驻司机”。若行宽不足允许视觉省略,但必须通过 `title`、tooltip 或等价交互查看完整汇总,不得静默丢失司机。
展示样例:
| 常驻司机 | 当前行订单司机(按首次出现顺序) | 车辆行展示 |
|---|---|---|
| 张三 | 张三、李四、王五、李四、赵六 | `张三(常驻),李四,王五,赵六` |
| 无 | 李四、王五、李四 | `李四,王五` |
| 张三 | 张三、张三 | `张三(常驻)` |
| 无 | 空 | `无常驻司机` |
#### 2.3 订单条块保持既有语义
已排订单条块继续展示本条 `driverName`
- `residentMatch=true`:标记“常驻”。
- `residentMatch=false`:标记“临时”。
- `residentMatch=null`:显示“待派司机”。
不要从 `drivers` 页面列表或姓名/车牌文本反推常驻关系;所有 Long ID 继续按字符串比较,禁止转为 JS `Number`
#### 2.4 前端验收清单
- [ ] 截图中车辆无常驻司机但订单已有实际司机时,车辆行显示订单实际司机姓名,不再只显示“无常驻司机”。
- [ ] 常驻司机与多名订单司机并存时,展示严格为 `张三(常驻),李四,王五,赵六`,常驻第一且只出现一次。
- [ ] 同一实际司机出现在多个订单或多个有效派车段时只展示一次;空 ID、空姓名和未派条块不产生空白分隔项。
- [ ] 其他司机顺序跟随当前 `assignments[]` 首次出现顺序;切换月份、车队、车型或状态筛选后按新响应重新计算,不残留旧司机。
- [ ] 订单条块的 `driverName/residentMatch` 常驻、临时、待派展示不回归。
- [ ] 补充纯汇总函数或 `VehicleGantt` 组件测试,至少覆盖上表四组样例、Long ID 字符串去重和筛选响应替换。
- [ ] 真实页面复验行宽溢出场景可查看完整司机列表,Console 无异常,且不增加司机列表或候选接口请求。
在本节完成并通过真实矩阵页面复验前,`frontend_status` 保持 `claimed`,不得流转为 `implemented/released/verified`
### 3. 同步完成 #5292 页面改造
用户截图仍出现“收取车费日期”,说明当前测试页面仍在使用旧构建或旧逻辑。新页面必须继续执行 #5292
- 删除“收取车费日期”及旧免费日期提交逻辑;
- 使用 `dailyVehiclePlan` 渲染“服务日期 × 稳定车辆槽位”;
- 保存时只提交 `dailyPlan`,不得同时提交旧 `items``chargeableServiceDates``vehicleFeeWaiverReason``confirmAllServiceDatesFree`
## 响应示例
```json
{
"id": "9007199254740993",
"plate": "蒙A-88888",
"primaryDriverId": "9007199254740994",
"primaryDriverName": "王师傅",
"primaryDriverPhone": "138****1234",
"assignments": [
{
"id": "9007199254740995",
"assignmentGroupId": "9007199254740996",
"vehicleId": "9007199254740993",
"vehiclePlate": "蒙A-88888",
"driverId": "9007199254740994",
"driverName": "王师傅",
"residentMatch": true
}
]
}
```
所有 Long ID 仍按 JSON 字符串处理,禁止转为 JS `Number`
## 不影响范围
- 不修改派单状态机、车辆/司机占用、保险、对账或常驻关系写入。
- 不自动选择不可派司机,不绕过候选接口与最终派单锁内校验。
- 不修改 `hl-ui` 仓库;前端消费状态独立流转。
## 验证证据
- 后端 PR [wx/HL#5300](https://git.1814.love:8443/wx/HL/pulls/5300) 已 squash 合并至 `dev-v3`,合并提交 `fca8cedc6`
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康。
- 定向矩阵测试43 tests,0 failures,0 errors,0 skipped。
- Fleet reactor verify2456 tests,0 failures,0 errors,1 skipped;模块 Spotless、Jar、JaCoCo 成功。
- 真实测试网关 `GET /admin/fleet/matrix/grid?year=2026&month=8&season=active`HTTP/code 200;返回 19 辆车、38 个派车段,新增车辆/司机字段完整,Long ID 均为字符串,`residentMatch` 三态合法,常驻司机手机号全部脱敏,无业务写入。
- 网关证据:`D:/work2/HL-v3/.tmp/5299-gateway.json`,SHA-256 `d94ee78fbe52cc193a28e5bf182e1353d2eca9d87824b1c2eb526c62a4fe645d`
- OpenAPI/oasdiff项目尚未配置可复现 Swagger2→OAS3 与 oasdiff,状态为 `not_configured`;使用源码字段对比、Controller 序列化测试及真实网关响应作为人工回退证据,未冒充工具通过。
当前状态:后端已部署、网关已验证;前端仅有可达的部分实现 `bfcfafc6`,运行态复验及车辆行司机汇总均未通过,因此 source 状态保持 `claimed`;修复并通过上述页面验收前不得流转为 `implemented``released``verified`
关联:#5299#5292#5283

查看文件

@ -1,90 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "4933"
title: "矩阵空闲格候选排除车辆日期冲突"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "27b5161696add9bab725611900888488b95ff510"
target_release: ""
verified_at: "2026-07-28T17:51:52+08:00"
status_note: "后端月度未派清单契约未变;本次交接前端按所选车辆空闲闭区间过滤候选。"
updated_at: "2026-07-28"
base: "dev-v3"
---
# 车务矩阵:空闲格候选必须排除车辆日期冲突
> **服务**`hl-fleet-service`(既有接口,无后端代码变更)
>
> **Issue**#4933 的矩阵空闲格直接派单前端实现回归
>
> **日期**2026-07-28
>
> **影响范围**:管理后台车务矩阵点击车辆空闲格后的“选择当前矩阵可派订单”抽屉
## 关键结论
`GET /admin/fleet/matrix/unassigned-orders` 的既有语义是返回当前年月及车型筛选下的**全局未派订单池**,不是针对某辆车某段空闲日期计算后的候选集合。前端点击空闲格时已经持有 `vehicleId``clickedDate``startDate``endDate` 和该车辆矩阵占用,但当前抽屉只按“未绑定车辆”过滤,因此会展示完整用车区间不在空闲区间内、甚至与该车已有派车日期重叠的订单。
本次不要求后端新增字段或改变全局未派池语义。前端必须在打开空闲格抽屉时,根据点击上下文和当前矩阵车辆占用过滤候选;派单提交时仍由后端在资源锁内执行最终冲突重校验。
## 变更接口
| 方法 | 路径 | 后端结构变化 | 前端正确消费方式 |
|---|---|---|---|
| GET | `/admin/fleet/matrix/unassigned-orders` | 无;仍传 `year``month`、可选 `typeKeys[]`,仍返回当月全局有效未派行 | 仅作为候选数据源;进入某辆车空闲格抽屉前,结合点击上下文和该车矩阵占用做闭区间资格过滤 |
| GET | `/admin/fleet/matrix/grid` | 无 | 继续作为车辆、派车段和空闲区间的数据源,不新增前端猜测的返程日、缓冲日或状态 |
### 前端过滤不变量
1. 候选订单的全部有效 `serviceDateSegments[]` 必须完整落入所选空闲闭区间 `[startDate, endDate]`;只有后端未提供该数组字段时,才兼容使用订单 `startDate/endDate`
2. 候选完整有效日期与所选车辆当前矩阵占用不得有任一日期重叠;首日、末日及边界同日均按占用处理。
3. 不允许为了让候选“可派”而把订单完整区间静默裁成空闲区间交集。候选不满足完整容纳条件时,应直接从抽屉排除。
4. 前端过滤不得替代创建或改派接口的后端锁内冲突校验;矩阵快照过期时以后端写侧拒绝为准。
5. 取消派车继续按矩阵现有有效性口径排除;不得自行增加返程后缓冲日或改变派车状态语义。
### 验收矩阵
以下日期均按闭区间判断:
| 场景 | 已有占用 | 候选完整区间 | 所选空闲区间 | 预期 |
|---|---|---|---|---|
| 候选起始日重叠 | 08-0308-05 | 08-0508-08 | 08-0608-31 | 排除 |
| 候选结束日重叠 | 08-0808-10 | 08-0508-08 | 08-0108-07 | 排除 |
| 候选包含已有占用 | 08-0508-06 | 08-0408-07 | 08-0108-31 | 排除 |
| 候选被已有占用包含 | 08-0308-09 | 08-0408-07 | 08-0108-31 | 排除 |
| 边界相邻但不重叠 | 08-0308-05 | 08-0608-07 | 08-0608-31 | 保留 |
| 完整区间不在空闲区间 | 无额外占用 | 08-0408-07 | 08-0608-31 | 排除,不得裁成 08-0608-07 |
| 无冲突且完整容纳 | 08-0308-05 | 08-2908-31 | 08-0608-31 | 保留 |
## 运行复现
测试页面 `fleet/matrix` 的 2026-08 月矩阵中:
- 点击车辆 `蒙C05E05``2026-08-21` 空闲格;页面明确给出的空闲区间是 `2026-08-062026-08-31`
- 抽屉仍显示订单 `26-4700`,完整区间为 `2026-08-042026-08-07`
- 同一车辆既有订单 `26-4338` 的逐日用车详情显示 `2026-08-03``2026-08-04``2026-08-05` 均为已规划用车。
- 因而 `26-4700` 既未被空闲区间完整容纳,又在 08-04、08-05 与该车已有占用重叠,必须排除。
- 同抽屉订单 `26-5568` 的完整区间为 `2026-08-292026-08-31`,应继续保留。
## 验证证据
- 运行页面已复现当前失败:空闲格抽屉显示 `共 2 单`,其中包含不合格的 `26-4700`
- 运行订单详情已核实车辆 `蒙C05E05` 在 08-0308-05 的逐日用车事实,不是根据甘特位置猜测日期。
- `hl-ui origin/v2.1@e744c9096a6807439b3e4514a7687cf2f058b3fc`
- `useFleetMatrixData.js` 调用月度未派接口时仅传 `year/month/typeKeys`
- `index.vue``idleAssignableOrders` 仅过滤 `!order.vehicle`
- `matrixAssignmentContext.js` 已构造车辆和空闲区间上下文,但当前只在选中订单后用于派单弹窗。
- `wx/HL origin/dev-v3@883fc61a006f6d7e40ca1c8723ffc6d28e37bf75``MatrixUnassignedReqVO` 只有 `year/month/typeKeys``MatrixService#queryUnassignedOrders` 按月返回全局未派行,当前行为符合既有接口契约。
- 前端修复、上述七类单测、发布及页面复核尚未执行,因此 `frontend_status` 保持 `pending`
## 不影响范围
- 不修改后端请求/响应字段、错误码或状态机。
- 不修改派单创建、改派的车辆锁、司机锁和闭区间冲突校验。
- 不修改全局“打开未派订单窗口”的月度未派池展示;本规则仅用于从具体车辆空闲格进入的候选抽屉。
- 不涉及生产或数据库写入。

查看文件

@ -1,580 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5295"
title: "Step3 聚合车辆费用"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "7cd0b151ce96e04206450d262ab5f0de4ee69ee6"
target_release: ""
verified_at: "2026-07-28T21:20:30+08:00"
status_note: "hl-order-service-v3 已部署并通过网关验证;hl-admin 已改用 Step3 GET 聚合 vehicleFees,PUT 保存响应仅按成功/失败处理;全量 checkpoint 通过"
updated_at: "2026-07-28"
base: "dev-v3"
---
# 【修改接口·管理后台】Step3 聚合车辆费用 (#5295)
> **PR**: #5297 | **更新时间**: 2026-07-28 11:26
## 1. 接口背景
核单 Step3 页面原来需要分别读取人员费用和车辆费用。本次把车辆费用聚合到 Step3 查询响应里:进入 Step3 时同屏拿到人员费用与车辆费用;保存人员费用时,同一次保存会校验车辆费用是否满足核单条件,保存接口响应只表达成功或失败。
## 2. 变更接口
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | Step 3 查询人员费用核单明细 | GET | `/v3/admin/order/:orderId/settlement/step3` | 修改接口 | 响应新增 `vehicleFees`,包含车辆费用顶层状态、总金额和逐日明细 |
| 2 | Step 3 录人员费用核单明细 | PUT | `/v3/admin/order/:orderId/settlement/step3` | 修改接口 | 保存人员费用时校验车辆费用;响应只表达成功或失败,`data` 不返回操作数据 ID |
## 3. 接口详情
### 3.1 GET Step 3 查询人员费用核单明细
- **方法 + 路径**: `GET /v3/admin/order/:orderId/settlement/step3`
- **接口名**: Step 3 查询人员费用核单明细
- **使用场景**: 进入核单 Step3 页面时调用,展示人员费用表和车辆费用块。
- **认证**: 需要管理后台登录态;无权限或未登录按统一鉴权错误返回。
- **幂等性**: 幂等,只读查询。
- **限流**: 无接口级特殊限流。
- **响应类型**: `Result<SettlementStaffFeesSaveRespVO>`
### 3.2 PUT Step 3 录人员费用核单明细
- **方法 + 路径**: `PUT /v3/admin/order/:orderId/settlement/step3`
- **接口名**: Step 3 录人员费用核单明细
- **使用场景**: 用户保存 Step3 人员费用时调用。保存成功以 `code=200``message` 表达,`data` 不返回新增、更新、删除 ID 或车辆费用块。
- **认证**: 需要管理后台登录态和核单资金写权限。
- **幂等性**: 同一 `items` 内容重复提交,人员费用结果保持一致;车辆费用已冻结后再次保存仍按成功或失败返回。
- **限流**: 无接口级特殊限流。
- **响应类型**: `Result<Void>`
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 字段 | 类型 | 必填 | 适用接口 | 说明 |
|------|------|------|----------|------|
| `orderId` | Long/String | 是 | GET、PUT | 订单 ID,必须大于 0;JSON 示例中按字符串展示,避免大整数精度问题 |
GET 无 Query 参数,无请求体。
### 4.2 PUT 请求体字段
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `items` | Array<StaffFeeItem> | 是 | 人员费用行数组,全量替换语义 | 数组字段必须存在;数组元素按下表校验 |
| `items[].id` | Long/String | 否 | 已存在行 ID;为空表示新增 | 已有行更新时传 |
| `items[].staffRole` | String | 是 | 人员角色 | 仅允许 `LEADER``DRIVER``GUIDE``PHOTOGRAPHER``OTHER` |
| `items[].staffId` | Long/String | 否 | 关联人员分配 ID | 多人聚合行可为空 |
| `items[].detail` | Object | 是 | 按 `staffRole` 区分的明细 JSON | 不能省略;各角色结构见下表 |
| `items[].reimburse` | Decimal/String | 否 | 小额报销金额 | 必须大于等于 0;为空按 0 处理 |
| `items[].paymentMethod` | String | 否 | 统一付款类型 | 仅允许 `CASH_PAID``COMPANY_PAID``SIGNED` |
| `items[].voucherUrls` | Array<String> | 否 | 人员费用凭证 URL 数组 | 最多 9 个;每个元素必须是 http/https URL,单个最多 1024 字符 |
| `items[].settlementConfirmStatus` | String | 否 | 人员费用核单确认状态 | 仅允许 `UNCONFIRMED``CONFIRMED` |
| `items[].settleStatus` | String | 否 | 辅助人员结算状态 | 仅允许 `PENDING``COMPLETED`;主报账人行可为空 |
| `items[].settledDate` | String(date) | 否 | 辅助人员结算日期 | 格式 `YYYY-MM-DD` |
| `items[].transferRef` | String | 条件必填 | 辅助人员结算转账流水号 | `settleStatus=COMPLETED` 时必填,最多 128 字符 |
| `items[].remark` | String | 否 | 备注 | 最多 500 字符 |
### 4.3 `detail` 字段结构
| `staffRole` | `detail` 结构 | 必填说明 |
|-------------|---------------|----------|
| `DRIVER` | `days[]``service_date``vehicle_brief``daily_fee``is_used``note`),以及 `extra_cost``extra_breakdown[]` | `days[]` 必须存在;每个元素必须包含 `service_date``daily_fee` |
| `GUIDE` | `persons[]``name``days``per_day``note` | `persons[]` 必须存在;每个元素必须包含 `name``days``per_day` |
| `PHOTOGRAPHER` | `persons[]``name``days``per_day``note` | `persons[]` 必须存在;每个元素必须包含 `name``days``per_day` |
| `LEADER` | `days``per_day` | `days``per_day` 必须存在 |
| `OTHER` | `items[]``name``amount``note` | `items[]` 用于其他人员费用明细 |
## 5. 出参字段
### 5.1 统一响应包装
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码;`200` 表示成功 |
| `data` | Object/null | GET 成功时为 `SettlementStaffFeesSaveRespVO`;PUT 成功和失败时为 `null` |
| `message` | String | 响应消息 |
### 5.2 GET 成功响应 `data` 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `totalActualCost` | String(decimal) | 人员费用实际成本合计 |
| `items` | Array<StaffFeeRespItem> | 人员费用明细行 |
| `vehicleFees` | Object | GET 本次新增:车辆费用块;无有效车辆需求时仍返回对象,`items=[]`、金额为 `0.00``frozen=false` |
### 5.3 `data.items[]` 人员费用明细
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 人员费用行 ID |
| `staffRole` | String | 人员角色:`LEADER``DRIVER``GUIDE``PHOTOGRAPHER``OTHER` |
| `staffId` | String/null | 关联人员分配 ID |
| `staffName` | String/null | 人员姓名快照 |
| `totalPlannedCost` | String(decimal) | 计划成本 |
| `totalActualCost` | String(decimal) | 实际成本 |
| `detail` | Object | 按 `staffRole` 区分的明细 JSON |
| `reimburse` | String(decimal) | 小额报销金额 |
| `paymentMethod` | String/null | 人员费用付款类型:`CASH_PAID``COMPANY_PAID``SIGNED` |
| `voucherUrls` | Array<String> | 凭证 URL 数组 |
| `settlementConfirmStatus` | String | 核单确认状态:`UNCONFIRMED``CONFIRMED` |
| `settleStatus` | String/null | 辅助人员结算状态:`PENDING``COMPLETED`;主报账人行可为空 |
| `settledDate` | String(date)/null | 辅助人员结算日期 |
| `transferRef` | String/null | 辅助人员结算转账流水号 |
| `isPrimaryReporter` | Boolean | 是否主报账人 |
| `remark` | String/null | 备注 |
### 5.4 `data.vehicleFees` 车辆费用顶层
| 字段 | 类型 | 说明 |
|------|------|------|
| `orderId` | String | 订单 ID |
| `frozen` | Boolean | 车辆费用是否已冻结;保存成功并冻结后,后续 GET 返回 `true` |
| `requirementId` | String/null | 当前车辆需求 ID;无有效车辆需求时为 `null` |
| `settlementReady` | Boolean | 车辆费用是否满足核单条件;为 `false` 时 PUT 可能返回 `584101` |
| `totalAmount` | String(decimal) | 车辆费用总金额 |
| `totalVehicleFee` | String(decimal) | 车辆费用总金额,兼容旧字段名;前端展示可读取该字段 |
| `items` | Array<VehicleFeeItem> | 车辆费用逐日明细;无有效车辆需求时为空数组 |
### 5.5 `data.vehicleFees.items[]` 车辆费用明细
| 字段 | 类型 | 说明 |
|------|------|------|
| `sourceDetailId` | String/null | 逐日费用来源明细 ID;冻结后的旧数据可能为空 |
| `serviceDate` | String(date)/null | 逐日服务日期 |
| `assignmentGroupId` | String/null | 派车组 ID;逐日来源可为空 |
| `assignmentSlotId` | String/null | 派车明细 ID;逐日来源可为空 |
| `vehicleId` | String/null | 车辆 ID |
| `vehiclePlate` | String/null | 车牌号 |
| `vehicleModelId` | String/null | 车型 ID |
| `vehicleModelName` | String/null | 车型名称 |
| `vehicleModel` | String/null | 车型展示文本,兼容旧字段 |
| `driverId` | String/null | 司机 ID |
| `driverName` | String/null | 司机姓名 |
| `startDate` | String(date)/null | 费用服务开始日期;逐日费用通常等于 `serviceDate` |
| `endDate` | String(date)/null | 费用服务结束日期;逐日费用通常等于 `serviceDate` |
| `chargeableServiceDates` | Array<String(date)> | 计费服务日期列表 |
| `freeServiceDates` | Array<String(date)> | 免费服务日期列表 |
| `vehicleFeeWaiverReason` | String/null | 免车费原因 |
| `dailyPrice` | String(decimal) | 当日车费 |
| `paymentTypeCode` | String | 车辆费用付款类型编码:`CASH_PAID``SIGNED``COMPANY_PAID` |
| `paymentTypeName` | String/null | 车辆费用付款类型名称 |
| `amount` | String(decimal) | 本条核单金额 |
| `autoVehicleFeeTotal` | String(decimal) | 自动计算车辆费用金额 |
| `autoVehicleFeeComplete` | Boolean | 自动计算金额是否完整 |
| `vehicleFeeTotal` | String(decimal) | 本条车辆费用金额,兼容旧字段名 |
| `vehicleFeeSource` | String | 费用来源:`AUTO``MANUAL` |
| `vehicleFeeAdjustmentReason` | String/null | 手工调整原因 |
| `vehicleFeeAdjustedBy` | String/null | 手工调整人 ID |
| `vehicleFeeAdjustedAt` | String(datetime)/null | 手工调整时间 |
| `settlementReady` | Boolean | 本条车辆费用是否满足核单条件 |
## 6. 枚举 / 数据字典
### 6.1 `paymentTypeCode`(车辆费用付款类型)
**所属字段**: `data.vehicleFees.items[].paymentTypeCode` | **类型**: `String` | **必填**: 是
| 值 | 中文 | 说明 |
|----|------|------|
| `CASH_PAID` | 现付 | 车辆费用已由现场现金或等价方式支付 |
| `SIGNED` | 签单 | 车辆费用采用签单方式结算 |
| `COMPANY_PAID` | 公司支付 | 车辆费用由公司统一支付 |
### 6.2 `paymentMethod`(人员费用付款类型)
**所属字段**: `items[].paymentMethod``data.items[].paymentMethod` | **类型**: `String` | **必填**: 否
| 值 | 中文 | 说明 |
|----|------|------|
| `CASH_PAID` | 现付 | 人员费用已现场支付 |
| `COMPANY_PAID` | 公司支付 | 人员费用由公司支付 |
| `SIGNED` | 签单 | 人员费用采用签单方式 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `584100` | 车辆总车费暂时不可用 | GET 或 PUT Step3 读取车辆费用失败,或车辆费用响应与当前订单不匹配 |
| `584101` | 存在未完结派车或未确认车辆总车费,暂不能核单 | PUT Step3 保存时,当前车辆费用 `settlementReady=false`,或任一车辆费用明细未满足冻结条件 |
| `584102` | 当前用车需求没有可核单的车辆总车费 | PUT Step3 保存时存在有效车辆需求,但车辆费用明细为空 |
## 8. 示例
### 8.1 典型成功GET Step3 返回 9 行车辆费用
**请求**:
```http
GET /v3/admin/order/2079454953641836546/settlement/step3
Authorization: Bearer <token>
```
**响应**:
```json
{
"code": 200,
"message": "success",
"data": {
"totalActualCost": "3600.00",
"items": [
{
"id": "9300000000001",
"staffRole": "DRIVER",
"staffId": "6800001001",
"staffName": "司机A",
"totalPlannedCost": "2100.00",
"totalActualCost": "2100.00",
"detail": {
"days": [
{
"service_date": "2026-07-29",
"vehicle_brief": "蒙A12345",
"daily_fee": "700.00",
"is_used": true,
"note": ""
}
],
"extra_cost": "0.00",
"extra_breakdown": []
},
"reimburse": "0.00",
"paymentMethod": "COMPANY_PAID",
"voucherUrls": [],
"settlementConfirmStatus": "UNCONFIRMED",
"settleStatus": "PENDING",
"settledDate": null,
"transferRef": null,
"isPrimaryReporter": false,
"remark": ""
}
],
"vehicleFees": {
"orderId": "2079454953641836546",
"frozen": false,
"requirementId": "2079000000000000001",
"settlementReady": true,
"totalAmount": "6780.00",
"totalVehicleFee": "6780.00",
"items": [
{
"sourceDetailId": "2080000000000000001",
"serviceDate": "2026-07-29",
"assignmentGroupId": null,
"assignmentSlotId": null,
"vehicleId": "300000000000000001",
"vehiclePlate": "蒙A12345",
"vehicleModelId": "400000000000000001",
"vehicleModelName": "商务车",
"vehicleModel": "商务车",
"driverId": "500000000000000001",
"driverName": "宝音德力格尔",
"startDate": "2026-07-29",
"endDate": "2026-07-29",
"chargeableServiceDates": ["2026-07-29"],
"freeServiceDates": [],
"vehicleFeeWaiverReason": null,
"dailyPrice": "700.00",
"paymentTypeCode": "COMPANY_PAID",
"paymentTypeName": "公司支付",
"amount": "700.00",
"autoVehicleFeeTotal": "700.00",
"autoVehicleFeeComplete": true,
"vehicleFeeTotal": "700.00",
"vehicleFeeSource": "AUTO",
"vehicleFeeAdjustmentReason": null,
"vehicleFeeAdjustedBy": null,
"vehicleFeeAdjustedAt": null,
"settlementReady": true
},
{
"sourceDetailId": "2080000000000000002",
"serviceDate": "2026-07-29",
"assignmentGroupId": null,
"assignmentSlotId": null,
"vehicleId": "300000000000000002",
"vehiclePlate": "蒙A23456",
"vehicleModelId": "400000000000000002",
"vehicleModelName": "越野车",
"vehicleModel": "越野车",
"driverId": "500000000000000002",
"driverName": "阿拉坦",
"startDate": "2026-07-29",
"endDate": "2026-07-29",
"chargeableServiceDates": ["2026-07-29"],
"freeServiceDates": [],
"vehicleFeeWaiverReason": null,
"dailyPrice": "700.00",
"paymentTypeCode": "SIGNED",
"paymentTypeName": "签单",
"amount": "700.00",
"autoVehicleFeeTotal": "700.00",
"autoVehicleFeeComplete": true,
"vehicleFeeTotal": "700.00",
"vehicleFeeSource": "AUTO",
"vehicleFeeAdjustmentReason": null,
"vehicleFeeAdjustedBy": null,
"vehicleFeeAdjustedAt": null,
"settlementReady": true
},
{
"sourceDetailId": "2080000000000000003",
"serviceDate": "2026-07-29",
"assignmentGroupId": null,
"assignmentSlotId": null,
"vehicleId": "300000000000000003",
"vehiclePlate": "蒙A34567",
"vehicleModelId": "400000000000000003",
"vehicleModelName": "中巴",
"vehicleModel": "中巴",
"driverId": "500000000000000003",
"driverName": "巴雅尔",
"startDate": "2026-07-29",
"endDate": "2026-07-29",
"chargeableServiceDates": ["2026-07-29"],
"freeServiceDates": [],
"vehicleFeeWaiverReason": null,
"dailyPrice": "860.00",
"paymentTypeCode": "CASH_PAID",
"paymentTypeName": "现付",
"amount": "860.00",
"autoVehicleFeeTotal": "860.00",
"autoVehicleFeeComplete": true,
"vehicleFeeTotal": "860.00",
"vehicleFeeSource": "AUTO",
"vehicleFeeAdjustmentReason": null,
"vehicleFeeAdjustedBy": null,
"vehicleFeeAdjustedAt": null,
"settlementReady": true
}
]
}
}
}
```
说明:上例只展开 2026-07-29 的 3 行;同一订单还可能继续返回 2026-07-30、2026-07-31 的逐日车辆费用。验收样例中 3 天 x 3 司机共 9 行,合计 `6780.00`
### 8.2 边界情况:无有效车辆需求时返回空未冻结块
**请求**:
```http
GET /v3/admin/order/2079576729147338754/settlement/step3
Authorization: Bearer <token>
```
**响应**:
```json
{
"code": 200,
"message": "success",
"data": {
"totalActualCost": "0.00",
"items": [],
"vehicleFees": {
"orderId": "2079576729147338754",
"frozen": false,
"requirementId": null,
"settlementReady": false,
"totalAmount": "0.00",
"totalVehicleFee": "0.00",
"items": []
}
}
}
```
### 8.3 典型成功PUT Step3 只返回成功结果
**请求**:
```http
PUT /v3/admin/order/2079454953641836546/settlement/step3
Authorization: Bearer <token>
Content-Type: application/json
```
```json
{
"items": [
{
"id": "9300000000001",
"staffRole": "DRIVER",
"staffId": "6800001001",
"detail": {
"days": [
{
"service_date": "2026-07-29",
"vehicle_brief": "蒙A12345",
"daily_fee": "700.00",
"is_used": true,
"note": ""
}
],
"extra_cost": "0.00",
"extra_breakdown": []
},
"reimburse": "0.00",
"paymentMethod": "COMPANY_PAID",
"voucherUrls": [],
"settlementConfirmStatus": "UNCONFIRMED",
"settleStatus": "PENDING",
"settledDate": null,
"transferRef": null,
"remark": ""
}
]
}
```
**响应**:
```json
{
"code": 200,
"message": "success",
"data": null
}
```
### 8.4 业务失败PUT 时车辆费用未满足核单条件
**请求**:
```http
PUT /v3/admin/order/2079454953641836546/settlement/step3
Authorization: Bearer <token>
Content-Type: application/json
```
```json
{
"items": [
{
"id": "9300000000001",
"staffRole": "DRIVER",
"staffId": "6800001001",
"detail": {
"days": [
{
"service_date": "2026-07-29",
"vehicle_brief": "蒙A12345",
"daily_fee": "700.00",
"is_used": true,
"note": ""
}
],
"extra_cost": "0.00",
"extra_breakdown": []
},
"reimburse": "0.00",
"paymentMethod": "COMPANY_PAID",
"voucherUrls": [],
"settlementConfirmStatus": "UNCONFIRMED",
"settleStatus": "PENDING",
"settledDate": null,
"transferRef": null,
"remark": ""
}
]
}
```
**响应**:
```json
{
"code": 584101,
"message": "存在未完结派车或未确认车辆总车费,暂不能核单",
"data": null
}
```
## 9. 业务边界
- **适用场景**: 核单 Step3 页面查询和保存;页面需要同时展示人员费用与车辆费用时,直接使用 GET Step3 响应。
- **车辆费用可为空的场景**: 订单没有有效车辆需求时,GET Step3 返回 `vehicleFees.items=[]``frozen=false``settlementReady=false`、金额为 `0.00`
- **PUT 保存门禁**: 存在有效车辆需求时,PUT Step3 会校验车辆费用是否满足核单条件;不满足时返回 `584101`,本次人员费用保存不视为成功。
- **空明细门禁**: 存在有效车辆需求但没有可核单车辆费用明细时,PUT Step3 返回 `584102`
- **已冻结场景**: 车辆费用已冻结后,GET Step3 返回冻结后的 `vehicleFees`
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `data`PUT Step3 | 返回新增、更新、删除 ID 等操作数据 | 只表达成功或失败,成功时 `data=null` |
| `data.vehicleFees` | GET Step3 不返回 | GET Step3 返回车辆费用块 |
| `data.vehicleFees.frozen` | 无 | GET Step3 返回车辆费用是否冻结 |
| `data.vehicleFees.requirementId` | 无 | GET Step3 返回当前车辆需求 ID;无有效车辆需求时为 `null` |
| `data.vehicleFees.settlementReady` | 无 | GET Step3 返回车辆费用是否满足核单条件 |
| `data.vehicleFees.totalAmount` | 无 | GET Step3 返回车辆费用总金额 |
| `data.vehicleFees.totalVehicleFee` | 无 | GET Step3 返回车辆费用总金额兼容字段 |
| `data.vehicleFees.items[]` | 无 | GET Step3 返回逐日车辆费用明细 |
| `data.vehicleFees.items[].sourceDetailId` | 无 | GET Step3 返回逐日费用来源明细 ID |
| `data.vehicleFees.items[].serviceDate` | 无 | GET Step3 返回逐日服务日期 |
| `data.vehicleFees.items[].dailyPrice` | 无 | GET Step3 返回当日车费 |
| `data.vehicleFees.items[].paymentTypeCode` | 无 | GET Step3 返回车辆费用付款类型编码 |
| `data.vehicleFees.items[].paymentTypeName` | 无 | GET Step3 返回车辆费用付款类型名称 |
| `data.vehicleFees.items[].amount` | 无 | GET Step3 返回本条核单金额 |
| `data.vehicleFees.items[].settlementReady` | 无 | GET Step3 返回本条车辆费用是否满足核单条件 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 进入 Step3 页面 | 需要单独读取人员费用和车辆费用 | 调用 GET Step3 即可拿到人员费用与车辆费用 |
| 保存 Step3 | 只保存人员费用,响应可能携带操作数据 ID | 保存人员费用时同步校验车辆费用;成功响应 `data=null`,不返回新增、更新、删除 ID |
| 无有效车辆需求 | Step3 查询无法直接表达车辆费用空态 | GET Step3 内直接返回空未冻结 `vehicleFees` 块 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**: 否。GET Step3 响应新增 `vehicleFees`,PUT Step3 成功响应 `data=null`
- **前端是否必须同步上线**: 否,但建议管理后台 Step3 页面尽快切到 `data.vehicleFees`,并按 PUT Step3 成功响应不含操作数据 ID 处理。
### 11.2 回滚方案
- **回滚后前端表现**: 如果回滚到旧契约,GET Step3 不再包含 `data.vehicleFees`;前端需要保留对 `vehicleFees` 缺失的空值兼容。
- **前端兼容建议**: 读取 `data.vehicleFees` 前先判空;为空时按车辆费用空态展示。
## 12. 注意事项
- 新 Step3 页面读取车辆费用时优先使用 `GET /v3/admin/order/:orderId/settlement/step3` 返回的 `data.vehicleFees`
- `paymentTypeCode` 是车辆费用付款类型字段,枚举值为 `CASH_PAID``SIGNED``COMPANY_PAID`;不要用人员费用的 `paymentMethod` 去覆盖车辆费用字段。
- `totalAmount``totalVehicleFee` 都表示车辆费用总金额;为兼容旧页面,当前两者应按同一金额展示。
- `frozen=false` 不等于接口失败;无有效车辆需求或车辆费用尚未满足核单条件时都可能返回未冻结块。
## 验证证据
- 后端 PR [wx/HL#5297](https://git.1814.love:8443/wx/HL/pulls/5297) 已合并至 `dev-v3`,合并提交 `5194183f6`
- `dev-v3@ca23f64fc` 执行 `mvn -pl hl-order-service-v3 -am test` 通过。
- 测试环境滚动部署任务 `90374dab` 成功;`hl-order-service-v3` 8086/8186 双实例健康。
- 经测试网关只读调用 `GET /v3/admin/order/:orderId/settlement/step3`HTTP 200、业务码 200,响应包含 `vehicleFees`,样本返回 9 条逐日费用明细。
- 网关证据:`D:/work2/HL-v3/.tmp/5295-gateway.json`,SHA-256 `dafa5257c4df241e9511c95dba1f689397742133e9f46aefa4701a6d00228ab6`
- OpenAPI/oasdiff项目未配置可复现的 Swagger 2 到 OAS3 导出与 oasdiff,状态为 `not_configured`;已用 Controller/VO 源码对比、Controller 测试和真实网关响应完成人工回退核对。
- `frontend_status` 保持 `pending`;前端真实领取后再迁移为 `claimed` 并填写 `frontend_owner`
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5295](https://git.1814.love:8443/wx/HL/issues/5295)
- **PR**: [#5297](https://git.1814.love:8443/wx/HL/pulls/5297)
- **Merge commit**: [5194183f6](https://git.1814.love:8443/wx/HL/commit/5194183f6)
- **Feature commit**: [07d36dd39](https://git.1814.love:8443/wx/HL/commit/07d36dd3989789379388895b86a0954434013108)
### 13.2 联系人
- **接口负责人**: @yaosutu

查看文件

@ -1,135 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5301"
title: "配车矩阵统一手动加急状态与统计"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "e1d0a44a734fefc4f12ff1b71d218df0b265dc62"
target_release: ""
verified_at: "2026-07-28T14:38:26+08:00"
status_note: "hl-admin 已消费人工加急徽章、精确 statuses 筛选与 grid/month 五键统计;业务提交 e1d0a44a 已在 origin/v2.1 可达,checkpoint 通过"
updated_at: "2026-07-28"
base: "dev-v3"
---
# Fleet配车矩阵统一手动加急状态与统计
> **服务**`hl-fleet-service`
> **Issue**#5301
> **影响页面**:管理后台车务管理 → 配车矩阵
> **兼容性**:只新增可选请求参数与响应字段;路径、方法、既有字段、持久化状态不变
## 问题与目标
派车看板已经使用当前有效用车需求的 `manualUrgent` 派生人工加急,配车矩阵此前只计算临近出团与 HOLD 超时自动紧急态。同一需求因此可能在看板显示加急、矩阵仍显示普通待派/待确认,矩阵筛选和月份统计也无法精确区分人工加急。
本次后端统一矩阵全部读视图的有效状态来源,并提供稳定的人工加急字段、中文标签、精确状态筛选和五键计数。人工加急不修改落库派单状态。
## 变更接口
### `GET /admin/fleet/matrix/grid`
#### 1. 新增请求参数 `statuses`
| 参数 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `statuses` | `String[]` | 否 | 按有效状态精确筛选,多值取并集;非空且至少含一个合法值时优先于旧 `status` |
合法值:
- `unassigned`
- `unassigned_urgent`
- `holding`
- `holding_urgent`
- `assigned`
- `completed`
兼容规则:
- 旧 `status=unassigned` 继续同时包含 `unassigned``unassigned_urgent`
- 旧 `status=assigned` 继续包含 `holding``holding_urgent``assigned`
- `statuses` 全空或全非法时回退旧 `status`
- 未传二者时保持全部展示。
请求示例:
```http
GET /admin/fleet/matrix/grid?year=2026&month=8&season=active&statuses=unassigned_urgent&statuses=holding_urgent
```
#### 2. `data.vehicles[].assignments[]` 新增字段
| 字段 | 类型 | 空值规则 | 说明 |
|---|---|---|---|
| `manualUrgent` | `Boolean` | 固定 `true/false` | 当前有效用车需求是否人工加急;order-v3 上下文整体不可用时为 `false`,不得前端猜测 |
| `assignmentStatusLabel` | `String` | 正常非空 | 后端统一有效状态中文标签,例如“待派车”“待确认”“已派车” |
| `urgentBadge` | `String` | 非紧急为空 | 人工加急固定“手动加急”;自动紧急继续返回既有 T-N/HOLD 超时文案 |
有效状态规则:
- 基础态 `unassigned``manualUrgent=true``assignmentStatus=unassigned_urgent`
- 基础态 `holding``manualUrgent=true``assignmentStatus=holding_urgent`
- 人工加急优先于临近出团/HOLD 超时自动派生;
- `assigned``completed` 不因人工加急改变;`canceled` 继续排除;
- 只消费当前有效需求上下文,旧需求派单不会继承当前需求的人工加急。
#### 3. `data.statusCounts.effectiveStatusCounts` 新增固定五键
```json
{
"unassigned": 2,
"unassigned_urgent": 1,
"holding": 3,
"holding_urgent": 1,
"assigned": 4
}
```
- 五个键始终存在,缺类为 `0`
- 每个活跃派车组只进入一个精确状态;
- 五键之和恒等于既有 `statusCounts.totalAssignments`
- 既有 `unassignedAssignments``assignedAssignments` 和订单级统计保持兼容聚合口径,不删除、不改名。
### `GET /admin/fleet/matrix/month-counts`
每月 `statusCounts` 同样新增 `effectiveStatusCounts`
- 固定返回 1–12 月,零值月份不省略;
- 同样固定五键;
- 与相同 `year/month/season/fleetTeamIds/typeKeys` 的 grid 顶部统计守恒;
- 年度读取仍为一次批量上下文,不循环产生 N+1。
## 前端必须调整
1. 派车条直接展示后端 `assignmentStatusLabel`;不要在前端维护第二套中文状态映射。
2. `manualUrgent=true` 且状态为 `unassigned_urgent/holding_urgent` 时,展示 `urgentBadge=手动加急`,颜色与派车看板人工加急保持一致。
3. 需要精确状态 Tab/筛选时改传 `statuses[]`;不要用旧 `status=unassigned` 期待只命中普通待派。
4. 精确分类数字使用 `statusCounts.effectiveStatusCounts`;既有全部/未派/已派聚合卡片可继续使用旧统计字段。
5. 月份切换统计直接使用 `month-counts[].statusCounts.effectiveStatusCounts`,不要前端遍历当前月卡片重算。
6. 继续保持 #5299 的实际车辆/司机/常驻标记和 Long ID 字符串处理;禁止转为 JS `Number`
## 不影响范围
- 不修改人工加急写接口或 order-v3 需求状态。
- 不修改派单落库状态、车辆/司机占用、费用、保险、对账或常驻关系。
- 不新增 Feign、数据库查询或逐订单 N+1。
- 不修改 `hl-ui` 仓库;前端消费状态独立流转。
## 验证证据
- 后端 PR [wx/HL#5304](https://git.1814.love:8443/wx/HL/pulls/5304) 已 squash 合并至 `dev-v3`,合并提交 `ed56bec1f`
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康。
- 定向 `MatrixServiceTest,MatrixControllerTest,BoardCandidateSourceTest`51 tests,0 failures,0 errors,0 skipped;覆盖人工加急、自动回退、精确筛选、五键守恒、当前需求门禁与全局降级。
- Fleet reactor verify2474 tests,0 failures,0 errors,1 skipped;Spotless 628 Java files clean。
- 真实测试网关grid 新字段完整且 `manualUrgent` 全为非空 Boolean;五键固定且和等于 `totalAssignments``statuses=assigned` 在旧 `status=unassigned` 同时传入时仍只返回 assigned,证明精确筛选优先;month-counts 固定 12 月且同月五键与 grid 相等;Long ID 为字符串、手机号脱敏、全程无业务写入。
- 当前测试矩阵数据没有 `manualUrgent=true` 的活跃派车组,因此真实网关未伪报人工加急正例;正例由 Service/Controller 定向测试覆盖。
- 网关证据:`D:/work2/HL-v3/.tmp/5301-gateway.json`,SHA-256 `6ae185282e2194a63168298168888f4c03086916b7ad91268b24ac133b2b363d`
- OpenAPI/oasdiff项目未配置可复现 Swagger2→OAS3 与 oasdiff,状态为 `not_configured`;使用源码字段对比、Controller/Service 测试及真实网关响应作为人工回退证据。Spring Cloud Contract 为 `not_required`
当前状态:后端已部署、网关已验证,前端消费保持 `pending`
关联:#5301#5299

查看文件

@ -1,89 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5302"
title: "派车档期按完整组判定同城衔接"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "仅修正候选与预校验的既有冲突语义,不新增字段或页面动作;前端继续按后端 available、availabilityReasonCode、conflicts 与 conflict 渲染即可"
updated_at: "2026-07-28"
base: "dev-v3"
---
# Fleet派车档期按完整组判定同城衔接
> **服务**`hl-fleet-service`
> **Issue**#5302
> **影响页面**:管理后台车务管理 → 派单候选弹窗、保存前预校验
> **兼容性**:路径、方法、请求/响应字段和错误码形状不变,仅修正既有业务语义
## 问题与目标
派车数据已按服务日期拆成逐日切片。同一辆车或司机已有 D1–D3 多日派车时,旧实现只读取新请求窗口内命中的切片;若新请求只查内部日 D2,D2 会被误当作旧派车组的真实首日/末日,并可能错误套用 R3-EX“真实首尾同城可衔接”,把双重占用误报为可共享。
本次后端在初筛命中后一次批量补齐同 `assignmentGroupId` 的全部有效切片,再按完整组真实首日、末日及首接客地、末送客地判断冲突。候选咨询、保存前预校验及最终写侧重校验使用同一口径。
## 变更接口
### `POST /admin/fleet/assignments/candidates`
响应形状不变,以下既有字段的语义修正:
| 场景 | `available` | `availabilityReasonCode` | `conflicts[].blocking` | `conflicts[].cityJunctionShareCandidate` |
|---|---:|---|---:|---:|
| 请求日位于已有多日派车组内部 | `false` | `ASSIGNMENT_CONFLICT` | `true` | `false` |
| 请求日仅与完整组真实首日或末日相接,且接送城市满足 R3-EX | `true` | `CITY_JUNCTION_SHAREABLE` | `false` | `true` |
| 无重叠 | `true` | `AVAILABLE` | - | - |
内部日冲突的 `conflicts[].startDate/endDate` 返回完整有效派车组范围;`availabilityWindows` 按该真实阻断范围扣减,不再把内部日当成可用窗口。
车辆和司机分别采用同一口径。仅查询 `holding/assigned` 活跃切片;历史空 `assignmentGroupId` 数据继续按原记录区间判断。
### `POST /admin/fleet/assignments/precheck`
响应形状不变:
- 内部日双重占用返回 `data.conflict=true`
- `data.conflicts[]` 的车辆、司机冲突日期段均为完整有效派车组范围;
- 内部日 `cityJunctionShareCandidate=false`
- 真实首尾同城衔接仍按既有 R3-EX 返回非阻断结果。
最终创建、改派、车务确认及撤销取消恢复仍在资源锁内重新校验,不信任前端咨询结果;本次同步修正这些写侧入口,避免候选正确但最终写入口径不同。
## 前端处理
无需修改前端代码或请求参数:
1. 继续以候选返回的 `available``availabilityReasonCode` 控制可选状态与原因展示;
2. 继续以预校验返回的 `conflict` 决定是否阻止提交;
3. 不要在前端自行按单日日期或城市覆盖后端冲突结果;
4. 已有 `ASSIGNMENT_CONFLICT``CITY_JUNCTION_SHAREABLE` 展示分支可直接消费修正后的数据。
因此 `frontend_status=not_required`
## 不影响范围
- 不新增或删除 API 字段,不修改 Long ID 字符串、手机号脱敏及错误码契约。
- 不修改派单状态机、占用写入、保险、价格、对账或车辆/司机常驻关系。
- 不将 `completed/canceled` 切片重新计入活跃占用。
- 不修改 `hl-ui` 仓库。
## 验证证据
- 后端 PR [wx/HL#5303](https://git.1814.love:8443/wx/HL/pulls/5303) 已 squash 合并至 `dev-v3`,合并提交 `d114232ad`
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康。
- 定向 `AssignmentServiceTest,AssignmentCandidateServiceTest,FleetAssignmentMapperTest`342 tests,0 failures,0 errors,0 skipped。
- Fleet reactor verify2466 tests,0 failures,0 errors,1 skipped;Spotless 628 Java files clean。
- 真实测试网关选择一个 D1–D4 活跃派车组的内部日 D2 调用候选与预校验HTTP/code 200;车辆与司机均 `available=false``ASSIGNMENT_CONFLICT``blocking=true``cityJunctionShareCandidate=false`,冲突范围返回完整 D1–D4;全程无业务写入。
- 网关证据:`D:/work2/HL-v3/.tmp/5302-gateway.json`,SHA-256 `b482a78d4129998c0ca3a8ee0e9a10ee63dfb25239fe1bd041b17f51043d4524`
- OpenAPI/oasdiff项目未配置可复现 Swagger2→OAS3 与 oasdiff,状态为 `not_configured`;使用源码对比、定向测试和真实网关响应作为人工回退证据。Spring Cloud Contract 为 `not_required`
当前状态:后端已部署、网关已验证,前端无需改造。
关联:#5302

查看文件

@ -1,79 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5305"
title: "逐日方案重提与完成闭环不变量"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "仅修正既有逐日派车保存与需求完成事务语义;接口路径、方法、请求响应字段和前端交互均不变"
updated_at: "2026-07-28"
base: "dev-v3"
---
# Fleet逐日方案重提与完成闭环不变量
> **服务**`hl-fleet-service`
> **Issue**#5305
> **影响页面**:管理后台车务管理 → 逐日逐车派车方案保存
> **兼容性**:接口路径、方法、请求/响应字段、错误码和数据库结构不变,仅修正既有事务语义
## 问题与目标
逐日派车支持重复提交最终方案。旧实现存在五类边界:全程不用车或没有实际创建命令时会绕过最终订单基线;留车/直派模式变化可能被误判为无变化;混合方案中的历史已完结切片可能因旧冻结标记缺失阻断需求完成;仅修改逐日价格原因不会落库;同槽索引的旧取消版本可能抢占当前稳定槽位。
本次后端统一修复上述重提、冻结和完成闭环,不新增前端参数或返回字段。
## 变更接口(既有语义修正)
### `POST /admin/fleet/assignments/batch`
请求和响应形状不变:
1. 包含 `dailyPlan` 的保存会在资源锁内完成写入后、冻结最终方案和生成需求完成 Outbox 前,再次读取当前订单详情、行程和 active 用车需求;日期、人数、行程或需求版本漂移时整批事务回滚。
2. 即使全部 `dailyPlan[].used=false`,或本次没有实际创建车辆/司机派单,也执行同一最终基线门禁。
3. 重提时同时比较目标 `holdMode`、派单状态、车辆、司机、接机参与、逐日价格和逐日价格调整原因:
- `holding → direct` 会按直派目标重新落地;
- `assigned → hold` 会按留车目标重新落地;
- 原因新增、修改和在不再偏离日历价时清空不再被忽略;偏离日历价时原因必填规则保持不变。
4. 历史 `completed` 逐日切片即使来自旧数据、没有 `dispatchPlanFinalized=1`,在同一矩阵已有显式冻结切片时也按不可变的已完成权威日处理;不修改其业务状态,也不会把纯旧版未冻结拓扑误认成新最终方案。
5. 同一 `fleetItemIndex` 同时存在旧 `canceled` 版本和当前在途/非取消版本时,稳定槽位优先沿用当前版本,避免重写命中历史槽位。
所有失败仍沿用现有错误响应结构;事务失败不产生需求完成事件。
## 前端处理
无需修改前端代码:
- 继续提交现有 `dailyPlan``holdMode` 和逐日价格原因;
- 基线漂移时继续展示后端现有失败提示并刷新后重试;
- 不需要新增字段、状态分支或页面组件。
因此 `frontend_status=not_required`
## 不影响范围
- 不修改 Controller、VO/DTO/BO、Feign 或 shared Java 契约。
- 不修改数据库字段、迁移、Long ID 字符串和手机号脱敏规则。
- 不改变已完成/已取消业务状态,不放宽历史、完结或关账只读门禁。
- 不修改 `hl-ui` 仓库。
## 验证证据
- 后端 PR `[wx/HL#5306](https://git.1814.love:8443/wx/HL/pulls/5306)` 已 squash 合并至 `dev-v3`,合并提交 ``594d932db``。
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康。
- 定向 `AssignmentServiceTest,FleetAssignmentMapperTest`328 tests,0 failures,0 errors,0 skipped。
- Fleet reactor verify2480 tests,0 failures,0 errors,1 skipped;Spotless 628 Java files clean。
- 独立 Reviewer 无未解决 P0–P1。
- 真实测试网关通过登录和车务矩阵只读查询确认部署后网关、认证及 Fleet 服务链路正常;本次内部事务修正没有可安全构造的写侧正例,核心五类边界由定向测试覆盖,探针不产生业务写入。
- 网关证据:`D:/work2/HL-v3/.tmp/5305-gateway.json`,SHA-256 ``4698b3b8b1765715f6dc823664b3d24df2e977d26e6e19da08b5e298c6b6b0f6``。
- 契约审查:`not_required`;当前 diff 无 Controller、请求响应模型、Feign 或 shared Java 变化。
当前状态:后端已部署、网关兼容探针已通过,前端无需改造。
关联:#5305

查看文件

@ -1,84 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5307"
title: "最终不用车稳定槽看板状态与筛选"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端 PR #5309 已合并并部署测试环境;既有字段的稳定槽聚合与状态筛选语义已修正,前端无需新增字段或改造"
updated_at: "2026-07-28"
base: "dev-v3"
---
# Fleet最终不用车稳定槽看板状态与筛选
> **服务**`hl-fleet-service`
> **Issue**#5307
> **影响页面**:管理后台车务看板、配车矩阵、未派窗口及当天清单
> **兼容性**:接口路径、方法、请求参数和响应字段均不变化;仅修正既有状态、代表资源、统计和筛选语义
## 问题与目标
逐日最终方案允许同一稳定车辆槽位部分日期实际用车、其他日期明确不用车。此前最终不用车切片仍以基础状态 `unassigned` 持久化,会覆盖同槽真实已派切片,导致看板误显示待派、代表车辆/司机为空,并使状态筛选和矩阵统计出现幽灵待派。
本次统一识别 `dispatchPlanFinalized=1,dailyVehicleUsed=0` 的最终不用车事实。该事实不占车辆/司机、不投保、不计费,也不能重新开放普通派车动作。
## 变更接口
### 车务看板 `/admin/fleet/board/orders`
- 同一稳定槽存在实际用车日和最终不用车日时,槽位状态、代表车辆和代表司机来自真实用车切片;稳定槽日期范围仍覆盖全部当前有效服务日。
- 全日期均最终不用车时,仅在看板只读聚合中归入“最终方案已完成”的既有 `assigned` 分面;数据库业务状态仍保持 `unassigned`,不会伪造派车、取消或完结事件。
- 全日期最终不用车记录固定关闭 `canAssign``canRejectRequirement``availableActionCodes` 和紧急徽章,不开放重复派车或需求驳回。
- `assignmentProgress` 按权威稳定槽守恒:混合槽和全最终不用车槽不再计入 `unassignedSlots`
- `status/statuses` 先取得基础态候选超集,输出前再按最终稳定槽状态复筛;`assigned``unassigned` 互斥筛选不再返回同一需求。
- 大候选游标扫描会在 5,000 安全上限前排除普通待派,只保留可能聚合为最终方案完成的 final-unused 候选。
### 配车矩阵 `/admin/fleet/matrix/*`
- 最终不用车切片不进入车辆甘特条、顶部待派统计、月份统计、未派窗口、当天待派清单、并行派车或衔接计算。
- 混合槽仍展示所有真实用车车辆段及实际司机,不由无车切片覆盖。
- 全日期最终不用车不会产生幽灵待派;详情接口原有 `dailyVehiclePlan[].planState=NOT_USED` 事实保持不变。
- 普通未最终化 `unassigned``holding/assigned/completed`、人工加急五键统计、Long ID 字符串和手机号脱敏均保持原语义。
## 前端处理
无需新增字段或修改请求。继续直接消费后端现有:
- `assignmentStatus``assignmentStatusLabel`
- `canAssign``canRejectRequirement``availableActionCodes`
- `assignmentProgress.finalizedByFleet`
- `assignmentSlots[]`
- 矩阵 `statusCounts``effectiveStatusCounts`
- 详情 `dailyVehiclePlan[].planState`
不要根据基础 `unassigned`、车辆为空或司机为空自行覆盖后端返回的稳定槽状态和操作权限。
## 不影响范围
- 不修改派单五态数据库状态机。
- 不修改逐日方案写入、车辆/司机占用、费用、保险、对账或完成回调。
- 不新增 Feign、数据库查询或逐订单 N+1。
- 不修改 `hl-ui`;本次为既有字段语义纠正,前端状态为 `not_required`
## 验证证据
- 后端 PR [wx/HL#5309](https://git.1814.love:8443/wx/HL/pulls/5309) 已 squash 合并至 `dev-v3`,合并提交 `ca23f64fc`
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康。
- 定向核心395 tests,0 failures,0 errors;Reviewer 上限回归 `BoardOrderServiceTest` 59 tests 通过。
- Fleet reactor verify2487 tests,0 failures,0 errors,1 skipped;Spotless 629 Java files clean。
- 独立 Reviewer 首轮发现 assigned 候选超集可能提前触发 5,000 上限;修复并补充“5,000 普通待派 + 1 最终不用车”游标测试后,终审无 P0–P2。
- 真实测试网关只读验证:矩阵精确状态筛选仅返回请求状态、五键统计守恒;看板 assigned/unassigned 返回状态与筛选一致且需求集合互斥;已完成最终方案未进入 unassigned;Long ID 仍为字符串、手机号保持脱敏,全程无业务写入。
- 测试环境 API 未暴露内部 final-unused 标记且当前样本未确认存在合法混合槽,因此未伪报真实网关正例;混合槽和全 final-unused 正例由 Service 定向测试覆盖。
- 网关证据:`D:/work2/HL-v3/.tmp/5307-gateway.json`,SHA-256 `699363c3a06b0d40205ea477c2a0a67daadc2e1718d08ba1102733e66cf2d505`
- OpenAPI 与 Spring Cloud Contract`not_required`,因为 Controller、DTO/VO/BO、Feign、shared Java、枚举及错误码形状均未变化。
当前状态:后端已部署、网关已验证,前端无需改造。
关联:#5307#5292#5301

查看文件

@ -1,93 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5308"
title: "最终派车方案取消恢复代际"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "仅修正 Fleet 内部最终方案身份、取消恢复与需求完成语义;接口路径、请求响应字段和前端交互均不变"
updated_at: "2026-07-28"
base: "dev-v3"
---
# Fleet最终派车方案取消恢复代际
> **服务**`hl-fleet-service`
> **Issue**#5308
> **影响页面**:管理后台车务管理 → 逐日派车方案保存、手工取消/恢复、DIRECT/HOLD 改派
> **兼容性**:接口路径、方法、请求/响应字段、错误码及业务五态不变;仅修正内部最终方案身份和需求完成闭环
## 问题与目标
自定义最终方案完成后,手工取消某个稳定车辆槽位会触发需求重开,恢复后应重新完成。旧实现只有布尔型最终标记,无法区分当前方案、合法改派 tombstone 和后续重提的新方案;多槽部分取消时,剩余槽位可能被误判为新的完整方案,旧取消行也可能在新方案后被错误恢复。
本次为 Fleet 内部最终派车方案增加持久化代际。代际是数据库内部不透明令牌,不进入 API、完成回调或前端状态。
## 变更接口(既有语义修正)
### `POST /admin/fleet/assignments/batch`
请求和响应形状不变:
1. 每次最终方案提交会按精确 `assignmentIds` 将全部当前可变切片标记为同一新代,并严格核对实际更新行数;精确重提、仅价格原因重提和 partial daily rewrite 均按新当前代收敛。
2. 当前方案按 `stable slotId × serviceDate` 全拓扑校验。多个非空 current generation、当前代外 active 行、孤儿 generation,以及存在退休历史但没有可解析非空 current 代的场景均 fail closed,不再回退为订单建议数量的“看似完整”方案。
3. 滚动发布窗口仅兼容“单一非空 current generation + `dispatchPlanFinalized=1` 的 legacy null 行”;这些行按同代完整拓扑校验,不按 generation 数值大小推断先后。
### 既有取消、恢复与改派接口
路径和字段均不变:
- 手工取消保留当前代身份;只恢复一个被取消槽位时仍保持不完整,全部当前代逻辑 key 恢复后才重新生成或重启 `REQUIREMENT_DONE`
- REOPEN 先到时,恢复后按相同 topology fingerprint 重新激活完成事件;恢复先到时,迟到 REOPEN 会因当前拓扑已完整而跳过,不回退需求状态。
- DIRECT/HOLD 正常改派的 replacement 继承当前代。合法 canceled tombstone 在存在唯一同代 active replacement 时不污染完整性;HOLD 在确认前仍不完整。
- 已退休旧代、已有同代 active replacement 的 tombstone、以及全局失效后仅保留退休代际证据的取消行均禁止恢复。
- driver reject、daily rewrite、订单/需求系统取消会退休 current 标记并保留 generation 作为禁止恢复证据;跨 requirement rebind 会清除旧身份,下一次最终提交建立新代。
- `completed``canceled` 业务状态和历史/完结/关账只读规则不变;generation 不进入完成 topology fingerprint。
## 数据库迁移
新增 Fleet 内部 nullable BIGINT`fleet_assignment.dispatch_plan_generation`
- 迁移先把存量 `canceled + dispatch_plan_finalized=1` 的历史改派 tombstone 退休为非 current。
- 再仅对非 canceled 的存量 current 行按 requirement 回填兼容 generation。
- canceled 历史不猜测代际;无新增索引,读取仍按 `requirement_id` 批量完成。
## 前端处理
无需修改前端代码:
- 继续使用现有逐日方案保存、取消、恢复、DIRECT/HOLD 改派接口和状态字段;
- 不新增 `generation` 请求或响应字段,不应在前端推断方案代际;
- 状态刷新、错误提示和页面交互保持现状。
因此 `frontend_status=not_required`,且不修改 `hl-ui`
## 不影响范围
- 不修改 Controller mapping、请求/响应 DTO/VO/BO、Feign、shared Java、枚举或错误码。
- 不修改车辆/司机冲突规则、费用、保险、对账或业务五态。
- 不按 Snowflake 数值比较代际先后。
- 不产生逐槽或逐候选 N+1。
## 验证证据
- 后端 PR [wx/HL#5312](https://git.1814.love:8443/wx/HL/pulls/5312) 已 squash 合并至 `dev-v3`,合并提交 `883fc61a0`
- `hl-fleet-service` 已滚动部署测试环境,8087/8187 双实例健康;验证部署任务 `493f862d` 成功。
- 定向 `AssignmentServiceTest`302 tests,0 failures,0 errors,0 skipped。
- Fleet reactor verify2507 tests,0 failures,0 errors,1 skipped;Spotless 629 Java files clean。
- 真表 BIGINT 落库读回、全局失效退休证据及迁移先退休 tombstone 再回填 active 的行为测试通过。
- 两轮独立 Reviewer 共发现 4 个 P1,均已修复并补充回归测试;无未解决 P0/P1。
- 真实测试网关只读验证:矩阵精确状态筛选和统计守恒、看板 assigned/unassigned 互斥、Long ID 字符串及手机号脱敏均保持兼容,全程无业务写入。
- 内部 generation 不通过网关暴露,且取消/恢复正例必须产生业务写入,因此未伪造网关正例;完整代际生命周期由 Service、Mapper 真表和迁移测试覆盖。
- 网关证据:`D:/work2/HL-v3/.tmp/5308-gateway.json`,SHA-256 `3778da1c8c0d270e3d1da75821e54bfce49ea25bc36f09e8440932ed6e4efa07`
- OpenAPI 与 Spring Cloud Contract`not_required`,因为无 API/Feign/shared Java 契约形状变化。
当前状态:后端已合并、部署并通过网关兼容探针,前端无需改造。
关联:#5308#5292#5305

查看文件

@ -1,892 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5310"
title: "核单其他收支保存即确认"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi:019fadb7-dac9-74bf-9581-058835251208"
frontend_ref: "1444fc7f0bf34efaec0ee9f775f7d529b609b847"
target_release: "v2.1"
verified_at: "2026-07-29T20:43:00+08:00"
status_note: "管理后台已停止调用已删除的其他收支独立确认接口;保存行为按后续 #5320 最终契约显式提交确认状态,pnpm checkpoint 全部通过。"
updated_at: "2026-07-28"
base: "dev-v3"
generated: "2026-07-28T11:54:21+08:00"
---
# 【修改接口·管理后台】核单其他收支保存即确认 (#5310)
> **PR**: #5313 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-28 11:54
## 1. 接口背景
核单「其他收入」「其他支出」不再需要先保存、再单独点确认。保存成功即视为已确认,前端不再调用独立确认接口。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 新增其他收入并原子创建订单增费 | POST | `/v3/admin/order/{orderId}/settlement/other-incomes` | 修改 | 保存成功后返回 `settlementConfirmStatus=CONFIRMED``settlementConfirmStatusName=已确认` |
| 2 | 修改其他收入;金额或项目名变化时原子冲销并重建订单增费 | PUT | `/v3/admin/order/{orderId}/settlement/other-incomes/{incomeId}` | 修改 | 保存成功后返回 `settlementConfirmStatus=CONFIRMED``settlementConfirmStatusName=已确认` |
| 3 | 新增其他支出 | POST | `/v3/admin/order/{orderId}/settlement/other-expenses` | 修改 | 保存成功后返回 `settlementConfirmStatus=CONFIRMED` |
| 4 | 修改其他支出 | PUT | `/v3/admin/order/{orderId}/settlement/other-expenses/{settlementId}` | 修改 | 保存成功后返回 `settlementConfirmStatus=CONFIRMED` |
| 5 | 批量确认其他收入 | POST | `/v3/admin/order/{orderId}/settlement/other-incomes/confirm` | 删除 | 接口删除;前端不要再调用 |
| 6 | 确认其他支出 | POST | `/v3/admin/order/{orderId}/settlement/other-expenses/confirm` | 删除 | 接口删除;前端不要再调用 |
## 3. 接口详情
### 3.1 新增其他收入
- **方法 + 路径**`POST /v3/admin/order/{orderId}/settlement/other-incomes`
- **接口名**:新增其他收入并原子创建订单增费
- **使用场景**:在核单其他收入页新增一条其他收入。
- **认证**:需要管理后台登录态;需要资金写入权限。
- **幂等性**:是;同一订单内 `requestId` 永久唯一,相同 `requestId` 且请求载荷一致时返回同一条记录。
- **限流**:无接口专属限流。
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | Long | 是 | 订单 ID,必须大于 0 |
**请求体字段**
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `requestId` | String | 是 | 客户端生成的稳定幂等请求 ID,同订单内永久唯一 | 非空,最长 64 字符 |
| `incomeDate` | String(date) | 是 | 收入日期,格式 `YYYY-MM-DD` | 非空 |
| `projectName` | String | 是 | 项目名称 | 非空,最长 100 字符 |
| `projectCategory` | String | 是 | 项目类别 | 非空,最长 64 字符 |
| `specification` | String | 否 | 票种/规格 | 最长 100 字符 |
| `quantity` | Decimal | 是 | 数量 | >= 0,最多 8 位整数、4 位小数 |
| `unitPrice` | Decimal | 是 | 核算单价 | >= 0,最多 8 位整数、2 位小数 |
| `settlementAmount` | Decimal | 是 | 核算金额 | > 0,最多 8 位整数、2 位小数;必须等于 `quantity * unitPrice` 四舍五入到 2 位 |
| `paymentMethod` | String | 是 | 付款类型 | `CASH_PAID` / `COMPANY_PAID` / `SIGNED` |
| `voucherUrls` | String[] | 否 | 凭证 URL 列表 | 最多 9 项;每项最长 1024 字符;必须是 http/https |
| `remark` | String | 否 | 备注 | 最长 500 字符 |
**响应字段:`Result<SettlementOtherIncomeItemRespVO>`**
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务码,成功为 `200` |
| `msg` | String | 响应消息 |
| `data.id` | String | 其他收入 ID |
| `data.requestId` | String | 手工新增幂等请求 ID;自动投影为空 |
| `data.incomeDate` | String(date) | 收入日期 |
| `data.projectName` | String | 项目名称 |
| `data.projectCategory` | String | 项目类别 |
| `data.projectCategoryName` | String | 项目类别名称 |
| `data.specification` | String | 票种/规格 |
| `data.quantity` | Decimal | 数量 |
| `data.unitPrice` | Decimal | 核算单价 |
| `data.settlementAmount` | Decimal | 核算金额 |
| `data.paymentMethod` | String | 付款类型 |
| `data.paymentMethodName` | String | 付款类型名称 |
| `data.voucherUrls` | String[] | 凭证 URL 列表 |
| `data.settlementConfirmStatus` | String | 确认状态;本接口保存成功返回 `CONFIRMED` |
| `data.settlementConfirmStatusName` | String | 确认状态名称;本接口保存成功返回 `已确认` |
| `data.remark` | String | 备注 |
| `data.sourceType` | String | 来源类型 |
| `data.sourceTypeName` | String | 来源类型名称 |
| `data.sourceId` | String | 来源附加费 ID |
**错误码**
| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、枚举非法、凭证 URL 非 http/https、金额不等于数量乘单价 |
| `584074` | 当前核单状态不允许修改其他收入 | 订单核单状态不是待核单或核单中 |
| `584075` | 其他收入关联的附加费来源无效 | 保存后无法得到有效来源记录 |
| `584076` | 其他收入核算金额必须等于数量乘以核算单价 | `settlementAmount``quantity * unitPrice` 不一致 |
| `584087` | requestId 已用于另一笔其他收入 | 同订单重复使用 `requestId`,但请求载荷不同 |
| `584089` | 核单或结算已完成,资金数据不可再修改 | 订单资金数据已锁定 |
**示例:典型成功**
请求:
```http
POST /v3/admin/order/60001/settlement/other-incomes
Authorization: Bearer <token>
Content-Type: application/json
```
```json
{
"requestId": "oi-20260728-0001",
"incomeDate": "2026-07-28",
"projectName": "临时加收房差",
"projectCategory": "房差",
"specification": "双人间",
"quantity": 2,
"unitPrice": 120.00,
"settlementAmount": 240.00,
"paymentMethod": "CASH_PAID",
"voucherUrls": ["https://cdn.example.com/vouchers/income-1.jpg"],
"remark": "现场补收"
}
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "99001",
"requestId": "oi-20260728-0001",
"incomeDate": "2026-07-28",
"projectName": "临时加收房差",
"projectCategory": "房差",
"projectCategoryName": "房差",
"specification": "双人间",
"quantity": 2,
"unitPrice": 120.00,
"settlementAmount": 240.00,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"voucherUrls": ["https://cdn.example.com/vouchers/income-1.jpg"],
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "现场补收",
"sourceType": "ORDER_SURCHARGE",
"sourceTypeName": "订单增费",
"sourceId": "88001"
}
}
```
**示例:边界成功**
请求:
```json
{
"requestId": "oi-20260728-0002",
"incomeDate": "2026-07-28",
"projectName": "其他收入",
"projectCategory": "其他",
"quantity": 0.0001,
"unitPrice": 100.00,
"settlementAmount": 0.01,
"paymentMethod": "SIGNED",
"voucherUrls": [],
"remark": null
}
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "99002",
"requestId": "oi-20260728-0002",
"incomeDate": "2026-07-28",
"projectName": "其他收入",
"projectCategory": "其他",
"projectCategoryName": "其他",
"specification": null,
"quantity": 0.0001,
"unitPrice": 100.00,
"settlementAmount": 0.01,
"paymentMethod": "SIGNED",
"paymentMethodName": "签单",
"voucherUrls": [],
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": null,
"sourceType": "ORDER_SURCHARGE",
"sourceTypeName": "订单增费",
"sourceId": "88002"
}
}
```
**示例:业务失败**
请求:
```json
{
"requestId": "oi-20260728-0003",
"incomeDate": "2026-07-28",
"projectName": "加收费用",
"projectCategory": "其他",
"quantity": 2,
"unitPrice": 100.00,
"settlementAmount": 199.00,
"paymentMethod": "CASH_PAID"
}
```
响应:
```json
{
"code": 584076,
"msg": "其他收入核算金额必须等于数量乘以核算单价",
"data": null
}
```
### 3.2 修改其他收入
- **方法 + 路径**`PUT /v3/admin/order/{orderId}/settlement/other-incomes/{incomeId}`
- **接口名**:修改其他收入;金额或项目名变化时原子冲销并重建订单增费
- **使用场景**:修改已有其他收入;也用于把存量 `UNCONFIRMED` 记录重新保存为 `CONFIRMED`
- **认证**:需要管理后台登录态;需要资金写入权限。
- **幂等性**:否。
- **限流**:无接口专属限流。
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | Long | 是 | 订单 ID,必须大于 0 |
| `incomeId` | Long | 是 | 其他收入 ID,必须大于 0 |
**请求体字段**
同 §3.1,但不包含 `requestId`
**响应字段**
同 §3.1;保存成功后 `data.settlementConfirmStatus=CONFIRMED``data.settlementConfirmStatusName=已确认`
**错误码**
| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、枚举非法、金额不一致 |
| `584073` | 其他收入不存在或不属于当前订单 | `incomeId` 不存在或不属于 `orderId` |
| `584074` | 当前核单状态不允许修改其他收入 | 订单核单状态不是待核单或核单中 |
| `584076` | 其他收入核算金额必须等于数量乘以核算单价 | `settlementAmount``quantity * unitPrice` 不一致 |
| `584089` | 核单或结算已完成,资金数据不可再修改 | 订单资金数据已锁定 |
**示例:典型成功**
请求:
```http
PUT /v3/admin/order/60001/settlement/other-incomes/99001
Authorization: Bearer <token>
Content-Type: application/json
```
```json
{
"incomeDate": "2026-07-28",
"projectName": "临时加收房差",
"projectCategory": "房差",
"specification": "双人间",
"quantity": 2,
"unitPrice": 130.00,
"settlementAmount": 260.00,
"paymentMethod": "COMPANY_PAID",
"voucherUrls": ["https://cdn.example.com/vouchers/income-2.jpg"],
"remark": "修改金额"
}
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "99001",
"requestId": "oi-20260728-0001",
"incomeDate": "2026-07-28",
"projectName": "临时加收房差",
"projectCategory": "房差",
"projectCategoryName": "房差",
"specification": "双人间",
"quantity": 2,
"unitPrice": 130.00,
"settlementAmount": 260.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": ["https://cdn.example.com/vouchers/income-2.jpg"],
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "修改金额",
"sourceType": "ORDER_SURCHARGE",
"sourceTypeName": "订单增费",
"sourceId": "88003"
}
}
```
**示例:边界成功(存量未确认重存)**
请求:
```json
{
"incomeDate": "2026-07-20",
"projectName": "历史其他收入",
"projectCategory": "其他",
"quantity": 1,
"unitPrice": 88.00,
"settlementAmount": 88.00,
"paymentMethod": "CASH_PAID",
"voucherUrls": [],
"remark": "重存后确认"
}
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "99010",
"requestId": "oi-old-001",
"incomeDate": "2026-07-20",
"projectName": "历史其他收入",
"projectCategory": "其他",
"projectCategoryName": "其他",
"specification": null,
"quantity": 1,
"unitPrice": 88.00,
"settlementAmount": 88.00,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"voucherUrls": [],
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "重存后确认",
"sourceType": "ORDER_SURCHARGE",
"sourceTypeName": "订单增费",
"sourceId": "88010"
}
}
```
**示例:业务失败**
请求:
```json
{
"incomeDate": "2026-07-28",
"projectName": "不存在记录",
"projectCategory": "其他",
"quantity": 1,
"unitPrice": 10.00,
"settlementAmount": 10.00,
"paymentMethod": "CASH_PAID"
}
```
响应:
```json
{
"code": 584073,
"msg": "其他收入不存在或不属于当前订单",
"data": null
}
```
### 3.3 新增其他支出
- **方法 + 路径**`POST /v3/admin/order/{orderId}/settlement/other-expenses`
- **接口名**:新增其他支出
- **使用场景**:在核单其他支出页新增一条其他支出。
- **认证**:需要管理后台登录态;需要资金写入权限。
- **幂等性**:否。
- **限流**:无接口专属限流。
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | Long | 是 | 订单 ID,必须大于 0 |
**请求体字段**
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `expenseType` | String | 是 | 支出类型 | `FUEL` / `TOLL` / `PARKING` / `RENTAL` / `MAINTENANCE` / `OTHER` |
| `projectName` | String | 是 | 项目名称 | 非空,最长 200 字符 |
| `expenseDate` | String(date) | 否 | 发生日期,格式 `YYYY-MM-DD` | 可空 |
| `actualAmount` | Decimal | 是 | 实际金额 | >= 0,最多 8 位整数、2 位小数 |
| `paymentMethod` | String | 是 | 付款类型 | `CASH_PAID` / `COMPANY_PAID` / `SIGNED` |
| `voucherUrls` | String[] | 否 | 凭证 URL 列表 | 最多 9 项;每项最长 1024 字符;必须是 http/https |
| `remark` | String | 否 | 备注 | 最长 512 字符 |
**响应字段:`Result<SettlementOtherExpenseRespVO>`**
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务码,成功为 `200` |
| `msg` | String | 响应消息 |
| `data.id` | String | 其他支出 ID |
| `data.expenseType` | String | 支出类型 |
| `data.projectName` | String | 项目名称 |
| `data.expenseDate` | String(date) | 发生日期 |
| `data.actualAmount` | String | 实际金额 |
| `data.paymentMethod` | String | 付款类型 |
| `data.voucherUrls` | String[] | 凭证 URL 列表 |
| `data.settlementConfirmStatus` | String | 确认状态;本接口保存成功返回 `CONFIRMED` |
| `data.remark` | String | 备注 |
**错误码**
| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、枚举非法 |
| `584095` | 其他支出字段超出允许范围 | `expenseType` 非法、项目名为空或超长、金额超范围、备注超长 |
| `584096` | 付款类型不合法 | `paymentMethod` 不是允许值 |
| `584097` | 凭证 URL 格式或数量不合法 | 凭证数量超限、非 http/https、单项超长 |
| `584307` | 当前核单状态不允许写入餐食或其他支出 | 订单核单状态不是待核单或核单中 |
| `584089` | 核单或结算已完成,资金数据不可再修改 | 订单资金数据已锁定 |
**示例:典型成功**
请求:
```http
POST /v3/admin/order/60001/settlement/other-expenses
Authorization: Bearer <token>
Content-Type: application/json
```
```json
{
"expenseType": "TOLL",
"projectName": "过路费",
"expenseDate": "2026-07-28",
"actualAmount": 50.00,
"paymentMethod": "CASH_PAID",
"voucherUrls": ["https://cdn.example.com/vouchers/toll.jpg"],
"remark": "高速通行费"
}
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "801",
"expenseType": "TOLL",
"projectName": "过路费",
"expenseDate": "2026-07-28",
"actualAmount": "50.00",
"paymentMethod": "CASH_PAID",
"voucherUrls": ["https://cdn.example.com/vouchers/toll.jpg"],
"settlementConfirmStatus": "CONFIRMED",
"remark": "高速通行费"
}
}
```
**示例:边界成功**
请求:
```json
{
"expenseType": "OTHER",
"projectName": "0元备注支出",
"expenseDate": null,
"actualAmount": 0.00,
"paymentMethod": "SIGNED",
"voucherUrls": [],
"remark": null
}
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "802",
"expenseType": "OTHER",
"projectName": "0元备注支出",
"expenseDate": null,
"actualAmount": "0.00",
"paymentMethod": "SIGNED",
"voucherUrls": [],
"settlementConfirmStatus": "CONFIRMED",
"remark": null
}
}
```
**示例:业务失败**
请求:
```json
{
"expenseType": "BAD_TYPE",
"projectName": "非法支出类型",
"actualAmount": 10.00,
"paymentMethod": "CASH_PAID"
}
```
响应:
```json
{
"code": 584095,
"msg": "其他支出字段超出允许范围",
"data": null
}
```
### 3.4 修改其他支出
- **方法 + 路径**`PUT /v3/admin/order/{orderId}/settlement/other-expenses/{settlementId}`
- **接口名**:修改其他支出
- **使用场景**:修改已有其他支出;也用于把存量 `UNCONFIRMED` 记录重新保存为 `CONFIRMED`
- **认证**:需要管理后台登录态;需要资金写入权限。
- **幂等性**:否。
- **限流**:无接口专属限流。
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | Long | 是 | 订单 ID,必须大于 0 |
| `settlementId` | Long | 是 | 其他支出 ID,必须大于 0 |
**请求体字段**
同 §3.3。
**响应字段**
同 §3.3;保存成功后 `data.settlementConfirmStatus=CONFIRMED`
**错误码**
| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 参数校验失败 | 必填缺失、字段长度超限、枚举非法 |
| `584091` | 其他支出不存在或不属于当前订单 | `settlementId` 不存在或不属于 `orderId` |
| `584095` | 其他支出字段超出允许范围 | `expenseType` 非法、项目名为空或超长、金额超范围、备注超长 |
| `584096` | 付款类型不合法 | `paymentMethod` 不是允许值 |
| `584097` | 凭证 URL 格式或数量不合法 | 凭证数量超限、非 http/https、单项超长 |
| `584307` | 当前核单状态不允许写入餐食或其他支出 | 订单核单状态不是待核单或核单中 |
| `584089` | 核单或结算已完成,资金数据不可再修改 | 订单资金数据已锁定 |
**示例:典型成功**
请求:
```http
PUT /v3/admin/order/60001/settlement/other-expenses/801
Authorization: Bearer <token>
Content-Type: application/json
```
```json
{
"expenseType": "PARKING",
"projectName": "停车费",
"expenseDate": "2026-07-28",
"actualAmount": 35.00,
"paymentMethod": "COMPANY_PAID",
"voucherUrls": ["https://cdn.example.com/vouchers/parking.jpg"],
"remark": "改为停车费"
}
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "801",
"expenseType": "PARKING",
"projectName": "停车费",
"expenseDate": "2026-07-28",
"actualAmount": "35.00",
"paymentMethod": "COMPANY_PAID",
"voucherUrls": ["https://cdn.example.com/vouchers/parking.jpg"],
"settlementConfirmStatus": "CONFIRMED",
"remark": "改为停车费"
}
}
```
**示例:边界成功(存量未确认重存)**
请求:
```json
{
"expenseType": "OTHER",
"projectName": "历史其他支出",
"expenseDate": "2026-07-20",
"actualAmount": 1.00,
"paymentMethod": "CASH_PAID",
"voucherUrls": [],
"remark": "重存后确认"
}
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "810",
"expenseType": "OTHER",
"projectName": "历史其他支出",
"expenseDate": "2026-07-20",
"actualAmount": "1.00",
"paymentMethod": "CASH_PAID",
"voucherUrls": [],
"settlementConfirmStatus": "CONFIRMED",
"remark": "重存后确认"
}
}
```
**示例:业务失败**
请求:
```json
{
"expenseType": "TOLL",
"projectName": "不存在记录",
"expenseDate": "2026-07-28",
"actualAmount": 50.00,
"paymentMethod": "CASH_PAID"
}
```
响应:
```json
{
"code": 584091,
"msg": "其他支出不存在或不属于当前订单",
"data": null
}
```
### 3.5 已删除:批量确认其他收入
- **原方法 + 路径**`POST /v3/admin/order/{orderId}/settlement/other-incomes/confirm`
- **原接口名**:批量确认其他收入
- **变更后**:接口已删除;新增/修改其他收入保存成功即确认。
- **请求体**:不再支持。旧请求体形如 `{"incomeIds":["99001"]}`,前端不要再发送。
- **响应**:不再返回原 `confirmedCount`;调用该路径按不存在接口处理。
**示例:删除后调用失败**
请求:
```json
{
"incomeIds": ["99001"]
}
```
响应:
```json
{
"code": 404,
"msg": "Not Found",
"data": null
}
```
### 3.6 已删除:确认其他支出
- **原方法 + 路径**`POST /v3/admin/order/{orderId}/settlement/other-expenses/confirm`
- **原接口名**:确认其他支出
- **变更后**:接口已删除;新增/修改其他支出保存成功即确认。
- **请求体**:不再支持。旧接口无请求体。
- **响应**:不再返回 `Boolean`;调用该路径按不支持的方法处理。
**示例:删除后调用失败**
请求:
```json
{}
```
响应:
```json
{
"code": 405,
"msg": "Method Not Allowed",
"data": null
}
```
## 4. 接口入参
各接口入参已在 §3 按接口自包含列出。
## 5. 出参字段
各接口出参已在 §3 按接口自包含列出。核心变化是保存类接口的确认状态返回值变为已确认:
| 接口 | 字段 | 类型 | 当前返回 |
|------|------|------|----------|
| POST/PUT 其他收入 | `data.settlementConfirmStatus` | String | `CONFIRMED` |
| POST/PUT 其他收入 | `data.settlementConfirmStatusName` | String | `已确认` |
| POST/PUT 其他支出 | `data.settlementConfirmStatus` | String | `CONFIRMED` |
## 6. 枚举 / 数据字典
### 6.1 paymentMethod付款类型
**所属字段**`paymentMethod` | **类型**`String` | **必填**:是
| 值 | 中文 | 说明 |
|----|------|------|
| `CASH_PAID` | 现付 | 现场现金或线下现付 |
| `COMPANY_PAID` | 公司付款 | 公司承担或公司支付 |
| `SIGNED` | 签单 | 签单结算 |
### 6.2 settlementConfirmStatus确认状态
**所属字段**`settlementConfirmStatus` | **类型**`String` | **必填**:响应字段
| 值 | 中文 | 说明 |
|----|------|------|
| `UNCONFIRMED` | 未确认 | 历史存量状态;本次变更后新增/修改保存不再产生该状态 |
| `CONFIRMED` | 已确认 | 新增/修改保存成功后的状态 |
### 6.3 expenseType其他支出类型
**所属字段**`expenseType` | **类型**`String` | **必填**:是
| 值 | 中文 | 说明 |
|----|------|------|
| `FUEL` | 油费 | 车辆类支出 |
| `TOLL` | 过路费 | 车辆类支出 |
| `PARKING` | 停车费 | 车辆类支出 |
| `RENTAL` | 租车费 | 车辆类支出 |
| `MAINTENANCE` | 维修费 | 车辆类支出 |
| `OTHER` | 其他 | 非上述类型的其他支出 |
### 6.4 sourceType其他收入来源类型
**所属字段**`sourceType` | **类型**`String` | **必填**:响应字段
| 值 | 中文 | 说明 |
|----|------|------|
| `ORDER_SURCHARGE` | 订单增费 | 其他收入关联的订单增费来源 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 参数校验失败 | 请求体格式错误、必填缺失、字段长度或格式非法 |
| `404` | 接口不存在 | 继续调用已删除的其他收入确认接口 |
| `405` | 方法不支持 | 继续调用已删除的其他支出确认接口 |
| `584073` | 其他收入不存在或不属于当前订单 | 修改其他收入时 `incomeId` 无效 |
| `584074` | 当前核单状态不允许修改其他收入 | 订单核单状态不是待核单或核单中 |
| `584075` | 其他收入关联的附加费来源无效 | 其他收入来源无效 |
| `584076` | 其他收入核算金额必须等于数量乘以核算单价 | 其他收入金额不一致 |
| `584087` | requestId 已用于另一笔其他收入 | 新增其他收入幂等键冲突 |
| `584089` | 核单或结算已完成,资金数据不可再修改 | 资金数据已锁定 |
| `584091` | 其他支出不存在或不属于当前订单 | 修改其他支出时 `settlementId` 无效 |
| `584095` | 其他支出字段超出允许范围 | 支出类型、项目名、金额或备注非法 |
| `584096` | 付款类型不合法 | `paymentMethod` 非法 |
| `584097` | 凭证 URL 格式或数量不合法 | 凭证 URL 非法 |
| `584098` | 餐食或其他支出存在未确认记录 | Step6 前仍有历史未确认餐食或其他支出 |
| `584307` | 当前核单状态不允许写入餐食或其他支出 | 订单核单状态不是待核单或核单中 |
## 8. 示例3 组:典型 / 边界 / 异常)
典型成功、边界成功、业务失败示例已按接口内联在 §3.1 至 §3.6。
## 9. 业务边界
- **适用场景**:订单核单状态为待核单或核单中,且当前账号具备资金写入权限时,可以新增/修改其他收入和其他支出。
- **不适用场景**:核单或结算已完成后,不允许再保存资金数据。
- **特殊边界**:历史已存在的 `UNCONFIRMED` 其他收入/其他支出不会因为本次接口变更自动变为 `CONFIRMED`;需要前端对该行发起对应 `PUT` 保存,保存成功后才会返回 `CONFIRMED`
- **删除接口边界**:不要再调用 `/other-incomes/confirm``/other-expenses/confirm`;保存类接口成功即可完成确认。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| 其他收入 `settlementConfirmStatus` | POST/PUT 保存后返回 `UNCONFIRMED`,需再调确认接口变为 `CONFIRMED` | POST/PUT 保存成功直接返回 `CONFIRMED` |
| 其他收入 `settlementConfirmStatusName` | POST/PUT 保存后返回 `未确认` | POST/PUT 保存成功返回 `已确认` |
| 其他支出 `settlementConfirmStatus` | POST/PUT 保存后返回 `UNCONFIRMED`,需再调确认接口变为 `CONFIRMED` | POST/PUT 保存成功直接返回 `CONFIRMED` |
| 其他收入确认响应 `confirmedCount` | `POST /other-incomes/confirm` 返回确认数量 | 接口删除,不再返回 |
| 其他支出确认响应 `data` | `POST /other-expenses/confirm` 返回 `true` | 接口删除,不再返回 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 新增其他收入 | 保存后仍是未确认,需要再调用确认接口 | 保存成功即确认 |
| 修改其他收入 | 修改后变为未确认,需要再调用确认接口 | 保存成功即确认 |
| 新增其他支出 | 保存后仍是未确认,需要再调用确认接口 | 保存成功即确认 |
| 修改其他支出 | 修改后变为未确认,需要再调用确认接口 | 保存成功即确认 |
| 存量未确认记录 | 可调用独立确认接口批量确认 | 需要逐条用 PUT 重存确认 |
| 独立确认按钮 | 调用确认接口 | 不再调用确认接口 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:是。两个确认接口删除,继续调用会失败。
- **前端是否必须同步上线**:是。需要停止调用已删除确认接口,并以保存接口返回的 `settlementConfirmStatus` 作为确认结果。
- **影响已有数据**:历史 `UNCONFIRMED` 其他收入/其他支出不会自动确认;需要通过对应 PUT 保存后确认。
### 11.2 回滚方案
- **回滚方式**:如需恢复旧交互,回滚本次接口契约变更对应 PR。
- **回滚后清理**:无前端侧额外清理数据。
- **回滚耗时**:以后端发布节奏为准。
## 12. 注意事项
- 前端保存其他收入/其他支出成功后,不要再追加调用确认接口。
- 前端如有独立「确认其他收入」「确认其他支出」按钮或批量确认流程,需要改为保存即确认的交互。
- 前端如检测到历史 `UNCONFIRMED` 记录,需要提示用户重新保存该条记录;重存后响应会返回 `CONFIRMED`
- 餐食费用确认接口 `POST /v3/admin/order/{orderId}/settlement/meals/confirm` 本次未删除,不属于本文变更范围。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5310](https://git.1814.love:8443/wx/HL/issues/5310)
- **PR**: [#5313](https://git.1814.love:8443/wx/HL/pulls/5313)
- **Merge commit**: [9b3af7c](https://git.1814.love:8443/wx/HL/commit/9b3af7c8587547fa7b1a0d4282bc4783dff98ea6)
### 13.2 联系人
- **后端负责人**: @yst

查看文件

@ -1,875 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5324"
title: "删除旧核单兼容接口"
consumer: "admin"
change_type: "删除接口"
backend_status: "deployed"
backend_ref: "PR #5328 · merge 709105c1a1d26ae1c867fcc998286781f499faf2"
deployment_status: "deployed"
deployment_ref: "deploy-panel task ad042377"
gateway_status: "verified"
verification_status: "verified"
verification_ref: "D:/work/project-doc/PRPs/reports/5328-deploy-qa-report.md · D:/work/project-doc/test/5328/evidence.json"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "2ab427c62a8d44155e740f4abfcc5c812a5b63ad"
target_release: ""
verified_at: "2026-07-29T10:21:44+08:00"
status_note: "部署 task ad042377 成功;8086/8186 正常;网关 20/20 HTTP 200、0 网络错误、0 个 5xx、RST 0;3 个删除路由均返回业务 404,4 个保留路由进入业务门禁且零写入。管理后台待迁移到双报表确认后调用 finalize 的五步流程。"
updated_at: "2026-07-29"
base: "dev-v3"
generated: "2026-07-28T18:04:49+08:00"
---
# 【删除接口·管理后台】删除旧核单兼容接口 (#5324)
> **PR**: #5328 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-28 18:04
## 1. 接口背景
核单完成入口统一为“双报表确认后完成核单”:管理后台先读取并确认主报账人报账表,再读取并确认单团核算表,最后携带两份报告的当前来源指纹调用 `finalize`。旧分类确认兼容接口和旧 Step6 提交入口不再提供。
## 2. 变更清单
### 2.1 删除的接口
| # | 接口 | 方法 | 路径 | 变更类型 | 前端动作 |
|---|------|------|------|----------|----------|
| 1 | 查询核单分类确认状态 | GET | `/v3/admin/order/{orderId}/settlement/category-checks` | 删除 | 删除调用及分类确认状态门禁 |
| 2 | 确认单个核单分类 | POST | `/v3/admin/order/{orderId}/settlement/category-checks/{category}/confirm` | 删除 | 删除调用及“本分类已确认”交互 |
| 3 | 旧 Step6 提交核单 | POST | `/v3/admin/order/{orderId}/settlement/step6/submit` | 删除 | 改为下表五步流程 |
### 2.2 唯一替代流程
| 顺序 | 接口 | 方法 | 路径 | 用途 |
|------|------|------|------|------|
| 1 | 查询主报账人报账表 | GET | `/v3/admin/order/{orderId}/settlement/reports/reimbursement` | 获取实时数据和主报账表 `sourceFingerprint` |
| 2 | 确认主报账人报账表 | POST | `/v3/admin/order/{orderId}/settlement/reports/reimbursement/confirm` | 确认转账、预支结清标志和签字凭证 |
| 3 | 查询单团核算表 | GET | `/v3/admin/order/{orderId}/settlement/reports/group` | 获取实时数据和单团核算表 `sourceFingerprint` |
| 4 | 确认单团核算表 | POST | `/v3/admin/order/{orderId}/settlement/reports/group/confirm` | 确认当前单团核算结果 |
| 5 | 完成核单 | POST | `/v3/admin/order/{orderId}/settlement/finalize` | 携带两份已确认报告的当前指纹完成核单 |
## 3. 接口详情
以下五个接口都需要管理后台登录态,房务角色不可访问;`orderId` 为必填路径参数,类型为 `Long/String`,值必须大于 0。
### 3.1 查询主报账人报账表
- **方法 + 路径**`GET /v3/admin/order/{orderId}/settlement/reports/reimbursement`
- **使用场景**:进入报账报表页面、确认前刷新、来源数据变化后重新获取。
- **幂等性**:幂等,只读。
- **请求体**:无。
- **成功响应**`Result<SettlementReimbursementReportRespVO>`,完整字段见 §5.2。
- **关键规则**:前端必须保存本次响应的 `data.sourceFingerprint`;确认请求不得使用缓存的旧指纹。
**请求示例**
```http
GET /v3/admin/order/1914050000000001/settlement/reports/reimbursement
Authorization: Bearer <token>
```
**响应示例**
```json
{
"code": 200,
"msg": "success",
"data": {
"id": null,
"orderId": "1914050000000001",
"reportStatus": "GENERATED",
"sourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"primaryReporterId": "3001",
"primaryReporterName": "王司机",
"primaryReporterRole": "DRIVER",
"reportVersion": 1,
"driverCollectedTailAmount": 2000.00,
"approvedAdvanceAmount": 500.00,
"reportablePaidCostAmount": 1000.00,
"reporterNetAmount": 1500.00,
"primaryReporterCollectedAmount": 2000.00,
"publicPrepaidAmount": 1000.00,
"primaryReporterDueAmount": 1000.00,
"advanceOutstandingAmount": 500.00,
"reconNetAmount": 1500.00,
"transferDirection": "REPORTER_TO_COMPANY",
"transferAmount": 1500.00,
"incomeLines": [
{
"type": "DRIVER_CASH_RECEIPT",
"receiptId": "9100000000001",
"amount": 2000.00,
"channel": "DRIVER_CASH",
"payType": "CASH",
"collectorStaffId": "3001",
"collectorName": "王司机",
"collectorRole": "DRIVER",
"receivedAt": "2026-07-27T18:30:00",
"remark": "司机代收尾款"
}
],
"expenseLines": [
{
"category": "HOTEL",
"kind": "HOTEL",
"hotelAssignmentId": "9200000000001",
"hotelName": "示例酒店",
"stayDate": "2026-07-20",
"amount": 1000.00,
"paymentMethod": "CASH_PAID",
"remark": null
}
],
"advanceLines": [
{
"type": "APPROVED_ADVANCE",
"advanceId": "9300000000001",
"payeeStaffId": "3001",
"payeeName": "王司机",
"payeeRole": "DRIVER",
"advanceType": "PUBLIC",
"amount": 500.00,
"purpose": "途中费用",
"voucherUrl": "https://oss.example.com/advance.jpg",
"status": "APPROVED",
"submittedAt": "2026-07-18T10:00:00",
"approvedAt": "2026-07-18T11:00:00",
"approvedBy": "10001"
}
],
"vehicleLines": [],
"transferStatus": null,
"transferDate": null,
"transferRef": null,
"advanceSettledFlag": false,
"signedVoucher": null,
"generatedBy": null,
"generatedByName": null,
"generatedAt": null,
"confirmedBy": null,
"confirmedByName": null,
"confirmedAt": null
}
}
```
### 3.2 确认主报账人报账表
- **方法 + 路径**`POST /v3/admin/order/{orderId}/settlement/reports/reimbursement/confirm`
- **使用场景**:已核对主报账表,且转账、预支标记和签字凭证已经填写完毕。
- **幂等性**:同一当前指纹和完全相同的确认内容可重复提交;确认后改传其它内容返回 `584317`
- **请求体**:见 §4.2。
- **成功响应**:与 §3.1 相同,`reportStatus=CONFIRMED`,并返回确认信息。
**请求示例**
```http
POST /v3/admin/order/1914050000000001/settlement/reports/reimbursement/confirm
Authorization: Bearer <token>
Content-Type: application/json
{
"expectedSourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"transferStatus": "COMPLETED",
"transferDate": "2026-07-28",
"transferRef": "BANK-20260728-001",
"advanceSettledFlag": true,
"signedVoucher": {
"files": [
{
"name": "司机签字单.pdf",
"url": "https://oss.example.com/signed-voucher.pdf"
}
],
"note": "签字凭证已回收"
}
}
```
**响应示例**
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "9400000000001",
"orderId": "1914050000000001",
"reportStatus": "CONFIRMED",
"sourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"primaryReporterId": "3001",
"primaryReporterName": "王司机",
"primaryReporterRole": "DRIVER",
"reportVersion": 1,
"driverCollectedTailAmount": 2000.00,
"approvedAdvanceAmount": 500.00,
"reportablePaidCostAmount": 1000.00,
"reporterNetAmount": 1500.00,
"primaryReporterCollectedAmount": 2000.00,
"publicPrepaidAmount": 1000.00,
"primaryReporterDueAmount": 1000.00,
"advanceOutstandingAmount": 500.00,
"reconNetAmount": 1500.00,
"transferDirection": "REPORTER_TO_COMPANY",
"transferAmount": 1500.00,
"incomeLines": [],
"expenseLines": [],
"advanceLines": [],
"vehicleLines": [],
"transferStatus": "COMPLETED",
"transferDate": "2026-07-28",
"transferRef": "BANK-20260728-001",
"advanceSettledFlag": true,
"signedVoucher": {
"files": [
{
"name": "司机签字单.pdf",
"url": "https://oss.example.com/signed-voucher.pdf"
}
],
"note": "签字凭证已回收"
},
"generatedBy": null,
"generatedByName": null,
"generatedAt": null,
"confirmedBy": "10001",
"confirmedByName": "财务管理员",
"confirmedAt": "2026-07-28T18:10:00"
}
}
```
### 3.3 查询单团核算表
- **方法 + 路径**`GET /v3/admin/order/{orderId}/settlement/reports/group`
- **使用场景**:主报账表确认后查看单团收入、成本、毛利和人均指标。
- **幂等性**:幂等,只读。
- **请求体**:无。
- **成功响应**`Result<SettlementGroupReportRespVO>`,完整字段见 §5.3。
- **关键规则**:前端必须保存本次响应的 `data.sourceFingerprint`;确认请求不得使用缓存的旧指纹。
**请求示例**
```http
GET /v3/admin/order/1914050000000001/settlement/reports/group
Authorization: Bearer <token>
```
**响应示例**
```json
{
"code": 200,
"msg": "success",
"data": {
"id": null,
"orderId": "1914050000000001",
"reportStatus": "GENERATED",
"sourceFingerprint": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"baseOrderAmount": 24800.00,
"otherIncomeAmount": 500.00,
"discountAmount": 300.00,
"adjustedReceivableAmount": 25000.00,
"paidAmount": 25000.00,
"actualRefundedAmount": 0.00,
"netRevenueAmount": 25000.00,
"netReceivedAmount": 25000.00,
"outstandingAmount": 0.00,
"hotelCost": 4280.00,
"ticketCost": 3680.00,
"mealCost": 860.00,
"vehicleCost": 1260.00,
"guideCost": 800.00,
"photographerCost": 600.00,
"otherExpenseCost": 300.00,
"insurancePremium": 180.00,
"totalCost": 11960.00,
"paidCost": 11960.00,
"unpaidCost": 0.00,
"grossProfit": 13040.00,
"grossProfitRate": 0.5216,
"travelerCount": 5,
"perCapitaRevenue": 5000.00,
"perCapitaCost": 2392.00,
"perCapitaProfit": 2608.00,
"incomeLines": [
{"type": "BASE_ORDER", "amount": 24800.00},
{"type": "OTHER_INCOME", "amount": 500.00},
{"type": "DISCOUNT", "amount": -300.00},
{"type": "ACTUAL_REFUND", "amount": 0.00}
],
"costCategories": [
{"category": "HOTEL", "amount": 4280.00},
{"category": "TICKET", "amount": 3680.00},
{"category": "MEAL", "amount": 860.00},
{"category": "VEHICLE", "amount": 1260.00},
{"category": "GUIDE", "amount": 800.00},
{"category": "PHOTOGRAPHER", "amount": 600.00},
{"category": "OTHER_EXPENSE", "amount": 300.00},
{"category": "INSURANCE", "amount": 180.00}
],
"generatedBy": null,
"generatedByName": null,
"generatedAt": null,
"confirmedBy": null,
"confirmedByName": null,
"confirmedAt": null
}
}
```
### 3.4 确认单团核算表
- **方法 + 路径**`POST /v3/admin/order/{orderId}/settlement/reports/group/confirm`
- **使用场景**:主报账表已经确认,且已核对当前单团收入、成本和利润。
- **幂等性**:相同当前指纹重复确认返回已确认结果。
- **请求体**:见 §4.3。
- **成功响应**:与 §3.3 相同,`reportStatus=CONFIRMED`,并返回确认人和确认时间。
**请求示例**
```http
POST /v3/admin/order/1914050000000001/settlement/reports/group/confirm
Authorization: Bearer <token>
Content-Type: application/json
{
"expectedSourceFingerprint": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
```
**响应示例**
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "9500000000001",
"orderId": "1914050000000001",
"reportStatus": "CONFIRMED",
"sourceFingerprint": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"baseOrderAmount": 24800.00,
"otherIncomeAmount": 500.00,
"discountAmount": 300.00,
"adjustedReceivableAmount": 25000.00,
"paidAmount": 25000.00,
"actualRefundedAmount": 0.00,
"netRevenueAmount": 25000.00,
"netReceivedAmount": 25000.00,
"outstandingAmount": 0.00,
"hotelCost": 4280.00,
"ticketCost": 3680.00,
"mealCost": 860.00,
"vehicleCost": 1260.00,
"guideCost": 800.00,
"photographerCost": 600.00,
"otherExpenseCost": 300.00,
"insurancePremium": 180.00,
"totalCost": 11960.00,
"paidCost": 11960.00,
"unpaidCost": 0.00,
"grossProfit": 13040.00,
"grossProfitRate": 0.5216,
"travelerCount": 5,
"perCapitaRevenue": 5000.00,
"perCapitaCost": 2392.00,
"perCapitaProfit": 2608.00,
"incomeLines": [
{"type": "BASE_ORDER", "amount": 24800.00},
{"type": "OTHER_INCOME", "amount": 500.00},
{"type": "DISCOUNT", "amount": -300.00},
{"type": "ACTUAL_REFUND", "amount": 0.00}
],
"costCategories": [
{"category": "HOTEL", "amount": 4280.00},
{"category": "TICKET", "amount": 3680.00},
{"category": "MEAL", "amount": 860.00},
{"category": "VEHICLE", "amount": 1260.00},
{"category": "GUIDE", "amount": 800.00},
{"category": "PHOTOGRAPHER", "amount": 600.00},
{"category": "OTHER_EXPENSE", "amount": 300.00},
{"category": "INSURANCE", "amount": 180.00}
],
"generatedBy": null,
"generatedByName": null,
"generatedAt": null,
"confirmedBy": "10001",
"confirmedByName": "财务管理员",
"confirmedAt": "2026-07-28T18:12:00"
}
}
```
### 3.5 完成核单
- **方法 + 路径**`POST /v3/admin/order/{orderId}/settlement/finalize`
- **使用场景**:两份报告均已确认且来源仍为当前版本时,点击“完成核单”。
- **幂等性**:已完成且存在当前终态结果时,重复提交返回当前终态结果。
- **请求体**:见 §4.4;请求体在业务上必填。
- **成功响应**`Result<SettlementSubmitRespVO>`,完整字段见 §5.4。
**请求示例**
```http
POST /v3/admin/order/1914050000000001/settlement/finalize
Authorization: Bearer <token>
Content-Type: application/json
{
"remark": "双报表已核对完成",
"reimbursementExpectedSourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"groupExpectedSourceFingerprint": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
```
**响应示例**
```json
{
"code": 200,
"msg": "success",
"data": {
"summaryId": "9600000000001",
"finalSnapshotId": "9600000000002",
"finalSnapshotVersionNo": 1,
"finalSnapshotStatus": "FINALIZED",
"orderId": "1914050000000001",
"settledAt": "2026-07-28T18:15:00",
"totalAmount": 25000.00,
"paidAmount": 25000.00,
"balanceAmount": 0.00,
"roomCost": 4280.00,
"ticketCost": 3680.00,
"staffCost": 1400.00,
"subsidyCost": 0.00,
"mealCost": 860.00,
"vehicleCost": 1260.00,
"otherExpenseCost": 300.00,
"insurancePremium": 180.00,
"totalActualCost": 11960.00,
"driverTransferAmount": 1000.00,
"profitAmount": 13040.00,
"profitRate": 0.5216,
"orderStatusAfter": "待财务复核",
"mqTriggered": true,
"warnings": []
}
}
```
## 4. 接口入参
### 4.1 五个替代接口共用路径参数
| 字段 | 类型 | 必填 | 说明 | 校验 |
|------|------|------|------|------|
| `orderId` | Long/String | 是 | 订单 ID | 必须大于 0 |
两个 GET 接口没有 Query 参数和请求体。
### 4.2 主报账表确认请求体
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `expectedSourceFingerprint` | String | 是 | §3.1 最新响应的 `data.sourceFingerprint` | 64 位小写十六进制 |
| `transferStatus` | String | 是 | 转账处理状态 | 固定传 `COMPLETED` |
| `transferDate` | String/date | 条件必填 | 转账日期 | `reporterNetAmount != 0` 时必填;格式 `YYYY-MM-DD` |
| `transferRef` | String | 条件必填 | 转账流水号或可追溯凭证号 | `reporterNetAmount != 0` 时不得为空白 |
| `advanceSettledFlag` | Boolean | 是 | 预支款项是否已处理完毕 | 不得为 `null` |
| `signedVoucher` | Object | 是 | 签字凭证 | 不得为 `null` |
| `signedVoucher.files` | Array | 是 | 签字凭证文件列表 | 至少 1 项 |
| `signedVoucher.files[].name` | String | 否 | 文件名 | 可为空 |
| `signedVoucher.files[].url` | String | 是 | 文件地址 | 不得为空白 |
| `signedVoucher.note` | String | 否 | 凭证备注 | 可为空 |
`reporterNetAmount = 0` 时,`transferDate``transferRef` 可不传;`transferStatus` 仍必须是 `COMPLETED`,签字凭证仍必须至少包含一个有效文件。
### 4.3 单团核算表确认请求体
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `expectedSourceFingerprint` | String | 是 | §3.3 最新响应的 `data.sourceFingerprint` | 64 位小写十六进制 |
### 4.4 完成核单请求体
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `remark` | String | 否 | 本次完成核单的整体备注 | 最长 500 字 |
| `reimbursementExpectedSourceFingerprint` | String | 是 | 已确认主报账表的当前 `sourceFingerprint` | 64 位小写十六进制 |
| `groupExpectedSourceFingerprint` | String | 是 | 已确认单团核算表的当前 `sourceFingerprint` | 64 位小写十六进制 |
### 4.5 指纹传递关系
| 来源 | 确认接口字段 | 完成核单字段 |
|------|--------------|--------------|
| `GET .../reports/reimbursement``data.sourceFingerprint` | `POST .../reports/reimbursement/confirm``expectedSourceFingerprint` | `POST .../finalize``reimbursementExpectedSourceFingerprint` |
| `GET .../reports/group``data.sourceFingerprint` | `POST .../reports/group/confirm``expectedSourceFingerprint` | `POST .../finalize``groupExpectedSourceFingerprint` |
## 5. 出参
### 5.1 统一响应外层
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | `200` 表示成功;其它值见 §7 |
| `msg` | String | 结果说明 |
| `data` | Object/null | 成功时为业务数据,失败时通常为 `null` |
所有 Long ID 以 JSON 字符串消费,避免前端数字精度损失;金额字段为十进制数。
### 5.2 主报账人报账表响应
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String/null | 报账表记录 ID;仅实时预览、尚未确认时可为 `null` |
| `orderId` | String | 订单 ID |
| `reportStatus` | String | `GENERATED` / `CONFIRMED` / `STALE`,见 §6.1 |
| `sourceFingerprint` | String | 当前来源指纹,64 位小写十六进制 |
| `primaryReporterId` | String/null | 主报账人 ID |
| `primaryReporterName` | String/null | 主报账人姓名 |
| `primaryReporterRole` | String/null | 主报账人角色 |
| `reportVersion` | Integer/null | 报账表版本 |
| `driverCollectedTailAmount` | Decimal | 司机代收尾款 |
| `approvedAdvanceAmount` | Decimal | 已审批预支合计 |
| `reportablePaidCostAmount` | Decimal | 主报账人已支付、可报账成本合计 |
| `reporterNetAmount` | Decimal | 报账净额:司机代收尾款 + 已审批预支 - 可报账已支付成本 |
| `primaryReporterCollectedAmount` | Decimal | 主报账人代收金额 |
| `publicPrepaidAmount` | Decimal | 公共预支金额 |
| `primaryReporterDueAmount` | Decimal | 主报账人应报账金额 |
| `advanceOutstandingAmount` | Decimal | 待处理预支金额 |
| `reconNetAmount` | Decimal | 报账净额兼容字段 |
| `transferDirection` | String | 转账方向,见 §6.2 |
| `transferAmount` | Decimal | 需转账金额,取 `reporterNetAmount` 绝对值 |
| `incomeLines` | Array<Object> | 司机代收尾款明细 |
| `expenseLines` | Array<Object> | 主报账人现金支付成本明细 |
| `advanceLines` | Array<Object> | 已审批预支明细 |
| `vehicleLines` | Array<Object> | 车辆逐日明细;允许空数组 |
| `transferStatus` | String/null | 未确认时可为空;确认后为 `COMPLETED` |
| `transferDate` | String/date/null | 转账日期 |
| `transferRef` | String/null | 转账流水号或凭证号 |
| `advanceSettledFlag` | Boolean | 预支是否已处理完毕 |
| `signedVoucher` | Object/null | 签字凭证,结构同 §4.2 |
| `generatedBy` | String/null | 历史生成操作人 ID |
| `generatedByName` | String/null | 历史生成操作人姓名 |
| `generatedAt` | String/date-time/null | 历史生成时间 |
| `confirmedBy` | String/null | 确认人 ID |
| `confirmedByName` | String/null | 确认人姓名 |
| `confirmedAt` | String/date-time/null | 确认时间 |
`incomeLines[]` 的固定字段为 `type``receiptId``amount``channel``payType``collectorStaffId``collectorName``collectorRole``receivedAt``remark`
`advanceLines[]` 的固定字段为 `type``advanceId``payeeStaffId``payeeName``payeeRole``advanceType``amount``purpose``voucherUrl``status``submittedAt``approvedAt``approvedBy`
`expenseLines[]` 至少包含 `category``kind``amount``paymentMethod`;按分类还会包含对应的名称、日期、数量、单价、人员或车辆标识、凭证和备注字段。前端列表应按字段是否存在展示,不依赖固定列宽。
### 5.3 单团核算表响应
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String/null | 单团核算表记录 ID;仅实时预览、尚未确认时可为 `null` |
| `orderId` | String | 订单 ID |
| `reportStatus` | String | `GENERATED` / `CONFIRMED` / `STALE` |
| `sourceFingerprint` | String | 当前来源指纹,64 位小写十六进制 |
| `baseOrderAmount` | Decimal | 订单基础金额 |
| `otherIncomeAmount` | Decimal | 其他收入 |
| `discountAmount` | Decimal | 优惠金额 |
| `adjustedReceivableAmount` | Decimal | 调整后应收 |
| `paidAmount` | Decimal | 已收金额 |
| `actualRefundedAmount` | Decimal | 实际退款 |
| `netRevenueAmount` | Decimal | 净收入 |
| `netReceivedAmount` | Decimal | 净已收 |
| `outstandingAmount` | Decimal | 待收金额 |
| `hotelCost` | Decimal | 住宿成本 |
| `ticketCost` | Decimal | 门票/游玩项目成本 |
| `mealCost` | Decimal | 餐食成本 |
| `vehicleCost` | Decimal | 车辆成本 |
| `guideCost` | Decimal | 导游成本 |
| `photographerCost` | Decimal | 摄影成本 |
| `otherExpenseCost` | Decimal | 其他支出成本 |
| `insurancePremium` | Decimal | 保险保费 |
| `totalCost` | Decimal | 总成本 |
| `paidCost` | Decimal | 已支付成本 |
| `unpaidCost` | Decimal | 未支付成本 |
| `grossProfit` | Decimal | 毛利 |
| `grossProfitRate` | Decimal | 毛利率;收入为 0 时为 0 |
| `travelerCount` | Integer | 出行人数 |
| `perCapitaRevenue` | Decimal | 人均收入 |
| `perCapitaCost` | Decimal | 人均成本 |
| `perCapitaProfit` | Decimal | 人均利润 |
| `incomeLines` | Array<Object> | 收入构成;元素字段为 `type``amount` |
| `costCategories` | Array<Object> | 成本构成;元素字段为 `category``amount` |
| `generatedBy` | String/null | 历史生成操作人 ID |
| `generatedByName` | String/null | 历史生成操作人姓名 |
| `generatedAt` | String/date-time/null | 历史生成时间 |
| `confirmedBy` | String/null | 确认人 ID |
| `confirmedByName` | String/null | 确认人姓名 |
| `confirmedAt` | String/date-time/null | 确认时间 |
### 5.4 完成核单响应
| 字段 | 类型 | 说明 |
|------|------|------|
| `summaryId` | String | 核单汇总 ID |
| `finalSnapshotId` | String | 核单终态版本 ID |
| `finalSnapshotVersionNo` | Integer | 核单终态版本号 |
| `finalSnapshotStatus` | String | 核单终态状态,成功时为 `FINALIZED` |
| `orderId` | String | 订单 ID |
| `settledAt` | String/date-time | 核单完成时间 |
| `totalAmount` | Decimal | 订单总金额 |
| `paidAmount` | Decimal | 已收金额 |
| `balanceAmount` | Decimal | 待收金额;完成核单时必须为 0 |
| `roomCost` | Decimal | 住宿实际成本 |
| `ticketCost` | Decimal | 门票实际成本 |
| `staffCost` | Decimal | 人员实际成本 |
| `subsidyCost` | Decimal | 补助实际成本 |
| `mealCost` | Decimal | 餐食实际成本 |
| `vehicleCost` | Decimal | 车辆实际成本 |
| `otherExpenseCost` | Decimal | 其他支出实际成本 |
| `insurancePremium` | Decimal | 保险保费 |
| `totalActualCost` | Decimal | 总实际成本 |
| `driverTransferAmount` | Decimal | 需与司机/主报账人结算的金额 |
| `profitAmount` | Decimal | 公司毛利 |
| `profitRate` | Decimal | 公司毛利率 |
| `orderStatusAfter` | String | 完成核单后的订单状态 |
| `mqTriggered` | Boolean | 核单完成事件是否已触发 |
| `warnings` | Array<String> | 软提示列表;不阻塞成功结果 |
## 6. 枚举 / 数据字典
### 6.1 `reportStatus`
**所属字段**:两份报告的 `reportStatus` | **类型**String
| 值 | 中文 | 说明 |
|----|------|------|
| `GENERATED` | 待确认 | 当前实时数据可供核对,尚未确认 |
| `CONFIRMED` | 已确认 | 当前来源数据已经确认 |
| `STALE` | 已失效 | 来源数据已变化,旧确认不能用于完成核单 |
### 6.2 `transferDirection`
**所属字段**:主报账表 `transferDirection` | **类型**String
| 值 | 中文 | 说明 |
|----|------|------|
| `REPORTER_TO_COMPANY` | 报账人转给公司 | `reporterNetAmount > 0` |
| `COMPANY_TO_REPORTER` | 公司转给报账人 | `reporterNetAmount < 0` |
| `BALANCED` | 无需转账 | `reporterNetAmount = 0` |
### 6.3 `transferStatus`
**所属字段**:主报账表确认请求和响应 `transferStatus` | **类型**String
| 值 | 中文 | 说明 |
|----|------|------|
| `COMPLETED` | 已完成 | 确认主报账表时唯一允许值 |
### 6.4 单团收入行 `type`
| 值 | 中文 | 金额符号 |
|----|------|----------|
| `BASE_ORDER` | 订单基础收入 | 正数 |
| `OTHER_INCOME` | 其他收入 | 正数 |
| `DISCOUNT` | 优惠 | 负数 |
| `ACTUAL_REFUND` | 实际退款 | 负数或 0 |
### 6.5 单团成本行 `category`
| 值 | 中文 |
|----|------|
| `HOTEL` | 住宿 |
| `TICKET` | 门票/游玩项目 |
| `MEAL` | 餐食 |
| `VEHICLE` | 车辆 |
| `GUIDE` | 导游 |
| `PHOTOGRAPHER` | 摄影 |
| `OTHER_EXPENSE` | 其他支出 |
| `INSURANCE` | 保险 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 参数校验失败 | `orderId <= 0`、请求体缺字段、指纹格式错误等 |
| `403` | 无访问或写入权限 | 房务角色访问,或操作人没有核单写权限 |
| `404` | 接口不存在 | 调用本次删除的 3 个旧接口 |
| `584082` | 存在待收尾款,请收齐后再提交核单 | `finalize` 时单团核算表 `outstandingAmount != 0` |
| `584312` | 主报账表尚未确认或数据已变化 | 单团核算表确认前,主报账表未确认或已失效 |
| `584314` | 单团核算表尚未确认或数据已变化 | `finalize` 时单团核算表未确认、已失效或指纹不匹配 |
| `584315` | 核单来源数据已变化,请刷新后重新确认 | 确认报告时提交的 `expectedSourceFingerprint` 不是当前值 |
| `584316` | 核单报告发生并发变化,请刷新后重试 | 多人同时确认同一报告发生冲突 |
| `584317` | 当前报告状态不允许执行该操作 | 确认内容不合法,或报告当前状态不允许重复变更 |
| `584325` | 完成核单必须提交主报账和单团核算的当前指纹 | `finalize` 缺少任一指纹或指纹不是 64 位小写十六进制 |
## 8. 示例(典型 / 边界 / 异常)
### 8.1 典型成功:五步完成核单
1. 调用 `GET .../reports/reimbursement`,保存响应 `sourceFingerprint=aaaa...`
2. 调用 `POST .../reports/reimbursement/confirm``expectedSourceFingerprint``aaaa...`,响应状态为 `CONFIRMED`
3. 调用 `GET .../reports/group`,保存响应 `sourceFingerprint=bbbb...`
4. 调用 `POST .../reports/group/confirm``expectedSourceFingerprint``bbbb...`,响应状态为 `CONFIRMED`
5. 调用 `POST .../finalize`,两个指纹分别传 `aaaa...``bbbb...`,响应 `finalSnapshotStatus=FINALIZED`
各步完整请求和响应见 §3.1§3.5。
### 8.2 边界:报账净额为 0
当最新主报账表返回 `reporterNetAmount=0``transferDirection=BALANCED` 时:
```http
POST /v3/admin/order/1914050000000001/settlement/reports/reimbursement/confirm
Authorization: Bearer <token>
Content-Type: application/json
{
"expectedSourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"transferStatus": "COMPLETED",
"advanceSettledFlag": true,
"signedVoucher": {
"files": [
{
"name": "司机签字单.pdf",
"url": "https://oss.example.com/signed-voucher.pdf"
}
],
"note": null
}
}
```
```json
{
"code": 200,
"msg": "success",
"data": {
"orderId": "1914050000000001",
"reportStatus": "CONFIRMED",
"sourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"reporterNetAmount": 0.00,
"transferDirection": "BALANCED",
"transferAmount": 0.00,
"transferStatus": "COMPLETED",
"transferDate": null,
"transferRef": null,
"advanceSettledFlag": true,
"signedVoucher": {
"files": [
{
"name": "司机签字单.pdf",
"url": "https://oss.example.com/signed-voucher.pdf"
}
],
"note": null
}
}
}
```
### 8.3 异常:来源数据变化
```http
POST /v3/admin/order/1914050000000001/settlement/reports/group/confirm
Authorization: Bearer <token>
Content-Type: application/json
{
"expectedSourceFingerprint": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
}
```
```json
{
"code": 584315,
"msg": "核单来源数据已变化,请刷新后重新确认",
"data": null
}
```
收到该错误后重新执行对应 GET,使用新的 `data.sourceFingerprint` 重新确认;不得继续用旧指纹调用 `finalize`
### 8.4 异常:仍有待收尾款
```http
POST /v3/admin/order/1914050000000001/settlement/finalize
Authorization: Bearer <token>
Content-Type: application/json
{
"reimbursementExpectedSourceFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"groupExpectedSourceFingerprint": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
```
```json
{
"code": 584082,
"msg": "存在待收尾款,请收齐后再提交核单",
"data": null
}
```
## 9. 业务边界
- 必须按“查询主报账表 → 确认主报账表 → 查询单团核算表 → 确认单团核算表 → 完成核单”的顺序执行。
- 两份报告的指纹互不通用;禁止把主报账表指纹传到单团核算字段,或反向混用。
- 每次确认前都应重新 GET;当 `reportStatus=STALE` 或收到 `584315` 时,必须刷新数据并使用新指纹。
- 单团核算表确认依赖当前有效的主报账表确认,否则返回 `584312`
- `finalize` 同时校验两份报告已确认、指纹仍为当前值,以及 `outstandingAmount=0`
- 主报账表 `reporterNetAmount != 0` 时,确认请求必须提供 `transferDate` 和非空 `transferRef`
- 主报账表确认始终要求至少一个含有效 `url` 的签字凭证文件。
## 10. 修改前后对比
### 10.1 接口级对比
| 功能 | 修改前 | 修改后 |
|------|--------|--------|
| 分类状态 | 调用 `GET .../category-checks` | 不再查询分类确认状态 |
| 分类确认 | 调用 `POST .../category-checks/{category}/confirm` | 保存各核单明细即可,不再单独确认分类 |
| 报账与单团核算 | 可能绕过双报表直接提交旧 Step6 | 必须分别 GET、confirm 两份报告 |
| 完成核单 | `POST .../step6/submit` | `POST .../finalize`,请求体必须携带两个当前指纹 |
### 10.2 请求体对比
| 入口 | 修改前 | 修改后 |
|------|--------|--------|
| 旧 `step6/submit` | 旧提交请求 | 接口删除 |
| 新 `finalize` | 不适用 | `remark` 可选;`reimbursementExpectedSourceFingerprint``groupExpectedSourceFingerprint` 必填 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**是。3 个旧接口已删除。
- **前端是否必须同步上线**:是。仍调用任一旧接口的管理后台将收到 404;旧 Step6 提交必须迁移为五步流程。
### 11.2 回滚原则
- 后端回退时,前端仍可保留五步新流程。
- 前端不得因为短期回退重新新增分类确认入口或恢复旧 Step6 调用;如需临时兼容,应单独确认接口契约后再处理。
## 12. 注意事项
- 删除 `category-checks` 查询、分类确认 API 封装、分类确认按钮和相关状态门禁。
- 删除 `step6/submit` API 封装及所有调用点。
- “完成核单”按钮改为调用 `finalize`,并在调用前确保两份报告都为 `CONFIRMED`
- 页面状态中分别保存两份 `sourceFingerprint`,不要只保存一个通用指纹。
- 主报账表确认成功后再开放单团核算确认;单团核算确认成功后再开放“完成核单”。
- 收到 `584312``584314``584315``584316` 时刷新对应报告,不得自动使用旧数据重试。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5324](https://git.1814.love:8443/wx/HL/issues/5324)
- **PR**: [#5328](https://git.1814.love:8443/wx/HL/pulls/5328)
- **Merge commit**: [709105c1a1](https://git.1814.love:8443/wx/HL/commit/709105c1a1d26ae1c867fcc998286781f499faf2)
### 13.2 联系人
- **后端负责人**: @yst
- **前端负责人**: 待认领

查看文件

@ -1,82 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5337"
title: "Fleet 日期清单隔离陈旧失败与当前弹窗状态"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "0f025662443f50ca40d118043ed491a2d918a855"
target_release: ""
verified_at: "2026-07-29T13:45:27+08:00"
status_note: "frontend-only复用 #4760 已部署 day-orders 契约,无新增后端发布;本次只交接前端异步 identity 修复,fresh gateway probe 不适用"
updated_at: "2026-07-29"
base: "dev-v3"
---
# Fleet日期清单隔离陈旧失败与当前弹窗状态
> **Tracking Issue**[wx/HL#5337](https://git.1814.love:8443/wx/HL/issues/5337)
> **责任端**`mmg/hl-ui` Fleet Matrix 前端
> **前端基线**`v2.1@18daf2a03e9946c46d7bf31c7577adcb05d3c168`
> **性质**frontend-only;不是后端缺陷,不新增后端接口、字段、错误码或部署
## 问题与根因
日期弹窗快速从 A 日期切到 B 日期时,`fetchDayOrders``dayRequestSeq` 只在 Promise 成功后检查。A 旧请求若在 B 新请求成功后才失败,会在 `await` 处直接 reject,随后 A 对应的 `openDayList` catch 无 `requestSeq + date` 身份校验并无条件清空共享 `dayOrders`
结果是标题仍属于 B,但 rows 被旧 A 清空,页面显示 B 日期假空数据,统一 error 也可能属于旧 A。旧 success 已有序号保护;缺口仅在 failure/error/clear/loading/title 的共同身份约束。
## 变更接口
### `GET /admin/fleet/matrix/day-orders`
接口契约保持不变,继续沿用 `#4760`
```http
GET /admin/fleet/matrix/day-orders?date=YYYY-MM-DD
```
- 请求参数、响应结构、空值和错误码均不变。
- `backend_status=deployed` 仅表示 `#4760` 的既有接口契约已部署,不表示 #5337 有后端代码发布。
- `gateway_status=not_required`:根因是前端 Promise 乱序与共享状态写入,接口 fresh probe 不能证明竞态修复。
## 前端必须调整
1. 为每次日期请求建立稳定的 `requestSeq + date` identity。
2. 只有当前 identity 可以写 rows、清空 rows、更新 error/loading 或改变弹窗状态。
3. 陈旧 success 与陈旧 failure 都必须 no-op,不能依赖只有成功路径执行的序号检查。
4. title、rows、error、loading 必须绑定同一 identity;禁止新日期标题搭配旧错误或假空数据。
## 前端验收
- [ ] deferred 测试覆盖 A旧 success 晚于B新 success、A旧 failure 晚于B新 success、A旧 success 晚于B新 failure、当前请求 failure 四类交错。
- [ ] A旧 failure 晚于B新 success 时,B rows、B title 与 B error state 保持不变,旧 A 完全 no-op。
- [ ] 只有当前 `requestSeq + date` identity 可以写 rows、clear、error 与 loading。
- [ ] 真实 Matrix 页面挂载测试快速点击两个日期列头并控制 Promise 顺序,断言最终标题、行数、空态和错误均属于最新日期。
- [ ] 不改变 `/day-orders` API 字段、后端行为或公共契约。
## 验证证据
- 权威审计:`C:/Users/Administrator/AppData/Local/Temp/fleet-frontend-current-remote-audit-task_e6c54bb3a8b9.md`
- 审计 SHA-256`ab3ceafdf3a433ea7d0a99d1f8962839ed49d2819d4eab1d6de13c85e891d882`
- 当前前端远端源码:`mmg/hl-ui v2.1@18daf2a03e9946c46d7bf31c7577adcb05d3c168`
- 源码证据:`useFleetMatrixData.js:553-559``matrix/index.vue:902-908``DayListModal.vue:10-16,120-124`
- 审计已确认当前测试没有 day-orders deferred race 页面挂载覆盖;本条目初始状态为 `pending`,不宣称前端已修复或测试已通过。
## 关联与去重
- `#4760` 是 day-orders 既有接口和 Fleet Matrix 真实数据源责任,本条目不修改其历史文件。
- D-03 的 `todayDay` 未消费仍归 `#4760` existing-follow-up,不在 #5337 创建重复验收。
- D-02 byOrder 分窗状态守恒由独立 `#5338` 跟踪。
## 后端 closeout 持久证据2026-07-31
- 消费端修复提交:`mmg/hl-ui@0f025662443f50ca40d118043ed491a2d918a855`;无关联 PR,提交已进入 `v2.1` 主线历史。
- 测试环境自动部署task `a44ef795``v2.1@6b3b04c58da5126f922cdaea52caeb13ab343444`,2026-07-31 11:27:30–11:27:40,`status=success``exit_code=0``has_build_error=false`
- A/B 乱序定向回归:从修复提交归档到独立 OS Temp 副本后实跑 `useFleetMatrixData.spec.js``day-orders-race.spec.js``matrix.spec.js`,结果为 3 files / 25 tests passed。
- 测试日志 SHA-256`abc6d6a39f9f40aed114b2c338850341f8583949be012012d12de7ff165c9175`;关单时已把命令、结果与哈希持久回写 `wx/HL#5337`
- 后端 API 仍复用 #4760`GET /admin/fleet/matrix/day-orders`,无 #5337 后端代码、数据库、网关或生产变更。
- 本节只补后端关单和消费端实现证据;没有实际测试页面人工操作证据,因此 `frontend_status` 继续保持 `implemented`,不升级为 `released``verified`

查看文件

@ -1,84 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5338"
title: "Fleet 按订单分窗保持路由状态集合守恒"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-ui-pi"
frontend_ref: "868739d793aff1ca265296a7bf2cd681fcb557bd"
target_release: ""
verified_at: ""
status_note: "frontend-only复用 #4760 grid/unassigned 与 #5301 coarse status 已部署契约;#5338 无后端 diff、PR、测试或部署要求。前端代码已有 implemented 证据,但尚无 target_release 或页面验证证据,不上调 released/verified"
updated_at: "2026-07-31"
base: "dev-v3"
---
# Fleet按订单分窗保持路由状态集合守恒
> **Tracking Issue**[wx/HL#5338](https://git.1814.love:8443/wx/HL/issues/5338)
> **责任端**`mmg/hl-ui` Fleet Matrix 分窗前端
> **前端基线**`v2.1@18daf2a03e9946c46d7bf31c7577adcb05d3c168`
> **性质**frontend-only;不是后端缺陷,不新增后端接口、字段、错误码或部署
## 问题与根因
修复前,`matrix-solo?solo=byOrder&status=assigned` 已把 route status 传给 grid,且 coarse assigned 正确映射为 `holding/holding_urgent/assigned`;但共享 composable 会继续合并不带 status 的 `unassigned-orders``SoloByOrderView.visibleOrders` 又只排除 canceled,未按 route status 最终收口。
这会令 assigned 分窗混入无车的 `unassigned/unassigned_urgent` rows。主矩阵原本已使用共享 `matchesMatrixStatusFilter`#5338 只补齐 byOrder 分窗的同一保护,不归因于后端 grid 或 coarse status 契约。
## 变更接口
### `GET /admin/fleet/matrix/grid`
### `GET /admin/fleet/matrix/unassigned-orders`
接口契约保持不变,继续沿用 `#4760/#5301`
- coarse `assigned` = `holding + holding_urgent + assigned`
- coarse `unassigned` = `unassigned + unassigned_urgent`
- 只有 route `all` 的最终显示集合可以同时包含已派与未派两侧。
- `backend_status=deployed` 仅表示上述既有接口契约已部署,不表示 #5338 有后端代码发布。
- `gateway_status=not_required`:根因是前端将两个成功响应合并后缺少 view-level route filter,fresh API probe 不能证明页面集合守恒。
## 前端实现结果implemented,不代表 released/verified
1. `SoloByOrderView` 最终展示复用共享 `matchesMatrixStatusFilter`,未维护第二套状态映射。
2. route query、grid/unassigned 请求、visibleOrders 与跨窗口 drag source 使用同一筛选 identity。
3. `assigned` 只显示 `holding/holding_urgent/assigned`,排除全部无车未派 rows。
4. `unassigned` 只显示 `unassigned/unassigned_urgent`,排除已绑车 rows;仅 `all` 合并两侧。
5. 最终过滤后的同一数组同时用于渲染与拖拽,drag source 不再绕过 visibleOrders。
## 前端实现验收6/6;不代表页面发布/验证)
- [x] 组件挂载测试覆盖 route `all/unassigned/assigned`,显示集合与主矩阵同筛选口径守恒。
- [x] `assigned` 样本同时包含 `holding/holding_urgent/assigned`,并排除 `unassigned/unassigned_urgent` 无车 rows。
- [x] `unassigned` 只含未派 rows 并排除所有已绑车 rows;只有 `all` 合并两侧。
- [x] 最终展示与 drag source 都复用 `matchesMatrixStatusFilter` 的同一过滤结果。
- [x] year/month/fleetTeamIds/typeKeys/status 从 route、请求到 visible/drag 集合保持同一身份。
- [x] 不改变 grid、unassigned-orders、statuses[] 或 coarse status 后端契约。
## 验证证据
- 前端业务提交:[`mmg/hl-ui@868739d793aff1ca265296a7bf2cd681fcb557bd`](https://git.1814.love:8443/mmg/hl-ui/commit/868739d793aff1ca265296a7bf2cd681fcb557bd),仅修改 `SoloByOrderView.vue` 并新增 `SoloByOrderView.spec.js`;没有后端文件。
- 前端任务账本:`mmg/hl-ui:.claude/tasks/done/changelog-5338-3c335437bf.md`,记录 `status=done``source_status=implemented`、6 个 mount tests、21 个 composable tests、source 46 tests、ESLint/Prettier/Stylelint 与 production build 通过。
- Changelog 领取/完成提交:`wx/hl-api-changelog@4ad3a92f66741651b60d6d1c6f3d12983e96ea56` / `@73c4daab04bf094507698d9ab814cdbdd711bf73`
- 2026-07-31 只读复核时,当前 `v2.1@6b3b04c58da5126f922cdaea52caeb13ab343444` 仍保留共享 `matchesMatrixStatusFilter``visibleOrders` 最终过滤与 `setupCrossWindowDrag(() => visibleOrders.value)`;实现提交已在当前分支祖先链中。
- Deploy Panel 测试环境任务 `a44ef795` 已把当前 `v2.1@6b3b04c58da5` 构建部署成功,但本条目没有 `target_release` 或浏览器页面验证证据;因此 `frontend_status` 保持 `implemented`,不得表述为 `released``verified`
- 前端仓库没有关联 PR,交付形态为直接提交;本记录不追建事后 PR。
## 后端独立闭环证据
- #5338 是 frontend-only 集合过滤,后端 contract review 结论为 **no contract change**Controller、DTO/VO、Feign、字段、枚举、错误码与数据库均无 diff;故 #5338 的后端 merge/test/deploy 均为 `not_required`,不伪造 PR、测试或发布。
- 既有 `grid + unassigned-orders` 契约已由 #4760 完成PR #4761/#4772 已合并,Fleet/User 定向测试、Fleet reactor、Spotless、测试部署及独立账号真实 API 回归证据均持久记录在 [wx/HL#4760](https://git.1814.love:8443/wx/HL/issues/4760)。
- coarse/精确状态契约已由 #5301 完成PR #5304 squash 合并到 `dev-v3@ed56bec1fa466f92e0d41d7b92de3ad4756517b3`;51 个定向测试、Fleet reactor 2474 tests、Spotless、Fleet 双实例部署及真实网关验证记录在 [wx/HL#5301](https://git.1814.love:8443/wx/HL/issues/5301)。
- 当前 `dev-v3` 源码仍明确实现 coarse `assigned = holding + holding_urgent + assigned`,并保留 grid/unassigned-orders 契约;#5338 不需要 fresh gateway probe,因为 API 成功不能证明前端合并后的页面集合。
## 关联与去重
- `#4760` 规定 byOrder 使用 grid + unassigned-orders 并携带 route status;本条目补充最终显示集合守恒,不修改其历史文件。
- `#5301` 规定 coarse assigned 与精确 statuses[];本条目不改变该契约。
- D-03 的 `todayDay` 未消费仍归 `#4760` existing-follow-up,不在 #5338 创建重复验收。
- D-01 day-orders 陈旧失败隔离由独立 `#5337` 跟踪。

查看文件

@ -1,894 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5342"
title: "核单报表取消中间确认并由 finalize 固化"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi:019fadb7-dac9-74bf-9581-058835251208"
frontend_ref: "1444fc7f0bf34efaec0ee9f775f7d529b609b847"
target_release: "v2.1"
verified_at: "2026-07-29T20:43:00+08:00"
status_note: "管理后台已删除两张报表的中间确认调用;finalize 请求结构按后续 #5343 最终契约收口,pnpm checkpoint 全部通过。"
updated_at: "2026-07-29"
base: "dev-v3"
---
# 【修改接口·管理后台】核单报表取消中间确认并由 finalize 固化 (#5342)
> **PR**: #5345 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-29 13:28
## 1. 接口背景
核单流程不再要求用户分别确认“主报账人报账表”和“单团核算表”。核单未完成时,两张报表查询接口按当前业务数据实时返回;点击完成核单时,前端一次性提交转账、垫资结清和签字凭证信息,服务端按提交时的当前数据重新计算并固化终态。核单完成后,两张报表查询接口只返回该次完成核单时固化的内容,后续来源数据变化不会改写该终态结果。
## 变更接口2. 变更清单)
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查询主报账人报账表 | GET | `/v3/admin/order/:orderId/settlement/reports/reimbursement` | 行为修改 | 未完成核单时实时计算;完成核单后读取终态结果 |
| 2 | 查询单团核算表 | GET | `/v3/admin/order/:orderId/settlement/reports/group` | 行为修改 | 未完成核单时实时计算;完成核单后读取终态结果 |
| 3 | 确认主报账人报账表 | POST | `/v3/admin/order/:orderId/settlement/reports/reimbursement/confirm` | 删除 | 接口下线,调用返回业务码 `404` |
| 4 | 确认单团核算表 | POST | `/v3/admin/order/:orderId/settlement/reports/group/confirm` | 删除 | 接口下线,调用返回业务码 `404` |
| 5 | 完成核单 | POST | `/v3/admin/order/:orderId/settlement/finalize` | 请求与行为修改 | 请求体改为必填;删除两个客户端指纹;新增转账、垫资和签字凭证字段;提交时实时重算并固化终态 |
## 3. 接口详情
### 3.1 查询主报账人报账表
- **方法 + 路径**: `GET /v3/admin/order/:orderId/settlement/reports/reimbursement`
- **使用场景**: 打开核单报账表或刷新核单数据
- **认证**: 管理后台 JWT;房务角色不可访问
- **幂等性**: 幂等,只读
- **请求体**: 无
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` |
**响应字段**
| 字段 | 类型 | 可空 | 说明 |
|------|------|------|------|
| `id` | String | 是 | 报账表记录 ID;实时报表和终态快照中可为 `null` |
| `orderId` | String | 否 | 订单 ID |
| `reportStatus` | String | 否 | 报表状态,见 §6.1 |
| `sourceFingerprint` | String | 否 | 当前报表来源指纹,仅用于识别数据版本;前端不再回传 |
| `primaryReporterId` | String | 是 | 主报账人 ID |
| `primaryReporterName` | String | 是 | 主报账人姓名 |
| `primaryReporterRole` | String | 是 | 主报账人角色 |
| `reportVersion` | Integer | 否 | 报账表结构版本 |
| `driverCollectedTailAmount` | Decimal | 否 | 主报账人代收尾款 |
| `approvedAdvanceAmount` | Decimal | 否 | 已审批垫资金额 |
| `reportablePaidCostAmount` | Decimal | 否 | 可报账的已付成本 |
| `reporterNetAmount` | Decimal | 否 | 报账人净额;大于 0 表示报账人应转给公司,小于 0 表示公司应转给报账人 |
| `primaryReporterCollectedAmount` | Decimal | 否 | 主报账人代收金额 |
| `publicPrepaidAmount` | Decimal | 否 | 公共预支金额 |
| `primaryReporterDueAmount` | Decimal | 否 | 主报账人应报账金额 |
| `advanceOutstandingAmount` | Decimal | 否 | 未结清垫资金额 |
| `reconNetAmount` | Decimal | 否 | 报账净额 |
| `transferDirection` | String | 否 | 转账方向,见 §6.2 |
| `transferAmount` | Decimal | 否 | 应转账金额的绝对值 |
| `incomeLines` | Array\<Object> | 否 | 主报账人代收明细,字段见下表 |
| `expenseLines` | Array\<Object> | 否 | 现金已付成本明细,字段随费用分类变化,字段见下表 |
| `advanceLines` | Array\<Object> | 否 | 已审批垫资明细,字段见下表 |
| `vehicleLines` | Array\<Object> | 否 | 车辆独立明细;当前返回空数组,车辆金额已进入费用分类和汇总金额 |
| `transferStatus` | String | 是 | 未完成核单时为 `null`;终态为 `COMPLETED` |
| `transferDate` | String/date | 是 | 转账日期,格式 `YYYY-MM-DD` |
| `transferRef` | String | 是 | 转账流水号 |
| `advanceSettledFlag` | Boolean | 是 | 垫资是否结清 |
| `signedVoucher` | Object | 是 | 签字凭证;结构同 finalize 请求的 `signedVoucher` |
| `generatedBy` | String | 是 | 历史生成操作人 ID;实时/终态模式下可为 `null` |
| `generatedByName` | String | 是 | 历史生成操作人姓名;实时/终态模式下可为 `null` |
| `generatedAt` | String/date-time | 是 | 历史生成时间;实时/终态模式下可为 `null` |
| `confirmedBy` | String | 是 | 完成核单操作人 ID;未完成核单时为 `null` |
| `confirmedByName` | String | 是 | 完成核单操作人姓名;未完成核单时为 `null` |
| `confirmedAt` | String/date-time | 是 | 完成核单时间;未完成核单时为 `null` |
**`incomeLines[]` 字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | String | 固定为 `DRIVER_CASH_RECEIPT` |
| `receiptId` | String | 收款记录 ID |
| `amount` | Decimal | 收款金额 |
| `channel` | String | 收款渠道;当前参与报账的值为 `DRIVER_CASH` |
| `payType` | String/null | 支付类型 |
| `collectorStaffId` | String/null | 收款人员 ID |
| `collectorName` | String/null | 收款人员姓名 |
| `collectorRole` | String/null | 收款人员角色 |
| `receivedAt` | String/date-time/null | 收款时间 |
| `remark` | String/null | 备注 |
**`advanceLines[]` 字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | String | 固定为 `APPROVED_ADVANCE` |
| `advanceId` | String | 垫资记录 ID |
| `payeeStaffId` | String/null | 收款人员 ID |
| `payeeName` | String/null | 收款人员姓名 |
| `payeeRole` | String/null | 收款人员角色 |
| `advanceType` | String/null | 垫资类型 |
| `amount` | Decimal | 已审批金额 |
| `purpose` | String/null | 用途 |
| `voucherUrl` | String/null | 垫资凭证地址 |
| `status` | String | 垫资状态 |
| `submittedAt` | String/date-time/null | 提交时间 |
| `approvedAt` | String/date-time/null | 审批时间 |
| `approvedBy` | String/null | 审批人 ID |
**`expenseLines[]` 公共字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `category` | String | 费用分类,见 §6.3 |
| `kind` | String | 明细类型,例如 `HOTEL``TICKET``MEAL``VEHICLE_FEE``STAFF:DRIVER` |
| `amount` | Decimal | 当前行实际成本 |
| `paymentMethod` | String | 当前仅包含 `CASH_PAID` 行 |
`expenseLines[]` 会按 `kind` 追加以下分类字段:
- `HOTEL`: `hotelAssignmentId``hotelId``roomTypeId``dayNumber``stayDate``hotelName``roomType``roomTypeName``roomCount``unitPrice``plannedCost``sourceType``sourceId``voucherUrls``remark`
- `TICKET`: `sourceType``scenicAssignmentId``dayNumber``dayDate``scenicName``specName``ticketCount``ticketUnitPrice``sellPrice``totalAmount``plannedCost``voucherUrls``remark`
- `MEAL`: `mealType``mealDate``mealName``quantity``unitPrice``voucherUrls``remark`
- `VEHICLE_FEE`: `sourceRecordType``sourceDetailId``serviceDate``vehicleId``vehiclePlate``vehicleModelId``vehicleModelName``driverId``driverName``startDate``endDate``dailyPrice``paymentTypeCode``paymentTypeName`
- `STAFF:*`: `staffRole``staffId``staffName``totalPlannedCost``voucherUrls``reimburse``settleStatus``settledDate``transferRef``detail``remark`
- `EXPENSE:*`: `expenseType``projectName``expenseDate``voucherUrls``remark`
- `SUBSIDY:*`: `subsidyType``projectName``expenseDate``voucherUrls``remark`
**行为**
- 订单不存在当前终态快照时,每次请求均按当前核单数据计算,`reportStatus=GENERATED`
- 订单存在当前终态快照时,返回完成核单时固化的报账表,`reportStatus=CONFIRMED`
- `sourceFingerprint` 继续返回,但不再作为任何前端确认或 finalize 入参。
### 3.2 查询单团核算表
- **方法 + 路径**: `GET /v3/admin/order/:orderId/settlement/reports/group`
- **使用场景**: 打开单团核算表或刷新核算结果
- **认证**: 管理后台 JWT;房务角色不可访问
- **幂等性**: 幂等,只读
- **请求体**: 无
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` |
**响应字段**
| 字段 | 类型 | 可空 | 说明 |
|------|------|------|------|
| `id` | String | 是 | 单团核算表记录 ID;实时报表和终态快照中可为 `null` |
| `orderId` | String | 否 | 订单 ID |
| `reportStatus` | String | 否 | 报表状态,见 §6.1 |
| `sourceFingerprint` | String | 否 | 当前报表来源指纹,仅用于识别数据版本;前端不再回传 |
| `baseOrderAmount` | Decimal | 否 | 订单基础金额 |
| `otherIncomeAmount` | Decimal | 否 | 其他收入金额 |
| `discountAmount` | Decimal | 否 | 优惠金额 |
| `adjustedReceivableAmount` | Decimal | 否 | 调整后应收金额 |
| `paidAmount` | Decimal | 否 | 已收金额 |
| `actualRefundedAmount` | Decimal | 否 | 实际退款金额 |
| `netRevenueAmount` | Decimal | 否 | 净收入 |
| `netReceivedAmount` | Decimal | 否 | 净已收 |
| `outstandingAmount` | Decimal | 否 | 待收金额 |
| `hotelCost` | Decimal | 否 | 住宿成本 |
| `ticketCost` | Decimal | 否 | 门票/游玩项目成本 |
| `mealCost` | Decimal | 否 | 餐食成本 |
| `vehicleCost` | Decimal | 否 | 车辆成本 |
| `guideCost` | Decimal | 否 | 导游/领队成本 |
| `photographerCost` | Decimal | 否 | 摄影成本 |
| `otherExpenseCost` | Decimal | 否 | 其他支出成本 |
| `insurancePremium` | Decimal | 否 | 保险保费 |
| `totalCost` | Decimal | 否 | 总成本 |
| `paidCost` | Decimal | 否 | 已付成本 |
| `unpaidCost` | Decimal | 否 | 未付成本 |
| `grossProfit` | Decimal | 否 | 毛利 |
| `grossProfitRate` | Decimal | 否 | 毛利率,小数形式 |
| `travelerCount` | Integer | 否 | 出行人数 |
| `perCapitaRevenue` | Decimal | 否 | 人均收入 |
| `perCapitaCost` | Decimal | 否 | 人均成本 |
| `perCapitaProfit` | Decimal | 否 | 人均利润 |
| `incomeLines` | Array\<Object> | 否 | 收入汇总行,固定结构见下表 |
| `costCategories` | Array\<Object> | 否 | 成本分类汇总,固定结构见下表 |
| `generatedBy` | String | 是 | 历史生成操作人 ID;实时/终态模式下可为 `null` |
| `generatedByName` | String | 是 | 历史生成操作人姓名;实时/终态模式下可为 `null` |
| `generatedAt` | String/date-time | 是 | 历史生成时间;实时/终态模式下可为 `null` |
| `confirmedBy` | String | 是 | 完成核单操作人 ID;未完成核单时为 `null` |
| `confirmedByName` | String | 是 | 完成核单操作人姓名;未完成核单时为 `null` |
| `confirmedAt` | String/date-time | 是 | 完成核单时间;未完成核单时为 `null` |
**`incomeLines[]` 字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | String | `BASE_ORDER``OTHER_INCOME``DISCOUNT``ACTUAL_REFUND` |
| `amount` | Decimal | 金额;优惠和实际退款以负数返回 |
**`costCategories[]` 字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `category` | String | `HOTEL``TICKET``MEAL``VEHICLE``GUIDE``PHOTOGRAPHER``OTHER_EXPENSE``INSURANCE` |
| `amount` | Decimal | 分类成本 |
**行为**
- 订单不存在当前终态快照时,每次请求均按当前核单数据计算,`reportStatus=GENERATED`
- 订单存在当前终态快照时,返回完成核单时固化的单团核算表,`reportStatus=CONFIRMED`
- `sourceFingerprint` 继续返回,但不再作为任何前端确认或 finalize 入参。
### 3.3 完成核单
- **方法 + 路径**: `POST /v3/admin/order/:orderId/settlement/finalize`
- **使用场景**: 用户检查实时主报账表和单团核算表后,点击完成核单
- **认证**: 管理后台 JWT;需要核单资金写权限;房务角色不可访问
- **幂等性**: 已存在当前终态快照时,重复请求返回已有终态结果,不重新生成新终态
- **限流**: 无接口级特殊限流
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` |
**请求体字段**
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `remark` | String | 否 | 核单整体备注 | 最多 `500` 字符 |
| `transferDate` | String/date | 条件必填 | 转账日期 | `reporterNetAmount != 0` 时必填;格式 `YYYY-MM-DD` |
| `transferRef` | String | 条件必填 | 转账流水号 | `reporterNetAmount != 0` 时必须为非空字符串;最多 `128` 字符 |
| `advanceSettledFlag` | Boolean | 是 | 垫资是否结清;必须明确传 `true``false` | 不可为 `null` |
| `signedVoucher` | Object | 是 | 签字凭证 | 不可为 `null` |
| `signedVoucher.files` | Array\<Object> | 是 | 签字凭证文件 | `1``9` 项;重复 URL 按规范化后的 URL 去重并保留首项 |
| `signedVoucher.files[].name` | String | 否 | 文件名 | 最多 `255` 字符 |
| `signedVoucher.files[].url` | String | 是 | 文件地址 | 非空;最多 `1024` 字符;必须是带有效主机名的绝对 `http/https` URL |
| `signedVoucher.note` | String | 否 | 签字凭证备注 | 最多 `500` 字符 |
以下字段已经删除,前端不得继续发送:
| 删除字段 | 原类型 | 迁移方式 |
|----------|--------|----------|
| `reimbursementExpectedSourceFingerprint` | String | 删除本地缓存和提交逻辑;完成核单时不再回传报账表指纹 |
| `groupExpectedSourceFingerprint` | String | 删除本地缓存和提交逻辑;完成核单时不再回传单团核算表指纹 |
**响应字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `summaryId` | String | 核单汇总 ID |
| `finalSnapshotId` | String | 核单终态快照 ID |
| `finalSnapshotVersionNo` | Integer | 终态快照版本号,从 `1` 开始 |
| `finalSnapshotStatus` | String | 当前终态固定为 `FINALIZED` |
| `orderId` | String | 订单 ID |
| `settledAt` | String/date-time | 核单完成时间 |
| `totalAmount` | String/Decimal | 订单总金额快照 |
| `paidAmount` | String/Decimal | 已付金额快照 |
| `balanceAmount` | String/Decimal | 尾款金额快照 |
| `roomCost` | String/Decimal | 住宿实际成本 |
| `ticketCost` | String/Decimal | 门票实际成本 |
| `staffCost` | String/Decimal | 人员费用实际成本 |
| `subsidyCost` | String/Decimal | 补助实际成本 |
| `mealCost` | String/Decimal | 餐食实际成本 |
| `vehicleCost` | String/Decimal | 车辆基础服务总车费 |
| `otherExpenseCost` | String/Decimal | 其他支出实际成本 |
| `insurancePremium` | String/Decimal | 保险实际保费 |
| `totalActualCost` | String/Decimal | 总实际成本 |
| `driverTransferAmount` | String/Decimal | 给司机/主报账人转回金额 |
| `profitAmount` | String/Decimal | 公司毛利 |
| `profitRate` | Decimal | 毛利率;订单总金额为 `0` 时返回 `0` |
| `orderStatusAfter` | String | 当前返回 `待财务复核` |
| `mqTriggered` | Boolean | 当前固定返回 `false`;完成核单不发布结算 MQ |
| `warnings` | Array\<String> | 软预警列表;不阻塞完成核单 |
**提交行为**
1. 前端不再先调用任何报表“确认”接口。
2. 服务端按提交时的当前核单数据重新计算两张报表和所有汇总金额,不采信前端缓存的金额或指纹。
3. `transferDate``transferRef``advanceSettledFlag``signedVoucher` 与本次完成核单结果一并固化。
4. 成功后,两张 GET 报表接口返回本次固化结果。
### 3.4 已删除的报表确认接口
以下接口不再有可用请求契约:
| 原接口 | 原请求字段 | 当前结果 |
|--------|------------|----------|
| `POST /v3/admin/order/:orderId/settlement/reports/reimbursement/confirm` | `expectedSourceFingerprint``transferStatus``transferDate``transferRef``advanceSettledFlag``signedVoucher` | 业务码 `404` |
| `POST /v3/admin/order/:orderId/settlement/reports/group/confirm` | `expectedSourceFingerprint` | 业务码 `404` |
前端必须删除这两个请求,不要用忽略 `404`、重试或降级继续调用的方式兼容。
## 4. 接口入参
### 4.1 通用路径参数
| 接口 | 字段 | 类型 | 必填 | 规则 |
|------|------|------|------|------|
| 两张 GET 报表、finalize | `orderId` | String/Long | 是 | 必须大于 `0` |
### 4.2 请求体变化总览
| 接口 | 修改前 | 修改后 |
|------|--------|--------|
| 主报账表确认 | 独立 POST 提交报账表指纹及凭证 | 接口删除 |
| 单团核算表确认 | 独立 POST 提交单团核算表指纹 | 接口删除 |
| finalize | 请求体可缺省;主要提交两个报告指纹和可选 `remark` | 请求体必填;提交 `remark``transferDate``transferRef``advanceSettledFlag``signedVoucher`;不再提交任何指纹 |
## 5. 出参
### 5.1 报表查询
- 两张 GET 接口的字段结构保持不变。
- 未完成核单时返回最新实时计算结果。
- 完成核单后返回完成核单时固化的结果。
- 报表 `sourceFingerprint` 仍存在于出参,但只表示数据版本,前端不得再将其用于确认或 finalize。
### 5.2 完成核单
- finalize 响应字段结构保持 `SettlementSubmitRespVO`
- `finalSnapshotId``finalSnapshotVersionNo``finalSnapshotStatus` 标识本次固化结果。
- `mqTriggered` 的当前契约为固定 `false`,前端不得用该字段判断是否需要等待 MQ。
## 6. 枚举 / 数据字典
### 6.1 `reportStatus`
**所属字段**: 两张报表响应 `reportStatus` | **类型**: String
| 值 | 中文 | 当前语义 |
|----|------|----------|
| `GENERATED` | 实时结果 | 订单未完成核单,响应按当前数据实时计算 |
| `CONFIRMED` | 已固化 | 订单已完成核单,响应来自终态结果 |
| `STALE` | 历史过期状态 | 仅兼容历史报表数据;新实时查询流程不要求前端据此重新生成或确认 |
### 6.2 `transferDirection`
**所属字段**: 主报账人报账表 `transferDirection` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `REPORTER_TO_COMPANY` | 报账人转给公司 | `reporterNetAmount > 0` |
| `COMPANY_TO_REPORTER` | 公司转给报账人 | `reporterNetAmount < 0` |
| `BALANCED` | 已平衡 | `reporterNetAmount = 0` |
### 6.3 报账费用分类
**所属字段**: `expenseLines[].category``costCategories[].category` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `HOTEL` | 住宿 | 住宿成本 |
| `TICKET` | 门票/游玩项目 | 门票及游玩项目成本 |
| `MEAL` | 餐食 | 餐食成本 |
| `VEHICLE` | 车辆 | 车辆成本 |
| `GUIDE` | 导游/领队 | 导游及领队成本 |
| `PHOTOGRAPHER` | 摄影 | 摄影成本 |
| `OTHER_EXPENSE` | 其他支出 | 其他支出成本 |
| `INSURANCE` | 保险 | 保险保费;仅用于单团核算表成本分类 |
### 6.4 `finalSnapshotStatus`
**所属字段**: finalize 响应 `finalSnapshotStatus` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `FINALIZED` | 已固化 | 当前核单终态有效 |
### 6.5 `transferStatus`
**所属字段**: 主报账人报账表 `transferStatus` | **类型**: String/null
| 值 | 中文 | 说明 |
|----|------|------|
| `COMPLETED` | 已完成 | finalize 成功后固化到终态报账表 |
| `null` | 未固化 | 尚未完成核单的实时报表 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 请求参数校验失败 | `orderId <= 0`、finalize 缺请求体/必填字段、字段超长、凭证文件数量不在 19 |
| `403` | 无访问权限 | 房务角色或无权访问当前订单 |
| `404` | 接口不存在 | 继续调用两个已删除的 `/confirm` 接口 |
| `584051` | 当前核单状态不允许提交结算 | review 状态不是待核单或核单中 |
| `584056` | 订单已结算,不能重复提交 | 已存在结算汇总但缺少可返回的当前终态 |
| `584071` | 无权访问该订单(公司隔离) | 当前管理员不能查看该订单 |
| `584077` | 存在未确认的其他收入 | finalize 前其他收入未确认 |
| `584078` | 其他收入关联附加费已失效或金额不一致 | finalize 前其他收入与当前附加费不一致 |
| `584079` | 存在未纳入核单分类的有效附加费 | finalize 前还有有效附加费未进入核单 |
| `584081` | 对账数据不一致 | 已付金额与有效支付、线下收款合计不一致 |
| `584082` | 存在待收尾款 | 单团核算 `outstandingAmount != 0` |
| `584085` | 存在辅助人员结算未完成 | 辅助人员未全部完成结算或缺转账流水 |
| `584092` | 存在未确认的人员费用 | 人员费用确认状态未全部完成 |
| `584097` | 凭证 URL 格式或数量不合法 | URL 不是有效绝对 `http/https` 地址、为空、过长或数量超限 |
| `584100` | 车辆总车费暂时不可用 | 报表查询或 finalize 暂时无法取得车辆费用 |
| `584101` | 存在未完结派车或未确认车辆总车费 | 当前车辆数据尚不能用于核单 |
| `584102` | 当前用车需求没有可核单的车辆总车费 | 有用车需求但没有可用车辆费用 |
| `584315` | 核单来源数据已变化,请刷新后重新确认 | finalize 提交期间当前用车需求发生变化 |
| `584316` | 核单报告发生并发变化,请刷新后重试 | 同一订单并发完成核单发生冲突 |
| `584317` | 当前报告状态不允许执行该操作 | 报账人净额非 0 但缺转账日期/流水,或签字凭证不可用 |
| `584320` | 核单分类明细尚未保存完整 | 当前分类数据不能用于报账和 finalize |
## 验证证据8. 示例)
### 8.1 典型成功:查询实时主报账人报账表
**请求**
```http
GET /v3/admin/order/1914050000000001/settlement/reports/reimbursement
Authorization: Bearer <admin-jwt>
```
无请求体。
**响应**
```json
{
"code": 200,
"msg": "success",
"data": {
"id": null,
"orderId": "1914050000000001",
"reportStatus": "GENERATED",
"sourceFingerprint": "7a0e84a9f9d0cb411f9cff8d6a0d1c047726bb05b68c8e23af5629afdbdd67c1",
"primaryReporterId": "3001",
"primaryReporterName": "示例报账人",
"primaryReporterRole": "DRIVER",
"reportVersion": 1,
"driverCollectedTailAmount": 2000.00,
"approvedAdvanceAmount": 500.00,
"reportablePaidCostAmount": 1200.00,
"reporterNetAmount": 1300.00,
"primaryReporterCollectedAmount": 2000.00,
"publicPrepaidAmount": 1200.00,
"primaryReporterDueAmount": 800.00,
"advanceOutstandingAmount": 500.00,
"reconNetAmount": 1300.00,
"transferDirection": "REPORTER_TO_COMPANY",
"transferAmount": 1300.00,
"incomeLines": [
{
"type": "DRIVER_CASH_RECEIPT",
"receiptId": "9100000000001",
"amount": 2000.00,
"channel": "DRIVER_CASH",
"payType": "CASH",
"collectorStaffId": "3001",
"collectorName": "示例报账人",
"collectorRole": "DRIVER",
"receivedAt": "2026-07-28T15:30:00",
"remark": "示例代收尾款"
}
],
"expenseLines": [
{
"category": "HOTEL",
"kind": "HOTEL",
"hotelAssignmentId": "9200000000001",
"hotelId": "1001",
"roomTypeId": "2001",
"dayNumber": 1,
"stayDate": "2026-07-20",
"hotelName": "示例酒店",
"roomType": "STANDARD",
"roomTypeName": "标准间",
"roomCount": 2,
"unitPrice": 300.00,
"plannedCost": 600.00,
"amount": 600.00,
"paymentMethod": "CASH_PAID",
"sourceType": "HOUSE_ASSIGNMENT",
"sourceId": "9200000000001",
"voucherUrls": ["https://oss.example.com/vouchers/hotel-1.pdf"],
"remark": null
}
],
"advanceLines": [
{
"type": "APPROVED_ADVANCE",
"advanceId": "9300000000001",
"payeeStaffId": "3001",
"payeeName": "示例报账人",
"payeeRole": "DRIVER",
"advanceType": "PUBLIC",
"amount": 500.00,
"purpose": "行程公共支出",
"voucherUrl": "https://oss.example.com/vouchers/advance-1.pdf",
"status": "APPROVED",
"submittedAt": "2026-07-19T10:00:00",
"approvedAt": "2026-07-19T11:00:00",
"approvedBy": "10001"
}
],
"vehicleLines": [],
"transferStatus": null,
"transferDate": null,
"transferRef": null,
"advanceSettledFlag": null,
"signedVoucher": null,
"generatedBy": null,
"generatedByName": null,
"generatedAt": null,
"confirmedBy": null,
"confirmedByName": null,
"confirmedAt": null
}
}
```
### 8.2 典型成功:查询实时单团核算表
**请求**
```http
GET /v3/admin/order/1914050000000001/settlement/reports/group
Authorization: Bearer <admin-jwt>
```
无请求体。
**响应**
```json
{
"code": 200,
"msg": "success",
"data": {
"id": null,
"orderId": "1914050000000001",
"reportStatus": "GENERATED",
"sourceFingerprint": "651cb6708a49f169e2ccb1b455267def9cc1a69d06935f9d9eeb41919897e0fb",
"baseOrderAmount": 24800.00,
"otherIncomeAmount": 500.00,
"discountAmount": 300.00,
"adjustedReceivableAmount": 25000.00,
"paidAmount": 25000.00,
"actualRefundedAmount": 0.00,
"netRevenueAmount": 25000.00,
"netReceivedAmount": 25000.00,
"outstandingAmount": 0.00,
"hotelCost": 4280.00,
"ticketCost": 3680.00,
"mealCost": 860.00,
"vehicleCost": 1260.00,
"guideCost": 800.00,
"photographerCost": 600.00,
"otherExpenseCost": 300.00,
"insurancePremium": 180.00,
"totalCost": 11960.00,
"paidCost": 11960.00,
"unpaidCost": 0.00,
"grossProfit": 13040.00,
"grossProfitRate": 0.521600,
"travelerCount": 5,
"perCapitaRevenue": 5000.00,
"perCapitaCost": 2392.00,
"perCapitaProfit": 2608.00,
"incomeLines": [
{
"type": "BASE_ORDER",
"amount": 24800.00
},
{
"type": "OTHER_INCOME",
"amount": 500.00
},
{
"type": "DISCOUNT",
"amount": -300.00
},
{
"type": "ACTUAL_REFUND",
"amount": 0.00
}
],
"costCategories": [
{
"category": "HOTEL",
"amount": 4280.00
},
{
"category": "TICKET",
"amount": 3680.00
},
{
"category": "MEAL",
"amount": 860.00
},
{
"category": "VEHICLE",
"amount": 1260.00
},
{
"category": "GUIDE",
"amount": 800.00
},
{
"category": "PHOTOGRAPHER",
"amount": 600.00
},
{
"category": "OTHER_EXPENSE",
"amount": 300.00
},
{
"category": "INSURANCE",
"amount": 180.00
}
],
"generatedBy": null,
"generatedByName": null,
"generatedAt": null,
"confirmedBy": null,
"confirmedByName": null,
"confirmedAt": null
}
}
```
### 8.3 典型成功:完成核单并固化
**请求**
```http
POST /v3/admin/order/1914050000000001/settlement/finalize
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{
"remark": "核单完成",
"transferDate": "2026-07-29",
"transferRef": "BANK-20260729-001",
"advanceSettledFlag": true,
"signedVoucher": {
"files": [
{
"name": "签字单.pdf",
"url": "https://oss.example.com/vouchers/signed-20260729.pdf"
}
],
"note": "签字凭证已回收"
}
}
```
**响应**
```json
{
"code": 200,
"msg": "success",
"data": {
"summaryId": "9400000000001",
"finalSnapshotId": "9400000000002",
"finalSnapshotVersionNo": 1,
"finalSnapshotStatus": "FINALIZED",
"orderId": "1914050000000001",
"settledAt": "2026-07-29T13:28:22",
"totalAmount": "25000.00",
"paidAmount": "25000.00",
"balanceAmount": "0.00",
"roomCost": "4280.00",
"ticketCost": "3680.00",
"staffCost": "1400.00",
"subsidyCost": "0.00",
"mealCost": "860.00",
"vehicleCost": "1260.00",
"otherExpenseCost": "300.00",
"insurancePremium": "180.00",
"totalActualCost": "11960.00",
"driverTransferAmount": "11780.00",
"profitAmount": "13040.00",
"profitRate": 0.5216,
"orderStatusAfter": "待财务复核",
"mqTriggered": false,
"warnings": []
}
}
```
### 8.4 边界:报账人净额为 0
当最新 `reporterNetAmount=0` 时,`transferDate``transferRef` 可以省略;`advanceSettledFlag``signedVoucher` 仍必须提交。
**请求**
```http
POST /v3/admin/order/1914050000000002/settlement/finalize
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{
"remark": "收支已平衡",
"advanceSettledFlag": false,
"signedVoucher": {
"files": [
{
"name": "签字单.jpg",
"url": "https://oss.example.com/vouchers/signed-balanced.jpg"
}
]
}
}
```
**响应**
```json
{
"code": 200,
"msg": "success",
"data": {
"summaryId": "9400000000011",
"finalSnapshotId": "9400000000012",
"finalSnapshotVersionNo": 1,
"finalSnapshotStatus": "FINALIZED",
"orderId": "1914050000000002",
"settledAt": "2026-07-29T13:40:00",
"totalAmount": "0.00",
"paidAmount": "0.00",
"balanceAmount": "0.00",
"roomCost": "0.00",
"ticketCost": "0.00",
"staffCost": "0.00",
"subsidyCost": "0.00",
"mealCost": "0.00",
"vehicleCost": "0.00",
"otherExpenseCost": "0.00",
"insurancePremium": "0.00",
"totalActualCost": "0.00",
"driverTransferAmount": "0.00",
"profitAmount": "0.00",
"profitRate": 0,
"orderStatusAfter": "待财务复核",
"mqTriggered": false,
"warnings": []
}
}
```
### 8.5 异常:仍调用已删除的确认接口
**请求**
```http
POST /v3/admin/order/1914050000000001/settlement/reports/group/confirm
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{
"expectedSourceFingerprint": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
}
```
**响应**
```json
{
"code": 404,
"msg": "请求地址不存在",
"data": null
}
```
### 8.6 异常:车辆费用尚未可核单
**请求**
```http
POST /v3/admin/order/1914050000000001/settlement/finalize
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{
"transferDate": "2026-07-29",
"transferRef": "BANK-20260729-001",
"advanceSettledFlag": true,
"signedVoucher": {
"files": [
{
"name": "签字单.pdf",
"url": "https://oss.example.com/vouchers/signed-20260729.pdf"
}
]
}
}
```
**响应**
```json
{
"code": 584101,
"msg": "存在未完结派车或未确认车辆总车费,暂不能核单",
"data": null
}
```
## 9. 业务边界
- 报表 GET 的“实时”以每次请求时可用于核单的当前数据为准,前端不要把上一次响应当作提交凭据。
- 完成核单前,前端可以重复查询两张报表;无需执行任何“生成”或“确认”步骤。
- finalize 不接收前端金额。页面展示金额与提交时权威数据发生变化时,以提交时重新计算结果为准。
- 报账人净额不为 `0` 时,必须同时提交 `transferDate` 和非空 `transferRef`
- `signedVoucher.files` 原始数组必须为 `1``9` 项;URL 会去除首尾空格、规范化并按 URL 去重。
- 完成核单成功后,两张报表进入终态读取;只有业务上的核单反确认使当前终态失效后,查询才重新进入实时模式。
- finalize 成功响应中的 `warnings` 是软预警,不表示提交失败。
- 已有当前终态时重复调用 finalize 返回已有终态,不会依据本次请求改写已固化的转账或凭证信息。
## 10. 修改前后对比
### 10.1 字段级对比
| 接口/字段 | 修改前 | 修改后 |
|-----------|--------|--------|
| finalize 请求体 | 可缺省 | 必填 |
| `remark` | 可选,最多 500 字符 | 保持不变 |
| `reimbursementExpectedSourceFingerprint` | finalize 必填 | 删除 |
| `groupExpectedSourceFingerprint` | finalize 必填 | 删除 |
| `transferDate` | 在主报账表确认接口提交 | 移至 finalize;报账人净额非 0 时必填 |
| `transferRef` | 在主报账表确认接口提交 | 移至 finalize;报账人净额非 0 时必填,最多 128 字符 |
| `advanceSettledFlag` | 在主报账表确认接口提交 | 移至 finalize,必填 |
| `signedVoucher` | 在主报账表确认接口提交 | 移至 finalize,必填 |
| `signedVoucher.files` | 原确认接口字段 | finalize 中要求 19 项 |
| `signedVoucher.files[].name` | 原确认接口未明确长度 | 最多 255 字符 |
| `signedVoucher.files[].url` | 原确认接口未明确长度 | 必填;最多 1024 字符;绝对 http/https URL |
| `signedVoucher.note` | 原确认接口未明确长度 | 最多 500 字符 |
| `mqTriggered` | 示例和历史说明可能按 `true` 理解 | 当前固定 `false` |
### 10.2 行为级对比
| 行为 | 修改前 | 修改后 |
|------|--------|--------|
| 主报账表 | 先读取,再调用独立 confirm | GET 实时读取;不再确认 |
| 单团核算表 | 主报账表确认后再读取并 confirm | GET 实时读取;不再确认 |
| 数据变化处理 | 前端携带两张报表指纹,指纹过期时刷新重试 | 前端不携带指纹;finalize 按提交时数据重算 |
| 转账/垫资/签字信息 | 主报账表 confirm 时提交 | finalize 时一次提交 |
| 完成核单后查询 | 依赖已确认报表记录 | 返回完成核单时固化的终态结果 |
| 重复 finalize | 依赖旧报告确认门禁 | 已有当前终态时返回已有结果 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**: 是。两个 POST 确认接口删除,finalize 请求字段和必填规则改变。
- **前端是否必须同步调整**: 是。旧页面继续调用 `/confirm` 会收到业务码 `404`;旧 finalize 请求缺少新必填字段会收到业务码 `400`
- **查询字段兼容性**: 两张 GET 报表的顶层字段结构保持不变,但数据时效语义变为“未终态实时、终态固定”。
### 11.2 回滚说明
- 如果接口契约回滚,前端需要同步恢复两次确认请求和两个指纹字段。
- 前后端不能混用新旧流程:新版前端不再保留报表确认指纹,旧版后端仍会要求指纹和独立确认。
## 12. 注意事项
- 删除“确认主报账表”“确认单团核算表”按钮、请求封装、loading 状态、重试逻辑和指纹缓存。
- 页面展示仍调用两个 GET 接口;无需在详情加载时调用任何写接口。
- “完成核单”按钮直接提交 finalize 新请求体。
- 不要把 GET 返回的 `sourceFingerprint` 填回 finalize。
- 不要继续发送已删除字段;即使服务端当前可能忽略未知 JSON 字段,前端类型和请求对象也应删除。
- `signedVoucher` 不是可选附件:至少需要一个有效 URL。
- `mqTriggered=false` 是当前固定契约,不要显示“MQ 触发失败”或据此轮询。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5342](https://git.1814.love:8443/wx/HL/issues/5342)
- **PR**: [#5345](https://git.1814.love:8443/wx/HL/pulls/5345)
- **Merge commit**: [eb9ecfafad](https://git.1814.love:8443/wx/HL/commit/eb9ecfafadde4cb9e2dbe5be8193abcf569ca44c)
### 13.2 联系人
- **后端负责人**: @yst
- **消费端**: v3 管理后台

查看文件

@ -1,874 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5343"
title: "核单确认收口到完成核单"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi:019fadb7-dac9-74bf-9581-058835251208"
frontend_ref: "1444fc7f0bf34efaec0ee9f775f7d529b609b847"
target_release: "v2.1"
verified_at: "2026-07-29T20:43:00+08:00"
status_note: "管理后台已删除旧报表 confirm 流程,完成核单改为提交双指纹与嵌套 reimbursementConfirmation;pnpm checkpoint 全部通过。"
updated_at: "2026-07-29"
base: "dev-v3"
---
# ⚠️【修改接口·管理后台】核单确认收口到完成核单 (#5343)
> **PR**: #5347 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-29
## 1. 接口背景
主报账表和单团核算表不再各自提供“确认”写操作。页面先通过两张 GET 报表取得同一轮核单事实对应的两个 `sourceFingerprint`,再由“完成核单”一次提交双指纹、转账信息、预支处理标志和签字凭证。
本文纠正并取代 `29_5342_核单报表取消中间确认并由finalize固化-修改接口-管理后台.md` 中关于 finalize 请求的说明:**双指纹没有删除,仍是 finalize 必填字段;转账与凭证字段必须放在必填的 `reimbursementConfirmation` 对象内。**
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查询主报账表 | GET | `/v3/admin/order/{orderId}/settlement/reports/reimbursement` | 行为明确 | 返回主报账数据及 `sourceFingerprint`,该指纹必须回传给 finalize |
| 2 | 查询单团核算表 | GET | `/v3/admin/order/{orderId}/settlement/reports/group` | 行为明确 | 返回单团核算数据及 `sourceFingerprint`,该指纹必须回传给 finalize |
| 3 | 完成核单 | POST | `/v3/admin/order/{orderId}/settlement/finalize` | 请求与行为修改 | 必填双指纹和嵌套 `reimbursementConfirmation`;成功后一次完成核单 |
| 4 | 确认主报账表 | POST | `/v3/admin/order/{orderId}/settlement/reports/reimbursement/confirm` | 删除接口 | 路由继续保持删除,不得调用 |
| 5 | 确认单团核算表 | POST | `/v3/admin/order/{orderId}/settlement/reports/group/confirm` | 删除接口 | 路由继续保持删除,不得调用 |
## 3. 接口详情
### 3.1 查询主报账表
- **方法与路径**`GET /v3/admin/order/{orderId}/settlement/reports/reimbursement`
- **使用场景**:展示主报账表,并在调用 finalize 前取得最新主报账指纹
- **认证**:管理后台 JWT;房务角色不可访问
- **幂等性**:幂等,只读
- **限流**:无接口级特殊限流
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` |
**请求体**
无。
**响应字段**
| `data` 字段 | JSON 类型 | 可空 | 说明 |
|-------------|-----------|:---:|------|
| `id` | string | 是 | 报账表记录 ID |
| `orderId` | string | 否 | 订单 ID |
| `reportStatus` | string | 否 | 报表状态,见 §6.1 |
| `sourceFingerprint` | string | 否 | 64 位小写十六进制 SHA-256;传给 finalize 的 `reimbursementExpectedSourceFingerprint` |
| `primaryReporterId` | string | 是 | 主报账人 ID |
| `primaryReporterName` | string | 是 | 主报账人姓名 |
| `primaryReporterRole` | string | 是 | 主报账人角色 |
| `reportVersion` | integer | 否 | 报账表结构版本 |
| `driverCollectedTailAmount` | number | 否 | 主报账人代收尾款 |
| `approvedAdvanceAmount` | number | 否 | 已审批预支金额 |
| `reportablePaidCostAmount` | number | 否 | 可报账的已付成本 |
| `reporterNetAmount` | number | 否 | 主报账人净额;决定转账日期和流水是否必填 |
| `primaryReporterCollectedAmount` | number | 否 | 主报账人代收金额 |
| `publicPrepaidAmount` | number | 否 | 公共预支金额 |
| `primaryReporterDueAmount` | number | 否 | 主报账人应报账金额 |
| `advanceOutstandingAmount` | number | 否 | 未结清预支金额 |
| `reconNetAmount` | number | 否 | 报账净额 |
| `transferDirection` | string | 否 | 转账方向,见 §6.2 |
| `transferAmount` | number | 否 | 应转账金额的绝对值 |
| `incomeLines` | array&lt;object&gt; | 否 | 主报账人代收明细,结构见下表 |
| `expenseLines` | array&lt;object&gt; | 否 | 主报账成本明细,结构见下表 |
| `advanceLines` | array&lt;object&gt; | 否 | 已审批预支明细,结构见下表 |
| `vehicleLines` | array&lt;object&gt; | 否 | 车辆独立明细;没有独立行时为 `[]` |
| `transferStatus` | string | 是 | 未完成核单时可为 `null`;终态为 `COMPLETED` |
| `transferDate` | string(date) | 是 | 转账日期,格式 `YYYY-MM-DD` |
| `transferRef` | string | 是 | 转账流水号 |
| `advanceSettledFlag` | boolean | 是 | 预支是否已处理 |
| `signedVoucher` | object | 是 | 签字凭证;结构与 finalize 的凭证一致 |
| `generatedBy` | string | 是 | 历史生成操作人 ID |
| `generatedByName` | string | 是 | 历史生成操作人姓名 |
| `generatedAt` | string(date-time) | 是 | 历史生成时间 |
| `confirmedBy` | string | 是 | 完成核单操作人 ID |
| `confirmedByName` | string | 是 | 完成核单操作人姓名 |
| `confirmedAt` | string(date-time) | 是 | 完成核单时间 |
**`incomeLines[]` 字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | string | 当前为 `DRIVER_CASH_RECEIPT` |
| `receiptId` | string | 收款记录 ID |
| `amount` | number | 收款金额 |
| `channel` | string | 收款渠道 |
| `payType` | string/null | 支付类型 |
| `collectorStaffId` | string/null | 收款人员 ID |
| `collectorName` | string/null | 收款人员姓名 |
| `collectorRole` | string/null | 收款人员角色 |
| `receivedAt` | string(date-time)/null | 收款时间 |
| `remark` | string/null | 备注 |
**`advanceLines[]` 字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | string | 当前为 `APPROVED_ADVANCE` |
| `advanceId` | string | 预支记录 ID |
| `payeeStaffId` | string/null | 收款人员 ID |
| `payeeName` | string/null | 收款人员姓名 |
| `payeeRole` | string/null | 收款人员角色 |
| `advanceType` | string/null | 预支类型 |
| `amount` | number | 已审批金额 |
| `purpose` | string/null | 用途 |
| `voucherUrl` | string/null | 预支凭证地址 |
| `status` | string | 预支状态 |
| `submittedAt` | string(date-time)/null | 提交时间 |
| `approvedAt` | string(date-time)/null | 审批时间 |
| `approvedBy` | string/null | 审批人 ID |
**`expenseLines[]` 公共字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `category` | string | 费用分类,见 §6.3 |
| `kind` | string | 明细类型,例如 `HOTEL``TICKET``MEAL``VEHICLE_FEE``STAFF:DRIVER` |
| `amount` | number | 当前行实际成本 |
| `paymentMethod` | string | 当前报账明细使用 `CASH_PAID` |
不同 `kind` 还会携带相应业务字段:
- `HOTEL``hotelAssignmentId``hotelId``roomTypeId``dayNumber``stayDate``hotelName``roomType``roomTypeName``roomCount``unitPrice``plannedCost``sourceType``sourceId``voucherUrls``remark`
- `TICKET``sourceType``scenicAssignmentId``dayNumber``dayDate``scenicName``specName``ticketCount``ticketUnitPrice``sellPrice``totalAmount``plannedCost``voucherUrls``remark`
- `MEAL``mealType``mealDate``mealName``quantity``unitPrice``voucherUrls``remark`
- `VEHICLE_FEE``sourceRecordType``sourceDetailId``serviceDate``vehicleId``vehiclePlate``vehicleModelId``vehicleModelName``driverId``driverName``startDate``endDate``dailyPrice``paymentTypeCode``paymentTypeName`
- `STAFF:*``staffRole``staffId``staffName``totalPlannedCost``voucherUrls``reimburse``settleStatus``settledDate``transferRef``detail``remark`
- `EXPENSE:*``expenseType``projectName``expenseDate``voucherUrls``remark`
- `SUBSIDY:*``subsidyType``projectName``expenseDate``voucherUrls``remark`
**错误与业务边界**
- `orderId <= 0` 返回 `400`
- 房务角色或无订单访问权限返回 `403`/对应订单访问错误。
- 未完成核单时返回当前核单事实的实时视图和当前指纹。
- 已完成核单时返回当前有效终态版本中的报账表和该版本指纹。
- 管理员反确认后再次 GET 会回到实时视图;前端必须重新取得指纹。
**典型请求**
```http
GET /v3/admin/order/1914050000000001/settlement/reports/reimbursement
Authorization: Bearer <admin-jwt>
```
无请求体。
**典型响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"id": null,
"orderId": "1914050000000001",
"reportStatus": "GENERATED",
"sourceFingerprint": "7a0e84a9f9d0cb411f9cff8d6a0d1c047726bb05b68c8e23af5629afdbdd67c1",
"primaryReporterId": "3001",
"primaryReporterName": "示例报账人",
"primaryReporterRole": "DRIVER",
"reportVersion": 1,
"driverCollectedTailAmount": 2000.00,
"approvedAdvanceAmount": 500.00,
"reportablePaidCostAmount": 1200.00,
"reporterNetAmount": 1300.00,
"primaryReporterCollectedAmount": 2000.00,
"publicPrepaidAmount": 1200.00,
"primaryReporterDueAmount": 800.00,
"advanceOutstandingAmount": 500.00,
"reconNetAmount": 1300.00,
"transferDirection": "REPORTER_TO_COMPANY",
"transferAmount": 1300.00,
"incomeLines": [],
"expenseLines": [],
"advanceLines": [],
"vehicleLines": [],
"transferStatus": null,
"transferDate": null,
"transferRef": null,
"advanceSettledFlag": null,
"signedVoucher": null,
"generatedBy": null,
"generatedByName": null,
"generatedAt": null,
"confirmedBy": null,
"confirmedByName": null,
"confirmedAt": null
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
### 3.2 查询单团核算表
- **方法与路径**`GET /v3/admin/order/{orderId}/settlement/reports/group`
- **使用场景**:展示单团核算表,并在调用 finalize 前取得最新单团指纹
- **认证**:管理后台 JWT;房务角色不可访问
- **幂等性**:幂等,只读
- **限流**:无接口级特殊限流
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` |
**请求体**
无。
**响应字段**
| `data` 字段 | JSON 类型 | 可空 | 说明 |
|-------------|-----------|:---:|------|
| `id` | string | 是 | 单团核算表记录 ID |
| `orderId` | string | 否 | 订单 ID |
| `reportStatus` | string | 否 | 报表状态,见 §6.1 |
| `sourceFingerprint` | string | 否 | 64 位小写十六进制 SHA-256;传给 finalize 的 `groupExpectedSourceFingerprint` |
| `baseOrderAmount` | number | 否 | 订单基础金额 |
| `otherIncomeAmount` | number | 否 | 其他收入金额 |
| `discountAmount` | number | 否 | 优惠金额 |
| `adjustedReceivableAmount` | number | 否 | 调整后应收金额 |
| `paidAmount` | number | 否 | 已收金额 |
| `actualRefundedAmount` | number | 否 | 实际退款金额 |
| `netRevenueAmount` | number | 否 | 净收入 |
| `netReceivedAmount` | number | 否 | 净已收 |
| `outstandingAmount` | number | 否 | 待收金额;不为 `0` 时不能 finalize |
| `hotelCost` | number | 否 | 住宿成本 |
| `ticketCost` | number | 否 | 门票/游玩项目成本 |
| `mealCost` | number | 否 | 餐食成本 |
| `vehicleCost` | number | 否 | 车辆成本 |
| `guideCost` | number | 否 | 导游/领队成本 |
| `photographerCost` | number | 否 | 摄影成本 |
| `otherExpenseCost` | number | 否 | 其他支出成本 |
| `insurancePremium` | number | 否 | 保险保费 |
| `totalCost` | number | 否 | 总成本 |
| `paidCost` | number | 否 | 已付成本 |
| `unpaidCost` | number | 否 | 未付成本 |
| `grossProfit` | number | 否 | 毛利 |
| `grossProfitRate` | number | 否 | 毛利率,小数形式 |
| `travelerCount` | integer | 否 | 出行人数 |
| `perCapitaRevenue` | number | 否 | 人均收入 |
| `perCapitaCost` | number | 否 | 人均成本 |
| `perCapitaProfit` | number | 否 | 人均利润 |
| `incomeLines` | array&lt;object&gt; | 否 | 收入汇总行 |
| `costCategories` | array&lt;object&gt; | 否 | 成本分类汇总 |
| `generatedBy` | string | 是 | 历史生成操作人 ID |
| `generatedByName` | string | 是 | 历史生成操作人姓名 |
| `generatedAt` | string(date-time) | 是 | 历史生成时间 |
| `confirmedBy` | string | 是 | 完成核单操作人 ID |
| `confirmedByName` | string | 是 | 完成核单操作人姓名 |
| `confirmedAt` | string(date-time) | 是 | 完成核单时间 |
**`incomeLines[]` 字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | string | `BASE_ORDER``OTHER_INCOME``DISCOUNT``ACTUAL_REFUND` |
| `amount` | number | 金额;优惠和实际退款以负数返回 |
**`costCategories[]` 字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `category` | string | `HOTEL``TICKET``MEAL``VEHICLE``GUIDE``PHOTOGRAPHER``OTHER_EXPENSE``INSURANCE` |
| `amount` | number | 分类成本 |
**错误与业务边界**
- `orderId <= 0` 返回 `400`
- 房务角色或无订单访问权限返回 `403`/对应订单访问错误。
- 未完成核单时返回实时视图;已完成核单时返回当前有效终态版本。
- 管理员反确认后,下一次 GET 会生成新的实时结果和指纹。
**典型请求**
```http
GET /v3/admin/order/1914050000000001/settlement/reports/group
Authorization: Bearer <admin-jwt>
```
无请求体。
**典型响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"id": null,
"orderId": "1914050000000001",
"reportStatus": "GENERATED",
"sourceFingerprint": "651cb6708a49f169e2ccb1b455267def9cc1a69d06935f9d9eeb41919897e0fb",
"baseOrderAmount": 24800.00,
"otherIncomeAmount": 500.00,
"discountAmount": 300.00,
"adjustedReceivableAmount": 25000.00,
"paidAmount": 25000.00,
"actualRefundedAmount": 0.00,
"netRevenueAmount": 25000.00,
"netReceivedAmount": 25000.00,
"outstandingAmount": 0.00,
"hotelCost": 4280.00,
"ticketCost": 3680.00,
"mealCost": 860.00,
"vehicleCost": 5200.00,
"guideCost": 800.00,
"photographerCost": 600.00,
"otherExpenseCost": 1200.00,
"insurancePremium": 180.00,
"totalCost": 16800.00,
"paidCost": 16800.00,
"unpaidCost": 0.00,
"grossProfit": 8200.00,
"grossProfitRate": 0.328,
"travelerCount": 5,
"perCapitaRevenue": 5000.00,
"perCapitaCost": 3360.00,
"perCapitaProfit": 1640.00,
"incomeLines": [
{"type": "BASE_ORDER", "amount": 24800.00},
{"type": "OTHER_INCOME", "amount": 500.00},
{"type": "DISCOUNT", "amount": -300.00},
{"type": "ACTUAL_REFUND", "amount": 0.00}
],
"costCategories": [
{"category": "HOTEL", "amount": 4280.00},
{"category": "TICKET", "amount": 3680.00},
{"category": "MEAL", "amount": 860.00},
{"category": "VEHICLE", "amount": 5200.00},
{"category": "GUIDE", "amount": 800.00},
{"category": "PHOTOGRAPHER", "amount": 600.00},
{"category": "OTHER_EXPENSE", "amount": 1200.00},
{"category": "INSURANCE", "amount": 180.00}
],
"generatedBy": null,
"generatedByName": null,
"generatedAt": null,
"confirmedBy": null,
"confirmedByName": null,
"confirmedAt": null
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
### 3.3 完成核单
- **方法与路径**`POST /v3/admin/order/{orderId}/settlement/finalize`
- **使用场景**:两张报表核对完成后,一次提交双指纹和主报账凭据
- **认证**:管理后台 JWT;房务角色不可访问
- **幂等性**:严格幂等,比较双指纹、规范化后的凭据和 `remark`
- **限流**:无接口级特殊限流
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | String/Long | 是 | 订单 ID,必须大于 `0` |
**请求体字段**
| 字段 | JSON 类型 | 必填 | 校验与规范化 |
|------|-----------|:---:|--------------|
| `remark` | string/null | 否 | 最长 500;去除首尾空格,空串按 `null` 比较 |
| `reimbursementExpectedSourceFingerprint` | string | 是 | 必须等于主报账 GET 返回的 64 位小写十六进制 `sourceFingerprint` |
| `groupExpectedSourceFingerprint` | string | 是 | 必须等于单团 GET 返回的 64 位小写十六进制 `sourceFingerprint` |
| `reimbursementConfirmation` | object | 是 | 主报账转账、预支和签字凭据 |
| `reimbursementConfirmation.transferDate` | string(date)/null | 条件必填 | `reporterNetAmount != 0` 时必填;净额为 `0` 时可为 `null` |
| `reimbursementConfirmation.transferRef` | string/null | 条件必填 | 去除首尾空格后最长 128;净额非 `0` 时长度必须为 1128 |
| `reimbursementConfirmation.advanceSettledFlag` | boolean | 是 | 必须明确传值,`false` 合法 |
| `reimbursementConfirmation.signedVoucher` | object | 是 | 缺失返回 `400` |
| `reimbursementConfirmation.signedVoucher.files` | array&lt;object&gt; | 业务必填 | 19 项;为 `null`、空数组、超过 9 项或含 `null` 项返回 `584317` |
| `reimbursementConfirmation.signedVoucher.files[].url` | string | 业务必填 | 去除首尾空格后长度 11024;不符合返回 `584317` |
| `reimbursementConfirmation.signedVoucher.files[].name` | string/null | 否 | 去除首尾空格;空串归一化为 `null`;非空最长 255 |
| `reimbursementConfirmation.signedVoucher.note` | string/null | 否 | 去除首尾空格;空串归一化为 `null`;非空最长 500 |
`transferStatus` **不得提交**。finalize 成功后,报账终态中的 `transferStatus` 固定为 `COMPLETED`
签字凭证文件按规范化后的 `url``name` 升序稳定保存。不得依赖请求数组原顺序进行严格幂等判断。
**响应字段**
| `data` 字段 | JSON 类型 | 说明 |
|-------------|-----------|------|
| `summaryId` | string | 核单汇总 ID |
| `finalSnapshotId` | string | 核单终态快照 ID |
| `finalSnapshotVersionNo` | integer | 终态版本号;首次为 1,反确认后再次 finalize 为上一版本 + 1 |
| `finalSnapshotStatus` | string | 成功固定为 `FINALIZED` |
| `orderId` | string | 订单 ID |
| `settledAt` | string(date-time) | ISO-8601 核单完成时间 |
| `totalAmount` | string | 订单总金额快照 |
| `paidAmount` | string | 已付金额快照 |
| `balanceAmount` | string | 尾款金额快照 |
| `roomCost` | string | 住宿实际成本 |
| `ticketCost` | string | 门票实际成本 |
| `staffCost` | string | 人员费用实际成本 |
| `subsidyCost` | string | 补助实际成本 |
| `mealCost` | string | 餐食实际成本 |
| `vehicleCost` | string | 车辆成本 |
| `otherExpenseCost` | string | 其他支出实际成本 |
| `insurancePremium` | string | 保险实际保费 |
| `totalActualCost` | string | 总实际成本 |
| `driverTransferAmount` | string | 给司机/主报账人转回金额 |
| `profitAmount` | string | 公司毛利 |
| `profitRate` | number | 毛利率;订单总金额为 0 时为 0 |
| `orderStatusAfter` | string | 成功后为 `待财务复核` |
| `mqTriggered` | boolean | 当前固定为 `false` |
| `warnings` | array&lt;string&gt; | 软预警列表;无预警为 `[]` |
**错误与业务边界**
- 缺 body、非法 JSON、`remark` 超长、双指纹格式错误,或缺少 `reimbursementConfirmation``advanceSettledFlag``signedVoucher`:返回 `400`
- 双指纹任一与当前冻结事实不一致:返回 `584315`,须重新 GET 两张报表。
- `transferRef` 条件不满足或超过 128,凭证 `files`/文件项/`url` 无效,或 `name`/`note` 超长:返回 `584317`
- 单团核算的 `outstandingAmount != 0`:返回 `584082`,不能完成核单。
- 完全相同的终态请求重试返回原 `summaryId``finalSnapshotId` 和版本号,不产生新版本。
- 已有当前终态时,双指纹、规范化凭据或 `remark` 任一不同:返回 `584316`
- 任一失败不留下部分完成结果。
**典型请求:净报账金额非 0**
```http
POST /v3/admin/order/1914050000000001/settlement/finalize
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{
"remark": "主报账人与单团核算均已核对",
"reimbursementExpectedSourceFingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"groupExpectedSourceFingerprint": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"reimbursementConfirmation": {
"transferDate": "2026-07-29",
"transferRef": "FT202607290001",
"advanceSettledFlag": false,
"signedVoucher": {
"files": [
{
"name": "司机签字报账单.pdf",
"url": "https://oss.example.com/settlement/driver-signed-20260729.pdf"
}
],
"note": "司机现场签字后上传"
}
}
}
```
**典型响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"summaryId": "9600000000001",
"finalSnapshotId": "9600000000002",
"finalSnapshotVersionNo": 1,
"finalSnapshotStatus": "FINALIZED",
"orderId": "1914050000000001",
"settledAt": "2026-07-29T10:30:25",
"totalAmount": "24800.00",
"paidAmount": "24800.00",
"balanceAmount": "0.00",
"roomCost": "4280.00",
"ticketCost": "3680.00",
"staffCost": "7000.00",
"subsidyCost": "720.00",
"mealCost": "860.00",
"vehicleCost": "5200.00",
"otherExpenseCost": "1200.00",
"insurancePremium": "180.00",
"totalActualCost": "23120.00",
"driverTransferAmount": "22940.00",
"profitAmount": "1680.00",
"profitRate": 0.0677,
"orderStatusAfter": "待财务复核",
"mqTriggered": false,
"warnings": []
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
**边界请求:`reporterNetAmount = 0`**
```http
POST /v3/admin/order/1914050000000002/settlement/finalize
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{
"remark": null,
"reimbursementExpectedSourceFingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"groupExpectedSourceFingerprint": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"reimbursementConfirmation": {
"transferDate": null,
"transferRef": null,
"advanceSettledFlag": false,
"signedVoucher": {
"files": [
{
"name": null,
"url": "https://oss.example.com/settlement/zero-net-signed.jpg"
}
],
"note": null
}
}
}
```
**边界响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"summaryId": "9600000000011",
"finalSnapshotId": "9600000000012",
"finalSnapshotVersionNo": 1,
"finalSnapshotStatus": "FINALIZED",
"orderId": "1914050000000002",
"settledAt": "2026-07-29T10:35:00",
"totalAmount": "0.00",
"paidAmount": "0.00",
"balanceAmount": "0.00",
"roomCost": "0.00",
"ticketCost": "0.00",
"staffCost": "0.00",
"subsidyCost": "0.00",
"mealCost": "0.00",
"vehicleCost": "0.00",
"otherExpenseCost": "0.00",
"insurancePremium": "0.00",
"totalActualCost": "0.00",
"driverTransferAmount": "0.00",
"profitAmount": "0.00",
"profitRate": 0,
"orderStatusAfter": "待财务复核",
"mqTriggered": false,
"warnings": []
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
**异常请求:凭证包含空 URL**
```http
POST /v3/admin/order/1914050000000001/settlement/finalize
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{
"reimbursementExpectedSourceFingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"groupExpectedSourceFingerprint": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"reimbursementConfirmation": {
"transferDate": "2026-07-29",
"transferRef": "FT202607290001",
"advanceSettledFlag": true,
"signedVoucher": {
"files": [
{"name": "签字单.pdf", "url": " "}
]
}
}
}
```
**异常响应**
```json
{
"code": 584317,
"message": "当前报告状态不允许执行该操作",
"data": null,
"traceId": "a1b2c3d4-e5f6-7890",
"success": false
}
```
**异常请求:缺少 `signedVoucher`**
```http
POST /v3/admin/order/1914050000000001/settlement/finalize
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{
"reimbursementExpectedSourceFingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"groupExpectedSourceFingerprint": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"reimbursementConfirmation": {
"transferDate": "2026-07-29",
"transferRef": "FT202607290001",
"advanceSettledFlag": true
}
}
```
**异常响应**
```json
{
"code": 400,
"message": "参数校验失败",
"data": null,
"traceId": "a1b2c3d4-e5f6-7890",
"success": false
}
```
### 3.4 已删除:确认主报账表
- **原方法与路径**`POST /v3/admin/order/{orderId}/settlement/reports/reimbursement/confirm`
- **当前契约**:接口已删除,无有效请求体或成功响应。
- **前端动作**删除请求封装、按钮、loading、重试和错误忽略逻辑。
**请求示例**
```http
POST /v3/admin/order/1914050000000001/settlement/reports/reimbursement/confirm
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{}
```
**响应示例**
```json
{
"code": 404,
"message": "请求地址不存在",
"data": null,
"success": false
}
```
### 3.5 已删除:确认单团核算表
- **原方法与路径**`POST /v3/admin/order/{orderId}/settlement/reports/group/confirm`
- **当前契约**:接口已删除,无有效请求体或成功响应。
- **前端动作**删除请求封装、按钮、loading、重试和错误忽略逻辑。
**请求示例**
```http
POST /v3/admin/order/1914050000000001/settlement/reports/group/confirm
Authorization: Bearer <admin-jwt>
Content-Type: application/json
{}
```
**响应示例**
```json
{
"code": 404,
"message": "请求地址不存在",
"data": null,
"success": false
}
```
## 4. 接口入参汇总
| 接口 | 入参 |
|------|------|
| 主报账 GET | 路径参数 `orderId`;无请求体 |
| 单团 GET | 路径参数 `orderId`;无请求体 |
| finalize | 路径参数 `orderId`;请求体必须包含两个指纹及 `reimbursementConfirmation` |
| 两个旧 confirm | 已删除,无有效入参 |
双指纹映射必须严格如下:
| 来源 | finalize 字段 |
|------|---------------|
| 主报账 GET 的 `data.sourceFingerprint` | `reimbursementExpectedSourceFingerprint` |
| 单团 GET 的 `data.sourceFingerprint` | `groupExpectedSourceFingerprint` |
## 5. 出参汇总
- 两张 GET 均返回 `Result<报表对象>`,其中 `sourceFingerprint` 是 finalize 的提交凭据。
- finalize 返回 `Result<SettlementSubmitRespVO>`,完整字段见 §3.3。
- 两个旧 confirm 不再返回业务成功响应,只会命中不存在的路由。
- 金额序列化以各字段表和示例为准finalize 的金额字段为字符串,两张 GET 的金额字段为 JSON number。
## 6. 枚举 / 数据字典
### 6.1 `reportStatus`
**所属字段**:两张报表响应 `reportStatus` | **类型**string
| 值 | 中文 | 说明 |
|----|------|------|
| `GENERATED` | 实时结果 | 当前不存在有效终态,按当前核单事实计算 |
| `CONFIRMED` | 已固化 | 返回当前有效终态版本中的报表 |
| `STALE` | 历史过期 | 兼容历史报表状态,不用于当前 finalize |
### 6.2 `transferDirection`
**所属字段**:主报账响应 `transferDirection` | **类型**string
| 值 | 中文 | 说明 |
|----|------|------|
| `REPORTER_TO_COMPANY` | 报账人转公司 | `reporterNetAmount > 0` |
| `COMPANY_TO_REPORTER` | 公司转报账人 | `reporterNetAmount < 0` |
| `BALANCED` | 已平衡 | `reporterNetAmount = 0` |
### 6.3 `category`
**所属字段**`expenseLines[].category``costCategories[].category` | **类型**string
| 值 | 中文 | 说明 |
|----|------|------|
| `HOTEL` | 住宿 | 住宿成本 |
| `TICKET` | 门票/游玩项目 | 门票及游玩成本 |
| `MEAL` | 餐食 | 餐食成本 |
| `VEHICLE` | 车辆 | 车辆成本 |
| `GUIDE` | 导游/领队 | 导游及领队成本 |
| `PHOTOGRAPHER` | 摄影 | 摄影成本 |
| `OTHER_EXPENSE` | 其他支出 | 其他支出成本 |
| `INSURANCE` | 保险 | 保险保费 |
### 6.4 `finalSnapshotStatus`
**所属字段**finalize 响应 `finalSnapshotStatus` | **类型**string
| 值 | 中文 | 说明 |
|----|------|------|
| `FINALIZED` | 已完成核单 | 当前终态版本有效 |
### 6.5 `transferStatus`
**所属字段**:主报账响应 `transferStatus` | **类型**string/null
| 值 | 中文 | 说明 |
|----|------|------|
| `COMPLETED` | 转账凭据已随核单固化 | finalize 成功后固定值 |
| `null` | 尚未固化 | 实时报表可为空 |
`transferStatus` 只出现在响应中,不是 finalize 入参。
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 请求/参数校验失败 | `orderId <= 0`、缺请求体、非法 JSON、双指纹格式错误、缺 `reimbursementConfirmation`/`advanceSettledFlag`/`signedVoucher``remark` 超长 |
| `403` | 无访问权限 | 房务角色或无权访问当前订单 |
| `404` | 路由不存在 | 调用两个已删除的报表 confirm 接口 |
| `584082` | 存在待收尾款 | 单团核算 `outstandingAmount != 0` |
| `584100` | 车辆费用暂时不可用 | 报表查询或 finalize 当前无法取得可核单车辆费用 |
| `584101` | 车辆事实未完成 | 存在未完结派车或未确认车辆费用 |
| `584102` | 缺少车辆费用 | 有用车需求但没有可核单车辆费用 |
| `584315` | 核单来源数据已变化 | 车辆候选与冻结事实不一致,或任一双指纹过期 |
| `584316` | 并发或严格幂等冲突 | 终态重试请求不同、并发完成/反确认冲突 |
| `584317` | 转账条件或签字凭证不合法 | 净额非 0 缺日期/流水、流水超长、files/文件项/url 无效、name/note 超长 |
| `584320` | 核单明细未准备好 | 当前分类数据不能用于报账或 finalize |
| `584321` | 缺少当前终态 | 后续财务复核缺少 current `FINALIZED` 终态 |
| `584325` | 双指纹兜底校验失败 | finalize 发现双指纹不完整或不合法 |
| `584326` | 终态组合不一致 | 当前终态、关联汇总或订单终态不匹配 |
## 8. 示例索引
| 场景 | 位置 |
|------|------|
| 主报账 GET 典型请求与响应 | §3.1 |
| 单团 GET 典型请求与响应 | §3.2 |
| finalize 净额非 0 典型成功 | §3.3 |
| finalize 净额为 0 合法边界 | §3.3 |
| finalize 凭证 URL 非法返回 584317 | §3.3 |
| finalize 缺 `signedVoucher` 返回 400 | §3.3 |
| 两个旧 confirm 返回 404 | §3.4、§3.5 |
## 9. 业务边界
- 必须先分别 GET 两张报表,再把两个 `sourceFingerprint` 一一映射到 finalize;不能复用旧指纹、互换字段或只传一个。
- 任一核单事实变化后,旧双指纹都会失效;收到 `584315` 后必须重新 GET 两张表。
- `outstandingAmount` 必须为 `0` 才能 finalize。
- `reporterNetAmount != 0` 时,`transferDate` 和非空 `transferRef` 同时必填;净额为 `0` 时二者可为 `null`
- `advanceSettledFlag=false` 是有效业务值,不等同于缺失。
- `signedVoucher` 始终必填,且 `files` 必须有 19 个合法文件项。
- 完全相同请求重试严格幂等;任何双指纹、规范化凭据或 `remark` 差异均返回 `584316`
- 管理员反确认使当前终态失效后,两张 GET 重新返回实时结果;再次 finalize 必须使用新双指纹,成功响应的 `finalSnapshotVersionNo` 为上一版本 + 1。
- finalize 成功后订单进入“待财务复核”。既有财务复核接口 `POST /v3/admin/order/{orderId}/settlement/confirm` 的请求/响应结构未在本次变更:请求仅含可选 `confirmRemark`;当前没有独立财务角色校验;成功 `data``orderId``settlementStatus=COMPLETED``settledAt``flowStatus=SETTLED`。其复核前提为当前有效 `FINALIZED` 终态及其关联汇总,旧报表 confirm 状态不参与判断。
## 10. 修改前后对比
### 10.1 字段级对比
| 接口/字段 | 修改前或错误说明 | 当前正确契约 |
|-----------|------------------|--------------|
| finalize 双指纹 | #5342 通知误写为删除 | 两个字段均必填 |
| `reimbursementExpectedSourceFingerprint` | 误写为不再回传 | 来自主报账 GET 的 `sourceFingerprint` |
| `groupExpectedSourceFingerprint` | 误写为不再回传 | 来自单团 GET 的 `sourceFingerprint` |
| `reimbursementConfirmation` | #5342 把内部字段错误提升到 finalize 顶层 | 必填嵌套对象 |
| `transferDate` | 误写为 finalize 顶层 | 位于 `reimbursementConfirmation` |
| `transferRef` | 误写为 finalize 顶层 | 位于 `reimbursementConfirmation`,trim 后最长 128 |
| `advanceSettledFlag` | 误写为 finalize 顶层 | 位于 `reimbursementConfirmation`,必填 boolean |
| `signedVoucher` | 误写为 finalize 顶层 | 位于 `reimbursementConfirmation`,必填 object |
| `transferStatus` | 可能沿用旧 confirm 传值 | finalize 不接收,成功后固定为 `COMPLETED` |
### 10.2 行为级对比
| 行为 | 修改前 | 当前 |
|------|--------|------|
| 主报账确认 | 独立 POST confirm | 接口删除,由 finalize 一次完成 |
| 单团确认 | 独立 POST confirm | 接口删除,由 finalize 一次完成 |
| finalize 前的数据校验 | 分散在两个 confirm | 两张 GET 取双指纹,finalize 一次校验 |
| 重复 finalize | 旧流程语义不明确 | 完全相同返回原结果,任一差异返回 `584316` |
| 反确认后再次核单 | 可能沿用旧报表结果 | 重新 GET 新指纹,再 finalize 生成版本号 + 1 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:是。两个 POST confirm 已删除,finalize 的双指纹及嵌套凭据均为必填。
- **前端是否必须同步上线**:是。按 #5342 错误契约提交会因缺双指纹或缺 `reimbursementConfirmation` 返回 `400`/业务错误。
- **查询兼容性**:两张 GET 的字段结构保持,`sourceFingerprint` 的用途明确为 finalize 必填凭据。
### 11.2 回滚说明
- 前后端必须使用同一版核单流程;不能混用“独立 confirm”和“finalize 双指纹”两套调用顺序。
- 若后端契约回滚,前端也需同步恢复对应请求模型与调用链,不能只单独回滚一端。
## 12. 注意事项
- 删除两个报表确认按钮及对应请求、loading、重试、错误忽略代码。
- 保留两个 GET 返回的 `sourceFingerprint`,并在点击完成核单前保存当前两份值。
- finalize 请求模型必须新增必填 `reimbursementConfirmation`,其余凭据字段不得放在顶层。
- 不要发送 `transferStatus`;页面在 finalize 成功后按响应/重新 GET 展示终态。
- 不要继续沿用 #5342 通知中的“删除双指纹”“finalize 顶层凭据字段”实现。
- 对 `584315` 进行刷新两张报表后重试;对 `584316` 不要静默覆盖终态。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5343](https://git.1814.love:8443/wx/HL/issues/5343)
- **PR**: [#5347](https://git.1814.love:8443/wx/HL/pulls/5347)
- **Merge commit**: [a892a6b56a](https://git.1814.love:8443/wx/HL/commit/a892a6b56a2c3c0c2e4e345156096ac1ac5750c0)
### 13.2 联系人
- **后端负责人**: @yst
- **消费端**: v3 管理后台

查看文件

@ -1,911 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5356"
title: "核单八类来源确认状态与车辆 Step3 接口统一"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "Pi"
frontend_ref: "9b9865a50304b1acc55ab5e4f94bb7dfe52293c0"
target_release: ""
verified_at: "2026-07-30"
status_note: "管理后台已统一八类核单来源与逐行确认状态,车辆改用 Order Step3 GET/PUT 并携带 version、保护 FLEET 权威字段;pnpm checkpoint 全量通过,业务提交 9b9865a50304b1acc55ab5e4f94bb7dfe52293c0 已推送至 origin/v2.1。后端车辆 DTO 正向数据与 Full E2E 仍受测试订单无可核单车辆费用限制。"
updated_at: "2026-07-30"
base: "dev-v3"
---
# ⚠️【修改接口·管理后台】核单八类来源确认状态与车辆 Step3 接口统一 (#5356)
> **PR**: #5362 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-30 15:46
## 1. 接口背景
核单页面需要用同一套规则识别“手工行”和“系统来源行”,并逐行完成确认。此前各 Tab 的 `sourceType`、确认状态和车辆费用入口不一致,车辆数据还残留过已下线接口的字段口径。本次统一八类核单来源与逐行确认语义,并新增 Order 侧车辆 Step3 草稿查询、全量保存接口。
管理后台应以本文列出的 Order 侧接口为准;已删除的
`GET /v3/admin/order/:orderId/settlement/vehicle-fees`
`POST /v3/admin/order/:orderId/settlement/vehicle-fees/confirm`
继续保持下线,不得恢复调用。
## 变更接口2. 变更清单)
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | Step 1 查询住宿核单明细 | GET | `/v3/admin/order/:orderId/settlement/step1` | 修改 | 来源值统一;系统派生行初始为未确认;ID 按字符串返回 |
| 2 | Step 1 保存住宿核单明细 | PUT | `/v3/admin/order/:orderId/settlement/step1` | 修改 | 支持逐行 `UNCONFIRMED/CONFIRMED`;手工新行必须先未确认 |
| 3 | Step 2 查询门票核单明细 | GET | `/v3/admin/order/:orderId/settlement/step2` | 修改 | 响应新增逐行确认状态;ID 按字符串返回 |
| 4 | Step 2 保存门票核单明细 | PUT | `/v3/admin/order/:orderId/settlement/step2` | 修改 | 请求新增逐行确认状态;手工新行必须先未确认 |
| 5 | 查询车辆核单草稿 | GET | `/v3/admin/order/:orderId/settlement/step3/vehicles` | 新增 | Order 侧车辆 Step3 唯一查询入口 |
| 6 | 全量保存车辆核单草稿 | PUT | `/v3/admin/order/:orderId/settlement/step3/vehicles` | 新增 | 带 `version` 全量保存,支持车务行确认和手工行维护 |
| 7 | 查询领队人员费用 | GET | `/v3/admin/order/:orderId/settlement/staff-fees/leaders` | 修改 | 新增来源与确认状态名称 |
| 8 | 保存领队人员费用 | PUT | `/v3/admin/order/:orderId/settlement/staff-fees/leaders` | 修改 | 请求行新增 `id/sourceType/settlementConfirmStatus` |
| 9 | 查询司机人员费用 | GET | `/v3/admin/order/:orderId/settlement/staff-fees/drivers` | 修改 | 新增来源与确认状态名称 |
| 10 | 保存司机人员费用 | PUT | `/v3/admin/order/:orderId/settlement/staff-fees/drivers` | 修改 | 请求行新增 `id/sourceType/settlementConfirmStatus` |
| 11 | 查询导游人员费用 | GET | `/v3/admin/order/:orderId/settlement/staff-fees/guides` | 修改 | 新增来源与确认状态名称 |
| 12 | 保存导游人员费用 | PUT | `/v3/admin/order/:orderId/settlement/staff-fees/guides` | 修改 | 请求行新增 `id/sourceType/settlementConfirmStatus` |
| 13 | 查询摄影师人员费用 | GET | `/v3/admin/order/:orderId/settlement/staff-fees/photographers` | 修改 | 新增来源与确认状态名称 |
| 14 | 保存摄影师人员费用 | PUT | `/v3/admin/order/:orderId/settlement/staff-fees/photographers` | 修改 | 请求行新增 `id/sourceType/settlementConfirmStatus` |
| 15 | 查询其他人员费用 | GET | `/v3/admin/order/:orderId/settlement/staff-fees/others` | 修改 | 新增来源与确认状态名称 |
| 16 | 保存其他人员费用 | PUT | `/v3/admin/order/:orderId/settlement/staff-fees/others` | 修改 | 请求行新增 `id/sourceType/settlementConfirmStatus` |
| 17 | 查询餐食费用 | GET | `/v3/admin/order/:orderId/settlement/meals` | 修改 | 响应新增来源、来源名称、确认状态名称 |
| 18 | 新增餐食费用 | POST | `/v3/admin/order/:orderId/settlement/meals` | 修改 | 请求新增来源和确认状态;新行必须先未确认 |
| 19 | 修改餐食费用 | PUT | `/v3/admin/order/:orderId/settlement/meals/:settlementId` | 修改 | 可把已存在未确认行保存为已确认 |
| 20 | 查询其他支出 | GET | `/v3/admin/order/:orderId/settlement/other-expenses` | 修改 | 响应新增来源、来源名称、确认状态名称 |
| 21 | 新增其他支出 | POST | `/v3/admin/order/:orderId/settlement/other-expenses` | 修改 | 手工新行必须先未确认 |
| 22 | 修改其他支出 | PUT | `/v3/admin/order/:orderId/settlement/other-expenses/:settlementId` | 修改 | 可把已存在未确认行保存为已确认 |
| 23 | 查询其他收入 | GET | `/v3/admin/order/:orderId/settlement/other-incomes` | 修改 | `sourceType` 从内部来源值改为 `MANUAL/SYSTEM` |
| 24 | 新增其他收入 | POST | `/v3/admin/order/:orderId/settlement/other-incomes` | 修改 | 手工新行必须先未确认 |
| 25 | 修改其他收入 | PUT | `/v3/admin/order/:orderId/settlement/other-incomes/:incomeId` | 修改 | 可把已存在未确认行保存为已确认 |
| 26 | 完成核单 | POST | `/v3/admin/order/:orderId/settlement/finalize` | 修改 | 只接受八类来源同步完成且所有非空明细均已确认的数据 |
## 3. 接口详情
以下接口均需登录态 JWT 和现有订单查看/核单权限;无接口级特殊限流。GET 为只读幂等;PUT 为全量替换幂等;POST 新增其他收入使用 `requestId` 保证同订单幂等。
### 3.1 住宿 Step 1
**接口**
| 方法 | 路径 | 使用场景 |
|---|---|---|
| GET | `/v3/admin/order/:orderId/settlement/step1` | 打开住宿 Tab、刷新系统配房来源 |
| PUT | `/v3/admin/order/:orderId/settlement/step1` | 全量保存住宿行及逐行确认状态 |
**路径参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `orderId` | String(Long) | 是 | 订单 ID,必须大于 0 |
**PUT 请求体**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | HotelItem[] | 是 | 全量数组;缺少的手工现存行按删除处理 |
| `items[].id` | String(Long) | 更新时是 | 已保存行 ID;新行为空 |
| `items[].hotelAssignmentId` | String(Long) | 否 | 配房来源行 ID;手工行为空 |
| `items[].hotelId` | String(Long) | 否 | 酒店 ID |
| `items[].roomTypeId` | String(Long) | 否 | 房型 ID |
| `items[].stayDate` | Date | 是 | `yyyy-MM-dd` |
| `items[].hotelName` | String | 是 | 最长 200 字符 |
| `items[].roomType` | String | 否 | 房型摘要,最长 64 字符 |
| `items[].roomTypeName` | String | 否 | 房型/规格名称,最长 128 字符 |
| `items[].roomCount` | Integer | 是 | 总间数 |
| `items[].unitPrice` | Decimal | 否 | 核算单价,必须大于等于 0 |
| `items[].plannedCost` | Decimal | 是 | 计划成本,必须大于等于 0 |
| `items[].actualCost` | Decimal | 是 | 实际成本,必须大于等于 0 |
| `items[].paymentMethod` | String | 条件必填 | `SIGNED/COMPANY_PAID/CASH_PAID`;手工行必填 |
| `items[].settleType` | String | 否 | `cash/sign/company`,兼容配房来源付款口径 |
| `items[].sourceType` | String | 是 | `HOUSE_ASSIGNMENT/MANUAL/SYSTEM``TEMPLATE` 仅兼容旧入参 |
| `items[].sourceId` | String(Long) | 否 | 系统来源业务 ID |
| `items[].settlementConfirmStatus` | String | 是 | `UNCONFIRMED/CONFIRMED` |
| `items[].remark` | String | 否 | 最长 500 字符 |
| `items[].voucherUrls` | String[] | 否 | 凭证 URL |
**GET 响应 `data[]`**
除上述行字段外,还返回:
| 字段 | 类型 | 说明 |
|---|---|---|
| `paymentMethodName` | String | 付款方式名称 |
| `sourceTypeName` | String | 来源名称:配房结果/手工/系统 |
| `settlementConfirmStatusName` | String | 未确认/已确认 |
**PUT 响应 `data`**
| 字段 | 类型 | 说明 |
|---|---|---|
| `addedIds` | Long[] | 新增行 ID |
| `updatedIds` | Long[] | 更新行 ID |
| `deletedIds` | Long[] | 删除行 ID |
| `totalActualCost` | String(Decimal) | 保存后住宿实际成本合计 |
**业务边界**
- 配房来源行首次进入草稿返回 `UNCONFIRMED`;带已有 `id` 保存时可改为 `CONFIRMED`
- 手工新行 `id=null` 时只允许 `UNCONFIRMED`;保存取得 ID 后,下一次 PUT 才可改为 `CONFIRMED`
- `TEMPLATE` 只兼容旧请求,响应统一为 `SYSTEM`
### 3.2 门票 Step 2
**接口**
| 方法 | 路径 | 使用场景 |
|---|---|---|
| GET | `/v3/admin/order/:orderId/settlement/step2` | 打开门票/游玩项目 Tab、刷新行程来源 |
| PUT | `/v3/admin/order/:orderId/settlement/step2` | 全量保存门票行及逐行确认状态 |
**PUT 请求体**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | TicketItem[] | 是 | 全量数组 |
| `items[].id` | String(Long) | 更新时是 | 已保存行 ID;新行为空 |
| `items[].sourceType` | String | 是 | `SCENIC_ASSIGNMENT/ACTIVITY_ASSIGNMENT/MANUAL``CUSTOM_ASSIGNMENT` 仅兼容旧入参 |
| `items[].scenicAssignmentId` | String(Long) | 系统行是 | 景区或活动来源 ID |
| `items[].dayNumber` | Integer | 否 | 行程第几天,响应派生 |
| `items[].dayDate` | Date | 是 | 行程日 |
| `items[].scenicName` | String | 是 | 项目名,最长 200 字符 |
| `items[].specName` | String | 否 | 票型/规格,最长 128 字符 |
| `items[].ticketCount` | Integer | 是 | 实际购票数量,可为 0 |
| `items[].ticketUnitPrice` | Decimal | 否 | 参考单价 |
| `items[].sellPrice` | Decimal | 否 | 客户成交单价,必须大于等于 0 |
| `items[].totalAmount` | Decimal | 否 | 客户成交小计,必须大于等于 0 |
| `items[].plannedCost` | Decimal | 是 | 计划成本,必须大于等于 0 |
| `items[].actualCost` | Decimal | 是 | 实际成本,必须大于等于 0 |
| `items[].paymentMethod` | String | 否 | `SIGNED/COMPANY_PAID/CASH_PAID` |
| `items[].voucherUrls` | String[] | 否 | 凭证 URL |
| `items[].remark` | String | 否 | 最长 500 字符 |
| `items[].settlementConfirmStatus` | String | 是 | `UNCONFIRMED/CONFIRMED` |
**GET 响应 `data[]`**
返回完整 `TicketItem`,并增加:
| 字段 | 类型 | 说明 |
|---|---|---|
| `sourceTypeName` | String | 景区/游玩项目/手工 |
| `paymentMethodName` | String | 付款方式名称 |
| `settlementConfirmStatusName` | String | 未确认/已确认 |
**PUT 响应**与住宿 Step 1 相同:`addedIds/updatedIds/deletedIds/totalActualCost`
**业务边界**
- 系统来源首次同步为 `UNCONFIRMED`,来源事实变化后会重新变为 `UNCONFIRMED`
- 手工新行必须先保存为 `UNCONFIRMED`,已有 ID 后可保存为 `CONFIRMED`
- 响应不再返回 `CUSTOM_ASSIGNMENT`,历史自定义值统一返回 `MANUAL`
### 3.3 车辆 Step 3
**接口**
| 方法 | 路径 | 使用场景 |
|---|---|---|
| GET | `/v3/admin/order/:orderId/settlement/step3/vehicles` | 查询 Order 侧车辆核单草稿 |
| PUT | `/v3/admin/order/:orderId/settlement/step3/vehicles` | 带版本全量保存车辆行 |
**PUT 请求体**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `version` | Long | 是 | GET 返回的草稿版本,最小 0 |
| `items` | VehicleItem[] | 是 | 全量明细;所有现存 `FLEET` 行必须原样带回 |
| `items[].id` | String(Long) | FLEET/更新时是 | 行 ID;手工新行为空 |
| `items[].sourceType` | String | 是 | `FLEET/MANUAL` |
| `items[].serviceDate` | Date | 是 | 服务日期 |
| `items[].vehicleId` | String(Long) | 否 | 车辆 ID |
| `items[].vehiclePlate` | String | 否 | 车牌,最长 64 字符 |
| `items[].vehicleModelId` | String(Long) | 否 | 车型 ID |
| `items[].vehicleModelName` | String | 否 | 车型名,最长 128 字符 |
| `items[].driverId` | String(Long) | 否 | 司机 ID |
| `items[].driverName` | String | 否 | 司机名,最长 64 字符 |
| `items[].amount` | Decimal | 是 | 金额,010 位整数、2 位小数 |
| `items[].paymentMethod` | String | 是 | `CASH_PAID/SIGNED/COMPANY_PAID` |
| `items[].settlementConfirmStatus` | String | 是 | `UNCONFIRMED/CONFIRMED` |
| `items[].remark` | String | 否 | 最长 500 字符 |
| `items[].voucherUrls` | String[] | 否 | 最多 9 个 http/https URL,单个最长 1024 字符 |
未知字段会被拒绝。
**GET/PUT 响应 `data`**
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderId` | String(Long) | 订单 ID |
| `version` | Long | 当前草稿版本 |
| `totalAmount` | Decimal | 当前全部明细金额合计 |
| `allConfirmed` | Boolean | 非空行是否全部已确认;合法空集为 `true` |
| `items` | VehicleItem[] | 当前全量明细 |
| `items[].id` | String(Long) | 行 ID |
| `items[].sourceType` | String | `FLEET/MANUAL` |
| `items[].sourceTypeName` | String | 车务/手工 |
| `items[].serviceDate` | Date | 服务日期 |
| `items[].vehicleId` | String(Long) | 车辆 ID,可空 |
| `items[].vehiclePlate` | String | 车牌,可空 |
| `items[].vehicleModelId` | String(Long) | 车型 ID,可空 |
| `items[].vehicleModelName` | String | 车型名,可空 |
| `items[].driverId` | String(Long) | 司机 ID,可空 |
| `items[].driverName` | String | 司机名,可空 |
| `items[].amount` | Decimal | 核单金额 |
| `items[].paymentMethod` | String | 付款方式编码 |
| `items[].paymentMethodName` | String | 付款方式名称 |
| `items[].settlementConfirmStatus` | String | 确认状态 |
| `items[].settlementConfirmStatusName` | String | 未确认/已确认 |
| `items[].remark` | String | 备注 |
| `items[].voucherUrls` | String[] | 凭证 URL |
**业务边界**
- `FLEET` 行的日期、车辆、司机、金额和付款方式不可修改或删除;只允许修改确认状态、备注、凭证。
- 全量保存时必须带回全部 `FLEET` 行。手工行可新增、修改或从全量数组中删除。
- 手工新行必须先保存为 `UNCONFIRMED`;已有 ID 后可保存为 `CONFIRMED`
- `version` 不匹配返回 584108,必须重新 GET 后再保存。
- 旧响应字段 `frozen/requirementId/settlementReady/totalVehicleFee` 及 Fleet 对账明细字段不再对管理后台输出。
### 3.4 人员费用五个 Tab
**接口路径**
| 角色 | GET/PUT 路径 | `detail` 结构 |
|---|---|---|
| 领队 | `/v3/admin/order/:orderId/settlement/staff-fees/leaders` | `days + per_day` |
| 司机 | `/v3/admin/order/:orderId/settlement/staff-fees/drivers` | `days[] + extra_cost + extra_breakdown[]` |
| 导游 | `/v3/admin/order/:orderId/settlement/staff-fees/guides` | `persons[]` |
| 摄影师 | `/v3/admin/order/:orderId/settlement/staff-fees/photographers` | `persons[]` |
| 其他 | `/v3/admin/order/:orderId/settlement/staff-fees/others` | `items[]` |
**PUT 请求体**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `items` | StaffItem[] | 是 | 当前角色全量数组;空数组清空该 Tab 可删除的行 |
| `items[].id` | String(Long) | 更新时是 | 已保存行 ID;新行为空 |
| `items[].sourceType` | String | 是 | `STAFF_ASSIGNMENT/MANUAL/SYSTEM` |
| `items[].staffId` | String(Long) | 否 | 人员安排 ID;聚合或手工行可空 |
| `items[].detail` | Object | 是 | 由路径角色固定,结构见下表 |
| `items[].reimburse` | Decimal | 否 | 小额报销,空按 0 |
| `items[].paymentMethod` | String | 否 | 空按 `COMPANY_PAID` |
| `items[].voucherUrls` | String[] | 否 | 最多 9 个 http/https URL |
| `items[].settlementConfirmStatus` | String | 是 | `UNCONFIRMED/CONFIRMED` |
| `items[].settleStatus` | String | 否 | 辅助人员 `PENDING/COMPLETED`,空按 `PENDING` |
| `items[].settledDate` | Date | 否 | 辅助人员结算日期 |
| `items[].transferRef` | String | 条件必填 | `settleStatus=COMPLETED` 时必填,最长 128 字符 |
| `items[].remark` | String | 否 | 最长 500 字符 |
**角色 `detail` 字段**
| 路径角色 | 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| leaders | `days` | Integer | 是 | 天数,最小 0 |
| leaders | `per_day` | Decimal | 是 | 每天费用,最小 0 |
| drivers | `days` | DriverDay[] | 是 | 服务日明细 |
| drivers | `days[].service_date` | Date | 是 | 服务日期 |
| drivers | `days[].vehicle_brief` | String | 否 | 车辆摘要 |
| drivers | `days[].daily_fee` | Decimal | 是 | 日费,仅回显,不计入人员费用 |
| drivers | `days[].is_used` | Boolean | 否 | 是否使用 |
| drivers | `days[].note` | String | 否 | 备注 |
| drivers | `extra_cost` | Decimal | 否 | 额外费用,空按 0 |
| drivers | `extra_breakdown` | ExtraItem[] | 否 | 合计必须等于 `extra_cost` |
| drivers | `extra_breakdown[].name` | String | 是 | 费用名 |
| drivers | `extra_breakdown[].amount` | Decimal | 是 | 金额,最小 0 |
| drivers | `extra_breakdown[].note` | String | 否 | 备注 |
| guides/photographers | `persons` | Person[] | 是 | 人员计费明细 |
| guides/photographers | `persons[].name` | String | 是 | 姓名 |
| guides/photographers | `persons[].days` | Integer | 是 | 天数,最小 0 |
| guides/photographers | `persons[].per_day` | Decimal | 是 | 每天费用,最小 0 |
| guides/photographers | `persons[].note` | String | 否 | 备注 |
| others | `items` | OtherItem[] | 是 | 其他人员费用项 |
| others | `items[].name` | String | 是 | 费用名称 |
| others | `items[].amount` | Decimal | 是 | 金额,最小 0 |
| others | `items[].note` | String | 否 | 备注 |
**GET 响应 `data`**
| 字段 | 类型 | 说明 |
|---|---|---|
| `totalActualCost` | Decimal | 当前 Tab 实际费用合计 |
| `items` | StaffItem[] | 已保存行;未保存时可返回候选草稿 |
| `items[].id` | String(Long) | 行 ID;未保存候选为空 |
| `items[].sourceType` | String | 来源编码 |
| `items[].sourceTypeName` | String | 人员安排/手工/系统 |
| `items[].staffId` | String(Long) | 人员安排 ID,可空 |
| `items[].staffName` | String | 人员姓名或聚合摘要 |
| `items[].detail` | Object | 对应角色明细 |
| `items[].totalPlannedCost` | Decimal | 计划成本 |
| `items[].totalActualCost` | Decimal | 实际成本 |
| `items[].reimburse` | Decimal | 小额报销 |
| `items[].paymentMethod` | String | 付款方式 |
| `items[].voucherUrls` | String[] | 凭证 URL |
| `items[].settlementConfirmStatus` | String | 确认状态 |
| `items[].settlementConfirmStatusName` | String | 未确认/已确认 |
| `items[].settleStatus` | String | 辅助人员结算状态;主报账人为空 |
| `items[].settledDate` | Date | 结算日期 |
| `items[].transferRef` | String | 转账流水号 |
| `items[].isPrimaryReporter` | Boolean | 是否主报账人 |
| `items[].remark` | String | 备注 |
PUT 成功返回统一成功包,`data=null`
### 3.5 餐食费用
**接口**
| 方法 | 路径 | 请求/响应 |
|---|---|---|
| GET | `/v3/admin/order/:orderId/settlement/meals` | `data` 为 MealItem[] |
| POST | `/v3/admin/order/:orderId/settlement/meals` | 请求 MealSave;响应 MealItem |
| PUT | `/v3/admin/order/:orderId/settlement/meals/:settlementId` | 请求 MealSave;响应 MealItem |
**MealSave 请求**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `mealType` | String | 是 | `BREAKFAST/LUNCH/DINNER` |
| `mealDate` | Date | 否 | 发生日期 |
| `mealName` | String | 是 | 最长 200 字符 |
| `quantity` | Integer | 是 | 110000 |
| `unitPrice` | Decimal | 是 | 最多 8 位整数、2 位小数,最小 0 |
| `paymentMethod` | String | 是 | `CASH_PAID/COMPANY_PAID/SIGNED` |
| `sourceType` | String | 是 | `MEAL_ASSIGNMENT/MANUAL/SYSTEM`;新增接口请传 `MANUAL` |
| `settlementConfirmStatus` | String | 是 | `UNCONFIRMED/CONFIRMED` |
| `voucherUrls` | String[] | 否 | 最多 9 个 http/https URL |
| `remark` | String | 否 | 最长 512 字符 |
`unitPrice × quantity` 不得超过 `99999999.99`
**MealItem 响应**
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | String(Long) | 餐食费用 ID |
| `mealType` | String | 餐型 |
| `mealDate` | Date | 发生日期 |
| `mealName` | String | 餐食名称 |
| `quantity` | Integer | 数量 |
| `unitPrice` | String(Decimal) | 单价 |
| `actualAmount` | String(Decimal) | 实际金额 |
| `paymentMethod` | String | 付款类型 |
| `sourceType` | String | 来源编码 |
| `sourceTypeName` | String | 餐饮安排/手工/系统 |
| `voucherUrls` | String[] | 凭证 URL |
| `settlementConfirmStatus` | String | 确认状态 |
| `settlementConfirmStatusName` | String | 未确认/已确认 |
| `remark` | String | 备注 |
POST 创建的是手工行,必须先传 `UNCONFIRMED`;PUT 已存在行时可传 `CONFIRMED`,且不得改变原 `sourceType`
### 3.6 其他支出
**接口**
| 方法 | 路径 | 请求/响应 |
|---|---|---|
| GET | `/v3/admin/order/:orderId/settlement/other-expenses` | `data` 为 OtherExpenseItem[] |
| POST | `/v3/admin/order/:orderId/settlement/other-expenses` | 请求 OtherExpenseSave;响应 OtherExpenseItem |
| PUT | `/v3/admin/order/:orderId/settlement/other-expenses/:settlementId` | 请求 OtherExpenseSave;响应 OtherExpenseItem |
**OtherExpenseSave 请求**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `expenseType` | String | 是 | `FUEL/TOLL/PARKING/RENTAL/MAINTENANCE/OTHER` |
| `projectName` | String | 是 | 最长 200 字符 |
| `expenseDate` | Date | 否 | 发生日期 |
| `actualAmount` | Decimal | 是 | 最多 8 位整数、2 位小数,最小 0 |
| `paymentMethod` | String | 是 | `CASH_PAID/COMPANY_PAID/SIGNED` |
| `settlementConfirmStatus` | String | 是 | `UNCONFIRMED/CONFIRMED` |
| `voucherUrls` | String[] | 否 | 最多 9 个 http/https URL |
| `remark` | String | 否 | 最长 512 字符 |
**OtherExpenseItem 响应**
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | String(Long) | 其他支出 ID |
| `expenseType` | String | 支出类型 |
| `projectName` | String | 项目名称 |
| `expenseDate` | Date | 发生日期 |
| `actualAmount` | String(Decimal) | 实际金额 |
| `paymentMethod` | String | 付款类型 |
| `sourceType` | String | `MANUAL/SYSTEM` |
| `sourceTypeName` | String | 手工/系统 |
| `voucherUrls` | String[] | 凭证 URL |
| `settlementConfirmStatus` | String | 确认状态 |
| `settlementConfirmStatusName` | String | 未确认/已确认 |
| `remark` | String | 备注 |
POST 创建的是 `MANUAL` 行且必须先为 `UNCONFIRMED`;已有 ID 后通过 PUT 可改为 `CONFIRMED`
### 3.7 其他收入
**接口**
| 方法 | 路径 | 请求/响应 |
|---|---|---|
| GET | `/v3/admin/order/:orderId/settlement/other-incomes` | `data` 为列表聚合对象 |
| POST | `/v3/admin/order/:orderId/settlement/other-incomes` | 请求 Create;响应 OtherIncomeItem |
| PUT | `/v3/admin/order/:orderId/settlement/other-incomes/:incomeId` | 请求 Update;响应 OtherIncomeItem |
**Create/Update 请求**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `requestId` | String | 仅 POST 是 | 同订单永久唯一,最长 64 字符 |
| `incomeDate` | Date | 是 | 收入日期 |
| `projectName` | String | 是 | 最长 100 字符 |
| `projectCategory` | String | 是 | 启用字典值,最长 64 字符 |
| `specification` | String | 否 | 票种/规格,最长 100 字符 |
| `quantity` | Decimal | 是 | 最多 8 位整数、4 位小数,最小 0 |
| `unitPrice` | Decimal | 是 | 最多 8 位整数、2 位小数,最小 0 |
| `settlementAmount` | Decimal | 是 | 最多 8 位整数、2 位小数,必须大于 0,且等于数量乘单价四舍五入到 2 位) |
| `paymentMethod` | String | 是 | `CASH_PAID/COMPANY_PAID/SIGNED` |
| `settlementConfirmStatus` | String | 是 | `UNCONFIRMED/CONFIRMED` |
| `voucherUrls` | String[] | 否 | 最多 9 个 http/https URL |
| `remark` | String | 否 | 最长 500 字符 |
**OtherIncomeItem 响应**
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | String(Long) | 其他收入 ID |
| `requestId` | String | 手工新增幂等 ID;系统投影为空 |
| `incomeDate` | Date | 收入日期 |
| `projectName` | String | 项目名称 |
| `projectCategory` | String | 项目类别 |
| `projectCategoryName` | String | 项目类别名称 |
| `specification` | String | 票种/规格 |
| `quantity` | Decimal | 数量 |
| `unitPrice` | Decimal | 核算单价 |
| `settlementAmount` | Decimal | 核算金额 |
| `paymentMethod` | String | 付款类型 |
| `paymentMethodName` | String | 付款类型名称 |
| `voucherUrls` | String[] | 凭证 URL |
| `settlementConfirmStatus` | String | 确认状态 |
| `settlementConfirmStatusName` | String | 未确认/已确认 |
| `remark` | String | 备注 |
| `sourceType` | String | `MANUAL/SYSTEM` |
| `sourceTypeName` | String | 手工/系统 |
| `sourceId` | String(Long) | 关联来源 ID |
**GET 聚合响应**
| 字段 | 类型 | 说明 |
|---|---|---|
| `items` | OtherIncomeItem[] | 其他收入明细 |
| `deductions` | Deduction[] | 只读减费明细 |
| `deductions[].id` | String(Long) | 减费 ID |
| `deductions[].discountName` | String | 减费名称 |
| `deductions[].discountAmount` | Decimal | 减费金额 |
| `deductions[].sourceType` | String | 减费来源 |
| `deductions[].sourceId` | String(Long) | 来源业务 ID |
| `deductions[].createdAt` | DateTime | 创建时间 |
| `summary.surchargeAmount` | Decimal | 有效增费合计 |
| `summary.discountAmount` | Decimal | 有效减费合计 |
| `summary.netAdjustmentAmount` | Decimal | 增费减去减费 |
手工新增行的公开来源为 `MANUAL`;系统自动投影行公开来源为 `SYSTEM`。原公开值 `ORDER_SURCHARGE` 不再返回。
### 3.8 完成核单
**接口**`POST /v3/admin/order/:orderId/settlement/finalize`
**请求体**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `remark` | String | 否 | 整体备注,最长 500 字符 |
| `reimbursementExpectedSourceFingerprint` | String | 是 | 主报账表 64 位小写 SHA-256 指纹 |
| `groupExpectedSourceFingerprint` | String | 是 | 单团核算表 64 位小写 SHA-256 指纹 |
| `reimbursementConfirmation` | Object | 是 | 主报账确认凭据 |
| `reimbursementConfirmation.transferDate` | Date | 条件必填 | 主报账净额非 0 时必填 |
| `reimbursementConfirmation.transferRef` | String | 条件必填 | 主报账净额非 0 时必填,最长 128 字符 |
| `reimbursementConfirmation.advanceSettledFlag` | Boolean | 是 | 预支是否已处理;`false` 是合法值 |
| `reimbursementConfirmation.signedVoucher` | Object | 是 | 签字凭证 |
| `reimbursementConfirmation.signedVoucher.files` | File[] | 是 | 19 项 |
| `reimbursementConfirmation.signedVoucher.files[].name` | String | 否 | 文件名,最长 255 字符 |
| `reimbursementConfirmation.signedVoucher.files[].url` | String | 是 | 文件 URL,最长 1024 字符 |
| `reimbursementConfirmation.signedVoucher.note` | String | 否 | 最长 500 字符 |
**响应 `data`**
| 字段 | 类型 | 说明 |
|---|---|---|
| `summaryId` | String(Long) | 核单汇总 ID |
| `finalSnapshotId` | String(Long) | 终态快照 ID |
| `finalSnapshotVersionNo` | Integer | 快照版本 |
| `finalSnapshotStatus` | String | 成功时为 `FINALIZED` |
| `orderId` | String(Long) | 订单 ID |
| `settledAt` | DateTime | 核单完成时间 |
| `totalAmount` | String(Decimal) | 订单总金额快照 |
| `paidAmount` | String(Decimal) | 已付金额快照 |
| `balanceAmount` | String(Decimal) | 尾款金额快照 |
| `roomCost` | String(Decimal) | 住宿实际成本 |
| `ticketCost` | String(Decimal) | 门票实际成本 |
| `staffCost` | String(Decimal) | 人员费用实际成本 |
| `subsidyCost` | String(Decimal) | 补助实际成本 |
| `mealCost` | String(Decimal) | 餐食实际成本 |
| `vehicleCost` | String(Decimal) | 车辆实际成本 |
| `otherExpenseCost` | String(Decimal) | 其他支出实际成本 |
| `insurancePremium` | String(Decimal) | 保险实际保费 |
| `totalActualCost` | String(Decimal) | 总实际成本 |
| `driverTransferAmount` | String(Decimal) | 给主报账人的转回金额 |
| `profitAmount` | String(Decimal) | 公司毛利 |
| `profitRate` | Decimal | 毛利率小数 |
| `orderStatusAfter` | String | 完成后的订单状态 |
| `mqTriggered` | Boolean | 当前固定为 `false` |
| `warnings` | String[] | 不阻塞完成核单的软预警 |
**业务边界**
- 住宿、门票/游玩、餐食、车辆、导游、摄影、其他收入、其他支出八类来源必须同步完成。
- 任一非空分类存在 `UNCONFIRMED` 行时,finalize 返回 584310,不生成终态快照。
- 系统来源事实变化会使对应行重新变为 `UNCONFIRMED`;应刷新、复核并保存后再 finalize。
## 4. 接口入参汇总
| 输入类型 | 适用接口 | 关键变化 |
|---|---|---|
| 路径参数 | 全部 26 个接口 | `orderId` 必填;行级修改另有 `settlementId/incomeId` |
| 全量明细 | 住宿、门票、车辆、五个人员 Tab | 必须提交完整 `items`;新增手工行先传 `UNCONFIRMED` |
| 单行保存 | 餐食、其他支出、其他收入 | POST 新增先未确认,PUT 已有行可确认 |
| 车辆版本 | 车辆 PUT | 必须原样回传最近 GET 的 `version` |
| 完成核单凭据 | finalize | 双报告指纹 + 主报账转账/签字凭据 |
完整字段、必填性和校验已分别内联在 §3.1§3.8。
## 5. 出参字段汇总
| 变化 | 适用响应 |
|---|---|
| 新增 `sourceType/sourceTypeName` | 人员、餐食、其他支出;其他收入的来源语义调整 |
| 新增 `settlementConfirmStatus/settlementConfirmStatusName` | 门票;餐食、其他支出、人员补齐名称 |
| Long ID 按字符串返回 | 住宿、门票、人员、车辆以及已有明确字符串序列化的资金明细 |
| 新车辆草稿结构 | `orderId/version/totalAmount/allConfirmed/items` |
| 删除车辆内部/兼容字段 | 不再输出 `frozen/requirementId/settlementReady/totalVehicleFee` 及 Fleet 对账字段 |
## 6. 枚举 / 数据字典
### 6.1 住宿 `sourceType`
| 值 | 中文 | 说明 |
|---|---|---|
| `HOUSE_ASSIGNMENT` | 配房结果 | 系统配房来源 |
| `MANUAL` | 手工 | 管理后台手工新增 |
| `SYSTEM` | 系统 | 其他系统来源;替代旧公开值 `TEMPLATE` |
| `TEMPLATE` | 历史兼容 | 仅请求兼容,响应不返回 |
### 6.2 门票 `sourceType`
| 值 | 中文 | 说明 |
|---|---|---|
| `SCENIC_ASSIGNMENT` | 景区 | 景区安排来源 |
| `ACTIVITY_ASSIGNMENT` | 游玩项目 | 活动安排来源 |
| `MANUAL` | 手工 | 手工新增;响应统一值 |
| `CUSTOM_ASSIGNMENT` | 历史兼容 | 仅请求兼容,响应归一为 `MANUAL` |
### 6.3 餐食 `sourceType`
| 值 | 中文 | 说明 |
|---|---|---|
| `MEAL_ASSIGNMENT` | 餐饮安排 | 系统餐饮安排来源 |
| `MANUAL` | 手工 | 管理后台新增 |
| `SYSTEM` | 系统 | 其他系统来源 |
### 6.4 车辆 `sourceType`
| 值 | 中文 | 说明 |
|---|---|---|
| `FLEET` | 车务 | 车务同步行,业务字段不可改删 |
| `MANUAL` | 手工 | 核单页手工补录 |
### 6.5 人员 `sourceType`
| 值 | 中文 | 说明 |
|---|---|---|
| `STAFF_ASSIGNMENT` | 人员安排 | 系统人员安排来源 |
| `MANUAL` | 手工 | 手工新增 |
| `SYSTEM` | 系统 | 其他系统来源 |
### 6.6 其他收入/其他支出 `sourceType`
| 值 | 中文 | 说明 |
|---|---|---|
| `MANUAL` | 手工 | 管理后台创建 |
| `SYSTEM` | 系统 | 自动投影或系统来源 |
### 6.7 `settlementConfirmStatus`
| 值 | 中文 | 说明 |
|---|---|---|
| `UNCONFIRMED` | 未确认 | 首次系统同步或手工新行的初始状态 |
| `CONFIRMED` | 已确认 | 已有行复核后保存的状态 |
### 6.8 `paymentMethod`
| 值 | 中文 | 说明 |
|---|---|---|
| `CASH_PAID` | 现付 | 现场/主报账人支付 |
| `COMPANY_PAID` | 公司付款 | 公司直接支付 |
| `SIGNED` | 签单 | 签单结算 |
### 6.9 餐型 `mealType`
| 值 | 中文 |
|---|---|
| `BREAKFAST` | 早餐 |
| `LUNCH` | 午餐 |
| `DINNER` | 晚餐 |
### 6.10 支出类型 `expenseType`
| 值 | 中文 |
|---|---|
| `FUEL` | 油费 |
| `TOLL` | 过路费 |
| `PARKING` | 停车费 |
| `RENTAL` | 租赁费 |
| `MAINTENANCE` | 维修保养 |
| `OTHER` | 其他 |
### 6.11 人员辅助结算状态 `settleStatus`
| 值 | 中文 | 说明 |
|---|---|---|
| `PENDING` | 待结算 | 默认值 |
| `COMPLETED` | 已结算 | 必须同时提交 `transferRef` |
## 7. 错误码
| code | 含义 | 触发场景 |
|---|---|---|
| `400` | 参数校验失败 | 必填缺失、格式/长度/枚举错误、车辆或人员请求出现未知字段 |
| `584001/584010/584020/584050/584070` | 订单不存在 | 对应住宿、门票、人员、finalize 或通用查询找不到订单 |
| `584002/584011/584021` | 当前状态不可写 | 住宿、门票、人员费用不在允许的核单阶段 |
| `584006/584012/584022` | 枚举或角色非法 | 住宿付款、门票来源、人员角色非法 |
| `584067/584068/584069` | 住宿资源无效或暂不可用 | 手工酒店/房型无效或资源服务不可用 |
| `584071` | 无权访问该订单 | 公司隔离或现有订单权限不满足 |
| `584073` | 其他收入不存在 | `incomeId` 不属于当前订单 |
| `584074` | 当前状态不允许修改其他收入 | 核单状态不可写 |
| `584076` | 其他收入金额不一致 | `settlementAmount != quantity × unitPrice` |
| `584077` | 存在未确认的其他收入 | 生成报告或完成核单前仍有其他收入未确认 |
| `584086` | 无权修改核单资金数据 | 非主管、管理员或财务 |
| `584087` | `requestId` 冲突 | 同订单相同 `requestId` 被另一笔请求占用 |
| `584089` | 核单或结算已完成 | 再次修改资金明细 |
| `584090/584091` | 餐食/其他支出不存在 | 行 ID 不属于当前订单 |
| `584092` | 存在未确认的人员费用 | 生成报告或完成核单前仍有人员费用未确认 |
| `584094/584095` | 餐食/其他支出字段非法 | 字段越界或试图改变系统来源 |
| `584096/584097` | 付款方式/凭证非法 | 枚举错误或 URL 数量、格式错误 |
| `584098` | 餐食或其他支出存在未确认记录 | 生成报告或完成核单前仍有餐食/其他支出未确认 |
| `584100/584101/584102` | 车辆来源不可用/未就绪/为空 | 车务数据不可读、未完结/未确认、无可核单费用 |
| `584103/584104/584105` | 其他收入字典非法或不可用 | 项目类别、规格无效或字典不可用 |
| `584106` | 确认状态非法 | 非 `UNCONFIRMED/CONFIRMED` |
| `584107` | 手工新增行必须先未确认 | `id=null` 的手工新行直接传 `CONFIRMED` |
| `584108` | 车辆草稿版本冲突 | PUT 的 `version` 已过期 |
| `584109` | 车务来源字段不可改删 | 修改/漏传 `FLEET` 行的权威字段 |
| `584310` | 八类核单未全部确认或数据已变化 | finalize 前有非空未确认行、来源未就绪 |
| `584315` | 报告来源已变化 | finalize 双报告指纹过期 |
| `584317` | 当前报告状态不允许操作 | finalize 凭据结构或状态不满足 |
| `584325` | 完成核单必须提交当前指纹 | 双报告指纹缺失 |
## 验证证据8. 示例:典型 / 边界 / 异常)
已完成以下测试环境路由与业务负向验证:
- 部署任务 `80f1695a` 成功,order-v3 主、副实例滚动完成。
- 两个核算中订单实调
`GET /v3/admin/order/:orderId/settlement/step3/vehicles`
均进入新代码并返回业务前置码 `584102`,证明新路由已生效。
- 旧 `GET /v3/admin/order/:orderId/settlement/vehicle-fees` 返回 `404`,证明旧入口已下线。
本节下列 JSON 是按已合并 Controller/VO 契约给出的自包含调用示例。由于测试订单缺少可核单车辆费用,本次未取得车辆 DTO 正向数据,也未完成车辆链路 Full E2E;不得把上述 584102 负向结果描述为正向业务通过。
### 8.1 典型成功:确认车辆系统来源行
**请求**
```http
PUT /v3/admin/order/900000000001/settlement/step3/vehicles
Authorization: Bearer <JWT>
Content-Type: application/json
```
```json
{
"version": 3,
"items": [
{
"id": "930000000001",
"sourceType": "FLEET",
"serviceDate": "2026-07-30",
"vehicleId": "880000000001",
"vehiclePlate": "藏A12345",
"vehicleModelId": "870000000001",
"vehicleModelName": "七座商务车",
"driverId": "860000000001",
"driverName": "张师傅",
"amount": 1200.00,
"paymentMethod": "COMPANY_PAID",
"settlementConfirmStatus": "CONFIRMED",
"remark": "金额已核对",
"voucherUrls": []
}
]
}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000001",
"version": 4,
"totalAmount": 1200.00,
"allConfirmed": true,
"items": [
{
"id": "930000000001",
"sourceType": "FLEET",
"sourceTypeName": "车务",
"serviceDate": "2026-07-30",
"vehicleId": "880000000001",
"vehiclePlate": "藏A12345",
"vehicleModelId": "870000000001",
"vehicleModelName": "七座商务车",
"driverId": "860000000001",
"driverName": "张师傅",
"amount": 1200.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "金额已核对",
"voucherUrls": []
}
]
},
"success": true
}
```
### 8.2 边界情况:合法空车辆草稿
无当前用车需求时,GET 可返回合法空集;`allConfirmed=true` 表示“空集中没有未确认行”,不表示存在车辆费用。
**请求**
```http
GET /v3/admin/order/900000000001/settlement/step3/vehicles
Authorization: Bearer <JWT>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000001",
"version": 1,
"totalAmount": 0.00,
"allConfirmed": true,
"items": []
},
"success": true
}
```
### 8.3 业务失败:手工新行直接确认
**请求**
```http
POST /v3/admin/order/900000000001/settlement/meals
Authorization: Bearer <JWT>
Content-Type: application/json
```
```json
{
"mealType": "LUNCH",
"mealDate": "2026-07-30",
"mealName": "团队午餐",
"quantity": 10,
"unitPrice": 50.00,
"paymentMethod": "CASH_PAID",
"sourceType": "MANUAL",
"settlementConfirmStatus": "CONFIRMED",
"voucherUrls": [],
"remark": null
}
```
**响应**
```json
{
"code": 584107,
"message": "手工新增核单明细必须先保存为未确认",
"data": null,
"success": false
}
```
## 9. 业务边界
- ✅ **系统来源首次同步**:生成已有 ID 的 `UNCONFIRMED` 行;复核后可直接在对应 PUT 中保存为 `CONFIRMED`
- ✅ **手工新增**:第一次必须保存为 `UNCONFIRMED`;接口返回 ID 后,第二次更新才允许保存为 `CONFIRMED`
- ✅ **来源统一**:手工行统一公开为 `MANUAL`;系统行公开为各分类系统来源值,无法细分的系统行为 `SYSTEM`
- ❌ **不可混用确认和辅助结算状态**`settlementConfirmStatus` 表示核单确认;人员 `settleStatus` 表示辅助人员款项是否结清。
- ❌ **不可修改系统权威字段**:系统来源事实变化后应重新 GET;车辆 `FLEET` 行不得由前端改删。
- ⚠️ **finalize 门禁**:八个核单分类来源必须就绪,且每个非空分类全部逐行 `CONFIRMED`
- ⚠️ **空分类**:合法空分类没有未确认行,但来源同步仍必须就绪;`allConfirmed=true` 不等于有费用。
## 10. 修改前后对比
### 10.1 字段级对比
| 范围 | 改前 | 改后 |
|---|---|---|
| 住宿系统来源 | `TEMPLATE` | `SYSTEM``TEMPLATE` 仅兼容旧入参 |
| 门票手工来源 | 可能返回 `CUSTOM_ASSIGNMENT` | 统一返回 `MANUAL` |
| 其他收入来源 | `ORDER_SURCHARGE` | `MANUAL``SYSTEM` |
| 门票行确认 | 无逐行确认字段 | 新增 `settlementConfirmStatus/Name` |
| 人员请求行 | 无 `id/sourceType/settlementConfirmStatus` | 三字段纳入全量保存契约 |
| 餐食请求/响应 | 无公开来源,确认名称不完整 | 增加 `sourceType/sourceTypeName/settlementConfirmStatusName` |
| 其他支出响应 | 无公开来源和确认名称 | 增加 `sourceType/sourceTypeName/settlementConfirmStatusName` |
| 住宿/门票/人员 ID | 部分按 JSON 数字输出 | 明细 `id`、来源 ID 按字符串输出 |
| 车辆顶层响应 | `frozen/requirementId/settlementReady/totalVehicleFee/items` | `orderId/version/totalAmount/allConfirmed/items` |
| 车辆行响应 | 暴露 Fleet 对账、冻结和自动车费字段 | 仅输出核单需要的来源、车辆、司机、金额、付款、确认、凭证字段 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 系统派生行初始状态 | 分类规则不一致,部分直接视为已确认 | 统一先 `UNCONFIRMED`,用户复核后保存为 `CONFIRMED` |
| 手工新增并确认 | 部分接口允许一次保存即确认 | 必须先未确认,取得 ID 后再确认 |
| 车辆入口 | 旧 `/settlement/vehicle-fees` 已下线且无新独立编辑入口 | 使用 `/settlement/step3/vehicles` GET/PUT |
| 车辆并发保存 | 无前端草稿版本 | 必须携带 `version`,冲突时刷新 |
| 完成核单 | 分类确认来源不完全统一 | 只接受八类来源就绪且非空行全部已确认的数据 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:是。车辆入口和响应结构为新契约;其他收入 `sourceType` 值发生变化;多个请求/响应新增确认与来源字段。
- **前端是否必须同步上线**:是。需切换车辆接口、适配字符串 ID、新来源枚举和“先保存未确认、再确认”的交互。
### 11.2 回滚说明
若后端回滚,前端需同时回滚车辆 Step3 新入口及新增字段依赖;旧
`/settlement/vehicle-fees` 两个接口在本次变更前已下线,不能作为回滚兜底。
## 12. 注意事项
- 删除对旧 `GET /settlement/vehicle-fees``POST /settlement/vehicle-fees/confirm` 的任何残留调用。
- 车辆保存必须回传最近 GET 的 `version` 和全部 `FLEET` 行;584108 时刷新后让用户重新确认。
- 不再把 `ORDER_SURCHARGE``TEMPLATE``CUSTOM_ASSIGNMENT` 当作新响应值。
- 所有 Long 类型字符串 ID 按字符串比较、传递,不转为 JavaScript Number。
- finalize 返回 584310 时,应刷新相关 Tab;来源变化可能已把已确认行重新置为未确认。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5356](https://git.1814.love:8443/wx/HL/issues/5356)
- **PR**: [#5362](https://git.1814.love:8443/wx/HL/pulls/5362)
- **Feature commit**: [6e396f6fc4](https://git.1814.love:8443/wx/HL/commit/6e396f6fc48fbf6581e87224bb85f2c811759727)
- **Merge commit**: [cfac945db2](https://git.1814.love:8443/wx/HL/commit/cfac945db268640b0b9e60b4d8c7ab55739a69e3)
### 13.2 联系人
- **后端负责人**: @yaosutu

查看文件

@ -1,261 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5360"
title: "核单车辆异步下拉"
consumer: "admin"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "partial"
frontend_status: "implemented"
frontend_owner: "Pi"
frontend_ref: "v2.1@0927f28de6c091d1eb1c867f5c96088358057043"
target_release: ""
verified_at: "2026-07-30"
status_note: "管理后台已在核单车辆手工行接入异步车辆下拉,按关键词远程检索并回填车辆、车型与常驻司机字符串 ID;pnpm checkpoint 全量通过,业务提交 0927f28de6c091d1eb1c867f5c96088358057043 已推送至 origin/v2.1。测试服真实订单正向响应与角色权限仍受有效登录态缺失限制。"
updated_at: "2026-07-31"
base: "dev-v3"
generated: "2026-07-30T17:18:34+08:00"
---
# ✨【新增接口·管理后台】核单车辆异步下拉 (#5360)
| 头部字段 | 当前值 |
|---|---|
| PR / 服务 | [#5361](https://git.1814.love:8443/wx/HL/pulls/5361) / `hl-order-service-v3` |
| 后端状态 | `deployed`:已合入 `dev-v3` 并部署测试服 |
| 网关状态 | `partial`:路由和鉴权响应已验证,正向业务响应待有效登录态复验 |
| 前端回写标志 | 已实现 |
| 前端认领信息 | `frontend_owner: Pi``frontend_ref: v2.1@0927f28de6c091d1eb1c867f5c96088358057043` |
| 更新时间 | `2026-07-30` |
## 1. 接口背景
核单页面需要按车牌、品牌型号、车型大类或常驻司机姓名异步检索车辆。新增轻量只读下拉接口,返回可直接作为车辆选项使用的七个字段。
## 2. 变更清单
| # | 接口名 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 查询核单车辆异步下拉 | `GET` | `/v3/admin/order/{orderId}/settlement/vehicle-options` | ✨ 新增接口 | 按关键词检索车辆,默认最多返回 10 条,最多返回 20 条 |
## 3. 接口详情
### 3.1 查询核单车辆异步下拉
- **接口说明**`keyword` 可匹配车牌、品牌型号、车型大类和常驻司机姓名;`limit` 默认 10、最大 20。
- **使用场景**:核单页面加载车辆选择器或按关键词刷新候选项。
- **认证**:需要管理后台登录态。房务管理员和房务组长不可调用;管理员、超级管理员可查看任意订单,其他后台角色仅可查看本人作为定制师的订单。
- **幂等性**:幂等,只读查询,无请求体、无幂等键。
- **限流**:本接口未声明独立限流规则。
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 字段 | 位置 | 类型 | 必填 | 默认值 | 说明与校验规则 |
|---|---|---|---|---|---|
| `orderId` | path | `String` | 是 | — | 订单 ID,必须是大于 0 的整数;按字符串传递,避免 JavaScript 数字精度损失 |
| `keyword` | query | `String` | 否 | 空 | 模糊匹配车牌、品牌型号、车型大类或常驻司机姓名;不传或仅空白字符表示不过滤 |
| `limit` | query | `Integer` | 否 | `10` | 期望返回条数;不传或非正数按 10 处理,超过 20 按 20 处理 |
### 4.2 请求体字段
无请求体。
## 5. 出参字段
响应类型:`Result<List<SettlementVehicleOptionRespVO>>`
### 5.1 统一响应
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `code` | `Integer` | 否 | `200` 表示成功;其他值见错误码 |
| `message` | `String` | 否 | 响应消息,成功时为 `成功` |
| `data` | `Array<VehicleOption>` | 失败时可空 | 车辆下拉项数组;没有匹配项时为 `[]` |
| `traceId` | `String` | 是 | 链路追踪 ID,未注入时可为 `null` 或不返回 |
| `success` | `Boolean` | 否 | `code === 200` 时为 `true` |
### 5.2 `data[]` 车辆下拉项
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `vehicleId` | `String` | 否 | 车辆 ID;JSON 固定按字符串返回 |
| `plate` | `String` | 是 | 车牌 |
| `modelName` | `String` | 是 | 品牌型号 |
| `typeName` | `String` | 是 | 车型大类名称 |
| `primaryDriverId` | `String` | 是 | 常驻司机 ID;无常驻司机时为 `null`;有值时按字符串返回 |
| `primaryDriverName` | `String` | 是 | 常驻司机姓名;无常驻司机时为 `null` |
| `label` | `String` | 否 | 下拉展示文案,依次包含车牌、品牌型号、车型大类和常驻司机姓名;无常驻司机时最后一段为 `无常驻司机` |
`data[]` 严格只有以上七个字段,不包含车辆费用、支付方式或其他未声明字段。
## 6. 枚举 / 数据字典
本接口的入参和出参不包含枚举或数据字典字段。
## 7. 错误码
| code | 含义 | 触发场景 |
|---|---|---|
| `400` | `订单 ID 必须大于 0` | `orderId <= 0`,参数校验失败 |
| `581007` | `订单不存在` | `orderId` 对应订单不存在 |
| `581008` | `无权查看此订单` | 非管理员后台角色访问其他定制师的订单,或请求上下文缺少可用于判断订单归属的管理员 ID |
| `581045` | `房务角色无权查看订单详情,房务仅可配房` | 房务管理员或房务组长调用本接口 |
| `584072` | `车务司机车辆信息暂时不可用,请稍后重试` | 车辆候选信息暂时不可用 |
管理后台登录态无效或缺失时,请求会在进入本接口前被统一认证拦截。
## 8. 示例
### 8.1 典型成功
**请求**
```http
GET /v3/admin/order/2079454953641836546/settlement/vehicle-options?keyword=%E5%BC%A0%E5%B8%88%E5%82%85&limit=10
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": [
{
"vehicleId": "9202101",
"plate": "蒙A-88888",
"modelName": "丰田汉兰达",
"typeName": "SUV",
"primaryDriverId": "9204101",
"primaryDriverName": "张师傅",
"label": "蒙A-88888***丰田汉兰达***SUV***张师傅"
}
],
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
### 8.2 边界情况
**场景说明**:不传关键词;`limit=20` 使用允许的最大返回条数;示例项没有常驻司机。
**请求**
```http
GET /v3/admin/order/2079454953641836546/settlement/vehicle-options?limit=20
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": [
{
"vehicleId": "9202102",
"plate": "蒙A-66666",
"modelName": "别克GL8",
"typeName": "商务车",
"primaryDriverId": null,
"primaryDriverName": null,
"label": "蒙A-66666***别克GL8***商务车***无常驻司机"
}
],
"traceId": "b2c3d4e5-f6a7-8901",
"success": true
}
```
没有匹配项时,`data` 返回空数组:
```json
{
"code": 200,
"message": "成功",
"data": [],
"traceId": "b2c3d4e5-f6a7-8901",
"success": true
}
```
### 8.3 业务失败
**场景说明**`orderId=0`,不满足大于 0 的校验规则。
**请求**
```http
GET /v3/admin/order/0/settlement/vehicle-options
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**响应**
```json
{
"code": 400,
"message": "订单 ID 必须大于 0",
"data": null,
"traceId": "c3d4e5f6-a7b8-9012",
"success": false
}
```
## 9. 业务边界
- **适用场景**:管理后台核单页面只读查询车辆候选;可按车牌、品牌型号、车型大类或常驻司机姓名搜索。
- **访问范围**:管理员、超级管理员可访问全部订单;其他允许查看订单详情的后台角色仅可访问本人作为定制师的订单。
- **不适用角色**:房务管理员、房务组长不可查看本接口数据。
- **返回范围**:查询结果最多 20 条;无匹配项返回 `[]`;接口不返回车辆费用、支付方式等核单数据。
- **特殊边界**`keyword` 为空或空白时不过滤;`limit <= 0` 按 10 处理;`limit > 20` 按 20 处理。
## 10. 修改前后对比
本次为新增接口,不修改任何既有接口的字段、类型、必填性、枚举或错误码,因此无字段级、行为级替换关系。
## 11. 影响评估 / 回滚
- **是否破坏向后兼容**:否。新增独立 GET 路径,不影响既有调用方。
- **前端是否必须同步上线**:否。未接入本接口的旧版管理后台可继续运行;需要核单车辆异步搜索能力时再接入。
- **回滚影响**:若新接口不可用,前端应停用本下拉数据源,不应改用未在本文声明的字段或接口代替。
## 12. 注意事项
- `vehicleId` 和非空的 `primaryDriverId` 必须始终按字符串保存、比较和提交,不能转为 JavaScript `Number`
- 前端只依赖 `data[]` 中声明的七个字段;`primaryDriverId``primaryDriverName` 允许为 `null`
- 搜索时传用户输入的 `keyword` 即可;不需要为车牌、车型或司机姓名拆分多次请求。
- 本接口为只读查询,成功响应不表示已选择、保存或核单确认车辆。
- 测试服已确认新路径进入统一鉴权链路;因现有测试登录态失效,真实订单正向响应、关键词过滤、`limit` 边界与角色权限仍待使用有效管理后台登录态复验。
### 12.1 验证状态
- **验证模式**`TARGETED_FALLBACK`,仅执行只读 GET,无写入。
- **已验证**:测试服双实例 OpenAPI 已加载本接口;真实网关请求已进入统一鉴权链路并返回标准五字段错误体。
- **待复验**:现有测试登录态已失效,真实订单 `code=200` 响应、七字段运行时值、关键词过滤、`limit` 边界和角色权限尚未形成正向实证。
- **验证边界**:上述状态不代表核单 Full E2E 已完成。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5360](https://git.1814.love:8443/wx/HL/issues/5360)
- **PR**: [#5361](https://git.1814.love:8443/wx/HL/pulls/5361)
- **Merge commit**: [0ff4ef45ccd0f4ebb1d3be00b27cde06f26bce0b](https://git.1814.love:8443/wx/HL/commit/0ff4ef45ccd0f4ebb1d3be00b27cde06f26bce0b)
- **Feature commit**: [5be7985164cdae5417cdeb2a2be7d104dc8fdae8](https://git.1814.love:8443/wx/HL/commit/5be7985164cdae5417cdeb2a2be7d104dc8fdae8)
### 13.2 联系人
- **后端负责人**yaosutu
- **前端状态**:已实现并推送 `0927f28de6c091d1eb1c867f5c96088358057043`

查看文件

@ -1,159 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5363"
title: "车务派车详情大交通契约补全"
consumer: "admin"
change_type: "修改接口"
backend_status: "pending"
gateway_status: "pending"
frontend_status: "implemented"
frontend_owner: "Pi"
frontend_ref: "v2.1@f4cac12fff3297fb42e6a217b1764b5408339133"
target_release: ""
verified_at: "2026-07-31"
status_note: "管理后台已补全大交通交通方式、真实双端路线与 legacy-only fail-closed 展示,checkpoint 通过并推送 f4cac12fff3297fb42e6a217b1764b5408339133;后端 PR #5365 虽已合并,但尚未部署或执行网关验证,因此 backend/gateway 继续保持 pending,前端不宣称页面联调 verified。"
updated_at: "2026-08-01"
base: "dev-v3"
---
# 车务:派车详情大交通契约补全 (#5363)
> **服务**`hl-order-service-v3``hl-fleet-service`
>
> **PR**[#5365](https://git.1814.love:8443/wx/HL/pulls/5365)已合并,merge `e8e654a482`
>
> **Backend Issue**[#5363](https://git.1814.love:8443/wx/HL/issues/5363)
>
> **Frontend tracking**[#5366](https://git.1814.love:8443/wx/HL/issues/5366)(管理后台已实现静态契约与回归测试;后端未部署前不宣称页面联调完成)
>
> **日期**2026-07-30
>
> **影响范围**:管理后台车务派车详情的大交通整团段与分批批次
## 变更接口
| 接口 | 方法 | 路径 | 变更类型 |
|---|---|---|---|
| 查询车务派车订单详情 | `GET` | `/admin/fleet/board/orders/:orderId` | 响应字段 additive 新增 |
接口路径、HTTP 方法、请求参数、错误码和既有响应字段均不变。
## 二、新增响应字段
以下字段同时新增到:
- `data.transport.arrive`
- `data.transport.depart`
- `data.transport.batches[]`
| 字段 | JSON 类型 | 可空 | 来源约束 | 说明 |
|---|---|---|---|---|
| `transportType` | `String` | 是 | order-v3 大交通计划开放字符串原值 | 当前已知 `FLIGHT``TRAIN``SELF_DRIVE``BUS``OTHER`;未来未知非空值也原样透传;只有精确 `SELF_DRIVE` 表示自驾 |
| `departStation` | `String` | 是 | order-v3 `departStation` 原值,源字段上限 100 字符 | 同一大交通计划的真实出发站;源未提供时为 `null` |
| `arriveStation` | `String` | 是 | order-v3 `arriveStation` 原值,源字段上限 100 字符 | 同一大交通计划的真实到达站;源未提供时为 `null` |
响应中的 `time` 仍为现有 date-time 字符串或 `null`,本次不改变时间口径。
## 三、兼容与部分路线语义
既有 `station` 保留且只作为方向相关的 legacy compatibility 字段:
- `direction=ARRIVAL``station = arriveStation`,它只代表已知到达端;
- `direction=DEPARTURE``station = departStation`,它只代表已知出发端。
路线展示只认两个新增端点:
- 两端都有值:显示 `departStation → arriveStation`
- 只有到达端:显示 `未知 → arriveStation`
- 只有出发端:显示 `departStation → 未知`
- 两端都没有而只有 legacy `station`:仅显示独立字段 `旧数据站点路线不完整station`,禁止箭头和完整路线语义。
禁止把 `station` 放入或复制到任一真实端点。新 `departStation``arriveStation` 为空时保持缺失,不得根据 `station`、方向、班次号、时间、备注或接送地点推断或补造。
## 四、交通方式语义
`transportType` 是 nullable/open `String`,不是 closed enum。当前可观测值与稳定文案为
- `FLIGHT`:飞机;
- `TRAIN`:火车;
- `SELF_DRIVE`:自驾;
- `BUS`:大巴;
- `OTHER`:其他。
只有精确 `transportType === "SELF_DRIVE"` 表示自驾。`null` 显示“未提供”;未来未知非空值显示“未知交通方式”并 fail-closed,不得丢弃原值、归并为已知类型,也不得从 `transportNo``time`、站点或备注推断交通类型。
## 五、响应示例
```json
{
"code": 200,
"data": {
"transport": {
"arrive": {
"planId": "9007199254740993",
"direction": "ARRIVAL",
"travelerIds": ["9007199254740995"],
"transportType": "FLIGHT",
"transportNo": "CA1234",
"time": "2026-07-29T10:30:00",
"station": "海拉尔东山国际机场",
"departStation": "北京首都机场",
"arriveStation": "海拉尔东山国际机场"
},
"depart": null,
"batches": [
{
"planId": "9007199254740997",
"direction": "DEPARTURE",
"travelerIds": ["9007199254740999"],
"transportType": "TRAIN",
"transportNo": "G5678",
"time": "2026-07-31T17:20:00",
"station": "海拉尔站",
"departStation": "海拉尔站",
"arriveStation": null
}
]
}
},
"success": true
}
```
## 六、前端消费动作
1. 路线只使用真实 `departStation``arriveStation`;单端缺失显示明确“未知”,legacy-only `station` 仅显示为独立“旧数据站点(路线不完整)”。
2. 仅按权威 `transportType=SELF_DRIVE` 进入自驾展示;`BUS`/`OTHER` 使用稳定文案,`null` 与未知字符串按上节 fail-closed。
3. `planId``travelerIds[]` 均为 JSON `String`,必须端到端保持字符串,禁止转为 JavaScript `Number`;精度安全用例使用示例中的超大 ID。
4. 前端已在 `v2.1@f4cac12fff3297fb42e6a217b1764b5408339133` 完成实现与具名 negative tests,`frontend_status``implemented`;后端未部署前不升级为页面联调 `verified`
5. 领取后按标准状态流转回写 `frontend_owner``frontend_ref``frontend_status`
## 七、Shared Java / Internal Feign 影响
面向前端的公开管理端契约是 `GET /admin/fleet/board/orders/:orderId`。order-v3 producer 与 Fleet consumer 之间另有内部契约 `GET /v3/internal/order/orders/:orderId/transport`,响应共享 Java DTO `OrderTransportForFleetDTO.TransportSegment/TransportBatch`
- `transportType``departStation``arriveStation` 都是 additive nullable/open `String`;旧 consumer 可忽略新增 JSON key,旧 producer 缺 key 时 Fleet 按 `null` 消费。
- 滚动发布顺序应先保证 order-v3 producer 兼容,再由 Fleet consumer 使用;不允许把 oasdiff/SCC 的 `not_configured` 写成 PASS。
- shared DTO 的非 board 消费者包括 assignment、H5 itinerary、通知快照/模板与 `OrderQueryFacade` 读路径;本次仅增加它们可忽略的字段,不改变其既有行为,也不授权它们推断路线或交通类型。
## 验证证据
- blocker focused30 tests,0 failure/error,4 个无 Docker 条件 skip。
- order producer/internal Controller 定向测试54 tests,0 failure/error/skip。
- Fleet consumer/admin Controller 定向测试65 tests,0 failure/error/skip。
- Fleet SpotlessBUILD SUCCESS。
- Fleet reactor verify2730 tests,0 failure,0 error,2 skips。
- order-v3 reactor verify7210 tests,0 failure,0 error,35 skips。
- 最终证据索引:`hl-5363-final-ac493-evidence-index.json``safe=true`,7 files。
- oasdiff`not_configured`;以字段级源码对比和 Controller JSON 测试作为 fallback。
- Spring Cloud Contract`not_configured`;以 producer/consumer 测试和两个 reactor verify 作为 fallback。
- changelog 草稿门禁:仓库单元测试 46/46、文件名校验、path aliases 校验及 workflow `lint --allow-pending` 均通过。
`--allow-pending` 只证明初始 pending 草稿结构合法,不是发布态 lint 证据。后端已合并但未部署,网关亦未验证,因此 `backend_status``gateway_status` 保持 `pending`;前端仅以 checkpoint 与已推送业务提交收口为 `implemented`,不表述为已联调或 `verified`
## 九、不影响范围
- 无 DDL、无历史数据迁移。
- 不改变派车状态机、候选/占用、费用、保险、Outbox 或消息模板。
- 不包含多司机通知/确认、整段资源应用或需求级原子确认。
- 不代表 `hl-ui` 已实现、发布或完成页面验证。

查看文件

@ -1,286 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5368"
title: "非订单页面统一补齐真实团号与团号搜索"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@4c8bdb97c89fa399e5fbb13240ea082f64fefc59"
target_release: "v2.1"
verified_at: "2026-08-03"
status_note: "PR #5386 已合并dev-v3@07834e4c5;order-v2/order-v3/user/fleet 已部署 TESTtasks 03b0fecb/2b54e87b/258865cf/d290d667并经网关验证profile/orders teamNo=groupCode+keyword 搜索、contract teamNo 过滤分页正确、fleet insurance/board teamNo 返回与查询orderNo 片段不命中、chat 会话 teamNo、月度对账 teamNo、雪花 ID JSON String。OpenAPI/oasdiff 与 Spring Cloud Contract 仍 not_configured人工回退证据见 D:/tmp/hl5368-gateway-evidence/)。管理后台已由 Pi 领取并开始逐页核对真实团号消费。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 管理端:非订单页面统一补齐真实团号与团号搜索
> **服务**: `hl-user-service``hl-order-service-v2``hl-order-service-v3``hl-fleet-service`
> **Issue**: [wx/HL#5368](https://git.1814.love:8443/wx/HL/issues/5368)
> **前端交接 Issue**: `wx/hl-api-changelog#64`
> **日期**: 2026-07-31
> **影响范围**: 管理后台工作台、订单列表、合同、评价、车务、房务、会话等订单关联页面
> **候选基线**: `dev-v3@07834e4c5`PR #5386 已合并)
> **合并 PR**: [#5386](https://git.1814.love:8443/wx/HL/pulls/5386) head `b65cff23a`,merge commit `07834e4c5`
> **后端/网关状态**: backend=deployed,gateway=verified2026-08-03
> **契约裁决yst 2026-08-03T16:24,承接 #5372**: `/internal/order/designer-list` 仅扩展 keyword 对真实 `groupCode` 的模糊搜索,保留原响应字段,**不新增 `teamNo`**;对外 `/admin/order/list``/admin/profile/orders` 返回 `teamNo: String|null`,user-service 在公开边界执行 `teamNo=groupCode`。后端已按此实现并合并。
---
## ⚠️ 关键变化
1. 非订单管理页面统一新增 canonical 字段 `teamNo: string | null`。它只表示真实团号:
- order-v3 来源为 `order_main.team_no`
- order-v2 来源为既有 `groupCode`
- 车务司机险来源为派单时真实团号快照;历史无值保持 `null`
2. **禁止 `teamNo || orderNo` 回退**`teamNo` 无值时前端统一显示 `-`(或既定空值占位),不得把 `orderNo``groupNo` 或其他编号伪装成团号。
3. `orderNo` 不删除:仍用于订单管理、内部关联、兼容展示和原有订单号搜索,但它不是团号。
4. 所有雪花 ID例如 `orderId``contractId``schemeId``taskId``assignmentId``driverId``requirementId``bizId`)按 JSON String 处理。前端不得转为 JavaScript `Number`,路由、行键和动作请求继续使用这些稳定 ID,不得使用 `teamNo` 替代。
5. 本文覆盖并纠正历史 `#5156` changelog 中“团号空值回退订单号”的建议:本次统一口径为**无真实团号即空值,不回退订单号**。
---
## 一、通用字段契约
| 字段 | JSON 类型 | 空值 | 用途 | 兼容规则 |
|------|-----------|------|------|----------|
| `teamNo` | `string \| null` | 未生成、历史无快照或下游降级时为 `null` | 页面团号展示、约定接口的团号搜索 | canonical 新字段;禁止从其他编号推导 |
| `orderNo` | `string \| null` | 依原接口 | 订单号展示、订单管理、内部关联及原有搜索 | 保留,不删除;不得作为团号回退 |
| `groupCode` | `string \| null` | 依原接口 | order-v2 兼容 | 保留;`teamNo` 的真实值取自该字段,但前端新代码读 `teamNo` |
| `groupNo` | `string \| null` | 依原接口 | 司机相关历史兼容 | 保留;司机详情新代码读 `teamNo` |
| 各类雪花 ID | `string``string \| null` | 依业务字段 | 路由、行键、详情和动作接口参数 | 禁止 `Number(id)`、数学运算或以 `teamNo` 替代 |
前端统一展示示例:
```ts
const visibleTeamNo = teamNo?.trim() || '-'
// 禁止teamNo?.trim() || orderNo?.trim() || '-'
```
---
## 变更接口
| # | 页面/接口 | 方法与路径 | 请求变化 | 响应变化 |
|---|-----------|------------|----------|----------|
| 1 | 管理后台工作台 | `GET /admin/profile/dashboard` | 无 | 各角色订单项补 `teamNo` |
| 2 | 定制师“我的订单” | `GET /admin/profile/orders` | `keyword` 增加团号模糊匹配 | `data.records[].teamNo` |
| 3 | 订单管理列表 | `GET /admin/order/list` | `keyword` 增加团号模糊匹配 | `data.records[].teamNo`;保留 `groupCode` |
| 4 | 合同列表 | `GET /v3/admin/contract/list` | 新增可选 query `teamNo`,模糊匹配 | `data.records[].teamNo`;合同/订单/方案 ID 为字符串 |
| 5 | 合同详情 | `GET /v3/admin/contract/<id>` | 无 | `data.teamNo`;相关雪花 ID 为字符串 |
| 6 | 评价列表 | `GET /v3/admin/review/list` | `keyword` 的 OR 搜索新增团号 | `data.records[].teamNo` |
| 7 | 评价详情 | `GET /v3/admin/review/<reviewId>` | 无 | `data.teamNo` |
| 8 | 司机险任务 | `GET /admin/fleet/insurance/tasks` | 新增可选 query `teamNo`,仅模糊匹配真实 `team_no` | `data.records[].teamNo`;历史空值为 `null` |
| 9 | 司机详情 | `GET /admin/fleet/drivers/<driverId>` | 无 | `data.relatedOrders[].teamNo`;保留 `groupNo` |
| 10 | 车务派单看板 | `GET /admin/fleet/board/orders` | 既有 `teamNo` 搜索只匹配真实团号,不再匹配订单号 | 既有 `teamNo` 保持;空值不回退 `orderNo` |
| 11 | 车务看板详情 | `GET /admin/fleet/board/orders/<orderId>` | 无 | 既有 `teamNo` 保持;空值不回退 `orderNo` |
| 12 | 车务矩阵 | `GET /admin/fleet/matrix/grid` | 团号搜索/展示遵循真实 `teamNo` | `assignments[].teamNo` 不回退 |
| 13 | 车务矩阵未派单 | `GET /admin/fleet/matrix/unassigned-orders` | 团号搜索/展示遵循真实 `teamNo` | `data[].teamNo` 不回退 |
| 14 | 车务矩阵单日清单 | `GET /admin/fleet/matrix/day-orders` | 团号搜索/展示遵循真实 `teamNo` | `data[].teamNo` 不回退 |
| 15 | 会话列表 | `GET /admin/message/chat/conversations` | 无 | `data.records[].teamNo``bizId``peerAdminId``requirementId` 为字符串 |
| 16 | 打开会话 | `POST /admin/message/chat/open``/open-house``/open-house-lead``/open-fleet` | 无 | `data.order.teamNo`;订单卡需求 ID 为字符串 |
| 17 | 房务选单池 | `GET /v3/admin/order/grab-pool/hotel-requirements` | `keyword` OR 搜索新增真实团号 | `data.records[].teamNo` |
| 18 | 房务我的/全部接单 | `GET /v3/admin/order/grab-pool/my-claims/hotel``/all-claims/hotel` | `keyword` OR 搜索新增真实团号 | `data.list[].teamNo` |
| 19 | 房务待办 | `GET /v3/admin/order/todos` | `keyword` 保留 title/reason OR 语义并增加真实团号 | `data.list[].teamNo` |
| 20 | 房务月度对账明细 | `GET /v3/admin/house/reconciliation/monthly/hotel-orders` | 无 | `data[].teamNo` |
内部 Feign 链路同步透传 `teamNo`,供 `/admin/profile/dashboard` 和管理端会话使用;前端不得直接调用 internal API。
> **契约裁决yst 2026-08-03,承接 #5372**`GET /internal/order/designer-list` 仅扩展既有 `keyword` 对真实 `groupCode` 的模糊搜索,**保留原响应字段,不新增 `teamNo`**;对外 `/admin/order/list``/admin/profile/orders` 返回 `teamNo: String|null`,user-service 在公开边界执行 `teamNo = groupCode`
---
## 三、接口详情与搜索口径
### 1. 工作台与我的订单
#### `GET /admin/profile/dashboard`
新增响应字段路径:
| 角色/区域 | 字段路径 | 类型 |
|-----------|----------|------|
| 管理员、定制师即将出行 | `data.upcomingTrips[].teamNo` | `string \| null` |
| 房务即将入住 | `data.upcomingTrips[].teamNo` | `string \| null` |
| 房务待办卡片 | `data.todoCards[].teamNo` | `string \| null` |
#### `GET /admin/profile/orders`
- `data.records[].teamNo: string | null`,真实值来自 order-v2 `groupCode`
- `keyword` 的 OR 搜索范围扩展为:订单号、团号、联系人姓名、产品名。
- 原响应 `groupCode` 保留兼容。
### 2. 订单管理列表 `GET /admin/order/list`
- 新增 `data.records[].teamNo: string | null`,值来自真实 `groupCode`
- 保留 `data.records[].groupCode`
- `keyword` 搜索规则:
- 订单号、团号、联系人姓名、产品名:模糊匹配;
- 完整手机号:精确匹配;
- 各条件保持 OR 语义。
示例字段值:
| 字段 | 示例 |
|------|------|
| `orderId` | `"2045390643479412737"` |
| `orderNo` | `"HL20260731123456"` |
| `groupCode` | `"26-0801"` |
| `teamNo` | `"26-0801"` |
### 3. 合同
#### `GET /v3/admin/contract/list`
新增 query
| 字段 | 类型 | 必填 | 规则 |
|------|------|------|------|
| `teamNo` | `string` | 否 | 模糊匹配 `order_main.team_no`;空白按未传处理 |
新增响应字段 `data.records[].teamNo: string | null`
#### `GET /v3/admin/contract/<id>`
新增响应字段 `data.teamNo: string | null`
下列 ID 以 JSON String 返回:`contractId``orderId``schemeId`;详情中的 `travelers[].travelerId` 及状态日志中的 `logId``contractId` 同样按字符串处理。`schemeId``travelerId` 等可空字段保持 `null`
#### ⚠️ v1/v3 切换口径2026-08-04 决策)
合同页**全量使用 v3 端点**,不再使用 v1
- 创建合同:`POST /v3/admin/contract/create``POST /v3/admin/contract/create-by-scheme`
- 刷新合同状态:`GET /v3/admin/contract/{id}/status`(实测成功)
- 列表、详情、作废、下载、重发短信:均走 `/v3/admin/contract/*`
v1`/admin/contract/*`)不再用于合同页:
- v1 存量历史合同(约 20 条,SIGNING/VOIDED自然消亡,**不迁移、不做兼容转换**;
- v1/v3 合同数据隔离不互通v3 创建的合同 `contractId` 不能调 v1 接口(返回错误码 `510001`;v1 创建的旧合同不会出现在 v3 列表/详情中。
前端合同页应统一按上述 v3 端点实现,移除 v1 合同调用,不得混用两套接口。
### 4. 评价
#### `GET /v3/admin/review/list`
- 新增 `data.records[].teamNo: string | null`
- `keyword` 保留原 `content``userNickname``orderNo``targetName` 的 OR 语义,并新增真实 `teamNo` 模糊匹配。
#### `GET /v3/admin/review/<reviewId>`
新增 `data.teamNo: string | null`。评价与订单等雪花 ID 继续按字符串消费。
### 5. 司机险任务 `GET /admin/fleet/insurance/tasks`
新增 query
| 字段 | 类型 | 必填 | 规则 |
|------|------|------|------|
| `teamNo` | `string` | 否 | 最长 64;trim 后模糊匹配任务真实 `team_no`,**不匹配 `order_no`** |
新增 `data.records[].teamNo: string | null`
- 新任务保存派单时的真实团号快照;
- 历史任务没有快照时返回 `null`,不伪造、不回填 `orderNo`
- `taskId``driverId``assignmentId``orderId``insuranceOrderId``handledBy` 均作为 JSON String可空字段允许 `null`)。
### 6. 司机详情 `GET /admin/fleet/drivers/<driverId>`
- `data.relatedOrders[].teamNo: string | null` 为 canonical 团号。
- `data.relatedOrders[].groupNo` 保留兼容。
- `teamNo` 无真实来源时返回 `null`,不从 `orderNo``groupNo` 推导。
### 7. 车务看板与矩阵
已有 `teamNo` 字段继续使用,但统一收紧:
- 看板 `teamNo` 查询仅匹配真实团号,不再把 `orderNo` 当团号命中;
- 页面可见团号只显示 `teamNo`;空值显示 `-`,不得回退 `orderNo`
- 看板、详情、矩阵的路由、行键、拖拽、派单、改派、取消等动作继续使用 `orderId``assignmentId``assignmentGroupId` 等字符串 ID;
- `orderNo` 可继续在明确标注“订单号”的区域展示,不得标成团号。
### 8. 管理端会话
- `GET /admin/message/chat/conversations``data.records[].teamNo: string | null`
- 四个打开会话接口:`data.order.teamNo: string | null`
- order-v3 不可达、订单无团号或历史摘要无值时,`teamNo` 返回 `null`,绝不回退 `orderNo`
- `bizId``peerAdminId``requirementId` 等雪花 ID 作为字符串处理。
### 9. 房务选单池、我的订单、待办与对账
- 选单池、我的接单、全部接单的 `keyword` 在原订单号/客人姓名/电话/产品名 OR 搜索基础上增加真实团号模糊匹配。
- 待办 `keyword` 保持 `title`/`reason` OR 搜索,并增加按真实团号预解析订单 ID;筛选在分页前生效。
- 相关列表项和月度对账酒店订单明细新增 `teamNo: string | null`
- 房务工作台通过 order-v3 → user-service Feign 链路透传同一字段;下游降级时字段保持 `null`
---
## 四、前端正确调用与展示
1. 页面展示团号只读 `teamNo`,空值显示 `-`;不要使用 `orderNo``groupCode``groupNo` 做运行时回退。
2. 订单管理已有代码可继续使用 `groupCode`,但新改页面统一迁移到 canonical `teamNo`
3. 明确标注“订单号”的区域可以继续显示 `orderNo`;“团号”区域不得混入订单号。
4. 搜索框按各接口契约传参:
- 合同、司机险使用独立 query `teamNo`
- profile orders、订单列表、评价、房务列表使用原 `keyword`
- 车务看板使用既有 `teamNo` 参数,但其语义已收紧为只查真实团号。
5. 所有雪花 ID 从响应到 store、路由参数、表格 row key 和动作 payload 全程保持字符串。禁止 `parseInt`、一元 `+``Number()` 或数值排序。
6. `frontend_status` 必须保持 `pending`;只有前端按自身流程完成、验证并回填合法引用后才能迁移状态,后端不得代填。
---
## 五、边界行为
- 真实团号未生成:`teamNo: null`,页面显示 `-`
- 历史司机险任务没有团号快照:`teamNo: null`,不做订单号回退。
- order-v3/Feign 降级:工作台或会话中 `teamNo` 可为 `null`,页面不得报错。
- 独立 `teamNo` 参数为空白:按未传处理。
- 团号关键词搜索在数据库分页前生效;不得仅过滤当前页。
- 原有 `orderNo``groupCode``groupNo` 及其他响应字段保持兼容。
- 本次为只读字段扩展和查询语义扩展,不改变订单状态机、合同状态机、评价审核、房务接单、车务派单和聊天权限。
---
## 六、展示与分页守恒
- 同一字符串 `orderId` 在工作台、合同、评价、车务、房务与聊天响应中的非空 `teamNo` 必须一致。
- 新字段与搜索扩展不增删业务状态,不改变状态标签、颜色、权限、排序主键或动作参数。
- 合同、评价、司机险、房务等分页列表必须在数据库分页前应用团号条件;`total` 与切页结果守恒,禁止仅过滤当前页。
- 房务 `todoCards``upcomingTrips` 中同一订单的 `teamNo` 必须一致;OPEN 聚合与历史分页均遵循相同真实团号来源。
---
## 七、不影响范围
- 不删除 `orderNo``groupCode``groupNo`
- 不改变任何写接口的稳定 ID 入参。
- 不授权前端直连 order-v3 internal API。
- 不代表后端已合并、网关已放行、测试服已部署或前端已适配。
- `D:/work2/hl-ui` 未修改;前端变更由前端 owner 独立完成。
---
## 验证证据(已完成项与剩余门禁)
- [x] PR #5386 已合并至 `dev-v3@07834e4c5`;合并前独立只读复审 P0/P1=0、唯一 P2profile/orders 镜像缺 displayStatus/displayStatusLabel/updateTime 透传)已修复并回归。
- [x] 全量验证通过order-v2 3496 tests、order-v3 7272 tests、user 3512 tests、fleet 2887 tests,全 0 failures;fleet spotless:check 通过;`FleetInsuranceTaskTeamNoMysqlTest` 以外部 MySQL 8 实跑 3/3。证据`D:/tmp/hl5368-fleet-verify-final2.log``D:/tmp/hl5368-user-test4.log`
- [x] 4 服务已部署 TESTorder-v2 task `03b0fecb`、order-v3 `2b54e87b`、user `258865cf`、fleet `d290d667`(均 success
- [x] 网关实测(`api.test.1814.love:9443`profile/orders 返回 `teamNo=groupCode` 且 keyword 团号搜索 total 正确;`/admin/order/list` 同;contract list 团号过滤分页前生效(精确 total=1/模糊 total=2且 detail 返回 teamNo;fleet insurance tasks 返回并可按 teamNo 查询(命中 4/不存在 0;fleet board teamNo 搜索仅匹配真实团号orderNo 片段不命中;chat 订单会话返回 teamNo、非订单会话 null;house 月度对账返回 teamNo;所有雪花 ID 均 JSON String;同一订单 2079576729147338754 在 contract 与月度对账均返回 `26-4165`。证据:`D:/tmp/hl5368-gateway-evidence/gateway-evidence.md`
- [ ] OpenAPI/oasdiff 与 Spring Cloud Contract 仍未配置(`not_configured`);已用上述生产者/消费者 JUnit 与真实网关 HTTP 作为人工回退证据。
- [ ] 测试环境 review 列表与 grab-pool 无业务数据total=0,团号 OR 搜索由 Mapper 单测覆盖,未做真数据命中验证。
- [ ] 前端完成展示、搜索、路由与动作回归后,由前端 owner 更新 `frontend_status`;当前保持 `pending`
---
## 八、相关文档
- 后端 Issue: [wx/HL#5368](https://git.1814.love:8443/wx/HL/issues/5368)
- 后端 Draft PR: [wx/HL#5386](https://git.1814.love:8443/wx/HL/pulls/5386)
- 前端交接 Issue: `wx/hl-api-changelog#64`
- 历史车务契约: `changelogs-v2/2026-07/85_5156_车务看板与矩阵主标识显示团号-前端待处理-管理后台.md`(其中团号空值回退订单号的建议被本次口径取代)

查看文件

@ -5,11 +5,7 @@ title: "车队独立管理及车队字典下线"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "implemented"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@ac4d5fd6292b13eb504f5393dfe07781800b9233"
updated_at: "2026-07-25T01:18:23.686Z"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-22T10:46:00+08:00"
---
@ -125,30 +121,10 @@ user-service 在“车务管理”目录下新增子菜单:
## 前端必须修改的范围
### 2026-07-24 页面复测反馈:列表列宽与暗色模式
测试环境 `/fleet/teams` 页面已经能展示负责人和脱敏电话,但当前样式仍需前端修正,本反馈不涉及后端接口或字段变化:
1. 表格列宽分配失衡。“车队名称”列占用过多空白,把“负责人 / 负责人电话”等核心联系人信息推到页面右侧,首屏信息密度过低。
2. 暗色模式不能只替换页面背景。当前筛选区、表头、行分隔线、空值、状态标签和操作区的层级与对比度不足,部分边界难以辨认。
3. 样式必须复用项目主题 token;禁止在本页写死仅适用于浅色模式的背景色、文字色或边框色。负责人电话仍只展示接口返回的脱敏值,样式调整不得绕过脱敏。
#### 展示矩阵
| 视口 / 主题 | 车队名称 | 负责人 / 负责人电话 | 其他列 | 验收表现 |
| --- | --- | --- | --- | --- |
| `>= 1440px`,浅色 | 弹性列,限制最大占比;超长省略并可查看完整名称 | 建议分别保留约 `120px / 140px`,左对齐 | 类型、付款方式、数量、排序、状态和操作按内容定宽 | 联系人紧邻业务字段,首屏无大段无意义空白 |
| `>= 1440px`,暗色 | 同浅色列宽规则 | 同浅色列宽规则 | 使用暗色主题 token | 页面、筛选区、表头、数据行、状态标签和操作区层级清楚 |
| `1024px - 1439px`,浅色/暗色 | 优先收缩并省略,不能无限占宽 | 不压缩为空或挤出主要阅读区 | 保留操作列可用宽度 | 联系人信息仍可直接阅读 |
| `< 1024px`,浅色/暗色 | 设置表格最小宽度 | 保持可读宽度 | 允许横向滚动 | 不通过隐藏关键列或强行挤压完成适配 |
空负责人和空电话统一显示 `—`。联系人文本左对齐;车辆数、排序、状态和操作居中。浅色与暗色模式都必须覆盖默认、悬停、聚焦、禁用和空数据状态;普通文本与背景建议至少达到 `4.5:1` 对比度,控件边界和状态提示应清晰可辨。
### 管理后台
1. 新增 `src/api/fleet/teams.js``src/views/fleet/teams/index.vue`,完成车队分页、新增、编辑、启停和删除:
- “车队管理”必须显示在“车务管理”目录内,不得作为一级菜单处理。
- 按上面的展示矩阵修正表格列宽和明暗主题样式,不能让“车队名称”列挤占联系人信息区域。
- 仅 `vehicleCount === 0` 时展示/启用删除动作;调用删除接口后刷新列表。
- 后端仍会独立校验车辆及未完结司机录入关联,返回 `601107` 时提示“请先完成车辆/司机转移”。
- 编辑历史迁入车队时补齐负责人、负责人电话、付款方式和排序。
@ -213,8 +189,6 @@ DELETE FROM sys_dict_type WHERE dict_type = 'fleet_attribution';
## 验收清单
- [ ] 独立车队菜单可分页、新增、编辑、启停,付款方式与资源页选项一致。
- [ ] `/fleet/teams` 在桌面端不再由“车队名称”列制造大段空白,负责人和脱敏电话位于首屏连续阅读区;窄屏按展示矩阵滚动而不是隐藏或挤压关键列。
- [ ] `/fleet/teams` 的浅色、暗色模式均使用主题 token,筛选区、表头、数据行、空值、状态标签和操作区在默认/悬停/聚焦/禁用状态下层级清晰。
- [ ] “车队管理”位于“车务管理”目录下;空车队可删除,非空车队删除入口禁用或明确提示后端 `601107`
- [ ] 历史迁入车队可通过编辑补齐负责人、负责人电话、付款方式和排序,保存时不允许提交空资料。
- [ ] 车辆新增/编辑/筛选/详情/导入均使用动态车队,不再出现固定三项。

查看文件

@ -135,17 +135,6 @@ POST /admin/fleet/assignments/{assignmentId}/confirm
- 等待态:“等待司机回复确认”
- 已登记态:“司机已确认接单,待车务确认执行”
### 2026-07-24 界面验收补充
当前“待确认”步骤中,“司机待确认通知 / 模板与预览均来自后端”标题区下方存在明显的
大块空白,导致模板选择行和消息预览整体下移。模板标签及消息正文已经正常显示,因此
这是前端布局问题,不是后端模板或渲染接口缺少数据。
- 移除标题区不必要的固定高度、最小高度或空占位,让高度由标题和副标题内容自然撑开。
- 标题区与模板选择行保持正常紧凑间距,不要为未来内容预留不可见空白。
- 常用桌面分辨率下,标题区底部到模板选择行的垂直空白不应超过 16px。
- 本项不新增接口、不调整字段,也不要为修复布局重新维护前端本地模板。
## 前端处理清单
- [ ] 模板列表来自后端 `hold_notify` 模板,默认选中 `isDefault=true`,不再使用前端假模板。
@ -156,7 +145,6 @@ POST /admin/fleet/assignments/{assignmentId}/confirm
- [ ] 无凭证时仍可成功登记司机确认并执行最终确认。
- [ ] `605025` 时提示“请先登记司机已确认接单”,不提示“缺少凭证”。
- [ ] 页面刷新后能按后端状态恢复待回复/已确认阶段。
- [ ] 修复“司机待确认通知”标题区异常留白,模板选择与消息预览紧凑衔接。
## 验证证据

查看文件

@ -1,9 +1,3 @@
---
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`

查看文件

@ -5,11 +5,7 @@ title: "派车按行程日标记车费日期"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "implemented"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@5eb8a46bee9a5a101371313e2088decd9ea843f2"
updated_at: "2026-07-25T03:03:38.490Z"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-22T18:00:00+08:00"
---

查看文件

@ -1,9 +1,3 @@
---
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,11 +5,7 @@ title: "用车手动加急与派车看板状态颜色"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "implemented"
frontend_status: "implemented"
frontend_owner: "hl-ui-codex"
frontend_ref: "mmg/hl-ui@d07506cd3aa8a23cb2aa90f891eb853e1b7dd13f"
updated_at: "2026-07-25T03:18:55.871Z"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-23T10:00:00+08:00"
---

查看文件

@ -5,14 +5,7 @@ title: "用车需求增加独立接机送机选择"
consumer: "admin"
backend: "verified"
gateway: "verified"
frontend: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "#5236 明确 supersedes #5193;85851ad6 已删除独立开关并改用实时大交通聚合,继续实现会回滚后续契约。"
updated_at: "2026-07-27"
frontend: "pending"
base: "dev-v3"
generated: "2026-07-23T17:39:06+08:00"
---

查看文件

@ -1,9 +1,3 @@
---
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

查看文件

@ -1,636 +0,0 @@
---
author: "yst(GIT)"
schema: "hl-changelog/v2"
ticket: "5380"
title: "核单人员页签收口与车辆空态契约"
consumer: "admin"
change_type: "删除接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "Pi"
frontend_ref: "v2.1@21dccf38d1413b098cfd5a456d3fb76611581ebb"
target_release: ""
verified_at: "2026-08-02"
status_note: "管理后台已仅保留导游、摄影师人员页签,删除领队/司机/其他人员 API 链路,并按 settlementReady/blockReasonCode 区分车辆两类空态;finalize 前重读权威 Step3。checkpoint 全量通过,业务提交 21dccf38d1413b098cfd5a456d3fb76611581ebb 已推送 origin/v2.1。网关有效登录态正向 curl 仍未完成,不标记 verified。"
updated_at: "2026-08-02"
base: "dev-v3"
---
# ⚠️【删除接口·管理后台】核单人员页签收口与车辆空态契约 (#5380)
> **PR**: #5393 | **服务**: order-v3 | **更新时间**: 2026-08-01
## 1. 接口背景
核单页面的人员费用仅保留导游、摄影师两类。领队、司机、其他人员不再作为核单人员费用页签,原有三组查询与保存接口同步删除。
车辆费用查询同时补齐两种空结果语义:订单没有当前用车需求时,空结果可以继续核单;订单有当前用车需求但车辆费用尚未就绪时,也返回成功空结果,并通过机器可读字段明确阻断原因。
## 变更接口清单
| # | 接口名 | 方法 | 路径 | 变更类型 | 说明 |
|---|--------|------|------|----------|------|
| 1 | 查询领队人员费用 | GET | `/v3/admin/order/:orderId/settlement/staff-fees/leaders` | 删除 | 不再提供领队核单 Tab 查询 |
| 2 | 全量替换领队人员费用 | PUT | `/v3/admin/order/:orderId/settlement/staff-fees/leaders` | 删除 | 不再提供领队核单 Tab 保存 |
| 3 | 查询司机人员费用 | GET | `/v3/admin/order/:orderId/settlement/staff-fees/drivers` | 删除 | 不再提供司机核单 Tab 查询 |
| 4 | 全量替换司机人员费用 | PUT | `/v3/admin/order/:orderId/settlement/staff-fees/drivers` | 删除 | 不再提供司机核单 Tab 保存 |
| 5 | 查询其他人员费用 | GET | `/v3/admin/order/:orderId/settlement/staff-fees/others` | 删除 | 不再提供其他人员核单 Tab 查询 |
| 6 | 全量替换其他人员费用 | PUT | `/v3/admin/order/:orderId/settlement/staff-fees/others` | 删除 | 不再提供其他人员核单 Tab 保存 |
| 7 | 查询车辆核单草稿 | GET | `/v3/admin/order/:orderId/settlement/step3/vehicles` | 修改 | 出参新增 `settlementReady``blockReasonCode`,并区分两种成功空结果 |
人员费用继续保留以下两组接口,路径和方法不变:
| Tab | 查询 | 保存 |
|-----|------|------|
| 导游 | `GET /v3/admin/order/:orderId/settlement/staff-fees/guides` | `PUT /v3/admin/order/:orderId/settlement/staff-fees/guides` |
| 摄影师 | `GET /v3/admin/order/:orderId/settlement/staff-fees/photographers` | `PUT /v3/admin/order/:orderId/settlement/staff-fees/photographers` |
## 3. 接口详情
### 3.1 删除:领队人员费用查询与保存
- **原接口名**:查询领队人员费用 / 全量替换领队人员费用
- **原方法与路径**
- `GET /v3/admin/order/:orderId/settlement/staff-fees/leaders`
- `PUT /v3/admin/order/:orderId/settlement/staff-fees/leaders`
- **使用场景**:已删除,不再用于核单页面。
- **认证**:原接口要求管理后台登录态;接口删除后,即使登录态有效也返回 HTTP 404。
- **幂等性**:不适用。
- **限流**:无接口级特殊限流。
**路径参数**
| 字段 | 类型 | 必填 | 说明 | 校验 |
|------|------|------|------|------|
| `orderId` | String(Long) | 是 | 订单 ID | 原校验为正整数;接口删除后不再进入参数校验 |
**请求体**
GET 无请求体。PUT 原有请求体不再接受;不得继续提交领队费用 `items`
**出参与错误码**
两个接口均无业务成功响应。任意订单调用均返回 HTTP 404,不能用其他人员费用路径替代领队路径。
| code | 含义 | 触发场景 |
|------|------|----------|
| `404` | 请求地址不存在 | 调用任一已删除的领队接口 |
**业务边界**
- 有效订单、历史订单和不存在的订单调用结果一致:路由不存在。
- 原有领队费用数据不构成前端可继续调用该接口的兼容理由。
**示例GET 已删除**
请求:
```http
GET /v3/admin/order/2080000000000000001/settlement/staff-fees/leaders
Authorization: Bearer JWT_TOKEN
```
无请求体。
响应:
```json
{
"code": 404,
"message": "请求地址不存在",
"data": null,
"success": false
}
```
**示例PUT 已删除**
请求:
```http
PUT /v3/admin/order/2080000000000000001/settlement/staff-fees/leaders
Authorization: Bearer JWT_TOKEN
Content-Type: application/json
```
```json
{
"items": []
}
```
响应:
```json
{
"code": 404,
"message": "请求地址不存在",
"data": null,
"success": false
}
```
### 3.2 删除:司机人员费用查询与保存
- **原接口名**:查询司机人员费用 / 全量替换司机人员费用
- **原方法与路径**
- `GET /v3/admin/order/:orderId/settlement/staff-fees/drivers`
- `PUT /v3/admin/order/:orderId/settlement/staff-fees/drivers`
- **使用场景**:已删除,不再用于核单页面;车辆费用继续使用 §3.4 的车辆核单草稿查询。
- **认证**:原接口要求管理后台登录态;接口删除后,即使登录态有效也返回 HTTP 404。
- **幂等性**:不适用。
- **限流**:无接口级特殊限流。
**路径参数**
| 字段 | 类型 | 必填 | 说明 | 校验 |
|------|------|------|------|------|
| `orderId` | String(Long) | 是 | 订单 ID | 原校验为正整数;接口删除后不再进入参数校验 |
**请求体**
GET 无请求体。PUT 原有请求体不再接受;不得继续提交司机费用 `items`
**出参与错误码**
两个接口均无业务成功响应。任意订单调用均返回 HTTP 404。司机人员费用接口与车辆核单草稿接口不是同一路由,不得改路径尾段后继续提交原司机费用请求体。
| code | 含义 | 触发场景 |
|------|------|----------|
| `404` | 请求地址不存在 | 调用任一已删除的司机接口 |
**业务边界**
- 有效订单、历史订单和不存在的订单调用结果一致:路由不存在。
- 车辆费用只读取 `/settlement/step3/vehicles` 的契约;已删除司机接口不再提供车辆费用补充入口。
**示例GET 已删除**
请求:
```http
GET /v3/admin/order/2080000000000000001/settlement/staff-fees/drivers
Authorization: Bearer JWT_TOKEN
```
无请求体。
响应:
```json
{
"code": 404,
"message": "请求地址不存在",
"data": null,
"success": false
}
```
**示例PUT 已删除**
请求:
```http
PUT /v3/admin/order/2080000000000000001/settlement/staff-fees/drivers
Authorization: Bearer JWT_TOKEN
Content-Type: application/json
```
```json
{
"items": []
}
```
响应:
```json
{
"code": 404,
"message": "请求地址不存在",
"data": null,
"success": false
}
```
### 3.3 删除:其他人员费用查询与保存
- **原接口名**:查询其他人员费用 / 全量替换其他人员费用
- **原方法与路径**
- `GET /v3/admin/order/:orderId/settlement/staff-fees/others`
- `PUT /v3/admin/order/:orderId/settlement/staff-fees/others`
- **使用场景**:已删除,不再用于核单页面。
- **认证**:原接口要求管理后台登录态;接口删除后,即使登录态有效也返回 HTTP 404。
- **幂等性**:不适用。
- **限流**:无接口级特殊限流。
**路径参数**
| 字段 | 类型 | 必填 | 说明 | 校验 |
|------|------|------|------|------|
| `orderId` | String(Long) | 是 | 订单 ID | 原校验为正整数;接口删除后不再进入参数校验 |
**请求体**
GET 无请求体。PUT 原有请求体不再接受;不得继续提交其他人员费用 `items`
**出参与错误码**
两个接口均无业务成功响应。任意订单调用均返回 HTTP 404,不能用导游或摄影师路径承载其他人员费用。
| code | 含义 | 触发场景 |
|------|------|----------|
| `404` | 请求地址不存在 | 调用任一已删除的其他人员接口 |
**业务边界**
- 有效订单、历史订单和不存在的订单调用结果一致:路由不存在。
- “其他人员”和“其他支出”是不同契约;本次删除不改变其他支出接口。
**示例GET 已删除**
请求:
```http
GET /v3/admin/order/2080000000000000001/settlement/staff-fees/others
Authorization: Bearer JWT_TOKEN
```
无请求体。
响应:
```json
{
"code": 404,
"message": "请求地址不存在",
"data": null,
"success": false
}
```
**示例PUT 已删除**
请求:
```http
PUT /v3/admin/order/2080000000000000001/settlement/staff-fees/others
Authorization: Bearer JWT_TOKEN
Content-Type: application/json
```
```json
{
"items": []
}
```
响应:
```json
{
"code": 404,
"message": "请求地址不存在",
"data": null,
"success": false
}
```
### 3.4 修改:查询车辆核单草稿
- **接口名**:查询车辆核单草稿
- **方法与路径**`GET /v3/admin/order/:orderId/settlement/step3/vehicles`
- **使用场景**:查询订单当前车辆核单明细及车辆费用是否已具备核单条件。
- **认证**:需要管理后台登录态并满足订单查看权限;房务角色不可访问。
- **幂等性**:是,只读查询。
- **限流**:无接口级特殊限流。
**路径参数**
| 字段 | 类型 | 必填 | 说明 | 校验 |
|------|------|------|------|------|
| `orderId` | String(Long) | 是 | 订单 ID | 正整数 |
无 Query 参数、无请求体。
**统一响应外层**
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务码;成功为 `200` |
| `message` | String | 结果说明 |
| `data` | Object/null | 成功时为车辆核单草稿;失败时为 `null` |
| `traceId` | String/null | 链路追踪 ID,未返回时可为空 |
| `success` | Boolean | `code=200` 时为 `true` |
**成功响应 `data`**
| 字段 | 类型 | 可空 | 说明 |
|------|------|------|------|
| `orderId` | String(Long) | 否 | 订单 ID,按字符串返回 |
| `version` | Long | 否 | 车辆核单草稿版本;空结果为 `0` |
| `totalAmount` | Decimal | 否 | 当前全部车辆明细金额合计;空结果为 `0.00` |
| `allConfirmed` | Boolean | 否 | 当前明细是否全部已确认;有需求但费用未就绪的空结果为 `false` |
| `settlementReady` | Boolean | 否 | 车辆费用是否已具备核单条件;本次新增公开字段 |
| `blockReasonCode` | String | 是 | 不具备核单条件时的机器可读原因;可核单时为 `null` |
| `items` | Array | 否 | 当前车辆费用全量明细;无明细时为 `[]` |
**`data.items[]`**
| 字段 | 类型 | 可空 | 说明 |
|------|------|------|------|
| `id` | String(Long) | 否 | 车辆核单明细 ID |
| `sourceType` | String | 否 | 来源编码,见 §6.1 |
| `sourceTypeName` | String | 否 | 来源名称 |
| `serviceDate` | String(date) | 否 | 服务日期,格式 `YYYY-MM-DD` |
| `vehicleId` | String(Long) | 是 | 车辆 ID |
| `vehiclePlate` | String | 是 | 车牌号 |
| `vehicleModelId` | String(Long) | 是 | 车型 ID |
| `vehicleModelName` | String | 是 | 车型名称 |
| `driverId` | String(Long) | 是 | 司机 ID |
| `driverName` | String | 是 | 司机姓名 |
| `amount` | Decimal | 否 | 核单金额 |
| `paymentMethod` | String | 否 | 付款方式编码,见 §6.2 |
| `paymentMethodName` | String | 否 | 付款方式名称 |
| `settlementConfirmStatus` | String | 否 | 核单确认状态编码,见 §6.3 |
| `settlementConfirmStatusName` | String | 否 | 核单确认状态名称 |
| `remark` | String | 是 | 备注 |
| `voucherUrls` | String[] | 否 | 凭证 URL;无凭证时为 `[]` |
**错误码**
| code | 含义 | 触发场景 |
|------|------|----------|
| `400` | 请求参数错误 | `orderId` 不是正整数 |
| `403` | 无访问权限 | 登录态或角色无权访问该接口 |
| `581007` | 订单不存在 | `orderId` 对应订单不存在 |
| `584071` | 无权访问该订单 | 当前账号不在订单可访问范围内 |
| `584100` | 车辆费用暂时不可用 | 车辆费用来源调用失败、响应身份不匹配或必要字段无效 |
| `584101` | 车辆费用尚未满足核单条件 | 已返回非空车辆明细,但存在未完结或未满足费用条件的明细 |
`584102` 不再用于本 GET 的“当前需求存在但车辆费用尚未生成”场景;该场景改为 `code=200` 的空结果,见下方示例。
**业务边界与判定表**
| 场景 | `items` | `totalAmount` | `settlementReady` | `blockReasonCode` | `allConfirmed` | 结果 |
|------|---------|---------------|-------------------|-------------------|----------------|------|
| 无当前用车需求 | `[]` | `0.00` | `true` | `null` | `true` | 成功,可继续完成核单 |
| 有当前用车需求,但车辆费用尚未生成 | `[]` | `0.00` | `false` | `VEHICLE_FEE_NOT_READY` | `false` | 成功,但不能完成核单 |
| 有明细且来源已就绪,仍有行未确认 | 非空 | 合计金额 | `true` | `null` | `false` | 成功,需先完成明细确认 |
| 有明细且来源已就绪,所有行已确认 | 非空 | 合计金额 | `true` | `null` | `true` | 成功,可继续完成核单 |
- `items=[]` 不是失败判据,必须结合 `settlementReady` 判断。
- `allConfirmed=true` 只表示没有未确认行;是否具备核单条件仍以 `settlementReady` 为准。
- 车辆费用来源调用失败或明细必要字段无效仍返回业务错误,不转换为空结果。
**示例 1典型成功,有已确认车辆明细**
请求:
```http
GET /v3/admin/order/900000000001/settlement/step3/vehicles
Authorization: Bearer JWT_TOKEN
```
无请求体。
响应:
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000001",
"version": 4,
"totalAmount": 1200.00,
"allConfirmed": true,
"settlementReady": true,
"blockReasonCode": null,
"items": [
{
"id": "930000000001",
"sourceType": "FLEET",
"sourceTypeName": "车务",
"serviceDate": "2026-08-01",
"vehicleId": "880000000001",
"vehiclePlate": "藏A12345",
"vehicleModelId": "870000000001",
"vehicleModelName": "七座商务车",
"driverId": "860000000001",
"driverName": "张师傅",
"amount": 1200.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "金额已核对",
"voucherUrls": []
}
]
},
"traceId": null,
"success": true
}
```
**示例 2边界成功,无当前用车需求**
请求:
```http
GET /v3/admin/order/900000000002/settlement/step3/vehicles
Authorization: Bearer JWT_TOKEN
```
无请求体。
响应:
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000002",
"version": 0,
"totalAmount": 0.00,
"allConfirmed": true,
"settlementReady": true,
"blockReasonCode": null,
"items": []
},
"traceId": null,
"success": true
}
```
**示例 3边界成功,有当前需求但车辆费用尚未就绪**
请求:
```http
GET /v3/admin/order/900000000003/settlement/step3/vehicles
Authorization: Bearer JWT_TOKEN
```
无请求体。
响应:
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000003",
"version": 0,
"totalAmount": 0.00,
"allConfirmed": false,
"settlementReady": false,
"blockReasonCode": "VEHICLE_FEE_NOT_READY",
"items": []
},
"traceId": null,
"success": true
}
```
**示例 4业务失败,车辆费用来源暂时不可用**
请求:
```http
GET /v3/admin/order/900000000004/settlement/step3/vehicles
Authorization: Bearer JWT_TOKEN
```
无请求体。
响应:
```json
{
"code": 584100,
"message": "车务车辆总车费暂时不可用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
```
## 6. 枚举 / 数据字典
### 6.1 `sourceType`
**所属字段**`data.items[].sourceType` **类型**String
| 值 | 中文 | 说明 |
|----|------|------|
| `FLEET` | 车务 | 车辆费用来源于当前车辆安排 |
| `MANUAL` | 手工 | 手工维护的车辆核单明细 |
### 6.2 `paymentMethod`
**所属字段**`data.items[].paymentMethod` **类型**String
| 值 | 中文 | 说明 |
|----|------|------|
| `CASH_PAID` | 现金已付 | 现金支付 |
| `SIGNED` | 签单 | 按签单方式结算 |
| `COMPANY_PAID` | 公司付款 | 由公司支付 |
### 6.3 `settlementConfirmStatus`
**所属字段**`data.items[].settlementConfirmStatus` **类型**String
| 值 | 中文 | 说明 |
|----|------|------|
| `UNCONFIRMED` | 未确认 | 当前车辆费用行尚未完成核单确认 |
| `CONFIRMED` | 已确认 | 当前车辆费用行已完成核单确认 |
### 6.4 `blockReasonCode`
**所属字段**`data.blockReasonCode` **类型**String/null
| 值 | 中文 | 说明 |
|----|------|------|
| `VEHICLE_FEE_NOT_READY` | 车辆费用尚未就绪 | 有当前用车需求,但尚无可返回的车辆费用明细;此时 `settlementReady=false` |
| `null` | 无阻断原因 | 此时 `settlementReady=true``null` 是空值,不是字符串 `"null"` |
## 验证证据
- PR #5393 已合并至 `dev-v3`,合并提交为 `e4c1720871f33db38936d709caa7696db199ad1f`
- 部署任务 `e6720666` 构建成功,按 8186→8086 完成滚动,两个实例均为 UP。
- 测试服管理后台真实页面成功读取 9 条 `FLEET` 车辆费用,未再出现旧的车辆空数据错误。
- 测试服 8086 OpenAPI 已确认仅保留 guides、photographers 两组人员费用接口;leaders、drivers、others 六个路由不存在,车辆响应包含 `settlementReady``blockReasonCode`
- 网关 curl 已确认路由可达,但旧 JWT 返回业务 401;逐接口正向网关 curl 因有效登录态缺失而阻断。
- **验收结论TARGETED_FALLBACK / PARTIAL**。已确认部署、双实例、页面车辆数据和服务 OpenAPI 契约;未完成带有效登录态的逐接口网关正向验证,不能描述为 Full E2E 或网关全量 `verified`
- PR 自动化记录:受影响测试 513 项通过,新增规格测试 116 项通过;模块全量 7198 项中 7166 项通过、31 项跳过、1 项失败,唯一失败为既有迁移版本重复问题。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| 车辆响应 `data.settlementReady` | 不对管理后台输出 | 新增 `Boolean`,明确车辆费用是否具备核单条件 |
| 车辆响应 `data.blockReasonCode` | 不存在 | 新增 `String/null`,不可核单时返回机器可读原因 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 核单人员 Tab | 领队、司机、导游、摄影师、其他人员共 5 个 | 仅保留导游、摄影师 2 个 |
| 领队人员费用 GET/PUT | 可查询、保存 | 路由删除,调用返回 HTTP 404 |
| 司机人员费用 GET/PUT | 可查询、保存 | 路由删除,调用返回 HTTP 404 |
| 其他人员费用 GET/PUT | 可查询、保存 | 路由删除,调用返回 HTTP 404 |
| 无当前用车需求 | 返回空明细,但响应未公开就绪原因字段 | 成功返回空明细,`settlementReady=true``blockReasonCode=null` |
| 有当前需求但车辆费用尚未生成 | GET 返回 `584102`,页面无法取得可判定空态 | 成功返回空明细,`settlementReady=false``blockReasonCode=VEHICLE_FEE_NOT_READY` |
| 车辆来源失败或明细无效 | 返回业务错误 | 仍返回业务错误,不伪装成空结果 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:是。领队、司机、其他人员共 6 个接口已删除。
- **前端是否必须同步上线**:是。管理后台必须移除这 3 个 Tab 及其查询、保存调用,仅保留导游、摄影师 Tab。
- **车辆字段兼容性**:新增字段本身为向后兼容;若仍沿用 `items=[]` 或捕获 `584102` 判断空态,将无法区分“无需求”和“费用未就绪”。
### 11.2 回滚边界
- 前端版本不得回滚到仍调用 leaders、drivers、others 六个路由的版本,否则对应页面请求固定失败。
- 若前端暂时不使用车辆新增字段,JSON 仍可解析,但不能可靠判断空结果是否允许完成核单。
## 12. 注意事项
- 删除领队、司机、其他人员 3 个核单 Tab 及其 GET/PUT 请求封装、请求状态和保存动作。
- 保留导游 `guides`、摄影师 `photographers` 两个 Tab,原路径不变。
- 车辆查询返回 `code=200``items=[]` 时,不得直接当作异常或无条件放行;必须读取 `settlementReady`
- 完成核单前同时检查 `settlementReady``allConfirmed`,不能只判断明细数组是否为空。
- 清理 GET 车辆费用遇到 `584102` 时的空态兼容逻辑;新的“有需求但费用未就绪”结果由 `blockReasonCode=VEHICLE_FEE_NOT_READY` 表达。
- `orderId`、车辆明细 ID、车辆 ID、车型 ID、司机 ID 均按字符串处理;金额按 Decimal 处理。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5380](https://git.1814.love:8443/wx/HL/issues/5380)
- **PR**: [#5393](https://git.1814.love:8443/wx/HL/pulls/5393)
- **Merge commit**: [e4c1720871f33db38936d709caa7696db199ad1f](https://git.1814.love:8443/wx/HL/commit/e4c1720871f33db38936d709caa7696db199ad1f)
### 13.2 联系人
- **后端负责人**: @yst
## 关联/联系人
### 链接
- [后端工单 #5380](https://git.1814.love:8443/wx/HL/issues/5380)
### 联系人
- **后端负责人**: @yst

查看文件

@ -1,224 +0,0 @@
---
author: "wx(GIT)"
schema: "hl-changelog/v2"
ticket: "5374"
title: "保险任务与派单候选补齐真实团号契约"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@193575171d817f1042040baf8e0d9d28e7206048"
target_release: "v2.1"
verified_at: "2026-08-03"
status_note: "后端 PR #5415 已合并为 dev-v3@ee7ad816b;测试部署任务 23928056 双实例健康,保险任务与派单候选经网关真登录验证,Flyway 001/002、nullable VARCHAR(32)、回填收敛和普通 EXPLAIN 均通过;管理后台已由 Pi 领取,正在适配。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# Fleet保险任务与派单候选补齐真实团号契约
> **服务**: hl-fleet-service (端口 8087)
> **PR**: [wx/HL#5415](https://git.1814.love:8443/wx/HL/pulls/5415)
> **Issue**: [wx/HL#5374](https://git.1814.love:8443/wx/HL/issues/5374)
> **日期**: 2026-08-02
> **影响范围**: 管理后台车务保险任务列表与派单候选冲突提示
---
## 一、关键变化
- 保险任务列表新增独立 `teamNo` 团号 contains 查询,并在任务项返回真实 `teamNo`
- 派单候选的车辆、司机冲突快照新增真实 `teamNo`
- 无团号统一返回 `null`,**禁止回退或伪装为 `orderNo`**;现有订单号和雪花 ID 字符串序列化契约不变。
---
## 变更接口
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 司机险任务/流水分页 | GET | `/admin/fleet/insurance/tasks` | 请求与响应字段新增 | 新增 `teamNo` 查询及任务团号 |
| 2 | 派单车辆/司机候选 | POST | `/admin/fleet/assignments/candidates` | 嵌套响应字段新增 | 两类 `conflicts[]` 新增真实团号 |
---
## 三、接口详情
### 1. 司机险任务/流水分页 `GET /admin/fleet/insurance/tasks`
**请求 VO**: `FleetInsuranceTaskPageReqVO`
#### 新增入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `teamNo` | Query | String | 否 | contains 模糊匹配 | 只搜索团号;不会 OR 匹配订单号;过滤在分页前完成 |
#### 新增出参 `Result<PageResult<FleetInsuranceTaskRespVO>>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.records[].teamNo` | `String|null` | 保险任务冻结的真实团号快照;无团号返回 `null` |
请求示例:
```http
GET /admin/fleet/insurance/tasks?teamNo=2607&page=1&pageSize=20
```
响应字段示例:
```json
{
"code": 200,
"data": {
"records": [
{
"taskId": "9007199254740993",
"orderId": "9007199254740995",
"orderNo": "26-0701",
"teamNo": "HL-2607-001"
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"success": true
}
```
### 2. 派单车辆/司机候选 `POST /admin/fleet/assignments/candidates`
**响应 VO**: `AssignmentCandidateRespVO.ConflictVO`
#### 新增出参
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.vehicles.records[].conflicts[].teamNo` | `String|null` | 冲突派单的 Fleet 本地真实团号快照 |
| `data.vehicles.list[].conflicts[].teamNo` | `String|null` | `records` 的兼容序列化别名,字段一致 |
| `data.drivers.records[].conflicts[].teamNo` | `String|null` | 冲突派单的 Fleet 本地真实团号快照 |
| `data.drivers.list[].conflicts[].teamNo` | `String|null` | `records` 的兼容序列化别名,字段一致 |
响应片段:
```json
{
"vehicles": {
"records": [
{
"vehicleId": "101",
"conflicts": [
{
"assignmentId": "9001",
"assignmentGroupId": "9101",
"orderNo": "26-0701",
"teamNo": "HL-2607-001",
"blocking": true,
"reasonCode": "ASSIGNMENT_CONFLICT"
}
]
}
]
},
"drivers": {
"records": [
{
"driverId": "201",
"conflicts": [
{
"assignmentId": "9001",
"assignmentGroupId": "9101",
"orderNo": "26-0701",
"teamNo": null,
"blocking": true,
"reasonCode": "ASSIGNMENT_CONFLICT"
}
]
}
]
}
}
```
---
## 四、契约约束与正确调用方式
- 前端展示团号必须读取 `teamNo`;值为 `null` 时展示 `-`
- 不得把 `orderNo` 当作团号回退值。
- `orderNo``orderId``assignmentId``assignmentGroupId` 继续保留;雪花 ID 继续按 JSON String 返回,用于路由和动作。
- `teamNo` 只增加展示与查询能力,不改变候选 `available``blocking`、同城首尾衔接、可用窗口或派单写侧锁内复核。
- 保险任务查询的 `teamNo` 是独立参数,不沿用派车看板兼容匹配订单号的既有语义。
---
## 五、数据库行为
- `fleet_insurance_task.team_no VARCHAR(32) NULL``fleet_assignment.team_no` 对齐。
- nullable DDL 与历史回填 DML 使用两个独立 Flyway 版本。
- 历史数据仅按 `assignment_id``fleet_assignment.team_no` 回填。
- 无匹配派单、来源团号为空或任务已有非空团号时保持原值;不跨服务查询、不按订单号猜测。
### 生产两阶段迁移门禁(本单未授权、未执行生产)
1. 第一阶段必须设置 `SPRING_FLYWAY_TARGET=20260802.001`,只允许 nullable DDL 落库;禁止携带默认 `latest` 直接滚动发布。
2. 完成 Fleet Java 全量滚动并确认所有旧实例退出;此时新写入已冻结 `teamNo`,不会再产生旧代码造成的新增空快照。
3. 重新只读统计待回填数、可匹配数并执行普通 `EXPLAIN`;影响行数 ≤ 50k、扫描行数 ≤ 200k、staging 单事务 ≤ 5s、锁等待 ≤ 1s、复制延迟增量 ≤ 2s 才可继续。
4. 第二阶段才移除 target 或设置 `SPRING_FLYWAY_TARGET=20260802.002`,由受控单实例执行幂等 DML,再核验 Flyway history 与剩余空值。
5. 任一阈值超限或证据缺失即停止第二阶段,改用 1k–5k 行受控分批;生产禁止 `EXPLAIN ANALYZE UPDATE`
---
## 六、边界行为
- `teamNo` 完整或部分关键词均按 contains 命中。
- 只有 `orderNo` 含关键词而 `teamNo` 不含时,不命中保险任务列表。
- 历史任务或冲突派单没有团号时,响应字段为 `null`,接口不异常。
- 空 `teamNo` 等同不启用该筛选;其他现有筛选条件与其按 AND 组合。
---
## 七、不影响范围
- 不修改派车矩阵、甘特、车务工作台或派车看板字段与搜索语义。
- 不修改司机档案 `relatedOrders`
- 不新增 order-v3 Feign,不修改 `hl-common-feign`、内部接口或共享 Java DTO。
- 不修改 `hl-ui`
---
## 验证证据
- 定向测试226 项通过,0 failures、0 errors、0 skipped。
- Fleet Spotless通过。
- JaCoCo changed executable lines9/9100%)。
- 独立 Reviewer(max):无 P0–P2。
- oasdiff项目缺少可复现 Swagger 2 → OAS3 转换链,标记 `not_configured`;已完成源码级手工契约对比。
- MySQL 8.0.33 隔离实例:`FleetInsuranceTaskTeamNoMysqlTest` 3/3 通过,0 skipped;H2 全迁移回归 `FleetInsuranceTaskMigrationTest` 8/8 通过。
- Fleet 全模块 `verify` 已执行 2764 项;接入本单隔离 Redis 后相关 4 项通过且无 key 残留,当前仅余 3 项基线失败Release E Windows 时序 2 项、保险并发夹具 1 项,后者已在未含本单改动的 `dev-v3@a999a3df7` 同样复现),未将该次命令记为通过。
- 测试部署及网关真实 round-trip 结果将在后端完成后更新本文件元数据与本节。
---
## 九、前端消费动作
1. 保险任务列表筛选增加 `teamNo` 参数,并按 `data.records[].teamNo` 展示团号。
2. 派单候选的车辆/司机冲突提示改用各自 `conflicts[].teamNo` 展示团号。
3. `teamNo === null` 时展示 `-`,不得显示 `orderNo` 作为替代团号。
4. 保留现有 ID 与订单号用于路由、动作及兼容逻辑。
## 关联 / 联系人
### 链接
- **Issue**: [#5374](https://git.1814.love:8443/wx/HL/issues/5374)
- **PR**: [#5415](https://git.1814.love:8443/wx/HL/pulls/5415)
- **Merge commit**: [ee7ad816b0](https://git.1814.love:8443/wx/HL/commit/ee7ad816b0)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,85 +0,0 @@
---
author: "wx(GIT)"
schema: "hl-changelog/v2"
ticket: "5336"
title: "修改派单 requestId 持久幂等:同键重放冻结结果、不同载荷 605059"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@49f38c7cfb4d5859a13c6ab1a7fea540cb524de5"
target_release: "v2.1"
verified_at: "2026-08-04"
status_note: "PR #5427 已 squash 合并到 dev-v3@d4243a2f6;测试部署任务 111419d8 双实例健康;网关真实 change 验证首次 200、同键同载荷重放冻结结果、同键异载荷 605059;管理后台已由 Pi 领取并开始适配。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 修改派单 requestId 持久幂等:同键重放冻结结果、不同载荷 605059
> **服务**`hl-fleet-service`
>
> **Issue**[#5336](https://git.1814.love:8443/wx/HL/issues/5336)
>
> **影响范围**:管理后台「修改派单」接口的幂等语义与错误码
---
## ⚠️ 关键变化
- 以前:`change` 接口对 `requestId` 只有 Redis 10 秒快速防重;TTL 到期后**同一 requestId + 相同 payload 重放会再次执行改派**(取消旧切片、生成新 group/assignment ID 与生命周期副作用)。
- 现在:`(operationType=CHANGE, requestId)` 在数据库持久唯一;成功后同键同 payload 重放**直接返回首次冻结结果,不再产生任何改派副作用**;同键不同 payload 返回 `605059`
- 前端/调用方以前以为的重放安全不再成立,**重试安全成为持久承诺**;任何新 `requestId` 的合法再次改派不受影响。
## 变更接口
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 修改派单 | POST | `/admin/fleet/assignments/:assignmentId/change` | 错误码与幂等语义 | 无请求/响应字段变化 |
## 变更详情
### 3.1 幂等语义(行为变化)
- 请求体字段(含 `requestId`)与响应字段**均不变**。
- 首次成功后,相同 `requestId` + 相同业务载荷(规范化后)重放:返回首次冻结的完整响应(同 `assignmentId/assignmentSlotId/newAssignmentGroupId/...` 等),**不**生成新的 group/assignment、OperationLog、Outbox 或生命周期 intent。
- Redis 10 秒快速防重仍保留,仅承担首次并发抑制,不再承担业务正确性;DB 唯一回执跨 TTL 兜底。
- 同 `requestId` + 不同业务载荷:稳定失败,返回:
| code | 含义 |
|---|---|
| `605059` | 幂等请求标识已用于不同业务载荷 |
- `requestId` 大小写精确区分;首尾空白或超过 64 字符返回参数错误。
- 新 `requestId` 的再次合法改派不受阻,正常形成新业务变更。
## 前端消费动作
1. 正常业务无需改动;重试逻辑可依赖持久幂等(同键同载荷重复提交不再重复改派)。
2. 若收到 `605059`,说明同一 `requestId` 被用于不同内容,必须更换新 `requestId` 重新提交,不得原地重试。
3. 前端双击/重放场景:响应内容与首次一致,无需重新拉取详情。
## 验证证据
- Fleet H2 集成:`AssignmentChangeReceiptServiceIntegrationTest` 8/8、迁移测试 2/2。
- 真实 MySQL 8.0.33 + Redis + Spring AOP`AssignmentChangeReceiptMysqlTest` 1/1含跨 TTL 并发 exactly-once
- `AssignmentServiceTest` 340/340;Fleet 全量 verify 结果以 PR 合入后工单回写为准。
- oasdiff 与 Spring Cloud Contract 未配置,不能标记工具 PASS;以上为源码级字段对比与集成测试回退证据。
## Internal Feign / shared Java
无内部 Feign 或共享 Java 契约变化。
## 关联 / 联系人
### 链接
- **Issue**: [#5336](https://git.1814.love:8443/wx/HL/issues/5336)
- **PR**: [#5427](https://git.1814.love:8443/wx/HL/pulls/5427)
- **Merge commit**: [d4243a2f65eb](https://git.1814.love:8443/wx/HL/commit/d4243a2f65eb)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,187 +0,0 @@
---
author: "wx(GIT)"
schema: "hl-changelog/v2"
ticket: "5364"
title: "连续执行段司机通知、回复与整组确认隔离"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@6e4c6b224b204cd792e02f29a6e61689b0d9a588"
target_release: "v2.1"
verified_at: "2026-08-03"
status_note: "后端 PR #5420 已 squash 合并到 dev-v3e543a41035;最终本地 Fleet 2820 项与 Order 7261 项 reactor verify 均通过。本工单冻结范围不要求运行时部署或网关验证;管理后台已由 Pi 领取,正在适配。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 连续执行段司机通知、回复与整组确认隔离(#5364
> **服务**`hl-fleet-service``hl-order-service-v3`
>
> **后端 PR**[#5420](https://git.1814.love:8443/wx/HL/pulls/5420)
>
> **Backend Issue**[#5364](https://git.1814.love:8443/wx/HL/issues/5364)
>
> **Frontend tracking**[#5366](https://git.1814.love:8443/wx/HL/issues/5366)
## 变更接口
| 接口 | 方法 | 路径 | 变更类型 |
|---|---|---|---|
| 查询车务派车订单详情 | `GET` | `/admin/fleet/board/orders/:orderId` | additive 响应字段 |
| 登记司机确认 | `POST` | `/admin/fleet/assignments/:assignmentId/driver-confirmation` | additive 请求字段与段级约束 |
| 用车需求整组原子确认 | `POST` | `/admin/fleet/assignments/requirements/:requirementId/confirm` | 新增接口 |
| 旧单派车确认 | `POST` | `/admin/fleet/assignments/:assignmentId/confirm` | multi-group 行为收紧,singleton 兼容 |
| 司机 H5 行程单 JSON | `GET` | `/app/h5/itinerary/:token` | additive/versioned 响应字段与段级裁剪 |
## 1. Board 详情字段
`GET /admin/fleet/board/orders/:orderId` 的既有字段保持不变,新增字段均为 additive/null-safe。
### 1.1 顶层需求快照
| 字段 | JSON 类型 | 可空 | 说明 |
|---|---|---|---|
| `requirementId` | `String(Long)` | 是 | 当前有效用车需求 ID |
| `requirementVersion` | `Integer` | 是 | 当前需求版本 |
| `requirementSha256` | `String` | 是 | 当前需求 canonical SHA-256;整组确认时原样回传 |
| `driverConfirmationSummary` | `Object` | 否 | 当前 generation 的服务端守恒汇总 |
`driverConfirmationSummary`
| 字段 | JSON 类型 | 说明 |
|---|---|---|
| `dispatchPlanGeneration` | `String(Long)` / `null` | 当前最终派车方案代际 |
| `requiredSegmentCount` | `Integer` | 当前需司机确认的 HOLD 执行段数 |
| `confirmedSegmentCount` | `Integer` | 已满足有效确认谓词的执行段数 |
| `pendingSegmentCount` | `Integer` | 待确认且非歧义段数 |
| `rejectedSegmentCount` | `Integer` | 当前拒绝且待改派段数 |
| `ambiguousSegmentCount` | `Integer` | 通知结果歧义段数 |
| `allDriverConfirmed` | `Boolean` | 所有 required 段是否均确认 |
| `allExecutionConfirmed` | `Boolean` | 全部实际用车切片是否均确认执行 |
守恒规则:`confirmedSegmentCount + pendingSegmentCount + rejectedSegmentCount + ambiguousSegmentCount = requiredSegmentCount`。消费者不得使用 `currentAssignment` 或任一代表司机自行计算整组完成。
### 1.2 `activeAssignments[]` 执行段字段
每项按独立 `assignmentGroupId` 返回;同槽多段、同司机非连续多段不得合并。
| 字段 | JSON 类型 | 可空 | 语义 |
|---|---|---|---|
| `assignmentGroupId` | `String(Long)` | 否 | 当前连续执行段身份 |
| `assignmentSlotId` | `String(Long)` | 否 | 稳定车辆槽位身份 |
| `serviceDates[]` | `String(date)[]` | 否 | 本段精确服务日,不补日期洞 |
| `holdNotificationGeneration` | `String(Long)` | 是 | 当前段通知代际 |
| `holdNotificationStatus` | `String` | 否 | `NOT_REQUIRED/PENDING/SENT/FAILED/AMBIGUOUS/INVALIDATED` |
| `holdSentAt` | `String(date-time)` | 是 | 有可信发送成功事实时才返回 |
| `driverConfirmedAt` | `String(date-time)` | 是 | 当前段司机确认时间 |
| `driverReplyNote` | `String` | 是 | 回复摘要 |
| `driverReplySource` | `String` | 是 | `NOTIFICATION/MANUAL_CONTACT`;历史未知保持 `null` |
| `driverReplyOperatorId` | `String(Long)` | 是 | 人工登记操作人;历史未知可空 |
| `driverReplyRecordedAt` | `String(date-time)` | 是 | 回复事实登记时间 |
| `driverConfirmationEvidencePresent` | `Boolean` | 否 | 是否存在确认凭证 |
| `driverConfirmationEvidenceCount` | `Integer` | 否 | 凭证数量,不新增文件元数据 |
`SENT` 只表示系统发送成功,不表示通道已提供送达回执;前端不得展示为 `DELIVERED`
## 2. 登记司机确认
`POST /admin/fleet/assignments/:assignmentId/driver-confirmation` 请求新增:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `driverReplySource` | `String` | 否 | 仅允许 `NOTIFICATION``MANUAL_CONTACT`;人工电话/微信确认必须显式传 `MANUAL_CONTACT` |
既有 `requestId``driverReplyNote``evidenceFileIds[]` 保持兼容。登记只更新 `assignmentId` 所属当前 group;旧 group、旧 generation、已改派/取消或资源快照漂移均 fail closed,不得跟随 slot 更新替代组。
## 3. 用车需求整组原子确认
新增 `POST /admin/fleet/assignments/requirements/:requirementId/confirm`
请求体:
```json
{
"orderId": "2080000000000000001",
"requestId": "requirement-confirm-5364-001",
"expectedRequirementVersion": 7,
"expectedRequirementSha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"expectedPlanGeneration": "12",
"groups": [
{
"assignmentGroupId": "2080000000000000101",
"sendItinerarySms": true
}
]
}
```
- `groups[]` 必须是 Board 当前 generation 中全部 used execution groups 的精确集合;按 `assignmentGroupId` 去重。
- 服务端重读 exact slot×service-date topology 并一次提交;失败不产生部分状态推进、receipt 或重复 Outbox。
- `(requirementId, requestId)` 是 durable 幂等键:同 canonical 请求返回首次稳定结果;同键不同载荷返回 `605059`
- 旧 `/:assignmentId/confirm` 在 multi-group 当前 generation 返回 `605057`;singleton 继续委托同一原子引擎。
主要业务错误:
| code | 含义 |
|---|---|
| `605055` | 派车方案 generation 已变化 |
| `605056` | 执行段精确集合已变化 |
| `605057` | multi-group 必须使用需求级整组确认 |
| `605058` | 至少一个当前执行段不满足最终确认条件 |
| `605059` | 幂等 requestId 已用于不同确认载荷 |
## 4. H5 段级最小化
`GET /app/h5/itinerary/:token` 新增/versioned 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `contractVersion` | `String` | 当前为 `V2` |
| `segmentDays` | `Integer` | token 所绑定执行段天数 |
| `serviceDates[]` | `String(date)[]` | 本段精确服务日期 |
| `segmentRoute` | `String` / `null` | 仅由本段逐日标题生成 |
既有 `days``route``daily``transport` 字段不删除且原语义不静默改写。V2 token 同时绑定 group、slot、plan generation、notification generation、driver、vehicle、精确日期摘要和 audience snapshot;任一漂移都返回失效,禁止按 slot 跳转到新司机或新 group。
## 5. 前端消费动作
1. Board 按 `activeAssignments[].assignmentGroupId` 渲染独立执行段,不以 `currentAssignment`、司机 ID 或连续日期猜测合并。
2. 整组确认提交 Board 返回的 `requirementVersion/requirementSha256/dispatchPlanGeneration` 与全部 group 精确集合;收到 `605055/605056` 后刷新,不做部分重试。
3. 人工确认显式传 `driverReplySource=MANUAL_CONTACT`;展示通知状态与确认来源为两个独立事实。
4. H5 使用 `serviceDates/segmentDays/segmentRoute` 展示本段范围;保留对既有字段的兼容读取。
5. 所有 JSON Long 按字符串处理,禁止转 JavaScript `Number`
## 6. Internal Feign / shared Java
Fleet 与 order-v3 新增内部 reservation/release 契约:
- `POST /v3/internal/order/orders/:orderId/requirement/vehicle/final-confirmation-reservations`
- `POST /v3/internal/order/orders/:orderId/requirement/vehicle/final-confirmation-reservations/release`
共享 DTO 采用 additive 字段,Fleet reservation、事务内 durable receipt、release Outbox 与 Order holder/fence 共同保证失败回滚和可重放释放。oasdiff 与 Spring Cloud Contract 均未配置,不能标记为工具 PASS;本次以 Controller JSON、producer/consumer 源码对比及两个 reactor verify 作为人工回退证据。
## 验证证据
- Fleet`mvn -pl hl-fleet-service -am verify`,2,820 tests,0 failures,0 errors,5 skipped,BUILD SUCCESS。
- Order`mvn -pl hl-order-service-v3 -am verify`,7,261 tests,0 failures,0 errors,36 skipped,BUILD SUCCESS。
- Fleet SpotlessBUILD SUCCESS;`git diff --check dev-v3`PASS。
- 最终独立 P0–P2 reviewGO,无可复现阻断项。
- 后端 PR #5420 已 squash 合并到 `dev-v3@e543a41035`;按 changelog 发布门禁记录 `backend_status=deployed`,本工单不包含运行时测试部署。
- 本工单冻结范围不执行部署或网关调用,`gateway_status=not_required`
- 前端未领取,`frontend_status=pending`;changelog 发布不代表前端已实现、发布或页面验证。
## 关联 / 联系人
### 链接
- **Issue**: [#5364](https://git.1814.love:8443/wx/HL/issues/5364)
- **PR**: [#5420](https://git.1814.love:8443/wx/HL/pulls/5420)
- **Merge commit**: [e543a41035ae](https://git.1814.love:8443/wx/HL/commit/e543a41035ae)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,151 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5379"
title: "终止行程车辆退款改为 Fleet 服务日权威判定"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@d7a7e995cb9555fe07ef13bfb3d0f781f139e535"
target_release: "v2.1"
verified_at: "2026-08-04"
status_note: "后端实现完成:双模块 verify 通过order 7402 全绿 / fleet 3010 仅 Docker 环境型 1 error,本单新增代码行覆盖率 91.7%,PR #5417+#5445 已合并 dev-v3 并部署 TESTfleet→order-v3 顺序,网关已验证预览成功路径15 行 VEHICLE+terminateDate wire key、终止提交成功COMPLETED+PENDING_REVIEW+settlement refund 落库)、幂等重放(同 terminateRefundId、阻断路径581018 COMPLETED 拒绝/584100 费用不可用 fail-closed、空集路径无需求允许空车辆成本;管理后台已由 Pi 领取并开始兼容适配。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 订单: 终止行程车辆退款改为 Fleet 服务日权威判定
> **服务**: hl-order-service-v3
> **PR**: #5417
> **Issue**: #5379
> **日期**: 2026-08-03
> **影响范围**: 管理后台订单终止退款预览及终止提交
---
## ⚠️ 关键变化
终止行程时,车辆是否已发生不再采用客户端提交的 `vehicles[].used`;后端按 Fleet 返回的 `serviceDate``endDayNumber` 对应终止日权威判定。`vehicles[].used` 仍为必填兼容字段,仅参与请求重放一致性校验。
## 变更接口
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 终止行程退款预览 | POST | `/v3/admin/order/:id/terminate/refund-preview` | 响应语义修改 | 车辆行 `defaultUsed` 仅供初始化展示,最终提交以后端权威事实为准 |
| 2 | 终止行程 | POST | `/v3/admin/order/:id/terminate` | 请求字段语义修改 | `vehicles[].used` 保持必填,但不再参与车辆退款金额计算 |
## 二、接口详情
### 1. 终止行程退款预览 `POST /v3/admin/order/:id/terminate/refund-preview`
**出参**: `Result<OrderTerminateRefundPreviewRespVO>`
### refundLines[] 统一资源行(新逻辑主用,提交 `lineUsages``lineKey` 回传)
`refundLines``List<TerminateRefundItemVO>`,住宿/门票/活动/服务/交通/备品/用车/保险全部资源统一展平为行,已按 `dayNumber` 展开(用车每天一行)。
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `refundLines[].lineKey` | String | 统一退款资源行 key,提交 `lineUsages` 时原样回传 | `ITINERARY_NODE:97010:2` |
| `refundLines[].categoryCode` | String | **资源类型 discriminator**`ROOM`/`TICKET`/`ACTIVITY`/`SERVICE`/`TRANSPORT`/`SUPPLIES`/`VEHICLE`/`INSURANCE`,前端据此判断每行资源类型 | `SERVICE` |
| `refundLines[].categoryName` | String | 资源分类名称(与 categoryCode 对应) | `服务` |
| `refundLines[].sourceType` | String | **资源来源类型 discriminator**`HOTEL_ASSIGNMENT`/`ITINERARY_NODE`/`BATCH_SUPPLIES`/`PRODUCT_SUPPLIES`/`VEHICLE_ASSIGNMENT`/`INSURANCE_ORDER`,决定 `sourceId` 语义 | `ITINERARY_NODE` |
| `refundLines[].sourceId` | String(Long) | 资源来源 ID,语义由 `sourceType` 决定Long 序列化为 String 防 JS 精度丢失) | `97010` |
| `refundLines[].refId` | String(Long) | 资源配单记录 IDString 防 JS 精度丢失) | `96011` |
| `refundLines[].name` | String | 资源名称快照(酒店名/景点名/车型) | `拉萨瑞吉·大床房` |
| `refundLines[].dayNumber` | Integer | 第几天(住宿/门票/用车每天行有值;保险为 null | `1` |
| `refundLines[].dealPrice` | String(BigDecimal) | 结算单价(住宿=settlementPrice/间·晚;门票按节点价格口径;用车=dailyFee | `1280.00` |
| `refundLines[].quantity` | Integer | 数量(住宿=间数;门票=张数;用车=车辆数;保险=1 | `1` |
| `refundLines[].billingType` | String | 计费方式快照,备品等资源使用 | `PER_QUANTITY` |
| `refundLines[].totalAmount` | String(BigDecimal) | 行金额合计 = dealPrice × quantity | `1280.00` |
| `refundLines[].defaultUsed` | Boolean | 前端初始化展示值;车辆行最终是否已发生由提交时 Fleet `serviceDate` 与终止日重新判定,非车辆行按提交 `used` 重算 | `false` |
| `refundLines[].locked` | Boolean | 是否锁定不退true=保险等不可退资源) | `false` |
| `refundLines[].lockedReason` | String | 锁定原因(`locked=true` 时有值) | `保险已生效不退` |
### 旧分组字段(兼容保留,与 refundLines 并存)
`rooms`/`tickets`/`services`/`supplies`/`vehicles`/`insurance` **继续返回**,类型均为 `List<TerminateRefundItemVO>`(与 `refundLines` 同构,字段集完全一致):
| 字段 | 类型 | 说明 |
|------|------|------|
| `rooms` | List&lt;TerminateRefundItemVO&gt; | 住宿清单(每晚×每组房一行) |
| `tickets` | List&lt;TerminateRefundItemVO&gt; | 门票/活动清单(景点/活动节点,含套餐内) |
| `services` | List&lt;TerminateRefundItemVO&gt; | 服务清单SERVICE/TRANSPORT 节点) |
| `supplies` | List&lt;TerminateRefundItemVO&gt; | 备品清单hasCost=true 的备品,无天维度整单一行) |
| `vehicles` | List&lt;TerminateRefundItemVO&gt; | 用车清单(每段车按 totalDays 展开成每天一行) |
| `insurance` | List&lt;TerminateRefundItemVO&gt; | 保险(整单 1 行,locked=true 不可退) |
> 前端可任选一种渲染:新逻辑按 `refundLines[]` + `categoryCode`/`sourceType` 区分资源;旧分组字段不会删除,兼容期可继续使用。
### days 与其余顶层字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `days[].terminateDate` | String(date) | 结束日期选项 wire key 固定为 `terminateDate`(原 `date` 字段改名),值 = departDate + (dayNumber-1);`dayNumber`/`label`/`isCurrent` 不变 |
### 2. 终止行程 `POST /v3/admin/order/:id/terminate`
**VO**: `OrderTerminateTripReqVO`
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `endDayNumber` | Body | Integer | ✅ | 1-based,必须落在订单行程范围内 | 用于确定终止日;预览返回的 `days[].terminateDate` 即对应日期 |
| `vehicles[].refId` | Body | Long/String ID | ✅ | 有效配车记录 ID | 同段车跨天可重复 |
| `vehicles[].dayNumber` | Body | Integer | ✅ | >= 1 | 区分同段车辆的服务日 |
| `vehicles[].used` | Body | Boolean | ✅ | 不可省略 | 兼容及重放摘要字段,不参与车辆退款金额计算 |
| `lineUsages[].used` | Body | Boolean | ✅ | 非车辆资源按该值重算 | 不覆盖车辆 Fleet 事实 |
## 三、契约约束与正确调用方式
| 场景 | 后端行为 |
|------|----------|
| `vehicles[].used` 与 Fleet 服务日事实不同 | 车辆退款采用 Fleet 服务日事实;客户端值只保留在请求摘要中 |
| 相同终止边界和相同正文重试 | 返回既有终止结果 |
| 已终止订单以不同 `endDayNumber` 或不同正文重试 | 拒绝,错误码 `581049` |
| `endDayNumber` 超出订单行程 | 拒绝,错误码 `581047` |
| Fleet `serviceDate` 无法映射到订单行程 | 拒绝,错误码 `581048` |
| Fleet 车辆事实不可用或不完整 | fail closed,错误码 `584100`,不按零车费继续 |
前端调用要求:
1. 继续提交完整的 `vehicles[].refId/dayNumber/used`,不要删除 `used` 字段。
2. 不要根据 `vehicles[].used` 自行推导最终车辆退款金额;以接口返回的 `baselineRefund``finalRefund` 为准。
3. 收到上述错误码时保留用户输入并提示重试或刷新事实,不要按“无车辆费用”继续。
## 四、边界行为与兼容性
- 请求字段、类型和必填性未删除,旧客户端 payload 可继续解析。
- `vehicles[].used` 的金额语义发生变化,但仍参与幂等重放摘要;重试时必须复用首次正文。
- 非车辆资源仍按既有 `lineUsages[].used` 规则处理。
- 后端修复尚未合并、部署或经网关验证;本文件不声明 TEST 环境可用。
## 验证证据
- 定向单元测试覆盖 Fleet 服务日权威判定、越界/不一致 fail-closed、重放冲突及车辆 `used` 不覆盖事实。
- 真实独立 Redis 验证覆盖 owner-token 在事务提交前被替换时,订单状态、终止退款和 Fleet Outbox 三类本地写入全部回滚SettlementFleetPlanLockCommitRedisIntegrationTest,本机 Redis 7.4 真实运行通过)。
- Fleet 真表 H2 IT 覆盖 reservation 生命周期与 terminate 截断隔离5 个;order-v3 真表 IT 覆盖 Outbox 最新命令因果查询。
- 双模块 reactor verifyorder-v3 7334 全绿;fleet 2914,仅 #5374 Docker MySQL 测试环境型失败(本机无 Docker
- 本单核心新增代码 changed-line coverage 90.5%≥90%;evidence 包含 raw JaCoCo、provider receipts 与 SHA-256 索引。
- MySQL 8.0.33 gate 凭据不可恢复,未运行(如实记录,不冒充通过)。
- PR 合并、TEST 部署与网关验收仍待完成,因此 `backend_status``gateway_status` 保持 `pending`
## 六、相关文档
- 关联 Issue: [wx/HL#5379](https://git.1814.love:8443/wx/HL/issues/5379)
- 关联 PR: [wx/HL#5417](https://git.1814.love:8443/wx/HL/pulls/5417)
## 关联 / 联系人
### 链接
- **Issue**: [#5379](https://git.1814.love:8443/wx/HL/issues/5379)
- **PR**: [#5417](https://git.1814.love:8443/wx/HL/pulls/5417)
- **Merge commit**: [39dfd80ae0](https://git.1814.love:8443/wx/HL/commit/39dfd80ae0)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,889 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5368"
title: "v3 合同管理完整接口与订单号搜索"
consumer: "admin"
change_type: "修改接口"
author: "yaosutu(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@5925a3ff810b5ef0dbce027a0057e343a1910446"
target_release: ""
verified_at: "2026-08-04T22:35:00+08:00"
status_note: "PR #5501 已合并 dev-v3;deploy-panel 任务 0eae2ffb 成功,hl-order-service-v3 8086/8186 双实例 UP。已通过有效管理后台鉴权验证 Gateway 合同列表 12 个只读用例与目标合同详情;列表/详情 teamNo 一致。QA 报告因未保存含 PII 的详情截图仍为 PARTIAL,不影响已完成的运行时功能门禁。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 🔧【修改接口·管理后台】v3 合同管理完整接口与订单号搜索 (#5368)
> **PR**: [#5501](https://git.1814.love:8443/wx/HL/pulls/5501) | **服务**: `hl-order-service-v3` | **更新日期**: 2026-08-04
> **范围**: 仅 8 个 admin Controller 的 56 个 `/v3/admin/*` 端点;不含 internal、mp、callback、job。
## 1. 接口背景与历史文档关系
管理后台合同页需要统一使用 v3 读写链路,并在合同列表按订单号定位数据。本文同时给出该页面依赖的全部 admin 契约,便于前端一次性移除 v1/v3 混用。
与已发布的 `2026-07/31_5368_非订单页面统一补齐真实团号与团号搜索` 的关系:
- 2026-07 文档对应 PR #5386 / commit `c9d1b12618`,已约定 `teamNo: string|null`、列表 `teamNo` 搜索、详情返回 `teamNo` 和 v3 完整写侧。
- 本文对应 PR #5501;不重复声称上述能力是本次新增,而是补齐全量 admin 契约并说明本次仅有的两项可见变化。
## 2. 本次真实变更
| # | 接口 | 原来 | 现在 |
|---|---|---|---|
| 1 | `GET /v3/admin/contract/list` | 无独立 `orderNo` 查询参数 | 新增可选 `orderNo`,对人类可读订单号做包含匹配 |
| 2 | 同上 | `teamNo` / `contactName` 文本边界未完整固化 | `orderNo` / `teamNo` / `contactName` 统一 trim;全空白忽略;`%``_``\` 都按字面字符匹配;多条件按 AND 组合 |
N+1 消除是内部性能修复,不属于前端契约变更。
## 3. 通用协议
- 认证:所有端点都需要管理后台 JWT;旅行社敏感写操作按接口标注需 ADMIN 或 SUPER_ADMIN。
- 成功包装:`{
"code":200,"data":...,"message":"操作成功","success":true
}`,字段名是 `message`,不是 `msg`。
- 分页包装:`data.records` / `data.total` / `data.page` / `data.pageSize`
- 失败包装:`{
"code":业务错误码,"data":null,"message":"可读提示","success":false
}`。
- 幂等GET 幂等;PUT 为目标状态更新时可重试;POST 创建/上传不得盲目重试;DELETE 不保证重复调用仍成功。
- 金额按 JSON string 消费;雪花 ID 按 string 消费,不得转 JavaScript `Number`
## 变更接口完整接口清单,56
| Controller | 数量 | 路径域 |
|---|---:|---|
| AdminContractController | 15 | `/v3/admin/contract` |
| AdminContractSchemeController | 7 | `/v3/admin/contract/scheme` |
| AdminContractSchemeAttachmentController | 4 | `/v3/admin/contract/scheme/<schemeId>/attachments` |
| AdminContractSchemeTourGuideController | 4 | `/v3/admin/contract/scheme/<schemeId>/tour-guides` |
| AdminClauseTemplateController | 6 | `/v3/admin/contract/clause-template` |
| AdminTravelAgencyController | 11 | `/v3/admin/travel-agency` |
| AdminTravelAgencyQualificationController | 4 | `/v3/admin/travel-agency/<agencyId>/qualification` |
| AdminTravelAgencyPaymentController | 5 | `/v3/admin/travel-agency/<agencyId>/payment` |
## 5. 接口详情
### 5.1 AdminContractController15
#### 5.1.1 创建合同(标准模式)
- **场景/协议**:手工填写全量签约数据;`POST /v3/admin/contract/create`;管理员 JWT;非幂等。
- **请求体(全部字段)**`orderId:long?``contractType:string?(TOUR/INSURANCE)``platform:string?(12301/LOCAL/TENCENT_ESIGN)``vendorCode:string?``templateCode:string!``agencyCode:string?``mchId:string?``transactorName:string?``transactorPhone:string?``destination:string!``routeName:string!``days:int?``nights:int?``departureDate:date!``returnDate:date!``departureCity:string?``groupId:string?``signatoryMode:int?(1..3)``signatoryName:string!``signatoryPhone:string!``signatoryIdType:int?``signatoryIdNumber:string!``signingPlace:string?``adultCost:decimal!``childCost:decimal?``totalAmount:decimal!``paymentMethod:int?(1..3)``disputeResolution:int?(1..2)``tribunalName:string?``litigationCourt:string?``contactName:string!``contactPhone:string!``supplementaryClause:string?``travelers:array!``accordingContract:boolean?``accordingContractPhase:object?``contractNum:string?``holdNum:string?``paymentDescription:string?``paymentOther:string?``agreeToBuyInsurance:boolean?``insuranceCompany:string?``insuranceCoverage:string?``insurancePremium:string?``insuranceProductName:string?``insurancePurchaseMethod:int?(1..3)``guideServiceCost:decimal?``paymentTime:string?``startHour:int?(0..23)``endHour:int?(0..23)``touristCondition:string?``schemeId:string?``travelers[]``name:string!``gender:string?(0/1/2)``age:int?``idCardType:int?(1/2)``idCardNo:string!``phone:string?``isSigner:boolean?``isChild:boolean?``nationality:string?``race:string?``roomGroupNo:int?`
- **响应(完整)**`ContractDetailVO``contractId:string, orderId:string, schemeId:string|null, orderNo:string|null, teamNo:string|null, templateCode, templateName, contractNumber, platform, contractType, contractTypeLabel, mode, modeLabel, status, statusLabel, signUrl, qrCodeUrl, fileUrl, agencyCode, travelAgencyName, destination, departureDate, returnDate, totalAmount:string, touristCount:int, contactName, contactPhone, createTime, supplementaryClause, travelers[], statusLogs[]`;`travelers[]={
travelerId:string,name,idCardType,idCardTypeLabel,idCardNo,phone,isSigner
}`;`statusLogs[]={
logId:string,contractId:string,oldStatus,oldStatusLabel,newStatus,newStatusLabel,source,sourceLabel,rawPayload,createTime
}`。
- **边界/错误**:行程未确认 `510220`;已有有效合同 `510214`;模板不存在 `510215`;签署人手机缺失 `510213`;必填/长度/手机格式错误返回参数校验失败。
- **典型示例**:请求 `{
"orderId":"2079000000000000001","templateCode":"TOURAGE_STANDARD","destination":"呼伦贝尔","routeName":"草原5日","departureDate":"2026-08-10","returnDate":"2026-08-14","signatoryName":"张某","signatoryPhone":"138****8000","signatoryIdNumber":"110101********1234","adultCost":"3000.00","totalAmount":"6000.00","contactName":"张某","contactPhone":"138****8000","travelers":[{
"name":"张某","idCardNo":"110101********1234","isSigner":true
}]
}`;响应 `{
"code":200,"data":{
"contractId":"2080000000000000001","orderId":"2079000000000000001","status":"GENERATED","totalAmount":"6000.00","travelers":[],"statusLogs":[]
},"message":"操作成功","success":true
}`。
#### 5.1.2 按方案创建合同
- **场景/协议**:后台从订单与方案自动组装合同;`POST /v3/admin/contract/create-by-scheme`; JWT;非幂等。
- **入参**:请求体 `orderId:string!``schemeId:string!`
- **响应(完整)**:与 5.1.1 的 `ContractDetailVO` 字段完全一致:`contractId,orderId,schemeId,orderNo,teamNo,templateCode,templateName,contractNumber,platform,contractType,contractTypeLabel,mode,modeLabel,status,statusLabel,signUrl,qrCodeUrl,fileUrl,agencyCode,travelAgencyName,destination,departureDate,returnDate,totalAmount,touristCount,contactName,contactPhone,createTime,supplementaryClause,travelers[],statusLogs[]`
- **边界/错误**:方案不存在/已停用 `510205`;方案未配模板 `510206`;订单信息失败 `510207`;行程未确认 `510220`;已有合同 `510214`
- **示例**:请求 `{
"orderId":"2079000000000000001","schemeId":"2020001"
}`;响应 `{
"code":200,"data":{
"contractId":"2080000000000000001","orderId":"2079000000000000001","schemeId":"2020001","status":"GENERATED","travelers":[],"statusLogs":[]
},"message":"操作成功","success":true
}`。
#### 5.1.3 作废合同
- **场景/协议**:不可恢复地作废有效合同;`POST /v3/admin/contract/<id>/invalidate`; JWT;重复调用不保证成功。
- **入参**:路径 `id:string!` 合同 ID;无请求体。
- **响应(完整)**`ContractVO={
contractId,orderId,schemeId,orderNo,teamNo,templateCode,templateName,contractNumber,platform,contractType,contractTypeLabel,mode,modeLabel,status,statusLabel,signUrl,qrCodeUrl,fileUrl,agencyCode,travelAgencyName,destination,departureDate,returnDate,totalAmount:string,touristCount,contactName,contactPhone,createTime
}`。
- **边界/错误**:合同不存在 `510001`;缺合同编号 `510003`;上游作废失败 `510104`
- **示例**:请求 `POST /v3/admin/contract/2080000000000000001/invalidate` 无 body;响应 `{
"code":200,"data":{
"contractId":"2080000000000000001","status":"VOIDED","statusLabel":"已作废"
},"message":"操作成功","success":true
}`。
#### 5.1.4 合同列表(本 PR 修改)
- **场景/协议**:合同管理分页列表;`GET /v3/admin/contract/list`; JWT;幂等。
- **Query全部**`page:int?=1``pageSize:int?=10``orderId:string?``status:string?``platform:string?``orderNo:string?``teamNo:string?``contactName:string?`。三个文本条件都 trim;全空白忽略;`%` / `_` / `\` 字面匹配;条件间 AND。
- **响应(完整)**`PageResult.records[]` 每项为 `ContractVO={
contractId,orderId,schemeId,orderNo,teamNo,templateCode,templateName,contractNumber,platform,contractType,contractTypeLabel,mode,modeLabel,status,statusLabel,signUrl,qrCodeUrl,fileUrl,agencyCode,travelAgencyName,destination,departureDate,returnDate,totalAmount:string,touristCount,contactName,contactPhone,createTime
}`;顶层 `total:long,page:int,pageSize:int`。
- **边界**`orderNo=%` 只匹配订单号中真实的 `%`,不会全表命中;`orderNo= ` 等价于未传;无结果返回空 `records`
- **示例**:请求 `GET /v3/admin/contract/list?page=1&pageSize=10&orderNo=HL2026&teamNo=26-08`;响应 `{
"code":200,"data":{
"records":[{
"contractId":"2080000000000000001","orderId":"2079000000000000001","orderNo":"HL202608040001","teamNo":"26-0804","status":"SIGNED","totalAmount":"6000.00"
}],"total":1,"page":1,"pageSize":10
},"message":"操作成功","success":true
}`。
#### 5.1.5 合同详情
- **场景/协议**:查看合同、出行人和状态日志;`GET /v3/admin/contract/<id>`; JWT;幂等。
- **入参**:路径 `id:string!`
- **响应(完整)**`ContractDetailVO`全字段:`contractId,orderId,schemeId,orderNo,teamNo,templateCode,templateName,contractNumber,platform,contractType,contractTypeLabel,mode,modeLabel,status,statusLabel,signUrl,qrCodeUrl,fileUrl,agencyCode,travelAgencyName,destination,departureDate,returnDate,totalAmount,touristCount,contactName,contactPhone,createTime,supplementaryClause,travelers[{
travelerId,name,idCardType,idCardTypeLabel,idCardNo,phone,isSigner
}],statusLogs[{
logId,contractId,oldStatus,oldStatusLabel,newStatus,newStatusLabel,source,sourceLabel,rawPayload,createTime
}]`。
- **错误/示例**:不存在 `510001`。请求 `GET /v3/admin/contract/2080000000000000001`;响应 `{
"code":200,"data":{
"contractId":"2080000000000000001","orderId":"2079000000000000001","teamNo":"26-0804","totalAmount":"6000.00","travelers":[{
"travelerId":"2081000000000000001","name":"张某"
}],"statusLogs":[]
},"message":"操作成功","success":true
}`。
#### 5.1.6 下载合同文件
- **场景/协议**:取得已签署/已上报合同下载 URL;`GET /v3/admin/contract/<id>/download`; JWT;幂等。
- **入参/响应**:路径 `id:string!``data:string` 为下载 URL,无其他 data 字段。
- **错误/边界**`510001` 不存在;`510101` 未签署完成;`510102` 文件 URL 缺失。
- **示例**:请求 `GET /v3/admin/contract/2080000000000000001/download`;响应 `{
"code":200,"data":"https://example.invalid/signed/contract.pdf?token=masked","message":"操作成功","success":true
}`。
#### 5.1.7 重发签署短信
- **场景/协议**:为未完成签署的合同重发短信;`POST /v3/admin/contract/<id>/resend-sms`; JWT;有 1 分钟频控,不可并发重试。
- **入参/响应**:路径 `id:string!``data:boolean`
- **错误/边界**`510001``510005` 已签署、`510006` 过于频繁、`510007` 合同编号缺失、`510008` 联系人手机缺失。
- **示例**:请求 `POST /v3/admin/contract/2080000000000000001/resend-sms` 无 body;响应 `{
"code":200,"data":true,"message":"操作成功","success":true
}`。
#### 5.1.8 合同模板列表
- **场景/协议**:创建合同时选择平台模板;`GET /v3/admin/contract/templates`; JWT;幂等。
- **Query**`platform:string?` (`12301` 等)。
- **响应(完整)**`data[]` 每项 `templateId:string,templateCode:string,templateName:string,platform:string,mode:string,status:string,description:string|null,createTime:datetime`
- **示例**:请求 `GET /v3/admin/contract/templates?platform=12301`;响应 `{
"code":200,"data":[{
"templateId":"101","templateCode":"TOURAGE_STANDARD","templateName":"标准旅游合同","platform":"12301","mode":"STANDARD","status":"ACTIVE","description":null,"createTime":"2026-08-01T10:00:00"
}],"message":"操作成功","success":true
}`。
#### 5.1.9 刷新合同状态
- **场景/协议**:主动从合同平台同步最新状态;`GET /v3/admin/contract/<id>/status`; JWT;查询可重试,但可产生状态更新。
- **入参/响应**:路径 `id:string!``ContractVO`完整字段同 5.1.3。
- **错误/示例**`510001``510103` 缺合同编号。请求 `GET /v3/admin/contract/2080000000000000001/status`;响应 `{
"code":200,"data":{
"contractId":"2080000000000000001","status":"SIGNED","statusLabel":"已签署"
},"message":"操作成功","success":true
}`。
#### 5.1.10 按订单查询全部合同
- **场景/协议**:查询订单下包含已作废数据的全部合同;`GET /v3/admin/contract/by-order/<orderId>`; JWT;幂等。
- **入参/响应**`orderId:string!``data[]` 每项为 5.1.3 的完整 `ContractVO`。无记录返回 `[]`
- **示例**:请求 `GET /v3/admin/contract/by-order/2079000000000000001`;响应 `{
"code":200,"data":[{
"contractId":"2080000000000000001","orderId":"2079000000000000001","status":"SIGNED","totalAmount":"6000.00"
}],"message":"操作成功","success":true
}`。
#### 5.1.11 获取订单有效合同
- **场景/协议**:取指定订单最新非作废合同;`GET /v3/admin/contract/active-by-order/<orderId>`; JWT;幂等。
- **入参/响应**`orderId:string!``data` 为 5.1.3 的完整 `ContractVO`,没有有效合同时可为 `null`
- **示例**:请求 `GET /v3/admin/contract/active-by-order/2079000000000000001`;响应 `{
"code":200,"data":{
"contractId":"2080000000000000001","status":"SIGNED","totalAmount":"6000.00"
},"message":"操作成功","success":true
}`。
#### 5.1.12 可用合同平台列表
- **场景/协议**:展示已注册平台及可用性;`GET /v3/admin/contract/platforms`; JWT;幂等;无入参。
- **响应(完整)**`data[]` 每项 `platformName:string,displayName:string,available:boolean`
- **示例**:请求 `GET /v3/admin/contract/platforms`;响应 `{
"code":200,"data":[{
"platformName":"12301","displayName":"12301团队报送","available":true
}],"message":"操作成功","success":true
}`。
#### 5.1.13 已配置旅行社列表
- **场景/协议**:按平台筛选可用旅行社;`GET /v3/admin/contract/agencies`; JWT;幂等。
- **Query**`platform:string?`
- **响应(完整)**`data[]` 每项 `code,agencyName,licenseNumber,businessLicenseNumber,agencyAddress,agencyCountry,agencyState,agencyCity,agencyDistrict,transactorName,transactorPhone,regionId,businessScope,zjParentId,appId,signKey,teamReportAppId,teamReportSignKey,mchId,complaintPhone,email,supportedPlatforms[],bankCard,complaintProvince,complaintCity,complaintAreaCode,complaintAddress,tribunalName,litigationCourt`。注意其中存在高敏配置,仅在授权后台使用,不得写日志/埋点。
- **示例**:请求 `GET /v3/admin/contract/agencies?platform=12301`;响应 `{
"code":200,"data":[{
"code":"hulai","agencyName":"呼籁旅行社","licenseNumber":"L-XX-100001","supportedPlatforms":["12301"],"appId":"***","signKey":"***"
}],"message":"操作成功","success":true
}`。
#### 5.1.14 报备合同(线下签约)
- **场景/协议**:线下签完后上报监管平台;`POST /v3/admin/contract/report`; JWT;非幂等。
- **请求体**:字段、必填性与校验完全同 5.1.1 `CreateContractRequest`
- **响应(完整)**:与 5.1.1 `ContractDetailVO` 完全一致。
- **错误/边界**:参数校验;未知旅行社 `510201`;上报失败 `510203`;腾讯电子签不支持 SYNC `510403`
- **示例**:请求同 5.1.1,`platform=12301`;响应 `{
"code":200,"data":{
"contractId":"2080000000000000002","mode":"SYNC","status":"REPORTED","travelers":[],"statusLogs":[]
},"message":"操作成功","success":true
}`。
#### 5.1.15 上传已签署合同 PDF
- **场景/协议**:仅 SYNC 合同上传签署后 PDF;`POST /v3/admin/contract/<id>/upload-pdf`; JWT;非幂等。
- **入参**:路径 `id:string!``multipart/form-data``file:file!`
- **响应(完整)**5.1.3 的完整 `ContractVO`
- **错误/边界**`510001``510105` 缺合同编号、`510106` 当前状态不允许、`510107` 上传失败;ONLINE/STANDARD 合同不适用。
- **示例**:请求 `POST /v3/admin/contract/2080000000000000002/upload-pdf` + `file=@signed-contract.pdf`;响应 `{
"code":200,"data":{
"contractId":"2080000000000000002","mode":"SYNC","status":"UPLOADED","fileUrl":"https://example.invalid/signed.pdf"
},"message":"操作成功","success":true
}`。
### 5.2 AdminContractSchemeController7
`ContractSchemeVO` 完整字段:`schemeId:string,name,description,contractPlatform,vendorCode,channel,contractTemplateCode,templateCode,contractTemplateName,contractMode,signatoryMode,agencyCode,agencyName,supplementaryClause,transactorName,transactorPhone,sortOrder,status,createTime``ContractSchemeRequest` 完整字段:`name:string!(max100),description:string?(max500),contractPlatform:string?(max50),vendorCode:string?(max32),channel:string?(max32),contractTemplateCode:string?(max50;兼容 templateCode/template_code),contractMode:string?(max50),signatoryMode:int?(1..3),agencyCode:string?(max50),supplementaryClause:string?(max2000),transactorName:string?(max50),transactorPhone:string?(11位手机),sortOrder:int?`
#### 5.2.1 启用方案列表
- `GET /v3/admin/contract/scheme/list`;产品/合同方案下拉;JWT;幂等;无入参。
- **响应(完整)**`data[]` 每项包含上述 `ContractSchemeVO` 全部 19 字段,仅返回 `status=ACTIVE`
- **示例**`GET .../list` → `{
"code":200,"data":[{
"schemeId":"2020001","name":"标准方案","contractPlatform":"12301","contractMode":"STANDARD","status":"ACTIVE"
}],"message":"操作成功","success":true
}`。
#### 5.2.2 全部方案列表
- `GET /v3/admin/contract/scheme/list-all`;方案管理页;JWT;幂等;无入参。
- **响应(完整)**`data[]` 每项包含 `ContractSchemeVO` 全部 19 字段,包含 ACTIVE/INACTIVE。
- **示例**`GET .../list-all` → `{
"code":200,"data":[{
"schemeId":"2020001","name":"标准方案","status":"ACTIVE"
},{
"schemeId":"2020002","name":"旧方案","status":"INACTIVE"
}],"message":"操作成功","success":true
}`。
#### 5.2.3 方案详情
- `GET /v3/admin/contract/scheme/<schemeId>`;编辑回显;JWT;幂等;路径 `schemeId:string!`
- **响应(完整)**`ContractSchemeVO` 全部 19 字段。不存在返回 `510209`
- **示例**`GET .../2020001` → `{
"code":200,"data":{
"schemeId":"2020001","name":"标准方案","contractPlatform":"12301","contractTemplateCode":"A00001","templateCode":"A00001","status":"ACTIVE"
},"message":"操作成功","success":true
}`。
#### 5.2.4 创建方案
- `POST /v3/admin/contract/scheme`;新增平台/模板/签约模式配置;JWT;非幂等。
- **请求体**:上述 `ContractSchemeRequest` 全部 13 字段。**响应**:上述 `ContractSchemeVO` 全部 19 字段。
- **边界/错误**`name` 必填;手机/长度/签署模式校验;未知旅行社 `510201`
- **示例**:请求 `{
"name":"标准方案","contractPlatform":"12301","contractTemplateCode":"A00001","contractMode":"STANDARD","signatoryMode":1,"agencyCode":"hulai","sortOrder":10
}`;响应 `{
"code":200,"data":{
"schemeId":"2020001","name":"标准方案","status":"ACTIVE"
},"message":"操作成功","success":true
}`。
#### 5.2.5 更新方案
- `PUT /v3/admin/contract/scheme/<schemeId>`;编辑方案;JWT;目标更新可重试。
- **入参**:路径 `schemeId:string!` + `ContractSchemeRequest` 全部字段。**响应**`ContractSchemeVO` 全部字段。
- **边界/错误**:已用方案创建的合同不受后续修改影响;不存在 `510209`
- **示例**:请求 `PUT .../2020001` + `{
"name":"标准方案V2","contractPlatform":"12301","contractMode":"STANDARD"
}`;响应 `{
"code":200,"data":{
"schemeId":"2020001","name":"标准方案V2","status":"ACTIVE"
},"message":"操作成功","success":true
}`。
#### 5.2.6 切换启停状态
- `PUT /v3/admin/contract/scheme/<schemeId>/toggle-status`;启用/停用方案;JWT;每次翻转,非幂等。
- **入参**`schemeId:string!`;无 body。**响应**`ContractSchemeVO` 全部字段。不存在 `510209`
- **示例**`PUT .../2020001/toggle-status` → `{
"code":200,"data":{
"schemeId":"2020001","status":"INACTIVE"
},"message":"操作成功","success":true
}`。
#### 5.2.7 删除方案
- `DELETE /v3/admin/contract/scheme/<schemeId>`;软删除;JWT;不保证重复调用成功。
- **入参/响应**`schemeId:string!`;成功 `data:null`,无其他响应字段。
- **错误/边界**:不存在 `510209`;被产品引用 `510218`;引用校验不可用 `510219`
- **示例**`DELETE .../2020001` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
### 5.3 AdminContractSchemeAttachmentController4
`AttachmentResp` 完整字段:`attachmentId:string,schemeId:string,fileName:string,ossUrl:string,ossKey:string,fileSize:long,fileType:string,sortOrder:int,createTime:datetime`
#### 5.3.1 附件列表
- `GET /v3/admin/contract/scheme/<schemeId>/attachments`;按 `sortOrder` 升序回显;JWT;幂等;`schemeId:string!`
- **响应(完整)**`data[]` 每项含 `AttachmentResp` 全部 9 字段;无配置返回 `[]`
- **示例**`GET .../2020001/attachments` → `{
"code":200,"data":[{
"attachmentId":"1010001","schemeId":"2020001","fileName":"service.pdf","ossUrl":"https://example.invalid/a.pdf","ossKey":"contract/a.pdf","fileSize":1024,"fileType":"PDF","sortOrder":10,"createTime":"2026-08-04T10:00:00"
}],"message":"操作成功","success":true
}`。
#### 5.3.2 上传附件
- `POST /v3/admin/contract/scheme/<schemeId>/attachments`;上传合同方案附件;JWT;非幂等。
- **入参**`schemeId:string!`;multipart `file:file!``fileType:string?(PDF/IMAGE/OTHER)``sortOrder:int?`。**响应**`AttachmentResp` 全部 9 字段。
- **错误/边界**空文件、超限、OSS 不可用或上传失败都返回 `510508`
- **示例**`POST .../2020001/attachments` + `file=@service.pdf&fileType=PDF&sortOrder=10` → `{
"code":200,"data":{
"attachmentId":"1010001","schemeId":"2020001","fileType":"PDF","sortOrder":10
},"message":"操作成功","success":true
}`。
#### 5.3.3 删除附件
- `DELETE /v3/admin/contract/scheme/<schemeId>/attachments/<attachmentId>`;软删;JWT;路径 `schemeId:string!`,`attachmentId:string!`;无 body。
- **响应/错误**:成功 `data:null`;附件不存在或不属于该方案 `510506`。已生成合同的旧 URL 不受影响。
- **示例**`DELETE .../2020001/attachments/1010001` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
#### 5.3.4 更新附件排序
- `PUT /v3/admin/contract/scheme/<schemeId>/attachments/<attachmentId>/sort`;JWT;目标更新可重试。
- **入参**:路径 `schemeId:string!`,`attachmentId:string!`;body `sortOrder:int!(>=0)`。**响应**`data:null`
- **错误/示例**:不存在 `510506`;负数为参数校验失败。请求 `{
"sortOrder":20
}` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
### 5.4 AdminContractSchemeTourGuideController4
`GuideResp={
guideId:string,schemeId:string,name:string,phone:string|null,licenseNumber:string|null,sortOrder:int,createTime:datetime
}`;`GuideSave={
name:string!(max50),phone:string?(11位手机,max20),licenseNumber:string?(max64),sortOrder:int?(>=0)
}`。
#### 5.4.1 导游列表
- `GET /v3/admin/contract/scheme/<schemeId>/tour-guides`;JWT;幂等;`schemeId:string!`;未配导游返回 `[]`,不兜底。
- **响应(完整)**`data[]` 每项含 `GuideResp` 全部 7 字段。
- **示例**`GET .../2020001/tour-guides` → `{
"code":200,"data":[{
"guideId":"3030001","schemeId":"2020001","name":"张某","phone":"138****8000","licenseNumber":"L-XX-100001","sortOrder":0,"createTime":"2026-08-04T10:00:00"
}],"message":"操作成功","success":true
}`。
#### 5.4.2 新增导游
- `POST /v3/admin/contract/scheme/<schemeId>/tour-guides`;JWT;创建加锁,非幂等。
- **入参**`schemeId:string!` + `GuideSave` 全部 4 字段。**响应**`GuideResp` 全部 7 字段。
- **边界/示例**:姓名必填;手机格式错误拒绝。请求 `{
"name":"张某","phone":"13800138000","licenseNumber":"L-XX-100001","sortOrder":0
}` → `{
"code":200,"data":{
"guideId":"3030001","schemeId":"2020001","name":"张某","phone":"13800138000","licenseNumber":"L-XX-100001","sortOrder":0,"createTime":"2026-08-04T10:00:00"
},"message":"操作成功","success":true
}`。
#### 5.4.3 修改导游
- `PUT /v3/admin/contract/scheme/<schemeId>/tour-guides/<guideId>`;JWT;目标更新可重试。
- **入参**`schemeId:string!`,`guideId:string!` + `GuideSave` 全部字段。**响应**`GuideResp` 全部字段。
- **错误/示例**:不存在/归属不符 `510507`。请求 `{
"name":"李某","sortOrder":1
}` → `{
"code":200,"data":{
"guideId":"3030001","schemeId":"2020001","name":"李某","sortOrder":1
},"message":"操作成功","success":true
}`。
#### 5.4.4 删除导游
- `DELETE /v3/admin/contract/scheme/<schemeId>/tour-guides/<guideId>`;JWT;不保证重复调用成功。
- **入参/响应**`schemeId:string!`,`guideId:string!`;成功 `data:null`。不存在/归属不符 `510507`
- **示例**`DELETE .../2020001/tour-guides/3030001` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
### 5.5 AdminClauseTemplateController6
`ClauseTemplateVO={
templateId:string,name:string,content:string,sortOrder:int,status:string,createTime:datetime
}`;`ClauseTemplateRequest={
name:string!,content:string!,sortOrder:int?
}`。
#### 5.5.1 启用模板列表
- `GET /v3/admin/contract/clause-template/list`;创建合同时选择;JWT;幂等;无入参。
- **响应(完整)**`data[]` 每项含 `ClauseTemplateVO` 全部 6 字段,仅 ACTIVE。
- **示例**`GET .../list` → `{
"code":200,"data":[{
"templateId":"401","name":"夏季小团","content":"脱敏示例条款","sortOrder":1,"status":"ACTIVE","createTime":"2026-08-04T10:00:00"
}],"message":"操作成功","success":true
}`。
#### 5.5.2 全部模板列表
- `GET /v3/admin/contract/clause-template/list-all`;模板管理页;JWT;幂等;无入参。
- **响应(完整)**`data[]` 每项含 `ClauseTemplateVO` 全部 6 字段,包含 ACTIVE/INACTIVE。
- **示例**`GET .../list-all` → `{
"code":200,"data":[{
"templateId":"401","name":"夏季小团","content":"条款","sortOrder":1,"status":"INACTIVE","createTime":"2026-08-04T10:00:00"
}],"message":"操作成功","success":true
}`。
#### 5.5.3 创建补充约定模板
- `POST /v3/admin/contract/clause-template`;JWT;非幂等。
- **请求/响应**body `ClauseTemplateRequest` 全部 3 字段;返回 `ClauseTemplateVO` 全部 6 字段。名称和内容必填。
- **示例**:请求 `{
"name":"夏季小团","content":"脱敏示例条款","sortOrder":1
}` → `{
"code":200,"data":{
"templateId":"401","name":"夏季小团","content":"脱敏示例条款","sortOrder":1,"status":"ACTIVE","createTime":"2026-08-04T10:00:00"
},"message":"操作成功","success":true
}`。
#### 5.5.4 更新补充约定模板
- `PUT /v3/admin/contract/clause-template/<id>`;JWT;可重试。
- **入参/响应**`id:string!` + `ClauseTemplateRequest` 全部字段;返回 `ClauseTemplateVO` 全部字段。旧合同快照不受影响。
- **错误/示例**:不存在 `510211`。请求 `{
"name":"夏季小团V2","content":"新条款","sortOrder":2
}` → `{
"code":200,"data":{
"templateId":"401","name":"夏季小团V2","content":"新条款","sortOrder":2,"status":"ACTIVE"
},"message":"操作成功","success":true
}`。
#### 5.5.5 切换模板启停
- `PUT /v3/admin/contract/clause-template/<id>/toggle-status`;JWT;每次翻转,非幂等。
- **入参/响应**`id:string!`;无 body;返回 `ClauseTemplateVO` 全部 6 字段。不存在 `510211`
- **示例**`PUT .../401/toggle-status` → `{
"code":200,"data":{
"templateId":"401","name":"夏季小团","content":"条款","sortOrder":1,"status":"INACTIVE","createTime":"2026-08-04T10:00:00"
},"message":"操作成功","success":true
}`。
#### 5.5.6 删除补充约定模板
- `DELETE /v3/admin/contract/clause-template/<id>`;JWT;软删;重复调用不保证成功。
- **入参/响应**`id:string!`;成功 `data:null`。已创建合同保留快照。不存在 `510211`
- **示例**`DELETE .../401` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
### 5.6 AdminTravelAgencyController11
`AgencySimpleResp={
agencyId:string,code,agencyName,isPrimary:int,sortOrder:int,createTime,updateTime
}`。
`AgencyResp` 完整字段:`agencyId:string,code,agencyName,licenseNumber,businessLicenseNumber,agencyCountry,agencyState,agencyCity,agencyDistrict,agencyAddress,transactorName,transactorPhone,email,bankCard,complaintPhone,complaintProvince,complaintCity,complaintAreaCode,complaintAddress,tribunalName,litigationCourt,regionId,businessScope,zjParentId,appId,signKey,teamReportAppId,teamReportSignKey,supportedPlatforms,supportedPlatformsList[],supportedPlatformsLabels[],isPrimary,status,statusLabel,sortOrder,visible,remark,qualifications[],payments[],createTime,updateTime`
`AgencySave` 完整字段:`code:string!(2..32,小写字母/数字/-),agencyName:string!(max128),licenseNumber:string?(max64),businessLicenseNumber:string!,agencyCountry,agencyState,agencyCity,agencyDistrict,agencyAddress,transactorName,transactorPhone,email,bankCard,complaintPhone,complaintProvince,complaintCity,complaintAreaCode,complaintAddress,tribunalName,litigationCourt,regionId,businessScope,zjParentId:int?,appId,signKey,teamReportAppId,teamReportSignKey,supportedPlatforms:string(JSON array),isPrimary:int?(0/1),status:string?,sortOrder:int?,visible:int?(0/1),remark`
#### 5.6.1 旅行社分页
- `GET /v3/admin/travel-agency/page`;管理列表;ADMIN+;幂等。
- **Query全部**`page:int?`,`pageSize:int?`,`status:string?(ENABLED/DISABLED)`,`isPrimary:int?(0/1)`,`agencyName:string?`
- **响应(完整)**`records[]` 每项含上述 `AgencyResp` 全部字段;顶层 `total,page,pageSize`
- **示例**`GET .../page?page=1&pageSize=10&status=ENABLED` → `{
"code":200,"data":{
"records":[{
"agencyId":"7001","code":"hulai","agencyName":"呼籁旅行社","status":"ENABLED","statusLabel":"启用","qualifications":[],"payments":[]
}],"total":1,"page":1,"pageSize":10
},"message":"操作成功","success":true
}`。
#### 5.6.2 启用旅行社下拉
- `GET /v3/admin/travel-agency/enabled`;ADMIN+;幂等;无入参。
- **响应(完整)**`data[]` 每项含 `AgencySimpleResp` 全部 7 字段。
- **示例**`GET .../enabled` → `{
"code":200,"data":[{
"agencyId":"7001","code":"hulai","agencyName":"呼籁旅行社","isPrimary":1,"sortOrder":1,"createTime":"2026-08-01T10:00:00","updateTime":"2026-08-04T10:00:00"
}],"message":"操作成功","success":true
}`。
#### 5.6.3 产品页可用旅行社
- `GET /v3/admin/travel-agency/enabled-for-product`;过滤经营许可证号为空的公司;ADMIN+;幂等;无入参。
- **响应(完整)**`data[]` 每项含 `AgencySimpleResp` 全部 7 字段。
- **示例**`GET .../enabled-for-product` → `{
"code":200,"data":[{
"agencyId":"7001","code":"hulai","agencyName":"呼籁旅行社","isPrimary":1,"sortOrder":1,"createTime":"2026-08-01T10:00:00","updateTime":"2026-08-04T10:00:00"
}],"message":"操作成功","success":true
}`。
#### 5.6.4 按合同平台筛选旅行社
- `GET /v3/admin/travel-agency/enabled-by-platform`;合同方案下拉;ADMIN+;幂等。
- **Query**`contractPlatform:string!` (`12301`/`TENCENT_ESIGN`)。**响应**`AgencySimpleResp` 全部 7 字段;主体公司优先、ID 升序。
- **边界/示例**:空值返回“合同平台不能为空”。`GET .../enabled-by-platform?contractPlatform=12301` → `{
"code":200,"data":[{
"agencyId":"7001","code":"hulai","agencyName":"呼籁旅行社","isPrimary":1,"sortOrder":1,"createTime":"2026-08-01T10:00:00","updateTime":"2026-08-04T10:00:00"
}],"message":"操作成功","success":true
}`。
#### 5.6.5 旅行社详情
- `GET /v3/admin/travel-agency/<id>`;含资质和支付子表;ADMIN+;幂等;`id:string!`
- **响应(完整)**:上述 `AgencyResp` 全部字段;`qualifications[]` 字段见 5.7;`payments[]` 字段见 5.8。不存在 `594001`
- **示例**`GET .../7001` → `{
"code":200,"data":{
"agencyId":"7001","code":"hulai","agencyName":"呼籁旅行社","status":"ENABLED","qualifications":[],"payments":[]
},"message":"操作成功","success":true
}`。
#### 5.6.6 新建旅行社
- `POST /v3/admin/travel-agency`;SUPER_ADMIN;非幂等。
- **请求体**:上述 `AgencySave` 全部 33 字段。**响应**`data:string` 新 agencyId,无其他 data 字段。
- **边界/错误**:编码重复 `594010`;非 SUPER_ADMIN `594009`;编码格式/必填/长度校验。高敏 key 不得打印。
- **示例**:请求 `{
"code":"demo-agency","agencyName":"示例旅行社","businessLicenseNumber":"9115********0001","supportedPlatforms":"[\"12301\"]","status":"ENABLED","isPrimary":0,"visible":1
}` → `{
"code":200,"data":"7002","message":"操作成功","success":true
}`。
#### 5.6.7 编辑旅行社
- `PUT /v3/admin/travel-agency/<id>`;ADMIN+;目标更新可重试。
- **入参/响应**`id:string!` + `AgencySave` 全部字段;成功 `data:null``code` 编辑时不可改。
- **错误/示例**:不存在 `594001`。请求 `{
"code":"hulai","agencyName":"呼籁旅行社","businessLicenseNumber":"9115********0001","status":"ENABLED"
}` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
#### 5.6.8 停用旅行社
- `PUT /v3/admin/travel-agency/<id>/disable`;ADMIN+;路径 `id:string!`;无 body;目标状态幂等。
- **响应/错误**:成功 `data:null`;不存在 `594001`;主体公司不可停用 `594005`
- **示例**`PUT .../7002/disable` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
#### 5.6.9 启用旅行社
- `PUT /v3/admin/travel-agency/<id>/enable`;ADMIN+;路径 `id:string!`;无 body;目标状态幂等。
- **响应/错误**:成功 `data:null`;不存在 `594001`
- **示例**`PUT .../7002/enable` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
#### 5.6.10 删除旅行社
- `DELETE /v3/admin/travel-agency/<id>`;ADMIN+;强保留语义(状态置 DISABLED;不保证重复调用成功。
- **入参/响应**`id:string!`;成功 `data:null`
- **错误/边界**`594001`;主体公司 `594005`;本接口不做物理删除。
- **示例**`DELETE .../7002` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
#### 5.6.11 设为主体公司
- `PUT /v3/admin/travel-agency/<id>/set-primary`;SUPER_ADMIN;路径 `id:string!`;无 body;目标状态幂等。
- **响应/错误**:成功 `data:null``594001`;目标停用或主体约束失败 `594005/594006`;非 SUPER_ADMIN `594009`
- **示例**`PUT .../7001/set-primary` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
### 5.7 AdminTravelAgencyQualificationController4
`QualificationResp={
qualificationId:string,agencyId:string,name,qualificationType,qualificationTypeLabel,fileUrl,fileType,fileTypeLabel,issueDate,expireDate,sortOrder,remark,createTime,updateTime
}`。
`QualificationSave={
name:string!(max64),qualificationType:string!(max32),fileUrl:string!,fileType:string?(PDF/IMAGE/OTHER),issueDate:date?,expireDate:date|null,sortOrder:int?,remark:string?
}`。
#### 5.7.1 资质列表
- `GET /v3/admin/travel-agency/<agencyId>/qualification`;ADMIN+;幂等;`agencyId:string!`
- **响应(完整)**`data[]` 每项含 `QualificationResp` 全部 14 字段;`fileUrl` 为有效期 30 分钟的临时 URL。
- **示例**`GET .../7001/qualification` → `{
"code":200,"data":[{
"qualificationId":"8001","agencyId":"7001","name":"旅行社业务经营许可证","qualificationType":"TRAVEL_AGENCY_LICENSE","qualificationTypeLabel":"旅行社业务经营许可证","fileUrl":"https://example.invalid/temp?token=masked","fileType":"PDF","fileTypeLabel":"PDF","issueDate":"2024-01-01","expireDate":null,"sortOrder":1,"remark":null,"createTime":"2026-08-01T10:00:00","updateTime":"2026-08-01T10:00:00"
}],"message":"操作成功","success":true
}`。
#### 5.7.2 新增资质
- `POST /v3/admin/travel-agency/<agencyId>/qualification`;ADMIN+;非幂等。
- **入参/响应**`agencyId:string!` + `QualificationSave` 全部 8 字段;成功 `data:string` 新 qualificationId。
- **错误/边界**:文件无效(>10MB、非 PDF/JPG/PNG、上传失败`594008`;长期有效传 `expireDate:null`
- **示例**:请求 `{
"name":"旅行社业务经营许可证","qualificationType":"TRAVEL_AGENCY_LICENSE","fileUrl":"oss://private/masked.pdf","fileType":"PDF","issueDate":"2024-01-01","expireDate":null,"sortOrder":1
}` → `{
"code":200,"data":"8001","message":"操作成功","success":true
}`。
#### 5.7.3 编辑资质
- `PUT /v3/admin/travel-agency/<agencyId>/qualification/<qualificationId>`;ADMIN+;目标更新可重试。
- **入参/响应**:路径中 `agencyId` 用于 URL 归类,后端操作键为 `qualificationId:string!`;body `QualificationSave` 全部字段;成功 `data:null`
- **错误/示例**:不存在 `594001`;文件无效 `594008`。请求 `{
"name":"经营许可证(新)","qualificationType":"TRAVEL_AGENCY_LICENSE","fileUrl":"oss://private/masked.pdf","fileType":"PDF"
}` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
#### 5.7.4 删除资质
- `DELETE /v3/admin/travel-agency/<agencyId>/qualification/<qualificationId>`;ADMIN+;软删;不保证重复调用成功。
- **入参/响应**`qualificationId:string!`;成功 `data:null`;不存在 `594001`
- **示例**`DELETE .../7001/qualification/8001` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
### 5.8 AdminTravelAgencyPaymentController5
`PaymentResp={
paymentId:string,agencyId:string,name,mchId,appId,apiV3Key,mchSerialNo,publicKeyId,privateKeyPath,publicKeyPath,isDefault:int,status,statusLabel,remark,createTime,updateTime
}`。
`PaymentSave={
name:string!(max64),mchId:string!(max32),appId:string!(max64),apiV3Key:string!,mchSerialNo:string?,publicKeyId:string?,isDefault:int?(0/1),status:string?(ENABLED/DISABLED),remark:string?
}`。这些接口包含高敏密钥/证书信息,仅 SUPER_ADMIN 使用,响应不得写入日志、埋点或客户端持久存储。
#### 5.8.1 支付配置列表
- `GET /v3/admin/travel-agency/<agencyId>/payment`;SUPER_ADMIN;幂等;`agencyId:string!`
- **响应(完整)**`data[]` 每项含 `PaymentResp` 全部 16 字段。
- **示例(脱敏)**`GET .../7001/payment` → `{
"code":200,"data":[{
"paymentId":"9001","agencyId":"7001","name":"微信支付-主商户","mchId":"1106******39","appId":"wx************ff36","apiV3Key":"***","mchSerialNo":"***","publicKeyId":"PUB_***","privateKeyPath":"cert/***/apiclient_key.pem","publicKeyPath":"cert/***/public_key.pem","isDefault":1,"status":"ENABLED","statusLabel":"启用","remark":null,"createTime":"2026-08-01T10:00:00","updateTime":"2026-08-01T10:00:00"
}],"message":"操作成功","success":true
}`。
#### 5.8.2 新增支付配置
- `POST /v3/admin/travel-agency/<agencyId>/payment`;SUPER_ADMIN;非幂等。
- **入参/响应**`agencyId:string!` + `PaymentSave` 全部 9 字段;成功 `data:string` 新 paymentId。`privateKeyPath/publicKeyPath` 不接受前端传入。
- **错误/边界**:商户号重复 `594011`;非 SUPER_ADMIN `594009`;必填/长度校验。
- **示例(虚构)**:请求 `{
"name":"微信支付-主商户","mchId":"1900000001","appId":"wxdemo000000000001","apiV3Key":"***REDACTED***","mchSerialNo":"***REDACTED***","publicKeyId":"PUB_DEMO","isDefault":1,"status":"ENABLED"
}` → `{
"code":200,"data":"9001","message":"操作成功","success":true
}`。
#### 5.8.3 编辑支付配置
- `PUT /v3/admin/travel-agency/<agencyId>/payment/<paymentId>`;SUPER_ADMIN;目标更新可重试。
- **入参/响应**URL 含 `agencyId`,操作键为 `paymentId:string!`;body `PaymentSave` 全部字段;成功 `data:null`
- **错误/示例**:不存在 `594001`;商户号冲突 `594011`。请求 `{
"name":"微信支付-主商户","mchId":"1900000001","appId":"wxdemo000000000001","apiV3Key":"***REDACTED***","status":"ENABLED"
}` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
#### 5.8.4 删除支付配置
- `DELETE /v3/admin/travel-agency/<agencyId>/payment/<paymentId>`;SUPER_ADMIN;不保证重复调用成功。
- **入参/响应**`paymentId:string!`;成功 `data:null`
- **错误/边界**:不存在 `594001`;默认商户不可删 `594007`,须先将另一条设为默认。
- **示例**`DELETE .../7001/payment/9002` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
#### 5.8.5 设为默认支付商户
- `PUT /v3/admin/travel-agency/<agencyId>/payment/<paymentId>/set-default`;SUPER_ADMIN;目标状态幂等。
- **入参/响应**`agencyId:string!`,`paymentId:string!`;无 body;成功 `data:null`
- **错误/边界**:支付配置不存在/归属不符 `594001`;同一公司最多一条 `isDefault=1`
- **示例**`PUT .../7001/payment/9001/set-default` → `{
"code":200,"data":null,"message":"操作成功","success":true
}`。
## 6. 枚举/数据字典(按类型分表)
### 6.1 `contract_status`
| 值 | 中文 | 说明 |
|---|---|---|
| `PENDING` | 待生成 | 初始等待 |
| `GENERATED` | 已生成 | 合同已生成,待签署 |
| `SIGNING` | 签署中 | 平台签署流程中 |
| `SIGNED` | 已签署 | 签署完成 |
| `REPORTED` | 已上报 | SYNC 报备完成 |
| `UPLOADED` | 已上传 | SYNC PDF 已上传 |
| `VOIDING` | 作废中 | 等待平台确认 |
| `VOIDED` | 已作废 | 终态 |
### 6.2 `contract_platform`
| 值 | 中文 | 说明 |
|---|---|---|
| `12301` | 12301 团队报送 | 监管平台 |
| `LOCAL` | 本地/线下 | 本地报备 |
| `TENCENT_ESIGN` | 腾讯电子签 | 电子签约 |
### 6.3 `contract_mode`
| 值 | 中文 | 说明 |
|---|---|---|
| `STANDARD` | 电子签约 | 平台生成并签署 |
| `SYNC` | 线下报备 | 线下签署后报备/上传 |
### 6.4 `common_status` / `contract_scheme_status` / `contract_template_status`
| 值 | 中文 | 说明 |
|---|---|---|
| `ACTIVE` | 启用 | 可供新业务选择 |
| `INACTIVE` | 停用 | 历史引用不受影响 |
### 6.5 `contract_attachment_type`
| 值 | 中文 | 说明 |
|---|---|---|
| `PDF` | PDF | PDF 附件 |
| `IMAGE` | 图片 | 图片附件 |
| `OTHER` | 其他 | 其他文件 |
### 6.6 `agency_status`
| 值 | 中文 | 说明 |
|---|---|---|
| `ENABLED` | 启用 | 可被产品/合同选择 |
| `DISABLED` | 停用 | 不可用于新业务 |
### 6.7 `agency_contract_platform`
| 值 | 中文 | 说明 |
|---|---|---|
| `12301` | 12301 团队报送 | 旅行社已开通 12301 |
| `TENCENT_ESIGN` | 腾讯电子签 | 旅行社已开通腾讯电子签 |
### 6.8 `agency_qualification_type`
| 值 | 中文 | 说明 |
|---|---|---|
| `BUSINESS_LICENSE` | 营业执照 | 公司营业执照 |
| `TRAVEL_AGENCY_LICENSE` | 旅行社业务经营许可证 | 旅行社经营资质 |
| `VALUE_ADDED_TELECOM_LICENSE` | 增值电信业务经营许可证 | 电信业务资质 |
### 6.9 `agency_qualification_file_type`
| 值 | 中文 | 说明 |
|---|---|---|
| `PDF` | PDF | PDF 文件 |
| `IMAGE` | 图片 | JPG/PNG 等 |
| `OTHER` | 其他 | 其他类型(服务端仍会做安全校验) |
### 6.10 其他取值
| 字段 | 值 | 说明 |
|---|---|---|
| `contractType` | `TOUR` / `INSURANCE` | 旅游合同 / 保险单 |
| `signatoryMode` | `1` / `2` / `3` | 短信 / 现场 / 线下 |
| `paymentMethod` | `1` / `2` / `3` | 现金 / 转账 / 在线 |
| `disputeResolution` | `1` / `2` | 诉讼 / 仲裁 |
| `insurancePurchaseMethod` | `1` / `2` / `3` | 委托旅行社 / 自行购买 / 放弃 |
| `gender` | `0` / `1` / `2` | 未知 / 男 / 女 |
| `idCardType` | `1` / `2` | 身份证 / 护照 |
| `channel` | `MP` / `ADMIN` / `CHANNEL` / `null` | 小程序 / 管理端 / 分销 / 兜底 |
## 7. 错误码摘要
| code | 含义 | 典型接口 |
|---:|---|---|
| 510001 | 合同不存在 | 详情/作废/下载/刷新/上传 |
| 510005-510008 | 短信重发状态、频控或必要数据缺失 | 重发短信 |
| 510101-510107 | 下载/刷新/作废/PDF 上传业务校验 | 合同写操作 |
| 510201-510220 | 旅行社、方案、模板、订单、签署人及重复合同校验 | 创建/方案管理 |
| 510403 | 腾讯电子签不支持 SYNC | 报备合同 |
| 510506-510508 | 附件/导游不存在或上传失败 | 方案附件/导游 |
| 594001 | 旅行社或其子资源不存在 | 旅行社/资质/支付 |
| 594005-594006 | 主体公司约束 | 停用/删除/设为主体 |
| 594007 | 默认支付商户不可删 | 删除支付配置 |
| 594008 | 资质文件无效 | 新增/编辑资质 |
| 594009 | 仅 SUPER_ADMIN 可执行 | 敏感管理操作 |
| 594010 | 旅行社编码重复 | 新建旅行社 |
| 594011 | 微信支付商户号冲突 | 新增/编辑支付配置 |
## 验证证据
- PR #5501 已合并merge commit `45117d3d5896515909e303dcbfa9d3616f881803`,head `f66d236f2aea2d0a63db8cb58d59f2ae74e2b6fb`
- 基于合并代码静态核对 8 个 admin Controller,端点计数 `15+7+4+4+6+11+4+5=56`
- 文档本地自检56 个接口小节、只有目标文件进入 commit、`git diff --check` 通过。
- deploy-panel 任务 `0eae2ffb` 执行成功;`hl-order-service-v3``dev-v3` 的 8086/8186 双实例均为 UP。
- 通过已登录管理后台的有效 Gateway 鉴权完成合同列表真实 HTTP 验证:`code=200``total=118`;共12个只读用例覆盖 `orderNo` 精确/模糊/trim、`teamNo``%`/`_` 字面匹配等边界。
- 真实非空样本为 `orderNo=HL20260803220907715``teamNo=26-2489`;随后在同一鉴权会话打开目标合同 `MOCK-2084280429048741890` 详情,详情显示 `teamNo=26-2489` 且含列表外字段,证明详情调用成功且列表/详情一致。
- QA 证据报告:`D:/work/project-doc/PRPs/reports/5368-contract-teamno-gateway-acceptance.md`,状态仍为 `PARTIAL`。原因是详情页含未脱敏联系电话/签署链接,为避免 PII 落盘未保存截图;运行时列表与详情功能门禁已验证。
- 本次未查询数据库实时行;上述结论来自部署状态与真实 Gateway HTTP/管理后台详情调用。
## 8. 业务边界与 v1/v3 数据隔离
- 根据 Issue #5368 在 2026-08-04 的决策,管理后台合同整页的列表、详情、创建、作废、刷新、下载、短信和上传统一调用 `/v3/admin/*`
- v1 存量合同不迁移,自然消亡;新合同全部走 v3。
- v1 与 v3 合同数据不互通;不得把 v3 响应的 `contractId` 传给 v1 端点,也不得用 v3 详情读 v1 旧 ID。
- `teamNo` 未生成时为 `null`;不得用 `orderNo` 伪装团号。
- 列表多条件为 AND;文本筛选是数据库分页前的包含匹配,`total` 与翻页结果一致。
## 9. 修改前后对比
| 维度 | 修改前 | 修改后 |
|---|---|---|
| 订单号搜索 | 合同列表不能独立按 `orderNo` 查询 | 可传 `orderNo` 包含匹配 |
| 文本空白 | 语义未完整固化 | 三个文本条件 trim,全空白忽略 |
| LIKE 特殊字符 | 可能被当作通配符 | `%``_``\` 按字面字符 |
| 数据版本 | 前端可能仍混用 v1/v3 | 合同整页读写统一 v3,v1 存量不迁移 |
## 10. 影响评估/回滚
- **向后兼容**:是。`orderNo` 为新增可选 query;原有参数和响应字段不删除。
- **前端是否必须同步上线**:不必与后端强绑同时上线;但合同页应尽快切到本文的 v3 端点,否则无法正确消费 v3 数据。
- **回滚后前端行为**:停止传递 `orderNo`;保留原 `teamNo/contactName` 及其他查询。不得回滚为 v1/v3 混用。
## 11. 注意事项
- 前端可清理“拉全量合同后在浏览器按订单号过滤”的 workaround,直接传 `orderNo`
- 请求参数中用户输入的 `%``_``\` 不需前端自行转义;正常 URL 编码即可。
- 支付配置、证书、身份证、手机号和签约密钥都不得写入前端日志/埋点;本文示例全部为虚构脱敏值。
## 12. 关联/联系人
- **Issue**: [#5368](https://git.1814.love:8443/wx/HL/issues/5368)
- **PR**: [#5501](https://git.1814.love:8443/wx/HL/pulls/5501)
- **Merge commit**: [45117d3d5896515909e303dcbfa9d3616f881803](https://git.1814.love:8443/wx/HL/commit/45117d3d5896515909e303dcbfa9d3616f881803)
- **Head commit**: [f66d236f2aea2d0a63db8cb58d59f2ae74e2b6fb](https://git.1814.love:8443/wx/HL/commit/f66d236f2aea2d0a63db8cb58d59f2ae74e2b6fb)
- **前置契约**: PR #5386 / commit `c9d1b12618``teamNo` 与 v3 写侧说明
- **后端负责人**: 腰苏图
## 关联/联系人
### 链接
- [后端工单 #5368](https://git.1814.love:8443/wx/HL/issues/5368)
- [后端 PR #5501](https://git.1814.love:8443/wx/HL/pulls/5501)
- Merge commit: `45117d3d58`
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,92 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5444"
title: "Step2 canonical full snapshot 与稳定槽位"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@6e4c6b224b204cd792e02f29a6e61689b0d9a588"
target_release: "v2.1"
verified_at: "2026-08-04"
status_note: "后端 canonicalSnapshot 契约已发布并完成网关验证;管理后台已由 Pi 领取并开始适配。"
updated_at: "2026-08-04"
base: "dev-v3"
generated: "2026-08-03T23:34:18+08:00"
---
# 【修改接口·管理后台】Step2 canonical full snapshot 与稳定槽位
> #5444#5366 B08/B09 后端):派车弹窗 Step2 canonical full snapshot 与稳定槽位。
> 后端实现已合入 PR #5448merge 8ceb8c5;测试部署 task 80234589;网关验证通过。
## 关联
- Issue: #5444
- PR: [#5448](https://git.1814.love:8443/wx/HL/pulls/5448)
### 链接
- **Issue**: [#5444](https://git.1814.love:8443/wx/HL/issues/5444)
- **PR**: [#5448](https://git.1814.love:8443/wx/HL/pulls/5448)
- **Merge commit**: [8ceb8c5cf2](https://git.1814.love:8443/wx/HL/commit/8ceb8c5cf2)
### 联系人
- **后端负责人**: @wx
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| `POST` | `/admin/fleet/assignments/candidates` | 响应新增 `canonicalSnapshot` 区块;请求新增可选同代校验参数additive,向后兼容 |
## 契约影响文件
- `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/test/java/com/hulalv/fleet/assignment/controller/AssignmentControllerTest.java`
## 请求新增(均可选)
- `expectedPlanGeneration`Long·StringStep2 幂等重试/同代校验期望计划代际;携带且与当前 canonical 快照不符时以 `605061` 拒绝generation 漂移 fail-closed
- `expectedSnapshotVersion`Long·String同上,期望快照版本。
## 响应新增 `canonicalSnapshot` 区块additive
- `planGeneration`Long·String当前计划代际不透明令牌;与 `snapshotVersion` 共同标识同一份 canonical 快照。
- `snapshotVersion`Long·String当前快照版本;同代内容修订递增。
- `retainedSlotIds`Long·String[]):有序 RetainedSlotSet稳定槽位集合;不重编号、不丢 protected/active slot、至少 1 个。
- `editableServiceDates`LocalDate[]):完整有序可编辑服务日。
- `cells`:每个 `RetainedSlotSet × editableServiceDates` 笛卡尔积位置唯一 cell
- `slotId`Long·String
- `serviceDate`LocalDate
- `used``USED` / `UNUSED` / `null`(无切片行=未编辑)
- `readOnly`Boolean只读 cell 不可被批量操作改写
- `readOnlyReason`String只读原因;可编辑为 null派单已完结 / 派单已取消 / 服务日期已过去 / 对账期已关账)
- `vehicleId` / `driverId`Long·String,未派或不用车为 null
- `selectedVehicle`(对象,未选或资源失效为 null`vehicleId` / `plate` / `modelName` / `seats`
- `selectedDriver`(对象,未选或资源失效为 null`driverId` / `name` / `maskedPhone`(司机域脱敏)/ `driverStatus` / `season`
## 行为说明B08/B09
- 首次进入 Step2 且不存在稳定槽位时,后端在 requirement 锁 + 独立新事务内原子生成至少 1 个稳定 `slotId` 并落 `fleet_assignment` unassigned 每日切片;同一 `planGeneration + snapshotVersion` 下重复读取/幂等重试不重复建槽、不重编号。
- `selectedVehicle`/`selectedDriver` 为与 `snapshotVersion` 同代的已选资源展示快照,独立于候选分页/筛选;null/失效资源 fail-closed 不编造。
- 未传 `requirementId` 或需求上下文不可用时 `canonicalSnapshot``null`,候选查询本身不受影响。
## 前端/调用方动作
- Step2 前端可消费 `canonicalSnapshot` 区块组织稳定槽位 UI;重复读取/幂等重试时回传 `expectedPlanGeneration`/`expectedSnapshotVersion` 做同代校验。
- 不新增前端建槽 API 与二次批量查询 API;`selectedVehicle`/`selectedDriver` 直接取自现有 `selectedVehicleId`/`selectedDriverId` 入参。
- 未消费新字段的既有调用保持兼容additive
## 验证证据
- 定向测试:`Step2CanonicalSnapshotServiceTest`11 用例:首次原子建槽/同代幂等不重编号/并发/漂移 fail-closed/脱敏/跨页过滤/null 失效/readOnly cell`AssignmentCandidateServiceTest``AssignmentControllerTest`canonicalSnapshot 挂载 + 透传),全部通过。
- Fleet 全量测试3022/3023 通过;唯一失败 `FleetInsuranceTaskTeamNoMysqlTest` 为 Testcontainers 无 Docker 环境型失败(基线上同样失败)。
- Spotless `spotless:check` 通过。
- 网关验证:`https://api.test.1814.love:9443` POST /admin/fleet/assignments/candidates 真实订单 3 槽×3 天 9 cell;同代 expected 重试 snapshotVersion 不变;漂移 605061 fail-closed;无 requirement/未知需求快照 null。证据 `D:/evidence/5444-gateway-step2-snapshot.json`
- 兼容性结论:请求/响应均为 additive 扩展,向后兼容。

查看文件

@ -1,125 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5446"
title: "HOLD 通知管理端状态查询与受控重试"
consumer: "admin"
author: "wx(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@6e4c6b224b204cd792e02f29a6e61689b0d9a588"
target_release: "v2.1"
verified_at: "2026-08-04"
status_note: "后端 PR #5447 已合并 dev-v3merge 15d79a8d4;测试部署 task ba1b84af 成功;网关验证 5 项通过(参数校验/missing/stale 拒绝);管理后台已由 Pi 领取并开始适配。"
updated_at: "2026-08-04"
base: "dev-v3"
generated: "2026-08-03T23:19:24+08:00"
---
# 【新增接口·管理后台】HOLD 通知管理端状态查询与受控重试
## 关联
- Issue: [#5446](https://git.1814.love:8443/wx/HL/issues/5446)
- Backend tracking: [#5366 B10](https://git.1814.love:8443/wx/HL/issues/5366)
- PR: [#5447](https://git.1814.love:8443/wx/HL/pulls/5447)merge commit 15d79a8d4
### 链接
- **Issue**: [#5446](https://git.1814.love:8443/wx/HL/issues/5446)
- **PR**: [#5447](https://git.1814.love:8443/wx/HL/pulls/5447)
- **Merge commit**: [15d79a8d4b](https://git.1814.love:8443/wx/HL/commit/15d79a8d4b)
### 联系人
- **后端负责人**: @wx
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| `GET` | `/admin/fleet/assignments/hold-notification/status` | `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/controller/AssignmentHoldNotificationAdminController.java` |
| `POST` | `/admin/fleet/assignments/hold-notification/retry` | 同上 |
路径前缀 `/admin/fleet/**` 已由网关登录/角色校验与 `FleetAdminRoleGuardInterceptor`VEHICLE_MANAGER / SUPER_ADMIN收口,无需新增网关规则。
## 1. 状态查询 `GET /admin/fleet/assignments/hold-notification/status`
请求参数query
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `assignmentGroupId` | `String(Long)` | 是 | 派车组 ID |
| `notificationGeneration` | `String(Long)` | 是 | HOLD 通知代际 |
| `attemptId` | `String` | 否 | 调用方生成的 attempt 标识(幂等/审计定位,≤64 字符 |
响应 `data`
| 字段 | JSON 类型 | 可空 | 说明 |
|---|---|---|---|
| `assignmentGroupId` | `String(Long)` | 否 | 回显 |
| `notificationGeneration` | `String(Long)` | 否 | 回显 |
| `assignmentStatus` | `String` | 是 | 派车状态;HOLDING=仍在待确认,其余=已离开 holding |
| `deliveryStatus` | `String` | 否 | `NOT_FOUND/PENDING/SENT/FAILED/AMBIGUOUS/INVALIDATED``SENT` 只表示系统发送成功,不表示通道送达回执 |
| `messageLogId` | `String(Long)` | 是 | 本地通知日志 ID |
| `outboxEventId` | `String(Long)` | 是 | 可靠通知 Outbox 事件 ID |
| `outboxStatus` | `String` | 是 | `PENDING/PROCESSING/QUARANTINED/SUCCESS` |
| `retryCount` | `Integer` | 是 | Outbox 累计处理重试次数 |
| `sentAt` | `String(date-time)` | 是 | 有可信发送成功事实时返回 |
| `dispatchAttemptedAt` | `String(date-time)` | 是 | 最近一次向通知中心发起分发的时间 |
| `lastError` | `String` | 是 | 最近失败/对账留痕(脱敏) |
| `canRetry` | `Boolean` | 否 | 是否允许管理端重试:仅明确失败且未发送成功且未取消时为 `true` |
| `lastReplayAttemptId` | `String` | 是 | 上次管理端重试 attemptId |
| `lastReplayReason` | `String` | 是 | 上次管理端重试原因 |
| `lastReplayedBy` | `String(Long)` | 是 | 上次重试操作人 |
| `lastReplayedAt` | `String(date-time)` | 是 | 上次重试时间 |
| `attemptStatus` | `String` | 是 | 本次请求 attempt 处理状态:`ACCEPTED/SUCCEEDED/REJECTED/AMBIGUOUS`(持久幂等:同一 attemptId 重复提交直接返回已记录结果) |
| `attemptResultNote` | `String` | 是 | 本次 attempt 处理结果摘要(脱敏) |
错误码400 参数校验 / `100001` 派车组不存在 / `100003` 通知代际已过期 / 401 未登录。
## 2. 受控重试 `POST /admin/fleet/assignments/hold-notification/retry`
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `assignmentGroupId` | `String(Long)` | 是 | 派车组 ID |
| `notificationGeneration` | `String(Long)` | 是 | HOLD 通知代际 |
| `attemptId` | `String` | 是 | 本次重试 attempt 标识(幂等键组成部分 + 审计,≤64 字符 |
| `reason` | `String` | 是 | 核查失败原因后的重试说明,≤200 字 |
行为约束:
- 幂等键 = `assignmentGroupId:notificationGeneration:attemptId`,持久化于 `fleet_hold_notification_retry_attempt` 唯一索引,重复提交(含 Redis 防重窗口过期后)幂等返回已记录结果,不重复发送、不重复写 Outbox;
- 仅允许明确失败(`FAILED`,Outbox `QUARANTINED``PENDING`+错误)的 HOLD 通知重试;`QUARANTINED` 重置失败预算,`PENDING`+错误立即重试;
- `UNKNOWN/AMBIGUOUS`dispatching先按通知中心供应商发送日志对账存在真实外部成功则补记发送事实并返回 `SENT`,仍无法判定则拒绝重试fail closed
- stale代际过期/missing派车组或日志不存在/wrong identity 一律拒绝;
- 重试复用原 Outbox 事件与供应商幂等键,不新建事件、不直接外呼,不改变派车状态;
- 操作人、原因、attemptId 记入 Outbox 审计列(`last_replay_*`)。
响应:同状态查询 `data`(重试后最新事实)。
错误码400 参数校验 / `100001` 派车组或通知日志不存在 / `100003` 通知代际已过期或已取消 / `100503` 资源竞争(并发重试)/ 401 未登录。
## 契约影响文件
- `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/controller/AssignmentHoldNotificationAdminController.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/service/AssignmentHoldNotificationAdminService.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/vo/AssignmentHoldNotificationStatusReqVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/vo/AssignmentHoldNotificationStatusRespVO.java`
- `hl-fleet-service/src/main/java/com/hulalv/fleet/messagetemplate/vo/AssignmentHoldNotificationRetryReqVO.java`
- `hl-fleet-service/src/main/java/db/migration/V20260803_008__add_outbox_hold_replay_attempt_id.java`
## 前端/调用方动作
管理后台新增适配:派车详情/司机通知维度展示 `deliveryStatus``canRetry``canRetry=true` 时提供重试按钮,重试需携带调用方生成的 `attemptId` 与原因;`AMBIGUOUS` 不提供重试入口(等待自动对账)。所有 Long ID 按 String 消费。
## 验证证据
- 定向测试:`AssignmentHoldNotificationAdminServiceTest` 24 项 / `AssignmentHoldNotificationAdminControllerTest` 5 项 / `HoldNotificationRetryAttemptMapperTest` 2 / `WechatMessageLogMapperTest` 2 / `AssignmentInsuranceOutboxMapperTest` 18 / 迁移可重入测试 2Fleet reactor verify 3047 项 0 failures,1 项 Docker 环境型 error 与本任务无关)
- Spotless: check 通过
- 网关验证:经 api.test.1814.love:9443 实测 5 项status 缺参 400 / 组不存在 100001 / 过期代际 100003 / retry 缺 attemptId 400 / retry 过期代际 100003,证据 sha256 c8759216
- 兼容性结论:纯新增端点,无既有字段或行为变更

查看文件

@ -1,213 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5452"
title: "车务 4 个列表接口非法日期参数统一友好错误文案"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@09808c07c18719e30ee7a8e2c8005852ea673e26"
target_release: "v2.1"
verified_at: "2026-08-04"
status_note: "后端完成PR #5465 已合并 dev-v3 并部署 TEST,网关验证 4 接口 × 2 种非法日期格式均返回统一友好文案(无 Spring 内部异常文本);前端需确认错误提示展示无需再适配旧文案。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 车务: 4 个列表接口非法日期参数统一友好错误文案
> **服务**: hl-fleet-service
> **PR**: #5465
> **Issue**: #5452
> **日期**: 2026-08-04
> **影响范围**: 管理后台车务端看板/司机/车辆/保险任务列表的日期筛选参数
---
## ⚠️ 关键变化
非法日期格式(如 `2026/05/01``2026-02-30`的报错文案由「Spring 内部异常堆栈长串」改为统一友好文案「参数【x】格式不正确」,与 order-v3 订单列表口径一致。
- 以前:`code=400``message``Failed to convert property value of type 'java.lang.String' to required type 'java.time.LocalDate' ... ConversionFailedException ... Parse attempt failed`(前端不可读,且泄漏内部异常类名与嵌套链)。
- 现在:`code=400``message``参数【startDayFrom】格式不正确`(日期格式形如 `2026-02-30` 等已符合 yyyy-MM-dd 但日期不存在时,追加提示「(日期请用 yyyy-MM-dd,日期时间请用 yyyy-MM-dd'T'HH:mm:ss
HTTP 状态、`code``data` 结构均不变;合法日期yyyy-MM-dd行为不变。
## 变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 看板列表 | GET | `/admin/fleet/board/orders` | 错误文案修改 | 日期参数非法时统一友好文案 |
| 2 | 司机档案分页 | GET | `/admin/fleet/drivers/page` | 错误文案修改 | 同上 |
| 3 | 车辆分页 | GET | `/admin/fleet/vehicles/page` | 错误文案修改 | 同上 |
| 4 | 保险任务列表 | GET | `/admin/fleet/insurance/tasks` | 错误文案修改 | 同上 |
## 接口详情
### 1. 看板列表 `GET /admin/fleet/board/orders`
**日期类入参**(全部可选,格式 `yyyy-MM-dd`
| 参数 | 类型 | 说明 |
|------|------|------|
| `startDayFrom` | String(date) | 行程区间起(含) |
| `startDayTo` | String(date) | 行程区间止(含) |
| `startDate` | String(date) | 日期区间起别名(未传 startDayFrom 时生效) |
| `endDate` | String(date) | 日期区间止别名(未传 startDayTo 时生效) |
**异常示例**(非法日期格式):
```text
GET /admin/fleet/board/orders?page=1&pageSize=20&startDayFrom=2026/05/01
Authorization: Bearer <token>
(无请求体)
```
```json
{
"code": 400,
"message": "参数【startDayFrom】格式不正确",
"data": null,
"traceId": null,
"success": false
}
```
```text
GET /admin/fleet/board/orders?page=1&pageSize=20&startDayFrom=2026-02-30
```
```json
{
"code": 400,
"message": "参数【startDayFrom】格式不正确日期请用 yyyy-MM-dd,日期时间请用 yyyy-MM-dd'T'HH:mm:ss",
"data": null,
"traceId": null,
"success": false
}
```
**典型成功示例**
```text
GET /admin/fleet/board/orders?page=1&pageSize=20&startDayFrom=2026-05-01
Authorization: Bearer <token>
```
```json
{"code": 200, "message": "成功", "data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
}, "success": true}
```
### 2. 司机档案分页 `GET /admin/fleet/drivers/page`
**日期类入参**(全部可选,格式 `yyyy-MM-dd`
| 参数 | 类型 | 说明 |
|------|------|------|
| `licenseExpireBefore` | String(date) | 驾照到期 ≤ 该日 |
| `insuranceAnnualEndBefore` | String(date) | 年保到期 ≤ 该日(仅 annual 行命中) |
**异常示例**
```text
GET /admin/fleet/drivers/page?page=1&pageSize=20&licenseExpireBefore=2026/07/01
Authorization: Bearer <token>
```
```json
{
"code": 400,
"message": "参数【licenseExpireBefore】格式不正确",
"data": null,
"traceId": null,
"success": false
}
```
### 3. 车辆分页 `GET /admin/fleet/vehicles/page`
**日期类入参**(全部可选,格式 `yyyy-MM-dd`
| 参数 | 类型 | 说明 |
|------|------|------|
| `insureDueBefore` | String(date) | 保险到期 ≤ 该日(含当日) |
| `inspectDueBefore` | String(date) | 年检到期 ≤ 该日(含当日) |
**异常示例**
```text
GET /admin/fleet/vehicles/page?page=1&pageSize=20&insureDueBefore=2026/07/01
Authorization: Bearer <token>
```
```json
{
"code": 400,
"message": "参数【insureDueBefore】格式不正确",
"data": null,
"traceId": null,
"success": false
}
```
### 4. 保险任务列表 `GET /admin/fleet/insurance/tasks`
**日期类入参**(全部可选,格式 `yyyy-MM-dd`
| 参数 | 类型 | 说明 |
|------|------|------|
| `serviceDateFrom` | String(date) | 服务日起(含) |
| `serviceDateTo` | String(date) | 服务日止(含) |
**异常示例**
```text
GET /admin/fleet/insurance/tasks?page=1&pageSize=20&serviceDateFrom=2026/07/01
Authorization: Bearer <token>
```
```json
{
"code": 400,
"message": "参数【serviceDateFrom】格式不正确",
"data": null,
"traceId": null,
"success": false
}
```
## 错误码
| code | 含义 | 说明 |
|------|------|------|
| 400 | 参数格式错误 | 日期参数非法格式;message 统一为「参数【字段名】格式不正确」 |
## 前端需要做什么
- 无需修改请求/响应字段结构;日期筛选组件仍按 `yyyy-MM-dd` 提交。
- 建议核对:错误提示直接展示 `message` 即可,不需要再解析/兜底 Spring 异常长串;如前端此前针对旧文案写过 workaround如截取、正则清洗,可清理。
## 验证证据
- 集成测试4 接口 × 2 种非法格式(斜杠分隔、不存在的日期)断言 `code=400` + 统一文案 + 不含 `ConversionFailedException`/`IllegalArgumentException`/`Failed to convert`
- `mvn -pl hl-fleet-service -am verify` 通过(本次改动相关 3072 用例全绿;仅 2 个环境性失败与本次无关Docker 缺失的保险集成测试 + 偶发时序的 releasee 进程测试,复跑通过)。
- 测试环境网关验证4 接口 × 2 种非法格式均返回统一友好文案;合法日期调用不受影响。
## 关联 / 联系人
### 链接
- **Issue**: [#5452](https://git.1814.love:8443/wx/HL/issues/5452)
- **PR**: [#5465](https://git.1814.love:8443/wx/HL/pulls/5465)
- **Merge commit**: [70483cfb63](https://git.1814.love:8443/wx/HL/commit/70483cfb63)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,142 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5455"
title: "看板列表非法枚举参数显式报错vehicleTypeKeys/statuses 同为拒绝)"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@09808c07c18719e30ee7a8e2c8005852ea673e26"
target_release: "v2.1"
verified_at: "2026-08-04"
status_note: "后端完成PR #5466 已合并 dev-v3 并部署 TEST,网关验证非法枚举返 100001、合法枚举筛选回归不变;前端需对 100001 错误码做提示处理。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 车务: 看板列表非法枚举参数显式报错vehicleTypeKeys/statuses 同为拒绝)
> **服务**: hl-fleet-service
> **PR**: #5466
> **Issue**: #5455
> **日期**: 2026-08-04
> **影响范围**: 管理后台车务端看板列表/汇总的车型与状态筛选参数
---
## ⚠️ 关键变化
`GET /admin/fleet/board/orders` 的枚举筛选参数传入非法值时,不再静默失效,统一返回 `100001 参数非法`(与 `variant` 非法值口径一致)。
| 参数 | 以前的行为 | 现在的行为 |
|------|-----------|-----------|
| `vehicleTypeKeys` / `typeKeys` | 非法值静默忽略 → 返回全量数据(筛选静默失效,如 `BAD_TYPE` → total=66 | 返回 `100001`,message 指明非法值与合法枚举 |
| `statuses` / `status` | 非法值静默忽略 → 返回 0 条(如 `BAD_STATUS` → total=0 | 返回 `100001`,message 指明非法值与合法枚举 |
**合法值不变**
- 车型:`suv`(越野)/ `mpv`(商务车)/ `bus`(大巴)/ `sedan`(轿车),支持多选、逗号分隔,兼容历史大写与中文别名(如 `越野``商务`)。
- 状态:`unassigned`(待派)/ `unassigned_urgent`(待派·临近出团)/ `holding`(排车锁定)/ `holding_urgent`(排车超时)/ `assigned`(已派)/ `change_requested`请求换车,M1 恒空)/ `canceled`(已取消)/ `completed`(已完成),支持中英文别名、大小写与逗号分隔。
## 变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 看板列表 | GET | `/admin/fleet/board/orders` | 校验新增 | 非法枚举返 100001 |
> 说明:`/admin/fleet/board/summary` 按设计忽略 `statuses`/`status`(不参与汇总过滤),非法状态值不报错,行为不变;`vehicleTypeKeys` 在汇总中参与过滤,非法值同样返 100001。
## 接口详情
### 1. 看板列表 `GET /admin/fleet/board/orders`
**枚举入参**(全部可选):
| 参数 | 类型 | 合法值 | 说明 |
|------|------|--------|------|
| `vehicleTypeKeys` | String[] | `suv`/`mpv`/`bus`/`sedan` | 车型大类多选,任一命中即返;未派按需求车型、已派按实际车辆大类过滤 |
| `typeKeys` | String[] | 同上 | 车型多选别名(未传 vehicleTypeKeys 时生效) |
| `statuses` | String[] | `unassigned`/`unassigned_urgent`/`holding`/`holding_urgent`/`assigned`/`change_requested`/`canceled`/`completed` | 多状态筛选,任一命中即返;空=不过滤 |
| `status` | String | 同上 | 状态筛选别名(单值或逗号分隔;与 statuses 合并) |
**异常示例**(非法车型枚举):
```text
GET /admin/fleet/board/orders?page=1&pageSize=20&vehicleTypeKeys=BAD_TYPE
Authorization: Bearer <token>
(无请求体)
```
```json
{
"code": 100001,
"message": "参数非法: vehicleTypeKeys 仅支持 suv/mpv/bus/sedan,传入非法值BAD_TYPE",
"data": null,
"traceId": null,
"success": false
}
```
**异常示例**(非法状态枚举,与车型同为拒绝口径):
```text
GET /admin/fleet/board/orders?page=1&pageSize=20&statuses=BAD_STATUS
```
```json
{
"code": 100001,
"message": "参数非法: statuses 含非法状态值BAD_STATUS合法值unassigned/unassigned_urgent/holding/holding_urgent/assigned/change_requested/canceled/completed",
"data": null,
"traceId": null,
"success": false
}
```
**典型成功示例**(合法多选):
```text
GET /admin/fleet/board/orders?page=1&pageSize=20&vehicleTypeKeys=suv,mpv&statuses=unassigned_urgent
Authorization: Bearer <token>
```
```json
{"code": 200, "message": "成功", "data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
}, "success": true}
```
## 错误码
| code | 含义 | 说明 |
|------|------|------|
| 100001 | 参数非法 | 枚举筛选参数含非法值;message 列出非法值与合法枚举 |
## 前端需要做什么
- 请求参数生成逻辑不变(合法值、多选、逗号分隔均兼容)。
- 新增处理:收到 `code=100001` 时展示 `message`(如「参数非法: vehicleTypeKeys 仅支持 suv/mpv/bus/sedan,传入非法值xxx」,不再静默展示全量/空结果。典型场景:下拉数据版本与后端枚举不一致、拼写错误。
- 建议核对:筛选组件本地若有非法值兜底逻辑(如清空筛选重查全量),可保留但应以 100001 提示为准。
## 验证证据
- 单元测试:非法 `vehicleTypeKeys`/`typeKeys`(含合法值混传非法值)/`statuses`/`status` 均抛 100001 且不查库;合法多选suv + 中文别名)筛选行为回归不变;汇总接口非法 statuses 按设计忽略不报错。
- `mvn -pl hl-fleet-service -am verify` 通过(本次改动相关用例全绿)。
- 测试环境网关验证:`vehicleTypeKeys=BAD_TYPE` → 100001;`statuses=BAD_STATUS` → 100001;合法枚举`suv``suv,mpv`)筛选正常。
## 关联 / 联系人
### 链接
- **Issue**: [#5455](https://git.1814.love:8443/wx/HL/issues/5455)
- **PR**: [#5466](https://git.1814.love:8443/wx/HL/pulls/5466)
- **Merge commit**: [fa954d22c7](https://git.1814.love:8443/wx/HL/commit/fa954d22c7)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,130 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5456"
title: "派单创建 holdMode 双流 500 修复与失败重试幂等释放"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-08-04"
base: "dev-v3"
---
# 车务派单创建: holdMode=0/1 创建 500 修复 + 失败后同 requestId 可重试
> **存放目录**: `changelogs-v2/2026-08/`
> **服务**: hl-fleet-service
> **PR**: #5467
> **Issue**: #5456 / #5461 / #5451
> **日期**: 2026-08-04
> **影响范围**: 管理后台「派单弹窗」创建派单(直接派定 holdMode=0 / 排车锁定 holdMode=1
---
## ⚠️ 关键变化
前一版(#5444 Step2 机制合并后)`POST /admin/fleet/assignments` 对任意 unassigned 订单创建派单必返 500
- holdMode=0直接派定`code=500 服务器内部错误[IllegalStateException]: DAILY_V3 snapshot invalid: daily rows do not cover declared topology`
- holdMode=1排车锁定`code=500 服务器内部错误[IllegalStateException]: 派车组身份不完整或已失效,拒绝签发行程单 token`
本次修复后两者均正常返回 200;同时修复业务校验失败`605036 司机与车辆不是常驻组合`)后同 requestId 立即重试被 `100502 派单创建处理中,请勿重复提交` 卡死的问题——失败后幂等键释放,同 requestId 可立即重试。
## 变更接口
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 创建派单 | POST | `/admin/fleet/assignments` | 行为修复 | holdMode=0/1 创建不再 500;业务失败后同 requestId 可立即重试 |
接口请求/响应字段无任何变化,仅行为修复。
## 二、接口契约变化
### POST /admin/fleet/assignments创建派单
**使用场景**:车务在派单弹窗选定车辆/司机后创建派单。`holdMode=1` → 落 holding 并发 HOLD 通知;`holdMode=0` → 落 assigned 直接派定。
**入参**(无变化):`orderId / requirementId / vehicleId / driverId / startDate / endDate / holdMode(0|1) / headcount / requestId / confirmCrossResident / messageTemplateId / customBody / pickupAt / dropoffAt`
**出参**(无变化):成功返回 `assignmentStatus=holding|assigned` 的派单写结果。
**行为变化**
| 场景 | 原来 → 现在 |
|------|-------------|
| holdMode=0 对 unassigned 订单创建 | 500 内部错误DAILY_V3 快照拓扑校验失败,事务回滚)→ 200 创建成功返回 assigned |
| holdMode=1 对 Step2 建槽订单创建 | 500 内部错误HOLD 通知签发行程单短链时组行未冻结)→ 200 创建成功返回 holding,HOLD 通知正常生成 |
| 业务校验失败(如 605036 非跨常驻未确认)后同 requestId 重试 | 100502 派单创建处理中,请勿重复提交(幂等键残留至 TTL 300s→ 幂等键释放,同 requestId 可立即重试并得到与首次一致的结果 |
| HOLD 通知短信中的行程链接 | 失败无法发送 → 短信正常发送,行程链接占位文案「行程确认后发送」,司机最终确认assigned后行程短信携带真实链接 |
**错误码**(无变化):`605001/605003/605005/605006/605008/605013/605014/605036/605041/100502`
## 三、典型示例
### 成功holdMode=0 直接派定)
请求:
```http
POST /admin/fleet/assignments
Authorization: Bearer <token>
Content-Type: application/json
{
"orderId": "2084276690049007618",
"requirementId": "2084276690049007619",
"vehicleId": "2079857985848320002",
"driverId": "2065272150012444674",
"startDate": "2026-08-03",
"endDate": "2026-08-05",
"holdMode": 0,
"headcount": 2,
"confirmCrossResident": true,
"requestId": "qa-5456-direct-001"
}
```
响应200
```json
{
"code": 200,
"message": "成功",
"data": {
"assignmentStatus": "assigned",
"confirmedAt": "2026-08-04T12:00:00"
}
}
```
### 业务失败后同 requestId 重试
第一次请求(不传 confirmCrossResident,非跨常驻组合`605036 司机与车辆不是常驻组合,请确认跨常驻车派单后重试`;补 `confirmCrossResident: true` 后**同 requestId** 立即重试 → 正常进入业务处理(不再 100502
## 四、前端需要做什么
无需修改。前端如遇 100502 且确认首次请求已失败,可直接同 requestId 重试;HOLD 通知短信内行程链接在司机确认前为占位文案。
## 验证证据
- hl-common/hl-starter-protection verify 31 例全过;hl-fleet-service verify 3060 例2 个基线环境失败与本改动无关)
- 回归测试IdempotentAspect 失败释放/成功保留;HOLD 快照降级短链;DIRECT 快照事件发布前冻结断言
- TEST 部署与网关验证见工单 #5456/#5461/#5451
## 关联 / 联系人
### 链接
- **Issue**: [#5456](https://git.1814.love:8443/wx/HL/issues/5456) / [#5461](https://git.1814.love:8443/wx/HL/issues/5461) / [#5451](https://git.1814.love:8443/wx/HL/issues/5451)
- **PR**: [#5467](https://git.1814.love:8443/wx/HL/pulls/5467)
- **Merge commit**: [ad3f3684c602](https://git.1814.love:8443/wx/HL/commit/ad3f3684c602)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,145 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5459"
title: "用车需求提交/修改接口补定制师归属校验(越权修复)"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
updated_at: "2026-08-04"
base: "dev-v3"
---
# 订单: 用车需求提交/修改接口补定制师归属校验(#5459 越权修复)
> **服务**: hl-order-service-v3
> **PR**: #5471
> **Issue**: #5459
> **日期**: 2026-08-04
> **影响范围**: 管理后台用车需求提交/修改(定制师弹窗)
---
## ⚠️ 关键变化(行为修复,契约新增错误码)
`PUT /v3/admin/order/:id/vehicle-requirement`(提交/修改/调整用车需求)此前无角色/归属校验,车务角色可越权修改任意订单需求。现补操作人守卫,与同模块加急接口(`/vehicle-requirement/:requirementId/urgent`)校验口径一致:
- **超管SUPER_ADMIN**:放行。
- **其余角色**:必须是该订单归属定制师(订单 `consultant_id` == 当前登录 adminId,否则返回 **582094**,请求不落库。
## 变更接口
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 提交/修改/调整用车需求 | PUT | `/v3/admin/order/:id/vehicle-requirement` | 行为变更 | 增加操作人归属校验,非本单定制师/非超管返回 582094 |
## 二、接口详情
### 1. 提交/修改/调整用车需求 `PUT /v3/admin/order/:id/vehicle-requirement`
**使用场景**:定制师在订单详情提交/修改/调整用车需求(无 active=INIT_SUBMIT / PENDING=PENDING_EDIT / DONE=DONE_ADJUST 三分支自动判断)。
**请求头**`Authorization: Bearer <token>`(角色来自登录态,无新增字段)
**入参**(无变化):
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `fleet[]` | Body | Array | ✅ | 车型需求列表 |
| `fleet[].vehicleType` | Body | String | ✅ | 车型大类 key如 suv/mpv |
| `fleet[].seats` | Body | Integer | ✅ | 座位数 |
| `fleet[].count` | Body | Integer | ✅ | 辆数 |
| `specialTags[]` | Body | Array | - | 特殊标签 |
| `remark` | Body | String | - | 备注 |
**出参**`Result<VehicleRequirementRespVO>`branchTaken/version/... 结构无变化)
**错误码(新增)**
| 错误码 | 场景 |
|--------|------|
| `582094` | **新增**:操作人非该订单定制师且非超管(如车务角色调用) |
| `582083` | 需求状态不允许此操作 |
| `582022` | 车型座位数选项非法 |
**典型成功示例**(定制师本人):
```http
PUT /v3/admin/order/2084280429287817218/vehicle-requirement
Authorization: Bearer <wx token>
Content-Type: application/json
{
"fleet": [
{
"vehicleType": "suv",
"seats": 5,
"count": 1
}
],
"specialTags": [],
"remark": "定制师本人提交"
}
```
响应(成功):`code=200`,data 含 `branchTaken=INIT_SUBMIT``version=1`
**越权拒绝示例**(车务角色调用他人订单):
```http
PUT /v3/admin/order/2084466629751558146/vehicle-requirement
Authorization: Bearer <admin(VEHICLE_MANAGER) token>
Content-Type: application/json
{
"fleet": [
{
"vehicleType": "suv",
"seats": 5,
"count": 1
}
],
"specialTags": [],
"remark": "越权尝试"
}
```
响应(拒绝):`code=582094`「仅该订单定制师或超管可提交/修改用车需求」,data=null。
## 三、契约约束
| 场景 | 后端行为 |
|------|----------|
| 车务VEHICLE_MANAGER调用任意订单 | 582094 拒绝,不落库(修复前返回 200 并成功修改需求) |
| 定制师本人(订单归属 consultantId 匹配) | 正常提交/修改 |
| 超管SUPER_ADMIN | 放行,不校验归属 |
| 校验口径 | 与 `POST /v3/admin/order/vehicle-requirement/:requirementId/urgent`582084完全一致 |
## 前端注意事项
- 管理后台车务端不展示/不调用此接口,前端**无需改动**;若存在车务侧直达该接口的旧逻辑,请清理。
- 定制师弹窗调用该接口的既有逻辑不受影响。
## 验证证据
- 定向单测 3 例车务角色拒绝582094 不落库)/ 定制师本人正常 / 超管放行。
- 模块全量 `mvn -pl hl-order-service-v3 -am test`7414 tests 0 failures 0 errorsBUILD SUCCESS
- TEST 网关实测adminVEHICLE_MANAGER对 wx 订单调用返回 582094 且需求未被修改;wxCUSTOMIZER本人调用返回 200 成功INIT_SUBMIT v1。超管路径由单测覆盖TEST 环境无超管账号)。
## 关联 / 联系人
### 链接
- **Issue**: [#5459](https://git.1814.love:8443/wx/HL/issues/5459)
- **PR**: [#5471](https://git.1814.love:8443/wx/HL/pulls/5471)
- **Merge commit**: [e4f3579878d3](https://git.1814.love:8443/wx/HL/commit/e4f3579878d3)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,152 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5460"
title: "终止行程接口幂等重放契约恢复:同正文重放返回既有结果,不同正文返回 581049"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@49f38c7cfb4d5859a13c6ab1a7fea540cb524de5"
target_release: "v2.1"
verified_at: "2026-08-04"
status_note: "行为修复:恢复 #5379 已承诺的终止行程幂等重放语义(#5379 最终合并版丢失了早期迭代的幂等实现,导致首次终止成功后同正文重放被 581018 提前拦截、无法取回既有结果。PR #5468 已合并 dev-v3 并部署 TEST,网关已验证新订单首次终止 200、同正文重放返回既有 terminateRefundId/finalRefund、不同边界/正文返回 581049修复前同场景实测 581018。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 终止行程接口幂等重放契约恢复:同正文重放返回既有结果,不同正文返回 581049
> **服务**: hl-order-service-v3
> **PR**: #5468
> **Issue**: #5460
> **日期**: 2026-08-04
> **影响范围**: 管理后台订单终止行程(出行中终止)提交接口的重试/重放行为
---
## ⚠️ 关键变化
**#5379 契约承诺的幂等重放语义此前在 TEST 实测失效**:首次终止提交成功但响应丢失后,对同一订单原样重放同一请求,接口返回 `581018「出行中取消仅适用于出行中订单」`,而不是返回既有终止结果;用不同正文重试也返回 581018 而不是 `581049`。**现已修复**,行为与 #5379 changelog 契约一致:
| 场景 | 修复前实测 | 修复后 |
|------|-----------|--------|
| 相同正文重放(首次已成功) | 581018错误 | `200`,返回既有终止结果(含 terminateRefundId / finalRefund |
| 不同正文 / 不同终止边界重试 | 581018错误 | `581049` 终止行程重试与首次终止边界或请求不一致 |
前端无需改动,但**超时/网络重试逻辑现在可以依赖该接口的幂等语义**:重试时原样复用首次请求正文即可取回既有结果,不要修改正文内容(包括 `vehicles[].used``lineUsages[].used`)。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 终止行程 | POST | `/v3/admin/order/:id/terminate` | 行为修复 | 幂等重放语义恢复(契约不变,见 #5379 |
## 三、接口详情
### 1. 终止行程 `POST /v3/admin/order/:id/terminate`
**请求体**: `OrderTerminateTripReqVO`(字段、必填性、约束均不变,与 #5379 一致)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `endDayNumber` | Integer | ✅ | 停在第几天1-based;**重试必须与首次完全一致** |
| `cancelReason` | String | ✅ | 终止原因;重试必须与首次完全一致 |
| `adjustAmount` | BigDecimal | 否 | 人工调整额;重试必须与首次一致0 与 null 视为等价) |
| `lineUsages[]` | Array | 否 | 统一资源行已用判定;重试必须与首次一致 |
| `rooms[]` / `tickets[]` / `services[]` / `supplies[]` | Array | 否 | 兼容分组入参;重试必须与首次一致 |
| `vehicles[]` | Array | 否 | 用车行refId/dayNumber/used;used 仍参与重放一致性校验,金额以后端 Fleet 事实为准 |
**业务边界(重放判定规则)**
1. 订单为 TRAVELLING 时:正常执行首次终止 → `200`,订单变 COMPLETED。
2. 订单已 COMPLETED已终止时,**先判定重放身份**,不再直接返回 581018
- 同边界 + 同正文(请求指纹一致)→ `200`,返回既有终止结果:`terminateRefundId``baselineRefund``adjustAmount``finalRefund``settlementRefundId``newStatus=COMPLETED``newFlowStatus=PENDING_REVIEW`;不产生任何副作用(不重复写退款、不重复发事件)。
- 同边界但正文不一致,或边界不一致(`endDayNumber` 等不同)→ `581049 终止行程重试与首次终止边界或请求不一致`
- 升级前已终止的订单(无指纹快照):按主表与明细快照做语义等价校验,等价重放同样返回既有结果,不等价返回 581049。
3. `endDayNumber` 超出订单行程 → `581047`(不变);车辆费用不可用 → `584100` fail closed不变
**错误码**
| 错误码 | 含义 | 触发条件 |
|--------|------|----------|
| 581018 | 出行中取消仅适用于出行中订单 | 订单非 TRAVELLING 且**从未终止**(自然结束/已取消等)时提交终止 |
| 581049 | 终止行程重试与首次终止边界或请求不一致 | 已终止订单以不同边界/不同正文重试 |
| 581047 | 终止行程:结束日不在订单行程范围内 | endDayNumber 越界 |
| 581044 | 终止行程:结束日之前的资源必须标记为已使用 | 结束日前资源 used=false |
**典型成功示例(同正文重放)**
请求:
```json
POST /v3/admin/order/2084280231786430466/terminate
Authorization: Bearer <token>
Content-Type: application/json
{
"cancelReason": "客户高反送医终止",
"endDayNumber": 2,
"adjustAmount": 0,
"lineUsages": [
{ "lineKey": "HOTEL_ASSIGNMENT:96011:1", "used": true },
{ "lineKey": "ITINERARY_NODE:97010:1", "used": true }
],
"vehicles": [
{ "refId": 9000004500000001, "dayNumber": 1, "used": true }
]
}
```
响应(首次终止与同正文重放均为此响应,重放时 terminateRefundId/finalRefund 与首次一致):
```json
{
"code": 200,
"data": {
"terminateRefundId": "2084497785524011009",
"baselineRefund": "6500.00",
"adjustAmount": "0.00",
"finalRefund": "6500.00",
"settlementRefundId": "2084497785561759746",
"newStatus": "COMPLETED",
"newFlowStatus": "PENDING_REVIEW"
},
"success": true
}
```
**异常示例(不同正文重试)**
```json
{
"code": 581049,
"message": "终止行程重试与首次终止边界或请求不一致",
"success": false
}
```
## 四、验证证据
- `mvn -pl hl-order-service-v3 -am verify` 全绿(含 spotless + 全部单元/集成测试;新增幂等重放回归:首次终止写指纹 → 同正文重放 → 不同正文冲突三态)。
- TEST 网关验证wx 定制师,2026-08-04新订单首次终止 200 且写指纹;同正文重放返回同一 terminateRefundId/finalRefund;不同 endDayNumber 返回 581049;升级前已终止订单同语义重放返回既有 terminateRefundId/finalRefund、不同边界返回 581049修复前同场景实测 581018
- 已部署 TESTdev-v3 滚动部署,双实例健康)。
## 五、相关文档
- 关联 Issue: [wx/HL#5460](https://git.1814.love:8443/wx/HL/issues/5460)
- 关联 PR: [wx/HL#5468](https://git.1814.love:8443/wx/HL/pulls/5468)
- 契约基线: [03_5379_终止行程车辆退款权威口径-修改接口-管理后台.md](./03_5379_终止行程车辆退款权威口径-修改接口-管理后台.md)
## 关联 / 联系人
### 链接
- **Issue**: [#5460](https://git.1814.love:8443/wx/HL/issues/5460)
- **PR**: [#5468](https://git.1814.love:8443/wx/HL/pulls/5468)
- **Merge commit**: [316a7d2c29](https://git.1814.love:8443/wx/HL/commit/316a7d2c29)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,125 +0,0 @@
---
author: "wx(GIT)"
schema: "hl-changelog/v2"
ticket: "5463"
title: "派单司机/员工手机号密文解密失败降级容错"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "Pi"
frontend_ref: "hl-admin@4c8bdb97c89fa399e5fbb13240ea082f64fefc59"
target_release: "v2.1"
verified_at: "2026-08-04"
status_note: "订单详情/行程接口对手机号密文解密失败keyVersion 缺失/密文损坏)不再 500,降级返回空手机号;后端 warn 日志含 keyVersion 与密文指纹,不含明文。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 【修改接口·管理后台】派单司机/员工手机号密文解密失败降级容错 (#5463)
> **PR**: #5464 | **服务**: order-v3 | **更新时间**: 2026-08-04
## 1. 接口背景
订单详情/行程接口对含派单司机/员工手机号密文的记录,在密钥版本缺失或密文损坏时返回 500`assignment phone key version is unavailable`),用户端订单详情页直接报错不可用。本次改为解密失败降级:仅手机号字段返回空,订单其余字段正常展示。
## 变更接口清单
| # | 接口名 | 方法 | 路径 | 变更类型 | 说明 |
|---|--------|------|------|----------|------|
| 1 | 订单行程v2 详情页) | GET | `/v3/admin/order/{id}/itinerary` | 修改 | 手机号解密失败不再 500,降级为空展示 |
| 2 | 订单详情9 Tab 聚合) | GET | `/v3/admin/order/{id}` | 修改 | 车辆/人员手机号解密失败不再 500,降级为空展示 |
## 2. 行为变化
| 场景 | 原来 | 现在 |
|------|------|------|
| 派单/员工手机号密文 keyVersion 缺失(如配置未下发) | 整接口 500 | 200,手机号字段为 `null`/空,其余字段正常 |
| 密文损坏 / Base64 非法 / AAD 不匹配 | 整接口 500 | 200,手机号字段为 `null`/空,其余字段正常 |
| 手机号无密文(明文快照或空) | 正常展示 | 不变 |
| 密钥配置齐全且密文正常 | 正常解密展示 | 不变 |
**涉及字段**(响应中车辆/人员相关节点):
| 字段 | 类型 | 变化 |
|------|------|------|
| 车辆行 `driverPhone`(订单行程/详情车辆列表) | string | 解密失败时为 `null`(原来导致整接口 500 |
| 签收凭证 `driverPhone` / `guidePhone` / `leaderPhone` | string | 解密失败时为 `null`(原来导致整接口 500 |
## 3. 接口详情
### 3.1 订单行程
- **方法/路径**`GET /v3/admin/order/{id}/itinerary`
- **认证**:管理后台登录态(定制师/车务均可)
- **无请求体**`id` 为订单雪花 ID。
- **典型成功示例**(手机号解密失败时,其余字段完整返回):
```
GET /v3/admin/order/2084280253366124546/itinerary
Authorization: Bearer <token>
(无请求体)
```
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "2084280253366124546",
"orderNo": "HL20260803220000001",
"vehicles": [
{
"assignmentId": "2084280300000000001",
"driverName": "张师傅",
"driverPhone": null,
"licensePlate": "蒙A-12345",
"serviceStartDate": "2026-08-03",
"serviceEndDate": "2026-08-05"
}
]
}
}
```
### 3.2 订单详情
- **方法/路径**`GET /v3/admin/order/{id}`
- **行为**:与 3.1 一致;聚合内所有来源自 `driver_phone_ciphertext` / `staff_phone_ciphertext` 的手机号字段在解密失败时均为 `null`,不再触发整接口 500。
## 4. 错误码
无新增错误码。解密失败不再返回 500,改为 200 + 字段 `null`;后端以 warn 日志记录(`assignment phone decrypt degraded: ... keyVersion=... envelopeFingerprint=...`),日志不含明文与密钥。
## 5. 前端需要做什么
- 无需代码改动即可恢复可用(页面从 500 变为正常打开)。
- 请确认车辆/人员手机号为空(`null`)时页面展示兜底(如显示 `-` 或留空),避免出现 `undefined` 文案。
- 前端遇到 `driverPhone` 为空时,不要向用户提示“数据异常”,按“暂无/未填写”处理即可。
## 6. 后端测试、部署与网关验证
- 单元测试:`AssignmentPhoneCryptoTest` 10 个(正常解密 / keyVersion 缺失 / 损坏密文 / Base64 非法 / AAD 不匹配 / AAD 身份缺失 / 加密 fail-fast / previous-keys 轮换)全部通过。
- 模块验证:`mvn -pl hl-order-service-v3 -am verify` 7414 测试 0 失败。
- 部署TEST 环境 hl-order-service-v3 已部署。
- 网关验证:原 500 订单(`2084280253366124546` / `2084280407016062978``GET /v3/admin/order/{id}/itinerary` 恢复 200,手机号字段降级为空,其余字段正常。
## 验证证据
- 单元测试:`AssignmentPhoneCryptoTest` 10 个用例(正常 v1/v2 往返、keyVersion 缺失降级 null、损坏密文降级 null、Base64 非法降级 null、AAD 不匹配降级 null、AAD 身份缺失降级 null、加密 fail-fast、previous-keys 轮换)全通过。
- 模块验证:`mvn -pl hl-order-service-v3 -am verify` 7414 测试 0 失败。
- 部署TEST 环境 hl-order-service-v3 rolling 部署完成dev-v3 @ fcde24ec,双实例 UP
- 网关验证wx=CUSTOMIZER原 500 订单 `2084280253366124546` / `2084280407016062978` / `2084278362947158017``GET /v3/admin/order/{id}/itinerary` 全部恢复 200;含 v1 密文的订单 `2084266179660013569` 车辆行 `driverPhoneMasked` 正常掩码回显(测试服已补配 v1 密钥,存量 414 条密文可解密;无密钥时走降级路径返回空,由单元测试覆盖)。
## 关联 / 联系人
### 链接
- **Issue**: [#5463](https://git.1814.love:8443/wx/HL/issues/5463)
- **PR**: [#5464](https://git.1814.love:8443/wx/HL/pulls/5464)
- **Merge commit**: [fcde24ec63](https://git.1814.love:8443/wx/HL/commit/fcde24ec63)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,119 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5472"
title: "看板列表枚举筛选纯分隔符值校验补全status 参数)"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端完成PR #5473 已合并 dev-v3 并部署 TEST,网关验证 17/17;#5455 的 100001 错误处理提示已覆盖本单场景,前端无需新改动。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 车务: 看板列表枚举筛选纯分隔符值校验补全status 参数)
> **服务**: hl-fleet-service
> **PR**: #5473
> **Issue**: #5472
> **日期**: 2026-08-04
> **影响范围**: 管理后台车务端看板列表的状态筛选参数(#5455 校验契约的补全)
---
## ⚠️ 关键变化
#5455 后,`GET /admin/fleet/board/orders` 仍有一个校验绕过向量:**单值参数 `status`** 传入非空白但纯分隔符组成的值(如 `status=,``status=,,`)时,无任何合法状态 token,此前被静默放行返回全量数据;现在与非法状态值同为拒绝口径,返回 `100001 参数非法`
- 以前:`status=,``code=200` + 全量数据(筛选静默失效)。
- 现在:`status=,``code=100001`,message 指明非法值与合法枚举。
**数组参数无需变化**(行为如实说明):`statuses`/`vehicleTypeKeys`/`typeKeys` 为数组参数,Spring 绑定层会先按逗号拆分,纯分隔符值绑定为空数组,与空串等价,按「空=不过滤」返回 200 全量——这是设计口径,与 `statuses=`(空串)行为一致,不属于校验绕过。空值/纯空白(如 `vehicleTypeKeys= `)同样按「空=不过滤」。
## 变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 看板列表 | GET | `/admin/fleet/board/orders` | 校验补全 | 单值参数 `status` 纯分隔符值返 100001 |
## 接口详情
### 1. 看板列表 `GET /admin/fleet/board/orders`
**`status` 参数**(单值,与 statuses 合并):
| 参数 | 类型 | 合法值 | 说明 |
|------|------|--------|------|
| `status` | String | `unassigned`/`unassigned_urgent`/`holding`/`holding_urgent`/`assigned`/`change_requested`/`canceled`/`completed`(支持中英文别名、大小写、逗号分隔多值) | 状态筛选别名;非空白纯分隔符值返 100001 |
**异常示例**(纯分隔符值):
```text
GET /admin/fleet/board/orders?page=1&pageSize=20&status=,
Authorization: Bearer <token>
(无请求体)
```
```json
{
"code": 100001,
"message": "参数非法: statuses 含非法状态值:,合法值unassigned/unassigned_urgent/holding/holding_urgent/assigned/change_requested/canceled/completed",
"data": null,
"traceId": null,
"success": false
}
```
**典型成功示例**(合法单值):
```text
GET /admin/fleet/board/orders?page=1&pageSize=20&status=unassigned
Authorization: Bearer <token>
```
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
},
"success": true
}
```
## 错误码
| code | 含义 | 说明 |
|------|------|------|
| 100001 | 参数非法 | `status` 为非法值/非空白纯分隔符值(与 #5455 同为拒绝口径) |
## 前端需要做什么
- 无需改动:正常请求不产生纯分隔符值;`100001` 的错误提示处理已在 #5455 changelog 中说明(展示 message 即可)。
## 验证证据
- 单元测试BoardOrderServiceTest 79/79新增 4 例status/statuses/vehicleTypeKeys/typeKeys 纯分隔符拒绝且不查库;空值/纯空白/尾逗号合法回归)。
- 全量基线494f78e7b 上 hl-fleet-service 3064 例(仅 Docker 环境型 1 error,与改动无关
- 测试环境网关验证 17/17`status=,`/`status=,,` → 100001;数组参数纯分隔符 → 200空=不过滤,设计口径);空值/纯空白 → 200;合法值suv、suv,mpv、尾逗号、unassigned,holding→ 200;BAD_TYPE/BAD_STATUS → 100001。
## 关联 / 联系人
### 链接
- **Issue**: [#5472](https://git.1814.love:8443/wx/HL/issues/5472)
- **PR**: [#5473](https://git.1814.love:8443/wx/HL/pulls/5473)
- **Merge commit**: [49565f8a41](https://git.1814.love:8443/wx/HL/commit/49565f8a41)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,602 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5477"
title: "核单其他收入移除项目类别与票种规格字段"
consumer: "admin"
change_type: "修改接口"
author: "yaosutu(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@9f46fa040a77cd899bbc9c21b9b8304a518499e2"
target_release: ""
verified_at: "2026-08-04"
status_note: "PR #5488 已合并 dev-v3;测试服真实网关已验证 POST 无旧字段成功、PUT 多传旧字段被忽略、GET/POST/PUT 响应均不含 projectCategory/projectCategoryName/specification。前端需删除项目类别与票种规格控件及相关字段读写。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 【⚠️ 修改接口·管理后台】核单其他收入移除项目类别与票种规格字段(#5477
> **PR**: [#5488](https://git.1814.love:8443/wx/HL/pulls/5488) | **服务**: hl-order-service-v3 | **更新时间**: 2026-08-04 16:30
## 1. 接口背景
“其他收入”页签本身已表达项目类型,继续要求填写“项目类别”属于重复信息;“票种/规格”也不适用于其他收入。此次统一收口新增、修改和查询契约,前端应删除这两个控件及相关字段读写。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 前端动作 |
|---|---|---|---|---|---|
| 1 | 查询其他收入核单明细及增减费汇总 | GET | `/v3/admin/order/{orderId}/settlement/other-incomes` | 删除出参字段 | 停止读取 `projectCategory``projectCategoryName``specification` |
| 2 | 新增其他收入并原子创建订单增费 | POST | `/v3/admin/order/{orderId}/settlement/other-incomes` | 删除入参、出参字段 | 删除项目类别与票种规格控件;停止传两个旧入参 |
| 3 | 修改其他收入 | PUT | `/v3/admin/order/{orderId}/settlement/other-incomes/{incomeId}` | 删除入参、出参字段 | 删除项目类别与票种规格控件;停止传两个旧入参 |
> DELETE 接口签名与返回结构未变化,不属于本次契约变更。
## 3. 接口详情
### 3.1 查询其他收入核单明细及增减费汇总
- **方法/路径**`GET /v3/admin/order/{orderId}/settlement/other-incomes`
- **使用场景**:进入其他收入页签,以及新增、修改、删除后刷新明细和汇总。
- **认证**:需要管理后台登录态;配房角色不可访问。
- **幂等性**:只读接口,可安全重复调用。
- **限流**:未声明接口专属限流。
**路径参数**
| 字段 | 类型 | 必填 | 说明 | 校验 |
|---|---|---:|---|---|
| `orderId` | String(Long) | 是 | 订单 ID | 必须大于 0 |
**请求体**:无。
**成功响应 `data`**
| 字段 | 类型 | 可空 | 说明 |
|---|---|---:|---|
| `items` | `OtherIncomeItem[]` | 否 | 其他收入明细;无数据返回 `[]` |
| `deductions` | `Deduction[]` | 否 | 只读减费明细;无数据返回 `[]` |
| `summary` | `Summary` | 否 | 当前有效增减费汇总 |
`OtherIncomeItem` 完整字段:
| 字段 | 类型 | 可空 | 说明 |
|---|---|---:|---|
| `id` | String(Long) | 否 | 其他收入 ID |
| `requestId` | String | 是 | 手工新增幂等请求 ID;自动投影时为空 |
| `incomeDate` | LocalDate | 否 | 收入日期,`YYYY-MM-DD` |
| `projectName` | String | 否 | 项目名称 |
| `quantity` | Decimal | 否 | 数量 |
| `unitPrice` | Decimal | 否 | 核算单价 |
| `settlementAmount` | Decimal | 否 | 核算金额 |
| `paymentMethod` | String | 否 | 付款类型,见 §6.1 |
| `paymentMethodName` | String | 否 | 付款类型中文名 |
| `voucherUrls` | String[] | 是 | 凭证 URL 列表 |
| `settlementConfirmStatus` | String | 否 | 确认状态,见 §6.2 |
| `settlementConfirmStatusName` | String | 否 | 确认状态中文名 |
| `remark` | String | 是 | 备注 |
| `sourceType` | String | 否 | 来源类型,见 §6.3 |
| `sourceTypeName` | String | 否 | 来源类型中文名 |
| `sourceId` | String(Long) | 是 | 来源附加费 ID |
`Deduction` 完整字段:
| 字段 | 类型 | 可空 | 说明 |
|---|---|---:|---|
| `id` | String(Long) | 否 | 减费 ID |
| `discountName` | String | 否 | 减费名称 |
| `discountAmount` | Decimal | 否 | 减费金额 |
| `sourceType` | String | 是 | 来源类型 |
| `sourceId` | String(Long) | 是 | 来源业务 ID |
| `createdAt` | LocalDateTime | 是 | 创建时间 |
`Summary` 完整字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `surchargeAmount` | Decimal | 有效增费合计 |
| `discountAmount` | Decimal | 有效减费合计 |
| `netAdjustmentAmount` | Decimal | 净调整额,即增费减去减费 |
**本接口错误码与业务边界**:见 §7、§9;查询不允许再依赖三个已删除字段。
**典型成功示例**
```http
GET /v3/admin/order/2084000000000002978/settlement/other-incomes
Authorization: Bearer <token>
```
```json
{
"code": 200,
"message": "success",
"data": {
"items": [{
"id": "2084000000000004978",
"requestId": "oi-5477-0001",
"incomeDate": "2026-08-04",
"projectName": "酒店升级补差",
"quantity": 2,
"unitPrice": 12.34,
"settlementAmount": 24.68,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": [],
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": null,
"sourceType": "MANUAL",
"sourceTypeName": "手工",
"sourceId": "2084000000000004979"
}],
"deductions": [],
"summary": {
"surchargeAmount": 24.68,
"discountAmount": 0,
"netAdjustmentAmount": 24.68
}
},
"success": true
}
```
**边界示例(空列表)**
```http
GET /v3/admin/order/2084000000000002978/settlement/other-incomes
Authorization: Bearer <token>
```
```json
{
"code": 200,
"message": "success",
"data": {
"items": [],
"deductions": [],
"summary": {"surchargeAmount": 0, "discountAmount": 0, "netAdjustmentAmount": 0}
},
"success": true
}
```
**异常示例(非法订单 ID**
```http
GET /v3/admin/order/0/settlement/other-incomes
Authorization: Bearer <token>
```
```json
{"code": 400, "message": "订单 ID 必须大于 0", "data": null, "success": false}
```
### 3.2 新增其他收入并原子创建订单增费
- **方法/路径**`POST /v3/admin/order/{orderId}/settlement/other-incomes`
- **使用场景**:核单人员新增一条手工其他收入。
- **认证**:需要管理后台登录态;仅超级管理员、管理员或财务可写。
- **幂等性**`requestId` 是同一订单内永久唯一的稳定幂等键;同一 `requestId` 与相同载荷重试返回同一结果,不同载荷冲突返回 `584087`
- **限流**:未声明接口专属限流。
**路径参数**`orderId`,String(Long),必填且必须大于 0。
**请求体完整字段**
| 字段 | 类型 | 必填 | 校验与说明 |
|---|---|---:|---|
| `requestId` | String | 是 | 最长 64;同订单内永久唯一 |
| `incomeDate` | LocalDate | 是 | `YYYY-MM-DD` |
| `projectName` | String | 是 | 非空,最长 100 |
| `quantity` | Decimal | 是 | ≥0,最多 8 位整数、4 位小数 |
| `unitPrice` | Decimal | 是 | ≥0,最多 8 位整数、2 位小数 |
| `settlementAmount` | Decimal | 是 | ≥0.01,最多 8 位整数、2 位小数;必须等于 `quantity × unitPrice` 四舍五入到 2 位 |
| `paymentMethod` | String | 是 | 见 §6.1 |
| `settlementConfirmStatus` | String | 是 | 新增只能为 `UNCONFIRMED` |
| `voucherUrls` | String[] | 否 | 最多 9 项;每项最长 1024,必须为 `http/https` URL |
| `remark` | String | 否 | 最长 500 |
**成功响应**`data` 为 §3.1 的完整 `OtherIncomeItem`;不含 `projectCategory``projectCategoryName``specification`
**典型成功请求与响应**
```http
POST /v3/admin/order/2084000000000002978/settlement/other-incomes
Authorization: Bearer <token>
Content-Type: application/json
```
```json
{
"requestId": "oi-5477-0001",
"incomeDate": "2026-08-04",
"projectName": "酒店升级补差",
"quantity": 2,
"unitPrice": 12.34,
"settlementAmount": 24.68,
"paymentMethod": "COMPANY_PAID",
"settlementConfirmStatus": "UNCONFIRMED",
"voucherUrls": [],
"remark": null
}
```
```json
{
"code": 200,
"message": "success",
"data": {
"id": "2084000000000004978",
"requestId": "oi-5477-0001",
"incomeDate": "2026-08-04",
"projectName": "酒店升级补差",
"quantity": 2,
"unitPrice": 12.34,
"settlementAmount": 24.68,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": [],
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": null,
"sourceType": "MANUAL",
"sourceTypeName": "手工",
"sourceId": "2084000000000004979"
},
"success": true
}
```
**边界请求与响应(旧字段仍被旧客户端多传)**
```json
{
"requestId": "oi-5477-legacy-0001",
"incomeDate": "2026-08-04",
"projectName": "历史客户端补差",
"projectCategory": "HOTEL",
"specification": "VIP",
"quantity": 1,
"unitPrice": 0.01,
"settlementAmount": 0.01,
"paymentMethod": "CASH_PAID",
"settlementConfirmStatus": "UNCONFIRMED",
"voucherUrls": []
}
```
```json
{
"code": 200,
"message": "success",
"data": {
"id": "2084000000000004980",
"requestId": "oi-5477-legacy-0001",
"incomeDate": "2026-08-04",
"projectName": "历史客户端补差",
"quantity": 1,
"unitPrice": 0.01,
"settlementAmount": 0.01,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"voucherUrls": [],
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": null,
"sourceType": "MANUAL",
"sourceTypeName": "手工",
"sourceId": "2084000000000004981"
},
"success": true
}
```
旧字段会被忽略,响应不会回显。该兼容仅用于过渡;前端仍必须停止发送。
**异常请求与响应(缺少确认状态)**
```json
{
"requestId": "oi-5477-invalid-0001",
"incomeDate": "2026-08-04",
"projectName": "无确认状态",
"quantity": 1,
"unitPrice": 10,
"settlementAmount": 10,
"paymentMethod": "CASH_PAID"
}
```
```json
{"code": 400, "message": "确认状态不能为空", "data": null, "success": false}
```
### 3.3 修改其他收入
- **方法/路径**`PUT /v3/admin/order/{orderId}/settlement/other-incomes/{incomeId}`
- **使用场景**:修改已有其他收入的公开业务字段。
- **认证**:需要管理后台登录态;仅超级管理员、管理员或财务可写。
- **幂等性**:无单独幂等键;重复提交相同最终载荷不会改变公开结果。调用方不得依赖并发请求顺序。
- **限流**:未声明接口专属限流。
**路径参数**
| 字段 | 类型 | 必填 | 说明 | 校验 |
|---|---|---:|---|---|
| `orderId` | String(Long) | 是 | 订单 ID | 必须大于 0 |
| `incomeId` | String(Long) | 是 | 其他收入 ID | 必须大于 0,且必须属于该订单 |
**请求体完整字段**:与 POST 相同,但没有 `requestId``settlementConfirmStatus` 可为 `UNCONFIRMED``CONFIRMED`。字段长度、金额一致性、凭证约束均与 §3.2 相同。
**成功响应**`data` 为 §3.1 的完整 `OtherIncomeItem`;不含 `projectCategory``projectCategoryName``specification`
**典型成功请求与响应**
```http
PUT /v3/admin/order/2084000000000002978/settlement/other-incomes/2084000000000004978
Authorization: Bearer <token>
Content-Type: application/json
```
```json
{
"incomeDate": "2026-08-04",
"projectName": "酒店升级补差-已更新",
"quantity": 3,
"unitPrice": 15.00,
"settlementAmount": 45.00,
"paymentMethod": "CASH_PAID",
"settlementConfirmStatus": "UNCONFIRMED",
"voucherUrls": [],
"remark": "金额已核对"
}
```
```json
{
"code": 200,
"message": "success",
"data": {
"id": "2084000000000004978",
"requestId": "oi-5477-0001",
"incomeDate": "2026-08-04",
"projectName": "酒店升级补差-已更新",
"quantity": 3,
"unitPrice": 15,
"settlementAmount": 45,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"voucherUrls": [],
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": "金额已核对",
"sourceType": "MANUAL",
"sourceTypeName": "手工",
"sourceId": "2084000000000004982"
},
"success": true
}
```
**边界请求与响应(夹带旧字段)**:请求可额外包含 `projectCategory``specification`,服务端会忽略,仍按上述公开字段更新,响应不会出现三个旧字段。
```json
{
"incomeDate": "2026-08-04",
"projectName": "酒店升级补差-兼容请求",
"projectCategory": "OLD_JSON_CATEGORY_SHOULD_BE_IGNORED",
"specification": "OLD_JSON_SPEC_SHOULD_BE_IGNORED",
"quantity": 3,
"unitPrice": 15,
"settlementAmount": 45,
"paymentMethod": "CASH_PAID",
"settlementConfirmStatus": "UNCONFIRMED",
"voucherUrls": []
}
```
```json
{
"code": 200,
"message": "success",
"data": {
"id": "2084000000000004978",
"requestId": "oi-5477-0001",
"incomeDate": "2026-08-04",
"projectName": "酒店升级补差-兼容请求",
"quantity": 3,
"unitPrice": 15,
"settlementAmount": 45,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"voucherUrls": [],
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": null,
"sourceType": "MANUAL",
"sourceTypeName": "手工",
"sourceId": "2084000000000004982"
},
"success": true
}
```
**异常请求与响应(金额不一致)**
```json
{
"incomeDate": "2026-08-04",
"projectName": "金额错误",
"quantity": 3,
"unitPrice": 15,
"settlementAmount": 44,
"paymentMethod": "CASH_PAID",
"settlementConfirmStatus": "UNCONFIRMED"
}
```
```json
{"code": 584076, "message": "其他收入核算金额必须等于数量乘以核算单价", "data": null, "success": false}
```
## 4. 接口入参汇总
| 接口 | 路径参数 | 请求体差异 | 已删除字段 |
|---|---|---|---|
| GET | `orderId` | 无 | 无入参;响应删除三个字段 |
| POST | `orderId` | 比 PUT 多必填 `requestId`;新增必须从 `UNCONFIRMED` 开始 | `projectCategory``specification` |
| PUT | `orderId``incomeId` | 无 `requestId`;确认状态可为两种合法值 | `projectCategory``specification` |
完整字段、类型和校验均已在 §3 对应接口内列出。
## 5. 出参字段汇总
POST、PUT 返回单个 `OtherIncomeItem`;GET 返回 `items[] + deductions[] + summary``OtherIncomeItem` 的完整当前字段见 §3.1,三种接口均不再返回:
| 已删除字段 | 原类型 | 当前替代 |
|---|---|---|
| `projectCategory` | String | 无;前端删除对应状态与控件 |
| `projectCategoryName` | String | 无;前端删除对应展示读取 |
| `specification` | String | 无;前端删除对应状态与控件 |
## 6. 枚举 / 数据字典
### 6.1 `paymentMethod`(付款类型)
**所属字段**POST/PUT 入参、`OtherIncomeItem.paymentMethod` | **类型**String | **必填**:是
| 值 | 中文 | 说明 |
|---|---|---|
| `CASH_PAID` | 现付 | 已现场支付 |
| `COMPANY_PAID` | 公司付款 | 由公司付款 |
| `SIGNED` | 签单 | 签单结算 |
### 6.2 `settlementConfirmStatus`(确认状态)
**所属字段**POST/PUT 入参、`OtherIncomeItem.settlementConfirmStatus` | **类型**String | **必填**:是
| 值 | 中文 | 说明 |
|---|---|---|
| `UNCONFIRMED` | 未确认 | POST 新增时唯一允许的初始值 |
| `CONFIRMED` | 已确认 | 仅对已有明细通过 PUT 更新使用 |
### 6.3 `sourceType`(公开来源类型)
**所属字段**`OtherIncomeItem.sourceType` | **类型**String | **必填**:响应必有
| 值 | 中文 | 说明 |
|---|---|---|
| `MANUAL` | 手工 | 管理后台手工新增 |
| `SYSTEM` | 系统 | 系统投影来源 |
`projectCategory` 对应的数据字典不再属于其他收入接口契约;前端应删除该字典请求和映射逻辑。
## 7. 错误码
| code | 含义 | 触发场景 |
|---:|---|---|
| `400` | 参数校验失败 | 缺必填字段、长度/格式超限、非法枚举、订单 ID 非正数等 |
| `401` | 未登录或登录态失效 | 缺少有效管理后台凭证 |
| `584073` | 其他收入不存在或不属于当前订单 | PUT 的 `incomeId` 不存在或订单归属不符 |
| `584074` | 当前核单状态不允许修改其他收入 | 订单核单状态不是待核单或核单中 |
| `584075` | 其他收入关联的附加费来源无效 | 关联来源无法建立或已失效 |
| `584076` | 核算金额不等于数量乘以单价 | `quantity × unitPrice` 四舍五入到 2 位后与金额不一致 |
| `584086` | 无权修改核单资金数据 | 写接口调用角色不是超级管理员、管理员或财务 |
| `584087` | `requestId` 已用于另一笔其他收入 | POST 重用幂等键但载荷不同 |
| `584088` | 核单凭证数据损坏 | 历史凭证数据无法读取 |
| `584089` | 核单或结算已完成,资金数据不可修改 | 对完成后的资金事实调用 POST/PUT |
| `584106` | 确认状态非法 | 非 `UNCONFIRMED/CONFIRMED` |
| `584107` | 手工新增必须先保存为未确认 | POST 直接传 `CONFIRMED` |
旧错误码 `584103`(项目类别非法)、`584104`(规格非法)、`584105`(相关字典不可用)不再由这三个接口触发。
## 8. 示例索引
三类示例均已与接口放在一起,避免跨节拼接:
| 接口 | 典型成功 | 边界 | 业务失败 |
|---|---|---|---|
| GET | §3.1 有明细 | §3.1 空列表 | §3.1 非法 `orderId` |
| POST | §3.2 不传旧字段创建 | §3.2 旧请求多传字段被忽略 | §3.2 缺确认状态 |
| PUT | §3.3 正常更新 | §3.3 夹带旧字段被忽略 | §3.3 金额不一致 |
## 9. 业务边界
- ✅ GET 用于读取当前其他收入、减费与汇总;空数据稳定返回空数组。
- ✅ POST/PUT 仅适用于核单资金仍可修改的订单,且调用角色必须具备资金写权限。
- ✅ POST 必须携带稳定 `requestId`,并以 `UNCONFIRMED` 创建;后续可通过 PUT 改为 `CONFIRMED`
- ✅ `settlementAmount` 必须等于 `quantity × unitPrice` 四舍五入到 2 位。
- ⚠️ 旧客户端继续多传 `projectCategory/specification` 时,新接口会忽略;这不是继续保留控件的理由。
- ❌ 核单或结算完成后禁止 POST/PUT;不存在或跨订单的 `incomeId` 禁止更新。
- ❌ 前端不得从其他字段猜测、拼装或恢复已删除的项目类别与票种规格。
## 10. 修改前后对比
### 10.1 字段级对比
| 接口字段 | 原来 | 现在 |
|---|---|---|
| POST/PUT `projectCategory` | 必填 String,受项目类别字典校验 | 已从契约删除;旧 JSON 多传会被忽略 |
| POST/PUT `specification` | 可选 String,受规格字典校验 | 已从契约删除;旧 JSON 多传会被忽略 |
| GET/POST/PUT `projectCategory` | 响应返回 | 不再返回 |
| GET/POST/PUT `projectCategoryName` | 响应返回中文名 | 不再返回 |
| GET/POST/PUT `specification` | 响应返回 | 不再返回 |
### 10.2 行为级对比
| 行为 | 原来 | 现在 |
|---|---|---|
| 新增/修改表单 | 必须维护项目类别,可选维护票种规格 | 两个控件都删除,只提交当前公开字段 |
| 旧客户端多传旧字段 | 参与校验和保存 | 被忽略,且响应不回显 |
| 查询展示 | 可读取类别、类别名称和规格 | 三个字段不存在,禁止继续读取或设置默认值 |
| 相关字典异常 | 可能阻断写入 | 不再属于其他收入接口错误面 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:响应字段删除属于破坏性变化;但旧前端继续多传两个旧请求字段时,新接口会忽略,因此后端先上线兼容旧请求。
- **前端是否必须同步上线**:必须。删除项目类别与票种规格控件、请求字段、响应读取和相关字典依赖。
- **上线顺序边界**:允许“新后端 → 旧前端”短暂过渡;不允许“新前端 → 旧后端”,因为旧后端仍要求 `projectCategory`
### 11.2 回滚方案
- 若接口契约回滚到旧版本,必须同步恢复前端 `projectCategory` 必填提交,否则旧接口会拒绝新增/修改。
- 仅回滚前端到旧版本不会阻断新接口写入,但旧页面读取不到三个已删除响应字段,类别/规格区域会为空,因此不建议长期维持。
## 12. 注意事项
- 删除项目类别控件、票种规格控件及其表单校验。
- 停止在 POST/PUT 请求中传 `projectCategory``specification`
- 停止读取 GET/POST/PUT 响应中的 `projectCategory``projectCategoryName``specification`
- 删除其他收入页面对项目类别字典、票种规格字典的加载与映射。
- Long ID 继续按 String 消费;空明细继续按 `[]` 处理。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5477](https://git.1814.love:8443/wx/HL/issues/5477)
- **PR**: [#5488](https://git.1814.love:8443/wx/HL/pulls/5488)
- **Merge commit**: [da1ee4ebc106](https://git.1814.love:8443/wx/HL/commit/da1ee4ebc1068e5ca20b683769238a3cf165b4b5)
### 13.2 联系人
- **后端负责人**: @yaosutu
- **QA 验证**: Issue #5477 接口验收已完成管理后台真实网关,TARGETED_FALLBACK
## 关联/联系人
### 链接
- [后端工单 #5477](https://git.1814.love:8443/wx/HL/issues/5477)
- [后端 PR #5488](https://git.1814.love:8443/wx/HL/pulls/5488)
- Merge commit: `da1ee4ebc1`
### 联系人
- **后端负责人**: @yst

查看文件

@ -1,70 +0,0 @@
---
author: "wx(GIT)"
schema: "hl-changelog/v2"
ticket: "5479"
title: "看板列表同槽位跨段换车 assignmentSlots 按派车组拆分"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@e0c5f4f75087107fd0d82fbdc104c76d732292dc"
target_release: ""
verified_at: "2026-08-04"
status_note: "看板列表 assignmentSlots 修复:同槽位跨段换车(同 slot 多 assignment_group_id时由仅返回首段改为按派车组返回多段每段含各自区间/车辆/司机/司机手机号;slotSummary 各段区间合计覆盖全槽;单段槽位行为不变。前端若按单条槽位渲染需适配多段展示。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 【修改接口·管理后台】看板列表同槽位跨段换车 assignmentSlots 按派车组拆分 (#5479)
> **PR**: #5484 | **服务**: fleet | **更新时间**: 2026-08-04
## 1. 接口背景
车务看板列表(`GET /admin/fleet/board/orders`)的 `assignmentSlots[]` 原实现按稳定槽位(`assignment_slot_id`)聚合,同槽位跨段换车(同一槽位内前后两段使用不同车辆/司机,如 7 天行程前 3 天蒙A-H7777/阿拉坦 + 后 4 天蒙A-T1557/朝鲁门)时后段被折叠,列表只显示首段车辆/司机,与 matrix/grid 分段结果不一致。本次修复为按派车组(`assignment_group_id`)拆分,每段独立返回。
## 变更接口清单
| # | 接口名 | 方法 | 路径 | 变更类型 | 说明 |
|---|--------|------|------|----------|------|
| 1 | 车务看板列表 | GET | `/admin/fleet/board/orders` | 修改 | `assignmentSlots[]` 同槽跨段换车时由 1 条变为多段(每段独立 assignmentId/assignmentGroupId/区间/车辆/司机) |
## 2. 行为变化
| 场景 | 原来 | 现在 |
|------|------|------|
| 同槽位跨段换车(同 slot 多 group,如 08-10~12 H7777/阿拉坦 + 08-13~16 T1557/朝鲁门) | `assignmentSlots` 仅 1 条(首段车辆/司机,区间覆盖全槽 7 天) | 按派车组返回 2 段段1 slotSummary 08-10~08-12/3天/H7777/阿拉坦;段2 slotSummary 08-13~08-16/4天/T1557/朝鲁门(各自 `assignmentGroupId`/`assignmentId` |
| 单段槽位(同一槽位单派车组,绝大多数) | 1 条 | 1 条(不变) |
| canceled 历史段 | 不展示(整槽全 canceled 时展示 canceled 视图) | 不独立成段(整槽全 canceled 时行为不变) |
| 槽位区间覆盖 | 单条区间=全槽 | 各段区间合计覆盖全槽(不因拆分缩短) |
## 3. 接口详情
- **方法/路径**`GET /admin/fleet/board/orders?teamNo=&page=&pageSize=`
- **响应**`records[].assignmentSlots[]` 现可为多条(同槽跨段);每条结构不变(`assignmentId`/`assignmentGroupId`/`assignmentSlotId`/`slotSummary{assignmentSlotId, slotIndex, totalSlots, serviceStartDate, serviceEndDate, serviceDays, requiredVehicleType, ...}`/`vehiclePlate`/`vehicleModel`/`driverName`/`driverPhone`(掩码)/`assignmentStatus` 等),其中 `slotSummary.serviceStartDate/serviceEndDate/serviceDays` 为该段自身区间;`slotIndex/totalSlots` 为槽位维度(跨段时同一槽位多条共享相同 slotIndex/totalSlots
- **不变**:顶层 record 仍按 requirementId 聚合一条,代表槽位取排序首条;分页/筛选/排序不变
## 4. 前端需要做什么
看板列表若按 `assignmentSlots` 渲染车辆/司机卡片:**需支持同一槽位(相同 `assignmentSlotId`/`slotIndex`)多条段卡片**(展示各自区间、车牌、司机);跨段换车场景现可正确展示后段,勿再按首条截断。单段槽位展示无变化。`slotSummary.serviceStartDate/serviceEndDate/serviceDays` 为段区间(多段合计为全槽)。
## 5. 后端测试、部署与验证
- 单元测试新增同槽跨段换车拆分用例H7777/阿拉坦 3 天 + T1557/朝鲁门 4 天 → 2 段各自区间/车辆/司机正确);更新同槽多组状态冲突、最终不用车混合、全最终不用车 3 个既有用例为按段拆分语义;AssignmentServiceTest 360 + Board* 107 用例全通过。
- 模块验证:`mvn -pl hl-fleet-service -am verify` 3078 用例 0 失败Testcontainers 集成测试本机无 Docker 环境失败,基线同失败非本次引入)。
- 部署TEST hl-fleet-service rolling 完成task_id=1226b3fd,双实例 UP
- 验证TEST 2026-08-04订单 2081633366993494017teamNo=26-2830`assignmentSlots` 由修复前 1 条(仅 H7777/阿拉坦)变为 2 段08-10~12 蒙A-H7777/丰田汉兰达/阿拉坦/135****5019 + 08-13~16 蒙A-T1557/丰田普拉多/朝鲁门/199****1557均 assigned;DB fleet_assignment 7 行 2 组340035264187600896/340023199221813248与两段一一对应;单段槽位回归26-1427 订单 3 槽位各 1 条)正常。
## 关联 / 联系人
### 链接
- **Issue**: [#5479](https://git.1814.love:8443/wx/HL/issues/5479)
- **PR**: [#5484](https://git.1814.love:8443/wx/HL/pulls/5484)
- **Merge commit**: [631ebb259858](https://git.1814.love:8443/wx/HL/commit/631ebb259858)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,109 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5480"
title: "金额序列化规范化(#5480 候选金额 String;#5482 finance 零值 scale=2"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端完成PR #5487 已合并 dev-v3 并部署 TEST;网关实测 finance 零值 balancePaidAmount/fullPaidAmount/depositPaidAmount 已为 \"0.00\"(部署前 \"0\";candidates 当前 TEST 无候选车辆数据,协议价 String 化由序列化单测+代码证据覆盖AssignmentCandidateRespVOAmountSerializationTest。金额口径收敛到全站 String+scale=2 契约。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 金额序列化规范化(#5480/#5482
> **服务**: hl-fleet-service + hl-order-service-v3
> **PR**: #5487
> **Issue**: #5480P3#5482P2
> **联系人**: @wx
> **日期**: 2026-08-04
> **影响范围**: 管理后台车务候选列表金额字段、订单财务 Tab 零值金额字段
---
## ⚠️ 关键变化
金额 BigDecimal 序列化口径收敛:**金额字段一律 JSON String,且两位小数**(对齐平台「金额=String」铁律 #3978)。
1. **#5480fleet**`POST /admin/fleet/assignments/candidates` 车辆候选的 `protocolPrice` / `autoVehicleFeeTotal` 此前输出 **JSON number 浮点**`860.0` / `2580.0`),与同接口 `dailyVehicleFees[].calendarPrice/assignmentPrice`String `"860.00"`)及全站金额 String 契约不一致。现补 `@JsonSerialize(ToStringSerializer)`,输出 String `"860.00"` / `"2580.00"`
2. **#5482order-v3**`GET /v3/admin/order/{id}/finance` 零值金额 `depositPaidAmount` / `balancePaidAmount` / `fullPaidAmount` 此前输出 `"0"`scale=0,同响应 `totalAmount`/`paidAmount` 等为 `"0.00"`scale=2。现统一 scale=2,全部输出 `"0.00"`
**数值不变**(仅序列化形态收敛):前端金额按 String 处理(`>` `<` `-` `*` `/` 自动转数字)不受影响;仅 `a + b` 字符串拼接、`.toFixed()``=== "0.00"` 严格比较三类场景需按既有金额口径处理。
## 变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 派单候选查询 | POST | `/admin/fleet/assignments/candidates` | 序列化口径 | `protocolPrice`/`autoVehicleFeeTotal` number → String"860.00" |
| 2 | 订单财务 Tab | GET | `/v3/admin/order/{id}/finance` | 序列化口径 | 零值 `depositPaidAmount`/`balancePaidAmount`/`fullPaidAmount` "0" → "0.00" |
## 接口详情
### 1. 派单候选查询 `POST /admin/fleet/assignments/candidates`
**响应 `data.vehicleCandidates[]`VehicleCandidateVO**
| 字段 | 类型 | 变化 | 说明 |
|------|------|------|------|
| `protocolPrice` | String | number → String | 用车开始日价格日历参考价(兼容字段);例 `"860.00"` |
| `autoVehicleFeeTotal` | String | number → String | 价格日历自动总车费参考;例 `"2580.00"` |
| `dailyVehicleFees[].calendarPrice/assignmentPrice` | String | 无变化 | 保持 String 回归 |
```json
{
"vehicleId": "2064998142394183681",
"protocolPrice": "860.00",
"autoVehicleFeeTotal": "2580.00",
"dailyVehicleFees": [
{"serviceDate": "2026-07-29", "calendarPrice": "860.00", "assignmentPrice": "860.00", "source": "CALENDAR"}
]
}
```
### 2. 订单财务 Tab `GET /v3/admin/order/{id}/finance`
**响应 `data`FinanceVO**
| 字段 | 变化 | 说明 |
|------|------|------|
| `depositPaidAmount` | "0" → "0.00" | 无订金支付时零值统一两位小数 |
| `balancePaidAmount` | "0" → "0.00" | 无尾款支付时零值统一两位小数 |
| `fullPaidAmount` | "0" → "0.00" | 无全款支付时零值统一两位小数 |
| `totalAmount`/`paidAmount`/`balanceAmount`/`discountAmount`/`surchargeAmount`/`refundAmount` | 无变化 | 保持 "0.00" 形态 |
```json
{
"totalAmount": "1000.00",
"paidAmount": "100.00",
"depositPaidAmount": "100.00",
"balancePaidAmount": "0.00",
"fullPaidAmount": "0.00",
"balanceAmount": "900.00"
}
```
## 验证证据
- fleet`AssignmentCandidateRespVOAmountSerializationTest`2 例:正常值 String 化 + 零值类型保真,fleet verify 3080 例1 例 Docker 基线环境失败与改动无关,stash 复现)。
- order-v3`OrderDetailConverterTest` 新增零值 scale=2 + 序列化断言41/41,order-v3 verify 7443 例全过。
- 网关 TEST 实测(#5482finance 零值 `balancePaidAmount="0.00"`/`fullPaidAmount="0.00"``totalAmount="1000.00"` 同形态(部署前 `"0"`)。
- 网关 TEST#5480):当前环境无候选车辆需求数据(全需求 vehicles=0,端到端候选金额验证暂不可行;由序列化单测protocolPrice="860.00" String 断言)+ 同 VO dailyVehicleFees String 回归 + 代码注解证据覆盖。
## 关联/联系人
### 链接
- [后端工单 #5480](https://git.1814.love:8443/wx/HL/issues/5480)
- [后端 PR #5487](https://git.1814.love:8443/wx/HL/pulls/5487)
- Merge commit: `cbf08a4c02`
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,64 +0,0 @@
---
author: "wx(GIT)"
schema: "hl-changelog/v2"
ticket: "5505"
title: "HOLD 派车短信空串 code 降级:不再被阿里云拒收,通知链路收敛"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: "2026-08-04"
status_note: "HOLD待司机确认阶段派车短信因无真实行程短链code 为空)此前被阿里云 TEMPLATE_PARAMS_ILLEGAL 拒收、通知 outbox 无限重试;本次修复为按产品口径降级:短信通道明确 SKIPsend_log status=3 终态,原因含“短信按产品口径降级”,站内通知不受影响,outbox 收敛 SUCCESS。ASSIGNED 后真实短链短信ITINERARY_READY不受影响。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 【修改接口·管理后台】HOLD 派车短信空串 code 降级(#5505
> **PR**: #5509 | **服务**: fleet + user | **更新时间**: 2026-08-04
## 1. 接口背景
HOLD待司机确认派车阶段创建派单后触发 FLEET_DISPATCH_CREATED 短信:模板 SMS_510265061 正文固定含 `https://hr.1814.love/s/${code}` 链接参数,但 HOLD 阶段组行未冻结(#5461)不签发行程短链,`code` 为空串 → 阿里云以 `isv.TEMPLATE_PARAMS_ILLEGAL` 拒收整条短信,司机收不到通知且 fleet outbox 无限重试retry_count 持续增长)。
## 变更接口清单
| # | 接口名 | 方法 | 路径 | 变更类型 | 说明 |
|---|--------|------|------|----------|------|
| 1 | 通知发送日志 | — | 内部fleet→user 通知中心) | 修改 | reliable 短信 `code` 缺失/空串时明确 SKIPstatus=3,终态,不再外发含空链接参数的短信 |
| 2 | 派单事务 outbox | — | 内部fleet 定时重放) | 修改 | HOLD 占位快照 + 短信明确 SKIP + 全部通道确定性终态时按降级结算收敛为 SUCCESS,不再重试 |
## 2. 行为变化
| 场景 | 原来 | 现在 |
|------|------|------|
| HOLD 派车(待司机确认)短信 | code="" 外发 → 阿里云 TEMPLATE_PARAMS_ILLEGAL 拒收 → send_log FAIL、outbox 无限重试 | 短信通道明确 SKIPsend_log `status=3`,fail_reason 含「短信按产品口径降级」,outbox 收敛 `SUCCESS`;站内通知不受影响 |
| ASSIGNED 后真实短链短信ITINERARY_READY | 正常发送 | 不变code 有值) |
| 短信真实事故(模板配置错误等 FAIL | FAIL 重试至 QUARANTINED | 不变(不降级、不伪造成功) |
## 3. 接口详情
- 管理后台「通知日志」状态枚举不变0~6本次新增的短信 SKIP 使用既有 `status=3`SKIP,fail_reason 为降级说明,可据此识别 HOLD 占位短信
- `holdSentAt` 语义不变只有真实外部通道成功才写入;HOLD 降级场景不写
- 前端无字段/枚举变化;若通知日志页对 status=3 有专门文案,可补充「HOLD 占位短信降级(行程确认后发送)」类展示(非必须)
## 4. 前端需要做什么
无必须改动。可选:通知日志页 SKIPstatus=3文案适配。
## 5. 后端测试、部署与验证
- 定向fleet `AssignmentHoldNotificationEffectServiceTest` 32/32新增降级结算收敛、真实 FAIL 不降级 2 用例;占位快照断言改为 `code` 键不出现;user `SmsChannelSenderTest` 37/37两个历史「空 code 允许发送」断言修正为 SKIP 降级;fleet HOLD/ITINERARY 12 类 + user Dispatcher/LogService 71 用例全绿
- 模块验证:`mvn -pl hl-fleet-service,hl-user-service -am verify`——user 3518 全绿;fleet 3107 用例仅 ReleaseEOccupancyMysql8033RecoveryTest 10 错(缺 CI 专用属性 `fleet.mysql.releaseE.finalMigrationSha256`,基线 dev-v3 同失败,非本次引入)
- 部署TEST fleettask_id=80bf903a+ usertask_id=4bb72dfd双实例 UP
- 验证TEST 2026-08-04存量 PENDING HOLD_NOTIFICATION 事件重试后按降级收敛send_log SKIP 终态、outbox SUCCESS、无 TEMPLATE_PARAMS_ILLEGAL
## 关联 / 联系人
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,79 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5520"
title: "保险订单 totalPremium 恒 0 修复(保游金额字符串解析)"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@ef497e8e30d2df2d55d97f5554c8c5e921dc49ef"
target_release: ""
verified_at: ""
status_note: "后端完成PR #5521 已合并 dev-v3 并部署 TEST;网关实测新投保 totalPremium=\"88.00\"(修复前恒 \"0\",DB 落库 88.00 与 raw_response 一致;存量 252 行已回填全量一致 253/253。行为修复保单列表/详情/详情弹窗的 totalPremium 从恒 0 变为平台实际保费。"
updated_at: "2026-08-04"
base: "dev-v3"
---
# 保险订单 totalPremium 恒 0 修复(#5520
> **服务**: hl-order-service-v3+hl-fleet-service 对账联动)
> **PR**: #5521
> **Issue**: #5520P1
> **联系人**: @wx
> **日期**: 2026-08-04
> **影响范围**: 管理后台司机保险保单列表/详情 totalPremium 展示、退保/对账金额计算
---
## 变更说明
**根因**jackson-databind 2.13 的 `TextNode` 未覆盖 `decimalValue()`,保游响应中的字符串金额节点(`"TotalPremium":"88.00"`)调用时走基类默认实现恒返回 `BigDecimal.ZERO`,导致投保落库 `insurance_order.total_premium` 恒 0.00。
**修复**:新增 `BaoyouMoney` 安全解析(数值节点走 decimalValue、文本节点按 BigDecimal 解析、null/空串/非法兜底),替换保险链路 5 处金额解析(投保落库 TotalPremium、保单查询 TotalPremium、回调 OrderMoney、保游余额 Balance、费率 Premium
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| POST | /admin/fleet/drivers/{driverId}/insurance/purchase | 管理后台-司机保险 |
| GET | /admin/fleet/drivers/{driverId}/insurance/policies | 管理后台-司机保险 |
| POST | /admin/fleet/drivers/{driverId}/insurance/policies/{insuranceOrderId}/cancel | 管理后台-司机保险 |
| GET | /admin/fleet/reconciliation/insurance | 管理后台-保险对账 |
## 接口行为变化
| 接口 | 字段 | 修复前 | 修复后 |
|---|---|---|---|
| `POST /admin/fleet/drivers/{driverId}/insurance/purchase` | totalPremium | `"0"`(恒) | `"88.00"`(平台实际保费) |
| `GET /admin/fleet/drivers/{driverId}/insurance/policies` | totalPremium | `"0"` | `"88.00"` |
| 退保链路 | 退费计算基数 | 0 | 平台实际保费 |
| 保险对账baoyou 口径) | 摊销金额 | 0 | 保单实际保费(新链路) |
字段类型、命名、契约结构均无变化,仅值语义修正。
## 存量数据
- TEST 环境 252 行存量保单245 INSURED + 7 CANCELLED已按 `raw_response.TotalPremium` 精确回填 `total_premium=88.00`,全量 253/253 与平台响应一致。
- 存量 `fleet_insurance_task` 任务快照 premium_amount=0 为历史创建时未关联保单 id 的派生快照,不做模糊回填(避免错配);新链路任务金额从保单 total_premium 复制自动正确。
## 验证证据
- 网关实测api.test.1814.love:9443,admin,2026-08-04新投保 totalPremium="88.00"(修复前恒 "0",DB total_premium=88.00 与 raw_response 一致
- 存量 252 行回填后全量 253/253 raw==列 一致;退保金额保持 88.00、update_time 刷新
- 定向测试BaoyouMoneyTest 4/4 + savePurchaseResult 字符串金额回归,保险域 354/354 全绿PR #5521
## 关联/联系人
### 链接
- [后端工单 #5520](https://git.1814.love:8443/wx/HL/issues/5520)
- [后端 PR #5521](https://git.1814.love:8443/wx/HL/pulls/5521)
- Merge commit: `3bc4ff3ba8`
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,720 +0,0 @@
---
author: "yst(GIT)"
schema: "hl-changelog/v2"
ticket: "5356"
title: "车辆 Tab 三接口汇总"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "partial"
frontend_status: "implemented"
frontend_owner: "Pi"
frontend_ref: "v2.1@21dccf38d1413b098cfd5a456d3fb76611581ebb"
target_release: ""
verified_at: "2026-08-02"
status_note: "汇总 #5356#5360#5380 已实现的车辆 Tab 现行契约;管理后台使用 Step3 GET/PUT 和 vehicle-options,并按 settlementReady/blockReasonCode 区分车辆两类空态。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 🔧【修改接口·管理后台】车辆 Tab 三接口汇总 (#5356 / #5360 / #5380)
> **服务**`hl-order-service-v3` **更新时间**2026-08-05 **消费端**:管理后台
## 1. 接口背景
车辆核单 Tab 需要一份可直接对接的完整契约:先用车辆下拉接口检索手工行候选,再读取车辆核单草稿,最后按草稿版本全量保存。本文汇总三个现行接口,并纳入车辆草稿查询的最新空状态语义;不替代或修改三份历史通知。
## 2. 变更清单
| # | 接口名 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 查询车辆核单草稿 | `GET` | `/v3/admin/order/{orderId}/settlement/step3/vehicles` | 修改 | 返回车辆核单全量草稿;空结果用 `settlementReady/blockReasonCode` 区分是否可继续核单 |
| 2 | 全量保存车辆核单草稿 | `PUT` | `/v3/admin/order/{orderId}/settlement/step3/vehicles` | 修改 | 携带 `version` 全量保存,保护 `FLEET` 权威行并返回保存后的完整草稿 |
| 3 | 查询核单车辆异步下拉 | `GET` | `/v3/admin/order/{orderId}/settlement/vehicle-options` | 新增 | 按车牌、品牌型号、车型大类或常驻司机姓名检索候选车辆 |
三个接口均要求管理后台登录态和订单查看权限,房务角色不可访问;无接口级特殊限流。两个 GET 为只读幂等,PUT 以当前 `version` 和全量 `items` 保存。
## 3. 接口详情
### 3.1 查询车辆核单草稿
- **接口说明**:查询车辆 Tab 当前全量明细、草稿版本、确认状态以及车辆费用是否具备完成核单条件。
- **方法与路径**`GET /v3/admin/order/{orderId}/settlement/step3/vehicles`
- **认证**:管理后台登录态;需满足订单查看权限;房务角色不可访问。
- **幂等性**:是,只读查询。
- **限流**:未声明独立限流规则。
**路径参数**
| 字段 | 类型 | 必填 | 说明与校验 |
|---|---|---|---|
| `orderId` | String(Long) | 是 | 订单 ID,必须为正整数;按字符串传递 |
无 Query 参数、无请求体。
**统一响应外层**
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `code` | Integer | 否 | 成功为 `200` |
| `message` | String | 否 | 结果说明 |
| `data` | VehicleDraft/null | 失败时为空 | 成功时为车辆核单草稿 |
| `traceId` | String | 是 | 链路追踪 ID |
| `success` | Boolean | 否 | `code=200` 时为 `true` |
**成功响应 `data`**
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `orderId` | String(Long) | 否 | 订单 ID,按字符串返回 |
| `version` | Long | 否 | 当前草稿版本;首次空结果为 `0` |
| `totalAmount` | Decimal | 否 | 全部车辆明细金额合计;空结果为 `0.00` |
| `allConfirmed` | Boolean | 否 | 非空明细是否全部确认;空状态取值见判定表 |
| `settlementReady` | Boolean | 否 | 车辆费用是否具备完成核单条件 |
| `blockReasonCode` | String/null | 是 | 不具备条件时的机器可读原因;具备条件时为 `null` |
| `items` | VehicleItem[] | 否 | 当前全量明细;无明细时为 `[]` |
**`data.items[]`**
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `id` | String(Long) | 否 | 车辆核单明细 ID |
| `sourceType` | String | 否 | `FLEET``MANUAL` |
| `sourceTypeName` | String | 否 | 车务或手工 |
| `serviceDate` | String(date) | 否 | 服务日期,`YYYY-MM-DD` |
| `vehicleId` | String(Long) | 是 | 车辆 ID |
| `vehiclePlate` | String | 是 | 车牌号 |
| `vehicleModelId` | String(Long) | 是 | 车型 ID |
| `vehicleModelName` | String | 是 | 车型名称 |
| `driverId` | String(Long) | 是 | 司机 ID |
| `driverName` | String | 是 | 司机姓名 |
| `amount` | Decimal | 否 | 核单金额 |
| `paymentMethod` | String | 否 | 付款方式编码,见 §6.2 |
| `paymentMethodName` | String | 否 | 付款方式名称 |
| `settlementConfirmStatus` | String | 否 | 确认状态编码,见 §6.3 |
| `settlementConfirmStatusName` | String | 否 | 未确认或已确认 |
| `remark` | String | 是 | 备注 |
| `voucherUrls` | String[] | 否 | 凭证 URL;无凭证时为 `[]` |
**错误码**
| code | 含义 | 触发场景 |
|---|---|---|
| `400` | 请求参数错误 | `orderId` 不是正整数 |
| `403` | 无访问权限 | 登录态或角色无权访问 |
| `581007` | 订单不存在 | `orderId` 对应订单不存在 |
| `584071` | 无权访问该订单 | 当前账号不在订单可访问范围内 |
| `584100` | 车辆费用暂时不可用 | 车辆费用来源调用失败、响应身份不匹配或必要字段无效 |
| `584101` | 车辆费用尚未满足核单条件 | 已有非空车辆明细,但来源未完结或费用条件未满足 |
`584102` 不再表示“有需求但费用尚未生成”;该场景现在返回 `code=200` 的阻断空状态。
**业务边界与空状态判定**
| 场景 | `items` | `totalAmount` | `settlementReady` | `blockReasonCode` | `allConfirmed` | 结果 |
|---|---|---|---|---|---|---|
| 无当前用车需求 | `[]` | `0.00` | `true` | `null` | `true` | 成功,可继续完成核单 |
| 有当前用车需求,但费用尚未生成 | `[]` | `0.00` | `false` | `VEHICLE_FEE_NOT_READY` | `false` | 成功,但不能完成核单 |
| 有明细、来源就绪,仍有未确认行 | 非空 | 明细合计 | `true` | `null` | `false` | 成功,需先确认明细 |
| 有明细、来源就绪且全部确认 | 非空 | 明细合计 | `true` | `null` | `true` | 成功,可继续完成核单 |
- `items=[]` 不是失败判据,必须读取 `settlementReady`
- `allConfirmed=true` 只说明没有未确认行,能否完成核单仍以 `settlementReady` 为准。
- 车辆来源失败或必要字段无效仍返回业务错误,不转换为空结果。
**典型成功请求**
```http
GET /v3/admin/order/900000000001/settlement/step3/vehicles
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**典型成功响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000001",
"version": 4,
"totalAmount": 1200.00,
"allConfirmed": true,
"settlementReady": true,
"blockReasonCode": null,
"items": [
{
"id": "930000000001",
"sourceType": "FLEET",
"sourceTypeName": "车务",
"serviceDate": "2026-08-01",
"vehicleId": "880000000001",
"vehiclePlate": "藏A12345",
"vehicleModelId": "870000000001",
"vehicleModelName": "七座商务车",
"driverId": "860000000001",
"driverName": "张师傅",
"amount": 1200.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "金额已核对",
"voucherUrls": []
}
]
},
"traceId": null,
"success": true
}
```
**边界请求:有需求但费用未就绪**
```http
GET /v3/admin/order/900000000003/settlement/step3/vehicles
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**边界响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000003",
"version": 0,
"totalAmount": 0.00,
"allConfirmed": false,
"settlementReady": false,
"blockReasonCode": "VEHICLE_FEE_NOT_READY",
"items": []
},
"traceId": null,
"success": true
}
```
**边界请求:无当前用车需求**
```http
GET /v3/admin/order/900000000002/settlement/step3/vehicles
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**边界响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000002",
"version": 0,
"totalAmount": 0.00,
"allConfirmed": true,
"settlementReady": true,
"blockReasonCode": null,
"items": []
},
"traceId": null,
"success": true
}
```
**业务失败请求:车辆来源暂时不可用**
```http
GET /v3/admin/order/900000000004/settlement/step3/vehicles
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**业务失败响应**
```json
{
"code": 584100,
"message": "车务车辆总车费暂时不可用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
```
### 3.2 全量保存车辆核单草稿
- **接口说明**:携带查询所得版本,全量保存车辆核单行并返回保存后的完整草稿。
- **方法与路径**`PUT /v3/admin/order/{orderId}/settlement/step3/vehicles`
- **认证**:管理后台登录态;需满足订单查看及核单写权限;房务角色不可访问。
- **幂等性**:业务语义为全量替换;成功后版本递增,原请求不可原样重放,使用旧版本重试会返回 `584108`
- **限流**:未声明独立限流规则。
**路径参数**
| 字段 | 类型 | 必填 | 说明与校验 |
|---|---|---|---|
| `orderId` | String(Long) | 是 | 订单 ID,必须为正整数;按字符串传递 |
无 Query 参数。
**请求体**
| 字段 | 类型 | 必填 | 说明与校验 |
|---|---|---|---|
| `version` | Long | 是 | GET 返回的草稿版本,最小 `0` |
| `items` | VehicleItem[] | 是 | 全量明细;现存 `FLEET` 行必须全部原样带回 |
| `items[].id` | String(Long) | FLEET/更新时是 | 已保存行 ID;手工新行为空 |
| `items[].sourceType` | String | 是 | `FLEET``MANUAL` |
| `items[].serviceDate` | String(date) | 是 | 服务日期,`YYYY-MM-DD` |
| `items[].vehicleId` | String(Long) | 否 | 正整数车辆 ID |
| `items[].vehiclePlate` | String | 否 | 车牌,最长 64 字符 |
| `items[].vehicleModelId` | String(Long) | 否 | 正整数车型 ID |
| `items[].vehicleModelName` | String | 否 | 车型名,最长 128 字符 |
| `items[].driverId` | String(Long) | 否 | 正整数司机 ID |
| `items[].driverName` | String | 否 | 司机名,最长 64 字符 |
| `items[].amount` | Decimal | 是 | 大于等于 `0.00`;最多 10 位整数、2 位小数 |
| `items[].paymentMethod` | String | 是 | `CASH_PAID/SIGNED/COMPANY_PAID` |
| `items[].settlementConfirmStatus` | String | 是 | `UNCONFIRMED/CONFIRMED` |
| `items[].remark` | String | 否 | 最长 500 字符 |
| `items[].voucherUrls` | String[] | 否 | 最多 9 个 HTTP/HTTPS URL;单个最长 1024 字符 |
请求对象和明细对象均不接受未声明字段。
**成功响应**
响应类型同 §3.1 的 `VehicleDraft`,包含 `orderId/version/totalAmount/allConfirmed/settlementReady/blockReasonCode/items` 及完整明细字段;`version` 返回保存后的新版本。
**错误码**
| code | 含义 | 触发场景 |
|---|---|---|
| `400` | 参数校验失败 | 必填缺失、格式/长度/枚举错误或出现未知字段 |
| `403` | 无访问权限 | 登录态、角色或核单写权限不足 |
| `581007` | 订单不存在 | `orderId` 对应订单不存在 |
| `584071` | 无权访问该订单 | 当前账号不在订单可访问范围内 |
| `584089` | 核单或结算已完成 | 当前订单已不能修改资金明细 |
| `584106` | 确认状态非法 | 非 `UNCONFIRMED/CONFIRMED` |
| `584107` | 手工新增行必须先未确认 | `id=null``MANUAL` 新行直接传 `CONFIRMED` |
| `584108` | 车辆草稿版本冲突 | PUT 的 `version` 已过期 |
| `584109` | 车务来源字段不可改删 | 修改、删除或漏传现存 `FLEET` 行的权威字段 |
**业务边界**
- `FLEET` 行的服务日期、车辆、司机、金额和付款方式不可修改或删除;仅允许修改确认状态、备注和凭证。
- 全量保存必须带回所有现存 `FLEET` 行;`MANUAL` 行可新增、修改,或通过不再提交该行来删除。
- 手工新行首次只能提交 `UNCONFIRMED`;取得 `id` 后,下一次 PUT 才可改为 `CONFIRMED`
- `version` 冲突后必须重新 GET,并基于最新全量草稿重新编辑和保存。
**典型成功请求**
```http
PUT /v3/admin/order/900000000001/settlement/step3/vehicles
Authorization: Bearer <管理后台访问令牌>
Content-Type: application/json
```
```json
{
"version": 3,
"items": [
{
"id": "930000000001",
"sourceType": "FLEET",
"serviceDate": "2026-08-01",
"vehicleId": "880000000001",
"vehiclePlate": "藏A12345",
"vehicleModelId": "870000000001",
"vehicleModelName": "七座商务车",
"driverId": "860000000001",
"driverName": "张师傅",
"amount": 1200.00,
"paymentMethod": "COMPANY_PAID",
"settlementConfirmStatus": "CONFIRMED",
"remark": "金额已核对",
"voucherUrls": []
}
]
}
```
**典型成功响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000001",
"version": 4,
"totalAmount": 1200.00,
"allConfirmed": true,
"settlementReady": true,
"blockReasonCode": null,
"items": [
{
"id": "930000000001",
"sourceType": "FLEET",
"sourceTypeName": "车务",
"serviceDate": "2026-08-01",
"vehicleId": "880000000001",
"vehiclePlate": "藏A12345",
"vehicleModelId": "870000000001",
"vehicleModelName": "七座商务车",
"driverId": "860000000001",
"driverName": "张师傅",
"amount": 1200.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "金额已核对",
"voucherUrls": []
}
]
},
"traceId": null,
"success": true
}
```
**边界请求:新增金额为 0 的手工未确认行**
```http
PUT /v3/admin/order/900000000001/settlement/step3/vehicles
Authorization: Bearer <管理后台访问令牌>
Content-Type: application/json
```
```json
{
"version": 4,
"items": [
{
"sourceType": "MANUAL",
"serviceDate": "2026-08-02",
"vehicleId": "880000000002",
"vehiclePlate": "藏A54321",
"vehicleModelId": "870000000001",
"vehicleModelName": "七座商务车",
"driverId": null,
"driverName": null,
"amount": 0.00,
"paymentMethod": "CASH_PAID",
"settlementConfirmStatus": "UNCONFIRMED",
"remark": null,
"voucherUrls": []
}
]
}
```
**边界响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000001",
"version": 5,
"totalAmount": 0.00,
"allConfirmed": false,
"settlementReady": true,
"blockReasonCode": null,
"items": [
{
"id": "930000000002",
"sourceType": "MANUAL",
"sourceTypeName": "手工",
"serviceDate": "2026-08-02",
"vehicleId": "880000000002",
"vehiclePlate": "藏A54321",
"vehicleModelId": "870000000001",
"vehicleModelName": "七座商务车",
"driverId": null,
"driverName": null,
"amount": 0.00,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现金已付",
"settlementConfirmStatus": "UNCONFIRMED",
"settlementConfirmStatusName": "未确认",
"remark": null,
"voucherUrls": []
}
]
},
"traceId": null,
"success": true
}
```
**业务失败请求:提交过期版本**
```http
PUT /v3/admin/order/900000000001/settlement/step3/vehicles
Authorization: Bearer <管理后台访问令牌>
Content-Type: application/json
```
```json
{
"version": 3,
"items": []
}
```
**业务失败响应**
```json
{
"code": 584108,
"message": "车辆核单明细已变化,请刷新后重试",
"data": null,
"traceId": null,
"success": false
}
```
### 3.3 查询核单车辆异步下拉
- **接口说明**`keyword` 可匹配车牌、品牌型号、车型大类和常驻司机姓名;返回轻量车辆候选。
- **方法与路径**`GET /v3/admin/order/{orderId}/settlement/vehicle-options`
- **认证**:管理后台登录态;管理员、超级管理员可查看任意订单,其他允许角色仅可查看本人作为定制师的订单;房务角色不可访问。
- **幂等性**:是,只读查询。
- **限流**:未声明独立限流规则。
**路径与 Query 参数**
| 字段 | 位置 | 类型 | 必填 | 默认值 | 说明与校验 |
|---|---|---|---|---|---|
| `orderId` | path | String(Long) | 是 | — | 订单 ID,必须为正整数;按字符串传递 |
| `keyword` | query | String | 否 | 空 | 模糊匹配车牌、品牌型号、车型大类或常驻司机姓名;空白表示不过滤 |
| `limit` | query | Integer | 否 | `10` | 非正数按 10 处理;超过 20 按 20 处理 |
无请求体。
**统一响应外层**
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `code` | Integer | 否 | 成功为 `200` |
| `message` | String | 否 | 结果说明 |
| `data` | VehicleOption[] | 失败时为空 | 没有匹配项时为 `[]` |
| `traceId` | String | 是 | 链路追踪 ID |
| `success` | Boolean | 否 | `code=200` 时为 `true` |
**`data[]`**
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `vehicleId` | String(Long) | 否 | 车辆 ID,按字符串返回 |
| `plate` | String | 是 | 车牌 |
| `modelName` | String | 是 | 品牌型号 |
| `typeName` | String | 是 | 车型大类名称 |
| `primaryDriverId` | String(Long) | 是 | 常驻司机 ID;无常驻司机时为 `null` |
| `primaryDriverName` | String | 是 | 常驻司机姓名;无常驻司机时为 `null` |
| `label` | String | 否 | 展示文案,依次包含车牌、品牌型号、车型大类和常驻司机;无常驻司机时最后一段为“无常驻司机” |
`data[]` 只包含以上七个字段,不包含车辆费用、付款方式或核单确认状态。
**错误码**
| code | 含义 | 触发场景 |
|---|---|---|
| `400` | 订单 ID 必须大于 0 | `orderId <= 0` |
| `581007` | 订单不存在 | `orderId` 对应订单不存在 |
| `581008` | 无权查看此订单 | 非管理员访问其他定制师订单,或上下文缺少订单归属判断所需的管理员 ID |
| `581045` | 房务角色无权查看订单详情 | 房务管理员或房务组长调用 |
| `584072` | 车务司机车辆信息暂时不可用 | 车辆候选信息不可用 |
登录态无效或缺失时,请求在进入接口前由统一认证拦截。
**业务边界**
- 最多返回 20 条;没有匹配项返回 `[]`
- `keyword` 为空或空白时不过滤;`limit <= 0` 按 10,`limit > 20` 按 20。
- 本接口只查询候选车辆;成功响应不表示已选择、保存或确认车辆。
- `vehicleId` 与非空 `primaryDriverId` 必须按字符串处理。
**典型成功请求**
```http
GET /v3/admin/order/2079454953641836546/settlement/vehicle-options?keyword=%E5%BC%A0%E5%B8%88%E5%82%85&limit=10
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**典型成功响应**
```json
{
"code": 200,
"message": "成功",
"data": [
{
"vehicleId": "9202101",
"plate": "蒙A-88888",
"modelName": "丰田汉兰达",
"typeName": "SUV",
"primaryDriverId": "9204101",
"primaryDriverName": "张师傅",
"label": "蒙A-88888***丰田汉兰达***SUV***张师傅"
}
],
"traceId": null,
"success": true
}
```
**边界请求:最大条数且无匹配结果**
```http
GET /v3/admin/order/2079454953641836546/settlement/vehicle-options?keyword=%E4%B8%8D%E5%AD%98%E5%9C%A8&limit=20
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**边界响应**
```json
{
"code": 200,
"message": "成功",
"data": [],
"traceId": null,
"success": true
}
```
**业务失败请求:订单 ID 非法**
```http
GET /v3/admin/order/0/settlement/vehicle-options
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**业务失败响应**
```json
{
"code": 400,
"message": "订单 ID 必须大于 0",
"data": null,
"traceId": null,
"success": false
}
```
## 6. 枚举 / 数据字典
### 6.1 `sourceType`
**所属字段**:车辆草稿请求/响应 `items[].sourceType` **类型**String **必填**:是
| 值 | 中文 | 说明 |
|---|---|---|
| `FLEET` | 车务 | 车务同步的权威行,业务字段不可修改或删除 |
| `MANUAL` | 手工 | 管理后台手工维护的车辆核单行 |
### 6.2 `paymentMethod`
**所属字段**:车辆草稿请求/响应 `items[].paymentMethod` **类型**String **必填**:是
| 值 | 中文 | 说明 |
|---|---|---|
| `CASH_PAID` | 现金已付 | 现金支付 |
| `SIGNED` | 签单 | 按签单方式结算 |
| `COMPANY_PAID` | 公司付款 | 由公司支付 |
### 6.3 `settlementConfirmStatus`
**所属字段**:车辆草稿请求/响应 `items[].settlementConfirmStatus` **类型**String **必填**:是
| 值 | 中文 | 说明 |
|---|---|---|
| `UNCONFIRMED` | 未确认 | 当前车辆费用行尚未完成核单确认 |
| `CONFIRMED` | 已确认 | 当前车辆费用行已完成核单确认 |
### 6.4 `blockReasonCode`
**所属字段**:车辆草稿响应 `data.blockReasonCode` **类型**String/null
| 值 | 中文 | 说明 |
|---|---|---|
| `VEHICLE_FEE_NOT_READY` | 车辆费用尚未就绪 | 有当前用车需求,但尚无可返回的车辆费用明细;此时 `settlementReady=false` |
| `null` | 无阻断原因 | 此时 `settlementReady=true`;为 JSON 空值,不是字符串 `"null"` |
车辆下拉接口不包含枚举或数据字典字段。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
| GET 草稿 `data.settlementReady` | 不对管理后台输出 | 返回 Boolean,明确车辆费用是否具备完成核单条件 |
| GET 草稿 `data.blockReasonCode` | 不存在 | 返回 String/null;未就绪时为 `VEHICLE_FEE_NOT_READY` |
| 车辆下拉 `data[]` | 无独立候选接口 | 返回七字段轻量候选数组 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 无当前用车需求 | 空明细但无公开就绪原因字段 | `items=[]``settlementReady=true``blockReasonCode=null` |
| 有需求但费用未生成 | 返回 `584102` | 成功空结果:`items=[]``settlementReady=false``blockReasonCode=VEHICLE_FEE_NOT_READY` |
| 保存车辆草稿 | 车辆入口与字段口径分散 | 使用 Step3 PUT,携带 `version` 和全量 `items` |
| 手工选择车辆 | 无专用异步候选契约 | 使用 `vehicle-options` 按关键词查询,最多 20 条 |
## 11. 影响评估 / 回滚
- **是否破坏向后兼容**GET 空状态行为有兼容影响;依赖 `584102` 或仅看 `items.length` 的旧逻辑需调整。新增字段和车辆下拉接口本身向后兼容。
- **前端是否必须同步上线**:是。车辆 Tab 必须读取 `settlementReady`,并在 PUT 版本冲突后重新查询;需要手工车辆选择时使用 `vehicle-options`
- **回滚边界**:前端不得回滚到捕获 `584102` 识别未就绪空态的版本,也不得恢复已下线的旧车辆费用入口。
## 12. 注意事项
- GET 返回 `code=200``items=[]` 时,必须读取 `settlementReady`,不能直接判失败或无条件放行。
- 完成核单前同时检查 `settlementReady``allConfirmed`
- 清理对 GET `584102` 的空态兼容逻辑;未就绪现在由 `blockReasonCode=VEHICLE_FEE_NOT_READY` 表达。
- PUT 必须提交 GET 返回的当前 `version` 和全量明细;不要遗漏或改写 `FLEET` 权威字段。
- 订单、明细、车辆、车型和司机 ID 均按字符串处理;金额按 Decimal 处理。
- 车辆下拉只提供候选,不返回费用或确认状态;选中后仍需组装 `MANUAL` 行并通过 Step3 PUT 保存。
## 13. 关联 / 联系人
### 13.1 Issue、PR 与提交
| 范围 | Issue | PR | Feature commit | Merge commit |
|---|---|---|---|---|
| 车辆 Step3 GET/PUT | [#5356](https://git.1814.love:8443/wx/HL/issues/5356) | [#5362](https://git.1814.love:8443/wx/HL/pulls/5362) | [6e396f6fc4](https://git.1814.love:8443/wx/HL/commit/6e396f6fc48fbf6581e87224bb85f2c811759727) | [cfac945db2](https://git.1814.love:8443/wx/HL/commit/cfac945db268640b0b9e60b4d8c7ab55739a69e3) |
| 车辆异步下拉 | [#5360](https://git.1814.love:8443/wx/HL/issues/5360) | [#5361](https://git.1814.love:8443/wx/HL/pulls/5361) | [5be7985164](https://git.1814.love:8443/wx/HL/commit/5be7985164cdae5417cdeb2a2be7d104dc8fdae8) | [0ff4ef45](https://git.1814.love:8443/wx/HL/commit/0ff4ef45ccd0f4ebb1d3be00b27cde06f26bce0b) |
| GET 空状态语义 | [#5380](https://git.1814.love:8443/wx/HL/issues/5380) | [#5393](https://git.1814.love:8443/wx/HL/pulls/5393) | — | [e4c1720871](https://git.1814.love:8443/wx/HL/commit/e4c1720871f33db38936d709caa7696db199ad1f) |
### 13.2 联系人
- **后端负责人**@yst / yaosutu
- **前端消费方**:管理后台车辆核单 Tab
## 关联/联系人
### 链接
- [后端工单 #5356](https://git.1814.love:8443/wx/HL/issues/5356)
### 联系人
- **后端负责人**: @yst

查看文件

@ -1,323 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5429"
title: "核单门票/游玩统一资源日期价格下拉"
consumer: "admin"
author: "yst"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@0a8a72ed6a978a9f14187679188f4c8308e354cd"
target_release: ""
verified_at: ""
status_note: "hl-resource-service;PR #5528 与补丁 PR #5534 已合并并部署测试服,Gateway 真实验收通过;等待管理后台接入。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 【✨ 新增接口·管理后台】核单门票/游玩统一资源日期价格下拉(#5429
> **PR**: [#5528](https://git.1814.love:8443/wx/HL/pulls/5528)、[#5534](https://git.1814.love:8443/wx/HL/pulls/5534)
> **服务**: hl-resource-service
> **更新时间**: 2026-08-05
## 1. 接口背景
管理后台核单 Step 2 新增门票/游玩项目时,需要在同一个分页结果中搜索景区和游玩项目,并按实际游玩日期取得协议价、结算价和建议核算单价。该接口统一返回两类资源;即使当天未配置价格,也仍可返回资源供人工核单。
## 变更接口
| # | 接口名 | 方法 | 路径 | 变更类型 | 说明 |
|---|--------|------|------|----------|------|
| 1 | 分页查询门票/游玩资源选项 | GET | `/admin/resource-options/ticket-items` | 新增接口 | 按游玩日期统一查询景区和游玩项目,并返回资源级日期价格 |
## 3. 接口详情
- **使用场景**:核单 Step 2 新增或搜索实际发生的门票/游玩项目。
- **认证**:需要有效的管理后台 JWT,使用 `Authorization: Bearer <token>`
- **幂等性**:幂等,只读查询。
- **限流**:无接口专属限流约定。
- **响应类型**`Result<PageResult<TicketResourceOptionRespVO>>`
- **请求体**:无。
## 4. 接口入参
### 4.1 Query 参数
| 字段 | 类型 | 必填 | 默认值 | 校验规则 | 说明 |
|------|------|------|--------|----------|------|
| `dayDate` | String | 是 | 无 | ISO 日期 `yyyy-MM-dd` | 实际游玩日期;价格字段只匹配这一天的资源价格 |
| `keyword` | String | 否 | `null` | 最长 100 个字符 | 资源名称模糊搜索;自动去除首尾空格;去除后为空等同未传 |
| `resourceType` | String | 否 | `null` | `SCENIC``ACTIVITY` | 不传时同时查询景区和游玩项目 |
| `status` | Integer | 否 | `null` | `0``1` | 不传时同时包含已启用和已下架资源 |
| `page` | Integer | 否 | `1` | 最小值 1 | 当前页码 |
| `pageSize` | Integer | 否 | `20` | 1100 | 每页条数 |
### 4.2 请求体字段
无请求体。
## 5. 出参字段
### 5.1 统一响应与分页字段
| 字段 | 类型 | 可空 | 说明 |
|------|------|------|------|
| `code` | Integer | 否 | 业务状态码;成功为 `200` |
| `message` | String | 否 | 响应消息;成功为 `成功` |
| `success` | Boolean | 否 | `code == 200` 时为 `true` |
| `traceId` | String | 是 | 链路追踪 ID |
| `data` | Object | 否 | 分页结果 |
| `data.records` | Array | 否 | 本页资源列表;无匹配资源时为 `[]` |
| `data.total` | Integer | 否 | 符合条件的资源总数 |
| `data.page` | Integer | 否 | 当前页码 |
| `data.pageSize` | Integer | 否 | 每页条数 |
### 5.2 `data.records[]` 资源字段(完整 13 项)
| 字段 | 类型 | 可空 | 说明 |
|------|------|------|------|
| `resourceType` | String | 否 | 资源类型:`SCENIC` 景区、`ACTIVITY` 游玩项目 |
| `resourceId` | String | 否 | 资源 ID;固定按 JSON String 返回,不要转换为 JavaScript `Number` |
| `resourceName` | String | 否 | 资源名称;名称为 `null`、空字符串或纯空白的资源不会返回 |
| `status` | Integer | 否 | `0` 已下架、`1` 已启用 |
| `statusName` | String | 否 | 与 `status` 对应:`已下架``已启用` |
| `city` | String | 是 | 城市;景区优先返回城市名称;允许 `null` 或空字符串 |
| `settleType` | String | 是 | 结算方式:`cash``sign``company`;资源未配置时可为 `null` |
| `dayDate` | String | 否 | 本次查询的实际游玩日期,格式 `yyyy-MM-dd` |
| `protocolPrice` | Decimal | 是 | 该日期的协议价;未配置时为 `null` |
| `settlementPrice` | Decimal | 是 | 该日期的结算价;未配置时为 `null` |
| `ticketUnitPrice` | Decimal | 是 | 建议核算单价:优先取结算价,结算价为空时回退协议价;两者都为空时为 `null` |
| `priceConfigured` | Boolean | 否 | `ticketUnitPrice` 非空时为 `true`,否则为 `false` |
| `specName` | String | 否 | 固定返回 `成人票` |
## 6. 枚举 / 数据字典
### 6.1 `resourceType`
**所属字段**Query `resourceType`、响应 `data.records[].resourceType`
**类型**String
| 值 | 中文 | 说明 |
|----|------|------|
| `SCENIC` | 景区 | 来源为景区资源 |
| `ACTIVITY` | 游玩项目 | 来源为活动/游玩项目资源 |
### 6.2 `status` / `statusName`
**所属字段**Query `status`、响应 `data.records[].status/statusName`
| `status` | `statusName` | 说明 |
|----------|--------------|------|
| `1` | `已启用` | 排序时优先返回;可用于当前资源选择 |
| `0` | `已下架` | 默认查询仍会返回,满足事后核单;传 `status=1` 可排除 |
### 6.3 `settleType`(字典 `resource_settle_type`
**所属字段**:响应 `data.records[].settleType`
**类型**String / null
| 值 | 中文 | 说明 |
|----|------|------|
| `cash` | 现付 | 资源结算方式为现付 |
| `sign` | 签单 | 资源结算方式为签单 |
| `company` | 公司付款 | 资源结算方式为公司付款 |
### 6.4 价格字段关系
| 条件 | `ticketUnitPrice` | `priceConfigured` |
|------|-------------------|-------------------|
| `settlementPrice` 非空 | 取 `settlementPrice` | `true` |
| `settlementPrice` 为空、`protocolPrice` 非空 | 取 `protocolPrice` | `true` |
| 两个价格都为空 | `null` | `false` |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询完成;没有匹配资源时仍为成功,`records=[]` |
| `400` | 参数校验失败 | 缺少/无法解析 `dayDate``keyword` 超过 100 字符,`resourceType` 非法,`status` 非 0/1,或分页参数越界 |
| `401` | 未认证或认证失效 | 未携带有效的管理后台 JWT |
| `500` | 系统异常 | 查询过程发生未预期异常 |
## 8. 示例
### 8.1 典型成功:同时返回景区和游玩项目
**请求**
```http
GET /admin/resource-options/ticket-items?dayDate=2026-08-03&keyword=体验&page=1&pageSize=20
Authorization: Bearer <admin-token>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"resourceType": "SCENIC",
"resourceId": "2079454953641836546",
"resourceName": "草原景区体验区",
"status": 1,
"statusName": "已启用",
"city": "呼伦贝尔市",
"settleType": "sign",
"dayDate": "2026-08-03",
"protocolPrice": 100.00,
"settlementPrice": 88.00,
"ticketUnitPrice": 88.00,
"priceConfigured": true,
"specName": "成人票"
},
{
"resourceType": "ACTIVITY",
"resourceId": "2079454953641836550",
"resourceName": "骑马体验",
"status": 0,
"statusName": "已下架",
"city": null,
"settleType": "cash",
"dayDate": "2026-08-03",
"protocolPrice": 66.00,
"settlementPrice": null,
"ticketUnitPrice": 66.00,
"priceConfigured": true,
"specName": "成人票"
}
],
"total": 2,
"page": 1,
"pageSize": 20
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
### 8.2 边界情况:当天无价格
**请求**
```http
GET /admin/resource-options/ticket-items?dayDate=2026-12-31&resourceType=SCENIC&status=1&page=1&pageSize=20
Authorization: Bearer <admin-token>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"resourceType": "SCENIC",
"resourceId": "2079454953641836548",
"resourceName": "免费公园",
"status": 1,
"statusName": "已启用",
"city": "拉萨市",
"settleType": null,
"dayDate": "2026-12-31",
"protocolPrice": null,
"settlementPrice": null,
"ticketUnitPrice": null,
"priceConfigured": false,
"specName": "成人票"
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"traceId": "b2c3d4e5-f6a7-8901",
"success": true
}
```
### 8.3 业务失败:缺少游玩日期
**请求**
```http
GET /admin/resource-options/ticket-items?page=1&pageSize=20
Authorization: Bearer <admin-token>
```
无请求体。
**响应**
```json
{
"code": 400,
"message": "游玩日期不能为空",
"data": null,
"traceId": "c3d4e5f6-a7b8-9012",
"success": false
}
```
## 9. 业务边界
- 默认统一查询 `SCENIC``ACTIVITY``total` 是两类资源合并后的总数,不是分别分页后相加。
- 默认同时包含已启用和已下架资源;结果按“已启用优先 → 名称升序 → 资源类型升序 → 资源 ID 升序”稳定排序。
- 软删除资源不返回;资源名称为 `null`、空字符串或纯空白时也不返回,且不会计入 `total`
- `keyword` 只匹配资源名称;不会匹配城市或资源 ID。
- 当天没有价格不会排除资源;此时允许人工核单,价格字段按第 6.4 节返回。
- 页码超过最后一页时,返回 `code=200``records=[]``total` 仍为符合条件的总数。
- `city``settleType` 是可空字段,不能作为能否选择资源的判断条件。
## 验证证据
- PR #5528 与补丁 PR #5534 已合并到 `dev-v3`
- `hl-resource-service` 已部署测试服,经 Gateway 真实 HTTP 验收;补丁后全页扫描 121 条资源,空名称记录为 0,`resourceId` 按 String 返回。
## 10. 影响评估
- **是否破坏向后兼容**:否;这是新增只读接口,不改变已有接口。
- **前端是否必须同步上线**:否;后端上线后前端可按需接入。
- **ID 类型要求**`resourceId` 必须始终按 String 保存和传递。
## 11. 注意事项
- 选择行的核算单价使用 `ticketUnitPrice`,不要在前端重新实现结算价/协议价优先级。
- `priceConfigured=false` 不代表资源不可选,只表示该游玩日期没有可自动带出的价格。
- 不要根据 `city` 是否为空过滤资源。
- 不要在前端把两个资源类型拆成两次请求再自行合并分页;本接口已经提供统一分页和 `total`
## 12. 关联 / 联系人
### 12.1 链接
- **Issue**: [#5429](https://git.1814.love:8443/wx/HL/issues/5429)
- **功能 PR**: [#5528](https://git.1814.love:8443/wx/HL/pulls/5528)
- **功能 Merge commit**: [5ab2fb780](https://git.1814.love:8443/wx/HL/commit/5ab2fb7802772f99af9ee2715bb921e502a6f07b)
- **补丁 PR**: [#5534](https://git.1814.love:8443/wx/HL/pulls/5534)
- **补丁 Merge commit**: [87e8a96ba](https://git.1814.love:8443/wx/HL/commit/87e8a96ba16cbc505092562c7fae0f9e88984a5a)
### 12.2 联系人
- **后端负责人**: @yst
## 关联/联系人
### 链接
- [后端工单 #5429](https://git.1814.love:8443/wx/HL/issues/5429)
### 联系人
- **后端负责人**: @yst

查看文件

@ -1,83 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5515"
title: "车务保险任务列表枚举校验补全 + source 来源筛选生效"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@1553c988ac6065e693769a95f3f412c4e43df5e7"
target_release: ""
verified_at: ""
status_note: "后端完成PR #5522 已合并 dev-v3 并部署 TEST;非法枚举/分页返 400 明确错误码,source 筛选生效BAOYOU=206。前端若传 source 参数无需改动;旧传非法枚举值需适配 400 响应。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 车务: 保险任务列表枚举校验补全 + source 来源筛选生效
> **服务**: hl-fleet-service
> **PR**: #5522
> **Issue**: #5515#5516 分页校验同 PR
> **日期**: 2026-08-05
> **影响范围**: 管理后台车务保险菜单的任务列表筛选参数
---
## ⚠️ 关键变化
`GET /admin/fleet/insurance/tasks` 三个筛选参数行为变更:
1. **`taskType` 非法值不再静默空结果**`taskType=NOT_A_TYPE` 此前 `code=200` + `total=0`;现在返 `400`,message=`任务类型必须是 PURCHASE(投保)/REFUND(退保) 之一`
2. **`taskStatus` 非法值不再静默空结果**`taskStatus=BAD_STATUS` 此前 `code=200` + `total=0`;现在返 `400`,message=`任务状态必须是 PENDING/PROCESSING/SUCCESS/RESOLVED/IGNORED 之一`
3. **`source` 参数新增并生效**:此前 `source=BAOYOU` 被 Spring 静默忽略(返全量 207;现在按来源过滤BAOYOU=206 / OFFLINE=1,非法值返 `400``来源必须是 BAOYOU(保游)/OFFLINE(线下) 之一`)。
同源收紧(同 PR`GET /admin/fleet/matrix/grid``GET /admin/fleet/matrix/month-counts``season` 非法值返 `400``司机赛季必须是 active/pending/archived/blacklist 之一`);`GET /admin/fleet/board/expiry``GET /admin/fleet/board/orders` 分页参数 page/pageSize 非法(非数字/负数/0/超 100`400`页码最小为1 等),与 drivers/vehicles 列表口径一致。
## 变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保险任务列表 | GET | `/admin/fleet/insurance/tasks` | 校验补全+新增参数 | taskType/taskStatus 非法值 400;新增 source 筛选 |
| 2 | 矩阵主数据 | GET | `/admin/fleet/matrix/grid` | 校验补全 | season 非法值 400 |
| 3 | 矩阵月度统计 | GET | `/admin/fleet/matrix/month-counts` | 校验补全 | season 非法值 400 |
| 4 | 证件到期看板 | GET | `/admin/fleet/board/expiry` | 校验补全 | page/pageSize 非法值 400 |
| 5 | 看板列表 | GET | `/admin/fleet/board/orders` | 校验补全 | page/pageSize 非法值 400 |
## 接口详情
### 1. 保险任务列表 `GET /admin/fleet/insurance/tasks`
**`source` 参数(新增)**
- 类型string;枚举`BAOYOU`(保游·线上)/ `OFFLINE`(线下);空/缺省=全部
- 生效:与 taskType/taskStatus 等 AND 过滤下推 DB`source = ?` 等值条件)
**校验规则**taskType/taskStatus/source 均 `@Pattern`
- 非法值 → `code=400`,message 包含合法枚举(不再静默空结果)
- 合法值行为不变taskType=PURCHASE→28、taskStatus=PENDING→25 等回归不变)
**示例**
```
GET /admin/fleet/insurance/tasks?source=BAOYOU&taskType=PURCHASE
→ 200,仅返回 source=BAOYOU 的投保任务
GET /admin/fleet/insurance/tasks?taskType=NOT_A_TYPE
→ 400 任务类型必须是 PURCHASE(投保)/REFUND(退保) 之一
```
## 关联/联系人
### 链接
- [后端工单 #5515](https://git.1814.love:8443/wx/HL/issues/5515)
- [后端 PR #5522](https://git.1814.love:8443/wx/HL/pulls/5522)
- Merge commit: `f061e02d44`
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,49 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5530"
title: "前端样式与派单候选显示问题(用户实测反馈)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "not_required"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@82255a9b0bd1e0e1560da161a80ec12325688655"
target_release: ""
verified_at: ""
status_note: "用户wx测试环境实测反馈的两个前端显示问题,交接前端处理。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 前端样式与派单候选显示问题(用户实测反馈)
> **服务**: 前端(管理后台) | **日期**: 2026-08-05 | **来源**: wx 测试环境实测
本文件交接**两个前端显示问题**用户手动测试实测发现,请前端mmg处理。后端 API 契约未变,以下为前端展示层问题。
## 问题 1订单详情-行程/用车需求区域样式暗色显示异常
- **页面**:订单详情 → 行程/用车需求区域web.test.1814.love
- **现象**:该区域样式显示为**暗色**(暗色主题/暗色显示异常),与正常亮色主题不符,影响可读性。
- **期望**:恢复正常亮色/正确主题样式。
- **排查方向**:该页面主题/CSS 渲染(是否误用暗色主题或样式异常)。
## 问题 2派单"统一选择车辆/司机"候选显示两个问题
- **页面**:车务派单 → 统一选择车辆/司机弹窗fleet/board
- **问题 2a全程空闲筛选**:全程选择司机/车辆时,筛选结果应只显示**全程(如 08-28~08-31 四天)都空闲**的司机/车辆。当前显示不对——包含了非全程空闲(部分天占用)的候选。
- **问题 2b日期范围显示**:候选的服务日期当前显示为**单日**2026-08-28,用户需要显示**日期范围**2026-08-28 至 2026-08-31,全程
- **期望**
- 全程选择时只返回/显示全程空闲的司机/车辆(可用性按全程判断)
- 服务日期显示为范围startDate 至 endDate,非单日
- **后端说明**`POST /admin/fleet/assignments/candidates` 已支持 startDate/endDate 范围入参与档期冲突返回,前端可用此判断全程可用性并展示范围;若需后端补"全程空闲"过滤字段或范围返回,请在工单反馈,后端配合。
## 关联 / 联系人
### 联系人
- **反馈人**: @wx
- **后端对接**: @wx(如需后端配合 candidates 字段/过滤)
- **前端处理**: @mmg

查看文件

@ -1,50 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5530"
title: "投保界面去掉"按赛季"快捷预填选项(与按年重复)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "not_required"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@aa7061b0edeaf68a8baf306a858a91d8be8cb9a7"
target_release: ""
verified_at: ""
status_note: "wx 确认无"按赛季买"需求,投保界面"按赛季"与"按年"快捷预填重复,去掉"按赛季"保留"按年"。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 投保界面去掉"按赛季"快捷预填选项(与按年重复)
> **服务**: 前端(司机投保-保游网界面) | **日期**: 2026-08-05 | **来源**: wx 确认
## 背景wx 确认)
司机投保界面(保游网)的保障起止**快捷预填**有两个选项:"**按赛季**"7-01~次年6-30和"**按年**"(自然年)。
wx 确认:**没有"按赛季买"保险的需求**——"按赛季"(旅游经营年度 7-01~次年6-30与"按年"(年度覆盖)语义重复,造成选择困扰。
## 处理(前端)
**去掉"按赛季"快捷预填选项**,保留"按年"(及手动改起止)。
- 位置:`hl-ui/src/views/fleet/drivers/index.vue`(投保保障起止快捷预填按钮)
- 去掉"按赛季"按钮/选项;保留"按年"快捷预填 + 手动修改起止
- 后端无契约变更(投保入参不变,仅前端去掉冗余预填选项)
## 说明
- "按赛季"7-01~次年6-30本质是旅游经营年度的年保,与"按年"annual重复
- 保险类型实际只有 annual年保/ perTrip按行程,无独立"按赛季"类型
- 后端 Controller 注释提到"按赛季/按年快捷预填"仅为说明,不影响功能(如需同步清理注释可后续提)
## 关联 / 联系人
### 联系人
- **需求确认**: @wx
- **前端处理**: @mmg
- **后端对接**: @wx(无需改动,仅前端)

查看文件

@ -1,103 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5533"
title: "司机列表新增车型大类/所属车队展示与筛选,详情关联订单补齐接送机/金额/定制师"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@005ba4cf9151e39aac5bcf4c5e5480411d68264e"
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-08-05"
base: "dev-v3"
---
# 车务司机管理:列表车型大类/所属车队展示与筛选 + 详情关联订单字段补齐
> **服务**: hl-fleet-service
> **PR**: #5538
> **Issue**: #5533
> **日期**: 2026-08-05
> **影响范围**: 管理后台司机列表 / 司机详情关联订单区域
## 变更接口
| 接口 | 方法 | 路径 | 变更 |
|---|---|---|---|
| 司机档案分页查询 | GET | `/admin/fleet/drivers/page` | 列表项新增车型大类/所属车队字段;新增 `vehicleTypeKeys``teamIds` 筛选参数 |
| 司机档案详情查询 | GET | `/admin/fleet/drivers/司机ID` | `relatedOrders[]` 新增接送机地点、日费/总费、定制师字段 |
## 二、响应字段
### 2.1 司机列表 `records[]`(顶层新增)
| 字段 | 类型 | 必填性 | 值 | 说明 |
|---|---|---|---|---|
| `residentVehicleTypeKey` | `String` / null | 否 | `suv2`/`mpv`/`sedan`/`bus` 等 | 权威常驻车辆第一辆的车型大类 key与派单候选弹窗分类一致;无常驻车辆为 null |
| `residentVehicleTypeName` | `String` / null | 否 | `SUV系列`/`商务车`/`轿车系列`/`大巴系列` | 车型大类中文名;无常驻车辆为 null |
| `residentTeamId` | `String` / null | 否 | 车队雪花 IDJSON 字符串) | 权威常驻车辆第一辆的所属车队 ID;无常驻车辆为 null |
| `residentTeamName` | `String` / null | 否 | `合作车队B` | 所属车队名;无常驻车辆为 null |
`residentVehicles[]` 子项同步新增:`vehicleTypeKey``vehicleTypeName``fleetTeamId``fleetTeamName`
### 2.2 司机列表新增筛选参数
| 参数 | 类型 | 必填性 | 说明 |
|---|---|---|---|
| `vehicleTypeKeys` | `List<String>` | 否 | 车型大类 key 多选过滤(逗号分隔),常驻车辆挂在任一大类下即命中;不传不过滤 |
| `teamIds` | `List<Long>` | 否 | 所属车队 ID 多选过滤,常驻车辆属于任一支车队即命中;不传不过滤 |
### 2.3 司机详情 `relatedOrders[]`(新增)
| 字段 | 类型 | 必填性 | 值 | 说明 |
|---|---|---|---|---|
| `pickupAt` | `String` / null | 否 | `满洲里` | 接机地点fleet_assignment.pickup_at 快照);无记录为 null |
| `dropoffAt` | `String` / null | 否 | `满洲里` | 送机地点fleet_assignment.dropoff_at 快照);无记录为 null |
| `dailyFee` | `String` / null | 否 | `800.00` | 协议日费protocol_price 快照,JSON 字符串);无费用为 null |
| `totalFee` | `String` / null | 否 | `800.00` | 派车组总费vehicle_fee_total 快照,JSON 字符串);无费用为 null |
| `customizerName` | `String` / null | 否 | `王骁` | 定制师名planner_name 快照) |
原有字段orderId/orderNo/teamNo/customerName/tripStartDate/tripEndDate/destination/orderStatus/vehicleType/licensePlate不变;`destination` 保留为 pickup/dropoff 兼容拼接。
## 三、兼容性与前端事项
- 全部为向后兼容的响应字段扩展 + 可选筛选参数,不改既有字段语义、分页结构。
- 车型大类 key 以 vehicle_type 表 type_key 为准(与派单候选弹窗分类一致,如 `suv2`),前端筛选值应复用候选弹窗/字典数据源。
- 后端未修改 `mmg/hl-ui`,前端消费状态保持 `pending`
## 验证证据
- 定向测试 158 项全绿DriverServiceTest 144 / VehicleResidentReadPortImplTest 7 / DriverMapperPageExcludeTest 3 / DriverRelatedOrderReadPortImplTest 4;fleet verify 3130 项仅 1 既有基线 flaky 失败Step2CanonicalSnapshotServiceTest 时间敏感,stash 验证与本次无关,0 Errors;spotless 0 违规。
- 测试环境网关实测:
- 列表朝鲁门suv2/SUV系列/北疆协作车队、菜单师傅46622mpv/商务车/自有、王信sedan/轿车系列/合作车队A;无常驻车辆司机全字段 null
- 筛选:`vehicleTypeKeys=suv2` 3 人、`suv2,mpv` 多选 8 人、`teamIds=合作车队B` 1 人、组合筛选 1 人、不存在大类空结果正确
- 详情:乌云毕力格 relatedOrders 2 条——pickup/dropoff=满洲里、teamNo=26-4081、status=in_progress、dailyFee=0.00/totalFee=800.00、customizer=王骁、customer=卫予轩
## 五、关联 / 联系人
### 5.1 链接
- **Issue**: [#5533](https://git.1814.love:8443/wx/HL/issues/5533)
- **PR**: [#5538](https://git.1814.love:8443/wx/HL/pulls/5538)
- **Merge commit**: [9215f86ee](https://git.1814.love:8443/wx/HL/commit/9215f86eeccd9e1a1bd17e90ea6d2c1784a4db54)
### 5.2 联系人
- **后端负责人**: @wx
## 关联/联系人
### 链接
- [后端工单 #5533](https://git.1814.love:8443/wx/HL/issues/5533)
- [后端 PR #5538](https://git.1814.love:8443/wx/HL/pulls/5538)
- Merge commit: `9215f86ee6`
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,67 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5536"
title: "保险订单 v3 补 /orders 列表别名(对齐 v2 与前端调用)"
consumer: "admin"
change_type: "新增接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "verified"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@ef497e8e30d2df2d55d97f5554c8c5e921dc49ef"
target_release: ""
verified_at: "2026-08-05"
status_note: "后端完成PR #5541 已合并 dev-v3 并部署 TEST;网关实测 /v3/admin/insurance/orders 200 total=255,与 /list 完全一致(修复前 404。前端保险订单页无需改动,回归确认后置 verified。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 保险订单 v3 补 /orders 列表别名(#5536
> **服务**: hl-order-service-v3
> **PR**: #5541
> **Issue**: #5536P1
> **联系人**: @wx
> **日期**: 2026-08-05
> **影响范围**: 管理后台-保险管理-保险订单页(前端已调 /orders 复数路径)
---
## 背景
前端保险订单页调 `GET /v3/admin/insurance/orders`**404 数据加载失败**。v3 只有 `/v3/admin/insurance/list`(实测 code=200 total=255,缺 `/orders` 复数别名;v2`/admin/insurance/orders`)与前端调用均为复数路径。
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| GET | /v3/admin/insurance/orders | 管理后台-保险订单页 |
## 变更说明
`AdminInsuranceController.listOrders` 路由映射由 `"/list"` 扩展为 `/list``/orders` 两个路径——**同一方法同返回**(分页参数 `InsuranceQueryRequest` 与 /list 完全一致),不新增逻辑。
## 接口行为变化
| 接口 | 修复前 | 修复后 |
|---|---|---|
| `GET /v3/admin/insurance/orders` | 404 | 200,`PageResult<InsuranceOrderVO>`total=255,与 /list 一致) |
| `GET /v3/admin/insurance/list` | 200 | 不变 |
## 验证证据
- 网关实测api.test.1814.love:9443,admin,2026-08-05`/v3/admin/insurance/orders` 200 total=255;`/v3/admin/insurance/list` 200 total=255;首条 `insuranceOrderId=2084823362831306753 / extPolicyNo=MOCKPOL-... / totalPremium="88.00"`,两路径响应一致
- 定向测试AdminInsuranceControllerTest 19/19新增 `/orders` 别名与 `/list` 同返回断言用例)
## 关联/联系人
### 链接
- [后端工单 #5536](https://git.1814.love:8443/wx/HL/issues/5536)
- [后端 PR #5541](https://git.1814.love:8443/wx/HL/pulls/5541)
- Merge commit: `2936d67f1b`
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,352 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5539"
title: "核单资源下拉可选范围与中文字段契约"
consumer: "admin"
author: "yst"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@0a8a72ed6a978a9f14187679188f4c8308e354cd"
target_release: ""
verified_at: ""
status_note: "hl-resource-service;PR #5540 已合并并部署测试服,Gateway 真实验收通过;等待管理后台完成破坏性契约迁移。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 【⚠️ 修改接口·管理后台】核单资源下拉可选范围与中文字段契约(#5539
> **PR**: [#5540](https://git.1814.love:8443/wx/HL/pulls/5540)
> **服务**: hl-resource-service
> **更新时间**: 2026-08-05
## 1. 接口背景
核单门票/游玩统一资源下拉首版会返回下架资源,且资源类型、结算方式只有编码,默认核算价字段名也不够明确。本次收紧为只返回启用资源,并调整请求与响应字段。删除字段和字段改名属于破坏性契约变化,接入方需要按第 12 节迁移。
## 变更接口
| # | 接口名 | 方法 | 路径 | 变更类型 | 说明 |
|---|--------|------|------|----------|------|
| 1 | 分页查询门票/游玩资源选项 | GET | `/admin/resource-options/ticket-items` | 修改接口 | 仅返回启用资源,删除状态字段,增加中文名称字段并重命名默认核算价 |
## 3. 接口详情
- **使用场景**:核单 Step 2 新增或搜索可选的门票/游玩项目。
- **认证**:需要有效的管理后台 JWT,使用 `Authorization: Bearer <token>`
- **幂等性**:幂等,只读查询。
- **限流**:无接口专属限流约定。
- **响应类型**`Result<PageResult<TicketResourceOptionRespVO>>`
- **请求体**:无。
## 4. 接口入参
### 4.1 Query 参数(修改后完整字段)
| 字段 | 类型 | 必填 | 默认值 | 校验规则 | 说明 |
|------|------|------|--------|----------|------|
| `dayDate` | String | 是 | 无 | ISO 日期 `yyyy-MM-dd` | 实际游玩日期;价格字段只匹配这一天的资源价格 |
| `keyword` | String | 否 | `null` | 最长 100 个字符 | 资源名称模糊搜索;自动去除首尾空格;去除后为空等同未传 |
| `resourceType` | String | 否 | `null` | `SCENIC``ACTIVITY` | 不传时同时查询景区和游玩项目 |
| `page` | Integer | 否 | `1` | 最小值 1 | 当前页码 |
| `pageSize` | Integer | 否 | `20` | 1100 | 每页条数 |
### 4.2 已删除的 Query 参数
| 字段 | 修改前 | 修改后 |
|------|--------|--------|
| `status` | 可选,`0` 已下架、`1` 已启用;不传查询全部 | **已删除**;接口固定只返回启用资源 |
### 4.3 请求体字段
无请求体。
## 5. 出参字段
### 5.1 统一响应与分页字段
| 字段 | 类型 | 可空 | 说明 |
|------|------|------|------|
| `code` | Integer | 否 | 业务状态码;成功为 `200` |
| `message` | String | 否 | 响应消息;成功为 `成功` |
| `success` | Boolean | 否 | `code == 200` 时为 `true` |
| `traceId` | String | 是 | 链路追踪 ID |
| `data` | Object | 否 | 分页结果 |
| `data.records` | Array | 否 | 本页启用资源列表;无匹配资源时为 `[]` |
| `data.total` | Integer | 否 | 符合条件的启用资源总数 |
| `data.page` | Integer | 否 | 当前页码 |
| `data.pageSize` | Integer | 否 | 每页条数 |
### 5.2 `data.records[]` 资源字段(修改后完整 13 项)
| 字段 | 类型 | 可空 | 说明 |
|------|------|------|------|
| `resourceType` | String | 否 | 资源类型编码:`SCENIC``ACTIVITY` |
| `resourceTypeName` | String | 否 | 资源类型中文名:`景区``游玩项目` |
| `resourceId` | String | 否 | 资源 ID;固定按 JSON String 返回,不要转换为 JavaScript `Number` |
| `resourceName` | String | 否 | 资源名称;名称为 `null`、空字符串或纯空白的资源不会返回 |
| `city` | String | 是 | 城市;景区优先返回城市名称;原值为 `null`、空字符串或纯空白时统一返回 `null` |
| `settleType` | String | 是 | 结算方式编码;空值/空白统一为 `null`;未知非空编码去除首尾空格后原值返回 |
| `settleTypeName` | String | 是 | 结算方式中文名;`cash/sign/company` 分别为现付/签单/公司付款;编码为空或未知时为 `null` |
| `dayDate` | String | 否 | 本次查询的实际游玩日期,格式 `yyyy-MM-dd` |
| `protocolPrice` | Decimal | 是 | 该日期的协议价;未配置时为 `null` |
| `settlementPrice` | Decimal | 是 | 该日期的结算价;未配置时为 `null` |
| `defaultUnitPrice` | Decimal | 是 | 默认核算单价:优先取结算价,结算价为空时回退协议价;两者都为空时为 `null` |
| `priceConfigured` | Boolean | 否 | `defaultUnitPrice` 非空时为 `true`,否则为 `false` |
| `specName` | String | 否 | 固定返回 `成人票` |
### 5.3 已删除或改名的响应字段
| 修改前字段 | 修改后 | 迁移说明 |
|------------|--------|----------|
| `status` | **已删除** | 接口已固定过滤为启用资源,不再返回状态值 |
| `statusName` | **已删除** | 接口已固定过滤为启用资源,不再返回状态文案 |
| `ticketUnitPrice` | 改名为 `defaultUnitPrice` | 价格优先级不变:结算价优先、协议价回退 |
## 6. 枚举 / 数据字典
### 6.1 `resourceType` / `resourceTypeName`
| `resourceType` | `resourceTypeName` | 说明 |
|----------------|--------------------|------|
| `SCENIC` | `景区` | 景区资源 |
| `ACTIVITY` | `游玩项目` | 活动/游玩项目资源 |
### 6.2 `settleType` / `settleTypeName`
| `settleType` | `settleTypeName` | 说明 |
|--------------|------------------|------|
| `cash` | `现付` | 资源结算方式为现付 |
| `sign` | `签单` | 资源结算方式为签单 |
| `company` | `公司付款` | 资源结算方式为公司付款 |
| `null` | `null` | 原编码为空或空白 |
| 其他非空编码 | `null` | 编码去除首尾空格后原值返回,中文名为空 |
### 6.3 价格字段关系
| 条件 | `defaultUnitPrice` | `priceConfigured` |
|------|--------------------|-------------------|
| `settlementPrice` 非空 | 取 `settlementPrice` | `true` |
| `settlementPrice` 为空、`protocolPrice` 非空 | 取 `protocolPrice` | `true` |
| 两个价格都为空 | `null` | `false` |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询完成;没有匹配资源时仍为成功,`records=[]` |
| `400` | 参数校验失败 | 缺少/无法解析 `dayDate``keyword` 超过 100 字符,`resourceType` 非法,或分页参数越界 |
| `401` | 未认证或认证失效 | 未携带有效的管理后台 JWT |
| `500` | 系统异常 | 查询过程发生未预期异常 |
## 8. 示例
### 8.1 典型成功:启用景区与游玩项目
**请求**
```http
GET /admin/resource-options/ticket-items?dayDate=2026-08-03&keyword=体验&page=1&pageSize=20
Authorization: Bearer <admin-token>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"resourceType": "SCENIC",
"resourceTypeName": "景区",
"resourceId": "2079454953641836546",
"resourceName": "草原景区体验区",
"city": "呼伦贝尔市",
"settleType": "sign",
"settleTypeName": "签单",
"dayDate": "2026-08-03",
"protocolPrice": 100.00,
"settlementPrice": 88.00,
"defaultUnitPrice": 88.00,
"priceConfigured": true,
"specName": "成人票"
},
{
"resourceType": "ACTIVITY",
"resourceTypeName": "游玩项目",
"resourceId": "2079454953641836550",
"resourceName": "骑马体验",
"city": "海拉尔区",
"settleType": "cash",
"settleTypeName": "现付",
"dayDate": "2026-08-03",
"protocolPrice": 66.00,
"settlementPrice": null,
"defaultUnitPrice": 66.00,
"priceConfigured": true,
"specName": "成人票"
}
],
"total": 2,
"page": 1,
"pageSize": 20
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
### 8.2 边界情况:空白城市、未知结算编码且当天无价格
**请求**
```http
GET /admin/resource-options/ticket-items?dayDate=2026-12-31&resourceType=ACTIVITY&page=1&pageSize=20
Authorization: Bearer <admin-token>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"resourceType": "ACTIVITY",
"resourceTypeName": "游玩项目",
"resourceId": "2079454953641836558",
"resourceName": "特色体验",
"city": null,
"settleType": "other",
"settleTypeName": null,
"dayDate": "2026-12-31",
"protocolPrice": null,
"settlementPrice": null,
"defaultUnitPrice": null,
"priceConfigured": false,
"specName": "成人票"
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"traceId": "b2c3d4e5-f6a7-8901",
"success": true
}
```
### 8.3 业务失败:缺少游玩日期
**请求**
```http
GET /admin/resource-options/ticket-items?page=1&pageSize=20
Authorization: Bearer <admin-token>
```
无请求体。
**响应**
```json
{
"code": 400,
"message": "游玩日期不能为空",
"data": null,
"traceId": "c3d4e5f6-a7b8-9012",
"success": false
}
```
## 9. 业务边界
- 无论是否携带旧版 `status` 参数,接口都固定只返回已启用资源;调用方不应继续传该参数。
- 下架资源、软删除资源、名称为 `null`/空字符串/纯空白的资源均不返回,也不计入 `total`
- 默认统一查询 `SCENIC``ACTIVITY`;结果按“名称升序 → 资源类型升序 → 资源 ID 升序”稳定排序。
- `keyword` 只匹配资源名称;不会匹配城市或资源 ID。
- 当天没有价格不会排除资源,`defaultUnitPrice=null``priceConfigured=false`
- `city``settleType` 为空不影响资源是否返回;空白值会被规范为 `null`
- 未知非空结算编码保留在 `settleType`,但 `settleTypeName=null`
- 页码超过最后一页时,返回 `code=200``records=[]``total` 仍为符合条件的启用资源总数。
## 验证证据
- PR #5540 已合并到 `dev-v3``hl-resource-service` 已部署测试服。
- Gateway 真实 HTTP 验收确认仅启用过滤、新旧字段、String ID、结算方式映射、价格优先级、双价缺失、筛选与鉴权均按本文契约生效。
## 10. 修改前后对比
### 10.1 字段级对比
| 位置 | 修改前 | 修改后 |
|------|--------|--------|
| Query | `dayDate/keyword/resourceType/status/page/pageSize` | `dayDate/keyword/resourceType/page/pageSize` |
| 资源状态 | `status/statusName` | 两个字段均删除 |
| 资源类型中文名 | 无 | 新增 `resourceTypeName` |
| 结算方式中文名 | 无 | 新增 `settleTypeName` |
| 默认核算价 | `ticketUnitPrice` | 改名为 `defaultUnitPrice` |
| 城市空白值 | 可能返回空字符串/纯空白 | 统一返回 `null` |
| 结算编码空白值 | 可能返回空字符串/纯空白 | 统一返回 `null` |
### 10.2 行为级对比
| 行为 | 修改前 | 修改后 |
|------|--------|--------|
| 可选资源范围 | 默认包含启用和下架,可通过 `status` 筛选 | 固定只返回启用资源 |
| 默认核算价优先级 | 结算价优先,协议价回退 | 不变,仅字段改名 |
| 未知结算编码 | 只有原始编码 | 保留原始编码,中文名为 `null` |
| 排序 | 启用优先,再按名称/类型/ID | 全部为启用资源,按名称/类型/ID |
## 11. 影响评估 / 回滚
- **是否破坏向后兼容**:是;请求删除 `status`,响应删除和改名字段。
- **前端是否必须同步上线**:已接入首版接口的前端必须同步迁移;尚未接入的前端直接按新契约实现。
- **回滚影响**:若后端回滚到首版,`resourceTypeName/settleTypeName/defaultUnitPrice` 将消失,旧字段和下架资源会重新出现;前后端需保持同一契约版本。
## 12. 前端迁移清单
- [ ] 删除请求中的 `status` 参数和相关状态筛选逻辑。
- [ ] 删除对响应 `status``statusName` 的读取、展示与过滤。
- [ ] 将所有 `ticketUnitPrice` 读取改为 `defaultUnitPrice`
- [ ] 展示资源类型时读取 `resourceTypeName`;编码判断仍使用 `resourceType`
- [ ] 展示结算方式时读取 `settleTypeName`,并允许该字段为 `null`
- [ ] 允许 `city``settleType``settleTypeName`、三个价格字段为 `null`
- [ ] 不再在前端筛除下架资源;接口结果已经只包含启用资源。
- [ ] `resourceId` 继续按 String 保存和传递。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5539](https://git.1814.love:8443/wx/HL/issues/5539)
- **PR**: [#5540](https://git.1814.love:8443/wx/HL/pulls/5540)
- **Merge commit**: [7edbb6026](https://git.1814.love:8443/wx/HL/commit/7edbb6026d7f0d3551ed8c43ca57ea1698ebfeff)
- **前置 Issue**: [#5429](https://git.1814.love:8443/wx/HL/issues/5429)
### 13.2 联系人
- **后端负责人**: @yst
## 关联/联系人
### 链接
- [后端工单 #5539](https://git.1814.love:8443/wx/HL/issues/5539)
- [后端 PR #5540](https://git.1814.love:8443/wx/HL/pulls/5540)
- Merge commit: `7edbb6026d`
### 联系人
- **后端负责人**: @yst

查看文件

@ -1,83 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5549"
title: "司机保险切换到可售产品 + 不可售产品友好报错"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@085f4d7b58c3d61414df8d0461ab51e9eb61ea47"
target_release: "v2.1"
verified_at: "2026-08-05"
status_note: "管理后台现有投保弹窗已完全消费 driver-plan-options 动态计划,540034 由全局拦截器展示后端友好文案且页面不重复 toast;补充回归测试锁定计划 ID 字符串透传、动态选项和错误分支。后端完成PR #5553/#5556/#5557 合并 dev-v3 并部署 TEST;网关实测畅心游真实投保成功保单 11209006600507256500、不可售产品 540034 友好报错、plan-options 过滤真实生效。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 司机保险切换到可售产品 + 不可售产品友好报错(#5549
> **服务**: hl-order-service-v3
> **PR**: #5553 / #5556 / #5557
> **Issue**: #5549P1
> **联系人**: @wx
> **日期**: 2026-08-05
> **影响范围**: 管理后台-司机档案-保险投保;fleet 司机保险页Feign 消费 driver-purchase / driver-plan-options
---
## 背景
畅心游产品费率码旧值BY919212138521失效导致投保必 404#5545 排查);保游 GetProductList 下架产品本地不更新、司机端仍可选。需求:司机保险切到可售产品(未来星等)、不可售产品报错友好。
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| POST | /v3/internal/insurance/driver-purchase | fleet 司机保险投保Feign |
| GET | /v3/internal/insurance/driver-plan-options | fleet 司机保险计划下拉Feign |
## 变更说明
1. **同步链路**(保险产品同步 /v3/admin/insurance/sync-products以保游 GetProductList 为权威可售列表,列表外的存量启用产品标记 INACTIVE下架自动失效
2. **投保链路** driver-purchase
- 本地拦截:产品 INACTIVE下架标记→ 新错误码 **540034**
- 保游 404费率码失效→ 翻译 **540034**
- 保游业务拒绝501 URL 地址受限 / 地址受限 / 下架 / 失效)→ 翻译 **540034**(替代 540030 裸透传)
3. **plan-options**:过滤 INACTIVE 产品与孤儿计划的计划;司机端不再看到下架产品
## 接口行为变化
| 接口 | 修复前 | 修复后 |
|---|---|---|
| `POST driver-purchase`(不可售产品) | 540030「为投保人投保失败: [501]url地址受限」/ 裸 404 | **540034「该产品已下架/不可售,请更换可售产品」** |
| `POST driver-purchase`(可售产品) | 不变 | 不变(畅心游真实投保成功,保单号非 MOCK |
| `GET driver-plan-options` | 仅畅心游 20万计划 | 畅心游 20万 + 未来星 20/30/50万未来星产品计划 usage_category 已标注 BOTH;INACTIVE 产品被过滤 |
## 新增错误码
| 错误码 | 消息 |
|---|---|
| 540034 | 该产品已下架/不可售,请更换可售产品 |
## 验证证据
- 网关实测2026-08-05,内部接口 SSH 直连 8086
- 畅心游投保 → 200,extOrderNo=BX2026080517395620000670,回调后 INSURED + 真实保单号 **11209006600507256500**(非 MOCK
- 未来星投保 → **540034**「该产品已下架/不可售,请更换可售产品」(保游 501 URL 受限翻译)
- plan-options → 4 计划(畅心游+未来星 20/30/50万;临时标 INACTIVE 后过滤只剩畅心游,恢复后 4 计划
- 定向测试InsuranceCreateServiceTest/InsuranceManageServiceTest/InsuranceQueryServiceTest 111/111新增 5 用例540034 本地拦截/404 翻译/501 翻译/plan-options 过滤/同步标 INACTIVE
## 关联/联系人
### 链接
- [后端工单 #5549](https://git.1814.love:8443/wx/HL/issues/5549)
- [后端 PR #5557](https://git.1814.love:8443/wx/HL/pulls/5557)
- Merge commit: `269b565cd9`
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,64 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5552"
title: "派车板详情日历价列快照缺失时按车型实时兑底回显(不再显示 ¥0"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "N/A前端字段已存在,仅后端取值变化,无需前端改动"
target_release: ""
verified_at: ""
status_note: ""
updated_at: "2026-08-05"
base: "dev-v3"
---
# 派车板订单详情:日历价列兑底回显
> **服务**: hl-fleet-service
> **PR**: #5554#5555
> **Issue**: #5552
> **日期**: 2026-08-05
> **影响范围**: 管理后台派车板订单详情(每日派车计划 + 当前派车逐日费用)
## 变更接口
| 接口 | 方法 | 路径 | 变更 |
|---|---|---|---|
| 派车板订单详情 | GET | `/admin/fleet/board/orders/订单ID` | `dailyVehiclePlan[].calendarPrice``currentAssignment.dailyVehicleFees[].calendarPrice` 在快照缺失(历史派车时日历未设价,`vehicle_fee_calendar_price` 为 NULL时,按车辆车型vehicleModelId,价格日历取价键+ 服务日实时取日历价兑底回显 |
### 响应字段语义变更
| 字段 | 类型 | 旧语义 | 新语义 |
|---|---|---|---|
| `dailyVehiclePlan[].calendarPrice` | `BigDecimal` / null | 已派行快照缺失时为 null前端显示 ¥0 | 快照缺失时兑底为当前日历价(如 800.00);快照有值保持冻结价 |
| `dailyVehicleFees[].calendarPrice` | `BigDecimal` / null | 同上 | 同上,`calendarPriceMissing` 同步变 false |
| `dailyVehiclePlan[].assignmentPrice` | `BigDecimal` | 协议价快照 | **不变**(快照保持,不随兑底篡改) |
| `dailyVehiclePlan[].priceSource` | `String` | CALENDAR/OVERRIDE/MISSING 判定 | **不变**(按兑底后日历价与协议价比较判定) |
取价键说明:`fleet_pricing_calendar``vehicle_model_id`(车型)维护;兑底链路为 `vehicle_id → vehicle_model_id → 日历价`,绝不按 vehicleId 查日历。
## 验证证据
- 网关实测 `GET /admin/fleet/board/orders/2080599963561005058`
- 蒙A-H77772026-08-01~03`calendarPrice=800.00`(原 NULL`assignmentPrice=600/610/620`(快照保持)、`priceSource=OVERRIDE`
- 蒙A-E5555`calendarPrice=2500.00`(原 NULL
- `currentAssignment.dailyVehicleFees`2026-08-04`calendarPrice=800.00``calendarPriceMissing=false`(原 NULL/true
- 新派车行蒙C05E05 08-20~25`calendarPrice=1000.00` CALENDAR快照落库正确,不受影响
- 测试BoardOrderServiceTest 80 + VehicleServiceTest 113 全绿;spotless 0 违规
## 关联/联系人
### 链接
- [后端工单 #5552](https://git.1814.love:8443/wx/HL/issues/5552)
- [后端 PR #5555](https://git.1814.love:8443/wx/HL/pulls/5555)
- Merge commit: `da565dec88`
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,104 +0,0 @@
---
author: "wx(GIT)"
schema: "hl-changelog/v2"
ticket: "5558"
title: "司机保单新增 policyPdfUrl在线查看保单 PDF+ 投保 bindAnnual 缺省语义变更"
consumer: "admin"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@3e535fe3ad6a406529eb95d7787cefe11b9584cd"
target_release: "v2.1"
verified_at: "2026-08-06"
status_note: "司机详情与车务保险列表/详情仅在 policyPdfUrl 非空时显示查看保单并安全新窗口预览;空值隐藏且不再回退旧下载接口,投保沿用 bindAnnual 缺省绑定全年保险语义。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 司机险:保单行在线查看保单 PDF + 投保缺省绑定全年保险
> **服务**hl-fleet-service + hl-order-service-v3 + hl-common
> **PR**#5563
> **Issue**[wx/HL #5558](https://git.1814.love:8443/wx/HL/issues/5558)
> **日期**2026-08-05
> **影响范围**:管理后台司机详情「我的保单」区域、车务保险保单 Tab
## 变更接口
| 接口 | 方法 | 路径 | 变更 |
|---|---|---|---|
| 司机我的保单列表 | GET | `/admin/fleet/drivers/<driverId>/insurance/policies` | 每条记录新增 `policyPdfUrl` |
| 司机险保单列表(车务保险) | GET | `/admin/fleet/insurance/policies` | 每条记录新增 `policyPdfUrl` |
| 司机险保单详情 | GET | `/admin/fleet/insurance/policies/<insuranceOrderId>` | 响应新增 `policyPdfUrl` |
| 司机险投保 | POST | `/admin/fleet/drivers/<driverId>/insurance/purchase` | `bindAnnual` 缺省语义变更 |
## 二、响应字段
| 字段 | 类型 | 必填性 | 值 | 说明 |
|---|---|---|---|---|
| `policyPdfUrl` | `String` | 条件返 | 保游生成后上传 OSS 的 PDF 直链 | 保单行「查看保单」按钮数据源;空值=保单 PDF 未生成/无电子保单MOCK 单、MANUAL 年保单),前端应隐藏查看入口 |
响应示例(司机我的保单列表):
```json
{
"code": 200,
"data": [
{
"insuranceOrderId": "2084941753793552386",
"policyNo": "11209006600507254235",
"status": "INSURED",
"totalPremium": "7.00",
"policyPdfUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/insurance/policy/2026/08/05/policy_2084941753793552386.pdf"
}
],
"success": true
}
```
> 投保出参purchase 响应)也携带 `policyPdfUrl` 字段,但出单受理瞬间 PDF 尚未异步生成,恒为空。
## 三、入参语义变更bindAnnual 缺省)
| 入参 | 旧语义 | 新语义 |
|---|---|---|
| `bindAnnual` 缺省(不传) | false只出单不动档案 | **true直接购买默认全年保险**,出单成功后自动绑定司机档案insurance_type=annual + 保单号/保费/起止回填 + annualSource=baoyou |
| `bindAnnual: false` | 只出单不动档案 | 不变:只出单不动档案 |
| `bindAnnual: true` | 出单后绑定档案 | 不变 |
**背景**:司机详情页投保不传 `bindAnnual`,旧语义下保单落库但档案保险状态不同步(有保单显示但档案 `insurance_type=none`)。
## 四、兼容性与前端事项
- 三处响应均为**向后兼容的字段扩展**,不修改既有字段与分页结构。
- `bindAnnual` 缺省语义变更:前端若存在"只出单不动档案"的调用场景,需显式传 `false`;司机详情页/保险订单页投保路径无需改动(缺省即绑定档案)。
- 前端事项:
1. 司机详情「我的保单」列表与车务保险保单 Tab保单行增加「查看保单」按钮,`policyPdfUrl` 非空时可点击;浏览器内嵌预览(`window.open(policyPdfUrl)` 或 iframe,**不触发下载**。
2. `policyPdfUrl` 为空时隐藏查看入口。
- 后端未修改 `mmg/hl-ui`,前端消费状态保持 `pending`
## 验证证据
- `DriverInsuranceServiceTest`25 项通过(含 #5558 缺省绑定新语义用例)。
- order-v3 保险域全量229 项通过(三链 `policyPdfUrl` 断言)。
- fleet verify仅 2 个仓库基线失败(`Step2CanonicalSnapshotServiceTest``ReleaseEOccupancyMysql8033RecoveryTest`,dev-v3 干净工作区同失败,与本次改动无关)。
- 测试服部署hl-fleet-service + hl-order-service-v3 滚动部署成功20:13/20:14
- 网关验证:三个查询接口 `policyPdfUrl` 透传通过;真实保游单返回 OSS PDF 直链Content-Type: application/pdf,浏览器内嵌预览,MOCK 单返回 null。
- MOCKPOL 残留清理2026-08-05 21:40manifest `5558-70cdf37dc3be`hl-data-cleanup/v1,test批准执行,删除 mock 时段假保单全关联 4 行(`insurance_order`/`insured_person`/`insurance_status_log`/`fleet_insurance_task`),备份至 `manifests/5558-mock-policy-cleanup.backup.json`;网关复验司机46622 我的保单=空列表、车务保单列表=4 条真单PDF 全直链)、档案 `insurance_type=none` 未变。
- 投保端到端(缺省绑定档案)验证:保游测试环境 20:15 起 `POST /Insurance/Insuran` 真实投保请求 Read timed out第三方不可用,17:02~17:57 曾成功出单)。按 wx 指示**跳过保游端到端**2026-08-05 22:20,以 DriverInsuranceServiceTest 缺省绑定单测 + 契约链路代码审查 + 无保单场景网关验证为验收依据;预期投保成功 → 档案 `insurance_type=annual` + 四件套回填bindAnnual 缺省=true 已部署)。
- oasdiff`not_configured`,一期项目没有稳定 OAS3 导出与已配置工具,使用源码和 JSON 序列化测试回退。
- Spring Cloud Contract`not_configured`,使用生产者及消费者 reactor 测试回退。
## 关联/联系人
### 链接
- **后端工单**: [#5558](https://git.1814.love:8443/wx/HL/issues/5558)
- **后端 PR**: [#5563](https://git.1814.love:8443/wx/HL/pulls/5563)
- **Merge commit**: [`b0685cca8d`](https://git.1814.love:8443/wx/HL/commit/b0685cca8d)
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,77 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5559"
title: "派单候选接口新增司机自动代入建议suggestedDriverId/reason/message"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@c6b107b89320e3bc9103314cd8eb5ef32897f51f"
target_release: "v2.1"
verified_at: "2026-08-05"
status_note: "管理后台矩阵/看板共用派单弹窗已接入三类司机自动建议;建议仅使用本次真实可用候选并携车辆、司机二次校验,NONE 清空本轮旧司机并展示后端提示,多槽位排除、人工改选、跨常驻确认和请求竞态门禁保持。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 矩阵派单司机自动代入:候选接口返回代入建议
> **服务**: hl-fleet-service
> **PR**: #5560#5561
> **Issue**: #5559
> **日期**: 2026-08-05
> **影响范围**: 管理后台派单弹窗(矩阵派单/看板共用候选接口)
## 变更接口
| 接口 | 方法 | 路径 | 变更 |
|---|---|---|---|
| 派单候选资源查询 | POST | `/admin/fleet/assignments/candidates` | 响应新增 `suggestedDriverId``suggestedDriverReason``suggestedDriverMessage`(选车后司机自动代入建议) |
## 响应字段(顶层新增)
| 字段 | 类型 | 必填性 | 值 | 说明 |
|---|---|---|---|---|
| `suggestedDriverId` | `Long` / null | 否 | 司机雪花 ID | 已选车辆的自动代入司机;未选车或无空闲司机为 null |
| `suggestedDriverReason` | `String` | 否 | `RESIDENT_AVAILABLE` / `RESIDENT_BUSY_FALLBACK` / `FIRST_AVAILABLE` / `NONE` | 代入原因码 |
| `suggestedDriverMessage` | `String` | 否 | 中文文案 | 后端可直接展示的代入原因 |
### 代入规则
1. 已选车辆(`selectedVehicleId`)有**常驻司机且空闲** → 代入常驻司机(`RESIDENT_AVAILABLE`);常驻司机权威源为车辆候选列表 `primaryDriverId`(与前端展示同源),不依赖 `VehicleDO.primary_driver_id` 投影
2. 常驻司机**档期冲突/停用/休息** → 回退代入司机候选排序后第一个**纯空闲**司机(`RESIDENT_BUSY_FALLBACK`
3. **无常驻司机** → 代入排序后第一个空闲司机(`FIRST_AVAILABLE`;SMART 排序:可用性→评分→完成单量→年限)
4. **无任何空闲司机或未选车**`suggestedDriverId=null``NONE`
空闲判定与候选一致(无 blocking 档期冲突);同城衔接共享(`CITY_JUNCTION_SHAREABLE`)不自动代入(需人工决策)。
## 前端配合
派单弹窗**选车后**以 `suggestedDriverId` 填充司机选择:
- 现有逻辑只代入常驻司机(且常驻忙时不回退、无常驻不代入)→ 改用 `suggestedDriverReason` 三态:`RESIDENT_AVAILABLE`/`RESIDENT_BUSY_FALLBACK`/`FIRST_AVAILABLE` 均自动选中对应司机;`NONE` 保持手动选择并展示 `suggestedDriverMessage`
- `suggestedDriverId` 为 null 时清空司机选择(`NONE`
## 验证证据
- 网关实测TEST,2026-08-05
- 蒙A-K1999/S6666/T1557 常驻空闲 → `RESIDENT_AVAILABLE`(代入常驻司机,其 `available=true`
- 蒙C04E04无常驻`FIRST_AVAILABLE`(代入朝鲁门,`available=true`
- 蒙A-H7777常驻阿拉坦 08-10~12 有 2 个 blocking 档期冲突)→ `RESIDENT_BUSY_FALLBACK`(回退朝鲁门,`available=true`
- 蒙A-G8888常驻司机停用,候选列表仍有 `primaryDriverId`)→ `RESIDENT_BUSY_FALLBACK`(回退空闲司机)
- 未传 `selectedVehicleId``suggestedDriverId=null``NONE`
- 测试AssignmentCandidateServiceTest 41 全绿(新增 7 场景:常驻可用/档期冲突回退/停用回退/无常驻/无空闲/未选车/VehicleDO 投影缺失;fleet 全量 3155 仅 1 既有基线 flaky;spotless 0 违规
## 关联/联系人
### 链接
- [后端工单 #5559](https://git.1814.love:8443/wx/HL/issues/5559)
- [后端 PR #5561](https://git.1814.love:8443/wx/HL/pulls/5561)
- Merge commit: `efb981ca2a`
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,67 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5562"
title: "用车需求槽位模型改为全程槽expand 生成按车型全程行,车务每槽派 1 辆管全程)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@0da93eaef206f5daa85b99f45ecd0fd07873d5fc"
target_release: "v2.1"
verified_at: "2026-08-06"
status_note: "后端完成PR #5568(全程槽模型)+ #5569(换版沿用修复)已合并 dev-v3 并部署 TEST;网关验证通过全新展开 2 条全程行 / 换版保持全程行 / 每槽派 1 辆管全程 / 整组删 removedRowCount=1 / 按天派生展示不回归)。前端槽位列表与派车交互按全程槽语义适配。"
updated_at: "2026-08-05"
base: "dev-v3"
generated: "2026-08-05T22:45:18+08:00"
---
# 用车需求槽位模型改为全程槽expand 生成按车型全程行,车务每槽派 1 辆管全程)
> 后端完成PR #5568/#5569 已合并 dev-v3 并部署 TEST,网关验证 7/7 通过。
## 关联 / 联系人
### 链接
- **Issue**: [#5562](https://git.1814.love:8443/wx/HL/issues/5562)
- **PR**: [#5568](https://git.1814.love:8443/wx/HL/pulls/5568)(全程槽模型)、[#5569](https://git.1814.love:8443/wx/HL/pulls/5569)(换版沿用修复)
- **Merge commit**: [e051fb93d](https://git.1814.love:8443/wx/HL/commit/e051fb93d)、[41792d8b9](https://git.1814.love:8443/wx/HL/commit/41792d8b9)
### 联系人
- **后端负责人**: @wx
## 背景wx 口径)
槽位**数据模型本身**改为全程槽:定制师需求(例 1 SUV + 1 商务 × 4 天)→ `expand` 默认生成 **2 条全程行**SUV 全程槽 + 商务全程槽,各覆盖整个服务期),车务在每槽派 1 辆管全程的车,槽可修改/删除。**不是**按天 8 条散装切片 + 展示层聚合。存量按天数据不迁移(兼容运行),对账/逐日展示/行程/矩阵等按天界面从全程行派生,保持不变。
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| `PUT` | `/v3/admin/order/{id}/vehicle-requirement` | `VehicleRequirementAdminController`order-v3→ 触发 fleet expand |
路径前缀 `/v3/admin/**``/admin/fleet/**` 已由网关登录/角色校验收口,无需新增网关规则。**契约字段无变化**:需求提交 VOfleet[] 车型组合)与既有看板/详情/候选/派车/删除接口字段全部不变,变化在 Fleet 落库模型与派生行为。
## 模型行为fleet_assignment
- **新展开**:每需求×车型 1 条全程行——`start_date`/`end_date` 覆盖整个服务期,`service_date`/`assignment_group_id` 均为 NULL1SUV+1商务×4天 = 2 条 unassigned 行)
- **派车**create/batchCreate 在全程行上直接升级为 1 条 `assigned` 全程行(每槽 1 辆管全程);改派在槽上换车/司机
- **删除**`DELETE /admin/fleet/assignments/slots/{slotId}` 整组删removedRowCount=1+ `fleet_assignment_slot_removal` 持久化防展开复活
- **换版**服务期一致时全程行沿用rebind 保持 group_id IS NULL;日期变化取消重建
- **按天派生**(显示/对账/快照层不变):看板 `dailyVehiclePlan`、矩阵、DAILY_V3 快照、Step2 canonical 候选、对账/保险均从全程行按服务期展开逐日
## 前端/调用方动作
1. 车务槽位列表默认呈现"一条管全程"的槽位(详情 `vehicleSlots[]`、看板 `assignmentSlots[]` 已是全程区间语义);**删除按钮**调 `DELETE /admin/fleet/assignments/slots/{slotId}`605007 已派拒绝/605012 不存在)
2. 派车弹窗按全程区间提交(`startDate`=服务开始、`endDate`=服务结束、每槽 1 辆 1 司机);最终方案提交仍按逐日 `dailyPlan[]`(同槽各日须同一车/司机,后端整槽聚合)
3. 逐日细节(每日车/司机/价格/接送)继续用 `dailyVehiclePlan`,展示不变
## 验证证据
- 定向测试fleet verify 全绿3159 用例 + Testcontainers;新增全程行展开/派车/换版沿用/DAILY_V3 派生/看板派生测试
- 网关验证TEST全新展开 2 条全程行落库 → 换版保持全程行 → 每槽派 1 辆管全程assigned 全程行)→ 整组删 removedRowCount=1 → 详情 1 条全程槽canDelete/阻断原因)→ dailyVehiclePlan 派生 6 天不回归
- 兼容性结论存量按天数据兼容运行不迁移;legacy 区间行迁移逻辑保留

查看文件

@ -1,108 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5537"
title: "车务保险保单查看(手动保单可见)+ 任务/保单筛选加司机名"
consumer: "admin"
change_type: "新增接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@d5235e2d89ce009fbad251cf1428ba304333524c"
target_release: ""
verified_at: ""
status_note: "后端完成PR #5543 已合并 dev-v3merge e0cf4a47e并部署 TEST;网关验证通过保单列表/司机名筛选/详情/非法状态 400。前端需在车务保险菜单新增保单 Tab见展示矩阵。"
updated_at: "2026-08-05"
base: "dev-v3"
---
# 车务: 保险保单查看(手动保单可见)+ 司机名筛选
> **服务**: hl-fleet-service + hl-order-service-v3内部接口
> **PR**: #5543
> **Issue**: [#5537](https://git.1814.love:8443/wx/HL/issues/5537)
> **日期**: 2026-08-05
## 背景
车务保险页面(`/admin/fleet/insurance`)此前只有任务视图(`tasks` 接口 = `fleet_insurance_task` 投退保流水),**手动投保的保单(`insurance_order` bizType=DRIVER不可见**(排查实证:任务表 PURCHASE+SUCCESS+BAOYOU=0,手动投保从不落任务。本次新增**保单维度**展示 + 手动投保成交自动落任务。
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| `GET` | `/admin/fleet/insurance/policies` | `FleetInsuranceTaskController`fleet |
| `GET` | `/admin/fleet/insurance/policies/{insuranceOrderId}` | 同上 |
| `GET` | `/admin/fleet/insurance/tasks` | 同上(**新增 `driverName` 参数** |
| `GET` | `/v3/internal/insurance/policy-page` | `InternalInsuranceController`order,Feign 内部) |
| `GET` | `/v3/internal/insurance/policy/{insuranceOrderId}` | 同上Feign 内部) |
路径前缀 `/admin/fleet/**` 已由网关登录/角色校验与 `FleetAdminRoleGuardInterceptor`VEHICLE_MANAGER / SUPER_ADMIN收口,无需新增网关规则。
## 1. 保单列表 `GET /admin/fleet/insurance/policies`
请求参数query,均选填
| 参数 | 类型 | 说明 |
|---|---|---|
| `page` / `pageSize` | int | 分页pageSize ≤100 |
| `driverName` | string | 司机名/被保人姓名模糊搜索(两步查 insured_person |
| `policyNo` | string | 保单号精确ext_policy_no |
| `status` | string | PENDING=待出单 / INSURING=出单中 / INSURED=已承保 / CANCELLED=已退保 / FAILED=投保失败;非法枚举返 400 明确业务错误 |
| `coverageStartDateFrom` / `coverageStartDateTo` | date | 保障起期区间(按 start_date |
响应 `data`PageResult
| 字段 | JSON 类型 | 说明 |
|---|---|---|
| `insuranceOrderId` | string(Long) | 保单 ID雪花字符串 |
| `policyNo` | string | 保单号 |
| `extOrderNo` | string | 保游外部订单号(可空) |
| `totalPremium` | string(BigDecimal) | 保费(元) |
| `status` / `statusLabel` | string | 状态码 + 中文标签 |
| `source` | string | BAOYOU / MANUAL |
| `coverageStartDate` / `coverageEndDate` | date | 保障期间 |
| `insuredName` | string | 被保人姓名(首位;司机险一人一单) |
| `productName` / `planName` | string | 险种(产品·计划,可空) |
| `driverId` | string(Long) | 司机 IDbizId |
| `createTime` | string | 创建时间 |
**展示矩阵**
- 数据源:`insurance_order`bizType=DRIVER,与司机详情保单区域同源+ `insured_person` + `insurance_plan/product`
- 列建议:保单号 / 被保人 / 险种(产品·计划)/ 保费 / 保障期间 / 状态 / 创建时间
- **保额**:产品/计划/费率表均无权威结构化保额字段,**不展示保额列**(如需展示需保游产品侧补字段,另行排期)
- 状态色INSURED 绿、PENDING/INSURING 橙、CANCELLED 灰、FAILED 红
- 空态:空列表 + 提示"暂无保单"
## 2. 保单详情 `GET /admin/fleet/insurance/policies/{insuranceOrderId}`
响应 `data`:列表项全部字段 + `extPolicyNo` / `insuredPersons`(姓名+脱敏证件号,保前 3 尾 4/ `planId` / `policyHolderName` / `remark` / `updateTime`
错误码:**540201** 保单不存在或非司机险 / **605601** 保险服务不可用 / 401 未登录。
## 3. 任务列表新增 `driverName` 筛选
`GET /admin/fleet/insurance/tasks?driverName=斯琴`:按任务快照 `driver_name` 模糊匹配。**原有枚举校验taskType/taskStatus/source 非法值 400保持不变**。
## 4. 手动投保落任务(后端行为变更)
手动投保(`POST /admin/fleet/drivers/{driverId}/insurance/purchase`)出单成功(保单状态 INSURED后,自动在 `fleet_insurance_task` 落一条 PURCHASE+SUCCESS 流水bizKey=`MANUAL_PURCHASE:{保单ID}`,幂等;落库失败仅告警不反噬投保)。任务列表(含司机名筛选)可查到手动保单任务;**受理期INSURING保单不落任务**,由保单 Tab 覆盖可见性。
## 前端配合
1. 车务保险菜单新增**保单 Tab**(复用 tasks 页面框架),列与筛选见展示矩阵;
2. tasks Tab 筛选区增加**司机名**输入框(模糊搜索);
3. 保单号点击打开详情抽屉(被保人证件号已脱敏,无需前端二次处理)。
## 关联/联系人
### 链接
- [后端工单 #5537](https://git.1814.love:8443/wx/HL/issues/5537)
- [后端 PR #5543](https://git.1814.love:8443/wx/HL/pulls/5543)
- Merge commit: `e0cf4a47ef`
### 联系人
- **后端负责人**: @wx

查看文件

@ -1,305 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5567"
title: "历史终止订单车辆核单空态"
consumer: "admin"
change_type: "修改接口"
author: "yst(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@4a2dcc8e20fe23ffc2b3d35ae8f02e62661b642d"
target_release: "v2.1"
verified_at: "2026-08-06"
status_note: "PR #5570 已合并 dev-v3;2026-08-10 复核确认测试服已部署、网关链路实测连通(见文末验证证据章节)。管理后台待适配新增阻断枚举。"
updated_at: "2026-08-10"
base: "dev-v3"
---
# 🔧【修改接口·管理后台】历史终止订单车辆核单空态 (#5567)
> **PR**[#5570](https://git.1814.love:8443/wx/HL/pulls/5570) **服务**`hl-order-service-v3` **更新时间**2026-08-06 **消费端**:管理后台
## 1. 接口背景
部分历史终止订单只有旧版车辆费用记录,无法还原为当前车辆核单的权威金额。此类订单只需要安全展示为“缺少权威车辆费用来源、不可完成核单”,不应把它当作车务临时故障,也不得把未知金额当成 0 元已确认。
## 2. 变更清单
| # | 接口名 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 查询车辆核单草稿 | `GET` | `/v3/admin/order/{orderId}/settlement/step3/vehicles` | 修改 | `blockReasonCode` 新增 `LEGACY_VEHICLE_FEE_SOURCE_MISSING`;严格历史终止 LEGACY 场景由 `584100` 改为 `code=200` 的安全阻塞空态 |
## 3. 接口详情
### 3.1 查询车辆核单草稿
- **接口说明**:查询车辆 Tab 当前全量明细、草稿版本、确认状态,以及车辆费用是否具备完成核单条件。
- **使用场景**:进入管理后台订单核单的车辆 Tab 或刷新车辆核单状态。
- **认证**:需要管理后台登录态和订单查看权限;房务角色不可访问。
- **幂等性**:是,只读查询。
- **限流**:未声明接口级独立限流规则。
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 字段 | 位置 | 类型 | 必填 | 说明与校验 |
|---|---|---|---|---|
| `orderId` | Path | String(Long) | 是 | 订单 ID,必须为正整数;按字符串传递,避免大整数精度损失 |
无 Query 参数。
### 4.2 请求体字段
GET 请求无请求体。
## 5. 出参字段
响应类型:`Result<SettlementVehicleFeesRespVO>`
### 5.1 统一响应外层
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `code` | Integer | 否 | 成功为 `200`;失败见 §7 |
| `message` | String | 否 | 结果说明 |
| `data` | VehicleDraft/null | 失败时为空 | 成功时为车辆核单草稿 |
| `traceId` | String | 是 | 链路追踪 ID |
| `success` | Boolean | 否 | `code=200` 时为 `true` |
### 5.2 成功响应 `data`
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `orderId` | String(Long) | 否 | 订单 ID,按字符串返回 |
| `version` | Long | 否 | 当前车辆核单草稿版本;安全阻塞空态为 `0` |
| `totalAmount` | Decimal/null | 是 | 当前明细总金额;`LEGACY_VEHICLE_FEE_SOURCE_MISSING` 时必须为 `null`,表示金额未知,不是 `0.00` |
| `allConfirmed` | Boolean | 否 | 非空明细是否全部确认;LEGACY 安全阻塞空态固定为 `false` |
| `settlementReady` | Boolean | 否 | 车辆费用是否具备完成核单条件;LEGACY 安全阻塞空态固定为 `false` |
| `blockReasonCode` | String/null | 是 | 不具备条件时的机器可读原因,完整取值见 §6;具备条件时为 `null` |
| `items` | VehicleItem[] | 否 | 当前全量明细;LEGACY 安全阻塞空态固定为 `[]` |
### 5.3 `data.items[]`
| 字段 | 类型 | 可空 | 说明 |
|---|---|---|---|
| `id` | String(Long) | 否 | 车辆核单明细 ID |
| `sourceType` | String | 否 | 来源类型:`FLEET``MANUAL` |
| `sourceTypeName` | String | 否 | 来源名称:车务或手工 |
| `serviceDate` | String(date) | 否 | 服务日期,格式 `YYYY-MM-DD` |
| `vehicleId` | String(Long) | 是 | 车辆 ID |
| `vehiclePlate` | String | 是 | 车牌号 |
| `vehicleModelId` | String(Long) | 是 | 车型 ID |
| `vehicleModelName` | String | 是 | 车型名称 |
| `driverId` | String(Long) | 是 | 司机 ID |
| `driverName` | String | 是 | 司机姓名 |
| `amount` | Decimal | 否 | 核单金额 |
| `paymentMethod` | String | 否 | 付款方式编码 |
| `paymentMethodName` | String | 否 | 付款方式名称 |
| `settlementConfirmStatus` | String | 否 | 确认状态编码:`UNCONFIRMED``CONFIRMED` |
| `settlementConfirmStatusName` | String | 否 | 确认状态名称:未确认或已确认 |
| `remark` | String | 是 | 备注 |
| `voucherUrls` | String[] | 否 | 凭证 URL;无凭证时为 `[]` |
## 6. 枚举 / 数据字典
### 6.1 `blockReasonCode`
**所属字段**`data.blockReasonCode` **类型**String/null **可空**:是
| 值 | 中文 | 说明 |
|---|---|---|
| `VEHICLE_FEE_NOT_READY` | 车辆费用尚未就绪 | 有当前用车需求,但尚无可返回的车辆费用明细;`settlementReady=false``totalAmount=0.00``allConfirmed=false``items=[]` |
| `LEGACY_VEHICLE_FEE_SOURCE_MISSING` | 历史车辆费用权威来源缺失 | 历史终止 LEGACY 场景无法确认权威车辆金额;`settlementReady=false``totalAmount=null``allConfirmed=false``items=[]` |
| `null` | 无阻断原因 | 车辆费用来源已就绪;为 JSON 空值,不是字符串 `"null"` |
### 6.2 `items[].sourceType`
| 值 | 中文 | 说明 |
|---|---|---|
| `FLEET` | 车务 | 车务来源的车辆核单明细 |
| `MANUAL` | 手工 | 管理后台手工维护的车辆核单明细 |
### 6.3 `items[].paymentMethod`
| 值 | 中文 | 说明 |
|---|---|---|
| `CASH_PAID` | 现金已付 | 现金支付 |
| `SIGNED` | 签单 | 按签单方式结算 |
| `COMPANY_PAID` | 公司付款 | 由公司支付 |
### 6.4 `items[].settlementConfirmStatus`
| 值 | 中文 | 说明 |
|---|---|---|
| `UNCONFIRMED` | 未确认 | 当前车辆费用行尚未完成核单确认 |
| `CONFIRMED` | 已确认 | 当前车辆费用行已完成核单确认 |
## 7. 错误码
| code | 含义 | 触发场景 |
|---|---|---|
| `200` | 查询成功 | 包括历史终止 LEGACY 安全阻塞空态;此时以 `settlementReady``blockReasonCode` 判断是否可继续 |
| `400` | 请求参数错误 | `orderId` 不是正整数 |
| `403` | 无访问权限 | 登录态、角色或权限不允许访问 |
| `581007` | 订单不存在 | `orderId` 对应订单不存在 |
| `584071` | 无权访问该订单 | 当前账号不在订单可访问范围内 |
| `584100` | 车辆费用暂时不可用 | 真实车辆快照损坏、依赖故障、响应身份不匹配或必要字段无效;本次不把这些故障转换为空态 |
| `584101` | 车辆费用尚未满足核单条件 | 已有非空车辆明细,但来源未完结或费用条件未满足 |
## 8. 示例
### 8.1 典型成功:历史终止 LEGACY 安全阻塞空态
**请求**
```http
GET /v3/admin/order/9223372036854775000/settlement/step3/vehicles
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "9223372036854775000",
"version": 0,
"totalAmount": null,
"allConfirmed": false,
"settlementReady": false,
"blockReasonCode": "LEGACY_VEHICLE_FEE_SOURCE_MISSING",
"items": []
},
"traceId": null,
"success": true
}
```
### 8.2 边界情况:无当前用车需求
**场景说明**:该场景同样返回空数组,但它是合法可继续状态,金额为真实的 `0.00`,与历史 LEGACY 金额未知完全不同。
**请求**
```http
GET /v3/admin/order/900000000002/settlement/step3/vehicles
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderId": "900000000002",
"version": 0,
"totalAmount": 0.00,
"allConfirmed": true,
"settlementReady": true,
"blockReasonCode": null,
"items": []
},
"traceId": null,
"success": true
}
```
### 8.3 业务失败:真实车辆快照损坏或依赖故障
**请求**
```http
GET /v3/admin/order/900000000004/settlement/step3/vehicles
Authorization: Bearer <管理后台访问令牌>
```
无请求体。
**响应**
```json
{
"code": 584100,
"message": "车务车辆总车费暂时不可用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
```
## 9. 业务边界
- **适用场景**:仅当订单属于历史终止场景,并被判定为缺少当前权威车辆费用来源的 LEGACY 数据时,返回 `code=200``LEGACY_VEHICLE_FEE_SOURCE_MISSING`
- **不可放行**:该安全空态固定为 `totalAmount=null``allConfirmed=false``settlementReady=false``items=[]`。金额是未知,不得转成 `0`,也不得按“0 元且已确认”处理。
- **判断顺序**:先检查 `settlementReady`,再读取 `blockReasonCode`;不能仅凭 `code=200``items=[]``version=0` 判断可完成核单。
- **故障边界**:真实车辆快照损坏、依赖故障、响应身份不匹配或必要字段无效仍可返回 `584100`,不伪装为 LEGACY 安全空态。
- **其他订单**:非历史终止 LEGACY 场景沿用原有车辆核单成功、未就绪或失败契约。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 原来 | 现在 |
|---|---|---|
| `data.blockReasonCode` | `VEHICLE_FEE_NOT_READY``null` | 新增 `LEGACY_VEHICLE_FEE_SOURCE_MISSING` |
| LEGACY 空态 `data.totalAmount` | 无成功响应字段值 | `null`,明确表示金额未知 |
| LEGACY 空态 `data.allConfirmed` | 无成功响应字段值 | `false` |
| LEGACY 空态 `data.settlementReady` | 无成功响应字段值 | `false` |
| LEGACY 空态 `data.items` | 无成功响应字段值 | `[]` |
### 10.2 行为级对比
| 行为 | 原来 | 现在 |
|---|---|---|
| 历史终止 LEGACY 数据缺少权威车辆费用来源 | 返回 `code=584100`“车务车辆总车费暂时不可用” | 返回 `code=200` 的安全阻塞空态,`blockReasonCode=LEGACY_VEHICLE_FEE_SOURCE_MISSING` |
| 真实车辆快照损坏或依赖故障 | 返回 `584100` | 仍可返回 `584100`,行为不变 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:响应字段结构不变,枚举为新增值;但历史终止 LEGACY 场景由失败响应改为成功阻塞响应,依赖捕获 `584100` 的旧控制流需要适配。
- **前端是否必须同步上线**:需要识别新增枚举值,并按 `settlementReady=false` 保持阻塞;不得把 `totalAmount=null` 转成 0 或把空数组视为已确认。
### 11.2 回滚方案
- 若接口行为回滚,历史终止 LEGACY 场景会恢复为 `584100`;前端应同时兼容该错误码和本次新增的安全阻塞空态,避免回滚期间误放行。
## 12. 注意事项
- `totalAmount=null` 表示无法确认权威金额;`0.00` 才表示确定为 0 元,两者不可互换。
- `items=[]` 不代表车辆核单已完成;必须同时读取 `settlementReady``allConfirmed`
- `LEGACY_VEHICLE_FEE_SOURCE_MISSING` 是机器可读阻断原因,不是车务临时不可用的别名。
- 不要清除对 `584100` 的失败处理:真实快照损坏和依赖故障仍可能返回该错误码。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**[#5567](https://git.1814.love:8443/wx/HL/issues/5567)
- **PR**[#5570](https://git.1814.love:8443/wx/HL/pulls/5570)
- **Merge commit**[fbc42f31b173](https://git.1814.love:8443/wx/HL/commit/fbc42f31b17301b078a9b63c3c064eb983807f79)
### 13.2 联系人
- **后端负责人**@yst / yaosutu
- **前端消费方**:管理后台车辆核单 Tab
## 验证证据2026-08-10 复核回填,wx
> 本条 changelog 2026-08-06 推送时 backend_status=pending未部署,违反「测试服部署+实测后才通知前端」流程。2026-08-10 复核补齐部署与验证证据如下:
- 测试服 hl-order-service-v3 运行版本为 2026-08-10 15:10 构建dev-v3,晚于 PR #5570 合并点2026-08-06 08:17,本变更代码已在运行实例中。
- `GET /v3/admin/order/{orderId}/settlement/step3/vehicles` 经网关 9443 + 真 admin token 实测链路连通(普通订单返回既有业务码,行为正常)。
- 测试库当前无 `flow_status=TERMINATED` 的历史终止订单,`LEGACY_VEHICLE_FEE_SOURCE_MISSING` 安全空态场景暂无法端到端复现,该场景行为以合并代码 + 部署点位确认;如前端联调需要真实数据请联系后端造数。

查看文件

@ -1,72 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5571"
title: "派车价0与未填价格允许发送司机0价不再被非负金额拦截"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@e2a060d09a66cdb5b89184e3c35a3fbfd923064f"
target_release: "v2.1"
verified_at: "2026-08-06"
status_note: "后端完成PR #5577 + #5580 已合并 dev-v3 并部署 TEST;网关实测 0 价 batchCreate 发送司机成功assigned/protocolPrice=0.00)。前端需确认校验规则放行 0/空价并适配日格价格默认值展示anchor #5571,hl-ui v2.1 已有 DailyVehicleFeeList 编辑入口,需前端部署后核验)。"
updated_at: "2026-08-06"
base: "dev-v3"
generated: "2026-08-06T01:33:44+08:00"
---
# 派车价0与未填价格允许发送司机0价不再被非负金额拦截
> **服务**: hl-fleet-service
> **PR**: [#5577](https://git.1814.love:8443/wx/HL/pulls/5577) + [#5580](https://git.1814.love:8443/wx/HL/pulls/5580)
> **Issue**: [#5571](https://git.1814.love:8443/wx/HL/issues/5571)
> **日期**: 2026-08-06
## 背景
#5571P1 阻断协调台浏览器实测26-4220 派车 2 全程槽派好车/司机后,日格"本次派车价"显示 ¥0.00,行内报"**价格应为非负金额,整数最多 10 位、小数最多 2 位**","下一步·发送给司机"被前端表单校验拦死0/8 日格无法完成。wx 口径:**实际派车价格为 0 是允许的**(含团费/内部结算等场景——0 价不得拦截发送司机。
## 校验出处定位
- 该文案出自**前端** hl-ui v2.1 `src/views/fleet/board/utils/daily-vehicle-plan.js` 提交门禁(`ASSIGNMENT_PRICE_PATTERN` 拒绝空值)与 `DailyVehiclePlanMatrix` 输入校验;**后端 dev-v3 无此文案**。
- 触发链:后端详情 `dailyVehiclePlan[].assignmentPrice` 曾回显 `null`(逐日行未冻结价格时直接取单值 `protocolPrice`),前端显示层兜底为 ¥0.00 但提交校验拿到空值即拦截。
## 变更接口
| 方法 | 路径 | 来源 | 变更 |
|---|---|---|---|
| `POST` | `/admin/fleet/assignments/batch` | `AssignmentController`fleet | 语义变更:用车日 `assignmentPrice` 允许为空或 0未填→日历参考价代入;0 价为合法派车价 |
| `GET` | `/admin/fleet/board/orders/<orderId>` | `BoardOrderController`fleet | 语义变更:`dailyVehiclePlan[].assignmentPrice` 不再回显 null——快照价→协议价→日历参考价→0 兜底;`priceSource` 同步(快照源优先,全缺=MISSING |
| `POST` | `/admin/fleet/assignments/<assignmentId>/change` 等 | `AssignmentController`fleet | 语义变更DAILY_V3 快照豁免矩阵仅约束免费日;收费日 0 价(无豁免原因)不再被拦截 |
字段结构均不变(无新增/删除/改名),仅取值语义与门禁口径调整。
## 前端/调用方动作anchor #5571
1. **校验规则放行 0 与空价**`daily-vehicle-plan.js` 提交门禁与矩阵输入校验应允许 `0` 与空值(空值按 0/日历价兜底,由后端回显保证非 null;仅负数与超精度整数≤10 位、小数≤2 位)拒绝
2. **日格价格默认值展示**:后端已按"日历参考价代入→0 兜底"回显;前端如遇 `assignmentPrice=null` 的历史数据(修复前已存),可显示 `0.00``-`,不得触发"价格应为非负金额"拦截
3. **编辑入口**hl-ui v2.1 已具备 `DailyVehicleFeeList` 日格价格输入框editable 状态);若当前部署版本无编辑入口,请升级前端或按后端候选参考价自动代入
4. 与日历参考价不一致的 0 价仍必须填写改价原因(`priceAdjustmentReason`#5292 口径),前端原因输入逻辑保持不变
## 验证证据
- 定向测试VehicleFeeQuoteServiceTest 16/16未填→日历价代入、显式 0 价、0 日历价、AssignmentServiceTest 371/371null/0 放行、负数/缺司机拒绝、BoardOrderServiceTest 84/84回显兜底链快照 0 优先/日历价回退/全缺 0+MISSING、DailyVehicleAssignmentSnapshotFactoryTest 9/9收费日 0 价免豁免、免费日必填豁免;fleet 全量 `mvn -pl hl-fleet-service -am verify`3168 tests,0 failures,0 errors,4 skipped含 spotless:check
- 网关验证TEST0 价 batchCreateHL20260804104759654,5 日 dailyPlan 全 0 价 + 改价原因,holdMode=0→ code=200 发送司机成功assigned、protocolPrice=0.00、vehicleFeeTotal=0.00、MANUAL;详情回显 2080599963561005058 8/8 日格 assignmentPrice 非 null 且为逐日快照价600-730而非旧 protocolPrice 单值;候选接口日历价参考回显不变1500.00 CALENDAR
- 环境说明MySQL 集成测试依赖 127.0.0.1:3306 固定映射,本机 Docker 端口 TIME_WAIT 竞争导致部分用例偶发失败,清理残留容器+等待端口消退后单独重跑全绿
## 关联/联系人
### 链接
- [后端工单 #5571](https://git.1814.love:8443/wx/HL/issues/5571)
- [后端 PR #5577](https://git.1814.love:8443/wx/HL/pulls/5577)null/0 门禁放开 + 回显兜底链)+ [#5580](https://git.1814.love:8443/wx/HL/pulls/5580)(快照豁免矩阵放开收费日 0 价)
- Merge commits: [349f97cc5](https://git.1814.love:8443/wx/HL/commit/349f97cc52d572c2465f90d05f797e655be60291)#5577)、[ce28c53a8](https://git.1814.love:8443/wx/HL/commit/ce28c53a829860b240695792ac6a99e387d9d96e)#5580
- 关联:[#5292 逐日派车模型0 价口径)](https://git.1814.love:8443/wx/HL/issues/5292)、[#5562 全程槽模型](https://git.1814.love:8443/wx/HL/issues/5562)
### 联系人
- **后端负责人**: @wx
- **前端消费**: 待认领hl-admin

查看文件

@ -1,100 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5572"
title: "派车全程槽删除支持已派车联动取消释放(删除按钮点击无响应修复)"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@045434c1926505d7524239e00464d74e66e52d6b"
target_release: "v2.1"
verified_at: "2026-08-06"
status_note: "后端完成PR #5576 + #5579 已合并 dev-v3 并部署 TEST;网关验证通过已派车槽删除联动取消+释放占用 cancelledAssignmentCount=5、605007 仅剩 completed/finalized、slot_removal 防复活、修复占用上下文 500。前端需修复删除按钮 handler 静默 return 并适配已派车槽删除交互anchor #5572)。"
updated_at: "2026-08-06"
base: "dev-v3"
generated: "2026-08-06T00:10:04+08:00"
---
# 派车全程槽删除支持已派车联动取消释放
> **服务**: hl-fleet-service
> **PR**: [#5576](https://git.1814.love:8443/wx/HL/pulls/5576) + [#5579](https://git.1814.love:8443/wx/HL/pulls/5579)(占用上下文修复)
> **Issue**: [#5572](https://git.1814.love:8443/wx/HL/issues/5572)
> **日期**: 2026-08-06
## 背景
#5572P1协调台实测26-4220 派车页已派车/待确认全程槽的「删除」按钮点击完全无响应。定位结论:
1. **前端**hl-ui,本单交接`AssignModal.handleDeleteVehicleSlot` 对已派车槽(`isExistingAssignment`)或非草稿模式**静默 return**(无确认/toast,且 #5562 全程槽 changelog 前端未消费pending——按钮显示但点击无事件;
2. **后端**(本单修复):删除接口对 `holding`/`assigned` 槽返回 605007,与 #5562「槽可修改/删除」口径及 #5572 验收「已派车槽位删除的联动(释放车辆/司机占用、日格清理)」冲突。
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| `DELETE` | `/admin/fleet/assignments/slots/{slotId}` | `AssignmentController`fleet |
路径前缀 `/admin/fleet/**` 已由网关登录/角色校验与 `FleetAdminRoleGuardInterceptor`VEHICLE_MANAGER / SUPER_ADMIN收口,无需新增网关规则。
## 1. 删除车辆槽位 `DELETE /admin/fleet/assignments/slots/{slotId}`(口径变更)
请求参数不变:`slotId`PathVar+ `reason`Body 选填 ≤200 字)。
**删除规则(业务口径,#5572 变更)**
- **可删**(含已派车):槽位行均为 `unassigned`/普通 `canceled` 历史,**或存在 `holding`/`assigned`(已派车/待确认)行**——删除前联动取消派车:逐行 `assigned→canceled`CAS+ 状态机 + 取消事件(**释放车辆/司机占用、保险退保意图、需求重开**+ 操作日志;看板逐日派生自然过滤 canceled日格清理
- **拒绝605007**:槽位存在 `completed` 行(已完成行程保留历史),或**已最终确认方案dispatchPlanFinalized=1的取消行**(防 reconcile 复活 + 对账关联)
- **拒绝605027**行程已出发today > 服务开始日)的已派车槽位,与取消派单口径一致
- **605012**:槽位不存在
**删除动作(事务内)**
1. (有已派车时)联动取消该槽全部 `holding`/`assigned` 行并释放占用
2. 软删该槽位全部 `unassigned` 每日切片canceled 历史行保留——对账/保险 REFUND 关联不丢失)
3. `fleet_assignment_slot_removal` 写入删除意图(**条件放宽**:纯已派车槽 removed=0 也写,防重新展开复活)
4. Step2 canonical 快照:`retainedSlotIds` 移除该槽位 + `snapshotVersion` 递增 + `contentDigest` 重算
5. 派单操作日志追加 `slot_removed`(幂等键 `slot-remove:{slotId}`
**响应 `data`(新增字段)**
| 字段 | JSON 类型 | 说明 |
|---|---|---|
| `slotId` | string(Long) | 被删除槽位 ID |
| `requirementId` / `orderId` | string(Long) | 归属需求/订单 |
| `removedRowCount` | int | 软删除的未派单切片行数 |
| `cancelledAssignmentCount` | int | **新增**:联动取消的已派车/待确认行数0=无已派车) |
| `retainedSlotIds` | array(string) | 删除后快照保留槽位集合 |
| `snapshotVersion` | string(Long) | 删除后快照版本(无快照为 null |
**错误码变更****605007** 文案由「槽位已派车或已确认,请先取消派车后再删除」改为「槽位已完成派车或已最终确认,不可删除」;新增 **605027**(行程已出发,不能删除已派车槽位)。
## 2. 订单详情 `GET /admin/fleet/board/orders/{orderId}`(语义变更,字段不变)
`vehicleSlots[].canDelete`:已派车/待确认槽位由 `false` 改为 **`true`**(删除时联动取消释放);仅 `completed`/finalized 取消行或订单只读时仍为 `false``deleteBlockReason` 文案同步更新。
## 前端/调用方动作anchor #5572
1. **修复删除按钮点击无响应**`AssignModal.handleDeleteVehicleSlot``isExistingAssignment`/`!slotDraftMode` 的静默 return 需改为可见反馈;已派车槽位删除应弹二次确认(提示将联动取消派车并释放车辆/司机占用)后调 `DELETE /admin/fleet/assignments/slots/{slotId}`
2. **已派车槽位删除入口**:删除按钮 `v-if="!slot.isExistingAssignment"` 需放开至已派车槽(后端已支持联动释放);或保留按 `canDelete` 控制并展示 605007/605027 错误 toast**注意前端写死的 605007 文案已过时**,需改为「槽位已完成派车或已最终确认,不可删除」)
3. **#5562 全程槽语义适配**(原 pending全程行service_date NULL`isExistingAssignment` 判定、删除后刷新候选(`cancelledAssignmentCount` 可作删除结果提示)
## 验证证据
- 定向测试AssignmentServiceTest 371/371+3已派车槽联动取消删除成功/已出发 605027/completed 605007、BoardOrderServiceTest 84/84已派车槽 canDelete=true、AssignmentControllerTest 41/41;spotless:check 通过
- 网关验证TEST已派车槽 DELETE 200 + cancelledAssignmentCount=55 条 assigned 行联动取消)+ 行全部 canceled + 占用释放 + slot_removal 持久化;修复前网关实测 500缺 occupancy context已由 PR #5579 修复
- 环境说明fleet verify 3167 用例 Failures=0;MySQL 集成测试 13/13FleetAssignmentSlotRemovalMysqlTest 3/3 + ReleaseEOccupancyMysql8033RecoveryTest 10/10,3306 竞争窗口补跑)
## 关联/联系人
### 链接
- [后端工单 #5572](https://git.1814.love:8443/wx/HL/issues/5572)
- [后端 PR #5576](https://git.1814.love:8443/wx/HL/pulls/5576) + [#5579](https://git.1814.love:8443/wx/HL/pulls/5579)
- 关联:[#5562 全程槽模型](https://git.1814.love:8443/wx/HL/issues/5562)、[#5550 槽位删除](https://git.1814.love:8443/wx/HL/issues/5550)
### 联系人
- **后端负责人**: @wx
- **前端消费**: 待认领hl-admin

查看文件

@ -1,68 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5573"
title: "派车候选按槽位座位需求校验SUV槽5座不再误标座位不足"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@3e535fe3ad6a406529eb95d7787cefe11b9584cd"
target_release: "v2.1"
verified_at: "2026-08-06"
status_note: "后端完成PR #5578 已合并 dev-v3 并部署 TEST;网关验证通过商务槽 7 座/SUV 槽 5 座在整单 10-12 人下不再误标座位不足,车型匹配保留)。候选接口字段无变化,前端零改动;需确认派车弹窗仍传 fleetItemIndex/requiredVehicleType。"
updated_at: "2026-08-06"
base: "dev-v3"
generated: "2026-08-06T01:05:02+08:00"
---
# 派车候选按槽位座位需求校验SUV槽5座不再误标座位不足
> 后端完成PR #5578 已合并 dev-v3 并部署 TEST,网关验证 4/4 通过。
## 关联 / 联系人
### 链接
- **Issue**: [#5573](https://git.1814.love:8443/wx/HL/issues/5573)
- **PR**: [#5578](https://git.1814.love:8443/wx/HL/pulls/5578)
- **Merge commit**: [49e0331fe](https://git.1814.love:8443/wx/HL/commit/49e0331fe)
### 联系人
- **后端负责人**: @wx
## 背景
26-4220乘客 10 人,SUV 槽 5 座 + 商务槽 7 座)派车:候选面板此前按**整单 headcount**10 人校验每辆候选车,SUV 槽所有 SUV5-7 座)误标"需求不匹配·座位不足"(可选但误导),考斯特 19 座也被标不匹配。
## 口径
槽位级派车按**槽位座位需求**校验SUV 槽 5 座 → 车辆座位数 ≥ 5 即匹配;乘客分配跨槽SUV 载 4 + 商务载 6 = 10。车型匹配保留SUV 槽优先 SUV,其他车型仍标不匹配原因
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| `POST` | `/admin/fleet/assignments/candidates` | `AssignmentController`fleet |
路径前缀 `/admin/fleet/**` 已由网关登录/角色校验收口,无需新增网关规则。**请求/响应字段无变化**:后端从 order-v3 当前需求按 `fleetItemIndex`count 展开定位)推导槽位要求座位数;推导不到(缺 orderId/fleetItemIndex 或需求不可达)时回退整单 `headcount` 旧行为。
## 行为变化
- `seatsEnough` / `requirementMatched` 的座位维度:有槽位座位需求时按"车辆座位数 ≥ 槽位要求座位数"判定(不再用整单 headcount
- 车型匹配(`requiredVehicleType` vs 车辆车型 key逻辑不变
- `headcount` 参数保留(展示/回退用),无字段删除
## 前端/调用方动作
1. 派车弹窗**保持传 `fleetItemIndex`(当前槽位序号)与 `requiredVehicleType`(槽位车型)**——修复依赖这两个参数定位槽位座位需求
2. 展示逻辑不变:`seatsEnough=false` 才标"座位不足",`requirementMatched=false` 才标"需求不匹配"
## 验证证据
- 定向测试AssignmentCandidateServiceTest 43/43+2槽位 5 座配 10-12 人订单不误标 / 无槽位序号回退 headcount;fleet verify 3143 用例全绿
- 网关验证TEST商务槽 7 座headcount=107 座车 seatsEnough=true;SUV 槽 5 座headcount=12SUV 5-7 座 seatsEnough=true;mpv 车在 suv 槽仍 requirementMatched=false车型不匹配正确标注
- 兼容性结论:无契约变化;未传 fleetItemIndex 的旧调用保持原 headcount 校验

查看文件

@ -1,80 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5574"
title: "派车候选透传常驻司机赛季状态并对拉黑常驻给出明确自动代入原因"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "pi-main-session"
frontend_ref: "hl-admin@d97cc730b019f5947766f3778e2f5b3ca03a5280"
target_release: "v2.1"
verified_at: "2026-08-06"
status_note: "后端完成PR #5582 已合并 dev-v3 并部署 TEST,网关验证 3/3 通过。候选车辆 VO 新增 primaryDriverSeason 字段新增字段,兼容;suggestedDriverReason 新增 RESIDENT_BLACKLISTED 值(新增值,兼容)。前端可在车辆候选行标注'常驻司机已拉黑',并在自动代入提示中区分拉黑与档期冲突。"
updated_at: "2026-08-06"
base: "dev-v3"
generated: "2026-08-06T09:40:00+08:00"
---
# 派车候选透传常驻司机赛季状态并对拉黑常驻给出明确自动代入原因
> 后端完成PR #5582 已合并 dev-v3 并部署 TEST,网关验证 3/3 通过。
## 关联 / 联系人
### 链接
- **Issue**: [#5574](https://git.1814.love:8443/wx/HL/issues/5574)
- **PR**: [#5582](https://git.1814.love:8443/wx/HL/pulls/5582)
- **Merge commit**: [83dd24d1b](https://git.1814.love:8443/wx/HL/commit/83dd24d1b)
### 联系人
- **后端负责人**: @wx
## 背景
工单 #5574 三个现象经 TEST 库实证 + 候选链路代码核对,**均为测试环境数据状态**而非候选过滤 bug
1. 巴特尔(`2065272153565020161``season=blacklist`06-15 拉黑 reason=4444——司机候选 SQL 按 `season=ACTIVE` 过滤,黑名单司机不进候选是业务正确语义(#5139 契约:黑名单仍按现有业务守卫阻断);另一同名 active 巴特尔已软删06-12
2. 蒙A-G8888 `vehicle_status=busy`(今日起 08-14~26 有 9 条在途 assigned 派单,正确)但候选区间 08-28~31 无冲突显示"所选服务日期内可用"(也正确)——两者口径不同但不矛盾,候选 VO 已透传 `vehicleStatus` 供前端展示
3. 测A88V01 已软删06-12 创建 20 秒后删除)——车辆候选 @TableLogic 自动过滤软删,正确
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| `POST` | `/admin/fleet/assignments/candidates` | `AssignmentController`fleet |
路径前缀 `/admin/fleet/**` 已由网关登录/角色校验收口,无需新增网关规则。**兼容性**:仅新增字段与新增枚举值,无删除/无类型变化。
## 行为变化
### 车辆候选新增字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `primaryDriverSeason` | String/null | 常驻司机赛季状态(`active`/`pending`/`archived`/`blacklist`);无常驻或常驻已软删为 `null` |
车辆候选此前显示常驻司机姓名(`primaryDriverName`但司机候选无此人blacklist 被正确排除)时,前端无法向用户解释"常驻司机去哪了";新增字段后可按 `primaryDriverSeason=blacklist` 标注"常驻司机已拉黑"。
### suggestedDriverReason 新增枚举值
| 值 | 语义 |
|---|---|
| `RESIDENT_BLACKLISTED` | 常驻司机已拉黑season=blacklist,已回退自动代入空闲司机 |
此前已选车辆常驻司机为 blacklist 时,`getAssignmentCandidate` 返回 null与档期冲突/休整同落 null,自动代入回退原因误报 `RESIDENT_BUSY_FALLBACK`"常驻司机不空闲");现按 season 区分,返回 `RESIDENT_BLACKLISTED`"常驻司机已拉黑")。既有 `RESIDENT_BUSY_FALLBACK` 语义不变(档期冲突/休整/待激活/停用)。
## 前端/调用方动作
1. 车辆候选行可按 `primaryDriverSeason === "blacklist"` 显示"常驻司机已拉黑"标注(可选优化)
2. 自动代入提示按 `suggestedDriverReason` 展示后端 message`RESIDENT_BLACKLISTED` 已有明确文案)
3. `vehicleStatus` 已透传busy=今日起有在途派单),可在候选行展示"当前忙碌/所选区间可用"(可选优化)
## 验证证据
- 定向测试AssignmentCandidateServiceTest 45/45+2blacklist 常驻自动代入 RESIDENT_BLACKLISTED / 车辆候选 primaryDriverSeason 透传;fleet verify 全绿(含 MySQL 集成测试)
- 网关验证TESTG8888 车辆候选 `primaryDriverSeason=blacklist` + `vehicleStatus=busy` + `available=true`(区间无冲突);司机候选 22 人含"巴特尔"=0blacklist 正确排除;selectedVehicle=G8888 → `suggestedDriverReason=RESIDENT_BLACKLISTED` + "常驻司机已拉黑,已回退自动代入空闲司机"

查看文件

@ -1,64 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5575"
title: "用车需求重复提交幂等FOR UPDATE锁修复+同内容重放不产生新行)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: "pi-main-session"
frontend_ref: "N/A请求字段和成功控制流不变,现有页面不穷举 branchTaken。"
target_release: "v2.1"
verified_at: "2026-08-06"
status_note: "后端完成PR #5584/#5585 已合并 dev-v3 并部署 TEST;网关验证 3/3连续同内容重放 2 次均返回 IDEMPOTENT_NOOP 同 requirementId,无新行;内容变化仍走 PENDING_EDIT;存量 v3 双行已按清理 manifest 清理。响应新增 branchTaken=IDEMPOTENT_NOOP 取值(前端可据此提示'内容未变化'),其余契约不变。"
updated_at: "2026-08-06"
base: "dev-v3"
generated: "2026-08-06T11:07:21+08:00"
---
# 用车需求重复提交幂等FOR UPDATE锁修复+同内容重放不产生新行)
> 后端完成PR #5584/#5585 已合并 dev-v3 并部署 TEST,网关验证 3/3 通过。
## 关联 / 联系人
### 链接
- **Issue**: [#5575](https://git.1814.love:8443/wx/HL/issues/5575)
- **PR**: [#5584](https://git.1814.love:8443/wx/HL/pulls/5584)、[#5585](https://git.1814.love:8443/wx/HL/pulls/5585)
- **Merge commit**: [1839b2aa4](https://git.1814.love:8443/wx/HL/commit/1839b2aa4)、[dac3e9347](https://git.1814.love:8443/wx/HL/commit/dac3e9347)
### 联系人
- **后端负责人**: @wx
## 背景
订单 2080600006514872322 需求 V3 两条记录2085009136168177665 PROCESSING + 2085009096510935041 PENDING,同 version=3——重复提交双写双击/重试/重放)。
## 修复
1. **并发双写**`VehicleRequirementMapper.selectLatestByOrderIdForUpdate``.last("FOR UPDATE").last("LIMIT 1")`——MyBatis-Plus `last()` 覆盖式,LIMIT 1 覆盖掉 FOR UPDATE → 提交事务退化为快照读,并发提交基于同一快照算出同 version 双行。修复为单段 `.last("LIMIT 1 FOR UPDATE")`MySQL 语法 FOR UPDATE 须在 LIMIT 后)
2. **顺序重复提交**PENDING/PENDING_REVIEW 分支先做内容语义比较fleet/行程日期/接送机/特殊标签/备注,JsonNode 解析比较兼容存量空格/字段序差异),完全一致时返回 `IDEMPOTENT_NOOP` 分支的当前版本——不插新行、不失活旧行、不触发 expand/reconcile/outbox
## 变更接口
| 方法 | 路径 | 来源 |
|---|---|---|
| `PUT` | `/v3/admin/order/{id}/vehicle-requirement` | `VehicleRequirementAdminController`order-v3 |
**请求/响应字段无变化**。响应 `branchTaken` 新增取值 `IDEMPOTENT_NOOP`(重复提交时返回当前 requirementId/version 不变)。
## 前端/调用方动作
1. 提交按钮**可重复点击**:同内容重复提交返回 `branchTaken=IDEMPOTENT_NOOP`,前端可提示"内容未变化"或直接忽略,**无需**做前端防抖(后端已幂等)
2. 内容变化提交仍走 `PENDING_EDIT`/`DONE_ADJUST`,行为不变
## 验证证据
- 定向测试RequirementServiceTest 198/198+2同内容→IDEMPOTENT_NOOP 且 insert/update/expand 未调用;内容变化→PENDING_EDIT;MapperTest 7/7+2 SQL 形态断言;VehicleRequirementConcurrentVersionMysqlTest 4/4+1 真实 MySQL LIMIT 1 FOR UPDATE 并发用例)
- 网关验证TESTHL20260805165915481 连续同内容重放 2 次均返回 IDEMPOTENT_NOOP 同 requirementId2085199671193411585v1,无新行;存量带空格 fleet JSON 语义比较正确
- 数据清理2080600006514872322 v3 双行按 manifest 清理(删 2085009096510935041,保留 2085009136168177665,v1-v5 各版本唯一
- 兼容性结论:无契约变化;存量数据语义比较兼容;未传字段的旧调用保持原行为

查看文件

@ -1,391 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5581"
title: "核算餐厅/导游/摄影资源下拉选项接口"
consumer: "admin"
author: "yst"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "implemented"
frontend_owner: "mmg"
frontend_ref: "hl-admin@479c44ce399995782504887e0f3aa68274a24e19"
target_release: ""
verified_at: "2026-08-06"
status_note: "hl-resource-service;PR #5583 已合并 dev-v3 并部署测试服,Gateway 实调验证通过3 接口 + 必填/关键词/鉴权边界全绿);等待管理后台接入。"
updated_at: "2026-08-07"
base: "dev-v3"
---
# 核算餐厅 / 导游 / 摄影资源下拉选项接口(新增 3 个)
- **变更类型**:新增接口
- **端类型**:管理后台
- **日期**2026-08-06
- **服务**hl-resource-service资源服务
- **关联 Issue**#5581 新增核算餐厅导游摄影资源下拉接口
---
## 1. 接口背景
管理后台核单settlement场景在录入餐厅 / 导游 / 摄影的实际费用时,需要下拉选择对应的资源(餐厅资源、导游人员、摄影人员),并直接看到该资源的默认单价用于预填费用。
本次在资源服务新增 3 个统一前缀的资源下拉选项查询接口,供管理后台核单页消费。接口只返回"识别资源 + 展示名 + 价格"所需的最小字段集合,与订单侧的人员分配staffAssignment无任何关联。
---
## 2. 变更清单
| # | 方法 | 路径 | 说明 |
|---|------|------|------|
| 1 | GET | `/admin/resource-options/restaurants` | 分页查询餐厅资源选项 |
| 2 | GET | `/admin/resource-options/guides` | 分页查询导游资源选项(固定 STAFF_TYPE=GUIDE |
| 3 | GET | `/admin/resource-options/photographers` | 分页查询摄影资源选项(固定 STAFF_TYPE=PHOTOGRAPHER |
三个接口均为**新增**,无既有接口被修改或删除。
---
## 3. 接口详情
| 项 | 说明 |
|---|------|
| 使用场景 | 管理后台核单录入费用时,下拉选择餐厅 / 导游 / 摄影资源 |
| 认证 | 需要登录态,请求头携带 `Authorization: Bearer <token>`,未携带返回 401 |
| 幂等性 | GET 查询接口,天然幂等,可安全重试 |
| 限流 | 无业务级限流,受网关通用限流约束 |
| 分页 | 三个接口均为分页查询,返回统一分页结构 `PageResult` |
---
## 4. 接口入参
### 4.1 GET /admin/resource-options/restaurants
全部为 Query 参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| pageNo | Integer | 是 | 页码,从 1 开始 |
| pageSize | Integer | 是 | 每页条数 |
| keyword | String | 否 | 餐厅名称关键词,模糊匹配;服务端自动 trim,纯空白等价于不传 |
| city | String | 否 | 城市,精确匹配 |
### 4.2 GET /admin/resource-options/guides
全部为 Query 参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| pageNo | Integer | 是 | 页码,从 1 开始 |
| pageSize | Integer | 是 | 每页条数 |
| serviceDate | LocalDate | **是** | 服务日期,格式 `yyyy-MM-dd`;用于匹配当日人员价格,缺失返回 400 |
| keyword | String | 否 | 人员姓名关键词,模糊匹配;服务端自动 trim,纯空白等价于不传 |
注:服务端固定按 `STAFF_TYPE=GUIDE` 过滤,**不含助理导游**,调用方无需也不支持传 staffType。
### 4.3 GET /admin/resource-options/photographers
入参与 guides 完全一致serviceDate 必填),服务端固定按 `STAFF_TYPE=PHOTOGRAPHER` 过滤。
---
## 5. 出参字段
统一响应包装:`Result<PageResult<T>>``code=200` 表示成功。
实测响应顶层字段:`{ code, message, data, traceId, success }`(注意是 `message` 不是 `msg`);其中分页数据在 `data` 内,字段为 `{ records, total, page, pageSize }`(注意列表字段是 `records` 不是 `list`)。
### 5.1 餐厅资源选项 RestaurantResourceOptionRespVO
| 字段 | 类型 | 说明 |
|------|------|------|
| resourceId | String | 餐厅资源 ID,雪花 ID 序列化为字符串(防 JS 精度丢失)。**仅用于识别资源,不是订单侧人员分配 ID** |
| resourceName | String | 餐厅名称 |
| city | String \| null | 城市。取值规则cityName 优先、city 回退,两者均空白时为 null |
| settleType | null | 固定 null餐厅无结算方式概念 |
| settleTypeName | null | 固定 null |
| pricePerPerson | BigDecimal | 餐厅人均价。**餐厅唯一的价格来源**(不是日期价) |
| defaultUnitPrice | BigDecimal \| null | 默认单价,恒等于人均价;未配置人均价时为 null |
| priceConfigured | Boolean | 是否已配置人均价 |
### 5.2 人员资源选项 StaffResourceOptionRespVOguides / photographers 共用)
| 字段 | 类型 | 说明 |
|------|------|------|
| resourceId | String | staff 表 staff_id,雪花 ID 序列化为字符串(防 JS 精度丢失)。**仅用于识别资源,明确不是 staffAssignmentId** |
| resourceName | String | 人员姓名 |
| staffType | String | 人员类型编码:`GUIDE` / `PHOTOGRAPHER` |
| staffTypeName | String | 人员类型名称:`导游` / `摄影师` |
| settleType | String \| null | 结算方式编码:`cash` / `sign` / `company`;空白时为 null;未知编码原样保留返回 |
| settleTypeName | String \| null | 结算方式名称:`现付` / `签单` / `公司付款`;编码为空或未知时为 null |
| serviceDate | LocalDate | 服务日期,即入参 serviceDate,是共享人员类型价格的匹配日期 |
| protocolPrice | BigDecimal \| null | 当日共享协议价 |
| settlementPrice | BigDecimal \| null | 当日共享结算价 |
| defaultUnitPrice | BigDecimal \| null | 默认单价:结算价优先、协议价回退,两者均空时为 null |
| priceConfigured | Boolean | 是否至少配置了一种当日价格 |
注:响应**不暴露** calendarStatus 字段。
---
## 6. 枚举 / 数据字典
### 6.1 staffType人员类型,仅本接口出现的两个值
| 编码 | 名称staffTypeName |
|------|----------------------|
| GUIDE | 导游 |
| PHOTOGRAPHER | 摄影师 |
### 6.2 settleType结算方式
| 编码 | 名称settleTypeName |
|------|----------------------|
| cash | 现付 |
| sign | 签单 |
| company | 公司付款 |
说明:编码为空时 settleType / settleTypeName 均为 null;出现上表之外的未知编码时,settleType 原样返回、settleTypeName 为 null。
---
## 7. 错误码
| HTTP / code | 触发条件 | 返回 message |
|---|---|---|
| 400 | guides / photographers 未传 serviceDate | 服务日期不能为空 |
| 401 | 未携带有效的 Authorization 头(未登录 / token 失效) | 缺少有效的 Authorization 头 |
---
## 8. 示例
### 8.1 典型成功
请求(餐厅):
```
GET /admin/resource-options/restaurants?pageNo=1&pageSize=10&keyword=七间房&city=海拉尔
```
响应(实测):
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"resourceId": "2023382108491247617",
"resourceName": "七间房全羊馆",
"city": "海拉尔",
"settleType": null,
"settleTypeName": null,
"pricePerPerson": 120.00,
"defaultUnitPrice": 120.00,
"priceConfigured": true
}
],
"total": 1,
"page": 1,
"pageSize": 10
},
"traceId": null,
"success": true
}
```
请求(导游):
```
GET /admin/resource-options/guides?pageNo=1&pageSize=10&serviceDate=2026-08-06&keyword=李雪梅
```
响应(实测):
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"resourceId": "1002",
"resourceName": "李雪梅",
"staffType": "GUIDE",
"staffTypeName": "导游",
"settleType": "cash",
"settleTypeName": "现付",
"serviceDate": "2026-08-06",
"protocolPrice": 300.00,
"settlementPrice": 299.00,
"defaultUnitPrice": 299.00,
"priceConfigured": true
}
],
"total": 1,
"page": 1,
"pageSize": 10
},
"traceId": null,
"success": true
}
```
请求(摄影):
```
GET /admin/resource-options/photographers?pageNo=1&pageSize=10&serviceDate=2026-08-06
```
响应结构与导游一致,staffType 为 `PHOTOGRAPHER`、staffTypeName 为 `摄影师`
### 8.2 边界情况
keyword 为纯空白(自动 trim 后等价于不传,按全量分页返回):
```
GET /admin/resource-options/restaurants?pageNo=1&pageSize=10&keyword=%20%20
```
响应:正常 200,keyword 不生效,返回全量分页数据。
无任何匹配结果(空数组,注意 total=0、records 为空数组而非 null
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 10
},
"traceId": null,
"success": true
}
```
人员当日未配置任何价格defaultUnitPrice 为 null、priceConfigured 为 false
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"resourceId": "1003",
"resourceName": "张三",
"staffType": "GUIDE",
"staffTypeName": "导游",
"settleType": null,
"settleTypeName": null,
"serviceDate": "2026-08-06",
"protocolPrice": null,
"settlementPrice": null,
"defaultUnitPrice": null,
"priceConfigured": false
}
],
"total": 1,
"page": 1,
"pageSize": 10
},
"traceId": null,
"success": true
}
```
### 8.3 业务失败
guides / photographers 缺失必填的 serviceDate
```
GET /admin/resource-options/guides?pageNo=1&pageSize=10
```
响应:
```json
{
"code": 400,
"message": "服务日期不能为空",
"data": null,
"traceId": null,
"success": false
}
```
未登录调用:
```
GET /admin/resource-options/restaurants?pageNo=1&pageSize=10
(不携带 Authorization 头)
```
响应:
```json
{
"code": 401,
"message": "缺少有效的 Authorization 头",
"data": null,
"traceId": "00fef1580108417f",
"success": false
}
```
---
## 9. 业务边界
适用:
- 管理后台核单录入费用时,下拉选择餐厅 / 导游 / 摄影资源。
不适用 / 特殊边界:
- resourceId **仅用于识别资源**:餐厅接口的 resourceId 是餐厅资源 ID;人员接口的 resourceId 是 staff 表 staff_id,**明确不是订单侧的 staffAssignmentId**,不能拿它去调订单人员分配相关接口。
- 餐厅价格只有"人均价"一个来源,不存在按日期变化的价格;defaultUnitPrice 恒等于 pricePerPerson。
- 人员价格是"共享人员类型价格",按 serviceDate 匹配当日协议价 / 结算价;当日两种价格均未配置时 defaultUnitPrice 为 null、priceConfigured 为 false。
- defaultUnitPrice 取值规则固定为"结算价优先、协议价回退",调用方不需要自行二选一。
- guides 接口固定只返回 GUIDE 类型人员,**不含助理导游**;photographers 固定只返回 PHOTOGRAPHER 类型。
- 餐厅的 settleType / settleTypeName 固定为 null,不是数据缺失。
---
## 10. 修改前后对比
新增接口,无修改前后对比。
---
## 11. 影响评估 / 回滚
新增接口,无回滚影响:
- 不破坏任何既有接口与字段,无兼容性问题。
- 不要求前端同步上线;前端未接入时接口存在但不影响任何现有功能。
- 如需回滚,下线 3 个新端点即可,无数据 / 状态残留。
---
## 12. 注意事项
- 三个接口的资源 ID 均为雪花 ID 序列化后的 **String 类型**,前端请勿按 Number 处理,避免精度丢失。
- guides / photographers 的 serviceDate 是**必填**项(格式 `yyyy-MM-dd`),缺失直接 400;restaurants 无此参数。
- keyword 服务端自动 trim,前端无需预处理空白。
- 人员 settleType 可能出现上表之外的未知编码,此时 settleType 原样返回、settleTypeName 为 null,需按未知编码兜底处理。
- 响应不暴露 calendarStatus 字段。
---
## 13. 关联 / 联系人
- Issuehttps://git.1814.love:8443/wx/HL/issues/5581
- PRhttps://git.1814.love:8443/wx/HL/pulls/5583
- Commithttps://git.1814.love:8443/wx/HL/commit/258a7d9485cd2c0a8cbeca32b3b1e272d44b5376
- 后端负责人:腰苏图
- 接口已在测试服web.test.1814.love:9443实调验证通过。

查看文件

@ -1,70 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5586"
title: "投保成功出单后司机档案保险状态自动同步为有保INSURED回调兜底重绑"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端完成PR #5587 已合并 dev-v3 并部署 TEST;网关实测 bindAnnual=false 投保(档案 none→ INSURED 回调 → 档案自动兜底重绑 annual+保单号;退保→解绑→再投保闭环正常。纯后端数据同步修复,无前端契约变更。"
updated_at: "2026-08-06"
base: "dev-v3"
generated: "2026-08-06T11:58:00+08:00"
---
# 投保成功出单后司机档案保险状态自动同步为有保INSURED回调兜底重绑
> **服务**: hl-fleet-service
> **PR**: [#5587](https://git.1814.love:8443/wx/HL/pulls/5587)
> **Issue**: [#5586](https://git.1814.love:8443/wx/HL/issues/5586)
> **日期**: 2026-08-06
## 背景
协调台实测2026-08-06 11:14司机"菜单师傅"2073742230014644225真实保游投保成功出单insurance_order 2085202532040155138,INSURED,保单号 11209006600507258063,¥3,但 `fleet_driver.insurance_type` 仍 none、policy_no 空——投保成功→司机档案保险状态同步链路未闭合。#5558 已修"投保 bindAnnual 缺省绑定全年保险"(受理即绑),但档案无保(投保受理绑定缺失,或 FAILED/CANCELLED 通知先于 INSURED 乱序到达把绑定解掉后未重绑时,INSURED 回调仍被"绑定一致性/来源守卫"提前跳过。
## 变更接口
| 方法 | 路径 | 来源 | 变更 |
|---|---|---|---|
| `POST` | `/internal/fleet/drivers/insurance/annual-policy-status` | `InternalDriverInsuranceController`fleet | 语义变更INSURED 通知在 CAS 补填未命中且档案无保none/空)时**兜底重绑年保**type=annual+保单号+保费+保障起止+insurance_order_id+source=baoyou |
字段结构不变,仅通知处理语义调整。**行为矩阵**
| 档案状态 | INSURED 通知 | FAILED/CANCELLED 通知 |
|---|---|---|
| 绑该单annual+baoyou+orderId 匹配) | CAS 补填保单号(原行为) | CAS 解绑回 none原行为 |
| **档案无保none/空,orderId=null** | **兜底重绑年保(#5586 新增)** | 解绑 CAS 短路affectedRows=0,静默 |
| 绑别的单 / perTrip / manual 台账年保 | 跳过(不覆盖换绑与手动语义) | 跳过 |
## 前端/调用方动作
- **无需前端改动**(纯后端数据同步修复,接口字段不变)。
- 司机详情"投保"后刷新即可看到档案有保(保险 Tab 展示源=档案 insurance 字段 + 保单列表)。
## 验证证据
- 定向测试DriverServiceTest 147/147+3档案 none+INSURED→兜底重绑 / perTrip 档案→不动 / 缺保障起止→跳过);既有守卫测试(绑定不一致/source 非 baoyou/FAILED 解绑/INSURED 补填保持通过;DriverInsuranceServiceTest 25/25、DriverPolicyStatusOutboxEffectServiceTest 2/2。
- fleet 全量 `mvn -pl hl-fleet-service -am verify`BUILD SUCCESS3168 tests,0 failures,0 errors,4 skipped,含 spotless
- 网关验证TEST,司机=菜单师傅):① bindAnnual=true 投保→受理即绑 annual→INSURED 回调补填保单号;② 退保→保单 CANCELLED→档案解绑回 none;③ **bindAnnual=false 投保(档案保持 none→ INSURED 回调→档案自动兜底重绑**type=annual、policyNo=11209006600507256613、orderId=2085211341932400641、source=baoyou;④ 司机详情"有保"显示与 insurance_order 一致。
- 历史数据批量同步 SOPinternal 端点幂等重放 INSURED 通知(档案 none 自动兜底重绑);本环境 Nacos 断连期间无法经网关直达 internal 端点403 属正常,Feign LB 直连),重放由 Nacos 可达环境执行。
## 关联/联系人
### 链接
- [后端工单 #5586](https://git.1814.love:8443/wx/HL/issues/5586)
- [后端 PR #5587](https://git.1814.love:8443/wx/HL/pulls/5587)
- Merge commit: [df0c5d2c7](https://git.1814.love:8443/wx/HL/commit/df0c5d2c7029f19eae85c39ad9c04223faeffc5e)
- 关联:[#5558 投保缺省绑定全年保险](https://git.1814.love:8443/wx/HL/issues/5558)、[#3760 保游年险绑定](https://git.1814.love:8443/wx/HL/issues/3760)
### 联系人
- **后端负责人**: @wx
- **前端消费**: 无需改动

查看文件

@ -1,69 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5588"
title: "司机/车辆占用缓存态对账任务修正脏busy并释放过期holding占用"
consumer: "admin"
author: "wx(GIT)"
change_type: "新增接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "后端完成PR #5591 已合并 dev-v3 并部署 TEST,网关验证 3/3 通过。新增 internal 对账端点Quartz 触发),无前端 API 变化。"
updated_at: "2026-08-06"
base: "dev-v3"
generated: "2026-08-06T13:10:00+08:00"
---
# 司机/车辆占用缓存态对账任务修正脏busy并释放过期holding占用
> 后端完成PR #5591 已合并 dev-v3 并部署 TEST,网关验证 3/3 通过。
## 关联 / 联系人
### 链接
- **Issue**: [#5588](https://git.1814.love:8443/wx/HL/issues/5588)
- **PR**: [#5591](https://git.1814.love:8443/wx/HL/pulls/5591)
- **Merge commit**: [deecd62b3](https://git.1814.love:8443/wx/HL/commit/deecd62b3)
### 联系人
- **后端负责人**: @wx
## 背景
协调台 E2E 实证:派单发送被"尚未通过候选校验"拦截,根因是 driver_status/vehicle_status 未释放回 idle——**16 个 busy 司机中 6 个无 active 占用37%),车辆同样**,可派资源被大面积误锁。
代码链路核对结论取消派单释放占用的既有链路publishCanceledEvents → OCCUPANCY_RECOMPUTE outbox → hasActiveAssignmentBy* 反算 → applyOccupancy 守卫已覆盖全部取消入口。TEST 库脏 busy 实际根因:
1. **5 个司机从未有派单却 busy**(外部批量写入缓存列,无事件链路可修复)
2. **holding 单服务日自然过期后无事件触发反算**(如乌力吉/蒙C04E0408-03~05 holding 单 08-05 结束后占用永久残留)——`AssignmentCompleteJob` 只完结 assigned,不处理过期 holding
## 变更接口
| 方法 | 路径 | 来源 | 说明 |
|---|---|---|---|
| `POST` | `/internal/fleet/jobs/occupancy-reconcile/run` | `FleetJobInternalController`fleet | **新增** internal 端点,由 hl-user-service Quartzsys_job单点触发;返回本轮写入的 OCCUPANCY_RECOMPUTE 意图数 |
`/internal/*` 经 InternalAuthInterceptor 校验 X-Internal-Token,走内网 Feign LB 直连不经网关admin token 直 curl 403 属正常)。**无前端 API 变化**。
## 行为变化
新增 `OccupancyReconcileJob`(占用缓存态对账):
- **对账口径**:以 active 派单holding/assigned 且 end_date >= today为唯一真相;busy 但无 active 占用的司机/车辆为脏态
- **执行方式**:扫 busy 司机/车辆 + 过期 holding 行资源,逐一写 OCCUPANCY_RECOMPUTE 意图Outbox,由既有处理器在资源锁内反算并按守卫写回 busy/idle
- **安全性**不改派单行、不直接改缓存列、不绕过业务守卫rest/pending/maint 人工态不被覆盖);幂等(无 active 占用才回 idle;单资源失败不反噬整轮
- **附带修复**:过期 holding服务日已过但从未产生事件行资源的占用一并释放
## 前端/调用方动作
无。对账由 sys_job 定时触发(建议每日一次),无需前端配合。
## 验证证据
- 定向测试OccupancyReconcileJobTest 4/4;AssignmentServiceTest 372、AssignmentCandidateServiceTest 45 全过
- fleet verify **3184 项**(含 MySQL 8.0.33 集成0 失败;spotless 通过
- 网关验证TEST,12:58对账前脏 busy 司机=6 车辆=5;触发对账端点32 条 recompute 意图全 SUCCESS;对账后脏 busy **归零**(原 6 司机 5 车辆全部 idle;候选复验朝鲁门有 active 占用)仍 busy + available=falseASSIGNMENT_CONFLICT,**未被误释放**

查看文件

@ -1,51 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "5589"
title: "HOLD 通知派车组不存在 500 修复(全程槽占位消费组 id 回填 + 单行组查询回退)"
consumer: "admin"
change_type: "修改接口"
author: "wx(GIT)"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: "v2.1"
verified_at: "2026-08-06"
status_note: "后端完成PR #5590 已合并 dev-v3 并部署 TEST13:20 滚动 DONE;网关验证全链路 PASSbatchCreate holdMode=1 200修复前 500 派车组不存在)、组 id 回填=行 id、通知日志/outbox 正常、司机确认→确认执行 assigned。前端无需配合。"
updated_at: "2026-08-06"
base: "dev-v3"
generated: "2026-08-06T12:50:00+08:00"
---
# HOLD 通知派车组不存在 500 修复
> **服务**: hl-fleet-service
> **PR**: [#5590](https://git.1814.love:8443/wx/HL/pulls/5590)
> **Issue**: [#5589](https://git.1814.love:8443/wx/HL/issues/5589)
> **日期**: 2026-08-06
> **影响**: 🟢 **缺陷修复**,无契约变更。管理后台 HOLD 通知派车holdMode=1此前 500「HOLD 通知派车组不存在」,本次修复后全链路可用。前端无需改代码。
## 背景
#5589P1协调台实测`batchCreate` 派单 holdMode=0直接 assigned成功,holdMode=1HOLD 通知500 `IllegalStateException: HOLD 通知派车组不存在: groupId=…`
**根因**#5562 全程槽模型下,全程/单行占位行的 `assignment_group_id` 保持 NULL有意设计,占位消费升级`consumePlaceholderToActive`也未回填。HOLD 通知链路对单行组使用「组 id=行 id」回退语义`effectiveGroupId`),但组查询(`selectByAssignmentGroupId`)只按 `assignment_group_id` 匹配 → 查空 → 发送结果提交 500。
## 修复内容(内部,无接口/字段变化)
1. **占位消费回填组 id**`consumePlaceholderToActive` 消费单行占位时 `assignment_group_id=自身行 id`(与历史行迁移回填约定一致,新数据根治);
2. **组查询单行组回退**`selectByAssignmentGroupId` / `selectByAssignmentGroupIdForUpdate` 按组 id 查空时,回退 `assignment_id=groupId AND assignment_group_id IS NULL`(覆盖存量 NULL 组行,发送授权/结果提交/行程短信统一入口);
3. **CAS 回写回退**`markHoldNotificationSent` 按组更新 0 行时按行 id 回退更新(单行组 hold_sent_at 回写)。
## 受影响接口(行为修正,无字段增减)
```
POST /admin/fleet/assignments/batch-create holdMode=1 不再 500
POST /admin/fleet/assignment-hold-notifications (发送授权/重试对单行组可用)
```
## 验证
- 定向FleetAssignmentMapperTest 61/61+4组 id 回填/双回退、AssignmentHoldNotificationResultServiceTest 16/16+1全程单行组 commitSent、AssignmentServiceTest 372/372、HOLD 通知域 253/253;fleet 全量 3183 仅 2 个 MySQL 集成 Error 为 3306 端口竞争其他会话容器,空闲窗口补跑全绿MutationFence 2/2 + InsuranceCostFence 2/2 + ReleaseE 10/10
- 网关TEST 13:20 部署 → batchCreate holdMode=1 **200**(修复前 500→ 行 holding + 组 id 回填=行 id → HOLD 通知日志/outbox 正常(无行程短链产品降级为站内通知,非异常)→ 司机确认 driver_confirmed → 车务确认执行 **assigned + confirmedAt** → DB 对账全绿

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