first version

This commit is contained in:
2026-08-16 12:46:15 +08:00
commit 932fce1b9e
40 changed files with 4005 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
target/
+326
View File
@@ -0,0 +1,326 @@
# SimApi for Cangjiesimapi
> 仓颉版 SimApiASP.NET Core 风格 API 基础框架,移植自 C# 项目 [SimApi](https://github.com/SimcuTeam/simapi-net)`E:\simcu\simapi-net`)。
提供**统一响应格式、异常拦截、Token 认证、缓存、工具集、HTTP 客户端**等 API 基础能力。
---
## 快速开始
```cangjie
package your_app
import soulsoft_web_http.*
import soulsoft_web_routing.*
import soulsoft_web_hosting.*
import soulsoft_extensions_logging.*
import soulsoft_extensions_injection.*
import simapi.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, SimApiAuthControllerMVC 写法)
│ ├── exceptions/ # SimApiException
│ ├── extensions/ # SimApiExtensionsaddSimApi / 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
+20
View File
@@ -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"}
+40
View File
@@ -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}" ]
+53
View File
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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
}
}
+160
View File
@@ -0,0 +1,160 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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)
}
/**
* 序列化为 DataModelcode + message + datadata 为对象)。
*/
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
}
}
+88
View File
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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)
}
/**
* 序列化为 DataModelcode + message + datadata 为对象)。
*/
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 + datadata 保留原始 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
}
}
+100
View File
@@ -0,0 +1,100 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* JSON 序列化统一工具(对应 C# SimApiUtil.Json)。
*/
package simapi.communications
import std.collection.*
/**
* JSON 序列化静态工具类。
*
* 说明:序列化核心放在依赖图最底层的 simapi.communications 包,
* SimApiUtil.jsonsimapi.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"
}
}
+167
View File
@@ -0,0 +1,167 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* Communications/SimApiLoginItem:登录信息项。
*/
package simapi.communications
import std.collection.*
import soulsoft_serialization.*
/**
* 登录信息项:Token 认证通过后注入请求上下文。
* 对齐 C# SimApiLoginItemId / Type / Meta / ExtracamelCase 输出 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# 项目 SimApiE:\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() {}
}
+89
View File
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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# 项目 SimApiE:\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# 项目 SimApiE:\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() {}
}
+65
View File
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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())
}
}
+190
View File
@@ -0,0 +1,190 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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# 项目 SimApiE:\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() {}
}
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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# 项目 SimApiE:\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# 项目 SimApiE:\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() {}
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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/logoutSimApiAuthController.Logout[HttpPost]
* 由 MapControllerRoute(pattern=LogoutRoute) 注册)。
*/
@HttpPost["/auth/logout"]
public func logout(): Unit {
if (let Some(token) <- request.headers.get("Token")) {
_auth.logout(token)
}
}
}
+79
View File
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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)
}
}
+91
View File
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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 默认值 /configSimApiCommonController.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
}
}
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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
}
}
+315
View File
@@ -0,0 +1,315 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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# 的 SimApiExtensionsAddSimApi + 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# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* 自定义 IRequestDelegateFactory:在结果派发时自动封装响应,
* 对齐 C# SimApiResponseFilterIResultFilter)的行为:
* - 返回 SimApiBaseResponse 或其子类 → 原样输出
* - 返回 null/voidUnit)→ 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
}
}
+442
View File
@@ -0,0 +1,442 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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-CBCPKCS7 填充
* - 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 *= 3GF(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) 乘以 2xtime
@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 wordsAES-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
}
}
+222
View File
@@ -0,0 +1,222 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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)
}
}
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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)
}
}
+104
View File
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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) {
// 包不存在时跳过
}
}
/**
* 判断类型是否为控制器:
* - 继承 Controllersoulsoft_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
}
}
+64
View File
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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 _ => ()
}
}
}
+169
View File
@@ -0,0 +1,169 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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 + PKCS7Base64(IV + 密文)
SimApiAesUtil.encrypt(plain, appKey)
}
private static func generateNonce(): String {
// 对齐 C#nonce 直接用 Guid.NewGuid()
SimApiUtil.newGuid()
}
/// 从完整 URL 提取 hosthttps://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 ""
}
}
}
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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())
}
}
}
}
+156
View File
@@ -0,0 +1,156 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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)
}
}
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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
}
+145
View File
@@ -0,0 +1,145 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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.WriteLinemessage 末尾 \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
}
}
+95
View File
@@ -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)
}
+41
View File
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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# 项目 SimApiE:\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# 项目 SimApiE:\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"
}
}
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\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# 项目 SimApiE:\simcu\simapi-net)。
*/
public class SimApi {
private init() {}
/**
* 包版本号(编译期从 simapi-cj/cjpm.toml 读取)。
*/
@ReadTomlVersion[path: "../simapi-cj/cjpm.toml"]
public static let version: String = ""
}