# SimApi for Cangjie(simapi) > 仓颉版 SimApi:ASP.NET Core 风格 API 基础框架,移植自 C# 项目 [SimApi](https://github.com/SimcuTeam/simapi-net)(`E:\simcu\simapi-net`)。 提供**统一响应格式、异常拦截、Token 认证、缓存、工具集、HTTP 客户端、S3 存储、声明式注解**等 API 基础能力。 --- ## 引入 两种方式任选其一: **方式一:中央仓**(需先 `cjpm publish` 发布 `simcu::simapi`) ```toml [dependencies] "simcu::simapi" = "5.2.12" ``` **方式二:Git 仓库** ```toml [dependencies] "simcu::simapi" = { git = "https://gitcode.com/simcu/simapi-cj.git", version = "5.2.12" } ``` > 本地开发也可用 path 依赖:`"simcu::simapi" = { path = "../simapi-cj" }` --- ## 快速开始 ```cangjie package your_app import soulsoft_web_http.* import soulsoft_web_routing.* import soulsoft_web_hosting.* import soulsoft_extensions_logging.* import soulsoft_extensions_injection.* import simcu::simapi.* import simcu::simapi.communications.* import simcu::simapi.helpers.* main(args: Array) { let builder = WebHost.createBuilder(args) builder.services.addRouting() builder.services.addLogging() // 注册 SimApi 服务(与 addLogging 同样式) SimApiExtensions.addSimApi(builder) { options => options.enableSimApiAuth = true // Token 认证(未配 Redis 自动用 InMemory) options.enableSimApiCache = true // 缓存 options.enableSimApiException = true // 全局异常拦截 } let host = builder.build() SimApiExtensions.useSimApi(host) // 业务接口:返回对象自动封装为统一响应格式({code, message, data}) host.mapGet("hello") { context => context.response.write(SimApiUtil.json(Some(SimApiResponse("hello cangjie.")))) } host.run() } ``` ### 统一响应格式 所有接口输出 JSON,HTTP 状态码始终 `200`,错误信息在 `code` 字段: | code | 含义 | | ---- | ---------- | | 200 | 成功 | | 204 | 无数据 | | 400 | 参数错误 | | 401 | 需要登录 | | 403 | 无权访问 | | 404 | 资源不存在 | | 500 | 服务器错误 | 响应 JSON(经 simcu::serialization 反射序列化,字段**无下划线**): ```json { "code": 200, "message": "成功", "data": { ... } } ``` ### 异常处理流程 ``` 请求 → SimApiExceptionMiddleware(全异常捕获→HTTP 200+JSON) → SimApiAuthMiddleware(Token→LoginInfo) → 路由 → 业务处理 ``` --- ## 项目结构 ``` simapi-cj/ ├── cjpm.toml # 包配置 ├── src/ │ ├── SimApiExtensions.cj # 根包入口:SimApiExtensions 静态类(addSimApi / useSimApi + 内置路由 + 响应封装) │ ├── annotations/ # 声明式注解:@SimApiAuth(鉴权)、@OriginResponse(原样响应)、 │ │ # @SimApiSign(验签)、@AesBody(AES body 解密) │ ├── authsdk/ # 认证中心 SDK:SimApiAuthClient/Center/Iam + 网关中间件 + DTO │ ├── communications/ # SimApiBaseResponse, PageResponse, SimApiLoginItem, 请求 DTO │ ├── configurations/ # SimApiOptions + 各模块 Option(含 ConfigureSimApiXxx 回调) │ ├── controllers/ # SimApiBaseController, SimApiCommonController, SimApiAuthController(MVC 写法) │ ├── exceptions/ # SimApiException │ ├── helpers/ # SimApiError, SimApiUtil, SimApiAuth, SimApiCache, SimApiHttpClient, │ │ # SimApiAesUtil(AES-256), SimApiSignChecker(验签), SimApiAesBodyChecker(AES body), │ │ # SimApiStorage(S3/MinIO, 自实现 SigV4), SimApiRequestDelegateFactory, SimApiResultWriter │ ├── interfaces/ # ISimApiAuthChecker, IBindRequestContext, SimApiSignProviderBase, AesBodyProviderBase │ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志) │ ├── macros/ # ReadTomlVersion(编译期读版本号) │ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware │ └── models/ # SimApiBaseModel(实体基类) ``` --- ## 模块说明 ### 1. 错误处理 — SimApiError ```cangjie import simcu::simapi.helpers.* SimApiError.error(500, "服务器内部错误") // 直接抛错 SimApiError.errorWhen(amount <= 0, 400, "金额无效") // 条件为 true 时抛错 SimApiError.errorWhenFalse(hasPermission, 403, "无权操作") SimApiError.errorWhenNull(someOptional, 404, "用户不存在") ``` ### 2. 认证 — SimApiAuth ```cangjie import simcu::simapi.helpers.* import simcu::simapi.communications.* // 由 DI 注入(构造参数 options: SimApiOptions,从配置读 RedisConfiguration;未配则 InMemory) let auth: SimApiAuth = ... // 例:控制器构造注入 let token = auth.login(SimApiLoginItem(id: "user-001")) // 默认 7 天 let login = auth.getLogin(token) // 获取登录信息 auth.logout(token) // 退出登录 auth.logoutAll("user-001") // 退出全部 ``` - **Redis 模式**:配置 `RedisConfiguration` 时使用,支持多实例共享。连接串格式: - `"localhost:6379"`(基础) - `"localhost:6379,password=xxx"`(带密码) - `"localhost:6379,password=xxx,db=2"`(带密码 + DB 索引) - **InMemory 模式**:零配置,适合开发/测试;登录态带过期时间(对齐 C# 过期语义),重启后丢失 - **Token 传参**:Header `Token: ` 或 Query `token=` ### 2.1 声明式鉴权 — @SimApiAuth(对齐 C# [SimApiAuth]) 标注在控制器**方法或类**上,请求派发时自动执行鉴权(未登录 401 → 类型不匹配 403 → 遍历执行 `ISimApiAuthChecker`): ```cangjie import simcu::simapi.annotations.{SimApiAuth} @SimApiAuth // 类级:整个控制器需登录 public class MyController <: SimApiBaseController { @SimApiAuth["admin"] // 方法级:仅 admin 类型可访问 @HttpPost["my/admin-only"] public func adminOnly(): String { "ok" } } ``` > 说明:仓颉注解参数须为编译期常量,`@SimApiAuth` 支持单个类型参数(`@SimApiAuth["admin"]`)或逗号分隔多类型(`@SimApiAuth["admin,user"]`,对齐 C# `type.Split(",")`);空参数表示任意已登录用户。 ### 2.2 原样响应 — @OriginResponse 标注后跳过统一响应封装,接口返回什么就输出什么(对齐 C# `[OriginResponse]`): ```cangjie import simcu::simapi.annotations.{OriginResponse} @OriginResponse @HttpGet["raw"] public func raw(): String { "{\"raw\":true}" // 直接输出,不包 {code,message,data} } ``` ### 2.3 声明式验签 — @SimApiSign(对齐 C# [SimApiSign]) 标注在控制器**方法或类**上,请求派发时自动验签(appId 提取 → 密钥获取 → timestamp 过期校验 → nonce 去重 → MD5 比对): ```cangjie import simcu::simapi.annotations.{SimApiSign} import simcu::simapi.interfaces.{SimApiSignProviderBase} // 1. 继承 Provider 实现密钥获取(并注册到 DI) public class MySignProvider <: SimApiSignProviderBase { public override func getKey(appId: ?String): ?String { Some("my-secret-key") } } // 2. 方法标注 @SimApiSign,自动验签(provider 类型名从 DI 解析) @SimApiSign["MySignProvider"] public func signedAction(): String { "ok" } ``` `SimApiSignProviderBase` 可配置:`appIdName` / `timestampName` / `nonceName` / `signName` / `queryExpires` / `duplicateRequestProtection` / `signFields`(与 C# 一致)。也可手动调用 `SimApiSignChecker.verify(context, provider, cache)`。 ### 2.4 声明式 AES body — @AesBody(对齐 C# [AesBody]) 标注在**参数**上,请求派发时自动解密 `{"data":"密文"}` body 并反序列化为参数类型: ```cangjie import simcu::simapi.annotations.{AesBody} import simcu::simapi.interfaces.{AesBodyProviderBase} public class MyAesProvider <: AesBodyProviderBase { public override func getKey(appId: ?String): ?String { Some("aes-secret-key") } } public func create(@AesBody["MyAesProvider"] request: CreateRequest): String { // request 已自动解密并反序列化 "ok" } ``` 也可手动调用 `SimApiAesBodyChecker.decryptBody(context, provider)` 获取明文 JSON 字符串。 ### 3. 缓存 — SimApiCache ```cangjie // 由 DI 注入(构造参数 options: SimApiOptions) let cache: SimApiCache = ... cache.set("key", "value") let v = cache.get("key") // ?String cache.hasKey("key") // Bool cache.remove("key") ``` Key 自动加前缀 `SimApi:Cache:`。 ### 4. 工具集 — SimApiUtil ```cangjie SimApiUtil.cstNow // UTC+8 时间 SimApiUtil.timestampNow // 秒级时间戳 SimApiUtil.newGuid() // UUID v4(对齐 C# Guid.NewGuid()) SimApiUtil.md5("text") // 32 位十六进制 SimApiUtil.sha1("text") // 40 位 SimApiUtil.base64Encode("text") / base64Decode("...") SimApiUtil.base64Encode(obj) // 对象 → JSON → Base64(对齐 C# Base64Encode(object)) SimApiUtil.json(obj) // 对象 → JSON 字符串(simcu::serialization 反射) SimApiUtil.escapeJson(s) // JSON 字符串转义 SimApiUtil.fromJson(json) // JSON → T(对齐 C# FromJson,任意类免约束) SimApiUtil.base64DecodeTo(str) // Base64 → JSON → T SimApiUtil.checkCell("13800138000") // 手机号 SimApiUtil.checkEmail("a@b.com") // 邮箱 ``` > JSON 序列化/反序列化统一走 **simcu::serialization**(`JsonSerializer.Serialize` / `Deserialize`),任意类免标注、免接口约束。 ### 4.1 AES 加解密 — SimApiAesUtil(对齐 C# SimApiAesUtil) 纯仓颉实现 AES-256-CBC + PKCS7(S-box/密钥扩展/轮函数),与 .NET 双向互操作已验证: ```cangjie let encrypted = SimApiAesUtil.encrypt("明文", "key字符串") // Base64(随机IV + 密文) let plain = SimApiAesUtil.decrypt(encrypted, "key字符串") ``` - 密钥:`SHA256(key 字符串)` → 32 字节;IV 每次随机 16 字节前置;输出 `Base64(IV + 密文)` - 供 `SimApiHttpClient.aesQuery` / `aesSignQuery` 使用 ### 4.2 实体基类 — SimApiBaseModel(对齐 C# SimApiBaseModel) ```cangjie import simcu::simapi.models.* public class User <: SimApiBaseModel { public var _name: String = "" } let user = User() // _id 自动 GUID、_createdAt/_updatedAt 自动当前时间 user.mapData(source) // 反射映射:源对象同名同类型字段 → this(忽略 Id/CreatedAt/UpdatedAt) user.mapData(source, ["_name"]) // 白名单映射 user.updateTime() // 刷新 _updatedAt ``` ### 5. HTTP 客户端 — SimApiHttpClient 用于调用其他带签名/AES 的 SimApi 服务(**基于 stdx.net.http,不依赖 soulsoft_net_http**;内置 TLS:`https` 自动配置信任所有证书 + SNI): ```cangjie let client = SimApiHttpClient(options: SimApiHttpClientOptions()) // 配置 server/appId/appKey // 返回泛型 T(对齐 .NET SignQuery/AesQuery/AesSignQuery),T 任意类免约束 let resp1 = client.signQuery("/api/hello", body: "{\"a\":1}") let resp2 = client.aesQuery("/api/data", body: "{\"a\":1}") let resp3 = client.aesSignQuery("/api/data", body: "{\"a\":1}") ``` 签名参数名可配置(`signName / timestampName / nonceName / appIdName / signFields`,对齐 C# 的 virtual 属性)。AES 请求体用 `SimApiOneFieldRequest` 序列化为 `{"data":"密文"}`(对齐 C#)。 ### 5.1 请求日志 — enableRequestLog 记录每次请求的方法、URL、请求头、请求体、响应状态码、耗时与异常(对齐 C#): - 请求体按 **JSON 字段级截断**(仅对超长字符串字段截断,保留结构;非 JSON 整串截断) - 下游异常**捕获记录后重抛**(对齐 C# ExceptionDispatchInfo) ```cangjie SimApiExtensions.addSimApi(builder) { options => options.enableRequestLog = true options.simApiRequestLogOptions.showFullHeader = true // 打印完整 Header(默认只打 Token/Query-Id) options.simApiRequestLogOptions.requestStringLogLength = 200 // 请求体字段截断长度(0 不截断) } ``` 输出示例: ``` [GET] /hello *( RequestHeaders [Full] ) => {"host":"127.0.0.1:5000",...} *( RequestBody ) => {"name":"AAAA...(200)","image":"x"} *( Response [200] ) => 1.756400ms ``` ### 5.2 日志格式 — SimApiLogger `enableLogger`(默认 `true`)时自动使用 `SimApiLoggerProvider`,输出格式对齐 C# 原版: ``` [ 分类 ][ 时间:毫秒 ][ 级别 ] 消息内容 ``` 按级别着色:Debug 深紫 / Info 深青 / Warn 黄 / Error 红 / Fatal 深红。 ### 5.3 存储 — SimApiStorage(S3/MinIO,对齐 C# SimApiStorage) `enableSimApiStorage = true` 时注册 `SimApiStorage`(Scoped,内部自实现 AWS Signature V4,基于 stdx.net.http,无需 Minio SDK): ```cangjie SimApiExtensions.addSimApi(builder) { options => options.enableSimApiStorage = true options.configureSimApiStorage { storage => storage.endpoint = "http://192.168.0.2:9000" // 必须 http:// 或 https:// 开头 storage.serveUrl = "https://files.example.com" // 文件访问地址,不能以 / 结尾 storage.bucket = "app-files" storage.accessKey = "minioadmin" storage.secretKey = "minioadmin" } } ``` | 方法 | 说明 | |------|------| | `getUploadUrl(path, expire=7200)` | 上传预签名 URL,返回 `GetUploadUrlResponse(UploadUrl, DownloadUrl, Path)` | | `getDownloadUrl(path, expire=600)` | 下载预签名 URL | | `uploadFile(path, data, contentType="image/png")` | 直接 PUT 上传(字节数组) | | `deleteFiles(paths)` | 批量删除对象(S3 原生 DeleteObjects,一次请求删多个) | | `fullUrl(path)` / `getUrl(path)` | 补全访问 URL(`~/` 前缀依赖请求上下文) | | `getPath(url)` | 从 URL 还原相对路径(去掉 Endpoint/Bucket 或 ServeUrl 前缀) | > 说明:桶不存在时自动创建(对齐 C# BucketExists + MakeBucket,静态守卫只执行一次); > 预签名与上传使用 AWS SigV4(HMAC-SHA256 基于 stdx SHA256 自实现),已用 AWS 官方测试向量验证签名正确。 ### 6. 内置路由(UseSimApi 自动注册) | 路由 | 方法 | 条件 | 说明 | | ----------------- | -------- | ---------------------------- | -------------------------- | | `/user/info` | POST | `enableSimApiAuth` | 需登录,返回 LoginInfo | | `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 | | `/exception/{code}` | GET | 始终 | 错误反馈(抛 SimApiException) | 路由路径可自定义(`configureSimApiRoute`): ```cangjie options.configureSimApiRoute { route => route.userInfoRoute = Some("/my/user/info") // 自定义路径生效 route.logoutRoute = Some("/my/auth/logout") route.webConfigRoute = Some("/my/config") } ``` ### 7. 认证后处理 Hook — ISimApiAuthChecker 实现后每次认证成功都会调用(配合 `@SimApiAuth` 注解或手动 `requireLogin`): ```cangjie import simcu::simapi.interfaces.* class MyAuthChecker <: ISimApiAuthChecker { public func run(loginItem: SimApiLoginItem, token: String): Unit { // 认证成功后执行 } } ``` ### 8. 认证中心 SDK — AuthSDK(对齐 C# AuthSDK) `enableSimApiAuthGate = true` 时注册 `SimApiAuthClient` / `SimApiAuthCenter` / `SimApiAuthIam` 单例并挂载网关透传中间件: ```cangjie SimApiExtensions.addSimApi(builder) { options => options.enableSimApiAuthGate = true options.configureSimApiAuthCenter { auth => auth.server = "https://auth.example.com" auth.appId = "app-id" auth.appKey = "app-key" } } ``` | 类 | 说明 | |----|------| | `SimApiAuthClient` | `SimApiHttpClient` 子类,凭证取 AuthCenterOptions | | `SimApiAuthCenter` | 群组/Profile/内部应用/系统登录/安全验证等 12 个接口 + `VerifySign` | | `SimApiAuthIam` | 注册权限 / 获取权限标识 / 校验权限(无权限抛 403) | | `SimApiAuthCenterMiddleware` | 网关透传:`X-SimApi-Gate-Auth/Time/Sign` 三头 MD5 校验 → Base64 解码 LoginInfo | ```cangjie import simcu::simapi.authsdk.* let center = SimApiAuthCenter(client) // client 从 DI 注入 let groups = center.groupRelated(profileId) // 群组列表 let loginInfo = center.getLoginInfo(code) // 登录信息(场景校验) let iam = SimApiAuthIam(client) iam.checkPermission(profileId, "app:create") // 无权限抛 403 ``` --- ## SimApiOptions 完整配置 ```cangjie SimApiExtensions.addSimApi(builder) { options => options.redisConfiguration = "localhost:6379" // Redis(可选,支持 ,password=xxx,db=2) // 功能开关 options.enableSimApiAuth = false // Token 认证 options.enableSimApiCache = true // 缓存 options.enableSimApiException = true // 全局异常拦截 options.enableSimApiResponseFilter = true // 响应统一封装 options.enableSimApiHttpClient = false // HTTP 客户端 options.enableSimApiAuthGate = false // 认证中心 SDK + 网关中间件 options.enableRequestLog = false // 请求日志中间件 options.enableCors = true // 全量 CORS options.enableLogger = true // 控制台日志 // .NET 风格子模块配置回调(对齐 C# ConfigureSimApiXxx) options.configureSimApiRoute { route => route.userInfoRoute = Some("/user/info") // 内置路由自定义路径 route.logoutRoute = Some("/auth/logout") route.webConfigRoute = Some("/config") } options.configureSimApiRequestLog { opt => opt.showFullResponse = true opt.showFullHeader = false opt.requestStringLogLength = 50 } options.configureSimApiHttpClient { http => http.appId = "your-app-id" http.appKey = "your-app-key" http.server = "https://api.example.com" } options.configureSimApiAuthCenter { auth => auth.server = "https://auth.example.com" auth.appId = "auth-app-id" auth.appKey = "auth-app-key" } } ``` ## 内置控制器(MVC 写法) simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),`addSimApi` 自动注册内置控制器 + 自动扫描调用者包中的控制器(对齐 C# `Assembly.GetTypes()` 扫描): | 控制器 | 路由 | 说明 | |--------|------|------| | `SimApiCommonController` | `/exception/{code}`、`/config`、`/user/info` | 通用内置路由 | | `SimApiAuthController` | `/auth/logout` | 退出登录 | | `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken` / `requireLogin()` / `getLogin()` | ```cangjie import simcu::simapi.controllers.* import simcu::simapi.annotations.{SimApiAuth} // 控制器写法:继承 SimApiBaseController,注解路由 + DI 注入 @SimApiAuth // 类级鉴权(可选,替代 requireLogin) public class MyController <: SimApiBaseController { private let _auth: SimApiAuth public init(auth: SimApiAuth) { this._auth = auth } @HttpPost["my/route"] public func myAction(@FromBody request: MyRequest): String { "ok" } } ``` 宿主无需手动注册控制器:`builder.addSimApi {}` 内部自动扫描并注册。 --- ## 未实现模块(选项占位) 以下 C# 原包功能因仓颉生态暂无对应库(Hangfire/MQTT/Swashbuckle),**选项保留但未实现**: | 选项 | 原功能 | 状态 | |------|--------|------| | `enableSimApiDoc` | Swagger 文档(可换 soulsoft_web_openapi) | ❌ 未实现 | | `enableSynapse` | MQTT 通信 | ❌ 未实现 | | `enableJob` | Hangfire 任务调度 | ❌ 未实现 | > ✅ 已实现(曾为占位):`enableSimApiStorage`(S3/MinIO,自实现 AWS SigV4)、`enableSimApiAuthGate`(AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC,与 .NET 双向互操作)、`ISimApiAuthChecker`、`@SimApiSign` / `@AesBody` 声明式注解、内置路由自定义路径。 --- ## 依赖 | 依赖 | 用途 | |------|------| | `soulsoft_web_http / routing / hosting` | Web 框架(ASP.NET Core 仓颉移植版) | | `soulsoft_extensions_logging` 系列 | 日志 | | `soulsoft_extensions_injection` | 依赖注入 | | `soulsoft_extensions_configuration` | 配置 | | `simcu::serialization`(path 依赖) | JSON 序列化(simapi 自研,反射免标注) | | `redis`(pkg.cangjie-lang.cn) | Redis 客户端(认证/缓存 Redis 模式) | | `stdx`(CANGJIE_STDX_PATH) | 标准扩展库(md5/sha1/base64/http/tls) | > 构建前需设置 `CANGJIE_STDX_PATH` 指向本地 stdx 的 `static/stdx` 目录。 --- ## 许可证 MIT