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