23 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 | 8121 | 核单清单「用车安排」逐类判定,包车+接送机并存不再静默放行 | admin | wx(GIT) | 修改接口 | deployed | not_required | not_required | backend_status: deployed - hl-order-service-v3 COMMIT=7811104b4 BEHIND=0 STATE=ok(2026-09-22 AC-3 取证); gateway_status: not_required - 本单零网关改动,GET /v3/admin/order/{orderId}/confirm-checklist 是既有路由,走 hl-gateway 既有 /v3/admin/** 通配断言(application.yml:220-223),无新增 /admin/ 端点需要配路由; frontend_status: pending - 前端适配情况未知,后端不代填 前端核验(2026-09-22, mmg): ConfirmChecklistModal 按 passed===false 过滤、failReason 纯展示不做精确匹配、checkName 缺省兜底 code、动作按 code 映射(confirmChecklistActions.js),纯展示场景零改动;owner/ref/verified_at 按 not_required 口径留空。 | 2026-09-22 | dev-v3 |
order-v3: 核单清单「用车安排」逐类判定,包车+接送机并存不再静默放行
存放目录: 二期(v3) →
changelogs-v2/{YYYY-MM}/服务: hl-order-service-v3 (端口 8033) Issue: #8121 日期: 2026-09-22 影响范围: 管理后台「订单详情」→「确认核单」清单;接送机需求与行程用车并存的订单
⚠️ 关键变化
缺陷修复:同一订单同时买「行程用车(包车)」和「接送机」时,之前只要包车那类先办完,整个 VEHICLE_DONE 项就判 passed=true —— 接送机零派车也照样放行,没有异常、没有错误码,清单上是一个绿勾,订单就这么被确认掉了。修复后两类需求各自独立判定,任一类未就绪则整体 passed=false,且 failReason 点名是哪一类。
一、背景
同一张订单可以包含多个用车需求类别(行程用车、接送机)。原判定逻辑使用"单值投影"——只选中其中一类进行判定,导致当两类并存且其中一类已完成时,另一类的未就绪状态被静默忽略。核单清单是车务部门唯一的缺陷定位入口,绿勾放行的订单实际上出数不全,造成订单被错误地推入后续流程。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 订单确认核单清单 | GET | /v3/admin/order/{orderId}/confirm-checklist |
响应字段语义变化 | VEHICLE_DONE 改为逐类判定(两类并存时不再静默判过);items[*].failReason 未就绪时点名用车类别。响应结构不变 |
三、接口详情
1. 订单确认核单清单 GET /v3/admin/order/{orderId}/confirm-checklist
VO: OrderDetailService#getConfirmChecklist(Long) → ConfirmChecklistRespVO
使用场景
管理后台订单详情页面,确认前的最终核对清单。车务部门通过此清单判断订单是否已具备所有必要条件(付款、房间、用车、合同等),逐项绿勾后方可点击「确认」按钮推动订单进入后续流程。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
orderId |
Path | Long | ✅ | 订单必须存在 | 订单 ID |
出参 Result<ConfirmChecklistRespVO>
外层信封 Result:code(成功恒 200)、message、data、traceId、success(由 isSuccess() 序列化,等价于 code == 200),依据 hl-common/hl-common-core/src/main/java/com/hulalv/common/result/Result.java:21-45。
| 字段 | 类型 | 说明 |
|---|---|---|
allPassed |
Boolean | 是否五项全部通过,是 items / preview 的唯一开关(ConfirmChecklistRespVO.java:23-24、OrderDetailService.java:424-427) |
items |
List<ChecklistItemVO> | allPassed=false 时返回全部 5 项(含 passed=true 的项,不是只返回失败项);allPassed=true 时为 null(OrderDetailService.java:429) |
preview |
PreviewVO | allPassed=true 时返回确认弹框预览;allPassed=false 时为 null(OrderDetailService.java:430-432) |
PreviewVO 结构(仅 allPassed=true 时有值)
| 字段 | 类型 | 说明 | 源码依据 |
|---|---|---|---|
departureDate |
LocalDate | 出发日期,取订单 departDate |
OrderDetailService.java:1116 |
totalPeopleCount |
Integer | 总出行人数:优先按 order_traveler 实际条数;为 0 时回落「成人+儿童+幼儿+婴儿」 |
OrderDetailService.java:1117-1124 |
driverName |
String | 司机姓名。团车路径(车在团级承担、逐户无配车行)该栏留空 | OrderDetailService.java:1127-1147 |
driverPhoneMasked |
String | 司机手机(脱敏) | 同上 |
hotels |
List<HotelSummaryVO> | 元素为 {cityName, hotelName};按 dayNumber 升序,相邻「城市+酒店」相同的去重;无配房时为空数组 |
OrderDetailService.java:1151-1182 |
staffs |
List<StaffItemVO> | 本单人员:assignmentId / staffId(两个 Long 均序列化成字符串)/ staffName / staffPhone(已脱敏)/ staffRole / staffRoleName / isPrimaryReporter |
ConfirmChecklistRespVO.java:83-107、OrderDetailService.java:1209-1230 |
contractAutoAction |
对象 | {planName, autoSign},autoSign 恒 true(确认后自动发起线上签署);无 ACTIVE 合同方案时整个对象缺省 |
OrderDetailService.java:1187-1194 |
insuranceAutoAction |
对象 | {planName, peopleCount, effectiveDescription},effectiveDescription 是固定文案「出发前 24h 内生效」;无 ACTIVE 保险方案时整个对象缺省 |
OrderDetailService.java:1198-1205 |
preview没有rooms/drivers/notificationList这三个字段:通知块已于 #4844 删除(OrderDetailService.java:1232-1233),酒店与司机分别是上表的hotels与driverName/driverPhoneMasked。
请求示例
GET /v3/admin/order/2101908228545937410/confirm-checklist HTTP/1.1
Authorization: Bearer <token>
ChecklistItemVO 结构
| 字段 | 类型 | 说明 | 源码依据 |
|---|---|---|---|
code |
String | 检查项代码,5 个固定取值(见下表) | ConfirmChecklistRespVO.java:36-38 |
checkName |
String | 检查项中文名(见下方 ⚠️) | ConfirmChecklistRespVO.java:40-41 |
passed |
Boolean | 该项是否通过 | ConfirmChecklistRespVO.java:43-44 |
failReason |
String | 未通过原因;通过时不赋值(为 null) |
ConfirmChecklistRespVO.java:46-47 |
items 的 5 项及其数组顺序(OrderDetailService.java:392-422 按此顺序 add):
| # | code |
checkName |
源码依据 |
|---|---|---|---|
| 1 | PAYMENT_OK |
款项校验 | OrderDetailService.java:394-395 |
| 2 | TRAVELER_COMPLETE |
出行人信息 | OrderDetailService.java:897(code)/ :405(checkName) |
| 3 | HOTEL_DONE |
房型安排 | OrderDetailService.java:946 / :410 |
| 4 | VEHICLE_DONE |
用车安排 | OrderDetailService.java:1009 / :416 |
| 5 | CONTRACT_TEMPLATE_OK |
合同方案配置 | OrderDetailService.java:1089 / :421 |
⚠️ checkName 不要当必有字段用:源码对五项都调了 setCheckName(行号见上表),但第八章那条逐字实测读数里只有 VEHICLE_DONE 带 checkName,其余四项该键缺省——两者对不上,成因本轮没有查清。展示层请以 code 为主键并用它兜底文案,checkName 只作可选补充。
响应示例①:全通过(allPassed=true|格式示意)
items 置 null,preview 有值。下面的字段名取自 VO 源码,值用的是 @ApiModelProperty 上的示例值(ConfirmChecklistRespVO.java:55-142),不是某一单的实测读数:
{
"code": 200,
"message": "成功",
"data": {
"allPassed": true,
"items": null,
"preview": {
"departureDate": "2026-05-30",
"totalPeopleCount": 14,
"driverName": "扎西师傅",
"driverPhoneMasked": "1398761****",
"hotels": [{ "cityName": "拉萨", "hotelName": "瑞吉度假酒店" }],
"staffs": [
{
"assignmentId": "1234567890123456789",
"staffId": "9876543210987654321",
"staffName": "扎西师傅",
"staffPhone": "1398761****",
"staffRole": "DRIVER",
"staffRoleName": "司机",
"isPrimaryReporter": true
}
],
"contractAutoAction": { "planName": "标准跟团方案 v3.2", "autoSign": true },
"insuranceAutoAction": {
"planName": "安联境内旅行险 · 尊享版",
"peopleCount": 14,
"effectiveDescription": "出发前 24h 内生效"
}
}
}
}
响应示例②:接送机未就绪(实测原文,逐字)
订单 2101908228545937410(行程用车 DONE + 接送机 PENDING),2026-09-22 测试服 COMMIT=7811104b4 只读 GET 的原始读数:
{"code":200,"data":{"allPassed":false,"items":[{"code":"PAYMENT_OK","passed":true},{"code":"TRAVELER_COMPLETE","passed":true},{"code":"HOTEL_DONE","passed":false,"failReason":"用房需求未完成(当前: PENDING)"},{"code":"VEHICLE_DONE","checkName":"用车安排","passed":false,"failReason":"接送机需求未完成(镜像: DONE,当前需求: PENDING)"},{"code":"CONTRACT_TEMPLATE_OK","passed":true}],"preview":null}}
照着它写代码要注意两点:
items里五项都在,通过的项也在数组里(passed=true)——不要按「出现在数组里就是失败项」渲染,要按passed=false筛。- 这条读数的信封只有
code与data两个键;Result本身还带message/success/traceId(Result.java:21-45)。按data取值、按code判成败即可,不要依赖信封里某个键一定出现。
响应示例③:行程用车未就绪(格式示意,本轮无活体读数)
第八章五单覆盖的是「接送机侧未就绪」与「两类均就绪」,「行程用车未就绪」这一分支本轮没有活体读数。下面只给 items 数组里 VEHICLE_DONE 那一项的片段,文案按源码字符串模板(OrderDetailService.java:1060-1061)逐字拼出,同轮其余四项照常返回:
{
"code": "VEHICLE_DONE",
"checkName": "用车安排",
"passed": false,
"failReason": "行程用车需求未完成(镜像: PENDING,当前需求: PENDING)"
}
空数据 / 降级响应
无空数据场景:data 恒是一个 ConfirmChecklistRespVO 对象,allPassed 必有值,items 与 preview 按 allPassed 互斥其一为 null(OrderDetailService.java:427-432)。
无降级分支:调用链只读 order-v3 同进程的数据(活跃用车需求、配车记录、整团免车判定、房态、行程日、合同方案、保险方案、人员候选、出行人计数),不经跨服务 Feign,所以没有「下游挂了返回兜底值」这种形态。
订单不存在不是空数据,走错误响应(HTTP 200 + body code=581007),见下节。
错误响应
HTTP 状态恒 200,成败看 body 里的 code。 两处依据:
- 业务异常
BusinessException由全局 advice 处理,处理方法上标的是@ResponseStatus(HttpStatus.OK),body 为Result.error(code, message)(hl-common/hl-common-log/src/main/java/com/hulalv/common/exception/GlobalExceptionHandler.java:179-205)。 - 网关鉴权失败同样是 HTTP 200,401 / 403 只出现在 body 的
code里(hl-gateway/src/main/java/com/hulalv/gateway/filter/JwtAuthFilter.java:543-563,该写入器注释原文:「HTTP status 永远 200 (项目铁规)」)。
body code |
message | 触发条件 | 源码依据 |
|---|---|---|---|
200 |
成功 | 正常返回 | Result.java:47-53 |
581007 |
订单不存在 | orderId 查不到(判定在权限校验之前) |
OrderCoreErrorCode.java:17-19、OrderDetailService.java:778-782 |
581008 |
无权查看此订单 | 非 ADMIN / SUPER_ADMIN / VEHICLE_MANAGER,且当前操作人不是本单定制师(adminId != consultantId) |
OrderCoreErrorCode.java:21-23、OrderViewGuard.java:99-102 与 :128-137 |
581045 |
房务角色无权查看订单详情,房务仅可配房 | 角色为 ROOM_MANAGER / house_keeper_lead,Controller 入口即拒 | OrderController.java:250-256、OrderViewGuard.java:67-79、OrderCoreErrorCode.java:211-213 |
401 |
网关鉴权文案 | 未登录 / token 失效,请求被网关 JwtAuthFilter 拦在本服务之外 | JwtAuthFilter.java:543-545 |
错误 body 的关键字段(以订单不存在为例):
{ "code": 581007, "message": "订单不存在", "data": null }
业务边界
- 鉴权是两道门:① Controller 入口
OrderViewGuard.assertNotHouseRole(),房务管理员 / 房务组长 → 581045(OrderController.java:250-256);②requireOrderById内OrderViewGuard.assertOrderReadable(entity),ADMIN / SUPER_ADMIN / VEHICLE_MANAGER 放行,其余角色必须是本单定制师,否则 581008(OrderDetailService.java:778-787、OrderViewGuard.java:99-102)。 - 零角色账号可读:网关没透传
X-Admin-Role时(零角色 admin 账号签发的 token),assertNotHouseRole按「无角色 → 放行」处理(OrderViewGuard.java:67-72与该类 javadoc 的「已知例外」段)。前端不要把本接口能否调通当作角色可见性的判据。 - 资源不存在:
orderId查不到 → HTTP 200 +code=581007,不返回空对象(OrderDetailService.java:778-782)。 - 只读:入口标
@Transactional(readOnly = true)(OrderDetailService.java:369);本轮逐个核过它直接调用的协作方法(活跃用车需求、配车记录、整团免车判定、房态、行程日、合同方案、保险方案、人员候选、出行人计数),方法体内都是查询口,未见写库分支。
四、契约约束与正确调用方式
前端须知的 failReason 三个分支及其精确拼法
本接口响应的 failReason 字段有三种不同的文案模板。分支 2 与分支 3 的前缀拼法不同,前端若对此字符串做精确匹配,需要逐一处理:
| 分支 | 触发条件 | failReason 格式 | 示例 |
|---|---|---|---|
| 1 | 订单未提交任何用车需求 | 未提交用车需求 |
未提交用车需求 |
| 2 | 镜像或需求 status 非 DONE | {类别中文名}需求未完成(镜像: {镜像值},当前需求: {需求值}) |
行程用车需求未完成(镜像: PENDING,当前需求: PENDING) / 接送机需求未完成(镜像: DONE,当前需求: PENDING) |
| 3 | 无有效配车记录 | {类别中文名}未找到有效配车记录 |
行程用车未找到有效配车记录 / 接送机未找到有效配车记录 |
关键限定一:分支 2 的前缀是 {类别中文名}需求未完成…,分支 3 的前缀是 {类别中文名}未找到有效配车记录(无"需求"二字)。如果前端要按类别名跳转到对应 Tab,正则 ^{类别中文名} 能覆盖两个分支,但若要区分具体原因,需要分别匹配两个不同的字符串模式。
前端须知:failReason 指错的特殊情形
关键限定二(已知缺口,不是本次改动遗漏,而是现有设计的短路条件决定的):
当且仅当订单的镜像字段
order_main.vehicle_control_status不是 DONE、且该订单存在「行程用车」类活跃需求时,failReason恒点名「行程用车」—— 即使真正落后的是接送机。
原因:判定短路条件是 !镜像DONE || !该类自己DONE(OR),镜像非 DONE 时循环在第一条就返回,而活跃需求列表按枚举声明序排(TRAVEL 先于 TRANSFER),所以点名永远是行程用车。
反过来的可信范围(前端可用):
- 镜像
vehicle_control_status为 DONE 时,每一类按自己的 status 独立判定,failReason点名准确。实测读数:接送机需求未完成(镜像: DONE,当前需求: PENDING)。 - 订单不存在行程用车类活跃需求时(纯接送机单),循环第一条就是接送机,点名同样准确。
可操作的建议:想用 failReason 做「跳转到对应 Tab」的深链是可行的,但在镜像非 DONE 且存在行程用车需求这个窗口内需要额外逻辑 —— 比如先读全量需求列表,找出真正未就绪的那一类;或者让车务点击清单项后弹框让他选择哪一类的问题。不要写成「这个字段永远不可信」,那会让前端整个放弃深链功能,而大多数情形(镜像 DONE 或无行程用车需求)点名是准确的。
六、边界行为
- 全通过时
items为null(既有契约,不是本单改动:该写法自 2026-07-09a2b5e8cb3起就在,OrderDetailService.java:429):前端按allPassed判分支,items === null时只读preview,不要无条件遍历items。 - 未通过时
items是全量 5 项:含passed=true的项,失败项靠passed=false筛(OrderDetailService.java:392-422,第八章实测读数同)。 - 未登录:网关 JwtAuthFilter 拦截,HTTP 200 + body
code=401(JwtAuthFilter.java:543-563)。 - 资源不存在:HTTP 200 + body
code=581007。 - 权限不足:HTTP 200 + body
code=581008(非本单定制师)或code=581045(房务角色)。 - 不需要用车 / 整团免车:
VEHICLE_DONE直接passed=true且无failReason——订单needs_vehicle=false(OrderDetailService.java:1011-1013),或所在团已声明整团免车(OrderDetailService.java:1016-1018)。 - 需要用车但一条需求都没提:
VEHICLE_DONEpassed=false、failReason="未提交用车需求",这是正常返回不是异常(OrderDetailService.java:1023-1026)。 - 下游服务降级:无,调用链全在 order-v3 同进程,不经 Feign。
六.5、枚举
用车类别(VehicleRequirementKind)
所属字段: failReason 中的类别中文名前缀 | 类型: String
| 枚举值 | 中文名 | 说明 |
|---|---|---|
TRAVEL |
行程用车 | 团期行程用车,服务日冻结为行程日 |
TRANSFER |
接送机 | 接送机,服务日取航班/车次日期 |
六.6、修改前后对比
「改前」指的是本次提交 7811104b4 的父提交(#8056 已合入之后的状态),不是更早的历史版本。
| 行为 | 改前 | 改后 |
|---|---|---|
| 同一订单同时有行程用车 + 接送机,行程用车已 DONE、接送机未就绪 | VEHICLE_DONE passed=true(错误),清单放行 |
passed=false,failReason 点名接送机(需求未 DONE 走分支 2、缺配车记录走分支 3),清单拦截 |
failReason 文案 |
无类别前缀:用车需求未完成(镜像: X,当前需求: Y) / 未找到有效配车记录 |
两个失败分支都带类别中文名前缀,拼法见第四章三分支表 |
本次改动只落在上面两行:items / preview 的互斥语义、5 项的 code 与 checkName、preview 的字段集合都没有变(依据:7811104b4 只改了 OrderDetailService / VehicleRequirementKind / RequirementService 与一份单测,ConfirmChecklistRespVO 与 OrderController 零改动)。
六.7、影响评估
- 响应结构:无变化。
allPassed/items/preview的字段与互斥语义、5 项的code与checkName均与改前一致。 - 是否破坏向后兼容:只在
failReason文案上。改后两个失败分支的文案前面多了类别中文名(行程用车/接送机),前端若对该字符串做精确匹配会失配;只当文案展示则不受影响。 - 前端是否必须同步上线:仅当前端对
failReason做了精确匹配或前缀判断时需要同步;纯展示场景无需改动。 - 判定口径收紧:两类需求并存的订单,改前可能
passed=true、改后passed=false。前端不用改代码,但页面上会看到以前能确认的单现在被清单拦住——这是本次修复的预期结果。
七、不影响范围
- 仅影响: 管理后台订单详情「确认核单」清单页面
- 零影响:
- 订单创建接口
- 订单列表、详情查询(非确认流程)
- 支付、房间、合同等其他核单项
- 前端对用车需求的编辑操作(创建、修改、删除需求的接口无改)
- 历史数据(已确认的订单数据无回溯影响)
八、测试环境已验证
2026-09-22 在测试服(hl-order-service-v3 COMMIT=7811104b4、BEHIND=0、STATE=ok)用只读 GET 取证,五单:
| 订单 | 形态 | VEHICLE_DONE.passed |
failReason 实测原文 |
|---|---|---|---|
2101908228545937410 |
行程用车 DONE + 接送机 PENDING | false |
接送机需求未完成(镜像: DONE,当前需求: PENDING) |
2098374030837829634 |
两类需求都已派车 | true |
null |
2101146798373339137 |
全项就绪 | — | 整个 items 为 null |
2087157633055064066 |
只有行程用车(TRAVEL-only) | true |
null |
2101018930892972033 |
只有接送机(TRANSFER-only) | true |
null |
2101908228545937410 的完整响应(逐字原始读数):
{"code":200,"data":{"allPassed":false,"items":[{"code":"PAYMENT_OK","passed":true},{"code":"TRAVELER_COMPLETE","passed":true},{"code":"HOTEL_DONE","passed":false,"failReason":"用房需求未完成(当前: PENDING)"},{"code":"VEHICLE_DONE","checkName":"用车安排","passed":false,"failReason":"接送机需求未完成(镜像: DONE,当前需求: PENDING)"},{"code":"CONTRACT_TEMPLATE_OK","passed":true}],"preview":null}}
这五单覆盖不到的地方:它们全是「接送机侧未就绪」或「两类均就绪」,「行程用车未就绪」那一分支本轮没有活体读数。第三章「响应示例③」里该分支的文案是按源码字符串模板(OrderDetailService.java:1060-1061)拼出的格式示意,不是实测读数;前端要对它做精确匹配的话,以源码模板为准。
十、相关文档
本单契约的源码落点(origin/dev-v3,供对照取证):
- 接口入口:
hl-order-service-v3/src/main/java/com/hulalv/order/core/controller/admin/OrderController.java:250-256 - 清单装配:
hl-order-service-v3/src/main/java/com/hulalv/order/core/service/OrderDetailService.java:369-432 - 用车逐类判定与
failReason拼法:同上文件:1006-1080 - 响应 VO:
hl-order-service-v3/src/main/java/com/hulalv/order/core/controller/admin/vo/ConfirmChecklistRespVO.java - 错误码:
hl-order-service-v3/src/main/java/com/hulalv/order/errorcode/OrderCoreErrorCode.java:17-23、:211-213 - 权限守卫:
hl-order-service-v3/src/main/java/com/hulalv/order/core/guard/OrderViewGuard.java:67-79、:99-137 - 用车类别枚举:
hl-order-service-v3/src/main/java/com/hulalv/order/requirement/enums/VehicleRequirementKind.java
工单与提交链接见下方「关联 / 联系人」。
关联 / 联系人
链接
联系人
- 后端负责人: @wx