# Client Service 实现编写指南 ## 📋 概述 本指南总结 Client 模块 Service 层的实现经验,用于指导后续 Service 的编写。本指南基于 Contract-Manager 项目中已实现的 Service 模式整理。 --- ## 🏗️ 基础架构 ### 1. 继承层次结构 ```java // Service 接口定义 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(); } // 基础 Service 实现 public abstract class QueryService> implements ViewModelService { // 核心实现 } // 具体业务 Service @Service @CacheConfig(cacheNames = "business") public class XxxService extends QueryService { // 业务特定实现 } ``` ### 2. 核心特性 - **泛型支持**:`QueryService` 使用泛型处理不同类型的 Vo 和 ViewModel - **WebSocket 通信**:通过 `WebSocketClientService` 与 Server 端通信 - **异步处理**:使用 `CompletableFuture` 实现异步操作 - **缓存机制**:集成 Spring Cache 支持多级缓存 - **错误处理**:统一的异常处理和日志记录 --- ## 📝 Service 编写规范 ### 1. 类声明和注解 ```java @Service // Spring 组件注解 @CacheConfig(cacheNames = "xxx") // 缓存配置,xxx为业务域名称 public class XxxService extends QueryService { // Service 实现 } ``` ### 2. 缓存策略 #### 缓存注解使用 ```java @Cacheable(key = "#p0") // 按ID缓存 @Cacheable(key = "'code-'+#p0") // 按代码缓存 @Cacheable(key = "'name-'+#p0") // 按名称缓存 @CacheEvict(key = "#p0.id") // 删除时清除ID缓存 @CacheEvict(key = "'code-'+#p0.code") // 清除代码缓存 @Caching(evict = { @CacheEvict(key = "#p0.id"), @CacheEvict(key = "'code-'+#p0.code") }) // 批量清除缓存 ``` #### 缓存键设计原则 - **ID 缓存**:`#p0`(第一个参数,通常是ID) - **代码缓存**:`'code-'+#p0`(业务代码的缓存) - **名称缓存**:`'name-'+#p0`(业务名称的缓存) - **关联缓存**:`'company-'+#p0.id`(关联实体的缓存) ### 3. 核心方法实现 #### findById 方法 ```java @Cacheable(key = "#p0") @Override public XxxVo findById(Integer id) { return super.findById(id); // 调用父类方法 } ``` #### save 方法(带缓存清除) ```java @Caching(evict = { @CacheEvict(key = "#p0.id"), @CacheEvict(key = "'code-'+#p0.code") }) @Override public XxxVo save(XxxVo entity) { return super.save(entity); } ``` #### delete 方法(带缓存清除) ```java @Caching(evict = { @CacheEvict(key = "#p0.id"), @CacheEvict(key = "'code-'+#p0.code") }) @Override public void delete(XxxVo entity) { super.delete(entity); } ``` --- ## 🔄 异步通信模式 ### 1. 基本异步调用 ```java // 异步调用示例 public XxxVo findByCode(String code) { try { return async("findByCode", code, String.class).handle((response, ex) -> { if (ex != null) { throw new RuntimeException("远程方法 findByCode 调用失败", ex); } if (response != null) { return updateValue(createNewEntity(), response); } return null; }).get(); } catch (Exception e) { throw new RuntimeException("查找实体失败: " + code, e); } } ``` ### 2. 复杂对象处理 ```java public List findDetailsByXxxId(Integer xxxId) { try { return async("findDetailsByXxxId", List.of(xxxId), List.of(Integer.class)) .handle((response, ex) -> { if (ex != null) { throw new RuntimeException("远程方法调用失败", ex); } if (response != null) { try { List content = new ArrayList<>(); for (JsonNode node : response) { XxxDetailVo newEntity = new XxxDetailVo(); objectMapper.updateValue(newEntity, node); content.add(newEntity); } return content; } catch (Exception e) { throw new RuntimeException(response.toString(), e); } } return null; }).get(); } catch (Exception e) { throw new RuntimeException(e); } } ``` --- ## 💼 业务逻辑模式 ### 1. 文件系统集成 #### 路径管理 ```java @Autowired private SysConfService confService; private File basePath; public File getBasePath() { if (basePath == null) { basePath = new File(confService.getString(Constant.KEY_BASE_PATH)); } return basePath; } // 验证路径是否在基础目录内 public boolean checkXxxPathInBasePath(XxxVo xxx) { if (!existsXxxPath(xxx)) { return false; } File basePath = getBasePath(); if (basePath == null || !basePath.exists()) { throw new IllegalArgumentException("基础目录不存在"); } File path = new File(xxx.getPath()); return path.getAbsolutePath().startsWith(basePath.getAbsolutePath()); } // 检查路径是否存在 public boolean existsXxxPath(XxxVo xxx) { if (!StringUtils.hasText(xxx.getPath())) { return false; } File path = new File(xxx.getPath()); return path.exists(); } ``` #### 目录创建 ```java public File makePath(XxxVo xxx) { File basePath = getBasePath(); if (!basePath.exists()) { holder.error("存储目录不存在:" + basePath.getAbsolutePath()); return null; } // 构建目录路径逻辑 String fileName = FileUtils.escapeFileName(xxx.getName()); File dir = new File(basePath, fileName); if (!dir.exists()) { if (!dir.mkdir()) { holder.error("创建目录失败:" + dir.getAbsolutePath()); return null; } } return dir; } ``` ### 2. 业务验证模式 #### 数据完整性验证 ```java public void verifyXxx(XxxVo xxx, LocalDate verifyDate, MessageHolder holder) { // 检查关键字段 if (!StringUtils.hasText(xxx.getCode())) { holder.error("编号异常:未设置"); return; } // 检查状态字段 String status = xxx.getStatus(); if (StringUtils.hasText(status) && status.contains("无效")) { LocalDate end = xxx.getEndDate(); LocalDate begin = xxx.getBeginDate(); if (begin == null || end == null) { holder.error("状态异常:" + status); } else { if (!MyDateTimeUtils.dateValidFilter(verifyDate, begin, end, 0)) { holder.error("状态异常:" + status); } } } else { holder.error("状态异常:未设置"); } } ``` #### 关联实体验证 ```java public boolean verifyAsXxxType(XxxVo xxx, LocalDate verifyDate, MessageHolder holder) { boolean valid = false; // 检查关联实体 RelatedVo related = relatedService.findById(xxx.getRelatedId()); if (related == null) { holder.error("关联实体不存在"); valid = true; } // 检查关联数据 if (!StringUtils.hasText(xxx.getRelatedField())) { holder.error("关联字段未设置"); valid = true; } // 检查业务规则 if (xxx.getStatus() == XxxStatus.INACTIVE) { holder.error("业务状态异常:已停用"); valid = true; } return valid; } ``` ### 3. 复杂业务查询 #### 分页查询 ```java public List findAllByXxxCondition(XxxCondition condition, LocalDate beginDate, LocalDate endDate) { return findAll(ParamUtils.builder() .equals("field1", condition.getField1()) .between("createDate", beginDate, endDate) .equals("status", "ACTIVE") .build(), Pageable.unpaged()).getContent(); } ``` #### 组合条件查询 ```java public Page findAllWithComplexCondition(XxxQueryParam param) { ParamUtils.ParamBuilder builder = ParamUtils.builder() .equals("category", param.getCategory()); if (StringUtils.hasText(param.getName())) { builder.like("name", "%" + param.getName() + "%"); } if (param.getDateRange() != null) { builder.between("createDate", param.getDateRange().getStart(), param.getDateRange().getEnd()); } return findAll(builder.build(), Pageable.ofSize(param.getPageSize())); } ``` --- ## 🔧 工具类和依赖注入 ### 1. 常用工具类依赖 ```java // 常用注入 @Autowired private SysConfService confService; // 系统配置服务 @Autowired private RelatedService relatedService; // 关联实体服务 // 静态工具类使用 import com.ecep.contract.util.FileUtils; // 文件工具 import com.ecep.contract.util.ParamUtils; // 参数工具 import com.ecep.contract.util.MyStringUtils; // 字符串工具 ``` ### 2. 工具类使用示例 #### ParamUtils 构建查询条件 ```java // 构建复杂查询条件 Map params = ParamUtils.builder() .equals("field1", value1) .equals("field2", value2) .like("name", "%" + keyword + "%") .between("date", startDate, endDate) .in("status", List.of("ACTIVE", "PENDING")) .orderBy("createTime", "desc") .build(); ``` #### FileUtils 处理文件路径 ```java // 文件名转义 String safeFileName = FileUtils.escapeFileName(companyName); // 获取父级前缀 String parentPrefix = FileUtils.getParentPrefixByDistrict(district); ``` --- ## 📊 错误处理和日志 ### 1. 异常处理模式 ```java // 查询异常处理 try { return async("findByCode", code, String.class).handle((response, ex) -> { if (ex != null) { throw new RuntimeException("远程方法 findByCode 调用失败", ex); } if (response != null) { return updateValue(createNewEntity(), response); } return null; }).get(); } catch (Exception e) { throw new RuntimeException("查找实体失败: " + code, e); } // 业务验证异常 public boolean businessMethod(XxxVo xxx) { if (xxx == null) { return false; } if (!xxx.isValid()) { throw new IllegalArgumentException("实体数据无效"); } return true; } ``` ### 2. 日志记录 ```java // 在 QueryService 中已集成日志 private static final Logger logger = LoggerFactory.getLogger(QueryService.class); // 在业务方法中使用 public void deleteXxx(XxxVo xxx) { try { super.delete(xxx); logger.info("删除实体成功 #{}", xxx.getId()); } catch (Exception e) { logger.error("删除实体失败 #{}", xxx.getId(), e); throw new RuntimeException("删除实体失败", e); } } ``` --- ## 🧪 测试和验证 ### 1. 方法验证检查点 - **输入参数验证**:检查空值、格式、范围 - **业务逻辑验证**:检查状态、关联、权限 - **文件系统验证**:检查路径、权限、空间 - **数据库验证**:检查连接、事务、一致性 ### 2. 常见测试场景 ```java // 测试用例示例 @Test public void testFindById() { // 正常情况 XxxVo result = xxxService.findById(1); assertNotNull(result); // 异常情况 assertThrows(RuntimeException.class, () -> { xxxService.findById(-1); }); } @Test public void testSaveWithCache() { XxxVo xxx = createTestXxx(); xxx.setCode("TEST001"); XxxVo saved = xxxService.save(xxx); assertNotNull(saved.getId()); // 验证缓存 XxxVo cached = xxxService.findById(saved.getId()); assertEquals(saved.getId(), cached.getId()); } ``` --- ## 🎯 最佳实践 ### 1. 代码组织原则 - **单一职责**:每个 Service 专注特定的业务域 - **依赖注入**:合理使用 `@Autowired` 注入依赖 - **缓存策略**:为高频查询字段配置缓存 - **异常处理**:统一处理业务异常和系统异常 ### 2. 性能优化建议 - **缓存配置**:合理设置缓存过期时间 - **分页查询**:避免大数据量一次性查询 - **异步处理**:使用异步调用提升响应速度 - **批量操作**:考虑批量处理减少网络开销 ### 3. 可维护性提升 - **命名规范**:遵循项目统一的命名约定 - **注释文档**:为复杂业务逻辑添加注释 - **代码复用**:提取公共逻辑到工具类 - **版本兼容**:考虑前后端版本兼容性 --- ## 📚 相关规范 - **Controller 规范**:[client_controller_rules.md](.trae/rules/client_controller_rules.md) - **转换器规范**:[client_converter_rules.md](.trae/rules/client_converter_rules.md) - **Task 规范**:[client_task_rules.md](.trae/rules/client_task_rules.md) - **Entity 规范**:[entity_rules.md](.trae/rules/entity_rules.md) - **VO 规范**:[vo_rules.md](.trae/rules/vo_rules.md) --- *本文档基于 Contract-Manager 项目现有 Service 实现模式总结,遵循项目既定的技术架构和编程规范。* **文档版本**: v1.0.0 **最后更新**: 2024-12-19 **维护团队**: Contract Manager Development Team