diff --git a/changelogs-v2/2026-09/22_8161_收票核销硬钩稽挂票门槛与单据对账下钻-修改接口-管理后台.md b/changelogs-v2/2026-09/22_8161_收票核销硬钩稽挂票门槛与单据对账下钻-修改接口-管理后台.md new file mode 100644 index 00000000..e3f52d3c --- /dev/null +++ b/changelogs-v2/2026-09/22_8161_收票核销硬钩稽挂票门槛与单据对账下钻-修改接口-管理后台.md @@ -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)