diff --git a/backend/src/main/java/com/yfd/platform/annotation/Excel.java b/backend/src/main/java/com/yfd/platform/annotation/Excel.java
new file mode 100644
index 00000000..f01e0792
--- /dev/null
+++ b/backend/src/main/java/com/yfd/platform/annotation/Excel.java
@@ -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 设计)
+ *
标注在实体字段上,配合 {@code ExcelUtil} 即可实现该字段的导出。
+ * 支持的转换能力:字典翻译(dictType)、静态值转换(readConverterExp)、日期格式化。
+ *
+ * @Excel(name = "性别", dictType = "sys_user_sex", sort = 3)
+ * private Integer gender;
+ *
+ * @Excel(name = "状态", readConverterExp = "0=停用,1=正常", sort = 5)
+ * private Integer status;
+ *
+ * @Excel(name = "创建时间", dateFormat = "yyyy-MM-dd HH:mm:ss", sort = 6)
+ * private LocalDateTime createTime;
+ *
+ */
+@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.Date、LocalDate、LocalDateTime)
+ */
+ 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
+ }
+}
diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdWtBHController.java b/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdWtBHController.java
index 2eb6d5c9..f0eed18b 100644
--- a/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdWtBHController.java
+++ b/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdWtBHController.java
@@ -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 page = service.queryPageList(request);
+ new ExcelUtil<>(SdWtBH.class).exportExcel(response, page.getRecords(), "水温站数据", "水温站导出");
+ }
}
diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdWtBH.java b/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdWtBH.java
index 7f18dd9a..8a6151d2 100644
--- a/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdWtBH.java
+++ b/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdWtBH.java
@@ -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=高程 */
diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/warn/service/impl/WarnDataServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_env/warn/service/impl/WarnDataServiceImpl.java
index fc896698..583515f6 100644
--- a/backend/src/main/java/com/yfd/platform/qgc_env/warn/service/impl/WarnDataServiceImpl.java
+++ b/backend/src/main/java/com/yfd/platform/qgc_env/warn/service/impl/WarnDataServiceImpl.java
@@ -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) ")
diff --git a/backend/src/main/java/com/yfd/platform/utils/ExcelUtil.java b/backend/src/main/java/com/yfd/platform/utils/ExcelUtil.java
new file mode 100644
index 00000000..e2b10b19
--- /dev/null
+++ b/backend/src/main/java/com/yfd/platform/utils/ExcelUtil.java
@@ -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 设计)
+ *
+ * 使用方法:
+ *
+ * - 在实体字段上标注 {@link Excel} 注解,声明列名、排序、字典翻译、日期格式等;
+ * - Controller 中按需查询数据后调用:
+ * List<SysUser> list = sysUserService.list(wrapper);
+ * new ExcelUtil<>(SysUser.class).exportExcel(response, list, "用户数据", "用户数据");
+ *
+ *
+ * 特性:流式写出(SXSSF,适合大数据量)、表头冻结与自动筛选、列宽自适应、
+ * 字典翻译(复用 DictCache)、静态值转换(readConverterExp)、日期格式化、Long 转字符串避免精度丢失。
+ */
+@Slf4j
+public class ExcelUtil {
+
+ /** 表头背景色(中蓝色 RGB) */
+ private static final byte[] HEADER_BG = {(byte) 0x4A, (byte) 0x86, (byte) 0xC8};
+ /** 列宽上限(字符数),防止超长内容撑爆表格 */
+ private static final int MAX_COLUMN_WIDTH = 50;
+
+ private final Class clazz;
+ /** 参与导出的字段列表(已按 @Excel.sort 升序排列,仅保留 isExport=true) */
+ private final List exportFields;
+
+ public ExcelUtil(Class 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 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 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 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;
+ }
+}