refactor(logging): 重构日志记录方式

- 使用自定义 FsLogger 替代 java.util.logging.Logger
- 统一日志记录格式,支持错误码和异常信息
- 移除冗余的 FsClientUtil 类
- 重构 FsConfig 类,使用单例模式
- 更新相关类的日志记录方式
This commit is contained in:
liushuang 2025-08-16 10:27:00 +08:00
parent 5b9695b7e8
commit dfd230ca04
22 changed files with 3121 additions and 211 deletions

2
.gitignore vendored

@ -1,4 +1,5 @@
target/ target/
test/
!.mvn/wrapper/maven-wrapper.jar !.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/ !**/src/main/**/target/
!**/src/test/**/target/ !**/src/test/**/target/
@ -12,6 +13,7 @@ target/
*.iml *.iml
*.ipr *.ipr
.idea .idea
.kiro
### Eclipse ### ### Eclipse ###
.apt_generated .apt_generated

92
pom.xml

@ -6,7 +6,7 @@
<groupId>cn.isliu</groupId> <groupId>cn.isliu</groupId>
<artifactId>feishu-table-helper</artifactId> <artifactId>feishu-table-helper</artifactId>
<version>0.0.1</version> <version>0.0.2</version>
<name>${project.groupId}:${project.artifactId}</name> <name>${project.groupId}:${project.artifactId}</name>
<description> <description>
@ -57,6 +57,11 @@
<artifactId>okhttp</artifactId> <artifactId>okhttp</artifactId>
<version>4.12.0</version> <version>4.12.0</version>
</dependency> </dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>4.12.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.google.code.gson</groupId> <groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId> <artifactId>gson</artifactId>
@ -105,46 +110,53 @@
</executions> </executions>
</plugin> </plugin>
<!-- gpg plugin,用于签名认证 --> <!-- &lt;!&ndash; gpg plugin,用于签名认证 &ndash;&gt;-->
<plugin> <!-- <plugin>-->
<groupId>org.apache.maven.plugins</groupId> <!-- <groupId>org.apache.maven.plugins</groupId>-->
<artifactId>maven-gpg-plugin</artifactId> <!-- <artifactId>maven-gpg-plugin</artifactId>-->
<version>1.6</version> <!-- <version>1.6</version>-->
<executions> <!-- <executions>-->
<execution> <!-- <execution>-->
<id>sign-artifacts</id> <!-- <id>sign-artifacts</id>-->
<phase>verify</phase> <!-- <phase>verify</phase>-->
<goals> <!-- <goals>-->
<goal>sign</goal> <!-- <goal>sign</goal>-->
</goals> <!-- </goals>-->
</execution> <!-- </execution>-->
</executions> <!-- </executions>-->
</plugin> <!-- </plugin>-->
<!-- 发布到私服时需要注释掉下面两个插件 --> <!-- &lt;!&ndash; 发布到私服时需要注释掉下面两个插件 &ndash;&gt;-->
<!--staging puglin,用于自动执行发布阶段(免手动)--> <!-- &lt;!&ndash;staging puglin,用于自动执行发布阶段(免手动)&ndash;&gt;-->
<plugin> <!-- <plugin>-->
<groupId>org.sonatype.central</groupId> <!-- <groupId>org.sonatype.central</groupId>-->
<artifactId>central-publishing-maven-plugin</artifactId> <!-- <artifactId>central-publishing-maven-plugin</artifactId>-->
<version>0.5.0</version> <!-- <version>0.5.0</version>-->
<extensions>true</extensions> <!-- <extensions>true</extensions>-->
<configuration> <!-- <configuration>-->
<!-- 这里的publishingServerId是在settings.xml中配置的server认证信息 --> <!-- &lt;!&ndash; 这里的publishingServerId是在settings.xml中配置的server认证信息 &ndash;&gt;-->
<publishingServerId>central</publishingServerId> <!-- <publishingServerId>central</publishingServerId>-->
<!-- 这里的autoPublish是自动发布而不是手动发布 --> <!-- &lt;!&ndash; 这里的autoPublish是自动发布而不是手动发布 &ndash;&gt;-->
<autoPublish>true</autoPublish> <!-- <autoPublish>true</autoPublish>-->
<!-- 这里的waitUntil配置为published是等待发布完成因为发布完成的时间比较长所以可以不加这个参数 --> <!-- &lt;!&ndash; 这里的waitUntil配置为published是等待发布完成因为发布完成的时间比较长所以可以不加这个参数 &ndash;&gt;-->
<waitUntil>published</waitUntil> <!-- <waitUntil>published</waitUntil>-->
<!-- 这里的deploymentName是发布到中央仓库的名称 --> <!-- &lt;!&ndash; 这里的deploymentName是发布到中央仓库的名称 &ndash;&gt;-->
<deploymentName>${project.groupId}:${project.artifactId}:${project.version}</deploymentName> <!-- <deploymentName>${project.groupId}:${project.artifactId}:${project.version}</deploymentName>-->
</configuration> <!-- </configuration>-->
</plugin> <!-- </plugin>-->
<!-- release plugin,用于发布到release仓库部署插件 --> <!-- &lt;!&ndash; release plugin,用于发布到release仓库部署插件 &ndash;&gt;-->
<plugin> <!-- <plugin>-->
<groupId>org.apache.maven.plugins</groupId> <!-- <groupId>org.apache.maven.plugins</groupId>-->
<artifactId>maven-release-plugin</artifactId> <!-- <artifactId>maven-release-plugin</artifactId>-->
<version>2.5.3</version> <!-- <version>2.5.3</version>-->
</plugin> <!-- </plugin>-->
<!-- -->
<!-- &lt;!&ndash; exec plugin for running tests &ndash;&gt;-->
<!-- <plugin>-->
<!-- <groupId>org.codehaus.mojo</groupId>-->
<!-- <artifactId>exec-maven-plugin</artifactId>-->
<!-- <version>3.1.0</version>-->
<!-- </plugin>-->
</plugins> </plugins>
</build> </build>

