文件
hl-api-changelog/changelogs-v2/2026-09/30_8562_团期逐户用车区分从未提交与已被打回待重提-修改接口-管理后台.md
T
Mimingguang c98b11fa44
changelog-filename-gate / validate (push) Failing after 1s
chore(changelog): #8562 回写 implemented(hl-admin e3f3d552)
2026-10-01 00:09:34 +08:00

388 行
32 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "8562"
title: "团期逐户用车列表区分「从未提交」与「已被打回待重提」,打回明细走新字段下发"
consumer: "admin"
author: "wx(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "not_required"
frontend_status: "implemented"
frontend_owner: "hl-admin(claude-opus-4-8)"
frontend_ref: "e3f3d55265bf7f017d795e6e049601cee700dbfc"
target_release: "v2.1"
verified_at: "2026-09-30"
status_note: "GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households 的 households[] 新增三个字段:submitState(户级提交态,恒非 null,三取值 NEVER_SUBMITTED 从未提交 / SUBMITTED 已提交 / REJECTED_PENDING_RESUBMIT 已被打回待重提)、submitStateName(其中文名,后端下发)、rejectedRequirements(该户当前处于打回待重提的类别明细,恒非 null,无打回时为空数组,按展示序 TRAVEL 在前)。背景:用车的打回是「原地置 REJECTED_* + is_active=0」,被打回的户因此没有任何活跃需求行,与从未提交的户在 status 上完全同形(都是 null),车务照 status 催办会把「已交过、只是被驳回」和「压根没动过」混成一堆。四条必须照做的限定:(1) status 字段的取值规则一字未改、仍只由活跃行决定,判「有没有提交过」一律读 submitState,不要读 status 是否为 null;(2) requirements 列表内容零变化,被打回的行仍然不在里面,打回信息只在 rejectedRequirements;(3) rejectedRequirements 的元素刻意不与需求行同构(只有类别/打回状态/打回意见/打回时刻/版本号,没有车队明细、服务日期、座位数),不可当作需求行渲染,否则同一户会出现与活跃行自相矛盾的一条;(4) householdCount 口径再放宽一项,不再恒等于 needs_vehicle=true 的户数——一个 needs_vehicle 为假、没有活跃行、但有一类被打回的户现在也会进列表,判「这户为什么在列表里」看 submitState,不要拿 needsVehicle 反推。另两个数一字不动:vehicleRowCount(被打回的户贡献 0 行)与 countedHouseholdCount(只认活跃 TRAVEL 行),座位汇总口径不会因为有人被驳回而跳变。submitState 与 requirements 同受 kind 筛选影响:传 kind=TRAVEL 时,一个只有接送机被打回的户读成 NEVER_SUBMITTED;要看全貌就不传 kind(不传 = 两类都返)。已知边界(定案、非缺陷):TRANSFER 需求被「不再需要接送」失活(#8435)且没有新版时,既无活跃行也非打回,读成 NEVER_SUBMITTED,与从未提交对催办动作的要求一致,故不另立一态。入参、分页、排序、错误码(589500 / 589507 / 809000 / 401)与其余响应字段均未变化。;前端已交付:逐户表车侧判提交/判打回改读 submitState,「已打回」筛选与徽标覆盖车侧打回户,无活跃行户级文案用后端 submitStateName,rejectedRequirements 单独提示行不当需求行渲染,4 例定向测试全绿(hl-admin e3f3d552)"
updated_at: "2026-09-30"
base: "dev-v3"
---
# 团期用车逐户列表:区分「从未提交」与「已被打回待重提」
> **存放目录**: 二期(order-v3/fleet)→ `changelogs-v2/2026-09/`
## ⚠️ 关键变化
- **`households[]` 新增三个字段**:`submitState`(户级提交态,**恒非 null**)、`submitStateName`(其中文名)、`rejectedRequirements`(该户当前处于打回待重提的类别明细,**恒非 null**,无打回时为空数组)。
- 🔴 **判「这户有没有提交过」一律读 `submitState`,不要读 `status` 是否为 `null`**。用车的打回是「原地置 `REJECTED_*` + `is_active=0`」⇒ 被打回的户**没有任何活跃需求行**,`status` 同样是 `null`,与从未提交的户**完全同形**。照 `status` 催办会去催一个已经交过、只是被驳回的人,而真正该催的「按意见重提」在页面上看不出来。
- **`status` 的取值规则一字未改**(仍是活跃行展示序首条),`statusName` 同理。本次只新增旁路字段,既有映射与读数不受影响。
- **`requirements` 列表内容零变化**:被打回的行仍然**不在**里面(它已失活)。打回信息只在新字段 `rejectedRequirements` 里。
- 🔴 **`rejectedRequirements` 的元素不是需求行,不可当作需求行渲染**:它刻意与 `requirements[]` **不同构**——只带类别 / 打回状态 / 打回意见 / 打回时刻 / 版本号,**没有**车队明细、服务日期、座位数。把它拼进需求表会让同一户出现一条与活跃行自相矛盾的需求。
- 🔴 **`householdCount` 口径再放宽一项,不再恒等于「`needs_vehicle=true` 的户数」**:一个 `needs_vehicle` 为假、又没有活跃行、但有一类被打回的户现在也会进列表(它正是要催重提的人)。判「这户为什么在列表里」看 `submitState`,**不要拿 `needsVehicle` 反推**。
- **另两个数一字不动**:`vehicleRowCount`(被打回的户贡献 0 行)与 `countedHouseholdCount`(只认活跃 TRAVEL 行)。座位汇总口径不会因为「有人被驳回」而跳变。
- **`submitState` 随 `kind` 筛选变化**(与 `requirements` 同一口径):传 `kind=TRAVEL` 时,一个只有接送机被打回的户读成 `NEVER_SUBMITTED`。要看全貌就**不传** `kind`(不传 = 两类都返)。
## 一、背景(选填)
团期「查看需求」Tab 的用车逐户明细是车务与团期管理员的催办页:谁还没报、谁报了在等审、谁被驳回要改。但用车的打回实现是「把那一版原地置成 `REJECTED_*` 并把 `is_active` 置 0」,于是被打回的户在这个只读活跃行的端点里表现为「0 条需求行 + `status` 为 null」——与「从未提交」一模一样。更糟的是:`needs_vehicle` 为假、又没有活跃行的户改前压根不出卡,而被打回的户恰恰可能是这个形状,催办页上会**整户消失**。本次补的就是「这户到底是没交过,还是交过被驳回」这一维,以及「被驳回的是哪一类、意见是什么、什么时候驳的」这几个催办必需值。
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 团期子订单用车需求记录 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households` | 修改 | `households[]` 新增 `submitState` / `submitStateName` / `rejectedRequirements`;`householdCount` 口径放宽含打回户 |
## 三、接口详情
### 1. 团期子订单用车需求记录 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement/vehicle-households`
**VO**: `Long groupBatchId + String kind(query)→ GroupVehicleHouseholdsRespVO`
#### 使用场景
团期「查看需求」Tab 的用车逐户明细(汇总块下面那一块)。用于回答「这个团还差谁的用车需求」:本次起可以把「催首次提交」和「催按意见重提」分成两组,并在被驳回的户上直接展示驳回意见与时刻。
#### 入参
入参本次**零变化**。
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | path | Long | 是 | 雪花 ID;团期不存在返 589500 | 团期 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 | 本列表户数(按 orderId 去重),恒等于 `households` 长度。**口径放宽**:= 应报车户 ∪ 有活跃需求行的户 ∪ **处于打回待重提的户**;**不再恒等于 `needs_vehicle=true` 的户数** |
| vehicleRowCount | Integer | 需求行数 = Σ 各户 `requirements` 长度。未提交与被打回的户贡献 0 行,故**可能小于 `householdCount`**(别当「行数 ≥ 户数」不变量) |
| countedHouseholdCount | Integer | 计入车侧汇总的户数(= 有活跃 TRAVEL 行的户数)。判据一字未改,**不随 `kind` 筛选变化**,也不因有人被驳回而变 |
| households | Array | 逐户明细,按 `orderNo` 升序(`orderNo` 为空的排最后,按 `orderId` 兜底稳定) |
| households[].orderId | String | 子订单 ID |
| households[].orderNo | String | 子订单编号(**非团号**,形如 `HL` + `yyyyMMddHHmmssSSS`) |
| households[].teamNo | String | 子订单团号;未付订金尚未分配时为 `null`(不兜底、不回退成订单号) |
| households[].customerName | String | 主联系人姓名 |
| households[].participantCount | Integer | 出行人数(成人 + 儿童 + 小童 + 婴儿) |
| households[].consultantId | String | 定制师 ID;未指派为 `null` |
| households[].consultantName | String | 定制师姓名;未指派为 `null` |
| households[].countedInSummary | Boolean | 该户是否计入车侧汇总(= 有活跃 TRAVEL 行);**未变** |
| households[].status | String | 户级用车需求状态:**仅由活跃行决定**。`null` = 该户当前没有活跃需求行(**从未提交与已被打回失活两种情况都是 `null`**,要分辨读 `submitState`);非 `null` 时取展示序首条(TRAVEL 优先)的状态。**取值规则一字未改** |
| households[].statusName | String | 户级状态中文名;`status` 为 `null` 时同为 `null`。`PENDING_REVIEW` 按 kind 分两套文案(TRAVEL=待提交车务 / TRANSFER=待审核,#8218);**未变** |
| households[].requirements | Array | 该户的**活跃**用车需求行,0~2 条(TRAVEL / TRANSFER 各至多一条)。被打回的行已失活、**不在本列表内**;该户没有活跃行时为**空数组**(不是 `null`)。**本列表内容零变化** |
| households[].submitState | String | 🆕 户级提交态,**恒非 null**:`NEVER_SUBMITTED` / `SUBMITTED` / `REJECTED_PENDING_RESUBMIT`。`status` 为 `null` 时靠它分辨两种空态;一户两类不同时按展示序首条(TRAVEL 优先)取;**随 `kind` 筛选变化** |
| households[].submitStateName | String | 🆕 户级提交态中文名,与 `submitState` 一一对应:从未提交 / 已提交 / 已被打回待重提。后端下发,前端不自己映射 |
| households[].rejectedRequirements | Array | 🆕 该户**当前**处于打回待重提的类别明细,按展示序(TRAVEL 在前)。**恒非 null**,无打回时为空数组。**不是需求行,不可当作需求行渲染** |
| households[].rejectedRequirements[].kind | String | 需求类别:`TRAVEL` 行程用车 / `TRANSFER` 接送机 |
| households[].rejectedRequirements[].kindName | String | 类别中文名,后端下发 |
| households[].rejectedRequirements[].status | String | 打回状态编码:`REJECTED_TO_CONSULTANT` = 团期管理员打回定制师 / `REJECTED_TO_ADMIN` = 车务退回团期管理员 |
| households[].rejectedRequirements[].statusName | String | 打回状态中文名,与 `requirements[].statusName` 同一套车务文案:已驳回定制师 / 已驳回管理员 |
| households[].rejectedRequirements[].returnRemark | String | 打回意见;历史数据可能为 `null` |
| households[].rejectedRequirements[].returnedAt | String | 打回时刻 `yyyy-MM-dd HH:mm:ss`;历史数据可能为 `null` |
| households[].rejectedRequirements[].version | Integer | 被打回的那一版版本号。**TRAVEL 与 TRANSFER 各自独立递增,不可跨类比大小** |
#### 请求示例
```http
GET /v3/admin/order/group-batch/2104839654727618562/requirement/vehicle-households
Authorization: Bearer {token}
```
按类别筛选(只看行程用车):
```http
GET /v3/admin/order/group-batch/2104839654727618562/requirement/vehicle-households?kind=TRAVEL
Authorization: Bearer {token}
```
#### 响应示例
三户分别落在三个提交态上。`requirements[]` 行内字段与本次改动前完全一致,这里只保留几个便于对读的字段,未列出的行内字段(`fleet` / `serviceDates` / `specialTags` / 座位数等)照旧下发。
```json
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2104839654727618562",
"departDate": "2026-10-06",
"endDate": "2026-10-10",
"householdCount": 3,
"vehicleRowCount": 1,
"countedHouseholdCount": 1,
"households": [
{
"orderId": "2104839654727618570",
"orderNo": "HL20261006103015001",
"teamNo": "T26-3963",
"customerName": "周雅",
"participantCount": 4,
"consultantId": "1901233114509312088",
"consultantName": "苏晴",
"countedInSummary": true,
"status": "PENDING_REVIEW",
"statusName": "待提交车务",
"requirements": [
{
"requirementId": "2104839777884160001",
"kind": "TRAVEL",
"kindName": "行程用车",
"status": "PENDING_REVIEW",
"statusName": "待提交车务",
"headcount": 4,
"returnRemark": null,
"returnedAt": null
}
],
"submitState": "SUBMITTED",
"submitStateName": "已提交",
"rejectedRequirements": []
},
{
"orderId": "2104839654727618571",
"orderNo": "HL20261006103015002",
"teamNo": "T26-3964",
"customerName": "郑文博",
"participantCount": 2,
"consultantId": "1901233114509312088",
"consultantName": "苏晴",
"countedInSummary": false,
"status": null,
"statusName": null,
"requirements": [],
"submitState": "REJECTED_PENDING_RESUBMIT",
"submitStateName": "已被打回待重提",
"rejectedRequirements": [
{
"kind": "TRAVEL",
"kindName": "行程用车",
"status": "REJECTED_TO_CONSULTANT",
"statusName": "已驳回定制师",
"returnRemark": "第三天上午的用车时间与行程冲突,请改后重提",
"returnedAt": "2026-09-29 16:42:11",
"version": 2
}
]
},
{
"orderId": "2104839654727618572",
"orderNo": "HL20261006103015003",
"teamNo": null,
"customerName": "何嘉宁",
"participantCount": 3,
"consultantId": null,
"consultantName": null,
"countedInSummary": false,
"status": null,
"statusName": null,
"requirements": [],
"submitState": "NEVER_SUBMITTED",
"submitStateName": "从未提交",
"rejectedRequirements": []
}
]
},
"success": true
}
```
#### 空数据 / 降级响应
- 团期下没有在团子订单:`households` 为空数组 `[]`,三个计数均为 `0`,`departDate` / `endDate` 照常回显,不报错。
- 某户没有活跃需求行:`requirements` 为**空数组**(不是 `null`),`status` 与 `statusName` 为 `null`,而 `submitState` **仍然有确定取值**(`NEVER_SUBMITTED` 或 `REJECTED_PENDING_RESUBMIT`)——前端不必对 `submitState` 判空。
- 某户没有被打回的类别:`rejectedRequirements` 为**空数组**(不是 `null`)。
- 在团订单 ID 存在但订单行缺失(跨团挂单 / 订单被物理删这类数据异常):该户被跳过并在服务端留痕,整页照常返回;三个计数都按**最终列表**重算,不会出现「表头 42 户、列表里只有 30 户」这种自相矛盾的响应。
- 单次返回上限 **500 户**,超出按 `orderNo` 升序截断并在服务端留痕;截断后三个计数同样按截断后的列表重算。
#### 错误响应
```json
{
"code": 809000,
"message": "用车需求类别非法:BOTH",
"data": null,
"success": false
}
```
- `809000 用车需求类别非法:{0}`:`kind` 传了 `TRAVEL` / `TRANSFER` 之外的非空值(例如 `ALL`、`BOTH`、小写拼错)。要「两类都返」请**不传**该参数或传空串。
- `589500 团期不存在`:`groupBatchId` 查不到。
- `589507 无操作权限(当前角色未授予团期权限,或该团期不在您名下)`:缺团期查看权限,或该团期不在当前账号名下。
- `401`:未登录或令牌失效(网关返 HTTP 200 + 信封 `code: 401`,请按信封 `code` 判定)。
#### 业务边界
- 🔴 **判「有没有提交过」读 `submitState`,不读 `status` 是否为 `null`**。打回 = 原地置 `REJECTED_*` + `is_active=0` ⇒ 打回户与从未提交户的 `status` **都是 `null`**,在 `status` 这一维上不可分辨。`status` 的取值规则本次一字未改。
- 🔴 **`requirements` 列表内容零变化**:被打回的行不在里面,打回信息只在 `rejectedRequirements`。不要为了展示驳回意见去翻 `requirements[].returnRemark`——那一格记的是**该活跃行**历史上被打回过的痕迹(已重提后仍可能有值),不是「当前处于打回态」。
- 🔴 **`rejectedRequirements` 不可当作需求行渲染**:它与 `requirements[]` 刻意不同构,没有车队明细 / 服务日期 / 座位数。需要看需求内容时读该户的活跃行,或走需求版本历史端点。
- 🔴 **`householdCount` 不再恒等于 `needs_vehicle=true` 的户数**:一个 `needs_vehicle` 为假、没有活跃行、但有一类被打回的户也会进列表。判「这户为什么在列表里」看 `submitState`,不要拿 `needsVehicle` 反推。
- **`rejectedRequirements` 只列「当前」处于打回待重提的类别**,不是历史打回记录:打回后已重新提交的类别**不**出现在这里(那一类的现状在 `requirements` 里),否则页面会永远挂着一条早已处理完的驳回。
- **一户两类状态不同时,`submitState` 按展示序首条取**(TRAVEL 优先),与 `status` 同一条规则。例:TRAVEL 有活跃行、TRANSFER 被打回 ⇒ `submitState` 是 `SUBMITTED`,而 `rejectedRequirements` 里有 TRANSFER 那一条。**要逐类判断一律读 `requirements[].status` 与 `rejectedRequirements[].kind`,不要用户级的 `submitState` 推单类。**
- **`submitState` 随 `kind` 筛选变化**:传 `kind=TRAVEL` 时,一个只有接送机被打回的户读成 `NEVER_SUBMITTED`(该类别不在筛选范围内)。要看全貌不传 `kind`。
- **已知边界(定案,非缺陷)**:TRANSFER 需求被「不再需要接送」失活(#8435)且之后没有新版本时,该户既无活跃行也不处于打回态,会读成 `NEVER_SUBMITTED`。它与「从未提交」对催办动作的要求一致(要么提,要么整团免车),故不另立一态。
- **`version` 不可跨类比较**:TRAVEL 的 v3 与 TRANSFER 的 v3 之间没有先后关系,两类版本号各自独立递增。
- **`vehicleRowCount` 可能小于 `householdCount`**:未提交与被打回的户贡献 0 行。不要再把「行数 ≥ 户数」当不变量写断言。
- **`countedHouseholdCount` 不随 `kind` 筛选变化**(#8559),也不因驳回动作变化——座位汇总口径必须稳定。
- **Swagger 上该端点的 `notes` 仍按本次改动前的口径写着「被打回的需求行已失活,不在本列表内」**:这句对**需求行**依然成立(打回行确实不进 `requirements`),但对**户**不再成立——打回户现在会出现在 `households` 里。字段级语义以本交接件与各字段的 `@ApiModelProperty` 为准。
## 四、契约约束与正确调用方式(接口类必写)
1. **两个新字段恒非 null,直接读不用判空**:`submitState` 与 `rejectedRequirements` 对每一户都有确定取值(后者无打回时是空数组)。需要判空的仍是 `status` / `statusName` / `teamNo` / `consultantId` / `consultantName` 这些既有字段。
2. **催办分组按 `submitState` 做**:`NEVER_SUBMITTED` → 催首次提交;`REJECTED_PENDING_RESUBMIT` → 催按意见重提(意见与时刻在 `rejectedRequirements` 里);`SUBMITTED` → 具体到哪一步看 `status` / `requirements[].status`。
3. **不要用 `status == null` 当「未提交」的判据**:这是本次要解决的那个缺陷本身。前端若已有这段逻辑,请改为 `submitState === 'NEVER_SUBMITTED'`。
4. **不要把 `rejectedRequirements` 并进需求行列表**:两者结构刻意不同构。驳回信息建议单独渲染成一条提示条(类别 + 状态中文名 + 意见 + 时刻),与需求行区分开。
5. **不要拿 `needsVehicle` 反推「这户为什么在列表里」**:列表的并集口径已变,判据是 `submitState`。
6. **中文名一律用后端下发的**:`submitStateName` / `kindName` / `statusName` 都由后端给出,前端不要再本地维护映射表(`PENDING_REVIEW` 的文案还会按 kind 分叉成两种,本地表必然对不上,#8218)。
7. **要看全貌不传 `kind`**:不传 = 两类都返。传了 `kind` 则 `requirements`、`rejectedRequirements`、`submitState`、`householdCount`、`vehicleRowCount` 全部随之收窄(只有 `countedHouseholdCount` 三种筛选读数相同)。
8. **`kind` 只接受 `TRAVEL` / `TRANSFER`**:想表达「全部」请**不传**,传 `ALL` / `BOTH` 会返 809000。
9. **错误信封按 `code` 判**:业务失败与入参校验一律 HTTP 200 + 信封 `code`;测试环境网关对失效令牌也返回 HTTP 200 + `code: 401`。
## 五、数据库行为
本端点为只读查询,本次改动**不涉及任何 DDL 与 DML**:没有新增表、没有新增列、没有 Flyway 脚本、没有写入。
- `submitState` **不是数据库列**,不落表、不参与任何 SQL 过滤或分组,纯粹是响应字段——所以既有的按 `status` 筛选 / 统计的扫描路径不会把它重新捡起来,也不会误计。
- 打回明细取自用车需求的**版本历史行**(打回行已 `is_active=0`)。取数用**一次按子订单 ID 批量**的查询(与既有的活跃行查询同一形状,`requirement_kind` 的 `IN` 列表从 1 个值放宽到 2 个值),**不是逐户 N+1**;整页固定若干次查询,与户数无关。
- 判「某类是否处于打回待重提」复用的是定制师提交侧闸门的同一套算法(取最高版本组、看最后一次动作是否为打回),不新造判定规则。
## 六、边界行为
| 场景 | `status` | `requirements` | `submitState` | `rejectedRequirements` | 是否出现在列表 |
|------|----------|----------------|---------------|------------------------|----------------|
| 有活跃行(TRAVEL 或 TRANSFER) | 首条行的状态 | 1~2 条 | `SUBMITTED` | `[]` | 是 |
| 从未提交过任何版本 | `null` | `[]` | `NEVER_SUBMITTED` | `[]` | 是(`needs_vehicle` 为真即出卡) |
| 被打回、尚未重提 | `null` | `[]` | `REJECTED_PENDING_RESUBMIT` | 1~2 条 | 是(**本次新增的入列路径**) |
| 被打回后已重新提交 | 新行的状态 | 1~2 条 | `SUBMITTED` | `[]`(不挂已处理完的驳回) | 是 |
| TRAVEL 有活跃行 + TRANSFER 被打回 | TRAVEL 行的状态 | 1 条(TRAVEL) | `SUBMITTED`(按展示序首条) | 1 条(TRANSFER) | 是 |
| TRAVEL 被打回 + TRANSFER 有活跃行 | TRANSFER 行的状态 | 1 条(TRANSFER) | `REJECTED_PENDING_RESUBMIT`(TRAVEL 展示序在前) | 1 条(TRAVEL) | 是 |
| 只有 TRANSFER 被打回,且传了 `kind=TRAVEL` | `null` | `[]` | `NEVER_SUBMITTED`(该类别不在筛选内) | `[]` | 取决于 `needs_vehicle` |
| TRANSFER 被「不再需要接送」失活且无新版(#8435) | `null` | `[]` | `NEVER_SUBMITTED`(定案) | `[]` | 是 |
| 在团订单行缺失(数据异常) | — | — | — | — | 跳过该户并留痕,整页照常返回 |
| 户数超过 500 | — | — | — | — | 按 `orderNo` 升序截断,计数按截断后重算 |
## 六.5、枚举 / 数据字典
**户级提交态**(`submitState` → `submitStateName`,新增枚举,**恒非 null**)
| 码 | 中文名 | 语义 | 催办动作 |
|----|--------|------|----------|
| NEVER_SUBMITTED | 从未提交 | 在本次筛选的类别范围内既没有活跃需求行、也不处于打回态 | 催首次提交 |
| SUBMITTED | 已提交 | 至少一类存在活跃需求行(具体到哪一步看 `status`) | 按 `status` 跟进 |
| REJECTED_PENDING_RESUBMIT | 已被打回待重提 | 没有活跃行,但最高版本组的最后一次动作是打回 | 催「按意见改完再提」 |
> 展示名刻意与团期子订单列表那块的「未提交」用不同的词:那块只判「有没有活跃行」,被打回的户在那里也显示「未提交」;两处若同字,本次分开的这一步在页面上就白做了。
**打回状态**(`rejectedRequirements[].status` → `statusName`)
| 码 | 中文名 | 谁打回的 |
|----|--------|----------|
| REJECTED_TO_CONSULTANT | 已驳回定制师 | 团期管理员打回定制师 |
| REJECTED_TO_ADMIN | 已驳回管理员 | 车务退回团期管理员 |
**需求类别**(`kind` → `kindName`,未变)
| 码 | 中文名 |
|----|--------|
| TRAVEL | 行程用车 |
| TRANSFER | 接送机 |
展示序固定 TRAVEL 在前、TRANSFER 在后;`requirements` 与 `rejectedRequirements` 共用这个序。
**活跃需求行状态**(`requirements[].status`,未变):`PENDING_REVIEW` / `PENDING` / `PROCESSING` / `DONE`。`REJECTED_*` 不会出现在活跃行上。`PENDING_REVIEW` 的中文名按 kind 分叉:TRAVEL = 待提交车务、TRANSFER = 待审核(#8218)。
## 六.6、修改前后对比
| 字段 / 口径 | 改动前 | 改动后 |
|-------------|--------|--------|
| `households[].submitState` | 不存在 | 🆕 恒非 null 的三态字段,是 `status` 为 `null` 时唯一能分辨「从未提交 / 已被打回」的字段 |
| `households[].submitStateName` | 不存在 | 🆕 三态中文名,后端下发 |
| `households[].rejectedRequirements` | 不存在(打回信息在本端点完全取不到) | 🆕 恒非 null 的数组,列当前处于打回待重提的类别 + 意见 + 时刻 + 版本号 |
| `households[].status` / `statusName` | 只由活跃行决定 | **取值规则一字未改**(仍只由活跃行决定)。变的只是文档:不能再拿它判「有没有提交过」 |
| `households[].requirements` | 只含活跃行,打回行不在其中 | **内容零变化** |
| `householdCount` | = 应报车户 ∪ 有活跃需求行的户;恒等于 `needs_vehicle=true` 的户数(`needs_vehicle` 创单恒真) | 并集多一项「处于打回待重提的户」⇒ **不再恒等于 `needs_vehicle=true` 的户数**;`needs_vehicle` 为假但有一类被打回的户会进来 |
| `vehicleRowCount` | Σ 各户活跃行数 | **口径未变**(打回户贡献 0 行);与 `householdCount` 的差额多了「打回户」这一类 |
| `countedHouseholdCount` | 有活跃 TRAVEL 行的户数 | **一字未变**,不因驳回动作跳变 |
| 被打回户是否出现在列表 | `needs_vehicle` 为假时**不出卡**(催办页上整户消失) | 出卡,`submitState` = `REJECTED_PENDING_RESUBMIT` |
| 入参 / 分页 / 排序 / 错误码 | — | 全部未变 |
## 六.7、影响评估
- **前端必须改的**:如果页面上有「`status == null` ⇒ 显示未提交」这段逻辑,**必须**改成读 `submitState`——不改的话打回户会继续被标成「未提交」,本次改动在页面上等于没做。
- **前端应当改的**:催办清单按 `submitState` 分两组;被驳回的户上渲染 `rejectedRequirements` 里的类别 + 状态中文名 + 意见 + 时刻。
- **前端不要做的**:把 `rejectedRequirements` 拼进需求行表格(会出现与活跃行矛盾的一条);拿 `needsVehicle` 反推入列原因;跨类比较 `version`。
- **可能被读错的一处**:`requirements[].returnRemark` / `returnedAt` 记的是**该活跃行**历史上被打回过的痕迹,已重提后仍可能有值;「当前处于打回态」只看 `rejectedRequirements` 是否非空。
- **列表条数会变多**:`needs_vehicle` 为假、无活跃行、但有一类被打回的户从本次起入列。如果前端有基于户数的断言或埋点基线,会看到这一类团期的户数上升——这是预期,不是数据错误。
- **兼容性**:JSON 新增字段对已有前端反序列化无影响。既有字段一个没删、没改名、没改类型。
- **无副作用面**:只读端点,不涉及写入、事务、消息、权限判定变化;座位与汇总口径不变。
## 七、不影响范围
- **入参**:`groupBatchId`、`kind` 的取值域、缺省语义(不传 = 两类)、校验规则全部未变。
- **排序与上限**:`orderNo` 升序 + `orderId` 兜底、单次 500 户上限未变。
- **既有响应字段**:`groupBatchId` / `departDate` / `endDate` / `vehicleRowCount` / `countedHouseholdCount` / `households[]` 的既有字段(含 `status` / `statusName` / `requirements` 及行内所有字段)名称、类型、取值域、语义全部未变。
- **错误码**:未新增、未删除、未改文案(589500 / 589507 / 809000 / 401)。
- **权限码**:仍是团期查看权限,未收紧未放宽。
- **用房侧** `hotel-households` 端点:本次一行未改。
- **写路径**:逐户提交车务、打回、整体确认需求等写接口本次一行未改。
- **团级汇总** `requirement-summary`:口径与读数未变(`countedHouseholdCount` 是它的对账口,本次刻意保持不动)。
- **网关路由**:既有路由,本次无新增。
- **数据库**:无 DDL、无 DML、无 Flyway 脚本。
- **小程序端**:零影响。
## 八、测试环境已验证
本次改动的核心可验证面是「打回户与从未提交户在 `status` 上同形、在 `submitState` 上可分辨」,以及「既有三个计数与 `requirements` 内容不受影响」。已由下列自动化用例覆盖(`hl-order-service-v3`):
| 覆盖点 | 用例 |
|--------|------|
| 打回户与从未提交户 `status` 相同(都是 `null`)、`submitState` 不同 | `GroupBatchVehicleHousehold8562Test#households_rejectedAndNeverSubmitted_sameStatusDifferentSubmitState` |
| 打回户带出打回意见与打回状态中文名 | `#households_rejectedHousehold_carriesRemarkAndStatusName` |
| 存在打回时 `requirements` 仍然只含活跃行(内容零变化) | `#households_rejectionPresent_requirementsStillOnlyActiveRows` |
| TRAVEL 有活跃行 + TRANSFER 被打回 ⇒ `submitState` 按展示序首条(TRAVEL)取 | `#households_travelActiveTransferRejected_submitStateFollowsTravel` |
| TRAVEL 被打回 + TRANSFER 有活跃行 ⇒ `submitState` 同样按 TRAVEL 取 | `#households_travelRejectedTransferActive_submitStateFollowsTravel` |
| 传 `kind=TRANSFER` 时 TRAVEL 的打回被筛掉 | `#households_kindTransfer_travelRejectionFilteredOut` |
| `needs_vehicle` 为假但有一类被打回的户仍然入列 | `#households_rejectedButNeedsVehicleFalse_stillListed` |
| 完全没有打回时 `submitState` 为 `SUBMITTED`、`rejectedRequirements` 为空数组 | `#households_noRejectionAtAll_submitStateSubmitted` |
| 打回明细按子订单 ID 批量取数(固定次数,不随户数增长) | `RequirementServiceVehicleRejectionBatchTest` |
## 九、相关历史 PR
- PR #8623(本次):`feat(order-v3): 团期逐户用车区分「从未提交」与「已被打回待重提」(#8562)`。
- #8195:`householdCount` 改为「应报车户数」并开始包含未提交户,`status` / `statusName` 两个户级字段在那一单新增。
- #8559:`countedHouseholdCount` 不随 `kind` 筛选变化。
- #8577:只提交接送机的户不再被判「未提交用车需求」。
- #8601:逐户提交车务与打回的 `kind` 参数取消默认值。
- #8218:`PENDING_REVIEW` 的中文名按 kind 分叉(TRAVEL 待提交车务 / TRANSFER 待审核)。
- #8435:「不再需要接送」失活 TRANSFER 需求(本单已知边界的来源)。
## 十、相关文档
- `docs/CODE_RULES.md` §3:VO 命名、`@ApiModelProperty` 约定、禁 Entity 跨层(打回明细用独立 DTO 而非直接传需求行的依据)。
- `docs/CODE_RULES.md` §15.7:字典字面量单源——`submitStateName` / `kindName` / `statusName` 由后端下发的依据。
- Swagger:`hl-order-service-v3` → `团期需求` 分组。字段级语义以各字段 `@ApiModelProperty` 为准;该端点 `notes` 的那句「被打回的需求行不在本列表内」只对需求行成立、对户不成立(见「业务边界」最后一条)。
## 关联 / 联系人
### 链接
- 工单 #8562
- PR #8623
### 联系人
- 后端:wx
- 前端:mmg(管理后台 hl-ui)