From 92eea3108ae3c14400799aa166e3c71d534d2b55 Mon Sep 17 00:00:00 2001 From: xRain Date: Sun, 16 Aug 2026 23:44:20 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=AF=B9=E9=BD=90=20simapi-net=20?= =?UTF-8?q?=E7=9A=84=2020=20=E9=A1=B9=E5=B7=AE=E5=BC=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SimApiLoginItem._types 默认 [user] - 新增 SimApiIdOnlyRequest - errorWhenNone 改名 errorWhenNull - SimApiAuth.update 保留 TTL(Redis ttl/expire) - SimApiCache: set 接受 Any + 新增 get/getString - SimApiHttpClient 签名字段移出 Options - SimApiUtil: md5/sha1 支持 mode + 新增 paginate - @SimApiAuth 支持逗号分隔多类型 - Logger: 4 位毫秒 + IsEnabled 恒 true - 删除 /versions 端点;exceptionHandler 改为抛错 - UseSimApi 中间件顺序对齐 .NET - README 同步更新 --- .gitignore | 3 +- README.md | 11 ++-- src/attributes/SimApiAuth.cj | 5 +- src/authsdk/SimApiAuthCenter.cj | 2 + src/authsdk/SimApiAuthDto.cj | 5 ++ src/communications/SimApiBaseRequest.cj | 13 +++++ src/communications/SimApiLoginItem.cj | 2 +- src/configurations/SimApiHttpClientOptions.cj | 5 -- src/controllers/SimApiCommonController.cj | 19 ++----- src/extensions/SimApiExtensions.cj | 42 ++++++++-------- .../SimApiRequestDelegateFactory.cj | 12 ++++- src/helpers/SimApiAuth.cj | 5 ++ src/helpers/SimApiCache.cj | 26 +++++++--- src/helpers/SimApiError.cj | 2 +- src/helpers/SimApiHttpClient.cj | 5 -- src/helpers/SimApiUtil.cj | 50 +++++++++++++++++-- src/logger/SimApiLogger.cj | 11 ++-- 17 files changed, 142 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index 9f97022..ec8ef60 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -target/ \ No newline at end of file +target/ +*.cj.macrocall \ No newline at end of file diff --git a/README.md b/README.md index 6d0c45d..4b09b2e 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ import simapi.helpers.* SimApiError.error(500, "服务器内部错误") // 直接抛错 SimApiError.errorWhen(amount <= 0, 400, "金额无效") // 条件为 true 时抛错 SimApiError.errorWhenFalse(hasPermission, 403, "无权操作") -SimApiError.errorWhenNone(someOptional, 404, "用户不存在") +SimApiError.errorWhenNull(someOptional, 404, "用户不存在") ``` ### 2. 认证 — SimApiAuth @@ -143,7 +143,7 @@ public class MyController <: SimApiBaseController { } ``` -> 说明:仓颉注解参数须为编译期常量,`@SimApiAuth` 支持单类型参数(`@SimApiAuth["admin"]`);空参数表示任意已登录用户。多个 `ISimApiAuthChecker` 通过 `SimApiOptions.authCheckers` 注册(由 addSimApi 扫描调用者包填充)。 +> 说明:仓颉注解参数须为编译期常量,`@SimApiAuth` 支持单个类型参数(`@SimApiAuth["admin"]`)或逗号分隔多类型(`@SimApiAuth["admin,user"]`,对齐 C# `type.Split(",")`);空参数表示任意已登录用户。多个 `ISimApiAuthChecker` 通过 `SimApiOptions.authCheckers` 注册(由 addSimApi 扫描调用者包填充)。 ### 2.2 原样响应 — @OriginResponse @@ -272,7 +272,7 @@ let resp2 = client.aesQuery("/api/data", body: "{\"a\":1}") let resp3 = client.aesSignQuery("/api/data", body: "{\"a\":1}") ``` -签名参数名可配置(`simApiHttpClientOptions.signName / timestampName / nonceName / appIdName / signFields`,C# 侧为硬编码)。 +签名参数名可配置(`SimApiHttpClient` 实例属性 `signName / timestampName / nonceName / appIdName / signFields`,对齐 C# 的 virtual 属性)。 ### 5.1 请求日志 — enableRequestLog @@ -324,10 +324,9 @@ builder.addSimApi { options => | 路由 | 方法 | 条件 | 说明 | | ----------------- | -------- | ---------------------------- | -------------------------- | -| `/versions` | GET/POST | 始终 | 返回 SimApi/App 版本 | | `/user/info` | POST | `enableSimApiAuth` | 需登录,返回 LoginInfo | | `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 | -| `/exception/{code}` | GET | 始终 | 错误反馈 | +| `/exception/{code}` | GET | 始终 | 错误反馈(抛 SimApiException) | 路由路径可自定义(`configureSimApiRoute`,自定义值通过 `mapGet/mapPost` 真实注册,默认值由内置控制器特性路由覆盖): @@ -434,7 +433,7 @@ simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),`addSimAp | 控制器 | 路由 | 说明 | |--------|------|------| -| `SimApiCommonController` | `/exception/{code}`、`/config`、`/versions`、`/user/info` | 通用内置路由 | +| `SimApiCommonController` | `/exception/{code}`、`/config`、`/user/info` | 通用内置路由 | | `SimApiAuthController` | `/auth/logout` | 退出登录 | | `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken` / `requireLogin()` / `getLogin()` | diff --git a/src/attributes/SimApiAuth.cj b/src/attributes/SimApiAuth.cj index 222065f..5f5ec5c 100644 --- a/src/attributes/SimApiAuth.cj +++ b/src/attributes/SimApiAuth.cj @@ -16,9 +16,10 @@ package simapi.attributes * 用法: * @SimApiAuth // 任意已登录用户 * @SimApiAuth["admin"] // 仅 admin 类型 + * @SimApiAuth["admin,user"] // admin 或 user 类型(对齐 C# type.Split(",")) * - * 说明:仓颉注解参数须为编译期常量,且 String 无法作为 const 值数组元素 - * (内部为 Array),故与 C# 的 string[] 不同,这里支持单个类型参数。 + * 说明:仓颉注解参数须为编译期常量,String 无法作为 const 值数组元素, + * 故与 C# 的 string[] 不同,这里用逗号分隔字符串对齐 C# 多类型。 */ @Annotation[target: [MemberFunction, Type]] public class SimApiAuth { diff --git a/src/authsdk/SimApiAuthCenter.cj b/src/authsdk/SimApiAuthCenter.cj index f7dbafe..27d18e5 100644 --- a/src/authsdk/SimApiAuthCenter.cj +++ b/src/authsdk/SimApiAuthCenter.cj @@ -193,6 +193,8 @@ public class SimApiAuthCenter { public func getLoginInfo(code: String, scene!: ?String = None): LoginInfoResponse { var body = HashMap() body["code"] = code + // 说明:C# 的 ErrorWhenNull(resp, 400232, "登录信息获取失败") 对应 signQuery 内部 _data.getOrThrow() 的 + // None 分支;仓颉版 signQuery 返回非空 T(data 缺失即抛异常),故此处无需重复判空。 let resp = _client.signQuery("/api/auth/login/get", body: SimApiJson.json(Some(body))) SimApiError.errorWhen(resp._scene != scene, code: 403003, message: "登录场景不匹配") resp diff --git a/src/authsdk/SimApiAuthDto.cj b/src/authsdk/SimApiAuthDto.cj index 6dfb52c..395d4db 100644 --- a/src/authsdk/SimApiAuthDto.cj +++ b/src/authsdk/SimApiAuthDto.cj @@ -2,6 +2,10 @@ * Copyright (c) 2025 SimcuTeam. All rights reserved. * 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。 * AuthSDK 用到的 DTO(对齐 C# SimApiAuthCenterDto / SimApiAuthIamDto)。 + * + * 说明:C# 中这些 DTO 是 SimApiAuthCenterDto / SimApiAuthIamDto 的嵌套类; + * 仓颉不支持在类体内声明嵌套类(unexpected class declaration in class body), + * 故拍平为顶层类,语义与字段保持一致。 */ package simapi.authsdk @@ -26,6 +30,7 @@ public class AppAndProfileItem { /** * 安全确认响应(对齐 C# ConfirmResponse)。 + * _data 用 ?JsonValue 对齐 C# Dictionary?(任意 JSON 对象)。 */ @Serialization public class ConfirmResponse { diff --git a/src/communications/SimApiBaseRequest.cj b/src/communications/SimApiBaseRequest.cj index 08f9d5a..a3929ee 100644 --- a/src/communications/SimApiBaseRequest.cj +++ b/src/communications/SimApiBaseRequest.cj @@ -13,6 +13,19 @@ public class SimApiBaseRequest {} /** * 仅包含 Id 的请求。 */ +public class SimApiIdOnlyRequest { + public var id: Int64 = 0 + + public init() {} + + public init(id: Int64) { + this.id = id + } +} + +/** + * 仅包含 Id 的请求(字符串)。 + */ public class SimApiStringIdOnlyRequest { public var id: String = "" diff --git a/src/communications/SimApiLoginItem.cj b/src/communications/SimApiLoginItem.cj index 34b9d89..2bd1fed 100644 --- a/src/communications/SimApiLoginItem.cj +++ b/src/communications/SimApiLoginItem.cj @@ -20,7 +20,7 @@ import soulsoft_serialization.* */ public class SimApiLoginItem <: ISerialization { public var _id: String = "" - public var _types: Array = [] + public var _types: Array = ["user"] public var _meta: HashMap = HashMap() public var _extra: HashMap = HashMap() diff --git a/src/configurations/SimApiHttpClientOptions.cj b/src/configurations/SimApiHttpClientOptions.cj index 9f75478..db39212 100644 --- a/src/configurations/SimApiHttpClientOptions.cj +++ b/src/configurations/SimApiHttpClientOptions.cj @@ -13,11 +13,6 @@ public class SimApiHttpClientOptions { public var server: String = "" public var appId: String = "" public var appKey: String = "" - public var signName: String = "sign" - public var timestampName: String = "timestamp" - public var nonceName: String = "nonce" - public var appIdName: ?String = Some("appId") - public var signFields: Array = [] public init() {} } diff --git a/src/controllers/SimApiCommonController.cj b/src/controllers/SimApiCommonController.cj index 9f6fab4..1297c37 100644 --- a/src/controllers/SimApiCommonController.cj +++ b/src/controllers/SimApiCommonController.cj @@ -26,24 +26,11 @@ public class SimApiCommonController <: SimApiBaseController { /** * GET /exception/{code}:错误反馈页面(始终注册)。 - * 返回 SimApiBaseResponse(已是响应体,原样输出)。 + * 对齐 C# ExceptionHandler:抛 SimApiException,由异常中间件统一输出。 */ @HttpGet["exception/{code}"] - public func exceptionHandler(@FromRoute code: Int64): SimApiBaseResponse { - SimApiBaseResponse(code, SimApiBaseResponse.getDefaultMessage(code)) - } - - /** - * GET/POST /versions:返回 SimApi/App 版本信息(HashMap 由 SimApiResponseFilter 自动封装)。 - */ - @HttpGet["versions"] - public func versions(): HashMap { - versionsMap() - } - - @HttpPost["versions"] - public func versionsPost(): HashMap { - versionsMap() + public func exceptionHandler(@FromRoute code: Int64): Unit { + SimApiError.error(code: code) } /** diff --git a/src/extensions/SimApiExtensions.cj b/src/extensions/SimApiExtensions.cj index e75cbd5..711e2ff 100644 --- a/src/extensions/SimApiExtensions.cj +++ b/src/extensions/SimApiExtensions.cj @@ -216,10 +216,15 @@ extend WebHost <: SimApiHostExtensions { } // ===== 中间件与路由(对齐 C# UseSimApi(WebApplication) 的挂载顺序) ===== - // C# 挂载顺序(先挂载 = 外层):CORS(L425) → AuthGate(L454) → Auth(L462) → RequestLog(L519) → Exception(L525) - // OPTIONS 预检请求在 CORS 处短路(204,不调用 next),因此 RequestLog/Exception 均不会执行 + // C# 挂载顺序(先挂载 = 外层):ForwardedHeaders(L419) → CORS(L425) → AuthGate(L454) → Auth(L462) + // → 内置路由(L465-496) → Swagger(L502) → RequestLog(L519) → Exception(L525) → LowerUrl → Job - // CORS(对齐 C# builder.UseCors("any"),最先挂载) + // ForwardedHeaders(占位:soulsoft 暂无内置,对齐 C# 最先挂载) + if (options.enableForwardHeaders) { + logger.info("开始配置ForwardedHeaders...") + } + + // CORS(对齐 C# builder.UseCors("any")) if (options.enableCors) { logger.info("开始配置 Cors全部允许...") this.useCors() @@ -237,18 +242,6 @@ extend WebHost <: SimApiHostExtensions { this.use() } - // 请求日志中间件(对齐 C# builder.UseMiddleware()) - if (options.enableRequestLog) { - logger.info("开始配置 SimApiRequestLog...") - this.use() - } - - // 异常中间件最后挂载(最内层,对齐 C# builder.UseMiddleware()) - if (options.enableSimApiException) { - logger.info("开始配置 SimApiException...") - this.use() - } - // 内置路由:RouteOptions 自定义路径时真实注册(默认路径已由内置控制器特性路由覆盖, // 对齐 C# MapControllerRoute 语义;soulsoft 无约定路由 defaults,用 mapGet/mapPost 委托实现) let routeOptions = options.simApiRouteOptions @@ -308,11 +301,23 @@ extend WebHost <: SimApiHostExtensions { logger.info("注册内置Route: WebConfig => ${route}") } - // SimApiDoc(占位) + // SimApiDoc(占位,对齐 C# UseSwagger/UseSwaggerUI) if (options.enableSimApiDoc) { logger.info("开始配置 SimApiDoc...") } + // 请求日志中间件(对齐 C# builder.UseMiddleware()) + if (options.enableRequestLog) { + logger.info("开始配置 SimApiRequestLog...") + this.use() + } + + // 异常中间件最后挂载(最内层,对齐 C# builder.UseMiddleware()) + if (options.enableSimApiException) { + logger.info("开始配置 SimApiException...") + this.use() + } + // URL 小写 if (options.enableLowerUrl) { logger.info("开始配置使用URL小写...") @@ -329,11 +334,6 @@ extend WebHost <: SimApiHostExtensions { logger.info("开始配置 SimApiResponseFilter...") } - // ForwardedHeaders(占位:soulsoft 暂无内置) - if (options.enableForwardHeaders) { - logger.info("开始配置ForwardedHeaders...") - } - // 映射控制器端点(对齐 C# UseSimApi 中的 MapControllers) let callSiteFactory = this.services.getOrThrow() if (callSiteFactory.isService()) { diff --git a/src/extensions/SimApiRequestDelegateFactory.cj b/src/extensions/SimApiRequestDelegateFactory.cj index c3ea676..80673c3 100644 --- a/src/extensions/SimApiRequestDelegateFactory.cj +++ b/src/extensions/SimApiRequestDelegateFactory.cj @@ -96,9 +96,17 @@ struct SimApiActionInvoker { SimApiError.error(code: 401, message: "需要登录") } - // 2. 类型权限校验 → 403 + // 2. 类型权限校验 → 403(对齐 C# Types.Intersect(loginInfo.Type).Any(),支持逗号分隔多类型) if (!auth.`type`.isEmpty()) { - if (!loginItem._types.contains(auth.`type`)) { + 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: "无权访问") } } diff --git a/src/helpers/SimApiAuth.cj b/src/helpers/SimApiAuth.cj index fe82d78..ac14666 100644 --- a/src/helpers/SimApiAuth.cj +++ b/src/helpers/SimApiAuth.cj @@ -114,7 +114,12 @@ public class SimApiAuth { let tokenKey = "${tokenCachePrefix}${token}" let json = loginItemJson(loginItem) if (let Some(redis) <- _redis) { + // 保留原过期时间(对齐 C# update 不刷新 TTL):先读旧 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)) { diff --git a/src/helpers/SimApiCache.cj b/src/helpers/SimApiCache.cj index 58d9eea..8ab915c 100644 --- a/src/helpers/SimApiCache.cj +++ b/src/helpers/SimApiCache.cj @@ -10,6 +10,7 @@ import std.collection.concurrent.* import std.convert.* import std.time.* import redis.client.* +import soulsoft_serialization.* import simapi.configurations.* import simapi.exceptions.* @@ -66,16 +67,17 @@ public class SimApiCache { * @param value 缓存值(不能为 null)。 * @param expireSeconds 过期秒数(可选,<=0 表示不过期)。 */ - public func set(key: String, value: String, expireSeconds!: Int64 = -1): Unit { + public func set(key: String, value: Any, expireSeconds!: Int64 = -1): Unit { + let json = SimApiUtil.json(Some(value)) if (let Some(redis) <- _redis) { if (expireSeconds > 0) { - redis.set("${prefix}${key}", Blob.fromUtf8(value), ex: Some(expireSeconds)) + redis.set("${prefix}${key}", Blob.fromUtf8(json), ex: Some(expireSeconds)) } else { - redis.set("${prefix}${key}", Blob.fromUtf8(value)) + redis.set("${prefix}${key}", Blob.fromUtf8(json)) } } else { let expireAt = if (expireSeconds > 0) { nowMillis() + expireSeconds * 1000 } else { 0 } - _store["${prefix}${key}"] = CacheEntry(value, expireAt) + _store["${prefix}${key}"] = CacheEntry(json, expireAt) } } @@ -94,13 +96,23 @@ public class SimApiCache { * 缓存 Key 是否存在。 */ public func hasKey(key: String): Bool { - get(key) != None + getString(key) != None } /** - * 获取 string 类型缓存。 + * 获取特定类型缓存(对齐 C# Get:从 JSON 反序列化)。 */ - public func get(key: String): ?String { + public func get(key: String): ?T where T <: ISerialization { + match (getString(key)) { + case Some(json) => Some(JsonSerializer.deserializeObject(json)) + case None => None + } + } + + /** + * 获取 string 类型缓存(对应 C# Get(string);Cangjie 不支持按泛型重载,故拆分为 getString/get)。 + */ + public func getString(key: String): ?String { if (let Some(redis) <- _redis) { return match (redis.get("${prefix}${key}")) { case Some(blob) => Some(blob.toUtf8()) diff --git a/src/helpers/SimApiError.cj b/src/helpers/SimApiError.cj index 916f46e..9f563b4 100644 --- a/src/helpers/SimApiError.cj +++ b/src/helpers/SimApiError.cj @@ -55,7 +55,7 @@ public class SimApiError { * @param code 错误代码,默认 404。 * @param message 错误描述。 */ - public static func errorWhenNone(condition: ?Any, code!: Int64 = 404, message!: String = ""): Unit { + public static func errorWhenNull(condition: ?Any, code!: Int64 = 404, message!: String = ""): Unit { match (condition) { case None => error(code: code, message: message) case _ => () diff --git a/src/helpers/SimApiHttpClient.cj b/src/helpers/SimApiHttpClient.cj index 4ca7c38..8876fdf 100644 --- a/src/helpers/SimApiHttpClient.cj +++ b/src/helpers/SimApiHttpClient.cj @@ -38,11 +38,6 @@ public open class SimApiHttpClient { server = httpOptions.server appId = httpOptions.appId appKey = httpOptions.appKey - signName = httpOptions.signName - timestampName = httpOptions.timestampName - nonceName = httpOptions.nonceName - appIdName = httpOptions.appIdName - signFields = httpOptions.signFields } /** diff --git a/src/helpers/SimApiUtil.cj b/src/helpers/SimApiUtil.cj index a444888..63aece0 100644 --- a/src/helpers/SimApiUtil.cj +++ b/src/helpers/SimApiUtil.cj @@ -61,10 +61,10 @@ public class SimApiUtil { * @param source 源字符串。 * @return 32 位十六进制小写。 */ - public static func md5(source: String): String { + public static func md5(source: String, mode!: String = "x2"): String { let md = MD5() md.write(source.toArray()) - toHexString(md.finish()) + formatHex(md.finish(), mode) } /** @@ -72,10 +72,10 @@ public class SimApiUtil { * @param source 源字符串。 * @return 40 位十六进制小写。 */ - public static func sha1(source: String): String { + public static func sha1(source: String, mode!: String = "x2"): String { let sha = SHA1() sha.write(source.toArray()) - toHexString(sha.finish()) + formatHex(sha.finish(), mode) } /** @@ -89,6 +89,28 @@ public class SimApiUtil { toHexString(sha.finish()) } + /// 按 .NET Md5/Sha1 的 mode 格式化:x2/x3/x4 → 每个字节 2/3/4 位十六进制 + private static func formatHex(bytes: Array, mode: String): String { + if (mode == "x2") { + return toHexString(bytes) + } + let two = toHexString(bytes) + var sb = StringBuilder() + for (i in 0..bytes.size) { + let pair = two[i * 2..i * 2 + 2] + if (mode == "x3") { + sb.append("0") + sb.append(pair) + } else if (mode == "x4") { + sb.append("00") + sb.append(pair) + } else { + sb.append(pair) + } + } + sb.toString() + } + /** * 字符串 Base64 编码。 */ @@ -112,7 +134,8 @@ public class SimApiUtil { } /** - * 判断是否是 Email 地址(简化校验)。 + * 判断是否是 Email 地址。 + * 说明:.NET 使用 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) @@ -189,4 +212,21 @@ public class SimApiUtil { public static func base64DecodeTo(base64Str: String): T where T <: ISerialization { fromJson(base64Decode(base64Str)) } + + /** + * 分页(对齐 C# Paginate 扩展;仓颉无 IQueryable,改为对 Array 切片)。 + */ + public static func paginate(list: Array, page: Int64, count: Int64): Array { + let p = if (page < 1) { 1 } else { page } + let c = if (count <= 0) { 10 } else { count } + let skip = (p - 1) * c + let total = list.size + if (skip >= total) { + return Array() + } + let end = if (skip + c > total) { total } else { skip + c } + list[skip..end] + } + + // 说明:C# 的 XmlDeserialize 依赖 System.Xml.Serialization,仓颉生态无 XML 序列化库,未移植。 } diff --git a/src/logger/SimApiLogger.cj b/src/logger/SimApiLogger.cj index 6c5b2c0..3a38cb7 100644 --- a/src/logger/SimApiLogger.cj +++ b/src/logger/SimApiLogger.cj @@ -48,7 +48,7 @@ public class SimApiLogger <: ILogger { } public func isEnabled(logLevel: LogLevel): Bool { - logLevel != LogLevel.Off + true } /** @@ -96,7 +96,7 @@ public class SimApiLogger <: ILogger { let hour = pad2(dt.hour) let minute = pad2(dt.minute) let second = pad2(dt.second) - let millis = pad3(dt.nanosecond / 1000000) + let millis = pad4(dt.nanosecond / 100000) "${year}-${month}-${day} ${hour}:${minute}:${second}:${millis}" } @@ -107,11 +107,14 @@ public class SimApiLogger <: ILogger { "${v}" } - private static func pad3(v: Int64): String { + private static func pad4(v: Int64): String { if (v < 10) { - return "00${v}" + return "000${v}" } if (v < 100) { + return "00${v}" + } + if (v < 1000) { return "0${v}" } "${v}"