docs(changelog): 团期查看需求页五处缺口的前端交接件 (#8195)
changelog-filename-gate / validate (push) Failing after 1s
changelog-filename-gate / validate (push) Failing after 1s
四个接口:新增「批量确认接送机需求」端点;vehicle-households 补 endDate/teamNo/ 户级 status/statusName 且未提交户进列表;hotel-households 补 endDate/teamNo; 保存正式用车需求新增车型字典校验(809119)。 三条会让前端静默出错的契约变化已写在正文开头: - orderNo 从来不是团号,团号改读新字段 teamNo(orderNo 保留不删,v-for :key 在用) - vehicleRowCount 不再恒 >= householdCount,旧不变量作废 - 批量确认部分失败仍返 code:200/success:true,要看 data.failedCount 覆盖边界照写未回避:vehicle-households 上「从未提交户」与「被打回户」完全同形 (均 requirements:[] + status:null),本接口给不出判据,出处 GroupVehicleHouseholdsRespVO.java:169-171。 同时逐行列出 hl-ui(origin/v2.1) src/api/orderV2GroupBatch.js 两处已过期的 JSDoc: getGroupVehicleHouseholds :600/:601-602/:607-620,getGroupHotelHouseholds :576-586。 后端已部署测试服并实测(order-v3 dev-v3/62449e550/2026-09-22 22:22:58/ok), 九条验收全部取到活体读数,原始报文落盘;正文无任何「等部署/另行通知」类前向引用。 Refs #8195 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -0,0 +1,455 @@
|
|||||||
|
---
|
||||||
|
schema: "hl-changelog/v2"
|
||||||
|
ticket: "8195"
|
||||||
|
title: "团期「查看需求」页五处缺口: 户级团号 teamNo / 未提交户进列表 / 接送机批量确认新端点 / 车型字典校验 / 团期 endDate"
|
||||||
|
consumer: "admin"
|
||||||
|
author: "wx(GIT)"
|
||||||
|
change_type: "修改接口"
|
||||||
|
backend_status: "deployed"
|
||||||
|
gateway_status: "not_required"
|
||||||
|
frontend_status: "pending"
|
||||||
|
frontend_owner: "mmg"
|
||||||
|
frontend_ref: ""
|
||||||
|
target_release: ""
|
||||||
|
verified_at: ""
|
||||||
|
status_note: "backend_status=deployed: hl-order-service-v3 已滚动到测试服,deploy-status.sh 读数 dev-v3 / 62449e550 / DEPLOYED_AT 2026-09-22 22:22:58 / STATE=ok,62449e550 即本单合并提交本身;九条验收项全部在测试服网关上取到活体读数,原始报文逐条落盘。gateway_status=not_required: 新端点路径落在 hl-gateway 既有 /v3/admin/** 通配上,零新增路由——判据不是推断而是实测:该路径返 400「请至少选择一个要确认的子订单」(业务校验),而故意写错的同前缀路径返 404「接口不存在」,两种报文形态不同 ⇒ 路由确实存在。frontend_status=pending: 本条新增 4 个响应字段、1 个端点,且改了 householdCount 的口径,hl-ui 需要改;hl-ui(origin/v2.1) 里 src/api/orderV2GroupBatch.js 两处 JSDoc 描述的是改前契约,改后已不准确,逐行列在第六.6 节。⚠️ 契约边界:本接口上「从未提交需求的户」与「提交后被打回的户」完全同形(两者都是 requirements:[] + status:null),不可区分,依据 GroupVehicleHouseholdsRespVO.java:169-171。"
|
||||||
|
updated_at: "2026-09-22"
|
||||||
|
base: "dev-v3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 团期「查看需求」页五处缺口: 户级团号 teamNo / 未提交户进列表 / 接送机批量确认新端点 / 车型字典校验 / 团期 endDate
|
||||||
|
|
||||||
|
> **存放目录**: 二期(order-v3)→ `changelogs-v2/2026-09/`
|
||||||
|
|
||||||
|
## ⚠️ 关键变化
|
||||||
|
|
||||||
|
三条,改前改后行为不同,按这个顺序看:
|
||||||
|
|
||||||
|
1. **`orderNo` 从来就不是团号。** 改前两个 households 接口的 Swagger 把 `orderNo` 标成「子订单团号」、example 写 `GT-26-0081`,照着当团号渲染出来的其实是订单号 `HL20260922210334521`。本次**新增 `teamNo` 字段**承载真团号,`orderNo` 字段**保留不删**(前端 `v-for :key` 在用),但注解已订正为「子订单编号(非团号)」。**团号请改读 `teamNo`。**
|
||||||
|
2. **`vehicleRowCount` 不再恒 ≥ `householdCount`。** 改前「没提交过用车需求的户」根本不出现在车侧响应里;改后它们**进列表**(`requirements: []`、`status: null`),`householdCount` 随之变成「应报车的户数 = `households` 长度」。原先「行数 ≥ 户数」这个不变量**作废**,别再拿它写断言。实测基线读数 `householdCount=5 / countedHouseholdCount=0 / vehicleRowCount=2`。
|
||||||
|
3. **接送机批量确认允许部分成功,且部分失败时 HTTP 仍是 `code:200` / `success:true`。** 失败的户在 `data.failed[]` 里逐条给 `orderId + errorCode + reason`。**不要用 `success` 判断「是不是全成了」**,要看 `failedCount`。
|
||||||
|
|
||||||
|
## 一、背景
|
||||||
|
|
||||||
|
工单 #8195,wx 在团期「查看需求」页上点出的五处缺口,合并为一个 PR(#8204,squash `62449e550`)。五处分别对应:缺陷 1 团号、缺陷 2 未提交户不可见、缺陷 3 接送机无批量确认、缺陷 4 车型无字典校验、缺陷 5 缺团期结束日。
|
||||||
|
|
||||||
|
## 二、变更接口清单
|
||||||
|
|
||||||
|
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 1 | 批量确认接送机需求 | POST | `/v3/admin/order/group-batch/{groupBatchId}/requirement/transfer/batch-confirm` | 新增 | 允许部分成功;幂等窗口 120s |
|
||||||
|
| 2 | 团期子订单用车需求记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` | 修改 | +`endDate` +`teamNo` +户级`status`/`statusName`;未提交户进列表 |
|
||||||
|
| 3 | 团期子订单订房记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households` | 修改 | +`endDate` +`teamNo`;`orderNo` 注解订正 |
|
||||||
|
| 4 | 保存团期正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 修改 | 车型必须命中字典,否则 809119 |
|
||||||
|
|
||||||
|
## 三、接口详情
|
||||||
|
|
||||||
|
### 1. 批量确认接送机需求 `POST /v3/admin/order/group-batch/{groupBatchId}/requirement/transfer/batch-confirm`
|
||||||
|
|
||||||
|
**VO**: `TransferBatchConfirmReqVO` → `Result<TransferBatchConfirmRespVO>`
|
||||||
|
|
||||||
|
#### 使用场景
|
||||||
|
|
||||||
|
「查看需求」页「用车」板块,团期管理员勾选若干户的接送机需求,一次性确认并转交车务。等价于逐户调 `POST /v3/admin/order/{id}/vehicle-requirement/dispatch?kind=TRANSFER`,区别是**允许部分成功**:整批里只要有一户能确认,接口就按业务成功返回,失败户单独列出而不回滚成功户。权限码与整团确认、按户打回同为 `group-batch:demand:confirm`——同一个 Tab 里同一批人的同一类动作,分码会出现「能逐户放行却不能批量放行」这种前端无法向运营解释的组合。
|
||||||
|
|
||||||
|
#### 入参
|
||||||
|
|
||||||
|
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `groupBatchId` | path | String | 是 | 雪花 ID | 团期 ID,字符串透传,禁 `Number()` |
|
||||||
|
| `orderIds` | body | Long[] | 是 | `@NotEmpty`、`@Size(max=200)` | 待确认的子订单 ID,1~200 户,**服务端去重** |
|
||||||
|
| `dispatchRemark` | body | String | 否 | `@Size(max=500)` | 确认备注,**整批共用**,提供给车队 |
|
||||||
|
|
||||||
|
#### 出参
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `groupBatchId` | String | 团期 ID(`ToStringSerializer`) |
|
||||||
|
| `requestedCount` | int | 去重后的待确认户数,**恒等于 `successCount + failedCount`**,可用于对账 |
|
||||||
|
| `successCount` | int | 确认成功的户数 |
|
||||||
|
| `failedCount` | int | 确认失败的户数;**> 0 时请展示 `failed` 明细,不要只提示「部分成功」** |
|
||||||
|
| `succeededOrderIds` | String[] | 成功的子订单 ID,按请求顺序,字符串形态防 JS 精度丢失 |
|
||||||
|
| `failed` | FailedItem[] | 失败明细,按请求顺序;**全部成功时是空数组,不是 null** |
|
||||||
|
| `failed[].orderId` | String | 子订单 ID |
|
||||||
|
| `failed[].errorCode` | Integer | 业务错误码;**非业务异常(系统故障)时为 null** |
|
||||||
|
| `failed[].reason` | String | 已渲染的中文报文,可直接展示 |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v3/admin/order/group-batch/2102383303678189570/requirement/transfer/batch-confirm
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"orderIds":[2102383417822003201,2102383458422841346],"dispatchRemark":"11/20 首都机场接"}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
全部成功(测试服实测原文,`ac/06-batch-confirm-1.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":200,"message":"成功","data":{"groupBatchId":"2102383303678189570","requestedCount":2,"successCount":2,"failedCount":0,"succeededOrderIds":["2102383417822003201","2102383458422841346"],"failed":[]},"traceId":null,"success":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
部分成功(测试服实测原文,`ac/07-partial-fail.json`)。**注意 `code` 是 200、`success` 是 true,但有一户没确认成功**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":200,"message":"成功","data":{"groupBatchId":"2102383303678189570","requestedCount":2,"successCount":1,"failedCount":1,"succeededOrderIds":["2102405916668440577"],"failed":[{"orderId":"2102383417822003201","errorCode":582083,"reason":"需求状态不允许此操作,请检查当前状态"}]},"traceId":null,"success":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 空数据 / 降级响应
|
||||||
|
|
||||||
|
`orderIds` 传空数组或不传时不进业务逻辑,直接被参数校验拦下,不产生任何写操作。`succeededOrderIds` 与 `failed` 在任何成功响应里都是数组,不会是 `null`——全成功时 `failed` 是 `[]`,全失败时 `succeededOrderIds` 是 `[]`,前端可以无条件 `.map()` 而不必先判空。
|
||||||
|
|
||||||
|
#### 错误响应
|
||||||
|
|
||||||
|
整批被拒的两种形态(幂等拦截为测试服实测原文 `ac/08-batch-confirm-retry.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":400,"message":"请至少选择一个要确认的子订单"}
|
||||||
|
{"code":100502,"message":"接送机需求确认处理中,请勿重复提交","data":null,"traceId":null,"success":false}
|
||||||
|
```
|
||||||
|
|
||||||
|
单户被拒**不走错误响应**,而是进上面 `data.failed[]`,典型 `errorCode` 为 `582083`「需求状态不允许此操作,请检查当前状态」(例如该户已经是 `PENDING`)。
|
||||||
|
|
||||||
|
#### 业务边界
|
||||||
|
|
||||||
|
- **幂等窗口 120 秒**:`@Idempotent` 的 key 由 `groupBatchId` + `orderIds` 共同决定 ⇒ **换一批 `orderIds` 不受上一次影响**,同一批在 120s 内第二次必被 `100502` 拒。
|
||||||
|
- **部分成功是设计,不是异常**:失败户不会被静默跳过,也不会把整批回滚。
|
||||||
|
- **确认只改需求状态,不产生派车记录**:本阶段只把 `order_vehicle_requirement.status` 与 `order_main.vehicle_control_status` 从 `PENDING_REVIEW` 置为 `PENDING`;实测两次调用前后 `fleet_assignment` 按 `order_id` 过滤 `COUNT(*)` 均为 0。真正的派车分单是车队侧后续独立动作。
|
||||||
|
- **重复的 `orderIds` 服务端去重**:`requestedCount` 是去重**后**的数,前端拿它对账不会因为自己传重而对不上。
|
||||||
|
- **响应体里不含刷新后的行**:确认成功后需要重新拉一次 `vehicle-households` 才能看到新状态。
|
||||||
|
|
||||||
|
### 2. 团期子订单用车需求记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households`
|
||||||
|
|
||||||
|
**VO**: `Result<GroupVehicleHouseholdsRespVO>`
|
||||||
|
|
||||||
|
#### 使用场景
|
||||||
|
|
||||||
|
「查看需求」页「用车」板块下半块的逐户列表。本次把它从「已提交需求的户的列表」改成「应报车的户的列表」——运营需要看见「谁还没交」,而改前那些户根本不出现在响应里,页面上无从催办。
|
||||||
|
|
||||||
|
#### 入参
|
||||||
|
|
||||||
|
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `groupBatchId` | path | String | 是 | 雪花 ID | 团期 ID,字符串透传,禁 `Number()` |
|
||||||
|
| `kind` | query | String | 否 | `TRAVEL` / `TRANSFER` | **不传 = 两类都返**(与提交侧「不传按 TRAVEL」的缺省相反,此处未改) |
|
||||||
|
|
||||||
|
#### 出参
|
||||||
|
|
||||||
|
新增 4 个字段、2 个既有字段口径变化,其余未动:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `endDate` | String | **新增**。团期结束日期 `yyyy-MM-dd`;团期未定结束日时为 null(与团期详情 `endDate` 同源) |
|
||||||
|
| `households[].teamNo` | String | **新增**。子订单团号,取 `order_main.team_no`;**未付订金尚未分配时原样返 null**,后端不兜底成 `orderNo`、不回退空串 |
|
||||||
|
| `households[].status` | String | **新增**(户级)。**null = 该户一份用车需求都没提交**;非 null 时取展示序首条(TRAVEL 优先)的状态 |
|
||||||
|
| `households[].statusName` | String | **新增**(户级)。与 `status` 同一条需求行的中文名;`status` 为 null 时本字段也为 null |
|
||||||
|
| `householdCount` | int | **口径变更**。改前 = 有活跃需求行的户数;改后 = 应报车的户数,恒等于 `households` 长度,**含一份都没提交的户**。仍按 `orderId` 去重(一户同时报行程用车与接送机只算 1 户) |
|
||||||
|
| `vehicleRowCount` | int | 口径未变、**关系变了**。= Σ 各户 `requirements` 长度,未提交户贡献 0 行 ⇒ **可能小于 `householdCount`** |
|
||||||
|
| `countedHouseholdCount` | int | 口径未变、**分母变了**。= 有活跃 TRAVEL 行的户数。`householdCount − countedHouseholdCount` 从改前的「只报了接送机的户」变成「只报了接送机的户 **+ 一份都没提交的户**」 |
|
||||||
|
| `households[].orderNo` | String | 值与形态都没变(`"HL" + yyyyMMddHHmmssSSS`,定长 19),只是 Swagger 不再谎称它是团号 |
|
||||||
|
| `requirements[].status` | String | **未变**,早就有。逐条的权威状态在这里,不在户级 `status` |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v3/admin/order/group-batch/2102383303678189570/requirement/vehicle-households
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
测试服实测(`ac/00-vehicle-households.json`)顶层为 `householdCount=5`、`countedHouseholdCount=0`、`vehicleRowCount=2`、`endDate="2026-11-21"`,五户的关键字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[{"orderNo":"HL20260922210334521","teamNo":"26-3724","status":null,"requirements":[]},
|
||||||
|
{"orderNo":"HL20260922210348601","teamNo":null,"status":null,"requirements":[]},
|
||||||
|
{"orderNo":"HL20260922210358692","teamNo":null,"status":null,"requirements":[]},
|
||||||
|
{"orderNo":"HL20260922210401823","teamNo":"26-0847","status":"PENDING_REVIEW","requirements":["…1 条"]},
|
||||||
|
{"orderNo":"HL20260922210411494","teamNo":"26-7970","status":"PENDING_REVIEW","requirements":["…1 条"]}]
|
||||||
|
```
|
||||||
|
|
||||||
|
三个计数两两不等,正好演示新口径:5 户全在列表里,只有 2 户提交过,且两条都是 TRANSFER,所以计入车侧汇总的 TRAVEL 户数是 0。
|
||||||
|
|
||||||
|
#### 空数据 / 降级响应
|
||||||
|
|
||||||
|
该户没提交任何用车需求时 `requirements` 是 `[]`(**空数组,不是 null**)、`status` 与 `statusName` 均为 null;团期未定结束日时 `endDate` 为 null;未付订金时 `teamNo` 为 null。整团一户都没有时三个计数为 0、`households` 为 `[]`,接口仍返 200。以上四种降级都不会让接口报错,前端需要各自有占位显示。
|
||||||
|
|
||||||
|
#### 错误响应
|
||||||
|
|
||||||
|
本次未改,沿用团期段位错误码:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":589500,"message":"团期不存在","data":null,"traceId":null,"success":false}
|
||||||
|
```
|
||||||
|
|
||||||
|
另有 `589507`(`GROUP_BATCH_PERMISSION_DENIED`,团期操作/读取被拒)由拦截器透 `message`,前端直接展示即可。
|
||||||
|
|
||||||
|
#### 业务边界
|
||||||
|
|
||||||
|
- **户级 `status` 是折叠态用的,不是权威状态**:一户可能同时有 TRAVEL 与 TRANSFER 两条活跃行、状态各自独立(例如 TRAVEL 已放行 `PENDING`、TRANSFER 还在 `PENDING_REVIEW`),户级 `status` 只取展示序第一条。**按条判断一律读 `requirements[].status`。**
|
||||||
|
- 🔴 **「从未提交」与「提交后被打回」在本接口上不可区分**:打回 = 原地置 `REJECTED_*` + `is_active=0`,失活行不进 `requirements` ⇒ 被打回的户同样是 `requirements: []` + `status: null`,与从未提交的户**完全同形**。依据 `GroupVehicleHouseholdsRespVO.java:169-171`。要把这两种人分开,本接口给不出判据。
|
||||||
|
- **`teamNo` 与 `orders` 接口同源同值**:取 `order_main.team_no`,不是 `order_group_batch.batch_no`(那是整团一个值,放在逐户列表上每行都一样,这一列就没有分辨力了)。三个读口(hotel-households / vehicle-households / orders)的 `teamNo` 实测逐字符相等。
|
||||||
|
|
||||||
|
### 3. 团期子订单订房记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/hotel-households`
|
||||||
|
|
||||||
|
**VO**: `Result<GroupHotelHouseholdsRespVO>`
|
||||||
|
|
||||||
|
#### 使用场景
|
||||||
|
|
||||||
|
「查看需求」页「用房」板块下半块的逐户列表,与 `requirement-summary` 并列调用。本次只补两个字段并订正一处注解,列表口径没动——用房侧本来就包含未提交的户(`status=null`、`days=[]`),这次是车侧向它对齐,不是它变了。
|
||||||
|
|
||||||
|
#### 入参
|
||||||
|
|
||||||
|
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `groupBatchId` | path | String | 是 | 雪花 ID | 团期 ID,字符串透传,禁 `Number()` |
|
||||||
|
|
||||||
|
#### 出参
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `endDate` | String | **新增**。团期结束日期 `yyyy-MM-dd`,未定时 null,与车侧同源同值 |
|
||||||
|
| `households[].teamNo` | String | **新增**。同车侧,取 `order_main.team_no`,未付订金时 null |
|
||||||
|
| `households[].orderNo` | String | 注解订正(「子订单团号」→「子订单编号(非团号)」),**值未变** |
|
||||||
|
| `households[].status` | String | **未变**。用房侧本来就有(未提交时为 null) |
|
||||||
|
| `households[].statusName` | String | **未变**。用房侧本来就有 |
|
||||||
|
| `householdCount` | int | **未变**。与 `countedHouseholdCount` 的差值仍是「未计入汇总(打回 / 未提交)的户数」 |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v3/admin/order/group-batch/2102383303678189570/requirement/hotel-households
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
测试服实测(`ac/00-hotel-households.json`),`endDate` 为 `"2026-11-21"`,五户 `teamNo` 与车侧、与 `GET .../orders` 三方逐字符相等:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[{"orderNo":"HL20260922210334521","teamNo":"26-3724"},
|
||||||
|
{"orderNo":"HL20260922210348601","teamNo":null},
|
||||||
|
{"orderNo":"HL20260922210358692","teamNo":null},
|
||||||
|
{"orderNo":"HL20260922210401823","teamNo":"26-0847"},
|
||||||
|
{"orderNo":"HL20260922210411494","teamNo":"26-7970"}]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 空数据 / 降级响应
|
||||||
|
|
||||||
|
未付订金的户 `teamNo` 为 null;团期未定结束日时 `endDate` 为 null。既有的降级行为一律未动:需订房但未提交的户仍会列出(`status=null`、`days=[]`),客户自订晚仍会列出且 `hotels=[]`,打回户仍列出且 `countedInSummary=false`。
|
||||||
|
|
||||||
|
#### 错误响应
|
||||||
|
|
||||||
|
本次未改,与车侧同段位:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":589500,"message":"团期不存在","data":null,"traceId":null,"success":false}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 业务边界
|
||||||
|
|
||||||
|
- **用房侧的列表口径没有跟着车侧一起改**:它本来就含未提交户,本次只补 `teamNo` 与 `endDate` 两个字段。
|
||||||
|
- **「汇总 == Σ 子订单」这条既有硬约束不受影响**:汇总逐日间数仍等于本接口 `countedInSummary=true` 各户逐日加总,差值仍是 `householdCount − countedHouseholdCount`。
|
||||||
|
- **`orderNo` 的排序契约未变**:`households` 仍按 `orderNo` 升序,新增 `teamNo` 不参与排序——**不要改用 `teamNo` 排序**,它可以为 null。
|
||||||
|
|
||||||
|
### 4. 保存团期正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement`
|
||||||
|
|
||||||
|
**VO**: `GroupVehicleRequirementSaveReqVO` → `Result<GroupVehicleRequirementRespVO>`
|
||||||
|
|
||||||
|
#### 使用场景
|
||||||
|
|
||||||
|
团期管理员新增 / 编辑整团的正式行程用车需求,一次全量替换整份(主表 + 全部分组 + 全部逐日行)。本次给分组里的车型加了字典校验:改前前端传什么就落什么,运营填错的车型要等到车队派车时才暴露;改后在保存这一步就拒。
|
||||||
|
|
||||||
|
#### 入参
|
||||||
|
|
||||||
|
结构不变,仅新增一条约束:
|
||||||
|
|
||||||
|
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `groupBatchId` | path | String | 是 | 雪花 ID | 团期 ID,字符串透传 |
|
||||||
|
| `groups[].vehicleType` | body | String | 是 | **新增:必须命中车型字典** | 车型大类编码,须存在且未下线,从下拉项取 |
|
||||||
|
| `groups[].groupName` | body | String | 是 | — | 组名,校验失败时会被写进错误报文,便于定位是哪一组 |
|
||||||
|
|
||||||
|
#### 出参
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `groups[].vehicleType` | String | 校验通过后回显的车型编码,未变 |
|
||||||
|
| `groups[].vehicleTypeName` | String | 车型中文名,后端按字典下发,**禁前端自映射** |
|
||||||
|
|
||||||
|
其余字段结构不变。
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"groups":[{"groupName":"AC9-BAD","vehicleType":"minivan"}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
`vehicleType` 须取自 `GET /admin/fleet/vehicle-types/list` 的下拉项;上例的 `minivan` 不在字典内,用于演示校验被触发。
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
合法车型实测(`ac/09-good-type-4.json` 节选):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":200,"message":"成功","data":{"groups":[{"vehicleType":"bus","vehicleTypeName":"大巴系列"}]},"success":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 空数据 / 降级响应
|
||||||
|
|
||||||
|
车型字典为空时**不放行**(fail-closed),不会退化成「不校验」——宁可让保存失败并提示运营去维护字典,也不能把一批查不到名字的车型放进正式需求,那会在车队侧变成一堆无法派车的行。
|
||||||
|
|
||||||
|
#### 错误响应
|
||||||
|
|
||||||
|
非法车型实测原文(`ac/09-bad-type.json`),报文里带**组名**,前端可直接定位到是哪一组填错:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code":809119,"message":"第 AC9-BAD 组的车型 minivan 不在车型字典内(不存在或已下线),请从下拉项中选择","data":null,"traceId":null,"success":false}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 业务边界
|
||||||
|
|
||||||
|
- **车型必须从 `GET /admin/fleet/vehicle-types/list` 的返回里选**,不要在前端硬编码枚举——字典行可被下线,下线后同一个编码就会被拒。
|
||||||
|
- **本端点另有两条既有前置会先于车型校验触发**(本次未改):团期阶段守卫(`RECRUITING` 阶段被 `589501`「团期状态不允许当前操作」拒,需先成团)、逐日乘车分组必须覆盖全团在团户与全部行程日(否则 `809109`「子订单 {0} 的 {1} 没有被任何乘车分组覆盖」)。
|
||||||
|
- **整份全量替换**:保存即覆盖,前端提交前必须带上未改动的分组与逐日行,否则会被删掉。
|
||||||
|
|
||||||
|
## 四、契约约束与正确调用方式
|
||||||
|
|
||||||
|
### ✅ 正确 / ❌ 错误对照
|
||||||
|
|
||||||
|
| 场景 | ❌ 错误 | ✅ 正确 |
|
||||||
|
|---|---|---|
|
||||||
|
| 渲染团号列 | 读 `orderNo` | 读 `teamNo`;为 null 时显示占位符,**不要回落成 `orderNo`** |
|
||||||
|
| 表头「共 N 户」 | 用行数或自行推算 | 取 `householdCount`(与 `households.length` 恒等) |
|
||||||
|
| 判断批量确认结果 | `if (res.success) { 提示全部成功 }` | `if (res.data.failedCount > 0) { 展示 res.data.failed 明细 }` |
|
||||||
|
| 判断某条需求的状态 | 读户级 `status` | 读 `requirements[i].status` |
|
||||||
|
| 车型下拉 | 前端写死枚举数组 | 调 `GET /admin/fleet/vehicle-types/list` |
|
||||||
|
| 列表排序 | 改用 `teamNo` 排序 | 仍按 `orderNo`(`teamNo` 可为 null) |
|
||||||
|
| 传 ID | `Number(orderId)` | 雪花 ID 一律字符串透传 |
|
||||||
|
|
||||||
|
### 状态切换后的必要动作
|
||||||
|
|
||||||
|
批量确认成功后,被确认户的 `order_vehicle_requirement.status` 与 `order_main.vehicle_control_status` 都变为 `PENDING`。响应体里不含刷新后的行,需要重新拉一次 `vehicle-households`。
|
||||||
|
|
||||||
|
## 五、数据库行为
|
||||||
|
|
||||||
|
批量确认端点写两张表:`order_vehicle_requirement.status`、`order_main.vehicle_control_status`,均 `PENDING_REVIEW → PENDING`,调用前后逐户 `SELECT` 核对过。**不写 `fleet_assignment`**(前后均 `COUNT(*)=0`)。清单里的 2、3 两个读接口不写库。
|
||||||
|
|
||||||
|
## 六、边界行为
|
||||||
|
|
||||||
|
| 情形 | 行为 |
|
||||||
|
|---|---|
|
||||||
|
| 未付订金的户 | `teamNo` 为 `null`(`team_no` 此时尚未分配),前端需要占位显示 |
|
||||||
|
| 一份用车需求都没提交的户 | 进车侧列表,`requirements: []`、`status: null`、`statusName: null` |
|
||||||
|
| 提交后被打回的户 | **与上一行完全同形,本接口不可区分** |
|
||||||
|
| 一户同时有 TRAVEL + TRANSFER | `householdCount` 只算 1 户,`vehicleRowCount` 算 2 行 |
|
||||||
|
| 批量确认里混入状态不对的户 | 整批仍按 `code:200` 返回,该户进 `failed[]` |
|
||||||
|
| 120s 内同批重复提交 | `code:100502`,整批拒 |
|
||||||
|
| 团期未定结束日 | `endDate: null` |
|
||||||
|
| 车型字典为空 | 保存被拒(fail-closed),不退化成不校验 |
|
||||||
|
|
||||||
|
## 六.5、枚举 / 数据字典
|
||||||
|
|
||||||
|
### `households[].status`(户级用车需求状态)
|
||||||
|
|
||||||
|
`PENDING_REVIEW`(待审核) / `PENDING` / `PROCESSING` / `DONE`,以及 **`null` = 该户一份都没提交**。`statusName` 由后端下发,**禁前端自映射**。
|
||||||
|
|
||||||
|
### `vehicleType`(车型大类编码)
|
||||||
|
|
||||||
|
**运行期字典,不是固定枚举**,取自 `fleet_vehicle_type`,通过 `GET /admin/fleet/vehicle-types/list` 下发。2026-09-22 测试服上存活 4 项(`suv2` / `mpv` / `sedan` / `bus`),但这是**当日快照、不是契约**——字典行可被增删下线,前端不得据此硬编码。
|
||||||
|
|
||||||
|
## 六.6、修改前后对比
|
||||||
|
|
||||||
|
### 字段级对比
|
||||||
|
|
||||||
|
| 接口 | 字段 | 改前 | 改后 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| hotel / vehicle-households | `orderNo` 的 Swagger 说明 | 「子订单团号」,example `GT-26-0081` | 「子订单编号(非团号;形如 HL + yyyyMMddHHmmssSSS)」,example `HL20260516143052999`。**字段值本身从未变过**,一直是订单号 |
|
||||||
|
| hotel / vehicle-households | `teamNo` | 不存在 | 新增,真团号,可为 null |
|
||||||
|
| hotel / vehicle-households | `endDate` | 不存在 | 新增,可为 null |
|
||||||
|
| vehicle-households | 户级 `status` / `statusName` | 不存在 | 新增(用房侧本来就有;`requirements[].status` 也早就有,别混淆) |
|
||||||
|
|
||||||
|
### 行为级对比
|
||||||
|
|
||||||
|
| 行为 | 改前 | 改后 |
|
||||||
|
|---|---|---|
|
||||||
|
| 没提交用车需求的户 | **不在车侧响应里** | 在响应里,空卡 |
|
||||||
|
| `householdCount` | 有活跃需求行的户数 | `households` 长度 |
|
||||||
|
| `vehicleRowCount` vs `householdCount` | 恒 ≥ | **可能 <** |
|
||||||
|
| 接送机确认 | 只能逐户 `dispatch?kind=TRANSFER` | 可批量,允许部分成功 |
|
||||||
|
| 提交非法车型 | 通过,落库 | `809119` 拒 |
|
||||||
|
|
||||||
|
### 🔴 hl-ui 里已经不准确的 JSDoc(`origin/v2.1`,`src/api/orderV2GroupBatch.js`)
|
||||||
|
|
||||||
|
两个函数的 JSDoc 都写于改前,各有过期处。行号为 2026-09-22 在 `origin/v2.1` 上实读:
|
||||||
|
|
||||||
|
**`getGroupVehicleHouseholds`(JSDoc `:597-622`)**
|
||||||
|
|
||||||
|
- **`:600`**「被打回的需求行不在列表内(失活即消失,勿按用房那套找打回户)」——这句本身仍成立,但它隐含的「列表里的户都提交过」已不成立:未提交户现在也在列表里,且**和被打回户长得一模一样**。
|
||||||
|
- **`:601-602`**「householdCount 按 orderId 去重…vehicleRowCount 数行,两者刻意不等价——表头「共 N 户」必须取 householdCount,禁取行数」——**结论仍然对**(表头就该取 `householdCount`),但它没说方向,读的人会默认行数 ≥ 户数,**改后可能反过来**。
|
||||||
|
- **`:607-620`** 的 `@returns` 结构体——缺 `endDate`、`teamNo`、户级 `status`、户级 `statusName` 四个新字段。注意 `:613` 已有的 `status/statusName` 是 `requirements[]` 里的,不是户级的。
|
||||||
|
|
||||||
|
**`getGroupHotelHouseholds`(JSDoc `:568-589`)**
|
||||||
|
|
||||||
|
- **`:576-586`** 的 `@returns` 结构体——缺 `endDate` 与 `teamNo`;`:579` 列的 `orderNo` 需要补一句「不是团号」。
|
||||||
|
- `:571-574` 关于打回户、未提交户与「汇总 == Σ 子订单」的几条口径**未变**,不必动。
|
||||||
|
|
||||||
|
## 六.7、影响评估
|
||||||
|
|
||||||
|
| 面 | 评估 |
|
||||||
|
|---|---|
|
||||||
|
| 破坏性 | **无字段删除、无字段改名、无类型变更**,`orderNo` 的值与形态都没动 ⇒ 前端不改也不会报错 |
|
||||||
|
| 但会静默出错的地方 | ①「共 N 户」若不是取 `householdCount` 而是别的推算,数字会和列表对不上;②「行数 ≥ 户数」的断言会在有未提交户时挂掉;③ 批量确认只看 `success` 会把部分失败当成全成功 |
|
||||||
|
| 需要前端动手的 | 团号列改读 `teamNo`、空卡的展示与催提交入口、批量确认按钮与部分失败明细、车型下拉改走字典接口、`endDate` 展示 |
|
||||||
|
|
||||||
|
## 七、不影响范围
|
||||||
|
|
||||||
|
- `GET /v3/admin/order/group-batch/{id}/orders`:未改,`teamNo` 本来就有,本次是让另外两个接口与它对齐。
|
||||||
|
- 用房侧的列表口径与「汇总 == Σ 子订单」硬约束:未改。
|
||||||
|
- 逐户 `POST /v3/admin/order/{id}/vehicle-requirement/dispatch`:未改,仍可用。
|
||||||
|
- 车侧汇总 `requirement-summary` 的既有口径:未改,仍只统计行程用车(#8151 的语义保持)。
|
||||||
|
- 小程序端、`/mp/` 接口:零改动。
|
||||||
|
- 网关:零新增路由。
|
||||||
|
- 结算侧 `settlement/**`:与本次改动零文件重叠。
|
||||||
|
|
||||||
|
## 八、测试环境已验证
|
||||||
|
|
||||||
|
网关 `https://api.test.1814.love:9443`,order-v3 读数 `dev-v3 / 62449e550 / 2026-09-22 22:22:58 / STATE=ok`(`62449e550` 即本单合并提交本身)。
|
||||||
|
|
||||||
|
| 验证项 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| `teamNo` 三方同源 | hotel / vehicle / orders 三个接口逐户逐字符相等;已付订金户非空、未付订金户为 `null`,两种情形都覆盖 |
|
||||||
|
| `teamNo ≠ orderNo` | 逐户为不同值 |
|
||||||
|
| 未提交户进列表 | 空卡与 `status="PENDING_REVIEW"` 的已提交户在**同一份响应**里同时存在,字段有分辨力 |
|
||||||
|
| 三个计数可区分 | 基线 `5 / 0 / 2`,终态 `6 / 0 / 3`,均两两不等 |
|
||||||
|
| 批量确认 | 2 户全成功;DB 回读两户双表均 `PENDING_REVIEW → PENDING` |
|
||||||
|
| 部分成功 | 1 成 1 败,失败户带 `orderId + errorCode(582083) + reason` |
|
||||||
|
| 幂等 | 同批 120s 内第二次 `code:100502`;DB 回读无重复行 |
|
||||||
|
| 车型字典两个方向 | 非法 `minivan` → `809119` 拒;合法 `bus` → 200 通过并回显 `vehicleTypeName` |
|
||||||
|
| `endDate` | hotel / vehicle / batch-detail 三方均 `2026-11-21`,逐字符相等 |
|
||||||
|
| 单测 / ArchTest | 定向 9 个类共 173 个用例逐类点名核对全绿,含 `MapperBoundaryArchTest` 27 与 `RedLineArchTest` 12 |
|
||||||
|
|
||||||
|
原始报文全部落盘(`D:/work2/_scratch/8195/ac/*.json`)。夹具为自建团期 `2102383303678189570` + 自建产品班期 `2102383185755312131`(`batchName` 为「8195夹具-2026年11月团期」),未复用他人夹具。
|
||||||
|
|
||||||
|
## 九、相关历史 PR
|
||||||
|
|
||||||
|
- #8151(团期子订单用车需求记录只读接口,本次改的就是它)
|
||||||
|
- #7441(团期正式用车需求的存 / 读 / 撤回 / 免车四端点)
|
||||||
|
|
||||||
|
## 十、相关文档
|
||||||
|
|
||||||
|
- Gitea Issue #8195
|
||||||
|
- PR #8204(squash `62449e550`)
|
||||||
|
- 相邻单 #8193(团期房务三事件站内信配置,同批部署)
|
||||||
|
|
||||||
|
## 关联 / 联系人
|
||||||
|
|
||||||
|
### 链接
|
||||||
|
|
||||||
|
- 工单: `https://git.1814.love:8443/wx/HL/issues/8195`
|
||||||
|
- PR: `https://git.1814.love:8443/wx/HL/pulls/8204`
|
||||||
|
|
||||||
|
### 联系人
|
||||||
|
|
||||||
|
- 后端: wx
|
||||||
|
- 前端: mmg(hl-ui,分支 `v2.1`)
|
||||||
在新工单中引用
屏蔽一个用户