# Client Service 实现编写指南

## 📋 概述

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

---

## 🏗️ 基础架构

### 1. 继承层次结构

```java
// 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. 类声明和注解

```java
@Service                              // Spring 组件注解
@CacheConfig(cacheNames = "xxx")      // 缓存配置，xxx为业务域名称
public class XxxService extends QueryService<XxxVo, XxxViewModel> {
    // 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<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. 文件系统集成

#### 路径管理
```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<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();
}
```

#### 组合条件查询
```java
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. 常用工具类依赖

```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<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 处理文件路径
```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