feat: 新增操作日志基础数据模型
- 添加操作类型枚举(OperType),支持增删改查等操作分类 - 添加系统类型枚举(SystemType),区分后台用户和手机端用户 - 添加操作日志实体类(SysOperLog),包含完整的日志记录字段 - 为实体类添加状态和类型的文本转换方法,便于前端展示
This commit is contained in:
parent
c9c1519469
commit
d0312ea461
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 "其它";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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 < #{expireTime}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -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>
|
||||||
@ -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 {
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user