Merge branch 'refactor/4-dto-refactor-dto-s-paging-request-class' into 'main'

refactor(dto): 重构数据传输对象和查询相关类

See merge request gewuyou/forgeboot!9
This commit is contained in:
gewuyou 2025-07-19 09:59:49 +00:00
commit 52075708f8
28 changed files with 738 additions and 450 deletions

View File

@ -9,6 +9,12 @@ publish:
script: script:
- echo "🔧 授予 gradlew 执行权限..." - echo "🔧 授予 gradlew 执行权限..."
- chmod +x gradlew - chmod +x gradlew
- ./gradlew publishMavenJavaPublicationToGitLabRepository - ./gradlew printVersion
# 强制刷新 SNAPSHOT 缓存并发布
- ./gradlew clean publishMavenJavaPublicationToGitLabRepository \
--no-daemon \
--refresh-dependencies \
-Porg.gradle.caching=false \
-Dorg.gradle.configuration-cache=false
tags: tags:
- java - java

View File

@ -1,22 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto
import com.gewuyou.forgeboot.webmvc.dto.enums.SortDirection
/**
* 排序条件
*
* @author gewuyou
* @since 2025-01-16 16:11:47
*/
class SortCondition {
/**
* 排序字段
*/
var field: String = "createdAt"
/**
* 排序方向
*/
var direction: SortDirection = SortDirection.DESC
}

View File

@ -0,0 +1,30 @@
package com.gewuyou.forgeboot.webmvc.dto.entities
import java.time.Instant
/**
* 日期范围条件
*
* 表示一个用于描述日期范围的条件类包含字段名起始日期和结束日期
*
* @property fieldName 用于存储该日期范围所对应字段的名称
* @property startDate 日期范围的开始时间可为空默认值为 null
* @property endDate 日期范围的结束时间可为空默认值为 null
*
* @since 2025-07-19 12:11:53
* @author gewuyou
*/
data class DateRangeCondition(
val fieldName: String,
val startDate: Instant? = null,
val endDate: Instant? = null
) {
/**
* 验证日期范围的有效性
*
* 检查日期范围是否有效当起始日期和结束日期都存在时要求起始日期不能晚于结束日期
*
* @return 如果日期范围有效则返回 true否则返回 false
*/
fun isValid(): Boolean = startDate == null || endDate == null || !startDate.isAfter(endDate)
}

View File

@ -0,0 +1,21 @@
package com.gewuyou.forgeboot.webmvc.dto.entities
import org.hibernate.query.SortDirection
/**
* 排序条件
*
* @author gewuyou
* @since 2025-01-16 16:11:47
*/
class SortCondition {
/**
* 排序字段
*/
var property: String = "createdAt"
/**
* 排序方向
*/
var direction: SortDirection = SortDirection.DESCENDING
}

View File

@ -1,11 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.enums
/**
* 排序方向枚举
*
* @author gewuyou
* @since 2025-01-16 17:56:01
*/
enum class SortDirection {
ASC, DESC
}

View File