@ -3,6 +3,8 @@ package cn.isliu;
import cn.isliu.core.BaseEntity; import cn.isliu.core.BaseEntity;
import cn.isliu.core.FsTableData; import cn.isliu.core.FsTableData;
import cn.isliu.core.Sheet; import cn.isliu.core.Sheet;
import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.client.FsClient;
import cn.isliu.core.config.FsConfig; import cn.isliu.core.config.FsConfig;
import cn.isliu.core.pojo.FieldProperty; import cn.isliu.core.pojo.FieldProperty;
import cn.isliu.core.service.CustomValueService; import cn.isliu.core.service.CustomValueService;
@ -38,19 +40,21 @@ public class FsHelper {
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
List<String> headers = PropertyUtil.getHeaders(fieldsMap); List<String> headers = PropertyUtil.getHeaders(fieldsMap);
FeishuClient client = FsClient.getInstance().getClient();
// 1创建sheet // 1创建sheet
String sheetId = FsApiUtil.createSheet(sheetName, FsClientUtil.getFeishuClient(), spreadsheetToken); String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
// 2 添加表头数据 // 2 添加表头数据
FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers), FsClientUtil.getFeishuClient()); FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers), client);
// 3 设置表格样式 // 3 设置表格样式
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size()), sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken); FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size()), sheetId, client, spreadsheetToken);
// 4 设置单元格为文本格式 // 4 设置单元格为文本格式
if (FsConfig.CELL_TEXT) { FsConfig fsConfig = FsConfig.getInstance();
if (fsConfig.isCellText()) {
String column = FsTableUtil.getColumnNameByNuNumber(headers.size()); String column = FsTableUtil.getColumnNameByNuNumber(headers.size());
FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, FsClientUtil.getFeishuClient(), spreadsheetToken); FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
} }
// 5 设置表格下拉 // 5 设置表格下拉
@ -72,7 +76,8 @@ public class FsHelper {
*/ */
public static <T> List<T> read(String sheetId, String spreadsheetToken, Class<T> clazz) { public static <T> List<T> read(String sheetId, String spreadsheetToken, Class<T> clazz) {
List<T> results = new ArrayList<>(); List<T> results = new ArrayList<>();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken); FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken); List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
@ -112,7 +117,8 @@ public class FsHelper {
Class<?> aClass = dataList.get(0).getClass(); Class<?> aClass = dataList.get(0).getClass();
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass); Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass);
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken); FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken); List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken);
Map<String, Integer> currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow)); Map<String, Integer> currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow));
@ -131,6 +137,8 @@ public class FsHelper {
// 初始化批量插入对象 // 初始化批量插入对象
CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues(); CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
FsConfig fsConfig = FsConfig.getInstance();
AtomicInteger rowCount = new AtomicInteger(row[0] + 1); AtomicInteger rowCount = new AtomicInteger(row[0] + 1);
for (T data : dataList) { for (T data : dataList) {
@ -142,7 +150,7 @@ public class FsHelper {
if (uniqueId != null && rowNum.get() != null) { if (uniqueId != null && rowNum.get() != null) {
rowNum.set(rowNum.get() + 1); rowNum.set(rowNum.get() + 1);
values.forEach((field, fieldValue) -> { values.forEach((field, fieldValue) -> {
if (!FsConfig.isCover && fieldValue == null) { if (!fsConfig.isCover() && fieldValue == null) {
return; return;
} }
@ -153,7 +161,7 @@ public class FsHelper {
} else { } else {
int rowCou = rowCount.incrementAndGet(); int rowCou = rowCount.incrementAndGet();
values.forEach((field, fieldValue) -> { values.forEach((field, fieldValue) -> {
if (!FsConfig.isCover && fieldValue == null) { if (!fsConfig.isCover() && fieldValue == null) {
return; return;
} }
@ -168,9 +176,9 @@ public class FsHelper {
int rowTotal = sheet.getGridProperties().getRowCount(); int rowTotal = sheet.getGridProperties().getRowCount();
int rowNum = rowCount.get(); int rowNum = rowCount.get();
if (rowNum > rowTotal) { if (rowNum > rowTotal) {
FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, FsClientUtil.getFeishuClient()); FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, client);
} }
return FsApiUtil.batchPutValues(sheetId, spreadsheetToken, resultValuesBuilder.build(), FsClientUtil.getFeishuClient()); return FsApiUtil.batchPutValues(sheetId, spreadsheetToken, resultValuesBuilder.build(), client);
} }
} }

@ -0,0 +1,97 @@
package cn.isliu.core.client;
/**
* 线程安全的飞书客户端管理器
* 使用双重检查锁定单例模式确保线程安全
*/
public class FsClient {
private static volatile FsClient instance;
private volatile FeishuClient client;
private final Object lock = new Object();
// 私有构造函数防止外部实例化
private FsClient() {
}
/**
* 获取单例实例 - 使用双重检查锁定模式
* @return FeishuClientManager实例
*/
public static FsClient getInstance() {
if (instance == null) {
synchronized (FsClient.class) {
if (instance == null) {
instance = new FsClient();
}
}
}
return instance;
}
/**
* 线程安全的客户端获取
* @return FeishuClient实例
* @throws IllegalStateException 如果客户端未初始化
*/
public FeishuClient getClient() {
FeishuClient currentClient = client;
if (currentClient == null) {
throw new IllegalStateException("FeishuClient not initialized. Please call initializeClient first.");
}
return currentClient;
}
/**
* 线程安全的客户端初始化
* @param appId 飞书应用ID
* @param appSecret 飞书应用密钥
* @return 初始化的FeishuClient实例
*/
public FeishuClient initializeClient(String appId, String appSecret) {
if (appId == null || appId.trim().isEmpty()) {
throw new IllegalArgumentException("appId cannot be null or empty");
}
if (appSecret == null || appSecret.trim().isEmpty()) {
throw new IllegalArgumentException("appSecret cannot be null or empty");
}
if (client == null) {
synchronized (lock) {
if (client == null) {
client = FeishuClient.newBuilder(appId, appSecret).build();
}
}
}
return client;
}
/**
* 设置客户端实例用于外部已构建的客户端
* @param feishuClient 外部构建的FeishuClient实例
*/
public void setClient(FeishuClient feishuClient) {
if (feishuClient == null) {
throw new IllegalArgumentException("FeishuClient cannot be null");
}
synchronized (lock) {
this.client = feishuClient;
}
}
/**
* 检查客户端是否已初始化
* @return true如果客户端已初始化否则false
*/
public boolean isInitialized() {
return client != null;
}
/**
* 重置客户端主要用于测试
*/
public synchronized void resetForTesting() {
client = null;
}
}

@ -0,0 +1,384 @@
package cn.isliu.core.client;
import cn.isliu.core.logging.FsLogger;
import okhttp3.*;
import okhttp3.logging.HttpLoggingInterceptor;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* 优化的HTTP客户端工厂
* 提供连接池优化超时配置重试机制和监控功能
*/
public class OptimizedHttpClientFactory {
// 连接池配置常量
private static final int MAX_IDLE_CONNECTIONS = 10;
private static final long KEEP_ALIVE_DURATION = 5; // minutes
private static final int CONNECT_TIMEOUT = 30; // seconds
private static final int READ_TIMEOUT = 60; // seconds
private static final int WRITE_TIMEOUT = 60; // seconds
private static final int CALL_TIMEOUT = 120; // seconds
// 重试配置常量
private static final int MAX_RETRY_ATTEMPTS = 3;
private static final long INITIAL_RETRY_DELAY = 1000; // milliseconds
/**
* 创建优化的HTTP客户端
*
* @return 配置优化的OkHttpClient实例
*/
public static OkHttpClient createOptimizedClient() {
return createOptimizedClient(new ClientConfig());
}
/**
* 使用自定义配置创建优化的HTTP客户端
*
* @param config 客户端配置
* @return 配置优化的OkHttpClient实例
*/
public static OkHttpClient createOptimizedClient(ClientConfig config) {
// 创建优化的连接池
ConnectionPool connectionPool = new ConnectionPool(
config.maxIdleConnections,
config.keepAliveDuration,
TimeUnit.MINUTES
);
// 创建HTTP客户端构建器
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectionPool(connectionPool)
.connectTimeout(config.connectTimeout, TimeUnit.SECONDS)
.readTimeout(config.readTimeout, TimeUnit.SECONDS)
.writeTimeout(config.writeTimeout, TimeUnit.SECONDS)
.callTimeout(config.callTimeout, TimeUnit.SECONDS)
.retryOnConnectionFailure(true);
// 添加拦截器链
addInterceptors(builder, config);
return builder.build();
}
/**
* 添加拦截器链
*
* @param builder OkHttp客户端构建器
* @param config 客户端配置
*/
private static void addInterceptors(OkHttpClient.Builder builder, ClientConfig config) {
// 添加重试拦截器
if (config.enableRetry) {
builder.addInterceptor(new RetryInterceptor(config.maxRetryAttempts, config.initialRetryDelay));
}
// 添加监控拦截器
if (config.enableMonitoring) {
builder.addInterceptor(new MonitoringInterceptor());
}
// 添加日志拦截器
if (config.enableLogging) {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(message ->
FsLogger.debug("HTTP: " + message));
loggingInterceptor.setLevel(config.loggingLevel);
builder.addInterceptor(loggingInterceptor);
}
// 添加用户代理拦截器
builder.addInterceptor(new UserAgentInterceptor());
}
/**
* 重试拦截器
* 实现指数退避重试策略
*/
public static class RetryInterceptor implements Interceptor {
private final int maxRetryAttempts;
private final long initialRetryDelay;
public RetryInterceptor(int maxRetryAttempts, long initialRetryDelay) {
this.maxRetryAttempts = maxRetryAttempts;
this.initialRetryDelay = initialRetryDelay;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException lastException = null;
for (int attempt = 0; attempt <= maxRetryAttempts; attempt++) {
try {
if (response != null) {
response.close();
}
response = chain.proceed(request);
// 如果响应成功或不可重试直接返回
if (response.isSuccessful() || !isRetryableResponse(response)) {
return response;
}
// 如果不是最后一次尝试等待后重试
if (attempt < maxRetryAttempts) {
long delay = calculateRetryDelay(attempt);
FsLogger.warn("HTTP request failed, retrying in {}ms. Attempt: {}/{}",
delay, attempt + 1, maxRetryAttempts);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Retry interrupted", e);
}
}
} catch (IOException e) {
lastException = e;
// 如果不是最后一次尝试等待后重试
if (attempt < maxRetryAttempts && isRetryableException(e)) {
long delay = calculateRetryDelay(attempt);
FsLogger.warn("HTTP request failed with exception, retrying in {}ms. Attempt: {}/{} - {}",
delay, attempt + 1, maxRetryAttempts, e.getMessage());
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("Retry interrupted", ie);
}
} else {
throw e;
}
}
}
// 如果所有重试都失败了
if (response != null && !response.isSuccessful()) {
return response;
}
if (lastException != null) {
throw lastException;
}
throw new IOException("All retry attempts failed");
}
/**
* 计算重试延迟时间指数退避
*
* @param attempt 当前重试次数
* @return 延迟时间毫秒
*/
private long calculateRetryDelay(int attempt) {
return initialRetryDelay * (1L << attempt); // 指数退避1s, 2s, 4s, 8s...
}
/**
* 判断响应是否可重试
*
* @param response HTTP响应
* @return 是否可重试
*/
private boolean isRetryableResponse(Response response) {
int code = response.code();
// 5xx服务器错误和429限流错误可重试
return code >= 500 || code == 429;
}
/**
* 判断异常是否可重试
*
* @param exception 异常
* @return 是否可重试
*/
private boolean isRetryableException(IOException exception) {
// 连接超时读取超时等网络异常可重试
return exception instanceof java.net.SocketTimeoutException ||
exception instanceof java.net.ConnectException ||
exception instanceof java.net.UnknownHostException;
}
}
/**
* 监控拦截器
* 收集请求性能指标和连接状态
*/
public static class MonitoringInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long startTime = System.currentTimeMillis();
try {
Response response = chain.proceed(request);
long duration = System.currentTimeMillis() - startTime;
// 记录成功请求的性能指标
FsLogger.apiCall(
request.method() + " " + request.url().encodedPath(),
request.url().query(),
duration
);
return response;
} catch (IOException e) {
long duration = System.currentTimeMillis() - startTime;
// 记录失败请求
FsLogger.warn("HTTP request failed: {} {} in {}ms - {}",
request.method(), request.url().encodedPath(), duration, e.getMessage());
throw e;
}
}
}
/**
* 用户代理拦截器
* 添加统一的User-Agent头
*/
public static class UserAgentInterceptor implements Interceptor {
private static final String USER_AGENT = "FeishuTableHelper/0.0.2 (Java)";
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request requestWithUserAgent = originalRequest.newBuilder()
.header("User-Agent", USER_AGENT)
.build();
return chain.proceed(requestWithUserAgent);
}
}
/**
* HTTP客户端配置类
*/
public static class ClientConfig {
// 连接池配置
public int maxIdleConnections = MAX_IDLE_CONNECTIONS;
public long keepAliveDuration = KEEP_ALIVE_DURATION;
// 超时配置
public int connectTimeout = CONNECT_TIMEOUT;
public int readTimeout = READ_TIMEOUT;
public int writeTimeout = WRITE_TIMEOUT;
public int callTimeout = CALL_TIMEOUT;
// 重试配置
public boolean enableRetry = true;
public int maxRetryAttempts = MAX_RETRY_ATTEMPTS;
public long initialRetryDelay = INITIAL_RETRY_DELAY;
// 监控配置
public boolean enableMonitoring = true;
// 日志配置
public boolean enableLogging = false; // 默认关闭详细日志
public HttpLoggingInterceptor.Level loggingLevel = HttpLoggingInterceptor.Level.BASIC;
/**
* 创建默认配置
*
* @return 默认配置实例
*/
public static ClientConfig defaultConfig() {
return new ClientConfig();
}
/**
* 创建生产环境配置
*
* @return 生产环境配置实例
*/
public static ClientConfig productionConfig() {
ClientConfig config = new ClientConfig();
config.enableLogging = false;
config.maxRetryAttempts = 2; // 生产环境减少重试次数
return config;
}
/**
* 创建开发环境配置
*
* @return 开发环境配置实例
*/
public static ClientConfig developmentConfig() {
ClientConfig config = new ClientConfig();
config.enableLogging = true;
config.loggingLevel = HttpLoggingInterceptor.Level.BODY;
return config;
}
// 流式配置方法
public ClientConfig maxIdleConnections(int maxIdleConnections) {
this.maxIdleConnections = maxIdleConnections;
return this;
}
public ClientConfig keepAliveDuration(long keepAliveDuration) {
this.keepAliveDuration = keepAliveDuration;
return this;
}
public ClientConfig connectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}
public ClientConfig readTimeout(int readTimeout) {
this.readTimeout = readTimeout;
return this;
}
public ClientConfig writeTimeout(int writeTimeout) {
this.writeTimeout = writeTimeout;
return this;
}
public ClientConfig callTimeout(int callTimeout) {
this.callTimeout = callTimeout;
return this;
}
public ClientConfig enableRetry(boolean enableRetry) {
this.enableRetry = enableRetry;
return this;
}
public ClientConfig maxRetryAttempts(int maxRetryAttempts) {
this.maxRetryAttempts = maxRetryAttempts;
return this;
}
public ClientConfig initialRetryDelay(long initialRetryDelay) {
this.initialRetryDelay = initialRetryDelay;
return this;
}
public ClientConfig enableMonitoring(boolean enableMonitoring) {
this.enableMonitoring = enableMonitoring;
return this;
}
public ClientConfig enableLogging(boolean enableLogging) {
this.enableLogging = enableLogging;
return this;
}
public ClientConfig loggingLevel(HttpLoggingInterceptor.Level loggingLevel) {
this.loggingLevel = loggingLevel;
return this;
}
}
}

@ -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);
}
}

@ -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 +
'}';
}
}

@ -0,0 +1,13 @@
package cn.isliu.core.config;
/**
* 配置变更监听器接口
*/
public interface ConfigChangeListener {
/**
* 配置变更时的回调方法
* @param event 配置变更事件
*/
void onConfigChanged(ConfigChangeEvent event);
}

@ -0,0 +1,77 @@
package cn.isliu.core.config;
/**
* 不可变的配置快照
* 用于线程安全的配置读取
*/
public class ConfigSnapshot {
private final int headLine;
private final int titleLine;
private final boolean isCover;
private final boolean cellText;
private final String foreColor;
private final String backColor;
private final long timestamp;
/**
* 创建配置快照
* @param headLine 头部行数
* @param titleLine 标题行数
* @param isCover 是否覆盖
* @param cellText 是否单元格文本
* @param foreColor 前景色
* @param backColor 背景色
*/
public ConfigSnapshot(int headLine, int titleLine, boolean isCover,
boolean cellText, String foreColor, String backColor) {
this.headLine = headLine;
this.titleLine = titleLine;
this.isCover = isCover;
this.cellText = cellText;
this.foreColor = foreColor;
this.backColor = backColor;
this.timestamp = System.currentTimeMillis();
}
public int getHeadLine() {
return headLine;
}
public int getTitleLine() {
return titleLine;
}
public boolean isCover() {
return isCover;
}
public boolean isCellText() {
return cellText;
}
public String getForeColor() {
return foreColor;
}
public String getBackColor() {
return backColor;
}
public long getTimestamp() {
return timestamp;
}
@Override
public String toString() {
return "ConfigSnapshot{" +
"headLine=" + headLine +
", titleLine=" + titleLine +
", isCover=" + isCover +
", cellText=" + cellText +
", foreColor='" + foreColor + '\'' +
", backColor='" + backColor + '\'' +
", timestamp=" + timestamp +
'}';
}
}

