文件
hl-api-changelog/changelogs-v2/2026-09/16_7781_核单分类未确认提示语补分类名584310-修改接口-管理后台.md
T

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 7781 核单分类未确认提示语补分类名(584310 文案含占位符) admin wx(GIT) 修改接口 deployed not_required not_required 已合并 dev-v3(squash 提交 d3d4755e9,PR #7781),测试服 hl-order-service-v3 已部署该提交(2026-09-16 07:10:22,deploy-status.sh 核实 commit=d3d4755e9、BEHIND=0/N)。触发点一(8 分类循环,HOTEL 命中)已用自造测试订单实测:POST /v3/admin/order/8880000000778101001/settlement/finalize 返回 code=584310、message=核单分类「住宿」明细尚未全部确认或数据已变化,占位符已正确替换为具体分类名。触发点二(SettlementVehicleFeeSnapshotWriter 车辆草稿未确认,分类恒填「车辆」)本次未取证:该分支要求先有真实车辆需求+Fleet 派车+车辆草稿快照(reconcile 需要 Fleet Feign 真实回填数据),用直接 SQL 造数无法安全绕过 draftService.loadCandidate/lockReadySnapshot 的事实一致性比对(会被判 REPORT_SOURCE_CHANGED 而非 584310),需要完整走一遍团期车务派车流程才能自然触发,超出本次取证窗口,未编造该路径证据。本次没有新增 admin 路由,gateway_status=not_required。frontend_status=pending:若 hl-ui 存在对旧文案 八个核单分类尚未全部确认或数据已变化 做精确字符串匹配的逻辑,必须改为按 code=584310 判定,新文案恒含动态分类名,旧字面量不会再出现。本单无独立 Gitea Issue,PR #7781 备注 无关联工单(顺带发现的两处问题)。mmg 2026-09-16 核实:旧文案「八个核单分类尚未全部确认或数据已变化」、584310、CATEGORY_CHECK_INCOMPLETE 在 hl-admin 全仓零引用——前端对该码本就是 request.js 拦截器透 message 原文展示口径,无精确字符串匹配可失效。新文案恒含分类名,透传即得更优提示。判 not_required。 2026-09-16 dev-v3

order-v3: 核单分类未确认提示语补分类名(584310)

存放目录: changelogs-v2/{YYYY-MM}/(管理后台,二期 order-v3)

服务: hl-order-service-v3 (端口 8083) PR: #7781 Issue: 无独立 Issue(PR 备注「无关联工单(顺带发现的两处问题)」) 日期: 2026-09-16 影响范围: 错误码 584310(CATEGORY_CHECK_INCOMPLETE)的 message 文本;code、HTTP 状态(恒 200)、data、请求/响应结构均未改


⚠️ 关键变化

🔴 错误码 584310 的 message 文本变了,code 和 HTTP 状态都没变(HTTP 恒 200,code=584310)。

  • 改前:八个核单分类尚未全部确认或数据已变化(固定文案,不含分类名)
  • 改后:核单分类「{0}」明细尚未全部确认或数据已变化,{0} 由后端填成实际未确认的分类名(取值来自 SettlementCategory 枚举 label,共 8 个分类,见「六.5、枚举」)

🟡 前端如果之前对旧文案「八个核单分类尚未全部确认或数据已变化」做过精确字符串匹配(例如按 message 做特殊提示/埋点),该匹配会永久失效——新文案恒含动态分类名,旧字面量不会再出现。必须改为按 code === 584310 判定,见「四、契约约束与正确调用方式」。

🟢 测试环境已验证(2026-09-16,order-v3 dev-v3 d3d4755e9):触发点一(8 分类循环,HOTEL 命中)自造订单实测通过,返回 code=584310、message=核单分类「住宿」明细尚未全部确认或数据已变化;触发点二(车辆草稿未确认)因需真实 Fleet 派车数据未取证,详见「八、测试环境已验证」。


一、背景

SettlementFinancialErrorCode.CATEGORY_CHECK_INCOMPLETE(584310)原来的模板不带占位符,但两处实际抛出点早就在传分类名参数——参数被静默丢弃(模板没有 {0} 时不经 MessageFormat.format,直接原样返回模板本身)。前端只能看到笼统的「八个核单分类尚未全部确认或数据已变化」,无法定位到底是哪个分类没确认。本单只改模板加占位符,并给两处抛出点分别核实/补传参数,不改判定逻辑本身、不改触发条件。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 完成核单 POST /v3/admin/order/{orderId}/settlement/finalize 错误码文案修正 584310 message 模板补分类占位符;code/HTTP 状态/响应结构不变

三、接口详情

1. 完成核单 POST /v3/admin/order/{orderId}/settlement/finalize

VO: 无请求体 → SettlementSubmitRespVO

使用场景

管理后台核单页「完成核单」按钮:提交主报账、单团核算双指纹,原子冻结车辆事实并把核单标记为终态。本单不改这个端点的请求/响应结构,只改它在两种失败场景下返回的 584310 message 文本。

584310 只能通过这一个端点触发——全仓 CATEGORY_CHECK_INCOMPLETE 共 2 处生产代码抛出点,两处都在本端点的调用链上(见「四、契约约束与正确调用方式」的调用链说明),没有第三条触发路径。

入参字段表

字段 位置 类型 必填 约束 说明
orderId Path Long 是 大于 0 订单 ID;不变

(无请求体,本单未改任何入参。)

出参字段表

字段 类型 说明
summaryId String(雪花 ID) 新写入的 settlement_summary 主键
finalSnapshotId String(雪花 ID) 核单终态快照 ID
finalSnapshotVersionNo Integer 核单终态快照版本号
finalSnapshotStatus String 核单终态快照状态
orderId String(雪花 ID) 订单 ID
settledAt String(LocalDateTime) 核单完成时间
totalAmount String(金额) 订单总金额快照
paidAmount String(金额) 已付金额快照
balanceAmount String(金额) 尾款金额快照
roomCost String(金额) 住宿实际成本
ticketCost String(金额) 门票实际成本
staffCost String(金额) 人员费用实际成本
subsidyCost String(金额) 补助实际成本
mealCost String(金额) 餐食实际成本
vehicleCost String(金额) 车辆基础服务总车费
otherExpenseCost String(金额) 其他支出实际成本
insurancePremium String(金额) 保险实际保费
totalActualCost String(金额) 总实际成本
driverTransferAmount String(金额) 给司机/主报账人转回金额
profitAmount String(金额) 公司毛利
profitRate Number(小数) 毛利率
orderStatusAfter String 结算后订单状态
mqTriggered Boolean 当前版本固定 false
warnings Array of WarningItemVO 软预警列表

以上字段全部来自 SettlementSubmitRespVO 既有定义,本单一个字段都没改;列出仅为满足模板「逐接口自包含」要求。

请求示例

POST /v3/admin/order/2099716954674597889/settlement/finalize HTTP/1.1
Authorization: Bearer {token}

(无请求体,仅 Path 参数 orderId;本单未改。)

响应示例

成功响应结构本单未动,字段值取自源码 @ApiModelProperty(example=...) 声明(本单只改错误响应文案,成功响应本身未实测,见「八、测试环境已验证」):

{
  "code": 200,
  "message": "成功",
  "data": {
    "summaryId": "9600000000001",
    "finalSnapshotId": "9600000000002",
    "finalSnapshotVersionNo": 1,
    "finalSnapshotStatus": "FINALIZED",
    "orderId": "12345678901234",
    "settledAt": "2026-05-26T10:30:25",
    "totalAmount": "24800.00",
    "paidAmount": "24800.00",
    "balanceAmount": "0.00",
    "roomCost": "4280.00",
    "ticketCost": "3680.00",
    "staffCost": "14260.00",
    "subsidyCost": "720.00",
    "mealCost": "860.00",
    "vehicleCost": "5200.00",
    "otherExpenseCost": "1260.00",
    "insurancePremium": "180.00",
    "totalActualCost": "23120.00",
    "driverTransferAmount": "22940.00",
    "profitAmount": "1680.00",
    "profitRate": 0.0677,
    "orderStatusAfter": "待财务复核",
    "mqTriggered": false,
    "warnings": []
  },
  "success": true
}

空数据 / 降级响应

无列表/分页语义,不存在空数据形态。warnings 为空数组时代表无软预警,不阻塞提交:

{ "code": 200, "success": true, "data": { "warnings": [] } }

错误响应

584310 可能由两个不同的判定点触发,都在本端点内,都是本单实际改动点:

触发点 场景 {0} 取值
SettlementCategoryCheckService.validateReadyForReport(hl-order-service-v3/src/main/java/com/hulalv/order/settlement/service/SettlementCategoryCheckService.java:85-92) 按 SettlementCategory 枚举声明顺序(住宿/门票游玩项目/餐食/车辆/导游/摄影/其他收入/其他支出)逐个检查「有明细行但未全部确认」,命中第一个就抛出并停止,不会一次性列出全部未确认分类 8 个分类中,第一个命中的那个
SettlementVehicleFeeSnapshotWriter.replaceAndFreezeDraftInCurrentTransaction(hl-order-service-v3/src/main/java/com/hulalv/order/settlement/service/SettlementVehicleFeeSnapshotWriter.java:250-256) 车辆草稿行存在未确认(settlementConfirmStatus 不等于 CONFIRMED);该方法只处理车辆草稿行,在整个 finalize 调用链里先于上一行的 8 分类循环执行 恒为「车辆」(硬编码 SettlementCategory.VEHICLE.getLabel(),与草稿行具体是哪一条无关)

示例一(车辆草稿未全部确认,命中较早的判定点):

{
  "code": 584310,
  "message": "核单分类「车辆」明细尚未全部确认或数据已变化",
  "data": null,
  "success": false
}

示例二(8 分类循环命中「餐食」,其余分类此前已确认或为空):

{
  "code": 584310,
  "message": "核单分类「餐食」明细尚未全部确认或数据已变化",
  "data": null,
  "success": false
}

改前(两处触发点,本单修复前的固定文案,不区分分类):

{
  "code": 584310,
  "message": "八个核单分类尚未全部确认或数据已变化",
  "data": null,
  "success": false
}

业务边界

  • 584310 只在本端点触发:全仓 CATEGORY_CHECK_INCOMPLETE 只有上表两处生产代码抛出点,且都只能通过 POST /v3/admin/order/{orderId}/settlement/finalize 这一条调用链到达(SettlementController.finalizeSettlement → SettlementFinalizeOrchestrator → SettlementFinalizeLockService → SettlementFinalizeTxService.finalizeSettlement,内部依次调用车辆冻结与 8 分类校验)。
  • 8 分类循环短路:validateReadyForReport 按枚举声明顺序(住宿/门票游玩项目/餐食/车辆/导游/摄影/其他收入/其他支出)遍历,一旦命中未确认分类立即抛出,不会把全部未确认分类一次性列全;前端不能假设 message 里的分类名是唯一一个未确认分类,重试提交、修完这个再确认下一个可能还会拿到另一个分类名的 584310。
  • 空分类不计入判定:某分类若没有任何明细行(rowCount() == 0),不需要确认,不会触发 584310,即使它看起来没被确认过。
  • 车辆草稿分类名恒为「车辆」:即使草稿行本身归属不同需求(如接送机/包车),触发点二的占位符固定填「车辆」,不细分到具体是哪一条草稿行。
  • data 恒为 null:与改前一致,本单未改 Result.error(ec, args) 的调用方式,仍是 code + message,不带附加结构化载荷。
  • 不做兼容双发:后端不会同时给出旧文案和新文案,也不新增字段承载纯分类名——分类名只存在于 message 字符串里,前端要拿分类名必须自己从 message 里按「」定界解析,或者干脆只按 code 判定、把 message 原样展示给用户(推荐做法,见下节)。

四、契约约束与正确调用方式

本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。

正确 / 错误 判定方式对照

场景 判定方式
正确:只用 code 判定是否为核单分类未确认错误 if (resp.code === 584310) { ... }
正确:需要展示分类名时,直接展示 message 原文(后端已经把分类名拼好) showToast(resp.message)
错误:对 message 做精确字符串匹配 if (resp.message === "八个核单分类尚未全部确认或数据已变化"),本单起该字面量永久不会再出现,匹配会一直失败
错误:假设一次 584310 只会报一个唯一未确认分类,据此更新本地已确认分类清单 见业务边界短路说明,可能还有其他未确认分类没被这次报出来

调用链(说明 584310 为什么只挂在这一个端点上)

SettlementController.finalizeSettlement                       (POST /v3/admin/order/{orderId}/settlement/finalize)
 -> SettlementFinalizeOrchestrator.finalizeSettlement
     -> SettlementFinalizeLockService.finalizeSettlement
         -> SettlementFinalizeTxService.finalizeSettlement
             +- SettlementVehicleFreezeService.freezeForFinalizeInCurrentTransaction
             |   -> SettlementVehicleFeeSnapshotWriter.replaceAndFreezeDraftInCurrentTransaction   (触发点二,恒「车辆」)
             +- SettlementService.finalizeInCurrentTransaction
                 -> SettlementReportFlowService.prepareFinalizationInCurrentTransaction
                     -> SettlementCategoryCheckService.validateReadyForReport                       (触发点一,8 分类循环)

前端无需改任何请求参数——本端点入参/路径未变;判定完全由后端按订单当前核单分类确认状态计算。


五、数据库行为

本单不改任何数据库读写行为,属于纯文案/参数传递修正:两处判定的读取逻辑(哪个分类算未确认)本身完全不变,只是把早就存在的 category.getLabel() 参数真正传进消息格式化。finalize 端点既有的核单写入语义(写 settlement_summary/终态快照/状态流水等)不在本单改动范围内,不在此重复。


六、边界行为

  • 未登录 → 401(网关拦截,不变)
  • orderId <= 0 → 400(@Min 校验,不变)
  • 8 个分类全部确认(或没有明细行)→ 不触发 584310,走正常 finalize 流程(不变)
  • 车辆草稿行未全部确认 → 584310,分类名填「车辆」(本单起分类名可见,改前是固定文案)
  • 非车辆分类(住宿/门票游玩项目/餐食/导游/摄影/其他收入/其他支出)有明细行但未全部确认 → 584310,分类名为 8 分类循环里第一个命中的分类名(本单起分类名可见)
  • 老数据兼容:本单不改任何判定条件,存量订单的确认状态数据无需回填、无需迁移,行为按新文案原样生效

六.5、枚举 / 数据字典

所属字段:错误响应 message 中占位符的取值来源(不是请求/响应 JSON 里的字段,message 整体仍是 String)| 枚举类:com.hulalv.order.settlement.enums.SettlementCategory

值(枚举名) label(占位符实际填入的中文) 说明
HOTEL 住宿
TICKET 门票/游玩项目 label 本身含斜杠,不是格式错误
MEAL 餐食
VEHICLE 车辆 触发点二恒填此值;触发点一按遍历顺序命中时也可能填此值
GUIDE 导游
PHOTOGRAPHER 摄影
OTHER_INCOME 其他收入
OTHER_EXPENSE 其他支出

触发点一(validateReadyForReport)按上表从上到下的顺序遍历并短路——VEHICLE 排第 4 位,只有前 3 个分类都确认完毕,才可能在这个循环里报出车辆分类;正常情况下车辆的未确认状态会先被触发点二拦截。


六.6、修改前后对比

字段级对比

字段 改前 改后
code(584310) 584310 584310(不变)
HTTP 状态 200 200(不变,恒 @ResponseStatus(HttpStatus.OK))
message 模板(SettlementFinancialErrorCode.CATEGORY_CHECK_INCOMPLETE) 八个核单分类尚未全部确认或数据已变化(无占位符) 核单分类「{0}」明细尚未全部确认或数据已变化(占位符为分类 label)
data null null(不变)

行为级对比

场景 改前返回的 message 改后返回的 message
车辆草稿未全部确认 八个核单分类尚未全部确认或数据已变化 核单分类「车辆」明细尚未全部确认或数据已变化
餐食有明细行但未全部确认(其余分类已确认) 八个核单分类尚未全部确认或数据已变化 核单分类「餐食」明细尚未全部确认或数据已变化
8 个分类全部确认 不触发 584310(不变) 不触发 584310(不变)

六.7、影响评估

  • 是否破坏向后兼容:code/HTTP 状态/data 结构均未改,接口契约的机器可读部分向后兼容;但 message 的字面量值变了——若前端按旧文案精确字符串匹配,这类匹配会被破坏(见下)。
  • 前端是否必须同步上线:视前端现有实现而定。若 hl-ui 对 584310 的 message 做过精确字符串匹配(不管是用于弹窗文案分支还是埋点),必须同步改为按 code === 584310 判定;若前端本就是拿到 584310 直接展示 message 原文、不做字符串匹配的口径,则零改动。本单未接触 hl-ui 代码库,无法从后端仓库判定该假设是否成立,需前端自行核实(对应 frontend_status: pending)。
  • 前端 workaround 清理点:若前端此前因为旧文案笼统、不知道是哪个分类而做过额外的提示语兜底(例如统一显示请检查全部 8 个分类),现在后端已经把具体分类名带出来,这类兜底提示可以清理、直接透传 message。

七、不影响范围

  • 仅影响:584310(CATEGORY_CHECK_INCOMPLETE)这一个错误码的 message 文本。
  • 零影响:
    • POST /v3/admin/order/{orderId}/settlement/finalize 的成功响应结构(SettlementSubmitRespVO 全部 24 个字段)——本单一个字段都没碰。
    • 584310 的 code 值、触发条件、HTTP 状态——判定逻辑本身完全不变,只是把已经存在的参数真正传进消息模板。
    • 该错误码段(584300-584399)内其余错误码(如 584311 主报账表尚未生成、584318 空分类必须显式确认等)——本单未动。
    • 核单页的其余接口(应收总览、报账表、单团核算表、反确认等)——本单只改了 finalize 一个端点能返回的一个错误码文案。
    • 小程序端(consumer: mp)——本端点为管理后台端点,小程序无影响。

八、测试环境已验证

部署确认(2026-09-16,deploy-status.sh 经 SSH 在测试服执行):

  SERVICE                  BRANCH                       COMMIT    BEHIND    DEPLOYED_AT         BY                     STATE
  hl-gateway               dev-v3                       6a7b43a17 29/Y      2026-09-15 12:16:26 root@local(python3)    ok
  hl-order-service-v3      dev-v3                       d3d4755e9 0/N       2026-09-16 07:10:22 root@192.168.100.168   ok

order-v3 当前跑的就是 PR #7781 的 squash 合并提交 d3d4755e9(BEHIND=0/N,dev-v3 之后没有再碰本服务的提交);hl-gateway 也在 dev-v3(本单未新增路由,无需网关改动)。

触发点一实测(8 分类循环,HOTEL 命中,SettlementCategoryCheckService.validateReadyForReport):

自造测试订单 order_id=8880000000778101001(order_no=HL7781FIXTURE01,customer_remark=#7781 584310 取证),造数:order_main(review_status=PENDING、paid_amount=0.00/deposit_amount=0.00 满足对账一致性前置)、order_settlement_hotel 一条 settlement_confirm_status=UNCONFIRMED 的住宿明细行(字段齐全,满足 readyLine 数据质量判据)、order_settlement_category_check 八个分类各一条 source_sync_status=READY(满足 readyForReport 前置,仅 HOTEL 有明细行)。

请求:

POST https://api.test.1814.love:9443/v3/admin/order/8880000000778101001/settlement/finalize HTTP/1.1
Authorization: Bearer {token}

请求时刻:2026-09-16 07:27:07

响应:

{
  "code": 584310,
  "message": "核单分类「住宿」明细尚未全部确认或数据已变化",
  "data": null,
  "traceId": null,
  "success": false
}

code=584310、message 占位符已正确替换为「住宿」(不含 {0} 字面量)、data=null,与源码预期一致。

触发点二未实测(车辆草稿未确认,SettlementVehicleFeeSnapshotWriter.replaceAndFreezeDraftInCurrentTransaction,分类恒填「车辆」):

该分支要求先有真实车辆需求(TRAVEL/TRANSFER)+ Fleet 已完结派车数据,finalize 在事务外先调用 SettlementVehicleFeeService.prepareFreezeCandidateForReport(经 SettlementVehicleDraftService.loadCandidate/reconcile 读 Fleet Feign 真实回填数据生成候选),事务内再由 SettlementVehicleFreezeService.freezeForFinalizeInCurrentTransaction 用 draftService.lockReadySnapshot 重新锁定一份快照,与候选比对不一致会先报 REPORT_SOURCE_CHANGED(而非 584310)。直接 SQL 向草稿表插入「未确认」行绕不过这道事实一致性校验(构造的草稿数据与 Fleet 真实候选对不上),需要完整走一遍团期车务派车(建需求→派车→回填每日车费→草稿 reconcile)才能自然产生「草稿行存在但未全部确认」的状态,超出本次取证窗口,未编造该路径证据。本单代码逻辑已按源码复核(见「三、接口详情」表格与 SettlementVehicleFeeSnapshotWriter.java:250-256),仅缺实测响应。

本地/CI 编译与单测证据(非测试环境实测,仅作为代码正确性的辅助信息):

  • mvn -o -pl hl-order-service-v3 -am test-compile 通过。
  • 定向单测合计 165/0/0/0:SettlementCategoryCheckServiceTest 16、SettlementPrototypeFlowContractTest 11、SettlementServicePr5Test 41、SettlementVehicleFeeSnapshotWriterTest 54、LayerEnforcementTest 5、MapperBoundaryArchTest 26、RedLineArchTest 12。
  • 两处触发点各补了一条断言渲染后 message 的用例(含 doesNotContain 对占位符原文的断言,防止占位符原样露给前端)。

十、相关文档

  • 关联 PR: wx/HL#7781
  • 关联 Issue:无独立 Issue(PR 备注「无关联工单(顺带发现的两处问题)」)
  • 合并提交:d3d4755e9331243dad0129c2da04466cafc3bca5(已在 dev-v3 主线)

关联 / 联系人

链接

  • Issue: 无独立 Issue(见上)
  • PR: #7781
  • Merge commit: d3d4755e9(已在 dev-v3 主线)

联系人

  • 后端负责人: @wx