1
0

Compare commits

...

53 Commits

Author SHA1 Message Date
6bf9a3295a docs: 将 database 模块文档拆分为独立文件 2026-04-01 17:31:25 +08:00
0a7e38f931 test: 添加全面的单元测试和集成测试
- 添加 common 模块单元测试 (ObjectHelper, SnowflakeHelper)
- 添加 database 模块集成测试 (SimpleServiceSupport, @SoftDelete)
- 添加 Controller REST API 契约测试
- 配置 H2 数据库和 p6spy 用于测试
- 更新 openspec 配置,添加并行任务和提问工具规则
2026-04-01 16:15:15 +08:00
fc9cb14daf chore: 将 .vscode 从 git 跟踪中移除并加入 gitignore 2026-04-01 11:06:42 +08:00
4abb65129b docs: 精简 README 和 openspec 配置,添加 AGENTS.md 2026-04-01 11:03:28 +08:00
8ef18a8e85 refactor(database): 合并数据库模块,简化包结构 2026-03-31 23:55:12 +08:00
22811e4adb chore: 添加 gitignore 规则和 openspec 配置 2026-03-31 11:19:10 +08:00
28bcc1a118 docs: 重构 README 文档,精简至 220 行,提取通用规范 2026-03-31 11:18:34 +08:00
e9b0e79d48 feat: 优化日志显示 2026-01-27 10:06:57 +08:00
18cd1dbed8 docs: 更新文档 2026-01-27 10:05:31 +08:00
2db84152b5 feat(jpa): 补充方法查询的测试用例 2026-01-22 23:55:26 +08:00
28baf5600b fix(jpa): 优化测试用例 2026-01-22 22:51:21 +08:00
015016a2da fix(jpa): 优化测试用例 2026-01-22 22:36:57 +08:00
7b555492ee feat(jpa): 增加默认 JpaQueryFactory 的注入 2026-01-22 22:36:38 +08:00
e7fa23a365 fix(jpa): 优化注释 2026-01-22 22:18:37 +08:00
2adf4951f7 fix(database): 补充事务注解 2026-01-22 22:18:37 +08:00
3692657b64 docs: 补充README 2026-01-22 22:18:37 +08:00
f81217f14a feat(jpa): 增加HQL查询实现对比 2026-01-22 18:24:23 +08:00
611988d7ab feat(jpa): 增加QueryDSL查询实现对比 2026-01-22 18:03:57 +08:00
324d646b27 feat(jpa): 增加Fenix查询实现对比 2026-01-22 17:08:38 +08:00
d1392f89a3 doc(jpa): 补充部份注释说明,优化日志输出 2026-01-22 16:06:02 +08:00
93eded44cb feat(jpa): 重新设计specification查询的例子 2026-01-22 15:42:34 +08:00
53d94c5f4c fix(eq): 完善测试用例 2026-01-21 17:16:53 +08:00
af4d13d76c fix(xbatis): 完善测试用例 2026-01-21 16:34:49 +08:00
a8e8ec0ef2 fix(jpa): 完善测试用例 2026-01-21 14:51:14 +08:00
02b9636fec fix(jpa): 修复测试用例 2026-01-21 13:23:33 +08:00
54c12deb25 feat(database): 调整模块依赖创建test公共包 2026-01-21 13:10:38 +08:00
e6a48d8e88 refactor(all): 调整模块依赖,划分代码范围 2026-01-21 10:33:08 +08:00
c839dfc4e3 feat(eq): 完成easy-query框架的适配 2026-01-20 18:47:08 +08:00
1b48081d5b build(deps): 统一管理 datasource-decorator 依赖版本并优化配置
- 在父 pom.xml 中统一管理 datasource-decorator.version 为 2.0.0
- 添加 xbatis 依赖管理,移除子模块中的重复配置
- 为 JPA 模块添加 p6spy 依赖,配置 SQL 日志格式
2026-01-07 16:28:06 +08:00
e4c0dc4884 refactor(jpa): 将测试数据中的字段名从 "in" 重命名为 "inside 2026-01-07 16:27:42 +08:00
8fc53e6fda feat(xbatis): 完成xbatis框架的适配 2026-01-07 16:21:00 +08:00
919664ba84 refactor(common): 将 Page 实体的 items 类型从 Stream 改为 List 2026-01-07 15:21:32 +08:00
f439381e04 refactor(service): 将查询条件解析逻辑封装到 QueryParser 中 2026-01-07 13:02:47 +08:00
657f9593ba refactor(jpa): 重命名 listPredicate 方法为 commonPredicates 2026-01-07 11:38:02 +08:00
af4be9db8f refactor(service): 将删除服务接口参数类型从 Iterable 改为 Set 2026-01-07 11:29:03 +08:00
d08f9db9ac refactor(exception): 将内部异常类抽取为公共异常类
将 SimpleServiceSupport 中的静态内部异常类抽取到 common 模块的独立异常类文件中,包括:
- IdNotFoundException: ID 未找到异常
- NotComparableException: 不可比较异常
- NotCollectionException: 非集合异常
- NotStringException: 非字符串异常
2026-01-07 11:17:03 +08:00
5bf6e9ecdc refactor(jpa): 将 Snowflake ID 生成逻辑抽取为公共工具类 2026-01-07 09:47:42 +08:00
1d6a08a16f build(deps): 移除 spring-boot-service-template-jpa-task 模块依赖 2026-01-06 15:58:27 +08:00
51b2cdb21d refactor(jpa): 使用 @Table 和 @Column 的 comment 属性替换 @Comment 注解 2026-01-06 15:53:40 +08:00
327e983c46 doc(common): 优化统一响应和工具类注释 2026-01-06 15:50:36 +08:00
d38196cb6e refactor(jpa): 使用 @Column 替换 @Comment 注解 2026-01-06 15:49:55 +08:00
8a944923ea refactor(common): 重构响应结构,使用泛型记录替代Map
- 将 responseCrudData() 重命名为 responseListData()
- 新增 ListItem 和 DetailItem 泛型记录替代 Map 包装
- 更新 QueryController 接口支持双泛型参数
- 优化类型安全性和代码可读性
2026-01-06 15:32:39 +08:00
6840d4a366 doc(common): 优化注释说明 2026-01-06 15:06:23 +08:00
8622891dbb refactor: 移除所有文件中的 @author 注释
移除所有 Java 文件中的 @author lanyuanxiaoyao 注释,统一代码风格。
2026-01-06 14:56:02 +08:00
d88078ce42 refactor(common): 优化继承结构,合并list和detail查询 2026-01-06 14:49:07 +08:00
3b755668e0 refactor(jpa): 将 Helper 类重命名为 DatabaseHelper 并调整包结构
- 将 Helper 类从 com.lanyuanxiaoyao.service.template.jpa 包迁移至 helper 子包
- 重命名类名为 DatabaseHelper 以更准确地表达其功能
- 同步更新测试类中的引用和类名使用
2026-01-06 14:27:03 +08:00
142b57975b refactor(common): 将包结构从 jpa 迁移至 common 并重构核心类
- 将所有控制器接口从 jpa 包迁移至 common 包
- 将 GlobalResponse、Query、Page 等核心类重构为 record 类型
- 移除 Lombok 依赖并简化代码结构
- 更新 SimpleService 接口以支持更通用的实体类型
- 调整 SimpleControllerSupport 和 SimpleServiceSupport 以适配新的 API
- 清理 web 模块的 pom.xml 中的冗余依赖和配置
2026-01-06 14:24:51 +08:00
2a374dc9c7 fix(jpa): 将默认分页常量类型从Integer改为int 2026-01-06 11:14:21 +08:00
116dc148f2 fix(jpa): 增加空值检查并优化查询条件处理
- 在 list 方法中增加空集合检查,避免无效查询
- 在 value 方法中增加空值检查,直接返回 null
- 重构查询条件处理逻辑,复用 path 变量减少重复调用
2026-01-06 10:58:22 +08:00
e8ffaab6a8 fix(jpa): 修正查询条件中 notEqual 方法的错误调用
在 SimpleServiceSupport 中,notEqual 查询条件错误地调用了 getEqual 方法,已修正为 getNotEqual 方法。
2026-01-06 10:47:51 +08:00
7e993f3de8 feat: 升级核心依赖版本并适配新API
- 将Spring Boot升级至4.0.0,Hibernate升级至7.1.8
- 迁移Hibernate命名策略至PhysicalNamingStrategySnakeCaseImpl
- 移除废弃的StandardGenerator接口引用
- 更新Jackson导入路径至tools.jackson包
2026-01-06 10:35:05 +08:00
bca64d3561 refactor: 优化代码架构 2025-09-29 14:09:53 +08:00
08cadc5f6f refactor: 改为多模块结构,增加更多的服务模板SDK 2025-09-29 10:17:36 +08:00
80 changed files with 4807 additions and 3023 deletions

11
.gitignore vendored
View File

@@ -54,12 +54,7 @@ buildNumber.properties
*.rar
hs_err_pid*
replay_pid*
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
.vscode/
.history/
*.vsix
.metadata
@@ -132,3 +127,7 @@ Icon
Network Trash Folder
Temporary Items
.apdisk
.opencode
.claude
.kilocode
openspec/changes/archive

1
AGENTS.md Normal file
View File

@@ -0,0 +1 @@
严格遵守openspec/config.yaml中context声明的项目规范

506
README.md
View File

