From 394161415cf44cb0cb291e8b9fddefe70ee8dde5 Mon Sep 17 00:00:00 2001 From: AiKrai Date: Mon, 28 Apr 2025 09:55:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(vertx-fw):=E9=87=8D=E6=9E=84=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/app/config/InjectConfig.kt | 22 ++- .../src/main/kotlin/app/config/RespBean.kt | 106 +++++------ .../config/auth/JwtAuthenticationHandler.kt | 50 ------ .../kotlin/app/config/auth/ResponseHandler.kt | 65 ------- .../app/config/handler/JwtAuthHandler.kt | 68 +++++++ .../app/config/handler/ResponseHandler.kt | 52 ++++++ .../config/{db => provider}/DbPoolProvider.kt | 2 +- .../{auth => provider}/JWTAuthProvider.kt | 4 +- .../kotlin/app/controller/Demo1Controller.kt | 2 + .../app/service/account/AccountService.kt | 2 +- .../{config => service}/auth/TokenService.kt | 6 +- .../main/kotlin/app/verticle/WebVerticle.kt | 125 +++---------- vertx-demo/src/main/resources/logback.xml | 98 ++++++++--- .../kotlin/org/aikrai/vertx/config/Config.kt | 2 +- .../vertx/config/FrameworkConfigModule.kt | 4 - .../config/resp/DefaultResponseHandler.kt | 43 ----- .../config/resp/ResponseHandlerInterface.kt | 8 - .../org/aikrai/vertx/context/RouterBuilder.kt | 9 +- .../aikrai/vertx/http/GlobalErrorHandler.kt | 121 +++++++++++++ .../aikrai/vertx/http/RequestLogHandler.kt | 166 ++++++++++++++++++ .../kotlin/org/aikrai/vertx/http/RespBean.kt | 150 ++++++++++++++++ .../vertx/resp/DefaultResponseHandler.kt | 40 +++++ .../vertx/resp/ResponseHandlerInterface.kt | 17 ++ 23 files changed, 796 insertions(+), 366 deletions(-) delete mode 100644 vertx-demo/src/main/kotlin/app/config/auth/JwtAuthenticationHandler.kt delete mode 100644 vertx-demo/src/main/kotlin/app/config/auth/ResponseHandler.kt create mode 100644 vertx-demo/src/main/kotlin/app/config/handler/JwtAuthHandler.kt create mode 100644 vertx-demo/src/main/kotlin/app/config/handler/ResponseHandler.kt rename vertx-demo/src/main/kotlin/app/config/{db => provider}/DbPoolProvider.kt (97%) rename vertx-demo/src/main/kotlin/app/config/{auth => provider}/JWTAuthProvider.kt (95%) rename vertx-demo/src/main/kotlin/app/{config => service}/auth/TokenService.kt (94%) delete mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/config/resp/DefaultResponseHandler.kt delete mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/config/resp/ResponseHandlerInterface.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/http/GlobalErrorHandler.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RequestLogHandler.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RespBean.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/resp/DefaultResponseHandler.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/resp/ResponseHandlerInterface.kt diff --git a/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt b/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt index bd72249..0c4f077 100644 --- a/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt +++ b/vertx-demo/src/main/kotlin/app/config/InjectConfig.kt @@ -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) } } \ No newline at end of file diff --git a/vertx-demo/src/main/kotlin/app/config/RespBean.kt b/vertx-demo/src/main/kotlin/app/config/RespBean.kt index 1923141..a0dfc06 100644 --- a/vertx-demo/src/main/kotlin/app/config/RespBean.kt +++ b/vertx-demo/src/main/kotlin/app/config/RespBean.kt @@ -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") +// } +// } +//} diff --git a/vertx-demo/src/main/kotlin/app/config/auth/JwtAuthenticationHandler.kt b/vertx-demo/src/main/kotlin/app/config/auth/JwtAuthenticationHandler.kt deleted file mode 100644 index 89938a1..0000000 --- a/vertx-demo/src/main/kotlin/app/config/auth/JwtAuthenticationHandler.kt +++ /dev/null @@ -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): Boolean { - for (pattern in excludePatterns) { - val regexPattern = pattern - .replace("**", ".+") - .replace("*", "[^/]+") - .replace("?", ".") - val isExclude = path.matches(regexPattern.toRegex()) - if (isExclude) return true - } - return false - } -} diff --git a/vertx-demo/src/main/kotlin/app/config/auth/ResponseHandler.kt b/vertx-demo/src/main/kotlin/app/config/auth/ResponseHandler.kt deleted file mode 100644 index e93f406..0000000 --- a/vertx-demo/src/main/kotlin/app/config/auth/ResponseHandler.kt +++ /dev/null @@ -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("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) - } -} diff --git a/vertx-demo/src/main/kotlin/app/config/handler/JwtAuthHandler.kt b/vertx-demo/src/main/kotlin/app/config/handler/JwtAuthHandler.kt new file mode 100644 index 0000000..2fb7f8a --- /dev/null +++ b/vertx-demo/src/main/kotlin/app/config/handler/JwtAuthHandler.kt @@ -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): Boolean { + for (pattern in excludePatterns) { + val regexPattern = pattern + .replace("**", ".+") + .replace("*", "[^/]+") + .replace("?", ".") + val isExclude = path.matches(regexPattern.toRegex()) + if (isExclude) return true + } + return false + } +} \ No newline at end of file diff --git a/vertx-demo/src/main/kotlin/app/config/handler/ResponseHandler.kt b/vertx-demo/src/main/kotlin/app/config/handler/ResponseHandler.kt new file mode 100644 index 0000000..4baec7c --- /dev/null +++ b/vertx-demo/src/main/kotlin/app/config/handler/ResponseHandler.kt @@ -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 mu.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("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) + } +} \ No newline at end of file diff --git a/vertx-demo/src/main/kotlin/app/config/db/DbPoolProvider.kt b/vertx-demo/src/main/kotlin/app/config/provider/DbPoolProvider.kt similarity index 97% rename from vertx-demo/src/main/kotlin/app/config/db/DbPoolProvider.kt rename to vertx-demo/src/main/kotlin/app/config/provider/DbPoolProvider.kt index 4d3942c..2d76052 100644 --- a/vertx-demo/src/main/kotlin/app/config/db/DbPoolProvider.kt +++ b/vertx-demo/src/main/kotlin/app/config/provider/DbPoolProvider.kt @@ -1,4 +1,4 @@ -package app.config.db +package app.config.provider import com.google.inject.Inject import com.google.inject.Provider diff --git a/vertx-demo/src/main/kotlin/app/config/auth/JWTAuthProvider.kt b/vertx-demo/src/main/kotlin/app/config/provider/JWTAuthProvider.kt similarity index 95% rename from vertx-demo/src/main/kotlin/app/config/auth/JWTAuthProvider.kt rename to vertx-demo/src/main/kotlin/app/config/provider/JWTAuthProvider.kt index 8fda9b2..5a967d7 100644 --- a/vertx-demo/src/main/kotlin/app/config/auth/JWTAuthProvider.kt +++ b/vertx-demo/src/main/kotlin/app/config/provider/JWTAuthProvider.kt @@ -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) } -} +} \ No newline at end of file diff --git a/vertx-demo/src/main/kotlin/app/controller/Demo1Controller.kt b/vertx-demo/src/main/kotlin/app/controller/Demo1Controller.kt index 130a750..a5fe7f6 100644 --- a/vertx-demo/src/main/kotlin/app/controller/Demo1Controller.kt +++ b/vertx-demo/src/main/kotlin/app/controller/Demo1Controller.kt @@ -10,6 +10,7 @@ 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") diff --git a/vertx-demo/src/main/kotlin/app/service/account/AccountService.kt b/vertx-demo/src/main/kotlin/app/service/account/AccountService.kt index 4950878..041de20 100644 --- a/vertx-demo/src/main/kotlin/app/service/account/AccountService.kt +++ b/vertx-demo/src/main/kotlin/app/service/account/AccountService.kt @@ -1,9 +1,9 @@ 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 diff --git a/vertx-demo/src/main/kotlin/app/config/auth/TokenService.kt b/vertx-demo/src/main/kotlin/app/service/auth/TokenService.kt similarity index 94% rename from vertx-demo/src/main/kotlin/app/config/auth/TokenService.kt rename to vertx-demo/src/main/kotlin/app/service/auth/TokenService.kt index 44f4c36..6c8b90b 100644 --- a/vertx-demo/src/main/kotlin/app/config/auth/TokenService.kt +++ b/vertx-demo/src/main/kotlin/app/service/auth/TokenService.kt @@ -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 @@ -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, expires: Int? = null): String { val jwtOptions = JWTOptions().setExpiresInSeconds(expires ?: (60 * 60 * 24 * 7)) diff --git a/vertx-demo/src/main/kotlin/app/verticle/WebVerticle.kt b/vertx-demo/src/main/kotlin/app/verticle/WebVerticle.kt index 2a928da..0a42062 100644 --- a/vertx-demo/src/main/kotlin/app/verticle/WebVerticle.kt +++ b/vertx-demo/src/main/kotlin/app/verticle/WebVerticle.kt @@ -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 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 { 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 { 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("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("responseData")}] - |>>>>>耗时:[$timeCost] - """.trimMargin() - } else { - """ - | - |>>>>>请求ID:[${ctx.get("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("responseData")}] - |>>>>>耗时:[$timeCost] - """.trimMargin() - } - logger.info(logContent) - } - ctx.next() - } } diff --git a/vertx-demo/src/main/resources/logback.xml b/vertx-demo/src/main/resources/logback.xml index 2d900fe..1099a1d 100644 --- a/vertx-demo/src/main/resources/logback.xml +++ b/vertx-demo/src/main/resources/logback.xml @@ -6,6 +6,10 @@ + + + + System.out @@ -14,15 +18,14 @@ + + ${ROOT}${APPNAME}-warn.log - [%-5level] %d [%thread] %class{36}.%M:%L - %m%n - + [%-5level] %d{ISO8601} [%thread] %logger{36} [${mdcPattern}] - %m%n - - - return level >= WARN; - + + WARN ACCEPT DENY @@ -36,15 +39,14 @@ + + ${ROOT}${APPNAME}-info.log - [%-5level] %d [%thread] %class{36}.%M:%L - %m%n - + [%-5level] %d{ISO8601} [%thread] %logger{36} [${mdcPattern}] - %m%n - - return level >= INFO; - + INFO ACCEPT DENY @@ -58,21 +60,21 @@ + + ${ROOT}${APPNAME}-debug.log [%-5level] %d [%thread] %class{36}.%M:%L - %m%n - - return level >= DEBUG; - + DEBUG ACCEPT DENY - ${ROOT}${APPNAME}-%d-info.%i.log + ${ROOT}${APPNAME}-%d-debug.%i.log ${MAXHISTORY} @@ -81,21 +83,21 @@ + + ${ROOT}${APPNAME}-trace.log [%-5level] %d [%thread] %class{36}.%M:%L - %m%n - - return level >= TRACE; - + TRACE ACCEPT DENY - ${ROOT}${APPNAME}-%d-info.%i.log + ${ROOT}${APPNAME}-%d-trace.%i.log ${MAXHISTORY} @@ -104,11 +106,67 @@ - + + + ${ROOT}${APPNAME}-json.log + + + + + + {"time":"%d{ISO8601}","level":"%level","thread":"%thread","logger":"%logger","mdc":{%mdc},"message":"%message"}%n + + + ${ROOT}${APPNAME}-%d-json.%i.log + ${MAXHISTORY} + + ${FILESIZE} + + + + + + + + ${ROOT}${APPNAME}-error.log + + [%-5level] %d{ISO8601} [%thread] %logger{36} [${mdcPattern}] - %m%n + + + ERROR + ACCEPT + DENY + + + ${ROOT}${APPNAME}-%d-error.%i.log + ${MAXHISTORY} + + ${FILESIZE} + + + + + + + + + + + diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt index e515982..7e22d89 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/Config.kt @@ -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 = emptyList()): List { diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/FrameworkConfigModule.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/FrameworkConfigModule.kt index 6be0a24..fde686c 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/FrameworkConfigModule.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/FrameworkConfigModule.kt @@ -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 { diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/resp/DefaultResponseHandler.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/resp/DefaultResponseHandler.kt deleted file mode 100644 index 9863cd8..0000000 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/resp/DefaultResponseHandler.kt +++ /dev/null @@ -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) - } - } -} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/resp/ResponseHandlerInterface.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/resp/ResponseHandlerInterface.kt deleted file mode 100644 index 2a26c82..0000000 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/config/resp/ResponseHandlerInterface.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/context/RouterBuilder.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/context/RouterBuilder.kt index 1596367..ebb6690 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/context/RouterBuilder.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/context/RouterBuilder.kt @@ -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) } } } diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/http/GlobalErrorHandler.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/http/GlobalErrorHandler.kt new file mode 100644 index 0000000..4545c2a --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/http/GlobalErrorHandler.kt @@ -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 mu.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 { + private val logger = KotlinLogging.logger {} + + override fun handle(ctx: RoutingContext) { + val failure = ctx.failure() + val statusCode = determineStatusCode(ctx, failure) + val requestId = ctx.get("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 { + 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") + } + } +} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RequestLogHandler.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RequestLogHandler.kt new file mode 100644 index 0000000..49cd6fc --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RequestLogHandler.kt @@ -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 mu.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 { + 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("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 + } +} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RespBean.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RespBean.kt new file mode 100644 index 0000000..5d7f32a --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/http/RespBean.kt @@ -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( + 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 success(data: T? = null, message: String = "Success", code: Int = HttpStatus.SUCCESS): RespBean { + 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 error(code: Int = HttpStatus.ERROR, message: String, data: T? = null): RespBean { + return RespBean( + code = code, + message = message, + data = data + ) + } + + /** + * 从异常创建错误响应 + * + * @param exception 异常 + * @param defaultStatusCode 默认状态码,如果无法从异常确定状态码 + * @return ApiResponse实例 + */ + fun fromException( + exception: Throwable, + defaultStatusCode: Int = HttpStatus.ERROR + ): RespBean { + // 确定状态码和消息 + 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 + } +} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/resp/DefaultResponseHandler.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/resp/DefaultResponseHandler.kt new file mode 100644 index 0000000..380d871 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/resp/DefaultResponseHandler.kt @@ -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("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) + } +} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/resp/ResponseHandlerInterface.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/resp/ResponseHandlerInterface.kt new file mode 100644 index 0000000..6b354b4 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/resp/ResponseHandlerInterface.kt @@ -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) +} \ No newline at end of file