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

508 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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