重构合同文件管理逻辑,增加错误处理和日志记录 新增ContractBalance实体、Repository和VO类 完善Voable接口文档和实现规范 更新项目架构文档和数据库设计 修复SmbFileService的连接问题 移动合同相关TabSkin类到contract包 添加合同文件重建任务的WebSocket支持
435 lines
13 KiB
Markdown
435 lines
13 KiB
Markdown
# 服务器端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<T>**: 实体基本操作接口
|
||
- **QueryService<Vo>**: 查询服务接口
|
||
- **VoableService<M, Vo>**: 实体与视图对象转换服务接口
|
||
|
||
## 注解使用规范
|
||
|
||
### 类级别注解
|
||
```java
|
||
@Lazy // 延迟加载,避免循环依赖
|
||
@Service // Spring服务组件
|
||
@CacheConfig(cacheNames = "业务缓存名称") // 缓存配置
|
||
public class CompanyService extends EntityService<Company, CompanyVo, Integer>
|
||
implements IEntityService<Company>, QueryService<CompanyVo>, VoableService<Company, CompanyVo> {
|
||
// 实现代码
|
||
}
|
||
```
|
||
|
||
### 方法级别注解
|
||
```java
|
||
// 查询方法缓存 - 使用参数作为缓存键
|
||
@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注入
|
||
```java
|
||
@Lazy
|
||
@Autowired
|
||
private CompanyRepository repository;
|
||
```
|
||
|
||
### Service间依赖注入
|
||
```java
|
||
@Lazy
|
||
@Autowired
|
||
private ContractService contractService;
|
||
|
||
@Lazy
|
||
@Autowired
|
||
private VendorService vendorService;
|
||
|
||
@Lazy
|
||
@Autowired
|
||
private CompanyContactService companyContactService;
|
||
```
|
||
|
||
### 外部服务依赖注入
|
||
```java
|
||
@Lazy
|
||
@Autowired
|
||
private CloudRkService cloudRkService;
|
||
|
||
@Lazy
|
||
@Autowired
|
||
private CloudTycService cloudTycService;
|
||
|
||
@Autowired(required = false) // 可选依赖
|
||
private YongYouU8Service yongYouU8Service;
|
||
```
|
||
|
||
## 查询实现模式
|
||
|
||
### 标准查询实现
|
||
```java
|
||
@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);
|
||
}
|
||
```
|
||
|
||
### 复杂查询条件构建
|
||
```java
|
||
@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)提供公司关联查询:
|
||
|
||
```java
|
||
@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)
|
||
```java
|
||
@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
|
||
```java
|
||
@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实现模式
|
||
|
||
### 文件路径管理
|
||
```java
|
||
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")`
|
||
|
||
### 缓存清理策略
|
||
```java
|
||
@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<T>进行分页查询,避免一次性加载大量数据:
|
||
```java
|
||
public Page<Project> findAll(Specification<Project> spec, Pageable pageable) {
|
||
return projectRepository.findAll(spec, pageable);
|
||
}
|
||
```
|
||
|
||
### 4. 批量操作
|
||
对于大量数据操作,考虑使用批量处理:
|
||
```java
|
||
public void saveAll(List<CompanyCustomerFile> files) {
|
||
companyCustomerFileService.saveAll(retrieveFiles);
|
||
}
|
||
```
|
||
|
||
## 异常处理规范
|
||
|
||
### 参数校验
|
||
```java
|
||
public void updateByVo(CustomerCatalog model, CustomerCatalogVo vo) {
|
||
if (model == null) {
|
||
throw new ServiceException("实体对象不能为空");
|
||
}
|
||
if (vo == null) {
|
||
throw new ServiceException("VO对象不能为空");
|
||
}
|
||
// ... 业务逻辑
|
||
}
|
||
```
|
||
|
||
### 业务异常处理
|
||
```java
|
||
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使用
|
||
```java
|
||
// 字段等值查询
|
||
spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "name", "code", "status");
|
||
|
||
// 参数关联查询
|
||
spec = SpecificationUtils.andParam(spec, paramsNode, "company", "catalog", "type");
|
||
|
||
// 搜索文本组合
|
||
spec = SpecificationUtils.andWith(searchText, this::buildSearchSpecification);
|
||
```
|
||
|
||
### 字符串工具使用CompanyBasicService
|
||
```java
|
||
// 全数字判断
|
||
if (MyStringUtils.isAllDigit(searchText)) {
|
||
// 数字处理逻辑
|
||
}
|
||
|
||
// 空值检查
|
||
if (!StringUtils.hasText(searchText)) {
|
||
return null;
|
||
}
|
||
```
|
||
|
||
## 最佳实践总结
|
||
|
||
1. **接口实现完整性**: 所有Service应实现三个核心接口,确保功能一致性
|
||
2. **缓存策略一致性**: 遵循统一的缓存键设计和清理策略
|
||
3. **依赖注入规范**: 使用@Lazy避免循环依赖,清晰管理Service间依赖关系
|
||
4. **查询性能优化**: 合理使用SpecificationUtils构建查询条件,支持分页查询
|
||
5. **异常处理统一**: 统一的异常处理方式,提高代码健壮性
|
||
6. **代码复用**: 继承合适的基类,复用通用逻辑
|
||
7. **文档注释**: 关键方法和复杂业务逻辑应有清晰的JavaDoc注释
|
||
8. **性能监控**: 关注缓存命中率,适时调整缓存策略
|
||
|
||
这套规范确保了Service层的代码质量、性能和可维护性,为整个系统的稳定运行提供了坚实基础。 |