Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
2026-10-17 01:12:05 +08:00
403 changed files with 44738 additions and 845 deletions
@@ -0,0 +1,250 @@
# 正式付款同步金蝶(星瀚)技术文档:财务应付单 + 付款申请下推
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)
> 对接系统:金蝶云·星瀚(业财融合 BFI,经 iPaaS 网关)
> 关联文档:`doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`、`doc/docs/业财融合系统接口文档.xlsx`、`doc/docs/业务流程/付款申请同步金蝶(星瀚)技术文档.md`(A103-3 预付款路径)
> 最后更新:2026-09-18(字段映射与联调默认值按当前代码重新梳理)
---
## 1. 背景与范围
正式结算单发起付款申请后,付款管理(`blade_payment_application`,`paymentType = settlement_payment`)生成一条记录;审批通过后勾选点"批量同步",需**先推送财务应付单(A102-2),再由应付下推付款申请(A103-1)**,两步完成后才视为同步成功。
- 触发入口:付款管理页面(前端 `tms-erp-web/src/views/payment/payment-application.vue`)"批量同步"按钮,权限码 `payment_application_sync`
- 对接接口:
- **A102-2 财务应付单保存** `POST /ierp/kapi/v2/awrp/ap/ap/ap_finapbill/save`
- **A103-1 付款申请保存(应付下推)** `POST /ierp/kapi/v2/awrp/ap/ap/ap_finapbill/push`
- 路径划分:**正式付款**(settlement_payment)走本文档 A102-2 → A103-1 两步链路;**预付款**(project_advance / progress_advance)维持原 A103-3 直推付款申请单,见《付款申请同步金蝶(星瀚)技术文档》
- 调用方式:前端 → TMS 后端转发 → 星瀚(OAuth2 凭证仅存后端,前端不持有)
- 落库结果:A103-1 返回的付款申请单 id 写入 `kingdee_bill_no`(既有字段);**A102-2 返回的应付单 id 不落库**,仅校验保存成功,后续如需可经 A102-4 按防重键(`awrp_src_sys` + `awrp_src_code`)查询(`BfiServiceImpl.queryApBill` 已封装),本次无表结构变更
---
## 2. 业务流程
### 2.1 整体链路(对应需求文档 4.3 步骤 4)
```
正式结算单发起付款申请 → 付款管理生成记录 → 审批通过(approved)
→ 批量同步(本文档)
① A102-2 财务应付单保存(防重键幂等)
② A103-1 应付下推付款申请(分录关联①的 awrp_entry_code)
→ 金蝶财务共享审批(黑盒) → 付款处理 → 付款完成
→ 后续(待建设):A103-2/A103-4 定时回写、A902 附件、B901 锁单
```
### 2.2 批量同步时序
```
前端 payment-application.vue blade-transport 后端 星瀚(BFI)
│ 勾选 approved 记录 │ │
│ POST /payment-application/ │ │
│ sync-kingdee-batch [id...] │ │
│──────────────────────────────────────>│ 逐单循环(单条失败不中断批次) │
│ │ FOR UPDATE 锁单 + approved 校验 │
│ │ ① 组装 BfiApBillSaveDTO │
│ │ POST ap_finapbill/save ──────────────>│
│ │ <── result[]{id,...} ─────────────────│
│ │ 仅校验保存成功(应付单id不落库) │
│ │ ② 组装 BfiPayApplyPushDTO │
│ │ entry.awrp_finapbill_entryid │
│ │ = paymentNo + "-1" │
│ │ POST ap_finapbill/push ──────────────>│
│ │ <── result[]{id,...} ─────────────────│
│ │ kingdeeBillNo = 付款申请单id │
│ │ kingdeeStatus = synced │
│ │ 下游台账联动(结算付款记录/预付台账) │
│ <── R<List<String>>(逐单结果)────────│ │
```
### 2.3 前置条件与幂等约定
| 约定 | 说明 |
| --- | --- |
| 状态校验 | 仅 `approvalStatus = approved` 允许同步 |
| 幂等跳过 | `kingdeeStatus = synced/paying/paid/closed` 的单据直接返回已存单据号,不重复推送 |
| ①防重键 | 星瀚应付单侧 `awrp_src_code`(=付款申请号_结算明细单据号1_单据号2...,去重拼接)+ `awrp_src_sys`,②失败后重推时①走更新语义,不重复生成应付单 |
| ②防重键 | 付款申请单侧 `awrp_src_code`(=paymentNo)+ `awrp_src_sys`;分录行 `awrp_entry_code`(=paymentNo + "-序号")不得重复 |
| ②关联键 | `awrp_finapbill_entryid` = ①明细的 `awrp_entry_code`(=paymentNo + "-序号"),**不依赖①返回的内部 id** |
| 失败重推 | 任一步失败置 `kingdeeStatus = failed`,可重新勾选同步(①已成功的应付单按防重键走更新语义) |
| 前置校验 | 分录解析前校验:未关联正式结算明细、货物类型未维护档案编码、计量单位为空均报错;未关联合同、组织编码为空、结算币种为空时同步失败 |
---
## 3. 接口定义
### 3.1 A102-2 财务应付单保存
- URL:`{thirdParty.bfi.baseUrl}/ierp/kapi/v2/awrp/ap/ap/ap_finapbill/save`
- Method:`POST`,请求体 `{"data": [ {主表字段, "detailentry":[明细]} ]}`
- 返回结构与 A103-3 一致:`data.result[].id` 为星瀚应付单内部主键,`billStatus=false` 时以 `errors` 抛出业务异常,`errorCode/message/status` 由 `BfiServiceImpl.commonCall` 统一校验。
### 3.2 A103-1 付款申请保存(应付下推)
- URL:`{thirdParty.bfi.baseUrl}/ierp/kapi/v2/awrp/ap/ap/ap_finapbill/push`
- Method:`POST`,请求体 `{"data": [ {主表字段, "entry":[分录]} ]}`
- 返回 `result[].id` 为星瀚付款申请单内部主键(写入 `kingdee_bill_no`),`result[].number` 为星瀚单据编号(FKSQ-xxxx,暂不落库),`result[].type = Push`。
---
## 4. 字段映射关系
### 4.1 A102-2 主表(`BfiApBillSaveDTO` ← 付款申请实体 + 合同 + 常量)
| 接口字段 | 含义 | 必填 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
| billtypeid_number | 单据类型.编码 | 是 | 按映射字典转换:`KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME`(默认"服务采购应付")经 `AP_BILL_BILLTYPE_MAP` 名称→编码 | 路由规则见 5.3;未维护名称报错 |
| org_number | 结算组织.编码 | 是 | **合同所属组织**:`contract.organizationId` → `dept.deptCode`(复用 `bfiOrgNumber`) | 缺失报错 |
| bizdate | 单据日期 | 是 | `applyDate`(空取当天) | 沿用 yyyyMMdd,**待确认是否需 yyyy-MM-dd** |
| asstacttype | 往来类型 | 是 | **按合同类别路由**(`bfiAsstactType`):`contractCategory = 客户合同` → `bd_customer`,否则 → `bd_supplier` | 与结算类型路由口径一致 |
| asstact_number | 往来户.编码 | 是 | **合同乙方**(`bfiAsstactNumber`):`contract.partyB` → 客商档案按全称/简称匹配取 `customerCode` | 乙方为空或客商档案未维护编码均报错,不回退原值 |
| purmode | 付款方式 | 是 | 常量 `BFI_AP_BILL_PUR_MODE_CASH` | `CASH` 现购(系统自动 100% 推付款) |
| currency_number | 结算币别.货币代码 | 是 | **合同结算币种** `contract.settlementCurrency`(复用 `bfiCurrency`);`RMB` 统一映射为 `CNY` | 缺失报错 |
| payorg_number | 付款组织.编码 | 是 | 同 org_number | 采购组织 purorg_number 暂不传 |
| payproperty_number | 款项性质.编码 | 是 | 常量 `BFI_AP_BILL_PAY_PROPERTY` | 暂按 `2001`,**待金蝶确认** |
| isincludetax / ispricetotal | 录入含税价 / 录入总价 | 否 | 固定 `true` / `true`;`isintax` 固定 `false` | 录入总价免单价换算 |
| awrp_src_sys | 外部系统编号 | 是 | 配置 `thirdParty.bfi.payApply.srcSys` | 应付单候选键 |
| awrp_src_code | 外部系统来源单据号 | 是 | **`paymentNo + "_" + 结算明细单据号1_单据号2...`**(单据号去重、保持行序,`String.join("_")`) | 应付单候选键 |
| awrp_src_type | 外部单据类型 | 是 | 常量 `BFI_AP_BILL_SRC_TYPE` | 暂按 `TMS财务应付单`,**待金蝶确认** |
未传的可选字段(代码注释保留):exratetable_number/exratedate(汇率表/汇率日期)、asstactname(往来单位名称)、remark(备注)、awrp_src_bt(来源单据)、awrp_src_con_no(上游合同号)、settlementtype_number(结算方式)、taxrateid_number(税率)、purorg_number(采购组织)、duedate、bookdate、department_number、preentry(预付信息)等。
### 4.2 A102-2 明细(`BfiApBillSaveEntryDTO`,按货物编码+计量单位聚合,多行)
分录数据由 `resolveBfiEntryGroups` 解析:**货物费用行**(`blade_formal_settlement_detail_fee`,每货物一条)优先,无费用行时兜底**结算明细行**;按货物编码+计量单位聚合(一行含多种货物类型时金额数量均分),已移除按申请付款金额分摊逻辑,**金额直接取结算金额**。
| 接口字段 | 含义 | 必填 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
| material_number | 物料编码.编码 | 是 | **联调临时:常量 `BFI_AP_BILL_MATERIAL_NUMBER_TEST`(`SP0324`)**(调试块 `if(true)` 强制覆盖) | 真实来源:货物类型 → 货物类型档案 `cargoCode`,档案对齐后恢复 |
| measureunit_number | 计量单位.编码 | 是 | **联调临时:常量 `BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST`(`TON`)**(同上) | 真实来源:快照运费计算单位 `quantityUnit`(档案暂无编码字段) |
| quantity | 明细.数量 | 是 | 组内 `transportQuantity` 合计 | ≤0 兜底 1 |
| e_pricetaxtotal | 明细.应付金额 | 是 | 组内 `settlementAmountTax`(结算金额含税)合计 | **必传**,缺失金蝶报"应付金额不能为空" |
| e_amount | 明细.金额(不含税) | 否 | 组内 `settlementAmountNoTax`(结算金额不含税)合计 | |
| project_number | 项目.项目编码 | 是 | **联调临时:常量 `BFI_AP_BILL_PROJECT_NUMBER_TEST`(`999`)** | 真实来源:`projectId` → `ProjectApply.projectCode`,无项目传 `999` |
| awrp_entry_code | 外部系统来源分录标识 | 是 | `paymentNo + "-" + 组序号` | 候选键;②下推时以 `awrp_finapbill_entryid` 关联此行 |
未传:planentity 付款计划(不传由金蝶按明细自动汇总)、e_tax、taxrateid_number、e_remark、awrp_src_con_eno。
### 4.3 A103-1 主表(`BfiPayApplyPushDTO`)
| 接口字段 | 含义 | 必填 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
| awrp_src_code / awrp_src_sys | 来源单据号/外部系统编号 | 是 | `paymentNo` / 配置 `payApply.srcSys` | 付款申请单候选键 |
| applydate | 申请日期 | 是 | `applyDate`(空取当天) | 同 bizdate 格式 |
| applycause | 请款事由 | 否 | 复用 `bfiApplyCause` | 结算付款 → `结算付款申请` |
| payorg_number / purorg_number | 付款/采购组织.编码 | 否 | 同 A102-2 合同组织编码 | 传值更稳,不传金蝶按应付单自动带出 |
| exratetable_number / exratedate | 汇率表/汇率日期 | 否 | 配置复用 / 同 applydate | 不传金蝶自动带出 |
| awrp_textfield1 | 付款渠道 | 否 | 复用 `bfiPaymentMethodLabel`:`paymentMethod` 经字典 `pay_method` 翻译为标签 | |
| creator_name | 申请人 | 否 | `applicantName` | |
### 4.4 A103-1 分录(`BfiPayApplyPushEntryDTO`,与应付单明细一一对应多行)
| 接口字段 | 含义 | 必填 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
| e_paymenttype_number | 付款类型.编码 | 是 | 常量 `201` | 正式付款 |
| awrp_finapbill_entryid | 结算单分录id | 是 | `paymentNo + "-" + 组序号` | **对应 A102-2 明细 awrp_entry_code** |
| awrp_entry_code | 对方唯一行 id | 是 | `paymentNo + "-" + 组序号` | 不得重复 |
| e_assacct | 往来账户 | 是 | `bankAccount` | 收款账号(页面"收款账户") |
| e_bebank_number | 往来银行.编码 | 是 | `bankName` | **必填编码,当前暂传银行名称,待银行档案编码映射**(与 A103-3 遗留项一致) |
| e_applyamount | 申请金额 | 是 | 组内 `settlementAmountTax`(结算金额含税) | 与对应应付单分录 `e_pricetaxtotal` 一致 |
| e_expaydate | 期望付款日 | 否 | 同 applydate | |
| project_number | 项目.项目编码 | 是 | **联调临时:常量 `BFI_AP_BILL_PROJECT_NUMBER_TEST`(`999`)**(真实取值 `bfiProjectNumber` 代码注释保留) | 无项目传 `999` |
| e_remark | 备注 | 否 | `remark` | |
| awrp_src_type | 外部单据类型 | 是 | 复用配置 `payApply.srcType` | |
未传:e_duedate、e_settlementtype_number、e_costcenter_number、awrp_splb_number。
---
## 5. 常量与待确认清单
### 5.1 设计约定
A102-2 所需的金蝶侧固定档案编码**不进 Nacos 配置**,统一定义在常量类 `org.springblade.transport.constant.KingdeeBfiConstant`;金蝶侧确认取值后**直接改常量重新发包**即可。
### 5.2 常量清单(`org.springblade.transport.constant.KingdeeBfiConstant`)
| 常量 | 当前值 | 用途 | 状态 |
| --- | --- | --- | --- |
| BFI_AP_BILL_BILLTYPE_NAME | `服务采购应付` | A102-2 单据类型默认名称,经 `AP_BILL_BILLTYPE_MAP` 转换为编码(`ApFin_service_BT_S`) | 待金蝶确认 |
| AP_BILL_BILLTYPE_MAP | 9 项映射 | 财务应付单类型字典(借贷项调整/费用采购应付/其他应付/产品委外应付/标准采购应付/采购费用应付/销售费用应付/服务采购应付/资产采购应付) | 全量字典 |
| AR_BILL_BILLTYPE_MAP | 5 项映射 | 财务应收单类型字典,当前付款链路未启用 | 全量字典 |
| PAY_APPLY_BILLTYPE_MAP | 2 项映射 | 付款申请单类型字典(采购付款申请/其他付款申请),使用规则待业务确认 | 全量字典 |
| BFI_SUPPLIER_ASSTACT_TYPE | `bd_supplier` | 往来类型-供应商 | 启用 |
| BFI_CUSTOMER_ASSTACT_TYPE | `bd_customer` | 往来类型-客户(合同类别为"客户合同"时使用) | 启用 |
| BFI_AP_BILL_PAY_PROPERTY | `2001` | A102-2 款项性质 | 待金蝶确认 |
| BFI_AP_BILL_PUR_MODE_CASH | `CASH` | A102-2 付款方式(现购,自动 100% 推付款) | 启用 |
| BFI_AP_BILL_PUR_MODE_CREDIT | `CREDIT` | A102-2 付款方式(赊购,金蝶侧可调金额) | 备用 |
| BFI_AP_BILL_MATERIAL_NUMBER_TEST | `SP0324` | **联调临时**:明细物料编码,调试块强制覆盖真实取值 | 联调后恢复档案取值 |
| BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST | `TON` | **联调临时**:明细计量单位编码,同上 | 联调后恢复档案取值 |
| BFI_AP_BILL_PROJECT_NUMBER_TEST | `999` | **联调临时**:A102-2/A103-1 项目编码 | 档案对齐后恢复 `bfiProjectNumber` |
| BFI_AP_BILL_SRC_TYPE | `TMS财务应付单` | A102-2 外部单据类型 | 待金蝶确认 |
### 5.3 单据类型路由与映射转换
| 正式结算单结算类型 | 对应金蝶单据 | 单据类型字典 | 当前状态 |
| --- | --- | --- | --- |
| 应付(payable) | 财务应付单(A102-2 `/ap_finapbill/save`) | `AP_BILL_BILLTYPE_MAP`(9 项) | **已启用**,默认"服务采购应付" |
| 应收(receivable) | 财务应收单(A302-2 `/ar_finarbill/save`) | `AR_BILL_BILLTYPE_MAP`(5 项) | 未启用,同步时拦截报错,推送规则待业务确认 |
转换流程:同步时按 `settlementId` 查正式结算单 `settlementType` 路由;应付走 A102-2,billtype 由默认名称"服务采购应付"经映射字典转编码;查不到结算单(聚合多结算单场景)暂按应付处理。
### 5.4 其他待确认项(代码内以 TODO 注释标注)
| 项 | 现状 | 待确认内容 |
| --- | --- | --- |
| 日期格式 | bizdate/applydate 沿用 A103-3 的 `yyyyMMdd` | 金蝶若校验 `yyyy-MM-dd` 需统一调整 |
| 结算方式/税率 | settlementtype_number、taxrateid_number 不传 | 金蝶是否强制、编码取值 |
| 预付抵扣 | preentry(y_billtype/y_billid/y_settleamt)不传 | 存在预付款抵扣时是否需传,本期不传、后续迭代 |
| 银行编码 | e_bebank_number 暂传银行名称 | 必填编码,待银行档案编码映射(与 A103-3 遗留项相同) |
| 物料/计量单位/项目编码 | 联调期临时传金蝶测试档案值(SP0324/TON/999,见 4.2/4.4 与 5.2) | 档案对齐后移除 A102-2 明细中的调试块并恢复真实取值 |
| 币种映射 | 合同结算币种 `RMB` 统一映射为 `CNY`(`bfiCurrency`) | 长期保留 |
| 付款申请单/收入确认单类型映射 | 字典已存 `PAY_APPLY_BILLTYPE_MAP`(2 项);收入确认单(ar_revcfmbill_*)字典未入库 | 付款申请单、收入确认单的映射使用规则待业务确认 |
---
## 6. 数据库变更
**本次无表结构变更。** A103-1 返回的付款申请单 id 仍落既有字段 `blade_payment_application.kingdee_bill_no`;A102-2 返回的应付单 id 不落库(下推关联、幂等重推均不依赖该 id),后续 A102-4 勾稽、应付单附件上传、反审核删除等场景需要时,按防重键 `awrp_src_sys + awrp_src_code(=paymentNo)` 经 `queryApBill` 查询即可。前端列表也无新增列。
---
## 7. 异常处理与批量语义
| 场景 | 行为 |
| --- | --- |
| 单笔同步 `syncKingdee` | 校验失败/推送失败置 `failed` 并抛 `ServiceException`,前端提示原因(含金蝶 errors 明细) |
| ①成功②失败 | `kingdeeStatus = failed`;重推时①按防重键走更新语义,继续② |
| 批量同步 `syncKingdeeBatch` | 逐单循环,**单条失败不中断批次**;返回结果列表附 `付款申请[id]同步失败:原因` |
| 货物类型未维护档案编码 / 计量单位为空 | 分录解析(`collectEntryGroups`)时拦截,报错提示补充档案 |
| 合同乙方为空 / 客商档案未维护客商编码 | 往来户取值(`bfiAsstactNumber`)时拦截,报错提示合同号与乙方名称 |
| 金蝶接口级失败 | `BfiServiceImpl.commonCall` 抛 `BfiException`,同上处理 |
| 重复点击同步 | 已 `synced` 单据跳过;`failed`/`unsynced` 允许重推 |
---
## 8. 遗留事项
1. **物料/计量单位/项目编码(联调临时)**:A102-2 明细当前经调试块固定传金蝶测试档案值(`SP0324`/`TON`,见 `pushBfiApBill` 内 `if(true)` 调试代码),项目编码 A102-2/A103-1 固定传 `999`;金蝶侧档案对齐后需**删除调试块**并恢复真实取值(货物类型档案编码 / 快照计量单位 / `bfiProjectNumber`)。
2. **应付单 id 不落库**:A102-2 仅校验保存成功;后续 A102-4 勾稽、应付单附件、反审核删除等需要 id 的场景,按防重键经 `queryApBill` 二次查询。
3. **银行编码映射**:e_bebank_number 暂传银行名称,待建立银行档案编码映射(与预付款路径遗留项相同)。
4. **预付抵扣 preentry**:正式付款涉及预付款抵扣时是否传预付信息,本期不传、后续迭代。
5. **付款完成模拟待拆除**:同步成功后仍保留 `paidAmount = 申请金额`、`mockPaymentRecords()` 模拟付款记录(单号前缀 `MOCK-PAY-`),真实流程应由 A103-2/A103-4 定时回写驱动,与预付款路径共用该遗留项。
6. **正式结算单同步金蝶仍为 mock**:`FormalSettlementServiceImpl.syncKingdee` 当前仅生成 `K3AP+时间戳` 假单号,与本文档的应付单推送(A102-2)无关联,后续如需结算单直推应付单需另行设计。
7. **单据编号 number**(AP/FKSQ-xxxx)暂未落库,如需展示可经 A102-4/A103-2 查询。
8. **单据类型映射规则待确认**:付款申请单(采购/其他)、收入确认单(ar_revcfmbill_*)的类型使用规则待业务确认;应收结算单对应财务应收单(A302-2)的推送链路未启用,当前同步时拦截报错。
---
## 9. 涉及代码位置
| 内容 | 位置 |
| --- | --- |
| 同步入口/分支 | `blade-service/blade-transport/.../service/impl/PaymentApplicationServiceImpl.java`(syncKingdee / syncKingdeeBatch / pushBfiApBill / pushBfiPayApplyPush / pushBfiPayApply / requireBfiSaveSuccess / requireBfiBillId) |
| A102-2 常量 | `blade-service/blade-transport/.../constant/KingdeeBfiConstant.java` |
| Controller | `blade-service/blade-transport/.../controller/PaymentApplicationController.java`(/sync-kingdee、/sync-kingdee-batch,本次无改动) |
| BFI 客户端 | `blade-third-party-api/blade-bfi-api/.../feign/IBfiClient.java`(AP_BILL_SAVE_URL / PAY_APPLY_PUSH_URL) |
| BFI 服务封装 | `blade-third-party-api/blade-bfi-api/.../service/impl/BfiServiceImpl.java`(saveApBill / pushPayApply,本次无改动) |
| 请求/返回模型 | `blade-bfi-api/.../pojo/dto/BfiApBillSaveDTO.java`、`BfiApBillSaveEntryDTO.java`、`BfiPayApplyPushDTO.java`、`BfiPayApplyPushEntryDTO.java` |
| 前端页面 | `tms-erp-web/src/views/payment/payment-application.vue`(无逻辑改动);`src/option/payment/paymentApplication.js`(无改动) |
@@ -0,0 +1,101 @@
# BFI 分页查询通用翻页工具(BfiUtil.queryAllPages)技术文档
> 适用系统:TMS(BladeX 微服务平台,模块:`blade-third-party-api/blade-bfi-api`)
> 组件类型:通用工具类(非业务流程),服务业财融合全部 `BfiPageVO` 分页查询接口
> 关联文档:`2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md`
> 最后更新:2026-09-17
---
## 1. 背景与范围
`IBfiService` 中共有 9 个查询接口返回 `BfiPageVO<T>` 分页结构,各接口的分页翻页逻辑完全一致(页码递增、空页/末页终止、防死循环保护)。原实现在 `PaymentApplicationServiceImpl` 中以私有方法 `queryPage` 承载,无法被其他查询场景复用,现抽取为 `BfiUtil.queryAllPages` 通用工具。
设计目标:
- **一处实现,全部查询复用**:付款结果回写(A103-2/A103-4)之外,后续 A102-4 应付勾稽、A302-4 应收核对、A303-4 开票结果回写、A801-3/A301-1/A301-2 收票收款认领等场景接入时零成本复用
- **调用方只关心"取第 N 页"**:翻页推进、终止判断、全量汇总由工具类负责
- **防死循环**:金蝶侧 `lastPage` 异常(恒为 false)时按最大页数保护终止
## 2. 适用接口清单
| 接口编号 | 名称 | `IBfiService` 方法 | 返回结构 |
| --- | --- | --- | --- |
| A103-2 | 付款申请单查询 | `queryPayApply` | `BfiPageVO<BfiPayApplyVO>` |
| A103-4 | 付款处理查询 | `queryPayBill` | `BfiPageVO<BfiPayBillVO>` |
| A102-4 | 财务应付单查询 | `queryApBill` | `BfiPageVO<BfiApBillVO>` |
| A302-4 | 财务应收单查询 | `queryArBill` | `BfiPageVO<BfiArBillVO>` |
| A303-4 | 开票申请单查询 | `queryInvoiceApply` | `BfiPageVO<BfiInvoiceApplyVO>` |
| A902-1 | 客户查询 | `queryCustomer` | `BfiPageVO<BfiCustomerVO>` |
| A801-3 | 收票单查询 | `queryReceivedInvoice` | `BfiPageVO<BfiReceivedInvoiceVO>` |
| A301-1 | 收款单查询 | `queryRecBill` | `BfiPageVO<BfiRecBillVO>` |
| A301-2 | 收款认领公告查询 | `queryClaimAnnounce` | `BfiPageVO<BfiClaimAnnounceVO>` |
不适用:`queryInvoicePool`(A801-1,rim 契约限制不能全量查询,已改为 pageNo/pageSize 单页透传,由 `BfiServiceImpl` 直接单次调用)、`queryExchangeRate`(A801-4,单页调用由 `BfiServiceImpl` 内部处理)。
## 3. API 定义
```java
// 默认最大页数保护(50 页)
public static <T> List<T> queryAllPages(IntFunction<BfiPageVO<T>> pageFetcher)
// 自定义最大页数保护
public static <T> List<T> queryAllPages(IntFunction<BfiPageVO<T>> pageFetcher, int maxPages)
```
| 参数 | 说明 |
| --- | --- |
| `pageFetcher` | 页码 → 分页结果函数。lambda 内负责按传入页码设置请求参数(页码必设,页大小按各接口限制设置)并调用 `IBfiService` 对应查询方法 |
| `maxPages` | 最大页数保护,防止金蝶侧 `lastPage` 异常导致死循环;默认 `BfiUtil.DEFAULT_MAX_PAGES = 50` |
| 返回值 | 全量数据行汇总列表(按页序追加);无数据时返回空列表 |
## 4. 翻页终止条件(按判断顺序)
| 序 | 条件 | 说明 |
| --- | --- | --- |
| 1 | 返回页为 null 或数据行为空 | 本页无数据即止 |
| 2 | `lastPage = true` | 金蝶标识已最后一页 |
| 3 | `rows.size() < 响应体 pageSize` | 末页数据不满一页,无需继续;**pageSize 取响应体返回值**(`BfiPageVO.pageSize`),调用方无需额外传入,响应体未返回 pageSize 时自动退化为依赖条件 2 与最大页数保护 |
| 4 | 页码超过 `maxPages` | 防御性终止,异常场景兜底 |
## 5. 用法示例
```java
// A103-2 付款申请单查询:来源单据号批量查询
private List<BfiPayApplyVO> queryPayApplyPages(List<String> awrpSrcCodes) {
BfiPayApplyQueryDTO param = new BfiPayApplyQueryDTO();
param.setAwrpSrcCode(awrpSrcCodes);
param.setAwrpSrcSys(bfiPayApplySrcSys);
return BfiUtil.queryAllPages(pageNo -> {
param.setPageNo(pageNo);
param.setPageSize(PAGE_SIZE);
return bfiService.queryPayApply(param);
});
}
```
lambda 内通过闭包复用同一参数对象,仅需覆盖 `pageNo`;请求参数的各接口约束(如 A103-4 付款组织编码必填、时间范围不超过 1 个月)由调用方在构造参数时保证。
## 6. 设计说明
| 决策 | 说明 |
| --- | --- |
| 抽取为 `IntFunction<BfiPageVO<T>>` 而非传入 DTO | 各查询接口的入参 DTO、Feign 方法签名各不相同,仅"按页码取结果"语义一致;函数式入参对调用方约束最小,DTO 字段设置留在调用方(编译期类型安全) |
| 不再需要 `rowsGetter` 参数 | 全部查询统一返回 `BfiPageVO`,`getRows` 内聚到工具类;早期版本传入 `BfiPageVO::getRows` 属冗余 |
| pageSize 判满取响应体 | 早期版本要求调用方传入请求 pageSize,两处易失配;响应体 `pageSize` 为金蝶实际执行值,更可靠 |
| 空列表而非 null | 无数据返回空列表,调用方无需判空 |
## 7. 迁移记录
| 变更 | 内容 |
| --- | --- |
| 新增 | `BfiUtil.queryAllPages` 两个重载 + `DEFAULT_MAX_PAGES` 常量 |
| 删除 | `PaymentApplicationServiceImpl` 私有方法 `queryPage` 及 `PAY_RESULT_MAX_PAGES` 常量 |
| 替换 | 付款结果回写 `queryPayApplyPages` / `queryPayBillPages` 改调 `BfiUtil.queryAllPages` |
## 8. 涉及代码位置
| 内容 | 位置 |
| --- | --- |
| 工具类 | `blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/util/BfiUtil.java` |
| 使用方 | `blade-service/blade-transport/.../service/impl/PaymentApplicationServiceImpl.java`(付款结果回写)、`KingdeeApBillSyncSupport.java`(应付单同步)、`CustomerArchiveServiceImpl.java`(客户档案同步) |
@@ -0,0 +1,167 @@
# 付款管理金蝶付款结果定时回写(A103-2 付款申请单查询 + A103-4 付款处理查询)技术文档
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)
> 对接系统:金蝶云·星瀚(业财融合 BFI,经 iPaaS 网关)
> 关联文档:`doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`(4.3 节步骤 11)、`付款申请同步金蝶(星瀚)技术文档.md`、`2026-09-16-付款管理正式付款同步金蝶(财务应付单+付款申请下推)技术文档.md`、`2026-09-17-BFI分页查询通用翻页工具(BfiUtil-queryAllPages)技术文档.md`
> 最后更新:2026-09-17
---
## 1. 背景与范围
付款申请推送金蝶(A103-1/A103-3)成功后,金蝶侧依次完成:财务共享审批(黑盒,机器人处理)→ 付款处理 → 付款完成。原实现将"付款完成"整段模拟(`paidAmount = 申请金额`、`MOCK-PAY-` 前缀付款记录、履约付款数据直接置 `paid`),本文档建设真实回写链路并停用模拟:
- 触发入口:
- **定时任务**:`PaymentKingdeeResultTask`,`@Scheduled` 默认每 10 分钟(`thirdParty.bfi.payResult.cron`),开关 `thirdParty.bfi.payResult.syncEnabled`
- **手动触发**:付款管理页面"同步付款结果"按钮(权限码复用 `payment_application_sync`)→ `POST /payment-application/sync-kingdee-result`
- 对接接口:
- **A103-2 付款申请单查询** `POST /ierp/kapi/v2/awrp/ap/ap_payapply/query`:按 `awrp_src_code`(=paymentNo)批量查询,回填金蝶单据编号(billno)、单据状态、付款状态、已付金额
- **A103-4 付款处理查询** `POST /ierp/kapi/v2/awrp/cas/cas_paybill/query`:按付款人(公司)编码 + 业务日期窗口查询,回填付款记录(付款单号、付款日期、付款金额、整体报文留档)
- 联动更新:付款完成(paystatus=Alreadypay)时复用既有逻辑联动履约付款数据(预结算预付记录 `blade_pre_settlement_advance`、正式结算付款记录 `blade_formal_settlement_payment`)
## 2. 业务流程
### 2.1 集成链路(对应方案文档 4.3 节步骤 11)
```
付款申请已推送金蝶(kingdeeStatus = synced)
→ 定时任务(每10分钟)
→ A103-2 付款申请单查询(awrp_src_code 批量,50单/批)
回填 kingdee_bill_id / kingdee_bill_no(billno)/ kingdeeStatus / paid_amount
→ A103-4 付款处理查询(付款中/已付款单据,按付款组织分组 + 最近1个月窗口)
回填付款记录 blade_payment_application_record(并清理历史 MOCK-PAY- 模拟记录)
→ 付款完成(Alreadypay):联动履约付款数据(预结算预付、正式结算付款记录)
→ (后续环节)付款附件推送 A902、合同系统履约同步 C106-2,待建设
```
### 2.2 定时回写时序
```
PaymentKingdeeResultTask PaymentApplicationServiceImpl 星瀚(BFI)
│ @Scheduled 每10分钟 │ │
│ syncKingdeePaymentResult() ──────>│ │
│ │ ①查本地 approved + synced/paying │
│ │ ②awrp_src_code 分批(50) ─────────────────>│ A103-2
│ │ <── billno/billstatus/paystatus/entry ────│
│ │ 逐单独立事务:回填单号/状态/已付金额 │
│ │ Alreadypay → 联动履约付款数据 │
│ │ ③付款组织分组 + 最近1个月窗口 ──────────────>│ A103-4
│ │ <── 付款处理单(sourcebillid/sourcebillnumber)│
│ │ 勾稽匹配 → 回填付款记录(幂等) │
│ <── 执行摘要日志 ──────────────────│ │
```
### 2.3 查询范围与幂等约定
| 约定 | 说明 |
| --- | --- |
| A103-2 查询范围 | `approvalStatus = approved` 且 `kingdeeStatus IN (synced, paying)`;`paid/closed` 为终态不再轮询 |
| A103-2 防重键 | `awrp_src_code`(=paymentNo)+ `awrp_src_sys`(=WLTMS)批量查询,50 单/批,自动翻页(页大小 200,最大 50 页保护) |
| 同号多单 | 同一 `awrp_src_code` 返回多条时取 `modifytime` 最新的一条 |
| A103-4 查询范围 | 付款中/已付款单据(本轮 A103-2 查到 `paystatus ≠ Unpaid` 的)+ 已付款但尚无真实付款记录的单据(补偿轮询) |
| A103-4 入参约束 | 付款人(公司)编码必填(优先取 A103-2 返回 `payorg_number`,兜底按合同所属组织解析);业务日期窗口取最近 1 个月(接口限制不超过 1 个月) |
| A103-4 勾稽键 | `sourcebillid`(=付款申请单星瀚id `kingdee_bill_id`)优先,`sourcebillnumber`(=付款申请单编号 `kingdee_bill_no`)兜底 |
| 付款记录幂等 | 按 `payment_application_id + payment_no`(=付款处理单号)判重,存在则更新;回填时清理该申请单下 `MOCK-PAY-` 前缀的历史模拟记录 |
| 逐单隔离 | 每笔申请通过 `SpringUtil` 自代理走独立事务(`applyPayApplyResult` / `applyPayBillRecord`),单条失败不中断批次,异常记录日志后继续 |
### 2.4 状态映射(kingdeeStatus 扩展)
| A103-2 返回 | 本地 kingdeeStatus | 展示名 | 是否终态 |
| --- | --- | --- | --- |
| `paystatus = Alreadypay` | `paid` | 已付款 | 是(触发履约联动) |
| `billstatus = D`(已关闭)且未付款 | `closed` | 已关闭 | 是 |
| `paystatus = Inpayment` | `paying` | 付款中 | 否(继续轮询) |
| `billstatus = A暂存/B已提交/C已审核` + `paystatus = Unpaid` | `synced` | 已生成 | 否(继续轮询) |
既有状态保持不变:`unsynced` 未生成、`failed` 生成失败(可重推)。
## 3. 字段映射关系
### 3.1 A103-2 回写(`BfiPayApplyVO` → `PaymentApplication`)
| 星瀚字段 | 含义 | 落库字段 | 说明 |
| --- | --- | --- | --- |
| id | 星瀚单据内部主键 | `kingdee_bill_id` | A902 附件上传等后续环节使用 |
| billno | 单据编号(FKSQ-xxxx) | `kingdee_bill_no` | 展示用金蝶单据号;A103-4 勾稽键之一 |
| billstatus | 单据状态 A/B/C/D | `kingdee_status` | 见 2.4 状态映射 |
| paystatus | 付款状态 | `kingdee_status` | 见 2.4 状态映射 |
| entry[].e_paidamt | 分录已付金额(汇总) | `paid_amount` | 分录缺失且已付款时兜底取申请金额 |
| payorg_number | 付款组织编码 | —(内存) | A103-4 付款人(公司)编码入参 |
### 3.2 A103-4 回填(`BfiPayBillVO` → `PaymentApplicationRecord`)
| 星瀚字段 | 含义 | 落库字段 | 说明 |
| --- | --- | --- | --- |
| billno | 付款处理单号 | `payment_no` / `kingdee_bill_no` | 幂等判重键 |
| paydate | 付款日期 | `paid_date` | 取前 10 位解析 yyyy-MM-dd,解析失败置空并告警 |
| actpayamt | 付款金额 | `paid_amount` | 2 位小数 |
| 整体报文 | 付款处理单全量 JSON | `voucher_json` | 凭证号、银行流水、电子回单等信息留档追溯 |
## 4. 付款完成联动(履约付款数据)
付款状态变为 `paid` 时复用既有逻辑(与原模拟阶段一致,数据源改为真实回写值):
| 联动项 | 逻辑 | 位置 |
| --- | --- | --- |
| 预结算预付记录 | `syncPreSettlementAdvance`:`paid_amount` = 申请金额、`bill_status = paid`、`kingdee_advance_no` = 金蝶单据号,刷新预结算单预付汇总与正式结算来源预付统计 | PaymentApplicationServiceImpl |
| 正式结算付款记录 | `syncFormalSettlementPayments`:按申请单-结算单关联分摊已付金额,创建/更新 `blade_formal_settlement_payment`(`bill_status = paid`、金蝶单据号),刷新结算单付款汇总 | 同上 |
部分付款(`paying`)仅更新付款申请 `paid_amount` 与付款记录,**不**触发履约联动(待付款完成一次性联动)。
## 5. 模拟逻辑停用说明
- `mockPaymentRecords()`(`MOCK-PAY-` 付款记录模拟)与 `syncKingdee` 内的付款完成模拟代码段**保留方法体、注释调用处**并备注原因:联调环境无金蝶付款数据时可恢复注释模拟使用。
- `syncKingdee` 现仅落 `kingdee_bill_id` + `kingdee_status = synced`;`kingdee_bill_no`、`paid_amount`、付款记录、履约付款数据全部由定时回写驱动。
- 存量数据兼容:
- 已 `synced` 单据(`kingdee_bill_no` 存的是旧语义星瀚id)由任务首轮 A103-2 按 paymentNo 查询自动修正为 billno;
- 手动重推幂等返回兼容旧数据(`kingdee_bill_id` 为空时回退返回 `kingdee_bill_no`)。
## 6. 数据库变更
| 变更 | 内容 | 脚本 |
| --- | --- | --- |
| 新增列 | `blade_payment_application.kingdee_bill_id` varchar(32):金蝶单据id(星瀚内部主键) | `doc/sql/transport/blade_payment_application_kingdee_bill_id_20260917.sql`(幂等可重复执行) |
| 语义调整 | `kingdee_bill_no`:星瀚id → **金蝶单据编号 billno**(由回写任务回填) | 无 DDL |
| 建表模板 | `blade_payment_application_20260821.sql` 已同步包含新列 | 新装环境直接执行 |
## 7. 配置项(Nacos:`thirdParty.bfi.payResult.*`)
模板见 `doc/nacos/blade-dev.yaml`、`blade-prod.yaml`、`third-party-api.yaml`:
| 配置键 | 环境变量 | 默认值 | 说明 |
| --- | --- | --- | --- |
| thirdParty.bfi.payResult.syncEnabled | BFI_PAY_RESULT_SYNC_ENABLED | true | 回写任务开关 |
| thirdParty.bfi.payResult.cron | BFI_PAY_RESULT_CRON | 0 */10 * * * ? | 调度表达式(默认每10分钟) |
调度启用:`TransportApplication` 增加 `@EnableScheduling`。
## 8. 异常处理
| 场景 | 行为 |
| --- | --- |
| A103-2/A103-4 接口级失败(status=false / errorCode≠0) | `BfiServiceImpl.commonCall` 抛 `BfiException`,当前批次终止,下轮任务自动重试 |
| 单笔回写异常 | 记录 error 日志(含 paymentNo 与堆栈),计数后继续下一单 |
| A103-4 付款组织编码获取失败(无 A103-2 数据且合同缺失/组织编码为空) | warn 日志跳过该单,下轮重试 |
| 付款日期解析失败 | warn 日志,`paid_date` 置空,其余字段正常回填 |
| 手动触发与定时任务并发 | 同单以 `updateById` 主键更新兜底,最终一致;付款记录按单号判重不重复插入 |
## 9. 涉及代码位置
| 内容 | 位置 |
| --- | --- |
| 定时任务入口 | `blade-service/blade-transport/.../task/PaymentKingdeeResultTask.java` |
| 回写编排 | `blade-service/blade-transport/.../service/impl/PaymentApplicationServiceImpl.java`(syncKingdeePaymentResult :409 / applyPayApplyResult :512 / applyPayBillRecord :538 / queryPayApplyPages / queryPayBillPages / kingdeeResultStatus / matchPayBill) |
| 手动触发接口 | `blade-service/blade-transport/.../controller/PaymentApplicationController.java`(POST /sync-kingdee-result) |
| 状态常量 | `blade-service/blade-transport/.../constant/KingdeeBfiConstant.java`(KINGDEE_STATUS_* / BFI_PAY_STATUS_* / BFI_BILL_STATUS_CLOSED) |
| 实体/翻译 | `blade-transport-api/.../pojo/entity/PaymentApplication.java`(kingdeeBillId);`transport/wrapper/PaymentApplicationWrapper.java`(状态翻译) |
| BFI 客户端 | `blade-third-party-api/blade-bfi-api/.../service/IBfiService.java`(queryPayApply / queryPayBill,本次无改动);分页通用翻页 `util/BfiUtil.java`(queryAllPages,A102-4/A302-4/A303-4 等分页查询可复用) |
| 前端 | `tms-erp-web/src/views/payment/payment-application.vue`(同步付款结果按钮);`src/api/payment/paymentApplication.js`(syncKingdeeResult / kingdeeStatusOptions) |
| SQL | `doc/sql/transport/blade_payment_application_kingdee_bill_id_20260917.sql`;建表模板 `blade_payment_application_20260821.sql` |
| Nacos 配置模板 | `doc/nacos/blade-dev.yaml`、`blade-prod.yaml`、`third-party-api.yaml`(thirdParty.bfi.payResult.*) |
## 10. 遗留事项与联调确认
1. **A103-4 勾稽键语义待联调确认**:以 `sourcebillid = 付款申请单星瀚id`、`sourcebillnumber = 付款申请单编号(billno)` 假设实现,联调时需用真实付款处理单验证;若金蝶侧源单指向不同,仅需调整 `matchPayBill` 匹配逻辑。
2. **付款日期格式**:`paydate` 按前 10 位 yyyy-MM-dd 兼容解析,金蝶若返回纯时间戳需调整 `parseKingdeeDate`。
3. **A103-2 分页翻页**:`awrp_src_code` 批量查询理论单页返回(批次 50 < 页大小 200),翻页保护仅防御异常场景。
4. **后续环节待建设**:付款附件推送 A902(凭 `kingdee_bill_id`)、合同系统履约同步 C106-2、付款申请反审核异常回收(B901 解锁)。
@@ -0,0 +1,307 @@
# 客商档案同步金蝶(星瀚)技术文档:A902-1 查询回填 + A902-2 审批推送
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)、前端 Saber3(页面:客商档案)
> 对接系统:金蝶云·星瀚(业财融合 BFI,经 iPaaS 网关)
> 关联文档:`doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`(图 01 客户新增与查询)、`doc/docs/TMS业财融合方案.docx`
> 最后更新:2026-09-17
---
## 1. 背景与范围
按业财融合「SC-01 客商主数据协同」流程,TMS 新增客商时须先查询金蝶是否已有该客商(A902-1),已有则回填金蝶维护的工商信息;TMS 审批通过后,将金蝶不存在的客商推送至金蝶完成创建(A902-2),金蝶校验工商信息后由机器人自动审核,业务系统方可使用该客商。
**现状结论**:现有客商新增交互(前端 `tms-erp-web/src/views/vehicle/customer-archive.vue`、后端 `CustomerArchiveServiceImpl` 的 `submit → submitApproval → approve/reject` 链路)**均无金蝶调用**;blade-transport 仅暴露汇率查询 `BfiExchangeRateController`。底层能力已就绪:`blade-bfi-api` 的 `IBfiService.queryCustomer`(A902-1)、`saveCustomer`(A902-2)及配套 DTO/VO/Feign。
**本期范围**:
- 新增客商时按统一社会信用代码查询金蝶并自动回填(A902-1)。
- 客商准入审批通过后自动推送金蝶(A902-2),失败可重推。
- 金蝶已存在(已审核)的客商:回填建档,审批通过后**跳过推送**。
**不在本期范围**:
- 客商更新(工商/个人信息变更走人工流程,非接口)。
- 客商数据定时同步子流程(A902-1 / A901-1 定时任务)。
- 合同合规系统相对方同步。
**关键约定**(已与业务确认):
| 约定 | 决策 |
| --- | --- |
| A902-1 触发时机 | 统一社会信用代码输入满 18 位且通过本地格式校验后自动查询(A902-1 按信用代码查询,名称/法人为表单必填项但不参与查询) |
| A902-2 触发时机 | 审批通过(`approve`)后自动推送,推送失败不影响审批结果,支持列表重推 |
| 金蝶已存在(已审核) | 回填建档,审批通过后跳过 A902-2 推送 |
---
## 2. 业务流程
### 2.1 业财融合整体链路(SC-01 客商主数据协同)
```
TMS 新增客商,录入统一社会信用代码
→ A902-1 客户查询(本文档 3.1)
├─ 金蝶已审核(C):提示并回填金蝶工商信息(工商信息只允许金蝶维护,TMS 锁定不可改)
├─ 金蝶暂存/已提交(A/B):提示"金蝶审核中,请稍后重新添加",流程终止
└─ 金蝶不存在:正常填写(现有营业执照 OCR 逻辑不变)
→ 填写客商业务信息 → 提交客商准入审批(TMS 内部审批,不调金蝶)
→ 审批通过 → A902-2 客商保存推送金蝶(本文档 3.2,金蝶机器人自动审核)
→ 审批通过前 A902-1 复查到金蝶已审核的 → 跳过推送,仅本地建档
→ 金蝶客户新增成功后自动下推供应商(金蝶侧行为,TMS 无感知)
```
### 2.2 客商档案金蝶协同状态(`kingdeeStatus`)
| 值 | 含义 | 设置时机 | 后续动作 |
| --- | --- | --- | --- |
| `none` | 未推送 | 新增客商默认值 | 审批通过后参与推送 |
| `exist` | 金蝶已有档案 | A902-1 查到状态=已审核(C) | 审批通过后跳过推送(终态) |
| `synced` | 已推送成功 | A902-2 返回成功,回写金蝶客户编码 | 金蝶受理(status=B),A902-1 复查 status=C 已审核才是最终成功;定时任务每轮复查审核状态,审核中不重推(避免金蝶侧重复),重推时幂等跳过 |
| `failed` | 推送失败 / 金蝶审核中 | A902-2 失败,或复查到 A/B 状态 | 列表「同步金蝶」按钮重推 |
### 2.3 前端查询回填时序(新增态)
```
customer-archive.vue blade-transport 星瀚(BFI)
│ 统一社会信用代码输入满18位 │ │
│ 且通过格式校验(防抖500ms) │ │
│ GET /bfi/customer/query-by-credit-code│ │
│──────────────────────────────────────>│ A902-1:pageNo=1,pageSize=1, │
│ │ societycreditcode=[code] ───────>│
│ │ <── rows[](含status/工商信息) ──│
│ <── R<BfiCustomerVO|null> ────────────│ │
│ status=C:提示"金蝶已创建该客户, │ │
│ 将获取金蝶的工商信息",回填并锁定 │ │
│ status=A/B:提示"金蝶审核中,请稍后重新添加",阻断保存 │
│ 不存在:正常填写 │ │
```
### 2.4 审批通过后推送时序
```
CustomerArchiveController CustomerArchiveServiceImpl 星瀚(BFI)
│ POST /customer-archive/approve │ │
│──────────────────────────────────────>│ approve() 落库审批通过 │
│ <── R 成功 ────────────────────────────│ │
│ POST /customer-archive/sync-kingdee │(审核通过后前端自动触发;失败亦可为 │
│──────────────────────────────────────>│ 人工在列表重推) │
│ │ 1. 校验 approvalStatus=approved │
│ │ 2. kingdeeStatus∈{synced,exist}→幂等返回 │
│ │ 3. A902-1 复查: │
│ │ 已审核(C)→置exist,跳过推送 │
│ │ 暂存/已提交(A/B)→置failed(金蝶审核中) │
│ │ 4. 不存在→组装DTO推 A902-2 ─────────────>│
│ │ <── result[]{billStatus,id/number} ─────│
│ │ 5. 成功→kingdeeNumber+synced │
│ │ 失败→failed+失败原因 │
│ <── R<推送结果> ───────────────────────│ │
```
> 实施说明:推送由后端 `/customer-archive/approve` 端点成功后**内部触发**(try-catch,推送失败不影响审批结果);`/sync-kingdee` 端点仅作为 `failed` 状态的人工重推入口,前端无需在审批成功后单独调用。
### 2.5 前置条件与幂等约定
| 约定 | 说明 |
| --- | --- |
| 推送前置状态 | 仅 `approvalStatus = approved` 允许推送金蝶 |
| 幂等跳过 | `kingdeeStatus ∈ {synced, exist}` 直接返回,不重复推送 |
| 防重键 | 星瀚侧以统一社会信用代码(`societycreditcode`)为候选键;TMS 侧 `validateUnique` 已保证信用代码本地唯一 |
| 失败重推 | `kingdeeStatus = failed` 时列表「同步金蝶」按钮可重推,权限码 `customer_archive_sync` |
| 推送失败不回滚审批 | 审核通过落库成功后推送;推送环节异常只置 `failed`,不影响审批结果 |
| 工商信息锁定 | 来源金蝶回填的工商字段 TMS 不允许变更,与「工商信息只在金蝶维护」规则一致 |
---
## 3. 接口定义
### 3.1 A902-1 客户查询
- URL:`GET {thirdParty.bfi.baseUrl}/ierp/kapi/v2/awrp/basedata/bd_customer/query`(可用 `thirdParty.bfi.customerQueryUrl` 覆盖)
- 鉴权:`access_token` 由 `BfiServiceImpl` 自动获取与缓存(OAuth2,有效期 2 小时),业务代码无需关心
- 本次调用参数:`pageNo=1`、`pageSize=1`、`societycreditcode=[统一社会信用代码]`;**不传 `status` 过滤**(需按返回状态区分三分支)
- 返回:`BfiPageVO<BfiCustomerVO>`,取 `rows` 第一条;关键返回字段见 4.1 回填映射表
- TMS 内部封装端点:`GET /blade-transport/bfi/customer/query-by-credit-code?creditCode={code}`,返回 `R<BfiCustomerVO>`(无数据返回 `R.data(null)`)
### 3.2 A902-2 客户保存
- URL:`POST {thirdParty.bfi.baseUrl}/ierp/kapi/v2/awrp/basedata/bd_customer/save`(可用 `thirdParty.bfi.customerSaveUrl` 覆盖)
- 请求体:`{"data": [ {…BfiCustomerSaveDTO 字段} ]}`(单条)
- 校验(`BfiServiceImpl.saveCustomer` 已内置):`name` 必填;`societycreditcode` 与 `idno` 不能同时为空;`entry_groupstandard` 必填
- 返回结构:
```json
{
"data": {
"failCount": "0",
"successCount": "1",
"result": [
{
"billIndex": 0,
"billStatus": true,
"errors": [],
"id": "2395327167211785216",
"number": "Cus-20260917-0001"
}
]
},
"errorCode": "0",
"status": true
}
```
- 成功判定:`errorCode=0` 且 `failCount=0` 且 `result[0].billStatus=true`;取 `number`(金蝶客户编码)回写 `kingdee_number`,取不到 `number` 时兜底取 `id`
- `status` 固定传 `B`(已提交),金蝶校验工商信息通过后由机器人自动审核为已审核
---
## 4. 字段映射与转换
### 4.1 A902-1 回填映射(金蝶 → TMS 表单,工商信息锁定)
| TMS 表单字段 | 金蝶字段(BfiCustomerVO) | 转换/回填规则 |
| --- | --- | --- |
| unifiedCreditCode | societycreditcode | 即查询入参,不覆盖 |
| fullName(客商名称) | name | 直接回填,锁定 |
| legalPerson(企业法人) | artificialperson | 直接回填,锁定 |
| registeredRegionName / registeredDetailAddress | bizpartner_address | 文本回填至详细地址;省市区无法结构化解析时不回填,锁定已回填项 |
| businessScope(经营范围) | businessscope | 直接回填,锁定 |
| registeredCapital(注册资金,万元) | regcapital | 正则提取首个数值按「万元」处理(如 `1000万元人民币`→`1000`);无法解析不回填 |
| businessTermType / businessEndDate | businessterm | 含「长期/无固定」→ `长期`;提取文本中最后一个 `yyyy-MM-dd` 日期为截止日 → `固定期限`;均无法解析不回填 |
| shortName(客商简称) | simplename | 仅当表单为空时回填,**不锁定**(业务信息,TMS 可改) |
| —(回填来源标记) | status | `status=C` 时置 `kingdeeStatus=exist`,随表单提交落库;金蝶客户编码与 TMS 客商编号一致,无需单独存储 |
**锁定字段集合**(`kingdeeStatus=exist` 时表单置灰):客商名称、企业法人、注册资金;统一社会信用代码仅编辑态锁定,新增态允许重新输入——修改代码后重置回填数据(`kingdeeStatus` 还原为 `none`,清空名称/法人/注册资金/详细地址/经营范围/营业期限),并对新代码重新触发 A902-1 查询。经营范围、营业期限、所在地区、详细地址仅回填、不锁定(TMS 可改)。客商分类、客商性质、所属组织、开票税点、信用额度、负责人、联系电话、客商材料等业务信息正常编辑。
TMS 无对应字段的金蝶返回项(注册资本原文、成立日期 establishdate、纳税人识别号等)不回填、不入库。
### 4.2 A902-2 推送转换表(TMS → 金蝶)
| 金蝶字段 | 来源 | 转换规则 |
| --- | --- | --- |
| status | 固定 | `B` 已提交(常量 `BfiConstant.BILL_STATUS_SUBMITTED`) |
| name | fullName | 直传 |
| societycreditcode | unifiedCreditCode | 直传(必填,候选键) |
| tx_register_no | unifiedCreditCode | 与统一社会信用代码一致时推送 |
| artificialperson | legalPerson | 直传 |
| type(伙伴类型) | customerNature(客商性质) | 客商性质字典(`nature_of_client`)dictKey 即金蝶伙伴类型编码,直接推送;dictKey 为空或非有效编码(1~17)时抛 `ServiceException`(推送置 failed,不兜底) |
| bizpartner_address | registeredAddress | `prepare()` 已拼接的完整地址直传 |
| businessscope | businessScope | 多选编码 → 选项 label 文本、以「、」拼接 |
| businessterm | businessTermType + businessEndDate | `长期` → `长期`;`固定期限` → `至 yyyy-MM-dd`(TMS 未采集起始日) |
| bizpartner_phone | contactPhone | 直传 |
| fcusbeizhu | remark | 直传,空串传 `""` |
| bizfunction | 固定 | `1,2,3,4` |
| awrp_datasourcesys | 配置 | `BfiProperties`(公共键 `thirdParty.bfi.srcSys`),默认 `WLTMS`(运输TMS) |
| awrp_cusfortrade | 固定 | `A` 是(`BfiConstant.CUS_FOR_TRADE_YES`) |
| entry_groupstandard | customerKind | `standardid_number` 固定 JBFLBZ;`groupid_number` 按客商类型:内部组织(internal)→ 1101,外部客商(external,含空)→ 1102 |
| createorg_number | 不传 | 由星瀚按默认创建组织处理 |
| number | customerCode | 传 TMS 客商编号,**金蝶客户编码与 TMS 客商编号保持一致**,无需本地冗余存储 |
| entry_bank | receiptAccounts 收款账户明细 | 每条账户一行分录:`bankaccount`=银行账号、`accountname`=开户行、`currency_number`=CNY(`BfiConstant.CURRENCY_CNY`)、`isdefault_bank`=是否默认;未填收款账户时不传该分录 |
| entry_linkman | principal + contactPhone | `contactperson`=客商负责人、`phone`=联系电话、`isdefault_linkman`=true;未填负责人时不传该分录 |
| 注册资金 regcapital | — | **A902-2 无此字段**,新客商推送不含;仅 A902-1 回填使用 |
### 4.3 伙伴类型取值(客商性质字典直取)
`nature_of_client` 字典已重构:**dictKey 即金蝶伙伴类型编码**,录入时按《客商填报审核编码规范》(`doc/docs/客商填报审核编码规范.xlsx`)选择性质(看我区/非我区、机构类别)。前端字典下拉按项目约定存 dictValue 文本,推送时 `CustomerBfiConverter.resolvePartnerType(customerNature)` 将其解析为 dictKey 作为 `type`:
| dictKey | dictValue(客商性质) | 金蝶伙伴类型 |
| --- | --- | --- |
| 8 | 我区国企/国企控股 | 8 我区国有企业 |
| 7 | 非我区国企/国企控股 | 7 非我区国有企业 |
| 6 | 民企/民企控股 | 6 民营企业 |
| 12 | 三资企业 | 12 其他 |
| 17 | 集体企业 | 17 集体经济组织 |
| 5 | 个体 | 5 个体户 |
取值规则(`CustomerBfiConverter.resolvePartnerType(customerNature)`):前端字典下拉存 **dictValue 文本**(项目约定),后端解析时——值为数字编码(dictKey,1~17)直接使用;为 dictValue 文本(如「我区国企/国企控股」,含存量数据)时按 `nature_of_client` 字典反查对应 dictKey;均无法解析时抛 `ServiceException`(提示「客商性质未配置有效的金蝶伙伴类型编码」),`syncKingdee` 推送环节捕获后置 `failed` 并记录失败原因,等待修正字典数据后重推。
固定值集中放在 `blade-transport` 常量类 `org.springblade.transport.constant.CustomerBfiConstants`(**不走 Nacos 配置**),伙伴类型编码全集见 `BfiCustomerSaveDTO.type` 注释(1 法人企业 / 2 非法人企业 / 3 非企业单位 / 4 个人 / 5 个体户 / 6 民营企业 / 7 非我区国有企业 / 8 我区国有企业 / 9 政府事业单位等 / 10 农民专业合作社 / 11 民政部门 / 12 其他 / 13 境外客商 / 14 工会 / 15 司法行政 / 16 虚拟客商 / 17 集体经济组织)。
---
## 5. 改动清单
### 5.1 后端(tms-api,blade-transport)
| 文件 | 动作 | 说明 |
| --- | --- | --- |
| `controller/BfiCustomerController.java` | 新增 | 仿 `BfiExchangeRateController`:`GET /bfi/customer/query-by-credit-code`,`pageNo=1/pageSize=1/societycreditcode=[code]` 调 `bfiService.queryCustomer`,返回 `R<BfiCustomerVO>` |
| `constant/CustomerBfiConstants.java` | 新增 | 见 4.3 |
| `service/impl/CustomerBfiConverter.java` | 新增 | ① `buildSaveDTO(CustomerArchiveVO)`:按 4.2 组装 `BfiCustomerSaveDTO`;② `resolvePartnerType(customerNature)`;③ `buildBusinessterm(...)`、`parseRegcapital(...)` 等解析工具 |
| `pojo/entity/CustomerArchive.java`(blade-transport-api) | 修改 | 新增 `kingdeeNumber`、`kingdeeStatus`、`kingdeeFailReason`、`kingdeeSyncTime` 字段 |
| `pojo/vo/CustomerArchiveVO.java`、`excel/CustomerArchiveExcel.java` | 修改 | 同步新增字段(Excel 增加「金蝶客户编码」「金蝶同步状态」列) |
| `service/impl/CustomerArchiveServiceImpl.java` | 修改 | ① 新增 `syncKingdee(Long id)`:2.4 时序 1-5 步;② `approve()` 落库后**由 Controller 调用** `syncKingdee`(推送异常内部消化不影响审批);③ 复用 `addChangeRecord` 记录「推送金蝶成功/失败」 |
| `controller/CustomerArchiveController.java` | 修改 | ① `approve` 端点成功后调用 `syncKingdee`;② 新增 `POST /customer-archive/sync-kingdee`(`@PreAuth` 权限码 `customer_archive_sync`) |
| `wrapper/CustomerArchiveWrapper.java` | 无改动 | BeanUtil.copyProperties 自动携带新字段 |
依赖说明:`blade-transport` 已依赖 `blade-bfi-api`(`BfiExchangeRateController`、`PaymentApplicationServiceImpl` 在用),无需新增 pom 依赖。
### 5.2 数据库(7 种数据库脚本同步,`doc/sql/`)
```sql
ALTER TABLE blade_customer_archive ADD COLUMN kingdee_status varchar(20) NOT NULL DEFAULT 'none' COMMENT '金蝶协同状态:none未推送/exist金蝶已有/synced已推送/failed推送失败';
ALTER TABLE blade_customer_archive ADD COLUMN kingdee_fail_reason varchar(500) DEFAULT NULL COMMENT '金蝶推送失败原因';
ALTER TABLE blade_customer_archive ADD COLUMN kingdee_sync_time datetime DEFAULT NULL COMMENT '金蝶推送时间';
CREATE INDEX idx_kingdee_status ON blade_customer_archive (kingdee_status);
```
> 库存量数据无需刷数:存量客商默认 `none`,仅在再次触发推送场景(重推按钮)才会与金蝶交互。
### 5.3 菜单按钮 SQL(权限码)
```sql
-- parent_id 取「客商档案」菜单实际 id
INSERT INTO blade_menu (id, parent_id, code, name, alias, path, source, sort, category, action, is_open, component, remark, is_deleted)
VALUES (2075431800000000001, {客商档案菜单id}, 'customer_archive_sync', '同步金蝶', 'customer_archive_sync', '', '', 1, 2, 0, 1, '', '', 0);
```
### 5.4 前端(tms-erp-web)
| 文件 | 动作 | 说明 |
| --- | --- | --- |
| `src/api/vehicle/customer-archive.js` | 修改 | 新增 `queryKingdeeCustomer(creditCode)`(GET `/blade-transport/bfi/customer/query-by-credit-code`)、`syncKingdee(id)`(POST `/blade-transport/customer-archive/sync-kingdee`) |
| `src/views/vehicle/customer-archive.vue` | 修改 | ① 新增态 watch `unifiedCreditCode`:满 18 位且通过现有格式校验(18 位字符 + 校验码)后防抖 500ms 自动查询;② 三分支处理(2.3);③ `kingdeeStatus==='exist'` 时锁定 4.1 锁定字段集合(disabled);④ `normalizeArchive()` 携带 `kingdeeStatus/kingdeeNumber`;⑤ 列表新增「同步金蝶」按钮(仅 `approved + failed` 可点)与「金蝶客户编码」「金蝶同步状态」列;⑥ 编辑态不触发查询;⑦ 审批通过推送由后端 approve 端点内部触发,前端无需额外调用 |
---
## 6. 异常处理
| 场景 | 处理 |
| --- | --- |
| 前端 A902-1 查询超时/异常 | `$message.warning('金蝶查询失败,可继续填写,审批通过后系统将自动复查')`,不阻断填写;后端推送前复查兜底 |
| A902-1 查到金蝶暂存/已提交 | 弹窗提示「金蝶审核中,请稍后重新添加」,前端阻断保存(流程终止,对齐流程图) |
| 推送时复查到金蝶已审核 | 置 `exist` + `kingdeeNumber`,跳过推送,返回「金蝶已有该客商档案,无需推送」 |
| 推送时复查到金蝶暂存/已提交 | 置 `failed`,`kingdee_fail_reason='金蝶侧该客商审核中,请稍后重推'` |
| A902-2 返回 `billStatus=false` / `failCount>0` | 置 `failed`,`errors` 拼接写入 `kingdee_fail_reason`(截断 500 字),日志 `@Slf4j` 占位符输出含客商 id 与信用代码 |
| A902-2 网关异常(超时/5xx/token 失效) | `BfiException` 统一上抛,同上置 `failed`;token 由 `BfiServiceImpl` 自动续期 |
| 重推仍失败 | 保持 `failed`;定时任务(`CustomerKingdeeSyncTask`,开关 `thirdParty.bfi.customer.syncEnabled`,默认关闭)每轮自动重推,也可列表「同步金蝶」人工重推;连续失败转人工核对金蝶侧该信用代码客商状态 |
---
## 7. 实施核对清单
1. [ ] 与财务核对 4.3 客商性质字典 dictKey 与金蝶伙伴类型编码对应关系(录入侧按《客商填报审核编码规范》选择性质),以及 `createorg_number` 取值。
2. [ ] DDL 按 `doc/sql/` 各数据库方言同步(MySQL / PostgreSQL / Oracle / SQL Server / 达梦 / 人大金仓 / 崖山),排序规则 `utf8mb4_general_ci`。
3. [ ] 前端锁定字段集合与 4.1 一致;`exist` 状态下后端不额外强校验锁定字段是否被改(前端已置灰,以提交值为准,工商信息实际以金蝶为准)。
4. [ ] 「同步金蝶」按钮权限码 `customer_archive_sync` 与菜单 SQL 一致。
5. [ ] A902-1/A902-2 联调环境验证:三分支提示文案、回填解析(regcapital/businessterm)、A902-2 成功回写编码。
6. [ ] 回归:现有新增/编辑/审批/导入导出不受影响(`kingdeeStatus` 默认 `none` 不参与既有校验)。
---
## 8. 实施记录
- 2026-09-17:按本文档完成后端(`CustomerArchive` 实体/Excel/Mapper 扩展、`CustomerBfiConstants`、`CustomerBfiConverter`、`CustomerArchiveServiceImpl.syncKingdee`、`BfiCustomerController`、`CustomerArchiveController` approve 挂推送 + `/sync-kingdee`)与前端(`customer-archive.js` 两个 API、`customer-archive.vue` 查询回填/锁定/阻断、列表「同步金蝶」按钮与「金蝶同步状态」列);`mvn clean compile -DskipTests` 与 `pnpm run build` 均通过。
- 2026-09-17(第二轮调整):
- **取消 `kingdee_number` 字段**:A902-2 `number` 直接传 TMS 客商编号,金蝶客户编码与客商编号保持一致;DDL 缩减为 3 字段,前端移除「金蝶客户编码」列及相关引用。
- **客商数据定时同步子流程本期不做**:经确认 ERP/TMS 无此既有逻辑,且金蝶 SDK(blade-bfi-api)暂未提供 A901-1 供应商查询;该子流程不阻塞核心链路,待 A901-1 能力补齐后按 §2.3 步骤9(ERP 定时调 A902-1/A901-1 更新客商数据与使用状态)另行立项。推送失败(`failed`)由列表「同步金蝶」按钮人工重推。
- 2026-09-17(第四轮调整):经确认放宽锁定范围,经营范围、营业期限、所在地区(省市区)、详细地址在 `exist` 状态下仅回填不置灰,可正常编辑;锁定字段集合缩减为客商名称、企业法人、注册资金。统一社会信用代码在新增态支持重新输入:修改代码即重置回填数据并重新触发 A902-1 查询,编辑态仍锁定。
- 2026-09-17(第五轮调整):A902-2 伙伴类型转换改为依据 `doc/docs/客商填报审核编码规范.xlsx`:以统一社会信用代码登记管理部门前缀为主(身份证/手机号→个人,1→政府事业单位等、3→司法行政、5→民政部门、8→工会、N→集体经济组织、Y→其他、92→个体户、93→农民专业合作社、91→企业按客商性质细分),原客商性质映射降级为前缀未识别/代码缺失时的兜底。`CustomerBfiConstants` 新增伙伴类型编码常量与前缀映射表,`CustomerBfiConverter.resolvePartnerType` 增加 `unifiedCreditCode` 入参。`mvn clean compile` 通过。
- 2026-09-17(第六轮调整):按数据库字典 `nature_of_client` 实际值(国企/国企控股、民企/民企控股、三资企业、集体企业、个体)重建 `NATURE_TYPE_MAPPING`;91 开头企业取消「含国有」文本特判,统一走性质映射(国企/国企控股→8)。三资企业→其他(12)、集体企业→集体经济组织(17) 为近似映射,待财务核对。`mvn clean compile` 通过。
- 2026-09-17(第七轮调整):BFI 推送配置抽取统一为 `org.springblade.transport.config.BfiProperties`(`@ConfigurationProperties(prefix = "thirdparty.bfi")`,prefix 须全小写,宽松绑定匹配 Nacos 中 camelCase 键):数据来源系统编码收口为公共键 `thirdParty.bfi.srcSys`(原 `thirdParty.bfi.customer.srcSys` / `thirdParty.bfi.payApply.srcSys` 废弃,环境变量改用 `BFI_SRC_SYS`),客商 `customer.createOrg`、付款申请 `payApply.billtype/exratetable/srcType` 集中管理;`CustomerBfiConverter`、`PaymentApplicationServiceImpl`、`KingdeeApBillSyncSupport` 改为注入 `BfiProperties`。Nacos 配置(blade-dev/blade-prod/third-party-api.yaml)同步:bfi 节点新增 `srcSys`、`customer.createOrg`,payApply 节点移除 `srcSys`。`mvn clean compile` 通过。
- 2026-09-17(第八轮调整):`nature_of_client` 字典重构为 dictKey 即金蝶伙伴类型编码(8 我区国企/国企控股、7 非我区国企/国企控股、6 民企/民企控股、12 三资企业、17 集体企业、5 个体),录入时按编码规范选择性质,推送侧 `resolvePartnerType` 直接取 dictKey 推送(1~17 校验,非法/为空/存量文本值兜底 `6`);删除信用代码前缀转换与文本映射表及 `CustomerBfiConstants` 中相关常量。`mvn clean compile` 通过。
- 2026-09-17(第九轮调整):伙伴类型取消兜底,`resolvePartnerType` 在客商性质 dictKey 为空或非有效编码(1~17)时抛 `ServiceException`,由 `syncKingdee` 推送环节捕获置 `failed` 并记录原因,修正数据后可重推;删除 `DEFAULT_PARTNER_TYPE` 常量。`mvn clean compile` 通过。
- 2026-09-20(第十轮调整):移除 `customer.createOrg` 配置项,`createorg_number` 固定传 `CustomerBfiConstants.CUSTOMER_CREATE_ORG`(WLTMS),`BfiProperties` 删除 customer 节点,仓库与 Nacos(blade-dev)配置同步移除。前端修复:统一社会信用代码查询去除同码去重守卫(原 `kingdeeQueriedCode` 拦截导致改回历史代码不再触发 A902-1 查询),代码一经变化即防抖重新查询。`mvn clean compile` 与 `pnpm run build` 通过。
- 2026-09-20(第十一轮调整):定时同步 `syncKingdeeCustomerData` 增加 ③ 已推送(synced)客商的金蝶审核确认——A902-2 成功仅代表金蝶受理(status=B),按信用代码 A902-1 批量复查,status=C 已审核才记为最终成功,A/B 审核中保持 synced 等待下轮复查(不重推,避免金蝶侧重复建档),汇总文案与日志体现「金蝶已审核/审核中/未查询到」统计;`CustomerArchiveServiceImpl` 常量补充 Javadoc,新增 `@SpringBootTest` 手动触发测试类 `CustomerArchiveSyncKingdeeDataTest`(pom 补 `spring-boot-starter-test`),Controller 不加测试端点。`mvn clean compile` 与 `test-compile` 通过。
- 待人工执行:`doc/sql/transport/blade_customer_archive_kingdee_20260917.sql`(含「同步金蝶」按钮菜单 INSERT);与财务核对客商性质映射键与 `createorg_number` 取值。
- 2026-09-17(第三轮新增定时任务):新增 `task/CustomerKingdeeSyncTask`(仿 `PaymentKingdeeResultTask`,开关 `thirdParty.bfi.customer.syncEnabled` 默认关闭、cron `thirdParty.bfi.customer.cron` 默认每天 02:00)与 `ICustomerArchiveService.syncKingdeeCustomerData()`:① 待推送客商(approved 且 none/failed)复用 `syncKingdee` 自动推送(自代理逐条独立事务,单条失败不中断);② 已同步客商(synced/exist)按信用代码分批(50/批)A902-1 查询,金蝶禁用(enable=0)联动停用客商档案并记变更,金蝶恢复可用不自动启用。A901-1 供应商查询仍待 SDK 提供能力后扩展。`mvn clean compile` 通过。
@@ -0,0 +1,145 @@
# 付款管理金蝶同步全链路逻辑梳理(推送链路与回写链路连贯性)
> 关联文档:
> - `2026-09-16-付款管理正式付款同步金蝶(财务应付单+付款申请下推)技术文档.md`
> - `2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md`
> - `../TMS业财融合-集成流程图文字版与业务流程梳理.md`(§4.3 流程步骤、§4.4 关键规则)
>
> 本文为 2026-09-18 幂等恢复改造后的全链路现状梳理,代码落点 `PaymentApplicationServiceImpl`。
## 1. 背景与范围
付款申请审批通过后与金蝶(星瀚)的双向同步:
- **推送链路(TMS → 金蝶)**:正式结算付款两步链路 A102-2 财务应付单保存 → A103-1 应付下推付款申请;预付款一步链路 A103-3 预付款保存。
- **回写链路(金蝶 → TMS)**:定时任务经 A103-2 付款申请单查询回写单据与付款状态,经 A103-4 付款处理查询回填付款记录。
原实现两步推送同处一个本地事务,A102-2 成功后 A103-1 失败会导致"金蝶已生成、本地回滚为生成失败"的断链,重试被"请勿重复推送"拦截而死锁。本次改造确立全链路恢复原则:
> **以金蝶侧实际生成为准:已生成则幂等跳过/回填勾稽,未生成才续推;推送接口真正报错才算同步失败。**
## 2. 接口与角色清单
| 接口 | 名称 | 方向 | 方法(TMS 侧) | 用途 |
| --- | --- | --- | --- | --- |
| A102-2 | 财务应付单保存 | 推送 | `pushBfiApBill` | 正式结算付款第一步 |
| A102-4 | 财务应付单查询 | 查询 | `queryApBillExistNos` | 推送前幂等预检(防重键) |
| A103-1 | 付款申请保存(应付下推) | 推送 | `pushBfiPayApplyPush` | 正式结算付款第二步 |
| A103-2 | 付款申请单查询 | 查询 | `queryPayApplyPages` / `queryLatestPayApply` | 恢复勾稽 + 定时回写 |
| A103-3 | 付款申请保存(预付款) | 推送 | `pushBfiPayApply` | 预付款一步链路 |
| A103-4 | 付款处理查询 | 查询 | `queryPayBillPages` | 回填付款记录 |
**防重键体系**(`awrp_src_sys = WLTMS`,常量统一在 `KingdeeBfiConstant`):
| 键 | 取值 | 使用接口 |
| --- | --- | --- |
| 付款申请单来源单号 | `awrp_src_code = paymentNo` | A103-1 / A103-2 / A103-3 |
| 财务应付单来源单号 | `awrp_src_code = paymentNo_结算明细单据号拼接`(`ExternalSrcCodeSupport.buildSrcCode`) | A102-2 / A102-4 |
| 分录标识 | `bfiEntryCode = paymentNo-分录序号(1 起)` | A102-2 `awrp_entry_code`、A103-1 `awrp_finapbill_entryid` / `awrp_entry_code`、A103-3(序号固定 1) |
公共约定:各接口日期文本统一 `yyyy-MM-dd HH:mm:ss`(`bfiDateTimeText`,申请日期为空取当天);分页查询统一 `BfiUtil.queryAllPages` 自动翻页(页大小 200、最大 50 页保护);批量入参按 50 单/批(`batchForeach`);A103-2 结果按来源单号取最近修改一条(`latestPayApplyBySrcCode`)。
## 3. 状态机
本地 `kingdeeStatus` 流转与 A103-2 状态映射(`kingdeeResultStatus`):
```
unsynced ──推送成功──> synced ──paystatus=Inpayment──> paying ──paystatus=Alreadypay──> paid(终态)
│ │ │
│ └──billstatus=D──> closed(终态) └──billstatus=D──> closed(终态)
└──推送异常(事务回滚 + 独立事务落状态)──> failed ──重推成功或回写勾稽纠偏──> synced/paying/paid
```
| A103-2 返回 | 本地状态 | 是否终态 |
| --- | --- | --- |
| paystatus=Alreadypay | paid | 是(不再轮询) |
| billstatus=D(已关闭) | closed | 是(不再轮询) |
| paystatus=Inpayment | paying | 否(继续轮询) |
| 其余(A暂存/B已提交/C已审核 + Unpaid) | synced | 否(继续轮询) |
| 无返回(金蝶未生成) | 状态不变 | — |
门禁:仅 `approvalStatus = approved` 允许同步;已 synced/paying/paid/closed 在 `doSyncKingdee` 入口幂等跳过并返回既有单据 id。
## 4. 推送链路完整逻辑(TMS → 金蝶)
### 4.1 入口与事务边界
- 单条:`POST /sync-kingdee` → `syncKingdee(id)`;批量:`POST /sync-kingdee-batch` → `syncKingdeeBatch(ids)`。
- 经自代理逐单调用 `doSyncKingdee`:**单据级事务**,单条失败只回滚自身,批量不中断。
- 失败时在独立事务 `markKingdeeSyncFailed` 落 `failed`(仅 approved 单据生效)。
- 批量同步共享 `KingdeeSyncContext`:档案缓存(合同/客商/项目/货物类型/结算明细)+ 应付单防重键缓存,批量 A102-4 预热一次(`preloadApBillExistNos`,仅正式结算单据),消除逐单远程查询 N+1。
### 4.2 正式结算付款链路(settlement_payment)
| 步骤 | 动作 | 说明 |
| --- | --- | --- |
| 1 | `requirePayableSettlement` | 结算类型路由:应付放行,应收(A302-2)待业务确认暂拦截 |
| 2 | `resolveBfiEntryGroups` | 结算明细按货物编码+计量单位聚合分录组(金额取结算金额含税/不含税) |
| 3 | A102-4 幂等预检 | 按防重键查应付单是否已生成(上下文缓存优先,懒加载兜底) |
| 3a | **已生成 → 幂等跳过保存**,返回已有单据编号并回填 `kingdeeBillNo` | 说明上次同步已在后续环节中断,应续推而非拦截报错 |
| 3b | 未生成 → A102-2 保存 | 仅校验保存成功,应付单 id 不落库;单据编号回填 `kingdeeBillNo` |
| 4 | **已存在时先 A103-2 恢复勾稽**(`reconcileExistPayApply`) | 付款申请单已生成 → 回填 `kingdeeBillId/kingdeeBillNo`,直接视为同步成功,跳过下推 |
| 5 | 勾稽未命中 → A103-1 应付下推 | 分录经 `bfiEntryCode` 与 A102-2 明细一一关联;返回星瀚单据 id |
| 6 | 落库 | `kingdeeBillId = 付款申请单 id`,`kingdeeStatus = synced` |
### 4.3 预付款链路(project_advance / progress_advance)
| 步骤 | 动作 | 说明 |
| --- | --- | --- |
| 1 | 同步前状态为 `failed` 时先 A103-2 恢复勾稽 | 金蝶已生成 → 回填并视为成功,避免重推产生重复单据;首推(状态非 failed)不多一次远程查询 |
| 2 | 勾稽未命中 → A103-3 保存 | 不绑应付单;返回星瀚单据 id |
| 3 | 落库 | 同 4.2 第 6 步 |
## 5. 回写链路完整逻辑(金蝶 → TMS)
触发:`PaymentKingdeeResultTask` 定时任务(默认每 10 分钟,`thirdParty.bfi.payResult.syncEnabled/cron`),也可手动 `POST /sync-kingdee-result`;逻辑收敛在 `syncKingdeePaymentResult()`。
### 5.1 ① A103-2 单据与付款状态回写
- 轮询范围:`approved` 且 `kingdeeStatus IN (synced, paying, failed)`;paid/closed 终态不再轮询。
- **failed 纳入勾稽**:金蝶已生成说明上次推送实际成功(如下推请求超时但远端已落单、本地事务回滚),查到即经 `applyPayApplyResult` 回填 `kingdeeBillNo/billno`、`kingdeeBillId/id`、状态、已付金额(分录 `e_paidamt` 汇总),自动纠偏,防止人工重推重复;未查到保持 failed 等待异常处理后重新推送。**任务只勾稽回写,不主动推送**。
- 付款完成(paid)联动:清理 MOCK 付款记录、履约付款数据(预结算预付记录、正式结算付款记录)。
### 5.2 ② A103-4 付款记录回填
- 候选:付款中/已付款单据(本轮 A103-2 查到 `paystatus ≠ Unpaid`)+ 已付款但无真实付款记录的单据(补偿轮询)。
- 按付款人(公司)编码分组查询(编码优先取 A103-2 `payorg_number`,兜底合同组织;业务日期窗口最近 1 个月,接口限制)。
- 勾稽:付款处理单源单 id(= `kingdeeBillId`)优先,源单编号(= `kingdeeBillNo`)兜底(`matchPayBill`)。
- 回填(`applyPayBillRecord`):按付款处理单号 upsert 付款记录(金额、日期、凭证留档 JSON)。
## 6. 两边逻辑连贯性对照
### 6.1 正常时序(正式结算付款)
```
TMS 审批通过 → A102-2 保存应付单(回填 kingdeeBillNo)→ A103-1 下推付款申请(落 kingdeeBillId,置 synced)
→ 任务 A103-2 回写(billno 覆盖 kingdeeBillNo,状态 synced→paying→paid)→ 任务 A103-4 回填付款记录 → 终态
```
### 6.2 异常场景矩阵(两边行为与闭环路径)
| # | 断点场景 | 金蝶侧实际 | 本地状态 | 恢复路径 | 闭环 |
| --- | --- | --- | --- | --- | --- |
| 1 | 推送前校验/档案缺失失败(A102-2 之前) | 无任何单据 | failed | 重新推送走全链路首推 | ✅ |
| 2 | A102-2 成功、A103-1 报错(如分录校验失败) | 应付单已生成 | failed(事务回滚,编号回填一并回滚) | 重推:A102-4 预检命中 → 幂等跳过保存 → A103-2 勾稽未命中 → A103-1 续推;下推再报错才真实失败 | ✅ 本次改造 |
| 3 | A103-1 远端已落单但响应超时/本地回滚 | 应付单 + 付款申请单均已生成 | failed | 重推:A102-4 命中跳过 → A103-2 勾稽命中 → 回填置 synced;或定时任务自动纠偏 | ✅ 本次改造 |
| 4 | 预付款 A103-3 远端成功但本地失败 | 付款申请单已生成 | failed | 重推前 A103-2 预检命中 → 回填跳过;或定时任务纠偏 | ✅ 本次改造 |
| 5 | 推送成功但 kingdeeBillNo 尚为应付单编号 | 付款申请单已生成 | synced | 定时任务 A103-2 回写覆盖为付款申请单编号(字段语义随阶段演进,设计如此) | ✅ |
| 6 | 手工重推与定时任务并发纠偏 | 单据唯一(防重键唯一) | — | 两侧均为"先查后动":勾稽只读远端按 paymentNo 唯一归并,重复执行幂等无冲突 | ✅ |
| 7 | 金蝶反审核删除后重推 | 无单据 | failed | 预检查无 → 重新推送生成新单据(符合"金蝶反审核+删除后 ERP 重推"规则) | ✅ |
| 8 | failed 且金蝶确无单据(如档案长期缺失) | 无单据 | failed | 每轮任务空查一次 A103-2(量级 = failed 单据数),状态保持,等待人工处理 | ⚠️ 已知边界 |
### 6.3 连贯性结论
1. **推送侧自洽**:三条件分支(应付单已存在 / 付款申请单已存在 / 均不存在)覆盖全部断点组合,任一断点重推均从断点续传而非从头重推,且以远端实际为准。
2. **回写侧衔接**:恢复成功后本地状态为 synced(或直接 paid/paying),落入任务轮询范围,付款状态与付款记录由任务持续驱动至终态,无需再次人工干预。
3. **双通道兜底**:手工重推(勾稽预检)与定时任务(勾稽纠偏)对同一断点均可闭环,先后到达不冲突。
4. **语义一致性**:防重键、分录标识、日期格式、A103-2 归并规则均已收敛为统一生成点(`apBillSrcCode`/`bfiEntryCode`/`bfiDateTimeText`/`latestPayApplyBySrcCode`/`batchForeach`),两侧关联字段不存在漂移可能。
## 7. 已知边界与注意事项
1. **failed 空查**:确无金蝶单据的 failed 单据会被任务持续空查(每轮 A103-2 一次),如量大可考虑按申请日期窗口截断或引入重试次数上限(当前不加,保持简单)。
2. **`kingdeeBillNo` 阶段语义**:推送瞬间为应付单编号,勾稽/回写后为付款申请单编号;依赖该字段展示时以回写后的值为准。
3. **应收结算(A302-2)未接入**:结算类型为应收的单据在 `requirePayableSettlement` 拦截,推送规则待业务确认。
4. **A102-2/A103-1 联调临时取值**:物料/计量单位/项目编码仍传金蝶测试档案值(调试块),档案对齐后需拆除,见 2026-09-16 技术文档遗留项。
5. **A103-2 勾稽不校验金额**:恢复勾稽以"来源单号存在"为准,不比对金额与分录;若远端单据被人工修改,以远端为准回写(与回写任务原则一致)。
@@ -0,0 +1,379 @@
# 应付单与付款申请同步金蝶全链路业务流程(正式结算单同步 → 付款管理批量同步 → 付款结果定时回写)
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)
> 对接系统:金蝶云·星瀚(业财融合 BFI,经 iPaaS 网关)
> 覆盖接口:A102-2 财务应付单保存、A102-4 财务应付单查询、A103-1 付款申请保存(应付下推)、A103-2 付款申请单查询
> 关联文档:`2026-09-18-付款管理金蝶同步全链路逻辑梳理(推送链路与回写链路连贯性).md`、`2026-09-16-付款管理正式付款同步金蝶(财务应付单+付款申请下推)技术文档.md`、`2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md`
> 最后更新:2026-09-20(按当前代码重新梳理:A102-2 已由正式结算单页面触发推送;接口入参来源及对应前端字段直接内嵌在各环节业务流程步骤中)
---
## 1. 整体业务链路
一笔正式结算付款要完成金蝶同步,整体分**三个环节**,由三个页面/功能接力完成:
```
环节一:正式结算单「同步金蝶」(人工,正式付款单页面)
审批通过的应付类正式结算单 → 推送金蝶财务应付单(A102-2,前置 A102-4 幂等预检)
→ 回写结算单:金蝶单据编号 + 同步状态 synced
环节二:付款管理「批量同步」(人工,付款管理页面)
审批通过的付款申请(结算付款)→ 前置校验应付单已生成(A102-4)→ A103-2 幂等预检
→ 推送金蝶付款申请单(A103-1 应付下推,分录勾稽应付单付款计划分录)
→ 回写付款申请:金蝶单据id/编号 + 同步状态 synced
(预付款 project_advance / progress_advance 不走本链路,直接 A103-3 保存,见关联文档)
环节三:PaymentKingdeeResultTask 定时回写(自动,默认每10分钟)
① A103-2 付款申请单查询:回填金蝶单据编号、单据状态、已付金额(synced → paying → paid/closed)
② A103-4 付款处理查询:回填真实付款记录(金额、付款日期、凭证留档)
```
核心设计原则:**以金蝶侧实际生成为准**。每一步推送前先查询(防重键)确认金蝶是否已有单据:已生成则幂等跳过/回填勾稽,未生成才推送;推送接口真正报错才算同步失败。任一环节中断后重新触发,均从断点续传而非从头重推。
---
## 2. 四个接口在链路中的角色
| 接口 | 名称 | 方向 | 触发环节 | 业务用途 |
| --- | --- | --- | --- | --- |
| A102-2 | 财务应付单保存 | TMS → 金蝶 | 环节一 | 一张正式结算单生成一张金蝶财务应付单 |
| A102-4 | 财务应付单查询 | TMS → 金蝶(查询) | 环节一前置 / 环节二前置 | ①推送前幂等预检应付单是否已生成;②反查应付单付款计划分录标识(awrp_entry_code_p),供 A103-1 勾稽 |
| A103-1 | 付款申请保存(应付下推) | TMS → 金蝶 | 环节二 | 基于已生成的应付单下推生成金蝶付款申请单 |
| A103-2 | 付款申请单查询 | TMS → 金蝶(查询) | 环节二前置(幂等预检/恢复勾稽)+ 环节三①(定时回写) | 查询付款申请单是否已生成、单据状态与付款进度 |
| A103-4 | 付款处理查询 | TMS → 金蝶(查询) | 环节三② | 回填实际付款记录(本文档附带说明,逻辑在定时任务内) |
(A103-3 预付款直接保存仅预付款链路使用,不在本文范围。)
---
## 3. 环节一:正式结算单「同步金蝶」(推送 A102-2)
### 3.1 业务流程
1. 操作入口:正式付款单(正式结算单)页面「同步金蝶」按钮,单笔操作。
2. 前置状态校验(不满足直接报错,不调用金蝶):
- 审批状态必须是 **审批通过(approved)**;草稿/审批中/已驳回/已作废均不允许。
- 结算类型必须是 **应付(payable)**;应收(receivable)对应财务应收单(A302-2),推送规则未确认,当前拦截报错。
- 同步状态已是 **已同步(synced)** 时幂等返回,不重复推送。
- 正式结算单必须存在结算明细(分录取数来源)。
3. 组装财务应付单防重键(外部系统来源单据号):**正式结算单号_结算明细单据号1_单据号2…**(单据号去重、保持行序、下划线拼接)。一张结算单对应一张金蝶应付单,同一结算单下多笔付款申请共享该应付单。
4. 解析应付单明细分录:逐条**应付货物费用行**生成一条分录(不聚合),取数校验(任一不满足报错拦截):
- 结算明细对应应付明细必须存在货物费用行(取数必须来自应付费用明细,无兜底);
- 货物类型必须在货物类型档案中维护货物编码(多种货物类型以「、/,」分隔时不拆行,取第一种类型);
- 运费计算单位不能为空(优先快照运费计算单位,兜底数量单位;计量单位档案暂无编码字段,暂以单位名称作编码)。
5. **A102-4 财务应付单查询**:按防重键查询金蝶是否已生成财务应付单。
- **已生成** → 幂等跳过保存,直接取已有单据编号进入第 7 步(说明上次同步已保存成功、在后续环节中断);
- **未生成** → 进入第 6 步调用 A102-2 保存。
A102-4 入参来源:
| 接口字段 | 含义 | 来源 | 前端字段(来源页面) |
| --- | --- | --- | --- |
| awrp_src_code | 外部系统来源单据号(支持多值) | 第 3 步组装的财务应付单防重键:正式结算单号_结算明细单据号拼接 | 系统组装:正式结算单页面「结算单号」formalSettlementNo + 结算明细「单据号」documentNo(`formal-settlement.vue` / `formalSettlementTable.js`) |
| awrp_src_sys | 外部系统编号 | 配置项 thirdParty.bfi.srcSys,默认 WLTMS | 无(后端配置,前端不出现) |
| pageNo / pageSize | 分页 | 自动翻页(页大小 200,最大 50 页保护) | 无(系统生成) |
A102-4 返回取用:`billno` 单据编号(预检命中时即结算单回写的金蝶单据编号);`planentity[].awrp_entry_code_p` 付款计划分录标识(环节二 A103-1 勾稽关联用)。
6. **A102-2 财务应付单保存**。
A102-2 主表入参来源:
| 接口字段 | 含义 | 必填 | 来源 | 前端字段(来源页面) |
| --- | --- | --- | --- | --- |
| billtypeid_number | 单据类型.编码 | 是 | 常量默认名称「服务采购应付」经单据类型映射字典转为编码 `ApFin_service_BT_S` | 无(后端常量) |
| org_number | 结算组织.编码 | 是 | 合同所属组织:合同.organizationId → 部门编码(未维护报错) | 合同管理页面「所属组织」organizationId/organizationName(`contract-manage.vue`);部门编码 deptCode 在系统部门管理维护 |
| bizdate | 单据日期 | 是 | 同步当日(格式 yyyy-MM-dd HH:mm:ss) | 无(系统生成) |
| asstacttype | 往来类型 | 是 | 按合同类别路由:合同类别=「客户合同」→ bd_customer(客户),否则 bd_supplier(供应商) | 合同管理页面「合同类别」contractCategory(下拉字典) |
| asstact_number | 往来户.编码 | 是 | 合同乙方 → 客商档案按全称/简称匹配取客商编码(乙方为空或未维护客商编码均报错) | 合同管理页面「乙方」partyB(下拉选择)→ 客商档案页面「客商编号」customerCode(按客商全称/简称匹配,`customer-archive.vue`) |
| purmode | 付款方式 | 是 | 常量 CASH(现购,金蝶自动 100% 推付款) | 无(后端常量) |
| currency_number | 结算币别.货币代码 | 是 | 合同结算币种,RMB 统一映射 CNY(为空报错) | 合同管理页面「结算币种」settlementCurrency(下拉选择) |
| payorg_number | 付款组织.编码 | 是 | 同结算组织编码 | 同 org_number |
| payproperty_number | 款项性质.编码 | 是 | 常量 2001(待金蝶确认) | 无(后端常量) |
| isincludetax / ispricetotal / isintax | 录入含税价/录入总价/是否含税 | 否 | 固定 true / true / false(录入总价免单价换算) | 无(后端常量) |
| awrp_src_sys | 外部系统编号 | 是 | 配置项,默认 WLTMS(应付单防重键之一) | 无(后端配置) |
| awrp_src_code | 外部系统来源单据号 | 是 | 第 3 步组装的防重键:正式结算单号_结算明细单据号拼接 | 系统组装:正式结算单页面「结算单号」formalSettlementNo + 结算明细「单据号」documentNo |
| awrp_src_type | 外部单据类型 | 是 | 常量「TMS财务应付单」(待金蝶确认) | 无(后端常量) |
A102-2 明细分录(detailentry,每条应付货物费用行一条)入参来源:
| 接口字段 | 含义 | 必填 | 来源 | 前端字段(来源页面) |
| --- | --- | --- | --- | --- |
| material_number | 物料编码.编码 | 是 | 货物类型 → 货物类型档案货物编码;**联调期临时固定传金蝶测试档案值 SP0324** | 应收应付明细页面「货物类型」cargoType(支持「、/,」多类型取第一种)→ 基础数据-货物类型页面「货物类型编码」cargoCode(`cargo-type.vue`) |
| measureunit_number | 计量单位.编码 | 是 | 快照运费计算单位(兜底数量单位);**联调期临时固定传 TON** | 应收应付明细页面「运费计算单位」priceUnit(历史快照兜底数量单位 quantityUnit;计量单位档案暂无编码字段暂传单位名称) |
| quantity | 数量 | 是 | 该行运输数量,无吨位兜底 1 | 应收应付明细页面「运输总量」transportQuantity |
| price | 单价 | 否 | 费用明细运输单价(单据头录入总价,单价仅参考) | 应收应付明细页面「运输单价」unitPrice |
| e_pricetaxtotal | 应付金额(含税) | 是 | 该费用行调整后总金额(含税) | 应收应付明细页面「调整后金额」afterAmount(含调整金额 adjustAmount 调整) |
| e_amount | 金额(不含税) | 否 | 结算货物费用行不含税金额(应付费用行无不含税字段,经结算费用行回链;无映射兜底 0) | 正式结算明细「结算金额(不含税)」settlementAmountNoTax(`formalSettlementTable.js`) |
| project_number | 项目.项目编码 | 是 | **联调期临时固定传金蝶测试档案值 999**(真实取值:项目档案项目编码,无项目传 999) | 联调期无;真实来源=项目申请页面「项目编号」projectCode(`project-apply.js`) |
| awrp_entry_code | 外部系统来源分录标识 | 是 | 应付货物费用行 id(雪花 id 天然唯一);A103-1 的 awrp_entry_code 与之对应 | 无(系统生成,前端不出现) |
付款计划(planentity)暂不传:由金蝶按明细自动汇总生成,其付款计划分录标识(awrp_entry_code_p)后续经 A102-4 反查。
7. 返回与落库:
- 成功:返回 `result[0].number`(金蝶应付单编号)→ 写入正式结算单金蝶单据编号,同步状态 = synced,记录同步时间;`result[0].id` 为星瀚内部主键,不落库。
- 失败:金蝶单据级失败以 errors 明细抛出业务异常,本次同步失败(结算单同步状态保持原值,可重新触发)。
### 3.2 最终金蝶数据
![image-20260920114138689](F:\workspace\work\xdwl\dev\tms\tms-api\doc\docs\业务流程\2026-09-20-应付单与付款申请同步金蝶全链路业务流程(A102-2保存-A102-4查询-A103-1下推-A103-2回写).assets\image-20260920114138689.png)
---
## 4. 环节二:付款管理「批量同步」(推送 A103-1)
### 4.1 业务流程
1. 操作入口:付款管理页面勾选记录点「批量同步」(也支持单笔同步接口);权限码 `payment_application_sync`。
2. 批量语义:
- 逐单独立事务:**单条失败不中断整批**,失败单据落「生成失败(failed)」状态并返回失败原因;
- 整批共享档案缓存(合同/客商/货物类型/结算明细等),首单加载后全批复用;
- 批量开始前整批一次 A102-4 批量查询(每批最多 50 个防重键,入参来源同环节一第 5 步:防重键 + WLTMS + 自动翻页),预热应付单是否已生成的缓存,消除逐单远程查询。
3. 前置状态校验(逐单,不满足不调金蝶):
- 审批状态必须是 **审批通过(approved)**;
- 金蝶同步状态为 **synced / paying / paid / closed** 的幂等跳过,直接返回已有金蝶单据 id,不重复推送;
- 付款类型为**结算付款(settlement_payment)** 走本链路;预付款(project_advance / progress_advance)走 A103-3 直推,不在本文范围。
4. **结算类型路由 + 应付单已生成校验(A102-4)**:关联正式结算单结算类型为应收(receivable)时拦截报错(A302-2 未启用);随后按每张关联正式结算单的防重键调 A102-4 校验应付单已生成,任一未生成都报错提示「请先在正式付款单页面同步金蝶」——即环节一未完成时环节二不允许执行。
5. **A103-2 幂等预检(恢复勾稽)**:按付款申请号查询金蝶付款申请单是否已生成(防上次下推成功但本地回滚的断链场景)。已生成 → 直接回填单据 id 与编号,视为同步成功,跳过下推进第 8 步;未生成 → 继续。
A103-2 入参来源(幂等预检):
| 接口字段 | 含义 | 来源 | 前端字段(来源页面) |
| --- | --- | --- | --- |
| awrp_src_code | 外部系统来源单据号(支持多值) | 付款申请号 paymentNo(付款申请单防重键) | 付款管理列表「单据号」paymentNo(系统生成,`payment-application.vue`) |
| awrp_src_sys | 外部系统编号 | 配置项 thirdParty.bfi.srcSys,默认 WLTMS | 无(后端配置) |
| pageNo / pageSize | 分页 | 自动翻页(页大小 200,最大 50 页保护) | 无(系统生成) |
取数规则:查询结果按来源单号归并,同一单号取最近修改(modifytime 最大)的一条;查到 `id`/`billno` 即回填并视为已生成。
6. **解析付款申请分录**:与环节一应付单明细分录同源(逐条应付货物费用行一条,校验项相同)。
7. **A103-1 付款申请保存(应付下推)**:每条分录先经 A102-4 按所属结算单防重键反查金蝶生成的付款计划分录标识(awrp_entry_code_p,同一结算单只反查一次,入参来源同环节一第 5 步),用于分录勾稽关联。
A103-1 主表入参来源:
| 接口字段 | 含义 | 必填 | 来源 | 前端字段(来源页面) |
| --- | --- | --- | --- | --- |
| awrp_src_code | 外部系统来源单据号 | 是 | 付款申请号 paymentNo(付款申请单防重键) | 付款管理列表「单据号」paymentNo(系统生成) |
| awrp_src_sys | 外部系统编号 | 是 | 配置项,默认 WLTMS | 无(后端配置) |
| applydate | 申请日期 | 是 | 付款申请申请日期(空取当天,格式 yyyy-MM-dd HH:mm:ss) | 付款申请表单「申请日期」applyDate(`payment-application-form.vue`,页面禁用、自动当天) |
其余主表字段(付款组织、采购组织、请款事由、汇率表、付款渠道、申请人等)当前不传,由金蝶根据应付单自动带出。
A103-1 分录(entry,与应付单明细分录一一对应)入参来源:
| 接口字段 | 含义 | 必填 | 来源 | 前端字段(来源页面) |
| --- | --- | --- | --- | --- |
| e_paymenttype_number | 付款类型.编码 | 是 | 常量 201(正式付款) | 无(后端常量) |
| awrp_finapbill_entryid | 结算单分录id(勾稽键) | 是 | 经 A102-4 反查该分录所属正式结算单应付单的付款计划分录标识 awrp_entry_code_p | 无(系统反查,前端不出现) |
| awrp_entry_code | 对方系统唯一行id(不能重复) | 是 | 应付货物费用行 id(与环节一应付单明细 awrp_entry_code 同源同值) | 无(系统生成) |
| e_assacct | 往来账户 | 是 | 付款申请收款账户(收款方银行账号) | 付款申请表单「收款账号」:选择收款方收款账号 receiptAccountId,落库字段 bankAccount(只读展示为 form.bankAccount) |
| e_bebank_number | 往来银行.编码 | 是 | **联调期临时固定传金蝶测试档案值 001110012009**(待建立银行档案编码映射) | 联调期无;真实对应付款申请表单「开户银行」bankName(随收款账号带出、只读) |
| e_applyamount | 申请金额 | 是 | 该分录应付金额(含税),与对应应付单分录一致 | 应收应付明细页面「调整后金额」afterAmount(非付款申请表单「申请付款金额」appliedAmount,分录金额不按申请金额分摊) |
| project_number | 项目.项目编码 | 是 | **联调期临时固定传 999** | 联调期无;真实来源=付款申请表单「所属项目」projectId → 项目申请「项目编号」projectCode |
| awrp_src_type | 外部单据类型 | 是 | 配置项(默认 0KzXs,待金蝶确认) | 无(后端配置) |
8. **落库**:金蝶单据 id(星瀚付款申请单内部主键)、金蝶单据编号(下推返回的单据编号,立即可见)、同步状态 = synced。
9. 失败处理:任一步异常 → 单据事务回滚,独立事务落「生成失败(failed)」(仅审批通过单据生效),返回原因(含金蝶 errors 明细)。failed 单据可重新勾选同步,从断点续传。
### 4.2 最终金蝶数据
![image-20260920114206305](F:\workspace\work\xdwl\dev\tms\tms-api\doc\docs\业务流程\2026-09-20-应付单与付款申请同步金蝶全链路业务流程(A102-2保存-A102-4查询-A103-1下推-A103-2回写).assets\image-20260920114206305.png)
---
## 5. 环节三:PaymentKingdeeResultTask 定时回写(A103-2 / A103-4)
### 5.1 任务与触发
- 调度:Spring 定时任务,默认**每 10 分钟**一次;由 Nacos 配置 `thirdParty.bfi.payResult.syncEnabled`(开关,默认关)与 `thirdParty.bfi.payResult.cron` 控制。
- 手动触发:付款管理「手动回写金蝶付款结果」接口,逻辑同一入口。
- 任务只**查询回写,不主动推送**。
### 5.2 ① A103-2 付款申请单查询回写
1. 轮询范围(同时满足):审批状态 = approved,且金蝶同步状态 ∈ {synced, paying, failed}。paid / closed 为终态不再轮询;failed 纳入勾稽——金蝶查到说明上次推送实际成功(如请求超时但远端已落单),自动纠偏回填,防止人工重推产生重复单据。
2. 查询方式:按付款申请号分批(每批 50)调 A103-2,自动翻页;同一来源单号取最近修改一条。
A103-2 入参来源(定时回写):
| 接口字段 | 含义 | 来源 | 前端字段(来源页面) |
| --- | --- | --- | --- |
| awrp_src_code | 外部系统来源单据号(批量分批,每批 50,支持多值) | 待回写付款申请的付款申请号 paymentNo | 付款管理列表「单据号」paymentNo(系统生成) |
| awrp_src_sys | 外部系统编号 | 配置项 thirdParty.bfi.srcSys,默认 WLTMS | 无(后端配置) |
| pageNo / pageSize | 分页 | 自动翻页(页大小 200,最大 50 页保护) | 无(系统生成) |
3. 回写内容(查到才更新,未查到状态不变):
- 金蝶单据编号 ← 返回 billno;金蝶单据 id ← 返回 id;
- 金蝶同步状态 ← 按「A103-2 状态映射」(见 6.3);
- 已付金额 ← 分录已付金额(e_paidamt)汇总;分录缺失且已付款时兜底取申请金额。
4. 付款完成(paid)联动:更新履约付款数据(预结算预付记录、正式结算付款记录)。
### 5.3 ② A103-4 付款处理查询回填付款记录
1. 回填候选:
- 本轮 A103-2 查到付款状态 ≠ Unpaid(已开始付款)的单据;
- 已付款(paid)但尚无真实付款记录的单据(补偿轮询)。
2. 查询方式:按**付款人(公司)编码**分组查询;业务日期窗口固定最近 1 个月(接口限制);自动翻页。
A103-4 入参来源:
| 接口字段 | 含义 | 来源 | 前端字段(来源页面) |
| --- | --- | --- | --- |
| org_number | 付款人(公司)编码(支持多值,按编码分组查询) | 优先 A103-2 返回的付款组织编码 payorg_number,兜底按合同所属组织解析部门编码 | 兜底来源=合同管理页面「所属组织」organizationId → 系统部门管理「部门编码」deptCode |
| begin_bizdate / end_bizdate | 业务日期窗口 | 最近 1 个月(接口限制不超过 1 个月),截止当天 | 无(系统生成) |
| pageNo / pageSize | 分页 | 自动翻页(页大小 200,最大 50 页保护) | 无(系统生成) |
3. 勾稽匹配:付款处理单的源单 id = 本地金蝶单据 id 优先;源单编号 = 本地金蝶单据编号 兜底。
4. 回填:按付款处理单号 upsert 付款记录——实际付款金额、付款日期、付款处理单整体 JSON 留档(凭证号、银行流水、电子回单等);同时清理历史模拟(MOCK-PAY- 前缀)付款记录。
---
## 6. 状态限制与状态机
### 6.1 各环节调用前的状态门禁汇总
| 环节 | 单据 | 必须满足 | 幂等跳过 | 拦截报错 |
| --- | --- | --- | --- | --- |
| 环节一 正式结算单同步 | 正式结算单 | 审批状态=approved;结算类型=payable;有结算明细 | 同步状态=synced 直接返回已有编号 | 草稿/审批中/已驳回/已作废;应收结算类型;无结算明细;档案缺失(组织编码/币种/客商编码/货物编码/计量单位/货物费用行) |
| 环节二 付款管理同步 | 付款申请 | 审批状态=approved | 金蝶状态∈{synced,paying,paid,closed} 直接返回已有单据id | 未审批;结算类型为应收;关联正式结算单的应付单未生成(提示先完成环节一);档案缺失 |
| 环节三 定时回写 | 付款申请 | 审批状态=approved 且金蝶状态∈{synced,paying,failed} | paid/closed 终态不再轮询 | 单笔回写异常仅计数不中断整轮 |
### 6.2 金蝶同步状态机(本地字段 kingdeeStatus / kingdeeSyncStatus)
```
unsynced 未生成 ──推送成功──> synced 已生成 ──paystatus=Inpayment──> paying 付款中 ──paystatus=Alreadypay──> paid 已付款(终态)
│ │ │
│ └────billstatus=D──> closed 已关闭(终态)└──billstatus=D──> closed 已关闭(终态)
└──推送异常(事务回滚+独立事务落状态)──> failed 生成失败 ──重推成功或回写勾稽纠偏──> synced / paying / paid
```
### 6.3 A103-2 返回值 → 本地状态映射
| A103-2 返回 | 本地状态 | 是否终态(停止轮询) |
| --- | --- | --- |
| 付款状态 paystatus = Alreadypay | paid 已付款 | 是 |
| 单据状态 billstatus = D | closed 已关闭 | 是 |
| 付款状态 paystatus = Inpayment | paying 付款中 | 否(继续轮询) |
| 其余(billstatus=A/B/C 且 paystatus=Unpaid) | synced 已生成 | 否(继续轮询) |
| 查无此单(金蝶未生成) | 状态不变 | —(failed 单据保持等待人工处理) |
---
## 7. 字典对照表
### 7.1 审批状态(approvalStatus,正式结算单/付款申请共用)
| 字典值 | 含义 |
| --- | --- |
| draft | 草稿 |
| reviewing | 审批中(财务审核) |
| approved | 审批通过(**唯一允许同步金蝶的状态**) |
| returned | 已驳回 |
| voided | 已作废 |
### 7.2 金蝶同步状态(kingdeeStatus / kingdeeSyncStatus,本地)
| 字典值 | 含义 | 说明 |
| --- | --- | --- |
| unsynced | 未生成 | 新建单据默认 |
| synced | 已生成 | 推送成功,等待金蝶付款 |
| paying | 付款中 | A103-2 paystatus=Inpayment 部分付款 |
| paid | 已付款 | A103-2 paystatus=Alreadypay,终态 |
| closed | 已关闭 | A103-2 billstatus=D,终态 |
| failed | 生成失败 | 推送异常/金蝶返回失败,可重推 |
### 7.3 金蝶付款申请单状态(A103-2 返回)
| 字段 | 字典值 | 含义 |
| --- | --- | --- |
| paystatus 付款状态 | Alreadypay | 已付款 |
| paystatus 付款状态 | Inpayment | 部分付款(付款中) |
| paystatus 付款状态 | Unpaid | 未付款 |
| billstatus 单据状态 | A | 暂存 |
| billstatus 单据状态 | B | 已提交 |
| billstatus 单据状态 | C | 已审核 |
| billstatus 单据状态 | D | 已关闭 |
### 7.4 金蝶单据枚举(常量维护,待金蝶侧确认后调整)
| 类别 | 字典值 | 含义 |
| --- | --- | --- |
| 付款类型 e_paymenttype_number | 201 | 正式付款(结算付款) |
| 付款类型 e_paymenttype_number | 202 | 预付款(A103-3 路径使用) |
| 往来类型 asstacttype | bd_supplier | 供应商(合同类别非「客户合同」) |
| 往来类型 asstacttype | bd_customer | 客户(合同类别=「客户合同」) |
| 付款方式 purmode | CASH | 现购(金蝶自动 100% 推付款,当前使用) |
| 付款方式 purmode | CREDIT | 赊购(金蝶侧可调付款金额,备用) |
| 财务应付单单据类型 | 服务采购应付 → ApFin_service_BT_S | A102-2 当前默认(另有借贷项调整/费用采购应付/其他应付/产品委外应付/标准采购应付/采购费用应付/销售费用应付/资产采购应付共 9 项映射字典) |
| 财务应收单单据类型 | 服务销售应收 → arfin_sersal_BT_S 等 5 项 | 应收链路未启用 |
| 付款申请单单据类型 | 采购付款申请 → ap_payapply_BT_S、其他付款申请 → ap_payapply_oth_BT_S | 使用规则待业务确认 |
| 款项性质 payproperty_number | 2001 | 材料采购(暂定,待金蝶确认) |
| 外部单据类型 awrp_src_type | TMS财务应付单(A102-2)/ 配置值(A103-1) | 应付单/付款申请单来源标识,待金蝶确认 |
### 7.5 TMS 业务类型
| 字段 | 字典值 | 含义 |
| --- | --- | --- |
| paymentType 付款类型 | settlement_payment | 结算付款(走 A102-4 预检 + A103-1 下推链路) |
| paymentType 付款类型 | project_advance | 项目预付款(走 A103-3 直推链路) |
| paymentType 付款类型 | progress_advance | 进度预付款(走 A103-3 直推链路) |
| settlementType 结算类型 | payable | 应付(对应财务应付单 A102-2,已启用) |
| settlementType 结算类型 | receivable | 应收(对应财务应收单 A302-2,未启用,同步拦截) |
| 请款事由 applycause | 结算付款申请 / 项目预付款申请 / 进度预付款申请 | 按 paymentType 路由(A103-3 路径使用) |
### 7.6 防重键体系(awrp_src_sys = WLTMS)
| 单据 | 防重键 awrp_src_code 取值 | 使用接口 |
| --- | --- | --- |
| 财务应付单 | 正式结算单号_结算明细单据号1_单据号2…(去重保序拼接) | A102-2 / A102-4 |
| 付款申请单 | 付款申请号(paymentNo) | A103-1 / A103-2 / A103-3 |
| 应付单付款计划分录 | awrp_entry_code_p:金蝶自动生成,A102-4 反查获得 | A103-1 勾稽关联 |
| 分录唯一标识 | awrp_entry_code:应付货物费用行 id(雪花 id) | A102-2 明细 / A103-1 分录 |
---
## 8. 异常与恢复场景
| # | 断点场景 | 金蝶侧实际 | 本地状态 | 恢复路径 |
| --- | --- | --- | --- | --- |
| 1 | 环节一推送前校验失败(档案缺失等) | 无应付单 | 结算单状态不变 | 补档案后重新同步 |
| 2 | A102-2 成功后中断(返回未落库) | 应付单已生成 | 结算单未 synced | 重新同步:A102-4 预检命中 → 幂等跳过保存,直接回填编号 |
| 3 | 环节二前置校验:应付单未生成 | 无应付单 | 付款申请状态不变 | 报错提示先在正式付款单页面同步金蝶(先完成环节一) |
| 4 | A103-1 远端已落单但本地回滚 | 应付单+付款申请单均已生成 | failed | 重推:A102-4 命中跳过前置 → A103-2 勾稽命中 → 回填置 synced;或定时任务自动纠偏 |
| 5 | 同步成功但状态待推进 | 付款申请单已生成 | synced | 定时任务 A103-2 回写驱动 synced→paying→paid/closed,A103-4 回填付款记录 |
| 6 | 金蝶反审核删除后重推 | 无单据 | failed/synced | 预检查无 → 重新推送生成新单据 |
| 7 | failed 且金蝶确无单据 | 无单据 | failed | 每轮任务空查一次 A103-2,状态保持,等待人工处理(已知边界) |
---
## 9. 涉及功能与代码位置(索引用)
| 内容 | 位置 |
| --- | --- |
| 环节一入口 | `FormalSettlementServiceImpl.syncKingdee`(正式付款单页面「同步金蝶」) |
| 环节二入口 | `PaymentApplicationController`(/sync-kingdee、/sync-kingdee-batch、/sync-kingdee-result)→ `PaymentApplicationServiceImpl`(syncKingdee / doSyncKingdee / syncKingdeeBatch) |
| 环节三定时任务 | `PaymentKingdeeResultTask` → `PaymentApplicationServiceImpl.syncKingdeePaymentResult`(applyPayApplyResult / applyPayBillRecord) |
| A102-2/A102-4 公共支撑 | `KingdeeApBillSyncSupport`(防重键组装、A102-4 查询/反查、A102-2 组装保存、分录解析、档案取数) |
| 金蝶常量/字典 | `KingdeeBfiConstant`;配置 `thirdParty.bfi.*`(srcSys=WLTMS 等) |
| BFI 客户端 | `blade-third-party-api/blade-bfi-api`:IBfiClient / BfiServiceImpl(saveApBill=A102-2、queryApBill=A102-4、pushPayApply=A103-1、queryPayApply=A103-2、queryPayBill=A103-4) |
## 10. 遗留注意事项
1. **联调临时取值**:A102-2 明细物料/计量单位、A103-1 往来银行编码、双方项目编码当前传金蝶测试档案值(SP0324 / TON / 001110012009 / 999),档案对齐后需恢复真实取值。
2. **应收链路未启用**:结算类型为应收的单据在两侧均拦截报错(A302-2 待业务确认)。
3. **kingdeeBillNo 阶段语义**:正式结算单上为应付单编号;付款申请上先为下推返回的付款申请单编号,后续被 A103-2 回写覆盖校准。
4. **failed 空查**:确无金蝶单据的 failed 付款申请会被定时任务每轮空查一次,量大可考虑窗口截断(当前保持简单)。
5. **付款申请单/收入确认单类型映射、预付抵扣 preentry、结算方式/税率**:均待业务或金蝶侧确认,本期不传/未启用。
@@ -0,0 +1,262 @@
# 应收结算与开票申请同步金蝶设计(A302-2/A302-4 + A303-1/A303-5)
> 日期:2026-09-20
> 模块:tms-api(blade-transport、blade-service-api/blade-transport-api、blade-third-party-api/blade-bfi-api 零改动)
> 关联文档:`tms-api/doc/docs/业务流程/2026-09-20-应付单与付款申请同步金蝶全链路业务流程(A102-2保存-A102-4查询-A103-1下推-A103-2回写).md`
> 状态:待评审
---
## 1. 背景与目标
正式结算单的应收(receivable)链路与开票申请的金蝶同步目前未接入:应收结算单「同步金蝶」直接拦截报错;开票申请「同步金蝶」为模拟实现(生成 `K3INV+时间戳` 假单号,未真正调金蝶)。BFI 客户端侧 `saveArBill`(A302-2)、`queryArBill`(A302-4)、`saveInvoiceApply`(A303-1)、`queryInvoiceApply`(A303-4)、`queryInvoiceApplyInvoice`(A303-5) 均已实现但无调用方。
本次目标:
1. **应收结算链路**:应收正式结算单「同步金蝶」→ A302-2 财务应收单保存(前置 A302-4 幂等预检),完全对齐应付链路(A102-2/A102-4)模式。
2. **开票申请链路真实化**:开票申请「同步金蝶」替换为真实 A303-1 推送(前置校验关联应收单已生成 + A303-4 幂等预检)。
3. **定时回写扩展**:复用 `PaymentKingdeeResultTask`,新增 ② A302-4 应收勾稽纠偏、③ A303-5 开票结果回写(发票结果落新表,状态推进 `invoiced` 终态)。
## 2. 范围
**做**:上述三条链路;新表 `blade_invoice_application_invoice`;`KingdeeBfiConstant`/`BfiProperties` 常量与配置扩展;定时任务编排扩展;手动触发接口。
**不做(后续迭代)**:收票 A303-2/A302-6、收款认领 A301-1/2/3、客商风险等级与高风险管控、履约计划、附件推送、收票认领先进先出规则、应付链路任何改动、BFI 客户端任何改动。
## 3. 总体原则(与既有链路一致)
1. **先内后外**:单据先在 TMS 审批通过(approved)再推金蝶。
2. **以金蝶侧实际生成为准**:每次推送前按防重键(awrp_src_sys=WLTMS + awrp_src_code)查金蝶幂等预检,已生成则回填跳过;推送失败可断点续传。
3. **failed 纳入定时勾稽纠偏**:远端实际成功但本地回滚的单据由定时任务查到后自动纠偏,防止人工重推产生重复单据。
## 4. 业务流程
```
【环节一】正式结算单「同步金蝶」(人工,单笔)
审批通过的应收正式结算单
→ 校验:approved;settlementType=receivable(本次放行);非 synced;有结算明细
→ 组装防重键(正式结算单号_结算明细单据号…,与应付同格式)
→ A302-4 幂等预检 ──已生成──> 幂等跳过保存,直接回填已有单号
→ A302-2 财务应收单保存(明细分录逐应收货物费用行)
→ 回写结算单:kingdeeBillNo + synced + syncedTime
【环节二】开票申请「同步金蝶」(人工,替换 K3INV 模拟)
审批通过的开票申请
→ 前置校验:A302-4 校验关联应收结算单的应收单已生成(未生成拦截,提示先完成环节一)
→ A303-4 幂等预检(billno=applicationNo)──已生成──> 直接回填,视为已同步
→ A303-1 开票申请单新增(购销方/商品行分录)
→ 回写开票申请:kingdeeBillNo(金蝶真实单号)+ synced
【环节三】PaymentKingdeeResultTask 定时(每10分钟,编排三个子流程,各自 try/catch 互不影响)
① A103-2/A103-4 付款回写(现状不动)
② A302-4 应收勾稽纠偏:轮询 approved+receivable+状态∈{synced,failed},
按防重键批量查询:查到→补金蝶单号、failed纠偏为synced;查不到→不变(不推进状态机)
③ A303-5 开票结果回写:轮询 approved+kingdeeStatus=synced+金蝶单号非空的开票申请,
serialNo 批量查询(≤50/批)+ 销方税号:查到发票→逐张落新表(按发票号幂等)
→ 申请单 kingdeeStatus=invoiced(终态);查不到→保持 synced 继续轮询
```
收款由收款认领链路(A301-x)负责,不在本次范围;应收单的审核/核销状态不回写。
## 5. 状态机与防重键
### 5.1 状态机
```
应收结算单 kingdeeSyncStatus(无新状态值):
unsynced ──A302-2成功──> synced(终态)
└──推送异常──> failed ──重推 或 定时A302-4勾稽纠偏──> synced
开票申请 kingdeeStatus(新增枚举值 invoiced,varchar 无需 DDL):
unsynced ──A303-1成功──> synced ──A303-5查到发票──> invoiced(终态)
└──推送异常──> failed ──人工重推(A303-4 预检命中→幂等回填)──> synced
注:定时任务只轮询 synced(failed 无真实金蝶单号,A303-5 查询无入口,纠偏走重推预检)
```
### 5.2 防重键体系(awrp_src_sys = WLTMS)
| 单据 | awrp_src_code | 使用接口 |
| --- | --- | --- |
| 财务应收单 | 正式结算单号_结算明细单据号1_单据号2…(去重保序下划线拼接) | A302-2 / A302-4(预检+定时勾稽) |
| 开票申请单 | 开票申请单号 applicationNo(A303-1 billno 候选键) | A303-1 / A303-4(幂等)/ A303-5(serialNo) |
| 分录唯一标识 | 应收货物费用行 id(雪花 id,awrp_entry_code) | A302-2 明细 |
## 6. 接口字段映射
### 6.1 A302-2 财务应收单保存(主表)
| 接口字段 | 取数 | 备注 |
| --- | --- | --- |
| billtype_number | 常量「服务销售应收」→ arfin_sersal_BT_S | AR_BILL_BILLTYPE_MAP 已有;映射名新增常量,待金蝶确认 |
| org_number / recorg_number | 合同所属组织 deptCode | 复用 bfiOrgNumber |
| bizdate | 同步当日 yyyy-MM-dd HH:mm:ss | |
| asstacttype | bd_customer(固定客户) | |
| asstact_number | 结算单付款方 payerName → 客商档案(全称/简称)客商编码 | 未维护报错;与应付(合同乙方)对称 |
| paymode | CASH(现销) | 待金蝶确认 |
| payproperty_number | 1001(一般销售) | 与应付 2001 区分,待金蝶确认 |
| currency_number | 合同结算币种(RMB→CNY) | 复用 bfiCurrency |
| isincludetax / ispricetotal / isintax | true / true / false | 与应付一致 |
| awrp_src_sys / awrp_src_code / awrp_src_type | WLTMS / 防重键 /「TMS财务应收单」 | src_type 新常量,待金蝶确认 |
| awrp_src_bt / awrp_src_con_no | 本期不传 | 与应付一致,保留注释 |
| duedate / 结算方式 / 税率编码 | 本期不传 | 待业务确认(遗留清单) |
### 6.2 A302-2 明细分录(entry,逐应收货物费用行,不聚合)
| 接口字段 | 取数 | 备注 |
| --- | --- | --- |
| e_material_number | 货物类型→货物类型档案货物编码 | 联调期固定 SP0324(同应付) |
| e_measureunit_number | 运费计算单位(兜底数量单位) | 联调期固定 TON(同应付) |
| e_quantity | 运输数量,兜底 1 | |
| e_unitprice | 运输单价 | ispricetotal=true 仅参考 |
| e_recamount | 应收金额(含税)= 费用行调整后总金额 afterAmount | 对应应付 e_pricetaxtotal 语义 |
| e_amount | 金额(不含税)= 结算费用行不含税回链 | 兜底 0,与应付同源逻辑 |
| project_number | 联调期 999 | |
| awrp_entry_code | 应收货物费用行 id | 候选键 |
收款计划 planentity 不传,由金蝶按明细自动汇总。
### 6.3 A303-1 开票申请单新增(主表)
| 接口字段 | 取数 | 备注 |
| --- | --- | --- |
| orgid_number | 申请部门 deptCode | deptId→SysCache |
| billno | applicationNo | 候选键,天然唯一 |
| billdate | applicationDate yyyy-MM-dd HH:mm:ss | |
| billproperties | 1(正数) | |
| invoicetype | electronic_normal→10xdp、electronic_special→08xdp | 数电普票/专票 |
| taxationstyle | 0(普通征税) | |
| buyeremail | departmentEmails | 必填 |
| buyerproperty | 0(企业) | |
| buyertaxno / buyername | 申请单快照 taxpayerNo / receiverName | |
| salername / salertaxno | 申请单快照 issuerName(结算单收款方/开票单位);按开票单位查客商档案(全称/简称)取统一社会信用代码 | A303-5 的 sellerTaxpayerId 同源,不走 Nacos 配置 |
| buyeraddr + showbuyeraddresstel / buyerbank + showbuyerbank | 申请单受票方快照 注册地址+联系电话、开户行+银行账号(新增/编辑选择开票信息时落库) | 展示开关固定 true;DTO 非必填,金蝶是否强校验待联调确认 |
| saleraddr + showsaleraddresstel / salerbank + showsalerbank | 客商档案注册地址+联系电话、客商开票信息(启用默认优先)开户行+银行账号;两个展示开关固定 true | DTO 标非必填,数电专票金蝶强校验(缺失即报错),随地址/银行值一同送 |
| totalamount | invoiceAmount(价税合计) | |
| invoiceamount / totaltax | 明细不含税合计 / 税额合计 | |
| customnameid_number | receiverCustomerId → 客商档案客商编码 | 未维护报错 |
| awrp_project_number | 联调期 999 | |
| awrp_orgfield_number | 申请部门 deptCode | |
| awrp_src_bt | ar_finarbill | 上游财务应收单 |
| awrp_src_con_no | 关联结算单金蝶应收单号 | 多张取第一张,拼接规则联调确认 |
| billsource / systemsource | 3(固定)/ 配置 srcSys | systemsource 待联调确认 |
| 合同号/合同金额/购方地址银行等选填 | 本期不传 | 保留注释(遗留清单) |
### 6.4 A303-1 分录(sim_original_bill_item,逐页面「开票内容」商品行一条)
取数口径:**商品行(`blade_invoice_application_line`)落库什么就传什么**,不回查开票项目主数据、不按开票明细→费用行展开分摊。
| 接口字段 | 取数 | 备注 |
| --- | --- | --- |
| goodsname / goodssimplename | line.goodsName / line.goodsCategory | 货物或服务简称 / 商品和服务分类 |
| taxratecodeid_number / goodscode | line.taxClassificationCode(商品行快照,同源) | 新增/编辑开票申请选择开票内容时随前端落库,缺失即报错提示重选;goodscode 联调确认必传 |
| num | line.quantity | 数量为 0 兜底 1 |
| unit | line.unit | 空兜底「个」 |
| taxunitprice | line.totalAmount ÷ quantity | 6 位小数;数量为 0 时按兜底 1 计算 |
| unitprice | line.unitPriceNoTax | 不含税单价 |
| taxamount / amount / tax | line.totalAmount / line.amountNoTax / line.taxAmount | 价税合计 / 不含税金额 / 税额 |
| taxrate | line.taxRate ÷ 100(0-100 转小数) | 税率为空即报错;行税率是百分数,与开票项目 default_tax_rate(小数)单位不同 |
| remark | line.remark | |
| awrp_entry_code | line.id | 外部来源分录标识(候选键),雪花 id 天然唯一 |
| benchmark / rowtype | 0(数量基准)/ 2(商品行) | rowtype 联调校准 |
| awrp_src_con_eno | 待联调确认 | 先不传,见遗留清单 |
| awrp_src_type | 配置 thirdParty.bfi.invoiceApply.srcType(默认 0KzXs) | 与付款申请 payApply.srcType 对齐 |
### 6.5 A302-4 / A303-5 回写取数
- **A302-4 勾稽**:按 awrp_src_code 归并,命中即以 `billno` 回填/纠偏,其余字段不取用。
- **A303-5**:`serialNo`(金蝶开票申请单号,≤50/批逗号拼接)+ `sellerTaxpayerId`(与 A303-1 同源:按各申请单开票单位查客商档案统一社会信用代码,按税号分组分批查询,无法解析的申请单跳过并告警)。返回为票易通原文 `List<Object>`:整体 JSON 留档 `raw_json`,宽松提取发票号码/开票日期/价税合计三个字段(Jackson 泛型解析,具体字段名联调校准后收敛为常量映射)。
## 7. 代码组织与改动清单
### 7.1 blade-transport(主改动)
| # | 文件 | 改动 |
| --- | --- | --- |
| 1 | `support/KingdeeArBillSyncSupport`(新建) | 应收单防重键组装(复用 ExternalSrcCodeSupport.buildSrcCode)、A302-4 批量查询 queryArBillExistNos、应收分录解析 resolveArEntryGroups(取数源同应付:blade_receivable_payable_cargo_fee)、pushArBill(A302-4 预检 + A302-2 组装保存)。公共档案取数(bfiOrgNumber/bfiCurrency/requireContract/客商编码查询)经注入 KingdeeApBillSyncSupport 复用,不复制不修改现有类 |
| 2 | `FormalSettlementServiceImpl.syncKingdee` | 移除应收拦截;按 settlementType 路由 payable→pushApBill / receivable→pushArBill;落库逻辑共用 |
| 3 | `FormalSettlementServiceImpl.syncKingdeeArResult`(新增) | A302-4 应收勾稽纠偏(轮询+批量查+回填纠偏),接口方法+Controller 手动触发端点 `/sync-kingdee-ar-result` |
| 4 | `InvoiceApplicationServiceImpl.syncKingdee` | 模拟实现替换为真实推送:A302-4 前置校验(关联应收单已生成)→ A303-4 幂等预检 → A303-1 保存 → 回写 |
| 5 | `InvoiceApplicationServiceImpl.syncKingdeeInvoiceResult`(新增) | A303-5 回写:轮询+批量查+发票落新表+invoiced 推进;Controller 手动触发端点 `/sync-kingdee-invoice-result` |
| 6 | `task/PaymentKingdeeResultTask` | 注入 IFormalSettlementService、IInvoiceApplicationService,编排 ①付款回写 ②应收勾稽 ③开票结果 三个子流程,各自独立 try/catch,汇总日志 |
| 7 | `constant/KingdeeBfiConstant` | 新增:BFI_AR_BILL_BILLTYPE_NAME「服务销售应收」、BFI_AR_BILL_PAY_PROPERTY=1001、BFI_AR_BILL_PAY_MODE_CASH=CASH、BFI_AR_BILL_SRC_TYPE「TMS财务应收单」、KINGDEE_STATUS_INVOICED=invoiced、发票类型映射(electronic_normal→10xdp / electronic_special→08xdp) |
| 8 | `config/BfiProperties` | 新增 invoiceApply 节点:srcType(默认 0KzXs)。销方名称/税号、税收分类编码不走配置:销方名称取申请单开票单位快照、税号按开票单位查客商档案统一社会信用代码、税收分类编码取商品行开票内容快照 |
| 9 | `mapper/InvoiceApplicationInvoiceMapper`(新建) | 接口 + XML 同包 |
### 7.2 blade-transport-api
| # | 文件 | 改动 |
| --- | --- | --- |
| 10 | `pojo/entity/InvoiceApplicationInvoice`(新建) | TenantEntity:invoiceApplicationId、serialNo、invoiceNo、invoiceDate、invoiceAmount、rawJson |
| 11 | `IFormalSettlementService` / `IInvoiceApplicationService` | 新增 syncKingdeeArResult / syncKingdeeInvoiceResult 接口方法 |
| 12 | 开票申请详情 VO | 附带发票结果列表(@TableField(exist=false) 或关联查询),前端展示开票结果 |
## 8. 数据库变更
新表 `blade_invoice_application_invoice`(MySQL 版,正式提交时同步 doc/sql 全部 7 库脚本;无菜单 SQL):
```sql
CREATE TABLE `blade_invoice_application_invoice` (
`id` bigint NOT NULL COMMENT '主键',
`tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
`invoice_application_id` bigint NOT NULL COMMENT '开票申请id',
`serial_no` varchar(64) DEFAULT NULL COMMENT '金蝶开票申请单编号',
`invoice_no` varchar(64) NOT NULL COMMENT '发票号码',
`invoice_date` datetime DEFAULT NULL COMMENT '开票日期',
`invoice_amount` decimal(18,2) DEFAULT NULL COMMENT '价税合计',
`raw_json` text COMMENT 'A303-5返回原文JSON留档',
`create_user` bigint DEFAULT NULL COMMENT '创建人',
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` bigint DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
`status` int DEFAULT NULL COMMENT '状态',
`is_deleted` int DEFAULT 0 COMMENT '是否已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_application_invoice` (`invoice_application_id`,`invoice_no`),
KEY `idx_iai_application_id` (`invoice_application_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开票申请金蝶发票结果';
```
落库幂等:按(invoice_application_id, invoice_no)唯一键,已存在跳过。开票结果不反写结算单——结算单开票状态仍由收票认领链路(InvoiceReceipt)驱动,避免双链路冲突。
## 9. 配置项(Nacos thirdParty.bfi.*)
```yaml
thirdParty:
bfi:
srcSys: WLTMS # 已有
payResult:
syncEnabled: true # 已有,任务总开关(三个子流程共用)
cron: 0 */10 * * * ? # 已有
invoiceApply:
srcType: 0KzXs # 新增:分录外部单据类型,与 payApply.srcType 对齐(有默认值,一般无需配置)
```
销方名称/销方税号、税收分类编码**不再需要配置**:开票申请「新增」页选择结算单即带出开票单位(销方),销方税号按开票单位查客商档案统一社会信用代码;税收分类编码随「开票内容」选择快照到商品行(blade_invoice_application_line.tax_classification_code,增量脚本 `blade_invoice_application_line_tax_code_20260921.sql`)。
## 10. 异常与恢复场景
| # | 断点场景 | 金蝶侧实际 | 本地状态 | 恢复路径 |
| --- | --- | --- | --- | --- |
| 1 | 应收单推送前校验失败(档案缺失等) | 无应收单 | 结算单状态不变 | 补档案重新同步 |
| 2 | A302-2 成功但本地回滚 | 应收单已生成 | 未 synced | 重新同步 A302-4 命中→幂等回填;或定时任务纠偏 |
| 3 | 开票申请同步时应收单未生成 | — | 不变 | 拦截报错,先完成环节一 |
| 4 | A303-1 成功但本地回滚 | 开票申请单已生成 | 未 synced | 重推时 A303-4 命中→幂等回填 |
| 5 | 金蝶开票处理中 | 无发票 | synced | A303-5 每轮空查,直到开出票 |
| 6 | A303-5 查到发票 | 发票已开出 | synced | 落发票结果表→invoiced 终态 |
任一子流程异常只计数不中断整轮(与付款回写一致)。
## 11. 验证
1. `mvn clean compile -DskipTests` 编译通过(AGENTS.md 要求;测试由用户执行)。
2. 每个被改符号编辑前跑 GitNexus impact(upstream),提交前 detect_changes(仓库强制规范)。
3. 联调验证顺序:应收结算单同步(A302-2/4)→ 开票申请同步(A303-1/4)→ 手动触发定时回写(A302-4 勾稽 + A303-5 回写)。
## 12. 遗留与待确认清单
1. **A302-2**:paymode CASH、款项性质 1001、单据类型「服务销售应收」、awrp_src_type「TMS财务应收单」、到期日/结算方式/税率编码——均待金蝶确认。
2. **A303-1**:rowtype、awrp_src_con_eno、购方地址/银行展示、systemsource 取值、多结算单单号拼接规则、部门邮箱分隔符——联调校准。
3. **A303-5**:返回原文字段名(发票号/开票日期/金额)联调后从宽松解析收敛为固定映射。
4. ~~**销方主体**:当前走配置~~ 已改为单据取数:销方名称=开票申请开票单位快照(结算单收款方带出),销方税号=开票单位对应客商档案统一社会信用代码(要求我方开票主体在客商档案中维护)。
5. **应收结算单开票完成状态**:本次不反写结算单;如业务需要"已开票"标识,后续在结算单侧增量。
@@ -0,0 +1,213 @@
# 收票管理同步金蝶全票池发票(A801-1 查询 + 批量同步 + 定时绑定关联)业务流程
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)
> 对接系统:金蝶发票云旗舰版·票易通(piaozone)全票池,经业财融合 BFI 网关(rim 契约)透传
> 关联文档:`doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`(进项收票 A801-1 章节)、`2026-09-17-BFI分页查询通用翻页工具(BfiUtil-queryAllPages)技术文档.md`、`2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md`、`2026-09-17-客商档案同步金蝶(A902-1查询回填+A902-2审批推送)技术文档.md`
> 最后更新:2026-09-20
>
> **⚠️ 退役声明(2026-09-22)**:本文档描述的**链路二(定时任务全量拉取、增量窗口、批量 upsert、定时绑定、漏绑补偿)及票种/客商税号双白名单、手动全量同步端点、按票号补查已全部退役删除**,选票改为实时查询 A801-1、镜像降级为登记存根(保存时单票重查落库),终态口径见 `2026-09-22-金蝶票池选票统一查镜像(收票登记+结算单认领+按票号补查+统一组件)业务流程.md`(已改写为实时查询版)。**仍然有效的部分**:链路一的手动逐单同步 `POST /invoice-receipt/sync-kingdee`(职责收敛为刷新已登记票的镜像行)、A801-1 契约与 `queryPoolByInvoiceNo` 单查口径、镜像表结构(含 `serial_no/buyer_tax_no/saler_tax_no/check_status` 等列,图05 推送链消费 `invoice_type_code/items_json`)。下文与退役链路相关的叙述保留作历史口径,不再代表现状。
---
## 1. 背景与范围
收票管理(`/payment/invoice-receipt`)当前依赖本地镜像表 `blade_kingdee_invoice_pool`(金蝶进项发票票据池镜像)选票登记,镜像数据无自动采集来源;「批量同步」按钮(权限码 `invoice_receipt_sync`)调用的 `syncKingdee` 仅回读本地镜像复制 `kingdeeBillNo/kingdeeStatus`,**并未真正请求金蝶**。本文档建设两条真实同步链路:
- **链路一(手动批量同步)**:收票管理列表勾选「审批通过」的收票登记 → 批量同步 → 后端逐单按发票号码调用 **A801-1 查询全票池发票数据** → 刷新票据池镜像并绑定/回填同步状态。
- **链路二(定时任务)**:新增定时任务分页拉取 A801-1 全票池数据(增量窗口)→ 按 `invoiceNo` upsert 镜像 → **与收票登记做绑定关联**(按发票号码回填 `kingdeeInvoicePoolId`、金蝶流水号与同步状态),覆盖金蝶侧采票滞后于收票登记的场景。
不在本期范围:A801-2 发票附件文件下载(`attachmentsJson` 仅记录 `downloadUrl` 备用)、收票单推送金蝶(A303-2)、收票认领结果与正式结算单自动认领。
## 2. 对接接口(A801-1)
| 项 | 说明 |
| --- | --- |
| 接口 | A801-1 查询全票池发票数据(票易通 rim 契约,经 BFI 网关 `/kapi/app/rim/message` 透传) |
| 既有能力 | `IBfiService.queryInvoicePool(BfiInvoicePoolQueryDTO)` 单页查询,本次**无改动复用** |
| 请求体 | `{ messageType: "invoiceQuery", messageId: <服务端填充>, queryParam: { pageNo, pageSize, ...筛选 } }`,`pageSize` 上限 500,`pageNo` 从 1 开始 |
| 响应 | `{ data: [发票...], errorCode, message, status }`,**不返回总数、无末页标识**;成功码 0000 由 `BfiServiceImpl.commonCall` 统一处理 |
| 关键查询条件 | `invoiceNo`(发票号码,手动同步按此精确查询)、`updateTime/updateTimeEnd`(更新时间窗口,定时任务增量拉取)、`buyerTaxNo/salerTaxNo`(税号)、`expenseStatus`(单据使用状态)、`invoiceDateStart/End`(开票日期区间) |
| 关键返回字段 | `invoiceNo` 发票号码、`serialNo` 发票流水号(**票池唯一标识**)、`invoiceDate`、`invoiceType`、`totalAmount` 价税合计、`taxAmount`、`taxRate`、`buyerName/buyerTaxNo`、`salerName/salerTaxNo`、`checkStatus` 查验状态、`delete` 是否作废删除、`downloadUrl` |
## 3. 业务流程
### 3.1 集成链路总览
```
金蝶发票云全票池(A801-1 查询全票池发票数据)
│
├── 链路一:手动批量同步(收票管理列表「批量同步」按钮)
│ 勾选 approvalStatus = approved 的收票登记(前端已限定)
│ → 逐单 POST /invoice-receipt/sync-kingdee
│ → 后端按登记发票号码精确查询 A801-1
│ → 命中:upsert 镜像 + 绑定 kingdeeInvoicePoolId + 回填金蝶流水号/同步状态
│ → 未命中:置 unsynced,流水记录原因「金蝶票池未查询到」
│
└── 链路二:定时任务(InvoiceReceiptKingdeeSyncTask,默认关闭)
→ A801-1 按 updateTime 增量窗口分页拉取(pageSize 500,最大 50 页保护)
→ 按 invoiceNo upsert 镜像 blade_kingdee_invoice_pool
→ 绑定关联:未绑定镜像的收票登记按 invoiceNo 匹配回填
→ 已绑定登记:跟随镜像刷新金蝶流水号/同步状态(delete=1 → failed)
```
### 3.2 链路一:手动批量同步时序
```
invoice-receipt.vue InvoiceReceiptController InvoiceReceiptServiceImpl 金蝶(BFI网关)
│ 批量同步按钮 │ │ │
│ 勾选 approved 行(前端校验) │ │ │
│ 逐单 syncKingdee(id) ─────────>│ POST /invoice-receipt/ │ │
│ │ sync-kingdee?id=xxx │ │
│ │ ──────────────────────────────>│ ①校验 approvalStatus=approved │
│ │ │ ②按 invoiceNo ───────────────>│ A801-1
│ │ │ <── 票池发票(0/1条)──────────│
│ │ │ ③命中:upsert 镜像(按 invoiceNo)│
│ │ │ 绑定 kingdeeInvoicePoolId │
│ │ │ 回填 kingdeeBillNo=serialNo │
│ │ │ 刷新 kingdeeStatus;写 sync 流水│
│ │ │ ④未命中:kingdeeStatus=unsynced │
│ │ │ 写 sync 流水(原因) │
│ <── 同步结果(金蝶流水号)──────────────────────────────────────│ │
```
前端 `handleBatchSync` 现有逻辑(`invoice-receipt.vue:366`)保持不变:仅允许勾选「审批通过」行、`Promise.all` 逐单调单条接口,失败由 axios 拦截器统一提示。
### 3.3 链路二:定时绑定时序
```
InvoiceReceiptKingdeeSyncTask InvoiceReceiptServiceImpl 金蝶(BFI网关)
│ @Scheduled cron(可配) │ │
│ syncEnabled 开关校验 │ │
│ syncKingdeeInvoicePool() ───────>│ ①增量窗口(滚动近N天)+ 发票种类/客商税号白名单 │
│ │ ②A801-1 分页拉取(pageSize 500,最大50页)────>│
│ │ <── 票池发票列表 ────────────────────────────│
│ │ ③白名单过滤(购销双方税号须命中客商档案)后按号批量 upsert 镜像│
│ │ ④绑定回填批量落库(页级事务,批量失败整页跳过) │
│ │ ⑤补偿绑定:未绑定登记按发票号码精确补查(不限窗口)──>│
│ │ 单轮限量、批量落库,直到绑定成功为止 │
│ <── 执行摘要日志(拉取/过滤/镜像/绑定/刷新/失败/补偿统计)│ │
```
- 任务类仿 `PaymentKingdeeResultTask` / `CustomerKingdeeSyncTask`:业务逻辑收敛在 `IInvoiceReceiptService.syncKingdeeInvoicePool()`,任务类只做开关与调度,便于手动触发联调。
- 增量窗口约定:**每轮固定滚动近 N 天**(可配,建议 30 天),不依赖"上次成功时间点"——单轮失败不会漏,同一批数据会被后续每轮反复幂等覆盖。
- **客商税号白名单(购、销双方均须命中)**:每轮加载客商档案有效税号(`unifiedCreditCode`,status=1),窗口拉回的发票须**购方税号与销方税号同时命中**该集合才入镜像、参与绑定;我方主体税号同样维护在客商档案中,故进项票要求「购方=我方、销方=建档客商」同时成立。白名单外发票不入镜像不绑定;**集合为空不限制**(档案未维护时兼容全量)。由此推论:**供应商未在客商档案维护统一社会信用代码,其进项票不会进票据池镜像,前端选票也就选不到**——档案维护是同步的前置条件,可用每页过滤明细日志的「客商税号白名单外 N 张」定位漏档。`buyerTaxNo`/`salerTaxNo` 均为 rim 契约单值参数,无法按税号集合远程查询,故采用窗口拉取 + 内存过滤的等价实现。
- **防漏绑补偿**:滚动窗口唯一的漏绑缝隙是「持续失败超过 N 天(窗口滑出 updateTime)」,第 ⑤ 步兜底——仍为未绑定(`kingdeeInvoicePoolId` 为空)的登记按发票号码精确补查 A801-1(优先复用本地镜像,缺失才远程),单轮限量(`compensateLimit`,默认 200,按创建时间先进先出),绑定成功即移出补查队列;金蝶票池暂无该发票时跳过不写库、待后续轮次。**手动同步与补偿绑定不受税号白名单约束**(票由登记侧发起,属我方票)。
- 提供手动触发端点 `POST /invoice-receipt/sync-invoice-pool`(权限码复用 `invoice_receipt_sync`),返回执行摘要,供联调与补偿。
### 3.4 匹配与幂等规则
| 约定 | 说明 |
| --- | --- |
| 匹配键 | `invoiceNo`(发票号码)。收票登记侧 `assertInvoiceUnused` 已保证一张发票仅可登记一次,镜像侧按 `invoiceNo` upsert 天然幂等 |
| 发票种类白名单 | `thirdParty.bfi.invoiceReceipt.invoiceTypes`(逗号分隔票易通编码,如 `26,27`)控制可同步的发票种类:白名单外发票**不落镜像不绑定**(手动同步记「发票类型不在同步范围」流水,定时拉取在类型过滤统计中体现);配置为空不限制。定时拉取翻页终止仍按原始页条数判断,不受过滤影响 |
| 客商税号白名单 | 客商档案有效税号(`unifiedCreditCode`,status=1)为白名单,窗口拉回的发票须**购方税号与销方税号同时命中**才入镜像、参与绑定(我方主体税号亦维护在客商档案中);**集合为空不限制**。供应商未建档/未维护统一社会信用代码 → 其进项票不进镜像、前端选票选不到。与发票种类白名单叠加生效;手动同步与补偿绑定不受此约束 |
| 镜像批量写库 | 同页镜像**一次批量查询**既有记录(`invoiceNo IN 同页号码`)+ `Db.saveBatch` / `Db.updateBatchById` **批量写入**(避免逐条查改 N+1);写入前按**票面字段指纹**比对(`attachments_json` 等大字段在内),**无变化的镜像不重写**,避免每轮整窗重写大字段;指纹中空值按「维持库中原值」处理,与 MyBatis-Plus 非空字段更新策略一致,防止源侧空字段每轮误判为变更。批量写失败中断本轮,由任务层记录异常、下轮重试(upsert 幂等) |
| 绑定批量落库 | 绑定回填先在**内存**完成(登记实体按页批量查出,票面字段不覆盖),再逐项比对 `kingdeeInvoicePoolId`/`kingdeeBillNo`/`kingdeeStatus`,**三项均无变化的登记不更新、不计入统计**;有变化的以**页级事务**批量落库:`updateBatchById` 登记绑定 + `Db.saveBatch` 同步流水(`applyInvoicePoolSyncBatch`,JDBC batch)。**流水只在绑定关系变化(原因「定时绑定金蝶票池发票」)或金蝶状态迁移(原因「定时刷新金蝶票池状态」)时写入**,纯流水号回填不刷流水。批量失败**整页跳过**计入失败统计,下轮任务幂等重试,**无逐单降级路径**(`applyInvoicePoolSyncResult` 仅手动单条同步使用) |
| 防漏绑补偿 | 增量窗口为滚动近 N 天,单轮失败不漏;唯一缝隙是「持续失败超 N 天窗口滑出」。补偿绑定步骤对未绑定登记(`kingdeeInvoicePoolId` 为空、非作废)批量补绑:本地镜像**一次批量查询**复用(主流程本轮已落库镜像命中免远程),缺失才按发票号码**精确补查** A801-1(rim 契约无批量号码参数,仅此处逐号远程),新增镜像批量写入 + 绑定页级事务批量落库;单轮限量 `compensateLimit`(默认 200)先进先出,绑定成功即出队;票池暂无则跳过待下轮,不写库不刷流水 |
| 手动同步查询 | A801-1 `invoiceNo` 精确单查(rim 契约单页,返回 0/1 条);多条理论上不存在,取第一条并 warn 日志 |
| 定时绑定范围 | `approvalStatus IN (draft, reviewing, approved, returned)` 且(`kingdeeInvoicePoolId` 为空 或 指向镜像不存在的记录);`voided` 不参与绑定 |
| 绑定回填范围 | 仅回填**绑定关系与金蝶侧信息**:`kingdeeInvoicePoolId`、`kingdeeBillNo`(=serialNo)、`kingdeeStatus`;**不覆盖**登记已保存的票面字段(发票号、金额、税额、收开票单位等),避免冲掉人工登记信息 |
| 镜像刷新口径 | 镜像表字段一律以 A801-1 返回为准(票池是唯一事实源);镜像存在即可被新登记选票 |
| 金额校验 | 绑定时若票池价税合计与登记 `invoiceAmount` 不一致仅记 warn 日志(跨系统开票金额可能变更),不阻断绑定 |
| 已绑定状态刷新 | 跟随镜像刷新 `kingdeeStatus`;票池 `delete=1`(作废删除)置 `failed`,流水记录原因,**不自动作废**收票登记,人工确认处理 |
| 逐单隔离 | 定时链路无逐单路径:镜像批量查改 + 绑定页级批量落库;仅批量落库异常时整页跳过、下轮幂等重试 |
| 关键日志 | 任务开始(增量窗口 + 发票种类 + 客商税号白名单个数 + 单页/最大页数)、每页拉取条数(含 BFI 侧远程耗时)、镜像批量写入条数(新增/更新/无变化)、每页落库小结(变更/绑定/状态刷新/流水条数)、绑定成功逐条(发票号码 + 登记id + 金蝶流水号)、落库异常(含页码与堆栈)、翻页触顶 warn(窗口尾部本轮未处理)、补偿小结(补偿绑定数 + 本地镜像命中 + 远程按号补查次数 + 票池无票清单 + 耗时)、锁跳过 warn、未启用分布式锁 warn、结束汇总摘要(翻页/拉取/类型过滤/客商过滤/镜像新增更新/绑定/状态刷新/登记无变化跳过/失败/补偿绑定/耗时) |
### 3.5 状态映射(kingdeeStatus 语义)
| 场景 | 本地 kingdeeStatus | 展示名 | 说明 |
| --- | --- | --- | --- |
| A801-1 命中且 `delete=0` | `synced` | 已同步 | 票池匹配成功,`kingdeeBillNo` 回填 `serialNo` |
| A801-1 未命中 | `unsynced` | 未同步 | 金蝶票池未查询到(可能未采集/发票号不符),流水记录原因 |
| A801-1 命中且 `delete=1` | `failed` | 同步失败 | 发票在金蝶侧已作废/删除,流水记录原因,人工确认 |
| 同步过程异常(接口失败) | 保持原状态 | — | 本轮终止或该单跳过,下轮重试 |
## 4. 字段映射关系
### 4.1 A801-1 → 镜像表 `blade_kingdee_invoice_pool`
| A801-1 字段 | 含义 | 落库字段 | 说明 |
| --- | --- | --- | --- |
| invoiceNo | 发票号码 | `invoice_no` | upsert 匹配键 |
| serialNo | 发票流水号 | `serial_no`(**新增列**) | 票池唯一标识 |
| invoiceDate | 开票日期 | `invoice_date` | yyyy-MM-dd |
| invoiceType | 发票类型编码 | `invoice_type` | 经 `BfiConstant.INVOICE_TYPE_NAMES` 编码转名称落库(与正式结算单认领口径一致) |
| taxRate | 税率(小数) | `tax_rate` | 小数转百分数(0.13 → 13),顶层为空回退首条明细 |
| totalAmount | 价税合计 | `invoice_amount` | 与正式结算单认领同口径(价税合计即发票金额) |
| taxAmount | 税额 | `tax_amount` | 2 位小数 |
| buyerName | 购方名称 | `receiver_name` | 受票单位 |
| buyerTaxNo | 购方税号 | `buyer_tax_no`(**新增列**) | 便于按组织税号过滤 |
| salerName | 销方名称 | `issuer_name` | 开票单位 |
| salerTaxNo | 销方税号 | `saler_tax_no`(**新增列**) | 便于按销方过滤 |
| checkStatus | 查验状态 | `check_status`(**新增列**) | 0 未查验 / 1 通过 / 2 不通过 |
| delete | 是否作废删除 | —(映射为登记侧 `failed`) | 不在镜像冗余存储,随查随判 |
| downloadUrl | 文件下载地址 | `attachments_json` | `{"source":"piaozone","path":"<downloadUrl>","name":"<发票号码>.pdf"}`,A801-2 接入备用 |
| updateTime/billCreateTime | 票池更新时间 | `source_updated_time` | 镜像排序字段 |
### 4.2 镜像/票池 → 收票登记绑定回填
| 来源 | 回填字段 | 说明 |
| --- | --- | --- |
| 镜像主键 | `kingdee_invoice_pool_id` | 绑定关系,绑定后登记详情/编辑可关联票池 |
| 镜像 `serial_no` | `kingdee_bill_no` | 金蝶侧唯一流水号(A801-1 无单据号概念,以流水号作金蝶标识) |
| 匹配结果 | `kingdee_status` | 见 3.5 状态映射 |
| — | `blade_invoice_receipt_record` | 每次同步写 `actionType=sync` 流水(成功/未匹配/作废原因),定时任务操作人以「定时任务」名义记录 |
## 5. 数据库变更
| 变更 | 内容 | 说明 |
| --- | --- | --- |
| 新增列 | `blade_kingdee_invoice_pool`:`serial_no` varchar(64)、`saler_tax_no` varchar(32)、`buyer_tax_no` varchar(32)、`check_status` varchar(8) | 独立幂等脚本 `doc/sql/transport/blade_kingdee_invoice_pool_a8011_20260920.sql`(参照 `blade_payment_application_kingdee_bill_id_20260917.sql` 风格,**不改动**建表模板);排序规则 `utf8mb4_general_ci` |
| 索引 | 无需新增 | 建表模板已有唯一索引 `uk_kingdee_invoice_pool_no (tenant_id, invoice_no)`,覆盖 upsert 与绑定匹配 |
| 收票表 | `blade_invoice_receipt` 无结构变更 | 复用既有 `kingdee_invoice_pool_id/kingdee_bill_no/kingdee_status` |
## 6. 配置项(Nacos:`thirdParty.bfi.invoiceReceipt.*`)
模板同步 `doc/nacos/blade-dev.yaml`、`blade-prod.yaml`、`third-party-api.yaml`:
| 配置键 | 环境变量 | 默认值 | 说明 |
| --- | --- | --- | --- |
| thirdParty.bfi.invoiceReceipt.syncEnabled | BFI_INVOICE_RECEIPT_SYNC_ENABLED | false | 定时绑定任务开关(上线初期关闭,联调稳定后开启) |
| thirdParty.bfi.invoiceReceipt.cron | BFI_INVOICE_RECEIPT_CRON | 0 */30 * * * ? | 调度表达式(默认每 30 分钟) |
| thirdParty.bfi.invoiceReceipt.incrementDays | BFI_INVOICE_RECEIPT_INCREMENT_DAYS | 30 | 首轮/兜底增量窗口天数 |
| thirdParty.bfi.invoiceReceipt.invoiceTypes | BFI_INVOICE_RECEIPT_INVOICE_TYPES | 空 | 同步发票种类白名单(逗号分隔票易通编码,如 26,27 数电普票/专票),为空不限制 |
| thirdParty.bfi.invoiceReceipt.compensateLimit | BFI_INVOICE_RECEIPT_COMPENSATE_LIMIT | 200 | 补偿绑定单轮限量(未绑定登记按发票号码精确补查条数上限) |
| thirdParty.bfi.invoiceReceipt.lockLeaseSeconds | BFI_INVOICE_RECEIPT_LOCK_LEASE_SECONDS | 1800 | 全票池同步 single-flight 锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放 |
**数据源前置条件**:`doc/nacos/blade-dev.yaml`、`blade-test.yaml`、`blade-prod.yaml` 的 MySQL 连接串须带 `rewriteBatchedStatements=true`,否则 `Db.saveBatch` / `updateBatchById` 仍是逐条往返、不构成真正的 JDBC 批量(该参数为 MySQL 专有,切换其他数据库需移除)。配置变更后需重启服务生效。
**分布式锁前置条件**:`RedisLockClient` 仅在 `blade.lock.enabled=true` 时装配(该开关无缺省值,不配即关闭)。dev/test 模板已开启,且 `blade.lock.address` 与 `spring.data.redis` 指向同一实例;prod 模板保持关闭,开启前须先补 `blade.lock.password`(与 `spring.data.redis` 一致),否则 Redisson 建连被拒会拖垮依赖该模板的所有服务。锁未启用时全票池同步照常执行,只是失去多副本 single-flight 保护并打 warn 日志。
## 7. 异常处理
| 场景 | 行为 |
| --- | --- |
| A801-1 接口级失败(status=false / errorCode≠0000) | `BfiServiceImpl.commonCall` 抛 `BfiException`:链路一该单同步失败并提示;链路二当前页终止,下轮任务自动重试 |
| 手动同步按发票号未查到 | 登记 `kingdeeStatus=unsynced`,流水记录「金蝶票池未查询到」,不抛异常中断批量 |
| 绑定时金额不一致 | warn 日志(发票号 + 双方金额),仍按 3.4 口径绑定 |
| 单条绑定/状态刷新异常 | 记录 error 日志(含发票号与堆栈),计数后继续下一单 |
| 定时任务与手动同步并发 | 链路二入口按租户加 Redis 分布式锁做 **single-flight**(`RedisLockClient.tryLock(key, REENTRANT, 0, lockLeaseSeconds)`,零等待):多副本调度、手动端点撞上进行中的轮次时,抢不到锁的一侧直接跳过并返回「同步进行中,本轮跳过」,不重复拉取与写库;未开启 `blade.lock.enabled` 时降级为无锁执行并 warn。锁之外仍靠镜像按 invoiceNo upsert、登记按主键更新、绑定判重幂等保证最终一致 |
| A801-1 翻页异常(rim 无总数) | 以「本页条数 < pageSize 或空页」终止,最大 50 页保护,防御死循环 |
## 8. 涉及代码位置
| 内容 | 位置 | 改动 |
| --- | --- | --- |
| 前端页面 | `tms-erp-web/src/views/payment/invoice-receipt.vue`(handleBatchSync)、`src/api/payment/invoiceReceipt.js`(syncKingdee) | 无需改动(复用现有单条同步接口与按钮) |
| 同步入口改造 | `blade-service/blade-transport/.../service/impl/InvoiceReceiptServiceImpl.java`(syncKingdee 接入 A801-1 查询 + 镜像 upsert + 绑定回填) | 修改 |
| 定时绑定编排 | 同上 `syncKingdeeInvoicePool()`(RedisLockClient single-flight + 租户锁键)、`executeInvoicePoolSync()`(增量窗口分页拉取 + 票面指纹比对 upsert + 变更比对绑定)、`applyInvoicePoolSyncBatch()`(页级事务:绑定批量更新 + 同步流水批量写入);`applyInvoicePoolSyncResult()` 仅手动单条同步使用 | 新增 |
| Service 接口 | `.../service/IInvoiceReceiptService.java`(syncKingdeeInvoicePool) | 新增 |
| 定时任务 | `blade-service/blade-transport/.../task/InvoiceReceiptKingdeeSyncTask.java` | 新增(仿 PaymentKingdeeResultTask) |
| 手动触发端点 | `.../controller/InvoiceReceiptController.java`(POST /invoice-receipt/sync-invoice-pool) | 新增(可选) |
| 镜像实体 | `blade-transport-api/.../pojo/entity/KingdeeInvoicePool.java`(serialNo/salerTaxNo/buyerTaxNo/checkStatus) | 修改 |
| BFI 客户端 | `blade-third-party-api/blade-bfi-api/.../service/IBfiService.java`(queryInvoicePool)、`pojo/dto/BfiInvoicePoolQueryDataDTO.java`、`pojo/vo/BfiInvoicePoolVO.java` | 无改动复用 |
| SQL | `doc/sql/transport/`(镜像表新增列 + 索引幂等脚本) | 新增 |
| Nacos 配置模板 | `doc/nacos/blade-dev.yaml`、`blade-test.yaml`、`blade-prod.yaml`(thirdParty.bfi.invoiceReceipt.*、MySQL `rewriteBatchedStatements`、`blade.lock.enabled`)、`third-party-api.yaml` | 新增 |
## 9. 遗留事项与联调确认
1. **A801-1 增量窗口口径**:`updateTime/updateTimeEnd`(yyyy-MM-dd HH:mm:ss)为票池更新时间,联调时需用真实票池数据验证窗口过滤有效性;无效则退化为按 `invoiceDateStart/End`(开票日期)滚动窗口全量比对(upsert 幂等,代价可接受)。
2. **`serialNo` 作金蝶标识**:A801-1 无金蝶单据号概念,以发票流水号回填 `kingdee_bill_no`;后续如接 A303-2 收票单推送,金蝶侧单据号语义另行扩展,避免混用。
3. **镜像存量数据**:镜像表存量记录 `serial_no` 为空,由首轮定时任务/手动同步按 invoiceNo 命中后补齐;未命中的存量数据保持原状不清理。
4. **A801-2 附件下载**:`attachmentsJson.path` 仅记录备用,查看/下载附件待 A801-2 接入后建设。
5. **票池作废联动**:`delete=1` 仅置 `failed` 提示人工确认,是否自动作废收票登记待业务确认后调整。
@@ -0,0 +1,240 @@
# 应收结算与开票申请同步金蝶全链路业务流程(正式结算单同步 → 开票申请同步 → 应收勾稽与开票结果定时回写)
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)
> 对接系统:金蝶云·星瀚(业财融合 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-10-08(联调纠偏:A303-1 awrp_src_con_no 改传首张结算单金蝶应收单防重键 awrp_src_code,传单据编号会报「源单编号不存在」;2026-10-01 上游来源单据放开传值 awrp_src_bt=ar_finarbill;2026-09-21 按当前代码梳理,销方名称/销方税号/税收分类编码随单据取数)
---
## 1. 整体业务链路
应收侧一笔结算到最终开出数电票,分**三个环节**,由两个页面 + 一个定时任务接力完成:
```
环节一:正式结算单「同步金蝶」(人工,正式收款单/结算单页面,单笔)
审批通过的应收类正式结算单 → 推送金蝶财务应收单(A302-2,前置 A302-4 幂等预检)
→ 回写结算单:金蝶单据编号 + 同步状态 synced
环节二:开票申请「同步金蝶」(人工,开票申请页面,单笔)
审批通过的开票申请 → 前置校验关联结算单的应收单已在金蝶生成(A302-4,缺一拦截)
→ A303-4 幂等预检(billno=开票申请单号,命中直接勾稽回填)
→ 推送金蝶开票申请单(A303-1,购销方信息 + 发票商品行分录)
→ 回写开票申请:金蝶单据编号 + 同步状态 synced
环节三:PaymentKingdeeResultTask 定时回写(自动,默认每10分钟,三子流程各自 try/catch 互不影响)
① A103-2/A103-4 付款结果回写(应付链路,本文不展开)
② A302-4 应收勾稽纠偏:轮询 approved+应收+{synced,failed},按防重键批量查询,
查到 → 补金蝶单号、failed 纠偏为 synced;查不到 → 状态不变
③ A303-5 开票结果回写:轮询 approved+synced+金蝶单号非空的开票申请,按销方税号分组
分批(≤50/批)查询,查到发票 → 逐张落结果表(发票号幂等)→ 申请单推进 invoiced(终态)
```
核心设计原则(与应付链路一致):
1. **先内后外**:单据先在 TMS 审批通过(approved)才允许推金蝶。
2. **以金蝶侧实际生成为准**:每次推送前按防重键查询确认,已生成则幂等跳过/回填勾稽,未生成才推送;推送接口真正报错才算失败。
3. **断点续传**:任一环节中断(远端成功、本地回滚)后重新触发或等定时勾稽,均从断点恢复,不产生重复单据。
4. **配置最小化**:金蝶推送所需的业务取数一律随单据快照或走档案(客商/开票项目/货物类型/部门),Nacos 仅剩外部系统编号 srcSys 与分录外部单据类型 srcType(有默认值)。
---
## 2. 五个接口在链路中的角色
| 接口 | 名称 | 方向 | 触发环节 | 业务用途 |
| --- | --- | --- | --- | --- |
| A302-2 | 财务应收单保存 | TMS → 金蝶 | 环节一 | 一张应收正式结算单生成一张金蝶财务应收单(ar_finarbill) |
| A302-4 | 财务应收单查询 | TMS → 金蝶(查询) | 环节一前置 / 环节二前置 / 环节三② | ①推送前幂等预检;②开票申请推送前校验关联应收单已生成;③定时勾稽纠偏(防重键批量查询,50/批+自动翻页) |
| A303-1 | 开票申请单新增 | TMS → 金蝶 | 环节二 | 生成金蝶开票申请单(上游勾稽财务应收单),由金蝶/票易通完成实际开票 |
| A303-4 | 开票申请单查询 | TMS → 金蝶(查询) | 环节二前置 | 按 billno=开票申请单号 幂等预检,断链重推时命中即勾稽回填 |
| A303-5 | 开票申请发票查询 | TMS → 金蝶(查询) | 环节三③ | 按开票申请单号+销方税号查已开发票,落 `blade_invoice_application_invoice` 并推进开票完成 |
收款认领(A301-x)、收票(A303-2/A302-6)、应收单核销状态回写、结算单开票完成反写均不在本链路(后续迭代)。
---
## 3. 环节一:正式结算单「同步金蝶」推送财务应收单(A302-2)
代码入口:`FormalSettlementServiceImpl.syncKingdee` → 按结算类型路由,应收走 `KingdeeArBillSyncSupport.pushArBill`(应付走 A102-2,见应付侧文档)。
### 3.1 业务流程
1. 操作入口:正式结算单页面「同步金蝶」按钮,单笔。
2. 前置校验(不满足直接报错,不调金蝶):
- 审批状态 = **approved**;
- 同步状态已是 **synced** → 幂等返回已有金蝶单号,不重复推送(failed 允许重推);
- 必须有结算明细(分录取数来源)。
3. 组装财务应收单防重键(awrp_src_code):**正式结算单号_结算明细单据号1_单据号2…**(去重保序、下划线拼接),与应付侧同规则;一张结算单只生成一张金蝶应收单。
4. 解析应收单明细分录(`resolveArEntryGroups`,逐**应收货物费用行**一条、不聚合):
- 结算明细 `sourceDetailId` 回链应收应付货物费用行(`blade_receivable_payable_cargo_fee`),无货物费用行直接报错拦截;
- 含税金额取费用行调整后总金额 `afterAmount`,不含税经结算货物费用行 `sourceFeeId` 回链(无映射兜底 0);
- 货物类型(「、/,」多类型不拆行,取第一种)必须在货物类型档案维护货物编码;计量单位取运费计算单位(兜底数量单位)。
5. **A302-4 幂等预检**:按防重键批量查询,已生成 → 幂等跳过保存、直接取已有单号回写;未生成 → 调 A302-2 保存。
6. **A302-2 保存**,主表入参来源:
| 接口字段 | 含义 | 必填 | 来源 | 前端字段(来源页面) |
| --- | --- | --- | --- | --- |
| billtype_number | 单据类型.编码 | 是 | 常量名称「服务销售应收」经 `AR_BILL_BILLTYPE_MAP` 转 `arfin_sersal_BT_S`(待金蝶确认,改常量名即可换映射) | 无(后端常量) |
| org_number / recorg_number | 结算组织/收款组织.编码 | 是 | 合同所属组织 deptCode(未维护报错) | 合同管理页面「所属组织」→ 部门编码 |
| bizdate | 单据日期 | 是 | 同步当日 yyyy-MM-dd HH:mm:ss | 无(系统生成) |
| asstacttype | 往来类型 | 是 | 固定 bd_customer(应收对方必为客户) | 无(后端常量) |
| asstact_number | 往来户.编码 | 是 | 结算单**付款方** payerName → 客商档案按全称/简称匹配取客商编码(未维护报错) | 正式结算单页面「付款方」payerName → 客商档案「客商编号」customerCode |
| paymode | 付款方式 | 是 | 常量 CASH(现销,金蝶自动 100% 推收款) | 无(后端常量,待金蝶确认) |
| payproperty_number | 款项性质.编码 | 是 | 常量 1001(一般销售,与应付 2001 区分,待金蝶确认) | 无(后端常量) |
| currency_number | 结算币别.货币代码 | 是 | 合同结算币种(RMB→CNY) | 合同管理页面「结算币种」 |
| isincludetax / ispricetotal | 录入含税价/录入总价 | 否 | 固定 true / true(免单价换算) | 无(后端常量) |
| awrp_src_sys | 外部系统编号(候选键) | 是 | 配置 thirdParty.bfi.srcSys,默认 WLTMS | 无(后端配置) |
| awrp_src_code | 外部来源单据号(候选键) | 是 | 第 3 步防重键 | 系统组装:「结算单号」+结算明细「单据号」 |
| awrp_src_type | 外部单据类型 | 是 | 常量「TMS财务应收单」(待金蝶确认) | 无(后端常量) |
| 到期日/结算方式/税率编码 | — | 否 | 本期不传(TODO 待金蝶确认后补充) | — |
明细分录(entry,逐应收货物费用行):
| 接口字段 | 含义 | 必填 | 来源 |
| --- | --- | --- | --- |
| e_material_number | 物料编码.编码 | 是 | 货物类型→货物类型档案货物编码;**联调期临时固定 SP0324**(pushArBill 内 `if (true)` 调试块覆盖,校准后移除) |
| e_measureunit_number | 计量单位.编码 | 是 | 运费计算单位(兜底数量单位,档案暂无编码传名称);**联调期临时固定 TON** |
| e_quantity / e_unitprice | 数量/单价 | 是/否 | 费用行运输数量(兜底 1)/运输单价(录入总价仅参考) |
| e_recamount | 应收金额(含税) | 是 | 费用行调整后总金额 afterAmount |
| e_amount | 金额(不含税) | 否 | 结算货物费用行不含税回链,兜底 0 |
| project_number | 项目.项目编码 | 是 | 联调期固定 999(真实来源=项目档案项目编码) |
| awrp_entry_code | 来源分录标识(候选键) | 是 | 应收货物费用行 id(雪花 id 天然唯一) |
收款计划 planentity 不传,由金蝶按明细自动汇总生成。
7. 回写落库:`result[0].number` → 结算单 `kingdeeBillNo` + `kingdeeSyncStatus=synced` + `syncedTime`,并记结算单变更历史「同步金蝶财务应收单{单号}」。失败抛业务异常,状态不变可重推。
### 3.2 状态机与勾稽纠偏
```
kingdeeSyncStatus:unsynced ──A302-2成功──> synced(终态)
└──推送异常──> failed ──人工重推 或 定时②A302-4勾稽纠偏──> synced
```
定时子流程②(`FormalSettlementServiceImpl.syncKingdeeArResult`,手动端点 `POST /formal-settlement/sync-kingdee-ar-result`):轮询 approved+receivable+{synced,failed},按防重键批量查 A302-4;查到 → 补回缺失单号、failed 纠偏为 synced;查不到 → 不动。返回汇总:待查 X 笔、回写 Y 笔(纠偏 Z 笔)。
---
## 4. 环节二:开票申请「同步金蝶」推送开票申请单(A303-1)
代码入口:`InvoiceApplicationServiceImpl.syncKingdee`;页面 `invoice-application-form.vue`(列表 `invoice-application.vue`)。
### 4.1 单据创建与审批(推送前)
1. 新增开票申请:选择**应收正式结算单**(可多张,须同合同/项目/组织/收付款方)→ 自动带出所属项目、**开票单位**(=结算单收款方,即销方)、承办部门、可开票金额;受票方从付款方客商的开票信息(`blade_customer_invoice_info`)选择,纳税人识别号/开户行/账号/注册地址随选择带出并快照到申请单。
2. 开票内容按「发票张」维护商品行:商品和服务分类 + 货物或服务简称均从**开票项目主数据**(`blade_invoice_item`)选择,选择时默认税率随带,并**快照税收分类编码**到商品行 `tax_classification_code`(A303-1 分录用,不走配置)。
3. 商品行只能整体开「运费」或整体开非运费(运费不得与其他费用并票);本次开票金额=商品行含税合计,不得超过可开票金额。
4. 状态流转:draft(草稿可编辑/删除)→ submit → reviewing → approve → approved(或 return 驳回、void 作废);仅 **approved** 允许同步金蝶。
5. 结算单开票状态(unreceived/partial/completed)由申请单分摊金额实时刷新(作废/删除联动回退),金蝶回写不反写结算单。
### 4.2 同步金蝶流程
1. 校验:approved;已 synced/invoiced 幂等返回(invoiced 终态);申请单号非空。
2. **A302-4 前置校验**:逐关联结算单组装应收单防重键批量查询,任一结算单的财务应收单未在金蝶生成 → 拦截报错「请先在正式付款单页面同步金蝶」(即先完成环节一)。
3. **A303-4 幂等预检**:billno=开票申请单号查询,金蝶已生成 → 直接回填单号 + synced(勾稽回填,防断链重推重复)。
4. 取数校验(任一不满足报错):开票单位(销方)非空;**销方税号=开票单位按全称/简称匹配客商档案的统一社会信用代码**;销方地址电话(客商档案注册地址+联系电话)与销方开户行账号(客商开票信息开户行+账号)均已维护(数电专票金蝶强校验);部门邮箱(发票推送邮箱)、购方识别号、申请部门编码、受票方客商编码均已维护;每条商品行已有税收分类编码快照与税率(历史单据缺失时提示编辑重选开票内容)。
5. **A303-1 新增**,主表入参来源:
| 接口字段 | 含义 | 必填 | 来源 | 前端字段(来源页面) |
| --- | --- | --- | --- | --- |
| orgid_number / awrp_orgfield_number | 组织/申请部门.编码 | 是 | 承办部门 deptId → SysCache 部门编码(未维护报错) | 开票申请页「承办部门」(结算单带出) |
| billno | 业务单据编号(候选键) | 是 | 开票申请单号 applicationNo | 系统生成(TMSKV 规则单号) |
| billdate | 单据日期 | 是 | 申请日期(提交时置当日) | 页面「申请日期」 |
| billproperties | 单据性质 | 是 | 固定 1(正数) | 无 |
| invoicetype | 发票种类 | 是 | electronic_normal→10xdp(数电普票)、electronic_special→08xdp(数电专票) | 页面「发票类型」下拉 |
| taxationstyle | 征税方式 | 是 | 固定 0(普通征税) | 无 |
| buyeremail | 发票推送邮箱 | 是 | 部门邮箱(客商开票信息邮箱多选,`;` 拼接) | 页面「部门邮箱」 |
| buyerproperty / buyertaxno / buyername | 购方企业类型/税号/名称 | 是 | 固定 0(企业)/ 申请单快照 taxpayerNo / receiverName | 页面「受票方单位」选择带出 |
| buyeraddr / showbuyeraddresstel | 购方地址及电话 / 展示买方地址电话 | 否(金蝶是否强校验待联调确认) | 申请单受票方快照 注册地址+联系电话拼接,展示开关固定 true | 页面「注册地址」「联系电话」(受票方开票信息带出) |
| buyerbank / showbuyerbank | 购方开户行及账号 / 展示买方银行 | 否(待联调确认) | 申请单受票方快照 开户行+银行账号拼接,展示开关固定 true | 页面「开户银行」「银行账号」(受票方开票信息带出) |
| salername | 销方名称 | 是 | 申请单快照开票单位 issuerName(=结算单收款方) | 页面「开票单位」(结算单带出) |
| salertaxno | 销方纳税人识别号 | 是 | 开票单位 → 客商档案统一社会信用代码(**A303-5 sellerTaxpayerId 同源**) | 客商档案「统一社会信用代码」 |
| saleraddr / showsaleraddresstel | 销方地址及电话 / 展示卖方地址电话 | 否(数电专票强校验必传) | 客商档案注册地址+联系电话拼接,展示开关固定 true | 客商档案「注册地址」「联系电话」 |
| salerbank / showsalerbank | 销方开户行及账号 / 展示卖方银行 | 否(数电专票强校验必传) | 客商开票信息(启用、默认优先)开户行+银行账号拼接,展示开关固定 true | 客商开票信息「开户银行」「银行账号」 |
| totalamount / invoiceamount / totaltax | 价税合计/不含税合计/税额合计 | 是 | 申请单开票金额 / 商品行不含税合计 / 税额合计 | 页面「本次开票金额」、商品行自动计算 |
| customnameid_number | 客户.编码 | 是 | 受票方客商档案客商编码(未维护报错) | 客商档案「客商编号」 |
| awrp_project_number | 项目.项目编码 | 是 | 联调期 999 | — |
| billsource | 数据来源 | 是 | 固定 3 | 无 |
| 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`,仅查张次与商品行两张表):
| 接口字段 | 含义 | 必填 | 来源 |
| --- | --- | --- | --- |
| rowtype / benchmark | 行性质/基准类型 | 是 | 固定 2(商品行)/ 0(数量基准)(rowtype 联调校准) |
| goodsname / goodssimplename | 货物名称/商品和服务分类 | 是 | 行 goodsName(货物或服务简称)/ goodsCategory(分类名称) |
| taxratecodeid_number / goodscode | 税收分类编码.编码/税收分类合并编码 | 是 | 同行快照 tax_classification_code(选开票内容时随开票项目主数据落库;goodscode 联调确认必传) |
| num / unit | 数量/计量单位 | 是 | 行数量(≤0 兜底 1)/ 单位(空兜底「个」) |
| taxunitprice / taxamount | 含税单价/含税金额 | 是 | 行价税合计÷数量、行价税合计 total_amount |
| amount / unitprice / tax | 不含税金额/单价/税额 | 是 | 行不含税金额 amount_no_tax、不含税单价 unit_price_no_tax、税额 tax_amount |
| taxrate | 税率 | 是 | 行税率 0-100 转小数字符串(13→"0.13"),行税率为空即报错 |
| remark | 备注 | 否 | 行备注 |
| awrp_entry_code | 来源分录标识(候选键) | 是 | 商品行 id(雪花 id 唯一) |
| awrp_src_type | 分录外部单据类型 | 是 | 配置 thirdParty.bfi.invoiceApply.srcType,默认 0KzXs(有默认值,一般无需配置) |
6. 回写落库:`result[0].number` → `kingdeeBillNo` + `kingdeeStatus=synced` + `syncedTime`,记录操作历史「同步金蝶」。
### 4.3 状态机
```
kingdeeStatus:unsynced ──A303-1成功──> synced ──A303-5查到发票──> invoiced(开票完成,终态)
└──推送异常──> failed ──人工重推(A303-4 预检命中→幂等回填)──> synced
定时③只轮询 synced(failed 无真实金蝶单号,A303-5 无查询入口)
```
---
## 5. 环节三③:开票结果定时回写(A303-5)
代码入口:`InvoiceApplicationServiceImpl.syncKingdeeInvoiceResult`,手动端点 `POST /invoice-application/sync-kingdee-invoice-result`。
1. 待回写集合:approved + synced + 金蝶单号非空,按 `kingdeeBillNo`(金蝶开票申请单号=serialNo)去重归并。
2. **销方税号解析(与 A303-1 同源)**:逐单按开票单位查客商档案统一社会信用代码,**按税号分组**后组内 ≤50/批逗号拼接调 A303-5;解析不到(开票单位为空/客商档案未维护信用代码)的单跳过并 warn 计数,不阻断整轮。
3. 返回为票易通原文 `data[]{billNo, invoiceList[]}`:按 billNo 归并回申请单,逐张宽松提取(候选键:发票号 invoiceNo/invoice_no/invoiceNumber;开票日期、价税合计同理),**原文整体 JSON 留档 `raw_json`**;字段名联调校准后收敛为固定映射。
4. 落 `blade_invoice_application_invoice`:按(invoice_application_id, invoice_no)唯一键幂等,已存在跳过;该单有新增发票 → `kingdeeStatus=invoiced`(开票完成,前端详情展示发票列表)。
5. 开票结果**不反写结算单**;结算单开票状态仍由申请单分摊驱动。
6. 汇总文案:待查 X 笔(销方税号无法解析跳过 N 笔),回写发票 Y 张(开票完成 M 笔,失败批次 K 个)。
任务调度:`PaymentKingdeeResultTask`(`thirdParty.bfi.payResult.cron` 默认每 10 分钟,总开关 `thirdParty.bfi.payResult.syncEnabled`,三子流程共用;任一子流程异常仅记日志不中断整轮)。
---
## 6. 防重键与幂等体系汇总
| 单据/对象 | 防重键/幂等依据 | 使用接口 |
| --- | --- | --- |
| 财务应收单 | awrp_src_sys=WLTMS + awrp_src_code=正式结算单号_结算明细单据号拼接 | A302-2 / A302-4(预检+定时勾稽+开票前置校验) |
| 应收单分录 | awrp_entry_code=应收货物费用行 id | A302-2 |
| 开票申请单 | billno=开票申请单号 applicationNo | A303-1 / A303-4(预检)/ A303-5(serialNo) |
| 开票申请分录 | awrp_entry_code=商品行 id | A303-1 |
| 金蝶发票结果 | 唯一键(invoice_application_id, invoice_no) | A303-5 落库 |
断点恢复场景:应收/开票推送成功但本地回滚 → 重推时 A302-4/A303-4 命中幂等回填;failed 应收单 → 定时勾稽自动纠偏;金蝶开票处理中 → ③每轮空查保持 synced,直到开出票。
---
## 7. 数据前提与上线操作
| # | 事项 | 说明 |
| --- | --- | --- |
| 1 | 建表/变更 SQL | `doc/sql/transport/blade_invoice_application_invoice_20260920.sql`(发票结果表);`blade_invoice_application_line_tax_code_20260921.sql`(商品行税收分类编码快照列,可重复执行) |
| 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 已传 srcSys、上游单据号已放开传首张结算单金蝶应收单编号);A303-5:原文字段名收敛 |
| 6 | 联调顺序 | 应收结算单同步(①)→ 开票申请同步(②)→ 手动触发 `sync-kingdee-ar-result` / `sync-kingdee-invoice-result` 验证回写 |
---
## 8. 关键代码索引
| 环节 | 代码位置(blade-service/blade-transport) |
| --- | --- |
| 环节一推送 | `service/impl/FormalSettlementServiceImpl.syncKingdee` + `support/KingdeeArBillSyncSupport`(pushArBill / arBillSrcCode / queryArBillExistNos / resolveArEntryGroups),档案取数复用 `support/KingdeeApBillSyncSupport` |
| 环节二推送 | `service/impl/InvoiceApplicationServiceImpl.syncKingdee / buildInvoiceApplyEntry` |
| 环节三②/③ | `FormalSettlementServiceImpl.syncKingdeeArResult` / `InvoiceApplicationServiceImpl.syncKingdeeInvoiceResult`,编排 `task/PaymentKingdeeResultTask` |
| 常量/配置 | `constant/KingdeeBfiConstant`(AR 单据类型映射、1001、CASH、TMS财务应收单、ar_finarbill、10xdp/08xdp、invoiced)、`config/BfiProperties`(thirdParty.bfi.*) |
| 手动端点 | `POST /formal-settlement/sync-kingdee-ar-result`、`POST /invoice-application/sync-kingdee-invoice-result`(网关按服务名路由至 blade-transport) |
| 前端 | 正式结算单页面(同步金蝶按钮)、`views/payment/invoice-application-form.vue`(新增开票申请:选结算单带出销方、开票内容快照税收分类编码) |
@@ -0,0 +1,197 @@
# 收票单推送金蝶全链路业务流程(A303-2 收票单保存 → A302-6 收票指定应付 → A801-3 幂等预检与定时回写)
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)
> 对接系统:金蝶云·星瀚(业财融合 BFI,经 iPaaS 网关 `/gwzh/BFI` 直连,不经过合同合规系统)
> 覆盖接口:A303-2 收票单批量保存、A302-6 收票指定应付、A801-3 收票单查询、A102-4 财务应付单查询(预检)、A801-1 查询全票池发票数据(镜像明细回填)
> 关联文档:`TMS业财融合-集成流程图文字版与业务流程梳理.md` 第 6 节(图05 定稿口径)、`2026-09-20-收票管理同步金蝶全票池发票(A801-1查询+批量同步+定时绑定关联)业务流程.md`(环节①镜像链路,本文档叠加其字段)、`2026-09-22-金蝶票池选票统一查镜像(收票登记+结算单认领+按票号补查+统一组件)业务流程.md`(选票统一查镜像)、`2026-09-21-应收结算与开票申请同步金蝶全链路业务流程….md`(销项侧对称链路)
> 最后更新:2026-09-22(按当前代码梳理;已编码完成,未联调)
---
## 1. 整体业务链路
图05「付款-收票查询及认领(进项收票)」分三个环节,环节①已建成,本文档覆盖环节②③:
```
环节①(已建成,不在本文):票池镜像拉取 + 收票登记选票 + 审批 + 批量同步/定时绑定
A801-1 拉全票池入镜像 blade_kingdee_invoice_pool(票种白名单 + 购销双税号 AND 白名单)
→ 收票登记查镜像选票(一票一登记)→ 审批通过 → 定时绑定回填 kingdeeInvoicePoolId/流水号
环节②:收票登记「推送金蝶收票单」(人工批量,POST /invoice-receipt/push-kingdee)
前置校验(审批通过 + 已绑定镜像 + 查验通过 + 恰好分摊一张应付结算单)
→ A102-4 预检关联财务应付单已在金蝶生成(缺则拦截"请先同步金蝶应付单")
→ A801-3 幂等预检(按发票号防重键,命中跳过保存续推指定)
→ A303-2 收票单批量保存 → 回写金蝶收票单号 + pushed
→ 立即 A302-6 收票指定应付(一票一应付)→ assigned(终态,锁单)
环节③:PaymentKingdeeResultTask 第④子流程 定时回写(A801-3,独立开关,默认每10分钟)
金蝶侧红冲/作废/部分红冲/全部红冲 → 回收为 failed 提示人工(不自动作废登记)
pushed(保存成功后中断、指定未完成)→ 指定补偿重发 A302-6
assigned 仅补登缺失的金蝶收票单号(保存成功但响应丢失的历史数据)
```
核心设计原则(与付款/应收链路一致):
1. **先内后外**:收票登记先审批通过(approved)并绑定镜像才允许推送。
2. **以金蝶侧实际生成为准**:每次推送前按防重键查询确认,已生成则幂等跳过保存、续推指定;推送接口真正报错才算失败。
3. **断点续传**:任一环节中断后重新触发(人工重推或定时补偿),从断点恢复,不产生重复单据。
4. **一票一应付**:金蝶当前不支持一张发票指定多张应付单分摊金额,收票登记分摊结算单数 ≠ 1 直接拦截(A302-6 按一对一指定)。
5. **异常回收人工处置**:金蝶侧删除/解除指定/红冲 → 本地置 failed 提示人工,须先在金蝶处置完毕再由人工重推。
---
## 2. 五个接口在链路中的角色
| 接口 | 名称 | 方向 | 触发环节 | 业务用途 |
| --- | --- | --- | --- | --- |
| A303-2 | 收票单批量保存 | TMS → 金蝶 | 环节② | 一张收票登记生成一张金蝶收票单(ap_invoice),票面明细逐行落分录 |
| A302-6 | 收票指定应付 | TMS → 金蝶 | 环节② / 环节③补偿 | 按「收票单防重键 ↔ 财务应付单防重键」绑定(op=assign),一票一应付 |
| A801-3 | 收票单查询 | TMS → 金蝶(查询) | 环节②前置 / 环节③ | 幂等预检 + 定时回写;契约无来源单号参数,按购方名称+修改时间窗口翻页、本地按防重键匹配 |
| A102-4 | 财务应付单查询 | TMS → 金蝶(查询) | 环节②前置 | 校验关联结算单的财务应付单已在金蝶生成(付款侧 A102-2 同步的产物),缺则拦截 |
| A801-1 | 查询全票池发票数据 | TMS → 金蝶(查询) | 环节②兜底 | 推送时镜像缺发票明细行/票种编码(早于字段上线或滑出增量窗口)按票号窄查回填 |
合同侧履约收票同步(C107-1)开发中、本期不接;进项发票附件下载(A801-2)备用。
---
## 3. 环节②:推送金蝶收票单编排
代码入口:`InvoiceReceiptController.pushKingdee` → `InvoiceReceiptServiceImpl.pushKingdeeReceivedInvoice(List<Long> ids)` → 逐单 `pushOneReceivedInvoice` → `KingdeeReceivedInvoiceSyncSupport`。
### 3.1 前置校验(预组装计划,逐单报因不拦截整批)
| # | 校验 | 拦截提示 |
| --- | --- | --- |
| 1 | 审批状态 = approved | 仅审批通过的收票登记允许推送金蝶 |
| 2 | 推送状态 ≠ assigned | 收票单已指定应付,无需重复推送 |
| 3 | 已绑定票池镜像(kingdeeInvoicePoolId 且镜像存在) | 收票登记未绑定票池镜像,请先完成同步绑定 |
| 4 | 镜像查验状态 checkStatus ≠ 2(金蝶每5分钟查验) | 金蝶发票查验不通过,请先人工核实票池发票 |
| 5 | 分摊结算单恰好 1 张(金蝶不支持多应付分摊) | 金蝶不支持一张发票指定多张应付单,收票登记须恰好分摊一张应付正式结算单 |
| 6 | 分摊结算单为审批通过、未作废的应付正式结算单,且有结算明细 | 结算单不是审批通过的应付正式结算单 / 无结算明细,无法组装应付单防重键 |
| 7 | A102-4 批量预检:财务应付单已生成(本批防重键一次查询) | 请先同步金蝶应付单:结算单 xxx 未生成财务应付单 |
预检拦截(1~7)**不改动推送状态**;仅保存/指定阶段真正失败才置 failed。
### 3.2 幂等预检与保存
1. **A801-3 幂等预检**:未终态计划按购方单位(接口必填)分组,回看窗口(`thirdParty.bfi.receivedInvoice.queryWindowDays`,默认 30 天,金蝶约束 ≤1 个月)`BfiUtil.queryAllPages` 翻页,返回行按 `awrpSrcSys + awrpSrcCode(=发票号码)` 本地匹配,同键多行取修改时间最新。
- 命中且发票状态正常 → 跳过 A303-2 保存,取金蝶单号**续推指定**(上次保存成功、指定中断的断点恢复);
- 命中但发票状态异常(作废2/红冲3/部分红冲7/全部红冲8)→ 置 failed 报人工,金蝶处置完毕重推;
- 未命中 → 执行保存。
2. **镜像明细兜底回填**:镜像 `items_json` 或 `invoice_type_code` 为空(入镜像早于字段上线/滑出 30 天增量窗口)→ 按票号窄查 A801-1(`fetchMirrorItemsFromPool`,镜像入池时已过白名单,回填不再复检)补写镜像后再组装。
3. **A303-2 保存**:`saveReceivedInvoice`(网关 body 为批量 data 数组,服务端当前单笔包装),成功取 `result[0].number` 即金蝶收票单号。
### 3.3 A303-2 报文取数
主表(联调口径:先只送 DTO 标注【必填】+ 票面核心字段,非必填 setter 注释保留):
| 字段 | 取数 |
| --- | --- |
| awrp_src_sys / awrp_src_code | `thirdParty.bfi.srcSys`(默认 WLTMS)/ **发票号码**(一票一登记天然唯一,重推同键幂等) |
| invoiceno | 发票号码(非必填但为收票单核心字段,必送) |
| receivedate / issuedate | 推送当日 / 镜像开票日期(yyyy-MM-dd HH:mm:ss) |
| biztype | `PUR`(采购,**待金蝶确认**,常量 `KingdeeBfiConstant.BFI_RECEIVED_BIZ_TYPE`) |
| invoicetypef7_number | 镜像 `invoice_type_code`(票易通原始编码 26/27,名称反查不可靠故新增此列) |
| paymenttype / asstacttype | `CREDIT` 赊购(不让金蝶自动推付款)/ `bd_supplier` 供应商 |
| org_number / currency_number | 分摊结算单关联合同的组织编码(部门编码)/ 结算币种(RMB→CNY),复用 `KingdeeApBillSyncSupport.bfiOrgNumber/bfiCurrency` |
| receivablessupp_number | 合同乙方(开票供应商)客商档案编码,复用 `bfiAsstactNumber`(含编码缓存) |
| exratetable_number / awrp_src_type | `01` / `TMS收票登记`(**待金蝶确认**) |
| buyername / asstactname | 镜像受票单位 / 开票单位 |
| isincludetax | true(录入价税合计) |
| 销/购方税号、地址、电话、开户行账号 | 非必传,setter 注释保留(被金蝶强校验时从镜像 `saler_tax_no/buyer_tax_no/bank_name/bank_account/phone` 补齐,参照 A303-1 联调经验) |
分录 entry(逐 A801-1 明细行展开,来源镜像 `items_json`):
| 字段 | 取数 |
| --- | --- |
| e_pricetaxtotal / e_amount / e_tax | 明细行 金额+税额 / 金额 / 税额(2 位小数) |
| quantity / price | 票面数量(≤0 兜底 1)/ 票面单价 |
| material_number | **联调临时值**,Nacos `thirdParty.bfi.receivedInvoice.materialNumber`(默认 SP0324,与应收/应付调试值同源,校准后移除) |
| e_taxclass_number | 票面明细税收分类编码 goodsCode(缺失报错拦截) |
| taxrateid_number | 票面税率(小数)换算金蝶税率编码:0.13→V13、0.01→V1、0→V0 |
| awrp_entry_code | **发票号码-行号**(候选键) |
### 3.4 A302-6 收票指定应付
保存成功先落 `pushed`(断点可恢复),随后立即执行指定:`invoiceAssign(op=assign, data=[{ap_invoice:[{发票号码,srcSys}], ap_finapbill:[{应付单防重键,srcSys}]}])`。财务应付单防重键与付款侧同源:**正式结算单号_结算明细单据号下划线拼接**(`KingdeeApBillSyncSupport.apBillSrcCode`)。成功后置 `assigned` 终态并记同步流水;失败置 failed + 原因,人工重推(重推凭 3.2 幂等预检跳过保存、仅补指定)。
---
## 4. 状态机与锁单
`blade_invoice_receipt.kingdee_push_status`(与票池绑定层 `kingdee_status` 语义分离,互不覆盖):
```
unsynced(默认)──保存成功──▶ pushed ──指定成功──▶ assigned(终态,锁作废)
│ │
└────保存/预检异常──────────┴──▶ failed ──人工处理后重推──▶(幂等续推)
▲
定时回写:金蝶红冲/作废/删除 ── pushed/assigned ──┘(异常回收)
```
| 状态 | 含义 | 允许操作 |
| --- | --- | --- |
| unsynced | 未推送 | 编辑(草稿态本不允许推送)、推送 |
| pushed | 金蝶收票单已保存、指定未完成/未确认(瞬态,正常链路即被指定接管) | 推送(幂等续推)、定时指定补偿 |
| assigned | 已指定应付,终态;金蝶侧生成收票单并锁定 | 仅定时回写轮询;**禁止作废**(锁单),金蝶异常回收获占 |
| failed | 保存/指定失败或金蝶侧异常回收(红冲/作废/状态异常) | 人工在金蝶删除/解除指定后重推(同防重键幂等) |
锁单防护:`voidBill` 在 pushed/assigned 时拦截「金蝶已生成收票单[xxx],不允许作废;请先在金蝶删除收票单/解除指定」;编辑/删除仅草稿、驳回态(既有约束,推送不受影响)。金蝶侧删除/解除指定**不自动执行**(unassign 不启用),一律人工处置后重推。
---
## 5. 环节③:A801-3 定时回写
代码入口:`PaymentKingdeeResultTask.syncKingdeeReceivedInvoiceResult`(独立开关 `thirdParty.bfi.receivedInvoiceResult.syncEnabled` 默认 false、`cron` 默认每10分钟)→ `InvoiceReceiptServiceImpl.syncKingdeeReceivedInvoiceResult`;联调可手动 `POST /invoice-receipt/sync-kingdee-result`。
轮询对象:pushed/assigned 且非作废、已绑定镜像的收票登记;按镜像购方名称分组、回看窗口(`thirdParty.bfi.receivedInvoice.resultWindowDays`,默认 30 天)批量翻页匹配防重键。
| 场景 | 处置 |
| --- | --- |
| 金蝶发票状态异常(作废/红冲/部分红冲/全部红冲) | → failed + 原因「已回收推送状态,请在金蝶删除/解除指定后重推」,不自动作废登记 |
| pushed 且金蝶侧收票单正常 | 指定补偿:批量预组装应付单防重键后重发 A302-6 → assigned;分摊关系不满足一票一应付的置 failed 报人工 |
| assigned 且本地缺金蝶收票单号 | 单号补登(保存成功但响应丢失的历史数据),不写流水 |
| 窗口内查不到 | **不改状态**(金蝶修改时间延迟/滑窗不误判删除),由人工重推兜底 |
---
## 6. 防重键与幂等体系汇总
| 单据/对象 | 防重键/幂等依据 | 使用接口 |
| --- | --- | --- |
| 金蝶收票单 | awrp_src_sys=WLTMS + awrp_src_code=**发票号码** | A303-2(保存候选键)/ A801-3(预检+回写本地匹配)/ A302-6(ap_invoice 引用) |
| 收票单分录 | awrp_entry_code=发票号码-行号 | A303-2 |
| 财务应付单(被指定方) | awrp_src_code=正式结算单号_结算明细单据号拼接(与 A102-2 同源) | A102-4 预检 / A302-6(ap_finapbill 引用) |
| 票池镜像 | 唯一索引(tenant_id, invoice_no),票面指纹含新增 items_json/invoice_type_code | A801-1 upsert |
| 收票登记 | 发票号码同租户唯一(一票一登记) | 登记层保证 |
断点恢复:保存成功响应丢失 → 重推 A801-3 命中跳过保存;指定失败 → failed 重推仅补指定(幂等预检在位);pushed 崩溃残留 → 定时指定补偿。
---
## 7. 数据前提与上线操作
| # | 事项 | 说明 |
| --- | --- | --- |
| 1 | 变更 SQL | `doc/sql/transport/blade_invoice_receipt_kingdee_push_20260922.sql`(幂等可重复执行):镜像表 `items_json`/`invoice_type_code`,收票登记表 `kingdee_received_invoice_no`/`kingdee_push_status`/`kingdee_push_msg`/`kingdee_push_time`。**执行后方可启动服务** |
| 2 | Nacos 配置 | 任务开关 `thirdParty.bfi.receivedInvoiceResult.syncEnabled/cron`;窗口 `thirdParty.bfi.receivedInvoice.queryWindowDays/resultWindowDays`(默认 30);联调临时值 `thirdParty.bfi.receivedInvoice.materialNumber`(默认 SP0324)。均有代码默认值,不配置也能跑 |
| 3 | 存量镜像补齐 | items/票种编码随 30 天增量窗口逐轮自动补写(指纹变更触发重写);窗口外老票由推送时 A801-1 窄查回填兜底 |
| 4 | 数据前提 | 收票登记须新链路登记(票在镜像、绑定收敛);**本地 21 条造数收票登记镜像悬空推不动,联调须新登记票**;供应商须建档并维护统一社会信用代码 + 客商编码;合同须维护组织编码/结算币种/乙方;结算单先完成付款侧 A102-2 应付单同步 |
| 5 | 联调校准项 | `biztype=PUR`、`awrp_src_type=TMS收票登记`、`materialNumber=SP0324`、金蝶税率编码 V13 命名、分录必填范围(金蝶强校验常多于 DTO 标注,参照 A303-1 经验逐步放开注释字段)、A302-6 结果结构与 `requireBfiSaveSuccess` 兼容性 |
| 6 | 联调顺序 | 执行 SQL → 全票池同步(补 items)→ 新收票登记选票→审批→批量同步绑定 → 付款侧结算单同步生成应付单 → `/push-kingdee` 推送 → 金蝶查收票单与指定关系 → 手动 `/sync-kingdee-result` 验证回写 |
---
## 8. 关键代码索引
| 环节 | 代码位置 |
| --- | --- |
| 推送端点 | `blade-service/blade-transport controller/InvoiceReceiptController.pushKingdee / syncKingdeeResult`(`POST /invoice-receipt/push-kingdee`、`POST /invoice-receipt/sync-kingdee-result`,菜单权限 invoice_receipt) |
| 推送编排/回写 | `service/impl/InvoiceReceiptServiceImpl.pushKingdeeReceivedInvoice / pushOneReceivedInvoice / buildReceivedInvoicePushPlan / syncKingdeeReceivedInvoiceResult / applyReceivedInvoicePushResult` |
| 报文组装 | `support/KingdeeReceivedInvoiceSyncSupport`(pushReceivedInvoice / assignApBill / queryReceivedInvoiceBySrcCodes / parseItems),应付侧复用 `support/KingdeeApBillSyncSupport`(apBillSrcCode / queryApBillExistNos / requireContract / bfiOrgNumber / bfiCurrency / bfiAsstactNumber) |
| 镜像明细落库 | `InvoiceReceiptServiceImpl.fillInvoicePool / buildItemsJson`(items_json、invoice_type_code 随收票登记保存时 `persistMirrorFromRemote` 单票 upsert 落库,无指纹比对表——票池改回实时查询后指纹机制已退役) |
| 定时任务 | `task/PaymentKingdeeResultTask.syncKingdeeReceivedInvoiceResult`(第④子流程独立调度) |
| 接口契约 | `blade-third-party-api/blade-bfi-api`:`BfiReceivedInvoiceSaveDTO/EntryDTO`(A303-2)、`BfiInvoiceAssignItemDTO/RefDTO`(A302-6)、`BfiReceivedInvoiceQueryDTO/VO/EntryVO`(A801-3)、`IBfiService.saveReceivedInvoice/invoiceAssign/queryReceivedInvoice` |
| 常量/配置 | `constant/KingdeeBfiConstant`(RECEIVED_PUSH_* 状态机、BFI_RECEIVED_BIZ_TYPE、BFI_RECEIVED_INVOICE_SRC_TYPE)、`config/BfiProperties.ReceivedInvoice` |
| 展示 | `InvoiceReceiptVO.kingdeePushStatusName` + `InvoiceReceiptWrapper` 翻译;列表查询支持 kingdeePushStatus 过滤 |
| 前端 | 本期未接按钮,收票管理页面「批量推送金蝶」与推送状态列后续迭代接入 |
@@ -0,0 +1,85 @@
# 金蝶票池选票实时查询(镜像降级为登记存根)业务流程
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport;前端:tms-erp-web)
> 对接系统:金蝶发票云旗舰版·票易通(piaozone)全票池 A801-1,经业财融合 BFI 网关(rim 契约)透传
> 关联文档:`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 附件下载镜像优先/回源链路。下文其余叙述作历史口径。
---
## 1. 背景与决策
2026-09-22 上午曾定稿"选票统一查本地镜像 + 全量同步维护镜像"方案。同日二次决策**推翻该方案**:同步体系(定时任务、增量窗口、批量 upsert、定时绑定、漏绑补偿、票种/客商税号双白名单)维护过重,且选票以金蝶侧实时状态为准更可靠。
**定稿口径**:
- 收票登记新增、正式结算单认领两处选票改为**实时查 A801-1 全票池**;A801-1 无总数,翻页采用"多取 1 条探测 hasMore + 有界循环补页 + 扫描游标"的**游标式下一页翻页**(无跳页能力,机制详见 §2)。
- (2026-09-23)选票**只出可用票**:金蝶返回行须同时满足 剩余可用金额 `availableAmount > 0` 且 金额使用状态 `amountUseStatus = "0"`(未使用,2-全部使用)。A801-1 无该维度的服务端过滤条件,只能拉取后客户端过滤,由此引入补页与游标(§2)。
- (2026-09-23)**单据使用状态**不再作为前端筛选条件,后端固定下推"未用"(`expenseStatus = "1"`)。
- `blade_kingdee_invoice_pool` 语义降级为**登记存根**:只存被业务选中的发票,写入时机=**收票登记保存时服务端重查金蝶落库**(否决"选中即写")。
- 全量同步体系**退役删除**:定时任务、增量窗口、镜像批量 upsert、定时绑定(receiptMap)、漏绑补偿、白名单过滤(票种 + 客商税号 AND,客商建档不再是选票前置)、手动全量同步端点与"立即同步"按钮、按票号补查(语义并入主查询)。
- 金蝶不可用时查询与保存重查均**直接报错**(镜像不再提供读服务,无降级路径——接受"每次开弹窗/每次保存打一次金蝶"的一次性成本,换取同步体系归零)。
## 2. 接口契约(后端)
两个门面端点分挂各自菜单(类级 `@PreAuth` 按菜单隔离),同一服务方法落点;**路径/方法不变、语义换芯**:
| 端点 | 说明 |
| --- | --- |
| `GET /invoice-receipt/invoice-pool`、`GET /formal-settlement/invoice-pool` | 实时统一查询,收参 `KingdeeInvoicePoolQueryDTO`,返回 `R<KingdeeInvoicePoolPageVO>`(游标分页) |
- 已删除端点:`POST /*/invoice-pool/fetch`(按票号补查,语义并入主查询的发票号码条件)、`POST /invoice-receipt/sync-invoice-pool`(手动全量同步)。
- **保留端点**:`POST /invoice-receipt/sync-kingdee`(列表"批量同步",职责收敛为刷新**已登记票**的镜像行与状态,镜像行此时必已存在)。
**查询条件**(`KingdeeInvoicePoolQueryDTO`):`current/size`(size 钳位 1~100)、`invoiceNo`、`invoiceDateStart/End`(服务端强校验 yyyy-MM-dd)、`salerTaxNo/buyerTaxNo`、`invoiceType`(票易通编码,如 26/27,选项取 `BfiConstant.INVOICE_TYPE_NAMES`)、`cursor`(2026-09-23 新增,下一页续扫游标)。`expenseStatus` 已从前端条件删除,后端固定下推"未用"(`KingdeeBfiConstant.POOL_EXPENSE_STATUS_UNUSED`)。**删除**了镜像时代的 `salerName/buyerName/claimed/registered` 条件(远程不支持名称模糊;本地状态不可下推)。
**A801-1 统一查询口径**:所有出站查询一律携带 `invoiceStatus = "0"`(正常票)与 `expenseStatus = "1"`(单据使用状态未用,后端固定;按票号单查 `queryPoolByInvoiceNo` 不下推 expenseStatus);返回行先做**选票可用过滤**(`isUsablePoolRow`:票号非空 + 剩余可用金额 `availableAmount > 0` + 金额使用状态 `amountUseStatus = "0"`,可用金额空白/解析失败按不可用兜底;口径常量集中在 `KingdeeBfiConstant`),再按 `invoiceNo` trim 全等防御过滤(防接口模糊匹配语义假命中),最后截断 size 条。
**有界循环补页分页(2026-09-23)**:可用过滤发生在拉取之后,单页远程返回可能大部分被过滤,因此 `invoicePoolPage` 在单请求内循环补页凑满一页——首拉 `size+1` 轻量探测,不足再按契约上限 500 行/次补拉,单请求最多 3 次远程调用(`POOL_MAX_REMOTE_CALLS`,过滤率过低时以此封顶防请求无限拉长),仍不足则返回稀疏页。远程短页(返回行数 < 请求数)即耗尽信号;`hasMore = 未耗尽 或 窗口内还有剩余过滤行`。**翻下一页必须回传上页 `nextCursor`**(格式"远程页码:已消费过滤行数",游标页码按 500 行窗口定位),否则按页码锚定会漏行/重行;游标解析失败自动降级为页码锚定,不阻断查询。
**返回体**(`KingdeeInvoicePoolPageVO`):`{ records: List<KingdeeInvoicePoolVO>, current, size, hasMore, nextCursor }`——**不再合成 total**;`nextCursor` 为下一页续扫游标,null 表示没有更多。行映射:票种编码→中文名、税率小数转百分数(顶层优先、回退明细首行)、`invoiceAmount = availableInvoiceAmount = matchedAmount = totalAmount`、`availableTaxAmount = availableAmount`(2026-09-23 新增"剩余可用金额"展示列,金蝶可用税额口径,**不改** `availableInvoiceAmount` 的价税合计口径——认领金额校验依赖该口径)、`attachmentJson = {source:'piaozone', path:downloadUrl, name:票号.pdf}`;`id` 置空(未入镜像)。`BfiInvoicePoolVO` 同步新增 `amountUseStatus` 字段承接金蝶返回。
**claimed/registered 纯展示装饰**:对本页票号批量查 `blade_formal_settlement_invoice`(关联主单未删且非 voided)与 `blade_invoice_receipt`(未删且非 voided),内存打标;只读展示,**不落库、不进查询条件**。
## 3. 保存时落库(收票登记,`persistMirrorFromRemote`)
`InvoiceReceiptSaveRequest` 已删除 `kingdeeInvoicePoolId` 字段(前端不再传)。save/submit 链路新增"镜像绑定"步骤:
```
① 按 invoiceNo 调 queryPoolByInvoiceNo 远程 A801-1 精确单查(含票号全等防御)
② 未命中或 delete="1" → ServiceException("金蝶票池未查询到发票 X,不能保存"),保存中止
(选中→保存竞态:票被删/红冲在此拦截;不主动作废既有登记,与列表同步口径一致)
③ 命中后按选票同口径 isUsablePoolRow 复核可用性(2026-09-23 新增):
剩余可用金额为 0 或金额使用状态已变更 → ServiceException("发票 X 剩余可用金额为0或金额使用状态已变更,不能保存")
(防选票到保存之间票被他人认领/使用造成超用;明细补查 fetchMirrorItemsFromPool 不做该过滤)
④ fillInvoicePool 构建/刷新镜像行(保留图05 的 invoiceTypeCode/itemsJson 映射)
⑤ 保存/更新镜像行 → receipt.kingdeeInvoicePoolId = mirror.id,
回填 kingdeeBillNo = serialNo、kingdeeStatus
```
镜像按 `(tenant_id, invoice_no)` 唯一(既有 uk),多单登记同票共享同一镜像行;单票 upsert,**无指纹表、无批量写**。编辑态表单"同步"按钮(`syncReferenceData`)为**纯预览**——调查询接口按票号取单行(records[0])回填表单,不写镜像;镜像刷新统一收敛到保存动作。
## 4. 前端统一组件(`src/components/kingdee-invoice-pool-dialog/main.vue`)
- Props:`modelValue / title / confirmText / multiple / initialInvoiceNo / queryApi`(`fetchApi/syncApi` 已删除);事件 `confirm(rows)`(单选 `[row]`、多选选中数组)。打开时重置查询、按 `initialInvoiceNo` 预填、清勾选、回第 1 页加载。
- 查询表单五条件(2026-09-23 删除"单据使用状态",后端固定查未用):发票号码、开票日期区间、销方税号、购方税号、发票类型(编码下拉);查询/重置按钮在加载期间禁用。
- 表格:票面列 + **剩余可用金额**列(2026-09-23 新增,`availableTaxAmount`)+ **认领状态/登记状态**两个 tag(仅展示);"金蝶同步"列已删除(远程票恒在池中无意义);行主键用发票号码兜底(`rowKey = invoiceNo || id`,远程票 id 恒空);单选 radio/双击直选,多选 `reserve-selection` 跨页保留。
- 分页区:**游标式**——"上一页/下一页" + "第 N 页" + 每页条数(10/20/50/100);**下一页请求回传上页 `nextCursor` 续扫**(新查询/换页大小/重新打开自动重置游标与页缓存);**上一页直接回显已浏览页缓存(`pageCache`),不再发请求**;`hasMore=false` 禁用下一页,第 1 页禁用上一页,加载中全部禁用;查询条件变更或换页大小即回第 1 页;翻页请求失败时页码回滚到发起前值(不停错页)。
- 接线:收票登记表单(`multiple=false`,回填不再取 `id`,金蝶单据号取 `serialNo`、状态固定显示"未同步",保存落库后由后端刷新);结算单编辑器(`multiple=true`、`confirm-text="确认认领"`,`handleInvoiceClaimConfirm` 把复数 `attachmentsJson` 归一为认领契约单数 `attachmentJson`)。
- 翻页期间票池数据变动:游标页可能重复/漏行(组件按发票号码做行主键与选择去重,结算认领确认前还有票号集合查重);最终一致性以保存/提交时服务端重查为准,无资金风险。
## 5. 数据清理
`doc/sql/transport/blade_kingdee_invoice_pool_prune_20260922.sql`(幂等可重跑):删除 `blade_kingdee_invoice_pool` 中未被任何未删 `blade_invoice_receipt.kingdee_invoice_pool_id` 引用的历史全量同步镜像行。**执行前必须先跑脚本内盘点 SELECT 并向用户报数确认**(dev 库清理口径:事务 + 回滚 SQL + mysqldump 文本兜底)。
## 6. 已知限制与验证
- 每次打开弹窗(最多 3 次补页调用)、每次保存都是金蝶远程调用(预期成本);金蝶不可用期间选票/收票保存均不可用。
- 单据使用状态固定后端下推"未用"(前端无此条件);选票可用过滤(`availableAmount > 0` 且 `amountUseStatus = "0"`)在拉取后进行,过滤率过低时单请求最多补页 3 次即返回稀疏页,靠 `hasMore + nextCursor` 继续翻页。
- 下一页依赖前端回传 `nextCursor`;上一页走前端缓存回显(0 请求),缓存页为浏览时快照,票池实时变动最终以保存/提交时服务端重查为准。
- 无表结构、无菜单 SQL 变更;Nacos 三份模板中 `thirdParty.bfi.invoiceReceipt.*` 同步相关键已随任务删除(图05 `sync-kingdee`/推送链使用的键保留)。
- 后端验证:`mvn clean compile -DskipTests -pl blade-service/blade-transport -am` + `mvn test-compile` 通过;前端 `pnpm run build` 通过;退役符号两仓库 grep 归零。
- 手工回归清单与过程记录:`.superpowers/sdd/2026-09-22-invoice-pool-remote-reversal/`(各任务简报/报告/评审包)。
@@ -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 取回回传 |
| 附件同步聚合态 | 无附件/未上传/部分未传/已上传(列表展示用,运行时聚合) |
| 重推补传 | 主单重复推送(含幂等跳过)时自动补传未成功附件,无新增交互 |
@@ -0,0 +1,222 @@
# 收款认领金蝶全链路业务流程(A301-2 认领公告同步 → A301-3 认领结果推送 → A301-1 收款单查询回写)
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)
> 对接系统:金蝶云·星瀚(业财融合 BFI,经 iPaaS 网关 `/gwzh/BFI` 直连,不经过合同合规系统)
> 覆盖接口:A301-2 收款认领公告查询、A301-3 收款认领结果推送、A301-1 收款单查询(收款处理查询)
> 关联文档:`TMS业财融合-集成流程图文字版与业务流程梳理.md` 第 7 节(图06 回款认领)、`2026-09-17-BFI分页查询通用翻页工具(BfiUtil-queryAllPages)技术文档.md`、`2026-09-23-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总).md`
> 最后更新:2026-09-24(按当前代码梳理;接口契约在 BFI 客户端已编码,本期完成业务侧接线与联调准备)
---
## 1. 整体业务链路
图06「收款查询及认领(回款认领)」采用**业务先行认领(场景二)**:金蝶司库收到银行流水并完成**第一轮自动认领(到客户维度)**,TMS 认领到合同/结算单维度后回推金蝶生成收款单。分三段:
```
阶段一:认领公告同步(TMS 手动触发,POST /receipt-flow/sync)
A301-2 收款认领公告查询(claimusers=当前登录用户真实姓名)
→ BfiUtil.queryAllPages 全量翻页 → 按 detail_serial_no 幂等 upsert 收款流水表 blade_kingdee_receipt_flow
→ 前端"手动同步流水"按钮直接调用(不再传本地 mock 行)
阶段二:二次认领(页面认领,POST /receipt-flow/claim)
用户在收款流水详情/批量认领页选择审批通过的应收正式结算单并按比例分摊
→ 本地生成认领主单 blade_receipt_claim + 分摊表 blade_receipt_claim_settlement + 流水金额状态更新
→ A301-3 收款认领结果推送(billid=公告id、分录=结算单分摊行)
→ 推送成功回写金蝶收款单号 + kingdee_bill_status=pushed;失败则抛异常整体回滚
阶段三:收款单查询回写(手动触发,POST /receipt-claim-record/query-kingdee-result)
A301-1 收款单查询(近 1 个月窗口全量翻页)
→ 按 sourcebillnumber=awrp_src_code 本地匹配已推送认领
→ 回写金蝶收款单号与单据状态(A暂存/B已提交/C已审核/D已收款)
```
核心设计原则(与付款/应收/收票链路一致):
1. **公告为准**:收款流水统一由 A301-2 认领公告落库,前端不再自造流水数据;按 `detail_serial_no` 幂等 upsert。
2. **推送先于结果**:认领保存与 A301-3 推送在同一事务内,推送失败整体回滚,避免金蝶与本地认领不一致。
3. **认领公告 id 必填**:A301-3 按 `billid` 定位公告,同步阶段必须落库公告 id(`bill_id`),缺失拦截。
4. **以金蝶侧实际生成为准**:A301-3 推送成功回写金蝶收款单号;A301-1 回写仅推进 `kingdee_bill_status`,不改本地认领金额。
5. **异常回收人工处置**:本期认领作废不推金蝶(本地生成 KDCX 冲单号),金蝶侧退票/红冲由人工处置后重推,后续迭代接入。
---
## 2. 三个接口在链路中的角色
| 接口 | 名称 | 方向 | 触发环节 | 业务用途 |
| --- | --- | --- | --- | --- |
| A301-2 | 收款认领公告查询 | TMS → 金蝶(查询) | 阶段一(手动同步) | 按参与认领用户拉取金蝶认领公告,落库存收款流水表供页面认领 |
| A301-3 | 收款认领结果推送 | TMS → 金蝶 | 阶段二(页面认领) | 将二次认领结果(认领至结算单,即应收单)推给金蝶生成收款单 |
| A301-1 | 收款单查询 | TMS → 金蝶(查询) | 阶段三(手动回写) | 查询金蝶收款单(收款处理)状态,按来源单据编码回写认领结果 |
查询类接口时间窗约束:A301-1 **业务日期范围 ≤ 1 个月**;A301-2 无时间窗参数,仅按用户与分页。
---
## 3. 阶段一:A301-2 认领公告同步
代码入口:`ReceiptFlowController.sync`(`POST /receipt-flow/sync`,菜单权限 `receipt_flow`,按钮权限 `receipt_flow_sync`)→ `ReceiptFlowServiceImpl.sync()`。
### 3.1 编排
1. **确定参与认领用户**:`UserCache.getUserRealName(AuthUtil.getUserId())`,为空拦截「当前用户无姓名,无法同步认领公告」。
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=当前用户)。
### 3.2 A301-2 返回 → 收款流水表字段映射
`blade_kingdee_receipt_flow`(新增列见 §7 SQL):
| A301-2 返回(BfiClaimAnnounceVO) | 收款流水表字段 | 说明 |
| --- | --- | --- |
| id | `bill_id` | **认领公告 id,A301-3 推送必填**,本期新增 |
| billno | `receipt_notice_no` | 认领通知单 |
| recviewpayer | `payer_name` | 付款人展示名 |
| reamount | `receipt_amount` | 收款金额 |
| oppunit | `counterparty_name` | 对方户名 |
| oppbanknumber | `counterparty_account` | 对方账号 |
| oppbank | `counterparty_bank` | 对方开户行 |
| description | `summary` | 摘要 |
| tradetime | `transaction_time` | 交易时间 |
| detailid(缺省回退 tradedetailno/bankdetailno) | `detail_serial_no` | 明细流水号,**幂等键(tenant_id, detail_serial_no 唯一索引)** |
| unclaimamount | `unclaimed_amount` | 未认领金额,本期新增 |
| fee | `fee` | 手续费,本期新增 |
| recpayee | `payee_name` | 收款人,本期新增 |
| claimstatus | `kingdee_claim_status` | 公告认领状态(0未认领/1已认领),本期新增 |
| claimme | `claim_me` | 我已认领,本期新增 |
| claimedamount | —(不落库) | 本地 `claimed_amount` 只记录 TMS 认领金额,与公告已认领金额语义区分 |
> **口径说明**:`claimed_amount`/`claim_status` 为**本地认领金额/认领状态**(由 claim/作废驱动),与金蝶公告的 `claimstatus`/`claimedamount` 互不覆盖;公告侧状态单独存 `kingdee_claim_status` / `unclaimed_amount`。
### 3.3 前端
`receipt-flow.vue`「手动同步流水」按钮不再生成 mock 行,直接 `POST /receipt-flow/sync`(无请求体),后端返回同步条数后刷新列表。
---
## 4. 阶段二:A301-3 认领结果推送
代码入口:`ReceiptFlowController.claim`(`POST /receipt-flow/claim`)→ `ReceiptFlowServiceImpl.claim(ReceiptClaimRequest)`。
### 4.1 编排
1. **本地校验**(沿用现有逻辑):流水锁行、结算单锁行、分摊金额上限校验(累计认领不超结算金额、不超流水剩余可认领金额)、关联结算单同一项目/组织/收付款方。
2. **本地落库**:插入 `blade_receipt_claim`(claim_status=claimed、kingdee_bill_status=none、awrp_src_code=生成的来源单据编码)+ 分摊表 `blade_receipt_claim_settlement` + 刷新结算单收款汇总 + 更新流水的 `claimed_amount/claim_status` + 认领留痕。
3. **生成来源单据编码**:`awrp_src_code = SKCL + yyyyMMddHHmmss + 认领id尾号6位`(参照冲单号生成,同文件 `buildKingdeeBillNo` 风格),落库 `blade_receipt_claim.awrp_src_code`(本期新增),作为 A301-1 回写匹配键。
4. **A301-3 推送**:`bfiService.pushClaimResult(dto)`(网关 body 单笔包装)。推送成功(`BfiBillSaveResultVO` 全项成功)→ 回写 `kingdee_bill_no`(`result[0].number` 金蝶收款单号)+ `kingdee_bill_status=pushed`;失败 → 抛 `ServiceException`(汇总行错误文本)**整体回滚**。
> **幂等说明**:`awrp_src_sys + awrp_src_code` 为金蝶候选键;推送成功但本地回滚(网络/DB 抖动)后重推会**重新生成 awrp_src_code** 产生新单,联调期以推送失败回滚为主路径规避;后续如遇需要可改为推送前置生码 + 成功补偿落库。
### 4.2 A301-3 报文取数
| DTO 字段 | 取数 | 说明 |
| --- | --- | --- |
| billid | 流水 `bill_id` | **必填**,缺失拦截「认领公告id缺失,请先同步金蝶收款流水」 |
| awrp_src_sys | `thirdParty.bfi.srcSys`(默认 WLTMS) | 与付款/收票链路同源 |
| awrp_src_code | `SKCL+时间戳+认领id尾号` | 见 4.1 |
| recpaytype_number | `KingdeeBfiConstant.BFI_CLAIM_REC_PAY_TYPE`(**待确认**,示例 101) | 收款类型编码 |
| paymenttype | `KingdeeBfiConstant.BFI_CLAIM_PAYMENT_TYPE`(**待确认**,默认 bd_customer) | 付款人类型 |
| recviewpayer | 流水 `payer_name` | 付款人名称 |
| recpayer | 流水 `payer_id`(同步时暂不落库) | **待确认**:金蝶付款人 master id,DTO 标记必填;联调若金蝶强校验则需按付款人名称反查 A902-1 客户/供应商档案 id 补齐 |
| entryentity[] | 各结算单分摊行 | 见 4.3 |
### 4.3 A301-3 分录取数
| entryentity 字段 | 取数 |
| --- | --- |
| e_receivableamt | 分摊金额 `allocated_receipt_amount` |
| e_corebilltype | `ar_finarbill`(财务应收单,复用 `KingdeeBfiConstant.BFI_AR_BILL_SRC_BT`),有金蝶应收单号时携带 |
| e_corebillno | 分摊结算单 `formal_settlement.kingdee_bill_no`(A302-2 同步回写,缺失时不传,`@JsonInclude(NON_NULL)` 省略) |
| seq | 分摊行序号(1 起) |
| e_receivingtype_number | 同 `recpaytype_number`(**待确认**,示例仅含 e_receivableamt,其余非必填按需放开) |
### 4.4 金额流转
认领分摊后收款流水本地状态:
```
claim_amount(认领主单) = Σ 分摊金额
flow.claimed_amount = Σ 该流水所有有效认领分摊(local)
flow.claim_status = 0 → unclaimed;<receipt_amount → partial;≥ → claimed
结算单 claimed/remaining = refreshPaymentSummary(沿用现有)
```
**重要**:A301-3 推送的是认领结果(资金去向),**不修改**流水的收款金额本身;`unclaimed_amount` 为金蝶公告侧口径,仅展示、不参与本地计算。
---
## 5. 阶段三:A301-1 收款单查询回写
代码入口:`ReceiptClaimRecordController.queryKingdeeResult`(`POST /receipt-claim-record/query-kingdee-result`,菜单权限 `receipt_claim_record`)→ `ReceiptClaimRecordServiceImpl.refreshKingdeeResult()`。
### 5.1 编排
1. **轮询对象**:`claim_status=claimed` 且 `awrp_src_code` 非空且 `kingdee_bill_status IN (pushed, approved)` 的认领记录。
2. **A301-1 查询**:业务日期默认**今天回溯 30 天**(≤1 个月),`BfiUtil.queryAllPages` 全量翻页(pageSize 200)。
3. **本地匹配**:金蝶收款单 `sourcebillnumber == 认领.awrp_src_code` → 命中。
4. **回写**:`kingdee_bill_no = 收款单 billno`(第一条命中,随后仅推进状态);`kingdee_bill_status` 按单据状态映射:
| 金蝶 billstatus | 本地 kingdee_bill_status | 展示 |
| --- | --- | --- |
| A 暂存 / B 已提交 | pushed | 已推送 |
| C 已审核 | approved | 审核通过 |
| D 已收款 | received | 已收款 |
| 其他/G 退单 | failed | 处理失败 |
> 定时回写开关(复用 `PaymentKingdeeResultTask` 编排风格)本期预留,未接入定时调度;后续按认证领记录量接入独立子流程(开关 `thirdParty.bfi.receiptClaimResult.*`)。
---
## 6. 状态机与防重体系
### 6.1 收款流水本地认领状态
```
unclaimed ──认领分摊──▶ partial/claimed ──(全部作废)──▶ unclaimed
▲ │
└────同步公告幂等 upsert 不覆盖(公告已认领金额不落 claimed_amount)
```
### 6.2 认领记录金蝶状态
```
none(认领落库默认)──A301-3推送成功──▶ pushed ──A301-1回写D──▶ received(终态)
│ │ └──C──▶ approved──┐
└────推送失败(抛异常回滚,无残留) └──A/B/garker──▶ failed ──┘
作废(本期不推金蝶):→ voided + KDCX 冲单号(本地)
```
### 6.3 防重键与幂等汇总
| 对象 | 防重键/幂等依据 | 使用接口 |
| --- | --- | --- |
| 收款流水 | 唯一索引(tenant_id, detail_serial_no) | A301-2 sync upsert |
| 金蝶收款单 | awrp_src_sys + awrp_src_code | A301-3(候选键)/ A301-1(回写按 sourcebillnumber 匹配) |
| 认领记录 | 本地主键 + awrp_src_code(回写匹配键) | claim / A301-1 |
---
## 7. 数据前提与上线操作
| # | 事项 | 说明 |
| --- | --- | --- |
| 1 | 变更 SQL | `doc/sql/transport/blade_receipt_flow_claim_20260924.sql`(幂等可重复执行):收款流水表新增 `bill_id/payer_id/kingdee_claim_status/claim_me/unclaimed_amount/fee/payee_name`,认领主单新增 `awrp_src_code`。**执行后方可启动服务** |
| 2 | Nacos 配置 | 无新增必配项;A301-1 回写窗口(31 天)本周期内为代码常量,后续抽配置 |
| 3 | claimusers 前提 | 金蝶认领公告的 `claimusers` 字段须与 TMS 用户真实姓名匹配,否则手动同步查不到公告;联调时需金蝶侧在公告维护认领人 |
| 4 | A301-3 待确认字段 | `recpaytype_number`(示例 101)、`paymenttype`(默认 bd_customer)、`recpayer`(金蝶付款人 master id 来源)、分录 `e_corebilltype/e_corebillno/e_receivingtype_number` 是否必传——以金蝶接口文档为准,常量收敛于 `KingdeeBfiConstant.BFI_CLAIM_*` |
| 5 | 结算单前提 | 认领分摊的应收正式结算单须审批通过、未作废,且已完成付款侧/应收侧金蝶同步(`formal_settlement.kingdee_bill_no` 有值)以便分录携带财务应收单号 |
| 6 | 联调顺序 | 执行 SQL → 金蝶维护认领公告及认领人 → 页面「手动同步流水」→ 收款流水列表/详情认领(选择结算单分摊)→ 推送若失败按金蝶报错调整常量 → 认领记录页「查询金蝶认领结果」验证回写 |
---
## 8. 关键代码索引
| 环节 | 代码位置 |
| --- | --- |
| 认领公告同步 | `controller/ReceiptFlowController.sync`(`POST /receipt-flow/sync`)→ `service/impl/ReceiptFlowServiceImpl.sync()` |
| 认领推送 | `controller/ReceiptFlowController.claim`(`POST /receipt-flow/claim`)→ `service/impl/ReceiptFlowServiceImpl.claim()/pushClaimResultByBfi()` |
| 收款单回写 | `controller/ReceiptClaimRecordController.queryKingdeeResult`(`POST /receipt-claim-record/query-kingdee-result`)→ `service/impl/ReceiptClaimRecordServiceImpl.refreshKingdeeResult()` |
| BFI 客户端契约 | `blade-third-party-api/blade-bfi-api`:`IBfiService.queryClaimAnnounce/pushClaimResult/queryRecBill`、`BfiClaimAnnounceVO`、`BfiClaimPushDTO/EntryDTO`、`BfiRecBillVO`、`BfiClaimAnnounceQueryDTO/BfiRecBillQueryDTO` |
| 常量/配置 | `constant/KingdeeBfiConstant`(BFI_CLAIM_REC_PAY_TYPE、BFI_CLAIM_PAYMENT_TYPE、KINGDEE_STATUS_PUSHED/RECEIVED)、`config/BfiProperties`(srcSys) |
| 展示 | `ReceiptFlowVO.kingdeeClaimStatusName` + `ReceiptFlowWrapper` 翻译;`ReceiptClaimRecordVO.kingdeeBillStatusName`(含 pushed/received) |
| 前端 | `tms-erp-web:views/payment/receipt-flow.vue`(手动同步去 mock)、`views/payment/receipt-claim-record.vue`(查询金蝶认领结果按钮)、`api/payment/receiptFlow.js、receiptClaimRecord.js` |
@@ -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,203 @@
# 金蝶单据锁定与解锁业务流程(lockBusiData 锁定 - unLockNotifyBusiData 回调 - 失败补偿 - 删单还原)
- 日期:2026-10-09(2026-10-10 变更:① lockBusiData 确认为 1对1 接口,不支持逗号拼接批量,锁定/补偿改为逐张调用;② 付款申请单纳入锁单范围并完成接线。
2026-10-15 联调修正:③ 回调方向与锁定相反,解锁定位改为「金蝶单号+我方单号」双条件反查(targetBillType 回调为空不参与匹配);④ 回调处理批量化去 N+1;⑤ 付款申请 DELETE 还原链路实测通过;⑥ 解锁终态页面标识通用方案(KingdeeUnlockMark))
- 范围:正式结算单(应付 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`)。
**回调方向与锁定相反(2026-10-15 真实报文确认)**:lockBusiData 由 WLTMS 发起(source=我方、target=金蝶),
unLockNotifyBusiData 由金蝶发起(source=金蝶、target=我方)。因此回调报文中:
`sourceBillNo`=金蝶单号(即锁记录 `targetBillNo`)、`targetBillNo`=WLTMS 单号(即锁记录 `sourceBillNo`)、
`targetBillType` **实测为空**(不参与匹配)。真实报文示例:
```json
{"lockAction":"UNLOCK","operationType":"DELETE","operator":"刘泉佳","sourceBillNo":"FKSQ-00000227",
"sourceBillType":"ap_payapply","sourceSystem":"XH","targetBillNo":"FKD2026101400001",
"targetBillType":"","targetSystem":"WLTMS","time":"2026-10-15 10:10:56"}
```
处理逻辑(`KingdeeBillLockServiceImpl.onUnlockNotify`):
```
校验:sourceSystem=XH、targetSystem=WLTMS、lockAction=UNLOCK、sourceBillNo/targetBillNo 非空(否则应答失败)
│
├─ 一次 IN 查询:target_bill_no in (回调sourceBillNo拆分, 支持逗号批量) and source_bill_no = 回调targetBillNo,
│ 按 id 降序取回,内存归并出各金蝶单号最新一条锁记录(避免逐张查询 N+1)
│
├─ 逐张匹配:无记录或非 locked → 幂等跳过(warn 留痕,重复通知防金蝶重试风暴)
│
├─ 命中 locked → 解锁状态批量落库:lock_status→unlocked、operationType、unlockTime、notifyOperator
│ (updateBatchById 一次批量更新,避免逐张 update N+1)
│
└─ operationType=DELETE(金蝶把单删了)→ 按「锁记录自身 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 → 该张跳过,不重复还原。
**事务语义**:整个回调处理在 `onUnlockNotify` 单一事务内(还原分支 `voidByKingdee`/`unlockBill` 均为 REQUIRED 传播加入同事务)——
任一张还原失败,**整批回滚**(包括已批量落库的解锁状态),应答失败触发金蝶重试,重试时按幂等口径从头重处理。
不存在"部分成功"中间态;若单张问题单阻塞整批回调,需人工处理后金蝶重试自动闭环。
**解锁后数据变化**(2026-10-15 实测口径):
锁记录(三类单据通用,`blade_kingdee_bill_lock`):
| 字段 | 变化 |
| --- | --- |
| lock_status | locked → unlocked(终态) |
| operation_type | 回调报文 operationType(SAVE-金蝶锁单(保存触发解锁,单据仍在)/ DELETE-金蝶删单) |
| unlock_time | WLTMS 处理时间(注意:非金蝶报文 time,金蝶侧操作时间以日志为准) |
| notify_operator | 回调报文 operator(金蝶侧操作人) |
业务单据(按类型):
| 业务单据 | 作废状态 | 金蝶同步信息 | 其他 |
| --- | --- | --- | --- |
| 正式结算单(SETTLEMENT) | approval_status → voided(已作废,终态),当前节点→已作废 | kingdeeSyncStatus → unsynced,清 kingdeeBillNo/kingdeeBillId/syncedTime | 变更流水记"金蝶删单回调自动废除",操作人=金蝶报文 operator |
| 开票申请(INVOICE_APPLY) | approval_status → voided(作废) | kingdeeStatus → unsynced,清单据编号 | 刷新关联结算单开票状态;动作流水记 kingdee_delete |
| 付款申请(PAY_APPLY) | **不作废**,approval_status 不变 | kingdeeStatus synced/paying → failed(仅这两态可变,幂等守卫),清 kingdeeBillId/kingdeeBillNo(原值日志留档) | 不自动重推,人工核实后重推;页面靠解锁标识区分金蝶删单与推送失败 |
### 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. **回调幂等**(2026-10-15 修正):回调方向与锁定相反,按「金蝶单号(回调 sourceBillNo)+ 我方单号(回调 targetBillNo)」双条件找最新 locked 记录(targetBillType 回调为空不参与匹配),找不到/已 unlocked 直接跳过应答成功,防金蝶重试风暴;
批量回调一次 IN 查询 + updateBatchById 批量落库,无逐张 N+1
5. **DELETE 还原口径**:作废源单 + 清金蝶同步信息 + 关联状态刷新(结算单/开票申请);付款申请不作废源单,走生成失败+人工重推(2026-10-15 联调实测通过);作废是终态,重推需新单重走流程
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`;
锁定失败摘要拼入「同步金蝶」返回值(与开票申请同款),页面直接可见
8. **解锁终态页面标识**(2026-10-15 新增,通用方案):付款申请解锁后不作废(仅 kingdeeStatus=failed),与推送失败无法区分;
结算单/开票申请虽变已作废,但看不出是金蝶删单导致。统一方案:
VO 实现 `KingdeeUnlockMark` 展示契约(解锁时间/操作人/动作名称三字段),
`KingdeeBillLockService.fillUnlockMarks` 对无活跃锁的行一次 IN 反查最新 unlocked 锁记录(`mapUnlockedLocks`)统一回填;
前端三个页面(付款申请/开票申请/正式结算单)金蝶锁状态列渲染:`kingdeeUnlockTime` 非空 →
「金蝶删单」红 tag /「金蝶锁单」橙 tag,tooltip 显示操作人+时间;后续单据(如收票单)接入锁单时 VO 实现契约即可复用
## 五、代码落点速查
| 内容 | 位置 |
| --- | --- |
| 锁单 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` 字段 |
| 解锁终态标识(2026-10-15) | 展示契约 `KingdeeUnlockMark`(三 VO 实现)+ `IKingdeeBillLockService.mapUnlockedLocks/fillUnlockMarks`(批量反查 unlocked 留痕并回填);VO 字段 `kingdeeUnlockTime/kingdeeUnlockOperator/kingdeeUnlockOperationName`;前端 `payment-application.vue`/`invoice-application.vue`/`formal-settlement-table-panel.vue` 的 `lockTag` 渲染金蝶删单/解锁标签 |
| 锁记录实体/服务 | `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 反审核删除兜底同口径;
2026-10-15 付款申请链路已联调实测通过:回调方向反转定位、DELETE 还原、解锁标识均验证 OK)
@@ -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,641 @@
# 金蝶(星瀚)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`(常量当前值 "999",注释示例 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 残留**:全部推送链路(应付/应收/付款/预付款/开票申请)仍传测试值 999;A103-3「取项目档案真实编码」的逻辑已注释停用(详见入参映射表 14-#2) | 上线前处理 |
| 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`,2026-10-15 增补),替代 09-27 版;单链路细节仍以 `doc/docs/业务流程/` 对应专项文档为深入参考,冲突处以本文 + 当前代码为准。
- **各推送接口的逐字段入参取值映射(页面字段→表字段/配置对照)见《2026-10-15-金蝶(星瀚)BFI推送入参取值映射表(页面字段-表字段-配置对照).md》**,其第 14 节含与本文记载不一致的代码级核对结论。
- 09-27 版中被本文修订的关键结论:A801-3 回写缺陷、票池「实时回源无定时」架构、收款认领「全手动无定时」、分录调试覆盖、物料映射常量、failed 落库口径、A801-5/A801-2/A902「未实现」状态。
Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

@@ -0,0 +1,327 @@
# 金蝶(星瀚)BFI 对接操作手册(纯操作版)
> 整理日期:2026-10-14
> 适用对象:TMS 业务操作人员(财务/业务)、系统管理员、实施运维。
> 本文只讲**怎么操作**:在哪个页面、点什么按钮、前提是什么、成功的标志是什么、失败了怎么处理。
> 接口与代码细节见同目录《2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册)》。
---
## 1. 全局须知
金蝶测试环境地址:https://cqcs-test.gxwuzi.com:8022/ierp/login.html
账号密码:
​ 13545909224 liuquanjia@123
​ 13737128880 websoft5386339
### 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)
![img.png](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/img.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 代码现状整理;系统升级后按钮与提示如有出入,以页面实际为准,并核对全链路总纲文档的更新说明。
@@ -0,0 +1,259 @@
# 付款申请同步金蝶(星瀚)技术文档
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)
> 对接系统:金蝶云·星瀚(业财融合 BFI,经 iPaaS 网关)
> 关联文档:`doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`、`doc/docs/TMS业财融合方案.docx`、`doc/docs/业财融合系统接口文档.xlsx`
> 最后更新:2026-09-16
---
## 1. 背景与范围
付款申请审核通过后,需将单据推送至金蝶云·星瀚生成付款申请单,并把星瀚返回的**单据内部主键(id)**记录到 `blade_payment_application.kingdee_bill_no`,供后续附件上传、状态查询、付款结果回写使用。
- 触发入口:付款申请页面(前端 `tms-erp-web/src/views/payment/payment-application.vue`)"批量同步"按钮,权限码 `payment_application_sync`
- 对接接口:**A103-3 付款申请保存** `POST /ierp/kapi/v2/awrp/ap/ap/ap_payapply/save`
- 覆盖路径:**预付款**(project_advance / progress_advance)与**正式付款**(settlement_payment)本次均走该接口;正式付款按方案文档原规划走 `ap_finapbill/push`(A103-1 应付下推),待后续切换
- 调用方式:前端 → TMS 后端转发 → 星瀚(OAuth2 凭证仅存后端配置,前端不持有)
---
## 2. 业务流程
### 2.1 业财融合整体链路(SC-04 对公付款)
```
TMS 付款申请审核通过
→ 批量同步(本文档):推送金蝶生成付款申请单(A103-3)
→ 金蝶财务共享审批(黑盒,机器人处理)
→ 金蝶付款处理
→ TMS 定时查询回写付款结果(A103-2 付款申请单查询 / A103-4 付款处理查询,待建设)
→ 附件推送(A902,凭付款申请单 id)与合同系统履约同步(C106-2,待建设)
```
### 2.2 TMS 付款申请状态流转
| 阶段 | approvalStatus | 说明 |
| --- | --- | --- |
| 草稿 | `draft` | 可编辑、可删除 |
| 审批中 | `reviewing` | 提交后进入财务审核 |
| 审批通过 | `approved` | **仅此状态允许同步金蝶**;可作废(voided) |
| 已驳回 | `returned` | 驳回后可编辑重新提交 |
| 已作废 | `voided` | 终态,不可同步 |
### 2.3 批量同步时序
```
前端 payment-application.vue blade-transport 后端 星瀚(BFI)
│ 勾选 approved 记录 │ │
│ POST /payment-application/ │ │
│ sync-kingdee-batch [id...] │ │
│──────────────────────────────────────>│ 逐单循环(单条失败不中断批次) │
│ │ FOR UPDATE 锁单 + 状态校验 │
│ │ 组装 BfiPayApplySaveDTO │
│ │ POST /ierp/kapi/v2/awrp/ap/ │
│ │ ap/ap_payapply/save ───────>│
│ │ <── result[]{billStatus,id,...} ─│
│ │ kingdeeBillNo = id │
│ │ kingdeeStatus = synced │
│ │ 下游台账联动(预付/结算付款记录) │
│ <── R<List<String>>(逐单结果)────────│ │
```
### 2.4 前置条件与幂等约定
| 约定 | 说明 |
| --- | --- |
| 状态校验 | 仅 `approvalStatus = approved` 允许同步 |
| 幂等跳过 | `kingdeeStatus = synced` 的单据直接返回已存单据号,不重复推送 |
| 防重键 | 星瀚侧 `awrp_src_code`(=paymentNo)+ `awrp_src_sys` 组成候选键,重复推送走更新语义;分录行 `awrp_entry_code`(=paymentNo + "-1")不得重复 |
| 失败重推 | 同步失败置 `kingdeeStatus = failed`,可重新勾选同步 |
| 锁单 | 推送成功后金蝶侧锁定,推送字段不允许修改;如需调整须金蝶反审核+删除后重推(B901 LOCK/UNLOCK) |
---
## 3. 接口定义(A103-3 付款申请保存)
### 3.1 鉴权
`access_token` 通过 `POST /ierp/kapi/oauth2/getToken` 获取(参数 clientId / clientSecret / username / accountId / nonce / timestamp),有效期 2 小时,由 `BfiServiceImpl` 自动获取与缓存,业务代码无需关心。
### 3.2 请求
- URL:`{thirdParty.bfi.baseUrl}/ierp/kapi/v2/awrp/ap/ap/ap_payapply/save`
- Method:`POST`,请求体 `{"data": [ {主表字段, "entry":[分录]} ]}`
### 3.3 返回结构(示例)
```json
{
"data": {
"failCount": "0",
"result": [
{
"billIndex": 0,
"billStatus": true,
"errors": [],
"id": "2395327167211785216",
"keys": {
"awrp_src_code": "00225",
"awrp_src_sys": "测试系统"
},
"number": "FKSQ-00000147",
"type": "Add"
}
],
"successCount": "1"
},
"errorCode": "0",
"message": null,
"status": true
}
```
| 字段 | 含义 | TMS 处理 |
| --- | --- | --- |
| data.successCount / failCount | 成功/失败数量(字符串数字) | `BfiBillSaveResultVO`,辅助 `isAllSuccess()` |
| result[].billIndex | 对应请求 data 数组下标 | 逐单调用恒为 0 |
| result[].billStatus | 单据级成败 | false 时以 errors 抛出业务异常 |
| result[].id | **星瀚付款申请单内部主键** | **写入 `kingdee_bill_no`** |
| result[].number | 星瀚单据编号(FKSQ-xxxx) | 暂不落库,可经 A103-2 查询勾稽 |
| result[].type | Add 新增 / Push 下推 | 仅记录 |
| errorCode / message / status | 接口级状态 | `commonCall` 统一校验,失败抛 `BfiException` |
---
## 4. 字段映射关系
### 4.1 主表(`BfiPayApplySaveDTO` ← `PaymentApplication` 实体 / 配置)
| 接口字段 | 含义 | 来源 | 说明 |
| --- | --- | --- | --- |
| awrp_src_code | 来源单据号(防重键) | `paymentNo` | 付款申请单号 |
| awrp_src_sys | 外部系统编号(防重键) | 配置 `payApply.srcSys` | 默认 `WLTMS` |
| settleorg_number / applyorg_number / payorg_number / purorg_number | 结算/申请/付款/采购组织编码 | 合同所属组织 | `organizationId` → 组织编码 `deptCode`,为空回退组织名称;缺失报错 |
| billtype_number | 单据类型编码 | 配置 `payApply.billtype` | 默认 `ap_payapply_oth_BT_S` 其他付款申请 |
| applydate | 申请日期 | `applyDate` | yyyy-MM-dd,空取当天 |
| applycause | 请款事由 | paymentType 映射 | 见 5.1 |
| paycurrency_number / settlecurrency_number | 付款/结算币种 | 合同结算币种 `settlementCurrency` | 缺失报错 |
| awrp_src_type | 外部单据类型 | 配置 `payApply.srcType` | 默认 `TMS付款申请` |
| exratetable_number | 汇率表编码 | 配置 `payApply.exratetable` | 默认 `01` |
| exratedate | 汇率日期 | 同 applydate | yyyy-MM-dd |
| awrp_textfield1 | 付款渠道 | `paymentMethod` 字典标签 | 见 5.4 |
| awrp_src_bt | 来源单据类型 | 有合同传 `awrp_contract_info` | 可选 |
| awrp_src_con_no | 上游合同号 | `contractNo` | 可选,随 awrp_src_bt 一起传 |
### 4.2 分录(`BfiPayApplySaveEntryDTO`,单条申请固定 1 行)
| 接口字段 | 含义 | 来源 | 说明 |
| --- | --- | --- | --- |
| e_paymenttype_number | 付款类型编码 | paymentType 映射 | `201` 正式付款 / `202` 预付款,见 5.1 |
| e_asstacttype | 往来类型 | 固定 `bd_supplier` | 供应商 |
| e_asstact_number | 往来户编码 | payeeName → 客商档案 `customerCode` | 按 fullName/shortName 匹配,未匹配回退 payeeName |
| e_assacct | 往来账户 | `bankAccount` | 收款账号 |
| e_bebank_number | 往来银行编码 | `bankName` | 暂传名称,待银行编码映射 |
| e_applyamount / e_payamount | 申请/应付金额 | `appliedAmount` | 2 位小数 |
| e_expaydate | 期望付款日 | 同 applydate | — |
| project_number | 项目编码 | projectId → `ProjectApply.projectCode` | 无项目传 `999` |
| awrp_entry_code | 对方唯一行 id(防重) | `paymentNo + "-1"` | 不得重复 |
| e_remark | 备注 | `remark` | 可选 |
未传的可选字段:purdept_number、awrp_department_number、e_duedate、e_settlementtype_number、e_costcenter_number、awrp_src_con_eno、awrp_splb_number。
> 前置校验:推送前按 `contractId` 加载合同,未关联合同、合同不存在、所属组织编码为空、结算币种为空时,同步失败并标记 `kingdeeStatus = failed`。
---
## 5. 字典与枚举对应关系
### 5.1 付款类型 paymentType → 接口编码
| paymentType(TMS) | 中文含义 | e_paymenttype_number | applycause 请款事由 |
| --- | --- | --- | --- |
| `project_advance` | 项目预付 | `202` | 项目预付款申请 |
| `progress_advance` | 进度预付 | `202` | 进度预付款申请 |
| `settlement_payment` | 结算付款(正式付款) | `201` | 结算付款申请 |
### 5.2 审批状态 approvalStatus
`draft` → `reviewing` → `approved` / `returned`;`approved` → `voided`。仅 `approved` 可同步。
### 5.3 金蝶同步状态 kingdeeStatus
| 值 | 含义 | 触发 |
| --- | --- | --- |
| `unsynced` | 未生成 | 新建单据默认 |
| `synced` | 已生成 | 推送成功,`kingdee_bill_no` = 星瀚单据 id |
| `failed` | 生成失败 | 推送异常/金蝶返回 billStatus=false,可重推 |
### 5.4 付款渠道 awrp_textfield1
`paymentMethod` 存业务字典 `pay_method`(DictBiz)的 key/value,推送时经 `DictBizCache.getList("pay_method")` 翻译为字典标签(如电汇、汇票);未命中字典时原样传递。汇票类付款(标签含"汇票")在 TMS 侧另有台账占用逻辑,与推送无关。
### 5.5 其他固定值
| 常量 | 值 | 用途 |
| --- | --- | --- |
| 往来类型 | `bd_supplier` | e_asstacttype |
| 上游合同类型 | `awrp_contract_info` | awrp_src_bt |
| 无项目编码 | `999` | project_number |
---
## 6. 配置项(Nacos:`thirdParty.bfi.*`)
模板见 `doc/nacos/blade-dev.yaml`、`blade-prod.yaml`、`third-party-api.yaml`,支持环境变量覆盖:
| 配置键 | 环境变量 | 默认值 | 说明 |
| --- | --- | --- | --- |
| thirdParty.bfi.baseUrl | BFI_BASE_URL | — | 星瀚网关地址(已有) |
| thirdParty.bfi.clientId / clientSecret / username / accountId | BFI_* | — | OAuth2 凭证(已有) |
| thirdParty.bfi.payApply.srcSys | BFI_PAY_APPLY_SRC_SYS | WLTMS | 外部系统编号(防重键) |
| thirdParty.bfi.payApply.billtype | BFI_PAY_APPLY_BILLTYPE | ap_payapply_oth_BT_S | 单据类型编码(其他付款申请) |
| thirdParty.bfi.payApply.exratetable | BFI_PAY_APPLY_EXRATE_TABLE | 01 | 汇率表编码 |
| thirdParty.bfi.payApply.srcType | BFI_PAY_APPLY_SRC_TYPE | TMS付款申请 | 外部单据类型 |
> 组织编码与币种不走配置:分别取自付款申请关联合同的所属组织(`blade_contract_manage.organization_id` → 组织编码,为空回退组织名称)与结算币种(`settlement_currency`)。
---
## 7. 异常处理与批量语义
| 场景 | 行为 |
| --- | --- |
| 单笔同步 `syncKingdee` | 校验失败/推送失败置 `failed` 并抛 `ServiceException`,前端提示原因(含金蝶 errors 明细) |
| 批量同步 `syncKingdeeBatch` | 逐单循环,**单条失败不中断批次**;失败单标记 `failed` 后继续下一单;返回结果列表中附 `付款申请[id]同步失败:原因` |
| 金蝶接口级失败(status=false / errorCode≠0) | `BfiServiceImpl.commonCall` 抛 `BfiException`,同上处理 |
| 金蝶单据级失败(billStatus=false) | 以 `errors` 拼接抛出 |
| 重复点击同步 | 已 `synced` 单据跳过;`failed`/`unsynced` 允许重推(防重键保证金蝶侧幂等) |
---
## 8. result.id 的后续使用(待建设环节)
| 环节 | 接口 | id 用法 |
| --- | --- | --- |
| 付款申请附件上传 | A902 `/frame/attachment/uploadFile` | `attachmentUploadFileArgs.billPkId` = 该 id |
| 付款申请单状态查询 | A103-2 `ap_payapply/query` | 按 awrp_src_code+awrp_src_sys 查询,返回体含 id/billno/billstatus(A暂存/B已提交/C已审核/D已关闭)/paystatus(未付/部分付/已付),与本地 id 勾稽 |
| 付款处理查询 | A103-4 `cas_paybill/query` | 返回 sourcebillid/sourcebillnumber,与 id/编号核对付款结果 |
## 9. 遗留事项
1. **付款完成模拟待拆除**:同步成功后仍保留 `paidAmount = 申请金额`、`mockPaymentRecords()` 模拟付款记录(单号前缀 `MOCK-PAY-`)、下游预付/结算付款记录直接置 `paid`。真实流程应由 A103-2/A103-4 定时回写驱动,待回写任务建设时移除。
2. **正式付款路径切换**:按方案文档正式付款应走 A103-1 `ap_finapbill/push`(应付下推,分录需 `awrp_finapbill_entryid` 关联应付行),当前与预付款共用 A103-3。
3. **银行编码映射**:e_bebank_number 暂传银行名称,待建立银行档案编码映射。
4. **单据编号 number**(FKSQ-xxxx)暂未落库,如需展示可经 A103-2 查询或后续增加字段。
---
## 10. 涉及代码位置
| 内容 | 位置 |
| --- | --- |
| 同步入口/字段映射 | `blade-service/blade-transport/.../service/impl/PaymentApplicationServiceImpl.java`(syncKingdee / syncKingdeeBatch / pushBfiPayApply) |
| Controller | `blade-service/blade-transport/.../controller/PaymentApplicationController.java`(/sync-kingdee、/sync-kingdee-batch) |
| BFI 客户端 | `blade-third-party-api/blade-bfi-api/.../feign/IBfiClient.java`(PRE_PAY_APPLY_SAVE_URL) |
| BFI 服务封装 | `blade-third-party-api/blade-bfi-api/.../service/impl/BfiServiceImpl.java`(savePrePayApply / commonCall / getToken) |
| 请求/返回模型 | `blade-bfi-api/.../pojo/dto/BfiPayApplySaveDTO.java`、`BfiPayApplySaveEntryDTO.java`;`pojo/vo/BfiBillSaveResultVO.java`、`BfiBillSaveResultItemVO.java` |
| 前端页面 | `tms-erp-web/src/views/payment/payment-application.vue`(handleSync);`src/api/payment/paymentApplication.js`(syncKingdeeBatch) |
| Nacos 配置模板 | `doc/nacos/blade-dev.yaml`、`blade-prod.yaml`、`third-party-api.yaml`(thirdParty.bfi.payApply.*) |
+37 -1
View File
@@ -47,7 +47,8 @@ blade:
datasource:
dev:
# MySql
url: jdbc:mysql://localhost:3306/transport?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
# rewriteBatchedStatements=true:MyBatis-Plus saveBatch/updateBatchById 依赖该参数才会合并为多值语句,否则仍是逐条往返(仅 MySQL 支持,切换其他库需移除)
url: jdbc:mysql://localhost:3306/transport?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
username: root
password: root
# PostgreSQL
@@ -137,6 +138,41 @@ thirdParty:
# 微信小程序(手机号一键登录 / openid)
appId: ${WECHAT_MINI_APP_ID:}
appSecret: ${WECHAT_MINI_APP_SECRET:}
bfi:
# 业财融合(BFI)应用及接口配置,敏感值通过环境变量注入
baseUrl: ${BFI_BASE_URL:http://172.16.203.134:8000/gwzh/BFI}
authorization: ${BFI_AUTHORIZATION:3F7e8yI6RV8S53mJ9tYa0672r3W4n279}
clientId: ${BFI_CLIENT_ID:WLTMS}
clientSecret: ${BFI_CLIENT_SECRET:XDWLwltmstest@HT2026}
username: ${BFI_USERNAME:XDWL-03024}
accountId: ${BFI_ACCOUNT_ID:2454399096727798784}
language: ${BFI_LANGUAGE:zh_CN}
# 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS)
srcSys: ${BFI_SRC_SYS:WLTMS}
# 付款申请推送配置(A103-3 付款申请保存)
payApply:
# 单据类型编码:ap_payapply_BT_S 采购付款申请、ap_payapply_oth_BT_S 其他付款申请
billtype: ${BFI_PAY_APPLY_BILLTYPE:ap_payapply_oth_BT_S}
# 组织编码、币种取自付款申请关联合同的所属组织与结算币种
# 汇率表编码
exratetable: ${BFI_PAY_APPLY_EXRATE_TABLE:01}
# 外部单据类型
srcType: ${BFI_PAY_APPLY_SRC_TYPE:0KzXs}
# 金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询)
payResult:
# 回写任务开关
syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true}
# 调度 cron,默认每10分钟
cron: ${BFI_PAY_RESULT_CRON:0 */10 * * * ?}
# 收款认领推送金蝶定时任务(A301-3 收款认领结果推送)
receiptClaimPush:
# 推送任务开关,默认关闭(联调稳定后开启)
syncEnabled: ${BFI_RECEIPT_CLAIM_PUSH_SYNC_ENABLED:false}
# 调度 cron,默认每10分钟
cron: ${BFI_RECEIPT_CLAIM_PUSH_CRON:0 */10 * * * ?}
# single-flight 分布式锁租约秒数,须大于最坏一轮耗时(逐认领人远程查公告+逐条推送),
# 进程异常退出时锁在租约到期后自动释放
lockLeaseSeconds: ${BFI_RECEIPT_CLAIM_PUSH_LOCK_LEASE_SECONDS:1800}
#百度OCR配置,API Key和Secret Key请通过环境变量注入
baidu:
+47 -2
View File
@@ -35,6 +35,8 @@ blade:
#分布式锁配置
lock:
##是否启用分布式锁
##开启前必须先补 password(与 spring.data.redis 一致),否则 Redisson 无密码连接会被拒绝,
##影响的是全部依赖该模板的服务;收票全票池等同步定时任务依赖它做多副本 single-flight
enabled: false
##将docker脚本部署的redis服务映射为宿主机ip
##生产环境推荐使用阿里云高可用redis服务并设置密码
@@ -43,7 +45,7 @@ blade:
#通用开发生产环境数据库地址(特殊情况可在对应的子工程里配置覆盖)
datasource:
prod:
url: jdbc:mysql://172.16.203.228:3306/transport?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
url: jdbc:mysql://172.16.203.228:3306/transport?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
username: transport
password: ab3acJYSC8A4ySpk
@@ -91,7 +93,6 @@ thirdParty:
processExecuteUrl: ${MK_PROCESS_EXECUTE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/execute}
processDeleteUrl: ${MK_PROCESS_DELETE_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/delete}
getCurrentNodesUrl: ${MK_CURRENT_NODES_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getCurNodesInfo}
getProcessInfoUrl: ${MK_PROCESS_INFO_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getProcessInfo}
getNodeHandlersUrl: ${MK_NODE_HANDLERS_URL:/openapi/sys-lbpm/sysLbpmProcess/openSupport/getNodeHandlerInfos}
getManualNodeUrl: ${MK_MANUAL_NODE_URL:/openapi/sys-lbpm/sysLbpmTemplate/openSupport/getManualNode}
pushOrgDeptUrl: ${MK_PUSH_ORG_DEPT_URL:/openapi/sys-org/v2/push/orgDept}
@@ -108,6 +109,41 @@ thirdParty:
mini:
appId: ${WECHAT_MINI_APP_ID:}
appSecret: ${WECHAT_MINI_APP_SECRET:}
bfi:
# 业财融合(BFI)应用及接口配置,敏感值通过环境变量注入
baseUrl: ${BFI_BASE_URL:http://172.16.203.134:8000/gwzh/BFI}
authorization: ${BFI_AUTHORIZATION:}
clientId: ${BFI_CLIENT_ID:}
clientSecret: ${BFI_CLIENT_SECRET:}
username: ${BFI_USERNAME:}
accountId: ${BFI_ACCOUNT_ID:}
language: ${BFI_LANGUAGE:zh_CN}
# 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS)
srcSys: ${BFI_SRC_SYS:WLTMS}
# 付款申请推送配置(A103-3 付款申请保存)
payApply:
# 单据类型编码:ap_payapply_BT_S 采购付款申请、ap_payapply_oth_BT_S 其他付款申请
billtype: ${BFI_PAY_APPLY_BILLTYPE:ap_payapply_oth_BT_S}
# 组织编码、币种取自付款申请关联合同的所属组织与结算币种
# 汇率表编码
exratetable: ${BFI_PAY_APPLY_EXRATE_TABLE:01}
# 外部单据类型
srcType: ${BFI_PAY_APPLY_SRC_TYPE:0KzXs}
# 金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询)
payResult:
# 回写任务开关
syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true}
# 调度 cron,默认每10分钟
cron: ${BFI_PAY_RESULT_CRON:0 */10 * * * ?}
# 收款认领推送金蝶定时任务(A301-3 收款认领结果推送)
receiptClaimPush:
# 推送任务开关,默认关闭(联调稳定后开启)
syncEnabled: ${BFI_RECEIPT_CLAIM_PUSH_SYNC_ENABLED:false}
# 调度 cron,默认每10分钟
cron: ${BFI_RECEIPT_CLAIM_PUSH_CRON:0 */10 * * * ?}
# single-flight 分布式锁租约秒数,须大于最坏一轮耗时(逐认领人远程查公告+逐条推送),
# 进程异常退出时锁在租约到期后自动释放
lockLeaseSeconds: ${BFI_RECEIPT_CLAIM_PUSH_LOCK_LEASE_SECONDS:1800}
#百度OCR配置,API Key和Secret Key请通过环境变量注入
baidu:
@@ -120,6 +156,15 @@ baidu:
request-timeout: 30s
token-refresh-advance: 1m
# 对象存储(blade-file 分片上传 + blade-transport 凭证读写共用)
# endpoint:强烈建议内网 MinIO(http://IP:19000);若只能走网关可填 https://tms-prod.xinghy56.com/oss(代码会自动处理 /oss)
# transform-endpoint:浏览器预签名外网地址
file:
storage:
minio:
# endpoint: http://内网MinIO地址:端口
transform-endpoint: https://tms-prod.xinghy56.com/oss
powerjob:
worker:
server-address: 172.16.203.228:7700
+3 -3
View File
@@ -33,14 +33,14 @@ spring:
blade:
#分布式锁配置
lock:
##是否启用分布式锁
enabled: false
##是否启用分布式锁(收票全票池同步等定时任务依赖 RedisLockClient 做 single-flight,多副本部署必须开启)
enabled: true
##将docker脚本部署的redis服务映射为宿主机ip
address: redis://192.168.0.188:3379
#通用开发生产环境数据库地址(特殊情况可在对应的子工程里配置覆盖)
datasource:
test:
url: jdbc:mysql://192.168.0.188:3306/bladex?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
url: jdbc:mysql://192.168.0.188:3306/bladex?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
username: root
password: root
+10
View File
@@ -228,6 +228,9 @@ blade:
- /customer-archive/public/**
- /blade-transport/mk-process/public/**
- /mk-process/public/**
# 金蝶单据解锁回调(unLockNotifyBusiData):金蝶侧无令牌调用,免登免授权
- /blade-transport/kingdee-bfi/**
- /kingdee-bfi/**
- /blade-openapi/openApi/mk/process/commonCallback
- /openApi/mk/process/commonCallback
- /feign/client/businessProcess/getCurrentNodes
@@ -254,6 +257,13 @@ blade:
- method: POST
pattern: /dashboard/submit
expression: "hasAnyRole('administrator', 'admin', 'user')"
# 金蝶单据解锁回调(unLockNotifyBusiData):金蝶侧无令牌调用,免登免授权
- method: POST
pattern: /blade-transport/kingdee-bfi/**
expression: "permitAll()"
- method: POST
pattern: /kingdee-bfi/**
expression: "permitAll()"
#基础认证配置
basic:
- method: ALL
+112
View File
@@ -32,3 +32,115 @@ thirdParty:
# 微信小程序 AppId / AppSecret(Nacos 配置,勿提交真实 secret)
appId: ${WECHAT_MINI_APP_ID:}
appSecret: ${WECHAT_MINI_APP_SECRET:}
bfi:
# 业财融合(BFI)开放接口地址
baseUrl: ${BFI_BASE_URL:http://172.16.203.134:8000/gwzh/BFI}
# 网关认证请求头 Authorization 的值
authorization: ${BFI_AUTHORIZATION:}
# 第三方应用系统编码,即 appId
clientId: ${BFI_CLIENT_ID:}
# 第三方应用AccessToken认证密钥,即 appSecret,敏感值通过环境变量注入
clientSecret: ${BFI_CLIENT_SECRET:}
# 访问用户名
username: ${BFI_USERNAME:}
# 数据中心ID
accountId: ${BFI_ACCOUNT_ID:}
# 语言字串,默认 zh_CN
language: ${BFI_LANGUAGE:zh_CN}
# 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS)
srcSys: ${BFI_SRC_SYS:WLTMS}
# 应付/应收/收票单分录物料映射(A102-2/A302-2/A303-2 共用,未配置使用代码默认值):
# 运输类按项目业务模式映射——国内运输按运单运输方式映射、国际运输映射国际货代服务;
# 装卸大类及手动添加的非运费杂费映射装卸物料
# 注意:Map 的中文 key 必须用 ["中文"] 方括号写法,否则 Spring Boot 绑定失败、服务启动报错
material:
# 国内运输:运单运输方式 -> 金蝶物料编码(公路运输/铁路运输/水路运输/航空运输四种)
domestic:
"[公路运输]": ${BFI_MATERIAL_DOMESTIC_ROAD:XDWL11010001}
"[铁路运输]": ${BFI_MATERIAL_DOMESTIC_RAIL:XDWL11010002}
"[水路运输]": ${BFI_MATERIAL_DOMESTIC_WATER:XDWL11010003}
"[航空运输]": ${BFI_MATERIAL_DOMESTIC_AIR:Item-00001082}
# 国际运输业务模式物料编码(国际货运代理服务)
international: ${BFI_MATERIAL_INTERNATIONAL:Item-00001081}
# 装卸大类、手动添加非运费杂费物料编码(装卸)
loading: ${BFI_MATERIAL_LOADING:XDWL11002004}
# 付款申请推送配置(A103-3 付款申请保存)
payApply:
# 单据类型编码:ap_payapply_BT_S 采购付款申请、ap_payapply_oth_BT_S 其他付款申请
billtype: ${BFI_PAY_APPLY_BILLTYPE:ap_payapply_oth_BT_S}
# 组织编码、币种取自付款申请关联合同的所属组织与结算币种
# 汇率表编码
exratetable: ${BFI_PAY_APPLY_EXRATE_TABLE:01}
# 外部单据类型
srcType: ${BFI_PAY_APPLY_SRC_TYPE:0KzXs}
# 金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询)
payResult:
# 回写任务开关
syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true}
# 调度 cron,默认每10分钟(:01 起步,错峰避让其他金蝶任务)
cron: ${BFI_PAY_RESULT_CRON:0 1/10 * * * ?}
# 金蝶收票单结果定时回写(A801-3 异常回收/指定补偿/单号补登)
receivedInvoiceResult:
# 回写任务开关,默认关闭(联调稳定后开启)
syncEnabled: ${BFI_RECEIVED_INVOICE_RESULT_SYNC_ENABLED:false}
# 调度 cron,默认每10分钟(:08 起步,错峰避让其他金蝶任务)
cron: ${BFI_RECEIVED_INVOICE_RESULT_CRON:0 8/10 * * * ?}
# 收款认领推送金蝶定时任务(A301-3 收款认领结果推送)
receiptClaimPush:
# 推送任务开关,默认关闭(联调稳定后开启)
syncEnabled: ${BFI_RECEIPT_CLAIM_PUSH_SYNC_ENABLED:false}
# 调度 cron,默认每10分钟(:04 起步,错峰避让其他金蝶任务)
cron: ${BFI_RECEIPT_CLAIM_PUSH_CRON:0 4/10 * * * ?}
# single-flight 分布式锁租约秒数,须大于最坏一轮耗时(逐认领人远程查公告+逐条推送),
# 进程异常退出时锁在租约到期后自动释放
lockLeaseSeconds: ${BFI_RECEIPT_CLAIM_PUSH_LOCK_LEASE_SECONDS:1800}
# 金蝶单据锁定定时补偿
billLock:
# 锁定/解锁总开关(推送后锁定+解锁回调+补偿重锁共用),默认开启保持既有行为
enabled: ${BFI_BILL_LOCK_ENABLED:true}
# 锁定补偿开关,默认关闭(安全兜底)
retryEnabled: ${BFI_BILL_LOCK_RETRY_ENABLED:false}
# 补偿调度 cron,默认每5分钟(:02 起步,错峰避让其他金蝶任务)
retryCron: ${BFI_BILL_LOCK_RETRY_CRON:0 2/5 * * * ?}
# 收款认领公告查询配置(A301-2)
claimAnnounce:
# 查询时间窗口(月数)
months: ${BFI_CLAIM_ANNOUNCE_MONTHS:6}
# 认领人:配置后公告查询固定使用该值,不再按当前登录用户姓名查询
claimusers: ${BFI_CLAIM_ANNOUNCE_CLAIMUSERS:}
# 金蝶全票池发票定时同步(A801-1 窗口拉取+镜像 upsert,选票统一查本地)
invoiceReceipt:
# 同步任务开关,默认关闭(上线先手动 POST /invoice-receipt/sync-invoice-pool 跑一次全窗口铺底)
syncEnabled: ${BFI_INVOICE_RECEIPT_SYNC_ENABLED:false}
# 调度 cron,默认每30分钟(:16 起步,错峰避让其他金蝶任务)
cron: ${BFI_INVOICE_RECEIPT_CRON:0 16/30 * * * ?}
# 增量窗口天数(按票池更新时间滚动)
incrementDays: ${BFI_INVOICE_RECEIPT_INCREMENT_DAYS:30}
# 同步发票种类白名单(逗号分隔票易通编码,如 26,27 数电普票/专票),为空不限制
invoiceTypes: ${BFI_INVOICE_RECEIPT_INVOICE_TYPES:26,27}
# 补偿绑定单轮限量(未绑定登记按发票号码精确补查条数上限)
compensateLimit: ${BFI_INVOICE_RECEIPT_COMPENSATE_LIMIT:200}
# single-flight 分布式锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放
lockLeaseSeconds: ${BFI_INVOICE_RECEIPT_LOCK_LEASE_SECONDS:1800}
# 审批通过自动同步金蝶(事件触发:结算单A102-2/A302-2、付款申请A103-1/A103-3、开票申请A303-1、收票A303-2+A302-6)
autoSync:
# 结算单审批后自动推送开关
settlement: ${BFI_AUTO_SYNC_SETTLEMENT:false}
# 付款申请审批后自动推送开关
payment: ${BFI_AUTO_SYNC_PAYMENT:false}
# 开票申请审批后自动推送开关
invoiceApply: ${BFI_AUTO_SYNC_INVOICE_APPLY:false}
# 收票登记审批后自动推送开关
receipt: ${BFI_AUTO_SYNC_RECEIPT:false}
# failed 单据定时重推开关(仅重推生成失败,不扫 unsynced 存量)
retryEnabled: ${BFI_AUTO_SYNC_RETRY_ENABLED:false}
# 重推调度 cron,默认每10分钟(:05 起步,错峰避让其他金蝶任务)
retryCron: ${BFI_AUTO_SYNC_RETRY_CRON:0 5/10 * * * ?}
# 客商金蝶数据定时同步(A902-1 复查 / A902-2 推送)
customer:
# 同步任务开关,默认关闭
syncEnabled: ${BFI_CUSTOMER_SYNC_ENABLED:false}
# 同步调度 cron,默认每天 02:00 执行
cron: ${BFI_CUSTOMER_SYNC_CRON:0 0 2 * * ?}
# 同步 single-flight 锁租约秒数,须大于最坏一轮耗时(复查 + 批量推送 + 停用联动)
lockLeaseSeconds: ${BFI_CUSTOMER_SYNC_LOCK_LEASE_SECONDS:1800}
+2 -2
View File
@@ -1261,11 +1261,11 @@ CREATE TABLE `blade_user` (
`real_name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真名',
`avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像',
`email` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`phone` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机',
`phone` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机',
`birthday` datetime NULL DEFAULT NULL COMMENT '生日',
`sex` int NULL DEFAULT NULL COMMENT '性别',
`role_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色id',
`dept_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门id',
`dept_id` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门id',
`post_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '岗位id',
`leader_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '主管id',
`is_leader` int NULL DEFAULT 0 COMMENT '是否主管',
@@ -0,0 +1,24 @@
-- 客商档案:金蝶(星瀚)协同字段
-- kingdee_status:none未推送 / exist金蝶已有档案(A902-1查到已审核,跳过推送) / synced已推送成功 / failed推送失败或金蝶审核中
-- 金蝶客户编码与 TMS 客商编号(customer_code)一致(A902-2 number 传入客商编号),无需单独存储
ALTER TABLE `blade_customer_archive`
ADD COLUMN `kingdee_status` varchar(20) NOT NULL DEFAULT 'none' COMMENT '金蝶协同状态:none未推送/exist金蝶已有/synced已推送/failed推送失败' AFTER `approved_time`,
ADD COLUMN `kingdee_fail_reason` varchar(500) NULL DEFAULT NULL COMMENT '金蝶推送失败原因' AFTER `kingdee_status`,
ADD COLUMN `kingdee_sync_time` datetime NULL DEFAULT NULL COMMENT '金蝶推送时间' AFTER `kingdee_fail_reason`;
CREATE INDEX idx_kingdee_status ON blade_customer_archive (kingdee_status);
-- 列表「同步金蝶」按钮权限(审批通过且未推送/推送失败时可点;parent_id 通过菜单 code 关联)
INSERT INTO blade_menu (id, parent_id, code, name, alias, path, source, sort, category, action, is_open, component, remark, is_deleted)
SELECT 2077600000000000011, m.id, 'customer_archive_sync', '同步金蝶', 'customer_archive_sync', '', '', 10, 2, 0, 1, NULL, '', 0
FROM blade_menu m
WHERE m.code = 'customer_archive' AND m.is_deleted = 0
LIMIT 1
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
name = VALUES(name),
alias = VALUES(alias),
sort = VALUES(sort),
category = VALUES(category),
is_deleted = 0;
@@ -0,0 +1,22 @@
-- 修复「同步金蝶」按钮菜单:按 code 幂等补齐/纠正,避免权限勾了但不生效
-- 执行后需:角色重新勾选「同步金蝶」→ 用户重新登录
INSERT INTO blade_menu (id, parent_id, code, name, alias, path, source, sort, category, action, is_open, component, remark, is_deleted)
SELECT 2077600000000000011, m.id, 'customer_archive_sync', '同步金蝶', 'customer_archive_sync', '', '', 10, 2, 0, 1, NULL, '', 0
FROM blade_menu m
WHERE m.code = 'customer_archive' AND m.is_deleted = 0
AND NOT EXISTS (
SELECT 1 FROM blade_menu x WHERE x.code = 'customer_archive_sync' AND x.is_deleted = 0
)
LIMIT 1;
UPDATE blade_menu child
INNER JOIN blade_menu parent ON parent.code = 'customer_archive' AND parent.is_deleted = 0
SET
child.parent_id = parent.id,
child.name = '同步金蝶',
child.alias = 'customer_archive_sync',
child.category = 2,
child.sort = 10,
child.is_deleted = 0
WHERE child.code = 'customer_archive_sync';
@@ -0,0 +1,22 @@
-- 开票申请金蝶发票结果表(A303-5 定时回写落库,发票号唯一幂等)
CREATE TABLE IF NOT EXISTS `blade_invoice_application_invoice` (
`id` bigint(20) NOT NULL COMMENT '主键',
`tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
`status` int(11) DEFAULT '1' COMMENT '状态',
`is_deleted` int(11) DEFAULT '0' COMMENT '是否删除',
`invoice_application_id` bigint(20) NOT NULL COMMENT '开票申请ID',
`serial_no` varchar(64) DEFAULT NULL COMMENT '金蝶开票申请单编号(A303-5 serialNo)',
`invoice_no` varchar(64) NOT NULL COMMENT '发票号码',
`invoice_date` datetime DEFAULT NULL COMMENT '开票日期',
`invoice_amount` decimal(18,2) DEFAULT NULL COMMENT '价税合计',
`raw_json` text COMMENT 'A303-5返回原文JSON留档',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_application_invoice` (`tenant_id`,`invoice_application_id`,`invoice_no`),
KEY `idx_iai_application_id` (`invoice_application_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='开票申请金蝶发票结果';
@@ -0,0 +1,65 @@
-- ============================================================
-- blade_invoice_application_invoice_urls_20260930.sql
-- ============================================================
-- 开票申请金蝶发票结果表补充发票文件下载地址(A303-5 返回,正式结算单详情查看/下载发票):
-- invoice_file_url 板式文件下载地址(数电票为OFD,航信百望税盘普通电票为PDF,电子专票/UKey电票为OFD)
-- invoice_image_url PDF图片预览地址
-- invoice_pdf_file_url 数电PDF下载地址(发票云5.0.025版本返回)
-- invoice_xml_file_url 数电XML下载地址(发票云5.0.025版本返回)
-- 以下结构升级支持重复执行
-- ① blade_invoice_application_invoice.invoice_file_url
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_application_invoice` ADD COLUMN `invoice_file_url` varchar(1024) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''板式文件下载地址(数电票为OFD,普通电票为PDF,电子专票为OFD)'' AFTER `invoice_amount`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_application_invoice'
AND COLUMN_NAME = 'invoice_file_url'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ② blade_invoice_application_invoice.invoice_image_url
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_application_invoice` ADD COLUMN `invoice_image_url` varchar(1024) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''PDF图片预览地址'' AFTER `invoice_file_url`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_application_invoice'
AND COLUMN_NAME = 'invoice_image_url'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ③ blade_invoice_application_invoice.invoice_pdf_file_url
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_application_invoice` ADD COLUMN `invoice_pdf_file_url` varchar(1024) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''数电PDF下载地址(发票云5.0.025版本返回)'' AFTER `invoice_image_url`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_application_invoice'
AND COLUMN_NAME = 'invoice_pdf_file_url'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ④ blade_invoice_application_invoice.invoice_xml_file_url
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_application_invoice` ADD COLUMN `invoice_xml_file_url` varchar(1024) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''数电XML下载地址(发票云5.0.025版本返回)'' AFTER `invoice_pdf_file_url`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_application_invoice'
AND COLUMN_NAME = 'invoice_xml_file_url'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,21 @@
-- 应收结算与开票申请同步金蝶(A303-1 开票申请推送)
-- 开票申请商品行新增字段:
-- tax_classification_code 税收分类编码(开票项目主数据 blade_invoice_item.tax_classification_code,
-- 新增/编辑开票申请选择开票内容时快照落库,A303-1 分录 taxratecodeid.number 取数来源)
-- 销方名称/销方税号不走 Nacos 配置与表结构:销方名称取开票申请开票单位快照(issuer_name,结算单收款方带出),
-- 销方税号按开票单位查客商档案统一社会信用代码(unified_credit_code)
-- 开票申请基础建表脚本 blade_invoice_application_20260821.sql 保持不动,本脚本支持重复执行
-- 以下结构升级支持重复执行
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_application_line` ADD COLUMN `tax_classification_code` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''税收分类编码(开票内容快照)'' AFTER `goods_name`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_application_line'
AND COLUMN_NAME = 'tax_classification_code'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,50 @@
-- ============================================================
-- blade_invoice_application_sheet_kingdee_bill_no_20260924.sql
-- ============================================================
-- 开票申请同步金蝶按发票张次拆分开票(A303-1 一张发票张次一张开票申请单)
-- blade_invoice_application_sheet.kingdee_bill_no 本张发票对应的金蝶开票申请单编号(A303-1 返回,A303-5 serialNo 查询入口)
-- blade_invoice_application.kingdee_bill_no 多张发票时逗号拼接全部金蝶单号,列加宽至 varchar(500) 防溢出
-- blade_invoice_application_record.kingdee_bill_no 操作记录的同一字段同步加宽,避免多张单号拼接超长导致插入失败
-- 以下结构升级支持重复执行
-- ① blade_invoice_application_sheet.kingdee_bill_no
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_application_sheet` ADD COLUMN `kingdee_bill_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶开票申请单编号(本张发票对应)'' AFTER `invoice_amount`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_application_sheet'
AND COLUMN_NAME = 'kingdee_bill_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ② blade_invoice_application.kingdee_bill_no 加宽
SET @ddl_sql = (
SELECT IF(CHARACTER_MAXIMUM_LENGTH < 500,
'ALTER TABLE `blade_invoice_application` MODIFY COLUMN `kingdee_bill_no` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶开票申请单编号(多张发票逗号拼接)''',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_application'
AND COLUMN_NAME = 'kingdee_bill_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ③ blade_invoice_application_record.kingdee_bill_no 加宽
SET @ddl_sql = (
SELECT IF(CHARACTER_MAXIMUM_LENGTH < 500,
'ALTER TABLE `blade_invoice_application_record` MODIFY COLUMN `kingdee_bill_no` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶开票申请单编号(多张发票逗号拼接)''',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_application_record'
AND COLUMN_NAME = 'kingdee_bill_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,33 @@
-- 收票管理金蝶功能精简:删除 blade_invoice_receipt 冗余同步字段
-- kingdee_bill_no 金蝶单据号(冗余,等于票池镜像 serialNo,可经 kingdee_invoice_pool_id 关联查询)
-- kingdee_status 金蝶同步状态(冗余,手动同步功能已下线,推送状态 kingdee_push_status 为唯一状态源)
-- 金蝶状态展示口径:kingdee_push_status 四态经 kingdeePushStatusName 展示(未推送/已保存收票单/已指定应付/推送失败)
-- 以下结构升级支持重复执行
-- ① blade_invoice_receipt.kingdee_bill_no
SET @ddl_sql = (
SELECT IF(COUNT(*) > 0,
'ALTER TABLE `blade_invoice_receipt` DROP COLUMN `kingdee_bill_no`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_receipt'
AND COLUMN_NAME = 'kingdee_bill_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ② blade_invoice_receipt.kingdee_status(索引 idx_invoice_receipt_kingdee 随列自动删除)
SET @ddl_sql = (
SELECT IF(COUNT(*) > 0,
'ALTER TABLE `blade_invoice_receipt` DROP COLUMN `kingdee_status`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_receipt'
AND COLUMN_NAME = 'kingdee_status'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,94 @@
-- 收票管理 图05 环节②③:推送金蝶收票单(A303-2 收票单批量保存 + A302-6 收票指定应付 + A801-3 定时回写)
-- 票据池镜像表 blade_kingdee_invoice_pool 新增:
-- invoice_type_code 票易通原始发票类型编码(A801-1 invoiceType,如 26/27,A303-2 invoicetypef7_number 取数)
-- items_json A801-1 发票明细行JSON(品名/数量/单价/金额/税率/税额/税收分类编码,A303-2 分录取数来源)
-- 收票登记表 blade_invoice_receipt 新增:
-- kingdee_received_invoice_no 金蝶收票单单据编号(A303-2 返回/幂等预检命中回填)
-- kingdee_push_status 收票单推送状态(unsynced 未推送 / pushed 已保存 / assigned 已指定应付 / failed 推送失败)
-- kingdee_push_msg 推送失败原因(金蝶侧删除/红冲/查验不通过等异常回收提示人工)
-- kingdee_push_time 最近一次推送/回写时间
-- 以下结构升级支持重复执行
-- ① blade_kingdee_invoice_pool.invoice_type_code
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `invoice_type_code` varchar(8) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''票易通原始发票类型编码(A801-1 invoiceType,如26/27)'' AFTER `invoice_type`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'invoice_type_code'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ② blade_kingdee_invoice_pool.items_json
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `items_json` longtext COLLATE utf8mb4_general_ci COMMENT ''A801-1发票明细行JSON(A303-2分录取数来源)'' AFTER `attachments_json`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'items_json'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ③ blade_invoice_receipt.kingdee_received_invoice_no
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_receipt` ADD COLUMN `kingdee_received_invoice_no` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶收票单单据编号(A303-2生成)'' AFTER `kingdee_bill_no`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_receipt'
AND COLUMN_NAME = 'kingdee_received_invoice_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ④ blade_invoice_receipt.kingdee_push_status
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_receipt` ADD COLUMN `kingdee_push_status` varchar(16) COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''unsynced'' COMMENT ''收票单推送状态(unsynced未推送/pushed已保存/assigned已指定应付/failed推送失败)'' AFTER `kingdee_status`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_receipt'
AND COLUMN_NAME = 'kingdee_push_status'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ⑤ blade_invoice_receipt.kingdee_push_msg
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_receipt` ADD COLUMN `kingdee_push_msg` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''收票单推送失败原因'' AFTER `kingdee_push_status`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_receipt'
AND COLUMN_NAME = 'kingdee_push_msg'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ⑥ blade_invoice_receipt.kingdee_push_time
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_receipt` ADD COLUMN `kingdee_push_time` datetime DEFAULT NULL COMMENT ''最近一次收票单推送/回写时间'' AFTER `kingdee_push_msg`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_receipt'
AND COLUMN_NAME = 'kingdee_push_time'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,75 @@
-- ① blade_kingdee_attachment_sync 建表【修复索引超长问题】
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'CREATE TABLE `blade_kingdee_attachment_sync` (
`id` bigint NOT NULL COMMENT ''主键'',
`tenant_id` varchar(12) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''租户ID'',
`biz_type` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT ''业务单据类型(pay_application/formal_settlement_ap/formal_settlement_ar/invoice_receipt/invoice_application)'',
`biz_id` bigint NOT NULL COMMENT ''业务单据id'',
`attachment_key` varchar(768) COLLATE utf8mb4_general_ci NOT NULL COMMENT ''附件幂等键(用户附件=附件url;发票文件=A801-2的path)'',
`attachment_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''附件名称(展示/排查用)'',
`kingdee_path` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶侧文件路径(A902 返回 data.path)'',
`sync_status` tinyint NOT NULL DEFAULT 0 COMMENT ''同步状态:1 成功 / 0 失败'',
`fail_reason` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''失败原因(超限/下载失败/金蝶报错)'',
`sync_time` datetime DEFAULT NULL COMMENT ''最近上传时间'',
`create_user` bigint DEFAULT NULL COMMENT ''创建人'',
`create_dept` bigint DEFAULT NULL COMMENT ''创建部门'',
`create_time` datetime DEFAULT NULL COMMENT ''创建时间'',
`update_user` bigint DEFAULT NULL COMMENT ''修改人'',
`update_time` datetime DEFAULT NULL COMMENT ''修改时间'',
`status` int DEFAULT NULL COMMENT ''业务状态'',
`is_deleted` int DEFAULT 0 COMMENT ''是否已删除'',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_attachment` (`biz_type`,`biz_id`,`attachment_key`(256)),
KEY `idx_biz_type_biz_id` (`biz_type`,`biz_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT=''金蝶附件同步表(A902)''',
'SELECT 1')
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_attachment_sync'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ② blade_formal_settlement.kingdee_bill_id
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_bill_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶单据id(星瀚内部主键,A902 附件上传定位用)'' AFTER `kingdee_bill_no`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_formal_settlement'
AND COLUMN_NAME = 'kingdee_bill_id'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ③ blade_invoice_receipt.kingdee_received_invoice_id
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_receipt` ADD COLUMN `kingdee_received_invoice_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶收票单id(星瀚内部主键,A902 附件上传定位用)'' AFTER `kingdee_received_invoice_no`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_receipt'
AND COLUMN_NAME = 'kingdee_received_invoice_id'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- ④ blade_invoice_application.kingdee_bill_id
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_application` ADD COLUMN `kingdee_bill_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶开票申请单id(星瀚内部主键,A902 附件上传定位用)'' AFTER `kingdee_bill_no`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_application'
AND COLUMN_NAME = 'kingdee_bill_id'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,32 @@
-- 金蝶单据锁定记录表:lockBusiData 锁定 / unLockNotifyBusiData 解锁回调 / 锁定补偿
-- 一行 = 一张金蝶目标单:锁定留痕(人/时间/失败原因)+ 当前锁状态(编辑守卫/回调定位/补偿扫表)
-- lock_status:failed-锁定失败(补偿重试对象)/ locked-金蝶锁定中 / unlocked-已解锁(终态)
CREATE TABLE `blade_kingdee_bill_lock` (
`id` bigint NOT NULL COMMENT '主键',
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户ID',
`source_bill_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'WLTMS单据类型:SETTLEMENT正式结算单/INVOICE_APPLY开票申请单(预留PAY_APPLY付款申请单/RECEIVED_INVOICE收票单)',
`source_bill_id` bigint NOT NULL COMMENT 'WLTMS业务主键',
`source_bill_no` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'WLTMS单号',
`target_bill_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '金蝶单据类型:ApFin_other_BT_S应付单/arfin_other_BT_S应收单/cdm_paybill_bank_elect_BT_S开票单等固定值',
`target_bill_no` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '金蝶单据编号(开票申请按发票张次逐张一行)',
`lock_status` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '锁状态:failed锁定失败/locked金蝶锁定中/unlocked已解锁',
`operation_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '最近动作来源操作类型:SAVE单据新增/DELETE单据删除',
`try_count` int DEFAULT '0' COMMENT '锁定重试次数(补偿任务用,超上限停试)',
`lock_time` datetime DEFAULT NULL COMMENT '锁定时间',
`unlock_time` datetime DEFAULT NULL COMMENT '解锁时间',
`lock_operator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '推单发起人(锁定操作人)',
`notify_operator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '回调操作人(金蝶侧操作人)',
`fail_reason` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '锁定失败原因',
`create_user` bigint DEFAULT NULL COMMENT '创建人',
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` bigint DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
`status` int DEFAULT NULL COMMENT '状态',
`is_deleted` int DEFAULT '0' COMMENT '是否已删除',
PRIMARY KEY (`id`),
INDEX `idx_kbl_source` (`source_bill_type`, `source_bill_id`),
INDEX `idx_kbl_target` (`target_bill_type`, `target_bill_no`),
INDEX `idx_kbl_status` (`lock_status`, `try_count`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '金蝶单据锁定记录' ROW_FORMAT = DYNAMIC;
@@ -0,0 +1,61 @@
-- 收票管理:金蝶全票池发票同步(A801-1 查询 + 批量同步 + 定时绑定关联)
-- 票据池镜像表新增金蝶票池字段:
-- serial_no 发票流水号(票池唯一标识,A801-1 serialNo,同步回填 kingdee_bill_no)
-- buyer_tax_no 购方税号(A801-1 buyerTaxNo)
-- saler_tax_no 销方税号(A801-1 salerTaxNo)
-- check_status 查验状态(A801-1 checkStatus,0 未查验 / 1 通过 / 2 不通过)
-- 由手动批量同步与定时绑定任务按 tenant_id + invoice_no upsert 回填;
-- 发票号码唯一索引 uk_kingdee_invoice_pool_no(tenant_id, invoice_no)建表时已存在,无需新增
-- 以下结构升级支持重复执行
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `serial_no` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶发票流水号(票池唯一标识)'' AFTER `invoice_no`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'serial_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `buyer_tax_no` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''购方税号'' AFTER `receiver_name`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'buyer_tax_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `saler_tax_no` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''销方税号'' AFTER `issuer_name`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'saler_tax_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `check_status` varchar(8) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''查验状态(0未查验/1通过/2不通过)'' AFTER `kingdee_status`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'check_status'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,46 @@
-- 金蝶票池选票回切镜像读:镜像表新增可用过滤列 + 认领过滤索引
-- available_amount 剩余可用金额(A801-1 availableAmount,金蝶可用税额口径,选票本地过滤 >0)
-- amount_use_status 金额使用状态(A801-1 amountUseStatus,'0' 未使用可选 / '2' 全部使用)
-- 由定时同步(InvoiceReceiptKingdeeSyncTask)/ 手动同步端点 / 票号回源 / 收票保存重查 统一按 tenant_id + invoice_no upsert 回填;
-- 存量存根行两列为 NULL,不参与选票(可用过滤为 NULL 排除),随增量窗口同步自然回填
-- 认领过滤 EXISTS 子查询补 (tenant_id, invoice_no) 索引,避免统一查询全表扫
-- 以下结构升级支持重复执行
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `available_amount` decimal(18,2) DEFAULT NULL COMMENT ''剩余可用金额(A801-1 availableAmount,金蝶可用税额口径)'' AFTER `source_updated_time`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'available_amount'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `amount_use_status` varchar(8) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金额使用状态(0 未使用 / 2 全部使用)'' AFTER `available_amount`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'amount_use_status'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @idx_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_formal_settlement_invoice` ADD INDEX `idx_settlement_invoice_no` (`tenant_id`, `invoice_no`)',
'SELECT 1')
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_formal_settlement_invoice'
AND INDEX_NAME = 'idx_settlement_invoice_no'
);
PREPARE idx_stmt FROM @idx_sql;
EXECUTE idx_stmt;
DEALLOCATE PREPARE idx_stmt;
@@ -0,0 +1,17 @@
-- 金蝶票池镜像降级为登记存根(2026-09-22 票池改回实时查询收尾)
-- 背景:选票链路已改为实时查询 A801-1,blade_kingdee_invoice_pool 仅保留被收票登记引用的镜像行(登记存根),
-- 历史全量同步落库的未被引用镜像行属于退役数据,予以清理。
-- 幂等:可重复执行,第二次影响 0 行。
-- 注意:执行前必须先向用户报盘点数字并获确认(dev 库清理口径:事务 + 回滚 SQL + mysqldump 文本兜底)。
-- 执行前先跑盘点(只读):
-- SELECT COUNT(*) AS prune_rows FROM blade_kingdee_invoice_pool p
-- LEFT JOIN blade_invoice_receipt r ON r.kingdee_invoice_pool_id = p.id AND r.is_deleted = 0
-- WHERE r.id IS NULL;
-- SELECT COUNT(*) AS keep_rows FROM blade_kingdee_invoice_pool p
-- INNER JOIN blade_invoice_receipt r ON r.kingdee_invoice_pool_id = p.id AND r.is_deleted = 0;
-- 票池镜像降级为登记存根:删除未被任何未删收票登记引用的镜像行(实时查询模式上线前执行)
-- DELETE p FROM blade_kingdee_invoice_pool p
-- LEFT JOIN blade_invoice_receipt r ON r.kingdee_invoice_pool_id = p.id AND r.is_deleted = 0
-- WHERE r.id IS NULL;
@@ -0,0 +1,17 @@
-- 付款管理:金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询)
-- 新增 kingdee_bill_id:保存 A103-1/A103-3 推送返回的星瀚单据内部主键(A902 附件上传等使用);
-- kingdee_bill_no 字段语义调整为保存金蝶单据编号(billno,如 FKSQ-xxxx),由定时回写任务回填
-- 以下结构升级支持重复执行
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_payment_application` ADD COLUMN `kingdee_bill_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶单据id(星瀚内部主键)'' AFTER `kingdee_bill_no`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_payment_application'
AND COLUMN_NAME = 'kingdee_bill_id'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,64 @@
-- 项目立项平台公司多选:ID 逗号分隔,名称顿号分隔
-- 原字段:undertake_dept_id bigint、undertake_dept_name varchar(100)
-- 选择 2 个及以上平台公司时,若未扩容会报 Incorrect integer value / Data truncation
-- 整列索引挂在 varchar(500) 上可能因键长超限导致 ALTER 失败,故先删索引再改前缀索引
DELIMITER $$
DROP PROCEDURE IF EXISTS `upgrade_project_apply_undertake_dept_multi_20261014`$$
CREATE PROCEDURE `upgrade_project_apply_undertake_dept_multi_20261014`()
BEGIN
DECLARE db_name varchar(128);
DECLARE need_modify tinyint DEFAULT 0;
SET db_name = DATABASE();
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = db_name
AND table_name = 'blade_project_apply'
AND column_name = 'undertake_dept_id'
AND (
data_type IN ('bigint', 'int', 'integer')
OR (data_type IN ('varchar', 'char') AND IFNULL(character_maximum_length, 0) < 500)
)
) OR EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = db_name
AND table_name = 'blade_project_apply'
AND column_name = 'undertake_dept_name'
AND data_type IN ('varchar', 'char')
AND IFNULL(character_maximum_length, 0) < 500
) THEN
SET need_modify = 1;
END IF;
IF need_modify = 1 THEN
IF EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = db_name
AND table_name = 'blade_project_apply'
AND index_name = 'idx_project_apply_dept'
) THEN
ALTER TABLE `blade_project_apply` DROP INDEX `idx_project_apply_dept`;
END IF;
ALTER TABLE `blade_project_apply`
MODIFY COLUMN `undertake_dept_id` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '平台公司ID,多个逗号分隔',
MODIFY COLUMN `undertake_dept_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '平台公司,多个顿号分隔';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = db_name
AND table_name = 'blade_project_apply'
AND index_name = 'idx_project_apply_dept'
) THEN
ALTER TABLE `blade_project_apply`
ADD INDEX `idx_project_apply_dept` (`undertake_dept_id`(191)) USING BTREE;
END IF;
END$$
CALL `upgrade_project_apply_undertake_dept_multi_20261014`()$$
DROP PROCEDURE `upgrade_project_apply_undertake_dept_multi_20261014`$$
DELIMITER ;
@@ -18,8 +18,8 @@ CREATE TABLE `blade_project_apply` (
`project_type` varchar(50) DEFAULT NULL COMMENT '项目类型',
`business_dept_id` bigint(20) DEFAULT NULL COMMENT '业务部门ID',
`business_dept_name` varchar(100) DEFAULT NULL COMMENT '业务部门',
`undertake_dept_id` bigint(20) DEFAULT NULL COMMENT '承办部门ID',
`undertake_dept_name` varchar(100) DEFAULT NULL COMMENT '承办部门',
`undertake_dept_id` varchar(500) DEFAULT NULL COMMENT '平台公司ID,多个逗号分隔',
`undertake_dept_name` varchar(500) DEFAULT NULL COMMENT '平台公司,多个顿号分隔',
`project_source` varchar(50) DEFAULT NULL COMMENT '项目由来',
`source_remark` varchar(500) DEFAULT NULL COMMENT '项目由来说明',
`fund_limit` decimal(18,2) DEFAULT NULL COMMENT '项目资金使用额度(万元)',
@@ -60,7 +60,7 @@ CREATE TABLE `blade_project_apply` (
`void_reason` varchar(500) DEFAULT NULL COMMENT '作废原因',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_project_apply_name` (`project_name`, `is_deleted`) USING BTREE,
KEY `idx_project_apply_dept` (`undertake_dept_id`) USING BTREE,
KEY `idx_project_apply_dept` (`undertake_dept_id`(191)) USING BTREE,
KEY `idx_project_apply_status` (`approval_status`) USING BTREE,
KEY `idx_project_apply_create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目立项';
@@ -0,0 +1,103 @@
-- 收款认领金蝶对接(A301-2 认领公告同步 / A301-3 认领结果推送 / A301-1 收款单回写)
-- 支持重复执行,兼容已执行 DDL 的场景
-- 1. 收款流水表新增金蝶认领公告字段
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `bill_id` bigint(20) DEFAULT NULL COMMENT ''金蝶认领公告id(A301-2返回,A301-3推送必填)'' AFTER `source_updated_time`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'bill_id'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `payer_id` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶付款人id(A301-3推送用)'' AFTER `bill_id`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'payer_id'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `kingdee_claim_status` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''公告认领状态(A301-2 claimstatus:0未认领/1已认领)'' AFTER `payer_id`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'kingdee_claim_status'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `claim_me` tinyint(1) DEFAULT NULL COMMENT ''我已认领(A301-2 claimme)'' AFTER `kingdee_claim_status`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'claim_me'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `unclaimed_amount` decimal(18,2) NOT NULL DEFAULT ''0.00'' COMMENT ''未认领金额(A301-2 unclaimamount)'' AFTER `claim_me`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'unclaimed_amount'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `fee` decimal(18,2) DEFAULT NULL COMMENT ''手续费(A301-2 fee)'' AFTER `unclaimed_amount`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'fee'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `payee_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''收款人(A301-2 recpayee)'' AFTER `fee`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'payee_name'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 2. 认领主单新增来源单据编码(A301-3 awrp_src_code,A301-1 回写匹配键)
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_receipt_claim` ADD COLUMN `awrp_src_code` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶认领结果推送来源单据编码(awrp_src_code)'' AFTER `kingdee_bill_status`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND COLUMN_NAME = 'awrp_src_code'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_receipt_claim` ADD KEY `idx_receipt_claim_src_code` (`tenant_id`,`awrp_src_code`)',
'SELECT 1')
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND INDEX_NAME = 'idx_receipt_claim_src_code'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,136 @@
-- 收款认领推送金蝶重构(认领本地化 / A301-3 定时+手动推送 / 按流水归集)
-- 支持重复执行,兼容 20260924 脚本已执行/未执行的环境,与该脚本无执行顺序要求
-- 变更点:
-- 1. 收款流水表新增推送域字段(推送状态/失败原因/推送时间/推送编码/金蝶收款单号/回写状态)
-- 2. 认领主单移除 awrp_src_code 及其索引(推送编码上移至流水:分摊结算单号拼接)
-- 3. 收款流水表新增定时任务扫描索引与 A301-1 回写匹配索引
-- 1. 收款流水表:推送状态
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `push_status` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''none'' COMMENT ''推送状态(none未推送/pushed已推送/failed推送失败)''',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'push_status'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 2. 收款流水表:最近一次推送失败原因
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `push_fail_reason` varchar(1000) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''最近一次推送失败原因(成功后清空)''',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'push_fail_reason'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 3. 收款流水表:首次推送成功时间
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `pushed_time` datetime DEFAULT NULL COMMENT ''首次推送金蝶成功时间''',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'pushed_time'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 4. 收款流水表:推送来源单据编码(A301-3 awrp_src_code,分摊结算单号拼接,A301-1 回写匹配键)
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `awrp_src_code` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶推送来源单据编码(分摊结算单号下划线拼接,A301-1回写匹配键)''',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'awrp_src_code'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 5. 收款流水表:金蝶收款单号(推送响应回填 / A301-1 回写推进)
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `kingdee_bill_no` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶收款单号''',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'kingdee_bill_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 6. 收款流水表:金蝶回写状态(A301-1 回写生命周期)
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `kingdee_bill_status` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''none'' COMMENT ''金蝶回写状态(none未推送/pushed已推送/approved审核通过/received已收款/failed失败)''',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'kingdee_bill_status'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 7. 认领主单:删除 awrp_src_code 索引(列上移至流水表;0924 未执行过则本就无此索引)
SET @ddl_sql = (
SELECT IF(COUNT(*) > 0,
'ALTER TABLE `blade_receipt_claim` DROP KEY `idx_receipt_claim_src_code`',
'SELECT 1')
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND INDEX_NAME = 'idx_receipt_claim_src_code'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 8. 认领主单:删除 awrp_src_code 列(0924 未执行过则本就无此列)
SET @ddl_sql = (
SELECT IF(COUNT(*) > 0,
'ALTER TABLE `blade_receipt_claim` DROP COLUMN `awrp_src_code`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND COLUMN_NAME = 'awrp_src_code'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 9. 收款流水表:定时任务扫描索引(push_status IN (none,failed) AND claim_status='claimed' AND bill_id 非空)
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD KEY `idx_kingdee_receipt_flow_push_scan` (`tenant_id`,`push_status`,`claim_status`)',
'SELECT 1')
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND INDEX_NAME = 'idx_kingdee_receipt_flow_push_scan'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 10. 收款流水表:A301-1 回写匹配索引(sourcebillnumber = awrp_src_code)
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_receipt_flow` ADD KEY `idx_kingdee_receipt_flow_src_code` (`tenant_id`,`awrp_src_code`)',
'SELECT 1')
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND INDEX_NAME = 'idx_kingdee_receipt_flow_src_code'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
-- 11. 按钮权限:收款流水页「推送金蝶」(父级菜单 2090000000000001260 收款流水,排序 4)
INSERT INTO `blade_menu`
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
VALUES
(2090000000000001264,2090000000000001260,'receipt_flow_push','推送金蝶','receipt_flow_push','','',4,2,0,1,NULL,'',0)
ON DUPLICATE KEY UPDATE
`name`=VALUES(`name`),
`alias`=VALUES(`alias`),
`is_deleted`=0;
@@ -0,0 +1,60 @@
-- 导入结算单批次表 + 菜单
CREATE TABLE IF NOT EXISTS `blade_settlement_import_batch` (
`id` bigint NOT NULL COMMENT '主键',
`tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID',
`batch_no` varchar(64) NOT NULL COMMENT '批次号 DJS+yyyyMMdd+序号',
`project_id` bigint DEFAULT NULL COMMENT '项目ID',
`project_name` varchar(200) DEFAULT NULL COMMENT '项目名称',
`settlement_type` varchar(30) NOT NULL COMMENT 'receivable/payable',
`settlement_bill_type` varchar(20) NOT NULL DEFAULT 'formal' COMMENT '结算单类型 formal/pre',
`customer_contract_id` bigint DEFAULT NULL COMMENT '客户合同ID',
`customer_contract_name` varchar(200) DEFAULT NULL COMMENT '客户合同名称',
`customer_name` varchar(200) DEFAULT NULL COMMENT '客户名称',
`carrier_contract_id` bigint DEFAULT NULL COMMENT '承运商合同ID',
`carrier_contract_name` varchar(200) DEFAULT NULL COMMENT '承运商合同名称',
`carrier_name` varchar(200) DEFAULT NULL COMMENT '承运商名称',
`waybill_mode` varchar(20) DEFAULT 'new' COMMENT '运单模式 new/history',
`import_status` varchar(32) NOT NULL COMMENT 'draft/completed/voided',
`waybill_count` int DEFAULT 0 COMMENT '运单数量',
`detail_count` int DEFAULT 0 COMMENT '明细数量',
`formal_settlement_id` bigint DEFAULT NULL COMMENT '正式结算单ID',
`formal_settlement_no` varchar(100) DEFAULT NULL COMMENT '正式结算单号',
`pre_settlement_id` bigint DEFAULT NULL COMMENT '预结算单ID',
`pre_settlement_no` varchar(100) DEFAULT NULL COMMENT '预结算单号',
`fee_item_names_json` longtext COMMENT '动态费用列名JSON数组',
`remark` varchar(200) DEFAULT NULL COMMENT '备注',
`waybill_import_batch_id` bigint DEFAULT NULL COMMENT '关联底层运单导入批次,可空',
`create_user` bigint DEFAULT NULL COMMENT '创建人',
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`status` int DEFAULT 1 COMMENT '业务状态',
`is_deleted` int DEFAULT 0 COMMENT '是否已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_settlement_import_batch_no` (`tenant_id`,`batch_no`),
KEY `idx_settlement_import_project` (`project_id`),
KEY `idx_settlement_import_formal` (`formal_settlement_id`),
KEY `idx_settlement_import_pre` (`pre_settlement_id`),
KEY `idx_settlement_import_waybill_batch` (`waybill_import_batch_id`),
KEY `idx_settlement_import_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='导入结算单批次';
-- 已建表环境增量字段(列已存在可忽略报错)
ALTER TABLE `blade_settlement_import_batch` ADD COLUMN `settlement_bill_type` varchar(20) NOT NULL DEFAULT 'formal' COMMENT '结算单类型 formal/pre' AFTER `settlement_type`;
ALTER TABLE `blade_settlement_import_batch` ADD COLUMN `pre_settlement_id` bigint DEFAULT NULL COMMENT '预结算单ID' AFTER `formal_settlement_no`;
ALTER TABLE `blade_settlement_import_batch` ADD COLUMN `pre_settlement_no` varchar(100) DEFAULT NULL COMMENT '预结算单号' AFTER `pre_settlement_id`;
ALTER TABLE `blade_settlement_import_batch` ADD KEY `idx_settlement_import_pre` (`pre_settlement_id`);
-- 运单关联导入结算批次(可选,若列已存在可忽略报错)
ALTER TABLE `blade_waybill` ADD COLUMN `settlement_import_batch_id` bigint DEFAULT NULL COMMENT '导入结算单批次ID';
ALTER TABLE `blade_waybill` ADD KEY `idx_waybill_settlement_import_batch_id` (`settlement_import_batch_id`);
INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES
(2090000000000002080,2090000000000001000,'settlement_import','导入结算单','settlement_import','/settlement/settlement-import','iconfont icon-caidanguanli',6,1,0,1,NULL,'',0),
(2090000000000002081,2090000000000002080,'settlement_import_view','查看','settlement_import_view','','',1,2,0,1,NULL,'',0),
(2090000000000002082,2090000000000002080,'settlement_import_add','新增','settlement_import_add','','',2,2,0,1,NULL,'',0),
(2090000000000002083,2090000000000002080,'settlement_import_detail','详情','settlement_import_detail','','',3,2,0,1,NULL,'',0),
(2090000000000002084,2090000000000002080,'settlement_import_void','作废','settlement_import_void','','',4,2,0,1,NULL,'',0),
(2090000000000002085,2090000000000002080,'settlement_import_template','下载模板','settlement_import_template','','',5,2,0,1,NULL,'',0)
ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0;
@@ -0,0 +1,23 @@
-- 结算单项目预付核销明细
CREATE TABLE IF NOT EXISTS `blade_settlement_project_advance_write_off` (
`id` bigint NOT NULL COMMENT '主键',
`tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID',
`settlement_type` varchar(20) NOT NULL COMMENT '结算单类型 pre/formal',
`settlement_id` bigint NOT NULL COMMENT '预结算/正式结算单ID',
`payment_application_id` bigint NOT NULL COMMENT '项目预付付款申请ID',
`payment_no` varchar(64) DEFAULT NULL COMMENT '预付单号',
`project_advance_amount` decimal(18,2) DEFAULT 0.00 COMMENT '项目预付金额',
`write_off_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '本次核销金额',
`kingdee_bill_no` varchar(100) DEFAULT NULL COMMENT '金蝶预付单号',
`create_user` bigint DEFAULT NULL COMMENT '创建人',
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`status` int DEFAULT 1 COMMENT '业务状态',
`is_deleted` int DEFAULT 0 COMMENT '是否已删除',
PRIMARY KEY (`id`),
KEY `idx_spa_write_off_settlement` (`settlement_type`,`settlement_id`),
KEY `idx_spa_write_off_payment` (`payment_application_id`),
KEY `idx_spa_write_off_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='结算单项目预付核销';
@@ -0,0 +1,9 @@
-- 用户多部门聚合字段扩容,避免 OA/IAM 同步时 dept_id 写入超长导致 Data truncation
-- 部署前先执行本脚本,再发布后端
ALTER TABLE `blade_user`
MODIFY COLUMN `dept_id` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门id';
-- 若库中已有 dept_codes 列,请同步执行以下语句(无该列时可跳过)
ALTER TABLE `blade_user`
MODIFY COLUMN `dept_codes` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门编码';
@@ -0,0 +1,5 @@
-- 用户手机号字段扩容:SM2 加密后 Hex 远超 varchar(45),避免 IAM/OA 同步插入失败
-- 部署前先执行本脚本,再发布后端
ALTER TABLE `blade_user`
MODIFY COLUMN `phone` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机';
@@ -0,0 +1,17 @@
-- 付款管理:金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询)
-- 新增 kingdee_bill_id:保存 A103-1/A103-3 推送返回的星瀚单据内部主键(A902 附件上传等使用);
-- kingdee_bill_no 字段语义调整为保存金蝶单据编号(billno,如 FKSQ-xxxx),由定时回写任务回填
-- 以下结构升级支持重复执行
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_payment_application` ADD COLUMN `kingdee_bill_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶单据id(星瀚内部主键)'' AFTER `kingdee_bill_no`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_payment_application'
AND COLUMN_NAME = 'kingdee_bill_id'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,16 @@
-- 客商档案:金蝶(星瀚)协同字段
-- kingdee_status:none未推送 / exist金蝶已有档案(A902-1查到已审核,跳过推送) / synced已推送成功 / failed推送失败或金蝶审核中
-- 金蝶客户编码与 TMS 客商编号(customer_code)一致(A902-2 number 传入客商编号),无需单独存储
ALTER TABLE `blade_customer_archive`
ADD COLUMN `kingdee_status` varchar(20) NOT NULL DEFAULT 'none' COMMENT '金蝶协同状态:none未推送/exist金蝶已有/synced已推送/failed推送失败' AFTER `approved_time`,
ADD COLUMN `kingdee_fail_reason` varchar(500) NULL DEFAULT NULL COMMENT '金蝶推送失败原因' AFTER `kingdee_status`,
ADD COLUMN `kingdee_sync_time` datetime NULL DEFAULT NULL COMMENT '金蝶推送时间' AFTER `kingdee_fail_reason`;
CREATE INDEX idx_kingdee_status ON blade_customer_archive (kingdee_status);
-- 列表「同步金蝶」按钮权限(审批通过后推送失败的重推入口;parent_id 通过菜单 code 关联,避免写死 id)
INSERT INTO blade_menu (id, parent_id, code, name, alias, path, source, sort, category, action, is_open, component, remark, is_deleted)
SELECT 2075431800000000001, m.id, 'customer_archive_sync', '同步金蝶', 'customer_archive_sync', '', '', 1, 2, 0, 1, '', '', 0
FROM blade_menu m
WHERE m.code = 'customer_archive' AND m.is_deleted = 0;
@@ -0,0 +1,61 @@
-- 收票管理:金蝶全票池发票同步(A801-1 查询 + 批量同步 + 定时绑定关联)
-- 票据池镜像表新增金蝶票池字段:
-- serial_no 发票流水号(票池唯一标识,A801-1 serialNo,同步回填 kingdee_bill_no)
-- buyer_tax_no 购方税号(A801-1 buyerTaxNo)
-- saler_tax_no 销方税号(A801-1 salerTaxNo)
-- check_status 查验状态(A801-1 checkStatus,0 未查验 / 1 通过 / 2 不通过)
-- 由手动批量同步与定时绑定任务按 tenant_id + invoice_no upsert 回填;
-- 发票号码唯一索引 uk_kingdee_invoice_pool_no(tenant_id, invoice_no)建表时已存在,无需新增
-- 以下结构升级支持重复执行
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `serial_no` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶发票流水号(票池唯一标识)'' AFTER `invoice_no`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'serial_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `buyer_tax_no` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''购方税号'' AFTER `receiver_name`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'buyer_tax_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `saler_tax_no` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''销方税号'' AFTER `issuer_name`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'saler_tax_no'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_kingdee_invoice_pool` ADD COLUMN `check_status` varchar(8) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''查验状态(0未查验/1通过/2不通过)'' AFTER `kingdee_status`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_kingdee_invoice_pool'
AND COLUMN_NAME = 'check_status'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;
@@ -0,0 +1,21 @@
-- 应收结算与开票申请同步金蝶(A303-1 开票申请推送)
-- 开票申请商品行新增字段:
-- tax_classification_code 税收分类编码(开票项目主数据 blade_invoice_item.tax_classification_code,
-- 新增/编辑开票申请选择开票内容时快照落库,A303-1 分录 taxratecodeid.number 取数来源)
-- 销方名称/销方税号不走 Nacos 配置与表结构:销方名称取开票申请开票单位快照(issuer_name,结算单收款方带出),
-- 销方税号按开票单位查客商档案统一社会信用代码(unified_credit_code)
-- 开票申请基础建表脚本 blade_invoice_application_20260821.sql 保持不动,本脚本支持重复执行
-- 以下结构升级支持重复执行
SET @ddl_sql = (
SELECT IF(COUNT(*) = 0,
'ALTER TABLE `blade_invoice_application_line` ADD COLUMN `tax_classification_code` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''税收分类编码(开票内容快照)'' AFTER `goods_name`',
'SELECT 1')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blade_invoice_application_line'
AND COLUMN_NAME = 'tax_classification_code'
);
PREPARE ddl_stmt FROM @ddl_sql;
EXECUTE ddl_stmt;
DEALLOCATE PREPARE ddl_stmt;

Some files were not shown because too many files have changed in this diff Show More