diff --git a/changelogs-v2/2026-09/24_8248_尾款收款模型重构-修改接口-管理后台.md b/changelogs-v2/2026-09/24_8248_尾款收款模型重构-修改接口-管理后台.md new file mode 100644 index 00000000..c634ea40 --- /dev/null +++ b/changelogs-v2/2026-09/24_8248_尾款收款模型重构-修改接口-管理后台.md @@ -0,0 +1,453 @@ +--- +schema: "hl-changelog/v2" +ticket: "8248" +title: "尾款收款模型重构:确认行程主报账人必填化 + 核单欠收硬闸按主报账人豁免 + 财务应收台账" +consumer: "admin" +author: "yst" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "2026-09-24" +status_note: "尾款收款模型重构 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 现网零漂移。" +updated_at: "2026-09-24" +base: "dev-v3" +--- + +# 尾款收款模型重构:确认行程主报账人必填化 + 核单欠收硬闸豁免 + 财务应收台账 + +> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/` +> +> **服务**: hl-order-service-v3(订单核心域 + 核单域)· hl-finance(收款域) +> **Issue**: https://git.1814.love/wx/HL/issues/8248 (主) · https://git.1814.love/wx/HL/issues/8265 · https://git.1814.love/wx/HL/issues/8191 +> **PR**: https://git.1814.love/wx/HL/pulls/8274 · https://git.1814.love/wx/HL/pulls/8263 · https://git.1814.love/wx/HL/pulls/8207 +> **日期**: 2026-09-24 +> **影响范围**: 管理后台「订单确认行程弹框」「核单提交」「财务应收台账」三处 + +--- + +## ⚠️ 关键变化(前端必读) + +- **确认行程前必须先有主报账人**:普通订单点「确认行程」时,如果本单还没有主报账人且弹框里也没选人,接口直接报 **581062「请先指定本单主报账人(尾款代收人)」**,确认动作不发生。确认弹框必须提供主报账人选择项并传 `reporterAssignmentId`。 +- **核单欠收硬闸放行面扩大**:提交核单时「尾款未收齐」不再一律拦死——只要本单已指定主报账人,欠收部分记为主报账人代收口径,允许提交核单;**只有「欠钱且无人兜底(无主报账人)」的单**才会继续被 584082 拦截。 +- **新增财务应收台账只读分页接口**:`GET /admin/finance/receipt/receivable/page`,订单维度看应收/已收/代收/欠收,代收列与欠收列互斥(指定主报账人后欠收挪入代收)。 + +--- + +## 一、接口背景 + +尾款收款模型重构 Epic(方案甲·全实时,不建债表)。旧模型里尾款由司导线下代收,经过「现场垫付 → 报销 → 核单 → 支付完成」长链路后才回写订单已付金额,导致: + +- 核单时「尾款未收齐」一律硬拦,司导已代收但还没走完报销回写的单被卡死; +- 财务看不到「这笔钱到底在谁手里」——是客户还欠着,还是司导代收未回款。 + +新模型的三条规则: + +1. **金额实时算**:应收/已收/欠收全部实时计算,不落地中间债表; +2. **归属实时读主报账人**:订单层有主报账人(reporterRank=PRIMARY)时,未收齐部分视为「主报账人代收中」;没有主报账人时才是「客户欠收」; +3. **结清看核单终态**:核单完成(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` + +| 字段 | 类型 | 说明 | +|------|------|------| +| 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`(核单提交结果,结构无变化)。 + +#### 业务边界(本次核心变化) + +| 场景 | 变更前 | 变更后 | +|------|--------|--------| +| 尾款未收齐 + **无主报账人** | 报 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>` + +分页外层固定为 `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:确认行程(确认弹框选了主报账人)** + +```http +POST /v3/admin/order/2086272700140957697/confirm-itinerary +Authorization: Bearer +Content-Type: application/json + +{ + "reporterAssignmentId": 2072930844657283074 +} +``` + +```json +{ + "code": 200, + "message": "成功", + "data": { + "success": true, + "oldStatus": "CUSTOMIZING", + "newStatus": "PENDING_DEPARTURE", + "oldFlowStatus": "CUSTOMIZING", + "newFlowStatus": "CONFIRMED", + "triggeredEvents": ["CHECKLIST_CONFIRMED"] + }, + "success": true +} +``` + +**接口3:应收台账分页(指定了主报账人的在途单,欠收进代收列)** + +```http +GET /admin/finance/receipt/receivable/page?page=1&pageSize=20&keyword=26-0001 +Authorization: Bearer +``` + +```json +{ + "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:尾款未收齐但已有主报账人——变更前被拦、变更后放行** + +```http +POST /v3/admin/order/2086272700140957697/settlement/finalize +Authorization: Bearer +``` + +```json +{ + "code": 200, + "message": "成功", + "data": { "submitted": true }, + "success": true +} +``` + +> 注意:提交成功不代表欠收已清,欠收金额留在报账快照勾稽字段中,由财务域下游追款。 + +**接口3:未指定主报账人的订单——同一笔未收齐金额进欠收列** + +```json +{ + "receivableStatus": "PARTIAL", + "receivableStatusName": "部分收款", + "receivableAmount": 12800.00, + "paidAmount": 6400.00, + "collectedAmount": 0, + "balanceAmount": 6400.00 +} +``` + +**接口3:空结果(关键词无匹配)** + +```json +{ + "code": 200, + "message": "成功", + "data": { "records": [], "total": 0, "page": 1, "pageSize": 20 }, + "success": true +} +``` + +### 8.3 业务失败 + +**接口1:无主报账人且弹框未选人 → 581062** + +```http +POST /v3/admin/order/2086272700140957697/confirm-itinerary +Authorization: Bearer +Content-Type: application/json + +{} +``` + +```json +{ + "code": 581062, + "message": "请先指定本单主报账人(尾款代收人)", + "data": null, + "success": false +} +``` + +**接口2:尾款未收齐且无主报账人 → 584082(唯一仍会被拦的情形)** + +```http +POST /v3/admin/order/2086272700140957697/settlement/finalize +Authorization: Bearer +``` + +```json +{ + "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(主):https://git.1814.love/wx/HL/issues/8248 +- Issue(确认行程主报账人必填化):https://git.1814.love/wx/HL/issues/8265 +- Issue(财务应收台账):https://git.1814.love/wx/HL/issues/8191 +- PR-6a(接口1):https://git.1814.love/wx/HL/pulls/8274 | merge commit:https://git.1814.love/wx/HL/commit/f6b1960a93 +- PR-5a(接口2):https://git.1814.love/wx/HL/pulls/8263 | merge commit:https://git.1814.love/wx/HL/commit/a480f3dacd +- PR-3(接口3):https://git.1814.love/wx/HL/pulls/8207 | merge commit:https://git.1814.love/wx/HL/commit/768136626e +- 后端负责人:yst(腰苏图)