Files
tms-erp-api/doc/docs/业务流程/2026-09-24-kingdee-attachment-sync-design.md
T

17 KiB
Raw Blame History

金蝶附件同步(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 取回回传
附件同步聚合态 无附件/未上传/部分未传/已上传(列表展示用,运行时聚合)
重推补传 主单重复推送(含幂等跳过)时自动补传未成功附件,无新增交互