mirror of
https://github.moeyy.xyz/https://github.com/GeWuYou/forgeboot
synced 2025-10-28 02:12:08 +08:00
feat(dto): Added DTO classes related to pagination query and delete requests
- The SortDirection enumeration class has been added to sort directions - Added the PageQueryReq pagination query request class - Added the DeleteByIdsReq deletion request class - Added the DynamicSpecificationBuilder dynamic query building tool class - Added the PageResult pagination result class - Added the R Unified Response Encapsulation class - Added the SortCondition sorting condition class - Added relevant internationalization resource files
This commit is contained in:
parent
b41d9b5c46
commit
d31e47d1f8
3
forgeboot-webmvc/dto/.gitattributes
vendored
Normal file
3
forgeboot-webmvc/dto/.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/gradlew text eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.jar binary
|
||||
40
forgeboot-webmvc/dto/.gitignore
vendored
Normal file
40
forgeboot-webmvc/dto/.gitignore
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Kotlin ###
|
||||
.kotlin
|
||||
15
forgeboot-webmvc/dto/build.gradle.kts
Normal file
15
forgeboot-webmvc/dto/build.gradle.kts
Normal file
@ -0,0 +1,15 @@
|
||||
plugins{
|
||||
alias(libs.plugins.forgeboot.i18n.keygen)
|
||||
}
|
||||
dependencies {
|
||||
api(project(Modules.I18n.API))
|
||||
api(project(Modules.TRACE.API))
|
||||
implementation(libs.kotlinReflect)
|
||||
compileOnly(libs.springBootDependencies.bom)
|
||||
compileOnly(libs.jackson.annotations)
|
||||
compileOnly(libs.springBootStarter.jpa)
|
||||
compileOnly(libs.springBootStarter.validation)
|
||||
}
|
||||
i18nKeyGen {
|
||||
rootPackage.set("com.gewuyou.forgeboot.webmvc.dto.i18n")
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package com.gewuyou.forgeboot.webmvc.dto
|
||||
|
||||
/**
|
||||
* 分页结果返回对象
|
||||
*
|
||||
* 通常用于封装分页查询的返回结果,包括总数、页码等信息。
|
||||
*
|
||||
* @author gewuyou
|
||||
* @since 2024-04-23 下午10:53:04
|
||||
*/
|
||||
open class PageResult<T> {
|
||||
|
||||
/**
|
||||
* 当前页的记录列表
|
||||
*/
|
||||
var records: MutableList<T>? = mutableListOf()
|
||||
|
||||
/**
|
||||
* 总记录数(整个数据集的记录总数)
|
||||
*/
|
||||
var totalRecords: Long = 0L
|
||||
|
||||
/**
|
||||
* 总页数(根据每页大小计算出的总页数)
|
||||
*/
|
||||
var totalPages: Long = 0L
|
||||
|
||||
/**
|
||||
* 当前页码(从1开始计数)
|
||||
*/
|
||||
var currentPage: Long = 1L
|
||||
|
||||
/**
|
||||
* 每页显示的记录数量
|
||||
*/
|
||||
var pageSize: Long = 10L
|
||||
|
||||
/**
|
||||
* 是否存在上一页
|
||||
*/
|
||||
var hasPrevious: Boolean = false
|
||||
|
||||
/**
|
||||
* 是否存在下一页
|
||||
*/
|
||||
var hasNext: Boolean = false
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* 创建一个空的分页结果实例
|
||||
*
|
||||
* @return 新的PageResult实例
|
||||
*/
|
||||
fun <T> of(): PageResult<T> = PageResult()
|
||||
|
||||
/**
|
||||
* 根据指定参数创建分页结果
|
||||
*
|
||||
* @param currentPage 当前页码
|
||||
* @param pageSize 每页大小
|
||||
* @param totalRecords 总记录数
|
||||
* @param records 当前页的记录列表
|
||||
* @return 填充好的PageResult实例
|
||||
*/
|
||||
fun <T> of(currentPage: Long, pageSize: Long, totalRecords: Long, records: MutableList<T>?): PageResult<T> {
|
||||
val result = PageResult<T>()
|
||||
result.records = records
|
||||
result.totalRecords = totalRecords
|
||||
result.pageSize = pageSize
|
||||
result.currentPage = currentPage
|
||||
result.totalPages = (totalRecords + pageSize - 1) / pageSize
|
||||
result.hasPrevious = currentPage > 1
|
||||
result.hasNext = currentPage < result.totalPages
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定参数创建分页结果(支持Int参数)
|
||||
*
|
||||
* @param currentPage 当前页码
|
||||
* @param pageSize 每页大小
|
||||
* @param totalRecords 总记录数
|
||||
* @param records 当前页的记录列表
|
||||
* @return 填充好的PageResult实例
|
||||
*/
|
||||
fun <T> of(currentPage: Int, pageSize: Int, totalRecords: Long, records: MutableList<T>?): PageResult<T> {
|
||||
return of(currentPage.toLong(), pageSize.toLong(), totalRecords, records)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个仅设置页码和页面大小的分页结果
|
||||
*
|
||||
* @param currentPage 当前页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 部分填充的PageResult实例
|
||||
*/
|
||||
fun <T> of(currentPage: Long, pageSize: Long): PageResult<T> {
|
||||
val result = PageResult<T>()
|
||||
result.currentPage = currentPage
|
||||
result.pageSize = pageSize
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个空的分页结果实例
|
||||
*
|
||||
* @return 新的PageResult实例
|
||||
*/
|
||||
fun <T> empty(): PageResult<T> = PageResult()
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,47 @@
|
||||
package com.gewuyou.forgeboot.common.result
|
||||
package com.gewuyou.forgeboot.webmvc.dto
|
||||
|
||||
import com.gewuyou.forgeboot.common.result.api.MessageResolver
|
||||
import com.gewuyou.forgeboot.common.result.api.RequestIdProvider
|
||||
import com.gewuyou.forgeboot.common.result.api.ResponseInformation
|
||||
import com.gewuyou.forgeboot.common.result.api.ResultExtender
|
||||
import com.gewuyou.forgeboot.common.result.impl.DefaultMessageResolver
|
||||
import com.gewuyou.forgeboot.common.result.impl.DefaultRequestIdProvider
|
||||
|
||||
import com.gewuyou.forgeboot.i18n.api.MessageResolver
|
||||
import com.gewuyou.forgeboot.i18n.api.ResponseInformation
|
||||
import com.gewuyou.forgeboot.trace.api.RequestIdProvider
|
||||
|
||||
|
||||
/**
|
||||
*默认请求ID提供商
|
||||
*
|
||||
* @since 2025-05-03 16:22:18
|
||||
* @author gewuyou
|
||||
*/
|
||||
val DefaultRequestIdProvider : RequestIdProvider = RequestIdProvider{""}
|
||||
|
||||
/**
|
||||
*默认消息解析器
|
||||
*
|
||||
* @since 2025-05-03 16:21:43
|
||||
* @author gewuyou
|
||||
*/
|
||||
val DefaultMessageResolver : MessageResolver = MessageResolver { code, _ -> code }
|
||||
|
||||
/**
|
||||
* 结果扩展器
|
||||
*
|
||||
* 用于扩展结果映射,通过实现此接口,可以自定义逻辑以向结果映射中添加、修改或删除元素
|
||||
* 主要用于在某个处理流程结束后,对结果数据进行额外的处理或装饰
|
||||
*
|
||||
* @since 2025-05-03 16:08:55
|
||||
* @author gewuyou
|
||||
*/
|
||||
fun interface ResultExtender {
|
||||
/**
|
||||
* 扩展结果映射
|
||||
*
|
||||
* 实现此方法以执行扩展逻辑,可以访问并修改传入的结果映射
|
||||
* 例如,可以用于添加额外的信息,修改现有值,或者根据某些条件删除条目
|
||||
*
|
||||
* @param resultMap 一个包含结果数据的可变映射,可以在此方法中对其进行修改
|
||||
*/
|
||||
fun extend(resultMap: MutableMap<String, Any?>)
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一响应封装类
|
||||
@ -18,7 +54,7 @@ data class R<T>(
|
||||
val message: String,
|
||||
val data: T? = null,
|
||||
val requestId: String? = null,
|
||||
val extra: Map<String, Any?> = emptyMap()
|
||||
val extra: Map<String, Any?> = emptyMap(),
|
||||
) {
|
||||
/**
|
||||
* 转换为可变 Map,包含 extra 中的字段
|
||||
@ -66,7 +102,7 @@ data class R<T>(
|
||||
messageResolver: MessageResolver? = null,
|
||||
i18bArgs: Array<Any>? = null,
|
||||
requestIdProvider: RequestIdProvider? = null,
|
||||
extenders: List<ResultExtender> = emptyList()
|
||||
extenders: List<ResultExtender> = emptyList(),
|
||||
): R<T> {
|
||||
val msg = (messageResolver ?: DefaultMessageResolver).resolve(info.responseI8nMessageCode, i18bArgs)
|
||||
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
|
||||
@ -91,7 +127,7 @@ data class R<T>(
|
||||
messageResolver: MessageResolver? = null,
|
||||
i18bArgs: Array<Any>? = null,
|
||||
requestIdProvider: RequestIdProvider? = null,
|
||||
extenders: List<ResultExtender> = emptyList()
|
||||
extenders: List<ResultExtender> = emptyList(),
|
||||
): R<T> {
|
||||
val msg = (messageResolver ?: DefaultMessageResolver).resolve(info.responseI8nMessageCode, i18bArgs)
|
||||
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
|
||||
@ -118,7 +154,7 @@ data class R<T>(
|
||||
i18nArgs: Array<Any>? = null,
|
||||
messageResolver: MessageResolver? = null,
|
||||
requestIdProvider: RequestIdProvider? = null,
|
||||
extenders: List<ResultExtender> = emptyList()
|
||||
extenders: List<ResultExtender> = emptyList(),
|
||||
): R<T> {
|
||||
val msg = (messageResolver ?: DefaultMessageResolver).resolve(messageCode, i18nArgs)
|
||||
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
|
||||
@ -145,7 +181,7 @@ data class R<T>(
|
||||
i18nArgs: Array<Any>? = null,
|
||||
messageResolver: MessageResolver? = null,
|
||||
requestIdProvider: RequestIdProvider? = null,
|
||||
extenders: List<ResultExtender> = emptyList()
|
||||
extenders: List<ResultExtender> = emptyList(),
|
||||
): R<T> {
|
||||
val msg = (messageResolver ?: DefaultMessageResolver).resolve(messageCode, i18nArgs)
|
||||
val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId()
|
||||
@ -0,0 +1,22 @@
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.gewuyou.forgeboot.webmvc.dto.enums
|
||||
|
||||
/**
|
||||
* 排序方向枚举
|
||||
*
|
||||
* @author gewuyou
|
||||
* @since 2025-01-16 17:56:01
|
||||
*/
|
||||
enum class SortDirection {
|
||||
ASC, DESC
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
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 {
|
||||
predicates.add(builder.greaterThanOrEqualTo(root.get(createAtName), it))
|
||||
}
|
||||
|
||||
// 添加结束日期条件
|
||||
endDate?.let {
|
||||
predicates.add(builder.lessThanOrEqualTo(root.get(createAtName), it))
|
||||
}
|
||||
|
||||
// 添加是否启用条件
|
||||
enabled?.let {
|
||||
predicates.add(builder.equal(root.get<Boolean>(enabledName), it))
|
||||
}
|
||||
|
||||
// 添加是否删除条件
|
||||
deleted.let {
|
||||
predicates.add(builder.equal(root.get<Boolean>(deletedName), it))
|
||||
}
|
||||
|
||||
return predicates
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.gewuyou.forgeboot.webmvc.dto.request
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.gewuyou.forgeboot.webmvc.dto.i18n.I18nKeys
|
||||
import jakarta.validation.constraints.NotEmpty
|
||||
|
||||
/**
|
||||
* 根据id列表删除请求
|
||||
*
|
||||
* 该类用于封装删除操作中需要传入的id列表,确保删除操作能够接收一个明确且不为空的id集合
|
||||
* 它使用了泛型T,使得它可以适用于不同类型的id(例如Long,String等)
|
||||
*
|
||||
* @author gewuyou
|
||||
* @since 2025-01-18 17:39:18
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class DeleteByIdsReq<T>(
|
||||
/**
|
||||
* 待删除的实体id列表
|
||||
*
|
||||
* 这个字段是删除操作的核心参数,它不能为空,以确保至少有一个id被指定用于删除
|
||||
* 使用@NotNull注解来确保在序列化和反序列化过程中,该字段不能为空
|
||||
*
|
||||
* @param ids 实体的唯一标识符列表,用于指定哪些实体应当被删除
|
||||
*/
|
||||
@field:NotEmpty(message = I18nKeys.Forgeboot.Dto.DELETE_IDS_NOTNOTEMPTY)
|
||||
var ids: List<T>,
|
||||
)
|
||||
@ -0,0 +1,90 @@
|
||||
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 com.gewuyou.forgeboot.webmvc.dto.i18n.I18nKeys
|
||||
import jakarta.validation.constraints.Min
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* 分页查询条件请求实体类
|
||||
*
|
||||
* 用于通用分页、排序、关键字搜索、日期范围与状态过滤。
|
||||
* 支持自定义泛型过滤器实体。
|
||||
*
|
||||
* @author gewuyou
|
||||
* @since 2025-01-16 16:01:12
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
open class PageQueryReq<T> {
|
||||
|
||||
/**
|
||||
* 当前页码(默认1)
|
||||
*/
|
||||
@field:Min(1, message = I18nKeys.Forgeboot.Dto.PAGEQUERY_CURRENTPAGE_MIN )
|
||||
var currentPage: Int = 1
|
||||
|
||||
/**
|
||||
* 每页条数(默认10)
|
||||
*/
|
||||
@field:Min(1, message = I18nKeys.Forgeboot.Dto.PAGEQUERY_PAGESIZE_MIN)
|
||||
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: LocalDateTime? = null
|
||||
|
||||
/**
|
||||
* 结束日期
|
||||
*/
|
||||
var endDate: LocalDateTime? = 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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
forgeboot.dto.delete.ids.notNotEmpty=请求删除的id列表不能为空\!
|
||||
forgeboot.dto.pageQuery.currentPage.min=当前页码不得小于1页\!
|
||||
forgeboot.dto.pageQuery.pageSize.min=每页条数不得小于1页\!
|
||||
@ -0,0 +1,3 @@
|
||||
forgeboot.dto.delete.ids.notNotEmpty=The list of IDs requested to be removed cannot be empty\!
|
||||
forgeboot.dto.pageQuery.currentPage.min=The current page number must not be less than 1 page\!
|
||||
forgeboot.dto.pageQuery.pageSize.min=The number of entries per page shall not be less than 1 page\!
|
||||
@ -0,0 +1,3 @@
|
||||
forgeboot.dto.delete.ids.notNotEmpty=请求删除的id列表不能为空\!
|
||||
forgeboot.dto.pageQuery.currentPage.min=当前页码不得小于1页\!
|
||||
forgeboot.dto.pageQuery.pageSize.min=每页条数不得小于1页\!
|
||||
Loading…
x
Reference in New Issue
Block a user