feat: 新增操作日志基础数据模型

- 添加操作类型枚举(OperType),支持增删改查等操作分类
- 添加系统类型枚举(SystemType),区分后台用户和手机端用户
- 添加操作日志实体类(SysOperLog),包含完整的日志记录字段
- 为实体类添加状态和类型的文本转换方法,便于前端展示
This commit is contained in:
Leo 2025-07-07 22:41:31 +08:00
parent c9c1519469
commit d0312ea461
15 changed files with 1777 additions and 0 deletions

View File

@ -0,0 +1,59 @@
package org.leocoder.thin.domain.enums.oper;
/**
* @author Leocoder
* @description [操作类型枚举]
*/
public enum OperType {
/**
* 新增
*/
INSERT("INSERT", "新增"),
/**
* 修改
*/
UPDATE("UPDATE", "修改"),
/**
* 删除
*/
DELETE("DELETE", "删除"),
/**
* 查询
*/
SELECT("SELECT", "查询"),
/**
* 导入
*/
IMPORT("IMPORT", "导入"),
/**
* 导出
*/
EXPORT("EXPORT", "导出"),
/**
* 其它
*/
OTHER("OTHER", "其它");
private final String code;
private final String desc;
OperType(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() {
return code;
}
public String getDesc() {
return desc;
}
}

View File

@ -0,0 +1,39 @@
package org.leocoder.thin.domain.enums.oper;
/**
* @author Leocoder
* @description [系统类型枚举]
*/
public enum SystemType {
/**
* 后台用户
*/
MANAGER("MANAGER", "后台用户"),
/**
* 手机端用户
*/
PHONE("PHONE", "手机端用户"),
/**
* 其它
*/
OTHER("OTHER", "其它");
private final String code;
private final String desc;
SystemType(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() {
return code;
}
public String getDesc() {
return desc;
}
}

View File

@ -0,0 +1,134 @@
package org.leocoder.thin.domain.pojo.system;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @author Leocoder
* @description [操作日志实体类]
*/
@Data
@TableName("sys_oper_log")
public class SysOperLog {
/**
* 操作主键
*/
@TableId(value = "oper_id", type = IdType.AUTO)
private Long operId;
/**
* 操作名称
*/
private String operName;
/**
* 操作类型
*/
private String operType;
/**
* 方法名称
*/
private String methodName;
/**
* 请求方式
*/
private String requestMethod;
/**
* 系统类型
*/
private String systemType;
/**
* 操作人员
*/
private String operMan;
/**
* 请求URL
*/
private String operUrl;
/**
* 主机地址
*/
private String operIp;
/**
* 操作地点
*/
private String operLocation;
/**
* 请求参数
*/
private String operParam;
/**
* 返回参数
*/
private String jsonResult;
/**
* 操作状态
*/
private String operStatus;
/**
* 错误消息
*/
private String errorMsg;
/**
* 操作时间
*/
private LocalDateTime operTime;
/**
* 消耗时间
*/
private String costTime;
/**
* @description [获取操作状态文本]
* @author Leocoder
*/
public String getOperStatusText() {
return "0".equals(operStatus) ? "正常" : "异常";
}
/**
* @description [获取操作类型文本]
* @author Leocoder
*/
public String getOperTypeText() {
switch (operType) {
case "INSERT": return "新增";
case "UPDATE": return "修改";
case "DELETE": return "删除";
case "SELECT": return "查询";
case "IMPORT": return "导入";
case "EXPORT": return "导出";
default: return "其它";
}
}
/**
* @description [获取系统类型文本]
* @author Leocoder
*/
public String getSystemTypeText() {
switch (systemType) {
case "MANAGER": return "后台用户";
case "PHONE": return "手机端用户";
default: return "其它";
}
}
}

View File

@ -0,0 +1,108 @@
package org.leocoder.system.controller.operlog;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.domain.pojo.system.SysOperLog;
import org.leocoder.domain.model.vo.system.SysOperLogVo;
import org.leocoder.system.service.operlog.SysOperLogService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [操作日志控制器]
*/
@RestController
@RequestMapping("/coder")
@Slf4j
@RequiredArgsConstructor
public class SysOperLogController {
private final SysOperLogService sysOperLogService;
/**
* @description [分页查询操作日志]
* @author Leocoder
*/
@GetMapping("/sysOperLog/listPage")
@SaCheckPermission("system:operlog:search")
public IPage<SysOperLog> listPage(SysOperLogVo vo) {
return sysOperLogService.listPage(vo);
}
/**
* @description [根据ID查询操作日志详情]
* @author Leocoder
*/
@GetMapping("/sysOperLog/getById/{operId}")
@SaCheckPermission("system:operlog:search")
public SysOperLog getById(@PathVariable Long operId) {
return sysOperLogService.getDetail(operId);
}
/**
* @description [查询操作日志详情]
* @author Leocoder
*/
@GetMapping("/sysOperLog/getDetailById/{operId}")
@SaCheckPermission("system:operlog:search")
public SysOperLog getDetailById(@PathVariable Long operId) {
return sysOperLogService.getDetail(operId);
}
/**
* @description [删除操作日志]
* @author Leocoder
*/
@PostMapping("/sysOperLog/deleteById/{operId}")
@SaCheckPermission("system:operlog:delete")
public void deleteById(@PathVariable Long operId) {
List<Long> operIds = List.of(operId);
sysOperLogService.deleteByIds(operIds);
}
/**
* @description [批量删除操作日志]
* @author Leocoder
*/
@PostMapping("/sysOperLog/batchDelete")
@SaCheckPermission("system:operlog:delete")
public void batchDelete(@RequestBody List<Long> operIds) {
sysOperLogService.deleteByIds(operIds);
}
/**
* @description [清空操作日志]
* @author Leocoder
*/
@PostMapping("/sysOperLog/clear")
@SaCheckPermission("system:operlog:delete")
public void clear() {
sysOperLogService.clear();
}
/**
* @description [获取操作统计]
* @author Leocoder
*/
@GetMapping("/sysOperLog/statistics")
@SaCheckPermission("system:operlog:search")
public Map<String, Object> statistics() {
return sysOperLogService.getStatistics();
}
/**
* @description [获取仪表盘统计]
* @author Leocoder
*/
@GetMapping("/sysOperLog/dashboard")
@SaCheckPermission("system:operlog:search")
public Map<String, Object> dashboard() {
return sysOperLogService.getDashboardStats();
}
}

View File

@ -0,0 +1,65 @@
package org.leocoder.system.service.operlog;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
import org.leocoder.domain.pojo.system.SysOperLog;
import org.leocoder.domain.model.vo.system.SysOperLogVo;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [操作日志服务接口]
*/
public interface SysOperLogService extends IService<SysOperLog> {
/**
* @description [分页查询操作日志]
* @author Leocoder
*/
Page<SysOperLog> listPage(SysOperLogVo vo);
/**
* @description [根据ID查询操作日志详情]
* @author Leocoder
*/
SysOperLog getDetail(Long operId);
/**
* @description [批量删除操作日志]
* @author Leocoder
*/
void deleteByIds(List<Long> operIds);
/**
* @description [清空操作日志]
* @author Leocoder
*/
void clear();
/**
* @description [导出操作日志]
* @author Leocoder
*/
void export(SysOperLogVo vo, HttpServletResponse response);
/**
* @description [获取操作统计]
* @author Leocoder
*/
Map<String, Object> getStatistics();
/**
* @description [清理过期日志]
* @author Leocoder
*/
void cleanExpiredLogs(int days);
/**
* @description [获取仪表盘统计]
* @author Leocoder
*/
Map<String, Object> getDashboardStats();
}

View File

@ -0,0 +1,290 @@
package org.leocoder.system.service.operlog;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.common.exception.coder.YUtil;
import org.leocoder.common.utils.cache.RedisUtil;
import org.leocoder.common.utils.date.DateUtil;
import org.leocoder.domain.pojo.system.SysOperLog;
import org.leocoder.domain.model.vo.system.SysOperLogVo;
import org.leocoder.mp.mapper.system.SysOperLogMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
/**
* @author Leocoder
* @description [操作日志服务实现]
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class SysOperLogServiceImpl extends ServiceImpl<SysOperLogMapper, SysOperLog> implements SysOperLogService {
private final RedisUtil redisUtil;
/**
* @description [分页查询操作日志]
* @author Leocoder
*/
@Override
public Page<SysOperLog> listPage(SysOperLogVo vo) {
Page<SysOperLog> page = Page.of(vo.getPageNo(), vo.getPageSize());
LambdaQueryWrapper<SysOperLog> wrapper = buildQueryWrapper(vo);
return this.page(page, wrapper);
}
/**
* @description [根据ID查询操作日志详情]
* @author Leocoder
*/
@Override
public SysOperLog getDetail(Long operId) {
YUtil.isNull(operId, "操作日志ID不能为空");
SysOperLog operLog = this.getById(operId);
YUtil.isNull(operLog, "操作日志不存在");
return operLog;
}
/**
* @description [批量删除操作日志]
* @author Leocoder
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteByIds(List<Long> operIds) {
log.info("开始删除操作日志ID列表: {}", operIds);
YUtil.isNull(operIds, "操作日志ID列表不能为空");
YUtil.isTrue(operIds.isEmpty(), "操作日志ID列表不能为空");
try {
// 检查要删除的记录是否存在
long existCount = this.count(new LambdaQueryWrapper<SysOperLog>()
.in(SysOperLog::getOperId, operIds));
log.info("要删除的记录数量: {},实际存在数量: {}", operIds.size(), existCount);
// 执行删除
boolean result = this.removeByIds(operIds);
log.info("删除操作执行结果: {}删除ID数量: {}", result, operIds.size());
if (!result) {
throw new RuntimeException("删除操作失败");
}
log.info("批量删除操作日志成功,数量: {}", operIds.size());
} catch (Exception e) {
log.error("删除操作日志失败ID列表: {}", operIds, e);
throw e;
}
}
/**
* @description [清空操作日志]
* @author Leocoder
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void clear() {
// 清空所有操作日志
this.remove(new LambdaQueryWrapper<>());
// 清空相关缓存
clearAllCache();
log.info("清空操作日志成功");
}
/**
* @description [导出操作日志]
* @author Leocoder
*/
@Override
public void export(SysOperLogVo vo, HttpServletResponse response) {
// 构建查询条件
LambdaQueryWrapper<SysOperLog> wrapper = buildQueryWrapper(vo);
// 查询所有匹配的记录
List<SysOperLog> list = this.list(wrapper);
// 导出Excel
try {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = "操作日志_" + DateUtil.format(new Date(), "yyyyMMdd_HHmmss") + ".xlsx";
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
// 使用EasyExcel导出
// EasyExcel.write(response.getOutputStream(), SysOperLog.class)
// .sheet("操作日志")
// .doWrite(list);
log.info("导出操作日志成功,数量: {}", list.size());
} catch (Exception e) {
log.error("导出操作日志失败", e);
throw new RuntimeException("导出操作日志失败", e);
}
}
/**
* @description [获取操作统计]
* @author Leocoder
*/
@Override
public Map<String, Object> getStatistics() {
Map<String, Object> stats = new HashMap<>();
// 从缓存获取今日统计
String today = DateUtil.format(new Date(), "yyyy-MM-dd");
String totalKey = "oper:stat:total:" + today;
String errorKey = "oper:stat:error:" + today;
Object todayCountObj = redisUtil.getKey(totalKey);
Object errorCountObj = redisUtil.getKey(errorKey);
Long todayCount = todayCountObj != null ? Long.valueOf(todayCountObj.toString()) : 0L;
Long errorCount = errorCountObj != null ? Long.valueOf(errorCountObj.toString()) : 0L;
stats.put("todayCount", todayCount);
stats.put("errorCount", errorCount);
// 从数据库获取总统计
Long totalCount = this.count();
stats.put("totalCount", totalCount);
// 获取操作类型分布
List<Map<String, Object>> typeStats = baseMapper.getOperTypeStats();
stats.put("typeStats", typeStats);
// 获取每日统计
List<Map<String, Object>> dailyStats = baseMapper.getDailyStats();
stats.put("dailyStats", dailyStats);
// 获取用户统计
List<Map<String, Object>> userStats = baseMapper.getUserStats();
stats.put("userStats", userStats);
return stats;
}
/**
* @description [清理过期日志]
* @author Leocoder
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void cleanExpiredLogs(int days) {
YUtil.isTrue(days <= 0, "保留天数必须大于0");
LocalDateTime expireTime = LocalDateTime.now().minusDays(days);
LambdaQueryWrapper<SysOperLog> wrapper = new LambdaQueryWrapper<>();
wrapper.lt(SysOperLog::getOperTime, expireTime);
long deleteCount = this.count(wrapper);
this.remove(wrapper);
log.info("清理{}天前的操作日志成功,删除数量: {}", days, deleteCount);
}
/**
* @description [获取仪表盘统计]
* @author Leocoder
*/
@Override
public Map<String, Object> getDashboardStats() {
Map<String, Object> stats = new HashMap<>();
// 今日统计
String today = DateUtil.format(new Date(), "yyyy-MM-dd");
Long todayCount = this.count(new LambdaQueryWrapper<SysOperLog>()
.ge(SysOperLog::getOperTime, today + " 00:00:00")
.le(SysOperLog::getOperTime, today + " 23:59:59"));
// 昨日统计
Date yesterdayDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
String yesterday = DateUtil.format(yesterdayDate, "yyyy-MM-dd");
Long yesterdayCount = this.count(new LambdaQueryWrapper<SysOperLog>()
.ge(SysOperLog::getOperTime, yesterday + " 00:00:00")
.le(SysOperLog::getOperTime, yesterday + " 23:59:59"));
// 本月统计
// 获取本月第一天
java.util.Calendar calendar = java.util.Calendar.getInstance();
calendar.set(java.util.Calendar.DAY_OF_MONTH, 1);
String monthStart = DateUtil.format(calendar.getTime(), "yyyy-MM-dd");
Long monthCount = this.count(new LambdaQueryWrapper<SysOperLog>()
.ge(SysOperLog::getOperTime, monthStart + " 00:00:00"));
// 总统计
Long totalCount = this.count();
stats.put("todayCount", todayCount);
stats.put("yesterdayCount", yesterdayCount);
stats.put("monthCount", monthCount);
stats.put("totalCount", totalCount);
// 增长率计算
if (yesterdayCount > 0) {
double growthRate = ((double) (todayCount - yesterdayCount) / yesterdayCount) * 100;
stats.put("growthRate", String.format("%.2f", growthRate));
} else {
stats.put("growthRate", "0.00");
}
return stats;
}
/**
* @description [构建查询条件]
* @author Leocoder
*/
private LambdaQueryWrapper<SysOperLog> buildQueryWrapper(SysOperLogVo vo) {
LambdaQueryWrapper<SysOperLog> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(vo.getOperName()), SysOperLog::getOperName, vo.getOperName())
.eq(StringUtils.isNotBlank(vo.getOperType()), SysOperLog::getOperType, vo.getOperType())
.eq(StringUtils.isNotBlank(vo.getSystemType()), SysOperLog::getSystemType, vo.getSystemType())
.like(StringUtils.isNotBlank(vo.getOperMan()), SysOperLog::getOperMan, vo.getOperMan())
.eq(StringUtils.isNotBlank(vo.getOperStatus()), SysOperLog::getOperStatus, vo.getOperStatus())
.like(StringUtils.isNotBlank(vo.getOperIp()), SysOperLog::getOperIp, vo.getOperIp())
.like(StringUtils.isNotBlank(vo.getMethodName()), SysOperLog::getMethodName, vo.getMethodName())
.like(StringUtils.isNotBlank(vo.getOperUrl()), SysOperLog::getOperUrl, vo.getOperUrl())
.eq(StringUtils.isNotBlank(vo.getRequestMethod()), SysOperLog::getRequestMethod, vo.getRequestMethod())
.like(StringUtils.isNotBlank(vo.getOperLocation()), SysOperLog::getOperLocation, vo.getOperLocation())
.ge(vo.getOperTimeStart() != null, SysOperLog::getOperTime, vo.getOperTimeStart())
.le(vo.getOperTimeEnd() != null, SysOperLog::getOperTime, vo.getOperTimeEnd())
.orderByDesc(SysOperLog::getOperTime);
return wrapper;
}
/**
* @description [清空所有缓存]
* @author Leocoder
*/
private void clearAllCache() {
try {
// 清空统计缓存
Collection<String> keys = redisUtil.keys("oper:stat:*");
if (keys != null && !keys.isEmpty()) {
redisUtil.deleteKeys(keys);
}
log.info("清空操作日志统计缓存成功");
} catch (Exception e) {
log.warn("清空操作日志统计缓存失败", e);
}
}
}

