fix: 金蝶附件上传补齐合规附件文件后缀名

合规报文无文件名字段,固定名付款特批附件无后缀被BFI拒收(无后缀名不支持上传);改为解析C103-5响应头Content-Disposition文件名,其次按Content-Type映射扩展名,兜底.pdf;主单已同步时重推同步金蝶自动重试附件上传
This commit is contained in:
刘泉佳
2026-10-27 18:39:28 +08:00
parent 5756cd9b2d
commit 79bdb8fc42
@@ -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;