fix: 增加导出excel工具类

This commit is contained in:
tangwei 2026-09-09 11:54:33 +08:00
parent a69695bafc
commit 8e5a76e644
5 changed files with 400 additions and 6 deletions

View File

@ -0,0 +1,82 @@
package com.yfd.platform.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 通用 Excel 导出列注解参考若依 RuoYi @Excel 设计
* <p>标注在实体字段上配合 {@code ExcelUtil<T>} 即可实现该字段的导出
* 支持的转换能力字典翻译dictType静态值转换readConverterExp日期格式化</p>
* <pre>
* &#64;Excel(name = "性别", dictType = "sys_user_sex", sort = 3)
* private Integer gender;
*
* &#64;Excel(name = "状态", readConverterExp = "0=停用,1=正常", sort = 5)
* private Integer status;
*
* &#64;Excel(name = "创建时间", dateFormat = "yyyy-MM-dd HH:mm:ss", sort = 6)
* private LocalDateTime createTime;
* </pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel {
/**
* 导出列名Excel 表头
*/
String name();
/**
* 列排序数值越小越靠前未标注 sort 的字段按声明顺序排在其后
*/
int sort() default Integer.MAX_VALUE;
/**
* 字典类型 "sys_user_sex"导出时自动将字典值翻译为字典标签
* 复用项目 DictCache 缓存无需额外查询
*/
String dictType() default "";
/**
* 静态转换表达式 "0=正常,1=停用"逗号分隔等号前为值后为标签
* 优先级高于 {@link #dictType()}
*/
String readConverterExp() default "";
/**
* 日期格式支持 java.util.DateLocalDateLocalDateTime
*/
String dateFormat() default "yyyy-MM-dd HH:mm:ss";
/**
* 列宽字符数0 表示根据表头与内容自动计算
*/
int width() default 0;
/**
* 是否导出该列
*/
boolean isExport() default true;
/**
* 对齐方式
*/
Align align() default Align.AUTO;
/**
* 单元格对齐
*/
enum Align {
/** 自动(文本左对齐、数值右对齐) */
AUTO,
/** 左对齐 */
LEFT,
/** 居中 */
CENTER,
/** 右对齐 */
RIGHT
}
}

View File

