Files
simapi-cj/README.md
T

548 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SimApi for Cangjiesimapi
> 仓颉版 SimApiASP.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<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(经 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(验签)、@AesBodyAES body 解密)
│ ├── authsdk/ # 认证中心 SDKSimApiAuthClient/Center/Iam + 网关中间件 + DTO
│ ├── communications/ # SimApiBaseResponse, PageResponse, SimApiLoginItem, 请求 DTO
│ ├── configurations/ # SimApiOptions + 各模块 Option(含 ConfigureSimApiXxx 回调)
│ ├── controllers/ # SimApiBaseController, SimApiCommonController, SimApiAuthControllerMVC 写法)
│ ├── 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: <value>` 或 Query `token=<value>`
### 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<T>(json) // JSON → T(对齐 C# FromJson<T>,任意类免约束)
SimApiUtil.base64DecodeTo<T>(str) // Base64 → JSON → T
SimApiUtil.checkCell("13800138000") // 手机号
SimApiUtil.checkEmail("a@b.com") // 邮箱
```
> JSON 序列化/反序列化统一走 **simcu::serialization**`JsonSerializer.Serialize` / `Deserialize<T>`),任意类免标注、免接口约束。
### 4.1 AES 加解密 — SimApiAesUtil(对齐 C# SimApiAesUtil
纯仓颉实现 AES-256-CBC + PKCS7S-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<T>` / `aesSignQuery<T>` 使用
### 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<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
```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 存储 — SimApiStorageS3/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 SigV4HMAC-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