比较提交

..
作者 SHA1 备注 提交日期
API Changelog Bot 07dbd2b595 docs(api): 补充 #4938 车务基线差异契约 2026-07-19 02:20:37 +08:00
API Changelog Bot 2bc7571953 docs(dashboard): 补充统计口径与金额字符串契约 (#5062) 2026-07-18 23:07:58 +08:00
API Changelog Bot d197a6c3aa docs(order-v3): 分离5037前后端契约边界 2026-07-18 22:07:36 +08:00
yaosutu 5e2c374ab1 新增核团核算列表详情接口变更说明 2026-07-18 18:03:32 +08:00
API Changelog Bot cb2bbc7d46 docs(house): 交接待最终确认派生待办 2026-07-18 18:02:42 +08:00
yaosutu ce5bac10e6 补充核单Step1住宿字段前端变更通知 2026-07-18 16:53:16 +08:00
yaosutu 74461331cb 推送核单Step2景区游玩项目字段前端变更 2026-07-18 16:42:29 +08:00
API Changelog Bot 575111baee docs(order): 发布核团详情司机车辆契约 (#5037) 2026-07-18 16:06:53 +08:00
yaosutu 4a88454172 新增预支审批列表接口变更说明 2026-07-18 15:47:44 +08:00
yaosutu cb3c557702 新增核单操作日志查询接口变更说明 2026-07-18 15:22:45 +08:00
API Changelog Bot a9fafb8a14 docs(fleet): 发布车务读模型前端契约 2026-07-18 11:43:16 +08:00
yaosutu e3c0209f5b 推送费用明细已收款拆分接口变更通知 2026-07-18 10:13:12 +08:00
API Changelog Bot 8319198461 docs(grassland): 记录排序与MP4修复正式发布 2026-07-17 12:00:01 +08:00
API Changelog Bot 1e47fb9069 docs(fleet): 补全 #4935 派单回调验收契约 2026-07-16 23:26:23 +08:00
API Changelog Bot 3d59972940 docs: 告知草原指南MP4物理交错修复 2026-07-16 21:27:02 +08:00
API Changelog Bot 88fb95dabb docs: 更新4907房务最终确认契约与验证证据 2026-07-16 18:53:19 +08:00
API Changelog Bot e6b9c86b0e docs: 更正草原指南倍速播放缓冲死锁 2026-07-16 16:58:06 +08:00
API Changelog Bot f852024d4a docs: 告知草原指南1x与2x播放缓冲优化 2026-07-16 16:31:51 +08:00
API Changelog Bot 98740244ca docs: 告知草原指南排序权重正序 #5011 2026-07-16 15:22:53 +08:00
API Changelog Bot da7c3808c9 docs: 标记草原指南免登录接口已正式发布 #5005 2026-07-16 14:26:59 +08:00
API Changelog Bot e8ce58c503 docs: 告知草原指南免登录只读接口 #5005 2026-07-16 10:20:43 +08:00
wx 86b3f7c799 Merge pull request 'docs(草原指南): 通知今日 hl-ui 改动已全部回退' (#11) from docs/release-grassland-folder-scope-prod-20260715 into main 2026-07-15 15:38:03 +08:00
API Changelog Bot d5941cb36c docs(草原指南): 通知今日前端改动已全部回退 2026-07-15 15:35:44 +08:00
API Changelog Bot f49d448d1d docs(fleet): 补充需求级派单完成回调契约 2026-07-15 14:52:30 +08:00
wx 41b7152d8d Merge pull request 'docs: 更正草原指南视频素材查询范围' (#10) from docs/grassland-video-current-folder-only into main 2026-07-15 14:39:19 +08:00
API Changelog Bot 43cc42cecb docs: 更正草原指南视频素材查询范围 2026-07-15 14:38:57 +08:00
wx 1cb2149e08 Merge pull request 'docs: 草原指南视频文件夹选择器显示规则' (#9) from docs/grassland-video-folder-selector-visibility into main 2026-07-15 12:09:00 +08:00
共修改 27 个文件,包含 5082 行新增和 9 行删除
@@ -0,0 +1,322 @@
# 【修改接口·管理后台】费用明细已收款拆分 (#5022)
> **PR**: #5025 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-18 00:00
## 1. 接口背景
管理后台订单费用明细需要区分展示已收订金、已收尾款、已收全款。原接口只返回 `paidAmount` 已收总额,无法直接区分不同收款类型。本次在订单费用接口响应 `data` 内新增 3 个拆分金额字段,`paidAmount` 仍表示已收总额。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 订单费用信息 | GET | `/v3/admin/order/{id}/finance` | 修改接口 | 响应 `data` 新增 `depositPaidAmount`、`balancePaidAmount`、`fullPaidAmount` |
## 3. 接口详情
### 3.1 订单费用信息
- **使用场景**: 查询单个订单的费用汇总、优惠、加价、退款、线上支付交易明细。
- **认证**: 需要管理后台登录态 JWT。
- **幂等性**: 查询接口,幂等。
- **限流**: 无新增限流规则。
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | String | 是 | 订单 ID,路径参数。示例:`2077233785174179841` |
### 4.2 请求体字段
GET 请求,无请求体。
## 5. 出参字段
### 5.1 顶层响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | Integer | 业务状态码,`200` 表示成功 |
| `message` | String | 响应消息 |
| `data` | Object | 订单费用信息 |
| `traceId` | String / null | 链路追踪 ID,可能为 `null` |
| `success` | Boolean | 请求是否成功 |
### 5.2 data 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `totalAmount` | String | 订单总金额,金额字符串,单位元 |
| `payableAmount` | String | 应付金额,金额字符串,单位元 |
| `paidAmount` | String | 已收总额,包含成功线上收款和未撤销线下收款 |
| `depositPaidAmount` | String | 新增。实际已收订金金额,成功线上订金 + 未撤销线下订金 |
| `balancePaidAmount` | String | 新增。实际已收尾款金额,成功线上尾款 + 未撤销线下尾款 |
| `fullPaidAmount` | String | 新增。实际已收全款金额,成功线上全款 + 未撤销线下全款 |
| `balanceAmount` | String | 待收余额,金额字符串,单位元 |
| `discountAmount` | String | 优惠总额,金额字符串,单位元 |
| `surchargeAmount` | String | 加价总额,金额字符串,单位元 |
| `refundAmount` | String | 已退金额,金额字符串,单位元 |
| `payments` | Array | 线上支付交易明细;仍只表示线上交易,不包含线下收款明细 |
| `discounts` | Array | 优惠明细 |
| `surcharges` | Array | 加价明细 |
### 5.3 payments 字段项
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 支付交易 ID |
| `paymentNo` | String | 支付流水号 |
| `amount` | String | 支付金额,单位元 |
| `paymentType` | String | 支付类型 |
| `status` | String | 支付状态 |
| `paidAt` | String / null | 支付成功时间,格式 `yyyy-MM-dd HH:mm:ss` |
> 本次未改变 `payments` 语义:它仍只表示线上支付交易明细。线下收款明细仍通过 `GET /v3/admin/order/{orderId}/payment/manual-receipt` 获取;线下金额已聚合进 `depositPaidAmount`、`balancePaidAmount`、`fullPaidAmount` 和 `paidAmount`。
### 5.4 discounts 字段项
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 优惠明细 ID |
| `name` | String | 优惠名称 |
| `amount` | String | 优惠金额,单位元 |
| `type` | String | 优惠类型 |
| `source` | String | 优惠来源 |
| `createdAt` | String | 创建时间,格式 `yyyy-MM-dd HH:mm:ss` |
### 5.5 surcharges 字段项
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 加价明细 ID |
| `name` | String | 加价名称 |
| `amount` | String | 加价金额,单位元 |
| `type` | String | 加价类型 |
| `createdAt` | String | 创建时间,格式 `yyyy-MM-dd HH:mm:ss` |
## 6. 枚举 / 数据字典
### 6.1 paymentType
**所属字段**: `payments[].paymentType` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `DEPOSIT` | 订金 | 订金支付 |
| `BALANCE` | 尾款 | 尾款支付 |
| `FULL` | 全款 | 全款支付 |
### 6.2 status
**所属字段**: `payments[].status` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `SUCCESS` | 支付成功 | 计入对应已收金额 |
| `PENDING` | 待支付 | 不计入对应已收金额 |
| `CLOSED` | 已关闭 | 不计入对应已收金额 |
| `FAILED` | 支付失败 | 不计入对应已收金额 |
### 6.3 type
**所属字段**: `discounts[].type`、`surcharges[].type` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `EARLY_BIRD` | 早鸟优惠 | 早鸟规则产生的优惠 |
| `MANUAL` | 手工调整 | 人工录入的优惠或加价 |
| `OTHER` | 其他 | 其他类型 |
### 6.4 source
**所属字段**: `discounts[].source` | **类型**: String
| 值 | 中文 | 说明 |
|----|------|------|
| `EARLY_BIRD_PLAN` | 早鸟方案 | 来源于早鸟优惠方案 |
| `MANUAL` | 手工录入 | 来源于人工录入 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 订单费用信息查询成功 |
| `401` | 未登录或登录失效 | 未携带有效管理后台 JWT |
| `403` | 无权限 | 当前账号无权访问该订单费用信息 |
| `404` | 订单不存在 | 路径参数 `id` 对应订单不存在 |
| `500` | 系统异常 | 服务端处理异常 |
## 8. 示例
### 8.1 典型成功
**请求**:
```http
GET /v3/admin/order/2077233785174179841/finance HTTP/1.1
Authorization: Bearer <token>
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"totalAmount": "3105.00",
"payableAmount": "2955.00",
"paidAmount": "2500.00",
"depositPaidAmount": "2000.00",
"balancePaidAmount": "500.00",
"fullPaidAmount": "0",
"balanceAmount": "455.00",
"discountAmount": "150.00",
"surchargeAmount": "0.00",
"refundAmount": "0.00",
"payments": [],
"discounts": [
{
"id": "2077233785199345666",
"name": "早鸟优惠:早鸟-小团减150(适用人群:成人/儿童/小童)",
"amount": "150.00",
"type": "EARLY_BIRD",
"source": "EARLY_BIRD_PLAN",
"createdAt": "2026-07-15 11:28:22"
}
],
"surcharges": []
},
"traceId": null,
"success": true
}
```
### 8.2 边界情况
**场景说明**: 订单暂无任何成功线上收款和未撤销线下收款时,所有已收拆分金额均返回 0 金额;明细数组可为空数组。
**请求**:
```http
GET /v3/admin/order/2077233785174179841/finance HTTP/1.1
Authorization: Bearer <token>
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"totalAmount": "3105.00",
"payableAmount": "2955.00",
"paidAmount": "0",
"depositPaidAmount": "0",
"balancePaidAmount": "0",
"fullPaidAmount": "0",
"balanceAmount": "2955.00",
"discountAmount": "150.00",
"surchargeAmount": "0.00",
"refundAmount": "0.00",
"payments": [],
"discounts": [],
"surcharges": []
},
"traceId": null,
"success": true
}
```
### 8.3 业务失败
**场景说明**: 订单 ID 不存在。
**请求**:
```http
GET /v3/admin/order/9999999999999999999/finance HTTP/1.1
Authorization: Bearer <token>
```
无请求体。
**响应**:
```json
{
"code": 404,
"message": "订单不存在",
"data": null,
"traceId": null,
"success": false
}
```
## 9. 业务边界
- **适用场景**: 管理后台查询订单费用明细时调用,订单存在且当前账号有访问权限。
- **不适用场景**: 用该接口获取线下收款明细列表;线下收款明细仍由 `GET /v3/admin/order/{orderId}/payment/manual-receipt` 返回。
- **特殊边界**: `payments` 为空不代表订单没有已收金额;可能存在未撤销线下收款,已聚合到本次新增的拆分金额和 `paidAmount`。
- **金额口径**: `paidAmount` 为已收总额;`depositPaidAmount`、`balancePaidAmount`、`fullPaidAmount` 为按收款类型拆分后的已收金额。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `data.depositPaidAmount` | 不返回 | 返回实际已收订金金额 |
| `data.balancePaidAmount` | 不返回 | 返回实际已收尾款金额 |
| `data.fullPaidAmount` | 不返回 | 返回实际已收全款金额 |
| `data.paidAmount` | 返回已收总额 | 继续返回已收总额,语义不变 |
| `data.payments` | 返回线上支付交易明细 | 继续只返回线上支付交易明细,语义不变 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 已收金额拆分 | 只能读取 `paidAmount` 总额 | 可读取订金、尾款、全款 3 类已收金额 |
| 线下收款聚合 | `paidAmount` 中包含线下收款,无法按类型拆分 | 线下收款按类型聚合进新增拆分字段和 `paidAmount` |
| 线上支付明细 | `payments` 表示线上支付交易明细 | 保持不变,仍不包含线下收款明细 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**: 否。本次只新增响应字段,未删除或改名已有字段。
- **前端是否必须同步上线**: 否。旧前端继续读取 `paidAmount` 不受影响;需要区分已收订金、已收尾款、已收全款时读取新增字段。
- **影响已有数据**: 否。历史订单按成功线上收款和未撤销线下收款聚合返回。
### 11.2 回滚方案
- **回滚方式**: 回滚 PR #5025 后,接口不再返回 3 个新增字段。
- **回滚后兼容**: 只依赖 `paidAmount` 的旧逻辑不受影响;依赖新增字段的消费方需要兼容字段缺失。
- **回滚后清理**: 无需清理前端侧数据。
## 12. 注意事项
- `depositPaidAmount`、`balancePaidAmount`、`fullPaidAmount` 是金额字符串,单位元。
- 金额为 0 时可能返回 `"0"` 或 `"0.00"`,消费方不要依赖固定小数位判断金额语义。
- `payments` 为空时仍可能存在已收金额,因为线下收款不进入 `payments`。
- 线下收款明细列表仍由 `GET /v3/admin/order/{orderId}/payment/manual-receipt` 获取。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5022](https://git.1814.love:8443/wx/HL/issues/5022)
- **PR**: [#5025](https://git.1814.love:8443/wx/HL/pulls/5025)
- **Merge commit**: [aeb2d6d24](https://git.1814.love:8443/wx/HL/commit/aeb2d6d248fcc0fe49a540bfb3b864f06bc00123)
### 13.2 联系人
- **后端负责人**: 腰苏图
@@ -0,0 +1,233 @@
# 【新增接口·管理后台】核团详情接入真实司机车辆连续服务区间(#5037)
> **Issue**: [wx/HL#5037](https://git.1814.love:8443/wx/HL/issues/5037)
>
> **PR**: [wx/HL#5048](https://git.1814.love:8443/wx/HL/pulls/5048)
>
> **服务**: `hl-order-service-v3` / `hl-fleet-service`
>
> **日期**: 2026-07-18
>
> **影响范围**: 管理后台订单中心核团详情中的司机车辆展示
> **契约边界修正**:本文只面向前端保留管理端公开接口;Fleet 与 Order v3 之间的
> internal Feign 契约已迁至 `hl-backend-changelog`,不再作为前端对接内容发布。
## 一、前端对接结论
1. 核团详情新增正式接口 `GET /v3/admin/order/{orderId}/settlement/return-detail`。
2. 前端只调用 Order v3 管理端接口;请求只有 Path 参数 `orderId`,无 Query 参数、无请求体。
3. 司机车辆数据来自 Fleet 真实派单,不再生成 mock/占位数据。
4. `driverVehicles` 永远是数组:无有效派车时返回 `[]`,不会返回 `null`。
5. Fleet 不可用时返回业务码 `584072`,前端应显示“暂时不可用/重试”,不得当成“没有派车”。
6. 同一 `vehicleId + driverId` 的连续自然日合并为一个闭区间;换车、换司机或日期断档会拆成不同项。
7. 同一订单允许多车、多司机并行,前端必须遍历完整数组,不能只展示第一项。
8. `driverId`、`vehicleId` 按字符串处理,禁止转 JavaScript `Number`。
9. `driverPhone` 已在 Fleet 出域前脱敏;前端不得尝试补全、缓存或日志打印明文手机号。
10. 房务管理员、房务组长无订单详情查看权限,调用会返回 `581045`;该入口面向有订单查看权限的管理端角色。
## 二、接口清单
| # | 接口 | 方法 | 路径 | 调用方 | 说明 |
|---|---|---|---|---|---|
| 1 | 核团详情 | GET | `/v3/admin/order/{orderId}/settlement/return-detail` | 管理后台 | 前端正式入口,返回当前有效司机车辆区间 |
## 三、管理端正式接口
### 3.1 请求
```http
GET /v3/admin/order/2000000000000000001/settlement/return-detail HTTP/1.1
Authorization: Bearer <admin-token>
```
### 3.2 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `orderId` | Path | String | 是 | 正整数 | 订单雪花 ID;按字符串传递 |
无 Query 参数、无请求体。当前有效用车需求由 Order v3 在服务端解析,前端不得缓存或拼接需求版本。
### 3.3 出参 `Result<SettlementReturnDetailRespVO>`
| 字段 | 类型 | 必定存在 | 说明 |
|---|---|---|---|
| `data.driverVehicles` | Array | 是 | 当前有效需求下的司机车辆连续服务区间;无数据固定 `[]` |
`driverVehicles[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `driverId` | String / null | 司机 ID;正常最终态有值,异常历史缺档案记录可能为 `null` |
| `driverName` | String / null | 司机姓名;档案归档时回退派单快照 |
| `driverPhone` | String / null | 脱敏手机号,例如 `138****0000` |
| `vehicleId` | String / null | 车辆 ID;按字符串处理 |
| `vehiclePlateNo` | String / null | 派单车牌快照 |
| `vehicleModelName` | String / null | 当前车型名,档案归档时回退派单快照 |
| `seatCount` | Integer / null | 当前车辆座位数 |
| `startDate` | String | 闭区间开始日期,格式 `yyyy-MM-dd` |
| `endDate` | String | 闭区间结束日期,格式 `yyyy-MM-dd` |
### 3.4 成功响应
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"driverVehicles": [
{
"driverId": "2000000000000000101",
"driverName": "测试司机",
"driverPhone": "138****0000",
"vehicleId": "2000000000000000201",
"vehiclePlateNo": "蒙A·TEST1",
"vehicleModelName": "测试七座车",
"seatCount": 7,
"startDate": "2026-07-23",
"endDate": "2026-07-28"
}
]
}
}
```
### 3.5 空业务结果
订单存在但没有当前有效用车需求、订单已取消,或当前需求没有 `assigned/completed` 最终派车时:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"driverVehicles": []
}
}
```
前端空态判断只能使用 `driverVehicles.length === 0`,不要判断 `data == null`。
## 四、聚合口径
### 4.1 纳入与排除
| Fleet 派单状态 | 是否展示 | 说明 |
|---|---|---|
| `assigned` | 是 | 已形成最终司机车辆关系 |
| `completed` | 是 | 已形成并完成的最终关系 |
| `unassigned` | 否 | 尚未派车 |
| `holding` | 否 | 排车/司机确认链路未最终完成 |
| `canceled` | 否 | 关系已失效 |
仅查询 Order v3 当前有效 `requirementId`。历史需求即使保留 `assigned/completed` 数据,也不会进入当前核团详情。
### 4.2 连续区间
```text
同车同司机:07-23、07-24、07-25 → 07-23 ~ 07-25(一项)
同车同司机:07-23、07-25 → 两项(日期断档)
同车换司机或同司机换车 → 分项
同日多车并行 → 全部返回
```
`startDate/endDate` 都包含当天。前端不得自行补日期、重算关系或按姓名/车牌合并。
## 五、错误码与前端行为
| code | 含义 | 前端处理 |
|---|---|---|
| `200` | 查询成功 | 渲染完整 `driverVehicles`;空数组展示空态 |
| `400` | `orderId` 非正数/参数非法 | 提示参数错误,不发起重试风暴 |
| `401` | 未登录或登录失效 | 走统一登录失效处理 |
| `581007` | 订单不存在 | 提示订单不存在/已删除 |
| `581045` | 房务角色无权查看订单详情 | 隐藏入口并走统一无权限提示 |
| `584072` | Fleet 司机车辆信息暂时不可用 | 保留页面上下文,显示错误与重试;禁止渲染空态 |
`584072` 与成功空数组含义不同:
```text
code=200 + driverVehicles=[] → 业务上确实没有有效司机车辆
code=584072 → 跨服务查询失败,当前状态未知
```
## 六、前端接入清单
- [ ] 核团详情改调 `GET /v3/admin/order/{orderId}/settlement/return-detail`。
- [ ] 遍历 `data.driverVehicles`,支持多车、多司机、多区间。
- [ ] 所有 ID 保持 String,不经过 `Number()`、`parseInt()`。
- [ ] 日期按后端闭区间直接展示,不自行合并或补齐断档。
- [ ] 空数组显示“暂无有效司机车辆”,不得生成 mock 卡片。
- [ ] `584072` 显示加载失败与重试,不显示空态。
- [ ] 房务角色不展示该入口。
## 七、兼容性与不影响范围
- 新增只读接口,不修改既有核单 Step1~Step6、汇总、日志或提交接口。
- 不修改派单写入、司机确认、改派、取消和完结状态机。
- 不涉及 DDL、Redis Key、MQ Topic 或网关顶级路由变更。
- 既有 `MockVehicleProvider` 仍只服务终止行程/退款金额计算,不参与本接口;金融计算链路不在本次变更范围。
- 本次未修改 `hl-ui`,需前端按本文完成接入。
## 八、测试环境验证
### 8.1 部署
```text
PR #5048 已合并:merge commit 736659cd4
Fleet:Deploy Panel 任务 9d0a2ada,8087/8187 滚动部署成功
Order v3:Deploy Panel 任务 3e4c2c49,8086/8186 滚动部署成功
测试环境随后再次滚动发布同一 dev-v3,16:00:37 完成;当前四端口均监听且 Nacos healthy
```
### 8.2 OpenAPI
```text
Order v3 /v2/api-docs?group=default:
/v3/admin/order/{orderId}/settlement/return-detail 存在
operation summary 存在,description 明确包含 584072
```
### 8.3 真实 API、DB 与日志
使用测试账号新获取的 `CUSTOMIZER` token,经 `https://api.test.1814.love:9443` 验证:
```text
主样本:Fleet DB 6 条连续日切片(2026-07-23 ~ 2026-07-28)
Fleet 8087/8187:均返回 1 个闭区间,与只读 DB 精确一致,手机号已脱敏
Order v3 网关:HTTP 200 / code 200,返回同一 1 个区间
Long ID:driverId/vehicleId 均为 JSON String
空样本:Fleet 最终态 0 行,driverVehicles=[] 且非 null
无 token:code 401
orderId=0:code 400
最终业务探测窗口:Order v3 8086/8186 均 0 ERROR/异常栈;Fleet 双实例 0 ERROR/异常栈
```
Order v3 全组 OpenAPI 生成仍会记录一条既有 Springfox 超长数字 example 的 `NumberFormatException` 栈;本次新增 operation 可正常读取,且业务 API 干净窗口无异常。该日志来自既有文档模型,不由 #5037 数据流触发。
## 九、后端验证证据
```text
最新 dev-v3 rebase 后目标回归:120/120 通过(Order v3 65、Fleet 55)
Fleet 全量 clean verify:1815/1815 通过
Fleet Spotless:502 个生产 Java 文件,0 违规
Order v3 全量:5660 tests;5 项失败均在纯上游基线独立复现,#5037 无新增失败
git diff --check、secret scan、数据流 gate_check:通过
独立盲审、API 契约审计、最终复审:无阻断项
```
## 十、回滚
- 无 DDL,代码回滚即可。
- 回滚顺序:先 Order v3,后 Fleet,避免消费者依赖不存在的提供方契约。
- 回滚后前端应兼容接口不可用,不得回退到本地 mock。
## 十一、关联链接
- Issue: [#5037](https://git.1814.love:8443/wx/HL/issues/5037)
- PR: [#5048](https://git.1814.love:8443/wx/HL/pulls/5048)
- Merge commit: [736659cd4](https://git.1814.love:8443/wx/HL/commit/736659cd4)
- 车务读模型统一说明: `60_4936_车务看板详情候选与矩阵读模型统一-管理后台.md`
@@ -0,0 +1,261 @@
# 【新增接口·管理后台】核单操作日志查询 (#5038)
> **PR**: [#5042](https://git.1814.love:8443/wx/HL/pulls/5042) | **服务**: `hl-order-service-v3` | **更新时间**: 2026-07-18 15:30
## 1. 接口背景
核单页新增独立操作日志页签,用于查看当前订单在核单流程中的关键写入动作,包括住宿核单、门票/活动核单、人员费用、补助、返还记录、主报账对账、提交核单和财务确认。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 查询核单操作日志 | GET | `/v3/admin/order/{orderId}/settlement/logs` | 新增接口 | 按订单分页返回核单专用操作日志 |
## 3. 接口详情
### 3.1 查询核单操作日志
- **使用场景**: 订单详情核单页签内展示核单操作历史。
- **认证**: 需要管理后台 JWT。
- **幂等性**: 是。GET 查询不产生写入。
- **排序**: 按 `operatedAt` 倒序;同一时间按 `id` 倒序。
- **请求体**: 无。
## 4. 接口入参
### 4.1 路径参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `orderId` | string | 是 | 订单 ID。后端按 64 位整数处理,前端按字符串保存和传递。 |
### 4.2 Query 参数
| 字段 | 类型 | 必填 | 默认值 | 校验规则 | 说明 |
|---|---|---|---|---|---|
| `page` | number | 否 | `1` | 最小 `1` | 当前页码。 |
| `pageSize` | number | 否 | `20` | `1` 到 `100` | 每页条数。 |
## 5. 出参
接口返回 `Result<PageResult<RecordVO>>`。
### 5.1 顶层响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | number | 状态码,成功为 `200`。 |
| `message` | string | 响应消息,成功为 `成功`。 |
| `data` | object | 分页数据。 |
| `traceId` | string \| null | 链路追踪 ID。 |
| `success` | boolean | 是否成功。 |
### 5.2 `data` 字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `records` | array | 操作日志记录列表。无日志时为空数组。 |
| `total` | number | 总记录数。 |
| `page` | number | 当前页码。 |
| `pageSize` | number | 每页条数。 |
### 5.3 `records[]` 字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 日志 ID。 |
| `orderId` | string | 订单 ID。 |
| `operationType` | string | 操作类型编码,见第 6 节。 |
| `operationTypeName` | string | 操作类型中文名。 |
| `operationObject` | string | 操作对象编码,见第 6 节。 |
| `operationObjectName` | string | 操作对象中文名。 |
| `content` | string | 操作内容。 |
| `operatorType` | string | 操作人类型,见第 6 节。 |
| `operatorId` | string \| null | 操作人 ID。系统自动操作时可为 `null`。 |
| `operatorName` | string | 操作人名称。 |
| `operatedAt` | string | 操作时间,格式示例 `2026-07-18 15:15:12`。 |
| `beforeSnapshot` | object \| array \| null | 改动前快照。结构随操作对象变化。 |
| `afterSnapshot` | object \| array \| null | 改动后快照。结构随操作对象变化。 |
| `changeItems` | array | 改动项列表。无差异时为空数组。 |
### 5.4 `changeItems[]` 字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `field` | string | 改动字段。非对象快照整体变化时为 `snapshot`。 |
| `fieldName` | string | 改动字段展示名。当前与 `field` 同值;整体变化时为 `整体快照`。 |
| `beforeValue` | any | 改动前值。 |
| `afterValue` | any | 改动后值。 |
## 6. 枚举 / 数据字典
### 6.1 `operationType`
| 值 | 中文 | 说明 |
|---|---|---|
| `SAVE_STEP1` | 保存住宿核单 | 保存 Step1 住宿核单明细时生成。 |
| `SAVE_STEP2` | 保存门票/活动核单 | 保存 Step2 门票/活动核单明细时生成。 |
| `SAVE_STEP3` | 保存人员费用 | 保存 Step3 人员费用核单时生成。 |
| `SAVE_STEP4` | 保存补助 | 保存 Step4 补助时生成。 |
| `ADD_REFUND` | 新增返还记录 | 新增 Step5 返还记录时生成;终止行程自动生成返还记录也使用该类型。 |
| `DELETE_REFUND` | 删除返还记录 | 删除 Step5 返还记录时生成。 |
| `SAVE_RECON` | 保存主报账对账 | 保存主报账对账信息时生成。 |
| `SUBMIT` | 提交核单 | Step6 提交核单时生成。 |
| `CONFIRM` | 财务确认 | 财务确认结算时生成。 |
### 6.2 `operationObject`
| 值 | 中文 | 说明 |
|---|---|---|
| `HOTEL` | 住宿核单 | 住宿核单相关操作对象。 |
| `TICKET` | 门票/活动核单 | 门票或活动核单相关操作对象。 |
| `STAFF_FEES` | 人员费用 | 司机、导游、摄影或其他人员费用。 |
| `SUBSIDY` | 补助 | 补助核单相关操作对象。 |
| `REFUND` | 返还记录 | 返还记录相关操作对象。 |
| `RECON` | 主报账对账 | 主报账对账相关操作对象。 |
| `SETTLEMENT` | 核单结算 | 提交核单或财务确认等整体结算操作对象。 |
### 6.3 `operatorType`
| 值 | 中文 | 说明 |
|---|---|---|
| `ADMIN` | 管理后台用户 | 管理后台人工操作。 |
| `SYSTEM` | 系统自动 | 定时任务或内部流程自动触发。 |
| `USER` | C 端用户 | 当前核单日志一般不使用,保留统一操作人类型。 |
| `MQ` | 消息回调 | 当前核单日志一般不使用,保留统一操作人类型。 |
## 7. 错误码
| code | 含义 | 触发场景 |
|---|---|---|
| `200` | 成功 | 查询成功,含无日志空列表。 |
| `400` | 参数校验失败 | `page < 1`、`pageSize < 1`、`pageSize > 100`,或 `orderId` 无法解析为整数。 |
| `401` | 未认证 | 未携带有效管理后台 JWT。 |
## 8. 示例
### 8.1 典型成功
**请求**
```http
GET /v3/admin/order/2077233855281971202/settlement/logs?page=1&pageSize=20
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "2078378034477375490",
"orderId": "2077233855281971202",
"operationType": "SAVE_STEP4",
"operationTypeName": "保存补助",
"operationObject": "SUBSIDY",
"operationObjectName": "补助",
"content": "保存补助",
"operatorType": "ADMIN",
"operatorId": "1001",
"operatorName": "admin",
"operatedAt": "2026-07-18 15:15:12",
"beforeSnapshot": [],
"afterSnapshot": [],
"changeItems": []
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"traceId": null,
"success": true
}
```
### 8.2 边界:暂无日志
**请求**
```http
GET /v3/admin/order/2076236236812345346/settlement/logs?page=1&pageSize=20
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
},
"traceId": null,
"success": true
}
```
### 8.3 异常:未登录
**请求**
```http
GET /v3/admin/order/2077233855281971202/settlement/logs?page=1&pageSize=20
```
**响应**
```json
{
"code": 401,
"message": "缺少有效 Authorization 头",
"data": null,
"traceId": null,
"success": false
}
```
## 9. 业务边界
- 该接口只查询核单专用操作日志,不返回订单状态日志、支付流水、调整订单记录或房车操作日志。
- 无核单日志时返回空分页,不视为异常。
- `beforeSnapshot`、`afterSnapshot` 的内部字段随操作对象变化,前端应把它们作为 JSON 快照展示或按对象类型做兼容解析。
- `changeItems` 只表达快照层面的差异;数组类快照整体变化时可能只返回一条 `field=snapshot` 的整体改动项。
- 查询接口本身不会生成日志;日志由对应核单写入动作生成。
## 10. 修改前后对比
新增接口,无历史接口可对比。
## 11. 影响评估 / 回滚
- **是否破坏向后兼容**: 否,新增接口。
- **前端是否必须同步上线**: 否。不接入该接口时只是不展示核单操作日志页签。
## 12. 注意事项
- 前端展示长整型 ID 时按字符串处理,避免精度丢失。
- 前端不要根据 `operationTypeName` 或 `operationObjectName` 反推状态;需要判断类型时使用编码字段。
- 空列表是合法状态,适用于未开始核单或日志功能上线前的历史订单。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5038](https://git.1814.love:8443/wx/HL/issues/5038)
- **PR**: [#5042](https://git.1814.love:8443/wx/HL/pulls/5042)
- **Merge commit**: [176405d](https://git.1814.love:8443/wx/HL/commit/176405d489df22a184e848df88a64fe104a6672f)
### 13.2 联系人
- **后端负责人**: @yaosutu
- **前端对接**: 管理后台前端
@@ -0,0 +1,319 @@
# 【新增接口·管理后台】预支审批列表接口 (#5041)
> **PR**: #5044 / #5046 | **服务**: hl-order-service-v3 + hl-user-service | **更新时间**: 2026-07-18 15:40
## 1. 接口背景
管理后台需要在财务菜单下独立查看待审批、已通过、已驳回的订单预支记录。此前预支记录只能从订单详情上下文查看,财务人员缺少全局审批列表入口。
本次新增全局分页查询接口,并新增菜单入口:
- 菜单目录:`财务管理`
- 菜单名称:`预支审批`
- 菜单路由:`advance-approvals`
- 前端组件:`finance/AdvanceApprovalList`
- 列表权限:`order:advance-approval:page`
- 通过按钮权限:`order:advance-approval:approve`
- 驳回按钮权限:`order:advance-approval:reject`
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 分页查询预支审批列表 | GET | `/v3/admin/order/advance-approvals/page` | 新增接口 | 按审批状态分页查询预支记录,并返回订单摘要字段 |
## 3. 接口详情
### 3.1 分页查询预支审批列表
- **使用场景**:财务人员进入“财务管理 / 预支审批”页面时查询预支审批列表。
- **认证**:需要管理后台 JWT。
- **权限点**:`order:advance-approval:page`。
- **幂等性**:是。该接口只读,不修改数据。
- **排序**:按 `submittedAt` 倒序,其次按 `createTime` 倒序,再按 `id` 倒序。
## 4. 接口入参
### 4.1 Query 参数
| 字段 | 类型 | 必填 | 默认值 | 说明 | 校验规则 |
|------|------|------|--------|------|----------|
| `page` | Integer | 否 | `1` | 页码 | 最小值 `1` |
| `pageSize` | Integer | 否 | `20` | 每页条数 | 最小值 `1`,最大值 `100` |
| `status` | String | 否 | `SUBMITTED` | 审批状态 | 可传 `SUBMITTED` / `APPROVED` / `REJECTED`;大小写不敏感,后端会转大写 |
| `orderId` | String | 否 | 无 | 订单 ID 精确筛选 | 雪花 ID,前端按字符串处理 |
| `keyword` | String | 否 | 无 | 订单关键字 | 模糊匹配订单号、团号、产品名 |
| `payeeName` | String | 否 | 无 | 收款人姓名 | 模糊匹配 |
| `createdByName` | String | 否 | 无 | 申请人姓名 | 模糊匹配 |
| `submittedAtFrom` | String | 否 | 无 | 提交时间开始 | 格式 `yyyy-MM-dd'T'HH:mm:ss` |
| `submittedAtTo` | String | 否 | 无 | 提交时间结束 | 格式 `yyyy-MM-dd'T'HH:mm:ss` |
### 4.2 请求体
无请求体。
## 5. 出参
### 5.1 响应结构
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
},
"traceId": "可选链路追踪ID",
"success": true
}
```
### 5.2 `data` 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `records` | Array | 当前页记录列表 |
| `total` | Integer | 总记录数 |
| `page` | Integer | 当前页码 |
| `pageSize` | Integer | 每页条数 |
### 5.3 `records[]` 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 预支 ID |
| `orderId` | String | 订单 ID |
| `payeeStaffId` | String | 收款人对应的订单人员分配 ID |
| `payeeName` | String | 收款人姓名 |
| `payeeRole` | String | 收款人角色编码 |
| `payeeRoleText` | String | 收款人角色中文 |
| `advanceType` | String | 预支类型 |
| `amount` | Number | 预支金额 |
| `purpose` | String | 用途说明 |
| `voucherUrl` | String | 凭证 URL,可为空 |
| `status` | String | 预支审批状态编码 |
| `statusText` | String | 预支审批状态中文 |
| `rejectReason` | String | 驳回原因,仅驳回记录通常有值 |
| `createdByName` | String | 申请人姓名 |
| `createTime` | String | 创建时间,格式为 ISO 日期时间 |
| `submittedAt` | String | 提交审批时间,格式为 ISO 日期时间 |
| `approvedAt` | String | 审批时间,通过或驳回后有值 |
| `approvedBy` | String | 审批人姓名 |
| `orderNo` | String | 订单号 |
| `teamNo` | String | 团号 |
| `productName` | String | 产品名称 |
| `departDate` | String | 出发日期,格式 `yyyy-MM-dd` |
| `returnDate` | String | 返程日期,格式 `yyyy-MM-dd` |
| `consultantName` | String | 定制师姓名 |
| `orderStatus` | String | 订单状态编码 |
| `orderStatusName` | String | 订单状态中文 |
| `flowStatus` | String | 流程状态编码 |
| `flowStatusName` | String | 流程状态中文 |
| `payStatus` | String | 支付状态编码 |
| `payStatusName` | String | 支付状态中文 |
| `settlementStatus` | String | 结算状态编码 |
| `settlementStatusName` | String | 结算状态中文 |
| `orderAmount` | String | 订单应收金额 |
| `paidAmount` | String | 订单已收金额 |
## 6. 枚举 / 数据字典
### 6.1 `status` / `records[].status`:预支审批状态
| 值 | 中文 | 说明 |
|----|------|------|
| `SUBMITTED` | 待审批 | 创建预支后进入待审批状态;默认查询此状态 |
| `APPROVED` | 已通过 | 财务审批通过 |
| `REJECTED` | 已驳回 | 财务审批驳回,通常带 `rejectReason` |
### 6.2 `records[].payeeRole`:收款人角色
| 值 | 中文 | 说明 |
|----|------|------|
| `DRIVER` | 司机 | 司机人员 |
| `LEADER` | 导游 | 导游人员 |
| `PHOTOGRAPHER` | 摄影师 | 摄影人员 |
| `OTHER` | 其他 | 其他人员 |
### 6.3 订单状态类字段
`orderStatus`、`flowStatus`、`payStatus`、`settlementStatus` 返回系统内已有状态编码;对应中文展示优先使用同记录里的 `orderStatusName`、`flowStatusName`、`payStatusName`、`settlementStatusName`,前端不需要硬编码中文。
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 查询成功 |
| `585005` | 预支当前状态不允许此操作 | `status` 传入值不在 `SUBMITTED` / `APPROVED` / `REJECTED` 内 |
| `400` | 参数校验失败 | `page < 1`、`pageSize < 1`、`pageSize > 100` 或日期格式不符合要求 |
| `401` | 未认证 | 未携带有效管理后台 JWT |
## 8. 示例
### 8.1 典型成功:查询待审批列表
**请求**:
```http
GET /v3/admin/order/advance-approvals/page?page=1&pageSize=10&status=SUBMITTED HTTP/1.1
Authorization: Bearer {adminToken}
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "2077000000000000001",
"orderId": "2076000000000000001",
"payeeStaffId": "2076000000000000101",
"payeeName": "张三",
"payeeRole": "DRIVER",
"payeeRoleText": "司机",
"advanceType": "ACCOMMODATION_DEPOSIT",
"amount": 500.00,
"purpose": "住宿押金",
"voucherUrl": "https://example.test/voucher/advance-001.jpg",
"status": "SUBMITTED",
"statusText": "待审批",
"rejectReason": null,
"createdByName": "腰苏图",
"createTime": "2026-07-18T10:20:30",
"submittedAt": "2026-07-18T10:20:30",
"approvedAt": null,
"approvedBy": null,
"orderNo": "HL202607180001",
"teamNo": "T202607180001",
"productName": "草原亲子 3 日游",
"departDate": "2026-07-21",
"returnDate": "2026-07-23",
"consultantName": "腰苏图",
"orderStatus": "PENDING_DEPARTURE",
"orderStatusName": "待出行",
"flowStatus": "PENDING_DEPARTURE",
"flowStatusName": "待出行",
"payStatus": "PAID",
"payStatusName": "已支付",
"settlementStatus": "NONE",
"settlementStatusName": "未核单",
"orderAmount": "3600.00",
"paidAmount": "3600.00"
}
],
"total": 1,
"page": 1,
"pageSize": 10
},
"success": true
}
```
### 8.2 边界情况:无记录
**请求**:
```http
GET /v3/admin/order/advance-approvals/page?page=1&pageSize=20&status=APPROVED&keyword=NO_MATCH_KEYWORD HTTP/1.1
Authorization: Bearer {adminToken}
```
无请求体。
**响应**:
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 20
},
"success": true
}
```
### 8.3 异常情况:非法审批状态
**请求**:
```http
GET /v3/admin/order/advance-approvals/page?page=1&pageSize=10&status=BAD_STATUS HTTP/1.1
Authorization: Bearer {adminToken}
```
无请求体。
**响应**:
```json
{
"code": 585005,
"message": "预支当前状态不允许此操作",
"data": null,
"success": false
}
```
## 9. 业务边界
- `status` 不传时默认查 `SUBMITTED`。
- `status` 支持小写或混合大小写,后端统一转大写后校验。
- `keyword` 只匹配订单号、团号、产品名。
- `payeeName` 只匹配收款人姓名。
- `createdByName` 只匹配申请人姓名。
- `submittedAtFrom` 与 `submittedAtTo` 都是闭区间过滤条件。
- 金额字段中,预支金额 `amount` 为数值;订单金额 `orderAmount`、`paidAmount` 为字符串,前端按字符串展示或转高精度数值处理。
- ID 类字段均按字符串处理,避免 JS 数字精度问题。
## 10. 修改前后对比
新增接口,无旧接口对比。
## 11. 影响评估 / 回滚
新增接口和新增菜单入口,不破坏已有接口契约。
- **是否破坏向后兼容**:否。
- **前端是否必须同步上线**:否;未接入该页面时不影响原订单详情预支能力。
- **回滚影响**:回滚后“财务管理 / 预支审批”菜单和列表接口不可用。
## 12. 注意事项
- 前端页面应挂到 `财务管理 / 预支审批`。
- 查询接口只负责列表展示,不执行审批动作。
- 审批通过、驳回按钮权限已经随菜单一起下发,按钮可以按权限点控制展示或禁用。
- 当前菜单按钮节点 `visible=false`,用于权限控制,不作为侧边栏可见菜单展示。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5041](https://git.1814.love:8443/wx/HL/issues/5041)
- **PR**: [#5044](https://git.1814.love:8443/wx/HL/pulls/5044)
- **部署修复 Issue**: [#5045](https://git.1814.love:8443/wx/HL/issues/5045)
- **部署修复 PR**: [#5046](https://git.1814.love:8443/wx/HL/pulls/5046)
- **Merge commit**: [b1ac11c03](https://git.1814.love:8443/wx/HL/commit/b1ac11c030a56a94fd8623f44b5a630fb41f2da9)
### 13.2 验证记录
- 测试服 `hl-user-service` 8081/8181 双实例部署成功。
- 测试服 `hl-order-service-v3` 8086/8186 双实例部署成功。
- 网关实调 `GET /v3/admin/order/advance-approvals/page?page=1&pageSize=10&status=SUBMITTED` 返回 `code=200`、`records=1`。
- 网关实调非法 `status=BAD_STATUS` 返回 `code=585005`。
- 网关实调 `/admin/menu/my` 已返回“预支审批”菜单和通过/驳回按钮权限。
### 13.3 联系人
- **后端负责人**: @yst
@@ -0,0 +1,359 @@
# 【修改接口·管理后台】核单 Step1 住宿成本字段 (#5043)
> **PR**: #5047 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-18 16:50
## 1. 接口背景
核单 Step1 住宿成本明细需要对齐原型里的酒店资源、房型资源、核算单价、来源和确认状态展示。现有接口保留原路径,在 `GET/PUT /v3/admin/order/{orderId}/settlement/step1` 上做兼容增强。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查询住宿核算明细 | GET | `/v3/admin/order/{orderId}/settlement/step1` | 修改接口 | `HotelItemVO` 出参新增 10 个字段 |
| 2 | 保存住宿核算明细 | PUT | `/v3/admin/order/{orderId}/settlement/step1` | 修改接口 | `HotelItemVO` 入参支持保存酒店/房型资源、单价、来源和确认状态字段 |
## 3. 接口详情
### 3.1 查询住宿核算明细
- **使用场景**:进入核单 Step1 住宿页签时查询住宿成本明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:幂等,只读查询。
- **响应结构**:`data` 为 `HotelItemVO[]` 数组。
### 3.2 保存住宿核算明细
- **使用场景**:保存核单 Step1 住宿成本明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:全量替换保存;同一份 `items` 重复提交后,以最后一次提交为准。
- **请求体兼容**:推荐使用 `{ "items": [...] }`;历史数组 body `[...]` 仍兼容。
## 4. 接口入参
### 4.1 路径参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | string | 是 | 订单 ID,长整型字符串 |
### 4.2 PUT 请求体字段
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `items` | array | 是 | 住宿成本明细行数组,全量替换保存 | 不允许为 `null` |
| `items[].id` | string | 否 | 已存在行 ID;全量替换保存时可不传 | 长整型字符串 |
| `items[].hotelAssignmentId` | string/null | 否 | 配房 assignment ID;手工行可为 `null` | 长整型字符串或 `null` |
| `items[].hotelId` | string/null | 否 | 酒店资源 ID;本次新增 | 长整型字符串或 `null` |
| `items[].roomTypeId` | string/null | 否 | 房型资源 ID;本次新增 | 长整型字符串或 `null` |
| `items[].stayDate` | string | 是 | 入住日期 | `yyyy-MM-dd`,不能早于订单出发日 |
| `items[].hotelName` | string | 是 | 酒店名称 | 1-200 字符 |
| `items[].roomType` | string/null | 否 | 房型分类或旧展示字段 | 最大 64 字符 |
| `items[].roomTypeName` | string/null | 否 | 房型/规格名称;本次新增 | 最大 64 字符 |
| `items[].roomCount` | integer | 是 | 总间数 | 正整数 |
| `items[].unitPrice` | number/null | 否 | 核算单价,单位元/间夜;本次新增;不传时按 `actualCost / roomCount` 降级计算 | `>= 0` |
| `items[].plannedCost` | number | 是 | 计划成本,单位元 | `>= 0` |
| `items[].actualCost` | number | 是 | 实际成本,单位元 | `>= 0` |
| `items[].paymentMethod` | string | 否 | 付款方式;与 `settleType` 二选一 | `SIGNED` / `COMPANY_PAID` / `CASH_PAID` |
| `items[].paymentMethodName` | string | 否 | 付款方式中文名,仅展示字段;保存时可不传;本次新增 | 最大 32 字符 |
| `items[].settleType` | string | 否 | 配房结算类型;与 `paymentMethod` 二选一 | `cash` / `sign` / `company` |
| `items[].sourceType` | string | 否 | 来源类型;本次新增;不传时按是否有 `hotelAssignmentId` 派生 | `HOUSE_ASSIGNMENT` / `MANUAL` / `TEMPLATE` |
| `items[].sourceTypeName` | string | 否 | 来源类型中文名,仅展示字段;保存时可不传;本次新增 | 最大 32 字符 |
| `items[].sourceId` | string/null | 否 | 来源业务 ID;本次新增;配房来源默认等于 `hotelAssignmentId` | 长整型字符串或 `null` |
| `items[].settlementConfirmStatus` | string | 否 | 核单确认状态;本次新增;不传默认 `CONFIRMED` | `UNCONFIRMED` / `CONFIRMED` |
| `items[].settlementConfirmStatusName` | string | 否 | 核单确认状态中文名,仅展示字段;保存时可不传;本次新增 | 最大 32 字符 |
| `items[].remark` | string/null | 否 | 备注 | 最大 500 字符 |
| `items[].voucherUrls` | array | 否 | 凭证图片 URL 数组 | 字符串数组 |
## 5. 出参
### 5.1 GET 响应字段:`HotelItemVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string/null | 核单住宿明细行 ID;首次派生未保存的行可为 `null` |
| `hotelAssignmentId` | string/null | 配房 assignment ID;手工行可为 `null` |
| `hotelId` | string/null | 酒店资源 ID;本次新增 |
| `roomTypeId` | string/null | 房型资源 ID;本次新增 |
| `stayDate` | string | 入住日期,`yyyy-MM-dd` |
| `hotelName` | string | 酒店名称 |
| `roomType` | string/null | 房型分类或旧展示字段 |
| `roomTypeName` | string/null | 房型/规格名称;本次新增 |
| `roomCount` | integer | 总间数 |
| `unitPrice` | number/null | 核算单价,单位元/间夜;本次新增 |
| `plannedCost` | number | 计划成本,单位元 |
| `actualCost` | number | 实际成本,单位元 |
| `paymentMethod` | string | 付款方式 |
| `paymentMethodName` | string/null | 付款方式中文名;本次新增 |
| `settleType` | string/null | 配房结算类型;保存草稿后可能为空 |
| `sourceType` | string | 来源类型;本次新增 |
| `sourceTypeName` | string/null | 来源类型中文名;本次新增 |
| `sourceId` | string/null | 来源业务 ID;本次新增 |
| `settlementConfirmStatus` | string | 核单确认状态;本次新增 |
| `settlementConfirmStatusName` | string/null | 核单确认状态中文名;本次新增 |
| `remark` | string/null | 备注 |
| `voucherUrls` | array | 凭证图片 URL 数组 |
### 5.2 PUT 响应字段:`SettlementHotelSaveRespVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `addedIds` | string[] | 本次保存新增的核单住宿明细行 ID 列表 |
| `updatedIds` | string[] | 本次保存更新的核单住宿明细行 ID 列表;当前全量替换语义下通常为空数组 |
| `deletedIds` | string[] | 本次保存删除的核单住宿明细行 ID 列表;当前返回通常为空数组 |
| `totalActualCost` | string | 保存后 Step1 实际成本合计,单位元 |
## 6. 枚举 / 数据字典
### 6.1 `paymentMethod`
**所属字段**:`items[].paymentMethod`、`data[].paymentMethod` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `SIGNED` | 签单 | 现场签单 |
| `COMPANY_PAID` | 公司付款 | 公司统一付款 |
| `CASH_PAID` | 现付 | 现场现金或线下现付 |
### 6.2 `settleType`
**所属字段**:`items[].settleType`、`data[].settleType` | **类型**:String | **必填**:否
| 值 | 中文 | 映射后的 `paymentMethod` |
|----|------|--------------------------|
| `cash` | 现付 | `CASH_PAID` |
| `sign` | 签单 | `SIGNED` |
| `company` | 公司付款 | `COMPANY_PAID` |
### 6.3 `sourceType`
**所属字段**:`items[].sourceType`、`data[].sourceType` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `HOUSE_ASSIGNMENT` | 配房结果 | 来自房务配房结果 |
| `MANUAL` | 手工 | 核单手工补充住宿行 |
| `TEMPLATE` | 模板 | 模板来源住宿行,当前预留 |
### 6.4 `settlementConfirmStatus`
**所属字段**:`items[].settlementConfirmStatus`、`data[].settlementConfirmStatus` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `UNCONFIRMED` | 未确认 | 核单住宿明细未确认 |
| `CONFIRMED` | 已确认 | 核单住宿明细已确认;不传时默认该值 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `584002` | 当前核单状态不允许录住宿核单 | PUT 保存时,订单不是「待核单」或「核单中」 |
| `584008` | 订单缺出发日期,无法派生 dayNumber | PUT 保存时订单出发日期为空 |
| `584009` | `stayDate` 早于订单出发日期 | PUT 保存时日期越界 |
| `584062` | 临时行必须指定付款方式 | `paymentMethod` 和 `settleType` 都为空 |
| `584064` | 配房记录 `settleType` 字典值非法 | `settleType` 不是 `cash/sign/company` |
| `100001` | 参数非法 | 字段格式不符合校验,例如枚举值不在允许范围内、金额小于 0 |
## 8. 示例
### 8.1 典型成功:GET 查询
**请求**
```http
GET /v3/admin/order/2077233855281971202/settlement/step1
Authorization: Bearer {token}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": [
{
"id": "2077328317421187001",
"hotelAssignmentId": "2077233886282088401",
"hotelId": "50001",
"roomTypeId": "51001",
"stayDate": "2026-07-18",
"hotelName": "海拉尔海棠酒店",
"roomType": "STANDARD",
"roomTypeName": "精品标间",
"roomCount": 2,
"unitPrice": 440.00,
"plannedCost": 880.00,
"actualCost": 880.00,
"paymentMethod": "CASH_PAID",
"paymentMethodName": "现付",
"settleType": "cash",
"sourceType": "HOUSE_ASSIGNMENT",
"sourceTypeName": "配房结果",
"sourceId": "2077233886282088401",
"settlementConfirmStatus": "CONFIRMED",
"settlementConfirmStatusName": "已确认",
"remark": "已核对",
"voucherUrls": []
}
]
}
```
### 8.2 边界成功:PUT 保存手工住宿行
**请求**
```http
PUT /v3/admin/order/2077233855281971202/settlement/step1
Authorization: Bearer {token}
Content-Type: application/json
```
```json
{
"items": [
{
"hotelAssignmentId": null,
"hotelId": null,
"roomTypeId": null,
"stayDate": "2026-07-18",
"hotelName": "临时补充酒店",
"roomType": "STANDARD",
"roomTypeName": "标准间",
"roomCount": 1,
"unitPrice": 300.00,
"plannedCost": 300.00,
"actualCost": 300.00,
"paymentMethod": "COMPANY_PAID",
"sourceType": "MANUAL",
"sourceId": null,
"settlementConfirmStatus": "UNCONFIRMED",
"voucherUrls": [],
"remark": "核单临时补充"
}
]
}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"addedIds": ["2078398065684692001"],
"updatedIds": [],
"deletedIds": [],
"totalActualCost": "300.00"
}
}
```
### 8.3 业务失败:缺付款方式
**请求**
```http
PUT /v3/admin/order/2077233855281971202/settlement/step1
Authorization: Bearer {token}
Content-Type: application/json
```
```json
{
"items": [
{
"hotelAssignmentId": null,
"stayDate": "2026-07-18",
"hotelName": "临时补充酒店",
"roomType": "STANDARD",
"roomCount": 1,
"plannedCost": 300.00,
"actualCost": 300.00
}
]
}
```
**响应**
```json
{
"code": 584062,
"message": "临时行(无配房关联)必须指定 paymentMethod",
"data": null,
"success": false
}
```
## 9. 业务边界
- `PUT` 是全量替换保存;前端保存时应提交页面当前完整明细列表。
- `hotelAssignmentId` 有值时通常表示配房来源;`hotelAssignmentId` 为空时通常表示手工补充行。
- `sourceType` 不传时,`hotelAssignmentId` 有值默认 `HOUSE_ASSIGNMENT`,否则默认 `MANUAL`。
- `sourceId` 不传且 `sourceType=HOUSE_ASSIGNMENT` 时,默认使用 `hotelAssignmentId`;其他来源可为 `null`。
- `unitPrice` 不传且 `roomCount > 0`、`actualCost` 有值时,返回时会按 `actualCost / roomCount` 保留 2 位小数。
- `settlementConfirmStatus` 不传时默认 `CONFIRMED`。
- `paymentMethod` 与 `settleType` 二选一;`paymentMethod` 优先,`settleType` 会映射成 `paymentMethod`。
- 订单核单状态必须是「待核单」或「核单中」才允许保存 Step1。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 修改前 | 修改后 |
|------|--------|--------|
| `hotelId` | 无 | 新增,酒店资源 ID |
| `roomTypeId` | 无 | 新增,房型资源 ID |
| `roomTypeName` | 无 | 新增,房型/规格名称 |
| `unitPrice` | 无 | 新增,核算单价 |
| `paymentMethodName` | 无 | 新增,付款方式中文名 |
| `sourceType` | 无 | 新增,来源类型 |
| `sourceTypeName` | 无 | 新增,来源类型中文名 |
| `sourceId` | 无 | 新增,来源业务 ID |
| `settlementConfirmStatus` | 无 | 新增,核单确认状态 |
| `settlementConfirmStatusName` | 无 | 新增,核单确认状态中文名 |
### 10.2 行为级对比
| 行为 | 修改前 | 修改后 |
|------|--------|--------|
| 住宿来源展示 | 只能通过 `hotelAssignmentId` 粗略判断 | 返回 `sourceType/sourceTypeName/sourceId` |
| 单价展示 | 前端只能根据总价和间数自行推算 | 返回 `unitPrice`,缺失时后端按实际成本和间数降级计算 |
| 确认状态展示 | 无独立字段 | 返回 `settlementConfirmStatus/settlementConfirmStatusName` |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:否。新增字段为兼容性新增,旧字段保留。
- **前端是否必须同步上线**:否。旧页面可继续按原字段展示;需要原型新增列时读取新增字段。
- **影响已有数据**:历史行新增字段可能为 `null`,前端需要保留空值展示逻辑。
### 11.2 回滚方案
- 回滚接口代码后,前端不要再依赖本次新增字段。
- 如果页面已使用新增列,回滚期间新增列需要降级为空态展示。
## 12. 注意事项
- `paymentMethodName`、`sourceTypeName`、`settlementConfirmStatusName` 都是展示字段,保存时可不传。
- `roomType` 是旧字段,`roomTypeName` 是本次新增的房型/规格名称;两者可能同时存在。
- `settlementConfirmStatus` 是核单明细确认状态,与房务配房确认状态不是同一个字段。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5043](https://git.1814.love:8443/wx/HL/issues/5043)
- **PR**: [#5047](https://git.1814.love:8443/wx/HL/pulls/5047)
- **Merge commit**: [0a9e83b39](https://git.1814.love:8443/wx/HL/commit/0a9e83b390d135c05b43bc18afe1b190329bf85c)
### 13.2 联系人
- **后端负责人**: @yaosutu
@@ -0,0 +1,306 @@
# 【修改接口·管理后台】核单 Step2 景区游玩项目字段 (#5049)
> **PR**: #5051 | **服务**: hl-order-service-v3 | **更新时间**: 2026-07-18 16:40
## 1. 接口背景
核单 Step2 的景区/游玩项目核算明细需要展示来源、行程天数、规格/票型、销售单价、销售小计和付款方式中文名。现有接口保留原路径,在原 `GET/PUT /v3/admin/order/{orderId}/settlement/step2` 上做兼容增强。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 查询门票/游玩项目核算明细 | GET | `/v3/admin/order/{orderId}/settlement/step2` | 修改接口 | `TicketItemVO` 出参新增 6 个字段 |
| 2 | 保存门票/游玩项目核算明细 | PUT | `/v3/admin/order/{orderId}/settlement/step2` | 修改接口 | `sourceType` 新增 `CUSTOM_ASSIGNMENT`,入参支持保存规格、销售单价、销售小计 |
## 3. 接口详情
### 3.1 查询门票/游玩项目核算明细
- **使用场景**:进入核单 Step2 景区/游玩项目页签时查询明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:幂等,只读查询。
- **响应结构**:`data` 为 `TicketItemVO[]` 数组。
### 3.2 保存门票/游玩项目核算明细
- **使用场景**:保存核单 Step2 景区/游玩项目核算明细。
- **认证**:需要管理后台 JWT。
- **幂等性**:全量替换保存;同一份 `items` 重复提交后,以最后一次提交为准。
- **请求体兼容**:推荐使用 `{ "items": [...] }`;历史数组 body `[...]` 仍兼容。
## 4. 接口入参
### 4.1 路径参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `orderId` | string | 是 | 订单 ID,长整型字符串 |
### 4.2 PUT 请求体字段
| 字段 | 类型 | 必填 | 说明 | 校验规则 |
|------|------|------|------|----------|
| `items` | array | 是 | 门票/游玩项目明细行数组,全量替换保存 | 不允许为 `null` |
| `items[].id` | string | 否 | 已存在行 ID;全量替换保存时可不传 | 长整型字符串 |
| `items[].sourceType` | string | 是 | 来源类型 | `SCENIC_ASSIGNMENT` / `ACTIVITY_ASSIGNMENT` / `CUSTOM_ASSIGNMENT` |
| `items[].sourceTypeName` | string | 否 | 来源类型中文名,仅展示字段;保存时可不传 | 最大 32 字符 |
| `items[].scenicAssignmentId` | string | 否 | 来源 assignment ID;手工项目传 `null` | 长整型字符串或 `null` |
| `items[].dayNumber` | integer | 否 | 行程第几天;保存时以后端根据 `dayDate` 计算后的值为准 | 从 1 开始 |
| `items[].dayDate` | string | 是 | 行程日期 | `yyyy-MM-dd`,不能早于订单出发日 |
| `items[].scenicName` | string | 是 | 景区/游玩项目名称 | 1-200 字符 |
| `items[].specName` | string | 否 | 规格/票型名称 | 最大 128 字符 |
| `items[].ticketCount` | integer | 是 | 实际购票数量 | 建议非负整数 |
| `items[].ticketUnitPrice` | number | 否 | 参考成本单价,单位元 | 小数 |
| `items[].sellPrice` | number | 否 | 客户成交单价,单位元 | `>= 0` |
| `items[].totalAmount` | number | 否 | 客户成交小计,单位元;为空且有 `sellPrice` 时后端按 `sellPrice * ticketCount` 降级计算 | `>= 0` |
| `items[].plannedCost` | number | 是 | 计划成本,单位元 | `>= 0` |
| `items[].actualCost` | number | 是 | 实际成本,单位元 | `>= 0` |
| `items[].paymentMethod` | string | 否 | 付款方式;为空时默认 `COMPANY_PAID` | `SIGNED` / `COMPANY_PAID` / `CASH_PAID` |
| `items[].paymentMethodName` | string | 否 | 付款方式中文名,仅展示字段;保存时可不传 | 最大 32 字符 |
| `items[].voucherUrls` | array | 否 | 凭证图片 URL 数组 | 字符串数组 |
| `items[].remark` | string | 否 | 备注 | 最大 500 字符 |
## 5. 出参
### 5.1 GET 响应字段:`TicketItemVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 核单明细行 ID |
| `sourceType` | string | 来源类型 |
| `sourceTypeName` | string | 来源类型中文名;本次新增 |
| `scenicAssignmentId` | string/null | 来源 assignment ID;手工项目为 `null` |
| `dayNumber` | integer/null | 行程第几天;本次新增 |
| `dayDate` | string | 行程日期,`yyyy-MM-dd` |
| `scenicName` | string | 景区/游玩项目名称 |
| `specName` | string/null | 规格/票型名称;本次新增 |
| `ticketCount` | integer | 实际购票数量 |
| `ticketUnitPrice` | number/null | 参考成本单价 |
| `sellPrice` | number/null | 客户成交单价;本次新增 |
| `totalAmount` | number/null | 客户成交小计;本次新增 |
| `plannedCost` | number | 计划成本 |
| `actualCost` | number | 实际成本 |
| `paymentMethod` | string | 付款方式 |
| `paymentMethodName` | string/null | 付款方式中文名;本次新增 |
| `voucherUrls` | array | 凭证图片 URL 数组 |
| `remark` | string/null | 备注 |
### 5.2 PUT 响应字段:`SettlementTicketSaveRespVO`
| 字段 | 类型 | 说明 |
|------|------|------|
| `addedIds` | string[] | 本次保存新增的核单明细行 ID 列表 |
| `updatedIds` | string[] | 本次保存更新的核单明细行 ID 列表;当前全量替换语义下通常为空数组 |
| `deletedIds` | string[] | 本次保存删除的核单明细行 ID 列表;当前返回通常为空数组 |
| `totalActualCost` | string | 保存后 Step2 实际成本合计,单位元 |
## 6. 枚举 / 数据字典
### 6.1 `sourceType`
**所属字段**:`items[].sourceType`、`data[].sourceType` | **类型**:String | **必填**:是
| 值 | 中文 | 说明 |
|----|------|------|
| `SCENIC_ASSIGNMENT` | 景区 | 景区来源行 |
| `ACTIVITY_ASSIGNMENT` | 游玩项目 | 游玩项目来源行 |
| `CUSTOM_ASSIGNMENT` | 手工项目 | 本次新增;核单手工补充行,`scenicAssignmentId` 可为 `null` |
### 6.2 `paymentMethod`
**所属字段**:`items[].paymentMethod`、`data[].paymentMethod` | **类型**:String | **必填**:否
| 值 | 中文 | 说明 |
|----|------|------|
| `SIGNED` | 签单 | 现场签单 |
| `COMPANY_PAID` | 公司付款 | 公司统一付款;未传 `paymentMethod` 时默认该值 |
| `CASH_PAID` | 现付 | 现场现金/线下现付 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| `584011` | 当前核单状态不允许录门票核单 | PUT 保存时,订单不是「待核单」或「核单中」 |
| `100001` | 参数非法 | 字段格式不符合校验,例如 `sourceType` 不在允许枚举内、金额小于 0 |
## 8. 示例
### 8.1 典型成功:GET 查询
**请求**
```http
GET /v3/admin/order/2077233855281971202/settlement/step2
Authorization: Bearer {token}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": [
{
"id": "2077328317421187073",
"sourceType": "SCENIC_ASSIGNMENT",
"sourceTypeName": "景区",
"scenicAssignmentId": "2077233886282088450",
"dayNumber": 5,
"dayDate": "2026-07-18",
"scenicName": "呼和诺尔草原旅游区",
"specName": null,
"ticketCount": 1,
"ticketUnitPrice": 59.00,
"sellPrice": null,
"totalAmount": null,
"plannedCost": 59.00,
"actualCost": 59.00,
"paymentMethod": "COMPANY_PAID",
"paymentMethodName": "公司付款",
"voucherUrls": [],
"remark": null
}
]
}
```
### 8.2 边界成功:PUT 保存手工项目
**请求**
```http
PUT /v3/admin/order/2077233855281971202/settlement/step2
Authorization: Bearer {token}
Content-Type: application/json
```
```json
{
"items": [
{
"sourceType": "CUSTOM_ASSIGNMENT",
"scenicAssignmentId": null,
"dayDate": "2026-07-18",
"scenicName": "临时补充游玩项目",
"specName": "成人票",
"ticketCount": 2,
"ticketUnitPrice": 12.34,
"sellPrice": 56.78,
"totalAmount": 113.56,
"plannedCost": 24.68,
"actualCost": 24.68,
"paymentMethod": "COMPANY_PAID",
"voucherUrls": [],
"remark": "核单临时补充"
}
]
}
```
**响应**
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"addedIds": ["2078398065684692994"],
"updatedIds": [],
"deletedIds": [],
"totalActualCost": "24.68"
}
}
```
### 8.3 业务失败:已核单订单禁止保存
**请求**
```http
PUT /v3/admin/order/2077233886248534018/settlement/step2
Authorization: Bearer {token}
Content-Type: application/json
```
```json
{
"items": []
}
```
**响应**
```json
{
"code": 584011,
"message": "当前核单状态为「已核单」,不允许录门票核单,必须为「待核单」或「核单中」",
"data": null,
"success": false
}
```
## 9. 业务边界
- `PUT` 是全量替换保存;前端保存时应提交页面当前完整明细列表。
- `CUSTOM_ASSIGNMENT` 表示核单手工补充项目,`scenicAssignmentId` 可以为 `null`。
- `dayNumber` 保存时以后端根据 `dayDate` 和订单出发日计算的结果为准。
- `totalAmount` 为空且 `sellPrice` 有值时,后端会按 `sellPrice * ticketCount` 降级计算。
- 未传 `paymentMethod` 时,后端默认使用 `COMPANY_PAID`。
- 订单核单状态必须是「待核单」或「核单中」才允许保存 Step2。
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 修改前 | 修改后 |
|------|--------|--------|
| `sourceType` | `SCENIC_ASSIGNMENT` / `ACTIVITY_ASSIGNMENT` | 新增 `CUSTOM_ASSIGNMENT` |
| `sourceTypeName` | 无 | 新增,返回来源中文名 |
| `dayNumber` | 无 | 新增,返回行程第几天 |
| `specName` | 无 | 新增,返回/保存规格或票型名称 |
| `sellPrice` | 无 | 新增,返回/保存客户成交单价 |
| `totalAmount` | 无 | 新增,返回/保存客户成交小计 |
| `paymentMethodName` | 无 | 新增,返回付款方式中文名 |
### 10.2 行为级对比
| 行为 | 修改前 | 修改后 |
|------|--------|--------|
| 手工补充项目来源 | 只能用既有来源类型兜底表达 | 可明确传 `CUSTOM_ASSIGNMENT` |
| 手工项目 assignment ID | 前端容易误以为必须有来源 ID | `CUSTOM_ASSIGNMENT` 下 `scenicAssignmentId` 可为 `null` |
| 销售金额展示 | 只能展示成本字段 | 可展示 `sellPrice` / `totalAmount` |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:否。新增字段为兼容性新增;旧字段继续保留。
- **前端是否必须同步上线**:否。旧页面可继续按原字段展示;需要原型新增列时再读取新字段。
- **影响已有数据**:不需要前端做数据迁移;历史行新字段可能为 `null`。
### 11.2 回滚方案
- 回滚接口代码后,前端不要再依赖 `CUSTOM_ASSIGNMENT` 和新增字段。
- 如已保存手工项目,回滚前应确认旧版本是否能识别该来源类型。
## 12. 注意事项
- 前端不要把 `sourceTypeName`、`paymentMethodName` 当作提交必填项;它们是展示字段。
- 前端保存时建议保留并回传用户编辑后的 `specName`、`sellPrice`、`totalAmount`。
- 已核单订单保存 Step2 会返回 `584011`,这不是接口异常。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5049](https://git.1814.love:8443/wx/HL/issues/5049)
- **PR**: [#5051](https://git.1814.love:8443/wx/HL/pulls/5051)
- **Merge commit**: [d60324fde](https://git.1814.love:8443/wx/HL/commit/d60324fde228b50c7ba70d1f39a641e851dc351d)
### 13.2 联系人
- **后端负责人**: @yst
@@ -0,0 +1,752 @@
# 【新增/修改接口·管理后台】核团核算列表与详情聚合 (#5055)
> **PR**: [#5058](https://git.1814.love:8443/wx/HL/pulls/5058) | **服务**: `hl-order-service-v3` | **更新时间**: 2026-07-18 18:20
## 1. 接口背景
核团核算页面需要一个常规产品订单入口列表,并且详情页需要一次性拿到订单信息、出行人、司机车辆、应收构成、线上支付和线下收款记录。此前详情接口字段不完整,前端需要自行合并多个接口;本次把核团入口和详情聚合契约收敛到订单服务接口。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 核团核算任务列表 | GET | `/v3/admin/order-settlement/tasks` | 新增接口 | 查询常规 CORE 产品、无团期批次、已完成订单的核团任务列表 |
| 2 | 查询核团详情 | GET | `/v3/admin/order/{orderId}/settlement/return-detail` | 修改接口 | 扩展订单信息、出行人中文枚举、司机车辆集合、应收汇总/明细、支付+线下收款合并记录 |
## 3. 接口详情
### 3.1 核团核算任务列表
- **使用场景**: 核团核算页的常规产品列表。
- **认证**: 需要管理后台 JWT。
- **幂等性**: 是,只读查询。
- **请求体**: 无。
- **响应结构**: `Result<PageResult<SettlementTaskRespVO>>`。
#### 3.1.1 Query 入参
| 字段 | 类型 | 必填 | 默认值 | 校验规则 | 说明 |
|---|---|---|---|---|---|
| `page` | number | 否 | `1` | 最小 `1` | 当前页码 |
| `pageSize` | number | 否 | `20` | `1` 到 `100` | 每页条数 |
| `keyword` | string | 否 | - | - | 关键词,按订单号、团号、产品名模糊查询 |
| `departureDateFrom` | string | 否 | - | `yyyy-MM-dd` | 出发日期开始 |
| `departureDateTo` | string | 否 | - | `yyyy-MM-dd` | 出发日期结束 |
| `settlementStatus` | string | 否 | - | `NONE` / `PENDING` / `COMPLETED` | 核算状态筛选 |
#### 3.1.2 响应字段
顶层统一响应:
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | number | 成功为 `200` |
| `message` | string | 成功为 `成功` |
| `data` | object | 分页数据 |
| `traceId` | string/null | 链路追踪 ID |
| `success` | boolean | 是否成功 |
`data` 分页字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `records` | array | 核团任务行列表,空结果返回 `[]` |
| `total` | number | 总记录数 |
| `page` | number | 当前页码 |
| `pageSize` | number | 每页条数 |
`records[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderId` | string | 订单 ID |
| `orderNo` | string | 订单号 |
| `teamNo` | string/null | 团号 |
| `productName` | string | 产品名称 |
| `departureDate` | string/null | 出发日期,格式 `yyyy-MM-dd` |
| `returnDate` | string/null | 返团日期,格式 `yyyy-MM-dd` |
| `peopleCount` | number | 出行人总数 |
| `peopleSummary` | string | 人数文案,如 `2成人2儿童`、`2成人1婴儿`、`0人` |
| `systemBalanceAmount` | number | 系统计算待收尾款 |
| `settlementStatus` | string | 核算状态 |
| `settlementStatusName` | string | 核算状态中文名 |
列表不返回 `routeName`、`driverName`、`vehiclePlateNo`。
#### 3.1.3 示例
典型成功:
**请求**
```http
GET /v3/admin/order-settlement/tasks?page=1&pageSize=10&settlementStatus=COMPLETED
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"orderId": "2077233886248534018",
"orderNo": "HL202607180001",
"teamNo": "T20260718001",
"productName": "呼伦贝尔草原 5 日游",
"departureDate": "2026-07-20",
"returnDate": "2026-07-24",
"peopleCount": 3,
"peopleSummary": "2成人1婴儿",
"systemBalanceAmount": 0.00,
"settlementStatus": "COMPLETED",
"settlementStatusName": "已结算"
}
],
"total": 12,
"page": 1,
"pageSize": 10
},
"traceId": null,
"success": true
}
```
空结果:
**请求**
```http
GET /v3/admin/order-settlement/tasks?page=1&pageSize=10&keyword=不存在的订单
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [],
"total": 0,
"page": 1,
"pageSize": 10
},
"traceId": null,
"success": true
}
```
### 3.2 查询核团详情
- **使用场景**: 点击核团任务后进入返团核算详情页。
- **认证**: 需要管理后台 JWT。
- **幂等性**: 是,只读查询。
- **请求体**: 无。
- **响应结构**: `Result<SettlementReturnDetailRespVO>`。
#### 3.2.1 路径入参
| 字段 | 类型 | 必填 | 校验规则 | 说明 |
|---|---|---|---|---|
| `orderId` | string | 是 | 长整型字符串,必须大于 `0` | 订单 ID |
#### 3.2.2 响应字段
`data` 顶层字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderInfo` | object/null | 订单信息;取消订单可能为空 |
| `travelers` | array | 出行人列表,敏感字段已脱敏 |
| `driverVehicles` | array | 当前有效司机车辆连续服务区间;无有效派车返回 `[]` |
| `receivableSummary` | object/null | 应收汇总 |
| `receivableItems` | array | 应收计算明细 |
| `collectionSummary` | object/null | 收款汇总 |
| `collectionRecords` | array | 线上支付和线下收款合并记录 |
`orderInfo` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderId` | string | 订单 ID |
| `orderNo` | string | 订单号 |
| `teamNo` | string/null | 团号 |
| `productName` | string | 产品名称 |
| `departureDate` | string/null | 出发日期,格式 `yyyy-MM-dd` |
| `returnDate` | string/null | 返团日期,格式 `yyyy-MM-dd` |
| `peopleCount` | number | 出行人总数 |
| `adultCount` | number | 成人数 |
| `childCount` | number | 儿童数 |
| `youngChildCount` | number | 幼童数 |
| `babyCount` | number | 婴儿数 |
| `peopleSummary` | string | 人数文案 |
| `consultantId` | string/null | 定制师 ID |
| `consultantName` | string/null | 定制师姓名 |
| `houseStaffId` | string/null | 房务人员 ID |
| `houseStaffName` | string/null | 房务人员姓名 |
| `fleetStaffId` | string/null | 车务人员 ID |
| `fleetStaffName` | string/null | 车务人员姓名 |
| `settlementStatus` | string | 核算状态 |
| `settlementStatusName` | string | 核算状态中文名 |
`travelers[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `travelerId` | string | 出行人 ID |
| `travelerName` | string | 出行人姓名 |
| `travelerType` | string | 出行人类型 |
| `travelerTypeName` | string | 出行人类型中文名 |
| `idType` | string/null | 证件类型 |
| `idTypeName` | string/null | 证件类型中文名 |
| `phone` | string/null | 脱敏手机号 |
| `idCardNo` | string/null | 脱敏证件号 |
`driverVehicles[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `driverId` | string/null | 司机 ID |
| `driverName` | string/null | 司机姓名 |
| `driverPhone` | string/null | 脱敏司机手机号 |
| `vehicleId` | string/null | 车辆 ID |
| `vehiclePlateNo` | string/null | 车牌号 |
| `vehicleModelName` | string/null | 车型名称 |
| `seatCount` | number/null | 座位数 |
| `startDate` | string | 连续服务开始日期,格式 `yyyy-MM-dd` |
| `endDate` | string | 连续服务结束日期,格式 `yyyy-MM-dd` |
`receivableSummary` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderAmount` | number | 订单基础金额 |
| `surchargeAmount` | number | 附加费金额 |
| `discountAmount` | number | 优惠金额 |
| `payableAmount` | number | 应收总额 |
| `formulaText` | string | 应收总额公式文案,固定为 `订单金额 + 附加费 - 优惠 = 应收总额` |
`receivableItems[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `sourceRecordId` | string/null | 来源记录 ID |
| `itemType` | string | 应收项类型 |
| `itemTypeName` | string | 应收项类型中文名 |
| `itemCode` | string/null | 应收项编码 |
| `itemName` | string/null | 应收项名称 |
| `direction` | string | 方向,`ADD` 增加应收,`DEDUCT` 减少应收 |
| `amount` | number | 金额 |
`collectionSummary` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `payableAmount` | number | 应收总额 |
| `paidAmount` | number | 累计已收 |
| `refundedAmount` | number | 累计已退 |
| `netPaidAmount` | number | 净已收,等于已收减已退 |
| `balanceAmount` | number | 待收尾款 |
| `depositPaidAmount` | number | 已收订金 |
| `balancePaidAmount` | number | 已收尾款 |
| `fullPaidAmount` | number | 已收全款 |
`collectionRecords[]` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `recordId` | string | 收款记录 ID |
| `recordType` | string | 记录类型,线上支付或线下收款 |
| `recordTypeName` | string | 记录类型中文名 |
| `payType` | string/null | 款项类型 |
| `payTypeName` | string/null | 款项类型中文名 |
| `channel` | string/null | 支付或收款渠道 |
| `channelName` | string/null | 支付或收款渠道中文名 |
| `receiptMethod` | string/null | 线下收款方式;在线支付和对公转账可为空 |
| `receiptMethodName` | string/null | 线下收款方式中文名 |
| `amount` | number | 金额 |
| `collectedAt` | string/null | 收款时间,格式 `yyyy-MM-dd HH:mm:ss` 或 ISO 时间字符串 |
| `status` | string/null | 收款记录状态 |
| `statusName` | string/null | 收款记录状态中文名 |
| `collectorName` | string/null | 代收人姓名 |
| `operatorName` | string/null | 登记人姓名 |
| `thirdPartyNoMasked` | string/null | 脱敏第三方交易号 |
| `transferRef` | string/null | 转账流水号 |
| `voucherUrls` | string[]/null | 凭证图片 URL |
| `remark` | string/null | 备注 |
| `includedInPaidAmount` | boolean | 是否计入已收金额 |
详情接口不返回 `needsVehicle`、`vehicleControlStatus`、`vehicleControlStatusName`。
#### 3.2.3 示例
典型成功:
**请求**
```http
GET /v3/admin/order/2077233886248534018/settlement/return-detail
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderInfo": {
"orderId": "2077233886248534018",
"orderNo": "HL202607180001",
"teamNo": "T20260718001",
"productName": "呼伦贝尔草原 5 日游",
"departureDate": "2026-07-20",
"returnDate": "2026-07-24",
"peopleCount": 3,
"adultCount": 2,
"childCount": 0,
"youngChildCount": 0,
"babyCount": 1,
"peopleSummary": "2成人1婴儿",
"consultantId": "1001",
"consultantName": "admin",
"houseStaffId": "20001",
"houseStaffName": "房务A",
"fleetStaffId": "30001",
"fleetStaffName": "车务A",
"settlementStatus": "COMPLETED",
"settlementStatusName": "已结算"
},
"travelers": [
{
"travelerId": "2077233886248535001",
"travelerName": "张三",
"travelerType": "ADULT",
"travelerTypeName": "成人",
"idType": "ID_CARD",
"idTypeName": "身份证",
"phone": "138****0000",
"idCardNo": "150***********1234"
}
],
"driverVehicles": [
{
"driverId": "2078304714008522754",
"driverName": "李师傅",
"driverPhone": "176****3787",
"vehicleId": "2065329514644152321",
"vehiclePlateNo": "蒙C01E01",
"vehicleModelName": "丰田埃尔法",
"seatCount": 7,
"startDate": "2026-07-20",
"endDate": "2026-07-24"
}
],
"receivableSummary": {
"orderAmount": 6000.00,
"surchargeAmount": 200.00,
"discountAmount": 90.00,
"payableAmount": 6110.00,
"formulaText": "订单金额 + 附加费 - 优惠 = 应收总额"
},
"receivableItems": [
{
"sourceRecordId": "2077233886248534018",
"itemType": "BASE_ORDER",
"itemTypeName": "订单基础应收",
"itemCode": "ORDER_AMOUNT",
"itemName": "呼伦贝尔草原 5 日游",
"direction": "ADD",
"amount": 6000.00
},
{
"sourceRecordId": "2077233886248536001",
"itemType": "SURCHARGE",
"itemTypeName": "附加费",
"itemCode": "SINGLE_ROOM",
"itemName": "单房差",
"direction": "ADD",
"amount": 200.00
},
{
"sourceRecordId": "2077233886248537001",
"itemType": "DISCOUNT",
"itemTypeName": "优惠",
"itemCode": "PROMOTION",
"itemName": "活动优惠",
"direction": "DEDUCT",
"amount": 90.00
}
],
"collectionSummary": {
"payableAmount": 6110.00,
"paidAmount": 6110.00,
"refundedAmount": 0.00,
"netPaidAmount": 6110.00,
"balanceAmount": 0.00,
"depositPaidAmount": 1000.00,
"balancePaidAmount": 5110.00,
"fullPaidAmount": 0.00
},
"collectionRecords": [
{
"recordId": "2077233886248538001",
"recordType": "ONLINE_PAYMENT",
"recordTypeName": "在线支付",
"payType": "DEPOSIT",
"payTypeName": "订金",
"channel": "WECHAT",
"channelName": "微信支付",
"receiptMethod": null,
"receiptMethodName": null,
"amount": 1000.00,
"collectedAt": "2026-07-10 10:30:00",
"status": "SUCCEEDED",
"statusName": "支付成功",
"collectorName": null,
"operatorName": null,
"thirdPartyNoMasked": "4200****0001",
"transferRef": null,
"voucherUrls": null,
"remark": null,
"includedInPaidAmount": true
},
{
"recordId": "2077233886248539001",
"recordType": "MANUAL_RECEIPT",
"recordTypeName": "线下收款",
"payType": "BALANCE",
"payTypeName": "尾款",
"channel": "DRIVER_CASH",
"channelName": "报账人收款",
"receiptMethod": "WECHAT_TRANSFER",
"receiptMethodName": "微信转账",
"amount": 5110.00,
"collectedAt": "2026-07-20 18:30:00",
"status": "CONFIRMED",
"statusName": "已确认",
"collectorName": "李师傅",
"operatorName": "admin",
"thirdPartyNoMasked": null,
"transferRef": null,
"voucherUrls": [
"https://oss.example.com/receipt/a.jpg"
],
"remark": "现场收尾款",
"includedInPaidAmount": true
}
]
},
"traceId": null,
"success": true
}
```
边界成功:无司机车辆、无收款记录。
**请求**
```http
GET /v3/admin/order/2077233886248534019/settlement/return-detail
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 200,
"message": "成功",
"data": {
"orderInfo": {
"orderId": "2077233886248534019",
"orderNo": "HL202607180002",
"teamNo": null,
"productName": "呼伦贝尔草原 5 日游",
"departureDate": "2026-07-20",
"returnDate": "2026-07-24",
"peopleCount": 1,
"adultCount": 1,
"childCount": 0,
"youngChildCount": 0,
"babyCount": 0,
"peopleSummary": "1成人",
"consultantId": "1001",
"consultantName": "admin",
"houseStaffId": null,
"houseStaffName": null,
"fleetStaffId": null,
"fleetStaffName": null,
"settlementStatus": "NONE",
"settlementStatusName": "未结算"
},
"travelers": [],
"driverVehicles": [],
"receivableSummary": {
"orderAmount": 3000.00,
"surchargeAmount": 0.00,
"discountAmount": 0.00,
"payableAmount": 3000.00,
"formulaText": "订单金额 + 附加费 - 优惠 = 应收总额"
},
"receivableItems": [
{
"sourceRecordId": "2077233886248534019",
"itemType": "BASE_ORDER",
"itemTypeName": "订单基础应收",
"itemCode": "ORDER_AMOUNT",
"itemName": "呼伦贝尔草原 5 日游",
"direction": "ADD",
"amount": 3000.00
}
],
"collectionSummary": {
"payableAmount": 3000.00,
"paidAmount": 0.00,
"refundedAmount": 0.00,
"netPaidAmount": 0.00,
"balanceAmount": 3000.00,
"depositPaidAmount": 0.00,
"balancePaidAmount": 0.00,
"fullPaidAmount": 0.00
},
"collectionRecords": []
},
"traceId": null,
"success": true
}
```
业务失败:订单不存在。
**请求**
```http
GET /v3/admin/order/999999999999999999/settlement/return-detail
Authorization: Bearer <JWT>
```
**响应**
```json
{
"code": 581007,
"message": "订单不存在",
"data": null,
"traceId": null,
"success": false
}
```
## 4. 入参汇总
| 接口 | 入参位置 | 字段 |
|---|---|---|
| `GET /v3/admin/order-settlement/tasks` | Query | `page`、`pageSize`、`keyword`、`departureDateFrom`、`departureDateTo`、`settlementStatus` |
| `GET /v3/admin/order/{orderId}/settlement/return-detail` | Path | `orderId` |
## 5. 出参汇总
| 接口 | 出参根结构 | 主要字段 |
|---|---|---|
| `GET /v3/admin/order-settlement/tasks` | `PageResult<SettlementTaskRespVO>` | `records[]`、`total`、`page`、`pageSize` |
| `GET /v3/admin/order/{orderId}/settlement/return-detail` | `SettlementReturnDetailRespVO` | `orderInfo`、`travelers`、`driverVehicles`、`receivableSummary`、`receivableItems`、`collectionSummary`、`collectionRecords` |
## 6. 枚举 / 数据字典
### 6.1 `settlementStatus`
**所属字段**: `settlementStatus`、`records[].settlementStatus`、`orderInfo.settlementStatus` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `NONE` | 未结算 | 初始态,尚未提交核单 |
| `PENDING` | 待财务复核 | 已提交核单,等待财务复核 |
| `COMPLETED` | 已结算 | 财务复核已完成 |
### 6.2 `travelerType`
**所属字段**: `travelers[].travelerType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `ADULT` | 成人 | 成人出行人 |
| `CHILD` | 儿童 | 儿童出行人 |
| `YOUNG_CHILD` | 幼童 | 幼童出行人 |
| `BABY` | 婴儿 | 婴儿出行人 |
### 6.3 `idType`
**所属字段**: `travelers[].idType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `ID_CARD` | 身份证 | 居民身份证 |
| `PASSPORT` | 护照 | 护照 |
| `BIRTH_CERT` | 出生证明 | 出生医学证明 |
### 6.4 `itemType`
**所属字段**: `receivableItems[].itemType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `BASE_ORDER` | 订单基础应收 | 订单基础金额 |
| `SURCHARGE` | 附加费 | 附加费用,增加应收 |
| `DISCOUNT` | 优惠 | 优惠项目,减少应收 |
### 6.5 `direction`
**所属字段**: `receivableItems[].direction` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `ADD` | 增加 | 计入应收增加项 |
| `DEDUCT` | 扣减 | 计入应收扣减项 |
### 6.6 `recordType`
**所属字段**: `collectionRecords[].recordType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `ONLINE_PAYMENT` | 在线支付 | 线上支付流水 |
| `MANUAL_RECEIPT` | 线下收款 | 管理后台登记的线下收款 |
### 6.7 `payType`
**所属字段**: `collectionRecords[].payType` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `DEPOSIT` | 订金 | 订金 |
| `FULL` | 全款 | 全款 |
| `BALANCE` | 尾款 | 尾款 |
### 6.8 `channel`
**所属字段**: `collectionRecords[].channel` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `WECHAT` | 微信支付 | 在线微信支付 |
| `ALIPAY` | 支付宝 | 在线支付宝支付 |
| `OFFLINE_TRANSFER` | 线下转账 | 线下转账渠道 |
| `DRIVER_CASH` | 报账人收款 | 报账人代收 |
| `BANK_TRANSFER` | 对公转账 | 对公银行转账 |
| `CONSULTANT_COLLECTION` | 定制师代收 | 定制师代收 |
### 6.9 `receiptMethod`
**所属字段**: `collectionRecords[].receiptMethod` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `WECHAT_TRANSFER` | 微信转账 | 线下微信转账 |
| `CASH` | 现金收款 | 现金收款 |
### 6.10 `collectionRecords[].status`
**所属字段**: `collectionRecords[].status` | **类型**: `String`
| 值 | 中文 | 说明 |
|---|---|---|
| `SUCCEEDED` | 支付成功 | 成功在线支付,计入已收 |
| `PENDING` | 待支付 | 在线支付待支付,不计入已收 |
| `CLOSED` | 已关闭 | 在线支付已关闭,不计入已收 |
| `REFUNDED` | 已退款 | 在线支付已退款 |
| `CONFIRMED` | 已确认 | 线下收款已确认,计入已收 |
| `VOIDED` | 已撤销 | 线下收款已撤销,不计入已收 |
## 7. 错误码
| code | 含义 | 触发场景 |
|---|---|---|
| `200` | 成功 | 查询成功 |
| `400` | 参数校验失败 | `page < 1`、`pageSize > 100`、`orderId <= 0`、日期格式非法、`settlementStatus` 非法 |
| `401` | 未认证 | 未携带有效管理后台 JWT |
| `581007` | 订单不存在 | 详情接口查询不存在的订单 |
| `581045` | 房务角色无权查看订单详情,房务仅可配房 | 房务管理员或房务组长访问列表或详情 |
| `584072` | 车务司机车辆信息暂时不可用,请稍后重试 | 详情接口读取司机车辆信息不可用 |
## 8. 示例
示例已按接口放在 `3.1.3` 和 `3.2.3`:列表接口包含典型成功和空结果;详情接口包含典型成功、无司机车辆/无收款记录边界成功、订单不存在业务失败。
## 9. 业务边界
- 列表只包含常规 CORE 产品、无团期批次、已完成订单;团期、小蒙马、导游/摄影团队独立列表不在本接口范围。
- 列表按返团日期倒序、订单 ID 倒序返回。
- 详情接口取消订单返回成功响应,但 `orderInfo`、汇总对象可为空,数组字段为空数组。
- 详情中的手机号、身份证号、第三方交易号、司机手机号均为脱敏值。
- `collectionRecords` 已合并在线支付和线下收款,前端不需要再把支付记录接口与线下收款接口自行合并。
- `receivableSummary.payableAmount` 是应收总额,计算项通过 `receivableItems` 返回。
## 10. 修改前后对比
### 10.1 列表接口
| 项 | 修改前 | 修改后 |
|---|---|---|
| 常规产品核团列表 | 无专用接口 | 新增 `GET /v3/admin/order-settlement/tasks` |
| 人数文案 | 无 | 返回 `peopleSummary` |
| 核算状态中文 | 无 | 返回 `settlementStatusName` |
| 司机/车牌 | 不适用 | 列表不返回司机和车牌字段 |
### 10.2 详情接口
| 字段/结构 | 修改前 | 修改后 |
|---|---|---|
| `orderInfo.peopleSummary` | 无 | 新增 |
| `orderInfo.adultCount/childCount/youngChildCount/babyCount` | 无 | 新增 |
| `orderInfo.consultantId/consultantName` | 无 | 新增 |
| `orderInfo.houseStaffId/houseStaffName` | 无 | 新增 |
| `orderInfo.fleetStaffId/fleetStaffName` | 无 | 新增 |
| `orderInfo.settlementStatusName` | 无 | 新增 |
| `travelers[].travelerTypeName` | 无 | 新增 |
| `travelers[].idTypeName` | 无 | 新增 |
| `driverVehicles` | 无 | 新增司机车辆集合 |
| `receivableSummary` | 不完整 | 新增订单金额、附加费、优惠、应收总额、公式文案 |
| `receivableItems` | 无 | 新增应收计算明细 |
| `collectionSummary` | 不完整 | 新增已收/已退/净已收/待收/订金/尾款/全款汇总 |
| `collectionRecords` | 无 | 新增在线支付+线下收款合并记录 |
| `needsVehicle/vehicleControlStatus/vehicleControlStatusName` | 可能需要前端关注 | 本接口不返回 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**: 否。列表为新增接口;详情为新增出参字段和新增集合结构。
- **前端是否必须同步上线**: 建议同步。核团列表页应改用新增列表接口;详情页可直接使用新增聚合字段,减少前端合并接口逻辑。
- **影响已有数据**: 不需要数据迁移。
### 11.2 回滚方案
- **回滚方式**: 回滚 PR #5058。
- **回滚后前端影响**: 新增列表接口不可用,详情新增字段消失;前端需要回退到原有多接口合并方案或旧页面逻辑。
## 12. 注意事项
- `orderId`、`travelerId`、`driverId`、`vehicleId` 等长整型 ID 均按字符串处理,避免 JS 精度丢失。
- 列表不要展示司机和车牌;司机车辆只在详情的 `driverVehicles` 中展示。
- 详情里的线下收款和在线支付已经按统一记录结构返回,`recordType` 用于区分来源。
- 金额字段单位均为元。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#5055](https://git.1814.love:8443/wx/HL/issues/5055)
- **PR**: [#5058](https://git.1814.love:8443/wx/HL/pulls/5058)
- **Merge commit**: [d916901f8](https://git.1814.love:8443/wx/HL/commit/d916901f8)
### 13.2 联系人
- **后端负责人**: @yst
- **需求确认**: @yaosutu
@@ -1,6 +1,6 @@
# 【行为变更·管理后台】订单调整保留配房与房务驳回定制师待办(#4907) # 【行为变更·管理后台】订单调整保留配房与房务驳回定制师待办(#4907)
> 2026-07-14 最终状态:#4907 后端链路已完成并关闭;最终全量复测与前端接入总览见 `57_房务全量API复测与前端最终接入核对-管理后台.md`。前端页面验收属于独立交付,不作为后端工单关单门禁。 > 2026-07-16 最终后端状态:零配房最终确认动作契约已由 PR #5014 补齐并合入 `dev-v3`。最新代码、部署、网关 API、DB/库存和日志证据均通过;前端页面实现属于独立交付,不作为后端工单关单门禁。
> 服务:`hl-order-service-v3` > 服务:`hl-order-service-v3`
> >
@@ -8,9 +8,9 @@
> >
> 接口结构:新增“单条配房晚次与资源原子调整”接口;其余沿用订单调整、房务详情、最终确认和房务驳回现有接口 > 接口结构:新增“单条配房晚次与资源原子调整”接口;其余沿用订单调整、房务详情、最终确认和房务驳回现有接口
> >
> 后端状态:PR [#4915](https://git.1814.love:8443/wx/HL/pulls/4915)、[#4925](https://git.1814.love:8443/wx/HL/pulls/4925)、[#4926](https://git.1814.love:8443/wx/HL/pulls/4926)、[#4928](https://git.1814.love:8443/wx/HL/pulls/4928)、[#4931](https://git.1814.love:8443/wx/HL/pulls/4931) 已合并;最新测试环境部署任务 `eb216570` 成功,`8086/8186` 双实例 UP > 后端状态:既有 PR [#4915](https://git.1814.love:8443/wx/HL/pulls/4915)、[#4925](https://git.1814.love:8443/wx/HL/pulls/4925)、[#4926](https://git.1814.love:8443/wx/HL/pulls/4926)、[#4928](https://git.1814.love:8443/wx/HL/pulls/4928)、[#4931](https://git.1814.love:8443/wx/HL/pulls/4931) 与最新 PR [#5014](https://git.1814.love:8443/wx/HL/pulls/5014) 均已合并;最终验证基线 `dev-v3@d54435af7`,测试环境部署任务 `86d9bf11` 成功,`8086/8186` 双实例 UP
> >
> 联调证据:2026-07-12 最终网关四场景探针通过,覆盖连续改需求、跨晚次原子移动、最终确认、供应商驳回、订单取消、库存迁移与库存不足补偿;报告 `D:/work2/HL-v3/.tmp/house-adjustment-flow-probe-20260712-212622.json` 为 `ok=true` > 联调证据:2026-07-16 部署后重新使用隔离订单执行接口面、缺口流程、订单日志和调整闭环四组探针,全部通过;最终调整报告 `D:/work2/HL-v3/.tmp/house-adjustment-flow-probe-20260716-184129.json` 为 `ok=true`
## 0. 2026-07-12 追加:返工标签唯一口径与前端未完成项 ## 0. 2026-07-12 追加:返工标签唯一口径与前端未完成项
@@ -202,6 +202,29 @@ POST /admin/house/assignments/requirements/{requirementId}/finalize
前端收到该错误后保留当前配房数据,提示房务逐日处理;不得清空页面状态或隐藏超出新行程的旧配房。 前端收到该错误后保留当前配房数据,提示房务逐日处理;不得清空页面状态或隐藏超出新行程的旧配房。
### 2.1 零配房动作契约
详情接口与最终确认写接口现已使用同一业务口径:
- 当前生效需求由当前房务持有。
- `houseStatus=CLAIMING`。
- 没有任何配房记录。
- 没有未闭环询房。
满足以上条件时,房务详情返回:
```json
{
"actions": {
"canFinalize": {
"enabled": true
}
}
}
```
以下场景仍保持禁用:存在部分配房、存在未闭环询房、非当前持有人、非当前生效需求或需求已经完成。最终确认成功后必须重新加载详情和待办;后端会关闭相关待办且不会创建配房或变更库存。
## 3. 房务驳回后的定制师待办 ## 3. 房务驳回后的定制师待办
```http ```http
@@ -247,10 +270,14 @@ Content-Type: application/json
## 5. 后端验证证据 ## 5. 后端验证证据
- PR:`wx/HL#4910/#4911/#4913/#4915/#4925/#4926/#4928/#4931` - PR:`wx/HL#4910/#4911/#4913/#4915/#4925/#4926/#4928/#4931/#5014`
- 最新测试环境部署任务:`eb216570`,`8086/8186` 双实例均 UP - 最终验证基线:`dev-v3@d54435af7`
- 定向测试:276 项通过;模块全量 5523 项仅复现 clean baseline 的 3 失败 + 2 错误,无新增回归 - 模块全量:5609 项测试,0 failure,0 error,15 skipped,`BUILD SUCCESS`
- 最终网关全流程报告:`D:/work2/HL-v3/.tmp/house-adjustment-flow-probe-20260712-212622.json`,四场景全部 `ok=true` - 最新测试环境部署任务:`86d9bf11`,`8086/8186` 双实例均 UP
- 实测通过:增晚、减晚、人数变化、连续调整新旧待办替代、改期+增晚同次提交、酒店/房型/房间数替换、入住日期对齐、人工清空、零配房最终确认、供应商驳回、订单取消、库存成功迁移、库存不足整单回滚 - 网关接口面:68/68 通过,报告 `D:/work2/HL-v3/.tmp/house-api-surface-probe-20260716-182908.json`
- 返工标签:`REQUIREMENT_ADJUSTED` 压制同需求 `PENDING_ARRANGE`;最终确认、供应商驳回、订单取消后统计均从 1 回到 0 - 缺口流程:3/3 通过,报告 `D:/work2/HL-v3/.tmp/house-api-gap-flow-probe-20260716-183121.json`
- 订单日志:3/3 通过,报告 `D:/work2/HL-v3/.tmp/house-order-log-flow-probe-20260716-183224.json`
- 调整闭环:4/4 通过,报告 `D:/work2/HL-v3/.tmp/house-adjustment-flow-probe-20260716-184129.json`
- 实测通过:增晚、减晚、人数变化、连续调整新旧待办替代、改期+增晚、跨晚次原子调整、酒店/房型/房间数替换、入住日期对齐、人工清空、零配房最终确认、供应商驳回、订单取消、库存成功迁移、库存不足整单回滚
- 返工标签:`REQUIREMENT_ADJUSTED` 压制同需求 `PENDING_ARRANGE`;最终确认、供应商驳回、订单取消后返工待办均正确关闭
@@ -0,0 +1,273 @@
# 【前端对接·管理后台】车务需求级派单完成回调与滚动发布契约
> Issue: [wx/HL#4935](https://git.1814.love:8443/wx/HL/issues/4935)
>
> PR: [wx/HL#4994](https://git.1814.love:8443/wx/HL/pulls/4994)、[wx/HL#5009](https://git.1814.love:8443/wx/HL/pulls/5009)
>
> 服务: `hl-fleet-service` / `hl-order-service-v3`
>
> 日期: 2026-07-16
>
> 影响范围: 车务派单完成、用车需求驳回、订单资源状态、看板刷新与部署兼容
## 一、关键纠正
此前链路可能在单个日期或单辆车派定后提前把整个用车需求写成 `DONE`。本次改为:
- 一个用车需求只做一次最终完成回调。
- 只有全部服务日期、全部车型项均已生成有效派单,并且每条逐日配置同时绑定车辆和司机,后端才允许整个需求完成。
- 前端不得根据“某一天已派”“某一辆车已派”自行把需求或订单资源节点标成完成。
- 前端继续直接使用看板/详情接口返回的状态、文案和能力字段,不维护独立状态映射。
## 二、前端接口结论
本次不新增前端调用接口,管理后台继续使用:
| 接口 | 方法 | 路径 | 前端用途 |
|---|---|---|---|
| 派单看板汇总 | GET | `/admin/fleet/board/summary` | 状态选项、文案、数量 |
| 派单看板列表 | GET | `/admin/fleet/board/orders` | 分页卡片、状态与能力字段 |
| 派单看板详情 | GET | `/admin/fleet/board/orders/{orderId}` | 当前需求、逐日行程、当前派单 |
| 派单时间线 | GET | `/admin/fleet/board/orders/{orderId}/timeline` | 已发生操作记录 |
前端处理规则:
1. 状态筛选使用 `summary.statusOptions`,卡片文案使用 `assignmentStatusLabel`。
2. 派车入口只看 `canAssign`,驳回入口只看 `canRejectRequirement`。
3. 派单、驳回或重试成功后重新请求汇总、列表和当前详情,不能只在本地改一张卡片。
4. 同一需求仍有未完成日期或其他车辆项时,后端保持进行中;前端不得提前展示“已完成”。
5. 后端部署开关关闭期间,需求级完成/驳回事件会保留待重放;前端不需要轮询内部 Outbox,也不得调用内部回调。
## 三、管理后台响应示例
### 3.1 仍有未完成配置
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"assignmentStatus": "holding",
"assignmentStatusLabel": "排车中",
"canAssign": true,
"canRejectRequirement": false,
"currentAssignment": {
"requirementId": "2075001000000000001"
}
}
}
```
该响应只表示需求仍在处理,不能因 `currentAssignment` 非空推断整个需求已完成。
### 3.2 整个需求完成后
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"assignmentStatus": "assigned",
"assignmentStatusLabel": "已派车",
"canAssign": false,
"canRejectRequirement": false
}
}
```
实际字段以看板接口当前 OpenAPI 为准;状态中文和能力判断均由后端返回。
## 四、内部回调契约
> 本节供后端与 QA 验收。以下 `/v3/internal/**` 接口不经过管理后台,不配置公网网关路由,前端禁止调用。
### 4.1 最终完成回调
```http
POST /v3/internal/order/vehicle-assignment/callback
Content-Type: application/json
```
请求示例:
```json
{
"orderId": "2074746808742928386",
"requirementId": "2075001000000000001",
"vehicleId": "2076001000000000001",
"vehicleType": "suv",
"vehicleCount": 1,
"licensePlate": "蒙A12345",
"brand": "丰田汉兰达",
"seats": 7,
"plannedDailyFee": "1300.00",
"dailyFeeSource": "PRICE_CALENDAR",
"driverStaffId": "2077001000000000001",
"driverName": "测试司机",
"driverPhone": "13800000000",
"topologyFingerprint": "<64位 SHA-256 摘要>",
"remark": "需求级最终派单快照"
}
```
成功响应:
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": null
}
```
新版 Fleet 必填字段:`orderId`、`requirementId`、`vehicleId`、`vehicleType`、`vehicleCount`、`topologyFingerprint`。其中 `topologyFingerprint` 是 Fleet 根据该需求全部有效逐日派单生成的 64 位 SHA-256 摘要;其余快照字段允许为空,后端不得伪造车牌、品牌、座位、价格或司机信息。
`vehicleId`、车牌和司机字段是稳定排序后的代表派单,`vehicleCount` 是该需求实际车辆组总数。完整逐日、多车辆和多司机拓扑仍以 Fleet 派单明细为准,不能从该轻量快照反推完整派车表。
滚动发布期间,旧版 Fleet 不传 `topologyFingerprint` 时,Order-v3 会根据完整回调快照生成 `legacy:` 前缀摘要并持久化。该兼容仅用于先升级 Order-v3、后升级 Fleet 的过渡期;新版 Fleet 仍必须发送摘要。
### 4.2 轻量进度回写
```http
POST /v3/internal/order/orders/{orderId}/requirement/vehicle/status?requirementId={requirementId}&status=PROCESSING
```
该接口只允许 `PROCESSING`。`DONE` 必须走最终完成回调并冻结快照。
### 4.3 驳回回写
```http
POST /v3/internal/order/orders/{orderId}/requirement/vehicle/reject
Content-Type: application/json
```
请求示例:
```json
{
"requirementId": "2075001000000000001",
"returnRemark": "车型需求不完整,请定制师补充",
"operatorId": "2078001000000000001"
}
```
只有当前需求不存在 `holding/assigned` 有效派单时才允许驳回。
## 五、状态、幂等和错误分支
| 场景 | 结果 | 副作用 |
|---|---|---|
| `PENDING/PROCESSING` 且无快照 | 原子写快照并完成需求 | 同事务写需求 `DONE`、订单车辆状态 `DONE`、待办/日志并尝试推进订单 |
| `DONE` 且摘要相同 | 幂等成功 | 不加需求写锁、不更新 `update_time`,不重复同步司机、待办、日志或推进订单 |
| `DONE` 且摘要变化 | 刷新轻量快照 | 只更新同一需求快照和司机信息,不重复推进订单、待办或时间线 |
| 旧版回调未传摘要 | 兼容成功 | Order-v3 生成稳定 `legacy:` 摘要;相同旧请求重放仍为零写入 |
| `DONE` 但无快照 | 返回 `582081` | 禁止补造快照,禁止继续副作用 |
| active 状态已有快照 | 返回 `582082` | 禁止重复回写 |
| 需求不存在或失效 | 返回 `582080` | 无写入 |
| 非法状态流转 | 返回 `582083` | 无写入 |
| 订单/需求已取消 | 跳过 | 不写完成快照,不推进订单 |
并发与重放需区分:同一摘要在 5 秒互斥窗口外再次提交时返回成功且数据库零写入;互斥窗口内的并发重复请求返回可识别冲突 `100502`,同样不得重复写快照、待办、流水或推进订单。前端遇到该冲突应刷新当前需求状态,不得自行补写完成状态。
错误响应示例:
```json
{
"code": 582081,
"message": "用车需求状态不允许回写配车",
"success": false,
"data": null
}
```
## 六、滚动发布与回滚
1. 先部署全部 `hl-order-service-v3` 实例并确认 Flyway 成功。
2. 保持 `FLEET_REQUIREMENT_LIFECYCLE_ENABLED=false`,再部署全部 `hl-fleet-service` 实例。
3. 确认 Fleet Flyway、健康与普通 Outbox 消费正常后,再启用开关。
4. 开关关闭时,需求级 Outbox 事件不会占用普通事件扫描窗口,也不会被丢弃;开启后继续重放。
5. 异常时先关闭开关,再回滚服务制品;兼容字段和历史 Outbox 不做破坏性回滚。
## 七、前端必须处理
1. 不新增内部回调请求,不把内部错误码做成独立前端流程。
2. 不按每日派单行数或单车派定结果推导需求完成。
3. 继续使用后端返回的 `statusOptions`、`assignmentStatusLabel`、`canAssign`、`canRejectRequirement`。
4. 操作成功后刷新服务端状态;并发处理中若能力字段变化,以最新接口响应为准。
5. 不修改既有分页、团号、联系人、定制师、逐日行程和大交通字段接法;这些仍以 `57_4882` 文档为准。
## 八、不影响范围
- 不修改 `hl-ui`,本文件仅做后端契约告知。
- 不新增管理后台分页或不分页接口。
- 不改变车型大类、司机占一座、司机险只计车队成本等既有口径。
- 不处理团期配车。
## 九、验证状态
### 9.1 合并前代码验证
```text
hl-order-service-v3 targeted: 198 tests,0 failures,0 errors,0 skipped
hl-order-service-v3 full verify: 5582 tests,0 failures,0 errors,15 skipped
hl-fleet-service targeted: 226 tests,0 failures,0 errors,0 skipped
hl-fleet-service full verify: 1721 tests,0 failures,0 errors,0 skipped
独立终审: P0=0,P1=0,P2=0
```
### 9.2 测试环境部署
- `hl-order-service-v3` Deploy Panel 任务 `29b541d6` 成功;`8086/8186` 双实例均启动并监听。
- `hl-fleet-service` Deploy Panel 任务 `aaee8d01` 成功;`8087/8187` 双实例均启动并监听。
- Nacos 已启用 `fleet.assign.requirement-lifecycle-enabled=true` 与 `fleet.feign.writeback.enabled=true`。
- 发布顺序按“Order-v3 全实例 -> Fleet 全实例 -> 开启需求级生命周期开关”执行,未跨过滚动发布护栏。
### 9.3 真实 API 与数据验收
使用独立车务账号和真实测试订单完成 DIRECT、HOLD、取消后迟到回调、同摘要重放、摘要变化刷新、非法参数及失效需求分支验收;未使用 `admin`、`wx` 或 Mock 数据。
公网网关 `https://api.test.1814.love:9443` 最终验证:
| 请求 | 结果 |
|---|---|
| 车务账号登录 | HTTP 200 |
| `GET /admin/fleet/board/summary` | HTTP 200,状态码/文案/数量由后端返回 |
| `GET /admin/fleet/board/orders?status=assigned&orderNo=...` | HTTP 200,精准返回 1 条 |
| `GET /admin/fleet/board/orders/{orderId}` | HTTP 200,返回逐日行程、车型诉求、司机确认凭证及当前派单 |
| `GET /admin/fleet/board/orders/{orderId}/timeline` | HTTP 200,返回完整操作时间线 |
HOLD 模式真实终态校验:
```json
{
"requirementStatus": "DONE",
"vehicleControlStatus": "DONE",
"hasFleetAssigned": true,
"snapshotCount": 1,
"activeDailySlices": 6,
"activeDailySliceStatus": "assigned",
"driverConfirmationEvidenceCount": 1,
"completionOutboxStatus": "SUCCESS"
}
```
取消订单迟到回调保持 `hasFleetAssigned=false`、快照数为 0、有效逐日派单数为 0;相同拓扑摘要在互斥窗口外重放返回 HTTP 200 且不重复推进待办、流水或订单状态,窗口内并发重复返回 `100502` 且无重复副作用。
### 9.4 OpenAPI 与日志
- Order-v3 OpenAPI 已公开内部最终回调及 `VehicleAssignmentCallbackReqVO` 的 6 个必填字段。
- Fleet OpenAPI 已公开创建、预检、取消、改派、最终确认、司机确认/拒绝、提前结束、需求驳回与撤销取消等 10 个生命周期接口。
- 2026-07-16 22:22 后四个目标实例均无 `ERROR` 级日志;目标订单与需求在四实例中均为 0 条 WARN/ERROR,日志可见司机确认、最终回调成功和 Order-v3 快照刷新。
- 测试环境另有保险 PDF 缺失与历史脏订单降级 WARN,未关联本次目标订单,不作为本契约成功响应的一部分。
Issue #4935 的代码、部署、网关 API、MySQL 终态和服务日志证据均已补齐,可按后端验收清单关单。
## 十、相关文档
- 当前看板字段、分页、统计、行程与保险:`57_4882_车务派单看板当前订单字段统计行程与保险事务收口-管理后台.md`
- 车务提需求与派单看板:`53_4871_车务提需求派单看板闭环契约-管理后台.md`
- 团号与定制师筛选:`54_4876_派单看板团号与定制师下拉筛选-管理后台.md`
- 后端最终回调契约:`hl-backend-changelog/changelogs/2026-07/14_1022_order-v3_vehicle-assignment-callback-contract.md`
@@ -0,0 +1,775 @@
# 【前端对接·管理后台】车务看板、详情、候选与矩阵读模型统一
> Issue: [wx/HL#4936](https://git.1814.love:8443/wx/HL/issues/4936)
>
> PR: [wx/HL#5031](https://git.1814.love:8443/wx/HL/pulls/5031)、[wx/HL#5032](https://git.1814.love:8443/wx/HL/pulls/5032)、[wx/HL#5034](https://git.1814.love:8443/wx/HL/pulls/5034)
>
> 服务: `hl-fleet-service` / `hl-order-service-v3`
>
> 日期: 2026-07-18
>
> 影响范围: 车务派单看板汇总与列表、派单详情、车辆/司机候选、矩阵月视图、相邻订单衔接风险
## 一、对接结论
1. 派单看板继续使用分页接口,`pageSize` 最大 100;没有新增“不分页全量接口”。
2. `/summary` 与 `/orders` 共用日期、车型、司机、联系人、团号、定制师和 `keyword` 筛选;汇总忽略 `status/statuses/page/pageSize`,返回同一筛选范围内的全部状态分面。`pendingCount/pendingUrgentCount/todayDepartCount/holdingTimeoutCount` 同样随这些订单筛选变化;`idleVehicleCount/idleDriverCount` 是不随订单筛选变化的全局资源指标。
3. 派单列表、详情和矩阵均以**当前订单 + 当前有效用车需求 + 当前有效派车组**为准,历史需求和历史派单不能覆盖当前数据。
4. 详情一次返回逐日行程、大交通、当前需求、全部有效派车组、生命周期、凭证和操作记录。
5. 候选车辆和司机分别分页,允许先选车或先选司机;返回完整闭区间可用时间窗、结构化可用性原因、冲突和常驻关系。
6. 矩阵按整月查询,但同一跨月派车组先补齐完整组再裁剪显示;不会因只查到月内一天而丢失真实起止日期。
7. 矩阵相邻订单衔接风险由后端返回 `status/statusLabel/style/reasonCode/reasonMessage`,前端不得自行根据颜色或时间重新推导。
8. **大交通允许不填写。** 无大交通时仍可提交用车需求、查询候选、预检和派车;前端只能显示提示,不得禁用派车按钮。
9. 所有雪花 ID 均按字符串处理,禁止转为 JavaScript `Number`。
10. 本次未修改 `hl-ui`,前端只按本文完成接口对接。
## 二、接口清单
| # | 接口 | 方法 | 路径 | 用途 |
|---|---|---|---|---|
| 1 | 看板汇总 | GET | `/admin/fleet/board/summary` | 同筛选状态计数、急单数、资源数、定制师选项 |
| 2 | 看板列表 | GET | `/admin/fleet/board/orders` | 分页卡片、统一筛选、当前状态和操作能力 |
| 3 | 看板详情 | GET | `/admin/fleet/board/orders/{orderId}` | 当前订单、当前需求、行程、大交通、派车组和日志 |
| 4 | 出行人脱敏列表 | GET | `/admin/fleet/board/orders/{orderId}/travelers` | 默认脱敏查看出行人 |
| 5 | 出行人明文查询 | POST | `/admin/fleet/board/orders/{orderId}/travelers/plain` | 有权限且有审计理由时查看明文 |
| 6 | 派单候选 | POST | `/admin/fleet/assignments/candidates` | 车辆和司机独立分页、冲突、可用时间窗、常驻关系 |
| 7 | 矩阵月视图 | GET | `/admin/fleet/matrix/grid` | 车辆月历、派车段、并行车辆、大交通和衔接风险 |
## 三、看板汇总与列表公共筛选
### 3.1 查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `statuses` | `string[]` | 否 | 多状态任一命中;支持重复 query 参数或逗号分隔。 |
| `status` | `string` | 否 | 单状态/逗号分隔别名,与 `statuses` 合并。 |
| `startDayFrom` | `date` | 否 | 日期区间起,与当前行程闭区间做重叠匹配。 |
| `startDate` | `date` | 否 | `startDayFrom` 别名;前者未传时生效。 |
| `startDayTo` | `date` | 否 | 日期区间止,与当前行程闭区间做重叠匹配。 |
| `endDate` | `date` | 否 | `startDayTo` 别名;前者未传时生效。 |
| `vehicleTypeKeys` | `string[]` | 否 | 车型大类:`suv/mpv/bus/sedan`,任一命中。 |
| `typeKeys` | `string[]` | 否 | `vehicleTypeKeys` 别名。 |
| `driverName` | `string` | 否 | 当前司机姓名模糊匹配。 |
| `keyword` | `string` | 否 | 司机、联系人/客户、团号、订单号、当前负责定制师展示名任一包含即命中。定制师展示名为企业微信昵称优先、用户名兜底;order-v3 降级时回退派单快照,只匹配后端最终解析出的一个展示名。 |
| `contactName` | `string` | 否 | 联系人/客户名模糊匹配。 |
| `contactKeyword` | `string` | 否 | `contactName` 别名。 |
| `teamNo` | `string` | 否 | 团号包含匹配,例如 `7218` 可命中 `26-7218`。 |
| `consultantId` | `string` | 否 | 当前负责定制师管理员 ID 精确匹配。 |
| `plannerName` | `string` | 否 | 定制师显示名模糊匹配兼容参数。 |
| `consultantName` | `string` | 否 | `plannerName` 别名。 |
| `variant` | `string` | 否 | `list` 默认;`grid` 为兼容值,其他值返回参数错误。 |
| `page` | `int` | 列表否 | 默认 1;汇总忽略。 |
| `pageSize` | `int` | 列表否 | 默认 20、最大 100;汇总忽略。 |
状态值:
```text
unassigned / unassigned_urgent / holding / holding_urgent /
assigned / change_requested / completed / canceled
```
状态含义由后端 `statusOptions` 返回。`holding` 是“车务已排车、司机尚未完成确认链路”,不是“车务正在浏览详情”。
### 3.2 汇总请求示例
```http
GET /admin/fleet/board/summary?startDate=2026-07-01&endDate=2026-07-31&typeKeys=suv&keyword=王
Authorization: Bearer <fleet-manager-token>
```
### 3.3 汇总响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"pendingCount": 3,
"pendingUrgentCount": 1,
"todayDepartCount": 1,
"idleVehicleCount": 9,
"idleDriverCount": 5,
"holdingTimeoutCount": 1,
"statusCounts": {
"unassigned": 3,
"holding": 1,
"assigned": 2,
"changeRequested": 0,
"completed": 4,
"canceled": 1,
"unassignedUrgent": 1,
"holdingUrgent": 1
},
"statusOptions": [
{
"value": "unassigned",
"label": "待派车",
"count": 3,
"urgentCount": 1
},
{
"value": "holding",
"label": "排车中",
"count": 1,
"urgentCount": 1
},
{
"value": "assigned",
"label": "已派车",
"count": 2,
"urgentCount": 0
}
],
"consultantOptions": [
{
"value": "2000000000000000001",
"label": "企业微信昵称"
}
]
}
}
```
一致性规则:
```text
同一组非状态筛选条件下:
summary.statusOptions[value=X].count
== orders?statuses=X 返回的 data.total
```
`idleVehicleCount/idleDriverCount` 是当前物理资源指标,不受订单文字筛选影响;其他订单状态计数使用同一筛选后的记录集。
### 3.4 列表请求示例
```http
GET /admin/fleet/board/orders?page=1&pageSize=20&statuses=unassigned,holding&keyword=7218&consultantId=2000000000000000001
Authorization: Bearer <fleet-manager-token>
```
### 3.5 列表响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"page": 1,
"pageSize": 20,
"total": 1,
"records": [
{
"id": "HL202607180001",
"orderNo": "HL202607180001",
"orderId": "2000000000000000101",
"teamNo": "26-7218",
"assignmentId": "2000000000000000201",
"assignmentGroupId": "2000000000000000201",
"fleetItemIndex": 0,
"customerName": "测试联系人",
"contactName": "测试联系人",
"productName": "测试产品",
"headcount": 4,
"adultCount": 3,
"childCount": 1,
"youngChildCount": 0,
"babyCount": 0,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"days": 3,
"pickupAt": null,
"dropoffAt": null,
"isHailarPickup": false,
"isHailarDropoff": false,
"consultantId": "2000000000000000001",
"plannerName": "企业微信昵称",
"consultantName": "企业微信昵称",
"consultantDisplayName": "企业微信昵称",
"specialTags": ["中文司机", "大行李空间"],
"requirementRemark": "无大交通,按行程安排车辆",
"requiredVehicles": [
{
"vehicleType": "suv",
"categoryLabel": "SUV系列",
"seats": 7,
"count": 1
}
],
"assignmentStatus": "unassigned",
"assignmentStatusLabel": "待派车",
"lifecycleStageCode": "requirement_pending",
"lifecycleStageLabel": "待车务派车",
"currentStep": 1,
"availableActionCodes": ["ASSIGN", "REJECT_REQUIREMENT"],
"urgentBadge": null,
"canAssign": true,
"canRejectRequirement": true
}
]
}
}
```
### 3.6 列表字段绑定规则
| 字段 | 前端规则 |
|---|---|
| `teamNo` | 展示当前团号;空值不回退拼造。 |
| `contactName` | 卡片联系人。 |
| `consultantDisplayName` | 定制师展示名,企业微信昵称优先、用户名兜底。 |
| `startDate/endDate/days` | 当前订单档期,闭区间含首尾。 |
| `specialTags/requirementRemark` | 当前有效用车需求,不得混入历史需求。 |
| `assignmentStatusLabel/lifecycleStageLabel` | 直接展示,前端不维护独立中文映射。 |
| `availableActionCodes/canAssign/canRejectRequirement` | 决定操作入口;急单样式不得隐藏按钮。 |
列表按派车组聚合;底层一天一条派车切片不会把同一派车组重复成多张卡片。紧急待处理在前、普通进行中次之、终态沉底,同优先级以稳定 ID 兜底;前端不得二次排序。
## 四、派单详情
### 4.1 请求
```http
GET /admin/fleet/board/orders/2000000000000000101
Authorization: Bearer <fleet-manager-token>
```
路径参数是数字订单 ID,按字符串传递,不是订单号。
### 4.2 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"id": "HL202607180001",
"orderNo": "HL202607180001",
"teamNo": "26-7218",
"customerName": "测试联系人",
"headcount": 4,
"adultCount": 3,
"childCount": 1,
"youngChildCount": 0,
"babyCount": 0,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"pickupAt": null,
"dropoffAt": null,
"productName": "测试产品",
"consultantId": "2000000000000000001",
"plannerName": "企业微信昵称",
"consultantName": "企业微信昵称",
"consultantDisplayName": "企业微信昵称",
"specialTags": ["中文司机", "大行李空间"],
"requirementRemark": "无大交通,按行程安排车辆",
"itinerary": {
"theme": "草原三日",
"route": "海拉尔 → 额尔古纳 → 满洲里",
"days": [
{
"dayNumber": 1,
"date": "2026-07-20",
"title": "抵达海拉尔",
"detail": "市区行程"
},
{
"dayNumber": 2,
"date": "2026-07-21",
"title": "额尔古纳",
"detail": "草原行程"
},
{
"dayNumber": 3,
"date": "2026-07-22",
"title": "满洲里",
"detail": "返程"
}
]
},
"transport": {
"transferTimeHint": "暂无接送机时间",
"arrive": null,
"depart": null,
"batches": [],
"pickupRequired": null
},
"currentAssignment": {
"id": "2000000000000000201",
"assignmentGroupId": "2000000000000000201",
"fleetItemIndex": 0,
"requiredVehicleType": "suv",
"requiredSeats": 7,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"vehicleId": "2000000000000000301",
"vehiclePlate": "蒙A·TEST1",
"driverId": "2000000000000000401",
"driverName": "测试司机",
"baseAssignmentStatus": "assigned",
"assignmentStatus": "assigned",
"lifecycleStageCode": "confirmed",
"lifecycleStageLabel": "已确认执行",
"currentStep": 4,
"availableActionCodes": ["CANCEL", "CHANGE_DRIVER", "COMPLETE_EARLY"],
"protocolPrice": "1300.00",
"driverConfirmationEvidenceFileIds": []
},
"activeAssignments": [
{
"assignmentGroupId": "2000000000000000201",
"fleetItemIndex": 0,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"vehiclePlate": "蒙A·TEST1",
"driverName": "测试司机",
"assignmentStatus": "assigned"
}
],
"operationLog": {
"records": [],
"total": 0,
"summary": {"totalCount": 0}
},
"relatedDetailReady": true
}
}
```
### 4.3 当前需求和派车组隔离
- `currentAssignment` 是最新一个有效派车组的兼容字段。
- `activeAssignments` 才是当前需求下全部有效派车组;一单多车时必须渲染完整数组。
- 历史需求、已取消组和旧订单快照不能进入当前需求详情。
- `baseAssignmentStatus` 是落库基础态;`assignmentStatus` 是当前有效状态,前端展示后者。
### 4.4 逐日行程规则
- `itinerary.days` 来自 `order_itinerary_day`,一天一条事实数据。
- 返回日期必须位于当前 `[startDate,endDate]`,按 `dayNumber` 稳定排序。
- 改期后按当前订单档期对齐;越界、旧版本和非法日序数据不返回。
- 无有效行程时返回 `days=[]`,前端显示空态,不得生成假行程。
### 4.5 大交通规则
有数据时:
- 到达接客使用大交通 `arriveTime`。
- 返程送客使用大交通 `departTime`。
- 分批接送完整返回 `batches`。
无数据时固定返回:
```json
{
"transport": {
"transferTimeHint": "暂无接送机时间",
"arrive": null,
"depart": null,
"batches": [],
"pickupRequired": null
}
}
```
**空大交通是正常业务状态,不是参数错误,也不是派车阻断条件。** `pickupAt/dropoffAt` 同样允许为 `null`。
## 五、车辆与司机候选
### 5.1 请求
```http
POST /admin/fleet/assignments/candidates
Authorization: Bearer <fleet-manager-token>
Content-Type: application/json
```
```json
{
"orderId": "2000000000000000101",
"requirementId": "2000000000000000501",
"fleetItemIndex": 0,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"headcount": 4,
"selectedVehicleId": null,
"selectedDriverId": null,
"excludeAssignmentId": null,
"vehicleKeyword": "GL8",
"driverKeyword": "张",
"vehiclePage": 1,
"vehiclePageSize": 20,
"driverPage": 1,
"driverPageSize": 20
}
```
`pickupAt/dropoffAt` 未出现是合法请求;有城市信息时可额外传入,供城市衔接判断使用。
### 5.2 请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `orderId` | `string` | 改派时是 | 当前订单 ID。 |
| `requirementId` | `string` | 改派时是 | 当前有效用车需求 ID。 |
| `fleetItemIndex` | `int` | 否 | 当前车型项序号,从 0 开始。 |
| `startDate/endDate` | `date` | 是 | 请求用车闭区间。 |
| `pickupAt/dropoffAt` | `string` | 否 | 城市衔接辅助信息;空值不阻断候选和派车。 |
| `headcount` | `int` | 否 | 乘客人数,不含司机。车辆乘客容量=`seats-1`。 |
| `selectedVehicleId` | `string` | 否 | 已选车辆,支持先选车。 |
| `selectedDriverId` | `string` | 否 | 已选司机,支持先选司机。 |
| `excludeAssignmentId` | `string` | 否 | 改派时排除当前派单,且必须属于当前订单和需求。 |
| `vehicleKeyword` | `string` | 否 | 车牌或车型。 |
| `driverKeyword` | `string` | 否 | 司机姓名或完整手机号。 |
| `vehiclePage/driverPage` | `int` | 是 | 各自分页页码,默认 1。 |
| `vehiclePageSize/driverPageSize` | `int` | 是 | 各自每页条数,最大 100。 |
### 5.3 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"vehicles": {
"records": [
{
"vehicleId": "2000000000000000301",
"plate": "蒙A·TEST1",
"modelName": "测试车型",
"seats": 7,
"passengerCapacity": 6,
"seatsEnough": true,
"fleet": "own",
"selected": false,
"available": true,
"availabilityReasonCode": "AVAILABLE",
"availabilityReasonMessage": "所选服务日期内可用",
"availabilityWindows": [
{"startDate": "2026-07-20", "endDate": "2026-07-22"}
],
"residentMatch": false,
"crossResident": false,
"requiresCrossResidentConfirmation": false,
"relationMessage": null,
"conflicts": []
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"drivers": {
"records": [
{
"driverId": "2000000000000000401",
"name": "测试司机",
"maskedPhone": "138****0000",
"years": 8,
"season": "active",
"completedOrderCount": 12,
"rating": 5.0,
"ratingDefaulted": true,
"selected": false,
"available": true,
"availabilityReasonCode": "CITY_JUNCTION_SHAREABLE",
"availabilityReasonMessage": "仅存在可衔接的同城边界占用",
"availabilityWindows": [
{"startDate": "2026-07-20", "endDate": "2026-07-22"}
],
"residentMatch": false,
"crossResident": false,
"requiresCrossResidentConfirmation": false,
"conflicts": []
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"selectedRelation": null
}
}
```
### 5.4 候选判定规则
| `availabilityReasonCode` | 含义 | `available` |
|---|---|---|
| `AVAILABLE` | 整个请求闭区间无阻塞占用 | `true` |
| `CITY_JUNCTION_SHAREABLE` | 只有满足规则的城市边界衔接 | `true` |
| `ASSIGNMENT_CONFLICT` | 请求区间内存在阻塞派单 | `false` |
- `availabilityWindows` 是请求区间内的实际可用**闭区间**,冲突会切分时间窗。
- 车辆和司机都必须覆盖完整请求区间才可直接选中。
- 司机无评价时返回 `rating=5.0` 且 `ratingDefaulted=true`;有真实评价时为 `false`。
- 车辆总座位数包含司机,`passengerCapacity=seats-1`;前端不得把司机座位再次给乘客。
- `selectedRelation` 在车辆和司机都已选时返回常驻关系。跨常驻组合必须显示后端提示并显式确认。
## 六、矩阵月视图
### 6.1 请求
```http
GET /admin/fleet/matrix/grid?year=2026&month=7&season=active&fleets=own,coopA&typeKeys=suv&status=all
Authorization: Bearer <fleet-manager-token>
```
### 6.2 请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `year` | `int` | 是 | 查询年份。 |
| `month` | `int` | 是 | 1-12,越界返回车务月份错误。 |
| `season` | `string` | 否 | `active` 默认;也支持 `pending/archived/blacklist`。 |
| `fleets` | `string[]` | 否 | `own/coopA/coopB` 多选。 |
| `typeKeys` | `string[]` | 否 | `suv/mpv/bus/sedan` 多选。 |
| `status` | `string` | 否 | `all` 默认、`unassigned`、`assigned`。 |
### 6.3 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"year": 2026,
"month": 7,
"daysInMonth": 31,
"todayDay": 18,
"weekendDays": [4, 5, 11, 12, 18, 19, 25, 26],
"fleetCount": {"own": 5, "coopA": 4, "coopB": 3},
"statusCounts": {
"totalAssignments": 21,
"unassignedAssignments": 10,
"assignedAssignments": 11,
"totalOrders": 16,
"unassignedOrders": 5,
"partialOrders": 2,
"assignedOrders": 11
},
"unassignedWindowCount": 5,
"vehicles": [
{
"id": "2000000000000000301",
"plate": "蒙A·TEST1",
"modelName": "测试车型",
"seats": 7,
"fleet": "own",
"primaryDriverName": "测试司机",
"primaryDriverPhone": "138****0000",
"assignments": [
{
"id": "2000000000000000201",
"assignmentGroupId": "2000000000000000201",
"orderNumericId": "2000000000000000101",
"orderNo": "HL202607180001",
"teamNo": "26-7218",
"consultantId": "2000000000000000001",
"consultantName": "企业微信昵称",
"customerName": "测试联系人",
"headcount": 4,
"adultCount": 3,
"childCount": 1,
"startDay": 20,
"endDay": 22,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"clippedHead": false,
"clippedTail": false,
"vehicleCategory": "suv",
"categoryLabel": "SUV系列",
"assignmentStatus": "assigned",
"protocolPrice": "1300.00",
"vehicleSpecialTags": ["中文司机"],
"vehicleRequirementRemark": "按行程安排",
"pickupTransports": [],
"dropoffTransports": [],
"parallelAssignments": [
{
"assignmentGroupId": "2000000000000000201",
"fleetItemIndex": 0,
"vehicleCategory": "suv",
"categoryLabel": "SUV系列",
"requiredSeats": 7,
"startDate": "2026-07-20",
"endDate": "2026-07-22",
"vehicleId": "2000000000000000301",
"vehiclePlate": "蒙A·TEST1",
"driverId": "2000000000000000401",
"driverName": "测试司机",
"assignmentStatus": "assigned",
"protocolPrice": "1300.00"
}
]
}
],
"connections": [
{
"fromAssignmentGroupId": "2000000000000000201",
"toAssignmentGroupId": "2000000000000000202",
"fromEndDate": "2026-07-22",
"toStartDate": "2026-07-23",
"previousDepartureTime": null,
"nextArrivalTime": null,
"previousCity": null,
"nextCity": null,
"connectionMinutes": null,
"thresholdMinutes": 120,
"status": "MISSING_TIME",
"statusLabel": "缺少接送时间",
"style": "RED_DASHED",
"reasonCode": "PREVIOUS_REQUIRED_DEPARTURE_MISSING",
"reasonMessage": "前一订单缺少必需送客批次"
}
]
}
]
}
}
```
### 6.4 派单统计口径
- `totalAssignments` 是派车行数,不是订单数。
- `totalOrders` 是去重订单数。
- 一单同时存在已派和未派项时计入 `partialOrders`,也计入 `unassignedOrders`。
- `unassignedWindowCount` 是含未派项的去重订单数,不等于未派逐日切片条数。
- 跨月派车组使用完整原始 `startDate/endDate`,仅 `startDay/endDay` 裁剪到当前月;`clippedHead/clippedTail` 告诉前端是否跨月续接。
### 6.5 相邻订单衔接四态
| `status` | `style` | 含义 |
|---|---|---|
| `MISSING_TIME` | `RED_DASHED` | 缺少必需大交通、时刻或城市,无法完成衔接判断。 |
| `DIFFERENT_CITY` | `DARK_RED` | 前后订单城市不同。 |
| `SAME_CITY_TOO_SHORT` | `LIGHT_RED` | 同城但间隔小于配置阈值。 |
| `SAME_CITY_OK` | `GREEN` | 同城且间隔达到配置阈值。 |
原因码:
```text
PREVIOUS_REQUIRED_DEPARTURE_MISSING
NEXT_REQUIRED_ARRIVAL_MISSING
PREVIOUS_DEPARTURE_TIME_MISSING
NEXT_ARRIVAL_TIME_MISSING
PREVIOUS_DEPARTURE_CITY_MISSING
NEXT_ARRIVAL_CITY_MISSING
DIFFERENT_CITY
SAME_CITY_INTERVAL_TOO_SHORT
SAME_CITY_INTERVAL_SUFFICIENT
```
同城最小衔接阈值读取 Nacos 配置,默认 120 分钟;恰好等于阈值属于 `SAME_CITY_OK`。
注意:`MISSING_TIME` 是矩阵风险提示,不表示订单不能派车。用户未填写大交通时仍允许完成派车。
## 七、出行人权限与审计
### 7.1 脱敏列表
```http
GET /admin/fleet/board/orders/2000000000000000101/travelers
Authorization: Bearer <fleet-manager-token>
```
默认返回姓名、年龄类型和脱敏证件/手机号;前端日常派车只使用此接口。
### 7.2 明文查询
```http
POST /admin/fleet/board/orders/2000000000000000101/travelers/plain
Authorization: Bearer <fleet-manager-token>
Content-Type: application/json
```
```json
{
"reason": "司机出发前核对接客人信息"
}
```
明文接口必须经过权限校验并记录操作人、订单、理由和时间。前端不得缓存、日志打印或二次持久化明文个人信息。
## 八、前端必须处理
1. 看板使用分页接口,分页器读取 `data.total/page/pageSize`。
2. 状态项、状态中文、急单数读取 `summary.statusOptions`,不维护独立枚举和独立计数。
3. 汇总请求必须携带与列表相同的非状态筛选;不要把当前状态筛选传成汇总统计范围。
4. 卡片展示 `teamNo/contactName/headcount/consultantDisplayName/startDate/endDate/specialTags/requirementRemark`。
5. 定制师选择值传 `consultantId`;显示使用后端返回的企业微信优先名称。
6. 统一文本框传 `keyword`,可同时匹配司机、联系人、团号、订单号和当前定制师展示名;定制师按企业微信昵称优先、用户名兜底,前端不得自行并行匹配多个名称别名。
7. 操作按钮使用 `availableActionCodes/canAssign/canRejectRequirement`,不能按颜色或前端状态猜测。
8. 详情多车读取 `activeAssignments`;`currentAssignment` 只是兼容的最新一组。
9. `transport.transferTimeHint="暂无接送机时间"` 时显示提示,但保持派车入口可用。
10. 候选资源分别读取 `vehicles/drivers` 分页;显示后端原因和常驻关系提示。
11. 矩阵直接使用 `connections[].status/style/reasonCode/reasonMessage`;大红、淡红、虚线红和绿色语义不能自行交换。
12. 所有雪花 ID 当字符串处理。
## 九、兼容性与不影响范围
- 保留 `status/startDate/endDate/typeKeys/contactKeyword/consultantName` 等兼容别名。
- 不新增前端内部接口,不改变现有派车写接口。
- 不要求填写大交通,也不把空大交通改成校验错误。
- 不改变“一天一条派车切片”的数据库事实模型;本轮只修正读模型聚合。
- 不处理团期配车。
- 不修改 `hl-ui`。
## 十、验证证据
### 10.1 代码与测试
```text
hl-fleet-service 定向测试:253/253 通过
hl-fleet-service 全量 verify:1799/1799 通过
hl-order-service-v3 相关契约测试:30/30 通过
独立代码评审:无 P0/P1 阻断项
OpenAPI 说明定向校验:spotless:check + compile 通过
```
Order-v3 全量测试中 4 项环境/基线失败已在同提交干净基线复现:3 项为 H2 缺少 `payment_manual_receipt`,1 项为既有本地缓存架构门禁;不由本次车务改动引入。
### 10.2 部署
```text
hl-order-service-v3:Deploy Panel 任务 22ec81e8,8086/8186 双实例成功
hl-fleet-service:Deploy Panel 任务 68487f94,8087/8187 双实例成功
hl-fleet-service OpenAPI 口径补充:Deploy Panel 任务 249594e3,8087/8187 双实例成功
```
部署后已通过网关读取车务 OpenAPI,确认线上文档明确区分“同一订单筛选范围内的状态计数”与“不随订单筛选变化的全局空闲资源数”,并包含统一关键词对当前定制师展示名的匹配规则。
### 10.3 真实网关 API
使用独立车务和定制师测试账号,经 `https://api.test.1814.love:9443` 完成真实订单全流程回归;未使用 `admin`、`wx` 或 Mock 数据。
```text
最终回归:161/161 通过,失败 0
覆盖:看板汇总/列表/详情、统一筛选、定制师、脱敏/明文出行人、候选车辆/司机、
常驻与跨常驻、预检、派车、司机确认、取消/恢复/改派/提前完结、矩阵、价格、
车辆/司机、对账、模板,以及无大交通订单完整派车链路。
```
无大交通真实场景额外断言:
```text
- 新建真实订单并补齐 2 名出行人
- 不创建任何大交通计划
- 成功提交有效用车需求
- 详情:arrive=null、depart=null、batches=[]、transferTimeHint=暂无接送机时间
- 候选查询成功,pickupAt/dropoffAt 均省略
- 派车预检成功,conflict=false
- 直派成功并进入已派车状态
- DB:派车组逐日 6 条,pickup_at/dropoff_at 6 条均为空
```
## 十一、相关文档
- 看板字段、统计、行程和保险事务:`57_4882_车务派单看板当前订单字段统计行程与保险事务收口-管理后台.md`
- 需求级最终完成回调:`59_4935_车务需求级派单完成回调与滚动发布契约-管理后台.md`
- 提需求和派单基础契约:`53_4871_车务提需求派单看板闭环契约-管理后台.md`
- 团号与定制师筛选:`54_4876_派单看板团号与定制师下拉筛选-管理后台.md`
@@ -0,0 +1,213 @@
# 【前端对接·管理后台】房务待办新增“待最终确认”派生项
> Issue: [wx/HL#5053](https://git.1814.love:8443/wx/HL/issues/5053)
>
> PR: [wx/HL#5059](https://git.1814.love:8443/wx/HL/pulls/5059)
>
> 合并提交: `989318c3925b`
>
> 服务: `hl-order-service-v3` / `hl-user-service`
>
> 日期: 2026-07-18
>
> 影响范围: 房务“待处理”列表、待办类型筛选、订单详情跳转、房务工作台待办统计
## 一、对接结论
1. 房务“待处理”页的唯一主数据源仍是 `GET /v3/admin/order/todos`,不要改用任何 `my-claims` 接单列表接口。`my-claims` 不返回完整的待办类型聚合,不能替代待办接口。
2. 待办接口新增可选类型 `PENDING_FINALIZE`,中文标签为“待最终确认”。它是查询时派生的虚拟待办,不落 `house_todo`,因此 `derived=true`、标签级 `todoId=null`。
3. 当前房务持有的 active 住宿需求处于 `status=PROCESSING`、`houseStatus=PENDING_FINALIZE` 时,接口返回该派生项;最终确认后需求进入 `DONE/CONFIRMED`,该项从列表、筛选结果和统计中自然消失。
4. `list[].todoTypes[]` 是一订单多标签的权威数据。点击 `PENDING_FINALIZE` 标签时必须使用该标签自己的 `requirementId`,不能依赖聚合行顶层 `requirementId`。
5. 派生项不可调用待办 `RESOLVE`。用户应打开 `OrderDetailModal` 完成“最终确认”,成功后刷新待办列表与工作台仪表盘。
6. 房务工作台 `GET /admin/profile/dashboard` 的 `todoSummary` 同步新增大写键 `PENDING_FINALIZE`。
7. 本次没有修改 `hl-ui`;下文列出的现有前端筛选和标签级参数问题需由前端处理。
## 二、待办接口变化
### 2.1 请求
```http
GET /v3/admin/order/todos?scope=mine&status=OPEN&page=1&pageSize=20
Authorization: Bearer <room-manager-token>
```
只看“待最终确认”时:
```http
GET /v3/admin/order/todos?scope=mine&status=OPEN&todoType=PENDING_FINALIZE&page=1&pageSize=20
Authorization: Bearer <room-manager-token>
```
查询参数名是 `todoType`,不是 `type`。`todoType` 支持逗号分隔多选。
### 2.2 响应示例
```json
{
"code": 200,
"data": {
"list": [
{
"id": null,
"orderId": "2000000000000000001",
"orderNo": "HL202607180001",
"todoType": "PENDING_FINALIZE",
"todoTypeLabel": "待最终确认",
"title": "待最终确认",
"urgency": "normal",
"status": "OPEN",
"derived": true,
"requirementId": "2000000000000000101",
"orderTodoCount": 1,
"todoTypes": [
{
"typeCode": "PENDING_FINALIZE",
"typeLabel": "待最终确认",
"urgency": "normal",
"count": 1,
"derived": true,
"todoId": null,
"requirementId": "2000000000000000101",
"unreadCount": null
}
]
}
],
"total": 1,
"stats": {
"PENDING_FINALIZE": 1
}
}
}
```
字段规则:
| 字段 | 前端规则 |
|---|---|
| `list[].todoTypes[]` | 一订单多待办类型的权威标签数组,必须完整渲染。 |
| `todoTypes[].typeCode` | 新增可选值 `PENDING_FINALIZE`。 |
| `todoTypes[].typeLabel` | 直接展示后端中文“待最终确认”。 |
| `todoTypes[].derived` | `true` 表示虚拟待办,由源业务状态自然消失。 |
| `todoTypes[].todoId` | `PENDING_FINALIZE` 固定为 `null`,禁止调用 `RESOLVE`。 |
| `todoTypes[].requirementId` | 打开该标签对应房务详情时使用;雪花 ID 按字符串透传。 |
| `list[].requirementId` | 只兼容聚合行主标签;一行多标签时不能替代标签级字段。 |
| `stats.PENDING_FINALIZE` | 当前 scope 内“待最终确认”需求数;无数据也返回 `0`。 |
`stats` 是当前 scope 的完整分类计数,不因本次 `todoType` facet 收窄;因此筛选结果 `total` 可以是 `1`,同时其他统计槽仍保留其真实值。
### 2.3 生命周期
```text
当前房务 + active requirement
status=PROCESSING + houseStatus=PENDING_FINALIZE
→ /todos 出现 PENDING_FINALIZE 派生标签
→ 用户从该标签进入订单详情并执行最终确认
→ status=DONE + houseStatus=CONFIRMED
→ /todos、todoType facet、stats 和 dashboard 中该项均消失/归零
```
该链路不创建 `house_todo` 记录,也不改变既有持久化待办的 RESOLVE 语义。
## 三、房务工作台统计变化
房务角色调用:
```http
GET /admin/profile/dashboard?period=today
Authorization: Bearer <room-manager-token>
```
`data.todoSummary` 新增:
```json
{
"total": 4,
"PENDING_FINALIZE": 1
}
```
- 键名固定为大写 `PENDING_FINALIZE`,与待办接口 `stats`、`todoTypes[].typeCode` 共用同一常量。
- 最终确认后该值归零;前端刷新列表时应同时刷新工作台数据或使对应查询缓存失效。
## 四、前端必须处理的现有问题
### 4.1 筛选器混用了“房务状态”和“待办类型”
当前 `src/views/housekeeper/todos/index.vue:220-227` 把以下值放在同一个 `STATUS_OPTIONS` 中:
```text
HOTEL_REPLY_TIMEOUT / PENDING_ARRANGE /
CLAIMING / IN_INQUIRY / PENDING_FINALIZE / EXCEPTION
```
但同文件 `:370` 固定请求 `status=OPEN`,`:374` 又把所有非“全部”选项都作为 `todoType`,最终由 `:386` 请求待办接口。
- `HOTEL_REPLY_TIMEOUT`、`PENDING_ARRANGE`、`PENDING_FINALIZE` 是合法 `HouseTodoType`。
- `CLAIMING`、`EXCEPTION` 是房务业务状态,不是待办类型,作为 `todoType` 请求会得到空结果。
- `IN_INQUIRY` 已不再是顶层房务状态,也不是待办类型。
前端应删除这三个无效 `todoType` 选项;如产品确实需要按房务状态筛选,应另行使用声明支持该参数的数据源,不能继续混传给 `/todos.todoType`。
同时,`src/api/housekeeper/todos.js:53` 的注释仍写 `params.type`,实际页面和后端均使用 `params.todoType`;请同步修正文档注释,API 调用本身仍是 `:66-67` 的 `/v3/admin/order/todos`。
### 4.2 标签级 `requirementId` 在映射时丢失
当前 `src/views/housekeeper/todos/index.vue:290-313` 把 `todoTypes[]` 映射成 `reasonTags` 时只保留了 `typeCode/label/derived`,未保留标签自己的 `requirementId`;`:267` 和 `:453` 只使用聚合行顶层 `requirementId`。
一订单多标签时,顶层字段属于“主标签”,不保证就是用户点击的 `PENDING_FINALIZE` 标签。建议保留标签字段并按点击项分发:
```js
const pendingFinalizeTag = row.todoTypes.find(
(item) => item.typeCode === 'PENDING_FINALIZE'
)
openOrderDetail({
orderId: row.orderId,
requirementId: pendingFinalizeTag?.requirementId,
claimScope: 'mine',
})
```
实际组件仍应复用现有 `OrderDetailModal`,传入:
```vue
<OrderDetailModal
:order-id="orderId"
:requirement-id="requirementId"
claim-scope="mine"
/>
```
所有 ID 均按字符串处理,禁止转为 JavaScript `Number`。
### 4.3 完成动作
`PENDING_FINALIZE` 的处理入口是订单详情内“最终确认”,不是待办 `RESOLVE`:
1. 从点击标签取得 `orderId + todoTypes[].requirementId`。
2. 以 `claimScope=mine` 打开 `OrderDetailModal`。
3. 用户执行一次“最终确认”。
4. 成功后刷新 `/v3/admin/order/todos` 和 `/admin/profile/dashboard`。
5. 列表行、筛选计数和工作台徽章应同步消失/归零。
## 五、已完成的后端与测试环境验证
- PR #5059 已合并到 `dev-v3`,合并提交为 `989318c3925b`。
- `hl-order-service-v3` TEST 部署任务 `fd8522fe` 成功,`8086/8186` 双实例健康。
- `hl-user-service` TEST 部署任务 `372a9f3e` 成功,`8081/8181` 双实例健康。
- 网关 API 实测:进入待最终确认前 `PENDING_FINALIZE=0`;测试需求进入 `PROCESSING/PENDING_FINALIZE` 后,列表、facet、`stats` 和 dashboard 均为 `1`;最终确认后均恢复为 `0`。
- TEST DB 对账:派生项出现时没有新增 `house_todo(PENDING_FINALIZE)`;最终确认后需求为 `DONE/CONFIRMED`,两晚配房仍为 `CONFIRMED`,房务归属和配房数据均保留。
- 真实调试 Chrome 已验证“待最终确认”标签可见、筛选只剩目标订单、最终确认后列表空态;浏览器验收仅作为前端对接参考,不改变上述接口契约。
## 六、前端验收清单
- [ ] “待处理”页仅以 `GET /v3/admin/order/todos` 为主数据源。
- [ ] `STATUS_OPTIONS` 不再把 `CLAIMING/IN_INQUIRY/EXCEPTION` 作为 `todoType` 发送。
- [ ] 使用 `todoType=PENDING_FINALIZE` 可筛出“待最终确认”订单。
- [ ] 完整渲染 `list[].todoTypes[]`,显示后端 `typeLabel`。
- [ ] 映射和点击事件保留 `todoTypes[].requirementId`。
- [ ] 以 `orderId + 标签级 requirementId + claimScope=mine` 打开 `OrderDetailModal`。
- [ ] 派生标签不调用待办 `RESOLVE`。
- [ ] 最终确认成功后刷新待办列表和 dashboard,标签与计数同步消失。
- [ ] 所有雪花 ID 均按字符串透传。
@@ -0,0 +1,510 @@
# 【#4938 前端对接·管理后台】车务创建、修改与最终确认返回订单调整基线差异
> Issue: [wx/HL#4938](https://git.1814.love:8443/wx/HL/issues/4938)
>
> PR: [wx/HL#5065](https://git.1814.love:8443/wx/HL/pulls/5065)
>
> 服务: `hl-fleet-service`
>
> 日期: 2026-07-18
>
> 影响范围: 车务直接派车、直接改派、`holding → assigned` 最终确认,以及订单调整后的日期、人数、车辆容量差异处理
>
> 状态: 后端已合并并部署测试环境;待管理后台按本文完成页面联调
## 一、前端对接结论
以下三个既有管理后台接口现在共用业务码 `605041` 返回结构化逐日差异:
| 操作 | 接口 | 触发 605041 的条件 |
|---|---|---|
| 创建派单 | `POST /admin/fleet/assignments` | `holdMode=0` 直接派车时最终基线不一致 |
| 修改派单 | `POST /admin/fleet/assignments/{assignmentId}/change` | `holdMode=0` 直接改派时最终基线不一致 |
| 最终确认 | `POST /admin/fleet/assignments/{assignmentId}/confirm` | 订单、需求、行程、逐日派单、人数或容量基线不一致 |
前端必须遵守以下判断:
1. 三个接口的 `605041` 当前均返回 HTTP 200,但统一响应体为 `code=605041`、`success=false`,不能只判断 HTTP 状态。
2. `605041` 的 `data` 不为空:
- create 返回 `AssignmentWriteRespVO`,保证 `data.dailyDifferences` 可读;
- change 返回 `ChangeAssignmentRespVO`,保证 `data.dailyDifferences` 可读;
- confirm 返回 `ConfirmRespVO`,保证 `data.confirmed=false` 和 `data.dailyDifferences` 可读。
3. `605041` 不会提交派单及其关联业务状态写入:
- create 不会新增或激活派单;
- change 不会取消旧派单,也不会生成可用的新派车版本;
- confirm 不会推进派单状态或确认时间;
- 三者都不会触发车辆/司机占用、保险、对账或订单派定结果变化。
- change 仍会按既有设计在独立事务保留一条 `CHANGE_FAILED` 操作审计;它不是有效派单版本,也不表示业务写入成功。
4. 收到 `605041` 后保持操作前页面状态,展示逐日差异,并重新拉取最新订单和车务详情。
5. 所有派单、车辆槽位、派车组、订单、车辆和司机雪花 ID 均按 JSON String 发送和读取,禁止转为 JavaScript `Number`。金额字段也按 String 读取。
## 二、605041 公共响应契约
### 2.1 统一响应外层
```json
{
"code": 605041,
"message": "订单行程或用车派单已变化,请按逐日差异处理后重试",
"data": {
"dailyDifferences": [
{
"serviceDate": "2026-07-22",
"differenceType": "CAPACITY_INSUFFICIENT",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": 5,
"capacityGap": 3,
"message": "车辆载客量不足,已按每车司机占一座计算"
}
]
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938f09a001",
"success": false
}
```
注意:
- `data` 的完整类型取决于调用的是 create、change 还是 confirm,不能跨接口复用成功响应模型。
- 除本文件明确保证的失败字段外,其余成功态字段在 `605041` 时为空,前端不得用它们推断写入结果。
- `traceId` 用于反馈和日志定位,不参与业务判断。
### 2.2 `dailyDifferences[]`
| 字段 | 类型 | 说明 |
|---|---|---|
| `serviceDate` | String/null | 发生差异的服务日,格式 `yyyy-MM-dd`;无法定位到单日时为空 |
| `differenceType` | String | 差异类型,取值见下表 |
| `assignmentId` | String/null | 可定位到具体派单时返回;仅用于定位差异,不代表本次写入成功 |
| `assignmentSlotId` | String/null | 可定位到稳定车辆槽位时返回 |
| `passengerCount` | Integer/null | 当前比对使用的乘客人数,不含司机 |
| `passengerCapacity` | Integer/null | 当日车辆合计可载客人数,每辆车已扣除司机一座 |
| `capacityGap` | Integer/null | 缺少座位数,等于 `passengerCount - passengerCapacity` |
| `message` | String | 后端生成的差异说明,可直接辅助展示 |
差异类型:
| `differenceType` | 含义 |
|---|---|
| `REQUIREMENT_VERSION_MISMATCH` | 当前生效用车需求已变化,或订单/需求已不可继续派单 |
| `ORDER_DATE_MISMATCH` | 订单当前日期与用车需求冻结日期不一致 |
| `ITINERARY_DATE_MISMATCH` | 逐日行程日期与用车需求冻结日期不一致,或缺少逐日行程 |
| `ASSIGNMENT_DATE_MISSING` | 某服务日缺少有效派单或车辆槽位 |
| `ASSIGNMENT_DATE_EXTRA` | 派单仍包含已不属于当前需求的服务日 |
| `HEADCOUNT_BASELINE_MISMATCH` | 订单当前人数、需求冻结人数或派单人数快照不一致 |
| `CAPACITY_INSUFFICIENT` | 当日所有车辆合计载客量不足 |
`dailyDifferences` 可能同时包含多种类型、多条服务日记录。前端应遍历数组展示,不得只取第一条,也不得自行重算人数或车辆容量。
## 三、创建派单 create
### 3.1 请求
```http
POST /admin/fleet/assignments
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"orderId": "2078001000000000101",
"orderNo": "26-0719",
"requirementId": "2078001000000000201",
"fleetItemIndex": 1,
"vehicleId": "2078001000000000301",
"driverId": "2078001000000000401",
"startDate": "2026-07-21",
"endDate": "2026-07-23",
"pickupAt": "海拉尔",
"dropoffAt": "满洲里",
"headcount": 8,
"protocolPrice": "1300.00",
"holdMode": 0,
"fromEntry": "from-board",
"skipCityJunctionException": false,
"strictSeats": true,
"confirmCrossResident": false,
"requestId": "fleet-create-4938-20260719-001"
}
```
`605041` 只适用于 `holdMode=0`。原有 `holdMode=1` 排车锁定流程不执行本次最终基线门禁。
### 3.2 holdMode=0 成功响应
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "2078001000000000501",
"assignmentGroupId": "2078001000000000601",
"assignmentSlotId": "2078001000000000701",
"assignmentStatus": "assigned",
"stageCode": "assigned",
"stageLabel": "已派车",
"currentStep": 4,
"skippedStepCodes": [
"DRIVER_CONFIRMATION",
"DRIVER_CONFIRMATION_EVIDENCE"
],
"protocolPrice": "1300.00",
"holdSentAt": null,
"confirmedAt": "2026-07-19 14:20:00",
"sideEffects": {
"vehicleStatusUpdated": "busy",
"driverStatusUpdated": "busy",
"reconPrepRowsCreated": 0,
"reconPrepMarkedCanceled": null
},
"dailyDifferences": null
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938c200001",
"success": true
}
```
仅在 `code=200` 时把新派单加入页面;`id`、`assignmentGroupId`、`assignmentSlotId` 均按 String 保存。
### 3.3 605041 失败响应
```json
{
"code": 605041,
"message": "订单行程或用车派单已变化,请按逐日差异处理后重试",
"data": {
"id": null,
"assignmentGroupId": null,
"assignmentSlotId": null,
"assignmentStatus": null,
"stageCode": null,
"stageLabel": null,
"currentStep": null,
"skippedStepCodes": null,
"protocolPrice": null,
"holdSentAt": null,
"confirmedAt": null,
"sideEffects": null,
"dailyDifferences": [
{
"serviceDate": "2026-07-22",
"differenceType": "CAPACITY_INSUFFICIENT",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": 5,
"capacityGap": 3,
"message": "车辆载客量不足,已按每车司机占一座计算"
}
]
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938c605041",
"success": false
}
```
此时不得把临时响应内容加入派单列表,不得本地占用车辆/司机;刷新后仍以服务端最新详情为准。
## 四、修改派单 change
### 4.1 请求
```http
POST /admin/fleet/assignments/2078001000000000501/change
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"effectiveDate": "2026-07-22",
"newVehicleId": "2078001000000000302",
"newDriverId": "2078001000000000402",
"holdMode": 0,
"protocolPrice": "1688.00",
"confirmCrossResident": false,
"reason": "订单调整后更换车辆和司机",
"requestId": "fleet-change-4938-20260719-001"
}
```
`newVehicleId`、`newDriverId` 至少传一个。`605041` 只适用于 `holdMode=0`;`holdMode=1` 仍按排车待司机确认流程处理。
### 4.2 holdMode=0 成功响应
```json
{
"code": 200,
"message": "成功",
"data": {
"assignmentId": "2078001000000000502",
"assignmentSlotId": "2078001000000000701",
"previousAssignmentGroupId": "2078001000000000601",
"newAssignmentGroupId": "2078001000000000602",
"assignmentStatus": "assigned",
"effectiveDate": "2026-07-22",
"affectedDays": 2,
"protocolPrice": "1688.00",
"otherVehicleCount": 1,
"warningCode": "ORDER_HAS_OTHER_VEHICLES",
"warningMessage": "该订单另有1个车辆槽位,当前仅修改本车辆,请核对其它车辆安排",
"otherVehicles": [
{
"assignmentSlotId": "2078001000000000702",
"vehiclePlate": "蒙B-66666",
"driverName": "李师傅",
"startDate": "2026-07-21",
"endDate": "2026-07-23"
}
],
"dailyDifferences": null
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938a200001",
"success": true
}
```
change 成功响应没有 `confirmed` 和 `sideEffects` 字段。只有 `code=200` 时才能用新派车组替换页面中的旧版本;`warningCode=ORDER_HAS_OTHER_VEHICLES` 时继续保留既有强提示。
### 4.3 605041 失败响应
```json
{
"code": 605041,
"message": "订单行程或用车派单已变化,请按逐日差异处理后重试",
"data": {
"assignmentId": null,
"assignmentSlotId": null,
"previousAssignmentGroupId": null,
"newAssignmentGroupId": null,
"assignmentStatus": null,
"effectiveDate": null,
"affectedDays": null,
"protocolPrice": null,
"otherVehicleCount": null,
"warningCode": null,
"warningMessage": null,
"otherVehicles": null,
"dailyDifferences": [
{
"serviceDate": null,
"differenceType": "HEADCOUNT_BASELINE_MISMATCH",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": null,
"capacityGap": null,
"message": "订单当前人数与用车需求冻结人数不一致"
},
{
"serviceDate": "2026-07-22",
"differenceType": "CAPACITY_INSUFFICIENT",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": 5,
"capacityGap": 3,
"message": "车辆载客量不足,已按每车司机占一座计算"
}
]
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938a605041",
"success": false
}
```
此时旧派单和原派车组仍是有效业务状态。后端可能新增一条 `CHANGE_FAILED` 操作审计,但不会生成可用的新派车版本。不得用 `dailyDifferences[].assignmentId` 替换页面主键,也不得本地切换车辆、司机或状态。
## 五、最终确认 confirm
### 5.1 请求
```http
POST /admin/fleet/assignments/2078001000000000501/confirm
Authorization: Bearer <admin-token>
Content-Type: application/json
```
```json
{
"requestId": "fleet-final-confirm-4938-20260719-001"
}
```
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| `assignmentId` | Path | String | 是 | 有效派单 ID | 当前派车组中的任一派单 ID |
| `requestId` | Body | String | 是 | 非空,最长 64 | 最终确认幂等标识 |
### 5.2 成功响应
```json
{
"code": 200,
"message": "成功",
"data": {
"confirmed": true,
"assignmentStatus": "assigned",
"stageCode": "assigned",
"stageLabel": "已派车",
"currentStep": 4,
"assignmentGroupId": "2078001000000000601",
"confirmedAt": "2026-07-19 14:30:00",
"itineraryUrl": "https://h5.example.com/#/itinerary/<signed-token>",
"sideEffects": {
"vehicleStatusUpdated": "busy",
"driverStatusUpdated": "busy",
"reconPrepRowsCreated": 0,
"reconPrepMarkedCanceled": null
},
"dailyDifferences": null
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938f200001",
"success": true
}
```
`itineraryUrl` 签发配置不可用时允许为空,不影响确认成功。前端只有在 `code=200 && data.confirmed===true` 时展示最终确认成功,并刷新派单详情、看板列表和汇总。
### 5.3 日期、人数与容量同时存在差异
```json
{
"code": 605041,
"message": "订单行程或用车派单已变化,请按逐日差异处理后重试",
"data": {
"confirmed": false,
"assignmentStatus": null,
"stageCode": null,
"stageLabel": null,
"currentStep": null,
"assignmentGroupId": null,
"confirmedAt": null,
"itineraryUrl": null,
"sideEffects": null,
"dailyDifferences": [
{
"serviceDate": "2026-07-21",
"differenceType": "ORDER_DATE_MISMATCH",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": null,
"passengerCapacity": null,
"capacityGap": null,
"message": "订单当前日期与用车需求冻结日期不一致"
},
{
"serviceDate": null,
"differenceType": "HEADCOUNT_BASELINE_MISMATCH",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": null,
"capacityGap": null,
"message": "订单当前人数与用车需求冻结人数不一致"
},
{
"serviceDate": "2026-07-22",
"differenceType": "CAPACITY_INSUFFICIENT",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": 8,
"passengerCapacity": 5,
"capacityGap": 3,
"message": "车辆载客量不足,已按每车司机占一座计算"
}
]
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938f605041",
"success": false
}
```
### 5.4 缺少某日车辆槽位
```json
{
"code": 605041,
"message": "订单行程或用车派单已变化,请按逐日差异处理后重试",
"data": {
"confirmed": false,
"assignmentStatus": null,
"stageCode": null,
"stageLabel": null,
"currentStep": null,
"assignmentGroupId": null,
"confirmedAt": null,
"itineraryUrl": null,
"sideEffects": null,
"dailyDifferences": [
{
"serviceDate": "2026-07-23",
"differenceType": "ASSIGNMENT_DATE_MISSING",
"assignmentId": null,
"assignmentSlotId": null,
"passengerCount": null,
"passengerCapacity": null,
"capacityGap": null,
"message": "该服务日缺少第2个车辆槽位派单"
}
]
},
"traceId": "7db459fd-0b8e-4f29-9c46-4938f605042",
"success": false
}
```
## 六、前端统一处理顺序
1. 先判断业务 `code`,再读取端点自己的 `data`:
- `code=200`:按对应成功模型处理;
- `code=605041`:按对应失败模型读取 `dailyDifferences`;
- 其他业务码:继续走既有错误处理。
2. `605041` 时不要乐观更新:
- create 不新增本地派单;
- change 不替换旧派单;
- confirm 不改为 `assigned` 或“已确认”。
3. 展示顶层 `message`,并按 `dailyDifferences` 列出服务日、差异类型、人数、容量和缺口;字段为空时隐藏对应展示项。
4. 重新请求最新订单和车务详情,避免继续使用操作前缓存。
5. 用户修正订单日期、行程、人数或派单后,生成新的 `requestId` 再提交新动作;仅在同一次动作网络结果不确定时复用原 `requestId`。
## 七、其他直接错误分支
| 业务码 | 常见场景 | 前端处理 |
|---|---|---|
| `605009` | 派单不存在 | 刷新详情和看板,停止操作旧记录 |
| `605020` | 当前状态不允许操作 | 刷新最新生命周期与操作能力 |
| `605025` | 最终确认前缺少司机确认或有效凭证 | 引导先完成司机确认与凭证登记 |
| `605001` / `605003` | 车辆或司机档期冲突 | 展示后端错误并重新选车/司机 |
| `605036` | 跨常驻车未显式确认 | 二次提示后携带 `confirmCrossResident=true` 重试 |
| `605041` | 订单、需求、行程、逐日派单、人数或容量基线不一致 | 使用当前端点的 `data.dailyDifferences` 展示并处理 |
请求字段为空、格式不正确或超过长度限制时走统一参数校验错误,前端应在发请求前完成同样约束。
## 八、不影响范围
- 不新增管理后台接口,三个接口的请求字段结构保持不变。
- `holdMode=1` 的排车锁定、司机确认与凭证登记流程保持不变。
- 不改变取消、司机拒接、驳回需求、撤销取消和提前完结的前端调用契约。
- 不要求前端计算订单人数、逐日服务日期或车辆载客量,这些均由后端权威校验并返回差异。
- 本文件只描述管理后台直接消费的 HTTP 契约,不包含服务间调用或发布实现细节。
## 九、验收状态与待补证据
已完成:
- 当前 worktree 中 create、change、confirm Controller 的 `605041` 强类型 `data` 静态核对。
- `AssignmentWriteRespVO`、`ChangeAssignmentRespVO`、`ConfirmRespVO` 与 `dailyDifferences` 字段静态核对。
- 三条失败路径不提交派单及其关联业务状态变化的源码顺序核对。
- 后端 PR #5065 已合并至 `dev-v3`;Fleet 双实例 `8087/8187` 已完成滚动部署并通过健康/Nacos 验证。
- 最终源码指纹下 Fleet `verify` 1909/1909、Order-v3 受影响回归 438/438、User Quartz 桥接 5/5 均通过。
前端联调仍需补充:
- 三个接口在测试环境 OpenAPI 中的请求/响应模型截图或导出差异。
- 经网关分别取得 create、change、confirm 的成功响应和 `605041` 真实响应,记录 HTTP 状态、业务码、`traceId` 与完整 `data`。
- 对 `605041` 前后做测试业务数据对照,确认派单版本/状态、车辆司机占用、保险、对账和订单派定结果未发生变化。
- 管理后台页面联调证据:差异列表展示、空字段处理、刷新行为、禁止乐观更新,以及修正后重新提交成功。
@@ -0,0 +1,83 @@
# 【修改接口·管理后台】订单工作台统计口径与金额字符串收口(#5062)
> Issue: [wx/HL#5062](https://git.1814.love:8443/wx/HL/issues/5062)
>
> 服务: `hl-user-service`、`hl-order-service-v3`
>
> 日期: 2026-07-18
>
> 影响入口: `GET /admin/profile/dashboard?period={today|week|month}`
## 一、前端结论
1. 路径、请求参数和角色分流不变,不需要新增接口调用。
2. GMV/收入改按真实收款时间归属:线上只统计成功支付,线下只统计未撤销收款;不再按订单创建时间归属订单累计实付。
3. 退款改按真实成功退款时间归属;财务近 30 天趋势返回真实每日收入和退款。
4. 所有金额字段固定按 JSON String 处理;比例 `gmvDiffRate` 仍为 JSON Number。
5. 雪花 ID(例如排行 `adminId`、即将出行 `orderId`)固定按 JSON String 处理,禁止转换为 JavaScript `Number`。
6. 排行订单数为期间发生有效收款的订单去重数,同一订单多笔收款只计一单、金额全部累加。
7. 权威统计源不可用时接口失败关闭,不会用部分成功数据或全零数据伪装成功。
## 二、受影响字段
### 2.1 ADMIN / CUSTOMIZER 工作台
| 字段 | JSON 类型 | 说明 |
|---|---|---|
| `overview.gmv` | String | 当前 period 内真实收款金额 |
| `overview.gmvDiffRate` | Number | 与上一等长期间相比的变化比例 |
| `trend[].gmv` | String | 对应日期的真实收款金额 |
| `ranking[].adminId` | String | 定制师雪花 ID |
| `ranking[].gmv` | String | 对应定制师期间真实收款金额 |
| `ranking[].orderCount` | Number | 发生有效收款的去重订单数 |
| `ranking[].avatar` | String/null | 定制师头像;用户信息降级时允许为空 |
| `upcomingTrips[].orderId` | String | 订单雪花 ID |
### 2.2 FINANCE 工作台
| 字段 | JSON 类型 | 说明 |
|---|---|---|
| `periodIncome` | String | 当前 period 内真实收入 |
| `periodRefund` | String | 当前 period 内成功退款 |
| `monthIncome` | String | 自然月真实收入 |
| `monthRefund` | String | 自然月成功退款 |
| `financeTrend[].date` | String | 日期,`yyyy-MM-dd` |
| `financeTrend[].income` | String | 当日真实收入 |
| `financeTrend[].refund` | String | 当日成功退款 |
## 三、响应片段
```json
{
"code": 200,
"success": true,
"data": {
"role": "FINANCE",
"periodIncome": "128000.00",
"periodRefund": "5600.00",
"monthIncome": "328000.00",
"monthRefund": "8600.00",
"financeTrend": [
{
"date": "2026-07-18",
"income": "12000.00",
"refund": "600.00"
}
]
}
}
```
## 四、前端检查清单
- [ ] 金额展示使用字符串格式化,不执行 `Number(amount)`。
- [ ] `gmvDiffRate` 继续按 Number 计算百分比。
- [ ] 所有 Long ID 保持字符串透传到路由和请求参数。
- [ ] 不再用订单创建日解释趋势 GMV;趋势日期是支付/收款发生日。
- [ ] 财务趋势同时渲染 `income` 与 `refund`,空日后端返回 `"0.00"`。
- [ ] 接口业务失败时展示重试,不把缺失统计源当作全零成功。
## 五、后端验证
- Dashboard、User 聚合、支付/退款 Feign、序列化与失败关闭相关测试已通过。
- Issue #5062 最终五模块全量测试:14,518 个测试,0 失败、0 错误;Fleet Reactor verify:1,824 个测试,0 失败、0 错误、0 跳过。
@@ -0,0 +1,59 @@
# 草原指南管理:视频素材仅查询当前文件夹
> **管理端 PR**: [wx/hl-ui#9](https://git.1814.love:8443/wx/hl-ui/pulls/9)
> **日期**: 2026-07-15
> **影响范围**: 管理后台草原指南新增/编辑页的视频素材选择弹窗
> **当前状态**: 已合入 `v2.1` 并部署测试环境
---
## 一、业务口径更正
视频素材选择器继续保留“草原指南”文件夹下拉,但素材列表改为严格按当前所选文件夹查询,不再自动包含子文件夹素材。
本记录更正 `15_fix_grassland_guide_video_folder_selector_visibility.md` 中关于递归查询的描述;文件夹树显示规则保持不变。
---
## 二、最终查询规则
- 选择“草原指南”根目录:只显示直接归属根目录的视频,不显示任意子文件夹视频。
- 选择具体子文件夹:只显示直接归属该文件夹的视频,不显示更深层子文件夹视频。
- 关键词搜索、分页和视频类型过滤均在当前文件夹范围内执行。
- 文件夹下拉仍锁定在“草原指南”分类树内,不能切换到其他业务分类。
对应请求规则:
```http
# 选择“草原指南”根目录
GET /admin/material/list?categoryCode=grassland_guide&fileType=video
# 选择具体子文件夹
GET /admin/material/list?categoryCode=grassland_guide&subCategoryId={文件夹ID}&fileType=video
```
上述请求均不提交 `includeDescendants=true`。
---
## 三、接口与配置影响
- 不新增或修改 API 字段。
- 后端 `includeDescendants` 可选能力保持不变,其他调用方仍可按需显式启用。
- 不修改数据库、字典、菜单或 Nacos 配置。
- 不影响封面图选择器、素材上传、小程序接口或草原指南保存逻辑。
---
## 四、验证记录
- [x] 根目录请求不包含 `subCategoryId` 和 `includeDescendants`。
- [x] 子文件夹请求包含对应 `subCategoryId`,不包含 `includeDescendants`。
- [x] 文件夹下拉、根节点和子节点继续显示。
- [x] 共享组件显式开启 `includeDescendants=true` 时仍正常传参。
- [x] Vitest:1 个测试文件、4 项测试通过。
- [x] 相关 ESLint 通过。
- [x] Vite 生产构建通过。
- [x] 独立代码复核无阻断项。
- [x] 测试环境部署任务 `418ff501` 成功。
- [x] 测试环境静态包 `VideoEditDrawer-qeoVDIga.js` 已确认保留文件夹下拉且不再传递 `include-descendants`。
@@ -0,0 +1,41 @@
# 草原指南管理:今日前端文件夹选择改动已全部回退
> **日期**:2026-07-15
> **影响范围**:`hl-ui` 草原指南视频素材选择器
> **当前状态**:今天由 `wx` 合入的相关前端代码已全部撤销
---
## 一、回退结论
今天合入 `hl-ui` 的草原指南素材文件夹选择改动不再作为当前实现,相关 PR 已通过纯 Git revert 全部撤销:
- `v2.1`:原 PR #7、#8、#9 已由回退 PR [wx/hl-ui#12](https://git.1814.love:8443/wx/hl-ui/pulls/12) 撤销。
- `master`:原 PR #10 已由回退 PR [wx/hl-ui#11](https://git.1814.love:8443/wx/hl-ui/pulls/11) 撤销。
回退后未新增任何替代前端实现。
## 二、当前代码与部署基线
| 分支 / 环境 | 当前版本 | 回退校验 |
|---|---|---|
| `v2.1` / 测试环境 | `30b3ee1f`,任务 `ca6a7df0` | 代码树与改动前 `797beda4` 完全一致 |
| `master` / 正式环境 | `f97a7ccb`,deploy `271` | 代码树与改动前 `4d305ca4` 完全一致 |
测试与正式环境的 `VideoEditDrawer` 静态资源均已复核,不再包含本次新增的 `allow-sub-category-select` 或 `include-descendants` 配置。
## 三、对早期记录的更正
以下两份记录仅保留为历史过程,不代表当前 `hl-ui` 代码状态:
- `15_fix_grassland_guide_video_folder_selector_visibility.md`
- `15_fix_grassland_guide_video_current_folder_scope.md`
前端不得再按上述两份记录继续实现或判断当前页面能力;如后续重新启动该需求,需要重新确认业务范围并另开前端任务。
## 四、后端接口边界
- `GET /admin/material/list` 的 `includeDescendants` 后端可选能力仍保留。
- 默认不传或传 `false` 时只查当前分类节点;显式传 `true` 时查询所选节点及其后代。
- 当前 `hl-ui` 回退后不消费本次新增的文件夹选择能力。
- 本次前端回退不修改后端接口、响应结构、数据库、字典、菜单或 Nacos 配置。
@@ -0,0 +1,20 @@
# 草原指南管理端免登录详情接口
- 工单:`wx/HL#5005`
- 测试环境:已部署 `dev-v3`
- 正式环境:已部署 `main@59e2b20c`
- 鉴权:不需要登录
- 数据范围:只允许查询 `PUBLISHED`
## 接口
`GET /admin/grassland-guide/public/videos/{videoId}`
返回列表字段,并增加:`videoMaterialId`、`videoUrl`、
`customCoverMaterialId`、`contentHtml`、`contentImageMaterialIds`、
`createdBy`、`updatedBy`、`createdAt`、`updatedAt`。
说明:管理端免登录预览不受 `loginRequired` 限制;该字段只表达小程序用户是否需要登录。
草稿、下架、删除或不存在的视频统一按不存在处理。
测试网关实测:HTTP 200,业务码 `200`。
@@ -0,0 +1,27 @@
# 草原指南管理端免登录列表接口
- 工单:`wx/HL#5005`
- 测试环境:已部署 `dev-v3`
- 正式环境:已部署 `main@59e2b20c`
- 鉴权:不需要登录
- 数据范围:仅返回 `PUBLISHED`,不会返回草稿、下架或已删除数据
## 接口
`GET /admin/grassland-guide/public/videos`
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| keyword | string | 否 | - | 匹配标题或简介 |
| featured | boolean | 否 | - | 是否精选 |
| page | integer | 否 | 1 | 最小 1 |
| pageSize | integer | 否 | 20 | 1~100 |
`data` 为分页对象:`records`、`total`、`page`、`pageSize`。列表项包含:
`videoId`、`title`、`summary`、`effectiveCoverUrl`、`coverSource`、
`durationSeconds`、`featured`、`loginRequired`、`sortWeight`、`status`、
`publishTime`、`linkedProductId`。
测试网关实测:HTTP 200,业务码 `200`。
@@ -0,0 +1,26 @@
# 草原指南管理端免登录播放接口
- 工单:`wx/HL#5005`
- 测试环境:已部署 `dev-v3`
- 正式环境:已部署 `main@59e2b20c`
- 鉴权:不需要登录
- 数据范围:只允许播放 `PUBLISHED`
## 接口
`GET /admin/grassland-guide/public/videos/{videoId}/play`
`data` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| videoId | string | 视频业务 ID |
| title | string | 标题 |
| videoUrl | string | OSS MP4 直出地址 |
| durationSeconds | integer | 素材自动解析的时长(秒) |
| effectiveCoverUrl | string | 自定义封面或 OSS 自动截帧封面 |
该接口是播放最小响应,不返回正文、创建人或操作信息。管理端免登录预览不受
`loginRequired` 限制;小程序端仍按该字段执行登录鉴权。
测试网关实测:HTTP 200,业务码 `200`,真实素材返回播放地址与时长。
@@ -0,0 +1,23 @@
# 草原指南管理端免登录推荐接口
- 工单:`wx/HL#5005`
- 测试环境:已部署 `dev-v3`
- 正式环境:已部署 `main@59e2b20c`
- 鉴权:不需要登录
- 数据范围:源视频和推荐结果都必须是 `PUBLISHED`,推荐结果自动排除自身
## 接口
`GET /admin/grassland-guide/public/videos/{videoId}/recommendations`
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| page | integer | 否 | 1 | 最小 1 |
| pageSize | integer | 否 | 20 | 1~100 |
`data` 为分页对象:`records`、`total`、`page`、`pageSize`;`records` 字段与列表接口一致。
推荐继续使用后端推荐算法,相关性不足时随机兜底,不需要前端拼装。
测试网关实测:HTTP 200,业务码 `200`。
@@ -0,0 +1,17 @@
# 草原指南管理列表按排序权重正序
- 工单:`wx/HL#5011`
- 测试环境:已部署 `dev-v3`
- 接口:`GET /admin/grassland-guide/videos`
## 变更
列表排序改为:
1. `sortWeight ASC`
2. `publishTime DESC`
3. `videoId DESC`
排序权重数字越小越靠前,`0` 排在所有正数权重之前。接口字段和请求参数没有变化,前端不需要改传参。
测试环境已用 57 条真实数据验证,权重顺序为 `0, 0, 0, 1, 100, 200...`。
@@ -0,0 +1,98 @@
# 草原指南 MP4 物理交错修复与测试数据回填
> 日期:2026-07-16
>
> 后端 Issue:[HL #5016](https://git.1814.love:8443/wx/HL/issues/5016)
>
> 后端 PR:[HL #5019](https://git.1814.love:8443/wx/HL/pulls/5019)
>
> 测试分支:`dev-v3`
>
> 影响服务:`hl-user-service`
>
> 本次不修改前端仓库,不新增或修改接口字段
## 1. 问题结论
草原指南真实 4K MP4 在 `1x`、`2x` 播放时出现固定位置反复缓冲。浏览器 Network 中同一文件产生大量被取消并重新发起的 `206 Partial Content` 请求,实际传输量可以超过文件本身体积。
该问题包含两个独立因素:
1. 历史 MP4 虽然已经把 `moov` 移到 `mdat` 前面,但音频、视频 Sample 在 `mdat` 中没有按时间物理交错。播放到同一时间点时,浏览器需要在文件相距很远的位置来回读取音视频数据,造成 Range 请求抖动、取消和重复下载。
2. 原片本身约 `357204588` 字节、`71` 秒,平均码率约 `40.25 Mbps`。2026-07-16 当前诊断链路连续读取 OSS Range 仅约 `0.52–1.44 MB/s`(约 `4.14–11.50 Mbps`),低于 `1x` 所需的约 `5.03 MB/s`,因此修复文件结构后仍可能因实际网络吞吐不足发生正常缓冲。
前端缓存策略不能补足长期吞吐缺口。`preload="auto"` 只能改善起播等待,不能让 `10 Mbps` 链路持续播放 `40 Mbps` 原片。
## 2. 后端修复
草原指南视频上传确认阶段新增 MP4 无损重封装:
- 不重新编码,不改变 H.264/AAC Sample。
- 不降低分辨率、帧率或码率。
- 保持原视频、音频轨的 Sample 数量、Sample 总字节数和时长。
- 将 `ftyp`、`moov` 放在文件前部。
- 以约 2 秒为窗口重新排列音频、视频 Chunk,使相同时间点的数据物理相邻。
- 重封装前后执行媒体指纹校验;不一致时拒绝替换原对象。
- 新对象使用 `.progressive-...mp4` Key,成功后再以 CAS 更新文件记录;旧对象暂不删除,便于回滚。
- 单个 Pod 同时只执行一个重封装任务,并检查临时磁盘空间,避免大文件并发耗尽磁盘。
接口路径和请求体保持不变:
```http
POST /admin/material/upload/confirm
```
前端仍然只提交原有 `materialId`、`description` 和 `tagIds`,不需要增加重封装参数。
## 3. 历史测试数据回填
已对测试环境问题视频执行一次性回填:
| 项目 | 值 |
| --- | --- |
| 草原指南视频 ID | `2076910159484928002` |
| 素材 ID | `2076909130286612482` |
| 文件 ID | `2076909128663379970` |
| 文件大小 | `357204588` 字节 |
| 新时长 | `71` 秒 |
| 新文件标识 | OSS Key 包含 `.progressive-2076909128663379970-` |
回填结果:
- `file_info` 已切换为新 `.progressive-...mp4`,状态为 `ACTIVE / OSS_MP4_READY_V1`。
- `material.oss_url`、自动封面和时长已切换。
- `grassland_guide_video` 自动封面和时长已同步,业务记录仍为 `PUBLISHED`。
- 资源服务 `8082/8182` 与网关 `8080` 的匿名详情、播放接口均返回新地址。
- OSS `HEAD` 返回 `200 video/mp4`、`Content-Length: 357204588`、`Accept-Ranges: bytes`。
- `Range: bytes=0-1048575` 返回 `206` 和正确的 `Content-Range`。
## 4. 部署与验证证据
- 修复提交:`4529da5e97fea21022ca700390a146944608eadc`
- `dev-v3` 合并提交:`65c63a68d903370d07dd80ffea62ac58ccfb31c8`
- `hl-user-service` 测试环境滚动部署任务:`f4d670ba`
- 两实例 `8081/8181` 均启动健康。
- `hl-user-service` 测试:`3226` 个测试,`0` 失败,`0` 错误,`6` 跳过。
- 真实 357 MB 文件无损重封装测试通过;重封装前后轨道 Sample 数、Sample 字节数和时长一致。
- 原文件约 44 秒处的音频、视频数据物理距离约 `185.7 MB`;重封装后缩短到约 `69.7 KB`。
## 5. 仍需处理的基础设施边界
本次后端修复解决“文件内部排列导致重复 Range 请求”的问题,但不能提高用户到 OSS 的实时带宽。
在“不降低码率、不生成低清档”的前提下:
- `1x` 需要链路持续高于约 `40.25 Mbps`,还应预留网络波动余量。
- `2x` 平均需要约 `80.50 Mbps`,短时峰值可能更高。
- 当前测试桶传输加速域名请求返回 `400`,未形成可用的加速播放链路。
- 若目标用户链路长期低于原片码率,只能选择 OSS 前置 CDN/边缘缓存、开通并验证 OSS 传输加速,或让用户在播放前基本下载完整文件;单纯调整浏览器缓冲逻辑无法解决。
接入 CDN 或 OSS 传输加速会改变基础设施和费用,须单独确认后实施。不得为规避该问题把流量代理到 Java 服务,也不得把几百 MB 文件整体读入服务内存。
## 6. 大文件上传确认超时提醒
本次真实文件在测试环境完成下载、重封装、上传共耗时约 `410` 秒,超过当前网关约 `60` 秒的请求超时。
- 服务端任务能够继续完成,但前端可能先收到 `504`。
- 在异步媒体处理改造完成前,前端不得因单次 `504` 立即重复提交或重复上传,应重新查询素材状态。
- 正式发布前应单独改造为异步处理状态机,或提供明确的后台任务查询接口;不建议简单把网关超时提高到数分钟。
@@ -0,0 +1,19 @@
# 草原指南小程序列表按排序权重正序
- 工单:`wx/HL#5011`
- 测试环境:已部署 `dev-v3`
- 影响接口:
- `GET /mp/grassland-guide/home`
- `GET /mp/grassland-guide/videos`
## 变更
普通视频分页列表排序改为:
1. `sortWeight ASC`
2. `publishTime DESC`
3. `videoId DESC`
排序权重数字越小越靠前,`0` 排在所有正数权重之前。首页精选区原本就是小权重优先,规则保持不变;相关推荐仍使用推荐算法,不改为纯权重排序。
接口字段和请求参数没有变化。
@@ -0,0 +1,108 @@
# 草原指南切换 2x 后永久缓冲更正
> 日期:2026-07-16
>
> 前端仓库:`mmg/hl-ui`
>
> 影响组件:`src/views/h5/grassland-guide/components/GuideVideoPlayer.vue`
>
> 影响辅助逻辑:`src/views/h5/grassland-guide/components/playbackBuffer.js`
>
> 本次后端接口、字段、OSS 地址和视频文件均无变化
## 1. 现象
视频在 `1x` 已经开始播放后切换到 `2x`,页面停在“正在缓冲”,即使等待较长时间也不能恢复。
Network 中可以看到同一个 MP4 存在多条大小不同的 `206 Partial Content` 请求。这是 Chrome 原生媒体加载器根据 MP4 元数据、当前播放位置和缓存状态发起的 HTTP Range 请求,Range 大小不固定是正常行为,不能据此判断 OSS 分片异常。
## 2. 已确认根因
线上播放器当前调用链为:
```text
切换 2x
→ 设置 video.playbackRate = 2
→ evaluateBuffer({ initial: true })
→ 要求 bufferAhead >= 15 秒
→ 未达到阈值
→ pauseForBuffering()
→ video.pause()
```
播放器暂停后,浏览器可以降低甚至停止后续媒体预取。当前代码又只依赖 `progress`、`canplay` 等媒体事件重新执行 `evaluateBuffer()`,没有保证这些事件一定继续产生,因此可能永远达不到 `15` 秒阈值,形成状态机死锁。
同类问题还存在于:
- 首次播放前等待固定 `8` 秒。
- `timeupdate` 检测到低水位后主动 `pause()`。
- `waiting`/`stalled` 事件再次主动 `pause()`。
这些逻辑把浏览器原生的“缺数据时等待并继续下载”变成了应用层“暂停后等待浏览器继续下载”,两者行为并不等价。
## 3. 必须修改的前端逻辑
### 3.1 播放与切换倍速
```js
async function play() {
playbackRequested.value = true
await videoRef.value?.play()
}
function togglePlaybackRate() {
const video = videoRef.value
if (!video) return
playbackRate.value = playbackRate.value === 1 ? 2 : 1
video.playbackRate = playbackRate.value
// 禁止在这里 pause()
// 禁止在这里重新执行固定秒数的初始缓冲门槛
}
```
### 3.2 缓冲状态由原生事件驱动
```js
function handleWaiting() {
if (!playbackRequested.value) return
buffering.value = true
recordPlaybackEvent('waiting')
}
function handleStalled() {
const video = videoRef.value
if (playbackRequested.value && video?.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
buffering.value = true
}
recordPlaybackEvent('stalled')
}
function handlePlaying() {
buffering.value = false
recordPlaybackEvent('playing')
}
```
事件处理器内不得调用 `video.pause()`。只要用户没有主动暂停,就保留原生播放意图,让浏览器在缺数据时自动等待、继续 Range 取流,并在数据恢复后自行继续播放。
### 3.3 删除主动低水位暂停
- 删除 `pauseForBuffering()` 对自动缓冲流程的使用。
- `handleTimeUpdate()` 只同步时间、记录 `bufferAhead` 和掉帧指标,不得检测低水位后暂停。
- `evaluateBuffer()` 不再阻塞首次播放或倍速切换;如保留该方法,只能用于诊断,不能控制 `play/pause`。
- 删除 `BUFFER_POLICIES` 中作为播放硬门槛的 `initial/low/resume`,避免后续重新引入死锁。
## 4. 验收要求
- [ ] `1x` 点击播放后能直接进入原生播放流程,不等待固定 `8` 秒。
- [ ] 播放中切换 `2x` 不触发 `pause` 事件。
- [ ] 切换 `2x` 后,即使触发 `waiting`,后续 Range 请求仍继续。
- [ ] 数据恢复后触发 `playing`,缓冲遮罩自动消失,播放继续。
- [ ] `1x ↔ 2x` 连续切换 10 次,不出现永久缓冲。
- [ ] 拖动进度后仍可重新播放,用户主动暂停不会被自动恢复。
- [ ] Console 中不再出现 `ratechange → initial-buffering → pause` 的调用序列。
- [ ] 使用 DevTools 测试真实用户表现时关闭 `Disable cache`;需要模拟弱网时单独选择网络限速,不把禁用缓存结果当作正常生产表现。
修复状态机后,若 `1x` 或 `2x` 仍出现能够自行恢复的短时 `waiting`,再根据 `bufferAhead`、实际下载速度和掉帧数判断是用户网络还是设备解码能力问题。播放器逻辑修复不能提高用户带宽;需要跨地区稳定承载原始 4K 高码率视频时,应另行评估 OSS 前置 CDN Range 缓存。
@@ -0,0 +1,15 @@
# 草原指南管理端免登录列表按排序权重正序
- 工单:`wx/HL#5011`
- 测试环境:已部署 `dev-v3`
- 接口:`GET /admin/grassland-guide/public/videos`
## 变更
列表排序改为:
1. `sortWeight ASC`
2. `publishTime DESC`
3. `videoId DESC`
排序权重数字越小越靠前,`0` 排在所有正数权重之前。接口字段、分页方式和免登录规则没有变化。
@@ -0,0 +1,128 @@
# 草原指南 4K 原片播放缓冲优化通知
> 日期:2026-07-16
>
> 前端仓库:`mmg/hl-ui`
>
> 影响页面:草原指南 H5 详情/播放页
>
> 本次后端接口、字段和 OSS 地址均无变化
> [!IMPORTANT]
> 2026-07-16 线上验证发现:原通知中的“主动暂停并等待固定缓冲秒数”会与浏览器原生媒体加载策略形成死锁,已经撤销。前端必须按独立更正通知
> [`16_fix_grassland_guide_playback_buffer_deadlock.md`](./16_fix_grassland_guide_playback_buffer_deadlock.md)
> 处理,不得再以 `8/15` 秒阈值阻塞播放或切换倍速。
## 1. 问题与诊断结论
草原指南详情直接播放后端返回的 OSS 原始 MP4。正式环境真实 4K 样本的媒体参数为:
- 文件约 `451.74 MiB`,时长约 `95` 秒。
- 分辨率 `3812 × 2160`,`60 fps`。
- H.264 Main Profile,Level `5.2`。
- 平均码率约 `40.20 Mbps`。
- `1x` 播放时短时峰值约 `72.98 Mbps`。
- `2x` 播放时平均网络消耗约 `80.40 Mbps`;短时峰值约 `142.17 Mbps`。
正式 OSS 已支持 HTTP Range,请求返回 `206 Partial Content`。同一诊断环境连续读取 OSS Range 的平均速度约 `230.52 Mbps`,高于该视频 `2x` 播放的短时峰值。因此目前没有证据表明 Java 服务或 OSS Range 能力是主要瓶颈;但该结果不代表每个用户到 OSS 的实时链路都能达到相同速度。
现已确认 `1x` 也会偶发卡顿。结合 `1x` 接近 `73 Mbps` 的短时峰值,更可能是用户链路瞬时波动与浏览器前向缓冲较浅共同造成:即使平均网速高于视频平均码率,只要短时间下载速度低于瞬时消耗速度,缓冲仍可能耗尽。因此首次 `1x` 播放和卡顿恢复也必须执行缓冲水位保护,不能只处理 `2x`。
此外,`4K 60 fps` 在 `2x` 下相当于设备需要承担接近 `120 fps` 的解码节奏。部分设备即使网络充足,也可能因硬件解码能力不足出现掉帧;前端需要把“等待网络缓冲”和“设备解码掉帧”分别记录。
## 2. 前端处理要求
### 2.1 保持原生 Range 播放
- 继续直接使用接口返回的 `videoUrl` 作为 `<video src>`。
- 设置 `preload="auto"`,允许浏览器提前加载媒体数据。
- 不要使用 `fetch`/`axios` 把完整 MP4 下载为 Blob 后再播放,避免一次性占用数百 MiB 内存并破坏原生 Range 调度。
- 不要改写 OSS 域名,不转成 HLS/M3U8,不接直播播放器。
- 本轮不降低码率、不转码、不新增清晰度档位。
### 2.2 计算真实前向缓冲
不能只读取 `video.buffered.end(video.buffered.length - 1)`。应找到包含 `currentTime` 的 buffered 区间,再计算:
```js
function getBufferAhead(video) {
const currentTime = video.currentTime
for (let index = 0; index < video.buffered.length; index += 1) {
const start = video.buffered.start(index)
const end = video.buffered.end(index)
if (currentTime >= start && currentTime <= end) {
return Math.max(0, end - currentTime)
}
}
return 0
}
```
### 2.3 `1x` 与倍速播放缓冲策略
- 用户点击播放时直接调用原生 `video.play()`,不得先暂停等待固定缓冲秒数。
- 用户切换 `2x` 时只设置 `video.playbackRate = 2`,不得调用 `pause()`,也不得重新执行“初始缓冲门槛”。
- `waiting` 事件只负责显示“正在缓冲”和记录诊断;不得在事件处理器内再次调用 `pause()`。
- 保持原生播放请求后,浏览器会继续 Range 取流;数据恢复后通过 `playing` 事件关闭缓冲提示。
- `stalled` 用于记录网络加载停滞;仅当仍有播放意图且 `readyState < HTMLMediaElement.HAVE_FUTURE_DATA` 时显示缓冲提示,不主动暂停。
- `timeupdate` 只采集缓冲指标,不得因低于人为水位而主动暂停。
- 用户主动暂停、拖动进度或离开页面时清除缓冲提示,避免与真实播放意图混淆。
- `preload="auto"` 只是浏览器提示,不能强制浏览器预取指定秒数。
`video.buffered` 用于诊断和观测,不再作为阻塞 `play()` 或切换倍速的硬门槛。简单的原生 `<video>` 无法保证“暂停后一定继续预取到 N 秒”;若业务将来要求确定性分片缓冲,需要另行评估 MSE/HLS 或 CDN,不应在原生 MP4 播放器中模拟。
### 2.4 记录卡顿证据
至少监听并记录以下事件和状态:
- 事件:`loadstart`、`loadedmetadata`、`canplay`、`canplaythrough`、`progress`、`waiting`、`stalled`、`playing`、`seeking`、`seeked`、`error`。
- 状态:`currentTime`、`playbackRate`、前向缓冲秒数、`readyState`、`networkState`。
- 浏览器支持时记录 `getVideoPlaybackQuality()` 的 `droppedVideoFrames` 和 `totalVideoFrames`。
日志不得记录完整带签名 URL、Token 或其他凭证。建议只记录 `videoId`、事件时间和上述播放指标。
判断口径:
- 出现 `waiting`/`stalled`,同时前向缓冲接近 `0`:网络或缓冲调度不足。
- 前向缓冲充足、没有 `waiting`,但 `droppedVideoFrames` 持续上升:设备解码能力不足。
## 3. 接口契约
播放接口保持不变:
```http
GET /admin/grassland-guide/public/videos/{videoId}/play
```
前端继续使用:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `videoId` | string | 视频业务 ID |
| `videoUrl` | string | OSS 原始 MP4 地址,直接交给原生 `<video>` |
| `durationSeconds` | integer | 后端从素材解析的时长 |
| `effectiveCoverUrl` | string | 生效封面 |
雪花 ID 必须按字符串处理。本次不增加缓冲、码率或清晰度字段,前端不得等待后端返回这些字段后才处理播放。
相关接口说明见:
- [`16_feat_grassland_guide_public_play.md`](./16_feat_grassland_guide_public_play.md)
- [`12_feat_grassland_guide_admin_mp_oss.md`](./12_feat_grassland_guide_admin_mp_oss.md)
## 4. 前端验收清单
- [ ] `<video>` 使用接口原始 `videoUrl`,并设置 `preload="auto"`。
- [ ] 没有将完整 MP4 下载为 Blob。
- [ ] 首次 `1x` 播放不受固定缓冲秒数阻塞。
- [ ] 切换 `2x` 不调用 `pause()`,不等待 `15` 秒缓冲。
- [ ] `waiting`/`stalled` 只更新提示与日志,不主动暂停;`playing` 能可靠关闭提示。
- [ ] 用户暂停、拖动和离开页面时不会被自动恢复播放。
- [ ] 能区分并记录缓冲耗尽与设备解码掉帧。
- [ ] 使用正式 4K 样本分别连续验证 `1x` 和 `2x`,记录等待次数、累计等待时长、最小前向缓冲和掉帧数。
- [ ] Chrome 桌面端和目标移动设备均完成验证。
若完成上述缓冲策略后,多个地区和设备仍普遍出现“前向缓冲耗尽”,再单独评估 OSS 前置 CDN Range 缓存或 OSS 传输加速;该基础设施调整会新增费用,不属于本次前端通知范围。
@@ -0,0 +1,34 @@
# 草原指南 MP4 重封装时间轴修复正式发布
> 日期:2026-07-17
>
> 后端 Issue:[HL #5021](https://git.1814.love:8443/wx/HL/issues/5021)
>
> 后端 PR:[HL #5026](https://git.1814.love:8443/wx/HL/pulls/5026)
>
> 正式分支:`main`
>
> 影响服务:`hl-user-service`
## 问题与修复
旧版 MP4 无损重封装虽然保持了 Sample 数据,但 `mvhd/tkhd/elst` 使用了不一致的时间单位,浏览器可能把完整视频识别成数秒并提前触发 `ended`。
后端现已统一 presentation timeline 的 movie timescale,并在替换 OSS 对象前校验 `mvhd/tkhd/elst/mdhd/stts` 一致性。视频仍保持原分辨率、帧率、编码和码率,不经过 Java 服务代理播放流量。
接口路径、请求体及响应字段均保持不变,前端无需适配新字段。
## 验证结果
- MP4 核心测试:30/30 通过
- `FileServiceTest` 与 `OssServiceTest`:141/141 通过
- Maven package:`BUILD SUCCESS`
- 测试环境真实媒体 1x/2x 播放已验收不卡顿
- 正式部署任务:`#283`
- 发布提交/镜像:`009d5ffe`
- `hl-user-service`:`2/2 Ready`
- 正式列表、详情、推荐、播放接口:HTTP/业务码均为 200
- 正式真实 MP4:`Content-Length: 592945035`、`Accept-Ranges: bytes`
- `Range: bytes=0-1023`:返回 206,`Content-Range: bytes 0-1023/592945035`
本次未修改或部署 `hl-ui`,无数据库结构及 Nacos 配置变更。
@@ -0,0 +1,25 @@
# 草原指南排序权重正序规则正式发布
> 日期:2026-07-17
>
> 后端 PR:[HL #5026](https://git.1814.love:8443/wx/HL/pulls/5026)
>
> 正式分支:`main`
>
> 影响服务:`hl-resource-service`
## 变更说明
草原指南列表排序权重统一按正序处理:数值越小越靠前,`0` 位于 `1` 之前。
本次不新增或删除接口,不修改请求参数与响应字段。前端继续使用原有列表接口,不需要自行反转结果。
## 正式验证
- 正式部署任务:`#282`
- 发布提交/镜像:`009d5ffe`
- `hl-resource-service`:`2/2 Ready`
- 匿名正式列表接口:HTTP 200、业务码 200
- 正式数据首屏排序权重:`0,1,2,3,4,5,6,7,8,9`
本次未修改或部署 `hl-ui`,无数据库结构及 Nacos 配置变更。