first version
This commit is contained in:
@@ -0,0 +1 @@
|
||||
target/
|
||||
@@ -0,0 +1,326 @@
|
||||
# SimApi for Cangjie(simapi)
|
||||
|
||||
> 仓颉版 SimApi:ASP.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.extensions.*
|
||||
import simapi.communications.*
|
||||
|
||||
main(args: Array<String>) {
|
||||
let builder = WebHost.createBuilder(args)
|
||||
builder.services.addRouting()
|
||||
builder.services.addLogging()
|
||||
|
||||
// 注册 SimApi 服务(与 addLogging 同样式)
|
||||
builder.addSimApi { options =>
|
||||
options.enableSimApiAuth = true // Token 认证(未配 Redis 自动用 InMemory)
|
||||
options.enableSimApiCache = true // 缓存
|
||||
options.enableSimApiException = true // 全局异常拦截
|
||||
}
|
||||
|
||||
let host = builder.build()
|
||||
host.useSimApi()
|
||||
|
||||
// 业务接口:返回统一响应格式
|
||||
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/
|
||||
│ ├── communications/ # SimApiBaseResponse, PageResponse, SimApiLoginItem, ApiResult, 请求 DTO
|
||||
│ ├── configurations/ # SimApiOptions + 各模块 Option(含 ConfigureSimApiXxx 回调)
|
||||
│ ├── controllers/ # SimApiBaseController, SimApiCommonController, SimApiAuthController(MVC 写法)
|
||||
│ ├── exceptions/ # SimApiException
|
||||
│ ├── extensions/ # SimApiExtensions(addSimApi / useSimApi + 内置路由)
|
||||
│ ├── helpers/ # SimApiError, SimApiUtil, SimApiAuth, SimApiCache, SimApiHttpClient
|
||||
│ ├── interfaces/ # ISimApiAuthChecker
|
||||
│ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志)
|
||||
│ └── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模块说明
|
||||
|
||||
### 1. 错误处理 — SimApiError
|
||||
|
||||
```cangjie
|
||||
import simapi.helpers.*
|
||||
|
||||
SimApiError.error(500, "服务器内部错误") // 直接抛错
|
||||
SimApiError.errorWhen(amount <= 0, 400, "金额无效") // 条件为 true 时抛错
|
||||
SimApiError.errorWhenFalse(hasPermission, 403, "无权操作")
|
||||
SimApiError.errorWhenNone(someOptional, 404, "用户不存在")
|
||||
```
|
||||
|
||||
### 2. 认证 — SimApiAuth
|
||||
|
||||
```cangjie
|
||||
import simapi.helpers.*
|
||||
import simapi.communications.*
|
||||
|
||||
let auth = SimApiAuth(redisConfiguration: "") // 配 Redis 用 Redis,否则 InMemory
|
||||
|
||||
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"`)时使用,支持多实例共享
|
||||
- **InMemory 模式**:零配置,适合开发/测试;重启后登录态丢失
|
||||
- **Token 传参**:Header `Token: <value>` 或 Query `token=<value>`
|
||||
|
||||
### 3. 缓存 — SimApiCache
|
||||
|
||||
```cangjie
|
||||
let cache = SimApiCache(redisConfiguration: "")
|
||||
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.md5("text") // 32 位十六进制
|
||||
SimApiUtil.sha1("text") // 40 位
|
||||
SimApiUtil.base64Encode("text") / base64Decode("...")
|
||||
SimApiUtil.checkCell("13800138000") // 手机号
|
||||
SimApiUtil.checkEmail("a@b.com") // 邮箱
|
||||
```
|
||||
|
||||
### 5. HTTP 客户端 — SimApiHttpClient
|
||||
|
||||
用于调用其他带签名/AES 的 SimApi 服务:
|
||||
|
||||
```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}")
|
||||
```
|
||||
|
||||
### 5.1 请求日志 — enableRequestLog
|
||||
|
||||
记录每次请求的方法、URL、请求头、请求体、响应状态码与耗时:
|
||||
|
||||
```cangjie
|
||||
builder.addSimApi { 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 ) =>
|
||||
|
||||
*( Response [200] ) => 1.756400ms
|
||||
```
|
||||
|
||||
### 5.2 日志格式 — SimApiLogger
|
||||
|
||||
`enableLogger`(默认 `true`)时自动使用 `SimApiLoggerProvider`(替换 soulsoft 默认控制台格式),输出格式对齐 C# 原版:
|
||||
|
||||
```
|
||||
[ 分类 ][ 时间:毫秒 ][ 级别 ]
|
||||
消息内容
|
||||
```
|
||||
|
||||
按级别着色:
|
||||
|
||||
| 级别 | 颜色 |
|
||||
|------|------|
|
||||
| Debug | 深紫(DarkMagenta) |
|
||||
| Info | 深青(DarkCyan) |
|
||||
| Warn | 黄(Yellow) |
|
||||
| Error | 红(Red) |
|
||||
| Fatal | 深红(DarkRed 粗体近似) |
|
||||
| 其他 | 白(White) |
|
||||
|
||||
### 6. 内置路由(UseSimApi 自动注册)
|
||||
|
||||
| 路由 | 方法 | 条件 | 说明 |
|
||||
| ----------------- | -------- | ---------------------------- | -------------------------- |
|
||||
| `/versions` | GET/POST | 始终 | 返回 SimApi/App 版本 |
|
||||
| `/user/info` | POST | `enableSimApiAuth` | 需登录,返回 LoginInfo |
|
||||
| `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 |
|
||||
| `/exception/{code}` | GET | 始终 | 错误反馈 |
|
||||
|
||||
### 7. 认证后处理 Hook — ISimApiAuthChecker
|
||||
|
||||
```cangjie
|
||||
import simapi.interfaces.*
|
||||
|
||||
class MyAuthChecker <: ISimApiAuthChecker {
|
||||
public func run(loginItem: SimApiLoginItem, token: String): Unit {
|
||||
// 认证成功后执行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SimApiOptions 完整配置
|
||||
|
||||
```cangjie
|
||||
builder.addSimApi { options =>
|
||||
options.redisConfiguration = "localhost:6379" // Redis(可选)
|
||||
|
||||
// 功能开关
|
||||
options.enableSimApiAuth = false // Token 认证
|
||||
options.enableSimApiCache = true // 缓存
|
||||
options.enableSimApiException = true // 全局异常拦截
|
||||
options.enableSimApiResponseFilter = true // 响应统一封装
|
||||
options.enableSimApiHttpClient = false // HTTP 客户端
|
||||
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")
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 内置控制器(MVC 写法)
|
||||
|
||||
simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),宿主通过 `addControllers` + 手动 `AssemblyPart` 注册(当前 cjc 无法自动扫描包子包):
|
||||
|
||||
| 控制器 | 路由 | 说明 |
|
||||
|--------|------|------|
|
||||
| `SimApiCommonController` | `/exception/{code}`、`/webconfig`、`/user/info` | 通用内置路由 |
|
||||
| `SimApiAuthController` | `/auth/logout` | 退出登录 |
|
||||
| `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken` / `requireLogin()` |
|
||||
|
||||
```cangjie
|
||||
import simapi.controllers.*
|
||||
|
||||
// 控制器写法:继承 SimApiBaseController,注解路由 + DI 注入
|
||||
public class MyController <: SimApiBaseController {
|
||||
private let _auth: SimApiAuth
|
||||
public init(auth: SimApiAuth) { this._auth = auth }
|
||||
|
||||
@HttpPost["my/route"]
|
||||
public func myAction(@FromBody request: MyRequest): ApiResult {
|
||||
requireLogin()
|
||||
ApiResult.ok()
|
||||
}
|
||||
}
|
||||
|
||||
// 宿主注册
|
||||
let mvc = builder.services.addControllers()
|
||||
mvc.addApplicationPart(AssemblyPart("simapi.controllers", [
|
||||
TypeInfo.of<SimApiCommonController>(),
|
||||
TypeInfo.of<SimApiAuthController>(),
|
||||
]))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 未实现模块(选项占位)
|
||||
|
||||
以下 C# 原包功能因仓颉生态暂无对应库,**选项保留但未实现**:
|
||||
|
||||
| 选项 | 原功能 | 状态 |
|
||||
|------|--------|------|
|
||||
| `enableSimApiDoc` | Swagger 文档 | ❌ 未实现 |
|
||||
| `enableSimApiStorage` | S3/MinIO 存储 | ❌ 未实现 |
|
||||
| `enableSynapse` | MQTT 通信 | ❌ 未实现 |
|
||||
| `enableJob` | Hangfire 任务调度 | ❌ 未实现 |
|
||||
| `enableSimApiAuthGate` | Auth Center 网关鉴权 | ❌ 未实现 |
|
||||
| `SimApiAesUtil` | AES-256-CBC | ⚠️ 仓颉 std 无 AES,暂用 Base64 占位 |
|
||||
|
||||
---
|
||||
|
||||
## 依赖
|
||||
|
||||
| 依赖 | 用途 |
|
||||
|------|------|
|
||||
| `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
|
||||
@@ -0,0 +1,20 @@
|
||||
version = 0
|
||||
|
||||
[requires]
|
||||
soulsoft_extensions_hosting = {version = "1.0.20260528"}
|
||||
soulsoft_web_http = {version = "1.0.20260528"}
|
||||
soulsoft_web_routing = {version = "1.0.20260528"}
|
||||
soulsoft_web_cors = {version = "1.0.20260528"}
|
||||
soulsoft_web_hosting = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_logging = {version = "1.0.20260528"}
|
||||
soulsoft_web_mvc = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_logging_console = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_options = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_logging_configuration = {version = "1.0.20260528"}
|
||||
soulsoft_serialization = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_configuration = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_options_configuration = {version = "1.0.20260528"}
|
||||
soulsoft_extensions_injection = {version = "1.0.20260528"}
|
||||
soulsoft_net_http = {version = "1.0.20260528"}
|
||||
redis = {version = "1.0.20260627"}
|
||||
soulsoft_identity_claims = {version = "1.0.20260528"}
|
||||
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
cjc-version = "1.1.3"
|
||||
name = "simapi"
|
||||
description = "SimApi 仓颉版:ASP.NET Core 风格 API 基础框架(统一响应/异常拦截/Token认证/缓存/工具集/HTTP客户端)"
|
||||
version = "5.2.12"
|
||||
target-dir = ""
|
||||
output-type = "static"
|
||||
override-compile-option = ""
|
||||
link-option = ""
|
||||
package-configuration = {}
|
||||
|
||||
[dependencies]
|
||||
soulsoft_web_http = "1.0.20260528"
|
||||
soulsoft_web_routing = "1.0.20260528"
|
||||
soulsoft_web_hosting = "1.0.20260528"
|
||||
soulsoft_web_mvc = "1.0.20260528"
|
||||
soulsoft_web_cors = "1.0.20260528"
|
||||
soulsoft_extensions_logging = "1.0.20260528"
|
||||
soulsoft_extensions_logging_console = "1.0.20260528"
|
||||
soulsoft_extensions_logging_configuration = "1.0.20260528"
|
||||
soulsoft_extensions_configuration = "1.0.20260528"
|
||||
soulsoft_extensions_injection = "1.0.20260528"
|
||||
soulsoft_extensions_options = "1.0.20260528"
|
||||
soulsoft_serialization = "1.0.20260528"
|
||||
soulsoft_net_http = "1.0.20260528"
|
||||
redis = "1.0.20260627"
|
||||
|
||||
[target]
|
||||
[target.x86_64-w64-mingw32]
|
||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -lcrypt32"
|
||||
[target.x86_64-w64-mingw32.bin-dependencies]
|
||||
path-option = [ "${CANGJIE_STDX_PATH}" ]
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -ldl"
|
||||
[target.x86_64-unknown-linux-gnu.bin-dependencies]
|
||||
path-option = [ "${CANGJIE_STDX_PATH}" ]
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -ldl"
|
||||
[target.aarch64-unknown-linux-gnu.bin-dependencies]
|
||||
path-option = [ "${CANGJIE_STDX_PATH}" ]
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.communications
|
||||
|
||||
/**
|
||||
* 基础请求 DTO。
|
||||
*/
|
||||
public class SimApiBaseRequest {}
|
||||
|
||||
/**
|
||||
* 仅包含 Id 的请求。
|
||||
*/
|
||||
public class SimApiStringIdOnlyRequest {
|
||||
public var id: String = ""
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(id: String) {
|
||||
this.id = id
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单字段请求。
|
||||
* @param T 数据类型。
|
||||
*/
|
||||
public class SimApiOneFieldRequest<T> {
|
||||
public var data: ?T = None
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(data: T) {
|
||||
this.data = Some(data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础分页请求。
|
||||
*/
|
||||
public class SimApiBasePageRequest {
|
||||
public var page: Int64 = 1
|
||||
public var count: Int64 = 20
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(page: Int64, count: Int64) {
|
||||
this.page = page
|
||||
this.count = count
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.communications
|
||||
|
||||
import std.collection.*
|
||||
import stdx.encoding.json.*
|
||||
import soulsoft_serialization.*
|
||||
import soulsoft_serialization.macros.*
|
||||
|
||||
/**
|
||||
* 基础响应体:所有接口统一返回该结构。
|
||||
* HTTP 状态码始终 200,业务错误通过 code 字段表达。
|
||||
*/
|
||||
@Serialization
|
||||
public open class SimApiBaseResponse {
|
||||
public var _code: Int64 = 200
|
||||
public var _message: String = "成功"
|
||||
|
||||
public init(code: Int64, message: String) {
|
||||
this._code = code
|
||||
this._message = message
|
||||
}
|
||||
|
||||
public init(code: Int64) {
|
||||
this._code = code
|
||||
this._message = getDefaultMessage(code)
|
||||
}
|
||||
|
||||
public init() {
|
||||
this._code = 200
|
||||
this._message = "成功"
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认错误代码对应提示信息。
|
||||
*/
|
||||
public static func getDefaultMessage(code: Int64): String {
|
||||
match (code) {
|
||||
case 200 => "成功"
|
||||
case 204 => "没有数据"
|
||||
case 400 => "参数错误"
|
||||
case 401 => "需要登录"
|
||||
case 403 => "无权访问"
|
||||
case 404 => "请求资源不存在"
|
||||
case 500 => "服务器错误"
|
||||
case _ => "未知错误代码"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为 JSON 字符串(可选 data 字段)。
|
||||
* @param dataJson 已序列化的 data JSON 字符串(可选)。
|
||||
*/
|
||||
public func toJsonString(dataJson!: String = ""): String {
|
||||
var sb = StringBuilder()
|
||||
sb.append("{\"code\":${_code},\"message\":\"${SimApiJson.escapeJson(_message)}\"")
|
||||
if (!dataJson.isEmpty()) {
|
||||
sb.append(",\"data\":${dataJson}")
|
||||
}
|
||||
sb.append("}")
|
||||
sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页内容返回。
|
||||
* @param T 列表元素类型。
|
||||
* 说明:泛型类暂不通过 @Serialization 宏序列化,可手动转换为 SimApiResponse。
|
||||
*/
|
||||
public class PageResponse<T> {
|
||||
public var _list: Array<T> = Array<T>()
|
||||
public var _page: Int64 = 1
|
||||
public var _count: Int64 = 20
|
||||
public var _total: Int64 = 0
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(list: Array<T>, page: Int64, count: Int64, total: Int64) {
|
||||
this._list = list
|
||||
this._page = page
|
||||
this._count = count
|
||||
this._total = total
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带数据的动态响应。
|
||||
* @param T 数据类型。
|
||||
* 说明:泛型类手写实现 ISerialization<SimApiResponse<T>>(serialize + deserialize),
|
||||
* data 内嵌为对象(对齐 C# SimApiBaseResponse<T>.Data 是 T? 而非字符串)。
|
||||
* 序列化支持动态结构(HashMap<String,Any> 等经 SimApiJson 内嵌);反序列化要求 T <: ISerialization<T>。
|
||||
*/
|
||||
public class SimApiResponse<T> <: SimApiBaseResponse & ISerialization<SimApiResponse<T>> where T <: ISerialization<T> {
|
||||
public var _data: ?T = None
|
||||
|
||||
public init() {
|
||||
super()
|
||||
}
|
||||
|
||||
public init(data: T) {
|
||||
super()
|
||||
this._data = Some(data)
|
||||
}
|
||||
|
||||
public init(code: Int64, message: String) {
|
||||
super(code, message)
|
||||
}
|
||||
|
||||
public init(code: Int64, message: String, data: T) {
|
||||
super(code, message)
|
||||
this._data = Some(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为 DataModel:code + message + data(data 为对象)。
|
||||
*/
|
||||
public override func serializeObject(options: JsonSerializerOptions): DataModel {
|
||||
let dms = DataModelStruct()
|
||||
dms.add(Field("code", DataModelInt(_code)))
|
||||
dms.add(Field("message", DataModelString(_message)))
|
||||
if (let Some(data) <- _data) {
|
||||
if (let ser: ISerializable <- data) {
|
||||
dms.add(Field("data", ser.serializeObject(options)))
|
||||
} else {
|
||||
// 动态结构(HashMap<String, Any> 等):经 SimApiJson 序列化后解析内嵌为对象
|
||||
let json = SimApiJson.json(Some(data))
|
||||
dms.add(Field("data", DataModel.fromJson(JsonValue.fromStr(json))))
|
||||
}
|
||||
}
|
||||
dms
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 DataModel 反序列化:code + message + data(对齐 .NET JsonSerializer.Deserialize<T>)。
|
||||
*/
|
||||
public static func deserializeObject(dm: DataModel, options: JsonSerializerOptions): SimApiResponse<T> {
|
||||
let resp = SimApiResponse<T>()
|
||||
if (let dms: DataModelStruct <- dm) {
|
||||
for (field in dms.getFields()) {
|
||||
match (field.getName()) {
|
||||
case "code" =>
|
||||
if (let v: DataModelInt <- field.getData()) {
|
||||
resp._code = v.getValue()
|
||||
}
|
||||
case "message" =>
|
||||
if (let v: DataModelString <- field.getData()) {
|
||||
resp._message = v.getValue()
|
||||
}
|
||||
case "data" =>
|
||||
resp._data = Some(T.deserializeObject(field.getData(), options))
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
resp
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.communications
|
||||
|
||||
import std.collection.*
|
||||
import stdx.encoding.json.*
|
||||
import soulsoft_serialization.*
|
||||
|
||||
/**
|
||||
* 带任意对象数据的响应(非泛型版,供响应自动封装使用)。
|
||||
*
|
||||
* 与 SimApiResponse<T> 的区别:data 为 Any(运行时类型不定),用于
|
||||
* SimApiRequestDelegateFactory 派发结果时统一包装 DTO/数组/动态结构。
|
||||
*
|
||||
* 序列化规则(对齐 SimApiResponse<T>):
|
||||
* - data 实现了 ISerializable(@Serialization DTO、Array<T> 等)→ data 内嵌为对象
|
||||
* - data 为动态结构(如 HashMap<String, Any>,不满足泛型 ISerialization 约束)
|
||||
* → 经 SimApiJson 序列化后解析内嵌为对象(而非字符串)
|
||||
* 因此 data 在 JSON 中始终是对象/数组/标量,不会是"JSON 字符串"。
|
||||
*/
|
||||
public class SimApiDataResponse <: SimApiBaseResponse & ISerialization<SimApiDataResponse> {
|
||||
public var _data: ?Any = None
|
||||
|
||||
public init() {
|
||||
super()
|
||||
}
|
||||
|
||||
public init(data: Any) {
|
||||
super()
|
||||
this._data = Some(data)
|
||||
}
|
||||
|
||||
public init(code: Int64, message: String) {
|
||||
super(code, message)
|
||||
}
|
||||
|
||||
public init(code: Int64, message: String, data: Any) {
|
||||
super(code, message)
|
||||
this._data = Some(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为 DataModel:code + message + data(data 为对象)。
|
||||
*/
|
||||
public override func serializeObject(options: JsonSerializerOptions): DataModel {
|
||||
let dms = DataModelStruct()
|
||||
dms.add(Field("code", DataModelInt(_code)))
|
||||
dms.add(Field("message", DataModelString(_message)))
|
||||
if (let Some(data) <- _data) {
|
||||
if (let ser: ISerializable <- data) {
|
||||
dms.add(Field("data", ser.serializeObject(options)))
|
||||
} else {
|
||||
// 动态结构(HashMap<String, Any> 等):经 SimApiJson 序列化后解析内嵌为对象
|
||||
let json = SimApiJson.json(Some(data))
|
||||
dms.add(Field("data", DataModel.fromJson(JsonValue.fromStr(json))))
|
||||
}
|
||||
}
|
||||
dms
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 DataModel 反序列化:code + message + data(data 保留原始 DataModel)。
|
||||
*/
|
||||
public static func deserializeObject(dm: DataModel, options: JsonSerializerOptions): SimApiDataResponse {
|
||||
let resp = SimApiDataResponse()
|
||||
if (let dms: DataModelStruct <- dm) {
|
||||
for (field in dms.getFields()) {
|
||||
match (field.getName()) {
|
||||
case "code" =>
|
||||
if (let v: DataModelInt <- field.getData()) {
|
||||
resp._code = v.getValue()
|
||||
}
|
||||
case "message" =>
|
||||
if (let v: DataModelString <- field.getData()) {
|
||||
resp._message = v.getValue()
|
||||
}
|
||||
case "data" =>
|
||||
resp._data = Some(field.getData())
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
resp
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* JSON 序列化统一工具(对应 C# SimApiUtil.Json)。
|
||||
*/
|
||||
|
||||
package simapi.communications
|
||||
|
||||
import std.collection.*
|
||||
|
||||
/**
|
||||
* JSON 序列化静态工具类。
|
||||
*
|
||||
* 说明:序列化核心放在依赖图最底层的 simapi.communications 包,
|
||||
* SimApiUtil.json(simapi.helpers)委托本类实现,避免循环依赖;
|
||||
* 全框架 JSON 输出统一走此处,保证转义与格式一致。
|
||||
*/
|
||||
public class SimApiJson {
|
||||
private init() {}
|
||||
|
||||
/**
|
||||
* 对象序列化为 JSON 字符串(统一入口)。
|
||||
* 支持 String/Int64/Bool/Float64/Array/HashMap,其他类型退化为字符串。
|
||||
* @param obj 任意对象(None 输出 null)。
|
||||
*/
|
||||
public static func json(obj: ?Any): String {
|
||||
if (let Some(obj) <- obj) {
|
||||
return jsonValue(obj)
|
||||
}
|
||||
"null"
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 字符串转义(统一入口)。
|
||||
* @param s 原始字符串。
|
||||
* @return 转义后可直接放入 JSON 字符串字面量的内容。
|
||||
*/
|
||||
public static func escapeJson(s: String): String {
|
||||
var sb = StringBuilder()
|
||||
for (c in s.runes()) {
|
||||
match (c) {
|
||||
case '"' => sb.append("\\\"")
|
||||
case '\\' => sb.append("\\\\")
|
||||
case '\n' => sb.append("\\n")
|
||||
case '\r' => sb.append("\\r")
|
||||
case '\t' => sb.append("\\t")
|
||||
case _ => sb.append(c)
|
||||
}
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private static func jsonValue(obj: Any): String {
|
||||
if (let s: String <- obj) {
|
||||
return "\"${escapeJson(s)}\""
|
||||
}
|
||||
if (let i: Int64 <- obj) {
|
||||
return "${i}"
|
||||
}
|
||||
if (let b: Bool <- obj) {
|
||||
return "${b}"
|
||||
}
|
||||
if (let f: Float64 <- obj) {
|
||||
return "${f}"
|
||||
}
|
||||
if (let arr: Array<Any> <- obj) {
|
||||
var sb = StringBuilder()
|
||||
sb.append("[")
|
||||
var first = true
|
||||
for (item in arr) {
|
||||
if (!first) { sb.append(",") }
|
||||
sb.append(jsonValue(item))
|
||||
first = false
|
||||
}
|
||||
sb.append("]")
|
||||
return sb.toString()
|
||||
}
|
||||
if (let map: HashMap<String, Any> <- obj) {
|
||||
var sb = StringBuilder()
|
||||
sb.append("{")
|
||||
var first = true
|
||||
for ((key, value) in map) {
|
||||
if (!first) { sb.append(",") }
|
||||
sb.append("\"${escapeJson(key)}\":${jsonValue(value)}")
|
||||
first = false
|
||||
}
|
||||
sb.append("}")
|
||||
return sb.toString()
|
||||
}
|
||||
// 其他类型退化为字符串
|
||||
return "\"${escapeJson(describe(obj))}\""
|
||||
}
|
||||
|
||||
private static func describe(obj: Any): String {
|
||||
if (let s: ToString <- obj) {
|
||||
return s.toString()
|
||||
}
|
||||
"null"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* Communications/SimApiLoginItem:登录信息项。
|
||||
*/
|
||||
|
||||
package simapi.communications
|
||||
|
||||
import std.collection.*
|
||||
import soulsoft_serialization.*
|
||||
|
||||
/**
|
||||
* 登录信息项:Token 认证通过后注入请求上下文。
|
||||
* 对齐 C# SimApiLoginItem(Id / Type / Meta / Extra,camelCase 输出 id/type/meta/extra)。
|
||||
*
|
||||
* 说明:因 _extra 为 HashMap<String, Any>(Any 不满足 soulsoft 的 ISerialization<V> 约束,
|
||||
* 无法用 @Serialization 宏),故手动实现 ISerialization<SimApiLoginItem>(serialize + deserialize);
|
||||
* 调用方统一通过 JsonSerializer.serializeObject<T>() / deserializeObject<T>() 使用
|
||||
* (对齐 .NET JsonSerializer.Serialize / Deserialize)。
|
||||
*/
|
||||
public class SimApiLoginItem <: ISerialization<SimApiLoginItem> {
|
||||
public var _id: String = ""
|
||||
public var _types: Array<String> = []
|
||||
public var _meta: HashMap<String, String> = HashMap<String, String>()
|
||||
public var _extra: HashMap<String, Any> = HashMap<String, Any>()
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(id: String) {
|
||||
this._id = id
|
||||
this._types = ["user"]
|
||||
this._meta = HashMap<String, String>()
|
||||
this._extra = HashMap<String, Any>()
|
||||
}
|
||||
|
||||
public init(id: String, types: Array<String>) {
|
||||
this._id = id
|
||||
this._types = types
|
||||
this._meta = HashMap<String, String>()
|
||||
this._extra = HashMap<String, Any>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为 DataModel:{"id","type","meta","extra"}。
|
||||
*/
|
||||
public func serializeObject(options: JsonSerializerOptions): DataModel {
|
||||
let dms = DataModelStruct()
|
||||
dms.add(Field("id", DataModelString(_id)))
|
||||
// type: string[]
|
||||
let typesSeq = DataModelSeq()
|
||||
for (t in _types) {
|
||||
typesSeq.add(DataModelString(t))
|
||||
}
|
||||
dms.add(Field("type", typesSeq))
|
||||
// meta: object<string,string>
|
||||
let metaStruct = DataModelStruct()
|
||||
for ((k, v) in _meta) {
|
||||
metaStruct.add(Field(k, DataModelString(v)))
|
||||
}
|
||||
dms.add(Field("meta", metaStruct))
|
||||
// extra: object<string,any>
|
||||
let extraStruct = DataModelStruct()
|
||||
for ((k, v) in _extra) {
|
||||
extraStruct.add(Field(k, anyToDataModel(v)))
|
||||
}
|
||||
dms.add(Field("extra", extraStruct))
|
||||
dms
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 DataModel 反序列化:{"id","type","meta","extra"}。
|
||||
*/
|
||||
public static func deserializeObject(dm: DataModel, options: JsonSerializerOptions): SimApiLoginItem {
|
||||
let item = SimApiLoginItem()
|
||||
if (let dms: DataModelStruct <- dm) {
|
||||
for (field in dms.getFields()) {
|
||||
match (field.getName()) {
|
||||
case "id" => item._id = field.getData() |> dataModelToString
|
||||
case "type" =>
|
||||
let list = ArrayList<String>()
|
||||
if (let seq: DataModelSeq <- field.getData()) {
|
||||
for (sub in seq.getItems()) {
|
||||
list.add(dataModelToString(sub))
|
||||
}
|
||||
}
|
||||
item._types = list.toArray()
|
||||
case "meta" =>
|
||||
if (let metaStruct: DataModelStruct <- field.getData()) {
|
||||
for (sub in metaStruct.getFields()) {
|
||||
item._meta[sub.getName()] = dataModelToString(sub.getData())
|
||||
}
|
||||
}
|
||||
case "extra" =>
|
||||
if (let extraStruct: DataModelStruct <- field.getData()) {
|
||||
for (sub in extraStruct.getFields()) {
|
||||
item._extra[sub.getName()] = dataModelToAny(sub.getData())
|
||||
}
|
||||
}
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
item
|
||||
}
|
||||
|
||||
private static func dataModelToString(dm: DataModel): String {
|
||||
if (let s: DataModelString <- dm) {
|
||||
return s.getValue()
|
||||
}
|
||||
if (let i: DataModelInt <- dm) {
|
||||
return "${i.getValue()}"
|
||||
}
|
||||
if (let b: DataModelBool <- dm) {
|
||||
return "${b.getValue()}"
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
private static func dataModelToAny(dm: DataModel): Any {
|
||||
if (let s: DataModelString <- dm) {
|
||||
return s.getValue()
|
||||
}
|
||||
if (let i: DataModelInt <- dm) {
|
||||
return i.getValue()
|
||||
}
|
||||
if (let b: DataModelBool <- dm) {
|
||||
return b.getValue()
|
||||
}
|
||||
if (let f: DataModelFloat <- dm) {
|
||||
return f.getValue()
|
||||
}
|
||||
if (let dms: DataModelStruct <- dm) {
|
||||
var map = HashMap<String, Any>()
|
||||
for (field in dms.getFields()) {
|
||||
map[field.getName()] = dataModelToAny(field.getData())
|
||||
}
|
||||
return map
|
||||
}
|
||||
if (let seq: DataModelSeq <- dm) {
|
||||
let list = ArrayList<Any>()
|
||||
for (item in seq.getItems()) {
|
||||
list.add(dataModelToAny(item))
|
||||
}
|
||||
return list.toArray()
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
private static func anyToDataModel(v: Any): DataModel {
|
||||
if (let s: String <- v) {
|
||||
return DataModelString(s)
|
||||
}
|
||||
if (let i: Int64 <- v) {
|
||||
return DataModelInt(i)
|
||||
}
|
||||
if (let b: Bool <- v) {
|
||||
return DataModelBool(b)
|
||||
}
|
||||
if (let f: Float64 <- v) {
|
||||
return DataModelFloat(f)
|
||||
}
|
||||
if (let s: ToString <- v) {
|
||||
return DataModelString(s.toString())
|
||||
}
|
||||
DataModelNull()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 对齐 C# 的 Configurations/SimApiAuthCenterOptions.cs。
|
||||
*/
|
||||
|
||||
package simapi.configurations
|
||||
|
||||
/**
|
||||
* 认证中心配置(对齐 C# SimApiAuthCenterOptions)。
|
||||
*/
|
||||
public class SimApiAuthCenterOptions {
|
||||
/**
|
||||
* 认证中心地址。
|
||||
*/
|
||||
public var server: String = ""
|
||||
|
||||
/**
|
||||
* 应用 Id。
|
||||
*/
|
||||
public var appId: String = ""
|
||||
|
||||
/**
|
||||
* 应用密钥。
|
||||
*/
|
||||
public var appKey: String = ""
|
||||
|
||||
/**
|
||||
* 开启则使用内部网关透传的 Middleware。
|
||||
* 注意:只有内部应用需要开启这个,也就是 api 通过内部网关代理后。
|
||||
*/
|
||||
public var useMiddleware: Bool = false
|
||||
|
||||
/**
|
||||
* 是否使用 IAM 认证。
|
||||
*/
|
||||
public var useIam: Bool = false
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 对齐 C# 的 Configurations/SimApiDocOptions.cs。
|
||||
*/
|
||||
|
||||
package simapi.configurations
|
||||
|
||||
import std.collection.*
|
||||
|
||||
/**
|
||||
* 文档组配置(对齐 C# SimApiDocGroupOption)。
|
||||
*/
|
||||
public class SimApiDocGroup {
|
||||
public var id: String = ""
|
||||
public var name: String = ""
|
||||
public var description: String = ""
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(id: String, name: String, description!: String = "") {
|
||||
this.id = id
|
||||
this.name = name
|
||||
this.description = description
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档授权配置(对齐 C# SimApiAuthOption)。
|
||||
* Type 支持 "SimApiAuth"、"ClientCredentials"、"Implicit"、"AuthorizationCode"、"Password"。
|
||||
*/
|
||||
public class SimApiAuthOption {
|
||||
/**
|
||||
* 认证方式(默认 ["SimApiAuth"])。
|
||||
* `type` 是仓颉关键字,用反引号转义以对齐 C# 属性名 Type。
|
||||
*/
|
||||
public var `type`: Array<String> = ["SimApiAuth"]
|
||||
|
||||
/**
|
||||
* 认证描述(默认 "认证服务器颁发的AccessToken")。
|
||||
*/
|
||||
public var description: String = "认证服务器颁发的AccessToken"
|
||||
|
||||
/**
|
||||
* 授权地址。
|
||||
*/
|
||||
public var authorizationUrl: String = ""
|
||||
|
||||
/**
|
||||
* Token 地址。
|
||||
*/
|
||||
public var tokenUrl: String = ""
|
||||
|
||||
/**
|
||||
* 授权范围。
|
||||
*/
|
||||
public var scopes: HashMap<String, String> = HashMap<String, String>()
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档相关配置(默认值与 C# 一致)。
|
||||
*/
|
||||
public class SimApiDocOptions {
|
||||
/**
|
||||
* 文档组配置(默认 [new("api", "Api", "Api接口文档")])。
|
||||
*/
|
||||
public var apiGroups: ArrayList<SimApiDocGroup> = ArrayList<SimApiDocGroup>()
|
||||
|
||||
/**
|
||||
* 授权配置(默认 SimApiAuthOption())。
|
||||
*/
|
||||
public var apiAuth = SimApiAuthOption()
|
||||
|
||||
/**
|
||||
* 文档页面标题(默认 "API接口文档")。
|
||||
*/
|
||||
public var documentTitle: String = "API接口文档"
|
||||
|
||||
/**
|
||||
* 接口支持的调用方式(默认仅 POST)。
|
||||
*/
|
||||
public var supportedMethods: Array<String> = ["POST"]
|
||||
|
||||
public init() {
|
||||
apiGroups.add(SimApiDocGroup("api", "Api", description: "Api接口文档"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 对齐 C# 的 Configurations/SimApiExceptionOptions.cs。
|
||||
*/
|
||||
|
||||
package simapi.configurations
|
||||
|
||||
import std.collection.*
|
||||
|
||||
/**
|
||||
* 异常处理相关配置(默认值与 C# 一致)。
|
||||
*/
|
||||
public class SimApiExceptionOptions {
|
||||
/**
|
||||
* 跳过不做业务错误处理的状态码(默认 [200, 301, 302])。
|
||||
*/
|
||||
public var skipStatusCodes: HashSet<Int64> = HashSet<Int64>([200, 301, 302])
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 对齐 C# 的 Configurations/SimApiHttpClientOptions.cs。
|
||||
*/
|
||||
|
||||
package simapi.configurations
|
||||
|
||||
/**
|
||||
* HTTP 客户端配置。
|
||||
*/
|
||||
public class SimApiHttpClientOptions {
|
||||
public var server: String = ""
|
||||
public var appId: String = ""
|
||||
public var appKey: String = ""
|
||||
public var signName: String = "sign"
|
||||
public var timestampName: String = "timestamp"
|
||||
public var nonceName: String = "nonce"
|
||||
public var appIdName: ?String = Some("appId")
|
||||
public var signFields: Array<String> = []
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 对齐 C# 的 Configurations/SimApiJobOptions.cs。
|
||||
*/
|
||||
|
||||
package simapi.configurations
|
||||
|
||||
import std.collection.*
|
||||
|
||||
/**
|
||||
* 任务调度服务器配置(对齐 C# SimApiJobServerConfig)。
|
||||
*/
|
||||
public class SimApiJobServer {
|
||||
/**
|
||||
* 队列(默认 ["default"])。
|
||||
*/
|
||||
public var queues: Array<String> = ["default"]
|
||||
|
||||
/**
|
||||
* 工作线程数(默认 5)。
|
||||
*/
|
||||
public var workerNum: Int64 = 5
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务调度相关配置(默认值与 C# 一致)。
|
||||
*/
|
||||
public class SimApiJobOptions {
|
||||
/**
|
||||
* WebUi 地址,设置为 null 表示不启用(默认 /jobs)。
|
||||
*/
|
||||
public var dashboardUrl: ?String = Some("/jobs")
|
||||
|
||||
/**
|
||||
* WebUi 用户(默认 admin)。
|
||||
*/
|
||||
public var dashboardAuthUser: String = "admin"
|
||||
|
||||
/**
|
||||
* WebUi 密码(默认 Admin@123!)。
|
||||
*/
|
||||
public var dashboardAuthPass: String = "Admin@123!"
|
||||
|
||||
/**
|
||||
* 设置为 null 使用默认 redis 配置。
|
||||
*/
|
||||
public var redisConfiguration: ?String = None
|
||||
|
||||
/**
|
||||
* 设置为 null 使用默认 redis 配置。
|
||||
*/
|
||||
public var database: ?Int64 = None
|
||||
|
||||
/**
|
||||
* 任务服务器配置(默认 [new()])。
|
||||
*/
|
||||
public var servers: ArrayList<SimApiJobServer> = ArrayList<SimApiJobServer>()
|
||||
|
||||
public init() {
|
||||
servers.add(SimApiJobServer())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 对齐 C# 的 Configurations/SimApiOptions.cs。
|
||||
*/
|
||||
|
||||
package simapi.configurations
|
||||
|
||||
import std.collection.*
|
||||
|
||||
/**
|
||||
* SimApi 全局配置:对应 C# 的 SimApi.Configurations.SimApiOptions。
|
||||
*/
|
||||
public class SimApiOptions {
|
||||
/**
|
||||
* Redis 配置;配置则使用 Redis,不配则自动使用 InMemory。
|
||||
*/
|
||||
public var redisConfiguration: String = ""
|
||||
|
||||
/**
|
||||
* 原样返回给前端的配置信息。
|
||||
*/
|
||||
public var webConfig: HashMap<String, Any> = HashMap<String, Any>()
|
||||
|
||||
/**
|
||||
* WebConfig 返回是否包含版本信息。
|
||||
*/
|
||||
public var webConfigIncludeVersion: Bool = true
|
||||
|
||||
/**
|
||||
* 是否启用后台任务系统(占位,未实现)。
|
||||
*/
|
||||
public var enableJob: Bool = false
|
||||
|
||||
/**
|
||||
* 启用 Token 认证。
|
||||
*/
|
||||
public var enableSimApiAuth: Bool = false
|
||||
|
||||
/**
|
||||
* 启用缓存功能(默认 true)。
|
||||
*/
|
||||
public var enableSimApiCache: Bool = true
|
||||
|
||||
/**
|
||||
* 启用网关授权(占位,未实现)。
|
||||
*/
|
||||
public var enableSimApiAuthGate: Bool = false
|
||||
|
||||
/**
|
||||
* 开启 S3 兼容存储(占位,未实现)。
|
||||
*/
|
||||
public var enableSimApiStorage: Bool = false
|
||||
|
||||
/**
|
||||
* 启用在线文档(占位,未实现)。
|
||||
*/
|
||||
public var enableSimApiDoc: Bool = false
|
||||
|
||||
/**
|
||||
* 是否启用 Synapse(占位,未实现)。
|
||||
*/
|
||||
public var enableSynapse: Bool = false
|
||||
|
||||
/**
|
||||
* 启用全部 CORS(默认 true)。
|
||||
*/
|
||||
public var enableCors: Bool = true
|
||||
|
||||
/**
|
||||
* 启用异常拦截(默认 true)。
|
||||
*/
|
||||
public var enableSimApiException: Bool = true
|
||||
|
||||
/**
|
||||
* 启用返回结果拦截(默认 true)。
|
||||
*/
|
||||
public var enableSimApiResponseFilter: Bool = true
|
||||
|
||||
/**
|
||||
* 启用请求日志中间件。
|
||||
*/
|
||||
public var enableRequestLog: Bool = false
|
||||
|
||||
/**
|
||||
* 开启 ForwardHeaders(默认 true)。
|
||||
*/
|
||||
public var enableForwardHeaders: Bool = true
|
||||
|
||||
/**
|
||||
* 将 url 格式化为小写(默认 true)。
|
||||
*/
|
||||
public var enableLowerUrl: Bool = true
|
||||
|
||||
/**
|
||||
* 启用格式化 Console Logger(默认 true)。
|
||||
*/
|
||||
public var enableLogger: Bool = true
|
||||
|
||||
/**
|
||||
* 是否启用 SimApiHttpClient。
|
||||
*/
|
||||
public var enableSimApiHttpClient: Bool = false
|
||||
|
||||
public var simApiJobOptions = SimApiJobOptions()
|
||||
public var simApiDocOptions = SimApiDocOptions()
|
||||
public var simApiStorageOptions = SimApiStorageOptions()
|
||||
public var simApiSynapseOptions = SimApiSynapseOptions()
|
||||
public var simApiAuthCenterOptions = SimApiAuthCenterOptions()
|
||||
public var simApiHttpClientOptions = SimApiHttpClientOptions()
|
||||
public var simApiExceptionOptions = SimApiExceptionOptions()
|
||||
public var simApiRouteOptions = SimApiRouteOptions()
|
||||
public var simApiRequestLogOptions = SimApiRequestLogOptions()
|
||||
|
||||
public init() {}
|
||||
|
||||
// ===== .NET 风格配置回调(对齐 C# ConfigureSimApiXxx(opt => ...)) =====
|
||||
|
||||
/**
|
||||
* 配置路由选项。
|
||||
* @param configure 路由配置回调。
|
||||
*/
|
||||
public func configureSimApiRoute(configure: (SimApiRouteOptions) -> Unit): Unit {
|
||||
configure(simApiRouteOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置异常处理选项。
|
||||
* @param configure 异常配置回调。
|
||||
*/
|
||||
public func configureSimApiException(configure: (SimApiExceptionOptions) -> Unit): Unit {
|
||||
configure(simApiExceptionOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 HTTP 客户端选项。
|
||||
* @param configure 客户端配置回调。
|
||||
*/
|
||||
public func configureSimApiHttpClient(configure: (SimApiHttpClientOptions) -> Unit): Unit {
|
||||
configure(simApiHttpClientOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置文档选项(占位)。
|
||||
* @param configure 文档配置回调。
|
||||
*/
|
||||
public func configureSimApiDoc(configure: (SimApiDocOptions) -> Unit): Unit {
|
||||
configure(simApiDocOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置存储选项(占位)。
|
||||
* @param configure 存储配置回调。
|
||||
*/
|
||||
public func configureSimApiStorage(configure: (SimApiStorageOptions) -> Unit): Unit {
|
||||
configure(simApiStorageOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置任务调度选项(占位)。
|
||||
* @param configure 任务配置回调。
|
||||
*/
|
||||
public func configureSimApiJob(configure: (SimApiJobOptions) -> Unit): Unit {
|
||||
configure(simApiJobOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 Synapse 选项(占位)。
|
||||
* @param configure Synapse 配置回调。
|
||||
*/
|
||||
public func configureSimApiSynapse(configure: (SimApiSynapseOptions) -> Unit): Unit {
|
||||
configure(simApiSynapseOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置请求日志选项(对齐 C# ConfigureSimApiRequestLog)。
|
||||
* @param configure 请求日志配置回调。
|
||||
*/
|
||||
public func configureSimApiRequestLog(configure: (SimApiRequestLogOptions) -> Unit): Unit {
|
||||
configure(simApiRequestLogOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置认证中心选项(对齐 C# ConfigureSimApiAuthCenter)。
|
||||
* @param configure 认证中心配置回调。
|
||||
*/
|
||||
public func configureSimApiAuthCenter(configure: (SimApiAuthCenterOptions) -> Unit): Unit {
|
||||
configure(simApiAuthCenterOptions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 对齐 C# 的 Configurations/SimApiRequestLogOptions.cs。
|
||||
*/
|
||||
|
||||
package simapi.configurations
|
||||
|
||||
/**
|
||||
* 请求日志配置。
|
||||
*/
|
||||
public class SimApiRequestLogOptions {
|
||||
/**
|
||||
* 是否打印完整的请求 Header。
|
||||
*/
|
||||
public var showFullHeader: Bool = false
|
||||
|
||||
/**
|
||||
* 是否打印完整的响应体。
|
||||
*/
|
||||
public var showFullResponse: Bool = false
|
||||
|
||||
/**
|
||||
* 请求字段显示最长长度(0 表示不截断)。
|
||||
*/
|
||||
public var requestStringLogLength: Int64 = 0
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 对齐 C# 的 Configurations/SimApiRouteOptions.cs。
|
||||
*/
|
||||
|
||||
package simapi.configurations
|
||||
|
||||
/**
|
||||
* 路由相关配置(默认值与 C# 一致)。
|
||||
*/
|
||||
public class SimApiRouteOptions {
|
||||
/**
|
||||
* 退出登录路由(默认 /auth/logout)。
|
||||
*/
|
||||
public var logoutRoute: ?String = Some("/auth/logout")
|
||||
|
||||
/**
|
||||
* 用户信息路由(默认 /user/info)。
|
||||
*/
|
||||
public var userInfoRoute: ?String = Some("/user/info")
|
||||
|
||||
/**
|
||||
* 前端配置路由(默认 /config)。
|
||||
*/
|
||||
public var webConfigRoute: ?String = Some("/config")
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 对齐 C# 的 Configurations/SimApiStorageOptions.cs。
|
||||
*/
|
||||
|
||||
package simapi.configurations
|
||||
|
||||
/**
|
||||
* 存储相关配置(占位:仓颉版暂未实现 S3/MinIO 存储)。
|
||||
*/
|
||||
public class SimApiStorageOptions {
|
||||
public var endpoint: String = ""
|
||||
public var accessKey: String = ""
|
||||
public var secretKey: String = ""
|
||||
public var bucket: String = ""
|
||||
public var serveUrl: String = ""
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 对齐 C# 的 Configurations/SimApiSynapseOptions.cs。
|
||||
*/
|
||||
|
||||
package simapi.configurations
|
||||
|
||||
/**
|
||||
* MQTT 通信配置(默认值与 C# 一致)。
|
||||
*/
|
||||
public class SimApiSynapseOptions {
|
||||
/**
|
||||
* Mqtt 服务器的 Websocket 地址。
|
||||
*/
|
||||
public var websocket: String = ""
|
||||
|
||||
public var username: String = ""
|
||||
public var password: String = ""
|
||||
public var sysName: String = ""
|
||||
public var appName: String = ""
|
||||
public var appId: String = ""
|
||||
|
||||
/**
|
||||
* RPC 超时秒数(默认 3)。
|
||||
*/
|
||||
public var rpcTimeout: Int64 = 3
|
||||
|
||||
/**
|
||||
* Event 是否使用负载均衡:订阅 $queue 主题,消息分发给不同 AppId。
|
||||
* false 时多个 AppId 都可同时收到消息(默认 false)。
|
||||
*/
|
||||
public var eventLoadBalancing: Bool = false
|
||||
|
||||
/**
|
||||
* 启用分布式配置存储(默认 true)。
|
||||
*/
|
||||
public var enableConfigStore: Bool = true
|
||||
|
||||
/**
|
||||
* 禁用事件客户端(默认 false)。
|
||||
*/
|
||||
public var disableEventClient: Bool = false
|
||||
|
||||
/**
|
||||
* 禁用 RPC 客户端(默认 false)。
|
||||
*/
|
||||
public var disableRpcClient: Bool = false
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* Controllers/SimApiAuthController:认证相关内置路由。
|
||||
*/
|
||||
|
||||
package simapi.controllers
|
||||
|
||||
import soulsoft_web_mvc.annotations.*
|
||||
import simapi.communications.*
|
||||
import simapi.helpers.*
|
||||
|
||||
/**
|
||||
* 认证控制器:退出登录。
|
||||
* 对齐 C# 的 SimApiAuthController。
|
||||
*/
|
||||
public class SimApiAuthController <: SimApiBaseController {
|
||||
private let _auth: SimApiAuth
|
||||
|
||||
public init(auth: SimApiAuth) {
|
||||
this._auth = auth
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/logout:退出登录(void 自动封装为 SimApiBaseResponse())。
|
||||
* 对齐 C# LogoutRoute 默认值 /auth/logout(SimApiAuthController.Logout,[HttpPost],
|
||||
* 由 MapControllerRoute(pattern=LogoutRoute) 注册)。
|
||||
*/
|
||||
@HttpPost["/auth/logout"]
|
||||
public func logout(): Unit {
|
||||
if (let Some(token) <- request.headers.get("Token")) {
|
||||
_auth.logout(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* Controllers/SimApiBaseController:基础控制器,所有控制器继承。
|
||||
* 提供当前登录信息访问(对齐 C# 的 LoginInfo / LoginToken)。
|
||||
*/
|
||||
|
||||
package simapi.controllers
|
||||
|
||||
import soulsoft_web_http.*
|
||||
import soulsoft_web_mvc.core.*
|
||||
import simapi.communications.*
|
||||
import simapi.helpers.*
|
||||
|
||||
/**
|
||||
* 基础控制器:所有控制器均继承本控制器。
|
||||
* 对齐 C# 的 SimApiBaseController([Consumes]/[Produces] JSON + 登录信息)。
|
||||
*/
|
||||
public open class SimApiBaseController <: Controller {
|
||||
/**
|
||||
* 当前登录信息(需 EnableSimApiAuth;未登录抛 401)。
|
||||
*/
|
||||
protected prop loginInfo: SimApiLoginItem {
|
||||
get() {
|
||||
if (let Some(item) <- context.items.get("LoginInfo")) {
|
||||
if (let login: SimApiLoginItem <- item) {
|
||||
return login
|
||||
}
|
||||
}
|
||||
SimApiError.error(code: 401, message: "需要登录")
|
||||
SimApiLoginItem("")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录 Token(需 EnableSimApiAuth;未登录抛 401)。
|
||||
*/
|
||||
protected prop loginToken: String {
|
||||
get() {
|
||||
if (let Some(item) <- context.items.get("LoginToken")) {
|
||||
if (let token: String <- item) {
|
||||
return token
|
||||
}
|
||||
}
|
||||
SimApiError.error(code: 401, message: "需要登录")
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录信息(可选)。
|
||||
*/
|
||||
protected func getLogin(): ?SimApiLoginItem {
|
||||
if (let Some(item) <- context.items.get("LoginInfo")) {
|
||||
if (let login: SimApiLoginItem <- item) {
|
||||
return Some(login)
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态,未登录抛 401。
|
||||
*/
|
||||
protected func requireLogin(): Unit {
|
||||
match (getLogin()) {
|
||||
case None => SimApiError.error(code: 401, message: "需要登录")
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定当前请求上下文(供 SimApiRequestDelegateFactory 调用;
|
||||
* Controller.setup 为 protected,此处以 public 方法暴露给框架)。
|
||||
*/
|
||||
public func bindRequestContext(context: HttpContext): Unit {
|
||||
this.setup(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* Controllers/SimApiCommonController:通用内置路由。
|
||||
*/
|
||||
|
||||
package simapi.controllers
|
||||
|
||||
import std.collection.*
|
||||
import soulsoft_web_mvc.annotations.*
|
||||
import simapi.communications.*
|
||||
import simapi.configurations.*
|
||||
import simapi.helpers.*
|
||||
|
||||
/**
|
||||
* 通用控制器:错误反馈、WebConfig、用户信息。
|
||||
* 对齐 C# 的 SimApiCommonController。
|
||||
* 控制器直接返回 SimApiBaseResponse / SimApiResponse<T>(对齐 C# SimApiBaseResponse<T>)。
|
||||
*/
|
||||
public class SimApiCommonController <: SimApiBaseController {
|
||||
private let _options: SimApiOptions
|
||||
|
||||
public init(options: SimApiOptions) {
|
||||
this._options = options
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /exception/{code}:错误反馈页面(始终注册)。
|
||||
* 返回 SimApiBaseResponse(已是响应体,原样输出)。
|
||||
*/
|
||||
@HttpGet["exception/{code}"]
|
||||
public func exceptionHandler(@FromRoute code: Int64): SimApiBaseResponse {
|
||||
SimApiBaseResponse(code, SimApiBaseResponse.getDefaultMessage(code))
|
||||
}
|
||||
|
||||
/**
|
||||
* GET/POST /versions:返回 SimApi/App 版本信息(HashMap 由 SimApiResponseFilter 自动封装)。
|
||||
*/
|
||||
@HttpGet["versions"]
|
||||
public func versions(): HashMap<String, Any> {
|
||||
versionsMap()
|
||||
}
|
||||
|
||||
@HttpPost["versions"]
|
||||
public func versionsPost(): HashMap<String, Any> {
|
||||
versionsMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* POST/GET /config:给前端的自定义信息(含版本)。
|
||||
* 对齐 C# WebConfigRoute 默认值 /config(SimApiCommonController.WebConfig,[HttpPost, HttpGet] 无路径,
|
||||
* 由 MapControllerRoute(pattern=WebConfigRoute) 注册;soulsoft 约定路由不支持 defaults,故用特性路由直接对齐路径)。
|
||||
*/
|
||||
@HttpGet["/config"]
|
||||
public func webConfig(): HashMap<String, Any> {
|
||||
webConfigMap()
|
||||
}
|
||||
|
||||
@HttpPost["/config"]
|
||||
public func webConfigPost(): HashMap<String, Any> {
|
||||
webConfigMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /user/info:获取已登录用户信息(需登录)。
|
||||
* 返回 SimApiLoginItem 由 SimApiResponseFilter 自动封装(data 为登录信息对象)。
|
||||
*/
|
||||
@HttpPost["/user/info"]
|
||||
public func userInfo(): SimApiLoginItem {
|
||||
requireLogin()
|
||||
loginInfo
|
||||
}
|
||||
|
||||
private func versionsMap(): HashMap<String, Any> {
|
||||
var map = HashMap<String, Any>()
|
||||
map["SimApi"] = SimApiUtil.simApiVersion
|
||||
map["App"] = SimApiUtil.appVersion
|
||||
map
|
||||
}
|
||||
|
||||
private func webConfigMap(): HashMap<String, Any> {
|
||||
var map = HashMap<String, Any>()
|
||||
for ((key, value) in _options.webConfig) {
|
||||
map[key] = value
|
||||
}
|
||||
if (_options.webConfigIncludeVersion) {
|
||||
map["Versions"] = versionsMap()
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.exceptions
|
||||
|
||||
/**
|
||||
* API 错误捕获异常:携带业务错误码,由异常中间件统一转换为 HTTP 200 + JSON。
|
||||
*/
|
||||
public class SimApiException <: Exception {
|
||||
public let code: Int64
|
||||
|
||||
public init(code: Int64, message!: String = "") {
|
||||
super(message)
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.extensions
|
||||
|
||||
import std.collection.*
|
||||
import std.convert.*
|
||||
import std.reflect.*
|
||||
import std.time.*
|
||||
import soulsoft_serialization.*
|
||||
import soulsoft_serialization.macros.*
|
||||
import soulsoft_web_http.*
|
||||
import soulsoft_web_hosting.*
|
||||
import soulsoft_web_mvc.*
|
||||
import soulsoft_web_mvc.applicationModels.*
|
||||
import soulsoft_web_mvc.routing.*
|
||||
import soulsoft_web_cors.*
|
||||
import soulsoft_web_routing.*
|
||||
import soulsoft_extensions_injection.*
|
||||
import soulsoft_extensions_logging.*
|
||||
import simapi.communications.*
|
||||
import simapi.configurations.*
|
||||
import simapi.controllers.*
|
||||
import simapi.helpers.*
|
||||
import simapi.logger.*
|
||||
import simapi.middlewares.*
|
||||
|
||||
/**
|
||||
* SimApi 扩展入口:对应 C# 的 SimApiExtensions(AddSimApi + UseSimApi)。
|
||||
*
|
||||
* 使用方式(仓颉版,与 addLogging 风格一致):
|
||||
* ```
|
||||
* let builder = WebHost.createBuilder(args)
|
||||
* builder.services.addLogging()
|
||||
* builder.addSimApi { options =>
|
||||
* options.enableSimApiAuth = true
|
||||
* }
|
||||
* let host = builder.build()
|
||||
* host.useSimApi()
|
||||
* host.run()
|
||||
* ```
|
||||
*/
|
||||
public interface SimApiBuilderExtensions {
|
||||
/**
|
||||
* 注册 SimApi 服务到 WebHostBuilder(自动 addRouting + addControllers + 扫描控制器)。
|
||||
* @param configure 配置回调。
|
||||
* @return 当前构建器。
|
||||
*/
|
||||
func addSimApi(configure: (SimApiOptions) -> Unit): WebHostBuilder
|
||||
|
||||
/**
|
||||
* 注册 SimApi 服务(默认配置,自动扫描控制器)。
|
||||
* @return 当前构建器。
|
||||
*/
|
||||
func addSimApi(): WebHostBuilder
|
||||
}
|
||||
|
||||
extend WebHostBuilder <: SimApiBuilderExtensions {
|
||||
/**
|
||||
* 注册 SimApi 服务(自动 addRouting + addControllers + addLogging + 扫描控制器)。
|
||||
* @param configure 配置回调。
|
||||
*/
|
||||
public func addSimApi(configure: (SimApiOptions) -> Unit): WebHostBuilder {
|
||||
// 自动注册路由(对齐 builder.Services.AddRouting())
|
||||
this.services.addRouting()
|
||||
// 先构造配置,供后续按开关注册服务(对齐 C# AddSimApi 中先读 options 再注册)
|
||||
let options = SimApiOptions()
|
||||
configure(options)
|
||||
// 响应封装(对齐 C# SimApiResponseFilter,受 EnableSimApiResponseFilter 开关控制):
|
||||
// 启用时注册自定义 IRequestDelegateFactory 自动封装响应。
|
||||
// 必须在 addControllers 之前:soulsoft 用 tryAddSingleton 注册,先到先得,不会被覆盖。
|
||||
// 未启用时使用 soulsoft 默认派发(String→ContentResult / ISerializable→ObjectResult / 其余→204)。
|
||||
if (options.enableSimApiResponseFilter) {
|
||||
this.services.addSingleton<IRequestDelegateFactory, SimApiRequestDelegateFactory>()
|
||||
}
|
||||
// 自动注册 MVC + 控制器(对齐 builder.Services.AddControllers())
|
||||
// 自动扫描调用者包中的 Controller 子类(对齐 C# 的 Assembly.GetTypes() 扫描)
|
||||
let controllers = SimApiControllerScanner.scan()
|
||||
this.services.addControllers(controllers)
|
||||
// 注册 SimApi 服务
|
||||
addSimApiCore(this, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 SimApi 服务(默认配置,自动扫描控制器)。
|
||||
*/
|
||||
public func addSimApi(): WebHostBuilder {
|
||||
addSimApi({_ =>})
|
||||
}
|
||||
}
|
||||
|
||||
private func addSimApiCore(builder: WebHostBuilder, options: SimApiOptions): WebHostBuilder {
|
||||
// 注册单例配置(对齐 C# builder.AddSingleton(simApiOptions))
|
||||
builder.services.addSingleton<SimApiOptions>(options)
|
||||
// 子配置不单独注册:中间件统一注入 SimApiOptions 后访问其属性
|
||||
// (对齐 C# SimApiExceptionMiddleware(..., SimApiOptions simApiOptions) 风格)
|
||||
|
||||
// 自定义日志格式(替换默认 console provider)
|
||||
if (options.enableLogger) {
|
||||
builder.services.addLogging {
|
||||
logging =>
|
||||
logging.clearProviders()
|
||||
logging.addProvider(SimApiLoggerProvider())
|
||||
}
|
||||
}
|
||||
|
||||
// 中间件无需注册:挂载时由 ActivatorUtilities 从 DI 解析构造参数创建
|
||||
// (对齐 C# builder.UseMiddleware<T>(),其中间件由 UseMiddleware 创建)
|
||||
|
||||
// 认证(DI 自动注入 SimApiOptions)
|
||||
if (options.enableSimApiAuth) {
|
||||
builder.services.addSingleton<SimApiAuth, SimApiAuth>()
|
||||
}
|
||||
|
||||
// 缓存(DI 自动注入 SimApiOptions)
|
||||
if (options.enableSimApiCache) {
|
||||
builder.services.addSingleton<SimApiCache, SimApiCache>()
|
||||
}
|
||||
|
||||
// HTTP 客户端(DI 自动注入 SimApiOptions)
|
||||
if (options.enableSimApiHttpClient) {
|
||||
builder.services.addSingleton<SimApiHttpClient, SimApiHttpClient>()
|
||||
}
|
||||
|
||||
// CORS(对齐 C# builder.Services.AddCors(policy => policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()))
|
||||
if (options.enableCors) {
|
||||
builder.services.addCors {
|
||||
cors =>
|
||||
cors.addDefaultPolicy {
|
||||
policy =>
|
||||
policy.allowAnyOrigin()
|
||||
policy.allowAnyMethod()
|
||||
policy.allowAnyHeader()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
/**
|
||||
* SimApi 主机扩展。
|
||||
*/
|
||||
public interface SimApiHostExtensions {
|
||||
/**
|
||||
* 应用 SimApi 中间件与内置路由到 WebHost。
|
||||
*/
|
||||
func useSimApi(): Unit
|
||||
}
|
||||
|
||||
extend WebHost <: SimApiHostExtensions {
|
||||
/**
|
||||
* 应用 SimApi 中间件与内置路由(日志输出对齐 C# UseSimApi)。
|
||||
*/
|
||||
public func useSimApi(): Unit {
|
||||
let options = this.services.getOrThrow<SimApiOptions>()
|
||||
let loggerFactory = this.services.getOrThrow<ILoggerFactory>()
|
||||
// 对齐 C# ILogger<SimApiOptions>:分类名为 SimApiOptions 的全限定名
|
||||
let logger = loggerFactory.createLogger<SimApiOptions>()
|
||||
|
||||
// ===== 基础信息(对齐 C# UseSimApi(IHost) 开头) =====
|
||||
let now = DateTime.now()
|
||||
logger.info("当前时区: ${now.zoneId}")
|
||||
logger.info("主应用版本: ${SimApiUtil.appVersion}\nSimApi版本: ${SimApiUtil.simApiVersion}")
|
||||
|
||||
// RedisCache
|
||||
if (!options.redisConfiguration.isEmpty()) {
|
||||
logger.info("开始配置 RedisCache ...")
|
||||
}
|
||||
|
||||
// SimApiCache
|
||||
if (options.enableSimApiCache) {
|
||||
logger.info("开始配置 SimApiCache...")
|
||||
}
|
||||
|
||||
// SimApiStorage(占位)
|
||||
if (options.enableSimApiStorage) {
|
||||
logger.info("开始配置 SimApiStorage...")
|
||||
}
|
||||
|
||||
// SimApiHttpClient
|
||||
if (options.enableSimApiHttpClient) {
|
||||
logger.info(
|
||||
"开始配置 SimApiHttpClient...\n服务器地址: ${options.simApiHttpClientOptions.server}\nAppId: ${options.simApiHttpClientOptions.appId}\nAppkey: ${options.simApiHttpClientOptions.appKey}")
|
||||
}
|
||||
|
||||
// Synapse(占位)
|
||||
if (options.enableSynapse) {
|
||||
logger.info("开始配置 SimApiSynapse...")
|
||||
}
|
||||
|
||||
// SimApiJob(占位)
|
||||
if (options.enableJob) {
|
||||
logger.info("开始配置 SimApiJob ...")
|
||||
}
|
||||
|
||||
// ===== 中间件与路由(对齐 C# UseSimApi(WebApplication) 的挂载顺序) =====
|
||||
// C# 挂载顺序(先挂载 = 外层):CORS(L425) → AuthGate(L454) → Auth(L462) → RequestLog(L519) → Exception(L525)
|
||||
// OPTIONS 预检请求在 CORS 处短路(204,不调用 next),因此 RequestLog/Exception 均不会执行
|
||||
|
||||
// CORS(对齐 C# builder.UseCors("any"),最先挂载)
|
||||
if (options.enableCors) {
|
||||
logger.info("开始配置 Cors全部允许...")
|
||||
this.useCors()
|
||||
}
|
||||
|
||||
// AuthGate(占位,对齐 C# UseMiddleware<SimApiAuthCenterMiddleware>)
|
||||
if (options.enableSimApiAuthGate) {
|
||||
logger.info("开始配置 SimApiAuthGate...")
|
||||
}
|
||||
|
||||
// 认证中间件(对齐 C# builder.UseMiddleware<SimApiAuthMiddleware>())
|
||||
if (options.enableSimApiAuth) {
|
||||
logger.info("开始配置 SimApiAuth...")
|
||||
this.use<SimApiAuthMiddleware>()
|
||||
}
|
||||
|
||||
// 请求日志中间件(对齐 C# builder.UseMiddleware<SimApiRequestLogMiddleware>())
|
||||
if (options.enableRequestLog) {
|
||||
logger.info("开始配置 SimApiRequestLog...")
|
||||
this.use<SimApiRequestLogMiddleware>()
|
||||
}
|
||||
|
||||
// 异常中间件最后挂载(最内层,对齐 C# builder.UseMiddleware<SimApiExceptionMiddleware>())
|
||||
if (options.enableSimApiException) {
|
||||
logger.info("开始配置 SimApiException...")
|
||||
this.use<SimApiExceptionMiddleware>()
|
||||
}
|
||||
|
||||
// 内置路由
|
||||
if (let Some(route) <- options.simApiRouteOptions.userInfoRoute) {
|
||||
logger.info("注册内置Route: UserInfo => ${route}")
|
||||
}
|
||||
if (let Some(route) <- options.simApiRouteOptions.logoutRoute) {
|
||||
logger.info("注册内置Route: Logout => ${route}")
|
||||
}
|
||||
if (let Some(route) <- options.simApiRouteOptions.webConfigRoute) {
|
||||
logger.info("注册内置Route: WebConfig => ${route}")
|
||||
}
|
||||
|
||||
// SimApiDoc(占位)
|
||||
if (options.enableSimApiDoc) {
|
||||
logger.info("开始配置 SimApiDoc...")
|
||||
}
|
||||
|
||||
// URL 小写
|
||||
if (options.enableLowerUrl) {
|
||||
logger.info("开始配置使用URL小写...")
|
||||
}
|
||||
|
||||
// SimApiJob Web 控制台(占位)
|
||||
if (options.enableJob && options.simApiJobOptions.dashboardUrl != None) {
|
||||
logger.info("开始配置 SimApiJob Web控制台...")
|
||||
}
|
||||
|
||||
// 响应封装(已实现:addSimApi 中按开关注册 SimApiRequestDelegateFactory 自动封装,
|
||||
// 对齐 C# SimApiResponseFilter;此处仅输出配置日志)
|
||||
if (options.enableSimApiResponseFilter) {
|
||||
logger.info("开始配置 SimApiResponseFilter...")
|
||||
}
|
||||
|
||||
// ForwardedHeaders(占位:soulsoft 暂无内置)
|
||||
if (options.enableForwardHeaders) {
|
||||
logger.info("开始配置ForwardedHeaders...")
|
||||
}
|
||||
|
||||
// 映射控制器端点(对齐 C# UseSimApi 中的 MapControllers)
|
||||
let callSiteFactory = this.services.getOrThrow<IServiceProviderIsService>()
|
||||
if (callSiteFactory.isService<ApplicationPartManager>()) {
|
||||
this.mapControllers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SimApi MVC 注册扩展:对齐 .NET 的 builder.Services.AddControllers()。
|
||||
* 注册 MVC 服务 + SimApi 内置控制器 + 用户控制器。
|
||||
*
|
||||
* 说明:控制器端点映射使用 soulsoft_web_mvc 的 mapControllers()(对齐 .NET MapControllers()),
|
||||
* 宿主在 WebHost 上直接调用 host.mapControllers() 即可。
|
||||
*/
|
||||
public interface SimApiMvcBuilderExtensions {
|
||||
/**
|
||||
* 注册 MVC 服务与控制器。
|
||||
* @param controllerTypes 用户控制器类型列表(可选)。
|
||||
* @return MVC 构建器。
|
||||
*/
|
||||
func addControllers(controllerTypes: Array<TypeInfo>): MvcBuilder
|
||||
}
|
||||
|
||||
extend ServiceCollection <: SimApiMvcBuilderExtensions {
|
||||
/**
|
||||
* 注册 MVC 服务,并注册 SimApi 内置控制器 + 用户控制器到 ApplicationPartManager。
|
||||
* 对齐 .NET 的 AddControllers()(含控制器发现)。
|
||||
* @param controllerTypes 用户控制器类型列表。
|
||||
* @return MVC 构建器。
|
||||
*/
|
||||
public func addControllers(controllerTypes: Array<TypeInfo>): MvcBuilder {
|
||||
// 调用 soulsoft_web_mvc 的无参 addControllers() 注册 MVC 核心服务
|
||||
let mvc = this.addControllers()
|
||||
let types = ArrayList<TypeInfo>()
|
||||
// SimApi 内置控制器
|
||||
types.add(TypeInfo.of<SimApiCommonController>())
|
||||
types.add(TypeInfo.of<SimApiAuthController>())
|
||||
// 用户控制器
|
||||
for (t in controllerTypes) {
|
||||
types.add(t)
|
||||
}
|
||||
let part = AssemblyPart("simapi.controllers", types.toArray())
|
||||
mvc.addApplicationPart(part)
|
||||
mvc
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* 自定义 IRequestDelegateFactory:在结果派发时自动封装响应,
|
||||
* 对齐 C# SimApiResponseFilter(IResultFilter)的行为:
|
||||
* - 返回 SimApiBaseResponse 或其子类 → 原样输出
|
||||
* - 返回 null/void(Unit)→ SimApiBaseResponse()({code:200, message:成功})
|
||||
* - 返回 String → SimApiResponse<String>(data 为字符串)
|
||||
* - 返回其他对象 → SimApiResponse<Any>(data 为对象)
|
||||
*
|
||||
* 注册方式:必须在 soulsoft addControllers() 之前注册(tryAddSingleton 先到先得)。
|
||||
*/
|
||||
|
||||
package simapi.extensions
|
||||
|
||||
import std.collection.*
|
||||
import std.reflect.*
|
||||
import soulsoft_web_http.*
|
||||
import soulsoft_web_mvc.core.*
|
||||
import soulsoft_web_mvc.routing.*
|
||||
import soulsoft_web_mvc.controllers.*
|
||||
import soulsoft_web_mvc.modelBindings.*
|
||||
import soulsoft_web_mvc.abstractions.*
|
||||
import soulsoft_extensions_options.*
|
||||
import soulsoft_extensions_injection.*
|
||||
import simapi.communications.*
|
||||
import simapi.controllers.*
|
||||
|
||||
/**
|
||||
* 自定义请求委托工厂:接管 soulsoft 的 ControllerRequestDelegateFactory,
|
||||
* 在结果派发时自动封装响应(对齐 C# SimApiResponseFilter)。
|
||||
*/
|
||||
public class SimApiRequestDelegateFactory <: IRequestDelegateFactory {
|
||||
private let _mvcOptions: MvcOptions
|
||||
private let _modelBinder: IActionModelBinder
|
||||
|
||||
public init(mvcOptions: IOptions<MvcOptions>, modelBinder: IActionModelBinder, services: IServiceProvider) {
|
||||
_mvcOptions = mvcOptions.value
|
||||
_modelBinder = modelBinder
|
||||
}
|
||||
|
||||
public func createRequestDelegate(actionDescriptor: ControllerActionDescriptor): RequestDelegate {
|
||||
return {
|
||||
context => SimApiActionInvoker(context, _modelBinder, actionDescriptor, _mvcOptions).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单次请求的动作执行器(复制 soulsoft ControllerActionInvoker,
|
||||
* dispatchResult 改为自动封装响应)。
|
||||
*/
|
||||
struct SimApiActionInvoker {
|
||||
SimApiActionInvoker(let context: HttpContext, let modelBinder: IActionModelBinder,
|
||||
let actionDescriptor: ControllerActionDescriptor, let mvcOptions: MvcOptions) {
|
||||
}
|
||||
|
||||
public func apply(): Unit {
|
||||
let controller = createControllerInstance()
|
||||
let modelBindingContext = ActionBindingContext(context, actionDescriptor.actionFunction.parameters)
|
||||
let boundParameters = modelBinder.bind(modelBindingContext)
|
||||
if (!modelBindingContext.modelState.isValid) {
|
||||
handleInvalidModelState(modelBindingContext)
|
||||
} else {
|
||||
let actionResult = actionDescriptor.actionFunction.apply(controller, boundParameters)
|
||||
dispatchResult(actionResult)
|
||||
}
|
||||
}
|
||||
|
||||
/// 模型绑定失败时写入 ProblemDetails 响应
|
||||
private func handleInvalidModelState(modelBindingContext: ActionBindingContext) {
|
||||
let options = context.services.getOrThrow<IOptions<ApiBehaviorOptions>>()
|
||||
if (let Some(factory) <- options.value.invalidModelStateResponseFactory) {
|
||||
let actionContext = ActionContext(context, modelBindingContext.modelState)
|
||||
factory(actionContext).invoke(context)
|
||||
} else {
|
||||
let details = createValidationProblemDetails(modelBindingContext)
|
||||
if (let Some(status) <- details.status) {
|
||||
context.response.statusCode = UInt16(status)
|
||||
}
|
||||
context.response.writeAsJson(details)
|
||||
}
|
||||
}
|
||||
|
||||
/// 结果派发 + 自动封装(对齐 C# SimApiResponseFilter)
|
||||
private func dispatchResult(actionResult: Any) {
|
||||
if (let result: IActionResult <- actionResult) {
|
||||
// 显式返回 IActionResult(如 ContentResult)→ 原样
|
||||
result.invoke(context)
|
||||
} else if (let result: SimApiBaseResponse <- actionResult) {
|
||||
// 已是 SimApiBaseResponse(含子类)→ 原样输出
|
||||
ObjectResult<Any>(result).invoke(context)
|
||||
} else if (let result: String <- actionResult) {
|
||||
// String → SimApiResponse<String>(data 为字符串)
|
||||
ObjectResult<Any>(SimApiResponse<String>(result)).invoke(context)
|
||||
} else if (let result: Unit <- actionResult) {
|
||||
// void/无返回 → SimApiBaseResponse()({code:200, message:成功})
|
||||
context.response.writeAsJson(SimApiBaseResponse())
|
||||
} else {
|
||||
// 其他对象(DTO/数组/动态结构)→ SimApiDataResponse
|
||||
// data 由 SimApiDataResponse.serializeObject 内嵌为对象(ISerializable → 对象;
|
||||
// HashMap<String,Any> 等动态结构 → SimApiJson 序列化后解析内嵌),不会变成 JSON 字符串
|
||||
ObjectResult<Any>(SimApiDataResponse(actionResult)).invoke(context)
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据 ModelState 错误构建 ValidationProblemDetails
|
||||
private func createValidationProblemDetails(modelBindingContext: ActionBindingContext) {
|
||||
let details = ValidationProblemDetails()
|
||||
if (hasUnsupportedContentTypeError(modelBindingContext.modelState)) {
|
||||
details.`type` = "https://tools.ietf.org/html/rfc9110#section-15.5.16"
|
||||
details.title = "Unsupported Media Type"
|
||||
details.status = 415
|
||||
} else {
|
||||
details.`type` = "https://tools.ietf.org/html/rfc9110#section-15.5.1"
|
||||
details.title = "One or more validation errors occurred."
|
||||
details.status = 400
|
||||
for ((name, entry) in modelBindingContext.modelState) {
|
||||
details.errors.add(name, entry.errors |> map {f => f.description} |> collectArray)
|
||||
}
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
/// 检查 ModelState 中是否含有 UnsupportedContentTypeException 错误
|
||||
private func hasUnsupportedContentTypeError(modelState: ModelStateDictionary) {
|
||||
for ((_, entry) in modelState) {
|
||||
for (error in entry.errors) {
|
||||
if (error.exception.flatMap {f => f as UnsupportedContentTypeException}.isSome()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// 通过 DI 容器实例化控制器,并注入当前 HttpContext
|
||||
private func createControllerInstance(): Object {
|
||||
let instance = ActivatorUtilities.createInstance(context.services, actionDescriptor.controllerType)
|
||||
if (let controller: SimApiBaseController <- instance) {
|
||||
controller.bindRequestContext(context)
|
||||
}
|
||||
return instance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.helpers
|
||||
|
||||
import std.collection.*
|
||||
import std.random.*
|
||||
import stdx.crypto.digest.*
|
||||
import stdx.encoding.base64.*
|
||||
|
||||
/**
|
||||
* AES-256-CBC + PKCS7 加解密工具(对齐 C# SimApiAesUtil)。
|
||||
*
|
||||
* 约定(与 C# 完全一致):
|
||||
* - 密钥:SHA256(key 字符串) → 32 字节
|
||||
* - 模式:AES-256-CBC,PKCS7 填充
|
||||
* - IV:每次加密随机生成 16 字节,前置在密文前
|
||||
* - 输出:Base64(IV(16) + 密文)
|
||||
*
|
||||
* 仓颉生态(stdx / soulsoft)均无现成 AES 实现,此处纯仓颉实现 FIPS-197 AES-256。
|
||||
*/
|
||||
public class SimApiAesUtil {
|
||||
private init() {}
|
||||
|
||||
/**
|
||||
* AES 加密:Base64(随机IV + 密文)。
|
||||
* @param plainText 明文。
|
||||
* @param key 字符串密钥(SHA256 处理后为 256 位)。
|
||||
* @return Base64(IV + 密文)。
|
||||
*/
|
||||
public static func encrypt(plainText: String, key: String): String {
|
||||
if (plainText.isEmpty()) {
|
||||
throw Exception("plainText 不能为空")
|
||||
}
|
||||
if (key.isEmpty()) {
|
||||
throw Exception("key 不能为空")
|
||||
}
|
||||
let keyBytes = processKey(key)
|
||||
let iv = generateIv()
|
||||
let padded = pkcs7Pad(plainText.toArray())
|
||||
let cipher = cbcEncrypt(padded, keyBytes, iv)
|
||||
|
||||
// IV + 密文 → Base64
|
||||
var out = ArrayList<Byte>()
|
||||
for (b in iv) {
|
||||
out.add(toB(b))
|
||||
}
|
||||
for (b in cipher) {
|
||||
out.add(toB(b))
|
||||
}
|
||||
toBase64String(out.toArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* AES 解密。
|
||||
* @param cipherText Base64(IV + 密文)。
|
||||
* @param key 字符串密钥(与加密时相同)。
|
||||
* @return 明文。
|
||||
*/
|
||||
public static func decrypt(cipherText: String, key: String): String {
|
||||
if (cipherText.isEmpty()) {
|
||||
throw Exception("cipherText 不能为空")
|
||||
}
|
||||
if (key.isEmpty()) {
|
||||
throw Exception("key 不能为空")
|
||||
}
|
||||
let all = fromBase64String(cipherText).getOrThrow { Exception("Base64 解码失败") }
|
||||
if (all.size < 32) {
|
||||
throw Exception("密文长度非法")
|
||||
}
|
||||
var iv = Array<UInt8>(16, repeat: 0)
|
||||
var cipher = Array<UInt8>(all.size - 16, repeat: 0)
|
||||
for (i in 0..16) {
|
||||
iv[i] = toU8(all[i])
|
||||
}
|
||||
for (i in 0..cipher.size) {
|
||||
cipher[i] = toU8(all[i + 16])
|
||||
}
|
||||
let keyBytes = processKey(key)
|
||||
let padded = cbcDecrypt(cipher, keyBytes, iv)
|
||||
let plain = pkcs7Unpad(padded)
|
||||
String.fromUtf8(toBytes(plain))
|
||||
}
|
||||
|
||||
// ===== 密钥与 IV =====
|
||||
|
||||
/// SHA256(key) → 32 字节密钥
|
||||
@OverflowWrapping
|
||||
private static func processKey(key: String): Array<UInt8> {
|
||||
let md = SHA256()
|
||||
md.write(key.toArray())
|
||||
let digest = md.finish()
|
||||
var out = Array<UInt8>(digest.size, repeat: 0)
|
||||
for (i in 0..digest.size) {
|
||||
out[i] = toU8(digest[i])
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 16 字节随机 IV
|
||||
private static func generateIv(): Array<UInt8> {
|
||||
let rnd = Random()
|
||||
var iv = Array<UInt8>(16, repeat: 0)
|
||||
for (i in 0..16) {
|
||||
iv[i] = UInt8(rnd.nextUInt64() & 0xFFu64)
|
||||
}
|
||||
iv
|
||||
}
|
||||
|
||||
// ===== PKCS7 填充 =====
|
||||
|
||||
@OverflowWrapping
|
||||
private static func pkcs7Pad(data: Array<Byte>): Array<UInt8> {
|
||||
let padLen = 16 - (data.size % 16)
|
||||
var out = ArrayList<UInt8>()
|
||||
for (b in data) {
|
||||
out.add(toU8(b))
|
||||
}
|
||||
for (i in 0..padLen) {
|
||||
out.add(UInt8(padLen))
|
||||
}
|
||||
out.toArray()
|
||||
}
|
||||
|
||||
@OverflowWrapping
|
||||
private static func pkcs7Unpad(data: Array<UInt8>): Array<UInt8> {
|
||||
if (data.size == 0) {
|
||||
return Array<UInt8>(0, repeat: 0)
|
||||
}
|
||||
let padLen = Int64(data[data.size - 1])
|
||||
if (padLen < 1 || padLen > 16) {
|
||||
throw Exception("PKCS7 填充非法")
|
||||
}
|
||||
data[0..data.size - padLen]
|
||||
}
|
||||
|
||||
// ===== CBC 模式 =====
|
||||
|
||||
@OverflowWrapping
|
||||
private static func cbcEncrypt(padded: Array<UInt8>, key: Array<UInt8>, iv: Array<UInt8>): Array<UInt8> {
|
||||
let roundKeys = keyExpansion(key)
|
||||
var prev = Array<UInt8>(16, repeat: 0)
|
||||
for (i in 0..16) {
|
||||
prev[i] = iv[i]
|
||||
}
|
||||
var out = ArrayList<UInt8>()
|
||||
let n = padded.size / 16
|
||||
for (block in 0..n) {
|
||||
var state = Array<UInt8>(16, repeat: 0)
|
||||
for (i in 0..16) {
|
||||
state[i] = padded[block * 16 + i] ^ prev[i]
|
||||
}
|
||||
let enc = aesEncryptBlock(state, roundKeys)
|
||||
for (b in enc) {
|
||||
out.add(b)
|
||||
}
|
||||
prev = enc
|
||||
}
|
||||
out.toArray()
|
||||
}
|
||||
|
||||
@OverflowWrapping
|
||||
private static func cbcDecrypt(cipher: Array<UInt8>, key: Array<UInt8>, iv: Array<UInt8>): Array<UInt8> {
|
||||
let roundKeys = keyExpansion(key)
|
||||
var prev = Array<UInt8>(16, repeat: 0)
|
||||
for (i in 0..16) {
|
||||
prev[i] = iv[i]
|
||||
}
|
||||
var out = ArrayList<UInt8>()
|
||||
let n = cipher.size / 16
|
||||
for (block in 0..n) {
|
||||
var blockBytes = Array<UInt8>(16, repeat: 0)
|
||||
for (i in 0..16) {
|
||||
blockBytes[i] = cipher[block * 16 + i]
|
||||
}
|
||||
let dec = aesDecryptBlock(blockBytes, roundKeys)
|
||||
for (i in 0..16) {
|
||||
out.add(dec[i] ^ prev[i])
|
||||
}
|
||||
prev = blockBytes
|
||||
}
|
||||
out.toArray()
|
||||
}
|
||||
|
||||
// ===== AES-256 核心(FIPS-197) =====
|
||||
|
||||
/// S-box / 逆 S-box(运行时生成,避免手写 256 字节表出错)
|
||||
private static let sbox: Array<UInt8> = generateSbox()
|
||||
private static let invSbox: Array<UInt8> = generateInvSbox()
|
||||
|
||||
@OverflowWrapping
|
||||
private static func generateSbox(): Array<UInt8> {
|
||||
var s = Array<UInt8>(256, repeat: 0)
|
||||
var p = 1u8
|
||||
var q = 1u8
|
||||
while (true) {
|
||||
// p *= 3(GF(2^8) 生成元遍历)
|
||||
p = p ^ (p << 1u8) ^ (if ((p & 0x80u8) != 0u8) { 0x1Bu8 } else { 0u8 })
|
||||
// q /= 3(等价乘以 0xF6)
|
||||
q = q ^ (q << 1u8)
|
||||
q = q ^ (q << 2u8)
|
||||
q = q ^ (q << 4u8)
|
||||
q = q ^ (if ((q & 0x80u8) != 0u8) { 0x09u8 } else { 0u8 })
|
||||
// 仿射变换
|
||||
let x = q ^ rotl8(q, 1) ^ rotl8(q, 2) ^ rotl8(q, 3) ^ rotl8(q, 4)
|
||||
s[Int64(p)] = x ^ 0x63u8
|
||||
if (p == 1u8) {
|
||||
break
|
||||
}
|
||||
}
|
||||
// p 序列遍历非零元素,s[0] 从未赋值:AES 规定 S(0) = 0x63(0 的逆为 0,仿射变换结果)
|
||||
s[0] = 0x63u8
|
||||
s
|
||||
}
|
||||
|
||||
@OverflowWrapping
|
||||
private static func generateInvSbox(): Array<UInt8> {
|
||||
var inv = Array<UInt8>(256, repeat: 0)
|
||||
for (i in 0..256) {
|
||||
inv[Int64(sbox[i])] = UInt8(i)
|
||||
}
|
||||
inv
|
||||
}
|
||||
|
||||
/// 8 位循环左移
|
||||
private static func rotl8(v: UInt8, n: Int64): UInt8 {
|
||||
UInt8(((UInt16(v) << n) | (UInt16(v) >> (8 - n))) & 0xFFu16)
|
||||
}
|
||||
|
||||
/// GF(2^8) 乘以 2(xtime)
|
||||
@OverflowWrapping
|
||||
private static func xtime(a: UInt8): UInt8 {
|
||||
if ((a & 0x80u8) != 0u8) {
|
||||
(a << 1u8) ^ 0x1Bu8
|
||||
} else {
|
||||
a << 1u8
|
||||
}
|
||||
}
|
||||
|
||||
/// GF(2^8) 通用乘法
|
||||
@OverflowWrapping
|
||||
private static func gfMul(a: UInt8, b: UInt8): UInt8 {
|
||||
var result = 0u8
|
||||
var aa = a
|
||||
var bb = b
|
||||
for (i in 0..8) {
|
||||
if ((bb & 1u8) != 0u8) {
|
||||
result = result ^ aa
|
||||
}
|
||||
let hi = (aa & 0x80u8) != 0u8
|
||||
aa = aa << 1u8
|
||||
if (hi) {
|
||||
aa = aa ^ 0x1Bu8
|
||||
}
|
||||
bb = bb >> 1u8
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// 密钥扩展:32 字节密钥 → 240 字节轮密钥(60 words,AES-256 共 15 轮)
|
||||
@OverflowWrapping
|
||||
private static func keyExpansion(key: Array<UInt8>): Array<UInt8> {
|
||||
var w = Array<UInt8>(240, repeat: 0)
|
||||
for (i in 0..32) {
|
||||
w[i] = key[i]
|
||||
}
|
||||
var rcon = 1u8
|
||||
for (i in 8..60) {
|
||||
var temp = Array<UInt8>(4, repeat: 0)
|
||||
for (j in 0..4) {
|
||||
temp[j] = w[(i - 1) * 4 + j]
|
||||
}
|
||||
if (i % 8 == 0) {
|
||||
// RotWord
|
||||
let t0 = temp[0]
|
||||
temp[0] = temp[1]
|
||||
temp[1] = temp[2]
|
||||
temp[2] = temp[3]
|
||||
temp[3] = t0
|
||||
// SubWord
|
||||
for (j in 0..4) {
|
||||
temp[j] = sbox[Int64(temp[j])]
|
||||
}
|
||||
temp[0] = temp[0] ^ rcon
|
||||
rcon = xtime(rcon)
|
||||
} else if (i % 8 == 4) {
|
||||
for (j in 0..4) {
|
||||
temp[j] = sbox[Int64(temp[j])]
|
||||
}
|
||||
}
|
||||
for (j in 0..4) {
|
||||
w[i * 4 + j] = w[(i - 8) * 4 + j] ^ temp[j]
|
||||
}
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
@OverflowWrapping
|
||||
private static func aesEncryptBlock(input: Array<UInt8>, roundKeys: Array<UInt8>): Array<UInt8> {
|
||||
var state = Array<UInt8>(16, repeat: 0)
|
||||
for (i in 0..16) {
|
||||
state[i] = input[i]
|
||||
}
|
||||
addRoundKey(state, roundKeys, 0)
|
||||
for (round in 1..14) {
|
||||
subBytes(state)
|
||||
shiftRows(state)
|
||||
mixColumns(state)
|
||||
addRoundKey(state, roundKeys, round)
|
||||
}
|
||||
subBytes(state)
|
||||
shiftRows(state)
|
||||
addRoundKey(state, roundKeys, 14)
|
||||
state
|
||||
}
|
||||
|
||||
@OverflowWrapping
|
||||
private static func aesDecryptBlock(input: Array<UInt8>, roundKeys: Array<UInt8>): Array<UInt8> {
|
||||
var state = Array<UInt8>(16, repeat: 0)
|
||||
for (i in 0..16) {
|
||||
state[i] = input[i]
|
||||
}
|
||||
addRoundKey(state, roundKeys, 14)
|
||||
// 轮 13..1(逆序)
|
||||
for (i in 0..13) {
|
||||
let round = 13 - i
|
||||
invShiftRows(state)
|
||||
invSubBytes(state)
|
||||
addRoundKey(state, roundKeys, round)
|
||||
invMixColumns(state)
|
||||
}
|
||||
invShiftRows(state)
|
||||
invSubBytes(state)
|
||||
addRoundKey(state, roundKeys, 0)
|
||||
state
|
||||
}
|
||||
|
||||
@OverflowWrapping
|
||||
private static func addRoundKey(state: Array<UInt8>, roundKeys: Array<UInt8>, round: Int64): Unit {
|
||||
for (i in 0..16) {
|
||||
state[i] = state[i] ^ roundKeys[round * 16 + i]
|
||||
}
|
||||
}
|
||||
|
||||
@OverflowWrapping
|
||||
private static func subBytes(state: Array<UInt8>): Unit {
|
||||
for (i in 0..16) {
|
||||
state[i] = sbox[Int64(state[i])]
|
||||
}
|
||||
}
|
||||
|
||||
@OverflowWrapping
|
||||
private static func invSubBytes(state: Array<UInt8>): Unit {
|
||||
for (i in 0..16) {
|
||||
state[i] = invSbox[Int64(state[i])]
|
||||
}
|
||||
}
|
||||
|
||||
/// ShiftRows:行 r 循环左移 r 字节(列主序 state[i] = s[r + 4*c])
|
||||
@OverflowWrapping
|
||||
private static func shiftRows(state: Array<UInt8>): Unit {
|
||||
var tmp = Array<UInt8>(16, repeat: 0)
|
||||
for (r in 0..4) {
|
||||
for (c in 0..4) {
|
||||
tmp[r + 4 * c] = state[r + 4 * ((c + r) % 4)]
|
||||
}
|
||||
}
|
||||
for (i in 0..16) {
|
||||
state[i] = tmp[i]
|
||||
}
|
||||
}
|
||||
|
||||
/// InvShiftRows:行 r 循环右移 r 字节
|
||||
@OverflowWrapping
|
||||
private static func invShiftRows(state: Array<UInt8>): Unit {
|
||||
var tmp = Array<UInt8>(16, repeat: 0)
|
||||
for (r in 0..4) {
|
||||
for (c in 0..4) {
|
||||
tmp[r + 4 * c] = state[r + 4 * ((c - r % 4 + 4) % 4)]
|
||||
}
|
||||
}
|
||||
for (i in 0..16) {
|
||||
state[i] = tmp[i]
|
||||
}
|
||||
}
|
||||
|
||||
/// MixColumns:每列乘固定矩阵 [[2,3,1,1],[1,2,3,1],[1,1,2,3],[3,1,1,2]]
|
||||
@OverflowWrapping
|
||||
private static func mixColumns(state: Array<UInt8>): Unit {
|
||||
for (c in 0..4) {
|
||||
let a0 = state[0 + 4 * c]
|
||||
let a1 = state[1 + 4 * c]
|
||||
let a2 = state[2 + 4 * c]
|
||||
let a3 = state[3 + 4 * c]
|
||||
state[0 + 4 * c] = gfMul(a0, 2u8) ^ gfMul(a1, 3u8) ^ a2 ^ a3
|
||||
state[1 + 4 * c] = a0 ^ gfMul(a1, 2u8) ^ gfMul(a2, 3u8) ^ a3
|
||||
state[2 + 4 * c] = a0 ^ a1 ^ gfMul(a2, 2u8) ^ gfMul(a3, 3u8)
|
||||
state[3 + 4 * c] = gfMul(a0, 3u8) ^ a1 ^ a2 ^ gfMul(a3, 2u8)
|
||||
}
|
||||
}
|
||||
|
||||
/// InvMixColumns:每列乘逆矩阵 [[14,11,13,9],[9,14,11,13],[13,9,14,11],[11,13,9,14]]
|
||||
@OverflowWrapping
|
||||
private static func invMixColumns(state: Array<UInt8>): Unit {
|
||||
for (c in 0..4) {
|
||||
let a0 = state[0 + 4 * c]
|
||||
let a1 = state[1 + 4 * c]
|
||||
let a2 = state[2 + 4 * c]
|
||||
let a3 = state[3 + 4 * c]
|
||||
state[0 + 4 * c] = gfMul(a0, 14u8) ^ gfMul(a1, 11u8) ^ gfMul(a2, 13u8) ^ gfMul(a3, 9u8)
|
||||
state[1 + 4 * c] = gfMul(a0, 9u8) ^ gfMul(a1, 14u8) ^ gfMul(a2, 11u8) ^ gfMul(a3, 13u8)
|
||||
state[2 + 4 * c] = gfMul(a0, 13u8) ^ gfMul(a1, 9u8) ^ gfMul(a2, 14u8) ^ gfMul(a3, 11u8)
|
||||
state[3 + 4 * c] = gfMul(a0, 11u8) ^ gfMul(a1, 13u8) ^ gfMul(a2, 9u8) ^ gfMul(a3, 14u8)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 字节转换 =====
|
||||
|
||||
/// Byte → UInt8:负 Byte(>127 字节的补码)按位重解释为 0-255
|
||||
private static func toU8(b: Byte): UInt8 {
|
||||
if (b < 0) {
|
||||
UInt8(Int64(b) + 256)
|
||||
} else {
|
||||
UInt8(Int64(b))
|
||||
}
|
||||
}
|
||||
|
||||
private static func toB(u: UInt8): Byte {
|
||||
let b: Byte = u
|
||||
b
|
||||
}
|
||||
|
||||
private static func toBytes(data: Array<UInt8>): Array<Byte> {
|
||||
var out = Array<Byte>(data.size, repeat: 0)
|
||||
for (i in 0..data.size) {
|
||||
out[i] = toB(data[i])
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.helpers
|
||||
|
||||
import std.collection.*
|
||||
import std.collection.concurrent.*
|
||||
import std.convert.*
|
||||
import stdx.encoding.json.*
|
||||
import soulsoft_serialization.*
|
||||
import redis.client.*
|
||||
import simapi.communications.*
|
||||
import simapi.configurations.*
|
||||
import simapi.exceptions.*
|
||||
|
||||
/**
|
||||
* 认证助手:基于 Header Token 的登录态管理。
|
||||
* 支持两种存储模式:
|
||||
* - Redis 模式:配置了 RedisConfiguration 时使用,支持多实例共享。
|
||||
* - InMemory 模式:未配置 Redis 时自动使用,重启后登录态丢失。
|
||||
*/
|
||||
public class SimApiAuth {
|
||||
private static let tokenCachePrefix = "SimApi:Auth:Token:"
|
||||
private static let tokenSetCachePrefix = "SimApi:Auth:User:"
|
||||
|
||||
private var _redis: ?RedisClient = None
|
||||
private var _redisHost: String = ""
|
||||
private var _redisPort: UInt16 = 6379
|
||||
|
||||
// InMemory 模式:token → 登录信息 JSON
|
||||
private let _tokenStore = ConcurrentHashMap<String, String>()
|
||||
// InMemory 模式:userId → token 集合
|
||||
private let _userTokens = ConcurrentHashMap<String, HashSet<String>>()
|
||||
|
||||
/**
|
||||
* 创建认证助手(依赖注入 SimApiOptions)。
|
||||
* @param options SimApi 配置(redisConfiguration 非空时使用 Redis,否则 InMemory)。
|
||||
*/
|
||||
public init(options: SimApiOptions) {
|
||||
let redisConfiguration = options.redisConfiguration
|
||||
if (!redisConfiguration.isEmpty()) {
|
||||
let (host, port) = parseRedisConfig(redisConfiguration)
|
||||
_redisHost = host
|
||||
_redisPort = port
|
||||
_redis = Some(RedisClient(host, port))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录:生成 token 并保存登录信息。
|
||||
* @param loginItem 登录信息。
|
||||
* @param expireSeconds 过期秒数,默认 7 天。
|
||||
* @param token 指定 token(可选,默认随机生成)。
|
||||
* @return 登录 token。
|
||||
*/
|
||||
public func login(loginItem: SimApiLoginItem, expireSeconds!: Int64 = 604800, token!: String = ""): String {
|
||||
let newToken = if (token.isEmpty()) { generateToken() } else { token }
|
||||
let tokenKey = "${tokenCachePrefix}${newToken}"
|
||||
let setKey = "${tokenSetCachePrefix}${loginItem._id}"
|
||||
let json = loginItemJson(loginItem)
|
||||
|
||||
if (let Some(redis) <- _redis) {
|
||||
redis.set(tokenKey, Blob.fromUtf8(json), ex: Some(expireSeconds))
|
||||
redis.sadd(setKey, [Blob.fromUtf8(newToken)])
|
||||
redis.expire(setKey, expireSeconds)
|
||||
} else {
|
||||
_tokenStore[newToken] = json
|
||||
var tokens = _userTokens.get(loginItem._id)
|
||||
if (tokens == None) {
|
||||
tokens = HashSet<String>()
|
||||
_userTokens[loginItem._id] = tokens.getOrThrow()
|
||||
}
|
||||
tokens.getOrThrow().add(newToken)
|
||||
}
|
||||
return newToken
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新登录信息(token 不变)。
|
||||
* @param loginItem 新的登录信息。
|
||||
* @param token 已有 token。
|
||||
* @return 原 token。
|
||||
*/
|
||||
public func update(loginItem: SimApiLoginItem, token: String): String {
|
||||
let tokenKey = "${tokenCachePrefix}${token}"
|
||||
let json = loginItemJson(loginItem)
|
||||
if (let Some(redis) <- _redis) {
|
||||
redis.set(tokenKey, Blob.fromUtf8(json))
|
||||
} else {
|
||||
_tokenStore[token] = json
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录信息。
|
||||
* @param token 登录 token。
|
||||
* @return 登录信息;token 无效返回 None。
|
||||
*/
|
||||
public func getLogin(token: String): ?SimApiLoginItem {
|
||||
let tokenKey = "${tokenCachePrefix}${token}"
|
||||
let json: ?String
|
||||
if (let Some(redis) <- _redis) {
|
||||
json = match (redis.get(tokenKey)) {
|
||||
case Some(blob) => Some(blob.toUtf8())
|
||||
case None => None
|
||||
}
|
||||
} else {
|
||||
json = _tokenStore.get(token)
|
||||
}
|
||||
return match (json) {
|
||||
case Some(j) => Some(parseLoginItem(j))
|
||||
case None => None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某用户全部登录信息。
|
||||
* @param userId 用户 ID。
|
||||
* @return 登录信息数组。
|
||||
*/
|
||||
public func getAllLogins(userId: String): Array<SimApiLoginItem> {
|
||||
let tokens = getTokensOfUser(userId)
|
||||
var result = ArrayList<SimApiLoginItem>()
|
||||
for (token in tokens) {
|
||||
if (let Some(item) <- getLogin(token)) {
|
||||
result.add(item)
|
||||
} else {
|
||||
removeTokenOfUser(userId, token)
|
||||
}
|
||||
}
|
||||
return result.toArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录。
|
||||
* @param token 登录 token。
|
||||
*/
|
||||
public func logout(token: String): Unit {
|
||||
let item = getLogin(token)
|
||||
if (let Some(item) <- item) {
|
||||
removeTokenOfUser(item._id, token)
|
||||
}
|
||||
let tokenKey = "${tokenCachePrefix}${token}"
|
||||
if (let Some(redis) <- _redis) {
|
||||
redis.del([tokenKey])
|
||||
} else {
|
||||
_tokenStore.remove(token)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出某用户全部登录。
|
||||
* @param userId 用户 ID。
|
||||
*/
|
||||
public func logoutAll(userId: String): Unit {
|
||||
let tokens = getTokensOfUser(userId)
|
||||
if (let Some(redis) <- _redis) {
|
||||
for (token in tokens) {
|
||||
redis.del(["${tokenCachePrefix}${token}"])
|
||||
}
|
||||
redis.del(["${tokenSetCachePrefix}${userId}"])
|
||||
} else {
|
||||
_userTokens.remove(userId)
|
||||
for (token in tokens) {
|
||||
_tokenStore.remove(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getTokensOfUser(userId: String): ArrayList<String> {
|
||||
var result = ArrayList<String>()
|
||||
if (let Some(redis) <- _redis) {
|
||||
let members = redis.smembers("${tokenSetCachePrefix}${userId}")
|
||||
for (m in members) {
|
||||
result.add(m.toUtf8())
|
||||
}
|
||||
} else {
|
||||
if (let Some(tokens) <- _userTokens.get(userId)) {
|
||||
for (t in tokens) {
|
||||
result.add(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func removeTokenOfUser(userId: String, token: String): Unit {
|
||||
if (let Some(redis) <- _redis) {
|
||||
redis.srem("${tokenSetCachePrefix}${userId}", [Blob.fromUtf8(token)])
|
||||
} else {
|
||||
if (let Some(tokens) <- _userTokens.get(userId)) {
|
||||
tokens.remove(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func generateToken(): String {
|
||||
// 对齐 C#:token = Guid.NewGuid().ToString()(小写、8-4-4-4-12)
|
||||
SimApiUtil.newGuid()
|
||||
}
|
||||
|
||||
private static func parseRedisConfig(config: String): (String, UInt16) {
|
||||
let parts = config.split(":")
|
||||
if (parts.size == 2) {
|
||||
return (parts[0], UInt16.parse(parts[1]))
|
||||
}
|
||||
return (config, 6379u16)
|
||||
}
|
||||
|
||||
private static func loginItemJson(item: SimApiLoginItem): String {
|
||||
// 统一 JSON 序列化:对齐 .NET JsonSerializer.Serialize(item)
|
||||
JsonSerializer.serializeObject<SimApiLoginItem>(item)
|
||||
}
|
||||
|
||||
private static func parseLoginItem(json: String): SimApiLoginItem {
|
||||
// 统一 JSON 反序列化:对齐 .NET JsonSerializer.Deserialize<SimApiLoginItem>(json)
|
||||
JsonSerializer.deserializeObject<SimApiLoginItem>(json)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.helpers
|
||||
|
||||
import std.collection.*
|
||||
import std.collection.concurrent.*
|
||||
import std.convert.*
|
||||
import redis.client.*
|
||||
import simapi.configurations.*
|
||||
import simapi.exceptions.*
|
||||
|
||||
/**
|
||||
* 缓存助手:Key 自动加前缀 "SimApi:Cache:"。
|
||||
* 存储后端与 SimApiAuth 一致:配置了 Redis 用 Redis,否则 InMemory。
|
||||
*/
|
||||
public class SimApiCache {
|
||||
private static let prefix = "SimApi:Cache:"
|
||||
|
||||
private var _redis: ?RedisClient = None
|
||||
private let _store = ConcurrentHashMap<String, String>()
|
||||
|
||||
/**
|
||||
* 创建缓存(依赖注入 SimApiOptions)。
|
||||
* @param options SimApi 配置(redisConfiguration 非空时使用 Redis,否则 InMemory)。
|
||||
*/
|
||||
public init(options: SimApiOptions) {
|
||||
let redisConfiguration = options.redisConfiguration
|
||||
if (!redisConfiguration.isEmpty()) {
|
||||
let (host, port) = parseRedisConfig(redisConfiguration)
|
||||
_redis = Some(RedisClient(host, port))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存。
|
||||
* @param key 缓存键。
|
||||
* @param value 缓存值(不能为 null)。
|
||||
* @param expireSeconds 过期秒数(可选)。
|
||||
*/
|
||||
public func set(key: String, value: String, expireSeconds!: Int64 = -1): Unit {
|
||||
if (let Some(redis) <- _redis) {
|
||||
if (expireSeconds > 0) {
|
||||
redis.set("${prefix}${key}", Blob.fromUtf8(value), ex: Some(expireSeconds))
|
||||
} else {
|
||||
redis.set("${prefix}${key}", Blob.fromUtf8(value))
|
||||
}
|
||||
} else {
|
||||
_store["${prefix}${key}"] = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除缓存。
|
||||
*/
|
||||
public func remove(key: String): Unit {
|
||||
if (let Some(redis) <- _redis) {
|
||||
redis.del(["${prefix}${key}"])
|
||||
} else {
|
||||
_store.remove("${prefix}${key}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存 Key 是否存在。
|
||||
*/
|
||||
public func hasKey(key: String): Bool {
|
||||
get(key) != None
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 string 类型缓存。
|
||||
*/
|
||||
public func get(key: String): ?String {
|
||||
if (let Some(redis) <- _redis) {
|
||||
return match (redis.get("${prefix}${key}")) {
|
||||
case Some(blob) => Some(blob.toUtf8())
|
||||
case None => None
|
||||
}
|
||||
}
|
||||
return _store.get("${prefix}${key}")
|
||||
}
|
||||
|
||||
private static func parseRedisConfig(config: String): (String, UInt16) {
|
||||
let parts = config.split(":")
|
||||
if (parts.size == 2) {
|
||||
return (parts[0], UInt16.parse(parts[1]))
|
||||
}
|
||||
return (config, 6379u16)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* Helpers/SimApiControllerScanner:自动发现调用者包中的 MVC 控制器。
|
||||
*
|
||||
* 对齐 C# 的控制器发现机制:
|
||||
* - C# 通过 StackTrace 获取调用程序集,再 Assembly.GetTypes() 扫描所有类型
|
||||
* - 仓颉版通过 Error.getStackTrace() 获取调用者包名,再 PackageInfo 枚举类型,
|
||||
* 过滤出继承 Controller 的类型(含子包)
|
||||
*/
|
||||
|
||||
package simapi.helpers
|
||||
|
||||
import std.collection.*
|
||||
import std.core.*
|
||||
import std.reflect.*
|
||||
import soulsoft_web_mvc.core.*
|
||||
|
||||
/**
|
||||
* 控制器自动扫描器:从调用栈定位调用者包,枚举该包(含子包)中继承 Controller 的类型。
|
||||
*/
|
||||
public class SimApiControllerScanner {
|
||||
private init() {}
|
||||
|
||||
/**
|
||||
* 扫描调用者包及其所有子包中的控制器类型。
|
||||
* @return 找到的控制器类型列表(不含抽象类型与 Controller 基类本身)。
|
||||
*/
|
||||
public static func scan(): Array<TypeInfo> {
|
||||
let callerPackage = getCallerPackage()
|
||||
var result = ArrayList<TypeInfo>()
|
||||
collectControllers(callerPackage, result)
|
||||
result.toArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用者(应用)包名:遍历栈帧,跳过 simapi/soulsoft/std 等框架包,
|
||||
* 返回第一个应用包的 declaringClass(对齐 C# 通过 StackTrace 找调用程序集)。
|
||||
*/
|
||||
public static func getCallerPackage(): String {
|
||||
try {
|
||||
throw Exception("probe")
|
||||
} catch (ex: Exception) {
|
||||
let st = ex.getStackTrace()
|
||||
for (el in st) {
|
||||
let name = el.declaringClass
|
||||
// 跳过本类及框架包
|
||||
if (name.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
if (name.startsWith("simapi.") || name == "simapi") {
|
||||
continue
|
||||
}
|
||||
if (name.startsWith("soulsoft_") || name.startsWith("std.") || name.startsWith("stdx.")) {
|
||||
continue
|
||||
}
|
||||
return name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集指定包及其子包中的控制器类型。
|
||||
*/
|
||||
private static func collectControllers(packageName: String, result: ArrayList<TypeInfo>): Unit {
|
||||
if (packageName.isEmpty()) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
let info = PackageInfo.get(packageName)
|
||||
for (ti in info.typeInfos) {
|
||||
if (isController(ti)) {
|
||||
result.add(ti)
|
||||
}
|
||||
}
|
||||
// 递归扫描子包(subPackages 的 name 是短名,需拼接全限定名)
|
||||
for (sub in info.subPackages) {
|
||||
collectControllers("${packageName}.${sub.name}", result)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// 包不存在时跳过
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断类型是否为控制器:
|
||||
* - 继承 Controller(soulsoft_web_mvc.core.Controller)
|
||||
* - 非抽象类
|
||||
* - 不是 Controller 基类本身
|
||||
*/
|
||||
private static func isController(typeInfo: TypeInfo): Bool {
|
||||
if (let classTypeInfo: ClassTypeInfo <- typeInfo) {
|
||||
if (classTypeInfo.isAbstract()) {
|
||||
return false
|
||||
}
|
||||
if (typeInfo == TypeInfo.of<Controller>()) {
|
||||
return false
|
||||
}
|
||||
return typeInfo.isSubtypeOf(TypeInfo.of<Controller>())
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.helpers
|
||||
|
||||
import simapi.exceptions.*
|
||||
|
||||
/**
|
||||
* 错误抛出辅助类:所有业务错误统一通过这里抛出 SimApiException。
|
||||
* 对应 C# 的 SimApi.Helpers.SimApiError。
|
||||
*/
|
||||
public class SimApiError {
|
||||
private init() {}
|
||||
|
||||
/**
|
||||
* 直接抛错。
|
||||
* @param code 错误代码,默认 500。
|
||||
* @param message 错误描述,默认空(由 code 自动带取描述)。
|
||||
*/
|
||||
public static func error(code!: Int64 = 500, message!: String = ""): Unit {
|
||||
throw SimApiException(code, message: message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件为 true 时抛错。
|
||||
* @param condition 检测条件。
|
||||
* @param code 错误代码,默认 400。
|
||||
* @param message 错误描述。
|
||||
*/
|
||||
public static func errorWhen(condition: Bool, code!: Int64 = 400, message!: String = ""): Unit {
|
||||
if (condition) {
|
||||
error(code: code, message: message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件为 true 时抛错(别名)。
|
||||
*/
|
||||
public static func errorWhenTrue(condition: Bool, code!: Int64 = 400, message!: String = ""): Unit {
|
||||
errorWhen(condition, code: code, message: message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件为 false 时抛错。
|
||||
*/
|
||||
public static func errorWhenFalse(condition: Bool, code!: Int64 = 400, message!: String = ""): Unit {
|
||||
errorWhen(!condition, code: code, message: message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定的可选值为 None 时抛错。
|
||||
* @param condition 检测的可选值。
|
||||
* @param code 错误代码,默认 404。
|
||||
* @param message 错误描述。
|
||||
*/
|
||||
public static func errorWhenNone(condition: ?Any, code!: Int64 = 404, message!: String = ""): Unit {
|
||||
match (condition) {
|
||||
case None => error(code: code, message: message)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.helpers
|
||||
|
||||
import std.collection.*
|
||||
import std.io.*
|
||||
import stdx.net.tls.*
|
||||
import stdx.net.tls.common.*
|
||||
import soulsoft_net_http.{HttpClient, HttpRequestMessage, JsonContent}
|
||||
import soulsoft_net_http.{HttpMethod as NetHttpMethod}
|
||||
import soulsoft_serialization.*
|
||||
import simapi.communications.*
|
||||
import simapi.configurations.*
|
||||
import simapi.exceptions.*
|
||||
|
||||
/**
|
||||
* HTTP 客户端:用于调用其他带签名/AES 的 SimApi 服务。
|
||||
* 对齐 C# 的 SimApi.Helpers.SimApiHttpClient:
|
||||
* - 内部使用 soulsoft_net_http 的 HttpClient(等价 .NET 的 System.Net.Http.HttpClient)
|
||||
* - 返回泛型 T(反序列化响应 body 的 data 字段),不再返回 String
|
||||
* @param T 响应 data 的数据类型(需实现 ISerialization<T>,如 SimApiLoginItem、String、Int64 等)。
|
||||
*/
|
||||
public class SimApiHttpClient {
|
||||
public var server: String
|
||||
public var appId: String
|
||||
public var appKey: String
|
||||
public var signName: String = "sign"
|
||||
public var timestampName: String = "timestamp"
|
||||
public var nonceName: String = "nonce"
|
||||
public var appIdName: ?String = Some("appId")
|
||||
public var signFields: Array<String> = []
|
||||
|
||||
public init(options!: SimApiOptions = SimApiOptions()) {
|
||||
let httpOptions = options.simApiHttpClientOptions
|
||||
server = httpOptions.server
|
||||
appId = httpOptions.appId
|
||||
appKey = httpOptions.appKey
|
||||
signName = httpOptions.signName
|
||||
timestampName = httpOptions.timestampName
|
||||
nonceName = httpOptions.nonceName
|
||||
appIdName = httpOptions.appIdName
|
||||
signFields = httpOptions.signFields
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起签名请求(GET query 签名 + POST body)。
|
||||
* 对齐 C# SignQuery<T>:query 串 = SignFields + AppId + timestamp + nonce,整体拼 AppKey 取 MD5 作为 sign。
|
||||
* @param url 请求路径(相对路径,自动拼接 server)。
|
||||
* @param body 请求体 JSON 字符串(可选)。
|
||||
* @param queries 额外查询参数(可选)。
|
||||
* @return 响应 data 字段反序列化后的 T。
|
||||
*/
|
||||
public func signQuery<T>(url: String, body!: String = "", queries!: HashMap<String, String> = HashMap<String, String>()): T where T <: ISerialization<T> {
|
||||
var queryUrl = StringBuilder()
|
||||
for (field in signFields) {
|
||||
queryUrl.append("${field}=")
|
||||
if (let Some(v) <- queries.get(field)) {
|
||||
queryUrl.append(v)
|
||||
}
|
||||
queryUrl.append("&")
|
||||
}
|
||||
if (let Some(name) <- appIdName) {
|
||||
queryUrl.append("${name}=${appId}&")
|
||||
}
|
||||
let timestamp = Int64(SimApiUtil.timestampNow)
|
||||
let nonce = generateNonce()
|
||||
queryUrl.append("${timestampName}=${timestamp}&${nonceName}=${nonce}")
|
||||
let signStr = "${queryUrl.toString()}&${appKey}"
|
||||
var path = "${server}${url}?${queryUrl.toString()}&${signName}=${SimApiUtil.md5(signStr)}"
|
||||
for ((k, v) in queries) {
|
||||
if (!signFields.contains(k)) {
|
||||
path = "${path}&${k}=${v}"
|
||||
}
|
||||
}
|
||||
return query<T>(path, body)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起 AES 加密请求:body 加密后放入 {"data": "..."} 提交。
|
||||
* 对齐 C# AesQuery<T>(SimApiOneFieldRequest<string> { Data = Encrypt(body, AppKey) })。
|
||||
* @param url 请求路径(相对路径,自动拼接 server)。
|
||||
* @param body 请求体 JSON 字符串。
|
||||
* @return 响应 data 字段反序列化后的 T。
|
||||
*/
|
||||
public func aesQuery<T>(url: String, body: String): T where T <: ISerialization<T> {
|
||||
var target = "${server}${url}"
|
||||
if (let Some(name) <- appIdName) {
|
||||
target = "${target}?${name}=${appId}"
|
||||
}
|
||||
let encrypted = aesEncrypt(body)
|
||||
let req = "{\"data\":\"${encrypted}\"}"
|
||||
return query<T>(target, req)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起 AES 加密 + 签名请求。
|
||||
* 对齐 C# AesSignQuery<T>。
|
||||
* @param url 请求路径(相对路径,自动拼接 server)。
|
||||
* @param body 请求体 JSON 字符串。
|
||||
* @param queries 额外查询参数(可选)。
|
||||
* @return 响应 data 字段反序列化后的 T。
|
||||
*/
|
||||
public func aesSignQuery<T>(url: String, body: String, queries!: HashMap<String, String> = HashMap<String, String>()): T where T <: ISerialization<T> {
|
||||
let encrypted = aesEncrypt(body)
|
||||
let req = "{\"data\":\"${encrypted}\"}"
|
||||
return signQuery<T>(url, body: req, queries: queries)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起 POST 请求并反序列化 SimApiBaseResponse<T>,返回 data 字段。
|
||||
* 对齐 C# Query<T>:
|
||||
* ErrorWhenFalse(IsSuccessStatusCode) → ReadFromJsonAsync<SimApiBaseResponse<T>> → ErrorWhen(Code != 200) → return Data。
|
||||
* 注意:必须 noProxy(),否则会走系统代理(192.168.0.250:8118)导致连接被拒。
|
||||
*/
|
||||
private func query<T>(url: String, body: String): T where T <: ISerialization<T> {
|
||||
let client = HttpClient.create { builder =>
|
||||
builder.noProxy()
|
||||
// 支持 https:配置 TLS(信任所有证书 + SNI 域名)
|
||||
var tls = TlsClientConfig()
|
||||
tls.verifyMode = CertificateVerifyMode.TrustAll
|
||||
let host = extractHost(url)
|
||||
if (!host.isEmpty()) {
|
||||
tls.serverName = Some(host)
|
||||
}
|
||||
builder.tlsConfig(tls)
|
||||
}
|
||||
try {
|
||||
let request = HttpRequestMessage(NetHttpMethod.Post, url)
|
||||
request.content = JsonContent.create(body)
|
||||
let response = client.send(request)
|
||||
try {
|
||||
SimApiError.errorWhenFalse(response.isSuccessStatusCode, code: response.statusCode, message: "HTTP ERROR: ${response.statusCode}")
|
||||
let result = response.content.readFromJson<SimApiResponse<T>>()
|
||||
SimApiError.errorWhen(result._code != 200, code: result._code, message: result._message)
|
||||
return result._data.getOrThrow()
|
||||
} finally {
|
||||
response.close()
|
||||
}
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
private func aesEncrypt(plain: String): String {
|
||||
// 对齐 C#:SimApiAesUtil.Encrypt(plain, AppKey)(AES-256-CBC + PKCS7,Base64(IV + 密文))
|
||||
SimApiAesUtil.encrypt(plain, appKey)
|
||||
}
|
||||
|
||||
private static func generateNonce(): String {
|
||||
// 对齐 C#:nonce 直接用 Guid.NewGuid()
|
||||
SimApiUtil.newGuid()
|
||||
}
|
||||
|
||||
/// 从完整 URL 提取 host(https://host[:port]/path → host)。
|
||||
private static func extractHost(fullUrl: String): String {
|
||||
match (fullUrl.indexOf("://")) {
|
||||
case Some(i) =>
|
||||
let rest = fullUrl[i + 3..]
|
||||
let slash = rest.indexOf("/") ?? rest.size
|
||||
let q = rest.indexOf("?") ?? rest.size
|
||||
let end = if (slash < q) { slash } else { q }
|
||||
return rest[0..end]
|
||||
case None => return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.helpers
|
||||
|
||||
import soulsoft_web_http.*
|
||||
import simapi.communications.*
|
||||
|
||||
/**
|
||||
* 响应封装:对写操作(Unit)返回统一成功响应,对已有 SimApiBaseResponse 透传。
|
||||
* 在仓颉版中以中间件形式实现,对应 C# 的 SimApiResponseFilter。
|
||||
*/
|
||||
public class SimApiResponseFilter {
|
||||
public init() {}
|
||||
|
||||
/**
|
||||
* 包装响应委托:捕获下一级写入的响应内容。
|
||||
* 说明:仓颉版约定各路由处理器直接返回 SimApiBaseResponse,
|
||||
* 由 SimApiExtensions 统一写入,本类保留供扩展使用。
|
||||
*/
|
||||
public func wrap(next: RequestDelegate): RequestDelegate {
|
||||
return {
|
||||
context =>
|
||||
next(context)
|
||||
if (!context.response.hasStarted) {
|
||||
context.response.writeAsJson(SimApiBaseResponse())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.helpers
|
||||
|
||||
import std.collection.*
|
||||
import std.time.*
|
||||
import std.random.*
|
||||
import stdx.crypto.digest.*
|
||||
import stdx.encoding.hex.*
|
||||
import stdx.encoding.base64.*
|
||||
import std.regex.*
|
||||
import simapi.communications.*
|
||||
import simapi.macros.*
|
||||
|
||||
/**
|
||||
* 工具类:对应 C# 的 SimApi.Helpers.SimApiUtil。
|
||||
* 提供时间、哈希、Base64、JSON、校验等常用能力。
|
||||
*/
|
||||
public class SimApiUtil {
|
||||
private init() {}
|
||||
|
||||
/**
|
||||
* 当前 CST 时间(UTC+8)。
|
||||
*/
|
||||
public static prop cstNow: DateTime {
|
||||
get() {
|
||||
DateTime.nowUTC().addHours(8)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前秒级 Unix 时间戳(Double)。
|
||||
*/
|
||||
public static prop timestampNow: Float64 {
|
||||
get() {
|
||||
let ts = DateTime.nowUTC().toUnixTimeStamp()
|
||||
Float64(ts / Duration.second)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SimApi 包版本号(编译期从 simapi-cj/cjpm.toml 读取。
|
||||
* 宏在调用方项目编译时展开,cwd 为应用根,故用 ../simapi-cj 相对路径)。
|
||||
*/
|
||||
@ReadTomlVersion[path: "../simapi-cj/cjpm.toml"]
|
||||
public static let simApiVersion: String = ""
|
||||
|
||||
/**
|
||||
* 应用版本号(编译期从调用方项目 cjpm.toml 读取)。
|
||||
*/
|
||||
@ReadTomlVersion[path: "cjpm.toml"]
|
||||
public static let appVersion: String = ""
|
||||
|
||||
/**
|
||||
* MD5 加密字符串。
|
||||
* @param source 源字符串。
|
||||
* @return 32 位十六进制小写。
|
||||
*/
|
||||
public static func md5(source: String): String {
|
||||
let md = MD5()
|
||||
md.write(source.toArray())
|
||||
toHexString(md.finish())
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA1 加密字符串。
|
||||
* @param source 源字符串。
|
||||
* @return 40 位十六进制小写。
|
||||
*/
|
||||
public static func sha1(source: String): String {
|
||||
let sha = SHA1()
|
||||
sha.write(source.toArray())
|
||||
toHexString(sha.finish())
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA256 加密字符串。
|
||||
* @param source 源字符串。
|
||||
* @return 64 位十六进制小写。
|
||||
*/
|
||||
public static func sha256(source: String): String {
|
||||
let sha = SHA256()
|
||||
sha.write(source.toArray())
|
||||
toHexString(sha.finish())
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串 Base64 编码。
|
||||
*/
|
||||
public static func base64Encode(str: String): String {
|
||||
toBase64String(str.toArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Base64 解码字符串。
|
||||
*/
|
||||
public static func base64Decode(base64Str: String): String {
|
||||
let decoded = fromBase64String(base64Str).getOrThrow { Exception("Base64 解码失败") }
|
||||
String.fromUtf8(decoded)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测手机号是否正确(中国大陆 11 位手机号)。
|
||||
*/
|
||||
public static func checkCell(cell: String): Bool {
|
||||
Regex("^1[3456789]\\d{9}$").matches(cell)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是 Email 地址(简化校验)。
|
||||
*/
|
||||
public static func checkEmail(email: String): Bool {
|
||||
Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$").matches(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 UUID v4 字符串(对齐 C# Guid.NewGuid().ToString():小写、8-4-4-4-12 连字符格式)。
|
||||
* 仓颉标准库没有 GUID 生成器(stdx 的 GUID 是 stdx.net.http 内部类型),
|
||||
* 此处用随机数自行构造:16 字节随机数 + 版本位(4)+ 变体位(10)。
|
||||
*/
|
||||
public static func newGuid(): String {
|
||||
let rnd = Random()
|
||||
var bytes = Array<UInt8>(16, repeat: 0u8)
|
||||
let h = rnd.nextUInt64()
|
||||
let l = rnd.nextUInt64()
|
||||
for (i in 0..8) {
|
||||
bytes[i] = UInt8((h >> UInt64(i * 8)) & 0xFFu64)
|
||||
bytes[8 + i] = UInt8((l >> UInt64(i * 8)) & 0xFFu64)
|
||||
}
|
||||
// UUID v4:版本位(第 7 字节高 4 位 = 4),变体位(第 9 字节高 2 位 = 10)
|
||||
bytes[6] = (bytes[6] & 0x0Fu8) | 0x40u8
|
||||
bytes[8] = (bytes[8] & 0x3Fu8) | 0x80u8
|
||||
let hex = "0123456789abcdef"
|
||||
var sb = StringBuilder()
|
||||
for (i in 0..16) {
|
||||
if (i == 4 || i == 6 || i == 8 || i == 10) {
|
||||
sb.append("-")
|
||||
}
|
||||
let b = bytes[i]
|
||||
// 注意:String 索引返回 UInt8(字节),必须转 Rune 再 append,否则输出十进制 ASCII 码
|
||||
sb.append(Rune(UInt32(hex[Int64((b >> 4u8) & 0x0Fu8)])))
|
||||
sb.append(Rune(UInt32(hex[Int64(b & 0x0Fu8)])))
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象序列化为 JSON 字符串(委托给 SimApiJson.json 统一实现,对齐 C# SimApiUtil.Json)。
|
||||
*/
|
||||
public static func json(obj: ?Any): String {
|
||||
SimApiJson.json(obj)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.interfaces
|
||||
|
||||
import simapi.communications.*
|
||||
|
||||
/**
|
||||
* 认证后处理 Hook:实现后每次认证成功都会调用。
|
||||
* 对应 C# 的 SimApi.Interfaces.ISimApiAuthChecker。
|
||||
*/
|
||||
public interface ISimApiAuthChecker {
|
||||
/**
|
||||
* 认证成功后执行。
|
||||
* @param loginItem 登录信息。
|
||||
* @param token 登录 token。
|
||||
*/
|
||||
func run(loginItem: SimApiLoginItem, token: String): Unit
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.logger
|
||||
|
||||
import std.collection.concurrent.*
|
||||
import std.env.*
|
||||
import std.time.*
|
||||
import soulsoft_extensions_logging.*
|
||||
|
||||
/**
|
||||
* SimApi 日志记录器:输出格式对齐 C# 原版 SimApiLogger。
|
||||
* 格式:`[ 分类 ][ 时间 ][ 级别 ]\n消息\n[异常]`
|
||||
* 按日志级别着色输出。
|
||||
*/
|
||||
public class SimApiLogger <: ILogger {
|
||||
private let _name: String
|
||||
|
||||
public init(name: String) {
|
||||
_name = name
|
||||
}
|
||||
|
||||
public func log<TState>(logLevel: LogLevel, eventId: EventId, state: TState, exception: ?Exception,
|
||||
formatter: (TState, ?Exception) -> String): Unit where TState <: ToString {
|
||||
if (!isEnabled(logLevel)) {
|
||||
return
|
||||
}
|
||||
|
||||
let now = DateTime.now()
|
||||
let timeStr = formatTime(now)
|
||||
let levelStr = levelName(logLevel)
|
||||
|
||||
var sb = StringBuilder()
|
||||
sb.append("${color(levelColor(logLevel))}[ ${_name} ][ ${timeStr} ][ ${levelStr} ]\n")
|
||||
sb.append("${state}\n")
|
||||
if (let Some(ex) <- exception) {
|
||||
sb.append("${ex}\n")
|
||||
}
|
||||
sb.append(resetColor())
|
||||
// 对齐 C# Console.WriteLine:message 末尾 \n 之后再补一个 \n,形成空行分隔
|
||||
sb.append("\n")
|
||||
|
||||
let writer = getStdOut()
|
||||
writer.write(sb.toString())
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
public func isEnabled(logLevel: LogLevel): Bool {
|
||||
logLevel != LogLevel.Off
|
||||
}
|
||||
|
||||
/**
|
||||
* 级别名称,对齐 C# 的 LogLevel.ToString()。
|
||||
*/
|
||||
private static func levelName(logLevel: LogLevel): String {
|
||||
match (logLevel) {
|
||||
case LogLevel.Trace => "Trace"
|
||||
case LogLevel.Debug => "Debug"
|
||||
case LogLevel.Info => "Information"
|
||||
case LogLevel.Warn => "Warning"
|
||||
case LogLevel.Error => "Error"
|
||||
case LogLevel.Fatal => "Critical"
|
||||
case _ => "None"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 级别对应 ANSI 前景色(对齐 C# ConsoleColor):
|
||||
* Debug→DarkMagenta, Info→DarkCyan, Warn→Yellow, Error→Red, Fatal→DarkRed, 其他→White
|
||||
*/
|
||||
private static func levelColor(logLevel: LogLevel): String {
|
||||
match (logLevel) {
|
||||
case LogLevel.Debug => "35" // DarkMagenta
|
||||
case LogLevel.Info => "36" // DarkCyan
|
||||
case LogLevel.Warn => "33" // Yellow
|
||||
case LogLevel.Error => "31" // Red
|
||||
case LogLevel.Fatal => "31;1" // DarkRed(粗体近似)
|
||||
case _ => "37" // White
|
||||
}
|
||||
}
|
||||
|
||||
private static func color(code: String): String {
|
||||
"\u{1b}[${code}m"
|
||||
}
|
||||
|
||||
private static func resetColor(): String {
|
||||
"\u{1b}[0m"
|
||||
}
|
||||
|
||||
private static func formatTime(dt: DateTime): String {
|
||||
let year = dt.year
|
||||
let month = pad2(dt.monthValue)
|
||||
let day = pad2(dt.dayOfMonth)
|
||||
let hour = pad2(dt.hour)
|
||||
let minute = pad2(dt.minute)
|
||||
let second = pad2(dt.second)
|
||||
let millis = pad3(dt.nanosecond / 1000000)
|
||||
"${year}-${month}-${day} ${hour}:${minute}:${second}:${millis}"
|
||||
}
|
||||
|
||||
private static func pad2(v: Int64): String {
|
||||
if (v < 10) {
|
||||
return "0${v}"
|
||||
}
|
||||
"${v}"
|
||||
}
|
||||
|
||||
private static func pad3(v: Int64): String {
|
||||
if (v < 10) {
|
||||
return "00${v}"
|
||||
}
|
||||
if (v < 100) {
|
||||
return "0${v}"
|
||||
}
|
||||
"${v}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SimApi 日志提供程序:按分类缓存日志记录器。
|
||||
*/
|
||||
public class SimApiLoggerProvider <: ILoggerProvider {
|
||||
private let _loggers = ConcurrentHashMap<String, SimApiLogger>()
|
||||
|
||||
public init() {}
|
||||
|
||||
public func createLogger(categoryName: String): ILogger {
|
||||
if (let Some(logger) <- _loggers.get(categoryName)) {
|
||||
return logger
|
||||
}
|
||||
return _loggers.entryView(categoryName) {
|
||||
view => if (view.value.isNone()) {
|
||||
view.value = SimApiLogger(categoryName)
|
||||
}
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
public func close(): Unit {}
|
||||
|
||||
public func isClosed(): Bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
macro package simapi.macros
|
||||
|
||||
import std.ast.*
|
||||
import std.fs.*
|
||||
import std.collection.*
|
||||
|
||||
/**
|
||||
* 编译期宏:从指定 cjpm.toml 读取 version 字段并替换变量声明。
|
||||
*
|
||||
* 用法:
|
||||
* ```
|
||||
* @ReadTomlVersion[path: "cjpm.toml"] // 读取当前包(编译目录)的版本
|
||||
* let appVersion: String = ""
|
||||
* @ReadTomlVersion[path: "../simapi-cj/cjpm.toml"] // 读取父包版本
|
||||
* let simApiVersion: String = ""
|
||||
* ```
|
||||
*
|
||||
* 说明:宏在编译期执行,工作目录为 cjpm build 的运行目录(应用项目根)。
|
||||
* 版本号仅匹配 `version = "x.y.z"`(排除 cjc-version)。
|
||||
*/
|
||||
public macro ReadTomlVersion(attr: Tokens, input: Tokens): Tokens {
|
||||
var path = "cjpm.toml"
|
||||
try {
|
||||
let attrText = attr.toString()
|
||||
if (let Some(idx) <- attrText.indexOf("path")) {
|
||||
let rest = attrText[idx..]
|
||||
if (let Some(q1) <- rest.indexOf("\"")) {
|
||||
let after = rest[(q1 + 1)..]
|
||||
if (let Some(q2) <- after.indexOf("\"")) {
|
||||
path = after[0..q2]
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
var version = "0.0.0"
|
||||
try {
|
||||
let f = File(path, Read)
|
||||
var buf = Array<Byte>(8192, repeat: 0)
|
||||
var bytes = ArrayList<Byte>()
|
||||
var n = f.read(buf)
|
||||
while (n > 0) {
|
||||
for (i in 0..n) {
|
||||
bytes.add(buf[i])
|
||||
}
|
||||
n = f.read(buf)
|
||||
}
|
||||
f.close()
|
||||
// 按行解析(version 行是纯 ASCII;避免整体 UTF-8 解码含中文的 description 失败)
|
||||
let data = bytes.toArray()
|
||||
var lineStart = 0
|
||||
for (i in 0..data.size) {
|
||||
let isLast = (i == data.size - 1)
|
||||
if (data[i] == 10 || isLast) {
|
||||
let end = if (data[i] == 10) { i } else { i + 1 }
|
||||
let lineBytes = data[lineStart..end]
|
||||
let lineText = String.fromUtf8(lineBytes)
|
||||
if (!lineText.contains("cjc-version") && lineText.contains("version")) {
|
||||
let parts = lineText.split("\"")
|
||||
if (parts.size >= 2) {
|
||||
version = parts[1]
|
||||
}
|
||||
}
|
||||
lineStart = i + 1
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
version = "error:${ex.message}"
|
||||
}
|
||||
|
||||
// 生成替换声明:保留原声明的修饰符与变量名,只把值替换为版本号
|
||||
// declPrefix 例:"public static let" / "private let" / "let"
|
||||
var declPrefix = "let"
|
||||
var varName = "v"
|
||||
try {
|
||||
let inputText = input.toString()
|
||||
if (let Some(letIdx) <- inputText.indexOf("let")) {
|
||||
let prefix = inputText[0..letIdx].trimEnd()
|
||||
if (!prefix.isEmpty()) {
|
||||
declPrefix = "${prefix} let"
|
||||
}
|
||||
let rest = inputText[letIdx..]
|
||||
let afterLet = rest[3..].trimStart()
|
||||
if (let Some(nameEnd) <- afterLet.indexOf(":")) {
|
||||
varName = afterLet[0..nameEnd].trimEnd()
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
// 用 cangjieLex 把 "public static let name: String = "version"" 整体解析为 token 流
|
||||
let declText = "${declPrefix} ${varName}: String = \"${version}\""
|
||||
cangjieLex(declText)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.middlewares
|
||||
|
||||
import soulsoft_web_http.*
|
||||
import simapi.communications.*
|
||||
import simapi.helpers.*
|
||||
|
||||
/**
|
||||
* 认证信息获取中间件:从 Header Token 或 Query token 解析登录信息并注入上下文。
|
||||
* 对应 C# 的 SimApi.Middlewares.SimApiAuthMiddleware。
|
||||
*/
|
||||
public class SimApiAuthMiddleware <: IMiddleware {
|
||||
private let _auth: SimApiAuth
|
||||
|
||||
public init(auth: SimApiAuth) {
|
||||
_auth = auth
|
||||
}
|
||||
|
||||
/**
|
||||
* 中间件入口(IMiddleware 风格,对齐 C# UseMiddleware<T> 的 InvokeAsync)。
|
||||
*/
|
||||
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
|
||||
var token = context.request.headers.get("Token")
|
||||
if (token == None || token == Some("")) {
|
||||
token = context.request.query.get("token")
|
||||
}
|
||||
if (let Some(token) <- token) {
|
||||
if (!token.isEmpty()) {
|
||||
if (let Some(login) <- _auth.getLogin(token)) {
|
||||
context.items["LoginToken"] = token
|
||||
context.items["LoginInfo"] = login
|
||||
}
|
||||
}
|
||||
}
|
||||
next(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.middlewares
|
||||
|
||||
import soulsoft_web_http.*
|
||||
import soulsoft_extensions_logging.*
|
||||
import simapi.communications.*
|
||||
import simapi.exceptions.*
|
||||
import simapi.configurations.*
|
||||
|
||||
/**
|
||||
* 异常处理中间件:全异常捕获,统一输出 HTTP 200 + JSON 响应。
|
||||
* 对应 C# 的 SimApi.Middlewares.SimApiExceptionMiddleware。
|
||||
*/
|
||||
public class SimApiExceptionMiddleware <: IMiddleware {
|
||||
private let _options: SimApiOptions
|
||||
private let _logger: ILogger
|
||||
|
||||
public init(options: SimApiOptions, loggerFactory: ILoggerFactory) {
|
||||
_options = options
|
||||
_logger = loggerFactory.createLogger("SimApi.ExceptionMiddleware")
|
||||
}
|
||||
|
||||
/**
|
||||
* 中间件入口(IMiddleware 风格,对齐 C# UseMiddleware<T> 的 InvokeAsync)。
|
||||
*/
|
||||
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
|
||||
try {
|
||||
// 透传 Query-Id 请求头
|
||||
if (let Some(queryId) <- context.request.headers.get("Query-Id")) {
|
||||
context.response.headers.add("Query-Id", queryId)
|
||||
}
|
||||
next(context)
|
||||
// 若响应未开始且状态码不在跳过列表,则视为业务错误
|
||||
if (!context.response.hasStarted) {
|
||||
let code = Int64(context.response.statusCode)
|
||||
if (!_options.simApiExceptionOptions.skipStatusCodes.contains(code)) {
|
||||
throw SimApiException(code)
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
let response = buildResponse(ex, context)
|
||||
if (!context.response.hasStarted) {
|
||||
context.response.statusCode = 200
|
||||
context.response.contentType = "application/json; charset=utf-8"
|
||||
context.response.write(responseJson(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func buildResponse(ex: Exception, context: HttpContext): SimApiBaseResponse {
|
||||
if (let simEx: SimApiException <- ex) {
|
||||
let message = if (simEx.message.isEmpty()) {
|
||||
SimApiBaseResponse.getDefaultMessage(simEx.code)
|
||||
} else {
|
||||
simEx.message
|
||||
}
|
||||
var response = SimApiBaseResponse(simEx.code, message)
|
||||
if (context.response.statusCode == 404) {
|
||||
response.message = "接口不存在"
|
||||
}
|
||||
return response
|
||||
}
|
||||
_logger.error(EventId(0), ex, ex.message)
|
||||
return SimApiBaseResponse(500)
|
||||
}
|
||||
|
||||
private func responseJson(response: SimApiBaseResponse): String {
|
||||
response.toJsonString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi.middlewares
|
||||
|
||||
import std.collection.*
|
||||
import std.io.*
|
||||
import std.time.*
|
||||
import soulsoft_web_http.*
|
||||
import soulsoft_extensions_logging.*
|
||||
import simapi.communications.*
|
||||
import simapi.configurations.*
|
||||
|
||||
/**
|
||||
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码与耗时。
|
||||
* 对应 C# 的 SimApi.Middlewares.SimApiRequestLogMiddleware。
|
||||
*/
|
||||
public class SimApiRequestLogMiddleware <: IMiddleware {
|
||||
private let _options: SimApiOptions
|
||||
private let _logger: ILogger
|
||||
|
||||
public init(options: SimApiOptions, loggerFactory: ILoggerFactory) {
|
||||
_options = options
|
||||
_logger = loggerFactory.createLogger("SimApi.RequestLog")
|
||||
}
|
||||
|
||||
/**
|
||||
* 中间件入口(IMiddleware 风格,对齐 C# UseMiddleware<T> 的 InvokeAsync)。
|
||||
*/
|
||||
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
|
||||
let start = MonoTime.now()
|
||||
let fullUrl = context.request.getDisplayUrl()
|
||||
var sb = StringBuilder()
|
||||
|
||||
sb.append("[${context.request.method}] ${fullUrl}\n")
|
||||
|
||||
// 请求头
|
||||
if (_options.simApiRequestLogOptions.showFullHeader) {
|
||||
sb.append("*( RequestHeaders [Full] ) =>\n")
|
||||
sb.append(serializeHeaders(context))
|
||||
} else {
|
||||
sb.append("*( RequestHeaders ) =>\n")
|
||||
let token = context.request.headers.get("Token") ?? ""
|
||||
let queryId = context.request.headers.get("Query-Id") ?? ""
|
||||
sb.append("Token: ${token} QueryId: ${queryId}\n")
|
||||
}
|
||||
|
||||
// 请求体
|
||||
sb.append("*( RequestBody ) =>\n")
|
||||
sb.append(readRequestBody(context))
|
||||
|
||||
// 调用下一级
|
||||
next(context)
|
||||
|
||||
// 响应信息
|
||||
let elapsed = MonoTime.now() - start
|
||||
let elapsedMs = elapsed / Duration.millisecond
|
||||
sb.append("*( Response [${context.response.statusCode}] ) => ${elapsedMs}ms\n")
|
||||
|
||||
_logger.info(sb.toString())
|
||||
}
|
||||
|
||||
private func serializeHeaders(context: HttpContext): String {
|
||||
var sb = StringBuilder()
|
||||
sb.append("{")
|
||||
var first = true
|
||||
for ((name, values) in context.request.headers) {
|
||||
if (!first) { sb.append(",") }
|
||||
sb.append("\"${SimApiJson.escapeJson(name)}\":\"${SimApiJson.escapeJson(joinValues(values))}\"")
|
||||
first = false
|
||||
}
|
||||
sb.append("}\n")
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private func joinValues(values: Collection<String>): String {
|
||||
var sb = StringBuilder()
|
||||
var first = true
|
||||
for (v in values) {
|
||||
if (!first) { sb.append(",") }
|
||||
sb.append(v)
|
||||
first = false
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private func readRequestBody(context: HttpContext): String {
|
||||
try {
|
||||
context.request.enableBuffering()
|
||||
var buffer = Array<Byte>(4096, repeat: 0)
|
||||
var sb = StringBuilder()
|
||||
var read = context.request.body.read(buffer)
|
||||
while (read > 0) {
|
||||
sb.appendFromUtf8(buffer.slice(0, read))
|
||||
read = context.request.body.read(buffer)
|
||||
}
|
||||
let bodyText = sb.toString()
|
||||
// 重置流位置,供后续业务读取
|
||||
if (let seekable: Seekable <- context.request.body) {
|
||||
seekable.seek(SeekPosition.Begin(0))
|
||||
}
|
||||
return truncateBody(bodyText)
|
||||
} catch (_: Exception) {
|
||||
return "(读取请求体失败)\n"
|
||||
}
|
||||
}
|
||||
|
||||
private func truncateBody(body: String): String {
|
||||
if (_options.simApiRequestLogOptions.requestStringLogLength <= 0 ||
|
||||
body.size <= _options.simApiRequestLogOptions.requestStringLogLength) {
|
||||
return body + "\n"
|
||||
}
|
||||
// 简单按长度截断(不做 JSON 字段级截断,保持实现简洁)
|
||||
let chars = body.toArray()
|
||||
var sb = StringBuilder()
|
||||
for (i in 0.._options.simApiRequestLogOptions.requestStringLogLength) {
|
||||
sb.append(chars[i])
|
||||
}
|
||||
return "${sb.toString()}...(${body.size})\n"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simapi
|
||||
|
||||
import simapi.communications.*
|
||||
import simapi.configurations.*
|
||||
import simapi.exceptions.*
|
||||
import simapi.extensions.*
|
||||
import simapi.helpers.*
|
||||
import simapi.interfaces.*
|
||||
import simapi.macros.*
|
||||
import simapi.middlewares.*
|
||||
|
||||
/**
|
||||
* SimApi 仓颉版:ASP.NET Core 风格 API 基础框架。
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net)。
|
||||
*/
|
||||
public class SimApi {
|
||||
private init() {}
|
||||
|
||||
/**
|
||||
* 包版本号(编译期从 simapi-cj/cjpm.toml 读取)。
|
||||
*/
|
||||
@ReadTomlVersion[path: "../simapi-cj/cjpm.toml"]
|
||||
public static let version: String = ""
|
||||
}
|
||||
Reference in New Issue
Block a user