Compare commits

...

3 Commits

Author SHA1 Message Date
c10bd369c0 up 2025-11-26 16:10:17 +08:00
f0e85c5a18 docs: 添加项目文档和架构设计文件
删除旧的package.json文件
添加天眼查下载信用报告文档
添加项目文档总览、架构设计、API文档、开发指南和数据库设计文档
2025-11-26 16:10:01 +08:00
a784438e97 feat: 实现员工同步任务的WebSocket支持及合同名称锁定功能
- 为EmployeesSyncTask添加WebSocket客户端和服务端支持,实现实时任务进度反馈
- 新增合同名称锁定功能,防止误修改重要合同名称
- 优化SmbFileService的连接异常处理,提高稳定性
- 重构ContractFilesRebuildTasker的任务执行逻辑,改进错误处理
- 更新tasker_mapper.json注册EmployeesSyncTask
- 添加相关任务文档和验收报告

修复WebSocketClientSession的任务完成状态处理问题
改进UITools中任务执行的线程管理
优化DepartmentService的findByCode方法返回类型
2025-11-20 16:26:34 +08:00
113 changed files with 2737 additions and 2803 deletions

View File

@@ -86,8 +86,8 @@ public interface WebSocketServerTasker extends Callable<Object> {
```json
{
"tasks": {
"CustomTasker": "com.ecep.contract.service.tasker.CustomTasker"
// 其他已注册任务...
"CustomTasker": "com.ecep.contract.service.tasker.CustomTasker",
"OtherTasker": "..."
},
"descriptions": "任务注册信息, 客户端的任务可以通过 WebSocket 调用"
}
@@ -137,6 +137,7 @@ WebSocketServerTaskManager类在启动时会读取`tasker_mapper.json`文件,
protected Object execute(MessageHolder holder) throws Exception {
// 调用父类初始化
super.execute(holder);
updateTitle("同步修复所有合同");
// 执行具体业务逻辑
repair(holder);
return null; // 通常返回 null 或处理结果
@@ -146,7 +147,6 @@ WebSocketServerTaskManager类在启动时会读取`tasker_mapper.json`文件,
5. **使用更新方法提供状态反馈**:在执行过程中使用 `updateTitle`、`updateProgress` 等方法提供实时反馈
```java
updateTitle("同步修复所有合同");
updateProgress(counter.incrementAndGet(), total);
```

View File

@@ -0,0 +1,182 @@
# Tasker实现规范指南
## 1. Tasker架构概述
Tasker是系统中用于执行异步任务的核心框架主要用于处理耗时操作并提供实时进度反馈。Tasker架构包括
- **Tasker基类**:提供任务执行的基础框架和通信功能
- **WebSocketServerTasker接口**定义WebSocket通信任务的规范
- **MessageHolder**:用于在任务执行过程中传递消息和更新进度
## 2. 服务器端Tasker实现规范
### 2.1 基本结构
服务器端Tasker实现应遵循以下结构
```java
@Data
@EqualsAndHashCode(callSuper = true)
public class YourTaskNameTask extends Tasker<Object> implements WebSocketServerTasker {
// 服务依赖
@Setter
private YourService service;
@Override
public void init(JsonNode argsNode) {
// 初始化任务标题
updateTitle("任务标题");
// 初始化参数
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 实现业务逻辑
// 使用holder.info()、holder.error()等方法反馈消息
return null;
}
}
```
### 2.2 关键注意事项
1. **继承与实现**
- 必须继承`Tasker<Object>`基类
- 必须实现`WebSocketServerTasker`接口
2. **方法实现**
- **不要重复实现**Tasker基类已提供的handler设置方法`setMessageHandler``setTitleHandler``setPropertyHandler``setProgressHandler`
- 必须实现`init`方法,设置任务标题和初始化参数
- 必须实现`execute`方法,实现具体业务逻辑
3. **进度和消息反馈**
- 使用`updateProgress`方法提供进度更新
- 使用`MessageHolder``info``error``debug`等方法提供消息反馈
4. **任务取消**
- 在适当的地方检查`isCancelled()`状态,支持任务取消
5. **注册要求**
- 任务类必须在`tasker_mapper.json`中注册
## 3. 客户端Tasker实现规范
客户端Tasker实现应遵循
```java
public class YourTaskNameTask extends Tasker<Object> {
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 客户端任务逻辑
return null;
}
}
```
## 4. 通信规范
### 4.1 命名规范
- 客户端和服务器端对应的Tasker类应使用**相同的类名**
- 示例:客户端的`ContractSyncAllTask`对应服务器端的`ContractSyncAllTask`
### 4.2 消息传递
- 使用`MessageHolder`进行消息传递
- 支持不同级别的消息INFO、ERROR、DEBUG
## 5. 最佳实践
1. **任务拆分**
- 将大型任务拆分为多个小任务,提高可维护性
2. **异常处理**
- 捕获并正确处理异常,使用`holder.error()`提供详细错误信息
3. **资源管理**
- 确保及时释放资源,特别是在任务取消的情况下
4. **日志记录**
- 使用`holder.debug()`记录调试信息
- 使用`holder.info()`记录进度信息
5. **状态检查**
- 定期检查`isCancelled()`状态,确保任务可以被及时取消
## 6. 代码示例
### 6.1 服务器端Tasker示例
```java
@Data
@EqualsAndHashCode(callSuper = true)
public class ExampleSyncTask extends Tasker<Object> implements WebSocketServerTasker {
@Setter
private ExampleService exampleService;
@Override
public void init(JsonNode argsNode) {
updateTitle("示例同步任务");
// 从argsNode初始化参数
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
try {
// 获取总记录数
long total = exampleService.getTotalCount();
holder.info("开始同步数据,共" + total + "条记录");
// 分批处理
long processed = 0;
List<ExampleEntity> entities = exampleService.getAllEntities();
for (ExampleEntity entity : entities) {
// 检查是否取消
if (isCancelled()) {
holder.info("任务已取消");
return null;
}
// 处理单个实体
exampleService.processEntity(entity);
processed++;
// 更新进度
updateProgress(processed, total);
if (processed % 100 == 0) {
holder.info("已处理" + processed + "/" + total + "条记录");
}
}
holder.info("同步完成,共处理" + processed + "条记录");
return processed;
} catch (Exception e) {
holder.error("同步失败: " + e.getMessage());
throw e;
}
}
}
```
## 7. 常见错误和避免方法
1. **重复实现handler方法**
- 错误在子类中重复实现setMessageHandler等方法
- 解决删除子类中的这些方法直接使用Tasker基类提供的实现
2. **缺少任务注册**
- 错误任务类没有在tasker_mapper.json中注册
- 解决确保所有服务器端Tasker类都在配置文件中正确注册
3. **类名不匹配**
- 错误客户端和服务器端Tasker类名不一致
- 解决:确保两端使用相同的类名
4. **进度更新不正确**
- 错误:进度计算错误或没有更新
- 解决:正确计算和更新进度,提高用户体验
5. **资源泄漏**
- 错误:未及时关闭数据库连接等资源
- 解决使用try-with-resources或在finally块中关闭资源

View File

@@ -178,8 +178,11 @@ Contract-Manager/
2. 配置连接到服务端的WebSocket地址
3. 打包为可执行jar或使用jpackage创建安装包
## 开发环境要求
## Tasker实现规范指南
有关Tasker框架的详细实现规范请参阅 [Tasker实现规范指南](docs/task/tasker_implementation_guide.md)。
## 开发环境要求
- JDK 21
- Maven 3.6+
- MySQL 8.0+

View File

@@ -6,12 +6,12 @@
<parent>
<groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId>
<version>0.0.134-SNAPSHOT</version>
<version>0.0.135-SNAPSHOT</version>
</parent>
<groupId>com.ecep.contract</groupId>
<artifactId>client</artifactId>
<version>0.0.134-SNAPSHOT</version>
<version>0.0.135-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.134-SNAPSHOT</version>
<version>0.0.135-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>

View File

@@ -206,6 +206,12 @@ public class WebSocketClientService {
send(objectMapper.writeValueAsString(message));
}
/**
* WebSocketServerCallbackManage#onMessage 负责接收处理
*
* @param msg
* @return
*/
public CompletableFuture<JsonNode> send(SimpleMessage msg) {
CompletableFuture<JsonNode> future = new CompletableFuture<>();
try {
@@ -353,14 +359,6 @@ public class WebSocketClientService {
return online;
}
public void withSession(Consumer<WebSocketClientSession> sessionConsumer) {
WebSocketClientSession session = createSession();
try {
sessionConsumer.accept(session);
} finally {
// closeSession(session);vvvv
}
}
public void closeSession(WebSocketClientSession session) {
if (session != null) {

View File

@@ -1,22 +1,19 @@
package com.ecep.contract;
import com.ecep.contract.constant.WebSocketConstant;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import java.beans.PropertyDescriptor;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import com.ecep.contract.constant.WebSocketConstant;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
/**
*
*/
@@ -27,6 +24,8 @@ public class WebSocketClientSession {
*/
@Getter
private final String sessionId = UUID.randomUUID().toString();
@Getter
private boolean done = false;
private WebSocketClientTasker tasker;
@@ -69,6 +68,8 @@ public class WebSocketClientSession {
handleAsStart(args);
} else if (type.equals("done")) {
handleAsDone(args);
done = true;
close();
} else {
tasker.updateMessage(java.util.logging.Level.INFO, "未知的消息类型: " + node.toString());
}
@@ -83,7 +84,6 @@ public class WebSocketClientSession {
private void handleAsDone(JsonNode args) {
tasker.updateMessage(java.util.logging.Level.INFO, "任务完成");
close();
}
private void handleAsProgress(JsonNode args) {

View File

@@ -5,20 +5,22 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
/**
* WebSocket客户端任务接口
* 定义了所有通过WebSocket与服务器通信的任务的通用方法
* 包括任务名称、更新消息、更新标题、更新进度等操作
*
* <p>
* 所有通过WebSocket与服务器通信的任务类都应实现此接口, 文档参考 .trace/rules/client_task_rules.md
*/
public interface WebSocketClientTasker {
/**s
/**
* s
* 获取任务名称
* 任务名称用于唯一标识任务, 服务器端会根据任务名称来调用对应的任务处理函数
*
*
* @return 任务名称
*/
String getTaskName();
@@ -26,7 +28,7 @@ public interface WebSocketClientTasker {
/**
* 更新任务执行过程中的消息
* 客户端可以通过此方法向用户展示任务执行过程中的重要信息或错误提示
*
*
* @param level 消息级别, 用于区分不同类型的消息, 如INFO, WARNING, SEVERE等
* @param message 消息内容, 可以是任意字符串, 用于展示给用户
*/
@@ -35,7 +37,7 @@ public interface WebSocketClientTasker {
/**
* 更新任务标题
* 客户端可以通过此方法向用户展示任务的当前执行状态或重要信息
*
*
* @param title 任务标题
*/
void updateTitle(String title);
@@ -43,7 +45,7 @@ public interface WebSocketClientTasker {
/**
* 更新任务进度
* 客户端可以通过此方法向用户展示任务的执行进度, 如文件上传进度、数据库操作进度等
*
*
* @param current 当前进度
* @param total 总进度
*/
@@ -55,12 +57,15 @@ public interface WebSocketClientTasker {
*/
default void cancelTask() {
// 默认实现为空,由具体任务重写
// 需要获取到 session
// 发送 cancel 指令
// 关闭 session.close()
}
/**
* 调用远程WebSocket任务
* 客户端可以通过此方法向服务器提交任务, 并等待服务器返回任务执行结果
*
*
* @param holder 消息持有者,用于记录任务执行过程中的消息
* @param locale 语言环境
* @param args 任务参数
@@ -76,22 +81,31 @@ public interface WebSocketClientTasker {
return null;
}
webSocketService.withSession(session -> {
try {
session.submitTask(this, locale, args);
holder.info("已提交任务到服务器: " + getTaskName());
} catch (JsonProcessingException e) {
String errorMsg = "任务提交失败: " + e.getMessage();
holder.warn(errorMsg);
throw new RuntimeException("任务提交失败: " + e.getMessage(), e);
}
});
WebSocketClientSession session = webSocketService.createSession();
try {
session.submitTask(this, locale, args);
holder.info("已提交任务到服务器: " + getTaskName());
} catch (JsonProcessingException e) {
String errorMsg = "任务提交失败: " + e.getMessage();
holder.warn(errorMsg);
throw new RuntimeException("任务提交失败: " + e.getMessage(), e);
}
// while (!session.isDone()) {
// // 使用TimeUnit
// try {
// TimeUnit.SECONDS.sleep(1);
// } catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }
// }
return null;
}
/**
* 异步调用远程WebSocket任务
*
*
* @param holder 消息持有者,用于记录任务执行过程中的消息
* @param locale 语言环境
* @param args 任务参数
@@ -113,7 +127,7 @@ public interface WebSocketClientTasker {
/**
* 生成唯一的任务ID
*
*
* @return 唯一的任务ID
*/
default String generateTaskId() {

View File

@@ -91,27 +91,32 @@ public class YongYouU8ManagerWindowController
public void onContractGroupSyncAction(ActionEvent event) {
ContractGroupSyncTask task = new ContractGroupSyncTask();
Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
UITools.showTaskDialogAndWait("合同组数据同步", task, null);
// Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
}
public void onContractTypeSyncAction(ActionEvent event) {
ContractTypeSyncTask task = new ContractTypeSyncTask();
Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
UITools.showTaskDialogAndWait("合同类型数据同步", task, null);
// Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
}
public void onContractKindSyncAction(ActionEvent event) {
ContractKindSyncTask task = new ContractKindSyncTask();
Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
UITools.showTaskDialogAndWait("合同类型数据同步", task, null);
// Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
}
public void onVendorClassSyncAction(ActionEvent event) {
VendorClassSyncTask task = new VendorClassSyncTask();
Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
UITools.showTaskDialogAndWait("客户分类数据同步", task, null);
// Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
}
public void onCustomerClassSyncAction(ActionEvent event) {
CustomerClassSyncTask task = new CustomerClassSyncTask();
Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
UITools.showTaskDialogAndWait("客户分类数据同步", task, null);
// Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
}
}

View File

@@ -2,6 +2,7 @@ package com.ecep.contract.controller.contract;
import java.io.File;
import javafx.scene.control.*;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@@ -26,13 +27,6 @@ import com.ecep.contract.vo.ContractVo;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
@@ -44,6 +38,8 @@ import javafx.stage.WindowEvent;
public class ContractWindowController
extends AbstEntityController<ContractVo, ContractViewModel> {
public static void show(ContractVo contract, Window owner) {
show(ContractViewModel.from(contract), owner);
}
@@ -69,6 +65,7 @@ public class ContractWindowController
public Button openRelativeCompanyVendorBtn;
public TextField nameField;
public CheckBox contractNameLockedCk;
public TextField guidField;
public TextField codeField;
public TextField parentCodeField;

View File

@@ -47,7 +47,7 @@ public class EmployeeManagerSkin
controller.accountColumn.setCellValueFactory(param -> param.getValue().getAccount());
controller.departmentColumn.setCellValueFactory(param -> param.getValue().getDepartment());
controller.departmentColumn.setCellFactory(param -> new DepartmentTableCell<>(getDepartmentService()));
controller.departmentColumn.setCellFactory(DepartmentTableCell.forTableColumn(getDepartmentService()));
controller.emailColumn.setCellValueFactory(param -> param.getValue().getEmail());
controller.createdColumn.setCellValueFactory(param -> param.getValue().getCreated());

View File

@@ -0,0 +1,24 @@
package com.ecep.contract.controller.permission;
import com.ecep.contract.controller.tab.AbstEntityBasedTabSkin;
import com.ecep.contract.service.FunctionService;
import com.ecep.contract.service.PermissionService;
import com.ecep.contract.vm.FunctionViewModel;
import com.ecep.contract.vo.FunctionVo;
public abstract class AbstEmployeeFunctionBasedTabSkin
extends AbstEntityBasedTabSkin<EmployeeFunctionWindowController, FunctionVo, FunctionViewModel> {
public AbstEmployeeFunctionBasedTabSkin(EmployeeFunctionWindowController controller) {
super(controller);
}
FunctionService getFunctionService() {
return getCachedBean(FunctionService.class);
}
public PermissionService getPermissionService() {
return controller.permissionService;
}
}

View File

@@ -0,0 +1,27 @@
package com.ecep.contract.controller.permission;
import com.ecep.contract.controller.tab.TabSkin;
import javafx.scene.control.Tab;
public class EmployeeFunctionTabSkinBase extends AbstEmployeeFunctionBasedTabSkin implements TabSkin {
public EmployeeFunctionTabSkinBase(EmployeeFunctionWindowController controller) {
super(controller);
}
@Override
public Tab getTab() {
return controller.baseInfoTab;
}
@Override
public void initializeTab() {
controller.nameField.textProperty().bindBidirectional(viewModel.getName());
controller.keyField.textProperty().bindBidirectional(viewModel.getKey());
controller.controllerField.textProperty().bindBidirectional(viewModel.getController());
controller.iconField.textProperty().bindBidirectional(viewModel.getIcon());
controller.descriptionField.textProperty().bindBidirectional(viewModel.getDescription());
controller.activeField.selectedProperty().bindBidirectional(viewModel.getActive());
}
}

View File

@@ -0,0 +1,83 @@
package com.ecep.contract.controller.permission;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import com.ecep.contract.controller.AbstEntityController;
import com.ecep.contract.service.FunctionService;
import com.ecep.contract.service.PermissionService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.vm.FunctionViewModel;
import com.ecep.contract.vo.FunctionVo;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
@Lazy
@Scope("prototype")
@Component
@FxmlPath("/ui/employee/function.fxml")
public class EmployeeFunctionWindowController extends AbstEntityController<FunctionVo, FunctionViewModel> {
private static final Logger logger = LoggerFactory.getLogger(EmployeeFunctionWindowController.class);
public static void show(FunctionViewModel viewModel, Window window) {
show(EmployeeFunctionWindowController.class, viewModel, window);
}
public BorderPane root;
public TabPane tabPane;
/*
* 基本信息标签页
*/
public Tab baseInfoTab;
public TextField nameField;
public TextField keyField;
public TextField controllerField;
public TextField iconField;
public TextField descriptionField;
@FXML
public CheckBox activeField;
public Label versionLabel;
/*
* 权限标签页
*/
public Tab permissionTab;
@Autowired
PermissionService permissionService;
@Autowired
FunctionService functionService;
@Override
public void onShown(WindowEvent windowEvent) {
super.onShown(windowEvent);
getTitle().bind(viewModel.getName().map(name -> "[" + viewModel.getId().get() + "] " + name + " 功能详情"));
}
@Override
protected void registerTabSkins() {
registerTabSkin(baseInfoTab, tab -> new EmployeeFunctionTabSkinBase(this));
}
@Override
public FunctionService getViewModelService() {
return functionService;
}
}

View File

@@ -61,8 +61,6 @@ public class EmployeeFunctionsManagerWindowController
public void onCreateNewAction(ActionEvent event) {
}
public void onReBuildFilesAction(ActionEvent event) {
}
@Override
public FunctionService getViewModelService() {

View File

@@ -54,9 +54,6 @@ public class EmployeeRoleManagerWindowController
public void onCreateNewAction(ActionEvent event) {
}
public void onReBuildFilesAction(ActionEvent event) {
}
@Override
public EmployeeRoleService getViewModelService() {
return employeeRoleService;

View File

@@ -50,7 +50,7 @@ public class EmployeeRoleTabSkinFunctions extends AbstEmployeeRoleBasedTabSkin {
}
private void loadSelectedRoles() {
List<FunctionVo> selectedRoles = getRoleService().getFunctionsByRoleId(viewModel.getId().get());
List<FunctionVo> selectedRoles = getRoleService().getFunctionsByRole(getEntity());
if (selectedRoles != null) {
functionsField.getTargetItems().setAll(selectedRoles);
}
@@ -100,8 +100,7 @@ public class EmployeeRoleTabSkinFunctions extends AbstEmployeeRoleBasedTabSkin {
private void saveRoles(ActionEvent event) {
EmployeeRoleVo entity = getEntity();
entity.setFunctions(functionsField.getTargetItems());
save(entity);
getRoleService().saveRoleFunctions(entity, functionsField.getTargetItems());
loadSelectedRoles();
}
}

View File

@@ -6,8 +6,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.logging.Level;
import com.ecep.contract.task.CompanyMergeClientTasker;
import com.ecep.contract.util.ParamUtils;
import com.ecep.contract.util.UITools;
import com.ecep.contract.vo.CompanyOldNameVo;
import com.ecep.contract.vo.CompanyVo;
import org.springframework.util.StringUtils;
@@ -138,53 +142,20 @@ public class CompanyTabSkinOldName
}
private void onTableMergeAction(ActionEvent event) {
CompanyVo updater = getParent();
HashSet<String> nameSet = new HashSet<>();
nameSet.add(updater.getName());
// 收集所有曾用名
List<String> nameList = dataSet.stream()
.map(viewModel -> viewModel.getName().get())
.filter(Objects::nonNull)
.toList();
List<CompanyOldNameViewModel> removes = new ArrayList<>();
for (CompanyOldNameViewModel viewModel : dataSet) {
// 创建独立的WebSocket客户端任务器
CompanyMergeClientTasker task = new CompanyMergeClientTasker();
if (!nameSet.add(viewModel.getName().get())) {
// fixed 曾用名中有重复时,删除重复的
deleteRow(viewModel);
removes.add(viewModel);
}
}
if (!removes.isEmpty()) {
Platform.runLater(() -> {
dataSet.removeAll(removes);
});
setStatus("移除重复的曾用名" + removes.size());
}
task.setCompany(getParent());
task.setNameList(nameList);
UITools.showTaskDialogAndWait("合并曾用名", task, null);
int size = nameSet.size();
int count = 1;
int merge = 0;
for (String name : nameSet) {
controller.setRightStatus(count + "/" + size);
if (StringUtils.hasText(name)) {
List<CompanyVo> list = getParentService().findAllByName(name);
for (CompanyVo company : list) {
// fixed 曾用名中有可能有 updater 的名字,会导致自己删除自己
if (Objects.equals(company.getId(), updater.getId())) {
continue;
}
try {
getCompanyService().merge(company, updater);
setStatus("并户 " + company.getName() + "[" + company.getId() + "] 到当前公司");
merge++;
} catch (Exception e) {
throw new RuntimeException("合并 " + company.getName() + " -> " + updater.getName() + " 失败", e);
}
}
}
count++;
}
if (merge == 0) {
setStatus("没有需要并户的公司");
}
controller.setRightStatus("");
loadTableDataSet();
}
@Override
@@ -201,7 +172,7 @@ public class CompanyTabSkinOldName
return;
}
String path = viewModel.getPath().get();
if (StringUtils.hasText(path)) {
if (org.springframework.util.StringUtils.hasText(path)) {
if (item.startsWith(path)) {
item = "~" + item.substring(path.length());
}
@@ -216,4 +187,4 @@ public class CompanyTabSkinOldName
}
return companyOldNameService;
}
}
}

View File

@@ -85,7 +85,9 @@ public class ContractTabSkinBase extends AbstContractBasedTabSkin {
controller.payWayField.textProperty().bind(viewModel.getPayWay().map(ContractPayWay::getText));
controller.nameField.textProperty().bind(viewModel.getName());
controller.nameField.textProperty().bindBidirectional(viewModel.getName());
controller.nameField.editableProperty().bind(viewModel.getNameLocked());
controller.contractNameLockedCk.selectedProperty().bindBidirectional(viewModel.getNameLocked());
controller.codeField.textProperty().bind(viewModel.getCode());
controller.parentCodeField.textProperty().bindBidirectional(viewModel.getParentCode());
@@ -351,17 +353,20 @@ public class ContractTabSkinBase extends AbstContractBasedTabSkin {
}
if (initialDirectory == null) {
if (entity.getPayWay() == ContractPayWay.RECEIVE) {
// 根据项目设置初始目录
ProjectVo project = getProjectService().findById(entity.getProject());
if (project != null) {
// 根据项目销售方式设置初始目录
ProjectSaleTypeVo saleType = getSaleTypeService().findById(project.getSaleTypeId());
if (saleType != null) {
File dir = new File(saleType.getPath());
if (saleType.isStoreByYear()) {
dir = new File(dir, "20" + project.getCodeYear());
Integer projectId = entity.getProject();
if (projectId != null) {
// 根据项目设置初始目录
ProjectVo project = getProjectService().findById(projectId);
if (project != null) {
// 根据项目销售方式设置初始目录
ProjectSaleTypeVo saleType = getSaleTypeService().findById(project.getSaleTypeId());
if (saleType != null) {
File dir = new File(saleType.getPath());
if (saleType.isStoreByYear()) {
dir = new File(dir, "20" + project.getCodeYear());
}
initialDirectory = dir;
}
initialDirectory = dir;
}
}
} else if (entity.getPayWay() == ContractPayWay.PAY) {

View File

@@ -4,10 +4,15 @@ import com.ecep.contract.SpringApp;
import com.ecep.contract.service.DepartmentService;
import com.ecep.contract.vo.DepartmentVo;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class DepartmentTableCell<T> extends AsyncUpdateTableCell<T, Integer, DepartmentVo> {
public DepartmentTableCell(DepartmentService service) {
setService(service);
}
@@ -17,4 +22,20 @@ public class DepartmentTableCell<T> extends AsyncUpdateTableCell<T, Integer, Dep
return SpringApp.getBean(DepartmentService.class);
}
/**
* 为表格列创建 DepartmentTableCell 的工厂方法
*
* @param service 部门服务
* @param <T> 表格行数据类型
* @return 表格列的回调函数
*/
public static <T> Callback<TableColumn<T, Integer>, TableCell<T, Integer>> forTableColumn(DepartmentService service) {
return column -> new DepartmentTableCell<>(service);
}
@Override
public String format(DepartmentVo entity) {
return getService().getStringConverter().toString(entity);
}
}

View File

@@ -1,30 +1,37 @@
package com.ecep.contract.converter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import com.ecep.contract.service.DepartmentService;
import com.ecep.contract.vo.DepartmentVo;
import javafx.util.StringConverter;
import jakarta.annotation.PostConstruct;
@Lazy
@Component
public class DepartmentStringConverter extends EntityStringConverter<DepartmentVo> {
@Lazy
@Autowired
/**
* 部门字符串转换器
*/
public class DepartmentStringConverter extends StringConverter<DepartmentVo> {
private DepartmentService service;
public DepartmentStringConverter() {
}
@PostConstruct
private void init() {
setInitialized(department -> service.findById(department.getId()));
setSuggestion(service::search);
public DepartmentStringConverter(DepartmentService service) {
this.service = service;
}
@Override
public String toString(DepartmentVo department) {
if (department == null) {
return "-";
}
return department.getCode() + " " + department.getName();
}
@Override
public DepartmentVo fromString(String string) {
if (service == null || string == null || string.trim().isEmpty()) {
return null;
}
return service.findByCode(string.trim());
}
}

View File

@@ -1,30 +1,26 @@
package com.ecep.contract.converter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javafx.util.StringConverter;
import com.ecep.contract.service.VendorGroupService;
import com.ecep.contract.vo.VendorGroupVo;
import jakarta.annotation.PostConstruct;
@Lazy
@Component
public class VendorGroupStringConverter extends EntityStringConverter<VendorGroupVo> {
@Lazy
@Autowired
public class VendorGroupStringConverter extends StringConverter<VendorGroupVo> {
private VendorGroupService service;
public VendorGroupStringConverter() {
public VendorGroupStringConverter(VendorGroupService service) {
this.service = service;
}
@PostConstruct
private void init() {
setInitialized(group -> service.findById(group.getId()));
setSuggestion(service::search);
@Override
public String toString(VendorGroupVo object) {
return object == null ? "" : object.getCode() + " " + object.getName();
}
@Override
public VendorGroupVo fromString(String string) {
return service.findByCode(string);
}
}

View File

@@ -1,9 +1,11 @@
package com.ecep.contract.service;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.controlsfx.control.TaskProgressView;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@@ -21,6 +23,8 @@ import lombok.Data;
@Service
public class CloudRkService extends QueryService<CloudRkVo, CloudRkViewModel> {
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class EntInfo {
@@ -69,6 +73,10 @@ public class CloudRkService extends QueryService<CloudRkVo, CloudRkViewModel> {
.findFirst().orElse(null);
}
public Page<CloudRkVo> findAllByCompany(CompanyVo company) {
return findAll(ParamUtils.builder().equals("company", company.getId()).build(), Pageable.unpaged());
}
public boolean checkBlackListUpdateElapse(CompanyVo company, CloudRkVo cloudRk) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'checkBlackListUpdateElapse'");
@@ -78,4 +86,12 @@ public class CloudRkService extends QueryService<CloudRkVo, CloudRkViewModel> {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'updateBlackList'");
}
public void mergeTo(CompanyVo from, CompanyVo to) {
List<CloudRkVo> list = findAllByCompany(from).getContent();
for (CloudRkVo item : list) {
item.setCompanyId(to.getId());
save(item);
}
}
}

View File

@@ -1,7 +1,9 @@
package com.ecep.contract.service;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@@ -77,4 +79,16 @@ public class CloudTycService extends QueryService<CloudTycVo, CloudTycInfoViewMo
.findFirst().orElse(null);
}
public Page<CloudTycVo> findAllByCompany(CompanyVo company) {
return findAll(ParamUtils.builder().equals("company", company.getId()).build(), Pageable.unpaged());
}
public void mergeTo(CompanyVo from, CompanyVo to) {
List<CloudTycVo> list = findAllByCompany(from).getContent();
for (CloudTycVo item : list) {
item.setCompanyId(to.getId());
save(item);
}
}
}

View File

@@ -1,5 +1,6 @@
package com.ecep.contract.service;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.domain.Page;
@@ -32,4 +33,21 @@ public class CompanyContactService extends QueryService<CompanyContactVo, Compan
return page.getContent().getFirst();
}
public List<CompanyContactVo> findAllByCompany(CompanyVo company, LocalDate beginDate, LocalDate endDate) {
return findAll(ParamUtils.builder()
.equals("company", company.getId())
.between("setupDate", beginDate, endDate)
.build(), Pageable.unpaged()).getContent();
}
public void mergeTo(CompanyVo from, CompanyVo to) {
List<CompanyContactVo> contacts = findAllByCompany(from, LocalDate.MIN, LocalDate.MAX);
if (contacts == null || contacts.isEmpty()) {
return;
}
for (CompanyContactVo contact : contacts) {
contact.setCompanyId(to.getId());
save(contact);
}
}
}

View File

@@ -17,4 +17,12 @@ public class CompanyCustomerEntityService extends QueryService<CompanyCustomerEn
.equals("customer", customer.getId()).build(), Pageable.unpaged())
.getContent();
}
public void mergeTo(CustomerVo from, CustomerVo to) {
List<CompanyCustomerEntityVo> fromEntities = findAllByCustomer(from);
for (CompanyCustomerEntityVo fromEntity : fromEntities) {
fromEntity.setCustomerId(to.getId());
save(fromEntity);
}
}
}

View File

@@ -121,4 +121,12 @@ public class CompanyCustomerFileService extends QueryService<CustomerFileVo, Cus
public CustomerFileVo save(CustomerFileVo entity) {
return super.save(entity);
}
public void mergeTo(CustomerVo from, CustomerVo to) {
List<CustomerFileVo> fromFiles = findAllByCustomer(from);
for (CustomerFileVo fromFile : fromFiles) {
fromFile.setCustomer(to.getId());
save(fromFile);
}
}
}

View File

@@ -4,6 +4,7 @@ import java.io.File;
import java.time.LocalDate;
import java.util.List;
import com.ecep.contract.util.MyStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -70,16 +71,7 @@ public class CompanyOldNameService extends QueryService<CompanyOldNameVo, Compan
}
public CompanyOldNameVo findMatchByDate(CompanyVo company, LocalDate localDate) {
Page<CompanyOldNameVo> page = findAll(ParamUtils.builder()
.equals("ambiguity", false)
.equals("company", company.getId())
.and(b -> b.isNotNull("beginDate").greaterThan("beginDate", localDate))
.and(b -> b.isNotNull("endDate").lessThan("endDate", localDate))
.build(), Pageable.ofSize(1));
if (page.isEmpty()) {
return null;
}
return page.getContent().getFirst();
return null;
}
public List<CompanyOldNameVo> findAllByCompanyAndName(CompanyVo company, String oldName) {
@@ -90,4 +82,13 @@ public class CompanyOldNameService extends QueryService<CompanyOldNameVo, Compan
public List<CompanyOldNameVo> findAllByCompany(IdentityEntity company) {
return findAll(ParamUtils.equal("company", company.getId()), Pageable.unpaged()).getContent();
}
public void mergeTo(CompanyVo from, CompanyVo to) {
List<CompanyOldNameVo> fromOldNames = findAllByCompany(from);
for (CompanyOldNameVo fromOldName : fromOldNames) {
fromOldName.setMemo(MyStringUtils.appendIfAbsent(fromOldName.getMemo(), "转自 " + from.getId()));
fromOldName.setCompanyId(to.getId());
save(fromOldName);
}
}
}

View File

@@ -6,6 +6,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.ecep.contract.SpringApp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
@@ -62,9 +63,22 @@ public class CompanyService extends QueryService<CompanyVo, CompanyViewModel> {
return findAll(params, Pageable.unpaged()).getContent();
}
public void merge(CompanyVo company, CompanyVo updater) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'merge'");
public void merge(CompanyVo from, CompanyVo to) {
SpringApp.getBean(CloudRkService.class).mergeTo(from, to);
SpringApp.getBean(CloudTycService.class).mergeTo(from, to);
SpringApp.getBean(YongYouU8Service.class).mergeTo(from, to);
SpringApp.getBean(CompanyOldNameService.class).mergeTo(from, to);
SpringApp.getBean(CompanyContactService.class).mergeTo(from, to);
// 供应商和客户
SpringApp.getBean(VendorService.class).mergeTo(from, to);
SpringApp.getBean(CustomerService.class).mergeTo(from, to);
SpringApp.getBean(ContractService.class).mergeTo(from, to);
SpringApp.getBean(CompanyContactService.class).mergeTo(from, to);
delete(from);
}
public CompanyVo createNewCompany(String newCompanyName) {

View File

@@ -5,6 +5,9 @@ import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import com.ecep.contract.SpringApp;
import com.ecep.contract.util.MyStringUtils;
import com.ecep.contract.vo.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
@@ -20,10 +23,6 @@ import com.ecep.contract.constant.ContractConstant;
import com.ecep.contract.util.ContractUtils;
import com.ecep.contract.util.ParamUtils;
import com.ecep.contract.vm.ContractViewModel;
import com.ecep.contract.vo.ContractFileVo;
import com.ecep.contract.vo.ContractVo;
import com.ecep.contract.vo.ProjectVo;
import com.ecep.contract.vo.VendorVo;
@Service
@CacheConfig(cacheNames = "contract")
@@ -158,10 +157,24 @@ public class ContractService extends QueryService<ContractVo, ContractViewModel>
.build(), Pageable.unpaged()).getContent();
}
public List<ContractVo> findAllByCompanyCustomer(CustomerVo customer, LocalDate beginDate, LocalDate endDate) {
return findAll(ParamUtils.builder()
.equals("company", customer.getCompanyId())
.between("setupDate", beginDate, endDate)
.build(), Pageable.unpaged()).getContent();
}
public List<ContractVo> findAllByCompany(CompanyVo company, LocalDate beginDate, LocalDate endDate) {
return findAll(ParamUtils.builder()
.equals("company", company.getId())
.between("setupDate", beginDate, endDate)
.build(), Pageable.unpaged()).getContent();
}
public List<ContractVo> findAllSalesByProject(ProjectVo project) {
return findAll(ParamUtils.builder()
.equals("parentCode", "")
.equals("project", project.getId()).build(),
.equals("parentCode", "")
.equals("project", project.getId()).build(),
Pageable.unpaged()).getContent();
}
@@ -183,4 +196,18 @@ public class ContractService extends QueryService<ContractVo, ContractViewModel>
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'syncContractFile'");
}
public void mergeTo(CompanyVo from, CompanyVo to) {
List<ContractVo> contracts = findAllByCompany(from, LocalDate.MIN, LocalDate.MAX);
if (contracts == null || contracts.isEmpty()) {
return;
}
for (ContractVo contract : contracts) {
contract.setDescription(MyStringUtils.appendIfAbsent(contract.getDescription(), "转自 " + from.getId()));
contract.setCompanyId(to.getId());
save(contract);
}
}
}

View File

@@ -12,8 +12,6 @@ import org.springframework.stereotype.Service;
import com.ecep.contract.vm.ContractTypeViewModel;
import com.ecep.contract.vo.ContractTypeVo;
import javafx.util.StringConverter;
@Service
@CacheConfig(cacheNames = "contract-type")
public class ContractTypeService extends QueryService<ContractTypeVo, ContractTypeViewModel> {

View File

@@ -30,11 +30,7 @@ public class CustomerService extends QueryService<CustomerVo, CompanyCustomerVie
}
public CustomerVo findByCompany(CompanyVo company) {
Page<CustomerVo> page = findAll(ParamUtils.equal("company", company.getId()), Pageable.ofSize(1));
if (page.isEmpty()) {
return null;
}
return page.getContent().getFirst();
return findOneByProperty("company", company.getId());
}
/**
@@ -140,4 +136,43 @@ public class CustomerService extends QueryService<CustomerVo, CompanyCustomerVie
// 替换文件名中的非法字符
return fileName.replaceAll("[/\\:*?\"<>|]", "_");
}
public void mergeTo(CompanyVo from, CompanyVo to) {
CustomerVo fromCustomer = findByCompany(from);
if (fromCustomer == null) {
return;
}
CustomerVo toCustomer = findByCompany(to);
if (toCustomer == null) {
// 直接修改关联
fromCustomer.setCompanyId(to.getId());
save(fromCustomer);
return;
}
mergeTo(fromCustomer, toCustomer);
}
/**
* 合并客户
* 1. 删除源客户对象
* 2. 删除源客户对象关联的文件
* 3. 修改目标客户对象的关联公司
* 4. 修改目标客户对象的关联文件
*
* @param from
* @param to
*/
public void mergeTo(CustomerVo from, CustomerVo to) {
// file
CompanyCustomerFileService companyCustomerFileService = SpringApp.getBean(CompanyCustomerFileService.class);
companyCustomerFileService.mergeTo(from, to);
// entity
CompanyCustomerEntityService companyCustomerEntityService = SpringApp.getBean(CompanyCustomerEntityService.class);
companyCustomerEntityService.mergeTo(from, to);
// 删除源客户对象
delete(from);
}
}

View File

@@ -1,11 +1,81 @@
package com.ecep.contract.service;
import java.util.List;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import com.ecep.contract.converter.DepartmentStringConverter;
import com.ecep.contract.vm.DepartmentViewModel;
import com.ecep.contract.vo.DepartmentVo;
import javafx.util.StringConverter;
@Service
@CacheConfig(cacheNames = "department")
public class DepartmentService extends QueryService<DepartmentVo, DepartmentViewModel> {
private final DepartmentStringConverter stringConverter = new DepartmentStringConverter(this);
@Cacheable(key = "#p0")
@Override
public DepartmentVo findById(Integer id) {
return super.findById(id);
}
public DepartmentVo findByName(String name) {
try {
return async("findByName", name, String.class).handle((response, ex) -> {
DepartmentVo newEntity = createNewEntity();
return updateValue(newEntity, response);
}).get();
} catch (Exception e) {
throw new RuntimeException("查询实体失败" + name, e);
}
}
public DepartmentVo findByCode(String code) {
try {
return async("findByCode", code, String.class).handle((response, ex) -> {
if (ex != null) {
throw new RuntimeException("远程方法+findByCode+调用失败", ex);
}
DepartmentVo newEntity = createNewEntity();
return updateValue(newEntity, response);
}).get();
} catch (Exception e) {
throw new RuntimeException("查询实体失败" + code, e);
}
}
@Cacheable(key = "'departments'")
@Override
public List<DepartmentVo> findAll() {
return super.findAll();
}
@Caching(evict = {
@CacheEvict(key = "#p0.id"), @CacheEvict(key = "'departments'"),
})
@Override
public void delete(DepartmentVo entity) {
super.delete(entity);
}
@Caching(evict = {
@CacheEvict(key = "#p0.id"), @CacheEvict(key = "'departments'"),
})
@Override
public DepartmentVo save(DepartmentVo entity) {
return super.save(entity);
}
@Override
public StringConverter<DepartmentVo> getStringConverter() {
return stringConverter;
}
}

View File

@@ -1,7 +1,11 @@
package com.ecep.contract.service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.ecep.contract.vo.DepartmentVo;
import javafx.collections.ObservableList;
import org.springframework.stereotype.Service;
import com.ecep.contract.vm.EmployeeRoleViewModel;
@@ -11,11 +15,38 @@ import com.ecep.contract.vo.FunctionVo;
@Service
public class EmployeeRoleService extends QueryService<EmployeeRoleVo, EmployeeRoleViewModel> {
public List<FunctionVo> getFunctionsByRoleId(int roleId) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getFunctionsByRoleId'");
public List<FunctionVo> getFunctionsByRole(EmployeeRoleVo role) {
try {
return async("getFunctionsByRoleId", role.getId(), Integer.class).handle((response, ex) -> {
if (ex != null) {
throw new RuntimeException("远程方法+getFunctionsByRoleId+调用失败", ex);
}
List<FunctionVo> list = new ArrayList<>();
try {
objectMapper.readerForUpdating(list)
.forType(objectMapper.getTypeFactory().constructCollectionType(List.class, FunctionVo.class))
.readValue(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
return list;
}).get();
} catch (Exception e) {
throw new RuntimeException("查询实体失败, Function#" + role.getId(), e);
}
}
public void saveRoleFunctions(EmployeeRoleVo role, List<FunctionVo> functions) {
try {
async("saveRoleFunctions", new Object[]{role.getId(), functions.stream().mapToInt(FunctionVo::getId).toArray()}, new Object[]{Integer.class, Integer[].class}).handle((response, ex) -> {
if (ex != null) {
throw new RuntimeException("远程方法+saveRoleFunctions+调用失败", ex);
}
return null;
}).get();
} catch (Exception e) {
throw new RuntimeException("保存角色的功能失败, 角色#" + role.getId(), e);
}
}
}

View File

@@ -12,7 +12,7 @@ import com.ecep.contract.vm.CompanyVendorEntityViewModel;
@Service
public class VendorEntityService extends QueryService<VendorEntityVo, CompanyVendorEntityViewModel> {
/**
* 根据供应商ID查询关联实体列表
*/
@@ -20,14 +20,14 @@ public class VendorEntityService extends QueryService<VendorEntityVo, CompanyVen
return findAll(ParamUtils.builder().equals("vendorId", vendorId).build(), Pageable.unpaged())
.getContent();
}
/**
* 根据CompanyVendor对象查询关联的CompanyVendorEntity列表
*/
public List<VendorEntityVo> findByVendor(VendorVo vendor) {
return findByVendorId(vendor.getId());
}
/**
* 根据供应商ID创建新的CompanyVendorEntity实例
*/
@@ -43,10 +43,18 @@ public class VendorEntityService extends QueryService<VendorEntityVo, CompanyVen
}
return entity;
}
@Override
public String getBeanName() {
// 确保返回正确的服务名称
return "vendorEntityService";
}
public void mergeTo(VendorVo from, VendorVo to) {
List<VendorEntityVo> fromEntities = findByVendor(from);
for (VendorEntityVo fromEntity : fromEntities) {
fromEntity.setVendorId(to.getId());
save(fromEntity);
}
}
}

View File

@@ -97,33 +97,13 @@ public class VendorFileService extends QueryService<VendorFileVo, CompanyVendorF
}
public void verify(VendorVo companyVendor, LocalDate verifyDate, MessageHolder holder) {
// 查询所有评价表
List<VendorFileVo> files = findAllByVendorAndType(companyVendor, VendorFileType.EvaluationForm);
if (files == null || files.isEmpty()) {
holder.error("未见供应商评价");
return;
}
// 检索 验证日期最近一年内的有效评价表日期宽限7天
LocalDate begin = verifyDate.plusYears(-1);
VendorFileVo vendorFile = files.stream()
.filter(v -> v.getSignDate() != null && v.isValid())
.filter(v -> MyDateTimeUtils.dateValidFilter(v.getSignDate(), begin, verifyDate, 7))
.findFirst().orElse(null);
if (vendorFile == null) {
// 检索最后一个有效评价表
VendorFileVo latestFile = files.stream()
.filter(v -> v.getSignDate() != null && v.isValid())
.max(Comparator.comparing(VendorFileVo::getSignDate))
.orElse(null);
}
if (latestFile == null) {
holder.error("未匹配的供应商评价");
return;
}
// 提示评价表已过期
holder.error("供应商评价已过期:" + latestFile.getSignDate() + ", 检测日期:" + verifyDate);
}
public List<VendorFileVo> findAllByVendor(VendorVo vendor) {
return findAll(ParamUtils.builder()
.equals("vendor", vendor.getId())
.build(), Pageable.unpaged()).getContent();
}
public List<VendorFileVo> findAllByVendorAndType(VendorVo vendor, VendorFileType type) {
@@ -132,4 +112,12 @@ public class VendorFileService extends QueryService<VendorFileVo, CompanyVendorF
.equals("type", type.name())
.build(), Pageable.unpaged()).getContent();
}
public void mergeTo(VendorVo from, VendorVo to) {
List<VendorFileVo> fromFiles = findAllByVendor(from);
for (VendorFileVo fromFile : fromFiles) {
fromFile.setVendorId(to.getId());
save(fromFile);
}
}
}

View File

@@ -1,9 +1,11 @@
package com.ecep.contract.service;
import com.ecep.contract.converter.VendorGroupStringConverter;
import com.ecep.contract.model.VendorGroup;
import com.ecep.contract.util.ParamUtils;
import com.ecep.contract.vm.VendorGroupViewModel;
import com.ecep.contract.vo.VendorGroupVo;
import javafx.util.StringConverter;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
@@ -15,6 +17,8 @@ import org.springframework.stereotype.Service;
@Service
@CacheConfig(cacheNames = "vendor-group")
public class VendorGroupService extends QueryService<VendorGroupVo, VendorGroupViewModel> {
private VendorGroupStringConverter stringConverter = new VendorGroupStringConverter(this);
@Cacheable(key = "#id")
@Override
public VendorGroupVo findById(Integer id) {
@@ -53,4 +57,9 @@ public class VendorGroupService extends QueryService<VendorGroupVo, VendorGroupV
public void delete(VendorGroupVo entity) {
super.delete(entity);
}
@Override
public VendorGroupStringConverter getStringConverter() {
return stringConverter;
}
}

View File

@@ -1,18 +1,5 @@
package com.ecep.contract.service;
import java.io.File;
import java.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.VendorType;
@@ -21,11 +8,20 @@ import com.ecep.contract.model.VendorCatalog;
import com.ecep.contract.util.CompanyUtils;
import com.ecep.contract.util.FileUtils;
import com.ecep.contract.util.MyStringUtils;
import com.ecep.contract.util.ParamUtils;
import com.ecep.contract.vm.CompanyVendorViewModel;
import com.ecep.contract.vo.VendorVo;
import com.ecep.contract.vo.CompanyVo;
import com.ecep.contract.vo.ContractVo;
import com.ecep.contract.vo.VendorVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.File;
import java.time.LocalDate;
@Service
@CacheConfig(cacheNames = "vendor")
@@ -46,7 +42,7 @@ public class VendorService extends QueryService<VendorVo, CompanyVendorViewModel
}
return basePath;
}
public File getVendorApprovedListTemplate() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getVendorApprovedListTemplate'");
@@ -58,11 +54,7 @@ public class VendorService extends QueryService<VendorVo, CompanyVendorViewModel
@Cacheable(key = "'company-'+#p0.id")
public VendorVo findByCompany(CompanyVo company) {
Page<VendorVo> page = findAll(ParamUtils.equal("company", company.getId()), Pageable.ofSize(1));
if (page.isEmpty()) {
return null;
}
return page.getContent().getFirst();
return findOneByProperty("company", company.getId());
}
public void verify(ContractVo contract, MessageHolder holder) {
@@ -189,4 +181,32 @@ public class VendorService extends QueryService<VendorVo, CompanyVendorViewModel
public void delete(VendorVo entity) {
super.delete(entity);
}
public void mergeTo(CompanyVo from, CompanyVo to) {
VendorVo fromCustomer = findByCompany(from);
if (fromCustomer == null) {
return;
}
VendorVo toCustomer = findByCompany(to);
if (toCustomer == null) {
// 直接修改关联
fromCustomer.setCompanyId(to.getId());
save(fromCustomer);
return;
}
mergeTo(fromCustomer, toCustomer);
}
public void mergeTo(VendorVo from, VendorVo to) {
// file
VendorFileService companyVendorFileService = SpringApp.getBean(VendorFileService.class);
companyVendorFileService.mergeTo(from, to);
// entity
VendorEntityService companyCustomerEntityService = SpringApp.getBean(VendorEntityService.class);
companyCustomerEntityService.mergeTo(from, to);
// 删除源客户对象
delete(from);
}
}

View File

@@ -1,10 +1,13 @@
package com.ecep.contract.service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.controlsfx.control.TaskProgressView;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@@ -78,4 +81,15 @@ public class YongYouU8Service extends QueryService<CloudYuVo, CloudYuInfoViewMod
.findFirst().orElse(null);
}
public Page<CloudYuVo> findAllByCompany(CompanyVo company) {
return findAll(ParamUtils.builder().equals("company", company.getId()).build(), Pageable.unpaged());
}
public void mergeTo(CompanyVo from, CompanyVo to) {
List<CloudYuVo> list = findAllByCompany(from).getContent();
for (CloudYuVo item : list) {
item.setCompanyId(to.getId());
save(item);
}
}
}

View File

@@ -0,0 +1,44 @@
package com.ecep.contract.task;
import com.ecep.contract.WebSocketClientTasker;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.vo.CompanyVo;
import javafx.application.Platform;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
/**
* 公司合并客户端任务器
* 通过WebSocket连接与服务器端通信执行公司合并操作
*/
public class CompanyMergeClientTasker extends Tasker<Object> implements WebSocketClientTasker {
@Getter
@Setter
private CompanyVo company;
@Getter
@Setter
private List<String> nameList;
@Override
public String getTaskName() {
return "CompanyMergeTask";
}
@Override
public void updateProgress(long current, long total) {
super.updateProgress(current, total);
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
return callRemoteTask(holder, getLocale(), company.getId(), nameList.toArray());
}
}

View File

@@ -1,11 +1,21 @@
package com.ecep.contract.task;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
public class ContractGroupSyncTask extends Tasker<Object>{
public class ContractGroupSyncTask extends Tasker<Object> implements WebSocketClientTasker {
@Override
public Object execute(MessageHolder holder) {
return null;
public String getTaskName() {
return "ContractGroupSyncTask";
}
@Override
public Object execute(MessageHolder holder) {
return callRemoteTask(holder, getLocale());
}
@Override
public void updateProgress(long current, long total) {
super.updateProgress(current, total);
}
}

View File

@@ -1,10 +1,23 @@
package com.ecep.contract.task;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
public class ContractKindSyncTask extends Tasker<Object> implements WebSocketClientTasker {
public static final String TASK_NAME = "ContractKindSyncTask";
@Override
public String getTaskName() {
return TASK_NAME;
}
@Override
public void updateProgress(long workDone, long max) {
super.updateProgress(workDone, max);
}
public class ContractKindSyncTask extends Tasker<Object> {
@Override
public Object execute(MessageHolder holder) {
return null;
return callRemoteTask(holder, getLocale());
}
}

View File

@@ -18,12 +18,6 @@ public class ContractRepairAllTasker extends Tasker<Object> implements WebSocket
super.updateProgress(current, total);
}
@Override
public void updateTitle(String title) {
// 使用Tasker的updateTitle方法更新标题
super.updateTitle(title);
}
@Override
public String getTaskName() {
return "ContractRepairAllTask";
@@ -34,6 +28,7 @@ public class ContractRepairAllTasker extends Tasker<Object> implements WebSocket
logger.info("开始执行合同修复任务");
updateTitle("合同数据修复");
Object result = callRemoteTask(holder, getLocale());
logger.info("合同修复任务执行完成");
return result;
}

View File

@@ -4,9 +4,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.WebSocketClientTasker;
import com.ecep.contract.service.YongYouU8Service;
/**
* 合同同步任务
@@ -14,15 +12,6 @@ import com.ecep.contract.service.YongYouU8Service;
public class ContractSyncTask extends Tasker<Object> implements WebSocketClientTasker {
private static final Logger logger = LoggerFactory.getLogger(ContractSyncTask.class);
private YongYouU8Service yongYouU8Service;
private YongYouU8Service getYongYouU8Service() {
if (yongYouU8Service == null) {
yongYouU8Service = SpringApp.getBean(YongYouU8Service.class);
}
return yongYouU8Service;
}
public String getTaskName() {
return "ContractSyncTask";
}

View File

@@ -1,10 +1,27 @@
package com.ecep.contract.task;
import com.ecep.contract.MessageHolder;
import java.util.Locale;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
/**
* 合同类型同步任务
*/
public class ContractTypeSyncTask extends Tasker<Object> implements WebSocketClientTasker {
@Override
public String getTaskName() {
return "ContractTypeSyncTask";
}
@Override
public void updateProgress(long current, long total) {
super.updateProgress(current, total);
}
public class ContractTypeSyncTask extends Tasker<Object> {
@Override
public Object execute(MessageHolder holder) {
return null;
// 调用远程WebSocket任务
return callRemoteTask(holder, Locale.getDefault());
}
}

View File

@@ -1,13 +1,32 @@
package com.ecep.contract.task;
import com.ecep.contract.MessageHolder;
import java.util.Locale;
public class CustomerClassSyncTask extends Tasker<Object> {
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
public class CustomerClassSyncTask extends Tasker<Object> implements WebSocketClientTasker {
public static final String TASK_NAME = "CustomerClassSyncTask";
private static final Logger logger = LoggerFactory.getLogger(CustomerClassSyncTask.class);
@Override
public String getTaskName() {
return TASK_NAME;
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'execute'");
updateTitle("用友U8系统-同步客户分类");
return callRemoteTask(holder, Locale.getDefault());
}
// 显式重写 updateProgress 以解决方法隐藏冲突
@Override
public void updateProgress(long workDone, long max) {
super.updateProgress(workDone, max);
}
}

View File

@@ -1,20 +1,36 @@
package com.ecep.contract.task;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
/**
* 同步客户任务
*/
public class CustomerSyncTask extends Tasker<Object> {
public class CustomerSyncTask extends Tasker<Object> implements WebSocketClientTasker {
public static final String TASK_NAME = "CustomerSyncTask";
private static final Logger logger = LoggerFactory.getLogger(CustomerSyncTask.class);
@Override
public String getTaskName() {
return TASK_NAME;
}
// 显式重写 updateProgress 以解决方法隐藏冲突
@Override
public void updateProgress(long workDone, long max) {
super.updateProgress(workDone, max);
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("用友U8系统-同步客户");
return null;
return callRemoteTask(holder, Locale.getDefault());
}
}

View File

@@ -1,13 +1,37 @@
package com.ecep.contract.task;
import com.ecep.contract.MessageHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EmployeesSyncTask extends Tasker<Object> {
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
/**
* 员工同步任务客户端实现
* 用于通过WebSocket与服务器通信执行用友U8系统员工信息同步
*/
public class EmployeesSyncTask extends Tasker<Object> implements WebSocketClientTasker {
private static final Logger logger = LoggerFactory.getLogger(EmployeesSyncTask.class);
@Override
public String getTaskName() {
return "EmployeesSyncTask";
}
@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'");
}
// 设置任务标题
updateTitle("用友U8系统-同步员工信息");
// 更新任务消息
updateMessage("开始执行员工信息同步...");
// 调用远程WebSocket任务
return callRemoteTask(holder, getLocale());
}
}

View File

@@ -4,14 +4,11 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.logging.Level;
import com.ecep.contract.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import com.ecep.contract.Desktop;
import com.ecep.contract.Message;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.service.CompanyService;
import com.ecep.contract.service.EmployeeService;
import com.ecep.contract.service.SysConfService;
@@ -63,6 +60,22 @@ public abstract class Tasker<T> extends Task<T> {
return currentUser;
}
@Override
public void run() {
if (this instanceof WebSocketClientTasker) {
this.getState();
}
super.run();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (this instanceof WebSocketClientTasker) {
((WebSocketClientTasker) this).cancelTask();
}
return super.cancel(mayInterruptIfRunning);
}
@Override
protected T call() throws Exception {
MessageHolderImpl holder = new MessageHolderImpl();

View File

@@ -1,10 +1,28 @@
package com.ecep.contract.task;
import com.ecep.contract.MessageHolder;
import java.util.Locale;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
public class VendorClassSyncTask extends Tasker<Object> implements WebSocketClientTasker {
public static final String TASK_NAME = "VendorClassSyncTask";
@Override
public String getTaskName() {
return TASK_NAME;
}
@Override
public void updateProgress(long current, long total) {
// 直接使用父类的updateProgress方法
super.updateProgress(current, total);
}
public class VendorClassSyncTask extends Tasker<Object> {
@Override
public Object execute(MessageHolder holder) {
return null;
// 使用callRemoteTask调用远程任务
updateTitle("用友U8系统-同步供应商分类");
return callRemoteTask(holder, Locale.getDefault());
}
}

View File

@@ -4,16 +4,28 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
/**
* 供应商同步任务
*/
public class VendorSyncTask extends Tasker<Object> {
public class VendorSyncTask extends Tasker<Object> implements WebSocketClientTasker {
private static final Logger logger = LoggerFactory.getLogger(VendorSyncTask.class);
private static final String TASK_NAME = "VendorSyncTask";
@Override
public String getTaskName() {
return TASK_NAME;
}
@Override
public void updateProgress(long current, long total) {
super.updateProgress(current, total);
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("用友U8系统-同步供应商");
return null;
return callRemoteTask(holder, getLocale());
}
}

View File

@@ -4,6 +4,8 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -221,15 +223,19 @@ public class UITools {
if (task instanceof Tasker<?> tasker) {
// 提交任务
Desktop.instance.getExecutorService().submit(tasker);
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(tasker);
}
}
if (init != null) {
init.accept(consumer::test);
}
dialog.showAndWait();
if (task.isRunning()) {
if (task.getProgress() < 1) {
task.cancel();
}
// if (task.isRunning()) {
// }
}
private static String printStackTrace(Throwable e) {

View File

@@ -26,6 +26,10 @@ public class ContractViewModel extends IdentityViewModel<ContractVo> {
private SimpleStringProperty code = new SimpleStringProperty();
private SimpleStringProperty name = new SimpleStringProperty();
/**
* 名称是否锁定
*/
private SimpleBooleanProperty nameLocked = new SimpleBooleanProperty();
private SimpleStringProperty state = new SimpleStringProperty();
/**
* 分组
@@ -138,6 +142,7 @@ public class ContractViewModel extends IdentityViewModel<ContractVo> {
getPayWay().set(c.getPayWay());
getCode().set(c.getCode());
getName().set(c.getName());
getNameLocked().set(c.isNameLocked());
getState().set(c.getState());
getGroup().set(c.getGroupId());
@@ -198,6 +203,10 @@ public class ContractViewModel extends IdentityViewModel<ContractVo> {
contract.setName(name.get());
modified = true;
}
if (!Objects.equals(nameLocked.get(), contract.isNameLocked())) {
contract.setNameLocked(nameLocked.get());
modified = true;
}
if (!Objects.equals(state.get(), contract.getState())) {
contract.setState(state.get());
modified = true;

View File

@@ -3,7 +3,7 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.VBox?>
<VBox prefHeight="400.0" prefWidth="640.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.ecep.contract.manager.cloud.rk.CloudRkManagerWindowController">
fx:controller="com.ecep.contract.controller.cloud.rk.CloudRkManagerWindowController">
<children>
<MenuBar VBox.vgrow="NEVER">
<menus>
@@ -39,7 +39,6 @@
<Menu mnemonicParsing="false" text="Help">
<items>
<MenuItem mnemonicParsing="false" text="数据修复" onAction="#onDataRepairAction"/>
<MenuItem mnemonicParsing="false" text="数据迁移" onAction="#onDateTransferAction"/>
<MenuItem mnemonicParsing="false" text="About MyHelloApp"/>
</items>
</Menu>

View File

@@ -1,36 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.DatePicker?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import org.controlsfx.glyphfont.Glyph?>
<BorderPane fx:id="root" maxHeight="900" maxWidth="1024" minHeight="300" minWidth="200" prefHeight="561.0"
prefWidth="800.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.ecep.contract.controller.contract.ContractWindowController">
<BorderPane fx:id="root" maxHeight="900" maxWidth="1024" minHeight="300" minWidth="200" prefHeight="561.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.ecep.contract.controller.contract.ContractWindowController">
<top>
<ToolBar prefHeight="40.0" prefWidth="800.0" BorderPane.alignment="CENTER">
<items>
<Button onAction="#onContractOpenInExplorerAction" text="打开目录(_D)"/>
<Button onAction="#onSyncContractAction" text="同步(_R)"/>
<Button onAction="#onContractOpenInExplorerAction" text="打开目录(_D)" />
<Button onAction="#onSyncContractAction" text="同步(_R)" />
<Button fx:id="saveBtn" disable="true" text="保存(_S)">
<graphic>
<Glyph fontFamily="FontAwesome" icon="SAVE"/>
<Glyph fontFamily="FontAwesome" icon="SAVE" />
</graphic>
</Button>
<Button fx:id="verifyBtn" onAction="#onContractVerifyAction" text="合规(_V)">
<graphic>
<Glyph fontFamily="FontAwesome" icon="CHECK"/>
<Glyph fontFamily="FontAwesome" icon="CHECK" />
</graphic>
<tooltip>
<Tooltip text="按照预定规则检测合同是否符合合规要求"/>
<Tooltip text="按照预定规则检测合同是否符合合规要求" />
</tooltip>
</Button>
</items>
</ToolBar>
</top>
<center>
<TabPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="150.0"
tabClosingPolicy="UNAVAILABLE" tabMaxWidth="100.0" tabMinWidth="40.0">
<TabPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="150.0" tabClosingPolicy="UNAVAILABLE" tabMaxWidth="100.0" tabMinWidth="40.0">
<tabs>
<Tab fx:id="baseInfoTab" text="基本信息">
<content>
@@ -40,262 +54,190 @@
<children>
<GridPane VBox.vgrow="NEVER">
<columnConstraints>
<ColumnConstraints fillWidth="false" halignment="CENTER" hgrow="NEVER"
maxWidth="200.0" minWidth="80.0" prefWidth="120.0"/>
<ColumnConstraints hgrow="ALWAYS" maxWidth="1.7976931348623157E308"
minWidth="100.0" prefWidth="180.0"/>
<ColumnConstraints fillWidth="false" halignment="CENTER" hgrow="NEVER"
maxWidth="200.0" minWidth="80.0" prefWidth="120.0"/>
<ColumnConstraints hgrow="ALWAYS" maxWidth="1.7976931348623157E308"
minWidth="100.0" prefWidth="180.0"/>
<ColumnConstraints fillWidth="false" halignment="CENTER" hgrow="NEVER" maxWidth="200.0" minWidth="80.0" prefWidth="120.0" />
<ColumnConstraints hgrow="ALWAYS" maxWidth="1.7976931348623157E308" minWidth="100.0" prefWidth="180.0" />
<ColumnConstraints fillWidth="false" halignment="CENTER" hgrow="NEVER" maxWidth="200.0" minWidth="80.0" prefWidth="120.0" />
<ColumnConstraints hgrow="ALWAYS" maxWidth="1.7976931348623157E308" minWidth="100.0" prefWidth="180.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0"
vgrow="NEVER"/>
<RowConstraints fillHeight="false" maxHeight="1.7976931348623157E308"
minHeight="80.0" prefHeight="80.0" vgrow="NEVER"/>
<RowConstraints fillHeight="false" maxHeight="1.7976931348623157E308"
minHeight="30.0" prefHeight="42.0" vgrow="NEVER"/>
<RowConstraints fillHeight="false" maxHeight="1.7976931348623157E308"
minHeight="30.0" prefHeight="42.0" vgrow="NEVER"/>
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" minHeight="30.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" maxHeight="1.7976931348623157E308" minHeight="80.0" prefHeight="80.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" maxHeight="1.7976931348623157E308" minHeight="30.0" prefHeight="42.0" vgrow="NEVER" />
<RowConstraints fillHeight="false" maxHeight="1.7976931348623157E308" minHeight="30.0" prefHeight="42.0" vgrow="NEVER" />
</rowConstraints>
<children>
<Label text="合同名称 *"/>
<Label text="合同编码 *" GridPane.rowIndex="2"/>
<Label text="状态 *" GridPane.columnIndex="2" GridPane.rowIndex="4"/>
<TextField fx:id="codeField" GridPane.columnIndex="1"
GridPane.rowIndex="2"/>
<TextField fx:id="stateField" GridPane.columnIndex="3"
GridPane.rowIndex="4"/>
<Label text="合同名称 *" />
<Label text="合同编码 *" GridPane.rowIndex="2" />
<Label text="状态 *" GridPane.columnIndex="2" GridPane.rowIndex="4" />
<TextField fx:id="codeField" GridPane.columnIndex="1" GridPane.rowIndex="2" />
<TextField fx:id="stateField" GridPane.columnIndex="3" GridPane.rowIndex="4" />
<HBox GridPane.columnIndex="1" GridPane.columnSpan="3">
<children>
<TextField fx:id="nameField" HBox.hgrow="ALWAYS"/>
<Button fx:id="contractRenameBtn" mnemonicParsing="false"
text="合同更名"/>
<TextField fx:id="nameField" HBox.hgrow="ALWAYS" />
<CheckBox fx:id="contractNameLockedCk" mnemonicParsing="false">
<HBox.margin>
<Insets left="4.0" right="4.0" top="4.0" />
</HBox.margin>
<tooltip>
<Tooltip text="锁定合同名称" />
</tooltip>
</CheckBox>
<Button fx:id="contractRenameBtn" mnemonicParsing="false" text="合同更名" />
</children>
</HBox>
<Label layoutX="20.0" layoutY="28.0" text="GUID *"
GridPane.columnIndex="2" GridPane.rowIndex="17"/>
<Label layoutX="20.0" layoutY="58.0" text="分组 *"
GridPane.columnIndex="2" GridPane.rowIndex="3"/>
<Label layoutX="20.0" layoutY="28.0" text="备注"
GridPane.rowIndex="18"/>
<TextField fx:id="groupField" layoutX="202.0" layoutY="114.0"
GridPane.columnIndex="3" GridPane.rowIndex="3"/>
<TextField fx:id="guidField" GridPane.columnIndex="3"
GridPane.rowIndex="17"/>
<Label layoutX="56.0" layoutY="118.0" text="类型 *"
GridPane.columnIndex="2" GridPane.rowIndex="1"/>
<TextField fx:id="typeField" GridPane.columnIndex="3"
GridPane.rowIndex="1"/>
<TextField fx:id="kindField" GridPane.columnIndex="3"
GridPane.rowIndex="2"/>
<Label text="性质 *" GridPane.columnIndex="2" GridPane.rowIndex="2"/>
<Label text="合同起止日期 *" GridPane.rowIndex="8"/>
<Label text="存储目录" GridPane.rowIndex="15"/>
<TextField fx:id="pathField" GridPane.columnIndex="1"
GridPane.columnSpan="3" GridPane.rowIndex="15">
<Label layoutX="20.0" layoutY="28.0" text="GUID *" GridPane.columnIndex="2" GridPane.rowIndex="17" />
<Label layoutX="20.0" layoutY="58.0" text="分组 *" GridPane.columnIndex="2" GridPane.rowIndex="3" />
<Label layoutX="20.0" layoutY="28.0" text="备注" GridPane.rowIndex="18" />
<TextField fx:id="groupField" layoutX="202.0" layoutY="114.0" GridPane.columnIndex="3" GridPane.rowIndex="3" />
<TextField fx:id="guidField" GridPane.columnIndex="3" GridPane.rowIndex="17" />
<Label layoutX="56.0" layoutY="118.0" text="类型 *" GridPane.columnIndex="2" GridPane.rowIndex="1" />
<TextField fx:id="typeField" GridPane.columnIndex="3" GridPane.rowIndex="1" />
<TextField fx:id="kindField" GridPane.columnIndex="3" GridPane.rowIndex="2" />
<Label text="性质 *" GridPane.columnIndex="2" GridPane.rowIndex="2" />
<Label text="合同起止日期 *" GridPane.rowIndex="8" />
<Label text="存储目录" GridPane.rowIndex="15" />
<TextField fx:id="pathField" GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="15">
<GridPane.margin>
<Insets/>
<Insets />
</GridPane.margin>
</TextField>
<Label text="创建日期" GridPane.rowIndex="17"/>
<TextField fx:id="createdField" GridPane.columnIndex="1"
GridPane.rowIndex="17"/>
<Label text="创建日期" GridPane.rowIndex="17" />
<TextField fx:id="createdField" GridPane.columnIndex="1" GridPane.rowIndex="17" />
<HBox GridPane.columnIndex="1" GridPane.rowIndex="8">
<children>
<DatePicker fx:id="startDateField"
maxWidth="1.7976931348623157E308"
promptText="启时日期" HBox.hgrow="ALWAYS">
<DatePicker fx:id="startDateField" maxWidth="1.7976931348623157E308" promptText="启时日期" HBox.hgrow="ALWAYS">
<HBox.margin>
<Insets right="6.0"/>
<Insets right="6.0" />
</HBox.margin>
</DatePicker>
<DatePicker fx:id="endDateField"
maxWidth="1.7976931348623157E308"
promptText="截止日期" HBox.hgrow="ALWAYS"/>
<DatePicker fx:id="endDateField" maxWidth="1.7976931348623157E308" promptText="截止日期" HBox.hgrow="ALWAYS" />
</children>
</HBox>
<TextArea fx:id="descriptionField" GridPane.columnIndex="1"
GridPane.columnSpan="3" GridPane.rowIndex="18"/>
<Label text="提交人 *" GridPane.columnIndex="2" GridPane.rowIndex="8"/>
<TextArea fx:id="descriptionField" GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="18" />
<Label text="提交人 *" GridPane.columnIndex="2" GridPane.rowIndex="8" />
<HBox GridPane.columnIndex="3" GridPane.rowIndex="8">
<children>
<TextField fx:id="setupPersonField" HBox.hgrow="SOMETIMES"/>
<DatePicker fx:id="setupDateField" HBox.hgrow="SOMETIMES"/>
<TextField fx:id="setupPersonField" HBox.hgrow="SOMETIMES" />
<DatePicker fx:id="setupDateField" HBox.hgrow="SOMETIMES" />
</children>
</HBox>
<Label text="签订日期 *" GridPane.rowIndex="9"/>
<DatePicker fx:id="orderDateField" maxWidth="1.7976931348623157E308"
GridPane.columnIndex="1" GridPane.rowIndex="9"/>
<Label text="生效人 *" GridPane.columnIndex="2" GridPane.rowIndex="9"/>
<Label text="修改人 *" GridPane.columnIndex="2" GridPane.rowIndex="10"/>
<Label text="签订日期 *" GridPane.rowIndex="9" />
<DatePicker fx:id="orderDateField" maxWidth="1.7976931348623157E308" GridPane.columnIndex="1" GridPane.rowIndex="9" />
<Label text="生效人 *" GridPane.columnIndex="2" GridPane.rowIndex="9" />
<Label text="修改人 *" GridPane.columnIndex="2" GridPane.rowIndex="10" />
<HBox GridPane.columnIndex="3" GridPane.rowIndex="9">
<children>
<TextField fx:id="inurePersonField" HBox.hgrow="SOMETIMES"/>
<DatePicker fx:id="inureDateField" HBox.hgrow="SOMETIMES"/>
<TextField fx:id="inurePersonField" HBox.hgrow="SOMETIMES" />
<DatePicker fx:id="inureDateField" HBox.hgrow="SOMETIMES" />
</children>
</HBox>
<HBox GridPane.columnIndex="3" GridPane.rowIndex="10">
<children>
<TextField fx:id="varyPersonField" HBox.hgrow="SOMETIMES"/>
<DatePicker fx:id="varyDateField" HBox.hgrow="SOMETIMES"/>
<TextField fx:id="varyPersonField" HBox.hgrow="SOMETIMES" />
<DatePicker fx:id="varyDateField" HBox.hgrow="SOMETIMES" />
</children>
</HBox>
<TextField fx:id="parentCodeField" GridPane.columnIndex="1"
GridPane.rowIndex="3"/>
<Label text="主合同编码" GridPane.rowIndex="3"/>
<Label fx:id="versionLabel" text="\@Version" GridPane.rowIndex="19"/>
<HBox alignment="CENTER_RIGHT" spacing="5.0" GridPane.columnIndex="1"
GridPane.columnSpan="3" GridPane.rowIndex="16">
<TextField fx:id="parentCodeField" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<Label text="主合同编码" GridPane.rowIndex="3" />
<Label fx:id="versionLabel" text="\@Version" GridPane.rowIndex="19" />
<HBox alignment="CENTER_RIGHT" spacing="5.0" GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="16">
<children>
<Button fx:id="contractPathCreateBtn" mnemonicParsing="false"
text="创建目录"/>
<Button fx:id="contractPathChangeBtn" mnemonicParsing="false"
text="变更目录"/>
<Button fx:id="contractPathAsNameBtn" mnemonicParsing="false"
text="与合同名称同名"/>
<Button fx:id="contractPathAsCodeBtn" mnemonicParsing="false"
text="与合同号同名"/>
<Button fx:id="contractPathCreateBtn" mnemonicParsing="false" text="创建目录" />
<Button fx:id="contractPathChangeBtn" mnemonicParsing="false" text="变更目录" />
<Button fx:id="contractPathAsNameBtn" mnemonicParsing="false" text="与合同名称同名" />
<Button fx:id="contractPathAsCodeBtn" mnemonicParsing="false" text="与合同号同名" />
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" spacing="5.0" GridPane.columnIndex="1"
GridPane.rowIndex="4">
<HBox alignment="CENTER_RIGHT" spacing="5.0" GridPane.columnIndex="1" GridPane.rowIndex="4">
<children>
<Button fx:id="openMainContractBtn" mnemonicParsing="false"
text="打开主合同"/>
<Button fx:id="calcMainContractNoBtn" mnemonicParsing="false"
text="计算主合同编号"/>
<Button fx:id="openMainContractBtn" mnemonicParsing="false" text="打开主合同" />
<Button fx:id="calcMainContractNoBtn" mnemonicParsing="false" text="计算主合同编号" />
</children>
</HBox>
<Label text="对方单位 *" GridPane.rowIndex="5"/>
<HBox spacing="5.0" GridPane.columnIndex="1" GridPane.columnSpan="3"
GridPane.rowIndex="5">
<Label text="对方单位 *" GridPane.rowIndex="5" />
<HBox spacing="5.0" GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="5">
<children>
<TextField fx:id="companyField" HBox.hgrow="SOMETIMES"/>
<Button mnemonicParsing="false"
onAction="#onContractOpenRelativeCompanyAction"
text="详情">
<TextField fx:id="companyField" HBox.hgrow="SOMETIMES" />
<Button mnemonicParsing="false" onAction="#onContractOpenRelativeCompanyAction" text="详情">
<HBox.margin>
<Insets/>
<Insets />
</HBox.margin>
</Button>
<Button fx:id="openRelativeCompanyCustomerBtn"
mnemonicParsing="false" text="客户">
<Button fx:id="openRelativeCompanyCustomerBtn" mnemonicParsing="false" text="客户">
<HBox.margin>
<Insets/>
<Insets />
</HBox.margin>
</Button>
<Button fx:id="openRelativeCompanyVendorBtn"
mnemonicParsing="false" text="供应商"/>
<Button fx:id="openRelativeCompanyVendorBtn" mnemonicParsing="false" text="供应商" />
</children>
</HBox>
<Label text="业务员 *" GridPane.rowIndex="11"/>
<Label text="业务员 *" GridPane.rowIndex="11" />
<VBox GridPane.columnIndex="1" GridPane.rowIndex="11">
<children>
<TextField fx:id="employeeField"/>
<TextField fx:id="employeeField" />
</children>
<GridPane.margin>
<Insets bottom="5.0" top="5.0"/>
<Insets bottom="5.0" top="5.0" />
</GridPane.margin>
</VBox>
<Label text="经办人" GridPane.columnIndex="2" GridPane.rowIndex="11"/>
<Label text="经办人" GridPane.columnIndex="2" GridPane.rowIndex="11" />
<VBox GridPane.columnIndex="3" GridPane.rowIndex="11">
<children>
<TextField fx:id="handlerField"/>
<TextField fx:id="handlerField" />
</children>
<GridPane.margin>
<Insets bottom="5.0" top="5.0"/>
<Insets bottom="5.0" top="5.0" />
</GridPane.margin>
</VBox>
<Label text="归属项目" GridPane.rowIndex="1"/>
<Label text="归属项目" GridPane.rowIndex="1" />
<HBox GridPane.columnIndex="1" GridPane.rowIndex="1">
<children>
<TextField fx:id="projectField" prefWidth="190.0"
HBox.hgrow="ALWAYS"/>
<Button fx:id="linkContractProjectBtn" mnemonicParsing="false"
text="关联" textOverrun="CLIP">
<TextField fx:id="projectField" prefWidth="190.0" HBox.hgrow="ALWAYS" />
<Button fx:id="linkContractProjectBtn" mnemonicParsing="false" text="关联" textOverrun="CLIP">
</Button>
</children>
</HBox>
<Label text="1. (*) 字段数据同步自用友U8系统同步时将被覆盖"
textFill="#888888" wrapText="true" GridPane.columnIndex="1"
GridPane.columnSpan="3" GridPane.rowIndex="20"/>
<Label text="合同金额" GridPane.rowIndex="6"/>
<TextField fx:id="amountField" alignment="CENTER_RIGHT"
GridPane.columnIndex="1" GridPane.rowIndex="6"/>
<Label text="合同总数量 *" GridPane.rowIndex="12"/>
<Label text="合同总金额 *" GridPane.rowIndex="13"/>
<Label text="不含税总金额 *" GridPane.rowIndex="14"/>
<Label text="执行总数量 *" GridPane.columnIndex="2"
GridPane.rowIndex="12"/>
<Label text="执行总金额 *" GridPane.columnIndex="2"
GridPane.rowIndex="13"/>
<Label text="不含税总金额 *" GridPane.columnIndex="2"
GridPane.rowIndex="14"/>
<TextField fx:id="totalQuantityField" alignment="CENTER_RIGHT"
GridPane.columnIndex="1" GridPane.halignment="RIGHT"
GridPane.rowIndex="12"/>
<TextField fx:id="totalAmountField" alignment="CENTER_RIGHT"
GridPane.columnIndex="1" GridPane.halignment="RIGHT"
GridPane.rowIndex="13"/>
<TextField fx:id="totalUnTaxAmountField" alignment="CENTER_RIGHT"
GridPane.columnIndex="1" GridPane.halignment="RIGHT"
GridPane.rowIndex="14"/>
<TextField fx:id="execQuantityField" alignment="CENTER_RIGHT"
GridPane.columnIndex="3" GridPane.halignment="RIGHT"
GridPane.rowIndex="12"/>
<TextField fx:id="execAmountField" alignment="CENTER_RIGHT"
GridPane.columnIndex="3" GridPane.halignment="RIGHT"
GridPane.rowIndex="13"/>
<TextField fx:id="execUnTaxAmountField" alignment="CENTER_RIGHT"
GridPane.columnIndex="3" GridPane.halignment="RIGHT"
GridPane.rowIndex="14"/>
<Label text="付款方向 *" GridPane.columnIndex="2"
GridPane.rowIndex="6"/>
<TextField fx:id="payWayField" GridPane.columnIndex="3"
GridPane.rowIndex="6"/>
<Label text="1. (*) 字段数据同步自用友U8系统同步时将被覆盖" textFill="#888888" wrapText="true" GridPane.columnIndex="1" GridPane.columnSpan="3" GridPane.rowIndex="20" />
<Label text="合同金额" GridPane.rowIndex="6" />
<TextField fx:id="amountField" alignment="CENTER_RIGHT" GridPane.columnIndex="1" GridPane.rowIndex="6" />
<Label text="合同总数量 *" GridPane.rowIndex="12" />
<Label text="合同总金额 *" GridPane.rowIndex="13" />
<Label text="不含税总金额 *" GridPane.rowIndex="14" />
<Label text="执行总数量 *" GridPane.columnIndex="2" GridPane.rowIndex="12" />
<Label text="执行总金额 *" GridPane.columnIndex="2" GridPane.rowIndex="13" />
<Label text="不含税总金额 *" GridPane.columnIndex="2" GridPane.rowIndex="14" />
<TextField fx:id="totalQuantityField" alignment="CENTER_RIGHT" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="12" />
<TextField fx:id="totalAmountField" alignment="CENTER_RIGHT" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="13" />
<TextField fx:id="totalUnTaxAmountField" alignment="CENTER_RIGHT" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="14" />
<TextField fx:id="execQuantityField" alignment="CENTER_RIGHT" GridPane.columnIndex="3" GridPane.halignment="RIGHT" GridPane.rowIndex="12" />
<TextField fx:id="execAmountField" alignment="CENTER_RIGHT" GridPane.columnIndex="3" GridPane.halignment="RIGHT" GridPane.rowIndex="13" />
<TextField fx:id="execUnTaxAmountField" alignment="CENTER_RIGHT" GridPane.columnIndex="3" GridPane.halignment="RIGHT" GridPane.rowIndex="14" />
<Label text="付款方向 *" GridPane.columnIndex="2" GridPane.rowIndex="6" />
<TextField fx:id="payWayField" GridPane.columnIndex="3" GridPane.rowIndex="6" />
</children>
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin>
</GridPane>
<Pane prefHeight="50.0" prefWidth="200.0"/>
<Pane prefHeight="50.0" prefWidth="200.0" />
</children>
</VBox>
</content>
@@ -320,14 +262,13 @@
<bottom>
<HBox id="HBox" alignment="CENTER_LEFT" prefWidth="800.0" spacing="5.0">
<children>
<Label fx:id="leftStatusLabel" maxHeight="1.7976931348623157E308" text="Left status"
HBox.hgrow="ALWAYS">
<Label fx:id="leftStatusLabel" maxHeight="1.7976931348623157E308" text="Left status" HBox.hgrow="ALWAYS">
</Label>
<Pane HBox.hgrow="ALWAYS"/>
<Label fx:id="rightStatusLabel" text="Right status" HBox.hgrow="NEVER"/>
<Pane HBox.hgrow="ALWAYS" />
<Label fx:id="rightStatusLabel" text="Right status" HBox.hgrow="NEVER" />
</children>
<padding>
<Insets bottom="3.0" left="3.0" right="3.0" top="3.0"/>
<Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
</padding>
</HBox>
</bottom>

View File

@@ -7,7 +7,7 @@
<?import org.controlsfx.control.ListSelectionView?>
<BorderPane fx:id="root" maxHeight="900" maxWidth="1024" minHeight="300" minWidth="200" prefHeight="500.0"
prefWidth="800.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.ecep.contract.controller.permission.EmployeeFunctionsManagerWindowController">
fx:controller="com.ecep.contract.controller.permission.EmployeeFunctionWindowController">
<center>
<TabPane fx:id="tabPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="150.0"
tabClosingPolicy="UNAVAILABLE" tabMaxWidth="100.0" tabMinWidth="40.0">

View File

@@ -10,7 +10,6 @@
<Menu text="文件(_F)">
<items>
<MenuItem onAction="#onCreateNewAction" text="新建…(_N)"/>
<MenuItem onAction="#onReBuildFilesAction" text="同步…(_R)"/>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem mnemonicParsing="false" text="Preferences…"/>
</items>

View File

@@ -23,7 +23,8 @@
<?import javafx.scene.text.Font?>
<?import org.controlsfx.control.ListSelectionView?>
<BorderPane fx:id="root" maxHeight="900" maxWidth="1024" minHeight="300" minWidth="200" prefHeight="500.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.ecep.contract.controller.permission.EmployeeRoleManagerWindowController">
<BorderPane fx:id="root" maxHeight="900" maxWidth="1024" minHeight="300" minWidth="200" prefHeight="500.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/22"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.ecep.contract.controller.permission.EmployeeRoleWindowController">
<center>
<TabPane fx:id="tabPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minWidth="150.0" tabClosingPolicy="UNAVAILABLE" tabMaxWidth="100.0" tabMinWidth="40.0">
<tabs>

View File

@@ -9,7 +9,6 @@
<Menu text="文件(_F)">
<items>
<MenuItem onAction="#onCreateNewAction" text="新建…(_N)" />
<MenuItem onAction="#onReBuildFilesAction" text="同步…(_R)" />
<SeparatorMenuItem mnemonicParsing="false" />
<MenuItem mnemonicParsing="false" text="Preferences…" />
</items>

View File

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

View File

@@ -1,27 +1,14 @@
package com.ecep.contract.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.ecep.contract.util.HibernateProxyUtils;
import com.ecep.contract.vo.EmployeeRoleVo;
import com.ecep.contract.vo.FunctionVo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.Objects;
@Getter
@Setter
@jakarta.persistence.Entity
@@ -56,12 +43,12 @@ public class EmployeeRole implements IdentityEntity, NamedEntity, Voable<Employe
@Column(name = "IS_ACTIVE")
private boolean active = true;
@ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.MERGE })
@ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(name = "EMPLOYEE_ROLE_FUNCTIONS", joinColumns = @JoinColumn(name = "ROLE_ID"), inverseJoinColumns = @JoinColumn(name = "FUNC_ID"))
@JsonIgnore
private java.util.List<Function> functions = new java.util.ArrayList<>();
@ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.MERGE })
@ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(name = "EMPLOYEE_ROLES", joinColumns = @JoinColumn(name = "ROLE_ID"), inverseJoinColumns = @JoinColumn(name = "EMPLOYEE_ID"))
@JsonIgnore
private java.util.List<Employee> employees = new java.util.ArrayList<>();
@@ -93,22 +80,6 @@ public class EmployeeRole implements IdentityEntity, NamedEntity, Voable<Employe
vo.setSystemAdministrator(isSystemAdministrator());
vo.setManager(isManager());
vo.setActive(isActive());
// 转换functions列表
if (getFunctions() != null && !getFunctions().isEmpty()) {
List<FunctionVo> functionVos = new ArrayList<>();
for (Function function : getFunctions()) {
if (function != null) {
FunctionVo functionVo = new FunctionVo();
functionVo.setId(function.getId());
functionVo.setName(function.getName());
functionVo.setKey(function.getKey());
functionVos.add(functionVo);
}
}
vo.setFunctions(functionVos);
}
return vo;
}
}

View File

@@ -12,7 +12,7 @@ import java.util.Map;
import java.util.function.Consumer;
public class HttpJsonUtils {
public static void post(String url,
public static void post(String url, Map<String, String> headers,
Consumer<Map<String, Object>> data,
Consumer<JsonNode> consumer,
ObjectMapper objectMapper, Proxy proxy) throws IOException {
@@ -24,6 +24,10 @@ public class HttpJsonUtils {
conn.usingProxy();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
if (headers != null) {
headers.forEach(conn::setRequestProperty);
}
conn.setDoOutput(true);
conn.setDoInput(true);
PrintWriter writer = new PrintWriter(conn.getOutputStream());
@@ -37,12 +41,16 @@ public class HttpJsonUtils {
}
public static void get(String url, Consumer<JsonNode> consumer, ObjectMapper objectMapper, Proxy proxy) throws IOException {
public static void get(String url, Map<String, String> headers,
Consumer<JsonNode> consumer, ObjectMapper objectMapper, Proxy proxy) throws IOException {
URI uri = URI.create(url);
HttpURLConnection conn = (HttpURLConnection) (proxy == null ? uri.toURL().openConnection() : uri.toURL().openConnection(proxy));
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json, text/plain, */*");
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36");
if (headers != null) {
headers.forEach(conn::setRequestProperty);
}
try (InputStream is = conn.getInputStream()) {
JsonNode json = objectMapper.readTree(is);
consumer.accept(json);

View File

@@ -1,11 +1,14 @@
package com.ecep.contract.vo;
import java.io.Serializable;
import com.ecep.contract.model.IdentityEntity;
import com.ecep.contract.model.NamedEntity;
import com.ecep.contract.ContractPayWay;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Data;
@Data
@@ -15,6 +18,11 @@ public class ContractVo implements IdentityEntity, NamedEntity, CompanyBasedVo,
private String guid;
private String code;
private String name;
/**
* 是否锁定合同名称, 锁定后不允许修改合同名称
*/
private boolean nameLocked = false;
/**
* 合同对应的合作方公司
*/

View File

@@ -18,6 +18,7 @@ public class EmployeeRoleVo implements IdentityEntity, NamedEntity, Serializable
private boolean manager = false;
private boolean active = true;
private String description;
private List<FunctionVo> functions;
// 角色功能通过功能列表查询
// private List<FunctionVo> functions;
}

View File

@@ -0,0 +1,168 @@
# EmployeesSyncTask WebSocket升级验收报告
## 任务概述
将客户端和服务器端的EmployeesSyncTask升级以支持WebSocket通信实现实时的任务执行反馈。
## 实现成果
### 1. 客户端WebSocket支持 ✅
**文件**: `client/src/main/java/com/ecep/contract/task/EmployeesSyncTask.java`
**实现内容**:
- 继承 `Tasker<Object>`
- 实现 `WebSocketClientTasker` 接口
- 添加 `getTaskName()` 方法返回任务名称 "EmployeesSyncTask"
- 添加 `updateProgress()` 方法支持进度更新
- 重写 `execute()` 方法通过 `callRemoteTask()` 远程调用服务器端任务
- 完整的错误处理和日志记录
**关键代码特性**:
```java
@Override
protected Object execute(MessageHolder holder) {
MessageHolder msgHolder = holder.sub("执行用友U8系统员工信息同步");
msgHolder.debug("开始执行员工同步任务...");
try {
return callRemoteTask("EmployeesSyncTask", msgHolder);
} catch (Exception e) {
msgHolder.error("执行任务失败: " + e.getMessage());
logger.error("EmployeesSyncTask 执行失败", e);
throw new RuntimeException(e);
}
}
```
### 2. 服务器端WebSocket支持和注册 ✅
**文件**: `server/src/main/java/com/ecep/contract/cloud/u8/EmployeesSyncTask.java`
**实现内容**:
- 继承 `Tasker<Object>`
- 实现 `WebSocketServerTasker` 接口
- 添加WebSocket通信相关属性和处理器
- 实现完整的WebSocket服务器端接口方法:
- `init(JsonNode argsNode)` - 初始化参数处理
- `setSession(SessionInfo session)` - 设置WebSocket会话
- `setMessageHandler(BiConsumer<Message, Boolean>)` - 消息处理
- `setTitleHandler(BiConsumer<String, Boolean>)` - 标题更新
- `setPropertyHandler(BiConsumer<String, Object>)` - 属性设置
- `setProgressHandler(BiConsumer<Long, Long>)` - 进度更新
- 保持原有业务逻辑完整不变
- 集成消息推送和进度报告机制
**关键WebSocket集成**:
```java
@Override
protected Object execute(MessageHolder holder) throws Exception {
YongYouU8Service service = SpringApp.getBean(YongYouU8Service.class);
// 发送任务开始消息
if (messageHandler != null) {
messageHandler.accept(Message.info("开始从U8系统同步员工信息"), false);
}
// ... 业务逻辑执行 ...
// 发送任务完成消息
if (messageHandler != null) {
messageHandler.accept(Message.info("员工信息同步任务完成,共处理 " + counter.get() + " 条记录"), false);
}
}
```
### 3. 任务注册配置 ✅
**文件**: `server/src/main/resources/tasker_mapper.json`
**实现内容**:
- 添加任务映射: `"EmployeesSyncTask": "com.ecep.contract.cloud.u8.EmployeesSyncTask"`
- 确保任务可以通过WebSocket被发现和调用
- 与其他已注册任务保持一致的配置格式
## 质量验证
### 编译验证 ✅
- Maven编译成功无编译错误
- 所有依赖正确解析
- 代码符合项目编码规范
### 接口一致性验证 ✅
- 客户端实现完全符合 `WebSocketClientTasker` 接口规范
- 服务器端实现完全符合 `WebSocketServerTasker` 接口规范
- 任务名称 "EmployeesSyncTask" 在客户端和服务器端保持一致
- 参数传递和返回类型匹配
### 业务逻辑保持 ✅
- 原有员工信息同步业务逻辑完全保留
- U8系统数据查询逻辑未改变
- 员工属性映射和处理逻辑未改变
- 错误处理和日志记录逻辑增强
## WebSocket通信流程
### 1. 任务初始化
```
客户端 -> 服务器: 启动EmployeesSyncTask任务
服务器 -> 客户端: 发送任务开始消息
```
### 2. 执行进度反馈
```
服务器: 每处理一条员工记录
服务器 -> 客户端: 发送进度更新 (当前/总数)
服务器 -> 客户端: 发送详细处理消息
```
### 3. 任务完成
```
服务器: 所有记录处理完成
服务器 -> 客户端: 发送任务完成消息(含处理总数)
```
## 技术特性
### 实时反馈
- 任务开始、进行中、完成状态实时推送
- 进度条实时更新 (当前处理数/总数)
- 详细处理信息实时显示
### 错误处理
- 任务取消时发送取消通知
- 异常情况下发送错误消息
- 客户端和服务器端都有完整的异常捕获
### 性能优化
- 保持原有业务性能
- WebSocket消息处理不影响同步性能
- 异步消息推送,不阻塞业务逻辑
## 验收标准
| 验收项目 | 状态 | 说明 |
|---------|------|------|
| 客户端WebSocket支持 | ✅ | 正确实现WebSocketClientTasker接口 |
| 服务器端WebSocket支持 | ✅ | 正确实现WebSocketServerTasker接口 |
| 任务注册配置 | ✅ | 已在tasker_mapper.json中正确注册 |
| 代码编译 | ✅ | Maven编译无错误 |
| 业务逻辑保持 | ✅ | 原有功能完全保留 |
| 消息通信 | ✅ | 支持开始/进度/完成消息推送 |
| 错误处理 | ✅ | 完整的异常处理机制 |
## 后续建议
1. **集成测试**: 建议在实际U8环境进行端到端测试
2. **性能监控**: 在生产环境中监控WebSocket连接状态和消息传输
3. **用户培训**: 向用户介绍新的实时反馈功能
4. **文档更新**: 更新相关用户手册和技术文档
## 结论
**EmployeesSyncTask WebSocket升级任务已完成**
所有预定目标均已实现:
- 客户端成功支持WebSocket通信
- 服务器端成功实现WebSocket服务器端接口
- 任务配置正确注册
- 代码质量符合项目标准
- 业务功能完整保留并增强
该实现提供了完整的实时反馈机制,显著提升了用户体验,同时保持了系统的稳定性和性能。

View File

@@ -0,0 +1,191 @@
# EmployeesSyncTask WebSocket集成实现任务分解
## 任务概述
根据《EmployeesSyncTask WebSocket集成设计方案》将整个集成工作分解为6个原子任务按依赖关系排序执行。
## 任务依赖图
```mermaid
graph TD
A[任务4: 客户端WebSocket支持] --> C[任务6: 验证和测试]
B[任务5: 服务器端WebSocket支持和注册] --> C
C --> D[任务6: 验证和测试]
```
## 原子任务清单
### 任务4: 实现客户端WebSocket支持
**输入契约:**
- 现有客户端EmployeesSyncTask.java文件13行基础实现
- WebSocketClientTasker接口规范client/src/main/java/com/ecep/contract/WebSocketClientTasker.java
- 项目现有的客户端Tasker实现参考
- UITools任务对话框工具类
**输出契约:**
- 升级后的客户端EmployeesSyncTask.java文件实现WebSocketClientTasker接口
- 支持远程任务调用和实时进度同步
- 完整的错误处理和UI反馈机制
**实现约束:**
- 严格遵循WebSocketClientTasker接口规范
- 保持与现有客户端Tasker实现的一致性
- 实现getTaskName()方法返回"EmployeesSyncTask"
- 集成项目现有的UI反馈机制
**依赖关系:**
- 前置依赖: 无
- 后置依赖: 任务6验证测试
**验收标准:**
- [ ] 客户端代码编译通过
- [ ] 实现WebSocketClientTasker接口所有必需方法
- [ ] getTaskName()返回正确的任务名称
- [ ] 能够通过WebSocket提交任务到服务器
- [ ] 能够接收和显示服务器端推送的进度和消息
- [ ] 异常处理机制完善
---
### 任务5: 实现服务器端WebSocket支持和注册
**输入契约:**
- 现有服务器端EmployeesSyncTask.java文件159行完整业务逻辑
- WebSocketServerTasker接口规范server/src/main/java/com/ecep/contract/service/tasker/WebSocketServerTasker.java
- tasker_mapper.json配置文件server/src/main/resources/
- 现有服务器端WebSocket Tasker实现参考
**输出契约:**
- 升级后的服务器端EmployeesSyncTask.java文件实现WebSocketServerTasker接口
- 更新后的tasker_mapper.json配置文件包含EmployeesSyncTask注册
- 支持WebSocket通信的服务器端任务执行能力
**实现约束:**
- 严格遵循WebSocketServerTasker接口规范
- 保持现有业务逻辑execute方法完全不变
- 在tasker_mapper.json中正确注册任务
- 确保任务名称与客户端调用名称一致
**依赖关系:**
- 前置依赖: 无
- 后置依赖: 任务6验证测试
**验收标准:**
- [ ] 服务器端代码编译通过
- [ ] 实现WebSocketServerTasker接口继承自Callable<Object>
- [ ] 现有业务逻辑保持不变
- [ ] 在tasker_mapper.json中正确注册
- [ ] 服务器启动时任务注册成功
- [ ] 能够接收客户端WebSocket调用并执行任务
---
### 任务6: 验证和测试结果
**输入契约:**
- 升级后的客户端EmployeesSyncTask.java
- 升级后的服务器端EmployeesSyncTask.java
- 更新后的tasker_mapper.json配置文件
- 项目现有的任务监控和测试工具
**输出契约:**
- 完整的集成测试报告
- 发现的问题和解决方案记录
- 最终的验收确认结果
**实现约束:**
- 测试完整的端到端任务执行流程
- 验证WebSocket通信的稳定性和可靠性
- 确保用户体验流畅自然
- 所有边界情况和异常情况都得到妥善处理
**依赖关系:**
- 前置依赖: 任务4和任务5都已完成
- 后置依赖: 无
**验收标准:**
- [ ] 客户端能够成功连接服务器
- [ ] 任务能够成功提交并执行
- [ ] 进度能够实时同步到客户端UI
- [ ] 日志消息能够实时显示在客户端界面
- [ ] 任务取消功能正常工作
- [ ] 网络异常处理机制正常
- [ ] 服务器端任务注册和调用正常
- [ ] 整体功能符合设计要求
---
## 任务执行顺序
### 顺序1: 并行执行准备任务
- **任务4**: 实现客户端WebSocket支持
- **任务5**: 实现服务器端WebSocket支持和注册
### 顺序2: 验证测试
- **任务6**: 验证和测试结果
## 质量门控检查点
### 客户端实现检查点
1. **接口合规性**: 检查WebSocketClientTasker接口方法实现完整性
2. **代码编译性**: 验证客户端代码能够成功编译
3. **集成兼容性**: 确保与现有客户端架构兼容
### 服务器端实现检查点
1. **接口合规性**: 检查WebSocketServerTasker接口方法实现完整性
2. **业务一致性**: 验证现有业务逻辑未受影响
3. **配置正确性**: 确认tasker_mapper.json配置正确
4. **注册成功性**: 验证服务器启动时任务注册成功
### 集成测试检查点
1. **通信连通性**: 验证WebSocket连接建立正常
2. **数据同步性**: 确认进度和消息能够实时同步
3. **异常处理性**: 验证各种异常情况处理正常
4. **用户体验性**: 确认整体用户体验符合预期
## 异常情况处理
### 技术异常
- **编译错误**: 立即停止,修复编译问题后继续
- **WebSocket连接失败**: 检查网络配置和服务器状态
- **任务执行异常**: 查看服务器端日志,定位问题根因
### 业务异常
- **U8系统连接问题**: 检查U8系统网络和凭据
- **数据库操作异常**: 检查数据库连接和权限
- **数据格式异常**: 检查U8系统数据格式兼容性
### 用户体验异常
- **UI响应异常**: 检查客户端UI更新逻辑
- **进度显示异常**: 验证进度计算和同步逻辑
- **消息显示异常**: 检查日志消息处理和显示逻辑
## 风险缓解策略
### 高风险项
1. **WebSocket通信稳定性**: 通过异常处理和重连机制缓解
2. **数据一致性**: 通过保持现有业务逻辑确保数据安全
3. **性能影响**: 通过最小化通信开销降低性能影响
### 中风险项
1. **配置部署复杂性**: 通过详细的配置指南降低部署风险
2. **调试复杂性**: 通过完善的日志输出降低调试难度
### 低风险项
1. **代码风格一致性**: 通过遵循项目规范确保一致性
2. **文档完整性**: 通过详细文档确保可维护性
## 交付物清单
### 代码交付物
- [ ] 升级后的客户端EmployeesSyncTask.java
- [ ] 升级后的服务器端EmployeesSyncTask.java
- [ ] 更新后的tasker_mapper.json配置文件
### 文档交付物
- [ ] EmployeesSyncTask_WebSocket_Design.md已完成
- [ ] EmployeesSyncTask_Tasks.md当前文档
- [ ] 最终验收报告
### 测试交付物
- [ ] 集成测试用例和结果
- [ ] 性能测试结果(如需要)
- [ ] 用户验收确认
这个任务分解确保了EmployeesSyncTask WebSocket集成工作的系统性、可控性和高质量交付。

View File

@@ -0,0 +1,182 @@
# EmployeesSyncTask WebSocket集成设计方案
## 1. 当前状态分析
### 客户端现状
- **文件位置**: `client/src/main/java/com/ecep/contract/task/EmployeesSyncTask.java`
- **代码规模**: 13行基础空实现
- **继承关系**: 继承`Tasker<Object>`
- **问题**: 缺少`WebSocketClientTasker`接口实现,无法与服务器通信
### 服务器端现状
- **文件位置**: `server/src/main/java/com/ecep/contract/cloud/u8/EmployeesSyncTask.java`
- **代码规模**: 159行完整的U8系统员工同步逻辑
- **继承关系**: 继承`Tasker<Object>`
- **问题**: 缺少`WebSocketServerTasker`接口实现无法支持WebSocket通信
- **未注册**: 未在`tasker_mapper.json`中注册
## 2. WebSocket集成设计目标
### 核心目标
1. **统一任务模式**: 客户端调用服务器端任务执行
2. **实时状态同步**: 通过WebSocket实时推送任务进度和消息
3. **保持业务逻辑**: 服务器端业务逻辑保持不变,仅添加通信支持
4. **遵循项目规范**: 严格按照项目现有的WebSocket通信规范
### 设计原则
- **最小修改**: 服务器端现有业务逻辑保持不变
- **接口一致性**: 遵循项目现有的`WebSocketClientTasker``WebSocketServerTasker`接口规范
- **任务名称统一**: 使用相同的任务名称确保客户端能够调用服务器端任务
- **向后兼容**: 确保现有调用方式能够平滑升级
## 3. 客户端设计
### 接口实现要求
```java
public class EmployeesSyncTask extends Tasker<Object> implements WebSocketClientTasker {
// 1. 实现WebSocketClientTasker接口方法
// 2. 实现getTaskName()方法,返回"EmployeesSyncTask"
// 3. 实现消息和进度更新处理
// 4. 集成现有的任务监控和UI反馈机制
}
```
### 功能特性
- **远程任务调用**: 通过WebSocket调用服务器端EmployeesSyncTask
- **进度实时反馈**: 接收服务器端推送的进度更新并更新UI
- **消息日志显示**: 接收服务器端消息并在客户端显示
- **任务状态管理**: 支持任务取消、暂停等状态控制
- **错误处理**: 处理网络异常、任务执行异常等
### UI集成点
- **任务对话框**: 使用项目现有的`UITools.showTaskDialogAndWait()`方法
- **进度显示**: 更新任务进度条和百分比
- **日志输出**: 在任务对话框中显示实时日志消息
## 4. 服务器端设计
### 接口实现要求
```java
public class EmployeesSyncTask extends Tasker<Object> implements WebSocketServerTasker {
// 1. 实现WebSocketServerTasker接口继承自Callable<Object>
// 2. 保持现有的execute()业务逻辑不变
// 3. 自动获得WebSocket消息推送能力
}
```
### 保持的业务逻辑
- **U8数据同步**: 保持现有的U8系统Person数据表读取逻辑
- **员工信息映射**: 保持现有的员工和部门信息映射逻辑
- **数据库操作**: 保持现有的员工信息CRUD操作
- **进度管理**: 保持现有的进度计算和更新逻辑
### 新增的通信能力
- **WebSocket支持**: 通过`WebSocketServerTasker`接口获得WebSocket通信能力
- **消息推送**: 自动将`holder.debug/info/warn`消息推送到客户端
- **进度推送**: 自动将进度更新推送到客户端
- **状态同步**: 支持客户端的任务状态控制(如取消)
## 5. 配置注册设计
### 注册文件位置
`server/src/main/resources/tasker_mapper.json`
### 注册内容
```json
{
"taskers": {
"EmployeesSyncTask": "com.ecep.contract.cloud.u8.EmployeesSyncTask"
}
}
```
### 注册原则
- **任务名称**: 使用类名`EmployeesSyncTask`作为任务名称
- **完全限定名**: 使用完整的Java类路径
- **唯一性**: 确保任务名称在整个项目中唯一
## 6. 通信流程设计
### 完整调用流程
1. **UI触发**: 用户在客户端界面触发员工同步任务
2. **客户端创建**: 客户端创建`EmployeesSyncTask`实例
3. **WebSocket连接**: 建立客户端到服务器的WebSocket连接
4. **任务提交**: 客户端通过WebSocket提交任务请求
5. **服务器执行**: 服务器端创建`EmployeesSyncTask`实例并执行
6. **实时推送**: 服务器端实时推送执行进度和消息到客户端
7. **UI更新**: 客户端接收推送消息并更新UI显示
8. **任务完成**: 任务完成后返回执行结果
### 消息类型
- **进度更新**: `{type: "progress", current: 10, total: 100}`
- **日志消息**: `{type: "message", level: "info", message: "正在处理员工张三"}`
- **任务状态**: `{type: "status", status: "running|cancelled|completed"}`
- **错误信息**: `{type: "error", message: "执行出错信息"}`
## 7. 技术实现要点
### 客户端实现要点
1. **接口导入**: 导入`com.ecep.contract.WebSocketClientTasker`
2. **方法实现**: 实现`getTaskName()``updateTitle()`等接口方法
3. **UI集成**: 集成项目现有的任务监控和UI反馈机制
4. **异常处理**: 处理网络异常、任务执行异常等边界情况
### 服务器端实现要点
1. **接口导入**: 导入`com.ecep.contract.service.tasker.WebSocketServerTasker`
2. **继承关系**: 继承`Tasker<Object>`并实现`WebSocketServerTasker`
3. **业务保持**: 保持现有的execute()方法业务逻辑不变
4. **配置注册**: 在tasker_mapper.json中注册任务
### 配置部署要点
1. **配置更新**: 在tasker_mapper.json中添加EmployeesSyncTask注册
2. **重启服务**: 配置修改后需要重启服务器服务
3. **通信测试**: 验证客户端-服务器端WebSocket通信正常
## 8. 验收标准
### 功能验收
- [ ] 客户端能够成功调用服务器端EmployeesSyncTask
- [ ] 任务执行进度能够实时同步到客户端UI
- [ ] 任务执行日志能够实时显示在客户端界面
- [ ] 任务取消功能能够正常工作
- [ ] 网络异常时能够进行适当处理
### 技术验收
- [ ] 客户端代码编译通过
- [ ] 服务器端代码编译通过
- [ ] 服务器端能够正常启动和注册任务
- [ ] WebSocket连接建立和通信正常
- [ ] 数据传输格式符合项目规范
### 质量验收
- [ ] 代码风格符合项目规范
- [ ] 异常处理机制完善
- [ ] 日志输出清晰明确
- [ ] 用户体验流畅自然
- [ ] 性能表现良好
## 9. 风险评估与缓解
### 技术风险
- **WebSocket连接稳定性**: 通过异常处理和重连机制缓解
- **消息序列化兼容性**: 严格按照项目现有格式实现
- **任务执行超时**: 通过合理的超时设置和进度更新缓解
### 业务风险
- **数据一致性**: 保持现有业务逻辑,确保数据处理正确性
- **性能影响**: WebSocket通信对现有性能影响最小化
- **用户体验**: 确保升级后的用户体验不下降
## 10. 后续扩展建议
### 功能扩展
- **批量任务支持**: 支持同时执行多个员工同步任务
- **任务历史记录**: 保存任务执行历史和结果统计
- **高级筛选**: 支持按部门、状态等条件筛选同步范围
### 技术优化
- **连接池优化**: 优化WebSocket连接池管理
- **消息压缩**: 对大量日志消息进行压缩传输
- **缓存机制**: 对频繁查询的数据进行缓存优化
这个设计方案确保了EmployeesSyncTask能够顺利集成WebSocket通信能力同时保持现有业务逻辑的稳定性和可靠性。

View File

@@ -0,0 +1,182 @@
# Tasker实现规范指南
## 1. Tasker架构概述
Tasker是系统中用于执行异步任务的核心框架主要用于处理耗时操作并提供实时进度反馈。Tasker架构包括
- **Tasker基类**:提供任务执行的基础框架和通信功能
- **WebSocketServerTasker接口**定义WebSocket通信任务的规范
- **MessageHolder**:用于在任务执行过程中传递消息和更新进度
## 2. 服务器端Tasker实现规范
### 2.1 基本结构
服务器端Tasker实现应遵循以下结构
```java
@Data
@EqualsAndHashCode(callSuper = true)
public class YourTaskNameTask extends Tasker<Object> implements WebSocketServerTasker {
// 服务依赖
@Setter
private YourService service;
@Override
public void init(JsonNode argsNode) {
// 初始化任务标题
updateTitle("任务标题");
// 初始化参数
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 实现业务逻辑
// 使用holder.info()、holder.error()等方法反馈消息
return null;
}
}
```
### 2.2 关键注意事项
1. **继承与实现**
- 必须继承`Tasker<Object>`基类
- 必须实现`WebSocketServerTasker`接口
2. **方法实现**
- **不要重复实现**Tasker基类已提供的handler设置方法`setMessageHandler``setTitleHandler``setPropertyHandler``setProgressHandler`
- 必须实现`init`方法,设置任务标题和初始化参数
- 必须实现`execute`方法,实现具体业务逻辑
3. **进度和消息反馈**
- 使用`updateProgress`方法提供进度更新
- 使用`MessageHolder``info``error``debug`等方法提供消息反馈
4. **任务取消**
- 在适当的地方检查`isCancelled()`状态,支持任务取消
5. **注册要求**
- 任务类必须在`tasker_mapper.json`中注册
## 3. 客户端Tasker实现规范
客户端Tasker实现应遵循
```java
public class YourTaskNameTask extends Tasker<Object> {
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 客户端任务逻辑
return null;
}
}
```
## 4. 通信规范
### 4.1 命名规范
- 客户端和服务器端对应的Tasker类应使用**相同的类名**
- 示例:客户端的`ContractSyncAllTask`对应服务器端的`ContractSyncAllTask`
### 4.2 消息传递
- 使用`MessageHolder`进行消息传递
- 支持不同级别的消息INFO、ERROR、DEBUG
## 5. 最佳实践
1. **任务拆分**
- 将大型任务拆分为多个小任务,提高可维护性
2. **异常处理**
- 捕获并正确处理异常,使用`holder.error()`提供详细错误信息
3. **资源管理**
- 确保及时释放资源,特别是在任务取消的情况下
4. **日志记录**
- 使用`holder.debug()`记录调试信息
- 使用`holder.info()`记录进度信息
5. **状态检查**
- 定期检查`isCancelled()`状态,确保任务可以被及时取消
## 6. 代码示例
### 6.1 服务器端Tasker示例
```java
@Data
@EqualsAndHashCode(callSuper = true)
public class ExampleSyncTask extends Tasker<Object> implements WebSocketServerTasker {
@Setter
private ExampleService exampleService;
@Override
public void init(JsonNode argsNode) {
updateTitle("示例同步任务");
// 从argsNode初始化参数
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
try {
// 获取总记录数
long total = exampleService.getTotalCount();
holder.info("开始同步数据,共" + total + "条记录");
// 分批处理
long processed = 0;
List<ExampleEntity> entities = exampleService.getAllEntities();
for (ExampleEntity entity : entities) {
// 检查是否取消
if (isCancelled()) {
holder.info("任务已取消");
return null;
}
// 处理单个实体
exampleService.processEntity(entity);
processed++;
// 更新进度
updateProgress(processed, total);
if (processed % 100 == 0) {
holder.info("已处理" + processed + "/" + total + "条记录");
}
}
holder.info("同步完成,共处理" + processed + "条记录");
return processed;
} catch (Exception e) {
holder.error("同步失败: " + e.getMessage());
throw e;
}
}
}
```
## 7. 常见错误和避免方法
1. **重复实现handler方法**
- 错误在子类中重复实现setMessageHandler等方法
- 解决删除子类中的这些方法直接使用Tasker基类提供的实现
2. **缺少任务注册**
- 错误任务类没有在tasker_mapper.json中注册
- 解决确保所有服务器端Tasker类都在配置文件中正确注册
3. **类名不匹配**
- 错误客户端和服务器端Tasker类名不一致
- 解决:确保两端使用相同的类名
4. **进度更新不正确**
- 错误:进度计算错误或没有更新
- 解决:正确计算和更新进度,提高用户体验
5. **资源泄漏**
- 错误:未及时关闭数据库连接等资源
- 解决使用try-with-resources或在finally块中关闭资源

View File

@@ -0,0 +1,58 @@
# 天眼查企业信用报告下载操作指南
## 操作目标
使用 playwright MCP服务在天眼查平台上查询并下载目标企业的各类信用报告包括
- 企业分析报告
- 基础版企业信用报告
- 专业版企业信用报告
- 工商官网信息快照(图片格式)
## 准备工作
1. 确保已登录天眼查账户
2. 确认浏览器下载路径设置为:`C:\Users\SQQ\Downloads\`
3. 确保网络连接稳定
## 操作步骤
### 1. 企业搜索
- 页面导航到 `https://www.tianyancha.com/nsearch?key={企业名称}`
- 或者使用如下步骤:
- 在天眼查首页搜索框中输入目标企业全称
- 点击搜索按钮或按回车键
- 在搜索结果中点击目标企业进入详情页
### 2. 生成企业报告
- 在企业详情页找到并点击"报告"按钮
- 在弹出的报告选择框中,依次点击以下报告的"下载报告"按钮:
- 企业分析报告
- 专业版企业信用报告
- 基础版企业信用报告
- 每个报告下载点击有效时,页面上将提示会"报告已生成"
### 3. 下载PDF报告
- 进入个人中心报告页面https://www.tianyancha.com/usercenter/report
- 却换到 `报告下载`
- 等待对应的三个报告的状态变为"报告完成"
- 依次找到对应的报告行
- 点击对应报告行的操作列中的下载图标操作列里有4个图标第二个图标是下载需要鼠标点击才会出现下载界面这个图标是一个 SVG 图片DOM 对象上没有任何文本,不能通过文本定位到这个按钮),不要使用"在线预览"的段落
- 出现 报告下载 窗口
- 格式选中选中PDF格式
- 点击下载按钮,
- 关闭 报告下载 窗口
- 不要使用"在线预览"的段落,那样会打开一个新页面,而不是下载文件
- 不要使用 page.pdf 方法
### 4. 下载工商官网快照
- 在企业详情页找到"工商信息"模块
- 点击"工商官网快照"链接
- 在新打开的页面中点击"下载图片"按钮
### 5. 移动文件
- 将下载的PDF报告和图片文件移动至指定目录`C:\Users\SQQ\Downloads\`
## 注意事项
1. 报告生成需要一定时间,请耐心等待
2. 下载前确保报告状态为"报告完成"
3. 如果报告下载失败,可尝试重新点击下载按钮
4. 工商官网快照为图片格式其他报告为PDF格式
5. 部分高级报告可能需要会员权限才能下载

1929
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"mssql-mcp-server": "^1.2.2"
}
}

View File

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

View File

@@ -6,12 +6,12 @@
<parent>
<groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId>
<version>0.0.134-SNAPSHOT</version>
<version>0.0.135-SNAPSHOT</version>
</parent>
<groupId>com.ecep.contract</groupId>
<artifactId>server</artifactId>
<version>0.0.134-SNAPSHOT</version>
<version>0.0.135-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.134-SNAPSHOT</version>
<version>0.0.135-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -11,6 +11,7 @@ import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import com.ecep.contract.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -25,10 +26,6 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.ecep.contract.BlackReasonType;
import com.ecep.contract.IEntityService;
import com.ecep.contract.QueryService;
import com.ecep.contract.SpringApp;
import com.ecep.contract.cloud.CloudInfo;
import com.ecep.contract.ds.company.model.Company;
import com.ecep.contract.ds.company.model.CompanyBlackReason;
@@ -146,80 +143,6 @@ public class CloudRkService implements IEntityService<CloudRk>, QueryService<Clo
};
}
/**
* 更新黑名单列表
*/
public void updateBlackList(
Company company, CloudRk cloudRk, BlackListUpdateContext context) throws IOException {
List<String> companyNames = new ArrayList<>();
companyNames.add(company.getName());
// fixed 平台API使用企业名称可能记录的是曾用名
companyOldNameRepository.findAllByCompanyId(company.getId()).forEach(oldName -> {
// 歧义的曾用名不采用
if (oldName.getAmbiguity()) {
return;
}
companyNames.add(oldName.getName());
});
List<CompanyBlackReason> reasonList = new ArrayList<>();
List<CompanyBlackReason> dbReasons = companyBlackReasonRepository.findAllByCompany(company);
companyNames.forEach(name -> {
String url = context.getUrl() + URLEncoder.encode(name, StandardCharsets.UTF_8);
try {
HttpJsonUtils.get(url, json -> {
if (!json.has("success") || !json.get("success").asBoolean()) {
System.out.println("json = " + json.toPrettyString());
return;
}
if (json.has("data")) {
JsonNode data = json.get("data");
try {
if (data.has("blackReason")) {
for (JsonNode reason : data.get("blackReason")) {
toCompanyBlackReasonList(company, BlackReasonType.BLACK, reason, dbReasons,
reasonList, context.getObjectMapper());
}
}
if (data.has("greyReason")) {
for (JsonNode reason : data.get("greyReason")) {
toCompanyBlackReasonList(company, BlackReasonType.GRAY, reason, dbReasons,
reasonList, context.getObjectMapper());
}
}
} catch (Exception ex) {
logger.error("{} {},json = {}", company.getName(), ex.getMessage(), json, ex);
throw new RuntimeException(json.toString(), ex);
}
}
// 保存JSON数据到公司目录
String companyPath = company.getPath();
if (StringUtils.hasText(companyPath)) {
File dir = new File(companyPath);
if (dir.exists()) {
File file = new File(dir, FileUtils.FILE_BLACK_LIST_JSON);
try {
objectMapper.writeValue(file, json);
} catch (IOException e) {
logger.warn("Unable Save BlackList to {}, company:{}", file, company.getName(), e);
}
}
}
}, context.getObjectMapper(), context.getSocksProxy());
} catch (IOException e) {
throw new RuntimeException(e);
}
});
if (!reasonList.isEmpty()) {
companyBlackReasonRepository.saveAll(reasonList);
}
cloudRk.setCloudBlackListUpdated(LocalDateTime.now());
}
private void toCompanyBlackReasonList(
Company company, BlackReasonType type,
JsonNode reason, List<CompanyBlackReason> dbReasons,
@@ -381,15 +304,19 @@ public class CloudRkService implements IEntityService<CloudRk>, QueryService<Clo
}
}
// TODO 这个可以无法更新缓存
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "#p1.id"),
})
public void resetTo(Company from, Company to) {
public void resetTo(Company from, Company to, MessageHolder holder) {
List<CloudRk> list = cloudRKRepository.findAllByCompanyId(from.getId());
if (list.isEmpty()) {
holder.debug("No records to reset");
return;
}
for (CloudRk item : list) {
item.setCompany(to);
holder.info("Reset #" + item.getId());
}
cloudRKRepository.saveAll(list);
}

View File

@@ -14,10 +14,7 @@ import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -73,7 +70,15 @@ public class CloudRkCtx extends AbstractCtx implements CloudRkContext {
}
public void post(String url, Consumer<Map<String, Object>> data, Consumer<JsonNode> consumer) throws IOException {
HttpJsonUtils.post(url, data, consumer, getObjectMapper(), getSocksProxy());
Map<String, String> headers = new HashMap<>();
headers.put("token", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXB0X2NvZGUiOiJDNEU0M0lQVCIsImx0cGFfdG9rZW4iOm51bGwsImV4cGlyZV");
HttpJsonUtils.post(url, headers, data, consumer, getObjectMapper(), getSocksProxy());
}
public void get(String url, Consumer<JsonNode> consumer) throws IOException {
Map<String, String> headers = new HashMap<>();
headers.put("token", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXB0X2NvZGUiOiJDNEU0M0lQVCIsImx0cGFfdG9rZW4iOm51bGwsImV4cGlyZV");
HttpJsonUtils.get(url, headers, consumer, getObjectMapper(), getSocksProxy());
}
/**
@@ -162,10 +167,10 @@ public class CloudRkCtx extends AbstractCtx implements CloudRkContext {
for (String name : companyNames) {
String url = api + URLEncoder.encode(name, StandardCharsets.UTF_8);
try {
HttpJsonUtils.get(url, json -> {
get(url, json -> {
applyBlackReason(json, company, cloudRk, reasonList, dbReasons, holder);
saveJsonToFile(company, json, "black-" + name + ".json", holder);
}, getObjectMapper(), getSocksProxy());
});
} catch (IOException e) {
catchException(e, holder);
}
@@ -466,16 +471,15 @@ public class CloudRkCtx extends AbstractCtx implements CloudRkContext {
Company company, CloudRk cloudRk, MessageHolder holder) throws IOException {
String url = getConfService().getString(CloudRkService.KEY_ENT_FUZZY_URL);
List<CloudRkService.EntInfo> results = new ArrayList<>();
ObjectMapper objectMapper = getObjectMapper();
try {
holder.debug("POST " + url);
HttpJsonUtils.post(url, data -> {
post(url, data -> {
data.put("theKey", company.getName());
data.put("get", true);
}, json -> {
applyEnterpriseQuery(json, company, cloudRk, results, holder);
saveJsonToFile(company, json, "fuzzy.json", holder);
}, objectMapper, getSocksProxy());
});
} catch (IOException ex) {
catchException(ex, holder);
}

View File

@@ -5,7 +5,6 @@ import java.util.Optional;
import java.util.stream.Collectors;
import com.ecep.contract.QueryService;
import com.ecep.contract.ds.other.model.CloudRk;
import com.ecep.contract.util.SpecificationUtils;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
@@ -146,10 +145,15 @@ public class CloudTycService implements IEntityService<CloudTyc>, QueryService<C
}
}
public void resetTo(Company from, Company to) {
public void resetTo(Company from, Company to, MessageHolder holder) {
List<CloudTyc> list = cloudTycRepository.findAllByCompanyId(from.getId());
if (list.isEmpty()) {
holder.debug("No records to reset");
return;
}
for (CloudTyc item : list) {
item.setCompany(to);
holder.info("Reset #" + item.getId());
}
cloudTycRepository.saveAll(list);
}

View File

@@ -5,7 +5,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import com.ecep.contract.vo.ContractGroupVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
@@ -13,28 +12,53 @@ import org.springframework.beans.BeansException;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.contract.service.ContractGroupService;
import com.ecep.contract.model.ContractGroup;
import com.ecep.contract.service.tasker.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Setter;
/**
* 同步合同分组
* 合同分组同步任务
* <p>
* 该类实现了从U8系统同步合同分组到本地系统的功能
* <p>
* <b>实现特点:</b>
* 1. 继承Tasker基类获取通信框架和消息处理功能
* 2. 实现WebSocketServerTasker接口以支持WebSocket通信
* 3. 使用ContractGroupService处理具体的分组数据同步逻辑
* 4. 提供进度反馈和状态更新
* <p>
* <b>工作流程:</b>
* 1. init方法初始化任务标题
* 2. execute方法读取U8系统中的所有合同分组
* 3. 遍历并同步每个分组数据到本地
* 4. 提供实时进度更新
* <p>
* <b>注意事项:</b>
* 1. 不重复实现Tasker基类已提供的handler设置方法
* 2. 使用MessageHolder进行消息反馈
* 3. 在执行过程中检查cancelled状态以支持任务取消
* <p>
* <b>使用示例:</b>
* 该任务通常由WebSocketServerTaskManager自动实例化和执行通过WebSocket与客户端通信
*/
public class ContractGroupSyncTask extends Tasker<Object> {
public class ContractGroupSyncTask extends Tasker<Object> implements WebSocketServerTasker {
private static final Logger logger = LoggerFactory.getLogger(ContractGroupSyncTask.class);
@Setter
private ContractGroupService contractGroupService;
public ContractGroupSyncTask() {
updateTitle("用友U8系统-同步合同分组信息");
// 初始化逻辑已移至init方法
}
ContractGroupService getContractGroupService() {
if (contractGroupService == null) {
contractGroupService = SpringApp.getBean(ContractGroupService.class);
}
return contractGroupService;
public ContractGroupService getContractGroupService() {
return getCachedBean(ContractGroupService.class);
}
@Override
public void init(JsonNode argsNode) {
updateTitle("用友U8系统-同步合同分组信息");
}
@Override
@@ -44,7 +68,7 @@ public class ContractGroupSyncTask extends Tasker<Object> {
service = SpringApp.getBean(YongYouU8Service.class);
} catch (BeansException e) {
logger.error("can't get bean of YongYouU8Service", e);
holder.error("can't get bean of YongYouU8Service");
holder.info("can't get bean of YongYouU8Service");
return null;
}
@@ -53,7 +77,6 @@ public class ContractGroupSyncTask extends Tasker<Object> {
List<Map<String, Object>> list = service.queryAllContractGroup();
int size = list.size();
holder.debug("总共读取 CM_Group 数据 " + size + "");
for (Map<String, Object> map : list) {
if (isCancelled()) {
holder.info("Cancelled");
@@ -64,7 +87,6 @@ public class ContractGroupSyncTask extends Tasker<Object> {
// 更新进度
updateProgress(counter.incrementAndGet(), size);
}
return null;
}

View File

@@ -13,22 +13,20 @@ import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.contract.service.ContractKindService;
import com.ecep.contract.model.ContractKind;
import com.ecep.contract.service.tasker.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Setter;
/**
* 同步合同分类
*/
public class ContractKindSyncTask extends Tasker<Object> {
public class ContractKindSyncTask extends Tasker<Object> implements WebSocketServerTasker {
private static final Logger logger = LoggerFactory.getLogger(ContractKindSyncTask.class);
@Setter
private ContractKindService contractKindService;
public ContractKindSyncTask() {
updateTitle("用友U8系统-同步合同分类信息");
}
ContractKindService getContractKindService() {
if (contractKindService == null) {
contractKindService = SpringApp.getBean(ContractKindService.class);
@@ -36,35 +34,54 @@ public class ContractKindSyncTask extends Tasker<Object> {
return contractKindService;
}
@Override
public void init(JsonNode argsNode) {
// 初始化任务标题
updateTitle("用友U8系统-同步合同分类信息");
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
holder.info("开始执行合同类型同步任务...");
updateProgress(0, 100);
YongYouU8Repository repository = null;
try {
repository = SpringApp.getBean(YongYouU8Repository.class);
} catch (BeansException e) {
logger.error("can't get bean of YongYouU8Repository", e);
holder.error("can't get bean of YongYouU8Repository");
return null;
}
AtomicInteger counter = new AtomicInteger(0);
logger.info("读取 U8 系统 CM_Kind 数据...");
List<Map<String, Object>> list = repository.queryAllContractKind();
int size = list.size();
holder.debug("总共读取 CM_Kind 数据 " + size + "");
AtomicInteger counter = new AtomicInteger(0);
logger.info("正在读取U8系统中的合同分类数据...");
for (Map<String, Object> map : list) {
if (isCancelled()) {
holder.info("Cancelled");
return null;
List<Map<String, Object>> list = repository.queryAllContractKind();
int size = list.size();
holder.debug("共读取到" + size + "条合同分类数据,开始同步...");
for (Map<String, Object> map : list) {
if (isCancelled()) {
holder.info("任务已取消");
return null;
}
MessageHolder sub = holder.sub(counter.get() + "/" + size + ">");
sync(map, sub);
// 更新进度
int progress = (int) ((counter.incrementAndGet() * 100.0) / size);
updateProgress(counter.get(), size);
holder.info("同步进度: " + progress + "%");
}
MessageHolder sub = holder.sub(counter.get() + "/" + size + ">");
sync(map, sub);
// 更新进度
updateProgress(counter.incrementAndGet(), size);
}
return null;
holder.info("合同类型同步任务执行完成");
updateProgress(100, 100);
return null;
} catch (BeansException e) {
logger.error("无法获取YongYouU8Repository实例", e);
holder.error("无法获取YongYouU8Repository实例");
throw e;
} catch (Exception e) {
logger.error("合同类型同步任务执行失败", e);
holder.error("合同类型同步任务执行失败: " + e.getMessage());
throw e;
}
}
private void sync(Map<String, Object> map, MessageHolder holder) {

View File

@@ -13,58 +13,75 @@ import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.contract.service.ContractTypeService;
import com.ecep.contract.model.ContractType;
import com.ecep.contract.service.tasker.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Setter;
/**
* 同步合同类型
*/
public class ContractTypeSyncTask extends Tasker<Object> {
public class ContractTypeSyncTask extends Tasker<Object> implements WebSocketServerTasker {
private static final Logger logger = LoggerFactory.getLogger(ContractTypeSyncTask.class);
@Setter
private ContractTypeService contractTypeService;
public ContractTypeSyncTask() {
@Override
public void init(JsonNode argsNode) {
// 初始化任务标题
updateTitle("用友U8系统-同步合同类型信息");
}
ContractTypeService getContractTypeService() {
if (contractTypeService == null) {
contractTypeService = SpringApp.getBean(ContractTypeService.class);
}
return contractTypeService;
return getCachedBean(ContractTypeService.class);
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 初始化消息
holder.info("开始执行合同类型同步任务...");
updateProgress(0, 100);
YongYouU8Repository repository = null;
try {
repository = SpringApp.getBean(YongYouU8Repository.class);
} catch (BeansException e) {
logger.error("can't get bean of YongYouU8Service", e);
holder.error("can't get bean of YongYouU8Service");
return null;
}
AtomicInteger counter = new AtomicInteger(0);
logger.info("读取 U8 系统 CM_Type,CM_TypeClass 数据...");
List<Map<String, Object>> list = repository.queryAllContractType();
int size = list.size();
holder.debug("总共读取 CM_Type,CM_TypeClass 数据 " + size + "");
AtomicInteger counter = new AtomicInteger(0);
logger.info("正在读取U8系统中的合同类型数据...");
for (Map<String, Object> map : list) {
if (isCancelled()) {
holder.info("Cancelled");
return null;
List<Map<String, Object>> list = repository.queryAllContractType();
int size = list.size();
holder.debug("共读取到" + size + "条合同类型数据,开始同步...");
for (Map<String, Object> map : list) {
if (isCancelled()) {
holder.info("任务已取消");
return null;
}
MessageHolder sub = holder.sub(counter.get() + "/" + size + "> ");
sync(map, sub);
// 更新进度
int progress = (int) ((counter.incrementAndGet() * 100.0) / size);
updateProgress(counter.get(), size);
sub.info("同步进度: " + progress + "%");
}
MessageHolder sub = holder.sub(counter.get() + "/" + size + ">");
sync(map, sub);
// 更新进度
updateProgress(counter.incrementAndGet(), size);
}
return null;
holder.info("合同类型同步任务执行完成");
updateProgress(100, 100);
return null;
} catch (BeansException e) {
logger.error("无法获取YongYouU8Repository实例", e);
holder.error("无法获取YongYouU8Repository实例");
throw e;
} catch (Exception e) {
logger.error("合同类型同步任务执行失败", e);
holder.error("合同类型同步任务执行失败: " + e.getMessage());
throw e;
}
}
private void sync(Map<String, Object> map, MessageHolder holder) {

View File

@@ -5,37 +5,38 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import com.ecep.contract.ds.customer.service.CustomerCatalogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.customer.service.CustomerCatalogService;
import com.ecep.contract.ds.customer.service.CustomerService;
import com.ecep.contract.model.CustomerCatalog;
import com.ecep.contract.service.tasker.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.ecep.contract.vo.CustomerCatalogVo;
import lombok.Setter;
import com.fasterxml.jackson.databind.JsonNode;
/**
* 同步客户分类
*/
public class CustomerClassSyncTask extends Tasker<Object> {
public class CustomerClassSyncTask extends Tasker<Object> implements WebSocketServerTasker {
private static final Logger logger = LoggerFactory.getLogger(CustomerClassSyncTask.class);
@Setter
private CustomerService companyCustomerService;
public CustomerClassSyncTask() {
updateTitle("用友U8系统-同步客户分类信息");
}
CustomerService getCompanyCustomerService() {
if (companyCustomerService == null) {
companyCustomerService = SpringApp.getBean(CustomerService.class);
}
return companyCustomerService;
return getCachedBean(CustomerService.class);
}
@Override
public void init(JsonNode argsNode) {
// 初始化参数,当前任务不需要特殊参数
updateTitle("用友U8系统-同步客户分类信息");
}
@Override
@@ -45,27 +46,29 @@ public class CustomerClassSyncTask extends Tasker<Object> {
repository = SpringApp.getBean(YongYouU8Repository.class);
} catch (BeansException e) {
logger.error("can't get bean of YongYouU8Repository", e);
holder.error("can't get bean of YongYouU8Repository");
holder.error("获取YongYouU8Repository失败: " + e.getMessage());
return null;
}
AtomicInteger counter = new AtomicInteger(0);
logger.info("读取 U8 系统 CustomerClass 数据...");
logger.info("开始读取U8系统客户分类数据...");
List<Map<String, Object>> list = repository.queryAllCustomerClass();
int size = list.size();
holder.debug("总共读取 CustomerClass 数据 " + size + " ");
holder.debug("总共读取" + size + "客户分类数据");
for (Map<String, Object> map : list) {
if (isCancelled()) {
holder.info("Cancelled");
holder.info("任务已取消");
return null;
}
MessageHolder sub = holder.sub(counter.get() + "/" + size + ">");
sync(map, sub);
// 更新进度
updateProgress(counter.incrementAndGet(), size);
int current = counter.incrementAndGet();
updateProgress(current, size);
}
holder.info("客户分类数据同步完成");
return null;
}
@@ -74,33 +77,40 @@ public class CustomerClassSyncTask extends Tasker<Object> {
String code = (String) map.get("cCCCode");
String name = (String) map.get("cCCName");
holder.debug("处理客户分类:" + code + " - " + name);
var catalogService = getCachedBean(CustomerCatalogService.class);
CustomerCatalogVo customerCatalog = catalogService.findByCode(code);
if (customerCatalog == null) {
CustomerCatalog v1 = new CustomerCatalog();
v1.setCode(code);
v1.setName(name);
v1 = catalogService.save(v1);
holder.info("新建客户分类:" + code);
customerCatalog = v1.toVo();
}
try {
CustomerCatalogVo customerCatalog = catalogService.findByCode(code);
if (customerCatalog == null) {
CustomerCatalog v1 = new CustomerCatalog();
v1.setCode(code);
v1.setName(name);
v1 = catalogService.save(v1);
holder.info("新建客户分类:" + code);
customerCatalog = v1.toVo();
}
if (!Objects.equals(customerCatalog.getCode(), code)) {
customerCatalog.setCode(code);
holder.info("客户分类代码:" + customerCatalog.getCode() + " -> " + code);
modified = true;
}
if (!Objects.equals(customerCatalog.getName(), name)) {
customerCatalog.setName(name);
holder.info("客户分类名称:" + customerCatalog.getName() + " -> " + name);
modified = true;
}
if (!Objects.equals(customerCatalog.getCode(), code)) {
customerCatalog.setCode(code);
holder.info("客户分类代码:" + customerCatalog.getCode() + " -> " + code);
modified = true;
}
if (!Objects.equals(customerCatalog.getName(), name)) {
customerCatalog.setName(name);
holder.info("客户分类名称:" + customerCatalog.getName() + " -> " + name);
modified = true;
}
if (modified) {
var v1 = catalogService.getById(customerCatalog.getId());
catalogService.updateByVo(v1, customerCatalog);
catalogService.save(v1);
if (modified) {
var v1 = catalogService.getById(customerCatalog.getId());
catalogService.updateByVo(v1, customerCatalog);
catalogService.save(v1);
}
} catch (Exception e) {
logger.error("同步客户分类失败: " + code, e);
holder.error("同步客户分类失败: " + e.getMessage());
}
}
}

View File

@@ -6,8 +6,6 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import com.ecep.contract.ds.customer.service.CompanyCustomerEntityService;
import com.ecep.contract.vo.CompanyCustomerEntityVo;
import org.hibernate.Hibernate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,32 +16,44 @@ import com.ecep.contract.SpringApp;
import com.ecep.contract.cloud.u8.ctx.CompanyCtx;
import com.ecep.contract.cloud.u8.ctx.CustomerCtx;
import com.ecep.contract.constant.CloudServiceConstant;
import com.ecep.contract.ds.company.model.Company;
import com.ecep.contract.ds.company.service.CompanyService;
import com.ecep.contract.ds.contract.tasker.AbstContractRepairTasker;
import com.ecep.contract.ds.customer.service.CustomerService;
import com.ecep.contract.ds.other.model.CloudYu;
import com.ecep.contract.ds.company.model.Company;
import com.ecep.contract.ds.customer.model.CompanyCustomer;
import com.ecep.contract.ds.customer.model.CompanyCustomerEntity;
import com.ecep.contract.ds.customer.service.CompanyCustomerEntityService;
import com.ecep.contract.ds.customer.service.CustomerService;
import com.ecep.contract.ds.other.model.CloudYu;
import com.ecep.contract.service.tasker.WebSocketServerTasker;
import com.ecep.contract.vo.CompanyCustomerEntityVo;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Setter;
/**
* 同步客户任务
*/
public class CustomerSyncTask extends AbstContractRepairTasker {
public class CustomerSyncTask extends AbstContractRepairTasker implements WebSocketServerTasker {
private static final Logger logger = LoggerFactory.getLogger(CustomerSyncTask.class);
private YongYouU8Repository repository;
private final CustomerCtx customerCtx = new CustomerCtx();
@Setter
private YongYouU8Service yongYouU8Service;
public CustomerSyncTask() {
private CustomerService getCustomerService() {
return getCachedBean(CustomerService.class);
}
@Override
public void init(JsonNode argsNode) {
// 初始化参数,当前任务不需要特殊参数
updateTitle("用友U8系统-同步客户");
}
private CustomerService getCompanyCustomerService() {
return customerCtx.getCompanyCustomerService();
@Override
protected Object execute(MessageHolder holder) throws Exception {
repair(holder);
return null;
}
@Override
@@ -106,7 +116,7 @@ public class CustomerSyncTask extends AbstContractRepairTasker {
return;
}
if (!Hibernate.isInitialized(customer)) {
customer = getCompanyCustomerService().getById(customer.getId());
customer = getCustomerService().getById(customer.getId());
}
Company company = customer.getCompany();
if (company == null) {
@@ -130,7 +140,7 @@ public class CustomerSyncTask extends AbstContractRepairTasker {
return;
}
if (!Hibernate.isInitialized(customer)) {
customer = getCompanyCustomerService().getById(customer.getId());
customer = getCustomerService().getById(customer.getId());
}
Company company = customer.getCompany();
if (company == null) {

View File

@@ -6,6 +6,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import com.ecep.contract.vo.DepartmentVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
@@ -14,20 +15,34 @@ import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.other.service.DepartmentService;
import com.ecep.contract.ds.other.service.EmployeeService;
import com.ecep.contract.model.Department;
import com.ecep.contract.model.Employee;
import com.ecep.contract.service.tasker.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.ecep.contract.vo.EmployeeVo;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用友U8系统-同步员工信息
* 支持WebSocket通信的服务器端任务实现
*/
public class EmployeesSyncTask extends Tasker<Object> {
public class EmployeesSyncTask extends Tasker<Object> implements WebSocketServerTasker {
private static final Logger logger = LoggerFactory.getLogger(EmployeesSyncTask.class);
private final AtomicInteger counter = new AtomicInteger(0);
DepartmentService departmentService;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class SyncArgs {
private boolean fullSync = false; // 是否全量同步
private String departmentCode; // 指定部门代码(可选)
}
public EmployeesSyncTask() {
updateTitle("用友U8系统-同步员工信息");
}
@@ -39,18 +54,50 @@ public class EmployeesSyncTask extends Tasker<Object> {
return departmentService;
}
@Override
public void init(JsonNode argsNode) {
// 解析初始化参数,支持空参数调用
if (argsNode != null && argsNode.size() > 0) {
SyncArgs syncArgs = new SyncArgs();
// 检查是否有参数对象
if (argsNode.size() > 0) {
JsonNode firstArg = argsNode.get(0);
if (firstArg.isObject()) {
// 如果是对象参数解析fullSync和departmentCode
if (firstArg.has("fullSync")) {
syncArgs.setFullSync(firstArg.get("fullSync").asBoolean(false));
}
if (firstArg.has("departmentCode")) {
syncArgs.setDepartmentCode(firstArg.get("departmentCode").asText(null));
}
}
}
logger.info("初始化员工同步任务参数: {}", syncArgs);
} else {
logger.info("初始化员工同步任务,使用默认参数");
}
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
YongYouU8Service service = SpringApp.getBean(YongYouU8Service.class);
// 发送任务开始消息
holder.info("开始从U8系统同步员工信息");
holder.debug("读取 U8 系统 Person 数据表...");
List<Map<String, Object>> list = service.queryAllPerson();
int size = list.size();
holder.debug("总共读取 Person 数据 " + size + "");
// 发送总进度信息
updateProgress(10, 1000);
for (Map<String, Object> rs : list) {
if (isCancelled()) {
holder.debug("Cancelled");
holder.debug("任务已取消");
return null;
}
@@ -58,8 +105,13 @@ public class EmployeesSyncTask extends Tasker<Object> {
sync(rs, sub);
// 更新进度
updateProgress(counter.incrementAndGet(), size);
int current = counter.incrementAndGet();
updateProgress(10 + current * 1000 / size, 1000);
}
// 发送任务完成消息
holder.info("员工信息同步任务完成,共处理 " + counter.get() + " 条记录");
return null;
}
@@ -101,7 +153,7 @@ public class EmployeesSyncTask extends Tasker<Object> {
}
if (StringUtils.hasText(departmentCode)) {
Department departmentByCode = getDepartmentService().findByCode(departmentCode);
DepartmentVo departmentByCode = getDepartmentService().findByCode(departmentCode);
if (departmentByCode == null) {
subHolder.warn("部门代码:" + departmentCode + "未匹配到部门");
} else {

View File

@@ -13,19 +13,26 @@ import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.vendor.repository.VendorClassRepository;
import com.ecep.contract.model.VendorCatalog;
import com.ecep.contract.service.tasker.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Setter;
/**
* 同步供应商分类
*/
public class VendorClassSyncTask extends Tasker<Object> {
public class VendorClassSyncTask extends Tasker<Object> implements WebSocketServerTasker {
private static final Logger logger = LoggerFactory.getLogger(VendorClassSyncTask.class);
@Setter
private VendorClassRepository vendorClassRepository;
public VendorClassSyncTask() {
}
@Override
public void init(JsonNode argsNode) {
// 初始化参数,当前任务不需要特殊参数
updateTitle("用友U8系统-同步供应商分类信息");
}
@@ -43,7 +50,7 @@ public class VendorClassSyncTask extends Tasker<Object> {
service = SpringApp.getBean(YongYouU8Service.class);
} catch (BeansException e) {
logger.error("can't get bean of YongYouU8Service", e);
holder.error("can't get bean of YongYouU8Service");
holder.error("获取YongYouU8Service失败: " + e.getMessage());
return null;
}
@@ -51,7 +58,7 @@ public class VendorClassSyncTask extends Tasker<Object> {
logger.info("读取 U8 系统 VendorClass 数据表...");
List<Map<String, Object>> list = service.queryAllVendorClass();
int size = list.size();
holder.debug("总共读取 VendorClass 数据 " + size + " ");
holder.debug("总共读取" + size + "供应商分类数据");
for (Map<String, Object> map : list) {
if (isCancelled()) {
@@ -64,6 +71,8 @@ public class VendorClassSyncTask extends Tasker<Object> {
updateProgress(counter.incrementAndGet(), size);
}
updateMessage("供应商分类数据同步完成");
updateProperty("completed", true);
return null;
}
@@ -73,27 +82,33 @@ public class VendorClassSyncTask extends Tasker<Object> {
String code = (String) map.get("cVCCode");
String name = (String) map.get("cVCName");
VendorClassRepository repository = getVendorClassRepository();
VendorCatalog vendorCatalog = repository.findByCode(code).orElse(null);
if (vendorCatalog == null) {
vendorCatalog = new VendorCatalog();
holder.info("新建供应商分类:" + code);
modified = true;
}
try {
VendorClassRepository repository = getVendorClassRepository();
VendorCatalog vendorCatalog = repository.findByCode(code).orElse(null);
if (vendorCatalog == null) {
vendorCatalog = new VendorCatalog();
holder.info("新建供应商分类:" + code);
modified = true;
}
if (!Objects.equals(vendorCatalog.getCode(), code)) {
vendorCatalog.setCode(code);
holder.info("供应商分类代码:" + vendorCatalog.getCode() + " -> " + code);
modified = true;
}
if (!Objects.equals(vendorCatalog.getName(), name)) {
vendorCatalog.setName(name);
holder.info("供应商分类名称:" + vendorCatalog.getName() + " -> " + name);
modified = true;
}
if (!Objects.equals(vendorCatalog.getCode(), code)) {
vendorCatalog.setCode(code);
holder.info("供应商分类代码:" + vendorCatalog.getCode() + " -> " + code);
modified = true;
}
if (!Objects.equals(vendorCatalog.getName(), name)) {
vendorCatalog.setName(name);
holder.info("供应商分类名称:" + vendorCatalog.getName() + " -> " + name);
modified = true;
}
if (modified) {
repository.save(vendorCatalog);
if (modified) {
repository.save(vendorCatalog);
}
} catch (Exception e) {
logger.error("同步供应商分类失败: " + code, e);
holder.error("同步供应商分类失败: " + e.getMessage());
updateMessage("同步供应商分类失败: " + code + " - " + e.getMessage());
}
}
}

View File

@@ -3,6 +3,8 @@ package com.ecep.contract.cloud.u8;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
@@ -12,6 +14,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import com.ecep.contract.Message;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.cloud.u8.ctx.CompanyCtx;
@@ -25,17 +28,17 @@ import com.ecep.contract.ds.other.model.CloudYu;
import com.ecep.contract.ds.company.model.Company;
import com.ecep.contract.ds.vendor.model.Vendor;
import com.ecep.contract.ds.vendor.model.VendorEntity;
import com.ecep.contract.service.tasker.WebSocketServerTasker;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Setter;
/**
* 供应商同步任务
*/
public class VendorSyncTask extends AbstContractRepairTasker {
public class VendorSyncTask extends AbstContractRepairTasker implements WebSocketServerTasker {
private static final Logger logger = LoggerFactory.getLogger(VendorSyncTask.class);
private final VendorCtx vendorCtx = new VendorCtx();
private VendorService vendorService;
private VendorEntityService vendorEntityService;
private YongYouU8Repository repository;
@Setter
private YongYouU8Service yongYouU8Service;
@@ -45,17 +48,22 @@ public class VendorSyncTask extends AbstContractRepairTasker {
}
private VendorService getVendorService() {
if (vendorService == null) {
vendorService = SpringApp.getBean(VendorService.class);
}
return vendorService;
return getCachedBean(VendorService.class);
}
private VendorEntityService getVendorEntityService() {
if (vendorEntityService == null) {
vendorEntityService = SpringApp.getBean(VendorEntityService.class);
}
return vendorEntityService;
return getCachedBean(VendorEntityService.class);
}
@Override
public void init(JsonNode argsNode) {
// 初始化参数,当前任务不需要特殊参数
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
repair(holder);
return null;
}
@Override

View File

@@ -5,6 +5,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.util.SpecificationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -119,13 +120,13 @@ public class YongYouU8Service
* @param cloudYu Cloud Yu 对象
* @return 更新的 Cloud Yu
*/
@Caching(evict = { @CacheEvict(key = "#cloudYu.id") })
@Caching(evict = {@CacheEvict(key = "#cloudYu.id")})
@Override
public CloudYu save(CloudYu cloudYu) {
return cloudYuRepository.save(cloudYu);
}
@Caching(evict = { @CacheEvict(key = "#cloudYu.id") })
@Caching(evict = {@CacheEvict(key = "#cloudYu.id")})
@Override
public void delete(CloudYu vo) {
CloudYu entity = cloudYuRepository.findById(vo.getId()).orElse(null);
@@ -143,10 +144,15 @@ public class YongYouU8Service
}
}
public void resetTo(Company from, Company to) {
public void resetTo(Company from, Company to, MessageHolder holder) {
List<CloudYu> list = cloudYuRepository.findAllByCompanyId(from.getId());
if (list.isEmpty()) {
holder.debug("No records to reset");
return;
}
for (CloudYu item : list) {
item.setCompany(to);
holder.info("Reset #" + item.getId());
}
cloudYuRepository.saveAll(list);
}

View File

@@ -224,9 +224,16 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
if (updateText(contract::getCode, contract::setCode, code, holder, "合同编号")) {
modified = true;
}
if (updateText(contract::getName, contract::setName, name, holder, "合同名称")) {
modified = true;
// 合同名称是否锁定,锁定后不允许修改合同名称
if (contract.isNameLocked()) {
holder.debug("合同名称已锁定,不允许修改合同名称");
} else {
if (updateText(contract::getName, contract::setName, name, holder, "合同名称")) {
modified = true;
}
}
if (StringUtils.hasText(parentCode)) {
if (updateText(contract::getParentCode, contract::setParentCode, parentCode, holder, "父合同号")) {
modified = true;

View File

@@ -3,7 +3,15 @@ package com.ecep.contract.ds;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* 自定义JPA仓库接口
* 继承JpaRepository和JpaSpecificationExecutor, 提供基本的CRUD操作和查询功能
*
* 所有实体类都应实现此接口, 文档参考 .trace/rules/server_repository_rules.md、.trace/rules/repository_comprehensive_analysis_report.md
*
* @param <T> 实体类型
* @param <ID> 实体ID类型
*/
public interface MyRepository<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
}

View File

@@ -2,6 +2,7 @@ package com.ecep.contract.ds.company.service;
import java.util.List;
import com.ecep.contract.MessageHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -51,15 +52,17 @@ public class CompanyContactService implements IEntityService<CompanyContact>, Qu
return repository.save(contact);
}
public void resetTo(Company from, Company to) {
// 曾用名 关联到 updater
public void resetTo(Company from, Company to, MessageHolder holder) {
// 联系人 关联到 updater
List<CompanyContact> list = repository.findAllByCompany(from);
if (list.isEmpty()) {
holder.info("无联系人");
return;
}
for (CompanyContact oldName : list) {
oldName.setMemo(MyStringUtils.appendIfAbsent(oldName.getMemo(), "转自 " + from.getId()));
oldName.setCompany(to);
for (CompanyContact contact : list) {
contact.setMemo(MyStringUtils.appendIfAbsent(contact.getMemo(), "转自 " + from.getId()));
contact.setCompany(to);
holder.info("关联了 " + contact.getName() + "" + to.getName());
}
repository.saveAll(list);
}

View File

@@ -4,6 +4,8 @@ import java.io.File;
import java.time.LocalDate;
import java.util.List;
import com.ecep.contract.Message;
import com.ecep.contract.MessageHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -163,17 +165,21 @@ public class CompanyOldNameService implements IEntityService<CompanyOldName>, Qu
* @param from from
* @param to to
*/
public void resetTo(Company from, Company to) {
public void resetTo(Company from, Company to, MessageHolder holder) {
// 曾用名 关联到 to
List<CompanyOldName> list = companyOldNameRepository.findAllByCompanyId(from.getId());
if (list.isEmpty()) {
holder.info("无曾用名");
return;
}
for (CompanyOldName oldName : list) {
oldName.setMemo(MyStringUtils.appendIfAbsent(oldName.getMemo(), "转自 " + from.getId()));
oldName.setCompanyId(to.getId());
holder.info("关联了 " + oldName.getName() + "" + to.getName());
}
companyOldNameRepository.saveAll(list);
}
public void deleteByCompany(Company company) {

View File

@@ -10,6 +10,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.ecep.contract.ds.project.service.ProjectService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -92,6 +93,8 @@ public class CompanyService extends EntityService<Company, CompanyVo, Integer>
@Lazy
@Autowired(required = false)
private YongYouU8Service yongYouU8Service;
@Autowired
private ProjectService projectService;
@Override
protected CompanyRepository getRepository() {
@@ -130,7 +133,7 @@ public class CompanyService extends EntityService<Company, CompanyVo, Integer>
protected Specification<Company> buildParameterSpecification(JsonNode paramsNode) {
Specification<Company> spec = null;
// field
spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "name","uniscid", "abbName");
spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "name", "uniscid", "abbName");
return spec;
}
@@ -251,7 +254,7 @@ public class CompanyService extends EntityService<Company, CompanyVo, Integer>
// 合并重复的
for (Company company : removes) {
try {
merge(company, updater);
merge(company, updater, null);
} catch (Exception e) {
logger.error("合并 {} -> {} 时发生错误:{}", company.toPrettyString(), updater.toPrettyString(), e.getMessage(),
e);
@@ -275,11 +278,11 @@ public class CompanyService extends EntityService<Company, CompanyVo, Integer>
* </ul>
* 或者 把关联数据转移到其他公司
* <ul>
* <li>{@link VendorService#resetTo(Company, Company)}</li>
* <li>{@link CustomerService#resetTo(Company, Company)}</li>
* <li>{@link VendorService#resetTo(Company, Company, MessageHolder)}</li>
* <li>{@link CustomerService#mergeTo(Company, Company)}</li>
* <li>{@link CompanyOldNameService#resetTo(Company, Company)}</li>
* <li>{@link CompanyContactService#resetTo(Company, Company)}</li>
* <li>{@link ContractService#resetTo(Company, Company)}</li>
* <li>{@link ContractService#mergeTo(Company, Company, MessageHolder)}</li>
* <li>{@link CompanyContactService#resetTo(Company, Company)}</li>
* </ul>
* </p>
@@ -323,20 +326,20 @@ public class CompanyService extends EntityService<Company, CompanyVo, Integer>
* </ol>
* </p>
*/
public void merge(Company from, Company to) {
// cloudRkService.findById(from.getId());
cloudRkService.resetTo(from, to);
cloudTycService.resetTo(from, to);
yongYouU8Service.resetTo(from, to);
public void merge(Company from, Company to, MessageHolder holder) {
cloudRkService.resetTo(from, to, holder);
cloudTycService.resetTo(from, to, holder);
yongYouU8Service.resetTo(from, to, holder);
companyOldNameService.resetTo(from, to);
companyContactService.resetTo(from, to);
companyOldNameService.resetTo(from, to, holder);
companyContactService.resetTo(from, to, holder);
// 供应商和客户
vendorService.resetTo(from, to);
companyCustomerService.resetTo(from, to);
vendorService.resetTo(from, to, holder);
companyCustomerService.mergeTo(from, to, holder);
contractService.resetTo(from, to);
companyContactService.resetTo(from, to);
contractService.mergeTo(from, to, holder);
projectService.mergeTo(from, to, holder);
repository.delete(from);
if (logger.isInfoEnabled()) {
logger.info("Merge {} to {}", from, to);
@@ -428,7 +431,6 @@ public class CompanyService extends EntityService<Company, CompanyVo, Integer>
*
* @param company 要验证的公司
* @param verifyDate 验证日期
* @param status 状态输出
*/
public boolean verifyEnterpriseStatus(Company company, LocalDate verifyDate, MessageHolder holder) {
// 检查营业状态
@@ -506,7 +508,7 @@ public class CompanyService extends EntityService<Company, CompanyVo, Integer>
}
public Predicate buildSearchPredicate(String searchText, Path<Company> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
CriteriaBuilder builder) {
return builder.or(
builder.like(root.get("name"), "%" + searchText + "%"),
builder.like(root.get("shortName"), "%" + searchText + "%"),

View File

@@ -0,0 +1,102 @@
package com.ecep.contract.ds.company.tasker;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.ds.company.model.Company;
import com.ecep.contract.ds.company.service.CompanyService;
import com.ecep.contract.service.tasker.WebSocketServerTasker;
import com.ecep.contract.ui.Tasker;
import com.ecep.contract.vo.CompanyVo;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* 公司合并任务
* 用于通过WebSocket执行公司合并操作
*/
public class CompanyMergeServerTasker extends Tasker<Object> implements WebSocketServerTasker {
private static final Logger logger = LoggerFactory.getLogger(CompanyMergeServerTasker.class);
private CompanyVo company;
private List<String> nameList;
private CompanyService companyService;
@Override
public void init(JsonNode argsNode) {
companyService = getBean(CompanyService.class);
// 从第一个参数获取公司ID并查找公司
company = companyService.findById(argsNode.get(0).asInt());
// 从第二个参数获取名称列表
JsonNode nameListNode = argsNode.get(1);
nameList = new ArrayList<>();
// 遍历JSON数组将每个元素添加到nameList中
if (nameListNode != null && nameListNode.isArray()) {
for (JsonNode nameNode : nameListNode) {
nameList.add(nameNode.asText());
}
}
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
HashSet<String> nameSet = new HashSet<>(nameList);
nameSet.add(company.getName());
int size = nameSet.size();
int count = 1;
int merge = 0;
HashMap<Integer, Company> targetCompanyMap = new HashMap<>();
for (String name : nameSet) {
MessageHolder sub = holder.sub(count + "/" + size + ":" + name + ">");
if (name == null || name.isEmpty()) {
sub.warn("名称为空");
continue;
}
List<Company> list = companyService.findAllByName(name);
for (Company company : list) {
sub.debug("找到 " + company.getName());
targetCompanyMap.put(company.getId(), company);
}
count++;
}
targetCompanyMap.remove(this.company.getId());
if (targetCompanyMap.isEmpty()) {
holder.warn("没有需要并户的公司");
return null;
}
//
Company targetCompany = companyService.getById(this.company.getId());
for (Company company : targetCompanyMap.values()) {
try {
companyService.merge(company, targetCompany, holder);
holder.info("并户 " + company.getName() + "[" + company.getId() + "] 到当前公司");
merge++;
} catch (Exception e) {
holder.warn("合并 " + company.getName() + " -> " + targetCompany.getName() + " 失败: " + e.getMessage());
logger.warn("合并 {} -> {} 失败: {}", company.getName(), targetCompany.getName(), e.getMessage(), e);
}
}
if (merge == 0) {
holder.info("没有需要并户的公司");
} else {
holder.info("完成合并操作,共合并 " + merge + " 个公司");
}
updateTitle("公司合并任务完成");
return null;
}
}

View File

@@ -57,6 +57,9 @@ public class Contract
@Column(name = "NAME")
private String name;
@Column(name = "NAME_LOCKED")
private boolean nameLocked = false;
/**
* 合同状态U8 系统中的合同状态,目前含义未知
* <table>
@@ -301,20 +304,21 @@ public class Contract
vo.setId(id);
vo.setGuid(getGuid());
vo.setCode(getCode());
vo.setName(name);
vo.setName(getName());
vo.setNameLocked(isNameLocked());
if (getCompany() != null) {
vo.setCompanyId(getCompany().getId());
}
if (group != null) {
if (getGroup() != null) {
vo.setGroupId(group.getId());
}
if (type != null) {
if (getType() != null) {
vo.setTypeId(type.getId());
}
if (kind != null) {
if (getKind() != null) {
vo.setKindId(kind.getId());
}
if (project != null) {
if (getProject() != null) {
vo.setProject(project.getId());
}
vo.setParentCode(getParentCode());
@@ -356,5 +360,4 @@ public class Contract
vo.setVersion(getVersion());
return vo;
}
}

View File

@@ -9,6 +9,7 @@ import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.ecep.contract.*;
import com.ecep.contract.ds.contract.model.ContractItem;
import com.ecep.contract.vo.ContractCatalogVo;
import jakarta.persistence.criteria.Root;
@@ -25,11 +26,6 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.ecep.contract.ContractPayWay;
import com.ecep.contract.EntityService;
import com.ecep.contract.IEntityService;
import com.ecep.contract.QueryService;
import com.ecep.contract.SpringApp;
import com.ecep.contract.constant.ContractConstant;
import com.ecep.contract.ds.company.service.CompanyService;
import com.ecep.contract.ds.contract.repository.ContractRepository;
@@ -39,7 +35,6 @@ import com.ecep.contract.ds.project.service.ProjectService;
import com.ecep.contract.ds.company.model.Company;
import com.ecep.contract.ds.customer.model.CompanyCustomer;
import com.ecep.contract.ds.contract.model.Contract;
import com.ecep.contract.model.ContractCatalog;
import com.ecep.contract.ds.project.model.Project;
import com.ecep.contract.ds.vendor.model.Vendor;
import com.ecep.contract.service.VoableService;
@@ -421,14 +416,16 @@ public class ContractService extends EntityService<Contract, ContractVo, Integer
/**
* 重置合同关联企业
*/
public void resetTo(Company from, Company to) {
public void mergeTo(Company from, Company to, MessageHolder holder) {
List<Contract> list = contractRepository.findAllByCompanyId(from.getId());
if (list.isEmpty()) {
holder.info("没有合同需要处理");
return;
}
for (Contract contract : list) {
contract.setDescription(MyStringUtils.appendIfAbsent(contract.getDescription(), "转自 " + from.getId()));
contract.setCompany(to);
holder.info("Reset #" + contract.getId());
}
contractRepository.saveAll(list);
}
@@ -453,6 +450,7 @@ public class ContractService extends EntityService<Contract, ContractVo, Integer
contract.setCode(vo.getCode());
contract.setName(vo.getName());
contract.setNameLocked(vo.isNameLocked());
contract.setGuid(vo.getGuid());
contract.setState(vo.getState());
contract.setPath(vo.getPath());

View File

@@ -22,12 +22,22 @@ public class ContractFilesRebuildTasker extends Tasker<Object> implements WebSoc
private ContractVo contract;
private boolean repaired = false;
public ContractFilesRebuildTasker() {
updateTitle("合同文件重置");
@Override
public void init(JsonNode argsNode) {
log.info("初始化合同文件重建任务,参数: {}", argsNode);
// 从JSON参数中提取合同信息
if (argsNode != null && !argsNode.isEmpty()) {
ContractService contractService = getCachedBean(ContractService.class);
int contractId = argsNode.get(0).asInt();
this.contract = contractService.findById(contractId);
}
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("合同文件重置");
log.info("开始执行合同文件重建任务: {}", contract != null ? contract.getCode() : "未知合同");
try {
@@ -41,23 +51,22 @@ public class ContractFilesRebuildTasker extends Tasker<Object> implements WebSoc
// 执行文件重建逻辑
ContractCtx contractCtx = new ContractCtx();
boolean success = contractCtx.syncContractFiles(contract, holder);
boolean modified = contractCtx.syncContractFiles(contract, holder);
updateProgress(75, 100);
if (success) {
if (modified) {
repaired = true;
updateMessage("合同文件重建成功");
log.info("合同文件重建成功: {}", contract.getCode());
} else {
updateMessage("合同文件重建失败");
log.warn("合同文件重建失败: {}", contract.getCode());
}
updateMessage("合同文件重建成功");
log.info("合同文件重建成功: {}", contract.getCode());
updateProperty("repaired", repaired);
updateProgress(100, 100);
updateMessage("任务完成");
return success;
return modified;
} catch (Exception e) {
log.error("合同文件重建任务执行失败: {}", contract != null ? contract.getCode() : "未知合同", e);
@@ -66,15 +75,4 @@ public class ContractFilesRebuildTasker extends Tasker<Object> implements WebSoc
}
}
@Override
public void init(JsonNode argsNode) {
log.info("初始化合同文件重建任务,参数: {}", argsNode);
// 从JSON参数中提取合同信息
if (argsNode != null && argsNode.size() > 0) {
ContractService contractService = getCachedBean(ContractService.class);
int contractId = argsNode.get(0).asInt();
this.contract = contractService.findById(contractId);
}
}
}

View File

@@ -356,7 +356,7 @@ public class CustomerService extends CompanyBasicService
* @param from 要转移的客户信息所属的公司
* @param to 要转移到的公司
*/
public void resetTo(Company from, Company to) {
public void mergeTo(Company from, Company to, MessageHolder holder) {
// 这里使用Optional对象来处理可能为空的情况
Optional<CompanyCustomer> fromCustomer = repository.findByCompany(from);
if (fromCustomer.isEmpty()) {
@@ -373,7 +373,7 @@ public class CustomerService extends CompanyBasicService
}
// 把 fromCustomer 信息合并到 toCustomer
resetTo(fromCustomer.get(), toCustomer.get());
mergeTo(fromCustomer.get(), toCustomer.get(), holder);
}
@@ -383,7 +383,7 @@ public class CustomerService extends CompanyBasicService
* @param from 源客户对象
* @param to 目标客户对象
*/
public void resetTo(CompanyCustomer from, CompanyCustomer to) {
public void mergeTo(CompanyCustomer from, CompanyCustomer to, MessageHolder holder) {
// file
companyCustomerFileService.resetTo(from, to);
// entity

View File

@@ -2,6 +2,7 @@ package com.ecep.contract.ds.other.controller;
import java.util.Map;
import com.ecep.contract.vo.FunctionVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -73,7 +74,7 @@ public class EmployeeRoleController {
}
@RequestMapping("/getFunctionsByRoleId")
public java.util.List<com.ecep.contract.model.Function> getFunctionsByRoleId(Integer roleId) {
public java.util.List<FunctionVo> getFunctionsByRoleId(Integer roleId) {
return employeeRoleService.getFunctionsByRoleId(roleId);
}
}

Some files were not shown because too many files have changed in this diff Show More