@@ -1,469 +1,109 @@
# Spring Boot Service Template
这是一个基于 Spring Boot 的服务模板项目,旨在为开发者提供一个标准化的微服务基础结构,简化新项目的搭建过程,提高开发效率
微服务快速开发能力模板v1.1.0-SNAPSHOT。Java 17, Spring Boot 4.0.0, Spring Cloud 2025.1.0
## 目录
GroupId: `com.lanyuanxiaoyao`,根包: `com.lanyuanxiaoyao.service.template`
- [1. 项目概述](#1-项目概述)
- [2. 技术架构](#2-技术架构)
- [3. 项目结构](#3-项目结构)
- [4. 核心功能](#4-核心功能)
- [5. 使用指南](#5-使用指南)
- [6. 开发规范](#6-开发规范)
- [7. 项目集成指南](#7-项目集成指南)
## 模块
## 1. 项目概述
### 1.1 项目简介
Spring Boot Service Template 是一个标准化的微服务基础结构模板,专为 Java 开发者和微服务架构设计者打造。该项目提供了一套完整的 CRUD 操作框架,通过泛型支持不同类型的数据转换,大大减少了重复代码的编写。
该模板内置了完善的实体审计机制,自动维护实体的创建时间和修改时间。同时,提供了强大的查询功能,支持多种条件查询、分页和排序功能。
### 1.2 项目目标
提供统一的服务模板,简化新项目的搭建过程,提高开发效率。通过封装通用的业务逻辑,让开发者能够专注于核心业务功能的实现。
### 1.3 核心特性
- 标准化的项目结构:遵循业界最佳实践的目录结构和代码组织方式
- 简化依赖管理和构建流程:基于 Maven 的依赖管理,清晰的构建配置
- 支持快速构建和部署微服务:提供完整的微服务基础组件
- 泛型支持:通过泛型实现不同类型间的数据转换
- 完善的审计机制:自动维护实体的创建时间和修改时间
- 强大的查询功能:支持多种条件查询、分页和排序
- 灵活的扩展机制:易于定制和扩展的架构设计
### 1.4 适用场景
适用于需要快速搭建 Spring Boot 微服务的项目,特别是那些需要大量 CRUD 操作的业务系统。
## 2. 技术架构
### 2.1 技术选型
#### 2.1.1 后端技术栈
- Java 17
- Spring Boot 3.4.3
- Spring Data JPA
- QueryDSL 7.0
- Lombok
- Fenix Spring Boot Starter 3.1.0
#### 2.1.2 核心框架
- Spring Boot 作为核心框架,提供自动配置和快速开发能力
- Spring Data JPA 用于数据访问,简化数据库操作
- QueryDSL 用于类型安全的查询构建,避免运行时错误
- Fenix 用于复杂动态查询,提供更灵活的查询能力
- Lombok 减少样板代码,提高开发效率
#### 2.1.3 数据库技术
- H2 Database (测试环境)
### 2.2 架构设计
#### 2.2.1 整体架构图
```
┌─────────────────────────────────────┐
│ Controller Layer │
│ (处理HTTP请求数据转换与响应) │
├─────────────────────────────────────┤
│ Service Layer │
│ (业务逻辑处理,事务管理) │
├─────────────────────────────────────┤
│ Repository Layer │
│ (数据访问,数据库交互) │
├─────────────────────────────────────┤
│ Entity Layer │
│ (数据模型定义,实体映射) │
└─────────────────────────────────────┘
spring-boot-service-template/ (根 POM, packaging=pom)
├── spring-boot-service-template-common/ (jar — 通用工具)
└── spring-boot-service-template-database/ (jar — JPA 数据库能力)
```
#### 2.2.2 模块划分
- controller: 控制层,处理 HTTP 请求,包括接口定义和支持类
- entity: 实体层,定义数据模型和数据库映射
- service: 服务层,处理业务逻辑和事务管理
- repository: 仓储层,处理数据访问和数据库交互
- helper: 辅助类模块,提供工具类和通用方法
模块命名规则: `spring-boot-service-template-{capability}[-{impl}]`
#### 2.2.3 设计模式
- 模板方法模式:通过抽象类定义通用操作流程
- 策略模式:通过函数式接口实现数据转换策略
- 仓储模式:封装数据访问逻辑,提供统一的数据操作接口
## 包结构
## 3. 项目结构
### 3.1 目录结构说明
```
src/
├── main/
│ └── java/
│ └── com/lanyuanxiaoyao/service/template/
│ ├── controller/
├── entity/
├── helper/
├── repository/
│ └── service/
└── test/
└── java/
└── com/lanyuanxiaoyao/service/template/
├── controller/
├── entity/
├── repository/
└── service/
com.lanyuanxiaoyao.service.template.{module}/
configuration/ # @Configuration用 configuration 不用 config
controller/ # REST 控制器 + 接口
entity/ # JPA 实体、Query、GlobalResponse 等数据对象
exception/ # 自定义异常(用 exception 不用 exceptions
helper/ # 工具类(用 helper 不用 util/utils
repository/ # Spring Data 仓库
service/ # 业务服务
```
### 3.2 核心模块介绍
无独立 DTO/VO 包DTO 作为 Controller 内部 record。
#### 3.2.1 controller模块
提供基础的 CRUD 操作控制器接口和支持类。主要包含:
- [SimpleController](src/main/java/com/lanyuanxiaoyao/service/template/controller/SimpleController.java):定义基础 CRUD 接口
- [SimpleControllerSupport](src/main/java/com/lanyuanxiaoyao/service/template/controller/SimpleControllerSupport.java):实现基础 CRUD 功能
- [Query](src/main/java/com/lanyuanxiaoyao/service/template/controller/Query.java):查询条件封装类
- 其他辅助接口如 SaveController、ListController、DetailController、RemoveController
## database 模块
#### 3.2.2 entity模块
定义基础实体类,包含审计字段。主要包含:
- [IdOnlyEntity](src/main/java/com/lanyuanxiaoyao/service/template/entity/IdOnlyEntity.java):仅包含 ID 的基础实体
- [SimpleEntity](src/main/java/com/lanyuanxiaoyao/service/template/entity/SimpleEntity.java):包含基础字段的实体类,继承自 IdOnlyEntity
单表 CRUD → REST 接口快速实现框架。基于 JPA + Fenix + QueryDSL + MapStruct。
#### 3.2.3 service模块
提供基础服务接口和支持类。主要包含:
- [SimpleService](src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleService.java):定义基础服务接口
- [SimpleServiceSupport](src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java):实现基础服务功能
**文档**
- [开发指南](docs/database-development.md) - 模块架构、核心设计、技术实现、扩展指南
- [使用指南](docs/database-usage.md) - 快速开始、API 接口、查询条件、高级用法
#### 3.2.4 repository模块
定义数据访问仓储接口。主要包含:
- [SimpleRepository](src/main/java/com/lanyuanxiaoyao/service/template/repository/SimpleRepository.java):基础仓储接口
## 开发规范
#### 3.2.5 helper模块
提供辅助工具类。主要包含:
- [ObjectHelper](src/main/java/com/lanyuanxiaoyao/service/template/helper/ObjectHelper.java):对象帮助类
### 命名
### 3.3 测试模块结构
测试模块包含完整的测试用例,覆盖 controller、entity、repository 和 service 各层。通过实际的业务实体(如 Employee、Company、Report演示如何使用模板。
| 类别 | 模式 | 示例 |
| --------------- | ---------------------------------------- | ---------------------------------- |
| 实体基类 | `*Entity` | `IdOnlyEntity`, `SimpleEntity` |
| 业务实体 | 纯名词 | `Employee` |
| 控制器接口 | `*Controller` | `SaveController` |
| 控制器抽象基类 | `*ControllerSupport` | `SimpleControllerSupport` |
| 服务接口 | `*Service` | `SaveService` |
| 服务抽象基类 | `*ServiceSupport` | `SimpleServiceSupport` |
| 仓库 | `*Repository` | `EmployeeRepository` |
| 工具类 | `*Helper` | `ObjectHelper` |
| 配置类 | `*Configuration` | `QueryDSLConfiguration` |
| 异常 | `*Exception` | `IdNotFoundException` |
| 内部 DTO record | `SaveItem`/`ListItem`/`DetailItem` | Controller 内部 |
## 4. 核心功能
- 接口不加 `I` 前缀
- 方法: 动词开头 `save()`/`list()`/`detail()`/`remove()`;布尔用 `is`/`has` 前缀
- 常量 UPPER_SNAKE_CASE字段常量用 `@FieldNameConstants` 生成 `{Class}.Fields.{field}`
- 泛型: 简单用 `T`/`E`,领域用 `ENTITY`/`SAVE_ITEM`/`LIST_ITEM`/`DETAIL_ITEM`
### 4.1 基础CRUD操作
### 代码风格
#### 4.1.1 创建(Create)
支持通过 POST 请求创建实体对象。通过 save 接口实现,接收 SAVE_ITEM 类型的参数,通过 Mapper 转换为实体对象后保存。
- 2 空格缩进K&R 大括号,不用 Tab
- Import 排序: `java.*``jakarta.*` → 第三方 → `com.lanyuanxiaoyao.*`,不用通配符
- 积极使用 Java 17 特性: `record`(DTO) / `var` / `instanceof` 模式匹配 / Text Blocks / `String.formatted()`
- JavaDoc 用中文,含 `@param`/`@return`/`@see`,可用 `<p>`/`<h3>`/`<ul>`/`<pre>`
- 不写 `//` 行内注释,除非必要
#### 4.1.2 查询(Retrieve)
支持多种查询方式:
- 列表查询:获取所有实体对象
- 条件查询:根据指定条件查询实体对象
- 详情查询:根据 ID 获取单个实体对象
### 注解
#### 4.1.3 更新(Update)
支持通过 POST 请求更新实体对象。通过 save 接口实现,当传入包含 ID 的对象时执行更新操作。
用:
#### 4.1.4 删除(Delete)
支持通过 GET 请求删除实体对象。通过 remove 接口实现,根据 ID 删除指定实体。
- 实体: `@Getter` `@Setter` `@ToString` `@FieldNameConstants`,子类加 `@ToString(callSuper = true)`
- 日志: `@Slf4j`
- 注入: `@RequiredArgsConstructor(access = AccessLevel.PROTECTED)`(构造器注入)
- 事务: 写 `@Transactional(rollbackFor = Throwable.class)`,读 `@Transactional(readOnly = true)`
- 实体基类: `@MappedSuperclass` `@EntityListeners(AuditingEntityListener.class)`
### 4.2 查询功能详解
不用:
#### 4.2.1 简单查询
支持基于 ID 的简单查询,通过 detail 接口实现。
- 不用 `@Valid`/`@NotNull` 等校验注解
- 不用 Swagger/OpenAPI 注解
- 不用 `@Autowired` 字段注入
- 不写代码注释(`//`),除非必要
#### 4.2.2 条件查询
支持基于多种条件的复杂查询,通过 list 接口实现,支持以下查询条件:
- nullEqual: 字段值为null的条件
- notNullEqual: 字段值不为null的条件
- empty: 字段值为空的条件
- notEmpty: 字段值不为空的条件
- equal: 字段值相等的条件
- notEqual: 字段值不相等的条件
- like: 字段值模糊匹配的条件
- notLike: 字段值不模糊匹配的条件
- great: 字段值大于的条件
- less: 字段值小于的条件
- greatEqual: 字段值大于等于的条件
- lessEqual: 字段值小于等于的条件
- in: 字段值在指定范围内的条件
- notIn: 字段值不在指定范围内的条件
- between: 字段值在指定区间内的条件
- notBetween: 字段值不在指定区间内的条件
### 异常
#### 4.2.3 分页查询
支持分页查询功能,通过 page 配置实现分页参数设置。
继承 `RuntimeException`,构造器用 `String.formatted()`。查无数据用 `Optional.orElseThrow(() -> new IdNotFoundException(id))`。无全局 `@ControllerAdvice`
#### 4.2.4 排序查询
支持排序查询功能,通过 sort 配置实现排序参数设置。
### 依赖管理
### 4.3 数据实体设计
版本集中在根 POM `<properties>` + `<dependencyManagement>`,子模块不声明版本。
全局继承: `lombok` / `jspecify:1.0.0`
核心: `spring-boot-starter-data-jpa` / `fenix-spring-boot-starter:4.0.0` / `querydsl-jpa:7.1` / `mapstruct:1.6.3`
database 模块注解处理器: lombok → hibernate-jpamodelgen → querydsl-apt(jpa) → jakarta.persistence-api → mapstruct-processor
#### 4.3.1 基础实体类
定义通用的实体基类,包括 IdOnlyEntity 和 SimpleEntity。
### 通用原则
#### 4.3.2 审计字段
包含创建时间(createdTime)和修改时间(modifiedTime)等审计字段,通过 Spring Data JPA 的审计功能自动维护。
- 文档/注释/commit 用中文,代码标识符用英文
- 不引入新依赖前先复用已有组件,优先 JDK 和 Spring 内置能力
- 构造器注入,不使用 `@Autowired` 字段注入
#### 4.3.3 实体关系
支持常见的实体关系映射,如一对一、一对多、多对多等。
### 构建
## 5. 使用指南
### 5.1 环境准备
#### 5.1.1 JDK安装
需要安装 JDK 17 或更高版本。
#### 5.1.2 Maven配置
需要配置 Maven 3.x 环境。
### 5.2 项目构建
#### 5.2.1 依赖管理
通过 Maven 管理项目依赖。
#### 5.2.2 编译打包
使用 `mvn clean package` 命令编译打包项目。
### 5.3 运行部署
#### 5.3.1 本地运行
可以使用 `mvn spring-boot:run` 命令运行项目。
#### 5.3.2 生产部署
可通过生成的 JAR 文件直接运行或部署到服务器,使用命令:
`java -jar target/spring-boot-service-template-1.0-SNAPSHOT.jar`
## 6. 开发规范
### 6.1 代码规范
遵循 Java 标准编码规范和 Spring Boot 最佳实践。
### 6.2 接口规范
RESTful API 设计规范,统一的响应格式。
### 6.3 注释规范
遵循标准的 JavaDoc 格式编写注释,详细说明类、方法、参数和返回值的含义。
### 6.4 异常处理
统一异常处理机制,提供友好的错误信息。
## 7. 项目集成指南
本章节详细介绍如何在现有项目中集成 Spring Boot Service Template 的能力。
### 7.1 集成方式
通过 Maven 依赖引入(推荐)
### 7.2 Maven 依赖引入方式
#### 7.2.1 添加依赖
在您的项目 pom.xml 文件中添加以下依赖:
```xml
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```bash
mvn clean package
```
#### 7.2.2 配置依赖管理
确保您的项目中包含以下依赖管理配置:
```xml
<dependencyManagement>
<dependencies>
<!-- spring boot 相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
#### 7.2.3 配置编译插件
确保您的项目中包含以下编译插件配置:
```xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</path>
<path>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>6.6.3.Final</version>
</path>
<path>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>7.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
```
### 7.4 创建业务模块
创建业务模块的步骤如下:
#### 7.4.1 创建实体类
创建实体类并继承 [SimpleEntity](src/main/java/com/lanyuanxiaoyao/service/template/entity/SimpleEntity.java)
```java
@Entity
@Table(name = "employee")
public class Employee extends SimpleEntity {
private String name;
private Integer age;
// 其他字段和方法
}
```
#### 7.4.2 创建仓储接口
创建仓储接口并继承 [SimpleRepository](src/main/java/com/lanyuanxiaoyao/service/template/repository/SimpleRepository.java)
```java
@Repository
public interface EmployeeRepository extends SimpleRepository<Employee> {
// 自定义查询方法
}
```
#### 7.4.3 创建服务类
创建服务类并继承 [SimpleServiceSupport](src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java)
```java
@Service
public class EmployeeService extends SimpleServiceSupport<Employee> {
public EmployeeService(EmployeeRepository repository) {
super(repository);
}
// 自定义业务方法
}
```
#### 7.4.4 创建控制器类
创建控制器类并继承 [SimpleControllerSupport](src/main/java/com/lanyuanxiaoyao/service/template/controller/SimpleControllerSupport.java)
```java
@RestController
@RequestMapping("employee")
public class EmployeeController extends SimpleControllerSupport<Employee, EmployeeSaveItem, EmployeeListItem, EmployeeDetailItem> {
public EmployeeController(EmployeeService service) {
super(service);
}
@Override
protected Function<EmployeeSaveItem, Employee> saveItemMapper() {
// 实现保存项转换逻辑
}
@Override
protected Function<Employee, EmployeeListItem> listItemMapper() {
// 实现列表项转换逻辑
}
@Override
protected Function<Employee, EmployeeDetailItem> detailItemMapper() {
// 实现详情项转换逻辑
}
}
```
#### 7.4.5 创建数据传输对象
创建用于数据传输的对象:
```java
// 保存项
public class EmployeeSaveItem {
private Long id;
private String name;
private Integer age;
// getter和setter方法
}
// 列表项
public class EmployeeListItem {
private Long id;
private String name;
private Integer age;
// getter和setter方法
}
// 详情项
public class EmployeeDetailItem {
private Long id;
private String name;
private Integer age;
private LocalDateTime createdTime;
private LocalDateTime modifiedTime;
// getter和setter方法
}
```
### 7.5 配置启用 JPA 审计
在您的主应用类上添加 [@EnableJpaAuditing](https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/config/EnableJpaAuditing.html) 注解以启用 JPA 审计功能:
```java
@SpringBootApplication
@EnableJpaAuditing
public class YourApplication {
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
}
```
### 7.6 配置 Fenix
在您的 application.yml 或 application.properties 文件中添加 Fenix 配置:
```yaml
fenix:
# 是否开启 Fenix
enabled: true
# SQL 执行后的输出格式console: 控制台彩色输出; html: 彩色 HTML 输出; text: 纯文本输出
output-format: console
```
### 7.7 测试集成效果
完成以上步骤后,您可以运行您的应用程序并测试以下功能:
1. 创建实体POST /employee/save
2. 查询列表GET /employee/list 或 POST /employee/list带条件
3. 查询详情GET /employee/detail/{id}
4. 删除实体GET /employee/remove/{id}
通过以上步骤,您就可以成功在现有项目中集成 Spring Boot Service Template 的能力,快速实现 CRUD 功能。

View File

@@ -0,0 +1,205 @@
# Database 模块开发指南
单表 CRUD → REST 接口快速实现框架。基于 JPA + Fenix + QueryDSL + MapStruct。
## 架构
```
Controller (REST) → Service (业务) → Repository (数据访问) → Entity (模型)
```
### 组件结构
| 包 | 组件 | 职责 |
|---|---|---|
| entity | IdOnlyEntity, SimpleEntity | 实体基类 |
| entity | SnowflakeId, SnowflakeIdGenerator | ID 生成 |
| entity | Query, GlobalResponse, Page | 查询/响应封装 |
| repository | SimpleRepository | 统一数据访问接口 |
| service | SaveService, QueryService, RemoveService | 功能接口 |
| service | SimpleService, SimpleServiceSupport | 组合接口与实现 |
| service | QueryParser | 查询条件解析 |
| controller | SaveController, QueryController, RemoveController | REST 接口 |
| controller | SimpleController, SimpleControllerSupport | 组合接口与实现 |
| helper | DatabaseHelper, SnowflakeHelper | 工具类 |
| exception | *Exception | 异常定义 |
## 核心设计
### 实体继承
```
IdOnlyEntity (id: Long, @SnowflakeId)
SimpleEntity (+ createdTime, modifiedTime)
业务实体 (@Entity)
```
**实现要点**
- `@MappedSuperclass` 标记基类
- `@SnowflakeId` 触发 `SnowflakeIdGenerator` 生成 ID
- `@CreatedDate/@LastModifiedDate` + `AuditingEntityListener` 自动填充时间
### Repository
```java
@NoRepositoryBean
public interface SimpleRepository<E> extends
FenixJpaRepository<E, Long>, // CRUD + Fenix
FenixJpaSpecificationExecutor<E>, // Specification
ListQueryByExampleExecutor<E>, // Example
ListQuerydslPredicateExecutor<E> {} // QueryDSL
```
**核心能力**
- `saveOrUpdateByNotNullProperties()` - 部分字段更新
- Specification - 动态条件查询
- QueryDSL - 类型安全查询
### Service 接口组合
```java
SaveService<ENTITY> // save(entity), save(entities)
QueryService<ENTITY> // detail(id), list(), list(query), count()
RemoveService<ENTITY> // remove(id), remove(ids)
SimpleService<ENTITY> extends SaveService, QueryService, RemoveService
```
**SimpleServiceSupport 实现**
- 保存Fenix `saveOrUpdateByNotNullProperties()`
- 查询JPA Criteria + Specification
- 删除:`deleteBatchByIds()`
- 扩展点:重写 `commonPredicates()` 添加全局过滤条件
### Controller 接口组合
```java
SaveController<SAVE_ITEM> // POST /save
QueryController<LIST_ITEM, DETAIL_ITEM> // GET/POST /list, GET /detail/{id}
RemoveController // GET /remove/{id}
SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM>
```
**SimpleControllerSupport 实现**
- 调用 Service 方法
- 通过 Mapper 转换 DTO ↔ Entity
- 封装 GlobalResponse
- 扩展点:实现 `saveItemMapper()`, `listItemMapper()`, `detailItemMapper()`
### 查询条件
**Query 结构**`Query(query: Queryable, sort: List<Sortable>, page: Pageable)`
**QueryParser**:抽象类定义解析接口,`JpaQueryParser` 转换为 JPA Predicate
**支持操作**
| 类别 | 操作 |
|---|---|
| 空值 | nullEqual, notNullEqual, empty, notEmpty |
| 相等 | equal, notEqual |
| 模糊 | like, notLike, contain, notContain |
| 前后缀 | startWith, endWith, notStartWith, notEndWith |
| 比较 | great, less, greatEqual, lessEqual |
| 区间 | between, notBetween |
| 集合 | inside, notInside |
**实现**JPA CriteriaBuilder 构建,支持多级字段路径(如 `user.name`自动类型转换枚举、LocalDateTime
### 响应格式
```java
GlobalResponse<T>(status, message, data)
// status: 0 成功, 500 失败
// 列表: data = ListItem(items, total)
// 详情: data = DetailItem(item)
```
## 技术细节
### 雪花算法
`SnowflakeHelper`64 位 Long1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号
`SnowflakeIdGenerator`:实现 `IdentifierGenerator`,持久化时调用 `SnowflakeHelper.next()`
### 部分更新
Fenix `saveOrUpdateByNotNullProperties()`
- 自动判断 INSERT/UPDATE
- 仅更新非 null 字段
### 命名策略
`PhysicalNamingStrategySnakeCaseImpl`camelCase → snake_case
### 注解处理器
执行顺序lombok → hibernate-jpamodelgen → querydsl-apt → mapstruct-processor
生成getter/setter、JPA 元模型_Entity、QueryDSL Q 类QEntity、MapStruct Mapper
## 工具类
### DatabaseHelper
```java
// 生成 DDL
generateDDL(entityPackages, ddlFilePath, dialect, jdbc, username, password, driver)
// 生成脚手架
generateBasicFiles(entityPackages, projectRootPackage, projectRootPath, override)
```
### SnowflakeHelper
```java
Long id = SnowflakeHelper.next();
```
## 扩展指南
### 软删除
1. 创建 `SoftDeleteEntity extends SimpleEntity`,添加 `deleted` 字段
2. 重写 `commonPredicates()` 返回 `deleted = false` 条件
3. 覆盖 `remove()` 改为更新 `deleted` 字段
### 多租户
1. 创建 `TenantEntity extends SimpleEntity`,添加 `tenantId` 字段
2. 重写 `commonPredicates()` 添加租户过滤
3. ThreadLocal 或 Spring Security 存储当前租户
### 审计字段
`SimpleEntity` 添加 `createdBy`, `modifiedBy`,配合 Spring Security 获取当前用户
### 自定义查询操作符
1. `Query.Queryable` 添加字段
2. `QueryParser` 添加抽象方法
3. `JpaQueryParser` 实现转换为 Predicate
## 测试
- H2 内存数据库
- `@DataJpaTest` 测试 Repository
- `@WebMvcTest` 测试 Controller
- 测试用例:`src/test/java/.../integration/`
## 依赖
核心spring-boot-starter-data-jpa, fenix-spring-boot-starter:4.0.0, querydsl-jpa:7.1, mapstruct:1.6.3
传递spring-boot-service-template-common
## 注意事项
- 事务:写操作 `@Transactional(rollbackFor = Throwable.class)`,读操作 `@Transactional(readOnly = true)`
- 异常:不使用全局处理器,直接抛出
- 性能:批量操作使用批量方法
- 线程安全GlobalResponse 用 record 保证不可变SnowflakeHelper 用原子变量

409
docs/database-usage.md Normal file
View File

@@ -0,0 +1,409 @@
# Database 模块使用指南
单表 CRUD → REST 接口快速实现框架。
## 使用模式
### Web 应用
引入 database 模块,创建 Entity → Repository → Service → Controller实现 REST 接口。
### 非 Web 应用
**无需引入 web 依赖**database 模块的 `spring-boot-starter-web` scope 为 `provided`)。
仅使用 Entity → Repository → Service直接注入 Service 使用:
```java
@SpringBootApplication
public class Application implements CommandLineRunner {
@Autowired
private EmployeeService service;
@Override
public void run(String... args) {
Employee emp = new Employee();
emp.setName("张三");
Long id = service.save(emp);
Employee found = service.detail(id);
List<Employee> list = service.list();
}
}
```
适用:批处理、定时任务、数据迁移、命令行应用、后台服务。
## 快速开始
### 1. 添加依赖
```xml
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template-database</artifactId>
</dependency>
```
### 2. 创建实体
```java
@Getter @Setter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@Table(name = "employee")
public class Employee extends SimpleEntity {
@Column(comment = "员工姓名", nullable = false)
private String name;
@Column(comment = "部门ID")
private Long departmentId;
@Column(comment = "邮箱")
private String email;
}
```
继承 `SimpleEntity` 自动获得 `id`(雪花算法)、`createdTime``modifiedTime`(自动填充)。
### 3. 创建 Repository
```java
@Repository
public interface EmployeeRepository extends SimpleRepository<Employee> {}
```
继承能力CRUD、分页、Specification、QueryDSL、Example。
### 4. 创建 Service
```java
@Slf4j
@Service
public class EmployeeService extends SimpleServiceSupport<Employee> {
public EmployeeService(EmployeeRepository repository) {
super(repository);
}
}
```
自动获得完整 CRUD 能力。
### 5. 创建 ControllerWeb 应用)
```java
@Slf4j
@RestController
@RequestMapping("employee")
public class EmployeeController
extends SimpleControllerSupport<Employee, EmployeeController.SaveItem, EmployeeController.ListItem, EmployeeController.DetailItem> {
private final EmployeeService service;
public EmployeeController(EmployeeService service) {
super(service);
this.service = service;
}
@Override
protected Function<SaveItem, Employee> saveItemMapper() {
return item -> {
Employee entity = new Employee();
entity.setId(item.id());
entity.setName(item.name());
entity.setDepartmentId(item.departmentId());
entity.setEmail(item.email());
return entity;
};
}
@Override
protected Function<Employee, ListItem> listItemMapper() {
return entity -> new ListItem(
entity.getId(), entity.getName(), entity.getDepartmentId(),
entity.getEmail(), entity.getCreatedTime()
);
}
@Override
protected Function<Employee, DetailItem> detailItemMapper() {
return entity -> new DetailItem(
entity.getId(), entity.getName(), entity.getDepartmentId(),
entity.getEmail(), entity.getCreatedTime(), entity.getModifiedTime()
);
}
public record SaveItem(Long id, String name, Long departmentId, String email) {}
public record ListItem(Long id, String name, Long departmentId, String email, LocalDateTime createdTime) {}
public record DetailItem(Long id, String name, Long departmentId, String email, LocalDateTime createdTime, LocalDateTime modifiedTime) {}
}
```
实现三个 Mapper`saveItemMapper()`, `listItemMapper()`, `detailItemMapper()`
## 代码生成
```java
DatabaseHelper.generateBasicFiles(
"com.example.entity", // 实体包
"com.example", // 项目根包
"./src/main/java/com/example", // 源码路径
false // 是否覆盖
);
```
生成 Repository、Service、Controller。
## API 接口Web 应用)
### POST /{entity}/save
保存/更新实体。
请求(新增):
```json
{"name": "张三", "departmentId": 1, "email": "zhangsan@example.com"}
```
请求(更新):
```json
{"id": 123456789, "name": "李四"}
```
响应:
```json
{"status": 0, "message": "OK", "data": 123456789}
```
特性:不传 id 为新增,传 id 为更新(仅更新非 null 字段)。
### GET/POST /{entity}/list
GET获取全部列表
POST条件查询
```json
{
"query": {
"equal": {"departmentId": 1},
"like": {"name": "%张%"},
"greatEqual": {"createdTime": "2026-01-01 00:00:00"}
},
"sort": [{"column": "createdTime", "direction": "DESC"}],
"page": {"index": 1, "size": 20}
}
```
响应:
```json
{"status": 0, "message": "OK", "data": {"items": [...], "total": 100}}
```
### GET /{entity}/detail/{id}
响应:
```json
{"status": 0, "message": "OK", "data": {"id": 123, "name": "张三", ...}}
```
ID 不存在返回 500。
### GET /{entity}/remove/{id}
响应:
```json
{"status": 0, "message": "OK", "data": null}
```
## 查询条件
### Query 结构
```java
Query(
query: Queryable, // 查询条件
sort: List<Sortable>, // 排序
page: Pageable // 分页
)
```
### 查询操作
| 操作 | 类型 | 示例 |
|---|---|---|
| equal | Map | `{"name": "张三"}` |
| notEqual | Map | `{"status": "DELETED"}` |
| like | Map | `{"name": "%张%"}` |
| contain | Map | `{"name": "张"}``%张%` |
| startWith | Map | `{"name": "张"}``张%` |
| endWith | Map | `{"name": "三"}``%三` |
| great/greatEqual | Map | `{"age": 18}` |
| less/lessEqual | Map | `{"age": 60}` |
| between | Map | `{"age": {"start": 18, "end": 60}}` |
| inside | Map | `{"id": [1, 2, 3]}` |
| notInside | Map | `{"status": ["DELETED"]}` |
| nullEqual | List | `["deletedAt"]` |
| notNullEqual | List | `["email"]` |
### 排序
```json
{"sort": [{"column": "createdTime", "direction": "DESC"}]}
```
direction: `ASC` 升序,`DESC` 降序
### 分页
```json
{"page": {"index": 1, "size": 20}}
```
index 从 1 开始,默认 `(1, 10)`,无排序默认 `createdTime DESC`
## 高级用法
### 扩展 Service
```java
@Service
public class EmployeeService extends SimpleServiceSupport<Employee> {
private final EmployeeRepository repository;
public EmployeeService(EmployeeRepository repository) {
super(repository);
this.repository = repository;
}
// 自定义方法
public List<Employee> findByDepartmentId(Long departmentId) {
return repository.findAll(
(root, query, builder) -> builder.equal(root.get("departmentId"), departmentId)
);
}
// 全局过滤条件
@Override
protected Predicate commonPredicates(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return builder.equal(root.get("deleted"), false); // 软删除过滤
}
}
```
### QueryDSL 查询
```java
public List<Employee> findActiveEmployees() {
QEmployee q = QEmployee.employee;
return repository.findAll(q.status.eq("ACTIVE").and(q.deleted.isFalse()));
}
```
### MapStruct Mapper
```java
@Mapper
public interface EmployeeMapper {
Employee toEntity(SaveItem item);
ListItem toListItem(Employee entity);
DetailItem toDetailItem(Employee entity);
}
// Controller 中使用
@Override
protected Function<SaveItem, Employee> saveItemMapper() {
return mapper::toEntity;
}
```
## 实体设计
### 字段类型
- ID: `Long`(雪花算法)
- 时间: `LocalDateTime`
- 金额: `BigDecimal`
- 枚举: Java enum存储为字符串
- 布尔: `Boolean`
### 关联关系
```java
@Entity
public class Order extends SimpleEntity {
@ManyToOne
@JoinColumn(name = "customer_id")
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
}
```
注意:谨慎使用 `@OneToMany`,可能导致 N+1 问题。
### 索引
```java
@Entity
@Table(name = "employee", indexes = {
@Index(name = "idx_department", columnList = "department_id")
})
public class Employee extends SimpleEntity { ... }
```
## 工具类
### DatabaseHelper
```java
// 生成 DDL
DatabaseHelper.generateDDL(
"com.example.entity", "./sql", MySQL8Dialect.class,
"jdbc:mysql://localhost:3306/test", "root", "password",
com.mysql.cj.jdbc.Driver.class
);
```
### SnowflakeHelper
```java
Long id = SnowflakeHelper.next();
```
## 常见问题
**Q: 非 Web 应用如何使用?**
A: 不引入 web 依赖,创建 Entity → Repository → Service直接注入 Service 使用。
**Q: 如何实现软删除?**
A: 添加 `deleted` 字段,重写 `commonPredicates()` 过滤,覆盖 `remove()` 改为更新。
**Q: 如何处理复杂查询?**
A: 使用 QueryDSL 或 Repository `@Query` 方法:
```java
@Query("SELECT e FROM Employee e WHERE e.departmentId = :deptId")
List<Employee> findByDepartment(@Param("deptId") Long deptId);
```
**Q: 如何批量插入?**
A: `service.save(entities)``repository.saveAll(entities)`
**Q: 查询条件支持关联对象吗?**
A: 支持,使用多级路径如 `"department.name"`
## 最佳实践
1. DTO 设计SaveItem 可修改字段ListItem 列表字段DetailItem 完整字段
2. 事务Service 方法已加事务,无需重复
3. 性能:列表查询避免关联对象,使用投影或 DTO
4. 代码生成:初期脚手架生成,后期手动调整
## 测试用例
`src/test/java/.../integration/`

14
openspec/config.yaml Normal file
View File

@@ -0,0 +1,14 @@
schema: spec-driven
context: |
- **优先阅读README.md**获取项目结构与开发规范所有代码风格、命名、注解、依赖、API等规范以README为准
- 新增代码优先复用已有组件、工具、依赖库,不引入新依赖
- 涉及模块结构、API、实体等变更时同步更新README.md
- Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明
- 禁止创建git操作task
- 积极使用subagents精心设计并行任务节省上下文空间加速任务执行
- 优先使用提问工具对用户进行提问
rules:
proposal:
- 仔细审查每一个过往spec判断是否存在Modified Capabilities

306
pom.xml
View File

@@ -2,160 +2,182 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modelVersion>4.0.0</modelVersion>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template</artifactId>
<version>1.0.0-SNAPSHOT</version>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template</artifactId>
<version>1.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<modules>
<module>spring-boot-service-template-common</module>
<module>spring-boot-service-template-database</module>
</modules>
<spring-boot.version>3.5.0</spring-boot.version>
<spring-cloud.version>2025.0.0</spring-cloud.version>
<hibernate.version>6.6.15.Final</hibernate.version>
<fenix.version>3.1.0</fenix.version>
<querydsl.version>7.0</querydsl.version>
<mapstruct.version>1.6.3</mapstruct.version>
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
</properties>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>4.0.0</spring-boot.version>
<spring-cloud.version>2025.1.0</spring-cloud.version>
<hibernate.version>7.1.8.Final</hibernate.version>
<fenix.version>4.0.0</fenix.version>
<querydsl.version>7.1</querydsl.version>
<mapstruct.version>1.6.3</mapstruct.version>
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
<datasource-decorator.version>2.0.0</datasource-decorator.version>
<hutool.version>5.8.43</hutool.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.blinkfox</groupId>
<artifactId>fenix-spring-boot-starter</artifactId>
<version>${fenix.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template-database</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-ant</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- spring boot 相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.gavlyukovskiy</groupId>
<artifactId>p6spy-spring-boot-starter</artifactId>
<version>${datasource-decorator.version}</version>
</dependency>
<dependency>
<groupId>com.github.gavlyukovskiy</groupId>
<artifactId>datasource-proxy-spring-boot-starter</artifactId>
<version>${datasource-decorator.version}</version>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
<!-- jpa -->
<dependency>
<groupId>com.blinkfox</groupId>
<artifactId>fenix-spring-boot-starter</artifactId>
<version>${fenix.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-ant</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>${hutool.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencyManagement>
<dependencies>
<!-- spring boot 相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</plugin>
<plugin>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
</plugin>
<plugin>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.2.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</path>
<path>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jpa</classifier>
</path>
<path>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.2.0</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Aquerydsl.entityAccessors=true</arg>
<arg>-Aquerydsl.createDefaultVariable=true</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>${releases.id}</id>
<name>${releases.name}</name>
<url>${releases.url}</url>
</repository>
<snapshotRepository>
<id>${snapshots.id}</id>
<name>${snapshots.name}</name>
<url>${snapshots.url}</url>
</snapshotRepository>
</distributionManagement>
<distributionManagement>
<repository>
<id>${releases.id}</id>
<name>${releases.name}</name>
<url>${releases.url}</url>
</repository>
<snapshotRepository>
<id>${snapshots.id}</id>
<name>${snapshots.name}</name>
<url>${snapshots.url}</url>
</snapshotRepository>
</distributionManagement>
</project>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template</artifactId>
<version>1.1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-service-template-common</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,178 @@
package com.lanyuanxiaoyao.service.template.common.helper;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
/**
* 对象工具类,提供常用的对象判断和处理方法
* <p>
* 该类封装了对象空值判断、空值检查、类型判断等常用功能,
* 用于简化代码中的对象处理逻辑。
* </p>
*/
public class ObjectHelper {
/**
* 判断对象是否为null
* <p>
* 简单的null值检查用于避免空指针异常。
* </p>
*
* @param obj 待检查的对象
* @return 如果对象为null返回true否则返回false
*/
public static boolean isNull(Object obj) {
return obj == null;
}
/**
* 判断对象是否不为null
* <p>
* isNull方法的反向操作语义更清晰。
* </p>
*
* @param obj 待判断的对象
* @return 如果对象不为null则返回true否则返回false
*/
public static boolean isNotNull(Object obj) {
return !isNull(obj);
}
/**
* 判断对象是否为空
* <p>
* 支持多种类型的空值判断,包括:
* <ul>
* <li>null值</li>
* <li>集合Collection</li>
* <li>映射Map</li>
* <li>字符序列String、StringBuilder等</li>
* <li>各种基本类型数组</li>
* <li>Optional对象</li>
* </ul>
* </p>
*
* @param obj 待判断的对象
* @return 如果对象为null或为空则返回true否则返回false
*/
public static boolean isEmpty(Object obj) {
if (isNull(obj)) return true;
if (obj instanceof Collection<?> collection) return collection.isEmpty();
if (obj instanceof Map<?, ?> map) return map.isEmpty();
if (obj instanceof CharSequence sequence) return sequence.isEmpty();
if (obj instanceof Object[] array) return array.length == 0;
if (obj instanceof byte[] array) return array.length == 0;
if (obj instanceof short[] array) return array.length == 0;
if (obj instanceof int[] array) return array.length == 0;
if (obj instanceof long[] array) return array.length == 0;
if (obj instanceof float[] array) return array.length == 0;
if (obj instanceof double[] array) return array.length == 0;
if (obj instanceof char[] array) return array.length == 0;
if (obj instanceof boolean[] array) return array.length == 0;
if (obj instanceof Optional<?> optional) return optional.isEmpty();
return false;
}
/**
* 判断对象是否不为空
* <p>
* isEmpty方法的反向操作语义更清晰。
* </p>
*
* @param obj 待判断的对象
* @return 如果对象不为空则返回true否则返回false
*/
public static boolean isNotEmpty(Object obj) {
return !isEmpty(obj);
}
/**
* 如果对象为null则返回默认值
* <p>
* 提供对象的null值保护避免空指针异常。
* </p>
*
* @param object 待检查的对象
* @param defaultValue 默认值当object为null时返回
* @return 如果object不为null则返回object否则返回defaultValue
*/
public static <T> T defaultIfNull(final T object, final T defaultValue) {
return isNull(object) ? defaultValue : object;
}
/**
* 判断给定的类是否可比较
* <p>
* 可比较的类型包括枚举、字符序列、实现了Comparable接口的类、基本数据类型。
* </p>
*
* @param clazz 待判断的类对象
* @return 如果类是枚举、字符序列、可比较接口的实现类或基本数据类型则返回true否则返回false
*/
public static boolean isComparable(Class<?> clazz) {
if (isNull(clazz)) return false;
return clazz.isEnum() ||
CharSequence.class.isAssignableFrom(clazz) ||
Comparable.class.isAssignableFrom(clazz) ||
clazz.isPrimitive();
}
/**
* 判断对象是否可比较
* <p>
* 通过对象的类来判断其是否可比较。
* </p>
*
* @param obj 待判断的对象
* @return 如果对象所属的类可比较则返回true否则返回false
*/
public static boolean isComparable(Object obj) {
if (isNull(obj)) return false;
return isComparable(obj.getClass());
}
/**
* 判断给定的类是否为集合类型
*
* @param clazz 待判断的类对象
* @return 如果类是Collection的子类则返回true否则返回false
*/
public static boolean isCollection(Class<?> clazz) {
if (isNull(clazz)) return false;
return Collection.class.isAssignableFrom(clazz);
}
/**
* 判断对象是否为集合类型
*
* @param obj 待判断的对象
* @return 如果对象是集合类型则返回true否则返回false
*/
public static boolean isCollection(Object obj) {
if (isNull(obj)) return false;
return isCollection(obj.getClass());
}
/**
* 判断给定的类是否为字符串类型
*
* @param clazz 待判断的类对象
* @return 如果类是String类型则返回true否则返回false
*/
public static boolean isString(Class<?> clazz) {
if (isNull(clazz)) return false;
return String.class.isAssignableFrom(clazz);
}
/**
* 判断对象是否为字符串类型
*
* @param obj 待判断的对象
* @return 如果对象是字符串类型则返回true否则返回false
*/
public static boolean isString(Object obj) {
if (isNull(obj)) return false;
return isString(obj.getClass());
}
}

View File

@@ -0,0 +1,263 @@
package com.lanyuanxiaoyao.service.template.common.helper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.Arguments;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("ObjectHelper Tests")
class ObjectHelperTest {
@Nested
@DisplayName("isNull/isNotNull")
class NullTests {
@Test
@DisplayName("isNull with null object returns true")
void isNull_withNull_returnsTrue() {
assertThat(ObjectHelper.isNull(null)).isTrue();
}
@Test
@DisplayName("isNull with non-null object returns false")
void isNull_withNonNull_returnsFalse() {
assertThat(ObjectHelper.isNull(new Object())).isFalse();
}
@Test
@DisplayName("isNotNull with null object returns false")
void isNotNull_withNull_returnsFalse() {
assertThat(ObjectHelper.isNotNull(null)).isFalse();
}
@Test
@DisplayName("isNotNull with non-null object returns true")
void isNotNull_withNonNull_returnsTrue() {
assertThat(ObjectHelper.isNotNull(new Object())).isTrue();
}
}
@Nested
@DisplayName("isEmpty/isNotEmpty")
class EmptyTests {
static Stream<Arguments> emptyObjects() {
return Stream.of(
Arguments.of("null", null, true),
Arguments.of("empty collection", Collections.emptyList(), true),
Arguments.of("non-empty collection", List.of("a"), false),
Arguments.of("empty map", Collections.emptyMap(), true),
Arguments.of("non-empty map", Map.of("key", "value"), false),
Arguments.of("empty string", "", true),
Arguments.of("non-empty string", "text", false),
Arguments.of("empty char sequence", new StringBuilder(), true),
Arguments.of("non-empty char sequence", new StringBuilder("text"), false),
Arguments.of("empty object array", new Object[]{}, true),
Arguments.of("non-empty object array", new Object[]{"a"}, false),
Arguments.of("empty byte array", new byte[]{}, true),
Arguments.of("non-empty byte array", new byte[]{1}, false),
Arguments.of("empty short array", new short[]{}, true),
Arguments.of("non-empty short array", new short[]{1}, false),
Arguments.of("empty int array", new int[]{}, true),
Arguments.of("non-empty int array", new int[]{1}, false),
Arguments.of("empty long array", new long[]{}, true),
Arguments.of("non-empty long array", new long[]{1L}, false),
Arguments.of("empty float array", new float[]{}, true),
Arguments.of("non-empty float array", new float[]{1.0f}, false),
Arguments.of("empty double array", new double[]{}, true),
Arguments.of("non-empty double array", new double[]{1.0}, false),
Arguments.of("empty char array", new char[]{}, true),
Arguments.of("non-empty char array", new char[]{'a'}, false),
Arguments.of("empty boolean array", new boolean[]{}, true),
Arguments.of("non-empty boolean array", new boolean[]{true}, false),
Arguments.of("empty optional", Optional.empty(), true),
Arguments.of("non-empty optional", Optional.of("value"), false),
Arguments.of("non-empty object", new Object(), false)
);
}
@ParameterizedTest(name = "{0}")
@MethodSource("emptyObjects")
@DisplayName("isEmpty returns correct result")
void isEmpty_withVariousObjects_returnsCorrectResult(String description, Object obj, boolean expected) {
assertThat(ObjectHelper.isEmpty(obj)).isEqualTo(expected);
}
@ParameterizedTest(name = "{0}")
@MethodSource("emptyObjects")
@DisplayName("isNotEmpty returns opposite of isEmpty")
void isNotEmpty_withVariousObjects_returnsCorrectResult(String description, Object obj, boolean expected) {
assertThat(ObjectHelper.isNotEmpty(obj)).isEqualTo(!expected);
}
}
@Nested
@DisplayName("defaultIfNull")
class DefaultIfNullTests {
@Test
@DisplayName("defaultIfNull with null returns default value")
void defaultIfNull_withNull_returnsDefault() {
assertThat(ObjectHelper.defaultIfNull(null, "default")).isEqualTo("default");
}
@Test
@DisplayName("defaultIfNull with non-null returns original value")
void defaultIfNull_withNonNull_returnsOriginal() {
assertThat(ObjectHelper.defaultIfNull("value", "default")).isEqualTo("value");
}
@Test
@DisplayName("defaultIfNull with null and null default returns null")
void defaultIfNull_withBothNull_returnsNull() {
String result = ObjectHelper.defaultIfNull(null, null);
assertThat(result).isNull();
}
}
@Nested
@DisplayName("isComparable")
class ComparableTests {
@Test
@DisplayName("isComparable with enum class returns true")
void isComparable_withEnum_returnsTrue() {
assertThat(ObjectHelper.isComparable(TestEnum.class)).isTrue();
}
@Test
@DisplayName("isComparable with String class returns true")
void isComparable_withString_returnsTrue() {
assertThat(ObjectHelper.isComparable(String.class)).isTrue();
}
@Test
@DisplayName("isComparable with Integer class returns true")
void isComparable_withInteger_returnsTrue() {
assertThat(ObjectHelper.isComparable(Integer.class)).isTrue();
}
@Test
@DisplayName("isComparable with primitive class returns true")
void isComparable_withPrimitive_returnsTrue() {
assertThat(ObjectHelper.isComparable(int.class)).isTrue();
}
@Test
@DisplayName("isComparable with non-comparable class returns false")
void isComparable_withNonComparable_returnsFalse() {
assertThat(ObjectHelper.isComparable(Object.class)).isFalse();
}
@Test
@DisplayName("isComparable with null class returns false")
void isComparable_withNull_returnsFalse() {
Class<?> nullClass = null;
assertThat(ObjectHelper.isComparable(nullClass)).isFalse();
}
@Test
@DisplayName("isComparable with enum object returns true")
void isComparable_withEnumObject_returnsTrue() {
assertThat(ObjectHelper.isComparable(TestEnum.VALUE)).isTrue();
}
@Test
@DisplayName("isComparable with null object returns false")
void isComparable_withNullObject_returnsFalse() {
Object nullObj = null;
assertThat(ObjectHelper.isComparable(nullObj)).isFalse();
}
}
@Nested
@DisplayName("isCollection")
class CollectionTests {
@Test
@DisplayName("isCollection with List class returns true")
void isCollection_withList_returnsTrue() {
assertThat(ObjectHelper.isCollection(List.class)).isTrue();
}
@Test
@DisplayName("isCollection with Set class returns true")
void isCollection_withSet_returnsTrue() {
assertThat(ObjectHelper.isCollection(Set.class)).isTrue();
}
@Test
@DisplayName("isCollection with non-collection class returns false")
void isCollection_withNonCollection_returnsFalse() {
assertThat(ObjectHelper.isCollection(String.class)).isFalse();
}
@Test
@DisplayName("isCollection with null class returns false")
void isCollection_withNull_returnsFalse() {
Class<?> nullClass = null;
assertThat(ObjectHelper.isCollection(nullClass)).isFalse();
}
@Test
@DisplayName("isCollection with ArrayList object returns true")
void isCollection_withArrayListObject_returnsTrue() {
assertThat(ObjectHelper.isCollection(new ArrayList<>())).isTrue();
}
@Test
@DisplayName("isCollection with null object returns false")
void isCollection_withNullObject_returnsFalse() {
Object nullObj = null;
assertThat(ObjectHelper.isCollection(nullObj)).isFalse();
}
}
@Nested
@DisplayName("isString")
class StringTests {
@Test
@DisplayName("isString with String class returns true")
void isString_withStringClass_returnsTrue() {
assertThat(ObjectHelper.isString(String.class)).isTrue();
}
@Test
@DisplayName("isString with non-string class returns false")
void isString_withNonStringClass_returnsFalse() {
assertThat(ObjectHelper.isString(Integer.class)).isFalse();
}
@Test
@DisplayName("isString with null class returns false")
void isString_withNullClass_returnsFalse() {
Class<?> nullClass = null;
assertThat(ObjectHelper.isString(nullClass)).isFalse();
}
@Test
@DisplayName("isString with string object returns true")
void isString_withStringObject_returnsTrue() {
assertThat(ObjectHelper.isString("text")).isTrue();
}
@Test
@DisplayName("isString with null object returns false")
void isString_withNullObject_returnsFalse() {
Object nullObj = null;
assertThat(ObjectHelper.isString(nullObj)).isFalse();
}
}
private enum TestEnum {
VALUE
}
}

View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template</artifactId>
<version>1.1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-service-template-database</artifactId>
<dependencies>
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.blinkfox</groupId>
<artifactId>fenix-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-ant</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.gavlyukovskiy</groupId>
<artifactId>p6spy-spring-boot-starter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</path>
<path>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jpa</classifier>
</path>
<path>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.2.0</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Aquerydsl.entityAccessors=true</arg>
<arg>-Aquerydsl.createDefaultVariable=true</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,14 @@
package com.lanyuanxiaoyao.service.template.database.configuration;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QueryDSLConfiguration {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager manager) {
return new JPAQueryFactory(manager);
}
}

View File

@@ -0,0 +1,68 @@
package com.lanyuanxiaoyao.service.template.database.controller;
import com.lanyuanxiaoyao.service.template.database.entity.GlobalResponse;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
/**
* 查询控制器接口,用于定义统一的查询实体详情和列表的接口规范
* <p>
* 该接口提供了标准的查询功能,支持条件查询、分页查询和详情查询。
* 所有实现类应当遵循统一的请求响应格式。
* </p>
*
* <h3>查询条件说明</h3>
* <ul>
* <li><b>空值条件:</b> nullEqual、notNullEqual、empty、notEmpty</li>
* <li><b>相等条件:</b> equal、notEqual</li>
* <li><b>模糊匹配:</b> like、notLike、contain、notContain</li>
* <li><b>前后缀匹配:</b> startWith、notStartWith、endWith、notEndWith</li>
* <li><b>范围条件:</b> great、less、greatEqual、lessEqual</li>
* <li><b>集合条件:</b> inside、notInside</li>
* <li><b>区间条件:</b> between、notBetween</li>
* </ul>
*
* @param <LIST_ITEM> 列表查询结果的实体类型
* @param <DETAIL_ITEM> 详情查询结果的实体类型
*/
public interface QueryController<LIST_ITEM, DETAIL_ITEM> {
String LIST = "/list";
String DETAIL = "/detail/{id}";
/**
* 获取所有实体列表
* <p>
* 查询所有记录,不带任何过滤条件,返回分页格式的数据。
* 适用于获取全量数据的场景。
* </p>
*
* @return 返回包含实体列表的响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list() throws Exception;
/**
* 根据查询条件获取实体列表
* <p>
* 支持复杂的查询条件、排序和分页,返回符合条件的数据。
* 查询条件包括相等、模糊匹配、范围查询、集合查询等。
* </p>
*
* @param query 查询条件对象,包含过滤条件、排序规则和分页信息
* @return 返回符合条件的实体列表响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list(Query query) throws Exception;
/**
* 根据ID获取实体详情
* <p>
* 根据主键ID查询单条记录的详细信息。
* 适用于详情页面展示或数据编辑的场景。
* </p>
*
* @param id 实体主键ID
* @return 返回实体详情响应对象,格式:{status: 0, message: "OK", data: {item: 详情数据}}
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<DETAIL_ITEM> detail(Long id) throws Exception;
}

View File

@@ -0,0 +1,27 @@
package com.lanyuanxiaoyao.service.template.database.controller;
import com.lanyuanxiaoyao.service.template.database.entity.GlobalResponse;
/**
* 删除控制器接口,用于定义统一的删除实体对象的接口规范
* <p>
* 该接口提供了标准的删除功能通过主键ID删除单条记录。
* 所有实现类应当遵循统一的请求响应格式。
* </p>
*/
public interface RemoveController {
String REMOVE = "/remove/{id}";
/**
* 根据ID删除实体对象
* <p>
* 根据主键ID删除指定的记录执行成功后返回操作结果。
* 适用于单条记录删除的场景。
* </p>
*
* @param id 需要删除的实体主键ID
* @return 返回删除结果响应对象,格式:{status: 0, message: "OK", data: null}
* @throws Exception 删除过程中可能抛出的异常
*/
GlobalResponse<Object> remove(Long id) throws Exception;
}

View File

@@ -0,0 +1,29 @@
package com.lanyuanxiaoyao.service.template.database.controller;
import com.lanyuanxiaoyao.service.template.database.entity.GlobalResponse;
/**
* 保存控制器接口,用于定义统一的保存实体对象的接口规范
* <p>
* 该接口提供了标准的保存功能,支持新增和更新操作。
* 所有实现类应当遵循统一的请求响应格式。
* </p>
*
* @param <SAVE_ITEM> 保存操作的实体类型
*/
public interface SaveController<SAVE_ITEM> {
String SAVE = "/save";
/**
* 保存实体对象
* <p>
* 保存或更新实体对象,根据业务逻辑判断是新增还是更新操作。
* 返回保存后的实体ID便于前端获取操作结果。
* </p>
*
* @param item 需要保存的实体对象,包含完整的字段信息
* @return 返回保存后的实体ID响应对象格式{status: 0, message: "OK", data: 实体ID}
* @throws Exception 保存过程中可能抛出的异常
*/
GlobalResponse<Long> save(SAVE_ITEM item) throws Exception;
}

View File

@@ -0,0 +1,4 @@
package com.lanyuanxiaoyao.service.template.database.controller;
public interface SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> extends SaveController<SAVE_ITEM>, QueryController<LIST_ITEM, DETAIL_ITEM>, RemoveController {
}

View File

@@ -1,10 +1,10 @@
package com.lanyuanxiaoyao.service.template.controller;
package com.lanyuanxiaoyao.service.template.database.controller;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.helper.ObjectHelper;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import java.util.List;
import java.util.Map;
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
import com.lanyuanxiaoyao.service.template.database.entity.GlobalResponse;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.database.service.SimpleServiceSupport;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
@@ -20,78 +20,26 @@ import org.springframework.web.bind.annotation.RequestBody;
* 子类需要实现对应的Mapper函数来完成实体类与传输对象之间的转换
* </p>
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* // 保存实体
* POST /save
* {
* // 保存项的具体字段
* "name": "示例名称",
* "description": "示例描述"
* }
*
* // 查询列表无条件
* GET /list
*
* // 查询列表带条件
* POST /list
* {
* "query": {
* "equal": {
* "status": "ACTIVE"
* },
* "like": {
* "name": "关键字"
* }
* },
* "sort": [
* {
* "column": "createTime",
* "direction": "DESC"
* }
* ],
* "page": {
* "index": 0,
* "size": 10
* }
* }
*
* // 获取详情
* GET /detail/1
*
* // 删除实体
* GET /remove/1
* </pre>
* </p>
*
* <p>
* 支持的查询条件说明:
* <h3>设计特点</h3>
* <ul>
* <li>nullEqual: 指定字段值为null的条件列表</li>
* <li>notNullEqual: 指定字段值不为null的条件列表</li>
* <li>empty: 指定字段值为空的条件列表如空字符串空集合等</li>
* <li>notEmpty: 指定字段值不为空的条件列表</li>
* <li>equal: 指定字段值相等的条件映射字段名 -> </li>
* <li>notEqual: 指定字段值不相等的条件映射字段名 -> </li>
* <li>like: 指定字段模糊匹配的条件映射字段名 -> 匹配值</li>
* <li>notLike: 指定字段不模糊匹配的条件映射字段名 -> 匹配值</li>
* <li>great: 指定字段大于条件的映射字段名 -> </li>
* <li>less: 指定字段小于条件的映射字段名 -> </li>
* <li>greatEqual: 指定字段大于等于条件的映射字段名 -> </li>
* <li>lessEqual: 指定字段小于等于条件的映射字段名 -> </li>
* <li>in: 指定字段值在指定范围内的条件映射字段名 -> 值列表</li>
* <li>notIn: 指定字段值不在指定范围内的条件映射字段名 -> 值列表</li>
* <li>between: 指定字段值在指定区间内的条件映射字段名 -> 区间范围</li>
* <li>notBetween: 指定字段值不在指定区间内的条件映射字段名 -> 区间范围</li>
* <li>泛型设计支持任意实体类型和数据转换</li>
* <li>统一的异常处理和事务管理</li>
* <li>支持条件查询分页查询和详情查询</li>
* <li>提供抽象的Mapper方法便于子类实现数据转换逻辑</li>
* </ul>
*
* <h3>使用说明</h3>
* <p>子类需要实现以下抽象方法</p>
* <ul>
* <li>saveItemMapper(): 保存项到实体的转换函数</li>
* <li>listItemMapper(): 实体到列表项的转换函数</li>
* <li>detailItemMapper(): 实体到详情项的转换函数</li>
* </ul>
* </p>
*
* @param <ENTITY> 实体类型必须继承SimpleEntity
* @param <SAVE_ITEM> 保存项类型
* @param <LIST_ITEM> 列表项类型
* @param <DETAIL_ITEM> 详情项类型
* @author lanyuanxiaoyao
*/
@Slf4j
public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> implements SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> {
@@ -108,9 +56,13 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 保存实体对象
* <p>
* 将保存项转换为实体对象后保存返回保存后的实体ID
* 支持新增和更新操作通过事务保证数据一致性
* </p>
*
* @param item 需要保存的项
* @return GlobalResponse<Long> 返回保存后的实体ID
* @return 返回保存后的实体ID响应对象格式{status: 0, message: "OK", data: 实体ID}
* @throws Exception 保存过程中可能抛出的异常
*/
@Transactional(rollbackFor = Throwable.class)
@@ -123,17 +75,21 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 获取所有实体列表
* <p>
* 查询所有记录不带任何过滤条件返回分页格式的数据
* 将实体对象转换为列表项对象后返回
* </p>
*
* @return GlobalCrudResponse<LIST_ITEM> 返回实体列表
* @return 返回实体列表响应对象格式{status: 0, message: "OK", data: {items: [...], total: total}}
* @throws Exception 查询过程中可能抛出的异常
*/
@Transactional(readOnly = true)
@GetMapping(LIST)
@Override
public GlobalResponse<Map<String, Object>> list() throws Exception {
public GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list() throws Exception {
var mapper = listItemMapper();
var result = service.list();
return GlobalResponse.responseCrudData(
return GlobalResponse.responseListData(
result
.stream()
.map(entity -> {
@@ -150,22 +106,27 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 根据查询条件获取实体列表
* <p>
* 支持复杂的查询条件排序和分页返回符合条件的数据
* 将实体对象转换为列表项对象后返回
* </p>
*
* @param query 查询条件对象
* @return GlobalCrudResponse<LIST_ITEM> 返回符合条件的实体列表
* @param query 查询条件对象包含过滤条件排序规则和分页信息
* @return 返回符合条件的实体列表响应对象格式{status: 0, message: "OK", data: {items: [...], total: total}}
* @throws Exception 查询过程中可能抛出的异常
*/
@Transactional(readOnly = true)
@PostMapping(LIST)
@Override
public GlobalResponse<Map<String, Object>> list(@RequestBody Query query) throws Exception {
public GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list(@RequestBody Query query) throws Exception {
if (ObjectHelper.isNull(query)) {
return GlobalResponse.responseCrudData(List.of(), 0);
return GlobalResponse.responseListData();
}
var mapper = listItemMapper();
var result = service.list(query);
return GlobalResponse.responseCrudData(
result.get()
return GlobalResponse.responseListData(
result.items()
.stream()
.map(entity -> {
try {
return mapper.apply(entity);
@@ -174,15 +135,19 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
}
})
.toList(),
result.getTotalElements()
result.total()
);
}
/**
* 根据ID获取实体详情
* <p>
* 根据主键ID查询单条记录的详细信息转换为详情项对象后返回
* 如果记录不存在则抛出异常
* </p>
*
* @param id 实体ID
* @return GlobalResponse<DETAIL_ITEM> 返回实体详情
* @param id 实体主键ID
* @return 返回实体详情响应对象格式{status: 0, message: "OK", data: 详情数据}
* @throws Exception 查询过程中可能抛出的异常
*/
@Transactional(readOnly = true)
@@ -195,9 +160,13 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 根据ID删除实体对象
* <p>
* 根据主键ID删除指定的记录执行成功后返回成功响应
* 通过事务保证删除操作的一致性
* </p>
*
* @param id 需要删除的实体ID
* @return GlobalResponse<Object> 返回删除结果
* @param id 需要删除的实体主键ID
* @return 返回删除结果响应对象格式{status: 0, message: "OK", data: null}
* @throws Exception 删除过程中可能抛出的异常
*/
@Transactional(rollbackFor = Throwable.class)
@@ -210,6 +179,9 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 保存项映射器将保存项转换为实体对象
* <p>
* 子类需要实现此方法定义保存项到实体的转换逻辑
* </p>
*
* @return Function<SAVE_ITEM, ENTITY> 保存项到实体的转换函数
*/
@@ -217,6 +189,9 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 列表项映射器将实体对象转换为列表项
* <p>
* 子类需要实现此方法定义实体到列表项的转换逻辑
* </p>
*
* @return Function<ENTITY, LIST_ITEM> 实体到列表项的转换函数
*/
@@ -224,6 +199,9 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 详情项映射器将实体对象转换为详情项
* <p>
* 子类需要实现此方法定义实体到详情项的转换逻辑
* </p>
*
* @return Function<ENTITY, DETAIL_ITEM> 实体到详情项的转换函数
*/

View File

@@ -0,0 +1,359 @@
package com.lanyuanxiaoyao.service.template.database.entity;
import java.util.List;
import java.util.Map;
/**
* 全局统一API响应封装类
* <p>
* 该类用于统一封装RESTful API接口的响应结果提供标准化的响应格式。
* 通过状态码、消息和数据三个字段,清晰地表达请求的处理结果,便于前端统一处理。
* </p>
*
* <h3>设计特点</h3>
* <ul>
* <li>使用Java Record实现不可变线程安全</li>
* <li>泛型设计,支持任意类型的数据封装</li>
* <li>提供丰富的静态工厂方法,简化响应对象创建</li>
* <li>支持分页查询、详情查询等常见场景</li>
* </ul>
*
* <h3>响应格式示例</h3>
* <p><b>成功响应(无数据):</b></p>
* <pre>
* {
* "status": 0,
* "message": "OK",
* "data": null
* }
* </pre>
*
* <p><b>成功响应(带数据):</b></p>
* <pre>
* {
* "status": 0,
* "message": "操作成功",
* "data": {
* "id": 1,
* "name": "示例数据"
* }
* }
* </pre>
*
* <p><b>分页列表响应:</b></p>
* <pre>
* {
* "status": 0,
* "message": "OK",
* "data": {
* "items": [
* {"id": 1, "name": "数据1"},
* {"id": 2, "name": "数据2"}
* ],
* "total": 100
* }
* }
* </pre>
*
* <p><b>错误响应:</b></p>
* <pre>
* {
* "status": 500,
* "message": "系统异常,请稍后重试",
* "data": null
* }
* </pre>
*
* <h3>使用场景</h3>
* <ul>
* <li><b>RESTful API:</b> 作为所有API接口的标准响应格式</li>
* <li><b>前后端分离:</b> 前端可统一处理成功/失败逻辑</li>
* <li><b>微服务架构:</b> 服务间调用的标准化响应</li>
* <li><b>移动端接口:</b> 移动端APP的统一数据格式</li>
* </ul>
*
* @param <T> 响应数据的类型可以是任意对象、集合、Map或包装类
* @param status 响应状态码
* <ul>
* <li>0 - 成功</li>
* <li>500 - 服务器错误</li>
* <li>其他 - 业务自定义状态码</li>
* </ul>
* @param message 响应消息,对状态码的简短描述,如"OK"、"ERROR"或具体的错误信息
* @param data 响应数据具体的业务数据可以是任意类型。对于列表查询通常封装为包含items和total的Map对于详情查询直接返回对象或Map
*
* @see #responseSuccess()
* @see #responseError()
* @see #responseListData(Iterable, Long)
* @see #responseDetailData(Object)
*/
public record GlobalResponse<T>(Integer status, String message, T data) {
/**
* 成功状态码 - 表示请求处理成功
*/
private static final int SUCCESS_STATUS = 0;
/**
* 错误状态码 - 表示服务器内部错误或业务异常
*/
private static final int ERROR_STATUS = 500;
/**
* 成功默认消息 - 用于通用成功响应
*/
private static final String SUCCESS_MESSAGE = "OK";
/**
* 错误默认消息 - 用于通用错误响应
*/
private static final String ERROR_MESSAGE = "ERROR";
/**
* 返回默认错误响应
* <p>
* 使用默认错误消息"ERROR"状态码500数据为null。
* 适用于无法确定具体错误原因的通用异常场景。
* </p>
*
* @return 错误响应对象,格式:{status: 500, message: "ERROR", data: null}
*
* @see #responseError(String)
* @see #responseSuccess()
*/
public static GlobalResponse<Object> responseError() {
return responseError(ERROR_MESSAGE);
}
/**
* 返回指定错误消息的响应
* <p>
* 使用指定的错误消息状态码500数据为null。
* 适用于需要向客户端传递具体错误信息的场景。
* </p>
*
* @param message 错误消息内容,建议描述具体错误原因,便于前端展示和问题定位
* @return 错误响应对象,格式:{status: 500, message: "自定义错误信息", data: null}
*
* @see #responseError()
* @see #responseSuccess(String)
*/
public static GlobalResponse<Object> responseError(String message) {
return new GlobalResponse<>(ERROR_STATUS, message, null);
}
/**
* 返回默认成功响应
* <p>
* 使用默认成功消息"OK"状态码0数据为null。
* 适用于操作成功但不需要返回数据的场景。
* </p>
*
* @return 成功响应对象,格式:{status: 0, message: "OK", data: null}
*
* @see #responseSuccess(String)
* @see #responseSuccess(Object)
* @see #responseError()
*/
public static GlobalResponse<Object> responseSuccess() {
return responseSuccess(SUCCESS_MESSAGE);
}
/**
* 返回指定成功消息的响应
* <p>
* 使用指定的成功消息状态码0数据为null。
* 适用于需要向客户端返回自定义成功提示的场景。
* </p>
*
* @param message 成功消息内容,建议描述具体操作结果,便于用户理解
* @return 成功响应对象,格式:{status: 0, message: "自定义成功信息", data: null}
*
* @see #responseSuccess()
* @see #responseSuccess(Object)
* @see #responseSuccess(String, Object)
*/
public static GlobalResponse<Object> responseSuccess(String message) {
return responseSuccess(message, null);
}
/**
* 返回包含数据的成功响应
* <p>
* 使用默认成功消息"OK"状态码0包含指定数据。
* 适用于需要返回数据但不需要自定义消息的场景。
* </p>
*
* @param <E> 数据类型可以是任意Java对象
* @param data 业务数据可以是实体对象、Map、集合等
* @return 成功响应对象,格式:{status: 0, message: "OK", data: 业务数据}
*
* @see #responseSuccess(String, Object)
* @see #responseListData(Iterable, Long)
* @see #responseDetailData(Object)
*/
public static <E> GlobalResponse<E> responseSuccess(E data) {
return responseSuccess(SUCCESS_MESSAGE, data);
}
/**
* 返回包含指定消息和数据的成功响应
* <p>
* 使用指定的成功消息状态码0包含指定数据。
* 这是最完整的方法,适用于需要同时自定义消息和返回数据的场景。
* </p>
*
* @param <E> 数据类型
* @param message 成功消息内容,描述具体操作结果
* @param data 业务数据,可以是任意类型
* @return 成功响应对象,格式:{status: 0, message: "自定义消息", data: 业务数据}
*
* @see #responseSuccess()
* @see #responseSuccess(Object)
* @see #responseSuccess(String)
*/
public static <E> GlobalResponse<E> responseSuccess(String message, E data) {
return new GlobalResponse<>(SUCCESS_STATUS, message, data);
}
/**
* 返回Map类型数据的成功响应
* <p>
* 适用于需要返回结构化数据的场景,如分页查询结果、统计信息等。
* 内部使用responseSuccess方法封装。
* </p>
*
* @param data Map格式的业务数据键为String值为任意对象
* @return 成功响应对象,格式:{status: 0, message: "OK", data: Map数据}
*
* @see #responseMapData(String, Object)
* @see #responseSuccess(Object)
*/
public static GlobalResponse<Map<String, Object>> responseMapData(Map<String, Object> data) {
return responseSuccess(data);
}
/**
* 返回单个键值对的成功响应
* <p>
* 将单个键值对封装为Map后返回适用于返回单个配置项或简单结果的场景。
* </p>
*
* @param key 数据键名不能为null
* @param value 数据值,可以是任意对象
* @return 成功响应对象,格式:{status: 0, message: "OK", data: {key: value}}
*
* @see #responseMapData(Map)
* @see #responseSuccess(Object)
*/
public static GlobalResponse<Map<String, Object>> responseMapData(String key, Object value) {
return responseMapData(Map.of(key, value));
}
/**
* 返回空列表的成功响应
* <p>
* 适用于查询结果为空的场景返回空列表和总数为0。
* </p>
*
* @param <T> 数据项类型
* @return 成功响应对象,格式:{status: 0, message: "OK", data: {items: [], total: 0}}
*
* @see #responseListData(Iterable, Long)
* @see #responseListData(Iterable, Integer)
*/
public static <T> GlobalResponse<ListItem<T>> responseListData() {
return responseListData(List.of(), 0);
}
/**
* 返回CRUD列表数据的成功响应Integer类型总数
* <p>
* 适用于分页查询,将数据列表和总数封装为标准格式。
* 自动将Integer类型的总数转换为Long类型。
* </p>
*
* @param <T> 数据项类型
* @param data 数据列表可以是List、Set等Iterable实现
* @param total 总记录数Integer类型
* @return 成功响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
*
* @see #responseListData(Iterable, Long)
* @see #responseListData()
* @see #responseSuccess(Object)
*/
public static <T> GlobalResponse<ListItem<T>> responseListData(Iterable<T> data, Integer total) {
return responseListData(data, total.longValue());
}
/**
* 返回CRUD列表数据的成功响应Long类型总数
* <p>
* 适用于分页查询,将数据列表和总数封装为标准格式。
* 支持大数据量场景使用Long类型避免整数溢出。
* </p>
*
* @param <T> 数据项类型
* @param data 数据列表可以是List、Set等Iterable实现
* @param total 总记录数Long类型支持大数据量
* @return 成功响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
*
* @see #responseListData(Iterable, Integer)
* @see #responseListData()
* @see ListItem
* @see #responseSuccess(Object)
*/
public static <T> GlobalResponse<ListItem<T>> responseListData(Iterable<T> data, Long total) {
return responseSuccess(new ListItem<>(data, total));
}
/**
* 返回详情数据的成功响应
* <p>
* 适用于详情查询,将单条记录封装为标准格式。
* 便于前端统一处理详情数据的展示。
* </p>
*
* @param <T> 数据类型
* @param data 详情数据可以是实体对象、Map等
* @return 成功响应对象,格式:{status: 0, message: "OK", data: {item: 详情数据}}
*
* @see #responseSuccess(Object)
* @see DetailItem
*/
public static <T> GlobalResponse<DetailItem<T>> responseDetailData(T data) {
return responseSuccess(new DetailItem<>(data));
}
/**
* 列表数据封装类
* <p>
* 用于封装分页查询的结果,包含数据列表和总记录数。
* 便于前端进行分页控件的渲染和数据展示。
* </p>
*
* @param <T> 数据项类型
* @param items 数据列表,包含当前页的所有记录
* @param total 总记录数,用于计算总页数和显示分页信息
*
* @see #responseListData(Iterable, Long)
*/
public record ListItem<T>(Iterable<T> items, Long total) {
}
/**
* 详情数据封装类
* <p>
* 用于封装单条记录的查询结果,提供统一的详情数据结构。
* 便于前端统一处理详情页面的数据展示。
* </p>
*
* <p><b>注意:</b> item字段使用Object类型可以存储任意类型的详情数据。</p>
*
* @param <T> 数据类型(主要用于类型提示)
* @param item 单条记录数据可以是实体对象、Map、VO对象等
*
* @see #responseDetailData(Object)
*/
public record DetailItem<T>(T item) {
}
}

View File

@@ -1,6 +1,6 @@
package com.lanyuanxiaoyao.service.template.entity;
package com.lanyuanxiaoyao.service.template.database.entity;
import com.lanyuanxiaoyao.service.template.helper.SnowflakeId;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
@@ -8,7 +8,6 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
@@ -28,8 +27,6 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
* <li>@Comment: 为数据库字段添加注释</li>
* </ul>
* </p>
*
* @author lanyuanxiaoyao
*/
@Getter
@Setter
@@ -46,6 +43,6 @@ public class IdOnlyEntity {
*/
@Id
@SnowflakeId
@Comment("记录唯一标记")
@Column(comment = "记录唯一标记")
private Long id;
}

View File

@@ -0,0 +1,31 @@
package com.lanyuanxiaoyao.service.template.database.entity;
import java.util.List;
/**
* 分页数据封装类
* <p>
* 用于封装分页查询的结果,包含数据流和总记录数。
* 适用于需要流式处理大量数据的场景,同时提供总数用于分页计算。
* </p>
*
* <h3>使用场景</h3>
* <ul>
* <li>数据库分页查询结果封装</li>
* <li>大数据量流式处理</li>
* <li>分页控件的数据源</li>
* </ul>
*
* <h3>特点</h3>
* <ul>
* <li>使用Java Record实现不可变线程安全</li>
* <li>支持流式数据处理,内存效率高</li>
* <li>包含总记录数,便于分页计算</li>
* </ul>
*
* @param <ENTITY> 实体类型
* @param items 数据流,包含当前页的所有记录
* @param total 总记录数,用于计算总页数和显示分页信息
*/
public record Page<ENTITY>(List<ENTITY> items, long total) {
}

View File

@@ -0,0 +1,195 @@
package com.lanyuanxiaoyao.service.template.database.entity;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* 查询条件封装类,用于构建复杂的查询条件
* <p>
* 该类统一封装了查询条件、排序条件和分页条件,支持多种复杂的查询场景。
* 通过JSON格式传递查询参数后端自动解析并转换为数据库查询条件。
* </p>
*
* <p>前端传入的JSON格式示例:</p>
* <pre>
* {
* "query": {
* "equal": {
* "name": "张三"
* },
* "like": {
* "address": "%北京%"
* },
* "greatEqual": {
* "age": 18
* },
* "less": {
* "age": 60
* },
* "between": {
* "salary": {
* "start": 5000,
* "end": 10000
* }
* }
* },
* "sort": [
* {
* "column": "createTime",
* "direction": "DESC"
* }
* ],
* "page": {
* "index": 0,
* "size": 10
* }
* }
* </pre>
*
* <p>查询条件说明:</p>
* <ul>
* <li><b>nullEqual</b>: 字段值为null的条件列表</li>
* <li><b>notNullEqual</b>: 字段值不为null的条件列表</li>
* <li><b>empty</b>: 字段值为空的条件列表(如空字符串、空集合等)</li>
* <li><b>notEmpty</b>: 字段值不为空的条件列表</li>
* <li><b>equal</b>: 字段值相等的条件映射(字段名 -> 值)</li>
* <li><b>notEqual</b>: 字段值不相等的条件映射(字段名 -> 值)</li>
* <li><b>like</b>: 字段值模糊匹配的条件映射(字段名 -> 匹配值)</li>
* <li><b>notLike</b>: 字段值不模糊匹配的条件映射(字段名 -> 匹配值)</li>
* <li><b>contain</b>: 字段包含指定字符串的条件映射(字段名 -> 包含值)</li>
* <li><b>notContain</b>: 字段不包含指定字符串的条件映射(字段名 -> 不包含值)</li>
* <li><b>startWith</b>: 字段以指定字符串开头的条件映射(字段名 -> 开头值)</li>
* <li><b>notStartWith</b>: 字段不以指定字符串开头的条件映射(字段名 -> 不开头值)</li>
* <li><b>endWith</b>: 字段以指定字符串结尾的条件映射(字段名 -> 结尾值)</li>
* <li><b>notEndWith</b>: 字段不以指定字符串结尾的条件映射(字段名 -> 不结尾值)</li>
* <li><b>great</b>: 字段大于条件的映射(字段名 -> 值)</li>
* <li><b>less</b>: 字段小于条件的映射(字段名 -> 值)</li>
* <li><b>greatEqual</b>: 字段大于等于条件的映射(字段名 -> 值)</li>
* <li><b>lessEqual</b>: 字段小于等于条件的映射(字段名 -> 值)</li>
* <li><b>inside</b>: 字段值在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li><b>notInside</b>: 字段值不在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li><b>between</b>: 字段值在指定区间内的条件映射(字段名 -> 区间范围)</li>
* <li><b>notBetween</b>: 字段值不在指定区间内的条件映射(字段名 -> 区间范围)</li>
* </ul>
*
* @param query 查询条件对象,包含所有查询条件的封装
* @param sort 排序条件列表,支持多字段排序
* @param page 分页条件对象,指定页码和每页大小
*/
public record Query(
Queryable query,
List<Sortable> sort,
Pageable page
) {
/**
* 可查询条件类,封装各种查询条件
* <p>
* 该类包含了所有支持的查询条件类型,每个字段对应一种查询条件。
* 字段名即为JSON中的键名字段类型决定了查询条件的值类型。
* </p>
*
* @param nullEqual 字段值为null的条件列表列表中的每个元素都是一个字段名
* @param notNullEqual 字段值不为null的条件列表列表中的每个元素都是一个字段名
* @param empty 字段值为空的条件列表(如空字符串、空集合等),列表中的每个元素都是一个字段名
* @param notEmpty 字段值不为空的条件列表,列表中的每个元素都是一个字段名
* @param equal 字段值相等的条件映射,键为字段名,值为要相等的值
* @param notEqual 字段值不相等的条件映射,键为字段名,值为要不相等的值
* @param like 字段值模糊匹配的条件映射,键为字段名,值为模糊匹配的模式(支持%通配符)
* @param notLike 字段值不模糊匹配的条件映射,键为字段名,值为不匹配的模式
* @param contain 字段包含指定字符串的条件映射,键为字段名,值为要包含的字符串
* @param notContain 字段不包含指定字符串的条件映射,键为字段名,值为不包含的字符串
* @param startWith 字段以指定字符串开头的条件映射,键为字段名,值为开头字符串
* @param notStartWith 字段不以指定字符串开头的条件映射,键为字段名,值为不开头的字符串
* @param endWith 字段以指定字符串结尾的条件映射,键为字段名,值为结尾字符串
* @param notEndWith 字段不以指定字符串结尾的条件映射,键为字段名,值为不结尾的字符串
* @param great 字段大于条件的映射,键为字段名,值为比较的阈值
* @param less 字段小于条件的映射,键为字段名,值为比较的阈值
* @param greatEqual 字段大于等于条件的映射,键为字段名,值为比较的阈值
* @param lessEqual 字段小于等于条件的映射,键为字段名,值为比较的阈值
* @param inside 字段值在指定范围内的条件映射,键为字段名,值为允许的值列表
* @param notInside 字段值不在指定范围内的条件映射,键为字段名,值为不允许的值列表
* @param between 字段值在指定区间内的条件映射,键为字段名,值为区间范围对象
* @param notBetween 字段值不在指定区间内的条件映射,键为字段名,值为区间范围对象
*/
public record Queryable(
List<String> nullEqual,
List<String> notNullEqual,
List<String> empty,
List<String> notEmpty,
Map<String, ? extends Serializable> equal,
Map<String, ? extends Serializable> notEqual,
Map<String, String> like,
Map<String, String> notLike,
Map<String, String> contain,
Map<String, String> notContain,
Map<String, String> startWith,
Map<String, String> notStartWith,
Map<String, String> endWith,
Map<String, String> notEndWith,
Map<String, ? extends Serializable> great,
Map<String, ? extends Serializable> less,
Map<String, ? extends Serializable> greatEqual,
Map<String, ? extends Serializable> lessEqual,
Map<String, List<? extends Serializable>> inside,
Map<String, List<? extends Serializable>> notInside,
Map<String, Between> between,
Map<String, Between> notBetween
) {
/**
* 区间范围类,用于表示起始值和结束值
* <p>
* 主要用于 between 和 notBetween 查询条件,表示一个数值或时间的区间范围。
* </p>
*
* @param start 区间起始值(包含)
* @param end 区间结束值(包含)
*/
public record Between(
Object start,
Object end
) {
}
}
/**
* 排序条件类,用于指定排序字段和排序方向
*
* @param column 排序字段名,对应数据库表的列名或实体类的属性名
* @param direction 排序方向ASC表示升序DESC表示降序
*/
public record Sortable(
String column,
Direction direction
) {
/**
* 排序方向枚举
*/
public enum Direction {
/**
* 升序排列(从小到大)
*/
ASC,
/**
* 降序排列(从大到小)
*/
DESC,
}
}
/**
* 可分页条件类,用于指定分页参数
* <p>
* 页码从0开始计数即第一页的索引为0。
* </p>
*
* @param index 页码索引从0开始0表示第一页
* @param size 每页大小,即每页显示的记录数
*/
public record Pageable(
Integer index,
Integer size
) {
}
}

View File

@@ -1,5 +1,6 @@
package com.lanyuanxiaoyao.service.template.entity;
package com.lanyuanxiaoyao.service.template.database.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@@ -7,7 +8,6 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@@ -30,7 +30,6 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
* </ul>
* </p>
*
* @author lanyuanxiaoyao
*/
@Getter
@Setter
@@ -46,7 +45,7 @@ public class SimpleEntity extends IdOnlyEntity {
* </p>
*/
@CreatedDate
@Comment("记录创建时间")
@Column(comment = "记录创建时间")
private LocalDateTime createdTime;
/**
@@ -56,6 +55,6 @@ public class SimpleEntity extends IdOnlyEntity {
* </p>
*/
@LastModifiedDate
@Comment("记录更新时间")
@Column(comment = "记录更新时间")
private LocalDateTime modifiedTime;
}

View File

@@ -1,4 +1,4 @@
package com.lanyuanxiaoyao.service.template.helper;
package com.lanyuanxiaoyao.service.template.database.entity;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -10,4 +10,4 @@ import org.hibernate.annotations.IdGeneratorType;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface SnowflakeId {
}
}

View File

@@ -0,0 +1,20 @@
package com.lanyuanxiaoyao.service.template.database.entity;
import com.lanyuanxiaoyao.service.template.database.helper.SnowflakeHelper;
import java.io.Serializable;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
@Slf4j
public class SnowflakeIdGenerator implements IdentifierGenerator {
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) {
try {
return SnowflakeHelper.next();
} catch (Exception e) {
log.error("Generate snowflake id failed", e);
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,7 @@
package com.lanyuanxiaoyao.service.template.database.exception;
public class IdNotFoundException extends RuntimeException {
public IdNotFoundException(Long id) {
super("ID为 %d 的资源不存在".formatted(id));
}
}

View File

@@ -0,0 +1,7 @@
package com.lanyuanxiaoyao.service.template.database.exception;
public class NotCollectionException extends RuntimeException {
public NotCollectionException(String variable) {
super("变量 %s 不是集合".formatted(variable));
}
}

View File

@@ -0,0 +1,7 @@
package com.lanyuanxiaoyao.service.template.database.exception;
public class NotComparableException extends RuntimeException {
public NotComparableException(String variable) {
super("变量 %s 不能比较".formatted(variable));
}
}

View File

@@ -0,0 +1,7 @@
package com.lanyuanxiaoyao.service.template.database.exception;
public class NotStringException extends RuntimeException {
public NotStringException(String variable) {
super("变量 %s 不是字符串".formatted(variable));
}
}

View File

@@ -1,4 +1,4 @@
package com.lanyuanxiaoyao.service.template;
package com.lanyuanxiaoyao.service.template.database.helper;
import jakarta.persistence.Entity;
import java.io.IOException;
@@ -8,11 +8,11 @@ import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy;
import org.hibernate.boot.model.naming.PhysicalNamingStrategySnakeCaseImpl;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.schema.TargetType;
import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
import org.springframework.boot.hibernate.SpringImplicitNamingStrategy;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.ClassUtils;
@@ -38,10 +38,8 @@ import org.springframework.util.ClassUtils;
* );
* </pre>
* </p>
*
* @author lanyuanxiaoyao
*/
public class Helper {
public class DatabaseHelper {
public static void generateDDL(
Set<String> entityPackages,
String ddlFilePath,
@@ -54,7 +52,7 @@ public class Helper {
var metadataSources = new MetadataSources(
new StandardServiceRegistryBuilder()
.applySetting("hibernate.dialect", dialect.getName())
.applySetting("hibernate.physical_naming_strategy", CamelCaseToUnderscoresNamingStrategy.class.getName())
.applySetting("hibernate.physical_naming_strategy", PhysicalNamingStrategySnakeCaseImpl.class.getName())
.applySetting("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName())
.applySetting("hibernate.connection.url", jdbc)
.applySetting("hibernate.connection.username", username)
@@ -65,7 +63,7 @@ public class Helper {
for (String className : scanEntityPackage(entityPackages)) {
try {
var entityClass = ClassUtils.forName(className, Helper.class.getClassLoader());
var entityClass = ClassUtils.forName(className, DatabaseHelper.class.getClassLoader());
metadataSources.addAnnotatedClass(entityClass);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to load entity class: " + className, e);
@@ -106,7 +104,7 @@ public class Helper {
public static void generateBasicFiles(Set<String> entityPackages, String projectRootPackage, String projectRootPath, boolean override) throws IOException {
for (String className : scanEntityPackage(entityPackages)) {
try {
var entityClass = ClassUtils.forName(className, Helper.class.getClassLoader());
var entityClass = ClassUtils.forName(className, DatabaseHelper.class.getClassLoader());
var name = entityClass.getSimpleName();
// Repository
@@ -117,7 +115,7 @@ public class Helper {
package %s.repository;
import %s;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import com.lanyuanxiaoyao.service.template.database.repository.SimpleRepository;
import org.springframework.stereotype.Repository;
@Repository
@@ -136,7 +134,7 @@ public class Helper {
import %s;
import %s.repository.%sRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import com.lanyuanxiaoyao.service.template.database.service.SimpleServiceSupport;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -160,7 +158,7 @@ public class Helper {
import %s;
import %s.service.%sService;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import com.lanyuanxiaoyao.service.template.database.controller.SimpleControllerSupport;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -199,7 +197,7 @@ public class Helper {
}
}
""".formatted(projectRootPackage, className, projectRootPackage, name, camelConvert(name), name, name, name, name, name, name, name, name, name, name));
""".formatted(projectRootPackage, className, projectRootPackage, name, camelConvert(name), name, name, name, name, name, name, name, name, name, name, name));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to load entity class: " + className, e);
@@ -231,7 +229,7 @@ public class Helper {
// 2. 前一个字符是数字
// 3. 当前大写字母不是最后一个字符且下一个字符是小写处理连续大写字母如"XMLParser" -> "xml_parser"
char previousChar = camelCase.charAt(i - 1);
if (Character.isLowerCase(previousChar)
if (Character.isLowerCase(previousChar)
|| Character.isDigit(previousChar)
|| (i < camelCase.length() - 1 && Character.isLowerCase(camelCase.charAt(i + 1)))) {
result.append('_');

View File

@@ -0,0 +1,68 @@
package com.lanyuanxiaoyao.service.template.database.helper;
import java.time.Instant;
public class SnowflakeHelper {
/**
* 起始的时间戳
*/
private final static long START_TIMESTAMP = 1;
/**
* 序列号占用的位数
*/
private final static long SEQUENCE_BIT = 11;
/**
* 序列号最大值
*/
private final static long MAX_SEQUENCE_BIT = ~(-1 << SEQUENCE_BIT);
/**
* 时间戳值向左位移
*/
private final static long TIMESTAMP_OFFSET = SEQUENCE_BIT;
/**
* 序列号
*/
private static long sequence = 0;
/**
* 上一次时间戳
*/
private static long lastTimestamp = -1;
public static synchronized long next() {
long currentTimestamp = nowTimestamp();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock have moved backwards.");
}
if (currentTimestamp == lastTimestamp) {
// 相同毫秒内, 序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE_BIT;
// 同一毫秒的序列数已经达到最大
if (sequence == 0) {
currentTimestamp = nextTimestamp();
}
} else {
// 不同毫秒内, 序列号置为0
sequence = 0;
}
lastTimestamp = currentTimestamp;
return (currentTimestamp - START_TIMESTAMP) << TIMESTAMP_OFFSET | sequence;
}
private static long nextTimestamp() {
long milli = nowTimestamp();
while (milli <= lastTimestamp) {
milli = nowTimestamp();
}
return milli;
}
private static long nowTimestamp() {
return Instant.now().toEpochMilli();
}
}

View File

@@ -1,12 +1,10 @@
package com.lanyuanxiaoyao.service.template.repository;
package com.lanyuanxiaoyao.service.template.database.repository;
import com.blinkfox.fenix.jpa.FenixJpaRepository;
import com.blinkfox.fenix.specification.FenixJpaSpecificationExecutor;
import org.springframework.data.querydsl.ListQuerydslPredicateExecutor;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.query.ListQueryByExampleExecutor;
import org.springframework.data.repository.query.QueryByExampleExecutor;
/**
* 简单仓库接口整合多种数据访问功能
@@ -99,8 +97,6 @@ import org.springframework.data.repository.query.QueryByExampleExecutor;
* </p>
*
* @param <E> 实体类型
* @param <ID> 实体ID类型
* @author lanyuanxiaoyao
*/
@NoRepositoryBean
public interface SimpleRepository<E> extends FenixJpaRepository<E, Long>, FenixJpaSpecificationExecutor<E>, ListQueryByExampleExecutor<E>, ListQuerydslPredicateExecutor<E> {

View File

@@ -0,0 +1,128 @@
package com.lanyuanxiaoyao.service.template.database.service;
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class QueryParser<O> {
private final Query.Queryable queryable;
private final O container;
protected abstract void nullEqual(Query.Queryable queryable, O container);
protected abstract void notNullEqual(Query.Queryable queryable, O container);
protected abstract void empty(Query.Queryable queryable, O container);
protected abstract void notEmpty(Query.Queryable queryable, O container);
protected abstract void equal(Query.Queryable queryable, O container);
protected abstract void notEqual(Query.Queryable queryable, O container);
protected abstract void like(Query.Queryable queryable, O container);
protected abstract void notLike(Query.Queryable queryable, O container);
protected abstract void contain(Query.Queryable queryable, O container);
protected abstract void notContain(Query.Queryable queryable, O container);
protected abstract void startWith(Query.Queryable queryable, O container);
protected abstract void notStartWith(Query.Queryable queryable, O container);
protected abstract void endWith(Query.Queryable queryable, O container);
protected abstract void notEndWith(Query.Queryable queryable, O container);
protected abstract void great(Query.Queryable queryable, O container);
protected abstract void less(Query.Queryable queryable, O container);
protected abstract void greatEqual(Query.Queryable queryable, O container);
protected abstract void lessEqual(Query.Queryable queryable, O container);
protected abstract void inside(Query.Queryable queryable, O container);
protected abstract void notInside(Query.Queryable queryable, O container);
protected abstract void between(Query.Queryable queryable, O container);
protected abstract void notBetween(Query.Queryable queryable, O container);
public void build() {
if (ObjectHelper.isNull(queryable)) {
return;
}
if (ObjectHelper.isNotEmpty(queryable.nullEqual())) {
nullEqual(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.notNullEqual())) {
notNullEqual(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.empty())) {
empty(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.notEmpty())) {
notEmpty(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.equal())) {
equal(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.notEqual())) {
notEqual(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.like())) {
like(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.notLike())) {
notLike(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.contain())) {
contain(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.notContain())) {
notContain(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.startWith())) {
startWith(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.notStartWith())) {
notStartWith(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.endWith())) {
endWith(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.notEndWith())) {
notEndWith(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.great())) {
great(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.less())) {
less(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.greatEqual())) {
greatEqual(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.lessEqual())) {
lessEqual(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.inside())) {
inside(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.notInside())) {
notInside(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.between())) {
between(queryable, container);
}
if (ObjectHelper.isNotEmpty(queryable.notBetween())) {
notBetween(queryable, container);
}
}
}

View File

@@ -0,0 +1,92 @@
package com.lanyuanxiaoyao.service.template.database.service;
import com.lanyuanxiaoyao.service.template.database.entity.Page;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
import java.util.List;
import java.util.Set;
/**
* 查询服务接口,用于定义统一的查询实体详情和列表的服务规范
* <p>
* 该接口提供了标准的查询功能,支持详情查询、列表查询、分页查询和统计查询。
* 所有实现类应当遵循统一的查询逻辑和异常处理规范。
* </p>
*
* @param <ENTITY> 实体类型
*/
public interface QueryService<ENTITY> {
/**
* 根据ID获取实体详情
* <p>
* 查询单条记录的详细信息如果记录不存在返回null。
* </p>
*
* @param id 实体主键ID
* @return 实体详情如果不存在则返回null
* @throws Exception 查询过程中可能抛出的异常
*/
ENTITY detail(Long id) throws Exception;
/**
* 根据ID获取实体详情如果不存在则抛出异常
* <p>
* 查询单条记录的详细信息,如果记录不存在则抛出异常。
* 适用于需要确保记录存在的场景。
* </p>
*
* @param id 实体主键ID
* @return 实体详情
* @throws Exception 当记录不存在或查询失败时抛出异常
*/
ENTITY detailOrThrow(Long id) throws Exception;
/**
* 获取实体总数
* <p>
* 统计所有记录的数量,不带任何过滤条件。
* </p>
*
* @return 实体总数
* @throws Exception 查询过程中可能抛出的异常
*/
Long count() throws Exception;
/**
* 获取所有实体列表
* <p>
* 查询所有记录,不带任何过滤条件,返回完整列表。
* 适用于数据量较小或需要全量数据的场景。
* </p>
*
* @return 实体列表
* @throws Exception 查询过程中可能抛出的异常
*/
List<ENTITY> list() throws Exception;
/**
* 根据ID集合获取实体列表
* <p>
* 批量查询指定ID的记录返回对应的实体列表。
* 适用于需要批量获取特定记录的场景。
* </p>
*
* @param ids 实体ID集合
* @return 实体列表包含集合中ID对应的记录
* @throws Exception 查询过程中可能抛出的异常
*/
List<ENTITY> list(Set<Long> ids) throws Exception;
/**
* 根据查询条件获取分页实体列表
* <p>
* 支持复杂的查询条件、排序和分页,返回符合条件的数据。
* 这是最完整的查询方法,适用于大多数业务场景。
* </p>
*
* @param query 查询条件对象,包含过滤条件、排序规则和分页信息
* @return 分页实体列表,包含数据流和总记录数
* @throws Exception 查询过程中可能抛出的异常
*/
Page<ENTITY> list(Query query) throws Exception;
}

View File

@@ -0,0 +1,39 @@
package com.lanyuanxiaoyao.service.template.database.service;
import java.util.Set;
/**
* 删除服务接口,用于定义统一的删除实体对象的服务规范
* <p>
* 该接口提供了标准的删除功能,支持单条记录删除和批量删除。
* 所有实现类应当遵循统一的删除逻辑和异常处理规范。
* </p>
*
* @param <ENTITY> 实体类型
*/
public interface RemoveService<ENTITY> {
/**
* 根据ID删除实体对象
* <p>
* 删除指定ID的单条记录执行成功后无返回值。
* 适用于单条记录删除的场景。
* </p>
*
* @param id 需要删除的实体主键ID
* @throws Exception 删除过程中可能抛出的异常
*/
void remove(Long id) throws Exception;
/**
* 批量删除实体对象
* <p>
* 删除指定ID集合的多条记录执行成功后无返回值。
* 适用于批量删除的场景,提高删除效率。
* </p>
*
* @param ids 需要删除的实体ID集合
* @throws Exception 删除过程中可能抛出的异常
*/
void remove(Set<Long> ids) throws Exception;
}

View File

@@ -0,0 +1,38 @@
package com.lanyuanxiaoyao.service.template.database.service;
/**
* 保存服务接口,用于定义统一的保存实体对象的服务规范
* <p>
* 该接口提供了标准的保存功能,支持单条记录保存和批量保存。
* 所有实现类应当遵循统一的保存逻辑和异常处理规范。
* </p>
*
* @param <ENTITY> 实体类型
*/
public interface SaveService<ENTITY> {
/**
* 保存实体对象
* <p>
* 保存或更新单条实体记录,根据业务逻辑判断是新增还是更新操作。
* 返回保存后的实体ID便于后续操作。
* </p>
*
* @param entity 需要保存的实体对象,包含完整的字段信息
* @return 保存后的实体主键ID
* @throws Exception 保存过程中可能抛出的异常
*/
Long save(ENTITY entity) throws Exception;
/**
* 批量保存实体对象
* <p>
* 批量保存或更新多条实体记录,提高数据处理效率。
* 适用于批量数据导入或同步的场景。
* </p>
*
* @param entities 需要保存的实体对象集合
* @throws Exception 保存过程中可能抛出的异常
*/
void save(Iterable<ENTITY> entities) throws Exception;
}

View File

@@ -0,0 +1,4 @@
package com.lanyuanxiaoyao.service.template.database.service;
public interface SimpleService<ENTITY> extends SaveService<ENTITY>, QueryService<ENTITY>, RemoveService<ENTITY> {
}

View File

@@ -0,0 +1,540 @@
package com.lanyuanxiaoyao.service.template.database.service;
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
import com.lanyuanxiaoyao.service.template.database.entity.Page;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
import com.lanyuanxiaoyao.service.template.database.exception.IdNotFoundException;
import com.lanyuanxiaoyao.service.template.database.exception.NotCollectionException;
import com.lanyuanxiaoyao.service.template.database.exception.NotComparableException;
import com.lanyuanxiaoyao.service.template.database.exception.NotStringException;
import com.lanyuanxiaoyao.service.template.database.entity.IdOnlyEntity;
import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.database.repository.SimpleRepository;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.Named;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
/**
* 简单服务支持类,提供基础的业务逻辑实现
* <p>
* 该类实现了SimpleService接口提供实体的增删改查等基本操作。
* 通过继承此类,可以快速实现常见的业务逻辑功能,包括:
* <ul>
* <li>实体的保存和更新(支持部分字段更新)</li>
* <li>实体的条件查询和分页查询</li>
* <li>实体的详情查询(多种方式)</li>
* <li>实体的删除操作(支持批量删除)</li>
* <li>动态查询条件构建</li>
* </ul>
* </p>
*
* <h3>设计特点</h3>
* <ul>
* <li>泛型设计,支持任意实体类型</li>
* <li>事务管理,确保数据一致性</li>
* <li>动态查询条件,支持复杂的业务查询</li>
* <li>部分更新,只更新非空字段</li>
* <li>可扩展的查询条件构建</li>
* </ul>
*
* <h3>使用说明</h3>
* <p>子类可以重写以下方法:</p>
* <ul>
* <li>commonPredicates(): 添加自定义的查询条件</li>
* </ul>
*
* @param <ENTITY> 实体类型必须继承SimpleEntity
*/
@Slf4j
public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implements SimpleService<ENTITY> {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final int DEFAULT_PAGE_INDEX = 1;
private static final int DEFAULT_PAGE_SIZE = 10;
protected final SimpleRepository<ENTITY> repository;
/**
* 构造函数
*
* @param repository 简单仓库实例
*/
public SimpleServiceSupport(SimpleRepository<ENTITY> repository) {
this.repository = repository;
}
@Transactional(rollbackFor = Throwable.class)
@Override
public Long save(ENTITY entity) {
entity = repository.saveOrUpdateByNotNullProperties(entity);
return entity.getId();
}
@Transactional(rollbackFor = Throwable.class)
@Override
public void save(Iterable<ENTITY> entities) {
repository.saveOrUpdateAllByNotNullProperties(entities);
}
@Transactional(readOnly = true)
@Override
public Long count() {
return repository.count(this::commonPredicates);
}
@Transactional(readOnly = true)
@Override
public List<ENTITY> list() {
return repository.findAll(this::commonPredicates);
}
@Transactional(readOnly = true)
@Override
public List<ENTITY> list(Set<Long> ids) {
if (ObjectHelper.isEmpty(ids)) {
return List.of();
}
return repository.findAll(
(root, query, builder) -> {
var predicate = commonPredicates(root, query, builder);
var idsPredicate = builder.in(root.get(IdOnlyEntity.Fields.id)).value(ids);
return ObjectHelper.isNull(predicate)
? idsPredicate
: builder.and(predicate, idsPredicate);
}
);
}
protected Predicate commonPredicates(Root<ENTITY> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return null;
}
@Transactional(readOnly = true)
@Override
public Page<ENTITY> list(Query listQuery) {
var pageRequest = PageRequest.of(DEFAULT_PAGE_INDEX - 1, DEFAULT_PAGE_SIZE, Sort.by(SimpleEntity.Fields.createdTime).descending());
if (ObjectHelper.isNotNull(listQuery.page())) {
var index = Math.max(ObjectHelper.defaultIfNull(listQuery.page().index(), DEFAULT_PAGE_INDEX) - 1, 0);
var size = Math.max(ObjectHelper.defaultIfNull(listQuery.page().size(), DEFAULT_PAGE_SIZE), 1);
if (ObjectHelper.isNotEmpty(listQuery.sort())) {
pageRequest = PageRequest.of(index, size, Sort.by(
listQuery.sort()
.stream()
.map(sort -> new Sort.Order(Sort.Direction.fromString(sort.direction().name()), sort.column()))
.toList()
));
} else {
pageRequest = PageRequest.of(index, size, Sort.by(SimpleEntity.Fields.createdTime).descending());
}
}
var result = repository.findAll(
(root, query, builder) -> {
var predicate = commonPredicates(root, query, builder);
var predicates = new ArrayList<Predicate>();
new JpaQueryParser<>(listQuery.query(), predicates, root, query, builder).build();
var queryPredicate = predicates.size() == 1
? predicates.get(0)
: builder.and(predicates.toArray(Predicate[]::new));
return ObjectHelper.isNull(predicate)
? queryPredicate
: builder.and(predicate, queryPredicate);
},
pageRequest
);
return new Page<>(result.get().toList(), result.getTotalElements());
}
/**
* 根据ID获取实体详情Optional包装
* <p>
* 如果ID为空则返回空Optional否则根据ID查询实体。
* </p>
*
* @param id 实体ID
* @return 返回实体详情的Optional包装
*/
private Optional<ENTITY> detailOptional(Long id) {
if (ObjectHelper.isNull(id)) {
return Optional.empty();
}
return repository.findOne(
(root, query, builder) -> {
var predicate = commonPredicates(root, query, builder);
var idPredicate = builder.equal(root.get(IdOnlyEntity.Fields.id), id);
return ObjectHelper.isNull(predicate)
? idPredicate
: builder.and(predicate, idPredicate);
}
);
}
@Named("detail")
@Transactional(readOnly = true)
@Override
public ENTITY detail(Long id) {
return detailOptional(id).orElse(null);
}
@Named("detailOrThrow")
@Transactional(readOnly = true)
@Override
public ENTITY detailOrThrow(Long id) {
return detailOptional(id).orElseThrow(() -> new IdNotFoundException(id));
}
@Transactional(rollbackFor = Throwable.class)
@Override
public void remove(Long id) {
if (ObjectHelper.isNotNull(id)) {
repository.deleteBatchByIds(List.of(id));
}
}
@Transactional(rollbackFor = Throwable.class)
@Override
public void remove(Set<Long> ids) {
if (ObjectHelper.isNotEmpty(ids)) {
repository.deleteBatchByIds(ids);
}
}
@SuppressWarnings("unchecked")
private static final class JpaQueryParser<ENTITY> extends QueryParser<List<Predicate>> {
private final Root<ENTITY> root;
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private final CriteriaQuery<?> query;
private final CriteriaBuilder builder;
private JpaQueryParser(Query.Queryable queryable, List<Predicate> predicates, Root<ENTITY> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
super(queryable, predicates);
this.root = root;
this.query = query;
this.builder = builder;
}
/**
* 解析字段路径
* <p>
* 支持多级字段路径解析,使用"."分隔多级字段。
* 例如: "user.name" 表示实体的user属性的name字段。
* </p>
*
* @param root JPA Criteria查询根节点
* @param column 字段路径字符串
* @param <Y> 字段类型
* @return 返回字段路径对象
* @throws IllegalArgumentException 当字段路径为空时抛出
*/
private <Y> Path<Y> column(Root<ENTITY> root, String column) {
if (ObjectHelper.isEmpty(column)) {
throw new IllegalArgumentException("Column cannot be blank");
}
var columns = column.split("\\.");
Path<Y> path = root.get(columns[0]);
for (int i = 1; i < columns.length; i++) {
path = path.get(columns[i]);
}
return path;
}
/**
* 处理字段值
* <p>
* 对于枚举类型字段,将字符串值转换为对应的枚举值。
* 对于LocalDateTime类型字段将字符串转换为时间对象。
* 其他类型直接返回原值。
* </p>
*
* @param column 字段路径
* @param value 字段值
* @param <Y> 字段类型
* @return 处理后的字段值
* @throws IllegalArgumentException 当枚举类型字段的值不是字符串时抛出
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private <Y> Object value(Path<Y> column, Object value) {
if (ObjectHelper.isNull(value)) {
return null;
}
var javaType = column.getJavaType();
if (javaType.isEnum()) {
if (value instanceof String enumName) {
var enumType = (Class<Enum>) javaType;
return Enum.valueOf(enumType, enumName);
} else {
throw new IllegalArgumentException("枚举类型字段需要 String 类型的值");
}
} else if (javaType.isAssignableFrom(LocalDateTime.class)) {
return LocalDateTime.parse(String.valueOf(value), DATE_TIME_FORMATTER);
}
return value;
}
/**
* 检查字段类型是否可比较
*
* @param path 字段路径
* @param value 比较值
* @param column 字段名称
* @throws NotComparableException 当字段类型不可比较时抛出
*/
private void checkComparable(Path<?> path, Object value, String column) {
if (!ObjectHelper.isComparable(path.getJavaType()) || !ObjectHelper.isComparable(value)) {
throw new NotComparableException(column);
}
}
/**
* 检查区间值是否可比较
*
* @param path 字段路径
* @param value 区间对象
* @param column 字段名称
* @throws NotComparableException 当区间值不可比较时抛出
*/
private void checkComparable(Path<?> path, Query.Queryable.Between value, String column) {
checkComparable(path, value.start(), column);
checkComparable(path, value.end(), column);
}
/**
* 检查字段类型是否为集合
*
* @param path 字段路径
* @param column 字段名称
* @throws NotCollectionException 当字段类型不是集合时抛出
*/
private void checkCollection(Path<?> path, String column) {
if (!ObjectHelper.isCollection(path.getJavaType())) {
throw new NotCollectionException(column);
}
}
/**
* 检查值是否为集合
*
* @param value 值对象
* @param column 字段名称
* @throws NotCollectionException 当值不是集合时抛出
*/
private void checkCollection(Object value, String column) {
if (!ObjectHelper.isCollection(value)) {
throw new NotCollectionException(column);
}
}
/**
* 检查字段类型是否为字符串
*
* @param path 字段路径
* @param value 比较值
* @param column 字段名称
* @throws NotStringException 当字段类型不是字符串时抛出
*/
private void checkString(Path<?> path, Object value, String column) {
if (!ObjectHelper.isString(path.getJavaType()) || !ObjectHelper.isString(value)) {
throw new NotStringException(column);
}
}
@Override
protected void nullEqual(Query.Queryable queryable, List<Predicate> predicates) {
queryable.nullEqual().forEach(column -> predicates.add(builder.isNull(column(root, column))));
}
@Override
protected void notNullEqual(Query.Queryable queryable, List<Predicate> predicates) {
queryable.notNullEqual().forEach(column -> predicates.add(builder.isNotNull(column(root, column))));
}
@Override
protected void empty(Query.Queryable queryable, List<Predicate> predicates) {
queryable.empty().forEach(column -> {
var path = this.<Collection<Object>>column(root, column);
checkCollection(path, column);
predicates.add(builder.isEmpty(path));
});
}
@Override
protected void notEmpty(Query.Queryable queryable, List<Predicate> predicates) {
queryable.notEmpty().forEach(column -> {
var path = this.<Collection<Object>>column(root, column);
checkCollection(path, column);
predicates.add(builder.isNotEmpty(path));
});
}
@Override
protected void equal(Query.Queryable queryable, List<Predicate> predicates) {
queryable.equal().forEach((column, value) -> {
var path = column(root, column);
predicates.add(builder.equal(path, value(path, value)));
});
}
@Override
protected void notEqual(Query.Queryable queryable, List<Predicate> predicates) {
queryable.notEqual().forEach((column, value) -> {
var path = column(root, column);
predicates.add(builder.notEqual(path, value(path, value)));
});
}
@Override
protected void like(Query.Queryable queryable, List<Predicate> predicates) {
queryable.like().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.like(path, value));
});
}
@Override
protected void notLike(Query.Queryable queryable, List<Predicate> predicates) {
queryable.notLike().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.notLike(path, value));
});
}
@Override
protected void contain(Query.Queryable queryable, List<Predicate> predicates) {
queryable.contain().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.like(path, "%" + value + "%"));
});
}
@Override
protected void notContain(Query.Queryable queryable, List<Predicate> predicates) {
queryable.notContain().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.notLike(path, "%" + value + "%"));
});
}
@Override
protected void startWith(Query.Queryable queryable, List<Predicate> predicates) {
queryable.startWith().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.like(path, value + "%"));
});
}
@Override
protected void notStartWith(Query.Queryable queryable, List<Predicate> predicates) {
queryable.notStartWith().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.notLike(path, value + "%"));
});
}
@Override
protected void endWith(Query.Queryable queryable, List<Predicate> predicates) {
queryable.endWith().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.like(path, "%" + value));
});
}
@Override
protected void notEndWith(Query.Queryable queryable, List<Predicate> predicates) {
queryable.notEndWith().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.notLike(path, "%" + value));
});
}
@Override
protected void great(Query.Queryable queryable, List<Predicate> predicates) {
queryable.great().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.greaterThan(path, (Comparable<Object>) value(path, value)));
});
}
@Override
protected void less(Query.Queryable queryable, List<Predicate> predicates) {
queryable.less().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.lessThan(path, (Comparable<Object>) value(path, value)));
});
}
@Override
protected void greatEqual(Query.Queryable queryable, List<Predicate> predicates) {
queryable.greatEqual().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.greaterThanOrEqualTo(path, (Comparable<Object>) value(path, value)));
});
}
@Override
protected void lessEqual(Query.Queryable queryable, List<Predicate> predicates) {
queryable.lessEqual().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.lessThanOrEqualTo(path, (Comparable<Object>) value(path, value)));
});
}
@Override
protected void inside(Query.Queryable queryable, List<Predicate> predicates) {
queryable.inside()
.entrySet()
.stream()
.filter(entry -> ObjectHelper.isNotEmpty(entry.getValue()))
.forEach(entry -> predicates.add(builder.in(column(root, entry.getKey())).value(entry.getValue())));
}
@Override
protected void notInside(Query.Queryable queryable, List<Predicate> predicates) {
queryable.notInside()
.entrySet()
.stream()
.filter(entry -> ObjectHelper.isNotEmpty(entry.getValue()))
.forEach(entry -> predicates.add(builder.in(column(root, entry.getKey())).value(entry.getValue()).not()));
}
@Override
protected void between(Query.Queryable queryable, List<Predicate> predicates) {
queryable.between().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.between(path, (Comparable<Object>) value(path, value.start()), (Comparable<Object>) value(path, value.end())));
});
}
@Override
protected void notBetween(Query.Queryable queryable, List<Predicate> predicates) {
queryable.notBetween().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.between(path, (Comparable<Object>) value(path, value.start()), (Comparable<Object>) value(path, value.end())).not());
});
}
}
}

