refactor(logging): 重构日志记录方式
- 使用自定义 FsLogger 替代 java.util.logging.Logger - 统一日志记录格式,支持错误码和异常信息 - 移除冗余的 FsClientUtil 类 - 重构 FsConfig 类,使用单例模式 - 更新相关类的日志记录方式
This commit is contained in:
		
							parent
							
								
									5b9695b7e8
								
							
						
					
					
						commit
						dfd230ca04
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,4 +1,5 @@
 | 
				
			|||||||
target/
 | 
					target/
 | 
				
			||||||
 | 
					test/
 | 
				
			||||||
!.mvn/wrapper/maven-wrapper.jar
 | 
					!.mvn/wrapper/maven-wrapper.jar
 | 
				
			||||||
!**/src/main/**/target/
 | 
					!**/src/main/**/target/
 | 
				
			||||||
!**/src/test/**/target/
 | 
					!**/src/test/**/target/
 | 
				
			||||||
@ -12,6 +13,7 @@ target/
 | 
				
			|||||||
*.iml
 | 
					*.iml
 | 
				
			||||||
*.ipr
 | 
					*.ipr
 | 
				
			||||||
.idea
 | 
					.idea
 | 
				
			||||||
 | 
					.kiro
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Eclipse ###
 | 
					### Eclipse ###
 | 
				
			||||||
.apt_generated
 | 
					.apt_generated
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										92
									
								
								pom.xml
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										92
									
								
								pom.xml
									
									
									
									
									
								
							@ -6,7 +6,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <groupId>cn.isliu</groupId>
 | 
					    <groupId>cn.isliu</groupId>
 | 
				
			||||||
    <artifactId>feishu-table-helper</artifactId>
 | 
					    <artifactId>feishu-table-helper</artifactId>
 | 
				
			||||||
    <version>0.0.1</version>
 | 
					    <version>0.0.2</version>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <name>${project.groupId}:${project.artifactId}</name>
 | 
					    <name>${project.groupId}:${project.artifactId}</name>
 | 
				
			||||||
    <description>
 | 
					    <description>
 | 
				
			||||||
@ -57,6 +57,11 @@
 | 
				
			|||||||
            <artifactId>okhttp</artifactId>
 | 
					            <artifactId>okhttp</artifactId>
 | 
				
			||||||
            <version>4.12.0</version>
 | 
					            <version>4.12.0</version>
 | 
				
			||||||
        </dependency>
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>com.squareup.okhttp3</groupId>
 | 
				
			||||||
 | 
					            <artifactId>logging-interceptor</artifactId>
 | 
				
			||||||
 | 
					            <version>4.12.0</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
        <dependency>
 | 
					        <dependency>
 | 
				
			||||||
            <groupId>com.google.code.gson</groupId>
 | 
					            <groupId>com.google.code.gson</groupId>
 | 
				
			||||||
            <artifactId>gson</artifactId>
 | 
					            <artifactId>gson</artifactId>
 | 
				
			||||||
@ -105,46 +110,53 @@
 | 
				
			|||||||
                </executions>
 | 
					                </executions>
 | 
				
			||||||
            </plugin>
 | 
					            </plugin>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <!-- gpg plugin,用于签名认证 -->
 | 
					<!--            <!– gpg plugin,用于签名认证 –>-->
 | 
				
			||||||
            <plugin>
 | 
					<!--            <plugin>-->
 | 
				
			||||||
                <groupId>org.apache.maven.plugins</groupId>
 | 
					<!--                <groupId>org.apache.maven.plugins</groupId>-->
 | 
				
			||||||
                <artifactId>maven-gpg-plugin</artifactId>
 | 
					<!--                <artifactId>maven-gpg-plugin</artifactId>-->
 | 
				
			||||||
                <version>1.6</version>
 | 
					<!--                <version>1.6</version>-->
 | 
				
			||||||
                <executions>
 | 
					<!--                <executions>-->
 | 
				
			||||||
                    <execution>
 | 
					<!--                    <execution>-->
 | 
				
			||||||
                        <id>sign-artifacts</id>
 | 
					<!--                        <id>sign-artifacts</id>-->
 | 
				
			||||||
                        <phase>verify</phase>
 | 
					<!--                        <phase>verify</phase>-->
 | 
				
			||||||
                        <goals>
 | 
					<!--                        <goals>-->
 | 
				
			||||||
                            <goal>sign</goal>
 | 
					<!--                            <goal>sign</goal>-->
 | 
				
			||||||
                        </goals>
 | 
					<!--                        </goals>-->
 | 
				
			||||||
                    </execution>
 | 
					<!--                    </execution>-->
 | 
				
			||||||
                </executions>
 | 
					<!--                </executions>-->
 | 
				
			||||||
            </plugin>
 | 
					<!--            </plugin>-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <!-- 发布到私服时需要注释掉下面两个插件 -->
 | 
					<!--            <!– 发布到私服时需要注释掉下面两个插件 –>-->
 | 
				
			||||||
            <!--staging puglin,用于自动执行发布阶段(免手动)-->
 | 
					<!--            <!–staging puglin,用于自动执行发布阶段(免手动)–>-->
 | 
				
			||||||
            <plugin>
 | 
					<!--            <plugin>-->
 | 
				
			||||||
                <groupId>org.sonatype.central</groupId>
 | 
					<!--                <groupId>org.sonatype.central</groupId>-->
 | 
				
			||||||
                <artifactId>central-publishing-maven-plugin</artifactId>
 | 
					<!--                <artifactId>central-publishing-maven-plugin</artifactId>-->
 | 
				
			||||||
                <version>0.5.0</version>
 | 
					<!--                <version>0.5.0</version>-->
 | 
				
			||||||
                <extensions>true</extensions>
 | 
					<!--                <extensions>true</extensions>-->
 | 
				
			||||||
                <configuration>
 | 
					<!--                <configuration>-->
 | 
				
			||||||
                    <!-- 这里的publishingServerId是在settings.xml中配置的server认证信息 -->
 | 
					<!--                    <!– 这里的publishingServerId是在settings.xml中配置的server认证信息 –>-->
 | 
				
			||||||
                    <publishingServerId>central</publishingServerId>
 | 
					<!--                    <publishingServerId>central</publishingServerId>-->
 | 
				
			||||||
                    <!-- 这里的autoPublish是自动发布,而不是手动发布 -->
 | 
					<!--                    <!– 这里的autoPublish是自动发布,而不是手动发布 –>-->
 | 
				
			||||||
                    <autoPublish>true</autoPublish>
 | 
					<!--                    <autoPublish>true</autoPublish>-->
 | 
				
			||||||
                    <!-- 这里的waitUntil配置为published是等待发布完成,因为发布完成的时间比较长,所以可以不加这个参数 -->
 | 
					<!--                    <!– 这里的waitUntil配置为published是等待发布完成,因为发布完成的时间比较长,所以可以不加这个参数 –>-->
 | 
				
			||||||
                    <waitUntil>published</waitUntil>
 | 
					<!--                    <waitUntil>published</waitUntil>-->
 | 
				
			||||||
                    <!-- 这里的deploymentName是发布到中央仓库的名称 -->
 | 
					<!--                    <!– 这里的deploymentName是发布到中央仓库的名称 –>-->
 | 
				
			||||||
                    <deploymentName>${project.groupId}:${project.artifactId}:${project.version}</deploymentName>
 | 
					<!--                    <deploymentName>${project.groupId}:${project.artifactId}:${project.version}</deploymentName>-->
 | 
				
			||||||
                </configuration>
 | 
					<!--                </configuration>-->
 | 
				
			||||||
            </plugin>
 | 
					<!--            </plugin>-->
 | 
				
			||||||
            <!-- release plugin,用于发布到release仓库部署插件 -->
 | 
					<!--            <!– release plugin,用于发布到release仓库部署插件 –>-->
 | 
				
			||||||
            <plugin>
 | 
					<!--            <plugin>-->
 | 
				
			||||||
                <groupId>org.apache.maven.plugins</groupId>
 | 
					<!--                <groupId>org.apache.maven.plugins</groupId>-->
 | 
				
			||||||
                <artifactId>maven-release-plugin</artifactId>
 | 
					<!--                <artifactId>maven-release-plugin</artifactId>-->
 | 
				
			||||||
                <version>2.5.3</version>
 | 
					<!--                <version>2.5.3</version>-->
 | 
				
			||||||
            </plugin>
 | 
					<!--            </plugin>-->
 | 
				
			||||||
 | 
					<!--            -->
 | 
				
			||||||
 | 
					<!--            <!– exec plugin for running tests –>-->
 | 
				
			||||||
 | 
					<!--            <plugin>-->
 | 
				
			||||||
 | 
					<!--                <groupId>org.codehaus.mojo</groupId>-->
 | 
				
			||||||
 | 
					<!--                <artifactId>exec-maven-plugin</artifactId>-->
 | 
				
			||||||
 | 
					<!--                <version>3.1.0</version>-->
 | 
				
			||||||
 | 
					<!--            </plugin>-->
 | 
				
			||||||
        </plugins>
 | 
					        </plugins>
 | 
				
			||||||
    </build>
 | 
					    </build>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,8 @@ package cn.isliu;
 | 
				
			|||||||
import cn.isliu.core.BaseEntity;
 | 
					import cn.isliu.core.BaseEntity;
 | 
				
			||||||
import cn.isliu.core.FsTableData;
 | 
					import cn.isliu.core.FsTableData;
 | 
				
			||||||
import cn.isliu.core.Sheet;
 | 
					import cn.isliu.core.Sheet;
 | 
				
			||||||
 | 
					import cn.isliu.core.client.FeishuClient;
 | 
				
			||||||
 | 
					import cn.isliu.core.client.FsClient;
 | 
				
			||||||
import cn.isliu.core.config.FsConfig;
 | 
					import cn.isliu.core.config.FsConfig;
 | 
				
			||||||
import cn.isliu.core.pojo.FieldProperty;
 | 
					import cn.isliu.core.pojo.FieldProperty;
 | 
				
			||||||
import cn.isliu.core.service.CustomValueService;
 | 
					import cn.isliu.core.service.CustomValueService;
 | 
				
			||||||
