feat(contract): 新增合同余额功能及重构文件管理

重构合同文件管理逻辑,增加错误处理和日志记录
新增ContractBalance实体、Repository和VO类
完善Voable接口文档和实现规范
更新项目架构文档和数据库设计
修复SmbFileService的连接问题
移动合同相关TabSkin类到contract包
添加合同文件重建任务的WebSocket支持
This commit is contained in:
2025-11-19 00:50:16 +08:00
parent 87290f15b0
commit 02afa189f8
49 changed files with 7577 additions and 441 deletions

View File

@@ -1,260 +1,435 @@
# 服务器端 Service 类规则文档
# 服务器端Service设计规范
## 1. 概述
## 目录结构
本规则文档定义了 Contract-Manager 项目服务器端server模块Service 类的设计规范、实现标准和最佳实践。所有服务器端 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
└── ...
```
## 2. 目录结构
## 核心基类和接口体系
Service 类按业务领域组织,位于 `server/src/main/java/com/ecep/contract/ds/{业务领域}/service/` 目录下。其中 `{业务领域}` 对应具体的业务模块,如 `customer``contract``company``project``other` 等。
### 主要基类
- **EntityService<M, Vo, ID>**: 通用实体服务基类提供CRUD操作的标准实现
- **CompanyBasicService**: 专门处理公司相关业务的基础服务类,支持公司关联查询
**示例:**
- 客户分类服务:`server/src/main/java/com/ecep/contract/ds/customer/service/CustomerCatalogService.java`
- 员工服务:`server/src/main/java/com/ecep/contract/ds/other/service/EmployeeService.java`
### 核心接口
- **IEntityService<T>**: 实体基本操作接口
- **QueryService<Vo>**: 查询服务接口
- **VoableService<M, Vo>**: 实体与视图对象转换服务接口
## 3. 命名规范
## 注解使用规范
- **类名**:采用驼峰命名法,首字母大写,以 `Service` 结尾,表示这是一个服务类。
**示例**`CustomerCatalogService``EmployeeService`
### 类级别注解
```java
@Lazy // 延迟加载,避免循环依赖
@Service // Spring服务组件
@CacheConfig(cacheNames = "业务缓存名称") // 缓存配置
public class CompanyService extends EntityService<Company, CompanyVo, Integer>
implements IEntityService<Company>, QueryService<CompanyVo>, VoableService<Company, CompanyVo> {
// 实现代码
}
```
## 4. 接口实现
### 方法级别注解
```java
// 查询方法缓存 - 使用参数作为缓存键
@Cacheable(key = "#p0") // ID查询
public CompanyVo findById(Integer id) {
return repository.findById(id).map(Company::toVo).orElse(null);
}
所有业务领域的 Service 类必须实现以下三个核心接口:
@Cacheable(key = "'name-'+#p0") // 名称查询,带前缀
public CompanyVo findByName(String name) {
return repository.findFirstByName(name).map(Company::toVo).orElse(null);
}
### 4.1 IEntityService<T>
// 修改方法缓存清理 - 清理所有相关缓存
@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);
}
```
提供实体类的基本 CRUD 操作。泛型 `T` 表示实体类类型。
## 依赖注入规范
**主要方法:**
- `T getById(Integer id)`:根据 ID 查询实体对象
- `Page<T> findAll(Specification<T> spec, Pageable pageable)`:根据条件和分页参数查询实体列表
- `Specification<T> getSpecification(String searchText)`:构建搜索条件
- `void delete(T entity)`:删除实体
- `T save(T entity)`:保存实体
### Repository注入
```java
@Lazy
@Autowired
private CompanyRepository repository;
```
### 4.2 QueryService<Vo>
### Service间依赖注入
```java
@Lazy
@Autowired
private ContractService contractService;
提供 VO 对象的查询能力。泛型 `Vo` 表示视图对象类型。
@Lazy
@Autowired
private VendorService vendorService;
**主要方法:**
- `Vo findById(Integer id)`:根据 ID 查询 VO 对象
- `Page<Vo> findAll(JsonNode paramsNode, Pageable pageable)`:根据 JSON 查询参数和分页条件查询 VO 列表
- `default long count(JsonNode paramsNode)`:根据查询参数统计数据总数
@Lazy
@Autowired
private CompanyContactService companyContactService;
```
### 4.3 VoableService<M, Vo>
### 外部服务依赖注入
```java
@Lazy
@Autowired
private CloudRkService cloudRkService;
提供从 VO 对象更新实体对象的能力。泛型 `M` 表示实体类类型,`Vo` 表示视图对象类型。
@Lazy
@Autowired
private CloudTycService cloudTycService;
**主要方法:**
- `void updateByVo(M model, Vo vo)`:根据 VO 对象更新实体对象
@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 = "customer-catalog")
public class CustomerCatalogService implements IEntityService<CustomerCatalog>, QueryService<CustomerCatalogVo>,
VoableService<CustomerCatalog, CustomerCatalogVo> {
// 实现方法...
@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;
}
}
```
## 5. 注解规范
Service 类必须使用以下注解:
### 5.1 类级别注解
- `@Service`:标记这是一个 Spring 服务类,使其能够被自动扫描和管理
- `@Lazy`:延迟加载服务类,提高应用启动性能
- `@CacheConfig(cacheNames = "缓存名称")`:配置缓存名称,缓存名称通常与业务领域相关
**示例**`@CacheConfig(cacheNames = "customer-catalog")``@CacheConfig(cacheNames = "employee")`
### 5.2 方法级别注解
#### 5.2.1 查询方法注解
- `@Cacheable(key = "缓存键")`:将查询结果缓存起来,下次相同查询可以直接从缓存获取
**示例**`@Cacheable(key = "#p0")`(使用方法第一个参数作为缓存键)、`@Cacheable(key = "'code-'+#p0")`(使用前缀+参数值作为缓存键)
#### 5.2.2 数据修改方法注解
- `@Caching(evict = { ... })`:在保存或删除操作时清除相关缓存
**示例**
```java
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "'code-'+#p0.code"),
@CacheEvict(key = "'name-'+#p0.name"),
@CacheEvict(key = "'all'")
})
```
- `@Transactional`:标记方法需要在事务中执行,确保数据一致性
**示例**:用于包含多个数据操作的复杂业务方法
## 6. 缓存策略
Service 类必须遵循以下缓存策略:
### 6.1 查询缓存
- 所有 `findById`、`findByCode`、`findByName` 等单条查询方法都应使用 `@Cacheable` 注解缓存结果
- 缓存键设计应具有唯一性和可读性,通常包含参数值和适当的前缀
- 列表查询(如 `findAll`)可以考虑使用 `@Cacheable`,但需谨慎管理缓存失效
### 6.2 缓存清理
- 所有 `save`、`delete` 等修改数据的方法都应使用 `@Caching` 和 `@CacheEvict` 注解清理相关缓存
- 清理缓存时应考虑所有可能影响的查询,确保缓存数据的一致性
## 7. 方法实现规范
### 7.1 IEntityService 方法实现
- `getById`:直接调用 Repository 的 `findById` 方法,返回实体对象
```java
@Override
public CustomerCatalog getById(Integer id) {
return repository.findById(id).orElse(null);
}
```
- `findAll(Specification<T>, Pageable)`:直接调用 Repository 的 `findAll` 方法,返回实体分页对象
```java
@Override
public Page<CustomerCatalog> findAll(Specification<CustomerCatalog> spec, Pageable pageable) {
return repository.findAll(spec, pageable);
}
```
- `save/delete`:调用 Repository 的相应方法,并添加缓存清理注解
```java
@Caching(evict = { ... })
@Override
public CustomerCatalog save(CustomerCatalog catalog) {
return repository.save(catalog);
}
```
### 7.2 QueryService 方法实现
- `findById`:调用 Repository 的 `findById` 方法,然后调用实体类的 `toVo` 方法转换为 VO 对象
```java
@Cacheable(key = "#p0")
@Override
public CustomerCatalogVo findById(Integer id) {
return repository.findById(id).map(CustomerCatalog::toVo).orElse(null);
}
```
- `findAll(JsonNode, Pageable)`:构建 Specification调用 IEntityService 的 `findAll` 方法,然后使用 Stream API 的 `map` 方法将结果转换为 VO 对象
```java
@Override
public Page<CustomerCatalogVo> findAll(JsonNode paramsNode, Pageable pageable) {
Specification<CustomerCatalog> spec = null;
if (paramsNode.has(ServiceConstant.KEY_SEARCH_TEXT)) {
spec = getSpecification(paramsNode.get(ServiceConstant.KEY_SEARCH_TEXT).asText());
}
// 字段等值查询
spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "code", "name", "description");
return repository.findAll(spec, pageable).map(CustomerCatalog::toVo);
}
```
### 7.3 VoableService 方法实现
- `updateByVo`:将 VO 对象的属性逐个复制到实体对象,实现 VO 到实体的转换
```java
@Override
public void updateByVo(CustomerCatalog model, CustomerCatalogVo vo) {
// 参数校验
if (model == null) {
throw new ServiceException("实体对象不能为空");
}
if (vo == null) {
throw new ServiceException("VO对象不能为空");
}
// 映射基本属性
model.setCode(vo.getCode());
model.setName(vo.getName());
model.setDescription(vo.getDescription());
}
```
## 8. 依赖注入规范
- Service 类应使用 `@Autowired` 注解注入所需的 Repository 和其他依赖
- 为了避免循环依赖,所有注入的依赖都应使用 `@Lazy` 注解标记为延迟加载
```java
@Lazy
@Autowired
private CustomerCatalogRepository repository;
```
## 9. 查询条件构建
- 使用 `SpecificationUtils` 工具类辅助构建查询条件
- 实现 `getSpecification(String searchText)` 方法,提供基于搜索文本的模糊查询能力
```java
@Override
public Specification<CustomerCatalog> getSpecification(String searchText) {
if (!StringUtils.hasText(searchText)) {
return null;
}
String likeText = "%" + searchText + "%";
return (root, query, builder) -> {
return builder.or(
builder.like(root.get("code"), likeText),
builder.like(root.get("name"), likeText),
builder.like(root.get("description"), likeText));
};
}
```
## 10. 异常处理
- Service 类应适当处理异常,特别是对输入参数的校验
- 对于业务逻辑异常,应抛出 `ServiceException`
```java
if (model == null) {
throw new ServiceException("实体对象不能为空");
}
```
## 11. 最佳实践
1. **VO优先原则**QueryService 接口应使用 VO 类型作为泛型参数,返回 VO 对象而不是实体对象
2. **缓存粒度**:缓存键应设计得足够细粒度,避免缓存过大或频繁失效
3. **事务管理**:包含多个数据操作的方法应使用 `@Transactional` 注解确保事务一致性
4. **延迟加载**:所有依赖都应使用 `@Lazy` 注解,避免循环依赖问题
5. **参数校验**:方法开始时应进行参数校验,确保输入数据的合法性
6. **文档注释**:关键方法应有清晰的 JavaDoc 注释,说明其功能、参数和返回值
7. **代码复用**:尽量使用 SpecificationUtils 等工具类辅助构建查询条件,提高代码复用性
## 12. 不符合规范的Service类示例
以下是不符合规范的Service类实现应避免
### 2. 继承CompanyBasicService的Service
```java
// 错误QueryService泛型参数使用实体类型而非VO类型
@Lazy
@Service
@CacheConfig(cacheNames = "company")
public class CompanyService extends EntityService<Company, Integer>
implements IEntityService<Company>, QueryService<Company>, VoableService<Company, CompanyVo> {
// 实现方法...
}
// 错误:未使用缓存注解
@Service
public class ExampleService implements IEntityService<Example>, QueryService<ExampleVo>,
VoableService<Example, ExampleVo> {
// 未使用@Cacheable、@CacheEvict等缓存注解
@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;
}
}
```
## 13. 总结
## 文件管理Service实现模式
本规则文档定义了服务器端 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层的代码质量、性能和可维护性为整个系统的稳定运行提供了坚实基础。