View File

@@ -0,0 +1,104 @@
package com.lanyuanxiaoyao.service.template.database.helper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("SnowflakeHelper Tests")
class SnowflakeHelperTest {
@Nested
@DisplayName("ID Generation")
class IdGenerationTests {
@Test
@DisplayName("next returns positive number")
void next_returnsPositiveNumber() {
long id = SnowflakeHelper.next();
assertThat(id).isPositive();
}
@Test
@DisplayName("next returns monotonically increasing IDs")
void next_returnsMonotonicallyIncreasingIds() {
long id1 = SnowflakeHelper.next();
long id2 = SnowflakeHelper.next();
long id3 = SnowflakeHelper.next();
assertThat(id2).isGreaterThan(id1);
assertThat(id3).isGreaterThan(id2);
}
}
@Nested
@DisplayName("Uniqueness")
class UniquenessTests {
@Test
@DisplayName("batch generation produces unique IDs")
void batchGeneration_producesUniqueIds() {
int count = 10000;
Set<Long> ids = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < count; i++) {
ids.add(SnowflakeHelper.next());
}
assertThat(ids).hasSize(count);
}
@Test
@DisplayName("concurrent generation produces unique IDs")
void concurrentGeneration_producesUniqueIds() throws InterruptedException {
int threadCount = 10;
int idsPerThread = 1000;
Set<Long> ids = Collections.newSetFromMap(new ConcurrentHashMap<>());
CountDownLatch latch = new CountDownLatch(threadCount);
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
for (int j = 0; j < idsPerThread; j++) {
ids.add(SnowflakeHelper.next());
}
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
assertThat(ids).hasSize(threadCount * idsPerThread);
}
}
@Nested
@DisplayName("Performance")
class PerformanceTests {
@RepeatedTest(5)
@DisplayName("generates 1000 IDs quickly")
void generatesIdsQuickly() {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
SnowflakeHelper.next();
}
long elapsed = System.currentTimeMillis() - start;
assertThat(elapsed).isLessThan(1000);
}
}
}