View File

@ -0,0 +1,45 @@
package org.leocoder.system.task;
import org.leocoder.system.service.operlog.SysOperLogService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
import lombok.extern.slf4j.Slf4j;
import lombok.RequiredArgsConstructor;
/**
* @author Leocoder
* @description [操作日志清理任务]
*/
@Component
@ConditionalOnProperty(name = "coder.oper-log.clean.enabled", havingValue = "true", matchIfMissing = true)
@Slf4j
@RequiredArgsConstructor
public class OperLogCleanTask {
private final SysOperLogService sysOperLogService;
@Value("${coder.oper-log.clean.days:90}")
private int cleanDays;
/**
* @description [清理过期操作日志]
* @author Leocoder
*/
@Scheduled(cron = "${coder.oper-log.clean.cron:0 0 2 * * ?}")
public void cleanExpiredLogs() {
try {
log.info("开始清理{}天前的操作日志", cleanDays);
long startTime = System.currentTimeMillis();
sysOperLogService.cleanExpiredLogs(cleanDays);
long endTime = System.currentTimeMillis();
log.info("操作日志清理任务完成,耗时: {}ms", endTime - startTime);
} catch (Exception e) {
log.error("操作日志清理任务失败", e);
}
}
}

View File

@ -0,0 +1,73 @@
package org.leocoder.mp.mapper.system;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.leocoder.domain.pojo.system.SysOperLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [操作日志Mapper接口]
*/
@Mapper
public interface SysOperLogMapper extends BaseMapper<SysOperLog> {
/**
* @description [获取操作类型统计]
* @author Leocoder
*/
@Select("SELECT oper_type as operType, COUNT(*) as count FROM sys_oper_log GROUP BY oper_type")
List<Map<String, Object>> getOperTypeStats();
/**
* @description [获取每日操作统计]
* @author Leocoder
*/
@Select("SELECT DATE(oper_time) as date, COUNT(*) as count FROM sys_oper_log " +
"WHERE oper_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY DATE(oper_time) ORDER BY date")
List<Map<String, Object>> getDailyStats();
/**
* @description [获取用户操作统计]
* @author Leocoder
*/
@Select("SELECT oper_man as operMan, COUNT(*) as count FROM sys_oper_log " +
"WHERE oper_time >= DATE_SUB(NOW(), INTERVAL 1 DAY) GROUP BY oper_man " +
"ORDER BY count DESC LIMIT 10")
List<Map<String, Object>> getUserStats();
/**
* @description [获取错误操作统计]
* @author Leocoder
*/
@Select("SELECT COUNT(*) as errorCount FROM sys_oper_log WHERE oper_status = '1'")
Long getErrorCount();
/**
* @description [获取操作统计信息]
* @author Leocoder
*/
@Select("SELECT " +
"COUNT(*) as totalCount, " +
"COUNT(CASE WHEN oper_status = '0' THEN 1 END) as successCount, " +
"COUNT(CASE WHEN oper_status = '1' THEN 1 END) as failCount, " +
"COUNT(CASE WHEN DATE(oper_time) = CURDATE() THEN 1 END) as todayCount " +
"FROM sys_oper_log")
Map<String, Object> getOperationStats();
/**
* @description [批量插入操作日志]
* @author Leocoder
*/
int batchInsert(@Param("list") List<SysOperLog> operLogs);
/**
* @description [清理过期日志]
* @author Leocoder
*/
int cleanExpiredLogs(@Param("expireTime") LocalDateTime expireTime);
}

