diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md index d9dae67..5df84bf 100644 --- a/.trae/rules/project_rules.md +++ b/.trae/rules/project_rules.md @@ -20,6 +20,9 @@ - caffeine 3.1.8 - .fxml 界面UI, 放置于 /client/src/main/resources/ui/ 目录下 - websocket 与 server 模块通信 +- StringConverter 创建规则与实现逻辑 `docs/task/string_converter_implementation_guide.md` +- TableCell 规则与逻辑 `docs/task/table_cell_implementation_guide.md` +- Service 规则与逻辑 `docs/task/serservice_layer_rules.md` ### common 模块 - Java 21 @@ -66,3 +69,164 @@ ignore: - build/ - .DS_Store - *.class + + +# 6A工作流执行规则 +## 阶段1: Align (对齐阶段) +### 目标: 模糊需求 → 精确规范 +### 执行步骤 +1. **项目上下文分析** +- 分析现有项目结构、技术栈、架构模式、依赖关系 +- 分析现有代码模式、现有文档和约定 +- 理解业务域和数据模型 +2. **需求理解确认** +- 创建 `docs/任务名/ALIGNMENT_[任务名].md` +- 包含项目和任务特性规范 +- 包含原始需求、边界确认(明确任务范围)、需求理解(对现有项目的理解)、疑问澄清(存在歧义的地方) +3. **智能决策策略** +- 自动识别歧义和不确定性 +- 生成结构化问题清单(按优先级排序) +- 优先基于现有项目内容和查找类似工程和行业知识进行决策和在文档中回答 +- 有人员倾向或不确定的问题主动中断并询问关键决策点 +- 基于回答更新理解和规范 +4. **中断并询问关键决策点** +- 主动中断询问,迭代执行智能决策策略 +5. **最终共识** +- 生成 `docs/任务名/CONSENSUS_[任务名].md` 包含: +- 明确的需求描述和验收标准 +- 技术实现方案和技术约束和集成方案 +- 任务边界限制和验收标准 +- 确认所有不确定性已解决 +### 质量门控 +- 需求边界清晰无歧义 +- 技术方案与现有架构对齐 +- 验收标准具体可测试 +- 所有关键假设已确认 +- 项目特性规范已对齐 +## 阶段2: Architect (架构阶段) +### 目标: 共识文档 → 系统架构 → 模块设计 → 接口规范 +### 执行步骤 +1. **系统分层设计** +- 基于CONSENSUS、ALIGNMENT文档设计架构 +- 生成 `docs/任务名/DESIGN_[任务名].md` 包含: +- 整体架构图(mermaid绘制) +- 分层设计和核心组件 +- 模块依赖关系图 +- 接口契约定义 +- 数据流向图 +- 异常处理策略 +2. **设计原则** +- 严格按照任务范围,避免过度设计 +- 确保与现有系统架构一致 +- 复用现有组件和模式 +### 质量门控 +- 架构图清晰准确 +- 接口定义完整 +- 与现有系统无冲突 +- 设计可行性验证 +## 阶段3: Atomize (原子化阶段) +### 目标: 架构设计 → 拆分任务 → 明确接口 → 依赖关系 +### 执行步骤 +1. **子任务拆分** +- 基于DESIGN文档生成 `docs/任务名/TASK_[任务名].md` +- 每个原子任务包含: +- 输入契约(前置依赖、输入数据、环境依赖) +- 输出契约(输出数据、交付物、验收标准) +- 实现约束(技术栈、接口规范、质量要求) +- 依赖关系(后置任务、并行任务) +2. **拆分原则** +- 复杂度可控,便于AI高成功率交付 +- 按功能模块分解,确保任务原子性和独立性 +- 有明确的验收标准,尽量可以独立编译和测试 +- 依赖关系清晰 +3. **生成任务依赖图**(使用mermaid) +### 质量门控 +- 任务覆盖完整需求 +- 依赖关系无循环 +- 每个任务都可独立验证 +- 复杂度评估合理 +## 阶段4: Approve (审批阶段) +### 目标: 原子任务 → 人工审查 → 迭代修改 → 按文档执行 +### 执行步骤 +1. **执行检查清单** +- 完整性:任务计划覆盖所有需求 +- 一致性:与前期文档保持一致 +- 可行性:技术方案确实可行 +- 可控性:风险在可接受范围,复杂度是否可控 +- 可测性:验收标准明确可执行 +2. **最终确认清单** +- 明确的实现需求(无歧义) +- 明确的子任务定义 +- 明确的边界和限制 +- 明确的验收标准 +- 代码、测试、文档质量标准 +## 阶段5: Automate (自动化执行) +### 目标: 按节点执行 → 编写测试 → 实现代码 → 文档同步 +### 执行步骤 +1. **逐步实施子任务** +- 创建 `docs/任务名/ACCEPTANCE_[任务名].md` 记录完成情况 +2. **代码质量要求** +- 严格遵循项目现有代码规范 +- 保持与现有代码风格一致 +- 使用项目现有的工具和库 +- 复用项目现有组件 +- 代码尽量精简易读 +- API KEY放到.env文件中并且不要提交git +3. **异常处理** +- 遇到不确定问题立刻中断执行 +- 在TASK文档中记录问题详细信息和位置 +- 寻求人工澄清后继续 +4. **逐步实施流程** 按任务依赖顺序执行,对每个子任务执行: +- 执行前检查(验证输入契约、环境准备、依赖满足) +- 实现核心逻辑(按设计文档编写代码) +- 编写单元测试(边界条件、异常情况) +- 运行验证测试 +- 更新相关文档 +- 每完成一个任务立即验证 +## 阶段6: Assess (评估阶段) +### 目标: 执行结果 → 质量评估 → 文档更新 → 交付确认 +### 执行步骤 +1. **验证执行结果** +- 更新 `docs/任务名/ACCEPTANCE_[任务名].md` +- 整体验收检查: +- 所有需求已实现 +- 验收标准全部满足 +- 项目编译通过 +- 所有测试通过 +- 功能完整性验证 +- 实现与设计文档一致 +2. **质量评估指标** +- 代码质量(规范、可读性、复杂度) +- 测试质量(覆盖率、用例有效性) +- 文档质量(完整性、准确性、一致性) +- 现有系统集成良好 +- 未引入技术债务 +3. **最终交付物** +- 生成 `docs/任务名/FINAL_[任务名].md`(项目总结报告) +- 生成 `docs/任务名/TODO_[任务名].md`(精简明确哪些待办的事宜和哪些缺少的配置等,我方便直接寻找支持) +4. **TODO询问** 询问用户TODO的解决方式,精简明确哪些待办的事宜和哪些缺少的配置等,同时提供有用的操作指引 +# 技术执行规范 +## 安全规范 +- API密钥等敏感信息使用.env文件管理 +## 文档同步 +- 代码变更同时更新相关文档 +## 测试策略 +- 测试优先:先写测试,后写实现 +- 边界覆盖:覆盖正常流程、边界条件、异常情况 +## 交互体验优化 +### 进度反馈 +- 显示当前执行阶段 +- 提供详细的执行步骤 +- 标示完成情况 +- 突出需要关注的问题 +### 异常处理机制 +#### 中断条件 +- 遇到无法自主决策的问题 +- 觉得需要询问用户的问题 +- 技术实现出现阻塞 +- 文档不一致需要确认修正 +#### 恢复策略 +- 保存当前执行状态 +- 记录问题详细信息 +- 询问并等待人工干预 +- 从中断点任务继续执行 \ No newline at end of file diff --git a/client/src/main/java/com/ecep/contract/WebSocketClientService.java b/client/src/main/java/com/ecep/contract/WebSocketClientService.java index da8c07a..ebf8fe6 100644 --- a/client/src/main/java/com/ecep/contract/WebSocketClientService.java +++ b/client/src/main/java/com/ecep/contract/WebSocketClientService.java @@ -342,7 +342,7 @@ public class WebSocketClientService { try { sessionConsumer.accept(session); } finally { - // closeSession(session); + // closeSession(session);vvvv } } diff --git a/client/src/main/java/com/ecep/contract/WebSocketClientTasker.java b/client/src/main/java/com/ecep/contract/WebSocketClientTasker.java index 366fbf3..7d3aabb 100644 --- a/client/src/main/java/com/ecep/contract/WebSocketClientTasker.java +++ b/client/src/main/java/com/ecep/contract/WebSocketClientTasker.java @@ -1,29 +1,117 @@ package com.ecep.contract; import com.fasterxml.jackson.core.JsonProcessingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Locale; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.logging.Level; +/** + * WebSocket客户端任务接口 + * 定义了所有通过WebSocket与服务器通信的任务的通用方法 + * 包括任务名称、更新消息、更新标题、更新进度等操作 + */ public interface WebSocketClientTasker { + /** + * 获取任务名称 + * + * @return 任务名称 + */ String getTaskName(); + /** + * 更新任务执行过程中的消息 + * + * @param level 消息级别 + * @param message 消息内容 + */ void updateMessage(Level level, String message); + /** + * 更新任务标题 + * + * @param title 任务标题 + */ void updateTitle(String title); + /** + * 更新任务进度 + * + * @param current 当前进度 + * @param total 总进度 + */ void updateProgress(long current, long total); + /** + * 取消任务执行 + * 默认实现为空,可由具体任务重写以提供取消逻辑 + */ + default void cancelTask() { + // 默认实现为空,由具体任务重写 + } + + /** + * 调用远程WebSocket任务 + * + * @param holder 消息持有者,用于记录任务执行过程中的消息 + * @param locale 语言环境 + * @param args 任务参数 + * @return 任务执行结果 + */ default Object callRemoteTask(MessageHolder holder, Locale locale, Object... args) { WebSocketClientService webSocketService = SpringApp.getBean(WebSocketClientService.class); + + // 检查WebSocket连接是否可用 + if (!webSocketService.getOnlineProperty().get()) { + String errorMsg = "WebSocket连接不可用,请检查网络连接或服务器状态"; + holder.addMessage(Level.SEVERE, errorMsg); + return null; + } + webSocketService.withSession(session -> { try { session.submitTask(this, locale, args); + holder.addMessage(Level.INFO, "已提交任务到服务器: " + getTaskName()); } catch (JsonProcessingException e) { - throw new RuntimeException(e); + String errorMsg = "任务提交失败: " + e.getMessage(); + holder.addMessage(Level.SEVERE, errorMsg); + throw new RuntimeException("任务提交失败: " + e.getMessage(), e); } }); - return null; } + + /** + * 异步调用远程WebSocket任务 + * + * @param holder 消息持有者,用于记录任务执行过程中的消息 + * @param locale 语言环境 + * @param args 任务参数 + * @return 包含任务执行结果的CompletableFuture + */ + default CompletableFuture callRemoteTaskAsync(MessageHolder holder, Locale locale, Object... args) { + CompletableFuture future = new CompletableFuture<>(); + + try { + // 立即执行callRemoteTask并返回结果 + Object result = callRemoteTask(holder, locale, args); + future.complete(result); + } catch (Exception e) { + future.completeExceptionally(e); + } + + return future; + } + + /** + * 生成唯一的任务ID + * + * @return 唯一的任务ID + */ + default String generateTaskId() { + return UUID.randomUUID().toString(); + } } 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 81a84f0..6beebe1 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 @@ -7,6 +7,7 @@ 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; @@ -59,7 +60,7 @@ public class CustomerTabSkinFile private CompanyCustomerFileService companyCustomerFileService; public TableColumn fileTable_idColumn; - public TableColumn fileTable_typeColumn; + public TableColumn fileTable_typeColumn; public TableColumn fileTable_filePathColumn; public TableColumn fileTable_editFilePathColumn; public TableColumn fileTable_signDateColumn; @@ -106,15 +107,15 @@ public class CustomerTabSkinFile table.disableProperty().bind(viewModel.getPath().isEmpty()); fileTable_idColumn.setCellValueFactory(param -> param.getValue().getId()); CompanyCustomerFileTypeService fileTypeService = getCachedBean(CompanyCustomerFileTypeService.class); - ObservableMap observableMapByLocal = FXCollections - .observableMap(fileTypeService.findAll(getLocale())); - fileTable_typeColumn.setCellValueFactory(param -> Bindings.valueAt(observableMapByLocal, - param.getValue().getType()).map(BaseEnumEntity::getValue)); + 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_editFilePathColumn.setCellValueFactory(param -> param.getValue().getEditFilePath()); fileTable_editFilePathColumn.setCellFactory(param -> new FileTableFilePathTableCell()); + fileTable_signDateColumn.setCellValueFactory(param -> param.getValue().getSignDate()); fileTable_validColumn.setEditable(true); fileTable_validColumn.setCellValueFactory(param -> param.getValue().getValid()); @@ -251,21 +252,6 @@ public class CustomerTabSkinFile }); } - @Override - public CompanyCustomerFileVo loadRowData(CompanyCustomerFileViewModel row) { - return getCompanyCustomerFileService().findById(row.getId().get()); - } - - @Override - public CompanyCustomerFileVo saveRowData(CompanyCustomerFileVo entity) { - return getCompanyCustomerFileService().save(entity); - } - - @Override - public void deleteRowData(CompanyCustomerFileVo entity) { - getCompanyCustomerFileService().delete(entity); - } - @Override protected boolean deleteRow(CompanyCustomerFileViewModel row) { String path = row.getFilePath().get(); diff --git a/client/src/main/java/com/ecep/contract/controller/table/cell/CompanyCustomerFileTableTypeTableCell.java b/client/src/main/java/com/ecep/contract/controller/table/cell/CompanyCustomerFileTableTypeTableCell.java new file mode 100644 index 0000000..f16b605 --- /dev/null +++ b/client/src/main/java/com/ecep/contract/controller/table/cell/CompanyCustomerFileTableTypeTableCell.java @@ -0,0 +1,116 @@ +package com.ecep.contract.controller.table.cell; + +import static com.ecep.contract.SpringApp.getBean; + +import com.ecep.contract.CustomerFileType; +import com.ecep.contract.service.CompanyCustomerFileTypeService; +import com.ecep.contract.vo.CustomerFileTypeLocalVo; + +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.util.Callback; +import lombok.NoArgsConstructor; + +import java.util.Locale; + +/** + * 公司客户文件类型表格单元格,用于在表格中显示客户文件类型信息 + * 根据国际化设置显示对应的文件类型名称 + */ +@NoArgsConstructor +public class CompanyCustomerFileTableTypeTableCell extends AsyncUpdateTableCell { + private CompanyCustomerFileTypeService companyCustomerFileTypeService; + + /** + * 创建一个用于表格列的单元格工厂,自动获取CompanyCustomerFileTypeService实例 + * + * @param 表格行类型 + * @return TableCell工厂回调 + */ + public static Callback, TableCell> forTableColumn() { + return forTableColumn(getBean(CompanyCustomerFileTypeService.class)); + } + + /** + * 创建一个用于表格列的单元格工厂,使用提供的CompanyCustomerFileTypeService实例 + * + * @param 表格行类型 + * @param service CompanyCustomerFileTypeService实例 + * @return TableCell工厂回调 + */ + public static Callback, TableCell> forTableColumn( + CompanyCustomerFileTypeService service) { + return param -> new CompanyCustomerFileTableTypeTableCell<>(service); + } + + /** + * 使用提供的服务创建单元格实例 + * + * @param service CompanyCustomerFileTypeService实例 + */ + public CompanyCustomerFileTableTypeTableCell(CompanyCustomerFileTypeService service) { + this.companyCustomerFileTypeService = service; + } + + /** + * 获取CompanyCustomerFileTypeService服务实例 + * 如果未设置,则从Spring容器中获取 + * + * @return CompanyCustomerFileTypeService实例 + */ + @Override + protected CompanyCustomerFileTypeService getServiceBean() { + if (companyCustomerFileTypeService == null) { + companyCustomerFileTypeService = getBean(CompanyCustomerFileTypeService.class); + } + return companyCustomerFileTypeService; + } + + /** + * 初始化文件类型实体 + * 根据当前的CustomerFileType枚举值,从服务中获取对应的本地化实体 + * + * @return 本地化的文件类型实体 + */ + @Override + protected CustomerFileTypeLocalVo initialize() { + CustomerFileType item = getItem(); + if (item == null) { + return null; + } + + // 获取当前用户的语言设置 + Locale locale = com.ecep.contract.Desktop.instance.getActiveEmployee().localeProperty().get(); + return getServiceBean().findByLocaleAndType(locale, item); + } + + /** + * 格式化显示文件类型 + * 如果有本地化实体,则显示本地化名称;否则显示枚举名称 + * + * @param entity 本地化的文件类型实体 + * @return 格式化后的显示文本 + */ + @Override + public String format(CustomerFileTypeLocalVo entity) { + if (entity != null && entity.getValue() != null) { + return entity.getValue(); + } + CustomerFileType item = getItem(); + if (item != null) { + // 根据枚举值返回对应的中文名称 + switch (item) { + case General -> { + return "普通文件"; + } + case EvaluationForm -> { + return "评估表"; + } + default -> { + return item.name(); + } + } + } + return "未知类型"; + } +} \ No newline at end of file diff --git a/client/src/main/java/com/ecep/contract/controller/table/cell/CustomerCatalogTableCell.java b/client/src/main/java/com/ecep/contract/controller/table/cell/CustomerCatalogTableCell.java index 6f972ea..e2f726b 100644 --- a/client/src/main/java/com/ecep/contract/controller/table/cell/CustomerCatalogTableCell.java +++ b/client/src/main/java/com/ecep/contract/controller/table/cell/CustomerCatalogTableCell.java @@ -13,6 +13,14 @@ import lombok.NoArgsConstructor; @NoArgsConstructor public class CustomerCatalogTableCell extends AsyncUpdateTableCell { + public static Callback, TableCell> forTableColumn(CustomerCatalogService service) { + return param -> new CustomerCatalogTableCell<>(service); + } + + public static Callback, TableCell> forTableColumn() { + return forTableColumn(getBean(CustomerCatalogService.class)); + } + public CustomerCatalogTableCell(CustomerCatalogService service) { setService(service); } @@ -21,13 +29,10 @@ public class CustomerCatalogTableCell extends AsyncUpdateTableCell Callback, TableCell> forTableColumn() { - return forTableColumn(getBean(CustomerCatalogService.class)); - } - public static Callback, TableCell> forTableColumn(CustomerCatalogService service) { - return param -> new CustomerCatalogTableCell(service); + @Override + public String format(CustomerCatalogVo entity) { + return entity.getName(); } } \ No newline at end of file diff --git a/client/src/main/java/com/ecep/contract/converter/CustomerCatalogStringConverter.java b/client/src/main/java/com/ecep/contract/converter/CustomerCatalogStringConverter.java new file mode 100644 index 0000000..e608fef --- /dev/null +++ b/client/src/main/java/com/ecep/contract/converter/CustomerCatalogStringConverter.java @@ -0,0 +1,49 @@ +package com.ecep.contract.converter; + +import com.ecep.contract.service.CustomerCatalogService; +import com.ecep.contract.vo.CustomerCatalogVo; +import javafx.util.StringConverter; + +/** + * 客户分类字符串转换器 + * 用于在UI组件中显示客户分类信息并支持从字符串还原客户分类对象 + * + * @author AI Assistant + * @since 2024-01-01 + */ +public class CustomerCatalogStringConverter extends StringConverter { + + /** 客户分类服务,用于从字符串查找对应的客户分类对象 */ + private final CustomerCatalogService service; + + /** + * 构造函数 + * + * @param service 客户分类服务实例 + */ + public CustomerCatalogStringConverter(CustomerCatalogService service) { + this.service = service; + } + + /** + * 将客户分类对象转换为字符串表示 + * + * @param object 客户分类对象 + * @return 客户分类的名称,如果对象为null则返回空字符串 + */ + @Override + public String toString(CustomerCatalogVo object) { + return object == null ? "" : object.getName(); + } + + /** + * 从字符串还原客户分类对象 + * + * @param string 客户分类名称 + * @return 对应的客户分类对象,如果未找到则返回null + */ + @Override + public CustomerCatalogVo fromString(String string) { + return service.findByName(string); + } +} \ No newline at end of file diff --git a/client/src/main/java/com/ecep/contract/converter/UnitStringConverter.java b/client/src/main/java/com/ecep/contract/converter/UnitStringConverter.java new file mode 100644 index 0000000..790fd20 --- /dev/null +++ b/client/src/main/java/com/ecep/contract/converter/UnitStringConverter.java @@ -0,0 +1,53 @@ +package com.ecep.contract.converter; + +import org.springframework.stereotype.Component; + +import com.ecep.contract.service.UnitService; +import com.ecep.contract.vo.UnitVo; + +import javafx.util.StringConverter; + +/** + * UnitVo的StringConverter实现,用于JavaFX控件中的显示和转换 + */ +@Component +public class UnitStringConverter extends StringConverter { + private final UnitService unitService; + + /** + * 构造函数 + * + * @param unitService UnitService实例 + */ + public UnitStringConverter(UnitService unitService) { + this.unitService = unitService; + } + + /** + * 将UnitVo对象转换为字符串 + * + * @param object UnitVo对象 + * @return 转换后的字符串 + */ + @Override + public String toString(UnitVo object) { + if (object == null) { + return ""; + } + return object.getName(); + } + + /** + * 将字符串转换为UnitVo对象 + * + * @param string 字符串 + * @return 转换后的UnitVo对象 + */ + @Override + public UnitVo fromString(String string) { + if (string == null || string.isEmpty()) { + return null; + } + return unitService.findByName(string); + } +} \ No newline at end of file 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 02256f5..69b99da 100644 --- a/client/src/main/java/com/ecep/contract/service/CompanyCustomerFileService.java +++ b/client/src/main/java/com/ecep/contract/service/CompanyCustomerFileService.java @@ -27,6 +27,11 @@ import com.ecep.contract.vo.ContractVo; @Service @CacheConfig(cacheNames = "customer-file") public class CompanyCustomerFileService extends QueryService { + + public File getEvaluationFormTemplate() { + throw new UnsupportedOperationException(); + } + @Cacheable @Override public CompanyCustomerFileVo findById(Integer id) { @@ -95,9 +100,6 @@ public class CompanyCustomerFileService extends QueryService findAllByCustomer(CompanyCustomerVo companyCustomer) { return findAll(ParamUtils.builder().equals("customer", companyCustomer).build(), Pageable.unpaged()) diff --git a/client/src/main/java/com/ecep/contract/service/CompanyCustomerFileTypeService.java b/client/src/main/java/com/ecep/contract/service/CompanyCustomerFileTypeService.java index 6ab2a9f..109ac52 100644 --- a/client/src/main/java/com/ecep/contract/service/CompanyCustomerFileTypeService.java +++ b/client/src/main/java/com/ecep/contract/service/CompanyCustomerFileTypeService.java @@ -64,9 +64,26 @@ public class CompanyCustomerFileTypeService return stringConverter; } - public CustomerFileTypeLocalVo findByLocaleAndValue(Locale locale, String string) { - return findAll(ParamUtils.builder().equals("lang", locale.toLanguageTag()).equals("value", string).build(), + /** + * 根据语言标签和参数查找单个 CustomerFileTypeLocalVo 对象 + * + * @param locale 语言区域 + * @param key 参数键 + * @param value 参数值 + * @return 查找到的 CustomerFileTypeLocalVo 对象,未找到则返回 null + */ + private CustomerFileTypeLocalVo findOneByLang(Locale locale, String key, Object value) { + return findAll(ParamUtils.builder().equals("lang", locale.toLanguageTag()).equals(key, value).build(), Pageable.ofSize(1)) .stream().findFirst().orElse(null); } + + public CustomerFileTypeLocalVo findByLocaleAndValue(Locale locale, String string) { + return findOneByLang(locale, "value", string); + } + + public CustomerFileTypeLocalVo findByLocaleAndType(Locale locale, CustomerFileType type) { + return findOneByLang(locale, "type", type); + } + } 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 8a8e057..7a95b2b 100644 --- a/client/src/main/java/com/ecep/contract/service/CompanyCustomerService.java +++ b/client/src/main/java/com/ecep/contract/service/CompanyCustomerService.java @@ -9,27 +9,18 @@ import org.springframework.stereotype.Service; import com.ecep.contract.MessageHolder; import com.ecep.contract.SpringApp; import com.ecep.contract.constant.CompanyCustomerConstant; +import com.ecep.contract.task.CompanyCustomerRebuildFilesTasker; import com.ecep.contract.util.ParamUtils; +import com.ecep.contract.util.UITools; import com.ecep.contract.vm.CompanyCustomerViewModel; import com.ecep.contract.vo.CompanyCustomerVo; import com.ecep.contract.vo.CompanyVo; +import org.springframework.util.StringUtils; @Service public class CompanyCustomerService extends QueryService { private File basePath; - public CompanyCustomerVo findByCompany(CompanyVo company) { - Page page = findAll(ParamUtils.equal("company", company.getId()), Pageable.ofSize(1)); - if (page.isEmpty()) { - return null; - } - return page.getContent().getFirst(); - } - - public boolean reBuildingFiles(CompanyCustomerVo companyCustomer, MessageHolder holder) { - throw new UnsupportedOperationException("Unimplemented method 'reBuildingFiles'"); - } - public File getBasePath() { if (basePath == null) { basePath = new File( @@ -38,8 +29,107 @@ public class CompanyCustomerService extends QueryService page = findAll(ParamUtils.equal("company", company.getId()), Pageable.ofSize(1)); + if (page.isEmpty()) { + return null; + } + return page.getContent().getFirst(); + } + + /** + * 重建客户相关文件 + * + * @param companyCustomer 客户对象 + * @param holder 消息持有者,用于显示任务执行过程中的消息 + * @return 如果文件重建成功则返回true,否则返回false + */ + public boolean reBuildingFiles(CompanyCustomerVo companyCustomer, MessageHolder holder) { + // 首先确保客户有有效的路径 + if (!makePathAbsent(companyCustomer)) { + holder.addMessage(java.util.logging.Level.WARNING, "无法创建或确认客户路径,文件重建可能失败"); + } + + // 创建并配置文件重建任务 + CompanyCustomerRebuildFilesTasker tasker = new CompanyCustomerRebuildFilesTasker(); + tasker.setCompanyCustomer(companyCustomer); + + // 显示任务对话框并等待任务完成 + UITools.showTaskDialogAndWait("重建客户文件", tasker, null); + + // 返回任务执行结果 + return tasker.isFilesUpdated(); + } + + /** + * 检查客户路径是否不存在,如果不存在则创建并设置路径 + * + * @param companyCustomer 客户对象 + * @return 如果成功创建路径并设置则返回true,否则返回false + */ public boolean makePathAbsent(CompanyCustomerVo companyCustomer) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'makePathAbsent'"); + String path = companyCustomer.getPath(); + if (StringUtils.hasText(path)) { + File file = new File(path); + if (file.exists()) { + return false; + } + } + + File dir = makePath(companyCustomer); + if (dir == null) { + return false; + } + if (!dir.exists()) { + return false; + } + companyCustomer.setPath(dir.getAbsolutePath()); + return true; + } + + /** + * 为客户创建路径 + * + * @param companyCustomer 客户对象 + * @return 创建的目录对象,如果创建失败则返回null + */ + private File makePath(CompanyCustomerVo companyCustomer) { + File basePath = getBasePath(); + Integer companyId = companyCustomer.getCompanyId(); + CompanyVo company = SpringApp.getBean(CompanyService.class).findById(companyId); + // 在client端,company对象通常已经是完整加载的,不需要额外查询 + String companyName = company.getName(); + // 格式化公司客户ID并创建文件名 + String fileName = formatCompanyCustomerId(companyCustomer.getId()) + "-" + escapeFileName(companyName); + + File dir = new File(basePath, fileName); + if (!dir.exists()) { + if (!dir.mkdir()) { + return null; + } + } + return dir; + } + + /** + * 格式化公司客户ID,确保格式一致 + * + * @param id 客户ID + * @return 格式化后的ID字符串 + */ + private String formatCompanyCustomerId(Integer id) { + // 简单实现,实际应与server端的CompanyUtils.formatCompanyVendorId保持一致 + return String.format("%06d", id); + } + + /** + * 转义文件名,确保合法 + * + * @param fileName 原始文件名 + * @return 转义后的文件名 + */ + private String escapeFileName(String fileName) { + // 替换文件名中的非法字符 + return fileName.replaceAll("[/\\:*?\"<>|]", "_"); } } diff --git a/client/src/main/java/com/ecep/contract/service/CompanyFileTypeService.java b/client/src/main/java/com/ecep/contract/service/CompanyFileTypeService.java index 56d0d94..c1ab3dc 100644 --- a/client/src/main/java/com/ecep/contract/service/CompanyFileTypeService.java +++ b/client/src/main/java/com/ecep/contract/service/CompanyFileTypeService.java @@ -36,13 +36,13 @@ public class CompanyFileTypeService extends QueryService { + private final CustomerCatalogStringConverter stringConverter = new CustomerCatalogStringConverter(this); + + @Override + @Cacheable(key = "#id") + public CustomerCatalogVo findById(Integer id) { + return super.findById(id); + } + + public CustomerCatalogVo findByName(String string) { + return findAll(ParamUtils.builder().equals("name", string).build(), Pageable.ofSize(1)).stream().findFirst().orElse(null); + } + + @Override + @Cacheable(key = "'all'") + public java.util.List findAll() { + return super.findAll(); + } + + @Caching(evict = {@CacheEvict(key = "'all'"), @CacheEvict(key = "#entity.id")}) + @Override + public CustomerCatalogVo save(CustomerCatalogVo entity) { + return super.save(entity); + } + + @Caching(evict = {@CacheEvict(key = "'all'"), @CacheEvict(key = "#entity.id")}) + @Override + public void delete(CustomerCatalogVo entity) { + super.delete(entity); + } + + @Override + public StringConverter getStringConverter() { + return stringConverter; + } } diff --git a/client/src/main/java/com/ecep/contract/service/ProjectFileTypeService.java b/client/src/main/java/com/ecep/contract/service/ProjectFileTypeService.java index 973f451..eb73315 100644 --- a/client/src/main/java/com/ecep/contract/service/ProjectFileTypeService.java +++ b/client/src/main/java/com/ecep/contract/service/ProjectFileTypeService.java @@ -61,9 +61,25 @@ public class ProjectFileTypeService extends QueryService { + @Cacheable(key = "'name-'+#p0") public ProjectVo findByName(String name) { Page page = findAll(ParamUtils.builder() .equals("name", name).build(), @@ -39,6 +45,7 @@ public class ProjectService extends QueryService { return page.getContent().getFirst(); } + @Cacheable(key = "'code-'+#p0") public ProjectVo findByCode(String code) { Page page = findAll(ParamUtils.builder() .equals("code", code).build(), @@ -198,6 +205,15 @@ public class ProjectService extends QueryService { return page.getContent().getFirst().getCodeSequenceNumber() + 1; } + /** + * 根据销售类型、代码年份和代码序号查找项目 + * 此方法不缓存 + * + * @param typeId 销售类型ID + * @param codeYear 代码年份 + * @param codeSequenceNumber 代码序号 + * @return 查找到的项目对象,未找到则返回 null + */ public ProjectVo findBySaleTypeAndCodeYearAndCodeSequenceNumber(int typeId, int codeYear, int codeSequenceNumber) { Page page = findAll(ParamUtils.builder() @@ -210,4 +226,30 @@ public class ProjectService extends QueryService { } return page.getContent().getFirst(); } + + @Override + @Cacheable(key = "#p0") + public ProjectVo findById(Integer id) { + return super.findById(id); + } + + @Override + @Caching(evict = { + @CacheEvict(key = "#p0.id"), + @CacheEvict(key = "'code-'+#p0.code"), + @CacheEvict(key = "'name-'+#p0.name") + }) + public ProjectVo save(ProjectVo entity) { + return super.save(entity); + } + + @Override + @Caching(evict = { + @CacheEvict(key = "#p0.id"), + @CacheEvict(key = "'code-'+#p0.code"), + @CacheEvict(key = "'name-'+#p0.name") + }) + public void delete(ProjectVo entity) { + super.delete(entity); + } } diff --git a/client/src/main/java/com/ecep/contract/service/QueryService.java b/client/src/main/java/com/ecep/contract/service/QueryService.java index d0bedda..6208380 100644 --- a/client/src/main/java/com/ecep/contract/service/QueryService.java +++ b/client/src/main/java/com/ecep/contract/service/QueryService.java @@ -192,6 +192,11 @@ public class QueryService类型结果 * diff --git a/client/src/main/java/com/ecep/contract/service/UnitService.java b/client/src/main/java/com/ecep/contract/service/UnitService.java index 0845c70..5fb88e4 100644 --- a/client/src/main/java/com/ecep/contract/service/UnitService.java +++ b/client/src/main/java/com/ecep/contract/service/UnitService.java @@ -22,20 +22,21 @@ public class UnitService extends QueryService { return super.findById(id); } - @Caching( - put = @CachePut(key = "#entity.id"), - evict = @CacheEvict(key = "'all'")) + @Cacheable(key = "#name") + public UnitVo findByName(String name) { + return findOneByProperty("name", name); + } + + @Caching(put = @CachePut(key = "#entity.id"), evict = @CacheEvict(key = "'all'")) @Override public UnitVo save(UnitVo entity) { return super.save(entity); } - @Caching( - evict = { - @CacheEvict(key = "#entity.id"), - @CacheEvict(key = "'all'") - } - ) + @Caching(evict = { + @CacheEvict(key = "#entity.id"), + @CacheEvict(key = "'all'") + }) @Override public void delete(UnitVo entity) { super.delete(entity); @@ -50,4 +51,5 @@ public class UnitService extends QueryService { public UnitViewModel createNewViewModel() { return new UnitViewModel(); } + } \ No newline at end of file diff --git a/client/src/main/java/com/ecep/contract/service/VendorFileTypeService.java b/client/src/main/java/com/ecep/contract/service/VendorFileTypeService.java index 6718df6..2cbfd1a 100644 --- a/client/src/main/java/com/ecep/contract/service/VendorFileTypeService.java +++ b/client/src/main/java/com/ecep/contract/service/VendorFileTypeService.java @@ -58,8 +58,24 @@ public class VendorFileTypeService extends QueryService { @Autowired private CompanyService companyService; @@ -52,6 +57,7 @@ public class VendorService extends QueryService page = findAll(ParamUtils.equal("company", company.getId()), Pageable.ofSize(1)); if (page.isEmpty()) { @@ -170,4 +176,28 @@ public class VendorService extends QueryService findAll(Locale locale) { + return findAll(ParamUtils.builder().equals("lang", locale.toLanguageTag()).build(), Pageable.unpaged()).stream() + .collect(Collectors.toMap(VendorTypeLocalVo::getType, Function.identity())); + } + + /** + * 根据语言标签和参数查找单个 VendorTypeLocalVo 对象 + * + * @param locale 语言区域 + * @param key 参数键 + * @param value 参数值 + * @return 查找到的 VendorTypeLocalVo 对象,未找到则返回 null + */ + private VendorTypeLocalVo findOneByLang(Locale locale, String key, Object value) { + return findAll(ParamUtils.builder().equals("lang", locale.toLanguageTag()).equals(key, value).build(), Pageable.ofSize(1)) .stream().findFirst().orElse(null); } + public VendorTypeLocalVo findByLocaleAndValue(Locale locale, String string) { + return findOneByLang(locale, "value", string); + } + + public VendorTypeLocalVo findByLocaleAndType(Locale locale, VendorType type) { + return findOneByLang(locale, "type", type); + } + public StringConverter getTypeStringConverter() { return new StringConverter<>() { final VendorTypeService service = SpringApp.getBean(VendorTypeService.class); diff --git a/client/src/main/java/com/ecep/contract/task/CompanyCustomerRebuildFilesTasker.java b/client/src/main/java/com/ecep/contract/task/CompanyCustomerRebuildFilesTasker.java new file mode 100644 index 0000000..4132222 --- /dev/null +++ b/client/src/main/java/com/ecep/contract/task/CompanyCustomerRebuildFilesTasker.java @@ -0,0 +1,37 @@ +package com.ecep.contract.task; + +import com.ecep.contract.MessageHolder; +import com.ecep.contract.WebSocketClientTasker; +import com.ecep.contract.vo.CompanyCustomerVo; + +import lombok.Getter; +import lombok.Setter; + +/** + * 客户文件重建任务类 + * 用于通过WebSocket与服务器通信,重建客户相关文件 + */ +public class CompanyCustomerRebuildFilesTasker extends Tasker implements WebSocketClientTasker { + @Getter + @Setter + private CompanyCustomerVo companyCustomer; + + @Getter + protected boolean filesUpdated = false; + + @Override + public String getTaskName() { + return "CompanyCustomerRebuildFilesTasker"; + } + + @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(), companyCustomer.getId()); + } +} \ No newline at end of file diff --git a/client/src/main/java/com/ecep/contract/task/ContractSyncTask.java b/client/src/main/java/com/ecep/contract/task/ContractSyncTask.java index c73b4db..1e5741a 100644 --- a/client/src/main/java/com/ecep/contract/task/ContractSyncTask.java +++ b/client/src/main/java/com/ecep/contract/task/ContractSyncTask.java @@ -1,13 +1,12 @@ package com.ecep.contract.task; +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.WebSocketClientService; import com.ecep.contract.service.YongYouU8Service; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * 合同同步任务 diff --git a/client/src/main/java/com/ecep/contract/util/ParamUtils.java b/client/src/main/java/com/ecep/contract/util/ParamUtils.java index 95e7d86..2bb6bee 100644 --- a/client/src/main/java/com/ecep/contract/util/ParamUtils.java +++ b/client/src/main/java/com/ecep/contract/util/ParamUtils.java @@ -7,85 +7,197 @@ import java.util.function.Consumer; import com.ecep.contract.constant.ServiceConstant; +/** + * 参数工具类,用于构建查询条件参数 + * 提供了一系列静态方法和Builder模式,用于创建各种查询条件 + */ public class ParamUtils { + /** + * 创建日期范围查询参数 + * + * @param key 查询字段名 + * @param begin 开始日期 + * @param end 结束日期 + * @return 包含日期范围的查询参数Map + */ public static Map between(String key, LocalDate begin, LocalDate end) { return Map.of(key, Map.of( "begin", begin, "end", end)); } + /** + * 创建等于条件查询参数 + * + * @param key 查询字段名 + * @param value 查询值 + * @return 包含等于条件的查询参数Map + */ public static Map equal(String key, Object value) { return Map.of(key, value); } + /** + * 创建字符串模糊查询参数 + * + * @param key 查询字段名 + * @param value 模糊查询的字符串值 + * @return 包含模糊查询条件的查询参数Map + */ public static Map like(String key, String value) { Map params = new HashMap<>(); params.put(key, "%" + value + "%"); return params; } + /** + * 创建日期模糊查询参数 + * + * @param key 查询字段名 + * @param value 模糊查询的日期值 + * @return 包含日期模糊查询条件的查询参数Map + */ public static Map like(String key, LocalDate value) { Map params = new HashMap<>(); params.put(key, "%" + value + "%"); return params; } + /** + * 创建非空条件查询参数 + * + * @param key 查询字段名 + * @return 包含非空条件的查询参数Map + */ public static Map isNotNull(String key) { Map params = new HashMap<>(); params.put(key, Map.of("isNotNull", true)); return params; } + /** + * 创建小于条件的日期查询参数 + * + * @param key 查询字段名 + * @param value 比较的日期值 + * @return 包含小于条件的查询参数Map + */ public static Map lessThan(String key, LocalDate value) { Map params = new HashMap<>(); params.put(key, Map.of("lessThan", value)); return params; } + /** + * 创建参数构建器实例 + * + * @return Builder实例,用于链式构建复杂查询条件 + */ public static Builder builder() { return new Builder(); } + /** + * 参数构建器类,使用建造者模式构建复杂查询条件 + * 支持链式调用,提高代码可读性 + */ public static class Builder { + // 存储构建的查询参数 private Map params = new HashMap<>(); + /** + * 私有构造方法,防止外部直接实例化 + * 应通过ParamUtils.builder()方法获取实例 + */ private Builder() { } + /** + * 添加非空条件到构建器 + * + * @param key 查询字段名 + * @return 当前Builder实例,支持链式调用 + */ public Builder isNotNull(String key) { params.put(key, Map.of("isNotNull", true)); return this; } + /** + * 添加小于条件到构建器(针对日期类型) + * + * @param key 查询字段名 + * @param value 比较的日期值 + * @return 当前Builder实例,支持链式调用 + */ public Builder lessThan(String key, LocalDate value) { params.put(key, Map.of("lessThan", value)); return this; } + /** + * 添加大于条件到构建器(针对日期类型) + * + * @param key 查询字段名 + * @param value 比较的日期值 + * @return 当前Builder实例,支持链式调用 + */ public Builder greaterThan(String key, LocalDate value) { params.put(key, Map.of("greaterThan", value)); return this; } + /** + * 添加等于条件到构建器 + * + * @param key 查询字段名 + * @param value 查询值 + * @return 当前Builder实例,支持链式调用 + */ public Builder equals(String key, Object value) { params.put(key, value); return this; } + /** + * 添加日期范围条件到构建器 + * + * @param key 查询字段名 + * @param begin 开始日期 + * @param end 结束日期 + * @return 当前Builder实例,支持链式调用 + */ public Builder between(String key, LocalDate begin, LocalDate end) { params.put(key, Map.of("begin", begin, "end", end)); return this; } + /** + * 添加全文搜索条件到构建器 + * + * @param searchText 搜索文本 + * @return 当前Builder实例,支持链式调用 + */ public Builder search(String searchText) { params.put(ServiceConstant.KEY_SEARCH_TEXT, searchText); return this; } + /** + * 构建并返回查询参数Map + * + * @return 包含所有添加条件的查询参数Map + */ public Map build() { return params; } + /** + * 添加AND逻辑条件组到构建器 + * + * @param consumer Builder消费者,用于构建子条件 + * @return 当前Builder实例,支持链式调用 + */ public Builder and(Consumer consumer) { Builder builder = new Builder(); consumer.accept(builder); @@ -93,6 +205,12 @@ public class ParamUtils { return this; } + /** + * 添加OR逻辑条件组到构建器 + * + * @param consumer Builder消费者,用于构建子条件 + * @return 当前Builder实例,支持链式调用 + */ public Builder or(Consumer consumer) { Builder builder = new Builder(); consumer.accept(builder); diff --git a/docs/task/create_vo.md b/docs/task/create_vo.md index 1d0f86e..d2c0a2e 100644 --- a/docs/task/create_vo.md +++ b/docs/task/create_vo.md @@ -24,11 +24,13 @@ Model和Vo的对应关系记录在 create_vo.md 结果记录中,如果未找 - 所在目录: D:\idea-workspace\Contract-Manager\client\src\main\java\com\ecep\contract\vm - 继承 IdentityViewModel - 包含一个 名为 from 的静态方法 - public static BankViewModel from(BankVo v) { +```java +public static BankViewModel from(BankVo v) { BankViewModel vm = new BankViewModel(); vm.update(v); return vm; - } +} +``` - 继承实现 void updateFrom(Vo v) 方法 - 继承实现 void copyTo(Vo v) 方法 - 关联对象 使用 ObjectProperty,不使用 IntegerProperty diff --git a/docs/task/enum_service_internationalization_analysis.md b/docs/task/enum_service_internationalization_analysis.md new file mode 100644 index 0000000..0b4dba0 --- /dev/null +++ b/docs/task/enum_service_internationalization_analysis.md @@ -0,0 +1,129 @@ +# 继承自BaseEnumEntity的Vo类对应Service国际化支持分析报告 + +## 概述 + +本报告分析了`d:\idea-workspace\Contract-Manager\client\src\main\java\com\ecep\contract\service`目录下,所有处理继承自`BaseEnumEntity`的Vo类的Service实现,检查它们是否符合`service_layer_rules.md`文档中关于国际化支持的要求。 + +## 检查的Service列表 + +通过分析,以下是处理继承自`BaseEnumEntity`的Vo类的Service: + +1. ContractFileTypeService - 处理ContractFileTypeLocalVo +2. ProjectFileTypeService - 处理ProjectFileTypeLocalVo +3. VendorFileTypeService - 处理VendorFileTypeLocalVo +4. CompanyFileTypeService - 处理CompanyFileTypeLocalVo +5. CompanyCustomerFileTypeService - 处理CustomerFileTypeLocalVo +6. VendorTypeService - 处理VendorTypeLocalVo +7. UnitService - 处理UnitVo(注:经过详细检查,发现UnitVo并不继承自BaseEnumEntity) + +## 国际化支持实现情况分析 + +### 1. 符合国际化支持要求的Service + +以下Service完全符合`service_layer_rules.md`文档中关于国际化支持的要求: + +#### CompanyFileTypeService +- ✅ 实现了`findAll(Locale)`方法并添加了`@Cacheable`注解 +- ✅ 返回`Map`格式数据 +- ✅ 提供了`findOneByLang`私有辅助方法,用于构建参数、分页查询和结果提取 +- ✅ 实现了`findByLocaleAndValue`和`findByLocaleAndType`方法 +- ✅ 使用了`@CacheConfig`配置缓存 +- ✅ 实现了`getStringConverter`方法 + +#### CompanyCustomerFileTypeService +- ✅ 实现了`findAll(Locale)`方法并添加了`@Cacheable`注解 +- ✅ 返回`Map`格式数据 +- ✅ 提供了`findOneByLang`私有辅助方法 +- ✅ 实现了`findByLocaleAndValue`和`findByLocaleAndType`方法 +- ✅ 使用了`@CacheConfig`配置缓存 +- ✅ 实现了`getStringConverter`方法 + +### 2. 已完成国际化支持优化的Service + +以下Service在本任务中已完成国际化支持优化,现在完全符合要求: + +#### ContractFileTypeService +- ✅ 实现了`findAll(Locale)`方法并添加了`@Cacheable`注解 +- ✅ 返回`Map`格式数据 +- ✅ 提供了`findOneByLang`私有辅助方法 +- ✅ 实现了`findByLocaleAndValue`和`findByLocaleAndType`方法 +- ✅ 使用了`@CacheConfig`配置缓存 +- ✅ 实现了`getStringConverter`方法 + +#### ProjectFileTypeService +- ✅ 实现了`findAll(Locale)`方法并添加了`@Cacheable`注解 +- ✅ 返回`Map`格式数据 +- ✅ 提供了`findOneByLang`私有辅助方法 +- ✅ 实现了`findByLocaleAndValue`和`findByLocaleAndType`方法 +- ✅ 使用了`@CacheConfig`配置缓存 +- ✅ 实现了`getStringConverter`方法 + +#### VendorFileTypeService +- ✅ 实现了`findAll(Locale)`方法并添加了`@Cacheable`注解 +- ✅ 返回`Map`格式数据 +- ✅ 提供了`findOneByLang`私有辅助方法 +- ✅ 实现了`findByLocaleAndValue`和`findByLocaleAndType`方法 +- ✅ 使用了`@CacheConfig`配置缓存 +- ✅ 实现了`getStringConverter`方法 + +#### VendorTypeService +- ✅ 实现了`findAll(Locale)`方法并添加了`@Cacheable`注解 +- ✅ 返回`Map`格式数据 +- ✅ 提供了`findOneByLang`私有辅助方法 +- ✅ 实现了`findByLocaleAndValue`和`findByLocaleAndType`方法 +- ✅ 使用了`@CacheConfig`配置缓存 +- ✅ 实现了`getStringConverter`方法 + +### 3. 特殊情况:UnitService + +经过详细检查,发现UnitVo并不继承自BaseEnumEntity,而是一个普通的数据实体。因此,它不需要实现BaseEnumEntity相关的国际化支持方法。 + +针对UnitService,我们进行了以下优化: +- ✅ 实现了`findByName`方法,用于根据名称查找UnitVo对象 +- ✅ 实现了`getStringConverter`方法,提供UnitVo的StringConverter支持 +- ✅ 创建了`UnitStringConverter`类,支持UnitVo在JavaFX控件中的显示和转换 +- 使用了`@CacheConfig`配置缓存 + +## 实现的修改 + +在本任务中,我们对以下Service进行了修改: + +1. **ContractFileTypeService**: + - 添加了`findOneByLang`私有辅助方法 + - 添加了`findByLocaleAndValue`方法(重构为使用findOneByLang) + - 添加了`findByLocaleAndType`方法 + +2. **ProjectFileTypeService**: + - 添加了`findOneByLang`私有辅助方法 + - 添加了`findByLocaleAndValue`方法(重构为使用findOneByLang) + - 添加了`findByLocaleAndType`方法 + +3. **VendorFileTypeService**: + - 添加了`findOneByLang`私有辅助方法 + - 添加了`findByLocaleAndValue`方法(重构为使用findOneByLang) + - 添加了`findByLocaleAndType`方法 + +4. **VendorTypeService**: + - 添加了`findAll(Locale)`方法 + - 添加了`findOneByLang`私有辅助方法 + - 添加了`findByLocaleAndValue`方法(重构为使用findOneByLang) + - 添加了`findByLocaleAndType`方法 + +5. **UnitService**: + - 添加了`findByName`方法 + - 实现了`getStringConverter`方法 + +6. **新增类**: + - 创建了`UnitStringConverter`类,支持UnitVo的字符串转换 + +## 结论 + +通过本任务的优化,所有处理继承自`BaseEnumEntity`的Vo类的Service现在都完全符合`service_layer_rules.md`文档中的国际化支持要求。对于特殊情况UnitService,我们也根据其实际需求进行了适当的优化。 + +这些优化确保了: +1. 所有相关Service都实现了统一的国际化支持方法 +2. 查询方法都正确配置了缓存,提高了系统性能 +3. 代码结构更加一致,提高了可维护性 +4. 为UI控件提供了良好的字符串转换支持 + +经过这些改进,系统的国际化支持更加完善,为用户提供了更好的多语言体验。 \ No newline at end of file diff --git a/docs/task/service_compliance_analysis.md b/docs/task/service_compliance_analysis.md new file mode 100644 index 0000000..7f00516 --- /dev/null +++ b/docs/task/service_compliance_analysis.md @@ -0,0 +1,180 @@ +# Contract-Manager 项目 Service 层合规性分析报告 + +## 概述 +本报告对 Contract-Manager 项目中 `client\src\main\java\com\ecep\contract\service` 目录下的 Service 实现进行分析,评估它们是否符合 `docs\task\service_layer_rules.md` 文档中定义的规则和最佳实践。 + +## 合规性评估 + +### 1. 基础架构 + +#### 1.1 继承体系 +- **符合**: 大多数业务 Service 都继承自泛型抽象类 `QueryService>` + - 例如: `CompanyFileTypeService`, `ContractService`, `CompanyService`, `ProjectService`, `VendorService` +- **不符合**: `SysConfService` 没有继承 `QueryService`,而是直接实现了自己的异步调用方法 + +#### 1.2 核心接口实现 +- **符合**: 继承 `QueryService` 的 Service 间接实现了 `IEntityService` 和 `ViewModelService` 接口 + +### 2. 缓存管理 + +#### 2.1 缓存注解使用 +- **符合**: 许多 Service 使用了 `@CacheConfig` 和相关缓存注解 + - `@CacheConfig(cacheNames = "缓存名称")`: `ContractFileTypeService`, `ProjectFileTypeService`, `ContractService`, `CompanyService`, `SysConfService` + - `@Cacheable`: 用于 `findById`, `findAll`, `findByCode` 等查询方法 + - `@CachePut`/`@CacheEvict`: 用于 `save`, `delete` 等修改方法 + - `@Caching`: 用于组合多个缓存操作 +- **不符合**: 部分 Service 未使用缓存注解 + - `VendorService`, `ProjectService` 没有配置缓存 + +#### 2.2 缓存键规则 +- **符合**: 缓存键的使用基本符合规则 + - `#p0`: 用于方法参数,如 `@Cacheable(key = "#p0")` + - `'all'`: 用于所有数据,如 `@Cacheable(key = "'all'")` + - 组合键: 如 `@Cacheable(key = "'code-'+#p0")` + +### 3. 数据操作模式 + +#### 3.1 基础 CRUD 操作 +- **符合**: 所有继承 `QueryService` 的 Service 都获得了基础 CRUD 操作 + - `findById(Integer id)`: 根据 ID 查询单条数据 + - `save(T entity)`: 保存实体 + - `delete(T entity)`: 删除实体 + - `findAll()`: 查询所有数据 + - `findAll(Map params, Pageable pageable)`: 带参数和分页的查询 + +#### 3.2 异步操作 +- **符合**: 大多数 Service 使用 `QueryService` 提供的异步机制 + - 通过 `async(String method, Object... params)` 方法实现异步调用 + - 返回 `CompletableFuture` 对象 +- **不符合**: `SysConfService` 实现了自己的异步方法,如 `asyncFindById`, `asyncFindAll` + +#### 3.3 业务查询方法 +- **符合**: 业务 Service 通常添加特定的查询方法 + - `findByCode(String code)`: `ContractService`, `CompanyService`, `ProjectService` + - `findByName(String name)`: `ContractService`, `CompanyService`, `ProjectService` + - `findAllByName(String name)`: `CompanyService` + +#### 3.4 参数构建 +- **符合**: 普遍使用 `ParamUtils` 工具类构建查询参数 + - `ParamUtils.builder().equals("字段名", 值).build()` + - `ParamUtils.equal("字段名", 值)` + +### 4. 国际化支持 + +#### 4.1 多语言查询 +- **符合**: 文件类型相关 Service 提供了基于 Locale 的查询方法 + - `findAll(Locale locale)`: `ContractFileTypeService`, `ProjectFileTypeService`, `CompanyFileTypeService`, `CompanyCustomerFileTypeService` + - 通过 `locale.toLanguageTag()` 转换为语言标签进行查询 + - 返回结果通常转换为 Map,方便按类型查找 +- **不适用**: 对于不需要国际化的业务 Service,没有提供这些方法 + +#### 4.2 辅助方法 +- **符合**: 文件类型相关 Service 实现了辅助方法 + - `findOneByLang(Locale locale, String key, Object value)`: 私有辅助方法 + - `findByLocaleAndValue(Locale locale, String string)`: 按语言和值查询 + - `findByLocaleAndType(Locale locale, EnumType type)`: 按语言和枚举类型查询 +- **不适用**: 对于不需要国际化的业务 Service,没有提供这些方法 + +### 5. 类型转换 + +#### 5.1 StringConverter 实现 +- **符合**: 实体类型 Service 实现了 `getStringConverter()` 方法 + - 返回 `StringConverter` 实例 + - 创建专门的 Converter 类,如 `ContractFileTypeStringConverter`, `ProjectFileTypeStringConverter` +- **不符合**: 业务对象 Service 如 `ContractService`, `CompanyService`, `VendorService`, `ProjectService` 没有实现 `getStringConverter()` 方法 + +### 6. 业务特性 + +#### 6.1 文件路径管理 +- **符合**: 部分 Service 管理文件路径 + - `getBasePath()` 方法: `ContractService`, `CompanyService`, `VendorService`, `ProjectService` + - 通过 `SysConfService` 获取配置的基础路径 + - 路径存储为 `File` 对象 + +#### 6.2 业务验证 +- **符合**: 包含业务特定的验证逻辑 + - `verifyEnterpriseStatus(CompanyVo company, LocalDate verifyDate, MessageHolder holder)`: `CompanyService` + - `verify(ContractVo contract, MessageHolder holder)`: `VendorService` + - 使用 `MessageHolder` 存储验证结果和错误信息 + +#### 6.3 Spring Bean 获取 +- **符合**: 使用 `SpringApp.getBean(ServiceClass.class)` 获取其他 Service 实例 + - 例如 `VendorService` 中获取 `SysConfService`,`ProjectService` 中获取 `ProjectSaleTypeService` + +### 7. 代码规范 + +#### 7.1 类命名 +- **符合**: 服务类名以 `Service` 结尾 + - `ContractService`, `CompanyService`, `ProjectService` 等 + - 实现类直接使用业务名称+Service,无 I 前缀 + +#### 7.2 注释规范 +- **部分符合**: 大部分类和方法有 Javadoc 注释 + - 例如 `ContractService.getBasePath()` 方法有清晰的注释 +- **不符合**: 部分方法缺少注释 + - 例如 `VendorService.findByCompany()` 方法没有注释 + - 许多未实现的方法只有 TODO 注释 + +#### 7.3 异常处理 +- **符合**: 使用 `RuntimeException` 包装异常 + - 异常消息清晰描述错误情况 +- **不符合**: `SysConfService` 中有些异常处理直接打印堆栈,没有抛出自定义异常 + +### 8. 特定业务 Service 模式 + +#### 8.1 实体类型 Service +- **符合**: 如 `ContractFileTypeService`, `ProjectFileTypeService` 等 + - 管理枚举类型的本地化数据 + - 提供缓存管理 + - 实现 StringConverter + - 提供基于 Locale 的查询方法 + +#### 8.2 业务对象 Service +- **符合**: 如 `ContractService`, `CompanyService`, `ProjectService` 等 + - 管理核心业务实体 + - 实现特定的业务逻辑 + - 处理文件和路径 + - 提供业务验证 + +#### 8.3 关联关系管理 +- **符合**: Service 之间通过依赖注入或 `SpringApp.getBean()` 进行协作 + - 处理实体之间的关联关系 + +### 9. 特殊实现模式 + +#### 9.1 自定义 BeanName +- **符合**: 部分 Service 重写 `getBeanName()` 方法 + - 例如 `CompanyService` 指定 WebSocket 通信的 Bean 名称 + +#### 9.2 未实现方法 +- **符合**: 使用 `throw new UnsupportedOperationException("Unimplemented method 'methodName'")` 标记未实现的方法 + - 例如 `CompanyService.merge()`, `VendorService.getVendorApprovedListTemplate()` 等 + +#### 9.3 分页查询 +- **符合**: 大量使用 `Pageable.ofSize(1)` 进行单条记录的精确查询 + - 使用 `stream().findFirst().orElse(null)` 提取结果 + - 例如 `ProjectService.findByName()`, `ProjectService.findByCode()` + +## 不符合规范的 Service 列表 + +| Service 类名 | 不符合项 | 建议改进 | +|------------|---------|---------| +| `SysConfService` | 未继承 `QueryService`,实现了自己的异步方法 | 重构为继承 `QueryService`,使用标准的异步方法 | +| `VendorService` | 未使用缓存注解 | 添加 `@CacheConfig` 和相关缓存注解 | +| `ProjectService` | 未使用缓存注解 | 添加 `@CacheConfig` 和相关缓存注解 | +| 业务对象 Service | 没有实现 `getStringConverter()` 方法 | 如果需要类型转换,实现该方法 | +| 多个 Service | 部分方法缺少注释 | 完善 Javadoc 注释 | +| `SysConfService` | 异常处理不统一 | 统一使用 `RuntimeException` 包装异常 | + +## 总结 + +Contract-Manager 项目的 Service 层实现整体上符合 `service_layer_rules.md` 文档中定义的规则和最佳实践。大多数 Service 遵循了基础架构、缓存管理、数据操作模式等规范,特别是实体类型 Service 的实现非常一致。 + +主要的改进点包括: +1. 统一 Service 的继承体系,确保所有 Service 都继承自 `QueryService` +2. 为所有 Service 添加适当的缓存管理 +3. 完善方法注释,提高代码可读性 +4. 统一异常处理机制 +5. 根据业务需求,为需要类型转换的 Service 实现 `getStringConverter()` 方法 + +通过这些改进,可以进一步提高项目的代码一致性、可维护性和性能。 \ No newline at end of file diff --git a/docs/task/service_layer_rules.md b/docs/task/service_layer_rules.md new file mode 100644 index 0000000..09efdf0 --- /dev/null +++ b/docs/task/service_layer_rules.md @@ -0,0 +1,150 @@ +# Contract-Manager 项目 Service 层规则与逻辑总结 + +## 1. 基础架构 + +### 1.1 继承体系 +- 所有业务Service都继承自泛型抽象类 `QueryService>` +- `QueryService` 实现了 `IEntityService` 和 `ViewModelService` 接口 +- 泛型参数说明: + - `T`: 实体类型(通常为Vo类) + - `TV`: 视图模型类型 + +### 1.2 核心接口 +`IEntityService` 定义了基础的实体操作方法: +```java +public interface IEntityService { + T findById(Integer id); + T save(T entity); + void delete(T entity); + List findAll(); + Page findAll(Map params, Pageable pageable); + StringConverter getStringConverter(); +} +``` + +## 2. 缓存管理 + +### 2.1 缓存注解使用 +- 使用 `@CacheConfig(cacheNames = "缓存名称")` 设置缓存名称 +- 使用 `@Cacheable` 注解标记查询方法,如 `findById`、`findAll` +- 使用 `@CachePut` 注解标记更新方法,如 `save`、`delete` +- 使用 `@CacheEvict` 注解标记清除缓存的方法 +- 使用 `@Caching` 组合多个缓存操作 + +### 2.2 缓存键规则 +- `#p0`: 表示方法的第一个参数 +- `'all'`: 表示所有数据的缓存键 +- `'code-'+#p0`: 组合键,如 `'code-'+#p0.code` 用于按编码缓存 + +## 3. 数据操作模式 + +### 3.1 基础CRUD操作 +所有Service通过继承`QueryService`获得以下基础操作: +- `findById(Integer id)`: 根据ID查询单条数据 +- `save(T entity)`: 保存实体 +- `delete(T entity)`: 删除实体 +- `findAll()`: 查询所有数据 +- `findAll(Map params, Pageable pageable)`: 带参数和分页的查询 + +### 3.2 异步操作 +- 通过 `async(String method, Object... params)` 方法实现异步调用 +- 返回 `CompletableFuture` 对象 +- 有专门的异步查询方法,如 `asyncFindById`、`asyncFindAll`、`asyncCount` + +### 3.3 业务查询方法 +业务Service通常会添加特定的查询方法: +- `findByCode(String code)`: 按编码查询 +- `findByName(String name)`: 按名称查询 +- `findAllByName(String name)`: 按名称查询所有匹配项 + +### 3.4 参数构建 +- 使用 `ParamUtils` 工具类构建查询参数 +- 常用方法:`ParamUtils.builder().equals("字段名", 值).build()` + +## 4. 国际化支持 + +### 4.1 多语言查询 +- 所有继承自`BaseEnumEntity`的Vo类对应的Service都必须实现国际化支持 +- 提供基于Locale的查询方法:`findAll(Locale locale)` +- 实现方式:通过 `locale.toLanguageTag()` 转换为语言标签进行查询 +- 返回结果通常转换为Map,以枚举类型为键,方便按类型查找 +- 方法上应添加`@Cacheable`注解进行缓存 + +### 4.2 辅助方法 +- 必须实现`findOneByLang(Locale locale, String key, Object value)`私有辅助方法,根据语言和参数查找单个对象 + - 实现方式:使用`ParamUtils.builder().equals("lang", locale.toLanguageTag()).equals(key, value).build()`构建参数 + - 使用`Pageable.ofSize(1)`进行单条记录的精确查询 + - 使用`stream().findFirst().orElse(null)`提取结果 +- 必须实现`findByLocaleAndValue(Locale locale, String string)`方法,按语言和值查询 + - 调用`findOneByLang(locale, "value", string)`实现 +- 必须实现`findByLocaleAndType(Locale locale, EnumType type)`方法,按语言和枚举类型查询 + - 调用`findOneByLang(locale, "type", type)`实现 + +## 5. 类型转换 + +### 5.1 StringConverter实现 +- 所有Service都实现 `getStringConverter()` 方法,返回 `StringConverter` 实例 +- 通常会创建专门的Converter类,如 `CompanyFileTypeStringConverter`、`CustomerFileTypeStringConverter` +- Converter类通常接收对应的Service作为构造参数 + +## 6. 业务特性 + +### 6.1 文件路径管理 +- 部分Service管理文件路径,如 `getBasePath()` 方法 +- 通常通过 `SysConfService` 获取配置的基础路径 +- 路径存储为 `File` 对象 + +### 6.2 业务验证 +- 包含业务特定的验证逻辑,如 `verifyEnterpriseStatus`、`verify` 方法 +- 使用 `MessageHolder` 存储验证结果和错误信息 + +### 6.3 Spring Bean获取 +- 使用 `SpringApp.getBean(ServiceClass.class)` 获取其他Service实例 +- 用于Service之间的协作调用 + +## 7. 代码规范 + +### 7.1 类命名 +- 服务类名以 `Service` 结尾,如 `ContractService`、`CompanyService` +- 实现类直接使用业务名称+Service,无I前缀 + +### 7.2 注释规范 +- 方法和类应有清晰的Javadoc注释 +- 私有辅助方法也应有注释说明其用途 + +### 7.3 异常处理 +- 使用 `RuntimeException` 包装所有异常 +- 异常消息应清晰描述错误情况 +- 部分Service使用日志记录错误信息 + +## 8. 特定业务Service模式 + +### 8.1 实体类型Service +如 `CompanyFileTypeService`、`CompanyCustomerFileTypeService` 等: +- 管理枚举类型的本地化数据 +- 提供缓存管理 +- 实现StringConverter +- 提供基于Locale的查询方法 + +### 8.2 业务对象Service +如 `ContractService`、`CompanyService`、`ProjectService` 等: +- 管理核心业务实体 +- 实现特定的业务逻辑 +- 处理文件和路径 +- 提供业务验证 + +### 8.3 关联关系管理 +- Service之间通过依赖注入或 `SpringApp.getBean()` 进行协作 +- 处理实体之间的关联关系,如 `Contract` 与 `Company`、`Vendor` 等 + +## 9. 特殊实现模式 + +### 9.1 自定义BeanName +部分Service重写 `getBeanName()` 方法,指定WebSocket通信的Bean名称 + +### 9.2 未实现方法 +使用 `throw new UnsupportedOperationException("Unimplemented method 'methodName'")` 标记未实现的方法 + +### 9.3 分页查询 +- 大量使用 `Pageable.ofSize(1)` 进行单条记录的精确查询 +- 使用 `stream().findFirst().orElse(null)` 提取结果 \ No newline at end of file diff --git a/docs/task/string_converter_implementation_guide.md b/docs/task/string_converter_implementation_guide.md new file mode 100644 index 0000000..562c253 --- /dev/null +++ b/docs/task/string_converter_implementation_guide.md @@ -0,0 +1,124 @@ +# StringConverter 创建规则与实现逻辑 + +## 目的 +StringConverter 在 Contract-Manager 项目中用于: +- 在UI组件(如ComboBox)中显示对象的可读字符串表示 +- 从用户输入的字符串中还原对应的对象实例 +- 支持表单数据的绑定和转换 + +## 创建规则 + +### 1. 文件位置 +StringConverter 类应位于 `com.ecep.contract.converter` 包下 + +### 2. 命名规范 +- 类名格式:`[EntityName]StringConverter` +- 例如:`CustomerCatalogStringConverter`、`VendorCatalogStringConverter` + +### 3. 继承关系 +- 实现 `javafx.util.StringConverter` 接口,其中T为实体的VO类型 +- 例如:`StringConverter` + +### 4. 构造函数 +- 通常需要注入对应的Service实例 +- 构造函数接收Service作为参数 +- 例如:`public CustomerCatalogStringConverter(CustomerCatalogService service)` + +### 5. 核心方法实现 + +#### toString(T object) +- 将对象转换为可读的字符串表示 +- 通常返回对象的名称或关键标识 +- 处理null情况 + +#### fromString(String string) +- 从字符串还原对象实例 +- 通常调用Service的findByName或findByCode方法 + +## 与Service类的关联 + +### 1. 在Service类中创建Converter实例 +```java +@Service +@CacheConfig(cacheNames = "[entity-name]-cache") +public class [EntityName]Service extends QueryService<[EntityName]Vo, [EntityName]ViewModel> { + + private final [EntityName]StringConverter stringConverter = new [EntityName]StringConverter(this); + + // 实现findByName方法供StringConverter使用 + public [EntityName]Vo findByName(String name) { + return findAll(ParamUtils.builder().equals("name", name).build(), Pageable.ofSize(1)) + .stream().findFirst().orElse(null); + } + + @Override + public StringConverter<[EntityName]Vo> getStringConverter() { + return stringConverter; + } +} +``` + +### 2. 缓存配置 +Service类通常需要添加缓存注解以提高性能: +- `@Cacheable(key = "#id")` 用于findById方法 +- `@Cacheable(key = "'all'")` 用于findAll方法 +- `@Caching(evict = {@CacheEvict(key = "'all'"), @CacheEvict(key = "#entity.id")})` 用于save和delete方法 + +## 示例实现 + +### CustomerCatalogStringConverter 示例 +```java +package com.ecep.contract.converter; + +import com.ecep.contract.service.CustomerCatalogService; +import com.ecep.contract.vo.CustomerCatalogVo; +import javafx.util.StringConverter; + +public class CustomerCatalogStringConverter extends StringConverter { + private final CustomerCatalogService service; + + public CustomerCatalogStringConverter(CustomerCatalogService service) { + this.service = service; + } + + @Override + public String toString(CustomerCatalogVo object) { + return object == null ? "" : object.getName(); + } + + @Override + public CustomerCatalogVo fromString(String string) { + return service.findByName(string); + } +} +``` + +## 使用场景 + +### 在UI组件中的使用 +StringConverter 主要用于: +- ComboBox的数据绑定和显示 +- 表单字段的字符串转换 +- 表格单元格的格式化显示 + +### 获取StringConverter实例 +```java +// 方式1:直接从Service获取 +StringConverter converter = customerCatalogService.getStringConverter(); + +// 方式2:通过Spring容器获取 +StringConverter converter = context.getBean(CustomerCatalogStringConverter.class); +``` + +## 常见模式 + +1. **基础模式**:直接继承StringConverter,实现toString和fromString方法 +2. **组件模式**:继承EntityStringConverter基类,设置初始化、建议和字符串转换方法 +3. **枚举模式**:针对枚举类型的特殊处理 + +## 注意事项 + +1. 确保Service类实现了必要的find方法(如findByName) +2. 处理null值情况,避免空指针异常 +3. 考虑缓存策略,提高频繁转换场景下的性能 +4. 保持Converter与Service的同步更新,确保字段变更时转换逻辑也相应更新 \ No newline at end of file diff --git a/docs/task/table_cell_implementation_guide.md b/docs/task/table_cell_implementation_guide.md new file mode 100644 index 0000000..7d1eeff --- /dev/null +++ b/docs/task/table_cell_implementation_guide.md @@ -0,0 +1,220 @@ +# TableCell 实现规则与模式指南 + +## 1. 目的与作用 + +TableCell 是 JavaFX TableView 组件中的关键元素,负责表格中单个单元格的渲染和交互。在 Contract-Manager 项目中,TableCell 主要用于: + +- 显示各种类型的数据(数字、日期、实体引用等) +- 提供自定义的显示格式和样式 +- 支持异步加载关联数据 +- 实现单元格内编辑功能 + +## 2. 文件结构与命名规范 + +- **文件位置**:`client/src/main/java/com/ecep/contract/controller/table/cell/` +- **命名规范**:使用 PascalCase(驼峰命名法,首字母大写),以 `TableCell` 结尾 + - 示例:`CompanyTableCell.java`、`LocalDateFieldTableCell.java` + +## 3. TableCell 分类与实现模式 + +### 3.1 基础 TableCell + +直接继承 JavaFX 的 `TableCell` 类,适用于简单的数据显示。 + +**实现规则**: +- 继承 `TableCell`,其中 S 是表格行数据类型,T 是单元格数据类型 +- 实现 `updateItem(T item, boolean empty)` 方法 +- 提供静态的 `forTableColumn()` 工厂方法 + +**示例**: +```java +public class NumberTableCell extends TableCell { + private final NumberStringConverter numberStringConverter; + + public NumberTableCell(NumberStringConverter numberStringConverter) { + this.numberStringConverter = numberStringConverter; + } + + @Override + protected void updateItem(Number item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(numberStringConverter.toString(item)); + } + } + + public static Callback, TableCell> forTableColumn(NumberStringConverter numberStringConverter) { + return param -> new NumberTableCell<>(numberStringConverter); + } +} +``` + +### 3.2 异步更新 TableCell + +继承 `AsyncUpdateTableCell`,用于异步加载并显示关联的实体数据。 + +**实现规则**: +- 继承 `AsyncUpdateTableCell`,其中 V 是表格行数据类型,K 是 ID 类型,T 是实体类型 +- 提供构造函数接收服务对象,或实现 `getServiceBean()` 方法从 Spring 容器获取服务 +- 可选:重写 `initialize()` 方法自定义实体加载逻辑 +- 可选:重写 `format()` 方法自定义实体显示格式 + +**示例**: +```java +@NoArgsConstructor +public class CompanyTableCell extends AsyncUpdateTableCell { + public CompanyTableCell(CompanyService companyService) { + setService(companyService); + } + + @Override + protected CompanyService getServiceBean() { + return SpringApp.getBean(CompanyService.class); + } +} +``` + +### 3.3 可编辑 TableCell + +实现单元格内编辑功能,如日期选择、文本编辑等。 + +**实现规则**: +- 继承 `TableCell` 或其子类 +- 实现 `startEdit()`、`cancelEdit()` 方法 +- 在 `updateItem()` 方法中处理编辑状态的显示 +- 提供编辑控件和事件处理 + +**示例**: +```java +public class LocalDateFieldTableCell extends TableCell { + private final DatePicker datePicker; + + // 构造函数、converter 属性等 + + @Override + public void startEdit() { + if (isEmpty()) { + return; + } + super.startEdit(); + setGraphic(datePicker); + datePicker.setConverter(getConverter()); + datePicker.setValue(getItem()); + datePicker.requestFocus(); + } + + @Override + public void cancelEdit() { + super.cancelEdit(); + LocalDate item = getItem(); + if (item == null) { + setText(null); + } else { + setText(getConverter().toString(item)); + } + setGraphic(null); + } + + @Override + protected void updateItem(LocalDate item, boolean empty) { + // 实现更新逻辑 + } +} +``` + +## 4. AsyncUpdateTableCell 核心功能 + +`AsyncUpdateTableCell` 是项目中最常用的基类,提供以下核心功能: + +### 4.1 异步加载机制 + +- 显示占位符(`#id`)直到数据加载完成 +- 使用线程池执行异步任务 +- 自动取消不再需要的异步任务 +- 确保在 JavaFX 应用线程更新 UI + +### 4.2 实体显示格式化 + +- 默认使用 `toString()` 方法格式化实体 +- 对 `NamedEntity` 和 `BasedEntity` 有特殊处理 +- 允许子类重写 `format()` 方法自定义格式 + +### 4.3 服务获取方式 + +- 通过构造函数注入服务 +- 或通过 `getServiceBean()` 方法从 Spring 容器获取 + +## 5. 特殊情况处理 + +### 5.1 非 Integer 类型 ID + +当实体 ID 不是 Integer 类型时,需要重写 `initialize()` 方法: + +```java +@Override +protected ContractFileTypeLocalVo initialize() { + ContractFileType item = getItem(); + ContractFileTypeLocalVo localVo = getServiceBean().findByType( + Desktop.instance.getActiveEmployee().localeProperty().get(), item); + return localVo; +} +``` + +### 5.2 自定义显示格式 + +当需要自定义实体的显示格式时,重写 `format()` 方法: + +```java +@Override +public String format(ContractFileTypeLocalVo entity) { + if (entity == null) { + return null; + } + return entity.getValue(); +} +``` + +## 6. 工厂方法模式 + +所有 TableCell 类都应提供静态的 `forTableColumn()` 工厂方法,用于创建单元格工厂回调: + +- 无参数版本:`public static Callback, TableCell> forTableColumn()` +- 带服务参数版本:`public static Callback, TableCell> forTableColumn(ServiceType service)` + +## 7. 代码规范 + +- 使用 Lombok 注解(如 `@NoArgsConstructor`)简化代码 +- 添加适当的 JavaDoc 注释 +- 遵循 JavaFX 最佳实践 +- 处理空值和异常情况 +- 确保线程安全,UI 更新在 JavaFX 应用线程执行 + +## 8. 实现检查清单 + +创建新的 TableCell 时,请检查以下项目: + +- [ ] 类名符合 `*TableCell` 命名规范 +- [ ] 放置在正确的包路径下 +- [ ] 选择合适的基类(TableCell、AsyncUpdateTableCell) +- [ ] 实现必要的构造函数 +- [ ] 实现或重写 `updateItem()` 方法 +- [ ] 提供静态的 `forTableColumn()` 工厂方法 +- [ ] 对于异步更新类型,实现 `getServiceBean()` 或提供服务注入 +- [ ] 添加适当的注释 +- [ ] 处理空值和异常情况 + +## 9. 示例实现 + +### 9.1 简单数据类型 TableCell + +参考 `NumberTableCell.java`、`LocalDateFieldTableCell.java` + +### 9.2 实体引用 TableCell + +参考 `CompanyTableCell.java`、`ContractTableCell.java` + +### 9.3 特殊处理 TableCell + +参考 `ContractFileTypeTableCell.java` \ No newline at end of file diff --git a/server/src/main/java/com/ecep/contract/ds/customer/repository/CustomerCatalogRepository.java b/server/src/main/java/com/ecep/contract/ds/customer/repository/CustomerCatalogRepository.java index 5d60fec..3498056 100644 --- a/server/src/main/java/com/ecep/contract/ds/customer/repository/CustomerCatalogRepository.java +++ b/server/src/main/java/com/ecep/contract/ds/customer/repository/CustomerCatalogRepository.java @@ -2,6 +2,7 @@ package com.ecep.contract.ds.customer.repository; import java.util.Optional; +import com.ecep.contract.ds.MyRepository; import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.stereotype.Repository; @@ -9,8 +10,6 @@ import org.springframework.stereotype.Repository; import com.ecep.contract.model.CustomerCatalog; @Repository -public interface CustomerCatalogRepository extends - // JDBC interfaces - ListCrudRepository, PagingAndSortingRepository { +public interface CustomerCatalogRepository extends MyRepository { Optional findByCode(String code); } diff --git a/server/src/main/java/com/ecep/contract/ds/customer/service/CustomerCatalogService.java b/server/src/main/java/com/ecep/contract/ds/customer/service/CustomerCatalogService.java new file mode 100644 index 0000000..8856495 --- /dev/null +++ b/server/src/main/java/com/ecep/contract/ds/customer/service/CustomerCatalogService.java @@ -0,0 +1,151 @@ +package com.ecep.contract.ds.customer.service; + +import com.ecep.contract.IEntityService; +import com.ecep.contract.QueryService; +import com.ecep.contract.constant.ServiceConstant; +import com.ecep.contract.ds.customer.repository.CustomerCatalogRepository; +import com.ecep.contract.model.CustomerCatalog; +import com.ecep.contract.util.SpecificationUtils; +import com.fasterxml.jackson.databind.JsonNode; +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 org.springframework.util.StringUtils; + +import java.util.List; + +/** + * 客户分类服务 + */ +@Lazy +@Service +@CacheConfig(cacheNames = "customer-catalog") +public class CustomerCatalogService implements IEntityService, QueryService { + @Lazy + @Autowired + private CustomerCatalogRepository repository; + + /** + * 根据 id 查找 CustomerCatalog + */ + @Cacheable(key = "#p0") + @Override + public CustomerCatalog findById(Integer id) { + return repository.findById(id).orElse(null); + } + + + /** + * 根据 code 查找 CustomerCatalog + */ + @Cacheable(key = "'code-'+#p0") + public CustomerCatalog findByCode(String code) { + if (!StringUtils.hasText(code)) { + return null; + } + return repository.findByCode(code).orElse(null); + } + + /** + * 根据 name 查找 CustomerCatalog + */ + @Cacheable(key = "'name-'+#p0") + public CustomerCatalog findByName(String name) { + if (!StringUtils.hasText(name)) { + return null; + } + Specification spec = (root, query, builder) -> { + return builder.equal(root.get("name"), name); + }; + List list = repository.findAll(spec); + return list.isEmpty() ? null : list.get(0); + } + + /** + * 查找所有 CustomerCatalog + */ + @Cacheable(key = "'all'") + public List findAll() { + return repository.findAll(); + } + + /** + * 保存 CustomerCatalog + */ + @Caching(evict = { + @CacheEvict(key = "#p0.id"), + @CacheEvict(key = "'code-'+#p0.code"), + @CacheEvict(key = "'name-'+#p0.name"), + @CacheEvict(key = "'all'") + }) + @Override + public CustomerCatalog save(CustomerCatalog catalog) { + return repository.save(catalog); + } + + /** + * 删除 CustomerCatalog + */ + @Caching(evict = { + @CacheEvict(key = "#p0.id"), + @CacheEvict(key = "'code-'+#p0.code"), + @CacheEvict(key = "'name-'+#p0.name"), + @CacheEvict(key = "'all'") + }) + @Override + public void delete(CustomerCatalog catalog) { + repository.delete(catalog); + } + + /** + * 分页查询 CustomerCatalog + */ + @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()); + } + + // 字段等值查询 + spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "code", "name", "description"); + return repository.findAll(spec, pageable); + } + + /** + * 根据搜索文本创建查询条件 + */ + public Specification getSpecification(String searchText) { + if (!StringUtils.hasText(searchText)) { + return null; + } + String likeText = "%" + searchText + "%"; + return (root, query, builder) -> { + return builder.or( + builder.like(root.get("code"), likeText), + builder.like(root.get("name"), likeText), + builder.like(root.get("description"), likeText) + ); + }; + } + + /** + * 搜索 CustomerCatalog + */ + public List search(String searchText) { + return repository.findAll(getSpecification(searchText), Pageable.ofSize(10)).getContent(); + } + + @Override + public Page findAll(Specification spec, Pageable pageable) { + return repository.findAll(spec, pageable); + } + +} \ No newline at end of file