Files
simapi-cj/src/extensions/SimApiRequestDelegateFactory.cj
T
xrain 92eea3108a refactor: 对齐 simapi-net 的 20 项差异
- SimApiLoginItem._types 默认 [user]
- 新增 SimApiIdOnlyRequest
- errorWhenNone 改名 errorWhenNull
- SimApiAuth.update 保留 TTL(Redis ttl/expire)
- SimApiCache: set 接受 Any + 新增 get<T>/getString
- SimApiHttpClient 签名字段移出 Options
- SimApiUtil: md5/sha1 支持 mode + 新增 paginate
- @SimApiAuth 支持逗号分隔多类型
- Logger: 4 位毫秒 + IsEnabled 恒 true
- 删除 /versions 端点;exceptionHandler 改为抛错
- UseSimApi 中间件顺序对齐 .NET
- README 同步更新
2026-08-16 23:44:52 +08:00

207 lines
8.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 自定义 IRequestDelegateFactory:在结果派发时自动封装响应,
* 对齐 C# SimApiResponseFilterIResultFilter)的行为:
* - 返回 SimApiBaseResponse 或其子类 → 原样输出
* - 返回 null/voidUnit)→ SimApiBaseResponse(){code:200, message:成功}
* - 返回 String → SimApiResponse<String>data 为字符串)
* - 返回其他对象 → SimApiResponse<Any>data 为对象)
*
* 注册方式:必须在 soulsoft addControllers() 之前注册(tryAddSingleton 先到先得)。
*/
package simapi.extensions
import std.collection.*
import std.reflect.*
import soulsoft_web_http.*
import soulsoft_web_mvc.core.*
import soulsoft_web_mvc.routing.*
import soulsoft_web_mvc.controllers.*
import soulsoft_web_mvc.modelBindings.*
import soulsoft_web_mvc.abstractions.*
import soulsoft_extensions_options.*
import soulsoft_extensions_injection.*
import simapi.attributes.{SimApiAuth, OriginResponse}
import simapi.communications.*
import simapi.configurations.*
import simapi.controllers.*
import simapi.helpers.{SimApiError}
import simapi.interfaces.*
/**
* 自定义请求委托工厂:接管 soulsoft 的 ControllerRequestDelegateFactory
* 在结果派发时自动封装响应(对齐 C# SimApiResponseFilter)。
*/
public class SimApiRequestDelegateFactory <: IRequestDelegateFactory {
private let _mvcOptions: MvcOptions
private let _modelBinder: IActionModelBinder
public init(mvcOptions: IOptions<MvcOptions>, modelBinder: IActionModelBinder, services: IServiceProvider) {
_mvcOptions = mvcOptions.value
_modelBinder = modelBinder
}
public func createRequestDelegate(actionDescriptor: ControllerActionDescriptor): RequestDelegate {
return {
context => SimApiActionInvoker(context, _modelBinder, actionDescriptor, _mvcOptions).apply()
}
}
}
/**
* 单次请求的动作执行器(复制 soulsoft ControllerActionInvoker
* dispatchResult 改为自动封装响应)。
*/
struct SimApiActionInvoker {
SimApiActionInvoker(let context: HttpContext, let modelBinder: IActionModelBinder,
let actionDescriptor: ControllerActionDescriptor, let mvcOptions: MvcOptions) {
}
public func apply(): Unit {
let controller = createControllerInstance()
checkSimApiAuth()
let modelBindingContext = ActionBindingContext(context, actionDescriptor.actionFunction.parameters)
let boundParameters = modelBinder.bind(modelBindingContext)
if (!modelBindingContext.modelState.isValid) {
handleInvalidModelState(modelBindingContext)
} else {
let actionResult = actionDescriptor.actionFunction.apply(controller, boundParameters)
dispatchResult(actionResult)
}
}
/// 检查 @SimApiAuth 注解并执行鉴权(对齐 C# SimApiAuthAttribute.OnActionExecuting):
/// 未登录 401 → 类型权限 403 → 遍历执行 ISimApiAuthChecker
private func checkSimApiAuth() {
var auth: ?SimApiAuth = None
for (item in actionDescriptor.endpointMetadata) {
if (let a: SimApiAuth <- item) {
auth = Some(a)
break
}
}
if (let Some(auth) <- auth) {
// 1. 未登录 → 401
var loginItem: SimApiLoginItem = SimApiLoginItem("")
match (context.items.get("LoginInfo")) {
case Some(v) =>
if (let l: SimApiLoginItem <- v) {
loginItem = l
} else {
SimApiError.error(code: 401, message: "需要登录")
}
case None =>
SimApiError.error(code: 401, message: "需要登录")
}
// 2. 类型权限校验 → 403(对齐 C# Types.Intersect(loginInfo.Type).Any(),支持逗号分隔多类型)
if (!auth.`type`.isEmpty()) {
let requiredTypes = auth.`type`.split(",")
var matched = false
for (t in requiredTypes) {
if (loginItem._types.contains(t)) {
matched = true
break
}
}
if (!matched) {
SimApiError.error(code: 403, message: "无权访问")
}
}
// 3. 遍历执行 ISimApiAuthChecker(从 DI 按注册类型解析)
let token = match (context.items.get("LoginToken")) {
case Some(v) => if (let s: String <- v) { s } else { "" }
case None => ""
}
let options = context.services.getOrThrow<SimApiOptions>()
for (checkerType in options.authCheckers) {
let instance = context.services.getOrThrow(checkerType)
if (let checker: ISimApiAuthChecker <- instance) {
checker.run(loginItem, token)
}
}
}
}
/// 模型绑定失败时写入 ProblemDetails 响应
private func handleInvalidModelState(modelBindingContext: ActionBindingContext) {
let options = context.services.getOrThrow<IOptions<ApiBehaviorOptions>>()
if (let Some(factory) <- options.value.invalidModelStateResponseFactory) {
let actionContext = ActionContext(context, modelBindingContext.modelState)
factory(actionContext).invoke(context)
} else {
let details = createValidationProblemDetails(modelBindingContext)
if (let Some(status) <- details.status) {
context.response.statusCode = UInt16(status)
}
context.response.writeAsJson(details)
}
}
/// 结果派发 + 自动封装(对齐 C# SimApiResponseFilter
private func dispatchResult(actionResult: Any) {
// @OriginResponse:跳过统一封装,原样输出(对齐 C# OnResultExecuting 遇注解直接 return
var originResponse = false
for (item in actionDescriptor.endpointMetadata) {
if (item is OriginResponse) {
originResponse = true
break
}
}
if (originResponse) {
if (let s: String <- actionResult) {
// String 原样输出文本(对齐 C# string 返回直接写入)
context.response.contentType = "application/json; charset=utf-8"
context.response.write(s)
} else {
ObjectResult<Any>(actionResult).invoke(context)
}
return
}
// 统一封装(IActionResult/SimApiBaseResponse/String/Unit/其他对象)
SimApiResultWriter.write(context, actionResult)
}
/// 根据 ModelState 错误构建 ValidationProblemDetails
private func createValidationProblemDetails(modelBindingContext: ActionBindingContext) {
let details = ValidationProblemDetails()
if (hasUnsupportedContentTypeError(modelBindingContext.modelState)) {
details.`type` = "https://tools.ietf.org/html/rfc9110#section-15.5.16"
details.title = "Unsupported Media Type"
details.status = 415
} else {
details.`type` = "https://tools.ietf.org/html/rfc9110#section-15.5.1"
details.title = "One or more validation errors occurred."
details.status = 400
for ((name, entry) in modelBindingContext.modelState) {
details.errors.add(name, entry.errors |> map {f => f.description} |> collectArray)
}
}
return details
}
/// 检查 ModelState 中是否含有 UnsupportedContentTypeException 错误
private func hasUnsupportedContentTypeError(modelState: ModelStateDictionary) {
for ((_, entry) in modelState) {
for (error in entry.errors) {
if (error.exception.flatMap {f => f as UnsupportedContentTypeException}.isSome()) {
return true
}
}
}
return false
}
/// 通过 DI 容器实例化控制器,并注入当前 HttpContext
private func createControllerInstance(): Object {
let instance = ActivatorUtilities.createInstance(context.services, actionDescriptor.controllerType)
if (let controller: SimApiBaseController <- instance) {
controller.bindRequestContext(context)
}
return instance
}
}