@ -0,0 +1,190 @@
package com.gewuyou.forgeboot.webmvc.dto.extension
import com.gewuyou.forgeboot.webmvc.dto.page.*
import jakarta.persistence.criteria.CriteriaBuilder
import jakarta.persistence.criteria.Predicate
import jakarta.persistence.criteria.Root
import org.hibernate.query.SortDirection
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.data.jpa.domain.Specification
import java.time.Instant
/**
* Hibernate SortDirection 转换为 Spring Data Sort.Direction
*
* @return 返回对应的 Sort.Direction 实例
*/
fun SortDirection.toSpringDirection(): Sort.Direction =
if (this == SortDirection.DESCENDING) Sort.Direction.DESC else Sort.Direction.ASC
/**
* 将当前排序配置转换为可用于 Spring Data Pageable 的排序条件列表
*
* @param defaultSort 当没有其他排序条件时使用的默认排序默认为按 createdAt 降序
* @return 适用于 Pageable 的排序条件列表
*
* 处理逻辑优先级
* 1. 如果存在多个排序条件使用 sortConditions 构建排序列表
* 2. 如果指定了单一排序字段使用 sortBy sortDirection 构建排序条件
* 3. 如果没有指定任何排序条件使用默认排序
*/
fun Sortable.toSortOrders(defaultSort: Sort.Order = Sort.Order.desc("createdAt")): List<Sort.Order> {
return when {
sortConditions.isNotEmpty() -> {
// 使用 map 将每个 SortCondition 转换为 Spring 的 Sort.Order 对象
sortConditions.map {
Sort.Order(
it.direction.toSpringDirection(),
it.property
)
}
}
sortBy.isNotBlank() -> {
// 当存在单一排序字段时,创建对应的排序条件列表
listOf(Sort.Order(sortDirection.toSpringDirection(), sortBy))
}
else -> listOf(defaultSort)
}
}
/**
* 构建关键字搜索对应的 JPA Predicate 条件列表
*
* 该扩展方法用于为实现了 KeywordSearchable 接口的对象构建 JPA 查询中的关键字搜索条件
* 遍历 keywordSearchFields() 提供的字段列表对每个字段执行不区分大小写的模糊匹配查询
*
* @param root 实体类的 JPA 查询根对象用于获取实体字段
* @param cb JPA CriteriaBuilder 实例用于构建查询条件
* @return 返回构建的 [Predicate] 条件列表若关键字为空则返回空列表
*
* 重要逻辑说明
* 1. 如果 keyword 为空或空白字符串直接返回空列表跳过所有构建逻辑
* 2. 对每个 keywordSearchFields() 提供的字段尝试构建模糊匹配条件
* 3. 使用 lower() 方法实现不区分大小写的比较
* 4. 如果字段路径获取失败如字段不存在捕获异常并跳过该字段
*/
fun <T> KeywordSearchable.buildKeywordPredicates(
root: Root<T>,
cb: CriteriaBuilder,
): List<Predicate> {
val kw = keyword?.trim()?.takeIf { it.isNotEmpty() } ?: return emptyList()
return keywordSearchFields().mapNotNull { field ->
try {
val path = root.get<String>(field)
cb.like(cb.lower(path), "%${kw.lowercase()}%")
} catch (_: Exception) {
null
}
}
}
/**
* 根据 DateRangeFilterable 构建 JPA 日期范围谓词
*
* 该扩展方法用于为实现了 DateRangeFilterable 接口的对象构建 JPA 查询中的日期范围过滤条件
* 遍历 dataRangeFields 字段列表为每个字段根据提供的 startDate endDate 创建对应的谓词条件
*
* @param root 实体类的 JPA 查询根对象用于获取实体字段
* @param cb JPA CriteriaBuilder 实例用于构建查询条件
* @return 返回构建的 [Predicate] 条件列表若未设置日期范围则返回空列表
*/
fun <T> DateRangeFilterable.buildDateRangePredicates(
root: Root<T>,
cb: CriteriaBuilder,
): List<Predicate> {
val predicates = mutableListOf<Predicate>()
this.dateRanges().forEach { condition ->
/**
* 获取数据库字段的 Instant 类型路径用于构建时间范围查询条件
*/
val dbField = root.get<Instant>(condition.fieldName)
/**
* 如果 startDate 不为 null添加大于等于该日期时间的谓词条件
*/
predicates.addIfNotNull(condition.startDate) {
cb.greaterThanOrEqualTo(dbField, it)
}
/**
* 如果 endDate 不为 null添加小于等于该日期时间的谓词条件
*/
predicates.addIfNotNull(condition.endDate) {
cb.lessThanOrEqualTo(dbField, it)
}
}
return predicates
}
/**
* QueryComponent 转换为 JPA 查询所需的 Specification查询规范
*
* 该扩展方法将实现了不同过滤接口的 QueryComponent 对象转换为 Spring Data JPA 兼容的 Specification
* 转换过程包含以下主要步骤
* 1. 检查对象是否实现了 DateRangeFilterable 接口并构建日期范围查询条件
* 2. 检查对象是否实现了 StatusFilterable 接口并构建状态过滤查询条件
* 3. 检查对象是否实现了 KeywordSearchable 接口并构建关键字搜索查询条件
* 4. 检查对象是否实现了 Filterable 接口并使用其自定义的查询规范
* 5. 将所有查询条件组合为一个逻辑 AND 条件返回
*
* @return 返回构建的 Specification<T> 查询规范
*/
fun <T> QueryComponent.toSpecification(): Specification<T> {
return Specification { root, _, cb ->
val predicates = mutableListOf<Predicate>()
// 判断并组合 DateRange
(this as? DateRangeFilterable)?.let {
predicates += it.buildDateRangePredicates(root, cb)
}
// 判断并组合 Status
(this as? StatusFilterable)?.let {
predicates += it.buildStatusPredicates(root, cb)
}
// 判断并组合 KeywordSearchable
(this as? KeywordSearchable)?.let {
predicates += it.buildKeywordPredicates(root, cb)
}
// 判断并组合 Filterable
@Suppress("UNCHECKED_CAST")
(this as? Filterable<*, T>)?.let {
predicates += it.buildSpecification(root, cb)
}
// 组合所有查询条件,返回逻辑 AND 结果
cb.and(*predicates.toTypedArray())
}
}
/**
* QueryComponent 转换为 JPA 查询所需的 Specification 和分页请求对象
*
* 该扩展方法主要用于将自定义的查询组件对象转换为 Spring Data JPA 兼容的查询规范和分页参数
* 转换过程包含以下主要步骤
* 1. 通过 toSpecification 方法构建查询条件规范 (Specification)
* 2. 从对象中提取分页参数 (Pageable)并转换为 Spring Data PageRequest 实例
* 3. 从对象中提取排序信息 (Sortable)并结合 toSortOrders 方法构建排序条件
*
* @return 返回一个 Pair包含以下两个元素
* - Specification<T>JPA 查询条件规范
* - PageRequest包含分页和排序信息的请求对象
*
* 重要逻辑说明
* 1. currentPage 1 开始计数转换为 Spring Data 0 开始的页码
* 2. pageSize 保持不变直接用于分页请求
* 3. 如果对象实现了 Sortable 接口则使用其排序条件否则使用未排序状态
* 4. 如果对象未实现 Pageable 接口则使用默认分页参数第一页每页10条
*/
fun <T> QueryComponent.toJpaQuery(): Pair<Specification<T>, PageRequest> {
val specification = this.toSpecification<T>()
val pageable = this as? Pageable
val sortable = this as? Sortable
// 默认分页参数
val page = pageable?.let { (it.currentPage - 1).coerceAtLeast(0) } ?: 0
val size = pageable?.pageSize ?: 10
// 排序规则
val sort = sortable?.let { Sort.by(it.toSortOrders()) } ?: Sort.unsorted()
return specification to PageRequest.of(page, size, sort)
}

View File

