重构合同文件管理逻辑,增加错误处理和日志记录 新增ContractBalance实体、Repository和VO类 完善Voable接口文档和实现规范 更新项目架构文档和数据库设计 修复SmbFileService的连接问题 移动合同相关TabSkin类到contract包 添加合同文件重建任务的WebSocket支持
13 KiB
13 KiB
服务器端Service设计规范
目录结构
每个业务域下的service目录结构示例:
ds/
├── company/service/
│ ├── CompanyService.java # 主业务服务
│ ├── CompanyContactService.java # 联系人服务
│ ├── CompanyFileService.java # 文件管理服务
│ ├── CompanyOldNameService.java # 曾用名服务
│ └── ...
├── contract/service/
│ ├── ContractService.java # 主业务服务
│ ├── ContractCatalogService.java # 分类目录服务
│ └── ...
├── customer/service/
│ ├── CustomerService.java # 主业务服务(继承CompanyBasicService)
│ └── ...
├── project/service/
│ ├── ProjectService.java # 主业务服务
│ ├── ProjectFileService.java # 文件管理服务
│ └── ...
└── vendor/service/
├── VendorService.java # 主业务服务(继承CompanyBasicService)
└── ...
核心基类和接口体系
主要基类
- EntityService<M, Vo, ID>: 通用实体服务基类,提供CRUD操作的标准实现
- CompanyBasicService: 专门处理公司相关业务的基础服务类,支持公司关联查询
核心接口
- IEntityService: 实体基本操作接口
- QueryService: 查询服务接口
- VoableService<M, Vo>: 实体与视图对象转换服务接口
注解使用规范
类级别注解
@Lazy // 延迟加载,避免循环依赖
@Service // Spring服务组件
@CacheConfig(cacheNames = "业务缓存名称") // 缓存配置
public class CompanyService extends EntityService<Company, CompanyVo, Integer>
implements IEntityService<Company>, QueryService<CompanyVo>, VoableService<Company, CompanyVo> {
// 实现代码
}
方法级别注解
// 查询方法缓存 - 使用参数作为缓存键
@Cacheable(key = "#p0") // ID查询
public CompanyVo findById(Integer id) {
return repository.findById(id).map(Company::toVo).orElse(null);
}
@Cacheable(key = "'name-'+#p0") // 名称查询,带前缀
public CompanyVo findByName(String name) {
return repository.findFirstByName(name).map(Company::toVo).orElse(null);
}
// 修改方法缓存清理 - 清理所有相关缓存
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "'name-'+#p0.name"),
@CacheEvict(key = "'code-'+#p0.code")
})
public Contract save(Contract contract) {
return contractRepository.save(contract);
}
依赖注入规范
Repository注入
@Lazy
@Autowired
private CompanyRepository repository;
Service间依赖注入
@Lazy
@Autowired
private ContractService contractService;
@Lazy
@Autowired
private VendorService vendorService;
@Lazy
@Autowired
private CompanyContactService companyContactService;
外部服务依赖注入
@Lazy
@Autowired
private CloudRkService cloudRkService;
@Lazy
@Autowired
private CloudTycService cloudTycService;
@Autowired(required = false) // 可选依赖
private YongYouU8Service yongYouU8Service;
查询实现模式
标准查询实现
@Override
public Page<CompanyVo> findAll(JsonNode paramsNode, Pageable pageable) {
Specification<Company> spec = null;
// 搜索文本查询
if (paramsNode.has("searchText")) {
spec = getSpecification(paramsNode.get("searchText").asText());
}
// 字段等值查询
spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "name", "uniscid", "abbName");
// 分页查询并转换为VO
return findAll(spec, pageable).map(Company::toVo);
}
复杂查询条件构建
@Override
public Specification<Company> getSpecification(String searchText) {
if (!StringUtils.hasText(searchText)) {
return null;
}
return (root, query, builder) -> {
return builder.or(
builder.like(root.get("name"), "%" + searchText + "%"),
builder.like(root.get("code"), "%" + searchText + "%"),
builder.like(root.get("description"), "%" + searchText + "%"));
};
}
CompanyBasicService继承模式
继承CompanyBasicService的Service(如CustomerService、VendorService)提供公司关联查询:
@Override
public Specification<Vendor> getSpecification(String searchText) {
if (!StringUtils.hasText(searchText)) {
return null;
}
// 使用公司关联查询
Specification<Vendor> nameSpec = (root, query, builder) -> {
Path<Company> company = root.get("company");
return companyService.buildSearchPredicate(searchText, company, query, builder);
};
// 数字ID查询
if (MyStringUtils.isAllDigit(searchText)) {
try {
int id = Integer.parseInt(searchText);
nameSpec = SpecificationUtils.or(nameSpec, (root, query, builder) -> {
return builder.equal(root.get("id"), id);
});
} catch (Exception ignored) {
}
}
// 实体搜索
List<VendorEntity> searched = vendorEntityService.search(searchText);
if (!searched.isEmpty()) {
nameSpec = SpecificationUtils.or(nameSpec, (root, query, builder) -> {
return builder.in(root.get("id")).value(searched.stream()
.map(VendorEntity::getVendor)
.filter(Objects::nonNull)
.map(Vendor::getId)
.collect(Collectors.toSet()));
});
}
return nameSpec;
}
业务逻辑实现模式
1. 主业务Service(继承EntityService)
@Lazy
@Service
@CacheConfig(cacheNames = "contract")
public class ContractService extends EntityService<Contract, ContractVo, Integer>
implements IEntityService<Contract>, QueryService<ContractVo>, VoableService<Contract, ContractVo> {
@Override
protected ContractRepository getRepository() {
return contractRepository;
}
@Cacheable(key = "#p0")
public ContractVo findById(Integer id) {
return getRepository().findById(id).map(Contract::toVo).orElse(null);
}
// 业务特定方法
public List<Contract> findAllByCompany(Company company) {
return contractRepository.findAllByCompanyId(company.getId());
}
public File getContractCatalogPath(ContractCatalogVo catalog, ContractVo contract) {
// 文件路径处理逻辑
String parent = catalog.getParent();
File dir = getBasePath();
// ... 路径构建逻辑
return dir;
}
}
2. 继承CompanyBasicService的Service
@Lazy
@Service
@CacheConfig(cacheNames = "company-customer")
public class CustomerService extends CompanyBasicService
implements IEntityService<CompanyCustomer>, QueryService<CustomerVo>,
VoableService<CompanyCustomer, CustomerVo> {
// 提供公司关联查询
public CompanyCustomer findByCompany(Company company) {
return repository.findByCompany(company).orElse(null);
}
public CustomerVo findByCompany(CompanyVo company) {
return repository.findByCompanyId(company.getId()).map(CompanyCustomer::toVo).orElse(null);
}
// 文件重建业务逻辑
public boolean reBuildingFiles(CompanyCustomer companyCustomer, MessageHolder holder) {
List<CompanyCustomerFile> dbFiles = companyCustomerFileService.findAllByCustomer(companyCustomer);
Map<String, CompanyCustomerFile> map = new HashMap<>();
boolean modified = fetchDbFiles(dbFiles, map, holder::info);
List<File> needMoveToCompanyPath = new ArrayList<>();
fetchFiles(companyCustomer.getPath(), needMoveToCompanyPath, retrieveFiles, map, holder::info);
moveFileToCompany(companyCustomer.getCompany(), needMoveToCompanyPath);
holder.info("导入 " + retrieveFiles.size() + " 个文件");
if (!retrieveFiles.isEmpty()) {
retrieveFiles.forEach(v -> v.setCustomer(companyCustomer));
companyCustomerFileService.saveAll(retrieveFiles);
modified = true;
}
return modified;
}
}
文件管理Service实现模式
文件路径管理
public File getBasePath() {
return new File(confService.getString(ContractConstant.KEY_BASE_PATH));
}
public File getContractCatalogPath(ContractCatalogVo catalog, ContractVo contract) {
String parent = catalog.getParent();
File dir = getBasePath();
if (StringUtils.hasText(parent)) {
dir = new File(dir, parent);
if (!dir.exists() && !dir.mkdir()) {
System.out.println("unable make directory = " + dir.getAbsolutePath());
return null;
}
}
dir = new File(dir, catalog.getPath());
if (!dir.exists() && !dir.mkdir()) {
System.out.println("unable make directory = " + dir.getAbsolutePath());
return null;
}
if (catalog.isUseYear()) {
String code = contract.getCode();
String catalogCode = catalog.getCode();
int length = catalogCode.length();
String yearCode = code.substring(length, length + 2);
dir = new File(dir, "20" + yearCode);
if (!dir.exists() && !dir.mkdir()) {
System.out.println("unable make directory = " + dir.getAbsolutePath());
return null;
}
}
return dir;
}
缓存策略最佳实践
缓存键设计原则
- ID查询:
@Cacheable(key = "#p0") - 字符串查询:
@Cacheable(key = "'name-'+#p0") - 复合键查询:
@Cacheable(key = "'code-year-'+#p0+'-'+#p1")
缓存清理策略
@Caching(evict = {
@CacheEvict(key = "#p0.id"), // 主键缓存
@CacheEvict(key = "'name-'+#p0.name"), // 名称缓存
@CacheEvict(key = "'code-'+#p0.code"), // 编码缓存
@CacheEvict(key = "'guid-'+#p0.guid") // GUID缓存
})
public Contract save(Contract contract) {
return contractRepository.save(contract);
}
性能优化建议
1. 延迟加载
所有Service依赖都应使用@Lazy注解,避免循环依赖和启动时的性能问题。
2. 缓存粒度
- 单条查询使用细粒度缓存键
- 避免缓存大量数据的列表查询
- 合理设置缓存过期策略
3. 分页查询
使用Page进行分页查询,避免一次性加载大量数据:
public Page<Project> findAll(Specification<Project> spec, Pageable pageable) {
return projectRepository.findAll(spec, pageable);
}
4. 批量操作
对于大量数据操作,考虑使用批量处理:
public void saveAll(List<CompanyCustomerFile> files) {
companyCustomerFileService.saveAll(retrieveFiles);
}
异常处理规范
参数校验
public void updateByVo(CustomerCatalog model, CustomerCatalogVo vo) {
if (model == null) {
throw new ServiceException("实体对象不能为空");
}
if (vo == null) {
throw new ServiceException("VO对象不能为空");
}
// ... 业务逻辑
}
业务异常处理
public Company findAndRemoveDuplicateCompanyByUniscid(String uniscid) {
List<Company> companies = repository.findAllByUniscid(uniscid);
if (companies.isEmpty()) {
return null;
}
if (companies.size() == 1) {
return companies.getFirst();
} else {
List<Company> result = removeDuplicatesByUniscid(companies);
if (!result.isEmpty()) {
return result.getFirst();
}
}
return null;
}
工具类使用规范
SpecificationUtils使用
// 字段等值查询
spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "name", "code", "status");
// 参数关联查询
spec = SpecificationUtils.andParam(spec, paramsNode, "company", "catalog", "type");
// 搜索文本组合
spec = SpecificationUtils.andWith(searchText, this::buildSearchSpecification);
字符串工具使用CompanyBasicService
// 全数字判断
if (MyStringUtils.isAllDigit(searchText)) {
// 数字处理逻辑
}
// 空值检查
if (!StringUtils.hasText(searchText)) {
return null;
}
最佳实践总结
- 接口实现完整性: 所有Service应实现三个核心接口,确保功能一致性
- 缓存策略一致性: 遵循统一的缓存键设计和清理策略
- 依赖注入规范: 使用@Lazy避免循环依赖,清晰管理Service间依赖关系
- 查询性能优化: 合理使用SpecificationUtils构建查询条件,支持分页查询
- 异常处理统一: 统一的异常处理方式,提高代码健壮性
- 代码复用: 继承合适的基类,复用通用逻辑
- 文档注释: 关键方法和复杂业务逻辑应有清晰的JavaDoc注释
- 性能监控: 关注缓存命中率,适时调整缓存策略
这套规范确保了Service层的代码质量、性能和可维护性,为整个系统的稳定运行提供了坚实基础。