文件
hl-api-changelog/changelogs-v2/2026-09/30_8559_团期用车户数计入汇总读数不随kind筛选变化-修改接口-管理后台.md
T

417 行
20 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "8559"
title: "团期子订单用车需求记录:countedHouseholdCount / countedInSummary 不再随 kind 筛选归零"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "PR #8612 已 squash 合并 dev-v3(3ecf387979),hl-order-service-v3 dev-v3 分支已滚测试服。本条只改取值语义,字段名、字段个数、HTTP 形态全部不变。"
updated_at: "2026-09-30"
base: "dev-v3"
---
# hl-order-service-v3: 团期子订单用车需求记录的「计入汇总」读数不再随 kind 筛选归零
> **存放目录**: `changelogs-v2/2026-09/`
> **服务**: hl-order-service-v3 (端口 8086)
> **PR**: #8612
> **Issue**: #8559
> **日期**: 2026-09-30
> **影响范围**: 团期「查看需求」Tab 用车板块逐户明细端点 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` 的两个取值(顶层 `countedHouseholdCount`、每户 `households[].countedInSummary`)
---
## ⚠️ 关键变化
- 🔴 **这是取值语义纠错,不是字段变更**:`countedHouseholdCount` 和 `households[].countedInSummary` 的**字段名、类型、位置全部没动**,变的是它们在带 `kind` 筛选时返回的**值**。
- **改前**:带 `kind=TRANSFER` 调用时,`countedHouseholdCount` **恒为 0**,并且返回的每一户 `countedInSummary` **恒为 false**。原因是「这一户有没有被汇总计入座位」这个判据建在已经被 `kind` 截断过的需求行上——`kind=TRANSFER` 的结果集里不可能出现 TRAVEL 行,判据对每一户都落成 false。而团级汇总里那些户的座位是实实在在加进去的,同一页面两块数据互相矛盾。
- **改后**:判据改成「这一户在**全部活跃需求行**里有没有 TRAVEL 行」,与本次 `kind` 筛选无关。三种调用(不传 `kind` / `kind=TRAVEL` / `kind=TRANSFER`)下,同一户的 `countedInSummary` 取值相同,`countedHouseholdCount` 读数相同。
- **前端要做的事**:如果页面里有「`kind=TRANSFER` 时这个数恒为 0,所以隐藏/特判」这类兜底分支,**请删掉**——它现在会把正确的非零读数吞掉。另外不要再用「`countedInSummary` 全 false」去推断当前处于接送机筛选态。
- **刻意没改**:卡片出不出现**仍然随 `kind` 变**(`kind=TRANSFER` 时只报了行程用车的户不出卡),这是 #8151 起的既有行为,本次不动。
- **刻意没改**:`householdCount`(应报车户数)与 `vehicleRowCount`(需求行数)的口径,本次一个字都没动。
---
## 一、背景
团期「查看需求」Tab 的用车板块是上下两块:上面是团级汇总,下面是逐户明细。逐户明细支持按 `kind` 切换筛选(行程用车 / 接送机 / 全部)。`countedHouseholdCount` 回答的问题是「这个团有几户被汇总计入了座位」——它是用来跟上面那块汇总对账的,天然与「我现在正在看哪一类需求」无关。
| 维度 | 改前(`kind=TRANSFER`) | 改后(`kind=TRANSFER`) |
|------|------------------------|------------------------|
| `countedHouseholdCount` | 恒 `0` | 与不传 `kind` 时相同 |
| `households[].countedInSummary` | 每户恒 `false` | 与不传 `kind` 时逐户相同 |
| 卡片出现范围 | 随 `kind` 变 | 随 `kind` 变(未改) |
| 查库往返次数 | 1 次(IN 单值) | 1 次(IN 两值) |
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期子订单用车需求记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` | 取值语义修正 | `countedHouseholdCount` 与 `households[].countedInSummary` 不再随 `kind` 筛选归零 |
---
## 三、接口详情
### 1. 团期子订单用车需求记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households`
**VO**: `GroupVehicleHouseholdsRespVO`
#### 使用场景
团期详情「查看需求」Tab 的用车板块下半部分(逐户明细列表)。进入 Tab 时前端默认**不传** `kind`(两类都返);用户点「行程用车 / 接送机」切页签时带上 `kind`。本端点只读,无副作用。权限点 `group-batch:view`。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `groupBatchId` | Path | Long | ✅ | 团期主订单 ID | 团期不存在时抛团期未找到错误 |
| `kind` | Query | String | ❌ | `TRAVEL` / `TRANSFER`,不传或空白 = 两类都返 | 其它取值抛 809000「用车需求类别非法」。注意与提交侧「不传按 TRAVEL」的缺省刻意相反 |
#### 出参 `Result<GroupVehicleHouseholdsRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `groupBatchId` | String | 团期主订单 ID(雪花,序列化为字符串) |
| `departDate` | String(`yyyy-MM-dd`) | 团期出发日,可为 null |
| `endDate` | String(`yyyy-MM-dd`) | 团期结束日,与团期详情同源,可为 null |
| `householdCount` | Integer | 应报车户数 = `households` 数组长度(含一份需求都没提交的空卡) |
| `vehicleRowCount` | Integer | 需求行数 = 各户 `requirements` 长度之和;**可以小于 `householdCount`**,不要当不变式用 |
| `countedHouseholdCount` | Integer | 🔴 被汇总计入座位的户数 = `households` 中 `countedInSummary=true` 的条数。**不随 `kind` 筛选变化**,三种筛选读数相同。与 `householdCount` 的差 = 只报接送机的户 + 未提交的户 |
| `households` | Array | 逐户卡片,按 `orderNo` 升序(`orderNo` 为空的排最后);单次最多 500 户 |
| `households[].orderId` | String | 子订单 ID(雪花,序列化为字符串) |
| `households[].orderNo` | String | 子订单号 |
| `households[].teamNo` | String | 团号,可为 null(不用订单号顶替) |
| `households[].customerName` | String | 客户姓名 |
| `households[].participantCount` | Integer | 出行人数 |
| `households[].consultantId` | String | 定制师 adminId,未指派为 null |
| `households[].consultantName` | String | 定制师姓名,未指派为 null |
| `households[].countedInSummary` | Boolean | 🔴 该户是否被团级汇总计入座位。**不随 `kind` 筛选变化**,同一户三种筛选下取值相同 |
| `households[].status` | String | 展示用需求状态;该户一份需求都没提交时为 null |
| `households[].statusName` | String | 状态中文名,`status` 为 null 时为 null |
| `households[].requirements` | Array | 该户活跃需求行,0~2 条;无行时为**空数组**不是 null |
| `households[].requirements[].requirementId` | String | 需求行 ID(雪花,序列化为字符串) |
| `households[].requirements[].kind` | String | `TRAVEL` / `TRANSFER` |
| `households[].requirements[].kindName` | String | 类别中文名 |
| `households[].requirements[].status` | String | `PENDING_REVIEW` / `PENDING` / `PROCESSING` / `DONE` |
| `households[].requirements[].statusName` | String | 状态中文名 |
| `households[].requirements[].fleet` | Array | 车型项:`vehicleType` / `vehicleTypeName` / `seats` / `count` |
| `households[].requirements[].specialTags` | Array | 特殊诉求标签:`code` / `name` |
| `households[].requirements[].remark` | String | 备注,≤500,可为 null |
| `households[].requirements[].serviceDates` | Array\<String\> | 服务日期(`yyyy-MM-dd`) |
| `households[].requirements[].headcount` | Integer | 用车人数 |
| `households[].requirements[].totalSeatCount` | Integer | 总座位数 |
| `households[].requirements[].remainingPassengerSeats` | Integer | 余座(扣司机位后) |
| `households[].requirements[].pickupRequired` | Boolean | **仅 TRANSFER 行有值**,TRAVEL 行为 null |
| `households[].requirements[].dropoffRequired` | Boolean | **仅 TRANSFER 行有值**,TRAVEL 行为 null |
| `households[].requirements[].returnRemark` | String | 打回备注,可为 null |
| `households[].requirements[].returnedAt` | String(date-time) | 打回时刻,可为 null |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2101690789438570497/requirement/vehicle-households?kind=TRANSFER HTTP/1.1
Host: <admin-gateway>
Authorization: Bearer <admin-token>
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "2101690789438570497",
"departDate": "2026-09-12",
"endDate": "2026-09-16",
"householdCount": 3,
"vehicleRowCount": 1,
"countedHouseholdCount": 2,
"households": [
{
"orderId": "2102000000000000001",
"orderNo": "HL20260912100000001",
"teamNo": "26-0480",
"customerName": "陈昊",
"participantCount": 4,
"consultantId": "10001",
"consultantName": "李四",
"countedInSummary": true,
"status": "PENDING",
"statusName": "待车队配",
"requirements": [
{
"requirementId": "2103000000000000011",
"kind": "TRANSFER",
"kindName": "接送机",
"status": "PENDING",
"statusName": "待车队配",
"fleet": [
{ "vehicleType": "suv", "vehicleTypeName": "SUV", "seats": 7, "count": 1 }
],
"specialTags": [
{ "code": "child_seat", "name": "儿童安全座椅" }
],
"remark": null,
"serviceDates": ["2026-09-12"],
"headcount": 4,
"totalSeatCount": 7,
"remainingPassengerSeats": 2,
"pickupRequired": true,
"dropoffRequired": false,
"returnRemark": null,
"returnedAt": null
}
]
},
{
"orderId": "2102000000000000002",
"orderNo": "HL20260912100000002",
"teamNo": "26-0480",
"customerName": "王琳",
"participantCount": 2,
"consultantId": "10001",
"consultantName": "李四",
"countedInSummary": true,
"status": null,
"statusName": null,
"requirements": []
},
{
"orderId": "2102000000000000003",
"orderNo": "HL20260912100000003",
"teamNo": null,
"customerName": "赵敏",
"participantCount": 3,
"consultantId": null,
"consultantName": null,
"countedInSummary": false,
"status": null,
"statusName": null,
"requirements": []
}
]
}
}
```
> 上例即改后行为:`kind=TRANSFER` 筛选下仍然有 `countedHouseholdCount=2`(前两户各有一条活跃 TRAVEL 行,被团级汇总计入了座位),只有第三户没报行程用车所以是 `false`。改前这三户的 `countedInSummary` 会全是 `false`、`countedHouseholdCount` 会是 `0`。
#### 空数据 / 降级响应
团期下没有在团子订单(全部退团 / 取消)时,三个计数全 `0`、`households` 为**空数组**(不是 null):
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"groupBatchId": "2101690789438570497",
"departDate": "2026-09-12",
"endDate": "2026-09-16",
"householdCount": 0,
"vehicleRowCount": 0,
"countedHouseholdCount": 0,
"households": []
}
}
```
单次返回的户数上限为 500 户,超过时**截断**返回前 500 条并在服务端留 warn,不报错——页面仍可用。截断后 `householdCount` / `vehicleRowCount` / `countedHouseholdCount` 都按截断后的列表重算,三者与 `households` 数组始终自洽。
#### 错误响应
`kind` 传了 `TRAVEL` / `TRANSFER` 以外的值:
```json
{
"code": 809000,
"message": "用车需求类别非法:BUS",
"success": false,
"data": null
}
```
#### 业务边界
- **鉴权**:需要权限点 `group-batch:view`;未登录由网关拦截返 401。
- **只读**:本端点零写入,重复调用无副作用,可安全轮询。
- **户范围**:在团 = 仅排除 CANCELLED。退团 / 取消的户不出现在列表里。
- **只取活跃行**:被打回的需求行已失活,**不在本列表内**(与用房侧「列出打回户」的行为刻意不同)。因此一户被打回后在这里表现为 `requirements: []` 的空卡,而不是灰条。
- **卡片范围仍随 `kind` 变**:卡片集合 = 「应报车的户」∪「本次筛选命中需求行的户」。这一条本次未改。
- **`countedInSummary` 不随 `kind` 变**:它读的是该户在**全部**活跃行里有没有 TRAVEL 行。
- **`pickupRequired` / `dropoffRequired` 只在 TRANSFER 行有值**,TRAVEL 行恒 null,不要用 `false` 去区分。
- **雪花 ID 一律是字符串**:`groupBatchId` / `orderId` / `consultantId` / `requirementId` 都以字符串下发,JS 直接当数字用会被静默截断。
---
## 四、契约约束与正确调用方式
> 本节只写后端接受 / 拒绝请求的规则,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误调用对照
| 场景 | 请求 |
|------|------|
| ✅ 进 Tab 默认拉全部 | `GET .../vehicle-households`(不带 `kind`) |
| ✅ 切到行程用车页签 | `GET .../vehicle-households?kind=TRAVEL` |
| ✅ 切到接送机页签 | `GET .../vehicle-households?kind=TRANSFER` |
| ✅ 显式传空值 | `GET .../vehicle-households?kind=`(空白按「两类都返」处理) |
| ❌ 传车型当类别 | `GET .../vehicle-households?kind=bus` → 809000 |
| ❌ 传小写类别 | `GET .../vehicle-households?kind=transfer` → 809000 |
### 切换筛选时的必要动作
切换 `kind` 时,**顶部「计入汇总 N 户」这类读数不需要跟着置灰或隐藏**——它在三种筛选下是同一个数。如果前端此前为了绕开恒 0 做过「接送机页签下不展示该数」的兜底,现在应当删掉,否则接送机页签会永远看不到这个已经正确的读数。
---
## 五、数据库行为
本端点是 **GET 只读**,零写入:不建行、不改行、不软删、不产生任何 outbox / MQ 事件。
本次改动只调整了服务端的**读法**(原先按 `kind` 下推到查询条件,现在恒查两类再在内存里按 `kind` 分流),SQL 往返次数未变(同一条 IN 查询,`requirement_kind` 的 IN 列表从 1 个值放宽到 2 个值)。
---
## 六、边界行为
- 未登录 → 401(网关拦截)。
- 权限点缺失 → 权限校验失败,不返回数据。
- 团期 ID 不存在 → 抛团期未找到业务错误,HTTP 200 + 业务错误码。
- 团期下无在团子订单 → 三个计数 0 + `households: []`,不报错。
- 户数超 500 → 截断到前 500 条,三个计数按截断后重算,HTTP 200。
- 在团订单 ID 存在但订单行缺失(脏数据)→ 跳过该户并在服务端留 warn,整页仍可打开。
- 需求行 `requirement_kind` 为空的脏行 → 显式跳过,不进任何计数。
- 老数据兼容:历史需求行缺 `seats` / `count` 等字段时对应位为 null,不异常。
---
## 六.5、枚举 / 数据字典
### kind(用车需求类别,`VehicleRequirementKind`)
**所属字段**: 请求 Query `kind`、响应 `households[].requirements[].kind` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `TRAVEL` | 行程用车 | 团期行程期间的用车需求;`countedInSummary` 的判据只认这一类 |
| `TRANSFER` | 接送机 | 接送机 / 接送站需求;`pickupRequired` / `dropoffRequired` 只在这一类上有值 |
请求侧不传或传空白 = 两类都返(不是「默认 TRAVEL」)。
### status(需求行状态)
**所属字段**: `households[].status`、`households[].requirements[].status` | **类型**: `String`
| 值 | 中文 | 说明 |
|----|------|------|
| `PENDING_REVIEW` | 待审核 | 定制师已提交,等团期管理员下发车务 |
| `PENDING` | 待车队配 | 已下发车务,等车队配车 |
| `PROCESSING` | 配车中 | 车务处理中 |
| `DONE` | 配车完成 | 已完成 |
户级 `status` 为 null 表示该户一份活跃需求都没有(空卡)。
---
## 六.6、修改前后对比
### 字段级对比
| 字段 | 改前 | 改后 |
|------|------|------|
| `countedHouseholdCount` | 字段存在,类型 Integer;`kind=TRANSFER` 时恒 `0` | 字段、类型不变;三种筛选读数相同 |
| `households[].countedInSummary` | 字段存在,类型 Boolean;`kind=TRANSFER` 时恒 `false` | 字段、类型不变;同一户三种筛选取值相同 |
| 其余全部字段 | — | 未变(无新增、无删除、无改名、无类型变化) |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 不传 `kind` 时的两个读数 | 正确 | 正确(未变) |
| `kind=TRAVEL` 时的两个读数 | 正确 | 正确(未变) |
| `kind=TRANSFER` 时的两个读数 | `countedHouseholdCount=0`、每户 `countedInSummary=false` | 与不传 `kind` 时一致 |
| 「计入汇总」的判据数据源 | 已被 `kind` 截断的需求行 | 该户的全部活跃需求行 |
| 卡片是否随 `kind` 变 | 变 | 变(未改) |
| `householdCount` / `vehicleRowCount` | — | 未变 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否(无字段增删改名;只是 `kind=TRANSFER` 时的取值由恒 0 / 恒 false 变为真实值)
- **前端是否必须同步上线**: 否(不改也能正常渲染,只是接送机页签下该数从「永远 0」变成真实值)
- **前端 workaround 清理点**: 若页面里有「`kind=TRANSFER` 时 `countedHouseholdCount` 恒 0,所以隐藏该数 / 走另一套算法 / 用 `countedInSummary` 全 false 判断当前筛选态」这类兜底分支,**删掉它们**——保留会吞掉正确读数或做出错误的筛选态判断。
---
## 七、不影响范围
- **仅影响**: `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` 的 `countedHouseholdCount` 与 `households[].countedInSummary` 两个取值。
- **零影响**:
- 团级用车汇总端点(本次改的是明细侧读法,汇总侧一个字没动)
- 团期正式用车需求的存 / 读 / 撤回 / 免车四个端点
- 用房侧 `requirement/hotel-households`
- 单户用车需求的提交、下发、打回链路
- 团期配车(fleet 侧)任何端点
- 历史数据:本次只改读法,不做任何数据迁移
---
## 八、测试环境已验证
- **代码事实**(对 `origin/dev-v3` 逐一查证):
- 合并提交 `3ecf387979`(PR #8612 squash 合并进 `dev-v3`)。
- `GroupBatchVehicleHouseholdService#households` 查库改为恒取 `TRAVEL` + `TRANSFER` 两类,另建 `travelOrderIds` 集合承载「计入汇总」判据;`buildHousehold` 签名增加 `boolean countedInSummary` 形参,替代原先在被截断的 `rows` 上做的 `anyMatch`。
- `GroupVehicleHouseholdsRespVO#countedHouseholdCount` 与 `HouseholdItem#countedInSummary` 的 `@ApiModelProperty` 已同步写明「不随 kind 筛选变化,三种筛选读数相同」。
- 卡片集合仍取自按 `kind` 过滤后的 `rowsByOrder`(既有行为,未改)。
- **部署**:`hl-order-service-v3` 的 `dev-v3` 分支已滚到测试服,本端点走管理端网关 `/v3/admin/**` 既有通配路由,无新增路由。
```
GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households → 200 ✓
GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households?kind=TRAVEL → 200 ✓
GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households?kind=TRANSFER → 200 + countedHouseholdCount 与前两次一致 ✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| — | #8151 | 首次提供本端点(逐户明细 + `kind` 筛选) | ✅ 有效 |
| — | #8195 | 缺陷 2:`householdCount` 改为「应报车户数」含空卡;缺陷 5:`endDate` 与团期详情同源 | ✅ 有效 |
| **本 PR #8612** | **#8559** | 「计入汇总」判据改为不受 `kind` 截断 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#8559](https://git.1814.love:8443/wx/HL/issues/8559)
- 关联 PR: [wx/HL#8612](https://git.1814.love:8443/wx/HL/pulls/8612)
## 关联 / 联系人
### 链接
- **Issue**: [#8559](https://git.1814.love:8443/wx/HL/issues/8559)
- **PR**: [#8612](https://git.1814.love:8443/wx/HL/pulls/8612)
- **Merge commit**: [3ecf387979](https://git.1814.love:8443/wx/HL/commit/3ecf387979)
### 联系人
- **后端负责人**: @wx