org.apache.fesod
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java
index 8c1092398..5bd976e88 100644
--- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeStrategy.java
@@ -10,6 +10,7 @@ import org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
+import org.dromara.common.excel.handler.CellMergeHandler;
import java.util.List;
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/CellMergeHandler.java
similarity index 99%
rename from ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java
rename to ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/CellMergeHandler.java
index 55d6267f2..b82198af5 100644
--- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/CellMergeHandler.java
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/CellMergeHandler.java
@@ -1,4 +1,4 @@
-package org.dromara.common.excel.core;
+package org.dromara.common.excel.handler;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/ExcelDownHandler.java
similarity index 99%
rename from ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java
rename to ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/ExcelDownHandler.java
index d84580acb..5db6310e8 100644
--- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/ExcelDownHandler.java
@@ -1,4 +1,4 @@
-package org.dromara.common.excel.core;
+package org.dromara.common.excel.handler;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
@@ -26,6 +26,8 @@ import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.annotation.ExcelDynamicOptions;
import org.dromara.common.excel.annotation.ExcelEnumFormat;
+import org.dromara.common.excel.core.DropDownOptions;
+import org.dromara.common.excel.core.ExcelOptionsProvider;
import java.lang.reflect.Field;
import java.util.*;
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/SensitiveWriteHandler.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/SensitiveWriteHandler.java
new file mode 100644
index 000000000..4e2442033
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/handler/SensitiveWriteHandler.java
@@ -0,0 +1,165 @@
+package org.dromara.common.excel.handler;
+
+import cn.hutool.core.util.ObjectUtil;
+import org.apache.fesod.sheet.enums.CellDataTypeEnum;
+import org.apache.fesod.sheet.metadata.Head;
+import org.apache.fesod.sheet.metadata.data.WriteCellData;
+import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
+import org.apache.fesod.sheet.write.handler.CellWriteHandler;
+import org.apache.fesod.sheet.write.handler.context.CellWriteHandlerContext;
+import org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder;
+import org.apache.fesod.sheet.write.metadata.holder.WriteTableHolder;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.sensitive.annotation.Sensitive;
+import org.dromara.common.sensitive.core.SensitiveService;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Excel 导出脱敏处理器。
+ */
+public class SensitiveWriteHandler implements CellWriteHandler {
+
+ /**
+ * 脱敏服务 Bean。服务本身只取一次,具体是否脱敏仍由服务在当前线程内按登录态判断。
+ */
+ private static volatile SensitiveService sensitiveService;
+
+ /**
+ * 是否已尝试加载脱敏服务。没有引入实现时也只查一次,避免每个单元格反复访问 Spring 容器。
+ */
+ private static volatile boolean sensitiveServiceLoaded;
+
+ /**
+ * 有脱敏注解的列缓存。
+ *
+ * key 同时包含 sheet/table holder 身份与列号,避免同一个写处理器在多 sheet 或多 table 写出时列号相同但字段不同。
+ */
+ private final Map sensitiveColumnMap = new ConcurrentHashMap<>();
+
+ /**
+ * 无脱敏注解的列缓存。非敏感列命中后直接跳过,避免每行重复解析字段注解。
+ */
+ private final Set noneSensitiveColumnSet = ConcurrentHashMap.newKeySet();
+
+ /**
+ * 延迟获取脱敏服务
+ *
+ * @return 脱敏服务
+ */
+ private static SensitiveService getSensitiveService() {
+ if (!sensitiveServiceLoaded) {
+ synchronized (SensitiveWriteHandler.class) {
+ if (!sensitiveServiceLoaded) {
+ try {
+ sensitiveService = SpringUtils.getBean(SensitiveService.class);
+ } catch (NoSuchBeanDefinitionException ignored) {
+ sensitiveService = null;
+ }
+ sensitiveServiceLoaded = true;
+ }
+ }
+ }
+ return sensitiveService;
+ }
+
+ /**
+ * 在单元格数据转换后按字段脱敏注解改写导出值。
+ *
+ * @param context 单元格写入上下文
+ */
+ @Override
+ public void afterCellDataConverted(CellWriteHandlerContext context) {
+ if (Boolean.TRUE.equals(context.getHead()) || context.getColumnIndex() == null
+ || !(context.getOriginalValue() instanceof String text)) {
+ return;
+ }
+ Sensitive sensitive = getSensitive(context);
+ if (sensitive == null) {
+ return;
+ }
+ SensitiveService service = getSensitiveService();
+ if (ObjectUtil.isNotNull(service) && service.isSensitive(sensitive.roleKey(), sensitive.perms())) {
+ WriteCellData> cellData = context.getFirstCellData();
+ if (cellData != null) {
+ String value = sensitive.strategy().desensitizer().apply(text);
+ if (value == null) {
+ cellData.setType(CellDataTypeEnum.EMPTY);
+ return;
+ }
+ cellData.setType(CellDataTypeEnum.STRING);
+ cellData.setStringValue(value);
+ }
+ }
+ }
+
+ /**
+ * 获取当前单元格对应字段上的脱敏注解。
+ *
+ * @param context 单元格写入上下文
+ * @return 脱敏注解
+ */
+ private Sensitive getSensitive(CellWriteHandlerContext context) {
+ ColumnKey columnKey = buildColumnKey(context);
+ Sensitive cached = sensitiveColumnMap.get(columnKey);
+ if (cached != null) {
+ return cached;
+ }
+ if (noneSensitiveColumnSet.contains(columnKey)) {
+ return null;
+ }
+ Sensitive sensitive = resolveSensitive(context);
+ if (sensitive == null) {
+ noneSensitiveColumnSet.add(columnKey);
+ return null;
+ }
+ sensitiveColumnMap.put(columnKey, sensitive);
+ return sensitive;
+ }
+
+ /**
+ * 解析当前列对应字段上的脱敏注解。
+ *
+ * @param context 单元格写入上下文
+ * @return 脱敏注解
+ */
+ private Sensitive resolveSensitive(CellWriteHandlerContext context) {
+ Head headData = context.getHeadData();
+ Field field = headData == null ? null : headData.getField();
+ if (field == null) {
+ ExcelContentProperty contentProperty = context.getExcelContentProperty();
+ field = contentProperty == null ? null : contentProperty.getField();
+ }
+ return field == null ? null : field.getAnnotation(Sensitive.class);
+ }
+
+ /**
+ * 构建列缓存 key。
+ *
+ * Fesod 在自定义写出时同一个处理器可能服务多个 sheet/table
+ *
+ * @param context 单元格写入上下文
+ * @return 列缓存 key
+ */
+ private ColumnKey buildColumnKey(CellWriteHandlerContext context) {
+ WriteTableHolder tableHolder = context.getWriteTableHolder();
+ if (tableHolder != null) {
+ return new ColumnKey(System.identityHashCode(tableHolder), context.getColumnIndex());
+ }
+ WriteSheetHolder sheetHolder = context.getWriteSheetHolder();
+ return new ColumnKey(System.identityHashCode(sheetHolder), context.getColumnIndex());
+ }
+
+ /**
+ * 列缓存 key。
+ *
+ * @param holderIdentity sheet/table holder 对象身份
+ * @param columnIndex 列索引
+ */
+ private record ColumnKey(int holderIdentity, int columnIndex) {
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelBuilder.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelBuilder.java
index d1ce4f6c0..412c6aa4e 100644
--- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelBuilder.java
+++ b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelBuilder.java
@@ -27,6 +27,8 @@ import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.excel.convert.ExcelBigNumberConvert;
import org.dromara.common.excel.core.*;
import org.dromara.common.excel.handler.DataWriteHandler;
+import org.dromara.common.excel.handler.ExcelDownHandler;
+import org.dromara.common.excel.handler.SensitiveWriteHandler;
import java.io.*;
import java.net.URLEncoder;
@@ -474,6 +476,8 @@ public final class ExcelBuilder {
.autoCloseStream(false)
// 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert())
+ // 敏感字段脱敏
+ .registerWriteHandler(new SensitiveWriteHandler())
// 批注必填项处理
.registerWriteHandler(new DataWriteHandler(headType));
if (columnWidth == null) {
diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestSensitiveController.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestSensitiveController.java
index fe4ed6c3c..de1ef4da6 100644
--- a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestSensitiveController.java
+++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestSensitiveController.java
@@ -1,7 +1,13 @@
package org.dromara.demo.controller;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
+import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.dromara.common.core.domain.R;
+import org.dromara.common.excel.utils.ExcelBuilder;
import org.dromara.common.sensitive.annotation.Sensitive;
import org.dromara.common.sensitive.core.SensitiveService;
import org.dromara.common.sensitive.core.SensitiveStrategy;
@@ -10,6 +16,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import java.util.List;
+
/**
* 测试数据脱敏控制器
*
@@ -38,39 +46,61 @@ public class TestSensitiveController extends BaseController {
return R.ok(testSensitive);
}
+ /**
+ * 测试导出 Excel 脱敏
+ */
+ @GetMapping("/export")
+ public void exporttest(HttpServletResponse response) {
+ TestSensitive testSensitive = new TestSensitive();
+ testSensitive.setIdCard("210397198608215431");
+ testSensitive.setPhone("17640125371");
+ testSensitive.setAddress("北京市朝阳区某某四合院1203室");
+ testSensitive.setEmail("17640125371@163.com");
+ testSensitive.setBankCard("6226456952351452853");
+ ExcelBuilder.of(List.of(testSensitive), TestSensitive.class).sheetName("测试树表").toResponse(response);
+ }
+
/**
* 脱敏测试对象。
*/
@Data
+ @ExcelIgnoreUnannotated
+ @AllArgsConstructor
+ @NoArgsConstructor
static class TestSensitive {
/**
* 身份证
*/
+ @ExcelProperty(value = "身份证", index = 0)
@Sensitive(strategy = SensitiveStrategy.ID_CARD)
private String idCard;
/**
* 电话
*/
+ @ExcelProperty(value = "电话", index = 1)
@Sensitive(strategy = SensitiveStrategy.PHONE, roleKey = "common")
private String phone;
/**
* 地址
*/
+ @ExcelProperty(value = "地址", index = 2)
@Sensitive(strategy = SensitiveStrategy.ADDRESS, perms = "system:user:query")
private String address;
/**
* 邮箱
*/
+ @ExcelProperty(value = "邮箱", index = 3)
@Sensitive(strategy = SensitiveStrategy.EMAIL, roleKey = "common", perms = "system:user:query1")
private String email;
/**
* 银行卡
*/
+ @ExcelProperty(value = "银行卡", index = 4)
@Sensitive(strategy = SensitiveStrategy.BANK_CARD, roleKey = "common1", perms = "system:user:query")
private String bankCard;