✨ 金蝶附件同步(A902):五类单据推送后联带上传附件、独立同步状态与列表聚合

This commit is contained in:
刘泉佳
2026-09-24 13:26:54 +08:00
parent a17dd826ec
commit c2f3a48c0e
28 changed files with 1589 additions and 38 deletions
@@ -48,6 +48,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiVerifyTokenDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiWithdrawTokenDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiArBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiAttachmentUploadVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiClaimAnnounceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO;
@@ -65,11 +66,14 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiTokenVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiVerifyTokenVO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@@ -106,6 +110,7 @@ public interface IBfiClient {
String CLAIM_BILL_PUSH_URL = "${thirdParty.bfi.claimBillPushUrl:/ierp/kapi/v2/awrp/cas/cas/cas_claimbill/claimbillPush}";
String INVOICE_POOL_QUERY_URL = "${thirdParty.bfi.invoicePoolQueryUrl:/ierp/kapi/v2/awrp/rim/rim_invoice/query}";
String INVOICE_DOWNLOAD_URL = "${thirdParty.bfi.invoiceDownloadUrl:/ierp/kapi/v2/awrp/bosext/attachment/download}";
String ATTACHMENT_UPLOAD_URL = "${thirdParty.bfi.attachmentUploadUrl:/ierp/kapi/v2/frame/attachment/uploadFile}";
/**
* A100-1 获取令牌
@@ -451,4 +456,17 @@ public interface IBfiClient {
*/
@PostMapping(INVOICE_DOWNLOAD_URL)
Response invoiceDownload(@RequestHeader(BfiConstant.HEADER_ACCESS_TOKEN) String accessToken, @RequestBody BfiInvoiceDownloadDTO param);
/**
* A902 附件上传到指定单据(multipart/form-data)
*
* @param accessToken 访问令牌,通过 getToken 获取
* @param file 附件文件
* @param attachmentUploadFileArgs 上传参数 JSON 字符串:{"entityNumber":"ap_payapply","billPkId":2386455983883835392,"controlKey":"attachmentpanel"}
* @return 上传结果,data.path 即金蝶侧文件路径
*/
@PostMapping(value = ATTACHMENT_UPLOAD_URL, produces = MediaType.MULTIPART_FORM_DATA_VALUE)
BfiResultVO<BfiAttachmentUploadVO> uploadAttachment(@RequestHeader(BfiConstant.HEADER_ACCESS_TOKEN) String accessToken,
@RequestPart("file") MultipartFile file,
@RequestPart("attachmentUploadFileArgs") String attachmentUploadFileArgs);
}
@@ -0,0 +1,57 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.thirdparty.bfi.pojo.vo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 业财融合(BFI)A902 附件上传返回数据
* <p>
* 响应示例:
* <pre>{@code
* {
* "data": {"path": "211e6ce5c9805c00"},
* "errorCode": "0",
* "status": true
* }
* }</pre>
*
* @author Chill
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class BfiAttachmentUploadVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 金蝶侧文件路径(附件面板回显/排查用)
*/
private String path;
}
@@ -49,6 +49,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiRecBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiArBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiAttachmentUploadVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiClaimAnnounceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO;
@@ -293,4 +294,16 @@ public interface IBfiService {
* @return 文件流响应,调用方负责关闭
*/
Response downloadInvoice(String path);
/**
* A902 附件上传到指定单据
*
* @param entityNumber 星瀚单据标识(如 ap_payapply)
* @param billPkId 星瀚单据内码(保存/查询响应返回的 id)
* @param controlKey 附件面板标识,统一 attachmentpanel
* @param fileName 附件名称(含扩展名)
* @param fileContent 附件内容
* @return 上传结果,path 即金蝶侧文件路径
*/
BfiAttachmentUploadVO uploadAttachment(String entityNumber, String billPkId, String controlKey, String fileName, byte[] fileContent);
}
@@ -73,6 +73,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiVerifyTokenDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiWithdrawTokenDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiArBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiAttachmentUploadVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiClaimAnnounceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO;
@@ -91,6 +92,8 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiTokenVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiVerifyTokenVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil;
import org.springblade.thirdparty.bfi.util.InMemoryMultipartFile;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
@@ -548,6 +551,30 @@ public class BfiServiceImpl implements IBfiService {
}
}
@Override
public BfiAttachmentUploadVO uploadAttachment(String entityNumber, String billPkId, String controlKey, String fileName, byte[] fileContent) {
if (StringUtil.isBlank(entityNumber) || StringUtil.isBlank(billPkId)) {
throw new BfiException("上传BFI附件失败,entityNumber与billPkId不能为空");
}
if (StringUtil.isBlank(fileName) || fileContent == null || fileContent.length == 0) {
throw new BfiException("上传BFI附件失败,附件名称与内容不能为空");
}
String accessToken = getAccessToken();
// A902 参数原文 JSON:{"entityNumber":"ap_payapply","billPkId":2386455983883835392,"controlKey":"attachmentpanel"}
JSONObject args = new JSONObject();
args.set("entityNumber", entityNumber);
args.set("billPkId", billPkId);
args.set("controlKey", StringUtil.isBlank(controlKey) ? "attachmentpanel" : controlKey);
InMemoryMultipartFile file = new InMemoryMultipartFile("file", fileName, MediaType.APPLICATION_OCTET_STREAM_VALUE, fileContent);
BfiResultVO<BfiAttachmentUploadVO> result = commonCall("附件上传", () -> client.uploadAttachment(accessToken, file, args.toString()));
BfiAttachmentUploadVO uploadVO = result.getData();
if (uploadVO == null || StringUtil.isBlank(uploadVO.getPath())) {
log.error("调用BFI接口 附件上传返回数据为空:{}", JSONUtil.toJsonStr(result));
throw new BfiException("调用BFI接口附件上传异常,返回数据为空");
}
return uploadVO;
}
/**
* 公共调用方法,统一校验响应结果
*
@@ -0,0 +1,101 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.thirdparty.bfi.util;
import org.springframework.lang.NonNull;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 内存字节数组版 MultipartFile:A902 附件上传经 Feign multipart 发送时使用,
* 文件内容已整体读入内存(单文件软限制 20MB,见 KingdeeAttachmentConstant)
*
* @author Chill
*/
public class InMemoryMultipartFile implements MultipartFile {
private final String name;
private final String originalFilename;
private final String contentType;
private final byte[] content;
public InMemoryMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
this.name = name;
this.originalFilename = originalFilename;
this.contentType = contentType;
this.content = content == null ? new byte[0] : content;
}
@Override
@NonNull
public String getName() {
return name;
}
@Override
public String getOriginalFilename() {
return originalFilename;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public boolean isEmpty() {
return content.length == 0;
}
@Override
public long getSize() {
return content.length;
}
@Override
@NonNull
public byte[] getBytes() {
return content;
}
@Override
@NonNull
public InputStream getInputStream() {
return new ByteArrayInputStream(content);
}
@Override
public void transferTo(@NonNull File destination) throws IOException {
try (FileOutputStream outputStream = new FileOutputStream(destination)) {
outputStream.write(content);
}
}
}