@ -1,140 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.extension
import com.gewuyou.forgeboot.webmvc.dto.enums.SortDirection
import com.gewuyou.forgeboot.webmvc.dto.request.PageQueryReq
import jakarta.persistence.criteria.CriteriaBuilder
import jakarta.persistence.criteria.Predicate
import jakarta.persistence.criteria.Root
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
/**
* 标记属性用于模糊匹配查询条件
*
* 该注解应用于实体类属性表示在构建查询条件时使用LIKE操作符进行模糊匹配
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Like
/**
* 标记属性用于精确匹配查询条件
*
* 该注解应用于实体类属性表示在构建查询条件时使用EQUAL操作符进行精确匹配
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Equal
/**
* 标记属性用于IN集合查询条件
*
* 该注解应用于实体类属性表示在构建查询条件时使用IN操作符进行集合匹配
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class In
/**
* 标记属性用于OR逻辑分组
*
* 该注解应用于实体类属性表示该属性的查询条件需要与其他标记相同value值的属性
* 进行OR逻辑组合不同value值的组之间使用AND逻辑连接
*
* @property value 分组标识符相同value的注解属性会被组合为一个OR条件组
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class OrGroup(val value: String)
/**
* 分页查询扩展类
*
* 提供分页和查询条件构建的扩展方法用于将 PageQueryReq 转换为 Spring Data 的分页对象
* 并生成通用的查询条件谓词列表
*
* @since 2025-01-17
* @author gewuyou
*/
fun <T> PageQueryReq<T>.toPageable(defaultSort: Sort.Order = Sort.Order.desc("createdAt")): Pageable {
val orders = when {
// 检查排序条件
sortConditions.isNotEmpty() -> {
sortConditions.map {
if (it.direction == SortDirection.ASC) Sort.Order.asc(it.field) else Sort.Order.desc(it.field)
}
}
// 检查是否有单字段排序
sortBy.isNotBlank() -> {
listOf(
if (sortDirection == SortDirection.ASC) Sort.Order.asc(sortBy) else Sort.Order.desc(sortBy)
)
}
// 如果都没有则默认按创建时间排序
else -> listOf(defaultSort)
}
return PageRequest.of(currentPage - 1, pageSize, Sort.by(orders))
}
/**
* 构建查询条件谓词列表
*
* 根据请求参数生成标准查询条件包括时间范围启用状态和删除状态等常用过滤条件
*
* @param builder CriteriaBuilder 标准查询构造器
* @param root Root<S> 查询的根对象
* @param createAtName 创建时间字段名默认 "createdAt"
* @param enabledName 是否启用字段名默认 "enabled"
* @param deletedName 是否删除字段名默认 "deleted"
* @return MutableList<Predicate> 查询条件谓词列表
*/
fun <T, S> PageQueryReq<T>.getPredicates(
builder: CriteriaBuilder,
root: Root<S>,
createAtName: String = "createdAt",
enabledName: String = "enabled",
deletedName: String = "deleted"
): MutableList<Predicate> {
val predicates = mutableListOf<Predicate>()
// 添加开始日期条件(容错)
startDate?.let {
try {
predicates.add(builder.greaterThanOrEqualTo(root.get(createAtName), it))
} catch (_: IllegalArgumentException) {
// 字段不存在,忽略
}
}
// 添加结束日期条件(容错)
endDate?.let {
try {
predicates.add(builder.lessThanOrEqualTo(root.get(createAtName), it))
} catch (_: IllegalArgumentException) {
// 字段不存在,忽略
}
}
// 添加是否启用条件(通常字段存在,可不做容错)
enabled?.let {
try {
predicates.add(builder.equal(root.get<Boolean>(enabledName), it))
} catch (_: IllegalArgumentException) {
// 可选容错
}
}
// 添加是否删除条件(通常字段存在,可不做容错)
deleted.let {
try {
predicates.add(builder.equal(root.get<Boolean>(deletedName), it))
} catch (_: IllegalArgumentException) {
// 可选容错
}
}
return predicates
}

View File

@ -0,0 +1,25 @@
package com.gewuyou.forgeboot.webmvc.dto.page
import com.gewuyou.forgeboot.webmvc.dto.entities.DateRangeCondition
/**
* 日期范围可过滤接口
* 用于定义支持日期范围过滤的对象结构包含开始时间和结束时间属性
*
* 该接口继承自QueryComponent用于构建支持多日期范围过滤条件的查询组件
* 实现该接口的类需要提供具体的日期范围条件集合
*
* @since 2025-07-19 08:59:44
* @author gewuyou
*/
fun interface DateRangeFilterable : QueryComponent {
/**
* 获取所有日期范围条件集合
*
* 该方法返回一个包含所有日期范围条件的列表每个元素都是一个DateRangeCondition对象
* 表示一个独立的日期范围过滤条件
*
* @return List<DateRangeCondition> 类型的列表包含所有的日期范围条件
*/
fun dateRanges(): List<DateRangeCondition>
}

View File

