diff --git a/src/main/java/cn/isliu/FsHelper.java b/src/main/java/cn/isliu/FsHelper.java index ae59344..dd86664 100644 --- a/src/main/java/cn/isliu/FsHelper.java +++ b/src/main/java/cn/isliu/FsHelper.java @@ -5,12 +5,10 @@ import cn.isliu.core.FileData; import cn.isliu.core.FsTableData; import cn.isliu.core.Sheet; import cn.isliu.core.annotation.TableConf; -import cn.isliu.core.builder.MapWriteBuilder; -import cn.isliu.core.builder.ReadBuilder; -import cn.isliu.core.builder.SheetBuilder; -import cn.isliu.core.builder.WriteBuilder; +import cn.isliu.core.builder.*; import cn.isliu.core.client.FeishuClient; import cn.isliu.core.client.FsClient; +import cn.isliu.core.config.MapSheetConfig; import cn.isliu.core.config.MapTableConfig; import cn.isliu.core.enums.ErrorCode; import cn.isliu.core.enums.FileType; @@ -101,6 +99,86 @@ public class FsHelper { return new SheetBuilder<>(sheetName, spreadsheetToken, clazz); } + /** + * 使用 Map 配置创建飞书表格 + * + * 直接使用 Map 配置创建飞书表格,无需定义实体类和注解。 + * 适用于动态字段、临时表格创建等场景。 + * + * 使用示例: + *
+     * MapSheetConfig config = MapSheetConfig.sheetBuilder()
+     *     .titleRow(2)
+     *     .headLine(3)
+     *     .headStyle("#ffffff", "#000000")
+     *     .isText(true)
+     *     .enableDesc(true)
+     *     .addField(MapFieldDefinition.text("字段1", 0))
+     *     .addField(MapFieldDefinition.singleSelect("字段2", 1, "选项1", "选项2"))
+     *     .build();
+     *
+     * String sheetId = FsHelper.createMapSheet("表格名称", spreadsheetToken, config);
+     * 
+ * + * @param sheetName 工作表名称 + * @param spreadsheetToken 电子表格Token + * @param config 表格配置,包含字段定义、样式等信息 + * @return 创建成功返回工作表ID + */ + public static String createMapSheet(String sheetName, String spreadsheetToken, MapSheetConfig config) { + return new MapSheetBuilder(sheetName, spreadsheetToken) + .config(config) + .build(); + } + + /** + * 创建 Map 表格构建器 + * + * 返回一个 Map 表格构建器实例,支持链式调用和灵活配置。 + * 相比直接使用 createMapSheet 方法,构建器方式提供了更灵活的配置方式。 + * + * 支持动态添加字段,包括: + * - addField(field) - 添加单个字段 + * - addFields(fieldList) - 批量添加字段列表 + * - addFields(field1, field2, ...) - 批量添加字段(可变参数) + * - fields(fieldList) - 直接设置所有字段(覆盖现有) + * + * 使用示例: + *
+     * // 动态添加字段
+     * List<MapFieldDefinition> dynamicFields = getDynamicFields();
+     *
+     * String sheetId = FsHelper.createMapSheetBuilder("表格名称", spreadsheetToken)
+     *     .titleRow(2)
+     *     .headLine(3)
+     *     .headStyle("#ffffff", "#000000")
+     *     .isText(true)
+     *     .addFields(dynamicFields)  // 批量添加
+     *     .build();
+     *
+     * // 或者使用可变参数
+     * String sheetId = FsHelper.createMapSheetBuilder("表格名称", spreadsheetToken)
+     *     .addFields(
+     *         MapFieldDefinition.text("字段1", 0),
+     *         MapFieldDefinition.singleSelect("字段2", 1, "选项1", "选项2")
+     *     )
+     *     .build();
+     *
+     * // 创建分组表格
+     * String sheetId = FsHelper.createMapSheetBuilder("分组表格", spreadsheetToken)
+     *     .addFields(dynamicFields)
+     *     .groupFields("分组A", "分组B")
+     *     .build();
+     * 
+ * + * @param sheetName 工作表名称 + * @param spreadsheetToken 电子表格Token + * @return MapSheetBuilder实例,支持链式调用 + */ + public static MapSheetBuilder createMapSheetBuilder(String sheetName, String spreadsheetToken) { + return new MapSheetBuilder(sheetName, spreadsheetToken); + } + /** * 从飞书表格中读取数据 @@ -368,4 +446,83 @@ public class FsHelper { List> dataList) { return new MapWriteBuilder(sheetId, spreadsheetToken, dataList); } + + /** + * 从飞书表格读取数据并转换为 Map 格式 + * + * 直接从飞书表格读取数据并转换为 Map 列表,无需定义实体类和注解。 + * 适用于动态字段、临时数据读取等场景。 + * + * 返回的Map中包含两个特殊字段: + * - _uniqueId: 根据唯一键计算的唯一标识 + * - _rowNumber: 数据所在的行号(从1开始) + * + * 注意: + * - 如果某行数据的所有业务字段值都为null或空字符串,该行数据将被自动过滤,不会包含在返回结果中 + * + * 使用示例: + *
+     * MapTableConfig config = MapTableConfig.builder()
+     *     .titleRow(2)
+     *     .headLine(3)
+     *     .addUniKeyName("字段名1")
+     *     .build();
+     *
+     * List<Map<String, Object>> dataList = FsHelper.readMap(sheetId, spreadsheetToken, config);
+     * for (Map<String, Object> data : dataList) {
+     *     String value1 = (String) data.get("字段名1");
+     *     String value2 = (String) data.get("字段名2");
+     *     String uniqueId = (String) data.get("_uniqueId");
+     *     Integer rowNumber = (Integer) data.get("_rowNumber");
+     * }
+     * 
+ * + * @param sheetId 工作表ID + * @param spreadsheetToken 电子表格Token + * @param config 表格配置,包含标题行、数据起始行、唯一键等配置信息 + * @return Map数据列表,每个Map的key为字段名,value为字段值 + */ + public static List> readMap(String sheetId, String spreadsheetToken, MapTableConfig config) { + return new MapReadBuilder(sheetId, spreadsheetToken) + .config(config) + .build(); + } + + /** + * 创建 Map 数据读取构建器 + * + * 返回一个 Map 数据读取构建器实例,支持链式调用和灵活配置。 + * 相比直接使用 readMap 方法,构建器方式提供了更灵活的配置方式。 + * + * 返回的Map中包含两个特殊字段: + * - _uniqueId: 根据唯一键计算的唯一标识 + * - _rowNumber: 数据所在的行号(从1开始) + * + * 注意: + * - 如果某行数据的所有业务字段值都为null或空字符串,该行数据将被自动过滤,不会包含在返回结果中 + * - 分组读取时,如果某个分组下的某行数据所有字段值都为null或空字符串,该行数据也会被自动过滤 + * + * 使用示例: + *
+     * // 基础读取
+     * List<Map<String, Object>> dataList = FsHelper.readMapBuilder(sheetId, spreadsheetToken)
+     *     .titleRow(2)
+     *     .headLine(3)
+     *     .addUniKeyName("字段名1")
+     *     .build();
+     *
+     * // 分组读取
+     * Map<String, List<Map<String, Object>>> groupedData = FsHelper.readMapBuilder(sheetId, spreadsheetToken)
+     *     .titleRow(2)
+     *     .headLine(3)
+     *     .groupBuild();
+     * 
+ * + * @param sheetId 工作表ID + * @param spreadsheetToken 电子表格Token + * @return MapReadBuilder实例,支持链式调用 + */ + public static MapReadBuilder readMapBuilder(String sheetId, String spreadsheetToken) { + return new MapReadBuilder(sheetId, spreadsheetToken); + } } \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/builder/MapReadBuilder.java b/src/main/java/cn/isliu/core/builder/MapReadBuilder.java new file mode 100644 index 0000000..09c1282 --- /dev/null +++ b/src/main/java/cn/isliu/core/builder/MapReadBuilder.java @@ -0,0 +1,434 @@ +package cn.isliu.core.builder; + +import cn.isliu.core.*; +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.client.FsClient; +import cn.isliu.core.config.MapTableConfig; +import cn.isliu.core.logging.FsLogger; +import cn.isliu.core.utils.FsApiUtil; +import cn.isliu.core.utils.MapDataUtil; + +import java.util.*; +import java.util.stream.Collectors; + +import static cn.isliu.core.utils.FsTableUtil.*; + +/** + * Map 数据读取构建器 + * + * 提供链式调用方式从飞书表格读取数据并转换为 Map 格式 + * + * @author Ls + * @since 2025-10-16 + */ +public class MapReadBuilder { + + private final String sheetId; + private final String spreadsheetToken; + private MapTableConfig config; + + /** + * 构造函数 + * + * @param sheetId 工作表ID + * @param spreadsheetToken 电子表格Token + */ + public MapReadBuilder(String sheetId, String spreadsheetToken) { + this.sheetId = sheetId; + this.spreadsheetToken = spreadsheetToken; + this.config = MapTableConfig.createDefault(); + } + + /** + * 设置表格配置 + * + * @param config Map表格配置 + * @return MapReadBuilder实例 + */ + public MapReadBuilder config(MapTableConfig config) { + this.config = config; + return this; + } + + /** + * 设置标题行 + * + * @param titleRow 标题行行号 + * @return MapReadBuilder实例 + */ + public MapReadBuilder titleRow(int titleRow) { + this.config.setTitleRow(titleRow); + return this; + } + + /** + * 设置数据起始行 + * + * @param headLine 数据起始行号 + * @return MapReadBuilder实例 + */ + public MapReadBuilder headLine(int headLine) { + this.config.setHeadLine(headLine); + return this; + } + + /** + * 设置唯一键字段 + * + * @param uniKeyNames 唯一键字段名集合 + * @return MapReadBuilder实例 + */ + public MapReadBuilder uniKeyNames(Set uniKeyNames) { + this.config.setUniKeyNames(uniKeyNames); + return this; + } + + /** + * 添加唯一键字段 + * + * @param uniKeyName 唯一键字段名 + * @return MapReadBuilder实例 + */ + public MapReadBuilder addUniKeyName(String uniKeyName) { + this.config.addUniKeyName(uniKeyName); + return this; + } + + /** + * 执行数据读取 + * + * 注意: + * - 如果某行数据的所有业务字段值都为null或空字符串,该行数据将被自动过滤 + * - 返回的Map中包含 _uniqueId 和 _rowNumber 两个特殊字段 + * + * @return Map数据列表 + */ + public List> build() { + FeishuClient client = FsClient.getInstance().getClient(); + Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); + + // 读取字段位置映射 + Map titlePostionMap = readFieldsPositionMap(sheet, client); + config.setFieldsPositionMap(titlePostionMap); + + // 读取表格数据并转换为Map格式 + List> dataList = readTableData(sheet, client, titlePostionMap); + + FsLogger.info("【Map读取】成功读取 {} 条数据", dataList.size()); + return dataList; + } + + /** + * 执行分组数据读取 + * + * 注意: + * - 如果某个分组下某行数据的所有字段值都为null或空字符串,该行数据将被自动过滤 + * - 返回的Map中包含 _uniqueId 和 _rowNumber 两个特殊字段 + * + * @return 按分组字段组织的Map数据 + */ + public Map>> groupBuild() { + FeishuClient client = FsClient.getInstance().getClient(); + Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); + + // 读取字段位置映射 + Map titlePostionMap = readFieldsPositionMap(sheet, client); + config.setFieldsPositionMap(titlePostionMap); + + // 读取分组表格数据 + Map>> groupedData = readGroupedTableData(sheet, client, titlePostionMap); + + int totalCount = groupedData.values().stream().mapToInt(List::size).sum(); + FsLogger.info("【Map读取】成功读取 {} 个分组,共 {} 条数据", groupedData.size(), totalCount); + return groupedData; + } + + /** + * 读取字段位置映射 + */ + private Map readFieldsPositionMap(Sheet sheet, FeishuClient client) { + int titleRow = config.getTitleRow(); + int colCount = sheet.getGridProperties().getColumnCount(); + + // 读取标题行数据 + ValuesBatch valuesBatch = FsApiUtil.getSheetData( + sheet.getSheetId(), spreadsheetToken, + "A" + titleRow, + getColumnName(colCount - 1) + titleRow, + client + ); + + Map fieldsPositionMap = new HashMap<>(); + + if (valuesBatch != null && valuesBatch.getValueRanges() != null) { + for (ValueRange valueRange : valuesBatch.getValueRanges()) { + if (valueRange.getValues() != null && !valueRange.getValues().isEmpty()) { + List titleRowValues = valueRange.getValues().get(0); + for (int i = 0; i < titleRowValues.size(); i++) { + Object value = titleRowValues.get(i); + if (value != null) { + String fieldName = value.toString(); + String columnPosition = getColumnName(i); + fieldsPositionMap.put(fieldName, columnPosition); + } + } + } + } + } + + return fieldsPositionMap; + } + + /** + * 读取表格数据并转换为Map格式 + */ + private List> readTableData(Sheet sheet, FeishuClient client, Map titlePostionMap) { + int headLine = config.getHeadLine(); + int titleRow = config.getTitleRow(); + int totalRow = sheet.getGridProperties().getRowCount(); + int colCount = sheet.getGridProperties().getColumnCount(); + int startOffset = 1; + + // 批量读取数据 + int rowCountPerBatch = Math.min(totalRow, 100); + int actualRows = Math.max(0, totalRow - startOffset); + int batchCount = (actualRows + rowCountPerBatch - 1) / rowCountPerBatch; + + List> values = new LinkedList<>(); + for (int i = 0; i < batchCount; i++) { + int startRowIndex = startOffset + i * rowCountPerBatch; + int endRowIndex = Math.min(startRowIndex + rowCountPerBatch - 1, totalRow - 1); + + ValuesBatch valuesBatch = FsApiUtil.getSheetData( + sheet.getSheetId(), spreadsheetToken, + "A" + startRowIndex, + getColumnName(colCount - 1) + endRowIndex, + client + ); + + if (valuesBatch != null && valuesBatch.getValueRanges() != null) { + for (ValueRange valueRange : valuesBatch.getValueRanges()) { + if (valueRange.getValues() != null) { + values.addAll(valueRange.getValues()); + } + } + } + } + + // 处理表格数据 + TableData tableData = processSheetData(sheet, values); + List fsTableDataList = getFsTableData(tableData, new ArrayList<>()); + + // 获取标题映射 + Map titleMap = new HashMap<>(); + fsTableDataList.stream() + .filter(d -> d.getRow() == (titleRow - 1)) + .findFirst() + .ifPresent(d -> { + Map map = (Map) d.getData(); + titleMap.putAll(map); + }); + + // 转换为带字段名的Map数据 + return fsTableDataList.stream() + .filter(fsTableData -> fsTableData.getRow() >= headLine) + .map(item -> { + Map resultMap = new HashMap<>(); + Map map = (Map) item.getData(); + + map.forEach((k, v) -> { + String title = titleMap.get(k); + if (title != null) { + resultMap.put(title, v); + } + }); + + // 计算并设置唯一ID + String uniqueId = MapDataUtil.calculateUniqueId(resultMap, config); + if (uniqueId != null) { + resultMap.put("_uniqueId", uniqueId); + } + + // 设置行号 + resultMap.put("_rowNumber", item.getRow() + 1); + + return resultMap; + }) + .filter(map -> { + // 过滤条件1:Map不能为空 + if (map.isEmpty()) { + return false; + } + + // 过滤条件2:除了 _uniqueId 和 _rowNumber 外还有其他数据 + if (map.size() <= 2) { + return false; + } + + // 过滤条件3:检查是否所有业务字段的值都为null或空字符串 + boolean hasNonNullValue = false; + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // 跳过特殊字段 + if (key.equals("_uniqueId") || key.equals("_rowNumber")) { + continue; + } + + // 检查是否有非null且非空字符串的值 + if (value != null && !(value instanceof String && ((String) value).isEmpty())) { + hasNonNullValue = true; + break; + } + } + + return hasNonNullValue; + }) + .collect(Collectors.toList()); + } + + /** + * 读取分组表格数据 + */ + private Map>> readGroupedTableData(Sheet sheet, FeishuClient client, Map titlePostionMap) { + int headLine = config.getHeadLine(); + int titleRow = config.getTitleRow(); + int totalRow = sheet.getGridProperties().getRowCount(); + int colCount = sheet.getGridProperties().getColumnCount(); + int startOffset = 1; + + // 批量读取数据 + int rowCountPerBatch = Math.min(totalRow, 100); + int actualRows = Math.max(0, totalRow - startOffset); + int batchCount = (actualRows + rowCountPerBatch - 1) / rowCountPerBatch; + + List> values = new LinkedList<>(); + for (int i = 0; i < batchCount; i++) { + int startRowIndex = startOffset + i * rowCountPerBatch; + int endRowIndex = Math.min(startRowIndex + rowCountPerBatch - 1, totalRow - 1); + + ValuesBatch valuesBatch = FsApiUtil.getSheetData( + sheet.getSheetId(), spreadsheetToken, + "A" + startRowIndex, + getColumnName(colCount - 1) + endRowIndex, + client + ); + + if (valuesBatch != null && valuesBatch.getValueRanges() != null) { + for (ValueRange valueRange : valuesBatch.getValueRanges()) { + if (valueRange.getValues() != null) { + values.addAll(valueRange.getValues()); + } + } + } + } + + // 处理表格数据 + TableData tableData = processSheetData(sheet, values); + List fsTableDataList = getFsTableData(tableData, new ArrayList<>()); + + // 获取分组行和标题行 + Map categoryMap = new HashMap<>(); + Map> categoryPositionMap = new HashMap<>(); + + // 读取分组行(titleRow - 1) + fsTableDataList.stream() + .filter(d -> d.getRow() == (titleRow - 2)) + .findFirst() + .ifPresent(d -> { + Map map = (Map) d.getData(); + map.forEach((k, v) -> { + if (v != null && !v.isEmpty()) { + categoryMap.put(k, v); + categoryPositionMap.computeIfAbsent(v, k1 -> new ArrayList<>()).add(k); + } + }); + }); + + // 读取标题行 + Map titleMap = new HashMap<>(); + fsTableDataList.stream() + .filter(d -> d.getRow() == (titleRow - 1)) + .findFirst() + .ifPresent(d -> { + Map map = (Map) d.getData(); + map.forEach((k, v) -> { + if (v != null && !v.isEmpty()) { + String category = categoryMap.get(k); + if (category != null && !category.isEmpty()) { + titleMap.put(k, v); + } else { + titleMap.put(k, v); + } + } + }); + }); + + // 按分组组织数据 + Map>> groupedResult = new LinkedHashMap<>(); + + for (Map.Entry> entry : categoryPositionMap.entrySet()) { + String groupName = entry.getKey(); + List positions = entry.getValue(); + + List> groupData = fsTableDataList.stream() + .filter(fsTableData -> fsTableData.getRow() >= headLine) + .map(item -> { + Map resultMap = new HashMap<>(); + Map map = (Map) item.getData(); + + // 只提取该分组的字段 + positions.forEach(pos -> { + String title = titleMap.get(pos); + if (title != null && map.containsKey(pos)) { + resultMap.put(title, map.get(pos)); + } + }); + + // 计算并设置唯一ID + String uniqueId = MapDataUtil.calculateUniqueId(resultMap, config); + if (uniqueId != null) { + resultMap.put("_uniqueId", uniqueId); + } + + // 设置行号 + resultMap.put("_rowNumber", item.getRow() + 1); + + return resultMap; + }) + .filter(map -> { + // 过滤条件1:除了 _uniqueId 和 _rowNumber 外还有其他数据 + if (map.size() <= 2) { + return false; + } + + // 过滤条件2:检查是否所有业务字段的值都为null + boolean hasNonNullValue = false; + for (Map.Entry nullEntry : map.entrySet()) { + String key = nullEntry.getKey(); + Object value = nullEntry.getValue(); + + // 跳过特殊字段 + if (key.equals("_uniqueId") || key.equals("_rowNumber")) { + continue; + } + + // 检查是否有非null且非空字符串的值 + if (value != null && !(value instanceof String && ((String) value).isEmpty())) { + hasNonNullValue = true; + break; + } + } + + return hasNonNullValue; + }) + .collect(Collectors.toList()); + + groupedResult.put(groupName, groupData); + } + + return groupedResult; + } +} + diff --git a/src/main/java/cn/isliu/core/builder/MapSheetBuilder.java b/src/main/java/cn/isliu/core/builder/MapSheetBuilder.java new file mode 100644 index 0000000..d113b62 --- /dev/null +++ b/src/main/java/cn/isliu/core/builder/MapSheetBuilder.java @@ -0,0 +1,525 @@ +package cn.isliu.core.builder; + +import cn.isliu.core.annotation.TableConf; +import cn.isliu.core.annotation.TableProperty; +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.client.FsClient; +import cn.isliu.core.config.MapFieldDefinition; +import cn.isliu.core.config.MapSheetConfig; +import cn.isliu.core.converters.FieldValueProcess; +import cn.isliu.core.converters.OptionsValueProcess; +import cn.isliu.core.enums.BaseEnum; +import cn.isliu.core.enums.TypeEnum; +import cn.isliu.core.pojo.FieldProperty; +import cn.isliu.core.service.CustomCellService; +import cn.isliu.core.utils.FsApiUtil; +import cn.isliu.core.utils.FsTableUtil; +import cn.isliu.core.utils.MapOptionsUtil; + +import java.lang.annotation.Annotation; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Map 表格构建器 + * + * 提供链式调用方式创建飞书表格,使用配置对象而不是注解 + * + * @author Ls + * @since 2025-10-16 + */ +public class MapSheetBuilder { + + private final String sheetName; + private final String spreadsheetToken; + private MapSheetConfig config; + + /** + * 构造函数 + * + * @param sheetName 工作表名称 + * @param spreadsheetToken 电子表格Token + */ + public MapSheetBuilder(String sheetName, String spreadsheetToken) { + this.sheetName = sheetName; + this.spreadsheetToken = spreadsheetToken; + this.config = MapSheetConfig.createDefault(); + } + + /** + * 设置表格配置 + * + * @param config Map表格配置 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder config(MapSheetConfig config) { + this.config = config; + return this; + } + + /** + * 设置标题行 + * + * @param titleRow 标题行行号 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder titleRow(int titleRow) { + this.config.setTitleRow(titleRow); + return this; + } + + /** + * 设置数据起始行 + * + * @param headLine 数据起始行号 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder headLine(int headLine) { + this.config.setHeadLine(headLine); + return this; + } + + /** + * 设置唯一键字段 + * + * @param uniKeyNames 唯一键字段名集合 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder uniKeyNames(Set uniKeyNames) { + this.config.setUniKeyNames(uniKeyNames); + return this; + } + + /** + * 添加唯一键字段 + * + * @param uniKeyName 唯一键字段名 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder addUniKeyName(String uniKeyName) { + this.config.addUniKeyName(uniKeyName); + return this; + } + + /** + * 设置字段列表 + * + * @param fields 字段定义列表 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder fields(List fields) { + this.config.setFields(new ArrayList<>(fields)); + return this; + } + + /** + * 添加单个字段 + * + * @param field 字段定义 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder addField(MapFieldDefinition field) { + this.config.addField(field); + return this; + } + + /** + * 批量添加字段 + * + * @param fields 字段定义列表 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder addFields(List fields) { + this.config.addFields(fields); + return this; + } + + /** + * 批量添加字段(可变参数) + * + * @param fields 字段定义可变参数 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder addFields(MapFieldDefinition... fields) { + this.config.addFields(fields); + return this; + } + + /** + * 设置表头样式 + * + * @param fontColor 字体颜色 + * @param backColor 背景颜色 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder headStyle(String fontColor, String backColor) { + this.config.setHeadFontColor(fontColor); + this.config.setHeadBackColor(backColor); + return this; + } + + /** + * 设置是否为纯文本格式 + * + * @param isText 是否设置为纯文本 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder isText(boolean isText) { + this.config.setText(isText); + return this; + } + + /** + * 设置是否启用字段描述 + * + * @param enableDesc 是否启用 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder enableDesc(boolean enableDesc) { + this.config.setEnableDesc(enableDesc); + return this; + } + + /** + * 设置分组字段 + * + * @param groupFields 分组字段可变参数 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder groupFields(String... groupFields) { + this.config.setGroupFields(Arrays.asList(groupFields)); + return this; + } + + /** + * 添加自定义属性 + * + * @param key 属性键 + * @param value 属性值 + * @return MapSheetBuilder实例 + */ + public MapSheetBuilder addCustomProperty(String key, Object value) { + this.config.addCustomProperty(key, value); + return this; + } + + /** + * 构建表格并返回工作表ID + * + * @return 创建成功返回工作表ID + */ + public String build() { + // 检查字段列表 + if (config.getFields().isEmpty()) { + throw new IllegalArgumentException("字段定义列表不能为空"); + } + + // 判断是否为分组表格 + if (config.getGroupFields() != null && !config.getGroupFields().isEmpty()) { + return buildGroupSheet(); + } else { + return buildNormalSheet(); + } + } + + /** + * 构建普通表格 + */ + private String buildNormalSheet() { + // 转换字段定义为 FieldProperty + Map fieldsMap = convertToFieldsMap(config.getFields()); + + // 生成表头 + List headers = config.getFields().stream() + .sorted(Comparator.comparingInt(MapFieldDefinition::getOrder)) + .map(MapFieldDefinition::getFieldName) + .collect(Collectors.toList()); + + // 创建 TableConf + TableConf tableConf = createTableConf(); + + // 创建飞书客户端 + FeishuClient client = FsClient.getInstance().getClient(); + + // 1、创建sheet + String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken); + + // 2、添加表头数据 + Map fieldDescriptions = buildFieldDescriptions(); + FsApiUtil.putValues(spreadsheetToken, + FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions), + client); + + // 3、设置单元格为文本格式 + if (config.isText()) { + String column = FsTableUtil.getColumnNameByNuNumber(headers.size()); + FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken); + } + + // 4、设置表格样式 + FsApiUtil.setTableStyle( + FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), + client, spreadsheetToken); + + // 5、合并单元格 + List mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap); + if (!mergeCell.isEmpty()) { + mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken)); + } + + // 6、设置表格下拉 + try { + // 准备自定义属性,包含字段的 options 配置 + Map customProps = prepareCustomProperties(fieldsMap); + FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, + config.isEnableDesc(), customProps); + } catch (Exception e) { + Logger.getLogger(MapSheetBuilder.class.getName()).log(Level.SEVERE, + "【Map表格构建器】设置表格下拉异常!sheetId:" + sheetId + ", 错误信息:{}", e.getMessage()); + } + + return sheetId; + } + + /** + * 构建分组表格 + */ + private String buildGroupSheet() { + // 转换字段定义为 FieldProperty + Map fieldsMap = convertToFieldsMap(config.getFields()); + + // 生成表头 + List headers = config.getFields().stream() + .sorted(Comparator.comparingInt(MapFieldDefinition::getOrder)) + .map(MapFieldDefinition::getFieldName) + .collect(Collectors.toList()); + + // 创建 TableConf + TableConf tableConf = createTableConf(); + + // 创建飞书客户端 + FeishuClient client = FsClient.getInstance().getClient(); + + // 1、创建sheet + String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken); + + // 2、添加表头数据(分组模式) + List groupFieldList = config.getGroupFields(); + List headerList = FsTableUtil.getGroupHeaders(groupFieldList, headers); + Map fieldDescriptions = buildFieldDescriptions(); + FsApiUtil.putValues(spreadsheetToken, + FsTableUtil.getHeadTemplateBuilder(sheetId, headers, headerList, fieldsMap, + tableConf, fieldDescriptions, groupFieldList), + client); + + // 3、设置单元格为文本格式 + if (config.isText()) { + String column = FsTableUtil.getColumnNameByNuNumber(headerList.size()); + FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken); + } + + // 4、设置表格样式(分组模式) + Map positions = FsTableUtil.calculateGroupPositions(headers, groupFieldList); + positions.forEach((key, value) -> + FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, value, tableConf), + client, spreadsheetToken)); + + // 5、合并单元格 + List mergeCell = FsTableUtil.getMergeCell(sheetId, positions.values()); + if (!mergeCell.isEmpty()) { + mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken)); + } + + // 6、设置表格下拉 + try { + String[] headerWithColumnIdentifiers = FsTableUtil.generateHeaderWithColumnIdentifiers(headers, groupFieldList); + // 准备自定义属性,包含字段的 options 配置 + Map customProps = prepareCustomProperties(fieldsMap); + FsTableUtil.setTableOptions(spreadsheetToken, headerWithColumnIdentifiers, fieldsMap, + sheetId, config.isEnableDesc(), customProps); + } catch (Exception e) { + Logger.getLogger(MapSheetBuilder.class.getName()).log(Level.SEVERE, + "【Map表格构建器】设置表格下拉异常!sheetId:" + sheetId + ", 错误信息:{}", e.getMessage()); + } + + return sheetId; + } + + /** + * 将 MapFieldDefinition 列表转换为 FieldProperty Map + */ + private Map convertToFieldsMap(List fields) { + Map fieldsMap = new LinkedHashMap<>(); + + for (MapFieldDefinition field : fields) { + TableProperty tableProperty = createTableProperty(field); + FieldProperty fieldProperty = new FieldProperty(field.getFieldName(), tableProperty); + fieldsMap.put(field.getFieldName(), fieldProperty); + } + + return fieldsMap; + } + + /** + * 根据 MapFieldDefinition 创建 TableProperty 注解实例 + */ + private TableProperty createTableProperty(MapFieldDefinition field) { + return new TableProperty() { + @Override + public Class annotationType() { + return TableProperty.class; + } + + @Override + public String[] value() { + return new String[]{field.getFieldName()}; + } + + @Override + public String desc() { + return field.getDescription() != null ? field.getDescription() : ""; + } + + @Override + public String field() { + return field.getFieldName(); + } + + @Override + public int order() { + return field.getOrder(); + } + + @Override + public TypeEnum type() { + return field.getType() != null ? field.getType() : TypeEnum.TEXT; + } + + @Override + public Class enumClass() { + // 优先级1:如果配置了 enumClass,直接返回 + if (field.getEnumClass() != null && field.getEnumClass() != BaseEnum.class) { + return field.getEnumClass(); + } + + // 优先级2:如果没有配置 enumClass 但配置了 options,创建动态枚举类 + // 注意:这里返回 BaseEnum.class,实际的 options 通过 optionsClass 处理 + return BaseEnum.class; + } + + @Override + public Class fieldFormatClass() { + return FieldValueProcess.class; + } + + @Override + public Class optionsClass() { + // 优先级1:如果配置了 optionsClass,直接返回 + if (field.getOptionsClass() != null && field.getOptionsClass() != OptionsValueProcess.class) { + return field.getOptionsClass(); + } + + // 优先级2:如果配置了 options 但没有 optionsClass,创建动态的处理类 + if (field.getOptions() != null && !field.getOptions().isEmpty()) { + return MapOptionsUtil.createDynamicOptionsClass(field.getOptions()); + } + + // 优先级3:返回默认值 + return OptionsValueProcess.class; + } + }; + } + + /** + * 创建 TableConf 注解实例 + */ + private TableConf createTableConf() { + return new TableConf() { + @Override + public Class annotationType() { + return TableConf.class; + } + + @Override + public String[] uniKeys() { + Set uniKeyNames = config.getUniKeyNames(); + return uniKeyNames != null ? uniKeyNames.toArray(new String[0]) : new String[0]; + } + + @Override + public int headLine() { + return config.getHeadLine(); + } + + @Override + public int titleRow() { + return config.getTitleRow(); + } + + @Override + public boolean enableCover() { + return config.isEnableCover(); + } + + @Override + public boolean isText() { + return config.isText(); + } + + @Override + public boolean enableDesc() { + return config.isEnableDesc(); + } + + @Override + public String headFontColor() { + return config.getHeadFontColor(); + } + + @Override + public String headBackColor() { + return config.getHeadBackColor(); + } + }; + } + + /** + * 构建字段描述映射 + */ + private Map buildFieldDescriptions() { + Map descriptions = new HashMap<>(); + for (MapFieldDefinition field : config.getFields()) { + if (field.getDescription() != null && !field.getDescription().isEmpty()) { + descriptions.put(field.getFieldName(), field.getDescription()); + } + } + return descriptions; + } + + /** + * 准备自定义属性 + * + * 将字段配置的 options 放入 customProperties,供 DynamicOptionsProcess 使用 + */ + private Map prepareCustomProperties(Map fieldsMap) { + Map customProps = new HashMap<>(); + + // 复制原有的自定义属性 + if (config.getCustomProperties() != null) { + customProps.putAll(config.getCustomProperties()); + } + + // 为每个配置了 options 的字段添加选项到 customProperties + for (MapFieldDefinition field : config.getFields()) { + if (field.getOptions() != null && !field.getOptions().isEmpty()) { + // 使用字段名作为 key 前缀,避免冲突 + customProps.put("_dynamicOptions_" + field.getFieldName(), field.getOptions()); + } + } + + return customProps; + } +} + diff --git a/src/main/java/cn/isliu/core/config/MapFieldDefinition.java b/src/main/java/cn/isliu/core/config/MapFieldDefinition.java new file mode 100644 index 0000000..217294e --- /dev/null +++ b/src/main/java/cn/isliu/core/config/MapFieldDefinition.java @@ -0,0 +1,522 @@ +package cn.isliu.core.config; + +import cn.isliu.core.converters.OptionsValueProcess; +import cn.isliu.core.enums.BaseEnum; +import cn.isliu.core.enums.TypeEnum; + +import java.util.*; + +/** + * Map方式字段定义类 + * + * 用于替代 @TableProperty 注解,定义单个字段的所有属性 + * + * @author Ls + * @since 2025-10-16 + */ +public class MapFieldDefinition { + + /** + * 字段名称(对应表格列名) + */ + private String fieldName; + + /** + * 字段描述 + */ + private String description; + + /** + * 字段排序顺序,数值越小越靠前 + */ + private int order; + + /** + * 字段类型 + */ + private TypeEnum type = TypeEnum.TEXT; + + /** + * 下拉选项列表(当type为单选/多选时使用) + */ + private List options; + + /** + * 选项映射(code -> label) + */ + private Map optionsMap; + + /** + * 枚举类(可选,用于从枚举类生成选项) + */ + private Class enumClass; + + /** + * 选项处理类(可选,用于自定义选项处理逻辑) + */ + private Class optionsClass; + + /** + * 是否必填 + */ + private boolean required = false; + + /** + * 默认值 + */ + private String defaultValue; + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + + public TypeEnum getType() { + return type; + } + + public void setType(TypeEnum type) { + this.type = type; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public Map getOptionsMap() { + return optionsMap; + } + + public void setOptionsMap(Map optionsMap) { + this.optionsMap = optionsMap; + } + + public Class getEnumClass() { + return enumClass; + } + + public void setEnumClass(Class enumClass) { + this.enumClass = enumClass; + } + + public Class getOptionsClass() { + return optionsClass; + } + + public void setOptionsClass(Class optionsClass) { + this.optionsClass = optionsClass; + } + + public boolean isRequired() { + return required; + } + + public void setRequired(boolean required) { + this.required = required; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + MapFieldDefinition that = (MapFieldDefinition) o; + return order == that.order && required == that.required && Objects.equals(fieldName, that.fieldName) && Objects.equals(description, that.description) && type == that.type && Objects.equals(options, that.options) && Objects.equals(optionsMap, that.optionsMap) && Objects.equals(enumClass, that.enumClass) && Objects.equals(optionsClass, that.optionsClass) && Objects.equals(defaultValue, that.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(fieldName, description, order, type, options, optionsMap, enumClass, optionsClass, required, defaultValue); + } + + @Override + public String toString() { + return "MapFieldDefinition{" + + "fieldName='" + fieldName + '\'' + + ", description='" + description + '\'' + + ", order=" + order + + ", type=" + type + + ", options=" + options + + ", optionsMap=" + optionsMap + + ", enumClass=" + enumClass + + ", optionsClass=" + optionsClass + + ", required=" + required + + ", defaultValue='" + defaultValue + '\'' + + '}'; + } + + /** + * 创建构建器 + * + * @return Builder实例 + */ + public static Builder builder() { + return new Builder(); + } + + /** + * 快速创建文本字段 + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition text(String fieldName, int order) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.TEXT) + .build(); + } + + /** + * 快速创建文本字段(带描述) + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param description 字段描述 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition text(String fieldName, int order, String description) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.TEXT) + .description(description) + .build(); + } + + /** + * 快速创建单选字段 + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param options 选项列表 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition singleSelect(String fieldName, int order, String... options) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.SINGLE_SELECT) + .options(Arrays.asList(options)) + .build(); + } + + /** + * 快速创建单选字段(带选项列表) + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param options 选项列表 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition singleSelect(String fieldName, int order, List options) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.SINGLE_SELECT) + .options(options) + .build(); + } + + /** + * 快速创建单选字段(带描述) + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param description 字段描述 + * @param options 选项列表 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition singleSelectWithDesc(String fieldName, int order, String description, List options) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.SINGLE_SELECT) + .description(description) + .options(options) + .build(); + } + + /** + * 快速创建多选字段 + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param options 选项列表 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition multiSelect(String fieldName, int order, String... options) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.MULTI_SELECT) + .options(Arrays.asList(options)) + .build(); + } + + /** + * 快速创建多选字段(带选项列表) + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param options 选项列表 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition multiSelect(String fieldName, int order, List options) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.MULTI_SELECT) + .options(options) + .build(); + } + + /** + * 快速创建多选字段(带描述) + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param description 字段描述 + * @param options 选项列表 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition multiSelectWithDesc(String fieldName, int order, String description, List options) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.MULTI_SELECT) + .description(description) + .options(options) + .build(); + } + + /** + * 快速创建单选字段(使用枚举类) + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param enumClass 枚举类 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition singleSelectWithEnum(String fieldName, int order, Class enumClass) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.SINGLE_SELECT) + .enumClass(enumClass) + .build(); + } + + /** + * 快速创建单选字段(使用枚举类,带描述) + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param description 字段描述 + * @param enumClass 枚举类 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition singleSelectWithEnum(String fieldName, int order, String description, Class enumClass) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.SINGLE_SELECT) + .description(description) + .enumClass(enumClass) + .build(); + } + + /** + * 快速创建多选字段(使用枚举类) + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param enumClass 枚举类 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition multiSelectWithEnum(String fieldName, int order, Class enumClass) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.MULTI_SELECT) + .enumClass(enumClass) + .build(); + } + + /** + * 快速创建多选字段(使用枚举类,带描述) + * + * @param fieldName 字段名称 + * @param order 排序顺序 + * @param description 字段描述 + * @param enumClass 枚举类 + * @return MapFieldDefinition实例 + */ + public static MapFieldDefinition multiSelectWithEnum(String fieldName, int order, String description, Class enumClass) { + return builder() + .fieldName(fieldName) + .order(order) + .type(TypeEnum.MULTI_SELECT) + .description(description) + .enumClass(enumClass) + .build(); + } + + /** + * 从 Map 批量创建字段定义 + * + * @param fieldMap key为字段名,value为字段类型字符串 + * @return 字段定义列表 + */ + public static List fromMap(Map fieldMap) { + List fields = new ArrayList<>(); + int order = 0; + for (Map.Entry entry : fieldMap.entrySet()) { + TypeEnum type = parseType(entry.getValue()); + fields.add(builder() + .fieldName(entry.getKey()) + .order(order++) + .type(type) + .build()); + } + return fields; + } + + /** + * 从字段名列表快速创建文本字段 + * + * @param fieldNames 字段名称列表 + * @return 字段定义列表 + */ + public static List fromFieldNames(List fieldNames) { + List fields = new ArrayList<>(); + for (int i = 0; i < fieldNames.size(); i++) { + fields.add(text(fieldNames.get(i), i)); + } + return fields; + } + + /** + * 解析类型字符串 + */ + private static TypeEnum parseType(String typeStr) { + if (typeStr == null || typeStr.isEmpty()) { + return TypeEnum.TEXT; + } + + try { + return TypeEnum.valueOf(typeStr.toUpperCase()); + } catch (IllegalArgumentException e) { + return TypeEnum.TEXT; + } + } + + /** + * 字段定义构建器 + */ + public static class Builder { + private final MapFieldDefinition definition = new MapFieldDefinition(); + + public Builder fieldName(String fieldName) { + definition.fieldName = fieldName; + return this; + } + + public Builder description(String description) { + definition.description = description; + return this; + } + + public Builder order(int order) { + definition.order = order; + return this; + } + + public Builder type(TypeEnum type) { + definition.type = type; + return this; + } + + public Builder options(List options) { + if (options == null || options.isEmpty()) { + return this; + } + definition.options = new ArrayList<>(options); + return this; + } + + public Builder options(String... options) { + if (options == null || options.length == 0) { + return this; + } + definition.options = Arrays.asList(options); + return this; + } + + public Builder optionsMap(Map optionsMap) { + definition.optionsMap = new HashMap<>(optionsMap); + return this; + } + + public Builder enumClass(Class enumClass) { + definition.enumClass = enumClass; + return this; + } + + public Builder optionsClass(Class optionsClass) { + definition.optionsClass = optionsClass; + return this; + } + + public Builder required(boolean required) { + definition.required = required; + return this; + } + + public Builder defaultValue(String defaultValue) { + definition.defaultValue = defaultValue; + return this; + } + + public MapFieldDefinition build() { + // 验证必填字段 + if (definition.fieldName == null || definition.fieldName.isEmpty()) { + throw new IllegalArgumentException("字段名称不能为空"); + } + + return definition; + } + } +} + diff --git a/src/main/java/cn/isliu/core/config/MapSheetConfig.java b/src/main/java/cn/isliu/core/config/MapSheetConfig.java new file mode 100644 index 0000000..167eba3 --- /dev/null +++ b/src/main/java/cn/isliu/core/config/MapSheetConfig.java @@ -0,0 +1,426 @@ +package cn.isliu.core.config; + +import java.util.*; + +/** + * Map方式表格创建配置类 + * + * 继承 MapTableConfig,专门用于创建飞书表格 + * 相比父类增加了字段定义、样式配置等创建表格所需的属性 + * + * @author Ls + * @since 2025-10-16 + */ +public class MapSheetConfig extends MapTableConfig { + + /** + * 字段定义列表 + */ + private List fields = new ArrayList<>(); + + /** + * 表头字体颜色(十六进制,如 #ffffff) + */ + private String headFontColor = "#ffffff"; + + /** + * 表头背景颜色(十六进制,如 #000000) + */ + private String headBackColor = "#000000"; + + /** + * 是否将单元格设置为纯文本格式 + */ + private boolean isText = false; + + /** + * 是否启用字段描述行 + */ + private boolean enableDesc = false; + + public List getFields() { + return fields; + } + + public void setFields(List fields) { + this.fields = fields; + } + + public String getHeadFontColor() { + return headFontColor; + } + + public void setHeadFontColor(String headFontColor) { + this.headFontColor = headFontColor; + } + + public String getHeadBackColor() { + return headBackColor; + } + + public void setHeadBackColor(String headBackColor) { + this.headBackColor = headBackColor; + } + + public boolean isText() { + return isText; + } + + public void setText(boolean text) { + isText = text; + } + + public boolean isEnableDesc() { + return enableDesc; + } + + public void setEnableDesc(boolean enableDesc) { + this.enableDesc = enableDesc; + } + + public List getGroupFields() { + return groupFields; + } + + public void setGroupFields(List groupFields) { + this.groupFields = groupFields; + } + + public Map getCustomProperties() { + return customProperties; + } + + public void setCustomProperties(Map customProperties) { + this.customProperties = customProperties; + } + + /** + * 分组字段列表(用于创建分组表格) + */ + private List groupFields = new ArrayList<>(); + + /** + * 自定义属性映射(用于传递额外配置) + */ + private Map customProperties = new HashMap<>(); + + /** + * 创建默认配置 + * + * @return 默认配置实例 + */ + public static MapSheetConfig createDefault() { + return new MapSheetConfig(); + } + + /** + * 创建表格配置构建器 + * + * @return 配置构建器实例 + */ + public static SheetBuilder sheetBuilder() { + return new SheetBuilder(); + } + + /** + * 添加单个字段 + * + * @param field 字段定义 + * @return MapSheetConfig实例,支持链式调用 + */ + public MapSheetConfig addField(MapFieldDefinition field) { + this.fields.add(field); + return this; + } + + /** + * 批量添加字段 + * + * @param fields 字段定义列表 + * @return MapSheetConfig实例,支持链式调用 + */ + public MapSheetConfig addFields(List fields) { + this.fields.addAll(fields); + return this; + } + + /** + * 批量添加字段(可变参数) + * + * @param fields 字段定义可变参数 + * @return MapSheetConfig实例,支持链式调用 + */ + public MapSheetConfig addFields(MapFieldDefinition... fields) { + this.fields.addAll(Arrays.asList(fields)); + return this; + } + + /** + * 添加分组字段 + * + * @param groupField 分组字段名 + * @return MapSheetConfig实例,支持链式调用 + */ + public MapSheetConfig addGroupField(String groupField) { + this.groupFields.add(groupField); + return this; + } + + /** + * 添加自定义属性 + * + * @param key 属性键 + * @param value 属性值 + * @return MapSheetConfig实例,支持链式调用 + */ + public MapSheetConfig addCustomProperty(String key, Object value) { + this.customProperties.put(key, value); + return this; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + MapSheetConfig that = (MapSheetConfig) o; + return isText == that.isText && enableDesc == that.enableDesc && Objects.equals(fields, that.fields) && Objects.equals(headFontColor, that.headFontColor) && Objects.equals(headBackColor, that.headBackColor) && Objects.equals(groupFields, that.groupFields) && Objects.equals(customProperties, that.customProperties); + } + + @Override + public int hashCode() { + return Objects.hash(fields, headFontColor, headBackColor, isText, enableDesc, groupFields, customProperties); + } + + /** + * 表格配置构建器 + */ + public static class SheetBuilder { + private final MapSheetConfig config = new MapSheetConfig(); + + /** + * 设置标题行行号 + * + * @param titleRow 标题行行号 + * @return SheetBuilder实例 + */ + public SheetBuilder titleRow(int titleRow) { + config.setTitleRow(titleRow); + return this; + } + + /** + * 设置数据起始行行号 + * + * @param headLine 数据起始行行号 + * @return SheetBuilder实例 + */ + public SheetBuilder headLine(int headLine) { + config.setHeadLine(headLine); + return this; + } + + /** + * 设置唯一键字段名集合 + * + * @param uniKeyNames 唯一键字段名集合 + * @return SheetBuilder实例 + */ + public SheetBuilder uniKeyNames(Set uniKeyNames) { + config.setUniKeyNames(uniKeyNames); + return this; + } + + /** + * 添加唯一键字段名 + * + * @param uniKeyName 唯一键字段名 + * @return SheetBuilder实例 + */ + public SheetBuilder addUniKeyName(String uniKeyName) { + config.addUniKeyName(uniKeyName); + return this; + } + + /** + * 设置是否覆盖已存在数据 + * + * @param enableCover true表示覆盖,false表示不覆盖 + * @return SheetBuilder实例 + */ + public SheetBuilder enableCover(boolean enableCover) { + config.setEnableCover(enableCover); + return this; + } + + /** + * 设置字段定义列表 + * + * @param fields 字段定义列表 + * @return SheetBuilder实例 + */ + public SheetBuilder fields(List fields) { + config.fields = new ArrayList<>(fields); + return this; + } + + /** + * 添加单个字段 + * + * @param field 字段定义 + * @return SheetBuilder实例 + */ + public SheetBuilder addField(MapFieldDefinition field) { + config.fields.add(field); + return this; + } + + /** + * 批量添加字段 + * + * @param fields 字段定义列表 + * @return SheetBuilder实例 + */ + public SheetBuilder addFields(List fields) { + config.fields.addAll(fields); + return this; + } + + /** + * 批量添加字段(可变参数) + * + * @param fields 字段定义可变参数 + * @return SheetBuilder实例 + */ + public SheetBuilder addFields(MapFieldDefinition... fields) { + config.fields.addAll(Arrays.asList(fields)); + return this; + } + + /** + * 设置表头字体颜色 + * + * @param headFontColor 表头字体颜色(十六进制) + * @return SheetBuilder实例 + */ + public SheetBuilder headFontColor(String headFontColor) { + config.headFontColor = headFontColor; + return this; + } + + /** + * 设置表头背景颜色 + * + * @param headBackColor 表头背景颜色(十六进制) + * @return SheetBuilder实例 + */ + public SheetBuilder headBackColor(String headBackColor) { + config.headBackColor = headBackColor; + return this; + } + + /** + * 设置表头样式 + * + * @param fontColor 字体颜色 + * @param backColor 背景颜色 + * @return SheetBuilder实例 + */ + public SheetBuilder headStyle(String fontColor, String backColor) { + config.headFontColor = fontColor; + config.headBackColor = backColor; + return this; + } + + /** + * 设置是否将单元格设置为纯文本 + * + * @param isText 是否设置为纯文本 + * @return SheetBuilder实例 + */ + public SheetBuilder isText(boolean isText) { + config.isText = isText; + return this; + } + + /** + * 设置是否启用字段描述行 + * + * @param enableDesc 是否启用 + * @return SheetBuilder实例 + */ + public SheetBuilder enableDesc(boolean enableDesc) { + config.enableDesc = enableDesc; + return this; + } + + /** + * 设置分组字段列表 + * + * @param groupFields 分组字段列表 + * @return SheetBuilder实例 + */ + public SheetBuilder groupFields(List groupFields) { + config.groupFields = new ArrayList<>(groupFields); + return this; + } + + /** + * 设置分组字段(可变参数) + * + * @param groupFields 分组字段可变参数 + * @return SheetBuilder实例 + */ + public SheetBuilder groupFields(String... groupFields) { + config.groupFields = Arrays.asList(groupFields); + return this; + } + + /** + * 添加分组字段 + * + * @param groupField 分组字段名 + * @return SheetBuilder实例 + */ + public SheetBuilder addGroupField(String groupField) { + config.groupFields.add(groupField); + return this; + } + + /** + * 设置自定义属性映射 + * + * @param customProperties 自定义属性映射 + * @return SheetBuilder实例 + */ + public SheetBuilder customProperties(Map customProperties) { + config.customProperties = new HashMap<>(customProperties); + return this; + } + + /** + * 添加自定义属性 + * + * @param key 属性键 + * @param value 属性值 + * @return SheetBuilder实例 + */ + public SheetBuilder addCustomProperty(String key, Object value) { + config.customProperties.put(key, value); + return this; + } + + /** + * 构建配置对象 + * + * @return MapSheetConfig实例 + */ + public MapSheetConfig build() { + // 验证必填字段 + if (config.fields.isEmpty()) { + throw new IllegalArgumentException("字段定义列表不能为空"); + } + + return config; + } + } +} + diff --git a/src/main/java/cn/isliu/core/utils/MapOptionsUtil.java b/src/main/java/cn/isliu/core/utils/MapOptionsUtil.java new file mode 100644 index 0000000..a9695ee --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/MapOptionsUtil.java @@ -0,0 +1,122 @@ +package cn.isliu.core.utils; + +import cn.isliu.core.converters.OptionsValueProcess; +import cn.isliu.core.enums.BaseEnum; +import cn.isliu.core.pojo.FieldProperty; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Map 选项工具类 + * + * 提供选项提取、转换和处理的工具方法 + * + * @author Ls + * @since 2025-10-16 + */ +public class MapOptionsUtil { + + /** + * 从枚举类提取选项列表 + * + * 提取枚举类中所有枚举常量的描述(desc)作为下拉选项 + * + * @param enumClass 枚举类 + * @return 选项列表(枚举的desc值) + */ + public static List extractOptionsFromEnum(Class enumClass) { + if (enumClass == null || enumClass == BaseEnum.class) { + return new ArrayList<>(); + } + + return Arrays.stream(enumClass.getEnumConstants()) + .map(BaseEnum::getDesc) + .collect(Collectors.toList()); + } + + /** + * 从枚举类提取选项映射 + * + * 提取枚举类中所有枚举常量的 code -> desc 映射 + * + * @param enumClass 枚举类 + * @return 选项映射(code -> desc) + */ + public static Map extractOptionsMapFromEnum(Class enumClass) { + if (enumClass == null || enumClass == BaseEnum.class) { + return new HashMap<>(); + } + + Map optionsMap = new LinkedHashMap<>(); + Arrays.stream(enumClass.getEnumConstants()) + .forEach(e -> optionsMap.put(e.getCode(), e.getDesc())); + return optionsMap; + } + + /** + * 创建动态的 OptionsValueProcess 类 + * + * 用于处理直接配置的 options 列表,将其包装为 OptionsValueProcess + * + * @param options 选项列表 + * @return 动态创建的 OptionsValueProcess 类 + */ + public static Class createDynamicOptionsClass(final List options) { + if (options == null || options.isEmpty()) { + return OptionsValueProcess.class; + } + + // 返回一个匿名内部类 + // 注意:由于 Java 的限制,我们不能真正动态创建类 + // 这里返回一个包装器类,在 MapSheetBuilder 中特殊处理 + return DynamicOptionsProcess.class; + } + + /** + * 动态选项处理类(内部使用) + * + * 从 customProperties 中读取预先配置的选项列表 + * 在 MapSheetBuilder 中,会将字段的 options 放入 customProperties + */ + public static class DynamicOptionsProcess implements OptionsValueProcess { + @Override + public List process(Object value) { + if (value instanceof Map) { + Map properties = (Map) value; + + // 从 _field 获取当前字段信息 + Object fieldObj = properties.get("_field"); + if (fieldObj instanceof FieldProperty) { + FieldProperty fieldProperty = + (FieldProperty) fieldObj; + + // 获取字段名 + String[] fieldNames = fieldProperty.getTableProperty().value(); + if (fieldNames != null && fieldNames.length > 0) { + String fieldName = fieldNames[0]; + + // 从 customProperties 中获取该字段的 options + Object optionsObj = properties.get("_dynamicOptions_" + fieldName); + if (optionsObj instanceof List) { + return (List) optionsObj; + } + } + } + } + + return new ArrayList<>(); + } + } + + /** + * 判断是否为动态选项处理类 + * + * @param optionsClass 选项处理类 + * @return 是否为动态选项处理类 + */ + public static boolean isDynamicOptionsClass(Class optionsClass) { + return optionsClass != null && optionsClass == DynamicOptionsProcess.class; + } +} +