Files
contract-manager/.trae/rules/server_service_rules.md
songqq 02afa189f8 feat(contract): 新增合同余额功能及重构文件管理
重构合同文件管理逻辑,增加错误处理和日志记录
新增ContractBalance实体、Repository和VO类
完善Voable接口文档和实现规范
更新项目架构文档和数据库设计
修复SmbFileService的连接问题
移动合同相关TabSkin类到contract包
添加合同文件重建任务的WebSocket支持
2025-11-19 00:50:16 +08:00

13 KiB
Raw Blame History

服务器端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;
}

最佳实践总结

  1. 接口实现完整性: 所有Service应实现三个核心接口确保功能一致性
  2. 缓存策略一致性: 遵循统一的缓存键设计和清理策略
  3. 依赖注入规范: 使用@Lazy避免循环依赖清晰管理Service间依赖关系
  4. 查询性能优化: 合理使用SpecificationUtils构建查询条件支持分页查询
  5. 异常处理统一: 统一的异常处理方式,提高代码健壮性
  6. 代码复用: 继承合适的基类,复用通用逻辑
  7. 文档注释: 关键方法和复杂业务逻辑应有清晰的JavaDoc注释
  8. 性能监控: 关注缓存命中率,适时调整缓存策略

这套规范确保了Service层的代码质量、性能和可维护性为整个系统的稳定运行提供了坚实基础。