feat: 新增heritage-plugins业务功能插件(第3部分)

- 新增heritage-easyexcel:Excel导入导出插件,支持大数据量处理
- 新增heritage-oper-logs:操作日志插件,自动记录用户操作
- 新增heritage-job:定时任务插件,支持动态管理定时任务
This commit is contained in:
Leo 2025-10-08 02:07:44 +08:00
parent 2c31bf6d53
commit db6f4b1922
25 changed files with 3092 additions and 0 deletions

View File

@ -0,0 +1,35 @@
<?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.heritage</groupId>
<artifactId>heritage-plugins</artifactId>
<version>${revision}</version>
</parent>
<name>heritage-easyexcel</name>
<artifactId>heritage-easyexcel</artifactId>
<description>EasyExcel导入导出插件</description>
<dependencies>
<!-- 通用公共模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
<!-- Aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,18 @@
package org.leocoder.heritage.easyexcel.anno;
import org.leocoder.heritage.easyexcel.bigdata.BigDataEasyExcelUtil;
import org.leocoder.heritage.easyexcel.column.ColumnExcelUtil;
import org.leocoder.heritage.easyexcel.core.utils.EasyExcelUtil;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// core文件夹是核心代码bigdata是大数据导出column是自定义列导出
@Import({EasyExcelUtil.class, ColumnExcelUtil.class, BigDataEasyExcelUtil.class})
public @interface EnableCoderEasyExcel {
}

View File

@ -0,0 +1,171 @@
package org.leocoder.heritage.easyexcel.bigdata;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Date;
import java.util.List;
/**
* @author Leocoder
* @description [EasyExcelUtil千万级别分页]
*/
public class BigDataEasyExcelUtil {
private static final Logger log = LoggerFactory.getLogger(BigDataEasyExcelUtil.class);
private static final int MAXROWS = 10000;
/**
* @description [获取默认表头内容的样式]
*/
private static HorizontalCellStyleStrategy getDefaultHorizontalCellStyleStrategy() {
/** 表头样式 **/
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// 背景色浅灰色
headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
// 字体大小
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short) 20);
headWriteCellStyle.setWriteFont(headWriteFont);
//设置表头居中对齐
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
/** 内容样式 **/
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 内容字体样式名称大小
WriteFont contentWriteFont = new WriteFont();
contentWriteFont.setFontName("宋体");
contentWriteFont.setFontHeightInPoints((short) 20);
contentWriteCellStyle.setWriteFont(contentWriteFont);
//设置内容垂直居中对齐
contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
//设置内容水平居中对齐
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
//设置边框样式
contentWriteCellStyle.setBorderLeft(BorderStyle.HAIR);
contentWriteCellStyle.setBorderTop(BorderStyle.HAIR);
contentWriteCellStyle.setBorderRight(BorderStyle.HAIR);
contentWriteCellStyle.setBorderBottom(BorderStyle.HAIR);
// 头样式与内容样式合并
return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
}
/**
* @description [导出单个Sheet最大支持xlsx格式sheet的行数]
*/
public static void writeExcel(HttpServletResponse response, List<? extends Object> data, String fileName, String sheetName, Class clazz) throws Exception {
long exportStartTime = System.currentTimeMillis();
log.info("报表导出Size: " + data.size() + "条。");
EasyExcel.write(getOutputStream(fileName, response), clazz).excelType(ExcelTypeEnum.XLSX).sheet(sheetName).registerWriteHandler(getDefaultHorizontalCellStyleStrategy()).doWrite(data);
System.out.println("报表导出结束时间:" + new Date() + ";写入耗时: " + (System.currentTimeMillis() - exportStartTime) + "ms");
}
/**
* @param data 查询结果
* @param fileName 导出文件名称
* @param clazz 映射实体class类
* @param <T> 查询结果类型
* @description [单一类型大批量数据导出适用于超过一百万的数据需要分多个sheet页来导出自动分页]
*/
public static <T> void writeExcel(HttpServletResponse response, List<T> data, String fileName, Class clazz) throws Exception {
long exportStartTime = System.currentTimeMillis();
log.info("报表导出Size: " + data.size() + "条。");
// 分割的集合
List<List<T>> lists = SplitList.splitList(data, MAXROWS);
OutputStream out = getOutputStream(fileName, response);
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(out, clazz).excelType(ExcelTypeEnum.XLSX);
ExcelWriter excelWriter = excelWriterBuilder.build();
ExcelWriterSheetBuilder excelWriterSheetBuilder;
WriteSheet writeSheet;
for (int i = 1; i <= lists.size(); i++) {
excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
excelWriterSheetBuilder.sheetNo(i);
excelWriterSheetBuilder.sheetName("sheet" + i);
writeSheet = excelWriterSheetBuilder.build();
excelWriter.write(lists.get(i - 1), writeSheet);
}
out.flush();
excelWriter.finish();
out.close();
System.out.println("报表导出结束时间:" + new Date() + ";写入耗时: " + (System.currentTimeMillis() - exportStartTime) + "ms");
}
public static OutputStream getOutputStream(String fileName, HttpServletResponse response) throws Exception {
fileName = URLEncoder.encode(fileName, "UTF-8");
// .xls
// response.setContentType("application/vnd.ms-excel");
// .xlsx
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf8");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
return response.getOutputStream();
}
/**
* @param out 输出流
* @param flag 是否添加默认打印样式 true 添加 false 不添加大批量导出去除样式可以节省更多的资源
*/
public static ExcelWriter buildExcelWriter(OutputStream out, Boolean flag) {
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(out).excelType(ExcelTypeEnum.XLSX);
if (flag) {
excelWriterBuilder.registerWriteHandler(getDefaultHorizontalCellStyleStrategy());
}
return excelWriterBuilder.build();
}
/**
* @description [默认构建带样式]
*/
public static ExcelWriter buildExcelWriter(OutputStream out) {
return buildExcelWriter(out, true);
}
/**
* @description [单纯写入适用于手动分页]
*/
public static <T> void writeOnly(ExcelWriter excelWriter, List<T> data, Class clazz, Integer sheetNo, String sheetName) {
long exportStartTime = System.currentTimeMillis();
log.info("报表" + sheetNo + "写入Size: " + data.size() + "条。");
ExcelWriterSheetBuilder excelWriterSheetBuilder;
WriteSheet writeSheet;
excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
excelWriterSheetBuilder.sheetNo(sheetNo);
excelWriterSheetBuilder.sheetName(sheetName);
writeSheet = excelWriterSheetBuilder.build();
writeSheet.setClazz(clazz);
excelWriter.write(data, writeSheet);
log.info("报表" + sheetNo + "写入耗时: " + (System.currentTimeMillis() - exportStartTime) + "ms");
}
/**
* @description [导出]
*/
public static void finishWriter(OutputStream out, ExcelWriter excelWriter) throws IOException {
out.flush();
excelWriter.finish();
out.close();
System.out.println("报表导出结束时间:" + new Date());
}
}

View File

