From d0312ea4618e5a6691beed4950e8aa7be6d992cf Mon Sep 17 00:00:00 2001 From: Leo <98382335+gaoziman@users.noreply.github.com> Date: Mon, 7 Jul 2025 22:41:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=9F=BA=E7=A1=80=E6=95=B0=E6=8D=AE=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加操作类型枚举(OperType),支持增删改查等操作分类 - 添加系统类型枚举(SystemType),区分后台用户和手机端用户 - 添加操作日志实体类(SysOperLog),包含完整的日志记录字段 - 为实体类添加状态和类型的文本转换方法,便于前端展示 --- .../thin/domain/enums/oper/OperType.java | 59 ++ .../thin/domain/enums/oper/SystemType.java | 39 ++ .../thin/domain/pojo/system/SysOperLog.java | 134 ++++ .../operlog/SysOperLogController.java | 108 ++++ .../service/operlog/SysOperLogService.java | 65 ++ .../operlog/SysOperLogServiceImpl.java | 290 +++++++++ .../thin/system/task/OperLogCleanTask.java | 45 ++ .../mapper/system/SysOperLogMapper.java | 73 +++ .../mapper/system/SysOperLogMapper.xml | 51 ++ .../coder-common-thin-oper-logs/pom.xml | 20 + .../operlog/annotation/EnableOperLog.java | 22 + .../thin/operlog/annotation/OperLog.java | 50 ++ .../thin/operlog/aspect/OperLogAspect.java | 580 ++++++++++++++++++ .../operlog/config/OperLogAsyncConfig.java | 58 ++ .../operlog/service/OperLogAsyncService.java | 183 ++++++ 15 files changed, 1777 insertions(+) create mode 100644 coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/oper/OperType.java create mode 100644 coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/oper/SystemType.java create mode 100644 coder-common-thin-model/src/main/java/org/leocoder/thin/domain/pojo/system/SysOperLog.java create mode 100644 coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/operlog/SysOperLogController.java create mode 100644 coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/service/operlog/SysOperLogService.java create mode 100644 coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/service/operlog/SysOperLogServiceImpl.java create mode 100644 coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/task/OperLogCleanTask.java create mode 100644 coder-common-thin-mybatisplus/src/main/java/org/leocoder/thin/mybatisplus/mapper/system/SysOperLogMapper.java create mode 100644 coder-common-thin-mybatisplus/src/main/resources/mapper/system/SysOperLogMapper.xml create mode 100644 coder-common-thin-plugins/coder-common-thin-oper-logs/pom.xml create mode 100644 coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/annotation/EnableOperLog.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/annotation/OperLog.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/aspect/OperLogAspect.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/config/OperLogAsyncConfig.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/service/OperLogAsyncService.java diff --git a/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/oper/OperType.java b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/oper/OperType.java new file mode 100644 index 0000000..b9d6b8c --- /dev/null +++ b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/oper/OperType.java @@ -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; + } +} \ No newline at end of file diff --git a/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/oper/SystemType.java b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/oper/SystemType.java new file mode 100644 index 0000000..07cf921 --- /dev/null +++ b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/oper/SystemType.java @@ -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; + } +} \ No newline at end of file diff --git a/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/pojo/system/SysOperLog.java b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/pojo/system/SysOperLog.java new file mode 100644 index 0000000..4be63b1 --- /dev/null +++ b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/pojo/system/SysOperLog.java @@ -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 "其它"; + } + } +} \ No newline at end of file diff --git a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/operlog/SysOperLogController.java b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/operlog/SysOperLogController.java new file mode 100644 index 0000000..c0907f5 --- /dev/null +++ b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/operlog/SysOperLogController.java @@ -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 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 operIds = List.of(operId); + sysOperLogService.deleteByIds(operIds); + } + + /** + * @description [批量删除操作日志] + * @author Leocoder + */ + @PostMapping("/sysOperLog/batchDelete") + @SaCheckPermission("system:operlog:delete") + public void batchDelete(@RequestBody List 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 statistics() { + return sysOperLogService.getStatistics(); + } + + /** + * @description [获取仪表盘统计] + * @author Leocoder + */ + @GetMapping("/sysOperLog/dashboard") + @SaCheckPermission("system:operlog:search") + public Map dashboard() { + return sysOperLogService.getDashboardStats(); + } +} \ No newline at end of file diff --git a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/service/operlog/SysOperLogService.java b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/service/operlog/SysOperLogService.java new file mode 100644 index 0000000..84dbe11 --- /dev/null +++ b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/service/operlog/SysOperLogService.java @@ -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 { + + /** + * @description [分页查询操作日志] + * @author Leocoder + */ + Page listPage(SysOperLogVo vo); + + /** + * @description [根据ID查询操作日志详情] + * @author Leocoder + */ + SysOperLog getDetail(Long operId); + + /** + * @description [批量删除操作日志] + * @author Leocoder + */ + void deleteByIds(List operIds); + + /** + * @description [清空操作日志] + * @author Leocoder + */ + void clear(); + + /** + * @description [导出操作日志] + * @author Leocoder + */ + void export(SysOperLogVo vo, HttpServletResponse response); + + /** + * @description [获取操作统计] + * @author Leocoder + */ + Map getStatistics(); + + /** + * @description [清理过期日志] + * @author Leocoder + */ + void cleanExpiredLogs(int days); + + /** + * @description [获取仪表盘统计] + * @author Leocoder + */ + Map getDashboardStats(); +} \ No newline at end of file diff --git a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/service/operlog/SysOperLogServiceImpl.java b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/service/operlog/SysOperLogServiceImpl.java new file mode 100644 index 0000000..b1d6c81 --- /dev/null +++ b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/service/operlog/SysOperLogServiceImpl.java @@ -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 implements SysOperLogService { + + private final RedisUtil redisUtil; + + /** + * @description [分页查询操作日志] + * @author Leocoder + */ + @Override + public Page listPage(SysOperLogVo vo) { + Page page = Page.of(vo.getPageNo(), vo.getPageSize()); + + LambdaQueryWrapper 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 operIds) { + log.info("开始删除操作日志,ID列表: {}", operIds); + + YUtil.isNull(operIds, "操作日志ID列表不能为空"); + YUtil.isTrue(operIds.isEmpty(), "操作日志ID列表不能为空"); + + try { + // 检查要删除的记录是否存在 + long existCount = this.count(new LambdaQueryWrapper() + .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 wrapper = buildQueryWrapper(vo); + + // 查询所有匹配的记录 + List 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 getStatistics() { + Map 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> typeStats = baseMapper.getOperTypeStats(); + stats.put("typeStats", typeStats); + + // 获取每日统计 + List> dailyStats = baseMapper.getDailyStats(); + stats.put("dailyStats", dailyStats); + + // 获取用户统计 + List> 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 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 getDashboardStats() { + Map stats = new HashMap<>(); + + // 今日统计 + String today = DateUtil.format(new Date(), "yyyy-MM-dd"); + Long todayCount = this.count(new LambdaQueryWrapper() + .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() + .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() + .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 buildQueryWrapper(SysOperLogVo vo) { + LambdaQueryWrapper 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 keys = redisUtil.keys("oper:stat:*"); + if (keys != null && !keys.isEmpty()) { + redisUtil.deleteKeys(keys); + } + log.info("清空操作日志统计缓存成功"); + } catch (Exception e) { + log.warn("清空操作日志统计缓存失败", e); + } + } +} \ No newline at end of file diff --git a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/task/OperLogCleanTask.java b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/task/OperLogCleanTask.java new file mode 100644 index 0000000..a3d6416 --- /dev/null +++ b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/task/OperLogCleanTask.java @@ -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); + } + } +} \ No newline at end of file diff --git a/coder-common-thin-mybatisplus/src/main/java/org/leocoder/thin/mybatisplus/mapper/system/SysOperLogMapper.java b/coder-common-thin-mybatisplus/src/main/java/org/leocoder/thin/mybatisplus/mapper/system/SysOperLogMapper.java new file mode 100644 index 0000000..5d0af9c --- /dev/null +++ b/coder-common-thin-mybatisplus/src/main/java/org/leocoder/thin/mybatisplus/mapper/system/SysOperLogMapper.java @@ -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 { + + /** + * @description [获取操作类型统计] + * @author Leocoder + */ + @Select("SELECT oper_type as operType, COUNT(*) as count FROM sys_oper_log GROUP BY oper_type") + List> 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> 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> 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 getOperationStats(); + + /** + * @description [批量插入操作日志] + * @author Leocoder + */ + int batchInsert(@Param("list") List operLogs); + + /** + * @description [清理过期日志] + * @author Leocoder + */ + int cleanExpiredLogs(@Param("expireTime") LocalDateTime expireTime); +} \ No newline at end of file diff --git a/coder-common-thin-mybatisplus/src/main/resources/mapper/system/SysOperLogMapper.xml b/coder-common-thin-mybatisplus/src/main/resources/mapper/system/SysOperLogMapper.xml new file mode 100644 index 0000000..a202ff2 --- /dev/null +++ b/coder-common-thin-mybatisplus/src/main/resources/mapper/system/SysOperLogMapper.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + ( + #{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} + ) + + + + + + DELETE FROM sys_oper_log WHERE oper_time < #{expireTime} + + + \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-oper-logs/pom.xml b/coder-common-thin-plugins/coder-common-thin-oper-logs/pom.xml new file mode 100644 index 0000000..2c9d15a --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oper-logs/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + org.leocoder.thin + coder-common-thin-plugins + 1.0.0 + + + coder-common-thin-oper-logs + + + 17 + 17 + UTF-8 + + + \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/annotation/EnableOperLog.java b/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/annotation/EnableOperLog.java new file mode 100644 index 0000000..9457ecc --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/annotation/EnableOperLog.java @@ -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 { +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/annotation/OperLog.java b/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/annotation/OperLog.java new file mode 100644 index 0000000..c435ee8 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/annotation/OperLog.java @@ -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; +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/aspect/OperLogAspect.java b/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/aspect/OperLogAspect.java new file mode 100644 index 0000000..edf4ab2 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/aspect/OperLogAspect.java @@ -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 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 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]); + } +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/config/OperLogAsyncConfig.java b/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/config/OperLogAsyncConfig.java new file mode 100644 index 0000000..a62f838 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/config/OperLogAsyncConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/service/OperLogAsyncService.java b/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/service/OperLogAsyncService.java new file mode 100644 index 0000000..9db86dd --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oper-logs/src/main/java/org/leocoder/thin/operlog/service/OperLogAsyncService.java @@ -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 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 batchSaveOperLog(List 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); + } + } +} \ No newline at end of file