20 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 | 8248 | 尾款收款模型重构:确认行程主报账人必填化 + 核单欠收硬闸按主报账人豁免 + 财务应收台账 | admin | yst | 修改接口 | deployed | verified | verified | mmg | b556da24791ca0ecf2393ef0e8505989c7996f8a | v2.1 | 2026-09-24 | 尾款收款模型重构 Epic(方案甲·全实时)。PR-6a #8274(581062) + PR-5a #8263(584082) + PR-3 #8207 合并 + PR-5b #8325/②#8336/③#8328 部署。部署:hl-order-service-v3 dev-v3 @ baf643412,2026-09-24 滚动部署(8086/8186 均 UP)。部署后 fin↔order 对账 D1~D7 现网零漂移。 | 2026-09-24 mmg 交付:①LockModal 无主报账人选人必填化(星号+引导+置灰双保险);②两接口 JSDoc 补 581062/584082 新口径;③新增应收台账只读页(api/finance/receipt.js+finance/receipt/receivable,原型 recv-ledger 对齐,代收欠收互斥直显,订单号链接钻取);spec 5+3+2 例全绿;⚠菜单「收款管理/应收台账」依赖后端 sys_menu 补配(changelog 未提及,请后端补配) | 2026-09-24 | dev-v3 |
尾款收款模型重构:确认行程主报账人必填化 + 核单欠收硬闸豁免 + 财务应收台账
存放目录: 二期(v3) →
changelogs-v2/2026-09/服务: hl-order-service-v3(订单核心域 + 核单域)· hl-finance(收款域) Issue: wx/HL#8248 (主) · wx/HL#8265 · wx/HL#8191 PR: wx/HL#8274 · wx/HL#8263 · wx/HL#8207 日期: 2026-09-24 影响范围: 管理后台「订单确认行程弹框」「核单提交」「财务应收台账」三处
⚠️ 关键变化(前端必读)
- 确认行程前必须先有主报账人:普通订单点「确认行程」时,如果本单还没有主报账人且弹框里也没选人,接口直接报 581062「请先指定本单主报账人(尾款代收人)」,确认动作不发生。确认弹框必须提供主报账人选择项并传
reporterAssignmentId。 - 核单欠收硬闸放行面扩大:提交核单时「尾款未收齐」不再一律拦死——只要本单已指定主报账人,欠收部分记为主报账人代收口径,允许提交核单;只有「欠钱且无人兜底(无主报账人)」的单才会继续被 584082 拦截。
- 新增财务应收台账只读分页接口:
GET /admin/finance/receipt/receivable/page,订单维度看应收/已收/代收/欠收,代收列与欠收列互斥(指定主报账人后欠收挪入代收)。
一、接口背景
尾款收款模型重构 Epic(方案甲·全实时,不建债表)。旧模型里尾款由司导线下代收,经过「现场垫付 → 报销 → 核单 → 支付完成」长链路后才回写订单已付金额,导致:
- 核单时「尾款未收齐」一律硬拦,司导已代收但还没走完报销回写的单被卡死;
- 财务看不到「这笔钱到底在谁手里」——是客户还欠着,还是司导代收未回款。
新模型的三条规则:
- 金额实时算:应收/已收/欠收全部实时计算,不落地中间债表;
- 归属实时读主报账人:订单层有主报账人(reporterRank=PRIMARY)时,未收齐部分视为「主报账人代收中」;没有主报账人时才是「客户欠收」;
- 结清看核单终态:核单完成(FINALIZED)后由财务域支付完成事件回写已付金额,闭环。
本次三个接口分别对应:确认行程时把「主报账人」变成前置条件(接口1)、核单欠收硬闸按主报账人豁免(接口2)、财务侧新增应收台账把代收/欠收分列展示(接口3)。
二、变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 确认行程 | POST | /v3/admin/order/{orderId}/confirm-itinerary |
行为变更 + 新错误码 | 确认前必须已有主报账人,否则报 581062;入参出参结构不变 |
| 2 | 提交核单(完成核单) | POST | /v3/admin/order/{orderId}/settlement/finalize |
行为变更 | 584082 只拦「无主报账人且欠收」的单;有主报账人放行,欠收走代收口径留痕 |
| 3 | 财务应收台账分页 | GET | /admin/finance/receipt/receivable/page |
新增接口 | 订单维度应收/已收/代收/欠收分页,只读 |
说明:接口2 任务背景里常被称为「核单提交/生成报账」链路,硬闸实际落在「完成核单」写接口上;
GET /v3/admin/order/{orderId}/settlement/reports/reimbursement(查询主报账人报账表)出参结构无任何变化。
三、接口详情
接口1:确认行程 POST /v3/admin/order/{orderId}/confirm-itinerary
使用场景
订单详情页点「确认行程」,把订单从「定制中」推进到「待出行」。普通订单在确认弹框中选择本单主报账人(尾款代收人)后提交;团期子订单由团期扇出自动带主报账人,一般无需选择。
- 认证:管理后台 JWT
- 幂等性:非幂等写操作,重复确认会被状态机拦截(订单已不在「定制中」)
- 限流:无
入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| orderId | Path | Long | 是 | 订单 ID |
| reporterAssignmentId | Body | Long | 否 | 新的主报账人人员安排 ID;不传则沿用当前主报账人。注意:本单当前没有主报账人时,不传会被 581062 拦截 |
请求体整体可空({} 或不传 body),但仅当订单已有主报账人时才能通过。
出参 Result<OrderTransitionRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| success | Boolean | 是否成功 |
| oldStatus | String | 变更前订单状态(确认成功时恒为 CUSTOMIZING) |
| newStatus | String | 变更后订单状态(确认成功时恒为 PENDING_DEPARTURE) |
| oldFlowStatus | String | 变更前流程状态 |
| newFlowStatus | String | 变更后流程状态 |
| triggeredEvents | Array | 本次转换派生的事件列表 |
业务边界
- 仅「定制中」订单可确认;5 项前置 checklist 未全过时仍报 581036(既有行为不变)。
- 传了
reporterAssignmentId会在确认前先把该人员置为主报账人,再校验主报账人存在性——即「弹框选人」与「确认」是一步完成的。 - 团期子订单:团期人员扇出后订单层已有主报账人副本,不传
reporterAssignmentId也放行;扇出延迟窗口期(极短)可能暂无主报账人被 581062 拦截,稍候重试即可。 reporterAssignmentId传非数字/非法格式报 581046(既有行为不变)。
示例
典型成功(确认弹框选了主报账人)见「八、示例」8.1;无主报账人被拦见 8.3。
接口2:提交核单(完成核单)POST /v3/admin/order/{orderId}/settlement/finalize
使用场景
核单页核对完主报账、单团核算后点「提交核单/完成核单」,冻结核单快照并把订单推进到已核单。
- 认证:管理后台 JWT
- 幂等性:非幂等写操作,重复提交会被核单状态拦截
- 限流:无
入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| orderId | Path | Long | 是 | 订单 ID(无请求体) |
出参
Result<SettlementSubmitRespVO>(核单提交结果,结构无变化)。
业务边界(本次核心变化)
| 场景 | 变更前 | 变更后 |
|---|---|---|
| 尾款未收齐 + 无主报账人 | 报 584082 拦截 | 报 584082 拦截(不变) |
| 尾款未收齐 + 已有主报账人 | 报 584082 拦截 | 放行,欠收记为主报账人代收口径 |
| 尾款已收齐 | 放行 | 放行(不变) |
- 「放行」不等于「欠收已清」:欠收金额会留在报账快照的欠收勾稽字段里,由财务域下游追款兜底,前端不要把「提交成功」理解为「钱已收齐」。
- 代收口径:应代收 = 核单总额 − 客户线上已付(
paidAmount)。旧的「司机现金代收登记」项已废止,代收不再计入已付金额。 - 配套读接口
GET /v3/admin/order/{orderId}/settlement/reports/reimbursement(查询主报账人报账表)出参结构无变化。
示例
欠收但有主报账人提交成功、无主报账人被 584082 拦截,见「八、示例」8.2 / 8.3。
接口3:财务应收台账分页 GET /admin/finance/receipt/receivable/page(新增)
使用场景
财务「应收台账」页,按订单维度查看应收/已收/代收/欠收,用于内部财务对账与追款。
- 认证:管理后台 JWT(网关注入)
- 幂等性:只读
- 限流:无
入参(Query)
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| page | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 20 | 每页条数 |
| keyword | String | 否 | 空 | 搜索关键字(团号 / 客户姓名 / 产品名 / 订单号 模糊);空=不限 |
| receivableStatus | String | 否 | 空 | 收款状态筛选:UNPAID / PARTIAL / DONE;空或非法值=不按状态过滤 |
出参 Result<PageResult<ReceiptReceivableRowRespVO>>
分页外层固定为 records / total / page / pageSize。records[] 行字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 订单 ID(字符串防精度丢失) |
| orderNo | String | 订单号 |
| teamNo | String | 团号 |
| productName | String | 产品名 |
| customerName | String | 客户姓名 |
| customerPhone | String | 客户手机号(明文,内部财务对账用) |
| orderStatus | String | 订单粗状态枚举值(见「六、枚举」) |
| orderStatusName | String | 订单粗状态中文名 |
| receivableStatus | String | 收款状态:UNPAID 待收款 / PARTIAL 部分收款 / DONE 已收讫 |
| receivableStatusName | String | 收款状态中文名 |
| receivableAmount | Number | 应收总额(取消单归 0) |
| paidAmount | Number | 已收金额(毛额,退款不回减) |
| collectedAmount | Number | 代收金额(主报账人代收中的实时金额;取消单归 0) |
| balanceAmount | Number | 欠收金额(未指定主报账人的实时欠收;取消单归 0) |
业务边界
- 代收列与欠收列互斥:同一行
collectedAmount与balanceAmount必有一列为 0——订单指定了主报账人,未收齐部分进代收列;未指定主报账人,进欠收列。两列合计恒等于该单实时待收尾款。 - 收款状态由金额派生:已收=0 →
UNPAID;已收>0 且仍有欠收/代收 →PARTIAL;欠收代收均=0 且已收>0 →DONE。 - 只读接口,无登记/核销按钮;追款动作不在本接口。
- 本接口在 hl-finance 服务(
/admin/finance/*),与订单域接口不同服务,但同一网关入口。
示例
见「八、示例」8.1 / 8.2。
四、接口入参
各接口入参已分别内联在「三、接口详情」各小节,不再汇总大表。
五、出参字段
各接口出参已分别内联在「三、接口详情」各小节,不再汇总大表。
六、枚举 / 数据字典
6.1 reporterRank(报账人等级,接口1 相关概念)
| 值 | 中文名 | 说明 |
|---|---|---|
| PRIMARY | 主报账人 | 单内唯一;本次起确认行程前必须存在(尾款代收人) |
| SECONDARY | 次报账人 | 单内唯一,不满足接口1 的前置条件 |
| NONE | 非报账人 | 默认值 |
6.2 receivableStatus(收款状态,接口3 行字段 + 筛选项)
| 值 | 中文名 | 派生条件 |
|---|---|---|
| UNPAID | 待收款 | 已收金额 = 0 |
| PARTIAL | 部分收款 | 已收 > 0 且(欠收 + 代收)> 0 |
| DONE | 已收讫 | 欠收 + 代收 = 0 且已收 > 0 |
6.3 orderStatus(订单粗状态,接口3 行字段)
| 值 | 中文名 |
|---|---|
| PENDING_PAY | 待支付 |
| CUSTOMIZING | 定制中 |
| PENDING_DEPARTURE | 待出行 |
| TRAVELLING | 出行中 |
| COMPLETED | 已完成 |
| CANCELLED | 已取消 |
七、错误码
| 错误码 | 报文 | 触发接口 | 说明 |
|---|---|---|---|
| 581062 | 请先指定本单主报账人(尾款代收人) | 接口1 确认行程 | 本次新增。确认前置换后仍无主报账人时抛出;团期单扇出延迟窗口期被拦属预期,稍候重试 |
| 581036 | 确认订单前置校验未通过,请先补全所有必填项 | 接口1 确认行程 | 既有。5 项 checklist 未全过 |
| 581046 | 报账人ID格式非法,须为有效的数字ID | 接口1 确认行程 | 既有。reporterAssignmentId 格式非法 |
| 584082 | 存在待收尾款,请收齐后再提交核单 | 接口2 提交核单 | 既有但触发条件收紧:现在仅「无主报账人且欠收不为 0」才抛出;有主报账人的欠收单不再触发 |
八、示例
8.1 典型成功
接口1:确认行程(确认弹框选了主报账人)
POST /v3/admin/order/2086272700140957697/confirm-itinerary
Authorization: Bearer <admin token>
Content-Type: application/json
{
"reporterAssignmentId": 2072930844657283074
}
{
"code": 200,
"message": "成功",
"data": {
"success": true,
"oldStatus": "CUSTOMIZING",
"newStatus": "PENDING_DEPARTURE",
"oldFlowStatus": "CUSTOMIZING",
"newFlowStatus": "CONFIRMED",
"triggeredEvents": ["CHECKLIST_CONFIRMED"]
},
"success": true
}
接口3:应收台账分页(指定了主报账人的在途单,欠收进代收列)
GET /admin/finance/receipt/receivable/page?page=1&pageSize=20&keyword=26-0001
Authorization: Bearer <admin token>
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "2086272700140957697",
"orderNo": "HL20260920001",
"teamNo": "26-0001",
"productName": "呼伦贝尔草原 5 日游",
"customerName": "张三",
"customerPhone": "13800001111",
"orderStatus": "TRAVELLING",
"orderStatusName": "出行中",
"receivableStatus": "PARTIAL",
"receivableStatusName": "部分收款",
"receivableAmount": 12800.00,
"paidAmount": 6400.00,
"collectedAmount": 6400.00,
"balanceAmount": 0
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"success": true
}
8.2 边界情况
接口2:尾款未收齐但已有主报账人——变更前被拦、变更后放行
POST /v3/admin/order/2086272700140957697/settlement/finalize
Authorization: Bearer <admin token>
{
"code": 200,
"message": "成功",
"data": { "submitted": true },
"success": true
}
注意:提交成功不代表欠收已清,欠收金额留在报账快照勾稽字段中,由财务域下游追款。
接口3:未指定主报账人的订单——同一笔未收齐金额进欠收列
{
"receivableStatus": "PARTIAL",
"receivableStatusName": "部分收款",
"receivableAmount": 12800.00,
"paidAmount": 6400.00,
"collectedAmount": 0,
"balanceAmount": 6400.00
}
接口3:空结果(关键词无匹配)
{
"code": 200,
"message": "成功",
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
"success": true
}
8.3 业务失败
接口1:无主报账人且弹框未选人 → 581062
POST /v3/admin/order/2086272700140957697/confirm-itinerary
Authorization: Bearer <admin token>
Content-Type: application/json
{}
{
"code": 581062,
"message": "请先指定本单主报账人(尾款代收人)",
"data": null,
"success": false
}
接口2:尾款未收齐且无主报账人 → 584082(唯一仍会被拦的情形)
POST /v3/admin/order/2086272700140957697/settlement/finalize
Authorization: Bearer <admin token>
{
"code": 584082,
"message": "存在待收尾款,请收齐后再提交核单",
"data": null,
"success": false
}
九、业务边界
- 接口1 仅适用「定制中 → 待出行」确认动作;其他状态变更不受影响。
- 接口1 团期子订单正常路径无需前端传参(扇出已带主报账人);扇出延迟窗口被 581062 拦时引导「稍候重试」即可,不要当成数据异常。
- 接口2 放行后核单可正常完成,但财务仍会在应收台账/报账勾稽里看到代收欠收金额;「已核单」不等于「已收讫」。
- 接口3 为只读台账,不提供任何写操作;
customerPhone为明文,仅限内部财务对账场景使用,前端不要在非财务页面引用该接口。 - 接口3 已取消订单的金额列全部归 0。
十、修改前后对比
10.1 字段级对比
| 接口 | 字段 | 变更 |
|---|---|---|
| 接口1 确认行程 | 入参/出参全部字段 | 无变化(reporterAssignmentId 仍为非必填,但语义从「纯可选置换」变为「无主报账人时事实必填」) |
| 接口2 提交核单 | 入参/出参全部字段 | 无变化 |
| 接口3 应收台账 | 全部字段 | 新增接口,无对比 |
10.2 行为级对比
| 场景 | 变更前 | 变更后 |
|---|---|---|
| 确认行程时本单无主报账人 | 直接确认成功 | 报 581062,确认不发生;传 reporterAssignmentId 选人后放行 |
| 核单时尾款未收齐、有主报账人 | 报 584082 拦死 | 放行,欠收记主报账人代收口径 |
| 核单时尾款未收齐、无主报账人 | 报 584082 拦死 | 报 584082 拦死(不变) |
| 应代收口径 | 核单总额 −(已付 − 司机现金代收登记合计) | 核单总额 − 已付(司机现金代收登记项已废止) |
| 财务看尾款归属 | 无接口可看 | 应收台账代收/欠收互斥分列 |
十一、影响评估 / 回滚
- 破坏性:接口1 对「此前无主报账人也能确认」的流程是行为收紧,普通订单确认弹框必须支持选择主报账人并传
reporterAssignmentId,否则确认会被 581062 拦截——前端需要同步上线。 - 接口2 是放行面扩大,前端对 584082 的既有提示逻辑继续有效(触发面变窄),无强制改动;但原来「被拦 → 引导收尾款」的引导文案对「有主报账人」场景不再出现,如有相关 workaround 可清理。
- 接口3 纯新增,不影响存量页面。
- 回滚:后端回滚后,接口1 恢复「无主报账人也可确认」、接口2 恢复「欠收一律拦」、接口3 下线(请求返回 404)。回滚期间前端确认弹框保留选人逻辑无副作用(多传字段旧版兼容)。
十二、注意事项
- 确认弹框的主报账人候选来自本单人员安排,
reporterAssignmentId传人员安排 ID(不是用户 ID、不是员工编号)。 - 581062 与 581036 可能先后出现:先补 checklist(581036),再补主报账人(581062),前端引导顺序建议先 checklist 后选人。
- 「确认成功」「核单提交成功」都不代表尾款已收齐;尾款是否收讫以应收台账
receivableStatus=DONE为准。 - 应收台账的代收/欠收两列互斥,前端渲染时不要对两列同时展示非 0 值做兜底合并——合计即实时待收尾款。
- 本 Epic 还包含财务域支付完成后回写订单已付金额的配套链路(PR-5b 起),对管理后台 REST 契约无新增字段,不单独列接口。
十三、关联 / 联系人
- Issue(主):wx/HL#8248
- Issue(确认行程主报账人必填化):wx/HL#8265
- Issue(财务应收台账):wx/HL#8191
- PR-6a(接口1):wx/HL#8274 | merge commit:https://git.1814.love/wx/HL/commit/f6b1960a93
- PR-5a(接口2):wx/HL#8263 | merge commit:https://git.1814.love/wx/HL/commit/a480f3dacd
- PR-3(接口3):wx/HL#8207 | merge commit:https://git.1814.love/wx/HL/commit/768136626e
- 后端负责人:yst(腰苏图)