重构: 源码文件蛇形命名, 接口去I头(BindRequestContext/SimApiAuthChecker), 清理C#相关说明, serialization依赖路径改 serialization-cj

This commit is contained in:
2026-08-25 13:43:26 +08:00
parent 829f009229
commit 30e5f165f2
56 changed files with 319 additions and 332 deletions
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*/
package simcu::simapi.helpers
@@ -14,21 +14,21 @@ import simcu::simapi.exceptions.*
import simcu::simapi.interfaces.*
/**
* AES body 请求({"data": "密文"},对齐 C# SimApiOneFieldRequest<string>)。
* AES body 请求({"data": "密文"})。
*/
public class AesBodyRequest {
public var data: String = ""
}
/**
* 服务端 AES body 解密校验器(对齐 C# ModelBinders/AesBodyModelBinder
* 服务端 AES body 解密校验器。
*
* 仓颉无 ModelBinder 机制,按项目惯例由控制器在方法开头调用:
* let jsonStr = SimApiAesBodyChecker.decryptBody(context, provider)
* let request = JsonSerializer.Deserialize<XxxRequest>(jsonStr)
* let request = JsonSerializer.deserialize<XxxRequest>(jsonStr)
* 或标注 @AesBody 注解自动执行(SimApiRequestDelegateFactory)。
*
* 流程(与 C# 一致)
* 流程:
* 1. 读取 body 并反序列化为 {"data": "密文"}
* 2. 校验 Data 非空
* 3. 提取 appIdQuery/Header
@@ -53,7 +53,7 @@ public class SimApiAesBodyChecker {
}
// 2. 反序列化 {"data": "密文"}
let req = JsonSerializer.Deserialize<AesBodyRequest>(body)
let req = JsonSerializer.deserialize<AesBodyRequest>(body)
if (req.data.isEmpty()) {
SimApiError.error(code: 400, message: "请求体缺少密文Data字段")
}
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*/
package simcu::simapi.helpers
@@ -11,9 +11,9 @@ import stdx.crypto.digest.*
import stdx.encoding.base64.*
/**
* AES-256-CBC + PKCS7 加解密工具(对齐 C# SimApiAesUtil
* AES-256-CBC + PKCS7 加解密工具。
*
* 约定(与 C# 完全一致)
* 约定:
* - 密钥:SHA256(key 字符串) → 32 字节
* - 模式:AES-256-CBCPKCS7 填充
* - IV:每次加密随机生成 16 字节,前置在密文前
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*/
package simcu::simapi.helpers
@@ -113,14 +113,14 @@ public class SimApiAuth {
let tokenKey = "${tokenCachePrefix}${token}"
let json = loginItemJson(loginItem)
if (let Some(redis) <- _redis) {
// 保留原过期时间(对齐 C# update 不刷新 TTL:先读旧 TTLSET 后重新续期
// 保留原过期时间:先读旧 TTL,SET 后重新续期
let ttl = redis.ttl(tokenKey)
redis.set(tokenKey, Blob.fromUtf8(json))
if (ttl > 0) {
redis.expire(tokenKey, ttl)
}
} else {
// 保留原过期时间(对齐 C# update 不刷新 TTL
// 保留原过期时间
let expireAt = match (_tokenStore.get(token)) {
case Some(entry) => entry.expireAt
case None => 0
@@ -162,10 +162,10 @@ public class SimApiAuth {
}
}
/// 解析登录信息;JSON 无效/非对象时返回 None(视为 token 无效,对齐 C# 返回 null
/// 解析登录信息;JSON 无效/非对象时返回 None(视为 token 无效)
private static func parseLoginItemSafe(json: String): ?SimApiLoginItem {
try {
Some(JsonSerializer.Deserialize<SimApiLoginItem>(json))
Some(JsonSerializer.deserialize<SimApiLoginItem>(json))
} catch (_: Exception) {
None
}
@@ -253,7 +253,7 @@ public class SimApiAuth {
}
private static func generateToken(): String {
// 对齐 C#token = Guid.NewGuid().ToString()(小写、8-4-4-4-12
// token = Guid.NewGuid().ToString()(小写、8-4-4-4-12
SimApiUtil.newGuid()
}
@@ -294,7 +294,7 @@ public class SimApiAuth {
}
private static func loginItemJson(item: SimApiLoginItem): String {
// 统一 JSON 序列化:对齐 .NET JsonSerializer.Serialize(item)
JsonSerializer.Serialize(item)
// 统一 JSON 序列化:JsonSerializer.Serialize(item)
JsonSerializer.serialize(item)
}
}
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*/
package simcu::simapi.helpers
@@ -28,7 +28,7 @@ private struct CacheEntry {
/**
* 缓存助手:Key 自动加前缀 "SimApi:Cache:"。
* 存储后端与 SimApiAuth 一致:配置了 Redis 用 Redis(可带密码/DB 索引),否则 InMemory。
* InMemory 模式同样支持过期(对齐 C# DistributedCache 的过期语义)
* InMemory 模式同样支持过期。
*/
public class SimApiCache {
private static let prefix = "SimApi:Cache:"
@@ -100,17 +100,17 @@ public class SimApiCache {
}
/**
* 获取特定类型缓存(对齐 C# Get<T>从 JSON 反序列化,使用 simapi_serialization)。
* 获取特定类型缓存(从 JSON 反序列化,使用 simapi_serialization)。
*/
public func get<T>(key: String): ?T {
match (getString(key)) {
case Some(json) => Some(JsonSerializer.Deserialize<T>(json))
case Some(json) => Some(JsonSerializer.deserialize<T>(json))
case None => None
}
}
/**
* 获取 string 类型缓存(对应 C# Get(string)Cangjie 不支持按泛型重载,故拆分为 getString/get<T>)。
* 获取 string 类型缓存(仓颉不支持按泛型重载,故拆分为 getString/get<T>)。
*/
public func getString(key: String): ?String {
if (let Some(redis) <- _redis) {
@@ -1,10 +1,10 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
* Helpers/SimApiControllerScanner:自动发现调用者包中的 MVC 控制器。
*
* 对齐 C# 的控制器发现机制:
* - C# 通过 StackTrace 获取调用程序集,再 Assembly.GetTypes() 扫描所有类型
* 控制器发现机制:
* - 通过 StackTrace 获取调用程序集,再 Assembly.GetTypes() 扫描所有类型
* - 仓颉版通过 Error.getStackTrace() 获取调用者包名,再 PackageInfo 枚举类型,
* 过滤出继承 Controller 的类型(含子包)
*/
@@ -35,20 +35,19 @@ public class SimApiControllerScanner {
}
/**
* 扫描调用者包及其所有子包中 ISimApiAuthChecker 的实现类。
* 对齐 C# AddSimApi 中遍历调用者程序集 AddScoped 注册 checker 的机制。
* 扫描调用者包及其所有子包中 SimApiAuthChecker 的实现类。
* @return 找到的 checker 实现类型列表(不含抽象类型与接口本身)。
*/
public static func scanAuthCheckers(): Array<TypeInfo> {
let callerPackage = getCallerPackage()
var result = ArrayList<TypeInfo>()
collectImplementations(callerPackage, TypeInfo.of<ISimApiAuthChecker>(), result)
collectImplementations(callerPackage, TypeInfo.of<SimApiAuthChecker>(), result)
result.toArray()
}
/**
* 获取调用者(应用)包名:遍历栈帧,跳过 simapi/soulsoft/std 等框架包,
* 返回第一个应用包的 declaringClass(对齐 C# 通过 StackTrace 找调用程序集)
* 返回第一个应用包的 declaringClass。
*/
public static func getCallerPackage(): String {
try {
@@ -1,8 +1,8 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*
* 错误抛出:提供「顶层函数」+「SimApiError 类」两种写法(对齐 C# using static SimApi.Helpers.SimApiError
* 错误抛出:提供「顶层函数」+「SimApiError 类」两种写法。
* - 顶层函数:import simcu::simapi.helpers.* 后可直接 error(400) / errorWhen(...),无需前缀
* - SimApiError.error(...):旧写法,保留兼容
*/
@@ -11,7 +11,7 @@ package simcu::simapi.helpers
import simcu::simapi.exceptions.*
// ===== 顶层函数(推荐用法:直接 error(400),对齐 C# using static =====
// ===== 顶层函数(推荐用法:直接 error(400) =====
/**
* 直接抛错。
@@ -47,17 +47,16 @@ public func errorWhenFalse(condition: Bool, code!: Int64 = 400, message!: String
}
/**
* 给定的可选值None 时抛错。
* 给定的可选值None 时抛错,否则返回解包后的值
*/
public func errorWhenNull(condition: ?Any, code!: Int64 = 404, message!: String = ""): Unit {
SimApiError.errorWhenNull(condition, code: code, message: message)
public func errorWhenNone<T>(value: ?T, code!: Int64 = 404, message!: String = ""): T {
SimApiError.errorWhenNone(value, code: code, message: message)
}
// ===== 兼容门面(旧写法 SimApiError.error(...) 仍可用,内部为真实实现) =====
/**
* 错误抛出辅助类:所有业务错误统一通过这里抛出 SimApiException。
* 对应 C# 的 SimApi.Helpers.SimApiError。
*/
public class SimApiError {
private init() {}
@@ -98,12 +97,12 @@ public class SimApiError {
}
/**
* 给定的可选值None 时抛错。
* 给定的可选值None 时抛错,否则返回解包后的值
*/
public static func errorWhenNull(condition: ?Any, code!: Int64 = 404, message!: String = ""): Unit {
match (condition) {
case None => error(code: code, message: message)
case _ => ()
public static func errorWhenNone<T>(value: ?T, code!: Int64 = 404, message!: String = ""): T {
match (value) {
case Some(v) => v
case None => throw SimApiException(code, message: message)
}
}
}
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*/
package simcu::simapi.helpers
@@ -17,8 +17,8 @@ import simcu::simapi.exceptions.*
/**
* HTTP 客户端:用于调用其他带签名/AES 的 SimApi 服务。
* 对齐 C# 的 SimApi.Helpers.SimApiHttpClient
* - 内部使用 stdx.net.http 的 HttpClient(等价 .NET 的 System.Net.Http.HttpClient
* 说明
* - 内部使用 stdx.net.http 的 HttpClient(等价 System.Net.Http.HttpClient
* - 返回泛型 T(反序列化响应 body 的 data 字段),不再返回 String
* @param T 响应 data 的数据类型(任意类,simapi_serialization 反射反序列化)。
*/
@@ -41,7 +41,7 @@ public open class SimApiHttpClient {
/**
* 发起签名请求(GET query 签名 + POST body)。
* 对齐 C# SignQuery<T>query 串 = SignFields + AppId + timestamp + nonce,整体拼 AppKey 取 MD5 作为 sign。
* SignQuery<T>query 串 = SignFields + AppId + timestamp + nonce,整体拼 AppKey 取 MD5 作为 sign。
* @param url 请求路径(相对路径,自动拼接 server)。
* @param body 请求体 JSON 字符串(可选)。
* @param queries 额外查询参数(可选)。
@@ -74,7 +74,7 @@ public open class SimApiHttpClient {
/**
* 发起 AES 加密请求:body 加密后放入 {"data": "..."} 提交。
* 对齐 C# AesQuery<T>SimApiOneFieldRequest<string> { Data = Encrypt(body, AppKey) }
* AesQuery<T>:对应 SimApiOneFieldRequest<string> { Data = Encrypt(body, AppKey) }。
* @param url 请求路径(相对路径,自动拼接 server)。
* @param body 请求体 JSON 字符串。
* @return 响应 data 字段反序列化后的 T。
@@ -91,7 +91,6 @@ public open class SimApiHttpClient {
/**
* 发起 AES 加密 + 签名请求。
* 对齐 C# AesSignQuery<T>。
* @param url 请求路径(相对路径,自动拼接 server)。
* @param body 请求体 JSON 字符串。
* @param queries 额外查询参数(可选)。
@@ -105,7 +104,7 @@ public open class SimApiHttpClient {
/**
* 发起 POST 请求并反序列化 SimApiResponse<T>,返回 data 字段。
* 对齐 C# Query<T>
* Query<T>
* ErrorWhenFalse(IsSuccessStatusCode) → ReadFromJsonAsync<SimApiResponse<T>> → ErrorWhen(Code != 200) → return Data。
* 注意:必须 noProxy(),否则会走系统代理(192.168.0.250:8118)导致连接被拒。
* 反序列化使用 simapi_serializationDeserialize<T> 免约束)。
@@ -128,7 +127,7 @@ public open class SimApiHttpClient {
SimApiError.errorWhenFalse(isSuccess(response.status), code: Int64(response.status),
message: "HTTP ERROR: ${response.status}")
let json = readBodyText(response.body)
let result = JsonSerializer.Deserialize<SimApiResponse<T>>(json)
let result = JsonSerializer.deserialize<SimApiResponse<T>>(json)
SimApiError.errorWhen(result.code != 200, code: result.code, message: result.message)
return result.data.getOrThrow()
} finally {
@@ -168,12 +167,12 @@ public open class SimApiHttpClient {
}
private func aesEncrypt(plain: String): String {
// 对齐 C#SimApiAesUtil.Encrypt(plain, AppKey)AES-256-CBC + PKCS7Base64(IV + 密文)
// SimApiAesUtil.Encrypt(plain, AppKey)AES-256-CBC + PKCS7Base64(IV + 密文)
SimApiAesUtil.encrypt(plain, appKey)
}
private static func generateNonce(): String {
// 对齐 C#nonce 直接用 Guid.NewGuid()
// nonce 直接用 Guid.NewGuid()
SimApiUtil.newGuid()
}
@@ -1,8 +1,8 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
* 自定义 IRequestDelegateFactory:在结果派发时自动封装响应,
* 对齐 C# SimApiResponseFilterIResultFilter)的行为:
* SimApiResponseFilterIResultFilter)的行为:
* - 返回 SimApiBaseResponse 或其子类 → 原样输出
* - 返回 null/voidUnit)→ SimApiBaseResponse(){code:200, message:成功}
* - 返回 String → SimApiResponse<String>data 为字符串)
@@ -35,7 +35,7 @@ import simcu::simapi.interfaces.*
/**
* 自定义请求委托工厂:接管 soulsoft 的 ControllerRequestDelegateFactory
* 在结果派发时自动封装响应(对齐 C# SimApiResponseFilter
* 在结果派发时自动封装响应。
*/
public class SimApiRequestDelegateFactory <: IRequestDelegateFactory {
private let _mvcOptions: MvcOptions
@@ -106,7 +106,7 @@ struct SimApiActionInvoker {
// Query/Form/Route/Header/Services → soulsoft
bound[index] = soulsoftBound[index]
} else if (let Some(aes) <- parameter.findAnnotation<AesBody>()) {
// @AesBody → 解密 body 后按参数类型反序列化(对齐 C# AesBodyModelBinder
// @AesBody → 解密 body 后按参数类型反序列化
bound[index] = bindAesBody(context, parameter, aes)
} else {
// FromBody → simapi_serialization 按运行时类型反序列化(免 @Serialization 宏)
@@ -135,14 +135,14 @@ struct SimApiActionInvoker {
SimApiError.error(code: 400, message: "请求体不能为空")
}
try {
return JsonSerializer.Deserialize(parameter.typeInfo, body)
return JsonSerializer.deserialize(parameter.typeInfo, body)
} catch (ex: Exception) {
SimApiError.error(code: 400, message: "请求体反序列化失败: ${ex.message}")
}
()
}
/// @AesBody 参数绑定:解密 body 后按参数类型反序列化(对齐 C# AesBodyModelBinder
/// @AesBody 参数绑定:解密 body 后按参数类型反序列化
private func bindAesBody(context: ActionBindingContext, parameter: ParameterInfo, aes: AesBody): Any {
// 1. 从 DI 解析 keyProviderAesBodyProviderBase 实现)
let provider = resolveAesProvider(aes.keyProvider)
@@ -150,7 +150,7 @@ struct SimApiActionInvoker {
let plain = SimApiAesBodyChecker.decryptBody(context.httpContext, provider)
// 3. 按参数类型反序列化明文 JSON
try {
return JsonSerializer.Deserialize(parameter.typeInfo, plain)
return JsonSerializer.deserialize(parameter.typeInfo, plain)
} catch (ex: Exception) {
SimApiError.error(code: 400, message: "AES body 反序列化失败: ${ex.message}")
}
@@ -176,7 +176,7 @@ struct SimApiActionInvoker {
AesBodyProviderBase()
}
/// 检查 @SimApiSign 注解并执行验签(对齐 C# SimApiSignAttribute.OnActionExecuting
/// 检查 @SimApiSign 注解并执行验签
private func checkSimApiSign() {
var sign: ?SimApiSign = None
for (item in actionDescriptor.endpointMetadata) {
@@ -241,8 +241,8 @@ struct SimApiActionInvoker {
""
}
/// 检查 @SimApiAuth 注解并执行鉴权(对齐 C# SimApiAuthAttribute.OnActionExecuting
/// 未登录 401 → 遍历执行 ISimApiAuthChecker → 类型权限 403
/// 检查 @SimApiAuth 注解并执行鉴权:
/// 未登录 401 → 遍历执行 SimApiAuthChecker → 类型权限 403
private func checkSimApiAuth() {
var auth: ?SimApiAuthAttribute = None
for (item in actionDescriptor.endpointMetadata) {
@@ -265,17 +265,17 @@ struct SimApiActionInvoker {
SimApiError.error(code: 401, message: "需要登录")
}
// 2. 遍历执行 ISimApiAuthChecker(对齐 C# GetServices<ISimApiAuthChecker>():一次解析全部实现)
// 2. 遍历执行 SimApiAuthChecker
let token = match (context.items.get("LoginToken")) {
case Some(v) => if (let s: String <- v) { s } else { "" }
case None => ""
}
let checkers = context.services.getAll<ISimApiAuthChecker>()
let checkers = context.services.getAll<SimApiAuthChecker>()
for (checker in checkers) {
checker.run(loginItem, token)
}
// 3. 类型权限校验 → 403(对齐 C# Types.Intersect(loginInfo.Type).Any(),支持逗号分隔多类型)
// 3. 类型权限校验 → 403
if (!auth.`type`.isEmpty()) {
let requiredTypes = auth.`type`.split(",")
var matched = false
@@ -312,9 +312,9 @@ struct SimApiActionInvoker {
}
}
/// 结果派发 + 自动封装(对齐 C# SimApiResponseFilter
/// 结果派发 + 自动封装
private func dispatchResult(actionResult: Any) {
// @OriginResponse:跳过统一封装,原样输出(对齐 C# OnResultExecuting 遇注解直接 return
// @OriginResponse:跳过统一封装,原样输出
var originResponse = false
for (item in actionDescriptor.endpointMetadata) {
if (item is OriginResponse) {
@@ -324,7 +324,7 @@ struct SimApiActionInvoker {
}
if (originResponse) {
if (let s: String <- actionResult) {
// String 原样输出文本(对齐 C# string 返回直接写入)
// String 原样输出文本
context.response.contentType = "application/json; charset=utf-8"
SimApiResponseWriter.write(context, s)
} else {
@@ -367,10 +367,10 @@ struct SimApiActionInvoker {
}
/// 通过 DI 容器实例化控制器,并注入当前 HttpContext
/// (通过 IBindRequestContext 接口而非 SimApiBaseController,避免 helpers↔controllers 循环依赖)
/// (通过 BindRequestContext 接口而非 SimApiBaseController,避免 helpers↔controllers 循环依赖)
private func createControllerInstance(): Object {
let instance = ActivatorUtilities.createInstance(context.services, actionDescriptor.controllerType)
if (let controller: IBindRequestContext <- instance) {
if (let controller: BindRequestContext <- instance) {
controller.bindRequestContext(context)
}
return instance
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*/
package simcu::simapi.helpers
@@ -11,7 +11,7 @@ import simcu::simapi.communications.*
/**
* 响应封装:对写操作(Unit)返回统一成功响应,对已有 SimApiBaseResponse 透传。
* 在仓颉版中以中间件形式实现,对应 C# 的 SimApiResponseFilter
* 在仓颉版中以中间件形式实现。
*/
public class SimApiResponseFilter {
@@ -26,7 +26,7 @@ public class SimApiResponseFilter {
next(context)
if (!context.response.hasStarted) {
context.response.contentType = "application/json; charset=utf-8"
SimApiResponseWriter.write(context, JsonSerializer.Serialize(SimApiBaseResponse()))
SimApiResponseWriter.write(context, JsonSerializer.serialize(SimApiBaseResponse()))
}
}
}
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*/
package simcu::simapi.helpers
@@ -11,7 +11,7 @@ import soulsoft_web_http.*
* 响应写出工具:写出响应体并缓存文本,供请求日志中间件读取。
*
* 背景:soulsoft 的 HttpResponse.body 为只写流(read 抛 UnsupportedException),
* 无法像 C# 那样用 MemoryStream 替换 Body 捕获响应内容;
* 无法像原版那样用 MemoryStream 替换 Body 捕获响应内容;
* 故在统一写出入口缓存文本,请求日志中间件直接从 context.items 读取。
*/
public class SimApiResponseWriter {
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*/
package simcu::simapi.helpers
@@ -11,7 +11,7 @@ import simcu::serialization.*
import simcu::simapi.communications.*
/**
* 统一响应封装工具(对齐 C# SimApiResponseFilter 的包装分支)
* 统一响应封装工具。
* 供 SimApiRequestDelegateFactory 与内置路由委托复用:
* - SimApiBaseResponse(含子类)→ 原样输出
* - String → SimApiResponse<String>data 为字符串)
@@ -45,6 +45,6 @@ public class SimApiResultWriter {
/// simapi_serialization 序列化后直接写响应体(经 SimApiResponseWriter 缓存,供请求日志读取)
private static func writeJson(context: HttpContext, obj: Any): Unit {
context.response.contentType = "application/json; charset=utf-8"
SimApiResponseWriter.write(context, JsonSerializer.Serialize(obj))
SimApiResponseWriter.write(context, JsonSerializer.serialize(obj))
}
}
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*/
package simcu::simapi.helpers
@@ -11,13 +11,13 @@ import simcu::simapi.exceptions.*
import simcu::simapi.interfaces.*
/**
* 服务端验签校验器(对齐 C# Attributes/SimApiSignAttribute.OnActionExecuting
* 服务端验签校验器。
*
* 仓颉无声明式 ActionFilter 机制,按项目惯例(同 requireLogin)由控制器在需要验签的方法开头调用:
* SimApiSignChecker.verify(context, provider, cache)
* 或标注 @SimApiSign 注解自动执行(SimApiRequestDelegateFactory)。
*
* 校验流程(与 C# 完全一致)
* 校验流程:
* 1. 提取 appIdQuery/Header
* 2. provider.getKey(appId) 获取密钥
* 3. 提取并解析 timestamp / nonce
@@ -111,7 +111,7 @@ public class SimApiSignChecker {
}
}
/// 从 Query 或 Header 取参数(Query 优先,对齐 C# FirstOrDefault 语义
/// 从 Query 或 Header 取参数(Query 优先)
private static func getParam(context: HttpContext, name: String): ?String {
let q = context.request.query.get(name)
if (q != None && q != Some("")) {
@@ -1,13 +1,13 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
* Helpers/SimApiStorageS3/MinIO 兼容存储助手。
*
* 仓颉生态暂无 Minio SDK,此处自实现 AWS Signature V4HMAC-SHA256 基于 stdx SHA256):
* - 预签名 URLPUT 上传 / GET 下载)
* - 直接 PUT 上传(对象 + 建桶)
* - HEAD 检测桶是否存在
* 与 C# SimApiStorageMinio SDK)的公开方法语义对齐:
* 公开方法语义对齐:
* GetUploadUrl / GetDownloadUrl / UploadFile / FullUrl / GetUrl / GetPath
*/
@@ -26,7 +26,7 @@ import soulsoft_web_http.*
import simcu::simapi.configurations.*
/**
* 上传预签名 URL 响应(对齐 C# record GetUploadUrlResponse(UploadUrl, DownloadUrl, Path)
* 上传预签名 URL 响应。
*/
public class GetUploadUrlResponse {
public var uploadUrl: String = ""
@@ -43,12 +43,12 @@ public class GetUploadUrlResponse {
}
/**
* S3/MinIO 存储助手(对齐 C# Helpers/SimApiStorage
* S3/MinIO 存储助手。
*
* 说明:
* - 注册为 Scoped 以注入 IHttpContextAccessorsoulsoft DI 禁止 singleton 消费 scoped 服务);
* 桶的检测/创建由静态守卫保证整个进程只执行一次(对齐 C# 构造函数中 BucketExists+MakeBucket
* - fullUrl/getUrl 的 "~/" 分支依赖当前请求上下文(对齐 C# IHttpContextAccessor
* 桶的检测/创建由静态守卫保证整个进程只执行一次。
* - fullUrl/getUrl 的 "~/" 分支依赖当前请求上下文。
*/
public class SimApiStorage {
private static var _bucketEnsured: Bool = false
@@ -89,12 +89,12 @@ public class SimApiStorage {
_useSsl = useSsl
_host = host
_httpContextAccessor = httpContextAccessor
// 桶不存在则创建(对齐 C# BucketExists + MakeBucket;静态守卫保证只执行一次)
// 桶不存在则创建
ensureBucketOnce()
}
/**
* 获取上传预签名 URL(对齐 C# GetUploadUrl,默认 7200 秒)
* 获取上传预签名 URL。
*/
public func getUploadUrl(path: String, expire!: Int64 = 7200): GetUploadUrlResponse {
checkPath(path)
@@ -104,7 +104,7 @@ public class SimApiStorage {
}
/**
* 获取下载预签名 URL(对齐 C# GetDownloadUrl,默认 600 秒)
* 获取下载预签名 URL。
*/
public func getDownloadUrl(path: String, expire!: Int64 = 600): String {
checkPath(path)
@@ -113,7 +113,7 @@ public class SimApiStorage {
}
/**
* 直接上传文件(对齐 C# UploadFiledata 为文件字节)
* 直接上传文件。
*/
public func uploadFile(path: String, data: Array<Byte>, contentType!: String = "image/png"): Unit {
checkPath(path)
@@ -122,7 +122,7 @@ public class SimApiStorage {
}
/**
* 批量删除对象(对齐 C# Minio RemoveObjectsAsyncS3 原生 DeleteObjects 接口:
* 批量删除对象(S3 原生 DeleteObjects 接口:
* POST /{bucket}?delete,一次请求删除多个对象,无需逐个删除)。
* @param paths 对象路径数组(每个须以 / 开头)。
*/
@@ -143,7 +143,7 @@ public class SimApiStorage {
}
/**
* 使用 path 获取完整的访问 URL(对齐 C# FullUrl
* 使用 path 获取完整的访问 URL。
*/
public func fullUrl(path: ?String): ?String {
if (let Some(p) <- path) {
@@ -162,7 +162,7 @@ public class SimApiStorage {
}
/**
* 获取一个 Path 的访问 URL(对齐 C# GetUrl
* 获取一个 Path 的访问 URL。
*/
public func getUrl(path: ?String): ?String {
if (let Some(p) <- path) {
@@ -181,7 +181,7 @@ public class SimApiStorage {
}
/**
* 从 URL 中获取相对路径(对齐 C# GetPath;去掉 Endpoint/Bucket 或 ServeUrl 前缀)
* 从 URL 中获取相对路径。
*/
public func getPath(url: ?String): ?String {
if (let Some(u) <- url) {
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 遵循 MIT 许可证。
*/
package simcu::simapi.helpers
@@ -17,7 +17,7 @@ import simcu::simapi.communications.*
import simcu::simapi.macros.*
/**
* 工具类:对应 C# 的 SimApi.Helpers.SimApiUtil
* 工具类。
* 提供时间、哈希、Base64、JSON、校验等常用能力。
*/
public class SimApiUtil {
@@ -88,7 +88,7 @@ public class SimApiUtil {
toHexString(sha.finish())
}
/// 按 .NET Md5/Sha1 的 mode 格式化:x2/x3/x4 → 每个字节 2/3/4 位十六进制
/// 按 Md5/Sha1 的 mode 格式化:x2/x3/x4 → 每个字节 2/3/4 位十六进制
private static func formatHex(bytes: Array<Byte>, mode: String): String {
if (mode == "x2") {
return toHexString(bytes)
@@ -134,14 +134,14 @@ public class SimApiUtil {
/**
* 判断是否是 Email 地址。
* 说明:.NET 使用 System.Net.Mail.MailAddress 校验,仓颉无等价 API,此处用正则近似。
* 说明:原版使用 System.Net.Mail.MailAddress 校验,仓颉无等价 API,此处用正则近似。
*/
public static func checkEmail(email: String): Bool {
Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$").matches(email)
}
/**
* 生成 UUID v4 字符串(对齐 C# Guid.NewGuid().ToString():小写、8-4-4-4-12 连字符格式)
* 生成 UUID v4 字符串。
* 仓颉标准库没有 GUID 生成器(stdx 的 GUID 是 stdx.net.http 内部类型),
* 此处用随机数自行构造:16 字节随机数 + 版本位(4)+ 变体位(10)。
*/
@@ -172,18 +172,18 @@ public class SimApiUtil {
}
/**
* 将对象序列化为 JSON 字符串(对齐 C# SimApiUtil.Json
* 将对象序列化为 JSON 字符串。
* @param obj 任意对象(None 输出 null)。
*/
public static func json(obj: ?Any): String {
if (let Some(obj) <- obj) {
return JsonSerializer.Serialize(obj)
return JsonSerializer.serialize(obj)
}
"null"
}
/**
* JSON 字符串转义(对齐 C# 内部转义逻辑)
* JSON 字符串转义。
* @param s 原始字符串。
* @return 转义后可直接放入 JSON 字符串字面量的内容。
*/
@@ -203,17 +203,17 @@ public class SimApiUtil {
}
/**
* 从 JSON 字符串反序列化为 T(对齐 C# SimApiUtil.FromJson<T>
* 从 JSON 字符串反序列化为 T。
* @param T 目标类型(任意类,无需接口/宏约束)。
* @param jsonString JSON 字符串。
* @return 反序列化结果。
*/
public static func fromJson<T>(jsonString: String): T {
JsonSerializer.Deserialize<T>(jsonString)
JsonSerializer.deserialize<T>(jsonString)
}
/**
* 对象 Base64 编码(对象 → JSON → Base64,对齐 C# Base64Encode(object))。
* 对象 Base64 编码(对象 → JSON → Base64)。
* @param obj 任意对象(DTO/基础类型/HashMap 等)。
* @return Base64 字符串。
*/
@@ -223,7 +223,7 @@ public class SimApiUtil {
}
/**
* Base64 → JSON → T 反序列化(对齐 C# Base64Decode<T>
* Base64 → JSON → T 反序列化。
* @param T 目标类型(任意类,无需接口/宏约束)。
* @param base64Str Base64 字符串。
* @return 反序列化结果。
@@ -233,7 +233,7 @@ public class SimApiUtil {
}
/**
* 分页(对齐 C# Paginate 扩展;仓颉无 IQueryable,改为对 Array<T> 切片)
* 分页。
*/
public static func paginate<T>(list: Array<T>, page: Int64, count: Int64): Array<T> {
let p = if (page < 1) { 1 } else { page }
@@ -247,5 +247,5 @@ public class SimApiUtil {
list[skip..end]
}
// 说明:C# 的 XmlDeserialize<T> 依赖 System.Xml.Serialization,仓颉生态无 XML 序列化库,未移植。
// 说明:XmlDeserialize<T> 依赖 System.Xml.Serialization,仓颉生态无 XML 序列化库,未移植。
}