diff --git a/changelogs-v2/2026-09/24_8311_团期分组座位数改档位下拉与809124校验-修改接口-管理后台.md b/changelogs-v2/2026-09/24_8311_团期分组座位数改档位下拉与809124校验-修改接口-管理后台.md new file mode 100644 index 00000000..3c0ccde8 --- /dev/null +++ b/changelogs-v2/2026-09/24_8311_团期分组座位数改档位下拉与809124校验-修改接口-管理后台.md @@ -0,0 +1,518 @@ +--- +schema: "hl-changelog/v2" +ticket: "8311" +title: "团期正式用车需求分组座位数改由车队字典档位下拉选择(可选可改),新增 809124 档位校验与汇总座位兜底诊断" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "mmg" +frontend_ref: "" +target_release: "v2.1" +verified_at: "" +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。" +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