refactor(service): 修改IEntityService泛型为VO类型并优化缓存策略

重构所有注解@CacheConfig的Service类,将IEntityService泛型从实体类改为VO类
实现实体与VO之间的转换逻辑,使用VO替代实体进行缓存以避免序列化问题
更新相关依赖组件和测试用例,确保功能完整性和系统兼容性
优化Redis缓存配置,清理旧缓存数据并验证新缓存策略有效性
This commit is contained in:
2025-09-28 18:19:00 +08:00
parent df6188db40
commit b03b5385a5
75 changed files with 3144 additions and 1377 deletions

View File

@@ -6,12 +6,12 @@
<parent>
<groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId>
<version>0.0.86-SNAPSHOT</version>
<version>0.0.99-SNAPSHOT</version>
</parent>
<groupId>com.ecep.contract</groupId>
<artifactId>client</artifactId>
<version>0.0.86-SNAPSHOT</version>
<version>0.0.99-SNAPSHOT</version>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
@@ -22,7 +22,7 @@
<dependency>
<groupId>com.ecep.contract</groupId>
<artifactId>common</artifactId>
<version>0.0.86-SNAPSHOT</version>
<version>0.0.99-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
@@ -120,6 +120,23 @@
</execution>
</executions>
</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>
</build>

View File

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

View File

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

View File

