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();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void initConfig(FeishuClient client) {
|
// 通知配置变更
|
||||||
FsClientUtil.client = client;
|
ConfigSnapshot newSnapshot = getSnapshot();
|
||||||
|
notifyConfigChange(oldSnapshot, newSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getHeadLine() {
|
/**
|
||||||
|
* 验证配置参数
|
||||||
|
* @param builder 配置构建器
|
||||||
|
* @throws IllegalArgumentException 如果配置无效
|
||||||
|
*/
|
||||||
|
private void validateConfig(ConfigBuilder builder) {
|
||||||
|
if (builder.headLine != null && builder.headLine < 0) {
|
||||||
|
throw new IllegalArgumentException("headLine must be non-negative, got: " + builder.headLine);
|
||||||
|
}
|
||||||
|
if (builder.titleLine != null && builder.titleLine < 0) {
|
||||||
|
throw new IllegalArgumentException("titleLine must be non-negative, got: " + builder.titleLine);
|
||||||
|
}
|
||||||
|
if (builder.foreColor != null && !isValidColor(builder.foreColor)) {
|
||||||
|
throw new IllegalArgumentException("Invalid foreColor format: " + builder.foreColor);
|
||||||
|
}
|
||||||
|
if (builder.backColor != null && !isValidColor(builder.backColor)) {
|
||||||
|
throw new IllegalArgumentException("Invalid backColor format: " + builder.backColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证颜色格式
|
||||||
|
* @param color 颜色值
|
||||||
|
* @return 是否为有效的颜色格式
|
||||||
|
*/
|
||||||
|
private boolean isValidColor(String color) {
|
||||||
|
if (color == null || color.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 简单的十六进制颜色验证 #RRGGBB
|
||||||
|
return color.matches("^#[0-9A-Fa-f]{6}$");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知配置变更
|
||||||
|
* @param oldSnapshot 旧配置快照
|
||||||
|
* @param newSnapshot 新配置快照
|
||||||
|
*/
|
||||||
|
private void notifyConfigChange(ConfigSnapshot oldSnapshot, ConfigSnapshot newSnapshot) {
|
||||||
|
if (!listeners.isEmpty()) {
|
||||||
|
ConfigChangeEvent event = new ConfigChangeEvent(oldSnapshot, newSnapshot);
|
||||||
|
for (ConfigChangeListener listener : listeners) {
|
||||||
|
try {
|
||||||
|
listener.onConfigChanged(event);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 记录异常但不影响配置更新
|
||||||
|
System.err.println("Error notifying config change listener: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加配置变更监听器
|
||||||
|
* @param listener 监听器
|
||||||
|
*/
|
||||||
|
public void addConfigChangeListener(ConfigChangeListener listener) {
|
||||||
|
if (listener != null) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除配置变更监听器
|
||||||
|
* @param listener 监听器
|
||||||
|
*/
|
||||||
|
public void removeConfigChangeListener(ConfigChangeListener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 线程安全的配置读取 - 获取配置快照
|
||||||
|
* @return 不可变的配置快照
|
||||||
|
*/
|
||||||
|
public ConfigSnapshot getSnapshot() {
|
||||||
|
lock.readLock().lock();
|
||||||
|
try {
|
||||||
|
return new ConfigSnapshot(headLine, titleLine, isCover, cellText, foreColor, backColor);
|
||||||
|
} finally {
|
||||||
|
lock.readLock().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单独的getter方法,使用volatile保证可见性
|
||||||
|
public int getHeadLine() {
|
||||||
return headLine;
|
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