View File

@@ -0,0 +1,11 @@
package com.lanyuanxiaoyao.service.template.database.integration;
import com.blinkfox.fenix.EnableFenix;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableFenix
@EnableJpaAuditing
public class IntegrationTestConfiguration {
}

View File

@@ -0,0 +1,151 @@
package com.lanyuanxiaoyao.service.template.database.integration;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
public class QueryBuilder {
private List<String> nullEqual;
private List<String> notNullEqual;
private List<String> empty;
private List<String> notEmpty;
private Map<String, ? extends Serializable> equal;
private Map<String, ? extends Serializable> notEqual;
private Map<String, String> like;
private Map<String, String> notLike;
private Map<String, String> contain;
private Map<String, String> notContain;
private Map<String, String> startWith;
private Map<String, String> notStartWith;
private Map<String, String> endWith;
private Map<String, String> notEndWith;
private Map<String, ? extends Serializable> great;
private Map<String, ? extends Serializable> less;
private Map<String, ? extends Serializable> greatEqual;
private Map<String, ? extends Serializable> lessEqual;
private Map<String, List<? extends Serializable>> inside;
private Map<String, List<? extends Serializable>> notInside;
private Map<String, Query.Queryable.Between> between;
private Map<String, Query.Queryable.Between> notBetween;
public QueryBuilder nullEqual(List<String> nullEqual) {
this.nullEqual = nullEqual;
return this;
}
public QueryBuilder notNullEqual(List<String> notNullEqual) {
this.notNullEqual = notNullEqual;
return this;
}
public QueryBuilder empty(List<String> empty) {
this.empty = empty;
return this;
}
public QueryBuilder notEmpty(List<String> notEmpty) {
this.notEmpty = notEmpty;
return this;
}
public QueryBuilder equal(Map<String, ? extends Serializable> equal) {
this.equal = equal;
return this;
}
public QueryBuilder notEqual(Map<String, ? extends Serializable> notEqual) {
this.notEqual = notEqual;
return this;
}
public QueryBuilder like(Map<String, String> like) {
this.like = like;
return this;
}
public QueryBuilder notLike(Map<String, String> notLike) {
this.notLike = notLike;
return this;
}
public QueryBuilder contain(Map<String, String> contain) {
this.contain = contain;
return this;
}
public QueryBuilder notContain(Map<String, String> notContain) {
this.notContain = notContain;
return this;
}
public QueryBuilder startWith(Map<String, String> startWith) {
this.startWith = startWith;
return this;
}
public QueryBuilder notStartWith(Map<String, String> notStartWith) {
this.notStartWith = notStartWith;
return this;
}
public QueryBuilder endWith(Map<String, String> endWith) {
this.endWith = endWith;
return this;
}
public QueryBuilder notEndWith(Map<String, String> notEndWith) {
this.notEndWith = notEndWith;
return this;
}
public QueryBuilder great(Map<String, ? extends Serializable> great) {
this.great = great;
return this;
}
public QueryBuilder less(Map<String, ? extends Serializable> less) {
this.less = less;
return this;
}
public QueryBuilder greatEqual(Map<String, ? extends Serializable> greatEqual) {
this.greatEqual = greatEqual;
return this;
}
public QueryBuilder lessEqual(Map<String, ? extends Serializable> lessEqual) {
this.lessEqual = lessEqual;
return this;
}
public QueryBuilder inside(Map<String, List<? extends Serializable>> inside) {
this.inside = inside;
return this;
}
public QueryBuilder notInside(Map<String, List<? extends Serializable>> notInside) {
this.notInside = notInside;
return this;
}
public QueryBuilder between(Map<String, Query.Queryable.Between> between) {
this.between = between;
return this;
}
public QueryBuilder notBetween(Map<String, Query.Queryable.Between> notBetween) {
this.notBetween = notBetween;
return this;
}
public Query build() {
return new Query(new Query.Queryable(
nullEqual, notNullEqual, empty, notEmpty, equal, notEqual,
like, notLike, contain, notContain, startWith, notStartWith,
endWith, notEndWith, great, less, greatEqual, lessEqual,
inside, notInside, between, notBetween
), null, null);
}
}