@ -0,0 +1,38 @@
package com.gewuyou.forgeboot.webmvc.dto.page
import jakarta.persistence.criteria.CriteriaBuilder
import jakarta.persistence.criteria.Predicate
import jakarta.persistence.criteria.Root
/**
* 可过滤接口用于支持泛型类型的过滤操作
*
* 该接口定义了过滤功能的基本契约允许实现类根据给定的过滤条件构建查询规范
* 主要用于与 JPA Criteria API 集成以构建动态查询条件
*
* @param <FilterType> 过滤条件的类型通常是一个包含过滤参数的数据传输对象DTO
* @param <EntityType> 被查询的实体类型通常对应数据库中的某个实体类
*
* @since 2025-07-19 08:56:56
* @author gewuyou
*/
interface Filterable<FilterType, EntityType> : QueryComponent {
/**
* 获取当前的过滤条件对象
*
* @return 返回一个可空的 FilterType 实例表示当前的过滤条件
*/
val filter: FilterType?
/**
* 构建查询条件的谓词列表
*
* 此方法根据当前的过滤条件使用给定的 CriteriaBuilder 创建一组 Predicate 对象
* 这些谓词可以用于构建最终查询语句
*
* @param root 代表查询的根实体用于访问实体的属性
* @param cb CriteriaBuilder 实例用于创建查询条件
* @return 返回一个包含查询条件的 Predicate 列表
*/
fun buildSpecification(root: Root<EntityType>, cb: CriteriaBuilder): List<Predicate>
}

View File

@ -0,0 +1,29 @@
package com.gewuyou.forgeboot.webmvc.dto.page
/**
* 关键字可搜索接口用于支持包含关键字搜索条件的数据结构
*
* 该接口定义了关键字搜索所需的基础结构包括关键字本身和需要进行模糊匹配的字段列表
* 实现该接口的数据结构可以在查询中支持基于关键字的模糊匹配功能
*
* @since 2025-07-19 08:56:31
* @author gewuyou
*/
interface KeywordSearchable : QueryComponent {
/**
* 关键字用于模糊匹配
*
* 如果值为 null 或空字符串则表示不需要进行关键字过滤
*/
val keyword: String?
/**
* 获取需要被关键字模糊匹配的字段列表
*
* 返回的字段列表用于构建模糊查询条件每个字段将与关键字进行模糊匹配
* 字段名称应为数据结构中的实际字段名且应支持模糊匹配操作
*
* @return 需要进行模糊匹配的字段列表不为 null
*/
fun keywordSearchFields(): List<String>
}

View File

@ -0,0 +1,26 @@
package com.gewuyou.forgeboot.webmvc.dto.page
/**
* 可分页接口用于支持分页功能的类实现
*
* @since 2025-07-19 08:52:58
* @author gewuyou
*/
interface Pageable : QueryComponent {
/**
* 当前页码从1开始计数
*/
val currentPage: Int
/**
* 每页显示的数据条数
*/
val pageSize: Int
/**
* 计算并返回当前页的起始偏移量
*
* @return 返回计算后的偏移量用于数据库查询等场景
*/
fun getOffset(): Int = (currentPage - 1) * pageSize
}

View File

@ -0,0 +1,7 @@
package com.gewuyou.forgeboot.webmvc.dto.page
/**
* QueryComponent 是一个密封接口用于定义查询组件的公共契约
* 密封接口的实现类必须与接口本身在同一个包中以确保接口的封闭性和安全性
*/
sealed interface QueryComponent

View File

@ -0,0 +1,27 @@
package com.gewuyou.forgeboot.webmvc.dto.page
import com.gewuyou.forgeboot.webmvc.dto.entities.SortCondition
import org.hibernate.query.SortDirection
/**
* 可排序接口用于定义排序相关属性
*
* @since 2025-07-19 08:53:56
* @author gewuyou
*/
interface Sortable : QueryComponent {
/**
* 获取排序字段名称
*/
val sortBy: String
/**
* 获取排序方向升序或降序
*/
val sortDirection: SortDirection
/**
* 获取排序条件列表用于支持多条件排序
*/
val sortConditions: List<SortCondition>
}

View File

@ -0,0 +1,29 @@
package com.gewuyou.forgeboot.webmvc.dto.page
import jakarta.persistence.criteria.CriteriaBuilder
import jakarta.persistence.criteria.Predicate
import jakarta.persistence.criteria.Root
/**
* 启用状态可过滤接口用于支持启用/禁用状态的过滤功能
*
* 此接口定义了构建状态过滤条件的方法适用于基于 JPA Criteria API 的查询构建
* 实现该接口的类可通过 [buildStatusPredicates]方法提供具体过滤逻辑
*
* @since 2025-07-19 09:00:42
* @author gewuyou
*/
interface StatusFilterable : QueryComponent {
/**
* 构建与启用状态相关的查询条件列表
*
* 该方法用于生成一个 Predicate 列表用于 JPA Criteria 查询中对实体的状态字段进行过滤
* 通常用于支持启用/禁用状态的动态查询场景
*
* @param <T> 查询所涉及的实体类型
* @param root Criteria 查询的根对象用于访问实体属性
* @param cb CriteriaBuilder 实例用于构建查询条件
* @return 构建完成的 Predicate 条件列表
*/
fun <T> buildStatusPredicates(root: Root<T>, cb: CriteriaBuilder): List<Predicate>
}

View File

@ -0,0 +1,20 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.gewuyou.forgeboot.webmvc.dto.page.DateRangeFilterable
import com.gewuyou.forgeboot.webmvc.dto.page.Pageable
/**
* 抽象日期分页请求
*
* 用于封装带有日期范围过滤的分页请求参数实现分页和日期范围过滤功能的统一请求数据结构
*
* @property currentPage 当前页码默认值为1用于标识请求的页码位置
* @property pageSize 每页记录数默认值为10用于控制分页时每页返回的数据量
*
* @since 2025-07-19 10:19:23
* @author gewuyou
*/
abstract class AbstractDatePageRequest(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
) : Pageable, DateRangeFilterable

View File

