docs(changelog): #8164 收票核销硬钩稽PR-2——钱侧防御钩子599413/599414(应付/预付编辑改价超额+删除/驳回有票关联硬拦,修改接口-管理后台)
changelog-filename-gate / validate (push) Failing after 2s

这个提交包含在:
yaosutu
2026-09-22 14:42:14 +08:00
父节点 312ecd0f1f
当前提交 553b870997
@@ -0,0 +1,261 @@
---
schema: "hl-changelog/v2"
ticket: "8164"
title: "收票核销硬钩稽 PR-2——钱侧防御钩子:应付/预付编辑改价超额(599413)+ 删除/驳回有票关联(599414)硬拦"
consumer: "admin"
author: "yst(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "pending"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "收票(进项发票)域钱侧防御钩子上线:应付/预付的编辑草稿、删除草稿、驳回六个既有接口新增服务端硬校验——编辑改价后金额低于该单已被有效票匹配的金额报 599413;删除/驳回存在非作废收票关联的单据报 599414。无新接口、入参/出参字段零变化,仅行为收紧 + 新增两个错误码。已合并 dev-v3(PR #8165,merge commit c4a1f1fb69)并部署测试服、行为级验证通过。匹配额口径 = Σ fin_invoice_in_rel.match_amount(剔 VOIDED 作废票),作废票后匹配额自动释放、单据恢复可删改;无票或票已作废的单据不受影响。"
updated_at: "2026-09-22"
base: "dev-v3"
---
# 收票核销硬钩稽 PR-2:钱侧防御钩子(599413 / 599414)(管理后台)
> **服务**: hl-order-service-v3(hl-finance 模块,端口 8086/8186)
> **PR**: #8165
> **Issue**: #8164
> **日期**: 2026-09-22
> **影响范围**: 管理后台「财务管理 - 应付款管理 / 预付款管理」的**编辑草稿、删除草稿、驳回**操作(共 6 个既有接口)
---
## 一、接口背景
收票核销硬钩稽分两步走:
- **PR-1(#8163,已推)**:票侧门槛——登记/编辑发票挂 PAYMENT/PREPAY 关联单时,单据必须是 APPROVED/PAID 生效态,否则 599412。
- **PR-2(本次,#8165)**:钱侧防御钩子——反过来守「单据侧」:一张**已经被发票匹配过**的应付/预付单,不允许通过编辑把金额改到已匹配票额之下,也不允许直接删除/驳回,否则会出现「钱变了/钱没了,票还挂在上面」的票-款不符,对账口径失真。
PR-1 生效后正常路径下新票只能挂生效单,本组钩子主要拦**存量脏数据**(PR-1 之前挂上的草稿/审批中单据关联)与**冲抵重算实付额**(Epic #8127 改草稿冲抵明细会重算 actualPayAmount)两类接缝场景。涉票一律 fail-fast 硬拦,不放行。
**无新接口、无字段增删改名、出参结构零变化**,只有 2 个新错误码 + 6 个既有接口的行为收紧。
## 二、变更清单
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|----------|
| 1 | 编辑应付款草稿 | PUT | /admin/finance/payments/{id} | 行为收紧:改后实付额 < 已匹配票额 → 599413 |
| 2 | 删除应付款草稿 | DELETE | /admin/finance/payments/{id} | 行为收紧:存在非作废收票关联 → 599414 |
| 3 | 驳回应付款 | PUT | /admin/finance/payments/{id}/reject | 行为收紧:存在非作废收票关联 → 599414 |
| 4 | 编辑预付款草稿 | PUT | /admin/finance/prepays/{id} | 行为收紧:改后预付额 < 已匹配票额 → 599413 |
| 5 | 删除预付款草稿 | DELETE | /admin/finance/prepays/{id} | 行为收紧:存在非作废收票关联 → 599414 |
| 6 | 驳回预付款 | PUT | /admin/finance/prepays/{id}/reject | 行为收紧:存在非作废收票关联 → 599414 |
> 注意预付路径前缀是 **/admin/finance/prepays**(复数),与应付 /payments 一致。
## 三、接口详情
| 接口 | 使用场景 | 认证 | 幂等性 | 限流 |
|------|----------|------|--------|------|
| PUT /admin/finance/payments/{id} | 应付草稿(PENDING)编辑改价/改明细,含冲抵明细整体置换 | 管理后台 JWT | 非幂等(同内容重复提交幂等) | 网关默认 |
| DELETE /admin/finance/payments/{id} | 删除应付草稿(PENDING,软删) | 管理后台 JWT | 重复删除报单据不存在 | 网关默认 |
| PUT /admin/finance/payments/{id}/reject | 驳回审批中(SUBMITTED)应付单 | 管理后台 JWT | 非幂等(重复驳回报状态非法) | 网关默认 |
| PUT /admin/finance/prepays/{id} | 预付草稿(PENDING)编辑改价 | 管理后台 JWT | 非幂等 | 网关默认 |
| DELETE /admin/finance/prepays/{id} | 删除预付草稿(PENDING,软删) | 管理后台 JWT | 重复删除报单据不存在 | 网关默认 |
| PUT /admin/finance/prepays/{id}/reject | 驳回审批中(SUBMITTED)预付单 | 管理后台 JWT | 非幂等 | 网关默认 |
## 四、接口入参
入参结构**零变化**,仅服务端校验规则收紧。为自包含列关键字段:
### 4.1 PUT /admin/finance/payments/{id}(PaymentUpdateReqVO,不变)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| supplierId | Long | 是 | 供应商 ID |
| payeeAccountId | Long | 是 | 收款账户 ID |
| amount | BigDecimal | 是 | 付款金额(>0)。**新校验**:冲抵重算后的实付额(actualPayAmount = amount − Σ冲抵金额)不得低于该单已匹配票额,否则 599413 |
| paymentType | String | 是 | 付款类型(fin_payment_type 字典标签,≤32) |
| reason | String | 是 | 付款事由(≤512) |
| teamNo | String | 否 | 团号(≤32) |
| orderId | Long | 否 | 关联订单 ID |
| resourceId | Long | 否 | 关联资源 ID |
| prepayOffsets | Array | 否 | 冲抵预付款明细(可空=不冲抵;整体置换语义) |
### 4.2 PUT /admin/finance/prepays/{id}(PrepayUpdateReqVO,不变)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| supplierId | Long | 是 | 付款单位供应商 ID |
| offsetSupplierId | Long | 否 | 冲抵单位供应商 ID(默认=付款单位) |
| amount | BigDecimal | 是 | 预付金额(>0,整数≤13位、小数≤2位)。**新校验**:不得低于该单已匹配票额,否则 599413 |
| availableAmount | BigDecimal | 否 | 可冲抵金额(0 ≤ x ≤ amount) |
| payDate | LocalDate | 是 | 付款日期 |
| remark | String | 否 | 备注(≤512) |
### 4.3 DELETE / reject 四个接口
- DELETE /admin/finance/payments/{id}、DELETE /admin/finance/prepays/{id}:仅路径参数 id(Long),无请求体
- PUT /admin/finance/payments/{id}/reject、PUT /admin/finance/prepays/{id}/reject:路径参数 id + 请求体 { "reason": "驳回原因" }(不变)
## 五、出参字段
六个接口出参**零变化**:成功统一返回 Result&lt;Void&gt;({ "code": 0, "data": null, "msg": "" });失败走统一错误响应({ "code": 错误码, "data": null, "msg": "错误消息" })。
## 六、枚举 / 数据字典
### 6.1 单据状态(本次校验的前提,应付/预付一致)
| 值 | 中文名 | 可编辑/删除 | 可驳回 |
|----|--------|------------|--------|
| PENDING | 草稿 | 是(受 599413/599414 约束) | 否 |
| SUBMITTED | 审批中 | 否 | 是(受 599414 约束) |
| APPROVED | 已批准 | 否 | 否 |
| REJECTED | 已驳回 | 否 | 否 |
| PAID | 已付讫 | 否 | 否 |
### 6.2 发票状态(匹配额口径依据,本次无新增值)
| 值 | 中文名 | 是否计入匹配额 |
|----|--------|---------------|
| RECEIVED | 已收票 | 计入 |
| VERIFIED | 已核对 | 计入 |
| VOIDED | 已作废 | **不计入**(作废自动释放匹配额,单据恢复可删改) |
## 七、错误码
| 错误码 | 常量 | 消息 | 触发场景 |
|--------|------|------|----------|
| **599413** | INVOICE_IN_MATCH_OVER_BIZ | 收票匹配额超过单据金额(须先作废或改票) | **本次新增**。编辑应付/预付草稿时,改后金额(应付取冲抵后实付额 actualPayAmount,预付取 amount)低于该单已被有效票(剔 VOIDED)匹配的金额合计,整单回滚 |
| **599414** | INVOICE_IN_BIZ_HAS_REL | 单据已被收票关联(删除/驳回前须先作废或改票) | **本次新增**。删除草稿 / 驳回单据时,该单存在任何非作废发票的匹配关联,不删单、状态不推进 |
| 598802 | PAYMENT_STATUS_ILLEGAL | 付款单状态非法 | 应付编辑/删除时非 PENDING、驳回时非 SUBMITTED(既有) |
| 599002 | PREPAY_STATUS_ILLEGAL / PREPAY_TRANSITION_ILLEGAL | 预付单状态非法 | 预付同上(既有) |
| 599412 | INVOICE_IN_REL_BIZ_NOT_EFFECTIVE | 关联业务单据未生效(应付/预付须已批准或已付讫才可收票) | 票侧(PR-1)挂票门槛,与本组钩子互为对偶(既有) |
**599413 与 599414 的区别**:413 = 编辑改价场景,「金额不能低于已匹配票额」;414 = 删除/驳回场景,「有票关联的单据不许消失/退出对账」。两个码的引导动作一致:先到收票管理作废相关发票或改票释放匹配额,再回来操作单据。
## 八、示例
### 8.1 典型成功 —— 删除一张无收票关联的应付草稿
请求:
DELETE /admin/finance/payments/1982736450000042
响应:
```json
{
"code": 0,
"data": null,
"msg": ""
}
```
无收票关联(或关联票已全部作废)的单据删改驳回**不受影响**,行为与本次变更前完全一致。
### 8.2 边界情况 —— 编辑草稿,改后金额**等于**已匹配票额(等额放行)
场景:应付草稿 1982736450000055 已被一张 VERIFIED 发票匹配 8000.00。现将付款金额由 10000.00 改为 8000.00(无冲抵,实付额 = 8000.00)。
请求:
PUT /admin/finance/payments/1982736450000055
Content-Type: application/json
{
"supplierId": 1877665544332211,
"payeeAccountId": 1877665544000099,
"amount": 8000.00,
"paymentType": "GROUP_SETTLE",
"reason": "按实结调价",
"teamNo": "T20260918-01",
"orderId": null,
"resourceId": null,
"prepayOffsets": []
}
响应(已匹配 8000.00 ≤ 新实付 8000.00,放行):
```json
{
"code": 0,
"data": null,
"msg": ""
}
```
等额放行是有意设计:matched > newAmount 才拦,matched = newAmount 视为票-款刚好持平。预付编辑(PUT /admin/finance/prepays/{id})同理,比较口径为 amount。
### 8.3 业务失败 —— 编辑草稿把金额改到已匹配票额之下,触发 599413
同 8.2 的单据,若改为 5000.00(低于已匹配 8000.00):
```json
{
"code": 599413,
"data": null,
"msg": "收票匹配额超过单据金额(须先作废或改票)"
}
```
整单不落库、事务整体回滚。再给一个 599414 示例——删除一张已被发票匹配的应付草稿:
DELETE /admin/finance/payments/1982736450000055
```json
{
"code": 599414,
"data": null,
"msg": "单据已被收票关联(删除/驳回前须先作废或改票)"
}
```
驳回链路(PUT /admin/finance/payments/{id}/reject、PUT /admin/finance/prepays/{id}/reject,请求体 { "reason": "..." })命中有效票关联时同样返回 599414,状态不推进、不落审核流水。
## 九、业务边界
**适用场景**
- 应付/预付草稿的正常编辑、删除;SUBMITTED 单的驳回——只要无有效收票关联,行为与之前完全一致
**不适用场景(会被新钩子拦截)**
- 把单据金额改到「已被有效票匹配的金额」之下(599413)
- 删除 / 驳回仍挂着有效票(RECEIVED/VERIFIED)匹配的单据(599414)
**特殊边界**
- **匹配额口径**:Σ fin_invoice_in_rel.match_amount,只计有效票(发票非 VOIDED 且未软删);作废(VOIDED)发票后匹配额自动释放,单据即时恢复可删改,无需额外操作
- **EXPENSE 费用报销单不挂本守卫**(无供应商锚点、不进对账),其编辑/删除/驳回行为不变
- 正常路径下 PR-1 的 599412 门槛已要求 APPROVED/PAID 才可挂票,而生效单本就不可编辑/删除/驳回,故本组钩子**主要拦存量脏数据**(PR-1 之前挂到非生效单上的关联)与冲抵重算接缝
- 守卫与单据写操作同事务:守卫抛出即整单回滚,不会出现「单改了但校验没过」的中间态
## 十、修改前后对比
| 维度 | 修改前 | 修改后 |
|------|--------|--------|
| 编辑应付/预付草稿改价 | 只验状态(PENDING)+ 金额合法,可改到任意值 | 额外验:改后金额 ≥ 已匹配票额,否则 **599413** 整单回滚 |
| 删除应付/预付草稿 | 只验状态(PENDING),软删即走 | 额外验:无非作废收票关联,否则 **599414** 不删单 |
| 驳回应付/预付 | 只验状态(SUBMITTED) | 额外验:无非作废收票关联,否则 **599414** 状态不推进 |
| 入参/出参字段结构 | — | **零变化**(无字段增删改名,仅新增 2 个错误码 + 6 接口行为收紧) |
| 无票 / 票已作废的单据 | 正常删改驳回 | **不变**,照常放行 |
## 十一、影响评估 / 回滚
**是否破坏兼容**:行为级破坏(窄面)。原先「有票关联的草稿可删可改可驳回」的请求现在可能报 599413/599414;接口签名、字段、其余错误码均不变。
**前端需要同步上线吗**:不需要强同步,但建议在应付/预付的**编辑草稿、删除草稿、驳回**三处操作的错误提示里覆盖 599413 / 599414 两个新错误码(文案可直接用 msg)。正常业务流(PR-1 门槛 + 生效单不可删改)下极少触发,主要面向存量脏数据。
**回滚方案**:后端回滚本 PR 即恢复旧行为(不再拦截);无 DDL、无 Nacos 配置变更,前端无需配合回滚。存量数据不受影响(钩子只拦写操作,不做数据订正)。
## 十二、注意事项
1. **两个新错误码的 msg 已含引导动作**(「须先作废或改票」),前端可直接展示 msg 作为错误提示
2. 599413 的比较口径:应付取**冲抵后实付额** actualPayAmount(amount − ΣprepayOffsets 冲抵金额),不是表单里的 amount 原值;预付取 amount。编辑应付时若同时改了冲抵明细,触发 599413 的门槛以重算后的实付额为准
3. 匹配额统计**剔 VOIDED 作废票**:先把挂着的票作废,匹配额立即释放,单据即可正常删改——这是解除拦截的标准动作
4. EXPENSE 费用报销单不在本守卫范围,报销单操作不会出现这两个错误码
5. 本组钩子是 fail-fast 硬拦,**不存在 WARN 放行**路径;业务上确需解除拦截时,正确做法是作废/改票释放匹配额
## 十三、关联 / 联系人
- **Issue**: [#8164 收票核销硬钩稽 PR-2——钱侧防御钩子](https://git.1814.love:8443/wx/HL/issues/8164)
- **PR**: [#8165 feat(finance): 收票核销硬钩稽 PR-2——InvoiceInRelGuard 钱侧防御钩子 + 应付/预付六处接线](https://git.1814.love:8443/wx/HL/pulls/8165)
- **Commit**: [c4a1f1fb69](https://git.1814.love:8443/wx/HL/commit/c4a1f1fb691277bfa7590b1db24e6ef7654777e6)
- **关联前置**: PR-1 [#8163 挂票生效态门槛(599412)+ biz-recon 下钻](https://git.1814.love:8443/wx/HL/pulls/8163)(changelog 已推)
- **后端负责人**: 腰苏图(yst)