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");
 | 
					        
 | 
				
			||||||
        batchPutValuesBuilder.addRow(headers.toArray());
 | 
					        String position = FsTableUtil.getColumnNameByNuNumber(headers.size());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CustomValueService.ValueRequest.BatchPutValuesBuilder batchPutValuesBuilder 
 | 
				
			||||||
 | 
					                = CustomValueService.ValueRequest.batchPutValues();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 获取父级表头
 | 
				
			||||||
 | 
					//        int maxLevel = getMaxLevel(fieldsMap);
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//        Map<Integer, List<String>> levelListMap = groupFieldsByLevel(fieldsMap);
 | 
				
			||||||
 | 
					//        for (int i = maxLevel; i >= 1; i--) {
 | 
				
			||||||
 | 
					//            List<String> values = levelListMap.get(i);
 | 
				
			||||||
 | 
					//            batchPutValuesBuilder.addRange(sheetId + "!A" + i + ":" + position + i);
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//        }
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//        int titleRow = maxLevel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        int titleRow = tableConf.titleRow();
 | 
				
			||||||
 | 
					        if (tableConf.enableDesc()) {
 | 
				
			||||||
 | 
					            int descRow = titleRow + 1;
 | 
				
			||||||
 | 
					            batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + descRow);
 | 
				
			||||||
 | 
					            batchPutValuesBuilder.addRow(headers.toArray());
 | 
				
			||||||
 | 
					            batchPutValuesBuilder.addRow(getDescArray(headers, fieldsMap));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow);
 | 
				
			||||||
 | 
					            batchPutValuesBuilder.addRow(headers.toArray());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					       
 | 
				
			||||||
        return batchPutValuesBuilder.build();
 | 
					        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