@ -0,0 +1,65 @@
package org.leocoder.heritage.easyexcel.bigdata;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @description [List集合分割工具类]
* @author Leocoder
*/
public class SplitList {
/**
* @param list 待切割集合
* @param len 集合按照多大size来切割
*/
public static <T> List<List<T>> splitList(List<T> list, int len) {
if (list == null || list.size() == 0 || len < 1) {
return null;
}
List<List<T>> result = new ArrayList<List<T>>();
int size = list.size();
int count = (size + len - 1) / len;
for (int i = 0; i < count; i++) {
List<T> subList = list.subList(i * len, ((i + 1) * len > size ? size : len * (i + 1)));
result.add(subList);
}
return result;
}
/**
* @param source 源集合
* @param n 分成n个集合
* @param <T> 集合类型
* @description [集合平均分组]
*/
public static <T> List<List<T>> groupList(List<T> source, int n) {
if (source == null || source.size() == 0 || n < 1) {
return null;
}
if (source.size() < n) {
return Arrays.asList(source);
}
List<List<T>> result = new ArrayList<List<T>>();
int number = source.size() / n;
int remaider = source.size() % n;
// 偏移量每有一个余数分配就要往右偏移一位
int offset = 0;
for (int i = 0; i < n; i++) {
List<T> list1 = null;
if (remaider > 0) {
list1 = source.subList(i * number + offset, (i + 1) * number + offset + 1);
remaider--;
offset++;
} else {
list1 = source.subList(i * number + offset, (i + 1) * number + offset);
}
result.add(list1);
}
return result;
}
}

View File

@ -0,0 +1,265 @@
package org.leocoder.heritage.easyexcel.column;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.*;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Leocoder
* @description [ColumExcelUtil-自定义表头单/多sheet导出]
*/
public class ColumnExcelUtil {
/**
* 头部样式
*/
private static WriteCellStyle getHeadStyle() {
// 头的策略
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// 背景颜色
headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
headWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 字体
WriteFont headWriteFont = new WriteFont();
// 设置字体名字
headWriteFont.setFontName("微软雅黑");
// 设置字体大小
headWriteFont.setFontHeightInPoints((short) 10);
// 字体加粗
headWriteFont.setBold(false);
// 在样式用应用设置的字体
headWriteCellStyle.setWriteFont(headWriteFont);
// 边框样式
setBorderStyle(headWriteCellStyle);
// 设置自动换行
headWriteCellStyle.setWrapped(true);
// 设置水平对齐的样式为居中对齐
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 设置垂直对齐的样式为居中对齐
headWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置文本收缩至合适
// headWriteCellStyle.setShrinkToFit(true);
return headWriteCellStyle;
}
/**
* 内容样式
*/
private static WriteCellStyle getContentStyle() {
// 内容的策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 背景白色
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
contentWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 设置字体
WriteFont contentWriteFont = new WriteFont();
// 设置字体大小
contentWriteFont.setFontHeightInPoints((short) 10);
// 设置字体名字
contentWriteFont.setFontName("宋体");
// 在样式用应用设置的字体
contentWriteCellStyle.setWriteFont(contentWriteFont);
// 设置样式
setBorderStyle(contentWriteCellStyle);
// 水平居中
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
// 垂直居中
contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置自动换行
contentWriteCellStyle.setWrapped(true);
// 设置单元格格式是文本格式方式长数字文本科学计数法
// contentWriteCellStyle.setDataFormatData();
// 设置文本收缩至合适
// contentWriteCellStyle.setShrinkToFit(true);
return contentWriteCellStyle;
}
/**
* 边框样式
*/
private static void setBorderStyle(WriteCellStyle cellStyle) {
// 设置底边框
cellStyle.setBorderBottom(BorderStyle.THIN);
// 设置底边框颜色
cellStyle.setBottomBorderColor(IndexedColors.BLACK1.getIndex());
// 设置左边框
cellStyle.setBorderLeft(BorderStyle.THIN);
// 设置左边框颜色
cellStyle.setLeftBorderColor(IndexedColors.BLACK1.getIndex());
// 设置右边框
cellStyle.setBorderRight(BorderStyle.THIN);
// 设置右边框颜色
cellStyle.setRightBorderColor(IndexedColors.BLACK1.getIndex());
// 设置顶边框
cellStyle.setBorderTop(BorderStyle.THIN);
// 设置顶边框颜色
cellStyle.setTopBorderColor(IndexedColors.BLACK1.getIndex());
}
/**
* @param response 返回
* @param fileName 文件名
* @param sheetNames sheet集合
* @param headerList 表头集合
* @param dataList 数据集合
*/
public static void excelNoModelSheetExport(HttpServletResponse response, String fileName, List<String> sheetNames,
List<List<List<String>>> headerList, List<List<List<Object>>> dataList) {
ServletOutputStream out = null;
try {
// 清除缓存确保每次导出都是从零开始
CustomColumnWidthHandler.clearCache();
out = getOut(response, fileName);
sheetNames = sheetNames.stream().distinct().collect(Collectors.toList());
int num = sheetNames.size();
// 设置基础样式
HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(getHeadStyle(), getContentStyle());
// 创建ExcelWriter对象
ExcelWriter excelWriter = EasyExcel.write(out).build();
for (int i = 0; i < num; i++) {
ExcelWriterSheetBuilder sheetBuilder = EasyExcel.writerSheet(i, sheetNames.get(i)).head(headerList.get(i))
// 注册自定义列宽处理器
.registerWriteHandler(new CustomColumnWidthHandler())
// 注册样式策略
.registerWriteHandler(horizontalCellStyleStrategy);
// 写入数据
WriteSheet writeSheet = sheetBuilder.build();
if (CollectionUtils.isEmpty(dataList)) {
excelWriter.write(new ArrayList<>(), writeSheet);
} else {
excelWriter.write(dataList.get(i), writeSheet);
}
}
// 完成写入并关闭ExcelWriter
excelWriter.finish();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (Objects.nonNull(out)) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* @param response 返回
* @param fileName 文件名
* @param sheetNames sheet集合
* @param headerList 表头集合
* @param dataList 数据集合
* @param writeHandler 自定义样式
*/
public static void excelNoModelSheetExport(HttpServletResponse response, String fileName, List<String> sheetNames,
List<List<List<String>>> headerList, List<List<List<Object>>> dataList, WriteHandler writeHandler) {
ServletOutputStream out = null;
try {
// 清除缓存确保每次导出都是从零开始
CustomColumnWidthHandler.clearCache();
out = getOut(response, fileName);
sheetNames = sheetNames.stream().distinct().collect(Collectors.toList());
int num = sheetNames.size();
// 设置基础样式
HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(getHeadStyle(), getContentStyle());
ExcelWriter excelWriter = EasyExcel.write(out).build();
for (int i = 0; i < num; i++) {
ExcelWriterSheetBuilder sheetBuilder = EasyExcel.writerSheet(i, sheetNames.get(i)).head(headerList.get(i))
// 注册自定义列宽处理器
.registerWriteHandler(new CustomColumnWidthHandler())
// 注册样式策略
.registerWriteHandler(horizontalCellStyleStrategy);
if (Objects.nonNull(writeHandler)) {
// 自定义样式设置
sheetBuilder.registerWriteHandler(writeHandler);
}
// 写入数据
WriteSheet writeSheet = sheetBuilder.build();
if (CollectionUtils.isEmpty(dataList)) {
excelWriter.write(new ArrayList<>(), writeSheet);
} else {
// 完成写入并关闭ExcelWriter
excelWriter.write(dataList.get(i), writeSheet);
}
}
excelWriter.finish();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (Objects.nonNull(out)) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private static ServletOutputStream getOut(HttpServletResponse response, String fileName) throws Exception {
fileName = fileName + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ExcelTypeEnum.XLSX.getValue();
setAttachmentResponseHeader(response, fileName);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.MULTIPART_FORM_DATA_VALUE);
response.setContentType("application/vnd.ms-excel;charset=utf-8");
return response.getOutputStream();
}
/**
* 下载文件名重新编码
*
* @param response 响应对象
* @param realFileName 真实文件名
*/
public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) {
String percentEncodedFileName = percentEncode(realFileName);
String contentDispositionValue = "attachment; filename=" + percentEncodedFileName + ";filename*=utf-8''" + percentEncodedFileName;
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setHeader("Content-Disposition", contentDispositionValue);
}
/**
* 百分号编码工具方法
*
* @param s 需要百分号编码的字符串
* @return 百分号编码后的字符串
*/
public static String percentEncode(String s) {
String encode = URLEncoder.encode(s, StandardCharsets.UTF_8);
return encode.replaceAll("\\+", "%20");
}
}

View File

@ -0,0 +1,91 @@
package org.leocoder.heritage.easyexcel.column;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.springframework.util.CollectionUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [CustomHandler - 自定义列宽]
*/
public class CustomColumnWidthHandler extends AbstractColumnWidthStyleStrategy {
/**
* 最大列宽
*/
private static final int MAX_COLUMN_WIDTH = 255;
private static final Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>(8);
public CustomColumnWidthHandler() {
}
/**
* 清除缓存
*/
public static void clearCache() {
CACHE.clear();
}
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (isHead) {
// 如果不是最后一个表头则不改变列宽
List<String> headNameList = head.getHeadNameList();
if (!CollectionUtils.isEmpty(headNameList)) {
int size = headNameList.size();
if (!cell.getStringCellValue().equals(headNameList.get(size - 1))) {
return;
}
}
}
if (needSetWidth) {
Map<Integer, Integer> maxColumnWidthMap = CACHE.computeIfAbsent(writeSheetHolder.getSheetNo(), k -> new HashMap<>(16));
Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
if (columnWidth >= 0) {
if (columnWidth > MAX_COLUMN_WIDTH) {
columnWidth = MAX_COLUMN_WIDTH;
}
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
}
}
}
private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
} else {
WriteCellData<?> cellData = cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
} else {
switch (type) {
case STRING:
return cellData.getStringValue().getBytes().length;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
case DATE:
return cellData.getDateValue().toString().getBytes().length;
default:
return -1;
}
}
}
}
}

