zhou zhou
12 小时以前 6241b8acad2651564c1c668a9a54821361fbb4af
chore: sync rsf-server from isolated worktree
1个文件已删除
2个文件已添加
4个文件已修改
830 ■■■■ 已修改文件
rsf-server/src/main/java/com/vincent/rsf/server/common/service/ListExportService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/service/RedisService.java 252 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/RoleController.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/test/java/com/vincent/rsf/server/common/service/RedisServiceTest.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/test/java/com/vincent/rsf/server/common/utils/ExcelUtilReportStyleTest.java 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/test/java/com/vincent/rsf/server/system/controller/RoleControllerTest.java 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/service/ListExportService.java
@@ -144,11 +144,31 @@
        String reportDate = getMetaValue(metaMap, "reportDate", "");
        String printedAt = getMetaValue(metaMap, "printedAt", "");
        String operator = getMetaValue(metaMap, "operator", "");
        return new ExcelUtil.ExportMeta(reportTitle, reportDate, printedAt, operator, count);
        return new ExcelUtil.ExportMeta(
                reportTitle,
                reportDate,
                printedAt,
                operator,
                count,
                getReportStyle(metaMap)
        );
    }
    private String getMetaValue(Map<?, ?> metaMap, String key, String defaultValue) {
        Object value = metaMap.get(key);
        return Objects.isNull(value) ? defaultValue : String.valueOf(value);
    }
    private Map<String, Object> getReportStyle(Map<?, ?> metaMap) {
        Object reportStyleObject = metaMap.get("reportStyle");
        if (!(reportStyleObject instanceof Map<?, ?> reportStyleMap)) {
            return Collections.emptyMap();
        }
        Map<String, Object> reportStyle = new HashMap<>();
        for (Map.Entry<?, ?> entry : reportStyleMap.entrySet()) {
            reportStyle.put(String.valueOf(entry.getKey()), entry.getValue());
        }
        return reportStyle;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/service/RedisService.java
@@ -11,8 +11,6 @@
import java.util.Date;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
/**
 * redis tools
@@ -62,31 +60,6 @@
        return null;
    }
    private <T> T withJedis(Function<Jedis, T> action) {
        Jedis jedis = this.getJedis();
        if (jedis == null) {
            return null;
        }
        try (jedis) {
            return action.apply(jedis);
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    private void withJedisVoid(Consumer<Jedis> action) {
        Jedis jedis = this.getJedis();
        if (jedis == null) {
            return;
        }
        try (jedis) {
            action.accept(jedis);
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
    }
    // key - object ----------------------------------------------------------------------------------------------------------
    public String set(String flag, String key, Object value) {
@@ -97,7 +70,13 @@
            this.delete(flag, key);
            return null;
        }
        return withJedis(jedis -> jedis.set((flag + LINK + key).getBytes(), Serialize.serialize(value)));
        Jedis jedis = this.getJedis();
        try{
            return jedis.set((flag + LINK + key).getBytes(), Serialize.serialize(value));
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public String set(String flag, String key, Object value, Integer seconds) {
@@ -108,38 +87,58 @@
            this.delete(flag, key);
            return null;
        }
        return withJedis(jedis -> jedis.setex((flag + LINK + key).getBytes(), seconds, Serialize.serialize(value)));
        Jedis jedis = this.getJedis();
        try{
            return jedis.setex((flag + LINK + key).getBytes(), seconds, Serialize.serialize(value));
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public <T> T get(String flag, String key) {
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            byte[] bytes = jedis.get((flag + LINK + key).getBytes());
            if(bytes == null || bytes.length == 0 ) {
                return null;
            }
            return (T) Serialize.unSerialize(bytes);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public Long delete(String flag, String key) {
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> jedis.del((flag + LINK + key).getBytes()));
        Jedis jedis = this.getJedis();
        try{
            return jedis.del((flag + LINK + key).getBytes());
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public Long clear(String flag) {
        if(!this.initialize) {
            return null;
        }
        Jedis jedis = this.getJedis();
        this.setValue(flag, "CLEARING", "true");
        return withJedis(jedis -> {
        try{
            Object returnValue = jedis.eval("local keys = redis.call('keys', ARGV[1]) for i=1,#keys,1000 do redis.call('del', unpack(keys, i, math.min(i+4999, #keys))) end return #keys",0,flag + LINK + "*");
            return Long.parseLong(String.valueOf(returnValue));
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    // 为已存在的key设置过期时间 - 秒
@@ -147,9 +146,12 @@
        if(!this.initialize) {
            return;
        }
        withJedisVoid(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            jedis.expire((flag + LINK + key).getBytes(), seconds);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
    }
    // 为已存在的key设置过期时间 - 具体到时间戳 (秒)
@@ -157,9 +159,12 @@
        if(!this.initialize) {
            return;
        }
        withJedisVoid(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            jedis.expireAt((flag + LINK + key).getBytes(), toTime.getTime()/1000);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
    }
    // 获取过期剩余时间(秒) ttl == -1 没有设置过期时间; ttl == -2 key不存在
@@ -167,7 +172,13 @@
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> jedis.ttl((flag + LINK + key).getBytes()));
        Jedis jedis = this.getJedis();
        try{
            return jedis.ttl((flag + LINK + key).getBytes());
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
@@ -177,21 +188,39 @@
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> jedis.set(flag + LINK + key, value));
        Jedis jedis = this.getJedis();
        try{
            return jedis.set(flag + LINK + key, value);
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public String setValue(String flag, String key, String value, Integer seconds) {
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> jedis.setex(flag + LINK + key, seconds , value));
        Jedis jedis = this.getJedis();
        try{
            return jedis.setex(flag + LINK + key, seconds , value);
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public String getValue(String flag, String key) {
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> jedis.get(flag + LINK + key));
        Jedis jedis = this.getJedis();
        try{
            return jedis.get(flag + LINK + key);
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public Long deleteValue(String flag, String... key) {
@@ -199,13 +228,17 @@
            return null;
        }
        return withJedis(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            String[] keys = new String[key.length];
            for(int i=0;i<key.length;i++){
                keys[i] = flag + LINK + key[i];
            }
            return jedis.del(keys);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public Long clearValue(String flag) {
@@ -214,28 +247,39 @@
        }
        this.setValue(flag, "CLEARING", "true");
        return withJedis(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            Object returnValue = jedis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))",0,flag + LINK + "*");
            return Long.parseLong(String.valueOf(returnValue));
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public void setValueExpire(String flag, String key,int seconds){
        if(!this.initialize) {
            return;
        }
        withJedisVoid(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            jedis.expire((flag + LINK + key).getBytes(), seconds);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
    }
    public void setValueExpireAt(String flag, String key,Date atTime){
        if(!this.initialize) {
            return;
        }
        withJedisVoid(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            jedis.expireAt((flag + LINK + key).getBytes(), atTime.getTime()/1000);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
    }
@@ -249,63 +293,95 @@
            deleteMap(name,key);
            return null;
        }
        return withJedis(jedis -> jedis.hset(name.getBytes(), key.getBytes(), Serialize.serialize(value)));
        Jedis jedis = this.getJedis();
        try {
            return jedis.hset(name.getBytes(), key.getBytes(), Serialize.serialize(value));
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public <T> T getMap(String name, String key) {
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            byte[] bytes = jedis.hget(name.getBytes(), key.getBytes());
            if (bytes == null || bytes.length == 0) {
                return null;
            }
            return (T) Serialize.unSerialize(bytes);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public Set<String> getMapKeys(String name) {
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> jedis.hkeys(name));
        Jedis jedis = this.getJedis();
        try{
            return jedis.hkeys(name);
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public Long deleteMap(String name, String... key) {
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            String[] keys = new String[key.length];
            System.arraycopy(key, 0, keys, 0, key.length);
            return jedis.hdel(name, keys);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public Long clearMap(String name) {
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> jedis.del(name));
        Jedis jedis = this.getJedis();
        try{
            return jedis.del(name);
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public void setMapExpire(String name,int seconds){
        if(!this.initialize) {
            return;
        }
        withJedisVoid(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            jedis.expire(name.getBytes(), seconds);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
    }
    public void setMapExpireAt(String name,Date atTime){
        if(!this.initialize) {
            return;
        }
        withJedisVoid(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            jedis.expireAt(name.getBytes(), atTime.getTime()/1000);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
    }
@@ -319,7 +395,13 @@
        if(value == null){
            return null;
        }
        return withJedis(jedis -> jedis.rpush(name.getBytes(), Serialize.serialize(value)));
        Jedis jedis = this.getJedis();
        try{
            return jedis.rpush(name.getBytes(), Serialize.serialize(value));
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    // 获取列表头部元素 && 删除
@@ -327,13 +409,17 @@
        if(!this.initialize){
            return null;
        }
        return withJedis(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            byte[] bytes = jedis.lpop(name.getBytes());
            if(bytes == null || bytes.length == 0) {
                return null;
            }
            return (T) Serialize.unSerialize(bytes);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    // 删除
@@ -341,25 +427,37 @@
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> jedis.del(name));
        Jedis jedis = this.getJedis();
        try{
            return jedis.del(name);
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public void setListExpire(String name, int seconds){
        if(!this.initialize) {
            return;
        }
        withJedisVoid(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            jedis.expire(name.getBytes(), seconds);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
    }
    public void setListExpireAt(String name, Date atTime){
        if(!this.initialize) {
            return;
        }
        withJedisVoid(jedis -> {
        Jedis jedis = this.getJedis();
        try{
            jedis.expireAt(name.getBytes(), atTime.getTime()/1000);
        });
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
    }
    // count ----------------------------------------------------------------------------------------------------------
@@ -368,14 +466,26 @@
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> jedis.incr("COUNT." + key));
        Jedis jedis = this.getJedis();
        try{
            return jedis.incr("COUNT." + key);
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
    public Long decr(String key) {
        if(!this.initialize) {
            return null;
        }
        return withJedis(jedis -> jedis.decr("COUNT." + key));
        Jedis jedis = this.getJedis();
        try{
            return jedis.decr("COUNT." + key);
        } catch (Exception e) {
            log.error(this.getClass().getSimpleName(), e);
        }
        return null;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java
@@ -33,6 +33,8 @@
@Slf4j
public class ExcelUtil {
    private static final Pattern EXTEND_FIELD_SOURCE_PATTERN = Pattern.compile("^extendFields\\.\\[(.+)]$");
    private static final String SEQUENCE_SOURCE = "sequence";
    private static final String SEQUENCE_LABEL = "序号";
    public static void build(Workbook workbook, HttpServletResponse response) {
        response.reset();
@@ -136,13 +138,19 @@
    public static Workbook create(List<Map<String, Object>> rows, List<ExportColumn> columns, ExportMeta exportMeta) {
        XSSFWorkbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("export");
        int titleColumnCount = Math.max(columns.size(), 4);
        List<ExportColumn> effectiveColumns = buildEffectiveColumns(columns, exportMeta);
        boolean generatedSequenceColumn = hasGeneratedSequenceColumn(columns, effectiveColumns);
        int titleColumnCount = Math.max(effectiveColumns.size(), 4);
        int currentRowIndex = 0;
        CellStyle titleStyle = createTitleStyle(workbook);
        CellStyle subHeaderStyle = createSubHeaderStyle(workbook);
        CellStyle headerStyle = createHeaderStyle(workbook);
        CellStyle bodyStyle = createBodyStyle(workbook);
        if (exportMeta != null && exportMeta.isLandscape()) {
            sheet.getPrintSetup().setLandscape(true);
        }
        if (exportMeta != null && StringUtils.isNotBlank(exportMeta.getReportTitle())) {
            Row titleRow = sheet.createRow(currentRowIndex++);
@@ -162,26 +170,62 @@
        }
        Row header = sheet.createRow(currentRowIndex++);
        for (int index = 0; index < columns.size(); index++) {
        for (int index = 0; index < effectiveColumns.size(); index++) {
            Cell headerCell = header.createCell(index);
            headerCell.setCellValue(columns.get(index).getLabel());
            headerCell.setCellValue(effectiveColumns.get(index).getLabel());
            headerCell.setCellStyle(headerStyle);
        }
        int rowIndex = currentRowIndex;
        for (Map<String, Object> rowData : rows) {
        for (int dataIndex = 0; dataIndex < rows.size(); dataIndex++) {
            Map<String, Object> rowData = rows.get(dataIndex);
            Row row = sheet.createRow(rowIndex++);
            for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) {
                Object value = getRowValue(rowData, columns.get(columnIndex).getSource());
            for (int columnIndex = 0; columnIndex < effectiveColumns.size(); columnIndex++) {
                Object value = getExportCellValue(rowData, effectiveColumns.get(columnIndex).getSource(), generatedSequenceColumn, dataIndex);
                writeCellValue(row, columnIndex, value, bodyStyle);
            }
        }
        for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) {
        for (int columnIndex = 0; columnIndex < effectiveColumns.size(); columnIndex++) {
            sheet.autoSizeColumn(columnIndex);
        }
        return workbook;
    }
    private static List<ExportColumn> buildEffectiveColumns(List<ExportColumn> columns, ExportMeta exportMeta) {
        List<ExportColumn> effectiveColumns = new ArrayList<>(columns);
        if (exportMeta != null && exportMeta.isShowSequence() && !containsSequenceColumn(columns)) {
            effectiveColumns.add(0, new ExportColumn(SEQUENCE_SOURCE, SEQUENCE_LABEL));
        }
        return effectiveColumns;
    }
    private static boolean containsSequenceColumn(List<ExportColumn> columns) {
        for (ExportColumn column : columns) {
            if (SEQUENCE_SOURCE.equals(column.getSource())) {
                return true;
            }
        }
        return false;
    }
    private static boolean hasGeneratedSequenceColumn(List<ExportColumn> originalColumns, List<ExportColumn> effectiveColumns) {
        return effectiveColumns.size() > originalColumns.size()
                && !effectiveColumns.isEmpty()
                && SEQUENCE_SOURCE.equals(effectiveColumns.get(0).getSource());
    }
    private static Object getExportCellValue(
            Map<String, Object> rowData,
            String source,
            boolean generatedSequenceColumn,
            int dataIndex
    ) {
        if (generatedSequenceColumn && SEQUENCE_SOURCE.equals(source)) {
            return dataIndex + 1;
        }
        return getRowValue(rowData, source);
    }
    private static Object getRowValue(Map<String, Object> rowData, String source) {
@@ -301,13 +345,26 @@
        private final String printedAt;
        private final String operator;
        private final int count;
        private final Map<String, Object> reportStyle;
        public ExportMeta(String reportTitle, String reportDate, String printedAt, String operator, int count) {
            this(reportTitle, reportDate, printedAt, operator, count, Collections.emptyMap());
        }
        public ExportMeta(
                String reportTitle,
                String reportDate,
                String printedAt,
                String operator,
                int count,
                Map<String, ?> reportStyle
        ) {
            this.reportTitle = reportTitle;
            this.reportDate = reportDate;
            this.printedAt = printedAt;
            this.operator = operator;
            this.count = count;
            this.reportStyle = normalizeReportStyle(reportStyle);
        }
        public String getReportTitle() {
@@ -329,6 +386,41 @@
        public int getCount() {
            return count;
        }
        public Map<String, Object> getReportStyle() {
            return reportStyle;
        }
        public boolean isLandscape() {
            return "landscape".equalsIgnoreCase(Objects.toString(getReportStyleValue("orientation"), ""));
        }
        public boolean isShowSequence() {
            if (reportStyle.isEmpty() || !reportStyle.containsKey("showSequence")) {
                return true;
            }
            Object value = getReportStyleValue("showSequence");
            if (value instanceof Boolean boolValue) {
                return boolValue;
            }
            return !"false".equalsIgnoreCase(Objects.toString(value, ""));
        }
        private Object getReportStyleValue(String key) {
            return reportStyle.get(key);
        }
        private static Map<String, Object> normalizeReportStyle(Map<String, ?> reportStyle) {
            if (reportStyle == null || reportStyle.isEmpty()) {
                return Collections.emptyMap();
            }
            Map<String, Object> normalizedReportStyle = new HashMap<>();
            for (Map.Entry<String, ?> entry : reportStyle.entrySet()) {
                normalizedReportStyle.put(entry.getKey(), entry.getValue());
            }
            return Collections.unmodifiableMap(normalizedReportStyle);
        }
    }
    /**
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/RoleController.java
@@ -9,6 +9,8 @@
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.KeyValVo;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.service.ListExportHandler;
import com.vincent.rsf.server.common.service.ListExportService;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.system.controller.param.RoleScopeParam;
import com.vincent.rsf.server.system.entity.Role;
@@ -33,6 +35,38 @@
    private RoleService roleService;
    @Autowired
    private RoleMenuService roleMenuService;
    @Autowired
    private ListExportService listExportService;
    private final ListExportHandler<Role, BaseParam> roleExportHandler = new ListExportHandler<>() {
        @Override
        public List<Role> listByIds(List<Long> ids) {
            return roleService.listByIds(ids);
        }
        @Override
        public List<Role> listByFilter(Map<String, Object> sanitizedMap, BaseParam baseParam) {
            PageParam<Role, BaseParam> pageParam = new PageParam<>(baseParam, Role.class);
            return roleService.list(pageParam.buildWrapper(true));
        }
        @Override
        public Map<String, Object> toExportRow(Role record, List<ExcelUtil.ExportColumn> columns) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("name", record.getName());
            row.put("code", record.getCode());
            row.put("statusText", Objects.equals(record.getStatus(), 1) ? "正常" : "禁用");
            row.put("memo", record.getMemo());
            row.put("createTimeText", record.getCreateTime$());
            row.put("updateTimeText", record.getUpdateTime$());
            return row;
        }
        @Override
        public String defaultReportTitle() {
            return "角色管理报表";
        }
    };
    @PreAuthorize("hasAuthority('system:role:list')")
    @PostMapping("/role/page")
@@ -134,7 +168,7 @@
    @PreAuthorize("hasAuthority('system:role:list')")
    @PostMapping("/role/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(roleService.list(), Role.class), response);
        listExportService.export(map, exportMap -> buildParam(exportMap, BaseParam.class), roleExportHandler, response);
    }
    @PreAuthorize("hasAuthority('system:role:list')")
rsf-server/src/test/java/com/vincent/rsf/server/common/service/RedisServiceTest.java
File was deleted
rsf-server/src/test/java/com/vincent/rsf/server/common/utils/ExcelUtilReportStyleTest.java
New file
@@ -0,0 +1,121 @@
package com.vincent.rsf.server.common.utils;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class ExcelUtilReportStyleTest {
    @Test
    void exportMetaExposesPublicSixArgReportStyleConstructorContract() {
        Constructor<?> constructor = findPublicReportStyleConstructor(
                "ExcelUtil.ExportMeta has not exposed a public reportStyle constructor contract yet; expected a public 6-arg constructor whose sixth parameter is a Map-compatible reportStyle payload."
        );
        assertEquals(6, constructor.getParameterCount(), "ExcelUtil.ExportMeta must expose a public 6-arg constructor for reportStyle.");
        assertTrue(
                Map.class.isAssignableFrom(constructor.getParameterTypes()[5]),
                "ExcelUtil.ExportMeta public reportStyle constructor must accept a Map-compatible sixth argument."
        );
    }
    @Test
    void createBuildsSharedReportSkeletonWhenLandscapeReportStyleIsProvided() throws Exception {
        Constructor<?> constructor = findPublicReportStyleConstructor(
                "Cannot verify landscape reportStyle runtime behavior until ExcelUtil.ExportMeta exposes a public 6-arg constructor whose sixth parameter is a Map-compatible reportStyle payload."
        );
        List<Map<String, Object>> rows = List.of(row("1", "管理员"));
        List<ExcelUtil.ExportColumn> columns = List.of(
                new ExcelUtil.ExportColumn("sequence", "序号"),
                new ExcelUtil.ExportColumn("name", "角色名称")
        );
        ExcelUtil.ExportMeta exportMeta = (ExcelUtil.ExportMeta) constructor.newInstance(
                "角色管理报表",
                "2026-03-29",
                "2026-03-29 10:00:00",
                "tester",
                rows.size(),
                Map.of(
                        "orientation", "landscape",
                        "showSequence", true
                )
        );
        try (Workbook workbook = ExcelUtil.create(rows, columns, exportMeta)) {
            Sheet sheet = workbook.getSheetAt(0);
            assertEquals("角色管理报表", sheet.getRow(0).getCell(0).getStringCellValue());
            CellStyle titleStyle = sheet.getRow(0).getCell(0).getCellStyle();
            assertEquals(HorizontalAlignment.CENTER, titleStyle.getAlignment());
            Font titleFont = workbook.getFontAt(titleStyle.getFontIndex());
            assertTrue(titleFont.getFontHeightInPoints() >= 14);
            assertTrue(sheet.getRow(1).getCell(0).getStringCellValue().startsWith("报表日期:"));
            assertTrue(sheet.getPrintSetup().getLandscape());
            assertEquals("角色名称", sheet.getRow(3).getCell(1).getStringCellValue());
        }
    }
    @Test
    void createPrependsSequenceColumnByDefaultWhenReportStyleOmitsShowSequence() throws Exception {
        Constructor<?> constructor = findPublicReportStyleConstructor(
                "Cannot verify default showSequence behavior until ExcelUtil.ExportMeta exposes a public 6-arg constructor whose sixth parameter is a Map-compatible reportStyle payload."
        );
        List<Map<String, Object>> rows = List.of(row(null, "管理员"));
        List<ExcelUtil.ExportColumn> columns = List.of(
                new ExcelUtil.ExportColumn("name", "角色名称")
        );
        ExcelUtil.ExportMeta exportMeta = (ExcelUtil.ExportMeta) constructor.newInstance(
                "角色管理报表",
                "2026-03-29",
                "2026-03-29 10:00:00",
                "tester",
                rows.size(),
                Map.of("orientation", "portrait")
        );
        try (Workbook workbook = ExcelUtil.create(rows, columns, exportMeta)) {
            Sheet sheet = workbook.getSheetAt(0);
            assertEquals("序号", sheet.getRow(3).getCell(0).getStringCellValue());
            assertEquals("1", sheet.getRow(4).getCell(0).getStringCellValue());
            assertEquals("角色名称", sheet.getRow(3).getCell(1).getStringCellValue());
        }
    }
    private Constructor<?> findPublicReportStyleConstructor(String missingConstructorMessage) {
        for (Constructor<?> constructor : ExcelUtil.ExportMeta.class.getConstructors()) {
            if (!Modifier.isPublic(constructor.getModifiers()) || constructor.getParameterCount() != 6) {
                continue;
            }
            Class<?>[] parameterTypes = constructor.getParameterTypes();
            if (Map.class.isAssignableFrom(parameterTypes[5])) {
                return constructor;
            }
        }
        fail(missingConstructorMessage);
        return null;
    }
    private Map<String, Object> row(String sequence, String name) {
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("sequence", sequence);
        row.put("name", name);
        return row;
    }
}
rsf-server/src/test/java/com/vincent/rsf/server/system/controller/RoleControllerTest.java
New file
@@ -0,0 +1,240 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.service.ListExportService;
import com.vincent.rsf.server.system.entity.Role;
import com.vincent.rsf.server.system.service.RoleMenuService;
import com.vincent.rsf.server.system.service.RoleService;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.doReturn;
@ExtendWith(MockitoExtension.class)
class RoleControllerTest {
    @Mock
    private RoleService roleService;
    @Mock
    private RoleMenuService roleMenuService;
    @Mock
    private HttpServletResponse response;
    private RoleController roleController;
    private ListExportService listExportService;
    private ByteArrayOutputStream exportBuffer;
    @BeforeEach
    void setUp() throws Exception {
        roleController = new TestableRoleController();
        ReflectionTestUtils.setField(roleController, "roleService", roleService);
        ReflectionTestUtils.setField(roleController, "roleMenuService", roleMenuService);
        listExportService = new ListExportService();
        ReflectionTestUtils.setField(roleController, "listExportService", listExportService);
        exportBuffer = new ByteArrayOutputStream();
        lenient().doReturn(new ServletOutputStream() {
            @Override
            public void write(int b) {
                exportBuffer.write(b);
            }
            @Override
            public boolean isReady() {
                return true;
            }
            @Override
            public void setWriteListener(jakarta.servlet.WriteListener writeListener) {
            }
        }).when(response).getOutputStream();
    }
    @Test
    void exportPrefersSelectedIdsEvenWhenFiltersArePresent() throws Exception {
        Role role = role(1L, "管理员", "ADMIN", 1, "系统角色");
        Map<String, Object> body = new HashMap<>();
        body.put("ids", List.of(1L, 2L));
        body.put("name", "管理员");
        body.put("current", 1);
        body.put("pageSize", 20);
        body.put("meta", Map.of(
                "reportTitle", "角色管理报表",
                "columns", List.of(
                        Map.of("source", "name", "label", "角色名称"),
                        Map.of("source", "statusText", "label", "状态"),
                        Map.of("source", "memo", "label", "备注")
                )
        ));
        when(roleService.listByIds(List.of(1L, 2L))).thenReturn(List.of(role));
        roleController.export(body, response);
        verify(roleService).listByIds(List.of(1L, 2L));
        verify(roleService, never()).list(anyWrapper());
        assertExportSheet(
                exportBuffer.toByteArray(),
                List.of("序号", "角色名称", "状态", "备注"),
                List.of("1", "管理员", "正常", "系统角色")
        );
    }
    @Test
    void pageStillAcceptsFlatPrintQueryWithoutFilterFields() {
        Map<String, Object> body = new HashMap<>();
        body.put("current", 1);
        body.put("pageSize", 200);
        when(roleService.page(any(), any())).thenReturn(new Page<>());
        R result = roleController.page(body);
        assertNotNull(result);
        verify(roleService).page(any(), any());
    }
    @Test
    void exportUsesFilteredRoleListWhenIdsMissing() throws Exception {
        Role role = role(1L, "管理员", "ADMIN", 1, "系统角色");
        Map<String, Object> body = new HashMap<>();
        body.put("name", "管理员");
        body.put("current", 1);
        body.put("pageSize", 200);
        body.put("meta", Map.of(
                "reportTitle", "角色管理报表",
                "columns", List.of(
                        Map.of("source", "name", "label", "角色名称"),
                        Map.of("source", "statusText", "label", "状态")
                )
        ));
        when(roleService.list(anyWrapper())).thenReturn(List.of(role));
        roleController.export(body, response);
        verify(roleService).list(anyWrapper());
        verify(roleService, never()).listByIds(any());
    }
    @Test
    void exportInsertsSequenceHeaderFromReportStyleWhenSequenceColumnIsNotRequested() throws Exception {
        Role role = role(1L, "管理员", "ADMIN", 1, "系统角色");
        List<Map<String, Object>> requestedColumns = List.of(
                Map.of("source", "name", "label", "角色名称"),
                Map.of("source", "statusText", "label", "状态")
        );
        Map<String, Object> body = new HashMap<>();
        body.put("ids", List.of(1L));
        body.put("meta", Map.of(
                "reportTitle", "角色管理报表",
                "reportDate", "2026-03-29",
                "columns", requestedColumns,
                "reportStyle", Map.of(
                        "showSequence", true
                )
        ));
        when(roleService.listByIds(List.of(1L))).thenReturn(List.of(role));
        roleController.export(body, response);
        try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(exportBuffer.toByteArray()))) {
            Sheet sheet = workbook.getSheetAt(0);
            assertEquals("角色管理报表", sheet.getRow(0).getCell(0).getStringCellValue());
            assertTrue(sheet.getRow(1).getCell(0).getStringCellValue().startsWith("报表日期:"));
            assertAll("showSequence generated column",
                    () -> assertEquals("序号", sheet.getRow(3).getCell(0).getStringCellValue(),
                            "showSequence=true should prepend a generated 序号 column even when request columns omit it."),
                    () -> assertEquals("1", sheet.getRow(4).getCell(0).getStringCellValue(),
                            "showSequence=true should generate the first sequence cell as 1 in the exported data row.")
            );
            assertEquals("角色名称", sheet.getRow(3).getCell(1).getStringCellValue(),
                    "Generated 序号 column should shift requested columns to the right.");
        }
    }
    private void assertExportSheet(byte[] workbookBytes, List<String> expectedHeaders, List<String> expectedFirstRow) throws Exception {
        try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(workbookBytes))) {
            Sheet sheet = workbook.getSheetAt(0);
            for (int index = 0; index < expectedHeaders.size(); index++) {
                assertEquals(expectedHeaders.get(index), sheet.getRow(3).getCell(index).getStringCellValue());
                assertEquals(expectedFirstRow.get(index), sheet.getRow(4).getCell(index).getStringCellValue());
            }
        }
    }
    private Role role(Long id, String name, String code, Integer status, String memo) {
        Role role = new Role();
        role.setId(id);
        role.setName(name);
        role.setCode(code);
        role.setStatus(status);
        role.setMemo(memo);
        return role;
    }
    @SuppressWarnings("unchecked")
    private Wrapper<Role> anyWrapper() {
        return any(Wrapper.class);
    }
    private static final class TestableRoleController extends RoleController {
        @Override
        public <T extends BaseParam> T buildParam(Map<String, Object> map, Class<T> clz) {
            try {
                T param = clz.getDeclaredConstructor().newInstance();
                Map<String, Object> copy = new HashMap<>(map);
                Object metaObject = copy.remove("meta");
                if (metaObject instanceof Map<?, ?> metaMap) {
                    metaMap.forEach((key, value) -> copy.put(String.valueOf(key), value));
                }
                BaseParam baseParam = param;
                baseParam.setCurrent(asInteger(copy.remove("current")));
                baseParam.setPageSize(asInteger(copy.remove("pageSize")));
                baseParam.setOrderBy(asString(copy.remove("orderBy")));
                baseParam.setCondition(asString(copy.remove("condition")));
                baseParam.setMap(copy);
                return param;
            } catch (Exception error) {
                throw new RuntimeException(error);
            }
        }
        private Integer asInteger(Object value) {
            return value == null ? null : Integer.valueOf(String.valueOf(value));
        }
        private String asString(Object value) {
            return value == null ? null : String.valueOf(value);
        }
    }
}