fix: 完善工作流公共接口上传与错误契约
- 区分 HTTP 请求标识与内部上传标识,补齐 Redis 旧记录兼容和关联日志 - 保持归一化 MIME 一致,并隔离工作流鉴权错误契约对其他公共接口的影响 - 收口 Multipart 操作日志与对象存储故障分类,归档范围:S05
This commit is contained in:
@@ -6,12 +6,17 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
import tech.easyflow.common.util.RequestUtil;
|
||||
import tech.easyflow.common.web.error.RequestIdContext;
|
||||
import tech.easyflow.common.web.multipart.MultipartFileMetadataNormalizer;
|
||||
import tech.easyflow.log.annotation.LogReporterDisabled;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -110,10 +115,16 @@ public class ActionReportInterceptor implements HandlerInterceptor {
|
||||
sb.append("EasyFlow action report -------- ").append(timestamp).append(" -------------------------\n");
|
||||
sb.append("Request : ").append(request.getMethod())
|
||||
.append(" ").append(request.getRequestURI()).append("\n");
|
||||
String requestId = RequestIdContext.get(request);
|
||||
if (requestId != null) {
|
||||
sb.append("RequestId : ").append(requestId).append("\n");
|
||||
}
|
||||
|
||||
boolean multipartRequest = isMultipartRequest(request);
|
||||
|
||||
// 打印参数(GET / POST 表单参数),脱敏
|
||||
Map<String, String[]> params = request.getParameterMap();
|
||||
if (!params.isEmpty()) {
|
||||
if (!params.isEmpty() && !multipartRequest) {
|
||||
Map<String, Object> maskedParams = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String[]> entry : params.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
@@ -127,9 +138,17 @@ public class ActionReportInterceptor implements HandlerInterceptor {
|
||||
sb.append("Params : ").append(JSON.toJSONString(maskedParams)).append("\n");
|
||||
}
|
||||
|
||||
if (multipartRequest) {
|
||||
sb.append("Parts : ")
|
||||
.append(buildMultipartSummary(request))
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
// ====== 读取 POST Body ======
|
||||
String methodStr = request.getMethod();
|
||||
if ("POST".equalsIgnoreCase(methodStr) || "PUT".equalsIgnoreCase(methodStr) || "PATCH".equalsIgnoreCase(methodStr)) {
|
||||
if (!multipartRequest && ("POST".equalsIgnoreCase(methodStr)
|
||||
|| "PUT".equalsIgnoreCase(methodStr)
|
||||
|| "PATCH".equalsIgnoreCase(methodStr))) {
|
||||
String body = RequestUtil.readBodyString(request);
|
||||
if (body != null && !body.trim().isEmpty()) {
|
||||
try {
|
||||
@@ -208,8 +227,14 @@ public class ActionReportInterceptor implements HandlerInterceptor {
|
||||
if (ex != null) {
|
||||
sb.append('\n')
|
||||
.append("Status : FAILED\n")
|
||||
.append("Exception : ").append(ex.getClass().getSimpleName())
|
||||
.append(": ").append(ex.getMessage() != null ? ex.getMessage().split("\n")[0] : "Unknown");
|
||||
.append("Exception : ")
|
||||
.append(ex.getClass().getSimpleName());
|
||||
if (requestId == null) {
|
||||
sb.append(": ")
|
||||
.append(ex.getMessage() != null
|
||||
? ex.getMessage().split("\n")[0]
|
||||
: "Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 耗时 ======
|
||||
@@ -258,6 +283,67 @@ public class ActionReportInterceptor implements HandlerInterceptor {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否为 Multipart 表单。
|
||||
*
|
||||
* @param request 当前请求
|
||||
* @return 是否为 Multipart 请求
|
||||
*/
|
||||
private boolean isMultipartRequest(HttpServletRequest request) {
|
||||
String contentType = request.getContentType();
|
||||
return contentType != null
|
||||
&& contentType.regionMatches(
|
||||
true,
|
||||
0,
|
||||
MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
0,
|
||||
MediaType.MULTIPART_FORM_DATA_VALUE.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建不读取文件正文的 Multipart Part 摘要。
|
||||
*
|
||||
* @param request 当前请求
|
||||
* @return JSON 摘要
|
||||
*/
|
||||
String buildMultipartSummary(HttpServletRequest request) {
|
||||
if (!(request instanceof MultipartHttpServletRequest multipart)) {
|
||||
return "{\"available\":false}";
|
||||
}
|
||||
List<Map<String, Object>> parts = new ArrayList<>();
|
||||
multipart.getMultiFileMap().forEach((partName, files) -> {
|
||||
if (files == null || files.isEmpty()) {
|
||||
parts.add(Map.of("partName", partName, "fileCount", 0));
|
||||
return;
|
||||
}
|
||||
for (MultipartFile file : files) {
|
||||
if (file == null) {
|
||||
parts.add(Map.of("partName", partName, "file", "null"));
|
||||
continue;
|
||||
}
|
||||
String filename =
|
||||
MultipartFileMetadataNormalizer.sanitizeFilename(
|
||||
file.getOriginalFilename());
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("partName", partName);
|
||||
summary.put("fileName", filename);
|
||||
summary.put("size", file.getSize());
|
||||
summary.put(
|
||||
"contentType",
|
||||
MultipartFileMetadataNormalizer.normalizeContentType(
|
||||
filename,
|
||||
file.getContentType()));
|
||||
parts.add(summary);
|
||||
}
|
||||
});
|
||||
Set<String> textPartNames = new LinkedHashSet<>(
|
||||
multipart.getParameterMap().keySet());
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("files", parts);
|
||||
summary.put("textPartNames", textPartNames);
|
||||
return JSON.toJSONString(summary);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建方法签名:methodName(paramType paramName, ...)
|
||||
*/
|
||||
@@ -279,4 +365,4 @@ public class ActionReportInterceptor implements HandlerInterceptor {
|
||||
sig.append(")");
|
||||
return sig.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user