diff --git a/pom.xml b/pom.xml
index c71954f..a4590df 100644
--- a/pom.xml
+++ b/pom.xml
@@ -151,12 +151,12 @@
-
-
-
-
-
-
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
diff --git a/src/main/java/cn/isliu/FsHelper.java b/src/main/java/cn/isliu/FsHelper.java
index c369c8d..77300d5 100644
--- a/src/main/java/cn/isliu/FsHelper.java
+++ b/src/main/java/cn/isliu/FsHelper.java
@@ -4,9 +4,9 @@ import cn.isliu.core.BaseEntity;
import cn.isliu.core.FileData;
import cn.isliu.core.FsTableData;
import cn.isliu.core.Sheet;
+import cn.isliu.core.annotation.TableConf;
import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.client.FsClient;
-import cn.isliu.core.config.FsConfig;
import cn.isliu.core.enums.ErrorCode;
import cn.isliu.core.enums.FileType;
import cn.isliu.core.logging.FsLogger;
@@ -44,25 +44,26 @@ public class FsHelper {
Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
List headers = PropertyUtil.getHeaders(fieldsMap);
+ TableConf tableConf = PropertyUtil.getTableConf(clazz);
+
FeishuClient client = FsClient.getInstance().getClient();
// 1、创建sheet
String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
// 2 添加表头数据
- FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers), client);
+ FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf), client);
// 3 设置表格样式
- FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size()), sheetId, client, spreadsheetToken);
+ FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size(), tableConf), sheetId, client, spreadsheetToken);
// 4 设置单元格为文本格式
- FsConfig fsConfig = FsConfig.getInstance();
- if (fsConfig.isCellText()) {
+ if (tableConf.isText()) {
String column = FsTableUtil.getColumnNameByNuNumber(headers.size());
FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
}
// 5 设置表格下拉
- FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId);
+ FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc());
return sheetId;
}
@@ -82,7 +83,8 @@ public class FsHelper {
List results = new ArrayList<>();
FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
- List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
+ TableConf tableConf = PropertyUtil.getTableConf(clazz);
+ List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf);
Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
List fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList());
@@ -90,11 +92,15 @@ public class FsHelper {
fsTableDataList.forEach(tableData -> {
Object data = tableData.getData();
if (data instanceof HashMap) {
- JsonObject jsonObject = JSONUtil.convertHashMapToJsonObject((HashMap) data);
+ 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) t).setUniqueId(tableData.getUniqueId());
+ BaseEntity baseEntity = (BaseEntity) t;
+ baseEntity.setUniqueId(tableData.getUniqueId());
+ baseEntity.setRow(tableData.getRow());
+ baseEntity.setRowData(rowData);
}
results.add(t);
}
@@ -120,10 +126,11 @@ public class FsHelper {
Class> aClass = dataList.get(0).getClass();
Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass);
+ TableConf tableConf = PropertyUtil.getTableConf(aClass);
FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
- List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
+ List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf);
Map currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow));
final Integer[] row = {0};
@@ -133,7 +140,7 @@ public class FsHelper {
}
});
- Map titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken);
+ Map titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf);
Map fieldMap = new HashMap<>();
fieldsMap.forEach((field, fieldProperty) -> fieldMap.put(field, fieldProperty.getField()));
@@ -142,7 +149,6 @@ public class FsHelper {
CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
List fileDataList = new ArrayList<>();
- FsConfig fsConfig = FsConfig.getInstance();
AtomicInteger rowCount = new AtomicInteger(row[0] + 1);
@@ -155,7 +161,7 @@ public class FsHelper {
if (uniqueId != null && rowNum.get() != null) {
rowNum.set(rowNum.get() + 1);
values.forEach((field, fieldValue) -> {
- if (!fsConfig.isCover() && fieldValue == null) {
+ if (!tableConf.enableCover() && fieldValue == null) {
return;
}
@@ -177,7 +183,7 @@ public class FsHelper {
} else {
int rowCou = rowCount.incrementAndGet();
values.forEach((field, fieldValue) -> {
- if (!fsConfig.isCover() && fieldValue == null) {
+ if (!tableConf.enableCover() && fieldValue == null) {
return;
}
diff --git a/src/main/java/cn/isliu/core/BaseEntity.java b/src/main/java/cn/isliu/core/BaseEntity.java
index 4c33e89..391c9cc 100644
--- a/src/main/java/cn/isliu/core/BaseEntity.java
+++ b/src/main/java/cn/isliu/core/BaseEntity.java
@@ -1,5 +1,8 @@
package cn.isliu.core;
+import java.util.Map;
+import java.util.Objects;
+
/**
* 实体类基类
*
@@ -12,6 +15,14 @@ public abstract class BaseEntity {
* 唯一标识符,用于标识表格中的行数据
*/
public String uniqueId;
+ /**
+ * 行号,用于标识表格中的行位置
+ */
+ private Integer row;
+ /**
+ * 行数据,用于存储与表格行相关的信息
+ */
+ private Map rowData;
/**
* 获取唯一标识符
@@ -30,4 +41,32 @@ public abstract class BaseEntity {
public void setUniqueId(String uniqueId) {
this.uniqueId = uniqueId;
}
+
+ public Integer getRow() {
+ return row;
+ }
+
+ public void setRow(Integer row) {
+ this.row = row;
+ }
+
+ public Map getRowData() {
+ return rowData;
+ }
+
+ public void setRowData(Map rowData) {
+ this.rowData = rowData;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ BaseEntity that = (BaseEntity) o;
+ return Objects.equals(uniqueId, that.uniqueId) && Objects.equals(row, that.row) && Objects.equals(rowData, that.rowData);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uniqueId, row, rowData);
+ }
}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/annotation/TableConf.java b/src/main/java/cn/isliu/core/annotation/TableConf.java
new file mode 100644
index 0000000..4e54c4c
--- /dev/null
+++ b/src/main/java/cn/isliu/core/annotation/TableConf.java
@@ -0,0 +1,60 @@
+package cn.isliu.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 表格配置注解
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TableConf {
+ /**
+ * 表头行数
+ *
+ * @return 表头行数
+ */
+ int headLine() default 1;
+
+ /**
+ * 标题行数
+ *
+ * @return 标题行数
+ */
+ int titleRow() default 1;
+
+ /**
+ * 是否覆盖已存在数据
+ *
+ * @return 是否覆盖
+ */
+ boolean enableCover() default false;
+
+ /**
+ * 是否设置表格为纯文本
+ *
+ * @return 是否设置表格为纯文本
+ */
+ boolean isText() default false;
+
+ /**
+ * 是否开启字段描述
+ *
+ * @return 是否开启字段描述
+ */
+ boolean enableDesc() default false;
+
+ /**
+ * 字体颜色
+ *
+ * @return 字体颜色
+ */
+ String headFontColor() default "#ffffff";
+
+ /**
+ * 背景颜色
+ *
+ * @return 背景颜色
+ */
+ String headBackColor() default "#000000";
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/annotation/TableProperty.java b/src/main/java/cn/isliu/core/annotation/TableProperty.java
index 80dd366..f04f8d9 100644
--- a/src/main/java/cn/isliu/core/annotation/TableProperty.java
+++ b/src/main/java/cn/isliu/core/annotation/TableProperty.java
@@ -23,7 +23,14 @@ public @interface TableProperty {
*
* @return 列名字符串
*/
- String value() default "";
+ String[] value() default {};
+
+ /**
+ * 字段描述
+ *
+ * @return 字段描述字符串或字符串数组
+ */
+ String desc() default "";
/**
* 字段名
diff --git a/src/main/java/cn/isliu/core/config/ConfigBuilder.java b/src/main/java/cn/isliu/core/config/ConfigBuilder.java
deleted file mode 100644
index 4ca6076..0000000
--- a/src/main/java/cn/isliu/core/config/ConfigBuilder.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package cn.isliu.core.config;
-
-/**
- * 配置构建器,用于批量配置更新
- */
-public class ConfigBuilder {
-
- Integer headLine;
- Integer titleLine;
- Boolean isCover;
- Boolean cellText;
- String foreColor;
- String backColor;
-
- public ConfigBuilder() {
- }
-
- public ConfigBuilder headLine(int headLine) {
- if (headLine < 0) {
- throw new IllegalArgumentException("headLine must be non-negative, got: " + headLine);
- }
- this.headLine = headLine;
- return this;
- }
-
- public ConfigBuilder titleLine(int titleLine) {
- if (titleLine < 0) {
- throw new IllegalArgumentException("titleLine must be non-negative, got: " + titleLine);
- }
- this.titleLine = titleLine;
- return this;
- }
-
- public ConfigBuilder isCover(boolean isCover) {
- this.isCover = isCover;
- return this;
- }
-
- public ConfigBuilder cellText(boolean cellText) {
- this.cellText = cellText;
- return this;
- }
-
- public ConfigBuilder foreColor(String foreColor) {
- if (foreColor == null) {
- throw new IllegalArgumentException("foreColor cannot be null");
- }
- if (!isValidColor(foreColor)) {
- throw new IllegalArgumentException("Invalid foreColor format: " + foreColor);
- }
- this.foreColor = foreColor;
- return this;
- }
-
- public ConfigBuilder backColor(String backColor) {
- if (backColor == null) {
- throw new IllegalArgumentException("backColor cannot be null");
- }
- if (!isValidColor(backColor)) {
- throw new IllegalArgumentException("Invalid backColor format: " + backColor);
- }
- this.backColor = backColor;
- return this;
- }
-
- /**
- * 验证颜色格式
- * @param color 颜色值
- * @return 是否为有效的颜色格式
- */
- private boolean isValidColor(String color) {
- if (color == null || color.trim().isEmpty()) {
- return false;
- }
- // 简单的十六进制颜色验证 #RRGGBB
- return color.matches("^#[0-9A-Fa-f]{6}$");
- }
-
- /**
- * 应用配置到ThreadSafeConfig
- */
- public void apply() {
- FsConfig.getInstance().updateConfig(this);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/config/ConfigChangeEvent.java b/src/main/java/cn/isliu/core/config/ConfigChangeEvent.java
deleted file mode 100644
index 446a995..0000000
--- a/src/main/java/cn/isliu/core/config/ConfigChangeEvent.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package cn.isliu.core.config;
-
-/**
- * 配置变更事件
- */
-public class ConfigChangeEvent {
-
- private final ConfigSnapshot oldSnapshot;
- private final ConfigSnapshot newSnapshot;
- private final long timestamp;
-
- /**
- * 创建配置变更事件
- * @param oldSnapshot 旧配置快照
- * @param newSnapshot 新配置快照
- */
- public ConfigChangeEvent(ConfigSnapshot oldSnapshot, ConfigSnapshot newSnapshot) {
- this.oldSnapshot = oldSnapshot;
- this.newSnapshot = newSnapshot;
- this.timestamp = System.currentTimeMillis();
- }
-
- /**
- * 获取旧配置快照
- * @return 旧配置快照
- */
- public ConfigSnapshot getOldSnapshot() {
- return oldSnapshot;
- }
-
- /**
- * 获取新配置快照
- * @return 新配置快照
- */
- public ConfigSnapshot getNewSnapshot() {
- return newSnapshot;
- }
-
- /**
- * 获取事件时间戳
- * @return 时间戳
- */
- public long getTimestamp() {
- return timestamp;
- }
-
- /**
- * 检查指定字段是否发生变更
- * @param fieldName 字段名
- * @return 是否发生变更
- */
- public boolean hasChanged(String fieldName) {
- switch (fieldName.toLowerCase()) {
- case "headline":
- return oldSnapshot.getHeadLine() != newSnapshot.getHeadLine();
- case "titleline":
- return oldSnapshot.getTitleLine() != newSnapshot.getTitleLine();
- case "iscover":
- return oldSnapshot.isCover() != newSnapshot.isCover();
- case "celltext":
- return oldSnapshot.isCellText() != newSnapshot.isCellText();
- case "forecolor":
- return !oldSnapshot.getForeColor().equals(newSnapshot.getForeColor());
- case "backcolor":
- return !oldSnapshot.getBackColor().equals(newSnapshot.getBackColor());
- default:
- return false;
- }
- }
-
- @Override
- public String toString() {
- return "ConfigChangeEvent{" +
- "oldSnapshot=" + oldSnapshot +
- ", newSnapshot=" + newSnapshot +
- ", timestamp=" + timestamp +
- '}';
- }
-}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/config/ConfigChangeListener.java b/src/main/java/cn/isliu/core/config/ConfigChangeListener.java
deleted file mode 100644
index fe20a2c..0000000
--- a/src/main/java/cn/isliu/core/config/ConfigChangeListener.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package cn.isliu.core.config;
-
-/**
- * 配置变更监听器接口
- */
-public interface ConfigChangeListener {
-
- /**
- * 配置变更时的回调方法
- * @param event 配置变更事件
- */
- void onConfigChanged(ConfigChangeEvent event);
-}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/config/ConfigSnapshot.java b/src/main/java/cn/isliu/core/config/ConfigSnapshot.java
deleted file mode 100644
index 772162c..0000000
--- a/src/main/java/cn/isliu/core/config/ConfigSnapshot.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package cn.isliu.core.config;
-
-/**
- * 不可变的配置快照
- * 用于线程安全的配置读取
- */
-public class ConfigSnapshot {
-
- private final int headLine;
- private final int titleLine;
- private final boolean isCover;
- private final boolean cellText;
- private final String foreColor;
- private final String backColor;
- private final long timestamp;
-
- /**
- * 创建配置快照
- * @param headLine 头部行数
- * @param titleLine 标题行数
- * @param isCover 是否覆盖
- * @param cellText 是否单元格文本
- * @param foreColor 前景色
- * @param backColor 背景色
- */
- public ConfigSnapshot(int headLine, int titleLine, boolean isCover,
- boolean cellText, String foreColor, String backColor) {
- this.headLine = headLine;
- this.titleLine = titleLine;
- this.isCover = isCover;
- this.cellText = cellText;
- this.foreColor = foreColor;
- this.backColor = backColor;
- this.timestamp = System.currentTimeMillis();
- }
-
- public int getHeadLine() {
- return headLine;
- }
-
- public int getTitleLine() {
- return titleLine;
- }
-
- public boolean isCover() {
- return isCover;
- }
-
- public boolean isCellText() {
- return cellText;
- }
-
- public String getForeColor() {
- return foreColor;
- }
-
- public String getBackColor() {
- return backColor;
- }
-
- public long getTimestamp() {
- return timestamp;
- }
-
- @Override
- public String toString() {
- return "ConfigSnapshot{" +
- "headLine=" + headLine +
- ", titleLine=" + titleLine +
- ", isCover=" + isCover +
- ", cellText=" + cellText +
- ", foreColor='" + foreColor + '\'' +
- ", backColor='" + backColor + '\'' +
- ", timestamp=" + timestamp +
- '}';
- }
-}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/config/FsConfig.java b/src/main/java/cn/isliu/core/config/FsConfig.java
deleted file mode 100644
index 6d94ee2..0000000
--- a/src/main/java/cn/isliu/core/config/FsConfig.java
+++ /dev/null
@@ -1,302 +0,0 @@
-package cn.isliu.core.config;
-
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-/**
- * 线程安全的配置管理器
- * 使用volatile关键字和ReadWriteLock确保线程安全
- */
-public class FsConfig {
-
- // 使用volatile确保可见性
- private volatile int headLine = 1;
- private volatile int titleLine = 1;
- private volatile boolean isCover = false;
- private volatile boolean cellText = false;
- private volatile String foreColor = "#000000";
- private volatile String backColor = "#d5d5d5";
-
- // 读写锁保护配置更新操作
- private final ReadWriteLock lock = new ReentrantReadWriteLock();
-
- // 配置变更监听器列表
- private final List listeners = new CopyOnWriteArrayList<>();
-
- // 单例实例
- private static volatile FsConfig instance;
-
- private FsConfig() {
- }
-
- /**
- * 获取单例实例
- * @return ThreadSafeConfig实例
- */
- public static FsConfig getInstance() {
- if (instance == null) {
- synchronized (FsConfig.class) {
- if (instance == null) {
- instance = new FsConfig();
- }
- }
- }
- return instance;
- }
-
- /**
- * 原子性配置更新
- * @param builder 配置构建器
- */
- public void updateConfig(ConfigBuilder builder) {
- // 验证配置
- validateConfig(builder);
-
- ConfigSnapshot oldSnapshot = getSnapshot();
-
- lock.writeLock().lock();
- try {
- if (builder.headLine != null) {
- this.headLine = builder.headLine;
- }
- if (builder.titleLine != null) {
- this.titleLine = builder.titleLine;
- }
- if (builder.isCover != null) {
- this.isCover = builder.isCover;
- }
- if (builder.cellText != null) {
- this.cellText = builder.cellText;
- }
- if (builder.foreColor != null) {
- this.foreColor = builder.foreColor;
- }
- if (builder.backColor != null) {
- this.backColor = builder.backColor;
- }
- } finally {
- lock.writeLock().unlock();
- }
-
- // 通知配置变更
- ConfigSnapshot newSnapshot = getSnapshot();
- notifyConfigChange(oldSnapshot, newSnapshot);
- }
-
- /**
- * 验证配置参数
- * @param builder 配置构建器
- * @throws IllegalArgumentException 如果配置无效
- */
- private void validateConfig(ConfigBuilder builder) {
- if (builder.headLine != null && builder.headLine < 0) {
- throw new IllegalArgumentException("headLine must be non-negative, got: " + builder.headLine);
- }
- if (builder.titleLine != null && builder.titleLine < 0) {
- throw new IllegalArgumentException("titleLine must be non-negative, got: " + builder.titleLine);
- }
- if (builder.foreColor != null && !isValidColor(builder.foreColor)) {
- throw new IllegalArgumentException("Invalid foreColor format: " + builder.foreColor);
- }
- if (builder.backColor != null && !isValidColor(builder.backColor)) {
- throw new IllegalArgumentException("Invalid backColor format: " + builder.backColor);
- }
- }
-
- /**
- * 验证颜色格式
- * @param color 颜色值
- * @return 是否为有效的颜色格式
- */
- private boolean isValidColor(String color) {
- if (color == null || color.trim().isEmpty()) {
- return false;
- }
- // 简单的十六进制颜色验证 #RRGGBB
- return color.matches("^#[0-9A-Fa-f]{6}$");
- }
-
- /**
- * 通知配置变更
- * @param oldSnapshot 旧配置快照
- * @param newSnapshot 新配置快照
- */
- private void notifyConfigChange(ConfigSnapshot oldSnapshot, ConfigSnapshot newSnapshot) {
- if (!listeners.isEmpty()) {
- ConfigChangeEvent event = new ConfigChangeEvent(oldSnapshot, newSnapshot);
- for (ConfigChangeListener listener : listeners) {
- try {
- listener.onConfigChanged(event);
- } catch (Exception e) {
- // 记录异常但不影响配置更新
- System.err.println("Error notifying config change listener: " + e.getMessage());
- }
- }
- }
- }
-
- /**
- * 添加配置变更监听器
- * @param listener 监听器
- */
- public void addConfigChangeListener(ConfigChangeListener listener) {
- if (listener != null) {
- listeners.add(listener);
- }
- }
-
- /**
- * 移除配置变更监听器
- * @param listener 监听器
- */
- public void removeConfigChangeListener(ConfigChangeListener listener) {
- listeners.remove(listener);
- }
-
- /**
- * 线程安全的配置读取 - 获取配置快照
- * @return 不可变的配置快照
- */
- public ConfigSnapshot getSnapshot() {
- lock.readLock().lock();
- try {
- return new ConfigSnapshot(headLine, titleLine, isCover, cellText, foreColor, backColor);
- } finally {
- lock.readLock().unlock();
- }
- }
-
- // 单独的getter方法,使用volatile保证可见性
- public int getHeadLine() {
- return headLine;
- }
-
- public int getTitleLine() {
- return titleLine;
- }
-
- public boolean isCover() {
- return isCover;
- }
-
- public boolean isCellText() {
- return cellText;
- }
-
- public String getForeColor() {
- return foreColor;
- }
-
- public String getBackColor() {
- return backColor;
- }
-
- // 单独的setter方法,使用写锁保护
- public void setHeadLine(int headLine) {
- if (headLine < 0) {
- throw new IllegalArgumentException("headLine must be non-negative, got: " + headLine);
- }
-
- ConfigSnapshot oldSnapshot = getSnapshot();
-
- lock.writeLock().lock();
- try {
- this.headLine = headLine;
- } finally {
- lock.writeLock().unlock();
- }
-
- ConfigSnapshot newSnapshot = getSnapshot();
- notifyConfigChange(oldSnapshot, newSnapshot);
- }
-
- public void setTitleLine(int titleLine) {
- if (titleLine < 0) {
- throw new IllegalArgumentException("titleLine must be non-negative, got: " + titleLine);
- }
-
- ConfigSnapshot oldSnapshot = getSnapshot();
-
- lock.writeLock().lock();
- try {
- this.titleLine = titleLine;
- } finally {
- lock.writeLock().unlock();
- }
-
- ConfigSnapshot newSnapshot = getSnapshot();
- notifyConfigChange(oldSnapshot, newSnapshot);
- }
-
- public void setIsCover(boolean isCover) {
- ConfigSnapshot oldSnapshot = getSnapshot();
-
- lock.writeLock().lock();
- try {
- this.isCover = isCover;
- } finally {
- lock.writeLock().unlock();
- }
-
- ConfigSnapshot newSnapshot = getSnapshot();
- notifyConfigChange(oldSnapshot, newSnapshot);
- }
-
- public void setCellText(boolean cellText) {
- ConfigSnapshot oldSnapshot = getSnapshot();
-
- lock.writeLock().lock();
- try {
- this.cellText = cellText;
- } finally {
- lock.writeLock().unlock();
- }
-
- ConfigSnapshot newSnapshot = getSnapshot();
- notifyConfigChange(oldSnapshot, newSnapshot);
- }
-
- public void setForeColor(String foreColor) {
- if (foreColor == null) {
- throw new IllegalArgumentException("foreColor cannot be null");
- }
- if (!isValidColor(foreColor)) {
- throw new IllegalArgumentException("Invalid foreColor format: " + foreColor);
- }
-
- ConfigSnapshot oldSnapshot = getSnapshot();
-
- lock.writeLock().lock();
- try {
- this.foreColor = foreColor;
- } finally {
- lock.writeLock().unlock();
- }
-
- ConfigSnapshot newSnapshot = getSnapshot();
- notifyConfigChange(oldSnapshot, newSnapshot);
- }
-
- public void setBackColor(String backColor) {
- if (backColor == null) {
- throw new IllegalArgumentException("backColor cannot be null");
- }
- if (!isValidColor(backColor)) {
- throw new IllegalArgumentException("Invalid backColor format: " + backColor);
- }
-
- ConfigSnapshot oldSnapshot = getSnapshot();
-
- lock.writeLock().lock();
- try {
- this.backColor = backColor;
- } finally {
- lock.writeLock().unlock();
- }
-
- ConfigSnapshot newSnapshot = getSnapshot();
- notifyConfigChange(oldSnapshot, newSnapshot);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/enums/ErrorCode.java b/src/main/java/cn/isliu/core/enums/ErrorCode.java
index dc6151c..fd4d1b0 100644
--- a/src/main/java/cn/isliu/core/enums/ErrorCode.java
+++ b/src/main/java/cn/isliu/core/enums/ErrorCode.java
@@ -64,6 +64,13 @@ public enum ErrorCode implements BaseEnum {
TOKEN_EXPIRED("FS603", "Token expired", ErrorCategory.SECURITY),
ENCRYPTION_FAILED("FS604", "Encryption failed", ErrorCategory.SECURITY),
DECRYPTION_FAILED("FS605", "Decryption failed", ErrorCategory.SECURITY),
+
+ // Token管理相关错误 (FS610-FS619)
+ TOKEN_MANAGEMENT_ERROR("FS610", "Token management error", ErrorCategory.SECURITY),
+ TOKEN_FETCH_FAILED("FS611", "Failed to fetch token from API", ErrorCategory.SECURITY),
+ TOKEN_PARSE_ERROR("FS612", "Failed to parse token response", ErrorCategory.SECURITY),
+ TOKEN_CACHE_ERROR("FS613", "Token cache operation failed", ErrorCategory.SECURITY),
+ TOKEN_REFRESH_FAILED("FS614", "Token refresh operation failed", ErrorCategory.SECURITY),
// 业务逻辑相关错误 (FS700-FS799)
BUSINESS_LOGIC_ERROR("FS700", "Business logic error", ErrorCategory.BUSINESS),
@@ -247,6 +254,8 @@ public enum ErrorCode implements BaseEnum {
case API_SERVER_ERROR:
case SERVICE_UNAVAILABLE:
case CONNECTION_POOL_EXHAUSTED:
+ case TOKEN_FETCH_FAILED:
+ case TOKEN_REFRESH_FAILED:
return true;
default:
return false;
diff --git a/src/main/java/cn/isliu/core/exception/TokenManagementException.java b/src/main/java/cn/isliu/core/exception/TokenManagementException.java
new file mode 100644
index 0000000..fa68156
--- /dev/null
+++ b/src/main/java/cn/isliu/core/exception/TokenManagementException.java
@@ -0,0 +1,242 @@
+package cn.isliu.core.exception;
+
+import cn.isliu.core.enums.ErrorCode;
+import java.util.Map;
+
+/**
+ * Token management exception class
+ *
+ * Specialized exception for handling various error scenarios during tenant_access_token management,
+ * including token fetch failures, parsing errors, cache operation failures, etc.
+ *
+ * @author isliu
+ */
+public class TokenManagementException extends FsHelperException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructor - uses default TOKEN_MANAGEMENT_ERROR error code
+ *
+ * @param message error message
+ */
+ public TokenManagementException(String message) {
+ super(ErrorCode.TOKEN_MANAGEMENT_ERROR, message);
+ }
+
+ /**
+ * Constructor - uses default TOKEN_MANAGEMENT_ERROR error code with cause
+ *
+ * @param message error message
+ * @param cause root cause exception
+ */
+ public TokenManagementException(String message, Throwable cause) {
+ super(ErrorCode.TOKEN_MANAGEMENT_ERROR, message, cause);
+ }
+
+ /**
+ * Constructor - specifies specific error code
+ *
+ * @param errorCode specific token management error code
+ * @param message error message
+ */
+ public TokenManagementException(ErrorCode errorCode, String message) {
+ super(errorCode, message);
+ }
+
+ /**
+ * Constructor - specifies specific error code with cause
+ *
+ * @param errorCode specific token management error code
+ * @param message error message
+ * @param cause root cause exception
+ */
+ public TokenManagementException(ErrorCode errorCode, String message, Throwable cause) {
+ super(errorCode, message, cause);
+ }
+
+ /**
+ * Constructor - includes context information
+ *
+ * @param errorCode specific token management error code
+ * @param message error message
+ * @param context context information
+ */
+ public TokenManagementException(ErrorCode errorCode, String message, Map context) {
+ super(errorCode, message, context);
+ }
+
+ /**
+ * Constructor - full parameters
+ *
+ * @param errorCode specific token management error code
+ * @param message error message
+ * @param context context information
+ * @param cause root cause exception
+ */
+ public TokenManagementException(ErrorCode errorCode, String message, Map context, Throwable cause) {
+ super(errorCode, message, context, cause);
+ }
+
+ // Static factory methods for creating specific types of token management exceptions
+
+ /**
+ * Create token fetch failed exception
+ *
+ * @param message error message
+ * @param cause root cause exception
+ * @return TokenManagementException instance
+ */
+ public static TokenManagementException tokenFetchFailed(String message, Throwable cause) {
+ return new TokenManagementException(ErrorCode.TOKEN_FETCH_FAILED, message, cause);
+ }
+
+ /**
+ * Create token parse error exception
+ *
+ * @param message error message
+ * @param responseBody response body content for debugging
+ * @return TokenManagementException instance
+ */
+ public static TokenManagementException tokenParseError(String message, String responseBody) {
+ TokenManagementException exception = new TokenManagementException(ErrorCode.TOKEN_PARSE_ERROR, message);
+ if (responseBody != null) {
+ exception.addContext("responseBody", responseBody);
+ }
+ return exception;
+ }
+
+ /**
+ * Create token cache operation failed exception
+ *
+ * @param message error message
+ * @param cause root cause exception
+ * @return TokenManagementException instance
+ */
+ public static TokenManagementException tokenCacheError(String message, Throwable cause) {
+ return new TokenManagementException(ErrorCode.TOKEN_CACHE_ERROR, message, cause);
+ }
+
+ /**
+ * Create token refresh failed exception
+ *
+ * @param message error message
+ * @param cause root cause exception
+ * @return TokenManagementException instance
+ */
+ public static TokenManagementException tokenRefreshFailed(String message, Throwable cause) {
+ return new TokenManagementException(ErrorCode.TOKEN_REFRESH_FAILED, message, cause);
+ }
+
+ /**
+ * Create token fetch failed exception with API response info
+ *
+ * @param message error message
+ * @param apiCode API returned error code
+ * @param apiMessage API returned error message
+ * @return TokenManagementException instance
+ */
+ public static TokenManagementException tokenFetchFailedWithApiInfo(String message, int apiCode, String apiMessage) {
+ TokenManagementException exception = new TokenManagementException(ErrorCode.TOKEN_FETCH_FAILED, message);
+ exception.addContext("apiCode", apiCode);
+ exception.addContext("apiMessage", apiMessage);
+ return exception;
+ }
+
+ /**
+ * Create token management exception with retry information
+ *
+ * @param errorCode error code
+ * @param message error message
+ * @param retryCount retry count
+ * @param maxRetries maximum retry count
+ * @param cause root cause exception
+ * @return TokenManagementException instance
+ */
+ public static TokenManagementException withRetryInfo(ErrorCode errorCode, String message,
+ int retryCount, int maxRetries, Throwable cause) {
+ TokenManagementException exception = new TokenManagementException(errorCode, message, cause);
+ exception.addContext("retryCount", retryCount);
+ exception.addContext("maxRetries", maxRetries);
+ exception.addContext("retriesExhausted", retryCount >= maxRetries);
+ return exception;
+ }
+
+ /**
+ * Check if this is a network-related token management exception
+ *
+ * @return true if network-related exception
+ */
+ public boolean isNetworkRelated() {
+ Throwable rootCause = getRootCause();
+ return rootCause instanceof java.net.SocketTimeoutException ||
+ rootCause instanceof java.net.ConnectException ||
+ rootCause instanceof java.net.UnknownHostException ||
+ rootCause instanceof java.io.IOException;
+ }
+
+ /**
+ * Check if this is an API response-related exception
+ *
+ * @return true if API response-related exception
+ */
+ public boolean isApiResponseRelated() {
+ ErrorCode code = getErrorCode();
+ return code == ErrorCode.TOKEN_PARSE_ERROR ||
+ code == ErrorCode.TOKEN_FETCH_FAILED;
+ }
+
+ /**
+ * Check if this is a cache-related exception
+ *
+ * @return true if cache-related exception
+ */
+ public boolean isCacheRelated() {
+ return getErrorCode() == ErrorCode.TOKEN_CACHE_ERROR;
+ }
+
+ /**
+ * Get suggested retry delay time in milliseconds
+ *
+ * @param retryCount current retry count
+ * @return suggested delay time
+ */
+ public long getSuggestedRetryDelay(int retryCount) {
+ if (!isRetryable()) {
+ return 0;
+ }
+
+ // Exponential backoff strategy, base delay 1 second, max delay 30 seconds
+ long baseDelay = 1000; // 1 second
+ long maxDelay = 30000; // 30 seconds
+ long delay = Math.min(baseDelay * (1L << retryCount), maxDelay);
+
+ // Add random jitter to avoid thundering herd effect
+ double jitter = 0.1; // 10% jitter
+ long jitterAmount = (long) (delay * jitter * Math.random());
+
+ return delay + jitterAmount;
+ }
+
+ /**
+ * Get user-friendly error message
+ *
+ * @return user-friendly error message
+ */
+ @Override
+ public String getUserFriendlyMessage() {
+ ErrorCode code = getErrorCode();
+ switch (code) {
+ case TOKEN_FETCH_FAILED:
+ return "Failed to fetch access token, please check network connection and application configuration.";
+ case TOKEN_PARSE_ERROR:
+ return "Failed to parse access token response, please contact technical support.";
+ case TOKEN_CACHE_ERROR:
+ return "Token cache operation failed, system will re-fetch token.";
+ case TOKEN_REFRESH_FAILED:
+ return "Failed to refresh access token, please try again later.";
+ default:
+ return "Error occurred during token management, please try again later or contact technical support.";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/pojo/FieldProperty.java b/src/main/java/cn/isliu/core/pojo/FieldProperty.java
index ec12b30..e6ec9b8 100644
--- a/src/main/java/cn/isliu/core/pojo/FieldProperty.java
+++ b/src/main/java/cn/isliu/core/pojo/FieldProperty.java
@@ -89,7 +89,7 @@ public class FieldProperty {
*
* @return 列名字符串
*/
- public String getFieldName() {
+ public String[] getFieldName() {
return tableProperty.value();
}
diff --git a/src/main/java/cn/isliu/core/pojo/TokenInfo.java b/src/main/java/cn/isliu/core/pojo/TokenInfo.java
new file mode 100644
index 0000000..7589bb1
--- /dev/null
+++ b/src/main/java/cn/isliu/core/pojo/TokenInfo.java
@@ -0,0 +1,123 @@
+package cn.isliu.core.pojo;
+
+/**
+ * Token信息数据模型类
+ *
+ * 封装飞书API的tenant_access_token相关信息,包括token值、过期时间和获取时间。
+ * 提供便利方法用于检查token有效性、计算剩余时间和判断是否即将过期。
+ *
+ * @author FsHelper
+ * @since 1.0
+ */
+public class TokenInfo {
+
+ /**
+ * 实际的access token字符串
+ */
+ private final String token;
+
+ /**
+ * token过期的绝对时间戳(毫秒)
+ */
+ private final long expiresAt;
+
+ /**
+ * token获取的时间戳(毫秒),用于调试和日志
+ */
+ private final long fetchedAt;
+
+ /**
+ * 构造函数
+ *
+ * @param token 实际的access token字符串
+ * @param expiresAt token过期的绝对时间戳(毫秒)
+ * @param fetchedAt token获取的时间戳(毫秒)
+ */
+ public TokenInfo(String token, long expiresAt, long fetchedAt) {
+ this.token = token;
+ this.expiresAt = expiresAt;
+ this.fetchedAt = fetchedAt;
+ }
+
+ /**
+ * 根据token值和有效期秒数创建TokenInfo实例
+ *
+ * @param token 实际的access token字符串
+ * @param expireSeconds token有效期(秒)
+ * @return TokenInfo实例
+ */
+ public static TokenInfo create(String token, int expireSeconds) {
+ long now = System.currentTimeMillis();
+ long expiresAt = now + (expireSeconds * 1000L);
+ return new TokenInfo(token, expiresAt, now);
+ }
+
+ /**
+ * 获取token字符串
+ *
+ * @return token字符串
+ */
+ public String getToken() {
+ return token;
+ }
+
+ /**
+ * 获取token过期时间戳
+ *
+ * @return 过期时间戳(毫秒)
+ */
+ public long getExpiresAt() {
+ return expiresAt;
+ }
+
+ /**
+ * 获取token获取时间戳
+ *
+ * @return 获取时间戳(毫秒)
+ */
+ public long getFetchedAt() {
+ return fetchedAt;
+ }
+
+ /**
+ * 检查token是否仍然有效
+ *
+ * @return true表示token仍在有效期内,false表示已过期
+ */
+ public boolean isValid() {
+ return System.currentTimeMillis() < expiresAt;
+ }
+
+ /**
+ * 计算token剩余有效时间
+ *
+ * @return 剩余有效时间(秒),如果已过期则返回0
+ */
+ public long getRemainingSeconds() {
+ long remaining = (expiresAt - System.currentTimeMillis()) / 1000;
+ return Math.max(0, remaining);
+ }
+
+ /**
+ * 判断token是否即将过期
+ *
+ * 根据飞书API文档,当剩余有效期小于30分钟时,调用接口会返回新的token。
+ *
+ * @return true表示剩余有效期小于30分钟,false表示剩余有效期大于等于30分钟
+ */
+ public boolean isExpiringSoon() {
+ return getRemainingSeconds() < 1800; // 30分钟 = 1800秒
+ }
+
+ @Override
+ public String toString() {
+ return "TokenInfo{" +
+ "token='" + (token != null ? token.substring(0, Math.min(token.length(), 10)) + "..." : "null") + '\'' +
+ ", expiresAt=" + expiresAt +
+ ", fetchedAt=" + fetchedAt +
+ ", remainingSeconds=" + getRemainingSeconds() +
+ ", isValid=" + isValid() +
+ ", isExpiringSoon=" + isExpiringSoon() +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/pojo/TokenResponse.java b/src/main/java/cn/isliu/core/pojo/TokenResponse.java
new file mode 100644
index 0000000..5237b0a
--- /dev/null
+++ b/src/main/java/cn/isliu/core/pojo/TokenResponse.java
@@ -0,0 +1,197 @@
+package cn.isliu.core.pojo;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * 飞书API获取租户访问令牌的响应模型类
+ *
+ * 对应飞书API返回的JSON格式:
+ * {
+ * "code": 0,
+ * "msg": "ok",
+ * "tenant_access_token": "t-caecc734c2e3328a62489fe0648c4b98779515d3",
+ * "expire": 7200
+ * }
+ *
+ * @author FsHelper
+ * @since 1.0
+ */
+public class TokenResponse {
+
+ /**
+ * 响应状态码
+ * 0表示成功,非0表示失败
+ */
+ @SerializedName("code")
+ private int code;
+
+ /**
+ * 响应消息
+ * 通常成功时为"ok",失败时包含错误描述
+ */
+ @SerializedName("msg")
+ private String msg;
+
+ /**
+ * 租户访问令牌
+ * 用于后续API调用的认证
+ */
+ @SerializedName("tenant_access_token")
+ private String tenant_access_token;
+
+ /**
+ * 令牌有效期(秒)
+ * 通常为7200秒(2小时)
+ */
+ @SerializedName("expire")
+ private int expire;
+
+ /**
+ * 默认构造函数
+ */
+ public TokenResponse() {
+ }
+
+ /**
+ * 完整构造函数
+ *
+ * @param code 响应状态码
+ * @param msg 响应消息
+ * @param tenant_access_token 租户访问令牌
+ * @param expire 令牌有效期(秒)
+ */
+ public TokenResponse(int code, String msg, String tenant_access_token, int expire) {
+ this.code = code;
+ this.msg = msg;
+ this.tenant_access_token = tenant_access_token;
+ this.expire = expire;
+ }
+
+ /**
+ * 获取响应状态码
+ *
+ * @return 响应状态码,0表示成功
+ */
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * 设置响应状态码
+ *
+ * @param code 响应状态码
+ */
+ public void setCode(int code) {
+ this.code = code;
+ }
+
+ /**
+ * 获取响应消息
+ *
+ * @return 响应消息
+ */
+ public String getMsg() {
+ return msg;
+ }
+
+ /**
+ * 设置响应消息
+ *
+ * @param msg 响应消息
+ */
+ public void setMsg(String msg) {
+ this.msg = msg;
+ }
+
+ /**
+ * 获取租户访问令牌
+ *
+ * @return 租户访问令牌字符串
+ */
+ public String getTenant_access_token() {
+ return tenant_access_token;
+ }
+
+ /**
+ * 设置租户访问令牌
+ *
+ * @param tenant_access_token 租户访问令牌
+ */
+ public void setTenant_access_token(String tenant_access_token) {
+ this.tenant_access_token = tenant_access_token;
+ }
+
+ /**
+ * 获取令牌有效期
+ *
+ * @return 令牌有效期(秒)
+ */
+ public int getExpire() {
+ return expire;
+ }
+
+ /**
+ * 设置令牌有效期
+ *
+ * @param expire 令牌有效期(秒)
+ */
+ public void setExpire(int expire) {
+ this.expire = expire;
+ }
+
+ /**
+ * 检查响应是否成功
+ *
+ * @return true表示API调用成功,false表示失败
+ */
+ public boolean isSuccess() {
+ return code == 0;
+ }
+
+ /**
+ * 检查是否包含有效的令牌数据
+ *
+ * @return true表示包含有效的令牌数据
+ */
+ public boolean hasValidToken() {
+ return isSuccess() &&
+ tenant_access_token != null &&
+ !tenant_access_token.trim().isEmpty() &&
+ expire > 0;
+ }
+
+ @Override
+ public String toString() {
+ return "TokenResponse{" +
+ "code=" + code +
+ ", msg='" + msg + '\'' +
+ ", tenant_access_token='" +
+ (tenant_access_token != null ?
+ tenant_access_token.substring(0, Math.min(tenant_access_token.length(), 10)) + "..." :
+ "null") + '\'' +
+ ", expire=" + expire +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TokenResponse that = (TokenResponse) o;
+
+ if (code != that.code) return false;
+ if (expire != that.expire) return false;
+ if (!java.util.Objects.equals(msg, that.msg)) return false;
+ return java.util.Objects.equals(tenant_access_token, that.tenant_access_token);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = code;
+ result = 31 * result + (msg != null ? msg.hashCode() : 0);
+ result = 31 * result + (tenant_access_token != null ? tenant_access_token.hashCode() : 0);
+ result = 31 * result + expire;
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/service/AbstractFeishuApiService.java b/src/main/java/cn/isliu/core/service/AbstractFeishuApiService.java
index acb28a3..fac253f 100644
--- a/src/main/java/cn/isliu/core/service/AbstractFeishuApiService.java
+++ b/src/main/java/cn/isliu/core/service/AbstractFeishuApiService.java
@@ -1,6 +1,7 @@
package cn.isliu.core.service;
import cn.isliu.core.client.FeishuClient;
+import cn.isliu.core.exception.FsHelperException;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.lark.oapi.core.utils.Jsons;
@@ -17,6 +18,7 @@ public abstract class AbstractFeishuApiService {
protected final FeishuClient feishuClient;
protected final OkHttpClient httpClient;
protected final Gson gson;
+ protected final TenantTokenManager tokenManager;
protected static final String BASE_URL = "https://open.feishu.cn/open-apis";
protected static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
@@ -30,35 +32,22 @@ public abstract class AbstractFeishuApiService {
this.feishuClient = feishuClient;
this.httpClient = feishuClient.getHttpClient();
this.gson = Jsons.DEFAULT;
+ this.tokenManager = new TenantTokenManager(feishuClient);
}
/**
* 获取租户访问令牌
*
+ * 使用TenantTokenManager进行智能的token管理,包括缓存、过期检测和自动刷新。
+ *
* @return 访问令牌
* @throws IOException 请求异常
*/
protected String getTenantAccessToken() throws IOException {
- Map params = new HashMap<>();
- params.put("app_id", feishuClient.getAppId());
- params.put("app_secret", feishuClient.getAppSecret());
-
- RequestBody body = RequestBody.create(gson.toJson(params), JSON_MEDIA_TYPE);
-
- Request request =
- new Request.Builder().url(BASE_URL + "/auth/v3/tenant_access_token/internal").post(body).build();
-
- try (Response response = httpClient.newCall(request).execute()) {
- if (!response.isSuccessful() || response.body() == null) {
- throw new IOException("Failed to get tenant access token: " + response);
- }
-
- JsonObject jsonResponse = gson.fromJson(response.body().string(), JsonObject.class);
- if (jsonResponse.has("tenant_access_token")) {
- return jsonResponse.get("tenant_access_token").getAsString();
- } else {
- throw new IOException("Invalid token response: " + jsonResponse);
- }
+ try {
+ return tokenManager.getCachedTenantAccessToken();
+ } catch (FsHelperException e) {
+ throw new IOException("Failed to get tenant access token: " + e.getMessage(), e);
}
}
diff --git a/src/main/java/cn/isliu/core/service/CustomValueService.java b/src/main/java/cn/isliu/core/service/CustomValueService.java
index 679590f..4691128 100644
--- a/src/main/java/cn/isliu/core/service/CustomValueService.java
+++ b/src/main/java/cn/isliu/core/service/CustomValueService.java
@@ -1040,6 +1040,11 @@ public class CustomValueService extends AbstractFeishuApiService {
return this;
}
+ public BatchPutValuesBuilder setMajorDimension(String majorDimension) {
+ currentItem.setMajorDimension(majorDimension);
+ return this;
+ }
+
/**
* 构建向多个范围写入数据请求
*
@@ -1620,6 +1625,7 @@ public class CustomValueService extends AbstractFeishuApiService {
public static class ValueRangeItem {
private String range;
private String type;
+ private String majorDimension;
private List> values;
public ValueRangeItem() {
@@ -1644,6 +1650,14 @@ public class CustomValueService extends AbstractFeishuApiService {
this.range = range;
}
+ public String getMajorDimension() {
+ return majorDimension;
+ }
+
+ public void setMajorDimension(String majorDimension) {
+ this.majorDimension = majorDimension;
+ }
+
/**
* 获取数据值
*
diff --git a/src/main/java/cn/isliu/core/service/TenantTokenManager.java b/src/main/java/cn/isliu/core/service/TenantTokenManager.java
new file mode 100644
index 0000000..c9d8ddb
--- /dev/null
+++ b/src/main/java/cn/isliu/core/service/TenantTokenManager.java
@@ -0,0 +1,481 @@
+package cn.isliu.core.service;
+
+import cn.isliu.core.client.FeishuClient;
+import cn.isliu.core.enums.ErrorCode;
+import cn.isliu.core.exception.FsHelperException;
+import cn.isliu.core.logging.FsLogger;
+import cn.isliu.core.pojo.TokenInfo;
+import cn.isliu.core.pojo.TokenResponse;
+import com.google.gson.Gson;
+import com.lark.oapi.core.utils.Jsons;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * 飞书租户访问令牌管理器
+ *
+ * 负责管理tenant_access_token的获取、缓存、过期检测和自动刷新。
+ * 实现线程安全的token管理,避免频繁的API调用,提高系统性能。
+ *
+ * 核心功能:
+ * - 智能缓存:缓存有效的token,避免重复获取
+ * - 过期检测:自动检测token是否即将过期(剩余时间<30分钟)
+ * - 自动刷新:在token即将过期时自动获取新token
+ * - 线程安全:使用读写锁确保并发访问的安全性
+ * - 异常处理:完善的错误处理和重试机制
+ *
+ * @author FsHelper
+ * @since 1.0
+ */
+public class TenantTokenManager {
+
+ /** 飞书API基础URL */
+ private static final String BASE_URL = "https://open.feishu.cn/open-apis";
+
+ /** 获取租户访问令牌的API端点 */
+ private static final String TOKEN_ENDPOINT = "/auth/v3/tenant_access_token/internal";
+
+ /** JSON媒体类型 */
+ private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
+
+ /** Token缓存组件 */
+ private final TokenCache tokenCache;
+
+ /** 读写锁,确保线程安全 */
+ private final ReentrantReadWriteLock lock;
+
+ /** 飞书客户端 */
+ private final FeishuClient feishuClient;
+
+ /** HTTP客户端 */
+ private final OkHttpClient httpClient;
+
+ /** JSON序列化工具 */
+ private final Gson gson;
+
+ /**
+ * 构造函数
+ *
+ * @param feishuClient 飞书客户端实例,不能为null
+ * @throws IllegalArgumentException 如果feishuClient为null
+ */
+ public TenantTokenManager(FeishuClient feishuClient) {
+ if (feishuClient == null) {
+ throw new IllegalArgumentException("FeishuClient cannot be null");
+ }
+
+ this.feishuClient = feishuClient;
+ this.httpClient = feishuClient.getHttpClient();
+ this.gson = Jsons.DEFAULT;
+ this.tokenCache = new TokenCache();
+ this.lock = new ReentrantReadWriteLock();
+
+ FsLogger.debug("TenantTokenManager initialized for app_id: {}", feishuClient.getAppId());
+ }
+
+ /**
+ * 获取租户访问令牌 直接调用飞书API获取
+ *
+ * tenant_access_token 的最大有效期是 2 小时。
+ *
+ * 剩余有效期小于 30 分钟时,调用本接口会返回一个新的 tenant_access_token,这会同时存在两个有效的 tenant_access_token。
+ * 剩余有效期大于等于 30 分钟时,调用本接口会返回原有的 tenant_access_token。
+ *
+ * @return 获取到的租户访问令牌字符串
+ * @throws IOException 如果获取令牌时发生错误
+ */
+ public String getTenantAccessToken() throws IOException {
+ Map params = new HashMap<>();
+ params.put("app_id", feishuClient.getAppId());
+ params.put("app_secret", feishuClient.getAppSecret());
+
+ RequestBody body = RequestBody.create(gson.toJson(params), JSON_MEDIA_TYPE);
+
+ Request request =
+ new Request.Builder().url(BASE_URL + "/auth/v3/tenant_access_token/internal").post(body).build();
+
+ try (Response response = httpClient.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new IOException("HTTP request failed with status: " + response.code() +
+ ", message: " + response.message());
+ }
+
+ if (response.body() == null) {
+ throw new IOException("Response body is null");
+ }
+
+ String responseBody = response.body().string();
+ FsLogger.debug("Token API response received - status: {}, body_length: {} chars",
+ response.code(), responseBody.length());
+
+ // 解析响应
+ TokenResponse tokenResponse;
+ try {
+ tokenResponse = gson.fromJson(responseBody, TokenResponse.class);
+ } catch (Exception e) {
+ throw new IOException("Failed to parse token response JSON: " + responseBody, e);
+ }
+
+ // 检查API响应状态
+ if (tokenResponse.getCode() != 0) {
+ throw new IOException("API returned error code: " + tokenResponse.getCode() +
+ ", message: " + tokenResponse.getMsg());
+ }
+
+ // 验证响应数据
+ if (tokenResponse.getTenant_access_token() == null ||
+ tokenResponse.getTenant_access_token().trim().isEmpty()) {
+ throw new IOException("Invalid token response: tenant_access_token is null or empty");
+ }
+
+ if (tokenResponse.getExpire() <= 0) {
+ throw new IOException("Invalid token response: expire value is invalid: " +
+ tokenResponse.getExpire());
+ }
+
+ FsLogger.debug("New token created successfully - expires in: {} seconds, token: {}",
+ tokenResponse.getExpire(),
+ tokenResponse.getTenant_access_token());
+
+
+ return tokenResponse.getTenant_access_token();
+
+ }
+ }
+
+ /**
+ * 获取有效的租户访问令牌
+ *
+ * 该方法实现智能的token管理逻辑和完整的线程安全机制:
+ * 1. 首先使用读锁检查缓存中的token是否有效且未即将过期
+ * 2. 如果缓存的token有效且不会很快过期,直接返回
+ * 3. 如果token无效或即将过期,使用写锁获取新token
+ * 4. 在写锁中再次检查缓存(双重检查锁定模式),避免重复获取
+ * 5. 调用飞书API获取新token并更新缓存
+ *
+ * 线程安全保证:
+ * - 使用ReentrantReadWriteLock确保并发访问安全
+ * - 实现双重检查锁定模式避免重复token获取
+ * - 多个线程同时请求时,只有一个线程执行token获取操作
+ * - 其他线程等待获取完成后使用新token
+ *
+ * @return 有效的租户访问令牌字符串
+ * @throws FsHelperException 当token获取失败时抛出
+ */
+ public String getCachedTenantAccessToken() throws FsHelperException {
+ // 第一次检查:使用读锁检查缓存
+ // 这允许多个线程同时读取缓存,提高并发性能
+ lock.readLock().lock();
+ try {
+ TokenInfo cachedToken = tokenCache.get();
+ if (cachedToken != null && cachedToken.isValid() && !cachedToken.isExpiringSoon()) {
+ FsLogger.trace("Token cache hit - remaining: {} seconds", cachedToken.getRemainingSeconds());
+ logTokenDetails(cachedToken, "cache_hit");
+ return cachedToken.getToken();
+ }
+ } finally {
+ lock.readLock().unlock();
+ }
+
+ // 需要获取新token,使用写锁确保只有一个线程执行获取操作
+ lock.writeLock().lock();
+ try {
+ // 双重检查锁定模式:再次检查缓存
+ // 这是关键的并发控制机制,防止多个线程重复获取token
+ TokenInfo cachedToken = tokenCache.get();
+ if (cachedToken != null && cachedToken.isValid() && !cachedToken.isExpiringSoon()) {
+ FsLogger.debug("Token was refreshed by another thread, using updated token - remaining: {} seconds",
+ cachedToken.getRemainingSeconds());
+ return cachedToken.getToken();
+ }
+
+ // 记录token获取原因,便于调试并发问题
+ String reason = cachedToken == null ? "no cached token" :
+ !cachedToken.isValid() ? "token expired" : "token expiring soon";
+
+ long startTime = System.currentTimeMillis();
+ FsLogger.info("Token refresh triggered - reason: {}, thread: {}",
+ reason, Thread.currentThread().getName());
+
+ // 获取新token并原子性更新缓存
+ TokenInfo newToken = fetchNewToken();
+ tokenCache.set(newToken);
+
+ long duration = System.currentTimeMillis() - startTime;
+ FsLogger.info("Token refresh completed successfully - expires in: {} seconds, duration: {}ms, thread: {}",
+ newToken.getRemainingSeconds(), duration, Thread.currentThread().getName());
+
+ // 记录详细token信息
+ logTokenDetails(newToken, "token_refreshed");
+
+ // 记录性能指标
+ FsLogger.logPerformance("tenant_token_fetch", duration, true);
+
+ return newToken.getToken();
+
+ } catch (IOException e) {
+ // 记录错误日志
+ String context = String.format("app_id=%s, endpoint=%s, thread=%s",
+ feishuClient.getAppId(), BASE_URL + TOKEN_ENDPOINT,
+ Thread.currentThread().getName());
+ FsLogger.error(ErrorCode.TOKEN_FETCH_FAILED,
+ "Failed to fetch tenant access token from Feishu API", context, e);
+
+ // 清空缓存,强制下次重新获取
+ // 这个操作在写锁保护下是原子的
+ tokenCache.clear();
+ throw FsHelperException.builder(ErrorCode.TOKEN_FETCH_FAILED)
+ .message("Failed to fetch tenant access token from Feishu API")
+ .context("app_id", feishuClient.getAppId())
+ .context("endpoint", BASE_URL + TOKEN_ENDPOINT)
+ .context("thread", Thread.currentThread().getName())
+ .cause(e)
+ .build();
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * 从飞书API获取新的访问令牌
+ *
+ * 该方法负责:
+ * 1. 构建API请求参数(app_id和app_secret)
+ * 2. 发送HTTP POST请求到飞书API
+ * 3. 解析API响应并提取token信息
+ * 4. 创建TokenInfo实例并返回
+ *
+ * @return 新的TokenInfo实例
+ * @throws IOException 当网络请求失败或响应解析失败时抛出
+ */
+ private TokenInfo fetchNewToken() throws IOException {
+ // 构建请求参数
+ Map params = new HashMap<>();
+ params.put("app_id", feishuClient.getAppId());
+ params.put("app_secret", feishuClient.getAppSecret());
+
+ RequestBody body = RequestBody.create(gson.toJson(params), JSON_MEDIA_TYPE);
+ Request request = new Request.Builder()
+ .url(BASE_URL + TOKEN_ENDPOINT)
+ .post(body)
+ .build();
+
+ FsLogger.debug("Sending token request to: {}", BASE_URL + TOKEN_ENDPOINT);
+
+ try (Response response = httpClient.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new IOException("HTTP request failed with status: " + response.code() +
+ ", message: " + response.message());
+ }
+
+ if (response.body() == null) {
+ throw new IOException("Response body is null");
+ }
+
+ String responseBody = response.body().string();
+ FsLogger.debug("Token API response received - status: {}, body_length: {} chars",
+ response.code(), responseBody.length());
+
+ // 解析响应
+ TokenResponse tokenResponse;
+ try {
+ tokenResponse = gson.fromJson(responseBody, TokenResponse.class);
+ } catch (Exception e) {
+ throw new IOException("Failed to parse token response JSON: " + responseBody, e);
+ }
+
+ // 检查API响应状态
+ if (tokenResponse.getCode() != 0) {
+ throw new IOException("API returned error code: " + tokenResponse.getCode() +
+ ", message: " + tokenResponse.getMsg());
+ }
+
+ // 验证响应数据
+ if (tokenResponse.getTenant_access_token() == null ||
+ tokenResponse.getTenant_access_token().trim().isEmpty()) {
+ throw new IOException("Invalid token response: tenant_access_token is null or empty");
+ }
+
+ if (tokenResponse.getExpire() <= 0) {
+ throw new IOException("Invalid token response: expire value is invalid: " +
+ tokenResponse.getExpire());
+ }
+
+ // 创建TokenInfo实例
+ TokenInfo tokenInfo = TokenInfo.create(tokenResponse.getTenant_access_token(), tokenResponse.getExpire());
+
+ FsLogger.debug("New token created successfully - expires in: {} seconds, token_prefix: {}...",
+ tokenInfo.getRemainingSeconds(),
+ tokenInfo.getToken().substring(0, Math.min(10, tokenInfo.getToken().length())));
+
+ return tokenInfo;
+ }
+ }
+
+ /**
+ * 获取当前缓存的token信息(用于调试和监控)
+ *
+ * @return 当前缓存的TokenInfo,如果缓存为空返回null
+ */
+ public TokenInfo getCurrentTokenInfo() {
+ lock.readLock().lock();
+ try {
+ return tokenCache.get();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * 强制清空token缓存
+ *
+ * 该方法会清空当前缓存的token,强制下次调用getTenantAccessToken()时重新获取。
+ * 通常在以下情况下使用:
+ * - 检测到token无效时
+ * - 需要强制刷新token时
+ * - 系统重置或清理时
+ */
+ public void clearTokenCache() {
+ lock.writeLock().lock();
+ try {
+ tokenCache.clear();
+ FsLogger.info("Token cache cleared manually");
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * 检查当前是否有有效的缓存token
+ *
+ * @return true表示有有效的缓存token,false表示缓存为空或token已过期
+ */
+ public boolean hasValidCachedToken() {
+ lock.readLock().lock();
+ try {
+ return tokenCache.hasValidToken();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * 强制刷新token(用于主动刷新场景)
+ *
+ * 该方法会忽略当前缓存状态,强制获取新的token。
+ * 使用写锁确保与其他操作的线程安全。
+ *
+ * @return 新获取的租户访问令牌字符串
+ * @throws FsHelperException 当token获取失败时抛出
+ */
+ public String forceRefreshToken() throws FsHelperException {
+ lock.writeLock().lock();
+ try {
+ long startTime = System.currentTimeMillis();
+ FsLogger.info("Force refresh initiated - thread: {}", Thread.currentThread().getName());
+
+ TokenInfo newToken = fetchNewToken();
+ tokenCache.set(newToken);
+
+ long duration = System.currentTimeMillis() - startTime;
+ FsLogger.info("Force refresh completed - expires in: {} seconds, duration: {}ms, thread: {}",
+ newToken.getRemainingSeconds(), duration, Thread.currentThread().getName());
+
+ // 记录性能指标
+ FsLogger.logPerformance("tenant_token_force_refresh", duration, true);
+
+ return newToken.getToken();
+
+ } catch (IOException e) {
+ String context = String.format("app_id=%s, thread=%s",
+ feishuClient.getAppId(), Thread.currentThread().getName());
+ FsLogger.error(ErrorCode.TOKEN_FETCH_FAILED,
+ "Failed to force refresh tenant access token", context, e);
+
+ tokenCache.clear();
+ throw FsHelperException.builder(ErrorCode.TOKEN_FETCH_FAILED)
+ .message("Failed to force refresh tenant access token from Feishu API")
+ .context("app_id", feishuClient.getAppId())
+ .context("thread", Thread.currentThread().getName())
+ .cause(e)
+ .build();
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * 检查是否有其他线程正在获取token
+ *
+ * @return true表示有线程持有写锁(正在获取token),false表示没有
+ */
+ public boolean isTokenRefreshInProgress() {
+ return lock.isWriteLocked();
+ }
+
+ /**
+ * 获取当前持有读锁的线程数量
+ *
+ * @return 持有读锁的线程数量
+ */
+ public int getReadLockCount() {
+ return lock.getReadLockCount();
+ }
+
+ /**
+ * 获取缓存状态信息(用于监控和调试)
+ *
+ * @return 包含缓存状态的字符串描述
+ */
+ public String getCacheStatus() {
+ lock.readLock().lock();
+ try {
+ TokenInfo token = tokenCache.get();
+ String lockStatus = String.format("readers=%d, write_locked=%s",
+ lock.getReadLockCount(),
+ lock.isWriteLocked());
+
+ if (token == null) {
+ return String.format("Cache: empty, Lock: [%s]", lockStatus);
+ }
+ return String.format("Cache: token=%s..., valid=%s, expiring_soon=%s, remaining=%ds, Lock: [%s]",
+ token.getToken().substring(0, Math.min(10, token.getToken().length())),
+ token.isValid(),
+ token.isExpiringSoon(),
+ token.getRemainingSeconds(),
+ lockStatus);
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * 记录缓存状态日志(用于监控和调试)
+ */
+ public void logCacheStatus() {
+ String status = getCacheStatus();
+ FsLogger.debug("Token cache status: {}", status);
+ }
+
+ /**
+ * 记录详细的token信息(用于调试)
+ */
+ private void logTokenDetails(TokenInfo token, String operation) {
+ if (token != null) {
+ Map context = new HashMap<>();
+ context.put("operation", operation);
+ context.put("token_prefix", token.getToken().substring(0, Math.min(10, token.getToken().length())));
+ context.put("valid", token.isValid());
+ context.put("expiring_soon", token.isExpiringSoon());
+ context.put("remaining_seconds", token.getRemainingSeconds());
+ context.put("thread", Thread.currentThread().getName());
+
+ FsLogger.debug("Token details - operation: {}, valid: {}, expiring_soon: {}, remaining: {}s",
+ operation, token.isValid(), token.isExpiringSoon(), token.getRemainingSeconds());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/service/TokenCache.java b/src/main/java/cn/isliu/core/service/TokenCache.java
new file mode 100644
index 0000000..9ff97cf
--- /dev/null
+++ b/src/main/java/cn/isliu/core/service/TokenCache.java
@@ -0,0 +1,112 @@
+package cn.isliu.core.service;
+
+import cn.isliu.core.pojo.TokenInfo;
+
+/**
+ * Token缓存组件
+ *
+ * 提供线程安全的TokenInfo存储和访问功能。使用volatile关键字确保在多线程环境中
+ * 对token缓存的读写操作具有可见性保证。
+ *
+ * 该类设计为轻量级缓存,专门用于存储单个TokenInfo实例。通过volatile字段
+ * 确保线程间的可见性,配合外部的同步机制(如读写锁)实现完整的线程安全。
+ *
+ * 线程安全机制:
+ * - volatile字段确保内存可见性,防止CPU缓存导致的数据不一致
+ * - 配合外部ReentrantReadWriteLock实现原子性操作
+ * - 所有操作都是原子的引用赋值,避免数据竞争
+ *
+ * @author FsHelper
+ * @since 1.0
+ */
+public class TokenCache {
+
+ /**
+ * 当前缓存的token信息
+ *
+ * 使用volatile关键字确保多线程环境下的可见性:
+ * - 当一个线程修改currentToken时,其他线程能立即看到最新值
+ * - 防止CPU缓存导致的数据不一致问题
+ * - 与外部同步机制配合使用,确保完整的线程安全
+ */
+ private volatile TokenInfo currentToken;
+
+ /**
+ * 获取当前缓存的token信息
+ *
+ * @return 当前缓存的TokenInfo实例,如果缓存为空则返回null
+ */
+ public TokenInfo get() {
+ return currentToken;
+ }
+
+ /**
+ * 设置新的token信息到缓存
+ *
+ * 该操作是原子的,由于使用volatile字段,设置操作对所有线程立即可见。
+ * 通常在外部写锁保护下调用,确保与其他操作的原子性。
+ *
+ * @param tokenInfo 要缓存的TokenInfo实例,可以为null
+ */
+ public void set(TokenInfo tokenInfo) {
+ this.currentToken = tokenInfo;
+ }
+
+ /**
+ * 清空token缓存
+ *
+ * 将缓存设置为null,通常在token获取失败或需要强制刷新时调用。
+ * 该操作是原子的,由于使用volatile字段,清空操作对所有线程立即可见。
+ */
+ public void clear() {
+ this.currentToken = null;
+ }
+
+ /**
+ * 检查缓存是否为空
+ *
+ * @return true表示缓存为空,false表示缓存中有token信息
+ */
+ public boolean isEmpty() {
+ return currentToken == null;
+ }
+
+ /**
+ * 检查缓存中的token是否有效
+ *
+ * @return true表示缓存中有token且仍然有效,false表示缓存为空或token已过期
+ */
+ public boolean hasValidToken() {
+ TokenInfo token = currentToken;
+ return token != null && token.isValid();
+ }
+
+ /**
+ * 原子性地比较并设置token
+ *
+ * 该方法提供了一种安全的方式来更新token,只有当当前token与期望值相同时才进行更新。
+ * 虽然volatile保证了可见性,但这个方法在某些高级并发场景下可能有用。
+ *
+ * 注意:这个方法不是真正的CAS操作,因为Java对象引用的比较和设置不是原子的。
+ * 在实际使用中,应该依赖外部的同步机制(如写锁)来确保原子性。
+ *
+ * @param expected 期望的当前token值
+ * @param newToken 要设置的新token值
+ * @return true表示更新成功,false表示当前token与期望值不同
+ */
+ public boolean compareAndSet(TokenInfo expected, TokenInfo newToken) {
+ if (currentToken == expected) {
+ currentToken = newToken;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ TokenInfo token = currentToken;
+ return "TokenCache{" +
+ "currentToken=" + (token != null ? token.toString() : "null") +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/utils/FsTableUtil.java b/src/main/java/cn/isliu/core/utils/FsTableUtil.java
index 655e13f..e754b4d 100644
--- a/src/main/java/cn/isliu/core/utils/FsTableUtil.java
+++ b/src/main/java/cn/isliu/core/utils/FsTableUtil.java
@@ -1,18 +1,22 @@
package cn.isliu.core.utils;
import cn.isliu.core.*;
+import cn.isliu.core.annotation.TableConf;
import cn.isliu.core.annotation.TableProperty;
import cn.isliu.core.client.FsClient;
-import cn.isliu.core.config.FsConfig;
import cn.isliu.core.converters.OptionsValueProcess;
import cn.isliu.core.enums.BaseEnum;
import cn.isliu.core.enums.TypeEnum;
import cn.isliu.core.pojo.FieldProperty;
import cn.isliu.core.service.CustomValueService;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
@@ -32,7 +36,7 @@ public class FsTableUtil {
* @param spreadsheetToken 电子表格Token
* @return 飞书表格数据列表
*/
- public static List getFsTableData(Sheet sheet, String spreadsheetToken) {
+ public static List getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf) {
// 计算数据范围
GridProperties gridProperties = sheet.getGridProperties();
@@ -68,12 +72,12 @@ public class FsTableUtil {
List dataList = getFsTableData(tableData);
Map titleMap = new HashMap<>();
- dataList.stream().filter(d -> d.getRow() == (FsConfig.getInstance().getTitleLine() - 1)).findFirst()
+ dataList.stream().filter(d -> d.getRow() == (tableConf.titleRow() - 1)).findFirst()
.ifPresent(d -> {
Map map = (Map) d.getData();
titleMap.putAll(map);
});
- return dataList.stream().filter(fsTableData -> fsTableData.getRow() >= FsConfig.getInstance().getHeadLine()).map(item -> {
+ return dataList.stream().filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine()).map(item -> {
Map resultMap = new HashMap<>();
Map map = (Map) item.getData();
@@ -260,14 +264,14 @@ public class FsTableUtil {
return columnName.toString();
}
- public static Map getTitlePostionMap(Sheet sheet, String spreadsheetToken) {
+ public static Map getTitlePostionMap(Sheet sheet, String spreadsheetToken, TableConf tableConf) {
GridProperties gridProperties = sheet.getGridProperties();
int colCount = gridProperties.getColumnCount();
Map resultMap = new TreeMap<>();
ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken,
- "A" + FsConfig.getInstance().getTitleLine(),
- getColumnName(colCount - 1) + FsConfig.getInstance().getTitleLine(), FsClient.getInstance().getClient());
+ "A" + tableConf.titleRow(),
+ getColumnName(colCount - 1) + tableConf.titleRow(), FsClient.getInstance().getClient());
if (valuesBatch != null) {
List valueRanges = valuesBatch.getValueRanges();
if (valueRanges != null && !valueRanges.isEmpty()) {
@@ -288,8 +292,9 @@ public class FsTableUtil {
return resultMap;
}
- public static void setTableOptions(String spreadsheetToken, List headers, Map fieldsMap, String sheetId) {
+ public static void setTableOptions(String spreadsheetToken, List headers, Map fieldsMap, String sheetId, boolean enableDesc) {
List