Browse Source

feat(api): 添加系统错误日志功能

- 创建ErrorLogUtils工具类,提供静态方法记录业务错误、接口错误和异常
- 新增SysErrorLog实体类,定义错误日志数据表结构和字段映射
- 实现SysErrorLogController控制器,提供分页查询和详情查看接口
- 创建SysErrorLogData查询条件类,支持按站点、模块、时间等多维度筛选
- 实现SysErrorLogMapper数据访问层,包含分页查询和总数统计功能
- 配置SysErrorLogMapper.xml映射文件,定义动态SQL查询条件
- 构建SysErrorLogService服务层接口及其实现类,封装业务逻辑
- 支持工厂编码自动获取、调用方法名自动识别和异常堆栈自动提取功能
master
常熟吴彦祖 1 month ago
parent
commit
fb99b2d632
  1. 259
      src/main/java/com/gaotao/common/utils/ErrorLogUtils.java
  2. 48
      src/main/java/com/gaotao/modules/api/controller/SysErrorLogController.java
  3. 33
      src/main/java/com/gaotao/modules/api/dao/SysErrorLogMapper.java
  4. 137
      src/main/java/com/gaotao/modules/api/entity/SysErrorLog.java
  5. 56
      src/main/java/com/gaotao/modules/api/entity/SysErrorLogData.java
  6. 22
      src/main/java/com/gaotao/modules/api/service/SysErrorLogService.java
  7. 40
      src/main/java/com/gaotao/modules/api/service/impl/SysErrorLogServiceImpl.java
  8. 66
      src/main/resources/mapper/api/SysErrorLogMapper.xml

259
src/main/java/com/gaotao/common/utils/ErrorLogUtils.java