@@ -1,22 +1,192 @@
package com.ecep.contract;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.File;
import java.io.FileInputStream;
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.util.StringUtils;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import lombok.Getter;
import lombok.Setter;
/**
* 应用程序配置类,用于管理系统配置信息
*/
@Component
@ConfigurationProperties(prefix = "my")
public class MyProperties {
public class MyProperties implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(MyProperties.class);
private static final String FILE_NAME = "config.properties";
@Getter
@Setter
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() {
String downloadsPath = getDownloadsPath();
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");
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();
logger.debug("SpringApp.launch application.run().");
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);
});
CompletableFuture.runAsync(() -> {

View File

@@ -118,10 +118,24 @@ public class WebSocketClientService {
String errorMsg = node.get(WebSocketConstant.MESSAGE_FIELD_NAME).asText();
logger.error("收到错误消息: 错误码={}, 错误信息={}", errorCode, errorMsg);
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();
controller.setProperties(SpringApp.getBean(MyProperties.class));
controller.tryLogin();
// 需要把窗口顶置
isActive = true;
scheduleReconnect();
}
return;
}
@@ -164,8 +178,8 @@ public class WebSocketClientService {
};
private void onCallbackMessage(CompletableFuture<JsonNode> future, JsonNode node) {
if (node.has(WebSocketConstant.SUCCESS_FIELD_VALUE)) {
if (!node.get(WebSocketConstant.SUCCESS_FIELD_VALUE).asBoolean()) {
if (node.has(WebSocketConstant.SUCCESS_FIELD_NAME)) {
if (!node.get(WebSocketConstant.SUCCESS_FIELD_NAME).asBoolean()) {
future.completeExceptionally(
new RuntimeException(
"请求失败:来自服务器的消息=" + node.get(WebSocketConstant.MESSAGE_FIELD_NAME).asText()));
@@ -204,7 +218,7 @@ public class WebSocketClientService {
String json = objectMapper.writeValueAsString(msg);
callbacks.put(msg.getMessageId(), future);
if (webSocket.send(json)) {
logger.debug("send message success:{}", json);
logger.debug("send json success:{}", json);
} else {
if (isActive) {
future.completeExceptionally(new RuntimeException("Failed to send WebSocket message"));

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.util.StringConverter;
import javafx.util.converter.CurrencyStringConverter;
import javafx.util.converter.NumberStringConverter;
import lombok.Setter;
@@ -99,12 +100,24 @@ public class ContractTabSkinItemsV2
CurrencyStringConverter currencyStringConverter = new CurrencyStringConverter(numberInstance);
idColumn.setCellValueFactory(param -> param.getValue().getId());
titleColumn.setCellValueFactory(param -> param.getValue().getTitle());
specificationColumn.setCellValueFactory(param -> param.getValue().getSpecification());
unitColumn.setCellValueFactory(param -> param.getValue().getUnit());
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.setCellFactory(TextFieldTableCell.forTableColumn(currencyStringConverter));

View File

@@ -7,6 +7,7 @@ import com.ecep.contract.vo.InventoryCatalogVo;
import com.ecep.contract.vo.InventoryVo;
import javafx.util.Callback;
import javafx.util.StringConverter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@@ -21,6 +22,23 @@ public class InventoryTableCell<V> extends AsyncUpdateTableCell<V, Integer, Inve
InventoryService 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;

View File

@@ -3,6 +3,10 @@ package com.ecep.contract.controller.table.cell;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
import com.ecep.contract.MyDateTimeUtils;
/**
@@ -11,6 +15,40 @@ import com.ecep.contract.MyDateTimeUtils;
public class LocalDateTimeTableCell<T>
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;
public LocalDateTimeTableCell(DateTimeFormatter formatter) {
@@ -34,4 +72,5 @@ public class LocalDateTimeTableCell<T>
setText(item.format(formatter));
}
}
}

View File

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

View File

@@ -1,18 +1,32 @@
package com.ecep.contract.task;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
import com.ecep.contract.vo.ContractVo;
import lombok.Getter;
import lombok.Setter;
public class ContractVerifyTasker extends Tasker<Object> {
public class ContractVerifyTasker extends Tasker<Object> implements WebSocketClientTasker {
@Setter
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
protected Object execute(MessageHolder holder) throws Exception {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'execute'");
return callRemoteTask(holder, getLocale(), contract.getId());
}
}

View File

@@ -3,8 +3,8 @@ package com.ecep.contract.vm;
import com.ecep.contract.Desktop;
import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.SpringApp;
import com.ecep.contract.WebSocketClientService;
import com.ecep.contract.controller.CurrentEmployeeInitialedEvent;
import com.ecep.contract.model.EmployeeRole;
import com.ecep.contract.service.EmployeeService;
import com.ecep.contract.vo.EmployeeRoleVo;
import com.ecep.contract.vo.EmployeeVo;
@@ -138,11 +138,34 @@ public class CurrentEmployee extends EmployeeViewModel {
* 3. 更新当前用户的信息
* 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);
EmployeeVo employee = employeeService.findById(getId().get());
List<EmployeeRoleVo> roles = employeeService.getRolesByEmployeeId(getId().get());
WebSocketClientService webSocketService = SpringApp.getBean(WebSocketClientService.class);
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(() -> {
update(employee);
rolesProperty().setAll(roles);
@@ -151,9 +174,9 @@ public class CurrentEmployee extends EmployeeViewModel {
future.complete(null);
});
});
/**
* 定时更新用户活动状态
*/
// 定时更新用户活动状态
executorService.scheduleWithFixedDelay(() -> {
try {
// SpringApp.getBean(EmployeeService.class).updateActive(Desktop.instance.getSessionId());

View File

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

View File

@@ -3,7 +3,7 @@ package com.ecep.contract.constant;
public class WebSocketConstant {
public static final String MESSAGE_ID_FIELD_NAME = "messageId";
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 ERROR_CODE_FIELD_NAME = "errorCode";
@@ -13,5 +13,9 @@ public class WebSocketConstant {
public static final String SESSION_ID_FIELD_NAME = "sessionId";
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
@Entity
@Table(name = "COMPANY_CUSTOMER_FILE_TYPE_LOCAL")
@ToString
@ToString(callSuper = true)
public class CompanyCustomerFileTypeLocal extends BaseEnumEntity<CustomerFileType> implements Serializable, Voable<CompanyCustomerFileTypeLocalVo> {
private static final long serialVersionUID = 1L;

View File

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

View File

@@ -5,6 +5,8 @@ import java.time.LocalDate;
import java.util.Objects;
import com.ecep.contract.ContractFileType;
import com.ecep.contract.model.Voable;
import com.ecep.contract.vo.ContractFileVo;
import com.ecep.contract.util.HibernateProxyUtils;
import jakarta.persistence.Column;
@@ -30,7 +32,7 @@ import lombok.ToString;
@Entity
@Table(name = "CONTRACT_FILE")
@ToString
public class ContractFile implements IdentityEntity, ContractBasedEntity, Serializable {
public class ContractFile implements IdentityEntity, ContractBasedEntity, Serializable, Voable<ContractFileVo> {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -82,4 +84,23 @@ public class ContractFile implements IdentityEntity, ContractBasedEntity, Serial
public final int hashCode() {
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
@Entity
@Table(name = "CONTRACT_FILE_TYPE_LOCAL")
@ToString
@ToString(callSuper = true)
public class ContractFileTypeLocal extends BaseEnumEntity<ContractFileType> implements Serializable, Voable<ContractFileTypeLocalVo> {
private static final long serialVersionUID = 1L;
/**

View File

@@ -5,6 +5,8 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Objects;
import com.ecep.contract.model.Voable;
import com.ecep.contract.vo.InventoryVo;
import com.ecep.contract.util.HibernateProxyUtils;
import jakarta.persistence.AttributeOverride;
@@ -30,7 +32,7 @@ import lombok.ToString;
@Setter
@Entity
@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;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -184,4 +186,74 @@ public class Inventory implements IdentityEntity, BasedEntity, Serializable {
public final int hashCode() {
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;
/**
* 发票
* 收到的公司发票
*/
@Getter
@Setter

View File

@@ -65,6 +65,12 @@ public class ProjectType
@Override
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

@@ -1,16 +1,7 @@
#Updated config.properties
#Wed Mar 26 16:33:45 CST 2025
# 日志配置
logging.level.com.ecep.contract=DEBUG
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
#Contract Manager \u5E94\u7528\u7A0B\u5E8F\u914D\u7F6E
#Sat Sep 27 00:34:02 CST 2025
server.host=127.0.0.1
server.port=8080
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 其他支持
- [ ] 与相关团队的协调和沟通
- [ ] 变更管理和审批流程支持
- [ ] 风险评估和缓解策略建议
---
**更新记录**:
- 创建日期: -
- 更新日期: -
- 更新内容: -

34
pom.xml
View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId>
<version>0.0.86-SNAPSHOT</version>
<version>0.0.99-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>server</module>
@@ -109,37 +109,7 @@
</distributionManagement>
<build>
<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>
</build>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +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;
import static com.ecep.contract.SpringApp.getBean;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
@@ -31,13 +29,11 @@ import org.springframework.util.StringUtils;
import com.ecep.contract.BlackReasonType;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.cloud.AbstractCtx;
import com.ecep.contract.cloud.rk.CloudRkService;
import com.ecep.contract.ds.company.service.CompanyBlackReasonService;
import com.ecep.contract.ds.company.service.CompanyContactService;
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.Company;
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.node.ObjectNode;
import lombok.Setter;
public class CloudRkCtx extends AbstractCtx {
public class CloudRkCtx extends AbstractCtx implements CloudRkContext {
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;
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() {
if (companyBlackReasonService == null) {
companyBlackReasonService = getBean(CompanyBlackReasonService.class);
}
return companyBlackReasonService;
return getCachedBean(CompanyBlackReasonService.class);
}
ObjectMapper getObjectMapper() {
if (objectMapper == null) {
objectMapper = getBean(ObjectMapper.class);
}
return objectMapper;
return getCachedBean(ObjectMapper.class);
}
Proxy getSocksProxy() {
@@ -150,7 +116,11 @@ public class CloudRkCtx extends AbstractCtx {
} catch (Exception 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;
}
@@ -172,11 +142,12 @@ public class CloudRkCtx extends AbstractCtx {
}
}
CompanyBlackReasonService companyBlackReasonService = getCompanyBlackReasonService();
String api = getConfService().getString(CloudRkService.KEY_BLACK_LIST_URL);
List<String> companyNames = getCompanyService().getAllNames(company);
List<CompanyBlackReason> reasonList = new ArrayList<>();
List<CompanyBlackReason> dbReasons = getCompanyBlackReasonService().findAllByCompany(company);
List<CompanyBlackReason> dbReasons = companyBlackReasonService.findAllByCompany(company);
for (String name : companyNames) {
String url = api + URLEncoder.encode(name, StandardCharsets.UTF_8);
try {
@@ -195,7 +166,7 @@ public class CloudRkCtx extends AbstractCtx {
}
for (CompanyBlackReason companyBlackReason : reasonList) {
getCompanyBlackReasonService().save(companyBlackReason);
companyBlackReasonService.save(companyBlackReason);
}
cloudRk.setCloudBlackListUpdated(LocalDateTime.now());
return true;
@@ -634,14 +605,15 @@ public class CloudRkCtx extends AbstractCtx {
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) {
JsonNode node = data.get(field);
JsonNode node = data.get(field);
if (node == null || node.isNull()) {
return false;
}
LocalDateTime updated = getObjectMapper().convertValue(node, LocalDateTime.class);
updateLocalDateTime(getter, setter, updated, holder, topic);
LocalDateTime updated = getObjectMapper().convertValue(node, LocalDateTime.class);
updateLocalDateTime(getter, setter, updated, holder, topic);
return false;
}
@@ -654,7 +626,7 @@ public class CloudRkCtx extends AbstractCtx {
return;
}
CompanyContactService contactService = SpringApp.getBean(CompanyContactService.class);
CompanyContactService contactService = getCachedBean(CompanyContactService.class);
List<CompanyContact> contactList = contactService.findAllByCompanyAndName(company, legalRepresentative);
if (contactList == null) {
// db error
@@ -746,7 +718,7 @@ public class CloudRkCtx extends AbstractCtx {
historyNames.add(trimmed);
}
}
CompanyOldNameService service = SpringApp.getBean(CompanyOldNameService.class);
CompanyOldNameService service = getCachedBean(CompanyOldNameService.class);
List<CompanyOldName> oldNames = service.findAllByCompany(company);
for (CompanyOldName oldName : oldNames) {
// 已经存在的移除

View File

@@ -6,10 +6,9 @@ import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
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 org.slf4j.Logger;
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() {
return getCachedBean(CompanyCustomerService.class);
}

View File

@@ -6,6 +6,7 @@ import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import com.ecep.contract.ds.company.CompanyContext;
import org.springframework.util.StringUtils;
import com.ecep.contract.MessageHolder;
@@ -18,17 +19,7 @@ import com.ecep.contract.model.CompanyOldName;
import lombok.Setter;
public class CompanyCtx extends AbstractYongYouU8Ctx {
@Setter
private CompanyOldNameService companyOldNameService;
CompanyOldNameService getCompanyOldNameService() {
if (companyOldNameService == null) {
companyOldNameService = getBean(CompanyOldNameService.class);
}
return companyOldNameService;
}
public class CompanyCtx extends AbstractYongYouU8Ctx implements CompanyContext {
public boolean updateCompanyNameIfAbsent(Company company, String name, MessageHolder holder) {
if (!StringUtils.hasText(name)) {
return false;
@@ -110,7 +101,7 @@ public class CompanyCtx extends AbstractYongYouU8Ctx {
return null;
}
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)) {
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.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
import org.hibernate.Hibernate;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import com.ecep.contract.model.IdentityEntity;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonToken;
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.SerializationFeature;
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.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
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.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
@@ -43,7 +45,6 @@ public class JacksonConfig {
@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.build();
// 关闭日期时间格式化输出为时间戳而是输出为ISO格式的字符串
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
@@ -52,6 +53,9 @@ public class JacksonConfig {
// 处理循环引用,启用引用处理功能
objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
// 禁用在遇到空Bean时抛出异常
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// 配置Java 8时间模块的序列化格式
JavaTimeModule javaTimeModule = new JavaTimeModule();
// LocalDate
@@ -63,154 +67,23 @@ public class JacksonConfig {
// LocalDateTime
javaTimeModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
javaTimeModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
// 自定义LocalDateTime反序列化器支持多种格式
javaTimeModule.addDeserializer(LocalDateTime.class, new CustomLocalDateTimeDeserializer());
objectMapper.registerModule(javaTimeModule);
// 添加自定义模块用于处理JPA/Hibernate代理对象
SimpleModule proxyModule = new SimpleModule("HibernateProxyModule");
// SimpleModule proxyModule = new SimpleModule("HibernateProxyModule");
// 添加代理对象序列化器只输出ID字段
proxyModule.addSerializer(HibernateProxy.class, new HibernateProxySerializer());
// 使用BeanSerializerModifier来处理IdentityEntity类型避免递归调用
proxyModule.setSerializerModifier(new BeanSerializerModifier() {
@Override
public JsonSerializer<?> modifySerializer(SerializationConfig config,
BeanDescription beanDesc,
JsonSerializer<?> serializer) {
// 只对IdentityEntity类型进行修改
if (IdentityEntity.class.isAssignableFrom(beanDesc.getBeanClass()) &&
!HibernateProxy.class.isAssignableFrom(beanDesc.getBeanClass())) {
return new IdentityEntitySerializer(serializer);
}
return serializer;
}
});
// // 添加懒加载初始化器序列化器
// proxyModule.addSerializer(LazyInitializer.class, new
// HibernateProxySerializer());
// // 添加Hibernate代理对象反序列化器
// proxyModule.addDeserializer(HibernateProxy.class, new
// HibernateProxyDeserializer());
objectMapper.registerModule(proxyModule);
// objectMapper.registerModule(proxyModule);
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绑定认证
// 这里使用密码编码器加密后的固定密码,确保认证流程能够正常工作
return User.builder()
.username(employee.getName())
.username(employee.getAccount())
.password(passwordEncoder().encode("default123")) // 使用默认密码进行加密
.accountExpired(false) // 账户未过期
.accountLocked(false) // 账户未锁定

View File

@@ -1,9 +1,17 @@
package com.ecep.contract.controller;
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.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("/")
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

@@ -1,4 +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

@@ -83,7 +83,7 @@ public class ContractFileTypeService
}
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "#p0.id"), @CacheEvict(key = "'by-type-'+#p0.type.name()+'-'+#p0.lang"),
@CacheEvict(key = "'all-'+#p0.getLang()")
})
@Override
@@ -93,6 +93,7 @@ public class ContractFileTypeService
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "'by-type-'+#p0.type.name()+'-'+#p0.lang"),
@CacheEvict(key = "'all-'+#p0.getLang()")
})
@Override
@@ -117,4 +118,8 @@ public class ContractFileTypeService
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

@@ -9,7 +9,6 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -29,7 +28,7 @@ import com.ecep.contract.ds.company.service.CompanyExtendInfoService;
import com.ecep.contract.ds.contract.service.ExtendVendorInfoService;
import com.ecep.contract.ds.converter.NumberStringConverter;
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.ProjectCostService;
import com.ecep.contract.ds.project.service.ProjectQuotationService;
@@ -197,7 +196,7 @@ public class ContractVerifyComm extends VerifyContext {
/**
* 验证合同是否合规
*
*
* @param company
* @param contract
* @param holder
@@ -227,7 +226,9 @@ public class ContractVerifyComm extends VerifyContext {
case RECEIVE -> {
// 销售合同
CompanyExtendInfo companyExtendInfo = getCompanyExtendInfoService().findByCompany(company);
verifyAsCustomer(company, companyExtendInfo, contract, holder);
if (!verifyAsCustomer(company, companyExtendInfo, contract, holder)) {
passed = false;
}
// 销售合同下的采购合同
List<Contract> list = getContractService().findAllByParent(contract);
@@ -325,7 +326,7 @@ public class ContractVerifyComm extends VerifyContext {
}
private void verifyVendorFile(VendorGroup group, boolean assignedProvider, Contract contract,
MessageHolder holder) {
MessageHolder holder) {
if (group == null) {
return;
}
@@ -400,7 +401,7 @@ public class ContractVerifyComm extends VerifyContext {
}
String getFileTypeLocalValue(ContractFileType type) {
ContractFileTypeLocal fileTypeLocal = getFileTypeLocal(type);
ContractFileTypeLocal fileTypeLocal = getContractFileTypeService().findByTypeAndLang(type, getLocale());
if (fileTypeLocal == null) {
return type.name();
}
@@ -420,18 +421,20 @@ public class ContractVerifyComm extends VerifyContext {
}
private boolean verifyAsCustomer(Company company, CompanyExtendInfo companyExtendInfo, Contract contract,
MessageHolder holder) {
MessageHolder holder) {
boolean passed = true;
Project project = contract.getProject();
if (project == null) {
// 收款的合同时,检查是否关联了项目,如果没有则创建
if (contract.getPayWay() == ContractPayWay.RECEIVE) {
holder.debug("未关联项目,测试关联/创建项目...");
project = getProjectService().findByCode(contract.getCode());
if (project == null) {
holder.info("创建关联项目");
holder.info("根据合同号 " + contract.getCode() + ", 未找到相关项目, 创建相关项目");
try {
project = getProjectService().newInstanceByContract(contract);
project = getProjectService().save(project);
holder.info("创建关联项目成功:" + project.getCode() + " " + project.getName());
} catch (Exception e) {
holder.error("创建关联项目失败: " + e.getMessage());
throw new RuntimeException("code=" + contract.getCode(), e);
@@ -439,20 +442,26 @@ public class ContractVerifyComm extends VerifyContext {
}
contract.setProject(project);
contract = getContractService().save(contract);
holder.info("关联项目:" + project.getCode() + " " + project.getName());
} else {
holder.warn("未关联项目");
}
}
// fixed no hibernate session
if (project != null) {
if (!Hibernate.isInitialized(project)) {
project = getProjectService().findById(project.getId());
// fixed
contract.setProject(project);
}
}
if (project != null) {
holder.info("验证项目信息:" + project.getCode() + " " + project.getName());
verifyProject(contract, project, holder.sub("项目"));
holder.info("验证项目:" + project.getCode() + " " + project.getName());
if (!verifyProject(contract, project, holder.sub("项目"))) {
passed = false;
}
ProjectSaleType saleType = project.getSaleType();
if (saleType != null) {
@@ -463,6 +472,7 @@ public class ContractVerifyComm extends VerifyContext {
}
if (!contract.getPath().startsWith(saleType.getPath())) {
holder.error("合同目录未在销售类型目录下");
passed = false;
}
}
}
@@ -474,7 +484,7 @@ public class ContractVerifyComm extends VerifyContext {
}
if (verifyCustomerFiles) {
holder.debug("核验文件...");
holder.info("核验客户文件...");
if (!verifyContractFileAsCustomer(project, contract, holder)) {
passed = false;
}
@@ -519,7 +529,7 @@ public class ContractVerifyComm extends VerifyContext {
}
private boolean verifyCustomerFileByContract(CompanyCustomer companyCustomer, Contract contract,
MessageHolder holder) {
MessageHolder holder) {
List<LocalDate> verifyDates = new ArrayList<>();
LocalDate minDate = LocalDate.of(2022, 1, 1);
LocalDate developDate = companyCustomer.getDevelopDate();
@@ -605,7 +615,11 @@ public class ContractVerifyComm extends VerifyContext {
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();
if (saleType == null) {
String code = contract.getCode();
@@ -619,6 +633,7 @@ public class ContractVerifyComm extends VerifyContext {
}
if (project.getAmount() == null || project.getAmount() <= 0) {
holder.error("金额小于等于0");
passed = false;
}
//
@@ -709,6 +724,7 @@ public class ContractVerifyComm extends VerifyContext {
holder.warn("报价未创建");
}
}
return passed;
}
private boolean verifyContractFileAsCustomer(Project project, Contract contract, MessageHolder holder) {

View File

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

View File

@@ -16,7 +16,7 @@ public interface BaseEnumEntityRepository<N extends Enum<?>, T extends BaseEnumE
List<T> findAllByLang(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)) {
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.ds.other.repository.DepartmentRepository;
import com.ecep.contract.model.Department;
import com.ecep.contract.model.Employee;
import com.ecep.contract.service.VoableService;
import com.ecep.contract.vo.DepartmentVo;
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;
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.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -18,45 +12,34 @@ import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PingMessage;
import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
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.ds.other.service.EmployeeLoginHistoryService;
import com.ecep.contract.ds.other.service.EmployeeService;
import com.ecep.contract.model.Employee;
import com.ecep.contract.model.EmployeeLoginHistory;
import com.ecep.contract.model.Voable;
import com.ecep.contract.service.VoableService;
import com.ecep.contract.service.WebSocketServerTaskManager;
import com.ecep.contract.service.WebSocketServerCallbackManager;
import com.ecep.contract.service.tasker.WebSocketServerTaskManager;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
/**
* WebSocket处理器
* 处理与客户端的WebSocket连接、消息传递和断开连接
*/
@Component
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 ObjectMapper objectMapper;
@@ -69,26 +52,12 @@ public class WebSocketServerHandler extends TextWebSocketHandler {
private ScheduledExecutorService scheduledExecutorService;
@Autowired
private WebSocketServerTaskManager taskManager;
@Autowired
private WebSocketServerCallbackManager callbackManager;
// 存储所有活跃的WebSocket会话
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) {
this.objectMapper = objectMapper;
this.authenticationManager = authenticationManager;
@@ -101,6 +70,7 @@ public class WebSocketServerHandler extends TextWebSocketHandler {
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 添加会话到活跃会话集合
SessionInfo sessionInfo = new SessionInfo();
sessionInfo.setObjectMapper(objectMapper);
sessionInfo.setSession(session);
sessionInfo.setLoginHistoryId((Integer) session.getAttributes().get("loginHistoryId"));
sessionInfo.setEmployeeId((Integer) session.getAttributes().get("employeeId"));
@@ -108,7 +78,7 @@ public class WebSocketServerHandler extends TextWebSocketHandler {
if (sessionInfo.getEmployeeId() == null) {
logger.error("会话未绑定用户: {}", session.getId());
sendError(session, WebSocketConstant.ERROR_CODE_UNAUTHORIZED, "会话未绑定用户");
session.close(CloseStatus.NOT_ACCEPTABLE);
// session.close(CloseStatus.NOT_ACCEPTABLE);
return;
}
@@ -157,389 +127,49 @@ public class WebSocketServerHandler extends TextWebSocketHandler {
private boolean handleAsJson(WebSocketSession session, String payload) {
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;
}
JsonNode jsonNode = null;
try {
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) {
logger.warn("解析消息回调JSON失败: {}", payload, e);
return false;
}
if (jsonNode.has(WebSocketConstant.MESSAGE_ID_FIELD_NAME)) {
// 处理 messageId 的消息
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);
}
callbackManager.onMessage(sessionInfo, jsonNode);
return true;
}
if (jsonNode.has(WebSocketConstant.SESSION_ID_FIELD_NAME)) {
taskManager.onMessage(session, jsonNode);
taskManager.onMessage(sessionInfo, jsonNode);
return true;
}
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
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
logger.info("收到来自客户端的二进制消息: " + message.getPayload() + " (会话ID: " + session.getId() + ")");
@@ -642,7 +272,7 @@ public class WebSocketServerHandler extends TextWebSocketHandler {
try {
ObjectNode objectNode = objectMapper.createObjectNode();
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);
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,4 +1,4 @@
package com.ecep.contract.ds.company.tasker;
package com.ecep.contract.service.tasker;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.cloud.rk.CloudRkService;
@@ -13,15 +13,12 @@ import com.ecep.contract.constant.CloudServiceConstant;
import com.ecep.contract.model.CloudRk;
import com.ecep.contract.model.CloudYu;
import com.ecep.contract.model.Company;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.ecep.contract.util.MyStringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;

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;
@@ -32,7 +32,6 @@ import com.ecep.contract.model.Company;
import com.ecep.contract.model.CompanyCustomer;
import com.ecep.contract.model.CompanyCustomerEvaluationFormFile;
import com.ecep.contract.model.CompanyCustomerFile;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.ecep.contract.util.CompanyUtils;
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.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.CompanyCustomerFile;
import com.ecep.contract.model.Contract;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
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.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.model.Company;
import com.ecep.contract.model.CompanyCustomer;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;

View File

@@ -1,18 +1,14 @@
package com.ecep.contract.ds.company.tasker;
package com.ecep.contract.service.tasker;
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
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.model.Company;
import com.ecep.contract.model.Contract;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;

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 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 org.springframework.util.StringUtils;
@@ -13,12 +13,11 @@ import com.ecep.contract.MessageHolder;
import com.ecep.contract.model.Contract;
import lombok.Getter;
import lombok.Setter;
/**
* 合同修复任务
*/
public class ContractRepairTask extends AbstContractRepairTasker implements WebSocketServerTasker {
public class ContractRepairTasker extends AbstContractRepairTasker implements WebSocketServerTasker {
@Getter
private Contract contract;
@Getter
@@ -27,7 +26,7 @@ public class ContractRepairTask extends AbstContractRepairTasker implements WebS
private final ContractRepairComm comm = new ContractRepairComm();
public ContractRepairTask() {
public ContractRepairTasker() {
}
public void init(JsonNode argsNode) {

View File

@@ -1,11 +1,11 @@
package com.ecep.contract.ds.contract.tasker;
package com.ecep.contract.service.tasker;
import java.util.Locale;
import com.ecep.contract.MessageHolder;
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.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;
@@ -24,15 +24,18 @@ public class ContractVerifyTasker extends Tasker<Object> implements WebSocketSer
@Override
protected Object execute(MessageHolder holder) {
updateTitle("验证合同 " + contract.getCode() + " 及其子合同是否符合合规要求");
updateProgress(1, 10);
if (!comm.verify(contract, holder)) {
passed = false;
}
updateProgress(9, 10);
if (passed) {
holder.info("合规验证通过");
} else {
holder.error("合规验证不通过");
}
updateProgress(10, 10);
updateProperty("passed", passed);
return null;
}

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.ds.company.service.CompanyService;
import com.ecep.contract.ds.customer.service.CompanyCustomerFileService;
import com.ecep.contract.ds.customer.service.CompanyCustomerService;
import com.ecep.contract.model.Company;
import com.ecep.contract.model.CompanyCustomer;
import com.ecep.contract.model.CompanyCustomerFile;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;

View File

@@ -1,4 +1,4 @@
package com.ecep.contract.ds.project;
package com.ecep.contract.service.tasker;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -26,7 +26,6 @@ import com.ecep.contract.model.Inventory;
import com.ecep.contract.model.Project;
import com.ecep.contract.model.ProjectCost;
import com.ecep.contract.model.ProjectCostItem;
import com.ecep.contract.service.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.ecep.contract.util.NumberUtils;
import com.ecep.contract.util.TaxRateUtils;

View File

@@ -1,4 +1,4 @@
package com.ecep.contract.service;
package com.ecep.contract.service.tasker;
import java.io.IOException;
import java.io.InputStream;
@@ -42,11 +42,17 @@ public class WebSocketServerTaskManager implements InitializingBean {
Resource resource = resourceLoader.getResource("classpath:tasker_mapper.json");
try (InputStream inputStream = resource.getInputStream()) {
JsonNode rootNode = objectMapper.readTree(inputStream);
JsonNode taskersNode = rootNode.get("taskers");
if (taskersNode != null && taskersNode.isObject()) {
JsonNode tasksNode = rootNode.get("tasks");
if (tasksNode != null && tasksNode.isObject()) {
Map<String, String> taskMap = new java.util.HashMap<>();
taskersNode.fields().forEachRemaining(entry -> {
tasksNode.fields().forEachRemaining(entry -> {
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;
}

View File

@@ -1,4 +1,4 @@
package com.ecep.contract.service;
package com.ecep.contract.service.tasker;
import java.util.concurrent.Callable;
import java.util.function.BiConsumer;

View File

@@ -42,17 +42,14 @@ logging.level.org.springframework.web.servlet.mvc.method.annotation=TRACE
# Redis缓存配置
spring.cache.type=redis
spring.cache.redis.time-to-live=6h
spring.cache.redis.key-prefix=contract_manager_
spring.cache.redis.time-to-live=1h
spring.cache.redis.key-prefix=cms::
spring.cache.redis.use-key-prefix=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错误页面
server.error.whitelabel.enabled=false
# 设置错误处理路径确保404等错误能被全局异常处理器捕获
spring.mvc.throw-exception-if-no-handler-found=true
spring.web.resources.add-mappings=true

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

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