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