feat: 实现SMB文件服务并优化合同文件管理

- 新增SmbFileService服务类,支持SMB/CIFS协议的文件操作
- 修改合同文件管理逻辑,支持SMB路径检查与目录创建
- 优化BankTableCell实现工厂模式并更新相关文档
- 调整Redis配置并添加连接测试
- 修复合同发票视图模型的时间处理问题
- 更新项目版本至0.0.134-SNAPSHOT
This commit is contained in:
2025-11-12 16:32:03 +08:00
parent 1cb0edbd07
commit e761990ebf
22 changed files with 877 additions and 50 deletions

View File

@@ -154,4 +154,70 @@ public class [业务]WindowController extends AbstEntityController<[Vo类型], [
// 窗口显示后的逻辑 // 窗口显示后的逻辑
} }
} }
``` ```
## 13. TableCell 工厂模式规范
### 13.1 工厂方法命名
- TableCell类应提供静态工厂方法`forTableColumn`,用于创建单元格工厂
- 方法命名必须为`forTableColumn`,保持一致性
### 13.2 工厂方法参数
- 工厂方法应接收服务层参数,如`BankService`
- 参数类型应与TableCell构造函数所需的服务类型一致
### 13.3 工厂方法返回类型
- 返回类型应为`Callback<javafx.scene.control.TableColumn<V, T>, javafx.scene.control.TableCell<V, T>>`
- 其中`V`为ViewModel类型`T`为单元格值类型
### 13.4 工厂方法实现
- 在工厂方法内部创建并返回TableCell实例
- 使用泛型参数确保类型安全
### 13.5 使用方式
- 在设置表格列的单元格工厂时应调用TableCell的静态工厂方法
- 避免直接使用`new TableCell<>(service)`的方式创建实例
### 13.6 示例代码
```java
/**
* 银行单元格
*/
@NoArgsConstructor
public class BankTableCell<T> extends AsyncUpdateTableCell<T, Integer, BankVo> {
/**
* 创建单元格工厂
*
* @param bankService 银行服务
* @return 单元格工厂
*/
public static <V> Callback<javafx.scene.control.TableColumn<V, Integer>, javafx.scene.control.TableCell<V, Integer>> forTableColumn(
BankService bankService) {
return param -> new BankTableCell<V>(bankService);
}
public BankTableCell(BankService service) {
setService(service);
}
@Override
protected BankService getServiceBean() {
return getBean(BankService.class);
}
}
```
### 13.7 使用示例
```java
// 推荐方式:使用工厂方法
column.setCellFactory(BankTableCell.forTableColumn(getBankService()));
// 不推荐方式:直接实例化
// column.setCellFactory(param -> new BankTableCell<>(getBankService()));

View File

@@ -6,12 +6,12 @@
<parent> <parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId> <artifactId>Contract-Manager</artifactId>
<version>0.0.129-SNAPSHOT</version> <version>0.0.134-SNAPSHOT</version>
</parent> </parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>client</artifactId> <artifactId>client</artifactId>
<version>0.0.129-SNAPSHOT</version> <version>0.0.134-SNAPSHOT</version>
<properties> <properties>
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
@@ -22,7 +22,7 @@
<dependency> <dependency>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>common</artifactId> <artifactId>common</artifactId>
<version>0.0.129-SNAPSHOT</version> <version>0.0.134-SNAPSHOT</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@@ -53,7 +53,6 @@ public class WebSocketClientService {
@Getter @Getter
@Setter @Setter
private long readTimeout = 30000; private long readTimeout = 30000;
private String webSocketUrl = "ws://localhost:8080/ws";
private boolean isActive = false; // 标记连接是否活跃 private boolean isActive = false; // 标记连接是否活跃
private ScheduledFuture<?> heartbeatTask; // 心跳任务 private ScheduledFuture<?> heartbeatTask; // 心跳任务
private ScheduledFuture<?> reconnectFuture; // 修改类型为CompletableFuture<Void> private ScheduledFuture<?> reconnectFuture; // 修改类型为CompletableFuture<Void>
@@ -248,6 +247,8 @@ public class WebSocketClientService {
try { try {
// 构建WebSocket请求包含认证信息 // 构建WebSocket请求包含认证信息
var myProperties = SpringApp.getBean(MyProperties.class);
String webSocketUrl = "ws://" + myProperties.getServerHost() + ":" + myProperties.getServerPort() + "/ws";
Request request = new Request.Builder() Request request = new Request.Builder()
.url(webSocketUrl) .url(webSocketUrl)
.build(); .build();

View File

@@ -62,8 +62,9 @@ public class CompanyTabSkinBankAccount
bankAccountSearchBtn.setOnAction(this::onTableRefreshAction); bankAccountSearchBtn.setOnAction(this::onTableRefreshAction);
bankAccountTable_idColumn.setCellValueFactory(param -> param.getValue().getId()); bankAccountTable_idColumn.setCellValueFactory(param -> param.getValue().getId());
bankAccountTable_bankColumn.setCellValueFactory(param -> param.getValue().getBankId()); bankAccountTable_bankColumn.setCellValueFactory(param -> param.getValue().getBankId());
bankAccountTable_bankColumn.setCellFactory(param -> new BankTableCell<>(getBankService())); bankAccountTable_bankColumn.setCellFactory(BankTableCell.forTableColumn(getBankService()));
bankAccountTable_openingBankColumn.setCellValueFactory(param -> param.getValue().getOpeningBank()); bankAccountTable_openingBankColumn.setCellValueFactory(param -> param.getValue().getOpeningBank());
bankAccountTable_accountColumn.setCellValueFactory(param -> param.getValue().getAccount()); bankAccountTable_accountColumn.setCellValueFactory(param -> param.getValue().getAccount());

View File

@@ -1,20 +1,39 @@
package com.ecep.contract.controller.table.cell; package com.ecep.contract.controller.table.cell;
import com.ecep.contract.SpringApp;
import com.ecep.contract.service.BankService; import com.ecep.contract.service.BankService;
import com.ecep.contract.vo.BankVo; import com.ecep.contract.vo.BankVo;
import javafx.util.Callback;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import static com.ecep.contract.SpringApp.getBean;
/**
* 银行单元格
*/
@NoArgsConstructor @NoArgsConstructor
public class BankTableCell<T> extends AsyncUpdateTableCell<T, Integer, BankVo> { public class BankTableCell<T> extends AsyncUpdateTableCell<T, Integer, BankVo> {
/**
* 创建单元格工厂
*
* @param bankService 银行服务
* @return 单元格工厂
*/
public static <V> Callback<javafx.scene.control.TableColumn<V, Integer>, javafx.scene.control.TableCell<V, Integer>> forTableColumn(
BankService bankService) {
return param -> new BankTableCell<V>(bankService);
}
public BankTableCell(BankService service) { public BankTableCell(BankService service) {
setService(service); setService(service);
} }
@Override @Override
protected BankService getServiceBean() { protected BankService getServiceBean() {
return SpringApp.getBean(BankService.class); return getBean(BankService.class);
} }
@Override
public String format(BankVo entity) {
return getService().getStringConverter().toString(entity);
}
} }

View File

@@ -1,11 +1,13 @@
package com.ecep.contract.vm; package com.ecep.contract.vm;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Objects; import java.util.Objects;
import com.ecep.contract.util.NumberUtils; import com.ecep.contract.util.NumberUtils;
import com.ecep.contract.vo.ContractInvoiceVo; import com.ecep.contract.vo.ContractInvoiceVo;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import lombok.Data; import lombok.Data;
@@ -20,6 +22,11 @@ public class ContractInvoiceViewModel extends IdentityViewModel<ContractInvoiceV
* 关联的合同对象, Contract * 关联的合同对象, Contract
*/ */
private final SimpleObjectProperty<Integer> contract = new SimpleObjectProperty<>(); private final SimpleObjectProperty<Integer> contract = new SimpleObjectProperty<>();
/**
* 关联的合同项目对象, ContractItem
*/
private final SimpleObjectProperty<Integer> contractItem = new SimpleObjectProperty<>();
/** /**
* 关联的发票对象, Invoice * 关联的发票对象, Invoice
*/ */
@@ -46,7 +53,7 @@ public class ContractInvoiceViewModel extends IdentityViewModel<ContractInvoiceV
invoice.set(v.getInvoiceId()); invoice.set(v.getInvoiceId());
amount.set(v.getAmount() != null ? v.getAmount() : 0.0); amount.set(v.getAmount() != null ? v.getAmount() : 0.0);
remark.set(v.getRemark()); remark.set(v.getRemark());
createDate.set(v.getSetupDate());
updateDate.set(v.getUpdateDate()); updateDate.set(v.getUpdateDate());
creator.set(v.getSetupPersonId()); creator.set(v.getSetupPersonId());
updater.set(v.getUpdatePersonId()); updater.set(v.getUpdatePersonId());
@@ -85,23 +92,27 @@ public class ContractInvoiceViewModel extends IdentityViewModel<ContractInvoiceV
v.setRemark(remark.get()); v.setRemark(remark.get());
modified = true; modified = true;
} }
if (!Objects.equals(creator.get(), v.getSetupPersonId())) {
v.setSetupPersonId(creator.get());
modified = true;
}
if (!Objects.equals(updater.get(), v.getUpdatePersonId())) {
v.setUpdatePersonId(updater.get());
modified = true;
}
if (!Objects.equals(createDate.get(), v.getSetupDate())) { if (!Objects.equals(createDate.get(), v.getSetupDate())) {
v.setSetupDate(createDate.get()); v.setSetupDate(createDate.get());
modified = true; modified = true;
} }
if (!Objects.equals(updateDate.get(), v.getUpdateDate())) { if (!Objects.equals(updateDate.get(), v.getUpdateDate())) {
v.setUpdateDate(updateDate.get()); v.setUpdateDate(updateDate.get());
modified = true; modified = true;
} }
if (!Objects.equals(creator.get(), v.getSetupPersonId())) {
v.setSetupPersonId(creator.get());
modified = true;
}
if (!Objects.equals(updater.get(), v.getUpdatePersonId())) {
v.setUpdatePersonId(updater.get());
modified = true;
}
return modified; return modified;
} }

View File

@@ -0,0 +1 @@
# my.downloadsPath = C:\\Users\\SQQ\\Downloads\\

View File

@@ -20,7 +20,7 @@
<?import javafx.scene.paint.Color?> <?import javafx.scene.paint.Color?>
<?import javafx.scene.text.Font?> <?import javafx.scene.text.Font?>
<BorderPane fx:id="root" maxHeight="900" maxWidth="1024" minHeight="300" minWidth="200" prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.ecep.contract.ds.company.controller.contact.CompanyContactWindowController"> <BorderPane fx:id="root" maxHeight="900" maxWidth="1024" minHeight="300" minWidth="200" prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.ecep.contract.controller.company.CompanyContactWindowController">
<center> <center>
<TabPane fx:id="tabPane" tabClosingPolicy="UNAVAILABLE" tabMaxWidth="100.0" tabMinWidth="40.0"> <TabPane fx:id="tabPane" tabClosingPolicy="UNAVAILABLE" tabMaxWidth="100.0" tabMinWidth="40.0">
<tabs> <tabs>

View File

@@ -6,12 +6,12 @@
<parent> <parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId> <artifactId>Contract-Manager</artifactId>
<version>0.0.129-SNAPSHOT</version> <version>0.0.134-SNAPSHOT</version>
</parent> </parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>common</artifactId> <artifactId>common</artifactId>
<version>0.0.129-SNAPSHOT</version> <version>0.0.134-SNAPSHOT</version>
<properties> <properties>
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>

View File

@@ -1,5 +1,7 @@
#Contract Manager \u5E94\u7528\u7A0B\u5E8F\u914D\u7F6E #Contract Manager \u5E94\u7528\u7A0B\u5E8F\u914D\u7F6E
#Sat Sep 27 00:34:02 CST 2025 #Sat Sep 27 00:34:02 CST 2025
#server.host=cms.ecctrl.com
#server.port=80
server.host=127.0.0.1 server.host=127.0.0.1
server.port=8080 server.port=8080
user.name=qiqing.song user.name=qiqing.song

View File

@@ -10,7 +10,7 @@
</parent> </parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId> <artifactId>Contract-Manager</artifactId>
<version>0.0.129-SNAPSHOT</version> <version>0.0.134-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
<module>server</module> <module>server</module>

View File

@@ -6,12 +6,12 @@
<parent> <parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId> <artifactId>Contract-Manager</artifactId>
<version>0.0.129-SNAPSHOT</version> <version>0.0.134-SNAPSHOT</version>
</parent> </parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>server</artifactId> <artifactId>server</artifactId>
<version>0.0.129-SNAPSHOT</version> <version>0.0.134-SNAPSHOT</version>
<properties> <properties>
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
@@ -22,7 +22,7 @@
<dependency> <dependency>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>common</artifactId> <artifactId>common</artifactId>
<version>0.0.129-SNAPSHOT</version> <version>0.0.134-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -93,6 +93,12 @@
<artifactId>mssql-jdbc</artifactId> <artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<!-- SMBJ library for SMB/CIFS file operations -->
<dependency>
<groupId>com.hierynomus</groupId>
<artifactId>smbj</artifactId>
<version>0.11.5</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -1,6 +1,7 @@
package com.ecep.contract.cloud.u8.ctx; package com.ecep.contract.cloud.u8.ctx;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -50,6 +51,7 @@ import com.ecep.contract.ds.vendor.model.Vendor;
import com.ecep.contract.ds.vendor.model.VendorEntity; import com.ecep.contract.ds.vendor.model.VendorEntity;
import com.ecep.contract.ds.vendor.service.VendorEntityService; import com.ecep.contract.ds.vendor.service.VendorEntityService;
import com.ecep.contract.ds.vendor.service.VendorService; import com.ecep.contract.ds.vendor.service.VendorService;
import com.ecep.contract.service.SmbFileService;
import com.ecep.contract.util.BeanContext; import com.ecep.contract.util.BeanContext;
import com.ecep.contract.util.FileUtils; import com.ecep.contract.util.FileUtils;
import com.ecep.contract.util.NumberUtils; import com.ecep.contract.util.NumberUtils;
@@ -63,6 +65,7 @@ import com.ecep.contract.vo.CustomerVo;
import com.ecep.contract.vo.ProjectSaleTypeVo; import com.ecep.contract.vo.ProjectSaleTypeVo;
import com.ecep.contract.vo.ProjectVo; import com.ecep.contract.vo.ProjectVo;
import com.ecep.contract.vo.VendorEntityVo; import com.ecep.contract.vo.VendorEntityVo;
import com.hierynomus.smbj.common.SmbPath;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -995,13 +998,20 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
} }
public boolean updateContractPath(ContractVo contract, MessageHolder holder) { public boolean updateContractPath(ContractVo contract, MessageHolder holder) {
// 如果合同路径存在 // 如果设置了合同路径,检查路径是否存在
if (CompanyFileUtils.exists(contract.getPath())) { SmbFileService smbFileService = getCachedBean(SmbFileService.class);
File dir = new File(contract.getPath()); if (StringUtils.hasText(contract.getPath())) {
if (dir.exists()) { var smbPath = SmbPath.parse(contract.getPath());
return false; try {
if (smbFileService.exists(smbPath)) {
return false;
}
} catch (IOException e) {
holder.warn("检查合同路径 " + smbPath + " 异常:" + e.getMessage());
} }
return false;
} }
// 尝试创建合同路径 // 尝试创建合同路径
holder.debug("合同目录不存在,尝试创建"); holder.debug("合同目录不存在,尝试创建");
@@ -1022,15 +1032,17 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
return false; return false;
} }
File contractPath = new File(parent.getPath()); File contractPath = new File(parent.getPath());
if (!contractPath.exists()) { if (!smbFileService.exists(contractPath)) {
holder.debug("父合同 " + parentCode + " 目录不存在 " + parent.getPath()); holder.debug("父合同 " + parentCode + " 目录不存在 " + parent.getPath());
return false; return false;
} }
String contractDirName = contract.getCode(); String contractDirName = contract.getCode();
File path = new File(contractPath, contractDirName); File path = new File(contractPath, contractDirName);
if (!path.exists()) { if (!smbFileService.exists(path)) {
if (!path.mkdir()) { try {
holder.warn("创建目录失败 = " + path.getAbsolutePath()); smbFileService.mkdir(path);
} catch (IOException e) {
holder.warn("创建目录失败 = " + path.getAbsolutePath() + ", 异常: " + e.getMessage());
return false; return false;
} }
} }
@@ -1049,9 +1061,12 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
File dir = new File(saleType.getPath()); File dir = new File(saleType.getPath());
if (saleType.isStoreByYear()) { if (saleType.isStoreByYear()) {
dir = new File(dir, "20" + projectVo.getCodeYear()); dir = new File(dir, "20" + projectVo.getCodeYear());
if (!dir.exists()) { if (!smbFileService.exists(dir)) {
if (dir.mkdir()) { try {
holder.info("新建目录 " + dir.getAbsolutePath()); smbFileService.mkdir(dir);
} catch (IOException e) {
holder.warn("创建目录失败 = " + dir.getAbsolutePath() + ", 异常: " + e.getMessage());
return false;
} }
} }
} }
@@ -1074,10 +1089,11 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
String contractDirName = contract.getCode() + "-" + FileUtils.escapeFileName(contract.getName()); String contractDirName = contract.getCode() + "-" + FileUtils.escapeFileName(contract.getName());
File path = new File(contractPath, contractDirName); File path = new File(contractPath, contractDirName);
if (!path.exists()) { if (!smbFileService.exists(path)) {
if (!path.mkdir()) { try {
// smbFileService.mkdir(path);
holder.warn("创建目录失败 = " + path.getAbsolutePath()); } catch (IOException e) {
holder.warn("创建目录失败 = " + path.getAbsolutePath() + ", 异常: " + e.getMessage());
return false; return false;
} }
} }
@@ -1086,6 +1102,7 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
return true; return true;
} }
return false; return false;
} }
public boolean updateContractAmount(ContractVo contract, MessageHolder holder) { public boolean updateContractAmount(ContractVo contract, MessageHolder holder) {
@@ -1190,8 +1207,10 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
if (!StringUtils.hasText(contractPath)) { if (!StringUtils.hasText(contractPath)) {
return false; return false;
} }
SmbFileService smbFileService = getCachedBean(SmbFileService.class);
// 检查合同目录是否存在
File dir = new File(contractPath); File dir = new File(contractPath);
if (!dir.exists()) { if (!smbFileService.exists(dir)) {
holder.warn("合同目录不存在:" + contractPath); holder.warn("合同目录不存在:" + contractPath);
return false; return false;
} }
@@ -1228,11 +1247,11 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
} }
// 遍历合同目录下的文件,如果未创建,创建 // 遍历合同目录下的文件,如果未创建,创建
File[] files = dir.listFiles(); try {
if (files != null) { List<File> files = smbFileService.listFiles(dir);
for (File file : files) { for (File file : files) {
// 只处理文件 // 只处理文件
if (!file.isFile()) { if (!smbFileService.isFile(file)) {
continue; continue;
} }
String fileName = file.getName(); String fileName = file.getName();
@@ -1247,6 +1266,9 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
syncContractFile(contractFile, file, holder); syncContractFile(contractFile, file, holder);
retrieveFiles.add(contractFile); retrieveFiles.add(contractFile);
} }
} catch (IOException e) {
holder.error("遍历合同目录下的文件失败:" + contractPath + ",错误:" + e.getMessage());
return false;
} }
if (retrieveFiles.isEmpty()) { if (retrieveFiles.isEmpty()) {

View File

@@ -253,8 +253,10 @@ public class CustomerCtx extends AbstractYongYouU8Ctx {
updated = true; updated = true;
} }
ContractCtx ctx = getContractCtx();
ctx.initializeRepository(holder);
for (CompanyCustomerEntity entity : entities) { for (CompanyCustomerEntity entity : entities) {
if (getContractCtx().syncByCustomerEntity(companyCustomer, entity, holder)) { if (ctx.syncByCustomerEntity(companyCustomer, entity, holder)) {
updated = true; updated = true;
} }
} }

View File

@@ -249,8 +249,10 @@ public class VendorCtx extends AbstractYongYouU8Ctx {
} }
// 同步供应商关联的合同 // 同步供应商关联的合同
ContractCtx ctx = getContractCtx();
ctx.initializeRepository(holder);
for (VendorEntity entity : entities) { for (VendorEntity entity : entities) {
if (getContractCtx().syncByVendorEntity(vendor, entity, holder)) { if (ctx.syncByVendorEntity(vendor, entity, holder)) {
updated = true; updated = true;
} }
} }

View File

@@ -0,0 +1,30 @@
package com.ecep.contract.config;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.hierynomus.smbj.SMBClient;
import com.hierynomus.smbj.auth.NtlmAuthenticator;
@Configuration()
public class SmbConfig {
@Value("${smb.server.username}")
@Getter
private String username;
@Value("${smb.server.password}")
@Getter
private String password;
@Bean
public SMBClient smbClient() {
var smbConfig = com.hierynomus.smbj.SmbConfig.builder()
.withMultiProtocolNegotiate(true).withSigningRequired(true)
// .withAuthenticators(new NtlmAuthenticator(username, password))
.build();
return new SMBClient(smbConfig);
}
}

View File

@@ -34,4 +34,7 @@ public class WebSocketConfig implements WebSocketConfigurer {
new HttpSessionHandshakeInterceptor(List.of("JSESSIONID", "loginHistoryId", "employeeId"))) new HttpSessionHandshakeInterceptor(List.of("JSESSIONID", "loginHistoryId", "employeeId")))
.setAllowedOrigins("*"); .setAllowedOrigins("*");
} }
} }

View File

@@ -1,6 +1,7 @@
package com.ecep.contract.ds.company; package com.ecep.contract.ds.company;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@@ -9,12 +10,17 @@ import java.util.function.Function;
import com.ecep.contract.constant.CompanyConstant; import com.ecep.contract.constant.CompanyConstant;
import com.ecep.contract.util.FileUtils; import com.ecep.contract.util.FileUtils;
import com.hierynomus.smbj.common.SmbPath;
import org.springframework.beans.BeansException;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.ecep.contract.CompanyFileType; import com.ecep.contract.CompanyFileType;
import com.ecep.contract.MyDateTimeUtils; import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.SpringApp;
import com.ecep.contract.constant.CloudServiceConstant; import com.ecep.contract.constant.CloudServiceConstant;
import com.ecep.contract.ds.company.model.CompanyFile; import com.ecep.contract.ds.company.model.CompanyFile;
import com.ecep.contract.service.SmbFileService;
public class CompanyFileUtils { public class CompanyFileUtils {
@@ -130,10 +136,24 @@ public class CompanyFileUtils {
return modified; return modified;
} }
public static boolean exists(SmbPath path) throws IOException {
return SpringApp.getBean(SmbFileService.class).exists(path);
}
public static boolean exists(String path) { public static boolean exists(String path) {
if (!StringUtils.hasText(path)) { if (!StringUtils.hasText(path)) {
return false; return false;
} }
if (path.startsWith("\\\\")) {
var smbPath = SmbPath.parse(path);
try {
return SpringApp.getBean(SmbFileService.class).exists(smbPath);
} catch (IOException e) {
return false;
}
}
File file = new File(path); File file = new File(path);
return file.exists(); return file.exists();
} }

View File

@@ -287,6 +287,7 @@ public abstract class AbstContractRepairTasker extends Tasker<Object> {
SalesOrderCtx ctx = getSalesOrderCtx(); SalesOrderCtx ctx = getSalesOrderCtx();
ctx.initializeRepository(holder); ctx.initializeRepository(holder);
if (ctx.getRepository() == null) { if (ctx.getRepository() == null) {
holder.warn("未启用 " + CloudServiceConstant.U8_NAME + " 服务");
return; return;
} }
List<SalesOrder> orders = ctx.syncByContract(contract, holder); List<SalesOrder> orders = ctx.syncByContract(contract, holder);
@@ -294,6 +295,11 @@ public abstract class AbstContractRepairTasker extends Tasker<Object> {
return; return;
} }
SalesBillVoucherCtx voucherCtx = getSalesBillVoucherCtx(); SalesBillVoucherCtx voucherCtx = getSalesBillVoucherCtx();
voucherCtx.initializeRepository(holder);
if (voucherCtx.getRepository() == null) {
holder.warn("未启用 " + CloudServiceConstant.U8_NAME + " 服务");
return;
}
for (SalesOrder order : orders) { for (SalesOrder order : orders) {
voucherCtx.syncBySalesOrder(order, holder); voucherCtx.syncBySalesOrder(order, holder);
} }

View File

@@ -0,0 +1,493 @@
package com.ecep.contract.service;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ecep.contract.SpringApp;
import com.ecep.contract.config.SmbConfig;
import com.hierynomus.msdtyp.AccessMask;
import com.hierynomus.mserref.NtStatus;
import com.hierynomus.msfscc.FileAttributes;
import com.hierynomus.msfscc.fileinformation.FileAllInformation;
import com.hierynomus.mssmb2.SMB2CreateDisposition;
import com.hierynomus.mssmb2.SMB2CreateOptions;
import com.hierynomus.mssmb2.SMB2ShareAccess;
import com.hierynomus.mssmb2.SMBApiException;
import com.hierynomus.smbj.SMBClient;
import com.hierynomus.smbj.auth.AuthenticationContext;
import com.hierynomus.smbj.common.SmbPath;
import com.hierynomus.smbj.share.DiskShare;
import com.hierynomus.smbj.share.File;
import lombok.extern.slf4j.Slf4j;
/**
* SMB文件服务类提供SMB/CIFS协议的文件操作功能
*/
@Slf4j
@Service
public class SmbFileService implements DisposableBean {
private final SMBClient client;
private AuthenticationContext authContext;
private final SmbConfig smbConfig;
private final ReentrantLock authLock = new ReentrantLock();
// 连接空闲超时时间3分钟
private static final long CONNECTION_IDLE_TIMEOUT_MS = 3 * 60 * 1000;
// 连接信息内部类,用于存储连接和最后使用时间
private static class ConnectionInfo {
private final com.hierynomus.smbj.connection.Connection connection;
private volatile long lastUsedTimestamp;
public ConnectionInfo(com.hierynomus.smbj.connection.Connection connection) {
this.connection = connection;
this.lastUsedTimestamp = System.currentTimeMillis();
}
public com.hierynomus.smbj.connection.Connection getConnection() {
return connection;
}
public long getLastUsedTimestamp() {
return lastUsedTimestamp;
}
public void updateLastUsedTimestamp() {
this.lastUsedTimestamp = System.currentTimeMillis();
}
public boolean isIdle(long timeoutMs) {
return (System.currentTimeMillis() - lastUsedTimestamp) > timeoutMs;
}
}
// 连接池使用ConcurrentHashMap确保线程安全
private final Map<String, ConnectionInfo> connectionPool = new ConcurrentHashMap<>();
// 连接池锁,用于同步连接的创建和关闭
private final ReentrantLock connectionPoolLock = new ReentrantLock();
// 定时清理线程池
private final ScheduledExecutorService cleanupScheduler;
/**
* 构造函数注入SMB客户端和配置初始化定时清理任务
*
* @param smbClient SMB客户端实例
*/
public SmbFileService(@Autowired SMBClient smbClient, @Autowired ScheduledExecutorService executor) {
this.client = smbClient;
this.smbConfig = SpringApp.getBean(SmbConfig.class);
// 初始化定时清理任务每30秒运行一次
this.cleanupScheduler = executor;
// 启动定时清理任务延迟1分钟后开始每30秒执行一次
this.cleanupScheduler.scheduleAtFixedRate(this::cleanupIdleConnections, 1, 30, TimeUnit.SECONDS);
}
/**
* 获取认证上下文,线程安全实现
*
* @param host 主机名
* @return 认证上下文
*/
private AuthenticationContext getAuthenticationContext(String host) {
// 双重检查锁定模式,确保线程安全
if (authContext == null) {
authLock.lock();
try {
if (authContext == null) {
log.debug("Creating new AuthenticationContext for host: {}", host);
authContext = new AuthenticationContext(
smbConfig.getUsername(),
smbConfig.getPassword().toCharArray(),
"");
}
} finally {
authLock.unlock();
}
}
return authContext;
}
/**
* 执行SMB操作的通用方法简化连接和会话的创建
*
* @param smbPath SMB路径
* @param operation 要执行的操作
* @param <T> 操作返回类型
* @return 操作结果
* @throws IOException 如果操作失败
*/
/**
* 从连接池获取或创建连接
*
* @param hostname 主机名
* @return SMB连接
* @throws IOException 如果创建连接失败
*/
/**
* 从连接池获取或创建连接
*
* @param hostname 主机名
* @return SMB连接
* @throws IOException 如果创建连接失败
*/
private com.hierynomus.smbj.connection.Connection getConnection(String hostname) throws IOException {
// 首先检查连接池是否已有该主机的连接
ConnectionInfo connectionInfo = connectionPool.get(hostname);
com.hierynomus.smbj.connection.Connection connection = null;
// 如果连接存在且有效,则更新最后使用时间并返回
if (connectionInfo != null) {
connection = connectionInfo.getConnection();
if (connection != null && connection.isConnected()) {
// 更新连接的最后使用时间
connectionInfo.updateLastUsedTimestamp();
log.debug("Reusing SMB connection for host: {}", hostname);
return connection;
}
}
// 如果连接不存在或已关闭,则创建新连接
connectionPoolLock.lock();
try {
// 双重检查锁定模式
connectionInfo = connectionPool.get(hostname);
if (connectionInfo != null) {
connection = connectionInfo.getConnection();
if (connection != null && connection.isConnected()) {
connectionInfo.updateLastUsedTimestamp();
log.debug("Reusing SMB connection for host: {}", hostname);
return connection;
}
// 如果连接已失效,从池中移除
connectionPool.remove(hostname);
}
// 创建新连接
log.debug("Creating new SMB connection for host: {}", hostname);
connection = client.connect(hostname);
connectionInfo = new ConnectionInfo(connection);
connectionPool.put(hostname, connectionInfo);
} finally {
connectionPoolLock.unlock();
}
return connection;
}
/**
* 清理空闲连接的定时任务
*/
private void cleanupIdleConnections() {
log.debug("Running idle connections cleanup task");
// 创建要移除的连接列表避免在迭代时修改Map
List<String> idleHostnames = new java.util.ArrayList<>();
// 查找所有空闲连接
for (Map.Entry<String, ConnectionInfo> entry : connectionPool.entrySet()) {
String hostname = entry.getKey();
ConnectionInfo connectionInfo = entry.getValue();
// 检查连接是否空闲超时
if (connectionInfo != null && connectionInfo.isIdle(CONNECTION_IDLE_TIMEOUT_MS)) {
idleHostnames.add(hostname);
log.debug("Found idle connection for host: {}, will be closed", hostname);
}
}
// 关闭并移除空闲连接
if (!idleHostnames.isEmpty()) {
connectionPoolLock.lock();
try {
for (String hostname : idleHostnames) {
ConnectionInfo connectionInfo = connectionPool.get(hostname);
if (connectionInfo != null) {
try {
log.debug("Closing idle connection for host: {}", hostname);
connectionInfo.getConnection().close();
} catch (IOException e) {
log.error("Error closing idle connection for host: {}", hostname, e);
}
connectionPool.remove(hostname);
}
}
} finally {
connectionPoolLock.unlock();
}
}
log.debug("Idle connections cleanup completed, closed {} connections", idleHostnames.size());
}
/**
* 执行SMB操作的通用方法使用连接池
*
* @param smbPath SMB路径
* @param operation 要执行的操作
* @param <T> 操作返回类型
* @return 操作结果
* @throws IOException 如果操作失败
*/
private <T> T executeSmbOperation(SmbPath smbPath, SmbOperation<T> operation) throws IOException {
String hostname = smbPath.getHostname();
com.hierynomus.smbj.connection.Connection connection = null;
try {
// 从连接池获取连接
connection = getConnection(hostname);
// 使用获取的连接进行身份验证
var session = connection.authenticate(getAuthenticationContext(hostname));
try (var share = (DiskShare) session.connectShare(smbPath.getShareName())) {
return operation.execute(share, smbPath.getPath());
}
} catch (IOException e) {
// 如果操作失败且连接存在,检查连接状态
if (connection != null && !connection.isConnected()) {
// 从连接池移除失效的连接
connectionPool.remove(hostname);
}
throw e;
}
}
/**
* 上传文件到SMB服务器
*
* @param filePath 文件路径
* @param fileContent 文件内容字节数组
* @throws IOException 如果上传失败
*/
public void uploadFile(String filePath, byte[] fileContent) throws IOException {
Objects.requireNonNull(filePath, "File path cannot be null");
Objects.requireNonNull(fileContent, "File content cannot be null");
log.debug("Uploading file: {} with size: {} bytes", filePath, fileContent.length);
var smbPath = SmbPath.parse(filePath);
executeSmbOperation(smbPath, (share, path) -> {
// 创建目录(如果不存在)
String directoryPath = path.substring(0, path.lastIndexOf('/'));
if (!share.folderExists(directoryPath)) {
share.mkdir(directoryPath);
log.debug("Created directory: {}", directoryPath);
}
// 上传文件内容
try (File smbFile = share.openFile(smbPath.getPath(),
EnumSet.of(AccessMask.GENERIC_WRITE),
EnumSet.of(FileAttributes.FILE_ATTRIBUTE_NORMAL),
EnumSet.of(SMB2ShareAccess.FILE_SHARE_WRITE),
SMB2CreateDisposition.FILE_CREATE,
EnumSet.noneOf(SMB2CreateOptions.class))) {
try (OutputStream out = smbFile.getOutputStream()) {
out.write(fileContent);
}
}
log.info("Successfully uploaded file: {}", filePath);
return null;
});
}
/**
* 检查SMB路径是否存在
*
* @param smbPath SMB路径
* @return 如果路径存在则返回true
* @throws IOException 如果检查失败
*/
public boolean exists(SmbPath smbPath) throws IOException {
return executeSmbOperation(smbPath, (share, path) -> {
try {
FileAllInformation info = share.getFileInformation(path);
if (info.getStandardInformation().isDirectory()) {
return share.folderExists(path);
}
return share.fileExists(path);
} catch (SMBApiException e) {
if (e.getStatus().equals(NtStatus.STATUS_OBJECT_NAME_NOT_FOUND)) {
return false;
}
log.error("Error checking if path exists: {}", path, e);
throw e;
}
});
}
/**
* 检查文件是否存在基于File对象
*
* @param file 文件对象
* @return 如果文件存在则返回true
*/
public boolean exists(java.io.File file) {
Objects.requireNonNull(file, "File cannot be null");
try {
var smbPath = SmbPath.parse(file.getAbsolutePath());
return exists(smbPath);
} catch (IOException e) {
log.error("Error checking if file exists: {}", file.getAbsolutePath(), e);
return false;
}
}
/**
* 创建目录
*
* @param path 要创建的目录路径
* @throws IOException 如果创建失败
*/
public void mkdir(java.io.File path) throws IOException {
Objects.requireNonNull(path, "Path cannot be null");
var smbPath = SmbPath.parse(path.getAbsolutePath());
executeSmbOperation(smbPath, (share, smbFilePath) -> {
if (!share.folderExists(smbFilePath)) {
share.mkdir(smbFilePath);
log.debug("Created directory: {}", smbFilePath);
}
return null;
});
}
/**
* 列出目录中的文件(不包括子目录)
*
* @param dir 目录对象
* @return 文件列表
* @throws IOException 如果列出失败
*/
public List<java.io.File> listFiles(java.io.File dir) throws IOException {
Objects.requireNonNull(dir, "Directory cannot be null");
var smbPath = SmbPath.parse(dir.getAbsolutePath());
return executeSmbOperation(smbPath, (share, path) -> {
try {
FileAllInformation info = share.getFileInformation(path);
if (info.getStandardInformation().isDirectory() && share.folderExists(path)) {
var files = share.list(path);
return files.stream()
.filter(f -> !f.getFileName().startsWith(".") && !f.getFileName().equals(".."))
.filter(f -> {
try {
String fullPath = path + "\\" + f.getFileName();
return !share.getFileInformation(fullPath).getStandardInformation().isDirectory();
} catch (SMBApiException e) {
log.warn("Error checking file type for: {}", f.getFileName(), e);
return false;
}
})
.map(f -> new java.io.File(dir, f.getFileName()))
.toList();
}
} catch (SMBApiException e) {
log.error("Error listing files in directory: {}", path, e);
}
return Collections.emptyList();
});
}
/**
* 检查是否为文件(非目录)
* 修复:之前的实现逻辑错误,现在正确返回是否为文件
*
* @param file 文件对象
* @return 如果是文件非目录则返回true
* @throws IOException 如果检查失败
*/
public boolean isFile(java.io.File file) throws IOException {
Objects.requireNonNull(file, "File cannot be null");
var smbPath = SmbPath.parse(file.getAbsolutePath());
return executeSmbOperation(smbPath, (share, path) -> {
FileAllInformation fileInformation = share.getFileInformation(path);
// 修复:返回是否不是目录,即是否为文件
return !fileInformation.getStandardInformation().isDirectory();
});
}
/**
* 检查是否为目录
*
* @param file 文件对象
* @return 如果是目录则返回true
* @throws IOException 如果检查失败
*/
public boolean isDirectory(java.io.File file) throws IOException {
Objects.requireNonNull(file, "File cannot be null");
var smbPath = SmbPath.parse(file.getAbsolutePath());
return executeSmbOperation(smbPath, (share, path) -> {
FileAllInformation fileInformation = share.getFileInformation(path);
return fileInformation.getStandardInformation().isDirectory();
});
}
@Override
public void destroy() throws Exception {
shutdown();
}
/**
* 关闭并清理所有连接资源
*/
public void shutdown() {
log.debug("Shutting down SMB connection pool");
// 关闭定时清理任务
try {
cleanupScheduler.shutdown();
if (!cleanupScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupScheduler.shutdownNow();
}
} catch (InterruptedException e) {
cleanupScheduler.shutdownNow();
Thread.currentThread().interrupt();
}
// 关闭所有连接
connectionPoolLock.lock();
try {
for (Map.Entry<String, ConnectionInfo> entry : connectionPool.entrySet()) {
try {
log.debug("Closing connection to host: {}", entry.getKey());
entry.getValue().getConnection().close();
} catch (IOException e) {
log.error("Error closing connection to host: {}", entry.getKey(), e);
}
}
connectionPool.clear();
// 关闭SMB客户端
client.close();
} finally {
connectionPoolLock.unlock();
}
}
/**
* SMB操作接口用于执行具体的SMB操作
*/
@FunctionalInterface
private interface SmbOperation<T> {
T execute(DiskShare share, String path) throws IOException;
}
}

View File

@@ -25,9 +25,21 @@ spring.jpa.show-sql=false
spring.data.jpa.repositories.enabled=true spring.data.jpa.repositories.enabled=true
spring.lifecycle.timeout-per-shutdown-phase=10s spring.lifecycle.timeout-per-shutdown-phase=10s
# my.downloadsPath = C:\\Users\\SQQ\\Downloads\\
spring.data.redis.host=10.84.209.229 spring.data.redis.client-type=lettuce
spring.data.redis.host=10.84.210.110
spring.data.redis.port=6379
spring.data.redis.username=default
spring.data.redis.password=redis_NFYa7z
spring.data.redis.database=3 spring.data.redis.database=3
spring.data.redis.repositories.enabled=false
# Redis连接池和容错配置
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-wait=10000
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
spring.data.redis.timeout=60000
logging.level.org.hibernate.tool.hbm2ddl=DEBUG logging.level.org.hibernate.tool.hbm2ddl=DEBUG
@@ -53,3 +65,6 @@ spring.cache.redis.cache-null-values=true
server.error.whitelabel.enabled=false server.error.whitelabel.enabled=false
# 设置错误处理路径确保404等错误能被全局异常处理器捕获 # 设置错误处理路径确保404等错误能被全局异常处理器捕获
spring.web.resources.add-mappings=true spring.web.resources.add-mappings=true
smb.server.username=qiqing.song
smb.server.password=huez8310

View File

@@ -0,0 +1,127 @@
package com.ecep.contract.util;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Redis连接测试类
* 用于测试基于application.properties中配置的Redis连接参数是否能正常连接Redis服务器
*/
@SpringJUnitConfig
@SpringBootTest
public class RedisConnectionTest {
private static final Logger logger = LoggerFactory.getLogger(RedisConnectionTest.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* 测试Redis连接是否正常
* 验证StringRedisTemplate是否成功注入并且能够执行基本的Redis操作
*/
@Test
public void testRedisConnection() {
logger.info("开始测试Redis连接...");
// 验证RedisConnectionFactory是否成功注入
assertNotNull(redisConnectionFactory, "RedisConnectionFactory注入失败");
logger.info("RedisConnectionFactory注入成功");
// 验证StringRedisTemplate是否成功注入
assertNotNull(stringRedisTemplate, "StringRedisTemplate注入失败");
logger.info("StringRedisTemplate注入成功");
try {
// 尝试获取原生连接以验证连接是否正常
logger.info("尝试获取Redis原生连接...");
redisConnectionFactory.getConnection().close();
logger.info("成功获取并关闭Redis原生连接");
// 执行一个简单的Redis操作来验证连接是否正常
String testKey = "test:connection";
String testValue = "connection-test-value";
logger.info("执行Redis写操作key: {}", testKey);
// 写入测试数据
stringRedisTemplate.opsForValue().set(testKey, testValue);
logger.info("Redis写操作成功");
logger.info("执行Redis读操作key: {}", testKey);
// 读取测试数据并验证
String retrievedValue = stringRedisTemplate.opsForValue().get(testKey);
logger.info("Redis读操作成功获取到值: {}", retrievedValue);
assertTrue(testValue.equals(retrievedValue), "Redis写入和读取的值不匹配");
logger.info("Redis读写值验证成功");
// 清理测试数据
stringRedisTemplate.delete(testKey);
logger.info("Redis删除操作成功");
logger.info("Redis连接测试成功能够正常执行set、get和delete操作");
} catch (Exception e) {
// 记录详细的异常信息
logger.error("Redis连接测试失败: {}", e.getMessage(), e);
// 打印异常堆栈以方便调试
e.printStackTrace();
// 测试失败,但提供详细信息
assertTrue(false, "Redis连接测试失败: " + e.getMessage() + ", 请检查Redis服务器是否运行以及配置是否正确");
}
}
/**
* 测试Redis连接池状态
* 验证Redis连接池是否正常工作
*/
@Test
public void testRedisConnectionPool() {
logger.info("开始测试Redis连接池...");
assertNotNull(stringRedisTemplate, "StringRedisTemplate注入失败");
logger.info("StringRedisTemplate注入成功");
try {
// 执行多次Redis操作来测试连接池
logger.info("执行多次Redis操作以测试连接池...");
for (int i = 0; i < 10; i++) {
String testKey = "test:pool:" + i;
String testValue = "pool-test-value:" + i;
logger.info("第{}次测试 - 执行Redis写操作key: {}", i, testKey);
// 写入数据
stringRedisTemplate.opsForValue().set(testKey, testValue);
logger.info("第{}次测试 - 执行Redis读操作key: {}", i, testKey);
// 读取并验证
String retrievedValue = stringRedisTemplate.opsForValue().get(testKey);
logger.info("第{}次测试 - 获取到值: {}", i, retrievedValue);
assertTrue(testValue.equals(retrievedValue), "Redis连接池测试失败" + i + "次操作出错");
logger.info("第{}次测试 - 值验证成功", i);
// 清理数据
stringRedisTemplate.delete(testKey);
logger.info("第{}次测试 - 删除操作成功", i);
}
logger.info("Redis连接池测试成功能够正常处理多个连接请求");
} catch (Exception e) {
logger.error("Redis连接池测试失败: {}", e.getMessage(), e);
e.printStackTrace();
assertTrue(false, "Redis连接池测试失败: " + e.getMessage() + ", 请检查Redis连接池配置和服务器状态");
}
}
}