@ -0,0 +1,21 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.gewuyou.forgeboot.webmvc.dto.page.DateRangeFilterable
import com.gewuyou.forgeboot.webmvc.dto.page.Pageable
import com.gewuyou.forgeboot.webmvc.dto.page.StatusFilterable
/**
* 日期状态页面请求
* 用于封装包含分页日期范围和状态过滤条件的复合请求参数
*
* @property currentPage 当前页码从1开始计数的分页参数
* @property pageSize 每页记录数量控制分页大小
* @property statusConditions 状态条件映射键值对形式表示的状态过滤条件
*
* @since 2025-07-19 09:15:12
* @author gewuyou
*/
abstract class AbstractDateStatusPageRequest(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
) : Pageable, DateRangeFilterable, StatusFilterable

View File

@ -0,0 +1,23 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.gewuyou.forgeboot.webmvc.dto.page.Filterable
import com.gewuyou.forgeboot.webmvc.dto.page.Pageable
/**
* 抽象过滤的分页请求
*
* 该类实现了分页和过滤功能的请求数据封装用于构建带过滤条件的分页查询
*
* @param currentPage 当前页码从1开始计数默认为1
* @param pageSize 每页显示的数据数量默认为10
* @param filter 过滤条件对象泛型类型T的可空实例默认为null
*
* @since 2025-07-19 09:16:36
* @author gewuyou
*/
abstract class AbstractFilteredPageRequest<T,E>(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
override val filter: T? = null,
) : Pageable, Filterable<T,E>

View File

@ -0,0 +1,33 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.gewuyou.forgeboot.webmvc.dto.page.*
/**
* 抽象分页请求类用于封装分页查询的通用参数
*
* 该类实现了多种分页相关接口支持分页排序过滤关键词搜索状态条件筛选和日期范围过滤功能
*
* @param T 泛型类型参数表示自定义过滤条件的数据类型
*
* @property currentPage 当前页码默认值为1
* @property pageSize 每页记录数默认值为10
* @property sortBy 排序字段默认值为"createdAt"
* @property sortDirection 排序方向默认值为降序(SortDirection.DESCENDING)
* @property sortConditions 排序条件列表默认为空列表
* @property keyword 关键词搜索内容可为空
* @property filter 自定义过滤条件可为空类型为泛型T
*
* @author gewuyou
* @since 2025-07-19 10:25:55
*/
abstract class AbstractFullPageRequest<T,E>(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
override val keyword: String? = null,
override val filter: T? = null,
) : Pageable,
Sortable,
KeywordSearchable,
Filterable<T,E>,
DateRangeFilterable,
StatusFilterable

View File

@ -0,0 +1,26 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.gewuyou.forgeboot.webmvc.dto.page.Filterable
import com.gewuyou.forgeboot.webmvc.dto.page.KeywordSearchable
import com.gewuyou.forgeboot.webmvc.dto.page.Pageable
/**
* 抽象搜索过滤分页请求
*
* 该类实现了分页关键词搜索和过滤功能的通用请求对象
* 作为基类用于构建具有统一分页和搜索能力的请求数据结构
*
* @param currentPage 当前页码从1开始默认为1
* @param pageSize 每页记录数默认为10
* @param keyword 关键词搜索内容可为空
* @param filter 过滤条件对象泛型类型可为空
*
* @since 2025-07-19 10:22:41
* @author gewuyou
*/
abstract class AbstractSearchFilterPageRequest<T,E>(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
override val keyword: String? = null,
override val filter: T? = null,
) : Pageable, KeywordSearchable, Filterable<T,E>

View File

@ -0,0 +1,23 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.gewuyou.forgeboot.webmvc.dto.page.KeywordSearchable
import com.gewuyou.forgeboot.webmvc.dto.page.Pageable
/**
* 抽象搜索分页请求类
*
* 该类用于封装分页搜索请求的基本参数包括当前页码页大小和关键字
* 通过实现 [Pageable] [KeywordSearchable] 接口提供分页和关键字搜索功能
*
* @property currentPage 当前页码默认为 1
* @property pageSize 每页条目数默认为 10
* @property keyword 搜索关键字可为空默认为 null
*
* @since 2025-07-19 09:14:28
* @author gewuyou
*/
abstract class AbstractSearchPageRequest(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
override val keyword: String? = null
) : Pageable, KeywordSearchable

View File

@ -0,0 +1,28 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.gewuyou.forgeboot.webmvc.dto.entities.SortCondition
import com.gewuyou.forgeboot.webmvc.dto.page.DateRangeFilterable
import com.gewuyou.forgeboot.webmvc.dto.page.Filterable
import com.gewuyou.forgeboot.webmvc.dto.page.Pageable
import com.gewuyou.forgeboot.webmvc.dto.page.Sortable
/**
* 抽象排序日期过滤分页请求基类
*
* 该抽象类为分页请求提供了基础结构支持以下功能
* - 分页控制通过Pageable接口
* - 排序功能通过Sortable接口
* - 日期范围过滤通过DateRangeFilterable接口
* - 通用数据过滤通过Filterable<T,E>接口
*
* @property currentPage 当前页码从1开始计数默认值为1
* @property pageSize 每页记录数默认值为10
*
* @since 2025-07-19 14:50:36
* @author gewuyou
*/
abstract class AbstractSortedDateFilterPageRequest<T,E>(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
override val sortConditions: List<SortCondition> = mutableListOf()
) : Pageable, Sortable, DateRangeFilterable, Filterable<T,E>

View File