@ -1,56 +1,302 @@
package cn.isliu.core.config; package cn.isliu.core.config;
import cn.isliu.core.client.FeishuClient; import java.util.concurrent.locks.ReadWriteLock;
import cn.isliu.core.utils.FsClientUtil; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 线程安全的配置管理器
* 使用volatile关键字和ReadWriteLock确保线程安全
*/
public class FsConfig { public class FsConfig {
// 使用volatile确保可见性
private volatile int headLine = 1;
private volatile int titleLine = 1;
private volatile boolean isCover = false;
private volatile boolean cellText = false;
private volatile String foreColor = "#000000";
private volatile String backColor = "#d5d5d5";
public static int headLine = 1; // 读写锁保护配置更新操作
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public static int titleLine = 1; // 配置变更监听器列表
private final List<ConfigChangeListener> listeners = new CopyOnWriteArrayList<>();
public static boolean isCover = false; // 单例实例
public static boolean CELL_TEXT = false; private static volatile FsConfig instance;
public static String FORE_COLOR = "#000000";
public static String BACK_COLOR = "#d5d5d5";
public static void initConfig(String appId, String appSecret) { private FsConfig() {
FsClientUtil.initFeishuClient(appId, appSecret);
} }
public static void initConfig(int headLine, int titleLine, String appId, String appSecret) { /**
FsConfig.headLine = headLine; * 获取单例实例
FsConfig.titleLine = titleLine; * @return ThreadSafeConfig实例
FsClientUtil.initFeishuClient(appId, appSecret); */
public static FsConfig getInstance() {
if (instance == null) {
synchronized (FsConfig.class) {
if (instance == null) {
instance = new FsConfig();
}
}
}
return instance;
} }
public static void initConfig(int headLine, FeishuClient client) { /**
FsConfig.headLine = headLine; * 原子性配置更新
FsClientUtil.client = client; * @param builder 配置构建器
*/
public void updateConfig(ConfigBuilder builder) {
// 验证配置
validateConfig(builder);
ConfigSnapshot oldSnapshot = getSnapshot();
lock.writeLock().lock();
try {
if (builder.headLine != null) {
this.headLine = builder.headLine;
}
if (builder.titleLine != null) {
this.titleLine = builder.titleLine;
}
if (builder.isCover != null) {
this.isCover = builder.isCover;
}
if (builder.cellText != null) {
this.cellText = builder.cellText;
}
if (builder.foreColor != null) {
this.foreColor = builder.foreColor;
}
if (builder.backColor != null) {
this.backColor = builder.backColor;
}
} finally {
lock.writeLock().unlock();
} }
public static void initConfig(FeishuClient client) { // 通知配置变更
FsClientUtil.client = client; ConfigSnapshot newSnapshot = getSnapshot();
notifyConfigChange(oldSnapshot, newSnapshot);
} }
public static int getHeadLine() { /**
* 验证配置参数
* @param builder 配置构建器
* @throws IllegalArgumentException 如果配置无效
*/
private void validateConfig(ConfigBuilder builder) {
if (builder.headLine != null && builder.headLine < 0) {
throw new IllegalArgumentException("headLine must be non-negative, got: " + builder.headLine);
}
if (builder.titleLine != null && builder.titleLine < 0) {
throw new IllegalArgumentException("titleLine must be non-negative, got: " + builder.titleLine);
}
if (builder.foreColor != null && !isValidColor(builder.foreColor)) {
throw new IllegalArgumentException("Invalid foreColor format: " + builder.foreColor);
}
if (builder.backColor != null && !isValidColor(builder.backColor)) {
throw new IllegalArgumentException("Invalid backColor format: " + builder.backColor);
}
}
/**
* 验证颜色格式
* @param color 颜色值
* @return 是否为有效的颜色格式
*/
private boolean isValidColor(String color) {
if (color == null || color.trim().isEmpty()) {
return false;
}
// 简单的十六进制颜色验证 #RRGGBB
return color.matches("^#[0-9A-Fa-f]{6}$");
}
/**
* 通知配置变更
* @param oldSnapshot 旧配置快照
* @param newSnapshot 新配置快照
*/
private void notifyConfigChange(ConfigSnapshot oldSnapshot, ConfigSnapshot newSnapshot) {
if (!listeners.isEmpty()) {
ConfigChangeEvent event = new ConfigChangeEvent(oldSnapshot, newSnapshot);
for (ConfigChangeListener listener : listeners) {
try {
listener.onConfigChanged(event);
} catch (Exception e) {
// 记录异常但不影响配置更新
System.err.println("Error notifying config change listener: " + e.getMessage());
}
}
}
}
/**
* 添加配置变更监听器
* @param listener 监听器
*/
public void addConfigChangeListener(ConfigChangeListener listener) {
if (listener != null) {
listeners.add(listener);
}
}
/**
* 移除配置变更监听器
* @param listener 监听器
*/
public void removeConfigChangeListener(ConfigChangeListener listener) {
listeners.remove(listener);
}
/**
* 线程安全的配置读取 - 获取配置快照
* @return 不可变的配置快照
*/
public ConfigSnapshot getSnapshot() {
lock.readLock().lock();
try {
return new ConfigSnapshot(headLine, titleLine, isCover, cellText, foreColor, backColor);
} finally {
lock.readLock().unlock();
}
}
// 单独的getter方法使用volatile保证可见性
public int getHeadLine() {
return headLine; return headLine;
} }
public static int getTitleLine() { public int getTitleLine() {
return titleLine; return titleLine;
} }
public static FeishuClient getFeishuClient() { public boolean isCover() {
return FsClientUtil.client; return isCover;
} }
public static void setHeadLine(int headLine) { public boolean isCellText() {
FsConfig.headLine = headLine; return cellText;
} }
public static void setTitleLine(int titleLine) { public String getForeColor() {
FsConfig.titleLine = titleLine; return foreColor;
}
public String getBackColor() {
return backColor;
}
// 单独的setter方法使用写锁保护
public void setHeadLine(int headLine) {
if (headLine < 0) {
throw new IllegalArgumentException("headLine must be non-negative, got: " + headLine);
}
ConfigSnapshot oldSnapshot = getSnapshot();
lock.writeLock().lock();
try {
this.headLine = headLine;
} finally {
lock.writeLock().unlock();
}
ConfigSnapshot newSnapshot = getSnapshot();
notifyConfigChange(oldSnapshot, newSnapshot);
}
public void setTitleLine(int titleLine) {
if (titleLine < 0) {
throw new IllegalArgumentException("titleLine must be non-negative, got: " + titleLine);
}
ConfigSnapshot oldSnapshot = getSnapshot();
lock.writeLock().lock();
try {
this.titleLine = titleLine;
} finally {
lock.writeLock().unlock();
}
ConfigSnapshot newSnapshot = getSnapshot();
notifyConfigChange(oldSnapshot, newSnapshot);
}
public void setIsCover(boolean isCover) {
ConfigSnapshot oldSnapshot = getSnapshot();
lock.writeLock().lock();
try {
this.isCover = isCover;
} finally {
lock.writeLock().unlock();
}
ConfigSnapshot newSnapshot = getSnapshot();
notifyConfigChange(oldSnapshot, newSnapshot);
}
public void setCellText(boolean cellText) {
ConfigSnapshot oldSnapshot = getSnapshot();
lock.writeLock().lock();
try {
this.cellText = cellText;
} finally {
lock.writeLock().unlock();
}
ConfigSnapshot newSnapshot = getSnapshot();
notifyConfigChange(oldSnapshot, newSnapshot);
}
public void setForeColor(String foreColor) {
if (foreColor == null) {
throw new IllegalArgumentException("foreColor cannot be null");
}
if (!isValidColor(foreColor)) {
throw new IllegalArgumentException("Invalid foreColor format: " + foreColor);
}
ConfigSnapshot oldSnapshot = getSnapshot();
lock.writeLock().lock();
try {
this.foreColor = foreColor;
} finally {
lock.writeLock().unlock();
}
ConfigSnapshot newSnapshot = getSnapshot();
notifyConfigChange(oldSnapshot, newSnapshot);
}
public void setBackColor(String backColor) {
if (backColor == null) {
throw new IllegalArgumentException("backColor cannot be null");
}
if (!isValidColor(backColor)) {
throw new IllegalArgumentException("Invalid backColor format: " + backColor);
}
ConfigSnapshot oldSnapshot = getSnapshot();
lock.writeLock().lock();
try {
this.backColor = backColor;
} finally {
lock.writeLock().unlock();
}
ConfigSnapshot newSnapshot = getSnapshot();
notifyConfigChange(oldSnapshot, newSnapshot);
} }
} }

@ -1,7 +1,8 @@
package cn.isliu.core.converters; package cn.isliu.core.converters;
import cn.isliu.core.client.FsClient;
import cn.isliu.core.config.FsConfig;
import cn.isliu.core.utils.FsApiUtil; import cn.isliu.core.utils.FsApiUtil;
import cn.isliu.core.utils.FsClientUtil;
import cn.isliu.core.utils.FileUtil; import cn.isliu.core.utils.FileUtil;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
@ -10,12 +11,11 @@ import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level; import cn.isliu.core.logging.FsLogger;
import java.util.logging.Logger;
public class FileUrlProcess implements FieldValueProcess<String> { public class FileUrlProcess implements FieldValueProcess<String> {
private static final Logger log = Logger.getLogger(FileUrlProcess.class.getName()); // 使用统一的FsLogger替代java.util.logging.Logger
@Override @Override
public String process(Object value) { public String process(Object value) {
@ -44,7 +44,11 @@ public class FileUrlProcess implements FieldValueProcess<String> {
@Override @Override
public String reverseProcess(Object value) { public String reverseProcess(Object value) {
// 简单实现可以根据需要进行更复杂的反向处理 boolean cover = FsConfig.getInstance().isCover();
if (!cover && value != null) {
String str = value.toString();
byte[] imageData = FileUtil.getImageData(str);
}
if (value == null) { if (value == null) {
return null; return null;
} }
@ -83,20 +87,20 @@ public class FileUrlProcess implements FieldValueProcess<String> {
boolean isSuccess = true; boolean isSuccess = true;
try { try {
FsApiUtil.downloadMaterial(fileToken, filePath , FsClientUtil.getFeishuClient(), null); FsApiUtil.downloadMaterial(fileToken, filePath , FsClient.getInstance().getClient(), null);
url = filePath; url = filePath;
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING,"【飞书表格】 根据文件FileToken下载失败fileToken: {0}, e: {1}", new Object[]{fileToken, e.getMessage()}); FsLogger.warn("【飞书表格】 根据文件FileToken下载失败fileToken: {}, e: {}", fileToken, e.getMessage());
isSuccess = false; isSuccess = false;
} }
if (!isSuccess) { if (!isSuccess) {
String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(fileToken, FsClientUtil.getFeishuClient()); String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(fileToken, FsClient.getInstance().getClient());
// 根据临时下载地址下载 // 根据临时下载地址下载
FileUtil.downloadFile(tmpUrl, filePath); FileUtil.downloadFile(tmpUrl, filePath);
} }
log.info("【飞书表格】 文件上传-飞书图片上传成功fileToken: " + fileToken + ", filePath: " + filePath); FsLogger.info("【飞书表格】 文件上传-飞书图片上传成功fileToken: {}, filePath: {}", fileToken, filePath);
return url; return url;
} }
@ -110,19 +114,19 @@ public class FileUrlProcess implements FieldValueProcess<String> {
boolean isSuccess = true; boolean isSuccess = true;
try { try {
FsApiUtil.downloadMaterial(token, path , FsClientUtil.getFeishuClient(), null); FsApiUtil.downloadMaterial(token, path , FsClient.getInstance().getClient(), null);
url = path; url = path;
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING, "【飞书表格】 附件-根据文件FileToken下载失败fileToken: {0}, e: {1}", new Object[]{token, e.getMessage()}); FsLogger.warn("【飞书表格】 附件-根据文件FileToken下载失败fileToken: {}, e: {}", token, e.getMessage());
isSuccess = false; isSuccess = false;
} }
if (!isSuccess) { if (!isSuccess) {
String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(token, FsClientUtil.getFeishuClient()); String tmpUrl = FsApiUtil.downloadTmpMaterialUrl(token, FsClient.getInstance().getClient());
FileUtil.downloadFile(tmpUrl, path); FileUtil.downloadFile(tmpUrl, path);
} }
log.info("【飞书表格】 文件上传-附件上传成功fileToken: " + token + ", filePath: " + path); FsLogger.info("【飞书表格】 文件上传-附件上传成功fileToken: {}, filePath: {}", token, path);
return url; return url;
} }
} }

@ -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;
}
}
}

