diff --git a/changelogs-v2/2026-09/22_8195_团期查看需求页五处缺口-修改接口-管理后台.md b/changelogs-v2/2026-09/22_8195_团期查看需求页五处缺口-修改接口-管理后台.md new file mode 100644 index 00000000..151fb513 --- /dev/null +++ b/changelogs-v2/2026-09/22_8195_团期查看需求页五处缺口-修改接口-管理后台.md @@ -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` + +#### 使用场景 + +「查看需求」页「用车」板块,团期管理员勾选若干户的接送机需求,一次性确认并转交车务。等价于逐户调 `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` + +#### 使用场景 + +「查看需求」页「用车」板块下半块的逐户列表。本次把它从「已提交需求的户的列表」改成「应报车的户的列表」——运营需要看见「谁还没交」,而改前那些户根本不出现在响应里,页面上无从催办。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `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` + +#### 使用场景 + +「查看需求」页「用房」板块下半块的逐户列表,与 `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` + +#### 使用场景 + +团期管理员新增 / 编辑整团的正式行程用车需求,一次全量替换整份(主表 + 全部分组 + 全部逐日行)。本次给分组里的车型加了字典校验:改前前端传什么就落什么,运营填错的车型要等到车队派车时才暴露;改后在保存这一步就拒。 + +#### 入参 + +结构不变,仅新增一条约束: + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `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`)