Compare commits
2 Commits
e7016373c2
...
1447e26d25
| Author | SHA1 | Date | |
|---|---|---|---|
| 1447e26d25 | |||
| 394161415c |
@ -8,7 +8,7 @@ plugins {
|
||||
group = "com.demo"
|
||||
version = "1.0.0-SNAPSHOT"
|
||||
|
||||
val vertxVersion = "4.5.11"
|
||||
val vertxVersion = "4.5.14"
|
||||
val junitJupiterVersion = "5.9.1"
|
||||
|
||||
application {
|
||||
@ -67,44 +67,34 @@ dependencies {
|
||||
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||
implementation(platform("io.vertx:vertx-stack-depchain:$vertxVersion"))
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
|
||||
// 特定于vertx-demo的依赖,保留
|
||||
implementation("io.vertx:vertx-lang-kotlin:$vertxVersion")
|
||||
implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
||||
implementation("io.vertx:vertx-core:$vertxVersion")
|
||||
implementation("io.vertx:vertx-web:$vertxVersion")
|
||||
implementation("io.vertx:vertx-web-client:$vertxVersion")
|
||||
implementation("io.vertx:vertx-config:$vertxVersion")
|
||||
implementation("io.vertx:vertx-config-yaml:$vertxVersion")
|
||||
implementation("io.vertx:vertx-pg-client:$vertxVersion")
|
||||
implementation("io.vertx:vertx-mysql-client:$vertxVersion")
|
||||
implementation("io.vertx:vertx-sql-client-templates:$vertxVersion")
|
||||
implementation("io.vertx:vertx-auth-jwt:$vertxVersion")
|
||||
implementation("io.vertx:vertx-redis-client:$vertxVersion")
|
||||
|
||||
implementation("dev.langchain4j:langchain4j-open-ai:1.0.0-beta1")
|
||||
implementation("dev.langchain4j:langchain4j:1.0.0-beta1")
|
||||
|
||||
implementation("com.google.inject:guice:5.1.0")
|
||||
implementation("org.reflections:reflections:0.10.2")
|
||||
implementation("cn.hutool:hutool-core:5.8.24")
|
||||
// hutool
|
||||
implementation("cn.hutool:hutool-json:5.8.24")
|
||||
implementation("cn.hutool:hutool-crypto:5.8.24")
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
|
||||
// implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
|
||||
|
||||
implementation("dev.hsbrysk:caffeine-coroutines:1.0.0")
|
||||
|
||||
// log
|
||||
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
|
||||
implementation("org.slf4j:slf4j-api:2.0.6")
|
||||
implementation("ch.qos.logback:logback-classic:1.4.14")
|
||||
implementation("org.codehaus.janino:janino:3.1.8")
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
|
||||
implementation("org.slf4j:slf4j-api:2.0.17")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.18")
|
||||
|
||||
// db
|
||||
implementation("org.postgresql:postgresql:42.7.5")
|
||||
implementation("com.ongres.scram:client:2.1")
|
||||
|
||||
// doc
|
||||
implementation("io.swagger.core.v3:swagger-core:2.2.27")
|
||||
// implementation("io.swagger.core.v3:swagger-core:2.2.27")
|
||||
|
||||
// XML解析库
|
||||
implementation("javax.xml.bind:jaxb-api:2.3.1")
|
||||
|
||||
@ -2,9 +2,9 @@ package app
|
||||
|
||||
import app.config.InjectConfig
|
||||
import app.verticle.MainVerticle
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.vertx.core.Vertx
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mu.KotlinLogging
|
||||
import org.aikrai.vertx.config.Config
|
||||
|
||||
object Application {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package app.config
|
||||
|
||||
import app.config.auth.JWTAuthProvider
|
||||
import app.config.db.DbPoolProvider
|
||||
import app.config.provider.JWTAuthProvider
|
||||
import app.config.provider.DbPoolProvider
|
||||
import cn.hutool.core.lang.Snowflake
|
||||
import cn.hutool.core.util.IdUtil
|
||||
import com.google.inject.AbstractModule
|
||||
@ -15,32 +15,40 @@ import io.vertx.sqlclient.SqlClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.aikrai.vertx.config.DefaultScope
|
||||
import org.aikrai.vertx.config.FrameworkConfigModule
|
||||
import org.aikrai.vertx.http.GlobalErrorHandler
|
||||
import org.aikrai.vertx.http.RequestLogHandler
|
||||
|
||||
/**
|
||||
* 依赖注入配置
|
||||
*/
|
||||
object InjectConfig {
|
||||
fun configure(vertx: Vertx): Injector {
|
||||
return Guice.createInjector(InjectorModule(vertx))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guice模块配置
|
||||
*/
|
||||
class InjectorModule(
|
||||
private val vertx: Vertx,
|
||||
) : AbstractModule() {
|
||||
override fun configure() {
|
||||
// 1. 安装框架提供的配置模块
|
||||
install(FrameworkConfigModule())
|
||||
|
||||
// 2. 绑定 Vertx 实例和 CoroutineScope
|
||||
bind(Vertx::class.java).toInstance(vertx)
|
||||
bind(CoroutineScope::class.java).toInstance(DefaultScope(vertx))
|
||||
|
||||
// 3. 绑定 Snowflake
|
||||
bind(Snowflake::class.java).toInstance(IdUtil.getSnowflake())
|
||||
|
||||
// 4. 绑定数据库连接池 (使用 Provider 来延迟创建)
|
||||
bind(Pool::class.java).toProvider(DbPoolProvider::class.java).`in`(Singleton::class.java)
|
||||
bind(SqlClient::class.java).to(Pool::class.java) // 绑定 SqlClient 到 Pool
|
||||
bind(SqlClient::class.java).to(Pool::class.java)
|
||||
|
||||
// 5. 绑定 JWTAuth
|
||||
bind(JWTAuth::class.java).toProvider(JWTAuthProvider::class.java).`in`(Singleton::class.java)
|
||||
|
||||
// 6. 绑定错误处理和日志组件
|
||||
bind(GlobalErrorHandler::class.java).`in`(Singleton::class.java)
|
||||
bind(RequestLogHandler::class.java).`in`(Singleton::class.java)
|
||||
}
|
||||
}
|
||||
@ -1,52 +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(code, 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
//package app.config
|
||||
//
|
||||
//import com.fasterxml.jackson.annotation.JsonInclude
|
||||
//import org.aikrai.vertx.constant.HttpStatus
|
||||
//
|
||||
//@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
//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(code, 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,50 +0,0 @@
|
||||
package app.config.auth
|
||||
|
||||
import cn.hutool.core.lang.Snowflake
|
||||
import io.vertx.ext.web.RoutingContext
|
||||
import io.vertx.ext.web.handler.AuthenticationHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.aikrai.vertx.utlis.Meta
|
||||
|
||||
class JwtAuthenticationHandler(
|
||||
val scope: CoroutineScope,
|
||||
val tokenService: TokenService,
|
||||
val context: String,
|
||||
val snowflake: Snowflake
|
||||
) : AuthenticationHandler {
|
||||
override fun handle(event: RoutingContext) {
|
||||
event.put("requestId", snowflake.nextId())
|
||||
val path = event.request().path().replace("$context/", "/").replace("//", "/")
|
||||
if (isPathExcluded(path, anonymous)) {
|
||||
event.next()
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
try {
|
||||
val user = tokenService.getLoginUser(event)
|
||||
tokenService.verifyToken(user)
|
||||
event.setUser(user)
|
||||
event.next()
|
||||
} catch (e: Throwable) {
|
||||
event.fail(401, Meta.unauthorized(e.message ?: "token"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var anonymous = mutableListOf(
|
||||
"/apidoc.json"
|
||||
)
|
||||
|
||||
private fun isPathExcluded(path: String, excludePatterns: List<String>): Boolean {
|
||||
for (pattern in excludePatterns) {
|
||||
val regexPattern = pattern
|
||||
.replace("**", ".+")
|
||||
.replace("*", "[^/]+")
|
||||
.replace("?", ".")
|
||||
val isExclude = path.matches(regexPattern.toRegex())
|
||||
if (isExclude) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
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 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: Throwable) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package app.config.handler
|
||||
|
||||
import app.service.auth.TokenService
|
||||
import com.google.inject.Inject
|
||||
import io.vertx.ext.web.RoutingContext
|
||||
import io.vertx.ext.web.handler.AuthenticationHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.aikrai.vertx.config.ServerConfig
|
||||
import org.aikrai.vertx.constant.HttpStatus
|
||||
import org.aikrai.vertx.utlis.Meta
|
||||
import org.slf4j.MDC
|
||||
|
||||
/**
|
||||
* JWT认证处理器
|
||||
*/
|
||||
class JwtAuthHandler @Inject constructor(
|
||||
val scope: CoroutineScope,
|
||||
val tokenService: TokenService,
|
||||
val serverConfig: ServerConfig,
|
||||
) : AuthenticationHandler {
|
||||
override fun handle(ctx: RoutingContext) {
|
||||
val path = ctx.request().path().replace("${serverConfig.context}/", "/").replace("//", "/")
|
||||
if (isPathExcluded(path, anonymous)) {
|
||||
ctx.next()
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
val user = tokenService.getLoginUser(ctx)
|
||||
ctx.setUser(user)
|
||||
|
||||
// 将用户ID放入MDC
|
||||
user.principal().getString("sub")?.let { userId ->
|
||||
MDC.put("userId", userId)
|
||||
}
|
||||
ctx.next()
|
||||
} catch (e: Throwable) {
|
||||
MDC.remove("userId")
|
||||
|
||||
val metaError = when (e) {
|
||||
is Meta -> e
|
||||
else -> Meta.unauthorized(e.message ?: "认证失败")
|
||||
}
|
||||
ctx.fail(HttpStatus.UNAUTHORIZED, metaError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var anonymous = mutableListOf(
|
||||
"/apidoc.json"
|
||||
)
|
||||
|
||||
/**
|
||||
* 检查路径是否在排除列表中
|
||||
*/
|
||||
private fun isPathExcluded(path: String, excludePatterns: List<String>): Boolean {
|
||||
for (pattern in excludePatterns) {
|
||||
val regexPattern = pattern
|
||||
.replace("**", ".+")
|
||||
.replace("*", "[^/]+")
|
||||
.replace("?", ".")
|
||||
val isExclude = path.matches(regexPattern.toRegex())
|
||||
if (isExclude) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package app.config.handler
|
||||
|
||||
import com.google.inject.Singleton
|
||||
import io.vertx.core.http.HttpHeaders
|
||||
import io.vertx.ext.web.RoutingContext
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.http.RespBean
|
||||
import org.aikrai.vertx.jackson.JsonUtil
|
||||
import org.aikrai.vertx.resp.ResponseHandlerInterface
|
||||
|
||||
/**
|
||||
* 响应处理器,负责处理API响应
|
||||
*/
|
||||
@Singleton
|
||||
class ResponseHandler : ResponseHandlerInterface {
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
/**
|
||||
* 处理成功响应
|
||||
*/
|
||||
override suspend fun handle(
|
||||
ctx: RoutingContext,
|
||||
responseData: Any?,
|
||||
customizeResponse: Boolean
|
||||
) {
|
||||
// 使用RequestLogHandler设置的请求ID
|
||||
val requestId = ctx.get<String>("requestId")
|
||||
val code: Int
|
||||
val resStr = when (responseData) {
|
||||
is RespBean<*> -> {
|
||||
code = responseData.code
|
||||
responseData.requestId = requestId
|
||||
JsonUtil.toJsonStr(responseData)
|
||||
}
|
||||
// 否则使用RespBean包装
|
||||
else -> {
|
||||
val respBean = RespBean.Companion.success(responseData)
|
||||
respBean.requestId = requestId
|
||||
code = respBean.code
|
||||
JsonUtil.toJsonStr(respBean)
|
||||
}
|
||||
}
|
||||
ctx.put("responseData", resStr) // 存储响应内容用于日志
|
||||
|
||||
if (customizeResponse) return // 如果需要自定义响应,则不发送标准响应
|
||||
|
||||
ctx.response()
|
||||
.setStatusCode(code)
|
||||
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.end(resStr)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package app.config.db
|
||||
package app.config.provider
|
||||
|
||||
import com.google.inject.Inject
|
||||
import com.google.inject.Provider
|
||||
@ -1,4 +1,4 @@
|
||||
package app.config.auth
|
||||
package app.config.provider
|
||||
|
||||
import com.google.inject.Inject
|
||||
import com.google.inject.Provider
|
||||
@ -21,4 +21,4 @@ class JWTAuthProvider @Inject constructor(
|
||||
)
|
||||
return JWTAuth.create(vertx, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,11 +5,12 @@ import app.data.domain.account.AccountRepository
|
||||
import app.data.emun.Status
|
||||
import app.service.account.AccountService
|
||||
import com.google.inject.Inject
|
||||
import mu.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.auth.AllowAnonymous
|
||||
import org.aikrai.vertx.config.Config
|
||||
import org.aikrai.vertx.context.Controller
|
||||
import org.aikrai.vertx.context.D
|
||||
import org.aikrai.vertx.utlis.Meta
|
||||
|
||||
/**
|
||||
* 推荐代码示例
|
||||
@ -32,6 +33,7 @@ class Demo1Controller @Inject constructor(
|
||||
@D("account", "账号") account: Account?
|
||||
) {
|
||||
logger.info { "你好" }
|
||||
throw Meta.error("test", "test")
|
||||
println(age)
|
||||
println(list)
|
||||
println("test-$name")
|
||||
|
||||
@ -7,7 +7,7 @@ import io.vertx.core.http.HttpMethod
|
||||
import io.vertx.core.json.JsonObject
|
||||
import io.vertx.ext.web.client.WebClient
|
||||
import io.vertx.ext.web.client.WebClientOptions
|
||||
import mu.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.config.Config
|
||||
|
||||
class ApifoxClient @Inject constructor(
|
||||
|
||||
@ -5,7 +5,7 @@ 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 io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.config.RedisConfig
|
||||
|
||||
@Singleton
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
package app.service.account
|
||||
|
||||
import app.config.auth.TokenService
|
||||
import app.data.domain.account.Account
|
||||
import app.data.domain.account.AccountRepository
|
||||
import app.data.domain.account.LoginDTO
|
||||
import app.service.auth.TokenService
|
||||
import cn.hutool.core.lang.Snowflake
|
||||
import cn.hutool.crypto.SecureUtil
|
||||
import com.google.inject.Inject
|
||||
import io.vertx.ext.web.RoutingContext
|
||||
import mu.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.db.tx.withTransaction
|
||||
import org.aikrai.vertx.utlis.IpUtil
|
||||
import org.aikrai.vertx.utlis.Meta
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
package app.config.auth
|
||||
package app.service.auth
|
||||
|
||||
import app.data.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
|
||||
@ -14,7 +13,7 @@ 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 io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.auth.AuthUser
|
||||
import org.aikrai.vertx.constant.CacheConstants
|
||||
import org.aikrai.vertx.constant.Constants
|
||||
@ -23,7 +22,6 @@ 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,
|
||||
@ -54,8 +52,6 @@ class TokenService @Inject constructor(
|
||||
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))
|
||||
@ -15,7 +15,7 @@ import io.swagger.v3.oas.models.parameters.RequestBody
|
||||
import io.swagger.v3.oas.models.responses.ApiResponse
|
||||
import io.swagger.v3.oas.models.responses.ApiResponses
|
||||
import io.swagger.v3.oas.models.servers.Server
|
||||
import mu.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.context.Controller
|
||||
import org.aikrai.vertx.context.CustomizeRequest
|
||||
import org.aikrai.vertx.context.D
|
||||
|
||||
@ -2,7 +2,7 @@ package app.verticle
|
||||
|
||||
import com.google.inject.Inject
|
||||
import io.vertx.kotlin.coroutines.CoroutineVerticle
|
||||
import mu.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
|
||||
class MainVerticle @Inject constructor(
|
||||
private val webVerticle: WebVerticle,
|
||||
|
||||
@ -1,57 +1,49 @@
|
||||
package app.verticle
|
||||
|
||||
import app.config.RespBean
|
||||
import app.config.auth.JwtAuthenticationHandler
|
||||
import app.config.auth.ResponseHandler
|
||||
import app.config.auth.TokenService
|
||||
import app.data.domain.account.Account
|
||||
import app.config.handler.JwtAuthHandler
|
||||
import app.config.handler.ResponseHandler
|
||||
import app.port.aipfox.ApifoxClient
|
||||
import cn.hutool.core.lang.Snowflake
|
||||
import com.google.inject.Inject
|
||||
import com.google.inject.Injector
|
||||
import io.vertx.core.Handler
|
||||
import io.vertx.core.http.HttpHeaders
|
||||
import io.vertx.core.http.HttpMethod
|
||||
import io.vertx.core.http.HttpServerOptions
|
||||
import io.vertx.ext.web.Router
|
||||
import io.vertx.ext.web.RoutingContext
|
||||
import io.vertx.ext.web.handler.BodyHandler
|
||||
import io.vertx.ext.web.handler.CorsHandler
|
||||
import io.vertx.kotlin.coroutines.CoroutineVerticle
|
||||
import io.vertx.kotlin.coroutines.coAwait
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import mu.KotlinLogging
|
||||
import org.aikrai.vertx.auth.AuthUser
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.config.ServerConfig
|
||||
import org.aikrai.vertx.context.RouterBuilder
|
||||
import org.aikrai.vertx.jackson.JsonUtil
|
||||
import org.aikrai.vertx.utlis.LangUtil.toStringMap
|
||||
import org.aikrai.vertx.utlis.Meta
|
||||
import org.aikrai.vertx.http.GlobalErrorHandler
|
||||
import org.aikrai.vertx.http.RequestLogHandler
|
||||
|
||||
class WebVerticle @Inject constructor(
|
||||
private val getIt: Injector,
|
||||
private val serverConfig: ServerConfig,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val tokenService: TokenService,
|
||||
private val apifoxClient: ApifoxClient,
|
||||
private val snowflake: Snowflake,
|
||||
private val jwtAuthHandler: JwtAuthHandler,
|
||||
private val requestLogHandler: RequestLogHandler,
|
||||
private val responseHandler: ResponseHandler,
|
||||
private val serverConfig: ServerConfig
|
||||
) : CoroutineVerticle() {
|
||||
private val globalErrorHandler: GlobalErrorHandler,
|
||||
private val apiFoxClient: ApifoxClient,
|
||||
) : CoroutineVerticle() {
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
override suspend fun start() {
|
||||
val rootRouter = Router.router(vertx)
|
||||
val router = Router.router(vertx)
|
||||
setupRouter(rootRouter, router)
|
||||
val options = HttpServerOptions().setMaxFormAttributeSize(1024 * 1024)
|
||||
val options = HttpServerOptions()
|
||||
.setMaxFormAttributeSize(1024 * 1024)
|
||||
val server = vertx.createHttpServer(options)
|
||||
.requestHandler(rootRouter)
|
||||
.listen(serverConfig.port)
|
||||
.coAwait()
|
||||
|
||||
apifoxClient.importOpenapi()
|
||||
|
||||
logger.info { "http server start - http://127.0.0.1:${server.actualPort()}${serverConfig.context}" }
|
||||
// 生成ApiFox接口
|
||||
apiFoxClient.importOpenapi()
|
||||
logger.info { "HTTP服务启动 - http://127.0.0.1:${server.actualPort()}${serverConfig.context}" }
|
||||
}
|
||||
|
||||
override suspend fun stop() {
|
||||
@ -59,19 +51,18 @@ class WebVerticle @Inject constructor(
|
||||
|
||||
private fun setupRouter(rootRouter: Router, router: Router) {
|
||||
rootRouter.route("${serverConfig.context}*").subRouter(router)
|
||||
|
||||
router.route()
|
||||
.handler(corsHandler)
|
||||
.handler(BodyHandler.create())
|
||||
.handler(logHandler)
|
||||
.failureHandler(errorHandler)
|
||||
.handler(jwtAuthHandler)
|
||||
.handler(requestLogHandler)
|
||||
.failureHandler(globalErrorHandler)
|
||||
|
||||
val authHandler = JwtAuthenticationHandler(coroutineScope, tokenService, serverConfig.context, snowflake)
|
||||
router.route("/*").handler(authHandler)
|
||||
val routerBuilder = RouterBuilder(coroutineScope, router, serverConfig.scanPackage, responseHandler)
|
||||
.build{ getIt.getInstance(it) }
|
||||
|
||||
val routerBuilder = RouterBuilder(coroutineScope, router, serverConfig.scanPackage, responseHandler).build { service ->
|
||||
getIt.getInstance(service)
|
||||
}
|
||||
authHandler.anonymous.addAll(routerBuilder.anonymousPaths)
|
||||
jwtAuthHandler.anonymous.addAll(routerBuilder.anonymousPaths)
|
||||
}
|
||||
|
||||
private val corsHandler = CorsHandler.create()
|
||||
@ -81,76 +72,4 @@ class WebVerticle @Inject constructor(
|
||||
.allowedMethod(HttpMethod.PUT)
|
||||
.allowedMethod(HttpMethod.DELETE)
|
||||
.allowedMethod(HttpMethod.OPTIONS)
|
||||
|
||||
// 非业务异常处理
|
||||
private val errorHandler = Handler<RoutingContext> { ctx ->
|
||||
val failure = ctx.failure()
|
||||
if (failure != null) {
|
||||
logger.error { "${ctx.request().uri()}: ${failure.stackTraceToString()}" }
|
||||
val resObj = when (failure) {
|
||||
is Meta -> RespBean.failure(ctx.statusCode(), "${failure.name}:${failure.message}", failure.data)
|
||||
else -> RespBean.failure("${failure.javaClass.simpleName}${if (failure.message != null) ":${failure.message}" else ""}")
|
||||
}
|
||||
val resStr = JsonUtil.toJsonStr(resObj)
|
||||
ctx.put("responseData", resStr)
|
||||
ctx.response()
|
||||
.setStatusCode(ctx.statusCode())
|
||||
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.end(resStr)
|
||||
} else {
|
||||
logger.error("${ctx.request().uri()}: 未知错误")
|
||||
val resObj = RespBean.failure("未知错误")
|
||||
val resStr = JsonUtil.toJsonStr(resObj)
|
||||
ctx.put("responseData", resStr)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
<property name="FILESIZE" value="500MB"/>
|
||||
<property name="MAXHISTORY" value="100"/>
|
||||
|
||||
<!-- 定义MDC变量,如果不存在则为空字符串 -->
|
||||
<property name="mdcPattern" value="requestId=%X{requestId:-N/A} userId=%X{userId:-anon} ip=%X{remoteAddr:-} method=%X{method:-} path=%X{path:-} status=%X{statusCode:-} time=%X{duration:-}ms" />
|
||||
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<Target>System.out</Target>
|
||||
<encoder charset="utf-8">
|
||||
@ -14,15 +18,14 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 警告级别日志 -->
|
||||
<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${ROOT}${APPNAME}-warn.log</file>
|
||||
<encoder charset="utf-8">
|
||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||
</pattern>
|
||||
<pattern>[%-5level] %d{ISO8601} [%thread] %logger{36} [${mdcPattern}] - %m%n</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
|
||||
<evaluator>
|
||||
<expression>return level >= WARN;</expression>
|
||||
</evaluator>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>WARN</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
@ -36,15 +39,14 @@
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 信息级别日志 -->
|
||||
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${ROOT}${APPNAME}-info.log</file>
|
||||
<encoder charset="utf-8">
|
||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||
</pattern>
|
||||
<pattern>[%-5level] %d{ISO8601} [%thread] %logger{36} [${mdcPattern}] - %m%n</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<evaluator>
|
||||
<expression>return level >= INFO;</expression>
|
||||
</evaluator>
|
||||
<level>INFO</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
@ -58,21 +60,21 @@
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 调试级别日志 -->
|
||||
<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${ROOT}${APPNAME}-debug.log</file>
|
||||
<encoder charset="utf-8">
|
||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<evaluator>
|
||||
<expression>return level >= DEBUG;</expression>
|
||||
</evaluator>
|
||||
<level>DEBUG</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${ROOT}${APPNAME}-%d-info.%i.log</fileNamePattern>
|
||||
<fileNamePattern>${ROOT}${APPNAME}-%d-debug.%i.log</fileNamePattern>
|
||||
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||
<timeBasedFileNamingAndTriggeringPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
@ -81,21 +83,21 @@
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 跟踪级别日志 -->
|
||||
<appender name="TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${ROOT}${APPNAME}-trace.log</file>
|
||||
<encoder charset="utf-8">
|
||||
<pattern>[%-5level] %d [%thread] %class{36}.%M:%L - %m%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<evaluator>
|
||||
<expression>return level >= TRACE;</expression>
|
||||
</evaluator>
|
||||
<level>TRACE</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${ROOT}${APPNAME}-%d-info.%i.log</fileNamePattern>
|
||||
<fileNamePattern>${ROOT}${APPNAME}-%d-trace.%i.log</fileNamePattern>
|
||||
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||
<timeBasedFileNamingAndTriggeringPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
@ -104,11 +106,67 @@
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<root level="DEBUG">
|
||||
<!-- JSON格式日志(所有级别) -->
|
||||
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${ROOT}${APPNAME}-json.log</file>
|
||||
<!-- 使用 logstash-logback-encoder,需要添加相应依赖 -->
|
||||
<!-- 如果不想使用 logstash-logback-encoder,可以注释掉这个appender或者使用自定义JSON格式 -->
|
||||
<!--
|
||||
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
|
||||
<includeMdc>true</includeMdc>
|
||||
<customFields>{"application":"${APPNAME}"}</customFields>
|
||||
</encoder>
|
||||
-->
|
||||
<!-- 简单的JSON格式输出,不依赖额外库 -->
|
||||
<encoder charset="utf-8">
|
||||
<pattern>{"time":"%d{ISO8601}","level":"%level","thread":"%thread","logger":"%logger","mdc":{%mdc},"message":"%message"}%n</pattern>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${ROOT}${APPNAME}-%d-json.%i.log</fileNamePattern>
|
||||
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||
<timeBasedFileNamingAndTriggeringPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 错误级别日志 -->
|
||||
<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<!-- 添加缺少的file属性 -->
|
||||
<file>${ROOT}${APPNAME}-error.log</file>
|
||||
<encoder charset="utf-8">
|
||||
<pattern>[%-5level] %d{ISO8601} [%thread] %logger{36} [${mdcPattern}] - %m%n</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<level>ERROR</level>
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${ROOT}${APPNAME}-%d-error.%i.log</fileNamePattern>
|
||||
<maxHistory>${MAXHISTORY}</maxHistory>
|
||||
<timeBasedFileNamingAndTriggeringPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>${FILESIZE}</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 根Logger配置 -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<appender-ref ref="ERROR"/>
|
||||
<appender-ref ref="WARN"/>
|
||||
<appender-ref ref="INFO"/>
|
||||
<appender-ref ref="DEBUG"/>
|
||||
<appender-ref ref="TRACE"/>
|
||||
<appender-ref ref="JSON_FILE"/>
|
||||
</root>
|
||||
|
||||
<!-- 可选:为特定包设置日志级别 -->
|
||||
<!--
|
||||
<logger name="app" level="DEBUG"/>
|
||||
<logger name="org.aikrai.vertx" level="DEBUG"/>
|
||||
-->
|
||||
</configuration>
|
||||
|
||||
@ -2,12 +2,13 @@ plugins {
|
||||
kotlin("jvm") version "1.9.20"
|
||||
id("com.diffplug.spotless") version "6.25.0"
|
||||
id("com.github.johnrengelman.shadow") version "8.1.1"
|
||||
`java-library`
|
||||
}
|
||||
|
||||
group = "org.aikrai"
|
||||
version = "1.0.0-SNAPSHOT"
|
||||
|
||||
val vertxVersion = "4.5.11"
|
||||
val vertxVersion = "4.5.14"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@ -47,29 +48,28 @@ spotless {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20")
|
||||
implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
||||
implementation("io.vertx:vertx-core:$vertxVersion")
|
||||
implementation("io.vertx:vertx-web:$vertxVersion")
|
||||
implementation("io.vertx:vertx-config:$vertxVersion")
|
||||
implementation("io.vertx:vertx-config-yaml:$vertxVersion")
|
||||
implementation("io.vertx:vertx-sql-client-templates:$vertxVersion")
|
||||
implementation("io.vertx:vertx-auth-jwt:$vertxVersion")
|
||||
api("org.jetbrains.kotlin:kotlin-reflect:1.9.20")
|
||||
api("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
||||
api("io.vertx:vertx-core:$vertxVersion")
|
||||
api("io.vertx:vertx-web:$vertxVersion")
|
||||
api("io.vertx:vertx-config:$vertxVersion")
|
||||
api("io.vertx:vertx-config-yaml:$vertxVersion")
|
||||
api("io.vertx:vertx-sql-client-templates:$vertxVersion")
|
||||
api("io.vertx:vertx-auth-jwt:$vertxVersion")
|
||||
|
||||
implementation("com.google.inject:guice:7.0.0")
|
||||
implementation("org.reflections:reflections:0.10.2")
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
|
||||
api("com.google.inject:guice:7.0.0")
|
||||
api("org.reflections:reflections:0.10.2")
|
||||
api("com.fasterxml.jackson.core:jackson-databind:2.15.2")
|
||||
api("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
|
||||
|
||||
// hutool
|
||||
implementation("cn.hutool:hutool-core:5.8.35")
|
||||
api("cn.hutool:hutool-core:5.8.35")
|
||||
|
||||
// log
|
||||
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
|
||||
implementation("org.slf4j:slf4j-api:2.0.6")
|
||||
implementation("ch.qos.logback:logback-classic:1.4.14")
|
||||
implementation("org.codehaus.janino:janino:3.1.8")
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
|
||||
implementation("org.slf4j:slf4j-api:2.0.17")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.18")
|
||||
|
||||
// doc
|
||||
implementation("io.swagger.core.v3:swagger-core:2.2.27")
|
||||
api("io.swagger.core.v3:swagger-core:2.2.27")
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ object Config {
|
||||
val map = configMapRef.get()
|
||||
val subMap = map.filterKeys { it.startsWith("$keyPrefix.") }
|
||||
.mapKeys { it.key.removePrefix("$keyPrefix.") }
|
||||
return if (subMap.isEmpty()) null else subMap
|
||||
return subMap.ifEmpty { null }
|
||||
}
|
||||
|
||||
fun getStringList(key: String, defaultValue: List<String> = emptyList()): List<String> {
|
||||
|
||||
@ -11,10 +11,6 @@ import com.google.inject.Singleton
|
||||
*/
|
||||
class FrameworkConfigModule : AbstractModule() {
|
||||
|
||||
override fun configure() {
|
||||
// 这里不需要bind(Config::class.java),因为Config是object
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabaseConfig(): DatabaseConfig {
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
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: Throwable) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
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: Throwable)
|
||||
}
|
||||
@ -10,10 +10,10 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.aikrai.vertx.auth.*
|
||||
import org.aikrai.vertx.auth.AuthUser.Companion.validateAuth
|
||||
import org.aikrai.vertx.config.resp.DefaultResponseHandler
|
||||
import org.aikrai.vertx.config.resp.ResponseHandlerInterface
|
||||
import org.aikrai.vertx.db.annotation.EnumValue
|
||||
import org.aikrai.vertx.jackson.JsonUtil
|
||||
import org.aikrai.vertx.resp.DefaultResponseHandler
|
||||
import org.aikrai.vertx.resp.ResponseHandlerInterface
|
||||
import org.aikrai.vertx.utlis.ClassUtil
|
||||
import org.aikrai.vertx.utlis.Meta
|
||||
import org.reflections.Reflections
|
||||
@ -214,9 +214,10 @@ class RouterBuilder(
|
||||
} else {
|
||||
routeInfo.kFunction.call(instance, *params)
|
||||
}
|
||||
responseHandler.normal(ctx, result, routeInfo.customizeResp)
|
||||
responseHandler.handle(ctx, result, routeInfo.customizeResp)
|
||||
} catch (e: Throwable) {
|
||||
responseHandler.exception(ctx, e)
|
||||
// 异常冒泡到全局错误处理器
|
||||
ctx.fail(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import io.vertx.sqlclient.SqlConnection
|
||||
import io.vertx.sqlclient.Transaction
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import mu.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.utlis.Meta
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@ -4,7 +4,7 @@ import io.vertx.kotlin.coroutines.coAwait
|
||||
import io.vertx.sqlclient.Row
|
||||
import io.vertx.sqlclient.SqlClient
|
||||
import io.vertx.sqlclient.templates.SqlTemplate
|
||||
import mu.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.jackson.JsonUtil
|
||||
import org.aikrai.vertx.utlis.Meta
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory
|
||||
import io.vertx.kotlin.coroutines.coAwait
|
||||
import io.vertx.sqlclient.*
|
||||
import io.vertx.sqlclient.templates.SqlTemplate
|
||||
import mu.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.db.annotation.*
|
||||
import org.aikrai.vertx.db.tx.TxCtxElem
|
||||
import org.aikrai.vertx.jackson.JsonUtil
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
package org.aikrai.vertx.http
|
||||
|
||||
import com.google.inject.Singleton
|
||||
import io.vertx.core.Handler
|
||||
import io.vertx.core.http.HttpHeaders
|
||||
import io.vertx.ext.web.RoutingContext
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.constant.HttpStatus
|
||||
import org.aikrai.vertx.jackson.JsonUtil
|
||||
import org.aikrai.vertx.utlis.Meta
|
||||
import org.slf4j.MDC
|
||||
|
||||
/**
|
||||
* 全局错误处理器,负责捕获并处理所有未捕获的异常
|
||||
*/
|
||||
@Singleton
|
||||
class GlobalErrorHandler : Handler<RoutingContext> {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
override fun handle(ctx: RoutingContext) {
|
||||
val failure = ctx.failure()
|
||||
val statusCode = determineStatusCode(ctx, failure)
|
||||
val requestId = ctx.get<String>("requestId") ?: "N/A"
|
||||
|
||||
// 记录错误日志
|
||||
logError(ctx, failure, statusCode, requestId)
|
||||
|
||||
// 构建标准错误响应
|
||||
val apiResponse = buildErrorResponse(failure, statusCode)
|
||||
apiResponse.requestId = requestId
|
||||
|
||||
// 发送响应
|
||||
if (!ctx.response().ended()) {
|
||||
val responseJson = try {
|
||||
JsonUtil.toJsonStr(apiResponse)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "序列化错误响应失败 (请求ID: $requestId)" }
|
||||
// 回退到简单JSON
|
||||
"""{"code":500,"message":"内部服务器错误 - 无法序列化错误响应","data":null,"requestId":"$requestId","timestamp":${System.currentTimeMillis()}}"""
|
||||
}
|
||||
|
||||
ctx.put("responseData", responseJson) // 存储响应内容用于日志
|
||||
|
||||
ctx.response()
|
||||
.setStatusCode(statusCode)
|
||||
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.end(responseJson)
|
||||
} else {
|
||||
logger.warn { "请求 ${ctx.request().uri()} 的响应已结束 (请求ID: $requestId)" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定HTTP状态码
|
||||
*/
|
||||
private fun determineStatusCode(ctx: RoutingContext, failure: Throwable?): Int {
|
||||
// 优先使用RoutingContext中设置的状态码
|
||||
if (ctx.statusCode() >= 400) {
|
||||
return ctx.statusCode()
|
||||
}
|
||||
|
||||
// 根据异常类型确定状态码
|
||||
return when (failure) {
|
||||
is Meta -> HttpStatusMapping.getCode(failure.name, HttpStatus.ERROR)
|
||||
is IllegalArgumentException -> HttpStatus.BAD_REQUEST
|
||||
// 可添加更多异常类型的映射
|
||||
else -> HttpStatus.ERROR // 默认为500
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建标准错误响应
|
||||
*/
|
||||
private fun buildErrorResponse(failure: Throwable?, statusCode: Int): RespBean<Any?> {
|
||||
return when (failure) {
|
||||
null -> RespBean.error(statusCode, "发生未知错误")
|
||||
is Meta -> RespBean.fromException(failure, statusCode)
|
||||
else -> RespBean.fromException(failure, statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
*/
|
||||
private fun logError(ctx: RoutingContext, failure: Throwable?, statusCode: Int, requestId: String) {
|
||||
val request = ctx.request()
|
||||
val uri = request.uri()
|
||||
val method = request.method().name()
|
||||
val remoteAddr = request.remoteAddress()?.host()
|
||||
|
||||
// 将请求ID放入MDC
|
||||
MDC.put("requestId", requestId)
|
||||
|
||||
try {
|
||||
val logMessage = buildString {
|
||||
append("处理请求失败 - ")
|
||||
append("请求ID: $requestId, ")
|
||||
append("方法: $method, ")
|
||||
append("URI: $uri, ")
|
||||
append("客户端IP: $remoteAddr, ")
|
||||
append("状态码: $statusCode")
|
||||
if (failure != null) {
|
||||
append(", 异常类型: ${failure::class.java.name}")
|
||||
if (!failure.message.isNullOrBlank()) {
|
||||
append(", 消息: ${failure.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态码选择日志级别
|
||||
if (statusCode >= 500 && failure != null) {
|
||||
logger.error(failure) { logMessage } // 记录带堆栈的日志
|
||||
} else {
|
||||
logger.warn { logMessage } // 400级别错误只记录警告
|
||||
}
|
||||
} finally {
|
||||
// 清理MDC
|
||||
MDC.remove("requestId")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,166 @@
|
||||
package org.aikrai.vertx.http
|
||||
|
||||
import cn.hutool.core.lang.Snowflake
|
||||
import com.google.inject.Inject
|
||||
import io.vertx.core.Handler
|
||||
import io.vertx.core.http.HttpHeaders
|
||||
import io.vertx.core.http.HttpMethod
|
||||
import io.vertx.core.json.JsonObject
|
||||
import io.vertx.ext.web.RoutingContext
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.aikrai.vertx.jackson.JsonUtil
|
||||
import org.aikrai.vertx.utlis.IpUtil
|
||||
import org.slf4j.MDC
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* 请求日志处理器,负责生成请求ID,记录请求和响应的详细信息
|
||||
*/
|
||||
class RequestLogHandler @Inject constructor(
|
||||
private val snowflake: Snowflake
|
||||
) : Handler<RoutingContext> {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
override fun handle(ctx: RoutingContext) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
// 生成请求ID
|
||||
val requestId = snowflake.nextIdStr()
|
||||
ctx.put("requestId", requestId)
|
||||
|
||||
// 将请求ID放入MDC
|
||||
MDC.put("requestId", requestId)
|
||||
|
||||
// 记录基本请求信息
|
||||
val request = ctx.request()
|
||||
val method = request.method()
|
||||
val path = request.path()
|
||||
val remoteAddr = IpUtil.getIpAddr(request) // 使用工具类获取真实IP
|
||||
|
||||
// 将基本信息放入MDC
|
||||
MDC.put("method", method.name())
|
||||
MDC.put("path", path)
|
||||
MDC.put("remoteAddr", remoteAddr)
|
||||
|
||||
// 记录开始日志
|
||||
logger.info { "请求开始 - 方法: $method, 路径: $path, 客户端IP: $remoteAddr, 请求ID: $requestId" }
|
||||
|
||||
// 在请求结束时记录详细日志
|
||||
ctx.response().endHandler {
|
||||
val endTime = System.currentTimeMillis()
|
||||
val duration = endTime - startTime
|
||||
val response = ctx.response()
|
||||
val statusCode = response.statusCode
|
||||
|
||||
try {
|
||||
// 构建详细日志数据
|
||||
val logData = JsonObject()
|
||||
.put("timestamp", Instant.ofEpochMilli(endTime).toString())
|
||||
.put("requestId", requestId)
|
||||
.put("method", method.name())
|
||||
.put("uri", request.uri())
|
||||
.put("path", path)
|
||||
.put("statusCode", statusCode)
|
||||
.put("durationMs", duration)
|
||||
.put("remoteAddr", remoteAddr)
|
||||
.put("userAgent", request.getHeader(HttpHeaders.USER_AGENT))
|
||||
|
||||
// 尝试获取用户信息
|
||||
val userId = ctx.user()?.principal()?.getString("sub")
|
||||
if (userId != null) {
|
||||
logData.put("userId", userId)
|
||||
MDC.put("userId", userId)
|
||||
}
|
||||
|
||||
// 视请求方法,可能记录查询参数
|
||||
if (method == HttpMethod.GET || method == HttpMethod.DELETE) {
|
||||
val queryParams = request.params().iterator().asSequence()
|
||||
.map { it.key to it.value }
|
||||
.toMap()
|
||||
if (queryParams.isNotEmpty()) {
|
||||
logData.put("queryParams", JsonObject(queryParams))
|
||||
}
|
||||
}
|
||||
|
||||
// 根据内容类型,可能记录请求体(小心处理敏感信息)
|
||||
if (method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.PATCH) {
|
||||
val contentType = request.getHeader(HttpHeaders.CONTENT_TYPE)
|
||||
if (contentType?.contains("application/json") == true) {
|
||||
val body = ctx.body().asString()
|
||||
if (!body.isNullOrBlank()) {
|
||||
try {
|
||||
val bodyJson = JsonObject(body)
|
||||
// 处理敏感字段,如密码
|
||||
val sanitizedBody = sanitizeSensitiveData(bodyJson)
|
||||
logData.put("requestBody", sanitizedBody)
|
||||
} catch (e: Exception) {
|
||||
logData.put("requestBodyRaw", "无法解析为JSON: " + body.take(100) + "...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试获取和记录响应数据
|
||||
val responseData = ctx.get<String>("responseData")
|
||||
if (!responseData.isNullOrBlank()) {
|
||||
try {
|
||||
val responseJson = JsonObject(responseData)
|
||||
logData.put("responseBody", responseJson)
|
||||
} catch (e: Exception) {
|
||||
logData.put("responseBodyRaw", responseData.take(100) + "...")
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态码选择日志级别
|
||||
MDC.put("statusCode", statusCode.toString())
|
||||
MDC.put("duration", duration.toString())
|
||||
|
||||
val logMessage = buildString {
|
||||
append("请求完成 - ")
|
||||
append("方法: $method, ")
|
||||
append("路径: $path, ")
|
||||
append("状态码: $statusCode, ")
|
||||
append("耗时: ${duration}ms, ")
|
||||
append("请求ID: $requestId")
|
||||
}
|
||||
|
||||
when {
|
||||
statusCode >= 500 -> logger.error { logMessage }
|
||||
statusCode >= 400 -> logger.warn { logMessage }
|
||||
else -> logger.info { logMessage }
|
||||
}
|
||||
|
||||
// 以JSON格式记录详细信息(可根据需要启用)
|
||||
logger.debug { "请求详细信息: ${JsonUtil.toJsonStr(logData)}" }
|
||||
} finally {
|
||||
// 清理MDC
|
||||
MDC.remove("requestId")
|
||||
MDC.remove("method")
|
||||
MDC.remove("path")
|
||||
MDC.remove("remoteAddr")
|
||||
MDC.remove("userId")
|
||||
MDC.remove("statusCode")
|
||||
MDC.remove("duration")
|
||||
}
|
||||
}
|
||||
|
||||
// 继续下一个处理器
|
||||
ctx.next()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理敏感数据,避免在日志中记录敏感信息
|
||||
*/
|
||||
private fun sanitizeSensitiveData(json: JsonObject): JsonObject {
|
||||
val result = json.copy()
|
||||
val sensitiveFields = listOf("password", "passwordConfirm", "oldPassword", "newPassword", "token", "accessToken", "refreshToken")
|
||||
|
||||
for (field in sensitiveFields) {
|
||||
if (result.containsKey(field)) {
|
||||
result.put(field, "******")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
150
vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RespBean.kt
Normal file
150
vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RespBean.kt
Normal file
@ -0,0 +1,150 @@
|
||||
package org.aikrai.vertx.http
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import org.aikrai.vertx.constant.HttpStatus
|
||||
import org.aikrai.vertx.utlis.Meta
|
||||
|
||||
/**
|
||||
* 标准API响应格式,用于所有HTTP响应
|
||||
*
|
||||
* @param code 状态码
|
||||
* @param message 消息
|
||||
* @param data 数据(可为null)
|
||||
* @param requestId 请求ID(用于跟踪请求)
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
data class RespBean<T>(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val data: T? = null,
|
||||
var requestId: String? = null,
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* 创建成功响应
|
||||
*
|
||||
* @param data 响应数据
|
||||
* @param message 成功消息
|
||||
* @param code 状态码,默认为HttpStatus.SUCCESS
|
||||
* @return ApiResponse实例
|
||||
*/
|
||||
fun <T> success(data: T? = null, message: String = "Success", code: Int = HttpStatus.SUCCESS): RespBean<T> {
|
||||
val finalCode = if (data == null && code == HttpStatus.SUCCESS) HttpStatus.NO_CONTENT else code
|
||||
return RespBean(
|
||||
code = finalCode,
|
||||
message = message,
|
||||
data = data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误响应
|
||||
*
|
||||
* @param code 错误码
|
||||
* @param message 错误消息
|
||||
* @param data 错误相关数据(可选)
|
||||
* @return ApiResponse实例
|
||||
*/
|
||||
fun <T> error(code: Int = HttpStatus.ERROR, message: String, data: T? = null): RespBean<T> {
|
||||
return RespBean(
|
||||
code = code,
|
||||
message = message,
|
||||
data = data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从异常创建错误响应
|
||||
*
|
||||
* @param exception 异常
|
||||
* @param defaultStatusCode 默认状态码,如果无法从异常确定状态码
|
||||
* @return ApiResponse实例
|
||||
*/
|
||||
fun <T> fromException(
|
||||
exception: Throwable,
|
||||
defaultStatusCode: Int = HttpStatus.ERROR
|
||||
): RespBean<T> {
|
||||
// 确定状态码和消息
|
||||
var statusCode = defaultStatusCode
|
||||
val errorName: String
|
||||
val errorMessage: String
|
||||
val errorData: Any?
|
||||
|
||||
when (exception) {
|
||||
is Meta -> {
|
||||
// 根据Meta.name确定状态码
|
||||
statusCode = when (exception.name) {
|
||||
"Unauthorized" -> HttpStatus.UNAUTHORIZED
|
||||
"Forbidden" -> HttpStatus.FORBIDDEN
|
||||
"NotFound" -> HttpStatus.NOT_FOUND
|
||||
"RequiredArgument", "InvalidArgument", "BadRequest" -> HttpStatus.BAD_REQUEST
|
||||
"Timeout" -> HttpStatus.ERROR // 使用ERROR作为超时状态码
|
||||
"NotSupported" -> HttpStatus.UNSUPPORTED_TYPE
|
||||
"Unimplemented" -> HttpStatus.NOT_IMPLEMENTED
|
||||
else -> defaultStatusCode
|
||||
}
|
||||
errorName = exception.name
|
||||
errorMessage = exception.message
|
||||
errorData = exception.data
|
||||
}
|
||||
is IllegalArgumentException -> {
|
||||
statusCode = HttpStatus.BAD_REQUEST
|
||||
errorName = "BadRequest"
|
||||
errorMessage = exception.message ?: "Invalid argument"
|
||||
errorData = null
|
||||
}
|
||||
else -> {
|
||||
// 通用异常处理
|
||||
errorName = exception.javaClass.simpleName
|
||||
errorMessage = exception.message ?: "Internal Server Error"
|
||||
errorData = null
|
||||
}
|
||||
}
|
||||
|
||||
// 组合错误名称和消息
|
||||
val finalMessage = if (errorMessage.contains(errorName, ignoreCase = true)) {
|
||||
errorMessage
|
||||
} else {
|
||||
"$errorName: $errorMessage"
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return RespBean(
|
||||
code = statusCode,
|
||||
message = finalMessage,
|
||||
data = errorData as? T
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP状态码映射,用于将Meta.name映射到HTTP状态码
|
||||
*/
|
||||
object HttpStatusMapping {
|
||||
private val mapping = mapOf(
|
||||
"Unauthorized" to HttpStatus.UNAUTHORIZED,
|
||||
"Forbidden" to HttpStatus.FORBIDDEN,
|
||||
"NotFound" to HttpStatus.NOT_FOUND,
|
||||
"RequiredArgument" to HttpStatus.BAD_REQUEST,
|
||||
"InvalidArgument" to HttpStatus.BAD_REQUEST,
|
||||
"BadRequest" to HttpStatus.BAD_REQUEST,
|
||||
"Timeout" to HttpStatus.ERROR, // 使用ERROR作为超时状态码
|
||||
"Repository" to HttpStatus.ERROR,
|
||||
"Unimplemented" to HttpStatus.NOT_IMPLEMENTED,
|
||||
"NotSupported" to HttpStatus.UNSUPPORTED_TYPE
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取与Meta.name对应的HTTP状态码
|
||||
*
|
||||
* @param name Meta.name或其前缀
|
||||
* @param defaultCode 默认状态码
|
||||
* @return HTTP状态码
|
||||
*/
|
||||
fun getCode(name: String, defaultCode: Int = HttpStatus.ERROR): Int {
|
||||
// 检查是否包含前缀,如"Repository:"
|
||||
val baseName = name.substringBefore(':')
|
||||
return mapping[baseName] ?: mapping[name] ?: defaultCode
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package org.aikrai.vertx.resp
|
||||
|
||||
import io.vertx.core.http.HttpHeaders
|
||||
import io.vertx.ext.web.RoutingContext
|
||||
import org.aikrai.vertx.http.RespBean
|
||||
import org.aikrai.vertx.jackson.JsonUtil
|
||||
|
||||
/**
|
||||
* 默认响应处理器实现
|
||||
*/
|
||||
class DefaultResponseHandler : ResponseHandlerInterface {
|
||||
|
||||
/**
|
||||
* 处理成功响应
|
||||
*/
|
||||
override suspend fun handle(
|
||||
ctx: RoutingContext,
|
||||
responseData: Any?,
|
||||
customizeResponse: Boolean
|
||||
) {
|
||||
val requestId = ctx.get<String>("requestId")
|
||||
|
||||
// 使用RespBean包装响应数据
|
||||
val respBean = RespBean.success(responseData)
|
||||
respBean.requestId = requestId
|
||||
|
||||
val resStr = JsonUtil.toJsonStr(respBean)
|
||||
// 存储响应内容用于日志
|
||||
ctx.put("responseData", resStr)
|
||||
|
||||
// 如果需要自定义响应,则不发送标准响应
|
||||
if (customizeResponse) return
|
||||
|
||||
// 发送标准成功响应
|
||||
ctx.response()
|
||||
.setStatusCode(respBean.code)
|
||||
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.end(resStr)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package org.aikrai.vertx.resp
|
||||
|
||||
import io.vertx.ext.web.RoutingContext
|
||||
|
||||
/**
|
||||
* 响应处理器接口,负责处理API响应
|
||||
*/
|
||||
interface ResponseHandlerInterface {
|
||||
/**
|
||||
* 处理成功响应
|
||||
*
|
||||
* @param ctx 路由上下文
|
||||
* @param responseData 响应数据
|
||||
* @param customizeResponse 是否自定义响应,如果为true则由控制器自行处理响应
|
||||
*/
|
||||
suspend fun handle(ctx: RoutingContext, responseData: Any?, customizeResponse: Boolean = false)
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package org.aikrai.vertx.utlis
|
||||
|
||||
import io.vertx.core.MultiMap
|
||||
import mu.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import java.sql.Timestamp
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user