2026-08-16 12:46:15 +08:00
|
|
|
|
/*
|
|
|
|
|
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
|
|
|
|
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
|
|
|
|
|
* 自定义 IRequestDelegateFactory:在结果派发时自动封装响应,
|
|
|
|
|
|
* 对齐 C# SimApiResponseFilter(IResultFilter)的行为:
|
|
|
|
|
|
* - 返回 SimApiBaseResponse 或其子类 → 原样输出
|
|
|
|
|
|
* - 返回 null/void(Unit)→ 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.*
|
2026-08-16 22:46:48 +08:00
|
|
|
|
import simapi.attributes.{SimApiAuth, OriginResponse}
|
2026-08-16 12:46:15 +08:00
|
|
|
|
import simapi.communications.*
|
2026-08-16 22:46:48 +08:00
|
|
|
|
import simapi.configurations.*
|
2026-08-16 12:46:15 +08:00
|
|
|
|
import simapi.controllers.*
|
2026-08-16 22:46:48 +08:00
|
|
|
|
import simapi.helpers.{SimApiError}
|
|
|
|
|
|
import simapi.interfaces.*
|
2026-08-16 12:46:15 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 自定义请求委托工厂:接管 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()
|
2026-08-16 22:46:48 +08:00
|
|
|
|
checkSimApiAuth()
|
2026-08-16 12:46:15 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-16 22:46:48 +08:00
|
|
|
|
/// 检查 @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: "需要登录")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-16 23:44:20 +08:00
|
|
|
|
// 2. 类型权限校验 → 403(对齐 C# Types.Intersect(loginInfo.Type).Any(),支持逗号分隔多类型)
|
2026-08-16 22:46:48 +08:00
|
|
|
|
if (!auth.`type`.isEmpty()) {
|
2026-08-16 23:44:20 +08:00
|
|
|
|
let requiredTypes = auth.`type`.split(",")
|
|
|
|
|
|
var matched = false
|
|
|
|
|
|
for (t in requiredTypes) {
|
|
|
|
|
|
if (loginItem._types.contains(t)) {
|
|
|
|
|
|
matched = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!matched) {
|
2026-08-16 22:46:48 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-16 12:46:15 +08:00
|
|
|
|
/// 模型绑定失败时写入 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) {
|
2026-08-16 22:46:48 +08:00
|
|
|
|
// @OriginResponse:跳过统一封装,原样输出(对齐 C# OnResultExecuting 遇注解直接 return)
|
|
|
|
|
|
var originResponse = false
|
|
|
|
|
|
for (item in actionDescriptor.endpointMetadata) {
|
|
|
|
|
|
if (item is OriginResponse) {
|
|
|
|
|
|
originResponse = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
2026-08-16 12:46:15 +08:00
|
|
|
|
}
|
2026-08-16 22:46:48 +08:00
|
|
|
|
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)
|
2026-08-16 12:46:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 根据 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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|