feat(core): 重构配置管理并优化表格处理逻辑
- 移除 FsConfig 类,使用 TableConf 注解替代 - 重构 FsHelper 类,使用新配置方式- 优化 FsTableUtil 类,支持多级表头和描述行 - 更新 CustomValueService 类,添加 majorDimension 支持 - 修改 BaseEntity 类,增加 row 和 rowData 字段
This commit is contained in:
parent
aa14a659ee
commit
5a43a7f2e3
12
pom.xml
12
pom.xml
@ -151,12 +151,12 @@
|
||||
<!-- <version>2.5.3</version>-->
|
||||
<!-- </plugin>-->
|
||||
<!-- -->
|
||||
<!-- <!– exec plugin for running tests –>-->
|
||||
<!-- <plugin>-->
|
||||
<!-- <groupId>org.codehaus.mojo</groupId>-->
|
||||
<!-- <artifactId>exec-maven-plugin</artifactId>-->
|
||||
<!-- <version>3.1.0</version>-->
|
||||
<!-- </plugin>-->
|
||||
<!-- exec plugin for running tests -->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
@ -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<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
|
||||
List<String> 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<T> results = new ArrayList<>();
|
||||
FeishuClient client = FsClient.getInstance().getClient();
|
||||
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
|
||||
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
|
||||
TableConf tableConf = PropertyUtil.getTableConf(clazz);
|
||||
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf);
|
||||
|
||||
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
|
||||
List<String> 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<String, Object>) data);
|
||||
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) 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<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass);
|
||||
TableConf tableConf = PropertyUtil.getTableConf(aClass);
|
||||
|
||||
FeishuClient client = FsClient.getInstance().getClient();
|
||||
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
|
||||
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
|
||||
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf);
|
||||
Map<String, Integer> currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow));
|
||||
|
||||
final Integer[] row = {0};
|
||||
@ -133,7 +140,7 @@ public class FsHelper {
|
||||
}
|
||||
});
|
||||
|
||||
Map<String, String> titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken);
|
||||
Map<String, String> titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf);
|
||||
|
||||
Map<String, String> 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<FileData> 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;
|
||||
}
|
||||
|
||||
|
@ -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<String, Object> 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<String, Object> getRowData() {
|
||||
return rowData;
|
||||
}
|
||||
|
||||
public void setRowData(Map<String, Object> 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);
|
||||
}
|
||||
}
|
60
src/main/java/cn/isliu/core/annotation/TableConf.java
Normal file
60
src/main/java/cn/isliu/core/annotation/TableConf.java
Normal file
@ -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";
|
||||
}
|
@ -23,7 +23,14 @@ public @interface TableProperty {
|
||||
*
|
||||
* @return 列名字符串
|
||||
*/
|
||||
String value() default "";
|
||||
String[] value() default {};
|
||||
|
||||
/**
|
||||
* 字段描述
|
||||
*
|
||||
* @return 字段描述字符串或字符串数组
|
||||
*/
|
||||
String desc() default "";
|
||||
|
||||
/**
|
||||
* 字段名
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package cn.isliu.core.config;
|
||||
|
||||
/**
|
||||
* 配置变更监听器接口
|
||||
*/
|
||||
public interface ConfigChangeListener {
|
||||
|
||||
/**
|
||||
* 配置变更时的回调方法
|
||||
* @param event 配置变更事件
|
||||
*/
|
||||
void onConfigChanged(ConfigChangeEvent event);
|
||||
}
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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<ConfigChangeListener> 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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<String, Object> 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<String, Object> 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.";
|
||||
}
|
||||
}
|
||||
}
|
@ -89,7 +89,7 @@ public class FieldProperty {
|
||||
*
|
||||
* @return 列名字符串
|
||||
*/
|
||||
public String getFieldName() {
|
||||
public String[] getFieldName() {
|
||||
return tableProperty.value();
|
||||
}
|
||||
|
||||
|
123
src/main/java/cn/isliu/core/pojo/TokenInfo.java
Normal file
123
src/main/java/cn/isliu/core/pojo/TokenInfo.java
Normal file
@ -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() +
|
||||
'}';
|
||||
}
|
||||
}
|
197
src/main/java/cn/isliu/core/pojo/TokenResponse.java
Normal file
197
src/main/java/cn/isliu/core/pojo/TokenResponse.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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<String, String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<List<Object>> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据值
|
||||
*
|
||||
|
481
src/main/java/cn/isliu/core/service/TenantTokenManager.java
Normal file
481
src/main/java/cn/isliu/core/service/TenantTokenManager.java
Normal file
@ -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<String, String> 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<String, String> 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<String, Object> 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());
|
||||
}
|
||||
}
|
||||
}
|
112
src/main/java/cn/isliu/core/service/TokenCache.java
Normal file
112
src/main/java/cn/isliu/core/service/TokenCache.java
Normal file
@ -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") +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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<FsTableData> getFsTableData(Sheet sheet, String spreadsheetToken) {
|
||||
public static List<FsTableData> getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf) {
|
||||
|
||||
// 计算数据范围
|
||||
GridProperties gridProperties = sheet.getGridProperties();
|
||||
@ -68,12 +72,12 @@ public class FsTableUtil {
|
||||
List<FsTableData> dataList = getFsTableData(tableData);
|
||||
Map<String, String> 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<String, String> map = (Map<String, String>) 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<String, Object> resultMap = new HashMap<>();
|
||||
|
||||
Map<String, Object> map = (Map<String, Object>) item.getData();
|
||||
@ -260,14 +264,14 @@ public class FsTableUtil {
|
||||
return columnName.toString();
|
||||
}
|
||||
|
||||
public static Map<String, String> getTitlePostionMap(Sheet sheet, String spreadsheetToken) {
|
||||
public static Map<String, String> getTitlePostionMap(Sheet sheet, String spreadsheetToken, TableConf tableConf) {
|
||||
GridProperties gridProperties = sheet.getGridProperties();
|
||||
int colCount = gridProperties.getColumnCount();
|
||||
|
||||
Map<String, String> 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<ValueRange> valueRanges = valuesBatch.getValueRanges();
|
||||
if (valueRanges != null && !valueRanges.isEmpty()) {
|
||||
@ -288,8 +292,9 @@ public class FsTableUtil {
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
public static void setTableOptions(String spreadsheetToken, List<String> headers, Map<String, FieldProperty> fieldsMap, String sheetId) {
|
||||
public static void setTableOptions(String spreadsheetToken, List<String> headers, Map<String, FieldProperty> fieldsMap, String sheetId, boolean enableDesc) {
|
||||
List<Object> list = Arrays.asList(headers.toArray());
|
||||
int line = getMaxLevel(fieldsMap) + (enableDesc ? 2 : 1);
|
||||
fieldsMap.forEach((field, fieldProperty) -> {
|
||||
TableProperty tableProperty = fieldProperty.getTableProperty();
|
||||
String position = "";
|
||||
@ -300,7 +305,6 @@ public class FsTableUtil {
|
||||
position = FsTableUtil.getColumnNameByNuNumber(i + 1);
|
||||
}
|
||||
}
|
||||
int line = FsConfig.getInstance().getTitleLine() + 1;
|
||||
|
||||
if (tableProperty.enumClass() != BaseEnum.class) {
|
||||
FsApiUtil.setOptions(sheetId, FsClient.getInstance().getClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200,
|
||||
@ -324,19 +328,116 @@ public class FsTableUtil {
|
||||
});
|
||||
}
|
||||
|
||||
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers) {
|
||||
CustomValueService.ValueRequest.BatchPutValuesBuilder batchPutValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
|
||||
batchPutValuesBuilder.addRange(sheetId + "!A1:" + FsTableUtil.getColumnNameByNuNumber(headers.size()) + "1");
|
||||
batchPutValuesBuilder.addRow(headers.toArray());
|
||||
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
|
||||
Map<String, FieldProperty> fieldsMap, TableConf tableConf) {
|
||||
|
||||
String position = FsTableUtil.getColumnNameByNuNumber(headers.size());
|
||||
|
||||
CustomValueService.ValueRequest.BatchPutValuesBuilder batchPutValuesBuilder
|
||||
= CustomValueService.ValueRequest.batchPutValues();
|
||||
|
||||
// 获取父级表头
|
||||
// int maxLevel = getMaxLevel(fieldsMap);
|
||||
//
|
||||
// Map<Integer, List<String>> levelListMap = groupFieldsByLevel(fieldsMap);
|
||||
// for (int i = maxLevel; i >= 1; i--) {
|
||||
// List<String> values = levelListMap.get(i);
|
||||
// batchPutValuesBuilder.addRange(sheetId + "!A" + i + ":" + position + i);
|
||||
//
|
||||
// }
|
||||
//
|
||||
// int titleRow = maxLevel;
|
||||
|
||||
int titleRow = tableConf.titleRow();
|
||||
if (tableConf.enableDesc()) {
|
||||
int descRow = titleRow + 1;
|
||||
batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + descRow);
|
||||
batchPutValuesBuilder.addRow(headers.toArray());
|
||||
batchPutValuesBuilder.addRow(getDescArray(headers, fieldsMap));
|
||||
} else {
|
||||
batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow);
|
||||
batchPutValuesBuilder.addRow(headers.toArray());
|
||||
}
|
||||
|
||||
return batchPutValuesBuilder.build();
|
||||
}
|
||||
|
||||
public static String getDefaultTableStyle(String sheetId, int size) {
|
||||
private static int getMaxLevel(Map<String, FieldProperty> fieldsMap) {
|
||||
AtomicInteger maxLevel = new AtomicInteger(1);
|
||||
fieldsMap.forEach((field, fieldProperty) -> {
|
||||
TableProperty tableProperty = fieldProperty.getTableProperty();
|
||||
String[] value = tableProperty.value();
|
||||
if (value.length > maxLevel.get()) {
|
||||
maxLevel.set(value.length);
|
||||
}
|
||||
});
|
||||
return maxLevel.get();
|
||||
}
|
||||
|
||||
private static Object[] getDescArray(List<String> headers, Map<String, FieldProperty> fieldsMap) {
|
||||
Object[] descArray = new String[headers.size()];
|
||||
for (int i = 0; i < headers.size(); i++) {
|
||||
String header = headers.get(i);
|
||||
FieldProperty fieldProperty = fieldsMap.get(header);
|
||||
if (fieldProperty != null && fieldProperty.getTableProperty() != null) {
|
||||
String desc = fieldProperty.getTableProperty().desc();
|
||||
if (desc != null && !desc.isEmpty()) {
|
||||
try {
|
||||
JsonElement element = JsonParser.parseString(desc);
|
||||
if (element.isJsonObject()) {
|
||||
descArray[i] = element.getAsJsonObject();
|
||||
} else if (element.isJsonArray()) {
|
||||
descArray[i] = element.getAsJsonArray();
|
||||
} else {
|
||||
descArray[i] = desc;
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
descArray[i] = desc;
|
||||
}
|
||||
} else {
|
||||
descArray[i] = null;
|
||||
}
|
||||
} else {
|
||||
descArray[i] = null;
|
||||
}
|
||||
}
|
||||
return descArray;
|
||||
}
|
||||
|
||||
public static String getDefaultTableStyle(String sheetId, int size, TableConf tableConf) {
|
||||
int row = tableConf.titleRow();
|
||||
String colorTemplate = "{\"data\": [{\"style\": {\"font\": {\"bold\": true, \"clean\": false, \"italic\": false, \"fontSize\": \"10pt/1.5\"}, \"clean\": false, \"hAlign\": 1, \"vAlign\": 1, \"backColor\": \"#000000\", \"foreColor\": \"#ffffff\", \"formatter\": \"\", \"borderType\": \"FULL_BORDER\", \"borderColor\": \"#000000\", \"textDecoration\": 0}, \"ranges\": [\"SHEET_ID!RANG\"]}]}";
|
||||
colorTemplate = colorTemplate.replace("SHEET_ID", sheetId);
|
||||
colorTemplate = colorTemplate.replace("RANG", "A1:" + FsTableUtil.getColumnNameByNuNumber(size) + "1");
|
||||
colorTemplate = colorTemplate.replace("FORE_COLOR", FsConfig.getInstance().getForeColor())
|
||||
.replace("BACK_COLOR", FsConfig.getInstance().getBackColor());
|
||||
colorTemplate = colorTemplate.replace("RANG", "A" + row + ":" + FsTableUtil.getColumnNameByNuNumber(size) + row);
|
||||
colorTemplate = colorTemplate.replace("FORE_COLOR", tableConf.headFontColor())
|
||||
.replace("BACK_COLOR", tableConf.headBackColor());
|
||||
return colorTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据层级分组字段属性
|
||||
*
|
||||
* @param fieldsMap 字段属性映射
|
||||
* @return 按层级分组的映射,key为层级,value为该层级的字段名数组
|
||||
*/
|
||||
public static Map<Integer, List<String>> groupFieldsByLevel(Map<String, FieldProperty> fieldsMap) {
|
||||
Map<Integer, List<String>> levelMap = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, FieldProperty> entry : fieldsMap.entrySet()) {
|
||||
FieldProperty fieldProperty = entry.getValue();
|
||||
if (fieldProperty != null && fieldProperty.getTableProperty() != null) {
|
||||
String[] values = fieldProperty.getTableProperty().value();
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
levelMap.computeIfAbsent(i, k -> new ArrayList<>()).add(values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return levelMap;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String str ="支持1~3个搜索";
|
||||
System.out.println(str.length());
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
package cn.isliu.core.utils;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@ -16,7 +14,7 @@ public class JSONUtil {
|
||||
* @param data HashMap数据
|
||||
* @return 转换后的JsonObject
|
||||
*/
|
||||
public static JsonObject convertHashMapToJsonObject(HashMap<String, Object> data) {
|
||||
public static JsonObject convertMapToJsonObject(Map<String, Object> data) {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
@ -46,4 +44,13 @@ public class JSONUtil {
|
||||
}
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
public static boolean isValidJson(String json) {
|
||||
try {
|
||||
JsonParser.parseString(json);
|
||||
return true;
|
||||
} catch (JsonSyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package cn.isliu.core.utils;
|
||||
|
||||
import cn.isliu.core.annotation.TableConf;
|
||||
import cn.isliu.core.annotation.TableProperty;
|
||||
import cn.isliu.core.converters.FieldValueProcess;
|
||||
import cn.isliu.core.converters.OptionsValueProcess;
|
||||
@ -95,7 +96,9 @@ public class PropertyUtil {
|
||||
// 检查字段是否有@TableProperty注解
|
||||
if (field.isAnnotationPresent(TableProperty.class)) {
|
||||
TableProperty tableProperty = field.getAnnotation(TableProperty.class);
|
||||
String[] propertyValues = tableProperty.value().split("\\."); // 支持多个值
|
||||
String[] values = tableProperty.value();
|
||||
String value = values[values.length - 1];
|
||||
String[] propertyValues = value.split("\\."); // 支持多个值
|
||||
String propertyValue = (propertyValues.length > 0 && !propertyValues[0].isEmpty()) ? propertyValues[0] : field.getName();
|
||||
|
||||
processFieldForMap(field, propertyValue, keyPrefix, valuePrefix, result, depthMap, fieldsWithChildren, parentHasAnnotation);
|
||||
@ -150,8 +153,13 @@ public class PropertyUtil {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String value() {
|
||||
return value;
|
||||
public String[] value() {
|
||||
return new String[]{value};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String desc() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -362,4 +370,53 @@ public class PropertyUtil {
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static <T> TableConf getTableConf(Class<T> clazz) {
|
||||
TableConf tableConf;
|
||||
if (clazz.isAnnotationPresent(TableConf.class)) {
|
||||
tableConf = clazz.getAnnotation(TableConf.class);
|
||||
} else {
|
||||
tableConf = new TableConf() {
|
||||
@Override
|
||||
public Class<? extends Annotation> annotationType() {
|
||||
return TableConf.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int headLine() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int titleRow() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enableCover() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isText() {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean enableDesc() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String headFontColor() {
|
||||
return "#ffffff";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String headBackColor() {
|
||||
return "#000000";
|
||||
}
|
||||
};
|
||||
}
|
||||
return tableConf;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user