feat: 新增heritage-plugins业务功能插件(第3部分)
- 新增heritage-easyexcel:Excel导入导出插件,支持大数据量处理 - 新增heritage-oper-logs:操作日志插件,自动记录用户操作 - 新增heritage-job:定时任务插件,支持动态管理定时任务
This commit is contained in:
parent
2c31bf6d53
commit
db6f4b1922
35
heritage-plugins/heritage-easyexcel/pom.xml
Normal file
35
heritage-plugins/heritage-easyexcel/pom.xml
Normal 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>
|
||||
@ -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 {
|
||||
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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() + "列" + "数据解析异常");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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文件");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
85
heritage-plugins/heritage-job/pom.xml
Normal file
85
heritage-plugins/heritage-job/pom.xml
Normal 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>
|
||||
@ -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 {
|
||||
|
||||
}
|
||||
@ -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("定时任务插件已启用");
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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("操作失败,请重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
// 这里可以执行具体的业务逻辑
|
||||
// 示例:根据参数处理不同类型的数据
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
41
heritage-plugins/heritage-oper-logs/pom.xml
Normal file
41
heritage-plugins/heritage-oper-logs/pom.xml
Normal 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>
|
||||
@ -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 {
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user