@ -0,0 +1,23 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.gewuyou.forgeboot.webmvc.dto.page.DateRangeFilterable
import com.gewuyou.forgeboot.webmvc.dto.page.Pageable
import com.gewuyou.forgeboot.webmvc.dto.page.Sortable
/**
* 抽象排序日期分页请求
* 用于封装包含排序分页和日期范围过滤条件的请求参数
*
* @property currentPage 当前页码默认值为1
* @property pageSize 每页记录数默认值为10
* @property sortBy 排序字段名称默认值为"createdAt"
* @property sortDirection 排序方向默认值为降序(SortDirection.DESCENDING)
* @property sortConditions 排序条件集合默认值为空列表
*
* @since 2025-07-19 10:23:05
* @author gewuyou
*/
abstract class AbstractSortedDatePageRequest(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
) : Pageable, Sortable, DateRangeFilterable

View File

@ -0,0 +1,22 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.gewuyou.forgeboot.webmvc.dto.page.Pageable
import com.gewuyou.forgeboot.webmvc.dto.page.Sortable
/**
* 抽象排序分页请求数据类
*
* 该类用于封装带有排序功能的分页请求参数包含当前页码每页大小默认排序字段
* 排序方向以及扩展的排序条件集合通过实现 [Pageable] [Sortable] 接口
* 提供标准化的分页与排序属性
*
* @property currentPage 当前页码从1开始默认值为1
* @property pageSize 每页记录数默认值为10
*
* @since 2025-07-19 09:13:19
* @author gewuyou
*/
abstract class AbstractSortedPageRequest(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
) : Pageable, Sortable

View File

@ -0,0 +1,20 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.gewuyou.forgeboot.webmvc.dto.page.Pageable
/**
* 基本分页请求数据类
*
* 该类用于封装分页请求的基础信息包括当前页码和每页记录数
* 通过实现 Pageable 接口支持与分页处理相关的操作
*
* @property currentPage 当前页码默认值为 1
* @property pageSize 每页记录数默认值为 10
*
* @since 2025-07-19 09:12:22
* @author gewuyou
*/
data class BasicPageRequest(
override val currentPage: Int = 1,
override val pageSize: Int = 10
) : Pageable

View File

@ -1,93 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.request
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.gewuyou.forgeboot.webmvc.dto.SortCondition
import com.gewuyou.forgeboot.webmvc.dto.enums.SortDirection
import jakarta.validation.constraints.Min
import java.time.Instant
/**
* 分页查询条件请求实体类
*
* 用于通用分页排序关键字搜索日期范围与状态过滤
* 支持自定义泛型过滤器实体
*
* @author gewuyou
* @since 2025-01-16 16:01:12
*/
@JsonIgnoreProperties(ignoreUnknown = true)
open class PageQueryReq<T> {
/**
* 当前页码(默认1)
*/
@field:Min(1)
var currentPage: Int = 1
/**
* 每页条数(默认10)
*/
@field:Min(1)
var pageSize: Int = 10
/**
* 排序字段(单一字段)
*/
var sortBy: String = "createdAt"
/**
* 排序方向(单一字段ASC或DESC)
*/
var sortDirection: SortDirection = SortDirection.DESC
/**
* 排序条件实体类支持多字段排序
*/
var sortConditions: MutableList<SortCondition> = mutableListOf()
/**
* 关键字搜索常用于模糊查询
*/
var keyword: String? = null
/**
* 自定义过滤条件实体类
*/
var filter: T? = null
/**
* 开始日期
*/
var startDate: Instant? = null
/**
* 结束日期
*/
var endDate: Instant? = null
/**
* 是否启用
*/
var enabled: Boolean? = null
/**
* 是否删除
*/
var deleted: Boolean = false
/**
* 计算分页偏移量
*/
fun getOffset(): Int = (currentPage - 1) * pageSize
/**
* 校验日期范围是否合法开始时间不能晚于结束时间
*/
fun isDateRangeValid(): Boolean {
return startDate == null || endDate == null || !startDate!!.isAfter(endDate)
}
override fun toString(): String {
return "PageQueryReq(currentPage=$currentPage, pageSize=$pageSize, sortBy='$sortBy', sortDirection=$sortDirection, sortConditions=$sortConditions, keyword=$keyword, filter=$filter, startDate=$startDate, endDate=$endDate, enabled=$enabled, deleted=$deleted)"
}
}

View File

