- push_record/oplog/oapilog 各加 payload_size/response_size(写时算 UTF-8 字节数),列表只读该整数列、不关联大字段 - 去掉 oplog/oapilog 响应写时截断(全量 GZIP 入库),退役 maxResponseSize 注解属性与常量 - 删除 oapilog 列表逐行 findById+解压回填的 N+1,SysOapiLogListVo 去掉 responseData - 附三个 resources/sql 迁移(DBA 手动执行,含历史行回填)
This commit is contained in:
@@ -30,6 +30,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -77,18 +78,17 @@ public class OapiLogAspect {
|
||||
SysOapiLog entity = buildEntity(point, request, httpMethod, oapiLog);
|
||||
|
||||
boolean saveResponse = oapiLog != null ? oapiLog.saveResponse() : true;
|
||||
int maxResponseSize = oapiLog != null ? oapiLog.maxResponseSize() : 10240;
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
result = point.proceed();
|
||||
entity.setStatusCode(200);
|
||||
fillResponse(entity, result, saveResponse, maxResponseSize);
|
||||
fillResponse(entity, result, saveResponse);
|
||||
} catch (Throwable e) {
|
||||
entity.setStatusCode(500);
|
||||
entity.setErrorMsg(truncate(e.getMessage(), 2000));
|
||||
// 业务异常(DException/BizException)同样构造实际返回的响应体落库,便于详情下载
|
||||
fillErrorResponse(entity, e, saveResponse, maxResponseSize);
|
||||
fillErrorResponse(entity, e, saveResponse);
|
||||
throw e;
|
||||
} finally {
|
||||
entity.setCostTime(System.currentTimeMillis() - start);
|
||||
@@ -216,20 +216,20 @@ public class OapiLogAspect {
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入响应结果(脱敏 → 截断 → GZIP)。
|
||||
* 写入响应结果(脱敏 → GZIP,不截断)。
|
||||
*/
|
||||
private void fillResponse(SysOapiLog entity, Object result, boolean saveResponse, int maxResponseSize) {
|
||||
private void fillResponse(SysOapiLog entity, Object result, boolean saveResponse) {
|
||||
if (!saveResponse || result == null) {
|
||||
return;
|
||||
}
|
||||
writeResponseData(entity, result, maxResponseSize);
|
||||
writeResponseData(entity, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常路径:与 {@code GlobalExceptionHandler} 保持一致的响应体(DException/BizException → ApiResponse),
|
||||
* 使日志下载内容等于调用方实际收到的 body;其余异常不构造(响应不可预期)。
|
||||
*/
|
||||
private void fillErrorResponse(SysOapiLog entity, Throwable e, boolean saveResponse, int maxResponseSize) {
|
||||
private void fillErrorResponse(SysOapiLog entity, Throwable e, boolean saveResponse) {
|
||||
if (!saveResponse) {
|
||||
return;
|
||||
}
|
||||
@@ -237,7 +237,7 @@ public class OapiLogAspect {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
writeResponseData(entity, response, maxResponseSize);
|
||||
writeResponseData(entity, response);
|
||||
}
|
||||
|
||||
private ApiResponse<?> toApiResponse(Throwable e) {
|
||||
@@ -252,7 +252,7 @@ public class OapiLogAspect {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void writeResponseData(SysOapiLog entity, Object payload, int maxResponseSize) {
|
||||
private void writeResponseData(SysOapiLog entity, Object payload) {
|
||||
if (payload == null) {
|
||||
return;
|
||||
}
|
||||
@@ -261,10 +261,8 @@ public class OapiLogAspect {
|
||||
if (resp == null) {
|
||||
return;
|
||||
}
|
||||
if (resp.length() > maxResponseSize) {
|
||||
resp = resp.substring(0, maxResponseSize) + "...[截断]";
|
||||
}
|
||||
entity.setResponseData(GzipUtil.compress(resp));
|
||||
entity.setResponseSize(resp.getBytes(StandardCharsets.UTF_8).length);
|
||||
} catch (Exception e) {
|
||||
log.warn("开放接口日志响应序列化失败, uri={}", entity.getRequestUri(), e);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -45,9 +46,6 @@ public class OperationLogAspect {
|
||||
/** 不记录日志查询/下载接口自身,避免递归与刷屏。 */
|
||||
private static final String LOG_CONTROLLER = "com.yunhai.userbase.controller.api.SysOperationLogController";
|
||||
|
||||
/** GET 自动记录时使用的默认响应保存上限(10KB)。 */
|
||||
private static final int DEFAULT_MAX_RESPONSE_SIZE = 10240;
|
||||
|
||||
@Autowired
|
||||
private ISysOperationLogService sysOperationLogService;
|
||||
|
||||
@@ -76,13 +74,12 @@ public class OperationLogAspect {
|
||||
|
||||
// GET 未标注注解时使用默认参数;标注了则优先用注解配置
|
||||
boolean saveResponse = operationLog != null ? operationLog.saveResponse() : true;
|
||||
int maxResponseSize = operationLog != null ? operationLog.maxResponseSize() : DEFAULT_MAX_RESPONSE_SIZE;
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
result = point.proceed();
|
||||
entity.setStatusCode(200);
|
||||
fillResponse(entity, result, saveResponse, maxResponseSize);
|
||||
fillResponse(entity, result, saveResponse);
|
||||
} catch (Throwable e) {
|
||||
entity.setStatusCode(500);
|
||||
entity.setErrorMsg(truncate(e.getMessage(), 2000));
|
||||
@@ -174,18 +171,16 @@ public class OperationLogAspect {
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入响应结果(GZIP 压缩 + 截断)。
|
||||
* 写入响应结果(全量 GZIP 压缩入库,不再截断)。
|
||||
*/
|
||||
private void fillResponse(SysOperationLog entity, Object result, boolean saveResponse, int maxResponseSize) {
|
||||
private void fillResponse(SysOperationLog entity, Object result, boolean saveResponse) {
|
||||
if (!saveResponse || result == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String resp = JSON.toJSONString(result);
|
||||
if (resp.length() > maxResponseSize) {
|
||||
resp = resp.substring(0, maxResponseSize) + "...[截断]";
|
||||
}
|
||||
entity.setResponseData(GzipUtil.compress(resp));
|
||||
entity.setResponseSize(resp.getBytes(StandardCharsets.UTF_8).length);
|
||||
} catch (Exception e) {
|
||||
log.warn("操作日志响应结果序列化失败, uri={}", entity.getRequestUri(), e);
|
||||
}
|
||||
|
||||
@@ -32,9 +32,4 @@ public @interface OapiLog {
|
||||
* 是否保存响应结果(GZIP 压缩后入库)。
|
||||
*/
|
||||
boolean saveResponse() default true;
|
||||
|
||||
/**
|
||||
* 响应结果序列化后的最大字符数,超过则截断。默认 10KB。
|
||||
*/
|
||||
int maxResponseSize() default 10240;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,4 @@ public @interface OperationLog {
|
||||
* 是否保存响应结果(GZIP 压缩后入库)。
|
||||
*/
|
||||
boolean saveResponse() default true;
|
||||
|
||||
/**
|
||||
* 响应结果序列化后的最大字符数,超过则截断。默认 10KB。
|
||||
*/
|
||||
int maxResponseSize() default 10240;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,12 @@ public class SysOapiLog implements Serializable {
|
||||
@Column(name = "response_data", columnDefinition = "MEDIUMBLOB")
|
||||
private byte[] responseData;
|
||||
|
||||
/**
|
||||
* 响应原文(GZIP 前)UTF-8 字节数,写入时算好,列表按此列切换在线预览/下载,避免加载 response_data 大字段。
|
||||
*/
|
||||
@Column(name = "response_size")
|
||||
private Integer responseSize;
|
||||
|
||||
/**
|
||||
* HTTP状态码
|
||||
*/
|
||||
|
||||
@@ -106,6 +106,12 @@ public class SysOperationLog implements Serializable {
|
||||
@Column(name = "response_data", columnDefinition = "MEDIUMBLOB")
|
||||
private byte[] responseData;
|
||||
|
||||
/**
|
||||
* 响应原文(GZIP 前)UTF-8 字节数,写入时算好,列表按此列切换在线预览/下载,避免加载 response_data 大字段。
|
||||
*/
|
||||
@Column(name = "response_size")
|
||||
private Integer responseSize;
|
||||
|
||||
/**
|
||||
* HTTP状态码
|
||||
*/
|
||||
|
||||
@@ -38,8 +38,6 @@ public class SysOapiLogListVo {
|
||||
private String operationDesc;
|
||||
/** 请求参数JSON(脱敏后) */
|
||||
private String requestParams;
|
||||
/** 响应结果(GZIP 解压后原文,脱敏后;随列表带出,详情弹窗直接展示) */
|
||||
private String responseData;
|
||||
/** HTTP 状态码 */
|
||||
private Integer statusCode;
|
||||
/** 耗时(ms) */
|
||||
@@ -52,6 +50,8 @@ public class SysOapiLogListVo {
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
/** 响应原文字节数(用于前端按大小切换在线预览/下载,response_data 原文不进列表) */
|
||||
private Integer responseSize;
|
||||
|
||||
/**
|
||||
* 全参构造,顺序需与 {@code SysOapiLog} 的列表投影字段一致。<br>
|
||||
@@ -61,7 +61,8 @@ public class SysOapiLogListVo {
|
||||
String requestIp, String requestUri, String requestMethod,
|
||||
String operationType, String classMethod, String operationDesc,
|
||||
String requestParams, Integer statusCode, Long costTime,
|
||||
String userAgent, String errorMsg, LocalDateTime createTime) {
|
||||
String userAgent, String errorMsg, LocalDateTime createTime,
|
||||
Integer responseSize) {
|
||||
this.id = id;
|
||||
this.logLevel = logLevel;
|
||||
this.callerAppKey = callerAppKey;
|
||||
@@ -78,5 +79,6 @@ public class SysOapiLogListVo {
|
||||
this.userAgent = userAgent;
|
||||
this.errorMsg = errorMsg;
|
||||
this.createTime = createTime;
|
||||
this.responseSize = responseSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ public class SysOperationLogListVo {
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
/** 响应原文字节数(用于前端按大小切换在线预览/下载,response_data 原文不进列表) */
|
||||
private Integer responseSize;
|
||||
|
||||
/**
|
||||
* 全参构造,顺序需与 {@code SysOperationLog} 的列表投影字段一致。<br>
|
||||
@@ -57,7 +59,7 @@ public class SysOperationLogListVo {
|
||||
String requestIp, String requestUri, String requestMethod,
|
||||
String operationType, String classMethod, String requestParams,
|
||||
Integer statusCode, Long costTime, String userAgent,
|
||||
String errorMsg, LocalDateTime createTime) {
|
||||
String errorMsg, LocalDateTime createTime, Integer responseSize) {
|
||||
this.id = id;
|
||||
this.logLevel = logLevel;
|
||||
this.userId = userId;
|
||||
@@ -73,5 +75,6 @@ public class SysOperationLogListVo {
|
||||
this.userAgent = userAgent;
|
||||
this.errorMsg = errorMsg;
|
||||
this.createTime = createTime;
|
||||
this.responseSize = responseSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ public class PushRecordEntity {
|
||||
@Column(name = "payload", columnDefinition = "MEDIUMTEXT")
|
||||
private String payload;
|
||||
|
||||
/** 请求体 UTF-8 字节数(写入时算好,列表按此列切换在线预览/下载,避免查询 payload 大字段)。 */
|
||||
@Column(name = "payload_size")
|
||||
private Integer payloadSize;
|
||||
|
||||
@Column(name = "sign_t")
|
||||
private String signT;
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ public class PushRecordListVo {
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
/** 请求体字节数(用于前端按大小切换在线预览/下载,payload 原文不进列表) */
|
||||
private Integer payloadSize;
|
||||
|
||||
/**
|
||||
* 全参构造,顺序需与 {@code PushRecordEntity} 的列表投影字段一致。<br>
|
||||
@@ -54,7 +56,7 @@ public class PushRecordListVo {
|
||||
public PushRecordListVo(Long id, Integer subscriberAppKey, String appName, String interfacePath,
|
||||
String interfaceLabel, String requestUrl, String signT, Integer httpStatus,
|
||||
Integer respCode, String respMsg, Integer success, Integer attemptNo,
|
||||
Integer costMs, String triggerType, LocalDateTime createdAt) {
|
||||
Integer costMs, String triggerType, LocalDateTime createdAt, Integer payloadSize) {
|
||||
this.id = id;
|
||||
this.subscriberAppKey = subscriberAppKey;
|
||||
this.appName = appName;
|
||||
@@ -70,5 +72,6 @@ public class PushRecordListVo {
|
||||
this.costMs = costMs;
|
||||
this.triggerType = triggerType;
|
||||
this.createdAt = createdAt;
|
||||
this.payloadSize = payloadSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ public class SysOapiLogServiceImpl implements ISysOapiLogService {
|
||||
root.get("requestIp"), root.get("requestUri"), root.get("requestMethod"),
|
||||
root.get("operationType"), root.get("classMethod"), root.get("operationDesc"),
|
||||
root.get("requestParams"), root.get("statusCode"), root.get("costTime"),
|
||||
root.get("userAgent"), root.get("errorMsg"), root.get("createTime")));
|
||||
root.get("userAgent"), root.get("errorMsg"), root.get("createTime"), root.get("responseSize")));
|
||||
List<Predicate> predicates = buildPredicates(cb, root, query);
|
||||
cq.where(predicates.toArray(new Predicate[0]));
|
||||
cq.orderBy(cb.desc(root.get("createTime")));
|
||||
@@ -89,10 +89,6 @@ public class SysOapiLogServiceImpl implements ISysOapiLogService {
|
||||
typed.setFirstResult((pageIndex - 1) * pageSize);
|
||||
typed.setMaxResults(pageSize);
|
||||
List<SysOapiLogListVo> content = typed.getResultList();
|
||||
// 响应结果随列表带出(GZIP 解压后),供详情弹窗直接展示,无需再单独请求
|
||||
for (SysOapiLogListVo vo : content) {
|
||||
vo.setResponseData(getResponseData(vo.getId()));
|
||||
}
|
||||
|
||||
int totalPages = (int) Math.ceil((double) total / pageSize);
|
||||
boolean hasNext = pageIndex < totalPages;
|
||||
|
||||
@@ -74,7 +74,7 @@ public class SysOperationLogServiceImpl implements ISysOperationLogService {
|
||||
root.get("requestIp"), root.get("requestUri"), root.get("requestMethod"),
|
||||
root.get("operationType"), root.get("classMethod"), root.get("requestParams"),
|
||||
root.get("statusCode"), root.get("costTime"), root.get("userAgent"),
|
||||
root.get("errorMsg"), root.get("createTime")));
|
||||
root.get("errorMsg"), root.get("createTime"), root.get("responseSize")));
|
||||
List<Predicate> predicates = buildPredicates(cb, root, query);
|
||||
cq.where(predicates.toArray(new Predicate[0]));
|
||||
cq.orderBy(cb.desc(root.get("createTime")));
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -100,6 +101,8 @@ public class PushConsumer {
|
||||
rec.setInterfaceLabel(msg.getInterfaceLabel());
|
||||
rec.setRequestUrl(url == null ? "" : url);
|
||||
rec.setPayload(msg.getBody());
|
||||
rec.setPayloadSize(msg.getBody() == null ? 0
|
||||
: msg.getBody().getBytes(StandardCharsets.UTF_8).length);
|
||||
rec.setSignT(msg.getT());
|
||||
rec.setHttpStatus(httpStatus);
|
||||
rec.setRespCode(respCode);
|
||||
|
||||
@@ -66,7 +66,7 @@ public class PushRecordService {
|
||||
root.get("interfacePath"), root.get("interfaceLabel"), root.get("requestUrl"),
|
||||
root.get("signT"), root.get("httpStatus"), root.get("respCode"), root.get("respMsg"),
|
||||
root.get("success"), root.get("attemptNo"), root.get("costMs"),
|
||||
root.get("triggerType"), root.get("createdAt")));
|
||||
root.get("triggerType"), root.get("createdAt"), root.get("payloadSize")));
|
||||
List<Predicate> predicates = buildPredicates(cb, root, q);
|
||||
cq.where(predicates.toArray(new Predicate[0]));
|
||||
cq.orderBy(buildOrders(cb, root, pageable.getSort()));
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 为 push_record 表新增 payload_size 字段:写入时记录请求体 UTF-8 字节数,
|
||||
-- 供「推送日志」列表按大小切换在线预览/下载(列表只读该整数列,不再关联 payload 大字段)。
|
||||
-- 执行方式:由 DBA 手动在对应环境执行,AI 不代跑。
|
||||
-- 说明:dev 环境 spring.jpa.hibernate.ddl-auto=update 会自动建列,此时可跳过 ALTER、只跑回填。
|
||||
|
||||
ALTER TABLE push_record
|
||||
ADD COLUMN payload_size INT NULL COMMENT '请求体UTF-8字节数(写入时算,列表按此切换预览/下载)';
|
||||
|
||||
-- 回填历史行:LENGTH() 对 utf8mb4 MEDIUMTEXT 返回字节数,与下载体积一致。
|
||||
-- 大表回填会读 off-page blob,建议低峰分批执行。
|
||||
UPDATE push_record SET payload_size = LENGTH(payload) WHERE payload_size IS NULL;
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 为 sys_oapi_log 表新增 response_size 字段:写入时记录响应原文(GZIP 前)UTF-8 字节数,
|
||||
-- 供「开放接口日志」列表按大小切换在线预览/下载(列表只读该整数列,不再关联 response_data 大字段)。
|
||||
-- 同时本次移除了写时截断(响应改为全量 GZIP 入库),并删除了列表查询里逐行解压回填的 N+1。
|
||||
-- 执行方式:由 DBA 手动在对应环境执行,AI 不代跑。
|
||||
-- 说明:dev 环境 spring.jpa.hibernate.ddl-auto=update 会自动建列,此时可跳过 ALTER、只跑回填。
|
||||
|
||||
ALTER TABLE sys_oapi_log
|
||||
ADD COLUMN response_size INT NULL COMMENT '响应原文GZIP前UTF-8字节数(写入时算,列表按此切换预览/下载)';
|
||||
|
||||
-- 回填历史行:老数据 response_data 均为截断后(≤10KB)再 GZIP,LENGTH() 取的是压缩字节数(低估),
|
||||
-- 但老行解压后本就远小于 50KB 预览阈值,按可预览处理安全;新行由切面写入精确值。
|
||||
UPDATE sys_oapi_log SET response_size = LENGTH(response_data) WHERE response_size IS NULL;
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 为 sys_operation_log 表新增 response_size 字段:写入时记录响应原文(GZIP 前)UTF-8 字节数,
|
||||
-- 供「操作日志」列表按大小切换在线预览/下载(列表只读该整数列,不再关联 response_data 大字段)。
|
||||
-- 同时本次移除了写时截断(响应改为全量 GZIP 入库)。
|
||||
-- 执行方式:由 DBA 手动在对应环境执行,AI 不代跑。
|
||||
-- 说明:dev 环境 spring.jpa.hibernate.ddl-auto=update 会自动建列,此时可跳过 ALTER、只跑回填。
|
||||
|
||||
ALTER TABLE sys_operation_log
|
||||
ADD COLUMN response_size INT NULL COMMENT '响应原文GZIP前UTF-8字节数(写入时算,列表按此切换预览/下载)';
|
||||
|
||||
-- 回填历史行:老数据 response_data 均为截断后(≤10KB)再 GZIP,LENGTH() 取的是压缩字节数(低估),
|
||||
-- 但老行解压后本就远小于 50KB 预览阈值,按可预览处理安全;新行由切面写入精确值。
|
||||
UPDATE sys_operation_log SET response_size = LENGTH(response_data) WHERE response_size IS NULL;
|
||||
Reference in New Issue
Block a user