diff --git a/changelogs-v2/2026-09/22_8164_收票核销硬钩稽钱侧防御钩子-修改接口-管理后台.md b/changelogs-v2/2026-09/22_8164_收票核销硬钩稽钱侧防御钩子-修改接口-管理后台.md new file mode 100644 index 00000000..c152b926 --- /dev/null +++ b/changelogs-v2/2026-09/22_8164_收票核销硬钩稽钱侧防御钩子-修改接口-管理后台.md @@ -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<Void>({ "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)