@ -1,104 +0,0 @@
package com.gewuyou.forgeboot.webmvc.dto.util
import com.gewuyou.forgeboot.webmvc.dto.extension.Equal
import com.gewuyou.forgeboot.webmvc.dto.extension.In
import com.gewuyou.forgeboot.webmvc.dto.extension.Like
import com.gewuyou.forgeboot.webmvc.dto.extension.OrGroup
import com.gewuyou.forgeboot.webmvc.dto.extension.getPredicates
import com.gewuyou.forgeboot.webmvc.dto.request.PageQueryReq
import jakarta.persistence.criteria.Path
import jakarta.persistence.criteria.Predicate
import jakarta.persistence.criteria.Root
import org.springframework.data.jpa.domain.Specification
import kotlin.collections.isNotEmpty
import kotlin.collections.toTypedArray
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
/**
* 动态构建 JPA Specification 的工具类
*
* 支持字段注解控制查询方式@Like, @Equal, @In
* 支持嵌套字段路径 "user.name"
* 支持 OR 条件组@OrGroup("groupName")
* @since 2025-05-28 23:37:17
* @author gewuyou
*/
object DynamicSpecificationBuilder {
/**
* 构建动态查询 Specification
*
* @param query PageQuery<T> 查询参数对象
* @param createAtName 时间字段名
* @param enabledName 是否启用字段名
* @param deletedName 是否删除字段名
*/
inline fun <reified T : Any, reified E> build(
query: PageQueryReq<T>,
createAtName: String = "createdAt",
enabledName: String = "enabled",
deletedName: String = "deleted",
): Specification<E> {
return Specification { root, _, builder ->
val predicates = mutableListOf<Predicate>()
// 1. 加入基本条件(时间、启用、删除)
predicates += query.getPredicates(builder, root, createAtName, enabledName, deletedName)
// 2. 处理 filter 字段
val filterObj = query.filter ?: return@Specification builder.and(*predicates.toTypedArray())
// 3. OR 分组
val orGroups = mutableMapOf<String, MutableList<Predicate>>()
T::class.memberProperties.forEach { prop ->
prop.isAccessible = true
val value = prop.get(filterObj) ?: return@forEach
val fieldPath = prop.name
val path = getPath(root, fieldPath)
val predicate = when {
prop.findAnnotation<Like>() != null && value is String ->
builder.like(path as Path<String>, "%$value%")
prop.findAnnotation<Equal>() != null ->
builder.equal(path, value)
prop.findAnnotation<In>() != null && value is Collection<*> ->
path.`in`(value)
// 默认策略:非空值执行 equal
else -> builder.equal(path, value)
}
val orGroup = prop.findAnnotation<OrGroup>()?.value
if (orGroup != null) {
orGroups.getOrPut(orGroup) { mutableListOf() }.add(predicate)
} else {
predicates.add(predicate)
}
}
// 4. 添加 OR 条件组
orGroups.values.forEach { group ->
if (group.isNotEmpty()) {
predicates.add(builder.or(*group.toTypedArray()))
}
}
builder.and(*predicates.toTypedArray())
}
}
/**
* 支持嵌套字段路径解析例如 "user.name"
*/
fun <E> getPath(root: Root<E>, pathStr: String): Path<*> {
return pathStr.split(".").fold(root as Path<*>) { path, part ->
path.get<Any>(part)
}
}
}

View File

@ -1,7 +1,7 @@
package com.gewuyou.forgeboot.webmvc.spec.service package com.gewuyou.forgeboot.webmvc.spec.service
import com.gewuyou.forgeboot.webmvc.dto.PageResult import com.gewuyou.forgeboot.webmvc.dto.PageResult
import com.gewuyou.forgeboot.webmvc.dto.request.PageQueryReq import com.gewuyou.forgeboot.webmvc.dto.page.QueryComponent
/** /**
* CRUD 服务规范 * CRUD 服务规范
@ -10,11 +10,10 @@ import com.gewuyou.forgeboot.webmvc.dto.request.PageQueryReq
* *
* @param Entity 实体类型 * @param Entity 实体类型
* @param Id 实体标识符类型 * @param Id 实体标识符类型
* @param Filter 查询过滤器类型
* @since 2025-05-29 20:18:22 * @since 2025-05-29 20:18:22
* @author gewuyou * @author gewuyou
*/ */
interface CrudServiceSpec<Entity: Any, Id: Any, Filter: Any> { interface CrudServiceSpec<Entity: Any, Id: Any> {
/** /**
* 根据ID获取实体 * 根据ID获取实体
* *
@ -101,29 +100,39 @@ interface CrudServiceSpec<Entity: Any, Id: Any, Filter: Any> {
*/ */
fun saveAll(entities: List<Entity>): List<Entity> fun saveAll(entities: List<Entity>): List<Entity>
/**
* 分页查询实体列表
*
* 通过提供的查询组件进行分页数据检索返回包含分页信息的结果对象
*
* @param query 查询组件包含分页和过滤条件等信息
* @return 返回分页结果对象包含当前页的数据列表总记录数等信息
*/
fun page(query: QueryComponent): PageResult<Entity>
/** /**
* 分页查询并映射结果 * 分页查询并映射结果
* *
* @param query 分页查询请求包含查询参数和分页信息 * 通过提供的查询组件进行分页数据检索并使用给定的映射函数将结果转换为另一种类型
* @return 返回映射后的分页结果 * 适用于需要将实体转换为DTO或其他形式的场景
*
* @param query 查询组件包含分页和过滤条件等信息
* @param mapping 将实体转换为目标类型的函数
* @return 返回分页结果对象包含转换后的数据列表总记录数等信息
*/ */
fun page(query: PageQueryReq<Filter>): PageResult<Entity> fun <V> pageMapped(query: QueryComponent, mapping: (Entity) -> V): PageResult<V>
/** /**
* 分页查询并使用给定函数映射结果 * 查询符合条件的记录总数
* *
* @param query 分页查询请求包含查询参数和分页信息 * 根据查询组件中的过滤条件统计匹配的记录数量
* @param mapping 映射函数用于将实体映射为其他类型 * 通常用于分页时计算总页数或显示记录总数
* @param <V> 映射后的类型
* @return 返回映射后的分页结果
*/
fun <V> pageMapped(query: PageQueryReq<Filter>, mapping: (Entity) -> V): PageResult<V>
/**
* 根据过滤条件统计实体数量
* *
* @param filter 查询过滤器 * @param query 查询组件包含过滤条件等信息
* @return 返回满足条件的实体数量 * @return 返回符合条件的记录总数
*/ */
fun count(filter: Filter): Long fun count(query: QueryComponent): Long
} }

View File

