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.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URLDecoder;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -177,12 +179,14 @@ public class KingdeeAttachmentSyncSupport {
|
|||||||
throw new IllegalArgumentException("附件超过20MB大小限制("
|
throw new IllegalArgumentException("附件超过20MB大小限制("
|
||||||
+ formatSize(item.size()) + "),请压缩后重试");
|
+ formatSize(item.size()) + "),请压缩后重试");
|
||||||
}
|
}
|
||||||
byte[] content = fetchContent(item);
|
FetchedFile fetched = fetchContent(item);
|
||||||
|
byte[] content = fetched.content();
|
||||||
if (content.length > KingdeeAttachmentConstant.MAX_FILE_SIZE_BYTES) {
|
if (content.length > KingdeeAttachmentConstant.MAX_FILE_SIZE_BYTES) {
|
||||||
throw new IllegalArgumentException("附件超过20MB大小限制("
|
throw new IllegalArgumentException("附件超过20MB大小限制("
|
||||||
+ formatSize((long) content.length) + "),请压缩后重试");
|
+ 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,
|
BfiAttachmentUploadVO uploadVO = bfiService.uploadAttachment(entityNumber, billPkId,
|
||||||
KingdeeAttachmentConstant.CONTROL_KEY, fileName, content);
|
KingdeeAttachmentConstant.CONTROL_KEY, fileName, content);
|
||||||
log.info("金蝶附件上传成功:业务单据[{}:{}],附件[{}],kingdeePath={}", bizType, bizId, item.name(), uploadVO.getPath());
|
log.info("金蝶附件上传成功:业务单据[{}:{}],附件[{}],kingdeePath={}", bizType, bizId, item.name(), uploadVO.getPath());
|
||||||
@@ -384,15 +388,24 @@ public class KingdeeAttachmentSyncSupport {
|
|||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件内容拉取结果(合规附件按 C103-5 响应头解析真实文件名,其余来源 fileName 为 null 沿用原名称)
|
||||||
|
*
|
||||||
|
* @param content 附件内容
|
||||||
|
* @param fileName 解析出的文件名(可空)
|
||||||
|
*/
|
||||||
|
private record FetchedFile(byte[] content, String fileName) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拉取附件内容:合规附件经 C103-5 按合规附件ID 下载(流式读取,防大文件全量进内存失控);
|
* 拉取附件内容:合规附件经 C103-5 按合规附件ID 下载(流式读取,防大文件全量进内存失控);
|
||||||
* 星瀚侧文件经 A801-2 下载(流式读取);用户附件按 http(s) url 下载(流式读取)。调用方负责按 20MB 上限校验
|
* 星瀚侧文件经 A801-2 下载(流式读取);用户附件按 http(s) url 下载(流式读取)。调用方负责按 20MB 上限校验
|
||||||
*/
|
*/
|
||||||
private byte[] fetchContent(KingdeeAttachmentItem item) throws Exception {
|
private FetchedFile fetchContent(KingdeeAttachmentItem item) throws Exception {
|
||||||
if (item.hcsFdIdSource()) {
|
if (item.hcsFdIdSource()) {
|
||||||
Response response = hcsAttachmentSupport.downloadRaw(item.key());
|
Response response = hcsAttachmentSupport.downloadRaw(item.key());
|
||||||
try (InputStream inputStream = response.body().asInputStream()) {
|
try (InputStream inputStream = response.body().asInputStream()) {
|
||||||
return readCapped(inputStream);
|
return new FetchedFile(readCapped(inputStream), resolveHcsFileName(response, item.name()));
|
||||||
} finally {
|
} finally {
|
||||||
response.close();
|
response.close();
|
||||||
}
|
}
|
||||||
@@ -400,7 +413,7 @@ public class KingdeeAttachmentSyncSupport {
|
|||||||
if (item.kingdeePathSource()) {
|
if (item.kingdeePathSource()) {
|
||||||
Response response = bfiService.downloadInvoice(item.key());
|
Response response = bfiService.downloadInvoice(item.key());
|
||||||
try (InputStream inputStream = response.body().asInputStream()) {
|
try (InputStream inputStream = response.body().asInputStream()) {
|
||||||
return readCapped(inputStream);
|
return new FetchedFile(readCapped(inputStream), null);
|
||||||
} finally {
|
} finally {
|
||||||
response.close();
|
response.close();
|
||||||
}
|
}
|
||||||
@@ -414,10 +427,88 @@ public class KingdeeAttachmentSyncSupport {
|
|||||||
throw new IllegalStateException("附件下载失败,HTTP状态码 " + response.statusCode());
|
throw new IllegalStateException("附件下载失败,HTTP状态码 " + response.statusCode());
|
||||||
}
|
}
|
||||||
try (InputStream inputStream = response.body()) {
|
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 {
|
private byte[] readCapped(InputStream inputStream) throws IOException {
|
||||||
int bufferSize = 64 * 1024;
|
int bufferSize = 64 * 1024;
|
||||||
|
|||||||
Reference in New Issue
Block a user