@ -38,19 +40,21 @@ 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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        FeishuClient client = FsClient.getInstance().getClient();
 | 
				
			||||||
        // 1、创建sheet
 | 
					        // 1、创建sheet
 | 
				
			||||||
        String sheetId = FsApiUtil.createSheet(sheetName, FsClientUtil.getFeishuClient(), spreadsheetToken);
 | 
					        String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 2 添加表头数据
 | 
					        // 2 添加表头数据
 | 
				
			||||||
        FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers), FsClientUtil.getFeishuClient());
 | 
					        FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers), client);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 3 设置表格样式
 | 
					        // 3 设置表格样式
 | 
				
			||||||
        FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size()), sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken);
 | 
					        FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size()), sheetId, client, spreadsheetToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 4 设置单元格为文本格式
 | 
					        // 4 设置单元格为文本格式
 | 
				
			||||||
        if (FsConfig.CELL_TEXT) {
 | 
					        FsConfig fsConfig = FsConfig.getInstance();
 | 
				
			||||||
 | 
					        if (fsConfig.isCellText()) {
 | 
				
			||||||
            String column = FsTableUtil.getColumnNameByNuNumber(headers.size());
 | 
					            String column = FsTableUtil.getColumnNameByNuNumber(headers.size());
 | 
				
			||||||
            FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, FsClientUtil.getFeishuClient(), spreadsheetToken);
 | 
					            FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 5 设置表格下拉
 | 
					        // 5 设置表格下拉
 | 
				
			||||||
@ -72,7 +76,8 @@ public class FsHelper {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    public static <T> List<T> read(String sheetId, String spreadsheetToken, Class<T> clazz) {
 | 
					    public static <T> List<T> read(String sheetId, String spreadsheetToken, Class<T> clazz) {
 | 
				
			||||||
        List<T> results = new ArrayList<>();
 | 
					        List<T> results = new ArrayList<>();
 | 
				
			||||||
        Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken);
 | 
					        FeishuClient client = FsClient.getInstance().getClient();
 | 
				
			||||||
 | 
					        Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
 | 
				
			||||||
        List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
 | 
					        List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
 | 
					        Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
 | 
				
			||||||
@ -112,7 +117,8 @@ 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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken);
 | 
					        FeishuClient client = FsClient.getInstance().getClient();
 | 
				
			||||||
 | 
					        Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
 | 
				
			||||||
        List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
 | 
					        List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
 | 
				
			||||||
        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));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,6 +137,8 @@ public class FsHelper {
 | 
				
			|||||||
        // 初始化批量插入对象
 | 
					        // 初始化批量插入对象
 | 
				
			||||||
        CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
 | 
					        CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        FsConfig fsConfig = FsConfig.getInstance();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        AtomicInteger rowCount = new AtomicInteger(row[0] + 1);
 | 
					        AtomicInteger rowCount = new AtomicInteger(row[0] + 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (T data : dataList) {
 | 
					        for (T data : dataList) {
 | 
				
			||||||
@ -142,7 +150,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 (!fsConfig.isCover() && fieldValue == null) {
 | 
				
			||||||
                        return;
 | 
					                        return;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -153,7 +161,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 (!fsConfig.isCover() && fieldValue == null) {
 | 
				
			||||||
                        return;
 | 
					                        return;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -168,9 +176,9 @@ public class FsHelper {
 | 
				
			|||||||
        int rowTotal = sheet.getGridProperties().getRowCount();
 | 
					        int rowTotal = sheet.getGridProperties().getRowCount();
 | 
				
			||||||
        int rowNum = rowCount.get();
 | 
					        int rowNum = rowCount.get();
 | 
				
			||||||
        if (rowNum > rowTotal) {
 | 
					        if (rowNum > rowTotal) {
 | 
				
			||||||
            FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, FsClientUtil.getFeishuClient());
 | 
					            FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, client);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return FsApiUtil.batchPutValues(sheetId, spreadsheetToken, resultValuesBuilder.build(), FsClientUtil.getFeishuClient());
 | 
					        return FsApiUtil.batchPutValues(sheetId, spreadsheetToken, resultValuesBuilder.build(), client);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										97
									
								
								src/main/java/cn/isliu/core/client/FsClient.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										97
									
								
								src/main/java/cn/isliu/core/client/FsClient.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					package cn.isliu.core.client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 线程安全的飞书客户端管理器
 | 
				
			||||||
 | 
					 * 使用双重检查锁定单例模式确保线程安全
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class FsClient {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private static volatile FsClient instance;
 | 
				
			||||||
 | 
					    private volatile FeishuClient client;
 | 
				
			||||||
 | 
					    private final Object lock = new Object();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 私有构造函数防止外部实例化
 | 
				
			||||||
 | 
					    private FsClient() {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取单例实例 - 使用双重检查锁定模式
 | 
				
			||||||
 | 
					     * @return FeishuClientManager实例
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static FsClient getInstance() {
 | 
				
			||||||
 | 
					        if (instance == null) {
 | 
				
			||||||
 | 
					            synchronized (FsClient.class) {
 | 
				
			||||||
 | 
					                if (instance == null) {
 | 
				
			||||||
 | 
					                    instance = new FsClient();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return instance;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 线程安全的客户端获取
 | 
				
			||||||
 | 
					     * @return FeishuClient实例
 | 
				
			||||||
 | 
					     * @throws IllegalStateException 如果客户端未初始化
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FeishuClient getClient() {
 | 
				
			||||||
 | 
					        FeishuClient currentClient = client;
 | 
				
			||||||
 | 
					        if (currentClient == null) {
 | 
				
			||||||
 | 
					            throw new IllegalStateException("FeishuClient not initialized. Please call initializeClient first.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return currentClient;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 线程安全的客户端初始化
 | 
				
			||||||
 | 
					     * @param appId 飞书应用ID
 | 
				
			||||||
 | 
					     * @param appSecret 飞书应用密钥
 | 
				
			||||||
 | 
					     * @return 初始化的FeishuClient实例
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FeishuClient initializeClient(String appId, String appSecret) {
 | 
				
			||||||
 | 
					        if (appId == null || appId.trim().isEmpty()) {
 | 
				
			||||||
 | 
					            throw new IllegalArgumentException("appId cannot be null or empty");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (appSecret == null || appSecret.trim().isEmpty()) {
 | 
				
			||||||
 | 
					            throw new IllegalArgumentException("appSecret cannot be null or empty");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (client == null) {
 | 
				
			||||||
 | 
					            synchronized (lock) {
 | 
				
			||||||
 | 
					                if (client == null) {
 | 
				
			||||||
 | 
					                    client = FeishuClient.newBuilder(appId, appSecret).build();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return client;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 设置客户端实例(用于外部已构建的客户端)
 | 
				
			||||||
 | 
					     * @param feishuClient 外部构建的FeishuClient实例
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void setClient(FeishuClient feishuClient) {
 | 
				
			||||||
 | 
					        if (feishuClient == null) {
 | 
				
			||||||
 | 
					            throw new IllegalArgumentException("FeishuClient cannot be null");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        synchronized (lock) {
 | 
				
			||||||
 | 
					            this.client = feishuClient;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检查客户端是否已初始化
 | 
				
			||||||
 | 
					     * @return true如果客户端已初始化,否则false
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean isInitialized() {
 | 
				
			||||||
 | 
					        return client != null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 重置客户端(主要用于测试)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public synchronized void resetForTesting() {
 | 
				
			||||||
 | 
					        client = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,384 @@
 | 
				
			|||||||
 | 
					package cn.isliu.core.client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.isliu.core.logging.FsLogger;
 | 
				
			||||||
 | 
					import okhttp3.*;
 | 
				
			||||||
 | 
					import okhttp3.logging.HttpLoggingInterceptor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 优化的HTTP客户端工厂
 | 
				
			||||||
 | 
					 * 提供连接池优化、超时配置、重试机制和监控功能
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class OptimizedHttpClientFactory {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 连接池配置常量
 | 
				
			||||||
 | 
					    private static final int MAX_IDLE_CONNECTIONS = 10;
 | 
				
			||||||
 | 
					    private static final long KEEP_ALIVE_DURATION = 5; // minutes
 | 
				
			||||||
 | 
					    private static final int CONNECT_TIMEOUT = 30; // seconds
 | 
				
			||||||
 | 
					    private static final int READ_TIMEOUT = 60; // seconds
 | 
				
			||||||
 | 
					    private static final int WRITE_TIMEOUT = 60; // seconds
 | 
				
			||||||
 | 
					    private static final int CALL_TIMEOUT = 120; // seconds
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 重试配置常量
 | 
				
			||||||
 | 
					    private static final int MAX_RETRY_ATTEMPTS = 3;
 | 
				
			||||||
 | 
					    private static final long INITIAL_RETRY_DELAY = 1000; // milliseconds
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 创建优化的HTTP客户端
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 配置优化的OkHttpClient实例
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static OkHttpClient createOptimizedClient() {
 | 
				
			||||||
 | 
					        return createOptimizedClient(new ClientConfig());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 使用自定义配置创建优化的HTTP客户端
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param config 客户端配置
 | 
				
			||||||
 | 
					     * @return 配置优化的OkHttpClient实例
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static OkHttpClient createOptimizedClient(ClientConfig config) {
 | 
				
			||||||
 | 
					        // 创建优化的连接池
 | 
				
			||||||
 | 
					        ConnectionPool connectionPool = new ConnectionPool(
 | 
				
			||||||
 | 
					            config.maxIdleConnections,
 | 
				
			||||||
 | 
					            config.keepAliveDuration,
 | 
				
			||||||
 | 
					            TimeUnit.MINUTES
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 创建HTTP客户端构建器
 | 
				
			||||||
 | 
					        OkHttpClient.Builder builder = new OkHttpClient.Builder()
 | 
				
			||||||
 | 
					            .connectionPool(connectionPool)
 | 
				
			||||||
 | 
					            .connectTimeout(config.connectTimeout, TimeUnit.SECONDS)
 | 
				
			||||||
 | 
					            .readTimeout(config.readTimeout, TimeUnit.SECONDS)
 | 
				
			||||||
 | 
					            .writeTimeout(config.writeTimeout, TimeUnit.SECONDS)
 | 
				
			||||||
 | 
					            .callTimeout(config.callTimeout, TimeUnit.SECONDS)
 | 
				
			||||||
 | 
					            .retryOnConnectionFailure(true);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 添加拦截器链
 | 
				
			||||||
 | 
					        addInterceptors(builder, config);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return builder.build();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 添加拦截器链
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param builder OkHttp客户端构建器
 | 
				
			||||||
 | 
					     * @param config 客户端配置
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static void addInterceptors(OkHttpClient.Builder builder, ClientConfig config) {
 | 
				
			||||||
 | 
					        // 添加重试拦截器
 | 
				
			||||||
 | 
					        if (config.enableRetry) {
 | 
				
			||||||
 | 
					            builder.addInterceptor(new RetryInterceptor(config.maxRetryAttempts, config.initialRetryDelay));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 添加监控拦截器
 | 
				
			||||||
 | 
					        if (config.enableMonitoring) {
 | 
				
			||||||
 | 
					            builder.addInterceptor(new MonitoringInterceptor());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 添加日志拦截器
 | 
				
			||||||
 | 
					        if (config.enableLogging) {
 | 
				
			||||||
 | 
					            HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(message -> 
 | 
				
			||||||
 | 
					                FsLogger.debug("HTTP: " + message));
 | 
				
			||||||
 | 
					            loggingInterceptor.setLevel(config.loggingLevel);
 | 
				
			||||||
 | 
					            builder.addInterceptor(loggingInterceptor);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 添加用户代理拦截器
 | 
				
			||||||
 | 
					        builder.addInterceptor(new UserAgentInterceptor());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 重试拦截器
 | 
				
			||||||
 | 
					     * 实现指数退避重试策略
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static class RetryInterceptor implements Interceptor {
 | 
				
			||||||
 | 
					        private final int maxRetryAttempts;
 | 
				
			||||||
 | 
					        private final long initialRetryDelay;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public RetryInterceptor(int maxRetryAttempts, long initialRetryDelay) {
 | 
				
			||||||
 | 
					            this.maxRetryAttempts = maxRetryAttempts;
 | 
				
			||||||
 | 
					            this.initialRetryDelay = initialRetryDelay;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public Response intercept(Chain chain) throws IOException {
 | 
				
			||||||
 | 
					            Request request = chain.request();
 | 
				
			||||||
 | 
					            Response response = null;
 | 
				
			||||||
 | 
					            IOException lastException = null;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for (int attempt = 0; attempt <= maxRetryAttempts; attempt++) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    if (response != null) {
 | 
				
			||||||
 | 
					                        response.close();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    response = chain.proceed(request);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // 如果响应成功或不可重试,直接返回
 | 
				
			||||||
 | 
					                    if (response.isSuccessful() || !isRetryableResponse(response)) {
 | 
				
			||||||
 | 
					                        return response;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // 如果不是最后一次尝试,等待后重试
 | 
				
			||||||
 | 
					                    if (attempt < maxRetryAttempts) {
 | 
				
			||||||
 | 
					                        long delay = calculateRetryDelay(attempt);
 | 
				
			||||||
 | 
					                        FsLogger.warn("HTTP request failed, retrying in {}ms. Attempt: {}/{}", 
 | 
				
			||||||
 | 
					                            delay, attempt + 1, maxRetryAttempts);
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        try {
 | 
				
			||||||
 | 
					                            Thread.sleep(delay);
 | 
				
			||||||
 | 
					                        } catch (InterruptedException e) {
 | 
				
			||||||
 | 
					                            Thread.currentThread().interrupt();
 | 
				
			||||||
 | 
					                            throw new IOException("Retry interrupted", e);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                } catch (IOException e) {
 | 
				
			||||||
 | 
					                    lastException = e;
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // 如果不是最后一次尝试,等待后重试
 | 
				
			||||||
 | 
					                    if (attempt < maxRetryAttempts && isRetryableException(e)) {
 | 
				
			||||||
 | 
					                        long delay = calculateRetryDelay(attempt);
 | 
				
			||||||
 | 
					                        FsLogger.warn("HTTP request failed with exception, retrying in {}ms. Attempt: {}/{} - {}", 
 | 
				
			||||||
 | 
					                            delay, attempt + 1, maxRetryAttempts, e.getMessage());
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        try {
 | 
				
			||||||
 | 
					                            Thread.sleep(delay);
 | 
				
			||||||
 | 
					                        } catch (InterruptedException ie) {
 | 
				
			||||||
 | 
					                            Thread.currentThread().interrupt();
 | 
				
			||||||
 | 
					                            throw new IOException("Retry interrupted", ie);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        throw e;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 如果所有重试都失败了
 | 
				
			||||||
 | 
					            if (response != null && !response.isSuccessful()) {
 | 
				
			||||||
 | 
					                return response;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (lastException != null) {
 | 
				
			||||||
 | 
					                throw lastException;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            throw new IOException("All retry attempts failed");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 计算重试延迟时间(指数退避)
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param attempt 当前重试次数
 | 
				
			||||||
 | 
					         * @return 延迟时间(毫秒)
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        private long calculateRetryDelay(int attempt) {
 | 
				
			||||||
 | 
					            return initialRetryDelay * (1L << attempt); // 指数退避:1s, 2s, 4s, 8s...
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 判断响应是否可重试
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param response HTTP响应
 | 
				
			||||||
 | 
					         * @return 是否可重试
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        private boolean isRetryableResponse(Response response) {
 | 
				
			||||||
 | 
					            int code = response.code();
 | 
				
			||||||
 | 
					            // 5xx服务器错误和429限流错误可重试
 | 
				
			||||||
 | 
					            return code >= 500 || code == 429;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 判断异常是否可重试
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param exception 异常
 | 
				
			||||||
 | 
					         * @return 是否可重试
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        private boolean isRetryableException(IOException exception) {
 | 
				
			||||||
 | 
					            // 连接超时、读取超时等网络异常可重试
 | 
				
			||||||
 | 
					            return exception instanceof java.net.SocketTimeoutException ||
 | 
				
			||||||
 | 
					                   exception instanceof java.net.ConnectException ||
 | 
				
			||||||
 | 
					                   exception instanceof java.net.UnknownHostException;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 监控拦截器
 | 
				
			||||||
 | 
					     * 收集请求性能指标和连接状态
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static class MonitoringInterceptor implements Interceptor {
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public Response intercept(Chain chain) throws IOException {
 | 
				
			||||||
 | 
					            Request request = chain.request();
 | 
				
			||||||
 | 
					            long startTime = System.currentTimeMillis();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                Response response = chain.proceed(request);
 | 
				
			||||||
 | 
					                long duration = System.currentTimeMillis() - startTime;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // 记录成功请求的性能指标
 | 
				
			||||||
 | 
					                FsLogger.apiCall(
 | 
				
			||||||
 | 
					                    request.method() + " " + request.url().encodedPath(),
 | 
				
			||||||
 | 
					                    request.url().query(),
 | 
				
			||||||
 | 
					                    duration
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                return response;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (IOException e) {
 | 
				
			||||||
 | 
					                long duration = System.currentTimeMillis() - startTime;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // 记录失败请求
 | 
				
			||||||
 | 
					                FsLogger.warn("HTTP request failed: {} {} in {}ms - {}", 
 | 
				
			||||||
 | 
					                    request.method(), request.url().encodedPath(), duration, e.getMessage());
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                throw e;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 用户代理拦截器
 | 
				
			||||||
 | 
					     * 添加统一的User-Agent头
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static class UserAgentInterceptor implements Interceptor {
 | 
				
			||||||
 | 
					        private static final String USER_AGENT = "FeishuTableHelper/0.0.2 (Java)";
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public Response intercept(Chain chain) throws IOException {
 | 
				
			||||||
 | 
					            Request originalRequest = chain.request();
 | 
				
			||||||
 | 
					            Request requestWithUserAgent = originalRequest.newBuilder()
 | 
				
			||||||
 | 
					                .header("User-Agent", USER_AGENT)
 | 
				
			||||||
 | 
					                .build();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return chain.proceed(requestWithUserAgent);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * HTTP客户端配置类
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static class ClientConfig {
 | 
				
			||||||
 | 
					        // 连接池配置
 | 
				
			||||||
 | 
					        public int maxIdleConnections = MAX_IDLE_CONNECTIONS;
 | 
				
			||||||
 | 
					        public long keepAliveDuration = KEEP_ALIVE_DURATION;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 超时配置
 | 
				
			||||||
 | 
					        public int connectTimeout = CONNECT_TIMEOUT;
 | 
				
			||||||
 | 
					        public int readTimeout = READ_TIMEOUT;
 | 
				
			||||||
 | 
					        public int writeTimeout = WRITE_TIMEOUT;
 | 
				
			||||||
 | 
					        public int callTimeout = CALL_TIMEOUT;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 重试配置
 | 
				
			||||||
 | 
					        public boolean enableRetry = true;
 | 
				
			||||||
 | 
					        public int maxRetryAttempts = MAX_RETRY_ATTEMPTS;
 | 
				
			||||||
 | 
					        public long initialRetryDelay = INITIAL_RETRY_DELAY;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 监控配置
 | 
				
			||||||
 | 
					        public boolean enableMonitoring = true;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 日志配置
 | 
				
			||||||
 | 
					        public boolean enableLogging = false; // 默认关闭详细日志
 | 
				
			||||||
 | 
					        public HttpLoggingInterceptor.Level loggingLevel = HttpLoggingInterceptor.Level.BASIC;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 创建默认配置
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @return 默认配置实例
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        public static ClientConfig defaultConfig() {
 | 
				
			||||||
 | 
					            return new ClientConfig();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 创建生产环境配置
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @return 生产环境配置实例
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        public static ClientConfig productionConfig() {
 | 
				
			||||||
 | 
					            ClientConfig config = new ClientConfig();
 | 
				
			||||||
 | 
					            config.enableLogging = false;
 | 
				
			||||||
 | 
					            config.maxRetryAttempts = 2; // 生产环境减少重试次数
 | 
				
			||||||
 | 
					            return config;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 创建开发环境配置
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @return 开发环境配置实例
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        public static ClientConfig developmentConfig() {
 | 
				
			||||||
 | 
					            ClientConfig config = new ClientConfig();
 | 
				
			||||||
 | 
					            config.enableLogging = true;
 | 
				
			||||||
 | 
					            config.loggingLevel = HttpLoggingInterceptor.Level.BODY;
 | 
				
			||||||
 | 
					            return config;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 流式配置方法
 | 
				
			||||||
 | 
					        public ClientConfig maxIdleConnections(int maxIdleConnections) {
 | 
				
			||||||
 | 
					            this.maxIdleConnections = maxIdleConnections;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig keepAliveDuration(long keepAliveDuration) {
 | 
				
			||||||
 | 
					            this.keepAliveDuration = keepAliveDuration;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig connectTimeout(int connectTimeout) {
 | 
				
			||||||
 | 
					            this.connectTimeout = connectTimeout;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig readTimeout(int readTimeout) {
 | 
				
			||||||
 | 
					            this.readTimeout = readTimeout;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig writeTimeout(int writeTimeout) {
 | 
				
			||||||
 | 
					            this.writeTimeout = writeTimeout;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig callTimeout(int callTimeout) {
 | 
				
			||||||
 | 
					            this.callTimeout = callTimeout;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig enableRetry(boolean enableRetry) {
 | 
				
			||||||
 | 
					            this.enableRetry = enableRetry;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig maxRetryAttempts(int maxRetryAttempts) {
 | 
				
			||||||
 | 
					            this.maxRetryAttempts = maxRetryAttempts;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig initialRetryDelay(long initialRetryDelay) {
 | 
				
			||||||
 | 
					            this.initialRetryDelay = initialRetryDelay;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig enableMonitoring(boolean enableMonitoring) {
 | 
				
			||||||
 | 
					            this.enableMonitoring = enableMonitoring;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig enableLogging(boolean enableLogging) {
 | 
				
			||||||
 | 
					            this.enableLogging = enableLogging;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ClientConfig loggingLevel(HttpLoggingInterceptor.Level loggingLevel) {
 | 
				
			||||||
 | 
					            this.loggingLevel = loggingLevel;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										85
									
								
								src/main/java/cn/isliu/core/config/ConfigBuilder.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										85
									
								
								src/main/java/cn/isliu/core/config/ConfigBuilder.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										79
									
								
								src/main/java/cn/isliu/core/config/ConfigChangeEvent.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										79
									
								
								src/main/java/cn/isliu/core/config/ConfigChangeEvent.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					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 +
 | 
				
			||||||
 | 
					                '}';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								src/main/java/cn/isliu/core/config/ConfigChangeListener.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										13
									
								
								src/main/java/cn/isliu/core/config/ConfigChangeListener.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					package cn.isliu.core.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 配置变更监听器接口
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public interface ConfigChangeListener {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 配置变更时的回调方法
 | 
				
			||||||
 | 
					     * @param event 配置变更事件
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    void onConfigChanged(ConfigChangeEvent event);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										77
									
								
								src/main/java/cn/isliu/core/config/ConfigSnapshot.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										77
									
								
								src/main/java/cn/isliu/core/config/ConfigSnapshot.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					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,56 +1,302 @@
 | 
				
			|||||||
package cn.isliu.core.config;
 | 
					package cn.isliu.core.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import cn.isliu.core.client.FeishuClient;
 | 
					import java.util.concurrent.locks.ReadWriteLock;
 | 
				
			||||||
import cn.isliu.core.utils.FsClientUtil;
 | 
					import java.util.concurrent.locks.ReentrantReadWriteLock;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.concurrent.CopyOnWriteArrayList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 线程安全的配置管理器
 | 
				
			||||||
 | 
					 * 使用volatile关键字和ReadWriteLock确保线程安全
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
public class FsConfig {
 | 
					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";
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static int headLine = 1;
 | 
					    // 读写锁保护配置更新操作
 | 
				
			||||||
 | 
					    private final ReadWriteLock lock = new ReentrantReadWriteLock();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static int titleLine = 1;
 | 
					    // 配置变更监听器列表
 | 
				
			||||||
 | 
					    private final List<ConfigChangeListener> listeners = new CopyOnWriteArrayList<>();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static boolean isCover = false;
 | 
					    // 单例实例
 | 
				
			||||||
    public static boolean CELL_TEXT = false;
 | 
					    private static volatile FsConfig instance;
 | 
				
			||||||
    public static String FORE_COLOR = "#000000";
 | 
					 | 
				
			||||||
    public static String BACK_COLOR = "#d5d5d5";
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static void initConfig(String appId, String appSecret) {
 | 
					    private FsConfig() {
 | 
				
			||||||
        FsClientUtil.initFeishuClient(appId, appSecret);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static void initConfig(int headLine, int titleLine, String appId, String appSecret) {
 | 
					    /**
 | 
				
			||||||
        FsConfig.headLine = headLine;
 | 
					     * 获取单例实例
 | 
				
			||||||
        FsConfig.titleLine = titleLine;
 | 
					     * @return ThreadSafeConfig实例
 | 
				
			||||||
        FsClientUtil.initFeishuClient(appId, appSecret);
 | 
					     */
 | 
				
			||||||
 | 
					    public static FsConfig getInstance() {
 | 
				
			||||||
 | 
					        if (instance == null) {
 | 
				
			||||||
 | 
					            synchronized (FsConfig.class) {
 | 
				
			||||||
 | 
					                if (instance == null) {
 | 
				
			||||||
 | 
					                    instance = new FsConfig();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return instance;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static void initConfig(int headLine, FeishuClient client) {
 | 
					    /**
 | 
				
			||||||
        FsConfig.headLine = headLine;
 | 
					     * 原子性配置更新
 | 
				
			||||||
        FsClientUtil.client = client;
 | 
					     * @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);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static void initConfig(FeishuClient client) {
 | 
					    /**
 | 
				
			||||||
        FsClientUtil.client = client;
 | 
					     * 验证配置参数
 | 
				
			||||||
 | 
					     * @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);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static int getHeadLine() {
 | 
					    /**
 | 
				
			||||||
 | 
					     * 验证颜色格式
 | 
				
			||||||
 | 
					     * @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;
 | 
					        return headLine;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static int getTitleLine() {
 | 
					    public int getTitleLine() {
 | 
				
			||||||
        return titleLine;
 | 
					        return titleLine;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static FeishuClient getFeishuClient() {
 | 
					    public boolean isCover() {
 | 
				
			||||||
        return FsClientUtil.client;
 | 
					        return isCover;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static void setHeadLine(int headLine) {
 | 
					    public boolean isCellText() {
 | 
				
			||||||
        FsConfig.headLine = headLine;
 | 
					        return cellText;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public static void setTitleLine(int titleLine) {
 | 
					    public String getForeColor() {
 | 
				
			||||||
        FsConfig.titleLine = titleLine;
 | 
					        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);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,7 +1,8 @@
 | 
				
			|||||||
package cn.isliu.core.converters;
 | 
					package cn.isliu.core.converters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.isliu.core.client.FsClient;
 | 
				
			||||||
 | 
					import cn.isliu.core.config.FsConfig;
 | 
				
			||||||
import cn.isliu.core.utils.FsApiUtil;
 | 
					import cn.isliu.core.utils.FsApiUtil;
 | 
				
			||||||
import cn.isliu.core.utils.FsClientUtil;
 | 
					 | 
				
			||||||
import cn.isliu.core.utils.FileUtil;
 | 
					import cn.isliu.core.utils.FileUtil;
 | 
				
			||||||
import com.google.gson.JsonArray;
 | 
					import com.google.gson.JsonArray;
 | 
				
			||||||
import com.google.gson.JsonElement;
 | 
					import com.google.gson.JsonElement;
 | 
				
			||||||
@ -10,12 +11,11 @@ import java.io.File;
 | 
				
			|||||||
import java.util.ArrayList;
 | 
					import java.util.ArrayList;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.UUID;
 | 
					import java.util.UUID;
 | 
				
			||||||
import java.util.logging.Level;
 | 
					import cn.isliu.core.logging.FsLogger;
 | 
				
			||||||
import java.util.logging.Logger;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class FileUrlProcess implements FieldValueProcess<String> {
 | 
					public class FileUrlProcess implements FieldValueProcess<String> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static final Logger log = Logger.getLogger(FileUrlProcess.class.getName());
 | 
					    // 使用统一的FsLogger替代java.util.logging.Logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public String process(Object value) {
 | 
					    public String process(Object value) {
 | 
				
			||||||
@ -44,7 +44,11 @@ public class FileUrlProcess implements FieldValueProcess<String> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public String reverseProcess(Object value) {
 | 
					    public String reverseProcess(Object value) {
 | 
				
			||||||
        // 简单实现,可以根据需要进行更复杂的反向处理
 | 
					        boolean cover = FsConfig.getInstance().isCover();
 | 
				
			||||||
 | 
					        if (!cover && value != null) {
 | 
				
			||||||
 | 
					            String str = value.toString();
 | 
				
			||||||
 | 
					            byte[] imageData = FileUtil.getImageData(str);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        if (value == null) {
 | 
					        if (value == null) {
 | 
				
			||||||
            return null;
 | 
					            return null;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -83,20 +87,20 @@ public class FileUrlProcess implements FieldValueProcess<String> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        boolean isSuccess = true;
 | 
					        boolean isSuccess = true;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            FsApiUtil.downloadMaterial(fileToken, filePath , FsClientUtil.getFeishuClient(), null);
 | 
					            FsApiUtil.downloadMaterial(fileToken, filePath , FsClient.getInstance().getClient(), null);
 | 
				
			||||||
            url = filePath;
 | 
					            url = filePath;
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.WARNING,"【飞书表格】 根据文件FileToken下载失败!fileToken: {0}, e: {1}", new Object[]{fileToken, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 根据文件FileToken下载失败!fileToken: {}, e: {}", fileToken, e.getMessage());
 | 
				
			||||||
            isSuccess = false;
 | 
					            isSuccess = false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!isSuccess) {
 | 
					        if (!isSuccess) {
 | 
				
			||||||
            String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(fileToken, FsClientUtil.getFeishuClient());
 | 
					            String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(fileToken, FsClient.getInstance().getClient());
 | 
				
			||||||
            // 根据临时下载地址下载
 | 
					            // 根据临时下载地址下载
 | 
				
			||||||
            FileUtil.downloadFile(tmpUrl, filePath);
 | 
					            FileUtil.downloadFile(tmpUrl, filePath);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        log.info("【飞书表格】 文件上传-飞书图片上传成功!fileToken: " + fileToken + ", filePath: " + filePath);
 | 
					        FsLogger.info("【飞书表格】 文件上传-飞书图片上传成功!fileToken: {}, filePath: {}", fileToken, filePath);
 | 
				
			||||||
        return url;
 | 
					        return url;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -110,19 +114,19 @@ public class FileUrlProcess implements FieldValueProcess<String> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        boolean isSuccess = true;
 | 
					        boolean isSuccess = true;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            FsApiUtil.downloadMaterial(token, path , FsClientUtil.getFeishuClient(), null);
 | 
					            FsApiUtil.downloadMaterial(token, path , FsClient.getInstance().getClient(), null);
 | 
				
			||||||
            url = path;
 | 
					            url = path;
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.WARNING, "【飞书表格】 附件-根据文件FileToken下载失败!fileToken: {0}, e: {1}", new Object[]{token, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 附件-根据文件FileToken下载失败!fileToken: {}, e: {}", token, e.getMessage());
 | 
				
			||||||
            isSuccess = false;
 | 
					            isSuccess = false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!isSuccess) {
 | 
					        if (!isSuccess) {
 | 
				
			||||||
            String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(token, FsClientUtil.getFeishuClient());
 | 
					            String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(token, FsClient.getInstance().getClient());
 | 
				
			||||||
            FileUtil.downloadFile(tmpUrl, path);
 | 
					            FileUtil.downloadFile(tmpUrl, path);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        log.info("【飞书表格】 文件上传-附件上传成功!fileToken: " + token + ", filePath: " + path);
 | 
					        FsLogger.info("【飞书表格】 文件上传-附件上传成功!fileToken: {}, filePath: {}", token, path);
 | 
				
			||||||
        return url;
 | 
					        return url;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										309
									
								
								src/main/java/cn/isliu/core/enums/ErrorCode.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										309
									
								
								src/main/java/cn/isliu/core/enums/ErrorCode.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,309 @@
 | 
				
			|||||||
 | 
					package cn.isliu.core.enums;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Locale;
 | 
				
			||||||
 | 
					import java.util.ResourceBundle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 错误代码枚举
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * 定义标准化的错误代码,包含错误描述和分类信息,支持国际化错误消息
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @author isliu
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public enum ErrorCode implements BaseEnum {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 客户端相关错误 (FS001-FS099)
 | 
				
			||||||
 | 
					    CLIENT_NOT_INITIALIZED("FS001", "Client not initialized", ErrorCategory.CLIENT),
 | 
				
			||||||
 | 
					    CLIENT_INITIALIZATION_FAILED("FS002", "Client initialization failed", ErrorCategory.CLIENT),
 | 
				
			||||||
 | 
					    CLIENT_CONNECTION_FAILED("FS003", "Client connection failed", ErrorCategory.CLIENT),
 | 
				
			||||||
 | 
					    CLIENT_AUTHENTICATION_FAILED("FS004", "Client authentication failed", ErrorCategory.CLIENT),
 | 
				
			||||||
 | 
					    CLIENT_TIMEOUT("FS005", "Client operation timeout", ErrorCategory.CLIENT),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // API调用相关错误 (FS100-FS199)
 | 
				
			||||||
 | 
					    API_CALL_FAILED("FS100", "API call failed", ErrorCategory.API),
 | 
				
			||||||
 | 
					    API_RATE_LIMIT_EXCEEDED("FS101", "API rate limit exceeded", ErrorCategory.API),
 | 
				
			||||||
 | 
					    API_INVALID_REQUEST("FS102", "Invalid API request", ErrorCategory.API),
 | 
				
			||||||
 | 
					    API_UNAUTHORIZED("FS103", "API unauthorized access", ErrorCategory.API),
 | 
				
			||||||
 | 
					    API_FORBIDDEN("FS104", "API access forbidden", ErrorCategory.API),
 | 
				
			||||||
 | 
					    API_NOT_FOUND("FS105", "API resource not found", ErrorCategory.API),
 | 
				
			||||||
 | 
					    API_SERVER_ERROR("FS106", "API server error", ErrorCategory.API),
 | 
				
			||||||
 | 
					    API_RESPONSE_PARSE_ERROR("FS107", "API response parse error", ErrorCategory.API),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 线程安全相关错误 (FS200-FS299)
 | 
				
			||||||
 | 
					    THREAD_SAFETY_VIOLATION("FS200", "Thread safety violation", ErrorCategory.CONCURRENCY),
 | 
				
			||||||
 | 
					    CONCURRENT_MODIFICATION("FS201", "Concurrent modification detected", ErrorCategory.CONCURRENCY),
 | 
				
			||||||
 | 
					    DEADLOCK_DETECTED("FS202", "Deadlock detected", ErrorCategory.CONCURRENCY),
 | 
				
			||||||
 | 
					    RACE_CONDITION("FS203", "Race condition occurred", ErrorCategory.CONCURRENCY),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 配置相关错误 (FS300-FS399)
 | 
				
			||||||
 | 
					    CONFIGURATION_ERROR("FS300", "Configuration error", ErrorCategory.CONFIGURATION),
 | 
				
			||||||
 | 
					    INVALID_CONFIGURATION("FS301", "Invalid configuration", ErrorCategory.CONFIGURATION),
 | 
				
			||||||
 | 
					    CONFIGURATION_NOT_FOUND("FS302", "Configuration not found", ErrorCategory.CONFIGURATION),
 | 
				
			||||||
 | 
					    CONFIGURATION_PARSE_ERROR("FS303", "Configuration parse error", ErrorCategory.CONFIGURATION),
 | 
				
			||||||
 | 
					    CONFIGURATION_VALIDATION_FAILED("FS304", "Configuration validation failed", ErrorCategory.CONFIGURATION),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 资源相关错误 (FS400-FS499)
 | 
				
			||||||
 | 
					    RESOURCE_EXHAUSTED("FS400", "Resource exhausted", ErrorCategory.RESOURCE),
 | 
				
			||||||
 | 
					    MEMORY_INSUFFICIENT("FS401", "Insufficient memory", ErrorCategory.RESOURCE),
 | 
				
			||||||
 | 
					    CONNECTION_POOL_EXHAUSTED("FS402", "Connection pool exhausted", ErrorCategory.RESOURCE),
 | 
				
			||||||
 | 
					    FILE_NOT_FOUND("FS403", "File not found", ErrorCategory.RESOURCE),
 | 
				
			||||||
 | 
					    FILE_ACCESS_DENIED("FS404", "File access denied", ErrorCategory.RESOURCE),
 | 
				
			||||||
 | 
					    DISK_SPACE_INSUFFICIENT("FS405", "Insufficient disk space", ErrorCategory.RESOURCE),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 数据相关错误 (FS500-FS599)
 | 
				
			||||||
 | 
					    DATA_VALIDATION_FAILED("FS500", "Data validation failed", ErrorCategory.DATA),
 | 
				
			||||||
 | 
					    DATA_CONVERSION_ERROR("FS501", "Data conversion error", ErrorCategory.DATA),
 | 
				
			||||||
 | 
					    DATA_INTEGRITY_VIOLATION("FS502", "Data integrity violation", ErrorCategory.DATA),
 | 
				
			||||||
 | 
					    DATA_FORMAT_ERROR("FS503", "Data format error", ErrorCategory.DATA),
 | 
				
			||||||
 | 
					    DATA_SIZE_EXCEEDED("FS504", "Data size exceeded limit", ErrorCategory.DATA),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 安全相关错误 (FS600-FS699)
 | 
				
			||||||
 | 
					    SECURITY_VIOLATION("FS600", "Security violation", ErrorCategory.SECURITY),
 | 
				
			||||||
 | 
					    INVALID_CREDENTIALS("FS601", "Invalid credentials", ErrorCategory.SECURITY),
 | 
				
			||||||
 | 
					    ACCESS_DENIED("FS602", "Access denied", ErrorCategory.SECURITY),
 | 
				
			||||||
 | 
					    TOKEN_EXPIRED("FS603", "Token expired", ErrorCategory.SECURITY),
 | 
				
			||||||
 | 
					    ENCRYPTION_FAILED("FS604", "Encryption failed", ErrorCategory.SECURITY),
 | 
				
			||||||
 | 
					    DECRYPTION_FAILED("FS605", "Decryption failed", ErrorCategory.SECURITY),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 业务逻辑相关错误 (FS700-FS799)
 | 
				
			||||||
 | 
					    BUSINESS_LOGIC_ERROR("FS700", "Business logic error", ErrorCategory.BUSINESS),
 | 
				
			||||||
 | 
					    INVALID_OPERATION("FS701", "Invalid operation", ErrorCategory.BUSINESS),
 | 
				
			||||||
 | 
					    OPERATION_NOT_SUPPORTED("FS702", "Operation not supported", ErrorCategory.BUSINESS),
 | 
				
			||||||
 | 
					    PRECONDITION_FAILED("FS703", "Precondition failed", ErrorCategory.BUSINESS),
 | 
				
			||||||
 | 
					    WORKFLOW_ERROR("FS704", "Workflow error", ErrorCategory.BUSINESS),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 系统相关错误 (FS800-FS899)
 | 
				
			||||||
 | 
					    SYSTEM_ERROR("FS800", "System error", ErrorCategory.SYSTEM),
 | 
				
			||||||
 | 
					    SERVICE_UNAVAILABLE("FS801", "Service unavailable", ErrorCategory.SYSTEM),
 | 
				
			||||||
 | 
					    MAINTENANCE_MODE("FS802", "System in maintenance mode", ErrorCategory.SYSTEM),
 | 
				
			||||||
 | 
					    VERSION_INCOMPATIBLE("FS803", "Version incompatible", ErrorCategory.SYSTEM),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 未知错误 (FS999)
 | 
				
			||||||
 | 
					    UNKNOWN_ERROR("FS999", "Unknown error", ErrorCategory.UNKNOWN);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final String code;
 | 
				
			||||||
 | 
					    private final String defaultMessage;
 | 
				
			||||||
 | 
					    private final ErrorCategory category;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 构造函数
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param code 错误代码
 | 
				
			||||||
 | 
					     * @param defaultMessage 默认错误消息
 | 
				
			||||||
 | 
					     * @param category 错误分类
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    ErrorCode(String code, String defaultMessage, ErrorCategory category) {
 | 
				
			||||||
 | 
					        this.code = code;
 | 
				
			||||||
 | 
					        this.defaultMessage = defaultMessage;
 | 
				
			||||||
 | 
					        this.category = category;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取错误代码
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 错误代码
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public String getCode() {
 | 
				
			||||||
 | 
					        return code;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取默认描述
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 默认描述
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public String getDesc() {
 | 
				
			||||||
 | 
					        return defaultMessage;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取错误分类
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 错误分类
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public ErrorCategory getCategory() {
 | 
				
			||||||
 | 
					        return category;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取默认错误消息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 默认错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public String getDefaultMessage() {
 | 
				
			||||||
 | 
					        return defaultMessage;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取国际化错误消息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param locale 语言环境
 | 
				
			||||||
 | 
					     * @return 国际化错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public String getMessage(Locale locale) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            ResourceBundle bundle = ResourceBundle.getBundle("messages.errors", locale);
 | 
				
			||||||
 | 
					            return bundle.getString(this.code);
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            // 如果获取国际化消息失败,返回默认消息
 | 
				
			||||||
 | 
					            return defaultMessage;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取当前语言环境的错误消息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 当前语言环境的错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public String getMessage() {
 | 
				
			||||||
 | 
					        return getMessage(Locale.getDefault());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取格式化的错误消息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param locale 语言环境
 | 
				
			||||||
 | 
					     * @param args 格式化参数
 | 
				
			||||||
 | 
					     * @return 格式化的错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public String getFormattedMessage(Locale locale, Object... args) {
 | 
				
			||||||
 | 
					        String message = getMessage(locale);
 | 
				
			||||||
 | 
					        if (args != null && args.length > 0) {
 | 
				
			||||||
 | 
					            return String.format(message, args);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return message;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取当前语言环境的格式化错误消息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param args 格式化参数
 | 
				
			||||||
 | 
					     * @return 格式化的错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public String getFormattedMessage(Object... args) {
 | 
				
			||||||
 | 
					        return getFormattedMessage(Locale.getDefault(), args);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 根据错误代码获取枚举值
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param code 错误代码
 | 
				
			||||||
 | 
					     * @return 对应的枚举值,未找到返回UNKNOWN_ERROR
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static ErrorCode getByCode(String code) {
 | 
				
			||||||
 | 
					        if (code == null || code.trim().isEmpty()) {
 | 
				
			||||||
 | 
					            return UNKNOWN_ERROR;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for (ErrorCode errorCode : values()) {
 | 
				
			||||||
 | 
					            if (errorCode.getCode().equals(code)) {
 | 
				
			||||||
 | 
					                return errorCode;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return UNKNOWN_ERROR;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 根据分类获取所有错误代码
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param category 错误分类
 | 
				
			||||||
 | 
					     * @return 该分类下的所有错误代码
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static ErrorCode[] getByCategory(ErrorCategory category) {
 | 
				
			||||||
 | 
					        return java.util.Arrays.stream(values())
 | 
				
			||||||
 | 
					                .filter(errorCode -> errorCode.getCategory() == category)
 | 
				
			||||||
 | 
					                .toArray(ErrorCode[]::new);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检查是否为客户端错误
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 如果是客户端错误返回true
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean isClientError() {
 | 
				
			||||||
 | 
					        return category == ErrorCategory.CLIENT;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检查是否为服务器错误
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 如果是服务器错误返回true
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean isServerError() {
 | 
				
			||||||
 | 
					        return category == ErrorCategory.API || category == ErrorCategory.SYSTEM;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检查是否为可重试的错误
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 如果是可重试的错误返回true
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean isRetryable() {
 | 
				
			||||||
 | 
					        switch (this) {
 | 
				
			||||||
 | 
					            case API_RATE_LIMIT_EXCEEDED:
 | 
				
			||||||
 | 
					            case CLIENT_TIMEOUT:
 | 
				
			||||||
 | 
					            case API_SERVER_ERROR:
 | 
				
			||||||
 | 
					            case SERVICE_UNAVAILABLE:
 | 
				
			||||||
 | 
					            case CONNECTION_POOL_EXHAUSTED:
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检查是否为致命错误
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 如果是致命错误返回true
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean isFatal() {
 | 
				
			||||||
 | 
					        switch (this) {
 | 
				
			||||||
 | 
					            case CLIENT_NOT_INITIALIZED:
 | 
				
			||||||
 | 
					            case CLIENT_INITIALIZATION_FAILED:
 | 
				
			||||||
 | 
					            case CONFIGURATION_ERROR:
 | 
				
			||||||
 | 
					            case SECURITY_VIOLATION:
 | 
				
			||||||
 | 
					            case SYSTEM_ERROR:
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 错误分类枚举
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public enum ErrorCategory {
 | 
				
			||||||
 | 
					        /** 客户端错误 */
 | 
				
			||||||
 | 
					        CLIENT("Client"),
 | 
				
			||||||
 | 
					        /** API错误 */
 | 
				
			||||||
 | 
					        API("API"),
 | 
				
			||||||
 | 
					        /** 并发错误 */
 | 
				
			||||||
 | 
					        CONCURRENCY("Concurrency"),
 | 
				
			||||||
 | 
					        /** 配置错误 */
 | 
				
			||||||
 | 
					        CONFIGURATION("Configuration"),
 | 
				
			||||||
 | 
					        /** 资源错误 */
 | 
				
			||||||
 | 
					        RESOURCE("Resource"),
 | 
				
			||||||
 | 
					        /** 数据错误 */
 | 
				
			||||||
 | 
					        DATA("Data"),
 | 
				
			||||||
 | 
					        /** 安全错误 */
 | 
				
			||||||
 | 
					        SECURITY("Security"),
 | 
				
			||||||
 | 
					        /** 业务逻辑错误 */
 | 
				
			||||||
 | 
					        BUSINESS("Business"),
 | 
				
			||||||
 | 
					        /** 系统错误 */
 | 
				
			||||||
 | 
					        SYSTEM("System"),
 | 
				
			||||||
 | 
					        /** 未知错误 */
 | 
				
			||||||
 | 
					        UNKNOWN("Unknown");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private final String name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ErrorCategory(String name) {
 | 
				
			||||||
 | 
					            this.name = name;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public String getName() {
 | 
				
			||||||
 | 
					            return name;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										627
									
								
								src/main/java/cn/isliu/core/exception/ExceptionHandler.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										627
									
								
								src/main/java/cn/isliu/core/exception/ExceptionHandler.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,627 @@
 | 
				
			|||||||
 | 
					package cn.isliu.core.exception;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.isliu.core.enums.ErrorCode;
 | 
				
			||||||
 | 
					import java.util.concurrent.ConcurrentHashMap;
 | 
				
			||||||
 | 
					import java.util.concurrent.atomic.AtomicLong;
 | 
				
			||||||
 | 
					import java.util.concurrent.atomic.LongAdder;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.Collections;
 | 
				
			||||||
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
 | 
					import java.time.format.DateTimeFormatter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 统一异常处理器
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * 提供全局异常处理、异常分类转换、统计监控和恢复建议功能
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @author isliu
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class ExceptionHandler {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static volatile ExceptionHandler instance;
 | 
				
			||||||
 | 
					    private static final Object lock = new Object();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** 异常统计计数器 */
 | 
				
			||||||
 | 
					    private final Map<ErrorCode, LongAdder> exceptionCounters = new ConcurrentHashMap<>();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /** 异常分类统计 */
 | 
				
			||||||
 | 
					    private final Map<ErrorCode.ErrorCategory, LongAdder> categoryCounters = new ConcurrentHashMap<>();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /** 最近异常记录 */
 | 
				
			||||||
 | 
					    private final List<ExceptionRecord> recentExceptions = Collections.synchronizedList(new ArrayList<>());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /** 最大记录数量 */
 | 
				
			||||||
 | 
					    private static final int MAX_RECENT_EXCEPTIONS = 100;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /** 异常处理监听器 */
 | 
				
			||||||
 | 
					    private final List<ExceptionListener> listeners = Collections.synchronizedList(new ArrayList<>());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 私有构造函数
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private ExceptionHandler() {
 | 
				
			||||||
 | 
					        // 初始化所有错误代码的计数器
 | 
				
			||||||
 | 
					        for (ErrorCode errorCode : ErrorCode.values()) {
 | 
				
			||||||
 | 
					            exceptionCounters.put(errorCode, new LongAdder());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 初始化所有分类的计数器
 | 
				
			||||||
 | 
					        for (ErrorCode.ErrorCategory category : ErrorCode.ErrorCategory.values()) {
 | 
				
			||||||
 | 
					            categoryCounters.put(category, new LongAdder());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取单例实例
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 异常处理器实例
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static ExceptionHandler getInstance() {
 | 
				
			||||||
 | 
					        if (instance == null) {
 | 
				
			||||||
 | 
					            synchronized (lock) {
 | 
				
			||||||
 | 
					                if (instance == null) {
 | 
				
			||||||
 | 
					                    instance = new ExceptionHandler();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return instance;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 处理异常
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param throwable 原始异常
 | 
				
			||||||
 | 
					     * @return 处理后的FsHelperException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException handleException(Throwable throwable) {
 | 
				
			||||||
 | 
					        if (throwable == null) {
 | 
				
			||||||
 | 
					            return new FsHelperException(ErrorCode.UNKNOWN_ERROR, "Null exception occurred");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        FsHelperException fsException;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 如果已经是FsHelperException,直接使用
 | 
				
			||||||
 | 
					        if (throwable instanceof FsHelperException) {
 | 
				
			||||||
 | 
					            fsException = (FsHelperException) throwable;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // 转换为FsHelperException
 | 
				
			||||||
 | 
					            fsException = convertToFsHelperException(throwable);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 记录异常统计
 | 
				
			||||||
 | 
					        recordException(fsException);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 通知监听器
 | 
				
			||||||
 | 
					        notifyListeners(fsException);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return fsException;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 处理异常并提供上下文
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param throwable 原始异常
 | 
				
			||||||
 | 
					     * @param context 上下文信息
 | 
				
			||||||
 | 
					     * @return 处理后的FsHelperException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException handleException(Throwable throwable, Map<String, Object> context) {
 | 
				
			||||||
 | 
					        FsHelperException fsException = handleException(throwable);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (context != null && !context.isEmpty()) {
 | 
				
			||||||
 | 
					            fsException.addContext(context);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return fsException;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 处理异常并提供操作上下文
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param throwable 原始异常
 | 
				
			||||||
 | 
					     * @param operation 操作名称
 | 
				
			||||||
 | 
					     * @param additionalInfo 附加信息
 | 
				
			||||||
 | 
					     * @return 处理后的FsHelperException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException handleException(Throwable throwable, String operation, String additionalInfo) {
 | 
				
			||||||
 | 
					        FsHelperException fsException = handleException(throwable);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        fsException.addContext("operation", operation);
 | 
				
			||||||
 | 
					        if (additionalInfo != null) {
 | 
				
			||||||
 | 
					            fsException.addContext("additionalInfo", additionalInfo);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return fsException;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 将普通异常转换为FsHelperException
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param throwable 原始异常
 | 
				
			||||||
 | 
					     * @return FsHelperException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private FsHelperException convertToFsHelperException(Throwable throwable) {
 | 
				
			||||||
 | 
					        ErrorCode errorCode = classifyException(throwable);
 | 
				
			||||||
 | 
					        String message = throwable.getMessage() != null ? throwable.getMessage() : throwable.getClass().getSimpleName();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return FsHelperException.builder(errorCode)
 | 
				
			||||||
 | 
					            .message(message)
 | 
				
			||||||
 | 
					            .context("originalExceptionType", throwable.getClass().getSimpleName())
 | 
				
			||||||
 | 
					            .context("originalMessage", throwable.getMessage())
 | 
				
			||||||
 | 
					            .cause(throwable)
 | 
				
			||||||
 | 
					            .build();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 异常分类逻辑
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param throwable 异常
 | 
				
			||||||
 | 
					     * @return 对应的错误代码
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private ErrorCode classifyException(Throwable throwable) {
 | 
				
			||||||
 | 
					        if (throwable == null) {
 | 
				
			||||||
 | 
					            return ErrorCode.UNKNOWN_ERROR;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        String className = throwable.getClass().getSimpleName().toLowerCase();
 | 
				
			||||||
 | 
					        String message = throwable.getMessage() != null ? throwable.getMessage().toLowerCase() : "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 网络和连接相关异常
 | 
				
			||||||
 | 
					        if (className.contains("connect") || className.contains("socket") || className.contains("timeout")) {
 | 
				
			||||||
 | 
					            if (message.contains("timeout")) {
 | 
				
			||||||
 | 
					                return ErrorCode.CLIENT_TIMEOUT;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return ErrorCode.CLIENT_CONNECTION_FAILED;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 认证和授权异常
 | 
				
			||||||
 | 
					        if (className.contains("auth") || className.contains("credential") || className.contains("permission") ||
 | 
				
			||||||
 | 
					            message.contains("auth") || message.contains("credential") || message.contains("permission")) {
 | 
				
			||||||
 | 
					            if (message.contains("expired") || message.contains("token")) {
 | 
				
			||||||
 | 
					                return ErrorCode.TOKEN_EXPIRED;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (message.contains("unauthorized") || message.contains("401")) {
 | 
				
			||||||
 | 
					                return ErrorCode.API_UNAUTHORIZED;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (message.contains("forbidden") || message.contains("403")) {
 | 
				
			||||||
 | 
					                return ErrorCode.API_FORBIDDEN;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return ErrorCode.INVALID_CREDENTIALS;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // HTTP相关异常
 | 
				
			||||||
 | 
					        if (className.contains("http") || message.contains("http")) {
 | 
				
			||||||
 | 
					            if (message.contains("404") || message.contains("not found")) {
 | 
				
			||||||
 | 
					                return ErrorCode.API_NOT_FOUND;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (message.contains("429") || message.contains("rate limit")) {
 | 
				
			||||||
 | 
					                return ErrorCode.API_RATE_LIMIT_EXCEEDED;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (message.contains("500") || message.contains("502") || message.contains("503")) {
 | 
				
			||||||
 | 
					                return ErrorCode.API_SERVER_ERROR;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (message.contains("400") || message.contains("bad request")) {
 | 
				
			||||||
 | 
					                return ErrorCode.API_INVALID_REQUEST;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return ErrorCode.API_CALL_FAILED;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 并发相关异常 - 需要优先检查,因为ConcurrentModificationException包含"modification"
 | 
				
			||||||
 | 
					        if (className.equals("concurrentmodificationexception")) {
 | 
				
			||||||
 | 
					            return ErrorCode.CONCURRENT_MODIFICATION;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (className.contains("concurrent") || className.contains("thread") || className.contains("lock")) {
 | 
				
			||||||
 | 
					            return ErrorCode.THREAD_SAFETY_VIOLATION;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 数据相关异常
 | 
				
			||||||
 | 
					        if (className.contains("parse") || className.contains("json") || className.contains("xml")) {
 | 
				
			||||||
 | 
					            return ErrorCode.API_RESPONSE_PARSE_ERROR;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (className.contains("validation") || className.contains("illegal") || className.contains("invalid")) {
 | 
				
			||||||
 | 
					            return ErrorCode.DATA_VALIDATION_FAILED;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (className.contains("format") || className.contains("number") || className.contains("date")) {
 | 
				
			||||||
 | 
					            return ErrorCode.DATA_FORMAT_ERROR;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 资源相关异常
 | 
				
			||||||
 | 
					        if (className.contains("memory") || message.contains("out of memory")) {
 | 
				
			||||||
 | 
					            return ErrorCode.MEMORY_INSUFFICIENT;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (className.contains("file") || className.equals("ioexception") || className.startsWith("io")) {
 | 
				
			||||||
 | 
					            if (message.contains("not found") || message.contains("no such file")) {
 | 
				
			||||||
 | 
					                return ErrorCode.FILE_NOT_FOUND;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (message.contains("access denied") || message.contains("permission")) {
 | 
				
			||||||
 | 
					                return ErrorCode.FILE_ACCESS_DENIED;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return ErrorCode.RESOURCE_EXHAUSTED;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 配置相关异常
 | 
				
			||||||
 | 
					        if (className.contains("config") || className.contains("property") || className.contains("setting")) {
 | 
				
			||||||
 | 
					            return ErrorCode.CONFIGURATION_ERROR;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 业务逻辑异常
 | 
				
			||||||
 | 
					        if (className.contains("state") || className.contains("operation")) {
 | 
				
			||||||
 | 
					            return ErrorCode.INVALID_OPERATION;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 系统异常 - 更精确的匹配
 | 
				
			||||||
 | 
					        if (className.equals("runtimeexception") || className.contains("system")) {
 | 
				
			||||||
 | 
					            return ErrorCode.SYSTEM_ERROR;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 默认未知错误
 | 
				
			||||||
 | 
					        return ErrorCode.UNKNOWN_ERROR;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 记录异常统计
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param exception 异常
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void recordException(FsHelperException exception) {
 | 
				
			||||||
 | 
					        ErrorCode errorCode = exception.getErrorCode();
 | 
				
			||||||
 | 
					        ErrorCode.ErrorCategory category = errorCode.getCategory();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 增加计数
 | 
				
			||||||
 | 
					        exceptionCounters.get(errorCode).increment();
 | 
				
			||||||
 | 
					        categoryCounters.get(category).increment();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 记录最近异常
 | 
				
			||||||
 | 
					        ExceptionRecord record = new ExceptionRecord(
 | 
				
			||||||
 | 
					            exception.getExceptionId(),
 | 
				
			||||||
 | 
					            errorCode,
 | 
				
			||||||
 | 
					            exception.getMessage(),
 | 
				
			||||||
 | 
					            exception.getUserFriendlyMessage(),
 | 
				
			||||||
 | 
					            LocalDateTime.now(),
 | 
				
			||||||
 | 
					            exception.getContext()
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        synchronized (recentExceptions) {
 | 
				
			||||||
 | 
					            recentExceptions.add(record);
 | 
				
			||||||
 | 
					            // 保持最大记录数量
 | 
				
			||||||
 | 
					            if (recentExceptions.size() > MAX_RECENT_EXCEPTIONS) {
 | 
				
			||||||
 | 
					                recentExceptions.remove(0);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 通知异常监听器
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param exception 异常
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void notifyListeners(FsHelperException exception) {
 | 
				
			||||||
 | 
					        for (ExceptionListener listener : listeners) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                listener.onException(exception);
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                // 监听器异常不应影响主流程,只记录日志
 | 
				
			||||||
 | 
					                System.err.println("Exception listener failed: " + e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取异常统计信息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 异常统计信息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public ExceptionStatistics getStatistics() {
 | 
				
			||||||
 | 
					        Map<ErrorCode, Long> errorCodeCounts = new ConcurrentHashMap<>();
 | 
				
			||||||
 | 
					        Map<ErrorCode.ErrorCategory, Long> categoryCounts = new ConcurrentHashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (Map.Entry<ErrorCode, LongAdder> entry : exceptionCounters.entrySet()) {
 | 
				
			||||||
 | 
					            long count = entry.getValue().sum();
 | 
				
			||||||
 | 
					            if (count > 0) {
 | 
				
			||||||
 | 
					                errorCodeCounts.put(entry.getKey(), count);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (Map.Entry<ErrorCode.ErrorCategory, LongAdder> entry : categoryCounters.entrySet()) {
 | 
				
			||||||
 | 
					            long count = entry.getValue().sum();
 | 
				
			||||||
 | 
					            if (count > 0) {
 | 
				
			||||||
 | 
					                categoryCounts.put(entry.getKey(), count);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return new ExceptionStatistics(errorCodeCounts, categoryCounts, new ArrayList<>(recentExceptions));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取恢复建议
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param exception 异常
 | 
				
			||||||
 | 
					     * @return 恢复建议
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public RecoveryAdvice getRecoveryAdvice(FsHelperException exception) {
 | 
				
			||||||
 | 
					        ErrorCode errorCode = exception.getErrorCode();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        RecoveryAdvice.Builder builder = RecoveryAdvice.builder()
 | 
				
			||||||
 | 
					            .errorCode(errorCode)
 | 
				
			||||||
 | 
					            .isRetryable(errorCode.isRetryable())
 | 
				
			||||||
 | 
					            .isFatal(errorCode.isFatal());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 根据错误类型提供具体建议
 | 
				
			||||||
 | 
					        switch (errorCode.getCategory()) {
 | 
				
			||||||
 | 
					            case CLIENT:
 | 
				
			||||||
 | 
					                return builder
 | 
				
			||||||
 | 
					                    .immediateAction("检查客户端配置和初始化参数")
 | 
				
			||||||
 | 
					                    .longTermAction("确保客户端正确初始化并配置有效的认证信息")
 | 
				
			||||||
 | 
					                    .preventiveAction("在使用客户端前进行初始化检查")
 | 
				
			||||||
 | 
					                    .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case API:
 | 
				
			||||||
 | 
					                if (errorCode.isRetryable()) {
 | 
				
			||||||
 | 
					                    return builder
 | 
				
			||||||
 | 
					                        .immediateAction("等待一段时间后重试")
 | 
				
			||||||
 | 
					                        .longTermAction("实现指数退避重试策略")
 | 
				
			||||||
 | 
					                        .preventiveAction("监控API调用频率,避免超过限制")
 | 
				
			||||||
 | 
					                        .build();
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    return builder
 | 
				
			||||||
 | 
					                        .immediateAction("检查API请求参数和格式")
 | 
				
			||||||
 | 
					                        .longTermAction("验证API权限和认证信息")
 | 
				
			||||||
 | 
					                        .preventiveAction("在发送请求前验证参数完整性")
 | 
				
			||||||
 | 
					                        .build();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case CONFIGURATION:
 | 
				
			||||||
 | 
					                return builder
 | 
				
			||||||
 | 
					                    .immediateAction("检查配置文件的格式和内容")
 | 
				
			||||||
 | 
					                    .longTermAction("建立配置验证机制")
 | 
				
			||||||
 | 
					                    .preventiveAction("使用配置模板和验证规则")
 | 
				
			||||||
 | 
					                    .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case SECURITY:
 | 
				
			||||||
 | 
					                return builder
 | 
				
			||||||
 | 
					                    .immediateAction("检查认证凭据是否有效")
 | 
				
			||||||
 | 
					                    .longTermAction("实现凭据自动刷新机制")
 | 
				
			||||||
 | 
					                    .preventiveAction("定期更新和验证安全凭据")
 | 
				
			||||||
 | 
					                    .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case RESOURCE:
 | 
				
			||||||
 | 
					                return builder
 | 
				
			||||||
 | 
					                    .immediateAction("释放不必要的资源")
 | 
				
			||||||
 | 
					                    .longTermAction("优化资源使用策略")
 | 
				
			||||||
 | 
					                    .preventiveAction("实现资源监控和预警机制")
 | 
				
			||||||
 | 
					                    .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case DATA:
 | 
				
			||||||
 | 
					                return builder
 | 
				
			||||||
 | 
					                    .immediateAction("验证输入数据的格式和内容")
 | 
				
			||||||
 | 
					                    .longTermAction("加强数据验证和清理机制")
 | 
				
			||||||
 | 
					                    .preventiveAction("在处理前进行数据格式检查")
 | 
				
			||||||
 | 
					                    .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case CONCURRENCY:
 | 
				
			||||||
 | 
					                return builder
 | 
				
			||||||
 | 
					                    .immediateAction("检查并发访问的同步机制")
 | 
				
			||||||
 | 
					                    .longTermAction("重新设计线程安全的数据结构")
 | 
				
			||||||
 | 
					                    .preventiveAction("使用线程安全的组件和模式")
 | 
				
			||||||
 | 
					                    .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case BUSINESS:
 | 
				
			||||||
 | 
					                return builder
 | 
				
			||||||
 | 
					                    .immediateAction("检查业务规则和前置条件")
 | 
				
			||||||
 | 
					                    .longTermAction("完善业务逻辑验证")
 | 
				
			||||||
 | 
					                    .preventiveAction("在操作前验证业务规则")
 | 
				
			||||||
 | 
					                    .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case SYSTEM:
 | 
				
			||||||
 | 
					                return builder
 | 
				
			||||||
 | 
					                    .immediateAction("检查系统状态和资源可用性")
 | 
				
			||||||
 | 
					                    .longTermAction("实现系统监控和告警")
 | 
				
			||||||
 | 
					                    .preventiveAction("定期进行系统健康检查")
 | 
				
			||||||
 | 
					                    .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                return builder
 | 
				
			||||||
 | 
					                    .immediateAction("查看详细错误信息和日志")
 | 
				
			||||||
 | 
					                    .longTermAction("联系技术支持获取帮助")
 | 
				
			||||||
 | 
					                    .preventiveAction("加强错误处理和日志记录")
 | 
				
			||||||
 | 
					                    .build();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 添加异常监听器
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param listener 监听器
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void addListener(ExceptionListener listener) {
 | 
				
			||||||
 | 
					        if (listener != null) {
 | 
				
			||||||
 | 
					            listeners.add(listener);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 移除异常监听器
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param listener 监听器
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void removeListener(ExceptionListener listener) {
 | 
				
			||||||
 | 
					        listeners.remove(listener);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 清除统计信息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void clearStatistics() {
 | 
				
			||||||
 | 
					        exceptionCounters.values().forEach(LongAdder::reset);
 | 
				
			||||||
 | 
					        categoryCounters.values().forEach(LongAdder::reset);
 | 
				
			||||||
 | 
					        recentExceptions.clear();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 异常记录
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static class ExceptionRecord {
 | 
				
			||||||
 | 
					        private final String exceptionId;
 | 
				
			||||||
 | 
					        private final ErrorCode errorCode;
 | 
				
			||||||
 | 
					        private final String message;
 | 
				
			||||||
 | 
					        private final String userFriendlyMessage;
 | 
				
			||||||
 | 
					        private final LocalDateTime timestamp;
 | 
				
			||||||
 | 
					        private final Map<String, Object> context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public ExceptionRecord(String exceptionId, ErrorCode errorCode, String message, 
 | 
				
			||||||
 | 
					                             String userFriendlyMessage, LocalDateTime timestamp, Map<String, Object> context) {
 | 
				
			||||||
 | 
					            this.exceptionId = exceptionId;
 | 
				
			||||||
 | 
					            this.errorCode = errorCode;
 | 
				
			||||||
 | 
					            this.message = message;
 | 
				
			||||||
 | 
					            this.userFriendlyMessage = userFriendlyMessage;
 | 
				
			||||||
 | 
					            this.timestamp = timestamp;
 | 
				
			||||||
 | 
					            this.context = new ConcurrentHashMap<>(context != null ? context : new ConcurrentHashMap<>());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public String getExceptionId() { return exceptionId; }
 | 
				
			||||||
 | 
					        public ErrorCode getErrorCode() { return errorCode; }
 | 
				
			||||||
 | 
					        public String getMessage() { return message; }
 | 
				
			||||||
 | 
					        public String getUserFriendlyMessage() { return userFriendlyMessage; }
 | 
				
			||||||
 | 
					        public LocalDateTime getTimestamp() { return timestamp; }
 | 
				
			||||||
 | 
					        public Map<String, Object> getContext() { return new ConcurrentHashMap<>(context); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public String toString() {
 | 
				
			||||||
 | 
					            return String.format("[%s] %s - %s (%s)", 
 | 
				
			||||||
 | 
					                timestamp.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
 | 
				
			||||||
 | 
					                errorCode.getCode(), 
 | 
				
			||||||
 | 
					                message, 
 | 
				
			||||||
 | 
					                exceptionId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 异常统计信息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static class ExceptionStatistics {
 | 
				
			||||||
 | 
					        private final Map<ErrorCode, Long> errorCodeCounts;
 | 
				
			||||||
 | 
					        private final Map<ErrorCode.ErrorCategory, Long> categoryCounts;
 | 
				
			||||||
 | 
					        private final List<ExceptionRecord> recentExceptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public ExceptionStatistics(Map<ErrorCode, Long> errorCodeCounts, 
 | 
				
			||||||
 | 
					                                 Map<ErrorCode.ErrorCategory, Long> categoryCounts,
 | 
				
			||||||
 | 
					                                 List<ExceptionRecord> recentExceptions) {
 | 
				
			||||||
 | 
					            this.errorCodeCounts = new ConcurrentHashMap<>(errorCodeCounts);
 | 
				
			||||||
 | 
					            this.categoryCounts = new ConcurrentHashMap<>(categoryCounts);
 | 
				
			||||||
 | 
					            this.recentExceptions = new ArrayList<>(recentExceptions);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public Map<ErrorCode, Long> getErrorCodeCounts() { return new ConcurrentHashMap<>(errorCodeCounts); }
 | 
				
			||||||
 | 
					        public Map<ErrorCode.ErrorCategory, Long> getCategoryCounts() { return new ConcurrentHashMap<>(categoryCounts); }
 | 
				
			||||||
 | 
					        public List<ExceptionRecord> getRecentExceptions() { return new ArrayList<>(recentExceptions); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public long getTotalExceptions() {
 | 
				
			||||||
 | 
					            return errorCodeCounts.values().stream().mapToLong(Long::longValue).sum();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public ErrorCode getMostFrequentError() {
 | 
				
			||||||
 | 
					            return errorCodeCounts.entrySet().stream()
 | 
				
			||||||
 | 
					                .max(Map.Entry.comparingByValue())
 | 
				
			||||||
 | 
					                .map(Map.Entry::getKey)
 | 
				
			||||||
 | 
					                .orElse(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public ErrorCode.ErrorCategory getMostFrequentCategory() {
 | 
				
			||||||
 | 
					            return categoryCounts.entrySet().stream()
 | 
				
			||||||
 | 
					                .max(Map.Entry.comparingByValue())
 | 
				
			||||||
 | 
					                .map(Map.Entry::getKey)
 | 
				
			||||||
 | 
					                .orElse(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 恢复建议
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static class RecoveryAdvice {
 | 
				
			||||||
 | 
					        private final ErrorCode errorCode;
 | 
				
			||||||
 | 
					        private final boolean isRetryable;
 | 
				
			||||||
 | 
					        private final boolean isFatal;
 | 
				
			||||||
 | 
					        private final String immediateAction;
 | 
				
			||||||
 | 
					        private final String longTermAction;
 | 
				
			||||||
 | 
					        private final String preventiveAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private RecoveryAdvice(Builder builder) {
 | 
				
			||||||
 | 
					            this.errorCode = builder.errorCode;
 | 
				
			||||||
 | 
					            this.isRetryable = builder.isRetryable;
 | 
				
			||||||
 | 
					            this.isFatal = builder.isFatal;
 | 
				
			||||||
 | 
					            this.immediateAction = builder.immediateAction;
 | 
				
			||||||
 | 
					            this.longTermAction = builder.longTermAction;
 | 
				
			||||||
 | 
					            this.preventiveAction = builder.preventiveAction;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public ErrorCode getErrorCode() { return errorCode; }
 | 
				
			||||||
 | 
					        public boolean isRetryable() { return isRetryable; }
 | 
				
			||||||
 | 
					        public boolean isFatal() { return isFatal; }
 | 
				
			||||||
 | 
					        public String getImmediateAction() { return immediateAction; }
 | 
				
			||||||
 | 
					        public String getLongTermAction() { return longTermAction; }
 | 
				
			||||||
 | 
					        public String getPreventiveAction() { return preventiveAction; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public static Builder builder() {
 | 
				
			||||||
 | 
					            return new Builder();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public static class Builder {
 | 
				
			||||||
 | 
					            private ErrorCode errorCode;
 | 
				
			||||||
 | 
					            private boolean isRetryable;
 | 
				
			||||||
 | 
					            private boolean isFatal;
 | 
				
			||||||
 | 
					            private String immediateAction;
 | 
				
			||||||
 | 
					            private String longTermAction;
 | 
				
			||||||
 | 
					            private String preventiveAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            public Builder errorCode(ErrorCode errorCode) {
 | 
				
			||||||
 | 
					                this.errorCode = errorCode;
 | 
				
			||||||
 | 
					                return this;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            public Builder isRetryable(boolean isRetryable) {
 | 
				
			||||||
 | 
					                this.isRetryable = isRetryable;
 | 
				
			||||||
 | 
					                return this;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            public Builder isFatal(boolean isFatal) {
 | 
				
			||||||
 | 
					                this.isFatal = isFatal;
 | 
				
			||||||
 | 
					                return this;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            public Builder immediateAction(String immediateAction) {
 | 
				
			||||||
 | 
					                this.immediateAction = immediateAction;
 | 
				
			||||||
 | 
					                return this;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            public Builder longTermAction(String longTermAction) {
 | 
				
			||||||
 | 
					                this.longTermAction = longTermAction;
 | 
				
			||||||
 | 
					                return this;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            public Builder preventiveAction(String preventiveAction) {
 | 
				
			||||||
 | 
					                this.preventiveAction = preventiveAction;
 | 
				
			||||||
 | 
					                return this;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            public RecoveryAdvice build() {
 | 
				
			||||||
 | 
					                return new RecoveryAdvice(this);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 异常监听器接口
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public interface ExceptionListener {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 异常发生时的回调
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param exception 异常
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        void onException(FsHelperException exception);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,12 +1,453 @@
 | 
				
			|||||||
package cn.isliu.core.exception;
 | 
					package cn.isliu.core.exception;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class FsHelperException  extends RuntimeException {
 | 
					import cn.isliu.core.enums.ErrorCode;
 | 
				
			||||||
 | 
					import java.io.Serializable;
 | 
				
			||||||
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
 | 
					import java.time.format.DateTimeFormatter;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public FsHelperException(String message) {
 | 
					/**
 | 
				
			||||||
        super(message);
 | 
					 * 飞书助手异常类
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * 增强的异常类,支持错误代码、上下文信息、异常链分析和序列化
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @author isliu
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class FsHelperException extends RuntimeException implements Serializable {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final long serialVersionUID = 1L;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /** 错误代码 */
 | 
				
			||||||
 | 
					    private final ErrorCode errorCode;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /** 上下文信息 */
 | 
				
			||||||
 | 
					    private final Map<String, Object> context;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /** 异常唯一标识 */
 | 
				
			||||||
 | 
					    private final String exceptionId;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /** 异常发生时间 */
 | 
				
			||||||
 | 
					    private final LocalDateTime timestamp;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /** 用户友好的错误消息 */
 | 
				
			||||||
 | 
					    private final String userFriendlyMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 构造函数 - 仅包含错误代码
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param errorCode 错误代码
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException(ErrorCode errorCode) {
 | 
				
			||||||
 | 
					        this(errorCode, errorCode.getDefaultMessage(), null, null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 构造函数 - 包含错误代码和自定义消息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param errorCode 错误代码
 | 
				
			||||||
 | 
					     * @param message 自定义错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException(ErrorCode errorCode, String message) {
 | 
				
			||||||
 | 
					        this(errorCode, message, null, null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 构造函数 - 包含错误代码、消息和上下文
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param errorCode 错误代码
 | 
				
			||||||
 | 
					     * @param message 错误消息
 | 
				
			||||||
 | 
					     * @param context 上下文信息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException(ErrorCode errorCode, String message, Map<String, Object> context) {
 | 
				
			||||||
 | 
					        this(errorCode, message, context, null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 构造函数 - 包含错误代码、消息和原因
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param errorCode 错误代码
 | 
				
			||||||
 | 
					     * @param message 错误消息
 | 
				
			||||||
 | 
					     * @param cause 原因异常
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException(ErrorCode errorCode, String message, Throwable cause) {
 | 
				
			||||||
 | 
					        this(errorCode, message, null, cause);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 完整构造函数
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param errorCode 错误代码
 | 
				
			||||||
 | 
					     * @param message 错误消息
 | 
				
			||||||
 | 
					     * @param context 上下文信息
 | 
				
			||||||
 | 
					     * @param cause 原因异常
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException(ErrorCode errorCode, String message, Map<String, Object> context, Throwable cause) {
 | 
				
			||||||
 | 
					        super(buildDetailedMessage(errorCode, message, context), cause);
 | 
				
			||||||
 | 
					        this.errorCode = errorCode != null ? errorCode : ErrorCode.UNKNOWN_ERROR;
 | 
				
			||||||
 | 
					        this.context = context != null ? new HashMap<>(context) : new HashMap<>();
 | 
				
			||||||
 | 
					        this.exceptionId = UUID.randomUUID().toString();
 | 
				
			||||||
 | 
					        this.timestamp = LocalDateTime.now();
 | 
				
			||||||
 | 
					        this.userFriendlyMessage = generateUserFriendlyMessage(this.errorCode, message);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 添加基本上下文信息
 | 
				
			||||||
 | 
					        this.context.put("exceptionId", this.exceptionId);
 | 
				
			||||||
 | 
					        this.context.put("timestamp", this.timestamp.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
 | 
				
			||||||
 | 
					        this.context.put("errorCode", this.errorCode.getCode());
 | 
				
			||||||
 | 
					        this.context.put("errorCategory", this.errorCode.getCategory().getName());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 兼容性构造函数 - 保持向后兼容
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param message 错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException(String message) {
 | 
				
			||||||
 | 
					        this(ErrorCode.UNKNOWN_ERROR, message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 兼容性构造函数 - 保持向后兼容
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param message 错误消息
 | 
				
			||||||
 | 
					     * @param cause 原因异常
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    public FsHelperException(String message, Throwable cause) {
 | 
					    public FsHelperException(String message, Throwable cause) {
 | 
				
			||||||
        super(message, cause);
 | 
					        this(ErrorCode.UNKNOWN_ERROR, message, cause);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取错误代码
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 错误代码
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public ErrorCode getErrorCode() {
 | 
				
			||||||
 | 
					        return errorCode;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取上下文信息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 上下文信息的副本
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public Map<String, Object> getContext() {
 | 
				
			||||||
 | 
					        return new HashMap<>(context);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取异常唯一标识
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 异常唯一标识
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public String getExceptionId() {
 | 
				
			||||||
 | 
					        return exceptionId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取异常发生时间
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 异常发生时间
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public LocalDateTime getTimestamp() {
 | 
				
			||||||
 | 
					        return timestamp;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取用户友好的错误消息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 用户友好的错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public String getUserFriendlyMessage() {
 | 
				
			||||||
 | 
					        return userFriendlyMessage;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 添加上下文信息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param key 键
 | 
				
			||||||
 | 
					     * @param value 值
 | 
				
			||||||
 | 
					     * @return 当前异常实例(支持链式调用)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException addContext(String key, Object value) {
 | 
				
			||||||
 | 
					        if (key != null && value != null) {
 | 
				
			||||||
 | 
					            this.context.put(key, value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 添加多个上下文信息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param contextMap 上下文信息映射
 | 
				
			||||||
 | 
					     * @return 当前异常实例(支持链式调用)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public FsHelperException addContext(Map<String, Object> contextMap) {
 | 
				
			||||||
 | 
					        if (contextMap != null) {
 | 
				
			||||||
 | 
					            this.context.putAll(contextMap);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取指定键的上下文值
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param key 键
 | 
				
			||||||
 | 
					     * @return 上下文值,如果不存在返回null
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public Object getContextValue(String key) {
 | 
				
			||||||
 | 
					        return context.get(key);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检查是否包含指定的上下文键
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param key 键
 | 
				
			||||||
 | 
					     * @return 如果包含返回true
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean hasContextKey(String key) {
 | 
				
			||||||
 | 
					        return context.containsKey(key);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取根因异常
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 根因异常,如果没有返回当前异常
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public Throwable getRootCause() {
 | 
				
			||||||
 | 
					        Throwable rootCause = this;
 | 
				
			||||||
 | 
					        while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
 | 
				
			||||||
 | 
					            rootCause = rootCause.getCause();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return rootCause;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取异常链信息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 异常链描述
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public String getExceptionChain() {
 | 
				
			||||||
 | 
					        StringBuilder chain = new StringBuilder();
 | 
				
			||||||
 | 
					        Throwable current = this;
 | 
				
			||||||
 | 
					        int level = 0;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        while (current != null && level < 10) { // 防止无限循环
 | 
				
			||||||
 | 
					            if (level > 0) {
 | 
				
			||||||
 | 
					                chain.append("\n");
 | 
				
			||||||
 | 
					                for (int i = 0; i < level; i++) {
 | 
				
			||||||
 | 
					                    chain.append("  ");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                chain.append("Caused by: ");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            chain.append(current.getClass().getSimpleName())
 | 
				
			||||||
 | 
					                 .append(": ")
 | 
				
			||||||
 | 
					                 .append(current.getMessage());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            current = current.getCause();
 | 
				
			||||||
 | 
					            level++;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return chain.toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检查是否为可重试的异常
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 如果可重试返回true
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean isRetryable() {
 | 
				
			||||||
 | 
					        return errorCode.isRetryable();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检查是否为致命异常
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 如果是致命异常返回true
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean isFatal() {
 | 
				
			||||||
 | 
					        return errorCode.isFatal();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检查是否为客户端异常
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 如果是客户端异常返回true
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean isClientError() {
 | 
				
			||||||
 | 
					        return errorCode.isClientError();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 检查是否为服务器异常
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 如果是服务器异常返回true
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean isServerError() {
 | 
				
			||||||
 | 
					        return errorCode.isServerError();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取异常的详细信息(用于日志记录)
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 详细信息字符串
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public String getDetailedInfo() {
 | 
				
			||||||
 | 
					        StringBuilder info = new StringBuilder();
 | 
				
			||||||
 | 
					        info.append("Exception Details:\n");
 | 
				
			||||||
 | 
					        info.append("  ID: ").append(exceptionId).append("\n");
 | 
				
			||||||
 | 
					        info.append("  Timestamp: ").append(timestamp.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).append("\n");
 | 
				
			||||||
 | 
					        info.append("  Error Code: ").append(errorCode.getCode()).append("\n");
 | 
				
			||||||
 | 
					        info.append("  Category: ").append(errorCode.getCategory().getName()).append("\n");
 | 
				
			||||||
 | 
					        info.append("  Message: ").append(getMessage()).append("\n");
 | 
				
			||||||
 | 
					        info.append("  User Friendly Message: ").append(userFriendlyMessage).append("\n");
 | 
				
			||||||
 | 
					        info.append("  Retryable: ").append(isRetryable()).append("\n");
 | 
				
			||||||
 | 
					        info.append("  Fatal: ").append(isFatal()).append("\n");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (!context.isEmpty()) {
 | 
				
			||||||
 | 
					            info.append("  Context:\n");
 | 
				
			||||||
 | 
					            context.forEach((key, value) -> 
 | 
				
			||||||
 | 
					                info.append("    ").append(key).append(": ").append(value).append("\n"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (getCause() != null) {
 | 
				
			||||||
 | 
					            info.append("  Exception Chain:\n");
 | 
				
			||||||
 | 
					            info.append("    ").append(getExceptionChain().replace("\n", "\n    "));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return info.toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 构建详细的错误消息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param errorCode 错误代码
 | 
				
			||||||
 | 
					     * @param message 原始消息
 | 
				
			||||||
 | 
					     * @param context 上下文信息
 | 
				
			||||||
 | 
					     * @return 详细的错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static String buildDetailedMessage(ErrorCode errorCode, String message, Map<String, Object> context) {
 | 
				
			||||||
 | 
					        StringBuilder detailedMessage = new StringBuilder();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (errorCode != null) {
 | 
				
			||||||
 | 
					            detailedMessage.append("[").append(errorCode.getCode()).append("] ");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (message != null && !message.trim().isEmpty()) {
 | 
				
			||||||
 | 
					            detailedMessage.append(message);
 | 
				
			||||||
 | 
					        } else if (errorCode != null) {
 | 
				
			||||||
 | 
					            detailedMessage.append(errorCode.getDefaultMessage());
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            detailedMessage.append("Unknown error occurred");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (context != null && !context.isEmpty()) {
 | 
				
			||||||
 | 
					            detailedMessage.append(" (Context: ");
 | 
				
			||||||
 | 
					            context.entrySet().stream()
 | 
				
			||||||
 | 
					                   .filter(entry -> !"exceptionId".equals(entry.getKey()) && 
 | 
				
			||||||
 | 
					                                   !"timestamp".equals(entry.getKey()) &&
 | 
				
			||||||
 | 
					                                   !"errorCode".equals(entry.getKey()) &&
 | 
				
			||||||
 | 
					                                   !"errorCategory".equals(entry.getKey()))
 | 
				
			||||||
 | 
					                   .forEach(entry -> detailedMessage.append(entry.getKey())
 | 
				
			||||||
 | 
					                                                   .append("=")
 | 
				
			||||||
 | 
					                                                   .append(entry.getValue())
 | 
				
			||||||
 | 
					                                                   .append(", "));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (detailedMessage.toString().endsWith(", ")) {
 | 
				
			||||||
 | 
					                detailedMessage.setLength(detailedMessage.length() - 2);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            detailedMessage.append(")");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return detailedMessage.toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 生成用户友好的错误消息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param errorCode 错误代码
 | 
				
			||||||
 | 
					     * @param originalMessage 原始消息
 | 
				
			||||||
 | 
					     * @return 用户友好的错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static String generateUserFriendlyMessage(ErrorCode errorCode, String originalMessage) {
 | 
				
			||||||
 | 
					        if (errorCode == null) {
 | 
				
			||||||
 | 
					            return "系统发生未知错误,请稍后重试或联系技术支持。";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 获取国际化的用户友好消息
 | 
				
			||||||
 | 
					        String friendlyMessage = errorCode.getMessage();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 根据错误类型提供不同的用户友好消息
 | 
				
			||||||
 | 
					        switch (errorCode.getCategory()) {
 | 
				
			||||||
 | 
					            case CLIENT:
 | 
				
			||||||
 | 
					                return "客户端配置问题:" + friendlyMessage + "。请检查配置后重试。";
 | 
				
			||||||
 | 
					            case API:
 | 
				
			||||||
 | 
					                return "服务调用失败:" + friendlyMessage + "。请稍后重试或联系技术支持。";
 | 
				
			||||||
 | 
					            case CONFIGURATION:
 | 
				
			||||||
 | 
					                return "配置错误:" + friendlyMessage + "。请检查配置文件。";
 | 
				
			||||||
 | 
					            case SECURITY:
 | 
				
			||||||
 | 
					                return "安全验证失败:" + friendlyMessage + "。请检查认证信息。";
 | 
				
			||||||
 | 
					            case RESOURCE:
 | 
				
			||||||
 | 
					                return "资源不足:" + friendlyMessage + "。请稍后重试。";
 | 
				
			||||||
 | 
					            case DATA:
 | 
				
			||||||
 | 
					                return "数据处理错误:" + friendlyMessage + "。请检查输入数据。";
 | 
				
			||||||
 | 
					            case BUSINESS:
 | 
				
			||||||
 | 
					                return "业务规则错误:" + friendlyMessage + "。请检查操作是否符合业务要求。";
 | 
				
			||||||
 | 
					            case SYSTEM:
 | 
				
			||||||
 | 
					                return "系统错误:" + friendlyMessage + "。请联系技术支持。";
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                return friendlyMessage + "。如问题持续,请联系技术支持。";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 创建构建器
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param errorCode 错误代码
 | 
				
			||||||
 | 
					     * @return 异常构建器
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static Builder builder(ErrorCode errorCode) {
 | 
				
			||||||
 | 
					        return new Builder(errorCode);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 异常构建器类
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static class Builder {
 | 
				
			||||||
 | 
					        private final ErrorCode errorCode;
 | 
				
			||||||
 | 
					        private String message;
 | 
				
			||||||
 | 
					        private Map<String, Object> context = new HashMap<>();
 | 
				
			||||||
 | 
					        private Throwable cause;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private Builder(ErrorCode errorCode) {
 | 
				
			||||||
 | 
					            this.errorCode = errorCode;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public Builder message(String message) {
 | 
				
			||||||
 | 
					            this.message = message;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public Builder context(String key, Object value) {
 | 
				
			||||||
 | 
					            this.context.put(key, value);
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public Builder context(Map<String, Object> context) {
 | 
				
			||||||
 | 
					            if (context != null) {
 | 
				
			||||||
 | 
					                this.context.putAll(context);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public Builder cause(Throwable cause) {
 | 
				
			||||||
 | 
					            this.cause = cause;
 | 
				
			||||||
 | 
					            return this;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public FsHelperException build() {
 | 
				
			||||||
 | 
					            return new FsHelperException(errorCode, message, context, cause);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										417
									
								
								src/main/java/cn/isliu/core/logging/FsLogger.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										417
									
								
								src/main/java/cn/isliu/core/logging/FsLogger.java
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,417 @@
 | 
				
			|||||||
 | 
					package cn.isliu.core.logging;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.isliu.core.enums.ErrorCode;
 | 
				
			||||||
 | 
					import org.slf4j.Logger;
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory;
 | 
				
			||||||
 | 
					import org.slf4j.MDC;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.concurrent.ConcurrentHashMap;
 | 
				
			||||||
 | 
					import java.util.concurrent.atomic.AtomicLong;
 | 
				
			||||||
 | 
					import java.util.regex.Pattern;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 统一日志管理器
 | 
				
			||||||
 | 
					 * 提供结构化日志记录、敏感信息脱敏、性能监控等功能
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @author liu
 | 
				
			||||||
 | 
					 * @since 0.0.2
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class FsLogger {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private static final Logger logger = LoggerFactory.getLogger(FsLogger.class);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 敏感信息脱敏模式
 | 
				
			||||||
 | 
					    private static final Pattern SENSITIVE_PATTERN = Pattern.compile(
 | 
				
			||||||
 | 
					        "(appSecret|token|password|key|secret)=[^&\\s]*", 
 | 
				
			||||||
 | 
					        Pattern.CASE_INSENSITIVE
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 性能监控指标
 | 
				
			||||||
 | 
					    private static final Map<String, AtomicLong> performanceMetrics = new ConcurrentHashMap<>();
 | 
				
			||||||
 | 
					    private static final Map<String, AtomicLong> operationCounts = new ConcurrentHashMap<>();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 日志级别控制
 | 
				
			||||||
 | 
					    private static volatile LogLevel minLogLevel = LogLevel.INFO;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 日志采样配置
 | 
				
			||||||
 | 
					    private static volatile int samplingRate = 1; // 1表示不采样,10表示每10条记录1条
 | 
				
			||||||
 | 
					    private static final AtomicLong logCounter = new AtomicLong(0);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 日志级别枚举
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public enum LogLevel {
 | 
				
			||||||
 | 
					        TRACE(0), DEBUG(1), INFO(2), WARN(3), ERROR(4);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        private final int level;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        LogLevel(int level) {
 | 
				
			||||||
 | 
					            this.level = level;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public int getLevel() {
 | 
				
			||||||
 | 
					            return level;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 记录API调用日志
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param operation API操作名称
 | 
				
			||||||
 | 
					     * @param params 请求参数
 | 
				
			||||||
 | 
					     * @param duration 执行时长(毫秒)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void apiCall(String operation, String params, long duration) {
 | 
				
			||||||
 | 
					        if (!shouldLog(LogLevel.DEBUG)) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 设置MDC上下文
 | 
				
			||||||
 | 
					            MDC.put("operation", operation);
 | 
				
			||||||
 | 
					            MDC.put("duration", String.valueOf(duration));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 更新性能指标
 | 
				
			||||||
 | 
					            updatePerformanceMetrics(operation, duration);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 记录日志
 | 
				
			||||||
 | 
					            if (logger.isDebugEnabled()) {
 | 
				
			||||||
 | 
					                logger.debug("API调用 - 操作: {} | 参数: {} | 耗时: {}ms", 
 | 
				
			||||||
 | 
					                    operation, sanitizeParams(params), duration);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            MDC.clear();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 记录API调用日志(带上下文信息)
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param operation API操作名称
 | 
				
			||||||
 | 
					     * @param params 请求参数
 | 
				
			||||||
 | 
					     * @param duration 执行时长(毫秒)
 | 
				
			||||||
 | 
					     * @param context 上下文信息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void apiCall(String operation, String params, long duration, Map<String, Object> context) {
 | 
				
			||||||
 | 
					        if (!shouldLog(LogLevel.DEBUG)) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 设置MDC上下文
 | 
				
			||||||
 | 
					            MDC.put("operation", operation);
 | 
				
			||||||
 | 
					            MDC.put("duration", String.valueOf(duration));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 添加自定义上下文
 | 
				
			||||||
 | 
					            if (context != null) {
 | 
				
			||||||
 | 
					                context.forEach((key, value) -> MDC.put(key, String.valueOf(value)));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 更新性能指标
 | 
				
			||||||
 | 
					            updatePerformanceMetrics(operation, duration);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 记录日志
 | 
				
			||||||
 | 
					            if (logger.isDebugEnabled()) {
 | 
				
			||||||
 | 
					                logger.debug("API调用 - 操作: {} | 参数: {} | 耗时: {}ms | 上下文: {}", 
 | 
				
			||||||
 | 
					                    operation, sanitizeParams(params), duration, formatContext(context));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            MDC.clear();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 记录错误日志
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param errorCode 错误代码
 | 
				
			||||||
 | 
					     * @param message 错误消息
 | 
				
			||||||
 | 
					     * @param context 上下文信息
 | 
				
			||||||
 | 
					     * @param cause 异常原因
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void error(ErrorCode errorCode, String message, String context, Throwable cause) {
 | 
				
			||||||
 | 
					        if (!shouldLog(LogLevel.ERROR)) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 设置MDC上下文
 | 
				
			||||||
 | 
					            MDC.put("errorCode", errorCode.getCode());
 | 
				
			||||||
 | 
					            MDC.put("errorType", errorCode.name());
 | 
				
			||||||
 | 
					            MDC.put("context", context);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 记录日志
 | 
				
			||||||
 | 
					            logger.error("错误 [{}]: {} | 上下文: {}", errorCode.getCode(), message, context, cause);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            MDC.clear();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 记录错误日志(简化版本)
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param errorCode 错误代码
 | 
				
			||||||
 | 
					     * @param message 错误消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void error(ErrorCode errorCode, String message) {
 | 
				
			||||||
 | 
					        error(errorCode, message, null, null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 记录信息日志
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param message 日志消息
 | 
				
			||||||
 | 
					     * @param args 参数
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void info(String message, Object... args) {
 | 
				
			||||||
 | 
					        if (!shouldLog(LogLevel.INFO)) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.info(sanitizeMessage(message), sanitizeArgs(args));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 记录警告日志
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param message 日志消息
 | 
				
			||||||
 | 
					     * @param args 参数
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void warn(String message, Object... args) {
 | 
				
			||||||
 | 
					        if (!shouldLog(LogLevel.WARN)) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.warn(sanitizeMessage(message), sanitizeArgs(args));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 记录调试日志
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param message 日志消息
 | 
				
			||||||
 | 
					     * @param args 参数
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void debug(String message, Object... args) {
 | 
				
			||||||
 | 
					        if (!shouldLog(LogLevel.DEBUG)) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (logger.isDebugEnabled()) {
 | 
				
			||||||
 | 
					            logger.debug(sanitizeMessage(message), sanitizeArgs(args));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 记录跟踪日志
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param message 日志消息
 | 
				
			||||||
 | 
					     * @param args 参数
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void trace(String message, Object... args) {
 | 
				
			||||||
 | 
					        if (!shouldLog(LogLevel.TRACE)) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (logger.isTraceEnabled()) {
 | 
				
			||||||
 | 
					            logger.trace(sanitizeMessage(message), sanitizeArgs(args));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 记录性能指标
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param operation 操作名称
 | 
				
			||||||
 | 
					     * @param duration 执行时长
 | 
				
			||||||
 | 
					     * @param success 是否成功
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void logPerformance(String operation, long duration, boolean success) {
 | 
				
			||||||
 | 
					        if (!shouldLog(LogLevel.INFO)) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 设置MDC上下文
 | 
				
			||||||
 | 
					            MDC.put("operation", operation);
 | 
				
			||||||
 | 
					            MDC.put("duration", String.valueOf(duration));
 | 
				
			||||||
 | 
					            MDC.put("success", String.valueOf(success));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 更新性能指标
 | 
				
			||||||
 | 
					            updatePerformanceMetrics(operation, duration);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 记录日志
 | 
				
			||||||
 | 
					            logger.info("性能指标 - 操作: {} | 耗时: {}ms | 状态: {}", 
 | 
				
			||||||
 | 
					                operation, duration, success ? "成功" : "失败");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            MDC.clear();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取性能指标
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 性能指标映射
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static Map<String, Long> getPerformanceMetrics() {
 | 
				
			||||||
 | 
					        Map<String, Long> metrics = new ConcurrentHashMap<>();
 | 
				
			||||||
 | 
					        performanceMetrics.forEach((key, value) -> metrics.put(key, value.get()));
 | 
				
			||||||
 | 
					        return metrics;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 获取操作计数
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @return 操作计数映射
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static Map<String, Long> getOperationCounts() {
 | 
				
			||||||
 | 
					        Map<String, Long> counts = new ConcurrentHashMap<>();
 | 
				
			||||||
 | 
					        operationCounts.forEach((key, value) -> counts.put(key, value.get()));
 | 
				
			||||||
 | 
					        return counts;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 重置性能指标
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void resetMetrics() {
 | 
				
			||||||
 | 
					        performanceMetrics.clear();
 | 
				
			||||||
 | 
					        operationCounts.clear();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 设置最小日志级别
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param level 日志级别
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void setMinLogLevel(LogLevel level) {
 | 
				
			||||||
 | 
					        minLogLevel = level;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 设置日志采样率
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param rate 采样率(1表示不采样,10表示每10条记录1条)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void setSamplingRate(int rate) {
 | 
				
			||||||
 | 
					        if (rate < 1) {
 | 
				
			||||||
 | 
					            throw new IllegalArgumentException("采样率必须大于等于1");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        samplingRate = rate;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 敏感信息脱敏
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param params 参数字符串
 | 
				
			||||||
 | 
					     * @return 脱敏后的参数字符串
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static String sanitizeParams(String params) {
 | 
				
			||||||
 | 
					        if (params == null || params.isEmpty()) {
 | 
				
			||||||
 | 
					            return params;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return SENSITIVE_PATTERN.matcher(params).replaceAll("$1=***");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 消息脱敏
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param message 消息
 | 
				
			||||||
 | 
					     * @return 脱敏后的消息
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static String sanitizeMessage(String message) {
 | 
				
			||||||
 | 
					        if (message == null || message.isEmpty()) {
 | 
				
			||||||
 | 
					            return message;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return SENSITIVE_PATTERN.matcher(message).replaceAll("$1=***");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 参数脱敏
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param args 参数数组
 | 
				
			||||||
 | 
					     * @return 脱敏后的参数数组
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static Object[] sanitizeArgs(Object[] args) {
 | 
				
			||||||
 | 
					        if (args == null || args.length == 0) {
 | 
				
			||||||
 | 
					            return args;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Object[] sanitizedArgs = new Object[args.length];
 | 
				
			||||||
 | 
					        for (int i = 0; i < args.length; i++) {
 | 
				
			||||||
 | 
					            if (args[i] instanceof String) {
 | 
				
			||||||
 | 
					                sanitizedArgs[i] = sanitizeParams((String) args[i]);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                sanitizedArgs[i] = args[i];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return sanitizedArgs;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 格式化上下文信息
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param context 上下文映射
 | 
				
			||||||
 | 
					     * @return 格式化后的上下文字符串
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static String formatContext(Map<String, Object> context) {
 | 
				
			||||||
 | 
					        if (context == null || context.isEmpty()) {
 | 
				
			||||||
 | 
					            return "{}";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        StringBuilder sb = new StringBuilder("{");
 | 
				
			||||||
 | 
					        context.forEach((key, value) -> {
 | 
				
			||||||
 | 
					            if (sb.length() > 1) {
 | 
				
			||||||
 | 
					                sb.append(", ");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            sb.append(key).append("=").append(value);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        sb.append("}");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return sb.toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 更新性能指标
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param operation 操作名称
 | 
				
			||||||
 | 
					     * @param duration 执行时长
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static void updatePerformanceMetrics(String operation, long duration) {
 | 
				
			||||||
 | 
					        // 更新总耗时
 | 
				
			||||||
 | 
					        performanceMetrics.computeIfAbsent(operation + "_total_duration", k -> new AtomicLong(0))
 | 
				
			||||||
 | 
					                          .addAndGet(duration);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 更新最大耗时
 | 
				
			||||||
 | 
					        performanceMetrics.computeIfAbsent(operation + "_max_duration", k -> new AtomicLong(0))
 | 
				
			||||||
 | 
					                          .updateAndGet(current -> Math.max(current, duration));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 更新操作计数
 | 
				
			||||||
 | 
					        operationCounts.computeIfAbsent(operation, k -> new AtomicLong(0))
 | 
				
			||||||
 | 
					                       .incrementAndGet();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 判断是否应该记录日志
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param level 日志级别
 | 
				
			||||||
 | 
					     * @return 是否应该记录
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static boolean shouldLog(LogLevel level) {
 | 
				
			||||||
 | 
					        // 检查日志级别
 | 
				
			||||||
 | 
					        if (level.getLevel() < minLogLevel.getLevel()) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 检查采样率
 | 
				
			||||||
 | 
					        if (samplingRate > 1) {
 | 
				
			||||||
 | 
					            long count = logCounter.incrementAndGet();
 | 
				
			||||||
 | 
					            return count % samplingRate == 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -11,8 +11,8 @@ import com.google.gson.JsonElement;
 | 
				
			|||||||
import com.google.gson.JsonObject;
 | 
					import com.google.gson.JsonObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.*;
 | 
					import java.util.*;
 | 
				
			||||||
import java.util.logging.Level;
 | 
					import cn.isliu.core.logging.FsLogger;
 | 
				
			||||||
import java.util.logging.Logger;
 | 
					import cn.isliu.core.enums.ErrorCode;
 | 
				
			||||||
import java.util.stream.Collectors;
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.lang.reflect.InvocationTargetException;
 | 
					import java.lang.reflect.InvocationTargetException;
 | 
				
			||||||
@ -24,7 +24,7 @@ import java.lang.reflect.InvocationTargetException;
 | 
				
			|||||||
 * 支持不同字段类型的转换处理
 | 
					 * 支持不同字段类型的转换处理
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
public class ConvertFieldUtil {
 | 
					public class ConvertFieldUtil {
 | 
				
			||||||
    private static final Logger log = Logger.getLogger(ConvertFieldUtil.class.getName());
 | 
					    // 使用统一的FsLogger替代java.util.logging.Logger
 | 
				
			||||||
    private static final Gson gson = new Gson();
 | 
					    private static final Gson gson = new Gson();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -226,15 +226,15 @@ public class ConvertFieldUtil {
 | 
				
			|||||||
                    FieldValueProcess fieldValueProcess = fieldFormatClass.getDeclaredConstructor().newInstance();
 | 
					                    FieldValueProcess fieldValueProcess = fieldFormatClass.getDeclaredConstructor().newInstance();
 | 
				
			||||||
                    result = fieldValueProcess.process(result);
 | 
					                    result = fieldValueProcess.process(result);
 | 
				
			||||||
                } catch (InstantiationException e) {
 | 
					                } catch (InstantiationException e) {
 | 
				
			||||||
                    log.log(Level.SEVERE, "无法实例化字段格式化类: " + fieldFormatClass.getName(), e);
 | 
					                    FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "无法实例化字段格式化类: " + fieldFormatClass.getName(), "convertFieldValue", e);
 | 
				
			||||||
                } catch (IllegalAccessException e) {
 | 
					                } catch (IllegalAccessException e) {
 | 
				
			||||||
                    log.log(Level.SEVERE, "无法访问字段格式化类的构造函数: " + fieldFormatClass.getName(), e);
 | 
					                    FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "无法访问字段格式化类的构造函数: " + fieldFormatClass.getName(), "convertFieldValue", e);
 | 
				
			||||||
                } catch (NoSuchMethodException e) {
 | 
					                } catch (NoSuchMethodException e) {
 | 
				
			||||||
                    log.log(Level.SEVERE, "字段格式化类缺少无参构造函数: " + fieldFormatClass.getName(), e);
 | 
					                    FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "字段格式化类缺少无参构造函数: " + fieldFormatClass.getName(), "convertFieldValue", e);
 | 
				
			||||||
                } catch (InvocationTargetException e) {
 | 
					                } catch (InvocationTargetException e) {
 | 
				
			||||||
                    log.log(Level.SEVERE, "字段格式化类构造函数调用异常: " + fieldFormatClass.getName(), e);
 | 
					                    FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "字段格式化类构造函数调用异常: " + fieldFormatClass.getName(), "convertFieldValue", e);
 | 
				
			||||||
                } catch (Exception e) {
 | 
					                } catch (Exception e) {
 | 
				
			||||||
                    log.log(Level.SEVERE, "创建字段格式化类实例时发生未知异常: " + fieldFormatClass.getName(), e);
 | 
					                    FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "创建字段格式化类实例时发生未知异常: " + fieldFormatClass.getName(), "convertFieldValue", e);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -258,7 +258,7 @@ public class ConvertFieldUtil {
 | 
				
			|||||||
                    FieldValueProcess fieldValueProcess = fieldFormatClass.newInstance();
 | 
					                    FieldValueProcess fieldValueProcess = fieldFormatClass.newInstance();
 | 
				
			||||||
                    result = fieldValueProcess.reverseProcess(result);
 | 
					                    result = fieldValueProcess.reverseProcess(result);
 | 
				
			||||||
                } catch (InstantiationException | IllegalAccessException e) {
 | 
					                } catch (InstantiationException | IllegalAccessException e) {
 | 
				
			||||||
                    log.log(Level.FINE, "format value error", e);
 | 
					                    FsLogger.debug("format value error: {}", e.getMessage());
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ import cn.isliu.core.SheetMeta;
 | 
				
			|||||||
import cn.isliu.core.ValuesBatch;
 | 
					import cn.isliu.core.ValuesBatch;
 | 
				
			||||||
import cn.isliu.core.client.FeishuClient;
 | 
					import cn.isliu.core.client.FeishuClient;
 | 
				
			||||||
import cn.isliu.core.exception.FsHelperException;
 | 
					import cn.isliu.core.exception.FsHelperException;
 | 
				
			||||||
 | 
					import cn.isliu.core.logging.FsLogger;
 | 
				
			||||||
import cn.isliu.core.pojo.ApiResponse;
 | 
					import cn.isliu.core.pojo.ApiResponse;
 | 
				
			||||||
import cn.isliu.core.service.*;
 | 
					import cn.isliu.core.service.*;
 | 
				
			||||||
import com.google.gson.Gson;
 | 
					import com.google.gson.Gson;
 | 
				
			||||||
@ -25,8 +26,8 @@ import java.io.IOException;
 | 
				
			|||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Objects;
 | 
					import java.util.Objects;
 | 
				
			||||||
import java.util.concurrent.atomic.AtomicReference;
 | 
					import java.util.concurrent.atomic.AtomicReference;
 | 
				
			||||||
import java.util.logging.Logger;
 | 
					import cn.isliu.core.logging.FsLogger;
 | 
				
			||||||
import java.util.logging.Level;
 | 
					import cn.isliu.core.enums.ErrorCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -37,7 +38,7 @@ import java.util.logging.Level;
 | 
				
			|||||||
public class FsApiUtil {
 | 
					public class FsApiUtil {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static final Gson gson = new Gson();
 | 
					    private static final Gson gson = new Gson();
 | 
				
			||||||
    private static final Logger log = Logger.getLogger(FsApiUtil.class.getName());
 | 
					    // 使用统一的FsLogger替代java.util.logging.Logger
 | 
				
			||||||
    private static final String REQ_TYPE = "JSON_STR";
 | 
					    private static final String REQ_TYPE = "JSON_STR";
 | 
				
			||||||
    public static final int DEFAULT_ROW_NUM = 1000;
 | 
					    public static final int DEFAULT_ROW_NUM = 1000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -71,11 +72,11 @@ public class FsApiUtil {
 | 
				
			|||||||
            if (batchRangeResp.success()) {
 | 
					            if (batchRangeResp.success()) {
 | 
				
			||||||
                valuesBatch = gson.fromJson(gson.toJson(batchRangeResp.getData()), ValuesBatch.class);
 | 
					                valuesBatch = gson.fromJson(gson.toJson(batchRangeResp.getData()), ValuesBatch.class);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】获取Sheet数据失败! 错误信息:{0}", gson.toJson(batchRangeResp));
 | 
					                FsLogger.error(ErrorCode.API_CALL_FAILED, "【飞书表格】获取Sheet数据失败! 错误信息:" + gson.toJson(batchRangeResp));
 | 
				
			||||||
                throw new FsHelperException("【飞书表格】获取Sheet数据失败!");
 | 
					                throw new FsHelperException("【飞书表格】获取Sheet数据失败!");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】获取Sheet数据失败! 错误信息:{0}", e.getMessage());
 | 
					            FsLogger.error(ErrorCode.API_CALL_FAILED, "【飞书表格】获取Sheet数据失败! 错误信息:" + e.getMessage(), "getSheetData", e);
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】获取Sheet数据失败!");
 | 
					            throw new FsHelperException("【飞书表格】获取Sheet数据失败!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return valuesBatch;
 | 
					        return valuesBatch;
 | 
				
			||||||
@ -115,12 +116,12 @@ public class FsApiUtil {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                return sheet.get();
 | 
					                return sheet.get();
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 获取Sheet元数据异常!错误信息:{0}", gson.toJson(resp));
 | 
					                FsLogger.error(ErrorCode.API_CALL_FAILED, "【飞书表格】 获取Sheet元数据异常!错误信息:" + gson.toJson(resp));
 | 
				
			||||||
                throw new FsHelperException("【飞书表格】 获取Sheet元数据异常!错误信息:" + resp.getMsg());
 | 
					                throw new FsHelperException("【飞书表格】 获取Sheet元数据异常!错误信息:" + resp.getMsg());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 获取Sheet元数据异常!错误信息:{0}", e.getMessage());
 | 
					            FsLogger.error(ErrorCode.API_CALL_FAILED, "【飞书表格】 获取Sheet元数据异常!错误信息:" + e.getMessage(), "getSheetMeta", e);
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】 获取Sheet元数据异常!");
 | 
					            throw new FsHelperException("【飞书表格】 获取Sheet元数据异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -145,11 +146,11 @@ public class FsApiUtil {
 | 
				
			|||||||
            ApiResponse batchMergeResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchMergeRequest);
 | 
					            ApiResponse batchMergeResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchMergeRequest);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!batchMergeResp.success()) {
 | 
					            if (!batchMergeResp.success()) {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 合并单元格请求异常!参数:{0},错误信息:{1}", new Object[]{cell, batchMergeResp.getMsg()});
 | 
					                FsLogger.warn("【飞书表格】 合并单元格请求异常!参数:{},错误信息:{}", cell, batchMergeResp.getMsg());
 | 
				
			||||||
                throw new FsHelperException("【飞书表格】 合并单元格请求异常!");
 | 
					                throw new FsHelperException("【飞书表格】 合并单元格请求异常!");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 合并单元格异常!参数:{0},错误信息:{1}", new Object[]{cell, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 合并单元格异常!参数:{},错误信息:{}", cell, e.getMessage());
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】 合并单元格异常!");
 | 
					            throw new FsHelperException("【飞书表格】 合并单元格异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -167,11 +168,11 @@ public class FsApiUtil {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            ApiResponse apiResponse = client.customValues().valueBatchUpdate(spreadsheetToken, batchValueRequest);
 | 
					            ApiResponse apiResponse = client.customValues().valueBatchUpdate(spreadsheetToken, batchValueRequest);
 | 
				
			||||||
            if (!apiResponse.success()) {
 | 
					            if (!apiResponse.success()) {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 写入表格头数据异常!错误信息:{0}", apiResponse.getMsg());
 | 
					                FsLogger.warn("【飞书表格】 写入表格头数据异常!错误信息:{}", apiResponse.getMsg());
 | 
				
			||||||
                throw new FsHelperException("【飞书表格】 写入表格头数据异常!");
 | 
					                throw new FsHelperException("【飞书表格】 写入表格头数据异常!");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 写入表格头异常!错误信息:{0}", e.getMessage());
 | 
					            FsLogger.warn("【飞书表格】 写入表格头异常!错误信息:{}", e.getMessage());
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】 写入表格头异常!");
 | 
					            throw new FsHelperException("【飞书表格】 写入表格头异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -186,11 +187,11 @@ public class FsApiUtil {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest);
 | 
					            ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest);
 | 
				
			||||||
            if (!apiResponse.success()) {
 | 
					            if (!apiResponse.success()) {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!参数:{0},错误信息:{1}", new Object[]{style, apiResponse.getMsg()});
 | 
					                FsLogger.warn("【飞书表格】 写入表格样式数据异常!参数:{},错误信息:{}", style, apiResponse.getMsg());
 | 
				
			||||||
                throw new FsHelperException("【飞书表格】 写入表格样式数据异常!");
 | 
					                throw new FsHelperException("【飞书表格】 写入表格样式数据异常!");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 写入表格样式异常!参数:{0},错误信息:{1}", new Object[]{style, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 写入表格样式异常!参数:{},错误信息:{}", style, e.getMessage());
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】 写入表格样式异常!");
 | 
					            throw new FsHelperException("【飞书表格】 写入表格样式异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -209,7 +210,7 @@ public class FsApiUtil {
 | 
				
			|||||||
            ApiResponse addResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, addSheetRequest);
 | 
					            ApiResponse addResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, addSheetRequest);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (addResp.success()) {
 | 
					            if (addResp.success()) {
 | 
				
			||||||
                log.log(Level.INFO, "【飞书表格】 创建 sheet 成功! {0}", gson.toJson(addResp));
 | 
					                FsLogger.info("【飞书表格】 创建 sheet 成功! {}", gson.toJson(addResp));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                JsonObject jsObj = gson.fromJson(gson.toJson(addResp.getData()), JsonObject.class);
 | 
					                JsonObject jsObj = gson.fromJson(gson.toJson(addResp.getData()), JsonObject.class);
 | 
				
			||||||
                JsonArray replies = jsObj.getAsJsonArray("replies");
 | 
					                JsonArray replies = jsObj.getAsJsonArray("replies");
 | 
				
			||||||
@ -218,16 +219,16 @@ public class FsApiUtil {
 | 
				
			|||||||
                Reply reply = gson.fromJson(jsonObject, Reply.class);
 | 
					                Reply reply = gson.fromJson(jsonObject, Reply.class);
 | 
				
			||||||
                sheetId = reply.getAddSheet().getProperties().getSheetId();
 | 
					                sheetId = reply.getAddSheet().getProperties().getSheetId();
 | 
				
			||||||
                if (sheetId == null || sheetId.isEmpty()) {
 | 
					                if (sheetId == null || sheetId.isEmpty()) {
 | 
				
			||||||
                    log.log(Level.SEVERE, "【飞书表格】 创建 sheet 失败!");
 | 
					                    FsLogger.warn("【飞书表格】 创建 sheet 失败!");
 | 
				
			||||||
                    throw new FsHelperException("【飞书表格】创建 sheet 异常!SheetId返回为空!");
 | 
					                    throw new FsHelperException("【飞书表格】创建 sheet 异常!SheetId返回为空!");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 创建 sheet 失败!错误信息:{0}", gson.toJson(addResp));
 | 
					                FsLogger.warn("【飞书表格】 创建 sheet 失败!错误信息:{}", gson.toJson(addResp));
 | 
				
			||||||
                throw new FsHelperException("【飞书表格】 创建 sheet 异常!");
 | 
					                throw new FsHelperException("【飞书表格】 创建 sheet 异常!");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            String message = e.getMessage();
 | 
					            String message = e.getMessage();
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 创建 sheet 异常!错误信息:{0}", message);
 | 
					            FsLogger.warn("【飞书表格】 创建 sheet 异常!错误信息:{}", message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            throw new FsHelperException(message != null && message.contains("403")? "请按照上方操作,当前智投无法操作对应文档哦" : "【飞书表格】 创建 sheet 异常!");
 | 
					            throw new FsHelperException(message != null && message.contains("403")? "请按照上方操作,当前智投无法操作对应文档哦" : "【飞书表格】 创建 sheet 异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -248,7 +249,7 @@ public class FsApiUtil {
 | 
				
			|||||||
            ApiResponse copyResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, copyRequest);
 | 
					            ApiResponse copyResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, copyRequest);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (copyResp.success()) {
 | 
					            if (copyResp.success()) {
 | 
				
			||||||
                log.log(Level.INFO, "【飞书表格】 复制 sheet 成功! {0}", gson.toJson(copyResp));
 | 
					                FsLogger.info("【飞书表格】 复制 sheet 成功! {}", gson.toJson(copyResp));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                JsonObject jsObj = gson.fromJson(gson.toJson(copyResp.getData()), JsonObject.class);
 | 
					                JsonObject jsObj = gson.fromJson(gson.toJson(copyResp.getData()), JsonObject.class);
 | 
				
			||||||
                JsonArray replies = jsObj.getAsJsonArray("replies");
 | 
					                JsonArray replies = jsObj.getAsJsonArray("replies");
 | 
				
			||||||
@ -262,7 +263,7 @@ public class FsApiUtil {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 复制模版异常!错误信息:{0}", e.getMessage());
 | 
					            FsLogger.warn("【飞书表格】 复制模版异常!错误信息:{}", e.getMessage());
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】 复制模版异常!");
 | 
					            throw new FsHelperException("【飞书表格】 复制模版异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return sheetId;
 | 
					        return sheetId;
 | 
				
			||||||
@ -287,10 +288,10 @@ public class FsApiUtil {
 | 
				
			|||||||
            ApiResponse batchStyleResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchStyleRequest);
 | 
					            ApiResponse batchStyleResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchStyleRequest);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!batchStyleResp.success()) {
 | 
					            if (!batchStyleResp.success()) {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!参数:{0},错误信息:{1}", new Object[]{conf, batchStyleResp.getMsg()});
 | 
					                FsLogger.warn("【飞书表格】 写入表格样式数据异常!参数:{},错误信息:{}", conf, batchStyleResp.getMsg());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!", e);
 | 
					            FsLogger.warn("【飞书表格】 写入表格样式数据异常!{}", e.getMessage());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -314,10 +315,10 @@ public class FsApiUtil {
 | 
				
			|||||||
            ApiResponse response = client.customDataValidations().dataValidationBatchUpdate(spreadsheetToken, batchRequest);
 | 
					            ApiResponse response = client.customDataValidations().dataValidationBatchUpdate(spreadsheetToken, batchRequest);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!response.success()) {
 | 
					            if (!response.success()) {
 | 
				
			||||||
                log.log(Level.SEVERE, "设置下拉列表失败, sheetId:{0}, startPosition:{1}, endPosition: {2}, 返回信息:{3}", new Object[]{sheetId, startPosition, endPosition, gson.toJson(response)});
 | 
					                FsLogger.warn("设置下拉列表失败, sheetId:{}, startPosition:{}, endPosition: {}, 返回信息:{}", sheetId, startPosition, endPosition, gson.toJson(response));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "设置下拉列表失败,sheetId:{0}", new Object[]{sheetId});
 | 
					            FsLogger.warn("设置下拉列表失败,sheetId:{}", sheetId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -332,10 +333,10 @@ public class FsApiUtil {
 | 
				
			|||||||
            ApiResponse deleteResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, deleteRequest);
 | 
					            ApiResponse deleteResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, deleteRequest);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!deleteResp.success()) {
 | 
					            if (!deleteResp.success()) {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 删除 sheet 失败!参数:{0},错误信息:{1}", new Object[]{sheetId, deleteResp.getMsg()});
 | 
					                FsLogger.warn("【飞书表格】 删除 sheet 失败!参数:{},错误信息:{}", sheetId, deleteResp.getMsg());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 删除 sheet 异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 删除 sheet 异常!参数:{},错误信息:{}", sheetId, e.getMessage());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -358,7 +359,7 @@ public class FsApiUtil {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 下载素材异常!参数:{0},错误信息:{1}", new Object[]{fileToken, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 下载素材异常!参数:{},错误信息:{}", fileToken, e.getMessage());
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】 下载素材异常!");
 | 
					            throw new FsHelperException("【飞书表格】 下载素材异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -375,16 +376,16 @@ public class FsApiUtil {
 | 
				
			|||||||
            if (resp.success()) {
 | 
					            if (resp.success()) {
 | 
				
			||||||
                return resp.getData().getTmpDownloadUrls()[0].getTmpDownloadUrl();
 | 
					                return resp.getData().getTmpDownloadUrls()[0].getTmpDownloadUrl();
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 获取临时下载地址失败!参数:{0},错误信息:{1}", new Object[]{fileToken, gson.toJson(resp)});
 | 
					                FsLogger.warn("【飞书表格】 获取临时下载地址失败!参数:{},错误信息:{}", fileToken, gson.toJson(resp));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 获取临时下载地址异常!参数:{0},错误信息:{1}", new Object[]{fileToken, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 获取临时下载地址异常!参数:{},错误信息:{}", fileToken, e.getMessage());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return tmpUrl;
 | 
					        return tmpUrl;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static Object putValues(String spreadsheetToken, CustomValueService.ValueRequest putValuesBuilder, FeishuClient client) {
 | 
					    public static Object putValues(String spreadsheetToken, CustomValueService.ValueRequest putValuesBuilder, FeishuClient client) {
 | 
				
			||||||
        log.log(Level.INFO, "【飞书表格】 putValues 开始写入数据!参数:{0}", gson.toJson(putValuesBuilder));
 | 
					        FsLogger.info("【飞书表格】 putValues 开始写入数据!参数:{}", gson.toJson(putValuesBuilder));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 添加到批量请求中
 | 
					        // 添加到批量请求中
 | 
				
			||||||
        CustomValueService.ValueBatchUpdateRequest putDataRequest = CustomValueService.ValueBatchUpdateRequest.newBuilder()
 | 
					        CustomValueService.ValueBatchUpdateRequest putDataRequest = CustomValueService.ValueBatchUpdateRequest.newBuilder()
 | 
				
			||||||
@ -396,11 +397,11 @@ public class FsApiUtil {
 | 
				
			|||||||
            if (putResp.success()) {
 | 
					            if (putResp.success()) {
 | 
				
			||||||
                return putResp.getData();
 | 
					                return putResp.getData();
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 写入表格数据失败!参数:{0},错误信息:{1}", new Object[]{putValuesBuilder, putResp.getMsg()});
 | 
					                FsLogger.warn("【飞书表格】 写入表格数据失败!参数:{},错误信息:{}", putValuesBuilder, putResp.getMsg());
 | 
				
			||||||
                throw new FsHelperException("【飞书表格】 写入表格数据失败!");
 | 
					                throw new FsHelperException("【飞书表格】 写入表格数据失败!");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (IOException e) {
 | 
					        } catch (IOException e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 写入表格数据异常!参数:{0},错误信息:{1}", new Object[]{spreadsheetToken, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 写入表格数据异常!参数:{},错误信息:{}", spreadsheetToken, e.getMessage());
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】 写入表格数据异常!");
 | 
					            throw new FsHelperException("【飞书表格】 写入表格数据异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -408,7 +409,7 @@ public class FsApiUtil {
 | 
				
			|||||||
    public static Object batchPutValues(String sheetId, String spreadsheetToken,
 | 
					    public static Object batchPutValues(String sheetId, String spreadsheetToken,
 | 
				
			||||||
                                        CustomValueService.ValueRequest batchPutRequest, FeishuClient client) {
 | 
					                                        CustomValueService.ValueRequest batchPutRequest, FeishuClient client) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        log.log(Level.INFO, "【飞书表格】 batchPutValues 开始写入数据!参数:{0}", gson.toJson(batchPutRequest));
 | 
					        FsLogger.info("【飞书表格】 batchPutValues 开始写入数据!参数:{}", gson.toJson(batchPutRequest));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            CustomValueService.ValueBatchUpdateRequest batchPutDataRequest =
 | 
					            CustomValueService.ValueBatchUpdateRequest batchPutDataRequest =
 | 
				
			||||||
@ -420,11 +421,11 @@ public class FsApiUtil {
 | 
				
			|||||||
            if (batchPutResp.success()) {
 | 
					            if (batchPutResp.success()) {
 | 
				
			||||||
                return batchPutResp.getData();
 | 
					                return batchPutResp.getData();
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 批量写入数据失败!参数:{0},错误信息:{1}", new Object[]{sheetId, gson.toJson(batchPutResp)});
 | 
					                FsLogger.warn("【飞书表格】 批量写入数据失败!参数:{},错误信息:{}", sheetId, gson.toJson(batchPutResp));
 | 
				
			||||||
                throw new FsHelperException("【飞书表格】 批量写入数据失败!");
 | 
					                throw new FsHelperException("【飞书表格】 批量写入数据失败!");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 批量写入数据异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 批量写入数据异常!参数:{},错误信息:{}", sheetId, e.getMessage());
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】 批量写入数据异常!");
 | 
					            throw new FsHelperException("【飞书表格】 批量写入数据异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -443,11 +444,11 @@ public class FsApiUtil {
 | 
				
			|||||||
            if (batchResp.success()) {
 | 
					            if (batchResp.success()) {
 | 
				
			||||||
                return batchResp.getData();
 | 
					                return batchResp.getData();
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 添加行列失败!参数:{0},错误信息:{1}", new Object[]{sheetId, gson.toJson(batchResp)});
 | 
					                FsLogger.warn("【飞书表格】 添加行列失败!参数:{},错误信息:{}", sheetId, gson.toJson(batchResp));
 | 
				
			||||||
                throw new FsHelperException("【飞书表格】 添加行列失败!");
 | 
					                throw new FsHelperException("【飞书表格】 添加行列失败!");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (IOException e) {
 | 
					        } catch (IOException e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 添加行列异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 添加行列异常!参数:{},错误信息:{}", sheetId, e.getMessage());
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】 添加行列异常!");
 | 
					            throw new FsHelperException("【飞书表格】 添加行列异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -466,10 +467,10 @@ public class FsApiUtil {
 | 
				
			|||||||
            if (resp.success()) {
 | 
					            if (resp.success()) {
 | 
				
			||||||
                return resp.getData();
 | 
					                return resp.getData();
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 获取表格信息失败!参数:{0},错误信息:{1}", new Object[]{sheetId, resp.getMsg()});
 | 
					                FsLogger.warn("【飞书表格】 获取表格信息失败!参数:{},错误信息:{}", sheetId, resp.getMsg());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 获取表格信息异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 获取表格信息异常!参数:{},错误信息:{}", sheetId, e.getMessage());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -487,11 +488,11 @@ public class FsApiUtil {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest);
 | 
					            ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest);
 | 
				
			||||||
            if (!apiResponse.success()) {
 | 
					            if (!apiResponse.success()) {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 设置单元格类型失败!参数:{0},错误信息:{1}", new Object[]{sheetId, apiResponse.getMsg()});
 | 
					                FsLogger.warn("【飞书表格】 设置单元格类型失败!参数:{},错误信息:{}", sheetId, apiResponse.getMsg());
 | 
				
			||||||
                throw new FsHelperException("【飞书表格】 批量设置单元格类型失败!");
 | 
					                throw new FsHelperException("【飞书表格】 批量设置单元格类型失败!");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 设置单元格类型失败!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 设置单元格类型失败!参数:{},错误信息:{}", sheetId, e.getMessage());
 | 
				
			||||||
            throw new FsHelperException("【飞书表格】 批量设置单元格类型异常!");
 | 
					            throw new FsHelperException("【飞书表格】 批量设置单元格类型异常!");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -513,11 +514,11 @@ public class FsApiUtil {
 | 
				
			|||||||
            ApiResponse imageResp = client.customValues().valueBatchUpdate(spreadsheetToken, imageWriteRequest);
 | 
					            ApiResponse imageResp = client.customValues().valueBatchUpdate(spreadsheetToken, imageWriteRequest);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!imageResp.success()) {
 | 
					            if (!imageResp.success()) {
 | 
				
			||||||
                log.log(Level.SEVERE, "【飞书表格】 图片上传失败!参数:{0},错误信息:{1}", new Object[]{filePath, gson.toJson(imageResp)});
 | 
					                FsLogger.warn("【飞书表格】 图片上传失败!参数:{},错误信息:{}", filePath, gson.toJson(imageResp));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return imageResp.getData();
 | 
					            return imageResp.getData();
 | 
				
			||||||
        } catch (Exception e) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            log.log(Level.SEVERE, "【飞书表格】 图片上传异常!参数:{0},错误信息:{1}", new Object[]{filePath, e.getMessage()});
 | 
					            FsLogger.warn("【飞书表格】 图片上传异常!参数:{},错误信息:{}", filePath, e.getMessage());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,35 +0,0 @@
 | 
				
			|||||||
package cn.isliu.core.utils;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import cn.isliu.core.client.FeishuClient;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public class FsClientUtil {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public static FeishuClient client;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 获取飞书客户端
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param appId     飞书应用ID
 | 
					 | 
				
			||||||
     * @param appSecret 飞书应用密钥
 | 
					 | 
				
			||||||
     * @return 飞书客户端
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static FeishuClient initFeishuClient(String appId, String appSecret) {
 | 
					 | 
				
			||||||
        client = FeishuClient.newBuilder(appId, appSecret).build();
 | 
					 | 
				
			||||||
        return client;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 设置飞书客户端
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param appId     飞书应用ID
 | 
					 | 
				
			||||||
     * @param appSecret 飞书应用密钥
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public static void setClient(String appId, String appSecret) {
 | 
					 | 
				
			||||||
        client = FeishuClient.newBuilder(appId, appSecret).build();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public static FeishuClient getFeishuClient() {
 | 
					 | 
				
			||||||
        return client;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -2,6 +2,8 @@ package cn.isliu.core.utils;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import cn.isliu.core.*;
 | 
					import cn.isliu.core.*;
 | 
				
			||||||
import cn.isliu.core.annotation.TableProperty;
 | 
					import cn.isliu.core.annotation.TableProperty;
 | 
				
			||||||
 | 
					import cn.isliu.core.client.FsClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import cn.isliu.core.config.FsConfig;
 | 
					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;
 | 
				
			||||||
@ -51,7 +53,7 @@ public class FsTableUtil {
 | 
				
			|||||||
            // 3. 获取工作表数据
 | 
					            // 3. 获取工作表数据
 | 
				
			||||||
            ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken,
 | 
					            ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken,
 | 
				
			||||||
                    "A" + startRowIndex,
 | 
					                    "A" + startRowIndex,
 | 
				
			||||||
                    getColumnName(colCount - 1) + endRowIndex, FsClientUtil.getFeishuClient());
 | 
					                    getColumnName(colCount - 1) + endRowIndex, FsClient.getInstance().getClient());
 | 
				
			||||||
            if (valuesBatch != null) {
 | 
					            if (valuesBatch != null) {
 | 
				
			||||||
                List<ValueRange> valueRanges = valuesBatch.getValueRanges();
 | 
					                List<ValueRange> valueRanges = valuesBatch.getValueRanges();
 | 
				
			||||||
                for (ValueRange valueRange : valueRanges) {
 | 
					                for (ValueRange valueRange : valueRanges) {
 | 
				
			||||||
@ -66,12 +68,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.getTitleLine() - 1)).findFirst()
 | 
					        dataList.stream().filter(d -> d.getRow() == (FsConfig.getInstance().getTitleLine() - 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.getHeadLine()).map(item -> {
 | 
					        return dataList.stream().filter(fsTableData -> fsTableData.getRow() >= FsConfig.getInstance().getHeadLine()).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();
 | 
				
			||||||
@ -264,8 +266,8 @@ public class FsTableUtil {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        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.getTitleLine(),
 | 
					                "A" + FsConfig.getInstance().getTitleLine(),
 | 
				
			||||||
                getColumnName(colCount - 1) + FsConfig.getTitleLine(), FsClientUtil.getFeishuClient());
 | 
					                getColumnName(colCount - 1) + FsConfig.getInstance().getTitleLine(), 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()) {
 | 
				
			||||||
@ -298,10 +300,10 @@ public class FsTableUtil {
 | 
				
			|||||||
                        position = FsTableUtil.getColumnNameByNuNumber(i + 1);
 | 
					                        position = FsTableUtil.getColumnNameByNuNumber(i + 1);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                int line = FsConfig.getTitleLine() + 1;
 | 
					                int line = FsConfig.getInstance().getTitleLine() + 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (tableProperty.enumClass() != BaseEnum.class) {
 | 
					                if (tableProperty.enumClass() != BaseEnum.class) {
 | 
				
			||||||
                    FsApiUtil.setOptions(sheetId, FsClientUtil.getFeishuClient(), 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,
 | 
				
			||||||
                            Arrays.stream(tableProperty.enumClass().getEnumConstants()).map(BaseEnum::getDesc).collect(Collectors.toList()));
 | 
					                            Arrays.stream(tableProperty.enumClass().getEnumConstants()).map(BaseEnum::getDesc).collect(Collectors.toList()));
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -315,7 +317,7 @@ public class FsTableUtil {
 | 
				
			|||||||
                        throw new RuntimeException(e);
 | 
					                        throw new RuntimeException(e);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    FsApiUtil.setOptions(sheetId, FsClientUtil.getFeishuClient(), 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,
 | 
				
			||||||
                            result);
 | 
					                            result);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -333,7 +335,8 @@ public class FsTableUtil {
 | 
				
			|||||||
        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", "A1:" + FsTableUtil.getColumnNameByNuNumber(size) + "1");
 | 
				
			||||||
        colorTemplate = colorTemplate.replace("FORE_COLOR", FsConfig.FORE_COLOR).replace("BACK_COLOR", FsConfig.BACK_COLOR);
 | 
					        colorTemplate = colorTemplate.replace("FORE_COLOR", FsConfig.getInstance().getForeColor())
 | 
				
			||||||
 | 
					                .replace("BACK_COLOR", FsConfig.getInstance().getBackColor());
 | 
				
			||||||
        return colorTemplate;
 | 
					        return colorTemplate;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -11,8 +11,8 @@ import java.lang.reflect.ParameterizedType;
 | 
				
			|||||||
import java.lang.reflect.Type;
 | 
					import java.lang.reflect.Type;
 | 
				
			||||||
import java.math.BigDecimal;
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
import java.util.*;
 | 
					import java.util.*;
 | 
				
			||||||
import java.util.logging.Level;
 | 
					import cn.isliu.core.logging.FsLogger;
 | 
				
			||||||
import java.util.logging.Logger;
 | 
					import cn.isliu.core.enums.ErrorCode;
 | 
				
			||||||
import java.util.stream.Collectors;
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -23,7 +23,7 @@ import java.util.stream.Collectors;
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
public class GenerateUtil {
 | 
					public class GenerateUtil {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static final Logger log = Logger.getLogger(GenerateUtil.class.getName());
 | 
					    // 使用统一的FsLogger替代java.util.logging.Logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 根据配置和数据生成DTO对象(通用版本)
 | 
					     * 根据配置和数据生成DTO对象(通用版本)
 | 
				
			||||||
@ -49,7 +49,7 @@ public class GenerateUtil {
 | 
				
			|||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    setNestedField(t, fieldPath, value);
 | 
					                    setNestedField(t, fieldPath, value);
 | 
				
			||||||
                } catch (Exception e) {
 | 
					                } catch (Exception e) {
 | 
				
			||||||
                    log.log(Level.SEVERE, "【巨量广告助手】 获取字段值异常!参数:{0},异常:{1}", new Object[]{fieldPath, e.getMessage()});
 | 
					                    FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "【巨量广告助手】 获取字段值异常!参数:" + fieldPath + ",异常:" + e.getMessage(), "generateList", e);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
@ -129,7 +129,7 @@ public class GenerateUtil {
 | 
				
			|||||||
                    field.set(target, nestedObj);
 | 
					                    field.set(target, nestedObj);
 | 
				
			||||||
                } catch (InstantiationException e) {
 | 
					                } catch (InstantiationException e) {
 | 
				
			||||||
                    // 如果无法创建实例,则记录日志并跳过该字段
 | 
					                    // 如果无法创建实例,则记录日志并跳过该字段
 | 
				
			||||||
                    log.log(Level.WARNING, "无法创建嵌套对象实例: " + field.getType().getName() + ", 字段: " + fieldName, e);
 | 
					                    FsLogger.warn("无法创建嵌套对象实例: {} , 字段: {}", field.getType().getName(), fieldName);
 | 
				
			||||||
                    return;
 | 
					                    return;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -153,7 +153,7 @@ public class GenerateUtil {
 | 
				
			|||||||
                    return elementClass.getDeclaredConstructor().newInstance();
 | 
					                    return elementClass.getDeclaredConstructor().newInstance();
 | 
				
			||||||
                } catch (Exception e) {
 | 
					                } catch (Exception e) {
 | 
				
			||||||
                    // 如果无法创建实例,则记录日志并返回null
 | 
					                    // 如果无法创建实例,则记录日志并返回null
 | 
				
			||||||
                    log.log(Level.WARNING, "无法创建List元素实例: " + elementClass.getName(), e);
 | 
					                    FsLogger.warn("无法创建List元素实例: {}", elementClass.getName());
 | 
				
			||||||
                    return null;
 | 
					                    return null;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -354,7 +354,7 @@ public class GenerateUtil {
 | 
				
			|||||||
                    result.put(fieldName, value);
 | 
					                    result.put(fieldName, value);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } catch (Exception e) {
 | 
					            } catch (Exception e) {
 | 
				
			||||||
                log.log(Level.WARNING, "获取字段值异常,字段路径:" + fieldPath, e);
 | 
					                FsLogger.warn("获取字段值异常,字段路径:{}", fieldPath);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return result;
 | 
					        return result;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										70
									
								
								src/main/resources/messages/errors.properties
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										70
									
								
								src/main/resources/messages/errors.properties
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					# Default error messages (English)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Client errors (FS001-FS099)
 | 
				
			||||||
 | 
					FS001=Client not initialized. Please initialize the client before use.
 | 
				
			||||||
 | 
					FS002=Client initialization failed. Check your configuration and credentials.
 | 
				
			||||||
 | 
					FS003=Client connection failed. Please check network connectivity.
 | 
				
			||||||
 | 
					FS004=Client authentication failed. Invalid credentials provided.
 | 
				
			||||||
 | 
					FS005=Client operation timeout. The operation took too long to complete.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# API errors (FS100-FS199)
 | 
				
			||||||
 | 
					FS100=API call failed. The remote service returned an error.
 | 
				
			||||||
 | 
					FS101=API rate limit exceeded. Please reduce request frequency.
 | 
				
			||||||
 | 
					FS102=Invalid API request. Check request parameters and format.
 | 
				
			||||||
 | 
					FS103=API unauthorized access. Authentication required.
 | 
				
			||||||
 | 
					FS104=API access forbidden. Insufficient permissions.
 | 
				
			||||||
 | 
					FS105=API resource not found. The requested resource does not exist.
 | 
				
			||||||
 | 
					FS106=API server error. The remote server encountered an error.
 | 
				
			||||||
 | 
					FS107=API response parse error. Unable to parse server response.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Concurrency errors (FS200-FS299)
 | 
				
			||||||
 | 
					FS200=Thread safety violation detected. Concurrent access not properly synchronized.
 | 
				
			||||||
 | 
					FS201=Concurrent modification detected. Resource was modified by another thread.
 | 
				
			||||||
 | 
					FS202=Deadlock detected. Multiple threads are waiting for each other.
 | 
				
			||||||
 | 
					FS203=Race condition occurred. Unexpected behavior due to timing issues.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Configuration errors (FS300-FS399)
 | 
				
			||||||
 | 
					FS300=Configuration error. Invalid or missing configuration.
 | 
				
			||||||
 | 
					FS301=Invalid configuration. Configuration values are not valid.
 | 
				
			||||||
 | 
					FS302=Configuration not found. Required configuration is missing.
 | 
				
			||||||
 | 
					FS303=Configuration parse error. Unable to parse configuration file.
 | 
				
			||||||
 | 
					FS304=Configuration validation failed. Configuration does not meet requirements.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Resource errors (FS400-FS499)
 | 
				
			||||||
 | 
					FS400=Resource exhausted. System resources are not available.
 | 
				
			||||||
 | 
					FS401=Insufficient memory. Not enough memory to complete operation.
 | 
				
			||||||
 | 
					FS402=Connection pool exhausted. No available connections in pool.
 | 
				
			||||||
 | 
					FS403=File not found. The specified file does not exist.
 | 
				
			||||||
 | 
					FS404=File access denied. Insufficient permissions to access file.
 | 
				
			||||||
 | 
					FS405=Insufficient disk space. Not enough disk space available.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Data errors (FS500-FS599)
 | 
				
			||||||
 | 
					FS500=Data validation failed. Input data does not meet validation rules.
 | 
				
			||||||
 | 
					FS501=Data conversion error. Unable to convert data to required format.
 | 
				
			||||||
 | 
					FS502=Data integrity violation. Data consistency check failed.
 | 
				
			||||||
 | 
					FS503=Data format error. Data is not in expected format.
 | 
				
			||||||
 | 
					FS504=Data size exceeded limit. Data size is larger than allowed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Security errors (FS600-FS699)
 | 
				
			||||||
 | 
					FS600=Security violation. Security policy has been violated.
 | 
				
			||||||
 | 
					FS601=Invalid credentials. Username or password is incorrect.
 | 
				
			||||||
 | 
					FS602=Access denied. You do not have permission to perform this action.
 | 
				
			||||||
 | 
					FS603=Token expired. Authentication token has expired.
 | 
				
			||||||
 | 
					FS604=Encryption failed. Unable to encrypt sensitive data.
 | 
				
			||||||
 | 
					FS605=Decryption failed. Unable to decrypt data.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Business logic errors (FS700-FS799)
 | 
				
			||||||
 | 
					FS700=Business logic error. Operation violates business rules.
 | 
				
			||||||
 | 
					FS701=Invalid operation. The requested operation is not valid.
 | 
				
			||||||
 | 
					FS702=Operation not supported. This operation is not supported.
 | 
				
			||||||
 | 
					FS703=Precondition failed. Required conditions are not met.
 | 
				
			||||||
 | 
					FS704=Workflow error. Error in business workflow execution.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# System errors (FS800-FS899)
 | 
				
			||||||
 | 
					FS800=System error. An unexpected system error occurred.
 | 
				
			||||||
 | 
					FS801=Service unavailable. The service is temporarily unavailable.
 | 
				
			||||||
 | 
					FS802=System in maintenance mode. System is currently under maintenance.
 | 
				
			||||||
 | 
					FS803=Version incompatible. Software version is not compatible.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Unknown error (FS999)
 | 
				
			||||||
 | 
					FS999=Unknown error. An unexpected error occurred.
 | 
				
			||||||
							
								
								
									
										70
									
								
								src/main/resources/messages/errors_zh_CN.properties
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										70
									
								
								src/main/resources/messages/errors_zh_CN.properties
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					# Chinese error messages (\u4e2d\u6587\u9519\u8bef\u6d88\u606f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# \u5ba2\u6237\u7aef\u9519\u8bef (FS001-FS0099)
 | 
				
			||||||
 | 
					FS001=\u5ba2\u6237\u7aef\u672a\u521d\u59cb\u5316\u3002\u8bf7\u5728\u4f7f\u7528\u524d\u521d\u59cb\u5316\u5ba2\u6237\u7aef\u3002
 | 
				
			||||||
 | 
					FS002=\u5ba2\u6237\u7aef\u521d\u59cb\u5316\u5931\u8d25\u3002\u8bf7\u68c0\u67e5\u914d\u7f6e\u548c\u51ed\u636e\u3002
 | 
				
			||||||
 | 
					FS003=\u5ba2\u6237\u7aef\u8fde\u63a5\u5931\u8d25\u3002\u8bf7\u68c0\u67e5\u7f51\u7edc\u8fde\u63a5\u3002
 | 
				
			||||||
 | 
					FS004=\u5ba2\u6237\u7aef\u8ba4\u8bc1\u5931\u8d25\u3002\u63d0\u4f9b\u7684\u51ed\u636e\u65e0\u6548\u3002
 | 
				
			||||||
 | 
					FS005=\u5ba2\u6237\u7aef\u64cd\u4f5c\u8d85\u65f6\u3002\u64cd\u4f5c\u5b8c\u6210\u65f6\u95f4\u8fc7\u957f\u3002
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# API\u9519\u8bef (FS100-FS199)
 | 
				
			||||||
 | 
					FS100=API\u8c03\u7528\u5931\u8d25\u3002\u8fdc\u7a0b\u670d\u52a1\u8fd4\u56de\u9519\u8bef\u3002
 | 
				
			||||||
 | 
					FS101=API\u8c03\u7528\u9891\u7387\u8d85\u9650\u3002\u8bf7\u964d\u4f4e\u8bf7\u6c42\u9891\u7387\u3002
 | 
				
			||||||
 | 
					FS102=\u65e0\u6548\u7684API\u8bf7\u6c42\u3002\u8bf7\u68c0\u67e5\u8bf7\u6c42\u53c2\u6570\u548c\u683c\u5f0f\u3002
 | 
				
			||||||
 | 
					FS103=API\u672a\u6388\u6743\u8bbf\u95ee\u3002\u9700\u8981\u8eab\u4efd\u9a8c\u8bc1\u3002
 | 
				
			||||||
 | 
					FS104=API\u8bbf\u95ee\u88ab\u7981\u6b62\u3002\u6743\u9650\u4e0d\u8db3\u3002
 | 
				
			||||||
 | 
					FS105=API\u8d44\u6e90\u672a\u627e\u5230\u3002\u8bf7\u6c42\u7684\u8d44\u6e90\u4e0d\u5b58\u5728\u3002
 | 
				
			||||||
 | 
					FS106=API\u670d\u52a1\u5668\u9519\u8bef\u3002\u8fdc\u7a0b\u670d\u52a1\u5668\u9047\u5230\u9519\u8bef\u3002
 | 
				
			||||||
 | 
					FS107=API\u54cd\u5e94\u89e3\u6790\u9519\u8bef\u3002\u65e0\u6cd5\u89e3\u6790\u670d\u52a1\u5668\u54cd\u5e94\u3002
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# \u5e76\u53d1\u9519\u8bef (FS200-FS299)
 | 
				
			||||||
 | 
					FS200=\u68c0\u6d4b\u5230\u7ebf\u7a0b\u5b89\u5168\u8fdd\u89c4\u3002\u5e76\u53d1\u8bbf\u95ee\u672a\u6b63\u786e\u540c\u6b65\u3002
 | 
				
			||||||
 | 
					FS201=\u68c0\u6d4b\u5230\u5e76\u53d1\u4fee\u6539\u3002\u8d44\u6e90\u88ab\u53e6\u4e00\u4e2a\u7ebf\u7a0b\u4fee\u6539\u3002
 | 
				
			||||||
 | 
					FS202=\u68c0\u6d4b\u5230\u6b7b\u9501\u3002\u591a\u4e2a\u7ebf\u7a0b\u76f8\u4e92\u7b49\u5f85\u3002
 | 
				
			||||||
 | 
					FS203=\u53d1\u751f\u7ade\u6001\u6761\u4ef6\u3002\u7531\u4e8e\u65f6\u5e8f\u95ee\u9898\u5bfc\u81f4\u610f\u5916\u884c\u4e3a\u3002
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# \u914d\u7f6e\u9519\u8bef (FS300-FS399)
 | 
				
			||||||
 | 
					FS300=\u914d\u7f6e\u9519\u8bef\u3002\u914d\u7f6e\u65e0\u6548\u6216\u7f3a\u5931\u3002
 | 
				
			||||||
 | 
					FS301=\u65e0\u6548\u914d\u7f6e\u3002\u914d\u7f6e\u503c\u65e0\u6548\u3002
 | 
				
			||||||
 | 
					FS302=\u914d\u7f6e\u672a\u627e\u5230\u3002\u7f3a\u5c11\u5fc5\u9700\u7684\u914d\u7f6e\u3002
 | 
				
			||||||
 | 
					FS303=\u914d\u7f6e\u89e3\u6790\u9519\u8bef\u3002\u65e0\u6cd5\u89e3\u6790\u914d\u7f6e\u6587\u4ef6\u3002
 | 
				
			||||||
 | 
					FS304=\u914d\u7f6e\u9a8c\u8bc1\u5931\u8d25\u3002\u914d\u7f6e\u4e0d\u7b26\u5408\u8981\u6c42\u3002
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# \u8d44\u6e90\u9519\u8bef (FS400-FS499)
 | 
				
			||||||
 | 
					FS400=\u8d44\u6e90\u8017\u5c3d\u3002\u7cfb\u7edf\u8d44\u6e90\u4e0d\u53ef\u7528\u3002
 | 
				
			||||||
 | 
					FS401=\u5185\u5b58\u4e0d\u8db3\u3002\u6ca1\u6709\u8db3\u591f\u7684\u5185\u5b58\u5b8c\u6210\u64cd\u4f5c\u3002
 | 
				
			||||||
 | 
					FS402=\u8fde\u63a5\u6c60\u8017\u5c3d\u3002\u6c60\u4e2d\u6ca1\u6709\u53ef\u7528\u8fde\u63a5\u3002
 | 
				
			||||||
 | 
					FS403=\u6587\u4ef6\u672a\u627e\u5230\u3002\u6307\u5b9a\u7684\u6587\u4ef6\u4e0d\u5b58\u5728\u3002
 | 
				
			||||||
 | 
					FS404=\u6587\u4ef6\u8bbf\u95ee\u88ab\u62d2\u7edd\u3002\u8bbf\u95ee\u6587\u4ef6\u7684\u6743\u9650\u4e0d\u8db3\u3002
 | 
				
			||||||
 | 
					FS405=\u78c1\u76d8\u7a7a\u95f4\u4e0d\u8db3\u3002\u53ef\u7528\u78c1\u76d8\u7a7a\u95f4\u4e0d\u591f\u3002
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# \u6570\u636e\u9519\u8bef (FS500-FS599)
 | 
				
			||||||
 | 
					FS500=\u6570\u636e\u9a8c\u8bc1\u5931\u8d25\u3002\u8f93\u5165\u6570\u636e\u4e0d\u7b26\u5408\u9a8c\u8bc1\u89c4\u5219\u3002
 | 
				
			||||||
 | 
					FS501=\u6570\u636e\u8f6c\u6362\u9519\u8bef\u3002\u65e0\u6cd5\u5c06\u6570\u636e\u8f6c\u6362\u4e3a\u6240\u9700\u683c\u5f0f\u3002
 | 
				
			||||||
 | 
					FS502=\u6570\u636e\u5b8c\u6574\u6027\u8fdd\u89c4\u3002\u6570\u636e\u4e00\u81f4\u6027\u68c0\u67e5\u5931\u8d25\u3002
 | 
				
			||||||
 | 
					FS503=\u6570\u636e\u683c\u5f0f\u9519\u8bef\u3002\u6570\u636e\u4e0d\u662f\u9884\u671f\u683c\u5f0f\u3002
 | 
				
			||||||
 | 
					FS504=\u6570\u636e\u5927\u5c0f\u8d85\u9650\u3002\u6570\u636e\u5927\u5c0f\u8d85\u8fc7\u5141\u8bb8\u8303\u56f4\u3002
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# \u5b89\u5168\u9519\u8bef (FS600-FS699)
 | 
				
			||||||
 | 
					FS600=\u5b89\u5168\u8fdd\u89c4\u3002\u8fdd\u53cd\u4e86\u5b89\u5168\u7b56\u7565\u3002
 | 
				
			||||||
 | 
					FS601=\u65e0\u6548\u51ed\u636e\u3002\u7528\u6237\u540d\u6216\u5bc6\u7801\u4e0d\u6b63\u786e\u3002
 | 
				
			||||||
 | 
					FS602=\u8bbf\u95ee\u88ab\u62d2\u7edd\u3002\u60a8\u6ca1\u6709\u6267\u884c\u6b64\u64cd\u4f5c\u7684\u6743\u9650\u3002
 | 
				
			||||||
 | 
					FS603=\u4ee4\u724c\u5df2\u8fc7\u671f\u3002\u8eab\u4efd\u9a8c\u8bc1\u4ee4\u724c\u5df2\u8fc7\u671f\u3002
 | 
				
			||||||
 | 
					FS604=\u52a0\u5bc6\u5931\u8d25\u3002\u65e0\u6cd5\u52a0\u5bc6\u654f\u611f\u6570\u636e\u3002
 | 
				
			||||||
 | 
					FS605=\u89e3\u5bc6\u5931\u8d25\u3002\u65e0\u6cd5\u89e3\u5bc6\u6570\u636e\u3002
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# \u4e1a\u52a1\u903b\u8f91\u9519\u8bef (FS700-FS799)
 | 
				
			||||||
 | 
					FS700=\u4e1a\u52a1\u903b\u8f91\u9519\u8bef\u3002\u64cd\u4f5c\u8fdd\u53cd\u4e1a\u52a1\u89c4\u5219\u3002
 | 
				
			||||||
 | 
					FS701=\u65e0\u6548\u64cd\u4f5c\u3002\u8bf7\u6c42\u7684\u64cd\u4f5c\u65e0\u6548\u3002
 | 
				
			||||||
 | 
					FS702=\u64cd\u4f5c\u4e0d\u652f\u6301\u3002\u6b64\u64cd\u4f5c\u4e0d\u53d7\u652f\u6301\u3002
 | 
				
			||||||
 | 
					FS703=\u524d\u7f6e\u6761\u4ef6\u5931\u8d25\u3002\u4e0d\u6ee1\u8db3\u5fc5\u9700\u6761\u4ef6\u3002
 | 
				
			||||||
 | 
					FS704=\u5de5\u4f5c\u6d41\u9519\u8bef\u3002\u4e1a\u52a1\u5de5\u4f5c\u6d41\u6267\u884c\u9519\u8bef\u3002
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# \u7cfb\u7edf\u9519\u8bef (FS800-FS899)
 | 
				
			||||||
 | 
					FS800=\u7cfb\u7edf\u9519\u8bef\u3002\u53d1\u751f\u610f\u5916\u7684\u7cfb\u7edf\u9519\u8bef\u3002
 | 
				
			||||||
 | 
					FS801=\u670d\u52a1\u4e0d\u53ef\u7528\u3002\u670d\u52a1\u6682\u65f6\u4e0d\u53ef\u7528\u3002
 | 
				
			||||||
 | 
					FS802=\u7cfb\u7edf\u7ef4\u62a4\u6a21\u5f0f\u3002\u7cfb\u7edf\u5f53\u524d\u6b63\u5728\u7ef4\u62a4\u3002
 | 
				
			||||||
 | 
					FS803=\u7248\u672c\u4e0d\u517c\u5bb9\u3002\u8f6f\u4ef6\u7248\u672c\u4e0d\u517c\u5bb9\u3002
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# \u672a\u77e5\u9519\u8bef (FS999)
 | 
				
			||||||
 | 
					FS999=\u672a\u77e5\u9519\u8bef\u3002\u53d1\u751f\u4e86\u610f\u5916\u9519\u8bef\u3002
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user