package com.loan.system.domain.entity; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.*; import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.commons.lang3.exception.ExceptionUtils; import javax.persistence.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.io.IOException; import java.time.Instant; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Base64; import java.util.zip.GZIPOutputStream; @Entity @Table(name = "exception_log") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class ExceptionLog { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; @Size(max = 120) @Column(name = "exception_type", length = 120) private String exceptionType; @Size(max = 500) @Column(name = "message", length = 500) private String message; @Size(max = 200) @Column(name = "summary", length = 200) private String summary; @Size(max = 200) @Column(name = "cause", length = 200) private String cause; @Lob @Column(name = "stack_trace") private String stackTrace; @Size(max = 255) @Column(name = "url") private String url; @Column(name = "user_id") private Long userId; @Size(max = 45) @Column(name = "ip", length = 45) private String ip; @Size(max = 300) @Column(name = "user_agent", length = 300) private String userAgent; @Size(max = 10) @Column(name = "method", length = 10) private String method; @Lob @Column(name = "params") private String params; @Column(name = "is_resolve") private Boolean isResolve; @NotNull @Column(name = "create_time", nullable = false) private String createTime; private static final ObjectMapper MAPPER = new ObjectMapper(); /** 快速构建:只传异常 + 请求上下文 */ public static ExceptionLog build(Exception e, String url, String method, String ip, String userAgent, Long userId, Object paramsObj) { Throwable root = ExceptionUtils.getRootCause(e); if (root == null) root = e; return ExceptionLog.builder() .exceptionType(e.getClass().getSimpleName()) .message(trim(e.getMessage(), 500)) .summary(buildSummary(e)) .cause(root.getClass().getSimpleName()) .stackTrace(compressTrace(e, 50)) .url(trim(url, 255)) .method(trim(method, 10)) .ip(trim(ip, 45)) .userAgent(trim(userAgent, 300)) .userId(userId == null ? 0L : userId) .params(toJson(paramsObj)) .createTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) .build(); } /** 摘要:类名 + 第一行堆栈 */ private static String buildSummary(Exception e) { StringBuilder sb = new StringBuilder(e.getClass().getSimpleName()); if (e.getMessage() != null) { sb.append(": ").append(trim(e.getMessage(), 120)); } if (e.getStackTrace().length > 0) { StackTraceElement first = e.getStackTrace()[0]; sb.append(" at ") .append(first.getClassName()) .append(":") .append(first.getLineNumber()); } return trim(sb.toString(), 200); } /** 截取堆栈前 max 行并 GZIP + Base64 */ private static String compressTrace(Exception e, int max) { String raw = ExceptionUtils.getStackTrace(e); String[] lines = raw.split("\n"); StringBuilder sb = new StringBuilder(); for (int i = 0; i < Math.min(max, lines.length); i++) { sb.append(lines[i]).append('\n'); } try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); GZIPOutputStream gzip = new GZIPOutputStream(bos)) { gzip.write(sb.toString().getBytes("UTF-8")); gzip.finish(); return Base64.getEncoder().encodeToString(bos.toByteArray()); } catch (IOException ignored) { return ""; } } private static String toJson(Object obj) { if (obj == null) return null; try { JsonNode node = MAPPER.valueToTree(obj); // TODO 这里可继续做脱敏 return MAPPER.writeValueAsString(node); } catch (Exception ignore) { return null; } } private static String trim(String str, int max) { if (str == null) return ""; return str.length() > max ? str.substring(0, max) : str; } }