35 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 8278 | 团级用车分组座位充足性校验(809116)改为扣司机座,与子订单级/fleet 单车派车口径统一 | admin | wx(GIT) | 修改接口 | deployed | verified | not_required | PR #8296 已合并 dev-v3(a0973867f)。部署:hl-order-service-v3 dev-v3 @ a0973867f,2026-09-23 21:09:02 部署,deploy-status 读数 BEHIND=0 STATE=ok,8086/8186 两实例 Nacos 健康。测试服网关真实 PUT 实测(groupBatchId=2102749115823919105,20 座大巴 × 2 辆,两次请求均晚于部署时刻):headcount=40 时返回 809116(扣司机座后可载 38 人,不足 40);headcount=38 时返回 200,remainingPassengerSeats=0。定向单测 mvn -o -pl hl-order-service-v3 -am test:17 个外层类 148/0/0(含 9 个 @ArchTest 载体),BUILD SUCCESS。存量影响核查(测试服只读 SELECT,2026-09-23 20:14:20):活跃团级用车需求 131 个,其中带完整规格(seats/count 均非空)的分组 3 组,按新口径重算全部仍满足要求,按新口径会被拒的活跃分组数为 0;生产环境二期尚未开放,无生产存量。gateway_status: verified —— 路径本就在既有 /v3/admin/** order-service-v3 路由下,本次零路由改动,且已通过真实网关实测。;前端判 not_required:809116 全仓零文本/占位符解析(整句 toast+violations detail 直显);余座负值三处消费不 clamp 不拦截;spec 无刚好坐满放行预期 | 2026-09-23 | dev-v3 |
order-v3 团期需求: 团级用车分组座位充足性校验改为扣司机座
存放目录: 二期(v3) →
changelogs-v2/2026-09/服务: hl-order-service-v3(团期需求域) PR: #8296 Issue: #8278 日期: 2026-09-23 影响范围: 管理后台「团期详情 → 查看需求」Tab 的团级正式用车需求编辑弹窗、用车需求汇总草稿预览
⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
- 本次变化:
PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement的 809116 座位充足性校验,判据从「座位数 × 车辆数 < 该组最大单日乘车人数」改为「(座位数 − 1) × 车辆数 < 该组最大单日乘车人数」——每辆车扣 1 个司机座,与子订单级用车需求校验、fleet 单车派车(AssignmentService)口径统一。 - 前端以前以为的(对应
22_8152_...changelog 描述的旧行为):座位数 × 车辆数刚好等于该组最大单日人数的分组能保存成功,例如 19 座 × 1 辆、最大日人数 19 能保存;同一响应体里remainingPassengerSeats(余座回显)却是扣了司机座算出来的,可能已经是负数,前端只需原样展示负值提示缺口,不作为提交是否成功的依据。 - 实际现在的行为:同样 19 座 × 1 辆、最大日人数 19 这组输入,现在直接被 809116 拒绝。「拦截口径」与「回显口径」现在共用同一份计算(
VehicleSeatCalculator.SeatSummary),不再互相矛盾——凡是回显会算出remainingPassengerSeats为负的组合,保存时就先被拒绝,不会写入库。 - 809116 报文占位符从 5 个变成 6 个,新模板与两个换算样例见「三、接口详情 → 1 → 业务边界」。
- 本条取代
changelogs-v2/2026-09/22_8152_团期用车需求补车辆规格与接送机汇总-修改接口-管理后台.md中关于 809116 判据与文案的描述,具体被取代的位置见「十、相关文档」。
一、背景(选填)
#8278 AC-1 定案:旧口径下「拦截不扣司机座、回显扣司机座」是对称子域的口径漂移(CODE_RULES §15.7)——子订单级容量校验与 fleet 单车派车早已扣司机座,只有团级拦截没扣,导致「刚好坐满」的组合能保存成功,但回显立刻显示余座为负。本单让团级拦截口径向已有的、更严格的口径看齐(扣司机座),而不是放松回显。
最强反例(评审已确认,留档):「刚好坐满、司机另开一辆车」这类特殊排法在新口径下会被拒绝——这是业务上刻意收紧,因为司机座不能卖给乘客,运营应当据此把车辆规格填成真实的乘客运力,而不是靠"整车都算乘客座"的旧口径蒙混过关。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 保存团期正式用车需求 | PUT | /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement |
校验判据变更 + 错误报文格式变更 | 809116 扣司机座 |
| 2 | 用车需求汇总草稿 | GET | /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft |
响应字段语义说明 | violations[] 里的 809116 条目共用同一份新判据/新报文 |
三、接口详情
1. 保存团期正式用车需求 PUT /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement
VO: GroupVehicleRequirementSaveReqVO → GroupVehicleRequirementRespVO
使用场景
团期详情「查看需求 → 团级正式用车需求」编辑弹窗点保存。语义仍是整份全量替换:未出现在本次提交里的分组会被移出当前版本。请求体与响应体结构均不变,本条只改 809116 的判据与报文。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
| version | Body | Integer | ❌ | 首次保存传 null,其后回传上次拿到的值 | 乐观锁;不一致抛 809102 |
| remark | Body | String | ❌ | ≤500 | 整份需求备注 |
| groups | Body | Array | ✅ | 可以是空数组(@NotNull 非 @NotEmpty) |
全部乘车分组;有在团需车户却零分组抛 809103 |
| groups[].groupId | Body | Long | ❌ | 新增分组传 null | 带上它即声明「这就是库里那一组」,此时 groupCode 不得变更(否则 809104) |
| groups[].groupCode | Body | String | ✅ | ≤32,同一份内不得重复 | 直接作为车费 alloc_group |
| groups[].vehicleType | Body | String | ✅ | ≤64 | 车型文本/字典值 |
| groups[].serviceStartDate | Body | LocalDate | ✅ | yyyy-MM-dd |
本组服务开始日 |
| groups[].serviceEndDate | Body | LocalDate | ✅ | 不早于开始日 | 本组服务结束日 |
| groups[].seats | Body | Integer | ❌ | @Min(1);与 count 同填或同空 |
单车座位数(含驾驶位);本次改动后参与扣司机座的容量判据,存量分组可不传 |
| 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),且 ≥ 当日成员户数 |
该组该日乘车人数;本次改动后这个值与「(seats − 1) × count」比较,决定是否触发 809116 |
| groups[].days[].memberOrderIds | Body | Array<Long> | ✅ | 非空,须全属本团在团户 | 该组该日实际乘车的子订单集合 |
出参 Result<GroupVehicleRequirementRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| requirementId | String | 正式需求主键(Long 序列化为字符串) |
| groupBatchId | String | 团期聚合主键(Long 序列化为字符串) |
| status | String | DRAFT / CONFIRMED / DISPATCHED / DONE / PENDING_RECONFIRM / CANCELLED;PUT 后为 DRAFT |
| version | Integer | 版本号,下次提交须回传 |
| remark | String | 整份备注 |
| confirmedBy / confirmedAt | String / LocalDateTime | 整份确认人/时间;DRAFT 时为 null |
| planRefreshState / planRefreshReplayCount / blockedStage / planRefreshStalled / planRefreshStalledReason / planRefreshTimeoutAt / planRefreshReplayExhausted | - | 配车刷新状态相关字段,本次改动未涉及 |
| groups | Array | 全部乘车分组;整团免车态为空数组 |
| groups[].groupId | String | 分组主键(Long 序列化为字符串) |
| groups[].groupCode | String | 分组键 = 车费 alloc_group |
| groups[].vehicleType / vehicleTypeName | String | 车型文本/字典值 / 车型中文名 |
| groups[].serviceStartDate / serviceEndDate | LocalDate | 本组服务日范围 |
| groups[].seats | Integer | 单车座位数(含驾驶位);存量分组为 null |
| groups[].count | Integer | 车辆数量;存量分组为 null |
| groups[].specialTags | Array<SpecialTagItem> | 特殊诉求标签(code + name) |
| groups[].remark | String | 该组备注 |
| groups[].totalSeatCount | Integer | 总座位数 = seats × count(含驾驶位);座位或数量缺一即为 null |
| groups[].maxHeadcount | Integer | 该组 days 里的最大用车人数 |
| groups[].remainingPassengerSeats | Integer | 余座 = 扣司机座后的可乘座位 − maxHeadcount;可为负;计算公式本次未变,但新提交里凡是会算出负值的组合,保存时已被 809116 拦在前面,不会写入库——负值目前只可能出现在改动前已保存、尚未被下一次提交重新校验的存量分组上 |
| groups[].days[].tripDate / headcount / memberOrderIds / memberOrderCount | - | 逐日行程与成员,未变 |
请求示例
{
"version": null,
"remark": "#8278-AC6",
"groups": [
{
"groupId": null,
"groupCode": "AC6BUS",
"vehicleType": "bus",
"serviceStartDate": "2027-03-24",
"serviceEndDate": "2027-03-25",
"seats": 20,
"count": 2,
"specialTags": [],
"remark": "#8278-AC6",
"days": [
{ "tripDate": "2027-03-24", "headcount": 38, "memberOrderIds": [2102749115559677953] },
{ "tripDate": "2027-03-25", "headcount": 38, "memberOrderIds": [2102749115559677953] }
]
}
]
}
响应示例
真实网关实测响应(20 座 × 2 辆,headcount=38,(20−1)×2=38 恰好用完):
{
"code": 200,
"message": "成功",
"data": {
"requirementId": "2102750063359135746",
"groupBatchId": "2102749115823919105",
"status": "DRAFT",
"version": 1,
"remark": "#8278-AC6",
"confirmedBy": null,
"confirmedAt": null,
"planRefreshState": null,
"planRefreshReplayCount": 0,
"blockedStage": null,
"planRefreshStalled": false,
"planRefreshStalledReason": null,
"planRefreshTimeoutAt": null,
"planRefreshReplayExhausted": false,
"groups": [
{
"groupId": "2102750063363330049",
"groupCode": "AC6BUS",
"vehicleType": "bus",
"vehicleTypeName": "大巴系列",
"serviceStartDate": "2027-03-24",
"serviceEndDate": "2027-03-25",
"seats": 20,
"count": 2,
"specialTags": [],
"remark": "#8278-AC6",
"totalSeatCount": 40,
"maxHeadcount": 38,
"remainingPassengerSeats": 0,
"days": [
{ "tripDate": "2027-03-24", "headcount": 38, "memberOrderIds": ["2102749115559677953"], "memberOrderCount": 1 },
{ "tripDate": "2027-03-25", "headcount": 38, "memberOrderIds": ["2102749115559677953"], "memberOrderCount": 1 }
]
}
]
},
"success": true
}
空数据 / 降级响应
- 整团没有在团需车户时,
groups: []是合法提交,响应groups为空数组;不受本次改动影响。 - 存量分组(本次改动之前已保存的组)在数据库里原样保留旧值,不会因为本次上线被批量重算或清退;只有当它下次出现在某次 PUT 提交的
groups数组里(哪怕自身字段一个没改,只是跟其他组一起整份提交)时,才会按新口径重新校验——如果它本身坐不下(扣司机座后不够),这次整份保存会被 809116 拒绝、全部字段零写入。 - 车型中文名依赖车队侧车型库,降级行为不变:查不到编码或车队不可用时
vehicleTypeName为 null,接口仍 200,不阻断页面。
{ "code": 200, "message": "成功", "data": { "groups": [] }, "success": true }
错误响应
真实网关实测响应(20 座 × 2 辆,headcount=40,(20−1)×2=38 < 40):
{
"code": 809116,
"message": "第 AC6BUS 组座位数不足:20 座 × 2 辆,扣除 2 个司机座后可载客 38 人,少于该组最大乘车人数 40 人",
"success": false,
"data": null
}
业务边界
- 报文里「第 {0} 组」的 {0} 仍是
groupCode,不是序号(不变);一次只抛一条,按校验遍历顺序收集(不变)。 - 新模板与占位符:
第 {0} 组座位数不足:{1} 座 × {2} 辆,扣除 {3} 个司机座后可载客 {4} 人,少于该组最大乘车人数 {5} 人。{1}=单车座位数(含司机座),{2}=车辆数,{3}=扣除的司机座数(恒等于 {2}),{4}=(seats−1)×count算出的可载客座位,{5}=该组最大单日乘车人数。占位符从旧模板的 5 个({0}{4})变成 6 个({0}{5}),其中两个位置的语义变了:旧模板第 {0} 组座位数不足:{1} 座 × {2} 辆 = {3} 座,少于该组最大乘车人数 {4} 人里,{3} 是总座位数(seats × count),{4} 是该组最大乘车人数;新模板里 {3} 改为扣除的司机座数,{4} 改为可载客座位数,最大乘车人数挪到 {5}。{0}/{1}/{2} 语义不变。前端如果曾按位置/正则解析这句报文(而不是原样展示整句message),必须同步改;如果只是原样 toast 整句message字符串,零改动即可。 - 判据公式:
passengerSeatCapacity = max(0, seats × count − count),passengerSeatCapacity < maxHeadcount时拒绝。两个换算样例(公式自算,均为「改前放行、改后拒绝」的收窄场景):- 19 座 × 1 辆、该组最大日人数 19:
passengerSeatCapacity = max(0, 19×1 − 1) = 18,18 < 19→ 拒绝,809116:「第 {groupCode} 组座位数不足:19 座 × 1 辆,扣除 1 个司机座后可载客 18 人,少于该组最大乘车人数 19 人」。 - 7 座 × 2 辆、该组最大日人数 13:
passengerSeatCapacity = max(0, 7×2 − 2) = 12,12 < 13→ 拒绝,809116:「第 {groupCode} 组座位数不足:7 座 × 2 辆,扣除 2 个司机座后可载客 12 人,少于该组最大乘车人数 13 人」。
- 19 座 × 1 辆、该组最大日人数 19:
- seats 与 count 同生同死规则不变:只填一个抛 809118(报文字面文本未变,仍是「第 {0} 组的座位数与车辆数必须同时填写,或同时留空」);两个都不填 = 存量形态,座位校验整体跳过。
- 乐观锁
version不一致抛 809102(不变);特殊诉求标签字典校验(809117)、逐字段约束均不变。
2. 用车需求汇总草稿 GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/aggregate-draft
VO: GroupVehicleAggregateDraftRespVO
使用场景
团期详情「查看需求 → 团级正式用车需求」为空/未确认时,前端用它拉一份系统按子订单在团情况自动配出的推荐草稿(seats 取组内最大单车座位、count = ceil(最大日人数 / (seats − 1))),供运营参考后再决定是否提交。本次改动不影响这个推荐公式本身(GroupVehicleDraftAggregator.java:345,已用 git show a0973867f -- <该文件> 核对,本次合并只改了该文件的 javadoc 注释,公式代码未动),只影响 violations[] 数组里 809116 条目的判据与报文——它与保存端点共用同一份校验函数(collectFleetSpecViolations)。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | ✅ | - | 团期聚合主键 |
出参 Result<GroupVehicleAggregateDraftRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| groupBatchId | String | 团期聚合主键(Long 序列化为字符串) |
| currentStatus | String | 当前正式需求状态;未建过正式需求为 null |
| draft | Object | 结构同 GroupVehicleRequirementSaveReqVO;version 为当前生效版本号或 null;draft.groups[].groupId 恒为 null(草稿未落库) |
| droppedFleetItems | Array | 因非主车型/车型字典不可用被剔除的子订单车队行;字段 orderId/orderNo/vehicleType/seats/count/keptVehicleType/reason |
| staleHeadcountOrders | Array | 冻结人数与当前实际人数不一致的子订单;字段 orderId/orderNo/frozenHeadcount/liveHeadcount |
| paddedOrderDays | Array | 被自动补天的子订单;字段 orderId/orderNo/dates |
| violations | Array | 草稿自身触发的校验违规;恒非 null,无违规为空数组 |
| violations[].code | Integer | 错误码,本次改动相关的是 809116 |
| violations[].reason | String | 原因码,见「六.5」 |
| violations[].detail | String | 渲染文案,与「三 → 1 → 错误响应」里同码条目的 message 逐字相同 |
| violations[].groupCode / tripDate / orderId | String / LocalDate / String | 定位到具体分组/日期/子订单,均可为 null |
请求示例
GET /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement/aggregate-draft
响应示例
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2102749115823919105",
"currentStatus": null,
"draft": {
"version": null,
"remark": null,
"groups": [
{
"groupId": null,
"groupCode": "AC6BUS",
"vehicleType": "bus",
"serviceStartDate": "2027-03-24",
"serviceEndDate": "2027-03-25",
"seats": 20,
"count": 2,
"specialTags": [],
"remark": null,
"days": [
{ "tripDate": "2027-03-24", "headcount": 38, "memberOrderIds": ["2102749115559677953"] },
{ "tripDate": "2027-03-25", "headcount": 38, "memberOrderIds": ["2102749115559677953"] }
]
}
]
},
"droppedFleetItems": [],
"staleHeadcountOrders": [],
"paddedOrderDays": [],
"violations": []
},
"success": true
}
空数据 / 降级响应
- 整团没有可汇总内容时,
draft.groups为空数组,droppedFleetItems/staleHeadcountOrders/paddedOrderDays/violations均为空数组(javadoc 原文:恒非 null)。 - 有需车户缺少可汇总的行程用车需求时报 809121(本次改动未涉及此判据)。
- 车型字典不可用时报 809120(本次改动未涉及此判据)。
{ "code": 200, "message": "成功", "data": { "draft": { "groups": [] }, "droppedFleetItems": [], "staleHeadcountOrders": [], "paddedOrderDays": [], "violations": [] }, "success": true }
错误响应
{
"code": 809121,
"message": "团期 XXX 有 2 户缺少可汇总的行程用车需求,暂不能自动汇总:ORD001、ORD002",
"success": false,
"data": null
}
业务边界
- 推荐公式
count = ceil(最大日人数 / (seats − 1))保证草稿自己生成的分组,扣司机座后的可载客座位数恒 ≥ 该组最大日人数,所以由这个端点直接产出的草稿正常情况下不会在自己的violations[]里出现 809116。 violations[]里若确实出现 809116 条目(例如运营手工改过草稿后再调这个端点做二次校验),其detail文案与占位符结构(6 段)与「三 → 1 → 错误响应」逐字相同,前端复用同一份渲染/解析逻辑即可,不需要为本端点单独适配。- 本端点只读,不落库,多次调用互不影响、无并发/幂等问题。
四、契约约束与正确调用方式(接口类必写)
本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。
✅ 正确 / ❌ 错误 payload 对照(PUT .../vehicle-requirement 分组元素,seats/count/该组最大单日人数三者的关系)
| 场景 | seats × count | (seats−1) × count | 最大单日人数 | 判定 |
|---|---|---|---|---|
| ✅ 明显够坐 | 35 | 34 | 20 | 放行(改前改后均放行) |
| ✅ 恰好用完(边界) | 40 | 38 | 38 | 放行,remainingPassengerSeats=0(20 座×2 辆/38 人,已用真实网关实测) |
| ❌ 不扣司机座够坐、扣了不够(新增拒绝区间) | 19 | 18 | 19 | 809116 拒绝——改前放行、改后拒绝 |
| ❌ 不扣司机座够坐、扣了不够(新增拒绝区间) | 14 | 12 | 13 | 809116 拒绝——改前放行、改后拒绝(7 座×2 辆/13 人) |
| ❌ 两版本均拒绝 | 10 | 9 | 15 | 809116 拒绝(改前改后均拒绝,不扣司机座也不够坐) |
切换状态时的必要动作
seats/count 仍是「同填同空」的互斥对(不变):只提交其中一个会被 809118 拒绝;两个都不提交等价于存量形态,座位校验整体跳过。提交时不要依赖"隐藏输入框"的 UI 行为,后端只看 payload 里这两个字段是否同时为非 null。
五、数据库行为(涉及写操作时必写)
本次改动不涉及表结构变化,groups.seats/groups.count/groups_day.headcount 三列的落库口径与列值语义均未变——放行的提交,落库值仍是前端提交的原始 seats/count,后端不做任何扣减存储;变的只是"放不放行"这一步的判定发生在写库之前。
| 前端提交 | 改前落库结果 | 改后落库结果 |
|---|---|---|
| 19 座 × 1 辆、最大日人数 19(该组) | 校验通过,group.seats=19, group.count=1 落库 |
809116 拒绝,整份提交零写入(不含该分组之外的其他改动) |
| 20 座 × 2 辆、最大日人数 38(该组) | 校验通过,group.seats=20, group.count=2 落库 |
校验通过(边界恰好用完),落库同值 |
零写入范围:PUT 语义是整份全量替换,任一分组触发 809116 会导致这次提交整体失败,不只是该分组,其余分组在本次提交里的改动也不会落库(与改前逻辑一致,不是本次新增行为)。
六、边界行为
- 未登录 → 401(网关拦截,不变)。
groupBatchId不存在 → 团期聚合层报错(不变,未在本次改动范围)。- 车队/字典服务降级 → 809120(
vehicleType字典不可用)判据与报文不变,仍与 809116 相互独立、互不影响。 - 老数据兼容 → 存量分组的
seats/count为 null 时 809116 判据整体跳过(不变);已有seats/count但未随本次改动重新保存的组,回显仍按旧值展示,可能带负的remainingPassengerSeats(见「三 → 1 → 空数据 / 降级响应」)。 - 生产环境 → 二期功能尚未在生产开放,本次改动的行为差异目前不会被任何生产流量触发。
六.5、枚举 / 数据字典(接口出现枚举时必写)
status(正式用车需求状态,GroupVehicleRequirementRespVO.status)
所属字段: GroupVehicleRequirementRespVO.status | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
DRAFT |
草稿 | PUT 保存成功后的默认状态;本次改动后,坐不下的组合会在到达这一步之前就被 809116 拒绝 |
CONFIRMED |
已确认 | 整份确认后 |
DISPATCHED |
已派车 | fleet 已排车 |
DONE |
已完成 | 车务完成 |
PENDING_RECONFIRM |
待重新确认 | 确认后又被撤回/变更,需重新确认 |
CANCELLED |
已取消 | 整团取消 |
本次改动未涉及状态机迁移逻辑本身,取值与含义均不变。
violations[].reason(GroupVehicleAggregateDraftRespVO.Violation.reason,本次改动相关的两个取值)
所属字段: GroupVehicleAggregateDraftRespVO.Violation.reason | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
GROUP_SEATS_INSUFFICIENT |
座位数不足 | 对应 809116;本次改动后判据改为扣司机座 |
GROUP_SPEC_INCOMPLETE |
车辆规格不完整 | 对应 809118;判据与报文字面文本均未变 |
六.6、修改前后对比(修改/删除类接口必写,新增跳过)
字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
| 809116 报文占位符数量 | 5 个({0}~{4}) | 6 个({0}~{5}):{3} 从「总座位数(seats × count)」改为「扣除的司机座数(=车辆数)」;{4} 从「最大乘车人数」改为「可载客座位数」;「最大乘车人数」后移到新增的 {5} |
| 809116 判据表达式 | seats × count < 该组最大单日乘车人数 |
max(0, seats × count − count) < 该组最大单日乘车人数(即扣司机座后判断) |
remainingPassengerSeats(响应字段) |
计算公式含扣司机座,可能为负;负值组合仍能保存成功(拦截口径更宽松) | 计算公式不变;但新提交里会算出负值的组合,已先被 809116 拒绝,不会写入库 |
行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
提交 seats × count == 该组最大单日人数(如 19 座×1 辆/19 人) |
放行 | 809116 拒绝 |
提交 (seats−1) × count == 该组最大单日人数(如 20 座×2 辆/38 人) |
放行 | 放行(余座为 0,恰好用完) |
提交 (seats−1)×count < 该组最大单日人数 ≤ seats×count(如 19×1/19、7×2/13) |
放行(不扣司机座时够坐) | 809116 拒绝(扣司机座后不够坐,本次改动新增的拒绝区间) |
| 拦截口径 vs 回显口径是否一致 | 刻意相差 1 个司机座/车,可能「保存成功但 remainingPassengerSeats 为负」 | 两口径共用同一份 VehicleSeatCalculator 计算,新提交不会再出现这种矛盾 |
| 存量分组(改动前已保存、字段未变) | — | 不主动重算,原样保留在库里;只有它下次出现在某次 PUT 的 groups 数组里才会被新口径重新校验 |
fleet 单车派车(AssignmentService) |
已扣司机座 | 不变,本次改动是团级向它对齐 |
fleet 团级就绪检查黄牌(GroupDispatchReadinessService#seatShortageWarnings) |
不扣司机座 | 不变,仍不扣司机座,跟踪于 #8294,不在本次改动范围内 |
六.7、影响评估(修改/删除类必写)
- 是否破坏向后兼容: 部分是——请求体/响应体字段结构未变,但同一份 payload 在「恰好等于旧口径边界、不足新口径边界」的场景下,会从改前的 200 变成改后的 809116(809116 是 HTTP 200 下的业务失败,不是传输层错误)。
- 前端是否必须同步上线: 视前端现有实现而定。若前端只是把 809116 的
message整句原样 toast 展示,不改也能正常显示新文案,零改动。若前端曾按占位符位置/正则解析这句报文(例如截取「× {2} 辆」后面的数字单独展示),必须同步改成新的 6 段结构,否则会把 {3}(司机座数)或 {4}(可载客座位)错位显示。 - 前端 workaround 清理点: 若前端为「刚好坐满」这类输入写过专门的“应该能保存”预期用例,需要把预期改成会撞 809116;
remainingPassengerSeats为负仍是合法信号(不应做Math.max(0, x)clamp,也不应据此拦截提交,这条 22_8152 的既有结论继续有效),但不能再假设"负值一定对应一个刚保存成功的新组合"——新提交里的负值组合已经在保存时被拒绝,负值目前只可能来自尚未被下一次保存重新校验的存量分组。
七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- 仅影响:
PUT .../vehicle-requirement的 809116 触发条件与报文;GET .../vehicle-requirement/aggregate-draft响应体violations[]数组里 809116 条目的判据与报文(若出现)。 - 零影响:
- 两个接口的请求体/响应体字段结构(无新增、无删除字段)。
- 错误码码值本身(仍是 809116/809118,未变);809117(特殊诉求标签字典)、809102(乐观锁)、809103
809115、809119809122 等其余错误码的判据与报文。 GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement(读取回显):不触发任何校验,原样返回库里已落值。GET .../requirement/vehicle-households、GET .../requirement-summary、GET .../requirement/confirm-check:均未改动。aggregate-draft自身的推荐车辆数公式count = ceil(最大日人数 / (seats − 1))(GroupVehicleDraftAggregator.java:345):本次合并只改了该文件的 javadoc,公式代码本身未动。- fleet 单车派车口径(
AssignmentService):本就扣司机座,不受影响。 - fleet 团级就绪检查黄牌口径(
GroupDispatchReadinessService#seatShortageWarnings):仍不扣司机座,未随本次改动对齐,另开 #8294 跟进。 - 生产环境:二期功能尚未在生产开放,本条改动不影响任何生产流量。
- 历史数据:存量分组的
seats/count/headcount不做批量重算或迁移,见「六.6」。
八、测试环境已验证
真实网关实测(https://api.test.1814.love,hl-order-service-v3 dev-v3 @ a0973867f,2026-09-23 21:09:02 部署,deploy-status 读数 BEHIND=0 STATE=ok,两次 PUT 请求均晚于部署时刻):
PUT /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement
20 座 × 2 辆,headcount=40(两天) → code=809116
message=第 AC6BUS 组座位数不足:20 座 × 2 辆,扣除 2 个司机座后可载客 38 人,少于该组最大乘车人数 40 人 ✓
GET 同一团期同一端点回读 → code=200, data=null(校验在写库前抛出,零写入)✓
PUT /v3/admin/order/group-batch/2102749115823919105/vehicle-requirement
同一分组,headcount 改为 38(两天) → code=200, message=成功
data.groups[0]: seats=20, count=2, totalSeatCount=40, maxHeadcount=38, remainingPassengerSeats=0 ✓
GET 同一团期同一端点回读 → code=200, requirementId=2102750063359135746, status=DRAFT, version=1 ✓
验证团期:groupBatchId=2102749115823919105(本轮自建夹具,未影响任何既有排期/团期)。两次 PUT 之间唯一变量是 headcount(40→38),groupCode/vehicleType/seats/count/日期范围完全相同,809116 与 200 的分野只能来自本次判据变更。
定向单测(mvn -o -pl hl-order-service-v3 -am test -Dtest='GroupVehicleRequirementSaveTest*,GroupVehicleDraftAggregatorTest*,...',起跑 2026-09-23 20:31:52):17 个外层类 / 148 用例,Failures/Errors/Skipped 全 0,BUILD SUCCESS;含 VehicleSeatCalculatorTest(6)、GroupVehicleRequirementSaveTest(25,含新旧边界两条判据)、GroupVehicleDraftAggregatorTest(19)与 9 个 @ArchTest 载体。仅改注释的返工提交后,定向复跑受影响 3 类:50/0/0。
存量影响核查(测试服只读 SELECT,2026-09-23 20:14:20):活跃团级用车需求 131 个,其中带完整规格(seats/count 均非空)的分组 3 组;按新口径重算,这 3 组全部仍满足要求(新口径可载客座位 415 人不等,均 ≥ 该组最大日人数 25 人),按新口径会被拒的活跃分组数为 0。生产环境二期尚未开放,无生产存量。
限定:本次真实网关实测只覆盖了「一辆 20 座大巴 × 2 辆」这一种具体座位组合的团级 PUT 入口;其它座位数组合、子订单级校验、aggregate-draft 端点自身,由上述单测覆盖(aggregate-draft 与保存端点共享同一份校验代码,但本次未针对该端点单独发起真实 HTTP 调用)。存量核查的分母只有 3 组带规格(团级用车规格字段是 #8152 刚上线的新字段,前端入口尚未普及),0 组的读数只说明此刻不会新拒任何已存活跃版本,对未来接入更多分组后是否仍为 0 没有分辨力。
十、相关文档
- 关联 Issue: wx/HL#8278
- 关联 PR: wx/HL#8296
- 已知缺口(不在本次改动范围内): wx/HL#8294 —— fleet 团级就绪检查黄牌口径尚未对齐扣司机座
- 订正声明(取代旧描述,不改动原文件):本条取代
changelogs-v2/2026-09/22_8152_团期用车需求补车辆规格与接送机汇总-修改接口-管理后台.md中以下位置关于 809116 判据与文案的描述:- 第 36-38 行「⚠️ 关键变化」块(「拦截口径不扣司机位」「回显口径扣司机位」「20 座 × 1 辆 / 20 人能保存成功,而返回的
remainingPassengerSeats是 -1」):描述的是本次改动前的行为,改动后 20×1/20 这组输入已改为 809116 拒绝,两口径不再矛盾。 - 第 40、42、44 行「展示侧」「提交侧」「响应体里没有任何字段表示…」三段:其中「是否允许保存由后端 809116 判定(口径:
seats × count与该组最大乘车人数比较,不扣司机位)」这句已过时,判据已改为扣司机位。 - 第 240 行错误响应示例
"第 BUS 组座位数不足:19 座 × 1 辆 = 19 座,少于该组最大乘车人数 20 人":这是旧模板(5 个占位符)的渲染结果,新模板见本条「三 → 1 → 错误响应」与「三 → 1 → 业务边界」。 - 第 268 行业务边界「座位充足性判据 =
seats × count < 该组最大单日乘车人数,不扣司机位」:判据已变,见本条「六.6」。 - 第 354 行「20 座 × 1 辆载 20 人保存成功,余座
-1」:该结论已不成立,这组输入现在被拒绝。 - 第 944 行行为级对比表格「提交坐不下的规格 | 接受(无此字段) | 809116 拒绝(判据不扣司机位)」一行,末列「判据不扣司机位」已过时。
- 第 958-959 行「若此前把
remainingPassengerSeats之类余座数做过Math.max(0, x)处理,必须撤掉」「不要新增『余座为负则禁止保存』的前端校验」:这两条建议本身依然有效,但其背景「后端放行的组合」已收窄——新提交里会被后端放行的组合,扣司机座后已经够坐,不会再出现负值;负值目前只可能来自尚未被下一次保存重新校验的存量分组。
- 第 36-38 行「⚠️ 关键变化」块(「拦截口径不扣司机位」「回显口径扣司机位」「20 座 × 1 辆 / 20 人能保存成功,而返回的
关联 / 联系人
链接
联系人
- 后端负责人: @wx