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>-->
|
<!-- <version>2.5.3</version>-->
|
||||||
<!-- </plugin>-->
|
<!-- </plugin>-->
|
||||||
<!-- -->
|
<!-- -->
|
||||||
<!-- <!– exec plugin for running tests –>-->
|
<!-- exec plugin for running tests -->
|
||||||
<!-- <plugin>-->
|
<plugin>
|
||||||
<!-- <groupId>org.codehaus.mojo</groupId>-->
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
<!-- <artifactId>exec-maven-plugin</artifactId>-->
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
<!-- <version>3.1.0</version>-->
|
<version>3.1.0</version>
|
||||||
<!-- </plugin>-->
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@ import cn.isliu.core.BaseEntity;
|
|||||||
import cn.isliu.core.FileData;
|
import cn.isliu.core.FileData;
|
||||||
import cn.isliu.core.FsTableData;
|
import cn.isliu.core.FsTableData;
|
||||||
import cn.isliu.core.Sheet;
|
import cn.isliu.core.Sheet;
|
||||||
|
import cn.isliu.core.annotation.TableConf;
|
||||||
import cn.isliu.core.client.FeishuClient;
|
import cn.isliu.core.client.FeishuClient;
|
||||||
import cn.isliu.core.client.FsClient;
|
import cn.isliu.core.client.FsClient;
|
||||||
import cn.isliu.core.config.FsConfig;
|
|
||||||
import cn.isliu.core.enums.ErrorCode;
|
import cn.isliu.core.enums.ErrorCode;
|
||||||
import cn.isliu.core.enums.FileType;
|
import cn.isliu.core.enums.FileType;
|
||||||
import cn.isliu.core.logging.FsLogger;
|
import cn.isliu.core.logging.FsLogger;
|
||||||
@ -44,25 +44,26 @@ public class FsHelper {
|
|||||||
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
|
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
|
||||||
List<String> headers = PropertyUtil.getHeaders(fieldsMap);
|
List<String> headers = PropertyUtil.getHeaders(fieldsMap);
|
||||||
|
|
||||||
|
TableConf tableConf = PropertyUtil.getTableConf(clazz);
|
||||||
|
|
||||||
FeishuClient client = FsClient.getInstance().getClient();
|
FeishuClient client = FsClient.getInstance().getClient();
|
||||||
// 1、创建sheet
|
// 1、创建sheet
|
||||||
String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
|
String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
|
||||||
|
|
||||||
// 2 添加表头数据
|
// 2 添加表头数据
|
||||||
FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers), client);
|
FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf), client);
|
||||||
|
|
||||||
// 3 设置表格样式
|
// 3 设置表格样式
|
||||||
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size()), sheetId, client, spreadsheetToken);
|
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size(), tableConf), sheetId, client, spreadsheetToken);
|
||||||
|
|
||||||
// 4 设置单元格为文本格式
|
// 4 设置单元格为文本格式
|
||||||
FsConfig fsConfig = FsConfig.getInstance();
|
if (tableConf.isText()) {
|
||||||
if (fsConfig.isCellText()) {
|
|
||||||
String column = FsTableUtil.getColumnNameByNuNumber(headers.size());
|
String column = FsTableUtil.getColumnNameByNuNumber(headers.size());
|
||||||
FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
|
FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5 设置表格下拉
|
// 5 设置表格下拉
|
||||||
FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId);
|
FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc());
|
||||||
return sheetId;
|
return sheetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +83,8 @@ public class FsHelper {
|
|||||||
List<T> results = new ArrayList<>();
|
List<T> results = new ArrayList<>();
|
||||||
FeishuClient client = FsClient.getInstance().getClient();
|
FeishuClient client = FsClient.getInstance().getClient();
|
||||||
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
|
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
|
||||||
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
|
TableConf tableConf = PropertyUtil.getTableConf(clazz);
|
||||||
|
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf);
|
||||||
|
|
||||||
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
|
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
|
||||||
List<String> fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList());
|
List<String> fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList());
|
||||||
@ -90,11 +92,15 @@ public class FsHelper {
|
|||||||
fsTableDataList.forEach(tableData -> {
|
fsTableDataList.forEach(tableData -> {
|
||||||
Object data = tableData.getData();
|
Object data = tableData.getData();
|
||||||
if (data instanceof HashMap) {
|
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);
|
Map<String, Object> dataMap = ConvertFieldUtil.convertPositionToField(jsonObject, fieldsMap);
|
||||||
T t = GenerateUtil.generateInstance(fieldPathList, clazz, dataMap);
|
T t = GenerateUtil.generateInstance(fieldPathList, clazz, dataMap);
|
||||||
if (t instanceof BaseEntity) {
|
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);
|
results.add(t);
|
||||||
}
|
}
|
||||||
@ -120,10 +126,11 @@ public class FsHelper {
|
|||||||
|
|
||||||
Class<?> aClass = dataList.get(0).getClass();
|
Class<?> aClass = dataList.get(0).getClass();
|
||||||
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass);
|
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass);
|
||||||
|
TableConf tableConf = PropertyUtil.getTableConf(aClass);
|
||||||
|
|
||||||
FeishuClient client = FsClient.getInstance().getClient();
|
FeishuClient client = FsClient.getInstance().getClient();
|
||||||
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
|
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
|
||||||
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
|
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf);
|
||||||
Map<String, Integer> currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow));
|
Map<String, Integer> currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow));
|
||||||
|
|
||||||
final Integer[] row = {0};
|
final Integer[] row = {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<>();
|
Map<String, String> fieldMap = new HashMap<>();
|
||||||
fieldsMap.forEach((field, fieldProperty) -> fieldMap.put(field, fieldProperty.getField()));
|
fieldsMap.forEach((field, fieldProperty) -> fieldMap.put(field, fieldProperty.getField()));
|
||||||
@ -142,7 +149,6 @@ public class FsHelper {
|
|||||||
CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
|
CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
|
||||||
|
|
||||||
List<FileData> fileDataList = new ArrayList<>();
|
List<FileData> fileDataList = new ArrayList<>();
|
||||||
FsConfig fsConfig = FsConfig.getInstance();
|
|
||||||
|
|
||||||
AtomicInteger rowCount = new AtomicInteger(row[0] + 1);
|
AtomicInteger rowCount = new AtomicInteger(row[0] + 1);
|
||||||
|
|
||||||
@ -155,7 +161,7 @@ public class FsHelper {
|
|||||||
if (uniqueId != null && rowNum.get() != null) {
|
if (uniqueId != null && rowNum.get() != null) {
|
||||||
rowNum.set(rowNum.get() + 1);
|
rowNum.set(rowNum.get() + 1);
|
||||||
values.forEach((field, fieldValue) -> {
|
values.forEach((field, fieldValue) -> {
|
||||||
if (!fsConfig.isCover() && fieldValue == null) {
|
if (!tableConf.enableCover() && fieldValue == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +183,7 @@ public class FsHelper {
|
|||||||
} else {
|
} else {
|
||||||
int rowCou = rowCount.incrementAndGet();
|
int rowCou = rowCount.incrementAndGet();
|
||||||
values.forEach((field, fieldValue) -> {
|
values.forEach((field, fieldValue) -> {
|
||||||
if (!fsConfig.isCover() && fieldValue == null) {
|
if (!tableConf.enableCover() && fieldValue == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package cn.isliu.core;
|
package cn.isliu.core;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实体类基类
|
* 实体类基类
|
||||||
*
|
*
|
||||||
@ -12,6 +15,14 @@ public abstract class BaseEntity {
|
|||||||
* 唯一标识符,用于标识表格中的行数据
|
* 唯一标识符,用于标识表格中的行数据
|
||||||
*/
|
*/
|
||||||
public String uniqueId;
|
public String uniqueId;
|
||||||
|
/**
|
||||||
|
* 行号,用于标识表格中的行位置
|
||||||
|
*/
|
||||||
|
private Integer row;
|
||||||
|
/**
|
||||||
|
* 行数据,用于存储与表格行相关的信息
|
||||||
|
*/
|
||||||
|
private Map<String, Object> rowData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取唯一标识符
|
* 获取唯一标识符
|
||||||
@ -30,4 +41,32 @@ public abstract class BaseEntity {
|
|||||||
public void setUniqueId(String uniqueId) {
|
public void setUniqueId(String uniqueId) {
|
||||||
this.uniqueId = 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 列名字符串
|
* @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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -65,6 +65,13 @@ public enum ErrorCode implements BaseEnum {
|
|||||||
ENCRYPTION_FAILED("FS604", "Encryption failed", ErrorCategory.SECURITY),
|
ENCRYPTION_FAILED("FS604", "Encryption failed", ErrorCategory.SECURITY),
|
||||||
DECRYPTION_FAILED("FS605", "Decryption 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)
|
// 业务逻辑相关错误 (FS700-FS799)
|
||||||
BUSINESS_LOGIC_ERROR("FS700", "Business logic error", ErrorCategory.BUSINESS),
|
BUSINESS_LOGIC_ERROR("FS700", "Business logic error", ErrorCategory.BUSINESS),
|
||||||
INVALID_OPERATION("FS701", "Invalid operation", ErrorCategory.BUSINESS),
|
INVALID_OPERATION("FS701", "Invalid operation", ErrorCategory.BUSINESS),
|
||||||
@ -247,6 +254,8 @@ public enum ErrorCode implements BaseEnum {
|
|||||||
case API_SERVER_ERROR:
|
case API_SERVER_ERROR:
|
||||||
case SERVICE_UNAVAILABLE:
|
case SERVICE_UNAVAILABLE:
|
||||||
case CONNECTION_POOL_EXHAUSTED:
|
case CONNECTION_POOL_EXHAUSTED:
|
||||||
|
case TOKEN_FETCH_FAILED:
|
||||||
|
case TOKEN_REFRESH_FAILED:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
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 列名字符串
|
* @return 列名字符串
|
||||||
*/
|
*/
|
||||||
public String getFieldName() {
|
public String[] getFieldName() {
|
||||||
return tableProperty.value();
|
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;
|
package cn.isliu.core.service;
|
||||||
|
|
||||||
import cn.isliu.core.client.FeishuClient;
|
import cn.isliu.core.client.FeishuClient;
|
||||||
|
import cn.isliu.core.exception.FsHelperException;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.lark.oapi.core.utils.Jsons;
|
import com.lark.oapi.core.utils.Jsons;
|
||||||
@ -17,6 +18,7 @@ public abstract class AbstractFeishuApiService {
|
|||||||
protected final FeishuClient feishuClient;
|
protected final FeishuClient feishuClient;
|
||||||
protected final OkHttpClient httpClient;
|
protected final OkHttpClient httpClient;
|
||||||
protected final Gson gson;
|
protected final Gson gson;
|
||||||
|
protected final TenantTokenManager tokenManager;
|
||||||
|
|
||||||
protected static final String BASE_URL = "https://open.feishu.cn/open-apis";
|
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");
|
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.feishuClient = feishuClient;
|
||||||
this.httpClient = feishuClient.getHttpClient();
|
this.httpClient = feishuClient.getHttpClient();
|
||||||
this.gson = Jsons.DEFAULT;
|
this.gson = Jsons.DEFAULT;
|
||||||
|
this.tokenManager = new TenantTokenManager(feishuClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取租户访问令牌
|
* 获取租户访问令牌
|
||||||
*
|
*
|
||||||
|
* 使用TenantTokenManager进行智能的token管理,包括缓存、过期检测和自动刷新。
|
||||||
|
*
|
||||||
* @return 访问令牌
|
* @return 访问令牌
|
||||||
* @throws IOException 请求异常
|
* @throws IOException 请求异常
|
||||||
*/
|
*/
|
||||||
protected String getTenantAccessToken() throws IOException {
|
protected String getTenantAccessToken() throws IOException {
|
||||||
Map<String, String> params = new HashMap<>();
|
try {
|
||||||
params.put("app_id", feishuClient.getAppId());
|
return tokenManager.getCachedTenantAccessToken();
|
||||||
params.put("app_secret", feishuClient.getAppSecret());
|
} catch (FsHelperException e) {
|
||||||
|
throw new IOException("Failed to get tenant access token: " + e.getMessage(), e);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1040,6 +1040,11 @@ public class CustomValueService extends AbstractFeishuApiService {
|
|||||||
return this;
|
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 {
|
public static class ValueRangeItem {
|
||||||
private String range;
|
private String range;
|
||||||
private String type;
|
private String type;
|
||||||
|
private String majorDimension;
|
||||||
private List<List<Object>> values;
|
private List<List<Object>> values;
|
||||||
|
|
||||||
public ValueRangeItem() {
|
public ValueRangeItem() {
|
||||||
@ -1644,6 +1650,14 @@ public class CustomValueService extends AbstractFeishuApiService {
|
|||||||
this.range = range;
|
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;
|
package cn.isliu.core.utils;
|
||||||
|
|
||||||
import cn.isliu.core.*;
|
import cn.isliu.core.*;
|
||||||
|
import cn.isliu.core.annotation.TableConf;
|
||||||
import cn.isliu.core.annotation.TableProperty;
|
import cn.isliu.core.annotation.TableProperty;
|
||||||
import cn.isliu.core.client.FsClient;
|
import cn.isliu.core.client.FsClient;
|
||||||
|
|
||||||
import cn.isliu.core.config.FsConfig;
|
|
||||||
import cn.isliu.core.converters.OptionsValueProcess;
|
import cn.isliu.core.converters.OptionsValueProcess;
|
||||||
import cn.isliu.core.enums.BaseEnum;
|
import cn.isliu.core.enums.BaseEnum;
|
||||||
import cn.isliu.core.enums.TypeEnum;
|
import cn.isliu.core.enums.TypeEnum;
|
||||||
import cn.isliu.core.pojo.FieldProperty;
|
import cn.isliu.core.pojo.FieldProperty;
|
||||||
import cn.isliu.core.service.CustomValueService;
|
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.lang.reflect.InvocationTargetException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,7 +36,7 @@ public class FsTableUtil {
|
|||||||
* @param spreadsheetToken 电子表格Token
|
* @param spreadsheetToken 电子表格Token
|
||||||
* @return 飞书表格数据列表
|
* @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();
|
GridProperties gridProperties = sheet.getGridProperties();
|
||||||
@ -68,12 +72,12 @@ public class FsTableUtil {
|
|||||||
List<FsTableData> dataList = getFsTableData(tableData);
|
List<FsTableData> dataList = getFsTableData(tableData);
|
||||||
Map<String, String> titleMap = new HashMap<>();
|
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 -> {
|
.ifPresent(d -> {
|
||||||
Map<String, String> map = (Map<String, String>) d.getData();
|
Map<String, String> map = (Map<String, String>) d.getData();
|
||||||
titleMap.putAll(map);
|
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> resultMap = new HashMap<>();
|
||||||
|
|
||||||
Map<String, Object> map = (Map<String, Object>) item.getData();
|
Map<String, Object> map = (Map<String, Object>) item.getData();
|
||||||
@ -260,14 +264,14 @@ public class FsTableUtil {
|
|||||||
return columnName.toString();
|
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();
|
GridProperties gridProperties = sheet.getGridProperties();
|
||||||
int colCount = gridProperties.getColumnCount();
|
int colCount = gridProperties.getColumnCount();
|
||||||
|
|
||||||
Map<String, String> resultMap = new TreeMap<>();
|
Map<String, String> resultMap = new TreeMap<>();
|
||||||
ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken,
|
ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken,
|
||||||
"A" + FsConfig.getInstance().getTitleLine(),
|
"A" + tableConf.titleRow(),
|
||||||
getColumnName(colCount - 1) + FsConfig.getInstance().getTitleLine(), FsClient.getInstance().getClient());
|
getColumnName(colCount - 1) + tableConf.titleRow(), FsClient.getInstance().getClient());
|
||||||
if (valuesBatch != null) {
|
if (valuesBatch != null) {
|
||||||
List<ValueRange> valueRanges = valuesBatch.getValueRanges();
|
List<ValueRange> valueRanges = valuesBatch.getValueRanges();
|
||||||
if (valueRanges != null && !valueRanges.isEmpty()) {
|
if (valueRanges != null && !valueRanges.isEmpty()) {
|
||||||
@ -288,8 +292,9 @@ public class FsTableUtil {
|
|||||||
return resultMap;
|
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());
|
List<Object> list = Arrays.asList(headers.toArray());
|
||||||
|
int line = getMaxLevel(fieldsMap) + (enableDesc ? 2 : 1);
|
||||||
fieldsMap.forEach((field, fieldProperty) -> {
|
fieldsMap.forEach((field, fieldProperty) -> {
|
||||||
TableProperty tableProperty = fieldProperty.getTableProperty();
|
TableProperty tableProperty = fieldProperty.getTableProperty();
|
||||||
String position = "";
|
String position = "";
|
||||||
@ -300,7 +305,6 @@ public class FsTableUtil {
|
|||||||
position = FsTableUtil.getColumnNameByNuNumber(i + 1);
|
position = FsTableUtil.getColumnNameByNuNumber(i + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int line = FsConfig.getInstance().getTitleLine() + 1;
|
|
||||||
|
|
||||||
if (tableProperty.enumClass() != BaseEnum.class) {
|
if (tableProperty.enumClass() != BaseEnum.class) {
|
||||||
FsApiUtil.setOptions(sheetId, FsClient.getInstance().getClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200,
|
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) {
|
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
|
||||||
CustomValueService.ValueRequest.BatchPutValuesBuilder batchPutValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
|
Map<String, FieldProperty> fieldsMap, TableConf tableConf) {
|
||||||
batchPutValuesBuilder.addRange(sheetId + "!A1:" + FsTableUtil.getColumnNameByNuNumber(headers.size()) + "1");
|
|
||||||
|
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(headers.toArray());
|
||||||
|
batchPutValuesBuilder.addRow(getDescArray(headers, fieldsMap));
|
||||||
|
} else {
|
||||||
|
batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow);
|
||||||
|
batchPutValuesBuilder.addRow(headers.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
return batchPutValuesBuilder.build();
|
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\"]}]}";
|
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("SHEET_ID", sheetId);
|
||||||
colorTemplate = colorTemplate.replace("RANG", "A1:" + FsTableUtil.getColumnNameByNuNumber(size) + "1");
|
colorTemplate = colorTemplate.replace("RANG", "A" + row + ":" + FsTableUtil.getColumnNameByNuNumber(size) + row);
|
||||||
colorTemplate = colorTemplate.replace("FORE_COLOR", FsConfig.getInstance().getForeColor())
|
colorTemplate = colorTemplate.replace("FORE_COLOR", tableConf.headFontColor())
|
||||||
.replace("BACK_COLOR", FsConfig.getInstance().getBackColor());
|
.replace("BACK_COLOR", tableConf.headBackColor());
|
||||||
return colorTemplate;
|
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;
|
package cn.isliu.core.utils;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.*;
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -16,7 +14,7 @@ public class JSONUtil {
|
|||||||
* @param data HashMap数据
|
* @param data HashMap数据
|
||||||
* @return 转换后的JsonObject
|
* @return 转换后的JsonObject
|
||||||
*/
|
*/
|
||||||
public static JsonObject convertHashMapToJsonObject(HashMap<String, Object> data) {
|
public static JsonObject convertMapToJsonObject(Map<String, Object> data) {
|
||||||
JsonObject jsonObject = new JsonObject();
|
JsonObject jsonObject = new JsonObject();
|
||||||
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
||||||
String key = entry.getKey();
|
String key = entry.getKey();
|
||||||
@ -46,4 +44,13 @@ public class JSONUtil {
|
|||||||
}
|
}
|
||||||
return jsonObject;
|
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;
|
package cn.isliu.core.utils;
|
||||||
|
|
||||||
|
import cn.isliu.core.annotation.TableConf;
|
||||||
import cn.isliu.core.annotation.TableProperty;
|
import cn.isliu.core.annotation.TableProperty;
|
||||||
import cn.isliu.core.converters.FieldValueProcess;
|
import cn.isliu.core.converters.FieldValueProcess;
|
||||||
import cn.isliu.core.converters.OptionsValueProcess;
|
import cn.isliu.core.converters.OptionsValueProcess;
|
||||||
@ -95,7 +96,9 @@ public class PropertyUtil {
|
|||||||
// 检查字段是否有@TableProperty注解
|
// 检查字段是否有@TableProperty注解
|
||||||
if (field.isAnnotationPresent(TableProperty.class)) {
|
if (field.isAnnotationPresent(TableProperty.class)) {
|
||||||
TableProperty tableProperty = field.getAnnotation(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();
|
String propertyValue = (propertyValues.length > 0 && !propertyValues[0].isEmpty()) ? propertyValues[0] : field.getName();
|
||||||
|
|
||||||
processFieldForMap(field, propertyValue, keyPrefix, valuePrefix, result, depthMap, fieldsWithChildren, parentHasAnnotation);
|
processFieldForMap(field, propertyValue, keyPrefix, valuePrefix, result, depthMap, fieldsWithChildren, parentHasAnnotation);
|
||||||
@ -150,8 +153,13 @@ public class PropertyUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String value() {
|
public String[] value() {
|
||||||
return value;
|
return new String[]{value};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String desc() {
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -362,4 +370,53 @@ public class PropertyUtil {
|
|||||||
.map(Map.Entry::getKey)
|
.map(Map.Entry::getKey)
|
||||||
.collect(Collectors.toList());
|
.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