diff --git a/src/main/java/cn/isliu/FsHelper.java b/src/main/java/cn/isliu/FsHelper.java index 54f44b8..d422f31 100644 --- a/src/main/java/cn/isliu/FsHelper.java +++ b/src/main/java/cn/isliu/FsHelper.java @@ -118,7 +118,7 @@ public class FsHelper { TableConf tableConf = PropertyUtil.getTableConf(clazz); Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); - List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf); + List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, fieldsMap); List fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList()); @@ -179,10 +179,10 @@ public class FsHelper { FeishuClient client = FsClient.getInstance().getClient(); Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); - List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf); + List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, fieldsMap); Map currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow)); - final Integer[] row = {0}; + final Integer[] row = {tableConf.headLine()}; fsTableDataList.forEach(fsTableData -> { if (fsTableData.getRow() > row[0]) { row[0] = fsTableData.getRow(); @@ -190,32 +190,32 @@ public class FsHelper { }); Map titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf); + Set keys = titlePostionMap.keySet(); Map fieldMap = new HashMap<>(); - fieldsMap.forEach((field, fieldProperty) -> fieldMap.put(field, fieldProperty.getField())); + fieldsMap.forEach((field, fieldProperty) -> { + if (keys.contains(field)) { + fieldMap.put(field, fieldProperty.getField()); + } + }); // 初始化批量插入对象 CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues(); List fileDataList = new ArrayList<>(); - AtomicInteger rowCount = new AtomicInteger(row[0] + 1); + AtomicInteger rowCount = new AtomicInteger(row[0]); for (T data : dataList) { Map values = GenerateUtil.getFieldValue(data, fieldMap); - String uniqueId = GenerateUtil.getUniqueId(data); + String uniqueId = GenerateUtil.getUniqueId(data, tableConf); AtomicReference rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId)); if (uniqueId != null && rowNum.get() != null) { rowNum.set(rowNum.get() + 1); values.forEach((field, fieldValue) -> { - if (!tableConf.enableCover() && fieldValue == null) { - return; - } - String position = titlePostionMap.get(field); - if (fieldValue instanceof FileData) { FileData fileData = (FileData) fieldValue; String fileType = fileData.getFileType(); @@ -226,16 +226,15 @@ public class FsHelper { fileDataList.add(fileData); } } - resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get()) - .addRow(GenerateUtil.getRowData(fieldValue)); + + if (tableConf.enableCover() || fieldValue != null) { + resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get()) + .addRow(GenerateUtil.getRowData(fieldValue)); + } }); } else { int rowCou = rowCount.incrementAndGet(); values.forEach((field, fieldValue) -> { - if (!tableConf.enableCover() && fieldValue == null) { - return; - } - String position = titlePostionMap.get(field); if (fieldValue instanceof FileData) { FileData fileData = (FileData) fieldValue; @@ -244,15 +243,18 @@ public class FsHelper { fileData.setPosition(position + rowCou); fileDataList.add(fileData); } - resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou) - .addRow(GenerateUtil.getRowData(fieldValue)); + + if (tableConf.enableCover() || fieldValue != null) { + resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou) + .addRow(GenerateUtil.getRowData(fieldValue)); + } }); } } int rowTotal = sheet.getGridProperties().getRowCount(); int rowNum = rowCount.get(); - if (rowNum > rowTotal) { + if (rowNum >= rowTotal) { FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, client); } diff --git a/src/main/java/cn/isliu/core/FsTableData.java b/src/main/java/cn/isliu/core/FsTableData.java index 40dbc8c..d8213aa 100644 --- a/src/main/java/cn/isliu/core/FsTableData.java +++ b/src/main/java/cn/isliu/core/FsTableData.java @@ -1,5 +1,6 @@ package cn.isliu.core; +import java.util.Map; import java.util.Objects; public class FsTableData { @@ -7,6 +8,7 @@ public class FsTableData { private Integer row; private String uniqueId; private Object data; + private Map fieldsPositionMap; public FsTableData() { } @@ -17,6 +19,13 @@ public class FsTableData { this.data = data; } + public FsTableData(Integer row, String uniqueId, Object data, Map fieldsPositionMap) { + this.row = row; + this.uniqueId = uniqueId; + this.data = data; + this.fieldsPositionMap = fieldsPositionMap; + } + public Integer getRow() { return row; } @@ -41,16 +50,24 @@ public class FsTableData { this.data = data; } + public Map getFieldsPositionMap() { + return fieldsPositionMap; + } + + public void setFieldsPositionMap(Map fieldsPositionMap) { + this.fieldsPositionMap = fieldsPositionMap; + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; FsTableData that = (FsTableData) o; - return Objects.equals(row, that.row) && Objects.equals(uniqueId, that.uniqueId) && Objects.equals(data, that.data); + return Objects.equals(row, that.row) && Objects.equals(uniqueId, that.uniqueId) && Objects.equals(data, that.data) && Objects.equals(fieldsPositionMap, that.fieldsPositionMap); } @Override public int hashCode() { - return Objects.hash(row, uniqueId, data); + return Objects.hash(row, uniqueId, data, fieldsPositionMap); } @Override @@ -59,6 +76,7 @@ public class FsTableData { "row=" + row + ", uniqueId='" + uniqueId + '\'' + ", data=" + data + + ", fieldsPositionMap=" + fieldsPositionMap + '}'; } } diff --git a/src/main/java/cn/isliu/core/annotation/TableConf.java b/src/main/java/cn/isliu/core/annotation/TableConf.java index ec43b2d..cfc6be8 100644 --- a/src/main/java/cn/isliu/core/annotation/TableConf.java +++ b/src/main/java/cn/isliu/core/annotation/TableConf.java @@ -9,6 +9,14 @@ import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface TableConf { + + /** + * 表格唯一键 + * + * @return 表格唯一键 + */ + String[] uniKeys() default {}; + /** * 表头行数 * diff --git a/src/main/java/cn/isliu/core/builder/ReadBuilder.java b/src/main/java/cn/isliu/core/builder/ReadBuilder.java index c0d0d52..1d7001b 100644 --- a/src/main/java/cn/isliu/core/builder/ReadBuilder.java +++ b/src/main/java/cn/isliu/core/builder/ReadBuilder.java @@ -22,15 +22,15 @@ import java.util.stream.Collectors; * 提供链式调用方式读取飞书表格数据,支持忽略唯一字段等高级功能。 */ public class ReadBuilder { - + private final String sheetId; private final String spreadsheetToken; private final Class clazz; private List ignoreUniqueFields; - + /** * 构造函数 - * + * * @param sheetId 工作表ID * @param spreadsheetToken 电子表格Token * @param clazz 实体类Class对象 @@ -40,13 +40,13 @@ public class ReadBuilder { this.spreadsheetToken = spreadsheetToken; this.clazz = clazz; } - + /** * 设置计算唯一标识时忽略的字段列表 - * + * * 指定在计算数据行唯一标识时要忽略的字段名称列表。 * 这些字段的值变化不会影响数据行的唯一性判断。 - * + * * @param fields 要忽略的字段名称列表 * @return ReadBuilder实例,支持链式调用 */ @@ -54,12 +54,12 @@ public class ReadBuilder { this.ignoreUniqueFields = new ArrayList<>(fields); return this; } - + /** * 执行数据读取并返回实体类对象列表 - * + * * 根据配置的参数从飞书表格中读取数据并映射到实体类对象列表中。 - * + * * @return 映射后的实体类对象列表 */ public List build() { @@ -69,12 +69,12 @@ public class ReadBuilder { TableConf tableConf = PropertyUtil.getTableConf(clazz); Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); - + // 处理忽略字段名称映射 List processedIgnoreFields = processIgnoreFields(fieldsMap); - + // 使用支持忽略字段的方法获取表格数据 - List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields); + List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields, fieldsMap); List fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList()); @@ -96,12 +96,52 @@ public class ReadBuilder { }); return results; } - + + public Map> groupBuild() { + Map> results = new HashMap<>(); + FeishuClient client = FsClient.getInstance().getClient(); + Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); + TableConf tableConf = PropertyUtil.getTableConf(clazz); + + Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); + + // 处理忽略字段名称映射 + List processedIgnoreFields = processIgnoreFields(fieldsMap); + + // 使用支持忽略字段的方法获取表格数据 + Map> fsTableDataMap = FsTableUtil.getGroupFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields, fieldsMap); + + List fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList()); + + fsTableDataMap.forEach((key, fsTableDataList) -> { + List groupResults = new ArrayList<>(); + fsTableDataList.stream().filter(tableData -> tableData.getRow() >= tableConf.headLine()).forEach(tableData -> { + Object data = tableData.getData(); + if (data instanceof HashMap) { + Map rowData = (HashMap) data; + JsonObject jsonObject = JSONUtil.convertMapToJsonObject(rowData); + Map dataMap = ConvertFieldUtil.convertPositionToField(jsonObject, fieldsMap); + T t = GenerateUtil.generateInstance(fieldPathList, clazz, dataMap); + if (t instanceof BaseEntity) { + BaseEntity baseEntity = (BaseEntity) t; + baseEntity.setUniqueId(tableData.getUniqueId()); + baseEntity.setRow(tableData.getRow()); + baseEntity.setRowData(rowData); + } + groupResults.add(t); + } + }); + results.put(key, groupResults); + }); + + return results; + } + /** * 处理忽略字段名称映射 - * + * * 将实体字段名称映射为表格列名称 - * + * * @param fieldsMap 字段映射 * @return 处理后的忽略字段列表 */ @@ -109,22 +149,22 @@ public class ReadBuilder { if (ignoreUniqueFields == null || ignoreUniqueFields.isEmpty()) { return new ArrayList<>(); } - + List processedFields = new ArrayList<>(); - + // 遍历字段映射,找到对应的表格列名 for (Map.Entry entry : fieldsMap.entrySet()) { String fieldName = entry.getValue().getField(); // 获取字段的最后一部分名称(去掉嵌套路径) String simpleFieldName = fieldName.substring(fieldName.lastIndexOf(".") + 1); - + // 如果忽略字段列表中包含此字段名,则添加对应的表格列名 if (ignoreUniqueFields.contains(simpleFieldName)) { String tableColumnName = entry.getKey(); // 表格列名(注解中的value值) processedFields.add(tableColumnName); } } - + return processedFields; } } diff --git a/src/main/java/cn/isliu/core/builder/SheetBuilder.java b/src/main/java/cn/isliu/core/builder/SheetBuilder.java index bac3724..263bce1 100644 --- a/src/main/java/cn/isliu/core/builder/SheetBuilder.java +++ b/src/main/java/cn/isliu/core/builder/SheetBuilder.java @@ -10,10 +10,7 @@ import cn.isliu.core.utils.FsTableUtil; import cn.isliu.core.utils.PropertyUtil; import cn.isliu.core.utils.StringUtil; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -116,7 +113,9 @@ public class SheetBuilder { * @return SheetBuilder实例,支持链式调用 */ public SheetBuilder fieldDescription(Map fieldDescriptions) { - this.fieldDescriptions.putAll(fieldDescriptions); + if (fieldDescriptions != null && !fieldDescriptions.isEmpty()) { + this.fieldDescriptions.putAll(fieldDescriptions); + } return this; } @@ -172,21 +171,21 @@ public class SheetBuilder { // 2、添加表头数据 FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions), client); - // 3、设置表格样式 - FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), client, spreadsheetToken); - - // 4、合并单元格 - List mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap); - if (!mergeCell.isEmpty()) { - mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken)); - } - - // 5、设置单元格为文本格式 + // 3、设置单元格为文本格式 if (tableConf.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 { FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc(), customProperties); @@ -197,6 +196,57 @@ public class SheetBuilder { return sheetId; } + public String groupBuild(String ...groupFields) { + // 获取所有字段映射 + Map allFieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); + + // 根据includeFields过滤字段映射 + Map fieldsMap = filterFieldsMap(allFieldsMap); + + // 生成表头 + List headers = PropertyUtil.getHeaders(fieldsMap, includeFields); + + // 获取表格配置 + TableConf tableConf = PropertyUtil.getTableConf(clazz); + + // 创建飞书客户端 + FeishuClient client = FsClient.getInstance().getClient(); + + // 1、创建sheet + String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken); + + // 2、添加表头数据 + List groupFieldList = new ArrayList<>(Arrays.asList(groupFields)); + List headerList = FsTableUtil.getGroupHeaders(groupFieldList, headers); + FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, headerList, fieldsMap, tableConf, fieldDescriptions, groupFieldList), client); + + // 3、设置单元格为文本格式 + if (tableConf.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); + FsTableUtil.setTableOptions(spreadsheetToken, headerWithColumnIdentifiers, fieldsMap, sheetId, tableConf.enableDesc(), customProperties); + } catch (Exception e) { + Logger.getLogger(SheetBuilder.class.getName()).log(Level.SEVERE,"【表格构建器】设置表格下拉异常!sheetId:" + sheetId + ", 错误信息:{}", e.getMessage()); + } + + return sheetId; + } + /** * 根据包含字段列表过滤字段映射 * @@ -217,7 +267,7 @@ public class SheetBuilder { if (field.isEmpty()) { return false; } - return includeFields.contains(StringUtil.toUnderscoreCase(field)); + return includeFields.contains(field) || includeFields.contains(StringUtil.toUnderscoreCase(field)); }) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } diff --git a/src/main/java/cn/isliu/core/builder/WriteBuilder.java b/src/main/java/cn/isliu/core/builder/WriteBuilder.java index b23fef2..51e537c 100644 --- a/src/main/java/cn/isliu/core/builder/WriteBuilder.java +++ b/src/main/java/cn/isliu/core/builder/WriteBuilder.java @@ -13,10 +13,7 @@ import cn.isliu.core.pojo.FieldProperty; import cn.isliu.core.service.CustomValueService; import cn.isliu.core.utils.*; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -27,17 +24,18 @@ import java.util.stream.Collectors; * 提供链式调用方式写入飞书表格数据,支持忽略唯一字段等高级功能。 */ public class WriteBuilder { - + private final String sheetId; private final String spreadsheetToken; private final List dataList; private List ignoreUniqueFields; private Class clazz; private boolean ignoreNotFound; - + private String groupField; + /** * 构造函数 - * + * * @param sheetId 工作表ID * @param spreadsheetToken 电子表格Token * @param dataList 要写入的数据列表 @@ -48,14 +46,15 @@ public class WriteBuilder { this.dataList = dataList; this.clazz = null; this.ignoreNotFound = false; + this.groupField = null; } - + /** * 设置计算唯一标识时忽略的字段列表 - * + * * 指定在计算数据行唯一标识时要忽略的字段名称列表。 * 这些字段的值变化不会影响数据行的唯一性判断。 - * + * * @param fields 要忽略的字段名称列表 * @return WriteBuilder实例,支持链式调用 */ @@ -66,10 +65,10 @@ public class WriteBuilder { /** * 设置用于解析注解的实体类 - * + * * 指定用于解析@TableProperty注解的实体类。这个类可以与数据列表的类型不同, * 主要用于获取字段映射关系和表格配置信息。 - * + * * @param clazz 用于解析注解的实体类 * @return WriteBuilder实例,支持链式调用 */ @@ -92,12 +91,26 @@ public class WriteBuilder { this.ignoreNotFound = ignoreNotFound; return this; } - + + /** + * 设置分组字段 + * + * 配置分组字段,用于处理数据行分组。 + * 当数据行存在分组字段时,将按照分组字段进行分组,并分别处理每个分组。 + * + * @param groupField 分组字段名称 + * @return WriteBuilder实例,支持链式调用 + */ + public WriteBuilder groupField(String groupField) { + this.groupField = groupField; + return this; + } + /** * 执行数据写入并返回操作结果 - * + * * 根据配置的参数将数据写入到飞书表格中,支持新增和更新操作。 - * + * * @return 写入操作结果 */ public Object build() { @@ -107,45 +120,64 @@ public class WriteBuilder { Class aClass = clazz; Map fieldsMap; - TableConf tableConf = PropertyUtil.getTableConf(aClass); Map fieldMap = new HashMap<>(); Class sourceClass = dataList.get(0).getClass(); - if (aClass.equals(sourceClass)) { + if (aClass != null && aClass.equals(sourceClass)) { fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass); } else { fieldsMap = PropertyUtil.getTablePropertyFieldsMap(sourceClass); } - fieldsMap.forEach((field, fieldProperty) -> fieldMap.put(field, fieldProperty.getField())); + FeishuClient client = FsClient.getInstance().getClient(); + Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); + + TableConf tableConf = aClass != null ? PropertyUtil.getTableConf(aClass) : PropertyUtil.getTableConf(sourceClass); + Map titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf); + Set keys = titlePostionMap.keySet(); + + fieldsMap.forEach((field, fieldProperty) -> { + if (keys.contains(field)) { + fieldMap.put(field, fieldProperty.getField()); + } + }); // 处理忽略字段名称映射 List processedIgnoreFields = processIgnoreFields(fieldsMap); - FeishuClient client = FsClient.getInstance().getClient(); - Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); - // 使用支持忽略字段的方法获取表格数据 - List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields); - Map currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow)); + List fsTableDataList; + if (groupField == null) { + fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields, fieldsMap); + } else { + Map> groupFsTableData = FsTableUtil.getGroupFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields, fieldsMap); + fsTableDataList = groupFsTableData.get(groupField); + } - final Integer[] row = {0}; + if (!fsTableDataList.isEmpty()) { + Map fieldsPositionMap = fsTableDataList.get(0).getFieldsPositionMap(); + if (fieldsPositionMap != null) { + titlePostionMap = fieldsPositionMap; + } + } + + Map currTableRowMap = fsTableDataList.stream() + .filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine()) + .collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow)); + + final Integer[] row = {tableConf.headLine()}; fsTableDataList.forEach(fsTableData -> { - if (fsTableData.getRow() > row[0]) { - row[0] = fsTableData.getRow(); + if ((fsTableData.getRow() + 1) > row[0]) { + row[0] = fsTableData.getRow() + 1; } }); - Map titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf); - - - // 初始化批量插入对象 CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues(); List fileDataList = new ArrayList<>(); - AtomicInteger rowCount = new AtomicInteger(row[0] + 1); + AtomicInteger rowCount = new AtomicInteger(row[0]); for (T data : dataList) { Map values = GenerateUtil.getFieldValue(data, fieldMap); @@ -154,20 +186,17 @@ public class WriteBuilder { String uniqueId; if (data.getClass().equals(aClass)) { // 类型相同,使用忽略字段逻辑计算唯一标识 - uniqueId = calculateUniqueIdWithIgnoreFields(data, processedIgnoreFields, aClass); + uniqueId = calculateUniqueIdWithIgnoreFields(data, processedIgnoreFields, tableConf); } else { - uniqueId = GenerateUtil.getUniqueId(data); + uniqueId = GenerateUtil.getUniqueId(data, tableConf); } AtomicReference rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId)); if (uniqueId != null && rowNum.get() != null) { rowNum.set(rowNum.get() + 1); + Map finalTitlePostionMap = titlePostionMap; values.forEach((field, fieldValue) -> { - if (!tableConf.enableCover() && fieldValue == null) { - return; - } - - String position = titlePostionMap.get(field); + String position = finalTitlePostionMap.get(field); if (fieldValue instanceof FileData) { FileData fileData = (FileData) fieldValue; @@ -179,17 +208,17 @@ public class WriteBuilder { fileDataList.add(fileData); } } - resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get()) - .addRow(GenerateUtil.getRowData(fieldValue)); + if (tableConf.enableCover() || fieldValue != null) { + resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get()) + .addRow(GenerateUtil.getRowData(fieldValue)); + } }); } else if (!ignoreNotFound) { int rowCou = rowCount.incrementAndGet(); + Map finalTitlePostionMap1 = titlePostionMap; values.forEach((field, fieldValue) -> { - if (!tableConf.enableCover() && fieldValue == null) { - return; - } - String position = titlePostionMap.get(field); + String position = finalTitlePostionMap1.get(field); if (fieldValue instanceof FileData) { FileData fileData = (FileData) fieldValue; fileData.setSheetId(sheetId); @@ -197,15 +226,18 @@ public class WriteBuilder { fileData.setPosition(position + rowCou); fileDataList.add(fileData); } - resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou) - .addRow(GenerateUtil.getRowData(fieldValue)); + + if (tableConf.enableCover() || fieldValue != null) { + resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou) + .addRow(GenerateUtil.getRowData(fieldValue)); + } }); } } int rowTotal = sheet.getGridProperties().getRowCount(); int rowNum = rowCount.get(); - if (rowNum > rowTotal) { + if (rowNum >= rowTotal) { FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, client); } @@ -225,12 +257,12 @@ public class WriteBuilder { } return null; } - + /** * 处理忽略字段名称映射 - * + * * 将实体字段名称映射为表格列名称 - * + * * @param fieldsMap 字段映射 * @return 处理后的忽略字段列表 */ @@ -238,56 +270,56 @@ public class WriteBuilder { if (ignoreUniqueFields == null || ignoreUniqueFields.isEmpty()) { return new ArrayList<>(); } - + List processedFields = new ArrayList<>(); - + // 遍历字段映射,找到对应的表格列名 for (Map.Entry entry : fieldsMap.entrySet()) { String fieldName = entry.getValue().getField(); // 获取字段的最后一部分名称(去掉嵌套路径) String simpleFieldName = fieldName.substring(fieldName.lastIndexOf(".") + 1); - + // 如果忽略字段列表中包含此字段名,则添加对应的表格列名 if (ignoreUniqueFields.contains(simpleFieldName)) { String tableColumnName = entry.getKey(); // 表格列名(注解中的value值) processedFields.add(tableColumnName); } } - + return processedFields; } - + /** * 计算考虑忽略字段的唯一标识 - * + * * 根据忽略字段列表计算数据的唯一标识 - * + * * @param data 数据对象 * @param processedIgnoreFields 处理后的忽略字段列表 - * @param clazz 用于解析注解的实体类 + * @param tableConf 用于解析注解的表格配置 * @return 唯一标识 */ - private String calculateUniqueIdWithIgnoreFields(T data, List processedIgnoreFields, Class clazz) { + private String calculateUniqueIdWithIgnoreFields(T data, List processedIgnoreFields, TableConf tableConf) { try { // 获取所有字段值 Map allFieldValues = GenerateUtil.getFieldValue(data, new HashMap<>()); - + // 如果不需要忽略字段,使用原有逻辑 - if (processedIgnoreFields.isEmpty()) { - return GenerateUtil.getUniqueId(data); + if (tableConf.uniKeys().length > 0 || processedIgnoreFields.isEmpty()) { + return GenerateUtil.getUniqueId(data, tableConf); } - + // 移除忽略字段后计算唯一标识 Map filteredValues = new HashMap<>(allFieldValues); processedIgnoreFields.forEach(filteredValues::remove); - + // 将过滤后的值转换为JSON字符串并计算SHA256 String jsonStr = StringUtil.mapToJson(filteredValues); return StringUtil.getSHA256(jsonStr); - + } catch (Exception e) { // 如果计算失败,回退到原有逻辑 - return GenerateUtil.getUniqueId(data); + return GenerateUtil.getUniqueId(data, tableConf); } } } diff --git a/src/main/java/cn/isliu/core/client/FeishuClient.java b/src/main/java/cn/isliu/core/client/FeishuClient.java index 9e595d7..e442db5 100644 --- a/src/main/java/cn/isliu/core/client/FeishuClient.java +++ b/src/main/java/cn/isliu/core/client/FeishuClient.java @@ -23,8 +23,14 @@ public class FeishuClient { private final String appId; private final String appSecret; - // 服务管理器,用于统一管理自定义服务实例 - private final ServiceManager serviceManager = new ServiceManager<>(this); + // 自定义服务,处理官方SDK未覆盖的API + private volatile CustomSheetService customSheetService; + private volatile CustomDimensionService customDimensionService; + private volatile CustomCellService customCellService; + private volatile CustomValueService customValueService; + private volatile CustomDataValidationService customDataValidationService; + private volatile CustomProtectedDimensionService customProtectedDimensionService; + private volatile CustomFileService customFileService; private FeishuClient(String appId, String appSecret, Client officialClient, OkHttpClient httpClient) { this.appId = appId; @@ -36,7 +42,7 @@ public class FeishuClient { /** * 创建客户端构建器 * - * @param appId 应用ID + * @param appId 应用ID * @param appSecret 应用密钥 * @return 构建器 */ @@ -68,7 +74,14 @@ public class FeishuClient { * @return 扩展表格服务 */ public CustomSheetService customSheets() { - return serviceManager.getService(CustomSheetService.class, () -> new CustomSheetService(this)); + if (customSheetService == null) { + synchronized (this) { + if (customSheetService == null) { + customSheetService = new CustomSheetService(this); + } + } + } + return customSheetService; } /** @@ -77,7 +90,14 @@ public class FeishuClient { * @return 扩展行列服务 */ public CustomDimensionService customDimensions() { - return serviceManager.getService(CustomDimensionService.class, () -> new CustomDimensionService(this)); + if (customDimensionService == null) { + synchronized (this) { + if (customDimensionService == null) { + customDimensionService = new CustomDimensionService(this); + } + } + } + return customDimensionService; } /** @@ -86,7 +106,14 @@ public class FeishuClient { * @return 扩展单元格服务 */ public CustomCellService customCells() { - return serviceManager.getService(CustomCellService.class, () -> new CustomCellService(this)); + if (customCellService == null) { + synchronized (this) { + if (customCellService == null) { + customCellService = new CustomCellService(this); + } + } + } + return customCellService; } /** @@ -95,7 +122,14 @@ public class FeishuClient { * @return 扩展数据值服务 */ public CustomValueService customValues() { - return serviceManager.getService(CustomValueService.class, () -> new CustomValueService(this)); + if (customValueService == null) { + synchronized (this) { + if (customValueService == null) { + customValueService = new CustomValueService(this); + } + } + } + return customValueService; } /** @@ -104,7 +138,14 @@ public class FeishuClient { * @return 自定义数据验证服务 */ public CustomDataValidationService customDataValidations() { - return serviceManager.getService(CustomDataValidationService.class, () -> new CustomDataValidationService(this)); + if (customDataValidationService == null) { + synchronized (this) { + if (customDataValidationService == null) { + customDataValidationService = new CustomDataValidationService(this); + } + } + } + return customDataValidationService; } /** @@ -113,7 +154,14 @@ public class FeishuClient { * @return 扩展保护范围服务 */ public CustomProtectedDimensionService customProtectedDimensions() { - return serviceManager.getService(CustomProtectedDimensionService.class, () -> new CustomProtectedDimensionService(this)); + if (customProtectedDimensionService == null) { + synchronized (this) { + if (customProtectedDimensionService == null) { + customProtectedDimensionService = new CustomProtectedDimensionService(this); + } + } + } + return customProtectedDimensionService; } /** @@ -122,10 +170,16 @@ public class FeishuClient { * @return 扩展文件服务 */ public CustomFileService customFiles() { - return serviceManager.getService(CustomFileService.class, () -> new CustomFileService(this)); + if (customFileService == null) { + synchronized (this) { + if (customFileService == null) { + customFileService = new CustomFileService(this); + } + } + } + return customFileService; } - /** * 获取官方客户端 * @@ -178,7 +232,7 @@ public class FeishuClient { // 默认OkHttp配置 this.httpClientBuilder = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.MINUTES).readTimeout(10, TimeUnit.MINUTES) - .writeTimeout(10, TimeUnit.MINUTES).connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)); + .writeTimeout(10, TimeUnit.MINUTES).callTimeout(30, TimeUnit.MINUTES); } /** diff --git a/src/main/java/cn/isliu/core/client/FsClient.java b/src/main/java/cn/isliu/core/client/FsClient.java index 3e64305..ed291d4 100644 --- a/src/main/java/cn/isliu/core/client/FsClient.java +++ b/src/main/java/cn/isliu/core/client/FsClient.java @@ -1,5 +1,8 @@ package cn.isliu.core.client; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + /** * 线程安全的飞书客户端管理器 * 使用ThreadLocal为每个线程维护独立的客户端实例 @@ -8,6 +11,7 @@ public class FsClient implements AutoCloseable { private static volatile FsClient instance; private final ThreadLocal clientHolder = new ThreadLocal<>(); + private final Map clientMap = new ConcurrentHashMap<>(); // 私有构造函数防止外部实例化 private FsClient() { @@ -55,10 +59,16 @@ public class FsClient implements AutoCloseable { if (appSecret == null || appSecret.trim().isEmpty()) { throw new IllegalArgumentException("appSecret cannot be null or empty"); } - - FeishuClient client = FeishuClient.newBuilder(appId, appSecret).build(); - clientHolder.set(client); - return client; + if (clientMap.containsKey(appId + "_" + appSecret)) { + FeishuClient feishuClient = clientMap.get(appId + "_" + appSecret); + clientHolder.set(feishuClient); + return feishuClient; + } else { + FeishuClient client = FeishuClient.newBuilder(appId, appSecret).build(); + clientMap.put(appId + "_" + appSecret, client); + clientHolder.set(client); + return client; + } } /** diff --git a/src/main/java/cn/isliu/core/utils/FsTableUtil.java b/src/main/java/cn/isliu/core/utils/FsTableUtil.java index e11bc8f..876f3d0 100644 --- a/src/main/java/cn/isliu/core/utils/FsTableUtil.java +++ b/src/main/java/cn/isliu/core/utils/FsTableUtil.java @@ -28,7 +28,6 @@ import java.util.stream.Collectors; * 包括数据处理、样式设置、选项设置等功能 */ public class FsTableUtil { - /** * 获取飞书表格数据 * @@ -38,8 +37,8 @@ public class FsTableUtil { * @param spreadsheetToken 电子表格Token * @return 飞书表格数据列表 */ - public static List getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf) { - return getFsTableData(sheet, spreadsheetToken, tableConf, new ArrayList<>()); + public static List getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf, Map fieldsMap) { + return getFsTableData(sheet, spreadsheetToken, tableConf, new ArrayList<>(), fieldsMap); } /** @@ -51,9 +50,129 @@ public class FsTableUtil { * @param ignoreUniqueFields 计算唯一标识时忽略的字段列表 * @return 飞书表格数据列表 */ - public static List getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf, List ignoreUniqueFields) { - + public static Map> getGroupFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf, List ignoreUniqueFields, Map fieldsMap) { // 计算数据范围 + List> values = getSourceTableValues(sheet, spreadsheetToken); + + // 获取飞书表格数据 + TableData tableData = processSheetData(sheet, values); + + String[] uniKeys = tableConf.uniKeys(); + Set uniKeyNames = getUniKeyNames(fieldsMap, uniKeys); + + List dataList = getFsTableData(tableData, ignoreUniqueFields); + Map titleMap = new HashMap<>(); + Map> categoryPositionMap = new HashMap<>(); + dataList.stream().filter(d -> d.getRow() == (tableConf.titleRow() - 2)).findFirst().ifPresent(d -> { + Map map = (Map) d.getData(); + map.forEach((k, v) -> { + if (v != null && !v.isEmpty()) { + categoryPositionMap.computeIfAbsent(v, k1 -> new ArrayList<>()).add(k); + } + }); + }); + + dataList.stream().filter(d -> d.getRow() == (tableConf.titleRow() - 1)).findFirst() + .ifPresent(d -> { + Map map = (Map) d.getData(); + map.forEach((k, v) -> { + if (v != null && !v.isEmpty()) { + titleMap.put(k + "_" + v, v); + } else { + titleMap.put(k, v); + } + }); + }); + + List fsTableDataList = dataList.stream().filter(fsTableData -> fsTableData.getRow() >= (tableConf.headLine() -1)).map(item -> { + Map resultMap = new HashMap<>(); + + Map map = (Map) item.getData(); + map.forEach((k, v) -> { + titleMap.forEach((k1, v1) -> { + if (k1.startsWith(k)) { + resultMap.put(k1, v); + } + }); + }); + item.setData(resultMap); + return item; + }).collect(Collectors.toList()); + + Map> dataMap = new HashMap<>(); + categoryPositionMap.forEach((k1, v1) -> { + List fsList = new ArrayList<>(); + + for (FsTableData fsTableData : fsTableDataList) { + Map resultMap = new HashMap<>(); + Map fieldsPositionMap = new HashMap<>(); + Map data = (Map) fsTableData.getData(); + data.forEach((k, v) -> { + if (k != null) { + String[] split = k.split("_"); + if (v1.contains(split[0]) && split.length > 1) { + resultMap.put(split[1], v); + fieldsPositionMap.put(split[1], split[0]); + } + } + }); + + if(areAllValuesNullOrBlank(resultMap)) { + continue; + } + + String jsonStr; + if (!uniKeyNames.isEmpty()) { + List uniKeyValues = new ArrayList<>(); + for (String key : uniKeyNames) { + if (resultMap.containsKey(key)) { + uniKeyValues.add(resultMap.get(key)); + } + } + jsonStr = StringUtil.listToJson(uniKeyValues); + } else { + if (!ignoreUniqueFields.isEmpty()) { + Map clone = new HashMap<>(resultMap); + ignoreUniqueFields.forEach(clone::remove); + jsonStr = StringUtil.mapToJson(clone); + } else { + jsonStr = StringUtil.mapToJson(resultMap); + } + } + + FsTableData fsData = new FsTableData(); + fsData.setRow(fsTableData.getRow()); + String uniqueId = StringUtil.getSHA256(jsonStr); + fsData.setUniqueId(uniqueId); + fsData.setData(resultMap); + fsData.setFieldsPositionMap(fieldsPositionMap); + fsList.add(fsData); + } + dataMap.put(k1, fsList); + }); + return dataMap; + } + + @NotNull + private static Set getUniKeyNames(Map fieldsMap, String[] uniKeys) { + Set uniKeyNames = new HashSet<>(); + fieldsMap.forEach((k, v) -> { + String field = v.getField(); + if (field != null && !field.isEmpty()) { + if (Arrays.asList(uniKeys).contains(field)) { + uniKeyNames.add(k); + } + + if (Arrays.asList(uniKeys).contains(StringUtil.toUnderscoreCase(field))) { + uniKeyNames.add(k); + } + } + }); + return uniKeyNames; + } + + @NotNull + private static List> getSourceTableValues(Sheet sheet, String spreadsheetToken) { GridProperties gridProperties = sheet.getGridProperties(); int totalRow = gridProperties.getRowCount(); int rowCount = Math.min(totalRow, 100); // 每次读取的行数 @@ -80,16 +199,40 @@ public class FsTableUtil { } } } + return values; + } + + /** + * 获取飞书表格数据(支持忽略唯一字段) + * + * @param sheet 工作表对象 + * @param spreadsheetToken 电子表格Token + * @param tableConf 表格配置 + * @param ignoreUniqueFields 计算唯一标识时忽略的字段列表 + * @return 飞书表格数据列表 + */ + public static List getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf, List ignoreUniqueFields, Map fieldsMap) { + + // 计算数据范围 + List> values = getSourceTableValues(sheet, spreadsheetToken); // 获取飞书表格数据 TableData tableData = processSheetData(sheet, values); + String[] uniKeys = tableConf.uniKeys(); + Set uniKeyNames = getUniKeyNames(fieldsMap, uniKeys); List dataList = getFsTableData(tableData, ignoreUniqueFields); Map titleMap = new HashMap<>(); + Map fieldsPositionMap = new HashMap<>(); dataList.stream().filter(d -> d.getRow() == (tableConf.titleRow() - 1)).findFirst() .ifPresent(d -> { Map map = (Map) d.getData(); + map.forEach((k, v) -> { + if (v != null && !v.isEmpty()) { + fieldsPositionMap.put(v, k); + }; + }); titleMap.putAll(map); }); return dataList.stream().filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine()).map(item -> { @@ -103,6 +246,24 @@ public class FsTableUtil { } }); item.setData(resultMap); + item.setFieldsPositionMap(fieldsPositionMap); + + String jsonStr = null; + if (!uniKeyNames.isEmpty()) { + List uniKeyValues = new ArrayList<>(); + for (String key : uniKeyNames) { + if (resultMap.containsKey(key)) { + uniKeyValues.add(resultMap.get(key)); + } + } + jsonStr = StringUtil.listToJson(uniKeyValues); + } + + if (jsonStr != null) { + String uniqueId = StringUtil.getSHA256(jsonStr); + item.setUniqueId(uniqueId); + } + return item; }).collect(Collectors.toList()); } @@ -113,7 +274,7 @@ public class FsTableUtil { * @param tableData 表格数据对象 * @return 飞书表格数据列表 */ - private static List getFsTableData(TableData tableData) { + public static List getFsTableData(TableData tableData) { return getFsTableData(tableData, new ArrayList<>()); } @@ -124,7 +285,7 @@ public class FsTableUtil { * @param ignoreUniqueFields 忽略的唯一字段列表 * @return 飞书表格数据列表 */ - private static List getFsTableData(TableData tableData, List ignoreUniqueFields) { + public static List getFsTableData(TableData tableData, List ignoreUniqueFields) { List fsTableList = new LinkedList<>(); // 5. 访问补齐后的数据 @@ -381,6 +542,57 @@ public class FsTableUtil { }); } + public static void setTableOptions(String spreadsheetToken, String[] headers, Map fieldsMap, + String sheetId, boolean enableDesc, Map customProperties) { + + int line = getMaxLevel(fieldsMap) + (enableDesc ? 3 : 2); + fieldsMap.forEach((field, fieldProperty) -> { + TableProperty tableProperty = fieldProperty.getTableProperty(); + if (tableProperty != null) { + List positions = new ArrayList<>(); + for (String obj : headers) { + if (obj != null && obj.startsWith(field)) { + String[] split = obj.split("_"); + if (split.length > 1) { + positions.add(split[1]); + } + } + } + + if (!positions.isEmpty()) { + positions.forEach(position -> { + if (tableProperty.enumClass() != BaseEnum.class) { + FsApiUtil.setOptions(sheetId, FsClient.getInstance().getClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200, + Arrays.stream(tableProperty.enumClass().getEnumConstants()).map(BaseEnum::getDesc).collect(Collectors.toList())); + } + + if (tableProperty.optionsClass() != OptionsValueProcess.class) { + List result; + Class optionsClass = tableProperty.optionsClass(); + try { + Map properties = new HashMap<>(); + if (customProperties == null) { + properties.put("_field", fieldProperty); + } else { + customProperties.put("_field", fieldProperty); + } + OptionsValueProcess optionsValueProcess = optionsClass.getDeclaredConstructor().newInstance(); + result = (List) optionsValueProcess.process(customProperties == null ? properties : customProperties); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + + if (result != null && !result.isEmpty()) { + FsApiUtil.setOptions(sheetId, FsClient.getInstance().getClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200, + result); + } + } + }); + } + } + }); + } + public static void setTableOptions(String spreadsheetToken, List headers, Map fieldsMap, String sheetId, boolean enableDesc) { setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, enableDesc, null); } @@ -390,6 +602,81 @@ public class FsTableUtil { return getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, null); } + public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List headers, List headerList, Map fieldsMap, + TableConf tableConf, Map fieldDescriptions, List groupFields) { + String position = FsTableUtil.getColumnNameByNuNumber(headerList.size()); + + CustomValueService.ValueRequest.BatchPutValuesBuilder batchPutValuesBuilder + = CustomValueService.ValueRequest.batchPutValues(); + + int row = tableConf.titleRow(); + if (tableConf.enableDesc()) { + batchPutValuesBuilder.addRange(sheetId + "!A" + (row - 1) + ":" + position + (row + 1)); + batchPutValuesBuilder.addRow(getGroupArray(headers, headerList.size(), groupFields)); + batchPutValuesBuilder.addRow(headerList.toArray()); + batchPutValuesBuilder.addRow(getDescArray(headerList, fieldsMap, fieldDescriptions)); + } else { + batchPutValuesBuilder.addRange(sheetId + "!A" + (row - 1) + ":" + position + row); + batchPutValuesBuilder.addRow(getGroupArray(headers, headerList.size(), groupFields)); + batchPutValuesBuilder.addRow(headerList.toArray()); + } + return batchPutValuesBuilder.build(); + } + + private static Object[] getGroupArray(List headers, int size, List groupFields) { + Object[] groupArray = new Object[size]; + int index = 0; + + for (int i = 0; i < groupFields.size(); i++) { + if (i > 0) { + // 在非第一个groupField前添加null分隔符 + groupArray[index++] = null; + } + + String groupField = groupFields.get(i); + // 为当前groupField填充headers长度的数据 + for (int j = 0; j < headers.size(); j++) { + groupArray[index++] = groupField; + } + } + + return groupArray; + } + + /** + * 根据headers和groupFields生成带表格列标识的数据 + * + * @param headers 表头列表 + * @param groupFields 分组字段列表 + * @return 带表格列标识的数据数组 + */ + public static String[] generateHeaderWithColumnIdentifiers(List headers, List groupFields) { + // 计算结果数组大小 + // 每个groupField需要headers.size()个位置,加上(groupFields.size()-1)个null分隔符 + int size = groupFields.size() * headers.size() + (groupFields.size() - 1); + String[] result = new String[size]; + + int index = 0; + + for (int i = 0; i < groupFields.size(); i++) { + // 在非第一个groupField前添加null分隔符 + if (i > 0) { + result[index++] = null; + } + + // 为当前groupField填充headers长度的数据 + for (int j = 0; j < headers.size(); j++) { + // 获取对应的列标识(A, B, C, ...) + String columnIdentifier = getColumnName(index); + // 拼接header和列标识 + result[index++] = headers.get(j) + "_" + columnIdentifier; + } + } + + return result; + } + + public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List headers, Map fieldsMap, TableConf tableConf, Map fieldDescriptions) { @@ -531,6 +818,53 @@ public class FsTableUtil { return styleCellsBatchBuilder; } + public static CustomCellService.StyleCellsBatchBuilder getDefaultTableStyle(String sheetId, String[] position, TableConf tableConf) { + CustomCellService.StyleCellsBatchBuilder styleCellsBatchBuilder = CustomCellService.CellRequest.styleCellsBatch() + .addRange(sheetId, position[0] + 1, position[1] + 2) + .backColor(tableConf.headBackColor()) + .foreColor(tableConf.headFontColor()); + + return styleCellsBatchBuilder; + } + + public static Map calculateGroupPositions(List headers, List groupFields) { + Map positions = new HashMap<>(); + int index = 0; + + for (int i = 0; i < groupFields.size(); i++) { + String groupField = groupFields.get(i); + // 计算开始位置 + String startPosition = getColumnName(index); + // 计算结束位置 + index += headers.size() - 1; + String endPosition = getColumnName(index); + + positions.put(groupField, new String[]{startPosition, endPosition}); + + // 如果不是最后一个groupField,跳过null分隔符 + if (i < groupFields.size() - 1) { + index += 2; // 跳过当前末尾位置和null分隔符 + } else { + index += 1; // 只跳过当前末尾位置 + } + } + + return positions; + } + + public static List getGroupHeaders(List groupFieldList, List headers) { + List headerList = new ArrayList<>(); + + groupFieldList.forEach(groupField -> { + if (!headerList.isEmpty()) { + headerList.add(null); + } + headerList.addAll(headers); + }); + return headerList; + } + + /** * 根据层级分组字段属性,并按order排序 * @@ -688,6 +1022,22 @@ public class FsTableUtil { return result; } + // + public static List getMergeCell(String sheetId, Collection positions) { + List mergeRequests = new ArrayList<>(); + for (String[] position : positions) { + if (position.length == 2) { + CustomCellService.CellRequest mergeRequest = CustomCellService.CellRequest.mergeCells() + .sheetId(sheetId) + .startPosition(position[0] + 1) + .endPosition(position[1] + 1) + .build(); + mergeRequests.add(mergeRequest); + } + } + return mergeRequests; + } + public static List getMergeCell(String sheetId, Map fieldsMap) { List mergeRequests = new ArrayList<>(); @@ -937,4 +1287,23 @@ public class FsTableUtil { public static String addLineBreaksPer8Chars(String text) { return addLineBreaks(text, 8); } + + public static boolean areAllValuesNullOrBlank(Map map) { + if (map == null || map.isEmpty()) { + return true; + } + + for (Object value : map.values()) { + if (value != null) { + if (value instanceof String) { + if (!((String) value).isEmpty()) { + return false; + } + } else { + return false; + } + } + } + return true; + } } \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/utils/GenerateUtil.java b/src/main/java/cn/isliu/core/utils/GenerateUtil.java index 7d08b1d..2bc68b8 100644 --- a/src/main/java/cn/isliu/core/utils/GenerateUtil.java +++ b/src/main/java/cn/isliu/core/utils/GenerateUtil.java @@ -1,6 +1,7 @@ package cn.isliu.core.utils; import cn.isliu.core.FileData; +import cn.isliu.core.annotation.TableConf; import cn.isliu.core.annotation.TableProperty; import cn.isliu.core.enums.BaseEnum; import cn.isliu.core.enums.FileType; @@ -22,8 +23,6 @@ import java.util.stream.Collectors; */ public class GenerateUtil { - // 使用统一的FsLogger替代java.util.logging.Logger - /** * 根据配置和数据生成DTO对象(通用版本) * @@ -375,7 +374,7 @@ public class GenerateUtil { try { Object value = getNestedFieldValue(target, fieldPath); - if (value != null) { + if (fieldPath.split("\\.").length == 1) { result.put(fieldName, value); } } catch (Exception e) { @@ -481,13 +480,27 @@ public class GenerateUtil { return fieldValue; } - public static String getUniqueId(T data) { + public static String getUniqueId(T data, TableConf tableConf) { String uniqueId = null; try { Object uniqueIdObj = GenerateUtil.getNestedFieldValue(data, "uniqueId"); if (uniqueIdObj != null) { uniqueId = uniqueIdObj.toString(); } + if (uniqueId == null && tableConf.uniKeys().length > 0) { + String[] uniKeys = tableConf.uniKeys(); + List uniKeyValues = new ArrayList<>(); + for (String uniKey : uniKeys) { + Object value = GenerateUtil.getNestedFieldValue(data, uniKey); + if (value != null) { + uniKeyValues.add(value); + } + } + if (!uniKeyValues.isEmpty()) { + String jsonStr = StringUtil.listToJson(uniKeyValues); + uniqueId = StringUtil.getSHA256(jsonStr); + } + } } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/cn/isliu/core/utils/PropertyUtil.java b/src/main/java/cn/isliu/core/utils/PropertyUtil.java index 50626e0..f6a0410 100644 --- a/src/main/java/cn/isliu/core/utils/PropertyUtil.java +++ b/src/main/java/cn/isliu/core/utils/PropertyUtil.java @@ -440,11 +440,17 @@ public class PropertyUtil { tableConf = clazz.getAnnotation(TableConf.class); } else { tableConf = new TableConf() { + @Override public Class annotationType() { return TableConf.class; } + @Override + public String[] uniKeys() { + return new String[0]; + } + @Override public int headLine() { return 1; diff --git a/src/main/java/cn/isliu/core/utils/StringUtil.java b/src/main/java/cn/isliu/core/utils/StringUtil.java index a84dc1d..bff8d75 100644 --- a/src/main/java/cn/isliu/core/utils/StringUtil.java +++ b/src/main/java/cn/isliu/core/utils/StringUtil.java @@ -147,4 +147,24 @@ public class StringUtil { return sb.toString(); } + public static String listToJson(List uniKeyValues) { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append("["); + for (int i = 0; i < uniKeyValues.size(); i++) { + Object value = uniKeyValues.get(i); + if (value instanceof String) { + jsonBuilder.append("\"").append(value).append("\""); + } else if (value == null) { + jsonBuilder.append("null"); + } else { + jsonBuilder.append(value); + } + if (i < uniKeyValues.size() - 1) { + jsonBuilder.append(","); + } + } + jsonBuilder.append("]"); + return jsonBuilder.toString(); + } + }