changelog: #8220 团期正式行程用车需求自动汇总草稿端点(新增接口,管理后台)
changelog-filename-gate / validate (push) Failing after 2s
changelog-filename-gate / validate (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -0,0 +1,407 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8220"
|
||||
title: "团期正式行程用车需求自动汇总草稿"
|
||||
consumer: "admin"
|
||||
author: "jw(GIT)"
|
||||
change_type: "新增接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "verified"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: "mmg"
|
||||
frontend_ref: ""
|
||||
target_release: "v2.1"
|
||||
verified_at: "2026-09-23"
|
||||
status_note: "后端已部署 TEST 并经网关实测;待 mmg 在编辑弹窗接入「自动汇总」+ 乘车户全选 + 人数随所选户汇总"
|
||||
updated_at: "2026-09-23"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期正式行程用车需求自动汇总草稿(#8220)
|
||||
|
||||
> **服务**: hl-order-service-v3(经网关调用,无需关心服务端口)
|
||||
> **PR**: #8273
|
||||
> **Issue**: #8220
|
||||
> **日期**: 2026-09-23
|
||||
> **影响范围**: 管理后台团期详情「用车」Tab 的正式行程用车需求编辑弹窗
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
新增只读端点,按各子订单已提交的行程用车(TRAVEL)需求,自动汇总出团级正式用车需求草稿。草稿形状与保存端点 `PUT .../vehicle-requirement` 的请求体**完全一致**,前端可以直接灌进编辑弹窗、原样保存。
|
||||
|
||||
**四条会直接影响你怎么写代码的点,按严重度排**:
|
||||
|
||||
1. 🔴 **草稿之外的四个诊断字段必须展示,不能只看 `draft`**。`droppedFleetItems` 里是**没进草稿的车型需求**(一户报了多个车型时只保留主车型),不提示的话,管理员一保存,这些需求就从团级正式需求里消失了。另外三个字段(`staleHeadcountOrders` / `paddedOrderDays` / `violations`)见业务边界。
|
||||
2. 🔴 **车型回显是归一后的大类 key(`suv` / `bus` / `mpv` / `sedan`)**,与 #8221 交接件里提过的是同一个问题:车型下拉的 value 是 fleet typeKey `suv2`,拿 `suv` 逐字去比会判成「非字典值,请重选」,并被前端校验拦住保存。**判「是否字典值」要按归一后的大类比**,否则所有 SUV 组汇总出来都会被弹窗拦下。
|
||||
3. **有户还没提交行程用车需求时,本端点直接返回业务错误 `809121`**,报文逐户列出「订单号(orderId):原因」。这时不要尝试拼一份草稿,应提示管理员去催这些户。
|
||||
4. **成功码是 `code: 200`**;业务失败也返回 HTTP 200,**一律按 `body.code` 判**。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景(选填)
|
||||
|
||||
wx 2026-09-23 团期详情页「用车」Tab 反馈:「正式的行程用车需求要根据子订单的行程用车需求自动汇总 自动填写」,以及「得有个全选的按钮,根据选的乘车户自动把人数算出来」。此前编辑弹窗不拉取任何子订单需求数据,日期预填的是团期自己的出发/结束日,其余字段全靠手填。汇总规则由管理者于 2026-09-23 定案(#8220 评论)。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 新增接口 | 只读,返回可原样保存的草稿与汇总诊断 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft`
|
||||
|
||||
**VO**: `GroupVehicleAggregateDraftRespVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理员在编辑弹窗里点「自动汇总」时调用。拿到 `data.draft` 后填进弹窗,同时展示四个诊断字段;管理员确认后,把 `draft` 原样(或编辑后)交给 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` 保存。
|
||||
|
||||
弹窗里如果已经有编辑内容,点「自动汇总」前请二次确认「这将覆盖当前编辑内容」。本端点不管现在有没有正式需求,都返回完整汇总,要不要覆盖由前端交互决定。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| `groupBatchId` | Path | Long | ✅ | — | 团期 ID(`group_batch_id`),🔴 不是产品班期 ID |
|
||||
|
||||
#### 出参 `Result<GroupVehicleAggregateDraftRespVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `groupBatchId` | String(雪花 ID) | 团期 ID |
|
||||
| `currentStatus` | String / null | 当前正式需求状态;还没形成时为 null。**只有 null 或 `DRAFT` 时才能保存**(其余状态保存会报 809101 / 809115),前端据此决定「保存」按钮是否可用 |
|
||||
| `draft` | Object | 汇总草稿,与保存请求体同形,**恒非 null** |
|
||||
| `draft.version` | Integer / null | 当前正式需求的乐观锁版本;未形成时为 null。保存时原样带上 |
|
||||
| `draft.remark` | String / null | 整份备注,汇总时恒为 null |
|
||||
| `draft.groups[]` | Array | 乘车分组,没有可汇总内容时为空数组 |
|
||||
| `draft.groups[].groupId` | Long / null | 恒为 null(汇总产物一律是新组) |
|
||||
| `draft.groups[].groupCode` | String | 车型大写,同车型按连续日期段拆组时第二段起加序号:`BUS`、`BUS2`… |
|
||||
| `draft.groups[].vehicleType` | String | 归一后的车型大类 key:`bus` / `suv` / `mpv` / `sedan` |
|
||||
| `draft.groups[].serviceStartDate` | String(`yyyy-MM-dd`) | 本组首日 |
|
||||
| `draft.groups[].serviceEndDate` | String(`yyyy-MM-dd`) | 本组末日 |
|
||||
| `draft.groups[].seats` | Integer / null | 组内各户保留车型项里最大的单车座位数;各户都没填座位时,与 `count` 一起为 null |
|
||||
| `draft.groups[].count` | Integer / null | `ceil(本组最忙那天的人数 / (seats − 1))`,已扣除司机座 |
|
||||
| `draft.groups[].specialTags` | String[] | 组内各户特殊诉求编码的并集(去重、保持顺序) |
|
||||
| `draft.groups[].remark` | String / null | 逐户「订单号: 定制师备注」和「订单号 另报 suv 7座×1」拼接而成,超过 500 字截断。**仅供人眼留底** |
|
||||
| `draft.groups[].days[]` | Array | 逐日明细,正好铺满本组首日到末日 |
|
||||
| `draft.groups[].days[].tripDate` | String(`yyyy-MM-dd`) | 日期 |
|
||||
| `draft.groups[].days[].headcount` | Integer | 当天在组各户的**实时**人数之和 |
|
||||
| `draft.groups[].days[].memberOrderIds` | String[](雪花 ID) | 当天在组的子订单 ID。🔴 19 位雪花 ID,前端一律按字符串处理 |
|
||||
| `droppedFleetItems[]` | Array | 没进草稿的车型项,见业务边界第 1 条 |
|
||||
| `droppedFleetItems[].orderId` / `orderNo` | String / String | 所属子订单 |
|
||||
| `droppedFleetItems[].vehicleType` / `seats` / `count` | String / Integer / Integer | 被丢弃项(子订单原值) |
|
||||
| `droppedFleetItems[].keptVehicleType` | String | 该户被归入的主车型 |
|
||||
| `droppedFleetItems[].reason` | String | `NOT_PRIMARY_TYPE`(非主车型)/ `VEHICLE_TYPE_NOT_IN_DICT`(车型不在车型字典内) |
|
||||
| `staleHeadcountOrders[]` | Array | 实时人数与子订单需求提交时冻结的人数不一致的户:`orderId` / `orderNo` / `frozenHeadcount` / `liveHeadcount` |
|
||||
| `paddedOrderDays[]` | Array | 为了覆盖该户「出发~返回」每一天而补进分组、但不在该户行程用车服务日里的日期:`orderId` / `orderNo` / `dates[]` |
|
||||
| `violations[]` | Array | 草稿按保存时同一套校验预检出的问题:`code` / `reason` / `detail` / `groupCode` / `tripDate` / `orderId`。正常应为空数组 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```http
|
||||
GET /v3/admin/order/group-batch/2102692937584513025/vehicle-requirement/aggregate-draft
|
||||
Authorization: Bearer <admin token>
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2102692937584513025",
|
||||
"currentStatus": null,
|
||||
"draft": {
|
||||
"version": null,
|
||||
"remark": null,
|
||||
"groups": [
|
||||
{
|
||||
"groupId": null,
|
||||
"groupCode": "BUS",
|
||||
"vehicleType": "bus",
|
||||
"serviceStartDate": "2026-11-24",
|
||||
"serviceEndDate": "2026-11-26",
|
||||
"seats": 16,
|
||||
"count": 1,
|
||||
"specialTags": [],
|
||||
"remark": "HL20260923173356872: 8220 甲户多车型;HL20260923173356872 另报 suv 5座×1;HL20260923173401731: 8220 乙户",
|
||||
"days": [
|
||||
{
|
||||
"tripDate": "2026-11-24",
|
||||
"headcount": 5,
|
||||
"memberOrderIds": [
|
||||
"2102692937378992129",
|
||||
"2102692957587165185"
|
||||
]
|
||||
},
|
||||
{
|
||||
"tripDate": "2026-11-25",
|
||||
"headcount": 5,
|
||||
"memberOrderIds": [
|
||||
"2102692937378992129",
|
||||
"2102692957587165185"
|
||||
]
|
||||
},
|
||||
{
|
||||
"tripDate": "2026-11-26",
|
||||
"headcount": 5,
|
||||
"memberOrderIds": [
|
||||
"2102692937378992129",
|
||||
"2102692957587165185"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupId": null,
|
||||
"groupCode": "SUV",
|
||||
"vehicleType": "suv",
|
||||
"serviceStartDate": "2026-11-24",
|
||||
"serviceEndDate": "2026-11-26",
|
||||
"seats": 5,
|
||||
"count": 1,
|
||||
"specialTags": [],
|
||||
"remark": "HL20260923173405165: 8220 丙户",
|
||||
"days": [
|
||||
{
|
||||
"tripDate": "2026-11-24",
|
||||
"headcount": 2,
|
||||
"memberOrderIds": [
|
||||
"2102692971709370370"
|
||||
]
|
||||
},
|
||||
{
|
||||
"tripDate": "2026-11-25",
|
||||
"headcount": 2,
|
||||
"memberOrderIds": [
|
||||
"2102692971709370370"
|
||||
]
|
||||
},
|
||||
{
|
||||
"tripDate": "2026-11-26",
|
||||
"headcount": 2,
|
||||
"memberOrderIds": [
|
||||
"2102692971709370370"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"droppedFleetItems": [
|
||||
{
|
||||
"orderId": "2102692937378992129",
|
||||
"orderNo": "HL20260923173356872",
|
||||
"vehicleType": "suv",
|
||||
"seats": 5,
|
||||
"count": 1,
|
||||
"keptVehicleType": "bus",
|
||||
"reason": "NOT_PRIMARY_TYPE"
|
||||
}
|
||||
],
|
||||
"staleHeadcountOrders": [],
|
||||
"paddedOrderDays": [],
|
||||
"violations": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**示例说明**:示例值取自测试环境一次真实调用,不构成可复现夹具。
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
团里没有需要用车的户,也没有任何 TRAVEL 需求时,返回空草稿(**`groups` 是空数组,不是 null**):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"groupBatchId": "2102692937584513025",
|
||||
"currentStatus": null,
|
||||
"draft": { "version": null, "remark": null, "groups": [] },
|
||||
"droppedFleetItems": [],
|
||||
"staleHeadcountOrders": [],
|
||||
"paddedOrderDays": [],
|
||||
"violations": []
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
车型字典(车队服务)不可用时**不降级**,返回 809120,见错误响应。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**有户缺少可汇总的行程用车需求**(未提交 / 被打回未重提 / 车型都不在字典内 / 推不出日期 / 人数为 0):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809121,
|
||||
"message": "团期 2102692937584513025 有 1 户缺少可汇总的行程用车需求,暂不能自动汇总:HL20260923173405165(2102692971709370370):未提交行程用车需求",
|
||||
"data": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
**车型字典暂不可用**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 809120,
|
||||
"message": "车队车型字典暂不可用,无法校验车型,请稍后重试",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**无权限**(当前角色没有 `group-batch:demand:confirm`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 589507,
|
||||
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
|
||||
"success": false,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- **多车型户**:一户的行程用车同时报了多个车型(如 bus×1 + suv×1)时,这一户只进「主车型」组。主车型取座位数×辆数最大的那一项,相等时取车型编码字典序小的,所以同一份数据每次汇总结果都相同。其余车型进 `droppedFleetItems`,🔴 **前端必须醒目提示**「以下车型需求未包含在草稿中」。这样做是因为同一户同一天只能属于一个分组(809108)。
|
||||
- **拆组**:同一车型的日期如果断开(比如甲 10-08~10-09、乙 10-12~10-13),会拆成 `BUS` / `BUS2` 两组,不会产出某天零人的逐日行。
|
||||
- **补日**:团级保存要求每个需车户「出发~返回」的每一天都被分组覆盖(809109)。子订单行程用车的服务日如果比这个范围窄,多出来的日期也会把该户算进车,并列在 `paddedOrderDays` 里。前端提示「以下户在这些日期原本未报用车,已按行程补入」。
|
||||
- **人数**:逐日人数用订单**实时**人数,与户列表显示的人数一致。实时人数与子订单需求提交时冻结的人数不一致时,该户进 `staleHeadcountOrders`,说明那户的子订单用车需求已经过期,车务在子订单侧看到的还是旧值。
|
||||
- **座位与车数**:`count` 按扣掉司机座的口径算,所以草稿在团级(809116)和子订单级两种座位校验下都成立。
|
||||
- **`violations` 为空也不保证保存一定成功**:`currentStatus` 不是 null 或 `DRAFT`、或者汇总之后有人改过正式需求(version 变了),保存照样会被拒。
|
||||
- **只汇总行程用车(TRAVEL)**,接送机(TRANSFER)不进团车。
|
||||
- **本端点零写入**:不取锁、不进事务,调多少次都不影响数据。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
### ✅ 正确 / ❌ 错误请求对照
|
||||
|
||||
| 场景 | 请求 | 预期(HTTP 恒 200) |
|
||||
|------|------|------|
|
||||
| ✅ 各户都已提交 | `GET .../{groupBatchId}/vehicle-requirement/aggregate-draft` | `code: 200`,`draft.groups` 非空 |
|
||||
| ✅ 原样保存 | `PUT .../{groupBatchId}/vehicle-requirement`,请求体 = `data.draft` | `code: 200` |
|
||||
| ❌ 有户未提交 | 同上 GET | `code: 809121`,报文列出未提交的户 |
|
||||
| ❌ 路径传成产品班期 ID | `GET .../{productBatchId}/vehicle-requirement/aggregate-draft` | 团期不存在的错误码(589500) |
|
||||
|
||||
### 前端「乘车户全选 + 人数自动汇总」的取数口径(wx 反馈第 ③ 条)
|
||||
|
||||
- **户清单与每户人数**:用既有的 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households?kind=TRAVEL`,人数取 `households[].participantCount`。它与本端点草稿里的逐日人数同源,都是订单实时人数。
|
||||
- 🔴 **不要用分页的团期订单列表做「全选」**:那个列表单页上限 200,户数超过 200 时会漏选。`vehicle-households` 不分页,单次最多 500 户。
|
||||
- **某天的人数** = 当天勾选各户的 `participantCount` 之和。汇总草稿里的 `days[].headcount` 就是按这个口径算的,可以直接对照。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
本接口**只读**,没有任何数据库写入,不新增表、不改表结构,也没有 Flyway 脚本。测试环境实测:调用前后,团级正式需求相关表与子订单用车需求表的行数和 `update_time` 都没有变化(见第八节)。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- **鉴权**:需要 `group-batch:demand:confirm`,与 `GET/PUT .../vehicle-requirement` 同一个权限码。不带 token 时网关返回 401;角色没有该权限时返回 `code: 589507`。
|
||||
- **网关路由**:沿用现有的 `- Path=/v3/admin/**` → `lb://hl-order-service-v3`,本单**不新增路由**。
|
||||
- **团期不存在** → 589500。
|
||||
- **有户缺少可汇总需求** → 809121;**车型字典不可用** → 809120。
|
||||
|
||||
---
|
||||
|
||||
## 六.5 枚举 / 数据字典
|
||||
|
||||
### droppedFleetItems[].reason
|
||||
|
||||
**所属字段**: `droppedFleetItems[].reason` | **类型**: `String`
|
||||
|
||||
| 值 | 中文 | 说明 |
|
||||
|---|---|---|
|
||||
| `NOT_PRIMARY_TYPE` | 非主车型 | 该户多车型,只保留主车型 |
|
||||
| `VEHICLE_TYPE_NOT_IN_DICT` | 车型不在字典 | 车型归一后不在车队车型字典内 |
|
||||
|
||||
---
|
||||
|
||||
## 六.6 修改前后对比
|
||||
|
||||
无,本接口为新增。
|
||||
|
||||
---
|
||||
|
||||
## 六.7 影响评估
|
||||
|
||||
- **破坏兼容**:否,新增接口
|
||||
- **前端同步上线要求**:否,不接入也不影响现有编辑与保存流程
|
||||
- **新增错误码**:`809121`(有户缺少可汇总的行程用车需求)
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:团期正式行程用车需求编辑弹窗的「自动汇总」入口
|
||||
- **零影响**:
|
||||
- 正式用车需求的保存 / 读取 / 撤回 / 免车 / 确认(校验语义一处未改)
|
||||
- 子订单用车需求的提交与审核
|
||||
- 接送机(TRANSFER)链路
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
真实网关调用(`https://api.test.1814.love`),hl-order-service-v3 已部署 `dev-v3 @ 9b60bc62b`(本单合并提交,两个实例都在 17:21 滚动重启)。夹具全部自建:产品班期 `2102692899017912323`,团期 `2102692937584513025`,三户订单甲 / 乙 / 丙。
|
||||
|
||||
```
|
||||
GET .../2102692937584513025/vehicle-requirement/aggregate-draft (三户都没提交 TRAVEL)
|
||||
→ 809121,报文列出 3 户 ✓;甲户提交后列 2 户 ✓;乙户提交后列 1 户 ✓
|
||||
GET .../aggregate-draft (三户都已提交:甲 bus 16×1 + suv2 5×1、乙 bus 12×1、丙 suv2 5×1)
|
||||
→ 200:BUS 组(甲 + 乙,seats=16,count=1,每天 5 人)+ SUV 组(丙);
|
||||
droppedFleetItems = 甲户 suv 5×1(NOT_PRIMARY_TYPE);violations = [] ✓
|
||||
PUT .../2102692937584513025/vehicle-requirement 请求体 = data.draft 原样
|
||||
→ 200,version=1;GET 回读后逐字段比对与草稿一致 ✓(再汇总一次、带 version=1 原样保存 → 200,version=2 ✓)
|
||||
零写入:调汇总前后各读一次 order_group_vehicle_requirement / _group / _group_day / order_vehicle_requirement
|
||||
的全表行数与 MAX(update_time),以及本团相关行 → 两轮前后完全一致 ✓
|
||||
丙户另外提交了 TRANSFER(mpv,11-23 接机)→ 草稿里没有 mpv,也没有 11-23 ✓
|
||||
不带 token → code 401「缺少有效的 Authorization 头」 ✓
|
||||
定制师角色(无 group-batch:demand:confirm)→ code 589507 ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
- #8221(车型字典与存量车型归一):本端点输出的车型同样要通过 809119
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8220](https://git.1814.love/wx/HL/issues/8220)
|
||||
- 关联 PR: [wx/HL#8273](https://git.1814.love/wx/HL/pulls/8273)
|
||||
|
||||
---
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8220](https://git.1814.love/wx/HL/issues/8220)
|
||||
- **PR**: [#8273](https://git.1814.love/wx/HL/pulls/8273)
|
||||
- **Merge commit**: [9b60bc62b](https://git.1814.love/wx/HL/commit/9b60bc62b)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @jw
|
||||
在新工单中引用
屏蔽一个用户