diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md index 1e2fb0e..d2cc6ec 100644 --- a/.trae/rules/project_rules.md +++ b/.trae/rules/project_rules.md @@ -60,7 +60,7 @@ - `customer/`: 客户相关业务 - `project/`: 项目相关业务 - `vendor/`: 供应商相关业务 - - 每个业务领域包含:`model/`(实体类)、`repository/`(数据访问接口)、`service/`(业务逻辑)、`vo/`(视图对象) + - 每个业务领域包含:`model/`(实体类)、`repository/`(数据访问接口)、`service/`(业务逻辑, 详细规范见 `.trae\rules\server_service_rules.md`)、`tasker/`(任务处理器)、`controller/`(控制器) - `handler/`: WebSocket处理器 - `service/`: 服务层,包含一些通用服务和任务处理器 - `ui/`: UI相关组件 diff --git a/.trae/rules/server_service_rules.md b/.trae/rules/server_service_rules.md new file mode 100644 index 0000000..1087663 --- /dev/null +++ b/.trae/rules/server_service_rules.md @@ -0,0 +1,260 @@ +# 服务器端 Service 类规则文档 + +## 1. 概述 + +本规则文档定义了 Contract-Manager 项目服务器端(server模块)Service 类的设计规范、实现标准和最佳实践。所有服务器端 Service 类必须严格遵循本规则,以确保代码的一致性、可维护性和性能。 + +## 2. 目录结构 + +Service 类按业务领域组织,位于 `server/src/main/java/com/ecep/contract/ds/{业务领域}/service/` 目录下。其中 `{业务领域}` 对应具体的业务模块,如 `customer`、`contract`、`company`、`project`、`other` 等。 + +**示例:** +- 客户分类服务:`server/src/main/java/com/ecep/contract/ds/customer/service/CustomerCatalogService.java` +- 员工服务:`server/src/main/java/com/ecep/contract/ds/other/service/EmployeeService.java` + +## 3. 命名规范 + +- **类名**:采用驼峰命名法,首字母大写,以 `Service` 结尾,表示这是一个服务类。 + **示例**:`CustomerCatalogService`、`EmployeeService` + +## 4. 接口实现 + +所有业务领域的 Service 类必须实现以下三个核心接口: + +### 4.1 IEntityService + +提供实体类的基本 CRUD 操作。泛型 `T` 表示实体类类型。 + +**主要方法:** +- `T getById(Integer id)`:根据 ID 查询实体对象 +- `Page findAll(Specification spec, Pageable pageable)`:根据条件和分页参数查询实体列表 +- `Specification getSpecification(String searchText)`:构建搜索条件 +- `void delete(T entity)`:删除实体 +- `T save(T entity)`:保存实体 + +### 4.2 QueryService + +提供 VO 对象的查询能力。泛型 `Vo` 表示视图对象类型。 + +**主要方法:** +- `Vo findById(Integer id)`:根据 ID 查询 VO 对象 +- `Page findAll(JsonNode paramsNode, Pageable pageable)`:根据 JSON 查询参数和分页条件查询 VO 列表 +- `default long count(JsonNode paramsNode)`:根据查询参数统计数据总数 + +### 4.3 VoableService + +提供从 VO 对象更新实体对象的能力。泛型 `M` 表示实体类类型,`Vo` 表示视图对象类型。 + +**主要方法:** +- `void updateByVo(M model, Vo vo)`:根据 VO 对象更新实体对象 + +**实现示例:** +```java +@Lazy +@Service +@CacheConfig(cacheNames = "customer-catalog") +public class CustomerCatalogService implements IEntityService, QueryService, + VoableService { + // 实现方法... +} +``` + +## 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, Pageable)`:直接调用 Repository 的 `findAll` 方法,返回实体分页对象 + ```java + @Override + public Page findAll(Specification 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 findAll(JsonNode paramsNode, Pageable pageable) { + Specification 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 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类实现,应避免: + +```java +// 错误:QueryService泛型参数使用实体类型而非VO类型 +@Service +@CacheConfig(cacheNames = "company") +public class CompanyService extends EntityService + implements IEntityService, QueryService, VoableService { + // 实现方法... +} + +// 错误:未使用缓存注解 +@Service +public class ExampleService implements IEntityService, QueryService, + VoableService { + // 未使用@Cacheable、@CacheEvict等缓存注解 +} +``` + +## 13. 总结 + +本规则文档定义了服务器端 Service 类的完整规范,包括目录结构、命名规范、接口实现、注解规范、缓存策略、方法实现、依赖注入等方面。所有服务器端开发人员都必须严格遵循这些规则,以确保代码的一致性、可维护性和性能。 \ No newline at end of file diff --git a/client/pom.xml b/client/pom.xml index ff40d9f..09e056b 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -6,12 +6,12 @@ com.ecep.contract Contract-Manager - 0.0.100-SNAPSHOT + 0.0.101-SNAPSHOT com.ecep.contract client - 0.0.100-SNAPSHOT + 0.0.101-SNAPSHOT ${java.version} @@ -22,7 +22,7 @@ com.ecep.contract common - 0.0.100-SNAPSHOT + 0.0.101-SNAPSHOT org.springframework.boot diff --git a/client/src/main/java/com/ecep/contract/controller/cloud/rk/CloudRkManagerWindowController.java b/client/src/main/java/com/ecep/contract/controller/cloud/rk/CloudRkManagerWindowController.java index 26925c8..b73269a 100644 --- a/client/src/main/java/com/ecep/contract/controller/cloud/rk/CloudRkManagerWindowController.java +++ b/client/src/main/java/com/ecep/contract/controller/cloud/rk/CloudRkManagerWindowController.java @@ -65,32 +65,12 @@ public class CloudRkManagerWindowController getTitle().set("数据源:集团相关方"); } - private void initializeTask(Task task, String prefix, Consumer consumer) { - task.setOnScheduled(e -> { - consumer.accept("正在从相关方平台同步" + prefix + ",请稍后..."); - }); - task.setOnRunning(e -> { - consumer.accept("开始" + prefix + "..."); - }); - task.setOnSucceeded(e -> { - consumer.accept(prefix + "完成..."); - }); - task.exceptionProperty().addListener((observable, oldValue, newValue) -> { - consumer.accept(newValue.getMessage()); - logger.error("{} 发生异常", prefix, newValue); - }); - SpringApp.getBean(ScheduledExecutorService.class).submit(task); - consumer.accept("任务已创建..."); - } - public void onDataRepairAction(ActionEvent event) { } public void onSyncAction(ActionEvent event) { CloudRkSyncTask task = new CloudRkSyncTask(); - UITools.showTaskDialogAndWait("同步数据", task, consumer -> { - initializeTask(task, "同步数据", msg -> consumer.accept(Message.info(msg))); - }); + UITools.showTaskDialogAndWait("同步数据", task, null); } @Override diff --git a/client/src/main/java/com/ecep/contract/service/HolidayService.java b/client/src/main/java/com/ecep/contract/service/HolidayService.java index f1c19d5..175f3fb 100644 --- a/client/src/main/java/com/ecep/contract/service/HolidayService.java +++ b/client/src/main/java/com/ecep/contract/service/HolidayService.java @@ -1,15 +1,71 @@ package com.ecep.contract.service; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import java.time.LocalDate; +import com.ecep.contract.WebSocketClientService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 节假日服务类 + * + * @author System + */ @Lazy @Service +@RequiredArgsConstructor +@Slf4j public class HolidayService { + @Autowired + private WebSocketClientService webSocketClientService; + @Autowired + protected ObjectMapper objectMapper; + /** + * 调整日期到工作日 + * + * @param date 要调整的日期 + * @return 调整的日期 + */ public LocalDate adjustToWorkDay(LocalDate date) { - throw new RuntimeException("Not implemented"); + if (date == null) { + return null; + } + + try { + JsonNode json = webSocketClientService.invoke("holidayService", "adjustToWorkDay", date).get(); + if (json != null && !json.isEmpty()) { + return objectMapper.convertValue(json, LocalDate.class); + } + } catch (Exception e) { + log.error("调整日期到工作日失败: {}", e.getMessage(), e); + } + + // 如果调用失败,使用客户端本地逻辑作为后备 + return adjustToWorkDayLocally(date); + } + + + /** + * 客户端本地日期调整逻辑,作为服务器调用失败的后备方案 + * + * @param date 要调整的日期 + * @return 调整的日期 + */ + private LocalDate adjustToWorkDayLocally(LocalDate date) { + while (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) { + date = date.plusDays(1); + } + return date; } } diff --git a/client/src/main/java/com/ecep/contract/service/VendorFileService.java b/client/src/main/java/com/ecep/contract/service/VendorFileService.java index d43ef81..937e4ef 100644 --- a/client/src/main/java/com/ecep/contract/service/VendorFileService.java +++ b/client/src/main/java/com/ecep/contract/service/VendorFileService.java @@ -1,5 +1,6 @@ package com.ecep.contract.service; +import java.io.File; import java.time.LocalDate; import java.util.Comparator; import java.util.List; @@ -7,6 +8,8 @@ import java.util.Locale; import java.util.Map; import java.util.function.Consumer; +import com.ecep.contract.SpringApp; +import com.ecep.contract.vo.ContractVo; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -22,13 +25,62 @@ import com.ecep.contract.vo.VendorVo; @Service public class VendorFileService extends QueryService { - public LocalDate getNextSignDate(VendorVo companyVendor, Consumer state) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getNextSignDate'"); - } - public Map getFileTypeLocalMap(Locale locale) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getFileTypeLocalMap'"); + public LocalDate getNextSignDate(VendorVo vendor, Consumer state) { + LocalDate miniContractDate = LocalDate.of(2022, 1, 1); + + // 检索全部合同 + ContractService contractService = SpringApp.getBean(ContractService.class); + List contractList = contractService.findAllByCompanyVendor(vendor, null, null); + if (contractList.isEmpty()) { + state.accept("未发现已登记的合同"); + return null; + } + // 检索评价表 + List files = findAllByVendorAndType(vendor, VendorFileType.EvaluationForm); + VendorFileVo latestFile = files.stream() + .filter(v -> v.getSignDate() != null && v.isValid()) + .max(Comparator.comparing(VendorFileVo::getSignDate)) + .orElse(null); + + // 没有有效的评价表的评价日期 + if (latestFile == null) { + state.accept("未发现有效的评价表"); + // 返回最早的合同日期 + ContractVo firstContract = contractList.stream() + .filter(v -> v.getSetupDate() != null && !v.getSetupDate().isBefore(miniContractDate)) + .min(Comparator.comparing(ContractVo::getSetupDate)) + .orElse(null); + if (firstContract == null) { + state.accept("最早的合同不存在?"); + return null; + } + + LocalDate setupDate = firstContract.getSetupDate(); + state.accept("依据合同 " + firstContract.getCode() + " 的日期 " + setupDate + " 推算"); + return SpringApp.getBean(HolidayService.class).adjustToWorkDay(setupDate.plusDays(-7)); + } + + // 检查失效日期起的第一个合同 + LocalDate nextInValidDate = latestFile.getSignDate().plusYears(1); + File file = new File(latestFile.getFilePath()); + state.accept("依据 " + file.getName() + " 的失效期 " + nextInValidDate + " 检索合同"); + List matchedContracts = contractList.stream() + .filter(v -> v.getSetupDate().isAfter(nextInValidDate)).toList(); + // 没有在失效日期后的合同时,使用失效日期 + if (matchedContracts.isEmpty()) { + state.accept("未发现失效期 " + nextInValidDate + " 后的合同"); + return null; + } + state.accept("发现匹配合同 " + matchedContracts.size() + " 个"); + + // 按时间取最早一个 + ContractVo firstContract = matchedContracts.stream() + .min(Comparator.comparing(ContractVo::getSetupDate)) + .orElse(null); + LocalDate setupDate = firstContract.getSetupDate(); + state.accept("匹配失效期 " + nextInValidDate + " 后的第一个合同 " + firstContract.getCode()); + state.accept("依据合同 " + firstContract.getCode() + " 的日期 " + setupDate + " 推算"); + return SpringApp.getBean(HolidayService.class).adjustToWorkDay(setupDate.plusDays(-7)); } public void verify(VendorVo companyVendor, LocalDate verifyDate, MessageHolder holder) { diff --git a/common/pom.xml b/common/pom.xml index 2f9202e..d000796 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -6,12 +6,12 @@ com.ecep.contract Contract-Manager - 0.0.100-SNAPSHOT + 0.0.101-SNAPSHOT com.ecep.contract common - 0.0.100-SNAPSHOT + 0.0.101-SNAPSHOT ${java.version} diff --git a/common/src/main/java/com/ecep/contract/model/HolidayTable.java b/common/src/main/java/com/ecep/contract/model/HolidayTable.java index 1c29188..8e233b0 100644 --- a/common/src/main/java/com/ecep/contract/model/HolidayTable.java +++ b/common/src/main/java/com/ecep/contract/model/HolidayTable.java @@ -7,6 +7,7 @@ import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; import com.ecep.contract.util.HibernateProxyUtils; +import com.ecep.contract.vo.HolidayTableVo; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -21,7 +22,7 @@ import lombok.ToString; @Entity @Table(name = "HOLIDAY_TABLE") @ToString -public class HolidayTable { +public class HolidayTable implements Voable { @Id @Column(name = "ID", nullable = false) @JdbcTypeCode(SqlTypes.DATE) @@ -47,4 +48,12 @@ public class HolidayTable { public final int hashCode() { return HibernateProxyUtils.hashCode(this); } + + @Override + public HolidayTableVo toVo() { + HolidayTableVo vo = new HolidayTableVo(); + vo.setId(this.id); + vo.setHoliday(this.holiday); + return vo; + } } diff --git a/common/src/main/java/com/ecep/contract/vo/HolidayTableVo.java b/common/src/main/java/com/ecep/contract/vo/HolidayTableVo.java new file mode 100644 index 0000000..b2f4207 --- /dev/null +++ b/common/src/main/java/com/ecep/contract/vo/HolidayTableVo.java @@ -0,0 +1,37 @@ +package com.ecep.contract.vo; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Objects; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 节假日表视图对象 + * + * @author System + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class HolidayTableVo implements Serializable { + private static final long serialVersionUID = 1L; + + private LocalDate id; + private boolean holiday; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HolidayTableVo that = (HolidayTableVo) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index a20d2e4..ec11c30 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.ecep.contract Contract-Manager - 0.0.100-SNAPSHOT + 0.0.101-SNAPSHOT pom server diff --git a/server/pom.xml b/server/pom.xml index a7890ed..8ad32f1 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -6,12 +6,12 @@ com.ecep.contract Contract-Manager - 0.0.100-SNAPSHOT + 0.0.101-SNAPSHOT com.ecep.contract server - 0.0.100-SNAPSHOT + 0.0.101-SNAPSHOT ${java.version} @@ -22,7 +22,7 @@ com.ecep.contract common - 0.0.100-SNAPSHOT + 0.0.101-SNAPSHOT org.springframework.boot diff --git a/server/src/main/java/com/ecep/contract/ds/company/service/CompanyBasicService.java b/server/src/main/java/com/ecep/contract/ds/company/service/CompanyBasicService.java index f3d398a..42b139e 100644 --- a/server/src/main/java/com/ecep/contract/ds/company/service/CompanyBasicService.java +++ b/server/src/main/java/com/ecep/contract/ds/company/service/CompanyBasicService.java @@ -21,7 +21,6 @@ import com.ecep.contract.CustomerFileType; import com.ecep.contract.VendorFileType; import com.ecep.contract.SpringApp; import com.ecep.contract.ds.company.CompanyFileUtils; -import com.ecep.contract.ds.other.repository.HolidayTableRepository; import com.ecep.contract.ds.company.model.Company; import com.ecep.contract.model.CompanyBasicFile; import com.ecep.contract.ds.customer.model.CompanyCustomerFile; @@ -46,11 +45,11 @@ public abstract class CompanyBasicService { date = date.plusDays(-2); } - HolidayTableRepository holidayTableRepository = SpringApp.getBean(HolidayTableRepository.class); - //TODO 跳过节假日 + HolidayService holidayTableRepository = SpringApp.getBean(HolidayService.class); + // TODO 跳过节假日 int tryDays = 15; while (tryDays-- > 0) { - HolidayTable holidayTable = holidayTableRepository.findById(date).orElse(null); + HolidayTable holidayTable = holidayTableRepository.getById(date); if (holidayTable == null) { // 没有节假日定义,检查是否是工作日 DayOfWeek dayOfWeek = date.getDayOfWeek(); @@ -78,7 +77,6 @@ public abstract class CompanyBasicService { @Autowired protected CompanyService companyService; - protected boolean isEditableFile(String fileName) { return FileUtils.withExtensions(fileName, FileUtils.XLS, FileUtils.XLSX, @@ -93,7 +91,8 @@ public abstract class CompanyBasicService { public abstract , ID> void deleteFile(F file); - protected , ID> boolean fetchDbFiles(List dbFiles, Map map, Consumer status) { + protected , ID> boolean fetchDbFiles(List dbFiles, Map map, + Consumer status) { boolean modified = false; List editFiles = new ArrayList<>(); // 排除掉数据库中重复的 @@ -166,8 +165,8 @@ public abstract class CompanyBasicService { * @see CompanyCustomerFile * @see CustomerFileType */ - protected abstract > boolean fillFileAsDefaultType(F dbFile, File file, Consumer status); - + protected abstract > boolean fillFileAsDefaultType(F dbFile, File file, + Consumer status); protected void moveFileToCompany(Company company, List needMoveToCompanyPath) { if (needMoveToCompanyPath.isEmpty()) { @@ -219,8 +218,7 @@ public abstract class CompanyBasicService { List needMoveToCompanyPath, List retrieveFiles, Map map, - Consumer status - ) { + Consumer status) { if (!StringUtils.hasText(path)) { return; } @@ -281,7 +279,8 @@ public abstract class CompanyBasicService { * @param 类型类 * @param 文件类 */ - protected abstract > F fillFileType(File file, List fileList, Consumer status); + protected abstract > F fillFileType(File file, List fileList, + Consumer status); /** * @param customerFile 文件对象 @@ -292,7 +291,8 @@ public abstract class CompanyBasicService { * @param 文件类 * @return true 有修改 */ - protected > boolean fillFile(F customerFile, File file, List fileList, Consumer status) { + protected > boolean fillFile(F customerFile, File file, List fileList, + Consumer status) { String fileName = file.getName(); boolean modified = CompanyFileUtils.fillApplyDateAbsent(file, customerFile, F::getSignDate, F::setSignDate); // 评估表 @@ -320,7 +320,8 @@ public abstract class CompanyBasicService { * @param 文件类 * @return true:文件对象有修改,否则返回false */ - protected > boolean fillFileAsEvaluationFile(F customerFile, File file, List fileList, Consumer status) { + protected > boolean fillFileAsEvaluationFile(F customerFile, File file, + List fileList, Consumer status) { boolean modified = setFileTypeAsEvaluationForm(customerFile); String fileName = file.getName(); @@ -353,7 +354,8 @@ public abstract class CompanyBasicService { return modified; } - private > boolean useAsEditableFile(F customerFile, File file, List fileList, Consumer status) { + private > boolean useAsEditableFile(F customerFile, File file, List fileList, + Consumer status) { if (fileList == null) { return false; } @@ -403,7 +405,8 @@ public abstract class CompanyBasicService { return false; } - private > boolean useAsArchiveFile(F customerFile, File file, List fileList, Consumer status) { + private > boolean useAsArchiveFile(F customerFile, File file, List fileList, + Consumer status) { if (fileList == null) { return false; } @@ -449,7 +452,6 @@ public abstract class CompanyBasicService { return false; } - /** * 设置文件类型为表单文件 * @@ -468,5 +470,4 @@ public abstract class CompanyBasicService { */ protected abstract boolean isEvaluationFile(String fileName); - } diff --git a/server/src/main/java/com/ecep/contract/ds/company/service/HolidayService.java b/server/src/main/java/com/ecep/contract/ds/company/service/HolidayService.java new file mode 100644 index 0000000..d48b42d --- /dev/null +++ b/server/src/main/java/com/ecep/contract/ds/company/service/HolidayService.java @@ -0,0 +1,254 @@ +package com.ecep.contract.ds.company.service; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +import com.ecep.contract.IEntityService; +import com.ecep.contract.QueryService; +import com.ecep.contract.ds.other.repository.HolidayTableRepository; +import com.ecep.contract.model.HolidayTable; +import com.ecep.contract.service.VoableService; +import com.ecep.contract.vo.HolidayTableVo; +import com.fasterxml.jackson.databind.JsonNode; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import lombok.extern.slf4j.Slf4j; + +/** + * 节假日表服务类 + * + * @author System + */ +@Service +@CacheConfig(cacheNames = "HolidayTable") +@Slf4j +public class HolidayService implements IEntityService, QueryService, VoableService { + + @Autowired + private HolidayTableRepository holidayTableRepository; + + /** + * 调整日期到工作日 + * + * @param date 要调整的日期 + * @return 调整的日期 + */ + public LocalDate adjustToWorkDay(LocalDate date) { + if (date == null) { + return null; + } + + while (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY || isHoliday(date)) { + date = date.plusDays(1); + } + return date; + } + + /** + * 检查日期是否为节假日 + * + * @param date 日期 + * @return 是否为节假日 + */ + public boolean isHoliday(LocalDate date) { + if (date == null) { + return false; + } + + HolidayTable holidayTable = holidayTableRepository.findById(date).orElse(null); + return holidayTable != null && holidayTable.isHoliday(); + } + + @Override + public HolidayTable getById(Integer id) { + throw new UnsupportedOperationException("HolidayTable uses LocalDate as ID, please use getById(LocalDate id) method instead"); + } + + /** + * 根据LocalDate类型的ID查询实体 + * @param id 实体ID + * @return 实体对象 + */ + public HolidayTable getById(LocalDate id) { + if (id == null) { + return null; + } + return holidayTableRepository.findById(id).orElse(null); + } + + @Override + public HolidayTable save(HolidayTable entity) { + if (entity == null) { + return null; + } + return holidayTableRepository.save(entity); + } + + @Override + public void delete(HolidayTable entity) { + if (entity != null && entity.getId() != null) { + holidayTableRepository.deleteById(entity.getId()); + } + } + + /** + * 根据LocalDate类型的ID删除实体 + * @param id 实体ID + */ + public void deleteById(LocalDate id) { + if (id != null) { + holidayTableRepository.deleteById(id); + } + } + + /** + * 获取所有节假日 + * @param pageable 分页参数 + * @return 分页结果 + */ + public Page findAll(Pageable pageable) { + return holidayTableRepository.findAll(pageable); + } + + @Override + public Page findAll(Specification spec, Pageable pageable) { + // 由于HolidayTableRepository不支持Specification查询,返回所有节假日 + return findAll(pageable); + } + + @Override + public Specification getSpecification(String searchText) { + // 实现根据搜索文本构建规格化查询 + return (Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) -> { + List predicates = new ArrayList<>(); + + if (searchText != null && !searchText.trim().isEmpty()) { + try { + // 尝试将搜索文本解析为日期 + LocalDate date = LocalDate.parse(searchText.trim()); + predicates.add(criteriaBuilder.equal(root.get("id"), date)); + } catch (DateTimeParseException e) { + // 如果不是日期格式,尝试其他搜索方式 + // 由于HolidayTable只有id和holiday字段,这里无法进行其他字段的模糊搜索 + log.warn("Search text '{}' is not a valid date format", searchText); + } + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + } + + @Override + public HolidayTableVo findById(Integer id) { + throw new UnsupportedOperationException("HolidayTable uses LocalDate as ID, please use findById(Object id) instead"); + } + + @Cacheable(key = "#id", unless = "#result == null") + public HolidayTableVo findById(LocalDate id) { + HolidayTable entity = getById(id); + return entity != null ? entity.toVo() : null; + } + + @Override + public Page findAll(JsonNode paramsNode, Pageable pageable) { + // 实现根据JSON参数过滤节假日 + Page entityPage; + + if (paramsNode == null || paramsNode.isEmpty()) { + // 如果没有参数,返回所有节假日 + entityPage = findAll(pageable); + } else { + // 处理参数过滤 + Boolean isHoliday = null; + LocalDate startDate = null; + LocalDate endDate = null; + + if (paramsNode.has("isHoliday")) { + isHoliday = paramsNode.get("isHoliday").asBoolean(); + } + + if (paramsNode.has("startDate")) { + try { + startDate = LocalDate.parse(paramsNode.get("startDate").asText()); + } catch (Exception e) { + log.warn("Failed to parse startDate: {}", e.getMessage()); + } + } + + if (paramsNode.has("endDate")) { + try { + endDate = LocalDate.parse(paramsNode.get("endDate").asText()); + } catch (Exception e) { + log.warn("Failed to parse endDate: {}", e.getMessage()); + } + } + + // 根据参数组合选择合适的查询方法 + if (isHoliday != null && startDate != null && endDate != null) { + // 组合条件:是否为节假日 + 日期范围 + entityPage = holidayTableRepository.findByHolidayAndIdBetween(isHoliday, startDate, endDate, pageable); + } else if (isHoliday != null) { + // 单个条件:是否为节假日 + entityPage = holidayTableRepository.findByHoliday(isHoliday, pageable); + } else if (startDate != null && endDate != null) { + // 单个条件:日期范围 + entityPage = holidayTableRepository.findByIdBetween(startDate, endDate, pageable); + } else { + // 不满足上述条件,返回所有节假日 + entityPage = findAll(pageable); + } + } + + return entityPage.map(HolidayTable::toVo); + } + + @Override + public long count(JsonNode paramsNode) { + // 简单实现,返回所有节假日数量 + return holidayTableRepository.count(); + } + + @Override + public void updateByVo(HolidayTable model, HolidayTableVo vo) { + if (model == null || vo == null) { + return; + } + + // 更新实体属性 + model.setHoliday(vo.isHoliday()); + save(model); + } + + /** + * 创建节假日 + * + * @param vo 节假日VO对象 + * @return 创建后的节假日VO对象 + */ + public HolidayTableVo createByVo(HolidayTableVo vo) { + if (vo == null || vo.getId() == null) { + throw new IllegalArgumentException("HolidayTableVo or ID cannot be null"); + } + + HolidayTable entity = new HolidayTable(); + entity.setId(vo.getId()); + entity.setHoliday(vo.isHoliday()); + + HolidayTable savedEntity = save(entity); + return savedEntity.toVo(); + } +} diff --git a/server/src/main/java/com/ecep/contract/ds/other/repository/HolidayTableRepository.java b/server/src/main/java/com/ecep/contract/ds/other/repository/HolidayTableRepository.java index b1938e4..d174d32 100644 --- a/server/src/main/java/com/ecep/contract/ds/other/repository/HolidayTableRepository.java +++ b/server/src/main/java/com/ecep/contract/ds/other/repository/HolidayTableRepository.java @@ -1,14 +1,47 @@ package com.ecep.contract.ds.other.repository; +import java.time.LocalDate; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.stereotype.Repository; import com.ecep.contract.model.HolidayTable; -import java.time.LocalDate; - @Repository -public interface HolidayTableRepository // curd - extends CrudRepository, PagingAndSortingRepository { +public interface HolidayTableRepository // curd + extends CrudRepository, PagingAndSortingRepository { + + /** + * 根据是否为节假日过滤 + * + * @param isHoliday 是否为节假日 + * @param pageable 分页参数 + * @return 分页结果 + */ + Page findByHoliday(boolean isHoliday, Pageable pageable); + + /** + * 根据日期范围过滤 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param pageable 分页参数 + * @return 分页结果 + */ + Page findByIdBetween(LocalDate startDate, LocalDate endDate, Pageable pageable); + + /** + * 根据是否为节假日和日期范围组合过滤 + * + * @param isHoliday 是否为节假日 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param pageable 分页参数 + * @return 分页结果 + */ + Page findByHolidayAndIdBetween(boolean isHoliday, LocalDate startDate, LocalDate endDate, + Pageable pageable); }