View File

@ -0,0 +1,167 @@
package org.leocoder.heritage.easyexcel.column;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.ss.usermodel.*;
import java.util.List;
/**
* @author Leocoder
* @description [CustomStyle-自定义样式]
*/
public class CustomStyle implements CellWriteHandler {
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList,
Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
int rowIndex = cell.getRowIndex();
int columnIndex = cell.getColumnIndex();
String sheetName = cell.getSheet().getSheetName();
if (rowIndex == 1 && columnIndex == 0) {
// 设置表头统计的样式由于单元格是合并的只设置第一列就行了
setTotalStyle(writeSheetHolder, cellDataList, cell);
return;
}
if (!isHead) {
// 内容样式处理
Workbook workbook = cell.getSheet().getWorkbook();
if (sheetName.equals("手机")) {
// 判断是否为手机sheet
if (columnIndex == 3) {
// 判断价格
// 注意这里的 cell.get**Value 有多个方法一定要准确否则会报错报错后不会再进入这个拦截器直接导出了
// 如果无法准确判断应该用哪个 getValue可以 debug 测试
double value = cell.getNumericCellValue();
if (value > 5000.00) {
// 字体样式改为红色
CellStyle cellStyle = getContentCellStyle(workbook, IndexedColors.WHITE.getIndex(), IndexedColors.RED.getIndex());
//设置当前单元格样式
cell.setCellStyle(cellStyle);
// 这里要把 WriteCellData的样式清空
// 不然后面还有一个拦截器 FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到cell里面去 会导致自己设置的不一样
cellDataList.get(0).setWriteCellStyle(null);
}
} else if (columnIndex == 4) {
// 判断CPU
String value = cell.getStringCellValue();
if (value.contains("骁龙8 Gen3")) {
// 背景改为黄色
CellStyle cellStyle = getContentCellStyle(workbook, IndexedColors.YELLOW.getIndex(), IndexedColors.BLACK.getIndex());
cell.setCellStyle(cellStyle);
cellDataList.get(0).setWriteCellStyle(null);
}
}
} else {
// 电脑sheet
if (columnIndex == 2) {
// 判断价格
double value = cell.getNumericCellValue();
if (value > 10000.00) {
// 字体样式改为红色
CellStyle cellStyle = getContentCellStyle(workbook, IndexedColors.WHITE.getIndex(), IndexedColors.RED.getIndex());
cell.setCellStyle(cellStyle);
cellDataList.get(0).setWriteCellStyle(null);
}
} else if (columnIndex == 4) {
String value = cell.getStringCellValue();
if (value.contains("RTX 4090")) {
// 背景改为蓝色
CellStyle cellStyle = getContentCellStyle(workbook, IndexedColors.LIGHT_BLUE.getIndex(), IndexedColors.BLACK.getIndex());
cell.setCellStyle(cellStyle);
cellDataList.get(0).setWriteCellStyle(null);
}
}
}
}
}
private CellStyle getContentCellStyle(Workbook workbook, short ffColorIndex, short fontColorIndex) {
// 单元格策略
CellStyle cellStyle = workbook.createCellStyle();
// 设置背景颜色
cellStyle.setFillForegroundColor(ffColorIndex);
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 设置垂直居中为居中对齐
cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置左右对齐为居中对齐
cellStyle.setAlignment(HorizontalAlignment.CENTER);
// 自动换行
cellStyle.setWrapText(true);
// 设置边框
setBorderStyle(cellStyle);
// 字体
Font font = workbook.createFont();
//设置字体名字
font.setFontName("宋体");
//设置字体大小
font.setFontHeightInPoints((short) 10);
// 设置字体颜色
font.setColor(fontColorIndex);
//字体加粗
font.setBold(false);
//在样式用应用设置的字体
cellStyle.setFont(font);
return cellStyle;
}
private void setTotalStyle(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell) {
Workbook workbook = cell.getSheet().getWorkbook();
//设置行高
writeSheetHolder.getSheet().getRow(cell.getRowIndex()).setHeight((short) (1.4 * 256 * 2));
// 单元格策略
CellStyle cellStyle = workbook.createCellStyle();
// 设置背景颜色灰色
cellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 设置垂直居中为居中对齐
cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置左右对齐为靠右对齐
cellStyle.setAlignment(HorizontalAlignment.RIGHT);
// 自动换行
cellStyle.setWrapText(true);
// 设置边框
setBorderStyle(cellStyle);
// 字体
Font font = workbook.createFont();
//设置字体名字
font.setFontName("微软雅黑");
//设置字体大小
font.setFontHeightInPoints((short) 10);
//字体加粗
font.setBold(false);
//在样式用应用设置的字体
cellStyle.setFont(font);
//设置当前单元格样式
cell.setCellStyle(cellStyle);
// 这里要把 WriteCellData的样式清空
// 不然后面还有一个拦截器 FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到cell里面去 会导致自己设置的不一样
cellDataList.get(0).setWriteCellStyle(null);
}
/**
* 边框样式
*/
private static void setBorderStyle(CellStyle cellStyle) {
//设置底边框
cellStyle.setBorderBottom(BorderStyle.THIN);
//设置底边框颜色
cellStyle.setBottomBorderColor(IndexedColors.BLACK1.getIndex());
//设置左边框
cellStyle.setBorderLeft(BorderStyle.THIN);
//设置左边框颜色
cellStyle.setLeftBorderColor(IndexedColors.BLACK1.getIndex());
//设置右边框
cellStyle.setBorderRight(BorderStyle.THIN);
//设置右边框颜色
cellStyle.setRightBorderColor(IndexedColors.BLACK1.getIndex());
//设置顶边框
cellStyle.setBorderTop(BorderStyle.THIN);
//设置顶边框颜色
cellStyle.setTopBorderColor(IndexedColors.BLACK1.getIndex());
}
}

View File