View File

@@ -0,0 +1,208 @@
package com.lanyuanxiaoyao.service.template.database.integration;
import tools.jackson.databind.ObjectMapper;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
import com.lanyuanxiaoyao.service.template.database.integration.controller.TestController;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestStatus;
import com.lanyuanxiaoyao.service.template.database.integration.repository.TestRepository;
import com.lanyuanxiaoyao.service.template.database.integration.service.TestService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.AutoConfigureDataJpa;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(TestController.class)
@AutoConfigureDataJpa
@ActiveProfiles("test")
@Import({IntegrationTestConfiguration.class, TestService.class})
@DisplayName("SimpleControllerSupport Integration Tests")
class SimpleControllerSupportIntegrationTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
TestRepository repository;
@BeforeEach
void cleanUp() {
repository.deleteAll();
}
@Nested
@DisplayName("POST /save")
class SaveTests {
@Test
@DisplayName("save with valid data returns id")
void save_withValidData_returnsId() throws Exception {
var request = new TestController.SaveItem("张三", 25, TestStatus.ACTIVE, 5000.0, "java");
mockMvc.perform(post("/test/save")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0))
.andExpect(jsonPath("$.message").value("OK"))
.andExpect(jsonPath("$.data").isNumber());
}
@Test
@DisplayName("saveItemMapper converts correctly")
void saveItemMapper_convertsCorrectly() throws Exception {
var request = new TestController.SaveItem("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring");
String response = mockMvc.perform(post("/test/save")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andReturn().getResponse().getContentAsString();
Long id = objectMapper.readTree(response).get("data").asLong();
TestEntity saved = repository.findById(id).orElseThrow();
assertThat(saved.getName()).isEqualTo("张三");
assertThat(saved.getAge()).isEqualTo(25);
assertThat(saved.getStatus()).isEqualTo(TestStatus.ACTIVE);
assertThat(saved.getSalary()).isEqualTo(5000.0);
assertThat(saved.getTags()).isEqualTo("java,spring");
}
}
@Nested
@DisplayName("GET /list")
class ListTests {
@Test
@DisplayName("list returns all entities")
void list_returnsAllEntities() throws Exception {
repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
mockMvc.perform(get("/test/list"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0))
.andExpect(jsonPath("$.message").value("OK"))
.andExpect(jsonPath("$.data.items").isArray())
.andExpect(jsonPath("$.data.items.length()").value(2))
.andExpect(jsonPath("$.data.total").value(2));
}
@Test
@DisplayName("listItemMapper converts correctly")
void listItemMapper_convertsCorrectly() throws Exception {
var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
mockMvc.perform(get("/test/list"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.items[0].id").value(entity.getId()))
.andExpect(jsonPath("$.data.items[0].name").value("张三"))
.andExpect(jsonPath("$.data.items[0].age").value(25))
.andExpect(jsonPath("$.data.items[0].status").value("ACTIVE"));
}
}
@Nested
@DisplayName("POST /list")
class ListWithQueryTests {
@BeforeEach
void setUp() {
repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
}
@Test
@DisplayName("list with query filters correctly")
void listWithQuery_filtersCorrectly() throws Exception {
var query = new QueryBuilder().equal(java.util.Map.of("name", "张三")).build();
mockMvc.perform(post("/test/list")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(query)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.items.length()").value(1))
.andExpect(jsonPath("$.data.items[0].name").value("张三"));
}
@Test
@DisplayName("list with pagination returns correct page")
void listWithPagination_returnsCorrectPage() throws Exception {
var query = new Query(null, null, new Query.Pageable(1, 1));
mockMvc.perform(post("/test/list")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(query)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.items.length()").value(1))
.andExpect(jsonPath("$.data.total").value(2));
}
}
@Nested
@DisplayName("GET /detail/{id}")
class DetailTests {
@Test
@DisplayName("detail with existing id returns entity")
void detail_withExistingId_returnsEntity() throws Exception {
var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring"));
mockMvc.perform(get("/test/detail/{id}", entity.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0))
.andExpect(jsonPath("$.data.id").value(entity.getId()))
.andExpect(jsonPath("$.data.name").value("张三"))
.andExpect(jsonPath("$.data.age").value(25))
.andExpect(jsonPath("$.data.status").value("ACTIVE"))
.andExpect(jsonPath("$.data.salary").value(5000.0))
.andExpect(jsonPath("$.data.tags").value("java,spring"));
}
@Test
@DisplayName("detailItemMapper converts correctly")
void detailItemMapper_convertsCorrectly() throws Exception {
var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring"));
mockMvc.perform(get("/test/detail/{id}", entity.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.id").value(entity.getId()))
.andExpect(jsonPath("$.data.name").value("张三"))
.andExpect(jsonPath("$.data.tags").value("java,spring"));
}
}
@Nested
@DisplayName("GET /remove/{id}")
class RemoveTests {
@Test
@DisplayName("remove deletes entity")
void remove_deletesEntity() throws Exception {
var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
mockMvc.perform(get("/test/remove/{id}", entity.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0))
.andExpect(jsonPath("$.data").isEmpty());
assertThat(repository.findById(entity.getId())).isEmpty();
}
}
}

View File

@@ -0,0 +1,662 @@
package com.lanyuanxiaoyao.service.template.database.integration;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
import com.lanyuanxiaoyao.service.template.database.exception.IdNotFoundException;
import com.lanyuanxiaoyao.service.template.database.exception.NotCollectionException;
import com.lanyuanxiaoyao.service.template.database.exception.NotComparableException;
import com.lanyuanxiaoyao.service.template.database.exception.NotStringException;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestStatus;
import com.lanyuanxiaoyao.service.template.database.integration.repository.TestRepository;
import com.lanyuanxiaoyao.service.template.database.integration.service.TestService;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DataJpaTest
@ActiveProfiles("test")
@Import({IntegrationTestConfiguration.class, TestService.class})
@DisplayName("SimpleServiceSupport Integration Tests")
class SimpleServiceSupportIntegrationTest {
@Autowired
TestService service;
@Autowired
TestRepository repository;
@Autowired
EntityManager entityManager;
@BeforeEach
void cleanUp() {
repository.deleteAllInBatch();
}
@Nested
@DisplayName("CRUD Operations")
class CrudTests {
@Test
@DisplayName("save with new entity returns generated id")
void save_withNewEntity_returnsId() {
var entity = new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring");
Long id = service.save(entity);
assertThat(id).isNotNull().isPositive();
var saved = repository.findById(id).orElseThrow();
assertThat(saved.getName()).isEqualTo("张三");
assertThat(saved.getAge()).isEqualTo(25);
assertThat(saved.getStatus()).isEqualTo(TestStatus.ACTIVE);
}
@Test
@DisplayName("save with existing entity updates non-null fields")
void save_withExistingEntity_updatesFields() {
var entity = new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java");
Long id = service.save(entity);
var update = new TestEntity(null, 26, null, 6000.0, null);
update.setId(id);
service.save(update);
var updated = repository.findById(id).orElseThrow();
assertThat(updated.getName()).isEqualTo("张三");
assertThat(updated.getAge()).isEqualTo(26);
assertThat(updated.getStatus()).isEqualTo(TestStatus.ACTIVE);
assertThat(updated.getSalary()).isEqualTo(6000.0);
}
@Test
@DisplayName("save multiple entities saves all")
void save_multipleEntities_savesAll() {
var entities = List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python")
);
service.save(entities);
assertThat(repository.count()).isEqualTo(2);
}
@Test
@DisplayName("count returns correct total")
void count_returnsCorrectTotal() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python")
));
Long count = service.count();
assertThat(count).isEqualTo(2);
}
@Test
@DisplayName("list returns all entities")
void list_returnsAllEntities() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python")
));
List<TestEntity> result = service.list();
assertThat(result).hasSize(2);
}
@Test
@DisplayName("list with ids returns matching entities")
void list_withIds_returnsMatching() {
var e1 = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
var e2 = repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
repository.save(new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go"));
List<TestEntity> result = service.list(Set.of(e1.getId(), e2.getId()));
assertThat(result).hasSize(2)
.extracting(TestEntity::getName)
.containsExactlyInAnyOrder("张三", "李四");
}
@Test
@DisplayName("detail with existing id returns entity")
void detail_withExistingId_returnsEntity() {
var saved = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
TestEntity result = service.detail(saved.getId());
assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("张三");
}
@Test
@DisplayName("detail with non-existing id returns null")
void detail_withNonExistingId_returnsNull() {
TestEntity result = service.detail(999L);
assertThat(result).isNull();
}
@Test
@DisplayName("detailOrThrow with non-existing id throws exception")
void detailOrThrow_withNonExistingId_throwsException() {
assertThatThrownBy(() -> service.detailOrThrow(999L))
.isInstanceOf(IdNotFoundException.class);
}
@Test
@DisplayName("remove deletes entity")
void remove_deletesEntity() {
var saved = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
repository.flush();
entityManager.clear();
service.remove(saved.getId());
repository.flush();
var nativeQuery = entityManager.createNativeQuery("SELECT COUNT(*) FROM test_entity WHERE id = ?");
nativeQuery.setParameter(1, saved.getId());
var count = ((Number) nativeQuery.getSingleResult()).longValue();
assertThat(count).isZero();
}
@Test
@DisplayName("remove multiple entities deletes all")
void remove_multipleEntities_deletesAll() {
var e1 = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
var e2 = repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
service.remove(Set.of(e1.getId(), e2.getId()));
repository.flush();
assertThat(repository.count()).isZero();
}
}
@Nested
@DisplayName("Equality Conditions")
class EqualityConditionTests {
@BeforeEach
void setUp() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, null),
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
));
}
@Test
@DisplayName("equal condition filters correctly")
void equalCondition_filtersCorrectly() {
var query = new QueryBuilder().equal(Map.of("name", "张三")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(1);
assertThat(result.items().get(0).getName()).isEqualTo("张三");
}
@Test
@DisplayName("notEqual condition filters correctly")
void notEqualCondition_filtersCorrectly() {
var query = new QueryBuilder().notEqual(Map.of("name", "张三")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
assertThat(result.items()).noneMatch(e -> "张三".equals(e.getName()));
}
@Test
@DisplayName("nullEqual condition filters null values")
void nullEqualCondition_filtersNullValues() {
var query = new QueryBuilder().nullEqual(List.of("tags")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(1);
assertThat(result.items().get(0).getTags()).isNull();
}
@Test
@DisplayName("notNullEqual condition filters non-null values")
void notNullEqualCondition_filtersNonNullValues() {
var query = new QueryBuilder().notNullEqual(List.of("tags")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
assertThat(result.items()).allMatch(e -> e.getTags() != null);
}
}
@Nested
@DisplayName("String Conditions")
class StringConditionTests {
@BeforeEach
void setUp() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring"),
new TestEntity("李四三", 30, TestStatus.INACTIVE, 6000.0, "python"),
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
));
}
@Test
@DisplayName("like condition filters correctly")
void likeCondition_filtersCorrectly() {
var query = new QueryBuilder().like(Map.of("name", "%三%")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
}
@Test
@DisplayName("notLike condition filters correctly")
void notLikeCondition_filtersCorrectly() {
var query = new QueryBuilder().notLike(Map.of("name", "%三%")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(1);
assertThat(result.items().get(0).getName()).isEqualTo("王五");
}
@Test
@DisplayName("contain condition filters correctly")
void containCondition_filtersCorrectly() {
var query = new QueryBuilder().contain(Map.of("name", "")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
}
@Test
@DisplayName("notContain condition filters correctly")
void notContainCondition_filtersCorrectly() {
var query = new QueryBuilder().notContain(Map.of("name", "")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(1);
}
@Test
@DisplayName("startWith condition filters correctly")
void startWithCondition_filtersCorrectly() {
var query = new QueryBuilder().startWith(Map.of("name", "")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(1);
assertThat(result.items().get(0).getName()).isEqualTo("张三");
}
@Test
@DisplayName("notStartWith condition filters correctly")
void notStartWithCondition_filtersCorrectly() {
var query = new QueryBuilder().notStartWith(Map.of("name", "")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
assertThat(result.items()).noneMatch(e -> e.getName().startsWith(""));
}
@Test
@DisplayName("endWith condition filters correctly")
void endWithCondition_filtersCorrectly() {
var query = new QueryBuilder().endWith(Map.of("name", "")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
}
@Test
@DisplayName("notEndWith condition filters correctly")
void notEndWithCondition_filtersCorrectly() {
var query = new QueryBuilder().notEndWith(Map.of("name", "")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(1);
assertThat(result.items().get(0).getName()).isEqualTo("王五");
}
}
@Nested
@DisplayName("Comparison Conditions")
class ComparisonConditionTests {
@BeforeEach
void setUp() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 35, TestStatus.INACTIVE, 6000.0, "python"),
new TestEntity("王五", 45, TestStatus.DELETED, 7000.0, "go")
));
}
@Test
@DisplayName("great condition filters correctly")
void greatCondition_filtersCorrectly() {
var query = new QueryBuilder().great(Map.of("age", 30)).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
assertThat(result.items()).allMatch(e -> e.getAge() > 30);
}
@Test
@DisplayName("less condition filters correctly")
void lessCondition_filtersCorrectly() {
var query = new QueryBuilder().less(Map.of("age", 40)).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
assertThat(result.items()).allMatch(e -> e.getAge() < 40);
}
@Test
@DisplayName("greatEqual condition filters correctly")
void greatEqualCondition_filtersCorrectly() {
var query = new QueryBuilder().greatEqual(Map.of("age", 35)).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
assertThat(result.items()).allMatch(e -> e.getAge() >= 35);
}
@Test
@DisplayName("lessEqual condition filters correctly")
void lessEqualCondition_filtersCorrectly() {
var query = new QueryBuilder().lessEqual(Map.of("age", 35)).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
assertThat(result.items()).allMatch(e -> e.getAge() <= 35);
}
@Test
@DisplayName("between condition filters correctly")
void betweenCondition_filtersCorrectly() {
var query = new QueryBuilder().between(Map.of("age", new Query.Queryable.Between(25, 35))).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
assertThat(result.items()).allMatch(e -> e.getAge() >= 25 && e.getAge() <= 35);
}
@Test
@DisplayName("notBetween condition filters correctly")
void notBetweenCondition_filtersCorrectly() {
var query = new QueryBuilder().notBetween(Map.of("age", new Query.Queryable.Between(26, 44))).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
}
}
@Nested
@DisplayName("Collection Conditions")
class CollectionConditionTests {
@Test
@DisplayName("inside condition filters correctly")
void insideCondition_filtersCorrectly() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
));
var query = new QueryBuilder().inside(Map.of("age", List.of(25, 35))).build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
}
@Test
@DisplayName("notInside condition filters correctly")
void notInsideCondition_filtersCorrectly() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
));
var query = new QueryBuilder().notInside(Map.of("age", List.of(25, 35))).build();
var result = service.list(query);
assertThat(result.items()).hasSize(1);
assertThat(result.items().get(0).getAge()).isEqualTo(30);
}
@Test
@DisplayName("empty condition filters correctly")
void emptyCondition_filtersCorrectly() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
));
var query = new QueryBuilder().empty(List.of("tags")).build();
assertThatThrownBy(() -> service.list(query))
.isInstanceOf(NotCollectionException.class);
}
@Test
@DisplayName("notEmpty condition filters correctly")
void notEmptyCondition_filtersCorrectly() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
));
var query = new QueryBuilder().notEmpty(List.of("tags")).build();
assertThatThrownBy(() -> service.list(query))
.isInstanceOf(NotCollectionException.class);
}
}
@Nested
@DisplayName("Enum Condition")
class EnumConditionTests {
@BeforeEach
void setUp() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
));
}
@Test
@DisplayName("enum condition filters correctly")
void enumCondition_filtersCorrectly() {
var query = new QueryBuilder().equal(Map.of("status", "ACTIVE")).build();
var result = service.list(query);
assertThat(result.items()).hasSize(1);
assertThat(result.items().get(0).getStatus()).isEqualTo(TestStatus.ACTIVE);
}
}
@Nested
@DisplayName("Combined Conditions")
class CombinedConditionTests {
@BeforeEach
void setUp() {
repository.saveAll(List.of(
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
new TestEntity("王五", 35, TestStatus.ACTIVE, 7000.0, "go"),
new TestEntity("赵六", 40, TestStatus.ACTIVE, 8000.0, "rust")
));
}
@Test
@DisplayName("combined conditions filter correctly")
void combinedConditions_filterCorrectly() {
var query = new QueryBuilder()
.equal(Map.of("status", "ACTIVE"))
.great(Map.of("age", 30))
.build();
var result = service.list(query);
assertThat(result.items()).hasSize(2);
assertThat(result.items()).allMatch(e -> e.getStatus() == TestStatus.ACTIVE && e.getAge() > 30);
}
}
@Nested
@DisplayName("Pagination and Sorting")
class PaginationSortingTests {
@BeforeEach
void setUp() {
repository.deleteAllInBatch();
entityManager.flush();
entityManager.clear();
for (int i = 1; i <= 25; i++) {
repository.save(new TestEntity("用户" + i, 20 + i, TestStatus.ACTIVE, 5000.0 + i * 100, "tag" + i));
}
}
@Test
@DisplayName("default pagination returns first page with 10 items")
void defaultPagination_returnsFirstPage() {
var query = new Query(null, null, null);
var result = service.list(query);
assertThat(result.items()).hasSize(10);
assertThat(result.total()).isEqualTo(25);
}
@Test
@DisplayName("custom pagination returns correct page")
void customPagination_returnsCorrectPage() {
var query = new Query(null, null, new Query.Pageable(1, 5));
var result = service.list(query);
assertThat(result.items()).hasSize(5);
assertThat(result.total()).isEqualTo(25);
}
@Test
@DisplayName("custom sorting sorts correctly")
void customSorting_sortsCorrectly() {
var query = new Query(null, List.of(new Query.Sortable("age", Query.Sortable.Direction.ASC)), new Query.Pageable(1, 10));
var result = service.list(query);
assertThat(result.items()).hasSize(10);
assertThat(result.items().get(0).getAge()).isEqualTo(21);
assertThat(result.items()).isSortedAccordingTo((a, b) -> a.getAge().compareTo(b.getAge()));
}
@Test
@DisplayName("multiple sort fields sort correctly")
void multipleSortFields_sortCorrectly() {
repository.deleteAllInBatch();
entityManager.flush();
entityManager.clear();
repository.saveAll(List.of(
new TestEntity("A", 20, TestStatus.ACTIVE, 5000.0, null),
new TestEntity("B", 30, TestStatus.ACTIVE, 3000.0, null),
new TestEntity("C", 30, TestStatus.ACTIVE, 4000.0, null)
));
var query = new Query(null, List.of(
new Query.Sortable("age", Query.Sortable.Direction.ASC),
new Query.Sortable("salary", Query.Sortable.Direction.DESC)
), new Query.Pageable(1, 10));
var result = service.list(query);
assertThat(result.items()).hasSize(3);
assertThat(result.items().get(0).getName()).isEqualTo("A");
assertThat(result.items().get(1).getName()).isEqualTo("C");
assertThat(result.items().get(2).getName()).isEqualTo("B");
}
}
@Nested
@DisplayName("Exception Handling")
class ExceptionTests {
@Test
@DisplayName("like on non-string field throws NotStringException")
void likeOnNonStringField_throwsNotStringException() {
repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
var query = new QueryBuilder().like(Map.of("age", "%5%")).build();
assertThatThrownBy(() -> service.list(query))
.isInstanceOf(NotStringException.class);
}
@Test
@DisplayName("great on enum field with enum value throws exception")
void greatOnEnumField_withEnumValue_throwsException() {
repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
var query = new QueryBuilder().great(Map.of("status", TestStatus.ACTIVE)).build();
assertThatThrownBy(() -> service.list(query))
.isInstanceOf(org.springframework.dao.InvalidDataAccessApiUsageException.class)
.hasMessageContaining("枚举类型字段需要 String 类型的值");
}
@Test
@DisplayName("empty on non-collection field throws NotCollectionException")
void emptyOnNonCollectionField_throwsNotCollectionException() {
repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
var query = new QueryBuilder().empty(List.of("name")).build();
assertThatThrownBy(() -> service.list(query))
.isInstanceOf(NotCollectionException.class);
}
}
}