@ -0,0 +1,259 @@
package com.gaotao.common.utils;
import com.gaotao.modules.api.dao.SysErrorLogMapper;
import com.gaotao.modules.api.entity.SysErrorLog;
import com.gaotao.modules.sys.entity.SysUserEntity;
import jakarta.annotation.PostConstruct;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
/**
* 错误日志工具类
* 提供简单易用的静态方法记录错误日志
*
* 使用示例
* 1. 业务错误ErrorLogUtils.log("55", "立库自动化", "直接组盘", "W00250", "栈板不存在");
* 2. 接口错误ErrorLogUtils.logInterface("55", "立库自动化", "推送WCS", "WCS", "PushPalletDetailNew", "W00250", requestJson, "WCS返回失败");
* 3. 异常记录ErrorLogUtils.logException("55", "立库自动化", "直接组盘", palletId, e);
*
* 注意site参数可以传null传null时会自动从当前登录用户获取
*
* @author rqrq
* @date 2026/01/26
*/
@Component
public class ErrorLogUtils {
private static final Logger logger = LoggerFactory.getLogger(ErrorLogUtils.class);
private static SysErrorLogMapper errorLogMapper;
@Autowired
private SysErrorLogMapper mapper;
@PostConstruct
public void init() {
ErrorLogUtils.errorLogMapper = this.mapper;
}
// ==================== 业务错误记录 ====================
/**
* 记录业务错误
* @param site 工厂编码可为nullnull时自动获取
* @param moduleName 模块名称可为null
* @param functionName 功能名称可为null
* @param businessKey 业务主键W00250
* @param errorMessage 错误信息
*/
public static void log(String site, String moduleName, String functionName,
String businessKey, String errorMessage) {
saveLog(site, moduleName, functionName, businessKey, "N", null, null, null,
errorMessage, null);
}
/**
* 记录业务错误带详情
* @param site 工厂编码可为nullnull时自动获取
* @param moduleName 模块名称可为null
* @param functionName 功能名称可为null
* @param businessKey 业务主键
* @param errorMessage 错误信息
* @param errorDetail 错误详情
*/
public static void log(String site, String moduleName, String functionName,
String businessKey, String errorMessage, String errorDetail) {
saveLog(site, moduleName, functionName, businessKey, "N", null, null, null,
errorMessage, errorDetail);
}
// ==================== 接口错误记录 ====================
/**
* 记录接口错误
* @param site 工厂编码可为nullnull时自动获取
* @param moduleName 模块名称可为null
* @param functionName 功能名称可为null
* @param interfaceType 接口类型WCS/IFS/TUSK
* @param interfaceName 接口名称PushPalletDetailNew
* @param businessKey 业务主键
* @param requestData 接口入参JSON字符串
* @param errorMessage 错误信息
*/
public static void logInterface(String site, String moduleName, String functionName,
String interfaceType, String interfaceName,
String businessKey, String requestData,
String errorMessage) {
saveLog(site, moduleName, functionName, businessKey, "Y", interfaceType, interfaceName,
requestData, errorMessage, null);
}
/**
* 记录接口错误带详情
* @param site 工厂编码可为nullnull时自动获取
* @param moduleName 模块名称可为null
* @param functionName 功能名称可为null
* @param interfaceType 接口类型WCS/IFS/TUSK
* @param interfaceName 接口名称
* @param businessKey 业务主键
* @param requestData 接口入参
* @param errorMessage 错误信息
* @param errorDetail 错误详情如响应数据
*/
public static void logInterface(String site, String moduleName, String functionName,
String interfaceType, String interfaceName,
String businessKey, String requestData,
String errorMessage, String errorDetail) {
saveLog(site, moduleName, functionName, businessKey, "Y", interfaceType, interfaceName,
requestData, errorMessage, errorDetail);
}
// ==================== 异常记录 ====================
/**
* 记录异常自动提取堆栈
* @param site 工厂编码可为nullnull时自动获取
* @param moduleName 模块名称可为null
* @param functionName 功能名称可为null
* @param businessKey 业务主键
* @param e 异常对象
*/
public static void logException(String site, String moduleName, String functionName,
String businessKey, Exception e) {
String errorDetail = getStackTrace(e);
saveLog(site, moduleName, functionName, businessKey, "N", null, null, null,
e.getMessage(), errorDetail);
}
/**
* 记录接口异常
* @param site 工厂编码可为nullnull时自动获取
* @param moduleName 模块名称可为null
* @param functionName 功能名称可为null
* @param interfaceType 接口类型WCS/IFS/TUSK
* @param interfaceName 接口名称
* @param businessKey 业务主键
* @param requestData 接口入参
* @param e 异常对象
*/
public static void logInterfaceException(String site, String moduleName, String functionName,
String interfaceType, String interfaceName,
String businessKey, String requestData, Exception e) {
String errorDetail = getStackTrace(e);
saveLog(site, moduleName, functionName, businessKey, "Y", interfaceType, interfaceName,
requestData, e.getMessage(), errorDetail);
}
// ==================== 私有方法 ====================
/**
* 保存错误日志
*/
private static void saveLog(String site, String moduleName, String functionName, String businessKey,
String isInterface, String interfaceType, String interfaceName,
String requestData, String errorMessage, String errorDetail) {
try {
if (errorLogMapper == null) {
logger.warn("ErrorLogUtils未初始化,无法记录错误日志");
return;
}
SysErrorLog log = new SysErrorLog();
// site为null时自动获取 - rqrq
log.setSite(site != null ? site : getSite());
log.setModuleName(moduleName);
log.setFunctionName(functionName);
log.setBusinessKey(businessKey);
log.setIsInterface(isInterface);
log.setInterfaceType(interfaceType);
log.setInterfaceName(interfaceName);
log.setRequestData(requestData);
log.setMethodName(getCallerMethod()); // 自动获取调用者方法名
log.setErrorMessage(truncate(errorMessage, 500));
log.setErrorDetail(errorDetail);
log.setUsername(getUsername());
log.setCreatedTime(new Date());
errorLogMapper.insert(log);
} catch (Exception ex) {
// 记录日志失败不影响主流程仅打印警告
logger.warn("记录错误日志失败: {}", ex.getMessage());
}
}
/**
* 获取当前用户名
*/
private static String getUsername() {
try {
return ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername();
} catch (Exception e) {
return "后台定时任务";
}
}
/**
* 获取当前站点
*/
private static String getSite() {
try {
return ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getSite();
} catch (Exception e) {
return "55"; // 默认站点
}
}
/**
* 自动获取调用者的方法名
* 格式类名.方法名
*/
private static String getCallerMethod() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
// 0: getStackTrace
// 1: getCallerMethod
// 2: saveLog
// 3: log/logInterface/logException等方法
// 4: 实际调用者
if (stackTrace.length > 4) {
StackTraceElement caller = stackTrace[4];
String className = caller.getClassName();
// 只取类名不要包名
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
className = className.substring(lastDot + 1);
}
return className + "." + caller.getMethodName();
}
return "Unknown";
}
/**
* 截断字符串
*/
private static String truncate(String str, int maxLen) {
if (str == null) {
return null;
}
return str.length() > maxLen ? str.substring(0, maxLen) : str;
}
/**
* 获取异常堆栈信息
*/
private static String getStackTrace(Exception e) {
if (e == null) {
return null;
}
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
return sw.toString();
}
}

48
src/main/java/com/gaotao/modules/api/controller/SysErrorLogController.java