@ -0,0 +1,71 @@
package org.leocoder.heritage.easyexcel.core.enums;
import lombok.Getter;
/**
* @author Leocoder
* @description [ExcelTemplateEnum导入模板监听器]
*/
@Getter
public enum ExcelTemplateEnum {
/**
* 单sheet导出
*/
TEMPLATE_1("1", "下载模版"),
/**
* 模板格式
*/
TEMPLATE_SUFFIX("xlsx", ".xlsx"),
TEMPLATE_SUFFIX_XLS("xls", ".xls"),
TEMPLATE_SUFFIX_DOCX("docx", ".docx"),
/**
* 模板路径
*/
TEMPLATE_PATH("path", "excel"),
;
private final String code;
private final String desc;
ExcelTemplateEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
/**
* 通过code获取msg
*
* @param code 枚举值
* @return
*/
public static String getMsgByCode(String code) {
if (code == null) {
return null;
}
ExcelTemplateEnum enumList = getByCode(code);
if (enumList == null) {
return null;
}
return enumList.getDesc();
}
public static String getCode(ExcelTemplateEnum enumList) {
if (enumList == null) {
return null;
}
return enumList.getCode();
}
public static ExcelTemplateEnum getByCode(String code) {
for (ExcelTemplateEnum enumList : values()) {
if (enumList.getCode().equals(code)) {
return enumList;
}
}
return null;
}
}

View File

@ -0,0 +1,52 @@
package org.leocoder.heritage.easyexcel.core.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import java.util.ArrayList;
import java.util.List;
/**
* @author Leocoder
* @description [UploadDataListener]
*/
public class UploadDataListener<T> extends AnalysisEventListener<T> {
// 数据集
private final List<T> list = new ArrayList<>();
public List<T> getList() {
return this.list;
}
/**
* @description [每条数据都会进入]
* @author Leocoder
*/
@Override
public void invoke(T object, AnalysisContext analysisContext) {
this.list.add(object);
}
/**
* @description [数据解析完调用]
* @author Leocoder
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
/**
* @description [异常时调用]
* @author Leocoder
*/
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
// 数据解析异常
if (exception instanceof ExcelDataConvertException excelDataConvertException) {
throw new RuntimeException("" + excelDataConvertException.getRowIndex() + "" + excelDataConvertException.getColumnIndex() + "" + "数据解析异常");
}
}
}

View File