View File

@@ -0,0 +1,194 @@
package com.lanyuanxiaoyao.service.template.database.integration;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestSoftDeleteEntity;
import com.lanyuanxiaoyao.service.template.database.integration.repository.TestSoftDeleteRepository;
import com.lanyuanxiaoyao.service.template.database.integration.service.TestSoftDeleteService;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ActiveProfiles("test")
@Import({IntegrationTestConfiguration.class, TestSoftDeleteService.class})
@DisplayName("@SoftDelete Integration Tests")
class SoftDeleteIntegrationTest {
@Autowired
TestSoftDeleteService service;
@Autowired
TestSoftDeleteRepository repository;
@Autowired
EntityManager entityManager;
@BeforeEach
void cleanUp() {
repository.deleteAllInBatch();
}
@Nested
@DisplayName("Save and Delete Behavior")
class SaveDeleteTests {
@Test
@DisplayName("save entity sets deleted to false")
void saveEntity_setsDeletedFalse() {
var entity = new TestSoftDeleteEntity("张三", 25);
Long id = service.save(entity);
var saved = repository.findById(id).orElseThrow();
assertThat(saved.getName()).isEqualTo("张三");
var nativeQuery = entityManager.createNativeQuery("SELECT deleted FROM test_soft_delete_entity WHERE id = ?");
nativeQuery.setParameter(1, id);
var deleted = (Boolean) nativeQuery.getSingleResult();
assertThat(deleted).isFalse();
}
@Test
@DisplayName("remove sets deleted to true")
void remove_setsDeletedTrue() {
var entity = new TestSoftDeleteEntity("张三", 25);
Long id = service.save(entity);
repository.flush();
entityManager.clear();
service.remove(id);
repository.flush();
var nativeQuery = entityManager.createNativeQuery("SELECT deleted FROM test_soft_delete_entity WHERE id = ?");
nativeQuery.setParameter(1, id);
var deleted = (Boolean) nativeQuery.getSingleResult();
assertThat(deleted).isTrue();
}
@Test
@DisplayName("batch remove sets deleted to true")
void batchRemove_setsDeletedTrue() {
var e1 = new TestSoftDeleteEntity("张三", 25);
var e2 = new TestSoftDeleteEntity("李四", 30);
Long id1 = service.save(e1);
Long id2 = service.save(e2);
service.remove(Set.of(id1, id2));
var nativeQuery = entityManager.createNativeQuery("SELECT COUNT(*) FROM test_soft_delete_entity WHERE deleted = true");
var count = ((Number) nativeQuery.getSingleResult()).longValue();
assertThat(count).isEqualTo(2);
assertThat(repository.findAll()).isEmpty();
}
}
@Nested
@DisplayName("Query Behavior")
class QueryTests {
@Test
@DisplayName("list filters out deleted entities")
void list_filtersDeletedEntities() {
var e1 = repository.save(new TestSoftDeleteEntity("张三", 25));
repository.save(new TestSoftDeleteEntity("李四", 30));
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
.setParameter(1, e1.getId())
.executeUpdate();
entityManager.clear();
List<TestSoftDeleteEntity> result = service.list();
assertThat(result).hasSize(1);
assertThat(result.get(0).getName()).isEqualTo("李四");
}
@Test
@DisplayName("count excludes deleted entities")
void count_excludesDeletedEntities() {
var e1 = repository.save(new TestSoftDeleteEntity("张三", 25));
repository.save(new TestSoftDeleteEntity("李四", 30));
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
.setParameter(1, e1.getId())
.executeUpdate();
entityManager.clear();
Long count = service.count();
assertThat(count).isEqualTo(1);
}
@Test
@DisplayName("detail returns null for deleted entity")
void detail_returnsNullForDeletedEntity() {
var entity = repository.save(new TestSoftDeleteEntity("张三", 25));
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
.setParameter(1, entity.getId())
.executeUpdate();
entityManager.clear();
TestSoftDeleteEntity result = service.detail(entity.getId());
assertThat(result).isNull();
}
@Test
@DisplayName("query with conditions filters deleted entities")
void queryWithConditions_filtersDeletedEntities() {
var e1 = repository.save(new TestSoftDeleteEntity("张三", 25));
repository.save(new TestSoftDeleteEntity("李四", 30));
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
.setParameter(1, e1.getId())
.executeUpdate();
entityManager.clear();
var query = new QueryBuilder().equal(Map.of("name", "张三")).build();
var result = service.list(query);
assertThat(result.items()).isEmpty();
}
}
@Nested
@DisplayName("Restore Behavior")
class RestoreTests {
@Test
@DisplayName("restore deleted entity makes it visible again")
void restore_makesEntityVisible() {
var entity = repository.save(new TestSoftDeleteEntity("张三", 25));
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
.setParameter(1, entity.getId())
.executeUpdate();
entityManager.clear();
assertThat(service.list()).isEmpty();
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = false WHERE id = ?")
.setParameter(1, entity.getId())
.executeUpdate();
entityManager.clear();
List<TestSoftDeleteEntity> result = service.list();
assertThat(result).hasSize(1);
assertThat(result.get(0).getName()).isEqualTo("张三");
}
}
}

View File

@@ -0,0 +1,41 @@
package com.lanyuanxiaoyao.service.template.database.integration.controller;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestStatus;
import com.lanyuanxiaoyao.service.template.database.integration.service.TestService;
import com.lanyuanxiaoyao.service.template.database.controller.SimpleControllerSupport;
import java.util.function.Function;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController extends SimpleControllerSupport<TestEntity, TestController.SaveItem, TestController.ListItem, TestController.DetailItem> {
public TestController(TestService service) {
super(service);
}
@Override
protected Function<SaveItem, TestEntity> saveItemMapper() {
return item -> new TestEntity(item.name, item.age, item.status, item.salary, item.tags);
}
@Override
protected Function<TestEntity, ListItem> listItemMapper() {
return entity -> new ListItem(entity.getId(), entity.getName(), entity.getAge(), entity.getStatus());
}
@Override
protected Function<TestEntity, DetailItem> detailItemMapper() {
return entity -> new DetailItem(entity.getId(), entity.getName(), entity.getAge(), entity.getStatus(), entity.getSalary(), entity.getTags());
}
public record SaveItem(String name, Integer age, TestStatus status, Double salary, String tags) {
}
public record ListItem(Long id, String name, Integer age, TestStatus status) {
}
public record DetailItem(Long id, String name, Integer age, TestStatus status, Double salary, String tags) {
}
}

View File

@@ -0,0 +1,40 @@
package com.lanyuanxiaoyao.service.template.database.integration.controller;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestSoftDeleteEntity;
import com.lanyuanxiaoyao.service.template.database.integration.service.TestSoftDeleteService;
import com.lanyuanxiaoyao.service.template.database.controller.SimpleControllerSupport;
import java.util.function.Function;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test-soft-delete")
public class TestSoftDeleteController extends SimpleControllerSupport<TestSoftDeleteEntity, TestSoftDeleteController.SaveItem, TestSoftDeleteController.ListItem, TestSoftDeleteController.DetailItem> {
public TestSoftDeleteController(TestSoftDeleteService service) {
super(service);
}
@Override
protected Function<SaveItem, TestSoftDeleteEntity> saveItemMapper() {
return item -> new TestSoftDeleteEntity(item.name, item.age);
}
@Override
protected Function<TestSoftDeleteEntity, ListItem> listItemMapper() {
return entity -> new ListItem(entity.getId(), entity.getName(), entity.getAge());
}
@Override
protected Function<TestSoftDeleteEntity, DetailItem> detailItemMapper() {
return entity -> new DetailItem(entity.getId(), entity.getName(), entity.getAge());
}
public record SaveItem(String name, Integer age) {
}
public record ListItem(Long id, String name, Integer age) {
}
public record DetailItem(Long id, String name, Integer age) {
}
}

View File

@@ -0,0 +1,42 @@
package com.lanyuanxiaoyao.service.template.database.integration.entity;
import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.database.entity.SnowflakeId;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
@Entity
@Table(name = "test_entity")
@Getter
@Setter
@ToString
@FieldNameConstants
@NoArgsConstructor
@AllArgsConstructor
public class TestEntity extends SimpleEntity {
@SnowflakeId
@Column(comment = "名称")
private String name;
@Column(comment = "年龄")
private Integer age;
@Enumerated(EnumType.STRING)
@Column(comment = "状态")
private TestStatus status;
@Column(comment = "薪资")
private Double salary;
@Column(comment = "标签")
private String tags;
}

View File

@@ -0,0 +1,33 @@
package com.lanyuanxiaoyao.service.template.database.integration.entity;
import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.database.entity.SnowflakeId;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.SoftDelete;
import org.hibernate.annotations.SoftDeleteType;
@Entity
@Table(name = "test_soft_delete_entity")
@SoftDelete(strategy = SoftDeleteType.DELETED)
@Getter
@Setter
@ToString
@FieldNameConstants
@NoArgsConstructor
@AllArgsConstructor
public class TestSoftDeleteEntity extends SimpleEntity {
@SnowflakeId
@Column(comment = "名称")
private String name;
@Column(comment = "年龄")
private Integer age;
}

View File

@@ -0,0 +1,7 @@
package com.lanyuanxiaoyao.service.template.database.integration.entity;
public enum TestStatus {
ACTIVE,
INACTIVE,
DELETED
}

View File

@@ -0,0 +1,9 @@
package com.lanyuanxiaoyao.service.template.database.integration.repository;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
import com.lanyuanxiaoyao.service.template.database.repository.SimpleRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TestRepository extends SimpleRepository<TestEntity> {
}

View File

@@ -0,0 +1,9 @@
package com.lanyuanxiaoyao.service.template.database.integration.repository;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestSoftDeleteEntity;
import com.lanyuanxiaoyao.service.template.database.repository.SimpleRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TestSoftDeleteRepository extends SimpleRepository<TestSoftDeleteEntity> {
}

View File

@@ -0,0 +1,13 @@
package com.lanyuanxiaoyao.service.template.database.integration.service;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
import com.lanyuanxiaoyao.service.template.database.integration.repository.TestRepository;
import com.lanyuanxiaoyao.service.template.database.service.SimpleServiceSupport;
import org.springframework.stereotype.Service;
@Service
public class TestService extends SimpleServiceSupport<TestEntity> {
public TestService(TestRepository repository) {
super(repository);
}
}

View File

@@ -0,0 +1,13 @@
package com.lanyuanxiaoyao.service.template.database.integration.service;
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestSoftDeleteEntity;
import com.lanyuanxiaoyao.service.template.database.integration.repository.TestSoftDeleteRepository;
import com.lanyuanxiaoyao.service.template.database.service.SimpleServiceSupport;
import org.springframework.stereotype.Service;
@Service
public class TestSoftDeleteService extends SimpleServiceSupport<TestSoftDeleteEntity> {
public TestSoftDeleteService(TestSoftDeleteRepository repository) {
super(repository);
}
}

View File

@@ -0,0 +1,20 @@
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
decorator:
datasource:
p6spy:
logging: slf4j
log-format: "%(executionTime) ms | %(sqlSingleLine)"
logging:
level:
p6spy: INFO

View File

