feat(feishu): 增强飞书表格客户端功能和数据处理能力- 添加自定义服务支持,包括表格、行列、单元格、数据值等扩展服务

-优化客户端管理器,实现线程安全和客户端实例缓存
- 改进表格数据读取逻辑,支持字段映射和唯一标识生成
- 增强表格工具类功能,添加分组数据处理和表头模板构建
- 完善表格构建器,支持字段描述和下拉选项设置
-优化HTTP客户端配置,增加调用超时时间
- 修复数据处理中的空值判断逻辑- 添加表格样式设置和单元格合并功能
This commit is contained in:
liushuang 2025-10-14 15:07:39 +08:00
parent 3c31130cbe
commit 59d5d937cc
12 changed files with 772 additions and 150 deletions

@ -118,7 +118,7 @@ public class FsHelper {
TableConf tableConf = PropertyUtil.getTableConf(clazz); TableConf tableConf = PropertyUtil.getTableConf(clazz);
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf); List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, fieldsMap);
List<String> fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList()); List<String> fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList());
@ -179,10 +179,10 @@ public class FsHelper {
FeishuClient client = FsClient.getInstance().getClient(); FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf); List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, fieldsMap);
Map<String, Integer> currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow)); Map<String, Integer> currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow));
final Integer[] row = {0}; final Integer[] row = {tableConf.headLine()};
fsTableDataList.forEach(fsTableData -> { fsTableDataList.forEach(fsTableData -> {
if (fsTableData.getRow() > row[0]) { if (fsTableData.getRow() > row[0]) {
row[0] = fsTableData.getRow(); row[0] = fsTableData.getRow();
@ -190,32 +190,32 @@ public class FsHelper {
}); });
Map<String, String> titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf); Map<String, String> titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf);
Set<String> keys = titlePostionMap.keySet();
Map<String, String> fieldMap = new HashMap<>(); Map<String, String> 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(); CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
List<FileData> fileDataList = new ArrayList<>(); List<FileData> fileDataList = new ArrayList<>();
AtomicInteger rowCount = new AtomicInteger(row[0] + 1); AtomicInteger rowCount = new AtomicInteger(row[0]);
for (T data : dataList) { for (T data : dataList) {
Map<String, Object> values = GenerateUtil.getFieldValue(data, fieldMap); Map<String, Object> values = GenerateUtil.getFieldValue(data, fieldMap);
String uniqueId = GenerateUtil.getUniqueId(data); String uniqueId = GenerateUtil.getUniqueId(data, tableConf);
AtomicReference<Integer> rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId)); AtomicReference<Integer> rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId));
if (uniqueId != null && rowNum.get() != null) { if (uniqueId != null && rowNum.get() != null) {
rowNum.set(rowNum.get() + 1); rowNum.set(rowNum.get() + 1);
values.forEach((field, fieldValue) -> { values.forEach((field, fieldValue) -> {
if (!tableConf.enableCover() && fieldValue == null) {
return;
}
String position = titlePostionMap.get(field); String position = titlePostionMap.get(field);
if (fieldValue instanceof FileData) { if (fieldValue instanceof FileData) {
FileData fileData = (FileData) fieldValue; FileData fileData = (FileData) fieldValue;
String fileType = fileData.getFileType(); String fileType = fileData.getFileType();
@ -226,16 +226,15 @@ public class FsHelper {
fileDataList.add(fileData); fileDataList.add(fileData);
} }
} }
if (tableConf.enableCover() || fieldValue != null) {
resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get()) resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get())
.addRow(GenerateUtil.getRowData(fieldValue)); .addRow(GenerateUtil.getRowData(fieldValue));
}
}); });
} else { } else {
int rowCou = rowCount.incrementAndGet(); int rowCou = rowCount.incrementAndGet();
values.forEach((field, fieldValue) -> { values.forEach((field, fieldValue) -> {
if (!tableConf.enableCover() && fieldValue == null) {
return;
}
String position = titlePostionMap.get(field); String position = titlePostionMap.get(field);
if (fieldValue instanceof FileData) { if (fieldValue instanceof FileData) {
FileData fileData = (FileData) fieldValue; FileData fileData = (FileData) fieldValue;
@ -244,15 +243,18 @@ public class FsHelper {
fileData.setPosition(position + rowCou); fileData.setPosition(position + rowCou);
fileDataList.add(fileData); fileDataList.add(fileData);
} }
if (tableConf.enableCover() || fieldValue != null) {
resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou) resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou)
.addRow(GenerateUtil.getRowData(fieldValue)); .addRow(GenerateUtil.getRowData(fieldValue));
}
}); });
} }
} }
int rowTotal = sheet.getGridProperties().getRowCount(); int rowTotal = sheet.getGridProperties().getRowCount();
int rowNum = rowCount.get(); int rowNum = rowCount.get();
if (rowNum > rowTotal) { if (rowNum >= rowTotal) {
FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, client); FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, client);
} }