@ -0,0 +1,303 @@
package org.leocoder.heritage.easyexcel.core.utils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.easyexcel.core.enums.ExcelTemplateEnum;
import org.leocoder.heritage.easyexcel.core.listener.UploadDataListener;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author Leocoder
* @description [EasyExcelUtil导出工具类]
*/
@Slf4j
@Component
public class EasyExcelUtil {
/**
* @param file文件流
* @param clazz数据对象
* @param sheetName要读取的sheet [不传默认读取第一个sheet]
* @description [导入简单excel数据]
*/
public <T> List<T> importExcel(MultipartFile file, Class<T> clazz, String sheetName) {
try {
this.checkFile(file);
UploadDataListener<T> uploadDataListener = new UploadDataListener<>();
ExcelReaderBuilder builder = EasyExcelFactory.read(file.getInputStream(), clazz, uploadDataListener);
if (StringUtils.isEmpty(sheetName)) {
builder.sheet().doRead();
} else {
builder.sheet(sheetName).doRead();
}
return uploadDataListener.getList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @param multipartFile 传入文件
* @param objList 需要导入的sheet页实体类型集合
* @param index sheet页个数
* @param indexList 需要导入sheet页下标集合
* @description [指定sheet页导入通用方法]
*/
public <T> List<List<T>> importExcelsByIndex(MultipartFile multipartFile, List<T> objList, int index, List<Integer> indexList) {
try {
if (multipartFile == null) {
throw new RuntimeException("文件为空");
}
List<List<T>> resultList = new LinkedList<>();
//初始化导入sheet页实体类型下标
int objListClass = 0;
for (int i = 0; i < index; i++) {
if (indexList.contains(i)) {
UploadDataListener<T> uploadDataListener = new UploadDataListener<>();
List<T> excels;
EasyExcelFactory.read(multipartFile.getInputStream(), objList.get(objListClass).getClass(), uploadDataListener).sheet(i).doRead();
excels = uploadDataListener.getList();
resultList.add(excels);
objListClass++;
}
}
return resultList;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @param file 文件流
* @param index 需要读取的sheet个数 [默认0开始如果传入3则读取0 1 2]
* @param params 每个sheet里面需要封装的对象[如果index为3则需要传入对应的3个对象]
* @description [读取多个sheet]
*/
public <T> List<List<T>> importExcels(MultipartFile file, int index, List<Object> params) {
try {
this.checkFile(file);
List<List<T>> resultList = new LinkedList<>();
for (int i = 0; i < index; i++) {
UploadDataListener<T> uploadDataListener = new UploadDataListener<>();
ExcelReaderBuilder builder = EasyExcelFactory.read(file.getInputStream(), params.get(i).getClass(), uploadDataListener);
builder.sheet(i).doRead();
List<T> list = uploadDataListener.getList();
resultList.add(list);
}
return resultList;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @param response
* @param dataList 数据列表
* @param clazz 数据对象
* @param fileName 文件名称
* @param sheetName sheet名称
* @description [导出excel表格]
*/
public <T> void exportExcel(HttpServletResponse response, List<T> dataList, Class<T> clazz, String fileName, String sheetName) {
try {
response.setContentType("application/vnd.ms-excel; charset=utf-8");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ExcelTemplateEnum.TEMPLATE_SUFFIX.getDesc());
EasyExcelFactory.write(response.getOutputStream(), clazz).sheet(sheetName).doWrite(dataList);
} catch (IllegalStateException | IOException ex) {
log.error("导出Excel时发生异常: " + ex.getMessage(), ex);
}
}
/**
* @param dataList 多个数据列表
* @param clazzMap 对应每个列表里面的数据对应的sheet名称
* @param fileName 文件名
* @description [导出多个sheet]
*/
public <T> void exportExcels(HttpServletResponse response, List<List<?>> dataList, Map<Integer, String> clazzMap, String fileName) {
try {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ExcelTemplateEnum.TEMPLATE_SUFFIX.getDesc());
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
int len = dataList.get(0).size();
for (int i = 0; i < len; i++) {
List<?> objects = (List<?>) dataList.get(0).get(i);
Class<?> aClass = objects.get(0).getClass();
WriteSheet writeSheet0 = EasyExcel.writerSheet(i, clazzMap.get(i)).head(aClass).build();
excelWriter.write(objects, writeSheet0);
}
excelWriter.finish();
} catch (IOException ex) {
log.error("导出Excel时发生异常: " + ex.getMessage(), ex);
}
}
/**
* @param list 填充对象集合
* @param object 填充对象
* @param fileName 文件名称
* @param templateName 模板名称
* @description [根据模板将集合对象填充表格-单个sheet]
*/
public <T> void exportTemplateExcels(HttpServletResponse response, List<T> list, Object object, String fileName, String templateName) {
try {
String template = ExcelTemplateEnum.TEMPLATE_PATH.getDesc() + File.separator + templateName + ExcelTemplateEnum.TEMPLATE_SUFFIX.getDesc();
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(template);
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
ExcelWriter excelWriter = EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).build();
WriteSheet writeSheet0 = EasyExcelFactory.writerSheet(0).build();
excelWriter.fill(object, fillConfig, writeSheet0);
excelWriter.fill(list, fillConfig, writeSheet0);
excelWriter.finish();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param list1 填充对象集合
* @param list2 填充对象集合
* @param object1 填充对象
* @param object2 填充对象
* @param fileName 文件名称
* @param templateName 模板名称
* @description [根据模板将集合对象填充表格-多个sheet]
*/
public <T> void exportSheetTemplateExcels(HttpServletResponse response, List<T> list1, List<T> list2, Object object1, Object object2, String fileName, String templateName) {
try {
String template = ExcelTemplateEnum.TEMPLATE_PATH.getDesc() + File.separator + templateName + ExcelTemplateEnum.TEMPLATE_SUFFIX.getDesc();
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(template);
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
ExcelWriter excelWriter = EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).build();
WriteSheet writeSheet0 = EasyExcelFactory.writerSheet(0).build();
WriteSheet writeSheet1 = EasyExcelFactory.writerSheet(1).build();
excelWriter.fill(object1, fillConfig, writeSheet0);
excelWriter.fill(list1, fillConfig, writeSheet0);
excelWriter.fill(object2, fillConfig, writeSheet1);
excelWriter.fill(list2, fillConfig, writeSheet1);
excelWriter.finish();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param object 填充对象
* @param templateName 模板名称
* @param fileName 文件名称
* @param sheetName 需要写入的sheet名称 [不传填充到第一个sheet]
* @description [根据模板将单个对象填充表格]
*/
public void exportTemplateExcel(HttpServletResponse response, Object object, String templateName, String fileName, String sheetName) {
try {
String template = ExcelTemplateEnum.TEMPLATE_PATH.getDesc() + File.separator + templateName + ExcelTemplateEnum.TEMPLATE_SUFFIX.getDesc();
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(template);
if (StringUtils.isEmpty(sheetName)) {
EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).sheet().doFill(object);
} else {
EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).sheet(sheetName).doFill(object);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param list 填充对象集合
* @param fileName 文件名称
* @param templateName 模板名称
* @param sheetName 需要写入的sheet [不传填充到第一个sheet]
* @description [根据模板将集合对象填充表格]
*/
public <T> void exportTemplateExcelList(HttpServletResponse response, List<T> list, String fileName, String templateName, String sheetName) {
try {
log.info("模板名称:{}", templateName);
String template = ExcelTemplateEnum.TEMPLATE_PATH.getDesc() + File.separator + templateName + ExcelTemplateEnum.TEMPLATE_SUFFIX.getDesc();
log.info("模板路径:{}", template);
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(template);
// 全部填充全部加载到内存中一次填充
if (StringUtils.isEmpty(sheetName)) {
EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).sheet().doFill(list);
} else {
EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).sheet(sheetName).doFill(list);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param list 填充对象集合
* @param fileName 文件名称
* @param templateName 模板名称
* @description [根据模板将集合对象填充表格\
*/
public <T> void exportTemplateExcel2(HttpServletResponse response, List<T> list, String fileName, String templateName) {
try {
String template = ExcelTemplateEnum.TEMPLATE_PATH.getDesc() + File.separator + templateName + ExcelTemplateEnum.TEMPLATE_SUFFIX.getDesc();
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(template);
ExcelWriter excelWriter = EasyExcelFactory.write(getOutputStream(fileName, response)).withTemplate(inputStream).build();
WriteSheet writeSheet = EasyExcelFactory.writerSheet().build();
excelWriter.fill(list, writeSheet);
excelWriter.finish();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param fileName 文件名称
* @description [构建输出流]
*/
private OutputStream getOutputStream(String fileName, HttpServletResponse response) {
try {
fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ExcelTemplateEnum.TEMPLATE_SUFFIX.getDesc());
return response.getOutputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @description [文件格式校验]
*/
private void checkFile(MultipartFile file) {
if (file == null) {
throw new RuntimeException("文件不能为空");
}
String fileName = file.getOriginalFilename();
if (StringUtils.isEmpty(fileName)) {
throw new RuntimeException("文件不能为空");
}
if (!fileName.endsWith(ExcelTemplateEnum.TEMPLATE_SUFFIX.getDesc())
&& !fileName.endsWith(ExcelTemplateEnum.TEMPLATE_SUFFIX_XLS.getDesc())) {
throw new RuntimeException("请上传.xlsx或.xls文件");
}
}
}

View File

@ -0,0 +1,85 @@
<?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.heritage</groupId>
<artifactId>heritage-plugins</artifactId>
<version>${revision}</version>
</parent>
<artifactId>heritage-job</artifactId>
<description>定时任务插件模块</description>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
<!-- Hutool 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- Apache Commons Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- SpringDoc OpenAPI 3.0 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 项目内部依赖 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-model</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-mybatisplus</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,19 @@
package org.leocoder.heritage.job.anno;
import org.leocoder.heritage.job.config.JobConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* @author Leocoder
* @description [启用定时任务插件注解]
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({JobConfiguration.class})
public @interface EnableCoderJob {
}

View File

@ -0,0 +1,20 @@
package org.leocoder.heritage.job.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author Leocoder
* @description [定时任务插件配置类]
*/
@Slf4j
@Configuration
@ComponentScan(basePackages = "org.leocoder.heritage.job")
public class JobConfiguration {
public JobConfiguration() {
log.info("定时任务插件已启用");
}
}

View File

@ -0,0 +1,201 @@
package org.leocoder.heritage.job.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.cron.CronUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.domain.model.vo.system.SysJobVo;
import org.leocoder.heritage.domain.pojo.system.SysJob;
import org.leocoder.heritage.job.service.SysJobService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author Leocoder
* @description [定时任务管理控制器]
*/
@Tag(name = "定时任务管理", description = "系统定时任务的增删改查和执行控制")
@Slf4j
@RequestMapping("/coder")
@RequiredArgsConstructor
@RestController
public class SysJobController {
private final SysJobService sysJobService;
/**
* @description [多条件分页查询]
* @author Leocoder
*/
@Operation(summary = "分页查询定时任务", description = "根据查询条件分页获取系统定时任务列表")
@SaCheckPermission("monitor:job:list")
@GetMapping("/sysJob/listPage")
public IPage<SysJob> listPage(SysJobVo vo) {
// 分页构造器
Page<SysJob> page = new Page<>(vo.getPageNo(), vo.getPageSize());
// 条件构造器
LambdaQueryWrapper<SysJob> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(vo.getJobName()), SysJob::getJobName, vo.getJobName());
wrapper.eq(StringUtils.isNotBlank(vo.getJobType()), SysJob::getJobType, vo.getJobType());
wrapper.eq(StringUtils.isNotBlank(vo.getJobStatus()), SysJob::getJobStatus, vo.getJobStatus());
wrapper.orderByDesc(SysJob::getCreateTime);
// 进行分页查询
page = sysJobService.page(page, wrapper);
return page;
}
/**
* @description [查询所有]
* @author Leocoder
*/
@Operation(summary = "查询所有定时任务", description = "获取系统中所有定时任务信息")
@SaCheckPermission("monitor:job:list")
@GetMapping("/sysJob/list")
public List<SysJob> list(SysJobVo vo) {
LambdaQueryWrapper<SysJob> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(vo.getJobName()), SysJob::getJobName, vo.getJobName());
wrapper.eq(StringUtils.isNotBlank(vo.getJobType()), SysJob::getJobType, vo.getJobType());
wrapper.eq(StringUtils.isNotBlank(vo.getJobStatus()), SysJob::getJobStatus, vo.getJobStatus());
wrapper.orderByDesc(SysJob::getCreateTime);
return sysJobService.list(wrapper);
}
/**
* @description [根据ID查询数据]
* @author Leocoder
*/
@Operation(summary = "根据ID查询任务", description = "根据任务ID获取定时任务详细信息")
@SaCheckPermission("monitor:job:list")
@GetMapping("/sysJob/getById/{id}")
public SysJob getById(@PathVariable("id") Long id) {
return sysJobService.getById(id);
}
/**
* @description [删除任务]
* @author Leocoder
*/
@Operation(summary = "删除定时任务", description = "根据任务ID删除指定的定时任务")
@SaCheckPermission("monitor:job:delete")
@PostMapping("/sysJob/deleteById/{id}")
public String deleteById(@PathVariable("id") Long id) {
try {
// 1停止定时任务
CronUtil.remove(id + "");
// 2进行删除
boolean remove = sysJobService.removeById(id);
if (!remove) {
return "删除任务失败,请重试";
}
return "删除成功";
} catch (Exception e) {
log.error("删除定时任务失败", e);
return "删除任务失败:" + e.getMessage();
}
}
/**
* @description [批量删除]
* @author Leocoder
*/
@Operation(summary = "批量删除定时任务", description = "根据任务ID列表批量删除定时任务")
@SaCheckPermission("monitor:job:delete")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/sysJob/batchDelete")
public String batchDelete(@RequestBody List<Long> jobIds) {
try {
// 1停止定时任务
for (Long jobId : jobIds) {
CronUtil.remove(jobId + "");
}
// 2批量删除
boolean batch = sysJobService.removeBatchByIds(jobIds);
if (!batch) {
return "删除任务失败,请重试";
}
return "批量删除成功";
} catch (Exception e) {
log.error("批量删除定时任务失败", e);
return "删除任务失败:" + e.getMessage();
}
}
/**
* @description [任务调度状态修改]
* @author Leocoder
*/
@Operation(summary = "修改任务状态", description = "修改定时任务的运行状态和执行策略")
@SaCheckPermission("monitor:job:update")
@PostMapping("/sysJob/updateStatus/{id}/{jobStatus}/{policyStatus}")
public String updateStatus(@PathVariable("id") Long id,
@PathVariable("jobStatus") String jobStatus,
@PathVariable("policyStatus") String policyStatus) {
try {
sysJobService.updateStatus(id, jobStatus, policyStatus);
return "操作成功";
} catch (Exception e) {
log.error("修改任务状态失败", e);
return "操作失败:" + e.getMessage();
}
}
/**
* @description [立即运行任务-执行一次]
* @author Leocoder
*/
@Operation(summary = "立即执行任务", description = "手动立即执行指定的定时任务")
@SaCheckPermission("monitor:job:run")
@GetMapping("/sysJob/runNow/{id}")
public String runNow(@PathVariable Long id) {
try {
sysJobService.runNow(id);
return "任务执行成功";
} catch (Exception e) {
log.error("立即执行任务失败", e);
return "任务执行失败:" + e.getMessage();
}
}
/**
* @description [添加定时任务]
* @author Leocoder
*/
@Operation(summary = "新增定时任务", description = "创建新的定时任务配置")
@SaCheckPermission("monitor:job:add")
@PostMapping("/sysJob/add")
public String addJob(@RequestBody SysJob job) {
try {
sysJobService.addJob(job);
return "添加成功";
} catch (Exception e) {
log.error("添加定时任务失败", e);
return "添加失败:" + e.getMessage();
}
}
/**
* @description [修改定时任务]
* @author Leocoder
*/
@Operation(summary = "修改定时任务", description = "更新现有定时任务的配置信息")
@SaCheckPermission("monitor:job:update")
@PostMapping("/sysJob/update")
public String updateJob(@RequestBody SysJob job) {
try {
sysJobService.updateJob(job);
return "修改成功";
} catch (Exception e) {
log.error("修改定时任务失败", e);
return "修改失败:" + e.getMessage();
}
}
}

View File

@ -0,0 +1,48 @@
package org.leocoder.heritage.job.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.leocoder.heritage.domain.pojo.system.SysJob;
/**
* @author Leocoder
* @description [定时任务服务接口]
*/
public interface SysJobService extends IService<SysJob> {
/**
* @description [停止任务]
* @author Leocoder
*/
void pauseJob(Long id);
/**
* @description [启动定时任务]
* @author Leocoder
*/
void resumeJob(Long id);
/**
* @description [任务调度状态修改]
* @author Leocoder
*/
void updateStatus(Long id, String jobStatus, String policyStatus);
/**
* @description [立即运行任务-执行一次]
* @author Leocoder
*/
void runNow(Long id);
/**
* @description [添加定时任务]
* @author Leocoder
*/
void addJob(SysJob job);
/**
* @description [修改定时任务]
* @author Leocoder
*/
void updateJob(SysJob job);
}

View File

@ -0,0 +1,392 @@
package org.leocoder.heritage.job.service.impl;
import cn.hutool.cron.CronUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.domain.pojo.system.SysJob;
import org.leocoder.heritage.job.service.SysJobService;
import org.leocoder.heritage.job.task.CommonTimerTaskRunner;
import org.leocoder.heritage.mybatisplus.mapper.system.SysJobMapper;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.Method;
/**
* @author Leocoder
* @description [定时任务服务实现类]
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysJobServiceImpl extends ServiceImpl<SysJobMapper, SysJob> implements SysJobService {
private final SysJobMapper sysJobMapper;
/**
* @description [停止任务]
* @author Leocoder
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void pauseJob(Long id) {
SysJob byId = this.getById(id);
if (byId == null) {
throw new RuntimeException("任务不存在");
}
if ("1".equals(byId.getJobStatus())) {
throw new RuntimeException("该任务已处于停止状态");
}
CronUtil.remove(id + "");
LambdaUpdateWrapper<SysJob> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysJob::getJobStatus, "1");
updateWrapper.eq(SysJob::getJobId, id);
boolean update = this.update(updateWrapper);
if (!update) {
throw new RuntimeException("暂停任务失败,请重试");
}
}
/**
* @description [启动定时任务]
* @author Leocoder
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void resumeJob(Long id) {
if (null == id) {
throw new RuntimeException("该任务未查询到");
}
SysJob job = this.getById(id);
if (job == null) {
throw new RuntimeException("任务不存在");
}
// 先停止定时任务
CronUtil.remove(id + "");
// 再注册定时任务
CronUtil.schedule(job.getJobId() + "", job.getCronExpression(), () -> {
executeMethod(job.getClassPath(), job.getMethodName(), job.getJobParams());
});
// 更新任务状态为正常
LambdaUpdateWrapper<SysJob> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysJob::getJobStatus, "0");
updateWrapper.eq(SysJob::getJobId, id);
this.update(updateWrapper);
}
/**
* @description [执行方法多个参数传递必须使用逗号分割一个一个对应]
* @author Leocoder
*/
private void executeMethod(String classPath, String methodName, String params) {
if (StringUtils.isBlank(classPath)) {
throw new RuntimeException("类绝对路径不能为空");
}
if (StringUtils.isBlank(methodName)) {
throw new RuntimeException("方法名称不能为空");
}
try {
// 使用SpringUtil获取类的实例
CommonTimerTaskRunner runner = (CommonTimerTaskRunner) SpringUtil.getBean(Class.forName(classPath));
Method[] methods = runner.getClass().getMethods();
Method targetMethod = null;
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length > 0 && StringUtils.isBlank(params)) {
throw new IllegalArgumentException("缺少参数");
}
if (parameterTypes.length == 0 && StringUtils.isNotBlank(params)) {
throw new IllegalArgumentException("不应传递参数");
}
if (parameterTypes.length > 0 && StringUtils.isNotBlank(params)) {
String[] paramArray = params.split(",");
if (paramArray.length != parameterTypes.length) {
throw new IllegalArgumentException("参数数量不匹配");
}
for (int i = 0; i < paramArray.length; i++) {
Object convertedValue = convertToType(parameterTypes[i], paramArray[i]);
if (convertedValue == null) {
throw new IllegalArgumentException("参数类型不匹配");
}
}
targetMethod = method;
} else {
targetMethod = method;
}
break;
}
}
if (targetMethod != null) {
// 调用方法
if (StringUtils.isNotBlank(params)) {
targetMethod.invoke(runner, extractTypedParams(targetMethod.getParameterTypes(), params));
} else {
targetMethod.invoke(runner);
}
} else {
throw new IllegalArgumentException("指定方法不存在");
}
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
log.error("执行定时任务出现异常", e);
throw new RuntimeException("执行定时任务出现异常:" + e.getMessage());
}
}
/**
* @description [多参数使用逗号分隔开]
* @author Leocoder
*/
private Object[] extractTypedParams(Class<?>[] parameterTypes, String params) {
String[] paramArray = params.split(",");
Object[] typedParams = new Object[paramArray.length];
for (int i = 0; i < paramArray.length; i++) {
typedParams[i] = convertToType(parameterTypes[i], paramArray[i].trim());
}
return typedParams;
}
/**
* @description [类型转换]
* @author Leocoder
*/
private Object convertToType(Class<?> targetType, String value) {
if (targetType.equals(String.class)) {
return value;
} else if (targetType.equals(Integer.class) || targetType.equals(int.class)) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException ex) {
return null;
}
} else if (targetType.equals(Boolean.class) || targetType.equals(boolean.class)) {
return Boolean.parseBoolean(value);
} else if (targetType.equals(Long.class) || targetType.equals(long.class)) {
try {
return Long.parseLong(value);
} catch (NumberFormatException ex) {
return null;
}
}
// 根据需要添加更多类型转换
return null;
}
/**
* @description [任务调度状态修改]
* @author Leocoder
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void updateStatus(Long id, String jobStatus, String policyStatus) {
if (StringUtils.isBlank(jobStatus) || id == null) {
throw new RuntimeException("请传递相关信息");
}
if ("0".equals(jobStatus)) {
if ("1".equals(policyStatus)) {
resumeJob(id); // 启动定时任务
}
} else if ("1".equals(jobStatus)) {
pauseJob(id); // 停止定时任务
}
LambdaUpdateWrapper<SysJob> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(SysJob::getJobStatus, jobStatus);
wrapper.set(SysJob::getPolicyStatus, policyStatus);
wrapper.eq(SysJob::getJobId, id);
boolean update = this.update(wrapper);
if (!update) {
throw new RuntimeException("操作失败,请重试");
}
}
/**
* @description [立即运行任务-执行一次不影响定时调度]
* @author Leocoder
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void runNow(Long id) {
SysJob job = this.getById(id);
if (ObjectUtils.isEmpty(job) || job.getJobId() == null) {
throw new RuntimeException("未查到当前任务");
}
// 直接执行一次任务不影响现有的定时调度
executeMethod(job.getClassPath(), job.getMethodName(), job.getJobParams());
// 注意这里不移除定时任务让定时调度继续按计划运行
log.info("立即执行任务完成任务ID{},定时调度继续保持运行", id);
}
/**
* @description [添加定时任务]
* @author Leocoder
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void addJob(SysJob job) {
// 1参数校验
checkParams(job);
// 2是否添加重复的定时任务
LambdaQueryWrapper<SysJob> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysJob::getClassPath, job.getClassPath());
wrapper.eq(SysJob::getMethodName, job.getMethodName());
wrapper.eq(SysJob::getCronExpression, job.getCronExpression());
long count = this.count(wrapper);
if (count > 0) {
throw new RuntimeException("存在重复执行的定时任务,名称为:" + job.getJobName());
}
// 3添加定时任务
boolean save = this.save(job);
if (!save) {
throw new RuntimeException("添加失败,请重试");
}
// 4根据任务状态进行执行定时策略
if ("0".equals(job.getJobStatus())) {
if ("1".equals(job.getPolicyStatus())) {
// 开启定时任务
resumeJob(job.getJobId());
} else if ("2".equals(job.getPolicyStatus())) {
// 先停止定时任务
CronUtil.remove(job.getJobId() + "");
// 执行一次
executeMethod(job.getClassPath(), job.getMethodName(), job.getJobParams());
} else {
// 停止任务
CronUtil.remove(job.getJobId() + "");
LambdaUpdateWrapper<SysJob> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysJob::getPolicyStatus, "3");
updateWrapper.eq(SysJob::getJobId, job.getJobId());
boolean update = this.update(updateWrapper);
if (!update) {
throw new RuntimeException("操作失败,请重试");
}
}
} else {
// 停止任务计划策略并改为放弃执行
CronUtil.remove(job.getJobId() + "");
LambdaUpdateWrapper<SysJob> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysJob::getPolicyStatus, "3");
updateWrapper.eq(SysJob::getJobId, job.getJobId());
boolean update = this.update(updateWrapper);
if (!update) {
throw new RuntimeException("操作失败,请重试");
}
}
}
/**
* @description [参数校验]
* @author Leocoder
*/
private void checkParams(SysJob job) {
// 校验表达式
if (!CronExpression.isValidExpression(job.getCronExpression())) {
throw new RuntimeException("cron表达式" + job.getCronExpression() + "格式不正确");
}
// 校验定时任务类
try {
Class<?> actionClass = Class.forName(job.getClassPath());
if (!CommonTimerTaskRunner.class.isAssignableFrom(actionClass)) {
throw new RuntimeException("定时任务对应的类:" + job.getClassPath() + "不符合要求");
}
} catch (ClassNotFoundException e) {
throw new RuntimeException("定时任务找不到对应的类,名称为:" + job.getClassPath());
}
}
/**
* @description [修改定时任务]
* @author Leocoder
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void updateJob(SysJob job) {
// 1参数校验
checkParams(job);
if (job.getJobId() == null) {
throw new RuntimeException("请选择需要修改的任务");
}
// 2是否修改为数据库已经存在的定时任务保持与添加任务相同的检查逻辑
LambdaQueryWrapper<SysJob> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysJob::getClassPath, job.getClassPath());
wrapper.eq(SysJob::getMethodName, job.getMethodName());
wrapper.eq(SysJob::getCronExpression, job.getCronExpression());
wrapper.ne(SysJob::getJobId, job.getJobId()); // 排除当前任务
long count = this.count(wrapper);
if (count > 0) {
throw new RuntimeException("存在重复执行的定时任务,名称为:" + job.getJobName());
}
// 3修改任务
// 这里应该从登录用户上下文获取
job.setUpdateBy(CoderLoginUtil.getLoginName());
boolean update = this.updateById(job);
if (!update) {
throw new RuntimeException("修改失败,请重试");
}
// 4根据任务状态进行执行定时策略
if ("0".equals(job.getJobStatus())) {
if ("1".equals(job.getPolicyStatus())) {
// 先停止定时任务再开启定时任务
resumeJob(job.getJobId());
} else if ("2".equals(job.getPolicyStatus())) {
// 先停止定时任务
CronUtil.remove(job.getJobId() + "");
// 再开始执行一次定时任务
executeMethod(job.getClassPath(), job.getMethodName(), job.getJobParams());
} else {
// 停止任务计划策略并改为放弃执行
CronUtil.remove(job.getJobId() + "");
LambdaUpdateWrapper<SysJob> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysJob::getPolicyStatus, "3");
updateWrapper.eq(SysJob::getJobId, job.getJobId());
boolean updateBoolean = this.update(updateWrapper);
if (!updateBoolean) {
throw new RuntimeException("操作失败,请重试");
}
}
} else {
// 停止任务计划策略并改为放弃执行
CronUtil.remove(job.getJobId() + "");
LambdaUpdateWrapper<SysJob> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysJob::getPolicyStatus, "3");
updateWrapper.eq(SysJob::getJobId, job.getJobId());
boolean updateBoolean = this.update(updateWrapper);
if (!updateBoolean) {
throw new RuntimeException("操作失败,请重试");
}
}
}
}

View File

@ -0,0 +1,67 @@
package org.leocoder.heritage.job.task;
import cn.hutool.cron.CronUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.leocoder.heritage.domain.pojo.system.SysJob;
import org.leocoder.heritage.job.service.SysJobService;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import java.util.List;
/**
* @author Leocoder
* @description [定时任务监听器系统启动时将定时任务启动]
*/
@Slf4j
@Configuration
public class CoderJobListener implements ApplicationListener<ApplicationStartedEvent>, Ordered {
@Resource
private SysJobService sysJobService;
@SuppressWarnings("ALL")
@Override
public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
try {
// 查询状态正常的定时任务 AND 执行策略是立即执行类型的数据筛选后进行开始定时任务
LambdaQueryWrapper<SysJob> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysJob::getJobStatus, "0"); // 正常状态
wrapper.eq(SysJob::getPolicyStatus, "1"); // 立即执行
List<SysJob> jobList = sysJobService.list(wrapper);
if(CollectionUtils.isNotEmpty(jobList)){
for (SysJob sysJob : jobList) {
try {
// 启动定时任务
sysJobService.resumeJob(sysJob.getJobId());
log.info("自动启动定时任务:{},表达式:{}", sysJob.getJobName(), sysJob.getCronExpression());
} catch (Exception e) {
log.error("自动启动定时任务失败:{},错误:{}", sysJob.getJobName(), e.getMessage());
}
}
}
// 设置秒级别的启用
CronUtil.setMatchSecond(true);
log.info("启动定时器执行器,支持秒级精度");
CronUtil.restart();
log.info("定时任务系统初始化完成,共启动{}个任务", jobList != null ? jobList.size() : 0);
} catch (Exception e) {
log.error("定时任务系统初始化失败", e);
}
}
@Override
public int getOrder() {
return LOWEST_PRECEDENCE;
}
}

View File

@ -0,0 +1,55 @@
package org.leocoder.heritage.job.task;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.job.service.SysJobService;
import org.springframework.stereotype.Component;
/**
* @author Leocoder
* @description [定时任务执行器示例实现]
* 注意仅支持字符串数字布尔值进行传递参数
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CoderJobTimerTaskRunner implements CommonTimerTaskRunner {
private final SysJobService sysJobService;
/**
* @description [单个参数param可以传递参数通过数据库的job_params]
* @author Leocoder
*/
@Override
public void paramAction(String param) {
log.info("执行单参数定时任务,参数:{}", param);
// 这里可以执行具体的业务逻辑
// 示例发送邮件清理缓存同步数据等
}
/**
* @description [无参数执行]
* @author Leocoder
*/
@Override
public void noParamAction() {
log.info("执行无参数定时任务");
// 示例查询所有定时任务并输出
sysJobService.list().forEach(job ->
log.info("任务:{},状态:{}", job.getJobName(), job.getJobStatusText())
);
}
/**
* @description [多参数执行]
* @author Leocoder
*/
@Override
public void manyParamsAction(String param1, Integer param2) {
log.info("执行多参数定时任务参数1{}参数2{}", param1, param2);
// 这里可以执行具体的业务逻辑
// 示例根据参数处理不同类型的数据
}
}

View File

@ -0,0 +1,27 @@
package org.leocoder.heritage.job.task;
/**
* @author Leocoder
* @description [定时任务执行器通用接口 - 所有定时任务都实现这个接口方便我们遍历所有可用的定时任务类]
*/
public interface CommonTimerTaskRunner {
/**
* @description [单参数执行]
* @author Leocoder
*/
void paramAction(String param);
/**
* @description [无参数执行]
* @author Leocoder
*/
void noParamAction();
/**
* @description [多参数执行多个参数传递必须使用逗号分割一个一个对应]
* @author Leocoder
*/
void manyParamsAction(String param1, Integer param2);
}

View File

@ -0,0 +1,41 @@
<?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.heritage</groupId>
<artifactId>heritage-plugins</artifactId>
<version>${revision}</version>
</parent>
<name>heritage-oper-logs</name>
<artifactId>heritage-oper-logs</artifactId>
<description>异步操作日志功能插件支持AOP切面自动记录操作日志</description>
<dependencies>
<!-- 全局公共模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- 模型模块 -->
<dependency>
<groupId>org.leocoder.heritage</groupId>
<artifactId>heritage-model</artifactId>
<version>${revision}</version>
</dependency>
<!-- SpringBoot Aop 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@ -0,0 +1,581 @@
package org.leocoder.heritage.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.heritage.common.satoken.CoderLoginUtil;
import org.leocoder.heritage.common.utils.date.DateUtil;
import org.leocoder.heritage.common.utils.ip.IpAddressUtil;
import org.leocoder.heritage.common.utils.ip.IpUtil;
import org.leocoder.heritage.common.utils.json.JsonUtil;
import org.leocoder.heritage.common.utils.string.StringUtil;
import org.leocoder.heritage.domain.pojo.system.SysOperLog;
import org.leocoder.heritage.operlog.annotation.OperLog;
import org.leocoder.heritage.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.heritage.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("[");
// 最多显示3个元素
for (int i = 0; i < array.length && i < 3; i++) {
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 Map) {
Map<?, ?> map = (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 LocalDateTime) {
LocalDateTime ldt = (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 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 Map) {
Map<?, ?> map = (Map<?, ?>) value;
valueStr = "[Map, size=" + map.size() + "]";
} else {
valueStr = value.toString();
}
// 限制字段值长度
if (valueStr.length() > 50) {
valueStr = valueStr.substring(0, 47) + "...";
}
sb.append(valueStr);
first = false;
fieldCount++;
// 最多显示8个字段避免过长
if (fieldCount >= 8 || sb.length() > 300) break;
}
} catch (Exception e) {
// 忽略单个字段错误
}
}
sb.append("}");
return sb.toString();
} catch (Exception e) {
String className = obj.getClass().getSimpleName();
return "{" + className + " object}";
}
}
/**
* @description [过滤业务参数排除HttpServletResponse等非业务对象]
* @author Leocoder
*/
private Object[] filterBusinessArgs(Object[] args) {
if (args == null || args.length == 0) {
return new Object[0];
}
java.util.List<Object> businessArgs = new java.util.ArrayList<>();
for (Object arg : args) {
if (arg == null) {
continue;
}
String className = arg.getClass().getName();
// 排除Spring框架和Servlet相关对象
if (className.startsWith("jakarta.servlet.") ||
className.startsWith("javax.servlet.") ||
className.startsWith("org.springframework.") ||
className.startsWith("org.apache.catalina.")) {
continue;
}
businessArgs.add(arg);
}
return businessArgs.toArray(new Object[0]);
}
}

View File

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

View File

@ -0,0 +1,184 @@
package org.leocoder.heritage.operlog.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.heritage.common.exception.coder.YUtil;
import org.leocoder.heritage.common.utils.cache.RedisUtil;
import org.leocoder.heritage.common.utils.date.DateUtil;
import org.leocoder.heritage.domain.pojo.system.SysOperLog;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
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);
// 25小时过期
redisUtil.setCacheObjectHours(userKey, 1, 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);
}
}
}