- annotations 子包(原 attributes) - @SimApiSign / @AesBody 声明式注解(替代手动调用) - SimApiSignProviderBase / AesBodyProviderBase 移至 interfaces - SimApiHttpClient/SimApiStorage/verifySign 基于 stdx.net.http(不依赖 soulsoft_net_http) - SimApiUtil.json/escapeJson 直接实现、fromJson 免约束 - 依赖表:去掉 soulsoft_serialization/soulsoft_net_http,加 simapi_serialization
SimApi for Cangjie(simapi)
仓颉版 SimApi:ASP.NET Core 风格 API 基础框架,移植自 C# 项目 SimApi(
E:\simcu\simapi-net)。
提供统一响应格式、异常拦截、Token 认证、缓存、工具集、HTTP 客户端、S3 存储、声明式注解等 API 基础能力。
快速开始
package your_app
import soulsoft_web_http.*
import soulsoft_web_routing.*
import soulsoft_web_hosting.*
import soulsoft_extensions_logging.*
import soulsoft_extensions_injection.*
import simapi.*
import simapi.communications.*
import simapi.helpers.*
main(args: Array<String>) {
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<String>("hello cangjie."))))
}
host.run()
}
统一响应格式
所有接口输出 JSON,HTTP 状态码始终 200,错误信息在 code 字段:
| code | 含义 |
|---|---|
| 200 | 成功 |
| 204 | 无数据 |
| 400 | 参数错误 |
| 401 | 需要登录 |
| 403 | 无权访问 |
| 404 | 资源不存在 |
| 500 | 服务器错误 |
响应 JSON(经 simapi_serialization 反射序列化,字段无下划线):
{ "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
import simapi.helpers.*
SimApiError.error(500, "服务器内部错误") // 直接抛错
SimApiError.errorWhen(amount <= 0, 400, "金额无效") // 条件为 true 时抛错
SimApiError.errorWhenFalse(hasPermission, 403, "无权操作")
SimApiError.errorWhenNull(someOptional, 404, "用户不存在")
2. 认证 — SimApiAuth
import simapi.helpers.*
import 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: <value>或 Querytoken=<value>
2.1 声明式鉴权 — @SimApiAuth(对齐 C# [SimApiAuth])
标注在控制器方法或类上,请求派发时自动执行鉴权(未登录 401 → 类型不匹配 403 → 遍历执行 ISimApiAuthChecker):
import 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]):
import simapi.annotations.{OriginResponse}
@OriginResponse
@HttpGet["raw"]
public func raw(): String {
"{\"raw\":true}" // 直接输出,不包 {code,message,data}
}
2.3 声明式验签 — @SimApiSign(对齐 C# [SimApiSign])
标注在控制器方法或类上,请求派发时自动验签(appId 提取 → 密钥获取 → timestamp 过期校验 → nonce 去重 → MD5 比对):
import simapi.annotations.{SimApiSign}
import 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 并反序列化为参数类型:
import simapi.annotations.{AesBody}
import 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
// 由 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
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 字符串(simapi_serialization 反射)
SimApiUtil.escapeJson(s) // JSON 字符串转义
SimApiUtil.fromJson<T>(json) // JSON → T(对齐 C# FromJson<T>,任意类免约束)
SimApiUtil.base64DecodeTo<T>(str) // Base64 → JSON → T
SimApiUtil.checkCell("13800138000") // 手机号
SimApiUtil.checkEmail("a@b.com") // 邮箱
JSON 序列化/反序列化统一走 simapi_serialization(
JsonSerializer.Serialize/Deserialize<T>),任意类免标注、免接口约束。
4.1 AES 加解密 — SimApiAesUtil(对齐 C# SimApiAesUtil)
纯仓颉实现 AES-256-CBC + PKCS7(S-box/密钥扩展/轮函数),与 .NET 双向互操作已验证:
let encrypted = SimApiAesUtil.encrypt("明文", "key字符串") // Base64(随机IV + 密文)
let plain = SimApiAesUtil.decrypt(encrypted, "key字符串")
- 密钥:
SHA256(key 字符串)→ 32 字节;IV 每次随机 16 字节前置;输出Base64(IV + 密文) - 供
SimApiHttpClient.aesQuery<T>/aesSignQuery<T>使用
4.2 实体基类 — SimApiBaseModel(对齐 C# SimApiBaseModel)
import 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):
let client = SimApiHttpClient(options: SimApiHttpClientOptions()) // 配置 server/appId/appKey
// 返回泛型 T(对齐 .NET SignQuery<T>/AesQuery<T>/AesSignQuery<T>),T 任意类免约束
let resp1 = client.signQuery<SimApiLoginItem>("/api/hello", body: "{\"a\":1}")
let resp2 = client.aesQuery<SimApiLoginItem>("/api/data", body: "{\"a\":1}")
let resp3 = client.aesSignQuery<SimApiLoginItem>("/api/data", body: "{\"a\":1}")
签名参数名可配置(signName / timestampName / nonceName / appIdName / signFields,对齐 C# 的 virtual 属性)。AES 请求体用 SimApiOneFieldRequest<String> 序列化为 {"data":"密文"}(对齐 C#)。
5.1 请求日志 — enableRequestLog
记录每次请求的方法、URL、请求头、请求体、响应状态码、耗时与异常(对齐 C#):
- 请求体按 JSON 字段级截断(仅对超长字符串字段截断,保留结构;非 JSON 整串截断)
- 下游异常捕获记录后重抛(对齐 C# ExceptionDispatchInfo)
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):
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):
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):
import simapi.interfaces.*
class MyAuthChecker <: ISimApiAuthChecker {
public func run(loginItem: SimApiLoginItem, token: String): Unit {
// 认证成功后执行
}
}
8. 认证中心 SDK — AuthSDK(对齐 C# AuthSDK)
enableSimApiAuthGate = true 时注册 SimApiAuthClient / SimApiAuthCenter / SimApiAuthIam 单例并挂载网关透传中间件:
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 |
import 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 完整配置
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() |
import simapi.controllers.*
import 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 |
配置 |
simapi_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