Merge branch 'feature/payment-apply-bfi-sync' into feature/hcs-api-p0

# Conflicts:
#	blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerKingdeeSyncTask.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/task/PaymentKingdeeResultTask.java
#	doc/nacos/blade.yaml
#	doc/nacos/third-party-api.yaml
This commit is contained in:
刘泉佳
2026-10-15 04:07:13 +08:00
556 changed files with 35208 additions and 6751 deletions
@@ -4,7 +4,7 @@
> 对接系统:金蝶云·星瀚(业财融合 BFI,经 iPaaS 网关)
> 覆盖接口:A302-2 财务应收单保存、A302-4 财务应收单查询、A303-1 开票申请单新增、A303-4 开票申请单查询、A303-5 开票申请发票查询
> 关联文档:`2026-09-20-应收结算与开票申请同步金蝶设计.md`(设计口径与遗留清单)、`2026-09-20-应付单与付款申请同步金蝶全链路业务流程(A102-2保存-A102-4查询-A103-1下推-A103-2回写).md`(应付侧对称链路)
> 最后更新:2026-09-21(按当前代码梳理;销方名称/销方税号/税收分类编码已改为随单据取数,不再使用 Nacos 配置)
> 最后更新:2026-10-08(联调纠偏:A303-1 awrp_src_con_no 改传首张结算单金蝶应收单防重键 awrp_src_code,传单据编号会报「源单编号不存在」;2026-10-01 上游来源单据放开传值 awrp_src_bt=ar_finarbill;2026-09-21 按当前代码梳理,销方名称/销方税号/税收分类编码随单据取数)
---
@@ -156,8 +156,8 @@ kingdeeSyncStatus:unsynced ──A302-2成功──> synced(终态)
| customnameid_number | 客户.编码 | 是 | 受票方客商档案客商编码(未维护报错) | 客商档案「客商编号」 |
| awrp_project_number | 项目.项目编码 | 是 | 联调期 999 | — |
| billsource | 数据来源 | 是 | 固定 3 | 无 |
| awrp_src_bt / awrp_src_con_no | 上游来源单据/单号 | 是 | ar_finarbill / 关联结算单金蝶应收单号(多张取第一张,拼接规则联调确认) | 环节一回写的「金蝶单据编号」 |
| systemsource | 系统来源 | 否 | 暂不传(联调校准项) | — |
| awrp_src_bt / awrp_src_con_no | 上游来源单据/单号 | 是 | ar_finarbill / 首张关联结算单的金蝶应收单**防重键 awrp_src_code**(联调确认金蝶按防重键匹配源单,2026-10-08 纠偏:传单据编号报「源单编号不存在」;多张取第一张) | `结算单号_结算明细单据号拼接`(与 A302-2 防重键同源) |
| systemsource | 系统来源 | 否 | thirdParty.bfi.srcSys(默认 WLTMS) | 无(后端配置) |
分录(sim_original_bill_item,逐「开票内容」商品行一条,按发票张次+张内行号平铺):取数口径即商品行落库值,**不回查开票项目主数据、不按开票明细→费用行展开分摊**(实现见 `InvoiceApplicationServiceImpl.resolveInvoiceApplyEntries`,仅查张次与商品行两张表):
@@ -223,7 +223,7 @@ kingdeeStatus:unsynced ──A303-1成功──> synced ──A303-5查到发
| 2 | Nacos 配置 | 仅需已有 `thirdParty.bfi.srcSys`(默认 WLTMS)与任务开关/`cron`;`invoiceApply.salerName/salerTaxNo/taxratecode` **已取消**,`invoiceApply.srcType` 有默认值无需配置 |
| 3 | 客商档案 | 我方开票单位(结算单收款方)与受票方客户均须入客商档案:我方须维护**统一社会信用代码**(销方税号),受票方须维护**客商编码**;受票方另需维护客商开票信息(税号、邮箱等) |
| 4 | 主数据 | 开票项目须维护税收分类编码(新增开票内容选择即快照);货物类型档案货物编码;部门编码 |
| 5 | 联调校准项 | A302-2:单据类型/款项性质 1001/CASH/src_type 文案、物料 SP0324+计量 TON+项目 999 临时值(`pushArBill` 调试块);A303-1:rowtype、systemsource、多结算单上游单号拼接;A303-5:原文字段名收敛 |
| 5 | 联调校准项 | A302-2:单据类型/款项性质 1001/CASH/src_type 文案、物料 SP0324+计量 TON+项目 999 临时值(`pushArBill` 调试块);A303-1:rowtype(systemsource 已传 srcSys、上游单据号已放开传首张结算单金蝶应收单编号);A303-5:原文字段名收敛 |
| 6 | 联调顺序 | 应收结算单同步(①)→ 开票申请同步(②)→ 手动触发 `sync-kingdee-ar-result` / `sync-kingdee-invoice-result` 验证回写 |
---
@@ -5,6 +5,8 @@
> 关联文档:`2026-09-20-收票管理同步金蝶全票池发票(A801-1查询+批量同步+定时绑定关联)业务流程.md`(同步链已整体退役,仅存推送/回写侧仍有效的键与端点见该文档退役声明)、`2026-09-22-收票单推送金蝶全链路业务流程(A303-2保存-A302-6指定应付-A801-3预检回写).md`(图05 推送链路,镜像字段 invoice_type_code/items_json 由其取数消费)
> 设计依据:`docs/superpowers/specs/2026-09-22-invoice-pool-remote-reversal-design.md`
> 最后更新:2026-09-23(选票可用过滤 + 有界循环补页游标分页 + 单据使用状态收敛为后端固定查未用;2026-09-22 本文原为「金蝶票池选票统一查镜像」,同日二次决策改回实时查询后整篇重写;文件名保留以免断链)
>
> **⚠️ 退役声明(2026-10-05)**:本文描述的**实时查询链路(游标分页、可用过滤拉取后执行、claimed/registered 纯展示装饰)已被第三次定稿回切为镜像读**,终态口径见 `2026-10-05-金蝶票池选票回切镜像读(本地真分页+票号回源+瘦身同步+认领闸门)业务流程.md`。**仍然有效的部分**:保存重查闸门 `persistMirrorFromRemote`(含 isUsablePoolRow 复核,原样保留)、镜像 `(tenant_id, invoice_no)` 唯一与存根写入语义、A801-2 附件下载镜像优先/回源链路。下文其余叙述作历史口径。
---
@@ -0,0 +1,219 @@
# 金蝶附件同步(A902)设计文档
- 日期:2026-09-24
- 范围:后端同步链路联带 A902 附件上传(付款申请、正式结算应付/应收、收票登记、开票申请)+ 前端状态列/提示/超时调整
- 背景:单据推送金蝶成功后,TMS 侧附件(attachmentsJson)不随单上行,金蝶单据缺影像。星瀚 A902「附件上传到指定单据」接口已具备(见《业财融合系统接口文档.xlsx》同名页签),此前未在 BFI 客户端实现
- 前置:原计划复用 2026-09-24-kingdee-push-timeout-anticlick 的 loading+守卫+try/finally 模式;经评审确认该前置未落地,本期**不实施防连点(后补)**,本设计仍不新增前端状态机
## 一、目标
1. 五类单据推送金蝶成功后,后端联带逐个上传附件至金蝶对应单据附件面板(A902)
2. 收票登记含发票文件:票池镜像附件 path → A801-2 下载 → A902 回传收票单
3. 附件同步独立状态、失败可补传(重推自动补);前端列表列 + message 可见
4. 客商档案预留 entityNumber 映射配置,二期接入(开票申请 entityNumber 已确认为 `sim_original_bill`,提入本期)
## 二、A902 / A801-2 契约
### A902 附件上传到指定单据
- `POST /ierp/kapi/v2/frame/attachment/uploadFile`,multipart/form-data
- Header:`access_token`(复用现有 getToken/token 缓存)
- Body:`file`(文件)+ `attachmentUploadFileArgs`(JSON):`{"entityNumber":"ap_payapply","billPkId":2386455983883835392,"controlKey":"attachmentpanel"}`
- 返回:`{"data":{"path":"211e6ce5c9805c00"},"errorCode":"0","status":true}`,`data.path` 即金蝶侧文件路径
### A801-2 文件下载(收票发票文件用)
- `POST /ierp/kapi/v2/awrp/bosext/attachment/download`,body `path`(镜像 attachmentsJson 的 path)→ 返回文件流
### entityNumber / controlKey 映射
| 业务单据 | entityNumber | controlKey | 期次 |
| --- | --- | --- | --- |
| 付款申请单 | `ap_payapply` | `attachmentpanel` | 本期 |
| 财务应付单(正式结算应付面板) | `ap_finapbill` | `attachmentpanel` | 本期 |
| 财务应收单(正式结算应收面板) | `ar_finarbill` | `attachmentpanel` | 本期 |
| 收票单 | `ap_invoice` | `attachmentpanel` | 本期 |
| 开票申请单 | `sim_original_bill` | `attachmentpanel` | 本期 |
| 客商(资质附件) | 待金蝶确认 | 待确认 | 二期 |
映射以常量类维护:`KingdeeAttachmentConstant.ENTITY_NUMBERS`(biz_type → entityNumber),controlKey 统一 `attachmentpanel`;二期客商补条目不改架构。
## 三、领域模型
### 实体
**业务附件(不动)**:各单据 `attachmentsJson` JSON 列,是「单据有哪些附件」的业务事实,随表单编辑、随审批快照。本设计不读写其结构(只读 url/name)。
**附件同步表 `blade_kingdee_attachment_sync`(新增)**:
| 字段 | 说明 |
| --- | --- |
| id / tenant_id / 审计字段 | 标准结构 |
| biz_type | `pay_application` / `formal_settlement_ap` / `formal_settlement_ar` / `invoice_receipt` / `invoice_application` |
| biz_id | 单据 id |
| attachment_key | 幂等键:用户附件=附件 url;发票文件=A801-2 的 path(path 变化视为新文件可重传) |
| attachment_name | 附件名(展示/排查用) |
| kingdee_path | A902 返回的 data.path |
| sync_status | 1 成功 / 0 失败 |
| fail_reason | 失败原因(超限/下载失败/金蝶报错) |
| sync_time | 上传时间 |
- 唯一键 `uk_(biz_type, biz_id, attachment_key)`;同步前按 `(biz_type, biz_id)` 载入成功集合,命中即跳过(幂等)
- 同步流水落点见「四、同步流水落点(评审定案)」
**单据表加列**:`kingdee_bill_id`(星瀚单据内码,取自保存响应 `BfiBillSaveResultItemVO.id`)。付款申请已有该列;本期新增三列(SQL 见 `doc/sql/transport/blade_kingdee_attachment_sync_20260924.sql`):
| 表 | 列 | 说明 |
| --- | --- | --- |
| blade_formal_settlement | kingdee_bill_id | A102-2/A302-2 保存响应返回;幂等跳过/存量缺失经 A102-4/A302-4 按防重键反查回填 |
| blade_invoice_receipt | kingdee_received_invoice_id | A303-2 保存响应返回;A801-3 幂等预检命中/定时回写时一并回填 |
| blade_invoice_application | kingdee_bill_id | A303-1 保存响应返回;A303-4 勾稽回填一并带回 |
### 附件同步状态机(单附件行级)
```
(无记录) ──上传成功──> 成功(终态,幂等跳过)
└────上传失败──> 失败(记 fail_reason;重推时重新尝试,成功则覆盖为成功)
```
列表聚合展示态(运行时聚合,非存储字段,评审定案):`无附件` / `未上传` / `部分未传` / `已上传`。
判定:单据 attachmentsJson 无可上传附件→无附件;同步表成功数=0→未上传;0<成功数<附件数→部分未传;成功数≥附件数→已上传。
### 不变式
1. 附件上传必须晚于主单推送成功、拿到并落库 billPkId 之后
2. 附件失败永不回滚、不污染主单同步状态(逐附件独立 try/catch)
3. 同 `(biz_type, biz_id, attachment_key)` 最多一条成功记录
4. 不自动重试;重推主单时自动补传(主单被幂等预检跳过不影响附件补传分支)
5. 单文件软限制 20MB(常量,待金蝶确认后调整);超限/下载失败记原因跳过,不阻塞同单其余附件,更不阻塞同批其他单据
## 四、后端设计
### BFI 客户端(blade-bfi-api)
`IBfiClient` 新增一个方法,复用现有 token 配置(`BfiFeignClientConfig`):
- `uploadAttachment`(A902):`produces = MULTIPART_FORM_DATA_VALUE`,`@RequestPart("file")`(内容为内存字节数组,经 `InMemoryMultipartFile` 适配,单文件软限制 20MB)+ `@RequestPart("attachmentUploadFileArgs")`(JSON 字符串),返回 `BfiResultVO<BfiAttachmentUploadVO>`,`data.path` 即金蝶侧文件路径
- A801-2 下载**复用已有** `IBfiClient.invoiceDownload` / `IBfiService.downloadInvoice`(`feign.Response` 流式读取),本期仅在附件链路启用,无新增接口
### 附件同步支持类(blade-transport)
新增 `KingdeeAttachmentSyncSupport`(`org.springblade.transport.support`,与 `KingdeeApBillSyncSupport` 等同包同风格):
```
syncAttachments(bizType, bizId, billPkId, List<KingdeeAttachmentItem>) // 主流程
syncUserAttachments(bizType, bizId, billPkId, attachmentsJson) // 用户附件快捷入口
parseUserAttachments(json) / parseInvoicePoolFile(json) // 解析:数组项容忍 url/link/fileUrl/downloadUrl/domain/src;镜像对象取 path
countAttachments(json) / successCountByBizId(bizType, bizIds) // 列表聚合(一次 GROUP BY,防 N+1)
aggregateStatus(attachmentCount, successCount) // 四态判定
```
流程:载入已成功集合 → 过滤已传 → 过滤超限(记 fail_reason)→ 逐个(http(s) 下载 / A801-2 下载,流式封顶读取)→ A902 上传 → 落记录。单附件 try/catch,异常记失败继续下一个;结果汇总(`AttachmentSyncSummary`:total/success/failed/skipCount/failReasons)供接入点写操作流水与前端提示。同步表既有失败记录重试成功时覆盖为成功(复用主键 update)。
### 各模块接入点(主单成功、billPkId 落库之后,均在主单事务提交后执行)
| 模块 | 接入点 | entityNumber | 附件来源 |
| --- | --- | --- | --- |
| 付款申请 | `PaymentApplicationServiceImpl.syncKingdee(id, context)` 包装层(`doSyncKingdee` 事务提交后) | ap_payapply | 单据 attachmentsJson |
| 正式结算应付/应收 | `FormalSettlementServiceImpl.syncKingdee`(无事务包装)→ `doSyncKingdee`(@Transactional)提交后 | ap_finapbill / ar_finarbill | 结算单 attachmentsJson |
| 收票登记 | `pushOneReceivedInvoice`:A303-2 落 pushed 后、A302-6 指定应付前 | ap_invoice | 登记附件 + 镜像发票文件(A801-2 下载,key=path) |
| 开票申请 | `InvoiceApplicationServiceImpl.syncKingdee`(无事务包装)→ `doSyncKingdee` 提交后 | sim_original_bill | 单据 attachmentsJson |
事务边界(ADR-7):附件上传是慢远程调用,统一放在主单事务**之外**——正式结算/开票申请原 `syncKingdee` 事务体下移为 `doSyncKingdee`(经自代理调用),付款申请沿用既有包装层模式;附件失败仅在附件链路内消化,绝不回滚主单、不拉长主单持锁时间。
星瀚内码(billPkId)回填路径:
- 推送路径:保存/下推响应 `BfiBillSaveResultItemVO.id` 随单落库(`BfiApBillPushOutcome`/`ReceivedInvoicePushOutcome` 扩展携带 billId)
- 幂等/存量路径:本地 kingdee_bill_id 缺失时反查——正式结算 A102-4/A302-4(`queryApBillExistId`/`queryArBillExistId`)、收票 A801-3 预检行/定时回写行自带 id、开票申请 A303-4 勾稽行自带 id;反查命中即回填并记 info 日志,仍未命中则跳过附件待重推补传
重推补传:现有同步入口无改动——主单被幂等预检判已存在时,仍进入附件补传分支(billPkId 取本地 kingdee_bill_id,缺失则按上述路径反查回填)。
### 同步流水落点(评审定案)
附件明细以 `blade_kingdee_attachment_sync` 自身为准(sync_time/fail_reason 已是完整流水);仅在**有 actionType 流水表的单据**追加 `attachment_upload` 操作记录:收票登记(`InvoiceReceiptRecord`)、开票申请(`InvoiceApplicationRecord`);付款申请/正式结算无流水表,不追加。
### 列表状态聚合
各列表 VO(PaymentApplicationVO/InvoiceApplicationVO/FormalSettlementVO/InvoiceReceiptVO)新增三个展示字段:`attachmentCount`、`attachmentSyncStatus`(none/pending/partial/done)、`attachmentSyncStatusName`(无附件/未上传/部分未传/已上传)。各 `selectPage` 转换后按本页 biz_ids 一次 `GROUP BY biz_id`(`selectMaps`)查同步表成功计数,与解析 attachmentsJson 的附件数对比得出聚合态,避免 N+1;正式结算按应付/应收拆两组业务类型各查一次。
## 五、前端设计
| 项 | 内容 |
| --- | --- |
| 超时 | 实施勘误:paymentApplication/invoiceApplication/formalSettlement 的 syncKingdee 实际已是 120s(批量 300s),无需调整;唯一风险点是 `invoiceReceipt.js pushKingdee` 未设 timeout(走全局 10s 默认),已显式放宽为 120000 |
| 列表列 | payment-application.vue、invoice-application.vue、formal-settlement.vue(option `formalSettlementTable.js`)、invoice-receipt.vue 增「附件同步」列(`attachmentSyncStatusName`):无附件/未上传/部分未传/已上传 |
| message | 同步成功后 `await loadTable()` 刷新列表,按本次同步 ids 过滤刷新行中 `attachmentSyncStatus ∈ {pending, partial}` 的数量,追加 warning 提示「其中 N 单附件未全部上传,可重推补传」;收票页追加进推送结果弹窗文案 |
| 交互 | 无新增按钮(重推即补传);loading/守卫/try-finally 防连点模式本期未实施(见「不做的事」) |
## 六、不做的事
- 前端同步按钮防连点模式(loading+守卫+try/finally):本期评审确认不实施,后补
- 客商档案附件推送(二期,等金蝶确认 entityNumber/controlKey;本期仅预留映射配置 `KingdeeAttachmentConstant.ENTITY_NUMBERS`)
- 附件自动重试、定时补传任务
- attachmentsJson 结构改造(去 JSON 化属独立重构,见 ADR-3)
- OFD/PDF 发票文件在金蝶侧的可预览性验证
## 七、待确认(金蝶侧)
1. A902 单文件大小上限、允许格式(当前后端软限制 20MB 常量)
2. 客商的 entityNumber/controlKey
3. A902 调用频率限制(批量串行是否需间隔)
4. OFD 数电票文件上传收票单附件面板是否可用(主文档 641 行注明「金蝶不支持直接推送数电票 OFD 附件」,需验证 A902 通道例外与否)
## 八、验证
1. 后端:`mvn clean compile -DskipTests -pl`(改动模块)——✅ 已通过(bfi-api / transport-api / blade-transport)
2. 前端:`pnpm run typecheck`——⚠️ 仓库既有问题无法运行(vue-tsc 3.3.11 与 typescript@7.0.2 不兼容,TS7 移除 `./lib/tsc` 导出,与本特性无关);已用 @vue/compiler-sfc 解析 + `node --check` 校验全部改动 .vue/.js 语法通过
3. 手动(用户执行):
- 含附件单据(付款申请/开票申请/正式结算/收票登记)单条/批量同步 → 金蝶对应单据附件面板可见文件
- 同步成功但附件失败(断网/超限模拟)→ 主单状态正常、列表「附件同步」列示未传/部分未传、重推自动补传
- 重推已全部上传的单据 → A902 零调用(幂等跳过,日志「历史已成功 N 个」)
- 收票登记:镜像发票文件经 A801-2 下载后出现在收票单附件面板
- 关键日志链路:`调用BFI接口 附件上传开始/结束` → `金蝶附件上传成功` → `金蝶附件同步完成` → 各接入点「同步完成」日志
## 九、ADR 摘要(本次评审定案)
| # | 决策 | 备选与理由 |
| --- | --- | --- |
| ADR-1 | 后端联带自动传附件 | 前端两段式/独立按钮被否:状态机复杂、易漏传 |
| ADR-2 | 附件独立状态+失败可补传,不自动重试 | 主单状态机不被污染;重推即补传零新增交互 |
| ADR-3 | 幂等落独立同步表;**保留 attachmentsJson** | 附件业务事实(编辑/审批快照)与集成状态(幂等/path/原因)职责分离,url 幂等键是必要冗余而非数据冗余;推送期间单据锁定,无并发写 JSON 风险;去 JSON 化若要做属独立重构 |
| ADR-4 | 本期 5 单据(开票申请 entityNumber 已确认 `sim_original_bill` 提入本期),客商预留配置 | entityNumber 是硬约束,猜错即传不上 |
| ADR-5 | 同步接口超时 60s + 后端软限制跳过 | 大文件不拖死整批;不引入异步任务复杂度 |
| ADR-6 | 收票含发票文件:A801-2 下载→A902 回传,key=path | path 变化(红冲重开)自动触发重传 |
| ADR-7 | 附件上传在主单事务提交后执行(正式结算/开票申请拆 `doSyncKingdee` 事务体) | 附件是慢远程调用:放事务内会拉长持锁时间;放事务外失败只消化在附件链路,主单状态不被污染 |
## 十、实施记录(2026-09-24 as-built)
评审定案与设计的差异,均已按本节落地:
| # | 事项 | 设计原假设 | 实施定案 |
| --- | --- | --- | --- |
| 1 | 前端防连点模式 | 前置已落地、直接复用 | 未落地,本期不实施(后补) |
| 2 | 前端超时 | 30s→60s 全量放宽 | 其余已 120s/300s 不动;仅 `invoiceReceipt.pushKingdee` 显式 120s(原全局 10s 默认) |
| 3 | 同步流水落点 | 「现有同步流水」统一新增 action | 无统一流水表:新表即明细事实源;收票/开票申请有 actionType 流水表追加 `attachment_upload`,付款申请/正式结算不追加 |
| 4 | 聚合态术语 | 正文与术语表两版冲突 | 定案:无附件/未上传/部分未传/已上传(CONTEXT.md 已落档) |
| 5 | A801-2 客户端 | 需新增 downloadFile | 已有 `invoiceDownload`,直接复用 |
| 6 | 应付/应收/收票推送返回 | 未明确内码获取方式 | `BfiApBillPushOutcome`/`ReceivedInvoicePushOutcome` 扩展 billId;幂等/存量经查询接口反查回填(`queryApBillExistId`/`queryArBillExistId`、A801-3/A303-4 查询行自带 id) |
关键产物:
- 后端:`IBfiClient.uploadAttachment`、`BfiAttachmentUploadVO`、`InMemoryMultipartFile`、`KingdeeAttachmentConstant`(biz_type/entityNumber/controlKey/20MB/聚合态)、`KingdeeAttachmentSync` 实体 + Mapper/Service、`KingdeeAttachmentSyncSupport`
- 接线:PaymentApplicationServiceImpl、FormalSettlementServiceImpl、InvoiceReceiptServiceImpl、InvoiceApplicationServiceImpl、KingdeeApBillSyncSupport、KingdeeArBillSyncSupport、KingdeeReceivedInvoiceSyncSupport
- 前端:4 option 列 + 4 页面 warning 提示 + invoiceReceipt.js 超时
- SQL:`doc/sql/transport/blade_kingdee_attachment_sync_20260924.sql`(建表 + 3 加列,可重复执行)
- 提交:tms-api `dee649d`(docs) → `79aaa9b`(feat) → `9f95c75`(log);tms-erp-web `aabe67e`
## 术语表
| 术语 | 含义 |
| --- | --- |
| A902 | 星瀚「附件上传到指定单据」接口(注意与客商接口 A902-1/A902-2 是两套编号) |
| A801-2 | 星瀚「文件下载」接口(票池发票影像) |
| billPkId | 星瀚单据内码,保存响应 `BfiBillSaveResultItemVO.id`,A902 定位目标单据 |
| entityNumber | 星瀚单据标识(如 ap_payapply) |
| controlKey | 星瀚附件面板/附件字段标识,统一 attachmentpanel |
| 附件幂等键 | 判定「同一附件已传过」的键:用户附件=附件 url;发票文件=A801-2 的 path(path 变化视为新文件) |
| 镜像发票文件 | 票池镜像 attachmentsJson 保存的发票文件 {source,path,name},上传收票单时经 A801-2 取回回传 |
| 附件同步聚合态 | 无附件/未上传/部分未传/已上传(列表展示用,运行时聚合) |
| 重推补传 | 主单重复推送(含幂等跳过)时自动补传未成功附件,无新增交互 |
@@ -59,7 +59,7 @@
### 3.1 编排
1. **确定参与认领用户**:`UserCache.getUserRealName(AuthUtil.getUserId())`,为空拦截「当前用户无姓名,无法同步认领公告」。
2. **全量翻页**:`BfiUtil.queryAllPages(pageNo -> bfiService.queryClaimAnnounce(claimusers, pageNo, 200))`,受 `BfiUtil.DEFAULT_MAX_PAGES`(50 页)保护。
2. **全量翻页**:`BfiUtil.queryAllPages(pageNo -> bfiService.queryClaimAnnounce(queryDTO))`,受 `BfiUtil.DEFAULT_MAX_PAGES`(50 页)保护。时间窗口从 Nacos 配置 `thirdParty.bfi.claimAnnounce.months` 读取(默认 6 个月)。
3. **幂等落库**:按 `detail_serial_no`(取 `detailid`,缺省回退 `tradedetailno`/`bankdetailno`)`FOR UPDATE` 查重后 insert/update 收款流水表;重复明细流水号抛「明细流水号重复」拦截。
4. **操作留痕**:`blade_receipt_flow_record` 记 `sync` 操作(新增/更新逐行记录,operator=当前用户)。
@@ -0,0 +1,171 @@
# 金蝶附件下载(A801-2 前端下载通道)设计文档
- 日期:2026-09-27
- 范围:票池镜像发票文件面向前端的查看/下载通道(后端流式代理端点 + 前端三处入口改造);附件同步(A902)链路零改动
- 背景:2026-09-24 金蝶附件同步设计核验时确认缺口——票池镜像发票文件的 attachmentsJson 为 `{source:"piaozone", path, name}`,其中 `path` 是金蝶侧文件地址(A801-1 的 downloadUrl,非 http 直链),前端 `invoiceAttachmentUrl`/`attachmentFileUrl` 只识别 `url/link/fileUrl`,因此正式结算认领发票行、付款申请发票附件行的「查看/下载」按钮对这类文件弹「暂无可查看/下载附件」;A801-2 后端能力已具备(`IBfiClient.invoiceDownload`),但仅用于附件同步链路(A801-2 下载 → A902 回传),无面向前端的下载端点。
## 一、目标
1. 前端可在票池弹窗、正式结算认领发票行、付款申请发票附件行三处查看/下载票池发票文件,文件经后端代理 A801-2 流式返回
2. http 型附件(OSS 直链)的现有查看/下载行为完全不变,两类附件按字段特征自动分流
3. 不提供「任意 path」下载能力,防止开放代理与越权读取金蝶文件
## 二、现状与缺口(核验结论)
| 入口 | 现状 | 缺口 |
| --- | --- | --- |
| 票池弹窗 `kingdee-invoice-pool-dialog` | 仅选择发票,无附件操作列 | 无查看/下载入口 |
| 正式结算 `formal-settlement-editor.vue` 认领发票行 | 有 查看/下载 按钮,`invoiceAttachmentUrl` 只取 `url/link/fileUrl`(:2288-2305) | path 型附件取不到 url,按钮弹「暂无可查看/下载附件」 |
| 付款申请 `payment-application-form.vue` 发票附件行 | 同上(:1385-1411) | 同上 |
| 收票登记 `invoice-receipt-form.vue` | 登记附件(OSS 直链)正常 | 镜像发票文件入口由票池弹窗覆盖,本表单不改 |
行数据具备条件:票池弹窗选中行与认领行均 `...row` 展开(formal-settlement-editor.vue:2195-2216),自带 `invoiceNo` 与 `attachmentJson`,满足「按发票号定位」的入参要求(勘误说明见「实施记录」1)。
## 三、契约
### A801-2 文件下载(已有,零改动)
- `POST /ierp/kapi/v2/awrp/bosext/attachment/download`,body `{"path": "<票池 downloadUrl>"}`,Header `access_token`(复用现有 getToken/token 缓存)
- 返回文件流;客户端 `IBfiClient.invoiceDownload` / `BfiServiceImpl.downloadInvoice` 已实现(status 校验、日志齐全),本期直接复用
### 契约核验(2026-09-27,源:《业财融合系统接口文档.xlsx》页签「文件下载」「查询全票池发票数据」「附件上传到指定单据接口」)
1. **A801-2 失败响应是 JSON 而非流**:`{"data":null,"errorCode":"FILE_IS_NOT_EXIST","message":"附件不存在。","status":false}`;成功响应文档未给出字段定义(即原始文件流)。后端代理必须先判 Content-Type 再写流——失败时解析 `message`/`errorCode` 透传给前端,不能把 JSON 错误体当文件下发。
2. **path 形态三方一致**:A801-2 请求示例 `234e244b2c803400`、A801-1 票池响应 `downloadUrl:"234e244b2c803400"`(另有 `imageUrl`/`pdfurl`/`snapshotUrl` 同形态;`originalFileName` 为 `serialNo.pdf`)、A902 返回 `data.path:"211e6ce5c9805c00"`——均为 16 位 hex 文件 id。镜像 path 可直接作为 A801-2 入参(与现状一致),且 A902 返回的 kingdee_path 与 A801-2 的 path 大概率同命名空间(见「待确认」1)。
3. **entityNumber 映射获接口文档印证**:「附件上传到指定单据接口」页签内嵌映射表——付款申请单 `ap_payapply`、财务应付单 `ap_finapbill`、财务应收单 `ar_finarbill`、收票单 `ap_invoice`、开票申请单 `sim_original_bill`(controlKey 统一 `attachmentpanel`),与上期 `KingdeeAttachmentConstant` 完全一致,无需调整。
4. A801-1 上游为票易通(queryParam 见 open.piaozone.com),`downloadUrl` 的有效期文档未说明;A902 页签未载明单文件大小上限与频率限制——「待确认」项维持。
### 新增 TMS 下载端点
`GET /blade-transport/invoice-receipt/invoice-pool/attachment/download?invoiceNo=<发票号>`
- 鉴权:BladeX 登录态(网关 + Secure 框架),与同控制器 `/invoice-pool` 查询一致,不额外加 `@PreAuth`
- 租户隔离:`KingdeeInvoicePool extends TenantEntity`,`getById` 经 MP 租户插件自动过滤,跨租户 id 等同不存在
- 成功响应:文件流。Header:
- `Content-Type`:透传 BFI 响应头,缺失时兜底 `application/pdf`(票池发票文件均为 PDF)
- `Content-Length`:BFI 响应头携带时透传
- `Content-Disposition`:`attachment; filename="<ASCII兜底>"; filename*=UTF-8''<RFC5987编码>`,文件名取镜像 attachmentsJson 的 `name`(发票号.pdf)
- 失败响应(均在写响应头**之前**失败,走全局异常处理返回 `R` JSON):
| 场景 | 行为 |
| --- | --- |
| 镜像未命中且 A801-1 实时回源未命中(含跨租户票号,租户隔离下等同不存在) | `ServiceException("该发票没有金蝶侧文件")` |
| 镜像/实时行均无 downloadUrl(如 OFD-only 票) | 同上,`ServiceException("该发票没有金蝶侧文件")` |
| BFI 返回非 200,或响应体为 JSON 错误(如 `FILE_IS_NOT_EXIST`) | 解析 BFI `message`/`errorCode` 抛 `ServiceException`(如「调用BFI文件下载失败:附件不存在。」),错误信息透传前端 |
| 传输中断 | 已写头后无法回退,流中断即连接断开,前端 reject |
## 四、后端设计
接线(均在既有文件内新增,无新文件):
- `InvoiceReceiptController`:新增 `downloadPoolAttachment(@RequestParam String invoiceNo, HttpServletResponse response)`,与 `/invoice-pool` 同域同风格(`@GetMapping` + `@ApiOperationSupport` + `@Operation`)
- `IInvoiceReceiptService` / `InvoiceReceiptServiceImpl`:新增 `downloadPoolAttachment(String invoiceNo, HttpServletResponse response)`
流程:
```
取镜像(自动租户过滤)→ 解析 attachmentsJson 取 path/name
→ bfiService.downloadInvoice(path) ← 在写任何响应字节之前
→ 校验 status==200 且 Content-Type 非 JSON(防御 BFI 以 200 返回错误体)
→ 写响应头(Content-Type / Content-Length / Content-Disposition)
→ 8KB 缓冲流式拷贝 feign Response → ServletOutputStream,flush,finally 关闭 Response
```
设计要点:
1. **path 一律服务端解析,接口不收 path 参数(ADR-1)**:按发票号定位,解析顺序「镜像快照优先 → A801-1 实时回源兜底」——镜像快照与上传链路(A902 镜像文件取回)同源同口径(同取 `blade_kingdee_invoice_pool.attachmentsJson`);但票池弹窗行是 A801-1 实时行、未必已入镜像,故下载必须有实时回源兜底(上传只发生在已绑定镜像的收票单上,无此场景)。镜像快照过期(红冲重开)时报「附件不存在。」不自动刷新,与上传取快照口径一致,刷新机制见「待确认」2
2. **先校验后写流**:所有可预知错误发生在写头之前,保证前端拿到的是可识别的 JSON 错误而非损坏文件(ADR-3)。A801-2 失败时返回 JSON(`{"status":false,"errorCode":"FILE_IS_NOT_EXIST","message":"附件不存在。"}`,HTTP 状态文档未标注,按 200 + `status:false` 防御),解析 `message`/`errorCode` 透传给前端;成功时按 Content-Type 判定为文件流再写响应
3. **全程流式**:不落盘、不整文件进堆,与同步链路的 20MB `readCapped` 无关——下载通道**不设 20MB 上限**(ADR-4)
4. **复用 BFI 配置**:token 缓存与 Feign 超时(连 10s / 读 60s,`BfiFeignClientConfig`)不动;无事务、不落附件同步表、不记操作流水
5. **日志**:BFI 侧沿用「调用BFI接口 进项发票下载开始/结束」;业务侧补一条 info(操作人、发票号、字节数),便于审计
6. 文件名编码:RFC 5987 `filename*` + ASCII 兜底,中文发票号不乱码
## 五、前端设计
### API 与公共工具
- `src/api/payment/invoiceReceipt.js` 新增:
```js
// 金蝶票池发票文件下载(后端代理 A801-2),耗时较普通接口长,放宽到 120s
export const downloadInvoicePoolAttachment = invoiceNo =>
request({ url: `${baseUrl}/invoice-pool/attachment/download`, method: 'get', params: { invoiceNo }, responseType: 'blob', timeout: 120000 });
```
- 新增公共工具 `src/utils/kingdee-attachment.js`(三入口共用,避免复制三份):
- `isKingdeePathAttachment(attachment)`:`source === 'piaozone'`,或「有 `path` 且非 http(s) 开头」——判定为金蝶侧文件
- `downloadKingdeeAttachment(row)`:金蝶型 → 调下载 API;若响应 blob 的 `type` 含 `application/json` 则读取文本解析出 msg 后 `message.error`(兼容全局异常处理器以 200 返回 R 的情况);否则 `downloadFile(blob, name)`;URL 型 → 回退既有 `downloadFileByUrl`
- `viewKingdeeAttachment(row)`:金蝶型 → 拉取 blob 后 `URL.createObjectURL` 新窗口打开(PDF 可预览;OFD 等浏览器不支持的格式提示改用下载);URL 型 → 维持 `window.open(url)`
- 两个动作均带行级 loading 守卫(单行按钮级即可,沿用「防连点全局模式本期不实施」的口径)
### 入口改造(三处)
| 文件 | 改造点 |
| --- | --- |
| `src/components/kingdee-invoice-pool-dialog/main.vue` | 表格新增「附件」操作列:查看 / 下载(行自带 invoiceNo + attachmentJson) |
| `src/views/settlement/components/formal-settlement-editor.vue` | `viewInvoiceAttachment` / `downloadInvoiceAttachment`(:2296-2305)增加金蝶型分支,走公共工具;URL 型行为不变 |
| `src/views/payment/payment-application-form.vue` | `viewInvoiceAttachment` / `downloadInvoiceAttachment`(:1393-1411)同上 |
收票登记表单不改(镜像文件入口由票池弹窗覆盖)。
## 六、不做的事
- **已上传金蝶附件(`blade_kingdee_attachment_sync.kingdee_path`)的回源下载**:A801-2 对 A902 返回 path(如 `211e6ce5c9805c00`)的兼容性未验证,待金蝶确认后二期评估
- 批量发票文件打包 zip 下载
- 后端转存 OSS 再发直链的方案(ADR-2 否决,理由见下)
- 附件同步(A902)链路与 `KingdeeAttachmentSyncSupport` 的任何改动
## 七、待确认(金蝶侧)
1. A801-2 是否可下载 A902 返回的 `data.path`(决定二期「从金蝶恢复已上传附件」可行性)。契约核验补充依据:A902 返回 path(`211e6ce5c9805c00`)与 A801-2 path 参数示例(`234e244b2c803400`)同为 16 位 hex 文件 id,疑似同一文件命名空间,大概率兼容,需实测确认
2. 票池 `downloadUrl` 的有效期与失效表现:镜像落库后 path 是否会过期、失效时 A801-2 返回什么(已知失效类错误如 `FILE_IS_NOT_EXIST`「附件不存在。」);接口文档未载明有效期,上游为票易通(queryParam 见 open.piaozone.com),若会过期,需明确经 A801-1 刷新镜像的机制(影响下载失败时的用户提示与补救路径)
3. A801-2 频率限制(本场景为用户点击型、低频,预期无压力,确认即可);A902 页签亦未载明单文件大小上限与频率限制,上期 20MB 软限制待一并确认
## 八、验证
1. 后端:`mvn clean compile -DskipTests -pl blade-service/blade-transport -am`
2. 前端:`pnpm run typecheck` 因仓库既有问题不可用(vue-tsc 3.3.11 与 typescript@7.0.2 不兼容,与上期同因),沿用 @vue/compiler-sfc 解析 + `node --check` 校验全部改动 .vue/.js
3. 手动(用户执行):
- 票池弹窗行 查看/下载 → 得到 `发票号.pdf`,内容可打开
- 正式结算认领发票行、付款申请发票附件行:金蝶型附件 查看/下载 成功;OSS 型附件行为与改造前完全一致
- 镜像缺失 path / 发票未同步票池 → 友好提示,不产生损坏文件
- 不存在的发票号 → 「该发票没有金蝶侧文件」(镜像与 A801-1 回源均未命中;跨租户票号在租户隔离下等同不存在)
- BFI 停机/超时模拟 → 前端 JSON 错误识别并提示,浏览器不落损坏文件
- path 失效模拟(BFI 返回 `FILE_IS_NOT_EXIST`)→ 前端提示透传的「附件不存在。」,不落损坏文件
- 日志链路:`调用BFI接口 进项发票下载` → 业务侧下载 info
## 九、ADR 摘要
| # | 决策 | 备选与理由 |
| --- | --- | --- |
| ADR-1 | 按发票号定位,path 服务端解析(镜像优先 → A801-1 实时兜底) | 前端传 path 被否:等同开放代理,可遍历下载金蝶任意文件;只信镜像不够——票池弹窗是 A801-1 实时行、未必已入镜像;发票号在镜像表同租户唯一(A801-1 upsert 匹配键),天然带租户与数据边界 |
| ADR-2 | 后端流式代理,不转存 OSS | 转存需处理双存储一致性、清理策略与额外延时;票池文件为点击型低频小文件(PDF),代理足够;若日后高频/大文件再评估 |
| ADR-3 | 先校验后写流,错误返回 JSON | 先写头后失败会导致前端拿到损坏文件且无法提示;前端按响应 Content-Type 是否为 JSON 统一识别成败,不依赖 HTTP 状态码细节 |
| ADR-4 | 下载通道不设 20MB 上限 | 20MB 是 A902 上传软限制(金蝶契约),A801-2 下载无此约束,流式透传不占堆内存 |
| ADR-5 | 查看走 blob objectURL 新窗口,作为增强项 | 后端不区分 inline/attachment,减少契约面;OFD 等不可预览格式由前端降级为下载提示 |
## 十、实施记录(2026-09-27 as-built)
实施中发现的现状与设计的两处偏差,已按实际落地(勘误同上期 as-built 风格):
| # | 事项 | 设计原假设 | 实施定案 |
| --- | --- | --- | --- |
| 1 | 下载定位键 | 按票池镜像 id(`GET /invoice-pool/{poolId}/attachment/download`) | **按发票号**(`GET /invoice-pool/attachment/download?invoiceNo=`)。原因:票池弹窗行是 A801-1 实时行、`id` 恒空(`toPoolViewVO` 注释即注明);正式结算/付款详情行是关系行(`FormalSettlementInvoice`/`PaymentApplicationInvoice` 的 id 为关系行 id 而非镜像 id)。三处入口统一仅可靠携带 `invoiceNo`;票号在镜像表同租户唯一(A801-1 upsert 匹配键),按票号取**实时镜像**还自动跟随 path 变化(红冲重开)。定位顺序:镜像优先(0 远程调用)→ 镜像未落库/无文件时实时回源 A801-1 按票号取 `downloadUrl`(远程模糊匹配命中行做全等防御) |
| 2 | 失败返回方式 | 走全局异常处理返回 R JSON | 写流前的业务失败(票号空/无金蝶侧文件/BFI JSON 错误体)在服务内捕获后经 `WebUtil.renderJson(response, R.fail(msg))` 直接写 HTTP 200 + JSON。原因:blade-transport 无本地异常处理器,全局处理器行为在 jar 内不可控(可能非 200,axios 拦截器对 blob 错误体无法解析出 msg);`WebUtil.renderJson` 为本仓库既有惯例(运单批量导入校验同款)。前端 `kingdee-attachment.js` 统一按响应 Content-Type 含 `application/json` 识别失败并弹出 msg |
实施产物:
- 后端:`InvoiceReceiptController.downloadPoolAttachment`(`/invoice-receipt/invoice-pool/attachment/download`,GET,继承类级 `@PreAuth(menu="invoice_receipt")`)、`IInvoiceReceiptService.downloadPoolAttachment`、`InvoiceReceiptServiceImpl` 实现(`resolvePoolAttachment` 定位 + `doDownloadPoolAttachment` A801-2 流式透传 + `readBfiJsonMessage` 错误体解析,`bfiResponse.close()` finally 兜底;IOException(客户端断开)仅记日志)
- 前端:`api/payment/invoiceReceipt.js` 新增 `downloadInvoicePoolAttachment`(blob + 120s);新增 `utils/kingdee-attachment.js`(`isKingdeePathAttachment` 判定 + 下载/预览 + JSON 错误识别 + 静默标记防双 toast);三处入口——票池弹窗新增「附件」列(行级 `attachmentBusy` 守卫)、正式结算编辑器与付款申请表单的 查看/下载 按钮按附件类型分流(`kingdeeAttachmentBusy` 守卫,OSS 直链行为不变)
- 验证:后端 `mvn clean compile -DskipTests -pl blade-service/blade-transport -am` ✅;前端 @vue/compiler-sfc 解析 + `node --check` 全部改动文件 ✅(`pnpm run typecheck` 因仓库既有 vue-tsc/TS7 不兼容问题不可用,与上期同因)
## 术语表
| 术语 | 含义 |
| --- | --- |
| A801-2 | 星瀚「文件下载」接口,按 path 取回文件流(本期对前端暴露的下载通道) |
| 票池镜像 | `blade_kingdee_invoice_pool`,A801-1 全票池发票的本地镜像,attachmentsJson 存 `{source:"piaozone", path, name}` |
| path 型附件 | 金蝶侧文件:attachmentsJson 项有 `path` 无 http url,只能经 A801-2 取回 |
| URL 型附件 | OSS 直链附件:attachmentsJson 项有 `url/link/fileUrl`,前端可直连下载(现有行为) |
| billPkId | 星瀚单据内码(上期 A902 使用,本特性不涉及,仅口径区分) |
| 开放代理 | 接口接受任意目标地址参数并代为取回内容的安全缺陷,本设计以 id 定位规避 |
@@ -0,0 +1,213 @@
# TMS ↔ 金蝶 上传/下载链路梳理
- 日期:2026-09-27
- 依据:代码真实实现(blade-third-party-api/blade-bfi-api、blade-service/blade-transport)+ 《2026-09-24-kingdee-attachment-sync-design.md》《2026-09-27-kingdee-attachment-download-design.md》
- 回答三个问题:**系统需要从金蝶下载哪些东西**、**上传和下载的逻辑是什么**、**上传与下载能否对应上(闭环)**
---
## 一、结论摘要
1. **文件级下载目前只有一种:票池发票文件(发票影像 PDF)**。判断正确——全代码库中唯一返回文件流的金蝶接口是 A801-2「文件下载」(`IBfiClient.invoiceDownload`),它的消费方只有两处,都围绕票池发票文件:① 附件同步链路内「下载回传」收票单;② 面向前端的票池发票文件下载端点(2026-09-27 新增,当前未提交)。销项开票发票(A303-5 回写)不走文件流接口:A303-5 响应自带发票云文件直链 URL,落库后前端直连查看/下载(2026-09-30 补充,见 3.4)。
2. 除文件外,系统还从金蝶「下载」**数据**(JSON 查询结果):票池发票元数据(A801-1)、收票单结果(A801-3)、付款申请/付款处理结果(A103-2/A103-4)、应付/应收单结果(A102-4/A302-4)、开票申请单与开票结果(A303-4/A303-5)、客商(A902-1)、汇率(A801-4)。这些用于幂等预检、星瀚内码反查、状态/结果定时回写。
3. **上传与下载基本能对应上,但有三处缺口**:① 已上传金蝶的附件(`blade_kingdee_attachment_sync.kingdee_path`)没有回源下载通道(A801-2 是否兼容 A902 返回的 path 未验证,二期待金蝶确认);② 收款认领链路(A301-1/2/3)客户端已实现、业务零接线;③ 客商资质附件上传(A902)二期预留,entityNumber 未确认——客商附件目前既无上传也无下载。
4. 每一个「推送/保存」类接口都有对应的「查询」类接口做幂等预检或结果回写,数据面上传下载成对;文件面只有「下载发票→上传回传」一条闭环。
---
## 二、接口总清单与业务接线状态
`IBfiClient`(blade-bfi-api)共 25 个接口,按业务接线情况分三类:
### 2.1 业务已接线(上传/推送类)
| 编号 | 接口 | IBfiService 方法 | 业务调用点 |
| --- | --- | --- | --- |
| A103-1 | 应付下推付款申请 | `pushPayApply` | PaymentApplicationServiceImpl:1045 |
| A103-3 | 预付款申请保存 | `savePrePayApply` | PaymentApplicationServiceImpl:949 |
| A102-2 | 财务应付单保存 | `saveApBill` | KingdeeApBillSyncSupport:318 |
| A302-2 | 财务应收单保存 | `saveArBill` | KingdeeArBillSyncSupport:258 |
| A303-2 | 收票单新增(批量) | `saveReceivedInvoice` | KingdeeReceivedInvoiceSyncSupport:218 |
| A302-6 | 收票指定应付 | `invoiceAssign` | KingdeeReceivedInvoiceSyncSupport:288 |
| A303-1 | 开票申请单新增 | `saveInvoiceApply` | InvoiceApplicationServiceImpl:702 |
| A902-2 | 客户保存 | `saveCustomer` | CustomerArchiveServiceImpl:527 |
| **A902** | **附件上传到指定单据** | `uploadAttachment` | KingdeeAttachmentSyncSupport:164(唯一入口) |
### 2.2 业务已接线(下载/查询类)
| 编号 | 接口 | IBfiService 方法 | 业务调用点 | 用途 |
| --- | --- | --- | --- | --- |
| A801-1 | 查询全票池发票数据 | `queryInvoicePool` | InvoiceReceiptServiceImpl:285、1110 | 选票弹窗实时查询;保存收票登记落镜像;下载回源兜底 |
| A801-3 | 收票单查询 | `queryReceivedInvoice` | KingdeeReceivedInvoiceSyncSupport:115 | 推送幂等预检;定时回写 |
| **A801-2** | **文件下载(发票文件)** | `downloadInvoice` | InvoiceReceiptServiceImpl:1248、KingdeeAttachmentSyncSupport:367 | **见第三节** |
| A102-4 | 财务应付单查询 | `queryApBill` | KingdeeApBillSyncSupport:155、182 | 幂等预检;正式结算内码反查 |
| A302-4 | 财务应收单查询 | `queryArBill` | KingdeeArBillSyncSupport:106、133 | 幂等预检;应收结果回写、开票前置校验、内码反查 |
| A103-2 | 付款申请单查询 | `queryPayApply` | PaymentApplicationServiceImpl:743 | 付款结果定时回写;单条状态回查 |
| A103-4 | 付款处理查询 | `queryPayBill` | PaymentApplicationServiceImpl:833 | 付款处理结果勾稽回写 |
| A303-4 | 开票申请单查询 | `queryInvoiceApply` | InvoiceApplicationServiceImpl:581 | 推送幂等预检;附件内码反查 |
| A303-5 | 开票申请发票查询(OpenApi) | `queryInvoiceApplyInvoice` | InvoiceApplicationServiceImpl.executeInvoiceResultSync | 开票结果回写(发票号 + 板式文件/数电PDF/XML文件地址落库,2026-09-30 扩展) |
| A902-1 | 客户查询 | `queryCustomer` | CustomerArchiveServiceImpl:554、821;BfiCustomerController:70 | 推送前置幂等复查;定时同步复查;客商列表穿透查询 |
| A801-4 | 汇率查询 | `queryExchangeRate` | BfiExchangeRateController:63(GET /bfi/exchange-rate) | 前端汇率展示 |
### 2.3 客户端已实现、业务零调用(仅测试覆盖)
| 编号 | 接口 | 说明 |
| --- | --- | --- |
| A301-1 | 收款单查询(`queryRecBill`) | 收款认领链路未启用 |
| A301-2 | 收款认领公告查询(`queryClaimAnnounce`) | 同上 |
| A301-3 | 收款认领结果推送(`pushClaimResult`) | 同上(上传方向) |
| A100-2/3 | 令牌校验/撤回 | 基础设施预留,token 管理由 `BfiRequestInterceptor`/缓存承担 |
> 结论:**收款认领(收款单从金蝶下载 → TMS 认领 → 结果回推)是设计上预留、尚未接线的下载场景**,是目前除发票文件外唯一「未来可能需要从金蝶下载」的业务,与《业财融合方案》中收款认领规划对应。
---
## 三、文件下载链路(金蝶 → TMS)——「下载什么」的完整答案
### 3.1 唯一被下载的文件:票池发票文件
文件从哪来:A801-1 票池查询响应的 `downloadUrl`(16 位 hex 文件 id,另有 `imageUrl`/`pdfurl`/`snapshotUrl` 同形态),保存收票登记落镜像时序列化为 `blade_kingdee_invoice_pool.attachmentsJson`:`{"source":"piaozone","path":"<downloadUrl>","name":"<发票号>.pdf"}`(InvoiceReceiptServiceImpl `buildAttachmentJson`:1206-1215)。
文件怎么取:A801-2 `POST /ierp/kapi/v2/awrp/bosext/attachment/download`,body `{"path":"..."}`,返回文件流。BFI 层实现 `BfiServiceImpl.downloadInvoice`:532-555(校验 path 非空、HTTP 200、日志齐全),失败时金蝶返回 JSON 错误体(如 `FILE_IS_NOT_EXIST`「附件不存在。」)。
### 3.2 用途一:附件同步链路内下载回传(后端自动,无前端交互)
`KingdeeAttachmentSyncSupport.fetchContent`:365-385:当附件项 `kingdeePathSource=true`(镜像发票文件)时,调 `bfiService.downloadInvoice(path)` → `readCapped` 流式读取(64KB 缓冲,**20MB 封顶**,超限中断并记失败原因)→ 内存字节数组 → A902 上传至收票单附件面板。
特点:同步链路内的下载**必须**经 20MB 软限制(A902 上传契约约束),下载的文件即传即走、不落盘不存 OSS。
### 3.3 用途二:前端下载通道(用户点击,2026-09-27 新增,当前未提交)
端点:`GET /blade-transport/invoice-receipt/invoice-pool/attachment/download?invoiceNo=<发票号>`(InvoiceReceiptController:90-96,类级 `@PreAuth(menu="invoice_receipt")`)。
实现(InvoiceReceiptServiceImpl `downloadPoolAttachment`:1219-1312):
```
按发票号定位 path(resolvePoolAttachment)
① 本地镜像优先:blade_kingdee_invoice_pool 按票号查(MP 租户插件自动隔离),解析 attachmentsJson 取 path/name
② 镜像未落库或无文件 → A801-1 实时回源(queryPoolByInvoiceNo,模糊匹配结果做票号全等防御)
→ bfiService.downloadInvoice(path) ← 在写任何响应字节之前
→ 校验:HTTP 200(BFI 层)+ Content-Type 非 application/json(防 JSON 错误体当文件下发)
→ 写响应头(Content-Type 缺省兜底 application/pdf / Content-Length 透传 / Content-Disposition RFC5987 文件名)
→ 8KB 缓冲流式透传 feign Response → ServletOutputStream,不落盘不进堆,finally 关闭 Response
```
错误处理:写流前的业务失败(票号空/无金蝶侧文件/BFI JSON 错误体)经 `WebUtil.renderJson(response, R.fail(msg))` 写 HTTP 200 JSON,前端按响应 Content-Type 是否含 `application/json` 统一识别;传输中断(IOException,客户端断开)仅记日志。**此通道不设 20MB 上限**(20MB 是 A902 上传契约,A801-2 下载无此约束)。
前端三处入口(票池弹窗附件列、正式结算认领发票行、付款申请发票附件行)经 `kingdee-attachment.js` 分流:`source === 'piaozone'` 或「有 path 且非 http(s) 开头」判为金蝶型走该端点;OSS 直链附件维持现有 `downloadFileByUrl` 行为不变。
### 3.4 用途三:销项开票发票文件直链查看/下载(2026-09-30 新增)
开票申请链路的销项发票文件不走 A801-2:A303-5 开票结果查询响应自带发票云文件直链——`invoiceFileUrl`(板式文件:数电票 OFD、航信百望税盘普通电票 PDF、电子专票/UKey 电票 OFD)、`invoiceImageUrl`(PDF 图片预览地址)、`invoicePdfFileUrl`/`invoiceXmlFileUrl`(数电 PDF/XML,发票云 5.0.025+ 返回)。
- **落库**:`executeInvoiceResultSync` 回写时将四个地址独立落 `blade_invoice_application_invoice`(补丁 `blade_invoice_application_invoice_urls_20260930.sql`),原文 JSON 照旧留档;已落库但缺地址的历史记录由同任务字段级补空(只补空不覆盖已有值)。
- **展示**:正式结算单详情 `FormalSettlementVO.issuedInvoices`(`FormalSettlementServiceImpl.listIssuedInvoices`:结算单 → 开票申请关联 → 发票结果,仅保留非作废未删除申请),前端 `formal-settlement-editor.vue` 发票信息卡片内只读表格「开票发票」提供查看/下载。该列表只读、不落 `blade_formal_settlement_invoice`、不参与匹配金额计算,避免被 `rebuildInvoices` 全删重建逻辑误持久化。
- **下载通道**:URL 为 http(s) 直链,按 3.3 前端分流约定归入「OSS 直链」一类——前端 `window.open`(查看,数电 PDF 优先)/ `downloadFileByUrl`(下载,按直链扩展名命名)直连,不经 A801-2 代理,不新增文件流接口。
- **时效性**:直链有效期未知,与 5.2 缺口 #4(票池文件时效性)同源待金蝶确认;回写补齐策略为仅补空不刷新,若金蝶确认直链会过期需另行设计刷新机制。
### 3.5 数据下载(查询回写)的触发机制
| 定时任务(PaymentKingdeeResultTask / CustomerKingdeeSyncTask) | cron 默认 | 拉取内容 | 回写目标 |
| --- | --- | --- | --- |
| payResult | 每 10 分钟 | A103-2 付款申请单状态 + A103-4 付款处理结果 | 付款申请 kingdee_status |
| receivedInvoiceResult | 每 10 分钟 | A801-3 收票单结果 | 收票登记 kingdee_push_status、内码 |
| formalSettlement 应收回写(`syncKingdeeArResult`:631,随 payResult 任务调度) | 同上 | A302-4 应收单结果 | 正式结算应收面板状态 |
| invoiceApplication 开票结果(`syncKingdeeInvoiceResult`:803) | 同上 | A303-4 申请单 + A303-5 发票明细 | 开票申请状态 + 发票号落库 |
| customer(CustomerKingdeeSyncTask) | 每天凌晨 2 点 | A902-1 客商(按信用代码批量复查) | 客商档案协同状态 |
另有手动触发点:各单据「同步金蝶结果」按钮(如 InvoiceReceiptController `/sync-kingdee-result`)、推送前的幂等预检(A102-4/A302-4/A801-3/A303-4/A103-2)、附件内码反查、票池选票弹窗实时查询(A801-1,游标分页 + 可用性过滤)。
---
## 四、文件上传链路(TMS → 金蝶)——A902 附件同步
### 4.1 唯一上传通道
`KingdeeAttachmentSyncSupport.syncAttachments`:125-189,经 `BfiServiceImpl.uploadAttachment`:558-583 调 A902 `POST /ierp/kapi/v2/frame/attachment/uploadFile`(multipart,`file` + `attachmentUploadFileArgs` JSON:entityNumber/billPkId/controlKey),返回 `data.path` 落 `blade_kingdee_attachment_sync.kingdee_path`。
核心机制:
- **幂等**:按 `(biz_type, biz_id, attachment_key)` 唯一键;同步前载入已成功集合,命中即跳过(重推 A902 零调用);失败记录重试成功时覆盖更新
- **幂等键**:用户附件 = 附件 url;发票文件 = A801-2 的 path(path 变化即红冲重开,自动视为新文件重传)
- **限制**:单文件 20MB 软限制(下载前按声明大小拦截 + 下载后按实际字节校验)
- **隔离**:逐附件独立 try/catch,失败记 `fail_reason` 继续下一个;附件失败不回滚主单、不阻塞同批其他单据
- **补传**:不自动重试;重推主单时自动补传(主单被幂等预检跳过不影响附件分支)
### 4.2 五类单据接入点(均已落地,主单事务提交后执行)
| 单据 | biz_type → entityNumber | 接入点(代码位置) | 附件来源 | billPkId 缺失时的反查 |
| --- | --- | --- | --- | --- |
| 付款申请 | `pay_application` → `ap_payapply` | PaymentApplicationServiceImpl.syncKingdee:387 → doSyncKingdee 事务提交后 :403 → syncAttachmentsAfterPush:414 | 单据 attachmentsJson(用户附件) | 无(本地 kingdeeBillId 缺失即跳过待重推) |
| 正式结算-应付 | `formal_settlement_ap` → `ap_finapbill` | FormalSettlementServiceImpl.syncKingdee:510 → syncAttachmentsAfterPush:525 | 结算单 attachmentsJson | A102-4 按防重键反查回填(`resolveKingdeeBillId`) |
| 正式结算-应收 | `formal_settlement_ar` → `ar_finarbill` | 同上(按 settlementType 分流 :547-548) | 同上 | A302-4 反查回填 |
| 收票登记 | `invoice_receipt` → `ap_invoice` | InvoiceReceiptServiceImpl.pushOneReceivedInvoice:720,A303-2 落 pushed 后、A302-6 指定应付前 :786 → syncAttachmentsAfterPush:809 | 登记附件(用户附件)+ 镜像发票文件(**A801-2 下载**) | A801-3 预检行自带 id 回填 :752-754 |
| 开票申请 | `invoice_application` → `sim_original_bill` | InvoiceApplicationServiceImpl.syncKingdee:497 → syncAttachmentsAfterPush:511 | 单据 attachmentsJson | A303-4 勾稽回填带回 |
流水落点:收票登记(`InvoiceReceiptRecord`)与开票申请(`InvoiceApplicationRecord`)有 actionType 流水表,追加 `attachment_upload` 记录;付款申请/正式结算无流水表不追加。列表聚合态(无附件/未上传/部分未传/已上传)由 `successCountByBizId`(一次 GROUP BY)+ `aggregateStatus` 运行时聚合。
### 4.3 数据上传(单据推送)不在本档展开
付款申请(A103-1/A103-3)、财务应付/应收单(A102-2/A302-2)、收票单+指定应付(A303-2/A302-6)、开票申请(A303-1)、客商(A902-2)的报文组装与状态机,见各 Support 类与《业财融合方案》主文档。
---
## 五、上传 ↔ 下载对应关系(能否对上)
### 5.1 已闭环的对应
| 对应项 | 上传侧 | 下载侧 | 状态 |
| --- | --- | --- | --- |
| **票池发票文件** | A801-2 下载(20MB 封顶)→ A902 回传收票单附件面板,key=path | A801-2 前端下载端点(不设上限),镜像优先 + A801-1 回源 | ✅ 完整闭环:同一份文件既能自动回传金蝶,又能给前端人工查看/下载 |
| **用户附件(OSS 直链)** | http(s) 下载(60s 超时、20MB 封顶)→ A902 上传 5 类单据 | 不经金蝶:前端直连 OSS(`downloadFileByUrl` 现有行为) | ✅ 各自闭环,两类附件按字段特征分流互不干扰 |
| **单据数据面** | A103-1/3、A102-2、A302-2、A303-2/6、A303-1、A902-2 | A103-2/4、A102-4、A302-4、A801-3、A303-4/5、A902-1(幂等预检 + 内码反查 + 结果回写) | ✅ 每个推送接口都有查询接口配套,成对出现 |
| **票池元数据** | (消费方)收票登记落镜像、A303-2 分录取数 | A801-1 实时查询(选票弹窗 + 回源) | ✅ 闭环 |
| **销项开票发票文件** | (数据面)A303-1 开票申请推送 | A303-5 回写发票结果含文件直链,落库后前端直连查看/下载(不经 A801-2) | ✅ 闭环(2026-09-30 补充,见 3.4) |
### 5.2 对不上的缺口(单向或未启用)
| # | 缺口 | 现状 | 影响 |
| --- | --- | --- | --- |
| 1 | **已上传附件的金蝶侧回源下载** | A902 上传成功后 `kingdee_path` 已落 `blade_kingdee_attachment_sync`,但没有任何代码用它发起 A801-2 下载 | TMS 侧附件若 OSS 丢失/清理,无法从金蝶恢复。A902 返回 path(`211e6ce5c9805c00`)与 A801-2 path(`234e244b2c803400`)同为 16 位 hex,疑似同命名空间,但兼容性**未验证**——2026-09-27 设计「不做的事」第 1 条明确列为二期待确认项 |
| 2 | **收款认领链路** | A301-1 收款单查询、A301-2 认领公告查询、A301-3 结果推送在 `IBfiClient`/`BfiServiceImpl` 已完整实现,业务层零调用 | 金蝶收款数据下载 → TMS 认领 → 结果回推整条链路待启用;启用前 A301 系列不产生任何下载 |
| 3 | **客商资质附件** | `KingdeeAttachmentConstant.ENTITY_NUMBERS` 仅预留注释(CustomerArchiveServiceImpl 侧无任何附件上传接线);客商的 entityNumber/controlKey 待金蝶确认 | 客商附件目前既不上传也不下载,二期接入时补一个 biz_type 条目即可复用现有架构 |
| 4 | 票池文件时效性 | 镜像 `attachmentsJson` 落库后 path 是否会过期未知;下载时镜像快照过期(红冲重开)报「附件不存在」**不自动刷新**,与上传取快照口径一致。2026-09-30 落库的 A303-5 销项发票云直链(见 3.4)同样受时效性约束,回写仅补空不刷新 | 需金蝶确认 `downloadUrl`/发票直链有效期与失效表现,决定是否要补定时刷新镜像机制 |
| 5 | 附件面板反向查询 | 金蝶无「查询单据附件列表」接口接入 | 附件同步状态只能以本地同步表为准,无法与金蝶侧附件面板对账 |
---
## 六、验证核对表(文档 vs 代码一致性)
| 设计文档断言 | 代码核实结果 |
| --- | --- |
| A902 五类单据接入、entityNumber 映射 | ✅ `KingdeeAttachmentConstant.ENTITY_NUMBERS` 五条映射与文档完全一致,未配置类型抛 ServiceException |
| A801-2 复用 `invoiceDownload`,附件链路无新增接口 | ✅ `KingdeeAttachmentSyncSupport.fetchContent` 直接调 `bfiService.downloadInvoice` |
| 附件上传在主单事务提交后执行(ADR-7) | ✅ 付款申请经 `syncKingdee` 包装层(doSyncKingdee 事务外 :403);正式结算/开票申请经 `SpringUtil.getBean(...).doSyncKingdee` 自代理(:512/:499);收票在推送编排内但独立 try/catch(:786) |
| 幂等键 url/path、失败覆盖更新、20MB 双重校验 | ✅ syncAttachments:144-177(跳过/超限拦截/覆盖更新),readCapped:388-403 |
| 下载按发票号定位、镜像优先 + 实时回源、先校验后写流 | ✅ resolvePoolAttachment:1292-1312、doDownloadPoolAttachment:1242-1285,JSON 错误体解析 readBfiJsonMessage:1315-1332 |
| 下载不设 20MB 上限、8KB 流式透传 | ✅ DOWNLOAD_BUFFER_SIZE 缓冲拷贝,无 readCapped 调用 |
| 失败经 `WebUtil.renderJson` 返回 200 JSON(勘误 #2) | ✅ downloadPoolAttachment:1222-1235 |
| 收票/开票申请追加 `attachment_upload` 流水,付款/结算不追加 | ✅ InvoiceReceiptServiceImpl:823、InvoiceApplicationServiceImpl:522;付款/结算接入点无 record 调用 |
---
## 七、顺带发现的风险点(与本次梳理主题相关,建议确认)
1. **客商定时同步被硬编码过滤到单一客商**:`CustomerArchiveServiceImpl.syncKingdeeCustomerData`:625-628 的查询条件含 `.eq(CustomerArchive::getCustomerCode, "KS000067")`,即每天凌晨 2 点的客商同步任务只会处理这一个客商编码,疑似联调/灰度遗留。上线前需移除,否则其余客商的定时推送与状态复查全部不生效。
2. **2026-09-27 下载功能代码未提交**:`InvoiceReceiptController`、`IInvoiceReceiptService`、`InvoiceReceiptServiceImpl` 的下载端点改动仍在工作区(git status modified),设计文档已写 as-built,注意随下次提交入库。
3. 镜像定位 `resolvePoolAttachment`:1293-1294 按票号 selectList 后取 `mirrors.get(0)`:依赖「同租户票号唯一」(A801-1 upsert 匹配键保证),若上游契约变化出现重复票号将静默取第一条,可考虑加 `last("limit 1")` 或唯一索引兜底(当前表无票号唯一索引约束见 `blade_kingdee_invoice_pool_a8011_20260920.sql`)。
---
## 八、术语与代码索引
| 术语 | 代码落点 |
| --- | --- |
| A902 附件上传 | `IBfiClient.uploadAttachment`(blade-bfi-api feign)/ `BfiServiceImpl.uploadAttachment` |
| A801-2 文件下载 | `IBfiClient.invoiceDownload` / `BfiServiceImpl.downloadInvoice` |
| 附件同步主流程 | `org.springblade.transport.support.KingdeeAttachmentSyncSupport` |
| 附件同步表 | `blade_kingdee_attachment_sync`(实体 `KingdeeAttachmentSync`,建表 `doc/sql/transport/blade_kingdee_attachment_sync_20260924.sql`) |
| 票池镜像 | `blade_kingdee_invoice_pool`(`KingdeeInvoicePool`,`InvoiceReceiptServiceImpl` 内 upsert) |
| 开票回写发票结果 | `blade_invoice_application_invoice`(实体 `InvoiceApplicationInvoice`,建表 `blade_invoice_application_invoice_20260920.sql` + 文件地址补丁 `blade_invoice_application_invoice_urls_20260930.sql`) |
| 前端下载端点 | `InvoiceReceiptController.downloadPoolAttachment`(GET /invoice-receipt/invoice-pool/attachment/download) |
| entityNumber 映射 | `org.springblade.transport.constant.KingdeeAttachmentConstant` |
| 定时回写任务 | `org.springblade.transport.task.PaymentKingdeeResultTask`、`CustomerKingdeeSyncTask` |
@@ -0,0 +1,648 @@
# 金蝶(星瀚)BFI 接口调用全链路梳理(按业务域汇总·更新版)
> 整理日期:2026-09-27
> 依据:当前代码(`tms-api/blade-third-party-api/blade-bfi-api` 客户端 + `tms-api/blade-service/blade-transport` 业务实现,git 基线 `50573e7` 2026-09-24)与三份材料逐条核对:
> ① `doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`(2026-09-14,6 张集成流程图 OCR 文字版);
> ② `doc/docs/TMS业财融合方案.docx`(V1.2,2026-08-10,8 大集成场景方案);
> ③ `doc/docs/业务流程/2026-09-23-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总).md`(上一版总纲)。
> 本文是 09-23 总纲的**全面更新版**:自 09-23 以来代码有 12 个提交(09-23 19:00 收票防重键修复 + 09-24 两批重构/新增),接口清单、分录口径、勾稽方式、联调残留与缺陷清单均有实质变化,全文按当前代码重新核实。
> 每个接口仍按 **触发场景 → 调用顺序 → 参数来源 → 响应存储 → 依赖与幂等** 五个维度说明;文末附全景依赖、状态机、定时任务与配置开关、差异/风险清单。
---
## 目录
- [0. 阅读须知与本次更新要点](#0-阅读须知与本次更新要点)
- [1. 集成总览与公共机制](#1-集成总览与公共机制)
- [2. 业务域一:客商档案(A902-1 客户查询 / A902-2 客户保存)](#2-业务域一客商档案a902-1-客户查询--a902-2-客户保存)
- [3. 业务域二:财务应付与付款(A102-4 财务应付单查询 / A102-2 财务应付单保存 / A103-1 应付下推生成付款申请 / A103-3 预付款申请保存 / A103-2 付款申请单查询 / A103-4 付款处理查询)](#3-业务域二财务应付与付款)
- [4. 业务域三:财务应收与开票(A302-4 财务应收单查询 / A302-2 财务应收单保存 / A303-4 开票申请单查询 / A303-1 开票申请单新增 / A303-5 开票发票查询)](#4-业务域三财务应收与开票)
- [5. 业务域四:收票与金蝶票池(A801-1 全票池发票查询 / A303-2 收票单批量保存 / A302-6 收票指定应付 / A801-3 收票单查询)](#5-业务域四收票与金蝶票池)
- [6. 业务域五:收款认领(A301-2 收款认领公告查询 / A301-3 收款认领结果推送 / A301-1 收款单查询)★新增](#6-业务域五收款认领a301-2-收款认领公告查询--a301-3-收款认领结果推送--a301-1-收款单查询新增)
- [7. 业务域六:公共取数(A801-4 汇率)★新启用](#7-业务域六公共取数a801-4-汇率新启用)
- [8. 全景:接口先后顺序与依赖关系](#8-全景接口先后顺序与依赖关系)
- [9. 状态机汇总](#9-状态机汇总)
- [10. 定时任务与配置开关汇总](#10-定时任务与配置开关汇总)
- [11. 代码与三份材料差异 / 风险清单(更新版)](#11-代码与三份材料差异--风险清单更新版)
---
## 0. 阅读须知与本次更新要点
1. **接口编号口径**:金蝶星瀚侧开放接口以 `Axxx-x` 操作码标识,代码常量、文档标题、日志均沿用该口径。
2. **权威顺序**:以**当前代码**为准;与 09-23 总纲及单链路文档冲突处一律按本文第 11 节结论。
3. **本次(09-23 → 09-27)核心变化一览**:
| # | 变化 | 说明 |
|---|---|---|
| 1 | **收款认领链路接线(新增业务域五)** | A301-2 公告同步 / A301-3 认领推送 / A301-1 收款单回写由「客户端已实现无调用」变为「业务侧已接线(全部手动触发,无定时)」 |
| 2 | **A801-4 汇率查询启用(新增业务域六)** | 新增 `GET /bfi/exchange-rate`,结算页面按币种+日期取金蝶汇率;A102-2/A302-2 推送时传 `exchangerate/exratedate`(结算单页面维护值) |
| 3 | **应付/应收/收票分录口径重构** | 分录 = **结算明细行(每行一条)+ 手工结算合计费用项(每行一条)**(新表 `blade_formal_settlement_summary_fee`,仅 manualFlag=1);分录新增「运输货物 / 车号」字段(仅明细行传) |
| 4 | **A103-1 勾稽方式重大变化** | **A102-4 反查付款计划分录 `awrp_entry_code_p` 的步骤已移除**;`awrp_finapbill_entryid` 改为**本地组装的主结算单应付单防重键**(`settlementApBillSrcCode`),分录由多条费用行改为**单条**(付款申请号-1) |
| 5 | **A103-1 结算方式与往来账户完善** | `e_settlementtype_number` 按付款方式显式映射(汇票→012-02 应付票据电子银行承兑汇票,其余→02 银行转账);`e_bebank_number` 改取客商收款账户(`blade_customer_receipt_account`)**联行号**,未维护即拦截 |
| 6 | **开票申请按发票张次拆分** | 新表 `blade_invoice_application_sheet`:**每张发票张次推一张金蝶开票申请单**;billno 单张=申请单号、多张=申请单号-张次序号;A303-4 预检改为**按全部候选 billno 批量幂等预检**(已生成张次回填跳过、未生成张次重推) |
| 7 | **收票推送防重键修复** | A302-6 指定应付与 A801-3 **推送预检**统一改用收票登记 id(原误用发票号,导致指定应付报「收票单不存在」、预检永不命中);⚠️ **A801-3 定时回写仍按发票号匹配,缺陷残留**(见 11.2) |
| 8 | **物料/计量单位真实映射链路建成(调试覆盖仍在)** | 物料按「装卸大类 / 国际业务 / 国内+运输方式」映射名称 → `BFI_MATERIAL_NUMBER_MAP` 映射金蝶编码;计量单位按合同计费方案计费单位 → 计量单位档案金蝶编码(`blade_measurement_unit` 已初始化、新表 `blade_kingdee_material`);但三处 `if(true)` 调试组合块仍做**最终覆盖**(兜底 物流配送 Item-00001080+TON;收票侧未命中还**随机取组合**) |
| 9 | **客商分组 1102 问题修复** | `customerKind` 已落库(`CustomerArchive.customerKind`),internal→1101 / 其余→1102 可正确分支 |
| 10 | **主数据初始化 SQL** | `doc/sql/transport/kingdee_master_data_init_20260924.sql`:计量单位金蝶编码(TON/CUM/PCS/KG/KM/25/04/sou/014/013)+ 金蝶服务类物料档案 `blade_kingdee_material` |
4. **两条推送主线 + 两条数据主线 + 一条认领主线**:
- 应付主线:财务应付单(A102-2)→ 付款申请下推(A103-1,预付款走 A103-3)→ 状态回写(A103-2/A103-4);
- 应收主线:财务应收单(A302-2)→ 开票申请(A303-1,按张次拆分)→ 开票结果(A303-5);
- 数据主线:客商档案(A902-1/A902-2)、金蝶发票池(A801-1)、汇率(A801-4);
- 认领主线:收款认领公告(A301-2)→ 认领结果推送(A301-3)→ 收款单回写(A301-1)。
---
## 1. 集成总览与公共机制
### 1.1 调用架构
```
blade-transport 业务服务
│ 调用 IBfiService(参数校验 + 自动取令牌)
▼
blade-bfi-api(Feign 客户端 IBfiClient,@FeignClient url=${thirdParty.bfi.baseUrl})
│ 请求头:access_token(业务令牌) + Authorization(iPaaS 网关认证,BfiRequestInterceptor 统一注入)
▼
iPaaS 网关(/gwzh/BFI,不直连金蝶)
▼
金蝶星瀚 /ierp/kapi/...
```
- 客户端模块:`blade-third-party-api/blade-bfi-api`(`org.springblade.thirdparty.bfi`)。
- 业务编排模块:`blade-service/blade-transport`(`support/` 三个推送组件、`service/impl/` 各业务实现、`task/` 定时任务)。
- **无重试**:Feign 未配 Retryer(NEVER_RETRY)、无 fallback;失败抛 `BfiException`,由调用方决定(批量循环逐条 catch,单条失败不中断整批)。
- 配置前缀两套并存(读同一批 nacos 键的不同子集):
- bfi-api `BfiProperties`(prefix `third-party.bfi`):baseUrl / authorization / clientId / clientSecret / username / accountId / language / mockEnabled;
- transport `BfiProperties`(prefix `thirdparty.bfi`):srcSys(默认 WLTMS)/ payApply.*(billtype=`ap_payapply_oth_BT_S`、exratetable=`01`、srcType=`0KzXs`)/ invoiceApply.*(srcType=`0KzXs`)/ receivedInvoice.*;
- `payResult.*`、`receivedInvoiceResult.*`、`customer.*` 由定时任务以 `@Value` 消费。
- 配置样例见 `doc/nacos/third-party-api.yaml`(默认 baseUrl `http://172.16.203.134:8000/gwzh/BFI`)。
### 1.2 接口清单总表(含在用状态,2026-09-27)
来源:`blade-bfi-api/.../feign/IBfiClient.java`(URL 可被 `thirdParty.bfi.*Url` 配置覆盖)。
| 操作码 | 星瀚端点 | Feign 方法 | 用途 | 业务侧是否在用 |
|---|---|---|---|---|
| A100-1 | POST /ierp/kapi/oauth2/getToken | getToken | 获取令牌 | ✅ 自动(每次调用前) |
| A100-2 | POST /ierp/kapi/oauth2/verifyToken | verifyToken | 校验令牌 | ❌ 已实现无调用 |
| A100-3 | POST /ierp/kapi/oauth2/withdrawToken | withdrawToken | 撤回令牌 | ❌ 已实现无调用 |
| A102-2 | POST /ierp/kapi/v2/awrp/ap/ap/ap_finapbill/save | apBillSave | 财务应付单保存 | ✅ 正式结算单(应付)推送 |
| A102-4 | GET /ierp/kapi/v2/awrp/ap/ap_finapbill/query | apBillQuery | 财务应付单查询 | ✅ 预检/硬校验/批量预热(**反查付款计划分录已移除**) |
| A103-1 | POST /ierp/kapi/v2/awrp/ap/ap/ap_finapbill/push | payApplyPush | 应付下推生成付款申请 | ✅ 付款申请推送(单分录) |
| A103-2 | GET /ierp/kapi/v2/awrp/ap/ap_payapply/query | payApplyQuery | 付款申请单查询 | ✅ 推送预检 + 定时回写 |
| A103-3 | POST /ierp/kapi/v2/awrp/ap/ap/ap_payapply/save | savePrePayApply | 预付款申请保存 | ✅ 预付款(项目垫资/进度垫资)直推 |
| A103-4 | GET /ierp/kapi/v2/awrp/cas/cas_paybill/query | payBillQuery | 付款处理查询 | ✅ 定时回写付款记录 |
| A301-1 | GET /ierp/kapi/v2/awrp/cas/cas_recbill/queryRecBill | recBillQuery | 收款单查询(收款处理查询) | ✅ **认领结果回写(手动)** ★新启用 |
| A301-2 | GET /ierp/kapi/v2/awrp/cas/cas_claimannounce/query | claimAnnounceQuery | 收款认领公告查询 | ✅ **收款流水同步(手动)** ★新启用 |
| A301-3 | POST /ierp/kapi/v2/awrp/cas/cas/cas_claimbill/claimbillPush | claimBillPush | 收款认领结果推送 | ✅ **认领推送(认领事务内)** ★新启用 |
| A302-2 | POST /ierp/kapi/v2/awrp/ar/ar/ar_finarbill/save | arBillSave | 财务应收单保存 | ✅ 正式结算单(应收)推送 |
| A302-4 | GET /ierp/kapi/v2/awrp/ar/ar_finarbill/query | arBillQuery | 财务应收单查询 | ✅ 预检/硬校验/定时勾稽 |
| A302-6 | POST /ierp/kapi/v2/awrp/ap/ap/ap_invoice/assign | invoiceAssign | 收票指定应付 | ✅ 收票推送第二步 |
| A303-1 | POST /ierp/kapi/v2/awrp/sim/sim_original_bill/save | invoiceApplySave | 开票申请单新增 | ✅ **按发票张次逐张推送** |
| A303-2 | POST /ierp/kapi/v2/awrp/ap/ap_invoice/cusBatchSave | receivedInvoiceSave | 收票单批量保存 | ✅ 收票登记推送 |
| A303-4 | GET /ierp/kapi/v2/awrp/sim/sim_original_bill/query | invoiceApplyQuery | 开票申请单查询 | ✅ **批量 billno 幂等预检** |
| A303-5 | POST /ierp/kapi/app/sim/openApi | invoiceApplyOpenApi | 开票发票查询(票易通 openApi,Base64 包裹) | ✅ 定时回写开票结果 |
| A801-1 | POST /ierp/kapi/v2/awrp/rim/rim_invoice/query | invoicePoolQuery | 全票池发票查询(rim 契约) | ✅ 选票/保存重查/补票种(实时,无定时) |
| A801-2 | POST /ierp/kapi/v2/awrp/bosext/attachment/download | invoiceDownload | 进项发票文件下载 | ❌ 已实现无调用(备用) |
| A801-3 | GET /ierp/kapi/v2/awrp/ap/ap_invoice/apinvoicequery | receivedInvoiceQuery | 收票单查询 | ✅ 推送预检 + 定时回写 |
| A801-4 | GET /ierp/kapi/v2/awrp/base/bd_exrate_tree/Forexquery | forexQuery | 汇率查询 | ✅ **结算取汇率(手动,页面触发)** ★新启用 |
| A902-1 | GET /ierp/kapi/v2/awrp/basedata/bd_customer/query | customerQuery | 客户查询 | ✅ 档案回填/推送前复查/定时复查 |
| A902-2 | POST /ierp/kapi/v2/awrp/basedata/bd_customer/save | customerSave | 客户保存 | ✅ 档案推送 |
> 与基础业务文档《TMS业财融合-集成流程图文字版与业务流程梳理》第 10 节「接口速查」的差异:文档清单中的 **A901-1 供应商查询、A801-5 黑名单、A913 项目保存、A101-1/2 合同保存、A102-1/A302-1 暂估单、A902 附件上传** 均未在客户端实现(合同/项目同步走合同合规系统,非金蝶 A 类接口;A902 附件上传各推送链路均未接,方案文档中的「审批单据/磅单等作为附件推送金蝶」暂未实现)。
**查询类接口时间窗约束**(IBfiService Javadoc):A103-2 付款申请单查询 / A103-4 付款处理查询 / A102-4 财务应付单查询 / A302-4 财务应收单查询 / A303-4 开票申请单查询 / **A301-1 收款单查询** 查询区间 **≤ 1 个月**;A902-1 客户查询 **≤ 6 个月**;A301-2 收款认领公告查询 无时间窗参数(按认领用户+分页)。
### 1.3 认证与令牌(A100-1 获取令牌)
- **取令牌**:`BfiServiceImpl.getToken()`。请求体 `BfiTokenDTO`:`client_id / client_secret / username / accountId(数据中心) / language`,另由 `BfiUtil.fillRandomParam` 填充 `nonce`(UUID) 与 `timestamp`(服务端校验 ±5 分钟)。
- **缓存**:Redis,key=`blade:third:bfi:token`;TTL = `expires_in` − 120 秒缓冲;`getAccessToken()` 先读缓存,未命中才调 A100-1 获取令牌。无 refresh 机制;写入用 `setIfAbsent`,已有未过期缓存时新令牌不会覆盖(排障需知)。
- **注入**:双头——业务方法 `@RequestHeader("access_token")` 显式携带;`BfiRequestInterceptor` 统一加 `Authorization` 网关头。`BfiFeignClientConfig` 用空拦截器覆盖全局 BladeFeignRequestInterceptor;超时 10s/60s。
### 1.4 响应封装、分页与异常处理
- **统一响应** `BfiResultVO<T>`:`data / errorCode("0"=成功) / message / status(Boolean)`;`isSuccess() = status && errorCode=="0"`。三类封装:`commonCall`(标准)、`commonRimCall`(rim 契约,兼容 "0"/"0000",A801-1 全票池发票查询用)、`commonOpenApiCall`(A303-5 开票发票查询,只看 `success`;请求/响应 data 均 Base64(JSON))。
- **保存类响应** `BfiBillSaveResultVO`:`successCount/failCount`(String)、`result[]`(`billIndex/billStatus/entryIds/errors/id/number/type: Add/Push/Update`)。业务侧统一 `requireBfiSaveSuccess` 校验 `result[0].billStatus`,失败抛异常。
- **分页**:`BfiPageVO{filter, lastPage, pageNo, pageSize, totalCount, rows}`;通用翻页 `BfiUtil.queryAllPages`(页码递增,终止:页空 / `lastPage=true` / rows 不满页 / 超 50 页)适用于 9 个 BfiPageVO 类查询接口。**不适用**:A801-1 全票池发票查询(rim 契约无 lastPage/totalCount,游标分页 `PoolScanCursor`,单请求上限 3 次远程调用)与 A801-4 汇率查询(单页固定 pageSize=10)。
- **异常转换**:`BfiExceptionTranslator`(@RestControllerAdvice)→ `R.fail(500)`。
### 1.5 批量同步上下文 KingdeeSyncContext(09-24 重构)
`transport/service/KingdeeSyncContext.java`:Java record,8 个可写缓存:
| 缓存 | 内容 | 变化 |
|---|---|---|
| `measureUnitCodeByName` | 计量单位名称→金蝶编码 | ★新增(配合主数据初始化 SQL) |
| `contractCache` | 合同 id→ContractManage | 保留 |
| `customerCodeCache` | 名称→客商编码 | 保留 |
| `projectCache` | 项目 id→ProjectApply | 保留 |
| `apBillNoCache` | 应付单防重键→金蝶单号 | 保留(含「未生成」空串标记) |
| `settlementDetailCache` | 结算单 id→结算明细列表 | 保留 |
| `settlementCache` | 结算单 id→FormalSettlement | ★新增(结算单维度传参) |
| `receiptAccountCache` | 客商 id→收款账户 | ★新增(付款联行号取数) |
| ~~`cargoCodeByName`~~ / ~~`apBillPlanCodeCache`~~ | — | **已删除**(货物编码映射被物料映射替代;付款计划分录反查已移除) |
- 生命周期 = 一次批量同步方法调用(`empty()` 创建,方法结束即弃),非 Spring Bean;缓存未命中自动回退逐单查询。
- 使用方:`PaymentApplicationServiceImpl`(批量前 `preloadApBillExistNos` 整批预热)、`FormalSettlementServiceImpl`、`InvoiceReceiptServiceImpl`。
### 1.6 防重键(候选键)总表(2026-09-27 口径)
金蝶侧按「来源系统 + 来源单据号」幂等(`awrp_src_sys` + `awrp_src_code`,分录级 `awrp_entry_code`)。组装工具 `ExternalSrcCodeSupport.buildSrcCode`(下划线拼接、去空白、去重保序)。
| 金蝶单据 | awrp_src_code | awrp_src_sys / srcType | 分录 awrp_entry_code | 预检查重接口 |
|---|---|---|---|---|
| 财务应付单 A102-2 | `正式结算单号_结算明细单据号1_…` | WLTMS;`TMS财务应付单` | 明细行=**结算明细单据号**;手工合计费用项行=**结算单号-费用类型-费用项**拼接 | A102-4 财务应付单查询 |
| 财务应收单 A302-2 | 同应付口径 | WLTMS;`TMS财务应收单` | 同应付口径 | A302-4 财务应收单查询 |
| 付款申请 A103-1 | `paymentNo` | WLTMS;srcType=配置 `0KzXs` | **固定 `paymentNo-1`(单分录)** | A103-2 付款申请单查询 |
| 预付款申请 A103-3 | 付款申请号口径 | WLTMS;`0KzXs` | `paymentNo-1`(单分录) | A103-2 付款申请单查询 预检共用 |
| 开票申请 A303-1 | `billno = application_no`(单张)或 `application_no-张次序号`(多张)★变化 | systemsource=WLTMS;srcType=`0KzXs` | 开票商品行 id(blade_invoice_application_line.id) | A303-4 开票申请单查询(**批量 billno**) |
| 收票单 A303-2 | `收票登记 id` | WLTMS;`TMS收票登记` | 与 A102-2 分录同源同值(明细行/手工合计行) | A801-3 收票单查询 |
| 收款认领 A301-3 | `SKCL+yyyyMMddHHmmss+认领id尾号6位`(blade_receipt_claim.awrp_src_code)★新增 | WLTMS | 结算单分摊行(seq 序号) | A301-1 收款单查询(sourcebillnumber 匹配) |
| 客户 A902-2 | `number = TMS客商编号`(KS+6位自增),`societycreditcode` 为金蝶侧候选键 | awrp_datasourcesys=WLTMS | — | A902-1 客户查询 |
### 1.7 公共档案取数(应付/应收/收票三侧复用 `KingdeeApBillSyncSupport`)
| 取数项 | 规则 | 变化 |
|---|---|---|
| 组织 org/payorg(应收 recorg) | 结算单 `contract_id` → `blade_contract_manage.organization_id` → `SysCache.getDept().getDeptCode()` | 取数主体由合同改为结算单(结算单维度传参) |
| 币种 | 合同 `settlement_currency`,`RMB→CNY` | 不变 |
| **汇率 exchangerate / exratedate** | **正式结算单页面维护值**(来源:结算页面经 `GET /bfi/exchange-rate` 调金蝶 A801-4 汇率查询回填);未维护不送 | ★新增 |
| 往来户 asstact_number | 名称(合同乙方 / 结算单付款方 / 开票单位)→ 本地 `blade_customer_archive` 按 `full_name/short_name` 匹配取 `customer_code`;查不到即报错拦截 | 不变 |
| 往来类型 asstacttype | 合同类别=`客户合同` → `bd_customer`,否则 `bd_supplier` | 不变 |
| **物料编码** | 真实映射链路:装卸大类费用行→`装卸`;国际业务模式/项目业务类型→`国际货运代理服务费`;国内→`国内道路货物运输服务-`+运单运输方式;再经 `BFI_MATERIAL_NUMBER_MAP` 映射金蝶编码(`Item-00001081` / `XDWL11010001` 等);⚠️ 三处 `if(true)` 调试组合块仍做最终覆盖(兜底 物流配送 Item-00001080+TON;**收票侧未命中随机取**) | ★重构 |
| **计量单位** | 费用类型/费用项匹配合同计费方案计费单位 → `blade_measurement_unit` 金蝶编码(`context.measureUnitCodeByName`);⚠️ 同被调试组合块覆盖 | ★重构 |
| 金额(含税) | 结算明细行=结算金额(含税);手工合计费用项行=合计行结算金额;原三级取数兜底保留 | 调整 |
| 金额(不含税) | 结算明细行取结算金额(不含税);**手工合计费用项行无不含税金额不传** | 调整 |
| **运输货物 / 车号** | 结算明细行传货物名称 / 车号(金蝶接口新字段);合计费用项行不传 | ★新增 |
| 日期文本 | 统一 `yyyy-MM-dd HH:mm:ss` | 不变 |
---
## 2. 业务域一:客商档案(A902-1 客户查询 / A902-2 客户保存)
**定位**:全局数据前提。其他链路的往来户编码(`customer_code`)来自 `blade_customer_archive`,与金蝶客户的对应关系由本链路建立(**金蝶客户编码 = TMS 客商编号**,无独立存储)。
![客商档案页面(2026-09-29):新增弹窗按统一社会信用代码满 18 位防抖触发 A902-1 查询回填;审批通过档案的 A902-2 推送为行内「同步金蝶」链接(仅 approved 且 kingdeeStatus=failed 时显示,人工重推入口),另有定时复查/重推任务](./2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版).assets/bfi-01-customer-archive.png)
### 2.1 A902-1 客户查询(回填 / 复查)
**触发场景(3 个入口)**
| 入口 | 触发 | 代码 |
|---|---|---|
| ① 前端新增回填 | 录入统一社会信用代码满 18 位防抖查询 → `GET /bfi/customer/query-by-credit-code`(pageNo=1/pageSize=1) | BfiCustomerController |
| ② 单条推送前复查 | `/customer-archive/sync-kingdee` 或审批通过内部推送前,`syncKingdee(id)` 先查一次 | CustomerArchiveServiceImpl |
| ③ 定时批量复查 | `CustomerKingdeeSyncTask`(cron `thirdParty.bfi.customer.cron` 默认 `0 0 2 * * ?`,开关默认 false) | CustomerKingdeeSyncTask |
**调用顺序**:无固定前置;定时路径按 50 个信用代码/批、`BfiUtil.queryAllPages` 自动翻页取全,键=大写信用代码,单批异常不中断。
**参数来源**:`societycreditcode` = `blade_customer_archive.unified_credit_code`;分页参数固定。
**响应存储**:
- 前端回填:`status=C` 回填名称/法人/地址/经营范围/注册资金/经营期限等表单字段(锁定客商名称/法人/注册资金);`A/B` 前端阻断保存。原始报文不存储。
- 后端复查:`C`→`kingdee_status=exist`(终态);`A/B`→`failed`(「金蝶侧该客商审核中,请稍后重推」)。
- 定时附加职责:synced/exist 且启用客商复查 `C`→exist;`enable=0` → 联动停用 TMS 档案 `status=2`(单向,金蝶恢复不自动启用)。
- 落库字段:`kingdee_status / kingdee_fail_reason / kingdee_sync_time`。
### 2.2 A902-2 客户保存(推送)
**触发场景(3 入口,收敛于 `pushCustomerToKingdee`)**:① 审批通过自动推送(`POST /customer-archive/approve`,失败不影响审批);② 列表人工重推(`POST /customer-archive/sync-kingdee`,仅 approved,synced/exist 幂等返回);③ 定时重推(pending = approved 且 none/failed)。
**调用顺序**:先 A902-1 复查(金蝶已有 `C` 直接置 exist 跳过)→ 组装报文(`CustomerBfiConverter.buildSaveDTO`)→ A902-2 保存。**只推新增建档**:审批后的档案变更不重推(无更新链路);`synced`(金蝶受理 B)不重推,由定时复查升级 exist。
**参数要点**:
- `number`=`customer_code`(submit 时生成 `KS`+6 位自增);`name/societycreditcode/artificialperson` = full_name/unified_credit_code/legal_person;
- `type`=customer_nature→字典 dictKey(解析失败抛异常,不兜底);`bizfunction` 固定 `1,2,3,4`;
- `entry_groupstandard`:**★已修复** —— `customerKind` 已落库(`CustomerArchive.customerKind`),internal→1101(内部组织)、其余→1102(外部客商);
- `entry_bank`:`blade_customer_receipt_account`(账号/开户行/CNY/isdefault);⚠️ `bankNumber` 仍硬编码 "001110012009"(CustomerBfiConverter:141,todo);
- `entry_linkman`:principal + contact_phone;`status` 不传。
**响应存储**:`result[0].billStatus=true` → `synced`;`false` → `failed` + errors.rowMsg(截 500 字);异常 → `failed`。返回的 number/id 不落库。
**依赖与幂等**:无前置;被应付/应收/收票/开票链路取 `customer_code`;⚠️ 其他链路**不校验客商 `kingdee_status`**(软依赖,见 11.2-#3)。幂等:本地 synced/exist 直接返回;金蝶按 `societycreditcode` 防重。
---
## 3. 业务域二:财务应付与付款
**链路总览(三环节接力,★ 标注本次变化)**
```
环节一 正式结算单页面「同步金蝶」(POST /formal-settlement/sync-kingdee)
A102-4 预检 ──▶ A102-2 保存应付单(明细行+手工合计费用项分录,汇率随单)──▶ 结算单回写 kingdee_bill_no + synced
环节二 付款管理「同步金蝶 / 批量同步」(POST /payment-application/sync-kingdee[-batch])
A102-4 硬校验(应付单已生成,逐关联结算单) ★不再反查付款计划分录
──▶ A103-2 幂等预检 ──▶ A103-1 下推(单分录,勾稽键=主结算单应付防重键;预付款走 A103-3)──▶ 回写 kingdee_bill_id/no + synced
环节三 PaymentKingdeeResultTask 定时(每10分钟,默认关)
A103-2 查状态(synced→paying→paid/closed) ──▶ A103-4 查付款处理单 ──▶ 付款记录落库(含 voucher_json)
```
### 3.1 A102-2 财务应付单保存
![正式结算单页面(2026-09-29):应付 / 应收 Tab 同页,勾选一条单据后点工具栏「同步金蝶」——payable 路由 A102-2、receivable 路由 A302-2(4.1),成功后回写结算单 kingdee_bill_no 并置 synced](./2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版).assets/bfi-02-formal-settlement.png)
- **触发**:`POST /formal-settlement/sync-kingdee`(FormalSettlementController:209),结算类型=`payable` 路由(`receivable` 路由 A302-2 财务应收单保存,同一入口)。无定时。
- **顺序**:校验 approved+应付类型 → synced 幂等返回 → 组防重键 → **A102-4 预检** → 未生成才 A102-2 保存(`KingdeeApBillSyncSupport.pushApBill`)→ 回写。单笔单事务。
- **参数来源**:
- 防重键 `正式结算单号_结算明细单据号…`;srcType=`TMS财务应付单`;billtype=`服务采购应付`→`ApFin_service_BT_S`;
- org/payorg/币种/往来户/往来类型:见 1.7;**exchangerate/exratedate=结算单页面维护值(未维护不送)** ★新增;
- purmode=CASH、款项性质=2001、isincludetax/ispricetotal=true;**settlementtype_number(结算方式)不在应付单送**(仅 A103-1 分录需要);
- **分录(★新口径)**:`resolveEntryGroups` = 结算明细行(每行一条)+ **手工结算合计费用项**(`blade_formal_settlement_summary_fee.manualFlag=1`,每行一条;系统自动汇总行不送,防与明细重复);
- `awrp_entry_code`:明细行=结算明细单据号;合计费用项行=`结算单号-费用类型-费用项`拼接;
- 明细行带运输货物(货物名称)/车号;数量=运输量(兜底 1,合计行默认 1);单价=结算金额/数量反算;
- ⚠️ 调试组合块强制物料/计量单位(见 1.7);⚠️ 项目编码仍传测试值 `999`;
- **planentity 仍不传** → 金蝶按分录自动汇总生成付款计划。
- **响应存储**:`result[0].number` → `blade_formal_settlement.kingdee_bill_no`、`kingdee_sync_status='synced'`;id 不落库。失败抛异常整体回滚保持 unsynced(**不落 failed**)。
- **依赖**:本地数据齐备即可;**是 A103-1 应付下推生成付款申请与 A303-2 收票单批量保存的硬前置**。
- **幂等**:A102-4 按防重键预检,已存在直接返回已有 billno(alreadyExist=true)。
### 3.2 A102-4 财务应付单查询(★用途缩减)
- **触发**:纯内部步骤,无独立入口。**三个用途**(原四个,反查已移除):① A102-2 推送前幂等预检;② A103-1 推送前「应付单已生成」硬校验(逐关联结算单,未生成报「请先在正式付款单页面同步金蝶」);③ 批量同步整批预热(`preloadApBillExistNos`,未命中编码补空串标记避免重复远程查询)。
- ~~原用途④「反查付款计划分录 awrp_entry_code_p」已随 09-24 重构移除~~:A103-1 的勾稽键改为本地组装(见 3.3)。
- **参数/存储**:`awrp_src_code`(多值,50 键/批、页大小 200)+ `awrp_src_sys`;结果不落库,仅入 `apBillNoCache`。
- **幂等**:本身即防重手段。
### 3.3 A103-1 付款申请保存(应付下推)/ A103-3 预付款直推(★大改)
![付款管理页面(2026-09-29):「批量同步」= /payment-application/sync-kingdee[-batch](A102-4 硬校验 → A103-2 幂等预检 → A103-1 下推,预付款走 A103-3);「同步付款结果」手动触发 A103-2 / A103-4 状态与付款记录回写](./2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版).assets/bfi-03-payment-application.png)
- **触发**:`POST /payment-application/sync-kingdee`(单笔)与 `/sync-kingdee-batch`(批量)。**预付款**(project_advance/progress_advance)走 A103-3 直推。
- **调用顺序**(`doSyncKingdee`,单据级事务):
1. 锁单 + approved 校验 → 状态门禁(synced/paying/paid/closed 幂等跳过);
2. 结算类型路由 → **A102-4 硬校验**(关联的**每张**正式结算单应付单必须已生成,按本地组装的防重键逐单预检);
3. **A103-2 幂等预检**(`reconcileExistPayApply`:正式路径统一先预检;预付款路径仅 failed 恢复时预检——查到即回填跳过,防「远端已落单本地回滚」重推);
4. **A103-1 下推**(单分录)/ **A103-3 保存** → 回写。批处理:整批一次 A102-4 预热 → 逐单自代理独立事务 → 单条失败仅回滚自身、落 `failed`、不中断整批。
- **A103-1 付款申请保存(应付下推)参数(★新口径)**:
- 防重键 `awrp_src_code`=`paymentNo`;`applydate`=申请日期(空取当天);`exratetable_number`=配置 `01`(注释保留,实际不送——按应付单自动带出);`awrp_textfield1`=付款方式字典标签(非必传);
- **分录固定一条**:`e_paymenttype_number`=201(正式付款);**`e_settlementtype_number` 按付款方式映射(★新增):汇票付款→`012-02`(应付票据电子银行承兑汇票),其余→`02`(银行转账)**;
- **`awrp_finapbill_entryid` = 主结算单应付单防重键(★本地组装 `settlementApBillSrcCode`,不再 A102-4 反查 `awrp_entry_code_p`)**——主结算单=付款申请 `settlementId`,聚合多结算单取关联结算明细首个结算单 id;⚠️ 支撑类注释仍写「A102-4 反查、禁止自行拼接」,**注释与代码不一致,金蝶侧对该值的校验口径需联调确认**(见 11.2-#4);
- `awrp_entry_code`=`paymentNo-1`;`e_assacct`=付款申请 `bankAccount`(收款账号);**`e_bebank_number`=客商收款账户联行号(★`blade_customer_receipt_account`,未维护报错拦截)**;
- `e_applyamount`=申请付款金额(2 位 HALF_UP);project 仍传 `999`。
- **A103-3 预付款申请保存参数**:单分录 `paymentNo-1`;`e_paymenttype_number` 按付款类型(正式=201/预付);`e_assacct`=bankAccount;**`e_bebank_number`=`entity.getBankName()`**(⚠️ 语义存疑:往来银行.编码取的是名称字段,见 11.2-#5);`awrp_textfield1`=付款方式标签;汇率表/汇率日期=申请日期;**project=`entity.projectId`→项目档案 `projectCode`(真实编码,无则 999)** ★改善。
- **响应存储**:`requireBfiBillIdAndFillNo`:`result[0].id` → `kingdee_bill_id`;`result[0].number` → `kingdee_bill_no`(推送即时值,后续被 A103-2 回写覆盖校准);`kingdee_status='synced'`。异常:事务回滚后独立事务 `markKingdeeSyncFailed` → `failed`。
- **依赖**:硬前置 = A102-2 已生成(A102-4 校验)。失败重推断点续传:A102-4 命中跳过 → A103-2 命中直接回填。
- **幂等**:三层——本地状态门禁、A102-4 应付预检、A103-2 按 paymentNo 查重。
### 3.4 A103-2 付款申请单查询(状态回写 + 推送预检)
- **触发**:① 定时 `PaymentKingdeeResultTask.syncKingdeePaymentResult`(默认 `0 */10 * * * ?`,开关默认 false);② 手动 `POST /payment-application/sync-kingdee-result`;③ A103-1/A103-3 推送链内部幂等预检。
- **顺序**:查待回写(approved 且 status ∈ {synced, paying, **failed**})→ paymentNo 去重按 50/批查询 → 同一 srcCode 取 `modifytime` 最新 → 逐单独立事务回写。
- **响应存储**:`billno`→kingdee_bill_no、`id`→kingdee_bill_id;状态映射:`Alreadypay`→**paid**(终)> `billstatus=D`→**closed**(终)> `Inpayment`→**paying** > 其余→synced;`entry[].e_paidamt` 汇总→`paid_amount`(缺失且 paid 时兜底=申请金额);paid 联动履约(写 `blade_pre_settlement_advance` 与 `blade_formal_settlement_payment`);**failed 纳入勾稽纠偏**:查到即视为上次推送实际成功,纠正为 synced。
- **依赖/幂等**:按防重键查询,无硬前置;主键更新天然幂等;终态不再轮询;查无此单状态不变。
### 3.5 A103-4 付款处理查询(付款记录回写)
- **触发**:定时子流程 + 手动端点。**仅查询,不推送**。
- **顺序**:候选 = 本轮 A103-2 查到 `paystatus≠Unpaid` 的单 + 已 paid 但无真实付款记录的单(补偿);按付款组织编码分组逐组查询;按源单勾稽匹配(`sourcebillid`==kingdee_bill_id 优先,`sourcebillnumber`==kingdee_bill_no 兜底)。
- **参数**:`org_number` 优先取 A103-2 返回 `payorg_number`,兜底按合同所属组织解析;业务日期窗口=最近 1 个月;页大小 200 翻页。
- **响应存储**:upsert `blade_payment_application_record`——`billno`→payment_no+kingdee_bill_no;`actpayamt`→paid_amount;`paydate`→paid_date;**整单 JSON→`voucher_json`**(凭证号/银行流水/电子回单留档);清理 MOCK-PAY- 历史模拟记录。
- **依赖/幂等**:勾稽需已知 kingdee_bill_id → 必然晚于推送链;付款组织解析失败仅 warn 跳过下轮重试;按付款处理单号判重更新。
---
## 4. 业务域三:财务应收与开票
**链路总览**
```
环节一 正式结算单页面「同步金蝶」(应收类型)
A302-4 预检 ──▶ A302-2 保存应收单(明细行+手工合计费用项分录,汇率随单)──▶ 结算单回写 kingdee_bill_no + synced
环节二 开票申请「同步金蝶」
A302-4 硬校验(应收单已生成) ──▶ A303-4 批量 billno 幂等预检 ──▶ A303-1 按发票张次逐张新增 ──▶ 张次/申请单回写单号 + synced
环节三 定时回写(PaymentKingdeeResultTask 串行第 2/3 步)
A302-4 勾稽纠偏(failed→synced / 补单号)
A303-5 开票发票查询(serialNo 按 application.kingdee_bill_no 逗号拆分)──▶ 发票落库 ──▶ invoiced(终态)
```
> 推送链路**无批量端点**;查询侧为批量(防重键 50/批 + 自动翻页 200/页)。
### 4.1 A302-2 财务应收单保存
- **触发**:`POST /formal-settlement/sync-kingdee`,`settlementType=receivable` 路由 → `KingdeeArBillSyncSupport.pushArBill`。用户动作,无定时。
- **顺序**:防重键组装 → **A302-4 幂等预检**(命中即返回已有单号跳过保存)→ 结算单维度取数(合同/组织/币种/客商)→ 组装 `BfiArBillSaveDTO` → 保存 → 回写。
- **参数**:防重键同应付口径;srcType=`TMS财务应收单`;billtype=`服务销售应收`→`arfin_sersal_BT_S`;`bizdate`=同步当日;**exchangerate/exratedate=结算单页面维护值** ★新增;`asstacttype` 固定 `bd_customer`;`asstact_number`=结算单付款方 payer_name→客商编码;paymode=CASH、payproperty=1001;
**分录(★新口径,与 A102-2 完全同源)**:结算明细行 + 手工合计费用项各一条,`awrp_entry_code` 同应付口径;调试组合块同在;项目 `999`。
- **响应存储**:`result[0].number` → `blade_formal_settlement.kingdee_bill_no`、`synced`;id 不落库;失败回滚保持 unsynced(**不落 failed**)。
- **依赖/幂等**:是 A303-1 开票申请单新增的硬前置;A302-4 预检防重推。
### 4.2 A302-4 财务应收单查询
- **触发**:① A302-2 推送前幂等预检;② **定时勾稽纠偏**(`syncKingdeeArResult`:轮询 approved+receivable+status ∈ {synced, **failed**},批量 50 键/批:查到补缺失 kingdee_bill_no、failed 纠偏为 synced;查不到不变);③ **A303-1 推送前置硬校验**(任一关联结算单应收单未生成即拦截)。
- **参数/存储**:`awrp_src_code`(多值)+ `awrp_src_sys`;落库仅限勾稽纠偏补号/状态纠正。
- **依赖/幂等**:A302-2 财务应收单保存 →(A302-4 财务应收单查询)→ A303-1 开票申请单新增 硬顺序。
### 4.3 A303-4 开票申请单查询(★批量幂等预检)
- **触发**:仅一处——A303-1 推送前预检。
- **★变化**:按**全部候选 billno 列表**(单张=application_no;多张=application_no-1/2/3…)一次性查询(pageSize=max(张数,10)),命中的张次幂等回填跳过,未命中的张次重新推送——支持「部分张次已生成」的断链重推。
- **参数/存储**:查询键 `billno`(多值);命中结果仅用于跳过与回填 sheet.kingdee_bill_no。
### 4.4 A303-1 开票申请单新增(★按张次拆分重写)
![开票管理页面(2026-09-29):工具栏「批量同步」= /invoice-application/sync-kingdee,A302-4 硬校验 → A303-4 按全部候选 billno 批量幂等预检 → A303-1 按发票张次逐张新增;列表支持按金蝶单据状态筛选](./2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版).assets/bfi-04-invoice-application.png)
- **触发**:用户动作 `POST /invoice-application/sync-kingdee` → `syncKingdee`。单申请一次。
- **调用顺序**:校验 approved → synced/invoiced 幂等返回 → 关联结算单非空 → **A302-4 硬校验** → **A303-4 批量预检** → 销/购方取数校验 → **逐张发票张次**组装保存(`requireBfiSaveSuccess` 校验每张)→ 批量回写张次与申请单。
- **参数**:
- **张次(★新表 `blade_invoice_application_sheet`)**:每张金蝶开票申请单对应一个张次;`billno`=单张取 `application_no`(兼容存量)、多张取 `application_no-张次序号`;价税合计=本张 `invoice_amount`;分录=本张商品行(`blade_invoice_application_line` 按 `invoice_sheet_id` 分组、lineNo 升序),`awrp_entry_code`=商品行 id;
- 头字段(沿用):`orgid_number`=承办部门 dept_code;`billdate`=application_date;`invoicetype`:数电专票 `08xdp` / 数电普票 `10xdp`;`billproperties=1`、`taxationstyle=0`、`buyeremail`=department_emails;购方=申请单受票方快照列;销方=salername/开票单位税号(客商档案)/地址电话/银行账号;`customnameid_number`=receiver_customer_id→客商编码;`hsbz`:普票=1 否则 0;`awrp_project_number`=999;systemsource=WLTMS。
- **响应存储(★)**:每张 `sheet.kingdee_bill_no`=金蝶返回 number(幂等命中的张次回填候选 billno);**`entity.kingdee_bill_no`=全部张次单号逗号拼接**;`kingdee_status='synced'`;记操作历史。
- **依赖/幂等**:硬前置 = A302-2 应收单已生成;幂等 = billno 候选键 + A303-4 批量预检回填(**未生成张次可断点续推**)。
### 4.5 A303-5 开票发票查询(结果回写)
- **触发**:定时(PaymentKingdeeResultTask 第 3 步)+ 手动 `POST /invoice-application/sync-kingdee-invoice-result`。
- **顺序**:待回写 = approved + synced + 有 kingdee_bill_no → **按 `application.kingdee_bill_no` 逗号拆分逐个 serialNo**(★适配张次多单号)→ 按销方税号分组(开票单位→客商档案税号,解析不到跳过 warn)→ 组内 ≤50/批调 A303-5 开票发票查询(票易通 openApi,Base64 包裹)。
- **响应存储**:`billNo + invoiceList[]` → `blade_invoice_application_invoice`(serial_no、invoice_no、invoice_date、invoice_amount、raw_json);幂等 = 批量预查已有发票号 + 唯一键 `uk_application_invoice(tenant_id, application_id, invoice_no)`;有新增发票 → `invoiced` 终态。**发票结果不反写结算单**。
- **依赖/幂等**:必须先有 A303-1 开票申请单新增产生的单号;唯一键防重;批次失败仅计数不中断。
---
## 5. 业务域四:收票与金蝶票池
**架构现状**:票池浏览**实时回源 A801-1 全票池发票查询**(全量定时同步任务已退役),镜像表 `blade_kingdee_invoice_pool` 降级为**登记存根**;推送链 A303-2 收票单批量保存 → A302-6 收票指定应付 人工批量按钮触发;A801-3 收票单查询 承担推送预检 + 定时回写。
### 5.1 A801-1 全票池发票查询(选票 / 保存重查 / 补票种)
- **触发**:① 选票弹窗:收票登记 `GET /invoice-receipt/invoice-pool`、结算单认领 `GET /formal-settlement/invoice-pool`;② 收票登记**保存重查**(`persistMirrorFromRemote`,按票号精确单查,作为「保存重查闸门」);③ 推送时镜像缺票种编码的按票号窄查补填。**无定时任务**。
- **顺序**:游标分页(`PoolScanCursor`)→ 首拉 size+1 探测 → 不足按契约上限 500 行/次补拉,单请求最多 3 次远程调用 → 短页即耗尽。
- **参数**:`invoiceStatus`="0"(正常票)、`expenseStatus`="1"(未用,后端固定下推);票号/销购方税号/票种/日期来自前端 DTO(日期强校验)。
- **行过滤**:票号非空 + `amountUseStatus="0"` + `availableAmount>0`;按票号查询时 trim 全等防御(防远程模糊匹配假命中)。本地打标 registered/claimed 纯展示。
- **响应存储**:浏览**不落库**;落库仅保存收票登记(按票号精确单查→可用性复核→`(tenant_id, invoice_no)` FOR UPDATE + upsert 存根)与推送补查。镜像列含 `invoice_type_code`(票易通原始编码,A303-2 取数)、税率(小数→百分数)、票面四要素、`check_status`、`attachments_json`、`items_json`(仅存档,**不作分录取数**)。
- **依赖/幂等**:仅 BFI 网关;**金蝶不可用则选票与收票保存整体不可用**(无镜像读降级);保存重查闸门拦截红冲/作废/被占用竞态;一票一登记(`assertInvoiceUnused`)。
### 5.2 A303-2 收票单批量保存 + A302-6 指定应付(推送链)
![收票管理页面(2026-09-29):工具栏「批量同步」= /invoice-receipt/push-kingdee,链路 A102-4 应付预检 → A801-3 幂等预检(防重键=收票登记 id)→ A303-2 收票单保存 → A302-6 指定应付;选票 / 保存重查经 A801-1 实时回源票池](./2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版).assets/bfi-05-invoice-receipt.png)
- **触发**:`POST /invoice-receipt/push-kingdee`。单笔失败不中断整批,逐单报因。
- **调用顺序**(`pushOneReceivedInvoice`):
1. 批量取数防 N+1(分摊/镜像/结算单/明细一次取回);
2. 组装推送计划:approved、状态≠assigned、已绑定镜像、`checkStatus=2`(查验通过)、**恰好分摊 1 张审批通过应付结算单(一票一应付)**、有结算明细;不满足写 `blockedReason` 不改状态;
3. **A102-4 预检**:关联结算单应付单必须已生成,缺则拦截「请先同步金蝶应付单」;
4. **A801-3 幂等预检**:按镜像购方名称分组、窗口 30 天翻页,本地按 `awrpSrcSys+awrpSrcCode`(=**收票登记 id**,★243d5be 修复)匹配;命中且状态正常 → 跳过 A303-2 直接续推指定(断点恢复);命中但发票状态异常(作废 2/红冲 3/部分红冲 7/全部红冲 8)→ 置 failed 报人工;
5. 镜像缺票种 → A801-1 按票号窄查回填;
6. **A303-2 保存** → 成功先落单号 + `pushed`;
7. **A302-6 指定应付**(★入参改 receipt.getId())→ 成功 `assigned`(终态)+ 记流水;失败 `failed` + 原因。unassign 不自动执行。
- **参数**:
- A303-2 收票单批量保存:`awrp_src_code`=收票登记 id;srcType=`TMS收票登记`;`invoiceno`=receipt.invoice_no;`receivedate`=推送当日;`issuedate`=镜像 invoice_date;`invoicetypef7_number`=镜像 invoice_type_code(缺则报错);销购方四要素=镜像票面;org/币种/`receivablessupp_number`(合同乙方→客商编码)复用公共取数;**分录与 A102-2 同源同口径(★结算明细行+手工合计费用项)**;⚠️ 调试组合块**含随机兜底**(未命中组合随机选一个物料,见 11.2-#6);
- A302-6 收票指定应付:`ap_invoice` 引用 `awrp_src_code`=收票登记 id(★修复);`ap_finapbill` 引用 `apBillSrcCode`=正式结算单号_明细单据号(与 A102-2 同源)。
- **响应存储**:`result[0].number` → `blade_invoice_receipt.kingdee_received_invoice_no`;状态机 `unsynced→pushed→assigned/failed` → `kingdee_push_status`;失败原因→`kingdee_push_msg`(截 500 字);终态/失败写 `blade_invoice_receipt_record` 流水。`blade_invoice_receipt.kingdee_bill_no / kingdee_status` 两列已删除(20260923),金蝶状态唯一来源=`kingdee_push_status`。
- **依赖/幂等**:A302-6 依赖 A303-2 同键收票单已生成、且依赖 A102-2 应付单(硬预检);A801-3 预检命中跳过保存;pushed 断点由定时补偿重发 A302-6;assigned 拒绝重推;pushed/assigned 状态**禁止本地作废**(voidBill)。
### 5.3 A801-3 收票单查询(预检 + 定时回写)
- **触发**:① 推送链内部预检(上);② 定时 `PaymentKingdeeResultTask.syncKingdeeReceivedInvoiceResult`(独立开关默认 false,cron 默认每 10 分钟);③ 手动 `POST /invoice-receipt/sync-kingdee-result`。
- **回写顺序**:轮询 `kingdee_push_status ∈ (pushed, assigned)` 且非作废、已绑定镜像 → 按 `receiver_name`(接口必填购方名称)分组 → 窗口 30 天翻页 → 逐单处置:发票状态异常→`failed`+「已回收推送状态」;`pushed`→**补偿重发 A302-6**(★防重键已修复为登记 id)→ assigned;`assigned` 缺单号→仅补登单号;**窗口内查不到不改状态**。
- **⚠️ 疑似缺陷残留(★未修复)**:定时回写**仍按发票号码**组装查询集与匹配(`entry.getValue().stream().map(InvoiceReceipt::getInvoiceNo)` + `matched.get(receipt.getInvoiceNo())`),而 A303-2 落库的 `awrp_src_code` 是**收票登记 id** → 回写将**永远匹配不到 TMS 自推单据**(静默跳过,异常回收/指定补偿/单号补登全部失效)。09-23 的 243d5be 提交只修复了**推送预检与 A302-6** 路径,回写路径漏改。见 11.2-#1。
### 5.4 选票 / 认领流(是否调金蝶对照)
| 流 | 入口 | 金蝶调用 | 本地存储 |
|---|---|---|---|
| 收票登记 | `POST /invoice-receipt/save` | 保存重查 A801-1(闸门) | 镜像存根 upsert + 绑定 + 分摊 + 流水 |
| 结算单认领(按票) | `POST /formal-settlement/claim-invoices`;查票 `GET /formal-settlement/invoice-pool` | 查票实时 A801-1;**确认认领时不调金蝶** | `blade_formal_settlement_invoice` + 联动付款申请 matched;⚠️ **仍无保存重查闸门**(前端传票面直接落库,09-23 提案至今未实现) |
| 结算单收款认领(收票流水) | ReceiptFlowController / ReceiptClaimRecordController | **不调金蝶**(镜像流水由外部 `sync` 推入) | `blade_kingdee_receipt_flow`、`blade_receipt_claim(_settlement)`、`blade_receipt_flow_record` |
---
## 6. 业务域五:收款认领(A301-2 收款认领公告查询 / A301-3 收款认领结果推送 / A301-1 收款单查询)★新增
**定位**:图06「回款认领」采用**业务先行认领(场景二)**:金蝶司库收到银行流水完成第一轮自动认领(到客户维度),TMS 二次认领到结算单(应收单)维度后回推金蝶生成收款单。**三个接口本期全部接线,均为手动触发,无定时任务**(定时开关 `thirdParty.bfi.receiptClaimResult.*` 预留未接)。专项文档:`2026-09-24-收款认领金蝶全链路业务流程….md`。
```
阶段一 POST /receipt-flow/sync(手动同步流水按钮)
A301-2 认领公告查询(claimusers=当前登录用户真实姓名)
→ BfiUtil.queryAllPages 全量翻页 → 按 detail_serial_no 幂等 upsert blade_kingdee_receipt_flow
阶段二 POST /receipt-flow/claim(页面认领)
本地认领落库(blade_receipt_claim + 分摊表)+ A301-3 认领结果推送 同一事务
→ 成功回写金蝶收款单号 + kingdee_bill_status=pushed;失败抛异常整体回滚
阶段三 POST /receipt-claim-record/query-kingdee-result(手动回写)
A301-1 收款单查询(近 30 天窗口全量翻页)→ sourcebillnumber==awrp_src_code 匹配 → 回写单号与状态
```
### 6.1 A301-2 收款认领公告查询(公告同步)
![收款流水页面(2026-09-29):「手动同步流水」= POST /receipt-flow/sync(A301-2 公告查询,claimusers=当前用户真实姓名,全量翻页幂等落库);勾选流水后「批量认领」在认领事务内同步推送 A301-3](./2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版).assets/bfi-06-receipt-flow.png)
- **触发**:仅手动 `POST /receipt-flow/sync`(按钮权限 `receipt_flow_sync`)。前端已去 mock,直接调后端。
- **顺序**:确定参与认领用户(`UserCache.getUserRealName`,为空拦截)→ 全量翻页(pageSize 200,50 页保护,时间窗口从 Nacos 配置 `thirdParty.bfi.claimAnnounce.months` 读取,默认 6 个月)→ 幂等落库 → 操作留痕(`blade_receipt_flow_record` 记 sync)。
- **参数**:`claimusers`=当前用户真实姓名(**前提:金蝶公告的 claimusers 须与 TMS 用户姓名一致**,否则查不到公告)。
- **响应存储**:upsert `blade_kingdee_receipt_flow`,幂等键 `detail_serial_no`(取 detailid,缺省回退 tradedetailno/bankdetailno;重复明细流水号拦截);落库 `bill_id`(**认领公告 id,A301-3 收款认领结果推送必填**)、billno→receipt_notice_no、reamount、对方户名/账号/开户行、摘要、交易时间、unclaimed_amount、fee、payee_name、claimstatus→kingdee_claim_status、claimme。**公告侧已认领金额/状态与本地 claimed_amount/claim_status 互不覆盖**。
- **依赖/幂等**:独立;唯一索引 `(tenant_id, detail_serial_no)`。
### 6.2 A301-3 收款认领结果推送
- **触发**:页面认领 `POST /receipt-flow/claim`(流水详情/批量认领页选择审批通过的应收正式结算单按比例分摊)。
- **顺序**:本地校验(锁行、分摊金额上限、同一项目/组织/收付款方)→ 本地落库(claim + 分摊 + 流水 claimed_amount/claim_status 刷新 + 结算单收款汇总)→ 生成 `awrp_src_code = SKCL+yyyyMMddHHmmss+认领id尾号6位` → **A301-3 推送**(同事务)→ 成功回写 `kingdee_bill_no`(result[0].number)+ `kingdee_bill_status=pushed`;失败抛 `ServiceException` 整体回滚(无残留)。
- **参数**:`billid`=流水 `bill_id`(**必填**,缺失拦截「认领公告id缺失,请先同步金蝶收款流水」);`awrp_src_sys`=WLTMS;`awrp_src_code`=SKCL 码;`recpaytype_number`=常量(示例 101,**待金蝶确认**);`paymenttype`=bd_customer(**待确认**);`recpayer`=流水 payer_id(**待确认**:金蝶付款人 master id,联调若强校验需按名称反查 A902-1 补齐);分录 `entryentity[]`=各结算单分摊行:`e_receivableamt`=分摊金额、`e_corebilltype`=`ar_finarbill`、`e_corebillno`=分摊结算单 `kingdee_bill_no`(缺失不传)、`seq`=行号、`e_receivingtype_number` 同 recpaytype_number(待确认)。
- **依赖**:分摊结算单须审批通过、未作废、且已完成 A302-2 同步(有 kingdee_bill_no)以便分录携带应收单号。
- **幂等**:`awrp_src_sys+awrp_src_code` 为金蝶候选键;⚠️ 推送成功但本地回滚后重推会**重新生成 awrp_src_code** 产生新单(联调期以推送失败回滚为主路径规避)。
### 6.3 A301-1 收款单查询(认领结果回写)
![认领记录页面(2026-09-29):「查询金蝶认领结果」= POST /receipt-claim-record/query-kingdee-result,手动调 A301-1 收款单查询(今天回溯 30 天),按 sourcebillnumber == awrp_src_code(SKCL 码)匹配回写单号与状态](./2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版).assets/bfi-07-receipt-claim-record.png)
- **触发**:仅手动 `POST /receipt-claim-record/query-kingdee-result`。**仅查询,不推送**。
- **顺序**:轮询 `claim_status=claimed` 且 `awrp_src_code` 非空且 `kingdee_bill_status ∈ (pushed, approved)` → A301-1 按业务日期**今天回溯 30 天**(≤1 个月约束)全量翻页(pageSize 200)→ 本地匹配 `sourcebillnumber == awrp_src_code` → 逐条回写。
- **响应存储**:`kingdee_bill_no`=收款单 billno(首条命中后仅推进状态);`kingdee_bill_status` 映射:A 暂存/B 已提交→pushed;C 已审核→approved;**D 已收款→received(终态)**;其他/G 退单→failed。窗口内查不到不改状态(金蝶回单延迟兜底)。**不改本地认领金额/分摊**。
- **依赖/幂等**:依赖 A301-3 产生的 awrp_src_code;主键更新幂等。
---
## 7. 业务域六:公共取数(A801-4 汇率)★新启用
- **触发**:前端结算页面按「结算币种 + 汇率日期」调用 `GET /bfi/exchange-rate?currencyCode=&effectdate=`(BfiExchangeRateController),用于正式结算单页面维护汇率/汇率日期。手动、单次。
- **顺序**:按日期查 A801-4 全量汇率(当日默认,页大小固定 10)→ 本地按 `orgcur_number` 匹配币种 → 返回汇率 VO。
- **响应**:不落库(汇率/汇率日期随结算单提交落 `blade_formal_settlement.exchange_rate / exchange_rate_date`);查不到币种/日期返回 R.fail。
- **下游消费**:A102-2 / A302-2 保存时传 `exchangerate`(结算汇率)与 `exratedate`(汇率日期),未维护不传;A103-1 下推汇率相关字段不传(按应付单自动带出)。
- **与方案文档对照**:方案 V1.2「汇率在金蝶维护,TMS 结算时通过接口获取并记录在结算单据上,期末调汇全部在金蝶处理」——**代码已落地**。
---
## 8. 全景:接口先后顺序与依赖关系
### 8.1 链路依赖图
```
┌────────────────── 客商档案(全局前提,软依赖) ──────────────────┐
│ A902-2 客户保存(审批通过自动/人工/定时重推) │
│ A902-1 客户查询(前端回填/推送前复查/定时复查) │
│ ⇒ 本地 customer_code(其他链路只查本地表,不校验金蝶侧状态) │
└───────────────────────────┬───────────────────────────────────┘
▼
【应付主线】
正式结算单(应付) ─A102-4 预检─▶ A102-2 保存应付单 ─▶ 结算单.kingdee_bill_no + synced
│ (汇率 exchangerate/exratedate ← A801-4 页面取数)
│ A102-4 硬校验(每张关联结算单) │ A102-4 预检(收票推送硬前置)
▼ ▼
付款申请 ─A103-2 预检─▶ A103-1 下推(单分录;勾稽键=主结算单应付防重键,本地组装)
(预付款→A103-3 直推) ─▶ 付款申请.kingdee_bill_id/no + synced
│
定时:A103-2 状态回写(synced→paying→paid/closed, failed勾稽纠偏)
▼
A103-4 付款处理查询 ─▶ blade_payment_application_record(+voucher_json)
【应收主线】
正式结算单(应收) ─A302-4 预检─▶ A302-2 保存应收单 ─▶ 结算单.kingdee_bill_no + synced
│ A302-4 硬校验
▼
开票申请 ─A303-4 批量billno预检─▶ A303-1 逐张次新增 ─▶ 张次.kingdee_bill_no + 申请单.kingdee_bill_no(逗号拼接) + synced
│
定时:A302-4 勾稽纠偏 + A303-5(serialNo 按逗号拆分)─▶ 发票落库 ─▶ invoiced(终态)
【收票主线】(与应收主线无调用耦合,与应付主线强耦合)
金蝶票池 A801-1(实时选票 / 保存重查闸门 / 补票种) ─▶ 镜像存根 blade_kingdee_invoice_pool
│ 保存收票登记(一票一登记)
▼
收票登记 ─A102-4 预检(应付已生成) + A801-3 预检(防重键=登记id)─▶ A303-2 收票单保存 ─▶ pushed
▼
A302-6 指定应付(引用=登记id)─▶ assigned(终态)
│
定时:A801-3 回写(⚠️ 仍按发票号匹配,疑缺陷残留)
【收款认领主线】★新增(全部手动触发,无定时)
金蝶第一轮自动认领(到客户)─▶ A301-2 公告同步(claimusers=用户姓名)─▶ blade_kingdee_receipt_flow
│ 页面二次认领(分摊到应收结算单)
▼
认领落库 + A301-3 推送(billid=公告id;src_code=SKCL码;分录=e_corebillno=结算单应收单号)─▶ pushed
▼
手动:A301-1 收款单查询(sourcebillnumber==SKCL码)─▶ approved/received(终)
【公共取数】
结算页面 ─▶ A801-4 汇率查询 ─▶ 结算单.exchange_rate/exchange_rate_date ─▶ 随 A102-2/A302-2 推送
```
### 8.2 硬顺序规则(代码强制)
| # | 顺序规则 | 强制方式 |
|---|---|---|
| 1 | 客商档案先于一切推送:本地 `customer_code` 必须存在 | 查不到直接报错拦截;**金蝶侧是否已审核不校验**(软依赖) |
| 2 | A102-2(应付单)先于 A103-1(付款下推) | A102-4 硬校验(逐关联结算单);~~A102-4 反查 awrp_entry_code_p~~ **已移除** |
| 3 | A102-2(应付单)先于 A303-2/A302-6(收票推送) | A102-4 硬预检 |
| 4 | A302-2(应收单)先于 A303-1(开票申请) | A302-4 硬校验 |
| 5 | A303-2 收票单批量保存 先于 A302-6 收票指定应付 | 同一次推送内串行;assigned 依赖 pushed |
| 6 | A302-2(应收单)先于 A301-3(认领推送分录携带应收单号) | 分录 e_corebillno 缺失不传(软依赖),联调前提建议先同步应收 |
| 7 | 推送类先于回写类:A103-2 付款申请单查询/A103-4 付款处理查询、A302-4 财务应收单查询 勾稽、A303-5 开票发票查询、A801-3 收票单查询、A301-1 收款单查询 都依赖推送产生的键/单号 | 查询键(paymentNo / srcCode / billNo / serialNo / SKCL码)由推送链写入本地 |
| 8 | A801-4 汇率查询先于 A102-2 财务应付单保存/A302-2 财务应收单保存(如需携带汇率) | 页面维护动作,非代码强制(未维护不传) |
### 8.3 断链恢复语义(各链路通用模式)
- **本地失败、远端未落单**:状态保持 unsynced(应付/应收/开票)或 failed(付款申请/收票),可重推。
- **远端成功、本地回滚**:重推时预检命中幂等回填——应付/应收走 A102-4 财务应付单查询/A302-4 财务应收单查询 防重键、付款申请走 A103-2 付款申请单查询 按 paymentNo、开票走 A303-4 开票申请单查询 按候选 billno(张次级)、收票走 A801-3 收票单查询 按 srcCode(登记 id)、认领走 A301-1 收款单查询 按 sourcebillnumber(⚠️ A301-3 重推会换新码,见 6.2)。
- **推送成功后状态滞后**:定时任务回写(A103-2 付款申请单查询→paid/closed、A303-5 开票发票查询→invoiced、A801-3 收票单查询→assigned 补偿;A301-1 收款单查询 暂为手动)。
- **批量语义**:整批预热 → 逐单独立事务 → 单条失败不中断整批(付款批量、收票推送、定时回写均如此)。
---
## 9. 状态机汇总
| 表.字段 | 状态集 | 迁移 | 写入方 |
|---|---|---|---|
| `blade_formal_settlement.kingdee_sync_status` | unsynced / synced | unsynced→(A102-2 财务应付单保存 或 A302-2 财务应收单保存 成功)→synced;失败回滚保持 unsynced;定时勾稽可纠偏 failed→synced(防御性,代码无 failed 写入) | FormalSettlementServiceImpl |
| `blade_payment_application.kingdee_status` | unsynced / synced / paying / paid(终) / closed(终) / failed | unsynced→(A103-1 应付下推生成付款申请/A103-3 预付款申请保存)→synced→(Inpayment)→paying→(Alreadypay)→paid;billstatus=D→closed;推送异常→failed;failed 可被 A103-2 付款申请单查询 勾稽纠偏回 synced | PaymentApplicationServiceImpl + PaymentKingdeeResultTask |
| `blade_invoice_application.kingdee_status` | unsynced / synced / invoiced(终) | unsynced→(A303-1 开票申请单新增 全部张次成功)→synced→(A303-5 开票发票查询 有新发票)→invoiced;失败回滚保持 unsynced(**无 failed 写入**) | InvoiceApplicationServiceImpl + 定时 |
| `blade_invoice_application_sheet.kingdee_bill_no` | 空 / 金蝶单号 | 张次级单号,A303-1 推送或 A303-4 幂等命中时回填 | InvoiceApplicationServiceImpl |
| `blade_invoice_receipt.kingdee_push_status` | unsynced / pushed / assigned(终) / failed | unsynced→(A303-2 收票单批量保存)→pushed→(A302-6 收票指定应付)→assigned;A302-6 失败/发票异常回收→failed;pushed 由定时补偿重发 A302-6(⚠️ 回写匹配缺陷,见 11.2) | InvoiceReceiptServiceImpl + 定时 |
| `blade_customer_archive.kingdee_status` | none / synced / failed / exist(终) | 审批通过→(A902-2 客户保存)→synced(金蝶受理 B);金蝶审核 C 后定时复查→exist;推送失败/A/B→failed | CustomerArchiveServiceImpl + CustomerKingdeeSyncTask |
| `blade_receipt_claim.kingdee_bill_status` ★新增 | none / pushed / approved / received(终) / failed / voided | none→(A301-3 收款认领结果推送)→pushed→(A301-1 收款单查询: C)→approved→(D)→received;A/B→pushed 维持;G/其他→failed;作废→voided+KDCX 冲单号(本地) | ReceiptFlowServiceImpl + ReceiptClaimRecordServiceImpl |
| `blade_kingdee_receipt_flow.claim_status` ★新增 | unclaimed / partial / claimed | 公告同步不覆盖;认领分摊→partial/claimed;全部作废→unclaimed | ReceiptFlowServiceImpl |
---
## 10. 定时任务与配置开关汇总
| 任务类 | 方法 | 覆盖接口 | cron(默认) | 开关(默认) | 手动端点 |
|---|---|---|---|---|---|
| PaymentKingdeeResultTask | syncKingdeePaymentResult | ① A103-2 付款申请单查询 + A103-4 付款处理查询 ② A302-4 财务应收单查询 勾稽 ③ A303-5 开票发票查询 | `thirdParty.bfi.payResult.cron` = `0 */10 * * * ?` | `thirdParty.bfi.payResult.syncEnabled` = **false** | `POST /payment-application/sync-kingdee-result`、`POST /formal-settlement/sync-kingdee-ar-result`、`POST /invoice-application/sync-kingdee-invoice-result` |
| PaymentKingdeeResultTask | syncKingdeeReceivedInvoiceResult | A801-3 收票单查询 回写 | `thirdParty.bfi.receivedInvoiceResult.cron` = `0 */10 * * * ?` | `thirdParty.bfi.receivedInvoiceResult.syncEnabled` = **false** | `POST /invoice-receipt/sync-kingdee-result` |
| CustomerKingdeeSyncTask | syncKingdeeCustomerData | A902-1 客户查询 复查 + A902-2 客户保存 重推 | `thirdParty.bfi.customer.cron` = `0 0 2 * * ?` | `thirdParty.bfi.customer.syncEnabled` = **false** | `POST /customer-archive/sync-kingdee` |
| (预留未接) | — | A301-1 收款单查询 认领回写 | — | `thirdParty.bfi.receiptClaimResult.*`(代码未消费) | `POST /receipt-claim-record/query-kingdee-result`(手动) |
> 所有已接线的定时开关**代码默认关闭**,需 Nacos 显式开启。⚠️ 客商定时任务仍有联调残留过滤(只处理 KS000067,见 11.1-#3),开启前必须移除。收款认领三接口**全部手动触发**(A301-2 sync、A301-3 认领、A301-1 回写),与方案文档「每天定时获取金蝶收款流水」不一致(见 11.3)。
---
## 11. 代码与三份材料差异 / 风险清单(更新版)
### 11.1 联调残留(上线前必须处理;对照 09-23 清单标注状态)
| # | 位置 | 问题 | 状态 vs 09-23 |
|---|---|---|---|
| 1 | KingdeeApBillSyncSupport:299、KingdeeArBillSyncSupport:234、KingdeeReceivedInvoiceSyncSupport:244 | `if(true)` 调试组合块强制物料/计量单位(固定 4 组合,兜底 物流配送 Item-00001080+TON) | ⚠️ **部分解决**:真实映射链路已建成(装卸/国际/国内+运输方式 → 金蝶编码;计量单位档案编码),但调试块仍做**最终覆盖** |
| 2 | KingdeeReceivedInvoiceSyncSupport 调试块 | 收票侧映射表与应付侧不一致,且**未命中组合时随机取一个物料**(`new Random().nextInt`)——同一票每次推送物料可能不同 | ⚠️ **新增风险** |
| 3 | CustomerArchiveServiceImpl:629 | 定时客商同步硬编码 `.eq(customerCode, "KS000067")`,只处理一条 | ❌ **仍在** |
| 4 | CustomerBfiConverter:141 | 客商银行分录 `bankNumber` 硬编码 "001110012009"(todo) | ❌ **仍在**(付款申请侧已改为取客商收款账户真实联行号,仅客商推送残留) |
| 5 | KingdeeApBillSyncSupport / KingdeeArBillSyncSupport / A103-1 分录 / 开票申请 | 项目编码传测试值 `999` | ⚠️ **部分解决**:A103-3 预付款已按项目档案真实编码(无项目才 999);其余仍 999 |
| 6 | KingdeeBfiConstant 遗留常量 | `BFI_AP_BILL_MATERIAL_NUMBER_TEST(SP0324)`、`BFI_AP_BILL_BANKACCOUNT_NUMBER_TEST` 等联调常量仍在(BfiProperties.receivedInvoice.materialNumber 默认值亦为 SP0324) | ⚠️ 无调用方或被覆盖,建议清理 |
### 11.2 疑似缺陷(建议人工确认)
| # | 问题 | 证据 | 状态 vs 09-23 |
|---|---|---|---|
| 1 | **A801-3 定时回写按发票号码匹配防重键,而 A303-2 落库 awrp_src_code=收票登记 id** → 回写(异常回收/指定补偿/单号补登)永远匹配不到 TMS 自推单据,静默跳过。09-23 的 243d5be 只修复了推送预检与 A302-6 路径 | InvoiceReceiptServiceImpl(syncKingdeeReceivedInvoiceResult:`map(InvoiceReceipt::getInvoiceNo)` + `matched.get(receipt.getInvoiceNo())`,而预检路径已改 `String.valueOf(receipt.getId())`) | ⚠️ **部分修复(回写路径残留)** |
| 2 | A103-1(应付下推生成付款申请)`awrp_finapbill_entryid` 实际传**主结算单应付单防重键(本地组装)**,而 PaymentApplicationServiceImpl 与 KingdeeApBillSyncSupport 注释仍写「A102-4 反查 awrp_entry_code_p,禁止各接口自行拼接」——注释与代码矛盾;金蝶侧按单据头 srcCode 勾稽付款计划是否成立需联调确认 | PaymentApplicationServiceImpl:962-963 vs :1129 注释;KingdeeApBillSyncSupport:81/:359-361 注释 | ⚠️ **新增待确认** |
| 3 | 应收/开票链路 `failed` 状态无写入路径(回滚后停留 unsynced;轮询集合中的 failed 只能人工改库产生) | FormalSettlementServiceImpl、InvoiceApplicationServiceImpl | ❌ **仍在** |
| 4 | 其他链路推送前不校验客商 `kingdee_status`,客商未推送/未审核也会带编码推单 | KingdeeApBillSyncSupport.queryCustomerCode | ❌ **仍在** |
| 5 | A103-3 预付款分录 `e_bebank_number`(往来银行.编码)取 `entity.getBankName()`(名称字段)——与 A103-1 取联行号的口径不一致,疑似取错字段 | PaymentApplicationServiceImpl:885 vs :969 | ⚠️ **新增待确认** |
| 6 | 收款认领 A301-3:推送成功但本地回滚后重推会**重新生成 awrp_src_code** 产生金蝶重复收款单(联调期以推送失败回滚规避);且 `recpaytype_number/paymenttype/recpayer/e_receivingtype_number` 等字段取值待金蝶侧确认 | ReceiptFlowServiceImpl(09-24 专项文档 §4.1/§7) | ⚠️ **新增待确认** |
### 11.3 代码与三份材料的口径差异
**vs《TMS业财融合方案.docx》(V1.2 方案)**
| 方案描述 | 代码现状 |
|---|---|
| SC-04「付款申请可分多次下推」 | A103-1 当前**单分录单次下推**(一申请一单,`paymentNo-1`);多次下推未实现 |
| SC-04 预付/尾款两模式、实付=全额−预付抵扣−已付 | 本地金额口径已实现;金蝶侧预付冲应付由金蝶自动处理(代码不传冲抵明细) |
| 附件推送(审批单据/磅单/合同签章随单推金蝶 A902 附件上传) | **未实现**:A902 未在客户端实现,各推送链路均不传附件 |
| SC-05「结算审批时同步录入进项发票…同步生成收票单并自动绑定」 | 流程拆分:收票登记独立审批 → 人工批量 `push-kingdee` 推送(非结算审批时自动) |
| SC-07「每天定时获取金蝶收款流水」 | A301-2 当前**手动按钮触发**,无定时(`receiptClaimResult.*` 开关预留未接) |
| SC-08「按发票张数生成多个金蝶开票申请单,按票面行拆分明细」 | ✅ **已实现**(张次拆分) |
| 汇率「TMS 结算时通过接口获取金蝶汇率记录在结算单据上」 | ✅ **已实现**(A801-4 汇率查询 → 结算单页面维护 → 随 A102-2 财务应付单保存/A302-2 财务应收单保存 推送) |
| 客商「TMS 按机构类别转换伙伴类型再推送」 | `customer_nature` 字典映射;分组 1101/1102 已按落库 customerKind 区分 |
**vs《TMS业财融合-集成流程图文字版与业务流程梳理》(09-14)**
| 文字版描述 | 代码现状 |
|---|---|
| 图05/第6节 票池「定时/手动拉取全票池 → 本地镜像 → 查本地镜像选票」 | 已演进:**实时回源 A801-1 全票池发票查询**(无定时拉取),镜像降级为登记存根(09-23 已按此定稿) |
| 图05 状态一览「A303-2/A302-6/A801-3 已编码未联调」 | 已联调推进:A801-3 预检与 A302-6 防重键 bug 已修(243d5be),回写仍带缺陷(11.2-#1) |
| 图06/第7节 收款认领 A301-1/A301-2/A301-3 | ✅ **已接线**(手动触发;公告 claimusers 须与 TMS 用户姓名匹配;多个 A301-3 字段待金蝶确认) |
| 第9节 汇率 A801-4「TMS 结算时获取」 | ✅ 已实现 |
| 第9节 黑名单 A801-5、A901-1 供应商查询、A913 项目、A101-1/2 合同 | ❌ 均未实现(合同/项目/风险走合同合规系统 C 类接口,本期不在 BFI 范围) |
| 第9节 附件 A902 上传 | ❌ 未实现(方案中随单附件推送金蝶暂缺) |
**vs《2026-09-23 全链路梳理》(上一版总纲)**
| 09-23 结论 | 当前状态 |
|---|---|
| A301-1/2/3「业务侧无调用」 | ❌ 已过时:收款认领链路接线(业务域五) |
| A801-4「业务侧无调用」 | ❌ 已过时:结算取汇率(业务域七) |
| A102-4 用途④「反查 awrp_entry_code_p,是 A103-1 硬前置」 | ❌ 已过时:反查移除,勾稽键本地组装(11.2-#2) |
| A103-1 分录「逐费用行一条,e_bebank_number=测试常量」 | ❌ 已过时:单分录、联行号取客商收款账户、结算方式显式映射 |
| 开票申请「billno=application_no,一张申请一张金蝶单」 | ❌ 已过时:按张次拆分多单,billno 加张次后缀,A303-4 批量预检 |
| 9.1-#5 customerKind 恒推 1102 | ✅ 已修复(字段落库) |
| 9.2-#1 A801-3 回写匹配缺陷 | ⚠️ 部分修复(预检/A302-6 已修,回写残留) |
| 9.1-#1 物料 SP0324/TON 强制、#2 项目 999、#3 银行硬编码 | ⚠️ 演进(真实映射+调试覆盖并存;见 11.1) |
| 9.1-#3 付款 e_bebank_number 测试值 | ✅ 已修复(联行号);客商 converter 残留 |
### 11.4 其他已知注意点
- 客户端常量两处并存(`bfi/constant/BfiConstant` 与 `transport/constant/KingdeeBfiConstant`),语义重复,改动需双侧核对。
- `BfiBillSaveResultVO.successCount/failCount` 为 String 类型。
- token 缓存 `setIfAbsent`:已有缓存时强制取的新令牌不会覆盖旧缓存。
- 操作码与 URL 资源域不完全对应(A303-2 收票单批量保存/A801-3 收票单查询 挂 `/ap/ap_invoice/`,A303-1 开票申请单新增/A303-4 开票申请单查询 挂 `/sim/`,A301 系列收款认领接口挂 `/cas/`),检索时勿按 URL 前缀猜操作码。
- 结算单认领(claim-invoices)**仍无保存重查闸门**(前端传票面直接落库,09-23 提案未实现)。
- 主数据初始化 SQL `kingdee_master_data_init_20260924.sql`(计量单位金蝶编码 + `blade_kingdee_material`)须先执行,物料/单位映射才有正确档案值;认领链路须先执行 `blade_receipt_flow_claim_20260924.sql`。
---
## 附:本文档与单链路文档的关系
- 本文是**总纲更新版**:跨链路顺序、依赖、公共机制、状态机、差异风险汇总,基线为 2026-09-24 代码(`50573e7`)。
- 单链路细节仍以 `doc/docs/业务流程/` 对应文档为深入参考;其中与代码冲突处一律以本文第 11 节结论 + 当前代码为准。
- 09-23 版总纲中被本文覆盖的过时结论:A301 系列收款认领接口/A801-4 汇率查询 未调用、A102-4 财务应付单查询 反查、付款申请分录口径、开票单张口径、客商 1102 问题——请以本文为准。
@@ -0,0 +1,258 @@
# 收款认领推送金蝶重构设计(认领本地化 → A301-3 定时 + 手动推送 → 按流水归集)
> 适用系统:TMS(blade-transport)
> 对接系统:金蝶云·星瀚(BFI,经 iPaaS 网关直连)
> 覆盖接口:A301-2 收款认领公告查询(不变)、A301-3 收款认领结果推送(**触发时机重构**)、A301-1 收款单查询回写(**轮询对象改造**)
> 前置文档:`2026-09-24-收款认领金蝶全链路业务流程(A301-2公告同步-A301-3认领推送-A301-1收款单回写).md`(下称「旧文档」,描述的是重构前现状)
> 最后更新:2026-09-28(设计稿,未实施)
---
## 1. 为什么要重构
旧文档描述的现状(对应代码 `ReceiptFlowServiceImpl.claim()` L353):**每次认领落库后立即在事务内推送 A301-3**。带来三个问题:
1. **部分认领立即推**:一笔流水分两次认领(40 万 + 60 万),金蝶侧生成两张收款单,司库对账时一笔到账裂成多单。
2. **来源编码撞码**:`awrp_src_code` 用结算单号拼接,两次认领若关联同一批结算单,编码重复,A301-1 回写按 `sourcebillnumber` 匹配存在歧义(旧文档 §4.1 幂等说明已指出)。
3. **金额口径混乱**:本地认领上限按 `receipt_amount`(收款金额)控制,而金蝶公告侧有第一轮自动认领后的 `unclaimed_amount`(未认领金额),两个口径互不参照,可能出现本地认领金额与金蝶侧待认领金额对不上的脏数据。
## 2. 新设计原则(对旧文档原则的修订)
| # | 旧原则(旧文档 §1) | 新原则 |
| --- | --- | --- |
| 1 | 公告为准(A301-2 落库流水) | **不变** |
| 2 | 推送先于结果(认领与推送同事务,失败整体回滚) | **废弃**。改为:**认领本地先行,推送异步解耦**——认领动作只落本地库,永不直接调金蝶;推送由定时任务 + 前端手动按钮承担 |
| 3 | 认领公告 id 必填 | **不变**,并扩展:`unclaimed_amount` 同样必填,缺失拦截 |
| 4 | 以金蝶侧实际生成为准 | **不变**,扩展为:**推送前实时对账**——推送前重查 A301-2 拿最新 `unclaimed_amount`,校验通过才推 |
| 5 | 异常回收人工处置(作废不推金蝶) | **不变**,并强化:**已推送流水禁止二次推送**,作废后的金蝶侧冲销一律人工处置 |
新增两条核心规则(本次重构的业务闸门):
- **认领完成 = 累计有效认领金额 == 公告未认领金额**(`unclaimed_amount`,即 A301-2 的 `unclaimamount`;作废分摊不计入累计)。只有认领完成的流水才满足推送条件——**不认领完就不推**。
- **推送单元 = 收款流水**:一条流水只产生一张金蝶收款单,**单条分录、金额 = 全部未推送金额**(累计有效认领,闸门保证等于公告未认领金额);`awrp_src_code` 按分摊结算单号下划线拼接(A301-3 契约口径),一条流水只推送一次,编码不会撞码。
## 3. 领域模型与职责划分
### 3.1 三张表的分工
```
blade_kingdee_receipt_flow(收款流水) ← 推送的权威载体
认领状态 claim_status / 累计认领 claimed_amount(展示缓存)
公告侧 unclaimed_amount / bill_id / fee ...
推送状态 push_status / 推送失败原因 push_fail_reason / 推送时间 pushed_time
推送编码 awrp_src_code / 金蝶收款单号 kingdee_bill_no / 回写状态 kingdee_bill_status
blade_receipt_claim(认领主单) ← 认领动作的留痕
认领人/部门/日期/附件/备注、claim_amount(本次认领金额)
kingdee_bill_no / kingdee_bill_status 仅作冗余展示(由流水级联刷新)
【去掉】awrp_src_code(移至流水表)
blade_receipt_claim_settlement(分摊行) ← 累计认领的权威来源
累计认领 = Σ(status=1 行的 allocated_receipt_amount) ← 作废行 status=0,天然不计入
```
**权威口径**:累计认领永远以分摊表实时聚合为准(`flowClaimedAmount()`,现有实现),流水上的 `claimed_amount` 只是展示缓存,在认领/作废/同步/推送前校验四个时机重算维护。
### 3.2 流水新增字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `push_status` | varchar(20) | 推送状态:`none` 未推送(默认)/ `pushed` 已推送 / `failed` 推送失败 |
| `push_fail_reason` | varchar(1000) | 最近一次推送失败原因(成功后清空) |
| `pushed_time` | datetime | 首次推送成功时间 |
| `awrp_src_code` | varchar(64) | 推送来源编码 = **分摊结算单号下划线拼接**(A301-3 契约口径);推送时生成并落库,分摊不变则重试编码不变(依赖金蝶候选键 `awrp_src_sys+awrp_src_code` 幂等,防重试双单) |
认领状态 `claim_status` **口径变更**(字段沿用,含义改):
```
unclaimed 累计有效认领 = 0
partial 0 < 累计有效认领 < unclaimed_amount
claimed 累计有效认领 == unclaimed_amount(且 > 0) ← 「认领完成」,唯一可推送状态
```
不再与 `receipt_amount` 比较。前端「剩余可认领」= `unclaimed_amount − 累计有效认领`。
`kingdee_bill_status`(回写生命周期)从认领主单上移到流水:`none → pushed →(A301-1 回写)approved / received / failed`;认领主单保留同名冗余字段仅作展示。
### 3.3 状态机
```
流水推送状态:
none ──推送成功──▶ pushed ──A301-1 回写──▶ approved ──▶ received(终态)
│ │ └──(A/B/退单)──▶ failed(回写失败)
└──推送失败──▶ failed ──重试(同码)──▶ pushed / 维持 failed
※ pushed 为终态闸门:任何路径不允许二次推送(作废→重认领→再满足等式也不推)
流水认领状态(对照 unclaimed_amount,随认领/作废/同步刷新重算):
unclaimed ⇄ partial ⇄ claimed
```
## 4. 流程编排
### 4.1 认领(POST /receipt-flow/claim,纯本地化)
改造 `ReceiptFlowServiceImpl.claim()`:
1. 锁流水后校验:`bill_id` 非空(沿用)+ **`unclaimed_amount` 非空**(缺失拦截「公告未认领金额缺失,请重新同步金蝶收款流水」)。
2. 结算单侧校验全部不变(审批通过/应收类型/未作废/同项目组织收付款方/单张结算单累计 ≤ 结算金额)。
3. **流水维度上限改为公告口径**:`累计有效认领 + 本次分摊 ≤ unclaimed_amount`(原为 ≤ receipt_amount),报错文案「累计认领金额不能超过公告未认领金额」。
4. 本地落库:认领主单(`kingdee_bill_status=none`)+ 分摊行 + 刷新结算单收款汇总 + 更新流水 `claimed_amount/claim_status`(按 3.2 新口径)+ 认领留痕。
5. **删除 `pushClaimResultToKingdee(...)` 调用**(L353)——认领事务到此结束,不产生任何 BFI 调用。
### 4.2 推送(统一入口,定时任务与手动按钮共用)
新服务方法 `pushClaimToKingdee(Long flowId)`,**方法级事务,推送异常在事务内消化(不外抛),以返回值向调用方报告成败与原因**——保证失败路径的状态落库不被回滚:
```
① FOR UPDATE 锁流水
② 静态闸门(硬校验,不满足直接返回失败):
- bill_id 非空
- push_status != pushed ←「已推送禁止二次推送」终态闸门
- 累计有效认领(分摊表实时聚合)> 0 且 == unclaimed_amount(compareTo==0,严格相等)
③ 实时对账:重查 A301-2 拿最新公告
- 手动按钮:claimusers = 当前登录用户真实姓名
- 定时任务:按候选流水的认领人姓名(认领主单 claimer_name 去重)逐人查询,§4.3
- 按 bill_id 匹配本流水 → 刷新 unclaimed_amount / kingdee_claim_status / claim_me
- 公告查不到 → 失败:「金蝶公告不存在或已不在认领范围,请重新同步」
- 重验:累计有效认领 == 最新 unclaimed_amount,不等 → 失败并提示两侧金额差额
(不等说明金蝶侧有人认领过或金额有变,需人工核对后作废/补认领)
④ awrp_src_code:按分摊结算单号下划线拼接生成并落库(分摊不变则重试编码不变)
⑤ 组装 A301-3:billid=流水.bill_id,**单条分录**:
e_receivableamt = 全部未推送金额(累计有效认领);
seq / e_corebillno = 第一张结算单id(分摊按结算单id升序,多张取最小)
⑥ 调 pushClaimResult:
成功 → push_status=pushed、push_fail_reason 清空、pushed_time=now、
kingdee_bill_no=响应 number(**A301-3 成功响应 number 为空,取回执 keys.awrp_src_code 即来源编码占位**)、
kingdee_bill_status=pushed;
级联刷新该流水名下全部未作废认领主单的 kingdee_bill_no/kingdee_bill_status
失败 → push_status=failed、push_fail_reason=金蝶错误文本汇总(认领数据不回滚)
⑦ 推送动作留痕(blade_receipt_flow_record,operation=push)
```
**事务边界说明**:BFI HTTP 调用在事务内,流水行锁覆盖整个推送过程——天然防「定时任务与手动按钮并发双推」(后到者会看到 `push_status=pushed` 或等锁后走静态闸门被拦)。锁粒度为单流水行锁,持锁时长为一次接口调用,可接受。
### 4.3 定时任务 ReceiptClaimPushTask
沿用 `PaymentKingdeeResultTask` 编排模式(task 类只做开关与调度,逻辑收敛在服务方法,返回 summary):
```java
@Value("${thirdParty.bfi.receiptClaimPush.syncEnabled:false}") // 默认关闭,联调稳定后开启
@Scheduled(cron = "${thirdParty.bfi.receiptClaimPush.cron:0 */10 * * * ?}")
```
扫描对象:`bill_id` 非空 AND `push_status IN (none, failed)` AND `claim_status = claimed`(缓存口径预筛缩小集合)。
**A301-2 重查的认领人问题**:A301-2 的 `claimusers` 必填且为单个用户姓名(`BfiClaimAnnounceQueryDTO` L50-52),定时任务没有登录用户上下文。处理:
1. 取候选流水的有效认领主单 `claimer_name` 去重集合;
2. **按认领人串行处理**:先调 `BfiUtil.queryAllPages(queryClaimAnnounce)` 查该认领人的公告(时间窗沿用 `thirdParty.bfi.claimAnnounce.months`,默认 6 个月),推完其名下全部流水后再处理下一个认领人;
3. 该认领人名下流水按公告 `id == 流水.bill_id` 匹配刷新,再走 §4.2 ③-⑦;
4. 单流水推送失败 try/catch 独立消化,不影响同批其他流水;同一流水被多个认领人认领时只在首个覆盖它的认领人处推送一次。
> 联调确认项:`claimusers` 若支持逗号分隔多人,可合并为一次查询(见 §8)。
### 4.4 手动推送按钮(收款流水页)
- 行按钮「推送金蝶」:`POST /receipt-flow/push-kingdee`(body: flowId),按钮权限 `receipt_flow_push`。
- 前端可用条件(展示层控制,后端硬校验为准):`claim_status=claimed` 且 `push_status != pushed` 且 `bill_id` 非空。
- 后端走 §4.2 同一入口,用当前用户姓名重查公告;若当前用户不在公告认领范围内(查不到),提示「当前用户不在该公告认领范围内,请由认领人操作或重新同步后重试」。
- 列表新增「推送状态」「推送失败原因」列;详情弹窗展示推送留痕。
- 本期不做批量推送按钮(定时任务已覆盖批量场景)。
### 4.5 A301-1 回写改造(POST /receipt-claim-record/query-kingdee-result)
`ReceiptClaimRecordServiceImpl.refreshKingdeeResult()` 轮询对象由认领主单改为**流水**:
- 扫描:`push_status = pushed` 且 `kingdee_bill_status IN (pushed, approved)` 的流水;
- 匹配:金蝶收款单 `sourcebillnumber == 流水.awrp_src_code`(一流水一码,无歧义);
- 回写:`flow.kingdee_bill_status` 按状态映射推进(A/B→pushed、C→approved、D→received、其他→failed);`kingdee_bill_no` 以金蝶真实收款单号**覆盖**推送时的来源编码占位,并级联刷新名下未作废认领主单的冗余字段;
- 定时接入复用现有任务风格(`thirdParty.bfi.receiptClaimResult.*`,本期仍可手动触发)。
### 4.6 作废(不变 + 两点明确)
`voidClaim()` 现有逻辑保留:
1. **累计认领不考虑作废**:作废行 `status=0`,分摊聚合天然排除——与「累计认领不考虑作废的」要求一致,无需改代码;作废后重算流水 `claimed_amount/claim_status`(口径按 §3.2)。
2. **已推送的认领不允许作废**:流水 `push_status=pushed` 时作废直接拦截(后端硬校验 + 前端按钮隐藏),金蝶侧冲销/调整由人工处置;作废仅对未推送流水生效,永不触发推送、不生成冲单(金蝶单号/状态保留原值)。终态闸门继续兜底:即使数据漂移使等式再次满足,`push_status=pushed` 也阻止二次推送。
### 4.7 重新同步(A301-2 sync 补一处)
同步 upsert 刷新 `unclaimed_amount` 后,**对值发生变化的流水重算 `claim_status`**(对照分摊聚合),保证公告漂移后认领状态不失真。
## 5. 边界场景与防重汇总
| 场景 | 行为 |
| --- | --- |
| 定时任务与手动按钮并发推同一流水 | 流水行锁串行化 + `push_status != pushed` 静态闸门,不会双推 |
| 推送超时但金蝶实际已生成收款单 | 本地记 failed;重试时分摊不变则 `awrp_src_code` 不变,金蝶候选键幂等,不产生重复收款单 |
| 推送成功后作废一笔认领、再补认领到等式满足 | 终态闸门拦截,不二次推送;金蝶侧冲销人工处置 |
| 公告侧 unclaimed_amount 变小(他人在金蝶认领) | 推送前实时对账发现差额 → 拦截并提示差额,人工作废多余本地认领后重推 |
| 公告侧 unclaimed_amount 变大 | 用户补认领到等式满足后推送(认领上限随新值放开) |
| unclaimamount = 0 的公告 | 认领上限为 0,不可认领;累计认领 > 0 的推送前置条件天然排除空推 |
| 手动推送者不在公告认领范围 | A301-2 按其姓名查不到公告 → 拦截提示,改由认领人操作 |
防重键沿用旧文档 §6.3,变更点:`awrp_src_code` 由「认领主单、每次认领各自拼接」改为「流水、按该流水全部分摊结算单号拼接、仅推送一次」,A301-1 匹配键随之移到流水上。
## 6. 前端改动汇总(tms-erp-web)
| 页面 | 改动 |
| --- | --- |
| `receipt-flow.vue` | ① 认领按钮可用条件:`剩余可认领 = unclaimed_amount − 累计认领 > 0`(原按 receipt_amount);② 新增「推送金蝶」行按钮(条件 §4.4);③ 列表加「推送状态/推送失败原因」列;④ 详情展示推送留痕 |
| `receipt-flow-form.vue` | 分摊上限与提示文案改为公告未认领金额口径 |
| `receipt-claim-record.vue` | 不变(金蝶状态列继续展示冗余字段;「查询金蝶认领结果」按钮保留) |
| `api/payment/receiptFlow.js` | 新增 `pushKingdee` |
## 7. SQL 变更
新增幂等脚本 `doc/sql/transport/blade_receipt_flow_push_20260928.sql`,沿用仓库惯例(`information_schema` 探测 + `PREPARE/EXECUTE` 动态 DDL),**支持重复执行**:0924 脚本执行过与否均可跑,任意顺序、任意次数,已存在的列/索引自动跳过、已不存在的列/索引自动跳过删除。
| # | 对象 | 变更 | 说明 |
| --- | --- | --- | --- |
| 1-6 | `blade_kingdee_receipt_flow` | ADD COLUMN `push_status`(默认 none)、`push_fail_reason`、`pushed_time`、`awrp_src_code`、`kingdee_bill_no`、`kingdee_bill_status`(默认 none) | 推送域字段上收至流水 |
| 7-8 | `blade_receipt_claim` | DROP KEY `idx_receipt_claim_src_code`、DROP COLUMN `awrp_src_code` | 推送编码上移至流水;0924 未执行过则本就无此列,探测后自动跳过 |
| 9-10 | `blade_kingdee_receipt_flow` | ADD KEY `idx_kingdee_receipt_flow_push_scan` (tenant_id, push_status, claim_status)、`idx_kingdee_receipt_flow_src_code` (tenant_id, awrp_src_code) | 定时任务扫描 + A301-1 回写匹配 |
| 11 | `blade_menu` | INSERT 按钮权限 `receipt_flow_push`(父级 2090000000000001260 收款流水,id 2090000000000001264,ON DUPLICATE 幂等) | 手动推送按钮 |
0924 脚本(`blade_receipt_flow_claim_20260924.sql`)保持原样不动,两个脚本无执行顺序依赖。上线顺序:先执行 0924(若未执行)→ 执行 0928 → 启动服务。
## 8. 待联调确认项
| # | 事项 |
| --- | --- |
| 1 | `claimusers` 是否支持逗号分隔多人(支持则定时任务合并为一次 A301-2 查询) |
| 2 | A301-3 字段沿用旧文档 §7 待确认清单(recpaytype_number / paymenttype / recpayer / e_corebilltype / e_corebillno) |
| 3 | `unclaimamount` 是否已扣手续费(fee)口径——本方案等式直接对照 `unclaimamount` 本身,无论是否含费均自洽,但需确认推送后金蝶侧 `unclaimamount` 归零的时效,用于判断重查窗口 |
| 4 | 同一公告在金蝶侧被人工认领部分金额后,A301-2 返回的 `unclaimamount` 更新时效(影响实时对账的准确性预期) |
## 9. 与旧文档的差异对照
| 环节 | 旧(2026-09-24) | 新(本设计) |
| --- | --- | --- |
| 认领事务 | 落库后同事务立即推 A301-3,失败整体回滚 | 纯本地落库,不调 BFI |
| 推送触发 | 每次认领动作 | 定时任务(`receiptClaimPush.*` 开关)+ 手动按钮,双入口同一服务方法 |
| 推送条件 | 无(认领即推) | 累计有效认领 == 公告未认领金额(推送前重查 A301-2 实时对账) |
| 推送粒度 | 每次认领一张金蝶收款单 | 一条流水一张金蝶收款单(单条分录,金额=全部未推送金额) |
| awrp_src_code | 认领主单上,每次认领各自按结算单号拼接(多流水分摊到同一批结算单时撞码) | 流水上,按该流水全部分摊结算单号拼接,一条流水仅推送一次,不再撞码 |
| 认领上限/认领完成口径 | receipt_amount | unclaimed_amount |
| 推送状态/失败原因 | 无(失败即回滚无痕) | 流水上 push_status / push_fail_reason / pushed_time,失败可重试可追溯 |
| A301-1 轮询对象 | 认领主单 | 流水(级联刷新认领主单冗余字段) |
| 二次推送 | 可能(作废重认领再推) | 禁止(终态闸门) |
## 10. 代码改动索引(已实施,2026-09-28)
| 环节 | 位置 | 改动 |
| --- | --- | --- |
| 认领 | `ReceiptFlowServiceImpl.claim()` | 上限口径改公告未认领金额(缺失/为0拦截);删除同步推送调用,事务内只落本地 |
| 推送 | `IReceiptFlowService.pushClaimToKingdee(flowId)` / `pushClaimByTask()` | 新增。共用 `pushClaimLocked`(`TransactionTemplate` 编程式事务,流水行锁覆盖闸门→对账→推送→落库,防并发双推);失败不抛异常跨事务边界,以 `ReceiptClaimPushResult` 返回 |
| 定时任务 | `task/ReceiptClaimPushTask.java`(新增) | Nacos 开关 `thirdParty.bfi.receiptClaimPush.syncEnabled/cron`(默认关闭、每10分钟);候选=认领完成且 push_status ∈ (none, failed),按认领人姓名去重逐人重查 A301-2 |
| 推送报文 | `buildClaimPushDTO` | billid=流水.bill_id;**单条分录**:e_receivableamt=全部未推送金额(累计有效认领),seq/e_corebillno=第一张结算单id;awrp_src_code=分摊结算单号下划线拼接 |
| 手动接口 | `ReceiptFlowController.pushKingdee`(`POST /receipt-flow/push-kingdee`) | 成功返回金蝶收款单号,失败 R.fail(原因);按钮权限 `receipt_flow_push` |
| 回写 | `ReceiptClaimRecordServiceImpl.refreshKingdeeResult()` | 轮询对象改流水(push_status=pushed 且回写状态 pushed/approved),按流水 awrp_src_code 匹配,级联刷新认领主单冗余字段 |
| 作废 | `ReceiptClaimRecordServiceImpl.voidClaim()` | claim_status 重算口径改对照公告未认领金额;**不再生成本地 KDCX 冲单号**(金蝶单号/状态保留原值);**已推送流水(push_status=pushed)作废拦截** |
| 同步 | `ReceiptFlowServiceImpl.sync()` + 共享 `queryClaimAnnounces` | unclaimed_amount 刷新后按新口径重算 claim_status;公告查询方法供 sync/手动推送/定时任务共用 |
| 常量 | `KingdeeBfiConstant` | 新增 RECEIPT_CLAIM_PUSH_NONE/PUSHED/FAILED、RECEIPT_CLAIM_BILL_NONE |
| 实体/VO | `KingdeeReceiptFlow`(推送域6字段)、`ReceiptClaim`(删 awrpSrcCode)、`ReceiptClaimMapper.xml`(删列)、`ReceiptFlowVO/Wrapper`(pushStatusName/kingdeeBillStatusName、剩余可认领改公告口径)、`ReceiptClaimPushResult`(新增 DTO) | — |
| 详情留痕 | `ReceiptFlowServiceImpl.detail()` | claimRecords 含 claim + push 两类动作记录 |
| 批量化 | `FormalSettlementServiceImpl.refreshPaymentSummaries(Collection<Long>)`(新增)+ `calculatePaymentSummary` 拆纯计算重载 | 固定数次批量查询完成 N 张结算单收款汇总重算(关系/付款申请/申请状态/预收来源/认领分摊各一次 IN 查询 + updateBatchById);单笔 `refreshPaymentSummary` 与 `refreshPaymentSummariesForPreSettlement` 复用同一口径 |
| 批量化 | `claim()` / `voidClaim()` / `settlementCandidates()` / `sync()` | ① 结算单锁行改一次 `IN ... ORDER BY id FOR UPDATE`(与逐单加锁顺序一致防死锁);② 结算单累计认领额一次聚合查询(候选单列表页原先每页最多 200 次查询);③ 作废分摊行改单条 UPDATE(实体携带 status 保留审计字段填充);④ 同步留痕改 `Db.saveBatch` 批量插入 |
| 前端 | `receipt-flow.vue`(推送金蝶按钮+canPush、推送状态/失败原因/金蝶单号/金蝶状态列、详情推送字段)、`receipt-flow-form.vue`(剩余可认领金额字段、上限口径)、`receipt-flow-batch-form.vue`(批量口径)、`option/payment/receiptFlow.js`(新列)、`api/payment/receiptFlow.js`(pushKingdee) | — |
@@ -0,0 +1,110 @@
# 收款认领流程(业务版)
> 业务域:图06「收款查询及认领(回款认领)」的 TMS 二次认领环节
> 核心规则:**认领只在本系统登记,不同步金蝶;一笔流水认领完成后,才推送金蝶生成收款单——不认领完就不推。**
> 最后更新:2026-09-28
---
## 一、认领的对象:收款流水从哪来
金蝶司库收到银行收款流水后,先自动认领到客户维度,剩余未落到合同/结算单的部分形成**未认领金额**。业务人员在 TMS「收款流水」页面点「手动同步流水」,把这些公告同步成本系统的收款流水——每条流水带一个**公告未认领金额**,这就是后续认领的额度基准(重复同步不会产生重复流水,金额有变化会刷新)。
---
## 二、认领操作流程
**入口**:收款流水列表/详情的「认领」按钮——流水还有剩余可认领额度(公告未认领金额 − 累计认领)时可操作。
**操作步骤**:
| 步骤 | 操作 | 规则 |
| --- | --- | --- |
| 1. 选结算单 | 选择一张或多张应收正式结算单 | 须审批通过、未作废;多张须同一项目、同一组织、同一收付款方 |
| 2. 录分摊金额 | 逐张结算单录入本次认领的收款金额 | ① 单张结算单:累计被认领金额 ≤ 其结算金额;② 整条流水:累计认领金额 ≤ 公告未认领金额(只认领金蝶侧缺口) |
| 3. 确认 | 提交认领 | 生成一条认领记录(含分摊明细),刷新流水认领进度 |
**关键规则**:
1. 确认后认领只在本系统登记,**不与金蝶发生任何交互**。
2. 一条流水**允许多次认领**(部分认领):今天认 40 万给 A 结算单,明天再认 60 万给 B 结算单,直到累计认领等于公告未认领金额。
---
## 三、认领状态
### 流水认领状态(认领进度)
| 状态 | 含义 |
| --- | --- |
| 未认领 | 还没有任何有效认领 |
| 部分认领 | 累计认领 < 公告未认领金额 |
| **认领完成** | 累计认领 = 公告未认领金额(正好补齐金蝶侧缺口) |
- 累计认领金额 = 该流水下所有**有效**认领分摊之和,**作废的认领不计入**;
- 剩余可认领金额 = 公告未认领金额 − 累计认领金额。
### 认领记录状态
| 状态 | 含义 |
| --- | --- |
| 已认领 | 有效认领,金额计入流水累计 |
| 已作废 | 不再计入累计,流水认领进度相应回退 |
---
## 四、认领完成后的推送(认领流程的出口)
**推送条件(闸门)**:
1. 流水**认领完成**——累计认领 = 公告未认领金额,**不认领完就不推**;
2. 该流水**尚未推送过**——已推送的流水不允许二次推送。
**推送方式(两个入口,同一套规则)**:
- 定时任务自动推送:系统定期扫描满足条件的流水自动推;
- 手动推送:流水页面「推送金蝶」按钮,条件不满足时置灰。
**推送前对账**:推送前先到金蝶重查该公告的**最新未认领金额**,与本地累计认领核对——一致才推;不一致(如他人在金蝶侧手工认领过)则拦截并提示差额,人工核对作废或补认领后再推。
**推送结果**:
| 结果 | 表现 |
| --- | --- |
| 成功 | 金蝶生成**一张收款单**(一笔流水一张单,多张结算单是该单下的多条分摊明细);流水推送状态置"已推送",回填金蝶收款单号 |
| 失败 | 推送状态置"推送失败"并记录失败原因;**本地认领保留不回滚**;排除原因后可重新推送,重试不会在金蝶产生重复单据 |
### 流水推送状态
| 状态 | 含义 | 后续 |
| --- | --- | --- |
| 未推送 | 未满足推送条件或尚未轮到推送 | 继续认领 / 等待定时任务或手动推送 |
| 已推送 | 金蝶已生成收款单 | 等待金蝶处理结果回写(审核/收款状态由回写推进);不再接受二次推送 |
| 推送失败 | 上次推送被金蝶拒绝或异常 | 按失败原因整改后重新推送 |
> 认领记录页可随时手动「查询金蝶认领结果」,回写金蝶收款单号与处理状态(已推送 → 审核通过 → 已收款)。
---
## 五、作废认领
1. 仅认领人本人可作废自己的认领记录;作废后该笔分摊不再计入累计,流水认领状态相应回退(认领完成 → 部分认领/未认领)。
2. **已推送金蝶的认领不允许作废**(按钮不显示,后端拦截)——金蝶侧收款单已生成,冲销/调整由人工在金蝶处置。
3. **作废永远不会触发推送**:作废仅对未推送流水生效,纯本地生效,无金蝶影响;补认领到认领完成后仍可正常推送。
---
## 六、典型场景
1. **一次认领完成**:未认领金额 100 万,一次认领到 2 张结算单(60 万 + 40 万)→ 认领完成 → 推送,金蝶生成一张 100 万收款单。
2. **分两次认领完成**:先认 40 万(部分认领,不推金蝶),再认 60 万补齐 → 认领完成 → 推送,仍是一张收款单含两笔分摊。
3. **金额不一致被拦截**:本地累计 100 万,推送前金蝶重查最新未认领只剩 80 万 → 拦截提示差额 20 万 → 作废多余认领后再推。
4. **推送失败重试**:因结算单尚未同步金蝶应收被拒 → 记录失败原因 → 补齐前置后重推,不产生重复收款单。
---
## 七、注意事项
1. **结算单前提**:认领的结算单须已完成金蝶应收侧同步(有金蝶应收单号),推送时金蝶才能按应收单核销。
2. **认领人前提**:金蝶公告的认领人姓名须与 TMS 用户真实姓名一致,否则同步不到流水;推送前对账也要求操作者在公告认领范围内(定时任务按各认领人自动查询,不受单个人限制)。
3. **两个口径不要混**:「公告未认领金额」是金蝶侧待认领缺口(额度与认领完成的判定基准);「累计认领金额」是本系统登记的有效认领合计。认领完成 = 两者相等。
@@ -0,0 +1,169 @@
# 预付款申请(A103-3)金蝶链路补齐方案:e_bebank_number 统一、A103-2 消失解锁、回写默认开启
> 日期:2026-09-29
> 关联文档:
> - `doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`(图03 财务应付及付款,步骤4 推送金蝶 / 步骤13 异常回收)
> - `doc/adr/0005-B901事件驱动双表锁单模型.md`(UNLOCK 置失败态不自动重推;A103-2 连续两轮查询消失兜底复用 `unlockBill()`)
> - `doc/docs/业务流程/付款申请同步金蝶(星瀚)技术文档.md`(A103-3 接口定义)
> - `doc/docs/业务流程/2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md`
> - `doc/docs/业务流程/2026-09-18-付款管理金蝶同步全链路逻辑梳理(推送链路与回写链路连贯性).md`
## 1. 背景与范围
对照方案文档图03 对 A103-3 预付款路径做金蝶侧缺口盘点,本方案补齐三项(合同合规系统相关事项另期处理):
| # | 缺口 | 原状 | 本方案 |
|---|---|---|---|
| 1 | A103-3 分录 `e_bebank_number`(往来银行.编码)取 `bankName`(银行名称字段) | 与 A103-1 取客商收款账户联行号的口径不一致,疑似取错字段 | 统一取联行号,未维护即拦截 |
| 2 | 金蝶侧反审核删除付款申请单后,TMS 无感知、无恢复路径 | A103-2 查不到仅 `continue`,本地停留 synced/paying;状态门禁禁止重推 → 单据永久卡死 | 连续两轮查询消失 → `unlockBill()` 解锁置失败,交人工重推 |
| 3 | 定时回写任务默认关闭 | 代码 `@Value` 默认 false(注释却写"默认开启"),三套 Nacos 配置默认 false,上线必须手工开启 | 代码与 Nacos 默认均改为开启,注释纠正 |
不改动范围:A103-3 推送报文其余字段、三层幂等门禁、A902 附件链路、A103-4 付款记录回写、履约数据联动(均已就绪)。
## 2. 改造一:A103-3 `e_bebank_number` 与 A103-1 统一口径
### 2.1 变更内容
`PaymentApplicationServiceImpl#pushBfiPayApply`:
| 项 | 变更前 | 变更后 |
|---|---|---|
| 取值 | `entity.getBankName()`(付款申请"收款银行名称"文本) | `bfiBebankNumber(entity, context)` |
| 实际含义 | 银行名称(非编码,金蝶档案无法匹配) | 付款申请所选客商收款账户(`blade_customer_receipt_account`)**联行号 `cnapsCode`** |
| 缺失行为 | 不校验,直接传名称 | 收款账户不存在 / 未维护联行号 → `ServiceException` 拦截推送(与 A103-1 同口径) |
```java
// 往来银行.编码:与 A103-1 统一口径,取付款申请所选客商收款账户联行号(未维护即拦截)
entry.setEBebankNumber(this.bfiBebankNumber(entity, context));
```
### 2.2 取值链路(与 A103-1 完全一致)
1. 付款申请选择了收款账户(`receiptAccountId`)→ 按账户 id 直查,批量同步经 `KingdeeSyncContext.receiptAccountCache()` 复用;
2. 未选账户 → 按合同乙方客商 + 收款账号兜底匹配(默认账户优先);
3. `cnapsCode` 为空 → 拦截:"收款账户未维护联行号,请先在客商档案维护收款信息联行号"。
### 2.3 影响
- **新增推送前置条件**:预付款推送前必须维护收款账户联行号。存量已 synced 单据不受影响(状态门禁不会重推);failed 待重推单据需先补联行号。
- 解锁重推(改造二)产生的"新单"同样走该口径,与金蝶侧往来银行档案编码匹配。
## 3. 改造二:A103-2 消失解锁兜底(ADR-0005)
### 3.1 业务问题
图03 异常回收分支:财务共享驳回后,金蝶侧对该付款申请单**反审核 + 删除**,TMS 需解锁并允许重新推送。此前 TMS 只有正向回写:A103-2 查不到该单仅跳过,本地停留 synced/paying,且 `doSyncKingdee` 状态门禁禁止 synced/paying 重推——单据卡死,只能改库解救。
B901 UNLOCK 事件通知属合同系统对接域,本期不建设;按 ADR-0005 约定,A103-2 轮询兜底与 B901 共用同一 `unlockBill()` 方法,B901 后续接入时零改动复用。
### 3.2 判定规则:"连续两轮查询消失"
| 要素 | 规则 |
|---|---|
| 判定对象 | 本轮 A103-2 批量查询中未查得的、本地状态为 **synced / paying** 的单据 |
| 健康前提 | 同批次其余付款申请号正常返回(A103-2 服务整体故障时整轮抛异常,不做消失判定) |
| 排除对象 | failed(金蝶查不到属正常态,等待人工重推)、终态 paid/closed(不参与轮询)、paymentNo 为空(防御) |
| 计数存储 | Redis `transport:payment-application:kingdee-pay-miss:{付款申请id}`,每轮未查得 `INCR`,TTL 2100s(覆盖 3 个 10 分钟回写周期;任务中断超时后计数自然过期,需重新累计,方向保守) |
| 查得即清零 | A103-2 查得该单 → `DEL` 计数键 |
| 解锁阈值 | 连续计数 ≥ 2 轮 → 调用 `unlockBill(id, "A103-2 连续N轮查询消失")` |
| Redis 异常 | 放弃本轮判定(解锁会清空金蝶单号,属破坏性动作,宁可延迟不可误判) |
### 3.3 `unlockBill()` 语义(ADR-0005,接口方法,B901 可直接复用)
前置:仅 synced / paying 生效;failed 与终态单据自动跳过。
动作(单据级事务,条件更新防并发):
1. 清除消失计数键;
2. `kingdee_status`:synced/paying → **failed(生成失败)**;
3. `kingdee_bill_id`、`kingdee_bill_no` **清空**(原值 warn 日志留档;金蝶侧单据已删除,原内码对 A902 附件、A103-4 勾稽均已失效);
4. **不自动重推**——金蝶反审核删除大概率伴随驳回意见,原样重推等于无视驳回(ADR-0005),置 failed 回人工。
### 3.4 重推闭环与误判安全网
解锁后单据进入 failed,与"推送异常"恢复路径完全合并:
```mermaid
sequenceDiagram
participant T as TMS 定时回写(A103-2)
participant R as Redis 计数
participant K as 金蝶(星瀚)
participant U as 人工
T->>K: A103-2 按 paymentNo 批量查询
K-->>T: 某单未查得(同批其余正常)
T->>R: INCR 消失计数(第1轮,仅告警)
T->>K: 下一轮再查仍未见
T->>R: INCR 计数 ≥ 2
T->>T: unlockBill():status=failed、清单号/内码(日志留档)
U->>U: 核实金蝶确已删除(驳回原因)
U->>T: 「同步金蝶」重新推送
T->>K: A103-2 勾稽预检(failed 恢复场景)
alt 金蝶侧单据实际存在(误判)
K-->>T: 命中 → 回填单据id/编号,跳过推送(不产生重复单据)
else 确已删除
K-->>T: 未命中 → A103-3 重新保存,生成新单
T->>K: A902 附件上传到新单
end
```
要点:**即使误判解锁,重推链的 A103-2 勾稽预检也能兜底**——预付款路径 failed 恢复场景必经 `reconcileExistPayApply`,正式路径推送前统一预检,命中即回填跳过,不会产生重复单据。
### 3.5 涉及代码
| 位置 | 内容 |
|---|---|
| `IPaymentApplicationService#unlockBill(Long id, String source)` | 新增接口方法(B901 接入时复用) |
| `PaymentApplicationServiceImpl#unlockBill` | 解锁实现(事务 + 条件更新) |
| `PaymentApplicationServiceImpl#handlePayApplyMissed` | 消失轮次判定(回写主流程接入) |
| `executePaymentResultSync` ① 循环 | `payApply == null` → `handlePayApplyMissed`;查得 → `clearPayApplyMissKey` |
## 4. 改造三:定时回写默认开启
| 位置 | 变更 |
|---|---|
| `PaymentKingdeeResultTask` | 注释纠正为"代码默认关闭(安全兜底),由 Nacos 按环境显式开启"(`@Value` 兜底默认保持 `false`) |
| `doc/nacos/blade-dev.yaml` / `blade-prod.yaml` / `third-party-api.yaml` | `syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true}`——**环境配置默认开启**,未部署 Nacos 配置时代码兜底关闭 |
- 调度频率不变:`0 */10 * * * ?`(每 10 分钟)。
- 环境变量 `BFI_PAY_RESULT_SYNC_ENABLED=false` 仍可显式关闭(灰度/演练用)。
- 消失解锁依赖该任务运行,开关关闭期间兜底判定同步停摆(预期行为)。
## 5. 状态机全景(变更后)
```
unsynced ─A103-1/A103-3─▶ synced ─Inpayment─▶ paying ─Alreadypay─▶ paid(终)
│ ▲ │
│ └─ A103-2 勾稽纠偏 ──┘
├─ 连续两轮查询消失 / B901 UNLOCK ─▶ failed ─人工重推─▶ synced
推送异常 ─────────────────────────────────────────────────────▶ failed
billstatus=D ──────────────────────────────────────────────────▶ closed(终)
```
- 解锁后 `kingdee_bill_id/no` 清空 → A103-2/A103-4 后续轮询按 paymentNo 重新勾稷新单;
- failed 纳入轮询的既有纠偏逻辑与解锁路径互不冲突(failed 查不到不计数、查到回填 synced)。
## 6. 配置项汇总
| 配置 | 默认 | 说明 |
|---|---|---|
| `thirdParty.bfi.payResult.syncEnabled` | Nacos 配置 `true`;代码兜底 `false` | 回写任务总开关,关闭则状态回写与消失解锁同时停摆 |
| `thirdParty.bfi.payResult.cron` | `0 */10 * * * ?` | 回写周期,同时决定消失解锁的轮次间隔 |
| `thirdParty.bfi.payResult.lockLeaseSeconds` | `1800` | 回写 single-flight 锁租约 |
| Redis 键 `transport:payment-application:kingdee-pay-miss:{id}` | TTL 2100s | 消失轮次计数,无配置项(常量) |
## 7. 验证要点
1. **联行号拦截**:收款账户未维护联行号的预付款点击"同步金蝶" → 报"收款账户未维护联行号"且不产生金蝶单据;维护后重推成功,A103-2 查回的 `e_bebank_number` 为联行号。
2. **消失解锁**:推送成功(synced)→ 金蝶侧反审核并删除该付款申请单 → 等待 ≥2 个回写周期 → 日志出现"第1轮/连续2轮",单据变 failed、`kingdee_bill_id/no` 清空。
3. **误判回填**:解锁后金蝶侧单据实际存在(构造 A103-2 两次查询异常)→ 人工重推 → 勾稽命中回填,金蝶侧不出现重复单据。
4. **开关回归**:`BFI_PAY_RESULT_SYNC_ENABLED=false` 环境变量能覆盖默认值关闭任务。
## 8. 遗留事项(不在本方案范围)
| # | 事项 | 状态 |
|---|---|---|
| 1 | B901 UNLOCK 回调接入(解锁延迟从 ≤2 个回写周期缩短为事件即时) | 待建设,复用 `unlockBill()` 零改动 |
| 2 | closed 终态(金蝶驳回/关闭)票据额度释放与待处理提醒 | 未实现 |
| 3 | A103-3 预付款不传 `awrpSrcConNo`(合同号)是否符合金蝶勾稽要求 | 待联调确认 |
| 4 | A103-2 查询未传 modifytime 时间窗(接口约束 ≤1 个月)是否放行 | 待联调确认 |
@@ -0,0 +1,209 @@
# 金蝶(星瀚)BFI 定时任务逻辑全梳理(付款回写 + 应收勾稽 + 开票回写 + 收票回写 + 客商同步 + 收款认领推送)
> 整理日期:2026-09-30
> 依据:当前代码(git 基线 `2e91e11` 收款认领本地化提交),代码位置 `blade-service/blade-transport/src/main/java/org/springblade/transport/task/` 与各服务实现。
> 关联文档:`2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版).md`(总纲)、`2026-09-27-blade-transport定时任务全梳理(金蝶客商同步-金蝶结果回写-收票回写).md`(上一版,**未收录收款认领推送任务,行号已过时,以本文为准**)。
> 接口名称口径与总纲一致:编号后标注金蝶侧接口名称。
---
## 1. 任务总览
task 目录共 **3 个任务类、4 个调度方法**,覆盖 6 条金蝶(星瀚)链路的回写/同步/推送。调度由 `TransportApplication` 的 `@EnableScheduling` 开启;每个方法 = Nacos 开关 + cron 表达式,业务逻辑全部收敛在服务层方法(任务类只做开关判断、异常捕获与中文摘要日志),定时与手动端点共用同一服务方法。
| 调度方法 | 开关(thirdParty.bfi.*,代码默认) | 默认 cron | 服务方法(代码位置) | 覆盖接口 | 手动端点 |
|---|---|---|---|---|---|
| `PaymentKingdeeResultTask.syncKingdeePaymentResult` | `payResult.syncEnabled`(false) | `0 */10 * * * ?` | `PaymentApplicationServiceImpl.syncKingdeePaymentResult`(:644,编排 3 个子流程) | ① A103-2 付款申请单查询 + A103-4 付款处理查询 ② A302-4 财务应收单查询 勾稽 ③ A303-5 开票发票查询 | `POST /payment-application/sync-kingdee-result`、`POST /formal-settlement/sync-kingdee-ar-result`、`POST /invoice-application/sync-kingdee-invoice-result` |
| `PaymentKingdeeResultTask.syncKingdeeReceivedInvoiceResult` | `receivedInvoiceResult.syncEnabled`(false) | `0 */10 * * * ?` | `InvoiceReceiptServiceImpl.syncKingdeeReceivedInvoiceResult`(:968) | A801-3 收票单查询(回写) | `POST /invoice-receipt/sync-kingdee-result` |
| `CustomerKingdeeSyncTask.syncKingdeeCustomerData` | `customer.syncEnabled`(false) | `0 0 2 * * ?` 每天 02:00 | `CustomerArchiveServiceImpl.syncKingdeeCustomerData`(:637) | A902-1 客户查询 复查 + A902-2 客户保存 推送 | `POST /customer-archive/sync-kingdee` |
| `ReceiptClaimPushTask.pushClaimToKingdee` ★新增 | `receiptClaimPush.syncEnabled`(false) | `0 */10 * * * ?` | `ReceiptFlowServiceImpl.pushClaimByTask`(:714) | A301-3 收款认领结果推送 | `POST /receipt-flow/push-kingdee`(单条手动) |
> 认领链路另两个接口仍为**纯手动、无定时**:A301-2 收款认领公告查询(`POST /receipt-flow/sync`)、A301-1 收款单查询(`POST /receipt-claim-record/query-kingdee-result`)。旧总纲「收款认领三接口全部手动触发」的说法已被本任务打破:**A301-3 自 `2e91e11` 起为定时 + 手动双入口**。
> ⚠️ 代码开关默认值均为 `false`;`doc/nacos/third-party-api.yaml` 样例中 `payResult.syncEnabled` 的环境变量兜底值为 `true`(`BFI_PAY_RESULT_SYNC_ENABLED:true`),以 Nacos 实际下发为准。
---
## 2. 公共机制
### 2.1 single-flight 分布式锁(KingdeeSyncLockSupport,★09-27 后新建)
6 个服务方法入口统一经 `KingdeeSyncLockSupport.runSingleFlight(锁前缀, 租约秒数, 任务名, action)`(`support/KingdeeSyncLockSupport.java:61`):
- **零等待抢锁**:`tryLock(0, leaseSeconds)`,已有轮次在跑立刻放弃本轮并返回「XX进行中,本轮跳过」摘要(定时下轮幂等重试,手动稍后再试);多副本部署的定时任务与手动触发共用一道闸,同一轮全局最多一份在跑。
- **锁键 = 前缀 + 租户**:定时任务无登录上下文时按 `default` 租户加锁,锁粒度与多租户数据隔离边界一致。
- **租约兜底**:租约(各任务默认 1800 秒)须大于最坏一轮耗时(含翻页远程查询),否则租约到期另一副本会并发起跑;进程异常退出时锁到期自动释放。
- **降级**:`blade.lock.enabled=false` 时不加锁直接执行(多副本各自起跑),打 warn 日志——生产必须开启分布式锁。
各任务锁前缀与租约配置键:
| 任务 | 锁键前缀(transport:…) | 租约配置键(默认 1800) |
|---|---|---|
| 付款结果回写 | `payment-application:kingdee-pay-result-sync:` | `thirdParty.bfi.payResult.lockLeaseSeconds` |
| 应收勾稽 | `formal-settlement:…`(AR_RESULT) | `thirdParty.bfi.arResult.lockLeaseSeconds` |
| 开票结果回写 | `invoice-application:…`(INVOICE_RESULT) | `thirdParty.bfi.invoiceResult.lockLeaseSeconds` |
| 收票结果回写 | `invoice-receipt:…`(RECEIVED_RESULT) | ⚠️ `thirdParty.bfi.receivedInvoice.lockLeaseSeconds`(前缀少「Result」,与其余开关键不同名) |
| 客商同步 | `customer-archive:…`(CUSTOMER_SYNC) | `thirdParty.bfi.customer.lockLeaseSeconds` |
| 收款认领推送 | `receipt-flow:…`(RECEIPT_CLAIM_PUSH) | `thirdParty.bfi.receiptClaimPush.lockLeaseSeconds` |
### 2.2 批量与时间窗口径(IBfiService 契约)
- 批量查询统一 **50 键/批**(付款 `PAY_RESULT_QUERY_BATCH=50`、开票 `INVOICE_RESULT_QUERY_BATCH=50`、客商信用代码 `KINGDEE_CUSTOMER_QUERY_BATCH=50`),页大小 200,`BfiUtil.queryAllPages` 自动翻页(页空/`lastPage`/不满页/超 50 页终止)。
- 查询类接口时间窗:A103-2 付款申请单查询 / A103-4 付款处理查询 / A302-4 财务应收单查询 / A303-4 开票申请单查询 / A301-1 收款单查询 **≤ 1 个月**;A902-1 客户查询 **≤ 6 个月**;A301-2 收款认领公告查询 无接口级窗口(代码按 `thirdParty.bfi.claimAnnounce.months`,默认近 6 个月,逐认领人查询)。
- 事务边界:任务方法与 single-flight 主体**无事务**;单据级写操作经自代理/`TransactionTemplate` 走**独立事务**(如 `applyPayApplyResult`、`applyPayBillRecord`、`pushClaimLocked`),单条失败只回滚自身并计数,不中断整批。
- 可观测:每个子流程返回中文执行摘要(扫描数、成功/失败/纠偏/跳过计数),任务类 `log.info` 落盘。
---
## 3. 任务一:付款结果回写(PaymentKingdeeResultTask.syncKingdeePaymentResult)
编排 **3 个互相独立的子流程**(各自 try/catch,一个异常不影响其余两个):
### 3.1 子流程①:A103-2 付款申请单查询 + A103-4 付款处理查询
实现:`PaymentApplicationServiceImpl.executePaymentResultSync`(:655)。
```
待回写付款申请(approved + kingdeeStatus IN (synced, paying, failed),paid/closed 终态不再轮询)
→ A103-2 按 paymentNo(awrp_src_code)50/批查询,同一来源单号取 modifytime 最新一条
命中 ──> 清除消失计数 ──> applyPayApplyResult(独立事务):
回填 kingdee_bill_id / kingdee_bill_no
状态映射:paystatus=Alreadypay→paid(终)> billstatus=D→closed(终)> Inpayment→paying > 其余→synced
已付金额 = 分录 e_paidamt 汇总(分录缺失且 paid 时兜底=申请金额)
paid 联动履约:syncPreSettlementAdvance(预结算预付)+ syncFormalSettlementPayments(实付分摊/结算付款记录)
未命中 ──> 消失解锁判定(ADR-0005,见下)
→ A103-4 付款记录回填:
候选 = 本轮 A103-2 查得 paystatus≠Unpaid 的单 + 已 paid 但无真实付款记录的单(补偿)
按付款组织编码分组(org_number 优先取 A103-2 返回 payorg_number,兜底按合同所属组织解析,取不到 warn 跳过)
每组按最近 1 个月业务日期窗口翻页查询(接口 ≤1 个月约束)
勾稽:sourcebillid==kingdee_bill_id 优先,sourcebillnumber==kingdee_bill_no 兜底
命中 ──> applyPayBillRecord(独立事务):清理 MOCK-PAY- 模拟记录 → upsert blade_payment_application_record
(billno→payment_no、actpayamt→paid_amount、paydate→paid_date、整单 JSON→voucher_json 留档)
```
**★ 消失解锁(ADR-0005,`handlePayApplyMissed` :575)**:A103-2 连续未查得「已生成/付款中」单据时,按 Redis 计数累计消失轮次(键 `transport:payment-application:kingdee-pay-miss:{id}`,TTL 2100 秒≈覆盖 3 个回写周期;生成失败单据查不到属正常态不计数)。**连续 2 轮**(`PAY_RESULT_MISS_UNLOCK_ROUNDS=2`)未查得即判定金蝶侧已反审核删除,调用 `unlockBill`(:544):`kingdeeStatus synced/paying→failed`,**清空 kingdee_bill_id / kingdee_bill_no**(原值日志留档),不自动重推,等待人工核实后重推;重推时 A103-2 幂等预检兜底,误判场景(金蝶单据实际存在)自动回填不产生重复单据。Redis 异常时放弃本轮判定(解锁是破坏性动作,宁可延迟不可误判)。本轮重新查得或单据解锁时清除计数。
### 3.2 子流程②:A302-4 财务应收单查询 勾稽纠偏
实现:`FormalSettlementServiceImpl.executeArResultSync`(:690)。
- 轮询:approved + settlementType=receivable + kingdeeSyncStatus IN (synced, **failed**)。
- 逐单查结算明细组装防重键(与 A302-2 财务应收单保存传参一致:`正式结算单号_结算明细单据号…`),同一防重键取首单。
- `queryArBillExistNos` 批量查询:查到 → 回填缺失的 kingdee_bill_no、状态统一置 synced、记 syncedTime(failed→synced 即生成失败纠偏;synced 且已有单号的跳过);查不到 → 状态不变。
- 同步成功回写已含 **kingdee_bill_id(星瀚单据内码,A902 附件上传定位用)**。
### 3.3 子流程③:A303-5 开票发票查询 结果回写
实现:`InvoiceApplicationServiceImpl.executeInvoiceResultSync`(:878)。
- 轮询:approved + kingdeeStatus=synced + kingdee_bill_no 非空(invoiced 终态不再轮询;failed 无真实单号,纠偏走重推时的 A303-4 批量预检)。
- kingdee_bill_no 逗号拼接拆分为逐个 serialNo → 按开票单位反查客商档案取销方税号(取不到跳过并计数)→ 按税号分组 → 每组 ≤50/批调 A303-5 开票发票查询(票易通 openApi)。
- 发票落库:发票号批量去重(批次内预查已有记录),新增插入 `blade_invoice_application_invoice`(serial_no、invoice_no、invoice_date、invoice_amount、**发票文件地址四字段**:板式文件/预览图/数电 PDF/数电 XML、raw_json);**已有记录缺失文件地址的按本次返回字段级补空**(不覆盖已有值)。
- 有新增发票 → 申请单推进 invoiced(终态);发票插入、地址补齐、申请单更新三段**分开批量落库**。
---
## 4. 任务二:收票结果回写(A801-3 收票单查询,PaymentKingdeeResultTask.syncKingdeeReceivedInvoiceResult)
实现:`InvoiceReceiptServiceImpl.executeReceivedInvoiceResultSync`(:979)。处理推送链中断/异常的补偿,一轮一体处理三类场景:
```
待回写收票登记(kingdeePushStatus IN (pushed, assigned) + 非作废 + 已绑定票池镜像)
→ 批量加载 blade_kingdee_invoice_pool 镜像,按镜像购方名称分组(接口必填购方名称)
→ pushed 单据批量预组装财务应付单防重键(分摊→结算单→结算明细,一票一应付才可组装)
→ 回看窗口 = thirdParty.bfi.receivedInvoice.resultWindowDays(默认 30 天),按购方分组翻页查询
匹配键 = 收票登记 id(★已修复:与 A303-2 收票单批量保存 落库的 awrp_src_code 同源,
旧版按发票号匹配永远查不到自推单据的缺陷已关闭)
窗口内查不到 ──> 不判删除(金蝶修改时间延迟/滑窗),保持现状由人工重推兜底
命中后三分支:
① 发票状态异常(作废/红冲等) ──> 异常回收:置 failed +「金蝶删除/解除指定后重推」,记同步流水
② pushed 且状态正常 ──> 指定补偿:凭预组装防重键重发 A302-6 收票指定应付 → assigned
防重键缺失(不满足一票一应付)→ 置 failed 报人工
③ assigned 但缺金蝶收票单号 ──> 单号补登:仅补 kingdee_received_invoice_no(历史响应丢失数据)
→ 统一走 applyReceivedInvoicePushResult 落库(单号/状态/失败原因/推送时间),终态/失败记 blade_invoice_receipt_record 流水
```
---
## 5. 任务三:客商数据定时同步(CustomerKingdeeSyncTask.syncKingdeeCustomerData)
实现:`CustomerArchiveServiceImpl.executeCustomerDataSync`(:648)。一次查出全部审批通过客商(**★原 11.1-#3 硬编码 `.eq(customerCode,"KS000067")` 联调过滤已移除**),内存按金蝶协同状态分三组,单条失败不中断批次:
```
审批通过客商(approvalStatus=approved,一次全量查询)
① 待推送(none/failed)── 批量 A902-1 客户查询 复查(信用代码去重大写、50/批、自动翻页,避免逐条 N+1)
金蝶已有且已审核(status=C) ──> 直接置 exist(终态),跳过推送
金蝶侧暂存/已提交(A/B,审核中) ──> 置 failed「金蝶侧该客商审核中,请稍后重推」
金蝶不存在 ──> 逐条 A902-2 客户保存 推送(收款账户按客商 id 批量一次查,组装报文避免 N+1)
成功 → synced / 失败 → failed + 原因(截断留痕)
② 已推送复查(synced)── 与③共用同一次批量 A902-1(信用代码合并去重,仅调一次接口)
status=C ──> 升级 exist(终态,A902-2 受理 B 只是中间态)
仍在审核 ──> 保持 synced 下轮再看(不重推,防金蝶重复建档);查无档案 warn 计数
③ 停用联动候选(synced/exist 且本地启用)── 复查结果 enable=0
──> TMS 档案联动停用(status=2);金蝶恢复可用不自动启用(单向,人工控制)
→ 集中批量落库:updateBatchById 更新档案 + Db.saveBatch 插入 blade_customer_change_record 变更留痕(各自 try/catch)
```
---
## 6. 任务四:收款认领定时推送(ReceiptClaimPushTask.pushClaimToKingdee)★新增
> 背景:`2e91e11` 收款认领本地化改造——页面「认领」动作只在本地登记(`blade_receipt_claim` + 分摊表),**A301-3 收款认领结果推送改为「定时 + 手动」双入口**,按收款流水归集推送。旧方案的「认领事务内同步推送」「SKCL+时间戳防重码」口径均已废弃。
实现:`ReceiptFlowServiceImpl.pushClaimByTask`(:714,定时)与 `pushClaimToKingdee`(:685,手动单条)共用推送主体 `doPushClaimToKingdee`(:820)。
### 6.1 定时扫描与编排(executeReceiptClaimPush :728)
```
候选流水(blade_kingdee_receipt_flow):bill_id 非空(已同步到公告)
+ push_status IN (none, failed)(未推送/推送失败,pushed 终态闸门不重推)
+ claim_status = claimed(认领完成是唯一可推送状态)+ 有效
→ 按「认领人」分组(blade_receipt_claim.claimer_name,有效认领 claimed),认领人姓名排序串行处理:
每个认领人一次 A301-2 收款认领公告查询(claimusers=认领人姓名,近 claimAnnounce.months 个月全量翻页)
——定时任务无登录人上下文,A301-2 的 claimusers 又是单姓名必填,故必须逐人查询
公告按公告id(bill_id)归集 → 其名下每条候选流水按 bill_id 匹配:
公告查询失败 ──> 该认领人名下流水全部跳过(计数),下轮重试
未命中公告 ──> 单条跳过(计数)
命中 ──> pushClaimLocked 推送(成功后加入本轮已推集合,同一流水被多人认领只推一次)
→ 摘要:扫描 N 条,成功 X,失败 Y,公告未命中跳过 Z
```
### 6.2 单条推送主体(doPushClaimToKingdee,事务 + 行锁)
- **并发防护**:`pushClaimLocked` 经 `TransactionTemplate` 开事务,流水行 `FOR UPDATE` 行锁覆盖闸门、对账、推送、落库全过程——防定时任务与手动按钮对同一流水双推。
- **闸门**:`push_status=pushed` 拒绝重复推送(作废重认领使条件再满足也不推,金蝶侧人工处置);公告 id 缺失拦截「请先手动同步金蝶收款流水」。
- **推送前对账**:以最新公告刷新公告侧未认领金额/公告认领状态/我已认领,并按全部分摊行重算累计认领金额与 `claim_status` 快照落库;**非 claimed(金额与公告未认领金额不一致)拦截不推**。
- **防重键 ★口径变化**:`awrp_src_code` = 该流水**全部有效分摊行的结算单号去重下划线拼接**(不再是 `SKCL+yyyyMMddHHmmss+尾号`)。分摊不变则重试编码不变,金蝶按 `awrp_src_sys + awrp_src_code` 幂等,防重试双单;失败落库后下轮自动重试复用同一编码。
- **A301-3 报文(单分录)**:`billid`=公告 id(必填);`recpaytype_number`=常量 `999`(待金蝶确认)、`paymenttype`=`bd_customer`、`recviewpayer`/`recpayer`=流水付款方名称/账号;分录仅一条,`e_receivableamt`=累计认领金额(全部未推送金额),`seq`/`e_corebillno`=第一张结算单 id(`e_corebilltype` 已注释不传)。
- **结果处理**:全项 `billStatus=true` 才算成功;成功响应 `number` 为空,金蝶收款单号取**回执键值 `keys.awrp_src_code`**。成功 → 流水 `push_status=pushed`、`kingdee_bill_no`、`kingdee_bill_status=pushed`、清失败原因,并级联刷新名下未作废认领主单冗余字段 + 记 `blade_receipt_flow_record` 流水;失败 → `push_status=failed` + 原因(截 1000 字),本地认领数据保留,等待下轮/手动重推。
- **A301-1 收款单查询 回写**(仍纯手动 `POST /receipt-claim-record/query-kingdee-result`)按 `sourcebillnumber == 流水 awrp_src_code` 匹配回写收款单号与状态(A/B→pushed、C→approved、D→received 终态、G→failed),与上表状态机衔接。
---
## 7. 配置开关汇总(Nacos thirdParty.bfi.*)
| 配置键 | 代码默认 | yaml 样例(doc/nacos/third-party-api.yaml) | 说明 |
|---|---|---|---|
| `payResult.syncEnabled` / `payResult.cron` | false / `0 */10 * * * ?` | **true**(`BFI_PAY_RESULT_SYNC_ENABLED`)/ 同左 | 付款结果回写总开关(含应收勾稽、开票回写) |
| `payResult.lockLeaseSeconds` | 1800 | — | single-flight 锁租约 |
| `receivedInvoiceResult.syncEnabled` / `.cron` | false / `0 */10 * * * ?` | — | 收票结果回写 |
| `receivedInvoice.lockLeaseSeconds` / `receivedInvoice.resultWindowDays` | 1800 / 30 | — | ⚠️ 键名无「Result」后缀;回看窗口天数 |
| `customer.syncEnabled` / `customer.cron` | false / `0 0 2 * * ?` | — | 客商同步 |
| `customer.lockLeaseSeconds` | 1800 | — | single-flight 锁租约 |
| `receiptClaimPush.syncEnabled` / `.cron` / `.lockLeaseSeconds` | false / `0 */10 * * * ?` / 1800 | false / 同左 / 1800 | 收款认领定时推送 ★新增 |
| `claimAnnounce.months` | 近 6 个月 | — | A301-2 公告查询窗口(同步/手动推送对账/定时推送对账共用) |
> 分布式锁依赖 `blade.lock.enabled=true`;未开启时 single-flight 降级为直接执行(生产必须开启)。
---
## 8. 共性设计与既有关键机制的落地状态
1. **single-flight 全覆盖**:6 条定时/手动共用链路全部经分布式锁串行化,杜绝多副本重复回写/重复推送。
2. **failed 纳入轮询的自愈纠偏**:付款申请(A103-2 查到即纠正 synced)、应收结算单(A302-4 勾稽纠偏)——金蝶侧已生成说明上次推送实际成功,查到即回填,避免人工重推产生重复单据。
3. **消失解锁兜底**(ADR-0005):金蝶侧反审核删除的单据经「连续 2 轮查询消失」解锁置失败,解决 synced/paying 单据在金蝶被删后 TMS 永久锁死的问题。
4. **终态不轮询**:paid/closed(付款)、exist(客商)、invoiced(开票)、assigned/failed+窗口外(收票)、pushed(认领流水)均退出扫描集。
5. **批量防 N+1**:50/批远程查询、页大小 200 自动翻页、收款账户/镜像/分摊/结算明细批量预取、集中批量落库。
6. **对照 09-27 总纲第 11 节的两个状态变化**:
- 11.2-#1「A801-3 定时回写按发票号匹配」缺陷 **已修复**(现按收票登记 id 匹配,`queryReceivedInvoiceBySrcCodes`);
- 11.1-#3「客商定时同步硬编码 KS000067」**已移除**。
7. **旧文档过时点**(以本文为准):09-27 定时任务全梳理未收录 `ReceiptClaimPushTask`;「收款认领三接口全部手动」「A301-3 在认领事务内推送、awrp_src_code=SKCL 随机码」的描述已被认领本地化改造取代。
@@ -0,0 +1,177 @@
# 审批通过自动同步金蝶设计(事件触发 + 定时重推 + 手动兜底)
> 整理日期:2026-10-05
> 目标:推送到金蝶的各单据,**审批通过后自动推送,省掉页面手动点「同步金蝶」按钮这一步**;节点间的先后顺序由既有硬预检 + 失败重推消化,全程无需人工点击。
> 依据:`2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版).md`、`TMS业财融合-集成流程图文字版与业务流程梳理.md`、当前代码(探索核实)。
---
## 1. 范围
| 业务域 | 推送链路 | 现状触发 | 本设计 |
|---|---|---|---|
| 正式结算单(应付/应收) | A102-4 预检 → A102-2 / A302-2 | 手动按钮 | ✅ 审批通过自动推 |
| 付款申请(正式/预付) | A102-4 硬校验 → A103-2 预检 → A103-1 / A103-3 | 手动按钮(单/批) | ✅ 审批通过自动推 |
| 开票申请 | A302-4 硬校验 → A303-4 预检 → A303-1 逐张次 | 手动按钮 | ✅ 审批通过自动推 |
| 收票登记 | A102-4 预检 + A801-3 预检 → A303-2 → A302-6 | 手动批量按钮 | ✅ 审批通过自动推 |
| 客商档案 | A902-2 | 已是审批通过自动推 | ➖ 不动(先例) |
| 收款认领 | A301-3 | 认领页面动作事务内推送 + ReceiptClaimPushTask 定时 | ➖ 不动(无审批节点) |
| 查询回写类 | A103-2 / A303-5 / A801-3 / A301-1 | 定时/手动 | ➖ 不动(不是推送) |
## 2. 现状与差距(代码核实结论)
1. **无工作流引擎**:blade-transport 全模块零 flowable 引用,审批 = 直接置 `approvalStatus=approved`(`TransportBusiConstant.java:44`)。
2. **审批有两条入口**,自动推送必须全覆盖:
- 手动:`POST /xxx/approve` → `service.approve(request)`;
- OA 回调:`MkProcessPublicController` → `MkProcessMessageService` → `MkProcessBizHandlers.approveFromProcess`。
- ⚠️ 其中 **formal-settlement 与 payment-application 的 OA handler 直接置 approved、绕过 `service.approve`**(付款侧因此漏掉票据额度联动 `useBillBalance`,结算侧不写变更记录——既有不一致,本设计顺手统一)。
3. **推送失败落库口径不一致**:付款/收票失败落 `failed`;结算单/开票失败整体回滚保持 `unsynced`("没推过"与"推失败"无法区分,总纲 11.2-#3 缺陷)。
4. **事件设施现成**:voucher 模块已有 `ApplicationEventPublisher` + `@TransactionalEventListener(AFTER_COMMIT)` 模式。
5. 审批端点全部单条,无批量审批入口。
6. 客商档案先例:审批后推送异常被吞(Controller 层 try/catch + Service 层落 failed),**推送失败不影响审批结果**。
## 3. 总体方案
```
┌─ 手动 POST /xxx/approve ─→ service.approve() ─┐
审批通过(两条入口)─┤ ├─ 事务内置 approved + 发布 KingdeeBillApprovedEvent
└─ OA 回调 approveFromProcess ─────────────────┘ (OA handler 统一改走 service.approve)
│ 事务提交(AFTER_COMMIT)
▼
KingdeeApprovalSyncListener(域开关判断 + 全量 try/catch 吞异常)
│ 复用手动按钮同一 Service 方法
┌────────────┬────────────┴──────────┬──────────────┐
▼ ▼ ▼ ▼
结算单 sync 付款申请 sync 开票申请 sync 收票 push
(A102-2/A302-2) (A103-1/A103-3) (A303-1) (A303-2+A302-6)
│ │ │ │
成功→synced 成功→synced 成功→synced 成功→pushed/assigned
失败→failed 失败→failed 失败→failed 失败→failed
└────────────┴───────────┬───────────┴──────────────┘
▼
KingdeeAutoSyncRetryTask(定时,默认关):仅重推 failed
前置未就绪(如应付单未生成)→ 预检拦截 → 再次 failed → 下轮继续
前置就绪 → 自动续推成功(硬顺序由预检保证,不做级联)
```
三条不变式:
1. **审批结果不受推送影响**:事件在审批事务提交后才消费,监听器整体 try/catch,推送异常只落库 + 记日志(客商先例)。
2. **只有审批状态真正变为 approved 的那次才发布事件**(重复回调被 approve 的状态校验拒绝,天然不重复推送)。
3. **复用手动按钮同一 Service 方法**:自动推送与手动推送走完全相同的校验、预检、回写逻辑,不另写推送路径。
## 4. 设计要点
### 4.1 触发点(覆盖双入口)
- 新增事件 `KingdeeBillApprovedEvent(String bizType, Long id)`,`bizType` 复用 `MkProcessBizHandlers` 既有取值(`formal-settlement` / `payment-application` / `invoice-application` / `invoice-receipt`)。
- 4 个实体的 `service.approve()` 在状态置 approved 成功后 `publishEvent`(须确保 approve 方法在事务内执行——均有变更记录写入,已是 @Transactional;无事务时 `@TransactionalEventListener` 默认丢弃事件,实现时核实兜底)。
- **统一 OA 回调**:`MkProcessBizHandlers` 中 formal-settlement(:309-318)与 payment-application(:372-380)的 `approveFromProcess` 改为调 `service.approve(request)`,与 invoice-application / invoice-receipt 的 handler 写法对齐。副作用即修复:付款 OA 审批补上票据额度联动 `useBillBalance`、结算 OA 审批补上变更记录。
- 客商档案已有自己的审批后推送,不动。
### 4.2 执行模型:AFTER_COMMIT 同步推送
- 监听器 `KingdeeApprovalSyncListener` 用 `@TransactionalEventListener(phase = AFTER_COMMIT)`,事务提交后在当前线程调用推送(与客商档案现网行为一致)。正常推送秒级;Feign 超时上限 10s 连接 / 60s 读取。
- `ponytail:` 同步推送占用审批请求线程,若 BFI 网关大面积变慢导致审批接口可感知变慢,再把监听器内部改为提交到守护线程池(`MkProcessMessageService` 已有同款裸 `ScheduledExecutorService` 模式,1 处改动)。
- 路由:
| bizType | 调用(与手动按钮相同) |
|---|---|
| formal-settlement | `formalSettlementService.syncKingdee(id)`(内部按 settlementType 路由 A102-2/A302-2) |
| payment-application | `paymentApplicationService.syncKingdee(id)`(内部路由 A103-1/A103-3) |
| invoice-application | `invoiceApplicationService.syncKingdee(id)` |
| invoice-receipt | `invoiceReceiptService.pushKingdeeReceivedInvoice(List.of(id))` |
### 4.3 失败统一落 failed(修 11.2-#3)
- 结算单、开票申请的推送方法改为**"事务回滚后独立事务落 failed + 失败原因"**(沿用付款申请已有 `markKingdeeSyncFailed` 模式),与付款/收票口径统一。无需新列、无需 SQL(状态字段均为 varchar,新增 `failed` 取值即可)。
- 收票自动推送遇到推送计划不满足(未恰好分摊 1 张审批通过应付结算单、查验未通过等,现写 `blockedReason` 不动状态):**自动推送路径下置 failed + 原因**,纳入重推循环;前置补齐后重推自动续上。
- 前端列表的金蝶状态筛选/展示补 `failed` 选项(结算单/开票)。
### 4.4 节点先后顺序:预检保证,不做级联
- 每个节点只在**自身审批通过**时推自己;跨节点硬顺序(A102-2→A103-1/A303-2、A302-2→A303-1)仍由既有 A102-4/A302-4 硬预检强制。
- 前置未就绪(上游还没推成)→ 本次推送失败落 failed(原因"前置应付/应收单未生成")→ 重推任务下轮再试 → 上游就绪后自动续推成功。预检与断点续传机制天然支持。
- **不做级联**(上游推完自动连推下游):失败传播复杂、且下游有自己的审批节点,级联会绕过"审批通过才推"的语义。
### 4.5 定时重推任务(兜底)
- 新增 `KingdeeAutoSyncRetryTask`(`task/` 包,@Component + @Value 开关 + @Scheduled cron,与既有任务同款)。
- **只重推 `failed`,不扫描 `unsynced`**——存量"审批通过但从未推送"的历史单不自动补推(避免上线即批量推历史数据),仍走手动按钮。
- 每域候选集(均要求未作废):
| 域 | 查询条件 | 动作 |
|---|---|---|
| 结算单 | `kingdee_sync_status='failed'` 且 approved | `syncKingdee(id)` |
| 付款申请 | `kingdee_status='failed'` 且 approved | `syncKingdee(id)` |
| 开票申请 | `kingdee_status='failed'` 且 approved | `syncKingdee(id)` |
| 收票登记 | `kingdee_push_status='failed'` 且 approved | `pushKingdeeReceivedInvoice(List.of(id))` |
- 逐条 try/catch,单条失败不中断;与付款勾稽纠偏(A103-2 查到即纠正 synced)天然兼容:重推前预检命中远端已落单会幂等回填,不会重复生成。
- 开关 `thirdParty.bfi.autoSync.retryEnabled` 默认 false,cron 默认 `0 */10 * * * ?`。
- `ponytail:` 重推无次数上限,永久性坏数据(如联行号未维护)会每 10 分钟失败一次;噪音可接受后,再加 retry_count 上限列。
### 4.6 配置开关与灰度
监听器内按域判断,4 个开关全部默认 false(Nacos 显式打开),与既有定时任务开关风格一致:
```yaml
third-party:
bfi:
autoSync:
settlement: false # 结算单审批后自动推 A102-2/A302-2
payment: false # 付款申请审批后自动推 A103-1/A103-3
invoiceApply: false # 开票申请审批后自动推 A303-1
receipt: false # 收票登记审批后自动推 A303-2+A302-6
retryEnabled: false # failed 定时重推
```
> 注意配置前缀:transport 侧读 `third-party.bfi`(BfiProperties 现行前缀,与 nacos 键对齐),落地时以实际前缀为准。
> 关闭某域开关 = 该域回到现状(审批不推送、手动按钮照用);重推开关独立,可只关重推保手动。
### 4.7 保持不变的部分
- 手动同步按钮**全部保留**(失败重推与人工兜底入口,三入口并存:自动 + 定时 + 手动)。
- 结算单汇率未维护 → 照旧"不传不阻断",不在审批侧强校验。
- 定时回写任务(PaymentKingdeeResultTask / CustomerKingdeeSyncTask / ReceiptClaimPushTask)不动。
- 状态守卫式锁单(synced 禁止作废等)不动;B901 锁单回调本分支未实现,与本设计无耦合。
## 5. 改动清单(文件级)
| # | 文件 | 改动 |
|---|---|---|
| 1 | `transport/event/KingdeeBillApprovedEvent.java` | 新增 record(bizType, id) |
| 2 | `transport/listener/KingdeeApprovalSyncListener.java` | 新增:AFTER_COMMIT 监听 + 域开关 + 路由 4 个 sync 方法 + 全量 try/catch |
| 3 | `FormalSettlementServiceImpl.approve`(:539) | 末尾发布事件 |
| 4 | `PaymentApplicationServiceImpl.approve`(:400) | 末尾发布事件 |
| 5 | `InvoiceApplicationServiceImpl.approve`(:483) | 末尾发布事件 |
| 6 | `InvoiceReceiptServiceImpl.approve`(:587) | 末尾发布事件 |
| 7 | `MkProcessBizHandlers`(formal-settlement :309 / payment-application :372) | `approveFromProcess` 改调 `service.approve(request)`(统一入口 + 修复 useBillBalance 绕过) |
| 8 | `FormalSettlementServiceImpl` / `InvoiceApplicationServiceImpl` 推送失败路径 | 事务回滚后独立事务落 `failed` + 原因(沿用 `markKingdeeSyncFailed` 模式) |
| 9 | `InvoiceReceiptServiceImpl` 推送计划 blocked 分支 | 自动推送路径 blocked → failed + 原因 |
| 10 | `transport/task/KingdeeAutoSyncRetryTask.java` | 新增:4 域 failed 重推,开关 + cron |
| 11 | 前端结算单/开票列表 | 金蝶状态筛选/展示补 `failed`(小改) |
无新表、无 DDL、无新依赖。
## 6. 边界场景推演
| 场景 | 行为 |
|---|---|
| 推送成功但审批事务实际回滚(AFTER_COMMIT 后回滚不存在,理论不发生) | 不适用——事件只在事务成功提交后消费 |
| 推送成功、落 failed 前进程崩溃 | 下轮重推预检命中(A102-4/A103-2/A303-4/A801-3)幂等回填,不重复建单 |
| 付款审批通过但结算单应付单未生成 | A102-4 硬校验拦截 → failed("前置应付单未生成") → 重推任务续推 |
| 结算单推送失败挂 failed,下游付款/收票/开票全failed等它 | 重推任务每轮都重试各域 failed,上游成功后下游自动续上 |
| OA 审批通过的单 | OA handler 统一走 service.approve 后同样触发事件,覆盖 |
| 同一单重复 OA 回调 | approve 状态校验拒绝重复审批,不发布事件 |
| 永久性坏数据(如联行号未维护) | 每轮重推失败落原因,修数后下轮自动成功;重推无上限(见 4.5 天花板) |
| 历史存量 approved-unsynced 单 | 不自动补推,手动按钮处理 |
## 7. 待确认默认值(按推荐默认落地,可改)
| 默认 | 内容 | 备选 |
|---|---|---|
| D1 | AFTER_COMMIT 同步推送(同线程) | 改守护线程池异步(1 处改动,见 4.2) |
| D2 | OA handler 统一改走 service.approve(顺修 useBillBalance 绕过) | 保持 handler 直置、在各 handler 单独发事件 |
| D3 | 重推只扫 failed,存量 unsynced 不补推 | 加"存量补推"一次性开关 |
| D4 | 收票 blocked 条件在自动推送时置 failed | 保持 blockedReason 不动状态 + 重推扫 blocked |
| D5 | 重推无次数上限 | 加 retry_count 上限 |
@@ -0,0 +1,91 @@
# 金蝶票池选票回切镜像读(本地真分页 + 票号回源 + 瘦身同步 + 认领闸门)业务流程
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport;前端:tms-erp-web)
> 对接系统:金蝶发票云旗舰版·票易通(piaozone)全票池 A801-1,经业财融合 BFI 网关(rim 契约)透传
> 关联文档:`2026-09-22-金蝶票池选票统一查镜像…业务流程.md`(2026-09-22 定稿镜像读版、同日改写为实时版,本文第三次定稿)、`2026-09-23-金蝶票池选票方案对比评估与优化建议(镜像读+回源+保存重查闸门).md`(本方案评估依据)、`2026-09-20-收票管理同步金蝶全票池发票…业务流程.md`(首版同步体系,绑定/补偿/白名单部分维持退役)
> 设计依据:2026-10-05 grilling 会话定稿(Q1 回源 / Q2 瘦身同步 / Q3 双闸门 / Q4 全恢复条件能力 / Q5 邻接功能不动不留开关 / Q6 新分支 / Q7 手动 bootstrap)
> 最后更新:2026-10-05
---
## 1. 背景与决策
实时透传方案(2026-09-22 二次决策)运行暴露:每次开弹窗/翻页/保存都是一次 BFI 网关远程链路(秒级)、无 total 游标翻页不可跳页、名称模糊与认领/登记过滤被删、金蝶抖动期间选票整体瘫痪。2026-09-23 评估文档结论成立:**该回切的是"查询架构",不该回切的是"同步体系负担"**,且保存重查闸门是两版方案中唯一资金安全级防线。
**定稿口径**:
- 收票登记新增、正式结算单认领两处选票回切**查本地镜像** `blade_kingdee_invoice_pool`:本地 MySQL 真分页(total/跳页)、名称模糊、票种、双税号、claimed/registered 三态过滤全部恢复;**仅出可用票**(原 `isUsablePoolRow` 口径本地化执行:`available_amount > 0` 且 `amount_use_status = '0'`)。
- **票号未命中回源**:票号精确查本地无结果(且无认领/登记过滤条件)时,窄查一次 A801-1(票号全等防御 + 可用复核)→ upsert 入镜像 → 本地重查返回。兜底增量窗口缝隙内的当日新票;名称模糊本地为空**不回源**(远程不支持名称条件,救不了)。
- **瘦身版定时同步**回归:只保留「增量窗口分页拉取 + 镜像全列批量 upsert」,**不恢复**绑定回填、漏绑补偿、双白名单、指纹比对(保存即写存根绑定,登记侧无定时绑定需求;全列覆盖消灭指纹清单漏字段静默不同步的坑)。
- **双闸门保留/补齐**:收票保存重查(`persistMirrorFromRemote`)原样保留;结算认领确认(`claimInvoices`)补齐服务端按票号逐张重查校验——窗口拉取口径 `invoiceStatus="0"`,被删除/红冲票不再出现在拉取结果,镜像可能陈旧,资金安全由闸门保证。
## 2. 接口契约(后端)
| 端点 | 说明 |
| --- | --- |
| `GET /invoice-receipt/invoice-pool`、`GET /formal-settlement/invoice-pool` | 镜像统一查询,收参 `KingdeeInvoicePoolQueryDTO`,返回 `R<IPage<KingdeeInvoicePoolVO>>`(records/total/size/current) |
| `POST /invoice-receipt/sync-invoice-pool` | 手动触发全票池同步(bootstrap/联调/补偿三用),返回执行摘要 |
- **已删除**:游标契约 `KingdeeInvoicePoolPageVO`(hasMore/nextCursor)与 DTO `cursor` 字段、实时链路 `PoolScanCursor`/`toPoolViewVO`/`decorateUseFlags`。
- `KingdeeInvoicePoolQueryDTO` 条件:`current/size`(size 钳位 1~100)、`invoiceNo`(模糊)、`invoiceDateStart/End`(服务端强校验 yyyy-MM-dd)、`salerName/buyerName`(名称模糊)、`invoiceType`(票易通编码,匹配镜像 `invoice_type_code`)、`salerTaxNo/buyerTaxNo`(精确)、`claimed/registered`(三态过滤)。
- **KingdeeInvoicePoolMapper.xml**:`selectInvoicePoolPage` 单条 SQL 承载全部口径——`usableFilter`(`available_amount > 0 AND amount_use_status = '0'`)+ claimed/registered EXISTS 子查询(剔除作废结算单/作废登记,手工带租户与逻辑删除条件)+ 动态条件,`ORDER BY source_updated_time DESC`。
- VO 展示列:`availableTaxAmount`(剩余可用金额,金蝶可用税额口径,仅展示)与 `availableInvoiceAmount/matchedAmount`(价税合计口径,认领金额校验依赖)并存。
## 3. 保存与认领闸门
```
收票保存/提交(不变):persistMirrorFromRemote 按票号重查 A801-1
→ 未命中/delete=1 中止;isUsablePoolRow 复核(金额可用 0/使用状态变更中止)→ 写存根 + 绑定
结算认领确认(新增):claimInvoices 校验后逐张 assertInvoicesClaimable
→ 窄查 A801-1 + 票号全等防御 + isUsablePoolRow 同口径复核,任一不通过 ServiceException 中止
```
闸门数据不回写镜像(认领侧只校验不落库);可用性陈旧窗口由闸门兜底,无资损路径。
## 4. 定时同步(瘦身版)
```
InvoiceReceiptKingdeeSyncTask(@Scheduled cron,thirdParty.bfi.invoiceReceipt.cron 默认每30分钟,syncEnabled 默认关闭)
→ IInvoiceReceiptService.syncKingdeeInvoicePool()(KingdeeSyncLockSupport.runSingleFlight 单飞,锁键按租户)
→ executePoolSync():滚动近 incrementDays 天(默认30)窗口
① 发票种类白名单下推(thirdParty.bfi.invoiceReceipt.invoiceTypes,单值参数按编码拆轮,每轮独立额度;空不限制)
② updateTime 切片续扫(syncWindowSlice):单查询窗口受 50页×500条=2.5万硬上限,触顶自动二分
[start,mid)+[mid,end) 递归续扫(右片不含 mid 不重不漏;细到 1 秒仍满页防御报 error 跳过)
出站口径 invoiceStatus="0",不推 expenseStatus(在用/已用票随 updateTime 进窗口刷新使用状态,本地过滤自然排除)
→ batchUpsertInvoicePool:同页号码一次批量查既有镜像 → fillInvoicePool 全列填充 → 有变化才写:
轻量比对 serialNo/价税合计/税额/剩余可用金额/金额使用状态(compareTo 比金额,空对空不算变化),
无变化跳过 UPDATE(消整窗重复拉取的无效写);有变化 Db.updateBatchById、新增 Db.saveBatch
→ 已登记票跟随补绑:同页票号批量定位登记,仅未绑定存量登记补绑+写 sync 流水(页级事务批量落库)
→ 失败中断本轮由任务层记日志,下轮幂等重试
```
- 手动端点与定时任务共用单飞锁;`blade.lock.enabled=false` 时降级无锁并 warn(与付款回写/客商同步同模式)。
- 删除/红冲盲区:拉取不到的票镜像保持旧值看似可用——展示层陈旧可接受(无 `source_synced_time` 新增列,复用 `source_updated_time` 排序),资金安全由第 3 节双闸门保证。
## 5. 数据库与配置
| 项 | 内容 |
| --- | --- |
| 镜像表新增列 | `available_amount` decimal(18,2)、`amount_use_status` varchar(8),幂等脚本 `doc/sql/transport/blade_kingdee_invoice_pool_available_20261005.sql`(可重跑) |
| 新增索引 | `blade_formal_settlement_invoice (tenant_id, invoice_no)`(认领过滤 EXISTS 走索引) |
| Nacos | `third-party-api.yaml` 新增 `thirdParty.bfi.invoiceReceipt.*`:`syncEnabled`(false)/`cron`(每30分钟)/`incrementDays`(30)/`invoiceTypes`(白名单编码,如 26,27,下推远端按编码拆轮)/`lockLeaseSeconds`(1800) |
| 存量数据 | 存量存根行两列为 NULL,不参与选票(可用过滤排除),随增量窗口同步自然回填;回切当天手动跑一次 `POST /invoice-receipt/sync-invoice-pool` bootstrap 铺底 |
## 6. 前端统一组件(`src/components/kingdee-invoice-pool-dialog/main.vue`)
- 分页区回切 `el-pagination`(total/sizes/prev/pager/next/jumper,可跳页);游标机件(nextCursor/pageCache/上一页缓存回显)删除。
- 条件区恢复:开票单位/受票单位名称模糊 + 认领状态/登记状态三态下拉;保留:票号、日期区间(默认近一月)、双税号、发票类型(编码下拉 26/27)。
- 保留实时版增强:剩余可用金额列、认领/登记 tag、附件查看/下载(A801-2 代理)、`rowKey = id || invoiceNo`(回源响应理论上有 id,兜底不变)。
- 页脚提示"按名称检索范围为已同步票,未找到请改用发票号码或税号精确查找"。
- 接线不变:收票登记表单(单选)、结算单编辑器(多选、`handleInvoiceClaimConfirm` 归一 `attachmentJson`)。
## 7. 邻接功能(本次不动)
A801-2 附件下载(镜像优先/实时回源,镜像覆盖面恢复后受益)、A801-5 黑名单查询(实时全量)、收票推送链 A303-2/A302-6/A801-3(消费镜像 `invoice_type_code/items_json`,不受影响)、收票保存重查与存根写入语义。
## 8. 已知限制
- 名称模糊仅检索已同步票(组件页脚已声明);同步缝隙内新票靠票号回源兜底,纯浏览需等下一轮同步。
- 30 分钟全列重写为已知写放大(千~万行级,成本可接受;量级上来后升级为整行载荷规范化哈希,**不恢复**人工字段清单式指纹)。
- 首轮 bootstrap 前无条件浏览为空表(上线操作项,见第 5 节)。
- 后端验证:`mvn compile -DskipTests -pl blade-service/blade-transport -am` 通过;前端 `pnpm run build` 通过。
@@ -0,0 +1,153 @@
# 金蝶单据锁定与解锁业务流程(lockBusiData 锁定 - unLockNotifyBusiData 回调 - 失败补偿 - 删单还原)
- 日期:2026-10-09(2026-10-10 变更:① lockBusiData 确认为 1对1 接口,不支持逗号拼接批量,锁定/补偿改为逐张调用;② 付款申请单纳入锁单范围并完成接线)
- 范围:正式结算单(应付 A102-2 / 应收 A302-2)、开票申请单(A303-1)、付款申请单(A103-1 应付下推 / A103-3 直接保存,2026-10-10 新纳入)推送金蝶成功后的单据锁定、金蝶解锁/删单回调、锁定失败定时补偿
- 设计文档:`docs/superpowers/specs/2026-10-09-kingdee-bill-lock-unlock-design.md`
- 锁记录表:`blade_kingdee_bill_lock`(一行 = 一张金蝶目标单)
## 一、涉及单据与触发链路
| 业务单据 | 推送接口 | 推送入口 | 金蝶目标单 | 锁定触发 |
| --- | --- | --- | --- | --- |
| 正式结算单(应付) | A102-2 财务应付单保存 | 结算单页面「同步金蝶」 | ApFin_other_BT_S | 推送成功后 |
| 正式结算单(应收) | A302-2 财务应收单保存 | 同上 | arfin_other_BT_S | 推送成功后 |
| 开票申请单 | A303-1 开票申请新增 | 开票申请页面「同步金蝶」 | cdm_paybill_bank_elect_BT_S(每张发票张次一张) | 推送成功后 |
| 付款申请单 | A103-1 应付下推(正式结算付款)/ A103-3 直接保存(预付款) | 付款申请页面「同步金蝶」(单笔/批量) | ap_payapply_oth_BT_S(每张付款申请一张) | 推送成功后 |
- lockBusiData:WLTMS 调金蝶,源系统 WLTMS、目标系统 XH、lockAction=LOCK
- unLockNotifyBusiData:金蝶调 WLTMS,源系统 XH、目标系统 WLTMS、lockAction=UNLOCK
- 传参约定:接口 1对1,单次仅一个源单号(sourceBillNo)对一个金蝶单号(targetBillNo);多张金蝶单(开票申请一张主单对 N 张金蝶单)逐张调用,单张成败互不影响,成败结果批量落库
- 付款申请天然 1对1:一张付款申请对应一张金蝶付款申请单(A103-1/A103-3 均同类型),源单号=付款申请号 paymentNo
- WLTMS 单据类型常量已预留:`KingdeeLockConstant.SOURCE_PAY_APPLY`(PAY_APPLY)/`TARGET_PAY_APPLY`(ap_payapply_oth_BT_S),且已在 `TARGET_TO_SOURCE_MAP` 反查映射中
- 出入参同构,应答为 `{updateCount, updateResultList}` 裸结构(逐单 1-成功/0-失败)
## 二、主线流程
### 1. 推送并锁定(WLTMS → 金蝶)
```
用户点「同步金蝶」
├─ doSyncKingdee(单据级事务):幂等预检 → A102-2/A302-2/A303-1/A103-1/A103-3 保存 → 回写 kingdeeBillNo/同步状态=synced
│ └─ 事务提交
├─ 锁定(lockAfterPush):
│ ├─ 每张金蝶目标单一行锁记录,先落 failed 占位(保证补偿扫表无遗漏)
│ ├─ 接口1对1:逐张调用 LOCK + SAVE,单张成败互不影响
│ ├─ 单张应答成功 → 该张翻 locked ──→ 单据冻结
│ └─ 单张失败/超时 → 保持 failed,tryCount+1,失败原因落 fail_reason(主单同步状态不受影响)
└─ 附件联带上传(A902,原有动作,后置不受影响)
```
**锁定后的效果**:
- 开票申请:`作废`入口被 `checkEditable` 拦截(提示"该单据已被金蝶锁定,不可修改")
- 结算单:synced 状态本身已拦截编辑/调整/作废,锁定期被同一组守卫覆盖
- 付款申请:`作废`入口被 `checkEditable` 拦截(金蝶锁定中不可改动;草稿保存/删除仅限推送前,无需守卫)
- 重复点「同步金蝶」:推送幂等命中直接返回已有单号,已 locked 的锁记录跳过,不会重复锁
### 2. 金蝶侧解锁回调(金蝶 → WLTMS)
金蝶在星瀚侧对单据解锁或删单后,回调 `POST /kingdee-bfi/unLockNotifyBusiData`(`UNLOCK`;targetBillNo 兼容单张/逗号拼接):
```
校验:sourceSystem=XH、targetSystem=WLTMS、lockAction=UNLOCK(否则应答失败)
│
├─ operationType=SAVE(仅解锁,没删单)
│ └─ 锁记录 → unlocked,记录回调操作人/时间,解锁动作留痕
│
└─ operationType=DELETE(金蝶把单删了)
├─ 锁记录 → unlocked(同上)
└─ 按 targetBillType 反查 WLTMS 单据类型,分发还原:
├─ SETTLEMENT → 结算单作废(当前节点→已作废),
│ 同步信息还原推送前状态:kingdeeSyncStatus→unsynced、
│ 清 kingdeeBillNo/kingdeeBillId/syncedTime
│ 变更流水记"金蝶删单回调自动废除",操作人=金蝶报文 operator
├─ INVOICE_APPLY → 开票申请作废,kingdeeStatus→unsynced、清单据编号,
│ 刷新关联结算单开票状态,动作流水记 kingdee_delete
└─ PAY_APPLY → 付款申请复用 unlockBill(id, "金蝶删单回调")(ADR-0005 口径):
kingdeeStatus→生成失败、清 kingdeeBillId/kingdeeBillNo(原值日志留档),
不作废源单、不自动重推,人工核实后重推(A103-2 勾稽预检防重复推单)
应答:{updateCount: 处理张数, updateResultList: [1]}
```
重复回调幂等:单据已作废或锁记录已 unlocked → 该张跳过,不重复还原。
### 3. 锁定失败补偿(定时兜底,不依赖人工)
```
KingdeeBillLockRetryTask(thirdParty.bfi.billLock.retryEnabled,默认每5分钟)
└─ 扫 lock_status=failed 且 try_count<5 的锁记录
├─ 接口1对1:逐张重调 lockBusiData,单张成败互不影响;成败结果批量落库
├─ 成功 → 翻 locked,进入正常锁定态
└─ 失败 → try_count+1,超过 5 次停试,留 fail_reason 待人工排查
```
## 三、状态视角
**锁记录状态机**:
```
failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked(终态)
```
同单重新推送生成新锁记录,走新一轮循环。
付款申请双路解锁共用同一还原口径(unlockBill,ADR-0005):① 金蝶删单/解锁回调(本流程);② A103-2 定时回写"已同步单据连续两轮查询消失"兜底。两路幂等由 kingdeeStatus 状态机保证,非已生成/付款中状态自动跳过。
**业务单据生命周期**(以结算单为例):
```
草稿 → 提交 → 审批通过 ──同步金蝶──→ synced(金蝶单已生成)
│ │
│ ├─ 锁定成功:locked(冻结,不可动)
│ │ │
│ │ ├─ 金蝶解锁(SAVE):unlocked,流水留痕
│ │ └─ 金蝶删单(DELETE):作废 + 同步信息还原 unsynced
│ │ (结算明细/付款/开票关联的后续动作
│ │ 由各自前置校验自然拦截重推)
│ └─ 锁定失败:failed,定时补偿重锁
└─ 推送失败:failed(可重推)
```
## 四、关键设计点
1. **先占位再锁**:锁记录落库先于远程调用,补偿任务扫 `failed` 就是完整的待重试集合,不漏单
2. **锁失败不回滚推送**:金蝶单已生成,本地回滚只会制造断链;代价是短暂"已推未锁"窗口,由补偿任务闭合
3. **1对1逐张锁**:lockBusiData 单次仅支持一个源单号对一个金蝶单号,逐张调用互不连坐;成败结果 updateBatchById 批量落库,避免逐张 update
4. **回调幂等**:按 targetBillType+targetBillNo 找最新 locked 记录,找不到/已 unlocked 直接跳过应答成功,防金蝶重试风暴
5. **DELETE 还原口径**:作废源单 + 清金蝶同步信息 + 关联状态刷新;作废是终态,重推需新单重走流程(口径待业务联调确认)
6. **前端锁状态展示**(2026-10-10 新增):结算单/开票申请列表与详情 VO 填充 `kingdeeLockStatus`/`kingdeeLockFailReason`/`kingdeeTryCount`
(后端 `mapActiveLocks` 每页一次批量查锁表,无 N+1;unlocked 终态不返回即 null=无锁/已解锁)。
前端渲染:`locked` 红 tag「金蝶锁定中」并禁用编辑/作废(后端守卫是权威);`failed` 且 `tryCount<5` 橙 tag「锁定失败·补偿中」tooltip 显示原因;
`failed` 且 `tryCount≥5` 红 tag「锁定失败·已达重试上限」提示重推同步金蝶;null 不显示
7. **付款申请纳入**(2026-10-10 新增):一张付款申请=一张金蝶付款申请单,1对1约定天然满足,无需逐张拆分;
DELETE 还原不作废源单,复用既有 `unlockBill`(ADR-0005:生成失败+清编号内码+人工重推),与 A103-2 反审核删除兜底同口径;
推送结果即时回填 kingdeeBillNo(`requireBfiBillIdAndFillNo`),推送事务提交后即以该单号 `lockAfterPush`;
锁定失败摘要拼入「同步金蝶」返回值(与开票申请同款),页面直接可见
## 五、代码落点速查
| 内容 | 位置 |
| --- | --- |
| 锁单 Feign/DTO/应答 VO | `blade-bfi-api`:`BfiBillLockDTO`、`BfiBillLockResultVO`、`IBfiClient.lockBusiData`、`IBfiService.lockBusiData` |
| 锁类型常量与映射 | `transport/constant/KingdeeLockConstant`(PAY_APPLY/ap_payapply_oth_BT_S 已进 TARGET_TO_SOURCE_MAP) |
| 锁状态展示填充 | `IKingdeeBillLockService.mapActiveLocks`(批量查活跃锁)+ `FormalSettlementServiceImpl/InvoiceApplicationServiceImpl/PaymentApplicationServiceImpl.fillKingdeeLockStatus`(列表/详情填充)+ 三个 VO 的 `kingdeeLockStatus/kingdeeLockFailReason/kingdeeTryCount` 字段 |
| 锁记录实体/服务 | `KingdeeBillLock`、`IKingdeeBillLockService` + `KingdeeBillLockServiceImpl` |
| 回调端点 | `KingdeeBfiPublicController`:`POST /kingdee-bfi/unLockNotifyBusiData`(PERMIT_ALL + TenantIgnore) |
| 补偿定时任务 | `KingdeeBillLockRetryTask`(`thirdParty.bfi.billLock.retryEnabled`,默认每5分钟) |
| 推送后锁定触发 | `FormalSettlementServiceImpl.syncKingdee`、`InvoiceApplicationServiceImpl.syncKingdee`、`PaymentApplicationServiceImpl.syncKingdee`(均为事务提交后) |
| 删单还原 | `FormalSettlementServiceImpl.voidByKingdee`、`InvoiceApplicationServiceImpl.voidByKingdee`;付款申请复用 `PaymentApplicationServiceImpl.unlockBill`(ADR-0005 口径,回调分发于 `KingdeeBillLockServiceImpl.restoreOnKingdeeDelete`) |
| 建表脚本 | `doc/sql/transport/blade_kingdee_bill_lock_20261009.sql` |
## 六、联调前配置
1. Nacos 配置 `thirdParty.bfi.billLockUrl`:lockBusiData 真实路径(代码默认占位 `/ierp/kapi/app/billLock/lockBusiData`,以金蝶最终提供为准)
2. Nacos 开启补偿:`thirdParty.bfi.billLock.retryEnabled: true`(按环境)
3. 向金蝶提供回调地址:网关地址 + transport 服务路由 + `/kingdee-bfi/unLockNotifyBusiData`;
免登免授权已三层配置(照 BladeX 两层鉴权模型):
① 第一层 Token 放行:`blade.secure.skip-url` 放行 `/blade-transport/kingdee-bfi/**` 与 `/kingdee-bfi/**`;
② 第二层注解放行:端点类级 `@PreAuth(AuthConstant.PERMIT_ALL)`(即 permitAll())+ `@TenantIgnore`;
③ 框架层规则兜底:`blade.secure.auth` 追加两 pattern 的 `permitAll()` 规则(模板与 Nacos 实际均已配)。
Nacos 实际配置已核对一致;环境级 blade-dev/test/prod.yaml 无 secure 段覆盖。
联调验证:服务启动后无令牌 POST 应返回业务参数校验错误(JSON),返回 401「请求未授权」即白名单未生效
4. 与金蝶确认 WLTMS 单据类型编码(SETTLEMENT/INVOICE_APPLY/PAY_APPLY)与 DELETE 还原口径
(付款申请 DELETE 不作废源单、走生成失败+人工重推,与 A103-2 反审核删除兜底同口径,待业务确认)
@@ -0,0 +1,177 @@
# 金蝶应付/应收/收票单明细分录拆分与物料映射逻辑梳理(含取值口径)
> 更新日期:2026-10-13
> 适用链路:财务应付单(A102-2)、财务应收单(A302-2)、收票单(A303-2)明细分录
> 核心代码:`blade-transport` 模块 `org.springblade.transport.support.KingdeeApBillSyncSupport`(共享解析)
---
## 1. 总体设计
应付单、应收单、收票单三个金蝶单据的明细分录**共用同一份解析结果**(`KingdeeApBillSyncSupport.BfiEntryGroup`),拆分规则、物料映射、计量单位、税率分组只在应付支撑组件解析一次,三个消费端(`pushApBill` / `pushArBill` / `pushReceivedInvoice`)直接逐条转 DTO,保证三单明细同源同口径、分录标识同键、金蝶侧逐行勾稽。
```
正式结算单(blade_formal_settlement)
├── 结算明细行(blade_formal_settlement_detail,一个运单/一个车牌)
│ └── 货物费用行(blade_formal_settlement_detail_fee,一车多货按货物拆)
└── 结算合计费用项(blade_formal_settlement_summary_fee,仅取手动添加 manualFlag=1)
↓ 解析(KingdeeApBillSyncSupport.resolveEntryGroups)
BfiEntryGroup 列表(货物 → 费用大类 → 税率 三级拆分后)
↓ 同一份结果
应付单 A102-2 / 应收单 A302-2 / 收票单 A303-2 明细分录
```
---
## 2. 取值来源总表
| 数据 | 来源表/配置 | 字段 | 用途 |
|---|---|---|---|
| 结算明细行 | `blade_formal_settlement_detail` | `document_no` 单据号、`vehicle_no` 车牌号、`transport_type` 运输方式、`cargo_name` 货物名称、`transport_quantity` 运输量、`fee_items_json` 费用项JSON、`settlement_amount_tax/no_tax` 结算金额(含税/不含税) | 无货物费用行时的唯一货物行;车号/运输方式/数量取数 |
| 货物费用行 | `blade_formal_settlement_detail_fee` | `cargo_name`、`transport_quantity`、`fee_items_json`、`settlement_amount_tax/no_tax`、`line_no` | 一车多货按货物拆行的金额与数量来源 |
| 结算合计费用项 | `blade_formal_settlement_summary_fee` | `fee_type` 费用类型、`fee_item` 费用项、`settlement_amount`、`manual_flag` | 手动添加费用(manualFlag=1)每行单独一条分录 |
| 合同 | `blade_contract_manage` | `billing_plan_json` 计费方案JSON(规则含 `feeType`/`feeItem`/`billingUnit`/`taxRate`) | 税率分组、计量单位匹配 |
| 计量单位档案 | system 服务 `getMeasurementUnits()` | 单位名称 → `unit_code` | 计费单位 → 金蝶计量单位编码 |
| 费用项档案 | `blade_fee_item`(system 服务 `getFeeItems()`) | `name` 费用项名、`fee_category` 费用类型编码、`tax_rate` 税率(备用) | 费用大类判定 |
| 业务字典 | 字典 `fee_category` | 编码 → 名称(如"装卸"、"物流配送") | 费用类型编码翻译成名称后做大类判定 |
| 项目 | `ProjectApply`(`project_id` 关联) | `business_mode` 业务模式、`business_type` 业务类型 | 国内/国际运输判定(含"国际"即国际) |
| 物料/计量单位映射 | Nacos `thirdparty.bfi.material.*`(`BfiProperties.Material`) | 见第 6 节 | 物料编码、计量单位编码映射 |
`fee_items_json` 结构:`{"费用项名": 金额, ...}`(如 `{"运输费": 3120, "装卸费": 800}`),保持行序解析。
---
## 3. 明细拆分规则(核心逻辑)
### 3.1 三级拆分:货物 → 费用大类 → 税率
对每条结算明细行(一个运单,同一车牌号就是一个车):
1. **按货物拆**(一车多货):存在货物费用行(`blade_formal_settlement_detail_fee`)时,每个货物费用行单独拆;无货物费用行时结算明细行本身即唯一货物。
2. **按费用大类拆**:货物行的 `fee_items_json` 里逐个费用项判定大类(见第 4 节)——装卸大类、运输大类(非装卸全部归运输类)。
3. **按税率拆**:同大类内按计费方案税率分组,**税率不同分开多行**;费用项未配置税率时归入该大类"无税率组"(不拆)。
### 3.2 各字段取值
| 分录字段 | 运输大类行 | 装卸大类行 | 结算合计手动费用行 |
|---|---|---|---|
| 物料编码 | 按项目业务模式/运输方式映射(第 6 节) | `material.loading` 装卸物料 | 非运费类→`material.loading`;运费类→同运输行映射 |
| 计量单位 | 费用项/费用类型匹配合同计费方案计费单位 → 匹配计量单位档案编码 | 同左 | 同左 |
| 数量 | **运单运输量**(`transport_quantity`,未维护兜底 1),按税率拆多行时**每行都取运输量** | 1 | 1 |
| 含税金额 | 组内费用项金额合计 | 组内费用项金额合计 | 合计行 `settlement_amount` |
| 单价 | 金额 ÷ 数量(2 位小数反算) | 金额 ÷ 1 = 金额 | 金额 ÷ 1 = 金额 |
| 不含税金额 | **仅该货物行第一条运输类分录携带**整行 `settlement_amount_no_tax`(拆分行无分类不含税金额,保证不含税合计勾稽),其余行不传 | 不传 | 不传 |
| 运输货物 awrp_transport | 货物名称 | 货物名称 | 不传 |
| 车号 awrp_lpn | 结算明细车号(整单一个车) | 结算明细车号 | 不传 |
| 分录标识 awrp_entry_code | `单据号-货物序号-运输费[-税率]` | `单据号-货物序号-装卸[-税率]` | `结算单号-费用类型[-费用项]`(不变) |
- 货物序号:货物费用行按行序 1、2、3…;无费用行时为 1。单据号为空兜底明细行 id。
- 税率后缀:`-9`、`-6.5` 格式(去尾零);税率未配置不拼。
- entryCode 是金蝶候选键,收票单与应付单分录同键勾稽。
### 3.3 残差勾稽
货物行结算金额(含税)与费用项金额合计的**差额(残差)并入运输类无税率组**,保证:同一货物行拆出的全部分录含税金额合计 = 该行结算金额。典型场景:历史数据 `fee_items_json` 未含"运输费"项(运费金额在 `freight_amount` 字段)时,自动补一条运输类无税率分录承接差额。
### 3.4 三个标准场景
**场景 1:同费用大类、多税率(运输费存在 2 种税率)**
运输费 2000 元@9% + 附加运输补贴 1120 元@6%,运单 10 吨(水路-国内)→ 拆 2 行,每行数量均取运输量 10:
- 行 1:物料=水路运输编码,数量 10,金额 2000,单价 200
- 行 2:物料=水路运输编码,数量 10,金额 1120,单价 112
**场景 2:包含装卸大类**
运输费 3120 元@9% + 装卸费 800 元@6%,10 吨 → 拆 2 行:
- 运输行:物料=水路运输编码,数量 10,单价 312,金额 3120
- 装卸行:物料=装卸编码(XDWL11002004),数量 1,单价 800,金额 800
**场景 3:结算单手动添加费用**
手动新增仓储服务费 500 元(非运费类)→ 单独一行:物料=装卸编码,数量 1,单价 500,金额 500。
---
## 4. 费用大类判定(档案 + 字典优先,关键字兜底)
判定链路(`isLoadingCategory` / `isFreightCategory`):
```
费用项名 → blade_fee_item.fee_category(费用类型编码)
→ 字典 fee_category 翻译成费用类型名称
→ 名称含"装卸" → 装卸大类;否则运输大类
费用项不在档案 → 回退名称关键字判定(含"装卸"=装卸类;含"运费/运输费"=运费类)
```
- 判定对象从"费用项名"升级为"档案翻译后的费用类型名称":费用项叫"吊装作业"但档案归到"装卸"类型,也会正确映射装卸物料。
- 结算合计手动费用行的运费判定:档案分类名称 / `fee_type` / `fee_item` 任一含"运费"或"运输费" → 运费类映射运输物料;否则装卸物料(场景 3)。
- 档案读取失败直接拦截推送(与计量单位档案口径一致);分类映射缓存在 `KingdeeSyncContext.feeCategoryByName`,批量同步只加载一次。
---
## 5. 税率取值
- **来源**:合同计费方案 JSON(`blade_contract_manage.billing_plan_json`),每条规则的 `taxRate` 字段(百分数文本,"9" = 9%),按规则的 `feeItem`/`feeType` 建 Map(先到先得)。合同保存时已强制校验税率 0~100、最多 2 位小数。
- **用途**:仅用于**拆行分组**和分录标识后缀,**不传金蝶**——分录 DTO 的 `taxrateid_number` 为待金蝶确认字段,暂不送。
- **匹配方式**:按费用项名精确匹配;未配置税率的费用项归入无税率组。
- 结算合计手动费用行不取税率(每行单独一条,不分组)。
---
## 6. 物料编码映射(Nacos 可配)
配置前缀 `thirdparty.bfi.material.*`(`BfiProperties.Material`,代码带默认值,Nacos 覆盖即改,无需改代码):
| 配置项 | 默认值 | 说明 |
|---|---|---|
| `domestic."["公路运输"]"` | XDWL11010001 | 国内道路货物运输服务—公路运输 |
| `domestic."["铁路运输"]"` | XDWL11010002 | 国内道路货物运输服务—铁路运输 |
| `domestic."["水路运输"]"` | XDWL11010003 | 国内道路货物运输服务—水路运输 |
| `domestic."["航空运输"]"` | Item-00001082 | 国内道路货物运输服务—航空运输 |
| `international` | Item-00001081 | 国际货运代理服务费(业务模式/业务类型含"国际") |
| `loading` | XDWL11002004 | 装卸(装卸大类 + 手动添加非运费杂费) |
映射优先级(运输类):装卸大类 → `loading`;国际业务 → `international`;国内 → `domestic[运输方式]`,**运输方式未配置映射直接拦截报错(无兜底)**。物料编码缺失直接拦截报错。
**计量单位取值链路**:费用项/费用类型匹配合同计费方案 `billingUnit` → 按计量单位档案(单位名称匹配)取 `unit_code`;任一环节缺失拦截报错。
**⚠️ Nacos 配置坑**:`domestic` 的中文 key **必须用方括号写法** `"[公路运输]": xxx`,裸写中文 key 会导致 Spring Boot 绑定失败、服务无法启动(`No converter found from String to Map`)。
---
## 7. 消费链路与同源关系
| 单据 | 组件/方法 | 分录取数 |
|---|---|---|
| 财务应付单 A102-2 | `KingdeeApBillSyncSupport.pushApBill` | `entryGroups` 逐条转 `BfiApBillSaveEntryDTO` |
| 财务应收单 A302-2 | `KingdeeArBillSyncSupport.pushArBill` | 同一解析(`resolveArEntryGroups` 复用应付解析),逐条转 `BfiArBillSaveEntryDTO` |
| 收票单 A303-2 | `KingdeeReceivedInvoiceSyncSupport.pushReceivedInvoice` | 分摊结算单的同一解析结果,逐条转 `BfiReceivedInvoiceSaveEntryDTO`(无计量单位字段) |
| 付款申请 A103-1/A103-3 | 付款链路 | 同源解析,分录携带 `formalSettlementId` 按结算单勾稽各自应付单 |
幂等与防重:防重键 `awrp_src_code`(正式结算单号_结算明细单据号拼接)不变;分录标识 `awrp_entry_code` 因拆行规则变化,仅适用于新推送数据。
---
## 8. 代码位置索引
| 逻辑 | 位置 |
|---|---|
| 分录解析入口(多结算单/单结算单) | `KingdeeApBillSyncSupport.resolveEntryGroups` |
| 货物行拆分(货物→大类→税率) | `appendDetailEntries` / `appendCargoLines` |
| 费用项分组纯函数 | `splitFeeItems`(含残差勾稽,单测 `KingdeeEntrySplitTest`) |
| 费用大类判定 | `isLoadingCategory` / `isFreightCategory` / `feeCategoryByName` |
| 物料映射 | `resolveMaterialNumber`(读 `bfiProperties.getMaterial()`) |
| 计量单位解析 | `resolveEntryMeasureunit`(YAML 优先)→ `resolveMeasureunitNumber`(计费方案→档案) |
| 税率/计费单位解析 | `billingRuleUnits`(解析 `billing_plan_json` 一次取两样) |
| 结算合计手动费用行 | `appendSummaryEntry` |
| 配置类 | `org.springblade.transport.config.BfiProperties.Material` |
| Nacos 配置样例 | `doc/nacos/third-party-api.yaml`(thirdParty.bfi.material 段) |
---
## 9. 已知口径与限制
1. **运输大类内部多税率**:靠费用项级税率分组实现;同大类多税率时每个税率组一行、每行数量都取运输量。未配置税率的费用项合入无税率组不拆。
2. **非装卸的杂项费用**(运单自带,如补贴、其他费用):归运输大类、随运输行映射运输物料;金额残差兜底并入运输类无税率组。
3. **不含税金额**:拆分后仅运输类首行携带整行不含税金额(合计勾稽正确,单行口径不精确);装卸行/手动费用行不传(`e_amount` 非必传)。
4. **税率不传金蝶**:`taxrateid_number` 待金蝶确认编码规则(如 V13)后再补。
5. **规格型号**:金蝶保存 DTO 无该字段;车号/货物已有 `awrp_lpn`/`awrp_transport` 传参口径。
6. **装卸行计量单位**:走合同计费方案匹配(装卸费用项的计费方案规则计费单位 → 计量单位档案),装卸费用项未配计费方案规则时拦截报错,需在合同计费方案中维护。
@@ -0,0 +1,640 @@
# 金蝶(星瀚)BFI 全链路梳理:前端入口 → 后端链路 → 定时任务(业务域汇总 + 操作手册)
> 整理日期:2026-10-14
> 代码基线:`tms-api` 当前 master(`805ca9d3`,2026-10-13),全量按当前代码重新核实(上一版总纲基线为 09-24 `50573e7`)。
> 本文替代《2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版)》作为新总纲:
> - **组织方式改为三层**:每个业务域按「前端入口(页面/按钮)→ 后端链路(触发/顺序/口径/落库)→ 定时任务」梳理;
> - 新增**操作手册**(第 11 节):按业务时序给出端到端操作步骤与异常处理操作;
> - 自 09-27 以来代码有 40+ 提交(09-28~10-13),**收票回写缺陷已修、调试残留已清、新增锁单/解锁、审批自动同步、failed 重推、票池镜像读回切、全票池定时同步、A801-5 黑名单、A801-2/A902 附件接线、收款认领重构**等,接口清单、分录口径、状态机均有实质变化,全文按当前代码重写。
---
## 目录
- [0. 本次更新要点(09-27 → 10-14)](#0-本次更新要点09-27--10-14)
- [1. 集成总览与公共机制](#1-集成总览与公共机制)
- [2. 业务域一:客商档案(A902-1 / A902-2 / A801-5 黑名单)](#2-业务域一客商档案a902-1--a902-2--a801-5-黑名单)
- [3. 业务域二:财务应付与付款(A102-2 / A102-4 / A103-1 / A103-3 / A103-2 / A103-4)](#3-业务域二财务应付与付款)
- [4. 业务域三:财务应收与开票(A302-2 / A302-4 / A303-4 / A303-1 / A303-5)](#4-业务域三财务应收与开票)
- [5. 业务域四:收票与金蝶票池(A801-1 / A303-2 / A302-6 / A801-3 / A801-2 / A902 附件)](#5-业务域四收票与金蝶票池)
- [6. 业务域五:收款认领(A301-2 / A301-3 / A301-1)](#6-业务域五收款认领a301-2--a301-3--a301-1)
- [7. 业务域六:公共取数(A801-4 汇率)](#7-业务域六公共取数a801-4-汇率)
- [8. 全景:接口依赖与先后顺序](#8-全景接口依赖与先后顺序)
- [9. 状态机汇总](#9-状态机汇总)
- [10. 定时任务与配置开关汇总](#10-定时任务与配置开关汇总)
- [11. 操作流程手册](#11-操作流程手册)
- [12. 风险与待确认清单](#12-风险与待确认清单)
---
## 0. 本次更新要点(09-27 → 10-14)
| # | 变化 | 说明 |
|---|---|---|
| 1 | **⚠️ 旧总纲缺陷清单大面积清零** | A801-3 定时回写防重键已修复(改按收票登记 id,`7b98e33d` 系列);`if(true)` 调试组合块(强制物料/随机取物料)**已全部删除**;客商定时 KS000067 硬编码**已移除**;客商银行 `bankNumber` 硬编码已改真实联行号;A103-3 `e_bebank_number` 改取客商收款账户联行号(与 A103-1 口径统一) |
| 2 | **★新增横切机制一:单据锁定与解锁(10-09/10)** | BFI 新增 `lockBusiData`(出站锁单)与 `POST /kingdee-bfi/unLockNotifyBusiData`(金蝶回调,免登放行)。正式结算单/开票申请/付款申请推送成功后联带锁金蝶单据;金蝶侧删单回调还原本地状态;锁失败定时+手动补偿;列表回显锁状态。专项文档:`2026-10-09-金蝶单据锁定与解锁业务流程….md` |
| 3 | **★新增横切机制二:审批通过自动同步(10-10)** | `KingdeeApprovalSyncListener`(AFTER_COMMIT 事件)监听 4 域审批通过事件,自动调各自 `syncKingdee`;4 个 Nacos 开关默认 false;失败统一落 `failed`,审批接口回显警告文案;MK 流程审批回调统一改走 `service.approve`,OA 路径同样触发。专项文档:`2026-10-05-审批通过自动同步金蝶设计….md` |
| 4 | **★新增横切机制三:failed 单据定时重推 + 推送失败统一落 failed(10-10)** | `KingdeeAutoSyncRetryTask` 定时重推 4 域 failed 单据(不扫 unsynced);结算/开票链路异常也落 `failed`(此前只回滚保持 unsynced),旧总纲 11.2-#3 缺陷关闭 |
| 5 | **★票池架构回切:镜像读 + 全票池定时同步(10-05/10-08)** | 选票接口改读本地镜像表(本地真分页);票号精确查未命中才窄查 A801-1 回源;新增 `InvoiceReceiptKingdeeSyncTask` 全票池瘦身定时同步(invoiceTypes 白名单下推远端、2.5 万窗口硬上限二分时间片、绑定回填);结算单认领确认补上**保存重查闸门**(`assertInvoicesClaimable`)。旧总纲「实时回源、无定时」口径作废 |
| 6 | **★分录拆分口径重写(10-13)** | 应付/应收/收票分录改为**按 货物 → 费用大类(装卸/运输)→ 税率 三级拆分**,一车多货再按货物拆;0 元费用项/货物行跳过;物料编码改 Nacos 配置 `thirdparty.bfi.material.*`(`BFI_MATERIAL_NUMBER_MAP` 常量与 `blade_kingdee_material` 表已废弃);**计量单位当前不送**;`specification`(规格型号)字段本期不传。专项文档:`2026-10-13-金蝶应付应收收票单明细分录拆分与物料映射逻辑梳理.md` |
| 7 | **★收款认领链路重构(09-28~10-01)** | 认领本地化:先本地认领落库,**推送改定时+手动双入口按流水归集**(`ReceiptClaimPushTask`,旧总纲「全部手动、无定时」作废);`awrp_src_code`=分摊结算单号下划线拼接(重试复用幂等);分录单条、`e_corebillno` 传**结算单金蝶内码**(缺内码拦截不推);已推送认领禁止作废;A301-1 回写以真实收款单号覆盖占位单号 |
| 8 | **★A801-5 黑名单查询启用(10-01/10-08)** | 客商提审时按发票纳税人识别号(去重大写)调 A801-5;A 类命中硬拦截,B 类默认放行告警(可配置拦截),**查询异常上抛阻断提审** |
| 9 | **附件链路接线(09-27~10-05)** | A902 附件上传:正式结算/开票申请/付款申请/收票 4 条链路推送成功后联带上传(失败不污染主单,重推补传);A801-2 附件下载:前端统一走 `GET /invoice-receipt/invoice-pool/attachment/download` 流式代理,PDF 版式(`taxPdfUrl`/`pdfurl`)优先,OFD 按 `fileType=4` 识别 |
| 10 | **开票申请 A303-1 口径更新(09-30~10-11)** | 上游来源单据 `awrp_src_con_no`=首张结算单金蝶应收单防重键;特殊票种:纯货物运输行送 E04 否则 00;E04 随单报送货物运输单据体(车牌/起运/到达/货物/种类去重归并);`hsbz` 统一 0(不含税);旅客运输/代收车船税单据体 DTO 已建未接 |
| 11 | **组织编码改取金蝶部门编码(10-01)** | 应付/应收/开票的组织字段取 `SysCache.getDept().getKingdeeDeptCode()`,为空直接拦截(需在部门档案维护金蝶组织编码) |
| 12 | **定时任务加 single-flight 分布式锁(09-28/29)** | 全部金蝶定时任务经 `KingdeeSyncLockSupport.runSingleFlight`(Redis 锁,键含租户 id),多副本与手动触发共用一道闸;付款结果回写开关在 Nacos 样例中已显式开启,其余任务默认关闭 |
| 13 | **付款申请「消失解锁」兜底(09-29)** | A103-2 连续 2 轮查询不到已生成单(Redis 计数,TTL 2100s)→ 自动解锁置 `failed`,可重推 |
---
## 1. 集成总览与公共机制
### 1.1 调用架构(前端 → 后端 → 金蝶)
```
tms-erp-web(Vue3,src/views/** + src/api/**)
│ HTTP(网关按服务名路由 /blade-transport/**)
▼
blade-transport(Controller → ServiceImpl;推送编排 + 状态落库 + 锁单 + 审批事件)
│ IBfiService(参数校验 + Redis 令牌缓存)
▼
blade-bfi-api(IBfiClient @FeignClient url=${thirdParty.bfi.baseUrl})
│ access_token(业务令牌) + Authorization(iPaaS 网关头,BfiRequestInterceptor 注入)
▼
iPaaS 网关(/gwzh/BFI,不直连金蝶)
▼
金蝶星瀚 /ierp/kapi/...
反向:金蝶删单 ──▶ POST /blade-transport/kingdee-bfi/unLockNotifyBusiData(免登 skip-url)
定时:blade-transport task/ 包 7 个任务(Spring @Scheduled,全库无 PowerJob/XxlJob 接线)
```
- 无 Feign 重试(NEVER_RETRY)、无 fallback;失败抛 `BfiException`,批量循环逐条 catch,单条失败不中断整批。
- 令牌:Redis `blade:third:bfi:token`,TTL=expires_in−120s;`setIfAbsent` 写入(已有缓存不覆盖)。
- 配置前缀两套并存(读同一批 Nacos 键):bfi-api `third-party.bfi.*`(baseUrl/认证/令牌),transport `thirdparty.bfi.*`(业务开关/口径/映射)。样例:`doc/nacos/third-party-api.yaml`。
### 1.2 接口清单总表(2026-10-14,含在用状态)
来源:`blade-bfi-api/.../feign/IBfiClient.java`(URL 可被 `thirdParty.bfi.*Url` 覆盖)。
| 操作码 | 星瀚端点 | 用途 | 业务侧在用状态 |
|---|---|---|---|
| A100-1 | POST /ierp/kapi/oauth2/getToken | 获取令牌 | ✅ 自动(每次调用前) |
| A100-2 / A100-3 | verifyToken / withdrawToken | 校验/撤回令牌 | ❌ 已实现无调用 |
| A102-2 | POST …/awrp/ap/ap_finapbill/save | 财务应付单保存 | ✅ 正式结算单(应付)推送 |
| A102-4 | GET …/awrp/ap/ap_finapbill/query | 财务应付单查询 | ✅ 预检 / 硬校验 / 批量预热 |
| A103-1 | POST …/awrp/ap/ap/ap_finapbill/push | 应付下推生成付款申请 | ✅ 付款申请推送(单分录) |
| A103-2 | GET …/awrp/ap/ap_payapply/query | 付款申请单查询 | ✅ 推送预检 + 状态回写 + 消失解锁计数 |
| A103-3 | POST …/awrp/ap/ap/ap_payapply/save | 预付款申请保存 | ✅ 预付款直推 |
| A103-4 | GET …/awrp/cas/cas_paybill/query | 付款处理查询 | ✅ 付款记录回写(含 voucher_json) |
| A301-1 | GET …/awrp/cas/cas_recbill/queryRecBill | 收款单查询 | ✅ 认领结果回写(手动) |
| A301-2 | GET …/awrp/cas/cas_claimannounce/query | 收款认领公告查询 | ✅ 流水同步(手动 + 定时认领推送前置重查) |
| A301-3 | POST …/awrp/cas/cas/cas_claimbill/claimbillPush | 收款认领结果推送 | ✅ 定时 + 手动双入口,按流水归集 |
| A302-2 | POST …/awrp/ar/ar_finarbill/save | 财务应收单保存 | ✅ 正式结算单(应收)推送 |
| A302-4 | GET …/awrp/ar/ar_finarbill/query | 财务应收单查询 | ✅ 预检 / 硬校验 / 定时勾稽纠偏 |
| A302-6 | POST …/awrp/ap/ap/ap_invoice/assign | 收票指定应付 | ✅ 收票推送第二步 |
| A303-1 | POST …/awrp/sim/sim_original_bill/save | 开票申请单新增 | ✅ 按发票张次逐张推送 |
| A303-2 | POST …/awrp/ap/ap_invoice/cusBatchSave | 收票单批量保存 | ✅ 收票登记推送 |
| A303-4 | GET …/awrp/sim/sim_original_bill/query | 开票申请单查询 | ✅ 批量 billno 幂等预检 / 附件补传反查星瀚内码 |
| A303-5 | POST /ierp/kapi/app/sim/openApi | 开票发票查询(票易通) | ✅ 定时回写开票结果 |
| A801-1 | POST …/awrp/rim/rim_invoice/query | 全票池发票查询(rim 契约) | ✅ 定时全量同步 + 票号回源 + 保存/认领闸门 + 附件回源 |
| A801-2 | POST …/awrp/bosext/attachment/download | 进项发票文件下载 | ✅ **已接线**:附件查看/下载流式代理 |
| A801-3 | GET …/awrp/ap/ap_invoice/apinvoicequery | 收票单查询 | ✅ 推送预检 + 定时回写(防重键已修复=登记 id) |
| A801-4 | GET …/awrp/base/bd_exrate_tree/Forexquery | 汇率查询 | ✅ 结算页面取汇率 |
| A801-5 | POST …/awrp/bdm/bdm_blacklist_config/blacklistquery | 黑名单查询 | ✅ **新增启用**:客商提审校验 |
| A902 | POST /ierp/kapi/v2/frame/attachment/uploadFile | 附件上传 | ✅ **已接线**:4 条推送链路联带上传 |
| — | POST /API_lockAction_Router | lockBusiData 单据锁定 | ✅ 推送成功后锁单({updateCount,updateResultList} 应答) |
| — | (回调)POST /kingdee-bfi/unLockNotifyBusiData | 解锁/删单通知(unLockNotifyBusiData) | ✅ 金蝶回调本系统(免登放行) |
> 仍未实现:A901-1 供应商查询、A913 项目、A101-1/2 合同、A102-1/A302-1 暂估单(合同/项目走合同合规系统)。
> 查询时间窗约束:A103-2/A103-4/A102-4/A302-4/A303-4/A301-1 ≤ 1 个月;A902-1 ≤ 6 个月;A301-2 无时间窗参数(按认领人+分页,时间窗口取 `thirdParty.bfi.claimAnnounce.months` 默认 6)。
### 1.3 防重键(候选键)总表(2026-10-14 口径)
金蝶侧按「来源系统 + 来源单据号」幂等(`awrp_src_sys` + `awrp_src_code`)。组装:`ExternalSrcCodeSupport.buildSrcCode`(下划线拼接、去空白、去重保序)。
| 金蝶单据 | awrp_src_code | srcType | 分录键 | 预检接口 |
|---|---|---|---|---|
| 财务应付单 A102-2 | `正式结算单号_结算明细单据号1_…` | TMS财务应付单 | 按 货物-费用大类-税率 拆分行顺序生成 | A102-4 |
| 财务应收单 A302-2 | 同应付口径 | TMS财务应收单 | 同应付口径 | A302-4 |
| 付款申请 A103-1 | `paymentNo` | 配置 `0KzXs` | 固定 `paymentNo-1`(单分录);勾稽键 `awrp_finapbill_entryid`=主结算单应付防重键(本地组装) | A103-2 |
| 预付款申请 A103-3 | 付款申请号口径 | `0KzXs` | `paymentNo-1` | A103-2 |
| 开票申请 A303-1 | billno:单张=`application_no`,多张=`application_no-张次序号`;`awrp_src_con_no`=首张结算单应收防重键 | `0KzXs` | 开票商品行 id | A303-4(批量 billno) |
| 收票单 A303-2 | **收票登记 id** | TMS收票登记 | 与 A102-2 分录同源同口径 | A801-3(预检与回写同键) |
| 收款认领 A301-3 | **分摊结算单号下划线拼接**(重试复用同一编码)★改 | WLTMS | 单条:seq/e_corebillno=第一张结算单**金蝶内码** | A301-1(sourcebillnumber 匹配) |
| 客户 A902-2 | `number`=TMS 客商编号(KS+6位);`societycreditcode` 为金蝶侧候选键 | awrp_datasourcesys=WLTMS | — | A902-1 |
### 1.4 公共档案取数(应付/应收/收票复用 `KingdeeApBillSyncSupport`)
| 取数项 | 规则(当前代码) |
|---|---|
| 组织 org/payorg/recorg | 合同/申请部门 → `SysCache.getDept().getKingdeeDeptCode()`(**金蝶部门编码**,为空拦截)★改 |
| 币种 | 合同 `settlement_currency`,RMB→CNY |
| 汇率 exchangerate/exratedate | 结算单页面维护值(A801-4 回填),未维护不送 |
| 往来户 asstact_number | 名称 → 本地 `blade_customer_archive` 匹配取 `customer_code`,查不到报错拦截 |
| 往来类型 asstacttype | **固定 `bd_customer`**★改(按合同类别区分供应商的逻辑已注释停用) |
| 物料编码 | 运输类按 运输方式/国际/装卸 → Nacos `thirdparty.bfi.material.domestic.{railway,highway,waterway,aviation}` / `international` / `loading` 映射(默认 XDWL1101000x / Item-00001081 / XDWL11002004);运输方式未配置直接拦截 |
| 计量单位 | **当前不送**(DTO 字段保留,无 set 调用) |
| 金额 | 含税=结算金额;不含税:明细行有、手工合计费用项行不传 |
| 数量/单价 | 运输类=运单运输量(兜底 1;同税率拆多行每行都取运输量);装卸/杂费=1;单价=金额/数量反算 |
| 0 元行 | 费用项 signum()==0 与 0 元货物行**整行跳过**★新增 |
| 运输货物/车号 | 明细行传 `awrp_transport`(货物名称)/ `awrp_lpn`(车号);合计费用项行不传 |
### 1.5 三大横切机制(新总纲新增,读业务域前先看)
#### ① 审批通过自动同步(`KingdeeApprovalSyncListener`)
```
业务审批通过(页面「通过」或 MK/OA 流程回调,统一走 service.approve)
▶ 事务提交后 AFTER_COMMIT 发布 KingdeeBillApprovedEvent
▶ KingdeeApprovalSyncListener.onBillApproved(@TransactionalEventListener,fallbackExecution=true)
▶ 按 4 个开关分别调用 syncKingdee(id):
settlement → FormalSettlementServiceImpl.syncKingdee (A102-2 / A302-2)
payment → PaymentApplicationServiceImpl.syncKingdee (A103-1 / A103-3)
invoiceApply→ InvoiceApplicationServiceImpl.syncKingdee (A303-1 按张次)
receipt → InvoiceReceiptServiceImpl 推送 (A303-2 + A302-6)
▶ 失败:独立事务落 failed + ThreadLocal 失败原因
▶ 4 个审批 Controller 统一 pollSyncFailure() 拼警告返回前端:"审批通过,但自动同步金蝶失败:xxx"
```
- 开关(默认全 false):`thirdParty.bfi.autoSync.settlement / payment / invoiceApply / receipt`。
- 客商档案不走事件机制:`CustomerArchiveController.approve` 内直接 `syncKingdee(id)`(失败不阻断审批)。
- 推送成功后各链路在**同一方法内继续执行 lockAfterPush** 锁金蝶单据(见②)。
#### ② 单据锁定与解锁(`KingdeeBillLockService` + `KingdeeBfiPublicController`)
```
推送成功 ─▶ lockAfterPush(先落 blade_kingdee_bill_lock failed 占位 → lockBusiData → 翻 locked)
锁失败 ─▶ KingdeeBillLockRetryTask 定时补偿(cron thirdParty.bfi.billLock.retryCron,默认每5分钟,开关默认 false)
+ 手动端点 POST /formal-settlement/retry-kingdee-bill-lock(通用,扫 failed 且 try_count<5)
金蝶删单 ─▶ POST /kingdee-bfi/unLockNotifyBusiData(免登,@TenantIgnore)
├─ SETTLEMENT:作废结算单 + kingdee_sync_status 回 unsynced、清 kingdee_bill_no/id
├─ INVOICE_APPLY:作废开票申请 + 同上还原
└─ PAY_APPLY:不作废源单,解锁置 failed、清 kingdee_bill_id/no(可重推)
本地拦截 ─▶ 付款申请/开票申请作废前 checkEditable:locked 即抛「该单据已被金蝶锁定」
(正式结算单作废未接 checkEditable,靠列表锁状态前端禁用)
列表回显 ─▶ mapActiveLocks() 填充 VO kingdeeLockStatus(locked/failed),前端展示「金蝶锁定中/锁定失败」并禁编辑
```
- 收票单本期**不锁**(`KingdeeLockConstant` 映射预留)。
- 付款申请另有**消失解锁**:A103-2 连续 2 轮查询不到已生成单(Redis 计数 TTL 2100s)→ 自动解锁置 failed。
#### ③ 定时任务 single-flight 分布式锁(`KingdeeSyncLockSupport`)
- 所有金蝶定时任务经 `runSingleFlight`:RedisLockClient 零等待可重入锁,锁键=前缀+租户 id(无用户上下文用 default);`blade.lock.enabled=false` 降级直跑。
- 手动端点与定时任务共用同一把锁——**手动触发时若定时正在跑会直接返回**(防重复回写)。
---
## 2. 业务域一:客商档案(A902-1 / A902-2 / A801-5 黑名单)
**定位**:全局数据前提。其他链路的往来户编码(`customer_code`)来自 `blade_customer_archive`;金蝶客户编码 = TMS 客商编号。
### 2.1 前端入口
| 页面 | 操作 | 后端端点 | 触发时机 |
|---|---|---|---|
| 客商档案(`/vehicle/customer-archive`) | 新增/编辑表单录统一社会信用代码(满 18 位防抖) | `GET /bfi/customer/query-by-credit-code`(A902-1) | 回填名称/法人/地址/经营范围等并锁定;金蝶状态 A/B 阻断保存 |
| 同上 | 行内「同步金蝶」链接(仅 approved 且 failed 显示) | `POST /customer-archive/sync-kingdee`(A902-2) | 失败人工重推 |
| 同上 | 行内「审批通过」 | `POST /customer-archive/approve` → 内部 `syncKingdee(id)` | **提审校验 A801-5 黑名单;通过后自动推送 A902-2** |
### 2.2 后端链路
**A902-1 客户查询(3 入口)**:前端回填 / 单条推送前复查(`syncKingdee` 先查一次,C→exist 终态、A/B→failed)/ 定时批量复查(50 信用代码/批翻页取全)。
**A801-5 黑名单(★新增,提审拦截)**:`CustomerArchiveServiceImpl.submitApproval` → 取客商各发票 `taxNo` 去重大写 → `queryBlacklist("1", taxNos)`(enable=1,pageSize 上限 2000,循环翻页取全)→ **A 类硬拦截**;B 类默认放行告警(`thirdparty.bfi.blacklist.blockRiskWarning=true` 时拦截);其他类别放行告警;**查询异常上抛阻断提审**。
**A902-2 客户保存(3 入口收敛 `pushCustomerToKingdee`)**:审批通过自动(失败不影响审批)/ 人工重推(synced/exist 幂等返回)/ 定时重推(pending=approved 且 none/failed)。先 A902-1 复查(金蝶已有 C 直接置 exist 跳过)再保存。只推新增建档(审批后变更不重推)。参数要点:`entry_groupstandard` 按 `customerKind` internal→1101 / 其余→1102;`entry_bank` 取 `blade_customer_receipt_account`(账号/开户行/CNY/默认 + **真实联行号 cnaps_code**,硬编码已删);`bizfunction` 固定 `1,2,3,4`。
**定时附加职责**(`CustomerKingdeeSyncTask`):synced/exist 且启用客商复查 C→exist;金蝶侧停用 → 联动停用 TMS 档案 `status=2`(单向)。
**响应存储**:`kingdee_status / kingdee_fail_reason / kingdee_sync_time`。
**金蝶侧效果**:
![金蝶客户列表收到 A902-2 推送的客商(编码=TMS 客商编号 KS000073,客户基本分类标准 1101 内部客户/1102 外部客商)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a902-金蝶客户接收.png)
![TMS 客商档案与金蝶客户档案对照:编码一致(KS000060)、以统一社会信用代码为两侧关联键](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a902-客商编码对照.png)
---
## 3. 业务域二:财务应付与付款
### 3.1 链路总览
```
环节一 正式结算单(应付 Tab)审批通过自动同步 / 手动「同步金蝶」
A102-4 预检 ▶ A102-2 保存应付单 ▶ 回写 kingdee_bill_no + synced ▶ lockBusiData 锁单
环节二 付款申请审批通过自动同步 / 「批量同步」
A102-4 硬校验(逐关联结算单应付已生成)▶ A103-2 幂等预检
▶ A103-1 下推(预付款走 A103-3)▶ 回写 kingdee_bill_id/no + synced ▶ lockBusiData 锁单
环节三 PaymentKingdeeResultTask 定时(或手动「同步付款结果」)
A103-2 状态回写(synced→paying→paid/closed;failed 勾稽纠偏;连续2轮查不到→消失解锁置failed)
▶ A103-4 付款处理查询 ▶ 付款记录落库(含 voucher_json 凭证/银行流水/电子回单)
```
**金蝶侧效果**:
![金蝶财务应付单 AP-202609-800044 与 TMS 正式结算单对照:外部系统单据号=防重键(WLTMS_FJS2026092400001_YF20260912…),应付金额、明细行(物料/车号/运输货物/数量)逐行对应](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a102-应付单对照.png)
> **前置说明(预结算单)**:预结算单(PreSettlement)**不直接推送金蝶**——无金蝶字段与调用,`KingdeeBillApprovedEvent` 也不覆盖它。与金蝶的关联仅三处:
> ① 非 RMB 预结算在页面经 A801-4 带出汇率随单保存,**汇率不自动结转到正式结算单**——A102-2/A302-2 推送取的是正式结算单自身维护的 `exchange_rate/exchange_rate_date`(`KingdeeApBillSyncSupport:272-277`),转正式后须重新维护;
> ② 进度预付付款申请可关联预结算单(`payment_application.pre_settlement_id`),A103-3 汇率日期优先取关联结算单(正式→预结算)维护值,兜底申请日期;
> ③ A103-2 付款 paid 回写时联动履约:回写预结算垫资 `blade_pre_settlement_advance`(paidAmount 整额置满、billStatus=paid、kingdeeAdvanceNo=金蝶付款单号)并刷新预结算汇总与正式结算来源快照。
> 预结算「预开票」金蝶推送方案(2026-09-29)**未实现**;预结算审批走 MK 流程(`MkProcessBizHandlers.PreSettlementHandler`),直接置 approved、不发布金蝶事件。
### 3.2 前端入口
| 页面 | 操作 | 后端端点 | 说明 |
|---|---|---|---|
| 正式结算单(`/settlement/formal-settlement`,应付/应收 Tab) | 工具栏「同步金蝶」(单选一条) | `POST /formal-settlement/sync-kingdee?id=` | 30s 超时;成功回写金蝶单号 |
| 同上 | 行内「审批通过」 | `POST /formal-settlement/approve` | 开关开启时**自动同步**;`data=false` 前端提示失败原因 |
| 同上 | 列表「金蝶锁定」标签列 | 无调用(后端 mapActiveLocks 回显) | locked 红色标签禁编辑/作废 |
| 付款申请(`/payment/payment-application`) | 工具栏「批量同步」 | `POST /payment-application/sync-kingdee-batch`(ids) | 整批预热→逐单独立事务,单条失败不中断 |
| 同上 | 工具栏「同步付款结果」 | `POST /payment-application/sync-kingdee-result` | 手动触发 A103-2/A103-4 回写 |
| 同上 | 行内「审批通过」 | `POST /payment-application/approve` | 开关开启时自动下推+锁单 |
| 同上 | 单笔「同步金蝶」 | `POST /payment-application/sync-kingdee?id=` | 端点在;**前端 `handleSyncRow` 未绑定模板(死代码)**,单笔入口实际只有审批自动同步 |
### 3.3 后端要点(当前口径)
- **A102-2/A302-2 分录**:结算明细行按「货物→费用大类→税率」拆分 + 手工合计费用项(manualFlag=1,每行一条,0 元跳过);明细行带运输货物/车号;调试覆盖块已删。
- **A103-1 下推**:单分录 `paymentNo-1`;`e_settlementtype_number` 按付款方式映射(汇票→012-02,其余→02);`e_bebank_number`=客商收款账户联行号(未维护拦截);`awrp_finapbill_entryid`=主结算单应付防重键(本地组装;⚠️ 支撑类注释仍写「A102-4 反查」,注释与代码不一致,见 12 节)。
- **A103-3 预付款**:`e_bebank_number` 同样取客商收款账户联行号(★已与 A103-1 统一);project 取项目档案真实编码(无则 999)。
![TMS 付款申请(FKD2026092400001,申请付款 195.11)与金蝶付款申请单(FKSQ-00000199)对照:核销金额、往来户/往来账户(收款账号)、结算方式(银行转账)逐项对应;金蝶侧由 A103-1 下推自动生成付款计划](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a103-付款申请对照.png)
![预付款对照:TMS 进度预付申请(240.12)下推金蝶生成「其他付款申请」FKSQ-00000219(进项货付款申请),分录预付款金额、往来户、结算方式对应;附件区可见 A902 联带上传的文件](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a103-3-预付款对照.png)
- **A103-2 回写状态映射**:`Alreadypay`→paid(终)>`billstatus=D`→closed(终)>`Inpayment`→paying>其余 synced;`entry[].e_paidamt` 汇总→paid_amount(缺失且 paid 兜底=申请金额);paid 联动履约(预结算垫资/结算付款记录);**failed 纳入勾稽纠偏**(查到即纠正 synced);**查不到→消失解锁计数**(仅已生成/付款中参与,连续 2 轮→解锁置 failed)。
- **A103-4 付款记录**:按付款组织编码分组查询(`org_number` 优先取 A103-2 返回值),窗口最近 1 个月,`sourcebillid`==kingdee_bill_id 优先、`sourcebillnumber` 兜底;upsert `blade_payment_application_record`(actpayamt/paid_date/整单 JSON→voucher_json);清理 MOCK-PAY- 模拟记录。
- **A902 附件**:推送成功事务提交后联带上传(`KingdeeAttachmentSyncSupport.syncUserAttachments`),失败不污染主单状态,重推补传。
- **异常落库**:手动/自动同步失败统一独立事务落 `failed` + 原因(截 500 字),由「手动重推/failed 重推任务」续推。
---
## 4. 业务域三:财务应收与开票
### 4.1 链路总览
```
环节一 正式结算单(应收 Tab)审批通过自动同步 / 手动「同步金蝶」
A302-4 预检 ▶ A302-2 保存应收单 ▶ 回写 kingdee_bill_no + synced ▶ lockBusiData 锁单
环节二 开票申请审批通过自动同步 / 工具栏「批量同步」(逐单)
A302-4 硬校验(应收已生成)▶ A303-4 批量 billno 幂等预检(部分张次已生成可断链续推)
▶ A303-1 按发票张次逐张新增(E04/货物运输单据体/A902 附件)▶ 张次+申请单回写 ▶ lockBusiData 逐张锁单
环节三 PaymentKingdeeResultTask 定时(或手动端点)
A302-4 勾稽纠偏(failed→synced / 补单号)
A303-5 开票发票查询(serialNo 按 application.kingdee_bill_no 逗号拆分)▶ 发票落库(含文件地址)▶ invoiced(终态)
```
### 4.2 前端入口
| 页面 | 操作 | 后端端点 | 说明 |
|---|---|---|---|
| 正式结算单(应收 Tab) | 同应付:同步金蝶 / 审批通过 / 锁状态列 | 同 3.2 | `settlementType=receivable` 路由 A302-2 |
| 开票申请(`/payment/invoice-application`) | 工具栏「批量同步」 | `POST /invoice-application/sync-kingdee?id=`(逐单 Promise.all) | 仅审批通过单 |
| 同上 | 行内「审批通过」 | `POST /invoice-application/approve` | 开关开启时自动按张次推送 |
| 同上 | 锁定状态列 | 无调用 | 「金蝶锁定中」禁编辑/作废 |
| 正式结算单编辑器「开票发票」区 | 查看/下载发票文件 | 金蝶发票云直链(开票结果回写的 invoicePdfFileUrl 等,不走 A801-2 代理) | 10-02 起落库发票文件地址 |
### 4.3 后端要点(当前口径)
**金蝶侧效果**:
![金蝶财务应收单 AR-260924-012021 与 TMS 正式结算单对照:应收金额、费用合计对应;明细行物料编码/车号/运输货物/数量逐行对应](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a302-应收单对照.png)
![开票申请张次拆分对照:TMS 开票内容第1张/第2张(各含商品行)分别生成金蝶票易通开票申请单——购销方、不含税金额、未关联金额(¥4,386)逐项对应](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a303-1-开票张次对照.png)
- **张次拆分**(`blade_invoice_application_sheet`):每张发票张次推一张金蝶开票申请单;billno=单张取 application_no、多张取 `-张次序号`;分录=本张商品行(按 invoice_sheet_id 分组)。
- **A303-1 头字段口径(★更新)**:`invoicetype` 数电专票 08xdp / 数电普票 10xdp;`specialtype`:本张商品行**全部**为货物运输类(名称/分类含运费关键字)→ **E04**,否则 **00**;E04 随单报 `freights[]` 货物运输单据体(车牌 licenseplate/起运 startplace/到达 endplace/货物 transportgoods/运输方式 transporttype 字典映射,按「车牌×起运×到达×货物×种类」去重归并,空行剔除);`hsbz` 统一 **0**(不含税);`awrp_src_bt=ar_finarbill` + `awrp_src_con_no`=首张结算单金蝶应收防重键(金蝶按防重键校验源单,传编号会报源单不存在);`awrp_project_number`=999。旅客运输/代收车船税单据体 DTO 已建**未接入**。
- **A303-5 回写**:按销方税号分组(开票单位→客商档案税号)≤50/批;落 `blade_invoice_application_invoice`(serial_no/invoice_no/invoice_date/invoice_amount/raw_json + 发票文件地址);唯一键防重;有新增发票→invoiced 终态。发票结果不反写结算单(文件地址随发票记录供结算详情查看)。
- **A902 附件**:开票申请推送成功后联带上传(以本地 kingdeeBillId 定位金蝶单据,星瀚内码缺失时经 A303-4 反查回填)。
---
## 5. 业务域四:收票与金蝶票池
### 5.1 架构现状(★10-05 回切后终态)
```
【数据面】InvoiceReceiptKingdeeSyncTask 定时(默认关,每30分钟)
A801-1 全票池增量同步(incrementDays=30)→ invoiceTypes 白名单下推远端逐票种切片轮询
→ 2.5 万窗口硬上限触顶二分时间片递归 → 镜像 blade_kingdee_invoice_pool 批量 upsert(无变化跳写)
→ refreshBoundReceipts:未绑定存量登记补绑(已绑定零写入)
手动端点:POST /invoice-receipt/sync-invoice-pool(同一方法+同一把锁)
【选票面】收票登记表单 / 结算单认领弹窗(KingdeeInvoicePoolDialog)
GET /invoice-receipt/invoice-pool、GET /formal-settlement/invoice-pool
→ 本地镜像真分页;仅「票号精确查 + 无认领/登记过滤 + 本地空页」时窄查 A801-1 回源一次再本地重查
行过滤:票号非空 + amountUseStatus=0 + availableAmount>0
【闸门】收票登记保存 persistMirrorFromRemote(按票号精确单查+可用性复核+FOR UPDATE upsert)
结算单认领确认 assertInvoicesClaimable(★10-05 补上:逐票窄查+可用复核,红冲/作废/被占用在此拦截)
【推送链】审批通过自动推送(receipt 开关)/ 工具栏「批量同步」
A102-4 预检(应付已生成)→ A801-3 幂等预检(防重键=收票登记 id)
→ A303-2 收票单保存(付款方式固定现购)→ pushed → A302-6 指定应付 → assigned(终态)
→ A902 附件联带上传;缺票种编码按票号窄查 A801-1 补填
【回写面】PaymentKingdeeResultTask.syncKingdeeReceivedInvoiceResult(默认关,每10分钟)/ 手动端点
A801-3 按「收票登记 id」匹配(★缺陷已修)→ 发票状态异常回收置 failed
/ pushed 补偿重发 A302-6 / assigned 缺单号补登;窗口内查不到不改状态
【附件面】GET /invoice-receipt/invoice-pool/attachment/download?invoiceNo=(A801-2 流式代理)
镜像优先、实时回源兜底;pdfurl 优先 taxPdfUrl 兜底,都缺退 downloadUrl(fileType=4 判 OFD)
```
### 5.2 前端入口
| 页面 | 操作 | 后端端点 | 说明 |
|---|---|---|---|
| 收票登记(`/payment/invoice-receipt`) | 工具栏「批量同步」 | `POST /invoice-receipt/push-kingdee`(ids) | 单笔失败不中断整批,逐单报因 |
| 收票登记表单 | 发票号码放大镜(选票弹窗) | `GET /invoice-receipt/invoice-pool` | 镜像分页 + 回源;确认/保存前防重校验 |
| 收票登记表单 | 附件 查看/下载 | `GET /invoice-receipt/invoice-receipt/invoice-pool/attachment/download`(A801-2 代理) | 120s blob;PDF 优先 |
| 收票登记 | 行内「审批通过」 | `POST /invoice-receipt/approve` | receipt 开关开启时自动推送 |
| 正式结算单编辑器 | 发票认领区「查询可认领发票」/「确认识领」 | `GET /formal-settlement/invoice-pool`、`POST /formal-settlement/claim-invoices` | 确认时走认领闸门;未保存单仅本地暂存 |
| 共享弹窗 `KingdeeInvoicePoolDialog` | 行附件 查看/下载 | 同 A801-2 代理 | 票池弹窗、结算认领行、付款申请发票附件行、收票附件表 4 处复用 |
### 5.3 后端要点
**金蝶侧效果**:
![TMS 收票管理列表「金蝶状态」列:已指定应付(终态)/推送失败(带失败原因)/未推送,金蝶单据号 AVT-202608-000396 悬浮展示](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-收票金蝶状态列表.png)
![A902 附件上传金蝶侧效果:金蝶财务应付单详情「附件」区收到 TMS 推送单据联带上传的文件;付款计划区同时可见下推生成的付款计划(外部系统来源标识=防重键)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a902-附件上传金蝶侧.png)
![A801-2 发票查看/下载入口:正式结算单详情「发票信息」区认领的发票行提供 查看/下载(走后端 A801-2 流式代理,PDF 版式优先);付款信息区同时回显付款申请的金蝶单号](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a801-2-发票查看下载.png)
- 推送计划校验:approved、状态≠assigned、已绑定镜像、`checkStatus=2`、**恰好分摊 1 张审批通过应付结算单(一票一应付)**、有结算明细;不满足写 `blockedReason` 不改状态。
- A801-3 预检命中且状态正常→跳过 A303-2 直接续推指定(断点恢复);发票状态异常(作废2/红冲3/部分红冲7/全部红冲8)→failed 报人工。
- 落库:`kingdee_received_invoice_no`、状态机 `unsynced→pushed→assigned/failed` →`kingdee_push_status`(唯一金蝶状态来源);终态/失败写 `blade_invoice_receipt_record`;pushed/assigned 禁止本地作废。
- 全票池同步细节:单查询窗口 MAX_POOL_PAGES×500=2.5 万上限,`updateTime` 单调二分不重不漏;镜像 upsert 轻量比对(流水号/价税合计/税额/可用金额/使用状态)无变化跳写。
---
## 6. 业务域五:收款认领(A301-2 / A301-3 / A301-1)
**定位**:业务先行认领。金蝶司库第一轮自动认领到客户维度 → TMS 二次认领到结算单维度 → **本地认领完成后**(定时或手动)回推金蝶生成收款单 → 手动查询回写结果。★09-28 重构后:认领与推送解耦,推送按流水归集、定时+手动双入口。
### 6.1 链路总览
```
阶段一 收款流水页「流水同步」(手动)
POST /receipt-flow/sync ▶ A301-2 认领公告查询(claimusers=Nacos thirdParty.bfi.claimAnnounce.claimusers,
★不再按登录用户姓名;未配置则以空值调用会查不到数据)
▶ 全量翻页(窗口 months=6)▶ 按 detail_serial_no 幂等 upsert blade_kingdee_receipt_flow
阶段二 流水详情/批量认领(手动)
POST /receipt-flow/claim ▶ 本地落库(claim + 分摊 + 流水 claimed_amount/claim_status 刷新 + 结算单收款汇总)
▶ 本地完成即结束,**不调金蝶**(认领本地化★)
阶段三 推送金蝶(定时 ReceiptClaimPushTask 默认关每10分钟 / 手动流水行内「推送金蝶」)
pushClaimToKingdee ▶ 按认领人逐人重查 A301-2 → 扫「认领完成未推送」流水按流水归集
▶ 逐流水:awrp_src_code=分摊结算单号拼接(重试复用幂等)→ A301-3 推送
(分录单条:e_receivableamt=全部未推送金额;e_corebillno=第一张结算单金蝶内码,缺内码拦截不推)
▶ 成功回写 kingdee_bill_no(回执 keys.awrp_src_code 占位)+ kingdee_bill_status=pushed
阶段四 认领记录页「查询金蝶认领结果」(手动)
POST /receipt-claim-record/query-kingdee-result ▶ A301-1 收款单查询(今天回溯 30 天)
▶ sourcebillnumber==awrp_src_code 匹配 ▶ 真实收款单号覆盖占位单号 + 状态推进(C→approved,D→received 终态)
```
### 6.2 前端入口
| 页面 | 操作 | 后端端点 | 说明 |
|---|---|---|---|
| 收款流水(`/payment/receipt-flow`) | 工具栏「流水同步」 | `POST /receipt-flow/sync`(A301-2) | 权限 `receipt_flow_sync` |
| 同上 | 流水详情/批量认领表单「确认」 | `POST /receipt-flow/claim` | 选择审批通过应收结算单按比例分摊;已推送金蝶的认领禁止作废(后端硬校验) |
| 同上 | 行内「推送金蝶」 | `POST /receipt-flow/push-kingdee?flowId=` | 确认弹窗后按流水归集推送 |
| 认领记录(`/payment/receipt-claim-record`) | 工具栏「查询金蝶认领结果」 | `POST /receipt-claim-record/query-kingdee-result`(A301-1) | 手动;窗口内查不到不改状态 |
### 6.3 后端要点
**金蝶侧效果**:
![A301-2 对照:金蝶「认领公告列表」(RLTZ 通知单,待认领/部分认领)与 TMS 收款流水「手动同步流水」拉取的同批公告对照](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-2-公告同步对照.png)
![A301-3 对照:TMS 认领记录(RLTZ-20260612-0073 认领 828.00)推送后,金蝶「认领处理列表」生成认领单 RL-20260929-0058(已确认,认领通知单 0073 对应)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-认领推送对照.png)
![A301-3 金蝶侧认领结果详情:收款认领结果行「核心单据编号」=结算单星瀚内码(2586341365516352512,即 e_corebillno 传金蝶内码的落地证据)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-金蝶认领结果.png)
- 推送闸门:已推送(pushed)流水跳过不重推;`billid`(认领公告 id)缺失拦截「请先同步金蝶收款流水」;结算单金蝶内码缺失拦截不推。
- 幂等:`awrp_src_code` 重试复用同一编码;作废认领不再生成本地冲单(金蝶单据状态保留原值)。
- A301-3 字段待金蝶确认项仍在:`recpaytype_number`(示例 101)、`paymenttype`、`recpayer`(金蝶付款人 master id)。
---
## 7. 业务域六:公共取数(A801-4 汇率)
| 项 | 说明 |
|---|---|
| 前端入口 | 预结算单/正式结算单编辑器:非 RMB 且选了汇率日期(或币种切换)时自动 `GET /bfi/exchange-rate?currencyCode=&effectdate=`,回填结算汇率并重算本位币金额 |
| 后端 | 按日期查 A801-4 全量汇率(页大小固定 10)→ 本地按 `orgcur_number` 匹配 → 返回 VO;不落库 |
| 下游 | 汇率/汇率日期随结算单落 `blade_formal_settlement.exchange_rate / exchange_rate_date`,随 A102-2/A302-2 推送;A103-1 下推不传(按应付单自动带出) |
![外币结算汇率推送效果:TMS 结算单结算汇率 6.736700(美元)推送后,金蝶应付单汇率表 6.7367、金额/金额(本位币) ¥9,923.16 自动换算,外部单据类型=TMS财务应付单](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a801-4-外币汇率推送.png)
---
## 8. 全景:接口依赖与先后顺序
```
┌──────── 客商档案(全局前提,软依赖) ────────┐
│ A801-5 黑名单(提审拦截)→ A902-2 保存(审批自动/人工/定时)→ A902-1 复查
└───────────────────┬────────────────────────┘
▼
【应付主线】 正式结算单(应付)审批 ─A102-4 预检─▶ A102-2 保存 ─▶ synced ─▶ lockBusiData
│ A902 附件联带上传 (汇率 ← A801-4 页面回填)
│ A102-4 硬校验
▼
付款申请审批 ─A103-2 预检─▶ A103-1 下推(预付款→A103-3)─▶ synced ─▶ lockBusiData
│
▼
定时 A103-2 状态回写(纠偏/消失解锁)─▶ A103-4 付款记录(voucher_json)
【应收主线】 正式结算单(应收)审批 ─A302-4 预检─▶ A302-2 保存 ─▶ synced ─▶ lockBusiData
│ A302-4 硬校验
▼
开票申请审批 ─A303-4 批量预检─▶ A303-1 逐张次新增(E04/freights/A902 附件)─▶ synced ─▶ 逐张锁单
│
▼
定时 A302-4 勾稽纠偏 + A303-5 开票结果 ─▶ invoiced(终态,发票文件落库)
【收票主线】 定时 A801-1 全票池同步 ─▶ 镜像表 ◀── 选票读镜像(票号未命中窄查回源)
│ 收票登记保存(闸门)/ 结算认领确认(闸门★新增)
▼
收票登记审批 ─A102-4 预检─A801-3 预检(登记id)─▶ A303-2 保存 ─▶ pushed ─▶ A302-6 ─▶ assigned
│(本期不锁单;A902 附件联带上传)
▼
定时 A801-3 回写(异常回收/指定补偿/单号补登,按登记 id 匹配★已修)
【收款认领主线】 手动 A301-2 公告同步 ─▶ blade_kingdee_receipt_flow
│ 页面认领(本地化,不调金蝶)
▼
定时/手动 A301-3 按流水归集推送(e_corebillno=结算单金蝶内码)─▶ pushed
▼
手动 A301-1 收款单查询 ─▶ 真实收款单号覆盖 + approved/received(终)
【横切】 审批通过 AFTER_COMMIT 自动同步(4 开关)│ 推送成功→lockBusiData(3 链路)
金蝶删单→unLockNotifyBusiData 回调还原 │ 锁失败定时/手动补偿
failed 单据→KingdeeAutoSyncRetryTask 定时重推
```
**硬顺序规则(代码强制)**:
| # | 规则 | 强制方式 |
|---|---|---|
| 1 | 客商档案先于一切推送(本地 customer_code 必须存在) | 查不到直接报错;金蝶侧审核状态不校验(软依赖) |
| 2 | 客商提审先过 A801-5 黑名单 | A 类命中/查询异常即阻断提审 |
| 3 | A102-2 先于 A103-1 与 A303-2 | A102-4 硬校验/预检 |
| 4 | A302-2 先于 A303-1 与 A301-3(e_corebillno 需金蝶内码) | A302-4 硬校验;内码缺失拦截不推 |
| 5 | A303-2 先于 A302-6 | 同一次推送内串行 |
| 6 | 推送先于回写(A103-2/A103-4、A302-4 勾稽、A303-5、A801-3、A301-1) | 查询键由推送链写入本地 |
| 7 | 锁单在推送成功之后、金蝶删单回调可还原 | lockAfterPush 事务提交后执行;unLockNotifyBusiData 免登回调 |
| 8 | A801-4 汇率查询先于 A102-2/A302-2(如需携带汇率) | 页面联动维护,未维护不传 |
**断链恢复语义**:本地失败/远端未落单→unsynced 或 failed 可重推;远端成功本地回滚→重推时预检幂等回填(A102-4/A302-4/A103-2/A303-4/A801-3/awrp_src_code 复用);推送成功后状态滞后→定时回写;批量语义→整批预热+逐单独立事务+单条失败不中断。
---
## 9. 状态机汇总(2026-10-14)
| 表.字段 | 状态集 | 迁移 | 写入方 |
|---|---|---|---|
| `blade_formal_settlement.kingdee_sync_status` | unsynced / synced / **failed**(★新增写入口) | unsynced→(A102-2/A302-2 成功)→synced;推送异常→failed(独立事务);failed 可手动/定时重推、A302-4 勾稽纠偏回 synced;金蝶删单回调→voided + unsynced 还原 | FormalSettlementServiceImpl |
| `blade_payment_application.kingdee_status` | unsynced / synced / paying / paid(终) / closed(终) / failed | 同前版映射;failed 可纠偏回 synced;**连续 2 轮查不到→消失解锁置 failed**;金蝶删单回调→failed+清单号(不作废源单) | PaymentApplicationServiceImpl + 定时 |
| `blade_invoice_application.kingdee_status` | unsynced / synced / invoiced(终) / **failed**(★新增写入口) | unsynced→(A303-1 全部张次成功)→synced→(A303-5 新发票)→invoiced;推送异常→failed;金蝶删单回调→作废还原 | InvoiceApplicationServiceImpl + 定时 |
| `blade_invoice_receipt.kingdee_push_status` | unsynced / pushed / assigned(终) / failed | 同前版;A801-3 回写匹配键已修复为登记 id(异常回收/补偿/补登生效) | InvoiceReceiptServiceImpl + 定时 |
| `blade_customer_archive.kingdee_status` | none / synced / failed / exist(终) | 同前版;提审新增 A801-5 黑名单拦截(不产生状态) | CustomerArchiveServiceImpl + 定时 |
| `blade_kingdee_bill_lock.lock_status` ★新增 | failed / locked / unlocked | 先落 failed 占位→lockBusiData 成功→locked;补偿重试(try_count<5)或金蝶解锁/删单回调→unlocked | KingdeeBillLockServiceImpl + 定时 + 手动端点 |
| `blade_receipt_claim.kingdee_bill_status` | none / pushed / approved / received(终) / failed / voided | none→(A301-3)→pushed→(A301-1: C)→approved→(D)→received;已推送禁止作废 | ReceiptFlowServiceImpl + ReceiptClaimRecordServiceImpl |
| `blade_kingdee_receipt_flow.claim_status` | unclaimed / partial / claimed | 认领分摊→partial/claimed;全部作废→unclaimed;公告同步不覆盖 | ReceiptFlowServiceImpl |
---
## 10. 定时任务与配置开关汇总
全部在 `blade-transport/task/`,Spring `@Scheduled`,统一 single-flight(`KingdeeSyncLockSupport`,`blade.lock.enabled=false` 降级直跑)。
| 任务类 | 方法 | 覆盖业务(接口) | cron 键(默认) | 开关键(代码默认) | 手动端点 |
|---|---|---|---|---|---|
| PaymentKingdeeResultTask | syncKingdeePaymentResult | ① A103-2/A103-4 付款结果 ② A302-4 应收勾稽 ③ A303-5 开票结果 | `thirdParty.bfi.payResult.cron`(`0 */10 * * * ?`) | `thirdParty.bfi.payResult.syncEnabled`(false;**Nacos 样例 dev/prod 已显式 true**) | `/payment-application/sync-kingdee-result`、`/formal-settlement/sync-kingdee-ar-result`、`/invoice-application/sync-kingdee-invoice-result` |
| PaymentKingdeeResultTask | syncKingdeeReceivedInvoiceResult | A801-3 收票结果回写 | `thirdParty.bfi.receivedInvoiceResult.cron`(每10分钟) | `…receivedInvoiceResult.syncEnabled`(false) | `/invoice-receipt/sync-kingdee-result` |
| CustomerKingdeeSyncTask | syncKingdeeCustomerData | A902-1 复查 + A902-2 重推 + 金蝶停用联动 | `thirdParty.bfi.customer.cron`(`0 0 2 * * ?`) | `…customer.syncEnabled`(false;KS000067 残留已移除) | `/customer-archive/sync-kingdee` |
| **ReceiptClaimPushTask** ★新增 | pushClaimToKingdee | A301-3 认领推送(按流水归集,前置重查 A301-2) | `thirdParty.bfi.receiptClaimPush.cron`(每10分钟) | `…receiptClaimPush.syncEnabled`(false) | `/receipt-flow/push-kingdee` |
| **InvoiceReceiptKingdeeSyncTask** ★新增 | syncKingdeeInvoicePool | A801-1 全票池瘦身同步 + 绑定回填 | `thirdParty.bfi.invoiceReceipt.cron`(每30分钟) | `…invoiceReceipt.syncEnabled`(false);增量窗口 `…incrementDays`(30);票种白名单 `…invoiceTypes` | `/invoice-receipt/sync-invoice-pool` |
| **KingdeeAutoSyncRetryTask** ★新增 | retryKingdeeFailed | 4 域 failed 单据重推(结算/付款/开票/收票;不扫 unsynced;无次数上限,靠幂等预检防重) | `thirdParty.bfi.autoSync.retryCron`(每10分钟) | `…autoSync.retryEnabled`(false) | 各域 `/sync-kingdee` |
| **KingdeeBillLockRetryTask** ★新增 | retryFailedBillLocks | 锁单失败补偿(failed 且 try_count<5 重调 lockBusiData) | `thirdParty.bfi.billLock.retryCron`(每5分钟) | `…billLock.retryEnabled`(false) | `/formal-settlement/retry-kingdee-bill-lock` |
**审批自动同步开关(监听器,非定时)**:`thirdParty.bfi.autoSync.settlement / payment / invoiceApply / receipt`(默认全 false)。
**其他业务键**:`thirdParty.bfi.claimAnnounce.claimusers`(A301-2 认领人,**必配**)、`…claimAnnounce.months`(6)、`thirdparty.bfi.blacklist.blockRiskWarning`(false)、`thirdparty.bfi.material.*`(物料映射)。
> ⚠️ Nacos 样例(`doc/nacos/`)仅显式配置了 `payResult.syncEnabled=true`;`receiptClaimPush/invoiceReceipt` 样例仍为 false;`customer/receivedInvoiceResult/autoSync/billLock` 键未出现在样例中——生效值取决于各环境实际 Nacos,缺省走代码 false。**开启生产定时前逐项核对第 12 节风险清单。**
---
## 11. 操作流程手册
### 11.1 一次性前置准备(实施/运维)
| # | 事项 | 操作 |
|---|---|---|
| 1 | 部门金蝶组织编码 | 部门档案维护 `kingdee_dept_code`;未维护的部门推送组织取数会直接拦截 |
| 2 | 主数据 SQL | 执行 `doc/sql/transport/kingdee_master_data_init_20260924.sql`(计量单位档案等);认领链路执行 `blade_receipt_flow_claim_20260924.sql`;10-13 迁移脚本见 `doc/sql/transport/lqj/` |
| 3 | Nacos 业务配置 | `thirdParty.bfi.claimAnnounce.claimusers`(认领人,与金蝶公告 claimusers 一致)、`thirdParty.bfi.invoiceReceipt.invoiceTypes`(票池同步票种白名单)、`thirdparty.bfi.material.*`(物料映射,缺省有默认值但建议显式配置) |
| 4 | 定时开关按需开启 | 见第 10 节表:付款结果回写样例已开;其余按环境显式开启 `syncEnabled/retryEnabled` |
| 5 | 审批自动同步开关 | `thirdParty.bfi.autoSync.settlement/payment/invoiceApply/receipt` 按域开启;未开启时走页面「同步金蝶」手动推送 |
| 6 | 客商收款账户 | 维护 `blade_customer_receipt_account` 联行号(cnaps_code)——付款申请/预付款/客商推送均取真实联行号,缺失拦截 |
### 11.2 日常业务操作时序(财务/业务)
```
① 客商建档
新增客商 → 录统一社会信用代码(自动查金蝶回填)→ 上传发票信息 → 提交审批
(后端自动过 A801-5 黑名单,A 类命中被拦截)→ 审批通过 → 后端自动推送 A902-2
→ 列表 kingdee_status:synced(金蝶受理)→ 定时复查升级 exist(金蝶审核通过)
② 结算(应付/应收)
预结算/正式结算单编辑:非 RMB 币种选择汇率日期 → 自动带出金蝶汇率 → 提交审批
→ 审批通过(autoSync.settlement 开启时自动推送;或列表勾选点「同步金蝶」)
→ 成功后列表显示金蝶单号 + 「金蝶锁定中」标签(单据已被金蝶锁定,不可编辑/作废)
③ 付款
付款申请(关联已同步结算单)→ 审批通过(autoSync.payment 开启时自动下推;或「批量同步」)
→ 后端校验应付已生成 → A103-1 下推/预付款 A103-3 直推 → 锁单
→ 等 A103-2/A103-4 定时回写付款状态与付款记录(或点「同步付款结果」立即回写)
→ 状态流转 synced→paying→paid/closed,付款记录含凭证 JSON
④ 开票
开票申请(关联已同步应收结算单)→ 审批通过(autoSync.invoiceApply 开启时自动按张次推送;或「批量同步」)
→ 每张发票张次生成一张金蝶开票申请单 → 锁单 → 开票申请列表显示逗号拼接金蝶单号
→ 等 A303-5 定时回写发票结果(或手动端点)→ 状态 invoiced
→ 正式结算单详情「开票发票」区查看/下载发票文件
⑤ 收票(进项)
收票登记表单:发票号放大镜从金蝶票池选票(镜像读,票号未命中自动回源)→ 保存(重查闸门校验可用性)
→ 分摊到一张审批通过应付结算单 → 审批通过(autoSync.receipt 开启时自动推送;或「批量同步」)
→ A303-2 保存→pushed→A302-6 指定应付→assigned(终态)
→ 等 A801-3 定时回写(pushed 断点自动补偿重发 A302-6)
⑥ 收款认领
收款流水页「流水同步」拉取金蝶认领公告 → 流水详情/批量认领:选审批通过应收结算单按比例分摊(本地落库)
→ 「推送金蝶」(行内手动,或等 ReceiptClaimPushTask 定时)→ 金蝶生成收款单
→ 认领记录页「查询金蝶认领结果」回写真实收款单号与状态(approved/received)
注意:已推送金蝶的认领不可作废,需金蝶侧人工处置
⑦ 查看附件
金蝶票池/认领发票/收票附件的「查看/下载」统一走后端 A801-2 代理(PDF 版式优先)
```
### 11.3 异常处理操作
| 场景 | 现象 | 处理 |
|---|---|---|
| 推送失败(failed) | 列表金蝶状态 failed + 失败原因;审批时前端提示「审批通过,但自动同步金蝶失败:xxx」 | 修复原因后:客商/结算/付款/开票页手动「同步金蝶」重推;或等 `KingdeeAutoSyncRetryTask`(开启后每 10 分钟自动重推 failed) |
| 锁单失败 | 列表「锁定失败」标签;`blade_kingdee_bill_lock` failed 记录 | 等 `KingdeeBillLockRetryTask` 自动补偿(≤5 次);或 `POST /formal-settlement/retry-kingdee-bill-lock` 手动补偿;人工重推会重置计数 |
| 金蝶侧删单 | 金蝶回调 `POST /kingdee-bfi/unLockNotifyBusiData` | 自动还原:结算单/开票申请作废+状态回 unsynced 清单号(可重推);付款申请置 failed 清单号(不作废源单) |
| 单据被锁定后需修改 | 「金蝶锁定中」禁编辑/作废 | 只能在金蝶侧解锁/删单,触发回调还原后再改(无本地强制解锁入口,有意设计) |
| 付款单在金蝶被删且无回调 | A103-2 连续 2 轮(约 20 分钟)查不到 | 自动消失解锁置 failed,按 failed 重推 |
| 断链重推(远端已落单本地未同步) | 重推时预检命中 | 自动幂等回填单号跳过重复推送,无需人工清库 |
| 收票单发票状态异常 | 定时回写发现红冲/作废 → failed「已回收推送状态」 | 人工在金蝶侧处理后重新登记/推送 |
| 票池选不到票 | 镜像无数据 | 检查全票池同步开关与 `invoiceTypes` 白名单;或 `POST /invoice-receipt/sync-invoice-pool` 手动同步;票号精确查会自动回源 |
| 收款流水同步为空 | A301-2 查不到公告 | 检查 Nacos `claimAnnounce.claimusers` 是否与金蝶公告认领人一致 |
| 定时任务不跑/重复跑 | — | 核对 Nacos 开关键名(第 10 节);single-flight 锁保证多副本/手动不并发,`blade.lock.enabled=false` 时无锁 |
---
## 12. 风险与待确认清单(2026-10-14)
### 12.1 已关闭(旧总纲遗留,当前代码已核实修复)
| 旧问题 | 状态 |
|---|---|
| A801-3 定时回写按发票号匹配防重键(回写失效) | ✅ 已修复(登记 id,`InvoiceReceiptServiceImpl:1003-1009`) |
| `if(true)` 调试组合块(强制/随机物料) | ✅ 全部删除,真实拆分+配置映射 |
| 客商定时 KS000067 硬编码 | ✅ 已移除 |
| 客商推送 bankNumber 硬编码 / A103-3 e_bebank_number 取名称字段 | ✅ 均改取客商收款账户联行号 |
| 结算/开票失败不落 failed | ✅ 统一落 failed(03b34310) |
| 结算单认领无保存重查闸门 | ✅ 已实现 assertInvoicesClaimable |
### 12.2 仍需关注 / 联调确认
| # | 事项 | 证据/说明 |
|---|---|---|
| 1 | **A103-1 勾稽键口径**:代码传本地组装的应付防重键,`PaymentApplicationServiceImpl:1431-1443` 注释仍写「A102-4 反查 awrp_entry_code_p、禁止自行拼接」——注释与代码矛盾,金蝶侧付款计划分录标识是否等于防重键需联调实测 | 高优先 |
| 2 | **项目编码 999 残留**:A102-2/A302-2/A103-1/开票申请仍传测试值 999(仅 A103-3 已用真实项目编码) | 上线前处理 |
| 3 | **计量单位不送**:分录 DTO 有字段无赋值——需与金蝶确认不送计量单位是否影响单据校验 | 联调确认 |
| 4 | **A301-3 字段待金蝶确认**:`recpaytype_number`(101)/`paymenttype`/`recpayer` 取值口径 | 联调确认 |
| 5 | **A301-2 claimusers 未配置风险**:缺省空串调 A301-2 查不到数据(无报错),需在 Nacos 必配 | 配置项 |
| 6 | **其他链路不校验客商 kingdee_status**:客商未推送/未审核也会带编码推单(软依赖) | 既有风险 |
| 7 | **正式结算单作废未接 checkEditable**:仅靠前端锁状态标签禁用,接口层可绕过 | 建议补拦截 |
| 8 | **failed 重推无次数上限**:`KingdeeAutoSyncRetryTask` 每 10 分钟扫全量 failed,持续失败的单据会反复调金蝶(靠预检防重但不封顶) | 观察线上量 |
| 9 | **付款申请前端单笔「同步金蝶」按钮未绑定**(`handleSyncRow` 死代码):单笔入口仅剩审批自动同步 | 前端待补 |
| 10 | **旅客运输/代收车船税单据体**:DTO 已建未接(仅 freights 接入) | 按需迭代 |
| 11 | **方案 vs 代码差异(沿旧总纲)**:付款多次下推未实现(单分录单次);附件已落地(A902 4 链路)✅;收款流水同步已有定时(receiptClaimPush)✅;黑名单 A801-5 已实现 ✅ | — |
---
## 附:文档关系
- 本文为新总纲(基线 2026-10-13 `805ca9d3`),替代 09-27 版;单链路细节仍以 `doc/docs/业务流程/` 对应专项文档为深入参考,冲突处以本文 + 当前代码为准。
- 09-27 版中被本文修订的关键结论:A801-3 回写缺陷、票池「实时回源无定时」架构、收款认领「全手动无定时」、分录调试覆盖、物料映射常量、failed 落库口径、A801-5/A801-2/A902「未实现」状态。
@@ -0,0 +1,318 @@
# 金蝶(星瀚)BFI 对接操作手册(纯操作版)
> 整理日期:2026-10-14
> 适用对象:TMS 业务操作人员(财务/业务)、系统管理员、实施运维。
> 本文只讲**怎么操作**:在哪个页面、点什么按钮、前提是什么、成功的标志是什么、失败了怎么处理。
> 接口与代码细节见同目录《2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册)》。
---
## 1. 全局须知
### 1.1 金蝶单据状态含义(列表里看到的字段)
| 状态值 | 含义 | 还会变吗 |
|---|---|---|
| unsynced | 未推送金蝶 | 会(推送成功变 synced) |
| synced | 已推送金蝶,金蝶受理 | 会(继续走后续状态) |
| failed | 推送失败(有失败原因) | 会(重推成功变 synced) |
| paying | 金蝶付款中(仅付款申请) | 会(付款完成变 paid) |
| paid / closed | 已付款 / 已关闭(终态) | 不变 |
| invoiced | 已开票(终态,开票申请) | 不变 |
| assigned | 已指定应付(终态,收票) | 不变 |
| exist | 金蝶已有该客商且审核通过(终态,客商) | 不变 |
### 1.2 「金蝶锁定」标签是什么
结算单、付款申请、开票申请推送成功后,TMS 会**自动锁定金蝶侧单据**。列表出现红色「金蝶锁定中」标签时:**该单据不可修改、不可作废**。要改动只能先在金蝶侧解锁/删单,金蝶会回调 TMS 自动还原本地状态。
### 1.3 两条推送路径
- **自动**:审批「通过」时系统自动推送金蝶(需管理员开启对应开关,见第 4 节);开启后审批接口会直接返回推送结果,失败时页面提示「审批通过,但自动同步金蝶失败:xxx」。
- **手动**:列表勾选单据点工具栏「同步金蝶 / 批量同步 / 推送金蝶」按钮。开关没开时用这条路径。
两条路径效果完全一样,重复点不会重复推送(系统自动查重)。
---
## 2. 一次性准备(实施/管理员操作)
| # | 事项 | 操作人 | 操作内容 | 不做的后果 |
|---|---|---|---|---|
| 1 | 部门金蝶组织编码 | 管理员 | 在部门档案维护「金蝶组织编码」字段 | 该部门单据推送时直接报错拦截 |
| 2 | 客商收款账户联行号 | 财务 | 客商档案维护收款账户(账号/开户行,**联行号必填**) | 付款推送、客商推送报错拦截 |
| 3 | 主数据初始化 SQL | 运维 | 执行 `doc/sql/transport/` 下金蝶相关脚本(计量单位等主数据、认领表、10-13 迁移脚本 `lqj/`) | 对应链路数据缺失 |
| 4 | 认领人配置 | 运维 | Nacos 配 `thirdParty.bfi.claimAnnounce.claimusers`(值须与金蝶认领公告的认领人一致) | 收款流水同步查不到数据(不报错) |
| 5 | 票池票种白名单 | 运维 | Nacos 配 `thirdParty.bfi.invoiceReceipt.invoiceTypes`(要同步的发票种类编码,逗号分隔) | 票池同步不到对应票种 |
| 6 | 物料映射 | 运维 | Nacos 配 `thirdparty.bfi.material.*`(有默认值,建议显式确认) | 推送报「运输方式未配置」类拦截 |
| 7 | 定时任务开关 | 运维 | 按第 4 节清单逐项开启 | 只能全靠手动按钮 |
| 8 | 审批自动同步开关 | 运维 | 按第 4 节 `autoSync.*` 按域开启 | 审批通过后不自动推送,走手动 |
---
## 3. 日常操作流程
### 3.1 客商建档与同步
**页面:客商管理 → 客商档案**
![客商档案列表:行内「同步金蝶」链接仅在审批通过且推送失败(failed)的单据上显示;审批通过的客商由系统自动推送](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/01-客商档案列表.png)
1. 点「新增」,录入**统一社会信用代码**(满 18 位自动查金蝶回填工商信息并锁定相关字段)。
- 若提示金蝶侧该客商「审核中」——等金蝶审核完再保存/推送。
2. 维护**收款账户(联行号必填)**和发票信息(纳税人识别号用于黑名单校验)。
3. 提交审批:系统自动做**黑名单校验**。
- 提交被拦「命中黑名单」→ 该客商为国资委黑名单 A 类,**不能提交**,走线下处置。
4. 审批「通过」→ 系统自动推送金蝶 → 列表 `kingdee_status` 变 **synced**。
- 变 **failed** → 看失败原因(悬停/详情),修复后点行内「同步金蝶」重推。
5. synced 是「金蝶已受理」;金蝶审核通过后由定时任务升级为 **exist**(终态,无需操作)。
推送成功后金蝶侧效果(供确认推送结果时对照):
![金蝶客户列表收到推送的客商:编码=TMS 客商编号(KS000073),客户分类 1101 内部客户/1102 外部客商](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a902-金蝶客户接收.png)
![TMS 客商档案与金蝶客户档案对照:编码一致、以统一社会信用代码为关联键](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a902-客商编码对照.png)
![新增客商档案:统一社会信用代码录入满 18 位自动到金蝶查询并回填工商信息](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/02-客商档案新增-信用代码自动查询.png)
> 注意:审批通过后的**档案变更不会重新推送金蝶**。改了信用代码等关键信息需人工确认金蝶侧一致性。
### 3.2 预结算单(正式结算与预付款的前置)
**页面:结算管理 → 预结算单**
预结算单**本身不推送金蝶**(列表无金蝶状态字段),它是正式结算和进度预付的前置单据,与金蝶的关联有两处:①非人民币项目在此维护汇率(从金蝶自动带出);②进度预付款在金蝶付讫后自动回写本单的垫资记录。
![预结算单列表:工具栏「新增 / 预付申请 / 尾款结算」;草稿行可 编辑/删除/提交,审批中可 通过/驳回](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/11-预结算单列表.png)
1. 制单:「新增」录入结算金额与明细;**非人民币项目**:选择币种和汇率日期 → 系统自动从金蝶带出结算汇率并重算本位币。
![预结算单编辑页:汇率日期/结算汇率字段(人民币时禁用为 1.00,外币选择日期后自动带出)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/12-预结算单-汇率字段.png)
2. 提交审批 → 审批「通过」(或驳回后修改重新提交)。
3. 审批通过后的三个出口:
- **发起预付**:工具栏「预付申请」(限应付且已审批、未转正式)→ 生成进度预付付款申请,走 3.4 预付款推送金蝶;
- **转正式结算**:在正式结算单页面新建时选择本预结算单作为来源(可多张合并)→ 转入 3.3 正式结算推送;转正式后本单被锁定(节点显示「已锁定(正式结算)」并回写正式结算单号);
- **尾款结算**:应付单生成尾款结算单号(同样需先转入正式结算推金蝶)。
4. 注意:**正式结算单的汇率不自动继承预结算单**,转正式后需在正式结算单编辑时重新选汇率日期带出(见 3.8),否则推送金蝶不带汇率。
5. 金蝶付款回写联动:进度预付在金蝶付讫(paid)后,系统自动在本单垫资记录回写付款金额与金蝶付款单号,并刷新已付/未付汇总——无需手工登记(异常时管理员可用回写接口手工补)。
### 3.3 结算单推送(应付 / 应收)
**页面:结算管理 → 正式结算单(应付 / 应收 Tab)**
![正式结算单:应付/应收同页两个 Tab;工具栏「同步金蝶」需先勾选一条审批通过的单据;列表含结算汇率、金蝶单据状态、金蝶锁状态等列](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/03-正式结算单-同步金蝶.png)
前提:结算单已审批通过;非人民币结算单已维护汇率(见 3.8);客商已 synced/exist。
1. **自动路径**:审批「通过」时自动推送(开关开启时)。
2. **手动路径**:勾选一条单据 → 工具栏「同步金蝶」。
- 按钮是单选操作,一次推一张。
3. 成功标志:列表出现**金蝶单号**,状态变 **synced**,并出现「金蝶锁定中」标签。
4. 失败处理:状态变 failed → 看失败原因 → 常见原因:
- 「请先同步金蝶应付单」类 → 关联结算单还没推(应付/应收/收票有先后依赖,见 3.6);
- 客商编码取不到 → 先完成 3.1;
- 部门金蝶组织编码缺失 → 找管理员维护。
5. 纠偏:如果怀疑推送实际成功但本地状态没更新,等定时勾稽(或联系运维手动触发),系统会自动核对金蝶侧并纠正状态/补单号。
推送成功后金蝶侧效果(供确认推送结果时对照):
![金蝶财务应付单与 TMS 结算单对照:金蝶单上的「外部系统单据号」=TMS 防重键,应付金额与明细行(物料/车号/货物)逐行对应](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a102-应付单对照.png)
![金蝶财务应收单与 TMS 结算单对照:应收金额、费用合计对应,明细行物料/车号/运输货物逐行对应](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a302-应收单对照.png)
### 3.4 付款申请推送与结果回写
**页面:收付款管理 → 付款管理**
![付款管理:工具栏「批量同步」推送付款申请;列表自带「金蝶单据号 / 金蝶单据状态 / 金蝶锁状态 / 附件同步」列——图中首行即「已生成 + 金蝶锁定中 + 附件已上传」的已推送单据](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/04-付款管理-批量同步.png)
前提:付款申请已审批通过,且**关联的结算单已推送金蝶(synced)**。
1. 推送:
- 自动路径:审批「通过」时自动下推。
- 手动路径:勾选**审批通过**的单据 → 工具栏「批量同步」(一次可多张,单张失败不影响其他)。
- 预付款(项目垫资/进度垫资)走同样的按钮,系统自动走预付款通道。
2. 成功标志:状态 **synced** + 金蝶单号 + 锁定标签。
3. 查付款结果:
- 正常等定时任务回写(默认每 10 分钟,开关开启时);
- 急用点工具栏「**同步付款结果**」(勾选单据后显示;若未见到该按钮,说明当前环境以定时回写为主,等下一轮即可)。
4. 结果状态流转:synced → **paying**(金蝶付款中)→ **paid**(已付款,含付款记录/凭证信息)/ **closed**(关闭)。
5. 失败与特殊场景:
- failed → 修复后重推(批量同步按钮即可,系统会自动跳过金蝶已有的);
- 单据在金蝶被删:回调后自动置 failed 并清单号,直接重推;
- 金蝶删了但回调丢了:约 20 分钟后系统自动检测到「查不到」并解锁置 failed,再重推。
推送成功后金蝶侧效果(供确认推送结果时对照):
![TMS 付款申请与金蝶付款申请单对照:申请付款金额、收款账号、结算方式逐项对应;金蝶单号 FKSQ-… 与 TMS 列表回写的金蝶单号一致](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a103-付款申请对照.png)
![预付款推送效果:TMS 进度预付在金蝶生成「其他付款申请」,预付款金额、收款账户对应,附件随单上传](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a103-3-预付款对照.png)
> 注意:推送后单据被金蝶锁定,**不能改金额、不能作废**。发现金额错误必须先金蝶侧处理。
### 3.5 开票申请推送与开票结果
**页面:收付款管理 → 开票管理**
![开票管理:工具栏「批量同步」逐张推送(按发票张次拆单);搜索区可按「金蝶单据状态」筛选](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/05-开票管理-批量同步.png)
前提:开票申请已审批通过,且**关联结算单的应收单已推送金蝶**。
1. 推送:审批「通过」自动推送,或勾选后工具栏「批量同步」(逐张处理)。
- 系统按**发票张次**拆分:一张申请 3 张发票会生成 3 张金蝶开票申请单。
2. 成功标志:状态 **synced**,金蝶单号多张时逗号拼接显示;逐张锁定。
3. 查开票结果:等定时回写(默认每 10 分钟),状态变 **invoiced**(终态)。
4. 查看发票文件:正式结算单详情 →「开票发票」区 → 查看/下载(PDF 优先)。
推送成功后金蝶侧效果(供确认推送结果时对照):
![开票申请按发票张次拆分:TMS 开票内容第1张/第2张分别生成金蝶开票申请单,购销方、不含税金额逐项对应](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a303-1-开票张次对照.png)
### 3.6 收票登记(进项发票)
**页面:收付款管理 → 收票管理**
![收票管理:工具栏「批量同步」推送收票单并指定应付](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/06-收票管理-批量同步.png)
**硬性前提**:一张票只能登记一次;推送前必须已分摊到**恰好一张审批通过的应付结算单**,且该结算单已推送金蝶。
1. 选票:表单里点发票号放大镜 → 弹出「查询金蝶票据池」→ 勾选 → 确认(带出票面信息)。
![查询金蝶票据池弹窗:按发票号/日期/税号/票种等筛选;每行可「查看/下载」发票原件(PDF 优先);点「选择」带出到表单](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/07-收票登记-金蝶票池选票.png)
- 票池数据来自定时同步(默认每 30 分钟);票号精确搜索时若本地没有会**自动去金蝶现查**。
- 查不到票 → 等同步或让运维检查票种白名单配置/手动触发同步(见 4.2)。
2. 保存:系统自动**重查金蝶票池校验可用性**——票已被别人登记/认领/红冲/作废会保存失败并提示原因。
3. 分摊应付结算单后提交审批。
4. 推送:审批「通过」自动推送,或勾选 → 工具栏「批量同步」。
5. 状态流转:synced→(系统内)→ **pushed**(金蝶收票单已生成)→ **assigned**(已指定应付,终态)。
- 卡在 pushed 不用管:定时任务会自动补发「指定应付」。
- failed 且提示发票状态异常(红冲/作废)→ 该票在金蝶已不可用,需人工处理,不要重推。
6. 附件:表单附件区可查看/下载发票原件(系统自动从金蝶取,PDF 优先)。
推送成功后金蝶侧状态与效果(供确认推送结果时对照):
![收票管理列表「金蝶状态」列:已指定应付(终态)/推送失败(含失败原因)/未推送](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-收票金蝶状态列表.png)
![推送的单据附件已上传到金蝶侧单据的「附件」区](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a902-附件上传金蝶侧.png)
![正式结算单详情「发票信息」区:认领的发票可 查看/下载(系统从金蝶取原件,PDF 优先);付款信息区回显付款申请金蝶单号](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a801-2-发票查看下载.png)
### 3.7 收款认领
**页面:收付款管理 → 收款流水、认领记录**
这是「金蝶收到钱 → TMS 认领到结算单 → 回推金蝶」的流程:
1. **同步流水**:收款流水页 → 工具栏「**手动同步流水**」→ 拉取金蝶认领公告。
![收款流水:工具栏「手动同步流水」拉取金蝶认领公告、「批量认领」进入认领;列表展示认领通知单/付款人/对方户名等](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/08-收款流水-同步与推送.png)
- 查不到数据先检查认领人配置(第 2 节 #4)。
2. **认领**:点流水进入详情(或工具栏「批量认领」)→ 选择**审批通过且已推送金蝶的应收结算单**、填分摊金额 → 确认。
- 认领此时只落本地,不推金蝶。
- 分摊总额不能超过该流水未认领金额。
3. **推送金蝶**:
- 手动:流水详情/符合条件的行内「推送金蝶」;
- 自动:定时任务(默认每 10 分钟,开关开启时)自动推送认领完成的流水。
- 成功标志:流水显示金蝶收款单号、推送状态 pushed。
4. **回写结果**:认领记录页 → 工具栏「**查询金蝶认领结果**」→ 状态推进 approved(金蝶已审核)/ **received**(已收款,终态)。
![认领记录:工具栏「查询金蝶认领结果」回写金蝶收款单状态](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/09-认领记录-查询金蝶认领结果.png)
5. **作废限制**:已推送金蝶的认领**不能作废**(提示去金蝶侧人工处置)。未推送的可以作废。
推送成功后金蝶侧效果(供确认推送结果时对照):
![金蝶认领公告与 TMS 收款流水对照:「手动同步流水」拉取的就是金蝶认领公告列表里的 RLTZ 通知单](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-2-公告同步对照.png)
![TMS 认领记录推送后,金蝶「认领处理列表」生成对应认领单(认领通知单单号一致)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-认领推送对照.png)
![金蝶认领单详情:认领结果行的核心单据编号=TMS 分摊结算单对应的金蝶单据](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-金蝶认领结果.png)
### 3.8 汇率维护(非人民币结算)
结算单(预结算/正式结算)编辑时:选择非 RMB 币种 → 选择**汇率日期** → 系统自动从金蝶带出汇率并重算本位币金额。带不出(金蝶该日无汇率)→ 换日期或联系金蝶侧确认。
![结算单详情:汇率日期/结算汇率字段由金蝶汇率接口自动带出,本位币金额随之重算](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/10-结算单-汇率自动带出.png)
外币结算推送后,金蝶侧按该汇率自动换算本位币:
![金蝶侧效果:汇率 6.7367、金额(本位币)自动换算,与 TMS 结算单的本位币合计一致](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a801-4-外币汇率推送.png)
---
## 4. 定时任务与开关(运维视角)
### 4.1 任务清单(Nacos `thirdparty.bfi.*` 配置)
| 任务 | 干什么 | 开关键 | cron 键(默认频率) | 手动替代按钮 |
|---|---|---|---|---|
| 付款结果回写 | 付款状态(paid/closed)+ 付款记录/凭证回写;顺带应收勾稽纠偏、开票结果回写 | `thirdParty.bfi.payResult.syncEnabled`(样例已开) | `…payResult.cron`(10 分钟) | 付款「同步付款结果」、开票结果手动端点、结算勾稽手动端点 |
| 收票结果回写 | 收票单状态回写、pushed 断点补发指定应付 | `…receivedInvoiceResult.syncEnabled` | 每 10 分钟 | 收票「同步金蝶结果」 |
| 客商同步 | 金蝶审核状态复查(synced→exist)、停用联动、failed 重推 | `…customer.syncEnabled` | 每天 02:00 | 客商「同步金蝶」 |
| 收款认领推送 | 自动推送认领完成的流水到金蝶 | `…receiptClaimPush.syncEnabled` | 每 10 分钟 | 流水行内「推送金蝶」 |
| 全票池同步 | 拉金蝶全票池发票到本地镜像、补绑定 | `…invoiceReceipt.syncEnabled` | 每 30 分钟 | 手动端点 `POST /invoice-receipt/sync-invoice-pool` |
| failed 重推 | 自动重推所有推送失败的结算/付款/开票/收票单 | `…autoSync.retryEnabled` | 每 10 分钟 | 各页面「同步金蝶」 |
| 锁单补偿 | 推送成功但金蝶锁定失败的自动补锁(≤5 次) | `…billLock.retryEnabled` | 每 5 分钟 | 手动端点 `POST /formal-settlement/retry-kingdee-bill-lock` |
### 4.2 审批自动同步开关(按业务域独立)
`thirdParty.bfi.autoSync.settlement`(结算单)/ `…payment`(付款)/ `…invoiceApply`(开票)/ `…receipt`(收票),默认全关。**代码默认全部关闭,必须在 Nacos 显式开启**;客商档案审批推送不受此开关控制(始终自动)。
### 4.3 运维注意
- 所有定时任务带**分布式锁**:多实例部署只跑一份;手动按钮和定时共用锁,定时运行中手动触发会直接跳过(属正常)。
- 开了「failed 重推」后,持续失败的单据每 10 分钟会重试一次,**修复根因**(配置/数据)比反复观察更重要。
- 手动触发全票池同步与定时是同一逻辑,数据量大时耐心等待,不要反复点。
---
## 5. 常见问题与异常处理(FAQ)
| 现象 | 原因 | 处理 |
|---|---|---|
| 推送结算单不带汇率 | 正式结算单没维护汇率(预结算单的汇率**不自动继承**到正式结算单) | 在正式结算单编辑页重选汇率日期自动带出后再推 |
| 推送报「客商编码」类错误 | 客商未推送金蝶或档案缺数据 | 先完成客商同步(3.1),再推单据 |
| 推送报「请先同步金蝶应付单/应收单」 | 前置单据未推送 | 顺序:结算单(应付/应收) → 付款/开票/收票;先把前置结算单推送 |
| 推送报组织/部门编码类错误 | 部门没维护金蝶组织编码 | 管理员补维护后重推 |
| 推送报联行号类错误 | 客商收款账户联行号缺失 | 财务补维护收款账户后重推 |
| 提交客商审批被拦黑名单 | 纳税人识别号命中国资委黑名单(A 类) | 不可提交,走线下处置;B 类提示仅告警默认放行 |
| 审批通过但提示「自动同步金蝶失败」 | 推送失败但审批已生效 | 看失败原因 → 修复 → 手动「同步金蝶」重推(单据不会丢) |
| 列表「锁定失败」标签 | 推送成功但金蝶锁单接口失败 | 系统自动补偿(≤5 次);急用让运维调手动补偿端点 |
| 单据要改但显示「金蝶锁定中」 | 已推送并被金蝶锁定 | 到金蝶侧解锁/删单 → TMS 自动还原后即可编辑;不可强行作废 |
| 付款状态长时间不更新 | 回写定时未开启或金蝶侧未处理 | 点「同步付款结果」;仍无 → 金蝶侧查付款处理单 |
| 开票申请 synced 但没发票信息 | 开票结果未回写 | 等定时/联系运维手动触发;确认金蝶票易通已开票 |
| 票池选票查不到 | 镜像未同步到该票种 | 检查票种白名单配置;运维手动触发全票池同步;票号精确搜索可触发自动现查 |
| 保存收票登记被拦「可用金额为0」 | 票已被占用/认领/红冲 | 换票或在金蝶确认票据状态 |
| 收票卡在 pushed 不动 | 「指定应付」步骤未完成 | 开启收票回写定时自动补发,或重推一次 |
| 收款流水同步为空 | 认领人配置与金蝶不一致 | 检查 Nacos `claimAnnounce.claimusers` |
| 认领推送报「认领公告id缺失」 | 流水没同步过就认领了 | 先「流水同步」再操作 |
| 认领推送报金蝶内码缺失 | 分摊的结算单没推送金蝶 | 先完成结算单(应收)推送(3.3) |
| 已推送认领想作废 | 金蝶已生成收款单 | 不能作废,去金蝶侧人工处置 |
| 断网/金蝶宕机期间推了一半 | 部分单据 failed | 恢复后 failed 重推任务(或手动)自动续推,系统查重不会重复生成金蝶单据 |
---
## 6. 附:操作入口速查表
| 页面 | 按钮/操作 | 作用 |
|---|---|---|
| 客商档案 | (表单)录信用代码 | 自动查金蝶回填工商信息 |
| 客商档案 | 行内「同步金蝶」 | 失败重推客商 |
| 预结算单 | 工具栏「预付申请」/「尾款结算」 | 审批通过后发起进度预付 / 生成尾款结算号(预结算本身不推金蝶) |
| 正式结算单 | 新建时选择来源预结算单 | 预结算转正式结算(转后预结算锁定) |
| 正式结算单 | 工具栏「同步金蝶」(单选) | 推送应付/应收单到金蝶 |
| 正式结算单 | (编辑)汇率日期 | 自动带出金蝶汇率 |
| 正式结算单 | (编辑)发票认领「查询/确认识领」 | 从金蝶票池选票认领到结算单 |
| 付款申请 | 工具栏「批量同步」 | 下推付款申请/预付款到金蝶 |
| 付款申请 | 工具栏「同步付款结果」 | 立即回写付款状态与付款记录 |
| 开票申请 | 工具栏「批量同步」 | 按发票张次推送开票申请 |
| 收票登记 | (表单)发票号放大镜 | 从金蝶票池选票 |
| 收票登记 | 工具栏「批量同步」 | 推送收票单+指定应付 |
| 收票登记 | (表单)附件查看/下载 | 从金蝶取发票原件(PDF 优先) |
| 收款流水 | 工具栏「手动同步流水」 | 拉取金蝶认领公告 |
| 收款流水 | (详情/工具栏「批量认领」)认领 | 本地认领到结算单 |
| 收款流水 | 行内「推送金蝶」 | 推送认领结果到金蝶 |
| 认领记录 | 工具栏「查询金蝶认领结果」 | 回写金蝶收款单状态 |
| 运维专用 | `POST /invoice-receipt/sync-invoice-pool` | 手动全票池同步 |
| 运维专用 | `POST /formal-settlement/retry-kingdee-bill-lock` | 手动补偿锁单 |
| 运维专用 | `POST /formal-settlement/sync-kingdee-ar-result` | 手动应收勾稽纠偏 |
| 运维专用 | `POST /invoice-application/sync-kingdee-invoice-result` | 手动开票结果回写 |
---
> 本文基于 2026-10-14 代码现状整理;系统升级后按钮与提示如有出入,以页面实际为准,并核对全链路总纲文档的更新说明。