📝 新增收票管理同步金蝶全票池发票(A801-1 查询+批量同步+定时绑定关联)业务流程文档

This commit is contained in:
刘泉佳
2026-09-21 13:19:10 +08:00
parent 738c5c2539
commit 9a82ed2eaf
@@ -0,0 +1,206 @@
# 收票管理同步金蝶全票池发票(A801-1 查询 + 批量同步 + 定时绑定关联)业务流程
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)
> 对接系统:金蝶发票云旗舰版·票易通(piaozone)全票池,经业财融合 BFI 网关(rim 契约)透传
> 关联文档:`doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`(进项收票 A801-1 章节)、`2026-09-17-BFI分页查询通用翻页工具(BfiUtil-queryAllPages)技术文档.md`、`2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md`、`2026-09-17-客商档案同步金蝶(A902-1查询回填+A902-2审批推送)技术文档.md`
> 最后更新:2026-09-20
---
## 1. 背景与范围
收票管理(`/payment/invoice-receipt`)当前依赖本地镜像表 `blade_kingdee_invoice_pool`(金蝶进项发票票据池镜像)选票登记,镜像数据无自动采集来源;「批量同步」按钮(权限码 `invoice_receipt_sync`)调用的 `syncKingdee` 仅回读本地镜像复制 `kingdeeBillNo/kingdeeStatus`,**并未真正请求金蝶**。本文档建设两条真实同步链路:
- **链路一(手动批量同步)**:收票管理列表勾选「审批通过」的收票登记 → 批量同步 → 后端逐单按发票号码调用 **A801-1 查询全票池发票数据** → 刷新票据池镜像并绑定/回填同步状态。
- **链路二(定时任务)**:新增定时任务分页拉取 A801-1 全票池数据(增量窗口)→ 按 `invoiceNo` upsert 镜像 → **与收票登记做绑定关联**(按发票号码回填 `kingdeeInvoicePoolId`、金蝶流水号与同步状态),覆盖金蝶侧采票滞后于收票登记的场景。
不在本期范围:A801-2 发票附件文件下载(`attachmentsJson` 仅记录 `downloadUrl` 备用)、收票单推送金蝶(A303-2)、收票认领结果与正式结算单自动认领。
## 2. 对接接口(A801-1)
| 项 | 说明 |
| --- | --- |
| 接口 | A801-1 查询全票池发票数据(票易通 rim 契约,经 BFI 网关 `/kapi/app/rim/message` 透传) |
| 既有能力 | `IBfiService.queryInvoicePool(BfiInvoicePoolQueryDTO)` 单页查询,本次**无改动复用** |
| 请求体 | `{ messageType: "invoiceQuery", messageId: <服务端填充>, queryParam: { pageNo, pageSize, ...筛选 } }`,`pageSize` 上限 500,`pageNo` 从 1 开始 |
| 响应 | `{ data: [发票...], errorCode, message, status }`,**不返回总数、无末页标识**;成功码 0000 由 `BfiServiceImpl.commonCall` 统一处理 |
| 关键查询条件 | `invoiceNo`(发票号码,手动同步按此精确查询)、`updateTime/updateTimeEnd`(更新时间窗口,定时任务增量拉取)、`buyerTaxNo/salerTaxNo`(税号)、`expenseStatus`(单据使用状态)、`invoiceDateStart/End`(开票日期区间) |
| 关键返回字段 | `invoiceNo` 发票号码、`serialNo` 发票流水号(**票池唯一标识**)、`invoiceDate`、`invoiceType`、`totalAmount` 价税合计、`taxAmount`、`taxRate`、`buyerName/buyerTaxNo`、`salerName/salerTaxNo`、`checkStatus` 查验状态、`delete` 是否作废删除、`downloadUrl` |
## 3. 业务流程
### 3.1 集成链路总览
```
金蝶发票云全票池(A801-1 查询全票池发票数据)
│
├── 链路一:手动批量同步(收票管理列表「批量同步」按钮)
│ 勾选 approvalStatus = approved 的收票登记(前端已限定)
│ → 逐单 POST /invoice-receipt/sync-kingdee
│ → 后端按登记发票号码精确查询 A801-1
│ → 命中:upsert 镜像 + 绑定 kingdeeInvoicePoolId + 回填金蝶流水号/同步状态
│ → 未命中:置 unsynced,流水记录原因「金蝶票池未查询到」
│
└── 链路二:定时任务(InvoiceReceiptKingdeeSyncTask,默认关闭)
→ A801-1 按 updateTime 增量窗口分页拉取(pageSize 500,最大 50 页保护)
→ 按 invoiceNo upsert 镜像 blade_kingdee_invoice_pool
→ 绑定关联:未绑定镜像的收票登记按 invoiceNo 匹配回填
→ 已绑定登记:跟随镜像刷新金蝶流水号/同步状态(delete=1 → failed)
```
### 3.2 链路一:手动批量同步时序
```
invoice-receipt.vue InvoiceReceiptController InvoiceReceiptServiceImpl 金蝶(BFI网关)
│ 批量同步按钮 │ │ │
│ 勾选 approved 行(前端校验) │ │ │
│ 逐单 syncKingdee(id) ─────────>│ POST /invoice-receipt/ │ │
│ │ sync-kingdee?id=xxx │ │
│ │ ──────────────────────────────>│ ①校验 approvalStatus=approved │
│ │ │ ②按 invoiceNo ───────────────>│ A801-1
│ │ │ <── 票池发票(0/1条)──────────│
│ │ │ ③命中:upsert 镜像(按 invoiceNo)│
│ │ │ 绑定 kingdeeInvoicePoolId │
│ │ │ 回填 kingdeeBillNo=serialNo │
│ │ │ 刷新 kingdeeStatus;写 sync 流水│
│ │ │ ④未命中:kingdeeStatus=unsynced │
│ │ │ 写 sync 流水(原因) │
│ <── 同步结果(金蝶流水号)──────────────────────────────────────│ │
```
前端 `handleBatchSync` 现有逻辑(`invoice-receipt.vue:366`)保持不变:仅允许勾选「审批通过」行、`Promise.all` 逐单调单条接口,失败由 axios 拦截器统一提示。
### 3.3 链路二:定时绑定时序
```
InvoiceReceiptKingdeeSyncTask InvoiceReceiptServiceImpl 金蝶(BFI网关)
│ @Scheduled cron(可配) │ │
│ syncEnabled 开关校验 │ │
│ syncKingdeeInvoicePool() ───────>│ ①增量窗口(滚动近N天)+ 发票种类/客商税号白名单 │
│ │ ②A801-1 分页拉取(pageSize 500,最大50页)────>│
│ │ <── 票池发票列表 ────────────────────────────│
│ │ ③白名单过滤(购销双方税号须命中客商档案)后按号批量 upsert 镜像│
│ │ ④绑定回填批量落库(页级事务,批量失败整页跳过) │
│ │ ⑤补偿绑定:未绑定登记按发票号码精确补查(不限窗口)──>│
│ │ 单轮限量、批量落库,直到绑定成功为止 │
│ <── 执行摘要日志(拉取/过滤/镜像/绑定/刷新/失败/补偿统计)│ │
```
- 任务类仿 `PaymentKingdeeResultTask` / `CustomerKingdeeSyncTask`:业务逻辑收敛在 `IInvoiceReceiptService.syncKingdeeInvoicePool()`,任务类只做开关与调度,便于手动触发联调。
- 增量窗口约定:**每轮固定滚动近 N 天**(可配,建议 30 天),不依赖"上次成功时间点"——单轮失败不会漏,同一批数据会被后续每轮反复幂等覆盖。
- **客商税号白名单(购、销双方)**:每轮加载客商档案有效税号(`unifiedCreditCode`,status=1),窗口拉回的发票须**购方税号与销方税号同时命中**该集合才入镜像、参与绑定,白名单外发票不入镜像不绑定;**集合为空不限制**(档案未维护时兼容全量)。`buyerTaxNo`/`salerTaxNo` 均为 rim 契约单值参数,无法按税号集合远程查询,故采用窗口拉取 + 内存过滤的等价实现。
- **防漏绑补偿**:滚动窗口唯一的漏绑缝隙是「持续失败超过 N 天(窗口滑出 updateTime)」,第 ⑤ 步兜底——仍为未绑定(`kingdeeInvoicePoolId` 为空)的登记按发票号码精确补查 A801-1(优先复用本地镜像,缺失才远程),单轮限量(`compensateLimit`,默认 200,按创建时间先进先出),绑定成功即移出补查队列;金蝶票池暂无该发票时跳过不写库、待后续轮次。**手动同步与补偿绑定不受税号白名单约束**(票由登记侧发起,属我方票)。
- 提供手动触发端点 `POST /invoice-receipt/sync-invoice-pool`(权限码复用 `invoice_receipt_sync`),返回执行摘要,供联调与补偿。
### 3.4 匹配与幂等规则
| 约定 | 说明 |
| --- | --- |
| 匹配键 | `invoiceNo`(发票号码)。收票登记侧 `assertInvoiceUnused` 已保证一张发票仅可登记一次,镜像侧按 `invoiceNo` upsert 天然幂等 |
| 发票种类白名单 | `thirdParty.bfi.invoiceReceipt.invoiceTypes`(逗号分隔票易通编码,如 `26,27`)控制可同步的发票种类:白名单外发票**不落镜像不绑定**(手动同步记「发票类型不在同步范围」流水,定时拉取在类型过滤统计中体现);配置为空不限制。定时拉取翻页终止仍按原始页条数判断,不受过滤影响 |
| 客商税号白名单 | 客商档案有效税号(`unifiedCreditCode`,status=1)为白名单,窗口拉回的发票须**购方税号与销方税号同时命中**才入镜像、参与绑定;**集合为空不限制**。与发票种类白名单叠加生效;手动同步与补偿绑定不受此约束 |
| 镜像批量写库 | 同页镜像**一次批量查询**既有记录(`invoiceNo IN 同页号码`)+ `Db.saveBatch` / `Db.updateBatchById` **批量写入**(避免逐条查改 N+1);批量写失败中断本轮,由任务层记录异常、下轮重试(upsert 幂等) |
| 绑定批量落库 | 绑定回填先在**内存**完成(登记实体按页批量查出,票面字段不覆盖),再以**页级事务**批量落库:`updateBatchById` 登记绑定 + `Db.saveBatch` 同步流水(`applyInvoicePoolSyncBatch`,JDBC batch)。批量失败**整页跳过**计入失败统计,下轮任务幂等重试,**无逐单降级路径**(`applyInvoicePoolSyncResult` 仅手动单条同步使用) |
| 防漏绑补偿 | 增量窗口为滚动近 N 天,单轮失败不漏;唯一缝隙是「持续失败超 N 天窗口滑出」。补偿绑定步骤对未绑定登记(`kingdeeInvoicePoolId` 为空、非作废)批量补绑:本地镜像**一次批量查询**复用(主流程本轮已落库镜像命中免远程),缺失才按发票号码**精确补查** A801-1(rim 契约无批量号码参数,仅此处逐号远程),新增镜像批量写入 + 绑定页级事务批量落库;单轮限量 `compensateLimit`(默认 200)先进先出,绑定成功即出队;票池暂无则跳过待下轮,不写库不刷流水 |
| 手动同步查询 | A801-1 `invoiceNo` 精确单查(rim 契约单页,返回 0/1 条);多条理论上不存在,取第一条并 warn 日志 |
| 定时绑定范围 | `approvalStatus IN (draft, reviewing, approved, returned)` 且(`kingdeeInvoicePoolId` 为空 或 指向镜像不存在的记录);`voided` 不参与绑定 |
| 绑定回填范围 | 仅回填**绑定关系与金蝶侧信息**:`kingdeeInvoicePoolId`、`kingdeeBillNo`(=serialNo)、`kingdeeStatus`;**不覆盖**登记已保存的票面字段(发票号、金额、税额、收开票单位等),避免冲掉人工登记信息 |
| 镜像刷新口径 | 镜像表字段一律以 A801-1 返回为准(票池是唯一事实源);镜像存在即可被新登记选票 |
| 金额校验 | 绑定时若票池价税合计与登记 `invoiceAmount` 不一致仅记 warn 日志(跨系统开票金额可能变更),不阻断绑定 |
| 已绑定状态刷新 | 跟随镜像刷新 `kingdeeStatus`;票池 `delete=1`(作废删除)置 `failed`,流水记录原因,**不自动作废**收票登记,人工确认处理 |
| 逐单隔离 | 定时链路无逐单路径:镜像批量查改 + 绑定页级批量落库;仅批量落库异常时整页跳过、下轮幂等重试 |
| 关键日志 | 任务开始(增量窗口 + 发票种类 + 客商税号白名单个数)、每页拉取条数、镜像批量写入条数(新增/更新)、逐条绑定成功(发票号码 + 登记id + 金蝶流水号)、绑定异常(含堆栈)、结束汇总摘要(拉取/类型过滤/客商过滤/镜像新增更新/绑定/状态刷新/失败/补偿绑定) |
### 3.5 状态映射(kingdeeStatus 语义)
| 场景 | 本地 kingdeeStatus | 展示名 | 说明 |
| --- | --- | --- | --- |
| A801-1 命中且 `delete=0` | `synced` | 已同步 | 票池匹配成功,`kingdeeBillNo` 回填 `serialNo` |
| A801-1 未命中 | `unsynced` | 未同步 | 金蝶票池未查询到(可能未采集/发票号不符),流水记录原因 |
| A801-1 命中且 `delete=1` | `failed` | 同步失败 | 发票在金蝶侧已作废/删除,流水记录原因,人工确认 |
| 同步过程异常(接口失败) | 保持原状态 | — | 本轮终止或该单跳过,下轮重试 |
## 4. 字段映射关系
### 4.1 A801-1 → 镜像表 `blade_kingdee_invoice_pool`
| A801-1 字段 | 含义 | 落库字段 | 说明 |
| --- | --- | --- | --- |
| invoiceNo | 发票号码 | `invoice_no` | upsert 匹配键 |
| serialNo | 发票流水号 | `serial_no`(**新增列**) | 票池唯一标识 |
| invoiceDate | 开票日期 | `invoice_date` | yyyy-MM-dd |
| invoiceType | 发票类型编码 | `invoice_type` | 经 `BfiConstant.INVOICE_TYPE_NAMES` 编码转名称落库(与正式结算单认领口径一致) |
| taxRate | 税率(小数) | `tax_rate` | 小数转百分数(0.13 → 13),顶层为空回退首条明细 |
| totalAmount | 价税合计 | `invoice_amount` | 与正式结算单认领同口径(价税合计即发票金额) |
| taxAmount | 税额 | `tax_amount` | 2 位小数 |
| buyerName | 购方名称 | `receiver_name` | 受票单位 |
| buyerTaxNo | 购方税号 | `buyer_tax_no`(**新增列**) | 便于按组织税号过滤 |
| salerName | 销方名称 | `issuer_name` | 开票单位 |
| salerTaxNo | 销方税号 | `saler_tax_no`(**新增列**) | 便于按销方过滤 |
| checkStatus | 查验状态 | `check_status`(**新增列**) | 0 未查验 / 1 通过 / 2 不通过 |
| delete | 是否作废删除 | —(映射为登记侧 `failed`) | 不在镜像冗余存储,随查随判 |
| downloadUrl | 文件下载地址 | `attachments_json` | `{"source":"piaozone","path":"<downloadUrl>","name":"<发票号码>.pdf"}`,A801-2 接入备用 |
| updateTime/billCreateTime | 票池更新时间 | `source_updated_time` | 镜像排序字段 |
### 4.2 镜像/票池 → 收票登记绑定回填
| 来源 | 回填字段 | 说明 |
| --- | --- | --- |
| 镜像主键 | `kingdee_invoice_pool_id` | 绑定关系,绑定后登记详情/编辑可关联票池 |
| 镜像 `serial_no` | `kingdee_bill_no` | 金蝶侧唯一流水号(A801-1 无单据号概念,以流水号作金蝶标识) |
| 匹配结果 | `kingdee_status` | 见 3.5 状态映射 |
| — | `blade_invoice_receipt_record` | 每次同步写 `actionType=sync` 流水(成功/未匹配/作废原因),定时任务操作人以「定时任务」名义记录 |
## 5. 数据库变更
| 变更 | 内容 | 说明 |
| --- | --- | --- |
| 新增列 | `blade_kingdee_invoice_pool`:`serial_no` varchar(64)、`saler_tax_no` varchar(32)、`buyer_tax_no` varchar(32)、`check_status` varchar(8) | 独立幂等脚本 `doc/sql/transport/blade_kingdee_invoice_pool_a8011_20260920.sql`(参照 `blade_payment_application_kingdee_bill_id_20260917.sql` 风格,**不改动**建表模板);排序规则 `utf8mb4_general_ci` |
| 索引 | 无需新增 | 建表模板已有唯一索引 `uk_kingdee_invoice_pool_no (tenant_id, invoice_no)`,覆盖 upsert 与绑定匹配 |
| 收票表 | `blade_invoice_receipt` 无结构变更 | 复用既有 `kingdee_invoice_pool_id/kingdee_bill_no/kingdee_status` |
## 6. 配置项(Nacos:`thirdParty.bfi.invoiceReceipt.*`)
模板同步 `doc/nacos/blade-dev.yaml`、`blade-prod.yaml`、`third-party-api.yaml`:
| 配置键 | 环境变量 | 默认值 | 说明 |
| --- | --- | --- | --- |
| thirdParty.bfi.invoiceReceipt.syncEnabled | BFI_INVOICE_RECEIPT_SYNC_ENABLED | false | 定时绑定任务开关(上线初期关闭,联调稳定后开启) |
| thirdParty.bfi.invoiceReceipt.cron | BFI_INVOICE_RECEIPT_CRON | 0 */30 * * * ? | 调度表达式(默认每 30 分钟) |
| thirdParty.bfi.invoiceReceipt.incrementDays | BFI_INVOICE_RECEIPT_INCREMENT_DAYS | 30 | 首轮/兜底增量窗口天数 |
| thirdParty.bfi.invoiceReceipt.invoiceTypes | BFI_INVOICE_RECEIPT_INVOICE_TYPES | 空 | 同步发票种类白名单(逗号分隔票易通编码,如 26,27 数电普票/专票),为空不限制 |
| thirdParty.bfi.invoiceReceipt.compensateLimit | BFI_INVOICE_RECEIPT_COMPENSATE_LIMIT | 200 | 补偿绑定单轮限量(未绑定登记按发票号码精确补查条数上限) |
## 7. 异常处理
| 场景 | 行为 |
| --- | --- |
| A801-1 接口级失败(status=false / errorCode≠0000) | `BfiServiceImpl.commonCall` 抛 `BfiException`:链路一该单同步失败并提示;链路二当前页终止,下轮任务自动重试 |
| 手动同步按发票号未查到 | 登记 `kingdeeStatus=unsynced`,流水记录「金蝶票池未查询到」,不抛异常中断批量 |
| 绑定时金额不一致 | warn 日志(发票号 + 双方金额),仍按 3.4 口径绑定 |
| 单条绑定/状态刷新异常 | 记录 error 日志(含发票号与堆栈),计数后继续下一单 |
| 定时任务与手动同步并发 | 镜像按 invoiceNo upsert、登记按主键 `updateById`、绑定判重幂等,最终一致 |
| A801-1 翻页异常(rim 无总数) | 以「本页条数 < pageSize 或空页」终止,最大 50 页保护,防御死循环 |
## 8. 涉及代码位置
| 内容 | 位置 | 改动 |
| --- | --- | --- |
| 前端页面 | `tms-erp-web/src/views/payment/invoice-receipt.vue`(handleBatchSync)、`src/api/payment/invoiceReceipt.js`(syncKingdee) | 无需改动(复用现有单条同步接口与按钮) |
| 同步入口改造 | `blade-service/blade-transport/.../service/impl/InvoiceReceiptServiceImpl.java`(syncKingdee 接入 A801-1 查询 + 镜像 upsert + 绑定回填) | 修改 |
| 定时绑定编排 | 同上 `syncKingdeeInvoicePool()`(增量窗口分页拉取 + upsert + 绑定)、`applyInvoicePoolSyncResult()`(逐单独立事务:绑定回填 + 同步流水) | 新增 |
| Service 接口 | `.../service/IInvoiceReceiptService.java`(syncKingdeeInvoicePool) | 新增 |
| 定时任务 | `blade-service/blade-transport/.../task/InvoiceReceiptKingdeeSyncTask.java` | 新增(仿 PaymentKingdeeResultTask) |
| 手动触发端点 | `.../controller/InvoiceReceiptController.java`(POST /invoice-receipt/sync-invoice-pool) | 新增(可选) |
| 镜像实体 | `blade-transport-api/.../pojo/entity/KingdeeInvoicePool.java`(serialNo/salerTaxNo/buyerTaxNo/checkStatus) | 修改 |
| BFI 客户端 | `blade-third-party-api/blade-bfi-api/.../service/IBfiService.java`(queryInvoicePool)、`pojo/dto/BfiInvoicePoolQueryDataDTO.java`、`pojo/vo/BfiInvoicePoolVO.java` | 无改动复用 |
| SQL | `doc/sql/transport/`(镜像表新增列 + 索引幂等脚本) | 新增 |
| Nacos 配置模板 | `doc/nacos/blade-dev.yaml`、`blade-prod.yaml`、`third-party-api.yaml`(thirdParty.bfi.invoiceReceipt.*) | 新增 |
## 9. 遗留事项与联调确认
1. **A801-1 增量窗口口径**:`updateTime/updateTimeEnd`(yyyy-MM-dd HH:mm:ss)为票池更新时间,联调时需用真实票池数据验证窗口过滤有效性;无效则退化为按 `invoiceDateStart/End`(开票日期)滚动窗口全量比对(upsert 幂等,代价可接受)。
2. **`serialNo` 作金蝶标识**:A801-1 无金蝶单据号概念,以发票流水号回填 `kingdee_bill_no`;后续如接 A303-2 收票单推送,金蝶侧单据号语义另行扩展,避免混用。
3. **镜像存量数据**:镜像表存量记录 `serial_no` 为空,由首轮定时任务/手动同步按 invoiceNo 命中后补齐;未命中的存量数据保持原状不清理。
4. **A801-2 附件下载**:`attachmentsJson.path` 仅记录备用,查看/下载附件待 A801-2 接入后建设。
5. **票池作废联动**:`delete=1` 仅置 `failed` 提示人工确认,是否自动作废收票登记待业务确认后调整。