docs(changelog): #8161 收票核销硬钩稽PR-1——挂票生效态门槛599412+单据维度对账下钻biz-recon(修改接口-管理后台)
changelog-filename-gate / validate (push) Failing after 2s
changelog-filename-gate / validate (push) Failing after 2s
这个提交包含在:
@@ -0,0 +1,289 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8161"
|
||||
title: "收票核销硬钩稽 PR-1——登记/编辑挂票收紧为生效态门槛(新增错误码 599412)+ 新增单据维度收票对账下钻 biz-recon"
|
||||
consumer: "admin"
|
||||
author: "yst(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "merged"
|
||||
gateway_status: "pending"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "收票(进项发票)域挂票规则收紧:登记(create)/编辑(update)挂 PAYMENT/PREPAY 关联单时新增生效态硬校验,非 APPROVED/PAID 抛新错误码 599412(此前可挂草稿/审批中/已驳回单);同时新增单据维度收票对账下钻 GET /admin/finance/invoice-in/biz-recon。EXPENSE 关联规则不变。已合并 dev-v3(PR #8163,merge commit 90739708e5),待部署测试服后验证。前端走 biz-candidates 勾选关联的链路天然不触发 599412(候选只出 APPROVED/PAID 单);若有手填 bizId 或复用存量草稿单关联的入口需处理 599412 提示。"
|
||||
updated_at: "2026-09-22"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 收票核销硬钩稽 PR-1:挂票生效态门槛(599412)+ 单据维度收票对账下钻 biz-recon(管理后台)
|
||||
|
||||
> **服务**: hl-order-service-v3(hl-finance 模块,端口 8086/8186)
|
||||
> **PR**: #8163
|
||||
> **Issue**: #8161
|
||||
> **日期**: 2026-09-22
|
||||
> **影响范围**: 管理后台「财务管理 - 收票管理(进项发票)」的登记/编辑弹窗(挂关联业务单据)、供应商收票对账页的「按单据下钻」能力
|
||||
|
||||
---
|
||||
|
||||
## 一、接口背景
|
||||
|
||||
收票(进项发票)域已上线(#7857),主链为「登记收票 → 核对 → 作废」,供应商为对账锚点。
|
||||
|
||||
本次 PR 解决两个缺口:
|
||||
|
||||
1. **挂票门槛缺失**:此前登记(create)/ 编辑(update)发票挂关联业务单据时,后端只验「单据存在 + 归属本供应商」,**不验单据状态**——发票可以挂到草稿(PENDING)/ 审批中(SUBMITTED)/ 已驳回(REJECTED)的应付款、预付款单上。而这些非生效态单据的金额还可能被修改、单据还可能被驳回,造成「钱变了票不知道」,对账口径失真。本次对 PAYMENT / PREPAY 增加生效态硬校验:只有 **APPROVED(已批准)/ PAID(已付讫)**(金额已冻结、无变更入口)的单据才可挂票,否则抛新错误码 **599412**。
|
||||
2. **对账无法下钻**:供应商收票对账(supplier-recon)只有供应商维度合计,看不到单张业务单据的收票进度。本次新增单据维度下钻接口 biz-recon,按 bizType + bizId 查单张单据的应收票额 / 已匹配额(拆「已核对」「已收票未核对」两档)/ 未匹配额。
|
||||
|
||||
## 二、变更清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 |
|
||||
|---|------|------|------|----------|
|
||||
| 1 | 单据维度收票对账下钻 | GET | /admin/finance/invoice-in/biz-recon | 新增接口 |
|
||||
| 2 | 登记进项发票 | POST | /admin/finance/invoice-in/create | 行为收紧:PAYMENT/PREPAY 关联单须 APPROVED/PAID,否则 599412 |
|
||||
| 3 | 编辑进项发票 | PUT | /admin/finance/invoice-in/update | 行为收紧:同 #2(关联走全删重插,门槛自动覆盖存量数据) |
|
||||
|
||||
入参结构、出参结构对 #2 #3 均**零变化**(字段名/类型/必填全不变),仅服务端校验规则收紧 + 新增一个错误码。
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
| 接口 | 使用场景 | 认证 | 幂等性 | 限流 |
|
||||
|------|----------|------|--------|------|
|
||||
| GET /admin/finance/invoice-in/biz-recon | 收票对账页从供应商/单据行下钻,看单张应付/预付/费用报销单的收票进度与未匹配额 | 管理后台 JWT | 只读,天然幂等 | 网关默认 |
|
||||
| POST /admin/finance/invoice-in/create | 登记一张进项发票并可挂多笔业务单据 | 管理后台 JWT | 非幂等(发票号码唯一,重复登记 599401) | 网关默认 |
|
||||
| PUT /admin/finance/invoice-in/update | 编辑已收票(RECEIVED)态发票,关联全删重插 | 管理后台 JWT | 非幂等(同内容重复提交幂等) | 网关默认 |
|
||||
|
||||
## 四、接口入参
|
||||
|
||||
### 4.1 biz-recon Query 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| bizType | String | 是 | 业务类型:PAYMENT 应付款 / PREPAY 预付款 / EXPENSE 费用报销(大小写不敏感,取值域外按 599407 处理) |
|
||||
| bizId | Long | 是 | 业务单据 ID(应付单/预付单/费用报销单主键;查无此单 → 599407) |
|
||||
|
||||
### 4.2 create / update 请求体关联字段(结构不变,仅校验收紧)
|
||||
|
||||
请求体整体结构同既有版本(发票号码/发票类型/供应商/价税合计/日期/影像/备注等,见 #7857 changelog),本次只涉及 relations 数组元素的校验规则:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| relations[].bizType | String | 是 | PAYMENT / PREPAY / EXPENSE |
|
||||
| relations[].bizId | Long | 是 | 业务单据 ID。**新规则**:PAYMENT/PREPAY 单据状态必须是 APPROVED 或 PAID,否则整单拒绝并报 599412;EXPENSE 维持只验存在 |
|
||||
| relations[].matchAmount | BigDecimal | 是 | 本票对该笔的匹配金额(>0;ΣmatchAmount > 票面金额 → 599406) |
|
||||
|
||||
## 五、出参字段(biz-recon → InvoiceInBizReconRespVO)
|
||||
|
||||
统一响应包装 Result<InvoiceInBizReconRespVO>,data 字段如下:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| bizType | String | 业务类型英文码(PAYMENT/PREPAY/EXPENSE) |
|
||||
| bizTypeName | String | 业务类型中文名(应付款/预付款/费用报销),**直接展示,勿前端再硬编码 map** |
|
||||
| bizId | String | 业务单据 ID(Long 主键序列化为**字符串**,防 JS 精度丢失) |
|
||||
| bizNo | String | 业务单据号(快照,如应付单号/预付单号/报销单号) |
|
||||
| billStatus | String | 单据状态英文码:PENDING/SUBMITTED/APPROVED/REJECTED/PAID |
|
||||
| billStatusName | String | 单据状态中文名(草稿/审批中/已批准/已驳回/已付讫),直接展示 |
|
||||
| supplierId | String 或 null | 供应商 ID(Long 序列化为字符串);**EXPENSE 无供应商锚点,恒为 null** |
|
||||
| supplierName | String 或 null | 供应商名称(快照);EXPENSE 恒为 null |
|
||||
| billAmount | BigDecimal | 应收票额:PAYMENT 取实付金额 actual_pay_amount / PREPAY 取金额 / EXPENSE 取金额 |
|
||||
| matchedVerifiedAmount | BigDecimal | 已核对票匹配额:该单关联的 **VERIFIED** 发票的 Σmatch_amount(剔作废票) |
|
||||
| matchedReceivedAmount | BigDecimal | 已收票未核对匹配额:该单关联的 **RECEIVED** 发票的 Σmatch_amount(剔作废票) |
|
||||
| matchedAmount | BigDecimal | 已匹配额合计 = matchedVerifiedAmount + matchedReceivedAmount |
|
||||
| unmatchedAmount | BigDecimal | 未匹配额 = billAmount − matchedAmount,**可为负数(= 多收票),前端展示不要 max(0,·) 截断** |
|
||||
| matchedInvoiceCount | Long | 匹配发票张数(剔作废票,JSON number 非字符串);无任何匹配时为 0,各匹配额为 0 |
|
||||
|
||||
## 六、枚举 / 数据字典
|
||||
|
||||
### 6.1 bizType(业务类型)
|
||||
|
||||
| 值 | 中文名 | 说明 |
|
||||
|----|--------|------|
|
||||
| PAYMENT | 应付款 | 应收票额取实付金额(actual_pay_amount,含冲抵后口径) |
|
||||
| PREPAY | 预付款 | 应收票额取金额 |
|
||||
| EXPENSE | 费用报销 | 无供应商锚点(supplierId/supplierName 为 null),不进供应商对账 |
|
||||
|
||||
### 6.2 billStatus(单据状态,应付/预付/费用报销三域状态名一致)
|
||||
|
||||
| 值 | 中文名 | 是否可挂票 |
|
||||
|----|--------|-----------|
|
||||
| PENDING | 草稿 | 否(599412) |
|
||||
| SUBMITTED | 审批中 | 否(599412) |
|
||||
| APPROVED | 已批准 | **是** |
|
||||
| REJECTED | 已驳回 | 否(599412) |
|
||||
| PAID | 已付讫 | **是** |
|
||||
|
||||
> 「是否可挂票」只对 PAYMENT/PREPAY 强制;EXPENSE 不验状态。
|
||||
|
||||
### 6.3 发票状态(匹配额拆分依据,本次无新增值)
|
||||
|
||||
| 值 | 中文名 | 是否计入匹配额 |
|
||||
|----|--------|---------------|
|
||||
| RECEIVED | 已收票 | 计入 matchedReceivedAmount |
|
||||
| VERIFIED | 已核对 | 计入 matchedVerifiedAmount |
|
||||
| VOIDED | 已作废 | **不计入**(作废自动释放匹配额) |
|
||||
|
||||
## 七、错误码
|
||||
|
||||
| 错误码 | 常量 | 消息 | 触发场景 |
|
||||
|--------|------|------|----------|
|
||||
| **599412** | INVOICE_IN_REL_BIZ_NOT_EFFECTIVE | 关联业务单据未生效(应付/预付须已批准或已付讫才可收票) | **本次新增**。create/update 的 relations 挂 PAYMENT/PREPAY 且单据状态非 APPROVED/PAID 时抛出,整单拒绝 |
|
||||
| 599407 | INVOICE_IN_REL_BIZ_NOT_FOUND | 关联业务单据不存在 | biz-recon:bizType 取值域外或 bizId 查无此单;create/update:单据不存在、或不归属本发票的开票供应商 |
|
||||
| 599405 | INVOICE_IN_AMOUNT_INVALID | 发票金额非法 | relations[].matchAmount 为空或 ≤0(既有) |
|
||||
| 599406 | INVOICE_IN_AMOUNT_EXCEEDED | 匹配金额合计超过票面金额 | ΣmatchAmount > invoiceAmount(既有) |
|
||||
| 599411 | INVOICE_IN_REL_DUPLICATED | 同一张票重复关联同一业务单据 | 同一请求内 bizType+bizId 重复(既有) |
|
||||
|
||||
**599412 与 599407 的区别**:407 = 单据不存在 / 不归属本供应商;412 = 单据存在且归属正确,但**状态未生效**。前端错误提示文案可直接用 message,也可自行区分「选错单」与「单未生效」两种引导。
|
||||
|
||||
## 八、示例
|
||||
|
||||
### 8.1 典型成功 —— 应付单下钻,部分收票
|
||||
|
||||
请求:
|
||||
|
||||
GET /admin/finance/invoice-in/biz-recon?bizType=PAYMENT&bizId=1982736450011223
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"bizType": "PAYMENT",
|
||||
"bizTypeName": "应付款",
|
||||
"bizId": "1982736450011223",
|
||||
"bizNo": "PAY-202609-0042",
|
||||
"billStatus": "PAID",
|
||||
"billStatusName": "已付讫",
|
||||
"supplierId": "1877665544332211",
|
||||
"supplierName": "满洲里蓝天旅行社有限公司",
|
||||
"billAmount": 12000.00,
|
||||
"matchedVerifiedAmount": 8000.00,
|
||||
"matchedReceivedAmount": 2000.00,
|
||||
"matchedAmount": 10000.00,
|
||||
"unmatchedAmount": 2000.00,
|
||||
"matchedInvoiceCount": 2
|
||||
},
|
||||
"msg": ""
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 边界情况 —— EXPENSE 无匹配发票(各匹配额为 0、供应商字段为 null)
|
||||
|
||||
请求:
|
||||
|
||||
GET /admin/finance/invoice-in/biz-recon?bizType=EXPENSE&bizId=1966112233445566
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"bizType": "EXPENSE",
|
||||
"bizTypeName": "费用报销",
|
||||
"bizId": "1966112233445566",
|
||||
"bizNo": "EXP-202609-0118",
|
||||
"billStatus": "APPROVED",
|
||||
"billStatusName": "已批准",
|
||||
"supplierId": null,
|
||||
"supplierName": null,
|
||||
"billAmount": 3500.00,
|
||||
"matchedVerifiedAmount": 0,
|
||||
"matchedReceivedAmount": 0,
|
||||
"matchedAmount": 0,
|
||||
"unmatchedAmount": 3500.00,
|
||||
"matchedInvoiceCount": 0
|
||||
},
|
||||
"msg": ""
|
||||
}
|
||||
```
|
||||
|
||||
> 另一典型边界:**多收票**时 unmatchedAmount 为负数。例如 billAmount=10000、matchedAmount=12000,则 unmatchedAmount=-2000.00,属于有意返回,请原样展示(负值即「多收票」预警)。
|
||||
|
||||
### 8.3 业务失败 —— 登记发票挂「草稿态」应付单触发 599412
|
||||
|
||||
请求:
|
||||
|
||||
POST /admin/finance/invoice-in/create
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"invoiceNo": "INV-2026-092201",
|
||||
"invoiceType": "SPECIAL",
|
||||
"supplierId": 1877665544332211,
|
||||
"invoiceAmount": 5000.00,
|
||||
"invoiceDate": "2026-09-20",
|
||||
"receiveDate": "2026-09-22",
|
||||
"relations": [
|
||||
{ "bizType": "PAYMENT", "bizId": 1982736450000001, "matchAmount": 5000.00 }
|
||||
]
|
||||
}
|
||||
|
||||
(该应付单状态为 PENDING 草稿)响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 599412,
|
||||
"data": null,
|
||||
"msg": "关联业务单据未生效(应付/预付须已批准或已付讫才可收票)"
|
||||
}
|
||||
```
|
||||
|
||||
整单拒绝,发票不落库。update 链路表现相同。
|
||||
|
||||
## 九、业务边界
|
||||
|
||||
**适用场景**
|
||||
- 收票对账页:从供应商对账行 / 单据行下钻,看单张应付/预付/费用报销单的收票进度(biz-recon)
|
||||
- 登记/编辑发票:挂已批准或已付讫的应付/预付单、任意状态的费用报销单
|
||||
|
||||
**不适用场景**
|
||||
- biz-recon 是**单查**接口(一次一单),不是分页列表,不要拿它批量刷数
|
||||
- EXPENSE 费用报销无供应商锚点,不进供应商维度对账(supplier-recon 只覆盖应付/预付);biz-recon 支持查 EXPENSE 仅用于展示
|
||||
- 非生效态应付/预付单**不可**作为挂票对象(本次收紧点),「先挂票后补批」的流程不再可行
|
||||
|
||||
**特殊边界**
|
||||
- 通过「可关联业务单据候选」接口(GET /admin/finance/invoice-in/biz-candidates)勾选的链路**天然不触发 599412**——候选接口本来就只返回 APPROVED/PAID 生效单,与本次门槛口径一致。只有手填 bizId、直调接口、或编辑「存量挂草稿单」的发票时才可能撞上 599412
|
||||
- 作废(VOIDED)发票自动释放其匹配额,释放后 biz-recon 的各匹配额即时下降,无需前端额外刷新逻辑(重新调接口即可)
|
||||
|
||||
## 十、修改前后对比
|
||||
|
||||
| 维度 | 修改前 | 修改后 |
|
||||
|------|--------|--------|
|
||||
| create/update 挂 PAYMENT/PREPAY | 只验「单据存在 + 归属本供应商」,草稿/审批中/已驳回单**均可**挂票成功 | 额外验状态:非 APPROVED/PAID → 整单拒绝,报 **599412** |
|
||||
| create/update 挂 EXPENSE | 只验存在 | **不变**,仍只验存在 |
|
||||
| update 存量数据 | 编辑时关联全删重插,旧校验不拦非生效单 | 编辑一张「存量挂草稿单」的发票时,重插校验触发 599412,需先移除失效关联才能保存 |
|
||||
| 单据维度对账 | 无接口(只有供应商维度 supplier-recon) | 新增 GET /admin/finance/invoice-in/biz-recon |
|
||||
| 入参/出参字段结构 | — | **零变化**(无字段增删改名;仅新增 biz-recon 接口与 599412 错误码) |
|
||||
|
||||
## 十一、影响评估 / 回滚
|
||||
|
||||
**是否破坏兼容**:行为级破坏(窄面)。原先「挂非生效态应付/预付单」能成功的请求现在报 599412 失败;接口签名、字段、其余错误码均不变。
|
||||
|
||||
**前端需要同步上线吗**:不需要强同步,但建议排查:
|
||||
- 若登记/编辑弹窗的关联单来源**全部**走 biz-candidates 候选勾选 → 零改动,不会触发 599412
|
||||
- 若存在**手填 bizId / 直调接口 / 复制历史发票**等绕过候选的入口 → 需要把 599412 纳入错误提示(文案可直接用 msg),并引导用户先完成单据审批
|
||||
- 若有「多收票」展示位置 → 注意 unmatchedAmount 可为负,不要做 max(0, x) 截断
|
||||
|
||||
**回滚方案**:后端回滚本 PR 即恢复旧行为(不验状态、biz-recon 404);无 DDL、无 Nacos 配置变更,前端无需配合回滚。存量已挂非生效单的关联行**不会被追溯清理**,仍正常计入匹配额。
|
||||
|
||||
## 十二、注意事项
|
||||
|
||||
1. **两个对账接口是「有意的不同口径」,不要互相对数**:
|
||||
- biz-recon 的 matchedAmount = **Σ match_amount(关联行口径)**,计 VERIFIED + RECEIVED 两档发票对该单的匹配金额之和
|
||||
- supplier-recon 的 receivedAmount = **Σ invoice_amount(整票口径)**,仅计 VERIFIED 发票的整票价税合计
|
||||
- 一张 RECEIVED 未核对的票:在 biz-recon 里计入 matchedReceivedAmount,在 supplier-recon 里**不计**;一张票只部分匹配某单时:biz-recon 计 match_amount 部分,supplier-recon 核对后计整票 invoice_amount。两者天然不等,matchedVerifiedAmount / matchedReceivedAmount 拆两档正是为解释这个差异。前端不要把两个接口的金额互相核对相等,也不要混用字段名
|
||||
2. bizId / supplierId 是 **JSON 字符串**(Long 经 ToStringSerializer 序列化,防 JS 精度丢失);matchedInvoiceCount 是普通 JSON number
|
||||
3. bizTypeName / billStatusName 由后端返回中文,直接展示,**不要**再在前端维护硬编码 map
|
||||
4. EXPENSE 单的 supplierId / supplierName 恒为 null,展示时注意空值兜底
|
||||
5. biz-recon 对非法 bizType、不存在的 bizId 统一报 599407,不区分「类型错」与「单不存在」
|
||||
|
||||
## 十三、关联 / 联系人
|
||||
|
||||
- **Issue**: [#8161 收票核销硬钩稽](https://git.1814.love:8443/wx/HL/issues/8161)
|
||||
- **PR**: [#8163 feat(finance): 收票核销硬钩稽 PR-1——挂票生效态门槛 + 单据维度对账下钻接口](https://git.1814.love:8443/wx/HL/pulls/8163)
|
||||
- **Commit**: [90739708e5](https://git.1814.love:8443/wx/HL/commit/90739708e5b7c5630233c3a3cf72645d6c0c9a5c)
|
||||
- **设计文档(后端内部)**: 收票核销硬钩稽设计(PR-1 挂票门槛 + biz-recon 下钻;PR-2 钱变票钩子后续另行推送)
|
||||
- **后端负责人**: 腰苏图(yst)
|
||||
在新工单中引用
屏蔽一个用户