@ -0,0 +1,48 @@
package com.gaotao.modules.api.controller;
import com.gaotao.common.utils.PageUtils;
import com.gaotao.common.utils.R;
import com.gaotao.modules.api.entity.SysErrorLog;
import com.gaotao.modules.api.entity.SysErrorLogData;
import com.gaotao.modules.api.service.SysErrorLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 系统错误日志Controller
* 提供错误日志的查询接口
*
* @author rqrq
* @date 2026/01/26
*/
@RestController
@RequestMapping("/api/sysErrorLog")
public class SysErrorLogController {
@Autowired
private SysErrorLogService sysErrorLogService;
/**
* 分页查询错误日志
* @param data 查询条件
* @return 分页结果
* @author rqrq
*/
@PostMapping("/list")
public R list(@RequestBody SysErrorLogData data) {
PageUtils page = sysErrorLogService.queryPage(data);
return R.ok().put("page", page);
}
/**
* 查询错误日志详情
* @param id 日志ID
* @return 日志详情
* @author rqrq
*/
@PostMapping("/detail")
public R detail(@RequestBody SysErrorLogData data) {
SysErrorLog log = sysErrorLogService.getById(data.getId());
return R.ok().put("row", log);
}
}

33
src/main/java/com/gaotao/modules/api/dao/SysErrorLogMapper.java

@ -0,0 +1,33 @@
package com.gaotao.modules.api.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gaotao.modules.api.entity.SysErrorLog;
import com.gaotao.modules.api.entity.SysErrorLogData;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 系统错误日志Mapper
*
* @author rqrq
* @date 2026/01/26
*/
@Mapper
public interface SysErrorLogMapper extends BaseMapper<SysErrorLog> {
/**
* 分页查询错误日志
* @param data 查询条件
* @return 错误日志列表
*/
List<SysErrorLog> queryList(@Param("data") SysErrorLogData data);
/**
* 查询总数
* @param data 查询条件
* @return 总数
*/
int queryTotal(@Param("data") SysErrorLogData data);
}

137
src/main/java/com/gaotao/modules/api/entity/SysErrorLog.java

@ -0,0 +1,137 @@
package com.gaotao.modules.api.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import org.apache.ibatis.type.Alias;
import java.io.Serializable;
import java.util.Date;
/**
* 系统错误日志实体类
* 表名: sys_error_log
* 用途: 记录系统所有模块的错误信息供问题排查和统计分析
*
* 索引信息:
* - PRIMARY KEY (id)
* - INDEX idx_error_site_time (site, created_time)
* - INDEX idx_error_module (module_name)
* - INDEX idx_error_business (business_key)
*
* @author rqrq
* @date 2026/01/26
*/
@Data
@TableName("sys_error_log")
@Alias("SysErrorLog")
public class SysErrorLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 工厂编码
* 55
*/
@TableField("site")
private String site;
// ========== 模块和功能信息 ==========
/**
* 模块名称
* 立库自动化仓库管理
* 可为空
*/
@TableField("module_name")
private String moduleName;
/**
* 功能名称
* 直接组盘IFS重试
* 可为空
*/
@TableField("function_name")
private String functionName;
// ========== 业务信息 ==========
/**
* 业务主键
* 栈板号W00250申请单号REQ202601标签号A552026011600000892
*/
@TableField("business_key")
private String businessKey;
// ========== 接口信息 ==========
/**
* 是否接口调用
* Y- N-
*/
@TableField("is_interface")
private String isInterface;
/**
* 接口类型
* WCS/IFS/TUSK
*/
@TableField("interface_type")
private String interfaceType;
/**
* 接口名称
* PushPalletDetailNewMoveInventoryPart
*/
@TableField("interface_name")
private String interfaceName;
/**
* 接口入参
* JSON格式
*/
@TableField("request_data")
private String requestData;
// ========== 方法和错误信息 ==========
/**
* 后台方法名
* 自动获取格式类名.方法名
* WcsIntegrationServiceImpl.completePalletAssembly
*/
@TableField("method_name")
private String methodName;
/**
* 错误信息
* 简短错误描述最长500字符
*/
@TableField("error_message")
private String errorMessage;
/**
* 错误详情
* 完整堆栈或响应数据等
*/
@TableField("error_detail")
private String errorDetail;
// ========== 其他信息 ==========
/**
* 操作用户名
*/
@TableField("username")
private String username;
/**
* 创建时间
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private Date createdTime;
}

56
src/main/java/com/gaotao/modules/api/entity/SysErrorLogData.java

@ -0,0 +1,56 @@
package com.gaotao.modules.api.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.ibatis.type.Alias;
/**
* 系统错误日志查询条件类
* 继承SysErrorLog增加分页和查询参数
*
* @author rqrq
* @date 2026/01/26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Alias("SysErrorLogData")
public class SysErrorLogData extends SysErrorLog {
private static final long serialVersionUID = 1L;
// ========== 分页参数 ==========
/**
* 页码
*/
private Integer page;
/**
* 每页条数
*/
private Integer limit;
/**
* 分页起始位置
* SQL: OFFSET offset ROWS FETCH NEXT limit ROWS ONLY
*/
private Integer offset;
// ========== 查询参数 ==========
/**
* 开始时间
* 格式yyyy-MM-dd yyyy-MM-dd HH:mm:ss
*/
private String startTime;
/**
* 结束时间
* 格式yyyy-MM-dd yyyy-MM-dd HH:mm:ss
*/
private String endTime;
/**
* 错误信息模糊查询
*/
private String searchErrorMessage;
}

