--- schema: "hl-changelog/v2" ticket: "8311" title: "团期正式用车需求分组座位数改由车队字典档位下拉选择(可选可改),新增 809124 档位校验与汇总座位兜底诊断" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "debb0bc2e22be878243ea8ffe89dd0b9091656a1" target_release: "v2.1" verified_at: "2026-09-24" status_note: "PR #8324 squash 合并 dev-v3(1638faf08)。部署:hl-order-service-v3 dev-v3 @ 1638faf08,2026-09-24 12:06:26 滚动部署两实例,deploy-status 读数 BEHIND=0/N STATE=ok;请求路径上 hl-gateway 与 hl-fleet-service 同为 ok、BEHIND 右侧 N。测试服网关真实 PUT 实测(groupBatchId=2102692937584513025,BUS 16座/SUV 5座 DRAFT):BUS 13 座 → code=809124「第 BUS 组的座位数 13 不在车型 bus 的可选档位 [12, 15, 16, 19] 内,请从下拉项中选择」;SUV 6 座 → code=809124「…不在车型 suv 的可选档位 [5, 7] 内…」;两次拒绝后回读 version 与 seats 均未变(零写入);原值(BUS 16/SUV 5)→ code=200 且落库一致。aggregate-draft 实测回 seatOptionAdjusted=[](当前测试团期没有非法档位子订单)与 draft BUS=16/SUV=5。定向测试 14 个类全绿(SaveTest 31 / DraftAggregatorTest 24 / AggregateDraftTest 12 / ConfirmCheckTest 19 等)。809123 已被同日并入的 #8219 占用,本单让号到 809124。 | 2026-09-24 mmg 交付:编辑弹窗座位数手输改档位下拉(选项取字典 seatOptions 按 typeKey,存量非法值合成占位回显 rule 前置拦,档位不可得不拦),切车型联动清空座位,既有组/汇总灌入组号只读(手增空白组仍可填),汇总诊断四数组改五数组 seatOptionAdjusted 按三种 reason 分文案带 [groupCode] 定位;809124 无按码分支拦截器透报文(带组号+档位列表)即引导;spec 44 例全绿(3 新例+既有下标适配)" updated_at: "2026-09-24" base: "dev-v3" --- # order-v3 团期需求: 分组座位数改为字典档位可选 + 809124 档位校验 > **存放目录**: 二期(v3) → `changelogs-v2/2026-09/` > > **服务**: hl-order-service-v3(团期需求域,groupbatch 包) > **PR**: 待建 > **Issue**: #8311 > **日期**: 2026-09-24 > **影响范围**: 管理后台「团期详情 → 查看需求」Tab 的正式用车需求编辑弹窗(含「自动汇总」) --- ## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写) - **新增错误码 809124**:`PUT .../vehicle-requirement` 现在会校验「该组的 `seats` 必须落在该组 `vehicleType` 在车队车型字典里的可选座位档位(seatOptions)内」,不在档位内整份提交被拒。 - **本条与子订单级早已存在的同款校验对齐**:子订单用车需求的 `fleet[].seats` 一直有这条(错误码 **582024**「座位数不在该车型大类可选座位数中,请检查车型库」);团级此前只校验「半填 809118」与「容量不足 809116」,收任意 ≥1 的座位数。本单把团级补齐到同一口径。 - **前端交互随之改变**:座位数不再是自由输入框,改为下拉,选项取自 `GET /admin/fleet/vehicle-types/list` 里该 `typeKey` 的 `seatOptions`。**这条校验是硬约束,不是提示**——手输/绕过下拉提交字典外档位会被 809124 拒绝。 - **`aggregate-draft` 的 `draft.groups[].seats` 语义变更**:#8220 时它取「组内子订单报的最大单车座位」;现在取**该车型大类档位内的值**(子订单报的值在档位内就取组内最大,不在档位内或没报则兜底到该大类**最大档**),被兜底调整的户逐条列在新字段 `seatOptionAdjusted`。**前端必须展示该字段**,否则「子订单报 13 座、草稿变成 19 座」这件事在页面上不可见。 - **组号(`groupCode`)仍由后端生成**(车型大写 + 同车型拆组序号:`BUS`、`BUS2`…),表单不应提供输入框;同车型被拆成多组时,`seatOptionAdjusted[]` 里带 `groupCode` 用于定位是哪一组。 --- ## 一、背景(选填) wx 2026-09-24 定案:正式用车需求编辑弹窗里「单车座位数」应由手输改为**按车队字典档位下拉选择(可选、可改)**。落地时发现团级缺少子订单级早就有的「座位数必须在 seatOptions 内」校验:不补这条,下拉只是页面上的自觉,直接调接口仍能把车队没有的档位(如大巴 13 座)写进正式需求,车务排车时对不上型号。本单同时把「自动汇总」的座位默认值改为落在档位内,并对被调整的户出诊断,避免静默改数。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 保存团期正式用车需求 | PUT | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 新增校验 + 新错误码 | 新增 809124(座位数不在该组车型大类可选档位内);档位不可得复用 809120 | | 2 | 自动汇总正式用车需求草稿 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` | 响应新增字段 + 既有字段语义变更 | 新增 `seatOptionAdjusted[]`;`draft.groups[].seats` 改为档位内的值 | | 3 | 车型大类全量列表(复用,不改) | — | — | 复用不改 | 座位下拉数据源(`seatOptions`)仍走 `GET /admin/fleet/vehicle-types/list`,本单未改它;契约见第八节说明 | --- ## 三、接口详情 ### 1. 保存团期正式用车需求 `PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` **VO**: `GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO` #### 使用场景 团期详情「查看需求 → 团级正式用车需求」编辑弹窗点保存。语义仍是**整份全量替换**:未出现在本次提交里的分组会被移出当前版本。请求体与响应体结构均不变,本次只新增一条校验与一个错误码。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | - | 团期聚合主键 | | version | Body | Integer | ❌ | 首次保存传 null,其后回传上次拿到的值 | 乐观锁;不一致抛 809102 | | remark | Body | String | ❌ | ≤500 | 整份需求备注 | | groups | Body | Array | ✅ | 可以是空数组 | 全部乘车分组;有在团需车户却零分组抛 809103 | | groups[].groupId | Body | Long | ❌ | 新增分组传 null | 带上它即声明「这就是库里那一组」,此时 groupCode 不得变更(否则 809104) | | groups[].groupCode | Body | String | ✅ | ≤32,同一份内不得重复 | 直接作为车费 alloc_group;**表单不应给输入框,用后端生成的只读值** | | groups[].vehicleType | Body | String | ✅ | ≤64,取值见 `/admin/fleet/vehicle-types/list` | 车型大类;不在字典内抛 809119 | | groups[].serviceStartDate | Body | LocalDate | ✅ | `yyyy-MM-dd` | 本组服务开始日 | | groups[].serviceEndDate | Body | LocalDate | ✅ | 不早于开始日 | 本组服务结束日 | | groups[].seats | Body | Integer | ❌ | `@Min(1)`;与 count 同填或同空;**本次起必须落在该车型大类的 seatOptions 内** | 单车座位数(含驾驶位);下拉取值见 `GET /admin/fleet/vehicle-types/list` 的 `seatOptions`;存量分组(seats 与 count 皆 null)整条跳过本校验 | | groups[].count | Body | Integer | ❌ | `@Min(1)`;与 seats 同填或同空 | 车辆数量;只填一半抛 809118 | | groups[].specialTags | Body | Array<String> | ❌ | 取值须在字典 `vehicle_special_demand` 内 | 特殊诉求标签编码数组;字典外编码整份拒绝(809117) | | groups[].remark | Body | String | ❌ | ≤500 | 该组备注 / 其他诉求 | | groups[].days | Body | Array | ✅ | 非空,且正好铺满本组服务日范围 | 逐日用车人数与成员 | | groups[].days[].tripDate | Body | LocalDate | ✅ | 落在本组服务日范围内、不重复、不缺日 | 越界或重复抛 809105,缺日抛 809106 | | groups[].days[].headcount | Body | Integer | ✅ | `@Min(1)`,且 ≥ 当日成员户数 | 该组该日乘车人数 | | groups[].days[].memberOrderIds | Body | Array<Long> | ✅ | 非空,须全属本团在团户 | 该组该日实际乘车的子订单集合 | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | requirementId | String | 正式需求主键(Long 序列化为字符串) | | groupBatchId | String | 团期聚合主键 | | status | String | DRAFT / CONFIRMED / DISPATCHED / DONE / PENDING_RECONFIRM / CANCELLED;PUT 后为 DRAFT | | version | Integer | 版本号,下次提交须回传 | | remark | String | 整份备注 | | confirmedBy / confirmedAt | String / LocalDateTime | 整份确认人/时间;DRAFT 时为 null | | groups | Array | 全部乘车分组;整团免车态为空数组 | | groups[].groupCode | String | 分组键 = 车费 alloc_group(后端生成,前端只读回显) | | groups[].vehicleType / vehicleTypeName | String | 车型大类 key / 中文名 | | groups[].seats | Integer | 单车座位数(含驾驶位);存量分组为 null | | groups[].count | Integer | 车辆数量;存量分组为 null | | groups[].totalSeatCount | Integer | 总座位数 = seats × count;缺一即 null | | groups[].maxHeadcount | Integer | 该组 days 里的最大用车人数 | | groups[].remainingPassengerSeats | Integer | 余座 = 扣司机座后的可乘座位 − maxHeadcount;**可为负**,是缺口展示值,**不得据此拦截提交**(见 22_8152) | | groups[].days[] | Array | 逐日行程与成员(tripDate / headcount / memberOrderIds / memberOrderCount) | #### 请求示例 ```json { "version": 3, "remark": "9/24 换 19 座", "groups": [ { "groupId": null, "groupCode": "BUS", "vehicleType": "bus", "serviceStartDate": "2026-10-08", "serviceEndDate": "2026-10-10", "seats": 19, "count": 1, "specialTags": [], "remark": "含高速费", "days": [ { "tripDate": "2026-10-08", "headcount": 9, "memberOrderIds": ["2102692937378992129"] }, { "tripDate": "2026-10-09", "headcount": 9, "memberOrderIds": ["2102692937378992129"] }, { "tripDate": "2026-10-10", "headcount": 9, "memberOrderIds": ["2102692937378992129"] } ] } ] } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "requirementId": "2102750063359135746", "groupBatchId": "2102749115823919105", "status": "DRAFT", "version": 4, "remark": "9/24 换 19 座", "confirmedBy": null, "confirmedAt": null, "groups": [ { "groupId": "2102750063363330049", "groupCode": "BUS", "vehicleType": "bus", "vehicleTypeName": "大巴系列", "serviceStartDate": "2026-10-08", "serviceEndDate": "2026-10-10", "seats": 19, "count": 1, "specialTags": [], "remark": "含高速费", "totalSeatCount": 19, "maxHeadcount": 9, "remainingPassengerSeats": 9, "days": [ { "tripDate": "2026-10-08", "headcount": 9, "memberOrderIds": ["2102692937378992129"], "memberOrderCount": 1 } ] } ] }, "success": true } ``` #### 空数据 / 降级响应 - 整团没有在团需车户时 `groups: []` 是合法提交,响应 `groups` 为空数组。 - 车队车型字典(含座位档位)取不到时,**保存口 fail-closed**:抛 809120「车队车型字典暂不可用,无法校验车型,请稍后重试」,本次提交零写入。 - `vehicleTypeName` 仍可降级为 null(只影响展示),不影响本单校验。 ```json { "code": 200, "message": "成功", "data": { "groups": [] }, "success": true } ``` #### 错误响应 ```json { "code": 809124, "message": "第 BUS 组的座位数 13 不在车型 bus 的可选档位 [12, 15, 16, 19] 内,请从下拉项中选择", "success": false, "data": null } ``` #### 业务边界 - **报文字段**:{0}=组号(`groupCode`,不是序号)、{1}=本次提交的座位数、{2}=车型大类 key、{3}=该大类的可选档位(升序、方括号包裹)。一次只抛一条,按校验遍历顺序收集。 - **校验顺序**:座位档位(809124 / 809120)→ 半填(809118)→ 容量不足(809116)。**档位不可得(报 809120)时不会吞掉同组的半填与容量判据**,仍会继续判下去,避免「先提示稍后重试、补完车辆数才被告知半填」的来回。 - **存量分组(seats 与 count 皆 null)整条跳过**:不查字典、不报 809124/809120/809116。这是为了让「不带 fleet 结构再 PUT 一次」的历史团期不会被一刀拦死。 - **只填了车辆数(seats 为 null)时不判档位**:交给 809118 报半填,不会编出一条「座位数不在档位内」。 - **该车型大类在车队没有任何型号**(`seatOptions` 为空)按「档位不可得」处理 → 809120,需先去车务补型号。 - **注意与子订单级错误码不同**:子订单级同一件事是 582024,团级是 809124;两条链路的报文与码值刻意分开(团级报文必须带组号,一份十几组的整份提交被拒后要能定位是哪一组)。 --- ### 2. 自动汇总正式用车需求草稿 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft` **VO**: `GroupVehicleAggregateDraftRespVO` #### 使用场景 编辑弹窗点「自动汇总」时调用,拿到 `draft` 灌进表单、原样或编辑后 PUT 保存;四个(本次起为五个)诊断字段必须一并展示。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | ✅ | - | 团期聚合主键(不是产品班期 ID) | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | groupBatchId | String | 团期聚合主键 | | currentStatus | String | 当前正式需求状态;未形成时为 null。只有 null 或 DRAFT 能保存 | | draft | Object | 与保存请求体同形,可原样 PUT;`groupId` 恒为 null | | draft.groups[].groupCode | String | 后端生成的组号(车型大写 + 拆组序号:BUS、BUS2…),**前端只读展示,不要给输入框** | | draft.groups[].vehicleType | String | 归一后的车型大类 key(suv2 → suv) | | draft.groups[].seats | Integer | **本次起为档位内的值**:子订单报的座位在档位内取组内最大,不在档位内或没报则兜底该大类最大档;档位不可得时退回旧规则(取子订单报的最大值) | | draft.groups[].count | Integer | `ceil(本组最忙那天的人数 / (seats − 1))`,已扣司机座;seats < 2 时与 seats 一起为 null | | draft.groups[].days[] | Array | 逐日明细,正好铺满本组首日到末日(headcount 为该日实时人数之和) | | droppedFleetItems[] | Array | 没进草稿的车型项(多车型户只进主车型);**必须提示** | | staleHeadcountOrders[] | Array | 实时人数 ≠ 子订单冻结人数的户 | | paddedOrderDays[] | Array | 为过 809109 补进分组的日期 | | **seatOptionAdjusted[]** | Array | **本次新增**:座位档被兜底调整的户,见下 | | seatOptionAdjusted[].groupCode | String | 该户所在的草稿分组编码(同户同车型被拆成 BUS/BUS2 时用它区分) | | seatOptionAdjusted[].orderId / orderNo | String / String | 所属子订单(雪花 ID 按字符串处理) | | seatOptionAdjusted[].vehicleType | String | 车型大类(草稿里该组的 vehicleType) | | seatOptionAdjusted[].originalSeats | Integer / null | 子订单报的单车座位数;没报时为 null | | seatOptionAdjusted[].adoptedSeats | Integer | 草稿实际采用的座位数(该组档位内的值) | | seatOptionAdjusted[].seatOptions | Integer[] | 该大类的可选档位(升序) | | seatOptionAdjusted[].reason | String | `SEATS_NOT_IN_OPTIONS` / `SEATS_MISSING` / `SEATS_MISSING_ADOPTED_GROUP_VALUE` | | violations[] | Array | 草稿预检违规(与保存同一份校验内核):code / reason / detail / groupCode / tripDate / orderId | #### 请求示例 ```http GET /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement/aggregate-draft Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "groupBatchId": "2102749115823919105", "currentStatus": null, "draft": { "version": null, "remark": null, "groups": [ { "groupId": null, "groupCode": "BUS", "vehicleType": "bus", "serviceStartDate": "2026-10-08", "serviceEndDate": "2026-10-10", "seats": 19, "count": 1, "specialTags": [], "remark": "HL20260924001: 备注原文", "days": [ { "tripDate": "2026-10-08", "headcount": 9, "memberOrderIds": ["2102692937378992129"] } ] } ] }, "droppedFleetItems": [], "staleHeadcountOrders": [], "paddedOrderDays": [], "seatOptionAdjusted": [ { "groupCode": "BUS", "orderId": "2102692937378992129", "orderNo": "HL20260924001", "vehicleType": "bus", "originalSeats": 13, "adoptedSeats": 19, "seatOptions": [12, 15, 16, 19], "reason": "SEATS_NOT_IN_OPTIONS" } ], "violations": [] }, "success": true } ``` #### 空数据 / 降级响应 - 草稿没有可汇总内容时 `draft.groups` 为空数组,`seatOptionAdjusted` 为空数组,接口仍 200。 - 有需车户还没提交行程用车需求时报 809121 并逐户列出(不是空草稿)。 - **某车型大类在车队没有型号时,汇总口不 fail-closed**:退回旧规则(取子订单报的座位)、不出 `seatOptionAdjusted`,同时在 `violations` 里给出 809120,让页面能提示「这类车型还没在车务建档」。整张字典不可用(Feign 降级)仍直接抛 809120。**保存口对同一情形是 fail-closed**——这条差异是有意的:汇总是只读草稿,卡住它等于管理员连看都看不到。 ```json { "code": 200, "message": "成功", "data": { "draft": { "groups": [] }, "seatOptionAdjusted": [], "violations": [] }, "success": true } ``` #### 错误响应 ```json { "code": 809121, "message": "团期 2102749115823919105 有 2 户缺少可汇总的行程用车需求,暂不能自动汇总:HL20260924001(2102692937378992129):未提交行程用车需求、HL20260924002(2102692937378992130):车型均不在车型字典内", "success": false, "data": null } ``` #### 业务边界 - `reason` 的三种取值对前端提示语不同:`SEATS_NOT_IN_OPTIONS` = 「你报的 N 座这台车没有,已改用 M 座」;`SEATS_MISSING` = 「这户没报座位,跟字典最大档 M 座走」;`SEATS_MISSING_ADOPTED_GROUP_VALUE` = 「这户没报座位,采用的值 M 来自同组其它户报的合法档位(不是字典最大档)」。**不要把后两者渲染成「系统按字典给你选了 M 座」**。 - 座位是**单车**属性,多户报的值不相加(沿用 #8220)。 - 拆组逻辑不变:同车型按连续日期段拆组,第二段起组号加序号;诊断行按所属组带 `groupCode`。 - `draft.groups[].seats` 即使已被兜底到档位内,**保存时仍会按新校验复核**(同一条 809124);本端点不替代保存侧校验。 --- ## 四、契约约束与正确调用方式(接口类必写) - 座位下拉的数据源是 `GET /admin/fleet/vehicle-types/list` 返回项里的 `seatOptions`(该大类下型号 `seats` 去重升序),**按 `typeKey` 取**(SUV 这类在库里的 typeKey 可能是 `suv2`,返回的 `vehicleType` 是归一后的 `suv`,见 23_8221)。**不要在前端写死档位枚举**。 - 切换车型时必须同步换档位并清空旧值,否则旧档位会带着新车型一起提交 → 809124。 ### ✅ 正确 / ❌ 错误 payload 对照(分组元素) ```json // ✅ 大巴档位 12/15/16/19 中取值,seats 与 count 成对 { "groupCode": "BUS", "vehicleType": "bus", "seats": 19, "count": 1 } // ❌ 13 不在大巴档位内 → 809124 { "groupCode": "BUS", "vehicleType": "bus", "seats": 13, "count": 1 } // ❌ 只填座位数、车辆数漏填 → 809118(不是 809124) { "groupCode": "BUS", "vehicleType": "bus", "seats": 19 } // ✅ 存量分组形态:两列都不传,整条跳过档位与容量校验 { "groupCode": "BUS", "vehicleType": "bus" } ``` ### 切换状态时的必要动作 - 保存成功后响应 `version` 会 +1,下次保存必须回传新值,否则 809102。 - 「自动汇总」会整份覆盖当前编辑内容,前端须二次确认后再灌入。 --- ## 五、数据库行为(涉及写操作时必写) - 无表结构变更、无 Flyway。 - 809124 / 809120 都在写库之前抛出:**整份零写入**(校验失败时响应 `data` 为 null,可回读同一份数据确认未变)。 --- ## 六、边界行为 - 存量分组不批量重算、不迁移:只有它下次出现在某次 PUT 的 `groups` 数组里才会按新校验复核(两列皆 null 时仍整条跳过)。 - 同一份提交里多个组同车型时,字典只查一次(不是每组一次 Feign)。 - 车队车型字典服务超时/降级:保存口 fail-closed 报 809120;汇总口退回旧座位规则并在 `violations` 里报 809120。 --- ## 六.5、枚举 / 数据字典(接口出现枚举时必写) ### 错误码 | 码 | 触发 | 报文 | |---|---|---| | 809124 | 该组 `seats` 不在该组 `vehicleType` 的可选档位内 | 第 {0} 组的座位数 {1} 不在车型 {2} 的可选档位 {3} 内,请从下拉项中选择 | | 809120(复用) | 车队车型字典不可用 / 该大类在车队没有型号(档位不可得) | 车队车型字典暂不可用,无法校验车型,请稍后重试 | | 809118(不变) | `seats` 与 `count` 只填了一个 | 第 {0} 组的座位数与车辆数必须同时填写,或同时留空 | | 809116(不变) | 扣司机座后可载客座位少于该组最大单日人数 | 第 {0} 组座位数不足:{1} 座 × {2} 辆,扣除 {3} 个司机座后可载客 {4} 人,少于该组最大乘车人数 {5} 人 | ### seatOptionAdjusted[].reason | 取值 | 含义 | 建议提示语 | |---|---|---| | `SEATS_NOT_IN_OPTIONS` | 子订单报了座位但不在该大类档位内 | 该户报的 {originalSeats} 座本车型没有,草稿改用 {adoptedSeats} 座(可选 {seatOptions}) | | `SEATS_MISSING` | 子订单没报座位,草稿按字典最大档兜底 | 该户没报座位,草稿按 {adoptedSeats} 座(字典最大档)计 | | `SEATS_MISSING_ADOPTED_GROUP_VALUE` | 子订单没报座位,草稿采用的值来自同组其它户报的合法档位 | 该户没报座位,草稿跟随本组 {adoptedSeats} 座计 | --- ## 六.6、修改前后对比(修改/删除类接口必写,新增跳过) ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | 保存接口请求/响应字段 | — | 无新增、无删除,结构不变 | | `aggregate-draft` 响应 | 无 `seatOptionAdjusted` | 新增 `seatOptionAdjusted[]`(8 个子字段) | | `draft.groups[].seats` | 组内子订单报的最大单车座位 | 该大类档位内的值(在档位内取组内最大、否则兜底最大档;档位不可得退回旧规则) | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 提交字典内档位(如 bus 19 座) | 放行 | 放行(不变) | | 提交字典外档位(如 bus 13 座) | **放行** | **809124 拒绝** | | 提交座位数但车型大类在车队没有型号 | 放行 | 809120 拒绝(保存口);汇总口退回旧规则 + violations 里报 809120 | | 存量分组(seats/count 皆 null)整份再提交 | 跳过座位相关校验 | 不变,仍整条跳过 | | 预检/整团确认侧 | 不校档位 | 不变(该入口读的是库里已存值,且被 `doConfirm` 在事务内调用,见「七、不影响范围」) | --- ## 六.7、影响评估(修改/删除类必写) - **是否破坏向后兼容**:请求/响应结构不变,但**同一份 payload 可能从 200 变成 809124** —— 凡是座位数不在该大类档位内的提交(含手输、含车型切换后未更新座位数)都会被拒。809124 是 HTTP 200 下的业务失败。 - **前端是否必须同步上线**:**必须**。座位数必须改成档位下拉(否则运营按老习惯手输就会撞 809124);`seatOptionAdjusted` 必须展示(否则草稿改座位这件事不可见);组号输入框应改成只读展示。 - **存量数据影响**:不批量重算、不迁移。库里的历史非法档位只会在下一次保存/汇总时暴露:保存被 809124 拒(需管理员改成档位内值);预检与整团确认口**不**重判档位,不会把已确认流程卡死。 --- ## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面) - **仅影响**:`PUT .../vehicle-requirement` 的座位档位校验(新增 809124);`GET .../vehicle-requirement/aggregate-draft` 的 `seats` 取值与新增 `seatOptionAdjusted`。 - **零影响**: - 两个接口的请求/响应字段结构(除新增 `seatOptionAdjusted`)。 - 既有错误码 809116 / 809118 / 809117 / 809119 / 809120 / 809121 / 809102 等其余判据与报文。 - `GET .../vehicle-requirement`(读取回显)不触发校验,原样返回库里已落值。 - **整团确认预检(`GET .../requirement/confirm-check`)与 `confirm` 写口不重判座位档位**:它们读的是库里已存值,且确认链路在事务内,不在那里调字典 Feign。即「历史非法档位不会卡住确认」,只会在下次编辑保存时要求修正。 - `GET .../requirement/vehicle-households`、`GET .../requirement-summary` 未改动。 - 子订单级用车需求(582024 那条)未改动。 - fleet 服务与网关:零改动(`/admin/fleet/vehicle-types/list` 复用既有只读端点)。 - 数据库:无表变更、无 Flyway。 --- ## 八、测试环境已验证 **部署读数**(测试服 `deploy-status.sh`,2026-09-24 12:07 读取): ```text SERVICE BRANCH COMMIT BEHIND DEPLOYED_AT STATE hl-order-service-v3 dev-v3 1638faf08 0/N 2026-09-24 12:06:26 ok hl-gateway dev-v3 c238f38c3 182/Y 2026-09-21 14:44:55 ok hl-fleet-service dev-v3 cc70c9ba3 6/N 2026-09-24 10:49:54 ok ``` 被测服务 `BEHIND=0/N STATE=ok`;请求路径上的 `hl-gateway` 与断言依赖的 `hl-fleet-service` 同为 `ok`,两行的 BEHIND 右侧字母均为 `N`(落后提交未触及这两个服务本身)。 **网关真实请求与响应**(`https://api.test.1814.love`,`groupBatchId=2102692937584513025`,当前 DRAFT v3:BUS 16 座 ×1、SUV 5 座 ×1): ```text ① 原值提交 BUS 16 座 / SUV 5 座(均在档位内) PUT /v3/admin/order/group-batch/2102692937584513025/vehicle-requirement → http 200, code=200, message=成功;回读 version 2→3,seats 仍为 16/5 ✓ ② BUS 提交 13 座(bus 档位 12/15/16/19) → http 200, code=809124 message=第 BUS 组的座位数 13 不在车型 bus 的可选档位 [12, 15, 16, 19] 内,请从下拉项中选择 ✓ 拒绝后回读:version 仍为 3、seats 仍为 16/5、remark 未变(整份零写入)✓ ③ SUV 提交 6 座(suv 档位 5/7) → http 200, code=809124 message=第 SUV 组的座位数 6 不在车型 suv 的可选档位 [5, 7] 内,请从下拉项中选择 ✓ ④ GET /v3/admin/order/group-batch/2102692937584513025/vehicle-requirement/aggregate-draft → code=200;draft.groups = BUS(bus, 16 座, 1 辆, 3 天) + SUV(suv, 5 座, 1 辆, 3 天) seatOptionAdjusted = [](该团期子订单报的座位都合法,无兜底调整) violations = [];droppedFleetItems 1 条、stale/padded 0 条 ✓ ``` **定向测试逐类读数**(`mvn -o -pl hl-order-service-v3 -am test -Dtest='…' -DfailIfNoTests=false -Dhl.surefire.failIfNoTests=false`,14 个类合计 0 failures / 0 errors / 0 skipped,BUILD SUCCESS): | 测试类 | Tests run | |---|---| | GroupVehicleRequirementSaveTest | 31 | | GroupVehicleDraftAggregatorTest | 24 | | GroupVehicleRequirementAggregateDraftTest | 12 | | GroupVehicleRequirementConfirmCheckTest | 19 | | GroupVehicleRequirementLockContractTest | 3 | | GroupBatchRequirementServiceTest | 73 | | GroupBatchRequirementServiceConfirmVehicleTest | 17 | | GroupBatchRequirementServiceCheckVehicleTest | 17 | | GroupBatchRequirementConfirmReflectionGuardTest | 4 | | FleetVehicleTypeNameLoaderTest | 9 | | TransactionalRemoteCallArchTest | 1 | | ErrorCodeUniquenessGuardTest | 3 | | RequirementGroupBatchErrorCodeRangeTest | 4 | | ErrorCodeTemplateNumberFormattingTest | 1 | --- ## 十、相关文档 - 自动汇总草稿端点:#8220 → `changelogs-v2/2026-09/23_8220_团期正式行程用车需求自动汇总草稿-新增接口-管理后台.md` - 车辆规格四字段(seats/count/specialTags/remark):#8152 → `changelogs-v2/2026-09/22_8152_团期用车需求补车辆规格与接送机汇总-修改接口-管理后台.md` - 座位充足性校验扣司机座(809116):#8278 → `changelogs-v2/2026-09/23_8278_团级用车分组座位校验扣司机座-修改接口-管理后台.md` - 座位数下拉先例(子订单级):#4856 / #4871 → `changelogs-v2/2026-07/50_4856_订单调整用车座位数按车型型号下拉-管理后台.md` - 车型大类字典与 SUV 回显归一:#8202 / #8221 → `changelogs-v2/2026-09/` --- ## 关联 / 联系人 ### 链接 - Issue: https://git.1814.love/wx/HL/issues/8311 - 后端 PR: 待建 ### 联系人 - 后端: jw / wx