@@ -1,27 +0,0 @@
package com.lanyuanxiaoyao.service.template.controller;
/**
* 详情控制器接口,用于定义统一的获取实体详情的接口规范
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* GET /detail/1
* </pre>
* </p>
*
* @param <DETAIL_ITEM> 详情实体类型
* @author lanyuanxiaoyao
*/
public interface DetailController<DETAIL_ITEM> {
String DETAIL = "/detail/{id}";
/**
* 根据ID获取实体详情
*
* @param id 实体ID
* @return GlobalResponse<DETAIL_ITEM> 返回实体详情
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<DETAIL_ITEM> detail(Long id) throws Exception;
}

View File

@@ -1,77 +0,0 @@
package com.lanyuanxiaoyao.service.template.controller;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Setter
@Getter
@ToString
public class GlobalResponse<T> {
private static final int SUCCESS_STATUS = 0;
private static final int ERROR_STATUS = 500;
private static final String SUCCESS_MESSAGE = "OK";
private static final String ERROR_MESSAGE = "ERROR";
private Integer status;
private String message;
private T data;
private GlobalResponse() {
this(SUCCESS_STATUS, SUCCESS_MESSAGE, null);
}
private GlobalResponse(Integer status, String message) {
this(status, message, null);
}
private GlobalResponse(Integer status, String message, T data) {
this.status = status;
this.message = message;
this.data = data;
}
public static GlobalResponse<Object> responseError() {
return responseError(ERROR_MESSAGE);
}
public static GlobalResponse<Object> responseError(String message) {
return new GlobalResponse<>(ERROR_STATUS, message);
}
public static GlobalResponse<Object> responseSuccess() {
return responseSuccess(SUCCESS_MESSAGE);
}
public static GlobalResponse<Object> responseSuccess(String message) {
return responseSuccess(message, null);
}
public static <E> GlobalResponse<E> responseSuccess(E data) {
return responseSuccess(SUCCESS_MESSAGE, data);
}
public static <E> GlobalResponse<E> responseSuccess(String message, E data) {
return new GlobalResponse<>(SUCCESS_STATUS, message, data);
}
public static GlobalResponse<Map<String, Object>> responseMapData(Map<String, Object> data) {
return responseSuccess(data);
}
public static GlobalResponse<Map<String, Object>> responseMapData(String key, Object value) {
return responseMapData(Map.of(key, value));
}
public static <T> GlobalResponse<Map<String, Object>> responseCrudData(Iterable<T> data, Integer total) {
return responseCrudData(data, total.longValue());
}
public static <T> GlobalResponse<Map<String, Object>> responseCrudData(Iterable<T> data, Long total) {
return responseMapData(Map.of("items", data, "total", total));
}
public static GlobalResponse<Map<String, Object>> responseDetailData(Object detail) {
return responseMapData("detail", detail);
}
}

View File

@@ -1,78 +0,0 @@
package com.lanyuanxiaoyao.service.template.controller;
import java.util.Map;
/**
* 列表控制器接口,用于定义统一的获取实体列表的接口规范
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* {
* "query": {
* "equal": {
* "status": "ACTIVE"
* },
* "like": {
* "name": "关键字"
* }
* },
* "sort": [
* {
* "column": "createTime",
* "direction": "DESC"
* }
* ],
* "page": {
* "index": 0,
* "size": 10
* }
* }
* </pre>
* </p>
*
* <p>
* 支持的查询条件说明:
* <ul>
* <li>nullEqual: 指定字段值为null的条件列表</li>
* <li>notNullEqual: 指定字段值不为null的条件列表</li>
* <li>empty: 指定字段值为空的条件列表(如空字符串、空集合等)</li>
* <li>notEmpty: 指定字段值不为空的条件列表</li>
* <li>equal: 指定字段值相等的条件映射(字段名 -> 值)</li>
* <li>notEqual: 指定字段值不相等的条件映射(字段名 -> 值)</li>
* <li>like: 指定字段模糊匹配的条件映射(字段名 -> 匹配值)</li>
* <li>notLike: 指定字段不模糊匹配的条件映射(字段名 -> 匹配值)</li>
* <li>great: 指定字段大于条件的映射(字段名 -> 值)</li>
* <li>less: 指定字段小于条件的映射(字段名 -> 值)</li>
* <li>greatEqual: 指定字段大于等于条件的映射(字段名 -> 值)</li>
* <li>lessEqual: 指定字段小于等于条件的映射(字段名 -> 值)</li>
* <li>in: 指定字段值在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li>notIn: 指定字段值不在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li>between: 指定字段值在指定区间内的条件映射(字段名 -> 区间范围)</li>
* <li>notBetween: 指定字段值不在指定区间内的条件映射(字段名 -> 区间范围)</li>
* </ul>
* </p>
*
* @param <LIST_ITEM> 列表项的实体类型
* @author lanyuanxiaoyao
*/
public interface ListController<LIST_ITEM> {
String LIST = "/list";
/**
* 获取所有实体列表
*
* @return GlobalCrudResponse<LIST_ITEM> 返回实体列表
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<Map<String, Object>> list() throws Exception;
/**
* 根据查询条件获取实体列表
*
* @param query 查询条件对象
* @return GlobalCrudResponse<LIST_ITEM> 返回符合条件的实体列表
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<Map<String, Object>> list(Query query) throws Exception;
}

View File

@@ -1,247 +0,0 @@
package com.lanyuanxiaoyao.service.template.controller;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 查询条件封装类,用于构建复杂的查询条件
* 包含查询条件、排序条件和分页条件
*
* <p>前端传入的JSON格式示例:</p>
* <pre>
* {
* "query": {
* "equal": {
* "name": "张三"
* },
* "like": {
* "address": "%北京%"
* },
* "greatEqual": {
* "age": 18
* },
* "less": {
* "age": 60
* },
* "between": {
* "salary": {
* "start": 5000,
* "end": 10000
* }
* }
* },
* "sort": [
* {
* "column": "createTime",
* "direction": "DESC"
* }
* ],
* "page": {
* "index": 0,
* "size": 10
* }
* }
* </pre>
*
* <p>查询条件说明:</p>
* <ul>
* <li>nullEqual: 字段值为null的条件</li>
* <li>notNullEqual: 字段值不为null的条件</li>
* <li>empty: 字段值为空的条件</li>
* <li>notEmpty: 字段值不为空的条件</li>
* <li>equal: 字段值相等的条件</li>
* <li>notEqual: 字段值不相等的条件</li>
* <li>like: 字段值模糊匹配的条件</li>
* <li>notLike: 字段值不模糊匹配的条件</li>
* <li>great: 字段值大于的条件</li>
* <li>less: 字段值小于的条件</li>
* <li>greatEqual: 字段值大于等于的条件</li>
* <li>lessEqual: 字段值小于等于的条件</li>
* <li>in: 字段值在指定范围内的条件</li>
* <li>notIn: 字段值不在指定范围内的条件</li>
* <li>between: 字段值在指定区间内的条件</li>
* <li>notBetween: 字段值不在指定区间内的条件</li>
* </ul>
*/
@Setter
@Getter
@ToString
public class Query {
/**
* 查询条件
*/
private Queryable query;
/**
* 排序条件列表
*/
private List<Sortable> sort;
/**
* 分页条件
*/
private Pageable page;
/**
* 可查询条件类,封装各种查询条件
*/
@Setter
@Getter
@ToString
public static class Queryable {
/**
* 指定字段值为null的条件列表
*/
private List<String> nullEqual;
/**
* 指定字段值不为null的条件列表
*/
private List<String> notNullEqual;
/**
* 指定字段值为空的条件列表(如空字符串、空集合等)
*/
private List<String> empty;
/**
* 指定字段值不为空的条件列表
*/
private List<String> notEmpty;
/**
* 指定字段值相等的条件映射(字段名 -> 值)
*/
private Map<String, Object> equal;
/**
* 指定字段值不相等的条件映射(字段名 -> 值)
*/
private Map<String, Object> notEqual;
/**
* 指定字段模糊匹配的条件映射(字段名 -> 匹配值)
*/
private Map<String, String> like;
/**
* 指定字段不模糊匹配的条件映射(字段名 -> 匹配值)
*/
private Map<String, String> notLike;
/**
* 指定字段包含指定字符串的条件映射(字段名 -> 包含值)
*/
private Map<String, String> contain;
/**
* 指定字段不包含指定字符串的条件映射(字段名 -> 不包含值)
*/
private Map<String, String> notContain;
/**
* 指定字段以指定字符串开头的条件映射(字段名 -> 开头值)
*/
private Map<String, String> startWith;
/**
* 指定字段不以指定字符串开头的条件映射(字段名 -> 不开头值)
*/
private Map<String, String> notStartWith;
/**
* 指定字段以指定字符串结尾的条件映射(字段名 -> 结尾值)
*/
private Map<String, String> endWith;
/**
* 指定字段不以指定字符串结尾的条件映射(字段名 -> 不结尾值)
*/
private Map<String, String> notEndWith;
/**
* 指定字段大于条件的映射(字段名 -> 值)
*/
private Map<String, Object> great;
/**
* 指定字段小于条件的映射(字段名 -> 值)
*/
private Map<String, Object> less;
/**
* 指定字段大于等于条件的映射(字段名 -> 值)
*/
private Map<String, Object> greatEqual;
/**
* 指定字段小于等于条件的映射(字段名 -> 值)
*/
private Map<String, Object> lessEqual;
/**
* 指定字段值在指定范围内的条件映射(字段名 -> 值列表)
*/
private Map<String, List<Object>> inside;
/**
* 指定字段值不在指定范围内的条件映射(字段名 -> 值列表)
*/
private Map<String, List<Object>> notInside;
/**
* 指定字段值在指定区间内的条件映射(字段名 -> 区间范围)
*/
private Map<String, Between> between;
/**
* 指定字段值不在指定区间内的条件映射(字段名 -> 区间范围)
*/
private Map<String, Between> notBetween;
/**
* 区间范围类,用于表示起始值和结束值
*/
@Setter
@Getter
@ToString
public static class Between {
/**
* 起始值
*/
private Object start;
/**
* 结束值
*/
private Object end;
}
}
/**
* 可排序条件类,用于指定排序字段和排序方向
*/
@Setter
@Getter
@ToString
public static class Sortable {
/**
* 排序字段名
*/
private String column;
/**
* 排序方向
*/
private Direction direction;
/**
* 排序方向枚举
*/
public enum Direction {
/**
* 升序排列
*/
ASC,
/**
* 降序排列
*/
DESC,
}
}
/**
* 可分页条件类,用于指定分页参数
*/
@Setter
@Getter
@ToString
public static class Pageable {
/**
* 页码索引从0开始
*/
private Integer index;
/**
* 每页大小
*/
private Integer size;
}
}

View File

@@ -1,26 +0,0 @@
package com.lanyuanxiaoyao.service.template.controller;
/**
* 删除控制器接口,用于定义统一的删除实体对象的接口规范
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* DELETE /remove/1
* </pre>
* </p>
*
* @author lanyuanxiaoyao
*/
public interface RemoveController {
String REMOVE = "/remove/{id}";
/**
* 根据ID删除实体对象
*
* @param id 需要删除的实体ID
* @return GlobalResponse<Object> 返回删除结果
* @throws Exception 删除过程中可能抛出的异常
*/
GlobalResponse<Object> remove(Long id) throws Exception;
}

View File

@@ -1,30 +0,0 @@
package com.lanyuanxiaoyao.service.template.controller;
/**
* 保存控制器接口,用于定义统一的保存实体对象的接口规范
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* {
* // 实体对象的具体字段
* "name": "示例名称",
* "description": "示例描述"
* }
* </pre>
* </p>
*
* @author lanyuanxiaoyao
*/
public interface SaveController<SAVE_ITEM> {
String SAVE = "/save";
/**
* 保存实体对象
*
* @param item 需要保存的实体对象
* @return GlobalResponse<Long> 返回保存后的实体ID
* @throws Exception 保存过程中可能抛出的异常
*/
GlobalResponse<Long> save(SAVE_ITEM item) throws Exception;
}

View File

@@ -1,4 +0,0 @@
package com.lanyuanxiaoyao.service.template.controller;
public interface SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> extends SaveController<SAVE_ITEM>, ListController<LIST_ITEM>, DetailController<DETAIL_ITEM>, RemoveController {
}

View File

@@ -1,109 +0,0 @@
package com.lanyuanxiaoyao.service.template.helper;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
public class ObjectHelper {
/**
* 判断对象是否为null
*
* @param obj 待检查的对象
* @return 如果对象为null返回true否则返回false
*/
public static boolean isNull(Object obj) {
return obj == null;
}
/**
* 判断对象是否不为null
*
* @param obj 待判断的对象
* @return 如果对象不为null则返回true否则返回false
*/
public static boolean isNotNull(Object obj) {
return !isNull(obj);
}
/**
* 判断对象是否为空
*
* @param obj 待判断的对象
* @return 如果对象为null或为空则返回true否则返回false
*/
public static boolean isEmpty(Object obj) {
// 首先判断对象是否为null
if (isNull(obj)) return true;
// 判断是否为集合类型
else if (obj instanceof Collection<?> collection) return collection.isEmpty();
// 判断是否为Map类型
else if (obj instanceof Map<?, ?> map) return map.isEmpty();
// 判断是否为字符序列类型
else if (obj instanceof CharSequence sequence) return sequence.isEmpty();
// 判断是否为各种基本类型数组
else if (obj instanceof Object[] array) return array.length == 0;
else if (obj instanceof byte[] array) return array.length == 0;
else if (obj instanceof short[] array) return array.length == 0;
else if (obj instanceof int[] array) return array.length == 0;
else if (obj instanceof long[] array) return array.length == 0;
else if (obj instanceof float[] array) return array.length == 0;
else if (obj instanceof double[] array) return array.length == 0;
else if (obj instanceof char[] array) return array.length == 0;
else if (obj instanceof boolean[] array) return array.length == 0;
// 判断是否为Optional类型
else if (obj instanceof Optional<?> optional) return optional.isEmpty();
// 其他情况认为对象不为空
else return false;
}
public static boolean isNotEmpty(Object obj) {
return !isEmpty(obj);
}
public static <T> T defaultIfNull(final T object, final T defaultValue) {
return isNull(object) ? defaultValue : object;
}
/**
* 判断给定的类是否可比较
*
* @param clazz 待判断的类对象
* @return 如果类是枚举、字符序列、可比较接口的实现类或基本数据类型则返回true否则返回false
*/
public static boolean isComparable(Class<?> clazz) {
if (isNull(clazz)) return false;
// 判断类是否为可比较类型:枚举、字符序列、可比较接口实现类或基本数据类型
return clazz.isEnum() ||
CharSequence.class.isAssignableFrom(clazz) ||
Comparable.class.isAssignableFrom(clazz) ||
clazz.isPrimitive();
}
public static boolean isComparable(Object obj) {
if (isNull(obj)) return false;
return isComparable(obj.getClass());
}
public static boolean isCollection(Class<?> clazz) {
if (isNull(clazz)) return false;
return Collection.class.isAssignableFrom(clazz);
}
public static boolean isCollection(Object obj) {
if (isNull(obj)) return false;
return isCollection(obj.getClass());
}
public static boolean isString(Class<?> clazz) {
if (isNull(clazz)) return false;
return String.class.isAssignableFrom(clazz);
}
public static boolean isString(Object obj) {
if (isNull(obj)) return false;
return isString(obj.getClass());
}
}

View File

@@ -1,87 +0,0 @@
package com.lanyuanxiaoyao.service.template.helper;
import java.io.Serializable;
import java.time.Instant;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.id.factory.spi.StandardGenerator;
@Slf4j
public class SnowflakeIdGenerator implements IdentifierGenerator, StandardGenerator {
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) {
try {
return Snowflake.next();
} catch (Exception e) {
log.error("Generate snowflake id failed", e);
throw new RuntimeException(e);
}
}
public static class Snowflake {
/**
* 起始的时间戳
*/
private final static long START_TIMESTAMP = 1;
/**
* 序列号占用的位数
*/
private final static long SEQUENCE_BIT = 11;
/**
* 序列号最大值
*/
private final static long MAX_SEQUENCE_BIT = ~(-1 << SEQUENCE_BIT);
/**
* 时间戳值向左位移
*/
private final static long TIMESTAMP_OFFSET = SEQUENCE_BIT;
/**
* 序列号
*/
private static long sequence = 0;
/**
* 上一次时间戳
*/
private static long lastTimestamp = -1;
public static synchronized long next() {
long currentTimestamp = nowTimestamp();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock have moved backwards.");
}
if (currentTimestamp == lastTimestamp) {
// 相同毫秒内, 序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE_BIT;
// 同一毫秒的序列数已经达到最大
if (sequence == 0) {
currentTimestamp = nextTimestamp();
}
} else {
// 不同毫秒内, 序列号置为0
sequence = 0;
}
lastTimestamp = currentTimestamp;
return (currentTimestamp - START_TIMESTAMP) << TIMESTAMP_OFFSET | sequence;
}
private static long nextTimestamp() {
long milli = nowTimestamp();
while (milli <= lastTimestamp) {
milli = nowTimestamp();
}
return milli;
}
private static long nowTimestamp() {
return Instant.now().toEpochMilli();
}
}
}

View File

@@ -1,29 +0,0 @@
package com.lanyuanxiaoyao.service.template.service;
import com.lanyuanxiaoyao.service.template.controller.Query;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import java.util.List;
import java.util.Set;
import org.springframework.data.domain.Page;
public interface SimpleService<ENTITY extends SimpleEntity> {
Long save(ENTITY entity) throws Exception;
void save(Iterable<ENTITY> entities) throws Exception;
Long count() throws Exception;
List<ENTITY> list() throws Exception;
List<ENTITY> list(Set<Long> ids) throws Exception;
Page<ENTITY> list(Query query) throws Exception;
ENTITY detail(Long id) throws Exception;
ENTITY detailOrThrow(Long id) throws Exception;
void remove(Long id) throws Exception;
void remove(Iterable<Long> ids) throws Exception;
}

View File

@@ -1,690 +0,0 @@
package com.lanyuanxiaoyao.service.template.service;
import com.lanyuanxiaoyao.service.template.controller.Query;
import com.lanyuanxiaoyao.service.template.entity.IdOnlyEntity;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.helper.ObjectHelper;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.transaction.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.Named;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
/**
* 简单服务支持类,提供基础的业务逻辑实现
* <p>
* 该类实现了SimpleService接口提供实体的增删改查等基本操作。
* 通过继承此类,可以快速实现常见的业务逻辑功能,包括:
* <ul>
* <li>实体的保存和更新</li>
* <li>实体的条件查询和分页查询</li>
* <li>实体的详情查询(多种方式)</li>
* <li>实体的删除操作</li>
* </ul>
* </p>
*
* <p>
* 查询条件说明:
* <ul>
* <li>nullEqual: 指定字段值为null的条件列表</li>
* <li>notNullEqual: 指定字段值不为null的条件列表</li>
* <li>empty: 指定字段值为空的条件列表(如空字符串、空集合等)</li>
* <li>notEmpty: 指定字段值不为空的条件列表</li>
* <li>equal: 指定字段值相等的条件映射(字段名 -> 值)</li>
* <li>notEqual: 指定字段值不相等的条件映射(字段名 -> 值)</li>
* <li>contain: 指定字段包含指定字符串的条件映射(字段名 -> 包含值)</li>
* <li>notContain: 指定字段不包含指定字符串的条件映射(字段名 -> 不包含值)</li>
* <li>startWith: 指定字段以指定字符串开头的条件映射(字段名 -> 开头值)</li>
* <li>notStartWith: 指定字段不以指定字符串开头的条件映射(字段名 -> 不开头值)</li>
* <li>endWith: 指定字段以指定字符串结尾的条件映射(字段名 -> 结尾值)</li>
* <li>notEndWith: 指定字段不以指定字符串结尾的条件映射(字段名 -> 不结尾值)</li>
* <li>great: 指定字段大于条件的映射(字段名 -> 值)</li>
* <li>less: 指定字段小于条件的映射(字段名 -> 值)</li>
* <li>greatEqual: 指定字段大于等于条件的映射(字段名 -> 值)</li>
* <li>lessEqual: 指定字段小于等于条件的映射(字段名 -> 值)</li>
* <li>in: 指定字段值在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li>notIn: 指定字段值不在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li>between: 指定字段值在指定区间内的条件映射(字段名 -> 区间范围)</li>
* <li>notBetween: 指定字段值不在指定区间内的条件映射(字段名 -> 区间范围)</li>
* </ul>
* </p>
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* {
* "query": {
* "equal": {
* "status": "ACTIVE"
* },
* "like": {
* "name": "关键字"
* }
* },
* "sort": [
* {
* "column": "createdTime",
* "direction": "DESC"
* }
* ],
* "page": {
* "index": 1,
* "size": 10
* }
* }
* </pre>
* </p>
*
* @param <ENTITY> 实体类型必须继承SimpleEntity
* @author lanyuanxiaoyao
*/
@Slf4j
public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implements SimpleService<ENTITY> {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final Integer DEFAULT_PAGE_INDEX = 1;
private static final Integer DEFAULT_PAGE_SIZE = 10;
protected final SimpleRepository<ENTITY> repository;
/**
* 构造函数
*
* @param repository 简单仓库实例
*/
public SimpleServiceSupport(SimpleRepository<ENTITY> repository) {
this.repository = repository;
}
/**
* 保存实体对象
* <p>
* 使用saveOrUpdateByNotNullProperties方法保存实体只更新非空字段。
* 该方法具有事务性,遇到任何异常都会回滚。
* </p>
*
* @param entity 需要保存的实体对象
* @return Long 返回保存后的实体ID
*/
@Transactional(rollbackOn = Throwable.class)
@Override
public Long save(ENTITY entity) {
entity = repository.saveOrUpdateByNotNullProperties(entity);
return entity.getId();
}
/**
* 批量保存实体对象集合
* <p>
* 使用saveOrUpdateAllByNotNullProperties方法只更新非空字段。
* 该方法具有事务性,遇到任何异常都会回滚。
* </p>
*
* @param entities 需要保存的实体对象集合
*/
@Transactional(rollbackOn = Throwable.class)
@Override
public void save(Iterable<ENTITY> entities) {
repository.saveOrUpdateAllByNotNullProperties(entities);
}
/**
* 统计符合条件的实体数量
* <p>
* 根据[listPredicate](file:///Users/lanyuanxiaoyao/Project/IdeaProjects/spring-boot-service-template/src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java#L261-L263)方法构建的条件统计实体数量。
* </p>
*
* @return Long 返回符合条件的实体数量
*/
@Override
public Long count() {
return repository.count(this::listPredicate);
}
/**
* 获取所有符合条件的实体列表
* <p>
* 根据[listPredicate](file:///Users/lanyuanxiaoyao/Project/IdeaProjects/spring-boot-service-template/src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java#L261-L263)方法构建的条件查询所有实体。
* </p>
*
* @return List<ENTITY> 返回符合条件的实体列表
*/
@Override
public List<ENTITY> list() {
return repository.findAll(this::listPredicate);
}
/**
* 根据ID集合获取实体列表
* <p>
* 根据提供的ID集合查询对应的实体列表并结合[listPredicate](file:///Users/lanyuanxiaoyao/Project/IdeaProjects/spring-boot-service-template/src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java#L261-L263)方法构建的条件。
* </p>
*
* @param ids ID集合
* @return List<ENTITY> 返回ID集合对应的实体列表
*/
@Override
public List<ENTITY> list(Set<Long> ids) {
return repository.findAll(
(root, query, builder) -> {
var predicate = listPredicate(root, query, builder);
var idsPredicate = builder.in(root.get(IdOnlyEntity.Fields.id)).value(ids);
return ObjectHelper.isNull(predicate)
? idsPredicate
: builder.and(predicate, idsPredicate);
}
);
}
/**
* 解析字段路径
* <p>
* 支持多级字段路径解析,使用"/"分隔多级字段。
* 例如: "user/name" 表示实体的user属性的name字段。
* </p>
*
* @param root JPA Criteria查询根节点
* @param column 字段路径字符串
* @param <Y> 字段类型
* @return Path<Y> 返回字段路径对象
* @throws IllegalArgumentException 当字段路径为空时抛出
*/
private <Y> Path<Y> column(Root<ENTITY> root, String column) {
if (ObjectHelper.isEmpty(column)) {
throw new IllegalArgumentException("Column cannot be blank");
}
var columns = column.split("\\.");
Path<Y> path = root.get(columns[0]);
for (int i = 1; i < columns.length; i++) {
path = path.get(columns[i]);
}
return path;
}
/**
* 处理字段值
* <p>
* 对于枚举类型字段,将字符串值转换为对应的枚举值。
* 其他类型直接返回原值。
* </p>
*
* @param column 字段路径
* @param value 字段值
* @param <Y> 字段类型
* @return Object 处理后的字段值
* @throws IllegalArgumentException 当枚举类型字段的值不是字符串时抛出
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private <Y> Object value(Path<Y> column, Object value) {
var javaType = column.getJavaType();
if (javaType.isEnum()) {
if (value instanceof String enumName) {
var enumType = (Class<Enum>) javaType;
return Enum.valueOf(enumType, enumName);
} else {
throw new IllegalArgumentException("枚举类型字段需要 String 类型的值");
}
} else if (javaType.isAssignableFrom(LocalDateTime.class)) {
return LocalDateTime.parse(String.valueOf(value), DATE_TIME_FORMATTER);
}
return value;
}
/**
* 构建查询条件谓词列表
* <p>
* 根据Query.Queryable对象构建JPA Criteria查询的谓词列表。
* 支持多种查询条件类型,包括相等、不等、模糊匹配、范围查询等。
* </p>
*
* @param queryable 查询条件对象
* @param root JPA Criteria查询根节点
* @param query JPA Criteria查询对象
* @param builder JPA Criteria构建器
* @return List<Predicate> 返回构建的谓词列表
*/
@SuppressWarnings("unchecked")
protected Predicate queryPredicates(Query.Queryable queryable, Root<ENTITY> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
var predicates = new ArrayList<Predicate>();
if (ObjectHelper.isNull(queryable)) {
return null;
}
if (ObjectHelper.isNotEmpty(queryable.getNullEqual())) {
queryable.getNullEqual().forEach(column -> predicates.add(builder.isNull(column(root, column))));
}
if (ObjectHelper.isNotEmpty(queryable.getNotNullEqual())) {
queryable.getNotNullEqual().forEach(column -> predicates.add(builder.isNotNull(column(root, column))));
}
if (ObjectHelper.isNotEmpty(queryable.getEmpty())) {
queryable.getEmpty().forEach(column -> {
var path = this.<Collection<Object>>column(root, column);
checkCollection(path, column);
predicates.add(builder.isEmpty(path));
});
}
if (ObjectHelper.isNotEmpty(queryable.getNotEmpty())) {
queryable.getNotEmpty().forEach(column -> {
var path = this.<Collection<Object>>column(root, column);
checkCollection(path, column);
predicates.add(builder.isNotEmpty(path));
});
}
if (ObjectHelper.isNotEmpty(queryable.getEqual())) {
queryable.getEqual().forEach((column, value) -> {
var path = column(root, column);
predicates.add(builder.equal(path, value(path, value)));
});
}
if (ObjectHelper.isNotEmpty(queryable.getNotEqual())) {
queryable.getEqual().forEach((column, value) -> {
var path = column(root, column);
predicates.add(builder.notEqual(path, value(path, value)));
});
}
if (ObjectHelper.isNotEmpty(queryable.getLike())) {
queryable.getLike().forEach((column, value) -> {
var path = column(root, column);
checkString(path, value, column);
predicates.add(builder.like(column(root, column), value));
});
}
if (ObjectHelper.isNotEmpty(queryable.getNotLike())) {
queryable.getNotLike().forEach((column, value) -> {
var path = column(root, column);
checkString(path, value, column);
predicates.add(builder.notLike(column(root, column), value));
});
}
if (ObjectHelper.isNotEmpty(queryable.getContain())) {
queryable.getContain().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.like(column(root, column), "%" + value + "%"));
});
}
if (ObjectHelper.isNotEmpty(queryable.getNotContain())) {
queryable.getNotContain().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.notLike(column(root, column), "%" + value + "%"));
});
}
if (ObjectHelper.isNotEmpty(queryable.getStartWith())) {
queryable.getStartWith().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.like(column(root, column), value + "%"));
});
}
if (ObjectHelper.isNotEmpty(queryable.getNotStartWith())) {
queryable.getNotStartWith().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.notLike(column(root, column), value + "%"));
});
}
if (ObjectHelper.isNotEmpty(queryable.getEndWith())) {
queryable.getEndWith().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.like(column(root, column), "%" + value));
});
}
if (ObjectHelper.isNotEmpty(queryable.getNotEndWith())) {
queryable.getNotEndWith().forEach((column, value) -> {
var path = this.<String>column(root, column);
checkString(path, value, column);
predicates.add(builder.notLike(column(root, column), "%" + value));
});
}
if (ObjectHelper.isNotEmpty(queryable.getGreat())) {
queryable.getGreat().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.greaterThan(path, (Comparable<Object>) value(path, value)));
});
}
if (ObjectHelper.isNotEmpty(queryable.getLess())) {
queryable.getLess().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.lessThan(path, (Comparable<Object>) value(path, value)));
});
}
if (ObjectHelper.isNotEmpty(queryable.getGreatEqual())) {
queryable.getGreatEqual().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.greaterThanOrEqualTo(path, (Comparable<Object>) value(path, value)));
});
}
if (ObjectHelper.isNotEmpty(queryable.getLessEqual())) {
queryable.getLessEqual().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.lessThanOrEqualTo(path, (Comparable<Object>) value(path, value)));
});
}
if (ObjectHelper.isNotEmpty(queryable.getInside())) {
queryable.getInside()
.entrySet()
.stream()
.filter(entry -> ObjectHelper.isNotEmpty(entry.getValue()))
.forEach(entry -> predicates.add(builder.in(column(root, entry.getKey())).value(entry.getValue())));
}
if (ObjectHelper.isNotEmpty(queryable.getNotInside())) {
queryable.getNotInside()
.entrySet()
.stream()
.filter(entry -> ObjectHelper.isNotEmpty(entry.getValue()))
.forEach(entry -> predicates.add(builder.in(column(root, entry.getKey())).value(entry.getValue()).not()));
}
if (ObjectHelper.isNotEmpty(queryable.getBetween())) {
queryable.getBetween().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.between(column(root, column), (Comparable<Object>) value(path, value.getStart()), (Comparable<Object>) value(path, value.getEnd())));
});
}
if (ObjectHelper.isNotEmpty(queryable.getNotBetween())) {
queryable.getNotBetween().forEach((column, value) -> {
var path = this.<Comparable<Object>>column(root, column);
checkComparable(path, value, column);
predicates.add(builder.between(column(root, column), (Comparable<Object>) value(path, value.getStart()), (Comparable<Object>) value(path, value.getEnd())).not());
});
}
return predicates.size() == 1
? predicates.get(0)
: builder.and(predicates.toArray(Predicate[]::new));
}
/**
* 检查字段类型是否可比较
*
* @param path 字段路径
* @param column 字段名称
* @throws NotComparableException 当字段类型不可比较时抛出
*/
private void checkComparable(Path<?> path, Object value, String column) {
if (!ObjectHelper.isComparable(path.getJavaType()) || !ObjectHelper.isComparable(value)) {
throw new NotComparableException(column);
}
}
/**
* 检查区间值是否可比较
*
* @param value 区间对象
* @param column 字段名称
* @throws NotComparableException 当区间值不可比较时抛出
*/
private void checkComparable(Path<?> path, Query.Queryable.Between value, String column) {
checkComparable(path, value.getStart(), column);
checkComparable(path, value.getEnd(), column);
}
/**
* 检查字段类型是否为集合
*
* @param path 字段路径
* @param column 字段名称
* @throws NotCollectionException 当字段类型不是集合时抛出
*/
private void checkCollection(Path<?> path, String column) {
if (!ObjectHelper.isCollection(path.getJavaType())) {
throw new NotCollectionException(column);
}
}
/**
* 检查值是否为集合
*
* @param value 值对象
* @param column 字段名称
* @throws NotCollectionException 当值不是集合时抛出
*/
private void checkCollection(Object value, String column) {
if (!ObjectHelper.isCollection(value)) {
throw new NotCollectionException(column);
}
}
/**
* 检查字段类型是否为字符串
*
* @param path 字段路径
* @param column 字段名称
* @throws NotStringException 当字段类型不是字符串时抛出
*/
private void checkString(Path<?> path, Object value, String column) {
if (!ObjectHelper.isString(path.getJavaType()) || !ObjectHelper.isString(value)) {
throw new NotStringException(column);
}
}
/**
* 构建列表查询条件
* <p>
* 子类可以重写此方法以添加特定的查询条件。
* 默认返回空列表,表示不添加额外条件。
* </p>
*
* @param root JPA Criteria查询根节点
* @param query JPA Criteria查询对象
* @param builder JPA Criteria构建器
* @return List<Predicate> 返回查询条件谓词列表
*/
protected Predicate listPredicate(Root<ENTITY> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return null;
}
/**
* 根据查询条件分页获取实体列表
* <p>
* 支持复杂的查询条件和分页功能。
* 默认分页参数第1页每页10条记录按创建时间降序排列。
* </p>
*
* @param listQuery 查询条件对象
* @return Page<ENTITY> 返回分页查询结果
*/
@Override
public Page<ENTITY> list(Query listQuery) {
var pageRequest = PageRequest.of(DEFAULT_PAGE_INDEX - 1, DEFAULT_PAGE_SIZE, Sort.by(SimpleEntity.Fields.createdTime).descending());
if (ObjectHelper.isNotNull(listQuery.getPage())) {
var index = Math.max(ObjectHelper.defaultIfNull(listQuery.getPage().getIndex(), DEFAULT_PAGE_INDEX) - 1, 0);
var size = Math.max(ObjectHelper.defaultIfNull(listQuery.getPage().getSize(), DEFAULT_PAGE_SIZE), 1);
if (ObjectHelper.isNotEmpty(listQuery.getSort())) {
pageRequest = PageRequest.of(index, size, Sort.by(
listQuery.getSort()
.stream()
.map(sort -> new Sort.Order(Sort.Direction.fromString(sort.getDirection().name()), sort.getColumn()))
.toList()
));
} else {
pageRequest = PageRequest.of(index, size, Sort.by(SimpleEntity.Fields.createdTime).descending());
}
}
return repository.findAll(
(root, query, builder) -> {
var predicate = listPredicate(root, query, builder);
var queryPredicate = queryPredicates(listQuery.getQuery(), root, query, builder);
return ObjectHelper.isNull(predicate)
? queryPredicate
: builder.and(predicate, queryPredicate);
},
pageRequest
);
}
/**
* 根据ID获取实体详情Optional包装
* <p>
* 如果ID为空则返回空Optional否则根据ID查询实体。
* </p>
*
* @param id 实体ID
* @return Optional<ENTITY> 返回实体详情的Optional包装
*/
private Optional<ENTITY> detailOptional(Long id) {
if (ObjectHelper.isNull(id)) {
return Optional.empty();
}
return repository.findOne(
(root, query, builder) -> {
var predicate = listPredicate(root, query, builder);
var idPredicate = builder.equal(root.get(IdOnlyEntity.Fields.id), id);
return ObjectHelper.isNull(predicate)
? idPredicate
: builder.and(predicate, idPredicate);
}
);
}
/**
* 根据ID获取实体详情
* <p>
* 如果实体不存在则返回null。
* </p>
*
* @param id 实体ID
* @return ENTITY 返回实体详情不存在时返回null
*/
@Named("detail")
@Override
public ENTITY detail(Long id) {
return detailOptional(id).orElse(null);
}
/**
* 根据ID获取实体详情不存在时抛出异常
* <p>
* 如果实体不存在则抛出IdNotFoundException异常。
* </p>
*
* @param id 实体ID
* @return ENTITY 返回实体详情
* @throws IdNotFoundException 当实体不存在时抛出
*/
@Named("detailOrThrow")
@Override
public ENTITY detailOrThrow(Long id) {
return detailOptional(id).orElseThrow(() -> new IdNotFoundException(id));
}
/**
* 根据ID删除实体
* <p>
* 具有事务性,遇到任何异常都会回滚。
* 如果ID为空则不执行任何操作。
* </p>
*
* @param id 实体ID
*/
@Transactional(rollbackOn = Throwable.class)
@Override
public void remove(Long id) {
if (ObjectHelper.isNotNull(id)) {
repository.deleteById(id);
}
}
/**
* 根据ID集合批量删除实体
* <p>
* 使用deleteAllById方法根据ID集合批量删除实体。
* 该方法具有事务性,遇到任何异常都会回滚。
* 如果ID集合为空则不执行任何操作。
* </p>
*
* @param ids 实体ID集合
*/
@Transactional(rollbackOn = Throwable.class)
@Override
public void remove(Iterable<Long> ids) {
if (ObjectHelper.isNotEmpty(ids)) {
repository.deleteBatchByIds(ids);
}
}
/**
* ID未找到异常
* <p>
* 当根据ID查询实体但实体不存在时抛出此异常。
* </p>
*/
public static final class IdNotFoundException extends RuntimeException {
/**
* 构造函数带ID参数
*
* @param id 实体ID
*/
public IdNotFoundException(Long id) {
super("ID为 %d 的资源不存在".formatted(id));
}
}
/**
* 不可比较异常
* <p>
* 当尝试对不可比较的字段或值执行比较操作时抛出此异常。
* </p>
*/
public static final class NotComparableException extends RuntimeException {
/**
* 构造函数
*
* @param variable 变量名称
*/
public NotComparableException(String variable) {
super("变量 %s 不能比较".formatted(variable));
}
}
/**
* 非集合异常
* <p>
* 当尝试对非集合类型的字段或值执行集合操作时抛出此异常。
* </p>
*/
public static final class NotCollectionException extends RuntimeException {
/**
* 构造函数
*
* @param variable 变量名称
*/
public NotCollectionException(String variable) {
super("变量 %s 不是集合".formatted(variable));
}
}
/**
* 非字符串异常
* <p>
* 当尝试对非字符串类型的字段或值执行字符串操作时抛出此异常。
* </p>
*/
public static final class NotStringException extends RuntimeException {
/**
* 构造函数
*
* @param variable 变量名称
*/
public NotStringException(String variable) {
super("变量 %s 不是字符串".formatted(variable));
}
}
}