View File

@ -0,0 +1,51 @@
<?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">
<mapper namespace="org.leocoder.mp.mapper.system.SysOperLogMapper">
<!-- 基础字段映射 -->
<resultMap id="BaseResultMap" type="org.leocoder.domain.pojo.system.SysOperLog">
<id column="oper_id" property="operId" />
<result column="oper_name" property="operName" />
<result column="oper_type" property="operType" />
<result column="method_name" property="methodName" />
<result column="request_method" property="requestMethod" />
<result column="system_type" property="systemType" />
<result column="oper_man" property="operMan" />
<result column="oper_url" property="operUrl" />
<result column="oper_ip" property="operIp" />
<result column="oper_location" property="operLocation" />
<result column="oper_param" property="operParam" />
<result column="json_result" property="jsonResult" />
<result column="oper_status" property="operStatus" />
<result column="error_msg" property="errorMsg" />
<result column="oper_time" property="operTime" />
<result column="cost_time" property="costTime" />
</resultMap>
<!-- 批量插入操作日志 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO sys_oper_log (
oper_name, oper_type, method_name, request_method, system_type,
oper_man, oper_url, oper_ip, oper_location, oper_param,
json_result, oper_status, error_msg, oper_time, cost_time
) VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.operName}, #{item.operType}, #{item.methodName},
#{item.requestMethod}, #{item.systemType}, #{item.operMan},
#{item.operUrl}, #{item.operIp}, #{item.operLocation},
#{item.operParam}, #{item.jsonResult}, #{item.operStatus},
#{item.errorMsg}, #{item.operTime}, #{item.costTime}
)
</foreach>
</insert>
<!-- 清理过期日志 -->
<delete id="cleanExpiredLogs" parameterType="java.time.LocalDateTime">
DELETE FROM sys_oper_log WHERE oper_time &lt; #{expireTime}
</delete>
</mapper>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.thin</groupId>
<artifactId>coder-common-thin-plugins</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>coder-common-thin-oper-logs</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@ -0,0 +1,22 @@
package org.leocoder.operlog.annotation;
import org.leocoder.operlog.aspect.OperLogAspect;
import org.leocoder.operlog.config.OperLogAsyncConfig;
import org.leocoder.operlog.service.OperLogAsyncService;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* @author Leocoder
* @description [启用操作日志注解]
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({
OperLogAspect.class,
OperLogAsyncService.class,
OperLogAsyncConfig.class
})
public @interface EnableOperLog {
}

