文件
hl-api-changelog/changelogs-v2/2026-09/22_8121_核单清单用车安排逐类判定包车接送机并存不再静默放行-修改接口-管理后台.md
T
2026-09-22 06:27:19 +08:00

23 KiB
原始文件 Blame 文件历史

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-09 a2b5e8cb3 起就在,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_DONE passed=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