View File

@@ -1,90 +0,0 @@
package com.lanyuanxiaoyao.service.template;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
/**
* Helper测试类
* 用于测试驼峰命名法转下划线命名法的功能
*
* @author lanyuanxiaoyao
*/
@Slf4j
public class HelperTest {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 通过反射调用Helper类中的private静态方法camelConvert
var camelConvert = Helper.class.getDeclaredMethod("camelConvert", String.class);
camelConvert.setAccessible(true);
// 测试用例集合
Map<String, String> testCases = new HashMap<>();
// 基本转换测试
testCases.put("helloWorld", "hello_world");
testCases.put("firstName", "first_name");
testCases.put("lastName", "last_name");
testCases.put("URL", "url");
testCases.put("HTTPResponse", "http_response");
testCases.put("XMLParser", "xml_parser");
// 边界情况测试
testCases.put(null, null); // null输入
testCases.put("", ""); // 空字符串
testCases.put("a", "a"); // 单个小写字母
testCases.put("A", "a"); // 单个大写字母
testCases.put("aB", "a_b"); // 两个字符
testCases.put("Ab", "ab"); // 首字母大写
// 数字相关测试
testCases.put("field1Name", "field1_name");
testCases.put("field12Name", "field12_name");
testCases.put("2FARequired", "2_fa_required");
testCases.put("ID", "id");
testCases.put("userID", "user_id");
testCases.put("HTML5Parser", "html5_parser");
// 连续大写字母测试
testCases.put("HTTPSConnection", "https_connection");
testCases.put("XMLHttpRequest", "xml_http_request");
testCases.put("URLPath", "url_path");
testCases.put("APIKey", "api_key");
testCases.put("JWTToken", "jwt_token");
// 特殊场景测试
testCases.put("iPhone", "i_phone"); // 以小写字母开头,后面有大写
testCases.put("iOSVersion", "i_os_version"); // 连续小写字母后跟大写
testCases.put("CAPTCHA", "captcha"); // 全大写字母缩写
log.info("开始执行驼峰命名转下划线命名测试...");
int passedTests = 0;
int totalTests = testCases.size();
for (Map.Entry<String, String> testCase : testCases.entrySet()) {
String input = testCase.getKey();
String expected = testCase.getValue();
String actual = (String) camelConvert.invoke(null, input);
try {
Assert.isTrue(Objects.equals(expected, actual), "测试失败: 输入='%s', 期望='%s', 实际='%s'".formatted(input, expected, actual));
passedTests++;
log.info("✓ 测试通过: '{}' -> '{}'", input, actual);
} catch (Exception e) {
log.error("✗ {}", e.getMessage());
}
}
log.info("测试结果: {}/{} 通过", passedTests, totalTests);
if (passedTests == totalTests) {
log.info("所有测试通过!✓");
} else {
log.error("有测试失败!✗");
System.exit(1);
}
}
}

View File

@@ -1,307 +0,0 @@
package com.lanyuanxiaoyao.service.template;
import com.blinkfox.fenix.EnableFenix;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lanyuanxiaoyao.service.template.entity.Company;
import com.lanyuanxiaoyao.service.template.entity.Company_;
import com.lanyuanxiaoyao.service.template.entity.Employee;
import com.lanyuanxiaoyao.service.template.entity.Employee_;
import com.lanyuanxiaoyao.service.template.entity.QEmployee;
import com.lanyuanxiaoyao.service.template.entity.Report;
import com.lanyuanxiaoyao.service.template.entity.Report_;
import com.lanyuanxiaoyao.service.template.repository.EmployeeRepository;
import jakarta.annotation.Resource;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
/**
* @author lanyuanxiaoyao
* @version 20250814
*/
@SpringBootApplication
@EnableFenix
@EnableJpaAuditing
public class TestApplication {
private static final Logger log = LoggerFactory.getLogger(TestApplication.class);
private static final String BASE_URL = "http://localhost:2490";
private static final RestTemplate REST_CLIENT = new RestTemplate();
private static final ObjectMapper MAPPER = new ObjectMapper();
@Resource
private EmployeeRepository employeeRepository;
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@EventListener(ApplicationReadyEvent.class)
public void runTests() throws JsonProcessingException {
// 增
var cid1 = saveItem("company", "{\"name\": \"Apple\",\"members\": 10}").get("data").asLong();
var cid2 = saveItem("company", "{\"name\": \"Banana\",\"members\": 20}").get("data").asLong();
var cid3 = saveItem("company", "{\"name\": \"Cheery\",\"members\": 20}").get("data").asLong();
// 查
var companies = listItems("company");
Assert.isTrue(companies.at("/data/items").size() == 3, "数量错误");
Assert.isTrue(companies.at("/data/total").asLong() == 3, "返回数量错误");
// language=JSON
var companies2 = listItems("company", "{\n" +
" \"page\": {\n" +
" \"index\": 1,\n" +
" \"size\": 2\n" +
" }\n" +
"}");
Assert.isTrue(companies2.at("/data/items").size() == 2, "数量错误");
Assert.isTrue(companies2.at("/data/total").asLong() == 3, "返回数量错误");
// language=JSON
var companies3 = listItems("company", "{\n" +
" \"query\": {\n" +
" \"notNullEqual\": [\n" +
" \"name\"\n" +
" ],\n" +
" \"equal\": {\n" +
" \"name\": \"Apple\"\n" +
" },\n" +
" \"like\": {\n" +
" \"name\": \"Appl%\"\n" +
" },\n" +
" \"contain\": {\n" +
" \"name\": \"ple\"\n" +
" },\n" +
" \"startWith\": {\n" +
" \"name\": \"Appl\"\n" +
" },\n" +
" \"endWith\": {\n" +
" \"name\": \"le\"\n" +
" },\n" +
" \"less\": {\n" +
" \"members\": 50\n" +
" },\n" +
" \"greatEqual\": {\n" +
" \"members\": 0,\n" +
" \"createdTime\": \"2025-01-01 00:00:00\"\n" +
" },\n" +
" \"in\": {\n" +
" \"name\": [\n" +
" \"Apple\",\n" +
" \"Banana\"\n" +
" ]\n" +
" },\n" +
" \"between\": {\n" +
" \"members\": {\n" +
" \"start\": 0,\n" +
" \"end\": 50\n" +
" }\n" +
" }\n" +
" },\n" +
" \"page\": {\n" +
" \"index\": 1,\n" +
" \"size\": 2\n" +
" }\n" +
"}");
Assert.isTrue(companies3.at("/data/items").size() == 1, "数量错误");
Assert.isTrue(companies3.at("/data/total").asLong() == 1, "返回数量错误");
var company1 = detailItem("company", cid1);
Assert.isTrue(cid1 == company1.at("/data/id").asLong(), "id错误");
Assert.isTrue("Apple".equals(company1.at("/data/name").asText()), "name错误");
// 改
var cid4 = saveItem("company", "{\"id\": %d, \"name\": \"Dog\"}".formatted(cid2)).get("data").asLong();
Assert.isTrue(cid2 == cid4, "id错误");
var company2 = detailItem("company", cid2);
Assert.isTrue("Dog".equals(company2.at("/data/name").asText()), "name错误");
// 删
removeItem("company", cid3);
Assert.isTrue(listItems("company").at("/data/items").size() == 2, "数量错误");
Assert.isTrue(listItems("company").at("/data/total").asLong() == 2, "返回数量错误");
log.info(listItems("company").toPrettyString());
var eid1 = saveItem("employee", "{\"name\": \"Tom\",\"age\": 18, \"companyId\": %d}".formatted(cid1)).get("data").asLong();
var eid2 = saveItem("employee", "{\"name\": \"Jerry\",\"age\": 18, \"companyId\": %d}".formatted(cid1)).get("data").asLong();
var eid3 = saveItem("employee", "{\"name\": \"Mike\",\"age\": 18, \"companyId\": %d}".formatted(cid2)).get("data").asLong();
var employees = listItems("employee");
Assert.isTrue(employees.at("/data/items").size() == 3, "数量错误");
Assert.isTrue(employees.at("/data/total").asLong() == 3, "返回数量错误");
var employee1 = detailItem("employee", eid1);
Assert.isTrue(eid1 == employee1.at("/data/id").asLong(), "id错误");
Assert.isTrue("Tom".equals(employee1.at("/data/name").asText()), "name错误");
Assert.isTrue(18 == employee1.at("/data/age").asInt(), "age错误");
System.exit(0);
}
@EventListener(ApplicationReadyEvent.class)
public void runSpecificationTests() throws JsonProcessingException {
// 增
var cid1 = saveItem("company", "{\"name\": \"Apple\",\"members\": 10}").get("data").asLong();
var cid2 = saveItem("company", "{\"name\": \"Banana\",\"members\": 20}").get("data").asLong();
var cid3 = saveItem("company", "{\"name\": \"Cheery\",\"members\": 20}").get("data").asLong();
var eid1 = saveItem("employee", "{\"name\": \"Tom\",\"age\": 18, \"companyId\": %d}".formatted(cid1)).get("data").asLong();
var eid2 = saveItem("employee", "{\"name\": \"Jerry\",\"age\": 18, \"companyId\": %d}".formatted(cid1)).get("data").asLong();
var eid3 = saveItem("employee", "{\"name\": \"Mike\",\"age\": 18, \"companyId\": %d}".formatted(cid2)).get("data").asLong();
var rid = saveItem("report", "{\"employeeId\": %d, \"score\": 56.38, \"level\": \"A\"}".formatted(eid1)).get("data").asLong();
var rid2 = saveItem("report", "{\"employeeId\": %d, \"score\": 78.98, \"level\": \"B\"}".formatted(eid2)).get("data").asLong();
log.debug(
"Results: {}",
employeeRepository.findAll(
builder -> builder
.andIsNotNull(Employee.Fields.name)
.andEquals(Employee.Fields.name, "Tom")
.andLike(Employee.Fields.name, "To%")
.andStartsWith(Employee.Fields.name, "To")
.andEndsWith(Employee.Fields.name, "om")
.andLessThan(Employee.Fields.age, 50)
.andGreaterThanEqual(Employee.Fields.age, 0)
.andIn(Employee.Fields.name, List.of("Tom", "Mike"))
.andBetween(Employee.Fields.age, 0, 50)
.build()
)
);
log.debug(
"Results: {}",
employeeRepository.findAll(
(root, query, builder) ->
builder.and(
builder.isNotNull(root.get(Employee_.name)),
builder.equal(root.get(Employee_.name), "Tom"),
builder.like(root.get(Employee_.name), "To%"),
builder.lessThan(root.get(Employee_.age), 50),
builder.greaterThanOrEqualTo(root.get(Employee_.age), 0),
builder.in(root.get(Employee_.NAME)).value(List.of("Tom", "Mike")),
builder.between(root.get(Employee_.age), 0, 50),
builder.isNotEmpty(root.get(Employee_.company).get(Company_.employees)),
builder.isMember(Company.Industry.MEDIA, root.get(Employee_.company).get(Company_.industries))
)
)
);
log.debug(
"Results: {}",
employeeRepository.findAll(
QEmployee.employee.name.isNotNull()
.and(QEmployee.employee.name.eq("Tom"))
.and(QEmployee.employee.name.like("To%"))
.and(QEmployee.employee.name.startsWith("To"))
.and(QEmployee.employee.name.endsWith("om"))
.and(QEmployee.employee.age.lt(50))
.and(QEmployee.employee.age.goe(0))
.and(QEmployee.employee.name.in("Tom", "Mike"))
.and(QEmployee.employee.age.between(0, 50))
.and(QEmployee.employee.company().employees.isNotEmpty())
.and(QEmployee.employee.company().employees.any().name.ne("Tom"))
.and(QEmployee.employee.company().industries.contains(Company.Industry.MEDIA))
.and(QEmployee.employee.connections.containsKey(Employee.ConnectionType.EMAIL))
)
);
log.debug(
"Results: {}",
employeeRepository.findAll(
(root, query, builder) -> {
var reportRoot = query.from(Report.class);
return builder.and(
builder.equal(root.get(Employee_.id), reportRoot.get(Report_.employeeId)),
builder.equal(reportRoot.get(Report_.level), Report.Level.A)
);
}
)
);
System.exit(0);
}
@EventListener(ApplicationReadyEvent.class)
public void runNativeQueryTests() throws JsonProcessingException {
// 增
var cid1 = saveItem("company", "{\"name\": \"Apple\",\"members\": 10}").get("data").asLong();
var cid2 = saveItem("company", "{\"name\": \"Banana\",\"members\": 20}").get("data").asLong();
var cid3 = saveItem("company", "{\"name\": \"Cheery\",\"members\": 20}").get("data").asLong();
var eid1 = saveItem("employee", "{\"name\": \"Tom\",\"age\": 18, \"companyId\": %d}".formatted(cid1)).get("data").asLong();
var eid2 = saveItem("employee", "{\"name\": \"Jerry\",\"age\": 18, \"companyId\": %d}".formatted(cid1)).get("data").asLong();
var eid3 = saveItem("employee", "{\"name\": \"Mike\",\"age\": 18, \"companyId\": %d}".formatted(cid2)).get("data").asLong();
var list = employeeRepository.findAllEmployeeWithCompanyName();
Assert.isTrue(list.size() == 3, "数量错误");
log.debug("Results: {}", list);
var list_native = employeeRepository.findAllEmployeeWithCompanyNameNative();
Assert.isTrue(list_native.size() == 3, "数量错误");
log.debug("Results: {}", list_native);
}
private HttpHeaders headers() {
var headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
private JsonNode saveItem(String path, String body) throws JsonProcessingException {
var response = REST_CLIENT.postForEntity(
"%s/%s/save".formatted(BASE_URL, path),
new HttpEntity<>(body, headers()),
String.class
);
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求失败");
Assert.notNull(response.getBody(), "请求失败");
return MAPPER.readTree(response.getBody());
}
private JsonNode listItems(String path) throws JsonProcessingException {
var response = REST_CLIENT.getForEntity(
"%s/%s/list".formatted(BASE_URL, path),
String.class
);
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求失败");
Assert.notNull(response.getBody(), "请求失败");
return MAPPER.readTree(response.getBody());
}
private JsonNode listItems(String path, String query) throws JsonProcessingException {
var response = REST_CLIENT.postForEntity(
"%s/%s/list".formatted(BASE_URL, path),
new HttpEntity<>(query, headers()),
String.class
);
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求失败");
Assert.notNull(response.getBody(), "请求失败");
return MAPPER.readTree(response.getBody());
}
private JsonNode detailItem(String path, Long id) throws JsonProcessingException {
var response = REST_CLIENT.getForEntity(
"%s/%s/detail/%d".formatted(BASE_URL, path, id),
String.class
);
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求失败");
Assert.notNull(response.getBody(), "请求失败");
return MAPPER.readTree(response.getBody());
}
private void removeItem(String path, Long id) {
var response = REST_CLIENT.getForEntity(
"%s/%s/remove/%d".formatted(BASE_URL, path, id),
Void.class
);
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求失败");
}
}

View File

@@ -1,70 +0,0 @@
package com.lanyuanxiaoyao.service.template.controller;
import com.lanyuanxiaoyao.service.template.entity.Company;
import com.lanyuanxiaoyao.service.template.service.CompanyService;
import java.time.LocalDateTime;
import java.util.function.Function;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("company")
public class CompanyController extends SimpleControllerSupport<Company, CompanyController.SaveItem, CompanyController.ListItem, CompanyController.DetailItem> {
public CompanyController(CompanyService service) {
super(service);
}
@Override
protected Function<SaveItem, Company> saveItemMapper() {
return item -> {
var company = new Company();
company.setId(item.id());
company.setName(item.name());
company.setMembers(item.members());
return company;
};
}
@Override
protected Function<Company, ListItem> listItemMapper() {
return company -> new ListItem(
company.getId(),
company.getName(),
company.getMembers()
);
}
@Override
protected Function<Company, DetailItem> detailItemMapper() {
return company -> new DetailItem(
company.getId(),
company.getName(),
company.getMembers(),
company.getCreatedTime(),
company.getModifiedTime()
);
}
public record SaveItem(
Long id,
String name,
Integer members
) {
}
public record ListItem(
Long id,
String name,
Integer members
) {
}
public record DetailItem(
Long id,
String name,
Integer members,
LocalDateTime createdTime,
LocalDateTime modifiedTime
) {
}
}

View File

@@ -1,84 +0,0 @@
package com.lanyuanxiaoyao.service.template.controller;
import com.lanyuanxiaoyao.service.template.entity.Employee;
import com.lanyuanxiaoyao.service.template.service.CompanyService;
import com.lanyuanxiaoyao.service.template.service.EmployeeService;
import java.time.LocalDateTime;
import java.util.function.Function;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("employee")
public class EmployeeController extends SimpleControllerSupport<Employee, EmployeeController.SaveItem, EmployeeController.ListItem, EmployeeController.DetailItem> {
private final CompanyService companyService;
public EmployeeController(EmployeeService service, CompanyService companyService) {
super(service);
this.companyService = companyService;
}
@Override
protected Function<SaveItem, Employee> saveItemMapper() {
return item -> {
var employee = new Employee();
employee.setId(item.id());
employee.setName(item.name());
employee.setAge(item.age());
employee.setRole(Employee.Role.USER);
employee.setCompany(companyService.detailOrThrow(item.companyId()));
return employee;
};
}
@Override
protected Function<Employee, ListItem> listItemMapper() {
return employee -> new ListItem(
employee.getId(),
employee.getName(),
employee.getAge(),
employee.getRole()
);
}
@Override
protected Function<Employee, DetailItem> detailItemMapper() {
return employee -> new DetailItem(
employee.getId(),
employee.getCompany().getId(),
employee.getName(),
employee.getAge(),
employee.getRole(),
employee.getCreatedTime(),
employee.getModifiedTime()
);
}
public record SaveItem(
Long id,
Long companyId,
String name,
Integer age,
Employee.Role role
) {
}
public record ListItem(
Long id,
String name,
Integer age,
Employee.Role role
) {
}
public record DetailItem(
Long id,
Long companyId,
String name,
Integer age,
Employee.Role role,
LocalDateTime createdTime,
LocalDateTime modifiedTime
) {
}
}

View File

@@ -1,90 +0,0 @@
package com.lanyuanxiaoyao.service.template.controller;
import com.lanyuanxiaoyao.service.template.entity.Report;
import com.lanyuanxiaoyao.service.template.service.EmployeeService;
import com.lanyuanxiaoyao.service.template.service.ReportService;
import java.time.LocalDateTime;
import java.util.function.Function;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("report")
public class ReportController extends SimpleControllerSupport<Report, ReportController.SaveItem, ReportController.ListItem, ReportController.DetailItem> {
private final EmployeeService employeeService;
public ReportController(ReportService service, EmployeeService employeeService) {
super(service);
this.employeeService = employeeService;
}
@Override
protected Function<SaveItem, Report> saveItemMapper() {
return item -> {
var report = new Report();
report.setId(item.id());
report.setScore(item.score());
report.setLevel(item.level());
report.setEmployeeId(item.employeeId());
return report;
};
}
@Override
protected Function<Report, ListItem> listItemMapper() {
return report -> {
var employee = employeeService.detailOrThrow(report.getEmployeeId());
return new ListItem(
report.getId(),
employee.getId(),
employee.getName(),
report.getScore(),
report.getLevel()
);
};
}
@Override
protected Function<Report, DetailItem> detailItemMapper() {
return report -> {
var employee = employeeService.detailOrThrow(report.getEmployeeId());
return new DetailItem(
report.getId(),
employee.getId(),
employee.getName(),
report.getScore(),
report.getLevel(),
report.getCreatedTime(),
report.getModifiedTime()
);
};
}
public record SaveItem(
Long id,
Double score,
Report.Level level,
Long employeeId
) {
}
public record ListItem(
Long id,
Long employeeId,
String employeeName,
Double score,
Report.Level level
) {
}
public record DetailItem(
Long id,
Long employeeId,
String employeeName,
Double score,
Report.Level level,
LocalDateTime createdTime,
LocalDateTime modifiedTime
) {
}
}

View File

@@ -1,65 +0,0 @@
package com.lanyuanxiaoyao.service.template.entity;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinTable;
import jakarta.persistence.OneToMany;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SoftDelete;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@SoftDelete
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Comment("企业")
public class Company extends SimpleEntity {
@Column(nullable = false)
@Comment("名称")
private String name;
@Column(nullable = false)
@Comment("成员数")
private Integer members;
@OneToMany(mappedBy = "company")
@ToString.Exclude
private Set<Employee> employees;
@ElementCollection
@JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Set<Industry> industries = new HashSet<>();
public enum Industry {
TECHNOLOGY,
FINANCE,
MEDIA,
SERVICE,
GOVERNMENT,
EDUCATION,
HEALTHCARE,
CONSTRUCTION,
RETAIL,
OTHER,
}
}

View File

@@ -1,70 +0,0 @@
package com.lanyuanxiaoyao.service.template.entity;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapKeyEnumerated;
import java.util.HashMap;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SoftDelete;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@SoftDelete
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Comment("员工")
public class Employee extends SimpleEntity {
@Column(nullable = false)
@Comment("名称")
private String name;
@Column(nullable = false)
@Comment("年龄")
private Integer age;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
@Comment("角色")
private Role role;
@ManyToOne
@JoinColumn(nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@ToString.Exclude
private Company company;
@ElementCollection
@JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@MapKeyEnumerated(EnumType.STRING)
@Column(nullable = false)
private Map<ConnectionType, String> connections = new HashMap<>();
public enum Role {
USER,
ADMIN,
}
public enum ConnectionType {
EMAIL,
PHONE,
ADDRESS,
}
}

View File

@@ -1,44 +0,0 @@
package com.lanyuanxiaoyao.service.template.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SoftDelete;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@SoftDelete
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Comment("报告")
public class Report extends SimpleEntity {
@Column(nullable = false)
@Comment("分数")
private Double score = 0.0;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
@Comment("等级")
private Level level;
@Column(nullable = false)
@Comment("员工id")
private Long employeeId;
public enum Level {
A, B, C, D, E
}
}

View File

@@ -1,9 +0,0 @@
package com.lanyuanxiaoyao.service.template.entity.vo;
public record EmployeeWithCompanyName(
String name,
String companyName,
Integer age,
String role
) {
}

View File

@@ -1,8 +0,0 @@
package com.lanyuanxiaoyao.service.template.repository;
import com.lanyuanxiaoyao.service.template.entity.Company;
import org.springframework.stereotype.Repository;
@Repository
public interface CompanyRepository extends SimpleRepository<Company> {
}

View File

@@ -1,24 +0,0 @@
package com.lanyuanxiaoyao.service.template.repository;
import com.lanyuanxiaoyao.service.template.entity.Employee;
import com.lanyuanxiaoyao.service.template.entity.vo.EmployeeWithCompanyName;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@SuppressWarnings("NullableProblems")
@Repository
public interface EmployeeRepository extends SimpleRepository<Employee> {
@EntityGraph(attributePaths = {"company"})
@Override
Optional<Employee> findOne(Specification<Employee> specification);
@Query(value = "select e.name, c.name, e.age, e.role from employee e, company c where e.company_id = c.id", nativeQuery = true)
List<EmployeeWithCompanyName> findAllEmployeeWithCompanyNameNative();
@Query("select new com.lanyuanxiaoyao.service.template.entity.vo.EmployeeWithCompanyName(employee.name, employee.company.name, employee.age, cast(employee.role as string)) from Employee employee")
List<EmployeeWithCompanyName> findAllEmployeeWithCompanyName();
}

View File

@@ -1,8 +0,0 @@
package com.lanyuanxiaoyao.service.template.repository;
import com.lanyuanxiaoyao.service.template.entity.Report;
import org.springframework.stereotype.Repository;
@Repository
public interface ReportRepository extends SimpleRepository<Report> {
}

View File

@@ -1,12 +0,0 @@
package com.lanyuanxiaoyao.service.template.service;
import com.lanyuanxiaoyao.service.template.entity.Company;
import com.lanyuanxiaoyao.service.template.repository.CompanyRepository;
import org.springframework.stereotype.Service;
@Service
public class CompanyService extends SimpleServiceSupport<Company> {
public CompanyService(CompanyRepository repository) {
super(repository);
}
}

View File

@@ -1,12 +0,0 @@
package com.lanyuanxiaoyao.service.template.service;
import com.lanyuanxiaoyao.service.template.entity.Employee;
import com.lanyuanxiaoyao.service.template.repository.EmployeeRepository;
import org.springframework.stereotype.Service;
@Service
public class EmployeeService extends SimpleServiceSupport<Employee> {
public EmployeeService(EmployeeRepository repository) {
super(repository);
}
}

View File

@@ -1,12 +0,0 @@
package com.lanyuanxiaoyao.service.template.service;
import com.lanyuanxiaoyao.service.template.entity.Report;
import com.lanyuanxiaoyao.service.template.repository.ReportRepository;
import org.springframework.stereotype.Service;
@Service
public class ReportService extends SimpleServiceSupport<Report> {
public ReportService(ReportRepository repository) {
super(repository);
}
}

View File

@@ -1,15 +0,0 @@
server:
port: 2490
spring:
application:
name: Test
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
username: test
password: test
driver-class-name: org.h2.Driver
jpa:
show-sql: true
generate-ddl: true
fenix:
print-banner: false