vertx-pj:0.0.1
This commit is contained in:
commit
f28e1341d0
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
config/
|
||||||
|
gradle/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### Kotlin ###
|
||||||
|
.kotlin
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
||||||
51
README.md
Normal file
51
README.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Vertx-pj 说明
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
基于vert.x的web开发框架,提供了一些简单的封装,使得开发更加便捷。<br/>
|
||||||
|
**写的简单,问题不少,仅供参考**。
|
||||||
|
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
- **vertx-fw**: 简单封装的web开发框架
|
||||||
|
- **vertx-demo**: 使用vertx-fw开发的demo
|
||||||
|
|
||||||
|
## 注解说明
|
||||||
|
|
||||||
|
### @Controller
|
||||||
|
自定义Controller注解,用于路由注册
|
||||||
|
- 启动时使用反射获取所有标记了@Controller注解的类
|
||||||
|
- 获取类中所有方法,将其统一注册到router中
|
||||||
|
- 可选参数`prefix`:定义请求路径前缀
|
||||||
|
- 不填时默认使用类名(去除"Controller"后首字母小写)作为前缀
|
||||||
|
|
||||||
|
### @D
|
||||||
|
文档生成注解,可用于以下位置:
|
||||||
|
1. 类上:为Controller类添加说明
|
||||||
|
2. 方法上:为方法添加说明
|
||||||
|
3. 方法参数上:格式如 `@D("age", "年龄") age: Int?`
|
||||||
|
- name: 参数名,用于从query或body中自动获取参数
|
||||||
|
- value: 参数说明,用于文档生成
|
||||||
|
|
||||||
|
> 注:参数类型后的`?`表示可为空,不带`?`表示必填。框架会根据此进行参数校验。
|
||||||
|
|
||||||
|
### @AllowAnonymous
|
||||||
|
权限控制注解
|
||||||
|
- 可标记在Controller类或方法上
|
||||||
|
- 表示该接口不需要鉴权即可访问
|
||||||
|
|
||||||
|
### 权限相关注解
|
||||||
|
仿sa-token实现的权限控制:
|
||||||
|
- `@CheckRole()`:角色检查
|
||||||
|
- `@CheckPermission()`:权限检查
|
||||||
|
|
||||||
|
### 请求响应相关注解
|
||||||
|
|
||||||
|
#### @CustomizeRequest
|
||||||
|
请求方式定制
|
||||||
|
- 全局默认使用POST请求
|
||||||
|
- 使用`@CustomizeRequest("get")`可将方法改为GET请求
|
||||||
|
|
||||||
|
#### @CustomizeResponse
|
||||||
|
响应方式定制
|
||||||
|
- 全局默认返回JSON格式
|
||||||
|
- 使用`@CustomizeResponse`可获取response对象自定义返回内容
|
||||||
1
gradle.properties
Normal file
1
gradle.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
kotlin.code.style=official
|
||||||
6
settings.gradle.kts
Normal file
6
settings.gradle.kts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||||
|
}
|
||||||
|
rootProject.name = "vertx-pj"
|
||||||
|
include("vertx-fw")
|
||||||
|
include("vertx-demo")
|
||||||
115
vertx-demo/build.gradle.kts
Normal file
115
vertx-demo/build.gradle.kts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
plugins {
|
||||||
|
application
|
||||||
|
kotlin("jvm") version "1.9.20"
|
||||||
|
id("com.diffplug.spotless") version "6.25.0"
|
||||||
|
id("com.github.johnrengelman.shadow") version "8.1.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.demo"
|
||||||
|
version = "1.0.0-SNAPSHOT"
|
||||||
|
|
||||||
|
val vertxVersion = "4.5.11"
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("app.Application")
|
||||||
|
applicationDefaultJvmArgs = listOf(
|
||||||
|
"--add-opens",
|
||||||
|
"java.base/java.lang=ALL-UNNAMED",
|
||||||
|
"--add-opens",
|
||||||
|
"java.base/java.time=ALL-UNNAMED"
|
||||||
|
// "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.shadowJar {
|
||||||
|
archiveClassifier.set("fat")
|
||||||
|
mergeServiceFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
showStandardStreams = true
|
||||||
|
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||||
|
// events = setOf(PASSED, SKIPPED, FAILED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.compileKotlin {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.compileTestKotlin {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spotless {
|
||||||
|
kotlin {
|
||||||
|
ktlint()
|
||||||
|
.editorConfigOverride(
|
||||||
|
mapOf(
|
||||||
|
"ktlint_standard_no-wildcard-imports" to "disabled",
|
||||||
|
"ktlint_standard_trailing-comma-on-call-site" to "disabled",
|
||||||
|
"ktlint_standard_trailing-comma-on-declaration-site" to "disabled",
|
||||||
|
"indent_size" to "2"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
target("src/**/*.kt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":vertx-fw"))
|
||||||
|
// implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20")
|
||||||
|
// implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.20")
|
||||||
|
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||||
|
implementation(platform("io.vertx:vertx-stack-depchain:$vertxVersion"))
|
||||||
|
implementation("io.vertx:vertx-lang-kotlin:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-core:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-web:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-web-client:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-config:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-config-yaml:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-pg-client:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-mysql-client:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-sql-client-templates:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-auth-jwt:$vertxVersion")
|
||||||
|
|
||||||
|
implementation("com.google.inject:guice:5.1.0")
|
||||||
|
implementation("org.reflections:reflections:0.9.12")
|
||||||
|
implementation("cn.hutool:hutool-all:5.8.24")
|
||||||
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
|
||||||
|
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2")
|
||||||
|
// implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
|
||||||
|
implementation("dev.hsbrysk:caffeine-coroutines:1.0.0")
|
||||||
|
|
||||||
|
// log
|
||||||
|
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
|
||||||
|
implementation("org.slf4j:slf4j-api:2.0.6")
|
||||||
|
implementation("ch.qos.logback:logback-classic:1.4.14")
|
||||||
|
implementation("org.codehaus.janino:janino:3.1.8")
|
||||||
|
|
||||||
|
// jpa
|
||||||
|
implementation("jakarta.persistence:jakarta.persistence-api:3.2.0")
|
||||||
|
implementation("jakarta.validation:jakarta.validation-api:3.1.0")
|
||||||
|
|
||||||
|
// implementation("com.mysql:mysql-connector-j:9.1.0")
|
||||||
|
implementation("mysql:mysql-connector-java:5.1.49")
|
||||||
|
// doc
|
||||||
|
implementation("io.swagger.core.v3:swagger-core:2.2.27")
|
||||||
|
|
||||||
|
testImplementation("io.vertx:vertx-junit5:$vertxVersion")
|
||||||
|
|
||||||
|
implementation(fileTree(mapOf("dir" to "lib", "include" to listOf("*.jar"))))
|
||||||
|
}
|
||||||
27
vertx-demo/src/main/kotlin/app/Application.kt
Normal file
27
vertx-demo/src/main/kotlin/app/Application.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import app.config.InjectConfig
|
||||||
|
import app.verticle.MainVerticle
|
||||||
|
import io.vertx.core.Vertx
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import mu.KotlinLogging
|
||||||
|
|
||||||
|
object Application {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
runBlocking {
|
||||||
|
val vertx = Vertx.vertx()
|
||||||
|
val getIt = InjectConfig.configure(vertx)
|
||||||
|
val demoVerticle = getIt.getInstance(MainVerticle::class.java)
|
||||||
|
vertx.deployVerticle(demoVerticle).onComplete {
|
||||||
|
if (it.failed()) {
|
||||||
|
logger.error { "MainVerticle startup failed: ${it.cause()?.stackTraceToString()}" }
|
||||||
|
} else {
|
||||||
|
logger.info { "MainVerticle startup successfully" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
vertx-demo/src/main/kotlin/app/controller/AuthController.kt
Normal file
58
vertx-demo/src/main/kotlin/app/controller/AuthController.kt
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package app.controller
|
||||||
|
|
||||||
|
import app.config.Constant
|
||||||
|
import app.domain.user.LoginDTO
|
||||||
|
import app.domain.user.User
|
||||||
|
import app.domain.user.UserRepository
|
||||||
|
import app.util.CacheUtil
|
||||||
|
import cn.hutool.core.lang.Snowflake
|
||||||
|
import cn.hutool.crypto.SecureUtil
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import io.vertx.ext.auth.jwt.JWTAuth
|
||||||
|
import org.aikrai.vertx.auth.AllowAnonymous
|
||||||
|
import org.aikrai.vertx.auth.TokenUtil
|
||||||
|
import org.aikrai.vertx.context.Controller
|
||||||
|
import org.aikrai.vertx.context.D
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
|
||||||
|
@AllowAnonymous
|
||||||
|
@D("认证")
|
||||||
|
@Controller("/auth")
|
||||||
|
class AuthController @Inject constructor(
|
||||||
|
private val jwtAuth: JWTAuth,
|
||||||
|
private val snowflake: Snowflake,
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val cacheUtil: CacheUtil
|
||||||
|
) {
|
||||||
|
|
||||||
|
@D("注册")
|
||||||
|
suspend fun doSign(
|
||||||
|
@D("loginInfo", "账号信息") loginInfo: LoginDTO
|
||||||
|
): String {
|
||||||
|
userRepository.getByName(loginInfo.username)?.let {
|
||||||
|
throw Meta.failure("LoginFailed", "用户名已被使用")
|
||||||
|
}
|
||||||
|
val user = User().apply {
|
||||||
|
this.id = snowflake.nextId()
|
||||||
|
this.userName = loginInfo.username
|
||||||
|
this.password = SecureUtil.sha1(loginInfo.password)
|
||||||
|
this.loginName = loginInfo.username
|
||||||
|
}
|
||||||
|
cacheUtil.put(Constant.USER + user.id, user)
|
||||||
|
userRepository.create(user)
|
||||||
|
return TokenUtil.genToken(jwtAuth, mapOf("id" to user.id!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
@D("登录")
|
||||||
|
suspend fun doLogin(
|
||||||
|
@D("loginInfo", "账号信息") loginInfo: LoginDTO
|
||||||
|
): String {
|
||||||
|
val user = userRepository.getByName(loginInfo.username) ?: throw Meta.failure("LoginFailed", "用户名或密码错误")
|
||||||
|
if (user.password == SecureUtil.sha1(loginInfo.password)) {
|
||||||
|
cacheUtil.put(Constant.USER + user.id, user)
|
||||||
|
return TokenUtil.genToken(jwtAuth, mapOf("id" to user.id!!))
|
||||||
|
} else {
|
||||||
|
throw Meta.failure("LoginFailed", "用户名或密码错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
vertx-demo/src/main/kotlin/app/controller/Demo1Controller.kt
Normal file
61
vertx-demo/src/main/kotlin/app/controller/Demo1Controller.kt
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package app.controller
|
||||||
|
|
||||||
|
import app.domain.CargoType
|
||||||
|
import app.domain.user.User
|
||||||
|
import app.domain.user.UserRepository
|
||||||
|
import app.service.user.UserService
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.auth.AllowAnonymous
|
||||||
|
import org.aikrai.vertx.auth.CheckPermission
|
||||||
|
import org.aikrai.vertx.auth.CheckRole
|
||||||
|
import org.aikrai.vertx.config.Config
|
||||||
|
import org.aikrai.vertx.context.Controller
|
||||||
|
import org.aikrai.vertx.context.D
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推荐代码示例
|
||||||
|
*/
|
||||||
|
@D("测试1:测试")
|
||||||
|
@Controller
|
||||||
|
class Demo1Controller @Inject constructor(
|
||||||
|
private val userService: UserService,
|
||||||
|
private val userRepository: UserRepository
|
||||||
|
) {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
@D("参数测试", "详细说明......")
|
||||||
|
suspend fun test1(
|
||||||
|
@D("name", "姓名") name: String,
|
||||||
|
@D("age", "年龄") age: Int?,
|
||||||
|
@D("list", "列表") list: List<String>?,
|
||||||
|
@D("cargoType", "货物类型") cargoType: CargoType?
|
||||||
|
) {
|
||||||
|
logger.info { "你好" }
|
||||||
|
println(age)
|
||||||
|
println(list)
|
||||||
|
println("test-$name")
|
||||||
|
println(cargoType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@D("事务测试")
|
||||||
|
suspend fun testT() {
|
||||||
|
userService.testTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
@D("查询测试")
|
||||||
|
suspend fun getList(): List<User> {
|
||||||
|
val list = userRepository.getList()
|
||||||
|
println(list)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllowAnonymous
|
||||||
|
@D("配置读取测试")
|
||||||
|
suspend fun testRetriever(
|
||||||
|
@D("key", "key") key: String
|
||||||
|
) {
|
||||||
|
val configMap = Config.getKey(key)
|
||||||
|
println(configMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
vertx-demo/src/main/kotlin/app/domain/CargoType.kt
Normal file
18
vertx-demo/src/main/kotlin/app/domain/CargoType.kt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package app.domain
|
||||||
|
|
||||||
|
enum class CargoType(val message: String) {
|
||||||
|
|
||||||
|
RECEIVING("收货"),
|
||||||
|
|
||||||
|
SHIPPING("发货"),
|
||||||
|
|
||||||
|
INTERNAL_TRANSFER("内部调拨")
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(value: String?): CargoType? {
|
||||||
|
if (value.isNullOrBlank()) return null
|
||||||
|
return CargoType.values().find { it.name == value || it.message == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
vertx-demo/src/main/kotlin/app/domain/role/Role.kt
Normal file
42
vertx-demo/src/main/kotlin/app/domain/role/Role.kt
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package app.domain.role
|
||||||
|
|
||||||
|
import jakarta.persistence.*
|
||||||
|
import jakarta.validation.constraints.NotNull
|
||||||
|
import jakarta.validation.constraints.Size
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "`sys_role`", schema = "`vertx-demo`")
|
||||||
|
class Role : org.aikrai.vertx.utlis.Entity() {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "role_id", nullable = false)
|
||||||
|
var id: Long? = null
|
||||||
|
|
||||||
|
@Size(max = 30)
|
||||||
|
@NotNull
|
||||||
|
@Column(name = "role_name", nullable = false, length = 30)
|
||||||
|
var roleName: String? = null
|
||||||
|
|
||||||
|
@Size(max = 100)
|
||||||
|
@NotNull
|
||||||
|
@Column(name = "role_key", nullable = false, length = 100)
|
||||||
|
var roleKey: String? = null
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Column(name = "role_sort", nullable = false)
|
||||||
|
var roleSort: Int? = null
|
||||||
|
|
||||||
|
@Column(name = "data_scope")
|
||||||
|
var dataScope: Char? = null
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Column(name = "status", nullable = false)
|
||||||
|
var status: Char? = null
|
||||||
|
|
||||||
|
@Column(name = "del_flag")
|
||||||
|
var delFlag: Char? = null
|
||||||
|
|
||||||
|
@Size(max = 500)
|
||||||
|
@Column(name = "remark", length = 500)
|
||||||
|
var remark: String? = null
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package app.domain.role
|
||||||
|
|
||||||
|
import com.google.inject.ImplementedBy
|
||||||
|
import org.aikrai.vertx.db.Repository
|
||||||
|
|
||||||
|
@ImplementedBy(RoleRepositoryImpl::class)
|
||||||
|
interface RoleRepository : Repository<Long, Role>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package app.domain.role
|
||||||
|
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import io.vertx.sqlclient.SqlClient
|
||||||
|
import org.aikrai.vertx.db.RepositoryImpl
|
||||||
|
|
||||||
|
class RoleRepositoryImpl @Inject constructor(
|
||||||
|
sqlClient: SqlClient
|
||||||
|
) : RepositoryImpl<Long, Role>(sqlClient), RoleRepository
|
||||||
6
vertx-demo/src/main/kotlin/app/domain/user/LoginDTO.kt
Normal file
6
vertx-demo/src/main/kotlin/app/domain/user/LoginDTO.kt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package app.domain.user
|
||||||
|
|
||||||
|
data class LoginDTO(
|
||||||
|
var username: String,
|
||||||
|
var password: String
|
||||||
|
)
|
||||||
74
vertx-demo/src/main/kotlin/app/domain/user/User.kt
Normal file
74
vertx-demo/src/main/kotlin/app/domain/user/User.kt
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package app.domain.user
|
||||||
|
|
||||||
|
import jakarta.persistence.*
|
||||||
|
import java.sql.Timestamp
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "`sys_user`", schema = "`vertx-demo`")
|
||||||
|
class User {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "user_id", nullable = false)
|
||||||
|
var id: Long? = 0L
|
||||||
|
|
||||||
|
@Column(name = "dept_id")
|
||||||
|
var deptId: Long? = null
|
||||||
|
|
||||||
|
@Column(name = "login_name", nullable = false, length = 30)
|
||||||
|
var loginName: String? = ""
|
||||||
|
|
||||||
|
@Column(name = "user_name", length = 30)
|
||||||
|
var userName: String? = ""
|
||||||
|
|
||||||
|
@Column(name = "user_type", length = 2)
|
||||||
|
var userType: String? = ""
|
||||||
|
|
||||||
|
@Column(name = "email", length = 50)
|
||||||
|
var email: String? = ""
|
||||||
|
|
||||||
|
@Column(name = "phonenumber", length = 11)
|
||||||
|
var phonenumber: String? = ""
|
||||||
|
|
||||||
|
// @Column(name = "sex")
|
||||||
|
// var sex: Char? = null
|
||||||
|
|
||||||
|
@Column(name = "avatar", length = 100)
|
||||||
|
var avatar: String? = null
|
||||||
|
|
||||||
|
@Column(name = "password", length = 50)
|
||||||
|
var password: String? = null
|
||||||
|
|
||||||
|
@Column(name = "salt", length = 20)
|
||||||
|
var salt: String? = null
|
||||||
|
|
||||||
|
// @Column(name = "status")
|
||||||
|
// var status: Char? = null
|
||||||
|
|
||||||
|
// @Column(name = "del_flag")
|
||||||
|
// var delFlag: Char? = null
|
||||||
|
|
||||||
|
@Column(name = "login_ip", length = 128)
|
||||||
|
var loginIp: String? = null
|
||||||
|
|
||||||
|
@Column(name = "login_date")
|
||||||
|
var loginDate: Timestamp? = null
|
||||||
|
|
||||||
|
@Column(name = "pwd_update_date")
|
||||||
|
var pwdUpdateDate: Timestamp? = null
|
||||||
|
|
||||||
|
@Column(name = "create_by", length = 64)
|
||||||
|
var createBy: String? = null
|
||||||
|
|
||||||
|
@Column(name = "create_time")
|
||||||
|
var createTime: Timestamp? = null
|
||||||
|
|
||||||
|
@Column(name = "update_by", length = 64)
|
||||||
|
var updateBy: String? = null
|
||||||
|
|
||||||
|
@Column(name = "update_time")
|
||||||
|
var updateTime: Timestamp? = null
|
||||||
|
|
||||||
|
@Column(name = "remark", length = 500)
|
||||||
|
var remark: String? = null
|
||||||
|
}
|
||||||
13
vertx-demo/src/main/kotlin/app/domain/user/UserRepository.kt
Normal file
13
vertx-demo/src/main/kotlin/app/domain/user/UserRepository.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package app.domain.user
|
||||||
|
|
||||||
|
import com.google.inject.ImplementedBy
|
||||||
|
import org.aikrai.vertx.db.Repository
|
||||||
|
|
||||||
|
@ImplementedBy(UserRepositoryImpl::class)
|
||||||
|
interface UserRepository : Repository<Long, User> {
|
||||||
|
suspend fun getByName(name: String): User?
|
||||||
|
|
||||||
|
suspend fun testTransaction(): User?
|
||||||
|
|
||||||
|
suspend fun getList(): List<User>
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package app.domain.user
|
||||||
|
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import io.vertx.sqlclient.SqlClient
|
||||||
|
import org.aikrai.vertx.db.RepositoryImpl
|
||||||
|
|
||||||
|
class UserRepositoryImpl @Inject constructor(
|
||||||
|
sqlClient: SqlClient
|
||||||
|
) : RepositoryImpl<Long, User>(sqlClient), UserRepository {
|
||||||
|
|
||||||
|
override suspend fun getByName(name: String): User? {
|
||||||
|
val user = queryBuilder()
|
||||||
|
.eq(User::userName, name)
|
||||||
|
.getOne()
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun testTransaction(): User? {
|
||||||
|
// throw Meta.failure("test transaction", "test transaction")
|
||||||
|
return queryBuilder().getOne()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getList(): List<User> {
|
||||||
|
return queryBuilder().getList()
|
||||||
|
}
|
||||||
|
}
|
||||||
12
vertx-demo/src/main/kotlin/app/service/user/UserService.kt
Normal file
12
vertx-demo/src/main/kotlin/app/service/user/UserService.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package app.service.user
|
||||||
|
|
||||||
|
import app.domain.user.User
|
||||||
|
import app.service.user.impl.UserServiceImpl
|
||||||
|
import com.google.inject.ImplementedBy
|
||||||
|
|
||||||
|
@ImplementedBy(UserServiceImpl::class)
|
||||||
|
interface UserService {
|
||||||
|
suspend fun updateUser(user: User)
|
||||||
|
|
||||||
|
suspend fun testTransaction()
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package app.service.user.impl
|
||||||
|
|
||||||
|
import app.domain.role.RoleRepository
|
||||||
|
import app.domain.user.User
|
||||||
|
import app.domain.user.UserRepository
|
||||||
|
import app.service.user.UserService
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import org.aikrai.vertx.db.tx.withTransaction
|
||||||
|
|
||||||
|
class UserServiceImpl @Inject constructor(
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val roleRepository: RoleRepository
|
||||||
|
) : UserService {
|
||||||
|
|
||||||
|
override suspend fun updateUser(user: User) {
|
||||||
|
userRepository.getByName(user.userName!!)?.let {
|
||||||
|
userRepository.update(user)
|
||||||
|
roleRepository.update(user.id!!, mapOf("type" to "normal"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun testTransaction() {
|
||||||
|
// withTransaction嵌套时, 使用的是同一个事务对象,要成功全部成功,要失败全部失败
|
||||||
|
withTransaction {
|
||||||
|
val execute1 = userRepository.execute<Int>("update sys_user set email = '88888' where user_name = '运若汐'")
|
||||||
|
println("运若汐: $execute1")
|
||||||
|
withTransaction {
|
||||||
|
val execute = userRepository.execute<Int>("update sys_user set email = '88888' where user_name = '郸明'")
|
||||||
|
println("郸明: $execute")
|
||||||
|
// throw Meta.failure("test transaction", "test transaction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
vertx-demo/src/main/kotlin/app/util/CacheUtil.kt
Normal file
44
vertx-demo/src/main/kotlin/app/util/CacheUtil.kt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package app.util
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine
|
||||||
|
import com.google.inject.Singleton
|
||||||
|
import dev.hsbrysk.caffeine.CoroutineCache
|
||||||
|
import dev.hsbrysk.caffeine.buildCoroutine
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class CacheUtil {
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var cache: CoroutineCache<String, Any>? = null
|
||||||
|
|
||||||
|
fun getCache(): CoroutineCache<String, Any> {
|
||||||
|
return cache ?: synchronized(this) {
|
||||||
|
cache ?: init().also { cache = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init(): CoroutineCache<String, Any> {
|
||||||
|
return Caffeine.newBuilder()
|
||||||
|
.maximumSize(10_000)
|
||||||
|
.expireAfterAccess(30, TimeUnit.MINUTES)
|
||||||
|
.buildCoroutine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(str: String): Any? {
|
||||||
|
return getCache().getIfPresent(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(key: String, value: Any) {
|
||||||
|
getCache().put(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidate(key: String) {
|
||||||
|
getCache().synchronous().invalidate(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidateAll() {
|
||||||
|
getCache().synchronous().invalidateAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
54
vertx-demo/src/main/kotlin/app/verticle/ApifoxClient.kt
Normal file
54
vertx-demo/src/main/kotlin/app/verticle/ApifoxClient.kt
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package app.verticle
|
||||||
|
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import com.google.inject.name.Named
|
||||||
|
import io.vertx.core.Vertx
|
||||||
|
import io.vertx.core.http.HttpMethod
|
||||||
|
import io.vertx.core.json.JsonObject
|
||||||
|
import io.vertx.ext.web.client.WebClient
|
||||||
|
import io.vertx.ext.web.client.WebClientOptions
|
||||||
|
import io.vertx.kotlin.coroutines.CoroutineVerticle
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.openapi.OpenApiSpecGenerator
|
||||||
|
|
||||||
|
class ApifoxClient @Inject constructor(
|
||||||
|
private val vertx: Vertx,
|
||||||
|
@Named("apifox.token") private val token: String,
|
||||||
|
@Named("apifox.projectId") private val projectId: String,
|
||||||
|
@Named("apifox.folderId") private val folderId: String,
|
||||||
|
@Named("server.name") private val serverName: String,
|
||||||
|
@Named("server.port") private val port: String
|
||||||
|
) : CoroutineVerticle() {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
override suspend fun start() {
|
||||||
|
importOpenapi()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importOpenapi() {
|
||||||
|
val openApiJsonStr = OpenApiSpecGenerator().genOpenApiSpecStr(serverName, "1.0", "http://127.0.0.1:$port/api")
|
||||||
|
val options = WebClientOptions().setDefaultPort(443).setDefaultHost("api.apifox.com").setSsl(true)
|
||||||
|
val client = WebClient.create(vertx, options)
|
||||||
|
// 参数参考 https://apifox-openapi.apifox.cn/api-173409873
|
||||||
|
val reqOptions = JsonObject()
|
||||||
|
.put("targetEndpointFolderId", folderId)
|
||||||
|
.put("endpointOverwriteBehavior", "OVERWRITE_EXISTING")
|
||||||
|
.put("schemaOverwriteBehavior", "OVERWRITE_EXISTING")
|
||||||
|
.put("updateFolderOfChangedEndpoints", false)
|
||||||
|
.put("prependBasePath", false)
|
||||||
|
val requestBody = JsonObject().put("input", openApiJsonStr).put("options", reqOptions)
|
||||||
|
|
||||||
|
client.request(HttpMethod.POST, "/v1/projects/$projectId/import-openapi")
|
||||||
|
.putHeader("X-Apifox-Version", "2024-01-20")
|
||||||
|
.putHeader("Authorization", "Bearer $token")
|
||||||
|
.sendJsonObject(requestBody) { ar ->
|
||||||
|
if (ar.succeeded()) {
|
||||||
|
val response = ar.result()
|
||||||
|
logger.info("Received response with status code: ${response.statusCode()}")
|
||||||
|
logger.info("Response body: ${response.bodyAsString()}")
|
||||||
|
} else {
|
||||||
|
logger.warn("Request failed: ${ar.cause().message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
vertx-demo/src/main/kotlin/app/verticle/MainVerticle.kt
Normal file
31
vertx-demo/src/main/kotlin/app/verticle/MainVerticle.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package app.verticle
|
||||||
|
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import io.vertx.kotlin.coroutines.CoroutineVerticle
|
||||||
|
import mu.KotlinLogging
|
||||||
|
|
||||||
|
class MainVerticle @Inject constructor(
|
||||||
|
private val webVerticle: WebVerticle,
|
||||||
|
private val apifoxClient: ApifoxClient
|
||||||
|
) : CoroutineVerticle() {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
override suspend fun start() {
|
||||||
|
val verticles = listOf(
|
||||||
|
webVerticle,
|
||||||
|
apifoxClient
|
||||||
|
)
|
||||||
|
|
||||||
|
for (verticle in verticles) {
|
||||||
|
vertx.deployVerticle(verticle).onComplete {
|
||||||
|
val simpleName = verticle.javaClass.simpleName
|
||||||
|
if (it.failed()) {
|
||||||
|
logger.error { "$simpleName startup failed: ${it.cause()?.stackTraceToString()}" }
|
||||||
|
} else {
|
||||||
|
val deploymentId = it.result()
|
||||||
|
logger.info { "$simpleName startup successfully, deploymentId:$deploymentId" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
vertx-demo/src/main/kotlin/app/verticle/WebVerticle.kt
Normal file
105
vertx-demo/src/main/kotlin/app/verticle/WebVerticle.kt
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package app.verticle
|
||||||
|
|
||||||
|
import app.config.auth.AuthHandler
|
||||||
|
import app.config.auth.JwtAuthenticationHandler
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import com.google.inject.Injector
|
||||||
|
import com.google.inject.name.Named
|
||||||
|
import io.vertx.core.Handler
|
||||||
|
import io.vertx.core.http.HttpHeaders
|
||||||
|
import io.vertx.core.http.HttpMethod
|
||||||
|
import io.vertx.core.http.HttpServerOptions
|
||||||
|
import io.vertx.ext.web.Router
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
import io.vertx.ext.web.handler.BodyHandler
|
||||||
|
import io.vertx.ext.web.handler.CorsHandler
|
||||||
|
import io.vertx.kotlin.coroutines.CoroutineVerticle
|
||||||
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.config.FailureParser
|
||||||
|
import org.aikrai.vertx.context.RouterBuilder
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
|
||||||
|
class WebVerticle @Inject constructor(
|
||||||
|
private val getIt: Injector,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
private val authHandler: AuthHandler,
|
||||||
|
@Named("server.name") private val serverName: String,
|
||||||
|
@Named("server.port") private val port: String,
|
||||||
|
@Named("server.context") private val context: String
|
||||||
|
) : CoroutineVerticle() {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
override suspend fun start() {
|
||||||
|
val rootRouter = Router.router(vertx)
|
||||||
|
val router = Router.router(vertx)
|
||||||
|
setupRouter(rootRouter, router)
|
||||||
|
|
||||||
|
val options = HttpServerOptions().setMaxFormAttributeSize(1024 * 1024)
|
||||||
|
val server = vertx.createHttpServer(options)
|
||||||
|
.requestHandler(rootRouter)
|
||||||
|
.listen(port.toInt())
|
||||||
|
.coAwait()
|
||||||
|
|
||||||
|
logger.info { "http server start - http://127.0.0.1:${server.actualPort()}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun stop() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRouter(rootRouter: Router, router: Router) {
|
||||||
|
rootRouter.route("/api" + "*").subRouter(router)
|
||||||
|
router.route()
|
||||||
|
.handler(corsHandler)
|
||||||
|
.failureHandler(errorHandler)
|
||||||
|
.handler(BodyHandler.create())
|
||||||
|
|
||||||
|
val authHandler = JwtAuthenticationHandler(coroutineScope, authHandler, context)
|
||||||
|
router.route("/*").handler(authHandler)
|
||||||
|
|
||||||
|
val routerBuilder = RouterBuilder(coroutineScope, router).build { service ->
|
||||||
|
getIt.getInstance(service)
|
||||||
|
}
|
||||||
|
authHandler.exclude.addAll(routerBuilder.anonymousPaths)
|
||||||
|
// 生成 openapi.json
|
||||||
|
/*val openApiJsonStr = OpenApiSpecGenerator().genOpenApiSpecStr(serverName, "1.0", "http://127.0.0.1:$port/api")
|
||||||
|
val resourcesPath = "${System.getProperty("user.dir")}/src/main/resources"
|
||||||
|
val timestamp = System.currentTimeMillis()
|
||||||
|
vertx.fileSystem()
|
||||||
|
.writeFile(
|
||||||
|
"$resourcesPath/openapi/openapi-$timestamp.json",
|
||||||
|
Buffer.buffer(openApiJsonStr)
|
||||||
|
) { writeFileAsyncResult ->
|
||||||
|
if (!writeFileAsyncResult.succeeded()) writeFileAsyncResult.cause().printStackTrace()
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private val corsHandler = CorsHandler.create()
|
||||||
|
.addOrigin("*")
|
||||||
|
.allowedMethod(HttpMethod.GET)
|
||||||
|
.allowedMethod(HttpMethod.POST)
|
||||||
|
.allowedMethod(HttpMethod.PUT)
|
||||||
|
.allowedMethod(HttpMethod.DELETE)
|
||||||
|
.allowedMethod(HttpMethod.OPTIONS)
|
||||||
|
|
||||||
|
private val errorHandler = Handler<RoutingContext> { ctx ->
|
||||||
|
val failure = ctx.failure()
|
||||||
|
if (failure != null) {
|
||||||
|
logger.error { "${ctx.request().uri()}: ${failure.stackTraceToString()}" }
|
||||||
|
|
||||||
|
val parsedFailure = FailureParser.parse(ctx.statusCode(), failure)
|
||||||
|
val response = ctx.response()
|
||||||
|
|
||||||
|
response.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||||
|
response.statusCode = if (ctx.statusCode() != 200) ctx.statusCode() else 500
|
||||||
|
response.end(JsonUtil.toJsonStr(parsedFailure.response))
|
||||||
|
} else {
|
||||||
|
logger.error("${ctx.request().uri()}: 未知错误")
|
||||||
|
val response = ctx.response()
|
||||||
|
response.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||||
|
response.statusCode = 500
|
||||||
|
response.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
vertx-demo/src/main/resources/bootstrap.yml
Normal file
22
vertx-demo/src/main/resources/bootstrap.yml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
server:
|
||||||
|
name: vtx_demo
|
||||||
|
active: dev
|
||||||
|
port: 8080
|
||||||
|
context: api # 上下文
|
||||||
|
timeout: 120 # eventbus超时时间
|
||||||
|
http:
|
||||||
|
header: # header获取到的变量
|
||||||
|
- x-requested-with
|
||||||
|
- Access-Control-Allow-Origin
|
||||||
|
- origin
|
||||||
|
- Content-Type
|
||||||
|
- accept
|
||||||
|
event-bus:
|
||||||
|
timeout: 10000 # 毫秒
|
||||||
|
jwt:
|
||||||
|
key: 123456sdfjasdfjl # jwt加密key
|
||||||
|
|
||||||
|
apifox:
|
||||||
|
token: APS-xxx
|
||||||
|
projectId: xxx
|
||||||
|
folderId: xxx
|
||||||
102
vertx-demo/src/main/resources/logback.xml
Normal file
102
vertx-demo/src/main/resources/logback.xml
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration
|
||||||
|
debug="false" scan="true" scanPeriod="30 second">
|
||||||
|
<property name="ROOT" value="../bucket/log/"/>
|
||||||
|
<property name="APPNAME" value="vertx-fw"/>
|
||||||
|
<property name="FILESIZE" value="500MB"/>
|
||||||
|
<property name="MAXHISTORY" value="100"/>
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<Target>System.out</Target>
|
||||||
|
<encoder charset="utf-8">
|
||||||
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
|
</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<encoder charset="utf-8">
|
||||||
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
|
</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
|
||||||
|
<evaluator>
|
||||||
|
<expression>return level >= WARN;</expression>
|
||||||
|
</evaluator>
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${ROOT}${APPNAME}-%d-warn.%i.log</fileNamePattern>
|
||||||
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
|
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||||
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
|
</rollingPolicy>
|
||||||
|
</appender>
|
||||||
|
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<encoder charset="utf-8">
|
||||||
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
|
</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<level>INFO</level>
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${ROOT}${APPNAME}-%d-info.%i.log</fileNamePattern>
|
||||||
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
|
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||||
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
|
</rollingPolicy>
|
||||||
|
</appender>
|
||||||
|
<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<encoder charset="utf-8">
|
||||||
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
|
</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<level>DEBUG</level>
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
<rollingPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${ROOT}${APPNAME}-%d-debug.%i.log</fileNamePattern>
|
||||||
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
|
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||||
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
|
</rollingPolicy>
|
||||||
|
</appender>
|
||||||
|
<appender name="TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<encoder charset="utf-8">
|
||||||
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
|
</pattern>
|
||||||
|
</encoder>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<level>TRACE</level>
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
<rollingPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${ROOT}${APPNAME}-%d-trace.%i.log</fileNamePattern>
|
||||||
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
|
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||||
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
|
</rollingPolicy>
|
||||||
|
</appender>
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
<appender-ref ref="WARN"/>
|
||||||
|
<appender-ref ref="INFO"/>
|
||||||
|
<appender-ref ref="DEBUG"/>
|
||||||
|
<appender-ref ref="TRACE"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
27
vertx-demo/src/main/resources/sql/sys_role.sql
Normal file
27
vertx-demo/src/main/resources/sql/sys_role.sql
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for sys_role
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `sys_role`;
|
||||||
|
CREATE TABLE `sys_role` (
|
||||||
|
`role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
|
||||||
|
`role_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名称',
|
||||||
|
`role_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色权限字符串',
|
||||||
|
`role_sort` int(4) NOT NULL COMMENT '显示顺序',
|
||||||
|
`data_scope` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)',
|
||||||
|
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色状态(0正常 1停用)',
|
||||||
|
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
|
||||||
|
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
|
||||||
|
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
|
||||||
|
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (`role_id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of sys_role
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `sys_role` VALUES (1, '超级管理员', 'admin', 1, '1', '0', '0', 'admin', '2024-12-28 11:30:34', '', NULL, '超级管理员');
|
||||||
|
INSERT INTO `sys_role` VALUES (2, '普通角色', 'common', 2, '2', '0', '0', 'admin', '2024-12-28 11:30:34', '', NULL, '普通角色');
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
39
vertx-demo/src/main/resources/sql/sys_user.sql
Normal file
39
vertx-demo/src/main/resources/sql/sys_user.sql
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for sys_user
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `sys_user`;
|
||||||
|
CREATE TABLE `sys_user` (
|
||||||
|
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
||||||
|
`dept_id` bigint(20) NULL DEFAULT NULL COMMENT '部门ID',
|
||||||
|
`login_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '登录账号',
|
||||||
|
`user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户昵称',
|
||||||
|
`user_type` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '00' COMMENT '用户类型(00系统用户 01注册用户)',
|
||||||
|
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户邮箱',
|
||||||
|
`phonenumber` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '手机号码',
|
||||||
|
`sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
|
||||||
|
`avatar` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '头像路径',
|
||||||
|
`password` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码',
|
||||||
|
`salt` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '盐加密',
|
||||||
|
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
|
||||||
|
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
|
||||||
|
`login_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '最后登录IP',
|
||||||
|
`login_date` datetime NULL DEFAULT NULL COMMENT '最后登录时间',
|
||||||
|
`pwd_update_date` datetime NULL DEFAULT NULL COMMENT '密码最后更新时间',
|
||||||
|
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
|
||||||
|
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
|
||||||
|
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (`user_id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1875732675218882561 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of sys_user
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `sys_user` VALUES (1, 103, 'admin', '若依', '00', 'ry@163.com', '15888888888', '1', '', '29c67a30398638269fe600f73a054934', '111111', '0', '0', '127.0.0.1', NULL, NULL, 'admin', '2024-12-28 11:30:31', '', NULL, '管理员');
|
||||||
|
INSERT INTO `sys_user` VALUES (2, 105, 'ry', '若1', '00', 'ry@qq.com', '15666666666', '1', '', '8e6d98b90472783cc73c17047ddccf36', '222222', '0', '0', '127.0.0.1', NULL, NULL, 'admin', '2024-12-28 11:30:31', '', NULL, '测试员');
|
||||||
|
INSERT INTO `sys_user` VALUES (1875026959495516160, NULL, '运若汐', '运若汐', '', '88888', '', '0', NULL, '7c4a8d09ca3762af61e59520943dc26494f8941b', NULL, '0', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `sys_user` VALUES (1875027180531142656, NULL, '郸明', '郸明', '', '88888', '', '0', NULL, '7c4a8d09ca3762af61e59520943dc26494f8941b', NULL, '0', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `sys_user` VALUES (1875732675218882560, NULL, '易静', '易静', '', '88888', '', '0', NULL, '7c4a8d09ca3762af61e59520943dc26494f8941b', NULL, '0', '0', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
89
vertx-fw/build.gradle.kts
Normal file
89
vertx-fw/build.gradle.kts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "1.9.20"
|
||||||
|
id("com.diffplug.spotless") version "6.25.0"
|
||||||
|
id("com.github.johnrengelman.shadow") version "8.1.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "org.aikrai"
|
||||||
|
version = "1.0.0-SNAPSHOT"
|
||||||
|
|
||||||
|
val vertxVersion = "4.5.11"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.shadowJar {
|
||||||
|
archiveClassifier.set("fat")
|
||||||
|
mergeServiceFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
showStandardStreams = true
|
||||||
|
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.compileKotlin {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.compileTestKotlin {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spotless {
|
||||||
|
kotlin {
|
||||||
|
ktlint()
|
||||||
|
.editorConfigOverride(
|
||||||
|
mapOf(
|
||||||
|
"ktlint_standard_no-wildcard-imports" to "disabled",
|
||||||
|
"ktlint_standard_trailing-comma-on-call-site" to "disabled",
|
||||||
|
"ktlint_standard_trailing-comma-on-declaration-site" to "disabled",
|
||||||
|
"indent_size" to "2"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
target("src/**/*.kt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-core:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-web:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-config:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-config-yaml:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-pg-client:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-mysql-client:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-sql-client-templates:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-auth-jwt:$vertxVersion")
|
||||||
|
|
||||||
|
implementation("com.google.inject:guice:7.0.0")
|
||||||
|
implementation("org.reflections:reflections:0.9.12")
|
||||||
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
|
||||||
|
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2")
|
||||||
|
|
||||||
|
// hutool
|
||||||
|
implementation("cn.hutool:hutool-core:5.8.35")
|
||||||
|
|
||||||
|
// log
|
||||||
|
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
|
||||||
|
implementation("org.slf4j:slf4j-api:2.0.6")
|
||||||
|
implementation("ch.qos.logback:logback-classic:1.4.14")
|
||||||
|
implementation("org.codehaus.janino:janino:3.1.8")
|
||||||
|
|
||||||
|
// jpa
|
||||||
|
implementation("jakarta.persistence:jakarta.persistence-api:3.2.0")
|
||||||
|
implementation("jakarta.validation:jakarta.validation-api:3.1.0")
|
||||||
|
|
||||||
|
// doc
|
||||||
|
implementation("io.swagger.core.v3:swagger-core:2.2.27")
|
||||||
|
}
|
||||||
19
vertx-fw/src/main/kotlin/org/aikrai/vertx/auth/AuthUser.kt
Normal file
19
vertx-fw/src/main/kotlin/org/aikrai/vertx/auth/AuthUser.kt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package org.aikrai.vertx.auth
|
||||||
|
|
||||||
|
import io.vertx.ext.auth.impl.UserImpl
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
|
||||||
|
class AuthUser(
|
||||||
|
principal: Principal,
|
||||||
|
attributes: Attributes,
|
||||||
|
) : UserImpl(JsonUtil.toJsonObject(principal), JsonUtil.toJsonObject(attributes))
|
||||||
|
|
||||||
|
class Principal(
|
||||||
|
val id: Long,
|
||||||
|
val info: Any,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Attributes(
|
||||||
|
val role: Set<String>,
|
||||||
|
val permissions: Set<String>,
|
||||||
|
)
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package org.aikrai.vertx.auth
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class AllowAnonymous
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class CheckPermission(
|
||||||
|
val type: String = "",
|
||||||
|
val value: Array<String> = [],
|
||||||
|
val mode: Mode = Mode.AND,
|
||||||
|
val orRole: Array<String> = []
|
||||||
|
)
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class CheckRole(
|
||||||
|
val type: String = "",
|
||||||
|
val value: Array<String> = [],
|
||||||
|
val mode: Mode = Mode.AND
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class Mode {
|
||||||
|
AND,
|
||||||
|
OR
|
||||||
|
}
|
||||||
22
vertx-fw/src/main/kotlin/org/aikrai/vertx/auth/TokenUtil.kt
Normal file
22
vertx-fw/src/main/kotlin/org/aikrai/vertx/auth/TokenUtil.kt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package org.aikrai.vertx.auth
|
||||||
|
|
||||||
|
import io.vertx.core.json.JsonObject
|
||||||
|
import io.vertx.ext.auth.JWTOptions
|
||||||
|
import io.vertx.ext.auth.User
|
||||||
|
import io.vertx.ext.auth.authentication.TokenCredentials
|
||||||
|
import io.vertx.ext.auth.jwt.JWTAuth
|
||||||
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
|
|
||||||
|
class TokenUtil {
|
||||||
|
companion object {
|
||||||
|
fun genToken(jwtAuth: JWTAuth, info: Map<String, Any>): String {
|
||||||
|
val jwtOptions = JWTOptions().setExpiresInSeconds(60 * 60 * 24 * 7)
|
||||||
|
return jwtAuth.generateToken(JsonObject(info), jwtOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun authenticate(jwtAuth: JWTAuth, token: String): User? {
|
||||||
|
val tokenCredentials = TokenCredentials(token)
|
||||||
|
return jwtAuth.authenticate(tokenCredentials).coAwait() ?: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package org.aikrai.vertx.context
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class Controller(val prefix: String = "")
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.aikrai.vertx.context
|
||||||
|
|
||||||
|
import kotlin.annotation.AnnotationTarget
|
||||||
|
import kotlin.annotation.Target
|
||||||
|
|
||||||
|
@Target(allowedTargets = [AnnotationTarget.FUNCTION])
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class CustomizeRequest(
|
||||||
|
val method: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Target(allowedTargets = [AnnotationTarget.FUNCTION])
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class CustomizeResponse
|
||||||
11
vertx-fw/src/main/kotlin/org/aikrai/vertx/context/D.kt
Normal file
11
vertx-fw/src/main/kotlin/org/aikrai/vertx/context/D.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package org.aikrai.vertx.context
|
||||||
|
|
||||||
|
import kotlin.annotation.AnnotationTarget
|
||||||
|
import kotlin.annotation.Target
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class D(
|
||||||
|
val name: String = "",
|
||||||
|
val caption: String = ""
|
||||||
|
)
|
||||||
@ -0,0 +1,344 @@
|
|||||||
|
package org.aikrai.vertx.context
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import io.vertx.core.http.HttpMethod
|
||||||
|
import io.vertx.ext.auth.User
|
||||||
|
import io.vertx.ext.web.Router
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.aikrai.vertx.auth.*
|
||||||
|
import org.aikrai.vertx.utlis.ClassUtil
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
import org.reflections.Reflections
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.lang.reflect.ParameterizedType
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.reflect.KFunction
|
||||||
|
import kotlin.reflect.full.callSuspend
|
||||||
|
import kotlin.reflect.full.declaredFunctions
|
||||||
|
import kotlin.reflect.jvm.javaType
|
||||||
|
|
||||||
|
class RouterBuilder(
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
private val router: Router
|
||||||
|
) {
|
||||||
|
var anonymousPaths = ArrayList<String>()
|
||||||
|
|
||||||
|
fun build(getIt: (clazz: Class<*>) -> Any): RouterBuilder {
|
||||||
|
// 缓存路由信息
|
||||||
|
val routeInfoCache = mutableMapOf<Pair<String, HttpMethod>, RouteInfo>()
|
||||||
|
// 获取所有 Controller 类中的公共方法
|
||||||
|
val packagePath = ClassUtil.getMainClass()?.packageName
|
||||||
|
val controllerClassSet = Reflections(packagePath).getTypesAnnotatedWith(Controller::class.java)
|
||||||
|
val controllerMethods = ClassUtil.getPublicMethods(controllerClassSet)
|
||||||
|
for ((classType, methods) in controllerMethods) {
|
||||||
|
val controllerAnnotation = classType.getDeclaredAnnotationsByType(Controller::class.java).firstOrNull()
|
||||||
|
val prefixPath = controllerAnnotation?.prefix ?: ""
|
||||||
|
val classAllowAnonymous = classType.getAnnotation(AllowAnonymous::class.java) != null
|
||||||
|
if (classAllowAnonymous) {
|
||||||
|
val classPath = getReqPath(prefixPath, classType)
|
||||||
|
anonymousPaths.add("$classPath/**".replace("//", "/"))
|
||||||
|
}
|
||||||
|
for (method in methods) {
|
||||||
|
val reqPath = getReqPath(prefixPath, classType, method)
|
||||||
|
val httpMethod = getHttpMethod(method)
|
||||||
|
val allowAnonymous = method.getAnnotation(AllowAnonymous::class.java) != null
|
||||||
|
if (allowAnonymous) anonymousPaths.add(reqPath)
|
||||||
|
val customizeResp = method.getAnnotation(CustomizeResponse::class.java) != null
|
||||||
|
val role = method.getAnnotation(CheckRole::class.java)
|
||||||
|
val permissions = method.getAnnotation(CheckPermission::class.java)
|
||||||
|
val kFunction = classType.kotlin.declaredFunctions.find { it.name == method.name }
|
||||||
|
if (kFunction != null) {
|
||||||
|
val parameterInfo = kFunction.parameters.mapNotNull { parameter ->
|
||||||
|
val javaType = parameter.type.javaType
|
||||||
|
// 跳过协程的 Continuation 参数
|
||||||
|
if (javaType is Class<*> && Continuation::class.java.isAssignableFrom(javaType)) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
parameter.name ?: return@mapNotNull null
|
||||||
|
val annotation = parameter.annotations.find { it is D } as? D
|
||||||
|
val paramName = annotation?.name?.takeIf { it.isNotBlank() } ?: parameter.name ?: ""
|
||||||
|
val typeClass = when (javaType) {
|
||||||
|
is Class<*> -> javaType
|
||||||
|
is ParameterizedType -> javaType.rawType as? Class<*>
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
ParameterInfo(
|
||||||
|
name = paramName,
|
||||||
|
type = typeClass ?: parameter.type.javaType as Class<*>,
|
||||||
|
isNullable = parameter.type.isMarkedNullable,
|
||||||
|
isList = parameter.type.classifier == List::class,
|
||||||
|
isComplex = !parameter.type.classifier.toString().startsWith("class kotlin.") &&
|
||||||
|
!parameter.type.javaType.javaClass.isEnum &&
|
||||||
|
parameter.type.javaType is Class<*>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
routeInfoCache[reqPath to httpMethod] =
|
||||||
|
RouteInfo(classType, method, kFunction, parameterInfo, customizeResp, role, permissions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册路由处理器
|
||||||
|
routeInfoCache.forEach { (path, routeInfo) ->
|
||||||
|
router.route(routeInfo.httpMethod, path.first).handler { ctx ->
|
||||||
|
if (ctx.user() != null) {
|
||||||
|
val user = ctx.user() as AuthUser
|
||||||
|
if (!user.validateAuth(routeInfo)) {
|
||||||
|
ctx.fail(403, Meta.unauthorized("unauthorized"))
|
||||||
|
return@handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val instance = getIt(routeInfo.classType)
|
||||||
|
buildLambda(ctx, instance, routeInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildLambda(ctx: RoutingContext, instance: Any, routeInfo: RouteInfo) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
try {
|
||||||
|
val params = getParamsInstance(ctx, routeInfo.parameterInfo)
|
||||||
|
val result = if (routeInfo.kFunction.isSuspend) {
|
||||||
|
routeInfo.kFunction.callSuspend(instance, *params)
|
||||||
|
} else {
|
||||||
|
routeInfo.kFunction.call(instance, *params)
|
||||||
|
}
|
||||||
|
val json = serializeToJson(result)
|
||||||
|
if (routeInfo.customizeResp) return@launch
|
||||||
|
ctx.response()
|
||||||
|
.putHeader("Content-Type", "application/json")
|
||||||
|
.end(json)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handleError(ctx, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val objectMapper = jacksonObjectMapper()
|
||||||
|
|
||||||
|
private fun AuthUser.validateAuth(routeInfo: RouteInfo): Boolean {
|
||||||
|
// 如果没有权限要求,直接返回true
|
||||||
|
if (routeInfo.role == null && routeInfo.permissions == null) return true
|
||||||
|
// 验证角色
|
||||||
|
val hasValidRole = routeInfo.role?.let { role ->
|
||||||
|
val roleSet = attributes().getJsonArray("role").toSet() as Set<String>
|
||||||
|
if (roleSet.isEmpty()) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val reqRoleSet = (role.value + role.type).filter { it.isNotBlank() }.toSet()
|
||||||
|
validateSet(reqRoleSet, roleSet, role.mode)
|
||||||
|
}
|
||||||
|
} ?: true
|
||||||
|
|
||||||
|
// 验证权限
|
||||||
|
val hasValidPermission = routeInfo.permissions?.let { permissions ->
|
||||||
|
val permissionSet = attributes().getJsonArray("permissions").toSet() as Set<String>
|
||||||
|
val roleSet = attributes().getJsonArray("role").toSet() as Set<String>
|
||||||
|
if (permissionSet.isEmpty() && roleSet.isEmpty()) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
if (permissions.orRole.isNotEmpty()) {
|
||||||
|
val roleBoolean = validateSet(permissions.orRole.toSet(), roleSet, Mode.AND)
|
||||||
|
if (roleBoolean) return true
|
||||||
|
}
|
||||||
|
val reqPermissionSet = (permissions.value + permissions.type).filter { it.isNotBlank() }.toSet()
|
||||||
|
validateSet(reqPermissionSet, permissionSet, permissions.mode)
|
||||||
|
}
|
||||||
|
} ?: true
|
||||||
|
return hasValidRole && hasValidPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateSet(
|
||||||
|
required: Set<String>,
|
||||||
|
actual: Set<String>,
|
||||||
|
mode: Mode
|
||||||
|
): Boolean {
|
||||||
|
if (required.isEmpty()) return true
|
||||||
|
return when (mode) {
|
||||||
|
Mode.AND -> required == actual
|
||||||
|
Mode.OR -> required.any { it in actual }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getReqPath(prefix: String, clazz: Class<*>): String {
|
||||||
|
val basePath = if (prefix.isNotBlank()) {
|
||||||
|
StrUtil.toCamelCase(StrUtil.toUnderlineCase(prefix))
|
||||||
|
} else {
|
||||||
|
StrUtil.toCamelCase(StrUtil.toUnderlineCase(clazz.simpleName.removeSuffix("Controller")))
|
||||||
|
}
|
||||||
|
return "/$basePath".replace("//", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getReqPath(prefix: String, clazz: Class<*>, method: Method): String {
|
||||||
|
val basePath = if (prefix.isNotBlank()) {
|
||||||
|
StrUtil.toCamelCase(StrUtil.toUnderlineCase(prefix))
|
||||||
|
} else {
|
||||||
|
StrUtil.toCamelCase(StrUtil.toUnderlineCase(clazz.simpleName.removeSuffix("Controller")))
|
||||||
|
}
|
||||||
|
val methodName = StrUtil.toCamelCase(StrUtil.toUnderlineCase(method.name))
|
||||||
|
return "/$basePath/$methodName".replace("//", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getParamsInstance(ctx: RoutingContext, paramsInfo: List<ParameterInfo>): Array<Any?> {
|
||||||
|
val params = mutableListOf<Any?>()
|
||||||
|
val formAttributes = ctx.request().formAttributes().associate { it.key to it.value }
|
||||||
|
val queryParams = ctx.queryParams().entries().associate { it.key to it.value }
|
||||||
|
val combinedParams = formAttributes + queryParams
|
||||||
|
// 解析Body
|
||||||
|
val bodyStr = if (!ctx.body().isEmpty) ctx.body().asString() else ""
|
||||||
|
val bodyAsMap = if (bodyStr.isNotBlank()) {
|
||||||
|
try {
|
||||||
|
objectMapper.readValue(bodyStr, Map::class.java) as Map<String, Any>
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsInfo.forEach { param ->
|
||||||
|
if (param.isList) {
|
||||||
|
val listParamValue = ctx.queryParams().getAll(param.name)
|
||||||
|
if (listParamValue.isEmpty() && !param.isNullable) throw IllegalArgumentException("Missing required parameter: ${param.name}")
|
||||||
|
params.add(listParamValue)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
if (param.isComplex) {
|
||||||
|
try {
|
||||||
|
val value = objectMapper.readValue(bodyStr, param.type)
|
||||||
|
params.add(value)
|
||||||
|
return@forEach
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (!param.isNullable) throw IllegalArgumentException("Failed to parse request body for parameter: ${param.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.add(
|
||||||
|
when (param.type) {
|
||||||
|
RoutingContext::class.java -> ctx
|
||||||
|
User::class.java -> ctx.user()
|
||||||
|
else -> {
|
||||||
|
val bodyValue = bodyAsMap[param.name]
|
||||||
|
val paramValue = bodyValue?.toString() ?: combinedParams[param.name]
|
||||||
|
when {
|
||||||
|
paramValue == null -> {
|
||||||
|
if (!param.isNullable) throw IllegalArgumentException("Missing required parameter: ${param.name}") else null
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val value = getParamValue(paramValue.toString(), param.type)
|
||||||
|
if (!param.isNullable && value == null) {
|
||||||
|
throw IllegalArgumentException("Missing required parameter: ${param.name}")
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字符串参数值映射到目标类型。
|
||||||
|
*
|
||||||
|
* @param paramValue 参数的字符串值。
|
||||||
|
* @param type 目标 [Class] 类型。
|
||||||
|
* @return 转换为目标类型的参数值,如果转换失败则返回 `null`。
|
||||||
|
*/
|
||||||
|
private fun getParamValue(paramValue: String, type: Class<*>): Any? {
|
||||||
|
return when {
|
||||||
|
type.isEnum -> {
|
||||||
|
type.enumConstants.firstOrNull { (it as Enum<*>).name.equals(paramValue, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
type == String::class.java -> paramValue
|
||||||
|
type == Int::class.java || type == Integer::class.java -> paramValue.toIntOrNull()
|
||||||
|
type == Long::class.java || type == Long::class.java -> paramValue.toLongOrNull()
|
||||||
|
type == Double::class.java || type == Double::class.java -> paramValue.toDoubleOrNull()
|
||||||
|
type == Boolean::class.java || type == Boolean::class.java -> paramValue.toBoolean()
|
||||||
|
else -> paramValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 [CustomizeRequest] 注解确定给定 REST 方法的 HTTP 方法。
|
||||||
|
*
|
||||||
|
* @param method 目标方法。
|
||||||
|
* @return 对应的 [HttpMethod]。
|
||||||
|
*/
|
||||||
|
fun getHttpMethod(method: Method): HttpMethod {
|
||||||
|
val api = method.getAnnotation(CustomizeRequest::class.java)
|
||||||
|
return if (api != null) {
|
||||||
|
when (api.method.uppercase()) {
|
||||||
|
"GET" -> HttpMethod.GET
|
||||||
|
"PUT" -> HttpMethod.PUT
|
||||||
|
"DELETE" -> HttpMethod.DELETE
|
||||||
|
"PATCH" -> HttpMethod.PATCH
|
||||||
|
else -> HttpMethod.POST
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HttpMethod.POST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将对象序列化为 JSON 表示。
|
||||||
|
*
|
||||||
|
* @param obj 要序列化的对象。
|
||||||
|
* @return JSON 字符串。
|
||||||
|
*/
|
||||||
|
private fun serializeToJson(obj: Any?): String {
|
||||||
|
return objectMapper.writeValueAsString(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理错误并通过标准化的错误响应发送。
|
||||||
|
*
|
||||||
|
* @param ctx 发送响应的 [RoutingContext]。
|
||||||
|
* @param e 捕获的异常。
|
||||||
|
*/
|
||||||
|
private fun handleError(ctx: RoutingContext, e: Exception) {
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(500)
|
||||||
|
.putHeader("Content-Type", "application/json")
|
||||||
|
.end(
|
||||||
|
objectMapper.writeValueAsString(
|
||||||
|
mapOf(
|
||||||
|
"name" to e::class.simpleName,
|
||||||
|
"message" to (e.message ?: e.cause.toString()),
|
||||||
|
"data" to null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class RouteInfo(
|
||||||
|
val classType: Class<*>,
|
||||||
|
val method: Method,
|
||||||
|
val kFunction: KFunction<*>,
|
||||||
|
val parameterInfo: List<ParameterInfo>,
|
||||||
|
val customizeResp: Boolean,
|
||||||
|
val role: CheckRole? = null,
|
||||||
|
val permissions: CheckPermission? = null,
|
||||||
|
val httpMethod: HttpMethod = getHttpMethod(method)
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class ParameterInfo(
|
||||||
|
val name: String,
|
||||||
|
val type: Class<*>,
|
||||||
|
val isNullable: Boolean,
|
||||||
|
val isList: Boolean,
|
||||||
|
val isComplex: Boolean
|
||||||
|
)
|
||||||
|
}
|
||||||
59
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/DbPool.kt
Normal file
59
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/DbPool.kt
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package org.aikrai.vertx.db
|
||||||
|
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import com.google.inject.name.Named
|
||||||
|
import io.vertx.core.Vertx
|
||||||
|
import io.vertx.mysqlclient.MySQLBuilder
|
||||||
|
import io.vertx.mysqlclient.MySQLConnectOptions
|
||||||
|
import io.vertx.pgclient.PgBuilder
|
||||||
|
import io.vertx.pgclient.PgConnectOptions
|
||||||
|
import io.vertx.sqlclient.Pool
|
||||||
|
import io.vertx.sqlclient.PoolOptions
|
||||||
|
import io.vertx.sqlclient.SqlClient
|
||||||
|
|
||||||
|
class DbPool @Inject constructor(
|
||||||
|
vertx: Vertx,
|
||||||
|
@Named("databases.type") private val type: String,
|
||||||
|
@Named("databases.name") private val name: String,
|
||||||
|
@Named("databases.host") private val host: String,
|
||||||
|
@Named("databases.port") private val port: String,
|
||||||
|
@Named("databases.username") private val user: String,
|
||||||
|
@Named("databases.password") private val password: String
|
||||||
|
) {
|
||||||
|
private var pool: Pool
|
||||||
|
|
||||||
|
init {
|
||||||
|
val poolOptions = PoolOptions().setMaxSize(10)
|
||||||
|
pool = when (type.lowercase()) {
|
||||||
|
"mysql" -> {
|
||||||
|
val clientOptions = MySQLConnectOptions()
|
||||||
|
.setHost(host)
|
||||||
|
.setPort(port.toInt())
|
||||||
|
.setDatabase(name)
|
||||||
|
.setUser(user)
|
||||||
|
.setPassword(password)
|
||||||
|
.setTcpKeepAlive(true)
|
||||||
|
MySQLBuilder.pool().connectingTo(clientOptions).with(poolOptions).using(vertx).build()
|
||||||
|
}
|
||||||
|
"postgre", "postgresql" -> {
|
||||||
|
val clientOptions = PgConnectOptions()
|
||||||
|
.setHost(host)
|
||||||
|
.setPort(port.toInt())
|
||||||
|
.setDatabase(name)
|
||||||
|
.setUser(user)
|
||||||
|
.setPassword(password)
|
||||||
|
.setTcpKeepAlive(true)
|
||||||
|
PgBuilder.pool().connectingTo(clientOptions).with(poolOptions).using(vertx).build()
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("Unsupported database type: $type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getClient(): SqlClient {
|
||||||
|
return pool
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPool(): Pool {
|
||||||
|
return pool
|
||||||
|
}
|
||||||
|
}
|
||||||
39
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/QueryWrapper.kt
Normal file
39
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/QueryWrapper.kt
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package org.aikrai.vertx.db
|
||||||
|
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
|
interface QueryWrapper<T> {
|
||||||
|
fun select(vararg columns: String): QueryWrapper<T>
|
||||||
|
fun select(vararg columns: KProperty1<T, *>): QueryWrapper<T>
|
||||||
|
|
||||||
|
fun eq(column: String, value: Any): QueryWrapper<T>
|
||||||
|
fun eq(column: KProperty1<T, *>, value: Any): QueryWrapper<T>
|
||||||
|
fun eq(condition: Boolean, column: String, value: Any): QueryWrapper<T>
|
||||||
|
fun eq(condition: Boolean, column: KProperty1<T, *>, value: Any): QueryWrapper<T>
|
||||||
|
|
||||||
|
fun from(table: String): QueryWrapper<T>
|
||||||
|
|
||||||
|
fun like(column: String, value: String): QueryWrapper<T>
|
||||||
|
fun like(column: KProperty1<T, *>, value: String): QueryWrapper<T>
|
||||||
|
// fun like(condition: Boolean, column: String, value: String): QueryWrapper<T>
|
||||||
|
// fun like(condition: Boolean, column: KProperty1<T, *>, value: String): QueryWrapper<T>
|
||||||
|
|
||||||
|
// fun likeLeft(column: String, value: String): QueryWrapper<T>
|
||||||
|
fun likeLeft(column: KProperty1<T, *>, value: String): QueryWrapper<T>
|
||||||
|
|
||||||
|
// fun likeRight(column: String, value: String): QueryWrapper<T>
|
||||||
|
fun likeRight(column: KProperty1<T, *>, value: String): QueryWrapper<T>
|
||||||
|
|
||||||
|
fun `in`(column: KProperty1<T, *>, values: Collection<*>): QueryWrapper<T>
|
||||||
|
fun notIn(column: KProperty1<T, *>, values: Collection<*>): QueryWrapper<T>
|
||||||
|
|
||||||
|
fun groupBy(vararg columns: KProperty1<T, *>): QueryWrapper<T>
|
||||||
|
fun having(condition: String): QueryWrapper<T>
|
||||||
|
|
||||||
|
fun orderByAsc(vararg columns: KProperty1<T, *>): QueryWrapper<T>
|
||||||
|
fun orderByDesc(vararg columns: KProperty1<T, *>): QueryWrapper<T>
|
||||||
|
|
||||||
|
fun genSql(): String
|
||||||
|
suspend fun getList(): List<T>
|
||||||
|
suspend fun getOne(): T?
|
||||||
|
}
|
||||||
303
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/QueryWrapperImpl.kt
Normal file
303
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/QueryWrapperImpl.kt
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
package org.aikrai.vertx.db
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil
|
||||||
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
|
import io.vertx.sqlclient.Row
|
||||||
|
import io.vertx.sqlclient.SqlClient
|
||||||
|
import io.vertx.sqlclient.templates.SqlTemplate
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
import kotlin.reflect.jvm.javaField
|
||||||
|
|
||||||
|
class QueryWrapperImpl<T : Any>(
|
||||||
|
private val entityClass: Class<T>,
|
||||||
|
private val sqlClient: SqlClient,
|
||||||
|
) : QueryWrapper<T> {
|
||||||
|
private val conditions = mutableListOf<QueryCondition>()
|
||||||
|
|
||||||
|
override fun select(vararg columns: String): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.SELECT,
|
||||||
|
column = columns.joinToString(",")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun select(vararg columns: KProperty1<T, *>): QueryWrapper<T> {
|
||||||
|
columns.forEach {
|
||||||
|
val columnName = it.javaField?.getAnnotation(Column::class.java)?.name?.takeIf { it.isNotBlank() }
|
||||||
|
?: StrUtil.toUnderlineCase(it.name)
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.SELECT,
|
||||||
|
column = columnName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eq(column: String, value: Any): QueryWrapper<T> {
|
||||||
|
return eq(true, column, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eq(column: KProperty1<T, *>, value: Any): QueryWrapper<T> {
|
||||||
|
return eq(true, column, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eq(condition: Boolean, column: String, value: Any): QueryWrapper<T> {
|
||||||
|
if (condition) {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.WHERE,
|
||||||
|
column = column,
|
||||||
|
operator = "=",
|
||||||
|
value = value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eq(condition: Boolean, column: KProperty1<T, *>, value: Any): QueryWrapper<T> {
|
||||||
|
if (condition) {
|
||||||
|
val columnName = column.javaField?.getAnnotation(Column::class.java)?.name?.takeIf { it.isNotBlank() }
|
||||||
|
?: StrUtil.toUnderlineCase(column.name)
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.WHERE,
|
||||||
|
column = columnName,
|
||||||
|
operator = "=",
|
||||||
|
value = value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun from(table: String): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.FROM,
|
||||||
|
column = table
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun like(column: String, value: String): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.LIKE,
|
||||||
|
column = column,
|
||||||
|
operator = "LIKE",
|
||||||
|
value = "%$value%"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun like(column: KProperty1<T, *>, value: String): QueryWrapper<T> {
|
||||||
|
conditions.add(QueryCondition(QueryType.LIKE, column.name, "LIKE", "%$value%"))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun likeLeft(column: KProperty1<T, *>, value: String): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.WHERE,
|
||||||
|
column = column.name,
|
||||||
|
operator = "LIKE",
|
||||||
|
value = "%$value"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun likeRight(column: KProperty1<T, *>, value: String): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.WHERE,
|
||||||
|
column = column.name,
|
||||||
|
operator = "LIKE",
|
||||||
|
value = "$value%"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun `in`(column: KProperty1<T, *>, values: Collection<*>): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.WHERE,
|
||||||
|
column = column.name,
|
||||||
|
operator = "IN",
|
||||||
|
value = values
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notIn(column: KProperty1<T, *>, values: Collection<*>): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.WHERE,
|
||||||
|
column = column.name,
|
||||||
|
operator = "NOT IN",
|
||||||
|
value = values
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun groupBy(vararg columns: KProperty1<T, *>): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.GROUP_BY,
|
||||||
|
column = columns.joinToString(",") { it.name }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun having(condition: String): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.HAVING,
|
||||||
|
column = condition
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun orderByAsc(vararg columns: KProperty1<T, *>): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.ORDER_BY,
|
||||||
|
column = columns.joinToString(",") { it.name },
|
||||||
|
additional = mapOf("direction" to "ASC")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun orderByDesc(vararg columns: KProperty1<T, *>): QueryWrapper<T> {
|
||||||
|
conditions.add(
|
||||||
|
QueryCondition(
|
||||||
|
type = QueryType.ORDER_BY,
|
||||||
|
column = columns.joinToString(",") { it.name },
|
||||||
|
additional = mapOf("direction" to "DESC")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSql(): String {
|
||||||
|
val sqlBuilder = StringBuilder()
|
||||||
|
|
||||||
|
// SELECT 子句
|
||||||
|
sqlBuilder.append("SELECT ")
|
||||||
|
val selectCondition = conditions.find { it.type == QueryType.SELECT }
|
||||||
|
if (selectCondition != null) {
|
||||||
|
sqlBuilder.append(selectCondition.column)
|
||||||
|
} else {
|
||||||
|
sqlBuilder.append("*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FROM 子句
|
||||||
|
val from = conditions.filter { it.type == QueryType.FROM }
|
||||||
|
if (from.isNotEmpty()) {
|
||||||
|
sqlBuilder.append(" FROM ${from.first().column}")
|
||||||
|
} else {
|
||||||
|
entityClass.getAnnotation(Table::class.java)?.name?.let {
|
||||||
|
sqlBuilder.append(" FROM $it")
|
||||||
|
} ?: sqlBuilder.append(" FROM ${StrUtil.toUnderlineCase(entityClass.simpleName)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHERE 子句
|
||||||
|
val whereConditions = conditions.filter { it.type == QueryType.WHERE }
|
||||||
|
if (whereConditions.isNotEmpty()) {
|
||||||
|
sqlBuilder.append(" WHERE ")
|
||||||
|
sqlBuilder.append(
|
||||||
|
whereConditions.joinToString(" AND ") {
|
||||||
|
when (it.operator) {
|
||||||
|
"IN", "NOT IN" -> "${it.column} ${it.operator} (${(it.value as Collection<*>).joinToString(",")})"
|
||||||
|
"LIKE" -> "${it.column} ${it.operator} '${it.value}'"
|
||||||
|
else -> "${it.column} ${it.operator} '${it.value}'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GROUP BY 子句
|
||||||
|
conditions.find { it.type == QueryType.GROUP_BY }?.let {
|
||||||
|
sqlBuilder.append(" GROUP BY ${it.column}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HAVING 子句
|
||||||
|
conditions.find { it.type == QueryType.HAVING }?.let {
|
||||||
|
sqlBuilder.append(" HAVING ${it.column}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORDER BY 子句
|
||||||
|
val orderByConditions = conditions.filter { it.type == QueryType.ORDER_BY }
|
||||||
|
if (orderByConditions.isNotEmpty()) {
|
||||||
|
sqlBuilder.append(" ORDER BY ")
|
||||||
|
sqlBuilder.append(
|
||||||
|
orderByConditions.joinToString(", ") {
|
||||||
|
"${it.column} ${it.additional["direction"]}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqlBuilder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun genSql(): String {
|
||||||
|
return buildSql()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getList(): List<T> {
|
||||||
|
val sql = buildSql()
|
||||||
|
val objs = SqlTemplate
|
||||||
|
.forQuery(sqlClient, sql)
|
||||||
|
.mapTo(Row::toJson)
|
||||||
|
.execute(emptyMap())
|
||||||
|
.coAwait()
|
||||||
|
.toList()
|
||||||
|
return objs.map { JsonUtil.parseObject(it.encode(), entityClass) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getOne(): T? {
|
||||||
|
val sql = buildSql()
|
||||||
|
val obj = SqlTemplate
|
||||||
|
.forQuery(sqlClient, sql)
|
||||||
|
.mapTo(Row::toJson)
|
||||||
|
.execute(emptyMap())
|
||||||
|
.coAwait()
|
||||||
|
.firstOrNull()
|
||||||
|
return obj?.let { JsonUtil.parseObject(it.encode(), entityClass) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class QueryType {
|
||||||
|
SELECT,
|
||||||
|
WHERE,
|
||||||
|
FROM,
|
||||||
|
GROUP_BY,
|
||||||
|
HAVING,
|
||||||
|
ORDER_BY,
|
||||||
|
LIKE
|
||||||
|
}
|
||||||
|
|
||||||
|
data class QueryCondition(
|
||||||
|
val type: QueryType,
|
||||||
|
val column: String,
|
||||||
|
val operator: String? = null,
|
||||||
|
val value: Any? = null,
|
||||||
|
val additional: Map<String, Any> = emptyMap()
|
||||||
|
)
|
||||||
16
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/Repository.kt
Normal file
16
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/Repository.kt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package org.aikrai.vertx.db
|
||||||
|
|
||||||
|
interface Repository<TId, TEntity> {
|
||||||
|
suspend fun create(t: TEntity): Int
|
||||||
|
suspend fun delete(id: TId): Int
|
||||||
|
suspend fun update(t: TEntity): Int
|
||||||
|
suspend fun update(id: TId, parameters: Map<String, Any?>): Int
|
||||||
|
suspend fun get(id: TId): TEntity?
|
||||||
|
|
||||||
|
suspend fun createBatch(list: List<TEntity>): Int
|
||||||
|
|
||||||
|
suspend fun <R> execute(sql: String): R
|
||||||
|
|
||||||
|
suspend fun queryBuilder(): QueryWrapper<TEntity>
|
||||||
|
suspend fun queryBuilder(clazz: Class<*>): QueryWrapper<*>
|
||||||
|
}
|
||||||
272
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/RepositoryImpl.kt
Normal file
272
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/RepositoryImpl.kt
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
package org.aikrai.vertx.db
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference
|
||||||
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
|
import io.vertx.sqlclient.*
|
||||||
|
import io.vertx.sqlclient.templates.SqlTemplate
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.db.tx.TxCtx
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
import java.lang.reflect.ParameterizedType
|
||||||
|
import java.sql.Timestamp
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
open class RepositoryImpl<TId, TEntity : Any>(
|
||||||
|
private val sqlClient: SqlClient
|
||||||
|
) : Repository<TId, TEntity> {
|
||||||
|
private val clazz: Class<TEntity> = (this::class.java.genericSuperclass as ParameterizedType)
|
||||||
|
.actualTypeArguments[1] as Class<TEntity>
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val sqlTemplateMap: Map<Pair<String, String>, String> = mutableMapOf()
|
||||||
|
private val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSXXX")
|
||||||
|
|
||||||
|
override suspend fun <R> execute(sql: String): R {
|
||||||
|
return if (sql.trimStart().startsWith("SELECT", true)) {
|
||||||
|
val list = SqlTemplate.forQuery(getConnection(), sql).execute(mapOf())
|
||||||
|
.coAwait().map { it.toJson() }
|
||||||
|
val jsonObject = JsonUtil.toJsonObject(list)
|
||||||
|
val typeReference = object : TypeReference<R>() {}
|
||||||
|
JsonUtil.parseObject(jsonObject, typeReference, true)
|
||||||
|
} else {
|
||||||
|
val rowCount = SqlTemplate.forUpdate(getConnection(), sql).execute(mapOf())
|
||||||
|
.coAwait().rowCount()
|
||||||
|
rowCount as R
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun create(t: TEntity): Int {
|
||||||
|
val tableName = getTableName()
|
||||||
|
val sqlTemplate = sqlTemplateMap[Pair(tableName, "create")] ?: run {
|
||||||
|
val idColumnName = getIdColumnName()
|
||||||
|
val columnsMap = getColumnMappings()
|
||||||
|
// Exclude 'id' field if it's auto-generated
|
||||||
|
val fields = clazz.declaredFields.filter { it.name != idColumnName }
|
||||||
|
val columns = fields.map { columnsMap[it.name] }
|
||||||
|
val parameters = fields.map { it.name }
|
||||||
|
val sql =
|
||||||
|
"INSERT INTO $tableName (${columns.joinToString(", ")}) VALUES (${parameters.joinToString(", ") { "#{$it}" }})"
|
||||||
|
sqlTemplateMap.plus(Pair(tableName, "create")) to sql
|
||||||
|
sql
|
||||||
|
}
|
||||||
|
val params = getNonNullFields(t)
|
||||||
|
logger.info { "SQL: $sqlTemplate, PARAMS: $params" }
|
||||||
|
return SqlTemplate.forUpdate(getConnection(), sqlTemplate)
|
||||||
|
.execute(params)
|
||||||
|
.coAwait()
|
||||||
|
.rowCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(id: TId): Int {
|
||||||
|
val tableName = getTableName()
|
||||||
|
val sqlTemplate = sqlTemplateMap[Pair(tableName, "delete")] ?: run {
|
||||||
|
val idColumnName = getIdColumnName()
|
||||||
|
val sql = "DELETE FROM $tableName WHERE $idColumnName = #{id}"
|
||||||
|
sqlTemplateMap.plus(Pair(tableName, "delete")) to sql
|
||||||
|
sql
|
||||||
|
}
|
||||||
|
val params = mapOf("id" to id)
|
||||||
|
logger.debug { "SQL: $sqlTemplate, PARAMS: $params" }
|
||||||
|
return SqlTemplate.forUpdate(getConnection(), sqlTemplate)
|
||||||
|
.execute(params)
|
||||||
|
.coAwait()
|
||||||
|
.rowCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(t: TEntity): Int {
|
||||||
|
val tableName = getTableName()
|
||||||
|
val sqlTemplate = sqlTemplateMap[Pair(tableName, "update")] ?: run {
|
||||||
|
val idColumnName = getIdColumnName()
|
||||||
|
val columnsMap = getColumnMappings()
|
||||||
|
// Exclude 'id' from update fields
|
||||||
|
val fields = clazz.declaredFields.filter { it.name != idColumnName }
|
||||||
|
val setClause = fields.joinToString(", ") { "${columnsMap[it.name]} = #{${it.name}}" }
|
||||||
|
val sql = "UPDATE $tableName SET $setClause WHERE $idColumnName = #{id}"
|
||||||
|
sqlTemplateMap.plus(Pair(tableName, "update")) to sql
|
||||||
|
sql
|
||||||
|
}
|
||||||
|
// Get id value
|
||||||
|
val idColumnName = getIdColumnName()
|
||||||
|
val idField = clazz.declaredFields.find { it.name == idColumnName }
|
||||||
|
?: throw IllegalArgumentException("Class ${clazz.simpleName} must have an 'id' field for update operation.")
|
||||||
|
idField.isAccessible = true
|
||||||
|
val idValue = idField.get(t)
|
||||||
|
// Prepare parameters
|
||||||
|
val params = getNonNullFields(t) + mapOf("id" to idValue)
|
||||||
|
logger.debug { "SQL: $sqlTemplate, PARAMS: $params" }
|
||||||
|
return SqlTemplate.forUpdate(getConnection(), sqlTemplate)
|
||||||
|
.execute(params)
|
||||||
|
.coAwait()
|
||||||
|
.rowCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(id: TId, parameters: Map<String, Any?>): Int {
|
||||||
|
val tableName = getTableName()
|
||||||
|
val sqlTemplate = sqlTemplateMap[Pair(tableName, "update")] ?: run {
|
||||||
|
val idColumnName = getIdColumnName()
|
||||||
|
val columnsMap = getColumnMappings()
|
||||||
|
val setClause = parameters.keys.joinToString(", ") { "${columnsMap[it]} = #{$it}" }
|
||||||
|
val sql = "UPDATE $tableName SET $setClause WHERE $idColumnName = #{id}"
|
||||||
|
sqlTemplateMap.plus(Pair(tableName, "update")) to sql
|
||||||
|
sql
|
||||||
|
}
|
||||||
|
val params = parameters + mapOf("id" to id)
|
||||||
|
logger.debug { "SQL: $sqlTemplate, PARAMS: $params" }
|
||||||
|
return SqlTemplate.forUpdate(getConnection(), sqlTemplate)
|
||||||
|
.execute(params)
|
||||||
|
.coAwait()
|
||||||
|
.rowCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun get(id: TId): TEntity? {
|
||||||
|
val tableName = getTableName()
|
||||||
|
val sqlTemplate = sqlTemplateMap[Pair(tableName, "get")] ?: run {
|
||||||
|
val idColumnName = getIdColumnName()
|
||||||
|
val columnsMap = getColumnMappings()
|
||||||
|
val columns = columnsMap.values.joinToString(", ")
|
||||||
|
val sql = "SELECT $columns FROM $tableName WHERE $idColumnName = #{id}"
|
||||||
|
(sqlTemplateMap as MutableMap)[Pair(tableName, "get")] = sql
|
||||||
|
sql
|
||||||
|
}
|
||||||
|
val params = mapOf("id" to id)
|
||||||
|
logger.debug { "SQL: $sqlTemplate, PARAMS: $params" }
|
||||||
|
val rows = SqlTemplate
|
||||||
|
.forQuery(getConnection(), sqlTemplate)
|
||||||
|
.mapTo(Row::toJson)
|
||||||
|
.execute(params)
|
||||||
|
.coAwait()
|
||||||
|
.firstOrNull()
|
||||||
|
return rows?.let { JsonUtil.parseObject(it.toString(), clazz, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun queryBuilder(): QueryWrapper<TEntity> {
|
||||||
|
return QueryWrapperImpl(clazz, getConnection())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun queryBuilder(clazz: Class<*>): QueryWrapper<*> {
|
||||||
|
return QueryWrapperImpl(clazz, getConnection())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getConnection(): SqlClient {
|
||||||
|
return if (TxCtx.isTransactionActive(coroutineContext)) {
|
||||||
|
TxCtx.currentSqlConnection(coroutineContext) ?: run {
|
||||||
|
logger.error("TransactionContextElement.sqlConnection is null")
|
||||||
|
return sqlClient
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sqlClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他工具方法
|
||||||
|
override suspend fun createBatch(list: List<TEntity>): Int {
|
||||||
|
if (list.isEmpty()) return 0
|
||||||
|
var rowCount = 0
|
||||||
|
list.chunked(1000).forEach {
|
||||||
|
val sql = genBatchInsertSql(it)
|
||||||
|
rowCount += SqlTemplate.forUpdate(sqlClient, sql)
|
||||||
|
.execute(emptyMap())
|
||||||
|
.coAwait()
|
||||||
|
.rowCount()
|
||||||
|
}
|
||||||
|
return rowCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具方法:获取表名
|
||||||
|
private fun getTableName(): String {
|
||||||
|
return clazz.getAnnotation(Table::class.java)?.name?.takeIf { it.isNotBlank() }
|
||||||
|
?: StrUtil.toUnderlineCase(clazz.simpleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加获取ID字段名称的方法
|
||||||
|
private fun getIdColumnName(): String {
|
||||||
|
val idField = clazz.declaredFields.find { it.isAnnotationPresent(Id::class.java) }
|
||||||
|
?: throw IllegalArgumentException("No @Id field found in ${clazz.simpleName}")
|
||||||
|
return idField.getAnnotation(Column::class.java)?.name?.takeIf { it.isNotBlank() }
|
||||||
|
?: StrUtil.toUnderlineCase(idField.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getColumnMappings(): Map<String, String> {
|
||||||
|
return clazz.declaredFields.associate { field ->
|
||||||
|
val columnAnnotation = field.getAnnotation(Column::class.java)
|
||||||
|
val columnName = columnAnnotation?.name?.takeIf { it.isNotBlank() }
|
||||||
|
?: StrUtil.toUnderlineCase(field.name)
|
||||||
|
field.name to columnName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具方法:获取非空字段及其值
|
||||||
|
private fun getNonNullFields(t: TEntity): Map<String, Any> {
|
||||||
|
return clazz.declaredFields
|
||||||
|
.filter { field ->
|
||||||
|
field.isAccessible = true
|
||||||
|
// 排除被 @Transient 注解标记的字段
|
||||||
|
!field.isAnnotationPresent(Transient::class.java) &&
|
||||||
|
field.get(t) != null
|
||||||
|
}
|
||||||
|
.associate { field ->
|
||||||
|
field.name to field.get(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成批量 INSERT SQL 语句的函数
|
||||||
|
* @param objects 要插入的对象列表
|
||||||
|
* @return 生成的 SQL 语句字符串
|
||||||
|
*/
|
||||||
|
private fun <TEntity> genBatchInsertSql(objects: List<TEntity>): String {
|
||||||
|
// 如果对象列表为空,直接返回空字符串
|
||||||
|
if (objects.isEmpty()) return ""
|
||||||
|
|
||||||
|
// 将类名转换为下划线命名的表名,例如:UserInfo -> user_info
|
||||||
|
val tableName = StrUtil.toUnderlineCase(clazz.simpleName)
|
||||||
|
|
||||||
|
// 获取类的所有字段,包括私有字段
|
||||||
|
val fields = clazz.declaredFields.filter {
|
||||||
|
// 过滤掉静态字段和合成字段
|
||||||
|
!Modifier.isStatic(it.modifiers) && !it.isSynthetic
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保所有字段可访问
|
||||||
|
fields.forEach { it.isAccessible = true }
|
||||||
|
|
||||||
|
// 将字段名转换为下划线命名的列名,并用逗号隔开
|
||||||
|
val columnNames = fields.joinToString(", ") { StrUtil.toUnderlineCase(it.name) }
|
||||||
|
|
||||||
|
// SQL 转义函数
|
||||||
|
fun escapeSql(value: String): String = value.replace("'", "''")
|
||||||
|
|
||||||
|
// 格式化属性值为 SQL 字符串
|
||||||
|
fun formatValue(value: Any?): String = when (value) {
|
||||||
|
null -> "NULL" // 如果值为 null,返回字符串 "NULL"
|
||||||
|
is String -> "'${escapeSql(value)}'" // 字符串类型,加单引号并进行转义
|
||||||
|
is Enum<*> -> "'${value.name}'" // 枚举类型,使用枚举名,添加单引号
|
||||||
|
is Number, is Boolean -> value.toString() // 数字和布尔类型,直接转换为字符串
|
||||||
|
is Timestamp -> // 时间戳类型,格式化为指定的日期时间字符串
|
||||||
|
"'${formatter.format(value.toInstant().atZone(ZoneId.of("Asia/Shanghai")))}'"
|
||||||
|
|
||||||
|
is Array<*> -> // 数组类型处理
|
||||||
|
if (value.isEmpty()) "'{}'" else "'{${value.joinToString(",") { escapeSql(it?.toString() ?: "NULL") }}}'"
|
||||||
|
|
||||||
|
is Collection<*> -> // 集合类型处理
|
||||||
|
if (value.isEmpty()) "'{}'" else "'{${value.joinToString(",") { escapeSql(it?.toString() ?: "NULL") }}}'"
|
||||||
|
|
||||||
|
else -> "'${escapeSql(value.toString())}'" // 其他类型,调用 toString() 后转义并加单引号
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 VALUES 部分,每个对象对应一组值
|
||||||
|
val valuesList = objects.map { instance ->
|
||||||
|
fields.joinToString(", ", "(", ")") { field ->
|
||||||
|
// 获取属性值,并格式化为 SQL 字符串
|
||||||
|
formatValue(field.get(instance))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "INSERT INTO $tableName ($columnNames) VALUES ${valuesList.joinToString(", ")};"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package org.aikrai.vertx.db
|
||||||
|
|
||||||
|
object SqlHelper {
|
||||||
|
|
||||||
|
fun retBool(result: Int?): Boolean {
|
||||||
|
return null != result && result >= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
41
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/tx/TxCtxElem.kt
Normal file
41
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/tx/TxCtxElem.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package org.aikrai.vertx.db.tx
|
||||||
|
|
||||||
|
import io.vertx.sqlclient.SqlConnection
|
||||||
|
import io.vertx.sqlclient.Transaction
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class TxCtxElem(
|
||||||
|
val sqlConnection: SqlConnection,
|
||||||
|
val transaction: Transaction,
|
||||||
|
val isActive: Boolean = true,
|
||||||
|
val isNested: Boolean = false,
|
||||||
|
val transactionStack: Stack<TxCtxElem>,
|
||||||
|
val index: Int = transactionStack.size,
|
||||||
|
val transactionId: String = UUID.randomUUID().toString()
|
||||||
|
) : CoroutineContext.Element {
|
||||||
|
companion object Key : CoroutineContext.Key<TxCtxElem>
|
||||||
|
override val key: CoroutineContext.Key<*> = Key
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "TransactionContextElement(transactionId=$transactionId, isActive=$isActive, isNested=$isNested)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object TxCtx {
|
||||||
|
fun getTransactionId(context: CoroutineContext): String? {
|
||||||
|
return context[TxCtxElem.Key]?.transactionId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentTransaction(context: CoroutineContext): Transaction? {
|
||||||
|
return context[TxCtxElem.Key]?.transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentSqlConnection(context: CoroutineContext): SqlConnection? {
|
||||||
|
return context[TxCtxElem.Key]?.sqlConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTransactionActive(context: CoroutineContext): Boolean {
|
||||||
|
return context[TxCtxElem.Key]?.isActive ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
104
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/tx/TxMgr.kt
Normal file
104
vertx-fw/src/main/kotlin/org/aikrai/vertx/db/tx/TxMgr.kt
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package org.aikrai.vertx.db.tx
|
||||||
|
|
||||||
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
|
import io.vertx.sqlclient.Pool
|
||||||
|
import io.vertx.sqlclient.SqlConnection
|
||||||
|
import io.vertx.sqlclient.Transaction
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
suspend fun <T> withTransaction(block: suspend CoroutineScope.() -> T): Any? {
|
||||||
|
return TxMgrHolder.txMgr.withTransaction(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
object TxMgrHolder {
|
||||||
|
private val _txMgr = AtomicReference<TxMgr?>(null)
|
||||||
|
|
||||||
|
val txMgr: TxMgr
|
||||||
|
get() = _txMgr.get() ?: throw Meta.failure(
|
||||||
|
"TransactionError",
|
||||||
|
"TxMgr(TransactionManager)尚未初始化,请先调用initTxMgr()"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原子地初始化 TxMgr(TransactionManager)。
|
||||||
|
* 如果已经初始化,该方法将直接返回。
|
||||||
|
*
|
||||||
|
* @param pool SQL 客户端连接池。
|
||||||
|
*/
|
||||||
|
fun initTxMgr(pool: Pool) {
|
||||||
|
if (_txMgr.get() != null) return
|
||||||
|
val newManager = TxMgr(pool)
|
||||||
|
_txMgr.compareAndSet(null, newManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TxMgr(
|
||||||
|
private val pool: Pool
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
private val transactionStackMap = ConcurrentHashMap<CoroutineContext, Stack<TxCtxElem>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在事务上下文中执行一个块。
|
||||||
|
*
|
||||||
|
* @param block 需要在事务中执行的挂起函数。
|
||||||
|
* @return 块的结果。
|
||||||
|
*/
|
||||||
|
suspend fun <T> withTransaction(block: suspend CoroutineScope.() -> T): Any? {
|
||||||
|
val currentContext = coroutineContext
|
||||||
|
val transactionStack = currentContext[TxCtxElem]?.transactionStack ?: Stack<TxCtxElem>()
|
||||||
|
// 外层事务,嵌套事务,都创建新的连接和事务。实现外层事务回滚时所有嵌套事务回滚,嵌套事务回滚不影响外部事务
|
||||||
|
val connection: SqlConnection = pool.connection.coAwait()
|
||||||
|
val transaction: Transaction = connection.begin().coAwait()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val txCtxElem =
|
||||||
|
TxCtxElem(connection, transaction, true, transactionStack.isNotEmpty(), transactionStack)
|
||||||
|
transactionStack.push(txCtxElem)
|
||||||
|
logger.debug { (if (txCtxElem.isNested) "嵌套" else "") + "事务Id:" + txCtxElem.transactionId + "开始" }
|
||||||
|
|
||||||
|
withContext(currentContext + txCtxElem) {
|
||||||
|
val result = block()
|
||||||
|
if (txCtxElem.index == 0) {
|
||||||
|
while (transactionStack.isNotEmpty()) {
|
||||||
|
val txCtx = transactionStack.pop()
|
||||||
|
txCtx.transaction.commit().coAwait()
|
||||||
|
logger.debug { (if (txCtx.isNested) "嵌套" else "") + "事务Id:" + txCtx.transactionId + "提交" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Transaction failed, rollback" }
|
||||||
|
if (transactionStack.isNotEmpty() && !transactionStack.peek().isNested) {
|
||||||
|
// 外层事务失败,回滚所有事务
|
||||||
|
logger.error { "Rolling back all transactions" }
|
||||||
|
while (transactionStack.isNotEmpty()) {
|
||||||
|
val txCtxElem = transactionStack.pop()
|
||||||
|
txCtxElem.transaction.rollback().coAwait()
|
||||||
|
logger.debug { (if (txCtxElem.isNested) "嵌套" else "") + "事务Id:" + txCtxElem.transactionId + "回滚" }
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
} else {
|
||||||
|
// 嵌套事务失败,只回滚当前事务
|
||||||
|
val txCtxElem = transactionStack.pop()
|
||||||
|
txCtxElem.transaction.rollback().coAwait()
|
||||||
|
logger.debug(e) { (if (txCtxElem.isNested) "嵌套" else "") + "事务Id:" + txCtxElem.transactionId + "回滚" }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (transactionStack.isEmpty()) {
|
||||||
|
transactionStackMap.remove(currentContext) // 清理上下文
|
||||||
|
connection.close() // 仅在外层事务时关闭连接
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package org.aikrai.vertx.jackson
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyName
|
||||||
|
import com.fasterxml.jackson.databind.introspect.Annotated
|
||||||
|
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
|
||||||
|
class ColumnAnnotationIntrospector : JacksonAnnotationIntrospector() {
|
||||||
|
override fun findNameForDeserialization(annotated: Annotated?): PropertyName? {
|
||||||
|
return getColumnName(annotated)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findNameForSerialization(annotated: Annotated?): PropertyName? {
|
||||||
|
return getColumnName(annotated)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getColumnName(annotated: Annotated?): PropertyName? {
|
||||||
|
if (annotated == null) return null
|
||||||
|
val column = annotated.getAnnotation(Column::class.java)
|
||||||
|
return column?.let { PropertyName(it.name) }
|
||||||
|
}
|
||||||
|
}
|
||||||
145
vertx-fw/src/main/kotlin/org/aikrai/vertx/jackson/JsonUtil.kt
Normal file
145
vertx-fw/src/main/kotlin/org/aikrai/vertx/jackson/JsonUtil.kt
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package org.aikrai.vertx.jackson
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import com.fasterxml.jackson.databind.JavaType
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||||
|
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair
|
||||||
|
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import io.vertx.core.json.JsonArray
|
||||||
|
import io.vertx.core.json.JsonObject
|
||||||
|
|
||||||
|
object JsonUtil {
|
||||||
|
|
||||||
|
fun configure(writeClassName: Boolean) {
|
||||||
|
objectMapper = createObjectMapper(writeClassName)
|
||||||
|
objectMapperSnakeCase = createObjectMapperSnakeCase(writeClassName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var objectMapper = createObjectMapper(false)
|
||||||
|
private var objectMapperSnakeCase = createObjectMapperSnakeCase(false)
|
||||||
|
|
||||||
|
private val objectMapperDeserialization = run {
|
||||||
|
val mapper: ObjectMapper = jacksonObjectMapper()
|
||||||
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
mapper.registerModule(JavaTimeModule())
|
||||||
|
mapper.setAnnotationIntrospector(
|
||||||
|
AnnotationIntrospectorPair(ColumnAnnotationIntrospector(), mapper.deserializationConfig.annotationIntrospector)
|
||||||
|
)
|
||||||
|
mapper
|
||||||
|
}
|
||||||
|
|
||||||
|
private val objectMapperSnakeCaseDeserialization = run {
|
||||||
|
val mapper: ObjectMapper = jacksonObjectMapper()
|
||||||
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
mapper.propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
|
||||||
|
mapper.registerModule(JavaTimeModule())
|
||||||
|
mapper.setAnnotationIntrospector(
|
||||||
|
AnnotationIntrospectorPair(ColumnAnnotationIntrospector(), mapper.deserializationConfig.annotationIntrospector)
|
||||||
|
)
|
||||||
|
mapper
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> toJsonStr(value: T, snakeCase: Boolean = false): String {
|
||||||
|
if (snakeCase) {
|
||||||
|
return objectMapperSnakeCase.writeValueAsString(value)
|
||||||
|
}
|
||||||
|
return objectMapper.writeValueAsString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> toJsonObject(value: T, snakeCase: Boolean = false): JsonObject {
|
||||||
|
return JsonObject(toJsonStr(value, snakeCase))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> toJsonArray(value: T, snakeCase: Boolean = false): JsonArray {
|
||||||
|
return JsonArray(toJsonStr(value, snakeCase))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> parseObject(value: JsonObject, clazz: Class<T>, snakeCase: Boolean = false): T {
|
||||||
|
return parseObject(value.encode(), clazz, snakeCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> parseObject(value: JsonObject, typeReference: TypeReference<T>, snakeCase: Boolean = false): T {
|
||||||
|
return parseObject<T>(value.encode(), typeReference, snakeCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> parseObject(value: String, clazz: Class<T>, snakeCase: Boolean = false): T {
|
||||||
|
if (snakeCase) {
|
||||||
|
return objectMapperSnakeCaseDeserialization.readValue(value, clazz)
|
||||||
|
}
|
||||||
|
return objectMapperDeserialization.readValue(value, clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> parseObject(value: String, typeReference: TypeReference<T>, snakeCase: Boolean = false): T {
|
||||||
|
if (snakeCase) {
|
||||||
|
return objectMapperSnakeCaseDeserialization.readValue(value, typeReference)
|
||||||
|
}
|
||||||
|
return objectMapperDeserialization.readValue(value, typeReference)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> parseObject(value: Map<*, *>, clazz: Class<T>, snakeCase: Boolean = false): T {
|
||||||
|
return parseObject(toJsonStr(value), clazz, snakeCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> parseObject(value: Map<*, *>, typeReference: TypeReference<T>, snakeCase: Boolean = false): T {
|
||||||
|
return parseObject<T>(
|
||||||
|
toJsonStr(value),
|
||||||
|
typeReference,
|
||||||
|
snakeCase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> parseArray(value: JsonArray, typeReference: TypeReference<List<T>>, snakeCase: Boolean = false): List<T> {
|
||||||
|
return parseArray(value.encode(), typeReference, snakeCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> parseArray(value: String, typeReference: TypeReference<List<T>>, snakeCase: Boolean = false): List<T> {
|
||||||
|
if (snakeCase) {
|
||||||
|
return objectMapperSnakeCaseDeserialization.readValue(value, typeReference)
|
||||||
|
}
|
||||||
|
return objectMapperDeserialization.readValue(value, typeReference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomTypeResolverBuilder : ObjectMapper.DefaultTypeResolverBuilder(
|
||||||
|
ObjectMapper.DefaultTyping.NON_FINAL,
|
||||||
|
BasicPolymorphicTypeValidator.builder().build()
|
||||||
|
) {
|
||||||
|
override fun useForType(t: JavaType): Boolean {
|
||||||
|
if (t.rawClass.isEnum) return false
|
||||||
|
return t.rawClass.name.startsWith("app.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createObjectMapper(writeClassName: Boolean): ObjectMapper {
|
||||||
|
val mapper: ObjectMapper = jacksonObjectMapper()
|
||||||
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
mapper.registerModule(JavaTimeModule())
|
||||||
|
if (writeClassName) {
|
||||||
|
val typeResolver = CustomTypeResolverBuilder()
|
||||||
|
typeResolver.init(JsonTypeInfo.Id.CLASS, null)
|
||||||
|
typeResolver.inclusion(JsonTypeInfo.As.PROPERTY)
|
||||||
|
typeResolver.typeProperty("@t")
|
||||||
|
mapper.setDefaultTyping(typeResolver)
|
||||||
|
}
|
||||||
|
return mapper
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createObjectMapperSnakeCase(writeClassName: Boolean): ObjectMapper {
|
||||||
|
val mapper: ObjectMapper = jacksonObjectMapper()
|
||||||
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
mapper.propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
|
||||||
|
mapper.registerModule(JavaTimeModule())
|
||||||
|
if (writeClassName) {
|
||||||
|
val typeResolver = CustomTypeResolverBuilder()
|
||||||
|
typeResolver.init(JsonTypeInfo.Id.CLASS, null)
|
||||||
|
typeResolver.inclusion(JsonTypeInfo.As.PROPERTY)
|
||||||
|
typeResolver.typeProperty("@t")
|
||||||
|
mapper.setDefaultTyping(typeResolver)
|
||||||
|
}
|
||||||
|
return mapper
|
||||||
|
}
|
||||||
@ -0,0 +1,425 @@
|
|||||||
|
package org.aikrai.vertx.openapi
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil
|
||||||
|
import io.swagger.v3.core.util.Json
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI
|
||||||
|
import io.swagger.v3.oas.models.Operation
|
||||||
|
import io.swagger.v3.oas.models.PathItem
|
||||||
|
import io.swagger.v3.oas.models.Paths
|
||||||
|
import io.swagger.v3.oas.models.info.Info
|
||||||
|
import io.swagger.v3.oas.models.media.Content
|
||||||
|
import io.swagger.v3.oas.models.media.MediaType
|
||||||
|
import io.swagger.v3.oas.models.media.Schema
|
||||||
|
import io.swagger.v3.oas.models.parameters.Parameter
|
||||||
|
import io.swagger.v3.oas.models.parameters.RequestBody
|
||||||
|
import io.swagger.v3.oas.models.responses.ApiResponse
|
||||||
|
import io.swagger.v3.oas.models.responses.ApiResponses
|
||||||
|
import io.swagger.v3.oas.models.servers.Server
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.context.Controller
|
||||||
|
import org.aikrai.vertx.context.CustomizeRequest
|
||||||
|
import org.aikrai.vertx.context.D
|
||||||
|
import org.aikrai.vertx.utlis.ClassUtil
|
||||||
|
import org.reflections.Reflections
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.lang.reflect.ParameterizedType
|
||||||
|
import java.sql.Date
|
||||||
|
import java.sql.Time
|
||||||
|
import java.sql.Timestamp
|
||||||
|
import java.time.*
|
||||||
|
import kotlin.reflect.KParameter
|
||||||
|
import kotlin.reflect.jvm.javaType
|
||||||
|
import kotlin.reflect.jvm.kotlinFunction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAPI 规范生成器,用于生成 Vertx 路由的 OpenAPI 文档
|
||||||
|
*/
|
||||||
|
class OpenApiSpecGenerator {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
// 用于跟踪已处理的类型
|
||||||
|
private val processedTypes = mutableSetOf<Class<*>>()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* 基础类型到 OpenAPI 类型的映射关系
|
||||||
|
*/
|
||||||
|
private val PRIMITIVE_TYPE_MAPPING = mapOf(
|
||||||
|
// java.lang classes
|
||||||
|
String::class.java to "string",
|
||||||
|
Int::class.java to "integer",
|
||||||
|
Integer::class.java to "integer",
|
||||||
|
Long::class.java to "number",
|
||||||
|
java.lang.Long::class.java to "number",
|
||||||
|
Double::class.java to "number",
|
||||||
|
java.lang.Double::class.java to "number",
|
||||||
|
Boolean::class.java to "boolean",
|
||||||
|
java.lang.Boolean::class.java to "boolean",
|
||||||
|
List::class.java to "array",
|
||||||
|
Array::class.java to "array",
|
||||||
|
Collection::class.java to "array",
|
||||||
|
// java.time classes
|
||||||
|
Instant::class.java to "number",
|
||||||
|
LocalDate::class.java to "string",
|
||||||
|
LocalTime::class.java to "string",
|
||||||
|
LocalDateTime::class.java to "string",
|
||||||
|
ZonedDateTime::class.java to "number",
|
||||||
|
OffsetDateTime::class.java to "number",
|
||||||
|
Duration::class.java to "number",
|
||||||
|
Period::class.java to "string",
|
||||||
|
// java.sql classes
|
||||||
|
Date::class.java to "string",
|
||||||
|
Time::class.java to "number",
|
||||||
|
Timestamp::class.java to "number",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 OpenAPI 规范的 JSON 字符串
|
||||||
|
*
|
||||||
|
* @param title API 文档标题
|
||||||
|
* @param version API 版本号
|
||||||
|
* @param serverUrl 服务器 URL
|
||||||
|
* @return 格式化后的 OpenAPI 规范 JSON 字符串
|
||||||
|
*/
|
||||||
|
fun genOpenApiSpecStr(title: String, version: String, serverUrl: String): String {
|
||||||
|
return Json.pretty(generateOpenApiSpec(title, version, serverUrl))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 OpenAPI 规范对象
|
||||||
|
*
|
||||||
|
* @param title API 文档标题
|
||||||
|
* @param version API 版本号
|
||||||
|
* @param serverUrl 服务器 URL
|
||||||
|
* @return OpenAPI 规范对象
|
||||||
|
*/
|
||||||
|
private fun generateOpenApiSpec(title: String, version: String, serverUrl: String): OpenAPI {
|
||||||
|
logger.info("Generating OpenAPI Specification for Vertx routes")
|
||||||
|
return OpenAPI().apply {
|
||||||
|
info = Info().apply {
|
||||||
|
this.title = title
|
||||||
|
this.version = version
|
||||||
|
}
|
||||||
|
servers = listOf(Server().apply { url = serverUrl })
|
||||||
|
paths = generatePaths()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 API 路径信息
|
||||||
|
*
|
||||||
|
* @return Paths 对象,包含所有 API 路径的定义
|
||||||
|
*/
|
||||||
|
private fun generatePaths(): Paths {
|
||||||
|
val paths = Paths()
|
||||||
|
// 获取所有带有 @Controller 注解的类
|
||||||
|
val packageName = ClassUtil.getMainClass()?.packageName
|
||||||
|
val controllerClassSet = Reflections(packageName).getTypesAnnotatedWith(Controller::class.java)
|
||||||
|
ClassUtil.getPublicMethods(controllerClassSet).forEach { (controllerClass, methods) ->
|
||||||
|
val controllerInfo = extractControllerInfo(controllerClass)
|
||||||
|
methods.forEach { method ->
|
||||||
|
val pathInfo = generatePathInfo(method, controllerInfo)
|
||||||
|
paths.addPathItem(pathInfo.path, pathInfo.pathItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从控制器类中提取控制器相关信息
|
||||||
|
*
|
||||||
|
* @param controllerClass 控制器类
|
||||||
|
* @return 包含控制器名称、前缀和标签的 ControllerInfo 对象
|
||||||
|
*/
|
||||||
|
private fun extractControllerInfo(controllerClass: Class<*>): ControllerInfo {
|
||||||
|
val controllerAnnotation = controllerClass.getAnnotation(Controller::class.java)
|
||||||
|
val dAnnotation = controllerClass.getAnnotation(D::class.java)
|
||||||
|
val controllerName = controllerClass.simpleName.removeSuffix("Controller")
|
||||||
|
|
||||||
|
return ControllerInfo(
|
||||||
|
name = controllerName,
|
||||||
|
prefix = controllerAnnotation?.prefix?.takeIf { it.isNotBlank() } ?: controllerName,
|
||||||
|
tag = "${dAnnotation?.name ?: ""} $controllerName"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成路径信息
|
||||||
|
*
|
||||||
|
* @param method 方法对象
|
||||||
|
* @param controllerInfo 控制器信息
|
||||||
|
* @return 包含路径和路径项的 PathInfo 对象
|
||||||
|
*/
|
||||||
|
private fun generatePathInfo(method: Method, controllerInfo: ControllerInfo): PathInfo {
|
||||||
|
val methodDAnnotation = method.getAnnotation(D::class.java)
|
||||||
|
val path = buildPath(controllerInfo.prefix, method.name)
|
||||||
|
|
||||||
|
val operation = Operation().apply {
|
||||||
|
operationId = method.name
|
||||||
|
summary = "${methodDAnnotation?.name ?: ""} $path"
|
||||||
|
description = methodDAnnotation?.caption ?: summary
|
||||||
|
|
||||||
|
// 分离请求体参数和普通参数
|
||||||
|
val allParameters = getParameters(method)
|
||||||
|
val (bodyParams, queryParams) = allParameters.partition { it.`in` == "body" }
|
||||||
|
// 设置查询参数
|
||||||
|
parameters = queryParams
|
||||||
|
// 设置请求体
|
||||||
|
if (bodyParams.isNotEmpty()) {
|
||||||
|
requestBody = RequestBody().apply {
|
||||||
|
content = bodyParams.first().content
|
||||||
|
required = bodyParams.first().required
|
||||||
|
description = bodyParams.first().description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responses = generateResponsesFromReturnType(method)
|
||||||
|
deprecated = false
|
||||||
|
security = listOf()
|
||||||
|
tags = listOf(controllerInfo.tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pathItem = PathItem().apply {
|
||||||
|
operation(getHttpMethod(method), operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PathInfo(path, pathItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 API 路径
|
||||||
|
*
|
||||||
|
* @param controllerPrefix 控制器前缀
|
||||||
|
* @param methodName 方法名
|
||||||
|
* @return 格式化后的 API 路径
|
||||||
|
*/
|
||||||
|
private fun buildPath(controllerPrefix: String, methodName: String): String {
|
||||||
|
return (
|
||||||
|
"/${StrUtil.lowerFirst(StrUtil.toCamelCase(controllerPrefix))}/" +
|
||||||
|
StrUtil.lowerFirst(StrUtil.toCamelCase(methodName))
|
||||||
|
).replace("//", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据方法返回值类型生成响应对象
|
||||||
|
*
|
||||||
|
* @param method 方法对象
|
||||||
|
* @return ApiResponses 对象
|
||||||
|
*/
|
||||||
|
private fun generateResponsesFromReturnType(method: Method): ApiResponses {
|
||||||
|
val returnType = method.kotlinFunction?.returnType?.javaType
|
||||||
|
val schema = when (returnType) {
|
||||||
|
// 处理泛型返回类型
|
||||||
|
is ParameterizedType -> {
|
||||||
|
val rawType = returnType.rawType as Class<*>
|
||||||
|
val typeArguments = returnType.actualTypeArguments
|
||||||
|
when {
|
||||||
|
// 处理集合类型
|
||||||
|
Collection::class.java.isAssignableFrom(rawType) -> {
|
||||||
|
Schema<Any>().apply {
|
||||||
|
type = "array"
|
||||||
|
items = generateSchema(
|
||||||
|
typeArguments[0].let {
|
||||||
|
when (it) {
|
||||||
|
is Class<*> -> it
|
||||||
|
is ParameterizedType -> it.rawType as Class<*>
|
||||||
|
else -> Any::class.java
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 可以添加其他泛型类型的处理
|
||||||
|
else -> generateSchema(rawType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理普通类型
|
||||||
|
is Class<*> -> generateSchema(returnType)
|
||||||
|
else -> Schema<Any>().type("object")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponses().addApiResponse(
|
||||||
|
"200",
|
||||||
|
ApiResponse().apply {
|
||||||
|
description = "OK"
|
||||||
|
content = Content().addMediaType(
|
||||||
|
"application/json",
|
||||||
|
MediaType().schema(schema)
|
||||||
|
)
|
||||||
|
headers = mapOf()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取方法的参数列表
|
||||||
|
*
|
||||||
|
* @param method 方法对象
|
||||||
|
* @return OpenAPI 参数列表
|
||||||
|
*/
|
||||||
|
private fun getParameters(method: Method): List<Parameter> {
|
||||||
|
return method.kotlinFunction?.parameters
|
||||||
|
?.mapNotNull { parameter -> generateParameter(parameter) }
|
||||||
|
?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 OpenAPI 参数对象
|
||||||
|
*
|
||||||
|
* @param parameter Kotlin 参数对象
|
||||||
|
* @return OpenAPI 参数对象,如果无法生成则返回 null
|
||||||
|
*/
|
||||||
|
private fun generateParameter(parameter: KParameter): Parameter? {
|
||||||
|
if (parameter.kind == KParameter.Kind.INSTANCE) return null
|
||||||
|
val type =
|
||||||
|
(parameter.type.javaType as? Class<*>) ?: (parameter.type.javaType as? ParameterizedType)?.rawType as? Class<*>
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
val paramName = parameter.name ?: return null
|
||||||
|
val annotation = parameter.annotations.filterIsInstance<D>().firstOrNull()
|
||||||
|
|
||||||
|
// 检查是否为自定义数据类
|
||||||
|
if (!parameter.type.classifier.toString().startsWith("class kotlin.") &&
|
||||||
|
!(parameter.type.javaType as Class<*>).isEnum &&
|
||||||
|
parameter.type.javaType is Class<*>
|
||||||
|
) {
|
||||||
|
// 创建请求体参数
|
||||||
|
return Parameter().apply {
|
||||||
|
name = annotation?.name?.takeIf { it.isNotBlank() } ?: paramName
|
||||||
|
required = !parameter.type.isMarkedNullable
|
||||||
|
description = annotation?.caption?.takeIf { it.isNotBlank() } ?: name
|
||||||
|
`in` = "body"
|
||||||
|
schema = generateSchema(type)
|
||||||
|
content = Content().addMediaType(
|
||||||
|
"application/json",
|
||||||
|
MediaType().schema(schema)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理普通参数
|
||||||
|
return Parameter().apply {
|
||||||
|
name = annotation?.name?.takeIf { it.isNotBlank() } ?: paramName
|
||||||
|
required = !parameter.type.isMarkedNullable
|
||||||
|
description = annotation?.caption?.takeIf { it.isNotBlank() } ?: name
|
||||||
|
allowEmptyValue = parameter.type.isMarkedNullable
|
||||||
|
`in` = "query"
|
||||||
|
schema = generateSchema(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 OpenAPI Schema 对象
|
||||||
|
*
|
||||||
|
* @param type 参数类型
|
||||||
|
* @return OpenAPI Schema 对象
|
||||||
|
*/
|
||||||
|
private fun generateSchema(type: Class<*>): Schema<Any> {
|
||||||
|
// 如果该类型已经处理过,则返回一个空的 Schema,避免循环引用
|
||||||
|
if (processedTypes.contains(type)) {
|
||||||
|
return Schema<Any>().apply {
|
||||||
|
this.type = "object"
|
||||||
|
description = "Circular reference detected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 将当前类型添加到已处理集合中
|
||||||
|
processedTypes.add(type)
|
||||||
|
return when {
|
||||||
|
// 处理基础类型
|
||||||
|
PRIMITIVE_TYPE_MAPPING.containsKey(type) -> Schema<Any>().apply {
|
||||||
|
this.type = PRIMITIVE_TYPE_MAPPING[type]
|
||||||
|
deprecated = false
|
||||||
|
}
|
||||||
|
// 处理枚举类型
|
||||||
|
type.isEnum -> Schema<Any>().apply {
|
||||||
|
this.type = "string"
|
||||||
|
enum = type.enumConstants?.map { it.toString() }
|
||||||
|
}
|
||||||
|
type.name.startsWith("java.lang") || type.name.startsWith("java.time") || type.name.startsWith("java.sql") -> Schema<Any>().apply {
|
||||||
|
this.type = type.simpleName.lowercase()
|
||||||
|
deprecated = false
|
||||||
|
}
|
||||||
|
type.name.startsWith("java") -> Schema<Any>().apply {
|
||||||
|
this.type = type.simpleName.lowercase()
|
||||||
|
deprecated = false
|
||||||
|
}
|
||||||
|
// 处理自定义对象
|
||||||
|
else -> Schema<Any>().apply {
|
||||||
|
this.type = "object"
|
||||||
|
properties = type.declaredFields
|
||||||
|
.filter { !it.isSynthetic }
|
||||||
|
.associate { field ->
|
||||||
|
field.isAccessible = true
|
||||||
|
field.name to generateSchema(field.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
// 处理完后,从已处理集合中移除当前类型
|
||||||
|
processedTypes.remove(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成默认的 API 响应对象
|
||||||
|
*
|
||||||
|
* @return 包含默认响应的 ApiResponses 对象
|
||||||
|
*/
|
||||||
|
private fun generateDefaultResponses(): ApiResponses {
|
||||||
|
return ApiResponses().addApiResponse(
|
||||||
|
"200",
|
||||||
|
ApiResponse().apply {
|
||||||
|
description = "OK"
|
||||||
|
content = Content().addMediaType(
|
||||||
|
"*/*",
|
||||||
|
MediaType().schema(Schema<Any>().type("object"))
|
||||||
|
)
|
||||||
|
headers = mapOf()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取方法对应的 HTTP 方法
|
||||||
|
*
|
||||||
|
* @param method 方法对象
|
||||||
|
* @return PathItem.HttpMethod 枚举值
|
||||||
|
*/
|
||||||
|
private fun getHttpMethod(method: Method): PathItem.HttpMethod {
|
||||||
|
val api = method.getAnnotation(CustomizeRequest::class.java)
|
||||||
|
return if (api != null) {
|
||||||
|
when (api.method.uppercase()) {
|
||||||
|
"GET" -> PathItem.HttpMethod.GET
|
||||||
|
"PUT" -> PathItem.HttpMethod.PUT
|
||||||
|
"DELETE" -> PathItem.HttpMethod.DELETE
|
||||||
|
"PATCH" -> PathItem.HttpMethod.PATCH
|
||||||
|
else -> PathItem.HttpMethod.POST
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PathItem.HttpMethod.POST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制器信息数据类
|
||||||
|
*
|
||||||
|
* @property name 控制器名称
|
||||||
|
* @property prefix 控制器路径前缀
|
||||||
|
* @property tag API 文档标签
|
||||||
|
*/
|
||||||
|
private data class ControllerInfo(
|
||||||
|
val name: String,
|
||||||
|
val prefix: String,
|
||||||
|
val tag: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径信息数据类
|
||||||
|
*
|
||||||
|
* @property path API 路径
|
||||||
|
* @property pathItem OpenAPI 路径项对象
|
||||||
|
*/
|
||||||
|
private data class PathInfo(
|
||||||
|
val path: String,
|
||||||
|
val pathItem: PathItem
|
||||||
|
)
|
||||||
|
}
|
||||||
84
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/ClassUtil.kt
Normal file
84
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/ClassUtil.kt
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package org.aikrai.vertx.utlis
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ReflectUtil
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.util.*
|
||||||
|
import java.util.jar.JarFile
|
||||||
|
import kotlin.reflect.KMutableProperty1
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
import kotlin.reflect.jvm.javaGetter
|
||||||
|
import kotlin.reflect.jvm.javaSetter
|
||||||
|
|
||||||
|
object ClassUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用程序的主类
|
||||||
|
*
|
||||||
|
* @return 主类的 Class 对象,如果未找到则返回 null
|
||||||
|
*/
|
||||||
|
fun getMainClass(): Class<*>? {
|
||||||
|
val classLoader = ServiceLoader.load(ClassLoader::class.java).firstOrNull()
|
||||||
|
?: Thread.currentThread().contextClassLoader
|
||||||
|
val mainCommand = System.getProperty("sun.java.command")
|
||||||
|
if (mainCommand != null) {
|
||||||
|
val mainClassName: String? = if (mainCommand.endsWith(".jar")) {
|
||||||
|
try {
|
||||||
|
val jarFilePath = mainCommand.split(" ").first()
|
||||||
|
val jarFile = File(jarFilePath)
|
||||||
|
if (jarFile.exists()) {
|
||||||
|
JarFile(jarFile).use { jar ->
|
||||||
|
jar.manifest.mainAttributes.getValue("Main-Class")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// mainCommand 是类名
|
||||||
|
mainCommand.split(" ").first()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mainClassName.isNullOrEmpty()) {
|
||||||
|
return try {
|
||||||
|
classLoader.loadClass(mainClassName)
|
||||||
|
} catch (e: ClassNotFoundException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取类中的所有公共方法
|
||||||
|
*
|
||||||
|
* @return 包含控制器类和其方法数组的列表
|
||||||
|
*/
|
||||||
|
fun getPublicMethods(classSet: Set<Class<*>>): List<Pair<Class<*>, Array<Method>>> {
|
||||||
|
return classSet.map { clazz ->
|
||||||
|
val kClass = clazz.kotlin
|
||||||
|
// 获取所有属性的 getter 方法
|
||||||
|
val getters: Set<Method> = kClass.memberProperties.mapNotNull { it.javaGetter }.toSet()
|
||||||
|
// 获取所有可变属性的 setter 方法
|
||||||
|
val setters: Set<Method> = kClass.memberProperties
|
||||||
|
// 只处理可变属性
|
||||||
|
.filterIsInstance<KMutableProperty1<*, *>>()
|
||||||
|
.mapNotNull { it.javaSetter }.toSet()
|
||||||
|
// 合并 getter 和 setter 方法
|
||||||
|
val propertyAccessors: Set<Method> = getters + setters
|
||||||
|
// 获取所有公共方法
|
||||||
|
val allPublicMethods = ReflectUtil.getPublicMethods(clazz)
|
||||||
|
// 过滤掉不需要的方法
|
||||||
|
val filteredMethods = allPublicMethods.filter { method ->
|
||||||
|
// 1. 排除合成方法 2. 仅包括在当前类中声明的方法
|
||||||
|
!method.isSynthetic && method.declaringClass == clazz && !propertyAccessors.contains(method)
|
||||||
|
}.toTypedArray()
|
||||||
|
Pair(clazz, filteredMethods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/Entity.kt
Normal file
24
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/Entity.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package org.aikrai.vertx.utlis
|
||||||
|
|
||||||
|
import jakarta.persistence.*
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@MappedSuperclass
|
||||||
|
open class Entity {
|
||||||
|
|
||||||
|
@Column(name = "create_by", length = 64)
|
||||||
|
var createBy: String? = null
|
||||||
|
|
||||||
|
@Column(name = "create_time")
|
||||||
|
var createTime: Instant? = null
|
||||||
|
|
||||||
|
@Column(name = "update_by", length = 64)
|
||||||
|
var updateBy: String? = null
|
||||||
|
|
||||||
|
@Column(name = "update_time")
|
||||||
|
var updateTime: Instant? = null
|
||||||
|
|
||||||
|
@Version
|
||||||
|
@Column(name = "version")
|
||||||
|
var version: Long = 0
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package org.aikrai.vertx.utlis
|
||||||
|
|
||||||
|
import io.vertx.core.json.JsonObject
|
||||||
|
|
||||||
|
object FlattenUtil {
|
||||||
|
|
||||||
|
fun flattenMap(map: Map<String, Any>, parentKey: String = "", separator: String = "."): Map<String, Any> {
|
||||||
|
val flatMap = mutableMapOf<String, Any>()
|
||||||
|
for ((key, value) in map) {
|
||||||
|
val newKey = if (parentKey.isEmpty()) key else "$parentKey$separator$key"
|
||||||
|
when (value) {
|
||||||
|
is Map<*, *> -> {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
flatMap.putAll(flattenMap(value as Map<String, Any>, newKey, separator))
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> flatMap[newKey] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flatMap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flattenJsonObject(obj: JsonObject, parentKey: String = "", separator: String = "."): Map<String, Any> {
|
||||||
|
val flatMap = mutableMapOf<String, Any>()
|
||||||
|
for (key in obj.fieldNames()) {
|
||||||
|
val value = obj.getValue(key)
|
||||||
|
val newKey = if (parentKey.isEmpty()) key else "$parentKey$separator$key"
|
||||||
|
when (value) {
|
||||||
|
is JsonObject -> {
|
||||||
|
flatMap.putAll(flattenJsonObject(value, newKey, separator))
|
||||||
|
}
|
||||||
|
|
||||||
|
is Iterable<*> -> {
|
||||||
|
// 如果值是数组,可以根据需要展开或处理
|
||||||
|
flatMap[newKey] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
flatMap[newKey] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flatMap
|
||||||
|
}
|
||||||
|
}
|
||||||
82
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/LangUtil.kt
Normal file
82
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/LangUtil.kt
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package org.aikrai.vertx.utlis
|
||||||
|
|
||||||
|
import io.vertx.core.MultiMap
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import java.sql.Timestamp
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
import kotlin.collections.set
|
||||||
|
import kotlin.stackTraceToString
|
||||||
|
import kotlin.text.isNullOrBlank
|
||||||
|
import kotlin.text.trim
|
||||||
|
|
||||||
|
object LangUtil {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
fun <R> letTry(clause: () -> R): R? {
|
||||||
|
return try {
|
||||||
|
clause()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
logger.error { ex.stackTraceToString() }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T, R> T?.letIf(clause: (T) -> R): R? {
|
||||||
|
if (this != null) {
|
||||||
|
return clause(this)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String?.defaultAsNull(): String? {
|
||||||
|
if (this.isNullOrBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Long?.defaultAsNull(): Long? {
|
||||||
|
if (this == 0L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int?.defaultAsNull(): Int? {
|
||||||
|
if (this == 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Double?.defaultAsNull(): Double? {
|
||||||
|
if (this == 0.0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MultiMap.toMap(): Map<String, Any> {
|
||||||
|
val map = mutableMapOf<String, Any>()
|
||||||
|
forEach { map[it.key] == it.value }
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MultiMap.toStringMap(): Map<String, String> {
|
||||||
|
val map = mutableMapOf<String, String>()
|
||||||
|
forEach { map[it.key] = it.value.toString() }
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
fun offsetDateTime(epochMilli: Long?): OffsetDateTime? {
|
||||||
|
if (epochMilli == null || epochMilli == 0L) return null
|
||||||
|
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneId.systemDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun timestamp(): Timestamp {
|
||||||
|
return Timestamp.from(Instant.now())
|
||||||
|
}
|
||||||
|
}
|
||||||
78
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/Lazyload.kt
Normal file
78
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/Lazyload.kt
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package org.aikrai.vertx.utlis
|
||||||
|
|
||||||
|
import io.vertx.core.Closeable
|
||||||
|
import io.vertx.core.Promise
|
||||||
|
import io.vertx.core.Vertx
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
open class Lazyload<T>(private val block: suspend () -> T) {
|
||||||
|
protected var value: T? = null
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
fun dirty() {
|
||||||
|
value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
open suspend fun get(): T {
|
||||||
|
if (value == null) {
|
||||||
|
mutex.withLock {
|
||||||
|
if (value == null) {
|
||||||
|
value = block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeout: milliseconds
|
||||||
|
class Autoload<T>(
|
||||||
|
private val vertx: Vertx,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
private val timeout: Long,
|
||||||
|
private val block: suspend () -> T
|
||||||
|
) : Lazyload<T>(block), Closeable {
|
||||||
|
|
||||||
|
private var periodicId = vertx.setPeriodic(timeout) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
value = block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close(completion: Promise<Void>?) {
|
||||||
|
vertx.cancelTimer(periodicId)
|
||||||
|
completion?.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Aliveload<T>(
|
||||||
|
private val vertx: Vertx,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
private val timeout: Long,
|
||||||
|
private val block: suspend () -> T
|
||||||
|
) : Lazyload<T>(block), Closeable {
|
||||||
|
|
||||||
|
private var needsUpdate = true
|
||||||
|
|
||||||
|
private var periodicId = vertx.setPeriodic(timeout) {
|
||||||
|
if (needsUpdate) {
|
||||||
|
needsUpdate = false
|
||||||
|
coroutineScope.launch {
|
||||||
|
value = block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun get(): T {
|
||||||
|
needsUpdate = true
|
||||||
|
return super.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close(completion: Promise<Void>?) {
|
||||||
|
vertx.cancelTimer(periodicId)
|
||||||
|
completion?.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
48
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/Meta.kt
Normal file
48
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/Meta.kt
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package org.aikrai.vertx.utlis
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
|
||||||
|
@JsonIgnoreProperties("localizedMessage", "suppressed", "stackTrace", "cause")
|
||||||
|
class Meta(
|
||||||
|
val name: String,
|
||||||
|
override val message: String = "",
|
||||||
|
val data: Any? = null
|
||||||
|
) : RuntimeException(message, null, false, false) {
|
||||||
|
|
||||||
|
fun stackTraceToString(): String {
|
||||||
|
return JsonUtil.toJsonStr(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun failure(name: String, message: String): Meta =
|
||||||
|
Meta(name, message)
|
||||||
|
|
||||||
|
fun unimplemented(message: String): Meta =
|
||||||
|
Meta("unimplemented", message)
|
||||||
|
|
||||||
|
fun unauthorized(message: String): Meta =
|
||||||
|
Meta("unauthorized", message)
|
||||||
|
|
||||||
|
fun timeout(message: String): Meta =
|
||||||
|
Meta("timeout", message)
|
||||||
|
|
||||||
|
fun requireArgument(argument: String, message: String): Meta =
|
||||||
|
Meta("required_argument:$argument", message)
|
||||||
|
|
||||||
|
fun invalidArgument(argument: String, message: String): Meta =
|
||||||
|
Meta("invalid_argument:$argument", message)
|
||||||
|
|
||||||
|
fun notFound(argument: String, message: String): Meta =
|
||||||
|
Meta("not_found:$argument", message)
|
||||||
|
|
||||||
|
fun badRequest(message: String): Meta =
|
||||||
|
Meta("bad_request", message)
|
||||||
|
|
||||||
|
fun notSupported(message: String): Meta =
|
||||||
|
Meta("not_supported", message)
|
||||||
|
|
||||||
|
fun forbidden(message: String): Meta =
|
||||||
|
Meta("forbidden", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/TimeUtil.kt
Normal file
42
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/TimeUtil.kt
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package org.aikrai.vertx.utlis
|
||||||
|
|
||||||
|
import java.sql.Timestamp
|
||||||
|
import java.util.concurrent.ScheduledThreadPoolExecutor
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object TimeUtil {
|
||||||
|
private class Time {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var currentTimeMillis: Long
|
||||||
|
|
||||||
|
init {
|
||||||
|
currentTimeMillis = System.currentTimeMillis()
|
||||||
|
scheduleTick()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleTick() {
|
||||||
|
ScheduledThreadPoolExecutor(1) { runnable: Runnable? ->
|
||||||
|
val thread = Thread(runnable, "current-time-millis")
|
||||||
|
thread.isDaemon = true
|
||||||
|
thread
|
||||||
|
}.scheduleAtFixedRate(
|
||||||
|
{ currentTimeMillis = System.currentTimeMillis() },
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
TimeUnit.MILLISECONDS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val instance = Time()
|
||||||
|
|
||||||
|
val now: Long
|
||||||
|
get() {
|
||||||
|
return instance.currentTimeMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
fun now(): Timestamp {
|
||||||
|
return Timestamp(instance.currentTimeMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user