Merge remote-tracking branch 'origin/feature/kingdee-attachment-sync' into feature/payment-apply-bfi-sync

# Conflicts:
#	blade-common/src/main/java/org/springblade/common/feign/CustomFeignLogger.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
#	blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java
This commit is contained in:
刘泉佳
2026-09-28 10:50:09 +08:00
35 changed files with 2452 additions and 38 deletions
@@ -27,7 +27,12 @@ package org.springblade.thirdparty.bfi.config;
import feign.Request;
import feign.RequestInterceptor;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springblade.thirdparty.bfi.interceptor.BfiRequestInterceptor;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.TimeUnit;
@@ -61,4 +66,9 @@ public class BfiFeignClientConfig {
public Request.Options options() {
return new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
}
@Bean
public Encoder feignFormEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
}
@@ -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, consumes = 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;
@@ -533,12 +536,15 @@ public class BfiServiceImpl implements IBfiService {
BfiInvoiceDownloadDTO param = new BfiInvoiceDownloadDTO();
param.setPath(path);
String accessToken = getAccessToken();
long start = System.currentTimeMillis();
log.info("调用BFI接口 进项发票下载开始 path:{}", path);
try {
Response response = client.invoiceDownload(accessToken, param);
if (response == null || response.status() != 200) {
log.error("调用BFI接口 进项发票下载异常响应状态:{}", response == null ? "无响应" : response.status());
throw new BfiException("调用BFI接口进项发票下载异常,响应状态:" + (response == null ? "无响应" : response.status()));
}
log.info("调用BFI接口 进项发票下载结束 耗时:{}ms", System.currentTimeMillis() - start);
return response;
} catch (BfiException e) {
throw e;
@@ -548,6 +554,34 @@ 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);
long start = System.currentTimeMillis();
log.info("调用BFI接口 附件上传开始 entityNumber:{},billPkId:{},文件:{},大小:{}字节",
entityNumber, billPkId, fileName, fileContent.length);
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接口附件上传异常,返回数据为空");
}
log.info("调用BFI接口 附件上传结束 耗时:{}ms,kingdeePath:{}", System.currentTimeMillis() - start, uploadVO.getPath());
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);
}
}
}