Files
contract-manager/.trae/rules/client_service_rules.md
songqq 02afa189f8 feat(contract): 新增合同余额功能及重构文件管理
重构合同文件管理逻辑,增加错误处理和日志记录
新增ContractBalance实体、Repository和VO类
完善Voable接口文档和实现规范
更新项目架构文档和数据库设计
修复SmbFileService的连接问题
移动合同相关TabSkin类到contract包
添加合同文件重建任务的WebSocket支持
2025-11-19 00:50:16 +08:00

13 KiB
Raw Blame History

Client Service 实现编写指南

📋 概述

本指南总结 Client 模块 Service 层的实现经验,用于指导后续 Service 的编写。本指南基于 Contract-Manager 项目中已实现的 Service 模式整理。


🏗️ 基础架构

1. 继承层次结构

// Service 接口定义
public interface IEntityService<T> {
    T findById(Integer id);
    T save(T entity);
    void delete(T entity);
    List<T> findAll();
    Page<T> findAll(Map<String, Object> params, Pageable pageable);
    StringConverter<T> getStringConverter();
}

// 基础 Service 实现
public abstract class QueryService<T extends IdentityEntity, TV extends IdentityViewModel<T>>
        implements ViewModelService<T, TV> {
    // 核心实现
}

// 具体业务 Service
@Service
@CacheConfig(cacheNames = "business")
public class XxxService extends QueryService<XxxVo, XxxViewModel> {
    // 业务特定实现
}

2. 核心特性

  • 泛型支持QueryService 使用泛型处理不同类型的 Vo 和 ViewModel
  • WebSocket 通信:通过 WebSocketClientService 与 Server 端通信
  • 异步处理:使用 CompletableFuture 实现异步操作
  • 缓存机制:集成 Spring Cache 支持多级缓存
  • 错误处理:统一的异常处理和日志记录

📝 Service 编写规范

1. 类声明和注解

@Service                              // Spring 组件注解
@CacheConfig(cacheNames = "xxx")      // 缓存配置xxx为业务域名称
public class XxxService extends QueryService<XxxVo, XxxViewModel> {
    // Service 实现
}

2. 缓存策略

缓存注解使用

@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 方法

@Cacheable(key = "#p0")
@Override
public XxxVo findById(Integer id) {
    return super.findById(id);  // 调用父类方法
}

save 方法(带缓存清除)

@Caching(evict = {
    @CacheEvict(key = "#p0.id"),
    @CacheEvict(key = "'code-'+#p0.code")
})
@Override
public XxxVo save(XxxVo entity) {
    return super.save(entity);
}

delete 方法(带缓存清除)

@Caching(evict = {
    @CacheEvict(key = "#p0.id"),
    @CacheEvict(key = "'code-'+#p0.code")
})
@Override
public void delete(XxxVo entity) {
    super.delete(entity);
}

🔄 异步通信模式

1. 基本异步调用

// 异步调用示例
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. 复杂对象处理

public List<XxxDetailVo> 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<XxxDetailVo> 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. 文件系统集成

路径管理

@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();
}

目录创建

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. 业务验证模式

数据完整性验证

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("状态异常:未设置");
    }
}

关联实体验证

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. 复杂业务查询

分页查询

public List<XxxVo> 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();
}

组合条件查询

public Page<XxxVo> 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. 常用工具类依赖

// 常用注入
@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 构建查询条件

// 构建复杂查询条件
Map<String, Object> 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 处理文件路径

// 文件名转义
String safeFileName = FileUtils.escapeFileName(companyName);

// 获取父级前缀
String parentPrefix = FileUtils.getParentPrefixByDistrict(district);

📊 错误处理和日志

1. 异常处理模式

// 查询异常处理
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. 日志记录

// 在 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. 常见测试场景

// 测试用例示例
@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. 可维护性提升

  • 命名规范:遵循项目统一的命名约定
  • 注释文档:为复杂业务逻辑添加注释
  • 代码复用:提取公共逻辑到工具类
  • 版本兼容:考虑前后端版本兼容性

📚 相关规范


本文档基于 Contract-Manager 项目现有 Service 实现模式总结,遵循项目既定的技术架构和编程规范。

文档版本: v1.0.0
最后更新: 2024-12-19
维护团队: Contract Manager Development Team