Compare commits

..

40 Commits

Author SHA1 Message Date
643338f4b0 refactor(WebSocketServerCallbackManager): 使用常量替换硬编码的方法名
将硬编码的方法名替换为ServiceConstant中定义的常量,提高代码可维护性和可读性
2025-12-17 17:01:08 +08:00
880671a5a9 refactor: 使用常量替换硬编码方法名并优化参数构建
- 在ServiceConstant中添加常用方法名常量
- 使用ParamUtils重构多处参数构建逻辑
- 统一QueryService中的方法名调用为常量
- 修复CompanyOldNameService中的字段别名问题
2025-12-17 16:56:45 +08:00
4e738bea3c refactor: 移除冗余方法并优化查询逻辑
移除多个服务类中重复的findAll方法实现,统一使用父类方法
优化QueryService的findOneByProperty实现,提高可读性
为EntityService添加aliasFor方法支持字段别名
修复ContractVerifyWindowController的消息处理逻辑
添加查看验证状态的功能菜单项
2025-12-15 00:04:32 +08:00
3cf3a717be refactor(service): 移除冗余的getById方法
多个服务类中移除了重复的getById方法实现
2025-12-14 17:12:36 +08:00
e8c8305f40 refactor(vendor): 移除冗余方法并统一继承EntityService
重构供应商相关服务类,移除重复的getById等方法,统一继承EntityService基类
更新文档中的服务类检查状态
2025-12-14 16:36:11 +08:00
be63ff62a4 refactor(service): 重构多个服务类继承EntityService基类
重构多个服务类使其继承EntityService基类,统一实现基础CRUD操作
移除重复的findAll、getById等方法实现
添加缓存注解确保一致性
更新服务类文档说明继承关系
2025-12-14 15:54:21 +08:00
18057a657e refactor(service): 重构服务层代码,统一继承EntityService基类
refactor(repository): 将JpaRepository替换为自定义的MyRepository接口
refactor(tasker): 将findById方法更名为getById以提高一致性
docs: 更新server_entity_services.md文档中的服务实现状态
2025-12-14 14:47:51 +08:00
db07befffe refactor: 统一方法命名,提高代码一致性
将 newContractGroup 重命名为 createNewEntity 以符合命名规范
将 findById 重命名为 getById 以保持方法命名一致性
2025-12-14 13:14:19 +08:00
94030a5a39 Merge branch 'main' of http://gitea.ecctrl.com/songqq/contract-manager 2025-12-14 12:59:24 +08:00
7e59f2c17e refactor(服务层): 重构多个服务类继承EntityService基类
统一服务层实现,将PurchaseBillVoucherItemService、PurchaseOrdersService等服务类重构为继承EntityService基类
移除重复代码,实现通用CRUD操作
更新文档标记服务可用性
优化查询规范和缓存配置
2025-12-14 12:58:56 +08:00
c8b0d57f22 feat(EntityService): 实现 LIKE 和小于操作符的查询逻辑
添加 LIKE 操作符支持包含、开头和结尾匹配模式
实现小于操作符的比较逻辑,支持嵌套属性路径
2025-12-13 21:17:54 +08:00
6eebdb1744 refactor(service): 重构服务类继承EntityService基类
重构多个服务类使其继承EntityService基类,统一实现通用CRUD操作
移除重复代码,提取公共逻辑到基类中
更新缓存注解和查询规范实现
添加必要的重写方法如getRepository和createNewEntity
2025-12-13 20:32:06 +08:00
9dc90507cb refactor(vendor): 重构服务类继承EntityService基类
将多个vendor相关的服务类重构为继承自EntityService基类,统一实现通用CRUD操作
移除重复代码,简化各服务类的实现
添加必要的重写方法如getRepository和createNewEntity
2025-12-13 18:54:15 +08:00
0c26d329c6 refactor(service): 重构服务类继承EntityService以统一实现
将BankService、EmployeeService、CustomerCatalogService、InventoryService和DepartmentService重构为继承EntityService基类,统一实现通用CRUD操作和查询逻辑。移除重复代码,提高代码复用性和可维护性。
2025-12-13 18:17:53 +08:00
e3661630fe refactor: 将getSpecification重命名为getSearchSpecification以提高方法命名清晰度 2025-12-13 17:59:16 +08:00
5d6fb961b6 feat(查询): 实现通用过滤条件构建与解析功能
新增参数常量定义和查询条件构建工具,支持复杂条件组合
重构EntityService基类以支持通用过滤条件解析
优化SpecificationUtils工具类,增加搜索文本处理方法
2025-12-13 16:48:54 +08:00
72edb07798 feat: 添加合同全量同步任务和合格供方名录路径生成功能
refactor: 重构查询服务使用ParamConstant替换ServiceConstant
style: 清理无用代码和注释
fix: 修复CompanyCustomerEvaluationFormFileService查询方法
docs: 更新CloudYuVo和CompanyBankAccountVo字段注释
2025-12-13 11:11:37 +08:00
330418cfd6 feat(contract): 添加ContractFileTypeGroup枚举和ParamConstant类
添加合同模块的基础类型定义,为后续功能开发提供支持
2025-12-13 11:10:59 +08:00
c10bd369c0 up 2025-11-26 16:10:17 +08:00
f0e85c5a18 docs: 添加项目文档和架构设计文件
删除旧的package.json文件
添加天眼查下载信用报告文档
添加项目文档总览、架构设计、API文档、开发指南和数据库设计文档
2025-11-26 16:10:01 +08:00
a784438e97 feat: 实现员工同步任务的WebSocket支持及合同名称锁定功能
- 为EmployeesSyncTask添加WebSocket客户端和服务端支持,实现实时任务进度反馈
- 新增合同名称锁定功能,防止误修改重要合同名称
- 优化SmbFileService的连接异常处理,提高稳定性
- 重构ContractFilesRebuildTasker的任务执行逻辑,改进错误处理
- 更新tasker_mapper.json注册EmployeesSyncTask
- 添加相关任务文档和验收报告

修复WebSocketClientSession的任务完成状态处理问题
改进UITools中任务执行的线程管理
优化DepartmentService的findByCode方法返回类型
2025-11-20 16:26:34 +08:00
02afa189f8 feat(contract): 新增合同余额功能及重构文件管理
重构合同文件管理逻辑,增加错误处理和日志记录
新增ContractBalance实体、Repository和VO类
完善Voable接口文档和实现规范
更新项目架构文档和数据库设计
修复SmbFileService的连接问题
移动合同相关TabSkin类到contract包
添加合同文件重建任务的WebSocket支持
2025-11-19 00:50:16 +08:00
87290f15b0 feat(SMB): 重构SMB文件服务支持多服务器配置和连接池优化
重构SmbFileService以支持多服务器配置,引入连接池和会话池管理机制。主要变更包括:
1. 实现基于主机的多服务器认证配置
2. 新增连接池和会话池管理,提高连接复用率
3. 添加定时清理空闲连接和会话的功能
4. 优化异常处理和重试机制
5. 改进日志记录和资源释放

同时更新相关配置文件和应用属性以支持新功能:
1. 修改application.properties支持多服务器SMB配置
2. 增强SmbConfig类以管理多服务器配置
3. 添加任务映射到tasker_mapper.json
4. 新增客户端和服务端任务规则文档
2025-11-17 12:55:31 +08:00
e761990ebf feat: 实现SMB文件服务并优化合同文件管理
- 新增SmbFileService服务类,支持SMB/CIFS协议的文件操作
- 修改合同文件管理逻辑,支持SMB路径检查与目录创建
- 优化BankTableCell实现工厂模式并更新相关文档
- 调整Redis配置并添加连接测试
- 修复合同发票视图模型的时间处理问题
- 更新项目版本至0.0.134-SNAPSHOT
2025-11-12 16:32:03 +08:00
1cb0edbd07 build: 添加客户端应用配置文件 2025-11-12 16:31:35 +08:00
dd49c3927a feat(客户评估): 实现客户评估表单的搜索和显示功能
添加客户评估表单文件的搜索功能,支持按公司ID和搜索文本查询
新增CompanyCustomerEvaluationFormFileStringConverter用于文件显示转换
优化自动完成功能,支持自定义搜索逻辑
移除不必要的文件路径属性绑定
重构客户评估表单窗口控制器继承结构
2025-10-18 01:36:58 +08:00
7d4961dae4 feat: 更新项目版本至0.0.129-SNAPSHOT并修复多个问题
fix(ProjectIndustryService): 添加getStringConverter方法
fix(AbstContractRepairTasker): 初始化repository修复同步问题
fix(ProjectTabSkinBase): 处理saleTypeField空值情况
refactor(ProjectTypeService): 优化stringConverter初始化
feat(ProjectTypeStringConverter): 修改toString方法显示编码和名称
fix(CustomerCtx): 初始化repository修复客户更新问题
feat(ProjectCostWindowController): 根据版本控制审批标签显示
fix(DeliverySignMethodService): 添加saleType查询条件
feat: 添加DeliverySignMethodStringConverter
fix(SalesOrderCtx): 使用getRepository修复查询问题
fix(VendorCtx): 初始化repository修复供应商更新问题
refactor(CompanyTabSkinContract): 优化员工表格单元格创建
feat(DeliverySignMethodService): 添加findByCode和findByName方法
refactor(AbstEntityController): 优化视图模型更新逻辑
style(project.fxml): 调整界面布局和字段显示
2025-10-17 15:39:41 +08:00
235269f86f refactor(converter): 重构StringConverter实现并统一处理null值
重构各实体类的StringConverter实现,移除EntityStringConverter基类,改为直接实现StringConverter接口
在各Service中提供对应的StringConverter实例,统一处理null值情况
更新ComboBoxUtils使用Service提供的StringConverter
2025-10-17 00:27:01 +08:00
0b45f6eef2 refactor: 优化上下文对象初始化方式
将InventoryCtx、VendorCtx和CustomerCtx的初始化方式改为直接通过构造函数传入上下文对象,简化代码并提高可读性
2025-10-16 23:53:35 +08:00
22ab2c7bdf refactor(u8上下文): 重构上下文类初始化方式,使用BeanContext传递依赖
移除from方法,改为通过构造函数注入BeanContext
统一使用getCachedBean获取服务实例
添加无参构造函数和带BeanContext参数的构造函数
优化代码结构,移除无用导入
2025-10-16 18:52:25 +08:00
eea4d93ae1 feat(资金计划): 完善资金计划功能并优化界面显示
- 在ProjectFundPlanService中添加payWay和payDate字段映射
- 为资金计划添加项目关联处理
- 优化付款计划表格的列名和显示
- 重构ProjectTabSkinFundPlan的service获取逻辑
2025-10-16 18:25:28 +08:00
71a358fa77 feat(contract): 新增合同发票管理功能
实现合同发票的增删改查功能,包括:
1. 添加ContractInvoiceVo实体类及相关ViewModel
2. 创建合同发票数据库表CONTRACT_INVOICE
3. 实现前后端发票管理服务ContractInvoiceService
4. 开发发票管理界面及标签页
5. 添加发票表格单元格组件
6. 完善销售订单表结构,增加客户联系人字段
7. 更新pom.xml版本至0.0.122-SNAPSHOT

修复销售订单界面搜索字段命名不一致问题
2025-10-16 15:47:33 +08:00
cf0a7e18ea feat(contract): 新增合同内容管理功能
- 添加合同内容窗口控制器及相关视图模型
- 实现合同内容基础信息标签页
- 添加金额计算功能
- 完善合同内容相关字段绑定
- 优化合同内容界面布局
2025-10-16 00:09:01 +08:00
d2e0dc4555 refactor(controller): 移除冗余的service注入,使用BeanContext统一管理
feat(sales-order): 补充SalesOrderVo缺失字段并完善相关功能
feat(sales-order): 添加客户、税率等字段到销售订单界面
refactor(tab-skin): 重构TabSkin基类,统一bean获取方式
fix(fxml): 修正controller包路径和字段绑定
feat(repository): 添加findAllByOrder方法优化查询
2025-10-15 00:40:02 +08:00
1c1ff678a5 feat(contract): 添加采购订单和销售订单的合同标签页实现
实现采购订单和销售订单的合同标签页功能,包括表格列定义、单元格渲染和基本操作。采购订单标签页在合同为付款方式时启用,销售订单标签页在合同为收款方式时启用。两个标签页都支持双击行查看详情和刷新操作,销售订单标签页额外提供搜索功能。
2025-10-14 17:36:55 +08:00
5b3ab3ed00 refactor(util): 重构BeanContext接口及相关实现
将ContextUtils重命名为BeanContext,并统一客户端和服务端的实现
添加DefaultBeanContext作为默认实现
优化Inventory同步任务逻辑,支持WebSocket远程调用
更新tasker_mapper.json添加新的任务映射
移除未使用的syncInventory方法
2025-10-12 22:39:32 +08:00
59d78619da refactor(CloudRkService): 优化导入语句顺序并移除未使用的导入
移除未使用的Bank类导入,并按照标准顺序重新组织导入语句,提高代码可读性和维护性
2025-10-12 12:52:47 +08:00
86e18632aa feat(采购订单): 添加合同条目关联及税率绑定功能
新增采购订单条目与合同条目的关联字段,实现税率和税率锁定的UI绑定
优化采购订单同步逻辑,支持从U8系统获取更多字段信息
调整界面文本显示,修复部分字段绑定问题
2025-10-12 12:49:18 +08:00
ddd9dad945 refactor: 优化采购订单相关功能及代码结构
重构采购订单模块,包括以下改进:
1. 移除PurchaseOrderItemVo中冗余字段
2. 在ContractCtx中添加调试日志
3. 修改InventoryTabSkinContracts和PurchaseOrderTabSkinBillVoucher中的equals比较逻辑
4. 调整PurchaseOrderVo的税率字段类型并添加锁定标志
5. 修正FXML文件中的控制器路径
6. 优化InventoryStringConverter的toString方法格式
7. 在PurchaseBillVoucherItemService中添加凭证查询条件
8. 改进ContractRepairTasker的进度更新逻辑
9. 修复AbstContractRepairTasker中的子合同同步问题
10. 优化PurchaseOrderTabSkinItems的表格列显示
11. 添加InventoryCatalogStringConverter及相关缓存支持
12. 完善PurchaseBillVoucherService的查询逻辑
13. 增强ContractService的库存查询功能
14. 改进PurchaseOrderItemService的合同项查询逻辑
15. 为InventoryService添加缓存支持
16. 优化ContractTabSkinPurchaseOrders的员工列显示
17. 改进ContractTabSkinItemsV2的数量显示逻辑
18. 重构PurchaseOrderViewModel的数据绑定逻辑
2025-10-12 00:52:01 +08:00
333feb0d5a feat: 添加按ID查询的仓库方法并优化合同同步逻辑
添加了多个仓库的按ID查询方法,如findByCompanyId和findAllByContractId
优化了合同同步任务,使用ContractVo替代Contract减少数据库访问
重构了部分服务方法,支持通过ID直接操作,提升性能
修复了ProjectCostService中字段映射错误的问题
2025-10-11 17:56:02 +08:00
421 changed files with 19065 additions and 6032 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
PLAYWRIGHT_MCP_EXTENSION_TOKEN=TB7T39NhEbruyS7L-E7RXIGYk39PVK7eu1h-WP8M1Cg

4
.gitignore vendored
View File

@@ -35,4 +35,6 @@ build/
### VS Code ###
.vscode/
/config.properties
/config.properties
node_modules
node_modules

View File

@@ -154,4 +154,70 @@ public class [业务]WindowController extends AbstEntityController<[Vo类型], [
// 窗口显示后的逻辑
}
}
```
```
## 13. TableCell 工厂模式规范
### 13.1 工厂方法命名
- TableCell类应提供静态工厂方法`forTableColumn`,用于创建单元格工厂
- 方法命名必须为`forTableColumn`,保持一致性
### 13.2 工厂方法参数
- 工厂方法应接收服务层参数,如`BankService`
- 参数类型应与TableCell构造函数所需的服务类型一致
### 13.3 工厂方法返回类型
- 返回类型应为`Callback<javafx.scene.control.TableColumn<V, T>, javafx.scene.control.TableCell<V, T>>`
- 其中`V`为ViewModel类型`T`为单元格值类型
### 13.4 工厂方法实现
- 在工厂方法内部创建并返回TableCell实例
- 使用泛型参数确保类型安全
### 13.5 使用方式
- 在设置表格列的单元格工厂时应调用TableCell的静态工厂方法
- 避免直接使用`new TableCell<>(service)`的方式创建实例
### 13.6 示例代码
```java
/**
* 银行单元格
*/
@NoArgsConstructor
public class BankTableCell<T> extends AsyncUpdateTableCell<T, Integer, BankVo> {
/**
* 创建单元格工厂
*
* @param bankService 银行服务
* @return 单元格工厂
*/
public static <V> Callback<javafx.scene.control.TableColumn<V, Integer>, javafx.scene.control.TableCell<V, Integer>> forTableColumn(
BankService bankService) {
return param -> new BankTableCell<V>(bankService);
}
public BankTableCell(BankService service) {
setService(service);
}
@Override
protected BankService getServiceBean() {
return getBean(BankService.class);
}
}
```
### 13.7 使用示例
```java
// 推荐方式:使用工厂方法
column.setCellFactory(BankTableCell.forTableColumn(getBankService()));
// 不推荐方式:直接实例化
// column.setCellFactory(param -> new BankTableCell<>(getBankService()));

View File

@@ -1,83 +1,133 @@
# 客户端 Service 类规则
# Client Service 实现编写指南
## 1. 目录结构
- 所有客户端 Service 类位于 `client/src/main/java/com/ecep/contract/service/` 目录下
- 按业务领域组织,直接放置在 service 包下,不进行子包划分
- 服务类命名与实体类一一对应
## 📋 概述
## 2. 命名规范
- 服务类命名格式为:`[实体名称]Service.java`
- 例如:`CompanyService.java``ContractService.java``ProjectService.java`
- 基础服务接口命名为:`IEntityService.java``ViewModelService.java`
- 泛型基础服务类命名为:`QueryService.java`
本指南总结 Client 模块 Service 层的实现经验,用于指导后续 Service 的编写。本指南基于 Contract-Manager 项目中已实现的 Service 模式整理。
## 3. 继承关系
- 业务服务类通常继承自泛型基础服务类 `QueryService<T, TV>`
- `T` 表示 VO 类型(实现了 IdentityEntity 接口)
- `TV` 表示 ViewModel 类型(实现了 IdentityViewModel<T> 接口)
- `QueryService` 实现了 `ViewModelService<T, TV>` 接口
- `ViewModelService` 继承了 `IEntityService<T>` 接口
- 特定场景下可以不继承 `QueryService`,直接实现所需接口或创建独立服务类
---
## 🏗️ 基础架构
### 1. 继承层次结构
```java
// Service 接口定义
public interface IEntityService<T> {
T findById(Integer id);
T save(T entity);
void delete(T entity);
List<T> findAll();
Page<T> findAll(Map<String, Object> params, Pageable pageable);
StringConverter<T> getStringConverter();
}
// 基础 Service 实现
public abstract class QueryService<T extends IdentityEntity, TV extends IdentityViewModel<T>>
implements ViewModelService<T, TV> {
// 核心实现
}
// 具体业务 Service
@Service
@CacheConfig(cacheNames = "company")
public class CompanyService extends QueryService<CompanyVo, CompanyViewModel> {
// 业务方法实现
@CacheConfig(cacheNames = "business")
public class XxxService extends QueryService<XxxVo, XxxViewModel> {
// 业务特定实现
}
```
## 4. 注解使用
- **@Service**:标记为 Spring 服务组件,使其可被自动发现和注入
- **@CacheConfig**:配置缓存名称,通常与服务类名对应
- **@Cacheable**标记方法结果可缓存需指定缓存键key
- **@CacheEvict**:标记方法执行后清除缓存,可指定缓存键或清除所有
- **@Caching**:组合多个缓存操作(如同时清除多个缓存条目)
- **@Autowired**:用于自动注入依赖的其他服务
### 2. 核心特性
- **泛型支持**`QueryService` 使用泛型处理不同类型的 Vo 和 ViewModel
- **WebSocket 通信**:通过 `WebSocketClientService` 与 Server 端通信
- **异步处理**:使用 `CompletableFuture` 实现异步操作
- **缓存机制**:集成 Spring Cache 支持多级缓存
- **错误处理**:统一的异常处理和日志记录
---
## 📝 Service 编写规范
### 1. 类声明和注解
```java
@Service
@CacheConfig(cacheNames = "contract")
public class ContractService extends QueryService<ContractVo, ContractViewModel> {
@Autowired
private SysConfService confService;
@Cacheable(key = "#p0")
public ContractVo findById(Integer id) {
return super.findById(id);
}
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "'code-'+#p0.code")
})
public ContractVo save(ContractVo contract) {
return super.save(contract);
}
@Service // Spring 组件注解
@CacheConfig(cacheNames = "xxx") // 缓存配置xxx为业务域名称
public class XxxService extends QueryService<XxxVo, XxxViewModel> {
// Service 实现
}
```
## 5. 缓存机制
- 每个服务类应有独立的缓存名称空间
- 缓存键key应具有唯一性通常使用 ID、代码或名称等唯一标识
- 保存和删除操作时应清除相关缓存,保持数据一致性
- 可使用 SpEL 表达式动态生成缓存键
- 频繁查询的数据应考虑缓存,提高性能
### 2. 缓存策略
## 6. 异步调用机制
- 使用 `async()` 方法进行异步远程调用
- 方法参数通常包括:方法名、参数值、参数类型列表
- 使用 `CompletableFuture` 处理异步结果
- 使用 `handle()` 方法处理响应和异常
- 远程调用异常应包装为 RuntimeException 并提供详细错误信息
#### 缓存注解使用
```java
@Cacheable(key = "#p0") // 按ID缓存
@Cacheable(key = "'code-'+#p0") // 按代码缓存
@Cacheable(key = "'name-'+#p0") // 按名称缓存
@CacheEvict(key = "#p0.id") // 删除时清除ID缓存
@CacheEvict(key = "'code-'+#p0.code") // 清除代码缓存
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "'code-'+#p0.code")
}) // 批量清除缓存
```
#### 缓存键设计原则
- **ID 缓存**`#p0`第一个参数通常是ID
- **代码缓存**`'code-'+#p0`(业务代码的缓存)
- **名称缓存**`'name-'+#p0`(业务名称的缓存)
- **关联缓存**`'company-'+#p0.id`(关联实体的缓存)
### 3. 核心方法实现
#### findById 方法
```java
@Cacheable(key = "#p0")
@Override
public XxxVo findById(Integer id) {
return super.findById(id); // 调用父类方法
}
```
#### save 方法(带缓存清除)
```java
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "'code-'+#p0.code")
})
@Override
public XxxVo save(XxxVo entity) {
return super.save(entity);
}
```
#### delete 方法(带缓存清除)
```java
@Caching(evict = {
@CacheEvict(key = "#p0.id"),
@CacheEvict(key = "'code-'+#p0.code")
})
@Override
public void delete(XxxVo entity) {
super.delete(entity);
}
```
---
## 🔄 异步通信模式
### 1. 基本异步调用
```java
@Cacheable(key = "'code-'+#p0")
public ContractVo findByCode(String code) {
// 异步调用示例
public XxxVo findByCode(String code) {
try {
return async("findByCode", code, String.class).handle((response, ex) -> {
if (ex != null) {
throw new RuntimeException("远程方法+findByCode+调用失败", ex);
throw new RuntimeException("远程方法 findByCode 调用失败", ex);
}
if (response != null) {
return updateValue(createNewEntity(), response);
@@ -85,62 +135,374 @@ public ContractVo findByCode(String code) {
return null;
}).get();
} catch (Exception e) {
throw new RuntimeException("查失败: " + code, e);
throw new RuntimeException("查找实体失败: " + code, e);
}
}
```
## 7. 基础方法实现
- 应实现 `IEntityService<T>` 接口定义的核心方法:
- `findById(Integer id)`:根据 ID 查询实体
- `save(T entity)`:保存实体
- `delete(T entity)`:删除实体
- `findAll()`:查询所有实体
- `findAll(Map<String, Object> params, Pageable pageable)`:条件分页查询
- `getStringConverter()`:获取类型转换器
- 通常通过继承 `QueryService` 来复用这些基础方法的实现
- 可根据业务需求重写或扩展基础方法
## 8. 业务方法规范
- 业务方法应与服务端对应,保持方法名和参数一致
- 方法命名应清晰表达其功能,如 `findByName`, `findByCode`
- 复杂业务逻辑应封装为独立方法
- 参数校验应在方法开始处进行
- 返回值类型应明确,避免使用过于泛化的类型
## 9. 类型转换器
- 实现 `getStringConverter()` 方法,返回对应的 StringConverter 实例
- 通常创建专用的 Converter 类,如 `CustomerCatalogStringConverter`
- 转换器实例应作为服务类的成员变量,避免重复创建
### 2. 复杂对象处理
```java
public class CustomerCatalogService extends QueryService<CustomerCatalogVo, CustomerCatalogViewModel> {
private final CustomerCatalogStringConverter stringConverter = new CustomerCatalogStringConverter(this);
@Override
public StringConverter<CustomerCatalogVo> getStringConverter() {
return stringConverter;
public List<XxxDetailVo> findDetailsByXxxId(Integer xxxId) {
try {
return async("findDetailsByXxxId", List.of(xxxId), List.of(Integer.class))
.handle((response, ex) -> {
if (ex != null) {
throw new RuntimeException("远程方法调用失败", ex);
}
if (response != null) {
try {
List<XxxDetailVo> content = new ArrayList<>();
for (JsonNode node : response) {
XxxDetailVo newEntity = new XxxDetailVo();
objectMapper.updateValue(newEntity, node);
content.add(newEntity);
}
return content;
} catch (Exception e) {
throw new RuntimeException(response.toString(), e);
}
}
return null;
}).get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
```
## 10. 错误处理
- 远程调用异常应捕获并包装为更具描述性的 RuntimeException
- 提供详细的错误信息,包括调用的方法名和参数
- 对于可预期的业务异常,可添加专门的处理逻辑
- 不推荐使用 printStackTrace(),应使用日志记录异常
---
## 11. 工具方法和辅助功能
- 通用功能可封装为工具方法
- 配置相关操作可通过 `SysConfService` 实现
- 文件路径处理应使用 `File` 类和相关工具方法
- 日期时间处理应使用 Java 8+ 的日期时间 API
## 💼 业务逻辑模式
## 12. 最佳实践
- 遵循单一职责原则,每个服务类专注于一个业务领域
- 优先使用继承和接口实现来复用代码
- 合理使用缓存提高性能,但注意缓存一致性
- 异步调用应正确处理异常和超时情况
- 服务类之间的依赖应通过 `@Autowired` 注入,避免硬编码
- 方法实现应简洁明了,复杂逻辑应拆分
- 为重要方法添加 JavaDoc 注释,说明其功能和参数含义
### 1. 文件系统集成
#### 路径管理
```java
@Autowired
private SysConfService confService;
private File basePath;
public File getBasePath() {
if (basePath == null) {
basePath = new File(confService.getString(Constant.KEY_BASE_PATH));
}
return basePath;
}
// 验证路径是否在基础目录内
public boolean checkXxxPathInBasePath(XxxVo xxx) {
if (!existsXxxPath(xxx)) {
return false;
}
File basePath = getBasePath();
if (basePath == null || !basePath.exists()) {
throw new IllegalArgumentException("基础目录不存在");
}
File path = new File(xxx.getPath());
return path.getAbsolutePath().startsWith(basePath.getAbsolutePath());
}
// 检查路径是否存在
public boolean existsXxxPath(XxxVo xxx) {
if (!StringUtils.hasText(xxx.getPath())) {
return false;
}
File path = new File(xxx.getPath());
return path.exists();
}
```
#### 目录创建
```java
public File makePath(XxxVo xxx) {
File basePath = getBasePath();
if (!basePath.exists()) {
holder.error("存储目录不存在:" + basePath.getAbsolutePath());
return null;
}
// 构建目录路径逻辑
String fileName = FileUtils.escapeFileName(xxx.getName());
File dir = new File(basePath, fileName);
if (!dir.exists()) {
if (!dir.mkdir()) {
holder.error("创建目录失败:" + dir.getAbsolutePath());
return null;
}
}
return dir;
}
```
### 2. 业务验证模式
#### 数据完整性验证
```java
public void verifyXxx(XxxVo xxx, LocalDate verifyDate, MessageHolder holder) {
// 检查关键字段
if (!StringUtils.hasText(xxx.getCode())) {
holder.error("编号异常:未设置");
return;
}
// 检查状态字段
String status = xxx.getStatus();
if (StringUtils.hasText(status) && status.contains("无效")) {
LocalDate end = xxx.getEndDate();
LocalDate begin = xxx.getBeginDate();
if (begin == null || end == null) {
holder.error("状态异常:" + status);
} else {
if (!MyDateTimeUtils.dateValidFilter(verifyDate, begin, end, 0)) {
holder.error("状态异常:" + status);
}
}
} else {
holder.error("状态异常:未设置");
}
}
```
#### 关联实体验证
```java
public boolean verifyAsXxxType(XxxVo xxx, LocalDate verifyDate, MessageHolder holder) {
boolean valid = false;
// 检查关联实体
RelatedVo related = relatedService.findById(xxx.getRelatedId());
if (related == null) {
holder.error("关联实体不存在");
valid = true;
}
// 检查关联数据
if (!StringUtils.hasText(xxx.getRelatedField())) {
holder.error("关联字段未设置");
valid = true;
}
// 检查业务规则
if (xxx.getStatus() == XxxStatus.INACTIVE) {
holder.error("业务状态异常:已停用");
valid = true;
}
return valid;
}
```
### 3. 复杂业务查询
#### 分页查询
```java
public List<XxxVo> findAllByXxxCondition(XxxCondition condition, LocalDate beginDate, LocalDate endDate) {
return findAll(ParamUtils.builder()
.equals("field1", condition.getField1())
.between("createDate", beginDate, endDate)
.equals("status", "ACTIVE")
.build(), Pageable.unpaged()).getContent();
}
```
#### 组合条件查询
```java
public Page<XxxVo> findAllWithComplexCondition(XxxQueryParam param) {
ParamUtils.ParamBuilder builder = ParamUtils.builder()
.equals("category", param.getCategory());
if (StringUtils.hasText(param.getName())) {
builder.like("name", "%" + param.getName() + "%");
}
if (param.getDateRange() != null) {
builder.between("createDate", param.getDateRange().getStart(),
param.getDateRange().getEnd());
}
return findAll(builder.build(), Pageable.ofSize(param.getPageSize()));
}
```
---
## 🔧 工具类和依赖注入
### 1. 常用工具类依赖
```java
// 常用注入
@Autowired
private SysConfService confService; // 系统配置服务
@Autowired
private RelatedService relatedService; // 关联实体服务
// 静态工具类使用
import com.ecep.contract.util.FileUtils; // 文件工具
import com.ecep.contract.util.ParamUtils; // 参数工具
import com.ecep.contract.util.MyStringUtils; // 字符串工具
```
### 2. 工具类使用示例
#### ParamUtils 构建查询条件
```java
// 构建复杂查询条件
Map<String, Object> params = ParamUtils.builder()
.equals("field1", value1)
.equals("field2", value2)
.like("name", "%" + keyword + "%")
.between("date", startDate, endDate)
.in("status", List.of("ACTIVE", "PENDING"))
.orderBy("createTime", "desc")
.build();
```
#### FileUtils 处理文件路径
```java
// 文件名转义
String safeFileName = FileUtils.escapeFileName(companyName);
// 获取父级前缀
String parentPrefix = FileUtils.getParentPrefixByDistrict(district);
```
---
## 📊 错误处理和日志
### 1. 异常处理模式
```java
// 查询异常处理
try {
return async("findByCode", code, String.class).handle((response, ex) -> {
if (ex != null) {
throw new RuntimeException("远程方法 findByCode 调用失败", ex);
}
if (response != null) {
return updateValue(createNewEntity(), response);
}
return null;
}).get();
} catch (Exception e) {
throw new RuntimeException("查找实体失败: " + code, e);
}
// 业务验证异常
public boolean businessMethod(XxxVo xxx) {
if (xxx == null) {
return false;
}
if (!xxx.isValid()) {
throw new IllegalArgumentException("实体数据无效");
}
return true;
}
```
### 2. 日志记录
```java
// 在 QueryService 中已集成日志
private static final Logger logger = LoggerFactory.getLogger(QueryService.class);
// 在业务方法中使用
public void deleteXxx(XxxVo xxx) {
try {
super.delete(xxx);
logger.info("删除实体成功 #{}", xxx.getId());
} catch (Exception e) {
logger.error("删除实体失败 #{}", xxx.getId(), e);
throw new RuntimeException("删除实体失败", e);
}
}
```
---
## 🧪 测试和验证
### 1. 方法验证检查点
- **输入参数验证**:检查空值、格式、范围
- **业务逻辑验证**:检查状态、关联、权限
- **文件系统验证**:检查路径、权限、空间
- **数据库验证**:检查连接、事务、一致性
### 2. 常见测试场景
```java
// 测试用例示例
@Test
public void testFindById() {
// 正常情况
XxxVo result = xxxService.findById(1);
assertNotNull(result);
// 异常情况
assertThrows(RuntimeException.class, () -> {
xxxService.findById(-1);
});
}
@Test
public void testSaveWithCache() {
XxxVo xxx = createTestXxx();
xxx.setCode("TEST001");
XxxVo saved = xxxService.save(xxx);
assertNotNull(saved.getId());
// 验证缓存
XxxVo cached = xxxService.findById(saved.getId());
assertEquals(saved.getId(), cached.getId());
}
```
---
## 🎯 最佳实践
### 1. 代码组织原则
- **单一职责**:每个 Service 专注特定的业务域
- **依赖注入**:合理使用 `@Autowired` 注入依赖
- **缓存策略**:为高频查询字段配置缓存
- **异常处理**:统一处理业务异常和系统异常
### 2. 性能优化建议
- **缓存配置**:合理设置缓存过期时间
- **分页查询**:避免大数据量一次性查询
- **异步处理**:使用异步调用提升响应速度
- **批量操作**:考虑批量处理减少网络开销
### 3. 可维护性提升
- **命名规范**:遵循项目统一的命名约定
- **注释文档**:为复杂业务逻辑添加注释
- **代码复用**:提取公共逻辑到工具类
- **版本兼容**:考虑前后端版本兼容性
---
## 📚 相关规范
- **Controller 规范**[client_controller_rules.md](.trae/rules/client_controller_rules.md)
- **转换器规范**[client_converter_rules.md](.trae/rules/client_converter_rules.md)
- **Task 规范**[client_task_rules.md](.trae/rules/client_task_rules.md)
- **Entity 规范**[entity_rules.md](.trae/rules/entity_rules.md)
- **VO 规范**[vo_rules.md](.trae/rules/vo_rules.md)
---
*本文档基于 Contract-Manager 项目现有 Service 实现模式总结,遵循项目既定的技术架构和编程规范。*
**文档版本**: v1.0.0
**最后更新**: 2024-12-19
**维护团队**: Contract Manager Development Team