View File

@ -0,0 +1,50 @@
package org.leocoder.operlog.annotation;
import org.leocoder.domain.enums.oper.OperType;
import org.leocoder.domain.enums.oper.SystemType;
import java.lang.annotation.*;
/**
* @author Leocoder
* @description [操作日志注解]
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperLog {
/**
* 操作名称
*/
String value() default "";
/**
* 操作类型
*/
OperType operType() default OperType.OTHER;
/**
* 系统类型
*/
SystemType systemType() default SystemType.MANAGER;
/**
* 是否保存请求参数
*/
boolean saveRequestData() default true;
/**
* 是否保存返回结果
*/
boolean saveResponseData() default true;
/**
* 排除敏感字段
*/
String[] excludeFields() default {};
/**
* 是否异步记录
*/
boolean async() default true;
}

View File

@ -0,0 +1,580 @@
package org.leocoder.operlog.aspect;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.leocoder.common.satoken.CoderLoginUtil;
import org.leocoder.common.utils.date.DateUtil;
import org.leocoder.common.utils.ip.IpAddressUtil;
import org.leocoder.common.utils.ip.IpUtil;
import org.leocoder.common.utils.json.JsonUtil;
import org.leocoder.common.utils.string.StringUtil;
import org.leocoder.domain.pojo.system.SysOperLog;
import org.leocoder.operlog.annotation.OperLog;
import org.leocoder.operlog.service.OperLogAsyncService;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Map;
/**
* @author Leocoder
* @description [操作日志切面处理器]
*/
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class OperLogAspect {
private final OperLogAsyncService operLogAsyncService;
private final IpAddressUtil ipAddressUtil;
/**
* @description [配置切入点]
* @author Leocoder
*/
@Pointcut("@annotation(org.leocoder.operlog.annotation.OperLog)")
public void operLogPointcut() {
}
/**
* @description [处理操作日志记录]
* @author Leocoder
*/
@Around("operLogPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = null;
Exception exception = null;
try {
// 执行原方法
result = joinPoint.proceed();
return result;
} catch (Exception e) {
exception = e;
throw e;
} finally {
try {
// 处理操作日志
handleOperLog(joinPoint, result, exception, startTime);
} catch (Exception e) {
log.error("操作日志记录失败", e);
}
}
}
/**
* @description [处理操作日志]
* @author Leocoder
*/
private void handleOperLog(ProceedingJoinPoint joinPoint, Object result,
Exception exception, long startTime) {
// 获取注解信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
OperLog operLog = method.getAnnotation(OperLog.class);
if (operLog == null) {
return;
}
// 构建操作日志对象
SysOperLog operLogEntity = buildOperLog(joinPoint, operLog, result, exception, startTime);
// 保存操作日志
if (operLog.async()) {
operLogAsyncService.saveOperLog(operLogEntity);
} else {
operLogAsyncService.saveOperLogSync(operLogEntity);
}
}
/**
* @description [构建操作日志对象]
* @author Leocoder
*/
private SysOperLog buildOperLog(ProceedingJoinPoint joinPoint, OperLog operLog,
Object result, Exception exception, long startTime) {
SysOperLog operLogEntity = new SysOperLog();
// 设置基本信息
operLogEntity.setOperName(getOperName(operLog, joinPoint));
operLogEntity.setOperType(operLog.operType().getCode());
operLogEntity.setSystemType(operLog.systemType().getCode());
operLogEntity.setOperTime(LocalDateTime.now());
operLogEntity.setCostTime(String.valueOf(System.currentTimeMillis() - startTime));
// 设置方法信息
setMethodInfo(operLogEntity, joinPoint);
// 设置请求信息
setRequestInfo(operLogEntity);
// 设置用户信息
setUserInfo(operLogEntity);
// 设置请求参数
if (operLog.saveRequestData()) {
setRequestData(operLogEntity, joinPoint, operLog.excludeFields());
}
// 设置返回结果
if (operLog.saveResponseData()) {
if (result != null) {
setResponseData(operLogEntity, result);
} else if (exception == null) {
// 对于void方法且无异常的情况设置默认成功返回信息
setDefaultSuccessResponse(operLogEntity, operLog);
}
}
// 设置异常信息
if (exception != null) {
setExceptionInfo(operLogEntity, exception);
} else {
operLogEntity.setOperStatus("0");
}
return operLogEntity;
}
/**
* @description [获取操作名称]
* @author Leocoder
*/
private String getOperName(OperLog operLog, ProceedingJoinPoint joinPoint) {
String operName = operLog.value();
if (StringUtils.isBlank(operName)) {
// 如果没有设置操作名称使用方法名
operName = joinPoint.getSignature().getName();
}
return operName;
}
/**
* @description [设置方法信息]
* @author Leocoder
*/
private void setMethodInfo(SysOperLog operLogEntity, ProceedingJoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
String fullMethodName = className + "." + methodName;
// 限制方法名长度避免数据库字段超限数据库字段为varchar(64)
operLogEntity.setMethodName(StringUtil.substring(fullMethodName, 0, 64));
}
/**
* @description [设置请求信息]
* @author Leocoder
*/
private void setRequestInfo(SysOperLog operLogEntity) {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
operLogEntity.setRequestMethod(request.getMethod());
operLogEntity.setOperUrl(StringUtil.substring(request.getRequestURI(), 0, 255));
operLogEntity.setOperIp(IpUtil.getIpAddr(request));
operLogEntity.setOperLocation(ipAddressUtil.getAddress(operLogEntity.getOperIp()));
}
} catch (Exception e) {
log.warn("设置请求信息失败", e);
}
}
/**
* @description [设置用户信息]
* @author Leocoder
*/
private void setUserInfo(SysOperLog operLogEntity) {
try {
String userName = CoderLoginUtil.getUserName();
operLogEntity.setOperMan(userName);
} catch (Exception e) {
operLogEntity.setOperMan("匿名用户");
}
}
/**
* @description [设置请求参数]
* @author Leocoder
*/
private void setRequestData(SysOperLog operLogEntity, ProceedingJoinPoint joinPoint, String[] excludeFields) {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
Map<String, String[]> paramMap = request.getParameterMap();
if (!paramMap.isEmpty()) {
String params = toJsonStringWithExclude(paramMap, excludeFields);
operLogEntity.setOperParam(StringUtil.substring(params, 0, 2000));
} else {
// 如果没有HTTP请求参数获取方法参数通常是POST请求的JSON体
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
// 过滤掉非业务参数
Object[] businessArgs = filterBusinessArgs(args);
if (businessArgs.length > 0) {
String params = toJsonStringWithExclude(businessArgs, excludeFields);
operLogEntity.setOperParam(StringUtil.substring(params, 0, 2000));
}
}
}
}
} catch (Exception e) {
log.warn("设置请求参数失败", e);
}
}
/**
* @description [设置返回结果]
* @author Leocoder
*/
private void setResponseData(SysOperLog operLogEntity, Object result) {
try {
if (result != null) {
String jsonResult = JsonUtil.toJsonString(result);
operLogEntity.setJsonResult(StringUtil.substring(jsonResult, 0, 2000));
}
} catch (Exception e) {
log.warn("设置返回结果失败", e);
}
}
/**
* @description [设置异常信息]
* @author Leocoder
*/
private void setExceptionInfo(SysOperLog operLogEntity, Exception exception) {
operLogEntity.setOperStatus("1");
String errorMsg = StringUtil.substring(exception.getMessage(), 0, 2000);
operLogEntity.setErrorMsg(errorMsg);
}
/**
* @description [设置默认成功返回信息]
* @author Leocoder
*/
private void setDefaultSuccessResponse(SysOperLog operLogEntity, OperLog operLog) {
try {
// 根据操作类型生成相应的成功信息
String operationType = operLog.operType().getCode();
String operationName = operLog.value();
StringBuilder responseBuilder = new StringBuilder();
responseBuilder.append("{");
responseBuilder.append("\"success\":true,");
responseBuilder.append("\"code\":200,");
responseBuilder.append("\"message\":\"").append(getSuccessMessage(operationType, operationName)).append("\",");
responseBuilder.append("\"operationType\":\"").append(operationType).append("\",");
responseBuilder.append("\"timestamp\":\"").append(DateUtil.format(new java.util.Date(), "yyyy-MM-dd HH:mm:ss")).append("\"");
responseBuilder.append("}");
String jsonResult = responseBuilder.toString();
operLogEntity.setJsonResult(StringUtil.substring(jsonResult, 0, 2000));
} catch (Exception e) {
log.warn("设置默认成功返回信息失败", e);
// 失败时设置简单的成功信息
operLogEntity.setJsonResult("{\"success\":true,\"message\":\"操作成功\"}");
}
}
/**
* @description [根据操作类型获取成功消息]
* @author Leocoder
*/
private String getSuccessMessage(String operationType, String operationName) {
switch (operationType) {
case "INSERT":
return StringUtils.isNotBlank(operationName) ? operationName + "成功" : "新增操作成功";
case "UPDATE":
return StringUtils.isNotBlank(operationName) ? operationName + "成功" : "修改操作成功";
case "DELETE":
return StringUtils.isNotBlank(operationName) ? operationName + "成功" : "删除操作成功";
case "SELECT":
return StringUtils.isNotBlank(operationName) ? operationName + "成功" : "查询操作成功";
case "IMPORT":
return StringUtils.isNotBlank(operationName) ? operationName + "成功" : "导入操作成功";
case "EXPORT":
return StringUtils.isNotBlank(operationName) ? operationName + "成功" : "导出操作成功";
default:
return StringUtils.isNotBlank(operationName) ? operationName + "成功" : "操作成功";
}
}
/**
* @description [转换为JSON字符串并排除指定字段]
* @author Leocoder
*/
private String toJsonStringWithExclude(Object obj, String[] excludeFields) {
try {
if (obj == null) {
return "";
}
// 先尝试转换为JSON字符串
String jsonString;
try {
jsonString = JsonUtil.toJsonString(obj);
// 检查是否转换失败JsonUtil可能返回错误信息
if (jsonString == null || jsonString.startsWith("JSON数据转换失败") || jsonString.startsWith("数据转换失败")) {
return createSimpleString(obj, excludeFields);
}
} catch (Exception e) {
log.debug("JsonUtil转换失败使用备用方案", e);
return createSimpleString(obj, excludeFields);
}
// 如果没有需要排除的字段直接返回
if (excludeFields == null || excludeFields.length == 0) {
return jsonString;
}
// 解析JSON并排除指定字段
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
if (jsonNode.isObject()) {
ObjectNode objectNode = (ObjectNode) jsonNode;
for (String field : excludeFields) {
objectNode.remove(field);
}
return objectNode.toString();
}
return jsonString;
} catch (Exception e) {
log.debug("JSON排除字段失败返回原始字符串", e);
return jsonString;
}
} catch (Exception e) {
log.warn("参数序列化失败", e);
return "[参数转换失败]";
}
}
/**
* @description [创建简单字符串描述]
* @author Leocoder
*/
private String createSimpleString(Object obj, String[] excludeFields) {
try {
if (obj == null) {
return "null";
}
// 对于数组尝试展示具体内容
if (obj.getClass().isArray()) {
Object[] array = (Object[]) obj;
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < array.length && i < 3; i++) { // 最多显示3个元素
if (i > 0) sb.append(", ");
sb.append(createObjectString(array[i], excludeFields));
}
if (array.length > 3) {
sb.append(", ...").append(array.length - 3).append(" more");
}
sb.append("]");
return sb.toString();
}
// 对于集合
if (obj instanceof java.util.Collection) {
java.util.Collection<?> collection = (java.util.Collection<?>) obj;
return "[Collection, size=" + collection.size() + "]";
}
// 对于Map
if (obj instanceof java.util.Map) {
java.util.Map<?, ?> map = (java.util.Map<?, ?>) obj;
return "[Map, size=" + map.size() + "]";
}
// 对于其他对象
return createObjectString(obj, excludeFields);
} catch (Exception e) {
return "[object parsing failed]";
}
}
/**
* @description [创建对象字符串描述]
* @author Leocoder
*/
private String createObjectString(Object obj, String[] excludeFields) {
try {
if (obj == null) {
return "null";
}
// 基本类型直接返回
if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) {
return obj.toString();
}
// 日期类型
if (obj instanceof java.util.Date) {
return DateUtil.format((java.util.Date) obj, "yyyy-MM-dd HH:mm:ss");
}
if (obj instanceof java.time.LocalDateTime) {
java.time.LocalDateTime ldt = (java.time.LocalDateTime) obj;
return ldt.toString();
}
if (obj instanceof java.time.LocalDate) {
return obj.toString();
}
// 复杂对象提取关键字段信息
return extractObjectInfo(obj, excludeFields);
} catch (Exception e) {
return "{object}";
}
}
/**
* @description [提取对象信息]
* @author Leocoder
*/
private String extractObjectInfo(Object obj, String[] excludeFields) {
try {
StringBuilder sb = new StringBuilder("{");
java.lang.reflect.Field[] fields = obj.getClass().getDeclaredFields();
boolean first = true;
int fieldCount = 0;
for (java.lang.reflect.Field field : fields) {
try {
field.setAccessible(true);
String fieldName = field.getName();
// 跳过系统字段
if (fieldName.startsWith("$") || fieldName.equals("serialVersionUID")) {
continue;
}
// 排除指定字段
if (excludeFields != null) {
boolean excluded = false;
for (String excludeField : excludeFields) {
if (fieldName.equalsIgnoreCase(excludeField)) {
excluded = true;
break;
}
}
if (excluded) {
continue;
}
}
// 跳过敏感字段
if (fieldName.toLowerCase().contains("password") ||
fieldName.toLowerCase().contains("salt") ||
fieldName.toLowerCase().contains("token")) {
continue;
}
Object value = field.get(obj);
if (value != null) {
if (!first) sb.append(", ");
sb.append(fieldName).append("=");
// 处理不同类型的值
String valueStr;
if (value instanceof String || value instanceof Number || value instanceof Boolean) {
valueStr = value.toString();
} else if (value instanceof java.util.Date) {
valueStr = DateUtil.format((java.util.Date) value, "yyyy-MM-dd HH:mm:ss");
} else if (value instanceof java.time.LocalDateTime || value instanceof java.time.LocalDate) {
valueStr = value.toString();
} else if (value instanceof java.util.Collection) {
java.util.Collection<?> collection = (java.util.Collection<?>) value;
valueStr = "[Collection, size=" + collection.size() + "]";
} else if (value instanceof java.util.Map) {
java.util.Map<?, ?> map = (java.util.Map<?, ?>) value;
valueStr = "[Map, size=" + map.size() + "]";
} else {
valueStr = value.toString();
}
// 限制字段值长度
if (valueStr.length() > 50) {
valueStr = valueStr.substring(0, 47) + "...";
}
sb.append(valueStr);
first = false;
fieldCount++;
// 最多显示8个字段避免过长
if (fieldCount >= 8 || sb.length() > 300) break;
}
} catch (Exception e) {
// 忽略单个字段错误
}
}
sb.append("}");
return sb.toString();
} catch (Exception e) {
String className = obj.getClass().getSimpleName();
return "{" + className + " object}";
}
}
/**
* @description [过滤业务参数排除HttpServletResponse等非业务对象]
* @author Leocoder
*/
private Object[] filterBusinessArgs(Object[] args) {
if (args == null || args.length == 0) {
return new Object[0];
}
java.util.List<Object> businessArgs = new java.util.ArrayList<>();
for (Object arg : args) {
if (arg == null) {
continue;
}
String className = arg.getClass().getName();
// 排除Spring框架和Servlet相关对象
if (className.startsWith("jakarta.servlet.") ||
className.startsWith("javax.servlet.") ||
className.startsWith("org.springframework.") ||
className.startsWith("org.apache.catalina.")) {
continue;
}
businessArgs.add(arg);
}
return businessArgs.toArray(new Object[0]);
}
}