22
src/main/java/com/gaotao/modules/api/service/SysErrorLogService.java

@ -0,0 +1,22 @@
package com.gaotao.modules.api.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gaotao.common.utils.PageUtils;
import com.gaotao.modules.api.entity.SysErrorLog;
import com.gaotao.modules.api.entity.SysErrorLogData;
/**
* 系统错误日志Service接口
*
* @author rqrq
* @date 2026/01/26
*/
public interface SysErrorLogService extends IService<SysErrorLog> {
/**
* 分页查询错误日志
* @param data 查询条件
* @return 分页结果
*/
PageUtils queryPage(SysErrorLogData data);
}

40
src/main/java/com/gaotao/modules/api/service/impl/SysErrorLogServiceImpl.java

@ -0,0 +1,40 @@
package com.gaotao.modules.api.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gaotao.common.utils.PageUtils;
import com.gaotao.modules.api.dao.SysErrorLogMapper;
import com.gaotao.modules.api.entity.SysErrorLog;
import com.gaotao.modules.api.entity.SysErrorLogData;
import com.gaotao.modules.api.service.SysErrorLogService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 系统错误日志Service实现类
*
* @author rqrq
* @date 2026/01/26
*/
@Service("sysErrorLogService")
public class SysErrorLogServiceImpl extends ServiceImpl<SysErrorLogMapper, SysErrorLog>
implements SysErrorLogService {
@Override
public PageUtils queryPage(SysErrorLogData data) {
// 计算分页偏移量 - rqrq
if (data.getPage() != null && data.getLimit() != null) {
data.setOffset((data.getPage() - 1) * data.getLimit());
} else {
data.setPage(1);
data.setLimit(10);
data.setOffset(0);
}
// 查询数据 - rqrq
List<SysErrorLog> list = baseMapper.queryList(data);
int total = baseMapper.queryTotal(data);
return new PageUtils(list, total, data.getLimit(), data.getPage());
}
}

66
src/main/resources/mapper/api/SysErrorLogMapper.xml

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- rqrq - 系统错误日志Mapper XML -->
<mapper namespace="com.gaotao.modules.api.dao.SysErrorLogMapper">
<!-- 通用查询条件 - rqrq -->
<sql id="queryCondition">
<where>
<if test="data.site != null and data.site != ''">
AND site = #{data.site}
</if>
<if test="data.moduleName != null and data.moduleName != ''">
AND module_name = #{data.moduleName}
</if>
<if test="data.functionName != null and data.functionName != ''">
AND function_name = #{data.functionName}
</if>
<if test="data.businessKey != null and data.businessKey != ''">
AND business_key LIKE '%' + #{data.businessKey} + '%'
</if>
<if test="data.isInterface != null and data.isInterface != ''">
AND is_interface = #{data.isInterface}
</if>
<if test="data.interfaceType != null and data.interfaceType != ''">
AND interface_type = #{data.interfaceType}
</if>
<if test="data.interfaceName != null and data.interfaceName != ''">
AND interface_name LIKE '%' + #{data.interfaceName} + '%'
</if>
<if test="data.username != null and data.username != ''">
AND username LIKE '%' + #{data.username} + '%'
</if>
<if test="data.searchErrorMessage != null and data.searchErrorMessage != ''">
AND error_message LIKE '%' + #{data.searchErrorMessage} + '%'
</if>
<if test="data.startTime != null and data.startTime != ''">
AND created_time &gt;= #{data.startTime}
</if>
<if test="data.endTime != null and data.endTime != ''">
AND created_time &lt;= #{data.endTime}
</if>
</where>
</sql>
<!-- 分页查询错误日志 - rqrq -->
<select id="queryList" resultType="SysErrorLog">
SELECT
id, site, module_name, function_name, business_key,
is_interface, interface_type, interface_name, request_data,
method_name, error_message, error_detail,
username, created_time
FROM sys_error_log
<include refid="queryCondition"/>
ORDER BY created_time DESC
OFFSET #{data.offset} ROWS FETCH NEXT #{data.limit} ROWS ONLY
</select>
<!-- 查询总数 - rqrq -->
<select id="queryTotal" resultType="int">
SELECT COUNT(1)
FROM sys_error_log
<include refid="queryCondition"/>
</select>
</mapper>
Loading…
Cancel
Save