689 lines
26 KiB
Markdown
689 lines
26 KiB
Markdown
# SimApi for Cangjie(simapi)
|
||
|
||
> 仓颉版 SimApi:ASP.NET Core 风格 API 基础框架,移植自 [SimApi](https://github.com/SimcuTeam/simapi-net)。
|
||
|
||
提供**统一响应格式、异常拦截、Token 认证、缓存、工具集、HTTP 客户端、S3 存储、声明式注解、OpenAPI 文档**等 API 基础能力。
|
||
|
||
---
|
||
|
||
## 引入
|
||
|
||
两种方式任选其一:
|
||
|
||
**方式一:中央仓**(需先 `cjpm publish` 发布 `simcu::simapi`)
|
||
|
||
```toml
|
||
[dependencies]
|
||
"simcu::simapi" = "1.0.3"
|
||
```
|
||
|
||
**方式二:Git 仓库**
|
||
|
||
```toml
|
||
[dependencies]
|
||
"simcu::simapi" = { git = "https://gitcode.com/simcu/simapi-cj.git", version = "1.0.3" }
|
||
```
|
||
|
||
> 本地开发也可用 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/
|
||
│ ├── simapi_extensions.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/ # SimApiAuthChecker, BindRequestContext, SimApiSignProviderBase, AesBodyProviderBase
|
||
│ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志)
|
||
│ ├── macros/ # ReadTomlVersion(编译期读版本号)、EnumString(枚举字符串双向转换)
|
||
│ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware
|
||
│ ├── models/ # SimApiBaseModel(实体基类)
|
||
│ └── openapi/ # OpenAPI 文档生成 + Swagger UI 内置资源
|
||
│ ├── annotations/ # @SimApiDoc(文档元数据注解)
|
||
│ ├── metadata/ # IApiGroupNamesProvider, IApiResponseTypeMetadata 等元数据接口
|
||
│ ├── models/ # OpenApiDocument, OpenApiSchema, OpenApiInfo 等 OpenAPI 模型
|
||
│ ├── services/ # OpenApiDocumentService(文档生成), OpenApiSchemaService, OpenApiOptions
|
||
│ └── infrastructure/ # OpenApiConstants
|
||
```
|
||
|
||
---
|
||
|
||
## 模块说明
|
||
|
||
### 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 模式**:零配置,适合开发/测试;登录态带过期时间,重启后丢失
|
||
- **Token 传参**:Header `Token: <value>` 或 Query `token=<value>`
|
||
|
||
### 2.1 声明式鉴权 — @SimApiAuth
|
||
|
||
标注在控制器**方法或类**上,请求派发时自动执行鉴权(未登录 401 → 类型不匹配 403 → 遍历执行 `SimApiAuthChecker`):
|
||
|
||
```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"]`);空参数表示任意已登录用户。
|
||
|
||
### 2.2 原样响应 — @OriginResponse
|
||
|
||
标注后跳过统一响应封装,接口返回什么就输出什么:
|
||
|
||
```cangjie
|
||
import simcu::simapi.annotations.{OriginResponse}
|
||
|
||
@OriginResponse
|
||
@HttpGet["raw"]
|
||
public func raw(): String {
|
||
"{\"raw\":true}" // 直接输出,不包 {code,message,data}
|
||
}
|
||
```
|
||
|
||
### 2.3 声明式验签 — @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`。也可手动调用 `SimApiSignChecker.verify(context, provider, cache)`。
|
||
|
||
### 2.4 声明式 AES body — @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
|
||
SimApiUtil.md5("text") // 32 位十六进制
|
||
SimApiUtil.sha1("text") // 40 位
|
||
SimApiUtil.base64Encode("text") / base64Decode("...")
|
||
SimApiUtil.base64Encode(obj) // 对象 → JSON → Base64
|
||
SimApiUtil.json(obj) // 对象 → JSON 字符串(simcu::serialization 反射)
|
||
SimApiUtil.escapeJson(s) // JSON 字符串转义
|
||
SimApiUtil.fromJson<T>(json) // JSON → 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
|
||
|
||
纯仓颉实现 AES-256-CBC + PKCS7(S-box/密钥扩展/轮函数),加解密结果跨语言互通已验证:
|
||
|
||
```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
|
||
|
||
```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(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`)。AES 请求体用 `SimApiOneFieldRequest<String>` 序列化为 `{"data":"密文"}`。
|
||
|
||
### 5.1 请求日志 — enableRequestLog
|
||
|
||
记录每次请求的方法、URL、请求头、请求体、响应状态码、耗时与异常:
|
||
- 请求体按 **JSON 字段级截断**(仅对超长字符串字段截断,保留结构;非 JSON 整串截断)
|
||
- 下游异常**捕获记录后重抛**
|
||
|
||
```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`,输出格式与原版一致:
|
||
|
||
```
|
||
[ 分类 ][ 时间:毫秒 ][ 级别 ]
|
||
消息内容
|
||
```
|
||
|
||
按级别着色:Debug 深紫 / Info 深青 / Warn 黄 / Error 红 / Fatal 深红。
|
||
|
||
### 5.3 存储 — SimApiStorage(S3/MinIO)
|
||
|
||
`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 前缀) |
|
||
|
||
> 说明:桶不存在时自动创建(静态守卫只执行一次);
|
||
> 预签名与上传使用 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 — SimApiAuthChecker
|
||
|
||
实现后每次认证成功都会调用(配合 `@SimApiAuth` 注解):
|
||
|
||
```cangjie
|
||
import simcu::simapi.interfaces.*
|
||
|
||
class MyAuthChecker <: SimApiAuthChecker {
|
||
public func run(loginItem: SimApiLoginItem, token: String): Unit {
|
||
// 认证成功后执行
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8. 认证中心 SDK — 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
|
||
```
|
||
|
||
---
|
||
|
||
## OpenAPI 文档 — enableSimApiDoc
|
||
|
||
`enableSimApiDoc = true` 时自动生成 OpenAPI 3.0 JSON 文档并内置 Swagger UI 静态资源(无需外部文件,打包后不失效)。
|
||
|
||
### 文档分组
|
||
|
||
支持多个文档组,未标注 `@SimApiDoc` 的接口默认进入默认组文档:
|
||
|
||
```cangjie
|
||
SimApiExtensions.addSimApi(builder) { options =>
|
||
options.enableSimApiDoc = true
|
||
options.configureSimApiDoc { doc =>
|
||
doc.apiGroups.add(SimApiDocGroup("api", name: "App接口", description: "对接App相关接口"))
|
||
doc.apiGroups.add(SimApiDocGroup("admin", name: "后台管理接口", description: "后台管理接口"))
|
||
}
|
||
}
|
||
```
|
||
|
||
- `SimApiDocGroup(id, name!, description!, isDefault!)`:`name` 默认取 `id` 值
|
||
- `distinctGroups()`:按 id 去重,**用户配置覆盖默认值**(保留最后出现)
|
||
- 若所有组均未标记 `isDefault`,第一个组视为默认组
|
||
|
||
### @SimApiDoc 注解
|
||
|
||
```cangjie
|
||
import simcu::simapi.openapi.annotations.*
|
||
|
||
@SimApiDoc[tags: "登录", summary: "用户登录"]
|
||
@SimApiDoc[tags: "认证", summary: "后台登录", groupNames: "admin"]
|
||
@SimApiDoc[tags: "公共", groupNames: "api,admin"] // 同时出现在 api 和 admin 文档
|
||
@SimApiDoc[tags: "公共", groupNames: "*"] // 出现在所有文档
|
||
@SimApiDoc[ignore: true] // 不出现在文档中
|
||
```
|
||
|
||
- `groupNames`:逗号分隔多个组名,`*` 表示所有文档,空串表示未分组(仅进默认文档)
|
||
- `ignore: true`:从文档中隐藏
|
||
|
||
### 路由前缀
|
||
|
||
```cangjie
|
||
options.configureSimApiDoc { doc =>
|
||
doc.urlPrefix = "docs" // 默认值,可自定义
|
||
}
|
||
```
|
||
|
||
| 路由 | 说明 |
|
||
|------|------|
|
||
| `/{prefix}/all.html` | 多文档切换页(顶部栏下拉选择所有文档) |
|
||
| `/{prefix}/{id}.html` | 单文档页(无顶部栏,自动加载 `{id}.json`) |
|
||
| `/{prefix}/urls` | 文档列表 JSON(供 Swagger UI 下拉) |
|
||
| `/{prefix}/{id}.json` | OpenAPI 文档 JSON |
|
||
|
||
> Swagger UI 静态资源(CSS/JS/HTML)以 Base64 内联编译,运行时由 `OpenApiUIMiddleware` 解码输出。资源更新后运行 `pwsh tools/gen-swagger-ui-resources.ps1` 重新生成。
|
||
|
||
### 响应封装与文档
|
||
|
||
接口返回值自动封装为统一响应格式,**文档中 response schema 也体现封装**:
|
||
|
||
| 返回类型 | `@OriginResponse` | 文档 response schema |
|
||
|----------|-------------------|---------------------|
|
||
| `SimApiBaseResponse`/`SimApiResponse<T>`/`SimApiDataResponse` | - | 原样不封装 |
|
||
| 任意类型 | ✓ | 原样不封装 |
|
||
| `Unit` (void) | ✗ | `{code: int64, message: string}` |
|
||
| `String` | ✗ | `{code, message, data: {type: string}}` |
|
||
| DTO | ✗ | `{code, message, data: {$ref: DTO}}` |
|
||
|
||
### 认证锁图标
|
||
|
||
仅标注了 `@SimApiAuth` 的接口在文档中显示锁图标(operation 级 `security`),未标注的接口不显示。
|
||
|
||
动态注册的路由(lambda)需用 `withSimApiAuth` 扩展方法手动添加认证元数据:
|
||
|
||
```cangjie
|
||
host.mapPost(route, { context => ... })
|
||
.withOpenApi(SimApiDoc(tags: "认证", summary: "获取用户信息"))
|
||
.withSimApiAuth(SimApiAuth())
|
||
.withResponseType(TypeInfo.of<SimApiLoginItem>())
|
||
```
|
||
|
||
- `withSimApiAuth`:添加 `@SimApiAuth` 元数据(显示锁图标)
|
||
- `withResponseType`:添加响应类型元数据(动态路由无 `ControllerActionDescriptor`,需手动指定返回类型才能生成 response schema)
|
||
|
||
---
|
||
|
||
### EnumString — 枚举字符串双向转换
|
||
|
||
仓颉 1.1.3 枚举没有默认 `ToString`,且字符串插值要求实现 `ToString` 接口(见 RULE.MD §6.5)。在枚举声明上标注 `@EnumString`,编译期自动在枚举体外生成 extend,提供纯成员名的双向转换:
|
||
|
||
```cangjie
|
||
import simcu::simapi.macros.*
|
||
|
||
@EnumString
|
||
public enum AssetKind {
|
||
| Passport
|
||
| Package
|
||
}
|
||
|
||
// 生成(示意):
|
||
// extend AssetKind <: ToString {
|
||
// public func toString(): String { ... } // "Passport" / "Package"
|
||
// public static func fromString(s: String): AssetKind // 反查,未匹配抛 Exception
|
||
// }
|
||
|
||
let kind = AssetKind.fromString("Passport") // AssetKind.Passport
|
||
let s = "${kind}" // "Passport"
|
||
// AssetKind.fromString("Unknown") // 抛 Exception: AssetKind.fromString: 未知枚举值 'Unknown'
|
||
```
|
||
|
||
- 与 `@Derive[ToString]` 的区别:Derive 输出带类型名前缀(`Kind.A`),本宏输出**纯成员名**,适合写库 / 读库还原 / 字符串插值。
|
||
- 仅支持**纯无参构造器**枚举;带参数构造器、`...`(non-exhaustive)在编译期直接报错。
|
||
- 生成的是普通 extend,**零运行时开销**(match 穷尽构造器,无反射)。
|
||
|
||
### ReadTomlVersion — 编译期读版本号
|
||
|
||
读取 cjpm.toml 的 `version` 字段并替换变量声明,用于把版本号写进响应头等场景:
|
||
|
||
```cangjie
|
||
import simcu::simapi.macros.*
|
||
|
||
@ReadTomlVersion[path: "cjpm.toml"]
|
||
let appVersion: String = ""
|
||
|
||
@ReadTomlVersion[path: "../simapi-cj/cjpm.toml"] // 读取其他包版本
|
||
let simApiVersion: String = ""
|
||
```
|
||
|
||
---
|
||
|
||
## 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.enableSimApiDoc = false // OpenAPI 文档 + Swagger UI
|
||
options.enableRequestLog = false // 请求日志中间件
|
||
options.enableCors = true // 全量 CORS
|
||
options.enableLogger = true // 控制台日志
|
||
|
||
// 子模块配置回调(ConfigureSimApiXxx)
|
||
options.configureSimApiRoute { route =>
|
||
route.userInfoRoute = Some("/user/info") // 内置路由自定义路径
|
||
route.logoutRoute = Some("/auth/logout")
|
||
route.webConfigRoute = Some("/config")
|
||
}
|
||
options.configureSimApiDoc { doc =>
|
||
doc.urlPrefix = "docs" // 文档路由前缀(默认 "docs")
|
||
doc.apiGroups.add(SimApiDocGroup("api", name: "App接口", description: "App接口文档"))
|
||
doc.apiGroups.add(SimApiDocGroup("admin", name: "后台管理", description: "后台管理接口"))
|
||
}
|
||
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` 自动注册内置控制器 + 自动扫描调用者包中的控制器:
|
||
|
||
| 控制器 | 路由 | 说明 |
|
||
|--------|------|------|
|
||
| `SimApiCommonController` | `/exception/{code}`、`/config`、`/user/info` | 通用内置路由 |
|
||
| `SimApiAuthController` | `/auth/logout` | 退出登录 |
|
||
| `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken`(访问时自动校验登录,未登录抛 401) |
|
||
|
||
```cangjie
|
||
import simcu::simapi.controllers.*
|
||
import simcu::simapi.annotations.{SimApiAuth}
|
||
|
||
// 控制器写法:继承 SimApiBaseController,注解路由 + DI 注入
|
||
@SimApiAuth // 类级鉴权
|
||
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 {}` 内部自动扫描并注册。
|
||
|
||
---
|
||
|
||
## 未实现模块(选项占位)
|
||
|
||
以下原包功能因仓颉生态暂无对应库(Hangfire/MQTT),**选项保留但未实现**:
|
||
|
||
| 选项 | 原功能 | 状态 |
|
||
|------|--------|------|
|
||
| `enableSynapse` | MQTT 通信 | ❌ 未实现 |
|
||
| `enableJob` | Hangfire 任务调度 | ❌ 未实现 |
|
||
|
||
> ✅ 已实现(曾为占位):`enableSimApiDoc`(OpenAPI 文档 + Swagger UI)、`enableSimApiStorage`(S3/MinIO,自实现 AWS SigV4)、`enableSimApiAuthGate`(AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC)、`SimApiAuthChecker`、`@SimApiSign` / `@AesBody` 声明式注解、内置路由自定义路径。
|
||
|
||
---
|
||
|
||
## 依赖
|
||
|
||
| 依赖 | 用途 |
|
||
|------|------|
|
||
| `soulsoft_web_http / routing / hosting` | Web 框架 |
|
||
| `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) |
|
||
| `soulsoft_web_mvc` | MVC 框架(控制器路由、模型绑定) |
|
||
|
||
> 构建前需设置 `CANGJIE_STDX_PATH` 指向本地 stdx 的 `static/stdx` 目录。
|
||
> OpenAPI Swagger UI 静态资源内置(Base64 内联),更新资源后运行 `pwsh tools/gen-swagger-ui-resources.ps1`。
|
||
|
||
---
|
||
|
||
## 许可证
|
||
|
||
MIT
|