@ -0,0 +1,627 @@
package cn.isliu.core.exception;
import cn.isliu.core.enums.ErrorCode;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 统一异常处理器
*
* 提供全局异常处理异常分类转换统计监控和恢复建议功能
*
* @author isliu
*/
public class ExceptionHandler {
private static volatile ExceptionHandler instance;
private static final Object lock = new Object();
/** 异常统计计数器 */
private final Map<ErrorCode, LongAdder> exceptionCounters = new ConcurrentHashMap<>();
/** 异常分类统计 */
private final Map<ErrorCode.ErrorCategory, LongAdder> categoryCounters = new ConcurrentHashMap<>();
/** 最近异常记录 */
private final List<ExceptionRecord> recentExceptions = Collections.synchronizedList(new ArrayList<>());
/** 最大记录数量 */
private static final int MAX_RECENT_EXCEPTIONS = 100;
/** 异常处理监听器 */
private final List<ExceptionListener> listeners = Collections.synchronizedList(new ArrayList<>());
/**
* 私有构造函数
*/
private ExceptionHandler() {
// 初始化所有错误代码的计数器
for (ErrorCode errorCode : ErrorCode.values()) {
exceptionCounters.put(errorCode, new LongAdder());
}
// 初始化所有分类的计数器
for (ErrorCode.ErrorCategory category : ErrorCode.ErrorCategory.values()) {
categoryCounters.put(category, new LongAdder());
}
}
/**
* 获取单例实例
*
* @return 异常处理器实例
*/
public static ExceptionHandler getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new ExceptionHandler();
}
}
}
return instance;
}
/**
* 处理异常
*
* @param throwable 原始异常
* @return 处理后的FsHelperException
*/
public FsHelperException handleException(Throwable throwable) {
if (throwable == null) {
return new FsHelperException(ErrorCode.UNKNOWN_ERROR, "Null exception occurred");
}
FsHelperException fsException;
// 如果已经是FsHelperException直接使用
if (throwable instanceof FsHelperException) {
fsException = (FsHelperException) throwable;
} else {
// 转换为FsHelperException
fsException = convertToFsHelperException(throwable);
}
// 记录异常统计
recordException(fsException);
// 通知监听器
notifyListeners(fsException);
return fsException;
}
/**
* 处理异常并提供上下文
*
* @param throwable 原始异常
* @param context 上下文信息
* @return 处理后的FsHelperException
*/
public FsHelperException handleException(Throwable throwable, Map<String, Object> context) {
FsHelperException fsException = handleException(throwable);
if (context != null && !context.isEmpty()) {
fsException.addContext(context);
}
return fsException;
}
/**
* 处理异常并提供操作上下文
*
* @param throwable 原始异常
* @param operation 操作名称
* @param additionalInfo 附加信息
* @return 处理后的FsHelperException
*/
public FsHelperException handleException(Throwable throwable, String operation, String additionalInfo) {
FsHelperException fsException = handleException(throwable);
fsException.addContext("operation", operation);
if (additionalInfo != null) {
fsException.addContext("additionalInfo", additionalInfo);
}
return fsException;
}
/**
* 将普通异常转换为FsHelperException
*
* @param throwable 原始异常
* @return FsHelperException
*/
private FsHelperException convertToFsHelperException(Throwable throwable) {
ErrorCode errorCode = classifyException(throwable);
String message = throwable.getMessage() != null ? throwable.getMessage() : throwable.getClass().getSimpleName();
return FsHelperException.builder(errorCode)
.message(message)
.context("originalExceptionType", throwable.getClass().getSimpleName())
.context("originalMessage", throwable.getMessage())
.cause(throwable)
.build();
}
/**
* 异常分类逻辑
*
* @param throwable 异常
* @return 对应的错误代码
*/
private ErrorCode classifyException(Throwable throwable) {
if (throwable == null) {
return ErrorCode.UNKNOWN_ERROR;
}
String className = throwable.getClass().getSimpleName().toLowerCase();
String message = throwable.getMessage() != null ? throwable.getMessage().toLowerCase() : "";
// 网络和连接相关异常
if (className.contains("connect") || className.contains("socket") || className.contains("timeout")) {
if (message.contains("timeout")) {
return ErrorCode.CLIENT_TIMEOUT;
}
return ErrorCode.CLIENT_CONNECTION_FAILED;
}
// 认证和授权异常
if (className.contains("auth") || className.contains("credential") || className.contains("permission") ||
message.contains("auth") || message.contains("credential") || message.contains("permission")) {
if (message.contains("expired") || message.contains("token")) {
return ErrorCode.TOKEN_EXPIRED;
}
if (message.contains("unauthorized") || message.contains("401")) {
return ErrorCode.API_UNAUTHORIZED;
}
if (message.contains("forbidden") || message.contains("403")) {
return ErrorCode.API_FORBIDDEN;
}
return ErrorCode.INVALID_CREDENTIALS;
}
// HTTP相关异常
if (className.contains("http") || message.contains("http")) {
if (message.contains("404") || message.contains("not found")) {
return ErrorCode.API_NOT_FOUND;
}
if (message.contains("429") || message.contains("rate limit")) {
return ErrorCode.API_RATE_LIMIT_EXCEEDED;
}
if (message.contains("500") || message.contains("502") || message.contains("503")) {
return ErrorCode.API_SERVER_ERROR;
}
if (message.contains("400") || message.contains("bad request")) {
return ErrorCode.API_INVALID_REQUEST;
}
return ErrorCode.API_CALL_FAILED;
}
// 并发相关异常 - 需要优先检查因为ConcurrentModificationException包含"modification"
if (className.equals("concurrentmodificationexception")) {
return ErrorCode.CONCURRENT_MODIFICATION;
}
if (className.contains("concurrent") || className.contains("thread") || className.contains("lock")) {
return ErrorCode.THREAD_SAFETY_VIOLATION;
}
// 数据相关异常
if (className.contains("parse") || className.contains("json") || className.contains("xml")) {
return ErrorCode.API_RESPONSE_PARSE_ERROR;
}
if (className.contains("validation") || className.contains("illegal") || className.contains("invalid")) {
return ErrorCode.DATA_VALIDATION_FAILED;
}
if (className.contains("format") || className.contains("number") || className.contains("date")) {
return ErrorCode.DATA_FORMAT_ERROR;
}
// 资源相关异常
if (className.contains("memory") || message.contains("out of memory")) {
return ErrorCode.MEMORY_INSUFFICIENT;
}
if (className.contains("file") || className.equals("ioexception") || className.startsWith("io")) {
if (message.contains("not found") || message.contains("no such file")) {
return ErrorCode.FILE_NOT_FOUND;
}
if (message.contains("access denied") || message.contains("permission")) {
return ErrorCode.FILE_ACCESS_DENIED;
}
return ErrorCode.RESOURCE_EXHAUSTED;
}
// 配置相关异常
if (className.contains("config") || className.contains("property") || className.contains("setting")) {
return ErrorCode.CONFIGURATION_ERROR;
}
// 业务逻辑异常
if (className.contains("state") || className.contains("operation")) {
return ErrorCode.INVALID_OPERATION;
}
// 系统异常 - 更精确的匹配
if (className.equals("runtimeexception") || className.contains("system")) {
return ErrorCode.SYSTEM_ERROR;
}
// 默认未知错误
return ErrorCode.UNKNOWN_ERROR;
}
/**
* 记录异常统计
*
* @param exception 异常
*/
private void recordException(FsHelperException exception) {
ErrorCode errorCode = exception.getErrorCode();
ErrorCode.ErrorCategory category = errorCode.getCategory();
// 增加计数
exceptionCounters.get(errorCode).increment();
categoryCounters.get(category).increment();
// 记录最近异常
ExceptionRecord record = new ExceptionRecord(
exception.getExceptionId(),
errorCode,
exception.getMessage(),
exception.getUserFriendlyMessage(),
LocalDateTime.now(),
exception.getContext()
);
synchronized (recentExceptions) {
recentExceptions.add(record);
// 保持最大记录数量
if (recentExceptions.size() > MAX_RECENT_EXCEPTIONS) {
recentExceptions.remove(0);
}
}
}
/**
* 通知异常监听器
*
* @param exception 异常
*/
private void notifyListeners(FsHelperException exception) {
for (ExceptionListener listener : listeners) {
try {
listener.onException(exception);
} catch (Exception e) {
// 监听器异常不应影响主流程只记录日志
System.err.println("Exception listener failed: " + e.getMessage());
}
}
}
/**
* 获取异常统计信息
*
* @return 异常统计信息
*/
public ExceptionStatistics getStatistics() {
Map<ErrorCode, Long> errorCodeCounts = new ConcurrentHashMap<>();
Map<ErrorCode.ErrorCategory, Long> categoryCounts = new ConcurrentHashMap<>();
for (Map.Entry<ErrorCode, LongAdder> entry : exceptionCounters.entrySet()) {
long count = entry.getValue().sum();
if (count > 0) {
errorCodeCounts.put(entry.getKey(), count);
}
}
for (Map.Entry<ErrorCode.ErrorCategory, LongAdder> entry : categoryCounters.entrySet()) {
long count = entry.getValue().sum();
if (count > 0) {
categoryCounts.put(entry.getKey(), count);
}
}
return new ExceptionStatistics(errorCodeCounts, categoryCounts, new ArrayList<>(recentExceptions));
}
/**
* 获取恢复建议
*
* @param exception 异常
* @return 恢复建议
*/
public RecoveryAdvice getRecoveryAdvice(FsHelperException exception) {
ErrorCode errorCode = exception.getErrorCode();
RecoveryAdvice.Builder builder = RecoveryAdvice.builder()
.errorCode(errorCode)
.isRetryable(errorCode.isRetryable())
.isFatal(errorCode.isFatal());
// 根据错误类型提供具体建议
switch (errorCode.getCategory()) {
case CLIENT:
return builder
.immediateAction("检查客户端配置和初始化参数")
.longTermAction("确保客户端正确初始化并配置有效的认证信息")
.preventiveAction("在使用客户端前进行初始化检查")
.build();
case API:
if (errorCode.isRetryable()) {
return builder
.immediateAction("等待一段时间后重试")
.longTermAction("实现指数退避重试策略")
.preventiveAction("监控API调用频率避免超过限制")
.build();
} else {
return builder
.immediateAction("检查API请求参数和格式")
.longTermAction("验证API权限和认证信息")
.preventiveAction("在发送请求前验证参数完整性")
.build();
}
case CONFIGURATION:
return builder
.immediateAction("检查配置文件的格式和内容")
.longTermAction("建立配置验证机制")
.preventiveAction("使用配置模板和验证规则")
.build();
case SECURITY:
return builder
.immediateAction("检查认证凭据是否有效")
.longTermAction("实现凭据自动刷新机制")
.preventiveAction("定期更新和验证安全凭据")
.build();
case RESOURCE:
return builder
.immediateAction("释放不必要的资源")
.longTermAction("优化资源使用策略")
.preventiveAction("实现资源监控和预警机制")
.build();
case DATA:
return builder
.immediateAction("验证输入数据的格式和内容")
.longTermAction("加强数据验证和清理机制")
.preventiveAction("在处理前进行数据格式检查")
.build();
case CONCURRENCY:
return builder
.immediateAction("检查并发访问的同步机制")
.longTermAction("重新设计线程安全的数据结构")
.preventiveAction("使用线程安全的组件和模式")
.build();
case BUSINESS:
return builder
.immediateAction("检查业务规则和前置条件")
.longTermAction("完善业务逻辑验证")
.preventiveAction("在操作前验证业务规则")
.build();
case SYSTEM:
return builder
.immediateAction("检查系统状态和资源可用性")
.longTermAction("实现系统监控和告警")
.preventiveAction("定期进行系统健康检查")
.build();
default:
return builder
.immediateAction("查看详细错误信息和日志")
.longTermAction("联系技术支持获取帮助")
.preventiveAction("加强错误处理和日志记录")
.build();
}
}
/**
* 添加异常监听器
*
* @param listener 监听器
*/
public void addListener(ExceptionListener listener) {
if (listener != null) {
listeners.add(listener);
}
}
/**
* 移除异常监听器
*
* @param listener 监听器
*/
public void removeListener(ExceptionListener listener) {
listeners.remove(listener);
}
/**
* 清除统计信息
*/
public void clearStatistics() {
exceptionCounters.values().forEach(LongAdder::reset);
categoryCounters.values().forEach(LongAdder::reset);
recentExceptions.clear();
}
/**
* 异常记录
*/
public static class ExceptionRecord {
private final String exceptionId;
private final ErrorCode errorCode;
private final String message;
private final String userFriendlyMessage;
private final LocalDateTime timestamp;
private final Map<String, Object> context;
public ExceptionRecord(String exceptionId, ErrorCode errorCode, String message,
String userFriendlyMessage, LocalDateTime timestamp, Map<String, Object> context) {
this.exceptionId = exceptionId;
this.errorCode = errorCode;
this.message = message;
this.userFriendlyMessage = userFriendlyMessage;
this.timestamp = timestamp;
this.context = new ConcurrentHashMap<>(context != null ? context : new ConcurrentHashMap<>());
}
public String getExceptionId() { return exceptionId; }
public ErrorCode getErrorCode() { return errorCode; }
public String getMessage() { return message; }
public String getUserFriendlyMessage() { return userFriendlyMessage; }
public LocalDateTime getTimestamp() { return timestamp; }
public Map<String, Object> getContext() { return new ConcurrentHashMap<>(context); }
@Override
public String toString() {
return String.format("[%s] %s - %s (%s)",
timestamp.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
errorCode.getCode(),
message,
exceptionId);
}
}
/**
* 异常统计信息
*/
public static class ExceptionStatistics {
private final Map<ErrorCode, Long> errorCodeCounts;
private final Map<ErrorCode.ErrorCategory, Long> categoryCounts;
private final List<ExceptionRecord> recentExceptions;
public ExceptionStatistics(Map<ErrorCode, Long> errorCodeCounts,
Map<ErrorCode.ErrorCategory, Long> categoryCounts,
List<ExceptionRecord> recentExceptions) {
this.errorCodeCounts = new ConcurrentHashMap<>(errorCodeCounts);
this.categoryCounts = new ConcurrentHashMap<>(categoryCounts);
this.recentExceptions = new ArrayList<>(recentExceptions);
}
public Map<ErrorCode, Long> getErrorCodeCounts() { return new ConcurrentHashMap<>(errorCodeCounts); }
public Map<ErrorCode.ErrorCategory, Long> getCategoryCounts() { return new ConcurrentHashMap<>(categoryCounts); }
public List<ExceptionRecord> getRecentExceptions() { return new ArrayList<>(recentExceptions); }
public long getTotalExceptions() {
return errorCodeCounts.values().stream().mapToLong(Long::longValue).sum();
}
public ErrorCode getMostFrequentError() {
return errorCodeCounts.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
}
public ErrorCode.ErrorCategory getMostFrequentCategory() {
return categoryCounts.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
}
}
/**
* 恢复建议
*/
public static class RecoveryAdvice {
private final ErrorCode errorCode;
private final boolean isRetryable;
private final boolean isFatal;
private final String immediateAction;
private final String longTermAction;
private final String preventiveAction;
private RecoveryAdvice(Builder builder) {
this.errorCode = builder.errorCode;
this.isRetryable = builder.isRetryable;
this.isFatal = builder.isFatal;
this.immediateAction = builder.immediateAction;
this.longTermAction = builder.longTermAction;
this.preventiveAction = builder.preventiveAction;
}
public ErrorCode getErrorCode() { return errorCode; }
public boolean isRetryable() { return isRetryable; }
public boolean isFatal() { return isFatal; }
public String getImmediateAction() { return immediateAction; }
public String getLongTermAction() { return longTermAction; }
public String getPreventiveAction() { return preventiveAction; }
public static Builder builder() {
return new Builder();
}
public static class Builder {
private ErrorCode errorCode;
private boolean isRetryable;
private boolean isFatal;
private String immediateAction;
private String longTermAction;
private String preventiveAction;
public Builder errorCode(ErrorCode errorCode) {
this.errorCode = errorCode;
return this;
}
public Builder isRetryable(boolean isRetryable) {
this.isRetryable = isRetryable;
return this;
}
public Builder isFatal(boolean isFatal) {
this.isFatal = isFatal;
return this;
}
public Builder immediateAction(String immediateAction) {
this.immediateAction = immediateAction;
return this;
}
public Builder longTermAction(String longTermAction) {
this.longTermAction = longTermAction;
return this;
}
public Builder preventiveAction(String preventiveAction) {
this.preventiveAction = preventiveAction;
return this;
}
public RecoveryAdvice build() {
return new RecoveryAdvice(this);
}
}
}
/**
* 异常监听器接口
*/
public interface ExceptionListener {
/**
* 异常发生时的回调
*
* @param exception 异常
*/
void onException(FsHelperException exception);
}
}