@ -2,13 +2,12 @@ package com.gewuyou.forgeboot.webmvc.spec.service.impl
import com.gewuyou.forgeboot.webmvc.dto.PageResult import com.gewuyou.forgeboot.webmvc.dto.PageResult
import com.gewuyou.forgeboot.webmvc.dto.extension.map import com.gewuyou.forgeboot.webmvc.dto.extension.map
import com.gewuyou.forgeboot.webmvc.dto.extension.toJpaQuery
import com.gewuyou.forgeboot.webmvc.dto.extension.toPageResult import com.gewuyou.forgeboot.webmvc.dto.extension.toPageResult
import com.gewuyou.forgeboot.webmvc.dto.extension.toPageable import com.gewuyou.forgeboot.webmvc.dto.extension.toSpecification
import com.gewuyou.forgeboot.webmvc.dto.request.PageQueryReq import com.gewuyou.forgeboot.webmvc.dto.page.QueryComponent
import com.gewuyou.forgeboot.webmvc.spec.repository.CrudRepositorySpec import com.gewuyou.forgeboot.webmvc.spec.repository.CrudRepositorySpec
import com.gewuyou.forgeboot.webmvc.spec.service.CrudServiceSpec import com.gewuyou.forgeboot.webmvc.spec.service.CrudServiceSpec
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.domain.Specification
/** /**
@ -17,9 +16,9 @@ import org.springframework.data.jpa.domain.Specification
* @since 2025-05-29 20:37:27 * @since 2025-05-29 20:37:27
* @author gewuyou * @author gewuyou
*/ */
abstract class CrudServiceImplSpec<Entity : Any, Id : Any, Filter : Any>( abstract class CrudServiceImplSpec<Entity : Any, Id : Any>(
private val repository: CrudRepositorySpec<Entity, Id>, private val repository: CrudRepositorySpec<Entity, Id>,
) : CrudServiceSpec<Entity, Id, Filter> { ) : CrudServiceSpec<Entity, Id> {
/** /**
* 根据实体的唯一标识符查找实体对象 * 根据实体的唯一标识符查找实体对象
@ -31,41 +30,6 @@ abstract class CrudServiceImplSpec<Entity : Any, Id : Any, Filter : Any>(
return repository.findById(id).orElse(null) return repository.findById(id).orElse(null)
} }
/**
* 执行分页查询并返回原始实体的分页结果
*
* @param query 分页查询请求包含过滤条件和分页信息
* @return 返回实体类型的分页结果
*/
override fun page(query: PageQueryReq<Filter>): PageResult<Entity> {
return repository
.findAll(
buildSpecification(query),
resolvePageable(query)
)
.toPageResult()
}
/**
* 将分页请求解析为 Pageable 对象
* 子类可重写此方法来自定义分页逻辑例如默认排序
*
* @param query 分页查询请求
* @return 分页参数
*/
protected open fun resolvePageable(query: PageQueryReq<Filter>): Pageable {
return query.toPageable()
}
/**
* 构建 JPA Specification 查询条件
*
* @param query 包含过滤条件的分页请求
* @return 返回构建好的 Specification 查询条件
*/
protected abstract fun buildSpecification(query: PageQueryReq<Filter>): Specification<Entity>
/** /**
* 获取所有实体列表 * 获取所有实体列表
* *
@ -154,32 +118,50 @@ abstract class CrudServiceImplSpec<Entity : Any, Id : Any, Filter : Any>(
} }
/** /**
* 执行分页查询并将结果使用给定的映射函数转换为其他类型 * 分页查询实体列表
* *
* @param query 分页查询请求包含查询参数和分页信息 * 通过提供的查询组件进行分页数据检索返回包含分页信息的结果对象
* @param mapping 映射函数用于将实体映射为其他类型 *
* @param <V> 映射后的目标类型 * @param query 查询组件包含分页和过滤条件等信息
* @return 返回映射后的分页结果 * @return 返回分页结果对象包含当前页的数据列表总记录数等信息
*/ */
override fun <V> pageMapped( override fun page(query: QueryComponent): PageResult<Entity> {
query: PageQueryReq<Filter>, val (specification, pageable) = query.toJpaQuery<Entity>()
mapping: (Entity) -> V, return repository.findAll(specification, pageable).toPageResult()
): PageResult<V> {
val page = repository.findAll(
buildSpecification(query),
resolvePageable(query)
)
return page.toPageResult().map(mapping)
} }
/** /**
* 根据给定的过滤条件统计实体数量 * 分页查询并映射结果
* *
* @param filter 查询过滤器 * 通过提供的查询组件进行分页数据检索并使用给定的映射函数将结果转换为另一种类型
* @return 返回满足条件的实体数量 * 适用于需要将实体转换为DTO或其他形式的场景
*
* @param query 查询组件包含分页和过滤条件等信息
* @param mapping 将实体转换为目标类型的函数
* @return 返回分页结果对象包含转换后的数据列表总记录数等信息
*/ */
override fun count(filter: Filter): Long { override fun <V> pageMapped(
return repository.count(buildSpecification(PageQueryReq<Filter>().apply { this.filter = filter })) query: QueryComponent,
mapping: (Entity) -> V,
): PageResult<V> {
val (specification, pageable) = query.toJpaQuery<Entity>()
return repository.findAll(specification, pageable)
.toPageResult()
.map(mapping)
}
/**
* 查询符合条件的记录总数
*
* 根据查询组件中的过滤条件统计匹配的记录数量
* 通常用于分页时计算总页数或显示记录总数
*
* @param query 查询组件包含过滤条件等信息
* @return 返回符合条件的记录总数
*/
override fun count(query: QueryComponent): Long {
val specification = query.toSpecification<Entity>()
return repository.count(specification)
} }
/** /**