@ -1,5 +1,6 @@
package cn.isliu.core; package cn.isliu.core;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
public class FsTableData { public class FsTableData {
@ -7,6 +8,7 @@ public class FsTableData {
private Integer row; private Integer row;
private String uniqueId; private String uniqueId;
private Object data; private Object data;
private Map<String, String> fieldsPositionMap;
public FsTableData() { public FsTableData() {
} }
@ -17,6 +19,13 @@ public class FsTableData {
this.data = data; this.data = data;
} }
public FsTableData(Integer row, String uniqueId, Object data, Map<String, String> fieldsPositionMap) {
this.row = row;
this.uniqueId = uniqueId;
this.data = data;
this.fieldsPositionMap = fieldsPositionMap;
}
public Integer getRow() { public Integer getRow() {
return row; return row;
} }
@ -41,16 +50,24 @@ public class FsTableData {
this.data = data; this.data = data;
} }
public Map<String, String> getFieldsPositionMap() {
return fieldsPositionMap;
}
public void setFieldsPositionMap(Map<String, String> fieldsPositionMap) {
this.fieldsPositionMap = fieldsPositionMap;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
FsTableData that = (FsTableData) o; 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 @Override
public int hashCode() { public int hashCode() {
return Objects.hash(row, uniqueId, data); return Objects.hash(row, uniqueId, data, fieldsPositionMap);
} }
@Override @Override
@ -59,6 +76,7 @@ public class FsTableData {
"row=" + row + "row=" + row +
", uniqueId='" + uniqueId + '\'' + ", uniqueId='" + uniqueId + '\'' +
", data=" + data + ", data=" + data +
", fieldsPositionMap=" + fieldsPositionMap +
'}'; '}';
} }
} }

@ -9,6 +9,14 @@ import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Inherited @Inherited
public @interface TableConf { public @interface TableConf {
/**
* 表格唯一键
*
* @return 表格唯一键
*/
String[] uniKeys() default {};
/** /**
* 表头行数 * 表头行数
* *

@ -74,7 +74,7 @@ public class ReadBuilder<T> {
List<String> processedIgnoreFields = processIgnoreFields(fieldsMap); List<String> processedIgnoreFields = processIgnoreFields(fieldsMap);
// 使用支持忽略字段的方法获取表格数据 // 使用支持忽略字段的方法获取表格数据
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields); List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields, fieldsMap);
List<String> fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList()); List<String> fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList());
@ -97,6 +97,46 @@ public class ReadBuilder<T> {
return results; return results;
} }
public Map<String, List<T>> groupBuild() {
Map<String, List<T>> results = new HashMap<>();
FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
TableConf tableConf = PropertyUtil.getTableConf(clazz);
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
// 处理忽略字段名称映射
List<String> processedIgnoreFields = processIgnoreFields(fieldsMap);
// 使用支持忽略字段的方法获取表格数据
Map<String, List<FsTableData>> fsTableDataMap = FsTableUtil.getGroupFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields, fieldsMap);
List<String> fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList());
fsTableDataMap.forEach((key, fsTableDataList) -> {
List<T> groupResults = new ArrayList<>();
fsTableDataList.stream().filter(tableData -> tableData.getRow() >= tableConf.headLine()).forEach(tableData -> {
Object data = tableData.getData();
if (data instanceof HashMap) {
Map<String, Object> rowData = (HashMap<String, Object>) data;
JsonObject jsonObject = JSONUtil.convertMapToJsonObject(rowData);
Map<String, Object> 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;
}
/** /**
* 处理忽略字段名称映射 * 处理忽略字段名称映射
* *

@ -10,10 +10,7 @@ import cn.isliu.core.utils.FsTableUtil;
import cn.isliu.core.utils.PropertyUtil; import cn.isliu.core.utils.PropertyUtil;
import cn.isliu.core.utils.StringUtil; import cn.isliu.core.utils.StringUtil;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -116,7 +113,9 @@ public class SheetBuilder<T> {
* @return SheetBuilder实例支持链式调用 * @return SheetBuilder实例支持链式调用
*/ */
public SheetBuilder<T> fieldDescription(Map<String, String> fieldDescriptions) { public SheetBuilder<T> fieldDescription(Map<String, String> fieldDescriptions) {
if (fieldDescriptions != null && !fieldDescriptions.isEmpty()) {
this.fieldDescriptions.putAll(fieldDescriptions); this.fieldDescriptions.putAll(fieldDescriptions);
}
return this; return this;
} }
@ -172,21 +171,21 @@ public class SheetBuilder<T> {
// 2添加表头数据 // 2添加表头数据
FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions), client); FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions), client);
// 3设置表格样式 // 3设置单元格为文本格式
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), client, spreadsheetToken);
// 4合并单元格
List<CustomCellService.CellRequest> mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap);
if (!mergeCell.isEmpty()) {
mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken));
}
// 5设置单元格为文本格式
if (tableConf.isText()) { if (tableConf.isText()) {
String column = FsTableUtil.getColumnNameByNuNumber(headers.size()); String column = FsTableUtil.getColumnNameByNuNumber(headers.size());
FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken); FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
} }
// 4设置表格样式
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), client, spreadsheetToken);
// 5合并单元格
List<CustomCellService.CellRequest> mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap);
if (!mergeCell.isEmpty()) {
mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken));
}
// 6设置表格下拉 // 6设置表格下拉
try { try {
FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc(), customProperties); FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc(), customProperties);
@ -197,6 +196,57 @@ public class SheetBuilder<T> {
return sheetId; return sheetId;
} }
public String groupBuild(String ...groupFields) {
// 获取所有字段映射
Map<String, FieldProperty> allFieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
// 根据includeFields过滤字段映射
Map<String, FieldProperty> fieldsMap = filterFieldsMap(allFieldsMap);
// 生成表头
List<String> 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<String> groupFieldList = new ArrayList<>(Arrays.asList(groupFields));
List<String> 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<String, String[]> positions = FsTableUtil.calculateGroupPositions(headers, groupFieldList);
positions.forEach((key, value) -> FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, value, tableConf), client, spreadsheetToken));
// 5合并单元格
List<CustomCellService.CellRequest> 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<T> {
if (field.isEmpty()) { if (field.isEmpty()) {
return false; 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)); .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} }

