17 KiB
金蝶附件同步(A902)设计文档
- 日期:2026-09-24
- 范围:后端同步链路联带 A902 附件上传(付款申请、正式结算应付/应收、收票登记、开票申请)+ 前端状态列/提示/超时调整
- 背景:单据推送金蝶成功后,TMS 侧附件(attachmentsJson)不随单上行,金蝶单据缺影像。星瀚 A902「附件上传到指定单据」接口已具备(见《业财融合系统接口文档.xlsx》同名页签),此前未在 BFI 客户端实现
- 前置:原计划复用 2026-09-24-kingdee-push-timeout-anticlick 的 loading+守卫+try/finally 模式;经评审确认该前置未落地,本期不实施防连点(后补),本设计仍不新增前端状态机
一、目标
- 五类单据推送金蝶成功后,后端联带逐个上传附件至金蝶对应单据附件面板(A902)
- 收票登记含发票文件:票池镜像附件 path → A801-2 下载 → A902 回传收票单
- 附件同步独立状态、失败可补传(重推自动补);前端列表列 + message 可见
- 客商档案预留 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,bodypath(镜像 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<成功数<附件数→部分未传;成功数≥附件数→已上传。
不变式
- 附件上传必须晚于主单推送成功、拿到并落库 billPkId 之后
- 附件失败永不回滚、不污染主单同步状态(逐附件独立 try/catch)
- 同
(biz_type, biz_id, attachment_key)最多一条成功记录 - 不自动重试;重推主单时自动补传(主单被幂等预检跳过不影响附件补传分支)
- 单文件软限制 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 发票文件在金蝶侧的可预览性验证
七、待确认(金蝶侧)
- A902 单文件大小上限、允许格式(当前后端软限制 20MB 常量)
- 客商的 entityNumber/controlKey
- A902 调用频率限制(批量串行是否需间隔)
- OFD 数电票文件上传收票单附件面板是否可用(主文档 641 行注明「金蝶不支持直接推送数电票 OFD 附件」,需验证 A902 通道例外与否)
八、验证
- 后端:
mvn clean compile -DskipTests -pl(改动模块)——✅ 已通过(bfi-api / transport-api / blade-transport) - 前端:
pnpm run typecheck——⚠️ 仓库既有问题无法运行(vue-tsc 3.3.11 与 typescript@7.0.2 不兼容,TS7 移除./lib/tsc导出,与本特性无关);已用 @vue/compiler-sfc 解析 +node --check校验全部改动 .vue/.js 语法通过 - 手动(用户执行):
- 含附件单据(付款申请/开票申请/正式结算/收票登记)单条/批量同步 → 金蝶对应单据附件面板可见文件
- 同步成功但附件失败(断网/超限模拟)→ 主单状态正常、列表「附件同步」列示未传/部分未传、重推自动补传
- 重推已全部上传的单据 → 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-webaabe67e
术语表
| 术语 | 含义 |
|---|---|
| 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 取回回传 |
| 附件同步聚合态 | 无附件/未上传/部分未传/已上传(列表展示用,运行时聚合) |
| 重推补传 | 主单重复推送(含幂等跳过)时自动补传未成功附件,无新增交互 |