fix: 金蝶附件上传补齐合规附件文件后缀名
合规报文无文件名字段,固定名付款特批附件无后缀被BFI拒收(无后缀名不支持上传);改为解析C103-5响应头Content-Disposition文件名,其次按Content-Type映射扩展名,兜底.pdf;主单已同步时重推同步金蝶自动重试附件上传
This commit is contained in:
+97
-6
@@ -44,9 +44,11 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
@@ -177,12 +179,14 @@ public class KingdeeAttachmentSyncSupport {
|
||||
throw new IllegalArgumentException("附件超过20MB大小限制("
|
||||
+ formatSize(item.size()) + "),请压缩后重试");
|
||||
}
|
||||
byte[] content = fetchContent(item);
|
||||
FetchedFile fetched = fetchContent(item);
|
||||
byte[] content = fetched.content();
|
||||
if (content.length > KingdeeAttachmentConstant.MAX_FILE_SIZE_BYTES) {
|
||||
throw new IllegalArgumentException("附件超过20MB大小限制("
|
||||
+ formatSize((long) content.length) + "),请压缩后重试");
|
||||
}
|
||||
String fileName = Func.isBlank(item.name()) ? "attachment" : item.name();
|
||||
String fileName = Func.isNotBlank(fetched.fileName()) ? fetched.fileName()
|
||||
: (Func.isBlank(item.name()) ? "attachment" : item.name());
|
||||
BfiAttachmentUploadVO uploadVO = bfiService.uploadAttachment(entityNumber, billPkId,
|
||||
KingdeeAttachmentConstant.CONTROL_KEY, fileName, content);
|
||||
log.info("金蝶附件上传成功:业务单据[{}:{}],附件[{}],kingdeePath={}", bizType, bizId, item.name(), uploadVO.getPath());
|
||||
@@ -384,15 +388,24 @@ public class KingdeeAttachmentSyncSupport {
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件内容拉取结果(合规附件按 C103-5 响应头解析真实文件名,其余来源 fileName 为 null 沿用原名称)
|
||||
*
|
||||
* @param content 附件内容
|
||||
* @param fileName 解析出的文件名(可空)
|
||||
*/
|
||||
private record FetchedFile(byte[] content, String fileName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取附件内容:合规附件经 C103-5 按合规附件ID 下载(流式读取,防大文件全量进内存失控);
|
||||
* 星瀚侧文件经 A801-2 下载(流式读取);用户附件按 http(s) url 下载(流式读取)。调用方负责按 20MB 上限校验
|
||||
*/
|
||||
private byte[] fetchContent(KingdeeAttachmentItem item) throws Exception {
|
||||
private FetchedFile fetchContent(KingdeeAttachmentItem item) throws Exception {
|
||||
if (item.hcsFdIdSource()) {
|
||||
Response response = hcsAttachmentSupport.downloadRaw(item.key());
|
||||
try (InputStream inputStream = response.body().asInputStream()) {
|
||||
return readCapped(inputStream);
|
||||
return new FetchedFile(readCapped(inputStream), resolveHcsFileName(response, item.name()));
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
@@ -400,7 +413,7 @@ public class KingdeeAttachmentSyncSupport {
|
||||
if (item.kingdeePathSource()) {
|
||||
Response response = bfiService.downloadInvoice(item.key());
|
||||
try (InputStream inputStream = response.body().asInputStream()) {
|
||||
return readCapped(inputStream);
|
||||
return new FetchedFile(readCapped(inputStream), null);
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
@@ -414,10 +427,88 @@ public class KingdeeAttachmentSyncSupport {
|
||||
throw new IllegalStateException("附件下载失败,HTTP状态码 " + response.statusCode());
|
||||
}
|
||||
try (InputStream inputStream = response.body()) {
|
||||
return readCapped(inputStream);
|
||||
return new FetchedFile(readCapped(inputStream), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析合规附件文件名(BFI 附件上传要求带后缀名):名称已带后缀原样返回;
|
||||
* 否则优先取 C103-5 响应头 Content-Disposition 文件名,其次按 Content-Type 映射扩展名追加,兜底 .pdf
|
||||
*
|
||||
* @param response C103-5 文件流响应
|
||||
* @param fallbackName 兜底名称(无扩展名)
|
||||
* @return 带后缀名的文件名
|
||||
*/
|
||||
private String resolveHcsFileName(Response response, String fallbackName) {
|
||||
String name = Func.isBlank(fallbackName) ? "付款特批附件" : fallbackName.trim();
|
||||
if (name.lastIndexOf('.') > 0) {
|
||||
return name;
|
||||
}
|
||||
String fromDisposition = fileNameFromDisposition(firstHeader(response, "Content-Disposition"));
|
||||
if (Func.isNotBlank(fromDisposition) && fromDisposition.lastIndexOf('.') > 0) {
|
||||
return fromDisposition;
|
||||
}
|
||||
return name + extensionOfContentType(firstHeader(response, "Content-Type"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应头 Content-Disposition 文件名提取(容忍 filename*=UTF-8''xxx 与 filename="xxx" 两种形式,异常返回 null)
|
||||
*/
|
||||
private String fileNameFromDisposition(String disposition) {
|
||||
if (Func.isBlank(disposition)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String lower = disposition.toLowerCase();
|
||||
int index = lower.indexOf("filename*=");
|
||||
if (index >= 0) {
|
||||
String value = disposition.substring(index + "filename*=".length());
|
||||
int quote = value.indexOf("''");
|
||||
value = quote >= 0 ? value.substring(quote + 2) : value;
|
||||
return URLDecoder.decode(value.replaceAll("\"", "").trim(), StandardCharsets.UTF_8);
|
||||
}
|
||||
index = lower.indexOf("filename=");
|
||||
if (index >= 0) {
|
||||
return URLDecoder.decode(disposition.substring(index + "filename=".length())
|
||||
.replaceAll("\"", "").trim(), StandardCharsets.UTF_8);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 响应头格式异常按无文件名处理,走 Content-Type 扩展名兜底
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 响应头取值(key 忽略大小写,取首个非空值) */
|
||||
private String firstHeader(Response response, String name) {
|
||||
if (response.headers() == null) {
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<String, Collection<String>> header : response.headers().entrySet()) {
|
||||
if (header.getKey() != null && header.getKey().equalsIgnoreCase(name) && Func.isNotEmpty(header.getValue())) {
|
||||
for (String value : header.getValue()) {
|
||||
if (Func.isNotBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Content-Type → 扩展名(特批结果附件常规为 pdf,未识别类型兜底 .pdf) */
|
||||
private String extensionOfContentType(String contentType) {
|
||||
String type = contentType == null ? "" : contentType.split(";")[0].trim().toLowerCase();
|
||||
return switch (type) {
|
||||
case "application/pdf" -> ".pdf";
|
||||
case "image/jpeg" -> ".jpg";
|
||||
case "image/png" -> ".png";
|
||||
case "image/bmp" -> ".bmp";
|
||||
case "image/gif" -> ".gif";
|
||||
case "application/ofd" -> ".ofd";
|
||||
default -> ".pdf";
|
||||
};
|
||||
}
|
||||
|
||||
/** 流式读取并封顶(超出软限制即中断读取,避免大文件占满内存) */
|
||||
private byte[] readCapped(InputStream inputStream) throws IOException {
|
||||
int bufferSize = 64 * 1024;
|
||||
|
||||
Reference in New Issue
Block a user