重构合同文件管理逻辑,增加错误处理和日志记录 新增ContractBalance实体、Repository和VO类 完善Voable接口文档和实现规范 更新项目架构文档和数据库设计 修复SmbFileService的连接问题 移动合同相关TabSkin类到contract包 添加合同文件重建任务的WebSocket支持
508 lines
13 KiB
Markdown
508 lines
13 KiB
Markdown
# 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 |