Files
simapi-cj/README.md
T
xrain 8b0a8f783c refactor: SimApiExtensions 移入根包 simapi 并改为静态类,删除 extensions 子包
- src/extensions/SimApiExtensions.cj 删除,内容合并进 src/SimApiExtensions.cj(package simapi)
- 接口+extend 改为静态类 SimApiExtensions:
  SimApiExtensions.addSimApi(builder, configure) / useSimApi(host)(对齐 .NET 根命名空间静态类)
- 私有辅助 addSimApiCore / addControllers 收敛为私有静态方法
- 用法从 builder.addSimApi{} / host.useSimApi() 改为 SimApiExtensions.addSimApi(builder){} / useSimApi(host)
2026-08-17 09:16:42 +08:00

525 lines
20 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 客户端**等 API 基础能力。
---
## 快速开始
```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 simapi.*
import simapi.communications.*
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)
// 业务接口:返回统一响应格式
host.mapGet("hello") {
context =>
context.response.write(SimApiBaseResponse().toJsonString(dataJson: "\"hello cangjie.\""))
}
host.run()
}
```
### 统一响应格式
所有接口输出 JSON,HTTP 状态码始终 `200`,错误信息在 `code` 字段:
| code | 含义 |
| ---- | ---------- |
| 200 | 成功 |
| 204 | 无数据 |
| 400 | 参数错误 |
| 401 | 需要登录 |
| 403 | 无权访问 |
| 404 | 资源不存在 |
| 500 | 服务器错误 |
### 异常处理流程
```
请求 → SimApiExceptionMiddleware(全异常捕获→HTTP 200+JSON)
→ SimApiAuthMiddleware(Token→LoginInfo)
→ 路由 → 业务处理
```
---
## 项目结构
```
simapi-cj/
├── cjpm.toml # 包配置
├── src/
│ ├── SimApiExtensions.cj # 根包入口:SimApiExtensions 静态类(addSimApi / useSimApi + 内置路由 + 响应封装)
│ ├── attributes/ # 声明式注解:@SimApiAuth(鉴权)、@OriginResponse(原样响应)
│ ├── 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
│ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志)
│ ├── macros/ # ReadTomlVersion(编译期读版本号)
│ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware
│ └── models/ # SimApiBaseModel(实体基类)
```
---
## 模块说明
### 1. 错误处理 — SimApiError
```cangjie
import simapi.helpers.*
SimApiError.error(500, "服务器内部错误") // 直接抛错
SimApiError.errorWhen(amount <= 0, 400, "金额无效") // 条件为 true 时抛错
SimApiError.errorWhenFalse(hasPermission, 403, "无权操作")
SimApiError.errorWhenNull(someOptional, 404, "用户不存在")
```
### 2. 认证 — SimApiAuth
```cangjie
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>` 或 Query `token=<value>`
### 2.1 声明式鉴权 — @SimApiAuth(注解类,对齐 C# [SimApiAuth]
标注在控制器**方法或类**上,请求派发时自动执行鉴权(未登录 401 → 类型不匹配 403 → 遍历执行 `ISimApiAuthChecker`),替代手动 `requireLogin()`
```cangjie
import simapi.attributes.{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(",")`);空参数表示任意已登录用户。多个 `ISimApiAuthChecker` 通过 `SimApiOptions.authCheckers` 注册(由 addSimApi 扫描调用者包填充)。
### 2.2 原样响应 — @OriginResponse
标注后跳过统一响应封装,接口返回什么就输出什么(对齐 C# `[OriginResponse]`):
```cangjie
import simapi.attributes.{OriginResponse}
@OriginResponse
@HttpGet["raw"]
public func raw(): String {
"{\"raw\":true}" // 直接输出,不包 {code,message,data}
}
```
### 2.3 服务端验签 — SimApiSignChecker(对齐 C# [SimApiSign]
校验带签名请求(appId 提取 → 密钥获取 → timestamp 过期校验 → nonce 去重 → MD5 比对):
```cangjie
import simapi.helpers.{SimApiSignProviderBase, SimApiSignChecker}
// 1. 继承 Provider 实现密钥获取
public class MySignProvider <: SimApiSignProviderBase {
public override func getKey(appId: ?String): ?String {
// 根据 appId 返回密钥(如查库)
Some("my-secret-key")
}
}
// 2. 控制器方法开头调用校验
public func signedAction(): String {
SimApiSignChecker.verify(context, provider, cache)
"ok"
}
```
Provider 可配置:`appIdName` / `timestampName` / `nonceName` / `signName` / `queryExpires` / `duplicateRequestProtection` / `signFields`(与 C# `SimApiSignProviderBase` 一致)。
### 2.4 AES body 解密 — SimApiAesBodyChecker(对齐 C# [AesBody]
服务端接收 `{"data":"密文"}` 加密 body,解密后返回明文 JSON(控制器再反序列化为目标类型):
```cangjie
import simapi.helpers.{AesBodyProviderBase, SimApiAesBodyChecker}
public class MyAesProvider <: AesBodyProviderBase {
public override func getKey(appId: ?String): ?String {
Some("aes-secret-key")
}
}
public func create(@FromBody req: AesBodyRequest): String {
let json = SimApiAesBodyChecker.decryptBody(context, provider) // 解密后的 JSON 字符串
let dto = JsonSerializer.deserializeObject<MyDto>(json)
"ok"
}
```
### 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.fromJson<T>(json) // JSON → T(对齐 C# FromJson<T>T 需 ISerialization<T>
SimApiUtil.base64DecodeTo<T>(str) // Base64 → JSON → T(对齐 C# Base64Decode<T>
SimApiUtil.checkCell("13800138000") // 手机号
SimApiUtil.checkEmail("a@b.com") // 邮箱
```
### 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 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 服务(**内置 TLS 支持**`https` 自动配置信任所有证书 + SNI,仓颉生态下 stdx TLS 动态加载 openssl 可用):
```cangjie
let client = SimApiHttpClient(options: SimApiHttpClientOptions()) // 配置 server/appId/appKey
// 返回泛型 T(对齐 .NET SignQuery<T>/AesQuery<T>/AesSignQuery<T>),T 需实现 ISerialization<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}")
```
签名参数名可配置(`SimApiHttpClient` 实例属性 `signName / timestampName / nonceName / appIdName / signFields`,对齐 C# 的 virtual 属性)。
### 5.1 请求日志 — enableRequestLog
记录每次请求的方法、URL、请求头、请求体、响应状态码、耗时与异常(对齐 C#):
- 请求体按 **JSON 字段级截断**(仅对超长字符串字段截断,保留结构;非 JSON 整串截断)
- 下游异常**捕获记录后重抛**(对齐 C# ExceptionDispatchInfo
- 响应体因 soulsoft `HttpResponse.body` 只读不可替换,记录 `Content-Length` 作为替代(C# 用 MemoryStream 捕获)
```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`(替换 soulsoft 默认控制台格式),输出格式对齐 C# 原版:
```
[ 分类 ][ 时间:毫秒 ][ 级别 ]
消息内容
```
按级别着色:
| 级别 | 颜色 |
|------|------|
| Debug | 深紫(DarkMagenta |
| Info | 深青(DarkCyan |
| Warn | 黄(Yellow |
| Error | 红(Red |
| Fatal | 深红(DarkRed 粗体近似) |
| 其他 | 白(White |
### 5.3 存储 — SimApiStorageS3/MinIO,对齐 C# SimApiStorage
`enableSimApiStorage = true` 时注册 `SimApiStorage`Scoped,内部自实现 AWS Signature V4,无需 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 官方测试向量验证签名正确。
> 注册为 Scoped 是为了注入 `IHttpContextAccessor`soulsoft DI 禁止 singleton 消费 scoped 服务)。
### 6. 内置路由(UseSimApi 自动注册)
| 路由 | 方法 | 条件 | 说明 |
| ----------------- | -------- | ---------------------------- | -------------------------- |
| `/user/info` | POST | `enableSimApiAuth` | 需登录,返回 LoginInfo |
| `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 |
| `/exception/{code}` | GET | 始终 | 错误反馈(抛 SimApiException |
路由路径可自定义(`configureSimApiRoute`,自定义值通过 `mapGet/mapPost` 真实注册,默认值由内置控制器特性路由覆盖):
```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 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 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()` 扫描,见 `SimApiControllerScanner`):
| 控制器 | 路由 | 说明 |
|--------|------|------|
| `SimApiCommonController` | `/exception/{code}``/config``/user/info` | 通用内置路由 |
| `SimApiAuthController` | `/auth/logout` | 退出登录 |
| `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken` / `requireLogin()` / `getLogin()` |
```cangjie
import simapi.controllers.*
import simapi.attributes.{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/MinIO/Swashbuckle),**选项保留但未实现**:
| 选项 | 原功能 | 状态 |
|------|--------|------|
| `enableSimApiDoc` | Swagger 文档(可换 soulsoft_web_openapi | ❌ 未实现 |
| `enableSynapse` | MQTT 通信 | ❌ 未实现 |
| `enableJob` | Hangfire 任务调度 | ❌ 未实现 |
> ✅ 已实现(曾为占位):`enableSimApiStorage`S3/MinIO,自实现 AWS SigV4)、`enableSimApiAuthGate`AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC,与 .NET 双向互操作)、`ISimApiAuthChecker`(注解鉴权时执行)、内置路由自定义路径。
---
## 依赖
| 依赖 | 用途 |
|------|------|
| `soulsoft_web_http / routing / hosting` | Web 框架(ASP.NET Core 仓颉移植版) |
| `soulsoft_extensions_logging` 系列 | 日志 |
| `soulsoft_extensions_injection` | 依赖注入 |
| `soulsoft_extensions_configuration` | 配置 |
| `soulsoft_serialization` | JSON 序列化 |
| `redis`pkg.cangjie-lang.cn | Redis 客户端(认证/缓存 Redis 模式) |
| `stdx`CANGJIE_STDX_PATH | 标准扩展库(md5/sha1/base64/http |
> 构建前需设置 `CANGJIE_STDX_PATH` 指向本地 stdx 的 `static/stdx` 目录。
---
## 许可证
MIT