@ -1,12 +1,453 @@
package cn.isliu.core.exception; package cn.isliu.core.exception;
public class FsHelperException extends RuntimeException { import cn.isliu.core.enums.ErrorCode;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public FsHelperException(String message) { /**
super(message); * 飞书助手异常类
*
* 增强的异常类支持错误代码上下文信息异常链分析和序列化
*
* @author isliu
*/
public class FsHelperException extends RuntimeException implements Serializable {
private static final long serialVersionUID = 1L;
/** 错误代码 */
private final ErrorCode errorCode;
/** 上下文信息 */
private final Map<String, Object> context;
/** 异常唯一标识 */
private final String exceptionId;
/** 异常发生时间 */
private final LocalDateTime timestamp;
/** 用户友好的错误消息 */
private final String userFriendlyMessage;
/**
* 构造函数 - 仅包含错误代码
*
* @param errorCode 错误代码
*/
public FsHelperException(ErrorCode errorCode) {
this(errorCode, errorCode.getDefaultMessage(), null, null);
} }
/**
* 构造函数 - 包含错误代码和自定义消息
*
* @param errorCode 错误代码
* @param message 自定义错误消息
*/
public FsHelperException(ErrorCode errorCode, String message) {
this(errorCode, message, null, null);
}
/**
* 构造函数 - 包含错误代码消息和上下文
*
* @param errorCode 错误代码
* @param message 错误消息
* @param context 上下文信息
*/
public FsHelperException(ErrorCode errorCode, String message, Map<String, Object> context) {
this(errorCode, message, context, null);
}
/**
* 构造函数 - 包含错误代码消息和原因
*
* @param errorCode 错误代码
* @param message 错误消息
* @param cause 原因异常
*/
public FsHelperException(ErrorCode errorCode, String message, Throwable cause) {
this(errorCode, message, null, cause);
}
/**
* 完整构造函数
*
* @param errorCode 错误代码
* @param message 错误消息
* @param context 上下文信息
* @param cause 原因异常
*/
public FsHelperException(ErrorCode errorCode, String message, Map<String, Object> context, Throwable cause) {
super(buildDetailedMessage(errorCode, message, context), cause);
this.errorCode = errorCode != null ? errorCode : ErrorCode.UNKNOWN_ERROR;
this.context = context != null ? new HashMap<>(context) : new HashMap<>();
this.exceptionId = UUID.randomUUID().toString();
this.timestamp = LocalDateTime.now();
this.userFriendlyMessage = generateUserFriendlyMessage(this.errorCode, message);
// 添加基本上下文信息
this.context.put("exceptionId", this.exceptionId);
this.context.put("timestamp", this.timestamp.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
this.context.put("errorCode", this.errorCode.getCode());
this.context.put("errorCategory", this.errorCode.getCategory().getName());
}
/**
* 兼容性构造函数 - 保持向后兼容
*
* @param message 错误消息
*/
public FsHelperException(String message) {
this(ErrorCode.UNKNOWN_ERROR, message);
}
/**
* 兼容性构造函数 - 保持向后兼容
*
* @param message 错误消息
* @param cause 原因异常
*/
public FsHelperException(String message, Throwable cause) { public FsHelperException(String message, Throwable cause) {
super(message, cause); this(ErrorCode.UNKNOWN_ERROR, message, cause);
}
/**
* 获取错误代码
*
* @return 错误代码
*/
public ErrorCode getErrorCode() {
return errorCode;
}
/**
* 获取上下文信息
*
* @return 上下文信息的副本
*/
public Map<String, Object> getContext() {
return new HashMap<>(context);
}
/**
* 获取异常唯一标识
*
* @return 异常唯一标识
*/
public String getExceptionId() {
return exceptionId;
}
/**
* 获取异常发生时间
*
* @return 异常发生时间
*/
public LocalDateTime getTimestamp() {
return timestamp;
}
/**
* 获取用户友好的错误消息
*
* @return 用户友好的错误消息
*/
public String getUserFriendlyMessage() {
return userFriendlyMessage;
}
/**
* 添加上下文信息
*
* @param key
* @param value
* @return 当前异常实例支持链式调用
*/
public FsHelperException addContext(String key, Object value) {
if (key != null && value != null) {
this.context.put(key, value);
}
return this;
}
/**
* 添加多个上下文信息
*
* @param contextMap 上下文信息映射
* @return 当前异常实例支持链式调用
*/
public FsHelperException addContext(Map<String, Object> contextMap) {
if (contextMap != null) {
this.context.putAll(contextMap);
}
return this;
}
/**
* 获取指定键的上下文值
*
* @param key
* @return 上下文值如果不存在返回null
*/
public Object getContextValue(String key) {
return context.get(key);
}
/**
* 检查是否包含指定的上下文键
*
* @param key
* @return 如果包含返回true
*/
public boolean hasContextKey(String key) {
return context.containsKey(key);
}
/**
* 获取根因异常
*
* @return 根因异常如果没有返回当前异常
*/
public Throwable getRootCause() {
Throwable rootCause = this;
while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
rootCause = rootCause.getCause();
}
return rootCause;
}
/**
* 获取异常链信息
*
* @return 异常链描述
*/
public String getExceptionChain() {
StringBuilder chain = new StringBuilder();
Throwable current = this;
int level = 0;
while (current != null && level < 10) { // 防止无限循环
if (level > 0) {
chain.append("\n");
for (int i = 0; i < level; i++) {
chain.append(" ");
}
chain.append("Caused by: ");
}
chain.append(current.getClass().getSimpleName())
.append(": ")
.append(current.getMessage());
current = current.getCause();
level++;
}
return chain.toString();
}
/**
* 检查是否为可重试的异常
*
* @return 如果可重试返回true
*/
public boolean isRetryable() {
return errorCode.isRetryable();
}
/**
* 检查是否为致命异常
*
* @return 如果是致命异常返回true
*/
public boolean isFatal() {
return errorCode.isFatal();
}
/**
* 检查是否为客户端异常
*
* @return 如果是客户端异常返回true
*/
public boolean isClientError() {
return errorCode.isClientError();
}
/**
* 检查是否为服务器异常
*
* @return 如果是服务器异常返回true
*/
public boolean isServerError() {
return errorCode.isServerError();
}
/**
* 获取异常的详细信息用于日志记录
*
* @return 详细信息字符串
*/
public String getDetailedInfo() {
StringBuilder info = new StringBuilder();
info.append("Exception Details:\n");
info.append(" ID: ").append(exceptionId).append("\n");
info.append(" Timestamp: ").append(timestamp.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)).append("\n");
info.append(" Error Code: ").append(errorCode.getCode()).append("\n");
info.append(" Category: ").append(errorCode.getCategory().getName()).append("\n");
info.append(" Message: ").append(getMessage()).append("\n");
info.append(" User Friendly Message: ").append(userFriendlyMessage).append("\n");
info.append(" Retryable: ").append(isRetryable()).append("\n");
info.append(" Fatal: ").append(isFatal()).append("\n");
if (!context.isEmpty()) {
info.append(" Context:\n");
context.forEach((key, value) ->
info.append(" ").append(key).append(": ").append(value).append("\n"));
}
if (getCause() != null) {
info.append(" Exception Chain:\n");
info.append(" ").append(getExceptionChain().replace("\n", "\n "));
}
return info.toString();
}
/**
* 构建详细的错误消息
*
* @param errorCode 错误代码
* @param message 原始消息
* @param context 上下文信息
* @return 详细的错误消息
*/
private static String buildDetailedMessage(ErrorCode errorCode, String message, Map<String, Object> context) {
StringBuilder detailedMessage = new StringBuilder();
if (errorCode != null) {
detailedMessage.append("[").append(errorCode.getCode()).append("] ");
}
if (message != null && !message.trim().isEmpty()) {
detailedMessage.append(message);
} else if (errorCode != null) {
detailedMessage.append(errorCode.getDefaultMessage());
} else {
detailedMessage.append("Unknown error occurred");
}
if (context != null && !context.isEmpty()) {
detailedMessage.append(" (Context: ");
context.entrySet().stream()
.filter(entry -> !"exceptionId".equals(entry.getKey()) &&
!"timestamp".equals(entry.getKey()) &&
!"errorCode".equals(entry.getKey()) &&
!"errorCategory".equals(entry.getKey()))
.forEach(entry -> detailedMessage.append(entry.getKey())
.append("=")
.append(entry.getValue())
.append(", "));
if (detailedMessage.toString().endsWith(", ")) {
detailedMessage.setLength(detailedMessage.length() - 2);
}
detailedMessage.append(")");
}
return detailedMessage.toString();
}
/**
* 生成用户友好的错误消息
*
* @param errorCode 错误代码
* @param originalMessage 原始消息
* @return 用户友好的错误消息
*/
private static String generateUserFriendlyMessage(ErrorCode errorCode, String originalMessage) {
if (errorCode == null) {
return "系统发生未知错误,请稍后重试或联系技术支持。";
}
// 获取国际化的用户友好消息
String friendlyMessage = errorCode.getMessage();
// 根据错误类型提供不同的用户友好消息
switch (errorCode.getCategory()) {
case CLIENT:
return "客户端配置问题:" + friendlyMessage + "。请检查配置后重试。";
case API:
return "服务调用失败:" + friendlyMessage + "。请稍后重试或联系技术支持。";
case CONFIGURATION:
return "配置错误:" + friendlyMessage + "。请检查配置文件。";
case SECURITY:
return "安全验证失败:" + friendlyMessage + "。请检查认证信息。";
case RESOURCE:
return "资源不足:" + friendlyMessage + "。请稍后重试。";
case DATA:
return "数据处理错误:" + friendlyMessage + "。请检查输入数据。";
case BUSINESS:
return "业务规则错误:" + friendlyMessage + "。请检查操作是否符合业务要求。";
case SYSTEM:
return "系统错误:" + friendlyMessage + "。请联系技术支持。";
default:
return friendlyMessage + "。如问题持续,请联系技术支持。";
}
}
/**
* 创建构建器
*
* @param errorCode 错误代码
* @return 异常构建器
*/
public static Builder builder(ErrorCode errorCode) {
return new Builder(errorCode);
}
/**
* 异常构建器类
*/
public static class Builder {
private final ErrorCode errorCode;
private String message;
private Map<String, Object> context = new HashMap<>();
private Throwable cause;
private Builder(ErrorCode errorCode) {
this.errorCode = errorCode;
}
public Builder message(String message) {
this.message = message;
return this;
}
public Builder context(String key, Object value) {
this.context.put(key, value);
return this;
}
public Builder context(Map<String, Object> context) {
if (context != null) {
this.context.putAll(context);
}
return this;
}
public Builder cause(Throwable cause) {
this.cause = cause;
return this;
}
public FsHelperException build() {
return new FsHelperException(errorCode, message, context, cause);
}
} }
} }

