From e9978ea5e0a4bb676fb6f0e6ddd399adf6d19848 Mon Sep 17 00:00:00 2001 From: xRain Date: Tue, 18 Aug 2026 00:27:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=AE=9A=E4=B9=89=20FromBody?= =?UTF-8?q?=20=E7=BB=91=E5=AE=9A=E2=80=94=E2=80=94simapi=5Fserialization?= =?UTF-8?q?=20=E6=8C=89=E8=BF=90=E8=A1=8C=E6=97=B6=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=8F=8D=E5=BA=8F=E5=88=97=E5=8C=96=EF=BC=8CDTO=20=E5=85=8D=20?= =?UTF-8?q?@Serialization=20=E5=AE=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SimApiRequestDelegateFactory:无注解参数(FromBody)用 JsonSerializer.Deserialize(typeInfo, body) 反序列化(免宏、免约束),显式注解参数(Query/Form/Route/Header/Services)委托 soulsoft binder - 请求体只读一次并缓存到 context.items["SimApi:BodyCache"](body 流不可重读, 请求日志中间件与 FromBody 共用缓存) - 验证:/auth/login/admin 的 LoginAdminRequest(无宏)反序列化成功,走到业务密码校验 --- cjpm.lock | 18 ++-- src/helpers/SimApiRequestDelegateFactory.cj | 90 ++++++++++++++++++- src/middlewares/SimApiRequestLogMiddleware.cj | 3 +- 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/cjpm.lock b/cjpm.lock index 4e15e44..4292297 100644 --- a/cjpm.lock +++ b/cjpm.lock @@ -1,20 +1,20 @@ version = 0 [requires] - soulsoft_extensions_hosting = {version = "1.0.20260528"} soulsoft_web_http = {version = "1.0.20260528"} - soulsoft_web_hosting = {version = "1.0.20260528"} - soulsoft_web_routing = {version = "1.0.20260528"} + soulsoft_extensions_hosting = {version = "1.0.20260528"} soulsoft_extensions_options_configuration = {version = "1.0.20260528"} + soulsoft_serialization = {version = "1.0.20260528"} soulsoft_web_mvc = {version = "1.0.20260528"} - soulsoft_extensions_options = {version = "1.0.20260528"} - redis = {version = "1.0.20260627"} + soulsoft_web_routing = {version = "1.0.20260528"} + soulsoft_net_http = {version = "1.0.20260528"} + soulsoft_web_hosting = {version = "1.0.20260528"} soulsoft_web_cors = {version = "1.0.20260528"} soulsoft_identity_claims = {version = "1.0.20260528"} - soulsoft_extensions_logging_console = {version = "1.0.20260528"} - soulsoft_extensions_logging = {version = "1.0.20260528"} soulsoft_extensions_logging_configuration = {version = "1.0.20260528"} - soulsoft_serialization = {version = "1.0.20260528"} + redis = {version = "1.0.20260627"} soulsoft_extensions_configuration = {version = "1.0.20260528"} - soulsoft_net_http = {version = "1.0.20260528"} + soulsoft_extensions_logging_console = {version = "1.0.20260528"} + soulsoft_extensions_options = {version = "1.0.20260528"} soulsoft_extensions_injection = {version = "1.0.20260528"} + soulsoft_extensions_logging = {version = "1.0.20260528"} diff --git a/src/helpers/SimApiRequestDelegateFactory.cj b/src/helpers/SimApiRequestDelegateFactory.cj index edf508d..636291b 100644 --- a/src/helpers/SimApiRequestDelegateFactory.cj +++ b/src/helpers/SimApiRequestDelegateFactory.cj @@ -14,8 +14,10 @@ package simapi.helpers import std.collection.* +import std.io.* import std.reflect.* import soulsoft_web_http.* +import soulsoft_web_mvc.annotations.* import soulsoft_web_mvc.core.* import soulsoft_web_mvc.routing.* import soulsoft_web_mvc.controllers.* @@ -23,6 +25,7 @@ import soulsoft_web_mvc.modelBindings.* import soulsoft_web_mvc.abstractions.* import soulsoft_extensions_options.* import soulsoft_extensions_injection.* +import simapi_serialization.* import simapi.attributes.{SimApiAuth as SimApiAuthAttribute, OriginResponse} import simapi.communications.* import simapi.configurations.* @@ -60,8 +63,14 @@ struct SimApiActionInvoker { public func apply(): Unit { let controller = createControllerInstance() checkSimApiAuth() + // 预读并缓存请求体(body 流不可重读;若请求日志中间件已读,直接用其缓存) + if (!context.items.contains(BODY_CACHE_KEY)) { + context.items[BODY_CACHE_KEY] = readBody() + } let modelBindingContext = ActionBindingContext(context, actionDescriptor.actionFunction.parameters) - let boundParameters = modelBinder.bind(modelBindingContext) + // 自定义绑定:无注解参数(FromBody)用 simapi_serialization 反序列化, + // 显式注解参数(Query/Form/Route/Header/Services)委托 soulsoft binder。 + let boundParameters = bindParameters(modelBindingContext) if (!modelBindingContext.modelState.isValid) { handleInvalidModelState(modelBindingContext) } else { @@ -70,6 +79,85 @@ struct SimApiActionInvoker { } } + /// 请求体缓存键(body 流不可重读,读一次后缓存) + private static let BODY_CACHE_KEY = "SimApi:BodyCache" + + /// 绑定全部参数:FromBody 自实现(simapi_serialization),其余委托 soulsoft + private func bindParameters(context: ActionBindingContext): Array { + let params = context.parameters + if (params.size == 0) { + return [] + } + let bound = Array(params.size, repeat: ()) + // 存在显式参数时才委托 soulsoft 绑定(Query/Form/Route/Header/Services) + var hasExplicit = false + for (parameter in params) { + if (isExplicitlyBound(parameter)) { + hasExplicit = true + break + } + } + let soulsoftBound = if (hasExplicit) { modelBinder.bind(context) } else { Array(params.size, repeat: ()) } + for ((index, parameter) in params |> enumerate) { + if (isExplicitlyBound(parameter)) { + // Query/Form/Route/Header/Services → soulsoft + bound[index] = soulsoftBound[index] + } else { + // FromBody → simapi_serialization 按运行时类型反序列化(免 @Serialization 宏) + bound[index] = bindFromBody(context, parameter) + } + } + bound + } + + /// 参数是否显式指定绑定源(FromQuery/FromForm/FromRoute/FromHeader/FromServices) + private func isExplicitlyBound(parameter: ParameterInfo): Bool { + parameter.findAnnotation().isSome() || + parameter.findAnnotation().isSome() || + parameter.findAnnotation().isSome() || + parameter.findAnnotation().isSome() || + parameter.findAnnotation().isSome() + } + + /// 从请求体反序列化(simapi_serialization 按运行时类型,DTO 免标注) + private func bindFromBody(context: ActionBindingContext, parameter: ParameterInfo): Any { + let body = match (context.httpContext.items.get(BODY_CACHE_KEY)) { + case Some(v) => if (let s: String <- v) { s } else { "" } + case None => "" + } + if (body.isEmpty()) { + SimApiError.error(code: 400, message: "请求体不能为空") + } + try { + return JsonSerializer.Deserialize(parameter.typeInfo, body) + } catch (ex: Exception) { + SimApiError.error(code: 400, message: "请求体反序列化失败: ${ex.message}") + } + () + } + + /// 读取并重置请求体流(供后续业务读取) + private func readBody(): String { + try { + context.request.enableBuffering() + var buffer = Array(4096, repeat: 0) + var sb = StringBuilder() + var read = context.request.body.read(buffer) + while (read > 0) { + sb.appendFromUtf8(buffer.slice(0, read)) + read = context.request.body.read(buffer) + } + let text = sb.toString() + if (let seekable: Seekable <- context.request.body) { + seekable.seek(SeekPosition.Begin(0)) + } + text + } catch (ex: Exception) { + SimApiError.error(code: 400, message: "读取请求体失败: ${ex.message}") + } + "" + } + /// 检查 @SimApiAuth 注解并执行鉴权(对齐 C# SimApiAuthAttribute.OnActionExecuting): /// 未登录 401 → 类型权限 403 → 遍历执行 ISimApiAuthChecker private func checkSimApiAuth() { diff --git a/src/middlewares/SimApiRequestLogMiddleware.cj b/src/middlewares/SimApiRequestLogMiddleware.cj index 4c4b876..3082701 100644 --- a/src/middlewares/SimApiRequestLogMiddleware.cj +++ b/src/middlewares/SimApiRequestLogMiddleware.cj @@ -122,10 +122,11 @@ public class SimApiRequestLogMiddleware <: IMiddleware { read = context.request.body.read(buffer) } let bodyText = sb.toString() - // 重置流位置,供后续业务读取 + // 重置流位置,供后续业务读取;同时缓存 body(流可能不可重读) if (let seekable: Seekable <- context.request.body) { seekable.seek(SeekPosition.Begin(0)) } + context.items["SimApi:BodyCache"] = bodyText return truncateBody(bodyText) } catch (_: Exception) { return "(读取请求体失败)\n"