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 | 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:
SettlementCategoryCheckServiceTest16、SettlementPrototypeFlowContractTest11、SettlementServicePr5Test41、SettlementVehicleFeeSnapshotWriterTest54、LayerEnforcementTest5、MapperBoundaryArchTest26、RedLineArchTest12。 - 两处触发点各补了一条断言渲染后 message 的用例(含
doesNotContain对占位符原文的断言,防止占位符原样露给前端)。
十、相关文档
- 关联 PR: wx/HL#7781
- 关联 Issue:无独立 Issue(PR 备注「无关联工单(顺带发现的两处问题)」)
- 合并提交:
d3d4755e9331243dad0129c2da04466cafc3bca5(已在 dev-v3 主线)
关联 / 联系人
链接
联系人
- 后端负责人: @wx