@ -7,11 +7,15 @@ import com.yfd.platform.qgc_base.domain.SdWtBH;
import com.yfd.platform.qgc_base.domain.SdEngInfoBHOperateRequest;
import com.yfd.platform.qgc_base.service.ISdWtBHService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yfd.platform.utils.ExcelUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
@RestController
@RequestMapping("/base/wt")
@Tag(name = "水温站管理")
@ -51,4 +55,11 @@ public class SdWtBHController {
boolean result = service.delete(request == null ? null : request.getIds(), request == null ? null : request.getSource());
return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败");
}
@PostMapping("/export")
@Operation(summary = "水温站导出")
public void export(@RequestBody DataSourceRequest request, HttpServletResponse response) throws IOException {
Page<SdWtBH> page = service.queryPageList(request);
new ExcelUtil<>(SdWtBH.class).exportExcel(response, page.getRecords(), "水温站数据", "水温站导出");
}
}

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.yfd.platform.annotation.Excel;
import com.yfd.platform.annotation.FieldChinese;
import lombok.Data;
@ -23,15 +24,18 @@ public class SdWtBH implements Serializable {
/** 水温站站码 */
@TableId(type = IdType.INPUT)
@FieldChinese("水温站站码")
@Excel(name = "水温站站码")
private String stcd;
/** 数据时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@FieldChinese("数据时间")
@Excel(name="数据时间")
private Date tm;
/** 水温站站名 */
@FieldChinese("水温站站名")
@Excel(name="水温站站名")
private String stnm;
/** 站类 */
@ -39,38 +43,47 @@ public class SdWtBH implements Serializable {
private String sttp;
@TableField(exist = false)
@Excel(name="站类")
private String sttpName;
/** 经度 */
@FieldChinese("经度")
@Excel(name="经度")
private BigDecimal lgtd;
/** 纬度 */
@FieldChinese("纬度")
@Excel(name="纬度")
private BigDecimal lttd;
/** 高程 */
@FieldChinese("高程")
@Excel(name = "高程")
private BigDecimal elev;
/** 站址 */
@FieldChinese("站址")
@Excel(name="站址")
private String stlc;
/** 测站来源 */
@FieldChinese("测站来源")
@Excel(name="测站来源")
private String stsr;
/** 水温监测断面设备类型1=浮动式 2=固定式 */
@FieldChinese("水温监测断面设备类型")
@Excel(name="水温监测断面设备类型")
private Integer wtDeviceType;
/** 水温监测断面类型 */
@FieldChinese("水温监测断面类型")
@Excel(name="水温监测断面类型")
private String wtType;
/** 电站水温结构类型 */
@FieldChinese("电站水温结构类型")
@Excel(name="电站水温结构类型")
private String wts;
/** 固定垂向水温类型1=相对位置 2=高程 */

View File

@ -2620,7 +2620,7 @@ public class WarnDataServiceImpl implements WarnDataService {
}
String stcd = ao.getDvcd().trim();
String tm = DateUtil.formatDateTime(ao.getAitime());
String remarkJson = JSONUtil.toJsonStr(ao.getAidata());
// String remarkJson = JSONUtil.toJsonStr(ao.getAidata());
switch (ao.getType()) {
case "0000":
// ai心跳数据
@ -2634,12 +2634,12 @@ public class WarnDataServiceImpl implements WarnDataService {
aicomRows.add(new Object[]{
stcd, tm, "AI_5001",
ao.getAidata().getArea().toPlainString(),
ao.getFiles(), remarkJson
ao.getFiles()
});
break;
case "5011":
// 漂浮物清理
aicomRows.add(new Object[]{stcd, tm, "AI_5011", null, ao.getFiles(), remarkJson});
aicomRows.add(new Object[]{stcd, tm, "AI_5011", null, ao.getFiles()});
break;
case "5021":
// 视频流量
@ -2647,7 +2647,7 @@ public class WarnDataServiceImpl implements WarnDataService {
stcd, tm, "AI_5014",
ao.getAidata() != null && ao.getAidata().getFlow() != null
? ao.getAidata().getFlow().toPlainString() : null,
ao.getFiles(), remarkJson
ao.getFiles()
});
break;
case "AI_RIV":
@ -2656,7 +2656,7 @@ public class WarnDataServiceImpl implements WarnDataService {
aicomRows.add(new Object[]{
stcd, tm, ao.getType(),
ao.getAidata() != null ? ao.getAidata().getValue() : null,
ao.getFiles(), remarkJson
ao.getFiles()
});
break;
default:
@ -2678,7 +2678,7 @@ public class WarnDataServiceImpl implements WarnDataService {
sql.append(" UNION ALL ");
}
sql.append("SELECT ? AS STCD, TO_DATE(?, 'YYYY-MM-DD HH24:MI:SS') AS TM, ")
.append("? AS TYPE, ? AS AI_VAL, ? AS FID, ? AS REMARK FROM DUAL");
.append("? AS TYPE, ? AS AI_VAL, ? AS FID AS REMARK FROM DUAL");
}
sql.append(") source ON (target.STCD = source.STCD AND target.TM = source.TM AND target.TYPE = source.TYPE) ")
.append("WHEN NOT MATCHED THEN INSERT (STCD, TM, TYPE, AI_VAL, FID, REMARK, RECORD_TIME, IS_DELETED) ")

View File

@ -0,0 +1,288 @@
package com.yfd.platform.utils;
import com.yfd.platform.annotation.Excel;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 通用 Excel 导出工具参考若依 RuoYi ExcelUtil 设计
* <p>
* 使用方法
* <ol>
* <li>在实体字段上标注 {@link Excel} 注解声明列名排序字典翻译日期格式等</li>
* <li>Controller 中按需查询数据后调用<pre>
* List&lt;SysUser&gt; list = sysUserService.list(wrapper);
* new ExcelUtil&lt;&gt;(SysUser.class).exportExcel(response, list, "用户数据", "用户数据");
* </pre></li>
* </ol>
* 特性流式写出SXSSF适合大数据量表头冻结与自动筛选列宽自适应
* 字典翻译复用 DictCache静态值转换readConverterExp日期格式化Long 转字符串避免精度丢失
*/
@Slf4j
public class ExcelUtil<T> {
/** 表头背景色(中蓝色 RGB */
private static final byte[] HEADER_BG = {(byte) 0x4A, (byte) 0x86, (byte) 0xC8};
/** 列宽上限(字符数),防止超长内容撑爆表格 */
private static final int MAX_COLUMN_WIDTH = 50;
private final Class<T> clazz;
/** 参与导出的字段列表(已按 @Excel.sort 升序排列,仅保留 isExport=true */
private final List<Field> exportFields;
public ExcelUtil(Class<T> clazz) {
this.clazz = clazz;
this.exportFields = Arrays.stream(clazz.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(Excel.class))
.filter(f -> f.getAnnotation(Excel.class).isExport())
.sorted(Comparator.comparingInt(f -> f.getAnnotation(Excel.class).sort()))
.collect(Collectors.toList());
if (exportFields.isEmpty()) {
log.warn("ExcelUtil: 类 {} 未配置任何 @Excel 导出字段", clazz.getName());
}
}
// ==================== 对外 API ====================
/**
* 导出 Excel HTTP 响应自动设置 Content-Type 与文件名编码
*
* @param response HTTP 响应
* @param list 待导出数据
* @param sheetName 工作表名称
* @param fileName 下载文件名不含扩展名
*/
public void exportExcel(HttpServletResponse response, List<T> list, String sheetName, String fileName) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String encoded = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("\\+", "%20");
response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + encoded + ".xlsx");
exportExcel(response.getOutputStream(), list, sheetName);
}
/**
* 导出 Excel 到输出流xlsx 格式
*
* @param out 输出流
* @param list 待导出数据
* @param sheetName 工作表名称
*/
public void exportExcel(OutputStream out, List<T> list, String sheetName) throws IOException {
// SXSSF 流式写出内存中只保留 100 适合大批量导出
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
writeSheet(workbook, sheetName, list);
workbook.write(out);
workbook.dispose();
}
}
// ==================== 内部实现 ====================
private void writeSheet(SXSSFWorkbook workbook, String sheetName, List<T> list) {
String safeName = sanitizeSheetName(sheetName);
Sheet sheet = workbook.createSheet(safeName);
int columnCount = exportFields.size();
CellStyle headerStyle = buildHeaderStyle(workbook);
CellStyle dataLeft = buildDataStyle(workbook, HorizontalAlignment.LEFT);
CellStyle dataCenter = buildDataStyle(workbook, HorizontalAlignment.CENTER);
CellStyle dataRight = buildDataStyle(workbook, HorizontalAlignment.RIGHT);
int[] colMaxLen = new int[columnCount];
// 1. 表头行
Row headerRow = sheet.createRow(0);
for (int i = 0; i < columnCount; i++) {
Excel excel = exportFields.get(i).getAnnotation(Excel.class);
Cell cell = headerRow.createCell(i);
cell.setCellValue(excel.name());
cell.setCellStyle(headerStyle);
colMaxLen[i] = displayLength(excel.name());
}
// 冻结表头滚动时保持可见
sheet.createFreezePane(0, 1);
// 2. 数据行
int dataRows = list == null ? 0 : list.size();
for (int r = 0; r < dataRows; r++) {
Row row = sheet.createRow(r + 1);
T entity = list.get(r);
for (int i = 0; i < columnCount; i++) {
Field field = exportFields.get(i);
String text = convertToString(field, entity);
Cell cell = row.createCell(i);
cell.setCellValue(text);
cell.setCellStyle(chooseDataStyle(field, dataLeft, dataCenter, dataRight));
int len = displayLength(text);
if (len > colMaxLen[i]) {
colMaxLen[i] = len;
}
}
}
// 3. 列宽注解指定优先否则按内容自适应
for (int i = 0; i < columnCount; i++) {
Excel excel = exportFields.get(i).getAnnotation(Excel.class);
int width = excel.width() > 0 ? excel.width() : Math.min(colMaxLen[i] + 2, MAX_COLUMN_WIDTH);
sheet.setColumnWidth(i, width * 256);
}
// 4. 自动筛选表头 + 数据区
if (columnCount > 0) {
sheet.setAutoFilter(new CellRangeAddress(0, Math.max(dataRows, 0), 0, columnCount - 1));
}
}
/**
* 将实体字段值转换为 Excel 单元格字符串
*/
private String convertToString(Field field, Object entity) {
Excel excel = field.getAnnotation(Excel.class);
Object value = getFieldValue(field, entity);
if (value == null) {
return "";
}
String strValue = String.valueOf(value);
// 1. 静态转换表达式优先 "0=停用,1=正常"
if (!excel.readConverterExp().isEmpty()) {
for (String item : excel.readConverterExp().split(",")) {
String[] kv = item.split("=");
if (kv.length == 2 && kv[0].trim().equals(strValue)) {
return kv[1].trim();
}
}
return strValue;
}
// 2. 字典翻译 dictType="sys_user_sex" 字典标签
// if (!excel.dictType().isEmpty()) {
// try {
// String label = SpringUtils.getBean(DictCache.class).getLabel(excel.dictType(), value);
// if (label != null) {
// return label;
// }
// } catch (Exception e) {
// log.warn("Excel 字典翻译失败: dictType={}, value={}, err={}", excel.dictType(), value, e.getMessage());
// }
// }
// 3. 日期格式化
if (value instanceof LocalDateTime localDateTime) {
return localDateTime.format(DateTimeFormatter.ofPattern(excel.dateFormat()));
}
if (value instanceof LocalDate localDate) {
return localDate.format(DateTimeFormatter.ofPattern(excel.dateFormat()));
}
if (value instanceof Date date) {
return new SimpleDateFormat(excel.dateFormat()).format(date);
}
// 4. 默认Long 直接转字符串避免 Excel 科学计数法与前端精度丢失
return strValue;
}
private Object getFieldValue(Field field, Object entity) {
try {
field.setAccessible(true);
return field.get(entity);
} catch (IllegalAccessException e) {
log.warn("Excel 读取字段值失败: field={}, err={}", field.getName(), e.getMessage());
return null;
}
}
/**
* 根据注解对齐方式选择数据单元格样式
*/
private CellStyle chooseDataStyle(Field field, CellStyle left, CellStyle center, CellStyle right) {
Excel.Align align = field.getAnnotation(Excel.class).align();
return switch (align) {
case CENTER -> center;
case RIGHT -> right;
case AUTO, LEFT -> left;
};
}
private CellStyle buildHeaderStyle(SXSSFWorkbook workbook) {
// SXSSF 底层为 XSSFWorkbook样式均为 XSSFCellStyle可直接设置自定义 RGB 颜色
XSSFCellStyle style = (XSSFCellStyle) workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
setBorders(style);
style.setFillForegroundColor(new XSSFColor(HEADER_BG, null));
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
Font font = workbook.createFont();
font.setBold(true);
font.setColor(IndexedColors.WHITE.getIndex());
font.setFontHeightInPoints((short) 11);
style.setFont(font);
return style;
}
private CellStyle buildDataStyle(SXSSFWorkbook workbook, HorizontalAlignment alignment) {
CellStyle style = workbook.createCellStyle();
style.setAlignment(alignment);
style.setVerticalAlignment(VerticalAlignment.CENTER);
setBorders(style);
return style;
}
private void setBorders(CellStyle style) {
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
}
/**
* 计算字符串显示宽度中文字符按 2 个字符宽度计
*/
private int displayLength(String s) {
if (s == null || s.isEmpty()) {
return 0;
}
int len = 0;
for (int i = 0; i < s.length(); i++) {
len += s.charAt(i) > 0xFF ? 2 : 1;
}
return len;
}
/**
* 过滤 Excel 工作表名非法字符[]:*?/\
*/
private String sanitizeSheetName(String name) {
String safe = name == null || name.isBlank() ? "Sheet1" : name.replaceAll("[\\[\\]:*?/\\\\]", "_");
return safe.length() > 31 ? safe.substring(0, 31) : safe;
}
}