重构合同文件管理逻辑,增加错误处理和日志记录 新增ContractBalance实体、Repository和VO类 完善Voable接口文档和实现规范 更新项目架构文档和数据库设计 修复SmbFileService的连接问题 移动合同相关TabSkin类到contract包 添加合同文件重建任务的WebSocket支持
13 KiB
13 KiB
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. 可维护性提升
- 命名规范:遵循项目统一的命名约定
- 注释文档:为复杂业务逻辑添加注释
- 代码复用:提取公共逻辑到工具类
- 版本兼容:考虑前后端版本兼容性
📚 相关规范
- Controller 规范:client_controller_rules.md
- 转换器规范:client_converter_rules.md
- Task 规范:client_task_rules.md
- Entity 规范:entity_rules.md
- VO 规范:vo_rules.md
本文档基于 Contract-Manager 项目现有 Service 实现模式总结,遵循项目既定的技术架构和编程规范。
文档版本: v1.0.0
最后更新: 2024-12-19
维护团队: Contract Manager Development Team