diff --git a/.gitignore b/.gitignore
index acb7754..186de29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
target/
+test/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
@@ -12,6 +13,7 @@ target/
*.iml
*.ipr
.idea
+.kiro
### Eclipse ###
.apt_generated
diff --git a/pom.xml b/pom.xml
index f0dd732..c71954f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
cn.isliu
feishu-table-helper
- 0.0.1
+ 0.0.2
${project.groupId}:${project.artifactId}
@@ -57,6 +57,11 @@
okhttp
4.12.0
+
+ com.squareup.okhttp3
+ logging-interceptor
+ 4.12.0
+
com.google.code.gson
gson
@@ -105,46 +110,53 @@
-
-
- org.apache.maven.plugins
- maven-gpg-plugin
- 1.6
-
-
- sign-artifacts
- verify
-
- sign
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- org.sonatype.central
- central-publishing-maven-plugin
- 0.5.0
- true
-
-
- central
-
- true
-
- published
-
- ${project.groupId}:${project.artifactId}:${project.version}
-
-
-
-
- org.apache.maven.plugins
- maven-release-plugin
- 2.5.3
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/cn/isliu/FsHelper.java b/src/main/java/cn/isliu/FsHelper.java
index 50a581f..cf73a92 100644
--- a/src/main/java/cn/isliu/FsHelper.java
+++ b/src/main/java/cn/isliu/FsHelper.java
@@ -3,6 +3,8 @@ package cn.isliu;
import cn.isliu.core.BaseEntity;
import cn.isliu.core.FsTableData;
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.pojo.FieldProperty;
import cn.isliu.core.service.CustomValueService;
@@ -38,19 +40,21 @@ public class FsHelper {
Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
List headers = PropertyUtil.getHeaders(fieldsMap);
+ FeishuClient client = FsClient.getInstance().getClient();
// 1、创建sheet
- String sheetId = FsApiUtil.createSheet(sheetName, FsClientUtil.getFeishuClient(), spreadsheetToken);
+ String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
// 2 添加表头数据
- FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers), FsClientUtil.getFeishuClient());
+ FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers), client);
// 3 设置表格样式
- FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size()), sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken);
+ FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size()), sheetId, client, spreadsheetToken);
// 4 设置单元格为文本格式
- if (FsConfig.CELL_TEXT) {
+ FsConfig fsConfig = FsConfig.getInstance();
+ if (fsConfig.isCellText()) {
String column = FsTableUtil.getColumnNameByNuNumber(headers.size());
- FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, FsClientUtil.getFeishuClient(), spreadsheetToken);
+ FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
}
// 5 设置表格下拉
@@ -72,7 +76,8 @@ public class FsHelper {
*/
public static List read(String sheetId, String spreadsheetToken, Class clazz) {
List results = new ArrayList<>();
- Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken);
+ FeishuClient client = FsClient.getInstance().getClient();
+ Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
@@ -112,7 +117,8 @@ public class FsHelper {
Class> aClass = dataList.get(0).getClass();
Map 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 fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
Map currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow));
@@ -131,6 +137,8 @@ public class FsHelper {
// 初始化批量插入对象
CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
+ FsConfig fsConfig = FsConfig.getInstance();
+
AtomicInteger rowCount = new AtomicInteger(row[0] + 1);
for (T data : dataList) {
@@ -142,7 +150,7 @@ public class FsHelper {
if (uniqueId != null && rowNum.get() != null) {
rowNum.set(rowNum.get() + 1);
values.forEach((field, fieldValue) -> {
- if (!FsConfig.isCover && fieldValue == null) {
+ if (!fsConfig.isCover() && fieldValue == null) {
return;
}
@@ -153,7 +161,7 @@ public class FsHelper {
} else {
int rowCou = rowCount.incrementAndGet();
values.forEach((field, fieldValue) -> {
- if (!FsConfig.isCover && fieldValue == null) {
+ if (!fsConfig.isCover() && fieldValue == null) {
return;
}
@@ -168,9 +176,9 @@ public class FsHelper {
int rowTotal = sheet.getGridProperties().getRowCount();
int rowNum = rowCount.get();
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);
}
}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/client/FsClient.java b/src/main/java/cn/isliu/core/client/FsClient.java
new file mode 100644
index 0000000..a7a1d79
--- /dev/null
+++ b/src/main/java/cn/isliu/core/client/FsClient.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/client/OptimizedHttpClientFactory.java b/src/main/java/cn/isliu/core/client/OptimizedHttpClientFactory.java
new file mode 100644
index 0000000..47719b7
--- /dev/null
+++ b/src/main/java/cn/isliu/core/client/OptimizedHttpClientFactory.java
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/config/ConfigBuilder.java b/src/main/java/cn/isliu/core/config/ConfigBuilder.java
new file mode 100644
index 0000000..4ca6076
--- /dev/null
+++ b/src/main/java/cn/isliu/core/config/ConfigBuilder.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/config/ConfigChangeEvent.java b/src/main/java/cn/isliu/core/config/ConfigChangeEvent.java
new file mode 100644
index 0000000..446a995
--- /dev/null
+++ b/src/main/java/cn/isliu/core/config/ConfigChangeEvent.java
@@ -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 +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/config/ConfigChangeListener.java b/src/main/java/cn/isliu/core/config/ConfigChangeListener.java
new file mode 100644
index 0000000..fe20a2c
--- /dev/null
+++ b/src/main/java/cn/isliu/core/config/ConfigChangeListener.java
@@ -0,0 +1,13 @@
+package cn.isliu.core.config;
+
+/**
+ * 配置变更监听器接口
+ */
+public interface ConfigChangeListener {
+
+ /**
+ * 配置变更时的回调方法
+ * @param event 配置变更事件
+ */
+ void onConfigChanged(ConfigChangeEvent event);
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/config/ConfigSnapshot.java b/src/main/java/cn/isliu/core/config/ConfigSnapshot.java
new file mode 100644
index 0000000..772162c
--- /dev/null
+++ b/src/main/java/cn/isliu/core/config/ConfigSnapshot.java
@@ -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 +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/config/FsConfig.java b/src/main/java/cn/isliu/core/config/FsConfig.java
index 823d83e..6d94ee2 100644
--- a/src/main/java/cn/isliu/core/config/FsConfig.java
+++ b/src/main/java/cn/isliu/core/config/FsConfig.java
@@ -1,56 +1,302 @@
package cn.isliu.core.config;
-import cn.isliu.core.client.FeishuClient;
-import cn.isliu.core.utils.FsClientUtil;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+/**
+ * 线程安全的配置管理器
+ * 使用volatile关键字和ReadWriteLock确保线程安全
+ */
public class FsConfig {
-
-
- public static int headLine = 1;
-
- public static int titleLine = 1;
-
- public static boolean isCover = false;
- public static boolean CELL_TEXT = false;
- public static String FORE_COLOR = "#000000";
- public static String BACK_COLOR = "#d5d5d5";
-
- public static void initConfig(String appId, String appSecret) {
- FsClientUtil.initFeishuClient(appId, appSecret);
+
+ // 使用volatile确保可见性
+ private volatile int headLine = 1;
+ private volatile int titleLine = 1;
+ private volatile boolean isCover = false;
+ private volatile boolean cellText = false;
+ private volatile String foreColor = "#000000";
+ private volatile String backColor = "#d5d5d5";
+
+ // 读写锁保护配置更新操作
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+ // 配置变更监听器列表
+ private final List listeners = new CopyOnWriteArrayList<>();
+
+ // 单例实例
+ private static volatile FsConfig instance;
+
+ private FsConfig() {
}
-
- public static void initConfig(int headLine, int titleLine, String appId, String appSecret) {
- FsConfig.headLine = headLine;
- FsConfig.titleLine = titleLine;
- FsClientUtil.initFeishuClient(appId, appSecret);
+
+ /**
+ * 获取单例实例
+ * @return ThreadSafeConfig实例
+ */
+ public static FsConfig getInstance() {
+ if (instance == null) {
+ synchronized (FsConfig.class) {
+ if (instance == null) {
+ instance = new FsConfig();
+ }
+ }
+ }
+ return instance;
}
-
- public static void initConfig(int headLine, FeishuClient client) {
- FsConfig.headLine = headLine;
- FsClientUtil.client = client;
+
+ /**
+ * 原子性配置更新
+ * @param builder 配置构建器
+ */
+ public void updateConfig(ConfigBuilder builder) {
+ // 验证配置
+ validateConfig(builder);
+
+ ConfigSnapshot oldSnapshot = getSnapshot();
+
+ lock.writeLock().lock();
+ try {
+ if (builder.headLine != null) {
+ this.headLine = builder.headLine;
+ }
+ if (builder.titleLine != null) {
+ this.titleLine = builder.titleLine;
+ }
+ if (builder.isCover != null) {
+ this.isCover = builder.isCover;
+ }
+ if (builder.cellText != null) {
+ this.cellText = builder.cellText;
+ }
+ if (builder.foreColor != null) {
+ this.foreColor = builder.foreColor;
+ }
+ if (builder.backColor != null) {
+ this.backColor = builder.backColor;
+ }
+ } finally {
+ lock.writeLock().unlock();
+ }
+
+ // 通知配置变更
+ ConfigSnapshot newSnapshot = getSnapshot();
+ notifyConfigChange(oldSnapshot, newSnapshot);
}
-
- public static void initConfig(FeishuClient client) {
- FsClientUtil.client = client;
+
+ /**
+ * 验证配置参数
+ * @param builder 配置构建器
+ * @throws IllegalArgumentException 如果配置无效
+ */
+ private void validateConfig(ConfigBuilder builder) {
+ if (builder.headLine != null && builder.headLine < 0) {
+ throw new IllegalArgumentException("headLine must be non-negative, got: " + builder.headLine);
+ }
+ if (builder.titleLine != null && builder.titleLine < 0) {
+ throw new IllegalArgumentException("titleLine must be non-negative, got: " + builder.titleLine);
+ }
+ if (builder.foreColor != null && !isValidColor(builder.foreColor)) {
+ throw new IllegalArgumentException("Invalid foreColor format: " + builder.foreColor);
+ }
+ if (builder.backColor != null && !isValidColor(builder.backColor)) {
+ throw new IllegalArgumentException("Invalid backColor format: " + builder.backColor);
+ }
}
-
- public static int getHeadLine() {
+
+ /**
+ * 验证颜色格式
+ * @param color 颜色值
+ * @return 是否为有效的颜色格式
+ */
+ private boolean isValidColor(String color) {
+ if (color == null || color.trim().isEmpty()) {
+ return false;
+ }
+ // 简单的十六进制颜色验证 #RRGGBB
+ return color.matches("^#[0-9A-Fa-f]{6}$");
+ }
+
+ /**
+ * 通知配置变更
+ * @param oldSnapshot 旧配置快照
+ * @param newSnapshot 新配置快照
+ */
+ private void notifyConfigChange(ConfigSnapshot oldSnapshot, ConfigSnapshot newSnapshot) {
+ if (!listeners.isEmpty()) {
+ ConfigChangeEvent event = new ConfigChangeEvent(oldSnapshot, newSnapshot);
+ for (ConfigChangeListener listener : listeners) {
+ try {
+ listener.onConfigChanged(event);
+ } catch (Exception e) {
+ // 记录异常但不影响配置更新
+ System.err.println("Error notifying config change listener: " + e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * 添加配置变更监听器
+ * @param listener 监听器
+ */
+ public void addConfigChangeListener(ConfigChangeListener listener) {
+ if (listener != null) {
+ listeners.add(listener);
+ }
+ }
+
+ /**
+ * 移除配置变更监听器
+ * @param listener 监听器
+ */
+ public void removeConfigChangeListener(ConfigChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * 线程安全的配置读取 - 获取配置快照
+ * @return 不可变的配置快照
+ */
+ public ConfigSnapshot getSnapshot() {
+ lock.readLock().lock();
+ try {
+ return new ConfigSnapshot(headLine, titleLine, isCover, cellText, foreColor, backColor);
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ // 单独的getter方法,使用volatile保证可见性
+ public int getHeadLine() {
return headLine;
}
-
- public static int getTitleLine() {
+
+ public int getTitleLine() {
return titleLine;
}
-
- public static FeishuClient getFeishuClient() {
- return FsClientUtil.client;
+
+ public boolean isCover() {
+ return isCover;
}
-
- public static void setHeadLine(int headLine) {
- FsConfig.headLine = headLine;
+
+ public boolean isCellText() {
+ return cellText;
}
-
- public static void setTitleLine(int titleLine) {
- FsConfig.titleLine = titleLine;
+
+ public String getForeColor() {
+ return foreColor;
}
-}
+
+ public String getBackColor() {
+ return backColor;
+ }
+
+ // 单独的setter方法,使用写锁保护
+ public void setHeadLine(int headLine) {
+ if (headLine < 0) {
+ throw new IllegalArgumentException("headLine must be non-negative, got: " + headLine);
+ }
+
+ ConfigSnapshot oldSnapshot = getSnapshot();
+
+ lock.writeLock().lock();
+ try {
+ this.headLine = headLine;
+ } finally {
+ lock.writeLock().unlock();
+ }
+
+ ConfigSnapshot newSnapshot = getSnapshot();
+ notifyConfigChange(oldSnapshot, newSnapshot);
+ }
+
+ public void setTitleLine(int titleLine) {
+ if (titleLine < 0) {
+ throw new IllegalArgumentException("titleLine must be non-negative, got: " + titleLine);
+ }
+
+ ConfigSnapshot oldSnapshot = getSnapshot();
+
+ lock.writeLock().lock();
+ try {
+ this.titleLine = titleLine;
+ } finally {
+ lock.writeLock().unlock();
+ }
+
+ ConfigSnapshot newSnapshot = getSnapshot();
+ notifyConfigChange(oldSnapshot, newSnapshot);
+ }
+
+ public void setIsCover(boolean isCover) {
+ ConfigSnapshot oldSnapshot = getSnapshot();
+
+ lock.writeLock().lock();
+ try {
+ this.isCover = isCover;
+ } finally {
+ lock.writeLock().unlock();
+ }
+
+ ConfigSnapshot newSnapshot = getSnapshot();
+ notifyConfigChange(oldSnapshot, newSnapshot);
+ }
+
+ public void setCellText(boolean cellText) {
+ ConfigSnapshot oldSnapshot = getSnapshot();
+
+ lock.writeLock().lock();
+ try {
+ this.cellText = cellText;
+ } finally {
+ lock.writeLock().unlock();
+ }
+
+ ConfigSnapshot newSnapshot = getSnapshot();
+ notifyConfigChange(oldSnapshot, newSnapshot);
+ }
+
+ public void setForeColor(String foreColor) {
+ if (foreColor == null) {
+ throw new IllegalArgumentException("foreColor cannot be null");
+ }
+ if (!isValidColor(foreColor)) {
+ throw new IllegalArgumentException("Invalid foreColor format: " + foreColor);
+ }
+
+ ConfigSnapshot oldSnapshot = getSnapshot();
+
+ lock.writeLock().lock();
+ try {
+ this.foreColor = foreColor;
+ } finally {
+ lock.writeLock().unlock();
+ }
+
+ ConfigSnapshot newSnapshot = getSnapshot();
+ notifyConfigChange(oldSnapshot, newSnapshot);
+ }
+
+ public void setBackColor(String backColor) {
+ if (backColor == null) {
+ throw new IllegalArgumentException("backColor cannot be null");
+ }
+ if (!isValidColor(backColor)) {
+ throw new IllegalArgumentException("Invalid backColor format: " + backColor);
+ }
+
+ ConfigSnapshot oldSnapshot = getSnapshot();
+
+ lock.writeLock().lock();
+ try {
+ this.backColor = backColor;
+ } finally {
+ lock.writeLock().unlock();
+ }
+
+ ConfigSnapshot newSnapshot = getSnapshot();
+ notifyConfigChange(oldSnapshot, newSnapshot);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/converters/FileUrlProcess.java b/src/main/java/cn/isliu/core/converters/FileUrlProcess.java
index 35aee3f..18c5739 100644
--- a/src/main/java/cn/isliu/core/converters/FileUrlProcess.java
+++ b/src/main/java/cn/isliu/core/converters/FileUrlProcess.java
@@ -1,7 +1,8 @@
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.FsClientUtil;
import cn.isliu.core.utils.FileUtil;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -10,12 +11,11 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import cn.isliu.core.logging.FsLogger;
public class FileUrlProcess implements FieldValueProcess {
- private static final Logger log = Logger.getLogger(FileUrlProcess.class.getName());
+ // 使用统一的FsLogger替代java.util.logging.Logger
@Override
public String process(Object value) {
@@ -44,7 +44,11 @@ public class FileUrlProcess implements FieldValueProcess {
@Override
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) {
return null;
}
@@ -83,20 +87,20 @@ public class FileUrlProcess implements FieldValueProcess {
boolean isSuccess = true;
try {
- FsApiUtil.downloadMaterial(fileToken, filePath , FsClientUtil.getFeishuClient(), null);
+ FsApiUtil.downloadMaterial(fileToken, filePath , FsClient.getInstance().getClient(), null);
url = filePath;
} 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;
}
if (!isSuccess) {
- String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(fileToken, FsClientUtil.getFeishuClient());
+ String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(fileToken, FsClient.getInstance().getClient());
// 根据临时下载地址下载
FileUtil.downloadFile(tmpUrl, filePath);
}
- log.info("【飞书表格】 文件上传-飞书图片上传成功!fileToken: " + fileToken + ", filePath: " + filePath);
+ FsLogger.info("【飞书表格】 文件上传-飞书图片上传成功!fileToken: {}, filePath: {}", fileToken, filePath);
return url;
}
@@ -110,19 +114,19 @@ public class FileUrlProcess implements FieldValueProcess {
boolean isSuccess = true;
try {
- FsApiUtil.downloadMaterial(token, path , FsClientUtil.getFeishuClient(), null);
+ FsApiUtil.downloadMaterial(token, path , FsClient.getInstance().getClient(), null);
url = path;
} 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;
}
if (!isSuccess) {
- String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(token, FsClientUtil.getFeishuClient());
+ String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(token, FsClient.getInstance().getClient());
FileUtil.downloadFile(tmpUrl, path);
}
- log.info("【飞书表格】 文件上传-附件上传成功!fileToken: " + token + ", filePath: " + path);
+ FsLogger.info("【飞书表格】 文件上传-附件上传成功!fileToken: {}, filePath: {}", token, path);
return url;
}
}
diff --git a/src/main/java/cn/isliu/core/enums/ErrorCode.java b/src/main/java/cn/isliu/core/enums/ErrorCode.java
new file mode 100644
index 0000000..dc6151c
--- /dev/null
+++ b/src/main/java/cn/isliu/core/enums/ErrorCode.java
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/exception/ExceptionHandler.java b/src/main/java/cn/isliu/core/exception/ExceptionHandler.java
new file mode 100644
index 0000000..ac5fdc0
--- /dev/null
+++ b/src/main/java/cn/isliu/core/exception/ExceptionHandler.java
@@ -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 exceptionCounters = new ConcurrentHashMap<>();
+
+ /** 异常分类统计 */
+ private final Map categoryCounters = new ConcurrentHashMap<>();
+
+ /** 最近异常记录 */
+ private final List recentExceptions = Collections.synchronizedList(new ArrayList<>());
+
+ /** 最大记录数量 */
+ private static final int MAX_RECENT_EXCEPTIONS = 100;
+
+ /** 异常处理监听器 */
+ private final List 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 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 errorCodeCounts = new ConcurrentHashMap<>();
+ Map categoryCounts = new ConcurrentHashMap<>();
+
+ for (Map.Entry entry : exceptionCounters.entrySet()) {
+ long count = entry.getValue().sum();
+ if (count > 0) {
+ errorCodeCounts.put(entry.getKey(), count);
+ }
+ }
+
+ for (Map.Entry 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 context;
+
+ public ExceptionRecord(String exceptionId, ErrorCode errorCode, String message,
+ String userFriendlyMessage, LocalDateTime timestamp, Map 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 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 errorCodeCounts;
+ private final Map categoryCounts;
+ private final List recentExceptions;
+
+ public ExceptionStatistics(Map errorCodeCounts,
+ Map categoryCounts,
+ List recentExceptions) {
+ this.errorCodeCounts = new ConcurrentHashMap<>(errorCodeCounts);
+ this.categoryCounts = new ConcurrentHashMap<>(categoryCounts);
+ this.recentExceptions = new ArrayList<>(recentExceptions);
+ }
+
+ public Map getErrorCodeCounts() { return new ConcurrentHashMap<>(errorCodeCounts); }
+ public Map getCategoryCounts() { return new ConcurrentHashMap<>(categoryCounts); }
+ public List 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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/exception/FsHelperException.java b/src/main/java/cn/isliu/core/exception/FsHelperException.java
index e78886d..d7dc2df 100644
--- a/src/main/java/cn/isliu/core/exception/FsHelperException.java
+++ b/src/main/java/cn/isliu/core/exception/FsHelperException.java
@@ -1,12 +1,453 @@
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 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 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 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) {
- super(message, cause);
+ this(ErrorCode.UNKNOWN_ERROR, message, cause);
+ }
+
+ /**
+ * 获取错误代码
+ *
+ * @return 错误代码
+ */
+ public ErrorCode getErrorCode() {
+ return errorCode;
+ }
+
+ /**
+ * 获取上下文信息
+ *
+ * @return 上下文信息的副本
+ */
+ public Map 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 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 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 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 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);
+ }
}
}
diff --git a/src/main/java/cn/isliu/core/logging/FsLogger.java b/src/main/java/cn/isliu/core/logging/FsLogger.java
new file mode 100644
index 0000000..22363c9
--- /dev/null
+++ b/src/main/java/cn/isliu/core/logging/FsLogger.java
@@ -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 performanceMetrics = new ConcurrentHashMap<>();
+ private static final Map 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 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 getPerformanceMetrics() {
+ Map metrics = new ConcurrentHashMap<>();
+ performanceMetrics.forEach((key, value) -> metrics.put(key, value.get()));
+ return metrics;
+ }
+
+ /**
+ * 获取操作计数
+ *
+ * @return 操作计数映射
+ */
+ public static Map getOperationCounts() {
+ Map 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 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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java b/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java
index cb680e3..9403aed 100644
--- a/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java
+++ b/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java
@@ -11,8 +11,8 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.*;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import cn.isliu.core.logging.FsLogger;
+import cn.isliu.core.enums.ErrorCode;
import java.util.stream.Collectors;
import java.lang.reflect.InvocationTargetException;
@@ -24,7 +24,7 @@ import java.lang.reflect.InvocationTargetException;
* 支持不同字段类型的转换处理
*/
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();
/**
@@ -226,15 +226,15 @@ public class ConvertFieldUtil {
FieldValueProcess fieldValueProcess = fieldFormatClass.getDeclaredConstructor().newInstance();
result = fieldValueProcess.process(result);
} catch (InstantiationException e) {
- log.log(Level.SEVERE, "无法实例化字段格式化类: " + fieldFormatClass.getName(), e);
+ FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "无法实例化字段格式化类: " + fieldFormatClass.getName(), "convertFieldValue", e);
} catch (IllegalAccessException e) {
- log.log(Level.SEVERE, "无法访问字段格式化类的构造函数: " + fieldFormatClass.getName(), e);
+ FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "无法访问字段格式化类的构造函数: " + fieldFormatClass.getName(), "convertFieldValue", e);
} catch (NoSuchMethodException e) {
- log.log(Level.SEVERE, "字段格式化类缺少无参构造函数: " + fieldFormatClass.getName(), e);
+ FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "字段格式化类缺少无参构造函数: " + fieldFormatClass.getName(), "convertFieldValue", e);
} catch (InvocationTargetException e) {
- log.log(Level.SEVERE, "字段格式化类构造函数调用异常: " + fieldFormatClass.getName(), e);
+ FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "字段格式化类构造函数调用异常: " + fieldFormatClass.getName(), "convertFieldValue", 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();
result = fieldValueProcess.reverseProcess(result);
} catch (InstantiationException | IllegalAccessException e) {
- log.log(Level.FINE, "format value error", e);
+ FsLogger.debug("format value error: {}", e.getMessage());
}
}
}
diff --git a/src/main/java/cn/isliu/core/utils/FsApiUtil.java b/src/main/java/cn/isliu/core/utils/FsApiUtil.java
index 612cf57..b68aec6 100644
--- a/src/main/java/cn/isliu/core/utils/FsApiUtil.java
+++ b/src/main/java/cn/isliu/core/utils/FsApiUtil.java
@@ -7,6 +7,7 @@ import cn.isliu.core.SheetMeta;
import cn.isliu.core.ValuesBatch;
import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.exception.FsHelperException;
+import cn.isliu.core.logging.FsLogger;
import cn.isliu.core.pojo.ApiResponse;
import cn.isliu.core.service.*;
import com.google.gson.Gson;
@@ -25,8 +26,8 @@ import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
-import java.util.logging.Logger;
-import java.util.logging.Level;
+import cn.isliu.core.logging.FsLogger;
+import cn.isliu.core.enums.ErrorCode;
/**
@@ -37,7 +38,7 @@ import java.util.logging.Level;
public class FsApiUtil {
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";
public static final int DEFAULT_ROW_NUM = 1000;
@@ -71,11 +72,11 @@ public class FsApiUtil {
if (batchRangeResp.success()) {
valuesBatch = gson.fromJson(gson.toJson(batchRangeResp.getData()), ValuesBatch.class);
} else {
- log.log(Level.SEVERE, "【飞书表格】获取Sheet数据失败! 错误信息:{0}", gson.toJson(batchRangeResp));
+ FsLogger.error(ErrorCode.API_CALL_FAILED, "【飞书表格】获取Sheet数据失败! 错误信息:" + gson.toJson(batchRangeResp));
throw new FsHelperException("【飞书表格】获取Sheet数据失败!");
}
} 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数据失败!");
}
return valuesBatch;
@@ -115,12 +116,12 @@ public class FsApiUtil {
return sheet.get();
} 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());
}
} 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元数据异常!");
}
}
@@ -145,11 +146,11 @@ public class FsApiUtil {
ApiResponse batchMergeResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchMergeRequest);
if (!batchMergeResp.success()) {
- log.log(Level.SEVERE, "【飞书表格】 合并单元格请求异常!参数:{0},错误信息:{1}", new Object[]{cell, batchMergeResp.getMsg()});
+ FsLogger.warn("【飞书表格】 合并单元格请求异常!参数:{},错误信息:{}", cell, batchMergeResp.getMsg());
throw new FsHelperException("【飞书表格】 合并单元格请求异常!");
}
} catch (Exception e) {
- log.log(Level.SEVERE, "【飞书表格】 合并单元格异常!参数:{0},错误信息:{1}", new Object[]{cell, e.getMessage()});
+ FsLogger.warn("【飞书表格】 合并单元格异常!参数:{},错误信息:{}", cell, e.getMessage());
throw new FsHelperException("【飞书表格】 合并单元格异常!");
}
}
@@ -167,11 +168,11 @@ public class FsApiUtil {
ApiResponse apiResponse = client.customValues().valueBatchUpdate(spreadsheetToken, batchValueRequest);
if (!apiResponse.success()) {
- log.log(Level.SEVERE, "【飞书表格】 写入表格头数据异常!错误信息:{0}", apiResponse.getMsg());
+ FsLogger.warn("【飞书表格】 写入表格头数据异常!错误信息:{}", apiResponse.getMsg());
throw new FsHelperException("【飞书表格】 写入表格头数据异常!");
}
} catch (Exception e) {
- log.log(Level.SEVERE, "【飞书表格】 写入表格头异常!错误信息:{0}", e.getMessage());
+ FsLogger.warn("【飞书表格】 写入表格头异常!错误信息:{}", e.getMessage());
throw new FsHelperException("【飞书表格】 写入表格头异常!");
}
}
@@ -186,11 +187,11 @@ public class FsApiUtil {
ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest);
if (!apiResponse.success()) {
- log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!参数:{0},错误信息:{1}", new Object[]{style, apiResponse.getMsg()});
+ FsLogger.warn("【飞书表格】 写入表格样式数据异常!参数:{},错误信息:{}", style, apiResponse.getMsg());
throw new FsHelperException("【飞书表格】 写入表格样式数据异常!");
}
} catch (Exception e) {
- log.log(Level.SEVERE, "【飞书表格】 写入表格样式异常!参数:{0},错误信息:{1}", new Object[]{style, e.getMessage()});
+ FsLogger.warn("【飞书表格】 写入表格样式异常!参数:{},错误信息:{}", style, e.getMessage());
throw new FsHelperException("【飞书表格】 写入表格样式异常!");
}
}
@@ -209,7 +210,7 @@ public class FsApiUtil {
ApiResponse addResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, addSheetRequest);
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);
JsonArray replies = jsObj.getAsJsonArray("replies");
@@ -218,16 +219,16 @@ public class FsApiUtil {
Reply reply = gson.fromJson(jsonObject, Reply.class);
sheetId = reply.getAddSheet().getProperties().getSheetId();
if (sheetId == null || sheetId.isEmpty()) {
- log.log(Level.SEVERE, "【飞书表格】 创建 sheet 失败!");
+ FsLogger.warn("【飞书表格】 创建 sheet 失败!");
throw new FsHelperException("【飞书表格】创建 sheet 异常!SheetId返回为空!");
}
} else {
- log.log(Level.SEVERE, "【飞书表格】 创建 sheet 失败!错误信息:{0}", gson.toJson(addResp));
+ FsLogger.warn("【飞书表格】 创建 sheet 失败!错误信息:{}", gson.toJson(addResp));
throw new FsHelperException("【飞书表格】 创建 sheet 异常!");
}
} catch (Exception e) {
String message = e.getMessage();
- log.log(Level.SEVERE, "【飞书表格】 创建 sheet 异常!错误信息:{0}", message);
+ FsLogger.warn("【飞书表格】 创建 sheet 异常!错误信息:{}", message);
throw new FsHelperException(message != null && message.contains("403")? "请按照上方操作,当前智投无法操作对应文档哦" : "【飞书表格】 创建 sheet 异常!");
}
@@ -248,7 +249,7 @@ public class FsApiUtil {
ApiResponse copyResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, copyRequest);
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);
JsonArray replies = jsObj.getAsJsonArray("replies");
@@ -262,7 +263,7 @@ public class FsApiUtil {
}
}
} catch (Exception e) {
- log.log(Level.SEVERE, "【飞书表格】 复制模版异常!错误信息:{0}", e.getMessage());
+ FsLogger.warn("【飞书表格】 复制模版异常!错误信息:{}", e.getMessage());
throw new FsHelperException("【飞书表格】 复制模版异常!");
}
return sheetId;
@@ -287,10 +288,10 @@ public class FsApiUtil {
ApiResponse batchStyleResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchStyleRequest);
if (!batchStyleResp.success()) {
- log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!参数:{0},错误信息:{1}", new Object[]{conf, batchStyleResp.getMsg()});
+ FsLogger.warn("【飞书表格】 写入表格样式数据异常!参数:{},错误信息:{}", conf, batchStyleResp.getMsg());
}
} 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);
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) {
- 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);
if (!deleteResp.success()) {
- log.log(Level.SEVERE, "【飞书表格】 删除 sheet 失败!参数:{0},错误信息:{1}", new Object[]{sheetId, deleteResp.getMsg()});
+ FsLogger.warn("【飞书表格】 删除 sheet 失败!参数:{},错误信息:{}", sheetId, deleteResp.getMsg());
}
} 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) {
- log.log(Level.SEVERE, "【飞书表格】 下载素材异常!参数:{0},错误信息:{1}", new Object[]{fileToken, e.getMessage()});
+ FsLogger.warn("【飞书表格】 下载素材异常!参数:{},错误信息:{}", fileToken, e.getMessage());
throw new FsHelperException("【飞书表格】 下载素材异常!");
}
}
@@ -375,16 +376,16 @@ public class FsApiUtil {
if (resp.success()) {
return resp.getData().getTmpDownloadUrls()[0].getTmpDownloadUrl();
} else {
- log.log(Level.SEVERE, "【飞书表格】 获取临时下载地址失败!参数:{0},错误信息:{1}", new Object[]{fileToken, gson.toJson(resp)});
+ FsLogger.warn("【飞书表格】 获取临时下载地址失败!参数:{},错误信息:{}", fileToken, gson.toJson(resp));
}
} catch (Exception e) {
- log.log(Level.SEVERE, "【飞书表格】 获取临时下载地址异常!参数:{0},错误信息:{1}", new Object[]{fileToken, e.getMessage()});
+ FsLogger.warn("【飞书表格】 获取临时下载地址异常!参数:{},错误信息:{}", fileToken, e.getMessage());
}
return tmpUrl;
}
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()
@@ -396,11 +397,11 @@ public class FsApiUtil {
if (putResp.success()) {
return putResp.getData();
} else {
- log.log(Level.SEVERE, "【飞书表格】 写入表格数据失败!参数:{0},错误信息:{1}", new Object[]{putValuesBuilder, putResp.getMsg()});
+ FsLogger.warn("【飞书表格】 写入表格数据失败!参数:{},错误信息:{}", putValuesBuilder, putResp.getMsg());
throw new FsHelperException("【飞书表格】 写入表格数据失败!");
}
} catch (IOException e) {
- log.log(Level.SEVERE, "【飞书表格】 写入表格数据异常!参数:{0},错误信息:{1}", new Object[]{spreadsheetToken, e.getMessage()});
+ FsLogger.warn("【飞书表格】 写入表格数据异常!参数:{},错误信息:{}", spreadsheetToken, e.getMessage());
throw new FsHelperException("【飞书表格】 写入表格数据异常!");
}
}
@@ -408,7 +409,7 @@ public class FsApiUtil {
public static Object batchPutValues(String sheetId, String spreadsheetToken,
CustomValueService.ValueRequest batchPutRequest, FeishuClient client) {
- log.log(Level.INFO, "【飞书表格】 batchPutValues 开始写入数据!参数:{0}", gson.toJson(batchPutRequest));
+ FsLogger.info("【飞书表格】 batchPutValues 开始写入数据!参数:{}", gson.toJson(batchPutRequest));
try {
CustomValueService.ValueBatchUpdateRequest batchPutDataRequest =
@@ -420,11 +421,11 @@ public class FsApiUtil {
if (batchPutResp.success()) {
return batchPutResp.getData();
} else {
- log.log(Level.SEVERE, "【飞书表格】 批量写入数据失败!参数:{0},错误信息:{1}", new Object[]{sheetId, gson.toJson(batchPutResp)});
+ FsLogger.warn("【飞书表格】 批量写入数据失败!参数:{},错误信息:{}", sheetId, gson.toJson(batchPutResp));
throw new FsHelperException("【飞书表格】 批量写入数据失败!");
}
} catch (Exception e) {
- log.log(Level.SEVERE, "【飞书表格】 批量写入数据异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()});
+ FsLogger.warn("【飞书表格】 批量写入数据异常!参数:{},错误信息:{}", sheetId, e.getMessage());
throw new FsHelperException("【飞书表格】 批量写入数据异常!");
}
}
@@ -443,11 +444,11 @@ public class FsApiUtil {
if (batchResp.success()) {
return batchResp.getData();
} else {
- log.log(Level.SEVERE, "【飞书表格】 添加行列失败!参数:{0},错误信息:{1}", new Object[]{sheetId, gson.toJson(batchResp)});
+ FsLogger.warn("【飞书表格】 添加行列失败!参数:{},错误信息:{}", sheetId, gson.toJson(batchResp));
throw new FsHelperException("【飞书表格】 添加行列失败!");
}
} catch (IOException e) {
- log.log(Level.SEVERE, "【飞书表格】 添加行列异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()});
+ FsLogger.warn("【飞书表格】 添加行列异常!参数:{},错误信息:{}", sheetId, e.getMessage());
throw new FsHelperException("【飞书表格】 添加行列异常!");
}
}
@@ -466,10 +467,10 @@ public class FsApiUtil {
if (resp.success()) {
return resp.getData();
} else {
- log.log(Level.SEVERE, "【飞书表格】 获取表格信息失败!参数:{0},错误信息:{1}", new Object[]{sheetId, resp.getMsg()});
+ FsLogger.warn("【飞书表格】 获取表格信息失败!参数:{},错误信息:{}", sheetId, resp.getMsg());
}
} catch (Exception e) {
- log.log(Level.SEVERE, "【飞书表格】 获取表格信息异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()});
+ FsLogger.warn("【飞书表格】 获取表格信息异常!参数:{},错误信息:{}", sheetId, e.getMessage());
}
return null;
}
@@ -487,11 +488,11 @@ public class FsApiUtil {
ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest);
if (!apiResponse.success()) {
- log.log(Level.SEVERE, "【飞书表格】 设置单元格类型失败!参数:{0},错误信息:{1}", new Object[]{sheetId, apiResponse.getMsg()});
+ FsLogger.warn("【飞书表格】 设置单元格类型失败!参数:{},错误信息:{}", sheetId, apiResponse.getMsg());
throw new FsHelperException("【飞书表格】 批量设置单元格类型失败!");
}
} catch (Exception e) {
- log.log(Level.SEVERE, "【飞书表格】 设置单元格类型失败!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()});
+ FsLogger.warn("【飞书表格】 设置单元格类型失败!参数:{},错误信息:{}", sheetId, e.getMessage());
throw new FsHelperException("【飞书表格】 批量设置单元格类型异常!");
}
}
@@ -513,11 +514,11 @@ public class FsApiUtil {
ApiResponse imageResp = client.customValues().valueBatchUpdate(spreadsheetToken, imageWriteRequest);
if (!imageResp.success()) {
- log.log(Level.SEVERE, "【飞书表格】 图片上传失败!参数:{0},错误信息:{1}", new Object[]{filePath, gson.toJson(imageResp)});
+ FsLogger.warn("【飞书表格】 图片上传失败!参数:{},错误信息:{}", filePath, gson.toJson(imageResp));
}
return imageResp.getData();
} catch (Exception e) {
- log.log(Level.SEVERE, "【飞书表格】 图片上传异常!参数:{0},错误信息:{1}", new Object[]{filePath, e.getMessage()});
+ FsLogger.warn("【飞书表格】 图片上传异常!参数:{},错误信息:{}", filePath, e.getMessage());
}
return null;
diff --git a/src/main/java/cn/isliu/core/utils/FsClientUtil.java b/src/main/java/cn/isliu/core/utils/FsClientUtil.java
deleted file mode 100644
index 0d8fe56..0000000
--- a/src/main/java/cn/isliu/core/utils/FsClientUtil.java
+++ /dev/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;
- }
-}
diff --git a/src/main/java/cn/isliu/core/utils/FsTableUtil.java b/src/main/java/cn/isliu/core/utils/FsTableUtil.java
index 68e46a1..655e13f 100644
--- a/src/main/java/cn/isliu/core/utils/FsTableUtil.java
+++ b/src/main/java/cn/isliu/core/utils/FsTableUtil.java
@@ -2,6 +2,8 @@ package cn.isliu.core.utils;
import cn.isliu.core.*;
import cn.isliu.core.annotation.TableProperty;
+import cn.isliu.core.client.FsClient;
+
import cn.isliu.core.config.FsConfig;
import cn.isliu.core.converters.OptionsValueProcess;
import cn.isliu.core.enums.BaseEnum;
@@ -51,7 +53,7 @@ public class FsTableUtil {
// 3. 获取工作表数据
ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken,
"A" + startRowIndex,
- getColumnName(colCount - 1) + endRowIndex, FsClientUtil.getFeishuClient());
+ getColumnName(colCount - 1) + endRowIndex, FsClient.getInstance().getClient());
if (valuesBatch != null) {
List valueRanges = valuesBatch.getValueRanges();
for (ValueRange valueRange : valueRanges) {
@@ -66,12 +68,12 @@ public class FsTableUtil {
List dataList = getFsTableData(tableData);
Map 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 -> {
Map map = (Map) d.getData();
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 resultMap = new HashMap<>();
Map map = (Map) item.getData();
@@ -264,8 +266,8 @@ public class FsTableUtil {
Map resultMap = new TreeMap<>();
ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken,
- "A" + FsConfig.getTitleLine(),
- getColumnName(colCount - 1) + FsConfig.getTitleLine(), FsClientUtil.getFeishuClient());
+ "A" + FsConfig.getInstance().getTitleLine(),
+ getColumnName(colCount - 1) + FsConfig.getInstance().getTitleLine(), FsClient.getInstance().getClient());
if (valuesBatch != null) {
List valueRanges = valuesBatch.getValueRanges();
if (valueRanges != null && !valueRanges.isEmpty()) {
@@ -298,10 +300,10 @@ public class FsTableUtil {
position = FsTableUtil.getColumnNameByNuNumber(i + 1);
}
}
- int line = FsConfig.getTitleLine() + 1;
+ int line = FsConfig.getInstance().getTitleLine() + 1;
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()));
}
@@ -315,7 +317,7 @@ public class FsTableUtil {
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);
}
}
@@ -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\"]}]}";
colorTemplate = colorTemplate.replace("SHEET_ID", sheetId);
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;
}
}
\ No newline at end of file
diff --git a/src/main/java/cn/isliu/core/utils/GenerateUtil.java b/src/main/java/cn/isliu/core/utils/GenerateUtil.java
index 9995183..aa47b34 100644
--- a/src/main/java/cn/isliu/core/utils/GenerateUtil.java
+++ b/src/main/java/cn/isliu/core/utils/GenerateUtil.java
@@ -11,8 +11,8 @@ import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.*;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import cn.isliu.core.logging.FsLogger;
+import cn.isliu.core.enums.ErrorCode;
import java.util.stream.Collectors;
/**
@@ -23,7 +23,7 @@ import java.util.stream.Collectors;
*/
public class GenerateUtil {
- private static final Logger log = Logger.getLogger(GenerateUtil.class.getName());
+ // 使用统一的FsLogger替代java.util.logging.Logger
/**
* 根据配置和数据生成DTO对象(通用版本)
@@ -49,7 +49,7 @@ public class GenerateUtil {
try {
setNestedField(t, fieldPath, value);
} 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);
} catch (InstantiationException e) {
// 如果无法创建实例,则记录日志并跳过该字段
- log.log(Level.WARNING, "无法创建嵌套对象实例: " + field.getType().getName() + ", 字段: " + fieldName, e);
+ FsLogger.warn("无法创建嵌套对象实例: {} , 字段: {}", field.getType().getName(), fieldName);
return;
}
}
@@ -153,7 +153,7 @@ public class GenerateUtil {
return elementClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
// 如果无法创建实例,则记录日志并返回null
- log.log(Level.WARNING, "无法创建List元素实例: " + elementClass.getName(), e);
+ FsLogger.warn("无法创建List元素实例: {}", elementClass.getName());
return null;
}
}
@@ -354,7 +354,7 @@ public class GenerateUtil {
result.put(fieldName, value);
}
} catch (Exception e) {
- log.log(Level.WARNING, "获取字段值异常,字段路径:" + fieldPath, e);
+ FsLogger.warn("获取字段值异常,字段路径:{}", fieldPath);
}
}
return result;
diff --git a/src/main/resources/messages/errors.properties b/src/main/resources/messages/errors.properties
new file mode 100644
index 0000000..badce21
--- /dev/null
+++ b/src/main/resources/messages/errors.properties
@@ -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.
\ No newline at end of file
diff --git a/src/main/resources/messages/errors_zh_CN.properties b/src/main/resources/messages/errors_zh_CN.properties
new file mode 100644
index 0000000..d0723f0
--- /dev/null
+++ b/src/main/resources/messages/errors_zh_CN.properties
@@ -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
\ No newline at end of file