17 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 8161 | 收票核销硬钩稽 PR-1——登记/编辑挂票收紧为生效态门槛(新增错误码 599412)+ 新增单据维度收票对账下钻 biz-recon | admin | yst(GIT) | 修改接口 | merged | pending | verified | mmg | 5072da0b991cdcde4cc83cc5be94072eb049d224 | v2.1 | 2026-09-22 | 收票(进项发票)域挂票规则收紧:登记(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 提示。 前端已交付(5072da0b):invoice-in.js 新增 getInvoiceInBizRecon,详情「关联业务明细」行级「对账」下钻弹窗(五档金额后端字段直显、unmatchedAmount 负值红字不截断、EXPENSE 供应商空值兜底);599412 经核验前端全走 biz-candidates 勾选天然不触发,零改动。 | 2026-09-22 | dev-v3 |
收票核销硬钩稽 PR-1:挂票生效态门槛(599412)+ 单据维度收票对账下钻 biz-recon(管理后台)
服务: hl-order-service-v3(hl-finance 模块,端口 8086/8186) PR: #8163 Issue: #8161 日期: 2026-09-22 影响范围: 管理后台「财务管理 - 收票管理(进项发票)」的登记/编辑弹窗(挂关联业务单据)、供应商收票对账页的「按单据下钻」能力
一、接口背景
收票(进项发票)域已上线(#7857),主链为「登记收票 → 核对 → 作废」,供应商为对账锚点。
本次 PR 解决两个缺口:
- 挂票门槛缺失:此前登记(create)/ 编辑(update)发票挂关联业务单据时,后端只验「单据存在 + 归属本供应商」,不验单据状态——发票可以挂到草稿(PENDING)/ 审批中(SUBMITTED)/ 已驳回(REJECTED)的应付款、预付款单上。而这些非生效态单据的金额还可能被修改、单据还可能被驳回,造成「钱变了票不知道」,对账口径失真。本次对 PAYMENT / PREPAY 增加生效态硬校验:只有 APPROVED(已批准)/ PAID(已付讫)(金额已冻结、无变更入口)的单据才可挂票,否则抛新错误码 599412。
- 对账无法下钻:供应商收票对账(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
响应:
{
"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
响应:
{
"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 草稿)响应:
{
"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 配置变更,前端无需配合回滚。存量已挂非生效单的关联行不会被追溯清理,仍正常计入匹配额。
十二、注意事项
- 两个对账接口是「有意的不同口径」,不要互相对数:
- 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 拆两档正是为解释这个差异。前端不要把两个接口的金额互相核对相等,也不要混用字段名
- bizId / supplierId 是 JSON 字符串(Long 经 ToStringSerializer 序列化,防 JS 精度丢失);matchedInvoiceCount 是普通 JSON number
- bizTypeName / billStatusName 由后端返回中文,直接展示,不要再在前端维护硬编码 map
- EXPENSE 单的 supplierId / supplierName 恒为 null,展示时注意空值兜底
- biz-recon 对非法 bizType、不存在的 bizId 统一报 599407,不区分「类型错」与「单不存在」
十三、关联 / 联系人
- Issue: #8161 收票核销硬钩稽
- PR: #8163 feat(finance): 收票核销硬钩稽 PR-1——挂票生效态门槛 + 单据维度对账下钻接口
- Commit: 90739708e5
- 设计文档(后端内部): 收票核销硬钩稽设计(PR-1 挂票门槛 + biz-recon 下钻;PR-2 钱变票钩子后续另行推送)
- 后端负责人: 腰苏图(yst)