--- schema: "hl-changelog/v2" ticket: "7398" title: "应付款已付超额冲正(供应商退回):申请 / 分次确认到账 / 撤销剩余额度 / 分页" consumer: "admin" author: "jw(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "0fa6574f9cceb2f41f5e4208424ce64190da2014" target_release: "" verified_at: "2026-09-17" status_note: "后端交付(hl-finance,随 hl-order-service-v3 部署)。新增 4 个冲正接口与错误码 598814/598815/598816/598817/598818;付款明细新增 REVERSAL 负明细语义;资金流水新增 bizType=PAYMENT_REVERSAL。网关前缀 /admin/finance/** 已存在,无需新路由。前端需新增冲正申请与出纳确认到账两个界面。[mmg 2026-09-17 交付] 原型无冲正 UI,以后端契约为准嵌入应付款模块:①payable.js 冲正节 4 端点+四态字典(分页路径无 /page 后缀,598814-818 等拦截器透 message);②可冲正额=reversibleAmount(netPaidAmount−amount 2dp,非 −diffAmount,unpaidAmount 不参与,≥0.01 才显入口);③ReversalApplyModal 申请弹窗(sourceRefKey 原样回传、amount 预填可冲正额、reason 必填,文案明示确认到账后生效);④PaymentReversalPanel 冲正管理(orderId/supplierId/status 筛选分页,PENDING/PARTIAL 出确认到账/撤销,确认六字段含 ACTIVE 公司户下拉+默认今天+凭证号必填);⑤三处申请入口(ap-detail 构成明细+两建单面板超额行),统计页头部「冲正管理」进新视图返回强制刷新。api spec +5 例+组件 spec 11 例,scoped checkpoint 13 项全绿。" updated_at: "2026-09-17" base: "dev-v3" --- # finance: 应付款已付超额冲正(供应商退回) > **存放目录**: changelogs-v2/{YYYY-MM}/(管理后台,二期 finance) > > **服务**: hl-order-service-v3(hl-finance 模块同进程) > **PR**: #7889 > **Issue**: #7398 > **日期**: 2026-09-17 > **影响范围**: 应付款团期住宿付款身份的「已付超出当前应付」处置(申请冲正 → 出纳登记到账 → 统计与建议自动对平) --- ## 关键变化(给前端 mmg 的一句话) 1. **598812 有出口了**:团期住宿某付款身份「已付 > 当前应付」时(统计 `status=OVERPAID`、建议行 `netPaidAmount > amount`),财务可发起**冲正申请**,额度 ≤ `netPaidAmount − amount`。 2. **申请不改任何金额**:申请后统计、建议、再建单的结果都与申请前一样;**只有出纳确认到账后**才生效。 3. **确认到账可分次**:酒店分几次退就登记几次,每次一条入账流水 + 一条负明细;剩下不再退的额度可以**撤销**,已登记的部分保留。 4. **确认到账不看当前应付**:钱已经到了就必须能记进来,哪怕房又加回来、应付回升;应付回升产生的新欠付照常在建议里以差额行出现。 5. **统计读法不变**:`paidAmount`(原始已付,不减少)/ `refundedAmount`(已确认退款)/ `netPaidAmount`(净已付)三字段 #7396 已有,冲正确认后自动变化。 6. **凭证号必填且在同一申请内唯一**:重复提交同一次确认返回 598802,账户只入账一次。 --- ## 一、背景 #7396 让团期住宿按付款身份 `{orderId}:{stayDate}:{hotelId}:{roomTypeId}` 做金额对账,但「钱已付出、应付降到已付之下」只能报 598812,没有处理终态。本次补上「供应商退回」形态:申请 = 批准冲正额度,出纳每登记一次真实到账,就记一条资金流水(入账)和一条挂在原付款单下的负明细,原付款单不做任何修改。「抵扣下次付款」形态依赖预付域,本次不做。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 申请冲正 | POST | `/admin/finance/payment-reversals` | 新增 | 对已付超额的团期住宿付款身份申请退回额度,落 PENDING | | 2 | 确认本次到账 | PUT | `/admin/finance/payment-reversals/{id}/confirm` | 新增 | 出纳登记一次真实到账(可分次),记入账流水 + 负明细 | | 3 | 撤销剩余额度 | PUT | `/admin/finance/payment-reversals/{id}/cancel` | 新增 | 作废未到账的剩余额度,已确认部分保留 | | 4 | 冲正申请分页 | GET | `/admin/finance/payment-reversals` | 新增 | 按订单 / 供应商 / 状态过滤 | --- ## 三、接口详情 ### 1. 申请冲正 `POST /admin/finance/payment-reversals` **VO**: `PaymentReversalCreateReqVO → Result` #### 使用场景 建议行或按团统计显示某团期住宿付款身份已付超出当前应付(`status=OVERPAID` / `netPaidAmount > amount`),财务与酒店协商退回后发起申请。`sourceRefKey` 取建议行的 `sourceRefKey`,`orderId` 取建议行 / 统计行的 `orderId`。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | sourceRefType | Body | String | 是 | 本期仅 `GROUP_BATCH_STAY` | 来源类型 | | sourceRefKey | Body | String | 是 | ≤96;格式 `{orderId}:{yyyy-MM-dd}:{hotelId}:{roomTypeId}`,其中订单须等于 orderId | 付款身份业务键 | | orderId | Body | Long | 是 | - | 来源订单 ID | | amount | Body | BigDecimal | 是 | ≥0.01,两位小数,≤ 净已付 − 当前应付 | 申请冲正额度 | | reason | Body | String | 是 | ≤512,非空白 | 冲正原因 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | data.id | String | 新冲正申请 ID(雪花 ID,字符串输出) | #### 请求示例 ```json { "sourceRefType": "GROUP_BATCH_STAY", "sourceRefKey": "2100482262209380353:2027-01-05:2029926133256929282:2029944767501012994", "orderId": 2100482262209380353, "amount": 800.00, "reason": "该户退团,酒店同意退回已付房费" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "id": "2100483176424972289" }, "success": true } ``` #### 空数据 / 降级响应 无查询数据。申请成功后不产生资金流水、不写付款明细;统计、建议、再建单的结果与申请前逐字段相同(再建单仍 598812)。 #### 错误响应 ```json { "code": 598814, "message": "冲正金额超出可冲正额 0.00 元(已付超出当前应付的部分)", "data": null, "success": false } ``` | code | 触发 | |------|------| | 400 | 必填缺失 / amount ≤ 0 / 长度超限 | | 598808 | sourceRefType 不是 GROUP_BATCH_STAY,或 sourceRefKey 格式非法 / 与 orderId 不一致 | | 598814 | amount > 净已付 − 当前应付(未超额时可冲正额为 0;已全额退回后再申请同样命中) | | 598815 | 同一付款身份已有未完结申请(PENDING / PARTIAL) | | 100503 | 同订单付款写操作正在进行,稍后重试 | #### 业务边界 - 只处理「已付 > 当前应付」;「未付占用超额」仍走 #7396 的删除 / 驳回 / 撤销批准(598813)。 - 冲正挂在该付款身份下最近一次付讫的原付款单上;原付款单金额、状态、付款时间不变。 - 同一付款身份同时只允许一张未完结申请。 - 与付款单创建 / 编辑共用同一把订单锁,并发时串行执行。 --- ### 2. 确认本次到账 `PUT /admin/finance/payment-reversals/{id}/confirm` **VO**: `PaymentReversalConfirmReqVO → Result` #### 使用场景 出纳收到酒店退款回单后登记。酒店分几次退就调几次,每次填本次实际到账金额与入账的公司账户。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | id | Path | Long | 是 | - | 冲正申请 ID | | amount | Body | BigDecimal | 是 | ≥0.01,两位小数 | 本次实际到账金额 | | fundAccountId | Body | Long | 是 | 须为启用中的公司资金账户 | 入账账户(不会用原付款账户兜底) | | receivedDate | Body | String | 是 | `yyyy-MM-dd` | 到账日期(写入流水备注) | | voucherNo | Body | String | 是 | ≤64,非空白;同一申请内唯一 | 银行回单 / 凭证号 | | voucherUrl | Body | String | 否 | ≤500 | 凭证影像 URL(写入流水) | | remark | Body | String | 否 | ≤200 | 备注(写入流水备注) | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | data | Boolean | 恒 true | #### 请求示例 ```json { "amount": 400.00, "fundAccountId": 2100482405474148354, "receivedDate": "2026-09-17", "voucherNo": "HD-20260917-001", "voucherUrl": "https://oss.example.com/voucher/001.png", "remark": "酒店财务转账" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": true, "success": true } ``` #### 空数据 / 降级响应 无查询数据。成功后:申请 `confirmedAmount` 累加、状态变 PARTIAL(未满额)或 CONFIRMED(满额);资金流水新增一条 `direction=IN`、`bizType=PAYMENT_REVERSAL`、`bizId=冲正申请ID`;付款明细新增一条 `itemKind=REVERSAL`、金额为负的明细。 #### 错误响应 ```json { "code": 598817, "message": "确认到账金额超出冲正申请剩余额度 0.00 元", "data": null, "success": false } ``` ```json { "code": 595006, "message": "账户已停用", "data": null, "success": false } ``` | code | 触发 | |------|------| | 400 | 必填缺失(含 fundAccountId / voucherNo / receivedDate)/ amount ≤ 0 | | 598818 | 冲正申请不存在 | | 598802 | 申请已撤销;或同一申请下该凭证号已登记(重复提交);或并发下状态已被推进 | | 598817 | 累计到账超过申请额度(含已全额确认后再确认) | | 598816 | 本次到账超过该付款身份当前净已付 | | 595001 / 595006 | 入账账户不存在 / 已停用 | | 100503 | 同订单付款写操作正在进行,稍后重试 | #### 业务边界 - **不复核当前应付**:房务把房加回、应付回升后仍可登记到账;由此产生的欠付在建议里 `diffAmount > 0`,按 #7396 规则再建单。 - 任一校验失败整体回滚:不入账、不写明细、不累加额度。 - 两次不同凭证号的到账并发提交会先后成功;同凭证号只会成功一次。 - 入账账户余额按本次金额增加,原付款账户不变。 --- ### 3. 撤销剩余额度 `PUT /admin/finance/payment-reversals/{id}/cancel` **VO**: `PaymentReversalCancelReqVO → Result` #### 使用场景 酒店明确不再退款(或应付已恢复、钱没有退)时,作废申请剩余未到账的额度。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | id | Path | Long | 是 | - | 冲正申请 ID | | reason | Body | String | 是 | ≤512,非空白 | 撤销原因 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | data | Boolean | 恒 true | #### 请求示例 ```json { "reason": "酒店余款不再退回" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": true, "success": true } ``` #### 空数据 / 降级响应 无查询数据。成功后状态变 CANCELLED,`confirmedAmount` 不变,已登记的流水与负明细不变。 #### 错误响应 ```json { "code": 598802, "message": "应付单状态非法,当前状态不允许此操作", "data": null, "success": false } ``` | code | 触发 | |------|------| | 400 | reason 缺失或空白 | | 598818 | 冲正申请不存在 | | 598802 | 申请已 CONFIRMED 或已 CANCELLED;或与确认并发时确认先完成 | #### 业务边界 - 仅 PENDING / PARTIAL 可撤销;已全额确认的申请不能撤销(已发生的资金事实只能另行红冲,本期不提供)。 - 与确认并发时只有一个成功。 --- ### 4. 冲正申请分页 `GET /admin/finance/payment-reversals` **VO**: `PaymentReversalPageReqVO → Result>` #### 使用场景 冲正申请列表 / 出纳待确认列表(`status=PENDING` 或 `PARTIAL`)。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Query | Long | 否 | - | 订单 ID | | supplierId | Query | Long | 否 | - | 供应商 ID | | status | Query | String | 否 | PENDING / PARTIAL / CONFIRMED / CANCELLED | 状态 | | page | Query | Integer | 否 | ≥1,默认 1 | 页码 | | pageSize | Query | Integer | 否 | 1~100,默认 20 | 每页条数 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | records[].reversalId | String | 冲正申请 ID | | records[].reversalNo | String | 冲正单号(RF-yyyyMMdd####) | | records[].sourceRefType | String | 来源类型 | | records[].sourceRefKey | String | 付款身份业务键 | | records[].orderId | String | 订单 ID | | records[].teamNo | String | 团号快照 | | records[].supplierId | String | 供应商 ID | | records[].supplierName | String | 供应商全称快照 | | records[].paymentId | String | 被冲的原付款单 ID | | records[].amount | BigDecimal | 申请额度 | | records[].confirmedAmount | BigDecimal | 累计已确认到账 | | records[].remainingAmount | BigDecimal | 剩余可确认额度(CANCELLED / CONFIRMED 为 0) | | records[].status | String | PENDING / PARTIAL / CONFIRMED / CANCELLED | | records[].reason | String | 冲正原因 | | records[].appliedBy | String | 申请人 ID | | records[].appliedByName | String | 申请人姓名 | | records[].cancelReason | String | 撤销原因 | | records[].cancelledBy | String | 撤销人 ID | | records[].cancelledAt | String | 撤销时间 | | records[].createTime | String | 申请时间 | | total / page / pageSize | Long / Integer / Integer | 分页信息 | #### 请求示例 ```http GET /admin/finance/payment-reversals?orderId=2100482262209380353&status=CONFIRMED&page=1&pageSize=20 ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "records": [ { "reversalId": "2100483176424972289", "reversalNo": "RF-202609170002", "sourceRefType": "GROUP_BATCH_STAY", "sourceRefKey": "2100482262209380353:2027-01-05:2029926133256929282:2029944767501012994", "orderId": "2100482262209380353", "supplierId": "2096854417461403650", "paymentId": "2100482264935604226", "amount": 800.00, "confirmedAmount": 800.00, "remainingAmount": 0, "status": "CONFIRMED", "reason": "#7398 验收冲正" } ], "total": 1, "page": 1, "pageSize": 20 }, "success": true } ``` #### 空数据 / 降级响应 ```json { "code": 200, "message": "成功", "data": { "records": [], "total": 0, "page": 1, "pageSize": 20 }, "success": true } ``` #### 错误响应 ```json { "code": 400, "message": "每页条数最大为100", "data": null, "success": false } ``` #### 业务边界 - 按申请 ID 倒序(最新在前)。 - 单次到账明细(入账账户、金额、时间、凭证、经办人)在资金流水查询中按 `bizType=PAYMENT_REVERSAL`、`bizId=reversalId` 查看。 --- ## 四、契约约束与正确调用方式 - 申请的 `sourceRefKey` / `orderId` 必须原样取自建议行或统计行,不要前端拼接。 - 可冲正额 = 建议行 `netPaidAmount − amount`(≤0 时不应展示申请入口)。 - 确认到账时 `amount` 填**本次**实际到账金额,不是申请额;分次到账逐次调用,每次换一个凭证号。 - 所有 ID 按字符串处理,避免精度丢失。 - 统计与建议无需新接口:确认后 `refundedAmount` 增加、`netPaidAmount` 减少,`paidAmount` 不变。 --- ## 五、数据库行为 | 表 | 动作 | 说明 | |---|---|---| | `fin_payment_reversal` | 新建表(V20260917_114) | 冲正申请:amount 额度、confirmed_amount 累计到账、status 四态;申请 INSERT,确认 / 撤销按旧值条件更新 | | `fin_payment_item` | 新增列 `reversal_id` + 索引 `idx_reversal`(V20260917_114) | 确认时 INSERT 一条 `item_kind=REVERSAL`、金额为负、`payment_id`=原付款单、`reversal_id`=申请、`remark`=凭证号 | | `fin_fund_flow` | INSERT | 确认时一条 `direction=IN`、`biz_type=PAYMENT_REVERSAL`、`biz_id`=申请 ID | | `fin_fund_account` | UPDATE balance | 入账账户余额 + 本次金额 | | `fin_payment` | 不变 | 原付款单任何字段不改 | --- ## 六、边界行为 - 申请后、到账前:金额口径完全不变,再建单仍 598812。 - 到账后应付回升:确认照常成功,建议 `diffAmount` 出现正差额,按 #7396 建单。 - 部分到账后剩余不退:调撤销;统计中已退部分保留。 - 合并付款单(一张付多户):冲正只作用于申请的那一户,负明细 `orderId` 为该户,挂在合并付款单下;另一户统计不变。 - 已全额确认后不可撤销、不可再确认(598802 / 598817)。 ## 六.5、枚举 ### 冲正申请状态(status) | 值 | 含义 | |---|---| | PENDING | 待到账 | | PARTIAL | 部分到账 | | CONFIRMED | 全额到账 | | CANCELLED | 已撤销剩余额度 | ### 新增取值 | 字段 | 新值 | 含义 | |---|---|---| | 付款明细 `itemKind` | REVERSAL | 冲正负明细(金额为负) | | 资金流水 `bizType` | PAYMENT_REVERSAL | 应付款冲正入账 | --- ## 七、不影响范围 - 付款单创建 / 编辑 / 提交 / 批准 / 驳回 / 撤销批准 / 出纳付款的入参出参与判据不变。 - 建议与统计接口结构不变(相关字段 #7396 已提供)。 - 非团期住宿来源(行程节点 / 配房 / 手工)不支持冲正。 - 前端:新增「冲正申请」(统计 / 建议超额行入口)与「出纳确认到账」两个界面,由 mmg 承接。 --- ## 八、测试环境已验证 TEST(`https://api.test.1814.love:9443`,`hl-order-service-v3` = **dev-v3@977257515** 合并提交之后的构建;真实网关 + 管理员 token),2026-09-17: | 场景 | 结果 | |---|---| | 迁移 | `V20260917_114` 执行成功;`fin_payment_item.reversal_id` 与 `idx_reversal` 存在;新表不含账户 / 到账日 / 凭证列 | | 真实取消户闭环 | 已付 800、订单已取消应付 0:申请前后建单均 598812;申请 800 → 确认 800 → 入账流水 800、负明细 −800、Σ=0、统计 PAID、建议 overpaid 归零;原付款单逐字段不变 | | 申请不生效 | 申请后付款明细零新增,建议 / 统计 / 再建单结果与申请前逐字段相同 | | 入账账户 | 入 Y:Y +800、原付款账户 X 不变;fundAccountId 为空 → 400;停用账户 → 595006 且零流水零明细;重复提交同凭证 → 598802、余额只加一次 | | 上界 | 超额申请 598814;重复申请 598815;全额确认后再申请 598814、再确认 598817;净已付不足时确认 598816(零流水零明细);已确认 400 再确认 400 → 200 | | 分次到账 | 800 分两次 400:PARTIAL → CONFIRMED,两条流水两条负明细,统计 800/400/400 → 800/800/0 | | 应付回升 | 申请后未到账、应付恢复 → 撤销 200、零流水、再建单 598809;已到账、应付恢复 → 确认 200、统计 OWED 欠付 800、差额单 800 → 200;部分到账叠加回升同样可确认,剩余可撤销 | | 退后再建 | 退完后应付变 400 → 四入口差额 400 → 建单提交后统计欠付 400、在途 400、OWED → 再建 598809 → 付款后 PAID | | 合并付款 | 1600 合并单只退户 1:户 1 800/800/0、户 2 800/0/800;负明细挂 1600 主单、订单为户 1;供应商统计已退 +800 | | 并发 | 同申请并发两次确认 400 均成功、流水恰 2;三次并发恰 2 成功 1 次 598817;并发确认与申请 → 申请 598815;撤销与确认并发 8 轮均恰一方成功(PENDING / PARTIAL 两个分支、CANCELLED / CONFIRMED 两个终态都出现),撤销后已确认部分保留 | ``` POST /admin/finance/payment-reversals → 200 {"id":"2100483176424972289"} PUT /admin/finance/payment-reversals/2100483176424972289/confirm → 200 true PUT /admin/finance/payment-reversals/2100483176424972289/confirm(同凭证)→ 598802 PUT /admin/finance/payment-reversals/2100483176424972289/cancel → 598802 ``` 逐条验收记录见工单 #7398 验收评论。 --- ## 十、相关文档 - Issue: [#7398](https://git.1814.love:8443/wx/HL/issues/7398) - PR: [#7889](https://git.1814.love:8443/wx/HL/pulls/7889) - 前置: [#7396](https://git.1814.love:8443/wx/HL/issues/7396)(付款身份与金额对账) ## 关联 / 联系人 ### 链接 - **Issue**: [#7398](https://git.1814.love:8443/wx/HL/issues/7398) - **PR**: [#7889](https://git.1814.love:8443/wx/HL/pulls/7889) - **关联**: #7396(598812 的来源)、#7325(团期单户退团 / 取消) ### 联系人 - **后端负责人**: @jw - **前端负责人**: @mmg