vertx-pj:0.0.2
This commit is contained in:
parent
9f2105caf8
commit
bc505e93f2
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@ build/
|
|||||||
!**/src/test/**/build/
|
!**/src/test/**/build/
|
||||||
/config
|
/config
|
||||||
/gradle
|
/gradle
|
||||||
|
log/
|
||||||
|
|
||||||
### IntelliJ IDEA ###
|
### IntelliJ IDEA ###
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@ -9,6 +9,7 @@ group = "com.demo"
|
|||||||
version = "1.0.0-SNAPSHOT"
|
version = "1.0.0-SNAPSHOT"
|
||||||
|
|
||||||
val vertxVersion = "4.5.11"
|
val vertxVersion = "4.5.11"
|
||||||
|
val junitJupiterVersion = "5.9.1"
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass.set("app.Application")
|
mainClass.set("app.Application")
|
||||||
@ -68,11 +69,12 @@ spotless {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(fileTree(mapOf("dir" to "lib", "include" to listOf("*.jar"))))
|
||||||
implementation(project(":vertx-fw"))
|
implementation(project(":vertx-fw"))
|
||||||
// implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20")
|
// 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("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||||
implementation(platform("io.vertx:vertx-stack-depchain:$vertxVersion"))
|
implementation(platform("io.vertx:vertx-stack-depchain:$vertxVersion"))
|
||||||
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
implementation("io.vertx:vertx-lang-kotlin:$vertxVersion")
|
implementation("io.vertx:vertx-lang-kotlin:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-core:$vertxVersion")
|
implementation("io.vertx:vertx-core:$vertxVersion")
|
||||||
@ -84,13 +86,14 @@ dependencies {
|
|||||||
implementation("io.vertx:vertx-mysql-client:$vertxVersion")
|
implementation("io.vertx:vertx-mysql-client:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-sql-client-templates:$vertxVersion")
|
implementation("io.vertx:vertx-sql-client-templates:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-auth-jwt:$vertxVersion")
|
implementation("io.vertx:vertx-auth-jwt:$vertxVersion")
|
||||||
|
implementation("io.vertx:vertx-redis-client:$vertxVersion")
|
||||||
|
|
||||||
|
|
||||||
implementation("com.google.inject:guice:5.1.0")
|
implementation("com.google.inject:guice:5.1.0")
|
||||||
implementation("org.reflections:reflections:0.9.12")
|
implementation("org.reflections:reflections:0.10.2")
|
||||||
implementation("cn.hutool:hutool-all:5.8.24")
|
implementation("cn.hutool:hutool-all:5.8.24")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
|
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.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("com.github.ben-manes.caffeine:caffeine:3.1.8")
|
||||||
implementation("dev.hsbrysk:caffeine-coroutines:1.0.0")
|
implementation("dev.hsbrysk:caffeine-coroutines:1.0.0")
|
||||||
|
|
||||||
@ -101,15 +104,18 @@ dependencies {
|
|||||||
implementation("org.codehaus.janino:janino:3.1.8")
|
implementation("org.codehaus.janino:janino:3.1.8")
|
||||||
|
|
||||||
// jpa
|
// jpa
|
||||||
implementation("jakarta.persistence:jakarta.persistence-api:3.2.0")
|
// implementation("jakarta.persistence:jakarta.persistence-api:3.2.0")
|
||||||
implementation("jakarta.validation:jakarta.validation-api:3.1.0")
|
// implementation("jakarta.validation:jakarta.validation-api:3.1.0")
|
||||||
|
|
||||||
|
// db
|
||||||
|
implementation("org.postgresql:postgresql:42.7.5")
|
||||||
|
implementation("com.ongres.scram:client:2.1")
|
||||||
|
|
||||||
// implementation("com.mysql:mysql-connector-j:9.1.0")
|
|
||||||
implementation("mysql:mysql-connector-java:5.1.49")
|
|
||||||
// doc
|
// doc
|
||||||
implementation("io.swagger.core.v3:swagger-core:2.2.27")
|
implementation("io.swagger.core.v3:swagger-core:2.2.27")
|
||||||
|
|
||||||
testImplementation("io.vertx:vertx-junit5:$vertxVersion")
|
testImplementation("io.vertx:vertx-junit5")
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter:$junitJupiterVersion")
|
||||||
implementation(fileTree(mapOf("dir" to "lib", "include" to listOf("*.jar"))))
|
testImplementation("org.mockito:mockito-core:5.15.2")
|
||||||
|
testImplementation("org.mockito:mockito-junit-jupiter:5.15.2")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import app.verticle.MainVerticle
|
|||||||
import io.vertx.core.Vertx
|
import io.vertx.core.Vertx
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.config.Config
|
||||||
|
|
||||||
object Application {
|
object Application {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
@ -13,15 +14,16 @@ object Application {
|
|||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val vertx = Vertx.vertx()
|
val vertx = Vertx.vertx()
|
||||||
|
Config.init(vertx)
|
||||||
val getIt = InjectConfig.configure(vertx)
|
val getIt = InjectConfig.configure(vertx)
|
||||||
val demoVerticle = getIt.getInstance(MainVerticle::class.java)
|
val mainVerticle = getIt.getInstance(MainVerticle::class.java)
|
||||||
vertx.deployVerticle(demoVerticle).onComplete {
|
vertx.deployVerticle(mainVerticle).onComplete {
|
||||||
if (it.failed()) {
|
if (it.failed()) {
|
||||||
logger.error { "MainVerticle startup failed: ${it.cause()?.stackTraceToString()}" }
|
logger.error { "MainVerticle startup failed: ${it.cause()?.stackTraceToString()}" }
|
||||||
} else {
|
} else {
|
||||||
logger.info { "MainVerticle startup successfully" }
|
logger.info { "MainVerticle startup successfully" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package org.aikrai.vertx.config
|
package app.config
|
||||||
|
|
||||||
import io.vertx.mysqlclient.MySQLException
|
import io.vertx.mysqlclient.MySQLException
|
||||||
import io.vertx.pgclient.PgException
|
import io.vertx.pgclient.PgException
|
||||||
@ -40,7 +40,7 @@ object FailureParser {
|
|||||||
|
|
||||||
fun parse(statusCode: Int, error: Throwable): Failure {
|
fun parse(statusCode: Int, error: Throwable): Failure {
|
||||||
return when (error) {
|
return when (error) {
|
||||||
is SQLException -> Failure(statusCode, Meta.failure(error.javaClass.name, "执行错误"))
|
is SQLException -> Failure(statusCode, Meta.error(error.javaClass.name, "执行错误"))
|
||||||
else -> Failure(statusCode, error.toMeta())
|
else -> Failure(statusCode, error.toMeta())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,8 +9,6 @@ import io.vertx.core.Vertx
|
|||||||
import io.vertx.core.http.HttpServer
|
import io.vertx.core.http.HttpServer
|
||||||
import io.vertx.core.http.HttpServerOptions
|
import io.vertx.core.http.HttpServerOptions
|
||||||
import io.vertx.ext.auth.jwt.JWTAuth
|
import io.vertx.ext.auth.jwt.JWTAuth
|
||||||
import io.vertx.mysqlclient.MySQLBuilder
|
|
||||||
import io.vertx.mysqlclient.MySQLConnectOptions
|
|
||||||
import io.vertx.pgclient.PgBuilder
|
import io.vertx.pgclient.PgBuilder
|
||||||
import io.vertx.pgclient.PgConnectOptions
|
import io.vertx.pgclient.PgConnectOptions
|
||||||
import io.vertx.sqlclient.Pool
|
import io.vertx.sqlclient.Pool
|
||||||
@ -22,8 +20,7 @@ import org.aikrai.vertx.config.DefaultScope
|
|||||||
import org.aikrai.vertx.db.tx.TxMgrHolder.initTxMgr
|
import org.aikrai.vertx.db.tx.TxMgrHolder.initTxMgr
|
||||||
|
|
||||||
object InjectConfig {
|
object InjectConfig {
|
||||||
suspend fun configure(vertx: Vertx): Injector {
|
fun configure(vertx: Vertx): Injector {
|
||||||
Config.init(vertx)
|
|
||||||
return Guice.createInjector(InjectorModule(vertx))
|
return Guice.createInjector(InjectorModule(vertx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,7 +37,6 @@ class InjectorModule(
|
|||||||
}
|
}
|
||||||
bind(Vertx::class.java).toInstance(vertx)
|
bind(Vertx::class.java).toInstance(vertx)
|
||||||
bind(CoroutineScope::class.java).toInstance(coroutineScope)
|
bind(CoroutineScope::class.java).toInstance(coroutineScope)
|
||||||
bind(HttpServer::class.java).toInstance(vertx.createHttpServer(HttpServerOptions()))
|
|
||||||
bind(Snowflake::class.java).toInstance(IdUtil.getSnowflake())
|
bind(Snowflake::class.java).toInstance(IdUtil.getSnowflake())
|
||||||
bind(JWTAuth::class.java).toProvider(JWTAuthProvider::class.java).`in`(Singleton::class.java)
|
bind(JWTAuth::class.java).toProvider(JWTAuthProvider::class.java).`in`(Singleton::class.java)
|
||||||
|
|
||||||
@ -57,7 +53,6 @@ class InjectorModule(
|
|||||||
// val user = configMap["databases.username"].toString()
|
// val user = configMap["databases.username"].toString()
|
||||||
// val password = configMap["databases.password"].toString()
|
// val password = configMap["databases.password"].toString()
|
||||||
// val dbMap = Config.getKey("databases") as Map<String, String>
|
// val dbMap = Config.getKey("databases") as Map<String, String>
|
||||||
val type = Config.getKey("databases.type").toString()
|
|
||||||
val name = Config.getKey("databases.name").toString()
|
val name = Config.getKey("databases.name").toString()
|
||||||
val host = Config.getKey("databases.host").toString()
|
val host = Config.getKey("databases.host").toString()
|
||||||
val port = Config.getKey("databases.port").toString()
|
val port = Config.getKey("databases.port").toString()
|
||||||
@ -65,29 +60,13 @@ class InjectorModule(
|
|||||||
val password = Config.getKey("databases.password").toString()
|
val password = Config.getKey("databases.password").toString()
|
||||||
|
|
||||||
val poolOptions = PoolOptions().setMaxSize(10)
|
val poolOptions = PoolOptions().setMaxSize(10)
|
||||||
val pool = when (type.lowercase()) {
|
val clientOptions = PgConnectOptions()
|
||||||
"mysql" -> {
|
.setHost(host)
|
||||||
val clientOptions = MySQLConnectOptions()
|
.setPort(port.toInt())
|
||||||
.setHost(host)
|
.setDatabase(name)
|
||||||
.setPort(port.toInt())
|
.setUser(user)
|
||||||
.setDatabase(name)
|
.setPassword(password)
|
||||||
.setUser(user)
|
.setTcpKeepAlive(true)
|
||||||
.setPassword(password)
|
return PgBuilder.pool().connectingTo(clientOptions).with(poolOptions).using(vertx).build()
|
||||||
.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")
|
|
||||||
}
|
|
||||||
return pool
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
vertx-demo/src/main/kotlin/app/config/RespBean.kt
Normal file
54
vertx-demo/src/main/kotlin/app/config/RespBean.kt
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package app.config
|
||||||
|
|
||||||
|
import org.aikrai.vertx.constant.HttpStatus
|
||||||
|
|
||||||
|
|
||||||
|
data class RespBean(
|
||||||
|
val code: Int,
|
||||||
|
val message: String,
|
||||||
|
val data: Any?
|
||||||
|
) {
|
||||||
|
var requestId: Long = -1L
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* 创建一个成功的响应
|
||||||
|
*
|
||||||
|
* @param data 响应数据
|
||||||
|
* @return RespBean 实例
|
||||||
|
*/
|
||||||
|
fun success(data: Any? = null): RespBean {
|
||||||
|
val code = when (data) {
|
||||||
|
null -> HttpStatus.NO_CONTENT
|
||||||
|
else -> HttpStatus.SUCCESS
|
||||||
|
}
|
||||||
|
return RespBean(code, "Success", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建一个失败的响应
|
||||||
|
*
|
||||||
|
* @param status 状态码
|
||||||
|
* @param message 错误消息
|
||||||
|
* @return RespBean 实例
|
||||||
|
*/
|
||||||
|
fun failure(message: String, data: Any? = null): RespBean {
|
||||||
|
return failure(HttpStatus.ERROR, message, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun failure(code: Int, message: String, data: Any? = null): RespBean {
|
||||||
|
return RespBean(HttpStatus.ERROR, message, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 访问受限,授权过期
|
||||||
|
fun forbidden(message: String?): RespBean {
|
||||||
|
return failure(HttpStatus.FORBIDDEN, message ?: "Restricted access, expired authorizations")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未授权
|
||||||
|
fun unauthorized(message: String?): RespBean {
|
||||||
|
return failure(HttpStatus.UNAUTHORIZED, message ?: "Unauthorized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
package app.config.auth
|
|
||||||
|
|
||||||
import app.config.Constant
|
|
||||||
import app.domain.user.UserRepository
|
|
||||||
import app.util.CacheUtil
|
|
||||||
import com.google.inject.Inject
|
|
||||||
import io.vertx.ext.auth.jwt.JWTAuth
|
|
||||||
import org.aikrai.vertx.auth.Attributes
|
|
||||||
import org.aikrai.vertx.auth.AuthUser
|
|
||||||
import org.aikrai.vertx.auth.Principal
|
|
||||||
import org.aikrai.vertx.auth.TokenUtil
|
|
||||||
|
|
||||||
class AuthHandler @Inject constructor(
|
|
||||||
private val jwtAuth: JWTAuth,
|
|
||||||
private val userRepository: UserRepository,
|
|
||||||
private val cacheUtil: CacheUtil
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend fun handle(token: String): AuthUser? {
|
|
||||||
val userInfo = TokenUtil.authenticate(jwtAuth, token) ?: return null
|
|
||||||
val userId = userInfo.principal().getString("id").toLong()
|
|
||||||
val user = cacheUtil.get(Constant.USER + userId) ?: userRepository.get(userId)?.let {
|
|
||||||
cacheUtil.put(Constant.USER + userId, it)
|
|
||||||
} ?: return null
|
|
||||||
return AuthUser(
|
|
||||||
Principal(userId, user),
|
|
||||||
// get roles and permissions from database
|
|
||||||
Attributes(setOf("admin"), setOf("user:list")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package app.config.auth
|
package app.config.auth
|
||||||
|
|
||||||
|
import cn.hutool.core.lang.Snowflake
|
||||||
import io.vertx.ext.web.RoutingContext
|
import io.vertx.ext.web.RoutingContext
|
||||||
import io.vertx.ext.web.handler.AuthenticationHandler
|
import io.vertx.ext.web.handler.AuthenticationHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -7,41 +8,34 @@ import kotlinx.coroutines.launch
|
|||||||
import org.aikrai.vertx.utlis.Meta
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
|
||||||
class JwtAuthenticationHandler(
|
class JwtAuthenticationHandler(
|
||||||
private val coroutineScope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
private val authHandler: AuthHandler,
|
val tokenService: TokenService,
|
||||||
private val context: String,
|
val context: String,
|
||||||
|
val snowflake: Snowflake
|
||||||
) : AuthenticationHandler {
|
) : AuthenticationHandler {
|
||||||
|
|
||||||
var exclude = mutableListOf(
|
|
||||||
"/auth/**",
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun handle(event: RoutingContext) {
|
override fun handle(event: RoutingContext) {
|
||||||
|
event.put("requestId", snowflake.nextId())
|
||||||
val path = event.request().path().replace("$context/", "/").replace("//", "/")
|
val path = event.request().path().replace("$context/", "/").replace("//", "/")
|
||||||
if (isPathExcluded(path, exclude)) {
|
if (isPathExcluded(path, anonymous)) {
|
||||||
event.next()
|
event.next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
scope.launch {
|
||||||
val authorization = event.request().getHeader("Authorization") ?: null
|
try {
|
||||||
if (authorization == null || !authorization.startsWith("token ")) {
|
val user = tokenService.getLoginUser(event)
|
||||||
event.fail(401, Meta.unauthorized("无效Token"))
|
tokenService.verifyToken(user)
|
||||||
return
|
event.setUser(user)
|
||||||
}
|
|
||||||
|
|
||||||
val token = authorization.substring(6)
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
val authUser = authHandler.handle(token)
|
|
||||||
if (authUser != null) {
|
|
||||||
event.setUser(authUser)
|
|
||||||
event.next()
|
event.next()
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
event.fail(401, Meta.unauthorized("token"))
|
event.fail(401, Meta.unauthorized(e.message ?: "token"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var anonymous = mutableListOf(
|
||||||
|
"/apidoc.json"
|
||||||
|
)
|
||||||
|
|
||||||
private fun isPathExcluded(path: String, excludePatterns: List<String>): Boolean {
|
private fun isPathExcluded(path: String, excludePatterns: List<String>): Boolean {
|
||||||
for (pattern in excludePatterns) {
|
for (pattern in excludePatterns) {
|
||||||
val regexPattern = pattern
|
val regexPattern = pattern
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
package app.config.auth
|
||||||
|
|
||||||
|
import app.config.RespBean
|
||||||
|
import com.google.inject.Singleton
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.config.resp.ResponseHandlerInterface
|
||||||
|
import org.aikrai.vertx.constant.HttpStatus
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ResponseHandler: ResponseHandlerInterface {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
override suspend fun normal(
|
||||||
|
ctx: RoutingContext,
|
||||||
|
responseData: Any?,
|
||||||
|
customizeResponse: Boolean
|
||||||
|
) {
|
||||||
|
val requestId = ctx.get<Long>("requestId") ?: -1L
|
||||||
|
val code: Int
|
||||||
|
val resStr = when (responseData) {
|
||||||
|
is Unit -> {
|
||||||
|
code = HttpStatus.NO_CONTENT
|
||||||
|
null
|
||||||
|
}
|
||||||
|
is RespBean -> {
|
||||||
|
code = responseData.code
|
||||||
|
responseData.requestId = requestId
|
||||||
|
JsonUtil.toJsonStr(responseData)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val respBean = RespBean.success(responseData).apply {
|
||||||
|
this.requestId = requestId
|
||||||
|
}
|
||||||
|
code = respBean.code
|
||||||
|
JsonUtil.toJsonStr(respBean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.put("responseData", resStr)
|
||||||
|
if (customizeResponse) return
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(code)
|
||||||
|
.putHeader("Content-Type", "application/json")
|
||||||
|
.end(resStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务异常处理
|
||||||
|
override suspend fun exception(ctx: RoutingContext, e: Exception) {
|
||||||
|
logger.error { "${ctx.request().uri()}: ${e.stackTraceToString()}" }
|
||||||
|
val resObj = when(e) {
|
||||||
|
is Meta -> {
|
||||||
|
RespBean.failure("${e.name}:${e.message}", e.data)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
RespBean.failure("${e.javaClass.simpleName}${if (e.message != null) ":${e.message}" else ""}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val resStr = JsonUtil.toJsonStr(resObj)
|
||||||
|
ctx.put("responseData", resStr)
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(HttpStatus.ERROR)
|
||||||
|
.putHeader("Content-Type", "application/json")
|
||||||
|
.end(resStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
69
vertx-demo/src/main/kotlin/app/config/auth/TokenService.kt
Normal file
69
vertx-demo/src/main/kotlin/app/config/auth/TokenService.kt
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package app.config.auth
|
||||||
|
|
||||||
|
import app.domain.account.AccountRepository
|
||||||
|
import app.port.reids.RedisClient
|
||||||
|
import cn.hutool.core.lang.Snowflake
|
||||||
|
import cn.hutool.core.util.IdUtil
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import com.google.inject.Singleton
|
||||||
|
import io.vertx.core.cli.UsageMessageFormatter
|
||||||
|
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.ext.web.RoutingContext
|
||||||
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.auth.AuthUser
|
||||||
|
import org.aikrai.vertx.constant.CacheConstants
|
||||||
|
import org.aikrai.vertx.constant.Constants
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class TokenService @Inject constructor(
|
||||||
|
private val snowflake: Snowflake,
|
||||||
|
private val jwtAuth: JWTAuth,
|
||||||
|
private val redisClient: RedisClient,
|
||||||
|
private val accountRepository: AccountRepository,
|
||||||
|
) {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
private val expireSeconds = 60 * 60 * 24 * 7
|
||||||
|
|
||||||
|
suspend fun getLoginUser(ctx: RoutingContext): AuthUser {
|
||||||
|
val request = ctx.request()
|
||||||
|
val authorization = request.getHeader("Authorization")
|
||||||
|
if (UsageMessageFormatter.isNullOrEmpty(authorization) || !authorization.startsWith("token ")) {
|
||||||
|
throw Meta.unauthorized("token")
|
||||||
|
}
|
||||||
|
val token = authorization.substring(6)
|
||||||
|
val user = parseToken(token) ?: throw Meta.unauthorized("token")
|
||||||
|
val userToken = user.principal().getString(Constants.LOGIN_USER_KEY) ?: throw Meta.unauthorized("token")
|
||||||
|
val authInfoStr = redisClient.get(CacheConstants.LOGIN_TOKEN_KEY + userToken) ?: throw Meta.unauthorized("token")
|
||||||
|
return JsonUtil.parseObject(authInfoStr, AuthUser::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createToken(userId: Long, ip: String, client: String): String {
|
||||||
|
val token = IdUtil.randomUUID()
|
||||||
|
val userInfo = accountRepository.getInfo(userId)
|
||||||
|
val user = userInfo?.account ?: throw Meta.notFound("AccountNotFound", "账号不存在")
|
||||||
|
val authInfo = AuthUser(token, JsonUtil.toJsonObject(user), userInfo.rolesArr.toSet(), userInfo.accessArr.toSet(), ip, client)
|
||||||
|
val authInfoStr = JsonUtil.toJsonStr(authInfo)
|
||||||
|
redisClient.set(CacheConstants.LOGIN_TOKEN_KEY + token, authInfoStr, expireSeconds)
|
||||||
|
return genToken(mapOf(Constants.LOGIN_USER_KEY to token))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun verifyToken(loginUser: AuthUser) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun genToken(info: Map<String, Any>, expires: Int? = null): String {
|
||||||
|
val jwtOptions = JWTOptions().setExpiresInSeconds(expires ?: (60 * 60 * 24 * 7))
|
||||||
|
return jwtAuth.generateToken(JsonObject(info), jwtOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseToken(token: String): User? {
|
||||||
|
val tokenCredentials = TokenCredentials(token)
|
||||||
|
return jwtAuth.authenticate(tokenCredentials).coAwait() ?: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,58 +1,33 @@
|
|||||||
package app.controller
|
package app.controller
|
||||||
|
|
||||||
import app.config.Constant
|
import app.domain.account.LoginDTO
|
||||||
import app.domain.user.LoginDTO
|
import app.service.account.AccountService
|
||||||
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 com.google.inject.Inject
|
||||||
import io.vertx.ext.auth.jwt.JWTAuth
|
import io.vertx.ext.web.RoutingContext
|
||||||
import org.aikrai.vertx.auth.AllowAnonymous
|
import org.aikrai.vertx.auth.AllowAnonymous
|
||||||
import org.aikrai.vertx.auth.TokenUtil
|
|
||||||
import org.aikrai.vertx.context.Controller
|
import org.aikrai.vertx.context.Controller
|
||||||
import org.aikrai.vertx.context.D
|
import org.aikrai.vertx.context.D
|
||||||
import org.aikrai.vertx.utlis.Meta
|
|
||||||
|
|
||||||
@AllowAnonymous
|
@AllowAnonymous
|
||||||
@D("认证")
|
@D("认证")
|
||||||
@Controller("/auth")
|
@Controller("/auth")
|
||||||
class AuthController @Inject constructor(
|
class AuthController @Inject constructor(
|
||||||
private val jwtAuth: JWTAuth,
|
private val accountService: AccountService,
|
||||||
private val snowflake: Snowflake,
|
|
||||||
private val userRepository: UserRepository,
|
|
||||||
private val cacheUtil: CacheUtil
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@D("注册")
|
@D("注册")
|
||||||
suspend fun doSign(
|
suspend fun doSign(
|
||||||
|
context: RoutingContext,
|
||||||
@D("loginInfo", "账号信息") loginInfo: LoginDTO
|
@D("loginInfo", "账号信息") loginInfo: LoginDTO
|
||||||
): String {
|
): String {
|
||||||
userRepository.getByName(loginInfo.username)?.let {
|
return accountService.sign(context, loginInfo)
|
||||||
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("登录")
|
@D("登录")
|
||||||
suspend fun doLogin(
|
suspend fun doLogin(
|
||||||
|
context: RoutingContext,
|
||||||
@D("loginInfo", "账号信息") loginInfo: LoginDTO
|
@D("loginInfo", "账号信息") loginInfo: LoginDTO
|
||||||
): String {
|
): String {
|
||||||
val user = userRepository.getByName(loginInfo.username) ?: throw Meta.failure("LoginFailed", "用户名或密码错误")
|
return accountService.login(context, loginInfo)
|
||||||
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", "用户名或密码错误")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
package app.controller
|
package app.controller
|
||||||
|
|
||||||
import app.domain.CargoType
|
import app.domain.CargoType
|
||||||
import app.domain.user.User
|
import app.domain.account.Account
|
||||||
import app.domain.user.UserRepository
|
import app.domain.account.AccountRepository
|
||||||
import app.service.user.UserService
|
import app.service.account.AccountService
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.aikrai.vertx.auth.AllowAnonymous
|
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.config.Config
|
||||||
import org.aikrai.vertx.context.Controller
|
import org.aikrai.vertx.context.Controller
|
||||||
import org.aikrai.vertx.context.D
|
import org.aikrai.vertx.context.D
|
||||||
@ -19,14 +17,15 @@ import org.aikrai.vertx.context.D
|
|||||||
@D("测试1:测试")
|
@D("测试1:测试")
|
||||||
@Controller
|
@Controller
|
||||||
class Demo1Controller @Inject constructor(
|
class Demo1Controller @Inject constructor(
|
||||||
private val userService: UserService,
|
private val accountService: AccountService,
|
||||||
private val userRepository: UserRepository
|
private val accountRepository: AccountRepository
|
||||||
) {
|
) {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
@AllowAnonymous
|
||||||
@D("参数测试", "详细说明......")
|
@D("参数测试", "详细说明......")
|
||||||
suspend fun test1(
|
suspend fun test1(
|
||||||
@D("name", "姓名") name: String,
|
@D("name", "姓名") name: String?,
|
||||||
@D("age", "年龄") age: Int?,
|
@D("age", "年龄") age: Int?,
|
||||||
@D("list", "列表") list: List<String>?,
|
@D("list", "列表") list: List<String>?,
|
||||||
@D("cargoType", "货物类型") cargoType: CargoType?
|
@D("cargoType", "货物类型") cargoType: CargoType?
|
||||||
@ -40,12 +39,15 @@ class Demo1Controller @Inject constructor(
|
|||||||
|
|
||||||
@D("事务测试")
|
@D("事务测试")
|
||||||
suspend fun testT() {
|
suspend fun testT() {
|
||||||
userService.testTransaction()
|
accountService.testTransaction()
|
||||||
}
|
}
|
||||||
|
|
||||||
@D("查询测试")
|
@D("查询测试")
|
||||||
suspend fun getList(): List<User> {
|
suspend fun getUserList(
|
||||||
val list = userRepository.getList()
|
@D("userName", "用户名") userName: String?,
|
||||||
|
@D("phone", "手机号") phone: String?
|
||||||
|
): List<Account> {
|
||||||
|
val list = accountRepository.getUserList(userName, phone)
|
||||||
println(list)
|
println(list)
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|||||||
14
vertx-demo/src/main/kotlin/app/controller/HelloController.kt
Normal file
14
vertx-demo/src/main/kotlin/app/controller/HelloController.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package app.controller
|
||||||
|
|
||||||
|
import org.aikrai.vertx.auth.AllowAnonymous
|
||||||
|
import org.aikrai.vertx.context.Controller
|
||||||
|
import org.aikrai.vertx.context.D
|
||||||
|
|
||||||
|
@AllowAnonymous
|
||||||
|
@D("Hello")
|
||||||
|
@Controller("/")
|
||||||
|
class HelloController {
|
||||||
|
suspend fun hello(): String {
|
||||||
|
return "Hello"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
vertx-demo/src/main/kotlin/app/domain/account/Account.kt
Normal file
34
vertx-demo/src/main/kotlin/app/domain/account/Account.kt
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package app.domain.account
|
||||||
|
|
||||||
|
import org.aikrai.vertx.db.annotation.*
|
||||||
|
import org.aikrai.vertx.utlis.BaseEntity
|
||||||
|
import java.sql.Timestamp
|
||||||
|
|
||||||
|
@TableName("sys_user")
|
||||||
|
class Account : BaseEntity() {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
var userId: Long = 0L
|
||||||
|
|
||||||
|
@TableField("user_name")
|
||||||
|
var userName: String? = ""
|
||||||
|
|
||||||
|
var userType: String? = ""
|
||||||
|
|
||||||
|
var email: String? = ""
|
||||||
|
|
||||||
|
var phone: String? = ""
|
||||||
|
|
||||||
|
var avatar: String? = null
|
||||||
|
|
||||||
|
var password: String? = null
|
||||||
|
|
||||||
|
var status: Char? = null
|
||||||
|
|
||||||
|
var delFlag: Char? = null
|
||||||
|
|
||||||
|
var loginIp: String? = null
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
var loginDate: Timestamp? = null
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package app.domain.account
|
||||||
|
|
||||||
|
import app.base.domain.auth.modle.AccountRoleDTO
|
||||||
|
import app.domain.account.modle.AccountRoleAccessDTO
|
||||||
|
import com.google.inject.ImplementedBy
|
||||||
|
import org.aikrai.vertx.db.Repository
|
||||||
|
|
||||||
|
@ImplementedBy(AccountRepositoryImpl::class)
|
||||||
|
interface AccountRepository : Repository<Long, Account> {
|
||||||
|
suspend fun getByName(name: String): Account?
|
||||||
|
suspend fun getUserList(userName: String?, phone: String?): List<Account>
|
||||||
|
suspend fun getInfo(id: Long): AccountRoleAccessDTO?
|
||||||
|
|
||||||
|
suspend fun getAccountRole(id: Long): AccountRoleDTO?
|
||||||
|
suspend fun bindRoles(id: Long, roles: List<Long>)
|
||||||
|
suspend fun removeAllRole(id: Long): Int
|
||||||
|
}
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
package app.domain.account
|
||||||
|
|
||||||
|
import app.base.domain.auth.modle.AccountRoleDTO
|
||||||
|
import app.domain.account.modle.AccountRoleAccessDTO
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import com.google.inject.Singleton
|
||||||
|
import io.vertx.sqlclient.SqlClient
|
||||||
|
import org.aikrai.vertx.db.RepositoryImpl
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class AccountRepositoryImpl @Inject constructor(
|
||||||
|
sqlClient: SqlClient
|
||||||
|
) : RepositoryImpl<Long, Account>(sqlClient), AccountRepository {
|
||||||
|
|
||||||
|
override suspend fun getUserList(
|
||||||
|
userName: String?,
|
||||||
|
phone: String?
|
||||||
|
): List<Account> {
|
||||||
|
return queryBuilder()
|
||||||
|
.eq(!userName.isNullOrBlank(), Account::userName, userName)
|
||||||
|
.eq(!phone.isNullOrBlank(), Account::phone, phone)
|
||||||
|
.getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getByName(name: String): Account? {
|
||||||
|
val account = queryBuilder()
|
||||||
|
.eq(Account::userName, name)
|
||||||
|
.getOne()
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getInfo(id: Long): AccountRoleAccessDTO? {
|
||||||
|
val sql = """
|
||||||
|
SELECT
|
||||||
|
JSONB_BUILD_OBJECT(
|
||||||
|
'user_id', a.user_id, 'user_name', a.user_name, 'phone', a.phone, 'status', a.status, 'avatar', a.avatar,
|
||||||
|
'password', a.password
|
||||||
|
) AS account,
|
||||||
|
COALESCE(
|
||||||
|
JSONB_AGG(
|
||||||
|
DISTINCT JSONB_BUILD_OBJECT(
|
||||||
|
'role_id', r.role_id, 'role_name', r.role_name, 'role_key', r.role_key, 'remark', r.remark
|
||||||
|
)
|
||||||
|
) FILTER (WHERE r.role_id IS NOT NULL),
|
||||||
|
'[]'::jsonb
|
||||||
|
) AS roles,
|
||||||
|
COALESCE(
|
||||||
|
JSONB_AGG(
|
||||||
|
DISTINCT JSONB_BUILD_OBJECT(
|
||||||
|
'menu_id', m.menu_id, 'menu_name', m.menu_name, 'parent_id', m.parent_id, 'order_num', m.order_num,
|
||||||
|
'menu_type', m.menu_type, 'visible', m.visible, 'path', m.path, 'perms', m.perms,
|
||||||
|
'component', m.component
|
||||||
|
)
|
||||||
|
) FILTER (WHERE m.menu_id IS NOT NULL),
|
||||||
|
'[]'::jsonb
|
||||||
|
) AS access
|
||||||
|
FROM
|
||||||
|
sys_user a
|
||||||
|
LEFT JOIN sys_user_role ar ON a.user_id = ar.user_id
|
||||||
|
LEFT JOIN sys_role r ON ar.role_id = r.role_id
|
||||||
|
LEFT JOIN sys_role_menu rm ON r.role_id = rm.role_id
|
||||||
|
LEFT JOIN sys_menu m ON rm.menu_id = m.menu_id
|
||||||
|
where a.user_id = #{id}
|
||||||
|
GROUP BY a.user_id, a.user_name, a.phone, a.status, a.avatar, a.password;
|
||||||
|
""".trimIndent()
|
||||||
|
return get(sql, mapOf("id" to id), AccountRoleAccessDTO::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAccountRole(id: Long): AccountRoleDTO? {
|
||||||
|
val sql = """
|
||||||
|
SELECT a.user_id, a.user_name, a.phone, a.status, a.avatar, a.password,
|
||||||
|
COALESCE(
|
||||||
|
JSONB_AGG(
|
||||||
|
DISTINCT JSONB_BUILD_OBJECT(
|
||||||
|
'role_id', r.role_id, 'role_name', r.role_name, 'role_key', r.role_key, 'remark', r.remark
|
||||||
|
)
|
||||||
|
) FILTER (WHERE r.role_id IS NOT NULL),
|
||||||
|
'[]'::jsonb
|
||||||
|
) AS roles
|
||||||
|
FROM
|
||||||
|
account a
|
||||||
|
LEFT JOIN account_role ar ON a.id = ar.account_id
|
||||||
|
LEFT JOIN role r ON ar.role_id = r.id
|
||||||
|
WHERE a.id = #{id}
|
||||||
|
GROUP BY a.user_id, a.user_name, a.phone, a.status, a.avatar, a.password;
|
||||||
|
""".trimIndent()
|
||||||
|
return get(sql, mapOf("id" to id), AccountRoleDTO::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun bindRoles(id: Long, roles: List<Long>) {
|
||||||
|
if (roles.isEmpty()) return
|
||||||
|
val sql = StringBuilder("INSERT INTO account_role (account_id, role_id) VALUES ")
|
||||||
|
roles.forEachIndexed { index, roleId ->
|
||||||
|
sql.append("($id, $roleId)")
|
||||||
|
if (index < roles.size - 1) {
|
||||||
|
sql.append(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
execute(sql.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeAllRole(id: Long): Int {
|
||||||
|
val sql = "DELETE FROM account_role WHERE account_id = #{$id}"
|
||||||
|
return execute(sql)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package app.domain.user
|
package app.domain.account
|
||||||
|
|
||||||
data class LoginDTO(
|
data class LoginDTO(
|
||||||
var username: String,
|
var username: String,
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package app.domain.account.modle
|
||||||
|
|
||||||
|
import app.domain.account.Account
|
||||||
|
import app.domain.menu.Menu
|
||||||
|
import app.domain.role.Role
|
||||||
|
|
||||||
|
data class AccountRoleAccessDTO(
|
||||||
|
val account: Account,
|
||||||
|
val roles: List<Role>,
|
||||||
|
val access: List<Menu>,
|
||||||
|
) {
|
||||||
|
val rolesArr: List<String>
|
||||||
|
get() {
|
||||||
|
return roles.mapNotNull { it.roleKey }.filter { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
val accessArr: List<String>
|
||||||
|
get() {
|
||||||
|
return if (rolesArr.contains("admin")) {
|
||||||
|
listOf("*:*:*")
|
||||||
|
} else {
|
||||||
|
access.map { it.perms }.filter { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package app.base.domain.auth.modle
|
||||||
|
|
||||||
|
import app.domain.role.Role
|
||||||
|
|
||||||
|
data class AccountRoleDTO(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val phone: String,
|
||||||
|
// val status: AccountStatus,
|
||||||
|
val avatar: String,
|
||||||
|
val openid: String,
|
||||||
|
val unionid: String,
|
||||||
|
val sopenid: String,
|
||||||
|
val roles: List<Role>,
|
||||||
|
)
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package app.base.domain.auth.modle
|
||||||
|
|
||||||
|
class LoginUser {
|
||||||
|
var accountId: Long = 0L
|
||||||
|
var token: String = ""
|
||||||
|
var loginTime: Long = 0L
|
||||||
|
var expireTime: Long = 0L
|
||||||
|
var ipaddr: String = ""
|
||||||
|
var client: String = ""
|
||||||
|
}
|
||||||
35
vertx-demo/src/main/kotlin/app/domain/menu/Menu.kt
Normal file
35
vertx-demo/src/main/kotlin/app/domain/menu/Menu.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package app.domain.menu
|
||||||
|
|
||||||
|
import org.aikrai.vertx.db.annotation.IdType
|
||||||
|
import org.aikrai.vertx.db.annotation.TableId
|
||||||
|
import org.aikrai.vertx.db.annotation.TableName
|
||||||
|
import org.aikrai.vertx.utlis.BaseEntity
|
||||||
|
import kotlin.jvm.Transient
|
||||||
|
|
||||||
|
@TableName("sys_menu")
|
||||||
|
class Menu : BaseEntity() {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
var menuId = 0L
|
||||||
|
|
||||||
|
var menuName = ""
|
||||||
|
|
||||||
|
var parentId = 0L
|
||||||
|
|
||||||
|
var orderNum = 0
|
||||||
|
|
||||||
|
var path = ""
|
||||||
|
|
||||||
|
var component: String? = ""
|
||||||
|
|
||||||
|
var menuType = ""
|
||||||
|
|
||||||
|
var visible = "0"
|
||||||
|
|
||||||
|
var perms = ""
|
||||||
|
|
||||||
|
var parentName = ""
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var children = mutableListOf<Menu>()
|
||||||
|
}
|
||||||
151
vertx-demo/src/main/kotlin/app/domain/menu/MenuManager.kt
Normal file
151
vertx-demo/src/main/kotlin/app/domain/menu/MenuManager.kt
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package app.domain.menu
|
||||||
|
|
||||||
|
import app.base.domain.auth.menu.MenuRepository
|
||||||
|
import app.domain.account.Account
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import com.google.inject.Singleton
|
||||||
|
import io.vertx.ext.auth.User
|
||||||
|
import org.aikrai.vertx.auth.AuthUser
|
||||||
|
import org.aikrai.vertx.auth.AuthUser.Companion.isAdmin
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class MenuManager @Inject constructor(
|
||||||
|
private val menuRepository: MenuRepository
|
||||||
|
) {
|
||||||
|
suspend fun get(id: Long) = menuRepository.get(id)
|
||||||
|
suspend fun findAll() = menuRepository.list()
|
||||||
|
|
||||||
|
suspend fun getMenuTree(user: User): List<Menu> {
|
||||||
|
return getChildPerms(list(user), 0).toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun list(user: User, name: String? = null): List<Menu> {
|
||||||
|
val authUser = (user as AuthUser)
|
||||||
|
return if (authUser.isAdmin()) {
|
||||||
|
menuRepository.list(name)
|
||||||
|
} else {
|
||||||
|
menuRepository.list(name, (authUser.user as Account).userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findByRoleId(user: User, roleId: Long): List<Long> {
|
||||||
|
if ((user as AuthUser).isAdmin()) {
|
||||||
|
return menuRepository.list().map { it.menuId }
|
||||||
|
}
|
||||||
|
val menuList = menuRepository.list(roleId = roleId)
|
||||||
|
return menuList.map { it.menuId }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun add(
|
||||||
|
menuName: String,
|
||||||
|
parentId: Long?,
|
||||||
|
orderNum: Int?,
|
||||||
|
path: String,
|
||||||
|
component: String?,
|
||||||
|
menuType: String?,
|
||||||
|
visible: String?,
|
||||||
|
perms: String
|
||||||
|
) {
|
||||||
|
if (menuRepository.list(menuName).isNotEmpty()) {
|
||||||
|
throw Meta.error("MenuNameConflict", "菜单名称已存在")
|
||||||
|
}
|
||||||
|
val menu = Menu().apply {
|
||||||
|
this.menuName = menuName
|
||||||
|
this.path = path
|
||||||
|
this.perms = perms
|
||||||
|
parentId?.let { this.parentId = it }
|
||||||
|
orderNum?.let { this.orderNum = it }
|
||||||
|
component?.let { this.component = it }
|
||||||
|
menuType?.let { this.menuType = it }
|
||||||
|
visible?.let { this.visible = it }
|
||||||
|
}
|
||||||
|
menuRepository.create(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun edit(
|
||||||
|
menuId: Long,
|
||||||
|
menuName: String?,
|
||||||
|
parentId: Long?,
|
||||||
|
orderNum: Int?,
|
||||||
|
path: String?,
|
||||||
|
component: String?,
|
||||||
|
menuType: String?,
|
||||||
|
visible: String?,
|
||||||
|
perms: String?
|
||||||
|
) {
|
||||||
|
val menu = menuRepository.get(menuId) ?: throw Meta.notFound("MenuNotFound", "菜单不存在")
|
||||||
|
|
||||||
|
if (menuName != null && menuName != menu.menuName && menuRepository.list(menuName).isNotEmpty()) {
|
||||||
|
throw Meta.error("MenuNameConflict", "菜单名称已存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.apply {
|
||||||
|
menuName?.let { this.menuName = it }
|
||||||
|
path?.let { this.path = it }
|
||||||
|
perms?.let { this.perms = it }
|
||||||
|
parentId?.let { this.parentId = it }
|
||||||
|
orderNum?.let { this.orderNum = it }
|
||||||
|
component?.let { this.component = it }
|
||||||
|
menuType?.let { this.menuType = it }
|
||||||
|
visible?.let { this.visible = it }
|
||||||
|
}
|
||||||
|
menuRepository.update(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun remove(menuId: Long) {
|
||||||
|
menuRepository.delete(menuId)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* 根据父节点的ID获取所有子节点
|
||||||
|
*
|
||||||
|
* @param list 分类表
|
||||||
|
* @param parentId 传入的父节点ID
|
||||||
|
* @return List<SysMenu>
|
||||||
|
*/
|
||||||
|
fun getChildPerms(list: List<Menu>, parentId: Long): List<Menu> {
|
||||||
|
val returnList = mutableListOf<Menu>()
|
||||||
|
for (t in list) {
|
||||||
|
// 根据传入的某个父节点ID,遍历该父节点的所有子节点
|
||||||
|
if (t.parentId == parentId) {
|
||||||
|
recursionFn(list, t)
|
||||||
|
returnList.add(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归列表
|
||||||
|
*
|
||||||
|
* @param list 分类表
|
||||||
|
* @param t 子节点
|
||||||
|
*/
|
||||||
|
private fun recursionFn(list: List<Menu>, t: Menu) {
|
||||||
|
// 得到子节点列表
|
||||||
|
val childList = getChildList(list, t).toMutableList()
|
||||||
|
t.children = childList
|
||||||
|
for (tChild in childList) {
|
||||||
|
if (hasChild(list, tChild)) {
|
||||||
|
recursionFn(list, tChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 得到子节点列表
|
||||||
|
*/
|
||||||
|
private fun getChildList(list: List<Menu>, t: Menu): List<Menu> {
|
||||||
|
return list.filter { it.parentId == t.menuId }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否有子节点
|
||||||
|
*/
|
||||||
|
private fun hasChild(list: List<Menu>, t: Menu): Boolean {
|
||||||
|
return getChildList(list, t).isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
vertx-demo/src/main/kotlin/app/domain/menu/MenuRepository.kt
Normal file
11
vertx-demo/src/main/kotlin/app/domain/menu/MenuRepository.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package app.base.domain.auth.menu
|
||||||
|
|
||||||
|
import app.domain.menu.Menu
|
||||||
|
import app.domain.menu.MenuRepositoryImpl
|
||||||
|
import com.google.inject.ImplementedBy
|
||||||
|
import org.aikrai.vertx.db.Repository
|
||||||
|
|
||||||
|
@ImplementedBy(MenuRepositoryImpl::class)
|
||||||
|
interface MenuRepository : Repository<Long, Menu> {
|
||||||
|
suspend fun list(name: String? = null, accountId: Long? = null, roleId: Long? = null): List<Menu>
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package app.domain.menu
|
||||||
|
|
||||||
|
import app.base.domain.auth.menu.MenuRepository
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import io.vertx.sqlclient.SqlClient
|
||||||
|
import org.aikrai.vertx.db.RepositoryImpl
|
||||||
|
|
||||||
|
class MenuRepositoryImpl @Inject constructor(
|
||||||
|
sqlClient: SqlClient
|
||||||
|
) : RepositoryImpl<Long, Menu>(sqlClient), MenuRepository {
|
||||||
|
|
||||||
|
override suspend fun list(name: String?, accountId: Long?, roleId: Long?): List<Menu> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
14
vertx-demo/src/main/kotlin/app/domain/menu/MenuType.kt
Normal file
14
vertx-demo/src/main/kotlin/app/domain/menu/MenuType.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package app.base.domain.auth.menu
|
||||||
|
|
||||||
|
enum class MenuType(val desc: String) {
|
||||||
|
M("目录"),
|
||||||
|
C("菜单"),
|
||||||
|
F("按钮");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(value: String?): MenuType? {
|
||||||
|
if (value.isNullOrBlank()) return null
|
||||||
|
return MenuType.values().find { it.name == value || it.desc == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
vertx-demo/src/main/kotlin/app/domain/menu/modle/RouterVo.kt
Normal file
28
vertx-demo/src/main/kotlin/app/domain/menu/modle/RouterVo.kt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package app.base.domain.auth.menu.modle
|
||||||
|
|
||||||
|
data class RouterVo(
|
||||||
|
/**
|
||||||
|
* 路由名字
|
||||||
|
*/
|
||||||
|
var name: String? = null,
|
||||||
|
/**
|
||||||
|
* 路由地址
|
||||||
|
*/
|
||||||
|
var path: String? = null,
|
||||||
|
/**
|
||||||
|
* 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现
|
||||||
|
*/
|
||||||
|
var hidden: Boolean = false,
|
||||||
|
/**
|
||||||
|
* 组件地址
|
||||||
|
*/
|
||||||
|
var component: String? = null,
|
||||||
|
/**
|
||||||
|
* 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
|
||||||
|
*/
|
||||||
|
var alwaysShow: Boolean? = null,
|
||||||
|
/**
|
||||||
|
* 子路由
|
||||||
|
*/
|
||||||
|
var children: List<RouterVo>? = null
|
||||||
|
)
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package app.base.domain.auth.menu.modle
|
||||||
|
|
||||||
|
import app.domain.menu.Menu
|
||||||
|
|
||||||
|
class TreeSelect {
|
||||||
|
|
||||||
|
/** 节点ID */
|
||||||
|
var id: Long? = null
|
||||||
|
|
||||||
|
/** 节点名称 */
|
||||||
|
var label: String? = null
|
||||||
|
|
||||||
|
/** 节点禁用 */
|
||||||
|
var disabled: Boolean = false
|
||||||
|
|
||||||
|
/** 子节点 */
|
||||||
|
// @JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
|
var children: List<TreeSelect>? = null
|
||||||
|
|
||||||
|
constructor() // 无参构造函数
|
||||||
|
|
||||||
|
// constructor(dept: SysDept) { // 带 SysDept 参数的构造函数
|
||||||
|
// this.id = dept.deptId
|
||||||
|
// this.label = dept.deptName
|
||||||
|
// this.disabled = UserConstants.DEPT_DISABLE == dept.status
|
||||||
|
// this.children = dept.children.map { TreeSelect(it) }
|
||||||
|
// }
|
||||||
|
|
||||||
|
constructor(menu: Menu) { // 带 SysMenu 参数的构造函数
|
||||||
|
this.id = menu.menuId
|
||||||
|
this.label = menu.menuName
|
||||||
|
this.children = menu.children.map { TreeSelect(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,42 +1,22 @@
|
|||||||
package app.domain.role
|
package app.domain.role
|
||||||
|
|
||||||
import jakarta.persistence.*
|
import org.aikrai.vertx.db.annotation.TableName
|
||||||
import jakarta.validation.constraints.NotNull
|
import org.aikrai.vertx.utlis.BaseEntity
|
||||||
import jakarta.validation.constraints.Size
|
|
||||||
|
|
||||||
@Entity
|
@TableName("sys_role")
|
||||||
@Table(name = "`sys_role`", schema = "`vertx-demo`")
|
class Role : BaseEntity() {
|
||||||
class Role : org.aikrai.vertx.utlis.Entity() {
|
|
||||||
@Id
|
var roleId: Long = 0L
|
||||||
@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
|
var roleName: String? = null
|
||||||
|
|
||||||
@Size(max = 100)
|
|
||||||
@NotNull
|
|
||||||
@Column(name = "role_key", nullable = false, length = 100)
|
|
||||||
var roleKey: String? = null
|
var roleKey: String? = null
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Column(name = "role_sort", nullable = false)
|
|
||||||
var roleSort: Int? = null
|
var roleSort: Int? = null
|
||||||
|
|
||||||
@Column(name = "data_scope")
|
|
||||||
var dataScope: Char? = null
|
var dataScope: Char? = null
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Column(name = "status", nullable = false)
|
|
||||||
var status: Char? = null
|
var status: Char? = null
|
||||||
|
|
||||||
@Column(name = "del_flag")
|
|
||||||
var delFlag: Char? = null
|
var delFlag: Char? = null
|
||||||
|
|
||||||
@Size(max = 500)
|
|
||||||
@Column(name = "remark", length = 500)
|
|
||||||
var remark: String? = null
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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>
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package app.verticle
|
package app.port.aipfox
|
||||||
|
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
import com.google.inject.name.Named
|
import com.google.inject.name.Named
|
||||||
@ -7,7 +7,6 @@ import io.vertx.core.http.HttpMethod
|
|||||||
import io.vertx.core.json.JsonObject
|
import io.vertx.core.json.JsonObject
|
||||||
import io.vertx.ext.web.client.WebClient
|
import io.vertx.ext.web.client.WebClient
|
||||||
import io.vertx.ext.web.client.WebClientOptions
|
import io.vertx.ext.web.client.WebClientOptions
|
||||||
import io.vertx.kotlin.coroutines.CoroutineVerticle
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.aikrai.vertx.openapi.OpenApiSpecGenerator
|
import org.aikrai.vertx.openapi.OpenApiSpecGenerator
|
||||||
|
|
||||||
@ -18,14 +17,10 @@ class ApifoxClient @Inject constructor(
|
|||||||
@Named("apifox.folderId") private val folderId: String,
|
@Named("apifox.folderId") private val folderId: String,
|
||||||
@Named("server.name") private val serverName: String,
|
@Named("server.name") private val serverName: String,
|
||||||
@Named("server.port") private val port: String
|
@Named("server.port") private val port: String
|
||||||
) : CoroutineVerticle() {
|
) {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
override suspend fun start() {
|
fun importOpenapi() {
|
||||||
importOpenapi()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importOpenapi() {
|
|
||||||
val openApiJsonStr = OpenApiSpecGenerator().genOpenApiSpecStr(serverName, "1.0", "http://127.0.0.1:$port/api")
|
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 options = WebClientOptions().setDefaultPort(443).setDefaultHost("api.apifox.com").setSsl(true)
|
||||||
val client = WebClient.create(vertx, options)
|
val client = WebClient.create(vertx, options)
|
||||||
51
vertx-demo/src/main/kotlin/app/port/reids/RedisClient.kt
Normal file
51
vertx-demo/src/main/kotlin/app/port/reids/RedisClient.kt
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package app.port.reids
|
||||||
|
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import com.google.inject.Singleton
|
||||||
|
import io.vertx.core.Vertx
|
||||||
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
|
import io.vertx.redis.client.*
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.config.Config
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class RedisClient @Inject constructor(
|
||||||
|
vertx: Vertx
|
||||||
|
) {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
private val host = Config.getKey("redis.host").toString()
|
||||||
|
private val port = Config.getKey("redis.port").toString()
|
||||||
|
private val database = Config.getKey("redis.database").toString().toInt()
|
||||||
|
private val password = Config.getKey("redis.password").toString()
|
||||||
|
private val maxPoolSize = Config.getKey("redis.maxPoolSize").toString().toInt()
|
||||||
|
private val maxPoolWaiting = Config.getKey("redis.maxPoolWaiting").toString().toInt()
|
||||||
|
|
||||||
|
private var redisClient = Redis.createClient(
|
||||||
|
vertx,
|
||||||
|
RedisOptions()
|
||||||
|
.setType(RedisClientType.STANDALONE)
|
||||||
|
.addConnectionString("redis://$host:$port/$database")
|
||||||
|
.setPassword(password)
|
||||||
|
.setMaxPoolSize(maxPoolSize)
|
||||||
|
.setMaxPoolWaiting(maxPoolWaiting)
|
||||||
|
)
|
||||||
|
|
||||||
|
// EX秒,PX毫秒
|
||||||
|
suspend fun set(key: String, value: String, expireSeconds: Int) {
|
||||||
|
redisClient.send(Request.cmd(Command.SET, key, value, "EX", expireSeconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(key: String): String? {
|
||||||
|
val res = redisClient.send(Request.cmd(Command.GET, key)).coAwait()
|
||||||
|
return res?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun incr(key: String): Int {
|
||||||
|
val res = redisClient.send(Request.cmd(Command.INCR, key)).coAwait()
|
||||||
|
return res?.toInteger() ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun expire(key: String, expire: String) {
|
||||||
|
redisClient.send(Request.cmd(Command.EXPIRE, key, expire))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
package app.service.account
|
||||||
|
|
||||||
|
import app.config.auth.TokenService
|
||||||
|
import app.domain.account.Account
|
||||||
|
import app.domain.account.AccountRepository
|
||||||
|
import app.domain.account.LoginDTO
|
||||||
|
import cn.hutool.core.lang.Snowflake
|
||||||
|
import cn.hutool.crypto.SecureUtil
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
import org.aikrai.vertx.db.tx.withTransaction
|
||||||
|
import org.aikrai.vertx.utlis.IpUtil
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
class AccountService @Inject constructor(
|
||||||
|
private val snowflake: Snowflake,
|
||||||
|
private val accountRepository: AccountRepository,
|
||||||
|
private val tokenService: TokenService,
|
||||||
|
) {
|
||||||
|
suspend fun testTransaction() {
|
||||||
|
withTransaction {
|
||||||
|
accountRepository.update(1L, mapOf("avatar" to "test001"))
|
||||||
|
// throw Meta.failure("test transaction", "test transaction")
|
||||||
|
accountRepository.update(1L, mapOf("avatar" to "test002"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sign(
|
||||||
|
context: RoutingContext,
|
||||||
|
loginInfo: LoginDTO
|
||||||
|
): String {
|
||||||
|
val ipAddr = IpUtil.getIpAddr(context.request())
|
||||||
|
accountRepository.getByField(Account::userName, loginInfo.username)?.let {
|
||||||
|
throw Meta("authentication_failed", "名称已被占用")
|
||||||
|
}
|
||||||
|
val account = Account().apply {
|
||||||
|
// this.userId = snowflake.nextId()
|
||||||
|
this.userName = loginInfo.username
|
||||||
|
this.password = SecureUtil.sha1(loginInfo.password)
|
||||||
|
}
|
||||||
|
accountRepository.create(account)
|
||||||
|
return tokenService.createToken(account.userId, ipAddr, "management")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun login(
|
||||||
|
context: RoutingContext,
|
||||||
|
loginInfo: LoginDTO
|
||||||
|
): String {
|
||||||
|
val ipAddr = IpUtil.getIpAddr(context.request())
|
||||||
|
val account = accountRepository.getByField(Account::userName, loginInfo.username) ?: throw Meta(
|
||||||
|
"authentication_failed",
|
||||||
|
"账号或密码错误"
|
||||||
|
)
|
||||||
|
if (!SecureUtil.sha1(loginInfo.password).equals(account.password)) {
|
||||||
|
throw Meta(
|
||||||
|
"authentication_failed",
|
||||||
|
"账号或密码错误"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return tokenService.createToken(account.userId, ipAddr, "management")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun genNumericStr(length: Int): String {
|
||||||
|
val random = SecureRandom()
|
||||||
|
val numericCode = StringBuilder(length)
|
||||||
|
for (i in 0 until length) {
|
||||||
|
val digit = random.nextInt(10)
|
||||||
|
numericCode.append(digit)
|
||||||
|
}
|
||||||
|
return numericCode.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,14 +6,12 @@ import mu.KotlinLogging
|
|||||||
|
|
||||||
class MainVerticle @Inject constructor(
|
class MainVerticle @Inject constructor(
|
||||||
private val webVerticle: WebVerticle,
|
private val webVerticle: WebVerticle,
|
||||||
private val apifoxClient: ApifoxClient
|
|
||||||
) : CoroutineVerticle() {
|
) : CoroutineVerticle() {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
override suspend fun start() {
|
override suspend fun start() {
|
||||||
val verticles = listOf(
|
val verticles = listOf(
|
||||||
webVerticle,
|
webVerticle,
|
||||||
apifoxClient
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for (verticle in verticles) {
|
for (verticle in verticles) {
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
package app.verticle
|
package app.verticle
|
||||||
|
|
||||||
import app.config.auth.AuthHandler
|
import app.config.RespBean
|
||||||
import app.config.auth.JwtAuthenticationHandler
|
import app.config.auth.JwtAuthenticationHandler
|
||||||
|
import app.config.auth.ResponseHandler
|
||||||
|
import app.config.auth.TokenService
|
||||||
|
import app.domain.account.Account
|
||||||
|
import app.port.aipfox.ApifoxClient
|
||||||
|
import cn.hutool.core.lang.Snowflake
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
import com.google.inject.Injector
|
import com.google.inject.Injector
|
||||||
import com.google.inject.name.Named
|
import com.google.inject.name.Named
|
||||||
@ -17,32 +22,39 @@ import io.vertx.kotlin.coroutines.CoroutineVerticle
|
|||||||
import io.vertx.kotlin.coroutines.coAwait
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.aikrai.vertx.config.FailureParser
|
import org.aikrai.vertx.auth.AuthUser
|
||||||
|
import org.aikrai.vertx.config.Config
|
||||||
import org.aikrai.vertx.context.RouterBuilder
|
import org.aikrai.vertx.context.RouterBuilder
|
||||||
import org.aikrai.vertx.jackson.JsonUtil
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import org.aikrai.vertx.utlis.LangUtil.toStringMap
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
|
||||||
class WebVerticle @Inject constructor(
|
class WebVerticle @Inject constructor(
|
||||||
private val getIt: Injector,
|
private val getIt: Injector,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val authHandler: AuthHandler,
|
private val tokenService: TokenService,
|
||||||
@Named("server.name") private val serverName: String,
|
private val apifoxClient: ApifoxClient,
|
||||||
@Named("server.port") private val port: String,
|
private val snowflake: Snowflake,
|
||||||
@Named("server.context") private val context: String
|
private val responseHandler: ResponseHandler,
|
||||||
|
@Named("server.port") private val port: Int,
|
||||||
|
@Named("server.context") private val context: String,
|
||||||
) : CoroutineVerticle() {
|
) : CoroutineVerticle() {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
|
||||||
override suspend fun start() {
|
override suspend fun start() {
|
||||||
val rootRouter = Router.router(vertx)
|
val rootRouter = Router.router(vertx)
|
||||||
val router = Router.router(vertx)
|
val router = Router.router(vertx)
|
||||||
setupRouter(rootRouter, router)
|
setupRouter(rootRouter, router)
|
||||||
|
|
||||||
val options = HttpServerOptions().setMaxFormAttributeSize(1024 * 1024)
|
val options = HttpServerOptions().setMaxFormAttributeSize(1024 * 1024)
|
||||||
val server = vertx.createHttpServer(options)
|
val server = vertx.createHttpServer(options)
|
||||||
.requestHandler(rootRouter)
|
.requestHandler(rootRouter)
|
||||||
.listen(port.toInt())
|
.listen(port)
|
||||||
.coAwait()
|
.coAwait()
|
||||||
|
|
||||||
logger.info { "http server start - http://127.0.0.1:${server.actualPort()}" }
|
apifoxClient.importOpenapi()
|
||||||
|
|
||||||
|
logger.info { "http server start - http://127.0.0.1:${server.actualPort()}/$context" }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stop() {
|
override suspend fun stop() {
|
||||||
@ -54,25 +66,16 @@ class WebVerticle @Inject constructor(
|
|||||||
.handler(corsHandler)
|
.handler(corsHandler)
|
||||||
.failureHandler(errorHandler)
|
.failureHandler(errorHandler)
|
||||||
.handler(BodyHandler.create())
|
.handler(BodyHandler.create())
|
||||||
|
.handler(logHandler)
|
||||||
|
|
||||||
val authHandler = JwtAuthenticationHandler(coroutineScope, authHandler, context)
|
val authHandler = JwtAuthenticationHandler(coroutineScope, tokenService, context, snowflake)
|
||||||
router.route("/*").handler(authHandler)
|
router.route("/*").handler(authHandler)
|
||||||
|
|
||||||
val routerBuilder = RouterBuilder(coroutineScope, router).build { service ->
|
val scanPath = Config.getKeyAsString("server.package")
|
||||||
|
val routerBuilder = RouterBuilder(coroutineScope, router, scanPath, responseHandler).build { service ->
|
||||||
getIt.getInstance(service)
|
getIt.getInstance(service)
|
||||||
}
|
}
|
||||||
authHandler.exclude.addAll(routerBuilder.anonymousPaths)
|
authHandler.anonymous.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()
|
private val corsHandler = CorsHandler.create()
|
||||||
@ -83,23 +86,75 @@ class WebVerticle @Inject constructor(
|
|||||||
.allowedMethod(HttpMethod.DELETE)
|
.allowedMethod(HttpMethod.DELETE)
|
||||||
.allowedMethod(HttpMethod.OPTIONS)
|
.allowedMethod(HttpMethod.OPTIONS)
|
||||||
|
|
||||||
|
// 非业务异常处理
|
||||||
private val errorHandler = Handler<RoutingContext> { ctx ->
|
private val errorHandler = Handler<RoutingContext> { ctx ->
|
||||||
val failure = ctx.failure()
|
val failure = ctx.failure()
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
logger.error { "${ctx.request().uri()}: ${failure.stackTraceToString()}" }
|
logger.error { "${ctx.request().uri()}: ${failure.stackTraceToString()}" }
|
||||||
|
val resObj = when (failure) {
|
||||||
val parsedFailure = FailureParser.parse(ctx.statusCode(), failure)
|
is Meta -> RespBean.failure("${failure.name}:${failure.message}", failure.data)
|
||||||
val response = ctx.response()
|
else -> RespBean.failure("${failure.javaClass.simpleName}${if (failure.message != null) ":${failure.message}" else ""}")
|
||||||
|
}
|
||||||
response.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
val resStr = JsonUtil.toJsonStr(resObj)
|
||||||
response.statusCode = if (ctx.statusCode() != 200) ctx.statusCode() else 500
|
ctx.put("responseData", resStr)
|
||||||
response.end(JsonUtil.toJsonStr(parsedFailure.response))
|
ctx.response()
|
||||||
|
.setStatusCode(if (ctx.statusCode() != 200) ctx.statusCode() else 500)
|
||||||
|
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||||
|
.end(resStr)
|
||||||
} else {
|
} else {
|
||||||
logger.error("${ctx.request().uri()}: 未知错误")
|
logger.error("${ctx.request().uri()}: 未知错误")
|
||||||
val response = ctx.response()
|
val resObj = RespBean.failure("未知错误")
|
||||||
response.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
val resStr = JsonUtil.toJsonStr(resObj)
|
||||||
response.statusCode = 500
|
ctx.put("responseData", resStr)
|
||||||
response.end()
|
ctx.response()
|
||||||
|
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||||
|
.setStatusCode(500)
|
||||||
|
.end(resStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val logHandler = Handler<RoutingContext> { ctx ->
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
ctx.response().endHandler {
|
||||||
|
val end = System.currentTimeMillis()
|
||||||
|
val timeCost = "${end - start}ms".let {
|
||||||
|
when (end - start) {
|
||||||
|
in 0..500 -> it
|
||||||
|
in 501..2000 -> "$it⚠️"
|
||||||
|
else -> "$it❌"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val authUser = ctx.user() as? AuthUser
|
||||||
|
val logContent = if (authUser != null) {
|
||||||
|
val user = JsonUtil.parseObject(authUser.user, Account::class.java)
|
||||||
|
"""
|
||||||
|
|
|
||||||
|
|>>>>>请求ID:[${ctx.get<String>("requestId")}]
|
||||||
|
|>>>>>请求URL:[${ctx.request().path()}](${ctx.request().method()})
|
||||||
|
|>>>>>请求IP:[${ctx.request().remoteAddress().host()}]
|
||||||
|
|>>>>>用户名:[${user.userName}]
|
||||||
|
|>>>>>用户ID:[${user.userId}]
|
||||||
|
|>>>>>角色:[${authUser.roles}]
|
||||||
|
|>>>>>请求参数:[${JsonUtil.toJsonStr(ctx.request().params().toStringMap())}]
|
||||||
|
|>>>>>请求体:[${JsonUtil.toJsonStr(ctx.body().asString())}]
|
||||||
|
|>>>>>响应结果:[${ctx.get<String>("responseData")}]
|
||||||
|
|>>>>>耗时:[$timeCost]
|
||||||
|
""".trimMargin()
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
|
|
||||||
|
|>>>>>请求ID:[${ctx.get<String>("requestId")}]
|
||||||
|
|>>>>>请求URL:["${ctx.request().uri()}"](${ctx.request().method()})
|
||||||
|
|>>>>>请求IP:[${ctx.request().remoteAddress().host()}]
|
||||||
|
|>>>>>身份:[未验证]
|
||||||
|
|>>>>>请求参数:[${JsonUtil.toJsonStr(ctx.request().params().toStringMap())}]
|
||||||
|
|>>>>>请求体:[${JsonUtil.toJsonStr(ctx.body().asString())}]
|
||||||
|
|>>>>>响应结果:[${ctx.get<String>("responseData")}]
|
||||||
|
|>>>>>耗时:[$timeCost]
|
||||||
|
""".trimMargin()
|
||||||
|
}
|
||||||
|
logger.info(logContent)
|
||||||
|
}
|
||||||
|
ctx.next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
server:
|
server:
|
||||||
name: vtx_demo
|
name: vtx_demo
|
||||||
active: dev
|
|
||||||
port: 8080
|
|
||||||
context: api # 上下文
|
context: api # 上下文
|
||||||
timeout: 120 # eventbus超时时间
|
timeout: 120 # eventbus超时时间
|
||||||
http:
|
http:
|
||||||
@ -15,8 +13,3 @@ server:
|
|||||||
timeout: 10000 # 毫秒
|
timeout: 10000 # 毫秒
|
||||||
jwt:
|
jwt:
|
||||||
key: 123456sdfjasdfjl # jwt加密key
|
key: 123456sdfjasdfjl # jwt加密key
|
||||||
|
|
||||||
apifox:
|
|
||||||
token: APS-xxx
|
|
||||||
projectId: xxx
|
|
||||||
folderId: xxx
|
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
apifox:
|
||||||
|
token: APS-xxxxxxxxxxxxxxx
|
||||||
|
projectId: xxxxxx
|
||||||
|
folderId: xxxxxx
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
databases:
|
||||||
|
name: vertx-demo
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 5432
|
||||||
|
username: root
|
||||||
|
password: 123456
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
database: 0
|
||||||
|
password: xxx
|
||||||
|
maxPoolSize: 8
|
||||||
|
maxPoolWaiting: 2000
|
||||||
@ -1,9 +0,0 @@
|
|||||||
databases:
|
|
||||||
has-open: true
|
|
||||||
type: mysql
|
|
||||||
driver-class-name: com.mysql.jdbc.Driver
|
|
||||||
name: vertx-demo
|
|
||||||
host: 127.0.0.1
|
|
||||||
port: 3306
|
|
||||||
username: root
|
|
||||||
password: 123456
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
databases:
|
|
||||||
has-open: true
|
|
||||||
type: mysql
|
|
||||||
driver-class-name: com.mysql.jdbc.Driver
|
|
||||||
name: vertx-demo
|
|
||||||
host: 127.0.0.1
|
|
||||||
port: 3306
|
|
||||||
username: root
|
|
||||||
password: 123456
|
|
||||||
@ -1,10 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<configuration
|
<configuration
|
||||||
debug="false" scan="true" scanPeriod="30 second">
|
debug="false" scan="true" scanPeriod="30 second">
|
||||||
<property name="ROOT" value="../bucket/log/"/>
|
<property name="ROOT" value="./log/"/>
|
||||||
<property name="APPNAME" value="vertx-fw"/>
|
<property name="APPNAME" value="vertx-demo"/>
|
||||||
<property name="FILESIZE" value="500MB"/>
|
<property name="FILESIZE" value="500MB"/>
|
||||||
<property name="MAXHISTORY" value="100"/>
|
<property name="MAXHISTORY" value="100"/>
|
||||||
|
|
||||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<Target>System.out</Target>
|
<Target>System.out</Target>
|
||||||
<encoder charset="utf-8">
|
<encoder charset="utf-8">
|
||||||
@ -12,6 +13,7 @@
|
|||||||
</pattern>
|
</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
<encoder charset="utf-8">
|
<encoder charset="utf-8">
|
||||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
@ -28,18 +30,21 @@
|
|||||||
<fileNamePattern>${ROOT}${APPNAME}-%d-warn.%i.log</fileNamePattern>
|
<fileNamePattern>${ROOT}${APPNAME}-%d-warn.%i.log</fileNamePattern>
|
||||||
<maxHistory>${MAXHISTORY}</maxHistory>
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
<timeBasedFileNamingAndTriggeringPolicy
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
<maxFileSize>${FILESIZE}</maxFileSize>
|
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||||
</timeBasedFileNamingAndTriggeringPolicy>
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
<encoder charset="utf-8">
|
<encoder charset="utf-8">
|
||||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
</pattern>
|
</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
<level>INFO</level>
|
<evaluator>
|
||||||
|
<expression>return level >= INFO;</expression>
|
||||||
|
</evaluator>
|
||||||
<onMatch>ACCEPT</onMatch>
|
<onMatch>ACCEPT</onMatch>
|
||||||
<onMismatch>DENY</onMismatch>
|
<onMismatch>DENY</onMismatch>
|
||||||
</filter>
|
</filter>
|
||||||
@ -47,52 +52,59 @@
|
|||||||
<fileNamePattern>${ROOT}${APPNAME}-%d-info.%i.log</fileNamePattern>
|
<fileNamePattern>${ROOT}${APPNAME}-%d-info.%i.log</fileNamePattern>
|
||||||
<maxHistory>${MAXHISTORY}</maxHistory>
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
<timeBasedFileNamingAndTriggeringPolicy
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
<maxFileSize>${FILESIZE}</maxFileSize>
|
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||||
</timeBasedFileNamingAndTriggeringPolicy>
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
<encoder charset="utf-8">
|
<encoder charset="utf-8">
|
||||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
</pattern>
|
</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
<level>DEBUG</level>
|
<evaluator>
|
||||||
|
<expression>return level >= DEBUG;</expression>
|
||||||
|
</evaluator>
|
||||||
<onMatch>ACCEPT</onMatch>
|
<onMatch>ACCEPT</onMatch>
|
||||||
<onMismatch>DENY</onMismatch>
|
<onMismatch>DENY</onMismatch>
|
||||||
</filter>
|
</filter>
|
||||||
<rollingPolicy
|
<rollingPolicy
|
||||||
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
<fileNamePattern>${ROOT}${APPNAME}-%d-debug.%i.log</fileNamePattern>
|
<fileNamePattern>${ROOT}${APPNAME}-%d-info.%i.log</fileNamePattern>
|
||||||
<maxHistory>${MAXHISTORY}</maxHistory>
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
<timeBasedFileNamingAndTriggeringPolicy
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
<maxFileSize>${FILESIZE}</maxFileSize>
|
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||||
</timeBasedFileNamingAndTriggeringPolicy>
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<appender name="TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
<encoder charset="utf-8">
|
<encoder charset="utf-8">
|
||||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||||
</pattern>
|
</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
<level>TRACE</level>
|
<evaluator>
|
||||||
|
<expression>return level >= TRACE;</expression>
|
||||||
|
</evaluator>
|
||||||
<onMatch>ACCEPT</onMatch>
|
<onMatch>ACCEPT</onMatch>
|
||||||
<onMismatch>DENY</onMismatch>
|
<onMismatch>DENY</onMismatch>
|
||||||
</filter>
|
</filter>
|
||||||
<rollingPolicy
|
<rollingPolicy
|
||||||
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
<fileNamePattern>${ROOT}${APPNAME}-%d-trace.%i.log</fileNamePattern>
|
<fileNamePattern>${ROOT}${APPNAME}-%d-info.%i.log</fileNamePattern>
|
||||||
<maxHistory>${MAXHISTORY}</maxHistory>
|
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||||
<timeBasedFileNamingAndTriggeringPolicy
|
<timeBasedFileNamingAndTriggeringPolicy
|
||||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
<maxFileSize>${FILESIZE}</maxFileSize>
|
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||||
</timeBasedFileNamingAndTriggeringPolicy>
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
<root level="INFO">
|
|
||||||
|
<root level="DEBUG">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
<appender-ref ref="WARN"/>
|
<appender-ref ref="WARN"/>
|
||||||
<appender-ref ref="INFO"/>
|
<appender-ref ref="INFO"/>
|
||||||
|
|||||||
@ -1,39 +1,56 @@
|
|||||||
|
/*
|
||||||
|
Source Server Type : PostgreSQL
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for sys_user
|
-- Table structure for sys_user
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
DROP TABLE IF EXISTS `sys_user`;
|
DROP TABLE IF EXISTS "public"."sys_user";
|
||||||
CREATE TABLE `sys_user` (
|
CREATE TABLE "public"."sys_user" (
|
||||||
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
|
"user_id" int8 NOT NULL,
|
||||||
`dept_id` bigint(20) NULL DEFAULT NULL COMMENT '部门ID',
|
"dept_id" int8,
|
||||||
`login_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '登录账号',
|
"user_name" varchar(30) COLLATE "pg_catalog"."default",
|
||||||
`user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户昵称',
|
"nick_name" varchar(30) COLLATE "pg_catalog"."default",
|
||||||
`user_type` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '00' COMMENT '用户类型(00系统用户 01注册用户)',
|
"user_type" varchar(2) COLLATE "pg_catalog"."default",
|
||||||
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户邮箱',
|
"email" varchar(50) COLLATE "pg_catalog"."default",
|
||||||
`phonenumber` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '手机号码',
|
"phone" varchar(11) COLLATE "pg_catalog"."default",
|
||||||
`sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
|
"sex" char(1) COLLATE "pg_catalog"."default",
|
||||||
`avatar` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '头像路径',
|
"avatar" varchar(100) COLLATE "pg_catalog"."default",
|
||||||
`password` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码',
|
"password" varchar(100) COLLATE "pg_catalog"."default",
|
||||||
`salt` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '盐加密',
|
"status" char(1) COLLATE "pg_catalog"."default",
|
||||||
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
|
"del_flag" char(1) COLLATE "pg_catalog"."default",
|
||||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
|
"login_ip" varchar(128) COLLATE "pg_catalog"."default",
|
||||||
`login_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '最后登录IP',
|
"login_date" timestamp(6),
|
||||||
`login_date` datetime NULL DEFAULT NULL COMMENT '最后登录时间',
|
"create_by" varchar(64) COLLATE "pg_catalog"."default",
|
||||||
`pwd_update_date` datetime NULL DEFAULT NULL COMMENT '密码最后更新时间',
|
"create_time" timestamp(6),
|
||||||
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
|
"update_by" varchar(64) COLLATE "pg_catalog"."default",
|
||||||
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
"update_time" timestamp(6),
|
||||||
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
|
"remark" varchar(500) COLLATE "pg_catalog"."default"
|
||||||
`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
|
COMMENT ON COLUMN "public"."sys_user"."user_id" IS '用户ID';
|
||||||
) ENGINE = InnoDB AUTO_INCREMENT = 1875732675218882561 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
|
COMMENT ON COLUMN "public"."sys_user"."dept_id" IS '部门ID';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."user_name" IS '用户账号';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."nick_name" IS '用户昵称';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."user_type" IS '用户类型(00系统用户)';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."email" IS '用户邮箱';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."phone" IS '手机号码';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."sex" IS '用户性别(0男 1女 2未知)';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."avatar" IS '头像地址';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."password" IS '密码';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."status" IS '帐号状态(0正常 1停用)';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."del_flag" IS '删除标志(0代表存在 2代表删除)';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."login_ip" IS '最后登录IP';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."login_date" IS '最后登录时间';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."create_by" IS '创建者';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."create_time" IS '创建时间';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."update_by" IS '更新者';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."update_time" IS '更新时间';
|
||||||
|
COMMENT ON COLUMN "public"."sys_user"."remark" IS '备注';
|
||||||
|
COMMENT ON TABLE "public"."sys_user" IS '用户信息表';
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Records of sys_user
|
-- Primary Key structure for table 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, '管理员');
|
ALTER TABLE "public"."sys_user" ADD CONSTRAINT "sys_user_pkey" PRIMARY KEY ("user_id");
|
||||||
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;
|
|
||||||
|
|||||||
@ -0,0 +1,81 @@
|
|||||||
|
package app.controller
|
||||||
|
|
||||||
|
import app.config.InjectConfig
|
||||||
|
import app.domain.account.LoginDTO
|
||||||
|
import app.verticle.MainVerticle
|
||||||
|
import io.vertx.core.Vertx
|
||||||
|
import io.vertx.core.json.JsonObject
|
||||||
|
import io.vertx.ext.web.client.WebClient
|
||||||
|
import io.vertx.junit5.VertxExtension
|
||||||
|
import io.vertx.junit5.VertxTestContext
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.aikrai.vertx.config.Config
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuthControllerTest
|
||||||
|
*/
|
||||||
|
@ExtendWith(VertxExtension::class)
|
||||||
|
class AuthControllerTest{
|
||||||
|
private var port = 8080
|
||||||
|
private var basePath = "/api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test case for doSign
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun doSign(vertx: Vertx, testContext: VertxTestContext) {
|
||||||
|
val client = WebClient.create(vertx)
|
||||||
|
val loginDTO = LoginDTO("运若汐", "123456")
|
||||||
|
client.post(port, "127.0.0.1", "$basePath/auth/doSign")
|
||||||
|
.sendJson(loginDTO)
|
||||||
|
.onSuccess { response ->
|
||||||
|
val body = JsonObject(response.body())
|
||||||
|
assertEquals("Success", body.getString("message"))
|
||||||
|
testContext.completeNow()
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
testContext.failNow(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test case for doLogin
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun doLogin(vertx: Vertx, testContext: VertxTestContext) {
|
||||||
|
val client = WebClient.create(vertx)
|
||||||
|
val loginDTO = LoginDTO("运若汐", "123456")
|
||||||
|
client.post(port, "127.0.0.1", "$basePath/auth/doLogin")
|
||||||
|
.sendJson(loginDTO)
|
||||||
|
.onSuccess { response ->
|
||||||
|
val body = JsonObject(response.body())
|
||||||
|
assertEquals("Success", body.getString("message"))
|
||||||
|
testContext.completeNow()
|
||||||
|
}
|
||||||
|
.onFailure { error ->
|
||||||
|
testContext.failNow(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun startServer(vertx: Vertx, testContext: VertxTestContext) {
|
||||||
|
runBlocking { Config.init(vertx) }
|
||||||
|
val getIt = InjectConfig.configure(vertx)
|
||||||
|
val mainVerticle = getIt.getInstance(MainVerticle::class.java)
|
||||||
|
vertx.deployVerticle(mainVerticle).onComplete { ar ->
|
||||||
|
if (ar.succeeded()) {
|
||||||
|
Config.getKey("server.port")?.let {
|
||||||
|
port = it.toString().toInt()
|
||||||
|
}
|
||||||
|
Config.getKey("server.context")?.let {
|
||||||
|
basePath = "/$it".replace("//", "/")
|
||||||
|
}
|
||||||
|
vertx.setTimer(5000) { testContext.completeNow() }
|
||||||
|
} else testContext.failNow(ar.cause())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
vertx-demo/src/test/resources/application-apifox.yaml
Normal file
4
vertx-demo/src/test/resources/application-apifox.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
apifox:
|
||||||
|
token: APS-xxxxxxxxxxxxxxxxxxxx
|
||||||
|
projectId: xxxxx
|
||||||
|
folderId: xxxxx
|
||||||
14
vertx-demo/src/test/resources/application-database.yaml
Normal file
14
vertx-demo/src/test/resources/application-database.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
databases:
|
||||||
|
name: vertx-demo
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 5432
|
||||||
|
username: root
|
||||||
|
password: 123456
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 6379
|
||||||
|
database: 0
|
||||||
|
password: xxx
|
||||||
|
maxPoolSize: 8
|
||||||
|
maxPoolWaiting: 2000
|
||||||
5
vertx-demo/src/test/resources/application.yaml
Normal file
5
vertx-demo/src/test/resources/application.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
package: app
|
||||||
|
|
||||||
|
|
||||||
@ -55,21 +55,19 @@ spotless {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20")
|
||||||
implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-core:$vertxVersion")
|
implementation("io.vertx:vertx-core:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-web:$vertxVersion")
|
implementation("io.vertx:vertx-web:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-config:$vertxVersion")
|
implementation("io.vertx:vertx-config:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-config-yaml:$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-sql-client-templates:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-auth-jwt:$vertxVersion")
|
implementation("io.vertx:vertx-auth-jwt:$vertxVersion")
|
||||||
|
|
||||||
implementation("com.google.inject:guice:7.0.0")
|
implementation("com.google.inject:guice:7.0.0")
|
||||||
implementation("org.reflections:reflections:0.9.12")
|
implementation("org.reflections:reflections:0.10.2")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
|
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.module:jackson-module-kotlin:2.15.2")
|
||||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2")
|
|
||||||
|
|
||||||
// hutool
|
// hutool
|
||||||
implementation("cn.hutool:hutool-core:5.8.35")
|
implementation("cn.hutool:hutool-core:5.8.35")
|
||||||
@ -80,10 +78,6 @@ dependencies {
|
|||||||
implementation("ch.qos.logback:logback-classic:1.4.14")
|
implementation("ch.qos.logback:logback-classic:1.4.14")
|
||||||
implementation("org.codehaus.janino:janino:3.1.8")
|
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
|
// doc
|
||||||
implementation("io.swagger.core.v3:swagger-core:2.2.27")
|
implementation("io.swagger.core.v3:swagger-core:2.2.27")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,75 @@
|
|||||||
package org.aikrai.vertx.auth
|
package org.aikrai.vertx.auth
|
||||||
|
|
||||||
|
import io.vertx.core.json.JsonObject
|
||||||
import io.vertx.ext.auth.impl.UserImpl
|
import io.vertx.ext.auth.impl.UserImpl
|
||||||
import org.aikrai.vertx.jackson.JsonUtil
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
|
||||||
class AuthUser(
|
class AuthUser(
|
||||||
principal: Principal,
|
val token: String,
|
||||||
attributes: Attributes,
|
val user: JsonObject,
|
||||||
) : UserImpl(JsonUtil.toJsonObject(principal), JsonUtil.toJsonObject(attributes))
|
val roles: Set<String>,
|
||||||
|
val accesses: Set<String>,
|
||||||
|
val loginIp: String? = null,
|
||||||
|
val client: String? = null,
|
||||||
|
) : UserImpl(JsonObject(), JsonObject()) {
|
||||||
|
|
||||||
class Principal(
|
companion object {
|
||||||
val id: Long,
|
fun AuthUser.isAdmin(): Boolean {
|
||||||
val info: Any,
|
return roles.contains("admin")
|
||||||
)
|
}
|
||||||
|
|
||||||
class Attributes(
|
fun AuthUser.validateAuth(permission: CheckPermission? = null) {
|
||||||
val role: Set<String>,
|
validateAuth(null, permission)
|
||||||
val permissions: Set<String>,
|
}
|
||||||
)
|
|
||||||
|
fun AuthUser.validateAuth(role: CheckRole? = null, permission: CheckPermission? = null) {
|
||||||
|
// 如果没有权限要求,直接返回
|
||||||
|
if (role == null && permission == null) return
|
||||||
|
|
||||||
|
// 验证角色
|
||||||
|
role?.let { r ->
|
||||||
|
val roleSet = roles.toSet()
|
||||||
|
if (roleSet.contains("admin")) return
|
||||||
|
if (roleSet.isEmpty()) {
|
||||||
|
throw Meta.forbidden("权限不足")
|
||||||
|
} else {
|
||||||
|
val reqRoleSet = (r.value + r.type).filter { it.isNotBlank() }.toSet()
|
||||||
|
if (!validateSet(reqRoleSet, roleSet, r.mode)) {
|
||||||
|
throw Meta.forbidden("权限不足")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证权限
|
||||||
|
permission?.let { p ->
|
||||||
|
val permissionSet = accesses.toSet()
|
||||||
|
val roleSet = roles.toSet()
|
||||||
|
if (roleSet.contains("admin")) return
|
||||||
|
if (permissionSet.isEmpty() && roleSet.isEmpty()) {
|
||||||
|
throw Meta.forbidden("权限不足")
|
||||||
|
} else {
|
||||||
|
if (p.orRole.isNotEmpty()) {
|
||||||
|
val roleBoolean = validateSet(p.orRole.toSet(), roleSet, Mode.AND)
|
||||||
|
if (roleBoolean) return
|
||||||
|
}
|
||||||
|
val reqPermissionSet = (p.value + p.type).filter { it.isNotBlank() }.toSet()
|
||||||
|
if (!validateSet(reqPermissionSet, permissionSet, p.mode)) {
|
||||||
|
throw Meta.forbidden("权限不足")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -31,7 +31,22 @@ object Config {
|
|||||||
configMap[key]
|
configMap[key]
|
||||||
} else {
|
} else {
|
||||||
// 找到所有以 key 开头的条目
|
// 找到所有以 key 开头的条目
|
||||||
configMap.filterKeys { it.startsWith(key) }
|
val map = configMap.filterKeys { it.startsWith(key) }
|
||||||
|
// 如果没有找到任何匹配的条目,返回 null
|
||||||
|
return map.ifEmpty { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getKeyAsString(key: String): String? {
|
||||||
|
if (retriever.get() == null) throw IllegalStateException("Config not initialized")
|
||||||
|
// 检查 configMap 中是否存在指定的 key
|
||||||
|
return if (configMap.containsKey(key)) {
|
||||||
|
configMap[key].toString()
|
||||||
|
} else {
|
||||||
|
// 找到所有以 key 开头的条目
|
||||||
|
val map = configMap.filterKeys { it.startsWith(key) }
|
||||||
|
// 如果没有找到任何匹配的条目,返回 null
|
||||||
|
if (map.isEmpty()) return null else map.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
package org.aikrai.vertx.config.resp
|
||||||
|
|
||||||
|
import io.vertx.core.http.HttpHeaders
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.constant.HttpStatus
|
||||||
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
|
||||||
|
class DefaultResponseHandler: ResponseHandlerInterface {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
override suspend fun normal(
|
||||||
|
ctx: RoutingContext,
|
||||||
|
responseData: Any?,
|
||||||
|
customizeResponse: Boolean
|
||||||
|
) {
|
||||||
|
val resStr = JsonUtil.toJsonStr(responseData)
|
||||||
|
ctx.put("responseData", resStr)
|
||||||
|
if (customizeResponse) return
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(HttpStatus.SUCCESS)
|
||||||
|
.putHeader("Content-Type", "application/json")
|
||||||
|
.end(resStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun exception(ctx: RoutingContext, e: Exception) {
|
||||||
|
logger.error { "${ctx.request().uri()}: ${ctx.failure().stackTraceToString()}" }
|
||||||
|
val failure = ctx.failure()
|
||||||
|
if (failure == null) {
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(ctx.statusCode())
|
||||||
|
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||||
|
.end()
|
||||||
|
} else {
|
||||||
|
val resStr = JsonUtil.toJsonStr(failure)
|
||||||
|
ctx.put("responseData", resStr)
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(ctx.statusCode())
|
||||||
|
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||||
|
.end(resStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package org.aikrai.vertx.config.resp
|
||||||
|
|
||||||
|
import io.vertx.ext.web.RoutingContext
|
||||||
|
|
||||||
|
interface ResponseHandlerInterface {
|
||||||
|
suspend fun normal(ctx: RoutingContext, responseData: Any?, customizeResponse: Boolean = false)
|
||||||
|
suspend fun exception(ctx: RoutingContext, e: Exception)
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package org.aikrai.vertx.constant
|
||||||
|
|
||||||
|
object CacheConstants {
|
||||||
|
/**
|
||||||
|
* 搜索历史key
|
||||||
|
*/
|
||||||
|
const val SEARCH_CONFIG: String = "search_config:"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录用户 redis key
|
||||||
|
*/
|
||||||
|
const val LOGIN_TOKEN_KEY: String = "login_tokens:"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码 redis key
|
||||||
|
*/
|
||||||
|
const val CAPTCHA_CODE_KEY: String = "captcha_codes:"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数管理 cache key
|
||||||
|
*/
|
||||||
|
const val SYS_CONFIG_KEY: String = "sys_config:"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典管理 cache key
|
||||||
|
*/
|
||||||
|
const val SYS_DICT_KEY: String = "sys_dict:"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防重提交 redis key
|
||||||
|
*/
|
||||||
|
const val REPEAT_SUBMIT_KEY: String = "repeat_submit:"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流 redis key
|
||||||
|
*/
|
||||||
|
const val RATE_LIMIT_KEY: String = "rate_limit:"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录账户密码错误次数 redis key
|
||||||
|
*/
|
||||||
|
const val PWD_ERR_CNT_KEY: String = "pwd_err_cnt:"
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package org.aikrai.vertx.constant
|
||||||
|
|
||||||
|
object Constants {
|
||||||
|
// 令牌前缀
|
||||||
|
val LOGIN_USER_KEY = "login_user_key"
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
package org.aikrai.vertx.constant
|
||||||
|
|
||||||
|
object HttpStatus {
|
||||||
|
/**
|
||||||
|
* 操作成功
|
||||||
|
*/
|
||||||
|
const val SUCCESS: Int = 200
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象创建成功
|
||||||
|
*/
|
||||||
|
const val CREATED: Int = 201
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求已经被接受
|
||||||
|
*/
|
||||||
|
const val ACCEPTED: Int = 202
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作已经执行成功,但是没有返回数据
|
||||||
|
*/
|
||||||
|
const val NO_CONTENT: Int = 204
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源已被移除
|
||||||
|
*/
|
||||||
|
const val MOVED_PERM: Int = 301
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重定向
|
||||||
|
*/
|
||||||
|
const val SEE_OTHER: Int = 303
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源没有被修改
|
||||||
|
*/
|
||||||
|
const val NOT_MODIFIED: Int = 304
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数列表错误(缺少,格式不匹配)
|
||||||
|
*/
|
||||||
|
const val BAD_REQUEST: Int = 400
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未授权
|
||||||
|
*/
|
||||||
|
const val UNAUTHORIZED: Int = 401
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问受限,授权过期
|
||||||
|
*/
|
||||||
|
const val FORBIDDEN: Int = 403
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源,服务未找到
|
||||||
|
*/
|
||||||
|
const val NOT_FOUND: Int = 404
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不允许的http方法
|
||||||
|
*/
|
||||||
|
const val BAD_METHOD: Int = 405
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源冲突,或者资源被锁
|
||||||
|
*/
|
||||||
|
const val CONFLICT: Int = 409
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不支持的数据,媒体类型
|
||||||
|
*/
|
||||||
|
const val UNSUPPORTED_TYPE: Int = 415
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统内部错误
|
||||||
|
*/
|
||||||
|
const val ERROR: Int = 500
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口未实现
|
||||||
|
*/
|
||||||
|
const val NOT_IMPLEMENTED: Int = 501
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统警告消息
|
||||||
|
*/
|
||||||
|
const val WARN: Int = 601
|
||||||
|
}
|
||||||
@ -9,6 +9,8 @@ import io.vertx.ext.web.RoutingContext
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.aikrai.vertx.auth.*
|
import org.aikrai.vertx.auth.*
|
||||||
|
import org.aikrai.vertx.config.resp.DefaultResponseHandler
|
||||||
|
import org.aikrai.vertx.config.resp.ResponseHandlerInterface
|
||||||
import org.aikrai.vertx.utlis.ClassUtil
|
import org.aikrai.vertx.utlis.ClassUtil
|
||||||
import org.aikrai.vertx.utlis.Meta
|
import org.aikrai.vertx.utlis.Meta
|
||||||
import org.reflections.Reflections
|
import org.reflections.Reflections
|
||||||
@ -23,7 +25,9 @@ import kotlin.reflect.jvm.javaType
|
|||||||
|
|
||||||
class RouterBuilder(
|
class RouterBuilder(
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val router: Router
|
private val router: Router,
|
||||||
|
private val scanPath: String? = null,
|
||||||
|
private val responseHandler: ResponseHandlerInterface = DefaultResponseHandler()
|
||||||
) {
|
) {
|
||||||
var anonymousPaths = ArrayList<String>()
|
var anonymousPaths = ArrayList<String>()
|
||||||
|
|
||||||
@ -31,22 +35,18 @@ class RouterBuilder(
|
|||||||
// 缓存路由信息
|
// 缓存路由信息
|
||||||
val routeInfoCache = mutableMapOf<Pair<String, HttpMethod>, RouteInfo>()
|
val routeInfoCache = mutableMapOf<Pair<String, HttpMethod>, RouteInfo>()
|
||||||
// 获取所有 Controller 类中的公共方法
|
// 获取所有 Controller 类中的公共方法
|
||||||
val packagePath = ClassUtil.getMainClass()?.packageName
|
val packagePath = scanPath ?: ClassUtil.getMainClass().packageName
|
||||||
val controllerClassSet = Reflections(packagePath).getTypesAnnotatedWith(Controller::class.java)
|
val controllerClassSet = Reflections(packagePath).getTypesAnnotatedWith(Controller::class.java)
|
||||||
val controllerMethods = ClassUtil.getPublicMethods(controllerClassSet)
|
val controllerMethods = ClassUtil.getPublicMethods(controllerClassSet)
|
||||||
for ((classType, methods) in controllerMethods) {
|
for ((classType, methods) in controllerMethods) {
|
||||||
val controllerAnnotation = classType.getDeclaredAnnotationsByType(Controller::class.java).firstOrNull()
|
val controllerAnnotation = classType.getDeclaredAnnotationsByType(Controller::class.java).firstOrNull()
|
||||||
val prefixPath = controllerAnnotation?.prefix ?: ""
|
val prefixPath = controllerAnnotation?.prefix ?: ""
|
||||||
val classAllowAnonymous = classType.getAnnotation(AllowAnonymous::class.java) != null
|
val classAllowAnonymous = classType.getAnnotation(AllowAnonymous::class.java) != null
|
||||||
if (classAllowAnonymous) {
|
|
||||||
val classPath = getReqPath(prefixPath, classType)
|
|
||||||
anonymousPaths.add("$classPath/**".replace("//", "/"))
|
|
||||||
}
|
|
||||||
for (method in methods) {
|
for (method in methods) {
|
||||||
val reqPath = getReqPath(prefixPath, classType, method)
|
val reqPath = getReqPath(prefixPath, classType, method)
|
||||||
val httpMethod = getHttpMethod(method)
|
val httpMethod = getHttpMethod(method)
|
||||||
val allowAnonymous = method.getAnnotation(AllowAnonymous::class.java) != null
|
val allowAnonymous = method.getAnnotation(AllowAnonymous::class.java) != null
|
||||||
if (allowAnonymous) anonymousPaths.add(reqPath)
|
if (classAllowAnonymous || allowAnonymous) anonymousPaths.add(reqPath)
|
||||||
val customizeResp = method.getAnnotation(CustomizeResponse::class.java) != null
|
val customizeResp = method.getAnnotation(CustomizeResponse::class.java) != null
|
||||||
val role = method.getAnnotation(CheckRole::class.java)
|
val role = method.getAnnotation(CheckRole::class.java)
|
||||||
val permissions = method.getAnnotation(CheckPermission::class.java)
|
val permissions = method.getAnnotation(CheckPermission::class.java)
|
||||||
@ -72,6 +72,7 @@ class RouterBuilder(
|
|||||||
isNullable = parameter.type.isMarkedNullable,
|
isNullable = parameter.type.isMarkedNullable,
|
||||||
isList = parameter.type.classifier == List::class,
|
isList = parameter.type.classifier == List::class,
|
||||||
isComplex = !parameter.type.classifier.toString().startsWith("class kotlin.") &&
|
isComplex = !parameter.type.classifier.toString().startsWith("class kotlin.") &&
|
||||||
|
!parameter.type.classifier.toString().startsWith("class io.vertx") &&
|
||||||
!parameter.type.javaType.javaClass.isEnum &&
|
!parameter.type.javaType.javaClass.isEnum &&
|
||||||
parameter.type.javaType is Class<*>
|
parameter.type.javaType is Class<*>
|
||||||
)
|
)
|
||||||
@ -103,18 +104,14 @@ class RouterBuilder(
|
|||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
try {
|
try {
|
||||||
val params = getParamsInstance(ctx, routeInfo.parameterInfo)
|
val params = getParamsInstance(ctx, routeInfo.parameterInfo)
|
||||||
val result = if (routeInfo.kFunction.isSuspend) {
|
val resObj = if (routeInfo.kFunction.isSuspend) {
|
||||||
routeInfo.kFunction.callSuspend(instance, *params)
|
routeInfo.kFunction.callSuspend(instance, *params)
|
||||||
} else {
|
} else {
|
||||||
routeInfo.kFunction.call(instance, *params)
|
routeInfo.kFunction.call(instance, *params)
|
||||||
}
|
}
|
||||||
val json = serializeToJson(result)
|
responseHandler.normal(ctx, resObj, routeInfo.customizeResp)
|
||||||
if (routeInfo.customizeResp) return@launch
|
|
||||||
ctx.response()
|
|
||||||
.putHeader("Content-Type", "application/json")
|
|
||||||
.end(json)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
handleError(ctx, e)
|
responseHandler.exception(ctx, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,13 +173,14 @@ class RouterBuilder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getReqPath(prefix: String, clazz: Class<*>, method: Method): String {
|
private fun getReqPath(prefix: String, clazz: Class<*>, method: Method): String {
|
||||||
val basePath = if (prefix.isNotBlank()) {
|
var classPath = if (prefix.isNotBlank()) {
|
||||||
StrUtil.toCamelCase(StrUtil.toUnderlineCase(prefix))
|
StrUtil.toCamelCase(StrUtil.toUnderlineCase(prefix))
|
||||||
} else {
|
} else {
|
||||||
StrUtil.toCamelCase(StrUtil.toUnderlineCase(clazz.simpleName.removeSuffix("Controller")))
|
StrUtil.toCamelCase(StrUtil.toUnderlineCase(clazz.simpleName.removeSuffix("Controller")))
|
||||||
}
|
}
|
||||||
|
if (classPath == "/") classPath = ""
|
||||||
val methodName = StrUtil.toCamelCase(StrUtil.toUnderlineCase(method.name))
|
val methodName = StrUtil.toCamelCase(StrUtil.toUnderlineCase(method.name))
|
||||||
return "/$basePath/$methodName".replace("//", "/")
|
return "/$classPath/$methodName".replace("//", "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getParamsInstance(ctx: RoutingContext, paramsInfo: List<ParameterInfo>): Array<Any?> {
|
private fun getParamsInstance(ctx: RoutingContext, paramsInfo: List<ParameterInfo>): Array<Any?> {
|
||||||
@ -300,27 +298,6 @@ class RouterBuilder(
|
|||||||
private fun serializeToJson(obj: Any?): String {
|
private fun serializeToJson(obj: Any?): String {
|
||||||
return objectMapper.writeValueAsString(obj)
|
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(
|
private data class RouteInfo(
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,8 +8,8 @@ interface QueryWrapper<T> {
|
|||||||
|
|
||||||
fun eq(column: String, value: Any): QueryWrapper<T>
|
fun eq(column: String, value: Any): QueryWrapper<T>
|
||||||
fun eq(column: KProperty1<T, *>, 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 = true, column: String, value: Any?): QueryWrapper<T>
|
||||||
fun eq(condition: Boolean, column: KProperty1<T, *>, value: Any): QueryWrapper<T>
|
fun eq(condition: Boolean = true, column: KProperty1<T, *>, value: Any?): QueryWrapper<T>
|
||||||
|
|
||||||
fun from(table: String): QueryWrapper<T>
|
fun from(table: String): QueryWrapper<T>
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ interface QueryWrapper<T> {
|
|||||||
fun orderByAsc(vararg columns: KProperty1<T, *>): QueryWrapper<T>
|
fun orderByAsc(vararg columns: KProperty1<T, *>): QueryWrapper<T>
|
||||||
fun orderByDesc(vararg columns: KProperty1<T, *>): QueryWrapper<T>
|
fun orderByDesc(vararg columns: KProperty1<T, *>): QueryWrapper<T>
|
||||||
|
|
||||||
fun genSql(): String
|
fun genSql(): Pair<String, Map<String, String>>
|
||||||
suspend fun getList(): List<T>
|
suspend fun getList(): List<T>
|
||||||
suspend fun getOne(): T?
|
suspend fun getOne(): T?
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,17 +5,40 @@ import io.vertx.kotlin.coroutines.coAwait
|
|||||||
import io.vertx.sqlclient.Row
|
import io.vertx.sqlclient.Row
|
||||||
import io.vertx.sqlclient.SqlClient
|
import io.vertx.sqlclient.SqlClient
|
||||||
import io.vertx.sqlclient.templates.SqlTemplate
|
import io.vertx.sqlclient.templates.SqlTemplate
|
||||||
import jakarta.persistence.Column
|
import mu.KotlinLogging
|
||||||
import jakarta.persistence.Table
|
import org.aikrai.vertx.db.annotation.TableField
|
||||||
|
import org.aikrai.vertx.db.annotation.TableName
|
||||||
import org.aikrai.vertx.jackson.JsonUtil
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import kotlin.reflect.KProperty1
|
import kotlin.reflect.KProperty1
|
||||||
import kotlin.reflect.jvm.javaField
|
|
||||||
|
|
||||||
class QueryWrapperImpl<T : Any>(
|
class QueryWrapperImpl<T : Any>(
|
||||||
private val entityClass: Class<T>,
|
private val clazz: Class<T>
|
||||||
private val sqlClient: SqlClient,
|
|
||||||
) : QueryWrapper<T> {
|
) : QueryWrapper<T> {
|
||||||
private val conditions = mutableListOf<QueryCondition>()
|
var sqlClient: SqlClient? = null
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
private val conditions = CopyOnWriteArrayList<QueryCondition>()
|
||||||
|
private val sqlMap = ConcurrentHashMap<String, String>()
|
||||||
|
|
||||||
|
private val fields: List<Field> = clazz.declaredFields.filter {
|
||||||
|
!it.isAnnotationPresent(Transient::class.java) &&
|
||||||
|
!Modifier.isStatic(it.modifiers) &&
|
||||||
|
!it.isSynthetic
|
||||||
|
}.onEach { it.isAccessible = true }
|
||||||
|
|
||||||
|
private val fieldMappings: Map<String, String> = fields.associate { field ->
|
||||||
|
val fieldAnnotation = field.getAnnotation(TableField::class.java)
|
||||||
|
val fieldName = fieldAnnotation?.value?.takeIf { it.isNotBlank() }
|
||||||
|
?: StrUtil.toUnderlineCase(field.name)
|
||||||
|
field.name to fieldName
|
||||||
|
}
|
||||||
|
|
||||||
|
private val tableName: String = clazz.getAnnotation(TableName::class.java)?.value?.takeIf { it.isNotBlank() }
|
||||||
|
?: StrUtil.toUnderlineCase(clazz.simpleName)
|
||||||
|
|
||||||
override fun select(vararg columns: String): QueryWrapper<T> {
|
override fun select(vararg columns: String): QueryWrapper<T> {
|
||||||
conditions.add(
|
conditions.add(
|
||||||
@ -29,12 +52,10 @@ class QueryWrapperImpl<T : Any>(
|
|||||||
|
|
||||||
override fun select(vararg columns: KProperty1<T, *>): QueryWrapper<T> {
|
override fun select(vararg columns: KProperty1<T, *>): QueryWrapper<T> {
|
||||||
columns.forEach {
|
columns.forEach {
|
||||||
val columnName = it.javaField?.getAnnotation(Column::class.java)?.name?.takeIf { it.isNotBlank() }
|
|
||||||
?: StrUtil.toUnderlineCase(it.name)
|
|
||||||
conditions.add(
|
conditions.add(
|
||||||
QueryCondition(
|
QueryCondition(
|
||||||
type = QueryType.SELECT,
|
type = QueryType.SELECT,
|
||||||
column = columnName
|
column = fieldMappings[it.name] ?: it.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -49,8 +70,8 @@ class QueryWrapperImpl<T : Any>(
|
|||||||
return eq(true, column, value)
|
return eq(true, column, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun eq(condition: Boolean, column: String, value: Any): QueryWrapper<T> {
|
override fun eq(condition: Boolean, column: String, value: Any?): QueryWrapper<T> {
|
||||||
if (condition) {
|
if (condition && value != null && value.toString().isNotBlank()) {
|
||||||
conditions.add(
|
conditions.add(
|
||||||
QueryCondition(
|
QueryCondition(
|
||||||
type = QueryType.WHERE,
|
type = QueryType.WHERE,
|
||||||
@ -63,14 +84,12 @@ class QueryWrapperImpl<T : Any>(
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun eq(condition: Boolean, column: KProperty1<T, *>, value: Any): QueryWrapper<T> {
|
override fun eq(condition: Boolean, column: KProperty1<T, *>, value: Any?): QueryWrapper<T> {
|
||||||
if (condition) {
|
if (condition && value != null && value.toString().isNotBlank()) {
|
||||||
val columnName = column.javaField?.getAnnotation(Column::class.java)?.name?.takeIf { it.isNotBlank() }
|
|
||||||
?: StrUtil.toUnderlineCase(column.name)
|
|
||||||
conditions.add(
|
conditions.add(
|
||||||
QueryCondition(
|
QueryCondition(
|
||||||
type = QueryType.WHERE,
|
type = QueryType.WHERE,
|
||||||
column = columnName,
|
column = fieldMappings[column.name] ?: column.name,
|
||||||
operator = "=",
|
operator = "=",
|
||||||
value = value
|
value = value
|
||||||
)
|
)
|
||||||
@ -197,90 +216,135 @@ class QueryWrapperImpl<T : Any>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildSql(): String {
|
private fun buildSql(): String {
|
||||||
val sqlBuilder = StringBuilder()
|
try {
|
||||||
|
val sqlBuilder = StringBuilder()
|
||||||
|
// SELECT 子句
|
||||||
|
sqlBuilder.append("SELECT ")
|
||||||
|
val selectCondition = conditions.find { it.type == QueryType.SELECT }
|
||||||
|
if (selectCondition != null) {
|
||||||
|
sqlBuilder.append(selectCondition.column)
|
||||||
|
} else {
|
||||||
|
fieldMappings.values.joinToString(",").let {
|
||||||
|
sqlBuilder.append(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SELECT 子句
|
// FROM 子句
|
||||||
sqlBuilder.append("SELECT ")
|
val from = conditions.filter { it.type == QueryType.FROM }
|
||||||
val selectCondition = conditions.find { it.type == QueryType.SELECT }
|
if (from.isNotEmpty()) {
|
||||||
if (selectCondition != null) {
|
sqlBuilder.append(" FROM ${from.first().column}")
|
||||||
sqlBuilder.append(selectCondition.column)
|
} else {
|
||||||
} else {
|
sqlBuilder.append(" FROM $tableName")
|
||||||
sqlBuilder.append("*")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// FROM 子句
|
// WHERE 子句
|
||||||
val from = conditions.filter { it.type == QueryType.FROM }
|
val whereConditions = conditions.filter { it.type == QueryType.WHERE }
|
||||||
if (from.isNotEmpty()) {
|
if (whereConditions.isNotEmpty()) {
|
||||||
sqlBuilder.append(" FROM ${from.first().column}")
|
sqlBuilder.append(" WHERE ")
|
||||||
} else {
|
sqlBuilder.append(
|
||||||
entityClass.getAnnotation(Table::class.java)?.name?.let {
|
whereConditions.joinToString(" AND ") {
|
||||||
sqlBuilder.append(" FROM $it")
|
"${it.column} ${it.operator} #{${it.column}}"
|
||||||
} ?: 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 子句
|
// GROUP BY 子句
|
||||||
conditions.find { it.type == QueryType.GROUP_BY }?.let {
|
conditions.find { it.type == QueryType.GROUP_BY }?.let {
|
||||||
sqlBuilder.append(" GROUP BY ${it.column}")
|
sqlBuilder.append(" GROUP BY ${it.column}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// HAVING 子句
|
// HAVING 子句
|
||||||
conditions.find { it.type == QueryType.HAVING }?.let {
|
conditions.find { it.type == QueryType.HAVING }?.let {
|
||||||
sqlBuilder.append(" HAVING ${it.column}")
|
sqlBuilder.append(" HAVING ${it.column}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ORDER BY 子句
|
// ORDER BY 子句
|
||||||
val orderByConditions = conditions.filter { it.type == QueryType.ORDER_BY }
|
val orderByConditions = conditions.filter { it.type == QueryType.ORDER_BY }
|
||||||
if (orderByConditions.isNotEmpty()) {
|
if (orderByConditions.isNotEmpty()) {
|
||||||
sqlBuilder.append(" ORDER BY ")
|
sqlBuilder.append(" ORDER BY ")
|
||||||
sqlBuilder.append(
|
sqlBuilder.append(
|
||||||
orderByConditions.joinToString(", ") {
|
orderByConditions.joinToString(", ") {
|
||||||
"${it.column} ${it.additional["direction"]}"
|
"${it.column} ${it.additional["direction"]}"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
return sqlBuilder.toString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message + "SQL 构建失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sqlBuilder.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun genSql(): String {
|
private fun buildParams(): Map<String, String> {
|
||||||
return buildSql()
|
val params = mutableMapOf<String, String>()
|
||||||
|
conditions.filter { it.type == QueryType.WHERE }.forEach {
|
||||||
|
when (it.operator) {
|
||||||
|
"IN", "NOT IN" -> {
|
||||||
|
params[it.column] = "(${(it.value as Collection<*>).joinToString(",")})"
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
params[it.column] = it.value.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun genSql(): Pair<String, Map<String, String>> {
|
||||||
|
return (buildSql() to buildParams()).also { conditions.clear() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getList(): List<T> {
|
override suspend fun getList(): List<T> {
|
||||||
val sql = buildSql()
|
if (sqlClient == null) {
|
||||||
val objs = SqlTemplate
|
throw Meta.repository("SqlClientError", "SqlClient 未初始化")
|
||||||
.forQuery(sqlClient, sql)
|
}
|
||||||
.mapTo(Row::toJson)
|
try {
|
||||||
.execute(emptyMap())
|
val cacheKey = generateCacheKey(tableName, conditions)
|
||||||
.coAwait()
|
val sql = sqlMap.getOrPut(cacheKey) {
|
||||||
.toList()
|
buildSql()
|
||||||
return objs.map { JsonUtil.parseObject(it.encode(), entityClass) }
|
}
|
||||||
|
val params = buildParams()
|
||||||
|
logger.debug { "SQL: $sql ,PARAMS: $params" }
|
||||||
|
val objs = SqlTemplate
|
||||||
|
.forQuery(sqlClient, sql)
|
||||||
|
.mapTo(Row::toJson)
|
||||||
|
.execute(params)
|
||||||
|
.coAwait()
|
||||||
|
.toList()
|
||||||
|
return objs.map { JsonUtil.parseObject(it.encode(), clazz) }.also { conditions.clear() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
conditions.clear()
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getOne(): T? {
|
override suspend fun getOne(): T? {
|
||||||
val sql = buildSql()
|
if (sqlClient == null) {
|
||||||
val obj = SqlTemplate
|
throw Meta.repository("SqlClientError", "SqlClient 未初始化")
|
||||||
.forQuery(sqlClient, sql)
|
}
|
||||||
.mapTo(Row::toJson)
|
try {
|
||||||
.execute(emptyMap())
|
val cacheKey = generateCacheKey(tableName, conditions)
|
||||||
.coAwait()
|
val sql = sqlMap.getOrPut(cacheKey) { buildSql() }
|
||||||
.firstOrNull()
|
val params = buildParams()
|
||||||
return obj?.let { JsonUtil.parseObject(it.encode(), entityClass) }
|
logger.debug { "SQL: $sql ,PARAMS: $params" }
|
||||||
|
val resultSet = SqlTemplate.forQuery(sqlClient, sql).execute(params).coAwait()
|
||||||
|
val list = resultSet.map { it.toJson() }
|
||||||
|
return when (list.size) {
|
||||||
|
0 -> null
|
||||||
|
1 -> JsonUtil.parseObject(list[0], clazz, true)
|
||||||
|
else -> throw IllegalStateException("Expected single result but got ${list.size}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
conditions.clear()
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateCacheKey(tableName: String, conditions: List<QueryCondition>): String {
|
||||||
|
val keyBuilder = StringBuilder(tableName)
|
||||||
|
conditions.forEach { condition ->
|
||||||
|
keyBuilder.append("|${condition.type}|${condition.column}|${condition.operator}")
|
||||||
|
}
|
||||||
|
return keyBuilder.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package org.aikrai.vertx.db
|
package org.aikrai.vertx.db
|
||||||
|
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
interface Repository<TId, TEntity> {
|
interface Repository<TId, TEntity> {
|
||||||
suspend fun create(t: TEntity): Int
|
suspend fun create(t: TEntity): Int
|
||||||
suspend fun delete(id: TId): Int
|
suspend fun delete(id: TId): Int
|
||||||
@ -7,10 +9,8 @@ interface Repository<TId, TEntity> {
|
|||||||
suspend fun update(id: TId, parameters: Map<String, Any?>): Int
|
suspend fun update(id: TId, parameters: Map<String, Any?>): Int
|
||||||
suspend fun get(id: TId): TEntity?
|
suspend fun get(id: TId): TEntity?
|
||||||
|
|
||||||
|
suspend fun getByField(field: String, value: Any): TEntity?
|
||||||
|
suspend fun getByField(field: KProperty1<TEntity, *>, value: Any): TEntity?
|
||||||
|
|
||||||
suspend fun createBatch(list: List<TEntity>): Int
|
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<*>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,158 +1,300 @@
|
|||||||
package org.aikrai.vertx.db
|
package org.aikrai.vertx.db
|
||||||
|
|
||||||
|
import cn.hutool.core.util.IdUtil
|
||||||
import cn.hutool.core.util.StrUtil
|
import cn.hutool.core.util.StrUtil
|
||||||
import com.fasterxml.jackson.core.type.TypeReference
|
import com.fasterxml.jackson.core.type.TypeReference
|
||||||
|
import com.fasterxml.jackson.databind.type.TypeFactory
|
||||||
import io.vertx.kotlin.coroutines.coAwait
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
import io.vertx.sqlclient.*
|
import io.vertx.sqlclient.*
|
||||||
import io.vertx.sqlclient.templates.SqlTemplate
|
import io.vertx.sqlclient.templates.SqlTemplate
|
||||||
import jakarta.persistence.Column
|
|
||||||
import jakarta.persistence.Id
|
|
||||||
import jakarta.persistence.Table
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import org.aikrai.vertx.db.annotation.*
|
||||||
import org.aikrai.vertx.db.tx.TxCtx
|
import org.aikrai.vertx.db.tx.TxCtx
|
||||||
import org.aikrai.vertx.jackson.JsonUtil
|
import org.aikrai.vertx.jackson.JsonUtil
|
||||||
|
import org.aikrai.vertx.utlis.Meta
|
||||||
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Modifier
|
import java.lang.reflect.Modifier
|
||||||
import java.lang.reflect.ParameterizedType
|
import java.lang.reflect.ParameterizedType
|
||||||
|
import java.lang.reflect.Type
|
||||||
import java.sql.Timestamp
|
import java.sql.Timestamp
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
open class RepositoryImpl<TId, TEntity : Any>(
|
open class RepositoryImpl<TId, TEntity : Any>(
|
||||||
private val sqlClient: SqlClient
|
private val sqlClient: SqlClient
|
||||||
) : Repository<TId, TEntity> {
|
) : Repository<TId, TEntity> {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
private val clazz: Class<TEntity> = (this::class.java.genericSuperclass as ParameterizedType)
|
private val clazz: Class<TEntity> = (this::class.java.genericSuperclass as ParameterizedType)
|
||||||
.actualTypeArguments[1] as Class<TEntity>
|
.actualTypeArguments[1] as Class<TEntity>
|
||||||
private val logger = KotlinLogging.logger {}
|
private val sqlMap = ConcurrentHashMap<String, ConcurrentHashMap<String, String>>()
|
||||||
private val sqlTemplateMap: Map<Pair<String, String>, String> = mutableMapOf()
|
private val querySqlMap = ConcurrentHashMap<String, Any>()
|
||||||
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)) {
|
private val fields: List<Field> = clazz.declaredFields.filter {
|
||||||
val list = SqlTemplate.forQuery(getConnection(), sql).execute(mapOf())
|
!Modifier.isStatic(it.modifiers) &&
|
||||||
.coAwait().map { it.toJson() }
|
!it.isSynthetic &&
|
||||||
val jsonObject = JsonUtil.toJsonObject(list)
|
!it.isAnnotationPresent(Transient::class.java)
|
||||||
val typeReference = object : TypeReference<R>() {}
|
}.onEach { it.isAccessible = true }
|
||||||
JsonUtil.parseObject(jsonObject, typeReference, true)
|
|
||||||
} else {
|
private val fieldMappings: Map<String, String> = fields.associate { field ->
|
||||||
val rowCount = SqlTemplate.forUpdate(getConnection(), sql).execute(mapOf())
|
val fieldAnnotation = field.getAnnotation(TableField::class.java)
|
||||||
.coAwait().rowCount()
|
val fieldName = fieldAnnotation?.value?.takeIf { it.isNotBlank() }
|
||||||
rowCount as R
|
?: StrUtil.toUnderlineCase(field.name)
|
||||||
}
|
field.name to fieldName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val idField: Field =
|
||||||
|
clazz.declaredFields.find { it.isAnnotationPresent(TableId::class.java) }?.also { it.isAccessible = true }
|
||||||
|
?: throw IllegalArgumentException("No @Id field found in ${clazz.simpleName}")
|
||||||
|
|
||||||
|
private val idFieldName: String = idField.getAnnotation(TableField::class.java)?.value?.takeIf { it.isNotBlank() }
|
||||||
|
?: StrUtil.toUnderlineCase(idField.name)
|
||||||
|
|
||||||
|
private val tableName: String = clazz.getAnnotation(TableName::class.java)?.value?.takeIf { it.isNotBlank() }
|
||||||
|
?: StrUtil.toUnderlineCase(clazz.simpleName)
|
||||||
|
|
||||||
override suspend fun create(t: TEntity): Int {
|
override suspend fun create(t: TEntity): Int {
|
||||||
val tableName = getTableName()
|
try {
|
||||||
val sqlTemplate = sqlTemplateMap[Pair(tableName, "create")] ?: run {
|
val idAnnotation = idField.getAnnotation(TableId::class.java)
|
||||||
val idColumnName = getIdColumnName()
|
val idValue = idField.get(t)
|
||||||
val columnsMap = getColumnMappings()
|
val excludeId = idAnnotation != null && (idValue == null || idValue == 0L || idValue == -1L) &&
|
||||||
// Exclude 'id' field if it's auto-generated
|
(idAnnotation.type == IdType.AUTO)
|
||||||
val fields = clazz.declaredFields.filter { it.name != idColumnName }
|
val sqlKey = if (excludeId) "createExcludeId" else "createIncludeId"
|
||||||
val columns = fields.map { columnsMap[it.name] }
|
|
||||||
val parameters = fields.map { it.name }
|
val sqlTemplate = getOrCreateSql(tableName, sqlKey) {
|
||||||
val sql =
|
val columns = if (excludeId) {
|
||||||
"INSERT INTO $tableName (${columns.joinToString(", ")}) VALUES (${parameters.joinToString(", ") { "#{$it}" }})"
|
fields.filter { it.name != idField.name }.map { fieldMappings[it.name] }
|
||||||
sqlTemplateMap.plus(Pair(tableName, "create")) to sql
|
} else {
|
||||||
sql
|
fields.map { fieldMappings[it.name] }
|
||||||
|
}.joinToString(", ")
|
||||||
|
val parameters = if (excludeId) {
|
||||||
|
fields.filter { it.name != idField.name }.joinToString(", ") { "#{" + it.name + "}" }
|
||||||
|
} else {
|
||||||
|
fields.joinToString(", ") { "#{" + it.name + "}" }
|
||||||
|
}
|
||||||
|
val returning = if (excludeId) " RETURNING $idFieldName" else ""
|
||||||
|
"INSERT INTO $tableName ($columns) VALUES ($parameters)$returning"
|
||||||
|
}
|
||||||
|
|
||||||
|
val params = getNonNullFields(t).let {
|
||||||
|
if (excludeId) it.filterKeys { key -> key != idField.name } else it
|
||||||
|
}.toMutableMap()
|
||||||
|
|
||||||
|
// 填充ID
|
||||||
|
when (idAnnotation.type) {
|
||||||
|
IdType.INPUT -> {
|
||||||
|
if (idValue == 0L || idValue == -1L) throw Meta.repository("CreateError", "must provide ID value")
|
||||||
|
}
|
||||||
|
IdType.ASSIGN_ID -> params[idField.name] = IdUtil.getSnowflakeNextId()
|
||||||
|
IdType.ASSIGN_UUID -> params[idField.name] = IdUtil.simpleUUID()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
// 处理TableField注解
|
||||||
|
fields.forEach { field ->
|
||||||
|
val tableField = field.getAnnotation(TableField::class.java)
|
||||||
|
if (tableField != null && tableField.fill != FieldFill.DEFAULT) {
|
||||||
|
// fill属性不为DEFAULT时,根据fill属性填充字段
|
||||||
|
val value = when (tableField.fill) {
|
||||||
|
FieldFill.INSERT, FieldFill.UPDATE, FieldFill.INSERT_UPDATE -> {
|
||||||
|
when (field.type) {
|
||||||
|
LocalDateTime::class.java -> LocalDateTime.now()
|
||||||
|
Timestamp::class.java -> OffsetDateTime.ofInstant(Instant.now(), ZoneId.systemDefault())
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (value != null) params[field.name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (excludeId) {
|
||||||
|
logger.debug { "SQL: $sqlTemplate ,PARAMS: $params" }
|
||||||
|
// 执行查询以获取生成的ID
|
||||||
|
val result = SqlTemplate.forQuery(getConnection(), sqlTemplate)
|
||||||
|
.execute(params)
|
||||||
|
.coAwait()
|
||||||
|
val rows = result.toList()
|
||||||
|
if (rows.isEmpty()) throw IllegalStateException("Insert failed")
|
||||||
|
// 提取生成的ID并回填
|
||||||
|
val generatedId = rows.first().getValue(idFieldName)
|
||||||
|
idField.set(t, generatedId)
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
execute(sqlTemplate, params)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Error creating entity: $t" }
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
}
|
}
|
||||||
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 {
|
override suspend fun delete(id: TId): Int {
|
||||||
val tableName = getTableName()
|
try {
|
||||||
val sqlTemplate = sqlTemplateMap[Pair(tableName, "delete")] ?: run {
|
val sqlKey = "delete"
|
||||||
val idColumnName = getIdColumnName()
|
val sqlTemplate = getOrCreateSql(tableName, sqlKey) {
|
||||||
val sql = "DELETE FROM $tableName WHERE $idColumnName = #{id}"
|
"DELETE FROM $tableName WHERE $idFieldName = #{id}"
|
||||||
sqlTemplateMap.plus(Pair(tableName, "delete")) to sql
|
}
|
||||||
sql
|
val params = mapOf("id" to id)
|
||||||
|
if (logger.isDebugEnabled) {
|
||||||
|
logger.debug { "SQL: $sqlTemplate, PARAMS: $params" }
|
||||||
|
}
|
||||||
|
return execute(sqlTemplate, params)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Error deleting entity with id: $id" }
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
}
|
}
|
||||||
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 {
|
override suspend fun update(t: TEntity): Int {
|
||||||
val tableName = getTableName()
|
try {
|
||||||
val sqlTemplate = sqlTemplateMap[Pair(tableName, "update")] ?: run {
|
val sqlKey = "update"
|
||||||
val idColumnName = getIdColumnName()
|
val sqlTemplate = getOrCreateSql(tableName, sqlKey) {
|
||||||
val columnsMap = getColumnMappings()
|
val fields = clazz.declaredFields.filter { it.name != idFieldName }
|
||||||
// Exclude 'id' from update fields
|
val setClause = fields.joinToString(", ") { "${fieldMappings[it.name]} = #{${it.name}}" }
|
||||||
val fields = clazz.declaredFields.filter { it.name != idColumnName }
|
"UPDATE $tableName SET $setClause WHERE $idFieldName = #{id}"
|
||||||
val setClause = fields.joinToString(", ") { "${columnsMap[it.name]} = #{${it.name}}" }
|
}
|
||||||
val sql = "UPDATE $tableName SET $setClause WHERE $idColumnName = #{id}"
|
val params = getNonNullFields(t) + mapOf("id" to idField.get(t))
|
||||||
sqlTemplateMap.plus(Pair(tableName, "update")) to sql
|
logger.debug { "SQL: $sqlTemplate, PARAMS: $params" }
|
||||||
sql
|
return execute(sqlTemplate, params)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Error updating entity: $t" }
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
}
|
}
|
||||||
// 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 {
|
override suspend fun update(id: TId, parameters: Map<String, Any?>): Int {
|
||||||
val tableName = getTableName()
|
try {
|
||||||
val sqlTemplate = sqlTemplateMap[Pair(tableName, "update")] ?: run {
|
val sqlKey = "update_$parameters"
|
||||||
val idColumnName = getIdColumnName()
|
val sqlTemplate = getOrCreateSql(tableName, sqlKey) {
|
||||||
val columnsMap = getColumnMappings()
|
val setClause = parameters.keys.joinToString(", ") { "${fieldMappings[it]} = #{$it}" }
|
||||||
val setClause = parameters.keys.joinToString(", ") { "${columnsMap[it]} = #{$it}" }
|
"UPDATE $tableName SET $setClause WHERE $idFieldName = #{id}"
|
||||||
val sql = "UPDATE $tableName SET $setClause WHERE $idColumnName = #{id}"
|
}
|
||||||
sqlTemplateMap.plus(Pair(tableName, "update")) to sql
|
val params = parameters + mapOf("id" to id)
|
||||||
sql
|
logger.debug { "SQL: $sqlTemplate, PARAMS: $params" }
|
||||||
|
return execute(sqlTemplate, params)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Error updating entity with id: $id" }
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
}
|
}
|
||||||
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? {
|
override suspend fun get(id: TId): TEntity? {
|
||||||
val tableName = getTableName()
|
try {
|
||||||
val sqlTemplate = sqlTemplateMap[Pair(tableName, "get")] ?: run {
|
val sqlKey = "get"
|
||||||
val idColumnName = getIdColumnName()
|
val sqlTemplate = getOrCreateSql(tableName, sqlKey) {
|
||||||
val columnsMap = getColumnMappings()
|
val columns = fieldMappings.values.joinToString(", ")
|
||||||
val columns = columnsMap.values.joinToString(", ")
|
"SELECT $columns FROM $tableName WHERE $idFieldName = #{id}"
|
||||||
val sql = "SELECT $columns FROM $tableName WHERE $idColumnName = #{id}"
|
}
|
||||||
(sqlTemplateMap as MutableMap)[Pair(tableName, "get")] = sql
|
return get(sqlTemplate, mapOf("id" to id), clazz)
|
||||||
sql
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Error getting entity with id: $id" }
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
}
|
}
|
||||||
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> {
|
override suspend fun getByField(field: String, value: Any): TEntity? {
|
||||||
return QueryWrapperImpl(clazz, getConnection())
|
try {
|
||||||
|
val sqlKey = "getByField_$field"
|
||||||
|
val sqlTemplate = getOrCreateSql(tableName, sqlKey) {
|
||||||
|
val columns = fieldMappings.values.joinToString(", ")
|
||||||
|
"SELECT $columns FROM $tableName WHERE $field = #{value}"
|
||||||
|
}
|
||||||
|
val params = mapOf("value" to value)
|
||||||
|
logger.debug { "SQL: $sqlTemplate, PARAMS: $params" }
|
||||||
|
return get(sqlTemplate, params, clazz)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Error getting entity by field: $field = $value" }
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun queryBuilder(clazz: Class<*>): QueryWrapper<*> {
|
override suspend fun getByField(field: KProperty1<TEntity, *>, value: Any): TEntity? {
|
||||||
return QueryWrapperImpl(clazz, getConnection())
|
try {
|
||||||
|
val sqlKey = "getByField_${field.name}"
|
||||||
|
val sql = getOrCreateSql(tableName, sqlKey) {
|
||||||
|
val columns = fieldMappings.values.joinToString(", ")
|
||||||
|
"SELECT $columns FROM $tableName WHERE ${fieldMappings[field.name]} = #{value}"
|
||||||
|
}
|
||||||
|
val params = mapOf("value" to value)
|
||||||
|
logger.debug { "SQL: $sql, PARAMS: $params" }
|
||||||
|
return get(sql, params, clazz)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Error getting entity by field: ${field.name} = $value" }
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun createBatch(list: List<TEntity>): Int {
|
||||||
|
try {
|
||||||
|
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
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Error creating batch entities: $list" }
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// base方法
|
||||||
|
suspend fun <R> get(sql: String, params: Map<String, Any?>, clazz: Class<*>): R? {
|
||||||
|
logger.debug { "SQL: $sql, PARAMS: $params" }
|
||||||
|
val resultSet = SqlTemplate.forQuery(getConnection(), sql).execute(params).coAwait()
|
||||||
|
val list = resultSet.map { it.toJson() }
|
||||||
|
return when (list.size) {
|
||||||
|
0 -> null
|
||||||
|
1 -> JsonUtil.parseObject(list[0], clazz, true) as R
|
||||||
|
else -> throw IllegalStateException("Expected single result but got ${list.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <R> find(sql: String, params: Map<String, Any?>, clazz: Class<*>): R? {
|
||||||
|
logger.debug { "SQL: $sql, PARAMS: $params" }
|
||||||
|
val resultSet = SqlTemplate.forQuery(getConnection(), sql).execute(params).coAwait()
|
||||||
|
val list = resultSet.map { it.toJson() }
|
||||||
|
val listType = TypeFactory.defaultInstance().constructCollectionType(List::class.java, clazz)
|
||||||
|
val listTypeReference = object : TypeReference<List<Any>>() {
|
||||||
|
override fun getType() = listType
|
||||||
|
}
|
||||||
|
return JsonUtil.parseArray(JsonUtil.toJsonArray(list), listTypeReference, true) as R
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun execute(sql: String, params: Map<String, Any?> = emptyMap()): Int {
|
||||||
|
logger.debug { "SQL: $sql ,PARAMS: $params" }
|
||||||
|
return try {
|
||||||
|
SqlTemplate.forUpdate(getConnection(), sql)
|
||||||
|
.execute(params)
|
||||||
|
.coAwait()
|
||||||
|
.rowCount()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "Error executing SQL: $sql, PARAMS: $params" }
|
||||||
|
throw Meta.repository(e.javaClass.simpleName, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun queryBuilder(qClazz: Class<Any>? = null): QueryWrapper<TEntity> {
|
||||||
|
val qClass = qClazz ?: clazz
|
||||||
|
val connection = getConnection()
|
||||||
|
val queryWrapper = querySqlMap.getOrPut(qClass.simpleName) {
|
||||||
|
QueryWrapperImpl(qClass)
|
||||||
|
} as QueryWrapperImpl<TEntity>
|
||||||
|
queryWrapper.sqlClient = connection
|
||||||
|
return queryWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他工具方法
|
||||||
private suspend fun getConnection(): SqlClient {
|
private suspend fun getConnection(): SqlClient {
|
||||||
return if (TxCtx.isTransactionActive(coroutineContext)) {
|
return if (TxCtx.isTransactionActive(coroutineContext)) {
|
||||||
TxCtx.currentSqlConnection(coroutineContext) ?: run {
|
TxCtx.currentSqlConnection(coroutineContext) ?: run {
|
||||||
@ -164,55 +306,16 @@ open class RepositoryImpl<TId, TEntity : Any>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其他工具方法
|
// 通用获取或创建 SQL 模板的方法
|
||||||
override suspend fun createBatch(list: List<TEntity>): Int {
|
private fun getOrCreateSql(tableName: String, key: String, sqlProvider: () -> String): String {
|
||||||
if (list.isEmpty()) return 0
|
val tableSqlMap = sqlMap.computeIfAbsent(tableName) { ConcurrentHashMap() }
|
||||||
var rowCount = 0
|
return tableSqlMap.getOrPut(key, sqlProvider)
|
||||||
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> {
|
private fun getNonNullFields(t: TEntity): Map<String, Any> {
|
||||||
return clazz.declaredFields
|
return fields.filter { !it.isAnnotationPresent(Transient::class.java) && it.get(t) != null }
|
||||||
.filter { field ->
|
.associate { it.name to it.get(t) }
|
||||||
field.isAccessible = true
|
|
||||||
// 排除被 @Transient 注解标记的字段
|
|
||||||
!field.isAnnotationPresent(Transient::class.java) &&
|
|
||||||
field.get(t) != null
|
|
||||||
}
|
|
||||||
.associate { field ->
|
|
||||||
field.name to field.get(t)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -223,19 +326,15 @@ open class RepositoryImpl<TId, TEntity : Any>(
|
|||||||
private fun <TEntity> genBatchInsertSql(objects: List<TEntity>): String {
|
private fun <TEntity> genBatchInsertSql(objects: List<TEntity>): String {
|
||||||
// 如果对象列表为空,直接返回空字符串
|
// 如果对象列表为空,直接返回空字符串
|
||||||
if (objects.isEmpty()) return ""
|
if (objects.isEmpty()) return ""
|
||||||
|
|
||||||
// 将类名转换为下划线命名的表名,例如:UserInfo -> user_info
|
// 将类名转换为下划线命名的表名,例如:UserInfo -> user_info
|
||||||
val tableName = StrUtil.toUnderlineCase(clazz.simpleName)
|
val tableName = StrUtil.toUnderlineCase(clazz.simpleName)
|
||||||
|
|
||||||
// 获取类的所有字段,包括私有字段
|
// 获取类的所有字段,包括私有字段
|
||||||
val fields = clazz.declaredFields.filter {
|
val fields = clazz.declaredFields.filter {
|
||||||
// 过滤掉静态字段和合成字段
|
// 过滤掉静态字段和合成字段
|
||||||
!Modifier.isStatic(it.modifiers) && !it.isSynthetic
|
!Modifier.isStatic(it.modifiers) && !it.isSynthetic
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保所有字段可访问
|
// 确保所有字段可访问
|
||||||
fields.forEach { it.isAccessible = true }
|
fields.forEach { it.isAccessible = true }
|
||||||
|
|
||||||
// 将字段名转换为下划线命名的列名,并用逗号隔开
|
// 将字段名转换为下划线命名的列名,并用逗号隔开
|
||||||
val columnNames = fields.joinToString(", ") { StrUtil.toUnderlineCase(it.name) }
|
val columnNames = fields.joinToString(", ") { StrUtil.toUnderlineCase(it.name) }
|
||||||
|
|
||||||
@ -248,18 +347,14 @@ open class RepositoryImpl<TId, TEntity : Any>(
|
|||||||
is String -> "'${escapeSql(value)}'" // 字符串类型,加单引号并进行转义
|
is String -> "'${escapeSql(value)}'" // 字符串类型,加单引号并进行转义
|
||||||
is Enum<*> -> "'${value.name}'" // 枚举类型,使用枚举名,添加单引号
|
is Enum<*> -> "'${value.name}'" // 枚举类型,使用枚举名,添加单引号
|
||||||
is Number, is Boolean -> value.toString() // 数字和布尔类型,直接转换为字符串
|
is Number, is Boolean -> value.toString() // 数字和布尔类型,直接转换为字符串
|
||||||
is Timestamp -> // 时间戳类型,格式化为指定的日期时间字符串
|
is Timestamp -> // 时间戳类型
|
||||||
"'${formatter.format(value.toInstant().atZone(ZoneId.of("Asia/Shanghai")))}'"
|
"'${OffsetDateTime.ofInstant(value.toInstant(), ZoneId.systemDefault())}'"
|
||||||
|
|
||||||
is Array<*> -> // 数组类型处理
|
is Array<*> -> // 数组类型处理
|
||||||
if (value.isEmpty()) "'{}'" else "'{${value.joinToString(",") { escapeSql(it?.toString() ?: "NULL") }}}'"
|
if (value.isEmpty()) "'{}'" else "'{${value.joinToString(",") { escapeSql(it?.toString() ?: "NULL") }}}'"
|
||||||
|
|
||||||
is Collection<*> -> // 集合类型处理
|
is Collection<*> -> // 集合类型处理
|
||||||
if (value.isEmpty()) "'{}'" else "'{${value.joinToString(",") { escapeSql(it?.toString() ?: "NULL") }}}'"
|
if (value.isEmpty()) "'{}'" else "'{${value.joinToString(",") { escapeSql(it?.toString() ?: "NULL") }}}'"
|
||||||
|
|
||||||
else -> "'${escapeSql(value.toString())}'" // 其他类型,调用 toString() 后转义并加单引号
|
else -> "'${escapeSql(value.toString())}'" // 其他类型,调用 toString() 后转义并加单引号
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建 VALUES 部分,每个对象对应一组值
|
// 构建 VALUES 部分,每个对象对应一组值
|
||||||
val valuesList = objects.map { instance ->
|
val valuesList = objects.map { instance ->
|
||||||
fields.joinToString(", ", "(", ")") { field ->
|
fields.joinToString(", ", "(", ")") { field ->
|
||||||
@ -269,4 +364,12 @@ open class RepositoryImpl<TId, TEntity : Any>(
|
|||||||
}
|
}
|
||||||
return "INSERT INTO $tableName ($columnNames) VALUES ${valuesList.joinToString(", ")};"
|
return "INSERT INTO $tableName ($columnNames) VALUES ${valuesList.joinToString(", ")};"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isCollectionType(type: Type): Boolean {
|
||||||
|
return when (type) {
|
||||||
|
is Class<*> -> type.isArray || Collection::class.java.isAssignableFrom(type)
|
||||||
|
is ParameterizedType -> Collection::class.java.isAssignableFrom((type.rawType as Class<*>))
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
package org.aikrai.vertx.db.annotation
|
||||||
|
|
||||||
|
@MustBeDocumented
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS)
|
||||||
|
annotation class TableName(
|
||||||
|
val value: String = "",
|
||||||
|
// val schema: String = "",
|
||||||
|
// val keepGlobalPrefix: Boolean = false,
|
||||||
|
// val resultMap: String = "",
|
||||||
|
// val autoResultMap: Boolean = false,
|
||||||
|
// val excludeProperty: Array<String> = []
|
||||||
|
)
|
||||||
|
|
||||||
|
@MustBeDocumented
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.FIELD, AnnotationTarget.ANNOTATION_CLASS)
|
||||||
|
annotation class TableId(val value: String = "", val type: IdType = IdType.NONE)
|
||||||
|
|
||||||
|
@MustBeDocumented
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.FIELD, AnnotationTarget.ANNOTATION_CLASS)
|
||||||
|
annotation class TableField(
|
||||||
|
val value: String = "",
|
||||||
|
// val exist: Boolean = true,
|
||||||
|
// val condition: String = "",
|
||||||
|
// val update: String = "",
|
||||||
|
val fill: FieldFill = FieldFill.DEFAULT,
|
||||||
|
// val select: Boolean = true,
|
||||||
|
// val keepGlobalFormat: Boolean = false,
|
||||||
|
// val property: String = "",
|
||||||
|
// val numericScale: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IdType
|
||||||
|
* @property key Int
|
||||||
|
* @constructor
|
||||||
|
* @property AUTO IdType 数据库ID自增
|
||||||
|
* @property NONE IdType 无状态
|
||||||
|
* @property INPUT IdType 手动输入ID
|
||||||
|
* @property ASSIGN_ID IdType 默认 全局唯一ID (数字类型)
|
||||||
|
* @property ASSIGN_UUID IdType 全局唯一ID (字符串类型)
|
||||||
|
*/
|
||||||
|
enum class IdType(val key: Int) {
|
||||||
|
AUTO(0),
|
||||||
|
NONE(1),
|
||||||
|
INPUT(2),
|
||||||
|
ASSIGN_ID(3),
|
||||||
|
ASSIGN_UUID(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class FieldFill {
|
||||||
|
DEFAULT,
|
||||||
|
INSERT,
|
||||||
|
UPDATE,
|
||||||
|
INSERT_UPDATE
|
||||||
|
}
|
||||||
@ -22,7 +22,7 @@ object TxMgrHolder {
|
|||||||
private val _txMgr = AtomicReference<TxMgr?>(null)
|
private val _txMgr = AtomicReference<TxMgr?>(null)
|
||||||
|
|
||||||
val txMgr: TxMgr
|
val txMgr: TxMgr
|
||||||
get() = _txMgr.get() ?: throw Meta.failure(
|
get() = _txMgr.get() ?: throw Meta.error(
|
||||||
"TransactionError",
|
"TransactionError",
|
||||||
"TxMgr(TransactionManager)尚未初始化,请先调用initTxMgr()"
|
"TxMgr(TransactionManager)尚未初始化,请先调用initTxMgr()"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package org.aikrai.vertx.jackson
|
|||||||
import com.fasterxml.jackson.databind.PropertyName
|
import com.fasterxml.jackson.databind.PropertyName
|
||||||
import com.fasterxml.jackson.databind.introspect.Annotated
|
import com.fasterxml.jackson.databind.introspect.Annotated
|
||||||
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector
|
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector
|
||||||
import jakarta.persistence.Column
|
import org.aikrai.vertx.db.annotation.TableField
|
||||||
|
|
||||||
class ColumnAnnotationIntrospector : JacksonAnnotationIntrospector() {
|
class ColumnAnnotationIntrospector : JacksonAnnotationIntrospector() {
|
||||||
override fun findNameForDeserialization(annotated: Annotated?): PropertyName? {
|
override fun findNameForDeserialization(annotated: Annotated?): PropertyName? {
|
||||||
@ -16,7 +16,7 @@ class ColumnAnnotationIntrospector : JacksonAnnotationIntrospector() {
|
|||||||
|
|
||||||
private fun getColumnName(annotated: Annotated?): PropertyName? {
|
private fun getColumnName(annotated: Annotated?): PropertyName? {
|
||||||
if (annotated == null) return null
|
if (annotated == null) return null
|
||||||
val column = annotated.getAnnotation(Column::class.java)
|
val column = annotated.getAnnotation(TableField::class.java)
|
||||||
return column?.let { PropertyName(it.name) }
|
return column?.let { PropertyName(it.value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair
|
|||||||
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator
|
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||||
import io.vertx.core.json.JsonArray
|
import io.vertx.core.json.JsonArray
|
||||||
import io.vertx.core.json.JsonObject
|
import io.vertx.core.json.JsonObject
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ object JsonUtil {
|
|||||||
private var objectMapperSnakeCase = createObjectMapperSnakeCase(false)
|
private var objectMapperSnakeCase = createObjectMapperSnakeCase(false)
|
||||||
|
|
||||||
private val objectMapperDeserialization = run {
|
private val objectMapperDeserialization = run {
|
||||||
val mapper: ObjectMapper = jacksonObjectMapper()
|
val mapper: ObjectMapper = jacksonObjectMapper().registerKotlinModule()
|
||||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
mapper.registerModule(JavaTimeModule())
|
mapper.registerModule(JavaTimeModule())
|
||||||
mapper.setAnnotationIntrospector(
|
mapper.setAnnotationIntrospector(
|
||||||
@ -34,7 +35,7 @@ object JsonUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val objectMapperSnakeCaseDeserialization = run {
|
private val objectMapperSnakeCaseDeserialization = run {
|
||||||
val mapper: ObjectMapper = jacksonObjectMapper()
|
val mapper: ObjectMapper = jacksonObjectMapper().registerKotlinModule()
|
||||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
mapper.propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
|
mapper.propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
|
||||||
mapper.registerModule(JavaTimeModule())
|
mapper.registerModule(JavaTimeModule())
|
||||||
@ -116,7 +117,7 @@ private class CustomTypeResolverBuilder : ObjectMapper.DefaultTypeResolverBuilde
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createObjectMapper(writeClassName: Boolean): ObjectMapper {
|
private fun createObjectMapper(writeClassName: Boolean): ObjectMapper {
|
||||||
val mapper: ObjectMapper = jacksonObjectMapper()
|
val mapper: ObjectMapper = jacksonObjectMapper().registerKotlinModule()
|
||||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
mapper.registerModule(JavaTimeModule())
|
mapper.registerModule(JavaTimeModule())
|
||||||
if (writeClassName) {
|
if (writeClassName) {
|
||||||
@ -130,7 +131,7 @@ private fun createObjectMapper(writeClassName: Boolean): ObjectMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createObjectMapperSnakeCase(writeClassName: Boolean): ObjectMapper {
|
private fun createObjectMapperSnakeCase(writeClassName: Boolean): ObjectMapper {
|
||||||
val mapper: ObjectMapper = jacksonObjectMapper()
|
val mapper: ObjectMapper = jacksonObjectMapper().registerKotlinModule()
|
||||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
mapper.propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
|
mapper.propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
|
||||||
mapper.registerModule(JavaTimeModule())
|
mapper.registerModule(JavaTimeModule())
|
||||||
|
|||||||
@ -194,10 +194,11 @@ class OpenApiSpecGenerator {
|
|||||||
* @return 格式化后的 API 路径
|
* @return 格式化后的 API 路径
|
||||||
*/
|
*/
|
||||||
private fun buildPath(controllerPrefix: String, methodName: String): String {
|
private fun buildPath(controllerPrefix: String, methodName: String): String {
|
||||||
return (
|
val classPath = if (controllerPrefix != "/") {
|
||||||
"/${StrUtil.lowerFirst(StrUtil.toCamelCase(controllerPrefix))}/" +
|
StrUtil.toCamelCase(StrUtil.toUnderlineCase(controllerPrefix))
|
||||||
StrUtil.lowerFirst(StrUtil.toCamelCase(methodName))
|
} else ""
|
||||||
).replace("//", "/")
|
val methodPath = StrUtil.toCamelCase(StrUtil.toUnderlineCase(methodName))
|
||||||
|
return "/$classPath/$methodPath".replace("//", "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -274,7 +275,7 @@ class OpenApiSpecGenerator {
|
|||||||
val type =
|
val type =
|
||||||
(parameter.type.javaType as? Class<*>) ?: (parameter.type.javaType as? ParameterizedType)?.rawType as? Class<*>
|
(parameter.type.javaType as? Class<*>) ?: (parameter.type.javaType as? ParameterizedType)?.rawType as? Class<*>
|
||||||
?: return null
|
?: return null
|
||||||
|
if (type.packageName.startsWith("io.vertx")) return null
|
||||||
val paramName = parameter.name ?: return null
|
val paramName = parameter.name ?: return null
|
||||||
val annotation = parameter.annotations.filterIsInstance<D>().firstOrNull()
|
val annotation = parameter.annotations.filterIsInstance<D>().firstOrNull()
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
package org.aikrai.vertx.utlis
|
||||||
|
|
||||||
|
import org.aikrai.vertx.db.annotation.FieldFill
|
||||||
|
import org.aikrai.vertx.db.annotation.TableField
|
||||||
|
import org.aikrai.vertx.utlis.TimeUtil.now
|
||||||
|
import java.sql.Timestamp
|
||||||
|
|
||||||
|
open class BaseEntity {
|
||||||
|
|
||||||
|
var createBy: String? = null
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
var createTime: Timestamp = now()
|
||||||
|
|
||||||
|
var updateBy: String? = null
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
var updateTime: Timestamp = now()
|
||||||
|
|
||||||
|
var remark: String? = null
|
||||||
|
|
||||||
|
var version: Long = 0
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@ object ClassUtil {
|
|||||||
*
|
*
|
||||||
* @return 主类的 Class 对象,如果未找到则返回 null
|
* @return 主类的 Class 对象,如果未找到则返回 null
|
||||||
*/
|
*/
|
||||||
fun getMainClass(): Class<*>? {
|
fun getMainClass(): Class<*> {
|
||||||
val classLoader = ServiceLoader.load(ClassLoader::class.java).firstOrNull()
|
val classLoader = ServiceLoader.load(ClassLoader::class.java).firstOrNull()
|
||||||
?: Thread.currentThread().contextClassLoader
|
?: Thread.currentThread().contextClassLoader
|
||||||
val mainCommand = System.getProperty("sun.java.command")
|
val mainCommand = System.getProperty("sun.java.command")
|
||||||
@ -47,11 +47,11 @@ object ClassUtil {
|
|||||||
classLoader.loadClass(mainClassName)
|
classLoader.loadClass(mainClassName)
|
||||||
} catch (e: ClassNotFoundException) {
|
} catch (e: ClassNotFoundException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
null
|
throw Meta.error("MainClassNotFound", "获取启动类失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
throw Meta.error("MainClassNotFound", "获取启动类失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
318
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/IpUtil.kt
Normal file
318
vertx-fw/src/main/kotlin/org/aikrai/vertx/utlis/IpUtil.kt
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
package org.aikrai.vertx.utlis
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil
|
||||||
|
import io.vertx.core.http.HttpServerRequest
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
|
object IpUtil {
|
||||||
|
private const val REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)"
|
||||||
|
|
||||||
|
// 匹配 ip
|
||||||
|
private const val REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")"
|
||||||
|
const val REGX_IP_WILDCARD =
|
||||||
|
"(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}|((" + REGX_0_255 + "\\.){3}\\*))"
|
||||||
|
|
||||||
|
// 匹配网段
|
||||||
|
const val REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端IP
|
||||||
|
*
|
||||||
|
* @param request 请求对象
|
||||||
|
* @return IP地址
|
||||||
|
*/
|
||||||
|
fun getIpAddr(request: HttpServerRequest?): String {
|
||||||
|
if (request == null) return "unknown"
|
||||||
|
var ip: String? = request.getHeader("x-forwarded-for")
|
||||||
|
if (ip.isNullOrEmpty() || "unknown".equals(ip, ignoreCase = true)) {
|
||||||
|
ip = request.getHeader("Proxy-Client-IP")
|
||||||
|
}
|
||||||
|
if (ip.isNullOrEmpty() || "unknown".equals(ip, ignoreCase = true)) {
|
||||||
|
ip = request.getHeader("X-Forwarded-For")
|
||||||
|
}
|
||||||
|
if (ip.isNullOrEmpty() || "unknown".equals(ip, ignoreCase = true)) {
|
||||||
|
ip = request.getHeader("WL-Proxy-Client-IP")
|
||||||
|
}
|
||||||
|
if (ip.isNullOrEmpty() || "unknown".equals(ip, ignoreCase = true)) {
|
||||||
|
ip = request.getHeader("X-Real-IP")
|
||||||
|
}
|
||||||
|
if (ip.isNullOrEmpty() || "unknown".equals(ip, ignoreCase = true)) {
|
||||||
|
ip = request.remoteAddress().host()
|
||||||
|
}
|
||||||
|
return if (ip == "0:0:0:0:0:0:0:1") {
|
||||||
|
"127.0.0.1"
|
||||||
|
} else {
|
||||||
|
getMultistageReverseProxyIp(ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为内部IP地址
|
||||||
|
*
|
||||||
|
* @param ip IP地址
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
fun internalIp(ip: String?): Boolean {
|
||||||
|
val addr = textToNumericFormatV4(ip)
|
||||||
|
return internalIp(addr) || ip == "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为内部IP地址
|
||||||
|
*
|
||||||
|
* @param addr byte地址
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
private fun internalIp(addr: ByteArray?): Boolean {
|
||||||
|
if (addr == null || addr.size < 2) return true
|
||||||
|
|
||||||
|
val b0 = addr[0]
|
||||||
|
val b1 = addr[1]
|
||||||
|
// 10.x.x.x/8
|
||||||
|
val section1: Byte = 0x0A
|
||||||
|
// 172.16.x.x/12
|
||||||
|
val section2: Byte = 0xAC.toByte()
|
||||||
|
val section3: Byte = 0x10
|
||||||
|
val section4: Byte = 0x1F
|
||||||
|
// 192.168.x.x/16
|
||||||
|
val section5: Byte = 0xC0.toByte()
|
||||||
|
val section6: Byte = 0xA8.toByte()
|
||||||
|
return when (b0) {
|
||||||
|
section1 -> true
|
||||||
|
section2 -> {
|
||||||
|
if (b1 >= section3 && b1 <= section4) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section5 -> {
|
||||||
|
when (b1) {
|
||||||
|
section6 -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将IPv4地址转换成字节
|
||||||
|
*
|
||||||
|
* @param text IPv4地址
|
||||||
|
* @return byte 字节
|
||||||
|
*/
|
||||||
|
fun textToNumericFormatV4(text: String?): ByteArray? {
|
||||||
|
if (text.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val bytes = ByteArray(4)
|
||||||
|
val elements = text.split(".", limit = 5).toTypedArray()
|
||||||
|
return try {
|
||||||
|
when (elements.size) {
|
||||||
|
1 -> {
|
||||||
|
val l = elements[0].toLong()
|
||||||
|
if (l < 0L || l > 4294967295L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
bytes[0] = ((l shr 24) and 0xFF).toByte()
|
||||||
|
bytes[1] = ((l shr 16) and 0xFF).toByte()
|
||||||
|
bytes[2] = ((l shr 8) and 0xFF).toByte()
|
||||||
|
bytes[3] = (l and 0xFF).toByte()
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
var l = elements[0].toLong()
|
||||||
|
if (l < 0L || l > 255L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
bytes[0] = (l and 0xFF).toByte()
|
||||||
|
l = elements[1].toLong()
|
||||||
|
if (l < 0L || l > 16777215L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
bytes[1] = ((l shr 16) and 0xFF).toByte()
|
||||||
|
bytes[2] = ((l shr 8) and 0xFF).toByte()
|
||||||
|
bytes[3] = (l and 0xFF).toByte()
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
for (i in 0..1) {
|
||||||
|
val l = elements[i].toLong()
|
||||||
|
if (l < 0L || l > 255L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
bytes[i] = (l and 0xFF).toByte()
|
||||||
|
}
|
||||||
|
val l = elements[2].toLong()
|
||||||
|
if (l < 0L || l > 65535L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
bytes[2] = ((l shr 8) and 0xFF).toByte()
|
||||||
|
bytes[3] = (l and 0xFF).toByte()
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
4 -> {
|
||||||
|
for (i in 0..3) {
|
||||||
|
val l = elements[i].toLong()
|
||||||
|
if (l < 0L || l > 255L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
bytes[i] = (l and 0xFF).toByte()
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取IP地址
|
||||||
|
*
|
||||||
|
* @return 本地IP地址
|
||||||
|
*/
|
||||||
|
fun getHostIp(): String {
|
||||||
|
return try {
|
||||||
|
InetAddress.getLocalHost().hostAddress
|
||||||
|
} catch (e: UnknownHostException) {
|
||||||
|
"127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主机名
|
||||||
|
*
|
||||||
|
* @return 本地主机名
|
||||||
|
*/
|
||||||
|
fun getHostName(): String {
|
||||||
|
return try {
|
||||||
|
InetAddress.getLocalHost().hostName
|
||||||
|
} catch (e: UnknownHostException) {
|
||||||
|
"未知"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从多级反向代理中获得第一个非unknown IP地址
|
||||||
|
*
|
||||||
|
* @param ip 获得的IP地址
|
||||||
|
* @return 第一个非unknown IP地址
|
||||||
|
*/
|
||||||
|
fun getMultistageReverseProxyIp(ip: String?): String {
|
||||||
|
var ipAddress = ip
|
||||||
|
// 多级反向代理检测
|
||||||
|
if (!ipAddress.isNullOrEmpty() && ipAddress.contains(",")) {
|
||||||
|
val ips = ipAddress.trim().split(",")
|
||||||
|
for (subIp in ips) {
|
||||||
|
if (!isUnknown(subIp)) {
|
||||||
|
ipAddress = subIp
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StrUtil.sub(ipAddress, 0, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测给定字符串是否为未知,多用于检测HTTP请求相关
|
||||||
|
*
|
||||||
|
* @param checkString 被检测的字符串
|
||||||
|
* @return 是否未知
|
||||||
|
*/
|
||||||
|
fun isUnknown(checkString: String?): Boolean {
|
||||||
|
return checkString.isNullOrBlank() || "unknown".equals(checkString, ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为IP
|
||||||
|
*/
|
||||||
|
fun isIP(ip: String?): Boolean {
|
||||||
|
return !ip.isNullOrBlank() && Regex(REGX_IP).matches(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为IP,或 *为间隔的通配符地址
|
||||||
|
*/
|
||||||
|
fun isIpWildCard(ip: String?): Boolean {
|
||||||
|
return !ip.isNullOrBlank() && Regex(REGX_IP_WILDCARD).matches(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测参数是否在ip通配符里
|
||||||
|
*/
|
||||||
|
fun ipIsInWildCardNoCheck(ipWildCard: String, ip: String): Boolean {
|
||||||
|
val s1 = ipWildCard.split(".")
|
||||||
|
val s2 = ip.split(".")
|
||||||
|
var isMatchedSeg = true
|
||||||
|
for (i in s1.indices) {
|
||||||
|
if (s1[i] == "*") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (i >= s2.size || s1[i] != s2[i]) {
|
||||||
|
isMatchedSeg = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isMatchedSeg
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串
|
||||||
|
*/
|
||||||
|
fun isIPSegment(ipSeg: String?): Boolean {
|
||||||
|
return !ipSeg.isNullOrBlank() && Regex(REGX_IP_SEG).matches(ipSeg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断ip是否在指定网段中
|
||||||
|
*/
|
||||||
|
fun ipIsInNetNoCheck(iparea: String, ip: String): Boolean {
|
||||||
|
val idx = iparea.indexOf('-')
|
||||||
|
if (idx < 0) return false
|
||||||
|
val sips = iparea.substring(0, idx).split(".")
|
||||||
|
val sipe = iparea.substring(idx + 1).split(".")
|
||||||
|
val sipt = ip.split(".")
|
||||||
|
var ips: Long = 0
|
||||||
|
var ipe: Long = 0
|
||||||
|
var ipt: Long = 0
|
||||||
|
for (i in 0 until 4) {
|
||||||
|
ips = (ips shl 8) or sips[i].toLong()
|
||||||
|
ipe = (ipe shl 8) or sipe[i].toLong()
|
||||||
|
ipt = (ipt shl 8) or sipt[i].toLong()
|
||||||
|
}
|
||||||
|
var lower = ips
|
||||||
|
var upper = ipe
|
||||||
|
if (lower > upper) {
|
||||||
|
val t = lower
|
||||||
|
lower = upper
|
||||||
|
upper = t
|
||||||
|
}
|
||||||
|
return lower <= ipt && ipt <= upper
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验ip是否符合过滤串规则
|
||||||
|
*
|
||||||
|
* @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99`
|
||||||
|
* @param ip 校验IP地址
|
||||||
|
* @return boolean 结果
|
||||||
|
*/
|
||||||
|
fun isMatchedIp(filter: String?, ip: String?): Boolean {
|
||||||
|
if (filter.isNullOrEmpty() || ip.isNullOrEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val ips = filter.split(";")
|
||||||
|
for (iStr in ips) {
|
||||||
|
when {
|
||||||
|
isIP(iStr) && iStr == ip -> return true
|
||||||
|
isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip) -> return true
|
||||||
|
isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip) -> return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,43 +6,46 @@ import org.aikrai.vertx.jackson.JsonUtil
|
|||||||
@JsonIgnoreProperties("localizedMessage", "suppressed", "stackTrace", "cause")
|
@JsonIgnoreProperties("localizedMessage", "suppressed", "stackTrace", "cause")
|
||||||
class Meta(
|
class Meta(
|
||||||
val name: String,
|
val name: String,
|
||||||
override val message: String = "",
|
override val message: String = "Internal Server Error",
|
||||||
val data: Any? = null
|
val data: Any? = null
|
||||||
) : RuntimeException(message, null, false, false) {
|
) : RuntimeException(message, null, true, false) {
|
||||||
|
|
||||||
fun stackTraceToString(): String {
|
fun stackTraceToString(): String {
|
||||||
return JsonUtil.toJsonStr(this)
|
return JsonUtil.toJsonStr(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun failure(name: String, message: String): Meta =
|
fun error(name: String, message: String): Meta =
|
||||||
Meta(name, message)
|
Meta(name, message)
|
||||||
|
|
||||||
fun unimplemented(message: String): Meta =
|
fun unimplemented(message: String): Meta =
|
||||||
Meta("unimplemented", message)
|
Meta("Unimplemented", message)
|
||||||
|
|
||||||
fun unauthorized(message: String): Meta =
|
fun unauthorized(message: String): Meta =
|
||||||
Meta("unauthorized", message)
|
Meta("Unauthorized", message)
|
||||||
|
|
||||||
fun timeout(message: String): Meta =
|
fun timeout(message: String): Meta =
|
||||||
Meta("timeout", message)
|
Meta("Timeout", message)
|
||||||
|
|
||||||
fun requireArgument(argument: String, message: String): Meta =
|
fun requireArgument(argument: String, message: String): Meta =
|
||||||
Meta("required_argument:$argument", message)
|
Meta("RequiredArgument:$argument", message)
|
||||||
|
|
||||||
fun invalidArgument(argument: String, message: String): Meta =
|
fun invalidArgument(argument: String, message: String): Meta =
|
||||||
Meta("invalid_argument:$argument", message)
|
Meta("InvalidArgument:$argument", message)
|
||||||
|
|
||||||
fun notFound(argument: String, message: String): Meta =
|
fun notFound(argument: String, message: String): Meta =
|
||||||
Meta("not_found:$argument", message)
|
Meta("NotFound:$argument", message)
|
||||||
|
|
||||||
fun badRequest(message: String): Meta =
|
fun badRequest(message: String): Meta =
|
||||||
Meta("bad_request", message)
|
Meta("BadRequest", message)
|
||||||
|
|
||||||
fun notSupported(message: String): Meta =
|
fun notSupported(message: String): Meta =
|
||||||
Meta("not_supported", message)
|
Meta("NotSupported", message)
|
||||||
|
|
||||||
fun forbidden(message: String): Meta =
|
fun forbidden(message: String): Meta =
|
||||||
Meta("forbidden", message)
|
Meta("Forbidden", message)
|
||||||
|
|
||||||
|
fun repository(name: String, message: String?): Meta =
|
||||||
|
Meta("Repository:$name", message ?: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user