@ -0,0 +1,417 @@
package cn.isliu.core.logging;
import cn.isliu.core.enums.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;
/**
* 统一日志管理器
* 提供结构化日志记录敏感信息脱敏性能监控等功能
*
* @author liu
* @since 0.0.2
*/
public class FsLogger {
private static final Logger logger = LoggerFactory.getLogger(FsLogger.class);
// 敏感信息脱敏模式
private static final Pattern SENSITIVE_PATTERN = Pattern.compile(
"(appSecret|token|password|key|secret)=[^&\\s]*",
Pattern.CASE_INSENSITIVE
);
// 性能监控指标
private static final Map<String, AtomicLong> performanceMetrics = new ConcurrentHashMap<>();
private static final Map<String, AtomicLong> operationCounts = new ConcurrentHashMap<>();
// 日志级别控制
private static volatile LogLevel minLogLevel = LogLevel.INFO;
// 日志采样配置
private static volatile int samplingRate = 1; // 1表示不采样10表示每10条记录1条
private static final AtomicLong logCounter = new AtomicLong(0);
/**
* 日志级别枚举
*/
public enum LogLevel {
TRACE(0), DEBUG(1), INFO(2), WARN(3), ERROR(4);
private final int level;
LogLevel(int level) {
this.level = level;
}
public int getLevel() {
return level;
}
}
/**
* 记录API调用日志
*
* @param operation API操作名称
* @param params 请求参数
* @param duration 执行时长(毫秒)
*/
public static void apiCall(String operation, String params, long duration) {
if (!shouldLog(LogLevel.DEBUG)) {
return;
}
try {
// 设置MDC上下文
MDC.put("operation", operation);
MDC.put("duration", String.valueOf(duration));
// 更新性能指标
updatePerformanceMetrics(operation, duration);
// 记录日志
if (logger.isDebugEnabled()) {
logger.debug("API调用 - 操作: {} | 参数: {} | 耗时: {}ms",
operation, sanitizeParams(params), duration);
}
} finally {
MDC.clear();
}
}
/**
* 记录API调用日志带上下文信息
*
* @param operation API操作名称
* @param params 请求参数
* @param duration 执行时长(毫秒)
* @param context 上下文信息
*/
public static void apiCall(String operation, String params, long duration, Map<String, Object> context) {
if (!shouldLog(LogLevel.DEBUG)) {
return;
}
try {
// 设置MDC上下文
MDC.put("operation", operation);
MDC.put("duration", String.valueOf(duration));
// 添加自定义上下文
if (context != null) {
context.forEach((key, value) -> MDC.put(key, String.valueOf(value)));
}
// 更新性能指标
updatePerformanceMetrics(operation, duration);
// 记录日志
if (logger.isDebugEnabled()) {
logger.debug("API调用 - 操作: {} | 参数: {} | 耗时: {}ms | 上下文: {}",
operation, sanitizeParams(params), duration, formatContext(context));
}
} finally {
MDC.clear();
}
}
/**
* 记录错误日志
*
* @param errorCode 错误代码
* @param message 错误消息
* @param context 上下文信息
* @param cause 异常原因
*/
public static void error(ErrorCode errorCode, String message, String context, Throwable cause) {
if (!shouldLog(LogLevel.ERROR)) {
return;
}
try {
// 设置MDC上下文
MDC.put("errorCode", errorCode.getCode());
MDC.put("errorType", errorCode.name());
MDC.put("context", context);
// 记录日志
logger.error("错误 [{}]: {} | 上下文: {}", errorCode.getCode(), message, context, cause);
} finally {
MDC.clear();
}
}
/**
* 记录错误日志简化版本
*
* @param errorCode 错误代码
* @param message 错误消息
*/
public static void error(ErrorCode errorCode, String message) {
error(errorCode, message, null, null);
}
/**
* 记录信息日志
*
* @param message 日志消息
* @param args 参数
*/
public static void info(String message, Object... args) {
if (!shouldLog(LogLevel.INFO)) {
return;
}
logger.info(sanitizeMessage(message), sanitizeArgs(args));
}
/**
* 记录警告日志
*
* @param message 日志消息
* @param args 参数
*/
public static void warn(String message, Object... args) {
if (!shouldLog(LogLevel.WARN)) {
return;
}
logger.warn(sanitizeMessage(message), sanitizeArgs(args));
}
/**
* 记录调试日志
*
* @param message 日志消息
* @param args 参数
*/
public static void debug(String message, Object... args) {
if (!shouldLog(LogLevel.DEBUG)) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug(sanitizeMessage(message), sanitizeArgs(args));
}
}
/**
* 记录跟踪日志
*
* @param message 日志消息
* @param args 参数
*/
public static void trace(String message, Object... args) {
if (!shouldLog(LogLevel.TRACE)) {
return;
}
if (logger.isTraceEnabled()) {
logger.trace(sanitizeMessage(message), sanitizeArgs(args));
}
}
/**
* 记录性能指标
*
* @param operation 操作名称
* @param duration 执行时长
* @param success 是否成功
*/
public static void logPerformance(String operation, long duration, boolean success) {
if (!shouldLog(LogLevel.INFO)) {
return;
}
try {
// 设置MDC上下文
MDC.put("operation", operation);
MDC.put("duration", String.valueOf(duration));
MDC.put("success", String.valueOf(success));
// 更新性能指标
updatePerformanceMetrics(operation, duration);
// 记录日志
logger.info("性能指标 - 操作: {} | 耗时: {}ms | 状态: {}",
operation, duration, success ? "成功" : "失败");
} finally {
MDC.clear();
}
}
/**
* 获取性能指标
*
* @return 性能指标映射
*/
public static Map<String, Long> getPerformanceMetrics() {
Map<String, Long> metrics = new ConcurrentHashMap<>();
performanceMetrics.forEach((key, value) -> metrics.put(key, value.get()));
return metrics;
}
/**
* 获取操作计数
*
* @return 操作计数映射
*/
public static Map<String, Long> getOperationCounts() {
Map<String, Long> counts = new ConcurrentHashMap<>();
operationCounts.forEach((key, value) -> counts.put(key, value.get()));
return counts;
}
/**
* 重置性能指标
*/
public static void resetMetrics() {
performanceMetrics.clear();
operationCounts.clear();
}
/**
* 设置最小日志级别
*
* @param level 日志级别
*/
public static void setMinLogLevel(LogLevel level) {
minLogLevel = level;
}
/**
* 设置日志采样率
*
* @param rate 采样率1表示不采样10表示每10条记录1条
*/
public static void setSamplingRate(int rate) {
if (rate < 1) {
throw new IllegalArgumentException("采样率必须大于等于1");
}
samplingRate = rate;
}
/**
* 敏感信息脱敏
*
* @param params 参数字符串
* @return 脱敏后的参数字符串
*/
private static String sanitizeParams(String params) {
if (params == null || params.isEmpty()) {
return params;
}
return SENSITIVE_PATTERN.matcher(params).replaceAll("$1=***");
}
/**
* 消息脱敏
*
* @param message 消息
* @return 脱敏后的消息
*/
private static String sanitizeMessage(String message) {
if (message == null || message.isEmpty()) {
return message;
}
return SENSITIVE_PATTERN.matcher(message).replaceAll("$1=***");
}
/**
* 参数脱敏
*
* @param args 参数数组
* @return 脱敏后的参数数组
*/
private static Object[] sanitizeArgs(Object[] args) {
if (args == null || args.length == 0) {
return args;
}
Object[] sanitizedArgs = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String) {
sanitizedArgs[i] = sanitizeParams((String) args[i]);
} else {
sanitizedArgs[i] = args[i];
}
}
return sanitizedArgs;
}
/**
* 格式化上下文信息
*
* @param context 上下文映射
* @return 格式化后的上下文字符串
*/
private static String formatContext(Map<String, Object> context) {
if (context == null || context.isEmpty()) {
return "{}";
}
StringBuilder sb = new StringBuilder("{");
context.forEach((key, value) -> {
if (sb.length() > 1) {
sb.append(", ");
}
sb.append(key).append("=").append(value);
});
sb.append("}");
return sb.toString();
}
/**
* 更新性能指标
*
* @param operation 操作名称
* @param duration 执行时长
*/
private static void updatePerformanceMetrics(String operation, long duration) {
// 更新总耗时
performanceMetrics.computeIfAbsent(operation + "_total_duration", k -> new AtomicLong(0))
.addAndGet(duration);
// 更新最大耗时
performanceMetrics.computeIfAbsent(operation + "_max_duration", k -> new AtomicLong(0))
.updateAndGet(current -> Math.max(current, duration));
// 更新操作计数
operationCounts.computeIfAbsent(operation, k -> new AtomicLong(0))
.incrementAndGet();
}
/**
* 判断是否应该记录日志
*
* @param level 日志级别
* @return 是否应该记录
*/
private static boolean shouldLog(LogLevel level) {
// 检查日志级别
if (level.getLevel() < minLogLevel.getLevel()) {
return false;
}
// 检查采样率
if (samplingRate > 1) {
long count = logCounter.incrementAndGet();
return count % samplingRate == 0;
}
return true;
}
}