View File

@@ -0,0 +1,419 @@
# 客户端Tasker实现WebSocketClientTasker接口规范
## 概述
本文档基于 `ContractRepairAllTasker` 实现 `WebSocketClientTasker` 接口的经验总结了客户端Tasker类升级为支持WebSocket通信的最佳实践和规范。
## WebSocketClientTasker接口介绍
`WebSocketClientTasker` 接口定义了通过WebSocket与服务器通信的任务的通用方法包括任务名称、消息更新、进度更新等核心功能。
### 核心方法
1. **getTaskName()** - 获取任务名称用于在WebSocket通信中标识任务
2. **updateMessage(Level, String)** - 更新任务执行过程中的消息
3. **updateTitle(String)** - 更新任务标题
4. **updateProgress(long, long)** - 更新任务进度
5. **cancelTask()** - 取消任务执行(默认实现为空)
6. **callRemoteTask(MessageHolder, Locale, Object...)** - 调用远程WebSocket任务
7. **callRemoteTaskAsync(MessageHolder, Locale, Object...)** - 异步调用远程WebSocket任务
8. **generateTaskId()** - 生成唯一的任务ID
### 典型实现模式概览
通过分析项目中的17个实现类我们发现了以下典型实现模式
1. **标准实现**继承Tasker<Object>并实现WebSocketClientTasker
2. **属性注入**:使用@Setter注解或手动设置属性传递任务参数
3. **Spring Bean获取**通过SpringApp.getBean()获取服务实例
4. **消息更新**:简洁的消息更新方式
5. **参数传递**通过callRemoteTask的可变参数传递任务所需数据
## Tasker实现WebSocketClientTasker最佳实践
### 1. 类定义和继承
```java
/**
* 任务类描述
* 用于通过WebSocket与服务器通信执行具体操作
*/
public class 任务类名 extends Tasker<Object> implements WebSocketClientTasker {
private static final Logger logger = LoggerFactory.getLogger(任务类名.class);
// 实现方法
}
```
### 2. 参数传递模式
#### 2.1 使用@Setter注解注入参数
```java
/**
* 更新供应商评价表任务
*/
public class CompanyVendorEvaluationFormUpdateTask extends Tasker<Object> implements WebSocketClientTasker {
private static final Logger logger = LoggerFactory.getLogger(CompanyVendorEvaluationFormUpdateTask.class);
@Setter
private VendorVo vendor;
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("更新供应商评价表");
return callRemoteTask(holder, getLocale(), vendor.getId());
}
}
```
#### 2.2 使用@Getter和@Setter注解
```java
/**
* 客户文件重建任务类
*/
public class CustomerRebuildFilesTasker extends Tasker<Object> implements WebSocketClientTasker {
@Getter
@Setter
private CustomerVo companyCustomer;
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("重建客户文件");
return callRemoteTask(holder, getLocale(), companyCustomer.getId());
}
}
```
### 2. 必要方法实现
#### 2.1 getTaskName()
```java
@Override
public String getTaskName() {
return "Task名称"; // 必须与服务器端对应Tasker类名匹配
}
```
**注意事项**
- 任务名称必须与服务器端对应的Tasker注册名tasker_mapper.json中的key保持一致
- 名称应简洁明了,反映任务的核心功能
#### 2.2 updateProgress()
updateProgress 方法重载为public用于外部调用更新进度
```java
@Override
public void updateProgress(long current, long total) {
super.updateProgress(current, total); // 调用父类方法更新进度
}
```
#### 2.3 updateTitle()
```java
@Override
public void updateTitle(String title) {
super.updateTitle(title); // 使用Tasker的updateTitle方法更新标题
}
```
#### 2.4 execute() 方法重写
**标准实现**
```java
@Override
protected Object execute(MessageHolder holder) throws Exception {
logger.info("开始执行任务描述");
updateTitle("任务标题");
// 调用远程任务,可选传入参数
Object result = callRemoteTask(holder, getLocale(), 可选参数...);
logger.info("任务执行完成");
return result;
}
```
**简洁实现**(适用于简单任务):
```java
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("更新供应商评价表");
return callRemoteTask(holder, getLocale(), vendor.getId());
}
```
**带消息更新的实现**
```java
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 设置任务标题
updateTitle("全量库存同步任务");
// 更新任务消息
updateMessage("开始执行全量库存同步...");
// 调用远程WebSocket任务
return callRemoteTask(holder, getLocale());
}
```
**关键步骤**
1. 记录任务开始日志(可选)
2. 设置任务标题
3. 可选:添加任务开始消息
4. 调用远程任务执行核心逻辑,传入必要参数
5. 记录任务完成日志(可选)
6. 返回执行结果
### 3. 日志记录
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// 在类中定义
private static final Logger logger = LoggerFactory.getLogger(任务类名.class);
// 在关键位置使用日志
logger.info("任务开始");
logger.warn("警告信息");
logger.error("错误信息", exception);
```
**日志使用建议**
- 复杂任务建议记录详细日志
- 简单任务可以简化或省略日志记录
- 确保异常情况下有适当的错误日志记录
### 4. 异常处理
`execute`方法中应妥善处理可能的异常并通过MessageHolder通知用户
```java
@Override
protected Object execute(MessageHolder holder) throws Exception {
try {
// 任务执行逻辑
} catch (Exception e) {
logger.error("任务执行失败", e);
holder.addMessage(Level.SEVERE, "任务执行失败: " + e.getMessage());
throw e; // 向上抛出异常,让框架处理
}
}
```
**异常处理策略**
- 对于简单任务,可以依赖框架的异常处理机制
- 对于复杂任务,建议添加自定义的异常处理逻辑
- 确保异常信息对用户友好且具有足够的调试信息
## 完整实现示例
### 示例1简单任务实现
```java
package com.ecep.contract.task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
/**
* 合同修复任务类
* 用于通过WebSocket与服务器通信执行合同数据修复操作
*/
public class ContractRepairAllTasker extends Tasker<Object> implements WebSocketClientTasker {
private static final Logger logger = LoggerFactory.getLogger(ContractRepairAllTasker.class);
@Override
public void updateProgress(long current, long total) {
super.updateProgress(current, total);
}
@Override
public void updateTitle(String title) {
// 使用Tasker的updateTitle方法更新标题
super.updateTitle(title);
}
@Override
public String getTaskName() {
return "ContractRepairAllTask"; // 与服务器端对应Tasker类名匹配
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
logger.info("开始执行合同修复任务");
updateTitle("合同数据修复");
Object result = callRemoteTask(holder, getLocale());
logger.info("合同修复任务执行完成");
return result;
}
}
```
### 示例2带参数的任务实现
```java
package com.ecep.contract.task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.WebSocketClientTasker;
import com.ecep.contract.vo.VendorVo;
import lombok.Setter;
/**
* 更新供应商评价表
*/
public class CompanyVendorEvaluationFormUpdateTask extends Tasker<Object> implements WebSocketClientTasker {
private static final Logger logger = LoggerFactory.getLogger(CompanyVendorEvaluationFormUpdateTask.class);
@Setter
private VendorVo vendor;
@Override
public void updateProgress(long current, long total) {
super.updateProgress(current, total);
}
@Override
public String getTaskName() {
return "CompanyVendorEvaluationFormUpdateTask"; // 与服务器端对应Tasker类名匹配
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("更新供应商评价表");
return callRemoteTask(holder, getLocale(), vendor.getId());
}
}
```
### 示例3使用Spring Bean的任务实现
```java
package com.ecep.contract.task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.WebSocketClientTasker;
import com.ecep.contract.service.YongYouU8Service;
/**
* 合同同步任务
*/
public class ContractSyncTask extends Tasker<Object> implements WebSocketClientTasker {
private static final Logger logger = LoggerFactory.getLogger(ContractSyncTask.class);
private YongYouU8Service yongYouU8Service;
private YongYouU8Service getYongYouU8Service() {
if (yongYouU8Service == null) {
yongYouU8Service = SpringApp.getBean(YongYouU8Service.class);
}
return yongYouU8Service;
}
public String getTaskName() {
return "ContractSyncTask"; // 与服务器端对应Tasker类名匹配
}
@Override
public void updateProgress(long current, long total) {
super.updateProgress(current, total);
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("用友U8系统-同步合同");
return callRemoteTask(holder, getLocale());
}
}
```
## 注意事项和最佳实践
### 1. 命名规范
- 任务类名应采用驼峰命名法,以`Tasker`结尾或描述性名称如`Task`
- getTaskName()返回的名称应与服务器端对应Tasker类名完全匹配
- 类注释应清晰描述任务的功能和用途
### 2. 继承关系
- 必须同时继承Tasker类并实现WebSocketClientTasker接口
- Tasker泛型参数通常为Object
- 确保正确导入所有必要的包
### 3. 参数处理
- 对于需要参数的任务,使用@Setter注解简化属性设置
- 对于需要在多处使用的参数,考虑添加@Getter注解
- 确保参数验证(如果必要)
### 4. Spring Bean获取
- 使用SpringApp.getBean()获取所需的服务实例
- 考虑使用懒加载模式避免不必要的Bean初始化
### 5. 消息和进度更新
- 使用updateTitle()设置有意义的任务标题
- 通过MessageHolder或updateMessage()记录详细的执行消息
- 确保进度更新反映真实的执行进度
### 6. 异常处理
- 在关键操作处添加try-catch块
- 记录异常日志并通知用户
- 适当向上抛出异常以确保框架能正确处理
### 7. 日志级别使用
- INFO: 记录正常的操作流程
- WARNING: 记录可能的问题,但不影响继续执行
- ERROR: 记录严重错误,通常会终止执行
### 8. 远程调用参数
- 确保传入的参数类型与服务器端Tasker期望的一致
- 对于不需要参数的任务,可以不传入额外参数
- 对于需要多个参数的任务,确保参数顺序正确
### 9. 代码风格
- 保持代码简洁明了
- 遵循项目的代码格式化规范
- 添加必要的注释说明核心逻辑
### 10. 实现策略选择
- 简单任务:使用简洁的实现方式,省略不必要的日志
- 复杂任务:添加详细的日志记录和异常处理
- 有特定需求的任务:根据需要重写接口中的其他方法
## 与服务器端交互流程
1. 客户端Tasker通过callRemoteTask()方法提交任务
2. WebSocketClientService负责建立与服务器的连接并发送任务信息
3. 服务器接收到任务后创建对应的Tasker实例并执行
4. 执行过程中的消息、进度等通过WebSocket实时返回给客户端
5. 客户端Tasker通过updateMessage()、updateProgress()等方法更新UI
## 扩展和自定义
如需为特定任务提供自定义功能,可以:
1. 重写cancelTask()方法实现任务取消逻辑
2. 根据需要添加额外的字段和方法
3. 扩展execute()方法实现更复杂的任务流程
## 总结
通过分析项目中的17个WebSocketClientTasker实现类我们总结了客户端Tasker实现的多种模式和最佳实践。这些实现从简单到复杂涵盖了各种使用场景为后续Tasker的编写提供了全面的参考。
客户端Tasker类实现WebSocketClientTasker接口是实现与服务器实时通信的关键步骤。通过遵循本文档中的规范和最佳实践可以确保任务执行的可靠性、进度的实时更新和良好的用户体验。
在实际开发中,应根据任务的复杂度和具体需求,选择合适的实现模式和策略,同时保持代码的一致性和可维护性。

View File

@@ -0,0 +1,374 @@
# Repository一致性分析 - 综合报告
## 📊 执行摘要
### 分析概览
- **总Repository数量**: 22个
- **涉及业务域**: contract, company, customer, project, vendor
- **分析时间**: 2024年项目全面分析
- **分析范围**: 覆盖server模块下所有5个业务域的Repository实现
### 目录覆盖情况
`server/src/main/java/com/ecep/contract/ds/contract/repository/` (13个)
`server/src/main/java/com/ecep/contract/ds/company/repository/` (3个)
`server/src/main/java/com/ecep/contract/ds/customer/repository/` (2个)
`server/src/main/java/com/ecep/contract/ds/project/repository/` (3个)
`server/src/main/java/com/ecep/contract/ds/vendor/repository/` (2个)
## 🔍 关键发现与评估
### 🔴 严重问题继承层次不一致5个
#### 1. 直接继承JpaRepository4个
- `ContractFileRepository` - 直接继承`JpaRepository``JpaSpecificationExecutor`
- `ContractItemRepository` - 直接继承`JpaRepository``JpaSpecificationExecutor`
- `VendorFileRepository` - 直接继承`JpaRepository``JpaSpecificationExecutor`
- `ProjectQuotationRepository` - 直接继承`JpaRepository``JpaSpecificationExecutor`
#### 2. 继承多个冗余接口1个
- `PurchaseOrderRepository` - 同时继承了4个接口`CrudRepository`, `PagingAndSortingRepository`, `JpaRepository`, `JpaSpecificationExecutor`
### 🟡 中等问题6个
#### 注解缺失2个
- `CompanyFileRepository` - 缺少`@Repository`注解
- `ProjectFileRepository` - 缺少`@Repository`注解
#### 文档注释缺失4个
- `CompanyFileRepository` - 缺少JavaDoc注释
- `ProjectFileRepository` - 缺少JavaDoc注释
- `VendorFileRepository` - 缺少JavaDoc注释
- `ProjectQuotationRepository` - 缺少JavaDoc注释
### 📈 整体质量评估
#### 正确实现统计
-**17个Repository正确继承MyRepository** (77%)
-**5个Repository继承层次错误** (23%)
- 🟡 **2个Repository缺少注解** (9%)
- 🟡 **4个Repository文档不完整** (18%)
#### 质量分级分布
- **A级完全正确**: 13个 (59%)
- **B级轻微问题**: 6个 (27%)
- **C级严重错误**: 3个 (14%)
---
# 详细技术分析
## 继承一致性详细分析
### ✅ 正确继承MyRepository的Repository17个
**contract业务域7个**
- ContractRepository
- ContractBalanceRepository
- SalesOrderRepository
- ContractTypeRepository
- ContractKindRepository
- ContractInvoiceRepository
**company业务域3个**
- CompanyRepository
- CompanyFileRepository (有注解问题)
- CompanyCustomerRepository
**customer业务域2个**
- CustomerCatalogRepository
**project业务域2个**
- ProjectRepository
- ProjectFileRepository (有注解问题)
**vendor业务域1个**
- VendorRepository
### ❌ 继承层次错误的Repository5个
#### Contract业务域3个
1. **ContractFileRepository**
2. **ContractItemRepository**
3. **PurchaseOrderRepository**
#### Project业务域1个
4. **ProjectQuotationRepository**
#### Vendor业务域1个
5. **VendorFileRepository**
## 问题影响分析
### 技术影响
- ❌ 违反了统一的架构设计原则
- ❌ 失去了MyRepository提供的统一功能增强
- ❌ 代码风格不一致,影响可维护性
- ❌ 开发团队需要维护多种不同的实现模式
- ❌ 增加了代码维护成本和技术债务
### 业务影响
- ❌ 代码可维护性降低
- ❌ 新团队成员学习成本增加
- ❌ 代码审查复杂度提高
- ❌ 系统稳定性潜在风险增加
## 详细修复方案
### 🔴 优先级1修复继承层次错误
#### 1. ContractFileRepository
```java
// ❌ 当前错误实现
public interface ContractFileRepository
extends JpaRepository<ContractFile, Integer>, JpaSpecificationExecutor<ContractFile> {
// ✅ 正确修复
@Repository
public interface ContractFileRepository extends MyRepository<ContractFile, Integer> {
// 自动获得MyRepository提供的所有功能
}
```
#### 2. ContractItemRepository
```java
// ❌ 当前错误实现
public interface ContractItemRepository
extends JpaRepository<ContractItem, Integer>, JpaSpecificationExecutor<ContractItem> {
// ✅ 正确修复
@Repository
public interface ContractItemRepository extends MyRepository<ContractItem, Integer> {
// 统一继承结构,简化维护
}
```
#### 3. VendorFileRepository
```java
// ❌ 当前错误实现
@Repository
public interface VendorFileRepository
extends JpaRepository<VendorFile, Integer>, JpaSpecificationExecutor<VendorFile> {
// ✅ 正确修复
@Repository
public interface VendorFileRepository extends MyRepository<VendorFile, Integer> {
// 保持注解的同时修正继承结构
}
```
#### 4. ProjectQuotationRepository
```java
// ❌ 当前错误实现
@Repository
public interface ProjectQuotationRepository
extends JpaRepository<ProjectQuotation, Integer>, JpaSpecificationExecutor<ProjectQuotation> {
// ✅ 正确修复
@Repository
public interface ProjectQuotationRepository extends MyRepository<ProjectQuotation, Integer> {
// 获得MyRepository的所有增强功能
}
```
#### 5. PurchaseOrderRepository
```java
// ❌ 当前错误实现继承4个冗余接口
public interface PurchaseOrderRepository extends
CrudRepository<PurchaseOrder, Integer>,
PagingAndSortingRepository<PurchaseOrder, Integer>,
JpaRepository<PurchaseOrder, Integer>,
JpaSpecificationExecutor<PurchaseOrder> {
// ✅ 正确修复
@Repository
public interface PurchaseOrderRepository extends MyRepository<PurchaseOrder, Integer> {
// 单一继承,清晰简洁
}
```
### 🟡 优先级2修复注解缺失
#### CompanyFileRepository
```java
// ❌ 当前缺少注解
public interface CompanyFileRepository extends MyRepository<CompanyFile, Integer> {
// ✅ 添加注解
@Repository
public interface CompanyFileRepository extends MyRepository<CompanyFile, Integer> {
// 添加完整的JavaDoc注释
/**
* 公司文件数据访问接口
* 提供公司相关文件的CRUD操作和业务查询功能
*/
}
```
#### ProjectFileRepository
```java
// ❌ 当前缺少注解
public interface ProjectFileRepository extends MyRepository<ProjectFile, Integer> {
// ✅ 添加注解
@Repository
public interface ProjectFileRepository extends MyRepository<ProjectFile, Integer> {
/**
* 项目文件数据访问接口
* 提供项目文件的管理和查询功能
*/
}
```
### 🟡 优先级3完善文档注释
为以下Repository补充完整的JavaDoc注释
- CompanyFileRepository
- ProjectFileRepository
- VendorFileRepository
- ProjectQuotationRepository
标准JavaDoc格式示例
```java
/**
* [功能描述]数据访问接口
*
* 提供[业务描述]的CRUD操作和业务查询功能
*
* @author [作者]
* @since [版本]
*/
```
## 实施计划
### 阶段1立即修复1-2天
**目标**: 解决所有严重的继承层次错误
**执行步骤**:
1. 备份当前的Repository实现
2. 逐一修复5个继承层次错误的Repository
3. 添加缺失的@Repository注解
4. 运行编译测试确保无破坏性变更
5. 执行基本功能验证测试
**验收标准**:
- 所有Repository都继承MyRepository
- 编译通过,无语法错误
- 基本CRUD功能正常
### 阶段2文档完善1天
**目标**: 统一JavaDoc注释规范
**执行步骤**:
1. 为4个Repository补充JavaDoc注释
2. 检查并更新相关单元测试
3. 验证文档格式规范性
4. 代码审查确认
**验收标准**:
- 所有Repository都有完整的JavaDoc注释
- 测试用例覆盖主要功能
- 代码审查通过
### 阶段3质量保证半天
**目标**: 确保修改不影响系统稳定性
**执行步骤**:
1. 全面编译测试
2. 运行相关单元测试套件
3. 执行集成测试验证
4. 更新相关文档
5. 最终验收确认
**验收标准**:
- 所有测试通过
- 系统功能完整
- 性能无明显影响
## 对规范文档的影响
### ✅ 已更新的规范文档
- **`server_repository_rules.md`** - 已包含错误示例和正确实现指南
- 新增"2.2 重要错误示例"章节
- 新增"2.3 当前项目错误统计"章节
- 新增"2.5 MyRepository基类能力"章节
### 📋 需要更新的检查清单
1. **继承层次检查** - 确保所有Repository继承MyRepository
2. **注解完整性检查** - 验证所有Repository都有@Repository注解
3. **文档规范性检查** - 确认JavaDoc注释完整性
4. **代码风格一致性检查** - 验证命名规范和代码格式
## 🎯 预期收益
### 技术收益
-**统一架构设计** - 所有Repository遵循一致的设计模式
-**代码一致性提升** - 消除实现差异,提高代码质量
-**维护成本降低** - 单一继承结构,简化维护工作
-**团队开发效率提升** - 统一规范,降低学习成本
### 质量收益
-**减少代码冗余** - 消除重复的接口继承
-**提高代码可读性** - 统一模式,易于理解
-**降低技术债务** - 减少不一致实现的技术负担
-**增强系统稳定性** - 统一模式,减少潜在风险
## 📅 后续行动
### 立即行动(本周)
1. 🔧 修复5个继承层次错误的Repository
2. 🔧 添加缺失的@Repository注解
3. 🔧 编译测试验证功能完整性
### 短期完善(下周)
1. 📝 补充JavaDoc文档注释
2. 🧪 运行完整的单元测试套件
3. 👥 代码审查确认实现质量
### 长期维护
1. 📋 建立Repository开发规范检查清单
2. 🔍 定期进行Repository实现一致性检查
3. 🎓 团队培训和规范宣贯
4. 🤖 建立CI/CD流程中的自动检查机制
## 📞 技术支持
如需技术支持或有任何疑问,请参考:
### 相关文档
- **详细技术分析**: `repository_analysis_report.md`
- **实施规范指导**: `server_repository_rules.md`
- **项目整体规范**: `.trae/rules/` 目录
### 联系方式
- 技术问题: 参考详细分析报告中的修复代码示例
- 规范疑问: 查看server_repository_rules.md文档
- 实施支持: 遵循本报告的分阶段实施计划
---
## 结论
通过对项目中22个Repository的全面分析我们发现了明显的实现不一致性问题特别是23%的Repository存在继承层次错误。这些问题严重影响了代码的可维护性和架构的统一性。
**关键统计数据**:
- 17个Repository正确实现 (77%)
- 5个继承层次错误 (23%)
- 2个注解缺失 (9%)
- 4个文档不完整 (18%)
**修复优先级**:
1. **高优先级**: 修复5个继承层次错误1-2天
2. **中优先级**: 补充2个缺失注解半天
3. **低优先级**: 完善4个文档注释1天
建议按照本报告提出的分阶段实施计划逐步改进所有Repository实现确保项目达到统一的架构设计标准。通过这些改进将显著提升代码质量、降低维护成本并为团队的长期发展奠定坚实基础。
**分析状态**: ✅ 完成
**下一步**: 执行分阶段修复计划
**预计完成时间**: 3-4个工作日
---
**报告生成时间**: 2024年
**分析深度**: 全面技术分析
**实施可行性**: 高(详细修复方案已提供)

View File

@@ -0,0 +1,460 @@
# Server模块 Repository 实现经验总结
## 1. Repository 基本架构
### 1.1 接口设计原则
- Repository接口必须继承`MyRepository<T, ID>`基类
- 使用`@Repository`注解标记为Spring组件
- 遵循接口隔离原则,按业务功能组织方法
### 1.2 包结构规范
```
com.ecep.contract.ds.{business}.repository.{EntityName}Repository.java
```
示例:`ContractBalanceRepository`位于`com.ecep.contract.ds.contract.repository`
## 2. 接口继承层次
### 2.1 基础接口结构
```java
@Repository
public interface ContractBalanceRepository extends MyRepository<ContractBalance, Integer> {
// 自定义查询方法
}
```
### 2.2 ⚠️ 重要错误示例 - 避免这些实现方式
**❌ 错误示例1直接继承JpaRepository**
```java
// ContractFileRepository, ContractItemRepository, VendorFileRepository, ProjectQuotationRepository
@Repository
public interface ContractFileRepository
extends JpaRepository<ContractFile, Integer>, JpaSpecificationExecutor<ContractFile> {
// 错误不应该直接继承JpaRepository
List<ContractFile> findByContractId(Integer contractId);
}
```
**❌ 错误示例2继承多个冗余接口**
```java
// PurchaseOrderRepository
public interface PurchaseOrderRepository extends
CrudRepository<PurchaseOrder, Integer>,
PagingAndSortingRepository<PurchaseOrder, Integer>,
JpaRepository<PurchaseOrder, Integer>,
JpaSpecificationExecutor<PurchaseOrder> {
// 错误冗余接口继承应该只继承MyRepository
}
```
**❌ 错误示例3缺少@Repository注解**
```java
// CompanyFileRepository, ProjectFileRepository
public interface CompanyFileRepository extends MyRepository<CompanyFile, Integer> {
// 错误:缺少@Repository注解
List<CompanyFile> findByCompany(Company company);
}
```
**❌ 错误示例4缺少JavaDoc注释**
```java
// 多个Repository存在此问题
public interface VendorFileRepository extends MyRepository<VendorFile, Integer> {
// 错误缺少JavaDoc注释说明用途和方法
List<VendorFile> findAllByVendorId(int vendorId);
}
```
**✅ 正确实现示例**
```java
/**
* 合同Repository - 提供合同相关的数据库访问操作
*/
@Repository
public interface ContractRepository extends MyRepository<Contract, Integer> {
// 正确统一继承MyRepository有完整的JavaDoc
/**
* 根据合同代码查询合同
* @param code 合同代码
* @return 合同 Optional
*/
Optional<Contract> findByCode(String code);
/**
* 根据状态查询合同列表
* @param status 合同状态
* @return 合同列表
*/
List<Contract> findByStatus(ContractStatus status);
}
```
### 2.3 当前项目错误统计
基于对全项目22个Repository的全面分析发现以下问题分布
**🔴 继承层次错误5个**
- `ContractFileRepository` - 直接继承JpaRepository
- `ContractItemRepository` - 直接继承JpaRepository
- `VendorFileRepository` - 直接继承JpaRepository
- `ProjectQuotationRepository` - 直接继承JpaRepository
- `PurchaseOrderRepository` - 继承4个冗余接口
**🟡 注解缺失2个**
- `CompanyFileRepository` - 缺少@Repository注解
- `ProjectFileRepository` - 缺少@Repository注解
**🟡 文档注释不完整4个**
- `CompanyFileRepository` - 缺少JavaDoc注释
- `ProjectFileRepository` - 缺少JavaDoc注释
- `VendorFileRepository` - 缺少JavaDoc注释
- `ProjectQuotationRepository` - 缺少JavaDoc注释
**✅ 正确实现17个**
- contract: ContractRepository, ContractBalanceRepository, SalesOrderRepository, ContractTypeRepository, ContractKindRepository, ContractInvoiceRepository
- company: CompanyRepository
- customer: CompanyCustomerRepository, CustomerCatalogRepository
- project: ProjectRepository
- vendor: VendorRepository
### 2.5 MyRepository基类能力
`MyRepository`接口继承结构:
- `JpaRepository`提供CRUD操作
- 继承`JpaSpecificationExecutor`提供条件查询能力
- 继承`QueryByExampleExecutor`提供示例查询能力
## 3. 查询方法设计
### 3.1 方法命名规范
使用Spring Data JPA的查询方法命名约定
#### 基本查询方法
- `findBy{属性名}` - 根据属性查找
- `findBy{属性名}And{属性名}` - 多条件AND查询
- `findBy{属性名}Or{属性名}` - 多条件OR查询
- `findBy{属性名}OrderBy{排序属性}` - 带排序查询
#### 关联对象查询
- `findBy{关联对象属性名}.{关联对象属性}` - 嵌套属性查询
- 支持多级嵌套,如:`findByContractCompanyName`
#### 特殊查询类型
```java
// 根据业务ID查找
ContractBalance findByGuid(UUID guid);
// 根据合同ID查找带分页支持
Page<ContractBalance> findByContractId(Integer contractId, Pageable pageable);
// 根据多个条件组合查询
List<ContractBalance> findByContractIdAndRefId(Integer contractId, String refId);
```
### 3.2 自定义查询实现
当方法命名无法满足需求时,使用`@Query`注解:
```java
@Query("SELECT cb FROM ContractBalance cb WHERE cb.contract.id = :contractId " +
"AND cb.guid = :guid AND cb.setupDate >= :startDate")
Page<ContractBalance> findByContractAndGuidAndDateRange(
@Param("contractId") Integer contractId,
@Param("guid") UUID guid,
@Param("startDate") LocalDateTime startDate,
Pageable pageable
);
```
## 4. 分页和排序支持
### 4.1 分页查询
```java
// 在Repository接口中定义
Page<ContractBalance> findByContractId(Integer contractId, Pageable pageable);
// 在Service中调用
Page<ContractBalance> result = repository.findByContractId(contractId,
PageRequest.of(page, size, Sort.by("setupDate").descending()));
```
### 4.2 排序规范
- 默认按主键ID降序排列
- 业务相关字段按创建时间降序
- 支持多字段排序:`Sort.by("field1").ascending().and(Sort.by("field2").descending())`
## 5. 统计和聚合查询
### 5.1 基础统计
```java
@Query("SELECT COUNT(cb) FROM ContractBalance cb WHERE cb.contract.id = :contractId")
Long countByContractId(@Param("contractId") Integer contractId);
@Query("SELECT SUM(cb.balanceAmount) FROM ContractBalance cb WHERE cb.contract.id = :contractId")
BigDecimal sumBalanceByContractId(@Param("contractId") Integer contractId);
```
### 5.2 复杂聚合
```java
@Query("SELECT new com.ecep.contract.vo.ContractBalanceStatsVO(" +
"cb.contract.id, COUNT(cb), SUM(cb.balanceAmount)) " +
"FROM ContractBalance cb GROUP BY cb.contract.id")
List<ContractBalanceStatsVO> getBalanceStatsByContract();
```
## 6. 缓存策略
### 6.1 缓存注解使用
虽然Repository本身不直接使用缓存但需要考虑Service层的缓存需求
```java
// 在Service层配合使用
@Cacheable(key = "#p0")
ContractBalance findById(Integer id);
@CacheEvict(key = "#p0.id")
ContractBalance save(ContractBalance contractBalance);
```
### 6.2 缓存键设计原则
- 单一记录:使用主键作为键,如`"balance-" + id`
- 按业务维度缓存:使用业务标识,如`"contract-" + contractId`
- 避免缓存键冲突
## 7. 事务管理
### 7.1 事务传播级别
- 查询方法:默认`REQUIRED`
- 写操作:明确指定`@Transactional(propagation = Propagation.REQUIRED)`
### 7.2 异常处理
```java
@Transactional
public ContractBalance saveWithRetry(ContractBalance balance) {
try {
return repository.save(balance);
} catch (DataIntegrityViolationException e) {
// 处理数据完整性异常
throw new BusinessException("数据重复或违反约束", e);
}
}
```
## 8. 性能优化
### 8.1 懒加载优化
```java
// 在查询时指定抓取策略
@Query("SELECT cb FROM ContractBalance cb LEFT JOIN FETCH cb.contract " +
"LEFT JOIN FETCH cb.employee WHERE cb.id = :id")
Optional<ContractBalance> findByIdWithRelations(@Param("id") Integer id);
```
### 8.2 批量操作优化
```java
// 批量插入
@Modifying
@Query("DELETE FROM ContractBalance cb WHERE cb.contract.id IN :contractIds")
void deleteByContractIds(@Param("contractIds") List<Integer> contractIds);
```
## 9. 错误处理和调试
### 9.1 常见异常类型
- `DataAccessException` - 数据访问异常
- `DataIntegrityViolationException` - 数据完整性异常
- `EmptyResultDataAccessException` - 空结果异常
### 9.2 日志记录
```java
@Repository
@Slf4j
public class ContractBalanceRepository {
@Query("...")
public List<ContractBalance> findComplexQuery(...) {
log.debug("执行复杂查询: {}", jpql);
// 执行查询
}
}
```
## 10. 测试策略
### 10.1 Repository测试
```java
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ContractBalanceRepositoryTest {
@Autowired
private TestEntityManager em;
@Autowired
private ContractBalanceRepository repository;
@Test
void shouldFindByContractId() {
// 测试数据准备
ContractBalance balance = createTestBalance();
// 执行测试
Page<ContractBalance> result = repository.findByContractId(
balance.getContract().getId(),
PageRequest.of(0, 10)
);
// 断言验证
assertThat(result.getContent()).hasSize(1);
assertThat(result.getContent().get(0)).isEqualTo(balance);
}
}
```
### 10.2 性能测试
```java
@Test
@RepeatedTest(100)
@Timeout(value = 5, unit = TimeUnit.SECONDS)
void performanceTest() {
// 性能测试实现
}
```
## 11. 最佳实践总结
### 11.1 设计原则
1. **单一职责**每个Repository只负责一个实体类型
2. **接口隔离**:根据客户端需求设计专用查询方法
3. **命名清晰**:方法名应能直观表达查询意图
4. **性能优先**:合理使用索引和查询优化
### 11.2 代码规范
1. **注解完整**:正确使用`@Repository``@Query``@Param`
2. **参数校验**对传入参数进行null和有效性检查
3. **异常处理**:捕获并处理数据库访问异常
4. **日志记录**:记录关键查询操作和性能指标
### 11.3 维护性
1. **版本兼容**:考虑数据库模式变更的影响
2. **向后兼容**:新增方法不影响现有功能
3. **文档完整**复杂查询添加JavaDoc说明
4. **测试覆盖**:确保关键查询逻辑有测试覆盖
## 12. 常见问题和解决方案
### 12.1 N+1查询问题
**问题**查询列表时触发N+1次关联查询
**解决**:使用`JOIN FETCH``@EntityGraph`
### 12.2 性能瓶颈
**问题**:复杂查询导致响应慢
**解决**
1. 添加数据库索引
2. 优化查询语句
3. 使用分页限制结果集
4. 考虑使用缓存
### 12.3 事务死锁
**问题**:并发操作导致事务死锁
**解决**
1. 合理设计事务边界
2. 调整事务隔离级别
3. 实现重试机制
## 13. 扩展建议
### 13.1 动态查询支持
考虑使用`Querydsl``Specification`支持动态查询构建。
### 13.2 读写分离
实现主从数据库分离,提升查询性能。
### 13.3 分布式缓存
结合Redis等分布式缓存提升查询性能。
---
## 14. 实施检查清单
### 14.1 Repository实现后验证步骤
在每个Repository实现完成后请按以下清单进行验证
#### ✅ 基础架构检查
- [ ] Repository接口继承了 `MyRepository<T, ID>`
- [ ] 使用了 `@Repository` 注解
- [ ] 包路径符合规范:`com.ecep.contract.ds.{business}.repository`
- [ ] 类名符合命名规范:`{EntityName}Repository`
#### ✅ 方法设计检查
- [ ] 遵循Spring Data JPA命名约定
- [ ] 包含必要的业务查询方法
- [ ] 复杂查询使用 `@Query` 注解
- [ ] 分页方法支持 `Pageable` 参数
#### ✅ 文档和注释检查
- [ ] 类级别JavaDoc注释完整
- [ ] 关键方法有JavaDoc说明
- [ ] 参数和返回值有明确说明
#### ✅ 性能和质量检查
- [ ] 避免N+1查询问题
- [ ] 适当使用索引提示
- [ ] 包含必要的单元测试
- [ ] 异常处理考虑周全
### 14.2 常见错误检查
在提交代码前,特别检查是否犯了以下常见错误:
#### ❌ 继承层次错误
```java
// 错误示例 - 不要这样做
public interface SomeRepository extends JpaRepository<Entity, Integer>
public interface AnotherRepository extends CrudRepository<Entity, Integer>
// 正确做法 - 统一使用MyRepository
public interface SomeRepository extends MyRepository<Entity, Integer>
```
#### ❌ 方法命名不规范
```java
// 错误示例
List<Entity> getDataByCondition(String condition) // 使用get前缀而非find
// 正确做法
List<Entity> findByCondition(String condition) // 使用find前缀
```
#### ❌ 缺少必要文档
```java
// 错误示例 - 无注释
List<Entity> findByStatus(String status);
// 正确做法 - 完整注释
/**
* 根据状态查询实体列表
*
* @param status 状态值
* @return 符合状态的实体列表
*/
List<Entity> findByStatus(String status);
```
### 14.3 架构一致性验证
确保新实现的Repository与项目中其他Repository保持一致
1. **继承模式统一**所有Repository都必须继承MyRepository
2. **注解使用统一**:统一使用@Repository注解
3. **命名约定统一**遵循Spring Data JPA命名规范
4. **文档风格统一**保持JavaDoc注释风格一致
### 14.4 后续维护注意事项
- 新增查询方法时遵循现有命名规范
- 修改现有方法时保持向后兼容性
- 定期审查Repository方法的性能和合理性
- 及时更新相关文档和测试用例
---
*本文档基于ContractBalanceRepository实现经验总结结合项目实际情况分析其他Repository实现应参考此文档规范。特别注意避免文档中提到的常见错误。*

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层的代码质量、性能和可维护性为整个系统的稳定运行提供了坚实基础。

View File

@@ -0,0 +1,460 @@
# WebSocketServerTasker 接口实现规范
## 1. 概述
本文档总结了实现 `WebSocketServerTasker` 接口的标准做法和最佳实践基于对项目中已有实现类的分析。该接口用于服务器端实现支持WebSocket通信的任务处理器实现异步任务的执行和状态监控。
## 2. WebSocketServerTasker 接口介绍
`WebSocketServerTasker` 接口位于 `com.ecep.contract.service.tasker` 包下,继承自 `java.util.concurrent.Callable<Object>`,定义了以下核心方法:
```java
public interface WebSocketServerTasker extends Callable<Object> {
// 初始化任务,处理传入的参数
void init(JsonNode argsNode);
// 设置会话信息(默认实现为空)
default void setSession(SessionInfo session) {}
// 设置消息处理函数
void setMessageHandler(Predicate<Message> messageHandler);
// 设置标题处理函数
void setTitleHandler(Predicate<String> titleHandler);
// 设置属性处理函数
void setPropertyHandler(BiConsumer<String, Object> propertyHandler);
// 设置进度处理函数
void setProgressHandler(BiConsumer<Long, Long> progressHandler);
}
```
## 3. 基础实现模式
### 3.1 推荐实现方式
通过分析项目中的实现类,推荐以下标准实现模式:
1. **继承 Tasker<Object> 并实现 WebSocketServerTasker 接口**
```java
public class YourTasker extends Tasker<Object> implements WebSocketServerTasker {
// 实现代码
}
```
2. **在 Tasker 基类中已实现的方法**
`Tasker` 类已经提供了 `WebSocketServerTasker` 接口中大部分方法的实现,包括:
- `setMessageHandler`
- `setTitleHandler`
- `setPropertyHandler`
- `setProgressHandler`
- `call()` 方法(实现了 `Callable` 接口)
这使得实现类只需要关注特定业务逻辑的实现。
## 3.2 WebSocketServerTasker 注册配置
要使WebSocketServerTasker能够被客户端通过WebSocket调用必须在`tasker_mapper.json`配置文件中进行注册。
### 3.2.1 注册配置文件
配置文件位置:`server/src/main/resources/tasker_mapper.json`
### 3.2.2 注册格式
注册格式为JSON对象包含一个`tasks`字段,该字段是一个键值对映射,其中:
- **键**:任务名称(客户端通过此名称调用任务)
- **值**:任务类的完全限定名
```json
{
"tasks": {
"任务名称": "任务类的完全限定名",
"ContractVerifyTasker": "com.ecep.contract.service.tasker.ContractVerifyTasker"
},
"descriptions": "任务注册信息, 客户端的任务可以通过 WebSocket 调用"
}
```
### 3.2.3 注册示例
假设我们创建了一个名为`CustomTasker`的新任务类,其完全限定名为`com.ecep.contract.service.tasker.CustomTasker`,则注册方式如下:
```json
{
"tasks": {
"CustomTasker": "com.ecep.contract.service.tasker.CustomTasker",
"OtherTasker": "..."
},
"descriptions": "任务注册信息, 客户端的任务可以通过 WebSocket 调用"
}
```
### 3.2.4 注册机制说明
WebSocketServerTaskManager类在启动时会读取`tasker_mapper.json`文件,初始化任务类映射表。当客户端请求执行任务时,系统会:
1. 根据客户端提供的任务名称从映射表中查找对应的任务类
2. 使用反射机制实例化任务对象
3. 设置WebSocket会话和各类处理器
4. 执行任务并通过WebSocket返回结果
## 4. ContractRepairAllTasker 升级经验
通过分析 `ContractRepairAllTasker` 的实现,我们总结了以下升级经验:
### 4.1 升级步骤
1. **继承适当的基础类**:根据业务需求选择继承 `Tasker<Object>` 或其他特定的抽象类(如 `AbstContractRepairTasker`
2. **实现 WebSocketServerTasker 接口**:声明实现该接口,自动继承接口定义的方法
3. **实现 init 方法**:处理任务初始化和参数解析
```java
@Override
public void init(JsonNode argsNode) {
// 解析参数或初始化任务状态
// 如果 Client 没有传递参数,就不做处理
// do nothing
// 如果有参数,正确做法:检查参数有效性并安全解析
if (argsNode != null && argsNode.size() > 0) {
ContractService contractService = getCachedBean(ContractService.class);
int contractId = argsNode.get(0).asInt();
this.contract = contractService.findById(contractId);
}
}
```
4. **实现或重写 execute 方法**:提供具体的业务逻辑实现
```java
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 调用父类初始化
super.execute(holder);
updateTitle("同步修复所有合同");
// 执行具体业务逻辑
repair(holder);
return null; // 通常返回 null 或处理结果
}
```
5. **使用更新方法提供状态反馈**:在执行过程中使用 `updateTitle`、`updateProgress` 等方法提供实时反馈
```java
updateProgress(counter.incrementAndGet(), total);
```
6. **支持任务取消**:定期检查 `isCancelled()` 方法,支持任务的取消操作
```java
if (isCancelled()) {
break;
}
```
### 4.2 关键经验
1. **消息处理**:通过 `MessageHolder` 处理和记录执行过程中的消息,方便调试和错误追踪
2. **进度更新**:对于批量操作,使用计数器和总数定期更新进度,提供良好的用户体验
3. **异常处理**:在循环处理中捕获异常并记录,确保单个项目的失败不会导致整个任务失败
4. **分页处理**:对于大量数据的处理,使用分页查询避免内存溢出
## 5. 共同模式和最佳实践
通过分析项目中的17个实现类我们总结了以下共同模式和最佳实践
### 5.1 共同模式
1. **继承结构**:所有实现类都继承 `Tasker<Object>` 并实现 `WebSocketServerTasker` 接口
2. **核心方法实现**
- 实现 `init(JsonNode argsNode)` 方法处理参数
- 实现或重写 `execute(MessageHolder holder)` 方法实现业务逻辑
3. **状态更新**:使用 `updateTitle`、`updateMessage`、`updateProgress` 等方法更新任务状态
4. **Bean获取**:使用 `getCachedBean` 方法获取Spring管理的Bean
```java
ContractService contractService = getCachedBean(ContractService.class);
```
### 5.2 最佳实践
1. **参数验证**:在 `init` 方法中验证和转换输入参数
2. **进度反馈**:对于长时间运行的任务,提供合理的进度更新
3. **异常处理**:捕获并处理特定异常,提供有意义的错误信息
4. **取消支持**:实现任务取消机制,定期检查 `isCancelled()` 状态
5. **资源清理**:在任务结束或取消时清理资源
6. **日志记录**:使用 `slf4j` 记录关键操作和异常信息
7. **属性传递**:使用 `updateProperty` 方法传递任务执行结果或状态
```java
updateProperty("passed", passed);
```
## 6. 完整实现示例
### 6.1 简单任务实现示例
```java
package com.ecep.contract.service.tasker;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.ds.contract.service.ContractService;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
public class SimpleTasker extends Tasker<Object> implements WebSocketServerTasker {
@Getter
@Setter
private int entityId;
@Getter
@Setter
private boolean success = false;
@Override
public void init(JsonNode argsNode) {
// 解析参数
this.entityId = argsNode.get(0).asInt();
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 更新标题
updateTitle("执行简单任务:" + entityId);
try {
// 更新进度
updateProgress(1, 3);
// 执行业务逻辑
ContractService service = getCachedBean(ContractService.class);
updateProgress(2, 3);
// 处理结果
success = true;
holder.info("任务执行成功");
// 更新最终进度
updateProgress(3, 3);
// 更新结果属性
updateProperty("success", success);
} catch (Exception e) {
holder.error("任务执行失败: " + e.getMessage());
success = false;
updateProperty("success", success);
throw e;
}
return null;
}
}
```
### 6.2 批量处理任务实现示例
```java
package com.ecep.contract.service.tasker;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.ds.contract.service.ContractService;
import com.ecep.contract.ds.contract.model.Contract;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;
public class BatchProcessTasker extends Tasker<Object> implements WebSocketServerTasker {
private ContractService contractService;
private List<Integer> entityIds;
@Override
public void init(JsonNode argsNode) {
// 初始化服务引用
this.contractService = getCachedBean(ContractService.class);
// 解析实体ID列表
entityIds = new ArrayList<>();
for (JsonNode node : argsNode) {
entityIds.add(node.asInt());
}
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("批量处理实体任务");
AtomicInteger counter = new AtomicInteger(0);
long total = entityIds.size();
for (Integer id : entityIds) {
// 检查是否取消
if (isCancelled()) {
holder.info("任务已取消");
break;
}
try {
MessageHolder subHolder = holder.sub("处理 ID: " + id + " > ");
// 执行业务逻辑
Contract entity = contractService.getById(id);
// 处理实体...
subHolder.info("处理成功");
} catch (Exception e) {
holder.error("处理 ID: " + id + " 失败: " + e.getMessage());
// 记录异常但继续处理下一个
}
// 更新进度
updateProgress(counter.incrementAndGet(), total);
}
updateTitle("批量处理完成");
return null;
}
}
```
### 6.3 带有依赖的任务实现示例
```java
package com.ecep.contract.service.tasker;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.ds.contract.service.ContractService;
import com.ecep.contract.ds.customer.service.CustomerService;
import com.ecep.contract.ui.Tasker;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
public class DependentTasker extends Tasker<Object> implements WebSocketServerTasker {
@Getter
@Setter
private int contractId;
@Getter
@Setter
private boolean processed = false;
private ContractService contractService;
private CustomerService customerService;
@Override
public void init(JsonNode argsNode) {
// 解析参数
this.contractId = argsNode.get(0).asInt();
// 获取服务依赖
this.contractService = getCachedBean(ContractService.class);
this.customerService = getCachedBean(CustomerService.class);
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
updateTitle("执行带有服务依赖的任务");
updateProgress(1, 5);
// 使用多个服务协同完成任务
try {
// 步骤1: 获取合同信息
var contract = contractService.getById(contractId);
updateProgress(2, 5);
// 步骤2: 获取相关客户信息
var customer = customerService.getById(contract.getCustomerId());
updateProgress(3, 5);
// 步骤3: 执行业务逻辑
// ...
updateProgress(4, 5);
processed = true;
holder.info("任务处理成功");
// 更新结果属性
updateProperty("processed", processed);
} catch (Exception e) {
holder.error("任务处理失败: " + e.getMessage());
throw e;
}
updateProgress(5, 5);
return null;
}
}
```
## 7. 最佳实践总结
1. **继承 Tasker<Object> 并实现 WebSocketServerTasker 接口**:利用已有的基础实现
2. **合理实现 init 方法**:处理参数初始化,避免在 execute 中重复解析
3. **实现清晰的 execute 逻辑**
- 开始时设置任务标题
- 过程中定期更新进度
- 提供详细的消息反馈
- 结束时更新最终状态
4. **使用 Spring Bean**:通过 `getCachedBean` 获取需要的服务依赖
5. **支持任务取消**:在关键循环中检查 `isCancelled()` 状态
6. **异常处理**:捕获异常并记录,提供有意义的错误信息
7. **进度反馈**:对于长时间运行的任务,提供合理的进度更新频率
8. **属性传递**:使用 `updateProperty` 方法传递任务结果给调用方
9. **日志记录**:使用 slf4j 记录关键操作和异常
10. **资源管理**:确保在任务完成或取消时释放相关资源
## 8. 注意事项
1. **不要在构造函数中获取 Spring Bean**:使用 `init` 方法或懒加载方式获取
2. **避免阻塞操作**:长时间阻塞的操作应考虑异步处理
3. **线程安全**:注意多线程环境下的并发访问问题
4. **内存管理**对于处理大量数据的任务注意内存使用避免OOM
5. **超时处理**:考虑设置合理的超时机制
6. **事务管理**:根据需要考虑事务的范围和传播行为
7. **任务注册**创建新的WebSocketServerTasker后必须在`tasker_mapper.json`文件中注册,否则客户端将无法调用
8. **任务名称唯一性**:确保任务名称在`tasker_mapper.json`中唯一,避免名称冲突
9. **类路径正确**:注册时确保使用正确的类完全限定名,否则会导致实例化失败
10. **重启服务**:修改`tasker_mapper.json`后,需要重启服务才能使新的注册生效
通过遵循这些规范和最佳实践,可以确保实现的 `WebSocketServerTasker` 类具有良好的可维护性、可扩展性和用户体验。

View File

@@ -0,0 +1,182 @@
# Tasker实现规范指南
## 1. Tasker架构概述
Tasker是系统中用于执行异步任务的核心框架主要用于处理耗时操作并提供实时进度反馈。Tasker架构包括
- **Tasker基类**:提供任务执行的基础框架和通信功能
- **WebSocketServerTasker接口**定义WebSocket通信任务的规范
- **MessageHolder**:用于在任务执行过程中传递消息和更新进度
## 2. 服务器端Tasker实现规范
### 2.1 基本结构
服务器端Tasker实现应遵循以下结构
```java
@Data
@EqualsAndHashCode(callSuper = true)
public class YourTaskNameTask extends Tasker<Object> implements WebSocketServerTasker {
// 服务依赖
@Setter
private YourService service;
@Override
public void init(JsonNode argsNode) {
// 初始化任务标题
updateTitle("任务标题");
// 初始化参数
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 实现业务逻辑
// 使用holder.info()、holder.error()等方法反馈消息
return null;
}
}
```
### 2.2 关键注意事项
1. **继承与实现**
- 必须继承`Tasker<Object>`基类
- 必须实现`WebSocketServerTasker`接口
2. **方法实现**
- **不要重复实现**Tasker基类已提供的handler设置方法`setMessageHandler``setTitleHandler``setPropertyHandler``setProgressHandler`
- 必须实现`init`方法,设置任务标题和初始化参数
- 必须实现`execute`方法,实现具体业务逻辑
3. **进度和消息反馈**
- 使用`updateProgress`方法提供进度更新
- 使用`MessageHolder``info``error``debug`等方法提供消息反馈
4. **任务取消**
- 在适当的地方检查`isCancelled()`状态,支持任务取消
5. **注册要求**
- 任务类必须在`tasker_mapper.json`中注册
## 3. 客户端Tasker实现规范
客户端Tasker实现应遵循
```java
public class YourTaskNameTask extends Tasker<Object> {
@Override
protected Object execute(MessageHolder holder) throws Exception {
// 客户端任务逻辑
return null;
}
}
```
## 4. 通信规范
### 4.1 命名规范
- 客户端和服务器端对应的Tasker类应使用**相同的类名**
- 示例:客户端的`ContractSyncAllTask`对应服务器端的`ContractSyncAllTask`
### 4.2 消息传递
- 使用`MessageHolder`进行消息传递
- 支持不同级别的消息INFO、ERROR、DEBUG
## 5. 最佳实践
1. **任务拆分**
- 将大型任务拆分为多个小任务,提高可维护性
2. **异常处理**
- 捕获并正确处理异常,使用`holder.error()`提供详细错误信息
3. **资源管理**
- 确保及时释放资源,特别是在任务取消的情况下
4. **日志记录**
- 使用`holder.debug()`记录调试信息
- 使用`holder.info()`记录进度信息
5. **状态检查**
- 定期检查`isCancelled()`状态,确保任务可以被及时取消
## 6. 代码示例
### 6.1 服务器端Tasker示例
```java
@Data
@EqualsAndHashCode(callSuper = true)
public class ExampleSyncTask extends Tasker<Object> implements WebSocketServerTasker {
@Setter
private ExampleService exampleService;
@Override
public void init(JsonNode argsNode) {
updateTitle("示例同步任务");
// 从argsNode初始化参数
}
@Override
protected Object execute(MessageHolder holder) throws Exception {
try {
// 获取总记录数
long total = exampleService.getTotalCount();
holder.info("开始同步数据,共" + total + "条记录");
// 分批处理
long processed = 0;
List<ExampleEntity> entities = exampleService.getAllEntities();
for (ExampleEntity entity : entities) {
// 检查是否取消
if (isCancelled()) {
holder.info("任务已取消");
return null;
}
// 处理单个实体
exampleService.processEntity(entity);
processed++;
// 更新进度
updateProgress(processed, total);
if (processed % 100 == 0) {
holder.info("已处理" + processed + "/" + total + "条记录");
}
}
holder.info("同步完成,共处理" + processed + "条记录");
return processed;
} catch (Exception e) {
holder.error("同步失败: " + e.getMessage());
throw e;
}
}
}
```
## 7. 常见错误和避免方法
1. **重复实现handler方法**
- 错误在子类中重复实现setMessageHandler等方法
- 解决删除子类中的这些方法直接使用Tasker基类提供的实现
2. **缺少任务注册**
- 错误任务类没有在tasker_mapper.json中注册
- 解决确保所有服务器端Tasker类都在配置文件中正确注册
3. **类名不匹配**
- 错误客户端和服务器端Tasker类名不一致
- 解决:确保两端使用相同的类名
4. **进度更新不正确**
- 错误:进度计算错误或没有更新
- 解决:正确计算和更新进度,提高用户体验
5. **资源泄漏**
- 错误:未及时关闭数据库连接等资源
- 解决使用try-with-resources或在finally块中关闭资源

View File

@@ -178,8 +178,11 @@ Contract-Manager/
2. 配置连接到服务端的WebSocket地址
3. 打包为可执行jar或使用jpackage创建安装包
## 开发环境要求
## Tasker实现规范指南
有关Tasker框架的详细实现规范请参阅 [Tasker实现规范指南](docs/task/tasker_implementation_guide.md)。
## 开发环境要求
- JDK 21
- Maven 3.6+
- MySQL 8.0+
@@ -192,7 +195,4 @@ Contract-Manager/
2. 优化数据库查询性能
3. 增强系统安全机制
4. 改进用户界面体验
5. 扩展更多云端服务集成
5. 扩展更多云端服务集成

View File

@@ -6,12 +6,12 @@
<parent>
<groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId>
<version>0.0.102-SNAPSHOT</version>
<version>0.0.135-SNAPSHOT</version>
</parent>
<groupId>com.ecep.contract</groupId>
<artifactId>client</artifactId>
<version>0.0.102-SNAPSHOT</version>
<version>0.0.135-SNAPSHOT</version>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
@@ -22,7 +22,7 @@
<dependency>
<groupId>com.ecep.contract</groupId>
<artifactId>common</artifactId>
<version>0.0.102-SNAPSHOT</version>
<version>0.0.135-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>

View File

@@ -53,7 +53,6 @@ public class WebSocketClientService {
@Getter
@Setter
private long readTimeout = 30000;
private String webSocketUrl = "ws://localhost:8080/ws";
private boolean isActive = false; // 标记连接是否活跃
private ScheduledFuture<?> heartbeatTask; // 心跳任务
private ScheduledFuture<?> reconnectFuture; // 修改类型为CompletableFuture<Void>
@@ -207,6 +206,12 @@ public class WebSocketClientService {
send(objectMapper.writeValueAsString(message));
}
/**
* WebSocketServerCallbackManage#onMessage 负责接收处理
*
* @param msg
* @return
*/
public CompletableFuture<JsonNode> send(SimpleMessage msg) {
CompletableFuture<JsonNode> future = new CompletableFuture<>();
try {
@@ -248,6 +253,8 @@ public class WebSocketClientService {
try {
// 构建WebSocket请求包含认证信息
var myProperties = SpringApp.getBean(MyProperties.class);
String webSocketUrl = "ws://" + myProperties.getServerHost() + ":" + myProperties.getServerPort() + "/ws";
Request request = new Request.Builder()
.url(webSocketUrl)
.build();
@@ -352,14 +359,6 @@ public class WebSocketClientService {
return online;
}
public void withSession(Consumer<WebSocketClientSession> sessionConsumer) {
WebSocketClientSession session = createSession();
try {
sessionConsumer.accept(session);
} finally {
// closeSession(session);vvvv
}
}
public void closeSession(WebSocketClientSession session) {
if (session != null) {

View File

@@ -1,22 +1,19 @@
package com.ecep.contract;
import com.ecep.contract.constant.WebSocketConstant;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import java.beans.PropertyDescriptor;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import com.ecep.contract.constant.WebSocketConstant;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
/**
*
*/
@@ -27,6 +24,8 @@ public class WebSocketClientSession {
*/
@Getter
private final String sessionId = UUID.randomUUID().toString();
@Getter
private boolean done = false;
private WebSocketClientTasker tasker;
@@ -69,6 +68,8 @@ public class WebSocketClientSession {
handleAsStart(args);
} else if (type.equals("done")) {
handleAsDone(args);
done = true;
close();
} else {
tasker.updateMessage(java.util.logging.Level.INFO, "未知的消息类型: " + node.toString());
}
@@ -83,7 +84,6 @@ public class WebSocketClientSession {
private void handleAsDone(JsonNode args) {
tasker.updateMessage(java.util.logging.Level.INFO, "任务完成");
close();
}
private void handleAsProgress(JsonNode args) {

View File

@@ -5,39 +5,47 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
/**
* WebSocket客户端任务接口
* 定义了所有通过WebSocket与服务器通信的任务的通用方法
* 包括任务名称、更新消息、更新标题、更新进度等操作
* <p>
* 所有通过WebSocket与服务器通信的任务类都应实现此接口, 文档参考 .trace/rules/client_task_rules.md
*/
public interface WebSocketClientTasker {
/**
* s
* 获取任务名称
*
* 任务名称用于唯一标识任务, 服务器端会根据任务名称来调用对应的任务处理函数
*
* @return 任务名称
*/
String getTaskName();
/**
* 更新任务执行过程中的消息
*
* @param level 消息级别
* @param message 消息内容
* 客户端可以通过此方法向用户展示任务执行过程中的重要信息或错误提示
*
* @param level 消息级别, 用于区分不同类型的消息, 如INFO, WARNING, SEVERE等
* @param message 消息内容, 可以是任意字符串, 用于展示给用户
*/
void updateMessage(Level level, String message);
/**
* 更新任务标题
*
* 客户端可以通过此方法向用户展示任务的当前执行状态或重要信息
*
* @param title 任务标题
*/
void updateTitle(String title);
/**
* 更新任务进度
*
* 客户端可以通过此方法向用户展示任务的执行进度, 如文件上传进度、数据库操作进度等
*
* @param current 当前进度
* @param total 总进度
*/
@@ -49,11 +57,15 @@ public interface WebSocketClientTasker {
*/
default void cancelTask() {
// 默认实现为空,由具体任务重写
// 需要获取到 session
// 发送 cancel 指令
// 关闭 session.close()
}
/**
* 调用远程WebSocket任务
*
* 客户端可以通过此方法向服务器提交任务, 并等待服务器返回任务执行结果
*
* @param holder 消息持有者,用于记录任务执行过程中的消息
* @param locale 语言环境
* @param args 任务参数
@@ -69,22 +81,31 @@ public interface WebSocketClientTasker {
return null;
}
webSocketService.withSession(session -> {
try {
session.submitTask(this, locale, args);
holder.info("已提交任务到服务器: " + getTaskName());
} catch (JsonProcessingException e) {
String errorMsg = "任务提交失败: " + e.getMessage();
holder.warn(errorMsg);
throw new RuntimeException("任务提交失败: " + e.getMessage(), e);
}
});
WebSocketClientSession session = webSocketService.createSession();
try {
session.submitTask(this, locale, args);
holder.info("已提交任务到服务器: " + getTaskName());
} catch (JsonProcessingException e) {
String errorMsg = "任务提交失败: " + e.getMessage();
holder.warn(errorMsg);
throw new RuntimeException("任务提交失败: " + e.getMessage(), e);
}
// while (!session.isDone()) {
// // 使用TimeUnit
// try {
// TimeUnit.SECONDS.sleep(1);
// } catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }
// }
return null;
}
/**
* 异步调用远程WebSocket任务
*
*
* @param holder 消息持有者,用于记录任务执行过程中的消息
* @param locale 语言环境
* @param args 任务参数
@@ -106,7 +127,7 @@ public interface WebSocketClientTasker {
/**
* 生成唯一的任务ID
*
*
* @return 唯一的任务ID
*/
default String generateTaskId() {

View File

@@ -45,48 +45,55 @@ public abstract class AbstEntityController<T extends IdentityEntity, TV extends
protected CompletableFuture<T> loadedFuture;
private final ObservableList<TabSkin> tabSkins = FXCollections.observableArrayList();
@Override
public void onShown(WindowEvent windowEvent) {
super.onShown(windowEvent);
// 载数据
initializeData();
// 注册 skin
registerTabSkins();
// 初始化保存按钮
initializeSaveBtn();
// 安装 skin
installTabSkins();
}
protected void initializeSaveBtn() {
if (saveBtn != null) {
saveBtn.disableProperty().bind(createTabSkinChangedBindings().not());
saveBtn.setOnAction(event -> saveTabSkins());
}
}
protected void initializeData() {
ViewModelService<T, TV> service = getViewModelService();
if (service instanceof QueryService<T, TV> queryService) {
setStatus("读取...");
loadedFuture = queryService.asyncFindById(viewModel.getId().get());
loadedFuture.thenAccept(entity -> {
// fixed, bind change if new view model create
if (viewModel != null) {
viewModel.bindListener();
}
setStatus();
// BaseViewModel.updateInFxApplicationThread(entity, viewModel);
});
loadedFuture.exceptionally(ex -> {
handleException("载入失败,#" + viewModel.getId().get(), ex);
return null;
});
} else {
loadedFuture = CompletableFuture.supplyAsync(() -> {
T entity = getViewModelService().findById(viewModel.getId().get());
if (entity == null) {
return null;
}
Platform.runLater(() -> {
setStatus();
viewModel.update(entity);
});
viewModel.bindListener();
return entity;
return getViewModelService().findById(viewModel.getId().get());
});
}
registerTabSkins();
if (saveBtn != null) {
saveBtn.disableProperty().bind(createTabSkinChangedBindings().not());
saveBtn.setOnAction(event -> saveTabSkins());
}
loadedFuture.thenAccept(entity -> {
// fixed, bind change if new view model create
if (viewModel != null) {
updateViewModel(entity);
viewModel.bindListener();
}
setStatus();
});
loadedFuture.exceptionally(ex -> {
handleException("载入失败,#" + viewModel.getId().get(), ex);
return null;
});
}
installTabSkins();
protected void updateViewModel(T entity) {
BaseViewModel.updateInFxApplicationThread(entity, viewModel);
}
public T getEntity() {
@@ -108,6 +115,19 @@ public abstract class AbstEntityController<T extends IdentityEntity, TV extends
return saved;
}
public void save() {
T entity = getEntity();
if (entity == null) {
return;
}
if (viewModel.copyTo(entity)) {
save(entity);
}
}
/**
* 注册 skin
*/
protected void registerTabSkins() {
}
@@ -119,6 +139,9 @@ public abstract class AbstEntityController<T extends IdentityEntity, TV extends
return f;
}
/**
* 安装 skin
*/
protected void installTabSkins() {
for (TabSkin tabSkin : tabSkins) {
try {
@@ -229,10 +252,10 @@ public abstract class AbstEntityController<T extends IdentityEntity, TV extends
}
return CompletableFuture.allOf(list
.stream()
.map(RefreshableSkin::refresh)
.filter(Objects::nonNull)
.toArray(CompletableFuture<?>[]::new))
.stream()
.map(RefreshableSkin::refresh)
.filter(Objects::nonNull)
.toArray(CompletableFuture<?>[]::new))
.whenComplete((v, ex) -> {
if (ex != null) {
future.completeExceptionally(ex);

View File

@@ -9,7 +9,8 @@ import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import com.ecep.contract.util.ContextUtils;
import com.ecep.contract.util.BeanContext;
import com.ecep.contract.util.DefaultBeanContext;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.util.FxmlUtils;
import com.ecep.contract.vo.EmployeeVo;
@@ -42,7 +43,7 @@ import javafx.stage.Window;
import javafx.stage.WindowEvent;
import lombok.Getter;
public class BaseController implements ContextUtils {
public class BaseController implements BeanContext {
private static final Logger logger = LoggerFactory.getLogger(BaseController.class);
public static HashMap<String, Stage> stages = new HashMap<>();
@@ -51,7 +52,7 @@ public class BaseController implements ContextUtils {
}
public static <T extends BaseController> CompletableFuture<Void> show(Class<T> clz, Window owner,
Consumer<T> consumer) {
Consumer<T> consumer) {
String key = clz.getName();
if (toFront(key)) {
return null;
@@ -188,20 +189,13 @@ public class BaseController implements ContextUtils {
public Label rightStatusLabel;
private EmployeeVo currentUser;
private HashMap<Class<?>, Object> cachedBeans = new HashMap<>();
private BeanContext beanContext = new DefaultBeanContext();
public <T> T getBean(Class<T> requiredType) throws BeansException {
return SpringApp.getBean(requiredType);
return beanContext.getBean(requiredType);
}
@SuppressWarnings("unchecked")
public <T> T getCachedBean(Class<T> requiredType) throws BeansException {
Object object = cachedBeans.get(requiredType);
if (object == null) {
object = getBean(requiredType);
cachedBeans.put(requiredType, object);
}
return (T) object;
return beanContext.getCachedBean(requiredType);
}
public SysConfService getConfService() {

View File

@@ -1,5 +1,6 @@
package com.ecep.contract.controller.bank;
import org.controlsfx.control.SearchableComboBox;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
@@ -34,7 +35,6 @@ public class BankManagerWindowController
}
public void onCreateNewAction(ActionEvent event) {
}
public void onReBuildFilesAction(ActionEvent event) {

View File

@@ -4,6 +4,7 @@ import com.ecep.contract.controller.AbstEntityManagerSkin;
import com.ecep.contract.controller.ManagerSkin;
import com.ecep.contract.controller.company.CompanyWindowController;
import com.ecep.contract.controller.table.cell.CompanyTableCell;
import com.ecep.contract.controller.table.cell.LocalDateFieldTableCell;
import com.ecep.contract.controller.table.cell.LocalDateTimeTableCell;
import com.ecep.contract.service.CompanyService;
import com.ecep.contract.service.YongYouU8Service;
@@ -15,6 +16,7 @@ import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.cell.CheckBoxTableCell;
public class YongYouU8ManagerSkin
extends
@@ -36,10 +38,9 @@ public class YongYouU8ManagerSkin
@Override
public void initializeTable() {
controller.idColumn.setCellValueFactory(param -> param.getValue().getId());
controller.companyColumn.setCellValueFactory(param -> param.getValue().getCompany());
controller.companyColumn.setCellFactory(param -> new CompanyTableCell<>(getCompanyService()));
controller.cloudIdColumn.setCellValueFactory(param -> param.getValue().getCloudId());
controller.companyColumn.setCellValueFactory(param -> param.getValue().getCompany());
controller.companyColumn.setCellFactory( CompanyTableCell.forTableColumn(getCompanyService()));
controller.latestUpdateColumn.setCellValueFactory(param -> param.getValue().getLatestUpdate());
controller.latestUpdateColumn.setCellFactory(param -> new LocalDateTimeTableCell<>());
@@ -47,7 +48,14 @@ public class YongYouU8ManagerSkin
controller.cloudLatestColumn.setCellValueFactory(param -> param.getValue().getCloudLatest());
controller.cloudLatestColumn.setCellFactory(param -> new LocalDateTimeTableCell<>());
controller.descriptionColumn.setCellValueFactory(param -> param.getValue().getVendorCode());
controller.cloudVendorUpdateDateColumn.setCellValueFactory(param -> param.getValue().getVendorUpdateDate());
controller.cloudCustomerUpdateDateColumn.setCellValueFactory(param -> param.getValue().getCustomerUpdateDate());
controller.activeColumn.setCellValueFactory(param -> param.getValue().getActive());
controller.activeColumn.setCellFactory(CheckBoxTableCell.forTableColumn(controller.activeColumn));
controller.descriptionColumn.setCellValueFactory(param -> param.getValue().getExceptionMessage());
}
@Override
@@ -73,7 +81,7 @@ public class YongYouU8ManagerSkin
}
for (CloudYuInfoViewModel selectedItem : selectedItems) {
CloudYuVo yongYouU8Vo = getU8Service().findById(selectedItem.getId().get());
selectedItem.getCustomerCode().set("");
selectedItem.getExceptionMessage().set("");
if (selectedItem.copyTo(yongYouU8Vo)) {
CloudYuVo saved = getU8Service().save(yongYouU8Vo);
selectedItem.update(saved);

View File

@@ -37,8 +37,10 @@ public class YongYouU8ManagerWindowController
public TableColumn<CloudYuInfoViewModel, Number> idColumn;
public TableColumn<CloudYuInfoViewModel, LocalDateTime> latestUpdateColumn;
public TableColumn<CloudYuInfoViewModel, Integer> companyColumn;
public TableColumn<CloudYuInfoViewModel, String> cloudIdColumn;
public TableColumn<CloudYuInfoViewModel, LocalDateTime> cloudLatestColumn;
public TableColumn<CloudYuInfoViewModel, java.time.LocalDate> cloudVendorUpdateDateColumn;
public TableColumn<CloudYuInfoViewModel, java.time.LocalDate> cloudCustomerUpdateDateColumn;
public TableColumn<CloudYuInfoViewModel, Boolean> activeColumn;
public TableColumn<CloudYuInfoViewModel, String> descriptionColumn;
@Override
@@ -91,27 +93,32 @@ public class YongYouU8ManagerWindowController
public void onContractGroupSyncAction(ActionEvent event) {
ContractGroupSyncTask task = new ContractGroupSyncTask();
Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
UITools.showTaskDialogAndWait("合同组数据同步", task, null);
// Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
}
public void onContractTypeSyncAction(ActionEvent event) {
ContractTypeSyncTask task = new ContractTypeSyncTask();
Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
UITools.showTaskDialogAndWait("合同类型数据同步", task, null);
// Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
}
public void onContractKindSyncAction(ActionEvent event) {
ContractKindSyncTask task = new ContractKindSyncTask();
Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
UITools.showTaskDialogAndWait("合同类型数据同步", task, null);
// Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
}
public void onVendorClassSyncAction(ActionEvent event) {
VendorClassSyncTask task = new VendorClassSyncTask();
Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
UITools.showTaskDialogAndWait("客户分类数据同步", task, null);
// Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
}
public void onCustomerClassSyncAction(ActionEvent event) {
CustomerClassSyncTask task = new CustomerClassSyncTask();
Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
UITools.showTaskDialogAndWait("客户分类数据同步", task, null);
// Desktop.instance.getTaskMonitorCenter().registerAndStartTask(task);
}
}

View File

@@ -0,0 +1,24 @@
package com.ecep.contract.controller.contract;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecep.contract.controller.AbstEntityManagerSkin;
import com.ecep.contract.vm.ContractInvoiceViewModel;
import com.ecep.contract.vo.ContractInvoiceVo;
public class ContractInvoiceManagerSkin extends
AbstEntityManagerSkin<ContractInvoiceVo, ContractInvoiceViewModel, ContractInvoiceManagerSkin, ContractInvoiceManagerWindowController> {
private static final Logger logger = LoggerFactory.getLogger(ContractInvoiceManagerSkin.class);
public ContractInvoiceManagerSkin(ContractInvoiceManagerWindowController controller) {
super(controller);
}
@Override
public void initializeTable() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'initializeTable'");
}
}

View File

@@ -0,0 +1,48 @@
package com.ecep.contract.controller.contract;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import com.ecep.contract.controller.AbstManagerWindowController;
import com.ecep.contract.service.ContractInvoiceService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.vm.ContractInvoiceViewModel;
import com.ecep.contract.vo.ContractInvoiceVo;
import javafx.scene.control.TableColumn;
import javafx.stage.Stage;
@Lazy
@Scope("prototype")
@Component
@FxmlPath("/ui/contract/contract-invoice-manager.fxml")
public class ContractInvoiceManagerWindowController extends AbstManagerWindowController<ContractInvoiceVo, ContractInvoiceViewModel, ContractInvoiceManagerSkin> {
public TableColumn<ContractInvoiceViewModel, Number> idColumn;
public TableColumn<ContractInvoiceViewModel, String> codeColumn;
public TableColumn<ContractInvoiceViewModel, String> nameColumn;
public TableColumn<ContractInvoiceViewModel, Integer> contractColumn;
public TableColumn<ContractInvoiceViewModel, Integer> contractItemColumn;
@Autowired
private ContractInvoiceService contractInvoiceService;
@Override
public ContractInvoiceService getViewModelService() {
return contractInvoiceService;
}
@Override
protected ContractInvoiceManagerSkin createDefaultSkin() {
ContractInvoiceManagerSkin skin = new ContractInvoiceManagerSkin(this);
return skin;
}
@Override
public void show(Stage stage) {
super.show(stage);
getTitle().set("合同发票管理");
}
}

View File

@@ -0,0 +1,50 @@
package com.ecep.contract.controller.contract;
import org.springframework.stereotype.Component;
import com.ecep.contract.controller.AbstEntityController;
import com.ecep.contract.controller.tab.ContractInvoiceTabSkinBase;
import com.ecep.contract.service.ContractInvoiceService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.vm.ContractInvoiceViewModel;
import com.ecep.contract.vo.ContractInvoiceVo;
import javafx.collections.ObservableList;
import javafx.scene.control.Button;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.stage.Window;
@FxmlPath("/ui/contract/contract-invoice.fxml")
@Component
public class ContractInvoiceWindowController
extends AbstEntityController<ContractInvoiceVo, ContractInvoiceViewModel> {
public static void show(ContractInvoiceViewModel viewModel, Window owner) {
show(ContractInvoiceWindowController.class, viewModel, owner);
}
public Tab baseInfoTab;
public TextField codeField;
public TextField nameField;
public TextField invoiceField;
public TextField amountField;
public TextArea remarkField;
public Button saveBtn;
public Button cancelBtn;
@Override
public ContractInvoiceService getViewModelService() {
return getBean(ContractInvoiceService.class);
}
@Override
protected void registerTabSkins() {
TabPane tabPane = baseInfoTab.getTabPane();
ObservableList<Tab> tabs = tabPane.getTabs();
registerTabSkin(baseInfoTab, tab -> new ContractInvoiceTabSkinBase(this));
}
}

View File

@@ -0,0 +1,140 @@
package com.ecep.contract.controller.contract;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import com.ecep.contract.controller.AbstEntityController;
import com.ecep.contract.controller.inventory.InventoryWindowController;
import com.ecep.contract.controller.tab.ContractItemTabSkinBase;
import com.ecep.contract.service.ContractItemService;
import com.ecep.contract.service.InventoryService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.util.UITools;
import com.ecep.contract.vm.ContractItemViewModel;
import com.ecep.contract.vm.InventoryViewModel;
import com.ecep.contract.vo.ContractItemVo;
import com.ecep.contract.vo.InventoryVo;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
@Lazy
@Scope("prototype")
@Component
@FxmlPath("/ui/contract/contract-item.fxml")
public class ContractItemWindowController
extends AbstEntityController<ContractItemVo, ContractItemViewModel> {
public static void show(ContractItemVo contractItem, Window owner) {
show(ContractItemViewModel.from(contractItem), owner);
}
/**
* 显示界面
*/
public static void show(ContractItemViewModel viewModel, Window window) {
show(ContractItemWindowController.class, viewModel, window);
}
public BorderPane root;
public Tab baseInfoTab;
// 基本信息字段
public Label idLabel;
public TextField codeField;
public TextField titleField;
public TextField specificationField;
public TextField unitField;
public TextField inventoryField;
public Button openInventoryBtn;
// 财务信息字段
public TextField exclusiveTaxPriceField;
public TextField taxRateField;
public TextField taxPriceField;
public TextField quantityField;
public TextField taxAmountField;
public TextField exclusiveTaxAmountField;
// 日期信息字段
public DatePicker startDateField;
public DatePicker endDateField;
public TextField createDateLabel;
public TextField updateDateLabel;
public TextField creatorLabel;
public TextField updaterLabel;
// 其他信息
public TextField refIdField;
public TextArea remarkField;
@Override
public ContractItemService getViewModelService() {
return getCachedBean(ContractItemService.class);
}
public InventoryService getInventoryService() {
return getCachedBean(InventoryService.class);
}
public void onShown(WindowEvent windowEvent) {
super.onShown(windowEvent);
getTitle().bind(viewModel.getTitle()
.map(title -> "[" + viewModel.getId() + "] " + title + " 合同内容详情"));
root.getScene().getStylesheets().add("/ui/contract/contract.css");
}
@Override
protected void registerTabSkins() {
super.registerTabSkins();
TabPane tabPane = baseInfoTab.getTabPane();
ObservableList<Tab> tabs = tabPane.getTabs();
registerTabSkin(baseInfoTab, tab -> new ContractItemTabSkinBase(this));
}
/**
* 打开关联的存货信息
*/
public void onOpenInventoryAction(ActionEvent event) {
try {
if (viewModel.getInventory() == null) {
UITools.showAlertAndWait("没有关联的存货信息");
return;
}
Integer inventoryId = viewModel.getInventory().get();
InventoryVo inventory = getInventoryService().findById(inventoryId);
if (inventory != null) {
InventoryWindowController.show(InventoryViewModel.from(inventory), root.getScene().getWindow());
} else {
UITools.showAlertAndWait("未找到关联的存货信息");
}
} catch (Exception e) {
UITools.showAlertAndWait("打开存货信息失败: " + e.getMessage());
}
}
/**
* 计算税额和金额
*/
public void onCalculateAmountsAction(ActionEvent event) {
try {
viewModel.calculateAmounts();
UITools.showAlertAndWait("金额计算完成");
} catch (Exception e) {
UITools.showAlertAndWait("计算失败: " + e.getMessage());
}
}
}

View File

@@ -1,13 +1,13 @@
package com.ecep.contract.controller.tab;
package com.ecep.contract.controller.contract;
import com.ecep.contract.*;
import com.ecep.contract.constant.ContractConstant;
import com.ecep.contract.controller.contract.AbstContractTableTabSkin;
import com.ecep.contract.controller.contract.ContractWindowController;
import com.ecep.contract.controller.tab.ContractFilesRebuildTasker;
import com.ecep.contract.controller.tab.CustomerContractCostFormUpdateTask;
import com.ecep.contract.controller.tab.TabSkin;
import com.ecep.contract.controller.table.EditableEntityTableTabSkin;
import com.ecep.contract.controller.table.cell.ContractFileTypeTableCell;
import com.ecep.contract.controller.table.cell.LocalDateFieldTableCell;
import com.ecep.contract.model.ContractFileTypeLocal;
import com.ecep.contract.service.ContractFileService;
import com.ecep.contract.service.ContractFileTypeService;
import com.ecep.contract.util.FxmlPath;
@@ -21,7 +21,6 @@ import javafx.event.ActionEvent;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.WindowEvent;
import javafx.util.StringConverter;
import lombok.Setter;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.io.IOUtils;
@@ -94,21 +93,6 @@ public class ContractTabSkinFiles
return controller.fileTab;
}
static class ContractFileTypeLocalStringConverter extends StringConverter<ContractFileTypeLocal> {
@Override
public String toString(ContractFileTypeLocal local) {
if (local == null) {
return "-";
}
return local.getValue();
}
@Override
public ContractFileTypeLocal fromString(String string) {
return null;
}
}
@Override
public void initializeTab() {
TableView<ContractFileViewModel> table = getTableView();
@@ -134,6 +118,7 @@ public class ContractTabSkinFiles
.setCellFactory(ContractFileTypeTableCell.forTableColumn(getCachedBean(ContractFileTypeService.class)));
fileTable_typeColumn.setEditable(false);
/* 文件名编辑器 */
fileTable_filePathColumn.setCellValueFactory(param -> param.getValue().getFileName());
fileTable_filePathColumn.setCellFactory(TextFieldTableCell.forTableColumn());
@@ -193,6 +178,34 @@ public class ContractTabSkinFiles
createVendorContractRequestByTemplateUpdateMenuItem(),
createVendorContractApplyByTemplateUpdateMenuItem());
runAsync(() -> {
getCachedBean(ContractFileTypeService.class).findAll(getLocale()).forEach((k, v) -> {
if (isCustomer && !k.isSupportCustomer()) {
return;
}
if (isVendor && !k.isSupportVendor()) {
return;
}
{
MenuItem item = new MenuItem();
item.setText(v.getValue());
item.getProperties().put("typeLocal", v);
item.setOnAction(this::onFileTableContextMenuChangeTypeAndNameAction);
fileTable_menu_change_type.getItems().add(item);
}
if (StringUtils.hasText(v.getSuggestFileName())) {
MenuItem item = new MenuItem();
item.setText(v.getValue());
item.getProperties().put("typeLocal", v);
item.getProperties().put("rename", true);
item.setOnAction(this::onFileTableContextMenuChangeTypeAndNameAction);
fileTable_menu_change_type_and_name.getItems().add(item);
}
});
});
super.initializeTab();
}
@@ -540,13 +553,13 @@ public class ContractTabSkinFiles
//
fileTable_menu_change_type.setVisible(true);
for (MenuItem item : fileTable_menu_change_type.getItems()) {
ContractFileTypeLocal typeLocal = (ContractFileTypeLocal) item.getProperties().get("typeLocal");
ContractFileTypeLocalVo typeLocal = (ContractFileTypeLocalVo) item.getProperties().get("typeLocal");
item.setVisible(typeLocal.getType() != selectedItem.getType().get());
}
fileTable_menu_change_type_and_name.setVisible(true);
for (MenuItem item : fileTable_menu_change_type.getItems()) {
ContractFileTypeLocal typeLocal = (ContractFileTypeLocal) item.getProperties().get("typeLocal");
ContractFileTypeLocalVo typeLocal = (ContractFileTypeLocalVo) item.getProperties().get("typeLocal");
item.setVisible(typeLocal.getType() != selectedItem.getType().get());
}
@@ -559,7 +572,7 @@ public class ContractTabSkinFiles
if (selectedItems == null || selectedItems.isEmpty()) {
return;
}
ContractFileTypeLocal typeLocal = (ContractFileTypeLocal) item.getProperties().get("typeLocal");
ContractFileTypeLocalVo typeLocal = (ContractFileTypeLocalVo) item.getProperties().get("typeLocal");
if (typeLocal == null) {
return;
}

View File

@@ -1,7 +1,5 @@
package com.ecep.contract.controller.tab;
package com.ecep.contract.controller.contract;
import com.ecep.contract.controller.contract.AbstContractTableTabSkin;
import com.ecep.contract.controller.contract.ContractWindowController;
import com.ecep.contract.controller.table.cell.LocalDateTimeTableCell;
import com.ecep.contract.service.ContractPayPlanService;
import com.ecep.contract.util.FxmlPath;

View File

@@ -1,18 +1,17 @@
package com.ecep.contract.controller.tab;
package com.ecep.contract.controller.contract;
import java.time.LocalDateTime;
import com.ecep.contract.ContractPayWay;
import com.ecep.contract.controller.tab.TabSkin;
import com.ecep.contract.controller.table.cell.EmployeeTableCell;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.controller.contract.AbstContractTableTabSkin;
import com.ecep.contract.controller.contract.ContractWindowController;
import com.ecep.contract.controller.vendor.purchase.order.PurchaseOrderWindowController;
import com.ecep.contract.converter.EmployeeStringConverter;
import com.ecep.contract.service.CompanyService;
import com.ecep.contract.service.PurchaseOrdersService;
import com.ecep.contract.vm.PurchaseOrderViewModel;
import com.ecep.contract.vo.PurchaseOrderVo;
import com.ecep.contract.controller.table.cell.CompanyTableCell;
import com.ecep.contract.controller.table.cell.LocalDateTimeTableCell;
import javafx.scene.control.MenuItem;
@@ -70,17 +69,16 @@ public class ContractTabSkinPurchaseOrders
idColumn.setCellValueFactory(param -> param.getValue().getId());
codeColumn.setCellValueFactory(param -> param.getValue().getCode());
EmployeeStringConverter converter = getEmployeeStringConverter();
table_makerColumn.setCellValueFactory(param -> param.getValue().getMaker());
table_makerColumn.setCellFactory(CompanyTableCell.forTableColumn(getCompanyService()));
table_makerColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
table_makerDateColumn.setCellValueFactory(param -> param.getValue().getMakerDate());
table_makerDateColumn.setCellFactory(param -> new LocalDateTimeTableCell<>());
table_verifierColumn.setCellValueFactory(param -> param.getValue().getMaker());
table_verifierColumn.setCellFactory(CompanyTableCell.forTableColumn(getCompanyService()));
table_verifierColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
table_verifierDateColumn.setCellValueFactory(param -> param.getValue().getVerifierDate());
table_verifierDateColumn.setCellFactory(param -> new LocalDateTimeTableCell<>());
table_closerColumn.setCellValueFactory(param -> param.getValue().getMaker());
table_closerColumn.setCellFactory(CompanyTableCell.forTableColumn(getCompanyService()));
table_closerColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
table_closerDateColumn.setCellValueFactory(param -> param.getValue().getCloserDate());
table_closerDateColumn.setCellFactory(param -> new LocalDateTimeTableCell<>());

View File

@@ -1,25 +1,23 @@
package com.ecep.contract.controller.tab;
package com.ecep.contract.controller.contract;
import java.time.LocalDate;
import com.ecep.contract.ContractPayWay;
import com.ecep.contract.controller.table.cell.EmployeeTableCell;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.controller.contract.AbstContractTableTabSkin;
import com.ecep.contract.controller.contract.ContractWindowController;
import com.ecep.contract.controller.contract.sale_order.SalesOrderWindowController;
import com.ecep.contract.controller.tab.TabSkin;
import com.ecep.contract.controller.table.cell.CompanyTableCell;
import com.ecep.contract.controller.table.cell.EmployeeTableCell;
import com.ecep.contract.controller.table.cell.LocalDateFieldTableCell;
import com.ecep.contract.converter.EmployeeStringConverter;
import com.ecep.contract.service.CompanyService;
import com.ecep.contract.service.SaleOrdersService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.vm.SalesOrderViewModel;
import com.ecep.contract.vo.SalesOrderVo;
import javafx.scene.control.Button;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import lombok.Setter;
/**
@@ -32,6 +30,8 @@ public class ContractTabSkinSaleOrders
@Setter
private SaleOrdersService saleOrdersService;
private CompanyService companyService;
@Setter
private EmployeeStringConverter employeeStringConverter;
@@ -51,10 +51,21 @@ public class ContractTabSkinSaleOrders
*/
public TableColumn<SalesOrderViewModel, Integer> verifierColumn;
public TableColumn<SalesOrderViewModel, LocalDate> verifierDateColumn;
public TableColumn<SalesOrderViewModel, Number> refIdColumn;
public TableColumn<SalesOrderViewModel, Number> taxRateColumn;
public TableColumn<SalesOrderViewModel, String> customerAddressColumn;
/**
* 修改人, Employee
*/
public TableColumn<SalesOrderViewModel, Integer> modifierColumn;
public TableColumn<SalesOrderViewModel, LocalDate> modifierDateColumn;
/**
* 关闭人, Employee
*/
public TableColumn<SalesOrderViewModel, Integer> closerColumn;
public TableColumn<SalesOrderViewModel, LocalDate> closerDateColumn;
public TableColumn<SalesOrderViewModel, String> descriptionColumn;
public MenuItem subContractTable_menu_refresh;
public TextField contractSearchKeyField;
public Button searchBtn;
private Tab tab;
public ContractTabSkinSaleOrders(ContractWindowController controller, Tab tab) {
@@ -81,31 +92,37 @@ public class ContractTabSkinSaleOrders
@Override
public void initializeTab() {
contractSearchKeyField.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.ENTER) {
searchBtn.fire();
}
});
searchBtn.setOnAction(this::onTableRefreshAction);
super.initializeTab();
subContractTable_menu_refresh.setOnAction(this::onTableRefreshAction);
idColumn.setCellValueFactory(param -> param.getValue().getId());
codeColumn.setCellValueFactory(param -> param.getValue().getCode());
employeeColumn.setCellValueFactory(param -> param.getValue().getEmployee());
employeeColumn.setCellFactory(param -> new EmployeeTableCell<>(getEmployeeService()));
employeeColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
makerColumn.setCellValueFactory(param -> param.getValue().getMaker());
makerColumn.setCellFactory(param -> new EmployeeTableCell<>(getEmployeeService()));
makerColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
makerDateColumn.setCellValueFactory(param -> param.getValue().getMakerDate());
makerDateColumn.setCellFactory(LocalDateFieldTableCell.forTableColumn());
verifierColumn.setCellValueFactory(param -> param.getValue().getVerifier());
verifierColumn.setCellFactory(param -> new EmployeeTableCell<>(getEmployeeService()));
verifierColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
verifierDateColumn.setCellValueFactory(param -> param.getValue().getVerifierDate());
verifierDateColumn.setCellFactory(LocalDateFieldTableCell.forTableColumn());
// 设置新增字段的单元格值工厂和工厂类
refIdColumn.setCellValueFactory(param -> param.getValue().getRefId());
taxRateColumn.setCellValueFactory(param -> param.getValue().getTaxRate());
customerAddressColumn.setCellValueFactory(param -> param.getValue().getCustomerAddress());
modifierColumn.setCellValueFactory(param -> param.getValue().getModifier());
modifierColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
modifierDateColumn.setCellValueFactory(param -> param.getValue().getModifierDate());
modifierDateColumn.setCellFactory(LocalDateFieldTableCell.forTableColumn());
closerColumn.setCellValueFactory(param -> param.getValue().getCloser());
closerColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
closerDateColumn.setCellValueFactory(param -> param.getValue().getCloserDate());
closerDateColumn.setCellFactory(LocalDateFieldTableCell.forTableColumn());
descriptionColumn.setCellValueFactory(param -> param.getValue().getDescription());
super.initializeTab();
}
@Override
@@ -113,17 +130,17 @@ public class ContractTabSkinSaleOrders
SalesOrderWindowController.show(item, getTableView().getScene().getWindow());
}
private EmployeeStringConverter getEmployeeStringConverter() {
if (employeeStringConverter == null) {
employeeStringConverter = getBean(EmployeeStringConverter.class);
}
return employeeStringConverter;
}
SaleOrdersService getSaleOrdersService() {
if (saleOrdersService == null) {
saleOrdersService = getBean(SaleOrdersService.class);
}
return saleOrdersService;
}
CompanyService getCompanyService() {
if (companyService == null) {
companyService = getBean(CompanyService.class);
}
return companyService;
}
}

View File

@@ -1,10 +1,11 @@
package com.ecep.contract.controller.tab;
package com.ecep.contract.controller.contract;
import java.time.LocalDate;
import com.ecep.contract.ContractPayWay;
import com.ecep.contract.controller.contract.AbstContractTableTabSkin;
import com.ecep.contract.controller.contract.ContractWindowController;
import com.ecep.contract.controller.tab.TabSkin;
import com.ecep.contract.controller.table.cell.CompanyTableCell;
import com.ecep.contract.service.CompanyService;
import com.ecep.contract.service.ContractService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.util.ParamUtils;
@@ -34,6 +35,7 @@ public class ContractTabSkinSubContract
public TableColumn<ContractViewModel, LocalDate> subContractTable_inureDateColumn;
public TableColumn<ContractViewModel, LocalDate> subContractTable_orderDateColumn;
public TableColumn<ContractViewModel, LocalDate> subContractTable_varyDateColumn;
public TableColumn<ContractViewModel, Integer> subContractTable_companyColumn;
public MenuItem subContractTable_menu_refresh;
public TextField contractSearchKeyField;
public Button contractSearchBtn;
@@ -86,6 +88,8 @@ public class ContractTabSkinSubContract
subContractTable_inureDateColumn.setCellValueFactory(param -> param.getValue().getInureDate());
subContractTable_orderDateColumn.setCellValueFactory(param -> param.getValue().getOrderDate());
subContractTable_varyDateColumn.setCellValueFactory(param -> param.getValue().getVaryDate());
subContractTable_companyColumn.setCellValueFactory(param -> param.getValue().getCompany());
subContractTable_companyColumn.setCellFactory(CompanyTableCell.forTableColumn(getCachedBean(CompanyService.class)));
Platform.runLater(() -> {
getTableView().getSortOrder().add(subContractTable_codeColumn);

View File

@@ -1,4 +1,4 @@
package com.ecep.contract.controller.tab;
package com.ecep.contract.controller.contract;
import java.util.List;
@@ -7,8 +7,7 @@ import org.controlsfx.control.textfield.TextFields;
import com.ecep.contract.ContractPayWay;
import com.ecep.contract.SpringApp;
import com.ecep.contract.controller.contract.AbstContractTableTabSkin;
import com.ecep.contract.controller.contract.ContractWindowController;
import com.ecep.contract.controller.tab.TabSkin;
import com.ecep.contract.controller.table.EditableEntityTableTabSkin;
import com.ecep.contract.controller.table.cell.CompanyTableCell;
import com.ecep.contract.controller.table.cell.ContractFileTableCell;

View File

@@ -12,6 +12,9 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import javafx.scene.Node;
import javafx.scene.control.*;
import org.controlsfx.control.PopOver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -50,13 +53,6 @@ import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
@@ -79,6 +75,7 @@ public class ContractVerifyWindowController extends BaseController {
return super.show(loader, owner, modality);
}
@Setter
@Getter
public static class MessageExt extends Message {
@@ -90,12 +87,16 @@ public class ContractVerifyWindowController extends BaseController {
}
@Data
public static class Model implements MessageHolder {
public static class Model {
private SimpleStringProperty code = new SimpleStringProperty();
private SimpleStringProperty name = new SimpleStringProperty();
private SimpleObjectProperty<Integer> employee = new SimpleObjectProperty<>();
private SimpleObjectProperty<LocalDate> setupDate = new SimpleObjectProperty<>();
private SimpleListProperty<MessageExt> messages = new SimpleListProperty<>(FXCollections.observableArrayList());
}
static class MessageHolderImpl implements MessageHolder {
List<MessageExt> messages = new ArrayList<>();
@Override
public void addMessage(Level level, String message) {
@@ -261,6 +262,8 @@ public class ContractVerifyWindowController extends BaseController {
long total = contractService.count(params);
setStatus("合同:" + total + "");
MessageHolderImpl messageHolder = new MessageHolderImpl();
while (true) {
if (isCloseRequested()) {
break;
@@ -268,6 +271,7 @@ public class ContractVerifyWindowController extends BaseController {
Page<ContractVo> page = contractService.findAll(params, pageRequest);
for (ContractVo contract : page) {
messageHolder.messages.clear();
if (isCloseRequested()) {
break;
}
@@ -285,11 +289,11 @@ public class ContractVerifyWindowController extends BaseController {
model.getName().set(contract.getName());
model.getSetupDate().set(contract.getSetupDate());
comm.verify(contract, model);
comm.verify(contract, messageHolder);
setStatus("合同验证进度:" + counter.get() + " / " + total);
// 移除中间消息
if (!model.getMessages().isEmpty()) {
model.getMessages().removeIf(msg -> msg.getLevel().intValue() <= Level.INFO.intValue());
if (!messageHolder.messages.isEmpty()) {
model.getMessages().setAll(messageHolder.messages.stream().filter(msg -> msg.getLevel().intValue() > Level.INFO.intValue()).limit(50).toList());
}
if (model.getMessages().isEmpty()) {
if (onlyShowVerifiedChecker.isSelected()) {
@@ -325,6 +329,7 @@ public class ContractVerifyWindowController extends BaseController {
}
runAsync(() -> {
ContractVo contract = null;
MessageHolderImpl messageHolder = new MessageHolderImpl();
try {
contract = contractService.findByCode(contractCode);
} catch (Exception e) {
@@ -336,17 +341,18 @@ public class ContractVerifyWindowController extends BaseController {
}
model.getMessages().clear();
try {
comm.verify(contract, model);
// 移除中间消息
if (!model.getMessages().isEmpty()) {
model.getMessages().removeIf(msg -> msg.getLevel().intValue() <= Level.INFO.intValue());
}
comm.verify(contract, messageHolder);
} catch (Exception e) {
logger.error(model.getCode().get(), e);
model.error(e.getMessage());
messageHolder.error(e.getMessage());
}
if (model.getMessages().isEmpty()) {
// 移除中间消息
if (!messageHolder.messages.isEmpty()) {
model.getMessages().setAll(messageHolder.messages.stream().filter(msg -> msg.getLevel().intValue() > Level.INFO.intValue()).limit(50).toList());
}
if (messageHolder.messages.isEmpty()) {
Platform.runLater(() -> {
viewTableDataSet.remove(model);
});
@@ -380,6 +386,38 @@ public class ContractVerifyWindowController extends BaseController {
ContractWindowController.show(contract, viewTable.getScene().getWindow());
}
public void onShowVerifyStatusAction(ActionEvent event) {
// 在新新窗口中显示 状态消息 Model# messages
Model selectedItem = viewTable.getSelectionModel().getSelectedItem();
if (selectedItem == null) {
return;
}
ListView<MessageExt> listView = new ListView<>();
listView.setItems(selectedItem.messages);
listView.setCellFactory(v -> new ListCell<>() {
@Override
protected void updateItem(MessageExt item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
} else {
setText(item.getMessage());
setGraphic(new Label(item.getPrefix()));
}
}
});
PopOver popOver = new PopOver(listView);
popOver.setArrowLocation(PopOver.ArrowLocation.TOP_LEFT);
MenuItem menuItem = (MenuItem) event.getSource();
Node node = viewTable.lookup(".table-row-cell:selected");
popOver.show(node);
}
public void onExportVerifyResultAsFileAction(ActionEvent e) {
FileChooser chooser = new FileChooser();
chooser.setTitle("导出核验结果");

View File

@@ -2,6 +2,7 @@ package com.ecep.contract.controller.contract;
import java.io.File;
import javafx.scene.control.*;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@@ -12,14 +13,8 @@ import com.ecep.contract.DesktopUtils;
import com.ecep.contract.controller.AbstEntityController;
import com.ecep.contract.controller.company.CompanyWindowController;
import com.ecep.contract.controller.tab.ContractTabSkinBase;
import com.ecep.contract.controller.tab.ContractTabSkinFiles;
import com.ecep.contract.controller.tab.ContractTabSkinInvoices;
import com.ecep.contract.controller.tab.ContractTabSkinItemsV2;
import com.ecep.contract.controller.tab.ContractTabSkinPayPlan;
import com.ecep.contract.controller.tab.ContractTabSkinPurchaseOrders;
import com.ecep.contract.controller.tab.ContractTabSkinSaleOrders;
import com.ecep.contract.controller.tab.ContractTabSkinSubContract;
import com.ecep.contract.controller.tab.ContractTabSkinVendorBid;
import com.ecep.contract.service.CompanyService;
import com.ecep.contract.service.ContractService;
import com.ecep.contract.task.ContractRepairTask;
@@ -32,13 +27,6 @@ import com.ecep.contract.vo.ContractVo;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
@@ -50,6 +38,8 @@ import javafx.stage.WindowEvent;
public class ContractWindowController
extends AbstEntityController<ContractVo, ContractViewModel> {
public static void show(ContractVo contract, Window owner) {
show(ContractViewModel.from(contract), owner);
}
@@ -75,6 +65,7 @@ public class ContractWindowController
public Button openRelativeCompanyVendorBtn;
public TextField nameField;
public CheckBox contractNameLockedCk;
public TextField guidField;
public TextField codeField;
public TextField parentCodeField;
@@ -151,9 +142,16 @@ public class ContractWindowController
registerTabSkin(contractTab, t -> new ContractTabSkinSubContract(this));
tabs.remove(bidVendorTab);
Tab saleOrderTab = new Tab("销售订单");
payPlanTab.setText("收款计划");
tabs.add(saleOrderTab);
registerTabSkin(saleOrderTab, tab -> new ContractTabSkinSaleOrders(this, tab));
tabs.add(new Tab(""));
Tab invoiceTab = new Tab("");
tabs.add(invoiceTab);
registerTabSkin(invoiceTab, tab -> new ContractTabSkinInvoices(this, tab));
tabs.add(new Tab("出库单"));
tabs.add(new Tab("发货单"));
tabs.add(new Tab("签收单"));
} else if (payWay == ContractPayWay.PAY) {
registerTabSkin(extendVendorInfo, t -> new ContractTabSkinExtendVendorInfo(this));
tabs.remove(contractTab);
@@ -163,9 +161,10 @@ public class ContractWindowController
tabs.add(purchaseOrderTab);
registerTabSkin(purchaseOrderTab, tab -> new ContractTabSkinPurchaseOrders(this, tab));
tabs.add(new Tab("发货"));
tabs.add(new Tab("签收单"));
tabs.add(new Tab("入库"));
tabs.add(new Tab("付款单"));
payPlanTab.setText("付款计划");
}
registerTabSkin(itemTab, tab -> new ContractTabSkinItemsV2(this));

View File

@@ -43,6 +43,15 @@ public class SalesOrderWindowController extends AbstEntityController<SalesOrderV
public TextField makeDateField;
public TextField makerField;
public TextArea descriptionField;
public TextField refIdField;
public TextField taxRateField;
public TextField customerField;
public TextField customerAddressField;
public TextField modifierField;
public TextField modifierDateField;
public TextField closerField;
public TextField closerDateField;
public TextField contractField;
public static void show(SalesOrderViewModel viewModel, Window window) {

View File

@@ -1,11 +1,26 @@
package com.ecep.contract.controller.customer;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import com.ecep.contract.controller.AbstEntityController;
import com.ecep.contract.controller.tab.TabSkin;
import com.ecep.contract.service.CompanyCustomerFileTypeService;
import com.ecep.contract.service.ViewModelService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.vo.CustomerFileTypeLocalVo;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.*;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
import javafx.scene.text.FontWeight;
import javafx.util.converter.LocalDateStringConverter;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
@@ -15,6 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
import com.ecep.contract.controller.BaseController;
@@ -29,11 +45,9 @@ import com.ecep.contract.vm.CustomerFileViewModel;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.ActionEvent;
import javafx.geometry.Bounds;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.CheckBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
@@ -50,29 +64,30 @@ import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
@Lazy
@Scope("prototype")
@Component
public class CompanyCustomerEvaluationFormFileWindowController extends BaseController {
private static final Logger logger = LoggerFactory.getLogger(CompanyCustomerEvaluationFormFileWindowController.class);
@FxmlPath("/ui/company/customer/customer_evaluation_form.fxml")
public class CompanyCustomerEvaluationFormFileWindowController
extends AbstEntityController<CompanyCustomerEvaluationFormFileVo, CompanyCustomerEvaluationFormFileViewModel> {
private static final Logger logger = LoggerFactory
.getLogger(CompanyCustomerEvaluationFormFileWindowController.class);
public static void show(CustomerFileViewModel item, Window window) {
show(CompanyCustomerEvaluationFormFileWindowController.class, window, controller -> {
controller.fileViewModel = item;
});
}
public static void show(CompanyCustomerEvaluationFormFileVo saved, Window window) {
show(CompanyCustomerEvaluationFormFileViewModel.from(saved), window);
}
public static void show(CompanyCustomerEvaluationFormFileViewModel viewModel, Window window) {
String key = viewModel.getClass().getName() + "-" + viewModel.getId().get();
if (toFront(key)) {
return;
}
FxmlUtils.newLoaderAsyncWithRunLater("/ui/company/customer/customer_evaluation_form.fxml", null, loader -> {
CompanyCustomerEvaluationFormFileWindowController controller = loader.getController();
controller.viewModel = viewModel;
controller.show(loader, window, Modality.NONE, key);
});
show(CompanyCustomerEvaluationFormFileWindowController.class, viewModel, window);
}
public Label idField;
public TextField filePathField;
public CheckBox validField;
@@ -91,64 +106,66 @@ public class CompanyCustomerEvaluationFormFileWindowController extends BaseContr
public ScrollPane leftPane;
public Label totalCreditScoreLabel;
private CompanyCustomerEvaluationFormFileViewModel viewModel;
private final SimpleStringProperty catalogProperty = new SimpleStringProperty("");
private final SimpleStringProperty levelProperty = new SimpleStringProperty("");
private final SimpleIntegerProperty score1Property = new SimpleIntegerProperty(-1);
private final SimpleIntegerProperty score2Property = new SimpleIntegerProperty(-1);
private final SimpleIntegerProperty score3Property = new SimpleIntegerProperty(-1);
private final SimpleIntegerProperty score4Property = new SimpleIntegerProperty(-1);
private final SimpleIntegerProperty score5Property = new SimpleIntegerProperty(-1);
private final SimpleIntegerProperty creditLevelProperty = new SimpleIntegerProperty(-1);
private final SimpleIntegerProperty totalCreditScoreProperty = new SimpleIntegerProperty(-1);
private SimpleObjectProperty<Image> imageProperty = new SimpleObjectProperty<>();
private CustomerFileViewModel fileViewModel;
private CompletableFuture<CompanyCustomerEvaluationFormFileVo> loadedFuture;
@Lazy
@Autowired
private CompanyCustomerFileService companyCustomerFileService;
private SimpleBooleanProperty changed = new SimpleBooleanProperty(false);
@Lazy
@Autowired
private CompanyCustomerEvaluationFormFileService evaluationFormFileService;
@Override
public void show(Stage stage) {
super.show(stage);
stage.setFullScreen(false);
// Rectangle2D screenBounds = Screen.getPrimary().getVisualBounds();
//
// stage.setX(screenBounds.getMinX());
// stage.setY(screenBounds.getMinY());
// stage.setWidth(screenBounds.getWidth());
// stage.setHeight(screenBounds.getHeight());
//
// stage.isMaximized();
stage.setMaximized(true);
getTitle().set("客户评估表单");
}
@Override
public void onShown(WindowEvent windowEvent) {
super.onShown(windowEvent);
if (logger.isDebugEnabled()) {
logger.debug("onShown");
}
getTitle().set("客户评估表单");
}
initializePane();
@Override
public ViewModelService<CompanyCustomerEvaluationFormFileVo, CompanyCustomerEvaluationFormFileViewModel> getViewModelService() {
return evaluationFormFileService;
}
loadedFuture = CompletableFuture.supplyAsync(() -> {
int id = viewModel.getId().get();
CustomerFileVo customerFile = companyCustomerFileService.findById(id);
CompanyCustomerEvaluationFormFileVo formFile = evaluationFormFileService.findByCustomerFile(customerFile);
Platform.runLater(() -> update(formFile));
return formFile;
@Override
protected void initializeData() {
CompanyCustomerEvaluationFormFileViewModel viewModel = new CompanyCustomerEvaluationFormFileViewModel();
setViewModel(viewModel);
runAsync(() -> {
CompanyCustomerEvaluationFormFileVo item = getCachedBean(CompanyCustomerEvaluationFormFileService.class)
.findByCustomerFile(fileViewModel.getId().get());
viewModel.getId().set(item.getId());
updateViewModel(item);
super.initializeData();
Platform.runLater(this::initializePane);
});
}
@Override
protected void updateViewModel(CompanyCustomerEvaluationFormFileVo entity) {
super.updateViewModel(entity);
changed.set(false);
}
@Override
public BooleanBinding createTabSkinChangedBindings() {
return viewModel.getChanged().or(fileViewModel.getChanged()).or(changed);
}
@Override
public void saveTabSkins() {
save();
changed.setValue(false);
}
BiConsumer<ToggleGroup, String> stringRadioGroupUpdater = (group, newValue) -> {
if (newValue != null) {
for (Toggle toggle : group.getToggles()) {
@@ -164,14 +181,15 @@ public class CompanyCustomerEvaluationFormFileWindowController extends BaseContr
return;
}
}
changed.set(true);
}
Platform.runLater(() -> {
group.selectToggle(null);
// Toggle first = group.getToggles().getFirst();
// first.setSelected(true);
// first.setSelected(false);
// RadioButton btn = (RadioButton) first;
// btn.setText(newValue);
// Toggle first = group.getToggles().getFirst();
// first.setSelected(true);
// first.setSelected(false);
// RadioButton btn = (RadioButton) first;
// btn.setText(newValue);
});
};
@@ -182,9 +200,11 @@ public class CompanyCustomerEvaluationFormFileWindowController extends BaseContr
}
String data = (String) toggle.getUserData();
property.set(data);
changed.set(true);
};
BiConsumer<ToggleGroup, Number> numberRadioGroupUpdater = (group, newValue) -> {
System.out.println("group = " + group + ", newValue = " + newValue);
String value = String.valueOf(newValue);
for (Toggle toggle : group.getToggles()) {
String data = (String) toggle.getUserData();
@@ -195,12 +215,13 @@ public class CompanyCustomerEvaluationFormFileWindowController extends BaseContr
}
Platform.runLater(() -> {
group.selectToggle(null);
// Toggle first = group.getToggles().getFirst();
// first.setSelected(true);
// first.setSelected(false);
// RadioButton btn = (RadioButton) first;
// btn.setText(String.valueOf(newValue));
// Toggle first = group.getToggles().getFirst();
// first.setSelected(true);
// first.setSelected(false);
// RadioButton btn = (RadioButton) first;
// btn.setText(String.valueOf(newValue));
});
changed.set(true);
};
BiConsumer<SimpleIntegerProperty, Toggle> numberPropertyUpdater = (property, toggle) -> {
@@ -210,9 +231,9 @@ public class CompanyCustomerEvaluationFormFileWindowController extends BaseContr
}
String data = (String) toggle.getUserData();
property.set(Integer.parseInt(data));
changed.set(true);
};
int totalScoreToLevel(int score) {
if (score >= 200) {
return 4;
@@ -227,63 +248,73 @@ public class CompanyCustomerEvaluationFormFileWindowController extends BaseContr
}
boolean calcValid() {
if (creditLevelProperty.get() <= 0) {
if (viewModel.getCreditLevel().get() <= 0) {
return false;
}
if (!StringUtils.hasText(catalogProperty.get())) {
if (!StringUtils.hasText(viewModel.getCatalog().get())) {
return false;
}
if (!StringUtils.hasText(levelProperty.get())) {
if (!StringUtils.hasText(viewModel.getLevel().get())) {
return false;
}
if (score1Property.get() <= 0) {
if (viewModel.getScore1().get() <= 0) {
return false;
}
if (score2Property.get() <= 0) {
if (viewModel.getScore2().get() <= 0) {
return false;
}
if (score3Property.get() <= 0) {
if (viewModel.getScore3().get() <= 0) {
return false;
}
if (score4Property.get() <= 0) {
if (viewModel.getScore4().get() <= 0) {
return false;
}
if (score5Property.get() <= 0) {
if (viewModel.getScore5().get() <= 0) {
return false;
}
if (creditLevelProperty.get() <= 0) {
if (viewModel.getCreditLevel().get() <= 0) {
return false;
}
return true;
}
private void initializePane() {
setStatus("");
idField.textProperty().bind(viewModel.getId().asString());
// filePathField.textProperty().bind(viewModel.getFilePath());
// editFilePathField.textProperty().bind(viewModel.getEditFilePath());
// signDateField.valueProperty().bindBidirectional(viewModel.getSignDate());
// validField.selectedProperty().bindBidirectional(viewModel.getValid());
initializeRadioGroup(catalog, catalogProperty);
initializeRadioGroup(level, levelProperty);
initializeRadioGroup(score1, score1Property);
initializeRadioGroup(score2, score2Property);
initializeRadioGroup(score3, score3Property);
initializeRadioGroup(score4, score4Property);
initializeRadioGroup(score5, score5Property);
creditLevelProperty.addListener((observable, oldValue, newValue) -> {
numberRadioGroupUpdater.accept(creditLevel, newValue);
filePathField.textProperty().bind(fileViewModel.getFilePath());
editFilePathField.textProperty().bind(fileViewModel.getEditFilePath());
String pattern = "yyyy-MM-dd";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
signDateField.setConverter(new LocalDateStringConverter(formatter, null));
signDateField.valueProperty().bindBidirectional(fileViewModel.getSignDate());
signDateField.valueProperty().addListener((observable, oldValue, newValue) -> {
changed.set(true);
});
SimpleIntegerProperty[] scores = new SimpleIntegerProperty[]{score1Property, score2Property, score3Property, score4Property, score5Property};
initializeRadioGroup(catalog, viewModel.getCatalog());
initializeRadioGroup(level, viewModel.getLevel());
initializeRadioGroup(score1, viewModel.getScore1());
initializeRadioGroup(score2, viewModel.getScore2());
initializeRadioGroup(score3, viewModel.getScore3());
initializeRadioGroup(score4, viewModel.getScore4());
initializeRadioGroup(score5, viewModel.getScore5());
// 信用等级
viewModel.getCreditLevel().addListener((observable, oldValue, newValue) -> {
numberRadioGroupUpdater.accept(creditLevel, newValue);
});
numberRadioGroupUpdater.accept(creditLevel, viewModel.getCreditLevel().get());
SimpleIntegerProperty[] scores = new SimpleIntegerProperty[]{viewModel.getScore1(), viewModel.getScore2(),
viewModel.getScore3(), viewModel.getScore4(), viewModel.getScore5()};
totalCreditScoreProperty.bind(Bindings.createIntegerBinding(() -> {
int total = 0;
for (SimpleIntegerProperty score : scores) {
total += score.get();
}
creditLevelProperty.set(totalScoreToLevel(total));
viewModel.getCreditLevel().set(totalScoreToLevel(total));
return total;
}, scores));
@@ -291,70 +322,205 @@ public class CompanyCustomerEvaluationFormFileWindowController extends BaseContr
return "合计总分:" + score;
}));
Bindings.createBooleanBinding(() -> {
boolean valid = calcValid();
// viewModel.getValid().set(valid);
return valid;
}, catalogProperty, levelProperty, score1Property, score2Property, score3Property, score4Property, score5Property, creditLevelProperty).addListener(((observable, oldValue, newValue) -> {
logger.info("valid:{}", newValue);
}));
Bindings.createBooleanBinding(this::calcValid, viewModel.getCatalog(), viewModel.getLevel(),
viewModel.getScore1(), viewModel.getScore2(), viewModel.getScore3(), viewModel.getScore4(),
viewModel.getScore5(), viewModel.getCreditLevel())
.addListener((observable, oldValue, newValue) -> {
fileViewModel.getValid().set(newValue);
changed.set(true);
});
validField.selectedProperty().bindBidirectional(fileViewModel.getValid());
validField.setSelected(fileViewModel.getValid().getValue());
imageView.imageProperty().bind(viewModel.getFilePath().map(path -> {
if (FileUtils.withExtensions(path, FileUtils.PDF)) {
File pdfFile = new File(path);
try (PDDocument pdDocument = Loader.loadPDF(pdfFile)) {
PDFRenderer pdfRenderer = new PDFRenderer(pdDocument);
BufferedImage bufferedImage = pdfRenderer.renderImageWithDPI(0, 300);
// 获取 BufferedImage 的宽度和高度
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
WritableImage writableImage = new WritableImage(width, height);
PixelWriter pixelWriter = writableImage.getPixelWriter();
// 将 BufferedImage 的像素数据复制到 WritableImage
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int argb = bufferedImage.getRGB(x, y);
pixelWriter.setArgb(x, y, argb);
}
}
return writableImage;
} catch (Exception e) {
setStatus(e.getMessage());
}
return null;
} else {
File file = new File(path);
Image image = new Image(file.toURI().toString());
return image;
}
}));
fileViewModel.getFilePath().addListener((observable, oldValue, newValue) -> {
File file = new File(newValue);
loadFile(file);
});
if (StringUtils.hasText(fileViewModel.getFilePath().get())) {
loadFile(new File(fileViewModel.getFilePath().get()));
}
leftPane.widthProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
imageView.setFitWidth(leftPane.getWidth());
imageView.setFitHeight(leftPane.getHeight());
imageView.setFitHeight(-1);
});
});
imageView.setFitWidth(leftPane.getWidth());
imageView.setFitHeight(leftPane.getHeight());
imageView.setFitHeight(-1);
imageView.setOnScroll(event -> {
System.out.println("event = " + event);
System.out.println("event.getDeltaY() = " + event.getDeltaY());
Bounds bounds = imageView.getBoundsInLocal();
// Bounds latestBounds = (Bounds) imageView.getProperties().get("latestBounds");
// if (latestBounds != null) {
// double latestBoundsWidth = latestBounds.getWidth();
// }
// if (bounds.getWidth() < leftPane.getWidth()) {
imageView.setFitWidth(bounds.getWidth() + event.getDeltaY());
// } else {
imageView.setFitHeight(bounds.getHeight() + event.getDeltaY());
// }
imageView.setFitHeight(-1);
event.consume();
});
imageView.setOnMouseClicked(event -> {
System.out.println("imageView.getFitWidth() = " + imageView.getFitWidth());
System.out.println("imageView.getFitHeight() = " + imageView.getFitHeight());
System.out.println("leftPane.getWidth() = " + leftPane.getWidth());
System.out.println("leftPane.getViewportBounds().getWidth() = " + leftPane.getViewportBounds().getWidth());
if (event.getClickCount() == 2 && event.getButton() == MouseButton.PRIMARY) {
Image image = imageView.getImage();
if (image != null) {
System.out.println("image.getWidth() = " + image.getWidth());
if (image.getWidth() > imageView.getFitWidth()) {
imageView.setFitWidth(image.getWidth());
} else {
imageView.setFitWidth(leftPane.getWidth());
}
imageView.setFitHeight(-1);
}
}
});
}
private void loadFile(File file) {
setStatus("文件" + file.getAbsolutePath() + " 加载中...");
if (FileUtils.withExtensions(file.getName(), FileUtils.PDF)) {
loadPdf(file);
return;
}
Image image = new Image(file.toURI().toString(), true);
imageView.setImage(image);
}
private void loadPdf(File pdfFile) {
// 绘制文字, 等待加载
// 创建画布并绘制备用占位文字
javafx.scene.canvas.Canvas canvas = new javafx.scene.canvas.Canvas(leftPane.getWidth(), leftPane.getHeight());
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(javafx.scene.paint.Color.RED);
var h1 = javafx.scene.text.Font.font("Microsoft YaHei", FontWeight.BOLD, 24);
var h2 = javafx.scene.text.Font.font("Microsoft YaHei", FontWeight.NORMAL, 18);
gc.setFont(h1);
gc.fillText(fileViewModel.getType().get().name(), 50, 100);
Runnable updateImage = () -> {
WritableImage writableImage = new WritableImage((int) canvas.getWidth(), (int) canvas.getHeight());
// 将画布内容转为图像
Platform.runLater(() -> {
canvas.snapshot(null, writableImage);
imageView.setImage(writableImage);
});
};
runAsync(() -> {
CustomerFileTypeLocalVo localVo = getCachedBean(CompanyCustomerFileTypeService.class).findByLocaleAndType(getLocale(), fileViewModel.getType().get());
gc.setFill(Color.WHITE);
// 覆盖 fileViewModel.getType() 文字
gc.fillRect(0, 55, canvas.getWidth(), 55);
// 绘制 文件类型
gc.setFill(javafx.scene.paint.Color.RED);
gc.setFont(h1);
gc.fillText(localVo.getValue(), 50, 100);
updateImage.run();
});
gc.setStroke(javafx.scene.paint.Color.BLACK);
gc.setFont(h2);
gc.strokeText("正在加载文件..." + pdfFile.getName(), 50, 150);
updateImage.run();
runAsync(() -> {
//FileSystemUtils.
long fileSize = pdfFile.length();
byte[] bytes = new byte[0];
try (java.io.FileInputStream fis = new java.io.FileInputStream(pdfFile);
java.io.BufferedInputStream bis = new java.io.BufferedInputStream(fis)) {
bytes = new byte[(int) fileSize];
int totalBytesRead = 0;
int bytesRead;
byte[] buffer = new byte[8192]; // 8KB buffer
while ((bytesRead = bis.read(buffer)) != -1) {
System.arraycopy(buffer, 0, bytes, totalBytesRead, bytesRead);
totalBytesRead += bytesRead;
// 更新进度
double progress = (double) totalBytesRead / fileSize * 100;
final String status = String.format("正在加载文件... %s (%.1f%%)",
pdfFile.getName(), progress);
gc.setFill(Color.WHITE);
gc.fillRect(0, 200, canvas.getWidth(), 80);
gc.setFill(Color.BLACK);
gc.setFont(h2);
gc.fillText(status, 50, 250);
// 绘制进度条背景
gc.setFill(Color.LIGHTGRAY);
gc.fillRect(50, 270, 400, 20);
// 绘制进度条
gc.setFill(Color.GREEN);
gc.fillRect(50, 270, 400 * (totalBytesRead / (double) fileSize), 20);
// 绘制进度条边框
gc.setStroke(Color.BLACK);
gc.setLineWidth(1);
gc.strokeRect(50, 270, 400, 20);
updateImage.run();
}
gc.setFill(Color.BLACK);
gc.setFont(h2);
gc.fillText("Loading file: " + pdfFile.getName() + ", size: " + bytes.length + " bytes", 50, 320);
updateImage.run();
} catch (Exception e) {
throw new RuntimeException(e);
}
try (PDDocument pdDocument = Loader.loadPDF(bytes)) {
gc.setFill(Color.BLACK);
gc.setFont(h2);
gc.fillText("PDF has " + pdDocument.getNumberOfPages() + " pages", 50, 380);
updateImage.run();
PDFRenderer pdfRenderer = new PDFRenderer(pdDocument);
BufferedImage bufferedImage = pdfRenderer.renderImageWithDPI(0, 300);
// 获取 BufferedImage 的宽度和高度
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
canvas.resize(width, height);
GraphicsContext graphic = canvas.getGraphicsContext2D();
WritableImage writableImage1 = new WritableImage(width, height);
PixelWriter pixelWriter = writableImage1.getPixelWriter();
// 将 BufferedImage 的像素数据复制到 WritableImage
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int argb = bufferedImage.getRGB(x, y);
pixelWriter.setArgb(x, y, argb);
}
}
setStatus();
Platform.runLater(() -> {
imageView.setImage(writableImage1);
});
} catch (Exception e) {
setStatus(e.getMessage());
}
});
}
@@ -365,6 +531,7 @@ public class CompanyCustomerEvaluationFormFileWindowController extends BaseContr
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
stringPropertyUpdater.accept(property, newValue);
});
stringRadioGroupUpdater.accept(toggleGroup, property.getValue());
}
private void initializeRadioGroup(ToggleGroup toggleGroup, SimpleIntegerProperty property) {
@@ -375,21 +542,8 @@ public class CompanyCustomerEvaluationFormFileWindowController extends BaseContr
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
numberPropertyUpdater.accept(property, newValue);
});
numberRadioGroupUpdater.accept(toggleGroup, property.getValue());
}
private void update(CompanyCustomerEvaluationFormFileVo formFile) {
viewModel.update(formFile);
// formFile.getScoreTemplateVersion();
catalogProperty.set(formFile.getCatalog());
levelProperty.set(formFile.getLevel());
score1Property.set(formFile.getScore1());
score2Property.set(formFile.getScore2());
score3Property.set(formFile.getScore3());
score4Property.set(formFile.getScore4());
score5Property.set(formFile.getScore5());
creditLevelProperty.set(formFile.getCreditLevel());
}
}

View File

@@ -161,9 +161,7 @@ public class CustomerTabSkinFile
// 文件不是 Excel 文件时打开编辑UI
if (!FileUtils.withExtensions(item.getFilePath().get(), FileUtils.XLS,
FileUtils.XLSX)) {
CompanyCustomerEvaluationFormFileVo evaluationFormFile = getEvaluationFormFileService()
.findByCustomerFile(item.getId().get());
CompanyCustomerEvaluationFormFileWindowController.show(evaluationFormFile,
CompanyCustomerEvaluationFormFileWindowController.show(item,
controller.root.getScene().getWindow());
return;
}
@@ -211,10 +209,7 @@ public class CustomerTabSkinFile
model.update(saved);
dataSet.add(model);
CompanyCustomerEvaluationFormFileVo evaluationFormFile = getCachedBean(
CompanyCustomerEvaluationFormFileService.class).findByCustomerFile(saved);
CompanyCustomerEvaluationFormFileWindowController.show(evaluationFormFile,
CompanyCustomerEvaluationFormFileWindowController.show(model,
getTableView().getScene().getWindow());
});
return;

View File

@@ -14,30 +14,16 @@ import lombok.Setter;
public abstract class AbstEmployeeBasedTabSkin
extends AbstEntityBasedTabSkin<EmployeeWindowController, EmployeeVo, EmployeeViewModel>
implements TabSkin {
@Setter
private PermissionService permissionService;
@Setter
private EmployeeRoleService employeeRoleService;
public AbstEmployeeBasedTabSkin(EmployeeWindowController controller) {
super(controller);
}
protected EmployeeService getEmployeeService() {
return controller.employeeService;
}
protected EmployeeRoleService getEmployeeRoleService() {
if (employeeRoleService == null) {
employeeRoleService = getBean(EmployeeRoleService.class);
}
return employeeRoleService;
return getCachedBean(EmployeeRoleService.class);
}
protected PermissionService getPermissionService() {
if (permissionService == null) {
permissionService = getBean(PermissionService.class);
}
return permissionService;
return getCachedBean(PermissionService.class);
}
}

View File

@@ -47,7 +47,7 @@ public class EmployeeManagerSkin
controller.accountColumn.setCellValueFactory(param -> param.getValue().getAccount());
controller.departmentColumn.setCellValueFactory(param -> param.getValue().getDepartment());
controller.departmentColumn.setCellFactory(param -> new DepartmentTableCell<>(getDepartmentService()));
controller.departmentColumn.setCellFactory(DepartmentTableCell.forTableColumn(getDepartmentService()));
controller.emailColumn.setCellValueFactory(param -> param.getValue().getEmail());
controller.createdColumn.setCellValueFactory(param -> param.getValue().getCreated());

View File

@@ -2,7 +2,9 @@ package com.ecep.contract.controller.employee;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.ecep.contract.util.ParamUtils;
import org.springframework.data.domain.Pageable;
import com.ecep.contract.controller.tab.TabSkin;
@@ -48,7 +50,7 @@ public class EmployeeTabSkinRole
private void initializeListView() {
// 非系统内置账户
HashMap<String, Object> params = new HashMap<>();
Map<String, Object> params = ParamUtils.builder().build();
List<EmployeeRoleVo> roles = getEmployeeRoleService().findAll(params, Pageable.ofSize(500)).getContent();
controller.rolesField.getSourceItems().setAll(roles);

View File

@@ -10,6 +10,7 @@ import org.springframework.stereotype.Component;
import com.ecep.contract.controller.AbstManagerWindowController;
import com.ecep.contract.service.InventoryService;
import com.ecep.contract.task.InventoryAllSyncTask;
import com.ecep.contract.task.InventorySyncTask;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.util.UITools;
@@ -79,7 +80,7 @@ public class InventoryManagerWindowController
}
public void onSyncAction(ActionEvent event) {
InventorySyncTask task = new InventorySyncTask();
InventoryAllSyncTask task = new InventoryAllSyncTask();
UITools.showTaskDialogAndWait("同步数据", task, null);
}

View File

@@ -4,6 +4,7 @@ import java.text.NumberFormat;
import java.time.format.DateTimeFormatter;
import java.util.function.Consumer;
import com.ecep.contract.task.InventorySyncTask;
import org.springframework.util.StringUtils;
import com.ecep.contract.MessageHolder;
@@ -157,19 +158,9 @@ public class InventoryTabSkinBase
private void onSyncAction(ActionEvent event) {
InventoryVo inventory = getEntity();
setStatus("开始同步数据...");
if (inventory == null) {
setStatus("请选择要同步数据.");
return;
}
if (!StringUtils.hasText(inventory.getCode())) {
setStatus("请填写商品编码.");
return;
}
setStatus("正在同步数据...");
MessageHolder holder = (lv, msg) -> setStatus(msg);
getService().syncInventory(inventory, holder);
InventorySyncTask task = new InventorySyncTask();
task.setInventory(inventory);
UITools.showTaskDialogAndWait("同步数据", task, null);
}
@Override

View File

@@ -92,7 +92,7 @@ public class InventoryTabSkinContracts
@Override
public ParamUtils.Builder getSpecification(InventoryVo parent) {
ParamUtils.Builder params = getSpecification();
params.equals("inventory", parent);
params.equals("inventory", parent.getId());
return params;
}

View File

@@ -5,10 +5,12 @@ import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import com.ecep.contract.controller.AbstEntityController;
import com.ecep.contract.controller.BaseController;
import com.ecep.contract.service.InventoryService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.vm.InventoryViewModel;
import com.ecep.contract.vo.InventoryVo;
import javafx.stage.Window;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
@@ -26,6 +28,19 @@ import javafx.stage.WindowEvent;
@Component
@FxmlPath("/ui/inventory/inventory.fxml")
public class InventoryWindowController extends AbstEntityController<InventoryVo, InventoryViewModel> {
/**
* 显示界面
*/
public static void show(InventoryViewModel viewModel, Window window) {
BaseController.show(InventoryWindowController.class, viewModel, window);
}
/**
* 显示界面
*/
public static void show(InventoryVo inventory, Window owner) {
show(InventoryViewModel.from(inventory), owner);
}
@FXML
public BorderPane root;
@FXML

View File

@@ -0,0 +1,24 @@
package com.ecep.contract.controller.permission;
import com.ecep.contract.controller.tab.AbstEntityBasedTabSkin;
import com.ecep.contract.service.FunctionService;
import com.ecep.contract.service.PermissionService;
import com.ecep.contract.vm.FunctionViewModel;
import com.ecep.contract.vo.FunctionVo;
public abstract class AbstEmployeeFunctionBasedTabSkin
extends AbstEntityBasedTabSkin<EmployeeFunctionWindowController, FunctionVo, FunctionViewModel> {
public AbstEmployeeFunctionBasedTabSkin(EmployeeFunctionWindowController controller) {
super(controller);
}
FunctionService getFunctionService() {
return getCachedBean(FunctionService.class);
}
public PermissionService getPermissionService() {
return controller.permissionService;
}
}

View File

@@ -0,0 +1,27 @@
package com.ecep.contract.controller.permission;
import com.ecep.contract.controller.tab.TabSkin;
import javafx.scene.control.Tab;
public class EmployeeFunctionTabSkinBase extends AbstEmployeeFunctionBasedTabSkin implements TabSkin {
public EmployeeFunctionTabSkinBase(EmployeeFunctionWindowController controller) {
super(controller);
}
@Override
public Tab getTab() {
return controller.baseInfoTab;
}
@Override
public void initializeTab() {
controller.nameField.textProperty().bindBidirectional(viewModel.getName());
controller.keyField.textProperty().bindBidirectional(viewModel.getKey());
controller.controllerField.textProperty().bindBidirectional(viewModel.getController());
controller.iconField.textProperty().bindBidirectional(viewModel.getIcon());
controller.descriptionField.textProperty().bindBidirectional(viewModel.getDescription());
controller.activeField.selectedProperty().bindBidirectional(viewModel.getActive());
}
}

View File

@@ -0,0 +1,83 @@
package com.ecep.contract.controller.permission;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import com.ecep.contract.controller.AbstEntityController;
import com.ecep.contract.service.FunctionService;
import com.ecep.contract.service.PermissionService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.vm.FunctionViewModel;
import com.ecep.contract.vo.FunctionVo;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
@Lazy
@Scope("prototype")
@Component
@FxmlPath("/ui/employee/function.fxml")
public class EmployeeFunctionWindowController extends AbstEntityController<FunctionVo, FunctionViewModel> {
private static final Logger logger = LoggerFactory.getLogger(EmployeeFunctionWindowController.class);
public static void show(FunctionViewModel viewModel, Window window) {
show(EmployeeFunctionWindowController.class, viewModel, window);
}
public BorderPane root;
public TabPane tabPane;
/*
* 基本信息标签页
*/
public Tab baseInfoTab;
public TextField nameField;
public TextField keyField;
public TextField controllerField;
public TextField iconField;
public TextField descriptionField;
@FXML
public CheckBox activeField;
public Label versionLabel;
/*
* 权限标签页
*/
public Tab permissionTab;
@Autowired
PermissionService permissionService;
@Autowired
FunctionService functionService;
@Override
public void onShown(WindowEvent windowEvent) {
super.onShown(windowEvent);
getTitle().bind(viewModel.getName().map(name -> "[" + viewModel.getId().get() + "] " + name + " 功能详情"));
}
@Override
protected void registerTabSkins() {
registerTabSkin(baseInfoTab, tab -> new EmployeeFunctionTabSkinBase(this));
}
@Override
public FunctionService getViewModelService() {
return functionService;
}
}

View File

@@ -61,8 +61,6 @@ public class EmployeeFunctionsManagerWindowController
public void onCreateNewAction(ActionEvent event) {
}
public void onReBuildFilesAction(ActionEvent event) {
}
@Override
public FunctionService getViewModelService() {

View File

@@ -54,9 +54,6 @@ public class EmployeeRoleManagerWindowController
public void onCreateNewAction(ActionEvent event) {
}
public void onReBuildFilesAction(ActionEvent event) {
}
@Override
public EmployeeRoleService getViewModelService() {
return employeeRoleService;

View File

@@ -50,7 +50,7 @@ public class EmployeeRoleTabSkinFunctions extends AbstEmployeeRoleBasedTabSkin {
}
private void loadSelectedRoles() {
List<FunctionVo> selectedRoles = getRoleService().getFunctionsByRoleId(viewModel.getId().get());
List<FunctionVo> selectedRoles = getRoleService().getFunctionsByRole(getEntity());
if (selectedRoles != null) {
functionsField.getTargetItems().setAll(selectedRoles);
}
@@ -100,8 +100,7 @@ public class EmployeeRoleTabSkinFunctions extends AbstEmployeeRoleBasedTabSkin {
private void saveRoles(ActionEvent event) {
EmployeeRoleVo entity = getEntity();
entity.setFunctions(functionsField.getTargetItems());
save(entity);
getRoleService().saveRoleFunctions(entity, functionsField.getTargetItems());
loadSelectedRoles();
}
}

View File

@@ -40,15 +40,8 @@ import lombok.Setter;
* 基础信息
*/
public class ProjectTabSkinBase extends AbstProjectBasedTabSkin implements TabSkin {
@Setter
EmployeeStringConverter employeeStringConverter;
@Setter
LocalDateStringConverter localDateStringConverter;
@Setter
EmployeeService employeeService;
@Setter
private DeliverySignMethodService deliverySignMethodService;
public ProjectTabSkinBase(ProjectWindowController controller) {
super(controller);
@@ -59,10 +52,6 @@ public class ProjectTabSkinBase extends AbstProjectBasedTabSkin implements TabSk
return controller.baseInfoTab;
}
private EmployeeService getEmployeeService() {
return getBean(EmployeeService.class);
}
private ProjectTypeService getProjectTypeService() {
return getBean(ProjectTypeService.class);
}
@@ -103,7 +92,6 @@ public class ProjectTabSkinBase extends AbstProjectBasedTabSkin implements TabSk
controller.standardPayWayField.selectedProperty().bindBidirectional(viewModel.getStandardPayWay());
Bindings.bindBidirectional(controller.amountField.textProperty(), viewModel.getAmount(),
new NumberStringConverter());
ComboBoxUtils.initialComboBox(controller.saleTypeField, viewModel.getSaleType(), getSaleTypeService(), true);
ComboBoxUtils.initialComboBox(controller.projectTypeField, viewModel.getProjectType(), getProjectTypeService(),
true);
@@ -119,6 +107,9 @@ public class ProjectTabSkinBase extends AbstProjectBasedTabSkin implements TabSk
controller.saleTypeField.valueProperty().addListener((observable, oldValue, newValue) -> {
Predicate<DeliverySignMethodVo> predicate = p -> {
if (newValue == null) {
return true;
}
return p == null || Objects.equals(p.getSaleTypeId(), newValue.getId());
};

View File

@@ -1,14 +1,5 @@
package com.ecep.contract.controller.project;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ecep.contract.ContractPayWay;
import com.ecep.contract.controller.tab.TabSkin;
import com.ecep.contract.controller.table.EditableEntityTableTabSkin;
@@ -23,12 +14,18 @@ import com.ecep.contract.vo.ContractPayPlanVo;
import com.ecep.contract.vo.ContractVo;
import com.ecep.contract.vo.ProjectFundPlanVo;
import com.ecep.contract.vo.ProjectVo;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.Tab;
import javafx.scene.control.TableColumn;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 项目资金计划
@@ -51,19 +48,13 @@ public class ProjectTabSkinFundPlan
public TableColumn<ProjectFundPlanViewModel, String> payWayColumn;
public Button updatePlanBtn;
@Setter
private ProjectFundPlanService projectFundPlanService;
public ProjectTabSkinFundPlan(ProjectWindowController controller) {
super(controller);
}
@Override
protected ProjectFundPlanService getViewModelService() {
if (projectFundPlanService == null) {
projectFundPlanService = getBean(ProjectFundPlanService.class);
}
return projectFundPlanService;
return getProjectFundPlanService();
}
public ContractPayPlanService getContractPayPlanService() {
@@ -249,10 +240,7 @@ public class ProjectTabSkinFundPlan
}
private ProjectFundPlanService getProjectFundPlanService() {
if (projectFundPlanService == null) {
projectFundPlanService = getBean(ProjectFundPlanService.class);
}
return projectFundPlanService;
return getCachedBean(ProjectFundPlanService.class);
}
@Override

View File

@@ -149,6 +149,7 @@ public class ProjectWindowController extends AbstEntityController<ProjectVo, Pro
registerTabSkin(costTab, tab -> new ProjectTabSkinCost(this));
registerTabSkin(quotationApprovalTab, tab -> new ProjectTabSkinQuotation(this));
registerTabSkin(bidTab, tab -> new ProjectTabSkinBid(this));
// 资金计划
registerTabSkin(fundPlanTab, tab -> new ProjectTabSkinFundPlan(this));
// registerTabSkin(costItemTab, this::createCostItemTabSkin);
registerTabSkin(satisfactionTab, tab -> new ProjectTabSkinCustomerSatisfactionSurvey(this));
@@ -162,7 +163,6 @@ public class ProjectWindowController extends AbstEntityController<ProjectVo, Pro
private ProjectTabSkinBase createBaseTabSkin(Tab tab) {
ProjectTabSkinBase skin = new ProjectTabSkinBase(this);
skin.setLocalDateStringConverter(localDateStringConverter);
skin.setEmployeeService(employeeService);
return skin;
}

View File

@@ -38,17 +38,13 @@ public class ProjectCostTabSkinBase
implements TabSkin {
@Setter
LocalDateTimeStringConverter localDateTimeStringConverter;
ProjectService projectService;
public ProjectCostTabSkinBase(ProjectCostWindowController controller) {
super(controller);
}
ProjectService getProjectService() {
if (projectService == null) {
projectService = getBean(ProjectService.class);
}
return projectService;
return getCachedBean(ProjectService.class);
}
private LocalDateTimeStringConverter getLocalDateTimeStringConverter() {

View File

@@ -88,8 +88,23 @@ public class ProjectCostWindowController
@Override
protected void registerTabSkins() {
registerTabSkin(baseInfoTab, tab -> new ProjectCostTabSkinBase(this));
registerTabSkin(itemTab, tab -> new ProjectCostTabSkinItems(this));
}
@Override
protected void updateViewModel(ProjectCostVo entity) {
super.updateViewModel(entity);
var tabPane = baseInfoTab.getTabPane();
if (entity.getVersion() > 0) {
if (!tabPane.getTabs().contains(approvalTab)) {
tabPane.getTabs().add(approvalTab);
}
} else {
tabPane.getTabs().remove(approvalTab);
}
}
@Override

View File

@@ -1,9 +1,5 @@
package com.ecep.contract.controller.project.quotation;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.CompletableFuture;
import com.ecep.contract.DesktopUtils;
import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.controller.customer.CompanyCustomerEvaluationFormFileWindowController;
@@ -11,21 +7,13 @@ import com.ecep.contract.controller.tab.AbstEntityBasedTabSkin;
import com.ecep.contract.controller.tab.TabSkin;
import com.ecep.contract.converter.CompanyStringConverter;
import com.ecep.contract.converter.EmployeeStringConverter;
import com.ecep.contract.model.Employee;
import com.ecep.contract.service.CompanyCustomerEvaluationFormFileService;
import com.ecep.contract.service.CompanyCustomerFileService;
import com.ecep.contract.service.CustomerService;
import com.ecep.contract.service.CompanyService;
import com.ecep.contract.service.ProjectQuotationService;
import com.ecep.contract.service.ProjectService;
import com.ecep.contract.service.*;
import com.ecep.contract.util.ProxyUtils;
import com.ecep.contract.util.UITools;
import com.ecep.contract.vm.ProjectQuotationViewModel;
import com.ecep.contract.vo.CompanyCustomerEvaluationFormFileVo;
import com.ecep.contract.vo.CompanyVo;
import com.ecep.contract.vo.ProjectQuotationVo;
import com.ecep.contract.vo.ProjectVo;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.scene.control.Tab;
@@ -37,36 +25,22 @@ import javafx.util.converter.LocalDateTimeStringConverter;
import javafx.util.converter.NumberStringConverter;
import lombok.Setter;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.CompletableFuture;
/**
* 项目报价单
*/
public class ProjectQuotationTabSkinBase
extends AbstEntityBasedTabSkin<ProjectQuotationWindowController, ProjectQuotationVo, ProjectQuotationViewModel>
implements TabSkin {
@Setter
private LocalDateTimeStringConverter localDateTimeStringConverter;
@Setter
private ProjectQuotationService projectQuotationService;
@Setter
private LocalDateStringConverter localDateStringConverter;
@Setter
private EmployeeStringConverter employeeStringConverter;
@Setter
private CompanyStringConverter companyStringConverter;
@Setter
private CompanyService companyService;
@Setter
private CustomerService customerService;
@Setter
private CompanyCustomerFileService customerFileService;
@Setter
private CompanyCustomerEvaluationFormFileService evaluationFormFileService;
@Setter
private ProjectService projectService;
private ProjectService getProjectService() {
if (projectService == null) {
projectService = getBean(ProjectService.class);
}
return projectService;
return getCachedBean(ProjectService.class);
}
public ProjectQuotationTabSkinBase(ProjectQuotationWindowController controller) {
@@ -133,7 +107,10 @@ public class ProjectQuotationTabSkinBase
new NumberStringConverter(getLocale()));
UITools.autoCompletion(controller.evaluationFileField, viewModel.getEvaluationFile(),
getEvaluationFormFileService());
getEvaluationFormFileService(), getEvaluationFormFileService().getStringConverter(), searchText -> {
var project = getProjectService().findById(getEntity().getProject());
return getEvaluationFormFileService().searchByCompany(project.getCustomerId(), searchText);
});
controller.authorizationFileField.textProperty().bind(viewModel.getAuthorizationFile().map(File::getName));
@@ -220,7 +197,7 @@ public class ProjectQuotationTabSkinBase
}
private void evaluationFileAutoCompletion(TextField textField,
SimpleObjectProperty<CompanyCustomerEvaluationFormFileVo> property) {
SimpleObjectProperty<CompanyCustomerEvaluationFormFileVo> property) {
// 直接使用Vo类不再需要手动转换
}

View File

@@ -7,9 +7,11 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import com.ecep.contract.controller.BaseController;
import com.ecep.contract.service.EmployeeService;
import com.ecep.contract.util.BeanContext;
import com.ecep.contract.util.UITools;
public abstract class AbstGenericTabSkin<C extends BaseController> implements TabSkin {
public abstract class AbstGenericTabSkin<C extends BaseController> implements TabSkin, BeanContext {
private static final Logger logger = LoggerFactory.getLogger(AbstGenericTabSkin.class);
/**
* 控制器
@@ -49,4 +51,7 @@ public abstract class AbstGenericTabSkin<C extends BaseController> implements Ta
UITools.showExceptionAndWait(message, ex);
}
public EmployeeService getEmployeeService() {
return getCachedBean(EmployeeService.class);
}
}

View File

@@ -62,17 +62,13 @@ public class CompanyTabSkinBankAccount
bankAccountSearchBtn.setOnAction(this::onTableRefreshAction);
bankAccountTable_idColumn.setCellValueFactory(param -> param.getValue().getId());
bankAccountTable_bankColumn.setCellValueFactory(param -> param.getValue().getBankId());
bankAccountTable_bankColumn.setCellFactory(param -> new BankTableCell<>(getBankService()));
bankAccountTable_bankColumn.setCellFactory(BankTableCell.forTableColumn(getBankService()));
bankAccountTable_openingBankColumn.setCellValueFactory(param -> param.getValue().getOpeningBank());
bankAccountTable_accountColumn.setCellValueFactory(param -> param.getValue().getAccount());
bankAccountTable_menu_refresh.setOnAction(this::onTableRefreshAction);
bankAccountTable_menu_add.setOnAction(this::onTableAddAction);
bankAccountTable_menu_del.setOnAction(this::onTableDeleteAction);
super.initializeTab();
}

View File

@@ -164,18 +164,18 @@ public class CompanyTabSkinContract
contractTable_startDateColumn.setCellValueFactory(param -> param.getValue().getStartDate());
contractTable_endDateColumn.setCellValueFactory(param -> param.getValue().getEndDate());
contractTable_setupPersonColumn.setCellValueFactory(param -> param.getValue().getSetupPerson());
contractTable_setupPersonColumn.setCellFactory(param -> new EmployeeTableCell<>(getEmployeeService()));
contractTable_setupPersonColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
contractTable_setupDateColumn.setCellValueFactory(param -> param.getValue().getSetupDate());
// contractTable_setupDateColumn.setSortable(true);
// contractTable_setupDateColumn.setSortType(TableColumn.SortType.DESCENDING);
contractTable_inurePersonColumn.setCellValueFactory(param -> param.getValue().getInurePerson());
contractTable_inurePersonColumn.setCellFactory(param -> new EmployeeTableCell<>(getEmployeeService()));
contractTable_inurePersonColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
contractTable_inureDateColumn.setCellValueFactory(param -> param.getValue().getInureDate());
contractTable_varyPersonColumn.setCellValueFactory(param -> param.getValue().getVaryPerson());
contractTable_varyPersonColumn.setCellFactory(param -> new EmployeeTableCell<>(getEmployeeService()));
contractTable_varyPersonColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
contractTable_varyDateColumn.setCellValueFactory(param -> param.getValue().getVaryDate());
contractTable_createdColumn.setCellValueFactory(param -> param.getValue().getCreated());

View File

@@ -6,8 +6,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.logging.Level;
import com.ecep.contract.task.CompanyMergeClientTasker;
import com.ecep.contract.util.ParamUtils;
import com.ecep.contract.util.UITools;
import com.ecep.contract.vo.CompanyOldNameVo;
import com.ecep.contract.vo.CompanyVo;
import org.springframework.util.StringUtils;
@@ -138,53 +142,20 @@ public class CompanyTabSkinOldName
}
private void onTableMergeAction(ActionEvent event) {
CompanyVo updater = getParent();
HashSet<String> nameSet = new HashSet<>();
nameSet.add(updater.getName());
// 收集所有曾用名
List<String> nameList = dataSet.stream()
.map(viewModel -> viewModel.getName().get())
.filter(Objects::nonNull)
.toList();
List<CompanyOldNameViewModel> removes = new ArrayList<>();
for (CompanyOldNameViewModel viewModel : dataSet) {
// 创建独立的WebSocket客户端任务器
CompanyMergeClientTasker task = new CompanyMergeClientTasker();
if (!nameSet.add(viewModel.getName().get())) {
// fixed 曾用名中有重复时,删除重复的
deleteRow(viewModel);
removes.add(viewModel);
}
}
if (!removes.isEmpty()) {
Platform.runLater(() -> {
dataSet.removeAll(removes);
});
setStatus("移除重复的曾用名" + removes.size());
}
task.setCompany(getParent());
task.setNameList(nameList);
UITools.showTaskDialogAndWait("合并曾用名", task, null);
int size = nameSet.size();
int count = 1;
int merge = 0;
for (String name : nameSet) {
controller.setRightStatus(count + "/" + size);
if (StringUtils.hasText(name)) {
List<CompanyVo> list = getParentService().findAllByName(name);
for (CompanyVo company : list) {
// fixed 曾用名中有可能有 updater 的名字,会导致自己删除自己
if (Objects.equals(company.getId(), updater.getId())) {
continue;
}
try {
getCompanyService().merge(company, updater);
setStatus("并户 " + company.getName() + "[" + company.getId() + "] 到当前公司");
merge++;
} catch (Exception e) {
throw new RuntimeException("合并 " + company.getName() + " -> " + updater.getName() + " 失败", e);
}
}
}
count++;
}
if (merge == 0) {
setStatus("没有需要并户的公司");
}
controller.setRightStatus("");
loadTableDataSet();
}
@Override
@@ -201,7 +172,7 @@ public class CompanyTabSkinOldName
return;
}
String path = viewModel.getPath().get();
if (StringUtils.hasText(path)) {
if (org.springframework.util.StringUtils.hasText(path)) {
if (item.startsWith(path)) {
item = "~" + item.substring(path.length());
}
@@ -216,4 +187,4 @@ public class CompanyTabSkinOldName
}
return companyOldNameService;
}
}
}

View File

@@ -97,8 +97,10 @@ public class CompanyTabSkinOther
// Yu //
public TitledPane yuCloudPane;
public TextField cloudYuIdField;
public TextField cloudYuCloudIdField;
public TextField cloudYuLatestField;
public TextField cloudYuVendorUpdateDateField;
public TextField cloudYuCustomerUpdateDateField;
public CheckBox cloudYuActiveField;
public Label cloudYuVersionLabel;
public Button yuCloudPaneSaveButton;
@@ -417,8 +419,10 @@ public class CompanyTabSkinOther
}
cloudYuIdField.textProperty().bind(yuCloudInfoViewModel.getId().asString());
cloudYuCloudIdField.textProperty().bindBidirectional(yuCloudInfoViewModel.getCloudId());
cloudYuLatestField.textProperty().bind(yuCloudInfoViewModel.getLatestUpdate().map(MyDateTimeUtils::format));
cloudYuVendorUpdateDateField.textProperty().bind(yuCloudInfoViewModel.getVendorUpdateDate().map(MyDateTimeUtils::format));
cloudYuCustomerUpdateDateField.textProperty().bind(yuCloudInfoViewModel.getCustomerUpdateDate().map(MyDateTimeUtils::format));
cloudYuActiveField.selectedProperty().bindBidirectional(yuCloudInfoViewModel.getActive());
cloudYuVersionLabel.textProperty().bind(yuCloudInfoViewModel.getVersion().asString("Ver:%s"));
Button button = yuCloudPaneSaveButton;
@@ -436,6 +440,34 @@ public class CompanyTabSkinOther
button.setDisable(false);
});
});
DelayOnceExecutor saveExecutor = new DelayOnceExecutor(() -> {
save(yuCloudInfoViewModel);
cloudYuActiveField.setBorder(null);
}, 2, TimeUnit.SECONDS).exception(e -> logger.error(e.getMessage(), e));
cloudYuActiveField.setOnMouseClicked(event -> {
cloudYuActiveField.setBorder(Border.stroke(Color.RED));
saveExecutor.tick();
});
}
public void save(CloudYuInfoViewModel viewModel) {
int infoId = viewModel.getId().get();
YongYouU8Service service = getYongYouU8Service();
CloudYuVo cloudYu = service.findById(infoId);
if (cloudYu == null) {
throw new RuntimeException("CloudTyc not found");
}
if (viewModel.copyTo(cloudYu)) {
CloudYuVo saved = service.save(cloudYu);
if (Platform.isFxApplicationThread()) {
viewModel.update(saved);
} else {
Platform.runLater(() -> {
viewModel.update(saved);
});
}
}
}
private void onCloudYuUpdateButtonClicked(ActionEvent event) {
@@ -516,9 +548,11 @@ public class CompanyTabSkinOther
CompanyExtendInfoViewModel viewModel = extendInfoViewModel;
CompanyExtendInfoService service = getExtendInfoService();
CompanyExtendInfoVo extendInfo = service.findByCompany(company);
Platform.runLater(() -> {
viewModel.update(extendInfo);
});
if (extendInfo != null) {
Platform.runLater(() -> {
viewModel.update(extendInfo);
});
}
}
CloudRkService getCloudRkService() {

View File

@@ -1,23 +1,65 @@
package com.ecep.contract.controller.tab;
import com.ecep.contract.MessageHolder;
import com.ecep.contract.SpringApp;
import com.ecep.contract.task.Tasker;
import com.ecep.contract.WebSocketClientService;
import com.ecep.contract.WebSocketClientTasker;
import com.ecep.contract.vo.ContractVo;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import lombok.Getter;
import lombok.Setter;
public class ContractFilesRebuildTasker extends Tasker<Object> {
import java.util.Locale;
@Slf4j
public class ContractFilesRebuildTasker extends Tasker<Object> implements WebSocketClientTasker {
@Setter
private ContractVo contract;
@Getter
@Setter
private boolean repaired = false;
@Override
public Object execute(MessageHolder holder) {
return null;
public String getTaskName() {
return "ContractFilesRebuildTasker";
}
public boolean isRepaired() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'isRepaired'");
@Override
public void updateProgress(long workDone, long max) {
super.updateProgress(workDone, max);
}
@Override
public Object execute(MessageHolder holder) {
log.info("开始执行合同文件重建任务合同ID: {}", contract != null ? contract.getId() : "unknown");
if (contract == null) {
String errorMsg = "合同信息为空,无法执行文件重建任务";
holder.error(errorMsg);
throw new RuntimeException(errorMsg);
}
try {
holder.info("开始重建合同文件,合同编号: " + contract.getCode());
updateProgress(0, 100);
// 使用WebSocket调用远程任务
Object result = callRemoteTask(holder, Locale.getDefault(), contract.getId());
updateProgress(100, 100);
holder.info("合同文件重建任务已提交到服务器");
return result;
} catch (Exception e) {
log.error("合同文件重建任务执行失败", e);
holder.error("任务执行失败: " + e.getMessage());
throw new RuntimeException("合同文件重建任务执行失败", e);
}
}
}

View File

@@ -0,0 +1,31 @@
package com.ecep.contract.controller.tab;
import com.ecep.contract.controller.contract.ContractInvoiceWindowController;
import com.ecep.contract.vm.ContractInvoiceViewModel;
import com.ecep.contract.vo.ContractInvoiceVo;
import javafx.scene.control.Tab;
/**
* 合同发票基础信息标签页皮肤
*/
public class ContractInvoiceTabSkinBase
extends AbstEntityBasedTabSkin<ContractInvoiceWindowController, ContractInvoiceVo, ContractInvoiceViewModel>
implements TabSkin {
public ContractInvoiceTabSkinBase(ContractInvoiceWindowController controller) {
super(controller);
}
@Override
public Tab getTab() {
return controller.baseInfoTab;
}
@Override
public void initializeTab() {
// 可以在这里进行标签页的初始化工作
controller.codeField.textProperty().bindBidirectional(viewModel.getCode());
controller.nameField.textProperty().bindBidirectional(viewModel.getName());
controller.remarkField.textProperty().bindBidirectional(viewModel.getRemark());
}
}

View File

@@ -0,0 +1,84 @@
package com.ecep.contract.controller.tab;
import com.ecep.contract.controller.contract.ContractItemWindowController;
import com.ecep.contract.service.InventoryService;
import com.ecep.contract.util.UITools;
import com.ecep.contract.vm.ContractItemViewModel;
import com.ecep.contract.vo.ContractItemVo;
import javafx.scene.control.Tab;
import javafx.util.converter.LocalDateTimeStringConverter;
/**
* 合同项目基础信息标签页
*/
public class ContractItemTabSkinBase
extends AbstEntityBasedTabSkin<ContractItemWindowController, ContractItemVo, ContractItemViewModel> {
public ContractItemTabSkinBase(ContractItemWindowController controller) {
super(controller);
}
@Override
public Tab getTab() {
return controller.baseInfoTab;
}
@Override
public void initializeTab() {
// 数据绑定
controller.idLabel.textProperty().bind(viewModel.getId().asString());
controller.codeField.textProperty().bindBidirectional(viewModel.getCode());
controller.titleField.textProperty().bindBidirectional(viewModel.getTitle());
controller.specificationField.textProperty().bindBidirectional(viewModel.getSpecification());
controller.unitField.textProperty().bindBidirectional(viewModel.getUnit());
// 财务信息绑定
controller.exclusiveTaxPriceField.textProperty().bindBidirectional(viewModel.getExclusiveTaxPrice().asObject(),
new javafx.util.converter.DoubleStringConverter());
controller.taxRateField.textProperty().bindBidirectional(viewModel.getTaxRate().asObject(),
new javafx.util.converter.DoubleStringConverter());
controller.taxPriceField.textProperty().bindBidirectional(viewModel.getTaxPrice().asObject(),
new javafx.util.converter.DoubleStringConverter());
controller.quantityField.textProperty().bindBidirectional(viewModel.getQuantity().asObject(),
new javafx.util.converter.DoubleStringConverter());
controller.taxAmountField.textProperty().bindBidirectional(viewModel.getTaxAmount().asObject(),
new javafx.util.converter.DoubleStringConverter());
controller.exclusiveTaxAmountField.textProperty().bindBidirectional(
viewModel.getExclusiveTaxAmount().asObject(),
new javafx.util.converter.DoubleStringConverter());
// 日期绑定
controller.startDateField.valueProperty().bindBidirectional(viewModel.getStartDate());
controller.endDateField.valueProperty().bindBidirectional(viewModel.getEndDate());
// 其他信息绑定
controller.refIdField.textProperty().bindBidirectional(viewModel.getRefId().asObject(),
new javafx.util.converter.IntegerStringConverter());
controller.remarkField.textProperty().bindBidirectional(viewModel.getRemark());
// 设置只读字段
LocalDateTimeStringConverter localDateTimeStringConverter = controller.getCurrentEmployee()
.getLocalDateTimeStringConverter();
controller.createDateLabel.textProperty().bindBidirectional(viewModel.getCreateDate(),
localDateTimeStringConverter);
controller.updateDateLabel.textProperty().bindBidirectional(viewModel.getUpdateDate(),
localDateTimeStringConverter);
UITools.autoCompletion(controller.updaterLabel, viewModel.getCreator(), controller.getEmployeeService());
UITools.autoCompletion(controller.updaterLabel, viewModel.getUpdater(), controller.getEmployeeService());
// 设置存货显示信息
UITools.autoCompletion(controller.inventoryField, viewModel.getInventory(),
getCachedBean(InventoryService.class));
}
@Override
protected ContractItemVo save(ContractItemVo entity) {
// 保存前自动计算金额
viewModel.calculateAmounts();
return super.save(entity);
}
}

View File

@@ -85,7 +85,9 @@ public class ContractTabSkinBase extends AbstContractBasedTabSkin {
controller.payWayField.textProperty().bind(viewModel.getPayWay().map(ContractPayWay::getText));
controller.nameField.textProperty().bind(viewModel.getName());
controller.nameField.textProperty().bindBidirectional(viewModel.getName());
controller.nameField.editableProperty().bind(viewModel.getNameLocked());
controller.contractNameLockedCk.selectedProperty().bindBidirectional(viewModel.getNameLocked());
controller.codeField.textProperty().bind(viewModel.getCode());
controller.parentCodeField.textProperty().bindBidirectional(viewModel.getParentCode());
@@ -351,17 +353,20 @@ public class ContractTabSkinBase extends AbstContractBasedTabSkin {
}
if (initialDirectory == null) {
if (entity.getPayWay() == ContractPayWay.RECEIVE) {
// 根据项目设置初始目录
ProjectVo project = getProjectService().findById(entity.getProject());
if (project != null) {
// 根据项目销售方式设置初始目录
ProjectSaleTypeVo saleType = getSaleTypeService().findById(project.getSaleTypeId());
if (saleType != null) {
File dir = new File(saleType.getPath());
if (saleType.isStoreByYear()) {
dir = new File(dir, "20" + project.getCodeYear());
Integer projectId = entity.getProject();
if (projectId != null) {
// 根据项目设置初始目录
ProjectVo project = getProjectService().findById(projectId);
if (project != null) {
// 根据项目销售方式设置初始目录
ProjectSaleTypeVo saleType = getSaleTypeService().findById(project.getSaleTypeId());
if (saleType != null) {
File dir = new File(saleType.getPath());
if (saleType.isStoreByYear()) {
dir = new File(dir, "20" + project.getCodeYear());
}
initialDirectory = dir;
}
initialDirectory = dir;
}
}
} else if (entity.getPayWay() == ContractPayWay.PAY) {

View File

@@ -0,0 +1,124 @@
package com.ecep.contract.controller.tab;
import java.time.LocalDate;
import java.util.Objects;
import org.controlsfx.glyphfont.Glyph;
import org.springframework.stereotype.Component;
import com.ecep.contract.controller.contract.AbstContractTableTabSkin;
import com.ecep.contract.controller.contract.ContractInvoiceWindowController;
import com.ecep.contract.controller.contract.ContractWindowController;
import com.ecep.contract.controller.table.EditableEntityTableTabSkin;
import com.ecep.contract.controller.table.cell.ContractInvoiceTableCell;
import com.ecep.contract.controller.table.cell.EmployeeTableCell;
import com.ecep.contract.controller.table.cell.NumberTableCell;
import com.ecep.contract.model.IdentityEntity;
import com.ecep.contract.service.ContractInvoiceService;
import com.ecep.contract.service.ContractService;
import com.ecep.contract.service.InvoiceService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.util.UITools;
import com.ecep.contract.vm.ContractInvoiceViewModel;
import com.ecep.contract.vo.ContractInvoiceVo;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.ActionEvent;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.Region;
import javafx.util.StringConverter;
import javafx.util.converter.CurrencyStringConverter;
@FxmlPath("/ui/contract/contract-tab-invoices.fxml")
public class ContractTabSkinInvoices
extends AbstContractTableTabSkin<ContractInvoiceVo, ContractInvoiceViewModel>
implements TabSkin, EditableEntityTableTabSkin<ContractInvoiceVo, ContractInvoiceViewModel> {
private Tab tab;
public TableColumn<ContractInvoiceViewModel, Number> idColumn;
public TableColumn<ContractInvoiceViewModel, String> codeColumn;
public TableColumn<ContractInvoiceViewModel, String> nameColumn;
public TableColumn<ContractInvoiceViewModel, Integer> invoiceColumn;
public TableColumn<ContractInvoiceViewModel, Number> amountColumn;
public TableColumn<ContractInvoiceViewModel, String> remarkColumn;
public TableColumn<ContractInvoiceViewModel, Integer> creatorColumn;
public TableColumn<ContractInvoiceViewModel, LocalDate> createDateColumn;
public TableColumn<ContractInvoiceViewModel, Integer> updaterColumn;
public TableColumn<ContractInvoiceViewModel, LocalDate> updateDateColumn;
public Label totalAmountLabel;
public ContractTabSkinInvoices(ContractWindowController controller, Tab tab) {
super(controller);
this.tab = tab;
}
@Override
public Tab getTab() {
return tab;
}
@Override
protected ContractInvoiceService getViewModelService() {
return getBean(ContractInvoiceService.class);
}
@Override
public void initializeTable() {
super.initializeTable();
getTableView().setEditable(true);
// 初始化列
idColumn.setCellValueFactory(new PropertyValueFactory<>("id"));
idColumn.setPrefWidth(50);
codeColumn.setCellValueFactory(new PropertyValueFactory<>("code"));
codeColumn.setPrefWidth(120);
codeColumn.setCellFactory(TextFieldTableCell.forTableColumn());
nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
nameColumn.setPrefWidth(200);
nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
// 发票列
invoiceColumn.setCellValueFactory(new PropertyValueFactory<>("invoice"));
invoiceColumn.setPrefWidth(100);
invoiceColumn.setCellFactory(ContractInvoiceTableCell.forTableColumn(getCachedBean(InvoiceService.class)));
// 金额列
amountColumn.setCellValueFactory(new PropertyValueFactory<>("amount"));
amountColumn.setPrefWidth(100);
amountColumn.setCellFactory(NumberTableCell.forTableColumn(new CurrencyStringConverter(getLocale())));
remarkColumn.setCellValueFactory(new PropertyValueFactory<>("remark"));
remarkColumn.setPrefWidth(150);
remarkColumn.setCellFactory(TextFieldTableCell.forTableColumn());
// 创建人列
creatorColumn.setCellValueFactory(new PropertyValueFactory<>("creator"));
creatorColumn.setPrefWidth(80);
creatorColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
createDateColumn.setCellValueFactory(new PropertyValueFactory<>("createDate"));
createDateColumn.setPrefWidth(120);
// 更新人列
updaterColumn.setCellValueFactory(new PropertyValueFactory<>("updater"));
updaterColumn.setPrefWidth(80);
updaterColumn.setCellFactory(EmployeeTableCell.forTableColumn(getEmployeeService()));
updateDateColumn.setCellValueFactory(new PropertyValueFactory<>("updateDate"));
updateDateColumn.setPrefWidth(120);
}
}

View File

@@ -3,30 +3,41 @@ package com.ecep.contract.controller.tab;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import javax.naming.Binding;
import org.springframework.data.domain.Pageable;
import com.ecep.contract.ContractPayWay;
import com.ecep.contract.SpringApp;
import com.ecep.contract.controller.contract.AbstContractTableTabSkin;
import com.ecep.contract.controller.contract.ContractWindowController;
import com.ecep.contract.controller.inventory.InventoryWindowController;
import com.ecep.contract.controller.table.EditableEntityTableTabSkin;
import com.ecep.contract.controller.table.cell.AsyncUpdateTableCell;
import com.ecep.contract.controller.table.cell.EmployeeTableCell;
import com.ecep.contract.controller.table.cell.InventoryTableCell;
import com.ecep.contract.controller.table.cell.LocalDateTimeTableCell;
import com.ecep.contract.service.ContractItemService;
import com.ecep.contract.service.InventoryService;
import com.ecep.contract.service.PurchaseOrderItemService;
import com.ecep.contract.util.FxmlPath;
import com.ecep.contract.vm.ContractItemComposeViewModel;
import com.ecep.contract.util.ParamUtils;
import com.ecep.contract.vm.ContractItemViewModel;
import com.ecep.contract.vm.InventoryViewModel;
import com.ecep.contract.vo.ContractItemVo;
import com.ecep.contract.vo.InventoryVo;
import com.ecep.contract.vo.PurchaseOrderItemVo;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.util.Callback;
import javafx.util.StringConverter;
import javafx.util.converter.CurrencyStringConverter;
import javafx.util.converter.NumberStringConverter;
@@ -128,7 +139,11 @@ public class ContractTabSkinItemsV2
taxPriceColumn.setCellFactory(TextFieldTableCell.forTableColumn(currencyStringConverter));
quantityColumn.setCellValueFactory(param -> param.getValue().getQuantity());
quantityColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter(getLocale())));
if (getEntity().getPayWay() == ContractPayWay.PAY) {
quantityColumn.setCellFactory(QuantityTableCell.forTableColumn(new NumberStringConverter(getLocale())));
} else {
quantityColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter(getLocale())));
}
taxAmountColumn.setCellValueFactory(param -> param.getValue().getTaxAmount());
taxAmountColumn.setCellFactory(TextFieldTableCell.forTableColumn(currencyStringConverter));
@@ -184,5 +199,35 @@ public class ContractTabSkinItemsV2
}
showInOwner(InventoryWindowController.class, InventoryViewModel.from(inventory));
});
showInventory.visibleProperty().bind(
Bindings.select(getTableView().getSelectionModel().selectedItemProperty(), "inventory", "value").isNotNull());
contextMenu.getItems().addAll(showInventory);
}
public static class QuantityTableCell
extends AsyncUpdateTableCell<ContractItemViewModel, Number, PurchaseOrderItemVo> {
public static Callback<TableColumn<ContractItemViewModel, Number>, TableCell<ContractItemViewModel, Number>> forTableColumn(
NumberStringConverter stringConverter) {
;
return param -> new QuantityTableCell(stringConverter);
}
public QuantityTableCell(NumberStringConverter stringConverter) {
setService(SpringApp.getBean(PurchaseOrderItemService.class));
}
@Override
protected void setLoadingText() {
setText("-/" + getItem());
}
@Override
protected void asyncLoadAndUpdate() {
double sum = getService()
.findAll(ParamUtils.equal("contractItem", this.getTableRow().getItem().getId().get()),
Pageable.unpaged())
.stream().mapToDouble(PurchaseOrderItemVo::getQuantity).sum();
Platform.runLater(() -> setText(sum + "/" + getItem()));
}
}
}

View File

@@ -1,13 +1,9 @@
package com.ecep.contract.controller.tab;
import java.time.LocalDateTime;
import com.ecep.contract.MyDateTimeUtils;
import com.ecep.contract.SpringApp;
import com.ecep.contract.controller.contract.sale_order.SalesOrderWindowController;
import com.ecep.contract.converter.EmployeeStringConverter;
import com.ecep.contract.service.CompanyService;
import com.ecep.contract.service.ContractFileService;
import com.ecep.contract.service.ContractService;
import com.ecep.contract.util.UITools;
import com.ecep.contract.vm.SalesOrderViewModel;
import com.ecep.contract.vo.SalesOrderVo;
@@ -15,18 +11,12 @@ import com.ecep.contract.vo.SalesOrderVo;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import lombok.Setter;
import javafx.util.converter.NumberStringConverter;
public class SalesOrderTabSkinBase
extends AbstEntityBasedTabSkin<SalesOrderWindowController, SalesOrderVo, SalesOrderViewModel>
implements TabSkin {
@Setter
private ContractFileService companyContactService;
@Setter
private CompanyService companyService;
private EmployeeStringConverter employeeStringConverter;
public SalesOrderTabSkinBase(SalesOrderWindowController controller) {
super(controller);
}
@@ -39,13 +29,26 @@ public class SalesOrderTabSkinBase
@Override
public void initializeTab() {
controller.codeField.textProperty().bind(viewModel.getCode());
controller.refIdField.textProperty().bindBidirectional(viewModel.getRefId(), new NumberStringConverter(getLocale()));
controller.taxRateField.textProperty().bindBidirectional(viewModel.getTaxRate(), new NumberStringConverter(getLocale()));
initializeEmployeeField(controller.employeeField, viewModel.getEmployee());
initializeEmployeeField(controller.makerField, viewModel.getMaker());
initializeEmployeeField(controller.verifierField, viewModel.getVerifier());
initializeEmployeeField(controller.modifierField, viewModel.getModifier());
initializeEmployeeField(controller.closerField, viewModel.getCloser());
initializeCompanyField(controller.customerField, viewModel.getCustomer());
controller.customerAddressField.textProperty().bindBidirectional(viewModel.getCustomerAddress());
initializeContractField(controller.contractField, viewModel.getContract());
controller.makeDateField.textProperty().bind(viewModel.getMakerDate().map(MyDateTimeUtils::format));
controller.verifierDateField.textProperty().bind(viewModel.getVerifierDate().map(MyDateTimeUtils::format));
controller.modifierDateField.textProperty().bind(viewModel.getModifierDate().map(MyDateTimeUtils::format));
controller.closerDateField.textProperty().bind(viewModel.getCloserDate().map(MyDateTimeUtils::format));
controller.descriptionField.textProperty().bindBidirectional(viewModel.getDescription());
@@ -53,29 +56,23 @@ public class SalesOrderTabSkinBase
controller.saveBtn.setOnAction(this::onSaveAction);
}
protected String localDateTimeFormatter(SimpleObjectProperty<LocalDateTime> property) {
LocalDateTime dateTime = property.get();
if (dateTime == null) {
return "";
}
return MyDateTimeUtils.format(dateTime);
}
private void initializeEmployeeField(TextField textField, SimpleObjectProperty<Integer> property) {
UITools.autoCompletion(textField, property, controller.getEmployeeService());
}
public CompanyService getCompanyService() {
if (companyService == null) {
companyService = SpringApp.getBean(CompanyService.class);
}
return companyService;
private void initializeCompanyField(TextField textField, SimpleObjectProperty<Integer> property) {
UITools.autoCompletion(textField, property, getCompanyService());
}
private void initializeContractField(TextField textField, SimpleObjectProperty<Integer> property) {
UITools.autoCompletion(textField, property, getContractService());
}
public EmployeeStringConverter getEmployeeStringConverter() {
if (employeeStringConverter == null) {
employeeStringConverter = SpringApp.getBean(EmployeeStringConverter.class);
}
return employeeStringConverter;
public CompanyService getCompanyService() {
return getCachedBean(CompanyService.class);
}
public ContractService getContractService() {
return getCachedBean(ContractService.class);
}
}

View File

@@ -77,15 +77,14 @@ public class AsyncUpdateTableCell<V, K, T extends IdentityEntity> extends javafx
setText(null);
return;
}
// if (isInitialized(itemId)) {
// setText(format(itemId));
// return;
// }
setText("# " + itemId);
setLoadingText();
syncFuture = submit(this::asyncLoadAndUpdate);
}
protected void setLoadingText() {
setText("# " + getItem());
}
/**
* 检查实体是否已初始化
*
@@ -126,15 +125,8 @@ public class AsyncUpdateTableCell<V, K, T extends IdentityEntity> extends javafx
if (getItem() == null) {
return;
}
// if (getService() instanceof QueryService<T, ?> queryService) {
// queryService.findById(getItem().getId());
// queryService.asyncFindById(getItem().getId()).thenAccept(this::_updateEntity);
// return;
// }
T entity = initialize();
_updateEntity(entity);
}
private void _updateEntity(T entity) {

View File

@@ -1,20 +1,39 @@
package com.ecep.contract.controller.table.cell;
import com.ecep.contract.SpringApp;
import com.ecep.contract.service.BankService;
import com.ecep.contract.vo.BankVo;
import javafx.util.Callback;
import lombok.NoArgsConstructor;
import static com.ecep.contract.SpringApp.getBean;
/**
* 银行单元格
*/
@NoArgsConstructor
public class BankTableCell<T> extends AsyncUpdateTableCell<T, Integer, BankVo> {
/**
* 创建单元格工厂
*
* @param bankService 银行服务
* @return 单元格工厂
*/
public static <V> Callback<javafx.scene.control.TableColumn<V, Integer>, javafx.scene.control.TableCell<V, Integer>> forTableColumn(
BankService bankService) {
return param -> new BankTableCell<V>(bankService);
}
public BankTableCell(BankService service) {
setService(service);
}
@Override
protected BankService getServiceBean() {
return SpringApp.getBean(BankService.class);
return getBean(BankService.class);
}
@Override
public String format(BankVo entity) {
return getService().getStringConverter().toString(entity);
}
}

View File

@@ -0,0 +1,59 @@
package com.ecep.contract.controller.table.cell;
import static com.ecep.contract.SpringApp.getBean;
import com.ecep.contract.service.InvoiceService;
import com.ecep.contract.vo.InvoiceVo;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
import lombok.NoArgsConstructor;
/**
* 合同发票表格单元格
* 用于在表格中显示合同发票信息
*/
@NoArgsConstructor
public class ContractInvoiceTableCell<V> extends AsyncUpdateTableCell<V, Integer, InvoiceVo> {
/**
* 创建一个ContractInvoiceTableCell的TableCell工厂自动获取InvoiceService实例
*
* @param <S> 表格行类型
* @return TableCell工厂回调
*/
public static <S> Callback<TableColumn<S, Integer>, TableCell<S, Integer>> forTableColumn() {
return forTableColumn(getBean(InvoiceService.class));
}
/**
* 创建一个ContractInvoiceTableCell的TableCell工厂使用提供的InvoiceService实例
*
* @param <S> 表格行类型
* @param service InvoiceService实例
* @return TableCell工厂回调
*/
public static <S> Callback<TableColumn<S, Integer>, TableCell<S, Integer>> forTableColumn(InvoiceService service) {
return param -> new ContractInvoiceTableCell<S>(service);
}
/**
* 使用指定的InvoiceService创建ContractInvoiceTableCell
*
* @param invoiceService InvoiceService实例
*/
public ContractInvoiceTableCell(InvoiceService invoiceService) {
setService(invoiceService);
}
@Override
protected InvoiceService getServiceBean() {
return getBean(InvoiceService.class);
}
@Override
public String format(InvoiceVo entity) {
return entity.getCode();
}
}

View File

@@ -4,10 +4,15 @@ import com.ecep.contract.SpringApp;
import com.ecep.contract.service.DepartmentService;
import com.ecep.contract.vo.DepartmentVo;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class DepartmentTableCell<T> extends AsyncUpdateTableCell<T, Integer, DepartmentVo> {
public DepartmentTableCell(DepartmentService service) {
setService(service);
}
@@ -17,4 +22,20 @@ public class DepartmentTableCell<T> extends AsyncUpdateTableCell<T, Integer, Dep
return SpringApp.getBean(DepartmentService.class);
}
/**
* 为表格列创建 DepartmentTableCell 的工厂方法
*
* @param service 部门服务
* @param <T> 表格行数据类型
* @return 表格列的回调函数
*/
public static <T> Callback<TableColumn<T, Integer>, TableCell<T, Integer>> forTableColumn(DepartmentService service) {
return column -> new DepartmentTableCell<>(service);
}
@Override
public String format(DepartmentVo entity) {
return getService().getStringConverter().toString(entity);
}
}

View File

@@ -14,7 +14,7 @@ import lombok.NoArgsConstructor;
public class InventoryTableCell<V> extends AsyncUpdateTableCell<V, Integer, InventoryVo> {
/**
* 创建单元格工厂
*
*
* @param inventoryService 库存服务
* @return 单元格工厂
*/
@@ -22,12 +22,12 @@ public class InventoryTableCell<V> extends AsyncUpdateTableCell<V, Integer, Inve
InventoryService inventoryService) {
return param -> new InventoryTableCell<>(inventoryService);
}
/**
* 创建单元格工厂支持自定义StringConverter
*
*
* @param inventoryService 库存服务
* @param converter 字符串转换器
* @param converter 字符串转换器
* @return 单元格工厂
*/
public static <V> Callback<javafx.scene.control.TableColumn<V, Integer>, javafx.scene.control.TableCell<V, Integer>> forTableColumn(
@@ -57,7 +57,7 @@ public class InventoryTableCell<V> extends AsyncUpdateTableCell<V, Integer, Inve
public String format(InventoryVo entity) {
Integer catalogId = entity.getCatalogId();
InventoryCatalogVo catalog = getInventoryCatalogService().findById(catalogId);
return (catalog != null ? (catalog.getName() + " ") : "") + entity.getName();
return (catalog != null ? (catalog.getName() + " ") : "") + entity.getName() + " " + entity.getSpecification();
}
}

View File

@@ -42,7 +42,6 @@ public class VendorTypeTableCell<T> extends AsyncUpdateTableCell<T, VendorType,
protected VendorTypeLocalVo initialize() {
VendorType item = getItem();
VendorTypeLocalVo localVo = getServiceBean().findByType(item);
System.out.println("item = " + item + ", localVo = " + getServiceBean().getStringConverter().toString(localVo));
return localVo;
}

View File

@@ -213,6 +213,7 @@ public class VendorApprovedListTabSkinVendors
task.setEveryYearMinContracts(qualifiedVendorEveryYearMinContractsSpinner.getValue());
UITools.showTaskDialogAndWait("导入供方", task, null);
loadTableDataSet();
}
public void onVendorTableRefreshAction(ActionEvent event) {

View File

@@ -19,6 +19,7 @@ import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import com.ecep.contract.constant.CompanyVendorConstant;
import com.ecep.contract.service.*;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
@@ -119,7 +120,8 @@ public class VendorApprovedListVendorExportTask extends Tasker<Object> {
}
private File getVendorApprovedListTemplate() {
return getVendorService().getVendorApprovedListTemplate();
String path = getConfService().getString(CompanyVendorConstant.KEY_APPROVED_LIST_TEMPLATE);
return new File(path);
}
@Override

View File

@@ -95,6 +95,7 @@ public class VendorApprovedListVendorImportTask extends Tasker<Object> {
}
service = getBean(VendorApprovedService.class);
// 检索供方
VendorService vendorService = getBean(VendorService.class);
Page<VendorVo> page = vendorService.findAll(
ParamUtils.builder()
@@ -136,7 +137,17 @@ public class VendorApprovedListVendorImportTask extends Tasker<Object> {
// 明确 company 实例
CompanyVo company = initializedVendorCompany(vendor);
if (company == null) {
// 无效
// 无效, 删除异常数据
holder.error("供方(#" + vendor.getId() + ")无对应的公司消息");
List<VendorApprovedItemVo> items = getItemService().findAllByListAndVendor(approvedList, vendor);
if (items != null && !items.isEmpty()) {
// 删除
MessageHolder subHolder = holder.sub(" - ");
items.forEach(item -> {
getItemService().delete(item);
subHolder.info("删除 #" + item.getId() + ", " + item.getVendorName());
});
}
return;
}
@@ -144,15 +155,15 @@ public class VendorApprovedListVendorImportTask extends Tasker<Object> {
VendorType vendorType = vendor.getType();
if (vendorType == null) {
subHolder.error("供方分类为空");
return;
subHolder.debug("供方分类未设置");
}
// 确认供方的developDate 是否在供方名录的发布日期之后
LocalDate developDate = vendor.getDevelopDate();
if (developDate == null) {
subHolder.error("供方的开发日期为空");
subHolder.error("开发日期未设置");
} else if (developDate.isAfter(approvedList.getPublishDate())) {
subHolder.info("开发日期在供方名录发布之后, 跳过...");
return;
}
@@ -160,108 +171,13 @@ public class VendorApprovedListVendorImportTask extends Tasker<Object> {
List<VendorApprovedItemVo> items = getItemService().findAllByListAndVendor(approvedList, vendor);
if (items == null || items.isEmpty()) {
// 供方不在供方名录中时,新建一个
VendorApprovedItemVo item = new VendorApprovedItemVo();
item.setListId(approvedList.getId());
item.setVendorId(vendor.getId());
// 当前供应商分类是不合格供应商时
if (vendorType == VendorType.UNQUALIFIED) {
// 检索查看供方的评价表, 看与发布日期期间是否有评价表
if (!checkAsQualifiedVendorByEvaluationFormFiles(vendor, item, subHolder)) {
// 不合同供方,跳过
if (logUnqualifiedVendor) {
subHolder.info("供方是不合格供方, 不纳入名录");
}
// 上一年度的供方目录中是否有此供应商
return;
}
item.setDescription("");
}
// 当前供应商分类是合格供方时
else if (vendorType == VendorType.TYPICALLY) {
// 协议经销商,认定为合格供方
if (vendor.isProtocolProvider()) {
item.setType(VendorType.QUALIFIED);
item.setDescription("协议经销商");
}
// 非协议经销商
else {
// 查看供方的合同看近3年期间是否有合同
List<ContractVo> contracts = findAllVendorContracts(vendor, approvedList.getPublishDate());
if (contracts.isEmpty()) {
// 没有合同,应该归入不合格供应商
// 保持一般供应商分类,后期流程处理是否变更分类
item.setType(VendorType.TYPICALLY);
if (logTypicallyVendorNoThreeYearContract) {
subHolder.warn("供方近" + vendorContractMinusYear + "年没有合作, 应该转为不合格供应商");
}
item.setDescription(STR_MEET_UNQUALIFIED);
} else {
// 检查近3年期间是否都有合同
if (checkAllYearHasContract(contracts, approvedList.getPublishDate())) {
// 每年都有合同,合同数是否符合合格供方标准
if (checkAllYearMinHasContract(contracts, approvedList.getPublishDate())) {
// 保持一般供应商分类,后期流程处理是否变更分类
item.setType(VendorType.TYPICALLY);
subHolder.info("供方近" + vendorContractMinusYear + "年每年都有合作, 符合合格供方标准");
item.setDescription(STR_MEET_QUALIFIED);
} else {
item.setType(VendorType.TYPICALLY);
subHolder.warn("供方近" + vendorContractMinusYear + "年每年都有合作, 但是合同数不足, 应转为一般供应商");
item.setDescription(STR_MEET_TYPICALLY);
}
} else {
item.setType(VendorType.TYPICALLY);
item.setDescription("");
}
}
}
}
// 当前供应商分类是合格供方时
else if (vendorType == VendorType.QUALIFIED) {
// 查看供方的合同看近3年期间是否有合同
List<ContractVo> contracts = findAllVendorContracts(vendor, approvedList.getPublishDate());
item.setType(vendorType);
if (contracts.isEmpty()) {
if (logTypicallyVendorNoThreeYearContract) {
subHolder.warn("供方近" + vendorContractMinusYear + "年没有合作, 应该转为不合格供应商");
}
item.setDescription(STR_MEET_UNQUALIFIED);
} else {
// 检查近3年期间是否都有合同
if (checkAllYearHasContract(contracts, approvedList.getPublishDate())) {
// 每年都有合同,合同数是否符合合格供方标准
if (checkAllYearMinHasContract(contracts, approvedList.getPublishDate())) {
item.setDescription("");
} else {
subHolder.warn("供方近" + vendorContractMinusYear + "年每年都有合作, 但是合同数不足, 应转为一般供应商");
item.setDescription(STR_MEET_TYPICALLY);
}
} else {
subHolder.warn("供方近" + vendorContractMinusYear + "年非每年都有合作");
item.setDescription("");
}
}
}
// 未知分类时
else {
item.setDescription("未知供方分类");
}
// 匹配的历史名称
updateVendorNameWithOldName(vendor, item);
getItemService().save(item);
syncWhenNoItem(vendor, company, subHolder);
return;
}
if (items.size() == 1) {
VendorApprovedItemVo first = items.getFirst();
if (!StringUtils.hasText(first.getVendorName())) {
updateVendorNameWithOldName(vendor, first);
}
updateItem(vendor, first, subHolder);
return;
}
VendorApprovedItemVo first = items.getFirst();
syncItem(vendor, company, first, subHolder);
for (int i = 1; i < items.size(); i++) {
VendorApprovedItemVo item = items.get(i);
@@ -270,11 +186,123 @@ public class VendorApprovedListVendorImportTask extends Tasker<Object> {
}
}
private void updateVendorNameWithOldName(VendorVo vendor, VendorApprovedItemVo item) {
CompanyVo company = initializedVendorCompany(vendor);
if (company == null) {
/**
* 当没有匹配的供方名录项时
*
* @param vendor
* @param company
* @param subHolder
*/
private void syncWhenNoItem(VendorVo vendor, CompanyVo company, MessageHolder subHolder) {
VendorType vendorType = vendor.getType();
if (vendorType == null) {
subHolder.debug("供方分类未设置");
}
VendorApprovedItemVo item = new VendorApprovedItemVo();
item.setListId(approvedList.getId());
item.setVendorId(vendor.getId());
// 当前供应商分类是不合格供应商时
if (vendorType == VendorType.UNQUALIFIED) {
// 检索查看供方的评价表, 看与发布日期期间是否有评价表
if (!checkAsQualifiedVendorByEvaluationFormFiles(vendor, item, subHolder)) {
// 不合同供方,跳过
if (logUnqualifiedVendor) {
subHolder.info("供方是不合格供方, 不纳入名录");
}
// 上一年度的供方目录中是否有此供应商
return;
}
item.setDescription("");
}
// 当前供应商分类是合格供方时
else if (vendorType == VendorType.TYPICALLY) {
// 协议经销商,认定为合格供方
if (vendor.isProtocolProvider()) {
item.setType(VendorType.QUALIFIED);
item.setDescription("协议经销商");
}
// 非协议经销商
else {
// 查看供方的合同看近3年期间是否有合同
List<ContractVo> contracts = findAllVendorContracts(vendor, approvedList.getPublishDate());
if (contracts.isEmpty()) {
// 没有合同,应该归入不合格供应商
// 保持一般供应商分类,后期流程处理是否变更分类
item.setType(VendorType.TYPICALLY);
if (logTypicallyVendorNoThreeYearContract) {
subHolder.warn("供方近" + vendorContractMinusYear + "年没有合作, 应该转为不合格供应商");
}
item.setDescription(STR_MEET_UNQUALIFIED + "(缺合同1)");
} else {
// 检查近3年期间是否都有合同
if (checkAllYearHasContract(contracts, approvedList.getPublishDate())) {
// 每年都有合同,合同数是否符合合格供方标准
if (checkAllYearMinHasContract(contracts, approvedList.getPublishDate())) {
// 保持一般供应商分类,后期流程处理是否变更分类
item.setType(VendorType.TYPICALLY);
subHolder.info("供方近" + vendorContractMinusYear + "年每年都有合作, 符合合格供方标准");
item.setDescription(STR_MEET_QUALIFIED);
} else {
item.setType(VendorType.TYPICALLY);
subHolder.warn("供方近" + vendorContractMinusYear + "年每年都有合作, 但是合同数不足, 应转为一般供应商");
item.setDescription(STR_MEET_TYPICALLY);
}
} else {
item.setType(VendorType.TYPICALLY);
item.setDescription("");
}
}
}
}
// 当前供应商分类是合格供方时
else if (vendorType == VendorType.QUALIFIED) {
// 查看供方的合同看近3年期间是否有合同
List<ContractVo> contracts = findAllVendorContracts(vendor, approvedList.getPublishDate());
item.setType(vendorType);
if (contracts.isEmpty()) {
if (logTypicallyVendorNoThreeYearContract) {
subHolder.warn("供方近" + vendorContractMinusYear + "年没有合作, 应该转为不合格供应商");
}
item.setDescription(STR_MEET_UNQUALIFIED + "(缺合同2)");
} else {
// 检查近3年期间是否都有合同
if (checkAllYearHasContract(contracts, approvedList.getPublishDate())) {
// 每年都有合同,合同数是否符合合格供方标准
if (checkAllYearMinHasContract(contracts, approvedList.getPublishDate())) {
item.setDescription("");
} else {
subHolder.warn("供方近" + vendorContractMinusYear + "年每年都有合作, 但是合同数不足, 应转为一般供应商");
item.setDescription(STR_MEET_TYPICALLY);
}
} else {
subHolder.warn("供方近" + vendorContractMinusYear + "年非每年都有合作");
item.setDescription("");
}
}
}
// 未知分类时
else {
item.setDescription("未知供方分类");
}
// 匹配的历史名称
updateVendorNameWithOldName(company, item);
getItemService().save(item);
}
/**
* 匹配历史名称,当前供方名称为空时
* @param company
* @param item
*/
private void updateVendorNameWithOldName(CompanyVo company, VendorApprovedItemVo item) {
if (StringUtils.hasText(item.getVendorName())) {
// 已经有供方名称时,不更新
return;
}
CompanyOldNameVo companyOldName = getCompanyOldNameService().findMatchByDate(company,
approvedList.getPublishDate());
if (companyOldName != null) {
@@ -288,11 +316,12 @@ public class VendorApprovedListVendorImportTask extends Tasker<Object> {
@Setter
private boolean logUnqualifiedVendorRemove = true;
private void updateItem(
VendorVo vendor, VendorApprovedItemVo item, MessageHolder holder) {
private void syncItem(
VendorVo vendor, CompanyVo company, VendorApprovedItemVo item, MessageHolder holder) {
VendorType t1 = item.getType();
VendorType vendorType = vendor.getType();
VendorApprovedItemService itemService = getItemService();
updateVendorNameWithOldName(company, item);
if (t1 != vendorType) {
holder.warn("注意分类不一致, " + t1 + ", " + vendorType + ".");
}

View File

@@ -2,6 +2,7 @@ package com.ecep.contract.controller.vendor.approved_list;
import java.io.File;
import com.ecep.contract.task.VendorApprovedListMakePathTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -104,15 +105,13 @@ public class VendorApprovedListWindowController
}
public void onApprovedListCreatePathAction(ActionEvent event) {
VendorApprovedListMakePathTask task = new VendorApprovedListMakePathTask();
task.setApprovedList(getEntity());
UITools.showTaskDialogAndWait("创建目录", task, null);
int id = viewModel.getId().get();
VendorApprovedVo list = service.findById(id);
if (service.makePathAbsent(list)) {
VendorApprovedVo saved = service.save(list);
viewModel.update(saved);
} else {
setStatus("目录存在或创建失败");
}
viewModel.update(list);
}
public void onApprovedListChangePathAction(ActionEvent event) {
@@ -123,8 +122,4 @@ public class VendorApprovedListWindowController
task.setApprovedList(getEntity());
UITools.showTaskDialogAndWait("导出供方", task, null);
}
private void save(ActionEvent event) {
saveTabSkins();
}
}

View File

@@ -47,6 +47,13 @@ public class PurchaseOrderTabSkinBase
initializeEmployeeField(controller.verifierField, viewModel.getVerifier());
initializeEmployeeField(controller.closerField, viewModel.getCloser());
// 税率和税率锁定绑定
controller.taxRateField.textProperty().bindBidirectional(viewModel.getTaxRate(),
new javafx.util.converter.NumberStringConverter(getLocale(), "#.##"));
controller.taxRateLockedField.selectedProperty().bindBidirectional(viewModel.getTaxRateLocked());
viewModel.getVendorCode().bindBidirectional(controller.vendorCodeField.textProperty());
controller.makeDateField.textProperty().bind(viewModel.getMakerDate().map(MyDateTimeUtils::format));
controller.verifierDateField.textProperty().bind(viewModel.getVerifierDate().map(MyDateTimeUtils::format));
controller.modifierDateField.textProperty().bind(viewModel.getModifyDate().map(MyDateTimeUtils::format));
@@ -59,7 +66,6 @@ public class PurchaseOrderTabSkinBase
}
private void initializeEmployeeField(TextField textField, SimpleObjectProperty<Integer> property) {
EmployeeStringConverter converter = getEmployeeStringConverter();
UITools.autoCompletion(textField, property, controller.getEmployeeService());
}

View File

@@ -125,7 +125,7 @@ public class PurchaseOrderTabSkinBillVoucher
public Builder getSpecification(PurchaseOrderVo parent) {
Builder params = getSpecification();
params.equals("purchaseOrder", parent);
params.equals("purchaseOrder", parent.getId());
return params;
}

View File

@@ -149,7 +149,7 @@ public class PurchaseOrderTabSkinItems
idColumn.setCellValueFactory(param -> param.getValue().getId());
refIdColumn.setCellValueFactory(param -> param.getValue().getRefId());
nameColumn.setCellValueFactory(param -> param.getValue().getInventory());
nameColumn.setCellFactory(col -> new InventoryTableCell<>());
nameColumn.setCellFactory(InventoryTableCell.forTableColumn(getInventoryService()));
// unitColumn.setCellValueFactory(param -> param.getValue().getInventory());
// unitColumn.setCellFactory(col -> new UnitTableCell());
NumberCellFactory v1 = new NumberCellFactory("%,.1f", null);

View File

@@ -14,6 +14,7 @@ import com.ecep.contract.vm.PurchaseOrderViewModel;
import com.ecep.contract.vo.PurchaseOrderVo;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
@@ -49,6 +50,9 @@ public class PurchaseOrderWindowController
public TextField closerDateField;
public TextField closerField;
public Label versionLabel;
public TextField taxRateField;
public CheckBox taxRateLockedField;
public TextField vendorCodeField;
@Autowired
private PurchaseOrdersService service;

View File

@@ -0,0 +1,71 @@
package com.ecep.contract.converter;
import java.io.File;
import org.springframework.util.StringUtils;
import com.ecep.contract.SpringApp;
import com.ecep.contract.service.CompanyCustomerEvaluationFormFileService;
import com.ecep.contract.service.CompanyCustomerFileService;
import com.ecep.contract.vo.CompanyCustomerEvaluationFormFileVo;
import com.ecep.contract.vo.CustomerFileVo;
import javafx.util.StringConverter;
/**
* CompanyCustomerEvaluationFormFileVo的StringConverter实现用于JavaFX控件中的显示和转换
*/
public class CompanyCustomerEvaluationFormFileStringConverter
extends StringConverter<CompanyCustomerEvaluationFormFileVo> {
private final CompanyCustomerEvaluationFormFileService service;
private CompanyCustomerFileService customerFileService;
/**
* 构造函数
*
* @param service CompanyCustomerEvaluationFormFileService实例
*/
public CompanyCustomerEvaluationFormFileStringConverter(CompanyCustomerEvaluationFormFileService service) {
this.service = service;
}
private CompanyCustomerFileService getCustomerFileService() {
if (customerFileService == null) {
customerFileService = SpringApp.getBean(CompanyCustomerFileService.class);
}
return customerFileService;
}
/**
* 将CompanyCustomerEvaluationFormFileVo对象转换为字符串
*
* @param formFile CompanyCustomerEvaluationFormFileVo对象
* @return 转换后的字符串
*/
@Override
public String toString(CompanyCustomerEvaluationFormFileVo formFile) {
if (formFile == null || formFile.getCustomerFile() == null) {
return "";
}
CustomerFileVo customerFile = getCustomerFileService().findById(formFile.getCustomerFile());
if (customerFile == null || !StringUtils.hasText(customerFile.getFilePath())) {
return "#" + formFile.getCustomerFile();
}
File file = new File(customerFile.getFilePath());
return file.getName();
}
/**
* 将字符串转换为CompanyCustomerEvaluationFormFileVo对象
*
* @param string 字符串
* @return 转换后的CompanyCustomerEvaluationFormFileVo对象
*/
@Override
public CompanyCustomerEvaluationFormFileVo fromString(String string) {
// 由于文件名称可能不唯一这里返回null
// 实际使用时应该通过ID或其他唯一标识来查找
return null;
}
}

View File

@@ -0,0 +1,47 @@
package com.ecep.contract.converter;
import com.ecep.contract.service.DeliverySignMethodService;
import com.ecep.contract.vo.DeliverySignMethodVo;
import javafx.util.StringConverter;
/**
* DeliverySignMethodVo的StringConverter实现用于JavaFX控件中的显示和转换
*/
public class DeliverySignMethodStringConverter extends StringConverter<DeliverySignMethodVo> {
private final DeliverySignMethodService service;
/**
* 构造函数
*
* @param service DeliverySignMethodService实例
*/
public DeliverySignMethodStringConverter(DeliverySignMethodService service) {
this.service = service;
}
/**
* 将DeliverySignMethodVo对象转换为字符串
*
* @param method DeliverySignMethodVo对象
* @return 转换后的字符串
*/
@Override
public String toString(DeliverySignMethodVo method) {
return method == null ? "" : method.getCode() + " " + method.getName();
}
/**
* 将字符串转换为DeliverySignMethodVo对象
*
* @param string 字符串
* @return 转换后的DeliverySignMethodVo对象
*/
@Override
public DeliverySignMethodVo fromString(String string) {
if (string == null || string.isEmpty()) {
return null;
}
// 查找所有方法,然后通过名称匹配
return service.findByName(string);
}
}

View File

@@ -1,30 +1,37 @@
package com.ecep.contract.converter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import com.ecep.contract.service.DepartmentService;
import com.ecep.contract.vo.DepartmentVo;
import javafx.util.StringConverter;
import jakarta.annotation.PostConstruct;
@Lazy
@Component
public class DepartmentStringConverter extends EntityStringConverter<DepartmentVo> {
@Lazy
@Autowired
/**
* 部门字符串转换器
*/
public class DepartmentStringConverter extends StringConverter<DepartmentVo> {
private DepartmentService service;
public DepartmentStringConverter() {
}
@PostConstruct
private void init() {
setInitialized(department -> service.findById(department.getId()));
setSuggestion(service::search);
public DepartmentStringConverter(DepartmentService service) {
this.service = service;
}
@Override
public String toString(DepartmentVo department) {
if (department == null) {
return "-";
}
return department.getCode() + " " + department.getName();
}
@Override
public DepartmentVo fromString(String string) {
if (service == null || string == null || string.trim().isEmpty()) {
return null;
}
return service.findByCode(string.trim());
}
}

View File

@@ -0,0 +1,46 @@
package com.ecep.contract.converter;
import com.ecep.contract.service.InventoryCatalogService;
import com.ecep.contract.vo.InventoryCatalogVo;
import javafx.util.StringConverter;
/**
* 存货分类字符串转换器
* 用于在UI组件中显示存货分类信息并支持从字符串还原存货分类对象
*/
public class InventoryCatalogStringConverter<T> extends StringConverter<InventoryCatalogVo> {
/** 存货分类服务,用于从字符串查找对应的存货分类对象 */
private final InventoryCatalogService service;
/**
* 构造函数
*
* @param service 存货分类服务实例
*/
public InventoryCatalogStringConverter(InventoryCatalogService service) {
this.service = service;
}
/**
* 将存货分类对象转换为字符串表示
*
* @param object 存货分类对象
* @return 存货分类的名称如果对象为null则返回空字符串
*/
@Override
public String toString(InventoryCatalogVo object) {
return object == null ? "" : object.getName();
}
/**
* 从字符串还原存货分类对象
*
* @param string 存货分类名称
* @return 对应的存货分类对象如果未找到则返回null
*/
@Override
public InventoryCatalogVo fromString(String string) {
return service.findByName(string);
}
}

View File

@@ -0,0 +1,46 @@
package com.ecep.contract.converter;
import com.ecep.contract.service.InventoryService;
import com.ecep.contract.vo.InventoryVo;
import javafx.util.StringConverter;
/**
* 存货字符串转换器
* 用于在UI组件中显示存货信息并支持从字符串还原存货对象
*/
public class InventoryStringConverter extends StringConverter<InventoryVo> {
/** 存货服务,用于从字符串查找对应的存货对象 */
private final InventoryService service;
/**
* 构造函数
*
* @param service 存货服务实例
*/
public InventoryStringConverter(InventoryService service) {
this.service = service;
}
/**
* 将存货对象转换为字符串表示
*
* @param object 存货对象
* @return 存货的名称如果对象为null则返回空字符串
*/
@Override
public String toString(InventoryVo object) {
return object == null ? "" : (object.getName() + "-" + object.getSpecification());
}
/**
* 从字符串还原存货对象
*
* @param string 存货名称
* @return 对应的存货对象如果未找到则返回null
*/
@Override
public InventoryVo fromString(String string) {
return service.findByName(string);
}
}

View File

@@ -1,29 +1,25 @@
package com.ecep.contract.converter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import com.ecep.contract.service.ProductTypeService;
import com.ecep.contract.vo.ProductTypeVo;
import jakarta.annotation.PostConstruct;
import javafx.util.StringConverter;
@Lazy
@Component
public class ProductTypeStringConverter extends EntityStringConverter<ProductTypeVo> {
@Lazy
@Autowired
private ProductTypeService service;
public ProductTypeStringConverter() {
public class ProductTypeStringConverter extends StringConverter<ProductTypeVo> {
private final ProductTypeService service;
public ProductTypeStringConverter(ProductTypeService service) {
this.service = service;
}
@PostConstruct
private void init() {
setInitialized(type -> service.findById(type.getId()));
setSuggestion(service::search);
@Override
public String toString(ProductTypeVo type) {
return type == null ? "" : type.getCode() + " " + type.getName();
}
@Override
public ProductTypeVo fromString(String string) {
return service.findByName(string);
}

View File

@@ -1,5 +1,6 @@
package com.ecep.contract.converter;
import javafx.util.StringConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@@ -9,21 +10,21 @@ import com.ecep.contract.vo.ProductUsageVo;
import jakarta.annotation.PostConstruct;
@Lazy
@Component
public class ProductUsageStringConverter extends EntityStringConverter<ProductUsageVo> {
@Lazy
@Autowired
public class ProductUsageStringConverter extends StringConverter<ProductUsageVo> {
private ProductUsageService service;
public ProductUsageStringConverter() {
public ProductUsageStringConverter(ProductUsageService service) {
this.service = service;
}
@PostConstruct
private void init() {
setInitialized(usage -> service.findById(usage.getId()));
setSuggestion(service::search);
@Override
public String toString(ProductUsageVo usage) {
return usage == null ? "" : usage.getCode() + " " + usage.getName();
}
@Override
public ProductUsageVo fromString(String string) {
return service.findByName(string);
}

View File

@@ -1,30 +1,31 @@
package com.ecep.contract.converter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import com.ecep.contract.service.ProjectIndustryService;
import com.ecep.contract.vo.ProjectIndustryVo;
import jakarta.annotation.PostConstruct;
import javafx.util.StringConverter;
@Lazy
@Component
public class ProjectIndustryStringConverter extends EntityStringConverter<ProjectIndustryVo> {
@Lazy
@Autowired
public class ProjectIndustryStringConverter extends StringConverter<ProjectIndustryVo> {
private ProjectIndustryService service;
public ProjectIndustryStringConverter() {
public ProjectIndustryStringConverter(ProjectIndustryService service) {
this.service = service;
}
@PostConstruct
private void init() {
setInitialized(industry -> service.findById(industry.getId()));
setSuggestion(service::search);
@Override
public String toString(ProjectIndustryVo object) {
if (object == null) {
return "";
}
return object.getCode() + " " + object.getName();
}
@Override
public ProjectIndustryVo fromString(String string) {
if (string == null || string.isEmpty()) {
return null;
}
return service.findByName(string);
}
}

View File

@@ -0,0 +1,24 @@
package com.ecep.contract.converter;
import com.ecep.contract.service.ProjectSaleTypeService;
import com.ecep.contract.vo.ProjectSaleTypeVo;
import javafx.util.StringConverter;
public class ProjectSaleTypeStringConverter extends StringConverter<ProjectSaleTypeVo> {
private final ProjectSaleTypeService service;
public ProjectSaleTypeStringConverter(ProjectSaleTypeService service) {
this.service = service;
}
@Override
public String toString(ProjectSaleTypeVo type) {
return type == null ? "" : type.getCode() + " " + type.getName();
}
@Override
public ProjectSaleTypeVo fromString(String string) {
return service.findByName(string);
}
}

View File

@@ -0,0 +1,46 @@
package com.ecep.contract.converter;
import com.ecep.contract.service.ProjectTypeService;
import com.ecep.contract.vo.ProjectTypeVo;
import javafx.util.StringConverter;
/**
* ProjectTypeVo的StringConverter实现用于JavaFX控件中的显示和转换
*/
public class ProjectTypeStringConverter extends StringConverter<ProjectTypeVo> {
private final ProjectTypeService service;
/**
* 构造函数
*
* @param service ProjectTypeService实例
*/
public ProjectTypeStringConverter(ProjectTypeService service) {
this.service = service;
}
/**
* 将ProjectTypeVo对象转换为字符串
*
* @param type ProjectTypeVo对象
* @return 转换后的字符串
*/
@Override
public String toString(ProjectTypeVo type) {
return type == null ? "" : type.getCode() + " " + type.getName();
}
/**
* 将字符串转换为ProjectTypeVo对象
*
* @param string 字符串
* @return 转换后的ProjectTypeVo对象
*/
@Override
public ProjectTypeVo fromString(String string) {
if (string == null || string.isEmpty()) {
return null;
}
return service.findByName(string);
}
}

View File

@@ -13,6 +13,9 @@ public class VendorCatalogStringConverter extends StringConverter<VendorCatalogV
@Override
public String toString(VendorCatalogVo object) {
if (object == null) {
return "-";
}
return object.getName();
}

View File

@@ -1,30 +1,26 @@
package com.ecep.contract.converter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import javafx.util.StringConverter;
import com.ecep.contract.service.VendorGroupService;
import com.ecep.contract.vo.VendorGroupVo;
import jakarta.annotation.PostConstruct;
@Lazy
@Component
public class VendorGroupStringConverter extends EntityStringConverter<VendorGroupVo> {
@Lazy
@Autowired
public class VendorGroupStringConverter extends StringConverter<VendorGroupVo> {
private VendorGroupService service;
public VendorGroupStringConverter() {
public VendorGroupStringConverter(VendorGroupService service) {
this.service = service;
}
@PostConstruct
private void init() {
setInitialized(group -> service.findById(group.getId()));
setSuggestion(service::search);
@Override
public String toString(VendorGroupVo object) {
return object == null ? "" : object.getCode() + " " + object.getName();
}
@Override
public VendorGroupVo fromString(String string) {
return service.findByCode(string);
}
}

View File

@@ -1,9 +1,11 @@
package com.ecep.contract.service;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.controlsfx.control.TaskProgressView;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@@ -21,6 +23,8 @@ import lombok.Data;
@Service
public class CloudRkService extends QueryService<CloudRkVo, CloudRkViewModel> {
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class EntInfo {
@@ -69,6 +73,10 @@ public class CloudRkService extends QueryService<CloudRkVo, CloudRkViewModel> {
.findFirst().orElse(null);
}
public Page<CloudRkVo> findAllByCompany(CompanyVo company) {
return findAll(ParamUtils.builder().equals("company", company.getId()).build(), Pageable.unpaged());
}
public boolean checkBlackListUpdateElapse(CompanyVo company, CloudRkVo cloudRk) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'checkBlackListUpdateElapse'");
@@ -78,4 +86,12 @@ public class CloudRkService extends QueryService<CloudRkVo, CloudRkViewModel> {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'updateBlackList'");
}
public void mergeTo(CompanyVo from, CompanyVo to) {
List<CloudRkVo> list = findAllByCompany(from).getContent();
for (CloudRkVo item : list) {
item.setCompanyId(to.getId());
save(item);
}
}
}

View File

@@ -1,7 +1,9 @@
package com.ecep.contract.service;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@@ -77,4 +79,16 @@ public class CloudTycService extends QueryService<CloudTycVo, CloudTycInfoViewMo
.findFirst().orElse(null);
}
public Page<CloudTycVo> findAllByCompany(CompanyVo company) {
return findAll(ParamUtils.builder().equals("company", company.getId()).build(), Pageable.unpaged());
}
public void mergeTo(CompanyVo from, CompanyVo to) {
List<CloudTycVo> list = findAllByCompany(from).getContent();
for (CloudTycVo item : list) {
item.setCompanyId(to.getId());
save(item);
}
}
}

View File

@@ -1,5 +1,6 @@
package com.ecep.contract.service;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.domain.Page;
@@ -32,4 +33,21 @@ public class CompanyContactService extends QueryService<CompanyContactVo, Compan
return page.getContent().getFirst();
}
public List<CompanyContactVo> findAllByCompany(CompanyVo company, LocalDate beginDate, LocalDate endDate) {
return findAll(ParamUtils.builder()
.equals("company", company.getId())
.between("setupDate", beginDate, endDate)
.build(), Pageable.unpaged()).getContent();
}
public void mergeTo(CompanyVo from, CompanyVo to) {
List<CompanyContactVo> contacts = findAllByCompany(from, LocalDate.MIN, LocalDate.MAX);
if (contacts == null || contacts.isEmpty()) {
return;
}
for (CompanyContactVo contact : contacts) {
contact.setCompanyId(to.getId());
save(contact);
}
}
}

View File

@@ -17,4 +17,12 @@ public class CompanyCustomerEntityService extends QueryService<CompanyCustomerEn
.equals("customer", customer.getId()).build(), Pageable.unpaged())
.getContent();
}
public void mergeTo(CustomerVo from, CustomerVo to) {
List<CompanyCustomerEntityVo> fromEntities = findAllByCustomer(from);
for (CompanyCustomerEntityVo fromEntity : fromEntities) {
fromEntity.setCustomerId(to.getId());
save(fromEntity);
}
}
}

View File

@@ -1,5 +1,7 @@
package com.ecep.contract.service;
import com.ecep.contract.converter.CompanyCustomerEvaluationFormFileStringConverter;
import javafx.util.StringConverter;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@@ -14,6 +16,8 @@ import java.util.List;
public class CompanyCustomerEvaluationFormFileService
extends QueryService<CompanyCustomerEvaluationFormFileVo, CompanyCustomerEvaluationFormFileViewModel> {
private final StringConverter<CompanyCustomerEvaluationFormFileVo> stringConverter = new CompanyCustomerEvaluationFormFileStringConverter(this);
/**
* 根据ID查找客户评估表文件
*/
@@ -25,18 +29,17 @@ public class CompanyCustomerEvaluationFormFileService
* 根据客户文件查找评估表文件
*/
public CompanyCustomerEvaluationFormFileVo findByCustomerFile(CustomerFileVo customerFile) {
return findByCustomerFile(customerFile.getId());
return findOneByProperty("customerFile", customerFile.getId());
}
public CompanyCustomerEvaluationFormFileVo findByCustomerFile(Integer customerFileId) {
List<CompanyCustomerEvaluationFormFileVo> page = findAll(ParamUtils.builder()
.equals("customerFile", customerFileId)
.build(), Pageable.ofSize(1))
.getContent();
if (page.isEmpty()) {
return null;
}
return page.getFirst();
return findOneByProperty("customerFile", customerFileId);
}
public List<CompanyCustomerEvaluationFormFileVo> searchByCompany(Integer companyId, String searchText) {
ParamUtils.Builder params = getSpecification(searchText);
params.equals("company", companyId);
return findAll(params.build(), Pageable.ofSize(10)).getContent();
}
/**
@@ -46,4 +49,10 @@ public class CompanyCustomerEvaluationFormFileService
return super.save(formFile);
}
@Override
public StringConverter<CompanyCustomerEvaluationFormFileVo> getStringConverter() {
return stringConverter;
}
}

View File

@@ -121,4 +121,12 @@ public class CompanyCustomerFileService extends QueryService<CustomerFileVo, Cus
public CustomerFileVo save(CustomerFileVo entity) {
return super.save(entity);
}
public void mergeTo(CustomerVo from, CustomerVo to) {
List<CustomerFileVo> fromFiles = findAllByCustomer(from);
for (CustomerFileVo fromFile : fromFiles) {
fromFile.setCustomer(to.getId());
save(fromFile);
}
}
}

View File

@@ -25,7 +25,7 @@ import javafx.util.StringConverter;
@CacheConfig(cacheNames = "company-customer-file-type")
public class CompanyCustomerFileTypeService
extends QueryService<CustomerFileTypeLocalVo, CompanyCustomerFileTypeLocalViewModel> {
private final StringConverter<CustomerFileTypeLocalVo> stringConverter = new CustomerFileTypeStringConverter(this);
private final CustomerFileTypeStringConverter stringConverter = new CustomerFileTypeStringConverter(this);
@Cacheable(key = "#p0")
@Override
@@ -39,13 +39,13 @@ public class CompanyCustomerFileTypeService
return super.findAll();
}
@Caching(put = { @CachePut(key = "#p0.id"), @CachePut(key = "'all'") })
@Caching(put = {@CachePut(key = "#p0.id"), @CachePut(key = "'all'")})
@Override
public CustomerFileTypeLocalVo save(CustomerFileTypeLocalVo entity) {
return super.save(entity);
}
@Caching(put = { @CachePut(key = "#p0.id"), @CachePut(key = "'all'") })
@Caching(put = {@CachePut(key = "#p0.id"), @CachePut(key = "'all'")})
@Override
public void delete(CustomerFileTypeLocalVo entity) {
super.delete(entity);
@@ -53,8 +53,7 @@ public class CompanyCustomerFileTypeService
@Cacheable
public Map<CustomerFileType, CustomerFileTypeLocalVo> findAll(Locale locale) {
Map<String, Object> params = new HashMap<>();
params.put("lang", locale.toLanguageTag());
Map<String, Object> params = ParamUtils.builder().equals("lang", locale.toLanguageTag()).build();
return findAll(params, Pageable.unpaged()).stream()
.collect(Collectors.toMap(CustomerFileTypeLocalVo::getType, Function.identity()));
}
@@ -66,7 +65,7 @@ public class CompanyCustomerFileTypeService
/**
* 根据语言标签和参数查找单个 CustomerFileTypeLocalVo 对象
*
*
* @param locale 语言区域
* @param key 参数键
* @param value 参数值

View File

@@ -1,5 +1,6 @@
package com.ecep.contract.service;
import com.ecep.contract.util.ParamUtils;
import com.ecep.contract.vm.CompanyInvoiceInfoViewModel;
import com.ecep.contract.vo.CompanyInvoiceInfoVo;
import com.ecep.contract.vo.CompanyVo;
@@ -15,9 +16,8 @@ import java.util.Map;
public class CompanyInvoiceInfoService extends QueryService<CompanyInvoiceInfoVo, CompanyInvoiceInfoViewModel> {
public List<CompanyInvoiceInfoVo> searchByCompany(CompanyVo company, String searchText) {
Map<String, Object> params = new HashMap<>();
params.put("company", company);
params.put("searchText", searchText);
Map<String, Object> params = ParamUtils.builder().equals("company", company.getId())
.search(searchText).build();
return findAll(params, Pageable.unpaged()).getContent();
}
}

Some files were not shown because too many files have changed in this diff Show More