View File

@ -0,0 +1,58 @@
package org.leocoder.operlog.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.beans.factory.annotation.Value;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author Leocoder
* @description [操作日志异步配置]
*/
@Configuration
@EnableAsync
public class OperLogAsyncConfig {
@Value("${coder.oper-log.async.core-pool-size:2}")
private int corePoolSize;
@Value("${coder.oper-log.async.max-pool-size:5}")
private int maxPoolSize;
@Value("${coder.oper-log.async.queue-capacity:1000}")
private int queueCapacity;
@Value("${coder.oper-log.async.keep-alive-seconds:60}")
private int keepAliveSeconds;
/**
* @description [操作日志线程池]
* @author Leocoder
*/
@Bean("operLogExecutor")
public ThreadPoolTaskExecutor operLogExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(corePoolSize);
// 设置最大线程数
executor.setMaxPoolSize(maxPoolSize);
// 设置队列容量
executor.setQueueCapacity(queueCapacity);
// 设置线程名称前缀
executor.setThreadNamePrefix("oper-log-");
// 设置线程空闲时间
executor.setKeepAliveSeconds(keepAliveSeconds);
// 设置拒绝策略调用者运行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待任务完成后关闭
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}

View File

@ -0,0 +1,183 @@
package org.leocoder.operlog.service;
import org.leocoder.domain.pojo.system.SysOperLog;
import org.leocoder.common.exception.coder.YUtil;
import org.leocoder.common.utils.cache.RedisUtil;
import org.leocoder.common.utils.date.DateUtil;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import lombok.RequiredArgsConstructor;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* @author Leocoder
* @description [操作日志异步服务]
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class OperLogAsyncService {
private final RedisUtil redisUtil;
private final ApplicationContext applicationContext;
/**
* @description [异步保存操作日志]
* @author Leocoder
*/
@Async("operLogExecutor")
public CompletableFuture<Void> saveOperLog(SysOperLog operLog) {
try {
// 参数校验
YUtil.isNull(operLog, "操作日志对象不能为空");
// 通过ApplicationContext获取Service避免循环依赖
saveToDatabase(operLog);
// 更新统计缓存
updateStatCache(operLog);
log.debug("操作日志保存成功: {}", operLog.getOperName());
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
log.error("操作日志保存失败", e);
// 保存失败日志
saveFailedLog(operLog, e);
return CompletableFuture.failedFuture(e);
}
}
/**
* @description [同步保存操作日志]
* @author Leocoder
*/
public void saveOperLogSync(SysOperLog operLog) {
try {
YUtil.isNull(operLog, "操作日志对象不能为空");
saveToDatabase(operLog);
updateStatCache(operLog);
log.debug("操作日志同步保存成功: {}", operLog.getOperName());
} catch (Exception e) {
log.error("操作日志同步保存失败", e);
saveFailedLog(operLog, e);
}
}
/**
* @description [批量异步保存操作日志]
* @author Leocoder
*/
@Async("operLogExecutor")
public CompletableFuture<Void> batchSaveOperLog(List<SysOperLog> operLogs) {
try {
YUtil.isNull(operLogs, "操作日志列表不能为空");
YUtil.isTrue(operLogs.isEmpty(), "操作日志列表不能为空");
// 批量处理
operLogs.forEach(this::saveToDatabase);
// 更新批量统计缓存
operLogs.forEach(this::updateStatCache);
log.debug("批量保存操作日志成功,数量: {}", operLogs.size());
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
log.error("批量保存操作日志失败", e);
return CompletableFuture.failedFuture(e);
}
}
/**
* @description [保存到数据库]
* @author Leocoder
*/
private void saveToDatabase(SysOperLog operLog) {
try {
// 通过ApplicationContext获取SysOperLogService避免循环依赖
Object service = applicationContext.getBean("sysOperLogServiceImpl");
if (service != null) {
// 使用反射调用save方法
service.getClass().getMethod("save", Object.class).invoke(service, operLog);
log.debug("操作日志保存到数据库成功: {}", operLog.getOperName());
} else {
// 如果Service不存在暂时放到Redis队列中
String queueKey = "oper:log:queue";
redisUtil.setListLeft(queueKey, operLog);
log.debug("操作日志暂存到Redis队列: {}", operLog.getOperName());
}
} catch (Exception e) {
log.error("操作日志保存到数据库失败", e);
// 失败时放到Redis队列中
try {
String queueKey = "oper:log:queue";
redisUtil.setListLeft(queueKey, operLog);
log.debug("操作日志转存到Redis队列: {}", operLog.getOperName());
} catch (Exception ex) {
log.error("操作日志转存到Redis队列也失败", ex);
}
}
}
/**
* @description [更新统计缓存]
* @author Leocoder
*/
private void updateStatCache(SysOperLog operLog) {
try {
String today = DateUtil.format(new Date(), "yyyy-MM-dd");
// 用户操作统计
String userKey = "oper:stat:user:" + operLog.getOperMan() + ":" + today;
redisUtil.add1(userKey);
redisUtil.setCacheObjectHours(userKey, 1, 25); // 25小时过期
// 操作类型统计
String typeKey = "oper:stat:type:" + operLog.getOperType() + ":" + today;
redisUtil.add1(typeKey);
redisUtil.setCacheObjectHours(typeKey, 1, 25);
// 总操作统计
String totalKey = "oper:stat:total:" + today;
redisUtil.add1(totalKey);
redisUtil.setCacheObjectHours(totalKey, 1, 25);
// 错误统计
if ("1".equals(operLog.getOperStatus())) {
String errorKey = "oper:stat:error:" + today;
redisUtil.add1(errorKey);
redisUtil.setCacheObjectHours(errorKey, 1, 25);
}
} catch (Exception e) {
log.warn("更新统计缓存失败", e);
}
}
/**
* @description [保存失败日志]
* @author Leocoder
*/
private void saveFailedLog(SysOperLog operLog, Exception e) {
// 记录失败日志到特定文件或存储
log.warn("操作日志保存失败,详情: operName={}, operMan={}, error={}",
operLog.getOperName(), operLog.getOperMan(), e.getMessage());
// 可以考虑将失败的日志存储到Redis或文件中后续重试
try {
String failedKey = "oper:failed:" + System.currentTimeMillis();
redisUtil.setCacheObjectHours(failedKey, operLog, 24);
} catch (Exception ex) {
log.error("保存失败日志到Redis失败", ex);
}
}
}