From d31e47d1f8d7e4cb6f3b149660ee9f9dfb3645c5 Mon Sep 17 00:00:00 2001 From: gewuyou Date: Wed, 28 May 2025 23:46:48 +0800 Subject: [PATCH] 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 --- forgeboot-webmvc/dto/.gitattributes | 3 + forgeboot-webmvc/dto/.gitignore | 40 ++++++ forgeboot-webmvc/dto/build.gradle.kts | 15 +++ .../forgeboot/webmvc/dto/PageResult.kt | 112 ++++++++++++++++ .../com/gewuyou/forgeboot/webmvc/dto}/R.kt | 60 +++++++-- .../forgeboot/webmvc/dto/SortCondition.kt | 22 ++++ .../webmvc/dto/enums/SortDirection.kt | 11 ++ .../dto/extension/PageQueryExtension.kt | 123 ++++++++++++++++++ .../webmvc/dto/request/DeleteByIdsReq.kt | 28 ++++ .../webmvc/dto/request/PageQueryReq.kt | 90 +++++++++++++ .../dto/util/DynamicSpecificationBuilder.kt | 104 +++++++++++++++ .../main/resources/i18n/messages.properties | 3 + .../resources/i18n/messages_en.properties | 3 + .../resources/i18n/messages_zh_CN.properties | 3 + 14 files changed, 605 insertions(+), 12 deletions(-) create mode 100644 forgeboot-webmvc/dto/.gitattributes create mode 100644 forgeboot-webmvc/dto/.gitignore create mode 100644 forgeboot-webmvc/dto/build.gradle.kts create mode 100644 forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/PageResult.kt rename {forgeboot-common/forgeboot-common-result/forgeboot-common-result-impl/src/main/kotlin/com/gewuyou/forgeboot/common/result => forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto}/R.kt (76%) create mode 100644 forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/SortCondition.kt create mode 100644 forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/enums/SortDirection.kt create mode 100644 forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/extension/PageQueryExtension.kt create mode 100644 forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/request/DeleteByIdsReq.kt create mode 100644 forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/request/PageQueryReq.kt create mode 100644 forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/util/DynamicSpecificationBuilder.kt create mode 100644 forgeboot-webmvc/dto/src/main/resources/i18n/messages.properties create mode 100644 forgeboot-webmvc/dto/src/main/resources/i18n/messages_en.properties create mode 100644 forgeboot-webmvc/dto/src/main/resources/i18n/messages_zh_CN.properties diff --git a/forgeboot-webmvc/dto/.gitattributes b/forgeboot-webmvc/dto/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/forgeboot-webmvc/dto/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/forgeboot-webmvc/dto/.gitignore b/forgeboot-webmvc/dto/.gitignore new file mode 100644 index 0000000..5a979af --- /dev/null +++ b/forgeboot-webmvc/dto/.gitignore @@ -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 diff --git a/forgeboot-webmvc/dto/build.gradle.kts b/forgeboot-webmvc/dto/build.gradle.kts new file mode 100644 index 0000000..b43c132 --- /dev/null +++ b/forgeboot-webmvc/dto/build.gradle.kts @@ -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") +} diff --git a/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/PageResult.kt b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/PageResult.kt new file mode 100644 index 0000000..2a464e2 --- /dev/null +++ b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/PageResult.kt @@ -0,0 +1,112 @@ +package com.gewuyou.forgeboot.webmvc.dto + +/** + * 分页结果返回对象 + * + * 通常用于封装分页查询的返回结果,包括总数、页码等信息。 + * + * @author gewuyou + * @since 2024-04-23 下午10:53:04 + */ +open class PageResult { + + /** + * 当前页的记录列表 + */ + var records: MutableList? = 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 of(): PageResult = PageResult() + + /** + * 根据指定参数创建分页结果 + * + * @param currentPage 当前页码 + * @param pageSize 每页大小 + * @param totalRecords 总记录数 + * @param records 当前页的记录列表 + * @return 填充好的PageResult实例 + */ + fun of(currentPage: Long, pageSize: Long, totalRecords: Long, records: MutableList?): PageResult { + val result = PageResult() + 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 of(currentPage: Int, pageSize: Int, totalRecords: Long, records: MutableList?): PageResult { + return of(currentPage.toLong(), pageSize.toLong(), totalRecords, records) + } + + /** + * 创建一个仅设置页码和页面大小的分页结果 + * + * @param currentPage 当前页码 + * @param pageSize 每页大小 + * @return 部分填充的PageResult实例 + */ + fun of(currentPage: Long, pageSize: Long): PageResult { + val result = PageResult() + result.currentPage = currentPage + result.pageSize = pageSize + return result + } + + /** + * 创建一个空的分页结果实例 + * + * @return 新的PageResult实例 + */ + fun empty(): PageResult = PageResult() + } +} \ No newline at end of file diff --git a/forgeboot-common/forgeboot-common-result/forgeboot-common-result-impl/src/main/kotlin/com/gewuyou/forgeboot/common/result/R.kt b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/R.kt similarity index 76% rename from forgeboot-common/forgeboot-common-result/forgeboot-common-result-impl/src/main/kotlin/com/gewuyou/forgeboot/common/result/R.kt rename to forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/R.kt index 16aa130..fed3879 100644 --- a/forgeboot-common/forgeboot-common-result/forgeboot-common-result-impl/src/main/kotlin/com/gewuyou/forgeboot/common/result/R.kt +++ b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/R.kt @@ -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) +} /** * 统一响应封装类 @@ -18,7 +54,7 @@ data class R( val message: String, val data: T? = null, val requestId: String? = null, - val extra: Map = emptyMap() + val extra: Map = emptyMap(), ) { /** * 转换为可变 Map,包含 extra 中的字段 @@ -66,7 +102,7 @@ data class R( messageResolver: MessageResolver? = null, i18bArgs: Array? = null, requestIdProvider: RequestIdProvider? = null, - extenders: List = emptyList() + extenders: List = emptyList(), ): R { val msg = (messageResolver ?: DefaultMessageResolver).resolve(info.responseI8nMessageCode, i18bArgs) val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId() @@ -91,7 +127,7 @@ data class R( messageResolver: MessageResolver? = null, i18bArgs: Array? = null, requestIdProvider: RequestIdProvider? = null, - extenders: List = emptyList() + extenders: List = emptyList(), ): R { val msg = (messageResolver ?: DefaultMessageResolver).resolve(info.responseI8nMessageCode, i18bArgs) val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId() @@ -118,7 +154,7 @@ data class R( i18nArgs: Array? = null, messageResolver: MessageResolver? = null, requestIdProvider: RequestIdProvider? = null, - extenders: List = emptyList() + extenders: List = emptyList(), ): R { val msg = (messageResolver ?: DefaultMessageResolver).resolve(messageCode, i18nArgs) val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId() @@ -145,7 +181,7 @@ data class R( i18nArgs: Array? = null, messageResolver: MessageResolver? = null, requestIdProvider: RequestIdProvider? = null, - extenders: List = emptyList() + extenders: List = emptyList(), ): R { val msg = (messageResolver ?: DefaultMessageResolver).resolve(messageCode, i18nArgs) val reqId = (requestIdProvider ?: DefaultRequestIdProvider).getRequestId() diff --git a/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/SortCondition.kt b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/SortCondition.kt new file mode 100644 index 0000000..d7e9191 --- /dev/null +++ b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/SortCondition.kt @@ -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 +} diff --git a/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/enums/SortDirection.kt b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/enums/SortDirection.kt new file mode 100644 index 0000000..80b5dfd --- /dev/null +++ b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/enums/SortDirection.kt @@ -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 +} diff --git a/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/extension/PageQueryExtension.kt b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/extension/PageQueryExtension.kt new file mode 100644 index 0000000..268d77d --- /dev/null +++ b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/extension/PageQueryExtension.kt @@ -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 PageQueryReq.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 查询的根对象 + * @param createAtName 创建时间字段名,默认 "createdAt" + * @param enabledName 是否启用字段名,默认 "enabled" + * @param deletedName 是否删除字段名,默认 "deleted" + * @return MutableList 查询条件谓词列表 + */ +fun PageQueryReq.getPredicates( + builder: CriteriaBuilder, + root: Root, + createAtName: String = "createdAt", + enabledName: String = "enabled", + deletedName: String = "deleted" +): MutableList { + val predicates = mutableListOf() + + // 添加开始日期条件 + 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(enabledName), it)) + } + + // 添加是否删除条件 + deleted.let { + predicates.add(builder.equal(root.get(deletedName), it)) + } + + return predicates +} \ No newline at end of file diff --git a/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/request/DeleteByIdsReq.kt b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/request/DeleteByIdsReq.kt new file mode 100644 index 0000000..f95120c --- /dev/null +++ b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/request/DeleteByIdsReq.kt @@ -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( + /** + * 待删除的实体id列表 + * + * 这个字段是删除操作的核心参数,它不能为空,以确保至少有一个id被指定用于删除 + * 使用@NotNull注解来确保在序列化和反序列化过程中,该字段不能为空 + * + * @param ids 实体的唯一标识符列表,用于指定哪些实体应当被删除 + */ + @field:NotEmpty(message = I18nKeys.Forgeboot.Dto.DELETE_IDS_NOTNOTEMPTY) + var ids: List, +) \ No newline at end of file diff --git a/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/request/PageQueryReq.kt b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/request/PageQueryReq.kt new file mode 100644 index 0000000..66fbdd6 --- /dev/null +++ b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/request/PageQueryReq.kt @@ -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 { + + /** + * 当前页码(默认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 = 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) + } +} \ No newline at end of file diff --git a/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/util/DynamicSpecificationBuilder.kt b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/util/DynamicSpecificationBuilder.kt new file mode 100644 index 0000000..5db13de --- /dev/null +++ b/forgeboot-webmvc/dto/src/main/kotlin/com/gewuyou/forgeboot/webmvc/dto/util/DynamicSpecificationBuilder.kt @@ -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 查询参数对象 + * @param createAtName 时间字段名 + * @param enabledName 是否启用字段名 + * @param deletedName 是否删除字段名 + */ + inline fun build( + query: PageQueryReq, + createAtName: String = "createdAt", + enabledName: String = "enabled", + deletedName: String = "deleted", + ): Specification { + return Specification { root, _, builder -> + val predicates = mutableListOf() + + // 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>() + + 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() != null && value is String -> + builder.like(path as Path, "%$value%") + + prop.findAnnotation() != null -> + builder.equal(path, value) + + prop.findAnnotation() != null && value is Collection<*> -> + path.`in`(value) + + // 默认策略:非空值执行 equal + else -> builder.equal(path, value) + } + + val orGroup = prop.findAnnotation()?.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 getPath(root: Root, pathStr: String): Path<*> { + return pathStr.split(".").fold(root as Path<*>) { path, part -> + path.get(part) + } + } + +} \ No newline at end of file diff --git a/forgeboot-webmvc/dto/src/main/resources/i18n/messages.properties b/forgeboot-webmvc/dto/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..c7374c0 --- /dev/null +++ b/forgeboot-webmvc/dto/src/main/resources/i18n/messages.properties @@ -0,0 +1,3 @@ +forgeboot.dto.delete.ids.notNotEmpty=请求删除的id列表不能为空\! +forgeboot.dto.pageQuery.currentPage.min=当前页码不得小于1页\! +forgeboot.dto.pageQuery.pageSize.min=每页条数不得小于1页\! diff --git a/forgeboot-webmvc/dto/src/main/resources/i18n/messages_en.properties b/forgeboot-webmvc/dto/src/main/resources/i18n/messages_en.properties new file mode 100644 index 0000000..a160b47 --- /dev/null +++ b/forgeboot-webmvc/dto/src/main/resources/i18n/messages_en.properties @@ -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\! diff --git a/forgeboot-webmvc/dto/src/main/resources/i18n/messages_zh_CN.properties b/forgeboot-webmvc/dto/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 0000000..c7374c0 --- /dev/null +++ b/forgeboot-webmvc/dto/src/main/resources/i18n/messages_zh_CN.properties @@ -0,0 +1,3 @@ +forgeboot.dto.delete.ids.notNotEmpty=请求删除的id列表不能为空\! +forgeboot.dto.pageQuery.currentPage.min=当前页码不得小于1页\! +forgeboot.dto.pageQuery.pageSize.min=每页条数不得小于1页\!