--- schema: "hl-changelog/v2" ticket: "8202" title: "子订单行程用车车型大类接入车队字典校验(新增 582032/582033)" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "c314c8c67ddc61b8a1e4f4373d79e5a731b13272" target_release: "" verified_at: "2026-09-23" status_note: "PR #8224 已合并 dev-v3(b439565be)并滚动部署 TEST 双实例,Nacos 注册均 healthy:true。用车需求提交/调整两条写路径共用的车型大类校验链新增车队活字典比对,新增 582032/582033 两码;存量码 582022(填法不合法)不变。真实网关实测:两端点各以 vehicleType=\"__NOT_A_REAL_CATEGORY__\" 提交均返回 582022(填法层拦截优先于字典层)。前端 v2.1 已提前于 7df292624(09-23 09:27)改为车型字典下拉,新提交不会撞新码;仅存量已落库的非字典值在编辑态回显仍需前端显式提示。 | 2026-09-23 mmg 收尾交付:存量回显占位提示+fail-closed 拦截+JSDoc 订正,随 #8221 同 commit" updated_at: "2026-09-23" base: "dev-v3" --- # 用车需求模块: 子订单行程用车车型大类接入车队字典校验 > **存放目录**: 二期(v3) → `changelogs-v2/2026-09/` > > **服务**: hl-order-service-v3 > **PR**: #8224 > **Issue**: #8202 > **日期**: 2026-09-23 > **影响范围**: 管理后台子订单用车需求提交/修改、订单调整统一提交中的用车/接送机车型校验 --- ## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写) - 本次变化:`vehicleType` 归一成功(如 `大巴`→`bus`)后,新增一次**车队活字典**比对;该大类若已整类下线(车队活字典无一行在架代表),提交被拒。 - 前端以前以为的:只要 `vehicleType` 能归一为 `suv/mpv/bus/sedan` 之一(或直接传规范 key),提交就会成功。 - 实际新行为:归一成功只是必要条件,还须该大类**当前在车队活字典内**;否则报新码 `582032`。**存量码 `582022`(这串字符连大类都归一不出来)保持不变、且仍是校验链第一关**——按当前测试库数据,前端实际会撞上的是 `582022`(自由文本/拼写问题),`582032` 只在大类被整类下线时触发(详见下方六.5/六.6)。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 提交/修改/调整用车需求 | PUT | `/v3/admin/order/{id}/vehicle-requirement` | 校验链新增字典比对 | `fleet[].vehicleType` 归一后追加车队活字典校验 | | 2 | 调整订单统一提交 | POST | `/v3/admin/order/{id}/adjustment/submit` | 校验链新增字典比对 | `updates.vehicleRequirement.fleet[]`/`updates.transferRequirement.fleet[]` 复用同一条校验链 | --- ## 三、接口详情 ### 1. 提交/修改/调整用车需求 `PUT /v3/admin/order/{id}/vehicle-requirement` **VO**: `VehicleRequirementReqVO → VehicleRequirementRespVO` #### 使用场景 管理端子订单详情页提交/编辑用车需求(TRAVEL 行程用车 / TRANSFER 接送机)时调用;三分支由后端自动判断(无 active 需求=INIT_SUBMIT,PENDING=PENDING_EDIT,DONE=DONE_ADJUST),前端不用区分分支、始终整份提交。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | Authorization | Header | String | ✅ | - | 管理端登录令牌 | | id | Path | Long | ✅ | 订单 ID | 目标订单 | | kind | Body | String | ❌ | 正则 `TRAVEL\|TRANSFER` | 需求类别,缺省按订单类型推断 | | fleet | Body | Array\ | ✅ | 非空 | 车型组合列表 | | fleet[].vehicleType | Body | String | ✅ | 须能归一为 `suv/mpv/bus/sedan`(582022),归一后须在车队活字典内(**582032,本次新增**) | 车型大类,示例 `mpv`,可传中文别名(如"大巴") | | fleet[].seats | Body | Integer | ✅ | ≥0;须在该大类真实车型的座位选项内(582024) | 单车座位数 | | fleet[].count | Body | Integer | ✅ | >0 | 该车型数量 | | specialTags | Body | Array\ | ❌ | 须在 `vehicle_special_demand` 字典内 | 通用特殊诉求 | | pickupRequired / dropoffRequired | Body | Boolean | ❌ | - | 接送机方向标记(兼容字段) | | remark | Body | String | ❌ | ≤500 字 | 备注 | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | id | Long | 需求记录 ID | | kind | String | TRAVEL / TRANSFER | | version | Integer | 版本号 | | status | String | 需求状态 | | branchTaken | String | 本次实际走的分支:INIT_SUBMIT / PENDING_EDIT / DONE_ADJUST | | passengerCount | Integer | 出行人数 | | vehicleCount | Integer | 车辆总数 | | totalSeatCount | Integer | 总座位数 | | driverSeatCount | Integer | 司机座位数(= vehicleCount) | | passengerSeatCapacity | Integer | 载客座位容量 | | remainingPassengerSeats | Integer | 剩余可载客座位 | | previousVersion | Integer | DONE_ADJUST 分支下的上一版本号 | | assignmentDeletedCount | Integer | DONE_ADJUST 分支下被清理的派单数 | #### 请求示例 ```json { "kind": "TRAVEL", "fleet": [ { "vehicleType": "大巴", "seats": 7, "count": 1 } ], "remark": "接机后直达酒店" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "id": 2098765432109876543, "kind": "TRAVEL", "version": 2, "status": "PENDING", "branchTaken": "PENDING_EDIT", "vehicleCount": 1, "totalSeatCount": 7 }, "success": true } ``` #### 空数据 / 降级响应 写接口无空数据场景;提交成功即返回最新需求快照,无降级分支。 ```json { "code": 200, "data": { "branchTaken": "INIT_SUBMIT" }, "success": true } ``` #### 错误响应 `fleet[].vehicleType` 无法归一为 `suv/mpv/bus/sedan` 之一(含全部自由文本,如「别克GL8」)——**当前测试库数据下前端会实际撞上的错误**(存量码 582022,非本次新增,且排在校验链第一关): ```json { "code": 582022, "message": "车型必须是车型大类(SUV/MPV/大巴/轿车)", "success": false, "data": null } ``` 本次新增两码,仅在 `vehicleType` 归一**成功之后**才有机会触发: ```json { "code": 582032, "message": "车型 bus 不在车型字典内(不存在或已下线),请从下拉项中选择", "success": false, "data": null } ``` 582032 仅当归一得到的规范大类在车队活字典(`fleet_vehicle_type`)中已整类下线(该大类下所有型号均软删)时触发;测试库当前四个大类各有 1 行在架代表,本码在现有测试数据下不可达。 ```json { "code": 582033, "message": "车型字典不可用,请稍后重试", "success": false, "data": null } ``` 582033 仅当车队字典服务不可用(Feign 兜底返回空 Map)时触发,与本次具体填了什么车型无关,空字典按 fail-closed 处理。 #### 业务边界 - 鉴权:未登录 → 业务码 `401`。 - 校验顺序:`fleet` 非空(582021)→ 逐项归一 + 座位数/数量非负校验(582022/582023)→ **车队活字典比对(582032/582033,本次新增)** → 座位选项校验(582024)→ 特殊诉求校验(582025/582026)。任一步失败即拒,零写入。 - 存量已落库、含已下线大类的需求原样再次提交同一接口同样会被 582032 拒绝——这是有意设计(放行已下线大类等同于让下线动作失效),与团期批量车务 809119 同口径。 - 车型下拉数据源固定为 `GET /admin/fleet/vehicle-types/list`(车队服务只读端点,网关已放行订单域只读调用,无需新增路由配置)。 --- ### 2. 调整订单统一提交 `POST /v3/admin/order/{id}/adjustment/submit` **VO**: `AdjustmentSubmitReqVO → AdjustmentSubmitRespVO` #### 使用场景 管理端订单调整弹窗统一提交入口;前端在内存里收集出行人/改期/行程/房需求/用车需求/接送机需求等多个子领域的改动后一次性提交。本次改动只影响 `updates.vehicleRequirement.fleet[]`(行程用车)与 `updates.transferRequirement.fleet[]`(接送机)两个子领域各自的车型大类校验,其余子领域字段与行为不受影响。 #### 入参 > 仅列与本次改动相关的字段;其余子领域(`people`/`schedule`/`itinerary`/`hotelRequirement` 等)字段结构未变,不在此重复列出。 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | Authorization | Header | String | ✅ | - | 管理端登录令牌 | | id | Path | Long | ✅ | 订单 ID | 目标订单 | | updates | Body | Object | ✅ | 至少一个子领域非 null | 各子领域修改内容容器 | | updates.vehicleRequirement.fleet | Body | Array\ | ❌(该子领域非 null 时必传) | 非空则整组重新走校验链;结构与端点 1 的 `fleet` 完全相同 | 行程用车车型组合 | | updates.vehicleRequirement.fleet[].vehicleType | Body | String | 同端点 1 | 须归一成功(582022)且归一后在车队活字典内(**582032,本次新增**) | 车型大类 | | updates.transferRequirement.fleet | Body | Array\ | ❌(该子领域非 null 时必传) | 与 `vehicleRequirement.fleet` 走同一条校验方法,服务日由大交通信息派生 | 接送机车型组合 | | updates.transferRequirement.fleet[].vehicleType | Body | String | 同端点 1 | 同上 | 车型大类 | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | success | Boolean | 提交是否成功;失败由全局异常处理器返回 `Result{code,message,data:null}`,不会出现该字段为 `false` 的情形 | #### 请求示例 ```json { "updates": { "vehicleRequirement": { "fleet": [ { "vehicleType": "mpv", "seats": 7, "count": 1 } ], "remark": "新增 2 名成员,需更大车型" } } } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "success": true }, "success": true } ``` #### 空数据 / 降级响应 写接口无空数据场景;`updates` 内某子领域传 `null` 表示该子领域本次不改,不参与本次校验也不产生该子领域的变更记录。 ```json { "code": 200, "data": { "success": true }, "success": true } ``` #### 错误响应 与端点 1 完全同码同序(`AdjustmentService.validateVehicleSeatOptionsBeforeWrite` 从 `updates.vehicleRequirement`/`updates.transferRequirement` 的 `fleet` 字段构造 `VehicleRequirementReqVO` 后,调用与端点 1 相同的 `validateVehicleRequirementSeatOptions`)。当前测试库数据下前端会实际撞上的同样是 582022: ```json { "code": 582022, "message": "车型必须是车型大类(SUV/MPV/大巴/轿车)", "success": false, "data": null } ``` ```json { "code": 582032, "message": "车型 bus 不在车型字典内(不存在或已下线),请从下拉项中选择", "success": false, "data": null } ``` ```json { "code": 582033, "message": "车型字典不可用,请稍后重试", "success": false, "data": null } ``` #### 业务边界 - 鉴权:未登录 → 业务码 `401`。 - `submit` 整体在一个 `@Transactional(rollbackFor = Exception.class)` 事务内;`updates.vehicleRequirement` 与 `updates.transferRequirement` 两个子领域各自独立校验一次,任一子领域校验失败即整单回滚,**已通过校验的其他子领域改动也不会落库**。 - `updates.vehicleRequirement`/`updates.transferRequirement` 为 `null` 时该子领域完全跳过(既不校验也不改动),不受本次变更影响。 - 存量已落库、含已下线大类的需求原样再次经本接口提交同样会被 582032 拒绝,同端点 1。 --- ## 四、契约约束与正确调用方式(接口类必写) > 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。 ### ✅ 正确 / ❌ 错误 payload 对照 | 场景 | payload | 结果 | |------|---------|------| | ✅ 从 `GET /admin/fleet/vehicle-types/list` 下拉选值提交 | `vehicleType` 取字典返回的规范 key(`suv/mpv/bus/sedan`) | 通过归一 + 字典校验 | | ✅ 提交中文别名 | `vehicleType: "大巴"` | 归一为 `bus` 后再查字典,与直接传 `bus` 等价 | | ❌ 自由文本/型号名 | `vehicleType: "别克GL8"` | 582022,归一失败,校验链第一关即拒 | | ❌ 大类已被车队整类下线 | `vehicleType: "bus"` 但活字典无 `bus` 在架代表 | 582032,归一成功但字典比对不通过 | | ❌ 车队字典服务不可用 | 任意合法 `vehicleType` | 582033,fail-closed,不做逐项放行 | - `vehicleType` 一律以**归一后的规范 key**(非原始填法)落库;重新查看已提交需求时看到的也是规范 key,不是用户当时填的中文别名。 - 端点 1(`vehicle-requirement`)与端点 2(`adjustment/submit`)中的 `fleet[]` 结构与校验规则完全一致,前端不需要为两个入口分别处理错误码。 ### 前端已就绪的状态 mmg `v2.1` 分支 `7df292624`(2026-09-23 09:27)已将车型录入从自由文本改为 `GET /admin/fleet/vehicle-types/list` 字典下拉,新提交的 `vehicleType` 均取自字典返回值,不会触发本次新增的 582032/582033。 尚待前端覆盖的场景:**存量已落库需求的编辑态回显**——若某行 `vehicleType` 不在当前字典返回值内(历史遗留或大类刚被下线),下拉组件默认会渲染成空选项且用户无法感知原因;此时应显式提示"当前值不在字典内,请重新选择",而非静默清空。 **受影响的前端组件(实测点名,非推断)**:`src/views/order-v2/detail/modals/FunItemAdjustModal.vue`——hl-ui `v2.1` 上唯一调用 `putVehicleRequirement` 的 `.vue`(`:1177` import,`:2968` TRAVEL / `:2970` TRANSFER 两个调用点);api 封装在 `src/api/orderV2.js:1345` `putVehicleRequirement`。该组件已引入车型字典 api(`getVehicleTypesList`),本次要补的是「回显值不在字典返回集内」这一态的显式提示,不是接入下拉本身。 ⚠️ 顺带一条可查证的事实:`src/api/orderV2.js` 中 `putVehicleRequirement` 的 JSDoc 错误码一行写的是 `582024 / 582091`,不含本次新增的 `582032 / 582033`。注释不影响运行,但下一个照注释排错的人会找不到新码。 --- ## 五、数据库行为(涉及写操作时必写) - 无 Flyway migration、无 DDL、无表结构变更。 - 本次校验是**写前置校验**,不涉及任何新增列或索引;`vehicle_requirement` 表落库字段与校验前完全一致(仅 `vehicle_type` 列存的值继续是归一后的规范 key)。 - 校验失败(582022/582032/582033/582024 等)的请求**零写入**——包括 `vehicle_requirement` 主表、审计记录、派单联动,一律不产生任何行。 - 车队活字典本身(`fleet_vehicle_type` 表)不因本次改动新增/修改任何行,字典维护仍走车队服务既有入口。 --- ## 六、边界行为 - 未登录 → 业务码 `401`(网关包装 HTTP 200)。 - `fleet` 为空数组或缺失 → 582021。 - `vehicleType` 无法归一 → 582022(存量码,未变)。 - `vehicleType` 归一成功但大类已整类下线 → **582032(本次新增)**。 - 车队字典服务不可用(Feign 兜底返回空 Map)→ **582033(本次新增)**,对所有请求 fail-closed,不逐项放行。 - `seats`/`count` 非法 → 582023;座位数不在该大类真实型号座位选项内 → 582024(582024 判定排在字典校验之后,因此大类已下线时不会先看到一个指错方向的座位错误)。 - 存量已落库、含已下线大类的需求原样重新提交同一接口 → 同样被 582032 拒绝,零写入,不因"内容未变"豁免。 - `adjustment/submit` 路径下,`validateVehicleTypesAgainstFleetDict` 内的 Feign 调用运行在 `AdjustmentService.submit` 的物理事务内(`PROPAGATION_REQUIRED` 挂入);这是该路径已有的事务边界,本次改动未改变这一行为,也不在本单修复范围。 --- ## 六.5、枚举 / 数据字典 ### fleet[].vehicleType(车型大类规范 key) **所属字段**: `VehicleRequirementReqVO.fleet[].vehicleType` / `AdjustmentSubmitReqVO.updates.vehicleRequirement.fleet[].vehicleType` / `...transferRequirement.fleet[].vehicleType` | **类型**: String | 值 | 说明 | |----|------| | `suv` | SUV | | `mpv` | MPV / 商务车 | | `bus` | 大巴 | | `sedan` | 轿车 | - 归一表(含中文别名,如"大巴"→`bus`)由 `VehicleCategoryNormalizer` 维护,四值闭集,本次未新增第五类。 - 实际**是否可选**取决于车队活字典当前有哪些大类在架,前端应以 `GET /admin/fleet/vehicle-types/list` 的实时返回为准,不要硬编码这四个 key 一定全部可用。 --- ## 六.6、修改前后对比(修改/删除类接口必写,新增跳过) ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | `fleet[].vehicleType` | 只要能归一为 `suv/mpv/bus/sedan` 之一即接受,不管该大类当前是否在车队还在营 | 归一成功后额外要求该大类在车队活字典内有 ≥1 行在架代表,否则拒 | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 提交一个已被车队整类下线的大类(如运营已下线 `bus`) | 校验通过,正常落库 | 582032 拒绝,零写入 | | 车队字典服务不可用 | 不影响本校验链(该服务原本不参与车型大类校验) | 582033 拒绝,fail-closed | | 存量含已下线大类的需求原样重新提交 | 通过 | 582032 拒绝(与团期批量车务 809119 同口径) | --- ## 六.7、影响评估(修改/删除类必写) - **是否破坏向后兼容**: 部分破坏,且是有意为之。请求/响应结构不变;仅"归一成功但大类已被下线"这一种此前被接受的取值,改为拒绝。影响面严格限定在"大类已整类下线"这一种运营主动下线的场景,不影响任何仍在营的大类。 - **前端是否必须同步上线**: 否,mmg 已提前于本次后端上线(`7df292624` 早于 `b439565be`)改为字典下拉,新提交路径已规避新码。 - **前端 workaround 清理点**: 存量已落库需求在编辑态若命中已下线大类,需前端显式提示"当前值不在字典内,请重新选择"(见"四、契约约束"节),避免下拉渲染成空白且用户无法定位原因。落点组件:`src/views/order-v2/detail/modals/FunItemAdjustModal.vue`。 --- ## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面) - **仅影响**: 两个端点中 `fleet[].vehicleType` 的字典比对这一步;其余校验(座位数、数量、特殊诉求、座位选项)逻辑不变。 - **零影响**: - `VehicleCategoryNormalizer` 的四值归一表与别名规则本身(未新增/删减大类)。 - `adjustment/submit` 中 `people`/`schedule`/`itinerary`/`hotelRequirement` 等与用车无关的子领域。 - 团期批量车务(`GroupVehicleRequirementService`)校验链,809119/809120 已是同口径独立实现,本单未改动团级代码。 - 派单、车队分配、司机端等下游链路的既有行为。 - `GET /admin/fleet/vehicle-types/list` 端点本身(既有只读端点,本单未修改其实现或路由)。 --- ## 八、测试环境已验证 单元测试(`RequirementServiceVehicleTypeDictValidateTest`): - `validateVehicleTypes_retiredCategoryRejectedAndLiveCategoryAccepted`:同一桩字典(刻意不含 `bus`)下,`大巴`(归一为 `bus`)被拒且断言错误码为 582032、非 582022;同一字典下 `suv` 正常放行。 - `validateVehicleTypes_emptyDict_throws582033NotFail582032`:空字典(模拟车队服务不可用)下,`suv` 被拒且断言错误码为 582033、非 582032,报文不含用户填的车型值。 真实网关(TEST 环境)实证: ``` PUT /v3/admin/order/{id}/vehicle-requirement vehicleType=__NOT_A_REAL_CATEGORY__ → 582022 ✓ POST /v3/admin/order/{id}/adjustment/submit vehicleType=__NOT_A_REAL_CATEGORY__ → 582022 ✓ ``` 两端点对同一非法值返回一致的错误码(582022),验证校验链在两个入口上等价生效、归一失败判定优先于字典比对。 部署: PR #8224 已合并 `dev-v3`(合并提交 `b439565be`),TEST 环境双实例滚动部署完成,Nacos 注册均 `healthy:true`。 --- ## 十、相关文档 - 关联 Issue: [wx/HL#8202](https://git.1814.love:8443/wx/HL/issues/8202) - 关联 PR: [wx/HL#8224](https://git.1814.love:8443/wx/HL/pulls/8224) ## 关联 / 联系人 ### 链接 - **Issue**: [#8202](https://git.1814.love:8443/wx/HL/issues/8202) - **PR**: [#8224](https://git.1814.love:8443/wx/HL/pulls/8224) - **Merge commit**: [b439565be](https://git.1814.love:8443/wx/HL/commit/b439565be) ### 联系人 - **后端负责人**: @wx