@ -13,10 +13,7 @@ import cn.isliu.core.pojo.FieldProperty;
import cn.isliu.core.service.CustomValueService; import cn.isliu.core.service.CustomValueService;
import cn.isliu.core.utils.*; import cn.isliu.core.utils.*;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -34,6 +31,7 @@ public class WriteBuilder<T> {
private List<String> ignoreUniqueFields; private List<String> ignoreUniqueFields;
private Class<?> clazz; private Class<?> clazz;
private boolean ignoreNotFound; private boolean ignoreNotFound;
private String groupField;
/** /**
* 构造函数 * 构造函数
@ -48,6 +46,7 @@ public class WriteBuilder<T> {
this.dataList = dataList; this.dataList = dataList;
this.clazz = null; this.clazz = null;
this.ignoreNotFound = false; this.ignoreNotFound = false;
this.groupField = null;
} }
/** /**
@ -93,6 +92,20 @@ public class WriteBuilder<T> {
return this; return this;
} }
/**
* 设置分组字段
*
* 配置分组字段用于处理数据行分组
* 当数据行存在分组字段时将按照分组字段进行分组并分别处理每个分组
*
* @param groupField 分组字段名称
* @return WriteBuilder实例支持链式调用
*/
public WriteBuilder<T> groupField(String groupField) {
this.groupField = groupField;
return this;
}
/** /**
* 执行数据写入并返回操作结果 * 执行数据写入并返回操作结果
* *
@ -107,45 +120,64 @@ public class WriteBuilder<T> {
Class<?> aClass = clazz; Class<?> aClass = clazz;
Map<String, FieldProperty> fieldsMap; Map<String, FieldProperty> fieldsMap;
TableConf tableConf = PropertyUtil.getTableConf(aClass);
Map<String, String> fieldMap = new HashMap<>(); Map<String, String> fieldMap = new HashMap<>();
Class<?> sourceClass = dataList.get(0).getClass(); Class<?> sourceClass = dataList.get(0).getClass();
if (aClass.equals(sourceClass)) { if (aClass != null && aClass.equals(sourceClass)) {
fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass); fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass);
} else { } else {
fieldsMap = PropertyUtil.getTablePropertyFieldsMap(sourceClass); 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<String, String> titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf);
Set<String> keys = titlePostionMap.keySet();
fieldsMap.forEach((field, fieldProperty) -> {
if (keys.contains(field)) {
fieldMap.put(field, fieldProperty.getField());
}
});
// 处理忽略字段名称映射 // 处理忽略字段名称映射
List<String> processedIgnoreFields = processIgnoreFields(fieldsMap); List<String> processedIgnoreFields = processIgnoreFields(fieldsMap);
FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
// 使用支持忽略字段的方法获取表格数据 // 使用支持忽略字段的方法获取表格数据
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields); List<FsTableData> fsTableDataList;
Map<String, Integer> currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow)); if (groupField == null) {
fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields, fieldsMap);
} else {
Map<String, List<FsTableData>> groupFsTableData = FsTableUtil.getGroupFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields, fieldsMap);
fsTableDataList = groupFsTableData.get(groupField);
}
final Integer[] row = {0}; if (!fsTableDataList.isEmpty()) {
Map<String, String> fieldsPositionMap = fsTableDataList.get(0).getFieldsPositionMap();
if (fieldsPositionMap != null) {
titlePostionMap = fieldsPositionMap;
}
}
Map<String, Integer> currTableRowMap = fsTableDataList.stream()
.filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine())
.collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow));
final Integer[] row = {tableConf.headLine()};
fsTableDataList.forEach(fsTableData -> { fsTableDataList.forEach(fsTableData -> {
if (fsTableData.getRow() > row[0]) { if ((fsTableData.getRow() + 1) > row[0]) {
row[0] = fsTableData.getRow(); row[0] = fsTableData.getRow() + 1;
} }
}); });
Map<String, String> titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf);
// 初始化批量插入对象 // 初始化批量插入对象
CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues(); CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
List<FileData> fileDataList = new ArrayList<>(); List<FileData> fileDataList = new ArrayList<>();
AtomicInteger rowCount = new AtomicInteger(row[0] + 1); AtomicInteger rowCount = new AtomicInteger(row[0]);
for (T data : dataList) { for (T data : dataList) {
Map<String, Object> values = GenerateUtil.getFieldValue(data, fieldMap); Map<String, Object> values = GenerateUtil.getFieldValue(data, fieldMap);
@ -154,20 +186,17 @@ public class WriteBuilder<T> {
String uniqueId; String uniqueId;
if (data.getClass().equals(aClass)) { if (data.getClass().equals(aClass)) {
// 类型相同使用忽略字段逻辑计算唯一标识 // 类型相同使用忽略字段逻辑计算唯一标识
uniqueId = calculateUniqueIdWithIgnoreFields(data, processedIgnoreFields, aClass); uniqueId = calculateUniqueIdWithIgnoreFields(data, processedIgnoreFields, tableConf);
} else { } else {
uniqueId = GenerateUtil.getUniqueId(data); uniqueId = GenerateUtil.getUniqueId(data, tableConf);
} }
AtomicReference<Integer> rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId)); AtomicReference<Integer> rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId));
if (uniqueId != null && rowNum.get() != null) { if (uniqueId != null && rowNum.get() != null) {
rowNum.set(rowNum.get() + 1); rowNum.set(rowNum.get() + 1);
Map<String, String> finalTitlePostionMap = titlePostionMap;
values.forEach((field, fieldValue) -> { values.forEach((field, fieldValue) -> {
if (!tableConf.enableCover() && fieldValue == null) { String position = finalTitlePostionMap.get(field);
return;
}
String position = titlePostionMap.get(field);
if (fieldValue instanceof FileData) { if (fieldValue instanceof FileData) {
FileData fileData = (FileData) fieldValue; FileData fileData = (FileData) fieldValue;
@ -179,17 +208,17 @@ public class WriteBuilder<T> {
fileDataList.add(fileData); fileDataList.add(fileData);
} }
} }
if (tableConf.enableCover() || fieldValue != null) {
resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get()) resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get())
.addRow(GenerateUtil.getRowData(fieldValue)); .addRow(GenerateUtil.getRowData(fieldValue));
}
}); });
} else if (!ignoreNotFound) { } else if (!ignoreNotFound) {
int rowCou = rowCount.incrementAndGet(); int rowCou = rowCount.incrementAndGet();
Map<String, String> finalTitlePostionMap1 = titlePostionMap;
values.forEach((field, fieldValue) -> { values.forEach((field, fieldValue) -> {
if (!tableConf.enableCover() && fieldValue == null) {
return;
}
String position = titlePostionMap.get(field); String position = finalTitlePostionMap1.get(field);
if (fieldValue instanceof FileData) { if (fieldValue instanceof FileData) {
FileData fileData = (FileData) fieldValue; FileData fileData = (FileData) fieldValue;
fileData.setSheetId(sheetId); fileData.setSheetId(sheetId);
@ -197,15 +226,18 @@ public class WriteBuilder<T> {
fileData.setPosition(position + rowCou); fileData.setPosition(position + rowCou);
fileDataList.add(fileData); fileDataList.add(fileData);
} }
if (tableConf.enableCover() || fieldValue != null) {
resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou) resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou)
.addRow(GenerateUtil.getRowData(fieldValue)); .addRow(GenerateUtil.getRowData(fieldValue));
}
}); });
} }
} }
int rowTotal = sheet.getGridProperties().getRowCount(); int rowTotal = sheet.getGridProperties().getRowCount();
int rowNum = rowCount.get(); int rowNum = rowCount.get();
if (rowNum > rowTotal) { if (rowNum >= rowTotal) {
FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, client); FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, client);
} }
@ -264,17 +296,17 @@ public class WriteBuilder<T> {
* *
* @param data 数据对象 * @param data 数据对象
* @param processedIgnoreFields 处理后的忽略字段列表 * @param processedIgnoreFields 处理后的忽略字段列表
* @param clazz 用于解析注解的实体类 * @param tableConf 用于解析注解的表格配置
* @return 唯一标识 * @return 唯一标识
*/ */
private String calculateUniqueIdWithIgnoreFields(T data, List<String> processedIgnoreFields, Class<?> clazz) { private String calculateUniqueIdWithIgnoreFields(T data, List<String> processedIgnoreFields, TableConf tableConf) {
try { try {
// 获取所有字段值 // 获取所有字段值
Map<String, Object> allFieldValues = GenerateUtil.getFieldValue(data, new HashMap<>()); Map<String, Object> allFieldValues = GenerateUtil.getFieldValue(data, new HashMap<>());
// 如果不需要忽略字段使用原有逻辑 // 如果不需要忽略字段使用原有逻辑
if (processedIgnoreFields.isEmpty()) { if (tableConf.uniKeys().length > 0 || processedIgnoreFields.isEmpty()) {
return GenerateUtil.getUniqueId(data); return GenerateUtil.getUniqueId(data, tableConf);
} }
// 移除忽略字段后计算唯一标识 // 移除忽略字段后计算唯一标识
@ -287,7 +319,7 @@ public class WriteBuilder<T> {
} catch (Exception e) { } catch (Exception e) {
// 如果计算失败回退到原有逻辑 // 如果计算失败回退到原有逻辑
return GenerateUtil.getUniqueId(data); return GenerateUtil.getUniqueId(data, tableConf);
} }
} }
} }

