From 2057c3ca671e1279b0ae513df93c25ca3a685c20 Mon Sep 17 00:00:00 2001 From: songqq Date: Thu, 25 Sep 2025 00:14:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E4=B8=8E=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=AB=AFTasker?= =?UTF-8?q?=E9=80=9A=E4=BF=A1=E6=9C=BA=E5=88=B6=E5=8F=8A=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 重构Tasker基类与服务获取逻辑 fix: 修复文件路径显示问题及任务注册加载机制 docs: 添加客户端与服务器端Tasker通信规则文档 style: 优化代码格式与日志输出 build: 添加tasker_mapper.json配置文件 chore: 清理无用代码与文件 --- .trae/rules/project_rules.md | 1 + .../ecep/contract/WebSocketClientSession.java | 12 +- .../company/CompanyWindowController.java | 1 - .../SalesOrderWindowController.java | 2 +- ...mpanyCustomerEvaluationFormUpdateTask.java | 230 +----------------- ...ompanyCustomerManagerWindowController.java | 1 - .../CompanyCustomerNextSignDateTask.java | 34 +++ .../customer/CustomerTabSkinFile.java | 126 +++------- .../tab/ContractTabSkinSaleOrders.java | 2 +- .../controller/tab/SalesOrderTabSkinBase.java | 2 +- .../tab/SalesOrderTabSkinBillVoucher.java | 2 +- .../tab/SalesOrderTabSkinItems.java | 2 +- .../table/cell/FilePathTableCell.java | 39 ++- .../controller/vendor/VendorTabSkinFile.java | 1 - .../service/CompanyCustomerFileService.java | 11 +- .../service/CompanyCustomerService.java | 19 +- .../java/com/ecep/contract/task/Tasker.java | 36 ++- ...lient_server_tasker_communication_rules.md | 215 ++++++++++++++++ ...yCustomerEvaluationFormFileRepository.java | 3 + ...panyCustomerEvaluationFormFileService.java | 130 ++++++++++ .../service/CompanyCustomerFileService.java | 69 +++--- .../service/CompanyCustomerService.java | 24 +- ...mpanyCustomerEvaluationFormUpdateTask.java | 214 ++++++++++++++++ .../CompanyCustomerNextSignDateTask.java | 117 +++++++++ .../CompanyCustomerRebuildFilesTasker.java | 76 ++++++ .../service/WebSocketServerTaskManager.java | 67 +++-- server/src/main/resources/tasker_mapper.json | 11 + src/main/resources/_log4j2.xml | 13 - 28 files changed, 1014 insertions(+), 446 deletions(-) rename client/src/main/java/com/ecep/contract/controller/{customer => contract/sale_order}/SalesOrderWindowController.java (97%) create mode 100644 client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerNextSignDateTask.java create mode 100644 docs/task/client_server_tasker_communication_rules.md create mode 100644 server/src/main/java/com/ecep/contract/ds/customer/service/CompanyCustomerEvaluationFormFileService.java create mode 100644 server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerEvaluationFormUpdateTask.java create mode 100644 server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerNextSignDateTask.java create mode 100644 server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerRebuildFilesTasker.java create mode 100644 server/src/main/resources/tasker_mapper.json delete mode 100644 src/main/resources/_log4j2.xml diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md index 5df84bf..3039efa 100644 --- a/.trae/rules/project_rules.md +++ b/.trae/rules/project_rules.md @@ -23,6 +23,7 @@ - StringConverter 创建规则与实现逻辑 `docs/task/string_converter_implementation_guide.md` - TableCell 规则与逻辑 `docs/task/table_cell_implementation_guide.md` - Service 规则与逻辑 `docs/task/serservice_layer_rules.md` +- 客户端 Tasker 至 服务器端 Tasker 通信规则与逻辑 `docs/task/client_server_tasker_communication_rules.md` ### common 模块 - Java 21 diff --git a/client/src/main/java/com/ecep/contract/WebSocketClientSession.java b/client/src/main/java/com/ecep/contract/WebSocketClientSession.java index a151abb..984b4b8 100644 --- a/client/src/main/java/com/ecep/contract/WebSocketClientSession.java +++ b/client/src/main/java/com/ecep/contract/WebSocketClientSession.java @@ -41,7 +41,6 @@ public class WebSocketClientSession { webSocketService.send(arguments); } - public void onMessage(JsonNode node) { if (node.has("type")) { String type = node.get("type").asText(); @@ -66,15 +65,15 @@ public class WebSocketClientSession { } } + private void handleAsStart(JsonNode args) { + tasker.updateMessage(java.util.logging.Level.INFO, "任务开始"); + } + private void handleAsDone(JsonNode args) { tasker.updateMessage(java.util.logging.Level.INFO, "任务完成"); close(); } - private void handleAsStart(JsonNode args) { - tasker.updateMessage(java.util.logging.Level.INFO, "任务开始"); - } - private void handleAsProgress(JsonNode args) { long current = args.get(0).asLong(); long total = args.get(1).asLong(); @@ -91,7 +90,8 @@ public class WebSocketClientSession { Object object = webSocketService.getObjectMapper().convertValue(value, descriptor.getPropertyType()); System.out.println("object = " + object); System.out.println("descriptor.getWriteMethod() = " + descriptor.getWriteMethod()); - System.out.println("descriptor.getWriteMethod().getParameterTypes() = " + descriptor.getWriteMethod().getParameterTypes()); + System.out.println("descriptor.getWriteMethod().getParameterTypes() = " + + descriptor.getWriteMethod().getParameterTypes()); descriptor.getWriteMethod().invoke(tasker, object); } catch (Exception e) { tasker.updateMessage(java.util.logging.Level.SEVERE, "属性设置失败: " + name + " = " + value); diff --git a/client/src/main/java/com/ecep/contract/controller/company/CompanyWindowController.java b/client/src/main/java/com/ecep/contract/controller/company/CompanyWindowController.java index cb60ed9..748c6b8 100644 --- a/client/src/main/java/com/ecep/contract/controller/company/CompanyWindowController.java +++ b/client/src/main/java/com/ecep/contract/controller/company/CompanyWindowController.java @@ -264,7 +264,6 @@ public class CompanyWindowController public void onCompanyVerifyAction(ActionEvent event) { CompanyVo company = getEntity(); CompanyVerifyTasker task = new CompanyVerifyTasker(); - task.setCompanyService(companyService); task.setCompany(company); UITools.showTaskDialogAndWait("企业合规性验证", task, null); } diff --git a/client/src/main/java/com/ecep/contract/controller/customer/SalesOrderWindowController.java b/client/src/main/java/com/ecep/contract/controller/contract/sale_order/SalesOrderWindowController.java similarity index 97% rename from client/src/main/java/com/ecep/contract/controller/customer/SalesOrderWindowController.java rename to client/src/main/java/com/ecep/contract/controller/contract/sale_order/SalesOrderWindowController.java index 7fec501..9c06c71 100644 --- a/client/src/main/java/com/ecep/contract/controller/customer/SalesOrderWindowController.java +++ b/client/src/main/java/com/ecep/contract/controller/contract/sale_order/SalesOrderWindowController.java @@ -1,4 +1,4 @@ -package com.ecep.contract.controller.customer; +package com.ecep.contract.controller.contract.sale_order; import com.ecep.contract.vo.SalesOrderVo; import org.slf4j.Logger; diff --git a/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerEvaluationFormUpdateTask.java b/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerEvaluationFormUpdateTask.java index 038167e..4bdd622 100644 --- a/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerEvaluationFormUpdateTask.java +++ b/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerEvaluationFormUpdateTask.java @@ -1,240 +1,34 @@ package com.ecep.contract.controller.customer; -import static com.ecep.contract.util.ExcelUtils.setCellValue; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.time.LocalDate; -import java.util.Comparator; -import java.util.List; - -import com.ecep.contract.service.*; +import com.ecep.contract.MessageHolder; +import com.ecep.contract.WebSocketClientTasker; import com.ecep.contract.task.Tasker; -import com.ecep.contract.util.CompanyUtils; -import com.ecep.contract.vo.CompanyCustomerFileVo; import com.ecep.contract.vo.CompanyCustomerVo; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.ss.usermodel.WorkbookFactory; -import org.apache.poi.ss.util.CellRangeAddress; +import lombok.Setter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.util.StringUtils; -import com.ecep.contract.CustomerFileType; -import com.ecep.contract.MessageHolder; -import com.ecep.contract.SpringApp; -import com.ecep.contract.vo.CloudTycVo; -import com.ecep.contract.vo.CompanyVo; -import com.ecep.contract.vo.CompanyCustomerEvaluationFormFileVo; - -import lombok.Setter; - -public class CompanyCustomerEvaluationFormUpdateTask extends Tasker { +public class CompanyCustomerEvaluationFormUpdateTask extends Tasker implements WebSocketClientTasker { private static final Logger logger = LoggerFactory.getLogger(CompanyCustomerEvaluationFormUpdateTask.class); @Setter private CompanyCustomerVo customer; - @Setter - private CompanyService companyService; - private CompanyContactService companyContactService; - private CompanyCustomerService companyCustomerService; - @Setter - private CompanyCustomerFileService companyCustomerFileService; - - private CompanyCustomerService getCompanyCustomerService() { - if (companyCustomerService == null) { - companyCustomerService = SpringApp.getBean(CompanyCustomerService.class); - } - return companyCustomerService; + @Override + public String getTaskName() { + return getClass().getSimpleName(); } - private CompanyContactService getCompanyContactService() { - if (companyContactService == null) { - companyContactService = SpringApp.getBean(CompanyContactService.class); - } - return companyContactService; - } - - private CompanyCustomerFileService getCompanyCustomerFileService() { - if (companyCustomerFileService == null) { - companyCustomerFileService = SpringApp.getBean(CompanyCustomerFileService.class); - } - return companyCustomerFileService; - } - - private CompanyCustomerEvaluationFormFileService getCompanyCustomerEvaluationFormFileService() { - return getBean(CompanyCustomerEvaluationFormFileService.class); - } - - private File getEvaluationFormTemplate() { - return getCompanyCustomerFileService().getEvaluationFormTemplate(); + @Override + public void updateProgress(long current, long total) { + super.updateProgress(current, total); } @Override protected Object execute(MessageHolder holder) throws Exception { - try { - updateEvaluationForm(holder); - } catch (Exception ex) { - updateMessage(ex.getMessage()); - } - return null; + updateTitle("客户评估表更新任务"); + return callRemoteTask(holder, getLocale(), customer.getId()); } - public void updateEvaluationForm(MessageHolder holder) { - if (!StringUtils.hasText(customer.getPath())) { - holder.error("供应商目录未设置,请先设置供应商目录"); - return; - } - File template = getEvaluationFormTemplate(); - if (template == null) { - holder.error("评价表模板文件未设置,请先设置评价表模板文件"); - return; - } - if (!template.exists()) { - holder.error("评价表模板文件 " + template.getAbsolutePath() + " 不存在,请检查"); - return; - } - - File dir = new File(customer.getPath()); - String template_file_name = template.getName(); - File destFile = new File(dir, template_file_name); - - if (destFile.exists()) { - holder.info("表单文件已经存在," + destFile.getName()); - try ( - InputStream inp = new FileInputStream(destFile); - Workbook wb = WorkbookFactory.create(inp)) { - updateEvaluationForm(wb, destFile, holder); - holder.info("评价表已更新"); - } catch (Exception e) { - holder.error(e.getMessage()); - logger.error(e.getMessage(), e); - } - } else { - holder.info("根据模板 " + template_file_name + " 创建表单 " + destFile.getName()); - try ( - InputStream inp = new FileInputStream(template); - Workbook wb = WorkbookFactory.create(inp)) { - updateEvaluationForm(wb, destFile, holder); - holder.info("评价表已创建"); - CompanyCustomerFileVo customerFile = new CompanyCustomerFileVo(); - customerFile.setCustomer(customer.getId()); - customerFile.setFilePath(destFile.getAbsolutePath()); - customerFile.setType(CustomerFileType.General); - save(customerFile); - } catch (Exception e) { - holder.error(e.getMessage()); - logger.error(e.getMessage(), e); - } - } - updateProgress(1, 1); - } - - private void save(CompanyCustomerFileVo customerFile) { - getCompanyCustomerFileService().save(customerFile); - } - - /** - * 更新客户评估表,依据模板创建,如果已经存在生成的文件,则更新评估表 - * - * @param wb work book - * @param destFile 目标文件 - */ - public void updateEvaluationForm(Workbook wb, File destFile, MessageHolder holder) throws IOException { - Integer companyId = customer.getCompanyId(); - CompanyVo company = getCompanyService().findById(companyId); - Sheet sheet = wb.getSheetAt(0); - updateSheet(company, sheet, holder.sub(" - ")); - updateProgress(900, 1000); - // 输出到文件 - try (OutputStream fileOut = new FileOutputStream(destFile)) { - wb.write(fileOut); - } catch (FileNotFoundException e) { - holder.error("写评估表时发生文件错误,请检查评估表是否被打开中:" + e.getMessage()); - logger.error(e.getMessage(), e); - } - } - - private void updateSheet(CompanyVo company, Sheet sheet, MessageHolder holder) { - setCellValue(sheet, "B3", "客户编号:" + CompanyUtils.formatCompanyVendorId(customer.getId())); - setCellValue(sheet, "B4", "客户名称:" + company.getName()); - - LocalDate suggestDate = getCompanyCustomerFileService().getNextSignDate(customer, holder); - if (suggestDate == null) { - suggestDate = LocalDate.now(); - } - setCellValue(sheet, "H3", "评定时间:" + suggestDate); - setCellValue(sheet, "H4", "统一社会信用代码:"); - setCellValue(sheet, "H5", company.getUniscid()); - // 注册所属地 - setCellValue(sheet, "B5", "注册所属地:" + company.getDistrict()); - // 经营状态 - setCellValue(sheet, "D6", "经营状态:" + company.getEntStatus()); - // 成立日期 - setCellValue(sheet, "H6", "成立日期:" + company.getSetupDate()); - // 所属行业 - setCellValue(sheet, "D7", "所属行业:" + company.getIndustry()); - setCellValue(sheet, "D8", - "注册资金:" + company.getRegisteredCapital() + " " + company.getRegisteredCapitalCurrency()); - // 企业类型 - setCellValue(sheet, "H10", "企业类型:" + company.getEntType()); - // 天眼评分 - CloudTycService cloudTycService = SpringApp.getBean(CloudTycService.class); - CloudTycVo cloudTyc = cloudTycService.getOrCreateCloudTyc(company); - setCellValue(sheet, "D10", "天眼评分:" + (cloudTyc.getScore() > 0 ? cloudTyc.getScore() : "")); - - // 检索评估表 - List customerFiles = getCompanyCustomerFileService().findAllByCustomerAndType(customer, - CustomerFileType.EvaluationForm); - - List filteredList = customerFiles.stream().filter(file -> { - return file.getSignDate() != null && file.isValid(); - }) - .sorted(Comparator.comparing(CompanyCustomerFileVo::getSignDate)) - .map(getCompanyCustomerEvaluationFormFileService()::findByCustomerFile) - .toList(); - - if (filteredList.isEmpty()) { - setCellValue(sheet, "C40", "首次评价"); - try { - sheet.addMergedRegion(CellRangeAddress.valueOf("C40:K40")); - } catch (Exception ignored) { - } - } else { - setCellValue(sheet, "C40", "评价日期"); - try { - sheet.addMergedRegion(CellRangeAddress.valueOf("C40:D40")); - } catch (Exception ignored) { - } - setCellValue(sheet, "E40", "经济指标"); - setCellValue(sheet, "F40", "综合指标"); - setCellValue(sheet, "G40", "资信等级"); - String[] CreditLevelTitles = new String[] { "-", "差★", " 一般★★", " 较好★★★", " 好★★★★", " " }; - int baseRow = 40; - for (CompanyCustomerEvaluationFormFileVo form : filteredList) { - CompanyCustomerFileVo customerFile = getCompanyCustomerFileService().findById(form.getCustomerFile()); - setCellValue(sheet, baseRow, 2, String.valueOf(customerFile.getSignDate())); - setCellValue(sheet, baseRow, 4, form.getCatalog()); - setCellValue(sheet, baseRow, 5, form.getLevel()); - if (form.getCreditLevel() == null) { - setCellValue(sheet, baseRow, 6, "-"); - } else { - setCellValue(sheet, baseRow, 6, CreditLevelTitles[form.getCreditLevel()]); - } - try { - sheet.addMergedRegion(new CellRangeAddress(baseRow, baseRow, 2, 3)); - } catch (Exception ignored) { - } - baseRow++; - } - } - } } diff --git a/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerManagerWindowController.java b/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerManagerWindowController.java index 598604c..5b2a1af 100644 --- a/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerManagerWindowController.java +++ b/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerManagerWindowController.java @@ -63,7 +63,6 @@ public class CompanyCustomerManagerWindowController return getCachedBean(CompanyCustomerService.class); } - @Autowired private CompanyService getCompanyService() { return getCachedBean(CompanyService.class); } diff --git a/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerNextSignDateTask.java b/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerNextSignDateTask.java new file mode 100644 index 0000000..d3daeaf --- /dev/null +++ b/client/src/main/java/com/ecep/contract/controller/customer/CompanyCustomerNextSignDateTask.java @@ -0,0 +1,34 @@ +package com.ecep.contract.controller.customer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ecep.contract.MessageHolder; +import com.ecep.contract.WebSocketClientTasker; +import com.ecep.contract.task.Tasker; +import com.ecep.contract.vo.CompanyCustomerVo; + +import lombok.Setter; + +public class CompanyCustomerNextSignDateTask extends Tasker implements WebSocketClientTasker { + private static final Logger logger = LoggerFactory.getLogger(CompanyCustomerNextSignDateTask.class); + + @Setter + private CompanyCustomerVo customer; + + @Override + public String getTaskName() { + return getClass().getSimpleName(); + } + + @Override + public void updateProgress(long current, long total) { + super.updateProgress(current, total); + } + + @Override + protected Object execute(MessageHolder holder) throws Exception { + updateTitle("计算客户下一个评价日期"); + return callRemoteTask(holder, getLocale(), customer.getId()); + } +} \ No newline at end of file diff --git a/client/src/main/java/com/ecep/contract/controller/customer/CustomerTabSkinFile.java b/client/src/main/java/com/ecep/contract/controller/customer/CustomerTabSkinFile.java index 6beebe1..9e56f50 100644 --- a/client/src/main/java/com/ecep/contract/controller/customer/CustomerTabSkinFile.java +++ b/client/src/main/java/com/ecep/contract/controller/customer/CustomerTabSkinFile.java @@ -4,24 +4,20 @@ import java.io.File; import java.time.LocalDate; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ScheduledExecutorService; -import java.util.function.Consumer; -import com.ecep.contract.controller.table.cell.CompanyCustomerFileTableTypeTableCell; import org.springframework.util.StringUtils; import com.ecep.contract.CustomerFileType; import com.ecep.contract.DesktopUtils; -import com.ecep.contract.Message; import com.ecep.contract.MyDateTimeUtils; -import com.ecep.contract.SpringApp; import com.ecep.contract.constant.CompanyCustomerConstant; import com.ecep.contract.controller.table.EditableEntityTableTabSkin; -import com.ecep.contract.model.BaseEnumEntity; +import com.ecep.contract.controller.table.cell.CompanyCustomerFileTableTypeTableCell; +import com.ecep.contract.controller.table.cell.FilePathTableCell; import com.ecep.contract.service.CompanyCustomerEvaluationFormFileService; import com.ecep.contract.service.CompanyCustomerFileService; import com.ecep.contract.service.CompanyCustomerFileTypeService; -import com.ecep.contract.service.CompanyCustomerService; +import com.ecep.contract.task.CompanyCustomerRebuildFilesTasker; import com.ecep.contract.util.FileUtils; import com.ecep.contract.util.FxmlPath; import com.ecep.contract.util.ParamUtils; @@ -31,34 +27,24 @@ import com.ecep.contract.vo.CompanyCustomerEvaluationFormFileVo; import com.ecep.contract.vo.CompanyCustomerFileVo; import com.ecep.contract.vo.CompanyCustomerVo; import com.ecep.contract.vo.CompanyVo; -import com.ecep.contract.vo.CustomerFileTypeLocalVo; import javafx.application.Platform; -import javafx.beans.binding.Bindings; -import javafx.collections.FXCollections; -import javafx.collections.ObservableMap; -import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; -import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.CheckBoxTableCell; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; -import lombok.Setter; @FxmlPath("/ui/company/customer/customer-tab-file.fxml") public class CustomerTabSkinFile extends AbstCompanyCustomerTableTabSkin implements EditableEntityTableTabSkin { - @Setter - private CompanyCustomerFileService companyCustomerFileService; - public TableColumn fileTable_idColumn; public TableColumn fileTable_typeColumn; public TableColumn fileTable_filePathColumn; @@ -91,6 +77,14 @@ public class CustomerTabSkinFile return getCompanyCustomerFileService(); } + public CompanyCustomerFileService getCompanyCustomerFileService() { + return getCachedBean(CompanyCustomerFileService.class); + } + + public CompanyCustomerEvaluationFormFileService getEvaluationFormFileService() { + return getCachedBean(CompanyCustomerEvaluationFormFileService.class); + } + @Override public ParamUtils.Builder getSpecification(CompanyCustomerVo parent) { ParamUtils.Builder params = getSpecification(); @@ -105,16 +99,18 @@ public class CustomerTabSkinFile TableView table = getTableView(); table.disableProperty().bind(viewModel.getPath().isEmpty()); + + // fileTable_idColumn.setCellValueFactory(param -> param.getValue().getId()); CompanyCustomerFileTypeService fileTypeService = getCachedBean(CompanyCustomerFileTypeService.class); fileTable_typeColumn.setCellValueFactory(param -> param.getValue().getType()); fileTable_typeColumn.setCellFactory(CompanyCustomerFileTableTypeTableCell.forTableColumn(fileTypeService)); fileTable_filePathColumn.setCellValueFactory(param -> param.getValue().getFilePath()); - fileTable_filePathColumn.setCellFactory(param -> new FileTableFilePathTableCell()); + fileTable_filePathColumn.setCellFactory(FilePathTableCell.forTableColumn(viewModel.getPath())); fileTable_editFilePathColumn.setCellValueFactory(param -> param.getValue().getEditFilePath()); - fileTable_editFilePathColumn.setCellFactory(param -> new FileTableFilePathTableCell()); + fileTable_editFilePathColumn.setCellFactory(FilePathTableCell.forTableColumn(viewModel.getPath())); fileTable_signDateColumn.setCellValueFactory(param -> param.getValue().getSignDate()); fileTable_validColumn.setEditable(true); @@ -159,11 +155,12 @@ public class CustomerTabSkinFile protected void onTableRowDoubleClickedAction(CompanyCustomerFileViewModel item) { CustomerFileType fileType = item.getType().get(); if (fileType == CustomerFileType.EvaluationForm) { - CompanyCustomerEvaluationFormFileVo evaluationFormFile = getCachedBean( - CompanyCustomerEvaluationFormFileService.class).findByCustomerFile(item.getId().get()); + // 文件不是 Excel 文件时,打开编辑UI if (!FileUtils.withExtensions(item.getFilePath().get(), FileUtils.XLS, FileUtils.XLSX)) { + CompanyCustomerEvaluationFormFileVo evaluationFormFile = getEvaluationFormFileService() + .findByCustomerFile(item.getId().get()); CompanyCustomerEvaluationFormFileWindowController.show(evaluationFormFile, controller.root.getScene().getWindow()); return; @@ -239,17 +236,13 @@ public class CustomerTabSkinFile } public void onFileReBuildingAction(ActionEvent event) { - CompletableFuture.runAsync(() -> { - CompanyCustomerService customerService = getCompanyCustomerService(); - try { - CompanyCustomerVo companyCustomer = customerService.findById(viewModel.getId().get()); - if (customerService.reBuildingFiles(companyCustomer, (level, message) -> setStatus(message))) { - loadTableDataSet(); - } - } catch (Exception e) { - e.printStackTrace(); - } - }); + CompanyCustomerRebuildFilesTasker task = new CompanyCustomerRebuildFilesTasker(); + CompanyCustomerVo customer = getEntity(); + task.setCompanyCustomer(customer); + UITools.showTaskDialogAndWait("重建客户文件", task, null); + if (task.isFilesUpdated()) { + loadTableDataSet(); + } } @Override @@ -320,74 +313,19 @@ public class CustomerTabSkinFile // dataSet.remove(selectedItem); } - private void initializeTask(Task task, String prefix, Consumer consumer) { - task.setOnScheduled(e -> { - consumer.accept("正在" + prefix + ",请稍后..."); - }); - task.setOnRunning(e -> { - consumer.accept("开始" + prefix + "..."); - }); - task.setOnSucceeded(e -> { - consumer.accept(prefix + "同步完成..."); - }); - task.exceptionProperty().addListener((observable, oldValue, newValue) -> { - consumer.accept(newValue.getMessage()); - }); - SpringApp.getBean(ScheduledExecutorService.class).submit(task); - consumer.accept("任务已创建..."); - } - public void onUpdateEvaluationFormAction(ActionEvent event) { CompanyCustomerEvaluationFormUpdateTask task = new CompanyCustomerEvaluationFormUpdateTask(); - task.setCompanyService(getCompanyService()); - task.setCompanyCustomerFileService(getCompanyCustomerFileService()); - task.setCustomer(getCompanyCustomerService().findById(viewModel.getId().get())); - UITools.showTaskDialogAndWait("更新评价表", task, consumer -> { - initializeTask(task, "更新评价表", msg -> consumer.accept(Message.info(msg))); - }); + CompanyCustomerVo customer = getEntity(); + task.setCustomer(customer); + UITools.showTaskDialogAndWait("更新评价表", task, null); loadTableDataSet(); } public void onCalcNextSignDateAction(ActionEvent event) { - UITools.showDialogAndWait("计算客户下一个评价日期", "依据已有的客户评估表和登记采购的合同计算下一个评估日期", ds -> { - CompanyCustomerVo companyCustomer = getCompanyCustomerService().findById(viewModel.getId().get()); - LocalDate nextSignDate = getCompanyCustomerFileService().getNextSignDate(companyCustomer, (level, msg) -> { - Platform.runLater(() -> { - ds.add(msg); - }); - }); - if (nextSignDate != null) { - Platform.runLater(() -> { - ds.add("下一个评价日期:" + nextSignDate); - }); - } - }); - } - - class FileTableFilePathTableCell extends TableCell { - @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - if (empty || !StringUtils.hasText(item)) { - setText(""); - return; - } - - String path = viewModel.getPath().get(); - if (StringUtils.hasText(path)) { - if (item.startsWith(path)) { - item = "~" + item.substring(path.length()); - } - } - setText(item); - } - } - - private CompanyCustomerFileService getCompanyCustomerFileService() { - if (companyCustomerFileService == null) { - companyCustomerFileService = SpringApp.getBean(CompanyCustomerFileService.class); - } - return companyCustomerFileService; + CompanyCustomerNextSignDateTask task = new CompanyCustomerNextSignDateTask(); + CompanyCustomerVo customer = getEntity(); + task.setCustomer(customer); + UITools.showTaskDialogAndWait("计算客户下一个评价日期", task, null); } } diff --git a/client/src/main/java/com/ecep/contract/controller/tab/ContractTabSkinSaleOrders.java b/client/src/main/java/com/ecep/contract/controller/tab/ContractTabSkinSaleOrders.java index 40f7aa6..64a3d91 100644 --- a/client/src/main/java/com/ecep/contract/controller/tab/ContractTabSkinSaleOrders.java +++ b/client/src/main/java/com/ecep/contract/controller/tab/ContractTabSkinSaleOrders.java @@ -7,7 +7,7 @@ import com.ecep.contract.controller.table.cell.EmployeeTableCell; import com.ecep.contract.util.FxmlPath; import com.ecep.contract.controller.contract.AbstContractTableTabSkin; import com.ecep.contract.controller.contract.ContractWindowController; -import com.ecep.contract.controller.customer.SalesOrderWindowController; +import com.ecep.contract.controller.contract.sale_order.SalesOrderWindowController; import com.ecep.contract.controller.table.cell.LocalDateFieldTableCell; import com.ecep.contract.converter.EmployeeStringConverter; import com.ecep.contract.service.SaleOrdersService; diff --git a/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinBase.java b/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinBase.java index a3f105d..f7d8716 100644 --- a/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinBase.java +++ b/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinBase.java @@ -4,7 +4,7 @@ import java.time.LocalDateTime; import com.ecep.contract.MyDateTimeUtils; import com.ecep.contract.SpringApp; -import com.ecep.contract.controller.customer.SalesOrderWindowController; +import com.ecep.contract.controller.contract.sale_order.SalesOrderWindowController; import com.ecep.contract.converter.EmployeeStringConverter; import com.ecep.contract.model.Employee; import com.ecep.contract.model.SalesOrder; diff --git a/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinBillVoucher.java b/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinBillVoucher.java index 8fb393e..51b0b59 100644 --- a/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinBillVoucher.java +++ b/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinBillVoucher.java @@ -1,7 +1,7 @@ package com.ecep.contract.controller.tab; import com.ecep.contract.SpringApp; -import com.ecep.contract.controller.customer.SalesOrderWindowController; +import com.ecep.contract.controller.contract.sale_order.SalesOrderWindowController; import com.ecep.contract.controller.table.AbstEntityTableTabSkin; import com.ecep.contract.controller.table.cell.EmployeeTableCell; import com.ecep.contract.converter.EmployeeStringConverter; diff --git a/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinItems.java b/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinItems.java index 032454f..81b600a 100644 --- a/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinItems.java +++ b/client/src/main/java/com/ecep/contract/controller/tab/SalesOrderTabSkinItems.java @@ -9,7 +9,7 @@ import java.util.stream.Collectors; import org.springframework.util.StringUtils; import com.ecep.contract.SpringApp; -import com.ecep.contract.controller.customer.SalesOrderWindowController; +import com.ecep.contract.controller.contract.sale_order.SalesOrderWindowController; import com.ecep.contract.controller.table.AbstEntityTableTabSkin; import com.ecep.contract.model.Contract; import com.ecep.contract.service.ContractService; diff --git a/client/src/main/java/com/ecep/contract/controller/table/cell/FilePathTableCell.java b/client/src/main/java/com/ecep/contract/controller/table/cell/FilePathTableCell.java index 1393fd0..27038e8 100644 --- a/client/src/main/java/com/ecep/contract/controller/table/cell/FilePathTableCell.java +++ b/client/src/main/java/com/ecep/contract/controller/table/cell/FilePathTableCell.java @@ -1,4 +1,41 @@ package com.ecep.contract.controller.table.cell; -public class FilePathTableCell { +import javafx.beans.property.StringProperty; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.util.Callback; +import org.springframework.util.StringUtils; + +public class FilePathTableCell extends TableCell { + + private StringProperty path; + + /** + * 创建一个用于表格列的单元格工厂 + */ + public static Callback, TableCell> forTableColumn( + StringProperty parentPath) { + return param -> new FilePathTableCell<>(parentPath); + } + + public FilePathTableCell(StringProperty parentPath) { + this.path = parentPath; + } + + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || !StringUtils.hasText(item)) { + setText(""); + return; + } + + String path = this.path.get(); + if (StringUtils.hasText(path)) { + if (item.startsWith(path)) { + item = "~" + item.substring(path.length()); + } + } + setText(item); + } } diff --git a/client/src/main/java/com/ecep/contract/controller/vendor/VendorTabSkinFile.java b/client/src/main/java/com/ecep/contract/controller/vendor/VendorTabSkinFile.java index d8221c9..b952059 100644 --- a/client/src/main/java/com/ecep/contract/controller/vendor/VendorTabSkinFile.java +++ b/client/src/main/java/com/ecep/contract/controller/vendor/VendorTabSkinFile.java @@ -278,7 +278,6 @@ public class VendorTabSkinFile public void onUpdateEvaluationFormAction(ActionEvent event) { CompanyVendorEvaluationFormUpdateTask task = new CompanyVendorEvaluationFormUpdateTask(); - task.setCompanyService(getCompanyService()); UITools.showTaskDialogAndWait("更新评价表", task, null); loadTableDataSet(); } diff --git a/client/src/main/java/com/ecep/contract/service/CompanyCustomerFileService.java b/client/src/main/java/com/ecep/contract/service/CompanyCustomerFileService.java index 69b99da..2ab9020 100644 --- a/client/src/main/java/com/ecep/contract/service/CompanyCustomerFileService.java +++ b/client/src/main/java/com/ecep/contract/service/CompanyCustomerFileService.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Service; import com.ecep.contract.CustomerFileType; import com.ecep.contract.MessageHolder; import com.ecep.contract.SpringApp; +import com.ecep.contract.constant.CompanyCustomerConstant; import com.ecep.contract.model.CompanyCustomer; import com.ecep.contract.model.CompanyCustomerEvaluationFormFile; import com.ecep.contract.util.ParamUtils; @@ -27,9 +28,14 @@ import com.ecep.contract.vo.ContractVo; @Service @CacheConfig(cacheNames = "customer-file") public class CompanyCustomerFileService extends QueryService { - + public File getEvaluationFormTemplate() { - throw new UnsupportedOperationException(); + SysConfService confService = SpringApp.getBean(SysConfService.class); + String path = confService.getString(CompanyCustomerConstant.KEY_EVALUATION_FORM_TEMPLATE); + if (path == null) { + return null; + } + return new File(path); } @Cacheable @@ -100,7 +106,6 @@ public class CompanyCustomerFileService extends QueryService findAllByCustomer(CompanyCustomerVo companyCustomer) { return findAll(ParamUtils.builder().equals("customer", companyCustomer).build(), Pageable.unpaged()) .getContent(); diff --git a/client/src/main/java/com/ecep/contract/service/CompanyCustomerService.java b/client/src/main/java/com/ecep/contract/service/CompanyCustomerService.java index 7a95b2b..d8e1089 100644 --- a/client/src/main/java/com/ecep/contract/service/CompanyCustomerService.java +++ b/client/src/main/java/com/ecep/contract/service/CompanyCustomerService.java @@ -2,6 +2,7 @@ package com.ecep.contract.service; import java.io.File; +import javafx.application.Platform; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -39,9 +40,9 @@ public class CompanyCustomerService extends QueryService extends Task { @Setter protected java.util.function.Predicate messageHandler; private EmployeeVo currentUser; - @Setter - private CompanyService companyService; - @Setter - private EmployeeService employeeService; - @Setter - private SysConfService confService; + private HashMap, Object> cachedMap = new HashMap<>(); public SysConfService getConfService() { - if (confService == null) { - confService = getBean(SysConfService.class); - } - return confService; + return getBean(SysConfService.class); } public CompanyService getCompanyService() { - if (companyService == null) { - companyService = getBean(CompanyService.class); - } - return companyService; + return getBean(CompanyService.class); } public EmployeeService getEmployeeService() { - if (employeeService == null) { - employeeService = getBean(EmployeeService.class); - } - return employeeService; + return getBean(EmployeeService.class); } protected K getBean(Class requiredType) throws BeansException { - return SpringApp.getBean(requiredType); + return getCachedBean(requiredType); + } + + protected K getCachedBean(Class requiredType) { + @SuppressWarnings("unchecked") + K bean = (K) cachedMap.get(requiredType); + if (bean == null) { + bean = getBean(requiredType); + cachedMap.put(requiredType, bean); + } + return bean; } public EmployeeVo getCurrentUser() { @@ -72,7 +69,6 @@ public abstract class Tasker extends Task { try { return execute(holder); } catch (Exception e) { - holder.error(e.getMessage()); logger.error(e.getMessage(), e); throw e; diff --git a/docs/task/client_server_tasker_communication_rules.md b/docs/task/client_server_tasker_communication_rules.md new file mode 100644 index 0000000..131c055 --- /dev/null +++ b/docs/task/client_server_tasker_communication_rules.md @@ -0,0 +1,215 @@ +# 客户端 Tasker 至 服务器端 Tasker 通信规则与逻辑 + +本文档总结了 Contract-Manager 项目中客户端 Tasker 与服务器端 Tasker 之间的通信规则、调用逻辑和实现模式,基于对以下文件的分析: +- `d:\idea-workspace\Contract-Manager\server\src\main\java\com\ecep\contract\ds\customer\tasker\CompanyCustomerEvaluationFormUpdateTask.java` +- `d:\idea-workspace\Contract-Manager\client\src\main\java\com\ecep\contract\controller\customer\CompanyCustomerEvaluationFormUpdateTask.java` +- `d:\idea-workspace\Contract-Manager\client\src\main\java\com\ecep\contract\controller\customer\CustomerTabSkinFile.java` + +## 1. 架构设计原则 + +项目采用了清晰的客户端-服务器分离架构,任务处理遵循以下原则: +- **客户端轻量级**:负责任务发起、参数传递和结果展示 +- **服务器端重量级**:负责实际业务逻辑处理和数据操作 +- **WebSocket通信**:使用WebSocket实现客户端与服务器端的任务通信和进度同步 + +## 2. 类命名与结构规范 + +### 2.1 命名规则 +- 客户端与服务器端的对应Tasker类**必须使用相同的类名**(如示例中的`CompanyCustomerEvaluationFormUpdateTask`) +- 客户端Tasker位于`client`模块的控制器包下 +- 服务器端Tasker位于`server`模块的tasker包下 + +### 2.2 任务名称注册规则 +- 所有服务器端Tasker类必须通过配置文件`tasker_mapper.json`进行注册,并由`WebSocketServerTaskManager`类在`afterPropertiesSet`方法中加载 +- 注册格式为`"TaskClassName": "fully.qualified.ClassName"` +- 注册的Task名称将用于客户端和服务器端之间的任务识别 +- 配置文件`tasker_mapper.json`应位于`src/main/resources`目录下,格式如下: + +```json +{ + "taskers": { + "TaskClassName": "fully.qualified.ClassName", + // 更多任务映射... + } +} +``` + +#### 任务名称注册实现示例 +```java +@Override +public void afterPropertiesSet() throws Exception { + // 从tasker_mapper.json文件中加载任务注册信息 + try { + Resource resource = resourceLoader.getResource("classpath:tasker_mapper.json"); + try (InputStream inputStream = resource.getInputStream()) { + JsonNode rootNode = objectMapper.readTree(inputStream); + JsonNode taskersNode = rootNode.get("taskers"); + if (taskersNode != null && taskersNode.isObject()) { + Map taskMap = new java.util.HashMap<>(); + taskersNode.fields().forEachRemaining(entry -> { + taskMap.put(entry.getKey(), entry.getValue().asText()); + }); + taskClzMap = taskMap; + } + } + } catch (Exception e) { + logger.error("Failed to load tasker_mapper.json", e); + // 使用默认值作为fallback + taskClzMap = Map.of(); + } +}``` + +### 2.3 接口实现区分 +- 客户端Tasker实现`WebSocketClientTasker`接口 +- 服务器端Tasker实现`WebSocketServerTasker`接口 + +### 2.4 继承关系 +- 客户端和服务器端Tasker均继承自`Tasker`基类 + +## 3. 客户端Tasker实现规则 + +客户端Tasker是任务的发起方,需要遵循以下实现规则: + +### 3.1 核心属性 +- 通常包含一个可设置的业务对象(如示例中的`@Setter private CompanyCustomerVo customer;`) +- 配置Logger日志记录器 + +### 3.2 核心方法实现 +- **getTaskName()**:返回任务名称,通常使用类名 +- **updateProgress()**:继承或重写进度更新方法 +- **execute()**:调用`callRemoteTask()`方法将任务发送到服务器端,传递必要参数 + +### 3.3 示例实现 +```java +public class CompanyCustomerEvaluationFormUpdateTask extends Tasker implements WebSocketClientTasker { + private static final Logger logger = LoggerFactory.getLogger(CompanyCustomerEvaluationFormUpdateTask.class); + + @Setter + private CompanyCustomerVo customer; // 业务对象 + + @Override + public String getTaskName() { + return getClass().getSimpleName(); + } + + @Override + protected Object execute(MessageHolder holder) throws Exception { + updateTitle("客户评估表更新任务"); // 设置任务标题 + // 调用远程任务,传递locale和业务对象ID + return callRemoteTask(holder, getLocale(), customer.getId()); + } +} +``` + +## 4. 服务器端Tasker实现规则 + +服务器端Tasker是任务的实际执行者,需要遵循以下实现规则: + +### 4.1 参数接收 +- 实现`init(JsonNode argsNode)`方法接收客户端传递的参数 +- 从参数中提取业务对象ID并加载完整业务对象 + +### 4.2 服务获取 +- 通过`getCachedBean(Service.class)`方法获取所需的服务实例 +- 可以提供辅助方法封装服务获取逻辑 + +### 4.3 任务执行 +- 实现`execute(MessageHolder holder)`方法包含实际业务逻辑 +- 使用`holder.info()/error()`等方法记录任务执行状态 +- 调用`updateProgress()`方法更新任务进度 + +### 4.4 示例实现 +```java +public class CompanyCustomerEvaluationFormUpdateTask extends Tasker implements WebSocketServerTasker { + private CompanyCustomer customer; // 业务对象 + + @Override + public void init(JsonNode argsNode) { + // 从参数中提取业务对象ID并加载 + int customerId = argsNode.get(0).asInt(); + customer = getCachedBean(CompanyCustomerService.class).findById(customerId); + } + + // 辅助方法获取服务 + CompanyCustomerFileService getCompanyCustomerFileService() { + return getCachedBean(CompanyCustomerFileService.class); + } + + @Override + protected Object execute(MessageHolder holder) throws Exception { + // 执行实际业务逻辑 + updateEvaluationForm(holder); + return null; + } + + // 具体业务逻辑实现 + public void updateEvaluationForm(MessageHolder holder) { + // 业务逻辑代码... + updateProgress(1, 10); // 更新进度 + // 更多业务逻辑... + } +} +``` + +## 5. 客户端Tasker调用流程 + +在UI控制器中调用客户端Tasker的标准流程如下: + +### 5.1 实例化与参数设置 +1. 创建客户端Tasker实例 +2. 设置必要的业务对象参数 + +### 5.2 任务执行与监控 +1. 使用`UITools.showTaskDialogAndWait()`显示任务对话框 +2. 调用`initializeTask()`初始化任务执行环境 +3. 通过消费者函数处理任务消息 + +### 5.3 任务完成处理 +1. 任务完成后执行必要的数据刷新操作 + +### 5.4 示例调用 +```java +public void onUpdateEvaluationFormAction(ActionEvent event) { + // 1. 创建Tasker并设置参数 + CompanyCustomerEvaluationFormUpdateTask task = new CompanyCustomerEvaluationFormUpdateTask(); + task.setCustomer(getCompanyCustomerService().findById(viewModel.getId().get())); + + // 2. 显示任务对话框并执行任务 + UITools.showTaskDialogAndWait("更新评价表", task, consumer -> { + initializeTask(task, "更新评价表", msg -> consumer.accept(Message.info(msg))); + }); + + // 3. 任务完成后刷新数据 + loadTableDataSet(); +} +``` + +## 6. 任务进度管理 + +- 服务器端使用`updateProgress(current, total)`方法更新任务进度 +- 进度值通常以0-1000或类似小范围数值表示完成百分比 +- 客户端通过WebSocket接收进度更新并显示 + +## 7. 异常处理机制 + +- 服务器端使用`MessageHolder.error()`方法记录错误信息 +- 客户端通过任务对话框展示错误信息 +- 服务器端在关键操作点进行异常捕获和处理 + +## 8. 数据一致性保障 + +- 任务完成后客户端通常调用数据刷新方法(如`loadTableDataSet()`)确保UI显示最新数据 +- 服务器端负责业务数据的持久化操作 + +## 9. 最佳实践建议 + +1. **任务拆分**:复杂任务应拆分为多个小任务,便于进度跟踪和错误定位 +2. **状态反馈**:在关键节点提供清晰的状态信息,增强用户体验 +3. **资源释放**:确保文件流等资源正确关闭,避免资源泄露 +4. **事务控制**:对于涉及多步数据操作的任务,考虑使用事务确保数据一致性 +5. **错误重试**:针对网络波动等临时性问题,考虑实现任务重试机制 +6. **配置管理**:使用`tasker_mapper.json`文件统一管理任务注册信息,避免在代码中硬编码任务映射 +7. **异常处理**:确保实现配置文件加载失败的fallback机制,保证系统稳定性 +8. **版本控制**:对任务配置文件进行版本控制,便于追踪变更历史 + +通过遵循以上规则和模式,可以确保Contract-Manager项目中客户端与服务器端Tasker通信的一致性、可靠性和可维护性。 \ No newline at end of file diff --git a/server/src/main/java/com/ecep/contract/ds/customer/repository/CompanyCustomerEvaluationFormFileRepository.java b/server/src/main/java/com/ecep/contract/ds/customer/repository/CompanyCustomerEvaluationFormFileRepository.java index 5817d14..8bce60b 100644 --- a/server/src/main/java/com/ecep/contract/ds/customer/repository/CompanyCustomerEvaluationFormFileRepository.java +++ b/server/src/main/java/com/ecep/contract/ds/customer/repository/CompanyCustomerEvaluationFormFileRepository.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Repository; import com.ecep.contract.model.CompanyCustomer; import com.ecep.contract.model.CompanyCustomerEvaluationFormFile; +import com.ecep.contract.model.CompanyCustomerFile; @Repository public interface CompanyCustomerEvaluationFormFileRepository extends JpaRepository, JpaSpecificationExecutor { @@ -19,4 +20,6 @@ public interface CompanyCustomerEvaluationFormFileRepository extends JpaReposito @Query(value = "SELECT * FROM COMPANY_CUSTOMER_FILE ccf left join COMPANY_CUSTOMER_EVALUATION_FORM_FILE CCEFF on ccf.ID = CCEFF.ID " + "where ccf.CUSTOMER_ID=:customer and ccf.TYPE=:fileType", nativeQuery = true) List findAllByCustomerAndType(@Param("customer") int companyCustomerId, @Param("fileType") String type); + + CompanyCustomerEvaluationFormFile findByCustomerFile(CompanyCustomerFile customerFile); } diff --git a/server/src/main/java/com/ecep/contract/ds/customer/service/CompanyCustomerEvaluationFormFileService.java b/server/src/main/java/com/ecep/contract/ds/customer/service/CompanyCustomerEvaluationFormFileService.java new file mode 100644 index 0000000..4c09576 --- /dev/null +++ b/server/src/main/java/com/ecep/contract/ds/customer/service/CompanyCustomerEvaluationFormFileService.java @@ -0,0 +1,130 @@ +package com.ecep.contract.ds.customer.service; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.context.annotation.Lazy; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +import com.ecep.contract.IEntityService; +import com.ecep.contract.QueryService; +import com.ecep.contract.constant.ServiceConstant; +import com.ecep.contract.ds.customer.repository.CompanyCustomerEvaluationFormFileRepository; +import com.ecep.contract.model.CompanyCustomer; +import com.ecep.contract.model.CompanyCustomerEvaluationFormFile; +import com.ecep.contract.model.CompanyCustomerFile; +import com.ecep.contract.util.SpecificationUtils; +import com.fasterxml.jackson.databind.JsonNode; + +@Lazy +@Service +@CacheConfig(cacheNames = "company-customer-evaluation-form-file") +public class CompanyCustomerEvaluationFormFileService + implements IEntityService, QueryService { + private static final Logger logger = LoggerFactory.getLogger(CompanyCustomerEvaluationFormFileService.class); + + @Lazy + @Autowired + private CompanyCustomerEvaluationFormFileRepository repository; + + @Cacheable(key = "#p0") + @Override + public CompanyCustomerEvaluationFormFile findById(Integer id) { + return repository.findById(id).orElse(null); + } + + @Override + public Specification getSpecification(String searchText) { + if (!org.springframework.util.StringUtils.hasText(searchText)) { + return null; + } + return (root, query, builder) -> { + return builder.or( + builder.like(root.get("catalog"), "%" + searchText + "%"), + builder.like(root.get("level"), "%" + searchText + "%")); + }; + } + + @Caching(evict = { + @CacheEvict(key = "#p0.id"), + }) + @Override + public void delete(CompanyCustomerEvaluationFormFile formFile) { + repository.delete(formFile); + } + + @Caching(evict = { + @CacheEvict(key = "#p0.id"), + }) + @Override + public CompanyCustomerEvaluationFormFile save(CompanyCustomerEvaluationFormFile formFile) { + return repository.save(formFile); + } + + @Override + public Page findAll(Specification spec, + Pageable pageable) { + return repository.findAll(spec, pageable); + } + + @Override + public Page findAll(JsonNode paramsNode, Pageable pageable) { + Specification spec = null; + if (paramsNode.has(ServiceConstant.KEY_SEARCH_TEXT)) { + spec = getSpecification(paramsNode.get(ServiceConstant.KEY_SEARCH_TEXT).asText()); + } + + if (paramsNode.has("customer")) { + CompanyCustomer customer = new CompanyCustomer(); + customer.setId(paramsNode.get("customer").asInt()); + spec = SpecificationUtils.and(spec, + (root, query, builder) -> builder.equal(root.get("customerFile").get("customer"), customer)); + } + + spec = SpecificationUtils.andParam(spec, paramsNode, "customerFile"); + spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "catalog", "level"); + + return findAll(spec, pageable); + } + + /** + * 根据客户查询所有评估表文件 + * + * @param customer 客户实体 + * @return 评估表文件列表 + */ + public List findAllByCustomer(CompanyCustomer customer) { + return repository.findAllByCustomerFileCustomer(customer); + } + + /** + * 根据客户ID和文件类型查询评估表文件 + * + * @param companyCustomerId 客户ID + * @param type 文件类型 + * @return 评估表文件列表 + */ + public List findAllByCustomerIdAndType(int companyCustomerId, String type) { + return repository.findAllByCustomerAndType(companyCustomerId, type); + } + + /** + * 根据客户文件查询评估表文件 + * + * @param customerFile 客户文件实体 + * @return 评估表文件列表 + */ + public CompanyCustomerEvaluationFormFile findByCustomerFile(CompanyCustomerFile customerFile) { + return repository.findByCustomerFile(customerFile); + } + +} \ No newline at end of file diff --git a/server/src/main/java/com/ecep/contract/ds/customer/service/CompanyCustomerFileService.java b/server/src/main/java/com/ecep/contract/ds/customer/service/CompanyCustomerFileService.java index 77a4098..a3f61a7 100644 --- a/server/src/main/java/com/ecep/contract/ds/customer/service/CompanyCustomerFileService.java +++ b/server/src/main/java/com/ecep/contract/ds/customer/service/CompanyCustomerFileService.java @@ -5,12 +5,7 @@ import java.time.LocalDate; import java.util.Comparator; import java.util.List; import java.util.Optional; -import java.util.function.Consumer; -import com.ecep.contract.QueryService; -import com.ecep.contract.model.*; -import com.ecep.contract.util.SpecificationUtils; -import com.fasterxml.jackson.databind.JsonNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -27,20 +22,30 @@ import org.springframework.stereotype.Service; import com.ecep.contract.CustomerFileType; import com.ecep.contract.IEntityService; +import com.ecep.contract.MessageHolder; +import com.ecep.contract.QueryService; import com.ecep.contract.SpringApp; import com.ecep.contract.constant.CompanyCustomerConstant; +import com.ecep.contract.constant.ServiceConstant; import com.ecep.contract.ds.company.service.CompanyBasicService; import com.ecep.contract.ds.contract.service.ContractService; import com.ecep.contract.ds.customer.repository.CompanyCustomerEvaluationFormFileRepository; import com.ecep.contract.ds.customer.repository.CompanyCustomerFileRepository; import com.ecep.contract.ds.other.service.SysConfService; +import com.ecep.contract.model.CompanyCustomer; +import com.ecep.contract.model.CompanyCustomerEvaluationFormFile; +import com.ecep.contract.model.CompanyCustomerFile; +import com.ecep.contract.model.Contract; +import com.ecep.contract.util.SpecificationUtils; +import com.fasterxml.jackson.databind.JsonNode; import jakarta.persistence.criteria.Path; @Lazy @Service @CacheConfig(cacheNames = "company-customer-file") -public class CompanyCustomerFileService implements IEntityService, QueryService { +public class CompanyCustomerFileService + implements IEntityService, QueryService { private static final Logger logger = LoggerFactory.getLogger(CompanyCustomerFileService.class); @Lazy @Autowired @@ -64,13 +69,9 @@ public class CompanyCustomerFileService implements IEntityService findAll(JsonNode paramsNode, Pageable pageable) { Specification spec = null; - if (paramsNode.has("searchText")) { - spec = getSpecification(paramsNode.get("searchText").asText()); + if (paramsNode.has(ServiceConstant.KEY_SEARCH_TEXT)) { + spec = getSpecification(paramsNode.get(ServiceConstant.KEY_SEARCH_TEXT).asText()); } // field spec = SpecificationUtils.andParam(spec, paramsNode, "customer"); @@ -124,12 +123,10 @@ public class CompanyCustomerFileService implements IEntityService findAll(Specification spec, Sort sort) { return companyCustomerFileRepository.findAll(spec, sort); } - public List saveAll(List files) { return companyCustomerFileRepository.saveAll(files); } @@ -153,7 +150,8 @@ public class CompanyCustomerFileService implements IEntityService state) { + public LocalDate getNextSignDate(CompanyCustomer companyCustomer, MessageHolder holder) { LocalDate miniContractDate = LocalDate.of(2022, 1, 1); // 检索全部合同 ContractService contractService = SpringApp.getBean(ContractService.class); List contractList = contractService.findAllByCompanyCustomer(companyCustomer, null, null); if (contractList.isEmpty()) { - state.accept("未发现已登记的合同"); + holder.error("未发现已登记的合同"); return null; } // 检索评估表 @@ -189,46 +188,45 @@ public class CompanyCustomerFileService implements IEntityService v.getSetupDate() != null && !v.getSetupDate().isBefore(miniContractDate)) .min(Comparator.comparing(Contract::getSetupDate)) .orElse(null); if (firstContract == null) { - state.accept("最早的合同不存在?"); + holder.error("最早的合同不存在?"); return null; } LocalDate setupDate = firstContract.getSetupDate(); - state.accept("依据合同 " + firstContract.getCode() + " 的日期 " + setupDate + " 推算"); + holder.info("依据合同 " + firstContract.getCode() + " 的日期 " + setupDate + " 推算"); return CompanyBasicService.adjustToWorkDay(setupDate.plusDays(-7)); } // 检查失效日期起的第一个合同 LocalDate nextInValidDate = latestFile.getSignDate().plusYears(1); File file = new File(latestFile.getFilePath()); - state.accept("依据 " + file.getName() + " 的失效期 " + nextInValidDate + " 检索合同"); + holder.info("依据 " + file.getName() + " 的失效期 " + nextInValidDate + " 检索合同"); List matchedContracts = contractList.stream() .filter(v -> v.getSetupDate().isAfter(nextInValidDate)).toList(); // 没有在失效日期后的合同时,使用失效日期 if (matchedContracts.isEmpty()) { - state.accept("未发现失效期 " + nextInValidDate + " 后的合同"); + holder.warn("未发现失效期 " + nextInValidDate + " 后的合同"); return null; } - state.accept("发现匹配合同 " + matchedContracts.size() + " 个"); + holder.info("发现匹配合同 " + matchedContracts.size() + " 个"); // 按时间取最早一个 Contract firstContract = matchedContracts.stream() .min(Comparator.comparing(Contract::getSetupDate)) .orElse(null); LocalDate setupDate = firstContract.getSetupDate(); - state.accept("匹配失效期 " + nextInValidDate + " 后的第一个合同 " + firstContract.getCode()); - state.accept("依据合同 " + firstContract.getCode() + " 的日期 " + setupDate + " 推算"); + holder.info("匹配失效期 " + nextInValidDate + " 后的第一个合同 " + firstContract.getCode()); + holder.info("依据合同 " + firstContract.getCode() + " 的日期 " + setupDate + " 推算"); return CompanyBasicService.adjustToWorkDay(setupDate.plusDays(-7)); } - public File getEvaluationFormTemplate() { String path = confService.getString(CompanyCustomerConstant.KEY_EVALUATION_FORM_TEMPLATE); if (path == null) { @@ -263,5 +261,4 @@ public class CompanyCustomerFileService implements IEntityService status) { + public boolean reBuildingFiles(CompanyCustomer companyCustomer, MessageHolder holder) { List dbFiles = companyCustomerFileService.findAllByCustomer(companyCustomer); Map map = new HashMap<>(); - boolean modified = fetchDbFiles(dbFiles, map, status); + boolean modified = fetchDbFiles(dbFiles, map, holder::info); // 客户目录下 List retrieveFiles = new ArrayList<>(); List needMoveToCompanyPath = new ArrayList<>(); // TODO 客户有曾用名,可能存在多个目录 - fetchFiles(companyCustomer.getPath(), needMoveToCompanyPath, retrieveFiles, map, status); + fetchFiles(companyCustomer.getPath(), needMoveToCompanyPath, retrieveFiles, map, holder::info); // 移动文件到公司目录下 to company path moveFileToCompany(companyCustomer.getCompany(), needMoveToCompanyPath); - status.accept("导入 " + retrieveFiles.size() + " 个文件"); + holder.info("导入 " + retrieveFiles.size() + " 个文件"); if (!retrieveFiles.isEmpty()) { // update db @@ -184,7 +185,7 @@ public class CompanyCustomerService extends CompanyBasicService @SuppressWarnings("unchecked") @Override protected > boolean fillFileAsDefaultType(F dbFile, File file, - Consumer status) { + Consumer status) { dbFile.setType((T) CustomerFileType.General); fillFile(dbFile, file, null, status); companyCustomerFileService.save((CompanyCustomerFile) dbFile); @@ -193,7 +194,7 @@ public class CompanyCustomerService extends CompanyBasicService @Override protected > boolean fillFileAsEvaluationFile(F customerFile, File file, - List fileList, Consumer status) { + List fileList, Consumer status) { boolean modified = super.fillFileAsEvaluationFile(customerFile, file, fileList, status); if (fileList != null) { @@ -227,7 +228,7 @@ public class CompanyCustomerService extends CompanyBasicService } private > void updateEvaluationFileByJsonFile(F customerFile, File jsonFile, - Consumer status) throws IOException { + Consumer status) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); JsonNode root = objectMapper.readTree(jsonFile); if (!root.isObject()) { @@ -250,10 +251,11 @@ public class CompanyCustomerService extends CompanyBasicService logger.info("delete json file {}", jsonFile.getName()); } } + @SuppressWarnings("unchecked") @Override protected > F fillFileType(File file, List fileList, - Consumer status) { + Consumer status) { CompanyCustomerFile customerFile = new CompanyCustomerFile(); customerFile.setType(CustomerFileType.General); if (fillFile(customerFile, file, fileList, status)) { @@ -261,6 +263,7 @@ public class CompanyCustomerService extends CompanyBasicService } return null; } + @SuppressWarnings("unchecked") @Override protected > boolean setFileTypeAsEvaluationForm(F file) { @@ -276,7 +279,7 @@ public class CompanyCustomerService extends CompanyBasicService return (fileName.contains(CompanyCustomerConstant.EVALUATION_FORM_NAME1) || fileName.contains(CompanyCustomerConstant.EVALUATION_FORM_NAME2)) && (FileUtils.withExtensions(fileName, FileUtils.JPG, FileUtils.JPEG, - FileUtils.PDF)); + FileUtils.PDF)); } public boolean makePathAbsent(CompanyCustomer companyCustomer) { @@ -410,5 +413,4 @@ public class CompanyCustomerService extends CompanyBasicService return customerCatalogRepository.save(catalog); } - } diff --git a/server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerEvaluationFormUpdateTask.java b/server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerEvaluationFormUpdateTask.java new file mode 100644 index 0000000..ecba303 --- /dev/null +++ b/server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerEvaluationFormUpdateTask.java @@ -0,0 +1,214 @@ +package com.ecep.contract.ds.customer.tasker; + +import static com.ecep.contract.util.ExcelUtils.setCellValue; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; + +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.hibernate.Hibernate; +import org.springframework.util.StringUtils; + +import com.ecep.contract.CustomerFileType; +import com.ecep.contract.MessageHolder; +import com.ecep.contract.SpringApp; +import com.ecep.contract.cloud.tyc.CloudTycService; +import com.ecep.contract.ds.customer.service.CompanyCustomerFileService; +import com.ecep.contract.ds.customer.service.CompanyCustomerEvaluationFormFileService; +import com.ecep.contract.ds.customer.service.CompanyCustomerService; +import com.ecep.contract.model.CloudTyc; +import com.ecep.contract.model.Company; +import com.ecep.contract.model.CompanyCustomer; +import com.ecep.contract.model.CompanyCustomerEvaluationFormFile; +import com.ecep.contract.model.CompanyCustomerFile; +import com.ecep.contract.service.WebSocketServerTasker; +import com.ecep.contract.ui.Tasker; +import com.ecep.contract.util.CompanyUtils; +import com.ecep.contract.vo.CompanyCustomerEvaluationFormFileVo; +import com.ecep.contract.vo.CompanyCustomerFileVo; +import com.fasterxml.jackson.databind.JsonNode; + +public class CompanyCustomerEvaluationFormUpdateTask extends Tasker implements WebSocketServerTasker { + private CompanyCustomer customer; + + @Override + public void init(JsonNode argsNode) { + int customerId = argsNode.get(0).asInt(); + customer = getCachedBean(CompanyCustomerService.class).findById(customerId); + } + + CompanyCustomerFileService getCompanyCustomerFileService() { + return getCachedBean(CompanyCustomerFileService.class); + } + + CompanyCustomerEvaluationFormFileService getCompanyCustomerEvaluationFormFileService() { + return getCachedBean(CompanyCustomerEvaluationFormFileService.class); + } + + @Override + protected Object execute(MessageHolder holder) throws Exception { + updateEvaluationForm(holder); + return null; + } + + public void updateEvaluationForm(MessageHolder holder) { + if (!StringUtils.hasText(customer.getPath())) { + holder.error("供应商目录未设置,请先设置供应商目录"); + return; + } + File template = getCompanyCustomerFileService().getEvaluationFormTemplate(); + if (template == null) { + holder.error("评价表模板文件未设置,请先设置评价表模板文件"); + return; + } + if (!template.exists()) { + holder.error("评价表模板文件 " + template.getAbsolutePath() + " 不存在,请检查"); + return; + } + + updateProgress(1, 10); + File dir = new File(customer.getPath()); + String template_file_name = template.getName(); + File destFile = new File(dir, template_file_name); + + if (destFile.exists()) { + holder.info("表单文件已经存在," + destFile.getName()); + try ( + InputStream inp = new FileInputStream(destFile); + Workbook wb = WorkbookFactory.create(inp)) { + updateEvaluationForm(wb, destFile, holder); + holder.info("评价表已更新"); + } catch (Exception e) { + holder.error(e.getMessage()); + } + } else { + holder.info("根据模板 " + template_file_name + " 创建表单 " + destFile.getName()); + try (InputStream inp = new FileInputStream(template); Workbook wb = WorkbookFactory.create(inp)) { + updateEvaluationForm(wb, destFile, holder); + holder.info("评价表已创建"); + CompanyCustomerFile customerFile = new CompanyCustomerFile(); + customerFile.setCustomer(customer); + customerFile.setFilePath(destFile.getAbsolutePath()); + customerFile.setType(CustomerFileType.General); + getCompanyCustomerFileService().save(customerFile); + } catch (Exception e) { + holder.error(e.getMessage()); + } + } + updateProgress(10, 10); + } + + /** + * 更新客户评估表,依据模板创建,如果已经存在生成的文件,则更新评估表 + * + * @param wb work book + * @param destFile 目标文件 + */ + public void updateEvaluationForm(Workbook wb, File destFile, MessageHolder holder) throws IOException { + updateProgress(2, 10); + Company company = customer.getCompany(); + if (!Hibernate.isInitialized(company)) { + company = getCompanyService().findById(company.getId()); + customer.setCompany(company); + } + Sheet sheet = wb.getSheetAt(0); + updateSheet(company, sheet, holder.sub(" - ")); + updateProgress(8, 10); + // 输出到文件 + try (OutputStream fileOut = new FileOutputStream(destFile)) { + wb.write(fileOut); + } catch (FileNotFoundException e) { + holder.error("写评估表时发生文件错误,请检查评估表是否被打开中:" + e.getMessage()); + } + } + + private void updateSheet(Company company, Sheet sheet, MessageHolder holder) { + setCellValue(sheet, "B3", "客户编号:" + CompanyUtils.formatCompanyVendorId(customer.getId())); + setCellValue(sheet, "B4", "客户名称:" + company.getName()); + + LocalDate suggestDate = getCompanyCustomerFileService().getNextSignDate(customer, holder); + if (suggestDate == null) { + suggestDate = LocalDate.now(); + } + setCellValue(sheet, "H3", "评定时间:" + suggestDate); + setCellValue(sheet, "H4", "统一社会信用代码:"); + setCellValue(sheet, "H5", company.getUniscid()); + // 注册所属地 + setCellValue(sheet, "B5", "注册所属地:" + company.getDistrict()); + // 经营状态 + setCellValue(sheet, "D6", "经营状态:" + company.getEntStatus()); + // 成立日期 + setCellValue(sheet, "H6", "成立日期:" + company.getSetupDate()); + // 所属行业 + setCellValue(sheet, "D7", "所属行业:" + company.getIndustry()); + setCellValue(sheet, "D8", + "注册资金:" + company.getRegisteredCapital() + " " + company.getRegisteredCapitalCurrency()); + // 企业类型 + setCellValue(sheet, "H10", "企业类型:" + company.getEntType()); + // 天眼评分 + CloudTycService cloudTycService = SpringApp.getBean(CloudTycService.class); + CloudTyc cloudTyc = cloudTycService.getOrCreateCloudTyc(company); + setCellValue(sheet, "D10", "天眼评分:" + (cloudTyc.getScore() > 0 ? cloudTyc.getScore() : "")); + + // 检索评估表 + List customerFiles = getCompanyCustomerFileService().findAllByCustomerAndType(customer, + CustomerFileType.EvaluationForm); + + List filteredList = customerFiles.stream().filter(file -> { + return file.getSignDate() != null && file.isValid(); + }) + .sorted(Comparator.comparing(CompanyCustomerFile::getSignDate)) + .map(getCompanyCustomerEvaluationFormFileService()::findByCustomerFile) + .toList(); + + if (filteredList.isEmpty()) { + setCellValue(sheet, "C40", "首次评价"); + try { + sheet.addMergedRegion(CellRangeAddress.valueOf("C40:K40")); + } catch (Exception ignored) { + } + } else { + holder.info("客户有 " + filteredList.size() + " 条评价表"); + setCellValue(sheet, "C40", "评价日期"); + try { + sheet.addMergedRegion(CellRangeAddress.valueOf("C40:D40")); + } catch (Exception ignored) { + } + setCellValue(sheet, "E40", "经济指标"); + setCellValue(sheet, "F40", "综合指标"); + setCellValue(sheet, "G40", "资信等级"); + String[] CreditLevelTitles = new String[] { "-", "差★", " 一般★★", " 较好★★★", " 好★★★★", " " }; + int baseRow = 40; + for (CompanyCustomerEvaluationFormFile form : filteredList) { + CompanyCustomerFile customerFile = form.getCustomerFile(); + if (!Hibernate.isInitialized(customerFile)) { + customerFile = getCompanyCustomerFileService().findById(customerFile.getId()); + } + setCellValue(sheet, baseRow, 2, String.valueOf(customerFile.getSignDate())); + setCellValue(sheet, baseRow, 4, form.getCatalog()); + setCellValue(sheet, baseRow, 5, form.getLevel()); + if (form.getCreditLevel() == null) { + setCellValue(sheet, baseRow, 6, "-"); + } else { + setCellValue(sheet, baseRow, 6, CreditLevelTitles[form.getCreditLevel()]); + } + try { + sheet.addMergedRegion(new CellRangeAddress(baseRow, baseRow, 2, 3)); + } catch (Exception ignored) { + } + baseRow++; + } + } + } +} diff --git a/server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerNextSignDateTask.java b/server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerNextSignDateTask.java new file mode 100644 index 0000000..bed2c84 --- /dev/null +++ b/server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerNextSignDateTask.java @@ -0,0 +1,117 @@ +package com.ecep.contract.ds.customer.tasker; + +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; + +import com.ecep.contract.CustomerFileType; +import com.ecep.contract.MessageHolder; +import com.ecep.contract.ds.contract.service.ContractService; +import com.ecep.contract.ds.customer.service.CompanyCustomerFileService; +import com.ecep.contract.ds.customer.service.CompanyCustomerService; +import com.ecep.contract.model.CompanyCustomer; +import com.ecep.contract.model.CompanyCustomerFile; +import com.ecep.contract.model.Contract; +import com.ecep.contract.service.WebSocketServerTasker; +import com.ecep.contract.ui.Tasker; +import com.fasterxml.jackson.databind.JsonNode; + +public class CompanyCustomerNextSignDateTask extends Tasker implements WebSocketServerTasker { + + private CompanyCustomer customer; + + @Override + public void init(JsonNode argsNode) { + int customerId = argsNode.get(0).asInt(); + customer = getCachedBean(CompanyCustomerService.class).findById(customerId); + } + + CompanyCustomerFileService getCompanyCustomerFileService() { + return getCachedBean(CompanyCustomerFileService.class); + } + + CompanyCustomerService getCompanyCustomerService() { + return getCachedBean(CompanyCustomerService.class); + } + + ContractService getContractService() { + return getCachedBean(ContractService.class); + } + + @Override + protected Object execute(MessageHolder holder) throws Exception { + holder.info("开始计算客户下一个评价日期"); + + LocalDate nextSignDate = calculateNextSignDate(holder); + + if (nextSignDate != null) { + holder.info("下一个评价日期:" + nextSignDate); + } else { + holder.warn("无法计算下一个评价日期"); + } + + return nextSignDate; + } + + private LocalDate calculateNextSignDate(MessageHolder holder) { + LocalDate miniContractDate = LocalDate.of(2022, 1, 1); + + // 检索全部合同 + ContractService contractService = getContractService(); + List contractList = contractService.findAllByCompanyCustomer(customer, null, null); + if (contractList.isEmpty()) { + holder.error("未发现已登记的合同"); + return null; + } + + holder.info("发现" + contractList.size() + "份合同"); + + // 检索评估表 + List files = getCompanyCustomerFileService().findAllByCustomerAndType(customer, + CustomerFileType.EvaluationForm); + CompanyCustomerFile latestFile = files.stream() + .filter(v -> v.getSignDate() != null && v.isValid()) + .max(Comparator.comparing(CompanyCustomerFile::getSignDate)) + .orElse(null); + + // 没有有效的评估表的评价日期 + if (latestFile == null) { + holder.warn("未发现有效的评估表"); + // 返回最早的合同日期 + Contract firstContract = contractList.stream() + .filter(v -> v.getSetupDate() != null && !v.getSetupDate().isBefore(miniContractDate)) + .min(Comparator.comparing(Contract::getSetupDate)) + .orElse(null); + + if (firstContract == null) { + holder.error("最早的合同不存在"); + return null; + } + + LocalDate setupDate = firstContract.getSetupDate(); + holder.info("选择最早的合同:" + firstContract.getCode() + ",日期:" + setupDate); + return setupDate.plusDays(-7); + } + + // 有有效的评估表,计算下一个日期 + LocalDate lastSignDate = latestFile.getSignDate(); + holder.info("最近一次有效的评估表日期:" + lastSignDate); + + // 查找最近一次评估日期之后的合同 + Contract firstContractAfter = contractList.stream() + .filter(v -> v.getSetupDate() != null && v.getSetupDate().isAfter(lastSignDate)) + .min(Comparator.comparing(Contract::getSetupDate)) + .orElse(null); + + if (firstContractAfter == null) { + holder.warn("没有找到在" + lastSignDate + "之后的合同"); + // 返回当前日期作为建议 + return LocalDate.now(); + } + + LocalDate setupDate = firstContractAfter.getSetupDate(); + holder.info("找到最近一次评估日期后的第一个合同:" + firstContractAfter.getCode() + ",日期:" + setupDate); + + return setupDate.plusDays(-7); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerRebuildFilesTasker.java b/server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerRebuildFilesTasker.java new file mode 100644 index 0000000..910621e --- /dev/null +++ b/server/src/main/java/com/ecep/contract/ds/customer/tasker/CompanyCustomerRebuildFilesTasker.java @@ -0,0 +1,76 @@ +package com.ecep.contract.ds.customer.tasker; + +import org.hibernate.Hibernate; +import org.springframework.util.StringUtils; + +import com.ecep.contract.MessageHolder; +import com.ecep.contract.ds.company.service.CompanyService; +import com.ecep.contract.ds.customer.service.CompanyCustomerService; +import com.ecep.contract.model.Company; +import com.ecep.contract.model.CompanyCustomer; +import com.ecep.contract.service.WebSocketServerTasker; +import com.ecep.contract.ui.Tasker; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * 客户文件重建任务器 + * 用于重建客户相关文件,同步文件系统和数据库记录 + */ +public class CompanyCustomerRebuildFilesTasker extends Tasker implements WebSocketServerTasker { + + private CompanyCustomer customer; + + CompanyCustomerService getCompanyCustomerService() { + return getCachedBean(CompanyCustomerService.class); + } + + @Override + public void init(JsonNode argsNode) { + int customerId = argsNode.get(0).asInt(); + customer = getCompanyCustomerService().findById(customerId); + } + + @Override + protected Object execute(MessageHolder holder) throws Exception { + updateTitle("重建客户文件"); + updateProgress(0, 100); + + if (customer == null) { + holder.error("客户不存在"); + return false; + } + + if (!StringUtils.hasText(customer.getPath())) { + holder.error("客户路径未设置,无法重建文件"); + updateProgress(100, 100); + return false; + } + + Company company = customer.getCompany(); + if (!Hibernate.isInitialized(company)) { + company = getCachedBean(CompanyService.class).findById(company.getId()); + } + + try { + holder.info("开始重建客户文件:" + company.getName()); + holder.info("客户路径:" + customer.getPath()); + + boolean result = getCompanyCustomerService().reBuildingFiles(customer, holder); + + if (result) { + holder.info("客户文件重建成功"); + updateProperty("filesUpdated", true); + } else { + holder.info("客户文件重建完成,但没有更新任何文件"); + updateProperty("filesUpdated", false); + } + + updateProgress(100, 100); + return result; + } catch (Exception e) { + holder.error("客户文件重建失败:" + e.getMessage()); + updateProgress(100, 100); + throw e; + } + } +} \ No newline at end of file diff --git a/server/src/main/java/com/ecep/contract/service/WebSocketServerTaskManager.java b/server/src/main/java/com/ecep/contract/service/WebSocketServerTaskManager.java index e02388f..3acd847 100644 --- a/server/src/main/java/com/ecep/contract/service/WebSocketServerTaskManager.java +++ b/server/src/main/java/com/ecep/contract/service/WebSocketServerTaskManager.java @@ -1,25 +1,27 @@ package com.ecep.contract.service; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Service; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + import com.ecep.contract.Message; import com.ecep.contract.constant.WebSocketConstant; import com.ecep.contract.ui.Tasker; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; - -import java.io.IOException; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; @Service public class WebSocketServerTaskManager implements InitializingBean { @@ -28,16 +30,31 @@ public class WebSocketServerTaskManager implements InitializingBean { private ObjectMapper objectMapper; @Autowired private ScheduledExecutorService scheduledExecutorService; + @Autowired + private ResourceLoader resourceLoader; private Map taskClzMap = Map.of(); @Override public void afterPropertiesSet() throws Exception { - taskClzMap = Map.of( - "ContractSyncTask", "com.ecep.contract.cloud.u8.ContractSyncTask", - "ContractRepairTask", "com.ecep.contract.ds.contract.tasker.ContractRepairTask", - "ContractVerifyTask", "com.ecep.contract.ds.contract.tasker.ContractVerifyTask", - "ProjectCostImportItemsFromContractsTasker", "com.ecep.contract.ds.project.ProjectCostImportItemsFromContractsTasker" - ); + // 从tasker_mapper.json文件中加载任务注册信息 + try { + Resource resource = resourceLoader.getResource("classpath:tasker_mapper.json"); + try (InputStream inputStream = resource.getInputStream()) { + JsonNode rootNode = objectMapper.readTree(inputStream); + JsonNode taskersNode = rootNode.get("taskers"); + if (taskersNode != null && taskersNode.isObject()) { + Map taskMap = new java.util.HashMap<>(); + taskersNode.fields().forEachRemaining(entry -> { + taskMap.put(entry.getKey(), entry.getValue().asText()); + }); + taskClzMap = taskMap; + } + } + } catch (Exception e) { + logger.error("Failed to load tasker_mapper.json", e); + // 使用默认值作为fallback + taskClzMap = Map.of(); + } } public void onMessage(WebSocketSession session, JsonNode jsonNode) { @@ -51,7 +68,6 @@ public class WebSocketServerTaskManager implements InitializingBean { } } - private void handleAsSessionCallback(WebSocketSession session, String sessionId, JsonNode jsonNode) { if (!jsonNode.has("type")) { throw new IllegalArgumentException("缺失 type 参数"); @@ -120,8 +136,7 @@ public class WebSocketServerTaskManager implements InitializingBean { String text = objectMapper.writeValueAsString(Map.of( WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId, "type", type, - WebSocketConstant.ARGUMENTS_FIELD_NAME, args - )); + WebSocketConstant.ARGUMENTS_FIELD_NAME, args)); session.sendMessage(new TextMessage(text)); } catch (IOException e) { // 捕获所有可能的异常,防止影响主流程 @@ -130,7 +145,6 @@ public class WebSocketServerTaskManager implements InitializingBean { return true; } - private void sendError(WebSocketSession session, String sessionId, String message) { if (session == null || !session.isOpen()) { logger.warn("尝试向已关闭的WebSocket会话发送错误消息: {}", message); @@ -141,8 +155,7 @@ public class WebSocketServerTaskManager implements InitializingBean { String errorMessage = objectMapper.writeValueAsString(Map.of( WebSocketConstant.SESSION_ID_FIELD_NAME, sessionId, WebSocketConstant.SUCCESS_FIELD_VALUE, false, - WebSocketConstant.MESSAGE_FIELD_NAME, message - )); + WebSocketConstant.MESSAGE_FIELD_NAME, message)); // 检查会话状态并尝试发送错误消息 if (session.isOpen()) { diff --git a/server/src/main/resources/tasker_mapper.json b/server/src/main/resources/tasker_mapper.json new file mode 100644 index 0000000..d76eb0c --- /dev/null +++ b/server/src/main/resources/tasker_mapper.json @@ -0,0 +1,11 @@ +{ + "taskers": { + "ContractSyncTask": "com.ecep.contract.cloud.u8.ContractSyncTask", + "ContractRepairTask": "com.ecep.contract.ds.contract.tasker.ContractRepairTask", + "ContractVerifyTask": "com.ecep.contract.ds.contract.tasker.ContractVerifyTask", + "ProjectCostImportItemsFromContractsTasker": "com.ecep.contract.ds.project.ProjectCostImportItemsFromContractsTasker", + "CompanyCustomerEvaluationFormUpdateTask": "com.ecep.contract.ds.customer.tasker.CompanyCustomerEvaluationFormUpdateTask", + "CompanyCustomerNextSignDateTask": "com.ecep.contract.ds.customer.tasker.CompanyCustomerNextSignDateTask", + "CompanyCustomerRebuildFilesTasker": "com.ecep.contract.ds.customer.tasker.CompanyCustomerRebuildFilesTasker" + } +} \ No newline at end of file diff --git a/src/main/resources/_log4j2.xml b/src/main/resources/_log4j2.xml deleted file mode 100644 index dac5875..0000000 --- a/src/main/resources/_log4j2.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file