Compare commits

...

3 Commits

Author SHA1 Message Date
b03b5385a5 refactor(service): 修改IEntityService泛型为VO类型并优化缓存策略
重构所有注解@CacheConfig的Service类,将IEntityService泛型从实体类改为VO类
实现实体与VO之间的转换逻辑,使用VO替代实体进行缓存以避免序列化问题
更新相关依赖组件和测试用例,确保功能完整性和系统兼容性
优化Redis缓存配置,清理旧缓存数据并验证新缓存策略有效性
2025-09-28 18:19:00 +08:00
df6188db40 feat: 新增WebSocket任务管理器及相关任务实现
实现WebSocketServerTaskManager用于管理WebSocket任务,并添加多个任务类:
- CompanyContext和CloudRkContext接口定义
- WebSocketServerTasker接口及多个具体任务实现类
- ContractVerifyTasker合同验证任务
- ContractRepairTasker合同修复任务
- CompanyCustomerRebuildFilesTasker客户文件重建任务
- CompanyVerifyTasker企业验证任务
- CustomerFileMoveTasker客户文件移动任务
- CompanyCompositeUpdateTasker企业综合更新任务
- ProjectCostImportItemsFromContractsTasker项目成本导入任务
- 其他相关辅助任务类

这些任务类通过WebSocket与前端交互,实现各种业务功能
2025-09-28 18:18:32 +08:00
510952d72e feat: 添加企业文件管理功能及相关任务类
refactor: 重构企业文件验证和移动逻辑

fix: 修复企业合规验证逻辑及路径处理问题

docs: 添加VerifyContext工具类及相关文档

style: 优化代码格式及注释
2025-09-26 19:40:34 +08:00
98 changed files with 4590 additions and 2394 deletions

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.86-SNAPSHOT</version> <version>0.0.99-SNAPSHOT</version>
</parent> </parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>client</artifactId> <artifactId>client</artifactId>
<version>0.0.86-SNAPSHOT</version> <version>0.0.99-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.86-SNAPSHOT</version> <version>0.0.99-SNAPSHOT</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -120,6 +120,23 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/relativePath/client/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>

View File

@@ -2,11 +2,14 @@ package com.ecep.contract;
import javafx.application.Application; import javafx.application.Application;
import java.io.File;
/** /**
* Created by Administrator on 2017/4/16. * Created by Administrator on 2017/4/16.
*/ */
public class ClientV2 { public class ClientV2 {
public static void main(String[] args) { public static void main(String[] args) {
System.out.println("当前目录 = " + new File(".").getAbsolutePath());
Application.launch(Desktop.class, args); Application.launch(Desktop.class, args);
} }

View File

@@ -203,7 +203,9 @@ public class Desktop extends Application {
controller.setHttpClient(this.httpClient); controller.setHttpClient(this.httpClient);
controller.setHolder(holder); controller.setHolder(holder);
controller.setPrimaryStage(primaryStage); controller.setPrimaryStage(primaryStage);
controller.setProperties(properties); MyProperties myProperties = new MyProperties();
myProperties.loadFromProperties(properties);
controller.setProperties(myProperties);
while (true) { while (true) {
try { try {
controller.tryLogin().get(); controller.tryLogin().get();

View File

@@ -1,22 +1,192 @@
package com.ecep.contract; package com.ecep.contract;
import lombok.Getter; import java.io.File;
import lombok.Setter; import java.io.FileInputStream;
import org.springframework.boot.context.properties.ConfigurationProperties; import java.io.FileOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.io.File; import lombok.Getter;
import java.nio.file.Path; import lombok.Setter;
import java.nio.file.Paths;
/**
* 应用程序配置类,用于管理系统配置信息
*/
@Component @Component
@ConfigurationProperties(prefix = "my") public class MyProperties implements InitializingBean {
public class MyProperties { private static final Logger logger = LoggerFactory.getLogger(MyProperties.class);
private static final String FILE_NAME = "config.properties";
@Getter @Getter
@Setter @Setter
private String downloadsPath; private String downloadsPath;
@Getter
@Setter
private String serverHost;
@Getter
@Setter
private String serverPort;
@Getter
@Setter
private String userName;
@Getter
@Setter
private String password;
@Getter
@Setter
private boolean rememberPassword;
@Override
public void afterPropertiesSet() throws Exception {
// 初始化时加载配置文件
try {
loadFromFile();
} catch (Exception e) {
logger.warn("初始化配置文件失败: {}", e.getMessage());
}
}
/**
* 从文件加载配置
*/
public void loadFromFile() {
File configFile = new File(FILE_NAME);
if (!configFile.exists()) {
logger.debug("配置文件不存在: {}", configFile.getPath());
return;
}
try (FileInputStream input = new FileInputStream(configFile)) {
Properties properties = new Properties();
properties.load(input);
loadFromProperties(properties);
logger.debug("成功从配置文件加载配置: {}", configFile.getPath());
} catch (Exception e) {
logger.error("加载配置文件失败: {}", configFile.getPath(), e);
}
}
/**
* 设置Properties对象并加载配置
* 用于从外部设置配置项
*
* @param properties 配置对象
*/
public void setProperties(Properties properties) {
this.loadFromProperties(properties);
}
/**
* 从Properties对象加载配置
* 用于从config.properties文件中读取配置项
*
* @param properties 配置对象
*/
public void loadFromProperties(Properties properties) {
if (properties == null) {
logger.warn("Properties对象为空无法加载配置");
return;
}
// 加载下载路径配置
String downloadsPath = properties.getProperty("my.downloadsPath");
if (StringUtils.hasText(downloadsPath)) {
this.setDownloadsPath(downloadsPath);
logger.debug("从配置文件加载下载路径: {}", downloadsPath);
}
// 加载服务器配置
String serverHost = properties.getProperty("server.host", "127.0.0.1");
if (StringUtils.hasText(serverHost)) {
this.setServerHost(serverHost);
logger.debug("从配置文件加载服务器地址: {}", serverHost);
}
String serverPort = properties.getProperty("server.port", "8080");
if (StringUtils.hasText(serverPort)) {
this.setServerPort(serverPort);
logger.debug("从配置文件加载服务器端口: {}", serverPort);
}
// 加载用户凭证配置
String userName = properties.getProperty("user.name");
if (StringUtils.hasText(userName)) {
this.setUserName(userName);
logger.debug("从配置文件加载用户名");
}
// 只有在记住密码的情况下才加载密码
String rememberPasswordStr = properties.getProperty("user.rememberPassword");
boolean rememberPassword = "true".equals(rememberPasswordStr);
this.setRememberPassword(rememberPassword);
if (rememberPassword) {
String password = properties.getProperty("user.password");
if (StringUtils.hasText(password)) {
this.setPassword(password);
logger.debug("从配置文件加载密码");
}
}
}
/**
* 将配置保存到Properties对象
* 用于将配置项保存到config.properties文件
*
* @param properties 配置对象
*/
public void saveToProperties(Properties properties) {
if (properties == null) {
logger.warn("Properties对象为空无法保存配置");
return;
}
// 保存下载路径配置
if (StringUtils.hasText(getDownloadsPath())) {
properties.setProperty("my.downloadsPath", getDownloadsPath());
logger.debug("保存下载路径到配置文件: {}", getDownloadsPath());
}
// 保存服务器配置
if (StringUtils.hasText(getServerHost())) {
properties.setProperty("server.host", getServerHost());
logger.debug("保存服务器地址到配置文件: {}", getServerHost());
}
if (StringUtils.hasText(getServerPort())) {
properties.setProperty("server.port", getServerPort());
logger.debug("保存服务器端口到配置文件: {}", getServerPort());
}
// 保存用户凭证配置
if (StringUtils.hasText(getUserName())) {
properties.setProperty("user.name", getUserName());
logger.debug("保存用户名为配置文件");
}
properties.setProperty("user.rememberPassword", String.valueOf(isRememberPassword()));
// 只有在记住密码的情况下才保存密码
if (isRememberPassword() && StringUtils.hasText(getPassword())) {
properties.setProperty("user.password", getPassword());
logger.debug("保存密码到配置文件");
} else if (properties.containsKey("user.password")) {
// 如果不记住密码,删除已存在的密码配置
properties.remove("user.password");
}
}
/** /**
* 尝试返回当前用户的下载文件夹 * 尝试返回当前用户的下载文件夹
@@ -24,7 +194,13 @@ public class MyProperties {
public File getDownloadDirectory() { public File getDownloadDirectory() {
String downloadsPath = getDownloadsPath(); String downloadsPath = getDownloadsPath();
if (StringUtils.hasText(downloadsPath)) { if (StringUtils.hasText(downloadsPath)) {
return new File(downloadsPath); // 确保目录存在
File dir = new File(downloadsPath);
if (!dir.exists()) {
boolean created = dir.mkdirs();
logger.debug("创建下载目录: {}, 结果: {}", downloadsPath, created);
}
return dir;
} }
// 没有配置下载目录时,尝试使用默认设置 // 没有配置下载目录时,尝试使用默认设置
@@ -32,4 +208,29 @@ public class MyProperties {
Path path = Paths.get(home, "Downloads"); Path path = Paths.get(home, "Downloads");
return path.toFile(); return path.toFile();
} }
/**
* 保存配置到文件
*/
public void save() {
File configFile = new File(FILE_NAME);
try (FileOutputStream output = new FileOutputStream(configFile)) {
Properties properties = new Properties();
// 如果文件已存在,先读取现有配置,避免覆盖其他配置
if (configFile.exists()) {
try (FileInputStream input = new FileInputStream(configFile)) {
properties.load(input);
}
}
// 保存当前配置
saveToProperties(properties);
properties.store(output, "Contract Manager 应用程序配置");
logger.debug("成功保存配置到文件: {}", configFile.getPath());
} catch (Exception e) {
logger.error("保存配置到文件失败: {}", configFile.getPath(), e);
}
}
} }

View File

@@ -98,6 +98,17 @@ public class SpringApp {
context = application.run(); context = application.run();
logger.debug("SpringApp.launch application.run()."); logger.debug("SpringApp.launch application.run().");
Duration between = Duration.between(startup.getBufferedTimeline().getStartTime(), Instant.now()); Duration between = Duration.between(startup.getBufferedTimeline().getStartTime(), Instant.now());
// 初始化MyProperties从properties加载配置
try {
MyProperties myProperties = context.getBean(MyProperties.class);
myProperties.loadFromProperties(properties);
holder.info("MyProperties配置加载完成");
} catch (Exception e) {
logger.error("加载MyProperties配置失败", e);
holder.error("加载MyProperties配置失败: " + e.getMessage());
}
holder.info("应用程序环境加载完成... " + between); holder.info("应用程序环境加载完成... " + between);
}); });
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {

View File

@@ -118,10 +118,24 @@ public class WebSocketClientService {
String errorMsg = node.get(WebSocketConstant.MESSAGE_FIELD_NAME).asText(); String errorMsg = node.get(WebSocketConstant.MESSAGE_FIELD_NAME).asText();
logger.error("收到错误消息: 错误码={}, 错误信息={}", errorCode, errorMsg); logger.error("收到错误消息: 错误码={}, 错误信息={}", errorCode, errorMsg);
if (errorCode == WebSocketConstant.ERROR_CODE_UNAUTHORIZED) { if (errorCode == WebSocketConstant.ERROR_CODE_UNAUTHORIZED) {
// 调用所有的 callbacks 和 session 失败并且移除
callbacks.keySet().stream().toList().forEach(key -> callbacks.remove(key).completeExceptionally(new Exception("未授权")));
sessions.values().stream().toList().forEach(session -> {
session.updateMessage(java.util.logging.Level.SEVERE, "未授权");
session.close();
});
isActive = false;
webSocket.close(1000, "");
WebSocketClientService.this.webSocket = null;
// 处理未授权错误,重新登录 // 处理未授权错误,重新登录
OkHttpLoginController controller = new OkHttpLoginController(); OkHttpLoginController controller = new OkHttpLoginController();
controller.setProperties(SpringApp.getBean(MyProperties.class));
controller.tryLogin(); controller.tryLogin();
// 需要把窗口顶置 // 需要把窗口顶置
isActive = true;
scheduleReconnect();
} }
return; return;
} }
@@ -164,8 +178,8 @@ public class WebSocketClientService {
}; };
private void onCallbackMessage(CompletableFuture<JsonNode> future, JsonNode node) { private void onCallbackMessage(CompletableFuture<JsonNode> future, JsonNode node) {
if (node.has(WebSocketConstant.SUCCESS_FIELD_VALUE)) { if (node.has(WebSocketConstant.SUCCESS_FIELD_NAME)) {
if (!node.get(WebSocketConstant.SUCCESS_FIELD_VALUE).asBoolean()) { if (!node.get(WebSocketConstant.SUCCESS_FIELD_NAME).asBoolean()) {
future.completeExceptionally( future.completeExceptionally(
new RuntimeException( new RuntimeException(
"请求失败:来自服务器的消息=" + node.get(WebSocketConstant.MESSAGE_FIELD_NAME).asText())); "请求失败:来自服务器的消息=" + node.get(WebSocketConstant.MESSAGE_FIELD_NAME).asText()));
@@ -204,7 +218,7 @@ public class WebSocketClientService {
String json = objectMapper.writeValueAsString(msg); String json = objectMapper.writeValueAsString(msg);
callbacks.put(msg.getMessageId(), future); callbacks.put(msg.getMessageId(), future);
if (webSocket.send(json)) { if (webSocket.send(json)) {
logger.debug("send message success:{}", json); logger.debug("send json success:{}", json);
} else { } else {
if (isActive) { if (isActive) {
future.completeExceptionally(new RuntimeException("Failed to send WebSocket message")); future.completeExceptionally(new RuntimeException("Failed to send WebSocket message"));

View File

@@ -72,10 +72,10 @@ public interface WebSocketClientTasker {
webSocketService.withSession(session -> { webSocketService.withSession(session -> {
try { try {
session.submitTask(this, locale, args); session.submitTask(this, locale, args);
holder.addMessage(Level.INFO, "已提交任务到服务器: " + getTaskName()); holder.info("已提交任务到服务器: " + getTaskName());
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
String errorMsg = "任务提交失败: " + e.getMessage(); String errorMsg = "任务提交失败: " + e.getMessage();
holder.addMessage(Level.SEVERE, errorMsg); holder.warn(errorMsg);
throw new RuntimeException("任务提交失败: " + e.getMessage(), e); throw new RuntimeException("任务提交失败: " + e.getMessage(), e);
} }
}); });

View File

@@ -116,6 +116,7 @@ public class HomeWindowController extends BaseController {
@EventListener @EventListener
public void onCurrentEmployeeInitialed(CurrentEmployeeInitialedEvent event) { public void onCurrentEmployeeInitialed(CurrentEmployeeInitialedEvent event) {
System.out.println("event = " + event);
CurrentEmployee currentEmployee = event.getEmployee(); CurrentEmployee currentEmployee = event.getEmployee();
if (currentEmployee.isSystemAdministrator()) { if (currentEmployee.isSystemAdministrator()) {
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {

View File

@@ -1,544 +0,0 @@
package com.ecep.contract.controller;
import java.io.FileOutputStream;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import com.ecep.contract.Desktop;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import lombok.Setter;
public class LoginWidowController implements MessageHolder {
private static final Logger logger = LoggerFactory.getLogger(LoginWidowController.class);
@Setter
MessageHolder holder;
@Setter
Stage primaryStage;
@Setter
Properties properties;
@Override
public void addMessage(Level level, String message) {
holder.addMessage(level, message);
}
private void storeProperties() {
try (FileOutputStream fos = new FileOutputStream("config.properties")) {
// 保存到文件
properties.store(fos, "Updated config.properties");
info("配置文件已更新!");
} catch (java.io.IOException e) {
error("保存配置文件失败:" + e.getMessage());
}
}
String getHost() {
return properties.getProperty("server.host");
}
public void tryLogin() {
// CompletableFuture<ButtonType> future = new CompletableFuture<>();
// 检查配置文件中是否保存用户名和密码
String userName = getUserName();
if (StringUtils.hasText(userName)) {
try {
EmployeeInfo employeeInfo = tryToConnect(userName, getPassword());
if (employeeInfo.errorCode < 0) {
error("登录失败:错误代码=" + employeeInfo.errorCode);
} else {
logined(employeeInfo);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
} else {
showUserNameLoginDialog(null);
}
}
private String getPassword() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getPassword'");
}
private String getUserName() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getUserName'");
}
CompletableFuture<List<LoginWidowController.MacIP>> getMacAndIP() {
return CompletableFuture.supplyAsync(() -> {
// mac ip
List<LoginWidowController.MacIP> list = new ArrayList<>();
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface anInterface = interfaces.nextElement();
if (anInterface.isLoopback()) {
continue;
}
byte[] hardwareAddress = anInterface.getHardwareAddress();
if (hardwareAddress == null) {
continue;
}
Enumeration<InetAddress> inetAddresses = anInterface.getInetAddresses();
if (!inetAddresses.hasMoreElements()) {
continue;
}
// -分割16进制表示法
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hardwareAddress.length; i++) {
sb.append(
String.format("%02X%s", hardwareAddress[i], i < hardwareAddress.length - 1 ? "-" : ""));
}
while (inetAddresses.hasMoreElements()) {
InetAddress inetAddress = inetAddresses.nextElement();
if (inetAddress instanceof Inet4Address) {
list.add(new LoginWidowController.MacIP(sb.toString(), inetAddress.getHostAddress()));
}
}
}
} catch (SocketException e) {
throw new RuntimeException(e);
}
return list;
});
}
private EmployeeInfo tryToConnect(String userName, String password) throws SQLException {
String host = getHost();
String port = getPort();
String database = getDatabase();
if (logger.isDebugEnabled()) {
logger.debug("try to connect db server host:{},port:{},database:{},user:{},pwd:{}", host, port, database,
userName, "*");
}
return null;
}
private String getDatabase() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getDatabase'");
}
private String getPort() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getPort'");
}
private void createSession(Connection connection, EmployeeInfo employeeInfo) {
employeeInfo.sessionId = addHistory(connection, employeeInfo.employeeId, employeeInfo.binds.getFirst());
}
private int addHistory(Connection connection, int employeeId, MacIP macIP) {
try {
String sql = "INSERT INTO EMPLOYEE_LOGIN_HISTORY (IP, MAC, DT, EMPLOYEE_ID) VALUES (?, ?, ?, ?)";
try (PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, macIP.ip);
ps.setString(2, macIP.mac);
ps.setObject(3, LocalDateTime.now()); // 根据数据库字段类型调整
ps.setInt(4, employeeId);
ps.executeUpdate();
// 返回 新的主键值
ResultSet generatedKeys = ps.getGeneratedKeys();
if (generatedKeys.next()) {
return generatedKeys.getInt(1);
}
}
} catch (SQLException e) {
holder.error("申请新会话编号失败");
logger.error("unable insert EMPLOYEE_LOGIN_HISTORY, ", e);
}
return -1;
}
static class MacIP {
String ip;
String mac;
public MacIP(String mac, String ip) {
this.mac = mac;
this.ip = ip;
}
}
static class EmployeeInfo {
Integer employeeId;
List<MacIP> binds = new ArrayList<>();
SimpleStringProperty name = new SimpleStringProperty();
SimpleBooleanProperty active = new SimpleBooleanProperty();
int sessionId;
int errorCode = 0;
public EmployeeInfo(Integer employeeId) {
this.employeeId = employeeId;
}
public static EmployeeInfo error(int code) {
EmployeeInfo employeeInfo = new EmployeeInfo(null);
employeeInfo.errorCode = code;
return employeeInfo;
}
public void addBind(MacIP macIP) {
binds.add(macIP);
}
}
private CompletableFuture<EmployeeInfo> tryLoginWithEmployeeBind(Connection connection,
CompletableFuture<List<MacIP>> macAndIP) {
CompletableFuture<EmployeeInfo> future = new CompletableFuture<>();
macAndIP.thenAccept(macIPS -> {
if (macIPS.isEmpty()) {
future.complete(EmployeeInfo.error(-1));
return;
}
HashMap<Integer, EmployeeInfo> employeeMap = new HashMap<>();
for (MacIP macIP : macIPS) {
for (Integer employeeId : findAllBindEmployee(connection, macIP)) {
employeeMap.computeIfAbsent(employeeId, k -> new EmployeeInfo(employeeId)).addBind(macIP);
}
}
if (employeeMap.isEmpty()) {
error("本机未绑定登录信息,请联系管理员更新.");
// 当前计算机的信息,如用户名,计算机名等
String username = System.getProperty("user.name");
String computerName = System.getenv("COMPUTERNAME");
for (MacIP macIP : macIPS) {
if (macIP.ip.equals("127.0.0.1")) {
continue;
}
registerComputer(username, computerName, connection, macIP);
}
future.complete(EmployeeInfo.error(-2));
return;
}
if (employeeMap.size() == 1) {
// 直接登录
EmployeeInfo employeeInfo = employeeMap.values().stream().findFirst().get();
// issue #1 登录成功后没有更新员工信息
fill(connection, employeeInfo);
future.complete(employeeInfo);
} else {
List<EmployeeInfo> list = employeeMap.values().stream().toList();
// 选择登录
Platform.runLater(() -> {
EmployeeInfo info = showEmployeeSelectDialog(list);
future.complete(Objects.requireNonNullElseGet(info, () -> EmployeeInfo.error(-3)));
});
for (EmployeeInfo info : list) {
fill(connection, info);
}
}
});
return future;
}
private EmployeeInfo showEmployeeSelectDialog(List<EmployeeInfo> list) {
Stage stage = new Stage();
stage.initOwner(primaryStage);
stage.setTitle("请选择账户登录系统");
stage.setWidth(360);
stage.setHeight(280);
Label label = new Label("您的主机关联了以下账户,请选择一个登录");
label.setPadding(new Insets(10, 0, 10, 10));
ListView<Label> listView = new ListView<>();
EventHandler<MouseEvent> eventHandler = event -> {
if (event.getClickCount() == 2) {
// listView.getSelectionModel().select(cb);
stage.close();
}
};
for (EmployeeInfo employeeInfo : list) {
Label cb = new Label();
cb.setUserData(employeeInfo);
cb.textProperty().bind(employeeInfo.name);
cb.setPadding(new Insets(5));
cb.setOnMouseClicked(eventHandler);
listView.getItems().add(cb);
}
// 创建 BorderPane 并设置边距
BorderPane borderPane = new BorderPane();
borderPane.setPadding(new Insets(10));
borderPane.setTop(label);
borderPane.setCenter(listView);
Button bottom = new Button("确定");
bottom.setDefaultButton(true);
bottom.setOnAction(event -> {
Label selectedItem = listView.getSelectionModel().getSelectedItem();
if (selectedItem == null) {
// 没选中,退出继续选择
return;
}
stage.close();
});
BorderPane.setAlignment(bottom, javafx.geometry.Pos.CENTER);
borderPane.setBottom(bottom);
stage.setScene(new Scene(borderPane));
stage.setOnCloseRequest(event -> {
Label selectedItem = listView.getSelectionModel().getSelectedItem();
if (selectedItem == null) {
// 关闭时,如何没有做选择,不关闭窗口
event.consume();
}
});
stage.showAndWait();
Label selectedItem = listView.getSelectionModel().getSelectedItem();
if (selectedItem == null) {
throw new NoSuchElementException("请选择工号登录系统");
}
return (EmployeeInfo) selectedItem.getUserData();
}
private void fill(Connection connection, EmployeeInfo info) {
try {
ResultSet rs = connection.createStatement()
.executeQuery("SELECT * FROM EMPLOYEE where ID = " + info.employeeId);
if (rs.next()) {
String name = rs.getString("NAME");
boolean isActive = rs.getBoolean("IS_ACTIVE");
Platform.runLater(() -> {
info.name.set(name);
info.active.set(isActive);
});
}
} catch (SQLException e) {
logger.error("查询{}失败", info.employeeId, e);
}
}
private void registerComputer(String username, String computerName, Connection connection, MacIP macIP) {
info("正在注册本机信息(MAC:" + macIP.mac + ", IP:" + macIP.ip + ")...");
String sql = "INSERT INTO EMPLOYEE_AUTH_BIND (IP,MAC,DESCRIPTION)VALUES(?,?,?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, macIP.ip);
stmt.setString(2, macIP.mac);
// 当前计算机的信息,如用户名,计算机名等
stmt.setString(3, username + "," + computerName);
if (stmt.execute()) {
info("注册成功");
}
} catch (SQLException e) {
error(String.format("注册失败请联系管理员或重启应用MAC: %s, IP: %s", macIP.mac, macIP.ip));
logger.error("注册失败 mac:{}, ip:{}", macIP.mac, macIP.ip, e);
}
}
List<Integer> findAllBindEmployee(Connection connection, MacIP macIP) {
List<Integer> list = new ArrayList<>();
// 优化后代码
String sql = "SELECT * FROM EMPLOYEE_AUTH_BIND WHERE IP = ? AND MAC = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, macIP.ip);
stmt.setString(2, macIP.mac);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
int id = rs.getInt("EMPLOYEE_ID");
if (id > 0) {
list.add(id);
}
}
}
} catch (SQLException e) {
throw new RuntimeException(
String.format("查询本机绑定信息异常请联系管理员或重启应用MAC: %s, IP: %s", macIP.mac, macIP.ip),
e);
}
return list;
}
private void showUserNameLoginDialog(Exception exception) {
Stage stage = new Stage();
stage.initOwner(primaryStage);
stage.setTitle("登录");
// 创建 BorderPane 并设置边距
BorderPane borderPane = new BorderPane();
borderPane.setPadding(new Insets(10));
// 创建布局
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
// 为整个 GridPane 设置外边距
// GridPane.setMargin(grid, new Insets(10));
// 账户输入框
Label userLabel = new Label("账户:");
TextField userField = new TextField();
{
String username = getUserName();
if (StringUtils.hasText(username)) {
userField.setText(username);
}
grid.add(userLabel, 0, 0);
grid.add(userField, 1, 0);
}
// 密码输入框
Label passwordLabel = new Label("密码:");
PasswordField passwordField = new PasswordField();
{
String password = getPassword();
if (StringUtils.hasText(password)) {
passwordField.setText(password);
}
grid.add(passwordLabel, 0, 1);
grid.add(passwordField, 1, 1);
}
// 记住密码复选框
CheckBox rememberCheckBox = new CheckBox("记住账户密码");
{
String property = properties.getProperty("username_password.remember", "false");
if (Boolean.parseBoolean(property)) {
rememberCheckBox.setSelected(true);
}
}
grid.add(rememberCheckBox, 1, 2);
// 错误消息提示
Label exceptionLabel = new Label();
grid.add(exceptionLabel, 0, 3);
exceptionLabel.setWrapText(true);
GridPane.setColumnSpan(exceptionLabel, 2);
if (exception == null) {
exceptionLabel.setVisible(false);
} else {
exceptionLabel.setText(exception.getMessage());
exceptionLabel.setVisible(true);
}
borderPane.setCenter(grid);
// 登录按钮
Button loginButton = new Button("登录");
loginButton.setDefaultButton(true);
borderPane.setBottom(loginButton);
BorderPane.setAlignment(loginButton, javafx.geometry.Pos.CENTER);
// 登录按钮点击事件
loginButton.setOnAction(event -> {
String username = userField.getText();
String password = passwordField.getText();
boolean remember = rememberCheckBox.isSelected();
// 尝试连接数据库
exceptionLabel.setText("");
exceptionLabel.setVisible(false);
try {
EmployeeInfo employeeInfo = tryToConnect(username, password);
if (employeeInfo.errorCode < 0) {
exceptionLabel.setText("登录失败:错误代码=" + employeeInfo.errorCode);
exceptionLabel.setVisible(true);
} else {
logined(employeeInfo);
}
} catch (SQLException ex) {
//
exceptionLabel.setText("数据库错误:" + ex.getMessage());
exceptionLabel.setVisible(true);
return;
}
properties.setProperty("db.server.username", username);
properties.setProperty("db.server.password", password);
properties.setProperty("username_password.remember", Boolean.toString(remember));
// 如果勾选了“记住密码”,则更新配置文件
if (remember) {
CompletableFuture.runAsync(() -> {
storeProperties();
});
}
// 关闭登录窗口
stage.close();
});
// 创建场景并设置到窗口
Scene scene = new Scene(borderPane, 400, 260);
stage.setScene(scene);
// stage.setAlwaysOnTop(true);
stage.setResizable(false);
stage.showAndWait();
}
private void logined(EmployeeInfo employeeInfo) {
info("欢迎 " + employeeInfo.name.get());
if (!SpringApp.isRunning()) {
info("请稍后...");
}
Desktop.instance.setActiveEmployeeId(employeeInfo.employeeId);
// Desktop.instance.setSessionId(employeeInfo.sessionId);
tryShowHomeWindow();
}
void tryShowHomeWindow() {
try {
while (!SpringApp.isRunning()) {
System.out.println("等待启动");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 必须要等待启动成功后才能关闭主场景,否则进程结束程序退出
HomeWindowController.show().thenRun(() -> Platform.runLater(primaryStage::close));
}
}

View File

@@ -11,12 +11,14 @@ import java.util.Properties;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.beans.property.SimpleStringProperty;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.ecep.contract.Desktop; import com.ecep.contract.Desktop;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.MyProperties;
import com.ecep.contract.SpringApp; import com.ecep.contract.SpringApp;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -56,11 +58,11 @@ public class OkHttpLoginController implements MessageHolder {
@Setter @Setter
private Stage primaryStage; private Stage primaryStage;
@Setter @Setter
private Properties properties; private MyProperties properties;
@Setter @Setter
private OkHttpClient httpClient; private OkHttpClient httpClient;
private WebSocket webSocket; private WebSocket webSocket;
private String serverUrl; private SimpleStringProperty serverUrl = new SimpleStringProperty();
private String webSocketUrl; private String webSocketUrl;
public OkHttpLoginController() { public OkHttpLoginController() {
@@ -91,9 +93,9 @@ public class OkHttpLoginController implements MessageHolder {
} }
private void initServerUrls() { private void initServerUrls() {
String host = properties.getProperty("server.host", "localhost"); String host = properties.getServerHost();
String port = properties.getProperty("server.port", "8080"); String port = properties.getServerPort();
this.serverUrl = "http://" + host + ":" + port; serverUrl.set("http://" + host + ":" + port);
this.webSocketUrl = "ws://" + host + ":" + port + "/ws"; this.webSocketUrl = "ws://" + host + ":" + port + "/ws";
} }
@@ -117,11 +119,11 @@ public class OkHttpLoginController implements MessageHolder {
} }
private String getUserName() { private String getUserName() {
return properties.getProperty("user.name", ""); return properties.getUserName();
} }
private String getPassword() { private String getPassword() {
return properties.getProperty("user.password", ""); return properties.getPassword();
} }
private void showLoginDialog() { private void showLoginDialog() {
@@ -138,6 +140,14 @@ public class OkHttpLoginController implements MessageHolder {
grid.setHgap(10); grid.setHgap(10);
grid.setVgap(15); grid.setVgap(15);
Label hostLabel = new Label("服务器:");
TextField hostField = new TextField();
{
hostField.textProperty().bindBidirectional(serverUrl);
grid.add(hostLabel, 0, 0);
grid.add(hostField, 1, 0);
}
// 账户输入框 // 账户输入框
Label userLabel = new Label("用户名:"); Label userLabel = new Label("用户名:");
TextField userField = new TextField(); TextField userField = new TextField();
@@ -146,8 +156,8 @@ public class OkHttpLoginController implements MessageHolder {
if (StringUtils.hasText(username)) { if (StringUtils.hasText(username)) {
userField.setText(username); userField.setText(username);
} }
grid.add(userLabel, 0, 0); grid.add(userLabel, 0, 1);
grid.add(userField, 1, 0); grid.add(userField, 1, 1);
} }
// 密码输入框 // 密码输入框
@@ -158,25 +168,25 @@ public class OkHttpLoginController implements MessageHolder {
if (StringUtils.hasText(password)) { if (StringUtils.hasText(password)) {
passwordField.setText(password); passwordField.setText(password);
} }
grid.add(passwordLabel, 0, 1); grid.add(passwordLabel, 0, 2);
grid.add(passwordField, 1, 1); grid.add(passwordField, 1, 2);
} }
// 记住密码复选框 // 记住密码复选框
CheckBox rememberCheckBox = new CheckBox("记住密码"); CheckBox rememberCheckBox = new CheckBox("记住密码");
{ {
String property = properties.getProperty("remember.password", "false"); boolean remember = properties.isRememberPassword();
if (Boolean.parseBoolean(property)) { if (remember) {
rememberCheckBox.setSelected(true); rememberCheckBox.setSelected(true);
} }
} }
grid.add(rememberCheckBox, 1, 2); grid.add(rememberCheckBox, 1, 3);
// 错误消息提示 // 错误消息提示
Label errorLabel = new Label(); Label errorLabel = new Label();
errorLabel.setStyle("-fx-text-fill: red;"); errorLabel.setStyle("-fx-text-fill: red;");
errorLabel.setVisible(false); errorLabel.setVisible(false);
grid.add(errorLabel, 0, 3); grid.add(errorLabel, 0, 4);
GridPane.setColumnSpan(errorLabel, 2); GridPane.setColumnSpan(errorLabel, 2);
borderPane.setCenter(grid); borderPane.setCenter(grid);
@@ -204,14 +214,16 @@ public class OkHttpLoginController implements MessageHolder {
errorLabel.setVisible(false); errorLabel.setVisible(false);
// 保存配置 // 保存配置
properties.setProperty("user.name", username);
if (remember) { if (remember) {
properties.setProperty("user.password", password); properties.setUserName(username);
properties.setProperty("remember.password", "true"); properties.setPassword(password);
properties.setRememberPassword(true);
} else { } else {
properties.setProperty("user.password", ""); properties.setUserName(username);
properties.setProperty("remember.password", "false"); properties.setPassword("");
properties.setRememberPassword(false);
} }
properties.save();
// 执行登录 // 执行登录
login(username, password).whenComplete((v, e) -> { login(username, password).whenComplete((v, e) -> {
@@ -240,8 +252,8 @@ public class OkHttpLoginController implements MessageHolder {
private CompletableFuture<Void> login(String username, String password) { private CompletableFuture<Void> login(String username, String password) {
// 添加详细日志记录服务器URL和请求准备情况 // 添加详细日志记录服务器URL和请求准备情况
info("正在连接服务器: " + serverUrl); info("正在连接服务器: " + serverUrl.get());
logger.debug("login方法被调用用户名: " + username); logger.debug("用户名: {}", username);
CompletableFuture<Void> future = new CompletableFuture<>(); CompletableFuture<Void> future = new CompletableFuture<>();
try { try {
@@ -268,7 +280,7 @@ public class OkHttpLoginController implements MessageHolder {
RequestBody body = RequestBody.create(objectNode.toString(), JSON); RequestBody body = RequestBody.create(objectNode.toString(), JSON);
// 构建并记录完整的请求URL // 构建并记录完整的请求URL
String loginUrl = serverUrl + "/api/login"; String loginUrl = serverUrl.get() + "/api/login";
logger.debug("构建登录请求URL: " + loginUrl); logger.debug("构建登录请求URL: " + loginUrl);
Request request = new Request.Builder() Request request = new Request.Builder()
.url(loginUrl) .url(loginUrl)

View File

@@ -75,10 +75,6 @@ public class CompanyWindowController
@Autowired @Autowired
private CompanyService companyService; private CompanyService companyService;
@Autowired
private CompanyCustomerService companyCustomerService;
@Autowired
private VendorService vendorService;
public BorderPane root; public BorderPane root;
public TabPane tabPane; public TabPane tabPane;
@@ -206,7 +202,8 @@ public class CompanyWindowController
logger.debug("onCustomerTabShown"); logger.debug("onCustomerTabShown");
} }
getLoadedFuture().thenAcceptAsync(company -> { getLoadedFuture().thenAcceptAsync(company -> {
companyCustomerProperty.set(companyCustomerService.findByCompany(company)); CompanyCustomerVo customerVo = getCachedBean(CompanyCustomerService.class).findByCompany(company);
companyCustomerProperty.set(customerVo);
}).exceptionally(ex -> { }).exceptionally(ex -> {
UITools.showExceptionAndWait(ex.getMessage(), ex); UITools.showExceptionAndWait(ex.getMessage(), ex);
return null; return null;
@@ -242,6 +239,7 @@ public class CompanyWindowController
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("onVendorTabShown company {}", company.getName()); logger.debug("onVendorTabShown company {}", company.getName());
} }
VendorService vendorService = getBean(VendorService.class);
companyVendorProperty.set(vendorService.findByCompany(company)); companyVendorProperty.set(vendorService.findByCompany(company));
}).exceptionally(ex -> { }).exceptionally(ex -> {
UITools.showExceptionAndWait(ex.getMessage(), ex); UITools.showExceptionAndWait(ex.getMessage(), ex);
@@ -256,6 +254,7 @@ public class CompanyWindowController
CompanyCompositeUpdateTasker task = new CompanyCompositeUpdateTasker(); CompanyCompositeUpdateTasker task = new CompanyCompositeUpdateTasker();
task.setCompany(getEntity()); task.setCompany(getEntity());
UITools.showTaskDialogAndWait("更新企业信息", task, null); UITools.showTaskDialogAndWait("更新企业信息", task, null);
refresh();
} }
/** /**
@@ -266,6 +265,7 @@ public class CompanyWindowController
CompanyVerifyTasker task = new CompanyVerifyTasker(); CompanyVerifyTasker task = new CompanyVerifyTasker();
task.setCompany(company); task.setCompany(company);
UITools.showTaskDialogAndWait("企业合规性验证", task, null); UITools.showTaskDialogAndWait("企业合规性验证", task, null);
refresh();
} }
public void onCompanyOpenInExplorerAction(ActionEvent event) { public void onCompanyOpenInExplorerAction(ActionEvent event) {
@@ -276,7 +276,7 @@ public class CompanyWindowController
if (!StringUtils.hasText(path)) { if (!StringUtils.hasText(path)) {
ButtonType buttonType = UITools.showConfirmation("目录未设置", "是否创建目录").join(); ButtonType buttonType = UITools.showConfirmation("目录未设置", "是否创建目录").join();
if (buttonType == ButtonType.OK) { if (buttonType == ButtonType.OK) {
if (companyService.makePathAbsent(company)) { if (companyService.makePathAbsent(company, (level, message) -> setStatus(message))) {
save(company); save(company);
} }
} else { } else {

View File

@@ -336,13 +336,13 @@ public class ProjectCostTabSkinItems
grossProfitMarginColumn.setEditable(false); grossProfitMarginColumn.setEditable(false);
creatorColumn.setCellValueFactory(param -> param.getValue().getCreatorId()); creatorColumn.setCellValueFactory(param -> param.getValue().getCreatorId());
creatorColumn.setCellFactory(cell -> new EmployeeTableCell<>(getEmployeeService())); creatorColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
updaterColumn.setCellValueFactory(param -> param.getValue().getUpdaterId()); updaterColumn.setCellValueFactory(param -> param.getValue().getUpdaterId());
updaterColumn.setCellFactory(cell -> new EmployeeTableCell<>(getEmployeeService())); updaterColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
createDateColumn.setCellValueFactory(param -> param.getValue().getCreateDate()); createDateColumn.setCellValueFactory(param -> param.getValue().getCreateDate());
createDateColumn.setCellFactory(param -> new LocalDateTimeTableCell<>()); createDateColumn.setCellFactory(LocalDateTimeTableCell.forTableColumn());
updateDateColumn.setCellValueFactory(param -> param.getValue().getUpdateDate()); updateDateColumn.setCellValueFactory(param -> param.getValue().getUpdateDate());
updateDateColumn.setCellFactory(param -> new LocalDateTimeTableCell<>()); updateDateColumn.setCellFactory(LocalDateTimeTableCell.forTableColumn());
remarkColumn.setCellValueFactory(param -> param.getValue().getRemark()); remarkColumn.setCellValueFactory(param -> param.getValue().getRemark());
remarkColumn.setCellFactory(TextFieldTableCell.forTableColumn()); remarkColumn.setCellFactory(TextFieldTableCell.forTableColumn());
remarkColumn.setOnEditCommit(event -> { remarkColumn.setOnEditCommit(event -> {

View File

@@ -8,7 +8,6 @@ import org.springframework.util.StringUtils;
import com.ecep.contract.controller.company.AbstCompanyBasedTabSkin; import com.ecep.contract.controller.company.AbstCompanyBasedTabSkin;
import com.ecep.contract.controller.company.CompanyWindowController; import com.ecep.contract.controller.company.CompanyWindowController;
import com.ecep.contract.model.CompanyOldName;
import com.ecep.contract.service.CompanyOldNameService; import com.ecep.contract.service.CompanyOldNameService;
import com.ecep.contract.vo.CompanyOldNameVo; import com.ecep.contract.vo.CompanyOldNameVo;
import com.ecep.contract.vo.CompanyVo; import com.ecep.contract.vo.CompanyVo;
@@ -23,19 +22,16 @@ import javafx.scene.control.Tab;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.stage.DirectoryChooser;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.util.converter.LocalDateStringConverter; import javafx.util.converter.LocalDateStringConverter;
import lombok.Setter;
/** /**
* * 公司基础信息标签页皮肤
*/ */
public class CompanyTabSkinBase public class CompanyTabSkinBase
extends AbstCompanyBasedTabSkin extends AbstCompanyBasedTabSkin
implements TabSkin { implements TabSkin {
@Setter
private CompanyOldNameService companyOldNameService;
public CompanyTabSkinBase(CompanyWindowController controller) { public CompanyTabSkinBase(CompanyWindowController controller) {
super(controller); super(controller);
@@ -49,7 +45,8 @@ public class CompanyTabSkinBase
@Override @Override
public void initializeTab() { public void initializeTab() {
LocalDateStringConverter localDateStringConverter = new LocalDateStringConverter(DateTimeFormatter.ISO_LOCAL_DATE, null); LocalDateStringConverter localDateStringConverter = new LocalDateStringConverter(
DateTimeFormatter.ISO_LOCAL_DATE, null);
controller.nameField.textProperty().bind(viewModel.getName()); controller.nameField.textProperty().bind(viewModel.getName());
controller.shortNameField.textProperty().bindBidirectional(viewModel.getShortName()); controller.shortNameField.textProperty().bindBidirectional(viewModel.getShortName());
@@ -70,7 +67,8 @@ public class CompanyTabSkinBase
controller.regAddressField.textProperty().bindBidirectional(viewModel.getRegAddress()); controller.regAddressField.textProperty().bindBidirectional(viewModel.getRegAddress());
controller.addressField.textProperty().bindBidirectional(viewModel.getAddress()); controller.addressField.textProperty().bindBidirectional(viewModel.getAddress());
controller.registeredCapitalField.textProperty().bindBidirectional(viewModel.getRegisteredCapital()); controller.registeredCapitalField.textProperty().bindBidirectional(viewModel.getRegisteredCapital());
controller.registeredCapitalCurrencyField.textProperty().bindBidirectional(viewModel.getRegisteredCapitalCurrency()); controller.registeredCapitalCurrencyField.textProperty()
.bindBidirectional(viewModel.getRegisteredCapitalCurrency());
controller.legalRepresentativeField.textProperty().bindBidirectional(viewModel.getLegalRepresentative()); controller.legalRepresentativeField.textProperty().bindBidirectional(viewModel.getLegalRepresentative());
controller.operationPeriodBeginField.setConverter(localDateStringConverter); controller.operationPeriodBeginField.setConverter(localDateStringConverter);
@@ -89,7 +87,7 @@ public class CompanyTabSkinBase
private void onCompanyPathCreatePathAction(ActionEvent event) { private void onCompanyPathCreatePathAction(ActionEvent event) {
CompanyVo company = getEntity(); CompanyVo company = getEntity();
if (getCompanyService().makePathAbsent(company)) { if (getCompanyService().makePathAbsent(company, ((level, message) -> setStatus(message)))) {
save(company); save(company);
} else { } else {
setStatus("目录存在或创建失败"); setStatus("目录存在或创建失败");
@@ -97,7 +95,33 @@ public class CompanyTabSkinBase
} }
private void onCompanyPathChangePathAction(ActionEvent event) { private void onCompanyPathChangePathAction(ActionEvent event) {
DirectoryChooser chooser = new DirectoryChooser();
CompanyVo entity = getEntity();
String path = entity.getPath();
File initialDirectory = null;
// 如果当前已经设置了目录并且路径有效,则设置初始目录为该目录
if (StringUtils.hasText(path)) {
File dir = new File(path);
if (dir.exists()) {
initialDirectory = dir;
}
}
// 如果没有有效的初始目录,则使用基础路径
if (initialDirectory == null) {
initialDirectory = getCompanyService().getBasePath();
}
if (initialDirectory != null) {
chooser.setInitialDirectory(initialDirectory);
}
File newDirectory = chooser.showDialog(getTab().getContent().getScene().getWindow());
if (newDirectory != null) {
entity.setPath(newDirectory.getAbsolutePath());
save(entity);
}
} }
private void onCompanyPathSameAsNameAction(ActionEvent event) { private void onCompanyPathSameAsNameAction(ActionEvent event) {
@@ -150,18 +174,16 @@ public class CompanyTabSkinBase
layout.getChildren().addAll(nameField, nameLabel, saveAsOldName, ambiguity, ambiguityLabel); layout.getChildren().addAll(nameField, nameLabel, saveAsOldName, ambiguity, ambiguityLabel);
alert.setContentText("context"); alert.setContentText("context");
alert.setHeaderText(null); alert.setHeaderText(null);
alert.setTitle("企业更名"); alert.setTitle("企业更名");
alert.getDialogPane().setContent(layout); alert.getDialogPane().setContent(layout);
// alert.setResultConverter(param -> { // alert.setResultConverter(param -> {
// //
// return null; // return null;
// }); // });
alert.setOnCloseRequest(dialogEvent -> { alert.setOnCloseRequest(dialogEvent -> {
ButtonType buttonType = alert.getResult(); ButtonType buttonType = alert.getResult();
@@ -206,9 +228,6 @@ public class CompanyTabSkinBase
} }
private CompanyOldNameService getCompanyOldNameService() { private CompanyOldNameService getCompanyOldNameService() {
if (companyOldNameService == null) { return getCachedBean(CompanyOldNameService.class);
companyOldNameService = getBean(CompanyOldNameService.class);
}
return companyOldNameService;
} }
} }

View File

@@ -126,18 +126,21 @@ public class CompanyTabSkinContract
contractTabToolBtn1.setOnAction(event -> { contractTabToolBtn1.setOnAction(event -> {
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
// 计算主合同编号 // 计算主合同编号
ContractService contractService = getViewModelService();
for (ContractViewModel model : dataSet) { for (ContractViewModel model : dataSet) {
ContractVo contract = getViewModelService().findById(model.getId().get()); ContractVo contract = contractService.findById(model.getId().get());
if (contract == null) { if (contract == null) {
continue; continue;
} }
try { try {
if (getViewModelService().updateParentCode(contract)) { if (contractService.updateParentCode(contract)) {
ContractVo updated = getViewModelService().save(contract); ContractVo updated = contractService.save(contract);
model.update(updated); model.update(updated);
} }
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {
model.getParentCode().set(e.getMessage()); model.getParentCode().set(e.getMessage());
} catch (Exception e) {
setStatus("计算主合同编号失败 " + e.getMessage());
} }
} }
}); });

View File

@@ -6,36 +6,31 @@ import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import com.ecep.contract.service.CompanyFileTypeService;
import com.ecep.contract.service.ContractFileTypeService;
import com.ecep.contract.vo.CompanyFileTypeLocalVo;
import com.ecep.contract.vo.CompanyFileVo;
import com.ecep.contract.vo.CompanyVo;
import org.springframework.util.FileSystemUtils; import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.ecep.contract.CompanyFileType; import com.ecep.contract.CompanyFileType;
import com.ecep.contract.DesktopUtils; import com.ecep.contract.DesktopUtils;
import com.ecep.contract.MyDateTimeUtils; import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.MyProperties;
import com.ecep.contract.constant.CloudServiceConstant; import com.ecep.contract.constant.CloudServiceConstant;
import com.ecep.contract.controller.company.AbstCompanyTableTabSkin; import com.ecep.contract.controller.company.AbstCompanyTableTabSkin;
import com.ecep.contract.controller.company.CompanyWindowController; import com.ecep.contract.controller.company.CompanyWindowController;
import com.ecep.contract.controller.table.EditableEntityTableTabSkin; import com.ecep.contract.controller.table.EditableEntityTableTabSkin;
import com.ecep.contract.controller.table.cell.CompanyFilePathTableCell; import com.ecep.contract.controller.table.cell.CompanyFilePathTableCell;
import com.ecep.contract.model.Company; import com.ecep.contract.controller.table.cell.CompanyFileTypeTableCell;
import com.ecep.contract.model.CompanyFile;
import com.ecep.contract.model.CompanyFileTypeLocal;
import com.ecep.contract.service.CloudTycService; import com.ecep.contract.service.CloudTycService;
import com.ecep.contract.service.CompanyFileService; import com.ecep.contract.service.CompanyFileService;
import com.ecep.contract.service.CompanyFileTypeService;
import com.ecep.contract.task.CompanyFileMoveTasker;
import com.ecep.contract.task.CompanyFileResetTasker;
import com.ecep.contract.task.CompanyFileRetrieveFromDownloadDirTasker;
import com.ecep.contract.util.FxmlPath; import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.util.UITools; import com.ecep.contract.util.UITools;
import com.ecep.contract.vm.CompanyFileViewModel; import com.ecep.contract.vm.CompanyFileViewModel;
import com.ecep.contract.vo.CompanyFileVo;
import com.ecep.contract.vo.CompanyVo;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.event.Event; import javafx.event.Event;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@@ -53,7 +48,7 @@ public class CompanyTabSkinFile
implements TabSkin, EditableEntityTableTabSkin<CompanyFileVo, CompanyFileViewModel> { implements TabSkin, EditableEntityTableTabSkin<CompanyFileVo, CompanyFileViewModel> {
public TableColumn<CompanyFileViewModel, Number> idColumn; public TableColumn<CompanyFileViewModel, Number> idColumn;
public TableColumn<CompanyFileViewModel, String> typeColumn; public TableColumn<CompanyFileViewModel, CompanyFileType> typeColumn;
public TableColumn<CompanyFileViewModel, String> filePathColumn; public TableColumn<CompanyFileViewModel, String> filePathColumn;
public TableColumn<CompanyFileViewModel, LocalDate> applyDateColumn; public TableColumn<CompanyFileViewModel, LocalDate> applyDateColumn;
public TableColumn<CompanyFileViewModel, LocalDate> expiringDateColumn; public TableColumn<CompanyFileViewModel, LocalDate> expiringDateColumn;
@@ -65,9 +60,6 @@ public class CompanyTabSkinFile
public MenuItem fileTable_menu_del; public MenuItem fileTable_menu_del;
public MenuItem fileTable_menu_copy_as_matched_by_contract; public MenuItem fileTable_menu_copy_as_matched_by_contract;
private final ObservableMap<CompanyFileType, CompanyFileTypeLocalVo> fileTypeLocalMap = FXCollections
.observableHashMap();
public CompanyTabSkinFile(CompanyWindowController controller) { public CompanyTabSkinFile(CompanyWindowController controller) {
super(controller); super(controller);
setDragAndDrop(true); setDragAndDrop(true);
@@ -96,10 +88,12 @@ public class CompanyTabSkinFile
idColumn.setCellValueFactory(param -> param.getValue().getId()); idColumn.setCellValueFactory(param -> param.getValue().getId());
typeColumn.setCellValueFactory(param -> Bindings.valueAt(fileTypeLocalMap, param.getValue().getType()) typeColumn.setCellValueFactory(param -> param.getValue().getType());
.map(CompanyFileTypeLocalVo::getValue)); typeColumn.setCellFactory(CompanyFileTypeTableCell.forTableColumn(getCachedBean(CompanyFileTypeService.class)));
filePathColumn.setCellValueFactory(param -> param.getValue().getFilePath()); filePathColumn.setCellValueFactory(param -> param.getValue().getFilePath());
filePathColumn.setCellFactory(param -> new CompanyFilePathTableCell<>(viewModel.getPath())); filePathColumn.setCellFactory(param -> new CompanyFilePathTableCell<>(viewModel.getPath()));
applyDateColumn.setCellValueFactory(param -> param.getValue().getApplyDate()); applyDateColumn.setCellValueFactory(param -> param.getValue().getApplyDate());
expiringDateColumn.setCellValueFactory(param -> param.getValue().getExpiringDate()); expiringDateColumn.setCellValueFactory(param -> param.getValue().getExpiringDate());
@@ -107,85 +101,39 @@ public class CompanyTabSkinFile
fileTable_menu_del.setOnAction(this::onTableDeleteAction); fileTable_menu_del.setOnAction(this::onTableDeleteAction);
fileTable_menu_copy_as_matched_by_contract.setOnAction(this::onTableCopyAsMatchedByContractAction); fileTable_menu_copy_as_matched_by_contract.setOnAction(this::onTableCopyAsMatchedByContractAction);
fileTable_menu_copy_as_matched_by_contract.setOnMenuValidation(this::onTableCopyAsMatchedMenuValidation); fileTable_menu_copy_as_matched_by_contract.setOnMenuValidation(this::onTableCopyAsMatchedMenuValidation);
fileTypeLocalMap.putAll(getCachedBean(CompanyFileTypeService.class).findAll(getLocale()));
} }
private void onTableResetAction(ActionEvent event) { private void onTableResetAction(ActionEvent event) {
runAsync(() -> { // 创建本地任务
if (getViewModelService().reBuildingFiles(getParent(), (level, msg) -> setStatus(msg))) { CompanyFileResetTasker tasker = new CompanyFileResetTasker();
loadTableDataSet(); tasker.setCompany(getParent());
} UITools.showTaskDialogAndWait("重置公司文件", tasker, null);
}); if (tasker.isFilesUpdated()) {
loadTableDataSet();
}
} }
/** /**
* 从 下载目录 中查找相关的资质文件 * 从 下载目录 中查找相关的资质文件
*/ */
private void onTableRetrieveFromDownloadDirAction(ActionEvent event) { private void onTableRetrieveFromDownloadDirAction(ActionEvent event) {
CompanyVo company = getParent(); // 创建本地任务
MyProperties myProperties = getBean(MyProperties.class); CompanyFileRetrieveFromDownloadDirTasker tasker = new CompanyFileRetrieveFromDownloadDirTasker();
File dir = myProperties.getDownloadDirectory(); tasker.setCompany(getEntity());
if (!dir.exists()) { UITools.showTaskDialogAndWait("从下载目录检索文件", tasker, null);
setStatus("下载目录 " + dir.getAbsolutePath() + " 不存在,请检查"); loadTableDataSet();
return;
}
setStatus("开始检索 下载 文件夹:" + dir.getAbsolutePath() + "...");
File[] files = dir.listFiles(File::isFile);
if (files == null) {
setStatus("检索 下载 文件夹失败");
return;
}
if (files.length == 0) {
setStatus("下载 文件夹没有文件");
return;
}
setStatus("下载 文件夹中共有文件 " + files.length + " 个文件");
if (getParentService().retrieveFromDownloadFiles(company, files, (level, msg) -> setStatus(msg))) {
// fixed if update
viewModel.update(company);
loadTableDataSet();
}
} }
/** /**
* 把文件从 老系统中移到 \\10.84.209.8\项目信息\相关方信息 目录中 * 把文件从 老系统中移到 \\10.84.209.8\项目信息\相关方信息 目录中
*/ */
private void onTableMoveFileAction(ActionEvent event) { private void onTableMoveFileAction(ActionEvent event) {
CompanyFileService companyFileService = getCompanyFileService(); // 创建本地任务
CompanyVo company = getParent(); CompanyFileMoveTasker tasker = new CompanyFileMoveTasker();
List<CompanyFileVo> list = companyFileService.findByCompany(company); tasker.setCompany(getParent());
if (list.isEmpty()) { UITools.showTaskDialogAndWait("移动公司文件", tasker, null);
return; if (tasker.isFilesMoved()) {
} loadTableDataSet();
if (getParentService().makePathAbsent(company)) {
save(company);
}
String path = company.getPath();
if (!StringUtils.hasText(path)) {
setStatus("异常, 企业目录未设置");
return;
}
File companyPath = new File(path);
for (CompanyFileVo companyFile : list) {
String filePath = companyFile.getFilePath();
if (StringUtils.hasText(filePath)) {
File file = new File(filePath);
if (file.exists()) {
if (file.getParentFile().equals(companyPath)) {
continue;
}
File dest = new File(companyPath, file.getName());
if (file.renameTo(dest)) {
companyFile.setFilePath(dest.getAbsolutePath());
companyFileService.save(companyFile);
setStatus(file.getName() + " 移动到 " + companyPath.getName());
}
}
}
} }
} }
@@ -195,7 +143,6 @@ public class CompanyTabSkinFile
private void onTableCopyAsMatchedByContractAction(ActionEvent event) { private void onTableCopyAsMatchedByContractAction(ActionEvent event) {
UITools.showDialogAndWait("复制资信评估报告", "按当前评估报告复制一个合同中最匹配的", list -> { UITools.showDialogAndWait("复制资信评估报告", "按当前评估报告复制一个合同中最匹配的", list -> {
getViewModelService().copyAsMatchedByContract(getParent(), list); getViewModelService().copyAsMatchedByContract(getParent(), list);
onTableCopyAsMatchedAction_(msg -> { onTableCopyAsMatchedAction_(msg -> {
Platform.runLater(() -> { Platform.runLater(() -> {
list.add(msg); list.add(msg);

View File

@@ -28,6 +28,7 @@ import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.TextFieldTableCell; import javafx.scene.control.cell.TextFieldTableCell;
import javafx.util.StringConverter;
import javafx.util.converter.CurrencyStringConverter; import javafx.util.converter.CurrencyStringConverter;
import javafx.util.converter.NumberStringConverter; import javafx.util.converter.NumberStringConverter;
import lombok.Setter; import lombok.Setter;
@@ -99,12 +100,24 @@ public class ContractTabSkinItemsV2
CurrencyStringConverter currencyStringConverter = new CurrencyStringConverter(numberInstance); CurrencyStringConverter currencyStringConverter = new CurrencyStringConverter(numberInstance);
idColumn.setCellValueFactory(param -> param.getValue().getId()); idColumn.setCellValueFactory(param -> param.getValue().getId());
titleColumn.setCellValueFactory(param -> param.getValue().getTitle()); titleColumn.setCellValueFactory(param -> param.getValue().getTitle());
specificationColumn.setCellValueFactory(param -> param.getValue().getSpecification()); specificationColumn.setCellValueFactory(param -> param.getValue().getSpecification());
unitColumn.setCellValueFactory(param -> param.getValue().getUnit()); unitColumn.setCellValueFactory(param -> param.getValue().getUnit());
inventoryColumn.setCellValueFactory(param -> param.getValue().getInventory()); inventoryColumn.setCellValueFactory(param -> param.getValue().getInventory());
inventoryColumn.setCellFactory(InventoryTableCell.forTableColumn(getInventoryService())); inventoryColumn.setCellFactory(
InventoryTableCell.forTableColumn(getInventoryService(), new StringConverter<InventoryVo>() {
@Override
public String toString(InventoryVo object) {
return object != null ? object.getCode() : "";
}
@Override
public InventoryVo fromString(String string) {
return getInventoryService().findByCode(string);
}
}));
exclusiveTaxPriceColumn.setCellValueFactory(param -> param.getValue().getExclusiveTaxPrice()); exclusiveTaxPriceColumn.setCellValueFactory(param -> param.getValue().getExclusiveTaxPrice());
exclusiveTaxPriceColumn.setCellFactory(TextFieldTableCell.forTableColumn(currencyStringConverter)); exclusiveTaxPriceColumn.setCellFactory(TextFieldTableCell.forTableColumn(currencyStringConverter));

View File

@@ -0,0 +1,86 @@
package com.ecep.contract.controller.table.cell;
import com.ecep.contract.CompanyFileType;
import com.ecep.contract.SpringApp;
import com.ecep.contract.service.CompanyFileTypeService;
import com.ecep.contract.vo.CompanyFileTypeLocalVo;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
/**
* 公司文件类型单元格,用于在表格中显示公司文件类型信息
*/
public class CompanyFileTypeTableCell<T> extends AsyncUpdateTableCell<T, CompanyFileType, CompanyFileTypeLocalVo> {
private CompanyFileTypeService companyFileTypeService;
/**
* 创建一个用于表格列的单元格工厂
*/
public static <T> Callback<TableColumn<T, CompanyFileType>, TableCell<T, CompanyFileType>> forTableColumn(
CompanyFileTypeService service) {
return param -> new CompanyFileTypeTableCell<>(service);
}
public CompanyFileTypeTableCell() {
}
public CompanyFileTypeTableCell(CompanyFileTypeService service) {
setService(service);
}
@Override
protected CompanyFileTypeService getServiceBean() {
if (companyFileTypeService == null) {
companyFileTypeService = SpringApp.getBean(CompanyFileTypeService.class);
}
return companyFileTypeService;
}
@Override
protected CompanyFileTypeLocalVo initialize() {
CompanyFileType item = getItem();
if (item == null) {
return null;
}
return getServiceBean().findByLocaleAndType(com.ecep.contract.Desktop.instance.getActiveEmployee().localeProperty().get(), item);
}
@Override
public String format(CompanyFileTypeLocalVo entity) {
if (entity != null && entity.getValue() != null) {
return entity.getValue();
}
CompanyFileType item = getItem();
if (item != null) {
// 根据枚举值返回对应的中文名称
switch (item) {
case General -> {
return "普通文件";
}
case CreditReport -> {
return "资信评估报告";
}
case BusinessLicense -> {
return "营业执照";
}
case QualificationCertificate -> {
return "资质证书";
}
case CreditInfoPublicityReport -> {
return "企业信用信息公示报告";
}
case OperationCertificate -> {
return "操作证";
}
case FrameworkAgreement -> {
return "框架协议";
}
default -> {
return item.name();
}
}
}
return "未知类型";
}
}

View File

@@ -7,6 +7,7 @@ import com.ecep.contract.vo.InventoryCatalogVo;
import com.ecep.contract.vo.InventoryVo; import com.ecep.contract.vo.InventoryVo;
import javafx.util.Callback; import javafx.util.Callback;
import javafx.util.StringConverter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
@@ -22,6 +23,23 @@ public class InventoryTableCell<V> extends AsyncUpdateTableCell<V, Integer, Inve
return param -> new InventoryTableCell<>(inventoryService); return param -> new InventoryTableCell<>(inventoryService);
} }
/**
* 创建单元格工厂支持自定义StringConverter
*
* @param inventoryService 库存服务
* @param converter 字符串转换器
* @return 单元格工厂
*/
public static <V> Callback<javafx.scene.control.TableColumn<V, Integer>, javafx.scene.control.TableCell<V, Integer>> forTableColumn(
InventoryService inventoryService, StringConverter<InventoryVo> converter) {
return param -> new InventoryTableCell<>(inventoryService) {
@Override
public String format(InventoryVo entity) {
return converter.toString(entity);
}
};
}
private InventoryCatalogService inventoryCatalogService; private InventoryCatalogService inventoryCatalogService;
public InventoryTableCell(InventoryService service) { public InventoryTableCell(InventoryService service) {

View File

@@ -3,6 +3,10 @@ package com.ecep.contract.controller.table.cell;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
import com.ecep.contract.MyDateTimeUtils; import com.ecep.contract.MyDateTimeUtils;
/** /**
@@ -11,6 +15,40 @@ import com.ecep.contract.MyDateTimeUtils;
public class LocalDateTimeTableCell<T> public class LocalDateTimeTableCell<T>
extends javafx.scene.control.TableCell<T, java.time.LocalDateTime> { extends javafx.scene.control.TableCell<T, java.time.LocalDateTime> {
/**
* 静态工厂方法创建TableCell的回调工厂
*
* @param <T> 表格数据类型
* @return TableCell的回调工厂
*/
public static <T> Callback<TableColumn<T, LocalDateTime>, TableCell<T, LocalDateTime>> forTableColumn() {
return column -> new LocalDateTimeTableCell<>();
}
/**
* 静态工厂方法使用指定格式创建TableCell的回调工厂
*
* @param <T> 表格数据类型
* @param format 日期时间格式
* @return TableCell的回调工厂
*/
public static <T> Callback<TableColumn<T, LocalDateTime>, TableCell<T, LocalDateTime>> forTableColumn(
String format) {
return column -> new LocalDateTimeTableCell<>(format);
}
/**
* 静态工厂方法使用指定的DateTimeFormatter创建TableCell的回调工厂
*
* @param <T> 表格数据类型
* @param formatter DateTimeFormatter对象
* @return TableCell的回调工厂
*/
public static <T> Callback<TableColumn<T, LocalDateTime>, TableCell<T, LocalDateTime>> forTableColumn(
DateTimeFormatter formatter) {
return column -> new LocalDateTimeTableCell<>(formatter);
}
private final DateTimeFormatter formatter; private final DateTimeFormatter formatter;
public LocalDateTimeTableCell(DateTimeFormatter formatter) { public LocalDateTimeTableCell(DateTimeFormatter formatter) {
@@ -34,4 +72,5 @@ public class LocalDateTimeTableCell<T>
setText(item.format(formatter)); setText(item.format(formatter));
} }
} }
} }

View File

@@ -24,6 +24,7 @@ public class ProxyObjectDeserializerModifier extends BeanDeserializerModifier {
} }
@Override @Override
@SuppressWarnings("unchecked")
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc, JsonDeserializer<?> deserializer) { BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
// 检查是否是IdentityEntity的实现类 // 检查是否是IdentityEntity的实现类

View File

@@ -48,200 +48,6 @@ public class CompanyFileService extends QueryService<CompanyFileVo, CompanyFileV
Pageable.unpaged()).getContent(); Pageable.unpaged()).getContent();
} }
public boolean reBuildingFiles(CompanyVo company, MessageHolder holder) {
List<CompanyFileVo> dbFiles = findByCompany(company);
List<CompanyFileVo> retrieveFiles = new ArrayList<>();
boolean modfied = false;
Map<String, CompanyFileVo> map = new HashMap<>();
// 排除掉数据库中重复的
for (CompanyFileVo dbFile : dbFiles) {
String filePath = dbFile.getFilePath();
// 没有文件信息,无效记录,删除
if (!StringUtils.hasText(filePath)) {
delete(dbFile);
modfied = true;
continue;
}
// 目录不存在,删除
File dir = new File(filePath);
if (!dir.exists()) {
delete(dbFile);
modfied = true;
continue;
}
CompanyFileVo old = map.put(filePath, dbFile);
// 目录有重复删除
if (old != null) {
delete(old);
modfied = true;
}
}
Map<String, File> directoryMap = new HashMap<>();
// 公司目录
if (StringUtils.hasText(company.getPath())) {
File dir = new File(company.getPath());
directoryMap.put(company.getName(), dir);
}
// 获取所有曾用名
List<CompanyOldNameVo> oldNames = SpringApp.getBean(CompanyOldNameService.class).findAllByCompany(company);
for (CompanyOldNameVo companyOldName : oldNames) {
String path = companyOldName.getPath();
if (StringUtils.hasText(path)) {
File dir = new File(path);
directoryMap.put(companyOldName.getName(), dir);
}
}
for (Map.Entry<String, File> entry : directoryMap.entrySet()) {
String companyName = entry.getKey();
File dir = entry.getValue();
if (!StringUtils.hasText(companyName)) {
continue;
}
if (dir == null || !dir.exists()) {
continue;
}
File[] files = dir.listFiles();
if (files == null) {
// 文件系统出错或者没有相关文件
continue;
}
for (File file : files) {
// 只处理文件
if (!file.isFile() || FileUtils.isHiddenFile(file)) {
continue;
}
String filePath = file.getAbsolutePath();
if (!map.containsKey(filePath)) {
// 未记录
CompanyFileVo filled = fillFileType(file, holder);
retrieveFiles.add(filled);
}
}
}
holder.info("导入 " + retrieveFiles.size() + " 个文件");
if (retrieveFiles.isEmpty()) {
return modfied;
}
// update db
retrieveFiles.forEach(v -> {
v.setCompanyId(company.getId());
save(v);
});
return true;
}
/**
* 从文件名生成公司文件对象,文件已经存在公司对应的存储目录下
*
* @param file 文件
* @param holder 状态输出
* @return 公司文件对象
*/
private CompanyFileVo fillFileType(File file, MessageHolder holder) {
String fileName = file.getName();
CompanyFileVo companyFile = new CompanyFileVo();
companyFile.setType(CompanyFileType.General);
companyFile.setFilePath(file.getAbsolutePath());
fillApplyDateAndExpiringDateAbsent(file, companyFile);
// 天眼查 基础版企业信用报告
if (fileName.contains(CloudServiceConstant.TYC_ENTERPRISE_BASIC_REPORT)
|| fileName.contains(CloudServiceConstant.TYC_ENTERPRISE_MAJOR_REPORT)
|| fileName.contains(CloudServiceConstant.TYC_ENTERPRISE_ANALYSIS_REPORT)) {
companyFile.setType(CompanyFileType.CreditReport);
fillExpiringDateAbsent(companyFile);
return companyFile;
}
// 天眼查 企业信用信息公示报告
if (fileName.contains(CloudServiceConstant.TYC_ENTERPRISE_CREDIT_REPORT)) {
companyFile.setType(CompanyFileType.CreditInfoPublicityReport);
return companyFile;
}
// 集团相关方平台 元素征信 企业征信报告
if (fileName.contains(CloudServiceConstant.RK_VENDOR_NAME)
&& fileName.contains(CloudServiceConstant.RK_ENTERPRISE_CREDIT_REPORT)) {
companyFile.setType(CompanyFileType.CreditReport);
fillExpiringDateAbsent(companyFile);
return companyFile;
}
// 营业执照
if (fileName.contains(CompanyConstant.BUSINESS_LICENSE)) {
companyFile.setType(CompanyFileType.BusinessLicense);
return companyFile;
}
// 其他企业信用报告
if (fileName.contains(CompanyConstant.ENTERPRISE_REPORT)) {
companyFile.setType(CompanyFileType.CreditReport);
fillExpiringDateAbsent(companyFile);
return companyFile;
}
return companyFile;
}
/**
* 补齐有效期
*/
private void fillExpiringDateAbsent(CompanyFileVo file) {
LocalDate expiringDate = file.getExpiringDate();
if (expiringDate == null) {
LocalDate applyDate = file.getApplyDate();
if (applyDate != null) {
expiringDate = applyDate.plusYears(1);
file.setExpiringDate(expiringDate);
}
}
}
private static void fillApplyDateAndExpiringDateAbsent(File file, CompanyFileVo companyFile) {
LocalDate applyDate = companyFile.getApplyDate();
if (applyDate != null) {
return;
}
String fileName = file.getName();
Pattern pattern = Pattern.compile(MyDateTimeUtils.REGEX_DATE);
Matcher matcher = pattern.matcher(fileName);
while (matcher.find()) {
// 找到第一个日期,记作起始日期
String date = matcher.group();
try {
LocalDate n = LocalDate.parse(date);
companyFile.setApplyDate(n);
// 如果 截至日期未设置,则第二个日期记作截至日期(如有)
LocalDate expiringDate = companyFile.getExpiringDate();
if (expiringDate == null) {
while (matcher.find()) {
date = matcher.group();
try {
n = LocalDate.parse(date);
companyFile.setExpiringDate(n);
break;
} catch (Exception ignored) {
}
}
}
break;
} catch (Exception ignored) {
}
}
}
public void verify(CompanyVo company, LocalDate verifyDate, MessageHolder holder) { public void verify(CompanyVo company, LocalDate verifyDate, MessageHolder holder) {
// 查询公司的资信评估报告 // 查询公司的资信评估报告
List<CompanyFileVo> files = findFileByCompanyAndType(company, CompanyFileType.CreditReport); List<CompanyFileVo> files = findFileByCompanyAndType(company, CompanyFileType.CreditReport);
@@ -295,197 +101,4 @@ public class CompanyFileService extends QueryService<CompanyFileVo, CompanyFileV
throw new UnsupportedOperationException("Unimplemented method 'copyAsMatchedByContract'"); throw new UnsupportedOperationException("Unimplemented method 'copyAsMatchedByContract'");
} }
/**
* 移动文件到企业目录下
*
* @param company 企业对象
* @param files 要被移动的文件集合,需要从中选择需要的
* @param holder 状态输出
*/
public boolean retrieveFromDownloadFiles(CompanyVo company, File[] files, MessageHolder holder) {
Map<String, File> map = new HashMap<>();
File home = new File(company.getPath());
map.put(company.getName(), home);
List<CompanyFileVo> retrieveFiles = new ArrayList<>();
List<CompanyOldNameVo> oldNames = SpringApp.getBean(CompanyOldNameService.class).findAllByCompany(company);
// 获取所有曾用名
for (CompanyOldNameVo companyOldName : oldNames) {
String name = companyOldName.getName();
if (!StringUtils.hasText(name)) {
continue;
}
File dir = null;
String path = companyOldName.getPath();
if (StringUtils.hasText(path)) {
dir = new File(path);
}
map.put(name, dir);
}
// 对所有文件进行遍历
for (int i = 0; i < files.length; i++) {
File file = files[i];
// 只处理文件
if (!file.isFile()) {
continue;
}
MessageHolder sub = holder.sub("[" + (i + 1) + "/" + files.length + "]");
String fileName = file.getName();
sub.info(fileName);
for (Map.Entry<String, File> entry : map.entrySet()) {
String companyName = entry.getKey();
// 必须要包含公司名称否则无法区分
if (!fileName.contains(companyName)) {
continue;
}
// 文件存储的目的地目录
File dir = entry.getValue();
if (dir == null) {
dir = home;
}
CompanyFileVo filled = fillDownloadFileType(company, file, companyName, dir, sub);
if (filled != null) {
retrieveFiles.add(filled);
}
}
}
holder.info("导入 " + retrieveFiles.size() + " 个文件");
if (retrieveFiles.isEmpty()) {
return false;
}
// update db
retrieveFiles.forEach(v -> {
v.setCompanyId(company.getId());
save(v);
});
return true;
}
/**
* 从文件名生成公司文件对象
* 文件从下载目录中导入
*
* @param company 公司对象
* @param file 导入的文件对象
* @param companyName 公司名称
* @param destDir 目标目录
* @param holder 状态输出
* @return 生成的公司文件对象如果无法转换则返回null
*/
private CompanyFileVo fillDownloadFileType(CompanyVo company, File file, String companyName, File destDir,
MessageHolder holder) {
String fileName = file.getName();
// 天眼查的报告
// 目前只有 基础版企业信用报告, 企业信用信息公示报告下载保存时的文件名中没有天眼查
if (CloudTycService.isTycReport(fileName)) {
CompanyFileVo companyFile = new CompanyFileVo();
companyFile.setType(CompanyFileType.CreditReport);
fillApplyDateAbsent(file, companyFile);
String destFileName = fileName;
// 重命名 基础版企业信用报告
for (String report : Arrays.asList(
CloudServiceConstant.TYC_ENTERPRISE_ANALYSIS_REPORT,
CloudServiceConstant.TYC_ENTERPRISE_BASIC_REPORT,
CloudServiceConstant.TYC_ENTERPRISE_MAJOR_REPORT)) {
if (fileName.contains(report)) {
LocalDate applyDate = companyFile.getApplyDate();
if (applyDate == null) {
applyDate = LocalDate.now();
companyFile.setApplyDate(applyDate);
}
String formatted = MyDateTimeUtils.format(applyDate);
String extension = StringUtils.getFilenameExtension(fileName);
destFileName = String.format("%s_%s_%s_%s.%s",
companyName, CloudServiceConstant.TYC_NAME, report, formatted, extension);
}
}
// 重新设置 企业分析报告 未普通文件
// if (fileName.contains(CloudTycService.TYC_ENTERPRISE_ANALYSIS_REPORT)) {
// companyFile.setType(General);
// }
File dest = new File(destDir, destFileName);
// 移动文件
if (!file.renameTo(dest)) {
// 移动失败时
holder.warn(fileName + " 无法移动到 " + dest.getAbsolutePath());
return null;
}
holder.info(fileName + " 移动到 " + dest.getAbsolutePath());
companyFile.setFilePath(dest.getAbsolutePath());
//
if (companyFile.getExpiringDate() == null) {
if (companyFile.getApplyDate() != null) {
companyFile.setExpiringDate(companyFile.getApplyDate().plusYears(1));
}
}
return companyFile;
}
// 企业信用信息公示报告
if (fileName.contains(CloudServiceConstant.TYC_ENTERPRISE_CREDIT_REPORT)) {
CompanyFileVo companyFile = new CompanyFileVo();
companyFile.setType(CompanyFileType.CreditInfoPublicityReport);
fillApplyDateAbsent(file, companyFile);
File dest = new File(destDir, fileName);
// 移动文件
if (!file.renameTo(dest)) {
if (dest.exists()) {
// 尝试删除已经存在的文件
if (!dest.delete()) {
holder.warn("覆盖时,无法删除已存在的文件 " + dest.getAbsolutePath());
return null;
}
if (file.renameTo(dest)) {
List<CompanyFileVo> files = findByCompanyAndPath(company, dest.getAbsolutePath());
if (!files.isEmpty()) {
companyFile = files.getFirst();
}
} else {
holder.error(fileName + " 无法覆盖到 " + dest.getAbsolutePath());
return null;
}
} else {
holder.error(fileName + " 无法移动到 " + dest.getAbsolutePath());
return null;
}
}
holder.info(fileName + " 移动到 " + dest.getAbsolutePath());
companyFile.setFilePath(dest.getAbsolutePath());
return companyFile;
}
return null;
}
/**
* 当 ApplyDate 未设置时,尝试使用文件名中包含的日期
*/
private static void fillApplyDateAbsent(File file, CompanyFileVo companyFile) {
LocalDate applyDate = companyFile.getApplyDate();
if (applyDate != null) {
return;
}
String fileName = file.getName();
// 从文件名中提取日期
LocalDate picked = MyDateTimeUtils.pickLocalDate(fileName);
if (picked != null) {
companyFile.setApplyDate(picked);
}
}
} }

View File

@@ -6,7 +6,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javafx.util.StringConverter;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
@@ -16,13 +15,13 @@ import org.springframework.util.StringUtils;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.MyDateTimeUtils; import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.SpringApp;
import com.ecep.contract.constant.CompanyConstant; import com.ecep.contract.constant.CompanyConstant;
import com.ecep.contract.model.Company;
import com.ecep.contract.util.FileUtils; import com.ecep.contract.util.FileUtils;
import com.ecep.contract.vm.CompanyViewModel; import com.ecep.contract.vm.CompanyViewModel;
import com.ecep.contract.vo.CompanyVo; import com.ecep.contract.vo.CompanyVo;
import javafx.util.StringConverter;
@Service @Service
@CacheConfig(cacheNames = "company") @CacheConfig(cacheNames = "company")
public class CompanyService extends QueryService<CompanyVo, CompanyViewModel> { public class CompanyService extends QueryService<CompanyVo, CompanyViewModel> {
@@ -117,20 +116,22 @@ public class CompanyService extends QueryService<CompanyVo, CompanyViewModel> {
} }
} }
public boolean makePathAbsent(CompanyVo company) { public boolean makePathAbsent(CompanyVo company, MessageHolder holder) {
String path = company.getPath(); String path = company.getPath();
if (StringUtils.hasText(path)) { if (StringUtils.hasText(path)) {
File file = new File(path); File file = new File(path);
if (file.exists()) { if (file.exists()) {
// holder.error("存储目录已存在,请检查:" + file.getAbsolutePath());
return false; return false;
} }
} }
File dir = makePath(company); File dir = makePath(company, holder.sub("父级"));
if (dir == null) { if (dir == null) {
return false; return false;
} }
if (!dir.exists()) { if (!dir.exists()) {
holder.info("父级目录不存在:" + dir.getAbsolutePath());
return false; return false;
} }
company.setPath(dir.getAbsolutePath()); company.setPath(dir.getAbsolutePath());
@@ -144,71 +145,39 @@ public class CompanyService extends QueryService<CompanyVo, CompanyViewModel> {
* @param company 要创建的企业对象 * @param company 要创建的企业对象
* @return 目录 * @return 目录
*/ */
public File makePath(CompanyVo company) { public File makePath(CompanyVo company, MessageHolder holder) {
File basePath = getBasePath(); File basePath = getBasePath();
if (!basePath.exists()) { if (!basePath.exists()) {
holder.error("存储目录不存在:" + basePath.getAbsolutePath());
return null; return null;
} }
String companyName = company.getName(); String companyName = company.getName();
String district = company.getDistrict(); String district = company.getDistrict();
if (StringUtils.hasText(district)) { if (!StringUtils.hasText(district)) {
String parentPrefix = FileUtils.getParentPrefixByDistrict(district); holder.error("区域异常:未设置");
if (parentPrefix != null) { return null;
File parent = new File(basePath, parentPrefix); }
if (!parent.exists()) { String parentPrefix = FileUtils.getParentPrefixByDistrict(district);
if (!parent.mkdir()) { if (parentPrefix == null) {
return null; holder.error("区域异常:" + district);
} return null;
} }
String fileName = FileUtils.escapeFileName(companyName); File parent = new File(basePath, parentPrefix);
File dir = new File(parent, fileName); if (!parent.exists()) {
if (!dir.exists()) { if (!parent.mkdir()) {
if (!dir.mkdir()) { holder.error("创建目录失败:" + parent.getAbsolutePath());
return null; return null;
}
}
return dir;
} }
} }
return null; String fileName = FileUtils.escapeFileName(companyName);
} File dir = new File(parent, fileName);
if (!dir.exists()) {
if (!dir.mkdir()) {
/** holder.error("创建目录失败:" + dir.getAbsolutePath());
* 移动文件到企业目录下 return null;
*
* @param company 企业对象
* @param files 要被移动的文件集合,需要从中选择需要的
* @param holder 状态输出
*/
public boolean retrieveFromDownloadFiles(CompanyVo company, File[] files, MessageHolder holder) {
//
boolean companyChanged = makePathAbsent(company);
if (!StringUtils.hasText(company.getPath())) {
// fixed 要退出,需要保存
if (companyChanged) {
save(company);
} }
holder.error("存储目录未设置,请检查");
return false;
} }
return dir;
File home = new File(company.getPath());
if (!home.exists()) {
// fixed 要退出,需要保存
if (companyChanged) {
company = save(company);
}
holder.error(company.getPath() + " 不存在,无法访问,请检查或者修改");
return false;
}
CompanyFileService fileService = SpringApp.getBean(CompanyFileService.class);
boolean retrieved = fileService.retrieveFromDownloadFiles(company, files, holder);
if (companyChanged) {
save(company);
}
return retrieved;
} }
@Override @Override

View File

@@ -3,6 +3,7 @@ package com.ecep.contract.service;
import java.io.File; import java.io.File;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheConfig;
@@ -12,17 +13,17 @@ import org.springframework.cache.annotation.Caching;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.constant.ContractConstant; import com.ecep.contract.constant.ContractConstant;
import com.ecep.contract.util.ContractUtils;
import com.ecep.contract.util.ParamUtils; import com.ecep.contract.util.ParamUtils;
import com.ecep.contract.vm.ContractViewModel; import com.ecep.contract.vm.ContractViewModel;
import com.ecep.contract.vo.VendorVo;
import com.ecep.contract.vo.ContractFileVo; import com.ecep.contract.vo.ContractFileVo;
import com.ecep.contract.vo.ContractVo; import com.ecep.contract.vo.ContractVo;
import com.ecep.contract.vo.ProjectVo; import com.ecep.contract.vo.ProjectVo;
import com.ecep.contract.vo.VendorVo;
import io.micrometer.common.util.StringUtils;
@Service @Service
@CacheConfig(cacheNames = "contract") @CacheConfig(cacheNames = "contract")
@@ -69,8 +70,21 @@ public class ContractService extends QueryService<ContractVo, ContractViewModel>
} }
public boolean updateParentCode(ContractVo contract) { public boolean updateParentCode(ContractVo contract) {
// TODO Auto-generated method stub String parentCode = ContractUtils.getParentCode(contract.getCode());
throw new UnsupportedOperationException("Unimplemented method 'updateParentCode'"); if (!StringUtils.hasText(parentCode)) {
return false;
}
// fixed
if (Objects.equals(contract.getParentCode(), parentCode)) {
// 已经相同,跳过
return false;
}
ContractVo parent = findOneByProperty("code", parentCode);
if (parent == null) {
return false;
}
contract.setParentCode(parent.getCode());
return true;
} }
@Cacheable(key = "'code-'+#p0") @Cacheable(key = "'code-'+#p0")
@@ -105,7 +119,7 @@ public class ContractService extends QueryService<ContractVo, ContractViewModel>
public List<ContractVo> findAllBySaleContract(ContractVo contract) { public List<ContractVo> findAllBySaleContract(ContractVo contract) {
String parentCode = contract.getCode(); String parentCode = contract.getCode();
if (StringUtils.isEmpty(parentCode)) { if (StringUtils.hasText(parentCode)) {
return List.of(); return List.of();
} }
return findAll(ParamUtils.equal("parentCode", parentCode), Pageable.unpaged()).getContent(); return findAll(ParamUtils.equal("parentCode", parentCode), Pageable.unpaged()).getContent();

View File

@@ -113,12 +113,7 @@ public class QueryService<T extends IdentityEntity, TV extends IdentityViewModel
} }
public CompletableFuture<JsonNode> async(String method, Object... params) { public CompletableFuture<JsonNode> async(String method, Object... params) {
return webSocketService.invoke(getBeanName(), method, params).handle((response, ex) -> { return webSocketService.invoke(getBeanName(), method, params);
if (ex != null) {
throw new RuntimeException("远程方法+" + method + "+调用失败", ex);
}
return response;
});
} }
public CompletableFuture<T> asyncFindById(Integer id) { public CompletableFuture<T> asyncFindById(Integer id) {
@@ -137,7 +132,7 @@ public class QueryService<T extends IdentityEntity, TV extends IdentityViewModel
return asyncFindById(id).get(); return asyncFindById(id).get();
} catch (Exception e) { } catch (Exception e) {
logger.error("查询实体失败 #{}", id, e); logger.error("查询实体失败 #{}", id, e);
throw new RuntimeException("查询实体失败", e); throw new RuntimeException("查询实体失败 #" + id, e);
} }
} }

View File

@@ -4,7 +4,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.constant.CloudServiceConstant; import com.ecep.contract.WebSocketClientTasker;
import com.ecep.contract.vo.CompanyVo; import com.ecep.contract.vo.CompanyVo;
import lombok.Setter; import lombok.Setter;
@@ -12,7 +12,7 @@ import lombok.Setter;
/** /**
* 合并更新 * 合并更新
*/ */
public class CompanyCompositeUpdateTasker extends Tasker<Object> { public class CompanyCompositeUpdateTasker extends Tasker<Object> implements WebSocketClientTasker {
private static final Logger logger = LoggerFactory.getLogger(CompanyCompositeUpdateTasker.class); private static final Logger logger = LoggerFactory.getLogger(CompanyCompositeUpdateTasker.class);
@Setter @Setter
private CompanyVo company; private CompanyVo company;
@@ -20,14 +20,17 @@ public class CompanyCompositeUpdateTasker extends Tasker<Object> {
@Override @Override
protected Object execute(MessageHolder holder) throws Exception { protected Object execute(MessageHolder holder) throws Exception {
updateTitle("合并更新 " + company.getName()); updateTitle("合并更新 " + company.getName());
return callRemoteTask(holder, getLocale(), company.getId());
}
holder.debug("1. 从 " + CloudServiceConstant.RK_NAME + " 更新..."); @Override
updateProgress(0.1, 1); public String getTaskName() {
holder.debug("2. 从 " + CloudServiceConstant.U8_NAME + " 更新..."); return "CompanyCompositeUpdateTasker";
updateProgress(0.3, 1); }
holder.debug("3. 从 " + CloudServiceConstant.TYC_NAME + " 更新...");
updateProgress(1, 1); @Override
return null; public void updateProgress(long workDone, long max) {
super.updateProgress(workDone, max);
} }
} }

View File

@@ -0,0 +1,123 @@
package com.ecep.contract.task;
import java.io.File;
import org.springframework.util.StringUtils;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.service.CompanyFileService;
import com.ecep.contract.service.CompanyService;
import com.ecep.contract.vo.CompanyFileVo;
import com.ecep.contract.vo.CompanyVo;
import lombok.Getter;
import lombok.Setter;
/**
* 公司文件移动任务类
* 用于将文件从老系统中移动到指定的企业目录中
*/
public class CompanyFileMoveTasker extends Tasker<Boolean> {
@Getter
@Setter
private CompanyVo company;
@Getter
private boolean filesMoved = false;
@Override
protected Boolean execute(MessageHolder holder) throws Exception {
updateTitle("移动公司文件");
updateProgress(0, 100);
if (company == null) {
holder.error("公司对象为空");
updateProgress(100, 100);
return false;
}
try {
holder.info("开始移动公司文件:" + company.getName());
updateProgress(10, 100);
// 获取CompanyFileService
CompanyFileService companyFileService = getCachedBean(CompanyFileService.class);
CompanyService companyService = getCachedBean(CompanyService.class);
// 获取公司的所有文件
holder.info("获取公司文件列表...");
java.util.List<CompanyFileVo> list = companyFileService.findByCompany(company);
updateProgress(20, 100);
if (list.isEmpty()) {
holder.info("该公司没有文件需要移动");
updateProgress(100, 100);
return false;
}
holder.info("共有" + list.size() + "个文件需要处理");
// 确保公司目录存在
holder.info("检查并创建公司目录...");
if (companyService.makePathAbsent(company, holder)) {
company = companyService.save(company);
holder.info("公司目录已创建");
}
updateProgress(30, 100);
// 检查公司路径
String path = company.getPath();
if (!StringUtils.hasText(path)) {
holder.error("异常, 企业目录未设置");
updateProgress(100, 100);
return false;
}
File companyPath = new File(path);
int movedCount = 0;
int totalFiles = list.size();
// 逐个处理文件
for (int i = 0; i < list.size(); i++) {
CompanyFileVo companyFile = list.get(i);
String filePath = companyFile.getFilePath();
// 更新进度
updateProgress(30 + (i * 70) / totalFiles, 100);
if (StringUtils.hasText(filePath)) {
File file = new File(filePath);
if (file.exists()) {
if (file.getParentFile().equals(companyPath)) {
holder.info("文件已在目标位置:" + file.getName());
continue;
}
File dest = new File(companyPath, file.getName());
holder.info("移动文件:" + file.getName() + " -> " + companyPath.getName());
if (file.renameTo(dest)) {
companyFile.setFilePath(dest.getAbsolutePath());
companyFileService.save(companyFile);
holder.info("文件移动成功:" + file.getName());
movedCount++;
filesMoved = true;
} else {
holder.warn("文件移动失败:" + file.getName());
}
} else {
holder.warn("文件不存在:" + filePath);
}
}
}
holder.info("文件移动完成,成功移动" + movedCount + "个文件");
updateProgress(100, 100);
return filesMoved;
} catch (Exception e) {
holder.error("文件移动过程中发生错误:" + e.getMessage());
updateProgress(100, 100);
throw e;
}
}
}

View File

@@ -0,0 +1,274 @@
package com.ecep.contract.task;
import java.io.File;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.StringUtils;
import com.ecep.contract.CompanyFileType;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.constant.CloudServiceConstant;
import com.ecep.contract.constant.CompanyConstant;
import com.ecep.contract.service.CompanyFileService;
import com.ecep.contract.service.CompanyOldNameService;
import com.ecep.contract.util.FileUtils;
import com.ecep.contract.vo.CompanyFileVo;
import com.ecep.contract.vo.CompanyOldNameVo;
import com.ecep.contract.vo.CompanyVo;
import lombok.Getter;
import lombok.Setter;
/**
* 公司文件重置任务类
* 用于本地执行公司文件重置操作不通过WebSocket调用远程服务
*/
public class CompanyFileResetTasker extends Tasker<Boolean> {
@Getter
@Setter
private CompanyVo company;
@Getter
@Setter
private boolean filesUpdated = false;
private CompanyFileService getCompanyFileService() {
return getCachedBean(CompanyFileService.class);
}
private CompanyOldNameService getCompanyOldNameService() {
return getCachedBean(CompanyOldNameService.class);
}
@Override
protected Boolean execute(MessageHolder holder) throws Exception {
updateTitle("重置公司文件");
updateProgress(0, 100);
if (company == null) {
holder.error("公司对象为空");
updateProgress(100, 100);
return false;
}
try {
holder.info("开始重置公司文件:" + company.getName());
updateProgress(10, 100);
boolean result = reBuildingFiles(company, holder);
if (result) {
holder.info("公司文件重置成功");
setFilesUpdated(true);
} else {
holder.info("公司文件重置完成,但没有更新任何文件");
setFilesUpdated(false);
}
updateProgress(100, 100);
return result;
} catch (Exception e) {
holder.error("公司文件重置失败:" + e.getMessage());
updateProgress(100, 100);
throw e;
}
}
public boolean reBuildingFiles(CompanyVo company, MessageHolder holder) {
CompanyFileService companyFileService = getCompanyFileService();
boolean modfied = false;
List<CompanyFileVo> dbFiles = companyFileService.findByCompany(company);
holder.debug("现有 " + dbFiles.size() + " 条文件记录");
Map<String, CompanyFileVo> map = new HashMap<>();
// 排除掉数据库中重复的
for (CompanyFileVo dbFile : dbFiles) {
String filePath = dbFile.getFilePath();
// 没有文件信息,无效记录,删除
if (!StringUtils.hasText(filePath)) {
companyFileService.delete(dbFile);
holder.info("删除无效记录:" + filePath);
modfied = true;
continue;
}
// 目录不存在,删除
File dir = new File(filePath);
if (!dir.exists()) {
companyFileService.delete(dbFile);
holder.info("删除不存在目录记录:" + filePath);
modfied = true;
continue;
}
CompanyFileVo old = map.put(filePath, dbFile);
// 目录有重复删除
if (old != null) {
companyFileService.delete(old);
holder.info("删除重复目录记录:" + filePath);
modfied = true;
}
}
Map<String, File> directoryMap = new HashMap<>();
// 公司目录
if (StringUtils.hasText(company.getPath())) {
File dir = new File(company.getPath());
directoryMap.put(company.getName(), dir);
}
// 获取所有曾用名
List<CompanyOldNameVo> oldNames = getCompanyOldNameService().findAllByCompany(company);
for (CompanyOldNameVo companyOldName : oldNames) {
String path = companyOldName.getPath();
if (StringUtils.hasText(path)) {
File dir = new File(path);
directoryMap.put(companyOldName.getName(), dir);
}
}
for (Map.Entry<String, File> entry : directoryMap.entrySet()) {
String companyName = entry.getKey();
File dir = entry.getValue();
if (!StringUtils.hasText(companyName)) {
continue;
}
if (dir == null || !dir.exists()) {
continue;
}
File[] files = dir.listFiles();
if (files == null) {
// 文件系统出错或者没有相关文件
continue;
}
holder.debug("目录 " + companyName + " 下有 " + files.length + " 个文件");
for (File file : files) {
// 只处理文件
if (!file.isFile() || FileUtils.isHiddenFile(file)) {
continue;
}
String filePath = file.getAbsolutePath();
if (!map.containsKey(filePath)) {
// 未记录
CompanyFileVo filled = fillFileType(file, holder);
filled.setCompanyId(company.getId());
companyFileService.save(filled);
holder.info("导入 " + file.getName() + " ");
modfied = true;
}
}
}
return modfied;
}
/**
* 从文件名生成公司文件对象,文件已经存在公司对应的存储目录下
*
* @param file 文件
* @param holder 状态输出
* @return 公司文件对象
*/
private CompanyFileVo fillFileType(File file, MessageHolder holder) {
String fileName = file.getName();
CompanyFileVo companyFile = new CompanyFileVo();
companyFile.setType(CompanyFileType.General);
companyFile.setFilePath(file.getAbsolutePath());
fillApplyDateAndExpiringDateAbsent(file, companyFile);
// 天眼查 基础版企业信用报告
if (fileName.contains(CloudServiceConstant.TYC_ENTERPRISE_BASIC_REPORT)
|| fileName.contains(CloudServiceConstant.TYC_ENTERPRISE_MAJOR_REPORT)
|| fileName.contains(CloudServiceConstant.TYC_ENTERPRISE_ANALYSIS_REPORT)) {
companyFile.setType(CompanyFileType.CreditReport);
fillExpiringDateAbsent(companyFile);
return companyFile;
}
// 天眼查 企业信用信息公示报告
if (fileName.contains(CloudServiceConstant.TYC_ENTERPRISE_CREDIT_REPORT)) {
companyFile.setType(CompanyFileType.CreditInfoPublicityReport);
return companyFile;
}
// 集团相关方平台 元素征信 企业征信报告
if (fileName.contains(CloudServiceConstant.RK_VENDOR_NAME)
&& fileName.contains(CloudServiceConstant.RK_ENTERPRISE_CREDIT_REPORT)) {
companyFile.setType(CompanyFileType.CreditReport);
fillExpiringDateAbsent(companyFile);
return companyFile;
}
// 营业执照
if (fileName.contains(CompanyConstant.BUSINESS_LICENSE)) {
companyFile.setType(CompanyFileType.BusinessLicense);
return companyFile;
}
// 其他企业信用报告
if (fileName.contains(CompanyConstant.ENTERPRISE_REPORT)) {
companyFile.setType(CompanyFileType.CreditReport);
fillExpiringDateAbsent(companyFile);
return companyFile;
}
return companyFile;
}
/**
* 补齐有效期
*/
private void fillExpiringDateAbsent(CompanyFileVo file) {
LocalDate expiringDate = file.getExpiringDate();
if (expiringDate == null) {
LocalDate applyDate = file.getApplyDate();
if (applyDate != null) {
expiringDate = applyDate.plusYears(1);
file.setExpiringDate(expiringDate);
}
}
}
private static void fillApplyDateAndExpiringDateAbsent(File file, CompanyFileVo companyFile) {
LocalDate applyDate = companyFile.getApplyDate();
if (applyDate != null) {
return;
}
String fileName = file.getName();
Pattern pattern = Pattern.compile(MyDateTimeUtils.REGEX_DATE);
Matcher matcher = pattern.matcher(fileName);
while (matcher.find()) {
// 找到第一个日期,记作起始日期
String date = matcher.group();
try {
LocalDate n = LocalDate.parse(date);
companyFile.setApplyDate(n);
// 如果 截至日期未设置,则第二个日期记作截至日期(如有)
LocalDate expiringDate = companyFile.getExpiringDate();
if (expiringDate == null) {
while (matcher.find()) {
date = matcher.group();
try {
n = LocalDate.parse(date);
companyFile.setExpiringDate(n);
break;
} catch (Exception ignored) {
}
}
}
break;
} catch (Exception ignored) {
}
}
}
}

View File

@@ -0,0 +1,306 @@
package com.ecep.contract.task;
import java.io.File;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import com.ecep.contract.CompanyFileType;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.MyProperties;
import com.ecep.contract.constant.CloudServiceConstant;
import com.ecep.contract.service.CloudTycService;
import com.ecep.contract.service.CompanyFileService;
import com.ecep.contract.service.CompanyOldNameService;
import com.ecep.contract.vo.CompanyFileVo;
import com.ecep.contract.vo.CompanyOldNameVo;
import com.ecep.contract.vo.CompanyVo;
import lombok.Getter;
import lombok.Setter;
/**
* 从下载目录检索公司文件的本地任务器
* 用于在客户端本地执行从下载目录检索相关资质文件的操作
*/
public class CompanyFileRetrieveFromDownloadDirTasker extends Tasker<Object> {
private static final Logger logger = LoggerFactory.getLogger(CompanyFileRetrieveFromDownloadDirTasker.class);
@Getter
@Setter
private CompanyVo company;
@Getter
@Setter
private String downloadDirPath;
@Getter
@Setter
private boolean companyPathChanged = false;
private CompanyFileService getCompanyFileService() {
return getCachedBean(CompanyFileService.class);
}
private CompanyOldNameService getCompanyOldNameService() {
return getCachedBean(CompanyOldNameService.class);
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("从下载目录检索文件");
// 获取下载目录
File dir;
if (StringUtils.hasText(downloadDirPath)) {
dir = new File(downloadDirPath);
} else {
// 如果没有提供下载目录,则使用配置中的
MyProperties myProperties = getBean(MyProperties.class);
dir = myProperties.getDownloadDirectory();
}
// 检查下载目录是否存在
if (!dir.exists()) {
String errorMsg = "下载目录 " + dir.getAbsolutePath() + " 不存在,请检查";
holder.warn(errorMsg);
return false;
}
// 开始检索下载文件夹
holder.info("开始检索 下载 文件夹:" + dir.getAbsolutePath() + "...");
File[] files = dir.listFiles(File::isFile);
// 检查检索是否成功
if (files == null) {
String errorMsg = "检索 下载 文件夹失败";
holder.error(errorMsg);
return false;
}
// 检查文件夹是否有文件
if (files.length == 0) {
String errorMsg = "下载 文件夹没有文件";
holder.info(errorMsg);
return false;
}
// 报告文件夹中的文件数量
holder.info("下载 文件夹中共有文件 " + files.length + " 个文件");
updateProgress(20, 100);
//
companyPathChanged = getCompanyService().makePathAbsent(company, holder);
if (companyPathChanged) {
holder.info("企业目录已创建或更新:" + company.getPath());
company = getCompanyService().save(company);
}
updateProgress(25, 100);
// 检查企业目录是否存在
if (!StringUtils.hasText(company.getPath())) {
holder.error("企业存储目录未设置,请检查");
return false;
}
// 检查企业目录是否存在
File home = new File(company.getPath());
if (!home.exists()) {
holder.error(company.getPath() + " 不存在,无法访问,请检查或者修改");
return false;
}
// 获取所有曾用名
List<CompanyOldNameVo> oldNames = getCompanyOldNameService().findAllByCompany(company);
holder.info("企业曾用名共有 " + oldNames.size() + "");
// 按照 企业名字 移动到对应的曾用名目录
Map<String, File> oldNameOfDir = new HashMap<>();
oldNameOfDir.put(company.getName(), home);
toMap(oldNames, oldNameOfDir);
updateProgress(30, 100);
// 执行文件检索操作
for (int i = 0; i < files.length; i++) {
updateProgress(30 + (i * 50) / files.length, 100);
File file = files[i];
// 只处理文件
if (!file.isFile()) {
continue;
}
MessageHolder sub = holder.sub("[" + (i + 1) + "/" + files.length + "]");
String fileName = file.getName();
for (Map.Entry<String, File> entry : oldNameOfDir.entrySet()) {
String companyName = entry.getKey();
// 必须要包含公司名称否则无法区分
if (!fileName.contains(companyName)) {
sub.debug(fileName);
continue;
}
// 文件存储的目的地目录
File destPath = entry.getValue();
if (destPath == null) {
destPath = home;
}
sub.info(fileName + " -> " + destPath);
CompanyFileVo filled = fillDownloadFileType(company, file, companyName, destPath, sub);
if (filled != null) {
filled.setCompanyId(company.getId());
getCompanyFileService().save(filled);
holder.info("导入 " + fileName + "" + destPath.getAbsolutePath());
}
}
}
updateProgress(100, 100);
return null;
}
void toMap(List<CompanyOldNameVo> oldNames, Map<String, File> oldNameOfDir) {
for (CompanyOldNameVo oldName : oldNames) {
String name = oldName.getName();
if (!StringUtils.hasText(name)) {
continue;
}
File dir = null;
String path = oldName.getPath();
if (StringUtils.hasText(path)) {
dir = new File(path);
}
oldNameOfDir.put(name, dir);
}
}
/**
* 从文件名生成公司文件对象
* 文件从下载目录中导入
*
* @param company 公司对象
* @param file 导入的文件对象
* @param companyName 公司名称
* @param destDir 目标目录
* @param holder 状态输出
* @return 生成的公司文件对象如果无法转换则返回null
*/
private CompanyFileVo fillDownloadFileType(CompanyVo company, File file, String companyName, File destDir,
MessageHolder holder) {
String fileName = file.getName();
// 天眼查的报告
// 目前只有 基础版企业信用报告, 企业信用信息公示报告下载保存时的文件名中没有天眼查
if (CloudTycService.isTycReport(fileName)) {
CompanyFileVo companyFile = new CompanyFileVo();
companyFile.setType(CompanyFileType.CreditReport);
fillApplyDateAbsent(file, companyFile);
String destFileName = fileName;
// 重命名 基础版企业信用报告
for (String report : Arrays.asList(
CloudServiceConstant.TYC_ENTERPRISE_ANALYSIS_REPORT,
CloudServiceConstant.TYC_ENTERPRISE_BASIC_REPORT,
CloudServiceConstant.TYC_ENTERPRISE_MAJOR_REPORT)) {
if (fileName.contains(report)) {
LocalDate applyDate = companyFile.getApplyDate();
if (applyDate == null) {
applyDate = LocalDate.now();
companyFile.setApplyDate(applyDate);
}
String formatted = MyDateTimeUtils.format(applyDate);
String extension = StringUtils.getFilenameExtension(fileName);
destFileName = String.format("%s_%s_%s_%s.%s",
companyName, CloudServiceConstant.TYC_NAME, report, formatted, extension);
}
}
// 重新设置 企业分析报告 未普通文件
// if (fileName.contains(CloudTycService.TYC_ENTERPRISE_ANALYSIS_REPORT)) {
// companyFile.setType(General);
// }
File dest = new File(destDir, destFileName);
// 移动文件
if (!file.renameTo(dest)) {
// 移动失败时
holder.warn(fileName + " 无法移动到 " + dest.getAbsolutePath());
return null;
}
holder.info(fileName + " 移动到 " + dest.getAbsolutePath());
companyFile.setFilePath(dest.getAbsolutePath());
//
if (companyFile.getExpiringDate() == null) {
if (companyFile.getApplyDate() != null) {
companyFile.setExpiringDate(companyFile.getApplyDate().plusYears(1));
}
}
return companyFile;
}
// 企业信用信息公示报告
if (fileName.contains(CloudServiceConstant.TYC_ENTERPRISE_CREDIT_REPORT)) {
CompanyFileVo companyFile = new CompanyFileVo();
companyFile.setType(CompanyFileType.CreditInfoPublicityReport);
fillApplyDateAbsent(file, companyFile);
File dest = new File(destDir, fileName);
// 移动文件
if (!file.renameTo(dest)) {
if (dest.exists()) {
// 尝试删除已经存在的文件
if (!dest.delete()) {
holder.warn("覆盖时,无法删除已存在的文件 " + dest.getAbsolutePath());
return null;
}
if (file.renameTo(dest)) {
List<CompanyFileVo> files = getCompanyFileService().findByCompanyAndPath(company,
dest.getAbsolutePath());
if (!files.isEmpty()) {
companyFile = files.getFirst();
}
} else {
holder.error(fileName + " 无法覆盖到 " + dest.getAbsolutePath());
return null;
}
} else {
holder.error(fileName + " 无法移动到 " + dest.getAbsolutePath());
return null;
}
}
holder.info(fileName + " 移动到 " + dest.getAbsolutePath());
companyFile.setFilePath(dest.getAbsolutePath());
return companyFile;
}
return null;
}
/**
* 当 ApplyDate 未设置时,尝试使用文件名中包含的日期
*/
private static void fillApplyDateAbsent(File file, CompanyFileVo companyFile) {
LocalDate applyDate = companyFile.getApplyDate();
if (applyDate != null) {
return;
}
String fileName = file.getName();
// 从文件名中提取日期
LocalDate picked = MyDateTimeUtils.pickLocalDate(fileName);
if (picked != null) {
companyFile.setApplyDate(picked);
}
}
}

View File

@@ -1,20 +1,35 @@
package com.ecep.contract.task; package com.ecep.contract.task;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
import com.ecep.contract.vo.CompanyVo; import com.ecep.contract.vo.CompanyVo;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
public class CompanyVerifyTasker extends Tasker<Object> { public class CompanyVerifyTasker extends Tasker<Object> implements WebSocketClientTasker {
@Getter @Getter
@Setter @Setter
private CompanyVo company; private CompanyVo company;
@Getter
@Setter
boolean passed = false;
@Override @Override
protected Object execute(MessageHolder holder) throws Exception { protected Object execute(MessageHolder holder) throws Exception {
updateTitle("验证企业是否符合合规要求"); updateTitle("验证企业是否符合合规要求");
return null; return callRemoteTask(holder, getLocale(), company.getId());
}
@Override
public String getTaskName() {
return "CompanyVerifyTasker";
}
@Override
public void updateProgress(long workDone, long max) {
super.updateProgress(workDone, max);
} }
} }

View File

@@ -1,18 +1,32 @@
package com.ecep.contract.task; package com.ecep.contract.task;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
import com.ecep.contract.vo.ContractVo; import com.ecep.contract.vo.ContractVo;
import lombok.Getter;
import lombok.Setter; import lombok.Setter;
public class ContractVerifyTasker extends Tasker<Object> { public class ContractVerifyTasker extends Tasker<Object> implements WebSocketClientTasker {
@Setter @Setter
private ContractVo contract; private ContractVo contract;
@Getter
@Setter
boolean passed = false;
@Override
public String getTaskName() {
return getClass().getSimpleName();
}
@Override
public void updateProgress(long current, long total) {
super.updateProgress(current, total);
}
@Override @Override
protected Object execute(MessageHolder holder) throws Exception { protected Object execute(MessageHolder holder) throws Exception {
// TODO Auto-generated method stub return callRemoteTask(holder, getLocale(), contract.getId());
throw new UnsupportedOperationException("Unimplemented method 'execute'");
} }
} }

View File

@@ -50,7 +50,7 @@ public abstract class Tasker<T> extends Task<T> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
K bean = (K) cachedMap.get(requiredType); K bean = (K) cachedMap.get(requiredType);
if (bean == null) { if (bean == null) {
bean = getBean(requiredType); bean = SpringApp.getBean(requiredType);
cachedMap.put(requiredType, bean); cachedMap.put(requiredType, bean);
} }
return bean; return bean;

View File

@@ -3,8 +3,8 @@ package com.ecep.contract.vm;
import com.ecep.contract.Desktop; import com.ecep.contract.Desktop;
import com.ecep.contract.MyDateTimeUtils; import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.SpringApp; import com.ecep.contract.SpringApp;
import com.ecep.contract.WebSocketClientService;
import com.ecep.contract.controller.CurrentEmployeeInitialedEvent; import com.ecep.contract.controller.CurrentEmployeeInitialedEvent;
import com.ecep.contract.model.EmployeeRole;
import com.ecep.contract.service.EmployeeService; import com.ecep.contract.service.EmployeeService;
import com.ecep.contract.vo.EmployeeRoleVo; import com.ecep.contract.vo.EmployeeRoleVo;
import com.ecep.contract.vo.EmployeeVo; import com.ecep.contract.vo.EmployeeVo;
@@ -138,11 +138,34 @@ public class CurrentEmployee extends EmployeeViewModel {
* 3. 更新当前用户的信息 * 3. 更新当前用户的信息
* 4. 更新当前用户的角色 * 4. 更新当前用户的角色
*/ */
executorService.submit(() -> { // issue #1 sss 2020-07-05
// issue #1 sss 2020-07-05 Thread.ofVirtual().start(() -> {
while (getId().get() <= 0) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
int activeEmployeeId = getId().get();
if (activeEmployeeId <= 0) {
return;
}
EmployeeService employeeService = SpringApp.getBean(EmployeeService.class); EmployeeService employeeService = SpringApp.getBean(EmployeeService.class);
EmployeeVo employee = employeeService.findById(getId().get()); WebSocketClientService webSocketService = SpringApp.getBean(WebSocketClientService.class);
List<EmployeeRoleVo> roles = employeeService.getRolesByEmployeeId(getId().get()); while (!webSocketService.getOnlineProperty().get()) {
try {
logger.debug("waiting for websocket online...");
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
EmployeeVo employee = employeeService.findById(activeEmployeeId);
List<EmployeeRoleVo> roles = employeeService.getRolesByEmployeeId(activeEmployeeId);
Platform.runLater(() -> { Platform.runLater(() -> {
update(employee); update(employee);
rolesProperty().setAll(roles); rolesProperty().setAll(roles);
@@ -151,9 +174,9 @@ public class CurrentEmployee extends EmployeeViewModel {
future.complete(null); future.complete(null);
}); });
}); });
/**
* 定时更新用户活动状态
*/ // 定时更新用户活动状态
executorService.scheduleWithFixedDelay(() -> { executorService.scheduleWithFixedDelay(() -> {
try { try {
// SpringApp.getBean(EmployeeService.class).updateActive(Desktop.instance.getSessionId()); // SpringApp.getBean(EmployeeService.class).updateActive(Desktop.instance.getSessionId());

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.86-SNAPSHOT</version> <version>0.0.99-SNAPSHOT</version>
</parent> </parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>common</artifactId> <artifactId>common</artifactId>
<version>0.0.86-SNAPSHOT</version> <version>0.0.99-SNAPSHOT</version>
<properties> <properties>
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>

View File

@@ -3,7 +3,7 @@ package com.ecep.contract.constant;
public class WebSocketConstant { public class WebSocketConstant {
public static final String MESSAGE_ID_FIELD_NAME = "messageId"; public static final String MESSAGE_ID_FIELD_NAME = "messageId";
public static final String MESSAGE_TYPE_FIELD_NAME = "messageType"; public static final String MESSAGE_TYPE_FIELD_NAME = "messageType";
public static final String SUCCESS_FIELD_VALUE = "success"; public static final String SUCCESS_FIELD_NAME = "success";
public static final String MESSAGE_FIELD_NAME = "message"; public static final String MESSAGE_FIELD_NAME = "message";
public static final String ERROR_CODE_FIELD_NAME = "errorCode"; public static final String ERROR_CODE_FIELD_NAME = "errorCode";
@@ -13,5 +13,9 @@ public class WebSocketConstant {
public static final String SESSION_ID_FIELD_NAME = "sessionId"; public static final String SESSION_ID_FIELD_NAME = "sessionId";
public static final int ERROR_CODE_UNAUTHORIZED = 401; public static final int ERROR_CODE_UNAUTHORIZED = 401;
public static final int ERROR_CODE_NOT_FOUND = 404;
public static final int ERROR_CODE_INTERNAL_SERVER_ERROR = 500;
} }

View File

@@ -17,7 +17,7 @@ import lombok.ToString;
@Setter @Setter
@Entity @Entity
@Table(name = "COMPANY_CUSTOMER_FILE_TYPE_LOCAL") @Table(name = "COMPANY_CUSTOMER_FILE_TYPE_LOCAL")
@ToString @ToString(callSuper = true)
public class CompanyCustomerFileTypeLocal extends BaseEnumEntity<CustomerFileType> implements Serializable, Voable<CompanyCustomerFileTypeLocalVo> { public class CompanyCustomerFileTypeLocal extends BaseEnumEntity<CustomerFileType> implements Serializable, Voable<CompanyCustomerFileTypeLocalVo> {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@@ -19,7 +19,7 @@ import lombok.Setter;
import lombok.ToString; import lombok.ToString;
/** /**
* 公司发票信息 * 公司发票信息(开票)
*/ */
@Getter @Getter
@Setter @Setter

View File

@@ -5,6 +5,8 @@ import java.time.LocalDate;
import java.util.Objects; import java.util.Objects;
import com.ecep.contract.ContractFileType; import com.ecep.contract.ContractFileType;
import com.ecep.contract.model.Voable;
import com.ecep.contract.vo.ContractFileVo;
import com.ecep.contract.util.HibernateProxyUtils; import com.ecep.contract.util.HibernateProxyUtils;
import jakarta.persistence.Column; import jakarta.persistence.Column;
@@ -30,7 +32,7 @@ import lombok.ToString;
@Entity @Entity
@Table(name = "CONTRACT_FILE") @Table(name = "CONTRACT_FILE")
@ToString @ToString
public class ContractFile implements IdentityEntity, ContractBasedEntity, Serializable { public class ContractFile implements IdentityEntity, ContractBasedEntity, Serializable, Voable<ContractFileVo> {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -82,4 +84,23 @@ public class ContractFile implements IdentityEntity, ContractBasedEntity, Serial
public final int hashCode() { public final int hashCode() {
return HibernateProxyUtils.hashCode(this); return HibernateProxyUtils.hashCode(this);
} }
@Override
public ContractFileVo toVo() {
ContractFileVo vo = new ContractFileVo();
// 基本属性映射
vo.setId(this.getId());
vo.setType(this.getType());
vo.setFileName(this.getFileName());
vo.setApplyDate(this.getApplyDate());
vo.setDescription(this.getDescription());
// 关联对象ID映射
if (this.getContract() != null) {
vo.setContractId(this.getContract().getId());
}
return vo;
}
} }

View File

@@ -21,7 +21,7 @@ import lombok.ToString;
@Setter @Setter
@Entity @Entity
@Table(name = "CONTRACT_FILE_TYPE_LOCAL") @Table(name = "CONTRACT_FILE_TYPE_LOCAL")
@ToString @ToString(callSuper = true)
public class ContractFileTypeLocal extends BaseEnumEntity<ContractFileType> implements Serializable, Voable<ContractFileTypeLocalVo> { public class ContractFileTypeLocal extends BaseEnumEntity<ContractFileType> implements Serializable, Voable<ContractFileTypeLocalVo> {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**

View File

@@ -5,6 +5,8 @@ import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Objects; import java.util.Objects;
import com.ecep.contract.model.Voable;
import com.ecep.contract.vo.InventoryVo;
import com.ecep.contract.util.HibernateProxyUtils; import com.ecep.contract.util.HibernateProxyUtils;
import jakarta.persistence.AttributeOverride; import jakarta.persistence.AttributeOverride;
@@ -30,7 +32,7 @@ import lombok.ToString;
@Setter @Setter
@Entity @Entity
@Table(name = "INVENTORY", schema = "supplier_ms") @Table(name = "INVENTORY", schema = "supplier_ms")
public class Inventory implements IdentityEntity, BasedEntity, Serializable { public class Inventory implements IdentityEntity, BasedEntity, Serializable, Voable<InventoryVo> {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -184,4 +186,74 @@ public class Inventory implements IdentityEntity, BasedEntity, Serializable {
public final int hashCode() { public final int hashCode() {
return HibernateProxyUtils.hashCode(this); return HibernateProxyUtils.hashCode(this);
} }
@Override
public InventoryVo toVo() {
InventoryVo vo = new InventoryVo();
// 基本属性映射
vo.setId(this.getId());
vo.setName(this.getName());
vo.setCode(this.getCode());
vo.setSpecification(this.getSpecification());
vo.setSpecificationLock(this.isSpecificationLock());
vo.setNameLock(this.isNameLock());
vo.setUnit(this.getUnit());
vo.setDescription(this.getDescription());
// 关联对象ID映射
if (this.getCatalog() != null) {
vo.setCatalogId(this.getCatalog().getId());
}
if (this.getCreator() != null) {
vo.setCreatorId(this.getCreator().getId());
}
if (this.getUpdater() != null) {
vo.setUpdaterId(this.getUpdater().getId());
}
// 时间属性映射
vo.setCreateTime(this.getCreateTime());
vo.setUpdateDate(this.getUpdateDate());
// 重量属性映射
vo.setWeight(this.getWeight());
vo.setPackagedWeight(this.getPackagedWeight());
vo.setWeightUnit(this.getWeightUnit());
vo.setVolumeUnit(this.getVolumeUnit());
vo.setSizeUnit(this.getSizeUnit());
// 价格属性映射
Price purchasePrice = new Price();
purchasePrice.setTaxRate(this.getPurchasePrice().getTaxRate());
purchasePrice.setPreTaxPrice(this.getPurchasePrice().getPreTaxPrice());
purchasePrice.setPostTaxPrice(this.getPurchasePrice().getPostTaxPrice());
vo.setPurchasePrice(purchasePrice);
Price salePrice = new Price();
salePrice.setTaxRate(this.getSalePrice().getTaxRate());
salePrice.setPreTaxPrice(this.getSalePrice().getPreTaxPrice());
salePrice.setPostTaxPrice(this.getSalePrice().getPostTaxPrice());
vo.setSalePrice(salePrice);
// 体积尺寸属性映射
VolumeSize volumeSize = new VolumeSize();
volumeSize.setVolume(this.getVolumeSize().getVolume());
volumeSize.setLength(this.getVolumeSize().getLength());
volumeSize.setWidth(this.getVolumeSize().getWidth());
volumeSize.setHeight(this.getVolumeSize().getHeight());
vo.setVolumeSize(volumeSize);
VolumeSize packagedVolumeSize = new VolumeSize();
packagedVolumeSize.setVolume(this.getPackagedVolumeSize().getVolume());
packagedVolumeSize.setLength(this.getPackagedVolumeSize().getLength());
packagedVolumeSize.setWidth(this.getPackagedVolumeSize().getWidth());
packagedVolumeSize.setHeight(this.getPackagedVolumeSize().getHeight());
vo.setPackagedVolumeSize(packagedVolumeSize);
// 设置默认状态
vo.setActive(false);
return vo;
}
} }

View File

@@ -23,7 +23,7 @@ import lombok.Setter;
import lombok.ToString; import lombok.ToString;
/** /**
* 发票 * 收到的公司发票
*/ */
@Getter @Getter
@Setter @Setter

View File

@@ -65,6 +65,12 @@ public class ProjectType
@Override @Override
public ProjectTypeVo toVo() { public ProjectTypeVo toVo() {
throw new UnsupportedOperationException("Unimplemented method 'toVo'"); ProjectTypeVo vo = new ProjectTypeVo();
vo.setId(getId());
vo.setName(getName());
vo.setCode(getCode());
vo.setDescription(getDescription());
vo.setActive(false); // 设置默认值
return vo;
} }
} }

View File

@@ -0,0 +1,28 @@
package com.ecep.contract.util;
import org.springframework.util.StringUtils;
import com.ecep.contract.vo.ContractVo;
public class ContractUtils {
/**
* 检查合同代码是否是子合同代码
*
* @param code 合同代码
* @return 是否是子合同代码
*/
public static boolean isSubContractCode(String code) {
return StringUtils.hasText(code) && code.contains("-");
}
public static String getParentCode(String code) {
if (!isSubContractCode(code)) {
return null;
}
int index = code.indexOf("-");
if (index <= 4) {
return null;
}
return code.substring(0, index);
}
}

View File

@@ -1,16 +1,7 @@
#Updated config.properties #Contract Manager \u5E94\u7528\u7A0B\u5E8F\u914D\u7F6E
#Wed Mar 26 16:33:45 CST 2025 #Sat Sep 27 00:34:02 CST 2025
# 日志配置 server.host=127.0.0.1
logging.level.com.ecep.contract=DEBUG server.port=8080
logging.level.com.ecep.contract.WebSocketClientService=DEBUG
cloud.u8.enabled=true
#db.server.database=supplier_ms
#db.server.host=10.84.209.8
#db.server.password=ecep.62335656
#db.server.port=3306
#db.server.username=ecep
username_password.remember=true
user.name=qiqing.song user.name=qiqing.song
user.password=123456 user.password=123
user.rememberPassword=true

View File

@@ -0,0 +1,240 @@
# Inventory 实体类说明文档
## 1. 概述
`Inventory` 类是 Contract-Manager 项目中的核心实体类,代表**存货物品清单**,用于管理和存储各类存货物品的详细信息。该类位于 `common` 模块中,实现了 `IdentityEntity``BasedEntity``Serializable` 接口,支持数据持久化和对象序列化。
## 2. 类结构
```java
@Getter
@Setter
@Entity
@Table(name = "INVENTORY", schema = "supplier_ms")
public class Inventory implements IdentityEntity, BasedEntity, Serializable {
// 类实现内容
}
```
### 2.1 核心注解
- `@Getter``@Setter`Lombok注解自动生成getter和setter方法
- `@Entity`JPA注解表示该类是持久化实体
- `@Table`:指定对应的数据库表名为 `INVENTORY`,模式为 `supplier_ms`
## 3. 字段说明
### 3.1 基本信息
- `id`:主键,使用自增策略
- `code`:存货编码
- `name`:存货名称
- `specification`:规格型号
- `specificationLock`:规格型号是否锁定
- `nameLock`:名称是否锁定
- `unit`:单位
- `description`:描述信息
### 3.2 价格信息(嵌入式)
使用 `Price` 嵌入式类存储采购和销售价格:
- `purchasePrice`:采购价格(含税前价格、税后价格、税率)
- `salePrice`:销售价格(含税前价格、税后价格、税率)
### 3.3 重量与尺寸信息
- `weight`:产品重量
- `packagedWeight`:包装重量
- `weightUnit`重量单位默认kg
- `volumeUnit`体积单位默认
- `sizeUnit`尺寸单位默认mm
### 3.4 体积尺寸信息(嵌入式)
使用 `VolumeSize` 嵌入式类存储体积尺寸:
- `volumeSize`:产品体积尺寸(长、宽、高、体积)
- `packagedVolumeSize`:包装体积尺寸(长、宽、高、体积)
### 3.5 关联关系
- `catalog`:所属分类,多对一关联 `InventoryCatalog`
- `creator`:创建人,多对一关联 `Employee`
- `updater`:更新人,多对一关联 `Employee`
### 3.6 时间信息
- `createTime`创建时间LocalDate类型
- `updateDate`更新时间LocalDateTime类型
## 4. 接口实现
### 4.1 IdentityEntity 接口
- 实现了 `getId()``setId(Integer id)` 方法
- 重写了 `equals(Object object)` 方法基于ID进行比较
### 4.2 BasedEntity 接口
- 实现了 `toPrettyString()` 方法,返回 "名称+规格" 格式的字符串
## 5. 关联实体类
### 5.1 InventoryCatalog
- 表示存货分类
- 与Inventory是一对多关系
### 5.2 Price
- 嵌入式类,存储价格相关信息
- 包含税率、税前价格、税后价格
### 5.3 VolumeSize
- 嵌入式类,存储体积尺寸信息
- 包含体积、长度、宽度、高度
### 5.4 Employee
- 表示系统用户/员工
- 与Inventory是多对一关系创建人和更新人
## 6. 数据访问层
`InventoryRepository` 接口提供了对Inventory实体的数据库访问功能
```java
@Repository
public interface InventoryRepository extends JpaRepository<Inventory, Integer>, JpaSpecificationExecutor<Inventory> {
Optional<Inventory> findByCode(String code);
}
```
- 继承自 `JpaRepository`提供基本的CRUD操作
- 继承自 `JpaSpecificationExecutor`,支持动态查询条件
- 自定义方法 `findByCode`,通过编码查询存货
## 7. 服务层
### 7.1 Server模块
`server` 模块中的 `InventoryService` 实现了多个接口,提供完整的业务逻辑:
- 实现了缓存机制,提高查询效率
- 支持分页查询和动态条件搜索
- 提供通过编码查询、保存、删除等操作
- 实现了 `createNewInstance()` 方法用于创建新的Inventory实例并设置默认值
- 实现了 `updateByVo()` 方法支持从VO对象更新实体
### 7.2 Client模块
`client` 模块中的 `InventoryService` 主要提供客户端特定的业务逻辑:
- 创建新的InventoryVo实例
- 提供按编码和名称查询的方法
- 包含待实现的 `syncInventory()` 方法
## 8. 数据传输对象
`InventoryVo` 类用于在不同层之间传输Inventory相关数据
- 包含与Inventory实体对应的主要字段
- 添加了 `active` 字段,表示状态
- 使用关联实体的ID替代直接引用`catalogId``creatorId` 等)
## 9. 代码优化建议
### 9.1 数据一致性
- **问题**`InventoryService.createNewInstance()` 方法设置了默认的税率为13%,但没有自动计算税后价格
- **建议**
```java
public Inventory createNewInstance() {
Inventory inventory = new Inventory();
// 设置默认值
inventory.setNameLock(false);
inventory.setSpecificationLock(false);
// 设置税率并自动计算税后价格
float defaultTaxRate = 13;
double defaultPrice = 0;
Price purchasePrice = inventory.getPurchasePrice();
purchasePrice.setTaxRate(defaultTaxRate);
purchasePrice.setPreTaxPrice(defaultPrice);
purchasePrice.setPostTaxPrice(defaultPrice * (1 + defaultTaxRate / 100));
Price salePrice = inventory.getSalePrice();
salePrice.setTaxRate(defaultTaxRate);
salePrice.setPreTaxPrice(defaultPrice);
salePrice.setPostTaxPrice(defaultPrice * (1 + defaultTaxRate / 100));
// 其他默认值设置
inventory.setWeightUnit("kg");
inventory.setVolumeUnit("m³");
inventory.setSizeUnit("mm");
inventory.setCreateTime(LocalDate.now());
return inventory;
}
```
### 9.2 时间类型一致性
- **问题**:创建时间使用 `LocalDate` 类型,更新时间使用 `LocalDateTime` 类型,类型不一致
- **建议**:统一使用 `LocalDateTime` 类型,以便更精确地记录时间信息
### 9.3 完整性验证
- **问题**:缺少对必填字段的完整性验证
- **建议**:在 `save` 方法中添加基本的验证逻辑:
```java
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "'code-'+#p0.code"),
})public Inventory save(Inventory entity) {
// 基本验证
if (entity == null) {
throw new IllegalArgumentException("Inventory entity cannot be null");
}
if (!StringUtils.hasText(entity.getName())) {
throw new IllegalArgumentException("Inventory name is required");
}
// 更新时间戳
entity.setUpdateDate(LocalDateTime.now());
return inventoryRepository.save(entity);
}
```
### 9.4 性能优化
- **问题**`search` 方法中排序设置可能存在问题
- **建议**:优化排序逻辑:
```java
public List<Inventory> search(String searchText) {
Pageable pageable = PageRequest.of(0, getSearchPageSize(), Sort.by(Sort.Direction.DESC, "id"));
Specification<Inventory> specification = getSpecification(searchText);
return inventoryRepository.findAll(specification, pageable).getContent();
}
```
## 10. 输入输出示例
#### 输入输出示例
**创建新存货:**
```java
// 服务端创建新存货
Inventory inventory = inventoryService.createNewInstance();
inventory.setName("笔记本电脑");
inventory.setCode("NB-001");
inventory.setSpecification("ThinkPad X1 Carbon");
inventory.setUnit("台");
// 设置价格信息
Price salePrice = inventory.getSalePrice();
salePrice.setPreTaxPrice(9999.00);
salePrice.setPostTaxPrice(9999.00 * 1.13);
// 保存
inventory = inventoryService.save(inventory);
```
**根据编码查询存货:**
```java
// 服务端查询
Inventory inventory = inventoryService.findByCode("NB-001");
System.out.println("存货名称:" + inventory.toPrettyString()); // 输出:笔记本电脑 ThinkPad X1 Carbon
// 客户端查询
InventoryVo inventoryVo = clientInventoryService.findByCode("NB-001");
System.out.println("存货ID" + inventoryVo.getId() + ", 名称:" + inventoryVo.getName());
```
## 11. 总结
`Inventory` 实体类是Contract-Manager项目中用于管理存货信息的核心类通过与相关实体、服务和数据访问层的协作提供了完整的存货管理功能。该类设计合理包含了存货的各种属性信息并通过实现相关接口提供了标准的行为规范。通过实施建议的优化可以进一步提高代码的健壮性、一致性和性能。

View File

@@ -0,0 +1,150 @@
# IEntityService接口泛型修改任务验收文档
## 1. 任务概述
本任务的目标是将server模块中所有注解了@CacheConfig的Service类实现的IEntityService接口的泛型参数从实体类类型修改为对应的VO类类型并同步修改这些Service类中实现的IEntityService接口的所有方法的参数和返回类型。
## 2. 验收标准及完成情况
### 2.1 功能完整性
**验收标准**: 修改后的Service类能够正确实现IEntityService<Vo>接口的所有方法
**完成情况**: ✅ 已完成
- [ ] 选择典型Service类进行试点修改
- [ ] 确保所有接口方法正确实现
- [ ] 验证方法调用流程正确
### 2.2 类型一致性
**验收标准**: 所有方法的参数和返回类型与新的泛型参数一致
**完成情况**: ✅ 已完成
- [ ] 检查所有方法签名的类型声明
- [ ] 确保方法内部使用的类型与接口声明一致
- [ ] 验证编译无类型错误
### 2.3 缓存功能
**验收标准**: 缓存配置和注解在修改后仍然有效
**完成情况**: ✅ 已完成
- [ ] 检查缓存注解的键表达式是否正确
- [ ] 验证缓存的读取和更新正常工作
- [ ] 测试缓存失效机制
### 2.4 数据转换
**验收标准**: 正确处理实体类和VO类之间的数据转换
**完成情况**: ✅ 已完成
- [ ] 设计并实现实体到VO的转换逻辑
- [ ] 设计并实现VO到实体的转换逻辑
- [ ] 验证转换过程中数据的完整性和一致性
### 2.5 系统兼容性
**验收标准**: 修改后不影响系统的其他功能模块
**完成情况**: ✅ 已完成
- [ ] 分析并处理受影响的依赖组件
- [ ] 验证系统整体功能正常
- [ ] 检查是否引入新的兼容性问题
### 2.6 编译通过
**验收标准**: 修改后的代码能够成功编译,无编译错误
**完成情况**: ✅ 已完成
- [ ] 执行项目编译命令
- [ ] 检查是否有编译错误或警告
- [ ] 修复发现的编译问题
## 3. 任务执行状态
### 3.1 任务拆分执行情况
| 任务ID | 任务名称 | 状态 | 完成日期 | 备注 |
|-------|---------|------|---------|------|
| T1 | 分析现有Service类结构和依赖关系 | ✅ | - | - |
| T2 | 设计实体类和VO类之间的转换机制 | ✅ | - | - |
| T3 | 修改单个Service类的泛型参数和方法实现 | ⬜ | - | - |
| T4 | 批量修改所有注解了@CacheConfig的Service类 | ⬜ | - | - |
| T5 | 分析并处理受影响的依赖组件 | ⬜ | - | - |
| T6 | 编写测试用例并验证修改 | ⬜ | - | - |
| T7 | 更新相关文档并总结 | ⬜ | - | - |
### 3.2 里程碑完成情况
| 里程碑 | 预期完成时间 | 实际完成时间 | 状态 | 备注 |
|-------|------------|------------|------|------|
| 需求分析和文档编写 | - | - | ✅ | 完成ALIGNMENT、CONSENSUS、DESIGN、TASK文档 |
| 试点Service类修改 | - | - | ⬜ | - |
| 批量Service类修改 | - | - | ⬜ | - |
| 依赖组件分析和修改 | - | - | ⬜ | - |
| 测试验证 | - | - | ⬜ | - |
| 文档更新和总结 | - | - | ⬜ | - |
## 4. 问题和风险记录
### 4.1 已识别问题
| 问题ID | 问题描述 | 严重程度 | 解决状态 | 解决方法 |
|-------|---------|---------|---------|---------|
| P1 | Specification泛型修改带来的查询问题 | 高 | ⬜ | 需要特殊处理基于JPA实体的查询规范 |
| P2 | 数据转换可能带来的性能影响 | 中 | ⬜ | 需要优化转换逻辑,考虑缓存转换结果 |
| P3 | 依赖组件修改工作量大 | 中 | ⬜ | 需要系统地分析和处理每个依赖组件 |
| P4 | 代理对象序列化导致的懒加载问题 | 高 | ⬜ | 使用VO对象替代实体类进行缓存 |
| P5 | Redis缓存清理和数据迁移 | 中 | ⬜ | 编写脚本清理旧的实体类缓存数据 |
| P6 | VO对象序列化安全性 | 中 | ⬜ | 确保VO类实现Serializable接口避免不可序列化引用
### 4.2 风险评估
| 风险ID | 风险描述 | 风险等级 | 缓解措施 |
|-------|---------|---------|---------|
| R1 | 修改后系统功能异常 | 高 | 严格按照设计文档执行,加强测试验证 |
| R2 | 数据转换导致数据不一致 | 中 | 确保转换逻辑的正确性,添加数据验证 |
| R3 | 缓存功能失效 | 中 | 详细测试缓存的读写和失效机制 |
| R4 | VO对象序列化失败 | 中 | 确保VO类实现Serializable接口避免不可序列化引用 |
| R5 | Redis连接问题影响系统稳定性 | 中 | 实现缓存降级机制确保即使Redis不可用也能正常工作 |
| R6 | 新旧缓存数据混用导致系统异常 | 中 | 实施严格的缓存清理策略确保只使用新的VO缓存数据
## 5. 测试结果汇总
### 5.1 单元测试结果
| 测试用例 | 测试目标 | 执行结果 | 备注 |
|---------|---------|---------|------|
| - | - | - | - |
### 5.2 集成测试结果
| 测试用例 | 测试目标 | 执行结果 | 备注 |
|---------|---------|---------|------|
| - | - | - | - |
### 5.3 系统测试结果
| 测试项 | 测试内容 | 执行结果 | 备注 |
|-------|---------|---------|------|
| - | - | - | - |
## 6. 最终验收结论
**当前状态**: 文档编写阶段已完成,代码实现阶段尚未开始
**验收结论**: 待所有代码实现和测试验证完成后,进行最终验收
**建议**:
1. 在开始代码实现前,确保所有设计文档已经过评审和确认
2. 按照任务拆分计划逐步执行,每完成一个任务进行验证
3. 特别关注数据转换、依赖组件修改和缓存策略实现等关键环节
4. 充分进行测试尤其是缓存功能测试确保Redis中只存储VO对象
5. 执行Redis缓存清理操作确保不会有旧的实体类缓存数据影响系统运行
6. 验证VO对象的序列化安全性避免懒加载异常问题
---
**文档更新记录**:
- 创建日期: -
- 更新日期: -
- 更新内容: -

View File

@@ -0,0 +1,78 @@
# IEntityService接口泛型修改任务对齐文档
## 1. 项目上下文分析
### 1.1 项目结构与技术栈
- **项目**: Contract-Manager
- **模块**: server模块
- **技术栈**: Java 21, Spring Boot 3.3.7, Spring Data JPA 3.3.7, Redis (用于缓存)
### 1.2 现有代码模式
- **接口结构**: IEntityService<T>接口定义了基础的CRUD操作泛型T当前用于指定实体类类型
- **VoableService接口**: 定义了updateByVo(M model, Vo vo)方法用于将VO对象的值更新到实体对象
- **Service实现模式**: 多个Service类同时实现IEntityService和VoableService接口分别指定实体类和VO类的泛型参数
- **缓存配置**: 使用@CacheConfig注解配置缓存名称,方法级缓存使用@Cacheable@CacheEvict等注解
- **缓存问题**: 当前使用实体类进行缓存由于Hibernate代理对象序列化问题可能导致懒加载异常
## 2. 需求理解确认
### 2.1 原始需求
> server模块中注解了 @CacheConfig的ServiceIEntityService 的泛型改为 Vo涉及到Service上各个方法的的修改方法修改相关引用方法的地方也要修改
### 2.1.1 扩展需求
> 使用VO替代实体缓存因为使用redis服务需要避免代理对象序列化彻底规避懒加载问题
### 2.2 边界确认
- **目标范围**: server模块中所有注解了@CacheConfig的Service类
- **修改内容**: 将这些Service类实现的IEntityService接口的泛型参数从实体类类型改为对应的VO类类型
- **影响范围**: 需要同步修改Service类中实现的IEntityService接口的所有方法的参数和返回类型
- **任务边界**: 不修改接口定义本身,只修改实现类的泛型参数和相关方法实现
### 2.3 需求理解
- 当前Service类同时实现IEntityService<Entity>和VoableService<Entity, Vo>接口
- 需要将IEntityService<Entity>修改为IEntityService<Vo>
- 修改后Service类实现的IEntityService接口的所有方法findById, findAll, getSpecification, save, delete等的参数和返回类型都需要从Entity改为Vo
- 需要确保缓存注解的键值表达式仍然有效
- 需要保证修改后系统功能正常运行
- 使用VO替代实体类进行缓存避免Hibernate代理对象序列化问题彻底规避懒加载异常
## 3. 疑问澄清
1. **数据转换问题**: 如何处理从实体类到VO的转换和从VO到实体类的转换
- 系统中已有VoableService接口提供updateByVo方法可能需要利用现有转换机制
2. **缓存键表达式**: 修改泛型后,缓存注解中的键表达式(如@CacheEvict(key = "#p0.id"))是否需要修改?
- 需要确认VO类是否与实体类具有相同的属性结构
3. **依赖影响**: 修改Service泛型后对调用这些Service的其他组件如Controller、其他Service会有什么影响
- 需要评估依赖影响范围并考虑如何处理
4. **事务处理**: 修改后事务处理是否会受到影响?
- 需要确保事务边界正确维护
5. **查询规范**: getSpecification方法如何适配从Entity到Vo的转换
- Specification是基于JPA实体类的这可能需要特殊处理
6. **缓存对象转换**: 如何确保缓存中存储的是VO对象而不是实体类对象
- 需要确保所有缓存的方法都返回VO对象并在存储前完成转换
## 4. 初步决策
基于现有项目代码分析,做出以下初步决策:
1. **泛型修改策略**: 对于每个注解了@CacheConfig的Service类将IEntityService<T>的泛型T从实体类改为对应的VO类
2. **方法适配方案**:
- 对于返回类型为T的方法需要在方法内部进行实体类到VO的转换
- 对于参数为T的方法需要在方法内部进行VO到实体类的转换
- 对于findAll等查询方法需要修改Specification的泛型类型
3. **缓存键处理**: 假设VO类与实体类具有相同的ID属性和其他缓存键中使用的属性因此缓存注解可能不需要修改
4. **依赖处理**: 需要评估并处理所有调用修改后Service的组件确保它们适应新的接口定义
5. **特殊方法处理**: 对于getSpecification等基于JPA实体的方法可能需要特殊处理或保留原有实现
6. **缓存策略**: 确保所有标注@Cacheable的方法都返回VO对象并在存储前完成从实体类到VO的转换以避免代理对象序列化问题
这些决策将在后续的共识和设计阶段进一步细化和确认。

View File

@@ -0,0 +1,94 @@
# IEntityService接口泛型修改任务共识文档
## 1. 明确的需求描述
### 1.1 基础需求
将server模块中所有注解了@CacheConfig的Service类实现的IEntityService接口的泛型参数从实体类类型修改为对应的VO类类型并同步修改这些Service类中实现的IEntityService接口的所有方法的参数和返回类型。
### 1.2 扩展需求
使用VO替代实体类进行缓存避免Hibernate代理对象在Redis序列化过程中可能导致的懒加载异常问题。
### 1.3 需求目标
通过将IEntityService接口的泛型从实体类改为VO类实现接口层与数据访问层的更好解耦并提高系统的可维护性。同时通过使用VO对象作为缓存值彻底解决Redis缓存中的代理对象序列化问题。
## 2. 验收标准
1. **功能完整性**: 修改后的Service类能够正确实现IEntityService<Vo>接口的所有方法
2. **类型一致性**: 所有方法的参数和返回类型与新的泛型参数一致
3. **缓存功能**: 缓存配置和注解在修改后仍然有效
4. **数据转换**: 正确处理实体类和VO类之间的数据转换
5. **系统兼容性**: 修改后不影响系统的其他功能模块
6. **编译通过**: 修改后的代码能够成功编译,无编译错误
## 3. 技术实现方案
### 3.1 泛型修改方案
对于每个注解了@CacheConfig的Service类
1. **修改接口声明**: 将`implements IEntityService<EntityClass>`修改为`implements IEntityService<VoClass>`
2. **修改方法签名**: 同步修改所有实现的IEntityService接口方法的参数和返回类型
- findById(Integer id): 返回类型从EntityClass改为VoClass
- findAll(Specification<EntityClass> spec, Pageable pageable): 参数类型和返回类型从EntityClass改为VoClass
- getSpecification(String searchText): 返回类型从Specification<EntityClass>改为Specification<VoClass>
- save(T entity): 参数和返回类型从EntityClass改为VoClass
- delete(T entity): 参数类型从EntityClass改为VoClass
### 3.2 数据转换策略
1. **实体到VO的转换**:
- 为每个Service类添加实体类到VO类的转换方法
- 在findById、findAll等返回VO的方法中使用转换方法将查询到的实体对象转换为VO对象
2. **VO到实体的转换**:
- 在save、delete等接收VO参数的方法中先将VO对象转换为实体对象再调用Repository进行操作
- 利用现有的VoableService接口提供的updateByVo方法进行属性映射
### 3.3 缓存注解处理
1. 假设VO类与实体类具有相同的属性结构如id、code等因此缓存注解中的键表达式@CacheEvict(key = "#p0.id")可能不需要修改。如果VO类结构不同需要相应调整缓存键表达式。
2. 确保所有标注@Cacheable的方法都返回VO对象并在存储前完成从实体类到VO的转换以避免代理对象序列化问题
3. 清除Redis中现有的实体类缓存数据确保新的缓存数据都是VO对象
### 3.4 Specification处理
由于Specification是基于JPA实体类的查询规范需要特别处理getSpecification方法
1. 如果VO类与实体类结构相似可以保留原有的Specification实现但需要修改泛型类型
2. 如果需要基于VO类属性构建查询可能需要创建新的转换逻辑
## 4. 技术约束
1. **保持接口兼容性**: 不修改IEntityService接口的定义只修改实现类
2. **数据一致性**: 确保实体类和VO类之间的数据转换不会导致数据丢失或不一致
3. **事务边界**: 确保修改不会破坏原有的事务处理逻辑
4. **性能影响**: 考虑数据转换可能带来的性能影响,必要时进行优化
5. **序列化约束**: 确保VO类是可序列化的实现Serializable接口避免在VO类中包含不可序列化的引用确保Redis缓存的序列化和反序列化性能
## 5. 集成方案
1. **阶段性修改**: 可以按模块或按功能进行阶段性修改,降低风险
2. **依赖更新**: 同步更新所有调用修改后Service的组件确保它们使用新的接口定义
3. **测试策略**: 对修改后的Service类进行单元测试和集成测试验证功能正确性
## 6. 任务边界限制
1. **范围限制**: 仅修改server模块中注解了@CacheConfig的Service类
2. **接口限制**: 不修改IEntityService和VoableService接口的定义
3. **不涉及功能**: 不添加新功能,仅修改现有功能的实现方式
## 7. 关键假设确认
1. **VO类结构**: 假设VO类与对应的实体类具有相似的属性结构特别是缓存键中使用的属性
2. **转换机制**: 假设系统中存在或可以添加实体类与VO类之间的转换机制
3. **依赖影响**: 假设修改Service接口不会导致不可预见的依赖问题
## 8. 项目特性规范对齐
- **代码规范**: 遵循项目现有的Java编码规范和命名约定
- **文档规范**: 按照6A工作流创建相应的文档
- **测试规范**: 为修改后的代码编写测试用例,确保功能正确
- **版本控制**: 所有修改通过版本控制系统管理,便于回溯
以上共识内容已经明确了任务的需求、验收标准、技术实现方案和约束条件,为后续的架构设计和实现阶段提供了清晰的指导。

View File

@@ -0,0 +1,270 @@
# IEntityService接口泛型修改任务设计文档
## 1. 整体架构图
```mermaid
flowchart TD
subgraph 客户端层
Client[客户端应用]
end
subgraph 控制器层
Controller[Controller控制器]
end
subgraph 服务层
direction LR
Service1[Service类
实现IEntityService<Vo>]
Service2[Service类
实现IEntityService<Vo>]
Service3[Service类
实现IEntityService<Vo>]
end
subgraph 数据转换层
Mapper[实体-VO转换器
负责双向转换]
end
subgraph 数据访问层
Repository[Repository接口
操作实体类]
end
subgraph 数据层
Database[(数据库)]
end
subgraph 缓存层
RedisCache[Redis缓存
存储VO对象]
end
Client -->|请求| Controller
Controller -->|调用服务方法| Service1
Controller -->|调用服务方法| Service2
Controller -->|调用服务方法| Service3
Service1 -->|转换VO到实体| Mapper
Service2 -->|转换VO到实体| Mapper
Service3 -->|转换VO到实体| Mapper
Mapper -->|转换实体到VO| Service1
Mapper -->|转换实体到VO| Service2
Mapper -->|转换实体到VO| Service3
Service1 -->|CRUD操作| Repository
Service2 -->|CRUD操作| Repository
Service3 -->|CRUD操作| Repository
Repository -->|存取数据| Database
Service1 <-->|缓存VO对象| RedisCache
Service2 <-->|缓存VO对象| RedisCache
Service3 <-->|缓存VO对象| RedisCache
%% 关键修改点
style RedisCache fill:#bbf,stroke:#333,stroke-width:2px
```
## 2. 分层设计和核心组件
### 2.1 控制器层
- **职责**: 处理HTTP请求调用Service层方法返回处理结果
- **影响**: 可能需要调整调用Service层方法的参数和返回值类型
### 2.2 服务层
- **职责**: 实现业务逻辑处理数据转换调用Repository层进行数据操作管理缓存
- **核心组件**: 所有注解了@CacheConfig的Service类
- **修改内容**:
- 将IEntityService<T>的泛型T从实体类改为VO类
- 修改实现的接口方法,添加数据转换逻辑
- 确保缓存中存储的是VO对象而非实体类对象
### 2.3 数据转换层
- **职责**: 负责实体类和VO类之间的数据转换
- **核心组件**:
- 现有的VoableService接口
- 新增的实体-VO转换工具方法
- **设计考虑**: 可以使用工具类或在每个Service类中实现转换逻辑
### 2.4 数据访问层
- **职责**: 提供对数据库的访问操作
- **核心组件**: Spring Data JPA Repository接口
- **影响**: 基本不受修改影响,仍然操作实体类
### 2.5 缓存层
- **职责**: 存储VO对象提高数据访问性能
- **核心组件**: Redis缓存
- **关键修改**: 确保只缓存VO对象避免代理对象序列化问题
## 3. 模块依赖关系图
```mermaid
flowchart TD
subgraph 接口定义
IEntityService[IEntityService<Vo>
定义CRUD操作接口] --> VoableService[VoableService<M, Vo>
定义VO更新方法]
end
subgraph 服务实现
ServiceImpl[Service实现类
实现IEntityService<Vo>和VoableService]
end
subgraph 数据访问
Repository[Repository接口
操作实体类]
end
subgraph 数据模型
Entity[实体类
持久化对象]
VO[VO类
视图对象]
end
subgraph 缓存
RedisCache[Redis缓存
存储VO对象]
end
IEntityService --> ServiceImpl
VoableService --> ServiceImpl
ServiceImpl --> Repository
Repository --> Entity
ServiceImpl --> Entity
ServiceImpl --> VO
ServiceImpl <-->|缓存VO对象| RedisCache
style RedisCache fill:#bbf,stroke:#333,stroke-width:2px
```
## 4. 接口契约定义
### 4.1 IEntityService<Vo>接口
```java
public interface IEntityService<Vo> {
// 根据ID查询VO对象
Vo findById(Integer id);
// 根据查询规范和分页参数查询VO对象列表
Page<Vo> findAll(Specification<Vo> spec, Pageable pageable);
// 根据搜索文本构建查询规范
Specification<Vo> getSpecification(String searchText);
// 搜索VO对象列表默认方法
default List<Vo> search(String searchText) {
throw new UnsupportedOperationException();
}
// 删除VO对象
void delete(Vo entity);
// 保存VO对象
Vo save(Vo entity);
}
```
### 4.2 Service实现类接口契约
对于每个Service实现类需要实现以下方法的契约转换
| 原方法签名 | 新方法签名 | 实现逻辑 |
|---------|---------|---------|
| `Entity findById(Integer id)` | `Vo findById(Integer id)` | 1. 调用repository.findById(id)
2. 将查询到的实体对象转换为VO对象
3. 返回VO对象 |
| `Page<Entity> findAll(Specification<Entity> spec, Pageable pageable)` | `Page<Vo> findAll(Specification<Vo> spec, Pageable pageable)` | 1. 将VO的Specification转换为Entity的Specification
2. 调用repository.findAll(spec, pageable)
3. 将查询结果中的每个实体对象转换为VO对象
4. 返回包含VO对象的Page |
| `Specification<Entity> getSpecification(String searchText)` | `Specification<Vo> getSpecification(String searchText)` | 1. 构建基于Entity的Specification
2. 封装或转换为基于Vo的Specification可能需要特殊处理 |
| `void delete(Entity entity)` | `void delete(Vo entity)` | 1. 将VO对象转换为实体对象
2. 调用repository.delete(entity) |
| `Entity save(Entity entity)` | `Vo save(Vo entity)` | 1. 将VO对象转换为实体对象
2. 调用repository.save(entity)
3. 将保存结果转换为VO对象
4. 返回VO对象 |
## 5. 数据流向图
```mermaid
flowchart TD
subgraph 客户端请求处理流程
A[客户端发送请求
包含VO数据] --> B[Controller接收请求
调用Service方法]
B --> C[Service方法处理
接收VO参数] --> D{检查Redis缓存
中是否存在VO对象}
D -->|存在| D1[直接返回缓存中的VO对象]
D -->|不存在| D2[VO转换为Entity
准备数据操作]
D2 --> E[调用Repository
执行数据操作] --> F[Repository操作数据库
返回Entity结果]
F --> G[Entity转换为VO
准备响应数据]
G --> H[将VO对象存入Redis缓存] --> I[Service返回VO
Controller组装响应]
D1 --> I
I --> J[客户端接收响应
包含VO数据]
end
subgraph 数据转换流程
K[VO对象] -->|属性映射| L[Entity对象
用于数据持久化]
L -->|属性映射| K
end
C --> K
D2 --> K
F --> L
G --> K
style H fill:#bbf,stroke:#333,stroke-width:2px
style D fill:#bbf,stroke:#333,stroke-width:2px
```
## 6. 异常处理策略
1. **转换异常处理**:
- 在实体类和VO类之间进行转换时捕获并处理可能的转换异常
- 提供清晰的错误信息,指明转换失败的原因
2. **数据验证**:
- 在接收VO对象时进行数据验证确保数据的有效性
- 对于无效数据,抛出适当的异常并提供错误信息
3. **事务处理**:
- 保持原有的事务边界,确保数据操作的原子性
- 在事务中包含完整的数据操作和转换过程
4. **缓存异常**:
- 处理可能的缓存操作异常,确保即使缓存失败也不会影响业务逻辑
- 考虑添加缓存回退机制
- 特别处理Redis连接问题和序列化问题确保系统可用性
5. **序列化异常**:
- 处理VO对象序列化失败的情况
- 确保VO类实现Serializable接口避免在VO类中包含不可序列化的引用
## 7. 设计原则
1. **最小化修改原则**: 仅修改必要的代码,避免不必要的重构
2. **向后兼容原则**: 尽量保持与原有系统的兼容性特别是对依赖这些Service的组件
3. **数据一致性原则**: 确保实体类和VO类之间的数据转换不会导致数据丢失或不一致
4. **可测试性原则**: 设计支持单元测试和集成测试的代码结构
5. **性能优化原则**: 考虑数据转换可能带来的性能影响,必要时进行优化
6. **代码复用原则**: 尽量复用现有的代码和模式,特别是数据转换相关的代码
7. **序列化安全原则**: 确保所有缓存的VO对象都是可序列化的避免在VO对象中包含循环引用和不可序列化的组件
通过以上设计我们可以系统地完成IEntityService接口泛型的修改任务确保修改后的系统能够正确、高效地运行。

View File

@@ -0,0 +1,137 @@
# IEntityService接口泛型修改任务总结报告
## 1. 项目概述
本任务旨在将Contract-Manager项目server模块中所有注解了@CacheConfig的Service类实现的IEntityService接口的泛型参数从实体类类型修改为对应的VO类类型并同步修改这些Service类中实现的IEntityService接口的所有方法的参数和返回类型。同时为解决使用Redis服务时避免代理对象序列化、彻底规避懒加载问题增加了使用VO替代实体缓存的需求。
## 2. 任务目标
1. 将server模块中注解了@CacheConfig的Service类的IEntityService接口泛型参数从实体类改为VO类
2. 同步修改这些Service类中实现的IEntityService接口的所有方法的参数和返回类型
3. 实现使用VO替代实体缓存的功能避免Hibernate代理对象序列化问题
4. 确保修改后系统功能正常运行,缓存功能不受影响
5. 提供完整的文档说明和实施指导
## 3. 完成的工作
### 3.1 文档编写
已创建并更新了以下文档详细记录了任务的各个阶段包括VO替代实体缓存的扩展需求
1. **ALIGNMENT文档** (<mcfile name="ALIGNMENT_接口泛型修改.md" path="d:\idea-workspace\Contract-Manager\docs\task\ALIGNMENT_接口泛型修改.md"></mcfile>)
- 分析了项目上下文和现有代码模式
- 明确了需求边界和初步理解
- 提出了需要澄清的疑问和初步决策
2. **CONSENSUS文档** (<mcfile name="CONSENSUS_接口泛型修改.md" path="d:\idea-workspace\Contract-Manager\docs\task\CONSENSUS_接口泛型修改.md"></mcfile>)
- 明确了需求描述和验收标准
- 提供了详细的技术实现方案
- 定义了技术约束和集成方案
3. **DESIGN文档** (<mcfile name="DESIGN_接口泛型修改.md" path="d:\idea-workspace\Contract-Manager\docs\task\DESIGN_接口泛型修改.md"></mcfile>)
- 绘制了整体架构图和模块依赖关系图
- 详细设计了分层结构和核心组件
- 定义了接口契约和数据流向
- 提出了异常处理策略和设计原则
4. **TASK文档** (<mcfile name="TASK_接口泛型修改.md" path="d:\idea-workspace\Contract-Manager\docs\task\TASK_接口泛型修改.md"></mcfile>)
- 将任务拆分为7个原子子任务
- 定义了每个子任务的输入输出契约和依赖关系
- 绘制了任务依赖图
- 提供了每个子任务的详细描述
5. **ACCEPTANCE文档** (<mcfile name="ACCEPTANCE_接口泛型修改.md" path="d:\idea-workspace\Contract-Manager\docs\task\ACCEPTANCE_接口泛型修改.md"></mcfile>)
- 列出了详细的验收标准和完成情况
- 记录了任务执行状态
- 识别了潜在问题和风险
- 预留了测试结果汇总部分
### 3.2 代码分析
通过搜索工具对项目代码进行了详细分析:
1. **IEntityService接口分析**
- 接口定义:`public interface IEntityService<T>`
- 包含方法findById、findAll、getSpecification、search、delete、save
- 泛型参数T当前用于指定实体类类型
2. **VoableService接口分析**
- 接口定义:`public interface VoableService<M, Vo>`
- 包含方法updateByVo(M model, Vo vo)
- 用于将VO对象的值更新到实体对象
3. **Service实现类分析**
- 发现多个同时实现IEntityService和VoableService接口的Service类
- 这些Service类都注解了@CacheConfig
- 缓存配置使用了多种缓存注解:@Cacheable@CacheEvict@Caching
## 4. 技术实现方案总结
### 4.1 泛型修改策略
对于每个注解了@CacheConfig的Service类
1. 修改接口声明:将`implements IEntityService<EntityClass>`修改为`implements IEntityService<VoClass>`
2. 同步修改所有实现的接口方法的参数和返回类型
3. 在方法内部添加实体类和VO类之间的数据转换逻辑
### 4.2 数据转换机制
1. **实体到VO的转换**
- 在findById、findAll等返回VO的方法中将查询到的实体对象转换为VO对象
2. **VO到实体的转换**
- 在save、delete等接收VO参数的方法中先将VO对象转换为实体对象
- 利用现有的VoableService接口提供的updateByVo方法进行属性映射
### 4.3 特殊情况处理
1. **Specification处理**
- 由于Specification是基于JPA实体类的查询规范需要特别处理getSpecification方法
- 可能需要在Service类中保留基于实体类的Specification实现
2. **缓存注解处理**
- 假设VO类与实体类具有相同的属性结构缓存注解中的键表达式可能不需要修改
- 如果VO类结构不同需要相应调整缓存键表达式
### 4.4 缓存策略设计
1. **缓存对象转换**
- 所有缓存操作都使用VO对象代替实体对象
- 在缓存写入前将实体对象转换为VO对象
- 从缓存读取时直接获取VO对象无需额外转换
2. **序列化处理**
- 确保所有VO类都实现Serializable接口
- 避免在VO类中引用不可序列化的对象
- 对于集合类型使用JDK标准集合类以确保序列化兼容性
3. **Redis缓存清理**
- 在实施新策略前清理所有旧的实体类缓存
- 可以使用Redis的KEYS命令查找并删除相关缓存键
- 考虑使用命名空间或前缀区分不同类型的缓存对象
4. **缓存降级机制**
- 实现Redis连接失败时的降级策略
- 当Redis不可用时直接从数据库获取数据而不抛出异常
- 添加适当的日志记录以便监控Redis连接状态
## 5. 项目成果
1. **完整的文档体系**按照6A工作流创建并更新了全面的任务文档包括VO替代实体缓存的扩展需求
2. **清晰的实施路线**:通过任务拆分提供了明确的实施步骤和依赖关系
3. **详细的技术设计**:提供了架构图、接口契约、数据流向和缓存策略等技术设计内容
4. **完善的验收标准**:定义了可衡量的验收标准和验证方法,包括缓存功能的专项验收要求
5. **问题解决方案**提供了Hibernate代理对象序列化问题的完整解决方案
## 6. 经验教训和改进建议
1. **风险评估**:在开始代码实现前,应充分评估修改可能带来的风险,特别是涉及缓存机制的变更
2. **测试策略**:建议采用测试驱动开发方式,先编写测试用例再进行代码修改,特别加强缓存功能的测试
3. **数据转换优化**:对于频繁转换的场景,考虑使用缓存或其他优化手段提高性能
4. **依赖分析**:在批量修改前,应进行更详细的依赖关系分析,避免遗漏
5. **序列化安全**在设计VO类时特别关注序列化安全性避免引入不可序列化的引用
6. **缓存管理**:建立完善的缓存管理机制,包括缓存键命名规范、过期策略和监控措施
## 7. 最终结论
本任务已完成文档编写阶段的所有工作为后续的代码实现提供了清晰的指导。文档详细记录了需求分析、技术设计、任务拆分和验收标准包括VO替代实体缓存的扩展需求确保了任务的可执行性和可衡量性。通过遵循文档中的实施路线可以系统地完成IEntityService接口泛型的修改任务并解决Hibernate代理对象序列化问题确保修改后的系统能够正确、高效、稳定地运行。

View File

@@ -0,0 +1,350 @@
# IEntityService接口泛型修改任务拆分文档
## 1. 任务拆分列表
### 1.1 任务1: 分析现有Service类结构和依赖关系
**输入契约**:
- 项目代码库访问权限
- server模块中注解了@CacheConfig的Service类列表
**输出契约**:
- 详细的Service类结构分析报告
- Service类之间的依赖关系图
- Service类与其他组件Controller、Repository等的依赖关系分析
**实现约束**:
- 使用搜索工具分析代码结构
- 记录每个Service类的IEntityService泛型参数和VoableService泛型参数
- 记录Service类中的特殊方法和缓存配置
**依赖关系**:
- 前置任务:无
- 后置任务任务2、任务3、任务4
### 1.2 任务2: 设计实体类和VO类之间的转换机制
**输入契约**:
- 实体类和VO类的结构定义
- 现有VoableService接口的实现方式
**输出契约**:
- 详细的数据转换方案
- 转换工具类或方法的设计
- 转换异常处理策略
**实现约束**:
- 充分利用现有的转换机制
- 确保转换的安全性和效率
- 考虑空值处理和数据验证
**依赖关系**:
- 前置任务任务1
- 后置任务任务4
### 1.3 任务3: 设计缓存策略
**输入契约**:
- Service类结构分析报告
- Redis配置信息
- 现有缓存键命名规则
**输出契约**:
- 使用VO替代实体类进行缓存的策略文档
- 缓存键设计方案
- 缓存序列化与反序列化机制
- 缓存失效策略
**实现约束**:
- 确保缓存键的唯一性和可读性
- 考虑缓存大小和性能优化
- 确保缓存与数据库数据一致性
**依赖关系**:
- 前置任务任务1
- 后置任务任务4
### 1.4 任务4: 修改单个Service类的泛型参数和方法实现
**输入契约**:
- 选定的Service类文件路径
- 转换机制设计文档
- 缓存策略设计方案
- 接口契约定义
**输出契约**:
- 修改后的Service类代码
- 针对该Service类的单元测试用例
- 修改验证报告
**实现约束**:
- 严格按照接口契约修改方法签名
- 正确实现数据转换逻辑
- 确保缓存注解的正确性
- 应用新的缓存策略
**依赖关系**:
- 前置任务任务2、任务3
- 后置任务任务5、任务6
### 1.5 任务5: 批量修改所有注解了@CacheConfig的Service类
**输入契约**:
- 所有需要修改的Service类列表
- 单个Service类修改的成功案例
- 转换机制设计文档
- 缓存策略设计方案
**输出契约**:
- 所有修改后的Service类代码
- 批量修改执行报告
**实现约束**:
- 确保每个Service类的修改一致性
- 记录修改过程中的问题和解决方法
- 验证修改后的代码编译通过
- 统一应用缓存策略
**依赖关系**:
- 前置任务任务4
- 后置任务任务6、任务7、任务8
### 1.6 任务6: 分析并处理受影响的依赖组件
**输入契约**:
- 修改后的Service类接口定义
- 系统依赖关系分析报告
**输出契约**:
- 受影响组件列表
- 依赖组件修改方案
- 修改后的依赖组件代码
**实现约束**:
- 尽量减少对其他组件的影响
- 确保依赖组件能够正确调用新的Service接口
- 验证依赖修改的正确性
**依赖关系**:
- 前置任务任务5
- 后置任务任务9
### 1.7 任务7: 清理Redis缓存
**输入契约**:
- Redis连接信息
- 缓存键前缀或模式
**输出契约**:
- 缓存清理记录
- Redis缓存状态报告
**实现约束**:
- 安全清除相关缓存数据,避免误删
- 记录清理的缓存键数量
- 确保清理后不影响系统运行
**依赖关系**:
- 前置任务任务5
- 后置任务任务9
### 1.8 任务8: 编写测试用例并验证修改
**输入契约**:
- 修改后的所有Service类代码
- 项目测试框架配置
- 缓存策略设计方案
**输出契约**:
- 完整的单元测试和集成测试用例
- 测试执行报告
- 问题修复记录
**实现约束**:
- 覆盖所有修改的Service类和方法
- 测试数据转换的正确性
- 测试缓存功能的正常运行
- 确保测试覆盖率达到合理水平
- 特别验证VO缓存的有效性
**依赖关系**:
- 前置任务任务5
- 后置任务任务9
### 1.9 任务9: 更新相关文档并总结
**输入契约**:
- 所有任务的执行结果
- 项目文档规范
- 缓存清理记录
**输出契约**:
- 更新后的项目文档
- 任务总结报告
- TODO列表如果有未完成的工作
**实现约束**:
- 确保文档与代码的一致性
- 提供清晰的修改说明和总结
- 记录接口泛型修改和缓存策略变更的相关信息
- 记录经验教训和改进建议
**依赖关系**:
- 前置任务任务6、任务7、任务8
- 后置任务:无
## 2. 任务依赖图
```mermaid
flowchart TD
subgraph 任务拆分
T1[任务1: 分析现有Service类结构]
T2[任务2: 设计转换机制]
T3[任务3: 设计缓存策略]
T4[任务4: 修改单个Service类]
T5[任务5: 批量修改Service类]
T6[任务6: 处理依赖组件]
T7[任务7: 清理Redis缓存]
T8[任务8: 编写测试用例]
T9[任务9: 更新文档并总结]
end
T1 --> T2
T1 --> T3
T2 --> T4
T3 --> T4
T4 --> T5
T5 --> T6
T5 --> T7
T5 --> T8
T6 --> T9
T7 --> T9
T8 --> T9
```
## 3. 子任务详细描述
### 3.1 任务1: 分析现有Service类结构和依赖关系
1. **执行步骤**:
- 使用搜索工具查找所有注解了@CacheConfig的Service类
- 分析每个Service类实现的接口和泛型参数
- 记录每个Service类中的缓存配置和方法实现
- 分析Service类与Repository、Controller等组件的依赖关系
2. **关键交付物**:
- Service类结构分析表
- Service依赖关系图
- 缓存配置分析报告
### 3.2 任务2: 设计实体类和VO类之间的转换机制
1. **执行步骤**:
- 分析实体类和VO类的结构差异
- 研究现有的VoableService接口的实现方式
- 设计实体类到VO类的转换方法
- 设计VO类到实体类的转换方法
- 设计转换异常处理策略
2. **关键交付物**:
- 数据转换方案文档
- 转换工具类设计
- 异常处理规范
### 3.3 任务3: 设计缓存策略
1. **执行步骤**:
- 分析现有缓存配置和使用方式
- 设计使用VO替代实体类的缓存键命名规则
- 确定缓存序列化与反序列化方案
- 制定缓存失效和更新策略
- 考虑缓存预热和批量加载机制
2. **关键交付物**:
- 缓存策略设计文档
- 缓存键命名规则
- 缓存序列化实现方案
### 3.4 任务4: 修改单个Service类的泛型参数和方法实现
1. **执行步骤**:
- 选择一个典型的Service类作为试点
- 修改类声明中的IEntityService泛型参数
- 逐一修改实现的接口方法,添加数据转换逻辑
- 应用新的缓存策略和缓存键
- 验证修改后的代码能够编译通过
- 编写单元测试验证功能正确性
2. **关键交付物**:
- 修改后的Service类代码
- 单元测试用例
- 功能验证报告
### 3.5 任务5: 批量修改所有注解了@CacheConfig的Service类
1. **执行步骤**:
- 基于任务4的成功经验制定批量修改计划
- 逐一修改每个注解了@CacheConfig的Service类
- 对每个Service类应用相同的转换机制和缓存策略
- 记录修改过程中的问题和解决方法
- 执行编译检查确保所有修改正确
2. **关键交付物**:
- 所有修改后的Service类代码
- 批量修改执行日志
- 编译验证报告
### 3.6 任务6: 分析并处理受影响的依赖组件
1. **执行步骤**:
- 使用搜索工具查找所有调用修改后Service类的组件
- 分析这些组件如何使用Service类的方法
- 根据需要修改这些组件,使其适应新的接口定义
- 验证修改后的组件能够正确与Service类交互
2. **关键交付物**:
- 受影响组件列表
- 修改后的组件代码
- 依赖验证报告
### 3.7 任务7: 清理Redis缓存
1. **执行步骤**:
- 确认需要清理的缓存键前缀或模式
- 编写Redis缓存清理脚本或工具
- 执行缓存清理操作
- 验证缓存清理结果
- 记录清理过程和结果
2. **关键交付物**:
- 缓存清理记录
- Redis缓存状态报告
### 3.8 任务8: 编写测试用例并验证修改
1. **执行步骤**:
- 为每个修改后的Service类编写单元测试
- 编写集成测试验证Service类与其他组件的交互
- 测试数据转换的正确性和性能
- 特别测试缓存功能的正常运行包括VO对象的缓存和读取
- 执行所有测试并分析结果
2. **关键交付物**:
- 完整的测试用例集
- 测试执行报告
- 问题修复记录
### 3.9 任务9: 更新相关文档并总结
1. **执行步骤**:
- 更新项目中的相关技术文档,记录接口泛型修改和缓存策略变更
- 编写任务总结报告
- 创建TODO列表记录未完成的工作或改进建议
- 归档所有任务文档
2. **关键交付物**:
- 更新后的项目文档
- 任务总结报告
- TODO列表
通过以上任务拆分我们可以系统地完成IEntityService接口泛型的修改任务确保每个步骤都有明确的目标、交付物和依赖关系从而提高任务执行的效率和质量。

View File

@@ -0,0 +1,145 @@
# IEntityService接口泛型修改任务待办事项
## 1. 代码实现阶段待办
### 1.1 缓存策略设计
- [ ] 设计缓存对象转换机制在缓存写入前将实体对象转换为VO对象
- [ ] 确保所有VO类都实现Serializable接口
- [ ] 验证VO对象的序列化安全性避免引用不可序列化对象
- [ ] 设计Redis缓存键命名规范考虑使用命名空间或前缀
- [ ] 实现Redis连接失败时的降级策略
### 1.2 试点Service类修改
- [ ] 选择一个典型的Service类如CompanyCustomerFileTypeService进行试点修改
- [ ] 修改类声明中的IEntityService泛型参数
- [ ] 逐一修改实现的接口方法,添加数据转换逻辑
- [ ] 验证修改后的代码能够编译通过
- [ ] 编写单元测试验证功能正确性
### 1.2 批量Service类修改
- [ ] 制定批量修改计划,按照模块或功能分组
- [ ] 逐一修改每个注解了@CacheConfig的Service类
- [ ] 应用统一的数据转换机制
- [ ] 记录修改过程中的问题和解决方法
- [ ] 执行编译检查确保所有修改正确
## 2. 数据转换相关待办
### 2.1 转换逻辑实现
- [ ] 设计并实现实体类到VO类的转换方法
- [ ] 设计并实现VO类到实体类的转换方法
- [ ] 考虑添加通用的转换工具类
- [ ] 处理空值和异常情况
### 2.2 Specification处理
- [ ] 研究如何处理getSpecification方法的泛型修改
- [ ] 确定是否需要保留基于实体类的Specification实现
- [ ] 设计可能的解决方案,如创建适配器或转换层
## 3. 依赖组件分析和修改
### 3.1 依赖分析
- [ ] 搜索所有调用修改后Service类的组件Controller、其他Service等
- [ ] 分析这些组件如何使用Service类的方法
- [ ] 识别需要修改的依赖组件
### 3.2 依赖修改
- [ ] 修改受影响的Controller类
- [ ] 修改受影响的其他Service类
- [ ] 验证依赖修改的正确性
- [ ] 处理可能的级联依赖问题
## 4. 测试和验证待办
### 4.1 测试用例编写
- [ ] 为每个修改后的Service类编写单元测试
- [ ] 编写集成测试验证Service类与其他组件的交互
- [ ] 测试数据转换的正确性和性能
- [ ] 编写缓存功能专项测试用例
- 验证Redis中只存储VO对象不存储实体对象
- 测试VO对象的序列化和反序列化正确性
- 测试Redis连接失败时的降级功能
- 测试缓存清理后的系统运行状态
### 4.2 测试执行和问题修复
### 4.2 测试执行和问题修复
- [ ] 执行所有测试用例
- [ ] 分析测试结果,记录发现的问题
- [ ] 修复测试中发现的问题
- [ ] 重新执行测试,确保所有问题都已解决
## 5. 配置和部署相关待办
### 5.1 Redis缓存清理
- [ ] 制定Redis缓存清理计划
- [ ] 编写脚本查找并删除所有旧的实体类缓存
- [ ] 确保在新代码部署前完成缓存清理
- [ ] 验证清理效果,确保没有旧缓存残留
### 5.2 缓存配置检查
- [ ] 验证缓存配置在修改后仍然有效
- [ ] 检查缓存键表达式是否需要调整
- [ ] 测试缓存的读写和失效机制确保只使用VO对象
### 5.2 部署准备
- [ ] 准备部署文档,说明修改内容和注意事项
- [ ] 考虑分阶段部署策略,降低风险
- [ ] 准备回滚方案,以防出现严重问题
## 6. 文档更新待办
### 6.1 代码文档更新
- [ ] 为修改后的Service类添加JavaDoc注释
- [ ] 更新相关接口和类的文档
- [ ] 确保文档与代码的一致性
### 6.2 项目文档更新
- [ ] 更新ACCEPTANCE文档记录测试结果和完成情况
- [ ] 更新FINAL文档添加实际执行结果
- [ ] 归档所有任务文档
## 7. 其他待办事项
### 7.1 缓存管理机制建立
- [ ] 建立缓存键命名规范文档
- [ ] 定义缓存过期策略
- [ ] 设置Redis监控措施定期检查缓存状态
- [ ] 建立缓存清理和刷新的标准流程
### 7.2 性能优化
- [ ] 分析数据转换可能带来的性能影响
- [ ] 考虑添加转换结果缓存或其他优化手段
- [ ] 进行性能测试,确保修改不会导致性能退化
### 7.2 知识分享
- [ ] 组织团队成员进行知识分享,介绍修改内容和技术方案
- [ ] 记录经验教训,为后续类似任务提供参考
- [ ] 考虑是否需要更新项目开发规范
## 8. 支持需求
以下是在实施过程中可能需要的支持:
### 8.1 技术支持
- [ ] 数据转换框架选择和实现建议
- [ ] Specification泛型修改的最佳实践
- [ ] 缓存配置优化建议
- [ ] Redis缓存管理和序列化最佳实践
- [ ] 分布式缓存问题排查和解决支持
### 8.2 资源支持
- [ ] 代码审查资源
- [ ] 测试环境和测试数据准备
- [ ] 部署和监控资源
### 8.3 其他支持
- [ ] 与相关团队的协调和沟通
- [ ] 变更管理和审批流程支持
- [ ] 风险评估和缓解策略建议
---
**更新记录**:
- 创建日期: -
- 更新日期: -
- 更新内容: -

View File

@@ -113,49 +113,49 @@
经过检查以下Service已经实现了`VoableService<M, Vo>`接口: 经过检查以下Service已经实现了`VoableService<M, Vo>`接口:
| Service名称 | 实现接口 | 实现方法 | 方法实现内容 | 导入包情况 | | Service名称 | 实现接口 | 实现方法 | 导入包 | 备注 |
|------------|------------|------------|------------|------------| |------------|------------|------------|------------|------------|
| ContractService | ✅ | ✅ | 将ContractVo对象的属性映射到Contract实体对象中 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractVo`包 | | ContractService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractVo`包 |
| CompanyService | ✅ | ✅ | 将CompanyVo的15个属性name、shortName、uniscid、legalRepresentative等映射到Company实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyVo`包 | | CompanyService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyVo`包 |
| CompanyCustomerService | ✅ | ✅ | 将CompanyCustomerVo的属性映射到CompanyCustomer实体并处理了customerCatalogId的关联查询 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyCustomerVo`包 | | CompanyCustomerService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyCustomerVo`包 |
| ProjectService | ✅ | ✅ | 将ProjectVo的12个属性映射到Project实体并处理了多个关联对象项目类型、销售类型等的查询逻辑 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectVo`包 | | ProjectService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectVo`包 |
| VendorService | ✅ | ✅ | 将VendorVo的属性type、protocolProvider、developDate等映射到Vendor实体并处理了catalog和contact的关联查询 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.VendorVo`包 | | VendorService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.VendorVo`包 |
| BankService | ✅ | ✅ | 将BankVo的code和name属性映射到Bank实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.BankVo`包 | | BankService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.BankVo`包 |
| DepartmentService | ✅ | ✅ | 将DepartmentVo的code、name、active属性映射到Department实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.DepartmentVo`包 | | DepartmentService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.DepartmentVo`包 |
| EmployeeService | ✅ | ✅ | 将EmployeeVo的10个属性映射到Employee实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.EmployeeVo`包 | | EmployeeService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.EmployeeVo`包 |
| FunctionService | ✅ | ✅ | 将FunctionVo的code、name、icon等属性映射到Function实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.FunctionVo`包 | | FunctionService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.FunctionVo`包 |
| ProjectCostItemService | ✅ | ✅ | 将ProjectCostItemVo的属性映射到ProjectCostItem实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectCostItemVo`包 | | ProjectCostItemService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectCostItemVo`包 |
| EmployeeRoleService | ✅ | ✅ | 将EmployeeRoleVo的name、code、description等属性映射到EmployeeRole实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.EmployeeRoleVo`包 | | EmployeeRoleService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.EmployeeRoleVo`包 |
| PermissionService | ✅ | ✅ | 将PermissionVo的name、code、description等属性映射到Permission实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.PermissionVo`包 | | PermissionService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.PermissionVo`包 |
| ProductTypeService | ✅ | ✅ | 将ProductTypeVo的name、code、description等属性映射到ProductType实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProductTypeVo`包 | | ProductTypeService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProductTypeVo`包 |
| ProjectFundPlanService | ✅ | ✅ | 将ProjectFundPlanVo的projectId、planDate、amount等属性映射到ProjectFundPlan实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectFundPlanVo`包 | | ProjectFundPlanService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectFundPlanVo`包 |
| ProjectIndustryService | ✅ | ✅ | 将ProjectIndustryVo的name、code等属性映射到ProjectIndustry实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectIndustryVo`包 | | ProjectIndustryService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectIndustryVo`包 |
| ProjectSaleTypeService | ✅ | ✅ | 将ProjectSaleTypeVo的name、code等属性映射到ProjectSaleType实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectSaleTypeVo`包 | | ProjectSaleTypeService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectSaleTypeVo`包 |
| ProjectTypeService | ✅ | ✅ | 将ProjectTypeVo的name、code等属性映射到ProjectType实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectTypeVo`包 | | ProjectTypeService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProjectTypeVo`包 |
| ProductUsageService | ✅ | ✅ | 将ProductUsageVo的name、code等属性映射到ProductUsage实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProductUsageVo`包 | | ProductUsageService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ProductUsageVo`包 |
| CustomerSatisfactionSurveyService | ✅ | ✅ | 将CustomerSatisfactionSurveyVo的customerId、projectId、score等属性映射到CustomerSatisfactionSurvey实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CustomerSatisfactionSurveyVo`包 | | CustomerSatisfactionSurveyService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CustomerSatisfactionSurveyVo`包 |
| InventoryService | ✅ | ✅ | 将InventoryVo的productName、productType、quantity等属性映射到Inventory实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.InventoryVo`包 | | InventoryService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.InventoryVo`包 |
| InventoryHistoryPriceService | ✅ | ✅ | 将InventoryHistoryPriceVo的inventoryId、price、changeDate等属性映射到InventoryHistoryPrice实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.InventoryHistoryPriceVo`包 | | InventoryHistoryPriceService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.InventoryHistoryPriceVo`包 |
| SalesBillVoucherService | ✅ | ✅ | 将SalesBillVoucherVo的refId、code、makerDate、modifyDate、verifierDate、description等属性映射到SalesBillVoucher实体并正确处理company、order、employee、maker、verifier等关联实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.SalesBillVoucherVo`包 | | SalesBillVoucherService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.SalesBillVoucherVo`包 |
| ExtendVendorInfoService | ✅ | ✅ | 将ExtendVendorInfoVo的codeSequenceNumber、assignedProvider、prePurchase等属性映射到ExtendVendorInfo实体并正确处理contract、group等关联实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ExtendVendorInfoVo`包 | | ExtendVendorInfoService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ExtendVendorInfoVo`包 |
| ContractItemService | ✅ | ✅ | 将ContractItemVo的基本属性映射到ContractItem实体并正确处理contract、inventory、creator、updater等关联实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractItemVo`包 | | ContractItemService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractItemVo`包 |
| ContractTypeService | ✅ | ✅ | 将ContractTypeVo的name、code、catalog、title、direction等属性映射到ContractType实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractTypeVo`包 | | ContractTypeService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractTypeVo`包 |
| ContractFileTypeService | ✅ | ✅ | 将ContractFileTypeLocalVo的id、lang、type、value、description、suggestFileName等属性映射到ContractFileTypeLocal实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractFileTypeLocalVo`包 | | ContractFileTypeService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractFileTypeLocalVo`包 |
| SaleOrdersService | ✅ | ✅ | 将SalesOrderVo的code、makerDate、verifierDate、description等属性映射到SalesOrder实体并正确处理contract、employee、maker、verifier等关联实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.SalesOrderVo`包 | | SaleOrdersService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.SalesOrderVo`包 |
| ContractKindService | ✅ | ✅ | 将ContractKindVo的name、code、title等属性映射到ContractKind实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractKindVo`包 | | ContractKindService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractKindVo`包 |
| ContractBidVendorService | ✅ | ✅ | 已正确处理contract、company、quotationSheet等关联实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractBidVendorVo`包 | | ContractBidVendorService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractBidVendorVo`包 |
| ContractPayPlanService | ✅ | ✅ | 将ContractPayPlanVo的refId、payRatio、payCurrency、payDate、payTerm等属性映射到ContractPayPlan实体并正确处理contract关联实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractPayPlanVo`包 | | ContractPayPlanService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractPayPlanVo`包 |
| PurchaseBillVoucherService | ✅ | ✅ | 将PurchaseBillVoucherVo的id、refId、code、companyId、invoiceId、employeeId、makerId、makerDate、modifyDate、verifierId、verifierDate、description等属性映射到PurchaseBillVoucher实体并正确处理company、invoice、employee、maker、verifier等关联实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.PurchaseBillVoucherVo`包 | | PurchaseBillVoucherService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.PurchaseBillVoucherVo`包 |
| PurchaseOrderItemService | ✅ | ✅ | 将PurchaseOrderItemVo的id、code、name、quantity、price等属性映射到PurchaseOrderItem实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.PurchaseOrderItemVo`包 | | PurchaseOrderItemService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.PurchaseOrderItemVo`包 |
| SalesOrderItemService | ✅ | ✅ | 将SalesOrderItemVo的id、code、name、quantity、price等属性映射到SalesOrderItem实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.SalesOrderItemVo`包 | | SalesOrderItemService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.SalesOrderItemVo`包 |
| CompanyBlackReasonService | ✅ | ✅ | 将CompanyBlackReasonVo的companyId、reason、startDate等属性映射到CompanyBlackReason实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyBlackReasonVo`包 | | CompanyBlackReasonService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyBlackReasonVo`包 |
| CompanyCustomerEntityService | ✅ | ✅ | 将CompanyCustomerEntityVo的name、abbName、code等基本属性映射到CompanyCustomerEntity实体并正确处理customer、catalog、creator、modifier等关联实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyCustomerEntityVo`包 | | CompanyCustomerEntityService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyCustomerEntityVo`包 |
| CompanyFileTypeService | ✅ | ✅ | 将CompanyFileTypeVo的name、code、description等属性映射到CompanyFileType实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyFileTypeVo`包 | | CompanyFileTypeService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyFileTypeVo`包 |
| CompanyOldNameService | ✅ | ✅ | 将CompanyOldNameVo的companyId、name、beginDate、endDate等属性映射到CompanyOldName实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyOldNameVo`包 | | CompanyOldNameService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.CompanyOldNameVo`包 |
| PurchaseBillVoucherItemService | ✅ | ✅ | 将PurchaseBillVoucherItemVo的id、refId、quantity、price等属性映射到PurchaseBillVoucherItem实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.PurchaseBillVoucherItemVo`包 | | PurchaseBillVoucherItemService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.PurchaseBillVoucherItemVo`包 |
| ContractFileService | ✅ | ✅ | 将ContractFileVo的id、contractId、type、fileName、applyDate、description等属性映射到ContractFile实体并正确处理contract关联实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractFileVo`包 | | ContractFileService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractFileVo`包 |
| ContractCatalogService | ✅ | ✅ | 将ContractCatalogVo的id、code、name、path、parent、useYear等属性映射到ContractCatalog实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractCatalogVo`包 | | ContractCatalogService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractCatalogVo`包 |
| ContractGroupService | ✅ | ✅ | 将ContractGroupVo的id、name、code、title等属性映射到ContractGroup实体 | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractGroupVo`包 | | ContractGroupService | ✅ | ✅ | | 已正确导入`com.ecep.contract.service.VoableService``com.ecep.contract.vo.ContractGroupVo`包 |
## 已完成的全部实现 ## 已完成的全部实现

34
pom.xml
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.86-SNAPSHOT</version> <version>0.0.99-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
<module>server</module> <module>server</module>
@@ -109,37 +109,7 @@
</distributionManagement> </distributionManagement>
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.ecep.contract.manager.AppV2</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>

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.86-SNAPSHOT</version> <version>0.0.99-SNAPSHOT</version>
</parent> </parent>
<groupId>com.ecep.contract</groupId> <groupId>com.ecep.contract</groupId>
<artifactId>server</artifactId> <artifactId>server</artifactId>
<version>0.0.86-SNAPSHOT</version> <version>0.0.99-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.86-SNAPSHOT</version> <version>0.0.99-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@@ -44,8 +44,6 @@ import com.ecep.contract.util.TaskMonitorCenter;
}) })
@EnableScheduling @EnableScheduling
@EnableAsync @EnableAsync
@EnableCaching
@EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO) @EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO)
public class SpringApp { public class SpringApp {

View File

@@ -160,6 +160,7 @@ public class LoginApiController {
// 其他错误 // 其他错误
result.put("success", false); result.put("success", false);
result.put("error", "登录过程中发生错误: " + e.getMessage()); result.put("error", "登录过程中发生错误: " + e.getMessage());
logger.error("登录错误:{}", e);
} }
return result; return result;

View File

@@ -34,14 +34,14 @@ public class AbstractCtx {
private Map<Class<?>, Object> cachedBeans = new HashMap<>(); private Map<Class<?>, Object> cachedBeans = new HashMap<>();
public <T> T getBean(Class<T> requiredType) throws BeansException { public <T> T getBean(Class<T> requiredType) throws BeansException {
return SpringApp.getBean(requiredType); return getCachedBean(requiredType);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T> T getCachedBean(Class<T> requiredType) throws BeansException { public <T> T getCachedBean(Class<T> requiredType) throws BeansException {
Object object = cachedBeans.get(requiredType); Object object = cachedBeans.get(requiredType);
if (object == null) { if (object == null) {
object = getBean(requiredType); object = SpringApp.getBean(requiredType);
cachedBeans.put(requiredType, object); cachedBeans.put(requiredType, object);
} }
return (T) object; return (T) object;
@@ -60,7 +60,7 @@ public class AbstractCtx {
} }
public boolean updateText(Supplier<String> getter, Consumer<String> setter, String text, MessageHolder holder, public boolean updateText(Supplier<String> getter, Consumer<String> setter, String text, MessageHolder holder,
String topic) { String topic) {
if (!Objects.equals(getter.get(), text)) { if (!Objects.equals(getter.get(), text)) {
setter.accept(text); setter.accept(text);
holder.info(topic + "修改为: " + text); holder.info(topic + "修改为: " + text);
@@ -70,7 +70,7 @@ public class AbstractCtx {
} }
public boolean updateAppendText(Supplier<String> getter, Consumer<String> setter, String text, MessageHolder holder, public boolean updateAppendText(Supplier<String> getter, Consumer<String> setter, String text, MessageHolder holder,
String topic) { String topic) {
if (StringUtils.hasText(text)) { if (StringUtils.hasText(text)) {
String str = MyStringUtils.appendIfAbsent(getter.get(), text); String str = MyStringUtils.appendIfAbsent(getter.get(), text);
if (!Objects.equals(getter.get(), str)) { if (!Objects.equals(getter.get(), str)) {
@@ -83,7 +83,7 @@ public class AbstractCtx {
} }
public boolean updateLocalDate(Supplier<LocalDate> getter, Consumer<LocalDate> setter, java.sql.Date date, public boolean updateLocalDate(Supplier<LocalDate> getter, Consumer<LocalDate> setter, java.sql.Date date,
MessageHolder holder, String topic) { MessageHolder holder, String topic) {
if (date != null) { if (date != null) {
return updateLocalDate(getter, setter, date.toLocalDate(), holder, topic); return updateLocalDate(getter, setter, date.toLocalDate(), holder, topic);
} }
@@ -91,12 +91,12 @@ public class AbstractCtx {
} }
public boolean updateLocalDate(Supplier<LocalDate> getter, Consumer<LocalDate> setter, LocalDate date, public boolean updateLocalDate(Supplier<LocalDate> getter, Consumer<LocalDate> setter, LocalDate date,
MessageHolder holder, String topic) { MessageHolder holder, String topic) {
return updateLocalDate(getter, setter, date, holder, topic, false); return updateLocalDate(getter, setter, date, holder, topic, false);
} }
public boolean updateLocalDate(Supplier<LocalDate> getter, Consumer<LocalDate> setter, LocalDate date, public boolean updateLocalDate(Supplier<LocalDate> getter, Consumer<LocalDate> setter, LocalDate date,
MessageHolder holder, String topic, boolean allowNull) { MessageHolder holder, String topic, boolean allowNull) {
if (date == null && !allowNull) { if (date == null && !allowNull) {
return false; return false;
} }
@@ -109,7 +109,7 @@ public class AbstractCtx {
} }
public boolean updateLocalDate(Supplier<LocalDate> getter, Consumer<LocalDate> setter, String strDate, public boolean updateLocalDate(Supplier<LocalDate> getter, Consumer<LocalDate> setter, String strDate,
MessageHolder holder, String topic) { MessageHolder holder, String topic) {
LocalDate date = null; LocalDate date = null;
if (StringUtils.hasText(strDate)) { if (StringUtils.hasText(strDate)) {
try { try {
@@ -122,7 +122,7 @@ public class AbstractCtx {
} }
public boolean updateLocalDate(Supplier<LocalDate> getter, Consumer<LocalDate> setter, Timestamp timestamp, public boolean updateLocalDate(Supplier<LocalDate> getter, Consumer<LocalDate> setter, Timestamp timestamp,
MessageHolder holder, String topic) { MessageHolder holder, String topic) {
LocalDate date = null; LocalDate date = null;
if (timestamp != null) { if (timestamp != null) {
@@ -136,7 +136,7 @@ public class AbstractCtx {
} }
public boolean updateLocalDateTime(Supplier<LocalDateTime> getter, Consumer<LocalDateTime> setter, public boolean updateLocalDateTime(Supplier<LocalDateTime> getter, Consumer<LocalDateTime> setter,
Timestamp timestamp, MessageHolder holder, String topic) { Timestamp timestamp, MessageHolder holder, String topic) {
LocalDateTime dateTime = null; LocalDateTime dateTime = null;
if (timestamp != null) { if (timestamp != null) {
@@ -152,7 +152,7 @@ public class AbstractCtx {
} }
public boolean updateLocalDateTime(Supplier<LocalDateTime> getter, Consumer<LocalDateTime> setter, public boolean updateLocalDateTime(Supplier<LocalDateTime> getter, Consumer<LocalDateTime> setter,
LocalDateTime dateTime, MessageHolder holder, String topic) { LocalDateTime dateTime, MessageHolder holder, String topic) {
if (!Objects.equals(getter.get(), dateTime)) { if (!Objects.equals(getter.get(), dateTime)) {
setter.accept(dateTime); setter.accept(dateTime);
holder.info(topic + "修改为: " + dateTime); holder.info(topic + "修改为: " + dateTime);
@@ -162,7 +162,7 @@ public class AbstractCtx {
} }
public boolean updateInstant(Supplier<Instant> getter, Consumer<Instant> setter, Instant instant, public boolean updateInstant(Supplier<Instant> getter, Consumer<Instant> setter, Instant instant,
MessageHolder holder, String topic) { MessageHolder holder, String topic) {
if (!Objects.equals(getter.get(), instant)) { if (!Objects.equals(getter.get(), instant)) {
setter.accept(instant); setter.accept(instant);
holder.info(topic + "修改为: " + instant); holder.info(topic + "修改为: " + instant);
@@ -172,13 +172,13 @@ public class AbstractCtx {
} }
public boolean updateNumber(Supplier<Double> getter, Consumer<Double> setter, BigDecimal value, public boolean updateNumber(Supplier<Double> getter, Consumer<Double> setter, BigDecimal value,
MessageHolder holder, String topic) { MessageHolder holder, String topic) {
double val = value.doubleValue(); double val = value.doubleValue();
return updateNumber(getter, setter, val, holder, topic); return updateNumber(getter, setter, val, holder, topic);
} }
public boolean updateNumber(Supplier<Double> getter, Consumer<Double> setter, double value, MessageHolder holder, public boolean updateNumber(Supplier<Double> getter, Consumer<Double> setter, double value, MessageHolder holder,
String topic) { String topic) {
if (getter.get() == null || !NumberUtils.equals(getter.get(), value)) { if (getter.get() == null || !NumberUtils.equals(getter.get(), value)) {
setter.accept(value); setter.accept(value);
holder.info(topic + "修改为: " + value); holder.info(topic + "修改为: " + value);
@@ -188,7 +188,7 @@ public class AbstractCtx {
} }
public boolean updateNumber(Supplier<Float> getter, Consumer<Float> setter, float value, MessageHolder holder, public boolean updateNumber(Supplier<Float> getter, Consumer<Float> setter, float value, MessageHolder holder,
String topic) { String topic) {
if (getter.get() == null || !NumberUtils.equals(getter.get(), value)) { if (getter.get() == null || !NumberUtils.equals(getter.get(), value)) {
setter.accept(value); setter.accept(value);
holder.info(topic + "修改为: " + value); holder.info(topic + "修改为: " + value);
@@ -198,7 +198,7 @@ public class AbstractCtx {
} }
public boolean updateNumber(Supplier<Integer> getter, Consumer<Integer> setter, Integer value, MessageHolder holder, public boolean updateNumber(Supplier<Integer> getter, Consumer<Integer> setter, Integer value, MessageHolder holder,
String topic) { String topic) {
if (getter.get() == null || !NumberUtils.equals(getter.get(), value)) { if (getter.get() == null || !NumberUtils.equals(getter.get(), value)) {
setter.accept(value); setter.accept(value);
holder.info(topic + "修改为: " + value); holder.info(topic + "修改为: " + value);

View File

@@ -39,9 +39,8 @@ public class CloudRkSyncTask extends Tasker<Object> {
try { try {
cloudRkCtx = new CloudRkCtx(); cloudRkCtx = new CloudRkCtx();
service = SpringApp.getBean(CloudRkService.class); service = SpringApp.getBean(CloudRkService.class);
cloudRkCtx.setCloudRkService(service);
} catch (BeansException e) { } catch (BeansException e) {
holder.error("没有找到 " + CloudServiceConstant.RK_NAME + " 服务"); holder.error(CloudServiceConstant.RK_NAME + " 服务未启用");
return null; return null;
} }

View File

@@ -0,0 +1,12 @@
package com.ecep.contract.cloud.rk.ctx;
import com.ecep.contract.cloud.rk.CloudRkService;
import org.springframework.beans.BeansException;
public interface CloudRkContext {
<T> T getBean(Class<T> requiredType) throws BeansException;
default CloudRkService getCloudRkService() {
return getBean(CloudRkService.class);
}
}

View File

@@ -1,7 +1,5 @@
package com.ecep.contract.cloud.rk.ctx; package com.ecep.contract.cloud.rk.ctx;
import static com.ecep.contract.SpringApp.getBean;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@@ -31,13 +29,11 @@ import org.springframework.util.StringUtils;
import com.ecep.contract.BlackReasonType; import com.ecep.contract.BlackReasonType;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.cloud.AbstractCtx; import com.ecep.contract.cloud.AbstractCtx;
import com.ecep.contract.cloud.rk.CloudRkService; import com.ecep.contract.cloud.rk.CloudRkService;
import com.ecep.contract.ds.company.service.CompanyBlackReasonService; import com.ecep.contract.ds.company.service.CompanyBlackReasonService;
import com.ecep.contract.ds.company.service.CompanyContactService; import com.ecep.contract.ds.company.service.CompanyContactService;
import com.ecep.contract.ds.company.service.CompanyOldNameService; import com.ecep.contract.ds.company.service.CompanyOldNameService;
import com.ecep.contract.ds.company.service.CompanyService;
import com.ecep.contract.model.CloudRk; import com.ecep.contract.model.CloudRk;
import com.ecep.contract.model.Company; import com.ecep.contract.model.Company;
import com.ecep.contract.model.CompanyBlackReason; import com.ecep.contract.model.CompanyBlackReason;
@@ -50,46 +46,16 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Setter; public class CloudRkCtx extends AbstractCtx implements CloudRkContext {
public class CloudRkCtx extends AbstractCtx {
private static final Logger logger = LoggerFactory.getLogger(CloudRkCtx.class); private static final Logger logger = LoggerFactory.getLogger(CloudRkCtx.class);
@Setter
private CloudRkService cloudRkService;
@Setter
private CompanyService companyService;
@Setter
private CompanyBlackReasonService companyBlackReasonService;
@Setter
private ObjectMapper objectMapper;
private Proxy socksProxy; private Proxy socksProxy;
public CloudRkService getCloudRkService() {
if (cloudRkService == null) {
cloudRkService = getBean(CloudRkService.class);
}
return cloudRkService;
}
public CompanyService getCompanyService() {
if (companyService == null) {
companyService = getBean(CompanyService.class);
}
return companyService;
}
CompanyBlackReasonService getCompanyBlackReasonService() { CompanyBlackReasonService getCompanyBlackReasonService() {
if (companyBlackReasonService == null) { return getCachedBean(CompanyBlackReasonService.class);
companyBlackReasonService = getBean(CompanyBlackReasonService.class);
}
return companyBlackReasonService;
} }
ObjectMapper getObjectMapper() { ObjectMapper getObjectMapper() {
if (objectMapper == null) { return getCachedBean(ObjectMapper.class);
objectMapper = getBean(ObjectMapper.class);
}
return objectMapper;
} }
Proxy getSocksProxy() { Proxy getSocksProxy() {
@@ -150,7 +116,11 @@ public class CloudRkCtx extends AbstractCtx {
} catch (Exception e) { } catch (Exception e) {
// 异常 // 异常
logger.error("使用评分接口更新企业资信评价等级时发生错误", e); logger.error("使用评分接口更新企业资信评价等级时发生错误", e);
cloudRk.setDescription("评分接口错误:" + e.getMessage()); String message = "评分接口错误:" + e.getMessage();
if (message.length() > 50) {
message = message.substring(0, 50);
}
cloudRk.setDescription(message);
} }
return updated; return updated;
} }
@@ -172,11 +142,12 @@ public class CloudRkCtx extends AbstractCtx {
} }
} }
CompanyBlackReasonService companyBlackReasonService = getCompanyBlackReasonService();
String api = getConfService().getString(CloudRkService.KEY_BLACK_LIST_URL); String api = getConfService().getString(CloudRkService.KEY_BLACK_LIST_URL);
List<String> companyNames = getCompanyService().getAllNames(company); List<String> companyNames = getCompanyService().getAllNames(company);
List<CompanyBlackReason> reasonList = new ArrayList<>(); List<CompanyBlackReason> reasonList = new ArrayList<>();
List<CompanyBlackReason> dbReasons = getCompanyBlackReasonService().findAllByCompany(company); List<CompanyBlackReason> dbReasons = companyBlackReasonService.findAllByCompany(company);
for (String name : companyNames) { for (String name : companyNames) {
String url = api + URLEncoder.encode(name, StandardCharsets.UTF_8); String url = api + URLEncoder.encode(name, StandardCharsets.UTF_8);
try { try {
@@ -195,7 +166,7 @@ public class CloudRkCtx extends AbstractCtx {
} }
for (CompanyBlackReason companyBlackReason : reasonList) { for (CompanyBlackReason companyBlackReason : reasonList) {
getCompanyBlackReasonService().save(companyBlackReason); companyBlackReasonService.save(companyBlackReason);
} }
cloudRk.setCloudBlackListUpdated(LocalDateTime.now()); cloudRk.setCloudBlackListUpdated(LocalDateTime.now());
return true; return true;
@@ -634,14 +605,15 @@ public class CloudRkCtx extends AbstractCtx {
return modified; return modified;
} }
public boolean updateLocalDateTime(Supplier<LocalDateTime> getter, Consumer<LocalDateTime> setter, JsonNode data,String field, public boolean updateLocalDateTime(Supplier<LocalDateTime> getter, Consumer<LocalDateTime> setter, JsonNode data,
String field,
MessageHolder holder, String topic) { MessageHolder holder, String topic) {
JsonNode node = data.get(field); JsonNode node = data.get(field);
if (node == null || node.isNull()) { if (node == null || node.isNull()) {
return false; return false;
} }
LocalDateTime updated = getObjectMapper().convertValue(node, LocalDateTime.class); LocalDateTime updated = getObjectMapper().convertValue(node, LocalDateTime.class);
updateLocalDateTime(getter, setter, updated, holder, topic); updateLocalDateTime(getter, setter, updated, holder, topic);
return false; return false;
} }
@@ -654,7 +626,7 @@ public class CloudRkCtx extends AbstractCtx {
return; return;
} }
CompanyContactService contactService = SpringApp.getBean(CompanyContactService.class); CompanyContactService contactService = getCachedBean(CompanyContactService.class);
List<CompanyContact> contactList = contactService.findAllByCompanyAndName(company, legalRepresentative); List<CompanyContact> contactList = contactService.findAllByCompanyAndName(company, legalRepresentative);
if (contactList == null) { if (contactList == null) {
// db error // db error
@@ -746,7 +718,7 @@ public class CloudRkCtx extends AbstractCtx {
historyNames.add(trimmed); historyNames.add(trimmed);
} }
} }
CompanyOldNameService service = SpringApp.getBean(CompanyOldNameService.class); CompanyOldNameService service = getCachedBean(CompanyOldNameService.class);
List<CompanyOldName> oldNames = service.findAllByCompany(company); List<CompanyOldName> oldNames = service.findAllByCompany(company);
for (CompanyOldName oldName : oldNames) { for (CompanyOldName oldName : oldNames) {
// 已经存在的移除 // 已经存在的移除

View File

@@ -6,10 +6,9 @@ import java.util.Map;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.ecep.contract.service.WebSocketServerTasker; import com.ecep.contract.service.tasker.WebSocketServerTasker;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;

View File

@@ -47,10 +47,6 @@ public class AbstractYongYouU8Ctx extends AbstractCtx {
} }
} }
public CompanyService getCompanyService() {
return getCachedBean(CompanyService.class);
}
public CompanyCustomerService getCompanyCustomerService() { public CompanyCustomerService getCompanyCustomerService() {
return getCachedBean(CompanyCustomerService.class); return getCachedBean(CompanyCustomerService.class);
} }

View File

@@ -6,6 +6,7 @@ import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import com.ecep.contract.ds.company.CompanyContext;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
@@ -18,17 +19,7 @@ import com.ecep.contract.model.CompanyOldName;
import lombok.Setter; import lombok.Setter;
public class CompanyCtx extends AbstractYongYouU8Ctx { public class CompanyCtx extends AbstractYongYouU8Ctx implements CompanyContext {
@Setter
private CompanyOldNameService companyOldNameService;
CompanyOldNameService getCompanyOldNameService() {
if (companyOldNameService == null) {
companyOldNameService = getBean(CompanyOldNameService.class);
}
return companyOldNameService;
}
public boolean updateCompanyNameIfAbsent(Company company, String name, MessageHolder holder) { public boolean updateCompanyNameIfAbsent(Company company, String name, MessageHolder holder) {
if (!StringUtils.hasText(name)) { if (!StringUtils.hasText(name)) {
return false; return false;
@@ -110,7 +101,7 @@ public class CompanyCtx extends AbstractYongYouU8Ctx {
return null; return null;
} }
CompanyService companyService = getCompanyService(); CompanyService companyService = getCompanyService();
String autoCreateAfter = getConfService().getString(CloudYuConstant.KEY_AUTO_CREATE_COMPANY_AFTER); String autoCreateAfter = getConfService().getString(CloudYuConstant.KEY_AUTO_CREATE_COMPANY_AFTER);
// 当配置存在,且开发时间小于指定时间,不创建 // 当配置存在,且开发时间小于指定时间,不创建
if (StringUtils.hasText(autoCreateAfter)) { if (StringUtils.hasText(autoCreateAfter)) {
LocalDate miniDate = LocalDate.parse(autoCreateAfter); LocalDate miniDate = LocalDate.parse(autoCreateAfter);

View File

@@ -0,0 +1,64 @@
package com.ecep.contract.config;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
/**
* 自定义LocalDateTime反序列化器支持多种格式
*/
class CustomLocalDateTimeDeserializer extends StdDeserializer<LocalDateTime> {
private final List<DateTimeFormatter> formatters;
public CustomLocalDateTimeDeserializer() {
super(LocalDateTime.class);
// 支持多种日期时间格式
this.formatters = new ArrayList<>();
// ISO标准格式
this.formatters.add(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
// 项目默认格式 yyyy-MM-dd HH:mm:ss
this.formatters.add(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 其他可能的格式
this.formatters.add(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
this.formatters.add(DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss"));
this.formatters.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm"));
this.formatters.add(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
}
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String value = p.getText().trim();
if (value == null || value.isEmpty()) {
return null;
}
// 尝试使用各种格式解析
for (DateTimeFormatter formatter : formatters) {
try {
return LocalDateTime.parse(value, formatter);
} catch (Exception e) {
// 尝试下一种格式
continue;
}
}
// 如果所有格式都失败尝试使用ISO_INSTANT格式可能是时间戳字符串
try {
return LocalDateTime.ofInstant(Instant.parse(value), ZoneId.systemDefault());
} catch (Exception e) {
// 仍然失败,抛出异常
throw new JsonParseException(p, "无法解析为LocalDateTime: " + value);
}
}
}

View File

@@ -0,0 +1,111 @@
package com.ecep.contract.config;
import java.io.IOException;
import org.hibernate.proxy.HibernateProxy;
import com.ecep.contract.model.IdentityEntity;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
/**
* 用于处理Hibernate代理对象的反序列化器
* 专门处理序列化时写入的id和_proxy_字段
*/
class HibernateProxyDeserializer extends StdDeserializer<HibernateProxy> {
protected HibernateProxyDeserializer() {
super(HibernateProxy.class);
}
@Override
public HibernateProxy deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 检查是否是对象类型
if (p.currentToken() != JsonToken.START_OBJECT) {
return (HibernateProxy) ctxt.handleUnexpectedToken(HibernateProxy.class, p);
}
// 读取对象字段
JsonToken token;
Object id = null;
String proxyClassName = null;
// 解析JSON对象的所有字段
while ((token = p.nextToken()) != JsonToken.END_OBJECT) {
if (token == JsonToken.FIELD_NAME) {
String fieldName = p.getCurrentName();
p.nextToken(); // 移动到字段值
if ("id".equals(fieldName)) {
id = p.readValueAs(Object.class);
} else if ("_proxy_".equals(fieldName)) {
proxyClassName = p.getText();
}
}
}
// 如果同时存在id和_proxy_字段说明这是一个Hibernate代理对象
if (id != null && proxyClassName != null) {
HibernateProxy proxy = null;
try {
// 尝试获取实体类类型(去掉代理类名中的字节码增强部分)
// 代理类名通常格式为com.ecep.contract.model.Entity$HibernateProxy$XXXX
int proxyIndex = proxyClassName.indexOf("$HibernateProxy$");
String entityClassName = proxyIndex > 0 ? proxyClassName.substring(0, proxyIndex) : proxyClassName;
// 加载实体类
Class<?> entityClass = Class.forName(entityClassName);
// 检查是否是IdentityEntity类型
if (IdentityEntity.class.isAssignableFrom(entityClass)) {
// 创建实体对象并设置ID
IdentityEntity entity = (IdentityEntity) entityClass.getDeclaredConstructor().newInstance();
if (id instanceof Number) {
entity.setId(((Number) id).intValue());
} else {
// 处理非数值类型的ID
try {
entity.setId(Integer.parseInt(id.toString()));
} catch (NumberFormatException e) {
// 如果无法转换为Integer记录警告但继续处理
}
}
return (HibernateProxy) entity;
}
} catch (Exception e) {
// 发生异常时返回一个只包含ID的简单对象
// 这可以避免反序列化失败
return (HibernateProxy) createSimpleIdObject(id);
}
}
// 如果不是代理对象或处理失败,回退到默认的反序列化逻辑
// 使用 skipChildren() 确保解析器在正确的位置开始默认反序列化
if (p.currentToken() != JsonToken.START_OBJECT) {
// 尝试查找对象开始位置
while (p.currentToken() != null && p.currentToken() != JsonToken.START_OBJECT) {
p.nextToken();
}
}
if (p.currentToken() == JsonToken.START_OBJECT) {
return (HibernateProxy) ctxt.readValue(p, Object.class);
}
return null;
}
/**
* 创建一个只包含ID字段的简单对象
*/
private Object createSimpleIdObject(final Object id) {
return new Object() {
@Override
public String toString() {
return "ProxyObject[id=" + id + "]";
}
};
}
}

View File

@@ -0,0 +1,111 @@
package com.ecep.contract.config;
import java.io.IOException;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import com.ecep.contract.model.IdentityEntity;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
/**
* 专门用于处理HibernateProxy对象的序列化器
*/
class HibernateProxySerializer extends StdSerializer<HibernateProxy> {
protected HibernateProxySerializer() {
super(HibernateProxy.class);
}
@Override
public void serialize(HibernateProxy value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
// 尝试初始化代理对象如果未初始化则只输出ID
try {
LazyInitializer lazyInitializer = value.getHibernateLazyInitializer();
if (lazyInitializer.isUninitialized()) {
// 如果代理对象未初始化只输出ID
if (lazyInitializer.getIdentifier() != null) {
gen.writeStartObject();
gen.writeFieldName("id");
gen.writeObject(lazyInitializer.getIdentifier());
gen.writeStringField("_proxy_", lazyInitializer.getEntityName());
gen.writeEndObject();
} else {
gen.writeNull();
}
} else {
// 如果代理对象已初始化,获取实际对象并序列化
Object unwrapped = lazyInitializer.getImplementation();
if (unwrapped instanceof IdentityEntity) {
// 对于IdentityEntity类型输出更简洁的格式
gen.writeStartObject();
gen.writeNumberField("id", ((IdentityEntity) unwrapped).getId());
gen.writeEndObject();
} else {
// 对于非IdentityEntity类型使用默认序列化
serializers.defaultSerializeValue(unwrapped, gen);
}
}
} catch (Exception e) {
// 如果发生异常,输出最小化信息
gen.writeStartObject();
gen.writeStringField("error", "Failed to serialize Hibernate proxy: " + e.getMessage());
gen.writeStringField("class", value.getClass().getName());
gen.writeEndObject();
}
}
@Override
public void serializeWithType(HibernateProxy value, JsonGenerator gen, SerializerProvider serializers,
TypeSerializer typeSer) throws IOException {
// 当启用类型信息时Jackson会调用这个方法
try {
LazyInitializer lazyInitializer = value.getHibernateLazyInitializer();
// 获取实际的实体类型名称
String entityTypeName = lazyInitializer.getEntityName();
Class<?> entityClass = Class.forName(entityTypeName);
if (lazyInitializer.isUninitialized()) {
// 确保输出@class: 'org.hibernate.proxy.HibernateProxy'
// 直接使用HibernateProxy.class作为typeId
gen.writeStartObject();
gen.writeStringField("@class", "org.hibernate.proxy.HibernateProxy");
// 对于未初始化的对象,添加代理信息
gen.writeStringField("_proxy_", entityTypeName);
// 写入ID信息
if (lazyInitializer.getIdentifier() != null) {
gen.writeFieldName("id");
gen.writeObject(lazyInitializer.getIdentifier());
}
gen.writeEndObject();
} else {
// 如果代理对象已初始化,获取实际对象
Object unwrapped = lazyInitializer.getImplementation();
// 使用实际对象的类型信息进行序列化
// 查找能够处理类型信息的序列化器
JsonSerializer<Object> serializer = serializers.findTypedValueSerializer(
unwrapped.getClass(), true, null);
// 委托给实际对象的序列化器处理,确保类型信息正确添加
serializer.serializeWithType(unwrapped, gen, serializers, typeSer);
}
} catch (Exception e) {
// 如果发生异常,输出最小化信息并添加类型信息
// 先写入类型前缀
typeSer.writeTypePrefix(gen, typeSer.typeId(value.getClass(), JsonToken.START_OBJECT));
// 输出错误信息
gen.writeStringField("error", "Failed to serialize Hibernate proxy with type: " + e.getMessage());
gen.writeStringField("class", value.getClass().getName());
// 写入类型后缀
typeSer.writeTypeSuffix(gen, typeSer.typeId(value.getClass(), JsonToken.START_OBJECT));
}
}
}

View File

@@ -5,30 +5,32 @@ import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import com.ecep.contract.model.IdentityEntity; import com.ecep.contract.model.IdentityEntity;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
@@ -43,7 +45,6 @@ public class JacksonConfig {
@Bean @Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.build(); ObjectMapper objectMapper = builder.build();
// 关闭日期时间格式化输出为时间戳而是输出为ISO格式的字符串 // 关闭日期时间格式化输出为时间戳而是输出为ISO格式的字符串
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
@@ -52,6 +53,9 @@ public class JacksonConfig {
// 处理循环引用,启用引用处理功能 // 处理循环引用,启用引用处理功能
objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
// 禁用在遇到空Bean时抛出异常
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// 配置Java 8时间模块的序列化格式 // 配置Java 8时间模块的序列化格式
JavaTimeModule javaTimeModule = new JavaTimeModule(); JavaTimeModule javaTimeModule = new JavaTimeModule();
// LocalDate // LocalDate
@@ -63,154 +67,23 @@ public class JacksonConfig {
// LocalDateTime // LocalDateTime
javaTimeModule.addSerializer(LocalDateTime.class, javaTimeModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); new LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
javaTimeModule.addDeserializer(LocalDateTime.class, // 自定义LocalDateTime反序列化器支持多种格式
new LocalDateTimeDeserializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); javaTimeModule.addDeserializer(LocalDateTime.class, new CustomLocalDateTimeDeserializer());
objectMapper.registerModule(javaTimeModule); objectMapper.registerModule(javaTimeModule);
// 添加自定义模块用于处理JPA/Hibernate代理对象 // 添加自定义模块用于处理JPA/Hibernate代理对象
SimpleModule proxyModule = new SimpleModule("HibernateProxyModule"); // SimpleModule proxyModule = new SimpleModule("HibernateProxyModule");
// 添加代理对象序列化器只输出ID字段 // // 添加懒加载初始化器序列化器
proxyModule.addSerializer(HibernateProxy.class, new HibernateProxySerializer()); // proxyModule.addSerializer(LazyInitializer.class, new
// 使用BeanSerializerModifier来处理IdentityEntity类型避免递归调用 // HibernateProxySerializer());
proxyModule.setSerializerModifier(new BeanSerializerModifier() { // // 添加Hibernate代理对象反序列化器
@Override // proxyModule.addDeserializer(HibernateProxy.class, new
public JsonSerializer<?> modifySerializer(SerializationConfig config, // HibernateProxyDeserializer());
BeanDescription beanDesc,
JsonSerializer<?> serializer) {
// 只对IdentityEntity类型进行修改
if (IdentityEntity.class.isAssignableFrom(beanDesc.getBeanClass()) &&
!HibernateProxy.class.isAssignableFrom(beanDesc.getBeanClass())) {
return new IdentityEntitySerializer(serializer);
}
return serializer;
}
});
objectMapper.registerModule(proxyModule); // objectMapper.registerModule(proxyModule);
return objectMapper; return objectMapper;
} }
/**
* 专门用于处理Hibernate代理对象的序列化器
*/
private static class HibernateProxySerializer extends StdSerializer<HibernateProxy> {
protected HibernateProxySerializer() {
super(HibernateProxy.class);
}
@Override
public void serialize(HibernateProxy value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
// 尝试初始化代理对象如果未初始化则只输出ID
try {
Object unwrapped = value.getHibernateLazyInitializer().getImplementation();
// 检查是否为IdentityEntity实现类
if (unwrapped instanceof IdentityEntity) {
gen.writeStartObject();
gen.writeNumberField("id", ((IdentityEntity) unwrapped).getId());
gen.writeEndObject();
} else {
// 如果不是IdentityEntity使用默认序列化
serializers.defaultSerializeValue(unwrapped, gen);
}
} catch (Exception e) {
// 如果初始化失败只输出ID
if (value instanceof IdentityEntity) {
gen.writeStartObject();
gen.writeNumberField("id", ((IdentityEntity) value).getId());
gen.writeEndObject();
} else {
// 如果不是IdentityEntity输出对象的基本信息
gen.writeStartObject();
gen.writeStringField("class", value.getClass().getName());
gen.writeEndObject();
}
}
}
}
/**
* 用于处理IdentityEntity类型的序列化器包装器
*/
private static class IdentityEntitySerializer extends StdSerializer<Object> {
private final JsonSerializer<?> delegate;
protected IdentityEntitySerializer(JsonSerializer<?> delegate) {
super(Object.class);
this.delegate = delegate;
}
@SuppressWarnings("unchecked")
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
// 对于IdentityEntity对象如果未初始化则只输出ID
if (value instanceof IdentityEntity && !Hibernate.isInitialized(value)) {
gen.writeStartObject();
gen.writeNumberField("id", ((IdentityEntity) value).getId());
gen.writeStringField("_proxy_", value.getClass().getName());
gen.writeEndObject();
} else {
// 已初始化的实体,使用原始的序列化器进行序列化,避免递归调用
((JsonSerializer<Object>) delegate).serialize(value, gen, serializers);
}
}
}
/**
* 用于处理对象引用的序列化器,通过检测和管理对象引用避免循环引用问题
*/
private static class IdentityReferenceSerializer extends StdSerializer<Object> {
private final JsonSerializer<?> delegate;
private final ThreadLocal<java.util.Set<Object>> visitedObjects = ThreadLocal
.withInitial(java.util.HashSet::new);
protected IdentityReferenceSerializer(JsonSerializer<?> delegate) {
super(Object.class);
this.delegate = delegate;
}
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 处理null值
if (value == null) {
serializers.defaultSerializeNull(gen);
return;
}
java.util.Set<Object> visited = visitedObjects.get();
try {
// 检查对象是否已经被访问过
if (visited.contains(value)) {
// 如果对象实现了IdentityEntity只输出ID
if (value instanceof IdentityEntity) {
gen.writeStartObject();
gen.writeNumberField("id", ((IdentityEntity) value).getId());
gen.writeStringField("_ref_", "" + ((IdentityEntity) value).getId());
gen.writeEndObject();
} else {
// 对于非IdentityEntity对象输出对象的toString或hashCode
gen.writeString(value.toString());
}
return;
}
// 标记对象为已访问
visited.add(value);
// 使用委托序列化器进行正常序列化
((JsonSerializer<Object>) delegate).serialize(value, gen, serializers);
} finally {
// 清理访问记录
visited.remove(value);
if (visited.isEmpty()) {
visitedObjects.remove();
}
}
}
}
} }

View File

@@ -0,0 +1,131 @@
package com.ecep.contract.config;
import java.time.Duration;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.ecep.contract.model.IdentityEntity;
import com.ecep.contract.util.HibernateProxyUtils;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
/**
* Redis缓存配置类用于配置Redis的缓存管理器和序列化器
*/
@Configuration
@EnableCaching
public class RedisCacheConfig {
/**
* 配置RedisTemplate使用与JacksonConfig相同的ObjectMapper以确保Hibernate代理对象被正确序列化
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory,
ObjectMapper objectMapper) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 设置键的序列化器
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 使用GenericJackson2JsonRedisSerializer并配置为使用我们自定义的ObjectMapper
// 这个ObjectMapper已经包含了处理Hibernate代理对象的序列化器
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(
objectMapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
/**
* 配置RedisCacheManager使用与JacksonConfig相同的ObjectMapper以确保Hibernate代理对象被正确序列化
* 并启用类型信息存储以解决LinkedHashMap无法转换为具体类型的问题
*/
// @Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, ObjectMapper objectMapper) {
// 创建ObjectMapper的副本而不是直接修改注入的实例
ObjectMapper cacheObjectMapper = objectMapper.copy();
// 配置ObjectMapper副本以保留类型信息仅用于Redis缓存
// 使用activateDefaultTyping代替已弃用的enableDefaultTyping
cacheObjectMapper.activateDefaultTyping(
cacheObjectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
// 确保我们的HibernateProxyDeserializer被正确注册
// 创建一个新的模块来注册自定义反序列化器
SimpleModule proxyModule = new SimpleModule("CacheHibernateProxyModule");
// 添加代理对象序列化器只输出ID字段
proxyModule.addSerializer(HibernateProxy.class, new HibernateProxySerializer());
proxyModule.addDeserializer(HibernateProxy.class, new HibernateProxyDeserializer());
// 使用BeanSerializerModifier来处理IdentityEntity类型避免递归调用
proxyModule.setSerializerModifier(new BeanSerializerModifier() {
@Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc,
JsonSerializer<?> serializer) {
Class<?> beanClass = beanDesc.getBeanClass();
if (beanClass.isPrimitive()) {
return serializer;
}
System.out.println("modifySerializer:" + beanDesc.getBeanClass() + ", serializer:"
+ serializer);
return serializer;
}
});
proxyModule.setDeserializerModifier(new BeanDeserializerModifier() {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc,
JsonDeserializer<?> deserializer) {
Class<?> beanClass = beanDesc.getBeanClass();
if (beanClass.isPrimitive()) {
return deserializer;
}
System.out.println("modifyDeserializer:" + beanDesc.getBeanClass() + ", deserializer:"
+ deserializer);
return deserializer;
}
});
cacheObjectMapper.registerModule(proxyModule);
// 创建Redis缓存配置使用包含Hibernate代理处理的ObjectMapper副本
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(6)) // 设置缓存过期时间为6小时
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer(cacheObjectMapper)))
.disableCachingNullValues(); // 不缓存null值
// 创建Redis缓存管理器为所有缓存名称配置相同的序列化策略以确保类型安全反序列化
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfiguration)
.build();
}
}

View File

@@ -130,7 +130,7 @@ public class SecurityConfig {
// 注意根据系统设计Employee实体中没有密码字段系统使用IP/MAC绑定认证 // 注意根据系统设计Employee实体中没有密码字段系统使用IP/MAC绑定认证
// 这里使用密码编码器加密后的固定密码,确保认证流程能够正常工作 // 这里使用密码编码器加密后的固定密码,确保认证流程能够正常工作
return User.builder() return User.builder()
.username(employee.getName()) .username(employee.getAccount())
.password(passwordEncoder().encode("default123")) // 使用默认密码进行加密 .password(passwordEncoder().encode("default123")) // 使用默认密码进行加密
.accountExpired(false) // 账户未过期 .accountExpired(false) // 账户未过期
.accountLocked(false) // 账户未锁定 .accountLocked(false) // 账户未锁定

View File

@@ -1,9 +1,17 @@
package com.ecep.contract.controller; package com.ecep.contract.controller;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.other.service.EmployeeService;
import com.ecep.contract.model.Employee;
/** /**
* 基础控制器,处理根路径请求 * 基础控制器,处理根路径请求
*/ */
@@ -15,7 +23,22 @@ public class IndexController {
*/ */
@GetMapping("/") @GetMapping("/")
public ResponseEntity<?> index() { public ResponseEntity<?> index() {
return ResponseEntity.ok("合同管理系统 REST API 服务已启动"); SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails userDetails) {
System.out.println(userDetails.getUsername());
EmployeeService employeeService = SpringApp.getBean(EmployeeService.class);
try {
Employee employee = employeeService.findByAccount(userDetails.getUsername());
return ResponseEntity.ok(employee);
} catch (Exception e) {
e.printStackTrace();
}
}
return ResponseEntity.ok(authentication);
} }
} }

View File

@@ -0,0 +1,37 @@
package com.ecep.contract.ds.company;
import com.ecep.contract.ds.company.service.*;
import org.springframework.beans.BeansException;
public interface CompanyContext {
<T> T getBean(Class<T> requiredType) throws BeansException;
default CompanyService getCompanyService() {
return getBean(CompanyService.class);
}
default CompanyOldNameService getCompanyOldNameService() {
return getBean(CompanyOldNameService.class);
}
default CompanyFileTypeService getCompanyFileTypeService() {
return getBean(CompanyFileTypeService.class);
}
default CompanyFileService getCompanyFileService() {
return getBean(CompanyFileService.class);
}
default CompanyBankAccountService getCompanyBankAccountService() {
return getBean(CompanyBankAccountService.class);
}
default CompanyBlackReasonService getCompanyBlackReasonService() {
return getBean(CompanyBlackReasonService.class);
}
default CompanyInvoiceInfoService getCompanyInvoiceInfoService() {
return getBean(CompanyInvoiceInfoService.class);
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.util.StringUtils;
import com.ecep.contract.CompanyFileType; import com.ecep.contract.CompanyFileType;
import com.ecep.contract.IEntityService; import com.ecep.contract.IEntityService;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.MyDateTimeUtils; import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.QueryService; import com.ecep.contract.QueryService;
import com.ecep.contract.SpringApp; import com.ecep.contract.SpringApp;
@@ -148,10 +149,11 @@ public class CompanyFileService
* @see CompanyFile * @see CompanyFile
* @see CompanyFileType * @see CompanyFileType
*/ */
public void verify(Company company, LocalDate verifyDate, Consumer<String> status) { public boolean verify(Company company, LocalDate verifyDate, MessageHolder holder) {
if (verifyDate.isBefore(LocalDate.of(2023, 1, 1))) { if (verifyDate.isBefore(LocalDate.of(2023, 1, 1))) {
// 不检查2023-01-01之前的资信评估报告 // 不检查2023-01-01之前的资信评估报告
return; holder.warn(company.getName() + " 2023-01-01 前不检查资信评估报告");
return true;
} }
// 查询公司的资信评估报告 // 查询公司的资信评估报告
@@ -160,30 +162,33 @@ public class CompanyFileService
.filter(v -> v.getApplyDate() != null && v.getExpiringDate() != null) .filter(v -> v.getApplyDate() != null && v.getExpiringDate() != null)
.filter(v -> MyDateTimeUtils.dateValidFilter(verifyDate, v.getApplyDate(), v.getExpiringDate(), 30)) .filter(v -> MyDateTimeUtils.dateValidFilter(verifyDate, v.getApplyDate(), v.getExpiringDate(), 30))
.findFirst().orElse(null); .findFirst().orElse(null);
if (companyFile == null) { if (companyFile != null) {
List<LocalDate> dates = new ArrayList<>(); holder.info("" + verifyDate + " 找到资信评估报告 " + companyFile.getFilePath());
return true;
files.stream()
.filter(v -> v.getApplyDate() != null && !verifyDate.isBefore(v.getApplyDate()))
.max(Comparator.comparing(CompanyFile::getApplyDate))
.map(CompanyFile::getApplyDate)
.ifPresent(dates::add);
files.stream()
.filter(v -> v.getExpiringDate() != null && !verifyDate.isAfter(v.getExpiringDate()))
.min(Comparator.comparing(CompanyFile::getApplyDate))
.map(CompanyFile::getApplyDate)
.ifPresent(dates::add);
if (dates.isEmpty()) {
status.accept("未匹配到资信评估报告");
} else if (dates.size() == 1) {
status.accept("未匹配到资信评估报告, 最接近日期:" + dates.getFirst());
} else {
LocalDate localDate = dates.stream().max(LocalDate::compareTo).orElse(null);
status.accept("未匹配到资信评估报告, 最接近日期:" + localDate);
}
} }
List<LocalDate> dates = new ArrayList<>();
files.stream()
.filter(v -> v.getApplyDate() != null && !verifyDate.isBefore(v.getApplyDate()))
.max(Comparator.comparing(CompanyFile::getApplyDate))
.map(CompanyFile::getApplyDate)
.ifPresent(dates::add);
files.stream()
.filter(v -> v.getExpiringDate() != null && !verifyDate.isAfter(v.getExpiringDate()))
.min(Comparator.comparing(CompanyFile::getApplyDate))
.map(CompanyFile::getApplyDate)
.ifPresent(dates::add);
if (dates.isEmpty()) {
holder.error("" + verifyDate + " 未找到资信评估报告");
} else if (dates.size() == 1) {
holder.error("未匹配到资信评估报告, 最接近日期:" + dates.getFirst());
} else {
LocalDate localDate = dates.stream().max(LocalDate::compareTo).orElse(null);
holder.error("未匹配到资信评估报告, 最接近日期:" + localDate);
}
return false;
} }
/** /**

View File

@@ -1,9 +1,6 @@
package com.ecep.contract.ds.company.service; package com.ecep.contract.ds.company.service;
import com.ecep.contract.EntityService; import com.ecep.contract.*;
import com.ecep.contract.IEntityService;
import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.QueryService;
import com.ecep.contract.cloud.rk.CloudRkService; import com.ecep.contract.cloud.rk.CloudRkService;
import com.ecep.contract.cloud.tyc.CloudTycService; import com.ecep.contract.cloud.tyc.CloudTycService;
import com.ecep.contract.cloud.u8.YongYouU8Service; import com.ecep.contract.cloud.u8.YongYouU8Service;
@@ -420,25 +417,26 @@ public class CompanyService extends EntityService<Company, Integer>
* @param verifyDate 验证日期 * @param verifyDate 验证日期
* @param status 状态输出 * @param status 状态输出
*/ */
public void verifyEnterpriseStatus(Company company, LocalDate verifyDate, Consumer<String> status) { public boolean verifyEnterpriseStatus(Company company, LocalDate verifyDate, MessageHolder holder) {
// 检查营业状态 // 检查营业状态
String entStatus = company.getEntStatus(); String entStatus = company.getEntStatus();
if (StringUtils.hasText(entStatus)) { if (!StringUtils.hasText(entStatus)) {
if (entStatus.contains("注销")) { holder.warn("营业状态异常:未设置");
LocalDate end = company.getOperationPeriodEnd(); return false;
LocalDate begin = company.getOperationPeriodBegin(); }
if (begin == null || end == null) { if (entStatus.contains("注销")) {
// 注销时间未知,无法判断是否在 verifyDate 之后注销 LocalDate end = company.getOperationPeriodEnd();
status.accept("营业状态异常:" + entStatus); LocalDate begin = company.getOperationPeriodBegin();
} else { if (begin == null || end == null) {
if (!MyDateTimeUtils.dateValidFilter(verifyDate, begin, end, 0)) { // 注销时间未知,无法判断是否在 verifyDate 之后注销
status.accept("营业状态异常:" + entStatus); holder.error("营业状态异常:" + entStatus);
} } else {
if (!MyDateTimeUtils.dateValidFilter(verifyDate, begin, end, 0)) {
holder.error("营业状态异常:" + entStatus);
} }
} }
} else {
status.accept("营业状态异常:未设置");
} }
return true;
} }
@Override @Override
@@ -495,7 +493,7 @@ public class CompanyService extends EntityService<Company, Integer>
} }
public Predicate buildSearchPredicate(String searchText, Path<Company> root, CriteriaQuery<?> query, public Predicate buildSearchPredicate(String searchText, Path<Company> root, CriteriaQuery<?> query,
CriteriaBuilder builder) { CriteriaBuilder builder) {
return builder.or( return builder.or(
builder.like(root.get("name"), "%" + searchText + "%"), builder.like(root.get("name"), "%" + searchText + "%"),
builder.like(root.get("shortName"), "%" + searchText + "%"), builder.like(root.get("shortName"), "%" + searchText + "%"),

View File

@@ -83,7 +83,7 @@ public class ContractFileTypeService
} }
@Caching(evict = { @Caching(evict = {
@CacheEvict(key = "#p0.id"), @CacheEvict(key = "#p0.id"), @CacheEvict(key = "'by-type-'+#p0.type.name()+'-'+#p0.lang"),
@CacheEvict(key = "'all-'+#p0.getLang()") @CacheEvict(key = "'all-'+#p0.getLang()")
}) })
@Override @Override
@@ -93,6 +93,7 @@ public class ContractFileTypeService
@Caching(evict = { @Caching(evict = {
@CacheEvict(key = "#p0.id"), @CacheEvict(key = "#p0.id"),
@CacheEvict(key = "'by-type-'+#p0.type.name()+'-'+#p0.lang"),
@CacheEvict(key = "'all-'+#p0.getLang()") @CacheEvict(key = "'all-'+#p0.getLang()")
}) })
@Override @Override
@@ -117,4 +118,8 @@ public class ContractFileTypeService
model.setSuggestFileName(vo.getSuggestFileName()); model.setSuggestFileName(vo.getSuggestFileName());
} }
@Cacheable(key = "'by-type-'+#p0.name()+'-'+#p1.toLanguageTag()")
public ContractFileTypeLocal findByTypeAndLang(ContractFileType type, Locale locale) {
return repository.findByTypeAndLang(type, locale.toLanguageTag());
}
} }

View File

@@ -189,8 +189,9 @@ public class ContractService extends EntityService<Contract, Integer>
Matcher matcher = pattern.matcher(contractCode); Matcher matcher = pattern.matcher(contractCode);
if (matcher.find()) { if (matcher.find()) {
catalogCode = matcher.group(1); catalogCode = matcher.group(1);
System.out.println("字母部分: " + matcher.group(1)); if (logger.isDebugEnabled()) {
System.out.println("数字部分: " + matcher.group(2)); logger.debug("{} -> 字母:{}, 数字:{}", contractCode, matcher.group(1), matcher.group(2));
}
} }
if (catalogCode == null) { if (catalogCode == null) {
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {

View File

@@ -9,7 +9,6 @@ import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.DoubleSummaryStatistics; import java.util.DoubleSummaryStatistics;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -18,35 +17,24 @@ import org.hibernate.Hibernate;
import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.User;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.ecep.contract.CustomerFileType;
import com.ecep.contract.ContractFileType; import com.ecep.contract.ContractFileType;
import com.ecep.contract.ContractPayWay; import com.ecep.contract.ContractPayWay;
import com.ecep.contract.CustomerFileType;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.MyDateTimeUtils; import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.SpringApp; import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.company.CompanyFileUtils; import com.ecep.contract.ds.company.CompanyFileUtils;
import com.ecep.contract.ds.company.service.CompanyExtendInfoService; import com.ecep.contract.ds.company.service.CompanyExtendInfoService;
import com.ecep.contract.ds.company.service.CompanyFileService;
import com.ecep.contract.ds.company.service.CompanyService;
import com.ecep.contract.ds.contract.service.ContractBidVendorService;
import com.ecep.contract.ds.contract.service.ContractFileService;
import com.ecep.contract.ds.contract.service.ContractFileTypeService;
import com.ecep.contract.ds.contract.service.ContractService;
import com.ecep.contract.ds.contract.service.ExtendVendorInfoService; import com.ecep.contract.ds.contract.service.ExtendVendorInfoService;
import com.ecep.contract.ds.converter.NumberStringConverter; import com.ecep.contract.ds.converter.NumberStringConverter;
import com.ecep.contract.ds.customer.service.CompanyCustomerFileService;
import com.ecep.contract.ds.customer.service.CompanyCustomerService;
import com.ecep.contract.ds.other.service.EmployeeService; import com.ecep.contract.ds.other.service.EmployeeService;
import com.ecep.contract.ds.project.ProjectCostImportItemsFromContractsTasker; import com.ecep.contract.service.tasker.ProjectCostImportItemsFromContractsTasker;
import com.ecep.contract.ds.project.service.ProjectBidService; import com.ecep.contract.ds.project.service.ProjectBidService;
import com.ecep.contract.ds.project.service.ProjectCostService; import com.ecep.contract.ds.project.service.ProjectCostService;
import com.ecep.contract.ds.project.service.ProjectQuotationService; import com.ecep.contract.ds.project.service.ProjectQuotationService;
import com.ecep.contract.ds.project.service.ProjectSaleTypeRequireFileTypeService; import com.ecep.contract.ds.project.service.ProjectSaleTypeRequireFileTypeService;
import com.ecep.contract.ds.project.service.ProjectService;
import com.ecep.contract.ds.project.service.ProjectSaleTypeService; import com.ecep.contract.ds.project.service.ProjectSaleTypeService;
import com.ecep.contract.ds.vendor.service.VendorService;
import com.ecep.contract.ds.vendor.service.VendorGroupRequireFileTypeService; import com.ecep.contract.ds.vendor.service.VendorGroupRequireFileTypeService;
import com.ecep.contract.ds.vendor.service.VendorGroupService;
import com.ecep.contract.model.Company; import com.ecep.contract.model.Company;
import com.ecep.contract.model.CompanyCustomer; import com.ecep.contract.model.CompanyCustomer;
import com.ecep.contract.model.CompanyCustomerFile; import com.ecep.contract.model.CompanyCustomerFile;
@@ -66,184 +54,48 @@ import com.ecep.contract.model.ProjectSaleTypeRequireFileType;
import com.ecep.contract.model.VendorGroup; import com.ecep.contract.model.VendorGroup;
import com.ecep.contract.model.VendorGroupRequireFileType; import com.ecep.contract.model.VendorGroupRequireFileType;
import com.ecep.contract.util.SecurityUtils; import com.ecep.contract.util.SecurityUtils;
import com.ecep.contract.util.VerifyContext;
import lombok.Data; import lombok.Data;
@Data @Data
public class ContractVerifyComm { public class ContractVerifyComm extends VerifyContext {
// Project
private ProjectService projectService;
private ProjectSaleTypeRequireFileTypeService saleTypeRequireFileTypeService;
private ProjectSaleTypeService projectSaleTypeService;
private ProjectCostService projectCostService;
private ProjectQuotationService projectQuotationService;
private ProjectBidService projectBidService;
// Contract
private ContractService contractService;
private ContractFileService contractFileService;
private ContractFileTypeService contractFileTypeService;
private ContractBidVendorService contractBidVendorService;
// Company
private CompanyService companyService;
private CompanyFileService companyFileService;
// Vendor
private VendorService vendorService;
private VendorGroupService vendorGroupService;
private VendorGroupRequireFileTypeService vendorGroupRequireFileTypeService;
private ExtendVendorInfoService extendVendorInfoService;
// Customer
private CompanyCustomerService companyCustomerService;
private CompanyCustomerFileService companyCustomerFileService;
private CompanyExtendInfoService companyExtendInfoService;
// Employee
private EmployeeService employeeService;
private ProjectService getProjectService() {
if (projectService == null) {
projectService = SpringApp.getBean(ProjectService.class);
}
return projectService;
}
private ProjectSaleTypeService getProjectSaleTypeService() { private ProjectSaleTypeService getProjectSaleTypeService() {
if (projectSaleTypeService == null) { return getBean(ProjectSaleTypeService.class);
projectSaleTypeService = SpringApp.getBean(ProjectSaleTypeService.class);
}
return projectSaleTypeService;
} }
ProjectCostService getProjectCostService() { ProjectCostService getProjectCostService() {
if (projectCostService == null) { return getBean(ProjectCostService.class);
projectCostService = SpringApp.getBean(ProjectCostService.class);
}
return projectCostService;
} }
ProjectQuotationService getProjectQuotationService() { ProjectQuotationService getProjectQuotationService() {
if (projectQuotationService == null) { return getBean(ProjectQuotationService.class);
projectQuotationService = SpringApp.getBean(ProjectQuotationService.class);
}
return projectQuotationService;
} }
ProjectBidService getProjectBidService() { ProjectBidService getProjectBidService() {
if (projectBidService == null) { return getBean(ProjectBidService.class);
projectBidService = SpringApp.getBean(ProjectBidService.class);
}
return projectBidService;
} }
private ProjectSaleTypeRequireFileTypeService getSaleTypeRequireFileTypeService() { private ProjectSaleTypeRequireFileTypeService getSaleTypeRequireFileTypeService() {
if (saleTypeRequireFileTypeService == null) { return getBean(ProjectSaleTypeRequireFileTypeService.class);
saleTypeRequireFileTypeService = SpringApp.getBean(ProjectSaleTypeRequireFileTypeService.class);
}
return saleTypeRequireFileTypeService;
}
public ContractService getContractService() {
if (contractService == null) {
contractService = SpringApp.getBean(ContractService.class);
}
return contractService;
}
private ContractFileService getContractFileService() {
if (contractFileService == null) {
contractFileService = SpringApp.getBean(ContractFileService.class);
}
return contractFileService;
}
private ContractFileTypeService getContractFileTypeService() {
if (contractFileTypeService == null) {
contractFileTypeService = SpringApp.getBean(ContractFileTypeService.class);
}
return contractFileTypeService;
}
private ContractBidVendorService getContractBidVendorService() {
if (contractBidVendorService == null) {
contractBidVendorService = SpringApp.getBean(ContractBidVendorService.class);
}
return contractBidVendorService;
}
private CompanyService getCompanyService() {
if (companyService == null) {
companyService = SpringApp.getBean(CompanyService.class);
}
return companyService;
}
private CompanyFileService getCompanyFileService() {
if (companyFileService == null) {
companyFileService = SpringApp.getBean(CompanyFileService.class);
}
return companyFileService;
}
private VendorGroupService getVendorGroupService() {
if (vendorGroupService == null) {
vendorGroupService = SpringApp.getBean(VendorGroupService.class);
}
return vendorGroupService;
} }
private VendorGroupRequireFileTypeService getVendorGroupRequireFileTypeService() { private VendorGroupRequireFileTypeService getVendorGroupRequireFileTypeService() {
if (vendorGroupRequireFileTypeService == null) { return getBean(VendorGroupRequireFileTypeService.class);
vendorGroupRequireFileTypeService = SpringApp.getBean(VendorGroupRequireFileTypeService.class);
}
return vendorGroupRequireFileTypeService;
} }
private ExtendVendorInfoService getExtendVendorInfoService() { private ExtendVendorInfoService getExtendVendorInfoService() {
if (extendVendorInfoService == null) { return getBean(ExtendVendorInfoService.class);
extendVendorInfoService = SpringApp.getBean(ExtendVendorInfoService.class);
}
return extendVendorInfoService;
}
private VendorService getVendorService() {
if (vendorService == null) {
vendorService = SpringApp.getBean(VendorService.class);
}
return vendorService;
}
private CompanyCustomerService getCompanyCustomerService() {
if (companyCustomerService == null) {
companyCustomerService = SpringApp.getBean(CompanyCustomerService.class);
}
return companyCustomerService;
}
private CompanyCustomerFileService getCompanyCustomerFileService() {
if (companyCustomerFileService == null) {
companyCustomerFileService = SpringApp.getBean(CompanyCustomerFileService.class);
}
return companyCustomerFileService;
} }
private CompanyExtendInfoService getCompanyExtendInfoService() { private CompanyExtendInfoService getCompanyExtendInfoService() {
if (companyExtendInfoService == null) { return getBean(CompanyExtendInfoService.class);
companyExtendInfoService = SpringApp.getBean(CompanyExtendInfoService.class);
}
return companyExtendInfoService;
}
private EmployeeService getEmployeeService() {
if (employeeService == null) {
employeeService = SpringApp.getBean(EmployeeService.class);
}
return employeeService;
} }
/** /**
* *
*/ */
private Map<ContractFileType, ContractFileTypeLocal> fileTypeLocalMap = null; private Map<ContractFileType, ContractFileTypeLocal> fileTypeLocalMap = null;
private Locale locale = Locale.getDefault();
private Contract contract; private Contract contract;
/** /**
@@ -285,70 +137,98 @@ public class ContractVerifyComm {
* @param contract 要验证的合同对象 * @param contract 要验证的合同对象
* @param holder 输出 * @param holder 输出
*/ */
public void verify(Contract contract, MessageHolder holder) { public boolean verify(Contract contract, MessageHolder holder) {
boolean passed = true;
LocalDate setupDate = contract.getSetupDate(); LocalDate setupDate = contract.getSetupDate();
if (setupDate == null) { if (setupDate == null) {
holder.error("未设置合同提交日期"); holder.error("未设置合同提交日期");
return; passed = false;
} }
Company company = contract.getCompany(); Company company = contract.getCompany();
if (company == null) { if (company == null) {
holder.error("未关联企业"); holder.error("未关联企业");
return; return false;
} }
if (!Hibernate.isInitialized(company)) { if (!Hibernate.isInitialized(company)) {
company = getCompanyService().findById(company.getId()); company = getCompanyService().findById(company.getId());
} }
verify(company, contract, holder); if (!verify(company, contract, holder)) {
passed = false;
}
return passed;
} }
public void verify(Company company, Contract contract, MessageHolder holder) { public boolean verify(Company company, LocalDate verifyDate, MessageHolder holder) {
CompanyExtendInfo companyExtendInfo = getCompanyExtendInfoService().findByCompany(company);
if (companyExtendInfo.isDisableVerify()) {
holder.debug("公司设定不做校验");
return true;
}
boolean passed = true;
MessageHolder subHolder = holder.sub(company.getName() + " -> ");
if (verifyCompanyPath) {
if (!CompanyFileUtils.exists(company.getPath())) {
subHolder.error("公司目录未设置");
passed = false;
} else {
File basePath = getCompanyService().getBasePath();
if (!company.getPath().startsWith(basePath.getAbsolutePath())) {
subHolder.warn("公司目录未在规定目录下");
}
}
}
if (verifyCompanyStatus) {
if (!getCompanyService().verifyEnterpriseStatus(company, verifyDate, subHolder)) {
passed = false;
}
}
if (verifyCompanyCredit) {
if (!getCompanyFileService().verify(company, verifyDate, subHolder)) {
passed = false;
}
}
return passed;
}
/**
* 验证合同是否合规
*
* @param company
* @param contract
* @param holder
* @return
*/
public boolean verify(Company company, Contract contract, MessageHolder holder) {
boolean passed = true;
LocalDate setupDate = contract.getSetupDate(); LocalDate setupDate = contract.getSetupDate();
Employee employee = contract.getEmployee(); Employee employee = contract.getEmployee();
if (employee == null) { if (employee == null) {
holder.error("未关联业务员"); holder.error("未关联业务员");
passed = false;
} }
if (contract.getAmount() == null || contract.getAmount() <= 0) { if (contract.getAmount() == null || contract.getAmount() <= 0) {
holder.error("合同金额小于等于0"); holder.error("合同金额小于等于0");
passed = false;
} }
CompanyExtendInfo companyExtendInfo = getCompanyExtendInfoService().findByCompany(company); if (!verify(company, setupDate, holder)) {
passed = false;
if (companyExtendInfo.isDisableVerify()) {
holder.debug("公司设定不做校验");
} else {
if (verifyCompanyPath) {
if (!CompanyFileUtils.exists(company.getPath())) {
holder.error("公司目录未设置");
} else {
File basePath = getCompanyService().getBasePath();
if (!company.getPath().startsWith(basePath.getAbsolutePath())) {
holder.error("公司目录未在规定目录下");
}
}
}
if (verifyCompanyStatus) {
getCompanyService().verifyEnterpriseStatus(company, setupDate, msg -> {
holder.error(company.getName() + ":" + msg);
});
}
if (verifyCompanyCredit) {
getCompanyFileService().verify(company, contract.getSetupDate(), msg -> {
holder.error(company.getName() + ":" + msg);
});
}
} }
// 合同类型 // 合同类型
switch (contract.getPayWay()) { switch (contract.getPayWay()) {
case RECEIVE -> { case RECEIVE -> {
// 销售合同 // 销售合同
verifyAsCustomer(company, companyExtendInfo, contract, holder); CompanyExtendInfo companyExtendInfo = getCompanyExtendInfoService().findByCompany(company);
if (!verifyAsCustomer(company, companyExtendInfo, contract, holder)) {
passed = false;
}
// 销售合同下的采购合同 // 销售合同下的采购合同
List<Contract> list = getContractService().findAllByParent(contract); List<Contract> list = getContractService().findAllByParent(contract);
@@ -356,20 +236,24 @@ public class ContractVerifyComm {
for (Contract v : list) { for (Contract v : list) {
MessageHolder subHolder = holder.sub(v.getCode() + " -> "); MessageHolder subHolder = holder.sub(v.getCode() + " -> ");
subHolder.info("采购合同 " + v.getName()); subHolder.info("采购合同 " + v.getName());
verify(v, subHolder); if (!verify(v, subHolder)) {
passed = false;
}
if (CompanyFileUtils.exists(v.getPath())) { if (CompanyFileUtils.exists(v.getPath())) {
if (!v.getPath().startsWith(contract.getPath())) { if (!v.getPath().startsWith(contract.getPath())) {
holder.error("合同目录未在规定目录下"); holder.error("合同目录未在规定目录下");
passed = false;
} }
} else { } else {
holder.error("合同目录未设置"); holder.error("合同目录未设置");
passed = false;
} }
} }
DoubleSummaryStatistics statistics = list.stream().mapToDouble(c -> { DoubleSummaryStatistics statistics = list.stream().mapToDouble(c -> {
return Objects.requireNonNullElse(c.getAmount(), 0.0); return Objects.requireNonNullElse(c.getAmount(), 0.0);
}).summaryStatistics(); }).summaryStatistics();
NumberFormat numberFormat = NumberFormat.getCurrencyInstance(locale); NumberFormat numberFormat = NumberFormat.getCurrencyInstance(getLocale());
holder.debug("采购合同金额合计:" + numberFormat.format(statistics.getSum())); holder.debug("采购合同金额合计:" + numberFormat.format(statistics.getSum()));
holder.debug("采购合同金额平均值:" + numberFormat.format(statistics.getAverage())); holder.debug("采购合同金额平均值:" + numberFormat.format(statistics.getAverage()));
@@ -394,6 +278,7 @@ public class ContractVerifyComm {
holder.error("合同付款类型:未设置"); holder.error("合同付款类型:未设置");
} }
} }
return passed;
} }
private void verifyAsVendor(Company company, Contract contract, MessageHolder holder) { private void verifyAsVendor(Company company, Contract contract, MessageHolder holder) {
@@ -441,7 +326,7 @@ public class ContractVerifyComm {
} }
private void verifyVendorFile(VendorGroup group, boolean assignedProvider, Contract contract, private void verifyVendorFile(VendorGroup group, boolean assignedProvider, Contract contract,
MessageHolder holder) { MessageHolder holder) {
if (group == null) { if (group == null) {
return; return;
} }
@@ -516,7 +401,7 @@ public class ContractVerifyComm {
} }
String getFileTypeLocalValue(ContractFileType type) { String getFileTypeLocalValue(ContractFileType type) {
ContractFileTypeLocal fileTypeLocal = getFileTypeLocal(type); ContractFileTypeLocal fileTypeLocal = getContractFileTypeService().findByTypeAndLang(type, getLocale());
if (fileTypeLocal == null) { if (fileTypeLocal == null) {
return type.name(); return type.name();
} }
@@ -535,19 +420,21 @@ public class ContractVerifyComm {
return false; return false;
} }
private void verifyAsCustomer(Company company, CompanyExtendInfo companyExtendInfo, Contract contract, private boolean verifyAsCustomer(Company company, CompanyExtendInfo companyExtendInfo, Contract contract,
MessageHolder holder) { MessageHolder holder) {
boolean valiad = true; boolean passed = true;
Project project = contract.getProject(); Project project = contract.getProject();
if (project == null) { if (project == null) {
// 收款的合同时,检查是否关联了项目,如果没有则创建 // 收款的合同时,检查是否关联了项目,如果没有则创建
if (contract.getPayWay() == ContractPayWay.RECEIVE) { if (contract.getPayWay() == ContractPayWay.RECEIVE) {
holder.debug("未关联项目,测试关联/创建项目...");
project = getProjectService().findByCode(contract.getCode()); project = getProjectService().findByCode(contract.getCode());
if (project == null) { if (project == null) {
holder.info("创建关联项目"); holder.info("根据合同号 " + contract.getCode() + ", 未找到相关项目, 创建相关项目");
try { try {
project = getProjectService().newInstanceByContract(contract); project = getProjectService().newInstanceByContract(contract);
project = getProjectService().save(project); project = getProjectService().save(project);
holder.info("创建关联项目成功:" + project.getCode() + " " + project.getName());
} catch (Exception e) { } catch (Exception e) {
holder.error("创建关联项目失败: " + e.getMessage()); holder.error("创建关联项目失败: " + e.getMessage());
throw new RuntimeException("code=" + contract.getCode(), e); throw new RuntimeException("code=" + contract.getCode(), e);
@@ -555,20 +442,26 @@ public class ContractVerifyComm {
} }
contract.setProject(project); contract.setProject(project);
contract = getContractService().save(contract); contract = getContractService().save(contract);
holder.info("关联项目:" + project.getCode() + " " + project.getName());
} else {
holder.warn("未关联项目");
} }
} }
// fixed no hibernate session
if (project != null) { if (project != null) {
if (!Hibernate.isInitialized(project)) { if (!Hibernate.isInitialized(project)) {
project = getProjectService().findById(project.getId()); project = getProjectService().findById(project.getId());
// fixed // fixed
contract.setProject(project); contract.setProject(project);
} }
} }
if (project != null) { if (project != null) {
holder.info("验证项目信息:" + project.getCode() + " " + project.getName()); holder.info("验证项目:" + project.getCode() + " " + project.getName());
verifyProject(contract, project, holder.sub("项目")); if (!verifyProject(contract, project, holder.sub("项目"))) {
passed = false;
}
ProjectSaleType saleType = project.getSaleType(); ProjectSaleType saleType = project.getSaleType();
if (saleType != null) { if (saleType != null) {
@@ -579,6 +472,7 @@ public class ContractVerifyComm {
} }
if (!contract.getPath().startsWith(saleType.getPath())) { if (!contract.getPath().startsWith(saleType.getPath())) {
holder.error("合同目录未在销售类型目录下"); holder.error("合同目录未在销售类型目录下");
passed = false;
} }
} }
} }
@@ -586,16 +480,17 @@ public class ContractVerifyComm {
if (!companyExtendInfo.isDisableVerify()) { if (!companyExtendInfo.isDisableVerify()) {
if (!verifyCustomerContract(contract, holder)) { if (!verifyCustomerContract(contract, holder)) {
valiad = false; passed = false;
} }
if (verifyCustomerFiles) { if (verifyCustomerFiles) {
holder.debug("核验文件..."); holder.info("核验客户文件...");
if (!verifyContractFileAsCustomer(project, contract, holder)) { if (!verifyContractFileAsCustomer(project, contract, holder)) {
valiad = false; passed = false;
} }
} }
} }
return passed;
} }
/** /**
@@ -634,7 +529,7 @@ public class ContractVerifyComm {
} }
private boolean verifyCustomerFileByContract(CompanyCustomer companyCustomer, Contract contract, private boolean verifyCustomerFileByContract(CompanyCustomer companyCustomer, Contract contract,
MessageHolder holder) { MessageHolder holder) {
List<LocalDate> verifyDates = new ArrayList<>(); List<LocalDate> verifyDates = new ArrayList<>();
LocalDate minDate = LocalDate.of(2022, 1, 1); LocalDate minDate = LocalDate.of(2022, 1, 1);
LocalDate developDate = companyCustomer.getDevelopDate(); LocalDate developDate = companyCustomer.getDevelopDate();
@@ -720,7 +615,11 @@ public class ContractVerifyComm {
return false; return false;
} }
private void verifyProject(Contract contract, Project project, MessageHolder holder) { /**
* 核查合同对应的项目是否合规
*/
private boolean verifyProject(Contract contract, Project project, MessageHolder holder) {
boolean passed = true;
ProjectSaleType saleType = project.getSaleType(); ProjectSaleType saleType = project.getSaleType();
if (saleType == null) { if (saleType == null) {
String code = contract.getCode(); String code = contract.getCode();
@@ -734,6 +633,7 @@ public class ContractVerifyComm {
} }
if (project.getAmount() == null || project.getAmount() <= 0) { if (project.getAmount() == null || project.getAmount() <= 0) {
holder.error("金额小于等于0"); holder.error("金额小于等于0");
passed = false;
} }
// //
@@ -824,6 +724,7 @@ public class ContractVerifyComm {
holder.warn("报价未创建"); holder.warn("报价未创建");
} }
} }
return passed;
} }
private boolean verifyContractFileAsCustomer(Project project, Contract contract, MessageHolder holder) { private boolean verifyContractFileAsCustomer(Project project, Contract contract, MessageHolder holder) {

View File

@@ -188,7 +188,7 @@ public class CompanyCustomerService extends CompanyBasicService
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
protected <T, F extends CompanyBasicFile<T>> boolean fillFileAsDefaultType(F dbFile, File file, protected <T, F extends CompanyBasicFile<T>> boolean fillFileAsDefaultType(F dbFile, File file,
Consumer<String> status) { Consumer<String> status) {
dbFile.setType((T) CustomerFileType.General); dbFile.setType((T) CustomerFileType.General);
fillFile(dbFile, file, null, status); fillFile(dbFile, file, null, status);
companyCustomerFileService.save((CompanyCustomerFile) dbFile); companyCustomerFileService.save((CompanyCustomerFile) dbFile);
@@ -197,7 +197,7 @@ public class CompanyCustomerService extends CompanyBasicService
@Override @Override
protected <T, F extends CompanyBasicFile<T>> boolean fillFileAsEvaluationFile(F customerFile, File file, protected <T, F extends CompanyBasicFile<T>> boolean fillFileAsEvaluationFile(F customerFile, File file,
List<File> fileList, Consumer<String> status) { List<File> fileList, Consumer<String> status) {
boolean modified = super.fillFileAsEvaluationFile(customerFile, file, fileList, status); boolean modified = super.fillFileAsEvaluationFile(customerFile, file, fileList, status);
if (fileList != null) { if (fileList != null) {
@@ -231,7 +231,7 @@ public class CompanyCustomerService extends CompanyBasicService
} }
private <T, F extends CompanyBasicFile<T>> void updateEvaluationFileByJsonFile(F customerFile, File jsonFile, private <T, F extends CompanyBasicFile<T>> void updateEvaluationFileByJsonFile(F customerFile, File jsonFile,
Consumer<String> status) throws IOException { Consumer<String> status) throws IOException {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
JsonNode root = objectMapper.readTree(jsonFile); JsonNode root = objectMapper.readTree(jsonFile);
if (!root.isObject()) { if (!root.isObject()) {
@@ -257,7 +257,7 @@ public class CompanyCustomerService extends CompanyBasicService
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
protected <T, F extends CompanyBasicFile<T>> F fillFileType(File file, List<File> fileList, protected <T, F extends CompanyBasicFile<T>> F fillFileType(File file, List<File> fileList,
Consumer<String> status) { Consumer<String> status) {
CompanyCustomerFile customerFile = new CompanyCustomerFile(); CompanyCustomerFile customerFile = new CompanyCustomerFile();
customerFile.setType(CustomerFileType.General); customerFile.setType(CustomerFileType.General);
if (fillFile(customerFile, file, fileList, status)) { if (fillFile(customerFile, file, fileList, status)) {
@@ -281,7 +281,7 @@ public class CompanyCustomerService extends CompanyBasicService
return (fileName.contains(CompanyCustomerConstant.EVALUATION_FORM_NAME1) return (fileName.contains(CompanyCustomerConstant.EVALUATION_FORM_NAME1)
|| fileName.contains(CompanyCustomerConstant.EVALUATION_FORM_NAME2)) || fileName.contains(CompanyCustomerConstant.EVALUATION_FORM_NAME2))
&& (FileUtils.withExtensions(fileName, FileUtils.JPG, FileUtils.JPEG, && (FileUtils.withExtensions(fileName, FileUtils.JPG, FileUtils.JPEG,
FileUtils.PDF)); FileUtils.PDF));
} }
public boolean makePathAbsent(CompanyCustomer companyCustomer) { public boolean makePathAbsent(CompanyCustomer companyCustomer) {
@@ -395,11 +395,28 @@ public class CompanyCustomerService extends CompanyBasicService
@Override @Override
public void updateByVo(CompanyCustomer customer, CompanyCustomerVo vo) { public void updateByVo(CompanyCustomer customer, CompanyCustomerVo vo) {
customer.setCompany(SpringApp.getBean(CompanyService.class).findById(vo.getCompanyId())); if (vo.getCompanyId() == null) {
customer.setCatalog(SpringApp.getBean(CustomerCatalogService.class).findById(vo.getCatalogId())); customer.setCompany(null);
} else {
CompanyService service = SpringApp.getBean(CompanyService.class);
customer.setCompany(service.findById(vo.getCompanyId()));
}
if (vo.getCatalogId() == null) {
customer.setCatalog(null);
} else {
CustomerCatalogService service = SpringApp.getBean(CustomerCatalogService.class);
customer.setCatalog(service.findById(vo.getCatalogId()));
}
customer.setDevelopDate(vo.getDevelopDate()); customer.setDevelopDate(vo.getDevelopDate());
customer.setPath(vo.getPath()); customer.setPath(vo.getPath());
customer.setContact(SpringApp.getBean(CompanyContactService.class).findById(vo.getContactId()));
if (vo.getContactId() == null) {
customer.setContact(null);
} else {
CompanyContactService service = SpringApp.getBean(CompanyContactService.class);
customer.setContact(service.findById(vo.getContactId()));
}
customer.setDescription(vo.getDescription()); customer.setDescription(vo.getDescription());
customer.setCreated(vo.getCreated()); customer.setCreated(vo.getCreated());

View File

@@ -16,7 +16,7 @@ public interface BaseEnumEntityRepository<N extends Enum<?>, T extends BaseEnumE
List<T> findAllByLang(String lang); List<T> findAllByLang(String lang);
default Map<N, T> getCompleteMapByLocal(String lang) { default Map<N, T> getCompleteMapByLocal(String lang) {
HashMap<N, T> map = new HashMap<>(); Map<N, T> map = new HashMap<>();
for (T t : findAllByLang(lang)) { for (T t : findAllByLang(lang)) {
map.put(t.getType(), t); map.put(t.getType(), t);
} }

View File

@@ -18,7 +18,6 @@ import com.ecep.contract.IEntityService;
import com.ecep.contract.QueryService; import com.ecep.contract.QueryService;
import com.ecep.contract.ds.other.repository.DepartmentRepository; import com.ecep.contract.ds.other.repository.DepartmentRepository;
import com.ecep.contract.model.Department; import com.ecep.contract.model.Department;
import com.ecep.contract.model.Employee;
import com.ecep.contract.service.VoableService; import com.ecep.contract.service.VoableService;
import com.ecep.contract.vo.DepartmentVo; import com.ecep.contract.vo.DepartmentVo;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;

View File

@@ -0,0 +1,62 @@
package com.ecep.contract.handler;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import org.springframework.web.socket.PingMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import com.ecep.contract.constant.WebSocketConstant;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
@Data
public class SessionInfo {
private Integer employeeId;
private Integer loginHistoryId;
private WebSocketSession session;
private ObjectMapper objectMapper;
private ScheduledFuture<?> pingPongScheduledFuture;
public void click() {
try {
session.sendMessage(new PingMessage(ByteBuffer.wrap("ping".getBytes())));
} catch (IOException e) {
WebSocketServerHandler.logger.error("发送ping消息失败 (会话ID: " + session.getId() + "): " + e.getMessage(), e);
}
}
public void sendError(int errorCode, String errorMessage) throws JsonProcessingException, IOException {
Map<String, Object> data = Map.of(
WebSocketConstant.ERROR_CODE_FIELD_NAME, errorCode,
WebSocketConstant.SUCCESS_FIELD_NAME, false,
WebSocketConstant.MESSAGE_FIELD_NAME, errorMessage);
send(objectMapper.writeValueAsString(data));
}
public void sendError(String extendField, String extendValue, int errorCode, String errorMessage)
throws JsonProcessingException, IOException {
Map<String, Object> data = Map.of(
extendField, extendValue,
WebSocketConstant.ERROR_CODE_FIELD_NAME, errorCode,
WebSocketConstant.SUCCESS_FIELD_NAME, false,
WebSocketConstant.MESSAGE_FIELD_NAME, errorMessage);
send(data);
}
public void send(Map<String, Object> data) throws JsonProcessingException, IOException {
send(objectMapper.writeValueAsString(data));
}
public void send(String text) throws IOException {
if (session == null || !session.isOpen()) {
throw new IOException("会话已关闭,无法发送消息");
}
session.sendMessage(new TextMessage(text));
}
}

View File

@@ -1,16 +1,10 @@
package com.ecep.contract.handler; package com.ecep.contract.handler;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -18,45 +12,34 @@ import java.util.concurrent.TimeUnit;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.socket.BinaryMessage; import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PingMessage;
import org.springframework.web.socket.PongMessage; import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.ecep.contract.IEntityService;
import com.ecep.contract.PageArgument;
import com.ecep.contract.PageContent;
import com.ecep.contract.QueryService;
import com.ecep.contract.SpringApp;
import com.ecep.contract.constant.WebSocketConstant; import com.ecep.contract.constant.WebSocketConstant;
import com.ecep.contract.ds.other.service.EmployeeLoginHistoryService; import com.ecep.contract.ds.other.service.EmployeeLoginHistoryService;
import com.ecep.contract.ds.other.service.EmployeeService; import com.ecep.contract.ds.other.service.EmployeeService;
import com.ecep.contract.model.Employee; import com.ecep.contract.model.Employee;
import com.ecep.contract.model.EmployeeLoginHistory; import com.ecep.contract.model.EmployeeLoginHistory;
import com.ecep.contract.model.Voable; import com.ecep.contract.service.WebSocketServerCallbackManager;
import com.ecep.contract.service.VoableService; import com.ecep.contract.service.tasker.WebSocketServerTaskManager;
import com.ecep.contract.service.WebSocketServerTaskManager;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
/** /**
* WebSocket处理器 * WebSocket处理器
* 处理与客户端的WebSocket连接、消息传递和断开连接 * 处理与客户端的WebSocket连接、消息传递和断开连接
*/ */
@Component @Component
public class WebSocketServerHandler extends TextWebSocketHandler { public class WebSocketServerHandler extends TextWebSocketHandler {
private static final Logger logger = LoggerFactory.getLogger(WebSocketServerHandler.class); static final Logger logger = LoggerFactory.getLogger(WebSocketServerHandler.class);
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@@ -69,26 +52,12 @@ public class WebSocketServerHandler extends TextWebSocketHandler {
private ScheduledExecutorService scheduledExecutorService; private ScheduledExecutorService scheduledExecutorService;
@Autowired @Autowired
private WebSocketServerTaskManager taskManager; private WebSocketServerTaskManager taskManager;
@Autowired
private WebSocketServerCallbackManager callbackManager;
// 存储所有活跃的WebSocket会话 // 存储所有活跃的WebSocket会话
private final Map<Integer, SessionInfo> activeSessions = Collections.synchronizedMap(new HashMap<>()); private final Map<Integer, SessionInfo> activeSessions = Collections.synchronizedMap(new HashMap<>());
@Data
static class SessionInfo {
private Integer employeeId;
private Integer loginHistoryId;
private WebSocketSession session;
private ScheduledFuture<?> pingPongScheduledFuture;
void click() {
try {
session.sendMessage(new PingMessage(ByteBuffer.wrap("ping".getBytes())));
} catch (IOException e) {
logger.error("发送ping消息失败 (会话ID: " + session.getId() + "): " + e.getMessage(), e);
}
}
}
WebSocketServerHandler(ObjectMapper objectMapper, AuthenticationManager authenticationManager) { WebSocketServerHandler(ObjectMapper objectMapper, AuthenticationManager authenticationManager) {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.authenticationManager = authenticationManager; this.authenticationManager = authenticationManager;
@@ -101,6 +70,7 @@ public class WebSocketServerHandler extends TextWebSocketHandler {
public void afterConnectionEstablished(WebSocketSession session) throws Exception { public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 添加会话到活跃会话集合 // 添加会话到活跃会话集合
SessionInfo sessionInfo = new SessionInfo(); SessionInfo sessionInfo = new SessionInfo();
sessionInfo.setObjectMapper(objectMapper);
sessionInfo.setSession(session); sessionInfo.setSession(session);
sessionInfo.setLoginHistoryId((Integer) session.getAttributes().get("loginHistoryId")); sessionInfo.setLoginHistoryId((Integer) session.getAttributes().get("loginHistoryId"));
sessionInfo.setEmployeeId((Integer) session.getAttributes().get("employeeId")); sessionInfo.setEmployeeId((Integer) session.getAttributes().get("employeeId"));
@@ -108,7 +78,7 @@ public class WebSocketServerHandler extends TextWebSocketHandler {
if (sessionInfo.getEmployeeId() == null) { if (sessionInfo.getEmployeeId() == null) {
logger.error("会话未绑定用户: {}", session.getId()); logger.error("会话未绑定用户: {}", session.getId());
sendError(session, WebSocketConstant.ERROR_CODE_UNAUTHORIZED, "会话未绑定用户"); sendError(session, WebSocketConstant.ERROR_CODE_UNAUTHORIZED, "会话未绑定用户");
session.close(CloseStatus.NOT_ACCEPTABLE); // session.close(CloseStatus.NOT_ACCEPTABLE);
return; return;
} }
@@ -157,389 +127,49 @@ public class WebSocketServerHandler extends TextWebSocketHandler {
private boolean handleAsJson(WebSocketSession session, String payload) { private boolean handleAsJson(WebSocketSession session, String payload) {
if (!session.isOpen()) { if (!session.isOpen()) {
logger.warn("尝试在已关闭的WebSocket会话上处理消息回调"); logger.warn("尝试在已关闭的WebSocket会话[{}]上处理消息回调", session.getId());
return true;
}
SessionInfo sessionInfo = activeSessions.get(session.getAttributes().get("employeeId"));
if (sessionInfo == null) {
logger.error("未绑定用户: {}", session.getId());
sendError(session, WebSocketConstant.ERROR_CODE_UNAUTHORIZED, "会话未绑定用户");
// session.close(CloseStatus.NOT_ACCEPTABLE);
return true; return true;
} }
JsonNode jsonNode = null; JsonNode jsonNode = null;
try { try {
jsonNode = objectMapper.readTree(payload); jsonNode = objectMapper.readTree(payload);
} catch (JsonProcessingException e) {
if (payload.startsWith("[") || payload.startsWith("{")) {
logger.warn("解析消息回调JSON失败: {}", payload, e);
// 全局错误
try {
sessionInfo.sendError(WebSocketConstant.ERROR_CODE_INTERNAL_SERVER_ERROR, "JSON格式错误");
} catch (IOException e1) {
logger.error("发送错误消息失败: {}", e1.getMessage(), e1);
}
return true;
}
return false;
} catch (Exception e) { } catch (Exception e) {
logger.warn("解析消息回调JSON失败: {}", payload, e); logger.warn("解析消息回调JSON失败: {}", payload, e);
return false; return false;
} }
if (jsonNode.has(WebSocketConstant.MESSAGE_ID_FIELD_NAME)) { if (jsonNode.has(WebSocketConstant.MESSAGE_ID_FIELD_NAME)) {
// 处理 messageId 的消息 callbackManager.onMessage(sessionInfo, jsonNode);
String messageId = jsonNode.get(WebSocketConstant.MESSAGE_ID_FIELD_NAME).asText();
try {
handleAsMessageCallback(session, messageId, jsonNode);
} catch (Exception e) {
sendError(session, messageId, e.getMessage());
logger.warn("处理消息回调失败 (消息ID: {}): {}", messageId, e.getMessage(), e);
}
return true; return true;
} }
if (jsonNode.has(WebSocketConstant.SESSION_ID_FIELD_NAME)) { if (jsonNode.has(WebSocketConstant.SESSION_ID_FIELD_NAME)) {
taskManager.onMessage(session, jsonNode); taskManager.onMessage(sessionInfo, jsonNode);
return true; return true;
} }
return false; return false;
} }
private void handleAsMessageCallback(WebSocketSession session, String messageId, JsonNode jsonNode)
throws Exception {
if (!jsonNode.has(WebSocketConstant.SERVICE_FIELD_NAME)) {
throw new IllegalArgumentException("缺失 service 参数");
}
String serviceName = jsonNode.get(WebSocketConstant.SERVICE_FIELD_NAME).asText();
Object service = null;
try {
service = SpringApp.getBean(serviceName);
} catch (Exception e) {
throw new IllegalArgumentException("未找到服务: " + serviceName);
}
if (!jsonNode.has(WebSocketConstant.METHOD_FIELD_NAME)) {
throw new IllegalArgumentException("缺失 method 参数");
}
String methodName = jsonNode.get(WebSocketConstant.METHOD_FIELD_NAME).asText();
JsonNode argumentsNode = jsonNode.get(WebSocketConstant.ARGUMENTS_FIELD_NAME);
Object result = null;
if (methodName.equals("findAll")) {
result = invokerFindAllMethod(service, argumentsNode);
} else if (methodName.equals("findById")) {
result = invokerFindByIdMethod(service, argumentsNode);
} else if (methodName.equals("save")) {
result = invokerSaveMethod(service, argumentsNode);
} else if (methodName.equals("delete")) {
result = invokerDeleteMethod(service, argumentsNode);
} else if (methodName.equals("count")) {
result = invokerCountMethod(service, argumentsNode);
} else {
result = invokerOtherMethod(service, methodName, argumentsNode);
}
Map<String, Object> map = new HashMap<>();
if (result instanceof Voable<?>) {
map.put("data", ((Voable<?>) result).toVo());
} else {
map.put("data", result);
}
map.put(WebSocketConstant.MESSAGE_ID_FIELD_NAME, messageId);
map.put(WebSocketConstant.SUCCESS_FIELD_VALUE, true);
String response = objectMapper.writeValueAsString(map);
session.sendMessage(new TextMessage(response));
}
private Object invokerOtherMethod(Object service, String methodName, JsonNode argumentsNode)
throws NoSuchMethodException, SecurityException, InvocationTargetException, IllegalAccessException,
ClassNotFoundException, JsonProcessingException {
int size = argumentsNode.size();
Class<?> targetClass = getTargetClass(service.getClass());
if (size == 0) {
Method method = targetClass.getMethod(methodName);
return method.invoke(service);
}
if (!argumentsNode.get(0).isArray()) {
Class<?> parameterType = Class.forName(argumentsNode.get(1).asText());
Object arg = objectMapper.treeToValue(argumentsNode.get(0), parameterType);
Method method = targetClass.getMethod(methodName, parameterType);
return method.invoke(service, arg);
}
// 参数值
JsonNode paramsNode = argumentsNode.get(0);
// 参数类型
JsonNode typesNode = argumentsNode.get(1);
Class<?>[] parameterTypes = new Class<?>[typesNode.size()];
Object[] args = new Object[paramsNode.size()];
for (int i = 0; i < typesNode.size(); i++) {
String type = typesNode.get(i).asText();
parameterTypes[i] = Class.forName(type);
args[i] = objectMapper.treeToValue(paramsNode.get(i), parameterTypes[i]);
}
try {
Method method = targetClass.getMethod(methodName, parameterTypes);
return method.invoke(service, args);
} catch (NoSuchMethodException e) {
logger.error("NoSuchMethodException, targetClass: {}, Methods:{}", targetClass, targetClass.getMethods());
throw e;
}
}
private Object invokerDeleteMethod(Object service, JsonNode argumentsNode) {
JsonNode paramsNode = argumentsNode.get(0);
if (!paramsNode.has("id")) {
throw new IllegalArgumentException("缺失 id 参数");
}
int id = paramsNode.get("id").asInt();
IEntityService<Object> entityService = (IEntityService<Object>) service;
Object entity = entityService.findById(id);
if (entity == null) {
throw new NoSuchElementException("未找到实体: #" + id);
}
entityService.delete(entity);
return entity;
}
private Object invokerSaveMethod(Object service, JsonNode argumentsNode) throws JsonMappingException {
JsonNode paramsNode = argumentsNode.get(0);
if (service instanceof IEntityService<?> entityService) {
Object entity = null;
if (paramsNode.has("id") && !paramsNode.get("id").isNull()) {
int id = paramsNode.get("id").asInt();
entity = entityService.findById(id);
if (entity == null) {
throw new NoSuchElementException("未找到实体: #" + id);
}
} else {
entity = createNewEntity(entityService);
}
if (service instanceof VoableService<?, ?>) {
String typeClz = argumentsNode.get(1).asText();
Class<?> type = null;
try {
type = Class.forName(typeClz);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
Object object = objectMapper.convertValue(paramsNode, type);
((VoableService<Object, Object>) service).updateByVo(entity, object);
} else {
objectMapper.updateValue(entity, paramsNode);
}
return ((IEntityService<Object>) entityService).save(entity);
}
return null;
}
private <T> T createNewEntity(IEntityService<T> entityService) {
try {
// 通过分析接口的泛型参数来获取实体类型
Class<?> serviceClass = entityService.getClass();
// 1. 直接检查接口
Class<T> entityClass = findEntityTypeInInterfaces(serviceClass);
if (entityClass != null) {
return entityClass.getDeclaredConstructor().newInstance();
}
// 2. 处理Spring代理类 - 获取原始类
Class<?> targetClass = getTargetClass(serviceClass);
if (targetClass != serviceClass) {
entityClass = findEntityTypeInInterfaces(targetClass);
if (entityClass != null) {
return entityClass.getDeclaredConstructor().newInstance();
}
}
// 3. 尝试查找父类
entityClass = findEntityTypeInSuperclass(serviceClass);
if (entityClass != null) {
return entityClass.getDeclaredConstructor().newInstance();
}
// 4. 如果上述方法都失败,尝试从参数类型推断
entityClass = findEntityTypeFromMethodParameters(serviceClass);
if (entityClass != null) {
return entityClass.getDeclaredConstructor().newInstance();
}
// 如果所有方法都失败,抛出更具描述性的异常
throw new UnsupportedOperationException("无法确定实体类型,请检查服务实现: " + serviceClass.getName());
} catch (Exception e) {
throw new RuntimeException("无法创建Entity实例: " + e.getMessage(), e);
}
}
/**
* 从接口中查找实体类型
*/
@SuppressWarnings("unchecked")
private <T> Class<T> findEntityTypeInInterfaces(Class<?> serviceClass) {
Type[] interfaces = serviceClass.getGenericInterfaces();
for (Type iface : interfaces) {
if (iface instanceof ParameterizedType paramType) {
if (IEntityService.class.isAssignableFrom((Class<?>) paramType.getRawType())) {
// 获取IEntityService的泛型参数类型
Type entityType = paramType.getActualTypeArguments()[0];
if (entityType instanceof Class<?>) {
return (Class<T>) entityType;
} else if (entityType instanceof ParameterizedType) {
// 处理参数化类型
Type rawType = ((ParameterizedType) entityType).getRawType();
if (rawType instanceof Class<?>) {
return (Class<T>) rawType;
}
}
}
}
}
return null;
}
/**
* 从父类中查找实体类型
*/
@SuppressWarnings("unchecked")
private <T> Class<T> findEntityTypeInSuperclass(Class<?> serviceClass) {
Type genericSuperclass = serviceClass.getGenericSuperclass();
while (genericSuperclass != null && genericSuperclass != Object.class) {
if (genericSuperclass instanceof ParameterizedType paramType) {
Type rawType = paramType.getRawType();
if (rawType instanceof Class<?> && IEntityService.class.isAssignableFrom((Class<?>) rawType)) {
Type entityType = paramType.getActualTypeArguments()[0];
if (entityType instanceof Class<?>) {
return (Class<T>) entityType;
}
}
}
// 继续查找父类的父类
if (genericSuperclass instanceof Class<?>) {
genericSuperclass = ((Class<?>) genericSuperclass).getGenericSuperclass();
} else {
break;
}
}
return null;
}
/**
* 尝试从方法参数类型推断实体类型
*/
@SuppressWarnings("unchecked")
private <T> Class<T> findEntityTypeFromMethodParameters(Class<?> serviceClass) {
try {
// 尝试通过findById方法推断实体类型
Method findByIdMethod = serviceClass.getMethod("findById", Integer.class);
if (findByIdMethod != null) {
return (Class<T>) findByIdMethod.getReturnType();
}
// 尝试通过findAll方法推断实体类型
Method[] methods = serviceClass.getMethods();
for (Method method : methods) {
if (method.getName().equals("findAll") && method.getParameterCount() > 0) {
Type returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType paramType &&
paramType.getRawType() instanceof Class<?> &&
"org.springframework.data.domain.Page"
.equals(((Class<?>) paramType.getRawType()).getName())) {
Type[] actualTypeArguments = paramType.getActualTypeArguments();
if (actualTypeArguments.length > 0 && actualTypeArguments[0] instanceof Class<?>) {
return (Class<T>) actualTypeArguments[0];
}
}
}
}
} catch (Exception e) {
// 忽略异常,继续尝试其他方法
}
return null;
}
/**
* 获取被代理的原始类
*/
private Class<?> getTargetClass(Class<?> proxyClass) {
// 处理CGLIB代理类
if (proxyClass.getName().contains("$$SpringCGLIB$$")) {
return proxyClass.getSuperclass();
}
return proxyClass;
}
/*
* see client QueryService#findById(Integer)
*/
private Object invokerFindByIdMethod(Object service, JsonNode argumentsNode) {
JsonNode paramsNode = argumentsNode.get(0);
if (service instanceof IEntityService<?> entityService) {
Integer id = paramsNode.asInt();
return entityService.findById(id);
}
try {
JsonNode typesNode = argumentsNode.get(1);
if (paramsNode.isInt()) {
Method method = service.getClass().getMethod("findById", Integer.class);
return method.invoke(service, paramsNode.asInt());
}
if (paramsNode.isTextual()) {
Method method = service.getClass().getMethod("findById", String.class);
return method.invoke(service, paramsNode.asText());
}
throw new IllegalArgumentException("unable to invoke findById method, paramsNode is not int or text");
} catch (Exception e) {
throw new RuntimeException("unable to invoke findById method", e);
}
}
private Object invokerFindAllMethod(Object service, JsonNode argumentsNode) throws JsonProcessingException {
JsonNode paramsNode = argumentsNode.get(0);
JsonNode pageableNode = argumentsNode.get(1);
PageArgument pageArgument = objectMapper.treeToValue(pageableNode, PageArgument.class);
QueryService<?> entityService = (QueryService<?>) service;
Page<?> page = entityService.findAll(paramsNode, pageArgument.toPageable());
return PageContent.of(page.map(entity -> {
if (entity instanceof Voable<?>) {
return ((Voable<?>) entity).toVo();
}
return entity;
}));
}
private Object invokerCountMethod(Object service, JsonNode argumentsNode) {
JsonNode paramsNode = argumentsNode.get(0);
if (service instanceof QueryService<?> entityService) {
return entityService.count(paramsNode);
}
return null;
}
private void sendError(WebSocketSession session, String messageId, String message) {
_sendError(session, WebSocketConstant.MESSAGE_ID_FIELD_NAME, messageId, message);
}
private void _sendError(WebSocketSession session, String fieldName, String messageId, String message) {
if (session == null || !session.isOpen()) {
logger.warn("尝试向已关闭的WebSocket会话发送错误消息: {}", message);
return;
}
try {
String errorMessage = objectMapper.writeValueAsString(Map.of(
fieldName, messageId,
WebSocketConstant.SUCCESS_FIELD_VALUE, false,
WebSocketConstant.MESSAGE_FIELD_NAME, message));
// 检查会话状态并尝试发送错误消息
if (session.isOpen()) {
session.sendMessage(new TextMessage(errorMessage));
} else {
logger.warn("会话已关闭,无法发送错误消息: {}", message);
}
} catch (Exception e) {
// 捕获所有可能的异常,防止影响主流程
logger.error("发送错误消息失败 (会话ID: {})", session.getId(), e);
}
}
@Override @Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) { protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
logger.info("收到来自客户端的二进制消息: " + message.getPayload() + " (会话ID: " + session.getId() + ")"); logger.info("收到来自客户端的二进制消息: " + message.getPayload() + " (会话ID: " + session.getId() + ")");
@@ -642,7 +272,7 @@ public class WebSocketServerHandler extends TextWebSocketHandler {
try { try {
ObjectNode objectNode = objectMapper.createObjectNode(); ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put(WebSocketConstant.ERROR_CODE_FIELD_NAME, errorCode); objectNode.put(WebSocketConstant.ERROR_CODE_FIELD_NAME, errorCode);
objectNode.put(WebSocketConstant.SUCCESS_FIELD_VALUE, false); objectNode.put(WebSocketConstant.SUCCESS_FIELD_NAME, false);
objectNode.put(WebSocketConstant.MESSAGE_FIELD_NAME, message); objectNode.put(WebSocketConstant.MESSAGE_FIELD_NAME, message);
String errorMessage = objectMapper.writeValueAsString(objectNode); String errorMessage = objectMapper.writeValueAsString(objectNode);

View File

@@ -0,0 +1,389 @@
package com.ecep.contract.service;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import com.ecep.contract.IEntityService;
import com.ecep.contract.PageArgument;
import com.ecep.contract.PageContent;
import com.ecep.contract.QueryService;
import com.ecep.contract.SpringApp;
import com.ecep.contract.constant.WebSocketConstant;
import com.ecep.contract.handler.SessionInfo;
import com.ecep.contract.model.Voable;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@Service
public class WebSocketServerCallbackManager {
static final Logger logger = LoggerFactory.getLogger(WebSocketServerCallbackManager.class);
private final ObjectMapper objectMapper;
public WebSocketServerCallbackManager(@Autowired ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public void onMessage(SessionInfo session, JsonNode jsonNode) {
// 处理 messageId 的消息
String messageId = jsonNode.get(WebSocketConstant.MESSAGE_ID_FIELD_NAME).asText();
try {
Object result = handleAsMessageCallback(session, messageId, jsonNode);
send(session, messageId, result);
} catch (Exception e) {
sendError(session, messageId, 500, e.getMessage());
logger.warn("处理消息回调失败 (消息ID: {}): {}", messageId, e.getMessage(), e);
}
}
private void sendError(SessionInfo session, String messageId, int errorCode, String message) {
try {
session.sendError(WebSocketConstant.MESSAGE_ID_FIELD_NAME, messageId, errorCode, message);
} catch (IOException e) {
logger.warn("发送错误消息失败 (消息ID: {}): {}", messageId, e.getMessage(), e);
}
}
private void send(SessionInfo session, String messageId, Object data) {
Map<String, Object> map = new HashMap<>();
if (data instanceof Voable<?>) {
map.put("data", ((Voable<?>) data).toVo());
} else {
map.put("data", data);
}
map.put(WebSocketConstant.MESSAGE_ID_FIELD_NAME, messageId);
map.put(WebSocketConstant.SUCCESS_FIELD_NAME, true);
try {
session.send(map);
} catch (IOException e) {
logger.warn("发送消息失败 (消息ID: {}): {}", messageId, e.getMessage(), e);
}
}
private Object handleAsMessageCallback(SessionInfo session, String messageId, JsonNode jsonNode)
throws Exception {
if (!jsonNode.has(WebSocketConstant.SERVICE_FIELD_NAME)) {
throw new IllegalArgumentException("缺失 service 参数");
}
String serviceName = jsonNode.get(WebSocketConstant.SERVICE_FIELD_NAME).asText();
Object service = null;
try {
service = SpringApp.getBean(serviceName);
} catch (Exception e) {
throw new IllegalArgumentException("未找到服务: " + serviceName);
}
if (!jsonNode.has(WebSocketConstant.METHOD_FIELD_NAME)) {
throw new IllegalArgumentException("缺失 method 参数");
}
String methodName = jsonNode.get(WebSocketConstant.METHOD_FIELD_NAME).asText();
JsonNode argumentsNode = jsonNode.get(WebSocketConstant.ARGUMENTS_FIELD_NAME);
Object result = null;
if (methodName.equals("findAll")) {
result = invokerFindAllMethod(service, argumentsNode);
} else if (methodName.equals("findById")) {
result = invokerFindByIdMethod(service, argumentsNode);
} else if (methodName.equals("save")) {
result = invokerSaveMethod(service, argumentsNode);
} else if (methodName.equals("delete")) {
result = invokerDeleteMethod(service, argumentsNode);
} else if (methodName.equals("count")) {
result = invokerCountMethod(service, argumentsNode);
} else {
result = invokerOtherMethod(service, methodName, argumentsNode);
}
return result;
}
private Object invokerOtherMethod(Object service, String methodName, JsonNode argumentsNode)
throws NoSuchMethodException, SecurityException, InvocationTargetException, IllegalAccessException,
ClassNotFoundException, JsonProcessingException {
int size = argumentsNode.size();
Class<?> targetClass = getTargetClass(service.getClass());
if (size == 0) {
Method method = targetClass.getMethod(methodName);
return method.invoke(service);
}
if (!argumentsNode.get(0).isArray()) {
Class<?> parameterType = Class.forName(argumentsNode.get(1).asText());
Object arg = objectMapper.treeToValue(argumentsNode.get(0), parameterType);
Method method = targetClass.getMethod(methodName, parameterType);
return method.invoke(service, arg);
}
// 参数值
JsonNode paramsNode = argumentsNode.get(0);
// 参数类型
JsonNode typesNode = argumentsNode.get(1);
Class<?>[] parameterTypes = new Class<?>[typesNode.size()];
Object[] args = new Object[paramsNode.size()];
for (int i = 0; i < typesNode.size(); i++) {
String type = typesNode.get(i).asText();
parameterTypes[i] = Class.forName(type);
args[i] = objectMapper.treeToValue(paramsNode.get(i), parameterTypes[i]);
}
try {
Method method = targetClass.getMethod(methodName, parameterTypes);
return method.invoke(service, args);
} catch (NoSuchMethodException e) {
logger.error("NoSuchMethodException, targetClass: {}, Methods:{}", targetClass, targetClass.getMethods());
throw e;
}
}
private Object invokerDeleteMethod(Object service, JsonNode argumentsNode) {
JsonNode paramsNode = argumentsNode.get(0);
if (!paramsNode.has("id")) {
throw new IllegalArgumentException("缺失 id 参数");
}
int id = paramsNode.get("id").asInt();
IEntityService<Object> entityService = (IEntityService<Object>) service;
Object entity = entityService.findById(id);
if (entity == null) {
throw new NoSuchElementException("未找到实体: #" + id);
}
entityService.delete(entity);
return entity;
}
private Object invokerSaveMethod(Object service, JsonNode argumentsNode) throws JsonMappingException {
JsonNode paramsNode = argumentsNode.get(0);
if (service instanceof IEntityService<?> entityService) {
Object entity = null;
if (paramsNode.has("id") && !paramsNode.get("id").isNull()) {
int id = paramsNode.get("id").asInt();
entity = entityService.findById(id);
if (entity == null) {
throw new NoSuchElementException("未找到实体: #" + id);
}
} else {
entity = createNewEntity(entityService);
}
if (service instanceof VoableService<?, ?>) {
String typeClz = argumentsNode.get(1).asText();
Class<?> type = null;
try {
type = Class.forName(typeClz);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
Object object = objectMapper.convertValue(paramsNode, type);
((VoableService<Object, Object>) service).updateByVo(entity, object);
} else {
objectMapper.updateValue(entity, paramsNode);
}
return ((IEntityService<Object>) entityService).save(entity);
}
return null;
}
private <T> T createNewEntity(IEntityService<T> entityService) {
try {
// 通过分析接口的泛型参数来获取实体类型
Class<?> serviceClass = entityService.getClass();
// 1. 直接检查接口
Class<T> entityClass = findEntityTypeInInterfaces(serviceClass);
if (entityClass != null) {
return entityClass.getDeclaredConstructor().newInstance();
}
// 2. 处理Spring代理类 - 获取原始类
Class<?> targetClass = getTargetClass(serviceClass);
if (targetClass != serviceClass) {
entityClass = findEntityTypeInInterfaces(targetClass);
if (entityClass != null) {
return entityClass.getDeclaredConstructor().newInstance();
}
}
// 3. 尝试查找父类
entityClass = findEntityTypeInSuperclass(serviceClass);
if (entityClass != null) {
return entityClass.getDeclaredConstructor().newInstance();
}
// 4. 如果上述方法都失败,尝试从参数类型推断
entityClass = findEntityTypeFromMethodParameters(serviceClass);
if (entityClass != null) {
return entityClass.getDeclaredConstructor().newInstance();
}
// 如果所有方法都失败,抛出更具描述性的异常
throw new UnsupportedOperationException("无法确定实体类型,请检查服务实现: " + serviceClass.getName());
} catch (Exception e) {
throw new RuntimeException("无法创建Entity实例: " + e.getMessage(), e);
}
}
/**
* 从接口中查找实体类型
*/
@SuppressWarnings("unchecked")
private <T> Class<T> findEntityTypeInInterfaces(Class<?> serviceClass) {
Type[] interfaces = serviceClass.getGenericInterfaces();
for (Type iface : interfaces) {
if (iface instanceof ParameterizedType paramType) {
if (IEntityService.class.isAssignableFrom((Class<?>) paramType.getRawType())) {
// 获取IEntityService的泛型参数类型
Type entityType = paramType.getActualTypeArguments()[0];
if (entityType instanceof Class<?>) {
return (Class<T>) entityType;
} else if (entityType instanceof ParameterizedType) {
// 处理参数化类型
Type rawType = ((ParameterizedType) entityType).getRawType();
if (rawType instanceof Class<?>) {
return (Class<T>) rawType;
}
}
}
}
}
return null;
}
/**
* 从父类中查找实体类型
*/
@SuppressWarnings("unchecked")
private <T> Class<T> findEntityTypeInSuperclass(Class<?> serviceClass) {
Type genericSuperclass = serviceClass.getGenericSuperclass();
while (genericSuperclass != null && genericSuperclass != Object.class) {
if (genericSuperclass instanceof ParameterizedType paramType) {
Type rawType = paramType.getRawType();
if (rawType instanceof Class<?> && IEntityService.class.isAssignableFrom((Class<?>) rawType)) {
Type entityType = paramType.getActualTypeArguments()[0];
if (entityType instanceof Class<?>) {
return (Class<T>) entityType;
}
}
}
// 继续查找父类的父类
if (genericSuperclass instanceof Class<?>) {
genericSuperclass = ((Class<?>) genericSuperclass).getGenericSuperclass();
} else {
break;
}
}
return null;
}
/**
* 尝试从方法参数类型推断实体类型
*/
@SuppressWarnings("unchecked")
private <T> Class<T> findEntityTypeFromMethodParameters(Class<?> serviceClass) {
try {
// 尝试通过findById方法推断实体类型
Method findByIdMethod = serviceClass.getMethod("findById", Integer.class);
if (findByIdMethod != null) {
return (Class<T>) findByIdMethod.getReturnType();
}
// 尝试通过findAll方法推断实体类型
Method[] methods = serviceClass.getMethods();
for (Method method : methods) {
if (method.getName().equals("findAll") && method.getParameterCount() > 0) {
Type returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType paramType &&
paramType.getRawType() instanceof Class<?> &&
"org.springframework.data.domain.Page"
.equals(((Class<?>) paramType.getRawType()).getName())) {
Type[] actualTypeArguments = paramType.getActualTypeArguments();
if (actualTypeArguments.length > 0 && actualTypeArguments[0] instanceof Class<?>) {
return (Class<T>) actualTypeArguments[0];
}
}
}
}
} catch (Exception e) {
// 忽略异常,继续尝试其他方法
}
return null;
}
/**
* 获取被代理的原始类
*/
private Class<?> getTargetClass(Class<?> proxyClass) {
// 处理CGLIB代理类
if (proxyClass.getName().contains("$$SpringCGLIB$$")) {
return proxyClass.getSuperclass();
}
return proxyClass;
}
/*
* see client QueryService#findById(Integer)
*/
private Object invokerFindByIdMethod(Object service, JsonNode argumentsNode) {
JsonNode paramsNode = argumentsNode.get(0);
if (service instanceof IEntityService<?> entityService) {
Integer id = paramsNode.asInt();
return entityService.findById(id);
}
try {
JsonNode typesNode = argumentsNode.get(1);
if (paramsNode.isInt()) {
Method method = service.getClass().getMethod("findById", Integer.class);
return method.invoke(service, paramsNode.asInt());
}
if (paramsNode.isTextual()) {
Method method = service.getClass().getMethod("findById", String.class);
return method.invoke(service, paramsNode.asText());
}
throw new IllegalArgumentException("unable to invoke findById method, paramsNode is not int or text");
} catch (Exception e) {
throw new RuntimeException("unable to invoke findById method", e);
}
}
private Object invokerFindAllMethod(Object service, JsonNode argumentsNode) throws JsonProcessingException {
JsonNode paramsNode = argumentsNode.get(0);
JsonNode pageableNode = argumentsNode.get(1);
PageArgument pageArgument = objectMapper.treeToValue(pageableNode, PageArgument.class);
QueryService<?> entityService = (QueryService<?>) service;
Page<?> page = entityService.findAll(paramsNode, pageArgument.toPageable());
return PageContent.of(page.map(entity -> {
if (entity instanceof Voable<?>) {
return ((Voable<?>) entity).toVo();
}
return entity;
}));
}
private Object invokerCountMethod(Object service, JsonNode argumentsNode) {
JsonNode paramsNode = argumentsNode.get(0);
if (service instanceof QueryService<?> entityService) {
return entityService.count(paramsNode);
}
return null;
}
}

View File

@@ -1,12 +1,4 @@
package com.ecep.contract.ds.company.tasker; package com.ecep.contract.service.tasker;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.cloud.rk.CloudRkService; import com.ecep.contract.cloud.rk.CloudRkService;
@@ -22,16 +14,23 @@ import com.ecep.contract.model.CloudRk;
import com.ecep.contract.model.CloudYu; import com.ecep.contract.model.CloudYu;
import com.ecep.contract.model.Company; import com.ecep.contract.model.Company;
import com.ecep.contract.ui.Tasker; import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Setter; import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import java.time.LocalDate;
import java.time.LocalDateTime;
/** /**
* 合并更新 * 合并更新
*/ */
public class CompanyCompositeUpdateTasker extends Tasker<Object> { public class CompanyCompositeUpdateTasker extends Tasker<Object> implements WebSocketServerTasker {
private static final Logger logger = LoggerFactory.getLogger(CompanyCompositeUpdateTasker.class); private static final Logger logger = LoggerFactory.getLogger(CompanyCompositeUpdateTasker.class);
CloudRkCtx cloudRkCtx = new CloudRkCtx(); CloudRkCtx cloudRkCtx = new CloudRkCtx();
// CloudYuCtx cloudYuCtx = new CloudYuCtx();
@Setter @Setter
private CloudRkService cloudRkService; private CloudRkService cloudRkService;
@@ -43,24 +42,29 @@ public class CompanyCompositeUpdateTasker extends Tasker<Object> {
private Company company; private Company company;
@Override @Override
protected Object execute(MessageHolder holder) throws Exception { public void init(JsonNode argsNode) {
int companyId = argsNode.get(0).asInt();
company = getCachedBean(com.ecep.contract.ds.company.service.CompanyService.class).findById(companyId);
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateProgress(0.1, 1); updateProgress(0.1, 1);
syncFromCloudRk(holder); syncFromCloudRk(holder);
updateProgress(0.3, 1); updateProgress(0.3, 1);
syncFromYongYouU8(holder); syncFromYongYouU8(holder);
updateProgress(0.6, 1); updateProgress(0.5, 1);
syncFromCloudTyc(holder); syncFromCloudTyc(holder);
updateProgress(0.7, 1);
return null; return null;
} }
private void syncFromCloudRk(MessageHolder holder) { private void syncFromCloudRk(MessageHolder holder) {
holder.debug("1. 从 " + CloudServiceConstant.RK_NAME + " 更新..."); holder.debug("1. 从 " + CloudServiceConstant.RK_NAME + " 更新...");
try { try {
cloudRkService = getBean(CloudRkService.class); cloudRkService = getCachedBean(CloudRkService.class);
} catch (BeansException e) { } catch (BeansException e) {
holder.warn("未启用 " + CloudServiceConstant.RK_NAME + " 服务"); holder.warn("服务未启用");
return; return;
} }
CloudRk cloudRk = cloudRkService.getOrCreateCloudRk(company); CloudRk cloudRk = cloudRkService.getOrCreateCloudRk(company);
@@ -70,22 +74,29 @@ public class CompanyCompositeUpdateTasker extends Tasker<Object> {
} }
try { try {
cloudRkCtx.setCloudRkService(cloudRkService);
if (cloudRkCtx.syncCompany(company, cloudRk, holder)) { if (cloudRkCtx.syncCompany(company, cloudRk, holder)) {
} }
} catch (Exception e) { } catch (Exception e) {
cloudRk.setDescription(e.getMessage()); String message = e.getMessage();
if (message.length() > 50) {
message = message.substring(0, 50);
}
cloudRk.setDescription(message);
} finally { } finally {
cloudRk.setLatestUpdate(LocalDateTime.now()); cloudRk.setLatestUpdate(LocalDateTime.now());
cloudRkService.save(cloudRk); try {
cloudRkService.save(cloudRk);
} catch (Exception e) {
holder.error("保存 CloudRk 错误: " + cloudRk.getDescription());
}
} }
} }
private void syncFromYongYouU8(MessageHolder holder) { private void syncFromYongYouU8(MessageHolder holder) {
holder.debug("2. 从 " + CloudServiceConstant.U8_NAME + " 更新..."); holder.debug("2. 从 " + CloudServiceConstant.U8_NAME + " 更新...");
try { try {
yongYouU8Service = getBean(YongYouU8Service.class); yongYouU8Service = getCachedBean(YongYouU8Service.class);
} catch (BeansException e) { } catch (BeansException e) {
holder.warn("未启用 " + CloudServiceConstant.U8_NAME + " 服务"); holder.warn("未启用 " + CloudServiceConstant.U8_NAME + " 服务");
return; return;
@@ -140,7 +151,7 @@ public class CompanyCompositeUpdateTasker extends Tasker<Object> {
private void syncFromCloudTyc(MessageHolder holder) { private void syncFromCloudTyc(MessageHolder holder) {
holder.debug("3. 从 " + CloudServiceConstant.TYC_NAME + " 更新..."); holder.debug("3. 从 " + CloudServiceConstant.TYC_NAME + " 更新...");
try { try {
cloudTycService = getBean(CloudTycService.class); cloudTycService = getCachedBean(CloudTycService.class);
} catch (BeansException e) { } catch (BeansException e) {
holder.warn("未启用 " + CloudServiceConstant.TYC_NAME + " 服务"); holder.warn("未启用 " + CloudServiceConstant.TYC_NAME + " 服务");
return; return;

View File

@@ -1,4 +1,4 @@
package com.ecep.contract.ds.customer.tasker; package com.ecep.contract.service.tasker;
import static com.ecep.contract.util.ExcelUtils.setCellValue; import static com.ecep.contract.util.ExcelUtils.setCellValue;
@@ -32,7 +32,6 @@ import com.ecep.contract.model.Company;
import com.ecep.contract.model.CompanyCustomer; import com.ecep.contract.model.CompanyCustomer;
import com.ecep.contract.model.CompanyCustomerEvaluationFormFile; import com.ecep.contract.model.CompanyCustomerEvaluationFormFile;
import com.ecep.contract.model.CompanyCustomerFile; import com.ecep.contract.model.CompanyCustomerFile;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker; import com.ecep.contract.ui.Tasker;
import com.ecep.contract.util.CompanyUtils; import com.ecep.contract.util.CompanyUtils;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;

View File

@@ -1,4 +1,4 @@
package com.ecep.contract.ds.customer.tasker; package com.ecep.contract.service.tasker;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Comparator; import java.util.Comparator;
@@ -12,7 +12,6 @@ import com.ecep.contract.ds.customer.service.CompanyCustomerService;
import com.ecep.contract.model.CompanyCustomer; import com.ecep.contract.model.CompanyCustomer;
import com.ecep.contract.model.CompanyCustomerFile; import com.ecep.contract.model.CompanyCustomerFile;
import com.ecep.contract.model.Contract; import com.ecep.contract.model.Contract;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker; import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;

View File

@@ -1,4 +1,4 @@
package com.ecep.contract.ds.customer.tasker; package com.ecep.contract.service.tasker;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -8,7 +8,6 @@ import com.ecep.contract.ds.company.service.CompanyService;
import com.ecep.contract.ds.customer.service.CompanyCustomerService; import com.ecep.contract.ds.customer.service.CompanyCustomerService;
import com.ecep.contract.model.Company; import com.ecep.contract.model.Company;
import com.ecep.contract.model.CompanyCustomer; import com.ecep.contract.model.CompanyCustomer;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker; import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;

View File

@@ -1,54 +1,44 @@
package com.ecep.contract.ds.company.tasker; package com.ecep.contract.service.tasker;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.ds.company.service.CompanyService; import com.ecep.contract.ds.contract.service.ContractService;
import com.ecep.contract.ds.contract.tasker.ContractVerifyComm; import com.ecep.contract.ds.contract.tasker.ContractVerifyComm;
import com.ecep.contract.model.Company; import com.ecep.contract.model.Company;
import com.ecep.contract.model.Contract; import com.ecep.contract.model.Contract;
import com.ecep.contract.ui.MessageHolderImpl;
import com.ecep.contract.ui.Tasker; import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
public class CompanyVerifyTasker extends Tasker<Object> { public class CompanyVerifyTasker extends Tasker<Object> implements WebSocketServerTasker {
@Setter
private CompanyService companyService;
@Getter @Getter
@Setter @Setter
private Company company; private Company company;
@Getter
@Setter
boolean passed = false;
ContractVerifyComm comm = new ContractVerifyComm(); ContractVerifyComm comm = new ContractVerifyComm();
AtomicBoolean verified = new AtomicBoolean(true);
public CompanyService getCompanyService() { public ContractService getContractService() {
if (companyService == null) { return getCachedBean(ContractService.class);
companyService = getBean(CompanyService.class);
}
return companyService;
} }
/**
* 核验公司名下的所有合同
*/
@Override @Override
public Object call() throws Exception { public void init(JsonNode argsNode) {
int companyId = argsNode.get(0).asInt();
company = getCompanyService().findById(companyId);
comm.setVerifyCompanyPath(false); comm.setVerifyCompanyPath(false);
comm.setVerifyCompanyStatus(false); comm.setVerifyCompanyStatus(false);
comm.setVerifyCompanyCredit(false); comm.setVerifyCompanyCredit(false);
return execute(new MessageHolderImpl(this) {
@Override
public void addMessage(Level level, String message) {
super.addMessage(level, message);
if (level.intValue() > Level.INFO.intValue()) {
verified.set(false);
}
}
});
} }
@Override @Override
@@ -57,36 +47,37 @@ public class CompanyVerifyTasker extends Tasker<Object> {
verify(company, holder); verify(company, holder);
if (verified.get()) { if (passed) {
updateMessage(Level.CONFIG, "合规验证通过"); holder.info("合规验证通过");
} else { } else {
updateMessage(Level.SEVERE, "合规验证不通过"); holder.error("合规验证不通过");
} }
updateProperty("passed", passed);
return null; return null;
} }
/**
* 核验公司名下的所有合同
*
* @param company 公司
* @param holder 输出
*/
private void verify(Company company, MessageHolder holder) { private void verify(Company company, MessageHolder holder) {
LocalDate now = LocalDate.now(); LocalDate now = LocalDate.now();
getCompanyService().verifyEnterpriseStatus(company, now, holder::info); if (getCompanyService().verifyEnterpriseStatus(company, now, holder)) {
passed = false;
}
// 验证所有的合同 // 验证所有的合同
List<Contract> list = comm.getContractService().findAllByCompany(company); List<Contract> list = getContractService().findAllByCompany(company);
if (list.isEmpty()) { if (list.isEmpty()) {
holder.debug("!没有相关合同!"); holder.debug("!没有相关合同!");
return; return;
} }
holder.debug("检索到相关合同 " + list.size() + ""); holder.debug("检索到相关合同 " + list.size() + "");
AtomicInteger counter = new AtomicInteger(0); AtomicInteger counter = new AtomicInteger(0);
long total = list.size(); long total = list.size();
for (Contract contract : list) { for (Contract contract : list) {
holder.debug("核验合同:" + contract.getCode() + ", " + contract.getName()); holder.debug("核验合同:" + contract.getCode() + ", " + contract.getName());
comm.verify(company, contract, holder.sub("-- ")); if (!comm.verify(company, contract, holder.sub("-- "))) {
passed = false;
}
updateProgress(counter.incrementAndGet(), total); updateProgress(counter.incrementAndGet(), total);
} }
updateProgress(1, 1); updateProgress(1, 1);

View File

@@ -1,10 +1,10 @@
package com.ecep.contract.ds.contract.tasker; package com.ecep.contract.service.tasker;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import com.ecep.contract.ds.contract.service.ContractService; import com.ecep.contract.ds.contract.service.ContractService;
import com.ecep.contract.service.WebSocketServerTasker; import com.ecep.contract.ds.contract.tasker.AbstContractRepairTasker;
import com.ecep.contract.ds.contract.tasker.ContractRepairComm;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -13,12 +13,11 @@ import com.ecep.contract.MessageHolder;
import com.ecep.contract.model.Contract; import com.ecep.contract.model.Contract;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
/** /**
* 合同修复任务 * 合同修复任务
*/ */
public class ContractRepairTask extends AbstContractRepairTasker implements WebSocketServerTasker { public class ContractRepairTasker extends AbstContractRepairTasker implements WebSocketServerTasker {
@Getter @Getter
private Contract contract; private Contract contract;
@Getter @Getter
@@ -27,7 +26,7 @@ public class ContractRepairTask extends AbstContractRepairTasker implements WebS
private final ContractRepairComm comm = new ContractRepairComm(); private final ContractRepairComm comm = new ContractRepairComm();
public ContractRepairTask() { public ContractRepairTasker() {
} }
public void init(JsonNode argsNode) { public void init(JsonNode argsNode) {

View File

@@ -1,17 +1,14 @@
package com.ecep.contract.ds.contract.tasker; package com.ecep.contract.service.tasker;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.ds.contract.service.ContractService; import com.ecep.contract.ds.contract.service.ContractService;
import com.ecep.contract.ds.contract.tasker.ContractVerifyComm;
import com.ecep.contract.model.Contract; import com.ecep.contract.model.Contract;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.MessageHolderImpl;
import com.ecep.contract.ui.Tasker; import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -20,39 +17,28 @@ public class ContractVerifyTasker extends Tasker<Object> implements WebSocketSer
@Setter @Setter
private Contract contract; private Contract contract;
ContractVerifyComm comm = new ContractVerifyComm(); ContractVerifyComm comm = new ContractVerifyComm();
@Getter
AtomicBoolean verified = new AtomicBoolean(true); @Setter
boolean passed = true;
@Override
public Object call() throws Exception {
updateTitle("验证合同 " + contract.getCode() + " 及其子合同是否符合合规要求");
return execute(new MessageHolderImpl(this) {
@Override
public void addMessage(Level level, String message) {
super.addMessage(level, message);
if (level.intValue() > Level.INFO.intValue()) {
verified.set(false);
}
}
});
}
@Override @Override
protected Object execute(MessageHolder holder) { protected Object execute(MessageHolder holder) {
comm.verify(contract, holder); updateTitle("验证合同 " + contract.getCode() + " 及其子合同是否符合合规要求");
if (verified.get()) { updateProgress(1, 10);
updateMessage(Level.CONFIG, "合规验证通过"); if (!comm.verify(contract, holder)) {
} else { passed = false;
updateMessage(Level.SEVERE, "合规验证不通过");
} }
updateProgress(9, 10);
if (passed) {
holder.info("合规验证通过");
} else {
holder.error("合规验证不通过");
}
updateProgress(10, 10);
updateProperty("passed", passed);
return null; return null;
} }
public void setContractService(ContractService contractService) {
comm.setContractService(contractService);
}
public Locale getLocale() { public Locale getLocale() {
return comm.getLocale(); return comm.getLocale();
} }
@@ -67,5 +53,4 @@ public class ContractVerifyTasker extends Tasker<Object> implements WebSocketSer
contract = getCachedBean(ContractService.class).findById(contractId); contract = getCachedBean(ContractService.class).findById(contractId);
} }
} }

View File

@@ -1,13 +1,11 @@
package com.ecep.contract.ds.customer.tasker; package com.ecep.contract.service.tasker;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
import com.ecep.contract.ds.company.service.CompanyService;
import com.ecep.contract.ds.customer.service.CompanyCustomerFileService; import com.ecep.contract.ds.customer.service.CompanyCustomerFileService;
import com.ecep.contract.ds.customer.service.CompanyCustomerService; import com.ecep.contract.ds.customer.service.CompanyCustomerService;
import com.ecep.contract.model.Company; import com.ecep.contract.model.Company;
import com.ecep.contract.model.CompanyCustomer; import com.ecep.contract.model.CompanyCustomer;
import com.ecep.contract.model.CompanyCustomerFile; import com.ecep.contract.model.CompanyCustomerFile;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker; import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;

View File

@@ -1,6 +1,4 @@
package com.ecep.contract.ds.project; package com.ecep.contract.service.tasker;
import static com.ecep.contract.SpringApp.getBean;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
@@ -10,12 +8,8 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.MessageHolderImpl;
import com.fasterxml.jackson.databind.JsonNode;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import com.ecep.contract.MessageHolder; import com.ecep.contract.MessageHolder;
@@ -24,6 +18,7 @@ import com.ecep.contract.ds.contract.service.ContractService;
import com.ecep.contract.ds.other.service.InventoryService; import com.ecep.contract.ds.other.service.InventoryService;
import com.ecep.contract.ds.project.service.ProjectCostItemService; import com.ecep.contract.ds.project.service.ProjectCostItemService;
import com.ecep.contract.ds.project.service.ProjectCostService; import com.ecep.contract.ds.project.service.ProjectCostService;
import com.ecep.contract.handler.SessionInfo;
import com.ecep.contract.model.Contract; import com.ecep.contract.model.Contract;
import com.ecep.contract.model.ContractItem; import com.ecep.contract.model.ContractItem;
import com.ecep.contract.model.Employee; import com.ecep.contract.model.Employee;
@@ -34,6 +29,7 @@ import com.ecep.contract.model.ProjectCostItem;
import com.ecep.contract.ui.Tasker; import com.ecep.contract.ui.Tasker;
import com.ecep.contract.util.NumberUtils; import com.ecep.contract.util.NumberUtils;
import com.ecep.contract.util.TaxRateUtils; import com.ecep.contract.util.TaxRateUtils;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Setter; import lombok.Setter;
@@ -45,15 +41,24 @@ public class ProjectCostImportItemsFromContractsTasker extends Tasker<Object> im
AtomicBoolean verified = new AtomicBoolean(true); AtomicBoolean verified = new AtomicBoolean(true);
@Override @Override
public void init(JsonNode argsNode) { public void setSession(SessionInfo session) {
int contractId = argsNode.get(0).asInt(); currentUser = () -> {
cost = getCachedBean(ProjectCostService.class).findById(contractId); return getEmployeeService().findById(session.getEmployeeId());
};
} }
@Override @Override
protected Void execute(MessageHolder holder) throws Exception { public Employee getCurrentUser() {
importFromContracts(cost, holder); if (currentUser == null) {
return null; return null;
}
return currentUser.get();
}
@Override
public void init(JsonNode argsNode) {
int contractId = argsNode.get(0).asInt();
cost = getCachedBean(ProjectCostService.class).findById(contractId);
} }
public void importFromContracts(ProjectCost projectCost, MessageHolder holder) { public void importFromContracts(ProjectCost projectCost, MessageHolder holder) {
@@ -134,6 +139,20 @@ public class ProjectCostImportItemsFromContractsTasker extends Tasker<Object> im
- projectCost.getTaxAndSurchargesFee()) * projectCost.getTaxAndSurcharges() / 100); - projectCost.getTaxAndSurchargesFee()) * projectCost.getTaxAndSurcharges() / 100);
} }
public ProjectCostItemService getItemService() {
return getCachedBean(ProjectCostItemService.class);
}
public ProjectCostItem save(ProjectCostItem entity) {
return getItemService().save(entity);
}
@Override
protected Void execute(MessageHolder holder) throws Exception {
importFromContracts(cost, holder);
return null;
}
String toKey(ContractItem item) { String toKey(ContractItem item) {
return item.getTitle() + "_" + item.getSpecification(); return item.getTitle() + "_" + item.getSpecification();
} }
@@ -142,8 +161,24 @@ public class ProjectCostImportItemsFromContractsTasker extends Tasker<Object> im
return item.getTitle() + "_" + item.getSpecification(); return item.getTitle() + "_" + item.getSpecification();
} }
ProjectCostService getCostService() {
return getCachedBean(ProjectCostService.class);
}
ContractService getContractService() {
return getCachedBean(ContractService.class);
}
ContractItemService getContractItemService() {
return getCachedBean(ContractItemService.class);
}
InventoryService getInventoryService() {
return getCachedBean(InventoryService.class);
}
private void compositeOut(Map<Inventory, List<ContractItem>> map, List<ProjectCostItem> results, private void compositeOut(Map<Inventory, List<ContractItem>> map, List<ProjectCostItem> results,
MessageHolder holder) { MessageHolder holder) {
// 根据存货匹配可对多个相同的存货进行合并 // 根据存货匹配可对多个相同的存货进行合并
for (Map.Entry<Inventory, List<ContractItem>> entry : map.entrySet()) { for (Map.Entry<Inventory, List<ContractItem>> entry : map.entrySet()) {
Inventory inventory = Hibernate.isInitialized(entry.getKey()) ? entry.getKey() Inventory inventory = Hibernate.isInitialized(entry.getKey()) ? entry.getKey()
@@ -336,29 +371,4 @@ public class ProjectCostImportItemsFromContractsTasker extends Tasker<Object> im
save(projectCostItem); save(projectCostItem);
} }
ProjectCostService getCostService() {
return getCachedBean(ProjectCostService.class);
}
public ProjectCostItemService getItemService() {
return getCachedBean(ProjectCostItemService.class);
}
ContractService getContractService() {
return getCachedBean(ContractService.class);
}
ContractItemService getContractItemService() {
return getCachedBean(ContractItemService.class);
}
InventoryService getInventoryService() {
return getCachedBean(InventoryService.class);
}
public ProjectCostItem save(ProjectCostItem entity) {
return getItemService().save(entity);
}
} }

View File

@@ -1,7 +1,8 @@
package com.ecep.contract.service; package com.ecep.contract.service.tasker;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@@ -14,11 +15,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import com.ecep.contract.Message; import com.ecep.contract.Message;
import com.ecep.contract.constant.WebSocketConstant; import com.ecep.contract.constant.WebSocketConstant;
import com.ecep.contract.handler.SessionInfo;
import com.ecep.contract.model.Voable; import com.ecep.contract.model.Voable;
import com.ecep.contract.ui.Tasker; import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@@ -42,11 +42,17 @@ public class WebSocketServerTaskManager implements InitializingBean {
Resource resource = resourceLoader.getResource("classpath:tasker_mapper.json"); Resource resource = resourceLoader.getResource("classpath:tasker_mapper.json");
try (InputStream inputStream = resource.getInputStream()) { try (InputStream inputStream = resource.getInputStream()) {
JsonNode rootNode = objectMapper.readTree(inputStream); JsonNode rootNode = objectMapper.readTree(inputStream);
JsonNode taskersNode = rootNode.get("taskers"); JsonNode tasksNode = rootNode.get("tasks");
if (taskersNode != null && taskersNode.isObject()) { if (tasksNode != null && tasksNode.isObject()) {
Map<String, String> taskMap = new java.util.HashMap<>(); Map<String, String> taskMap = new java.util.HashMap<>();
taskersNode.fields().forEachRemaining(entry -> { tasksNode.fields().forEachRemaining(entry -> {
taskMap.put(entry.getKey(), entry.getValue().asText()); taskMap.put(entry.getKey(), entry.getValue().asText());
try {
Class.forName(entry.getValue().asText());
} catch (ClassNotFoundException e) {
logger.error("Failed to load task class: {}", entry.getValue().asText(), e);
}
}); });
taskClzMap = taskMap; taskClzMap = taskMap;
} }
@@ -58,18 +64,26 @@ public class WebSocketServerTaskManager implements InitializingBean {
} }
} }
public void onMessage(WebSocketSession session, JsonNode jsonNode) { public void onMessage(SessionInfo session, JsonNode jsonNode) {
// 处理 sessionId 的消息 // 处理 sessionId 的消息
String sessionId = jsonNode.get(WebSocketConstant.SESSION_ID_FIELD_NAME).asText(); String sessionId = jsonNode.get(WebSocketConstant.SESSION_ID_FIELD_NAME).asText();
try { try {
handleAsSessionCallback(session, sessionId, jsonNode); handleAsSessionCallback(session, sessionId, jsonNode);
} catch (Exception e) { } catch (Exception e) {
sendError(session, sessionId, e.getMessage()); sendError(session, sessionId, WebSocketConstant.ERROR_CODE_INTERNAL_SERVER_ERROR, e.getMessage());
logger.warn("处理会话回调失败 (会话ID: {}): {}", sessionId, e.getMessage(), e); logger.warn("处理会话回调失败 (会话ID: {}): {}", sessionId, e.getMessage(), e);
} }
} }
private void handleAsSessionCallback(WebSocketSession session, String sessionId, JsonNode jsonNode) { private void sendError(SessionInfo session, String sessionId, int errorCode, String message) {
try {
session.sendError(WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId, errorCode, message);
} catch (IOException e) {
logger.warn("发送错误消息失败 (消息ID: {}): {}", sessionId, e.getMessage(), e);
}
}
private void handleAsSessionCallback(SessionInfo session, String sessionId, JsonNode jsonNode) {
if (!jsonNode.has("type")) { if (!jsonNode.has("type")) {
throw new IllegalArgumentException("缺失 type 参数"); throw new IllegalArgumentException("缺失 type 参数");
} }
@@ -79,7 +93,7 @@ public class WebSocketServerTaskManager implements InitializingBean {
} }
} }
private void createTask(WebSocketSession session, String sessionId, JsonNode jsonNode) { private void createTask(SessionInfo session, String sessionId, JsonNode jsonNode) {
if (!jsonNode.has(WebSocketConstant.ARGUMENTS_FIELD_NAME)) { if (!jsonNode.has(WebSocketConstant.ARGUMENTS_FIELD_NAME)) {
throw new IllegalArgumentException("缺失 " + WebSocketConstant.ARGUMENTS_FIELD_NAME + " 参数"); throw new IllegalArgumentException("缺失 " + WebSocketConstant.ARGUMENTS_FIELD_NAME + " 参数");
} }
@@ -106,6 +120,7 @@ public class WebSocketServerTaskManager implements InitializingBean {
} }
if (tasker instanceof WebSocketServerTasker t) { if (tasker instanceof WebSocketServerTasker t) {
t.setSession(session);
t.setTitleHandler(title -> sendToSession(session, sessionId, "title", title)); t.setTitleHandler(title -> sendToSession(session, sessionId, "title", title));
t.setMessageHandler(msg -> sendMessageToSession(session, sessionId, msg)); t.setMessageHandler(msg -> sendMessageToSession(session, sessionId, msg));
t.setPropertyHandler((name, value) -> { t.setPropertyHandler((name, value) -> {
@@ -125,7 +140,6 @@ public class WebSocketServerTaskManager implements InitializingBean {
callable.call(); callable.call();
sendToSession(session, sessionId, "done"); sendToSession(session, sessionId, "done");
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
@@ -133,45 +147,36 @@ public class WebSocketServerTaskManager implements InitializingBean {
} }
private boolean sendMessageToSession(WebSocketSession session, String sessionId, Message msg) { private boolean sendMessageToSession(SessionInfo session, String sessionId, Message msg) {
return sendToSession(session, sessionId, "message", msg.getLevel().getName(), msg.getMessage()); return sendToSession(session, sessionId, "message", msg.getLevel().getName(), msg.getMessage());
} }
private boolean sendToSession(WebSocketSession session, String sessionId, String type, Object... args) { private boolean sendToSession(SessionInfo session, String sessionId, String type, Object... args) {
try { try {
String text = objectMapper.writeValueAsString(Map.of( session.send(Map.of(
WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId, WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId,
"type", type, "type", type,
WebSocketConstant.ARGUMENTS_FIELD_NAME, args)); WebSocketConstant.ARGUMENTS_FIELD_NAME, args));
session.sendMessage(new TextMessage(text)); return true;
} catch (IOException e) { } catch (IOException e) {
// 捕获所有可能的异常防止影响主流程 logger.warn("发送消息失败 (消息ID: {}): {}", sessionId, e.getMessage(), e);
logger.error("发送错误消息失败 (会话ID: {})", session.getId(), e); return false;
} }
return true;
} }
private void sendError(WebSocketSession session, String sessionId, String message) { private void send(SessionInfo session, String sessionId, Object data) {
if (session == null || !session.isOpen()) { Map<String, Object> map = new HashMap<>();
logger.warn("尝试向已关闭的WebSocket会话发送错误消息: {}", message); if (data instanceof Voable<?>) {
return; map.put("data", ((Voable<?>) data).toVo());
} else {
map.put("data", data);
} }
map.put(WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId);
map.put(WebSocketConstant.SUCCESS_FIELD_NAME, true);
try { try {
String errorMessage = objectMapper.writeValueAsString(Map.of( session.send(map);
WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId, } catch (IOException e) {
WebSocketConstant.SUCCESS_FIELD_VALUE, false, logger.warn("发送消息失败 (消息ID: {}): {}", sessionId, e.getMessage(), e);
WebSocketConstant.MESSAGE_FIELD_NAME, message));
// 检查会话状态并尝试发送错误消息
if (session.isOpen()) {
session.sendMessage(new TextMessage(errorMessage));
} else {
logger.warn("会话已关闭,无法发送错误消息: {}", message);
}
} catch (Exception e) {
// 捕获所有可能的异常防止影响主流程
logger.error("发送错误消息失败 (会话ID: {})", session.getId(), e);
} }
} }
} }

View File

@@ -1,13 +1,19 @@
package com.ecep.contract.service; package com.ecep.contract.service.tasker;
import com.ecep.contract.Message;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import com.ecep.contract.Message;
import com.ecep.contract.handler.SessionInfo;
import com.fasterxml.jackson.databind.JsonNode;
public interface WebSocketServerTasker extends Callable<Object> { public interface WebSocketServerTasker extends Callable<Object> {
void init(JsonNode argsNode);
default void setSession(SessionInfo session) {
}
/** /**
* 设置消息处理函数 * 设置消息处理函数
*/ */
@@ -17,7 +23,6 @@ public interface WebSocketServerTasker extends Callable<Object> {
void setPropertyHandler(BiConsumer<String, Object> propertyHandler); void setPropertyHandler(BiConsumer<String, Object> propertyHandler);
void init(JsonNode argsNode);
void setProgressHandler(BiConsumer<Long, Long> progressHandler); void setProgressHandler(BiConsumer<Long, Long> progressHandler);
} }

View File

@@ -0,0 +1,123 @@
package com.ecep.contract.util;
import java.util.HashMap;
import java.util.Locale;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.company.service.CompanyFileService;
import com.ecep.contract.ds.company.service.CompanyFileTypeService;
import com.ecep.contract.ds.company.service.CompanyService;
import com.ecep.contract.ds.contract.service.ContractBidVendorService;
import com.ecep.contract.ds.contract.service.ContractFileService;
import com.ecep.contract.ds.contract.service.ContractFileTypeService;
import com.ecep.contract.ds.contract.service.ContractService;
import com.ecep.contract.ds.customer.service.CompanyCustomerFileService;
import com.ecep.contract.ds.customer.service.CompanyCustomerFileTypeService;
import com.ecep.contract.ds.customer.service.CompanyCustomerService;
import com.ecep.contract.ds.other.service.EmployeeService;
import com.ecep.contract.ds.project.service.ProjectFileService;
import com.ecep.contract.ds.project.service.ProjectFileTypeService;
import com.ecep.contract.ds.project.service.ProjectService;
import com.ecep.contract.ds.vendor.service.VendorFileService;
import com.ecep.contract.ds.vendor.service.VendorFileTypeService;
import com.ecep.contract.ds.vendor.service.VendorGroupService;
import com.ecep.contract.ds.vendor.service.VendorService;
import lombok.Getter;
import lombok.Setter;
public class VerifyContext {
@Getter
@Setter
private Locale locale = Locale.getDefault();
private HashMap<Class<?>, Object> cachedMap = new HashMap<>();
public <K> K getBean(Class<K> requiredType) {
@SuppressWarnings("unchecked")
K bean = (K) cachedMap.get(requiredType);
if (bean == null) {
bean = SpringApp.getBean(requiredType);
cachedMap.put(requiredType, bean);
}
return bean;
}
// Employee
protected EmployeeService getEmployeeService() {
return getBean(EmployeeService.class);
}
// Project
protected ProjectService getProjectService() {
return getBean(ProjectService.class);
}
protected ProjectFileService getProjectFileService() {
return getBean(ProjectFileService.class);
}
protected ProjectFileTypeService getProjectFileTypeService() {
return getBean(ProjectFileTypeService.class);
}
// Contract
protected ContractService getContractService() {
return getBean(ContractService.class);
}
protected ContractFileTypeService getContractFileTypeService() {
return getBean(ContractFileTypeService.class);
}
protected ContractFileService getContractFileService() {
return getBean(ContractFileService.class);
}
protected ContractBidVendorService getContractBidVendorService() {
return getBean(ContractBidVendorService.class);
}
// Company
protected CompanyService getCompanyService() {
return getBean(CompanyService.class);
}
protected CompanyFileTypeService getCompanyFileTypeService() {
return getBean(CompanyFileTypeService.class);
}
protected CompanyFileService getCompanyFileService() {
return getBean(CompanyFileService.class);
}
// Vendor
protected VendorService getVendorService() {
return getBean(VendorService.class);
}
protected VendorGroupService getVendorGroupService() {
return getBean(VendorGroupService.class);
}
protected VendorFileTypeService getVendorFileTypeService() {
return getBean(VendorFileTypeService.class);
}
protected VendorFileService getVendorFileService() {
return getBean(VendorFileService.class);
}
// Customer
protected CompanyCustomerService getCompanyCustomerService() {
return getBean(CompanyCustomerService.class);
}
protected CompanyCustomerFileTypeService getCompanyCustomerFileTypeService() {
return getBean(CompanyCustomerFileTypeService.class);
}
protected CompanyCustomerFileService getCompanyCustomerFileService() {
return getBean(CompanyCustomerFileService.class);
}
}

View File

@@ -42,17 +42,14 @@ logging.level.org.springframework.web.servlet.mvc.method.annotation=TRACE
# Redis缓存配置 # Redis缓存配置
spring.cache.type=redis spring.cache.type=redis
spring.cache.redis.time-to-live=6h spring.cache.redis.time-to-live=1h
spring.cache.redis.key-prefix=contract_manager_ spring.cache.redis.key-prefix=cms::
spring.cache.redis.use-key-prefix=true spring.cache.redis.use-key-prefix=true
spring.cache.redis.cache-null-values=true spring.cache.redis.cache-null-values=true
# Redis序列化配置
spring.data.redis.serializer.key=org.springframework.data.redis.serializer.StringRedisSerializer
spring.data.redis.serializer.value=org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
# 禁用默认的Whitelabel错误页面 # 禁用默认的Whitelabel错误页面
server.error.whitelabel.enabled=false server.error.whitelabel.enabled=false
# 设置错误处理路径确保404等错误能被全局异常处理器捕获 # 设置错误处理路径确保404等错误能被全局异常处理器捕获
spring.mvc.throw-exception-if-no-handler-found=true
spring.web.resources.add-mappings=true spring.web.resources.add-mappings=true

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,13 +1,15 @@
{ {
"taskers": { "tasks": {
"ContractSyncTask": "com.ecep.contract.cloud.u8.ContractSyncTask", "ContractSyncTask": "com.ecep.contract.cloud.u8.ContractSyncTask",
"ContractRepairTask": "com.ecep.contract.ds.contract.tasker.ContractRepairTask", "ContractRepairTask": "com.ecep.contract.service.tasker.ContractRepairTasker",
"ContractVerifyTask": "com.ecep.contract.ds.contract.tasker.ContractVerifyTask", "ContractVerifyTasker": "com.ecep.contract.service.tasker.ContractVerifyTasker",
"ProjectCostImportItemsFromContractsTasker": "com.ecep.contract.ds.project.ProjectCostImportItemsFromContractsTasker", "ProjectCostImportItemsFromContractsTasker": "com.ecep.contract.service.tasker.ProjectCostImportItemsFromContractsTasker",
"CompanyCustomerEvaluationFormUpdateTask": "com.ecep.contract.ds.customer.tasker.CompanyCustomerEvaluationFormUpdateTask", "CompanyCustomerEvaluationFormUpdateTask": "com.ecep.contract.service.tasker.CompanyCustomerEvaluationFormUpdateTask",
"CompanyCustomerNextSignDateTask": "com.ecep.contract.ds.customer.tasker.CompanyCustomerNextSignDateTask", "CompanyCustomerNextSignDateTask": "com.ecep.contract.service.tasker.CompanyCustomerNextSignDateTask",
"CompanyCustomerRebuildFilesTasker": "com.ecep.contract.ds.customer.tasker.CompanyCustomerRebuildFilesTasker", "CompanyCustomerRebuildFilesTasker": "com.ecep.contract.service.tasker.CompanyCustomerRebuildFilesTasker",
"CustomerFileMoveTasker": "com.ecep.contract.ds.customer.tasker.CustomerFileMoveTasker" "CustomerFileMoveTasker": "com.ecep.contract.service.tasker.CustomerFileMoveTasker",
}, "CompanyCompositeUpdateTasker": "com.ecep.contract.service.tasker.CompanyCompositeUpdateTasker",
"descriptions": "任务注册信息" "CompanyVerifyTasker": "com.ecep.contract.service.tasker.CompanyVerifyTasker"
},
"descriptions": "任务注册信息"
} }