@ -23,8 +23,14 @@ public class FeishuClient {
private final String appId; private final String appId;
private final String appSecret; private final String appSecret;
// 服务管理器用于统一管理自定义服务实例 // 自定义服务处理官方SDK未覆盖的API
private final ServiceManager<FeishuClient> serviceManager = new ServiceManager<>(this); 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) { private FeishuClient(String appId, String appSecret, Client officialClient, OkHttpClient httpClient) {
this.appId = appId; this.appId = appId;
@ -68,7 +74,14 @@ public class FeishuClient {
* @return 扩展表格服务 * @return 扩展表格服务
*/ */
public CustomSheetService customSheets() { 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 扩展行列服务 * @return 扩展行列服务
*/ */
public CustomDimensionService customDimensions() { 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 扩展单元格服务 * @return 扩展单元格服务
*/ */
public CustomCellService customCells() { 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 扩展数据值服务 * @return 扩展数据值服务
*/ */
public CustomValueService customValues() { 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 自定义数据验证服务 * @return 自定义数据验证服务
*/ */
public CustomDataValidationService customDataValidations() { 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 扩展保护范围服务 * @return 扩展保护范围服务
*/ */
public CustomProtectedDimensionService customProtectedDimensions() { 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,9 +170,15 @@ public class FeishuClient {
* @return 扩展文件服务 * @return 扩展文件服务
*/ */
public CustomFileService customFiles() { 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配置 // 默认OkHttp配置
this.httpClientBuilder = this.httpClientBuilder =
new OkHttpClient.Builder().connectTimeout(10, TimeUnit.MINUTES).readTimeout(10, TimeUnit.MINUTES) 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);
} }
/** /**

@ -1,5 +1,8 @@
package cn.isliu.core.client; package cn.isliu.core.client;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* 线程安全的飞书客户端管理器 * 线程安全的飞书客户端管理器
* 使用ThreadLocal为每个线程维护独立的客户端实例 * 使用ThreadLocal为每个线程维护独立的客户端实例
@ -8,6 +11,7 @@ public class FsClient implements AutoCloseable {
private static volatile FsClient instance; private static volatile FsClient instance;
private final ThreadLocal<FeishuClient> clientHolder = new ThreadLocal<>(); private final ThreadLocal<FeishuClient> clientHolder = new ThreadLocal<>();
private final Map<String, FeishuClient> clientMap = new ConcurrentHashMap<>();
// 私有构造函数防止外部实例化 // 私有构造函数防止外部实例化
private FsClient() { private FsClient() {
@ -55,11 +59,17 @@ public class FsClient implements AutoCloseable {
if (appSecret == null || appSecret.trim().isEmpty()) { if (appSecret == null || appSecret.trim().isEmpty()) {
throw new IllegalArgumentException("appSecret cannot be null or empty"); throw new IllegalArgumentException("appSecret cannot be null or empty");
} }
if (clientMap.containsKey(appId + "_" + appSecret)) {
FeishuClient feishuClient = clientMap.get(appId + "_" + appSecret);
clientHolder.set(feishuClient);
return feishuClient;
} else {
FeishuClient client = FeishuClient.newBuilder(appId, appSecret).build(); FeishuClient client = FeishuClient.newBuilder(appId, appSecret).build();
clientMap.put(appId + "_" + appSecret, client);
clientHolder.set(client); clientHolder.set(client);
return client; return client;
} }
}
/** /**
* 设置客户端实例用于外部已构建的客户端 * 设置客户端实例用于外部已构建的客户端

@ -28,7 +28,6 @@ import java.util.stream.Collectors;
* 包括数据处理样式设置选项设置等功能 * 包括数据处理样式设置选项设置等功能
*/ */
public class FsTableUtil { public class FsTableUtil {
/** /**
* 获取飞书表格数据 * 获取飞书表格数据
* *
@ -38,8 +37,8 @@ public class FsTableUtil {
* @param spreadsheetToken 电子表格Token * @param spreadsheetToken 电子表格Token
* @return 飞书表格数据列表 * @return 飞书表格数据列表
*/ */
public static List<FsTableData> getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf) { public static List<FsTableData> getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf, Map<String, FieldProperty> fieldsMap) {
return getFsTableData(sheet, spreadsheetToken, tableConf, new ArrayList<>()); return getFsTableData(sheet, spreadsheetToken, tableConf, new ArrayList<>(), fieldsMap);
} }
/** /**
@ -51,9 +50,129 @@ public class FsTableUtil {
* @param ignoreUniqueFields 计算唯一标识时忽略的字段列表 * @param ignoreUniqueFields 计算唯一标识时忽略的字段列表
* @return 飞书表格数据列表 * @return 飞书表格数据列表
*/ */
public static List<FsTableData> getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf, List<String> ignoreUniqueFields) { public static Map<String, List<FsTableData>> getGroupFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf, List<String> ignoreUniqueFields, Map<String, FieldProperty> fieldsMap) {
// 计算数据范围 // 计算数据范围
List<List<Object>> values = getSourceTableValues(sheet, spreadsheetToken);
// 获取飞书表格数据
TableData tableData = processSheetData(sheet, values);
String[] uniKeys = tableConf.uniKeys();
Set<String> uniKeyNames = getUniKeyNames(fieldsMap, uniKeys);
List<FsTableData> dataList = getFsTableData(tableData, ignoreUniqueFields);
Map<String, String> titleMap = new HashMap<>();
Map<String, List<String>> categoryPositionMap = new HashMap<>();
dataList.stream().filter(d -> d.getRow() == (tableConf.titleRow() - 2)).findFirst().ifPresent(d -> {
Map<String, String> map = (Map<String, String>) 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<String, String> map = (Map<String, String>) d.getData();
map.forEach((k, v) -> {
if (v != null && !v.isEmpty()) {
titleMap.put(k + "_" + v, v);
} else {
titleMap.put(k, v);
}
});
});
List<FsTableData> fsTableDataList = dataList.stream().filter(fsTableData -> fsTableData.getRow() >= (tableConf.headLine() -1)).map(item -> {
Map<String, Object> resultMap = new HashMap<>();
Map<String, Object> map = (Map<String, Object>) 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<String, List<FsTableData>> dataMap = new HashMap<>();
categoryPositionMap.forEach((k1, v1) -> {
List<FsTableData> fsList = new ArrayList<>();
for (FsTableData fsTableData : fsTableDataList) {
Map<String, Object> resultMap = new HashMap<>();
Map<String, String> fieldsPositionMap = new HashMap<>();
Map<String, Object> data = (Map<String, Object>) 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<Object> 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<String, Object> 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<String> getUniKeyNames(Map<String, FieldProperty> fieldsMap, String[] uniKeys) {
Set<String> 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<List<Object>> getSourceTableValues(Sheet sheet, String spreadsheetToken) {
GridProperties gridProperties = sheet.getGridProperties(); GridProperties gridProperties = sheet.getGridProperties();
int totalRow = gridProperties.getRowCount(); int totalRow = gridProperties.getRowCount();
int rowCount = Math.min(totalRow, 100); // 每次读取的行数 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<FsTableData> getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf, List<String> ignoreUniqueFields, Map<String, FieldProperty> fieldsMap) {
// 计算数据范围
List<List<Object>> values = getSourceTableValues(sheet, spreadsheetToken);
// 获取飞书表格数据 // 获取飞书表格数据
TableData tableData = processSheetData(sheet, values); TableData tableData = processSheetData(sheet, values);
String[] uniKeys = tableConf.uniKeys();
Set<String> uniKeyNames = getUniKeyNames(fieldsMap, uniKeys);
List<FsTableData> dataList = getFsTableData(tableData, ignoreUniqueFields); List<FsTableData> dataList = getFsTableData(tableData, ignoreUniqueFields);
Map<String, String> titleMap = new HashMap<>(); Map<String, String> titleMap = new HashMap<>();
Map<String, String> fieldsPositionMap = new HashMap<>();
dataList.stream().filter(d -> d.getRow() == (tableConf.titleRow() - 1)).findFirst() dataList.stream().filter(d -> d.getRow() == (tableConf.titleRow() - 1)).findFirst()
.ifPresent(d -> { .ifPresent(d -> {
Map<String, String> map = (Map<String, String>) d.getData(); Map<String, String> map = (Map<String, String>) d.getData();
map.forEach((k, v) -> {
if (v != null && !v.isEmpty()) {
fieldsPositionMap.put(v, k);
};
});
titleMap.putAll(map); titleMap.putAll(map);
}); });
return dataList.stream().filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine()).map(item -> { return dataList.stream().filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine()).map(item -> {
@ -103,6 +246,24 @@ public class FsTableUtil {
} }
}); });
item.setData(resultMap); item.setData(resultMap);
item.setFieldsPositionMap(fieldsPositionMap);
String jsonStr = null;
if (!uniKeyNames.isEmpty()) {
List<Object> 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; return item;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
@ -113,7 +274,7 @@ public class FsTableUtil {
* @param tableData 表格数据对象 * @param tableData 表格数据对象
* @return 飞书表格数据列表 * @return 飞书表格数据列表
*/ */
private static List<FsTableData> getFsTableData(TableData tableData) { public static List<FsTableData> getFsTableData(TableData tableData) {
return getFsTableData(tableData, new ArrayList<>()); return getFsTableData(tableData, new ArrayList<>());
} }
@ -124,7 +285,7 @@ public class FsTableUtil {
* @param ignoreUniqueFields 忽略的唯一字段列表 * @param ignoreUniqueFields 忽略的唯一字段列表
* @return 飞书表格数据列表 * @return 飞书表格数据列表
*/ */
private static List<FsTableData> getFsTableData(TableData tableData, List<String> ignoreUniqueFields) { public static List<FsTableData> getFsTableData(TableData tableData, List<String> ignoreUniqueFields) {
List<FsTableData> fsTableList = new LinkedList<>(); List<FsTableData> fsTableList = new LinkedList<>();
// 5. 访问补齐后的数据 // 5. 访问补齐后的数据
@ -381,6 +542,57 @@ public class FsTableUtil {
}); });
} }
public static void setTableOptions(String spreadsheetToken, String[] headers, Map<String, FieldProperty> fieldsMap,
String sheetId, boolean enableDesc, Map<String, Object> customProperties) {
int line = getMaxLevel(fieldsMap) + (enableDesc ? 3 : 2);
fieldsMap.forEach((field, fieldProperty) -> {
TableProperty tableProperty = fieldProperty.getTableProperty();
if (tableProperty != null) {
List<String> 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<String> result;
Class<? extends OptionsValueProcess> optionsClass = tableProperty.optionsClass();
try {
Map<String, Object> properties = new HashMap<>();
if (customProperties == null) {
properties.put("_field", fieldProperty);
} else {
customProperties.put("_field", fieldProperty);
}
OptionsValueProcess optionsValueProcess = optionsClass.getDeclaredConstructor().newInstance();
result = (List<String>) 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<String> headers, Map<String, FieldProperty> fieldsMap, String sheetId, boolean enableDesc) { public static void setTableOptions(String spreadsheetToken, List<String> headers, Map<String, FieldProperty> fieldsMap, String sheetId, boolean enableDesc) {
setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, enableDesc, null); setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, enableDesc, null);
} }
@ -390,6 +602,81 @@ public class FsTableUtil {
return getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, null); return getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, null);
} }
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers, List<String> headerList, Map<String, FieldProperty> fieldsMap,
TableConf tableConf, Map<String, String> fieldDescriptions, List<String> 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<String> headers, int size, List<String> 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<String> headers, List<String> 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<String> headers, public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
Map<String, FieldProperty> fieldsMap, TableConf tableConf, Map<String, String> fieldDescriptions) { Map<String, FieldProperty> fieldsMap, TableConf tableConf, Map<String, String> fieldDescriptions) {
@ -531,6 +818,53 @@ public class FsTableUtil {
return styleCellsBatchBuilder; 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<String, String[]> calculateGroupPositions(List<String> headers, List<String> groupFields) {
Map<String, String[]> 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<String> getGroupHeaders(List<String> groupFieldList, List<String> headers) {
List<String> headerList = new ArrayList<>();
groupFieldList.forEach(groupField -> {
if (!headerList.isEmpty()) {
headerList.add(null);
}
headerList.addAll(headers);
});
return headerList;
}
/** /**
* 根据层级分组字段属性并按order排序 * 根据层级分组字段属性并按order排序
* *
@ -688,6 +1022,22 @@ public class FsTableUtil {
return result; return result;
} }
//
public static List<CustomCellService.CellRequest> getMergeCell(String sheetId, Collection<String[]> positions) {
List<CustomCellService.CellRequest> 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<CustomCellService.CellRequest> getMergeCell(String sheetId, Map<String, FieldProperty> fieldsMap) { public static List<CustomCellService.CellRequest> getMergeCell(String sheetId, Map<String, FieldProperty> fieldsMap) {
List<CustomCellService.CellRequest> mergeRequests = new ArrayList<>(); List<CustomCellService.CellRequest> mergeRequests = new ArrayList<>();
@ -937,4 +1287,23 @@ public class FsTableUtil {
public static String addLineBreaksPer8Chars(String text) { public static String addLineBreaksPer8Chars(String text) {
return addLineBreaks(text, 8); return addLineBreaks(text, 8);
} }
public static boolean areAllValuesNullOrBlank(Map<String, Object> 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;
}
} }

@ -1,6 +1,7 @@
package cn.isliu.core.utils; package cn.isliu.core.utils;
import cn.isliu.core.FileData; import cn.isliu.core.FileData;
import cn.isliu.core.annotation.TableConf;
import cn.isliu.core.annotation.TableProperty; import cn.isliu.core.annotation.TableProperty;
import cn.isliu.core.enums.BaseEnum; import cn.isliu.core.enums.BaseEnum;
import cn.isliu.core.enums.FileType; import cn.isliu.core.enums.FileType;
@ -22,8 +23,6 @@ import java.util.stream.Collectors;
*/ */
public class GenerateUtil { public class GenerateUtil {
// 使用统一的FsLogger替代java.util.logging.Logger
/** /**
* 根据配置和数据生成DTO对象通用版本 * 根据配置和数据生成DTO对象通用版本
* *
@ -375,7 +374,7 @@ public class GenerateUtil {
try { try {
Object value = getNestedFieldValue(target, fieldPath); Object value = getNestedFieldValue(target, fieldPath);
if (value != null) { if (fieldPath.split("\\.").length == 1) {
result.put(fieldName, value); result.put(fieldName, value);
} }
} catch (Exception e) { } catch (Exception e) {
@ -481,13 +480,27 @@ public class GenerateUtil {
return fieldValue; return fieldValue;
} }
public static <T> String getUniqueId(T data) { public static <T> String getUniqueId(T data, TableConf tableConf) {
String uniqueId = null; String uniqueId = null;
try { try {
Object uniqueIdObj = GenerateUtil.getNestedFieldValue(data, "uniqueId"); Object uniqueIdObj = GenerateUtil.getNestedFieldValue(data, "uniqueId");
if (uniqueIdObj != null) { if (uniqueIdObj != null) {
uniqueId = uniqueIdObj.toString(); uniqueId = uniqueIdObj.toString();
} }
if (uniqueId == null && tableConf.uniKeys().length > 0) {
String[] uniKeys = tableConf.uniKeys();
List<Object> 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) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

@ -440,11 +440,17 @@ public class PropertyUtil {
tableConf = clazz.getAnnotation(TableConf.class); tableConf = clazz.getAnnotation(TableConf.class);
} else { } else {
tableConf = new TableConf() { tableConf = new TableConf() {
@Override @Override
public Class<? extends Annotation> annotationType() { public Class<? extends Annotation> annotationType() {
return TableConf.class; return TableConf.class;
} }
@Override
public String[] uniKeys() {
return new String[0];
}
@Override @Override
public int headLine() { public int headLine() {
return 1; return 1;

@ -147,4 +147,24 @@ public class StringUtil {
return sb.toString(); return sb.toString();
} }
public static String listToJson(List<Object> 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();
}
} }