--- 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: "pending" 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` | 字段 | 类型 | 说明 | |------|------|------| | `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\ | 服务日期(`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: Authorization: Bearer ``` #### 响应示例 ```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