@ -11,8 +11,8 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import java.util.*; import java.util.*;
import java.util.logging.Level; import cn.isliu.core.logging.FsLogger;
import java.util.logging.Logger; import cn.isliu.core.enums.ErrorCode;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
@ -24,7 +24,7 @@ import java.lang.reflect.InvocationTargetException;
* 支持不同字段类型的转换处理 * 支持不同字段类型的转换处理
*/ */
public class ConvertFieldUtil { public class ConvertFieldUtil {
private static final Logger log = Logger.getLogger(ConvertFieldUtil.class.getName()); // 使用统一的FsLogger替代java.util.logging.Logger
private static final Gson gson = new Gson(); private static final Gson gson = new Gson();
/** /**
@ -226,15 +226,15 @@ public class ConvertFieldUtil {
FieldValueProcess fieldValueProcess = fieldFormatClass.getDeclaredConstructor().newInstance(); FieldValueProcess fieldValueProcess = fieldFormatClass.getDeclaredConstructor().newInstance();
result = fieldValueProcess.process(result); result = fieldValueProcess.process(result);
} catch (InstantiationException e) { } catch (InstantiationException e) {
log.log(Level.SEVERE, "无法实例化字段格式化类: " + fieldFormatClass.getName(), e); FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "无法实例化字段格式化类: " + fieldFormatClass.getName(), "convertFieldValue", e);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
log.log(Level.SEVERE, "无法访问字段格式化类的构造函数: " + fieldFormatClass.getName(), e); FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "无法访问字段格式化类的构造函数: " + fieldFormatClass.getName(), "convertFieldValue", e);
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
log.log(Level.SEVERE, "字段格式化类缺少无参构造函数: " + fieldFormatClass.getName(), e); FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "字段格式化类缺少无参构造函数: " + fieldFormatClass.getName(), "convertFieldValue", e);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
log.log(Level.SEVERE, "字段格式化类构造函数调用异常: " + fieldFormatClass.getName(), e); FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "字段格式化类构造函数调用异常: " + fieldFormatClass.getName(), "convertFieldValue", e);
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "创建字段格式化类实例时发生未知异常: " + fieldFormatClass.getName(), e); FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "创建字段格式化类实例时发生未知异常: " + fieldFormatClass.getName(), "convertFieldValue", e);
} }
} }
} }
@ -258,7 +258,7 @@ public class ConvertFieldUtil {
FieldValueProcess fieldValueProcess = fieldFormatClass.newInstance(); FieldValueProcess fieldValueProcess = fieldFormatClass.newInstance();
result = fieldValueProcess.reverseProcess(result); result = fieldValueProcess.reverseProcess(result);
} catch (InstantiationException | IllegalAccessException e) { } catch (InstantiationException | IllegalAccessException e) {
log.log(Level.FINE, "format value error", e); FsLogger.debug("format value error: {}", e.getMessage());
} }
} }
} }

@ -7,6 +7,7 @@ import cn.isliu.core.SheetMeta;
import cn.isliu.core.ValuesBatch; import cn.isliu.core.ValuesBatch;
import cn.isliu.core.client.FeishuClient; import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.exception.FsHelperException; import cn.isliu.core.exception.FsHelperException;
import cn.isliu.core.logging.FsLogger;
import cn.isliu.core.pojo.ApiResponse; import cn.isliu.core.pojo.ApiResponse;
import cn.isliu.core.service.*; import cn.isliu.core.service.*;
import com.google.gson.Gson; import com.google.gson.Gson;
@ -25,8 +26,8 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger; import cn.isliu.core.logging.FsLogger;
import java.util.logging.Level; import cn.isliu.core.enums.ErrorCode;
/** /**
@ -37,7 +38,7 @@ import java.util.logging.Level;
public class FsApiUtil { public class FsApiUtil {
private static final Gson gson = new Gson(); private static final Gson gson = new Gson();
private static final Logger log = Logger.getLogger(FsApiUtil.class.getName()); // 使用统一的FsLogger替代java.util.logging.Logger
private static final String REQ_TYPE = "JSON_STR"; private static final String REQ_TYPE = "JSON_STR";
public static final int DEFAULT_ROW_NUM = 1000; public static final int DEFAULT_ROW_NUM = 1000;
@ -71,11 +72,11 @@ public class FsApiUtil {
if (batchRangeResp.success()) { if (batchRangeResp.success()) {
valuesBatch = gson.fromJson(gson.toJson(batchRangeResp.getData()), ValuesBatch.class); valuesBatch = gson.fromJson(gson.toJson(batchRangeResp.getData()), ValuesBatch.class);
} else { } else {
log.log(Level.SEVERE, "【飞书表格】获取Sheet数据失败 错误信息:{0}", gson.toJson(batchRangeResp)); FsLogger.error(ErrorCode.API_CALL_FAILED, "【飞书表格】获取Sheet数据失败 错误信息:" + gson.toJson(batchRangeResp));
throw new FsHelperException("【飞书表格】获取Sheet数据失败"); throw new FsHelperException("【飞书表格】获取Sheet数据失败");
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】获取Sheet数据失败 错误信息:{0}", e.getMessage()); FsLogger.error(ErrorCode.API_CALL_FAILED, "【飞书表格】获取Sheet数据失败 错误信息:" + e.getMessage(), "getSheetData", e);
throw new FsHelperException("【飞书表格】获取Sheet数据失败"); throw new FsHelperException("【飞书表格】获取Sheet数据失败");
} }
return valuesBatch; return valuesBatch;
@ -115,12 +116,12 @@ public class FsApiUtil {
return sheet.get(); return sheet.get();
} else { } else {
log.log(Level.SEVERE, "【飞书表格】 获取Sheet元数据异常错误信息{0}", gson.toJson(resp)); FsLogger.error(ErrorCode.API_CALL_FAILED, "【飞书表格】 获取Sheet元数据异常错误信息" + gson.toJson(resp));
throw new FsHelperException("【飞书表格】 获取Sheet元数据异常错误信息" + resp.getMsg()); throw new FsHelperException("【飞书表格】 获取Sheet元数据异常错误信息" + resp.getMsg());
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 获取Sheet元数据异常错误信息{0}", e.getMessage()); FsLogger.error(ErrorCode.API_CALL_FAILED, "【飞书表格】 获取Sheet元数据异常错误信息" + e.getMessage(), "getSheetMeta", e);
throw new FsHelperException("【飞书表格】 获取Sheet元数据异常"); throw new FsHelperException("【飞书表格】 获取Sheet元数据异常");
} }
} }
@ -145,11 +146,11 @@ public class FsApiUtil {
ApiResponse batchMergeResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchMergeRequest); ApiResponse batchMergeResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchMergeRequest);
if (!batchMergeResp.success()) { if (!batchMergeResp.success()) {
log.log(Level.SEVERE, "【飞书表格】 合并单元格请求异常!参数:{0},错误信息:{1}", new Object[]{cell, batchMergeResp.getMsg()}); FsLogger.warn("【飞书表格】 合并单元格请求异常!参数:{},错误信息:{}", cell, batchMergeResp.getMsg());
throw new FsHelperException("【飞书表格】 合并单元格请求异常!"); throw new FsHelperException("【飞书表格】 合并单元格请求异常!");
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 合并单元格异常!参数:{0},错误信息:{1}", new Object[]{cell, e.getMessage()}); FsLogger.warn("【飞书表格】 合并单元格异常!参数:{},错误信息:{}", cell, e.getMessage());
throw new FsHelperException("【飞书表格】 合并单元格异常!"); throw new FsHelperException("【飞书表格】 合并单元格异常!");
} }
} }
@ -167,11 +168,11 @@ public class FsApiUtil {
ApiResponse apiResponse = client.customValues().valueBatchUpdate(spreadsheetToken, batchValueRequest); ApiResponse apiResponse = client.customValues().valueBatchUpdate(spreadsheetToken, batchValueRequest);
if (!apiResponse.success()) { if (!apiResponse.success()) {
log.log(Level.SEVERE, "【飞书表格】 写入表格头数据异常!错误信息:{0}", apiResponse.getMsg()); FsLogger.warn("【飞书表格】 写入表格头数据异常!错误信息:{}", apiResponse.getMsg());
throw new FsHelperException("【飞书表格】 写入表格头数据异常!"); throw new FsHelperException("【飞书表格】 写入表格头数据异常!");
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 写入表格头异常!错误信息:{0}", e.getMessage()); FsLogger.warn("【飞书表格】 写入表格头异常!错误信息:{}", e.getMessage());
throw new FsHelperException("【飞书表格】 写入表格头异常!"); throw new FsHelperException("【飞书表格】 写入表格头异常!");
} }
} }
@ -186,11 +187,11 @@ public class FsApiUtil {
ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest); ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest);
if (!apiResponse.success()) { if (!apiResponse.success()) {
log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!参数:{0},错误信息:{1}", new Object[]{style, apiResponse.getMsg()}); FsLogger.warn("【飞书表格】 写入表格样式数据异常!参数:{},错误信息:{}", style, apiResponse.getMsg());
throw new FsHelperException("【飞书表格】 写入表格样式数据异常!"); throw new FsHelperException("【飞书表格】 写入表格样式数据异常!");
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 写入表格样式异常!参数:{0},错误信息:{1}", new Object[]{style, e.getMessage()}); FsLogger.warn("【飞书表格】 写入表格样式异常!参数:{},错误信息:{}", style, e.getMessage());
throw new FsHelperException("【飞书表格】 写入表格样式异常!"); throw new FsHelperException("【飞书表格】 写入表格样式异常!");
} }
} }
@ -209,7 +210,7 @@ public class FsApiUtil {
ApiResponse addResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, addSheetRequest); ApiResponse addResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, addSheetRequest);
if (addResp.success()) { if (addResp.success()) {
log.log(Level.INFO, "【飞书表格】 创建 sheet 成功! {0}", gson.toJson(addResp)); FsLogger.info("【飞书表格】 创建 sheet 成功! {}", gson.toJson(addResp));
JsonObject jsObj = gson.fromJson(gson.toJson(addResp.getData()), JsonObject.class); JsonObject jsObj = gson.fromJson(gson.toJson(addResp.getData()), JsonObject.class);
JsonArray replies = jsObj.getAsJsonArray("replies"); JsonArray replies = jsObj.getAsJsonArray("replies");
@ -218,16 +219,16 @@ public class FsApiUtil {
Reply reply = gson.fromJson(jsonObject, Reply.class); Reply reply = gson.fromJson(jsonObject, Reply.class);
sheetId = reply.getAddSheet().getProperties().getSheetId(); sheetId = reply.getAddSheet().getProperties().getSheetId();
if (sheetId == null || sheetId.isEmpty()) { if (sheetId == null || sheetId.isEmpty()) {
log.log(Level.SEVERE, "【飞书表格】 创建 sheet 失败!"); FsLogger.warn("【飞书表格】 创建 sheet 失败!");
throw new FsHelperException("【飞书表格】创建 sheet 异常SheetId返回为空"); throw new FsHelperException("【飞书表格】创建 sheet 异常SheetId返回为空");
} }
} else { } else {
log.log(Level.SEVERE, "【飞书表格】 创建 sheet 失败!错误信息:{0}", gson.toJson(addResp)); FsLogger.warn("【飞书表格】 创建 sheet 失败!错误信息:{}", gson.toJson(addResp));
throw new FsHelperException("【飞书表格】 创建 sheet 异常!"); throw new FsHelperException("【飞书表格】 创建 sheet 异常!");
} }
} catch (Exception e) { } catch (Exception e) {
String message = e.getMessage(); String message = e.getMessage();
log.log(Level.SEVERE, "【飞书表格】 创建 sheet 异常!错误信息:{0}", message); FsLogger.warn("【飞书表格】 创建 sheet 异常!错误信息:{}", message);
throw new FsHelperException(message != null && message.contains("403")? "请按照上方操作,当前智投无法操作对应文档哦" : "【飞书表格】 创建 sheet 异常!"); throw new FsHelperException(message != null && message.contains("403")? "请按照上方操作,当前智投无法操作对应文档哦" : "【飞书表格】 创建 sheet 异常!");
} }
@ -248,7 +249,7 @@ public class FsApiUtil {
ApiResponse copyResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, copyRequest); ApiResponse copyResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, copyRequest);
if (copyResp.success()) { if (copyResp.success()) {
log.log(Level.INFO, "【飞书表格】 复制 sheet 成功! {0}", gson.toJson(copyResp)); FsLogger.info("【飞书表格】 复制 sheet 成功! {}", gson.toJson(copyResp));
JsonObject jsObj = gson.fromJson(gson.toJson(copyResp.getData()), JsonObject.class); JsonObject jsObj = gson.fromJson(gson.toJson(copyResp.getData()), JsonObject.class);
JsonArray replies = jsObj.getAsJsonArray("replies"); JsonArray replies = jsObj.getAsJsonArray("replies");
@ -262,7 +263,7 @@ public class FsApiUtil {
} }
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 复制模版异常!错误信息:{0}", e.getMessage()); FsLogger.warn("【飞书表格】 复制模版异常!错误信息:{}", e.getMessage());
throw new FsHelperException("【飞书表格】 复制模版异常!"); throw new FsHelperException("【飞书表格】 复制模版异常!");
} }
return sheetId; return sheetId;
@ -287,10 +288,10 @@ public class FsApiUtil {
ApiResponse batchStyleResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchStyleRequest); ApiResponse batchStyleResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchStyleRequest);
if (!batchStyleResp.success()) { if (!batchStyleResp.success()) {
log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!参数:{0},错误信息:{1}", new Object[]{conf, batchStyleResp.getMsg()}); FsLogger.warn("【飞书表格】 写入表格样式数据异常!参数:{},错误信息:{}", conf, batchStyleResp.getMsg());
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 写入表格样式数据异常!", e); FsLogger.warn("【飞书表格】 写入表格样式数据异常!{}", e.getMessage());
} }
} }
@ -314,10 +315,10 @@ public class FsApiUtil {
ApiResponse response = client.customDataValidations().dataValidationBatchUpdate(spreadsheetToken, batchRequest); ApiResponse response = client.customDataValidations().dataValidationBatchUpdate(spreadsheetToken, batchRequest);
if (!response.success()) { if (!response.success()) {
log.log(Level.SEVERE, "设置下拉列表失败, sheetId:{0}, startPosition:{1}, endPosition: {2}, 返回信息:{3}", new Object[]{sheetId, startPosition, endPosition, gson.toJson(response)}); FsLogger.warn("设置下拉列表失败, sheetId:{}, startPosition:{}, endPosition: {}, 返回信息:{}", sheetId, startPosition, endPosition, gson.toJson(response));
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "设置下拉列表失败sheetId:{0}", new Object[]{sheetId}); FsLogger.warn("设置下拉列表失败sheetId:{}", sheetId);
} }
} }
@ -332,10 +333,10 @@ public class FsApiUtil {
ApiResponse deleteResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, deleteRequest); ApiResponse deleteResp = client.customSheets().sheetsBatchUpdate(spreadsheetToken, deleteRequest);
if (!deleteResp.success()) { if (!deleteResp.success()) {
log.log(Level.SEVERE, "【飞书表格】 删除 sheet 失败!参数:{0},错误信息:{1}", new Object[]{sheetId, deleteResp.getMsg()}); FsLogger.warn("【飞书表格】 删除 sheet 失败!参数:{},错误信息:{}", sheetId, deleteResp.getMsg());
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 删除 sheet 异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()}); FsLogger.warn("【飞书表格】 删除 sheet 异常!参数:{},错误信息:{}", sheetId, e.getMessage());
} }
} }
@ -358,7 +359,7 @@ public class FsApiUtil {
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 下载素材异常!参数:{0},错误信息:{1}", new Object[]{fileToken, e.getMessage()}); FsLogger.warn("【飞书表格】 下载素材异常!参数:{},错误信息:{}", fileToken, e.getMessage());
throw new FsHelperException("【飞书表格】 下载素材异常!"); throw new FsHelperException("【飞书表格】 下载素材异常!");
} }
} }
@ -375,16 +376,16 @@ public class FsApiUtil {
if (resp.success()) { if (resp.success()) {
return resp.getData().getTmpDownloadUrls()[0].getTmpDownloadUrl(); return resp.getData().getTmpDownloadUrls()[0].getTmpDownloadUrl();
} else { } else {
log.log(Level.SEVERE, "【飞书表格】 获取临时下载地址失败!参数:{0},错误信息:{1}", new Object[]{fileToken, gson.toJson(resp)}); FsLogger.warn("【飞书表格】 获取临时下载地址失败!参数:{},错误信息:{}", fileToken, gson.toJson(resp));
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 获取临时下载地址异常!参数:{0},错误信息:{1}", new Object[]{fileToken, e.getMessage()}); FsLogger.warn("【飞书表格】 获取临时下载地址异常!参数:{},错误信息:{}", fileToken, e.getMessage());
} }
return tmpUrl; return tmpUrl;
} }
public static Object putValues(String spreadsheetToken, CustomValueService.ValueRequest putValuesBuilder, FeishuClient client) { public static Object putValues(String spreadsheetToken, CustomValueService.ValueRequest putValuesBuilder, FeishuClient client) {
log.log(Level.INFO, "【飞书表格】 putValues 开始写入数据!参数:{0}", gson.toJson(putValuesBuilder)); FsLogger.info("【飞书表格】 putValues 开始写入数据!参数:{}", gson.toJson(putValuesBuilder));
// 添加到批量请求中 // 添加到批量请求中
CustomValueService.ValueBatchUpdateRequest putDataRequest = CustomValueService.ValueBatchUpdateRequest.newBuilder() CustomValueService.ValueBatchUpdateRequest putDataRequest = CustomValueService.ValueBatchUpdateRequest.newBuilder()
@ -396,11 +397,11 @@ public class FsApiUtil {
if (putResp.success()) { if (putResp.success()) {
return putResp.getData(); return putResp.getData();
} else { } else {
log.log(Level.SEVERE, "【飞书表格】 写入表格数据失败!参数:{0},错误信息:{1}", new Object[]{putValuesBuilder, putResp.getMsg()}); FsLogger.warn("【飞书表格】 写入表格数据失败!参数:{},错误信息:{}", putValuesBuilder, putResp.getMsg());
throw new FsHelperException("【飞书表格】 写入表格数据失败!"); throw new FsHelperException("【飞书表格】 写入表格数据失败!");
} }
} catch (IOException e) { } catch (IOException e) {
log.log(Level.SEVERE, "【飞书表格】 写入表格数据异常!参数:{0},错误信息:{1}", new Object[]{spreadsheetToken, e.getMessage()}); FsLogger.warn("【飞书表格】 写入表格数据异常!参数:{},错误信息:{}", spreadsheetToken, e.getMessage());
throw new FsHelperException("【飞书表格】 写入表格数据异常!"); throw new FsHelperException("【飞书表格】 写入表格数据异常!");
} }
} }
@ -408,7 +409,7 @@ public class FsApiUtil {
public static Object batchPutValues(String sheetId, String spreadsheetToken, public static Object batchPutValues(String sheetId, String spreadsheetToken,
CustomValueService.ValueRequest batchPutRequest, FeishuClient client) { CustomValueService.ValueRequest batchPutRequest, FeishuClient client) {
log.log(Level.INFO, "【飞书表格】 batchPutValues 开始写入数据!参数:{0}", gson.toJson(batchPutRequest)); FsLogger.info("【飞书表格】 batchPutValues 开始写入数据!参数:{}", gson.toJson(batchPutRequest));
try { try {
CustomValueService.ValueBatchUpdateRequest batchPutDataRequest = CustomValueService.ValueBatchUpdateRequest batchPutDataRequest =
@ -420,11 +421,11 @@ public class FsApiUtil {
if (batchPutResp.success()) { if (batchPutResp.success()) {
return batchPutResp.getData(); return batchPutResp.getData();
} else { } else {
log.log(Level.SEVERE, "【飞书表格】 批量写入数据失败!参数:{0},错误信息:{1}", new Object[]{sheetId, gson.toJson(batchPutResp)}); FsLogger.warn("【飞书表格】 批量写入数据失败!参数:{},错误信息:{}", sheetId, gson.toJson(batchPutResp));
throw new FsHelperException("【飞书表格】 批量写入数据失败!"); throw new FsHelperException("【飞书表格】 批量写入数据失败!");
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 批量写入数据异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()}); FsLogger.warn("【飞书表格】 批量写入数据异常!参数:{},错误信息:{}", sheetId, e.getMessage());
throw new FsHelperException("【飞书表格】 批量写入数据异常!"); throw new FsHelperException("【飞书表格】 批量写入数据异常!");
} }
} }
@ -443,11 +444,11 @@ public class FsApiUtil {
if (batchResp.success()) { if (batchResp.success()) {
return batchResp.getData(); return batchResp.getData();
} else { } else {
log.log(Level.SEVERE, "【飞书表格】 添加行列失败!参数:{0},错误信息:{1}", new Object[]{sheetId, gson.toJson(batchResp)}); FsLogger.warn("【飞书表格】 添加行列失败!参数:{},错误信息:{}", sheetId, gson.toJson(batchResp));
throw new FsHelperException("【飞书表格】 添加行列失败!"); throw new FsHelperException("【飞书表格】 添加行列失败!");
} }
} catch (IOException e) { } catch (IOException e) {
log.log(Level.SEVERE, "【飞书表格】 添加行列异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()}); FsLogger.warn("【飞书表格】 添加行列异常!参数:{},错误信息:{}", sheetId, e.getMessage());
throw new FsHelperException("【飞书表格】 添加行列异常!"); throw new FsHelperException("【飞书表格】 添加行列异常!");
} }
} }
@ -466,10 +467,10 @@ public class FsApiUtil {
if (resp.success()) { if (resp.success()) {
return resp.getData(); return resp.getData();
} else { } else {
log.log(Level.SEVERE, "【飞书表格】 获取表格信息失败!参数:{0},错误信息:{1}", new Object[]{sheetId, resp.getMsg()}); FsLogger.warn("【飞书表格】 获取表格信息失败!参数:{},错误信息:{}", sheetId, resp.getMsg());
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 获取表格信息异常!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()}); FsLogger.warn("【飞书表格】 获取表格信息异常!参数:{},错误信息:{}", sheetId, e.getMessage());
} }
return null; return null;
} }
@ -487,11 +488,11 @@ public class FsApiUtil {
ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest); ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest);
if (!apiResponse.success()) { if (!apiResponse.success()) {
log.log(Level.SEVERE, "【飞书表格】 设置单元格类型失败!参数:{0},错误信息:{1}", new Object[]{sheetId, apiResponse.getMsg()}); FsLogger.warn("【飞书表格】 设置单元格类型失败!参数:{},错误信息:{}", sheetId, apiResponse.getMsg());
throw new FsHelperException("【飞书表格】 批量设置单元格类型失败!"); throw new FsHelperException("【飞书表格】 批量设置单元格类型失败!");
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 设置单元格类型失败!参数:{0},错误信息:{1}", new Object[]{sheetId, e.getMessage()}); FsLogger.warn("【飞书表格】 设置单元格类型失败!参数:{},错误信息:{}", sheetId, e.getMessage());
throw new FsHelperException("【飞书表格】 批量设置单元格类型异常!"); throw new FsHelperException("【飞书表格】 批量设置单元格类型异常!");
} }
} }
@ -513,11 +514,11 @@ public class FsApiUtil {
ApiResponse imageResp = client.customValues().valueBatchUpdate(spreadsheetToken, imageWriteRequest); ApiResponse imageResp = client.customValues().valueBatchUpdate(spreadsheetToken, imageWriteRequest);
if (!imageResp.success()) { if (!imageResp.success()) {
log.log(Level.SEVERE, "【飞书表格】 图片上传失败!参数:{0},错误信息:{1}", new Object[]{filePath, gson.toJson(imageResp)}); FsLogger.warn("【飞书表格】 图片上传失败!参数:{},错误信息:{}", filePath, gson.toJson(imageResp));
} }
return imageResp.getData(); return imageResp.getData();
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【飞书表格】 图片上传异常!参数:{0},错误信息:{1}", new Object[]{filePath, e.getMessage()}); FsLogger.warn("【飞书表格】 图片上传异常!参数:{},错误信息:{}", filePath, e.getMessage());
} }
return null; return null;

@ -1,35 +0,0 @@
package cn.isliu.core.utils;
import cn.isliu.core.client.FeishuClient;
public class FsClientUtil {
public static FeishuClient client;
/**
* 获取飞书客户端
*
* @param appId 飞书应用ID
* @param appSecret 飞书应用密钥
* @return 飞书客户端
*/
public static FeishuClient initFeishuClient(String appId, String appSecret) {
client = FeishuClient.newBuilder(appId, appSecret).build();
return client;
}
/**
* 设置飞书客户端
*
* @param appId 飞书应用ID
* @param appSecret 飞书应用密钥
*/
public static void setClient(String appId, String appSecret) {
client = FeishuClient.newBuilder(appId, appSecret).build();
}
public static FeishuClient getFeishuClient() {
return client;
}
}

@ -2,6 +2,8 @@ package cn.isliu.core.utils;
import cn.isliu.core.*; import cn.isliu.core.*;
import cn.isliu.core.annotation.TableProperty; import cn.isliu.core.annotation.TableProperty;
import cn.isliu.core.client.FsClient;
import cn.isliu.core.config.FsConfig; import cn.isliu.core.config.FsConfig;
import cn.isliu.core.converters.OptionsValueProcess; import cn.isliu.core.converters.OptionsValueProcess;
import cn.isliu.core.enums.BaseEnum; import cn.isliu.core.enums.BaseEnum;
@ -51,7 +53,7 @@ public class FsTableUtil {
// 3. 获取工作表数据 // 3. 获取工作表数据
ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken, ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken,
"A" + startRowIndex, "A" + startRowIndex,
getColumnName(colCount - 1) + endRowIndex, FsClientUtil.getFeishuClient()); getColumnName(colCount - 1) + endRowIndex, FsClient.getInstance().getClient());
if (valuesBatch != null) { if (valuesBatch != null) {
List<ValueRange> valueRanges = valuesBatch.getValueRanges(); List<ValueRange> valueRanges = valuesBatch.getValueRanges();
for (ValueRange valueRange : valueRanges) { for (ValueRange valueRange : valueRanges) {
@ -66,12 +68,12 @@ public class FsTableUtil {
List<FsTableData> dataList = getFsTableData(tableData); List<FsTableData> dataList = getFsTableData(tableData);
Map<String, String> titleMap = new HashMap<>(); Map<String, String> titleMap = new HashMap<>();
dataList.stream().filter(d -> d.getRow() == (FsConfig.getTitleLine() - 1)).findFirst() dataList.stream().filter(d -> d.getRow() == (FsConfig.getInstance().getTitleLine() - 1)).findFirst()
.ifPresent(d -> { .ifPresent(d -> {
Map<String, String> map = (Map<String, String>) d.getData(); Map<String, String> map = (Map<String, String>) d.getData();
titleMap.putAll(map); titleMap.putAll(map);
}); });
return dataList.stream().filter(fsTableData -> fsTableData.getRow() >= FsConfig.getHeadLine()).map(item -> { return dataList.stream().filter(fsTableData -> fsTableData.getRow() >= FsConfig.getInstance().getHeadLine()).map(item -> {
Map<String, Object> resultMap = new HashMap<>(); Map<String, Object> resultMap = new HashMap<>();
Map<String, Object> map = (Map<String, Object>) item.getData(); Map<String, Object> map = (Map<String, Object>) item.getData();
@ -264,8 +266,8 @@ public class FsTableUtil {
Map<String, String> resultMap = new TreeMap<>(); Map<String, String> resultMap = new TreeMap<>();
ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken, ValuesBatch valuesBatch = FsApiUtil.getSheetData(sheet.getSheetId(), spreadsheetToken,
"A" + FsConfig.getTitleLine(), "A" + FsConfig.getInstance().getTitleLine(),
getColumnName(colCount - 1) + FsConfig.getTitleLine(), FsClientUtil.getFeishuClient()); getColumnName(colCount - 1) + FsConfig.getInstance().getTitleLine(), FsClient.getInstance().getClient());
if (valuesBatch != null) { if (valuesBatch != null) {
List<ValueRange> valueRanges = valuesBatch.getValueRanges(); List<ValueRange> valueRanges = valuesBatch.getValueRanges();
if (valueRanges != null && !valueRanges.isEmpty()) { if (valueRanges != null && !valueRanges.isEmpty()) {
@ -298,10 +300,10 @@ public class FsTableUtil {
position = FsTableUtil.getColumnNameByNuNumber(i + 1); position = FsTableUtil.getColumnNameByNuNumber(i + 1);
} }
} }
int line = FsConfig.getTitleLine() + 1; int line = FsConfig.getInstance().getTitleLine() + 1;
if (tableProperty.enumClass() != BaseEnum.class) { if (tableProperty.enumClass() != BaseEnum.class) {
FsApiUtil.setOptions(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200, FsApiUtil.setOptions(sheetId, FsClient.getInstance().getClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200,
Arrays.stream(tableProperty.enumClass().getEnumConstants()).map(BaseEnum::getDesc).collect(Collectors.toList())); Arrays.stream(tableProperty.enumClass().getEnumConstants()).map(BaseEnum::getDesc).collect(Collectors.toList()));
} }
@ -315,7 +317,7 @@ public class FsTableUtil {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
FsApiUtil.setOptions(sheetId, FsClientUtil.getFeishuClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200, FsApiUtil.setOptions(sheetId, FsClient.getInstance().getClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200,
result); result);
} }
} }
@ -333,7 +335,8 @@ public class FsTableUtil {
String colorTemplate = "{\"data\": [{\"style\": {\"font\": {\"bold\": true, \"clean\": false, \"italic\": false, \"fontSize\": \"10pt/1.5\"}, \"clean\": false, \"hAlign\": 1, \"vAlign\": 1, \"backColor\": \"#000000\", \"foreColor\": \"#ffffff\", \"formatter\": \"\", \"borderType\": \"FULL_BORDER\", \"borderColor\": \"#000000\", \"textDecoration\": 0}, \"ranges\": [\"SHEET_ID!RANG\"]}]}"; String colorTemplate = "{\"data\": [{\"style\": {\"font\": {\"bold\": true, \"clean\": false, \"italic\": false, \"fontSize\": \"10pt/1.5\"}, \"clean\": false, \"hAlign\": 1, \"vAlign\": 1, \"backColor\": \"#000000\", \"foreColor\": \"#ffffff\", \"formatter\": \"\", \"borderType\": \"FULL_BORDER\", \"borderColor\": \"#000000\", \"textDecoration\": 0}, \"ranges\": [\"SHEET_ID!RANG\"]}]}";
colorTemplate = colorTemplate.replace("SHEET_ID", sheetId); colorTemplate = colorTemplate.replace("SHEET_ID", sheetId);
colorTemplate = colorTemplate.replace("RANG", "A1:" + FsTableUtil.getColumnNameByNuNumber(size) + "1"); colorTemplate = colorTemplate.replace("RANG", "A1:" + FsTableUtil.getColumnNameByNuNumber(size) + "1");
colorTemplate = colorTemplate.replace("FORE_COLOR", FsConfig.FORE_COLOR).replace("BACK_COLOR", FsConfig.BACK_COLOR); colorTemplate = colorTemplate.replace("FORE_COLOR", FsConfig.getInstance().getForeColor())
.replace("BACK_COLOR", FsConfig.getInstance().getBackColor());
return colorTemplate; return colorTemplate;
} }
} }

@ -11,8 +11,8 @@ import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
import java.util.logging.Level; import cn.isliu.core.logging.FsLogger;
import java.util.logging.Logger; import cn.isliu.core.enums.ErrorCode;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -23,7 +23,7 @@ import java.util.stream.Collectors;
*/ */
public class GenerateUtil { public class GenerateUtil {
private static final Logger log = Logger.getLogger(GenerateUtil.class.getName()); // 使用统一的FsLogger替代java.util.logging.Logger
/** /**
* 根据配置和数据生成DTO对象通用版本 * 根据配置和数据生成DTO对象通用版本
@ -49,7 +49,7 @@ public class GenerateUtil {
try { try {
setNestedField(t, fieldPath, value); setNestedField(t, fieldPath, value);
} catch (Exception e) { } catch (Exception e) {
log.log(Level.SEVERE, "【巨量广告助手】 获取字段值异常!参数:{0},异常:{1}", new Object[]{fieldPath, e.getMessage()}); FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "【巨量广告助手】 获取字段值异常!参数:" + fieldPath + ",异常:" + e.getMessage(), "generateList", e);
} }
} }
}); });
@ -129,7 +129,7 @@ public class GenerateUtil {
field.set(target, nestedObj); field.set(target, nestedObj);
} catch (InstantiationException e) { } catch (InstantiationException e) {
// 如果无法创建实例则记录日志并跳过该字段 // 如果无法创建实例则记录日志并跳过该字段
log.log(Level.WARNING, "无法创建嵌套对象实例: " + field.getType().getName() + ", 字段: " + fieldName, e); FsLogger.warn("无法创建嵌套对象实例: {} , 字段: {}", field.getType().getName(), fieldName);
return; return;
} }
} }
@ -153,7 +153,7 @@ public class GenerateUtil {
return elementClass.getDeclaredConstructor().newInstance(); return elementClass.getDeclaredConstructor().newInstance();
} catch (Exception e) { } catch (Exception e) {
// 如果无法创建实例则记录日志并返回null // 如果无法创建实例则记录日志并返回null
log.log(Level.WARNING, "无法创建List元素实例: " + elementClass.getName(), e); FsLogger.warn("无法创建List元素实例: {}", elementClass.getName());
return null; return null;
} }
} }
@ -354,7 +354,7 @@ public class GenerateUtil {
result.put(fieldName, value); result.put(fieldName, value);
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING, "获取字段值异常,字段路径:" + fieldPath, e); FsLogger.warn("获取字段值异常,字段路径:{}", fieldPath);
} }
} }
return result; return result;

@ -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.

@ -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