Compare commits
30
Commits
40e0726087
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
829f009229 | ||
|
|
57b781a316 | ||
|
|
b9ea4008c7 | ||
|
|
a1acb99e08 | ||
|
|
f88eee8d64 | ||
|
|
1ce85a68ab | ||
|
|
cf19f15569 | ||
|
|
3b252e08d5 | ||
|
|
8a5c34756b | ||
|
|
3925d0d5ad | ||
|
|
11360b3359 | ||
|
|
aa2af7aee1 | ||
|
|
5c30f201ad | ||
|
|
9c2e0ae0e3 | ||
|
|
79a997c014 | ||
|
|
c7099040fa | ||
|
|
ea31bd9b6a | ||
|
|
6e6b21ff83 | ||
|
|
ef9765de5d | ||
|
|
d76b08a542 | ||
|
|
e9978ea5e0 | ||
|
|
73f60f6f15 | ||
|
|
8b0a8f783c | ||
|
|
bcac7b1102 | ||
|
|
ff41d287ed | ||
|
|
2b8d2c47be | ||
|
|
67f7c16a3f | ||
|
|
b4e808e775 | ||
|
|
eb488bcd5a | ||
|
|
92eea3108a |
+2
-1
@@ -1 +1,2 @@
|
|||||||
target/
|
target/
|
||||||
|
*.cj.macrocall
|
||||||
@@ -2,7 +2,29 @@
|
|||||||
|
|
||||||
> 仓颉版 SimApi:ASP.NET Core 风格 API 基础框架,移植自 C# 项目 [SimApi](https://github.com/SimcuTeam/simapi-net)(`E:\simcu\simapi-net`)。
|
> 仓颉版 SimApi:ASP.NET Core 风格 API 基础框架,移植自 C# 项目 [SimApi](https://github.com/SimcuTeam/simapi-net)(`E:\simcu\simapi-net`)。
|
||||||
|
|
||||||
提供**统一响应格式、异常拦截、Token 认证、缓存、工具集、HTTP 客户端**等 API 基础能力。
|
提供**统一响应格式、异常拦截、Token 认证、缓存、工具集、HTTP 客户端、S3 存储、声明式注解**等 API 基础能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 引入
|
||||||
|
|
||||||
|
两种方式任选其一:
|
||||||
|
|
||||||
|
**方式一:中央仓**(需先 `cjpm publish` 发布 `simcu::simapi`)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
"simcu::simapi" = "1.0.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式二:Git 仓库**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
"simcu::simapi" = { git = "https://gitcode.com/simcu/simapi-cj.git", version = "1.0.3" }
|
||||||
|
```
|
||||||
|
|
||||||
|
> 本地开发也可用 path 依赖:`"simcu::simapi" = { path = "../simapi-cj" }`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -16,8 +38,9 @@ import soulsoft_web_routing.*
|
|||||||
import soulsoft_web_hosting.*
|
import soulsoft_web_hosting.*
|
||||||
import soulsoft_extensions_logging.*
|
import soulsoft_extensions_logging.*
|
||||||
import soulsoft_extensions_injection.*
|
import soulsoft_extensions_injection.*
|
||||||
import simapi.extensions.*
|
import simcu::simapi.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
main(args: Array<String>) {
|
main(args: Array<String>) {
|
||||||
let builder = WebHost.createBuilder(args)
|
let builder = WebHost.createBuilder(args)
|
||||||
@@ -25,19 +48,19 @@ main(args: Array<String>) {
|
|||||||
builder.services.addLogging()
|
builder.services.addLogging()
|
||||||
|
|
||||||
// 注册 SimApi 服务(与 addLogging 同样式)
|
// 注册 SimApi 服务(与 addLogging 同样式)
|
||||||
builder.addSimApi { options =>
|
SimApiExtensions.addSimApi(builder) { options =>
|
||||||
options.enableSimApiAuth = true // Token 认证(未配 Redis 自动用 InMemory)
|
options.enableSimApiAuth = true // Token 认证(未配 Redis 自动用 InMemory)
|
||||||
options.enableSimApiCache = true // 缓存
|
options.enableSimApiCache = true // 缓存
|
||||||
options.enableSimApiException = true // 全局异常拦截
|
options.enableSimApiException = true // 全局异常拦截
|
||||||
}
|
}
|
||||||
|
|
||||||
let host = builder.build()
|
let host = builder.build()
|
||||||
host.useSimApi()
|
SimApiExtensions.useSimApi(host)
|
||||||
|
|
||||||
// 业务接口:返回统一响应格式
|
// 业务接口:返回对象自动封装为统一响应格式({code, message, data})
|
||||||
host.mapGet("hello") {
|
host.mapGet("hello") {
|
||||||
context =>
|
context =>
|
||||||
context.response.write(SimApiBaseResponse().toJsonString(dataJson: "\"hello cangjie.\""))
|
context.response.write(SimApiUtil.json(Some(SimApiResponse<String>("hello cangjie."))))
|
||||||
}
|
}
|
||||||
|
|
||||||
host.run()
|
host.run()
|
||||||
@@ -58,6 +81,12 @@ main(args: Array<String>) {
|
|||||||
| 404 | 资源不存在 |
|
| 404 | 资源不存在 |
|
||||||
| 500 | 服务器错误 |
|
| 500 | 服务器错误 |
|
||||||
|
|
||||||
|
响应 JSON(经 simcu::serialization 反射序列化,字段**无下划线**):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "code": 200, "message": "成功", "data": { ... } }
|
||||||
|
```
|
||||||
|
|
||||||
### 异常处理流程
|
### 异常处理流程
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -74,16 +103,18 @@ main(args: Array<String>) {
|
|||||||
simapi-cj/
|
simapi-cj/
|
||||||
├── cjpm.toml # 包配置
|
├── cjpm.toml # 包配置
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── attributes/ # 声明式注解:@SimApiAuth(鉴权)、@OriginResponse(原样响应)
|
│ ├── SimApiExtensions.cj # 根包入口:SimApiExtensions 静态类(addSimApi / useSimApi + 内置路由 + 响应封装)
|
||||||
|
│ ├── annotations/ # 声明式注解:@SimApiAuth(鉴权)、@OriginResponse(原样响应)、
|
||||||
|
│ │ # @SimApiSign(验签)、@AesBody(AES body 解密)
|
||||||
│ ├── authsdk/ # 认证中心 SDK:SimApiAuthClient/Center/Iam + 网关中间件 + DTO
|
│ ├── authsdk/ # 认证中心 SDK:SimApiAuthClient/Center/Iam + 网关中间件 + DTO
|
||||||
│ ├── communications/ # SimApiBaseResponse, PageResponse, SimApiLoginItem, 请求 DTO
|
│ ├── communications/ # SimApiBaseResponse, PageResponse, SimApiLoginItem, 请求 DTO
|
||||||
│ ├── configurations/ # SimApiOptions + 各模块 Option(含 ConfigureSimApiXxx 回调)
|
│ ├── configurations/ # SimApiOptions + 各模块 Option(含 ConfigureSimApiXxx 回调)
|
||||||
│ ├── controllers/ # SimApiBaseController, SimApiCommonController, SimApiAuthController(MVC 写法)
|
│ ├── controllers/ # SimApiBaseController, SimApiCommonController, SimApiAuthController(MVC 写法)
|
||||||
│ ├── exceptions/ # SimApiException
|
│ ├── exceptions/ # SimApiException
|
||||||
│ ├── extensions/ # SimApiExtensions(addSimApi / useSimApi + 内置路由 + 响应封装)
|
|
||||||
│ ├── helpers/ # SimApiError, SimApiUtil, SimApiAuth, SimApiCache, SimApiHttpClient,
|
│ ├── helpers/ # SimApiError, SimApiUtil, SimApiAuth, SimApiCache, SimApiHttpClient,
|
||||||
│ │ # SimApiAesUtil(AES-256), SimApiSignChecker(验签), SimApiAesBodyChecker(AES body)
|
│ │ # SimApiAesUtil(AES-256), SimApiSignChecker(验签), SimApiAesBodyChecker(AES body),
|
||||||
│ ├── interfaces/ # ISimApiAuthChecker
|
│ │ # SimApiStorage(S3/MinIO, 自实现 SigV4), SimApiRequestDelegateFactory, SimApiResultWriter
|
||||||
|
│ ├── interfaces/ # ISimApiAuthChecker, IBindRequestContext, SimApiSignProviderBase, AesBodyProviderBase
|
||||||
│ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志)
|
│ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志)
|
||||||
│ ├── macros/ # ReadTomlVersion(编译期读版本号)
|
│ ├── macros/ # ReadTomlVersion(编译期读版本号)
|
||||||
│ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware
|
│ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware
|
||||||
@@ -97,19 +128,19 @@ simapi-cj/
|
|||||||
### 1. 错误处理 — SimApiError
|
### 1. 错误处理 — SimApiError
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
SimApiError.error(500, "服务器内部错误") // 直接抛错
|
SimApiError.error(500, "服务器内部错误") // 直接抛错
|
||||||
SimApiError.errorWhen(amount <= 0, 400, "金额无效") // 条件为 true 时抛错
|
SimApiError.errorWhen(amount <= 0, 400, "金额无效") // 条件为 true 时抛错
|
||||||
SimApiError.errorWhenFalse(hasPermission, 403, "无权操作")
|
SimApiError.errorWhenFalse(hasPermission, 403, "无权操作")
|
||||||
SimApiError.errorWhenNone(someOptional, 404, "用户不存在")
|
SimApiError.errorWhenNull(someOptional, 404, "用户不存在")
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 认证 — SimApiAuth
|
### 2. 认证 — SimApiAuth
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
|
|
||||||
// 由 DI 注入(构造参数 options: SimApiOptions,从配置读 RedisConfiguration;未配则 InMemory)
|
// 由 DI 注入(构造参数 options: SimApiOptions,从配置读 RedisConfiguration;未配则 InMemory)
|
||||||
let auth: SimApiAuth = ... // 例:控制器构造注入
|
let auth: SimApiAuth = ... // 例:控制器构造注入
|
||||||
@@ -127,12 +158,12 @@ auth.logoutAll("user-001") // 退出全部
|
|||||||
- **InMemory 模式**:零配置,适合开发/测试;登录态带过期时间(对齐 C# 过期语义),重启后丢失
|
- **InMemory 模式**:零配置,适合开发/测试;登录态带过期时间(对齐 C# 过期语义),重启后丢失
|
||||||
- **Token 传参**:Header `Token: <value>` 或 Query `token=<value>`
|
- **Token 传参**:Header `Token: <value>` 或 Query `token=<value>`
|
||||||
|
|
||||||
### 2.1 声明式鉴权 — @SimApiAuth(注解类,对齐 C# [SimApiAuth])
|
### 2.1 声明式鉴权 — @SimApiAuth(对齐 C# [SimApiAuth])
|
||||||
|
|
||||||
标注在控制器**方法或类**上,请求派发时自动执行鉴权(未登录 401 → 类型不匹配 403 → 遍历执行 `ISimApiAuthChecker`),替代手动 `requireLogin()`:
|
标注在控制器**方法或类**上,请求派发时自动执行鉴权(未登录 401 → 类型不匹配 403 → 遍历执行 `ISimApiAuthChecker`):
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simapi.attributes.{SimApiAuth}
|
import simcu::simapi.annotations.{SimApiAuth}
|
||||||
|
|
||||||
@SimApiAuth // 类级:整个控制器需登录
|
@SimApiAuth // 类级:整个控制器需登录
|
||||||
public class MyController <: SimApiBaseController {
|
public class MyController <: SimApiBaseController {
|
||||||
@@ -143,14 +174,14 @@ public class MyController <: SimApiBaseController {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> 说明:仓颉注解参数须为编译期常量,`@SimApiAuth` 支持单类型参数(`@SimApiAuth["admin"]`);空参数表示任意已登录用户。多个 `ISimApiAuthChecker` 通过 `SimApiOptions.authCheckers` 注册(由 addSimApi 扫描调用者包填充)。
|
> 说明:仓颉注解参数须为编译期常量,`@SimApiAuth` 支持单个类型参数(`@SimApiAuth["admin"]`)或逗号分隔多类型(`@SimApiAuth["admin,user"]`,对齐 C# `type.Split(",")`);空参数表示任意已登录用户。
|
||||||
|
|
||||||
### 2.2 原样响应 — @OriginResponse
|
### 2.2 原样响应 — @OriginResponse
|
||||||
|
|
||||||
标注后跳过统一响应封装,接口返回什么就输出什么(对齐 C# `[OriginResponse]`):
|
标注后跳过统一响应封装,接口返回什么就输出什么(对齐 C# `[OriginResponse]`):
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simapi.attributes.{OriginResponse}
|
import simcu::simapi.annotations.{OriginResponse}
|
||||||
|
|
||||||
@OriginResponse
|
@OriginResponse
|
||||||
@HttpGet["raw"]
|
@HttpGet["raw"]
|
||||||
@@ -159,36 +190,35 @@ public func raw(): String {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.3 服务端验签 — SimApiSignChecker(对齐 C# [SimApiSign])
|
### 2.3 声明式验签 — @SimApiSign(对齐 C# [SimApiSign])
|
||||||
|
|
||||||
校验带签名请求(appId 提取 → 密钥获取 → timestamp 过期校验 → nonce 去重 → MD5 比对):
|
标注在控制器**方法或类**上,请求派发时自动验签(appId 提取 → 密钥获取 → timestamp 过期校验 → nonce 去重 → MD5 比对):
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simapi.helpers.{SimApiSignProviderBase, SimApiSignChecker}
|
import simcu::simapi.annotations.{SimApiSign}
|
||||||
|
import simcu::simapi.interfaces.{SimApiSignProviderBase}
|
||||||
|
|
||||||
// 1. 继承 Provider 实现密钥获取
|
// 1. 继承 Provider 实现密钥获取(并注册到 DI)
|
||||||
public class MySignProvider <: SimApiSignProviderBase {
|
public class MySignProvider <: SimApiSignProviderBase {
|
||||||
public override func getKey(appId: ?String): ?String {
|
public override func getKey(appId: ?String): ?String {
|
||||||
// 根据 appId 返回密钥(如查库)
|
|
||||||
Some("my-secret-key")
|
Some("my-secret-key")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 控制器方法开头调用校验
|
// 2. 方法标注 @SimApiSign,自动验签(provider 类型名从 DI 解析)
|
||||||
public func signedAction(): String {
|
@SimApiSign["MySignProvider"]
|
||||||
SimApiSignChecker.verify(context, provider, cache)
|
public func signedAction(): String { "ok" }
|
||||||
"ok"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Provider 可配置:`appIdName` / `timestampName` / `nonceName` / `signName` / `queryExpires` / `duplicateRequestProtection` / `signFields`(与 C# `SimApiSignProviderBase` 一致)。
|
`SimApiSignProviderBase` 可配置:`appIdName` / `timestampName` / `nonceName` / `signName` / `queryExpires` / `duplicateRequestProtection` / `signFields`(与 C# 一致)。也可手动调用 `SimApiSignChecker.verify(context, provider, cache)`。
|
||||||
|
|
||||||
### 2.4 AES body 解密 — SimApiAesBodyChecker(对齐 C# [AesBody])
|
### 2.4 声明式 AES body — @AesBody(对齐 C# [AesBody])
|
||||||
|
|
||||||
服务端接收 `{"data":"密文"}` 加密 body,解密后返回明文 JSON(控制器再反序列化为目标类型):
|
标注在**参数**上,请求派发时自动解密 `{"data":"密文"}` body 并反序列化为参数类型:
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simapi.helpers.{AesBodyProviderBase, SimApiAesBodyChecker}
|
import simcu::simapi.annotations.{AesBody}
|
||||||
|
import simcu::simapi.interfaces.{AesBodyProviderBase}
|
||||||
|
|
||||||
public class MyAesProvider <: AesBodyProviderBase {
|
public class MyAesProvider <: AesBodyProviderBase {
|
||||||
public override func getKey(appId: ?String): ?String {
|
public override func getKey(appId: ?String): ?String {
|
||||||
@@ -196,13 +226,14 @@ public class MyAesProvider <: AesBodyProviderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func create(@FromBody req: AesBodyRequest): String {
|
public func create(@AesBody["MyAesProvider"] request: CreateRequest): String {
|
||||||
let json = SimApiAesBodyChecker.decryptBody(context, provider) // 解密后的 JSON 字符串
|
// request 已自动解密并反序列化
|
||||||
let dto = JsonSerializer.deserializeObject<MyDto>(json)
|
|
||||||
"ok"
|
"ok"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
也可手动调用 `SimApiAesBodyChecker.decryptBody(context, provider)` 获取明文 JSON 字符串。
|
||||||
|
|
||||||
### 3. 缓存 — SimApiCache
|
### 3. 缓存 — SimApiCache
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
@@ -226,12 +257,16 @@ SimApiUtil.md5("text") // 32 位十六进制
|
|||||||
SimApiUtil.sha1("text") // 40 位
|
SimApiUtil.sha1("text") // 40 位
|
||||||
SimApiUtil.base64Encode("text") / base64Decode("...")
|
SimApiUtil.base64Encode("text") / base64Decode("...")
|
||||||
SimApiUtil.base64Encode(obj) // 对象 → JSON → Base64(对齐 C# Base64Encode(object))
|
SimApiUtil.base64Encode(obj) // 对象 → JSON → Base64(对齐 C# Base64Encode(object))
|
||||||
SimApiUtil.fromJson<T>(json) // JSON → T(对齐 C# FromJson<T>,T 需 ISerialization<T>)
|
SimApiUtil.json(obj) // 对象 → JSON 字符串(simcu::serialization 反射)
|
||||||
SimApiUtil.base64DecodeTo<T>(str) // Base64 → JSON → T(对齐 C# Base64Decode<T>)
|
SimApiUtil.escapeJson(s) // JSON 字符串转义
|
||||||
|
SimApiUtil.fromJson<T>(json) // JSON → T(对齐 C# FromJson<T>,任意类免约束)
|
||||||
|
SimApiUtil.base64DecodeTo<T>(str) // Base64 → JSON → T
|
||||||
SimApiUtil.checkCell("13800138000") // 手机号
|
SimApiUtil.checkCell("13800138000") // 手机号
|
||||||
SimApiUtil.checkEmail("a@b.com") // 邮箱
|
SimApiUtil.checkEmail("a@b.com") // 邮箱
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> JSON 序列化/反序列化统一走 **simcu::serialization**(`JsonSerializer.Serialize` / `Deserialize<T>`),任意类免标注、免接口约束。
|
||||||
|
|
||||||
### 4.1 AES 加解密 — SimApiAesUtil(对齐 C# SimApiAesUtil)
|
### 4.1 AES 加解密 — SimApiAesUtil(对齐 C# SimApiAesUtil)
|
||||||
|
|
||||||
纯仓颉实现 AES-256-CBC + PKCS7(S-box/密钥扩展/轮函数),与 .NET 双向互操作已验证:
|
纯仓颉实现 AES-256-CBC + PKCS7(S-box/密钥扩展/轮函数),与 .NET 双向互操作已验证:
|
||||||
@@ -247,7 +282,7 @@ let plain = SimApiAesUtil.decrypt(encrypted, "key字符串")
|
|||||||
### 4.2 实体基类 — SimApiBaseModel(对齐 C# SimApiBaseModel)
|
### 4.2 实体基类 — SimApiBaseModel(对齐 C# SimApiBaseModel)
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simapi.models.*
|
import simcu::simapi.models.*
|
||||||
|
|
||||||
public class User <: SimApiBaseModel {
|
public class User <: SimApiBaseModel {
|
||||||
public var _name: String = ""
|
public var _name: String = ""
|
||||||
@@ -261,28 +296,27 @@ user.updateTime() // 刷新 _updatedAt
|
|||||||
|
|
||||||
### 5. HTTP 客户端 — SimApiHttpClient
|
### 5. HTTP 客户端 — SimApiHttpClient
|
||||||
|
|
||||||
用于调用其他带签名/AES 的 SimApi 服务(**内置 TLS 支持**:`https` 自动配置信任所有证书 + SNI,仓颉生态下 stdx TLS 动态加载 openssl 可用):
|
用于调用其他带签名/AES 的 SimApi 服务(**基于 stdx.net.http,不依赖 soulsoft_net_http**;内置 TLS:`https` 自动配置信任所有证书 + SNI):
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
let client = SimApiHttpClient(options: SimApiHttpClientOptions()) // 配置 server/appId/appKey
|
let client = SimApiHttpClient(options: SimApiHttpClientOptions()) // 配置 server/appId/appKey
|
||||||
|
|
||||||
// 返回泛型 T(对齐 .NET SignQuery<T>/AesQuery<T>/AesSignQuery<T>),T 需实现 ISerialization<T>
|
// 返回泛型 T(对齐 .NET SignQuery<T>/AesQuery<T>/AesSignQuery<T>),T 任意类免约束
|
||||||
let resp1 = client.signQuery<SimApiLoginItem>("/api/hello", body: "{\"a\":1}")
|
let resp1 = client.signQuery<SimApiLoginItem>("/api/hello", body: "{\"a\":1}")
|
||||||
let resp2 = client.aesQuery<SimApiLoginItem>("/api/data", body: "{\"a\":1}")
|
let resp2 = client.aesQuery<SimApiLoginItem>("/api/data", body: "{\"a\":1}")
|
||||||
let resp3 = client.aesSignQuery<SimApiLoginItem>("/api/data", body: "{\"a\":1}")
|
let resp3 = client.aesSignQuery<SimApiLoginItem>("/api/data", body: "{\"a\":1}")
|
||||||
```
|
```
|
||||||
|
|
||||||
签名参数名可配置(`simApiHttpClientOptions.signName / timestampName / nonceName / appIdName / signFields`,C# 侧为硬编码)。
|
签名参数名可配置(`signName / timestampName / nonceName / appIdName / signFields`,对齐 C# 的 virtual 属性)。AES 请求体用 `SimApiOneFieldRequest<String>` 序列化为 `{"data":"密文"}`(对齐 C#)。
|
||||||
|
|
||||||
### 5.1 请求日志 — enableRequestLog
|
### 5.1 请求日志 — enableRequestLog
|
||||||
|
|
||||||
记录每次请求的方法、URL、请求头、请求体、响应状态码、耗时与异常(对齐 C#):
|
记录每次请求的方法、URL、请求头、请求体、响应状态码、耗时与异常(对齐 C#):
|
||||||
- 请求体按 **JSON 字段级截断**(仅对超长字符串字段截断,保留结构;非 JSON 整串截断)
|
- 请求体按 **JSON 字段级截断**(仅对超长字符串字段截断,保留结构;非 JSON 整串截断)
|
||||||
- 下游异常**捕获记录后重抛**(对齐 C# ExceptionDispatchInfo)
|
- 下游异常**捕获记录后重抛**(对齐 C# ExceptionDispatchInfo)
|
||||||
- 响应体因 soulsoft `HttpResponse.body` 只读不可替换,记录 `Content-Length` 作为替代(C# 用 MemoryStream 捕获)
|
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
builder.addSimApi { options =>
|
SimApiExtensions.addSimApi(builder) { options =>
|
||||||
options.enableRequestLog = true
|
options.enableRequestLog = true
|
||||||
options.simApiRequestLogOptions.showFullHeader = true // 打印完整 Header(默认只打 Token/Query-Id)
|
options.simApiRequestLogOptions.showFullHeader = true // 打印完整 Header(默认只打 Token/Query-Id)
|
||||||
options.simApiRequestLogOptions.requestStringLogLength = 200 // 请求体字段截断长度(0 不截断)
|
options.simApiRequestLogOptions.requestStringLogLength = 200 // 请求体字段截断长度(0 不截断)
|
||||||
@@ -302,34 +336,53 @@ builder.addSimApi { options =>
|
|||||||
|
|
||||||
### 5.2 日志格式 — SimApiLogger
|
### 5.2 日志格式 — SimApiLogger
|
||||||
|
|
||||||
`enableLogger`(默认 `true`)时自动使用 `SimApiLoggerProvider`(替换 soulsoft 默认控制台格式),输出格式对齐 C# 原版:
|
`enableLogger`(默认 `true`)时自动使用 `SimApiLoggerProvider`,输出格式对齐 C# 原版:
|
||||||
|
|
||||||
```
|
```
|
||||||
[ 分类 ][ 时间:毫秒 ][ 级别 ]
|
[ 分类 ][ 时间:毫秒 ][ 级别 ]
|
||||||
消息内容
|
消息内容
|
||||||
```
|
```
|
||||||
|
|
||||||
按级别着色:
|
按级别着色:Debug 深紫 / Info 深青 / Warn 黄 / Error 红 / Fatal 深红。
|
||||||
|
|
||||||
| 级别 | 颜色 |
|
### 5.3 存储 — SimApiStorage(S3/MinIO,对齐 C# SimApiStorage)
|
||||||
|
|
||||||
|
`enableSimApiStorage = true` 时注册 `SimApiStorage`(Scoped,内部自实现 AWS Signature V4,基于 stdx.net.http,无需 Minio SDK):
|
||||||
|
|
||||||
|
```cangjie
|
||||||
|
SimApiExtensions.addSimApi(builder) { options =>
|
||||||
|
options.enableSimApiStorage = true
|
||||||
|
options.configureSimApiStorage { storage =>
|
||||||
|
storage.endpoint = "http://192.168.0.2:9000" // 必须 http:// 或 https:// 开头
|
||||||
|
storage.serveUrl = "https://files.example.com" // 文件访问地址,不能以 / 结尾
|
||||||
|
storage.bucket = "app-files"
|
||||||
|
storage.accessKey = "minioadmin"
|
||||||
|
storage.secretKey = "minioadmin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| Debug | 深紫(DarkMagenta) |
|
| `getUploadUrl(path, expire=7200)` | 上传预签名 URL,返回 `GetUploadUrlResponse(UploadUrl, DownloadUrl, Path)` |
|
||||||
| Info | 深青(DarkCyan) |
|
| `getDownloadUrl(path, expire=600)` | 下载预签名 URL |
|
||||||
| Warn | 黄(Yellow) |
|
| `uploadFile(path, data, contentType="image/png")` | 直接 PUT 上传(字节数组) |
|
||||||
| Error | 红(Red) |
|
| `deleteFiles(paths)` | 批量删除对象(S3 原生 DeleteObjects,一次请求删多个) |
|
||||||
| Fatal | 深红(DarkRed 粗体近似) |
|
| `fullUrl(path)` / `getUrl(path)` | 补全访问 URL(`~/` 前缀依赖请求上下文) |
|
||||||
| 其他 | 白(White) |
|
| `getPath(url)` | 从 URL 还原相对路径(去掉 Endpoint/Bucket 或 ServeUrl 前缀) |
|
||||||
|
|
||||||
|
> 说明:桶不存在时自动创建(对齐 C# BucketExists + MakeBucket,静态守卫只执行一次);
|
||||||
|
> 预签名与上传使用 AWS SigV4(HMAC-SHA256 基于 stdx SHA256 自实现),已用 AWS 官方测试向量验证签名正确。
|
||||||
|
|
||||||
### 6. 内置路由(UseSimApi 自动注册)
|
### 6. 内置路由(UseSimApi 自动注册)
|
||||||
|
|
||||||
| 路由 | 方法 | 条件 | 说明 |
|
| 路由 | 方法 | 条件 | 说明 |
|
||||||
| ----------------- | -------- | ---------------------------- | -------------------------- |
|
| ----------------- | -------- | ---------------------------- | -------------------------- |
|
||||||
| `/versions` | GET/POST | 始终 | 返回 SimApi/App 版本 |
|
|
||||||
| `/user/info` | POST | `enableSimApiAuth` | 需登录,返回 LoginInfo |
|
| `/user/info` | POST | `enableSimApiAuth` | 需登录,返回 LoginInfo |
|
||||||
| `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 |
|
| `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 |
|
||||||
| `/exception/{code}` | GET | 始终 | 错误反馈 |
|
| `/exception/{code}` | GET | 始终 | 错误反馈(抛 SimApiException) |
|
||||||
|
|
||||||
路由路径可自定义(`configureSimApiRoute`,自定义值通过 `mapGet/mapPost` 真实注册,默认值由内置控制器特性路由覆盖):
|
路由路径可自定义(`configureSimApiRoute`):
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
options.configureSimApiRoute { route =>
|
options.configureSimApiRoute { route =>
|
||||||
@@ -344,7 +397,7 @@ options.configureSimApiRoute { route =>
|
|||||||
实现后每次认证成功都会调用(配合 `@SimApiAuth` 注解或手动 `requireLogin`):
|
实现后每次认证成功都会调用(配合 `@SimApiAuth` 注解或手动 `requireLogin`):
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simapi.interfaces.*
|
import simcu::simapi.interfaces.*
|
||||||
|
|
||||||
class MyAuthChecker <: ISimApiAuthChecker {
|
class MyAuthChecker <: ISimApiAuthChecker {
|
||||||
public func run(loginItem: SimApiLoginItem, token: String): Unit {
|
public func run(loginItem: SimApiLoginItem, token: String): Unit {
|
||||||
@@ -358,7 +411,7 @@ class MyAuthChecker <: ISimApiAuthChecker {
|
|||||||
`enableSimApiAuthGate = true` 时注册 `SimApiAuthClient` / `SimApiAuthCenter` / `SimApiAuthIam` 单例并挂载网关透传中间件:
|
`enableSimApiAuthGate = true` 时注册 `SimApiAuthClient` / `SimApiAuthCenter` / `SimApiAuthIam` 单例并挂载网关透传中间件:
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
builder.addSimApi { options =>
|
SimApiExtensions.addSimApi(builder) { options =>
|
||||||
options.enableSimApiAuthGate = true
|
options.enableSimApiAuthGate = true
|
||||||
options.configureSimApiAuthCenter { auth =>
|
options.configureSimApiAuthCenter { auth =>
|
||||||
auth.server = "https://auth.example.com"
|
auth.server = "https://auth.example.com"
|
||||||
@@ -376,7 +429,7 @@ builder.addSimApi { options =>
|
|||||||
| `SimApiAuthCenterMiddleware` | 网关透传:`X-SimApi-Gate-Auth/Time/Sign` 三头 MD5 校验 → Base64 解码 LoginInfo |
|
| `SimApiAuthCenterMiddleware` | 网关透传:`X-SimApi-Gate-Auth/Time/Sign` 三头 MD5 校验 → Base64 解码 LoginInfo |
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simapi.authsdk.*
|
import simcu::simapi.authsdk.*
|
||||||
|
|
||||||
let center = SimApiAuthCenter(client) // client 从 DI 注入
|
let center = SimApiAuthCenter(client) // client 从 DI 注入
|
||||||
let groups = center.groupRelated(profileId) // 群组列表
|
let groups = center.groupRelated(profileId) // 群组列表
|
||||||
@@ -390,7 +443,7 @@ iam.checkPermission(profileId, "app:create") // 无权限抛 403
|
|||||||
## SimApiOptions 完整配置
|
## SimApiOptions 完整配置
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
builder.addSimApi { options =>
|
SimApiExtensions.addSimApi(builder) { options =>
|
||||||
options.redisConfiguration = "localhost:6379" // Redis(可选,支持 ,password=xxx,db=2)
|
options.redisConfiguration = "localhost:6379" // Redis(可选,支持 ,password=xxx,db=2)
|
||||||
|
|
||||||
// 功能开关
|
// 功能开关
|
||||||
@@ -430,17 +483,17 @@ builder.addSimApi { options =>
|
|||||||
|
|
||||||
## 内置控制器(MVC 写法)
|
## 内置控制器(MVC 写法)
|
||||||
|
|
||||||
simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),`addSimApi` 自动注册内置控制器 + 自动扫描调用者包中的控制器(对齐 C# `Assembly.GetTypes()` 扫描,见 `SimApiControllerScanner`):
|
simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),`addSimApi` 自动注册内置控制器 + 自动扫描调用者包中的控制器(对齐 C# `Assembly.GetTypes()` 扫描):
|
||||||
|
|
||||||
| 控制器 | 路由 | 说明 |
|
| 控制器 | 路由 | 说明 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| `SimApiCommonController` | `/exception/{code}`、`/config`、`/versions`、`/user/info` | 通用内置路由 |
|
| `SimApiCommonController` | `/exception/{code}`、`/config`、`/user/info` | 通用内置路由 |
|
||||||
| `SimApiAuthController` | `/auth/logout` | 退出登录 |
|
| `SimApiAuthController` | `/auth/logout` | 退出登录 |
|
||||||
| `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken` / `requireLogin()` / `getLogin()` |
|
| `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken` / `requireLogin()` / `getLogin()` |
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
import simapi.controllers.*
|
import simcu::simapi.controllers.*
|
||||||
import simapi.attributes.{SimApiAuth}
|
import simcu::simapi.annotations.{SimApiAuth}
|
||||||
|
|
||||||
// 控制器写法:继承 SimApiBaseController,注解路由 + DI 注入
|
// 控制器写法:继承 SimApiBaseController,注解路由 + DI 注入
|
||||||
@SimApiAuth // 类级鉴权(可选,替代 requireLogin)
|
@SimApiAuth // 类级鉴权(可选,替代 requireLogin)
|
||||||
@@ -461,16 +514,15 @@ public class MyController <: SimApiBaseController {
|
|||||||
|
|
||||||
## 未实现模块(选项占位)
|
## 未实现模块(选项占位)
|
||||||
|
|
||||||
以下 C# 原包功能因仓颉生态暂无对应库(Hangfire/MQTT/MinIO/Swashbuckle),**选项保留但未实现**:
|
以下 C# 原包功能因仓颉生态暂无对应库(Hangfire/MQTT/Swashbuckle),**选项保留但未实现**:
|
||||||
|
|
||||||
| 选项 | 原功能 | 状态 |
|
| 选项 | 原功能 | 状态 |
|
||||||
|------|--------|------|
|
|------|--------|------|
|
||||||
| `enableSimApiDoc` | Swagger 文档(可换 soulsoft_web_openapi) | ❌ 未实现 |
|
| `enableSimApiDoc` | Swagger 文档(可换 soulsoft_web_openapi) | ❌ 未实现 |
|
||||||
| `enableSimApiStorage` | S3/MinIO 存储 | ❌ 未实现 |
|
|
||||||
| `enableSynapse` | MQTT 通信 | ❌ 未实现 |
|
| `enableSynapse` | MQTT 通信 | ❌ 未实现 |
|
||||||
| `enableJob` | Hangfire 任务调度 | ❌ 未实现 |
|
| `enableJob` | Hangfire 任务调度 | ❌ 未实现 |
|
||||||
|
|
||||||
> ✅ 已实现(曾为占位):`enableSimApiAuthGate`(AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC,与 .NET 双向互操作)、`ISimApiAuthChecker`(注解鉴权时执行)、内置路由自定义路径。
|
> ✅ 已实现(曾为占位):`enableSimApiStorage`(S3/MinIO,自实现 AWS SigV4)、`enableSimApiAuthGate`(AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC,与 .NET 双向互操作)、`ISimApiAuthChecker`、`@SimApiSign` / `@AesBody` 声明式注解、内置路由自定义路径。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -482,9 +534,9 @@ public class MyController <: SimApiBaseController {
|
|||||||
| `soulsoft_extensions_logging` 系列 | 日志 |
|
| `soulsoft_extensions_logging` 系列 | 日志 |
|
||||||
| `soulsoft_extensions_injection` | 依赖注入 |
|
| `soulsoft_extensions_injection` | 依赖注入 |
|
||||||
| `soulsoft_extensions_configuration` | 配置 |
|
| `soulsoft_extensions_configuration` | 配置 |
|
||||||
| `soulsoft_serialization` | JSON 序列化 |
|
| `simcu::serialization`(path 依赖) | JSON 序列化(simapi 自研,反射免标注) |
|
||||||
| `redis`(pkg.cangjie-lang.cn) | Redis 客户端(认证/缓存 Redis 模式) |
|
| `redis`(pkg.cangjie-lang.cn) | Redis 客户端(认证/缓存 Redis 模式) |
|
||||||
| `stdx`(CANGJIE_STDX_PATH) | 标准扩展库(md5/sha1/base64/http) |
|
| `stdx`(CANGJIE_STDX_PATH) | 标准扩展库(md5/sha1/base64/http/tls) |
|
||||||
|
|
||||||
> 构建前需设置 `CANGJIE_STDX_PATH` 指向本地 stdx 的 `static/stdx` 目录。
|
> 构建前需设置 `CANGJIE_STDX_PATH` 指向本地 stdx 的 `static/stdx` 目录。
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
version = 0
|
version = 0
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
soulsoft_extensions_hosting = {version = "1.0.20260528"}
|
|
||||||
soulsoft_extensions_options_configuration = {version = "1.0.20260528"}
|
soulsoft_extensions_options_configuration = {version = "1.0.20260528"}
|
||||||
soulsoft_net_http = {version = "1.0.20260528"}
|
|
||||||
redis = {version = "1.0.20260627"}
|
|
||||||
soulsoft_web_http = {version = "1.0.20260528"}
|
soulsoft_web_http = {version = "1.0.20260528"}
|
||||||
soulsoft_identity_claims = {version = "1.0.20260528"}
|
|
||||||
soulsoft_web_routing = {version = "1.0.20260528"}
|
|
||||||
soulsoft_web_hosting = {version = "1.0.20260528"}
|
|
||||||
soulsoft_extensions_logging = {version = "1.0.20260528"}
|
soulsoft_extensions_logging = {version = "1.0.20260528"}
|
||||||
soulsoft_extensions_configuration = {version = "1.0.20260528"}
|
|
||||||
soulsoft_web_mvc = {version = "1.0.20260528"}
|
|
||||||
soulsoft_extensions_logging_console = {version = "1.0.20260528"}
|
|
||||||
soulsoft_web_cors = {version = "1.0.20260528"}
|
|
||||||
soulsoft_extensions_logging_configuration = {version = "1.0.20260528"}
|
|
||||||
soulsoft_extensions_injection = {version = "1.0.20260528"}
|
|
||||||
soulsoft_serialization = {version = "1.0.20260528"}
|
|
||||||
soulsoft_extensions_options = {version = "1.0.20260528"}
|
soulsoft_extensions_options = {version = "1.0.20260528"}
|
||||||
|
soulsoft_web_routing = {version = "1.0.20260528"}
|
||||||
|
soulsoft_extensions_logging_console = {version = "1.0.20260528"}
|
||||||
|
soulsoft_identity_claims = {version = "1.0.20260528"}
|
||||||
|
soulsoft_web_hosting = {version = "1.0.20260528"}
|
||||||
|
soulsoft_extensions_logging_configuration = {version = "1.0.20260528"}
|
||||||
|
soulsoft_extensions_hosting = {version = "1.0.20260528"}
|
||||||
|
soulsoft_web_mvc = {version = "1.0.20260528"}
|
||||||
|
soulsoft_extensions_configuration = {version = "1.0.20260528"}
|
||||||
|
redis = {version = "1.0.20260627"}
|
||||||
|
soulsoft_web_cors = {version = "1.0.20260528"}
|
||||||
|
soulsoft_serialization = {version = "1.0.20260528"}
|
||||||
|
soulsoft_extensions_injection = {version = "1.0.20260528"}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
cjc-version = "1.1.3"
|
cjc-version = "1.1.3"
|
||||||
name = "simapi"
|
name = "simapi"
|
||||||
|
organization = "simcu"
|
||||||
description = "SimApi 仓颉版:ASP.NET Core 风格 API 基础框架(统一响应/异常拦截/Token认证/缓存/工具集/HTTP客户端)"
|
description = "SimApi 仓颉版:ASP.NET Core 风格 API 基础框架(统一响应/异常拦截/Token认证/缓存/工具集/HTTP客户端)"
|
||||||
version = "5.2.12"
|
version = "1.0.3"
|
||||||
target-dir = ""
|
target-dir = ""
|
||||||
output-type = "static"
|
output-type = "static"
|
||||||
override-compile-option = ""
|
override-compile-option = ""
|
||||||
@@ -21,9 +22,8 @@
|
|||||||
soulsoft_extensions_configuration = "1.0.20260528"
|
soulsoft_extensions_configuration = "1.0.20260528"
|
||||||
soulsoft_extensions_injection = "1.0.20260528"
|
soulsoft_extensions_injection = "1.0.20260528"
|
||||||
soulsoft_extensions_options = "1.0.20260528"
|
soulsoft_extensions_options = "1.0.20260528"
|
||||||
soulsoft_serialization = "1.0.20260528"
|
|
||||||
soulsoft_net_http = "1.0.20260528"
|
|
||||||
redis = "1.0.20260627"
|
redis = "1.0.20260627"
|
||||||
|
"simcu::serialization" = { path = "../simapi-serialization" }
|
||||||
|
|
||||||
[target]
|
[target]
|
||||||
[target.x86_64-w64-mingw32]
|
[target.x86_64-w64-mingw32]
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
*
|
||||||
|
* 对齐 .NET:根命名空间 SimApi 下的静态类 SimApiExtensions(AddSimApi + UseSimApi)。
|
||||||
|
* 本文件同时是 simapi 根包的入口锚点:cjpm 要求 src 根目录至少有一个 .cj 文件,
|
||||||
|
* 否则不会扫描 src 子目录(helpers/controllers/...),整个包将编译为空。
|
||||||
|
*
|
||||||
|
* 使用方式:
|
||||||
|
* ```
|
||||||
|
* let builder = WebHost.createBuilder(args)
|
||||||
|
* builder.services.addLogging()
|
||||||
|
* SimApiExtensions.addSimApi(builder) { options =>
|
||||||
|
* options.enableSimApiAuth = true
|
||||||
|
* }
|
||||||
|
* let host = builder.build()
|
||||||
|
* SimApiExtensions.useSimApi(host)
|
||||||
|
* host.run()
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.extensions
|
package simcu::simapi
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.convert.*
|
import std.convert.*
|
||||||
import std.reflect.*
|
import std.reflect.*
|
||||||
import std.time.*
|
import std.time.*
|
||||||
import soulsoft_serialization.*
|
|
||||||
import soulsoft_serialization.macros.*
|
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
import soulsoft_web_hosting.*
|
import soulsoft_web_hosting.*
|
||||||
import soulsoft_web_mvc.*
|
import soulsoft_web_mvc.*
|
||||||
@@ -20,52 +34,30 @@ import soulsoft_web_cors.*
|
|||||||
import soulsoft_web_routing.*
|
import soulsoft_web_routing.*
|
||||||
import soulsoft_extensions_injection.*
|
import soulsoft_extensions_injection.*
|
||||||
import soulsoft_extensions_logging.*
|
import soulsoft_extensions_logging.*
|
||||||
import simapi.authsdk.*
|
import simcu::simapi.authsdk.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.configurations.*
|
import simcu::simapi.configurations.*
|
||||||
import simapi.controllers.*
|
import simcu::simapi.controllers.*
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
import simapi.logger.*
|
import simcu::simapi.interfaces.*
|
||||||
import simapi.middlewares.*
|
import simcu::simapi.logger.*
|
||||||
|
import simcu::simapi.middlewares.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SimApi 扩展入口:对应 C# 的 SimApiExtensions(AddSimApi + UseSimApi)。
|
* SimApi 扩展入口(对齐 C# 根命名空间 SimApi 的静态类 SimApiExtensions)。
|
||||||
*
|
|
||||||
* 使用方式(仓颉版,与 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 {
|
public class SimApiExtensions {
|
||||||
|
private init() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册 SimApi 服务到 WebHostBuilder(自动 addRouting + addControllers + 扫描控制器)。
|
* 注册 SimApi 服务到 WebHostBuilder(自动 addRouting + addControllers + 扫描控制器)。
|
||||||
|
* @param builder 主机构建器。
|
||||||
* @param configure 配置回调。
|
* @param configure 配置回调。
|
||||||
* @return 当前构建器。
|
* @return 当前构建器。
|
||||||
*/
|
*/
|
||||||
func addSimApi(configure: (SimApiOptions) -> Unit): WebHostBuilder
|
public static func addSimApi(builder: WebHostBuilder, 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())
|
// 自动注册路由(对齐 builder.Services.AddRouting())
|
||||||
this.services.addRouting()
|
builder.services.addRouting()
|
||||||
// 先构造配置,供后续按开关注册服务(对齐 C# AddSimApi 中先读 options 再注册)
|
// 先构造配置,供后续按开关注册服务(对齐 C# AddSimApi 中先读 options 再注册)
|
||||||
let options = SimApiOptions()
|
let options = SimApiOptions()
|
||||||
configure(options)
|
configure(options)
|
||||||
@@ -74,108 +66,33 @@ extend WebHostBuilder <: SimApiBuilderExtensions {
|
|||||||
// 必须在 addControllers 之前:soulsoft 用 tryAddSingleton 注册,先到先得,不会被覆盖。
|
// 必须在 addControllers 之前:soulsoft 用 tryAddSingleton 注册,先到先得,不会被覆盖。
|
||||||
// 未启用时使用 soulsoft 默认派发(String→ContentResult / ISerializable→ObjectResult / 其余→204)。
|
// 未启用时使用 soulsoft 默认派发(String→ContentResult / ISerializable→ObjectResult / 其余→204)。
|
||||||
if (options.enableSimApiResponseFilter) {
|
if (options.enableSimApiResponseFilter) {
|
||||||
this.services.addSingleton<IRequestDelegateFactory, SimApiRequestDelegateFactory>()
|
builder.services.addSingleton<IRequestDelegateFactory, SimApiRequestDelegateFactory>()
|
||||||
}
|
}
|
||||||
// 自动注册 MVC + 控制器(对齐 builder.Services.AddControllers())
|
// 自动注册 MVC + 控制器(对齐 builder.Services.AddControllers())
|
||||||
// 自动扫描调用者包中的 Controller 子类(对齐 C# 的 Assembly.GetTypes() 扫描)
|
// 自动扫描调用者包中的 Controller 子类(对齐 C# 的 Assembly.GetTypes() 扫描)
|
||||||
let controllers = SimApiControllerScanner.scan()
|
let controllers = SimApiControllerScanner.scan()
|
||||||
this.services.addControllers(controllers)
|
addControllers(builder.services, controllers)
|
||||||
// 注册 SimApi 服务
|
// 注册 SimApi 服务
|
||||||
addSimApiCore(this, options)
|
addSimApiCore(builder, options)
|
||||||
|
return builder
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册 SimApi 服务(默认配置,自动扫描控制器)。
|
* 注册 SimApi 服务(默认配置,自动扫描控制器)。
|
||||||
|
* @param builder 主机构建器。
|
||||||
|
* @return 当前构建器。
|
||||||
*/
|
*/
|
||||||
public func addSimApi(): WebHostBuilder {
|
public static func addSimApi(builder: WebHostBuilder): WebHostBuilder {
|
||||||
addSimApi({_ =>})
|
addSimApi(builder, {_ =>})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ISimApiAuthChecker 自动扫描注册(对齐 C# AddSimApi 中遍历调用者程序集 AddScoped 注册 checker)
|
|
||||||
// 扫描调用者包中的 ISimApiAuthChecker 实现类:填充 options.authCheckers(供 @SimApiAuth 执行时解析)
|
|
||||||
// 并注册到 DI(按实现类类型注册,供 getOrThrow(checkerType) 解析)
|
|
||||||
if (options.enableSimApiAuth) {
|
|
||||||
let checkers = SimApiControllerScanner.scanAuthCheckers()
|
|
||||||
for (checkerType in checkers) {
|
|
||||||
options.authCheckers.add(checkerType)
|
|
||||||
builder.services.addScoped(checkerType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存(DI 自动注入 SimApiOptions)
|
|
||||||
if (options.enableSimApiCache) {
|
|
||||||
builder.services.addSingleton<SimApiCache, SimApiCache>()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP 客户端(DI 自动注入 SimApiOptions)
|
|
||||||
if (options.enableSimApiHttpClient) {
|
|
||||||
builder.services.addSingleton<SimApiHttpClient, SimApiHttpClient>()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthGate 认证中心 SDK(对齐 C# 注册 SimApiAuthClient/Center/Iam 单例)
|
|
||||||
if (options.enableSimApiAuthGate) {
|
|
||||||
builder.services.addSingleton<SimApiAuthClient, SimApiAuthClient>()
|
|
||||||
builder.services.addSingleton<SimApiAuthCenter, SimApiAuthCenter>()
|
|
||||||
builder.services.addSingleton<SimApiAuthIam, SimApiAuthIam>()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)。
|
* 应用 SimApi 中间件与内置路由(日志输出对齐 C# UseSimApi)。
|
||||||
|
* @param host 构建完成的主机。
|
||||||
*/
|
*/
|
||||||
public func useSimApi(): Unit {
|
public static func useSimApi(host: WebHost): Unit {
|
||||||
let options = this.services.getOrThrow<SimApiOptions>()
|
let options = host.services.getOrThrow<SimApiOptions>()
|
||||||
let loggerFactory = this.services.getOrThrow<ILoggerFactory>()
|
let loggerFactory = host.services.getOrThrow<ILoggerFactory>()
|
||||||
// 对齐 C# ILogger<SimApiOptions>:分类名为 SimApiOptions 的全限定名
|
// 对齐 C# ILogger<SimApiOptions>:分类名为 SimApiOptions 的全限定名
|
||||||
let logger = loggerFactory.createLogger<SimApiOptions>()
|
let logger = loggerFactory.createLogger<SimApiOptions>()
|
||||||
|
|
||||||
@@ -194,7 +111,8 @@ extend WebHost <: SimApiHostExtensions {
|
|||||||
logger.info("开始配置 SimApiCache...")
|
logger.info("开始配置 SimApiCache...")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SimApiStorage(占位)
|
// SimApiStorage(已实现:addSimApi 中注册 Scoped,桶初始化惰性执行;
|
||||||
|
// 对齐 C# 的 GetService 预热,但 scoped 服务不能从根解析,故仅输出配置日志)
|
||||||
if (options.enableSimApiStorage) {
|
if (options.enableSimApiStorage) {
|
||||||
logger.info("开始配置 SimApiStorage...")
|
logger.info("开始配置 SimApiStorage...")
|
||||||
}
|
}
|
||||||
@@ -216,37 +134,30 @@ extend WebHost <: SimApiHostExtensions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 中间件与路由(对齐 C# UseSimApi(WebApplication) 的挂载顺序) =====
|
// ===== 中间件与路由(对齐 C# UseSimApi(WebApplication) 的挂载顺序) =====
|
||||||
// C# 挂载顺序(先挂载 = 外层):CORS(L425) → AuthGate(L454) → Auth(L462) → RequestLog(L519) → Exception(L525)
|
// C# 挂载顺序(先挂载 = 外层):ForwardedHeaders(L419) → CORS(L425) → AuthGate(L454) → Auth(L462)
|
||||||
// OPTIONS 预检请求在 CORS 处短路(204,不调用 next),因此 RequestLog/Exception 均不会执行
|
// → 内置路由(L465-496) → Swagger(L502) → RequestLog(L519) → Exception(L525) → LowerUrl → Job
|
||||||
|
|
||||||
// CORS(对齐 C# builder.UseCors("any"),最先挂载)
|
// ForwardedHeaders(占位:soulsoft 暂无内置,对齐 C# 最先挂载)
|
||||||
|
if (options.enableForwardHeaders) {
|
||||||
|
logger.info("开始配置ForwardedHeaders...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS(对齐 C# builder.UseCors("any"))
|
||||||
if (options.enableCors) {
|
if (options.enableCors) {
|
||||||
logger.info("开始配置 Cors全部允许...")
|
logger.info("开始配置 Cors全部允许...")
|
||||||
this.useCors()
|
host.useCors()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthGate(对齐 C# UseMiddleware<SimApiAuthCenterMiddleware>)
|
// AuthGate(对齐 C# UseMiddleware<SimApiAuthCenterMiddleware>)
|
||||||
if (options.enableSimApiAuthGate) {
|
if (options.enableSimApiAuthGate) {
|
||||||
logger.info("开始配置 SimApiAuthGate...")
|
logger.info("开始配置 SimApiAuthGate...")
|
||||||
this.use<SimApiAuthCenterMiddleware>()
|
host.use<SimApiAuthCenterMiddleware>()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 认证中间件(对齐 C# builder.UseMiddleware<SimApiAuthMiddleware>())
|
// 认证中间件(对齐 C# builder.UseMiddleware<SimApiAuthMiddleware>())
|
||||||
if (options.enableSimApiAuth) {
|
if (options.enableSimApiAuth) {
|
||||||
logger.info("开始配置 SimApiAuth...")
|
logger.info("开始配置 SimApiAuth...")
|
||||||
this.use<SimApiAuthMiddleware>()
|
host.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>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内置路由:RouteOptions 自定义路径时真实注册(默认路径已由内置控制器特性路由覆盖,
|
// 内置路由:RouteOptions 自定义路径时真实注册(默认路径已由内置控制器特性路由覆盖,
|
||||||
@@ -254,7 +165,7 @@ extend WebHost <: SimApiHostExtensions {
|
|||||||
let routeOptions = options.simApiRouteOptions
|
let routeOptions = options.simApiRouteOptions
|
||||||
if (let Some(route) <- routeOptions.userInfoRoute) {
|
if (let Some(route) <- routeOptions.userInfoRoute) {
|
||||||
if (route != "/user/info") {
|
if (route != "/user/info") {
|
||||||
this.mapPost(route, { context =>
|
host.mapPost(route, { context =>
|
||||||
let controller = ActivatorUtilities.createInstance(context.services,
|
let controller = ActivatorUtilities.createInstance(context.services,
|
||||||
TypeInfo.of<SimApiCommonController>())
|
TypeInfo.of<SimApiCommonController>())
|
||||||
if (let c: SimApiBaseController <- controller) {
|
if (let c: SimApiBaseController <- controller) {
|
||||||
@@ -269,7 +180,7 @@ extend WebHost <: SimApiHostExtensions {
|
|||||||
}
|
}
|
||||||
if (let Some(route) <- routeOptions.logoutRoute) {
|
if (let Some(route) <- routeOptions.logoutRoute) {
|
||||||
if (route != "/auth/logout") {
|
if (route != "/auth/logout") {
|
||||||
this.mapPost(route, { context =>
|
host.mapPost(route, { context =>
|
||||||
let controller = ActivatorUtilities.createInstance(context.services,
|
let controller = ActivatorUtilities.createInstance(context.services,
|
||||||
TypeInfo.of<SimApiAuthController>())
|
TypeInfo.of<SimApiAuthController>())
|
||||||
if (let c: SimApiBaseController <- controller) {
|
if (let c: SimApiBaseController <- controller) {
|
||||||
@@ -284,7 +195,7 @@ extend WebHost <: SimApiHostExtensions {
|
|||||||
}
|
}
|
||||||
if (let Some(route) <- routeOptions.webConfigRoute) {
|
if (let Some(route) <- routeOptions.webConfigRoute) {
|
||||||
if (route != "/config") {
|
if (route != "/config") {
|
||||||
this.mapGet(route, { context =>
|
host.mapGet(route, { context =>
|
||||||
let controller = ActivatorUtilities.createInstance(context.services,
|
let controller = ActivatorUtilities.createInstance(context.services,
|
||||||
TypeInfo.of<SimApiCommonController>())
|
TypeInfo.of<SimApiCommonController>())
|
||||||
if (let c: SimApiBaseController <- controller) {
|
if (let c: SimApiBaseController <- controller) {
|
||||||
@@ -294,7 +205,7 @@ extend WebHost <: SimApiHostExtensions {
|
|||||||
SimApiResultWriter.write(context, c.webConfig())
|
SimApiResultWriter.write(context, c.webConfig())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.mapPost(route, { context =>
|
host.mapPost(route, { context =>
|
||||||
let controller = ActivatorUtilities.createInstance(context.services,
|
let controller = ActivatorUtilities.createInstance(context.services,
|
||||||
TypeInfo.of<SimApiCommonController>())
|
TypeInfo.of<SimApiCommonController>())
|
||||||
if (let c: SimApiBaseController <- controller) {
|
if (let c: SimApiBaseController <- controller) {
|
||||||
@@ -308,11 +219,23 @@ extend WebHost <: SimApiHostExtensions {
|
|||||||
logger.info("注册内置Route: WebConfig => ${route}")
|
logger.info("注册内置Route: WebConfig => ${route}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SimApiDoc(占位)
|
// SimApiDoc(占位,对齐 C# UseSwagger/UseSwaggerUI)
|
||||||
if (options.enableSimApiDoc) {
|
if (options.enableSimApiDoc) {
|
||||||
logger.info("开始配置 SimApiDoc...")
|
logger.info("开始配置 SimApiDoc...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 请求日志中间件(对齐 C# builder.UseMiddleware<SimApiRequestLogMiddleware>())
|
||||||
|
if (options.enableRequestLog) {
|
||||||
|
logger.info("开始配置 SimApiRequestLog...")
|
||||||
|
host.use<SimApiRequestLogMiddleware>()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异常中间件最后挂载(最内层,对齐 C# builder.UseMiddleware<SimApiExceptionMiddleware>())
|
||||||
|
if (options.enableSimApiException) {
|
||||||
|
logger.info("开始配置 SimApiException...")
|
||||||
|
host.use<SimApiExceptionMiddleware>()
|
||||||
|
}
|
||||||
|
|
||||||
// URL 小写
|
// URL 小写
|
||||||
if (options.enableLowerUrl) {
|
if (options.enableLowerUrl) {
|
||||||
logger.info("开始配置使用URL小写...")
|
logger.info("开始配置使用URL小写...")
|
||||||
@@ -329,45 +252,93 @@ extend WebHost <: SimApiHostExtensions {
|
|||||||
logger.info("开始配置 SimApiResponseFilter...")
|
logger.info("开始配置 SimApiResponseFilter...")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForwardedHeaders(占位:soulsoft 暂无内置)
|
|
||||||
if (options.enableForwardHeaders) {
|
|
||||||
logger.info("开始配置ForwardedHeaders...")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 映射控制器端点(对齐 C# UseSimApi 中的 MapControllers)
|
// 映射控制器端点(对齐 C# UseSimApi 中的 MapControllers)
|
||||||
let callSiteFactory = this.services.getOrThrow<IServiceProviderIsService>()
|
let callSiteFactory = host.services.getOrThrow<IServiceProviderIsService>()
|
||||||
if (callSiteFactory.isService<ApplicationPartManager>()) {
|
if (callSiteFactory.isService<ApplicationPartManager>()) {
|
||||||
this.mapControllers()
|
host.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 {
|
private static func addSimApiCore(builder: WebHostBuilder, options: SimApiOptions): WebHostBuilder {
|
||||||
/**
|
// 注册单例配置(对齐 C# builder.AddSingleton(simApiOptions))
|
||||||
* 注册 MVC 服务,并注册 SimApi 内置控制器 + 用户控制器到 ApplicationPartManager。
|
builder.services.addSingleton<SimApiOptions>(options)
|
||||||
* 对齐 .NET 的 AddControllers()(含控制器发现)。
|
// 子配置不单独注册:中间件统一注入 SimApiOptions 后访问其属性
|
||||||
* @param controllerTypes 用户控制器类型列表。
|
// (对齐 C# SimApiExceptionMiddleware(..., SimApiOptions simApiOptions) 风格)
|
||||||
* @return MVC 构建器。
|
|
||||||
*/
|
// 自定义日志格式(替换默认 console provider)
|
||||||
public func addControllers(controllerTypes: Array<TypeInfo>): MvcBuilder {
|
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>()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISimApiAuthChecker 自动扫描注册(对齐 C# AddSimApi 中遍历调用者程序集 AddScoped(ISimApiAuthChecker, type))
|
||||||
|
// 扫描调用者包中的实现类,按【接口】注册;执行时用 getAll<ISimApiAuthChecker>() 一次解析全部实现,
|
||||||
|
// 无需在 SimApiOptions 里维护类型列表。
|
||||||
|
if (options.enableSimApiAuth) {
|
||||||
|
let checkers = SimApiControllerScanner.scanAuthCheckers()
|
||||||
|
for (checkerType in checkers) {
|
||||||
|
builder.services.addScoped(TypeInfo.of<ISimApiAuthChecker>(), checkerType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存(DI 自动注入 SimApiOptions)
|
||||||
|
if (options.enableSimApiCache) {
|
||||||
|
builder.services.addSingleton<SimApiCache, SimApiCache>()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP 客户端(DI 自动注入 SimApiOptions)
|
||||||
|
if (options.enableSimApiHttpClient) {
|
||||||
|
builder.services.addSingleton<SimApiHttpClient, SimApiHttpClient>()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储(S3/MinIO,对齐 C# AddHttpContextAccessor + AddSingleton<SimApiStorage>;
|
||||||
|
// 仓颉版注册为 Scoped 以便注入 IHttpContextAccessor(DI 禁止 singleton 消费 scoped),
|
||||||
|
// 桶初始化由静态守卫保证只执行一次)
|
||||||
|
if (options.enableSimApiStorage) {
|
||||||
|
builder.services.addHttpContextAccessor()
|
||||||
|
builder.services.addScoped<SimApiStorage, SimApiStorage>()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthGate 认证中心 SDK(对齐 C# 注册 SimApiAuthClient/Center/Iam 单例)
|
||||||
|
if (options.enableSimApiAuthGate) {
|
||||||
|
builder.services.addSingleton<SimApiAuthClient, SimApiAuthClient>()
|
||||||
|
builder.services.addSingleton<SimApiAuthCenter, SimApiAuthCenter>()
|
||||||
|
builder.services.addSingleton<SimApiAuthIam, SimApiAuthIam>()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 注册 MVC 服务,并注册 SimApi 内置控制器 + 用户控制器到 ApplicationPartManager(对齐 .NET AddControllers())。
|
||||||
|
private static func addControllers(services: ServiceCollection, controllerTypes: Array<TypeInfo>): MvcBuilder {
|
||||||
// 调用 soulsoft_web_mvc 的无参 addControllers() 注册 MVC 核心服务
|
// 调用 soulsoft_web_mvc 的无参 addControllers() 注册 MVC 核心服务
|
||||||
let mvc = this.addControllers()
|
let mvc = services.addControllers()
|
||||||
let types = ArrayList<TypeInfo>()
|
let types = ArrayList<TypeInfo>()
|
||||||
// SimApi 内置控制器
|
// SimApi 内置控制器
|
||||||
types.add(TypeInfo.of<SimApiCommonController>())
|
types.add(TypeInfo.of<SimApiCommonController>())
|
||||||
@@ -376,7 +347,7 @@ extend ServiceCollection <: SimApiMvcBuilderExtensions {
|
|||||||
for (t in controllerTypes) {
|
for (t in controllerTypes) {
|
||||||
types.add(t)
|
types.add(t)
|
||||||
}
|
}
|
||||||
let part = AssemblyPart("simapi.controllers", types.toArray())
|
let part = AssemblyPart("simcu::simapi.controllers", types.toArray())
|
||||||
mvc.addApplicationPart(part)
|
mvc.addApplicationPart(part)
|
||||||
mvc
|
mvc
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
* 声明式 AES body 解密注解(对齐 C# SimApi.Attributes.AesBodyAttribute)。
|
||||||
|
*
|
||||||
|
* 标注在控制器方法参数上,请求派发时(SimApiRequestDelegateFactory)自动解密并反序列化:
|
||||||
|
* - 读取请求体 {"data": "密文"}
|
||||||
|
* - 通过 keyProvider(DI 解析)获取密钥
|
||||||
|
* - SimApiAesUtil.decrypt 解密得到明文 JSON
|
||||||
|
* - simapi_serialization 按参数类型反序列化
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* public func create(@AesBody request: CreateRequest): Unit {
|
||||||
|
*
|
||||||
|
* 说明:仓颉注解参数须为编译期常量,无法直接持有 Type;
|
||||||
|
* 故 keyProvider 用类型名 String,运行时经 TypeInfo.get 解析后从 DI 取实例。
|
||||||
|
*/
|
||||||
|
|
||||||
|
package simcu::simapi.annotations
|
||||||
|
|
||||||
|
@Annotation[target: [Parameter]]
|
||||||
|
public class AesBody {
|
||||||
|
/// AES 密钥提供器类型名(DI 注册的 AesBodyProviderBase 实现类名)
|
||||||
|
public let keyProvider: String
|
||||||
|
|
||||||
|
public const init() {
|
||||||
|
this.keyProvider = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
public const init(keyProvider: String) {
|
||||||
|
this.keyProvider = keyProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.attributes
|
package simcu::simapi.annotations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 原样响应注解(对齐 C# SimApi.Attributes.OriginResponseAttribute)。
|
* 原样响应注解(对齐 C# SimApi.Attributes.OriginResponseAttribute)。
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.attributes
|
package simcu::simapi.annotations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 声明式鉴权注解(对齐 C# SimApi.Attributes.SimApiAuthAttribute)。
|
* 声明式鉴权注解(对齐 C# SimApi.Attributes.SimApiAuthAttribute)。
|
||||||
@@ -16,9 +16,10 @@ package simapi.attributes
|
|||||||
* 用法:
|
* 用法:
|
||||||
* @SimApiAuth // 任意已登录用户
|
* @SimApiAuth // 任意已登录用户
|
||||||
* @SimApiAuth["admin"] // 仅 admin 类型
|
* @SimApiAuth["admin"] // 仅 admin 类型
|
||||||
|
* @SimApiAuth["admin,user"] // admin 或 user 类型(对齐 C# type.Split(","))
|
||||||
*
|
*
|
||||||
* 说明:仓颉注解参数须为编译期常量,且 String 无法作为 const 值数组元素
|
* 说明:仓颉注解参数须为编译期常量,String 无法作为 const 值数组元素,
|
||||||
* (内部为 Array<UInt8>),故与 C# 的 string[] 不同,这里支持单个类型参数。
|
* 故与 C# 的 string[] 不同,这里用逗号分隔字符串对齐 C# 多类型。
|
||||||
*/
|
*/
|
||||||
@Annotation[target: [MemberFunction, Type]]
|
@Annotation[target: [MemberFunction, Type]]
|
||||||
public class SimApiAuth {
|
public class SimApiAuth {
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
* 声明式签名校验注解(对齐 C# SimApi.Attributes.SimApiSignAttribute)。
|
||||||
|
*
|
||||||
|
* 标注在控制器方法或类上,请求派发时(SimApiRequestDelegateFactory)自动执行验签:
|
||||||
|
* - 提取 appId / timestamp / nonce / sign(Query 优先,其次 Header)
|
||||||
|
* - 通过 keyProvider(DI 解析)获取密钥
|
||||||
|
* - 过期校验 + nonce 去重(需缓存)
|
||||||
|
* - 拼接 SignFields + appId + timestamp + nonce + key,MD5 比对
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* @SimApiSign // 默认 SimApiSignProviderBase(应用需注册实现)
|
||||||
|
* @SimApiSign["MySignProvider"] // 指定 provider 类型名(DI 注册的实现类)
|
||||||
|
*
|
||||||
|
* 说明:仓颉注解参数须为编译期常量,无法直接持有 Type;
|
||||||
|
* 故 keyProvider 用类型名 String,运行时经 TypeInfo.get 解析后从 DI 取实例。
|
||||||
|
*/
|
||||||
|
|
||||||
|
package simcu::simapi.annotations
|
||||||
|
|
||||||
|
@Annotation[target: [MemberFunction, Type]]
|
||||||
|
public class SimApiSign {
|
||||||
|
/// 签名提供器类型名(DI 注册的 SimApiSignProviderBase 实现类名)
|
||||||
|
public let keyProvider: String
|
||||||
|
|
||||||
|
public const init() {
|
||||||
|
this.keyProvider = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
public const init(keyProvider: String) {
|
||||||
|
this.keyProvider = keyProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,15 +4,16 @@
|
|||||||
* AuthSDK/SimApiAuthCenter:认证中心远程 SDK。
|
* AuthSDK/SimApiAuthCenter:认证中心远程 SDK。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.authsdk
|
package simcu::simapi.authsdk
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
|
import std.io.*
|
||||||
|
import stdx.net.http.*
|
||||||
import stdx.net.tls.*
|
import stdx.net.tls.*
|
||||||
import stdx.net.tls.common.*
|
import stdx.net.tls.common.*
|
||||||
import soulsoft_net_http.{HttpClient, HttpRequestMessage, JsonContent}
|
import simcu::serialization.*
|
||||||
import soulsoft_net_http.{HttpMethod as NetHttpMethod}
|
import simcu::simapi.communications.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.helpers.*
|
||||||
import simapi.helpers.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证中心远程 SDK(对齐 C# SimApiAuthCenter):
|
* 认证中心远程 SDK(对齐 C# SimApiAuthCenter):
|
||||||
@@ -38,32 +39,24 @@ public class SimApiAuthCenter {
|
|||||||
*/
|
*/
|
||||||
public func verifySign(appId: String, timestamp: String, nonce: String, sign: String): Unit {
|
public func verifySign(appId: String, timestamp: String, nonce: String, sign: String): Unit {
|
||||||
let url = "${_client.server}/api/auth/sign/verify?appId=${appId}×tamp=${timestamp}&nonce=${nonce}&sign=${sign}"
|
let url = "${_client.server}/api/auth/sign/verify?appId=${appId}×tamp=${timestamp}&nonce=${nonce}&sign=${sign}"
|
||||||
let http = HttpClient.create { builder =>
|
let http = ClientBuilder().
|
||||||
builder.noProxy()
|
noProxy().
|
||||||
var tls = TlsClientConfig()
|
tlsConfig(buildTlsConfig(_client.server)).
|
||||||
tls.verifyMode = CertificateVerifyMode.TrustAll
|
build()
|
||||||
match (_client.server.indexOf("://")) {
|
|
||||||
case Some(i) =>
|
|
||||||
let rest = _client.server[i + 3..]
|
|
||||||
let slash = rest.indexOf("/") ?? rest.size
|
|
||||||
let q = rest.indexOf("?") ?? rest.size
|
|
||||||
let end = if (slash < q) { slash } else { q }
|
|
||||||
let host = rest[0..end]
|
|
||||||
if (!host.isEmpty()) {
|
|
||||||
tls.serverName = Some(host)
|
|
||||||
}
|
|
||||||
case None => ()
|
|
||||||
}
|
|
||||||
builder.tlsConfig(tls)
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
let request = HttpRequestMessage(NetHttpMethod.Post, url)
|
let request = HttpRequestBuilder().
|
||||||
request.content = JsonContent.create("{}")
|
post().
|
||||||
|
url(url).
|
||||||
|
header("Content-Type", "application/json").
|
||||||
|
body("{}").
|
||||||
|
build()
|
||||||
let response = http.send(request)
|
let response = http.send(request)
|
||||||
try {
|
try {
|
||||||
response.ensureSuccessStatusCode()
|
SimApiError.errorWhenFalse(isSuccess(response.status), code: Int64(response.status),
|
||||||
let resp = response.content.readFromJson<SimApiBaseResponse>()
|
message: "HTTP ERROR: ${response.status}")
|
||||||
SimApiError.errorWhen(resp._code != 200, code: 400, message: "签名验证失败")
|
let json = readBodyText(response.body)
|
||||||
|
let resp = JsonSerializer.Deserialize<SimApiBaseResponse>(json)
|
||||||
|
SimApiError.errorWhen(resp.code != 200, code: 400, message: "签名验证失败")
|
||||||
} finally {
|
} finally {
|
||||||
response.close()
|
response.close()
|
||||||
}
|
}
|
||||||
@@ -72,6 +65,42 @@ public class SimApiAuthCenter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 构建 TLS 配置:信任所有证书 + SNI 域名
|
||||||
|
private static func buildTlsConfig(server: String): TlsClientConfig {
|
||||||
|
var tls = TlsClientConfig()
|
||||||
|
tls.verifyMode = CertificateVerifyMode.TrustAll
|
||||||
|
match (server.indexOf("://")) {
|
||||||
|
case Some(i) =>
|
||||||
|
let rest = server[i + 3..]
|
||||||
|
let slash = rest.indexOf("/") ?? rest.size
|
||||||
|
let q = rest.indexOf("?") ?? rest.size
|
||||||
|
let end = if (slash < q) { slash } else { q }
|
||||||
|
let host = rest[0..end]
|
||||||
|
if (!host.isEmpty()) {
|
||||||
|
tls.serverName = Some(host)
|
||||||
|
}
|
||||||
|
case None => ()
|
||||||
|
}
|
||||||
|
tls
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 2xx 视为成功
|
||||||
|
private static func isSuccess(status: UInt16): Bool {
|
||||||
|
status >= 200 && status < 300
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取响应体 InputStream 为字符串
|
||||||
|
private static func readBodyText(body: InputStream): String {
|
||||||
|
var buffer = Array<Byte>(4096, repeat: 0)
|
||||||
|
var sb = StringBuilder()
|
||||||
|
var read = body.read(buffer)
|
||||||
|
while (read > 0) {
|
||||||
|
sb.appendFromUtf8(buffer.slice(0, read))
|
||||||
|
read = body.read(buffer)
|
||||||
|
}
|
||||||
|
sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 群组相关 =====
|
// ===== 群组相关 =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +119,7 @@ public class SimApiAuthCenter {
|
|||||||
body["keyword"] = keyword
|
body["keyword"] = keyword
|
||||||
body["skip"] = skip
|
body["skip"] = skip
|
||||||
body["take"] = take
|
body["take"] = take
|
||||||
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/group/search", body: SimApiJson.json(Some(body)))
|
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/group/search", body: SimApiUtil.json(Some(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,7 +129,7 @@ public class SimApiAuthCenter {
|
|||||||
var body = HashMap<String, Any>()
|
var body = HashMap<String, Any>()
|
||||||
body["profileId"] = profileId
|
body["profileId"] = profileId
|
||||||
body["groupId"] = groupId
|
body["groupId"] = groupId
|
||||||
_client.signQuery<GroupDetailTreeNode>("/api/auth/group/detail", body: SimApiJson.json(Some(body)))
|
_client.signQuery<GroupDetailTreeNode>("/api/auth/group/detail", body: SimApiUtil.json(Some(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,7 +140,7 @@ public class SimApiAuthCenter {
|
|||||||
body["groupId"] = groupId
|
body["groupId"] = groupId
|
||||||
body["profileId"] = profileId
|
body["profileId"] = profileId
|
||||||
_client.signQuery<Array<String>>("/api/auth/internal/group/related-group-ids",
|
_client.signQuery<Array<String>>("/api/auth/internal/group/related-group-ids",
|
||||||
body: SimApiJson.json(Some(body)))
|
body: SimApiUtil.json(Some(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Profile 相关 =====
|
// ===== Profile 相关 =====
|
||||||
@@ -124,7 +153,7 @@ public class SimApiAuthCenter {
|
|||||||
body["keyword"] = keyword
|
body["keyword"] = keyword
|
||||||
body["skip"] = skip
|
body["skip"] = skip
|
||||||
body["take"] = take
|
body["take"] = take
|
||||||
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/profile/search", body: SimApiJson.json(Some(body)))
|
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/profile/search", body: SimApiUtil.json(Some(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,7 +166,7 @@ public class SimApiAuthCenter {
|
|||||||
arr.add(id)
|
arr.add(id)
|
||||||
}
|
}
|
||||||
body["ids"] = arr.toArray()
|
body["ids"] = arr.toArray()
|
||||||
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/profile/list", body: SimApiJson.json(Some(body)))
|
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/profile/list", body: SimApiUtil.json(Some(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== AuthGate 内部应用专用 =====
|
// ===== AuthGate 内部应用专用 =====
|
||||||
@@ -149,7 +178,7 @@ public class SimApiAuthCenter {
|
|||||||
var body = HashMap<String, Any>()
|
var body = HashMap<String, Any>()
|
||||||
body["ProfileId"] = profileId
|
body["ProfileId"] = profileId
|
||||||
body["AppId"] = applicationId
|
body["AppId"] = applicationId
|
||||||
_client.signQuery<Bool>("/api/auth/internal/app/check-owner", body: SimApiJson.json(Some(body)))
|
_client.signQuery<Bool>("/api/auth/internal/app/check-owner", body: SimApiUtil.json(Some(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,7 +193,7 @@ public class SimApiAuthCenter {
|
|||||||
}
|
}
|
||||||
body["AllowedAppIds"] = arr.toArray()
|
body["AllowedAppIds"] = arr.toArray()
|
||||||
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/internal/app/related",
|
_client.signQuery<Array<AppAndProfileItem>>("/api/auth/internal/app/related",
|
||||||
body: SimApiJson.json(Some(body)))
|
body: SimApiUtil.json(Some(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 系统登录 =====
|
// ===== 系统登录 =====
|
||||||
@@ -182,7 +211,7 @@ public class SimApiAuthCenter {
|
|||||||
if (let Some(scene) <- scene) { body["scene"] = scene }
|
if (let Some(scene) <- scene) { body["scene"] = scene }
|
||||||
if (let Some(data) <- data) { body["data"] = data }
|
if (let Some(data) <- data) { body["data"] = data }
|
||||||
if (let Some(backUrl) <- backUrl) { body["backUrl"] = backUrl }
|
if (let Some(backUrl) <- backUrl) { body["backUrl"] = backUrl }
|
||||||
let code = _client.signQuery<String>("/api/auth/login/code", body: SimApiJson.json(Some(body)))
|
let code = _client.signQuery<String>("/api/auth/login/code", body: SimApiUtil.json(Some(body)))
|
||||||
let server = _client.server
|
let server = _client.server
|
||||||
GetCodeResponse(code, server, "${server}/auth?code=${code}")
|
GetCodeResponse(code, server, "${server}/auth?code=${code}")
|
||||||
}
|
}
|
||||||
@@ -193,8 +222,10 @@ public class SimApiAuthCenter {
|
|||||||
public func getLoginInfo(code: String, scene!: ?String = None): LoginInfoResponse {
|
public func getLoginInfo(code: String, scene!: ?String = None): LoginInfoResponse {
|
||||||
var body = HashMap<String, Any>()
|
var body = HashMap<String, Any>()
|
||||||
body["code"] = code
|
body["code"] = code
|
||||||
let resp = _client.signQuery<LoginInfoResponse>("/api/auth/login/get", body: SimApiJson.json(Some(body)))
|
// 说明:C# 的 ErrorWhenNull(resp, 400232, "登录信息获取失败") 对应 signQuery 内部 data.getOrThrow() 的
|
||||||
SimApiError.errorWhen(resp._scene != scene, code: 403003, message: "登录场景不匹配")
|
// None 分支;仓颉版 signQuery 返回非空 T(data 缺失即抛异常),故此处无需重复判空。
|
||||||
|
let resp = _client.signQuery<LoginInfoResponse>("/api/auth/login/get", body: SimApiUtil.json(Some(body)))
|
||||||
|
SimApiError.errorWhen(resp.scene != scene, code: 403003, message: "登录场景不匹配")
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +241,7 @@ public class SimApiAuthCenter {
|
|||||||
if (let Some(data) <- data) { body["data"] = data }
|
if (let Some(data) <- data) { body["data"] = data }
|
||||||
if (let Some(backUrl) <- backUrl) { body["backUrl"] = backUrl }
|
if (let Some(backUrl) <- backUrl) { body["backUrl"] = backUrl }
|
||||||
body["profileId"] = userId
|
body["profileId"] = userId
|
||||||
let code = _client.signQuery<String>("/api/auth/confirm/code", body: SimApiJson.json(Some(body)))
|
let code = _client.signQuery<String>("/api/auth/confirm/code", body: SimApiUtil.json(Some(body)))
|
||||||
let server = _client.server
|
let server = _client.server
|
||||||
GetCodeResponse(code, server, "${server}/confirm?code=${code}")
|
GetCodeResponse(code, server, "${server}/confirm?code=${code}")
|
||||||
}
|
}
|
||||||
@@ -221,14 +252,14 @@ public class SimApiAuthCenter {
|
|||||||
public func confirm(code: String, scene: String, userId!: ?String = None): ConfirmResponse {
|
public func confirm(code: String, scene: String, userId!: ?String = None): ConfirmResponse {
|
||||||
var body = HashMap<String, Any>()
|
var body = HashMap<String, Any>()
|
||||||
body["code"] = code
|
body["code"] = code
|
||||||
let resp = _client.signQuery<ConfirmResponse>("/api/auth/confirm/get", body: SimApiJson.json(Some(body)))
|
let resp = _client.signQuery<ConfirmResponse>("/api/auth/confirm/get", body: SimApiUtil.json(Some(body)))
|
||||||
SimApiError.errorWhen(userId != Some(resp._profileId), code: 403002, message: "安全确认身份不匹配")
|
SimApiError.errorWhen(userId != Some(resp.profileId), code: 403002, message: "安全确认身份不匹配")
|
||||||
SimApiError.errorWhen(resp._scene != scene, code: 403003, message: "安全确认场景不匹配")
|
SimApiError.errorWhen(resp.scene != scene, code: 403003, message: "安全确认场景不匹配")
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 简单单字段请求体:{"field":"value"}
|
/// 简单单字段请求体:{"field":"value"}
|
||||||
private static func simpleBody(field: String, value: String): String {
|
private static func simpleBody(field: String, value: String): String {
|
||||||
"{\"${field}\":\"${SimApiJson.escapeJson(value)}\"}"
|
"{\"${field}\":\"${SimApiUtil.escapeJson(value)}\"}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
* AuthSDK/SimApiAuthCenterMiddleware:网关透传认证中间件。
|
* AuthSDK/SimApiAuthCenterMiddleware:网关透传认证中间件。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.authsdk
|
package simcu::simapi.authsdk
|
||||||
|
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.configurations.*
|
import simcu::simapi.configurations.*
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 网关透传认证中间件(对齐 C# SimApiAuthCenterMiddleware):
|
* 网关透传认证中间件(对齐 C# SimApiAuthCenterMiddleware):
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
* AuthSDK/SimApiAuthClient:认证中心专用签名客户端。
|
* AuthSDK/SimApiAuthClient:认证中心专用签名客户端。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.authsdk
|
package simcu::simapi.authsdk
|
||||||
|
|
||||||
import simapi.configurations.*
|
import simcu::simapi.configurations.*
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证中心签名客户端(对齐 C# SimApiAuthClient):
|
* 认证中心签名客户端(对齐 C# SimApiAuthClient):
|
||||||
|
|||||||
@@ -2,121 +2,108 @@
|
|||||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
* AuthSDK 用到的 DTO(对齐 C# SimApiAuthCenterDto / SimApiAuthIamDto)。
|
* AuthSDK 用到的 DTO(对齐 C# SimApiAuthCenterDto / SimApiAuthIamDto)。
|
||||||
|
*
|
||||||
|
* 说明:C# 中这些 DTO 是 SimApiAuthCenterDto / SimApiAuthIamDto 的嵌套类;
|
||||||
|
* 仓颉不支持在类体内声明嵌套类(unexpected class declaration in class body),
|
||||||
|
* 故拍平为顶层类,语义与字段保持一致。
|
||||||
|
*
|
||||||
|
* 序列化/反序列化由 simapi_serialization 反射处理(免标注、免约束)。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.authsdk
|
package simcu::simapi.authsdk
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import stdx.encoding.json.*
|
|
||||||
import soulsoft_serialization.*
|
|
||||||
import soulsoft_serialization.macros.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用/Profile 通用项(对齐 C# AppAndProfileItem)。
|
* 应用/Profile 通用项(对齐 C# AppAndProfileItem)。
|
||||||
*/
|
*/
|
||||||
@Serialization
|
|
||||||
public class AppAndProfileItem {
|
public class AppAndProfileItem {
|
||||||
public var _id: String = ""
|
public var id: String = ""
|
||||||
public var _name: String = ""
|
public var name: String = ""
|
||||||
public var _image: ?String = None
|
public var image: ?String = None
|
||||||
public var _description: ?String = None
|
public var description: ?String = None
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安全确认响应(对齐 C# ConfirmResponse)。
|
* 安全确认响应(对齐 C# ConfirmResponse)。
|
||||||
|
* data 用 ?HashMap<String, Any> 对齐 C# Dictionary<string,object>?(任意 JSON 对象)。
|
||||||
*/
|
*/
|
||||||
@Serialization
|
|
||||||
public class ConfirmResponse {
|
public class ConfirmResponse {
|
||||||
public var _applicationId: String = ""
|
public var applicationId: String = ""
|
||||||
public var _profileId: String = ""
|
public var profileId: String = ""
|
||||||
public var _scene: ?String = None
|
public var scene: ?String = None
|
||||||
public var _data: ?JsonValue = None
|
public var data: ?HashMap<String, Any> = None
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录信息响应(对齐 C# LoginInfoResponse)。
|
* 登录信息响应(对齐 C# LoginInfoResponse)。
|
||||||
*/
|
*/
|
||||||
@Serialization
|
|
||||||
public class LoginInfoResponse {
|
public class LoginInfoResponse {
|
||||||
public var _scene: ?String = None
|
public var scene: ?String = None
|
||||||
public var _data: ?JsonValue = None
|
public var data: ?HashMap<String, Any> = None
|
||||||
public var _profileId: String = ""
|
public var profileId: String = ""
|
||||||
public var _name: String = ""
|
public var name: String = ""
|
||||||
public var _image: ?String = None
|
public var image: ?String = None
|
||||||
public var _description: ?String = None
|
public var description: ?String = None
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取授权码响应(对齐 C# GetCodeResponse)。
|
* 获取授权码响应(对齐 C# GetCodeResponse)。
|
||||||
*/
|
*/
|
||||||
@Serialization
|
|
||||||
public class GetCodeResponse {
|
public class GetCodeResponse {
|
||||||
public var _code: String = ""
|
public var code: String = ""
|
||||||
public var _server: String = ""
|
public var server: String = ""
|
||||||
public var _fullUrl: String = ""
|
public var fullUrl: String = ""
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public init(code: String, server: String, fullUrl: String) {
|
public init(code: String, server: String, fullUrl: String) {
|
||||||
this._code = code
|
this.code = code
|
||||||
this._server = server
|
this.server = server
|
||||||
this._fullUrl = fullUrl
|
this.fullUrl = fullUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 群组关联项(对齐 C# GroupRelatedItem)。
|
* 群组关联项(对齐 C# GroupRelatedItem)。
|
||||||
*/
|
*/
|
||||||
@Serialization
|
|
||||||
public class GroupRelatedItem {
|
public class GroupRelatedItem {
|
||||||
public var _id: String = ""
|
public var id: String = ""
|
||||||
public var _name: String = ""
|
public var name: String = ""
|
||||||
public var _image: ?String = None
|
public var image: ?String = None
|
||||||
public var _description: ?String = None
|
public var description: ?String = None
|
||||||
public var _isOwner: Bool = false
|
public var isOwner: Bool = false
|
||||||
public var _isAdmin: Bool = false
|
public var isAdmin: Bool = false
|
||||||
public var _isMember: Bool = false
|
public var isMember: Bool = false
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 群组详情树节点(对齐 C# GroupDetailTreeNode,children 递归)。
|
* 群组详情树节点(对齐 C# GroupDetailTreeNode,children 递归)。
|
||||||
*/
|
*/
|
||||||
@Serialization
|
|
||||||
public class GroupDetailTreeNode {
|
public class GroupDetailTreeNode {
|
||||||
public var _id: String = ""
|
public var id: String = ""
|
||||||
public var _name: String = ""
|
public var name: String = ""
|
||||||
public var _image: ?String = None
|
public var image: ?String = None
|
||||||
public var _description: ?String = None
|
public var description: ?String = None
|
||||||
public var _sort: Int64 = 0
|
public var sort: Int64 = 0
|
||||||
public var _children: Array<GroupDetailTreeNode> = []
|
public var children: Array<GroupDetailTreeNode> = []
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限项(对齐 C# PermissionItem)。
|
* 权限项(对齐 C# PermissionItem)。
|
||||||
*/
|
*/
|
||||||
@Serialization
|
|
||||||
public class PermissionItem {
|
public class PermissionItem {
|
||||||
public var _identifier: String = ""
|
public var identifier: String = ""
|
||||||
public var _name: String = ""
|
public var name: String = ""
|
||||||
public var _group: String = ""
|
public var group: String = ""
|
||||||
public var _description: String = ""
|
public var description: String = ""
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public init(identifier: String, name: String, group: String, description: String) {
|
public init(identifier: String, name: String, group: String, description: String) {
|
||||||
this._identifier = identifier
|
this.identifier = identifier
|
||||||
this._name = name
|
this.name = name
|
||||||
this._group = group
|
this.group = group
|
||||||
this._description = description
|
this.description = description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
* AuthSDK/SimApiAuthIam:权限中心远程 SDK。
|
* AuthSDK/SimApiAuthIam:权限中心远程 SDK。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.authsdk
|
package simcu::simapi.authsdk
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限中心远程 SDK(对齐 C# SimApiAuthIam):
|
* 权限中心远程 SDK(对齐 C# SimApiAuthIam):
|
||||||
@@ -29,15 +29,15 @@ public class SimApiAuthIam {
|
|||||||
var items = ArrayList<Any>()
|
var items = ArrayList<Any>()
|
||||||
for (p in permissions) {
|
for (p in permissions) {
|
||||||
var item = HashMap<String, Any>()
|
var item = HashMap<String, Any>()
|
||||||
item["identifier"] = p._identifier
|
item["identifier"] = p.identifier
|
||||||
item["name"] = p._name
|
item["name"] = p.name
|
||||||
item["group"] = p._group
|
item["group"] = p.group
|
||||||
item["description"] = p._description
|
item["description"] = p.description
|
||||||
items.add(item)
|
items.add(item)
|
||||||
}
|
}
|
||||||
var body = HashMap<String, Any>()
|
var body = HashMap<String, Any>()
|
||||||
body["permissions"] = items.toArray()
|
body["permissions"] = items.toArray()
|
||||||
_client.signQuery<String>("/api/iam/permission/register", body: SimApiJson.json(Some(body)))
|
_client.signQuery<String>("/api/iam/permission/register", body: SimApiUtil.json(Some(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +49,7 @@ public class SimApiAuthIam {
|
|||||||
if (let Some(groupId) <- groupId) {
|
if (let Some(groupId) <- groupId) {
|
||||||
body["groupId"] = groupId
|
body["groupId"] = groupId
|
||||||
}
|
}
|
||||||
_client.signQuery<Array<String>>("/api/iam/permission/owned", body: SimApiJson.json(Some(body)))
|
_client.signQuery<Array<String>>("/api/iam/permission/owned", body: SimApiUtil.json(Some(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,7 +62,7 @@ public class SimApiAuthIam {
|
|||||||
if (let Some(groupId) <- groupId) {
|
if (let Some(groupId) <- groupId) {
|
||||||
body["groupId"] = groupId
|
body["groupId"] = groupId
|
||||||
}
|
}
|
||||||
let ok = _client.signQuery<Bool>("/api/iam/permission/check", body: SimApiJson.json(Some(body)))
|
let ok = _client.signQuery<Bool>("/api/iam/permission/check", body: SimApiUtil.json(Some(body)))
|
||||||
SimApiError.errorWhen(!ok, code: 403, message: "没有该权限")
|
SimApiError.errorWhen(!ok, code: 403, message: "没有该权限")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
* 通用请求 DTO(对齐 C# Communications/SimApiBaseRequest.cs)。
|
||||||
|
*
|
||||||
|
* 说明:这些类仅作反序列化目标(FromJson<T> / @FromBody),无参构造由编译器自动提供;
|
||||||
|
* 不声明显式构造器。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.communications
|
package simcu::simapi.communications
|
||||||
|
|
||||||
/**
|
|
||||||
* 基础请求 DTO。
|
|
||||||
*/
|
|
||||||
public class SimApiBaseRequest {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅包含 Id 的请求。
|
* 仅包含 Id 的请求。
|
||||||
*/
|
*/
|
||||||
|
public class SimApiIdOnlyRequest {
|
||||||
|
public var id: Int64 = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅包含 Id 的请求(字符串)。
|
||||||
|
*/
|
||||||
public class SimApiStringIdOnlyRequest {
|
public class SimApiStringIdOnlyRequest {
|
||||||
public var id: String = ""
|
public var id: String = ""
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public init(id: String) {
|
|
||||||
this.id = id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,11 +43,4 @@ public class SimApiOneFieldRequest<T> {
|
|||||||
public class SimApiBasePageRequest {
|
public class SimApiBasePageRequest {
|
||||||
public var page: Int64 = 1
|
public var page: Int64 = 1
|
||||||
public var count: Int64 = 20
|
public var count: Int64 = 20
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public init(page: Int64, count: Int64) {
|
|
||||||
this.page = page
|
|
||||||
this.count = count
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,35 +3,32 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.communications
|
package simcu::simapi.communications
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import stdx.encoding.json.*
|
|
||||||
import soulsoft_serialization.*
|
|
||||||
import soulsoft_serialization.macros.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础响应体:所有接口统一返回该结构。
|
* 基础响应体:所有接口统一返回该结构。
|
||||||
* HTTP 状态码始终 200,业务错误通过 code 字段表达。
|
* HTTP 状态码始终 200,业务错误通过 code 字段表达。
|
||||||
|
* 序列化/反序列化由 simapi_serialization 反射处理(无需接口/宏)。
|
||||||
*/
|
*/
|
||||||
@Serialization
|
public class SimApiBaseResponse {
|
||||||
public open class SimApiBaseResponse {
|
public var code: Int64 = 200
|
||||||
public var _code: Int64 = 200
|
public var message: String = "成功"
|
||||||
public var _message: String = "成功"
|
|
||||||
|
|
||||||
public init(code: Int64, message: String) {
|
public init(code: Int64, message: String) {
|
||||||
this._code = code
|
this.code = code
|
||||||
this._message = message
|
this.message = message
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(code: Int64) {
|
public init(code: Int64) {
|
||||||
this._code = code
|
this.code = code
|
||||||
this._message = getDefaultMessage(code)
|
this.message = getDefaultMessage(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
this._code = 200
|
this.code = 200
|
||||||
this._message = "成功"
|
this.message = "成功"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,112 +46,56 @@ public open class SimApiBaseResponse {
|
|||||||
case _ => "未知错误代码"
|
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 列表元素类型。
|
* @param T 列表元素类型。
|
||||||
* 说明:泛型类暂不通过 @Serialization 宏序列化,可手动转换为 SimApiResponse。
|
* 说明:泛型类暂不通过反射序列化,可手动转换为 SimApiResponse。
|
||||||
*/
|
*/
|
||||||
public class PageResponse<T> {
|
public class PageResponse<T> {
|
||||||
public var _list: Array<T> = Array<T>()
|
public var list: Array<T> = Array<T>()
|
||||||
public var _page: Int64 = 1
|
public var page: Int64 = 1
|
||||||
public var _count: Int64 = 20
|
public var count: Int64 = 20
|
||||||
public var _total: Int64 = 0
|
public var total: Int64 = 0
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public init(list: Array<T>, page: Int64, count: Int64, total: Int64) {
|
public init(list: Array<T>, page: Int64, count: Int64, total: Int64) {
|
||||||
this._list = list
|
this.list = list
|
||||||
this._page = page
|
this.page = page
|
||||||
this._count = count
|
this.count = count
|
||||||
this._total = total
|
this.total = total
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 带数据的动态响应。
|
* 带数据的动态响应。
|
||||||
* @param T 数据类型。
|
* @param T 数据类型。
|
||||||
* 说明:泛型类手写实现 ISerialization<SimApiResponse<T>>(serialize + deserialize),
|
* 序列化/反序列化由 simapi_serialization 反射处理:
|
||||||
* data 内嵌为对象(对齐 C# SimApiBaseResponse<T>.Data 是 T? 而非字符串)。
|
* - code/message/data 为普通字段,反射递归
|
||||||
* 序列化支持动态结构(HashMap<String,Any> 等经 SimApiJson 内嵌);反序列化要求 T <: ISerialization<T>。
|
* - data 支持任意 T(免约束)
|
||||||
|
* 注:泛型嵌套(SimApiResponse<T>)的反射反序列化已验证可用。
|
||||||
*/
|
*/
|
||||||
public class SimApiResponse<T> <: SimApiBaseResponse & ISerialization<SimApiResponse<T>> where T <: ISerialization<T> {
|
public class SimApiResponse<T> {
|
||||||
public var _data: ?T = None
|
public var code: Int64 = 200
|
||||||
|
public var message: String = "成功"
|
||||||
|
public var data: ?T = None
|
||||||
|
|
||||||
public init() {
|
public init() {}
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(data: T) {
|
public init(data: T) {
|
||||||
super()
|
this.data = Some(data)
|
||||||
this._data = Some(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(code: Int64, message: String) {
|
public init(code: Int64, message: String) {
|
||||||
super(code, message)
|
this.code = code
|
||||||
|
this.message = message
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(code: Int64, message: String, data: T) {
|
public init(code: Int64, message: String, data: T) {
|
||||||
super(code, message)
|
this.code = code
|
||||||
this._data = Some(data)
|
this.message = message
|
||||||
}
|
this.data = Some(data)
|
||||||
|
|
||||||
/**
|
|
||||||
* 序列化为 DataModel:code + message + data(data 为对象)。
|
|
||||||
*/
|
|
||||||
public override func serializeObject(options: JsonSerializerOptions): DataModel {
|
|
||||||
let dms = DataModelStruct()
|
|
||||||
dms.add(Field("code", DataModelInt(_code)))
|
|
||||||
dms.add(Field("message", DataModelString(_message)))
|
|
||||||
if (let Some(data) <- _data) {
|
|
||||||
if (let ser: ISerializable <- data) {
|
|
||||||
dms.add(Field("data", ser.serializeObject(options)))
|
|
||||||
} else {
|
|
||||||
// 动态结构(HashMap<String, Any> 等):经 SimApiJson 序列化后解析内嵌为对象
|
|
||||||
let json = SimApiJson.json(Some(data))
|
|
||||||
dms.add(Field("data", DataModel.fromJson(JsonValue.fromStr(json))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dms
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 DataModel 反序列化:code + message + data(对齐 .NET JsonSerializer.Deserialize<T>)。
|
|
||||||
*/
|
|
||||||
public static func deserializeObject(dm: DataModel, options: JsonSerializerOptions): SimApiResponse<T> {
|
|
||||||
let resp = SimApiResponse<T>()
|
|
||||||
if (let dms: DataModelStruct <- dm) {
|
|
||||||
for (field in dms.getFields()) {
|
|
||||||
match (field.getName()) {
|
|
||||||
case "code" =>
|
|
||||||
if (let v: DataModelInt <- field.getData()) {
|
|
||||||
resp._code = v.getValue()
|
|
||||||
}
|
|
||||||
case "message" =>
|
|
||||||
if (let v: DataModelString <- field.getData()) {
|
|
||||||
resp._message = v.getValue()
|
|
||||||
}
|
|
||||||
case "data" =>
|
|
||||||
resp._data = Some(T.deserializeObject(field.getData(), options))
|
|
||||||
case _ => ()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,7 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.communications
|
package simcu::simapi.communications
|
||||||
|
|
||||||
import std.collection.*
|
|
||||||
import stdx.encoding.json.*
|
|
||||||
import soulsoft_serialization.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 带任意对象数据的响应(非泛型版,供响应自动封装使用)。
|
* 带任意对象数据的响应(非泛型版,供响应自动封装使用)。
|
||||||
@@ -15,74 +11,29 @@ import soulsoft_serialization.*
|
|||||||
* 与 SimApiResponse<T> 的区别:data 为 Any(运行时类型不定),用于
|
* 与 SimApiResponse<T> 的区别:data 为 Any(运行时类型不定),用于
|
||||||
* SimApiRequestDelegateFactory 派发结果时统一包装 DTO/数组/动态结构。
|
* SimApiRequestDelegateFactory 派发结果时统一包装 DTO/数组/动态结构。
|
||||||
*
|
*
|
||||||
* 序列化规则(对齐 SimApiResponse<T>):
|
* 序列化/反序列化由 simapi_serialization 反射处理:
|
||||||
* - data 实现了 ISerializable(@Serialization DTO、Array<T> 等)→ data 内嵌为对象
|
* - code/message/data 为普通字段,反射递归
|
||||||
* - data 为动态结构(如 HashMap<String, Any>,不满足泛型 ISerialization 约束)
|
* - data 为动态结构(HashMap<String, Any>)同样反射支持
|
||||||
* → 经 SimApiJson 序列化后解析内嵌为对象(而非字符串)
|
|
||||||
* 因此 data 在 JSON 中始终是对象/数组/标量,不会是"JSON 字符串"。
|
|
||||||
*/
|
*/
|
||||||
public class SimApiDataResponse <: SimApiBaseResponse & ISerialization<SimApiDataResponse> {
|
public class SimApiDataResponse {
|
||||||
public var _data: ?Any = None
|
public var code: Int64 = 200
|
||||||
|
public var message: String = "成功"
|
||||||
|
public var data: ?Any = None
|
||||||
|
|
||||||
public init() {
|
public init() {}
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(data: Any) {
|
public init(data: Any) {
|
||||||
super()
|
this.data = Some(data)
|
||||||
this._data = Some(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(code: Int64, message: String) {
|
public init(code: Int64, message: String) {
|
||||||
super(code, message)
|
this.code = code
|
||||||
|
this.message = message
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(code: Int64, message: String, data: Any) {
|
public init(code: Int64, message: String, data: Any) {
|
||||||
super(code, message)
|
this.code = code
|
||||||
this._data = Some(data)
|
this.message = message
|
||||||
}
|
this.data = Some(data)
|
||||||
|
|
||||||
/**
|
|
||||||
* 序列化为 DataModel:code + message + data(data 为对象)。
|
|
||||||
*/
|
|
||||||
public override func serializeObject(options: JsonSerializerOptions): DataModel {
|
|
||||||
let dms = DataModelStruct()
|
|
||||||
dms.add(Field("code", DataModelInt(_code)))
|
|
||||||
dms.add(Field("message", DataModelString(_message)))
|
|
||||||
if (let Some(data) <- _data) {
|
|
||||||
if (let ser: ISerializable <- data) {
|
|
||||||
dms.add(Field("data", ser.serializeObject(options)))
|
|
||||||
} else {
|
|
||||||
// 动态结构(HashMap<String, Any> 等):经 SimApiJson 序列化后解析内嵌为对象
|
|
||||||
let json = SimApiJson.json(Some(data))
|
|
||||||
dms.add(Field("data", DataModel.fromJson(JsonValue.fromStr(json))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dms
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 DataModel 反序列化:code + message + data(data 保留原始 DataModel)。
|
|
||||||
*/
|
|
||||||
public static func deserializeObject(dm: DataModel, options: JsonSerializerOptions): SimApiDataResponse {
|
|
||||||
let resp = SimApiDataResponse()
|
|
||||||
if (let dms: DataModelStruct <- dm) {
|
|
||||||
for (field in dms.getFields()) {
|
|
||||||
match (field.getName()) {
|
|
||||||
case "code" =>
|
|
||||||
if (let v: DataModelInt <- field.getData()) {
|
|
||||||
resp._code = v.getValue()
|
|
||||||
}
|
|
||||||
case "message" =>
|
|
||||||
if (let v: DataModelString <- field.getData()) {
|
|
||||||
resp._message = v.getValue()
|
|
||||||
}
|
|
||||||
case "data" =>
|
|
||||||
resp._data = Some(field.getData())
|
|
||||||
case _ => ()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
|
||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
|
||||||
* JSON 序列化统一工具(对应 C# SimApiUtil.Json)。
|
|
||||||
*/
|
|
||||||
|
|
||||||
package simapi.communications
|
|
||||||
|
|
||||||
import std.collection.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON 序列化静态工具类。
|
|
||||||
*
|
|
||||||
* 说明:序列化核心放在依赖图最底层的 simapi.communications 包,
|
|
||||||
* SimApiUtil.json(simapi.helpers)委托本类实现,避免循环依赖;
|
|
||||||
* 全框架 JSON 输出统一走此处,保证转义与格式一致。
|
|
||||||
*/
|
|
||||||
public class SimApiJson {
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对象序列化为 JSON 字符串(统一入口)。
|
|
||||||
* 支持 String/Int64/Bool/Float64/Array/HashMap,其他类型退化为字符串。
|
|
||||||
* @param obj 任意对象(None 输出 null)。
|
|
||||||
*/
|
|
||||||
public static func json(obj: ?Any): String {
|
|
||||||
if (let Some(obj) <- obj) {
|
|
||||||
return jsonValue(obj)
|
|
||||||
}
|
|
||||||
"null"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON 字符串转义(统一入口)。
|
|
||||||
* @param s 原始字符串。
|
|
||||||
* @return 转义后可直接放入 JSON 字符串字面量的内容。
|
|
||||||
*/
|
|
||||||
public static func escapeJson(s: String): String {
|
|
||||||
var sb = StringBuilder()
|
|
||||||
for (c in s.runes()) {
|
|
||||||
match (c) {
|
|
||||||
case '"' => sb.append("\\\"")
|
|
||||||
case '\\' => sb.append("\\\\")
|
|
||||||
case '\n' => sb.append("\\n")
|
|
||||||
case '\r' => sb.append("\\r")
|
|
||||||
case '\t' => sb.append("\\t")
|
|
||||||
case _ => sb.append(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func jsonValue(obj: Any): String {
|
|
||||||
if (let s: String <- obj) {
|
|
||||||
return "\"${escapeJson(s)}\""
|
|
||||||
}
|
|
||||||
if (let i: Int64 <- obj) {
|
|
||||||
return "${i}"
|
|
||||||
}
|
|
||||||
if (let b: Bool <- obj) {
|
|
||||||
return "${b}"
|
|
||||||
}
|
|
||||||
if (let f: Float64 <- obj) {
|
|
||||||
return "${f}"
|
|
||||||
}
|
|
||||||
if (let arr: Array<Any> <- obj) {
|
|
||||||
var sb = StringBuilder()
|
|
||||||
sb.append("[")
|
|
||||||
var first = true
|
|
||||||
for (item in arr) {
|
|
||||||
if (!first) { sb.append(",") }
|
|
||||||
sb.append(jsonValue(item))
|
|
||||||
first = false
|
|
||||||
}
|
|
||||||
sb.append("]")
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
if (let map: HashMap<String, Any> <- obj) {
|
|
||||||
var sb = StringBuilder()
|
|
||||||
sb.append("{")
|
|
||||||
var first = true
|
|
||||||
for ((key, value) in map) {
|
|
||||||
if (!first) { sb.append(",") }
|
|
||||||
sb.append("\"${escapeJson(key)}\":${jsonValue(value)}")
|
|
||||||
first = false
|
|
||||||
}
|
|
||||||
sb.append("}")
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
// 其他类型退化为字符串
|
|
||||||
return "\"${escapeJson(describe(obj))}\""
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func describe(obj: Any): String {
|
|
||||||
if (let s: ToString <- obj) {
|
|
||||||
return s.toString()
|
|
||||||
}
|
|
||||||
"null"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,164 +4,36 @@
|
|||||||
* Communications/SimApiLoginItem:登录信息项。
|
* Communications/SimApiLoginItem:登录信息项。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.communications
|
package simcu::simapi.communications
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import soulsoft_serialization.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录信息项:Token 认证通过后注入请求上下文。
|
* 登录信息项:Token 认证通过后注入请求上下文。
|
||||||
* 对齐 C# SimApiLoginItem(Id / Type / Meta / Extra,camelCase 输出 id/type/meta/extra)。
|
* 对齐 C# SimApiLoginItem(Id / Type / Meta / Extra)。
|
||||||
*
|
*
|
||||||
* 说明:因 _extra 为 HashMap<String, Any>(Any 不满足 soulsoft 的 ISerialization<V> 约束,
|
* 序列化/反序列化由 simapi_serialization 反射处理(免标注、免约束),
|
||||||
* 无法用 @Serialization 宏),故手动实现 ISerialization<SimApiLoginItem>(serialize + deserialize);
|
* 字段 id/types/meta/extra 全部为受支持类型(String/Array/HashMap)。
|
||||||
* 调用方统一通过 JsonSerializer.serializeObject<T>() / deserializeObject<T>() 使用
|
|
||||||
* (对齐 .NET JsonSerializer.Serialize / Deserialize)。
|
|
||||||
*/
|
*/
|
||||||
public class SimApiLoginItem <: ISerialization<SimApiLoginItem> {
|
public class SimApiLoginItem {
|
||||||
public var _id: String = ""
|
public var id: String = ""
|
||||||
public var _types: Array<String> = []
|
public var types: Array<String> = ["user"]
|
||||||
public var _meta: HashMap<String, String> = HashMap<String, String>()
|
public var meta: HashMap<String, String> = HashMap<String, String>()
|
||||||
public var _extra: HashMap<String, Any> = HashMap<String, Any>()
|
public var extra: HashMap<String, Any> = HashMap<String, Any>()
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public init(id: String) {
|
public init(id: String) {
|
||||||
this._id = id
|
this.id = id
|
||||||
this._types = ["user"]
|
this.types = ["user"]
|
||||||
this._meta = HashMap<String, String>()
|
this.meta = HashMap<String, String>()
|
||||||
this._extra = HashMap<String, Any>()
|
this.extra = HashMap<String, Any>()
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(id: String, types: Array<String>) {
|
public init(id: String, types: Array<String>) {
|
||||||
this._id = id
|
this.id = id
|
||||||
this._types = types
|
this.types = types
|
||||||
this._meta = HashMap<String, String>()
|
this.meta = HashMap<String, String>()
|
||||||
this._extra = HashMap<String, Any>()
|
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 对齐 C# 的 Configurations/SimApiAuthCenterOptions.cs。
|
* 对齐 C# 的 Configurations/SimApiAuthCenterOptions.cs。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.configurations
|
package simcu::simapi.configurations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证中心配置(对齐 C# SimApiAuthCenterOptions)。
|
* 认证中心配置(对齐 C# SimApiAuthCenterOptions)。
|
||||||
@@ -36,5 +36,4 @@ public class SimApiAuthCenterOptions {
|
|||||||
*/
|
*/
|
||||||
public var useIam: Bool = false
|
public var useIam: Bool = false
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 对齐 C# 的 Configurations/SimApiDocOptions.cs。
|
* 对齐 C# 的 Configurations/SimApiDocOptions.cs。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.configurations
|
package simcu::simapi.configurations
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
|
|
||||||
@@ -55,8 +55,6 @@ public class SimApiAuthOption {
|
|||||||
* 授权范围。
|
* 授权范围。
|
||||||
*/
|
*/
|
||||||
public var scopes: HashMap<String, String> = HashMap<String, String>()
|
public var scopes: HashMap<String, String> = HashMap<String, String>()
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 对齐 C# 的 Configurations/SimApiExceptionOptions.cs。
|
* 对齐 C# 的 Configurations/SimApiExceptionOptions.cs。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.configurations
|
package simcu::simapi.configurations
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
|
|
||||||
@@ -17,5 +17,4 @@ public class SimApiExceptionOptions {
|
|||||||
*/
|
*/
|
||||||
public var skipStatusCodes: HashSet<Int64> = HashSet<Int64>([200, 301, 302])
|
public var skipStatusCodes: HashSet<Int64> = HashSet<Int64>([200, 301, 302])
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 对齐 C# 的 Configurations/SimApiHttpClientOptions.cs。
|
* 对齐 C# 的 Configurations/SimApiHttpClientOptions.cs。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.configurations
|
package simcu::simapi.configurations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP 客户端配置。
|
* HTTP 客户端配置。
|
||||||
@@ -13,11 +13,5 @@ public class SimApiHttpClientOptions {
|
|||||||
public var server: String = ""
|
public var server: String = ""
|
||||||
public var appId: String = ""
|
public var appId: String = ""
|
||||||
public var appKey: 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() {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 对齐 C# 的 Configurations/SimApiJobOptions.cs。
|
* 对齐 C# 的 Configurations/SimApiJobOptions.cs。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.configurations
|
package simcu::simapi.configurations
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
|
|
||||||
@@ -21,8 +21,6 @@ public class SimApiJobServer {
|
|||||||
* 工作线程数(默认 5)。
|
* 工作线程数(默认 5)。
|
||||||
*/
|
*/
|
||||||
public var workerNum: Int64 = 5
|
public var workerNum: Int64 = 5
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,20 +4,14 @@
|
|||||||
* 对齐 C# 的 Configurations/SimApiOptions.cs。
|
* 对齐 C# 的 Configurations/SimApiOptions.cs。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.configurations
|
package simcu::simapi.configurations
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.reflect.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SimApi 全局配置:对应 C# 的 SimApi.Configurations.SimApiOptions。
|
* SimApi 全局配置:对应 C# 的 SimApi.Configurations.SimApiOptions。
|
||||||
*/
|
*/
|
||||||
public class SimApiOptions {
|
public class SimApiOptions {
|
||||||
/**
|
|
||||||
* 已注册的 ISimApiAuthChecker 类型列表(由 addSimApi 扫描调用者程序集填充,
|
|
||||||
* 运行时在 @SimApiAuth 鉴权时逐个从 DI 解析执行,对齐 C# AddScoped 扫描)。
|
|
||||||
*/
|
|
||||||
public var authCheckers: ArrayList<TypeInfo> = ArrayList<TypeInfo>()
|
|
||||||
/**
|
/**
|
||||||
* Redis 配置;配置则使用 Redis,不配则自动使用 InMemory。
|
* Redis 配置;配置则使用 Redis,不配则自动使用 InMemory。
|
||||||
*/
|
*/
|
||||||
@@ -118,7 +112,6 @@ public class SimApiOptions {
|
|||||||
public var simApiRouteOptions = SimApiRouteOptions()
|
public var simApiRouteOptions = SimApiRouteOptions()
|
||||||
public var simApiRequestLogOptions = SimApiRequestLogOptions()
|
public var simApiRequestLogOptions = SimApiRequestLogOptions()
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
// ===== .NET 风格配置回调(对齐 C# ConfigureSimApiXxx(opt => ...)) =====
|
// ===== .NET 风格配置回调(对齐 C# ConfigureSimApiXxx(opt => ...)) =====
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 对齐 C# 的 Configurations/SimApiRequestLogOptions.cs。
|
* 对齐 C# 的 Configurations/SimApiRequestLogOptions.cs。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.configurations
|
package simcu::simapi.configurations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求日志配置。
|
* 请求日志配置。
|
||||||
@@ -16,14 +16,23 @@ public class SimApiRequestLogOptions {
|
|||||||
public var showFullHeader: Bool = false
|
public var showFullHeader: Bool = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否打印完整的响应体。
|
* 是否打印完整的响应体(false 时截断到 200 字符,对齐 C#)。
|
||||||
*/
|
*/
|
||||||
public var showFullResponse: Bool = false
|
public var showFullResponse: Bool = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求地址是否显示完整 URL(false 时仅显示路径+查询串)。
|
||||||
|
*/
|
||||||
|
public var showFullUrl: Bool = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否在请求行显示运行耗时(如 [POST] (12ms) http://...)。
|
||||||
|
*/
|
||||||
|
public var showRunTime: Bool = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求字段显示最长长度(0 表示不截断)。
|
* 请求字段显示最长长度(0 表示不截断)。
|
||||||
*/
|
*/
|
||||||
public var requestStringLogLength: Int64 = 0
|
public var requestStringLogLength: Int64 = 0
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 对齐 C# 的 Configurations/SimApiRouteOptions.cs。
|
* 对齐 C# 的 Configurations/SimApiRouteOptions.cs。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.configurations
|
package simcu::simapi.configurations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由相关配置(默认值与 C# 一致)。
|
* 路由相关配置(默认值与 C# 一致)。
|
||||||
@@ -25,5 +25,4 @@ public class SimApiRouteOptions {
|
|||||||
*/
|
*/
|
||||||
public var webConfigRoute: ?String = Some("/config")
|
public var webConfigRoute: ?String = Some("/config")
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 对齐 C# 的 Configurations/SimApiStorageOptions.cs。
|
* 对齐 C# 的 Configurations/SimApiStorageOptions.cs。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.configurations
|
package simcu::simapi.configurations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 存储相关配置(占位:仓颉版暂未实现 S3/MinIO 存储)。
|
* 存储相关配置(占位:仓颉版暂未实现 S3/MinIO 存储)。
|
||||||
@@ -16,5 +16,4 @@ public class SimApiStorageOptions {
|
|||||||
public var bucket: String = ""
|
public var bucket: String = ""
|
||||||
public var serveUrl: String = ""
|
public var serveUrl: String = ""
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 对齐 C# 的 Configurations/SimApiSynapseOptions.cs。
|
* 对齐 C# 的 Configurations/SimApiSynapseOptions.cs。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.configurations
|
package simcu::simapi.configurations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MQTT 通信配置(默认值与 C# 一致)。
|
* MQTT 通信配置(默认值与 C# 一致)。
|
||||||
@@ -47,5 +47,4 @@ public class SimApiSynapseOptions {
|
|||||||
*/
|
*/
|
||||||
public var disableRpcClient: Bool = false
|
public var disableRpcClient: Bool = false
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
* Controllers/SimApiAuthController:认证相关内置路由。
|
* Controllers/SimApiAuthController:认证相关内置路由。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.controllers
|
package simcu::simapi.controllers
|
||||||
|
|
||||||
import soulsoft_web_mvc.annotations.*
|
import soulsoft_web_mvc.annotations.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证控制器:退出登录。
|
* 认证控制器:退出登录。
|
||||||
|
|||||||
@@ -5,18 +5,19 @@
|
|||||||
* 提供当前登录信息访问(对齐 C# 的 LoginInfo / LoginToken)。
|
* 提供当前登录信息访问(对齐 C# 的 LoginInfo / LoginToken)。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.controllers
|
package simcu::simapi.controllers
|
||||||
|
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
import soulsoft_web_mvc.core.*
|
import soulsoft_web_mvc.core.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
|
import simcu::simapi.interfaces.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础控制器:所有控制器均继承本控制器。
|
* 基础控制器:所有控制器均继承本控制器。
|
||||||
* 对齐 C# 的 SimApiBaseController([Consumes]/[Produces] JSON + 登录信息)。
|
* 对齐 C# 的 SimApiBaseController([Consumes]/[Produces] JSON + 登录信息)。
|
||||||
*/
|
*/
|
||||||
public open class SimApiBaseController <: Controller {
|
public open class SimApiBaseController <: Controller & IBindRequestContext {
|
||||||
/**
|
/**
|
||||||
* 当前登录信息(需 EnableSimApiAuth;未登录抛 401)。
|
* 当前登录信息(需 EnableSimApiAuth;未登录抛 401)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
* Controllers/SimApiCommonController:通用内置路由。
|
* Controllers/SimApiCommonController:通用内置路由。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.controllers
|
package simcu::simapi.controllers
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import soulsoft_web_mvc.annotations.*
|
import soulsoft_web_mvc.annotations.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.configurations.*
|
import simcu::simapi.configurations.*
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用控制器:错误反馈、WebConfig、用户信息。
|
* 通用控制器:错误反馈、WebConfig、用户信息。
|
||||||
@@ -26,24 +26,11 @@ public class SimApiCommonController <: SimApiBaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /exception/{code}:错误反馈页面(始终注册)。
|
* GET /exception/{code}:错误反馈页面(始终注册)。
|
||||||
* 返回 SimApiBaseResponse(已是响应体,原样输出)。
|
* 对齐 C# ExceptionHandler:抛 SimApiException,由异常中间件统一输出。
|
||||||
*/
|
*/
|
||||||
@HttpGet["exception/{code}"]
|
@HttpGet["exception/{code}"]
|
||||||
public func exceptionHandler(@FromRoute code: Int64): SimApiBaseResponse {
|
public func exceptionHandler(@FromRoute code: Int64): Unit {
|
||||||
SimApiBaseResponse(code, SimApiBaseResponse.getDefaultMessage(code))
|
SimApiError.error(code: 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.exceptions
|
package simcu::simapi.exceptions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 错误捕获异常:携带业务错误码,由异常中间件统一转换为 HTTP 200 + JSON。
|
* API 错误捕获异常:携带业务错误码,由异常中间件统一转换为 HTTP 200 + JSON。
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
|
||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
|
||||||
* 自定义 IRequestDelegateFactory:在结果派发时自动封装响应,
|
|
||||||
* 对齐 C# SimApiResponseFilter(IResultFilter)的行为:
|
|
||||||
* - 返回 SimApiBaseResponse 或其子类 → 原样输出
|
|
||||||
* - 返回 null/void(Unit)→ SimApiBaseResponse()({code:200, message:成功})
|
|
||||||
* - 返回 String → SimApiResponse<String>(data 为字符串)
|
|
||||||
* - 返回其他对象 → SimApiResponse<Any>(data 为对象)
|
|
||||||
*
|
|
||||||
* 注册方式:必须在 soulsoft addControllers() 之前注册(tryAddSingleton 先到先得)。
|
|
||||||
*/
|
|
||||||
|
|
||||||
package simapi.extensions
|
|
||||||
|
|
||||||
import std.collection.*
|
|
||||||
import std.reflect.*
|
|
||||||
import soulsoft_web_http.*
|
|
||||||
import soulsoft_web_mvc.core.*
|
|
||||||
import soulsoft_web_mvc.routing.*
|
|
||||||
import soulsoft_web_mvc.controllers.*
|
|
||||||
import soulsoft_web_mvc.modelBindings.*
|
|
||||||
import soulsoft_web_mvc.abstractions.*
|
|
||||||
import soulsoft_extensions_options.*
|
|
||||||
import soulsoft_extensions_injection.*
|
|
||||||
import simapi.attributes.{SimApiAuth, OriginResponse}
|
|
||||||
import simapi.communications.*
|
|
||||||
import simapi.configurations.*
|
|
||||||
import simapi.controllers.*
|
|
||||||
import simapi.helpers.{SimApiError}
|
|
||||||
import simapi.interfaces.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义请求委托工厂:接管 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()
|
|
||||||
checkSimApiAuth()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查 @SimApiAuth 注解并执行鉴权(对齐 C# SimApiAuthAttribute.OnActionExecuting):
|
|
||||||
/// 未登录 401 → 类型权限 403 → 遍历执行 ISimApiAuthChecker
|
|
||||||
private func checkSimApiAuth() {
|
|
||||||
var auth: ?SimApiAuth = None
|
|
||||||
for (item in actionDescriptor.endpointMetadata) {
|
|
||||||
if (let a: SimApiAuth <- item) {
|
|
||||||
auth = Some(a)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (let Some(auth) <- auth) {
|
|
||||||
// 1. 未登录 → 401
|
|
||||||
var loginItem: SimApiLoginItem = SimApiLoginItem("")
|
|
||||||
match (context.items.get("LoginInfo")) {
|
|
||||||
case Some(v) =>
|
|
||||||
if (let l: SimApiLoginItem <- v) {
|
|
||||||
loginItem = l
|
|
||||||
} else {
|
|
||||||
SimApiError.error(code: 401, message: "需要登录")
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
SimApiError.error(code: 401, message: "需要登录")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 类型权限校验 → 403
|
|
||||||
if (!auth.`type`.isEmpty()) {
|
|
||||||
if (!loginItem._types.contains(auth.`type`)) {
|
|
||||||
SimApiError.error(code: 403, message: "无权访问")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 遍历执行 ISimApiAuthChecker(从 DI 按注册类型解析)
|
|
||||||
let token = match (context.items.get("LoginToken")) {
|
|
||||||
case Some(v) => if (let s: String <- v) { s } else { "" }
|
|
||||||
case None => ""
|
|
||||||
}
|
|
||||||
let options = context.services.getOrThrow<SimApiOptions>()
|
|
||||||
for (checkerType in options.authCheckers) {
|
|
||||||
let instance = context.services.getOrThrow(checkerType)
|
|
||||||
if (let checker: ISimApiAuthChecker <- instance) {
|
|
||||||
checker.run(loginItem, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 模型绑定失败时写入 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) {
|
|
||||||
// @OriginResponse:跳过统一封装,原样输出(对齐 C# OnResultExecuting 遇注解直接 return)
|
|
||||||
var originResponse = false
|
|
||||||
for (item in actionDescriptor.endpointMetadata) {
|
|
||||||
if (item is OriginResponse) {
|
|
||||||
originResponse = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (originResponse) {
|
|
||||||
if (let s: String <- actionResult) {
|
|
||||||
// String 原样输出文本(对齐 C# string 返回直接写入)
|
|
||||||
context.response.contentType = "application/json; charset=utf-8"
|
|
||||||
context.response.write(s)
|
|
||||||
} else {
|
|
||||||
ObjectResult<Any>(actionResult).invoke(context)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 统一封装(IActionResult/SimApiBaseResponse/String/Unit/其他对象)
|
|
||||||
SimApiResultWriter.write(context, actionResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 根据 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,44 +3,21 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.helpers
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.io.*
|
import std.io.*
|
||||||
import soulsoft_serialization.*
|
import simcu::serialization.*
|
||||||
import soulsoft_serialization.macros.*
|
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.exceptions.*
|
import simcu::simapi.exceptions.*
|
||||||
|
import simcu::simapi.interfaces.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AES body 请求({"data": "密文"},对齐 C# SimApiOneFieldRequest<string>)。
|
* AES body 请求({"data": "密文"},对齐 C# SimApiOneFieldRequest<string>)。
|
||||||
*/
|
*/
|
||||||
@Serialization
|
|
||||||
public class AesBodyRequest {
|
public class AesBodyRequest {
|
||||||
public var _data: String = ""
|
public var data: String = ""
|
||||||
|
|
||||||
public init() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AES body 密钥提供器(对齐 C# ModelBinders/AesBodyProviderBase):
|
|
||||||
* 应用继承本类并实现 getKey(appId),返回 appId 对应的 AES 密钥。
|
|
||||||
*/
|
|
||||||
public open class AesBodyProviderBase {
|
|
||||||
/// appId 字段名(None 表示不带 appId)
|
|
||||||
public var appIdName: ?String = Some("appId")
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 appId 获取密钥。
|
|
||||||
* @param appId 应用 ID(未配置 appIdName 时为 None)。
|
|
||||||
* @return 密钥;返回 None 表示获取失败。
|
|
||||||
*/
|
|
||||||
public open func getKey(appId: ?String): ?String {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +25,8 @@ public open class AesBodyProviderBase {
|
|||||||
*
|
*
|
||||||
* 仓颉无 ModelBinder 机制,按项目惯例由控制器在方法开头调用:
|
* 仓颉无 ModelBinder 机制,按项目惯例由控制器在方法开头调用:
|
||||||
* let jsonStr = SimApiAesBodyChecker.decryptBody(context, provider)
|
* let jsonStr = SimApiAesBodyChecker.decryptBody(context, provider)
|
||||||
* let request = JsonSerializer.deserializeObject<XxxRequest>(jsonStr)
|
* let request = JsonSerializer.Deserialize<XxxRequest>(jsonStr)
|
||||||
|
* 或标注 @AesBody 注解自动执行(SimApiRequestDelegateFactory)。
|
||||||
*
|
*
|
||||||
* 流程(与 C# 一致):
|
* 流程(与 C# 一致):
|
||||||
* 1. 读取 body 并反序列化为 {"data": "密文"}
|
* 1. 读取 body 并反序列化为 {"data": "密文"}
|
||||||
@@ -75,8 +53,8 @@ public class SimApiAesBodyChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 反序列化 {"data": "密文"}
|
// 2. 反序列化 {"data": "密文"}
|
||||||
let req = JsonSerializer.deserializeObject<AesBodyRequest>(body)
|
let req = JsonSerializer.Deserialize<AesBodyRequest>(body)
|
||||||
if (req._data.isEmpty()) {
|
if (req.data.isEmpty()) {
|
||||||
SimApiError.error(code: 400, message: "请求体缺少密文Data字段")
|
SimApiError.error(code: 400, message: "请求体缺少密文Data字段")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +76,7 @@ public class SimApiAesBodyChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. 解密
|
// 5. 解密
|
||||||
let jsonStr = SimApiAesUtil.decrypt(req._data, key.getOrThrow())
|
let jsonStr = SimApiAesUtil.decrypt(req.data, key.getOrThrow())
|
||||||
if (jsonStr.isEmpty()) {
|
if (jsonStr.isEmpty()) {
|
||||||
SimApiError.error(code: 400, message: "解密失败")
|
SimApiError.error(code: 400, message: "解密失败")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.helpers
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.random.*
|
import std.random.*
|
||||||
|
|||||||
+25
-17
@@ -3,18 +3,17 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.helpers
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.collection.concurrent.*
|
import std.collection.concurrent.*
|
||||||
import std.convert.*
|
import std.convert.*
|
||||||
import std.time.*
|
import std.time.*
|
||||||
import stdx.encoding.json.*
|
import simcu::serialization.*
|
||||||
import soulsoft_serialization.*
|
|
||||||
import redis.client.*
|
import redis.client.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.configurations.*
|
import simcu::simapi.configurations.*
|
||||||
import simapi.exceptions.*
|
import simcu::simapi.exceptions.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证助手:基于 Header Token 的登录态管理。
|
* 认证助手:基于 Header Token 的登录态管理。
|
||||||
@@ -85,7 +84,7 @@ public class SimApiAuth {
|
|||||||
public func login(loginItem: SimApiLoginItem, expireSeconds!: Int64 = 604800, token!: String = ""): String {
|
public func login(loginItem: SimApiLoginItem, expireSeconds!: Int64 = 604800, token!: String = ""): String {
|
||||||
let newToken = if (token.isEmpty()) { generateToken() } else { token }
|
let newToken = if (token.isEmpty()) { generateToken() } else { token }
|
||||||
let tokenKey = "${tokenCachePrefix}${newToken}"
|
let tokenKey = "${tokenCachePrefix}${newToken}"
|
||||||
let setKey = "${tokenSetCachePrefix}${loginItem._id}"
|
let setKey = "${tokenSetCachePrefix}${loginItem.id}"
|
||||||
let json = loginItemJson(loginItem)
|
let json = loginItemJson(loginItem)
|
||||||
|
|
||||||
if (let Some(redis) <- _redis) {
|
if (let Some(redis) <- _redis) {
|
||||||
@@ -94,10 +93,10 @@ public class SimApiAuth {
|
|||||||
redis.expire(setKey, expireSeconds)
|
redis.expire(setKey, expireSeconds)
|
||||||
} else {
|
} else {
|
||||||
_tokenStore[newToken] = TokenEntry(json, nowMillis() + expireSeconds * 1000)
|
_tokenStore[newToken] = TokenEntry(json, nowMillis() + expireSeconds * 1000)
|
||||||
var tokens = _userTokens.get(loginItem._id)
|
var tokens = _userTokens.get(loginItem.id)
|
||||||
if (tokens == None) {
|
if (tokens == None) {
|
||||||
tokens = HashSet<String>()
|
tokens = HashSet<String>()
|
||||||
_userTokens[loginItem._id] = tokens.getOrThrow()
|
_userTokens[loginItem.id] = tokens.getOrThrow()
|
||||||
}
|
}
|
||||||
tokens.getOrThrow().add(newToken)
|
tokens.getOrThrow().add(newToken)
|
||||||
}
|
}
|
||||||
@@ -114,7 +113,12 @@ public class SimApiAuth {
|
|||||||
let tokenKey = "${tokenCachePrefix}${token}"
|
let tokenKey = "${tokenCachePrefix}${token}"
|
||||||
let json = loginItemJson(loginItem)
|
let json = loginItemJson(loginItem)
|
||||||
if (let Some(redis) <- _redis) {
|
if (let Some(redis) <- _redis) {
|
||||||
|
// 保留原过期时间(对齐 C# update 不刷新 TTL):先读旧 TTL,SET 后重新续期
|
||||||
|
let ttl = redis.ttl(tokenKey)
|
||||||
redis.set(tokenKey, Blob.fromUtf8(json))
|
redis.set(tokenKey, Blob.fromUtf8(json))
|
||||||
|
if (ttl > 0) {
|
||||||
|
redis.expire(tokenKey, ttl)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 保留原过期时间(对齐 C# update 不刷新 TTL)
|
// 保留原过期时间(对齐 C# update 不刷新 TTL)
|
||||||
let expireAt = match (_tokenStore.get(token)) {
|
let expireAt = match (_tokenStore.get(token)) {
|
||||||
@@ -153,11 +157,20 @@ public class SimApiAuth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return match (json) {
|
return match (json) {
|
||||||
case Some(j) => Some(parseLoginItem(j))
|
case Some(j) => parseLoginItemSafe(j)
|
||||||
case None => None
|
case None => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 解析登录信息;JSON 无效/非对象时返回 None(视为 token 无效,对齐 C# 返回 null)
|
||||||
|
private static func parseLoginItemSafe(json: String): ?SimApiLoginItem {
|
||||||
|
try {
|
||||||
|
Some(JsonSerializer.Deserialize<SimApiLoginItem>(json))
|
||||||
|
} catch (_: Exception) {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取某用户全部登录信息。
|
* 获取某用户全部登录信息。
|
||||||
* @param userId 用户 ID。
|
* @param userId 用户 ID。
|
||||||
@@ -183,7 +196,7 @@ public class SimApiAuth {
|
|||||||
public func logout(token: String): Unit {
|
public func logout(token: String): Unit {
|
||||||
let item = getLogin(token)
|
let item = getLogin(token)
|
||||||
if (let Some(item) <- item) {
|
if (let Some(item) <- item) {
|
||||||
removeTokenOfUser(item._id, token)
|
removeTokenOfUser(item.id, token)
|
||||||
}
|
}
|
||||||
let tokenKey = "${tokenCachePrefix}${token}"
|
let tokenKey = "${tokenCachePrefix}${token}"
|
||||||
if (let Some(redis) <- _redis) {
|
if (let Some(redis) <- _redis) {
|
||||||
@@ -282,11 +295,6 @@ public class SimApiAuth {
|
|||||||
|
|
||||||
private static func loginItemJson(item: SimApiLoginItem): String {
|
private static func loginItemJson(item: SimApiLoginItem): String {
|
||||||
// 统一 JSON 序列化:对齐 .NET JsonSerializer.Serialize(item)
|
// 统一 JSON 序列化:对齐 .NET JsonSerializer.Serialize(item)
|
||||||
JsonSerializer.serializeObject<SimApiLoginItem>(item)
|
JsonSerializer.Serialize(item)
|
||||||
}
|
|
||||||
|
|
||||||
private static func parseLoginItem(json: String): SimApiLoginItem {
|
|
||||||
// 统一 JSON 反序列化:对齐 .NET JsonSerializer.Deserialize<SimApiLoginItem>(json)
|
|
||||||
JsonSerializer.deserializeObject<SimApiLoginItem>(json)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-10
@@ -3,15 +3,16 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.helpers
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.collection.concurrent.*
|
import std.collection.concurrent.*
|
||||||
import std.convert.*
|
import std.convert.*
|
||||||
import std.time.*
|
import std.time.*
|
||||||
import redis.client.*
|
import redis.client.*
|
||||||
import simapi.configurations.*
|
import simcu::serialization.*
|
||||||
import simapi.exceptions.*
|
import simcu::simapi.configurations.*
|
||||||
|
import simcu::simapi.exceptions.*
|
||||||
|
|
||||||
/// InMemory 存储项:缓存值 + 过期时间(epoch 毫秒,0 表示不过期)
|
/// InMemory 存储项:缓存值 + 过期时间(epoch 毫秒,0 表示不过期)
|
||||||
private struct CacheEntry {
|
private struct CacheEntry {
|
||||||
@@ -66,16 +67,17 @@ public class SimApiCache {
|
|||||||
* @param value 缓存值(不能为 null)。
|
* @param value 缓存值(不能为 null)。
|
||||||
* @param expireSeconds 过期秒数(可选,<=0 表示不过期)。
|
* @param expireSeconds 过期秒数(可选,<=0 表示不过期)。
|
||||||
*/
|
*/
|
||||||
public func set(key: String, value: String, expireSeconds!: Int64 = -1): Unit {
|
public func set(key: String, value: Any, expireSeconds!: Int64 = -1): Unit {
|
||||||
|
let json = SimApiUtil.json(Some(value))
|
||||||
if (let Some(redis) <- _redis) {
|
if (let Some(redis) <- _redis) {
|
||||||
if (expireSeconds > 0) {
|
if (expireSeconds > 0) {
|
||||||
redis.set("${prefix}${key}", Blob.fromUtf8(value), ex: Some(expireSeconds))
|
redis.set("${prefix}${key}", Blob.fromUtf8(json), ex: Some(expireSeconds))
|
||||||
} else {
|
} else {
|
||||||
redis.set("${prefix}${key}", Blob.fromUtf8(value))
|
redis.set("${prefix}${key}", Blob.fromUtf8(json))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let expireAt = if (expireSeconds > 0) { nowMillis() + expireSeconds * 1000 } else { 0 }
|
let expireAt = if (expireSeconds > 0) { nowMillis() + expireSeconds * 1000 } else { 0 }
|
||||||
_store["${prefix}${key}"] = CacheEntry(value, expireAt)
|
_store["${prefix}${key}"] = CacheEntry(json, expireAt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,13 +96,23 @@ public class SimApiCache {
|
|||||||
* 缓存 Key 是否存在。
|
* 缓存 Key 是否存在。
|
||||||
*/
|
*/
|
||||||
public func hasKey(key: String): Bool {
|
public func hasKey(key: String): Bool {
|
||||||
get(key) != None
|
getString(key) != None
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 string 类型缓存。
|
* 获取特定类型缓存(对齐 C# Get<T>:从 JSON 反序列化,使用 simapi_serialization)。
|
||||||
*/
|
*/
|
||||||
public func get(key: String): ?String {
|
public func get<T>(key: String): ?T {
|
||||||
|
match (getString(key)) {
|
||||||
|
case Some(json) => Some(JsonSerializer.Deserialize<T>(json))
|
||||||
|
case None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 string 类型缓存(对应 C# Get(string);Cangjie 不支持按泛型重载,故拆分为 getString/get<T>)。
|
||||||
|
*/
|
||||||
|
public func getString(key: String): ?String {
|
||||||
if (let Some(redis) <- _redis) {
|
if (let Some(redis) <- _redis) {
|
||||||
return match (redis.get("${prefix}${key}")) {
|
return match (redis.get("${prefix}${key}")) {
|
||||||
case Some(blob) => Some(blob.toUtf8())
|
case Some(blob) => Some(blob.toUtf8())
|
||||||
|
|||||||
@@ -9,13 +9,13 @@
|
|||||||
* 过滤出继承 Controller 的类型(含子包)
|
* 过滤出继承 Controller 的类型(含子包)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.helpers
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.core.*
|
import std.core.*
|
||||||
import std.reflect.*
|
import std.reflect.*
|
||||||
import soulsoft_web_mvc.core.*
|
import soulsoft_web_mvc.core.*
|
||||||
import simapi.interfaces.*
|
import simcu::simapi.interfaces.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制器自动扫描器:从调用栈定位调用者包,枚举该包(含子包)中继承 Controller 的类型。
|
* 控制器自动扫描器:从调用栈定位调用者包,枚举该包(含子包)中继承 Controller 的类型。
|
||||||
@@ -57,13 +57,16 @@ public class SimApiControllerScanner {
|
|||||||
let st = ex.getStackTrace()
|
let st = ex.getStackTrace()
|
||||||
for (el in st) {
|
for (el in st) {
|
||||||
let name = el.declaringClass
|
let name = el.declaringClass
|
||||||
// 跳过本类及框架包
|
// 跳过本类及框架包(兼容旧包名 simapi 与新包名 simcu::simapi)
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (name.startsWith("simapi.") || name == "simapi") {
|
if (name.startsWith("simapi.") || name == "simapi") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (name.startsWith("simcu::simapi.") || name == "simcu::simapi") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (name.startsWith("soulsoft_") || name.startsWith("std.") || name.startsWith("stdx.")) {
|
if (name.startsWith("soulsoft_") || name.startsWith("std.") || name.startsWith("stdx.")) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,59 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
*
|
||||||
|
* 错误抛出:提供「顶层函数」+「SimApiError 类」两种写法(对齐 C# using static SimApi.Helpers.SimApiError)。
|
||||||
|
* - 顶层函数:import simcu::simapi.helpers.* 后可直接 error(400) / errorWhen(...),无需前缀
|
||||||
|
* - SimApiError.error(...):旧写法,保留兼容
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.helpers
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import simapi.exceptions.*
|
import simcu::simapi.exceptions.*
|
||||||
|
|
||||||
|
// ===== 顶层函数(推荐用法:直接 error(400),对齐 C# using static) =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接抛错。
|
||||||
|
* @param code 错误代码,默认 500。
|
||||||
|
* @param message 错误描述,默认空(由 code 自动带取描述)。
|
||||||
|
*/
|
||||||
|
public func error(code!: Int64 = 500, message!: String = ""): Unit {
|
||||||
|
SimApiError.error(code: code, message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件为 true 时抛错。
|
||||||
|
* @param condition 检测条件。
|
||||||
|
* @param code 错误代码,默认 400。
|
||||||
|
* @param message 错误描述。
|
||||||
|
*/
|
||||||
|
public func errorWhen(condition: Bool, code!: Int64 = 400, message!: String = ""): Unit {
|
||||||
|
SimApiError.errorWhen(condition, code: code, message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件为 true 时抛错(别名)。
|
||||||
|
*/
|
||||||
|
public func errorWhenTrue(condition: Bool, code!: Int64 = 400, message!: String = ""): Unit {
|
||||||
|
SimApiError.errorWhenTrue(condition, code: code, message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件为 false 时抛错。
|
||||||
|
*/
|
||||||
|
public func errorWhenFalse(condition: Bool, code!: Int64 = 400, message!: String = ""): Unit {
|
||||||
|
SimApiError.errorWhenFalse(condition, code: code, message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给定的可选值为 None 时抛错。
|
||||||
|
*/
|
||||||
|
public func errorWhenNull(condition: ?Any, code!: Int64 = 404, message!: String = ""): Unit {
|
||||||
|
SimApiError.errorWhenNull(condition, code: code, message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 兼容门面(旧写法 SimApiError.error(...) 仍可用,内部为真实实现) =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 错误抛出辅助类:所有业务错误统一通过这里抛出 SimApiException。
|
* 错误抛出辅助类:所有业务错误统一通过这里抛出 SimApiException。
|
||||||
@@ -51,14 +99,11 @@ public class SimApiError {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 给定的可选值为 None 时抛错。
|
* 给定的可选值为 None 时抛错。
|
||||||
* @param condition 检测的可选值。
|
|
||||||
* @param code 错误代码,默认 404。
|
|
||||||
* @param message 错误描述。
|
|
||||||
*/
|
*/
|
||||||
public static func errorWhenNone(condition: ?Any, code!: Int64 = 404, message!: String = ""): Unit {
|
public static func errorWhenNull(condition: ?Any, code!: Int64 = 404, message!: String = ""): Unit {
|
||||||
match (condition) {
|
match (condition) {
|
||||||
case None => error(code: code, message: message)
|
case None => error(code: code, message: message)
|
||||||
case _ => ()
|
case _ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,24 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.helpers
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.io.*
|
import std.io.*
|
||||||
|
import stdx.net.http.*
|
||||||
import stdx.net.tls.*
|
import stdx.net.tls.*
|
||||||
import stdx.net.tls.common.*
|
import stdx.net.tls.common.*
|
||||||
import soulsoft_net_http.{HttpClient, HttpRequestMessage, JsonContent}
|
import simcu::serialization.*
|
||||||
import soulsoft_net_http.{HttpMethod as NetHttpMethod}
|
import simcu::simapi.communications.*
|
||||||
import soulsoft_serialization.*
|
import simcu::simapi.configurations.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.exceptions.*
|
||||||
import simapi.configurations.*
|
|
||||||
import simapi.exceptions.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP 客户端:用于调用其他带签名/AES 的 SimApi 服务。
|
* HTTP 客户端:用于调用其他带签名/AES 的 SimApi 服务。
|
||||||
* 对齐 C# 的 SimApi.Helpers.SimApiHttpClient:
|
* 对齐 C# 的 SimApi.Helpers.SimApiHttpClient:
|
||||||
* - 内部使用 soulsoft_net_http 的 HttpClient(等价 .NET 的 System.Net.Http.HttpClient)
|
* - 内部使用 stdx.net.http 的 HttpClient(等价 .NET 的 System.Net.Http.HttpClient)
|
||||||
* - 返回泛型 T(反序列化响应 body 的 data 字段),不再返回 String
|
* - 返回泛型 T(反序列化响应 body 的 data 字段),不再返回 String
|
||||||
* @param T 响应 data 的数据类型(需实现 ISerialization<T>,如 SimApiLoginItem、String、Int64 等)。
|
* @param T 响应 data 的数据类型(任意类,simapi_serialization 反射反序列化)。
|
||||||
*/
|
*/
|
||||||
public open class SimApiHttpClient {
|
public open class SimApiHttpClient {
|
||||||
public var server: String
|
public var server: String
|
||||||
@@ -38,11 +37,6 @@ public open class SimApiHttpClient {
|
|||||||
server = httpOptions.server
|
server = httpOptions.server
|
||||||
appId = httpOptions.appId
|
appId = httpOptions.appId
|
||||||
appKey = httpOptions.appKey
|
appKey = httpOptions.appKey
|
||||||
signName = httpOptions.signName
|
|
||||||
timestampName = httpOptions.timestampName
|
|
||||||
nonceName = httpOptions.nonceName
|
|
||||||
appIdName = httpOptions.appIdName
|
|
||||||
signFields = httpOptions.signFields
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,7 +47,7 @@ public open class SimApiHttpClient {
|
|||||||
* @param queries 额外查询参数(可选)。
|
* @param queries 额外查询参数(可选)。
|
||||||
* @return 响应 data 字段反序列化后的 T。
|
* @return 响应 data 字段反序列化后的 T。
|
||||||
*/
|
*/
|
||||||
public func signQuery<T>(url: String, body!: String = "", queries!: HashMap<String, String> = HashMap<String, String>()): T where T <: ISerialization<T> {
|
public func signQuery<T>(url: String, body!: String = "", queries!: HashMap<String, String> = HashMap<String, String>()): T {
|
||||||
var queryUrl = StringBuilder()
|
var queryUrl = StringBuilder()
|
||||||
for (field in signFields) {
|
for (field in signFields) {
|
||||||
queryUrl.append("${field}=")
|
queryUrl.append("${field}=")
|
||||||
@@ -85,13 +79,13 @@ public open class SimApiHttpClient {
|
|||||||
* @param body 请求体 JSON 字符串。
|
* @param body 请求体 JSON 字符串。
|
||||||
* @return 响应 data 字段反序列化后的 T。
|
* @return 响应 data 字段反序列化后的 T。
|
||||||
*/
|
*/
|
||||||
public func aesQuery<T>(url: String, body: String): T where T <: ISerialization<T> {
|
public func aesQuery<T>(url: String, body: String): T {
|
||||||
var target = "${server}${url}"
|
var target = "${server}${url}"
|
||||||
if (let Some(name) <- appIdName) {
|
if (let Some(name) <- appIdName) {
|
||||||
target = "${target}?${name}=${appId}"
|
target = "${target}?${name}=${appId}"
|
||||||
}
|
}
|
||||||
let encrypted = aesEncrypt(body)
|
let encrypted = aesEncrypt(body)
|
||||||
let req = "{\"data\":\"${encrypted}\"}"
|
let req = SimApiUtil.json(Some(SimApiOneFieldRequest<String>(encrypted)))
|
||||||
return query<T>(target, req)
|
return query<T>(target, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,39 +97,40 @@ public open class SimApiHttpClient {
|
|||||||
* @param queries 额外查询参数(可选)。
|
* @param queries 额外查询参数(可选)。
|
||||||
* @return 响应 data 字段反序列化后的 T。
|
* @return 响应 data 字段反序列化后的 T。
|
||||||
*/
|
*/
|
||||||
public func aesSignQuery<T>(url: String, body: String, queries!: HashMap<String, String> = HashMap<String, String>()): T where T <: ISerialization<T> {
|
public func aesSignQuery<T>(url: String, body: String, queries!: HashMap<String, String> = HashMap<String, String>()): T {
|
||||||
let encrypted = aesEncrypt(body)
|
let encrypted = aesEncrypt(body)
|
||||||
let req = "{\"data\":\"${encrypted}\"}"
|
let req = SimApiUtil.json(Some(SimApiOneFieldRequest<String>(encrypted)))
|
||||||
return signQuery<T>(url, body: req, queries: queries)
|
return signQuery<T>(url, body: req, queries: queries)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发起 POST 请求并反序列化 SimApiBaseResponse<T>,返回 data 字段。
|
* 发起 POST 请求并反序列化 SimApiResponse<T>,返回 data 字段。
|
||||||
* 对齐 C# Query<T>:
|
* 对齐 C# Query<T>:
|
||||||
* ErrorWhenFalse(IsSuccessStatusCode) → ReadFromJsonAsync<SimApiBaseResponse<T>> → ErrorWhen(Code != 200) → return Data。
|
* ErrorWhenFalse(IsSuccessStatusCode) → ReadFromJsonAsync<SimApiResponse<T>> → ErrorWhen(Code != 200) → return Data。
|
||||||
* 注意:必须 noProxy(),否则会走系统代理(192.168.0.250:8118)导致连接被拒。
|
* 注意:必须 noProxy(),否则会走系统代理(192.168.0.250:8118)导致连接被拒。
|
||||||
|
* 反序列化使用 simapi_serialization(Deserialize<T> 免约束)。
|
||||||
*/
|
*/
|
||||||
private func query<T>(url: String, body: String): T where T <: ISerialization<T> {
|
private func query<T>(url: String, body: String): T {
|
||||||
let client = HttpClient.create { builder =>
|
let client = ClientBuilder().
|
||||||
builder.noProxy()
|
noProxy().
|
||||||
// 支持 https:配置 TLS(信任所有证书 + SNI 域名)
|
tlsConfig(buildTlsConfig(url)).
|
||||||
var tls = TlsClientConfig()
|
readTimeout(Duration.second * 30).
|
||||||
tls.verifyMode = CertificateVerifyMode.TrustAll
|
build()
|
||||||
let host = extractHost(url)
|
|
||||||
if (!host.isEmpty()) {
|
|
||||||
tls.serverName = Some(host)
|
|
||||||
}
|
|
||||||
builder.tlsConfig(tls)
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
let request = HttpRequestMessage(NetHttpMethod.Post, url)
|
let request = HttpRequestBuilder().
|
||||||
request.content = JsonContent.create(body)
|
post().
|
||||||
|
url(url).
|
||||||
|
header("Content-Type", "application/json").
|
||||||
|
body(body).
|
||||||
|
build()
|
||||||
let response = client.send(request)
|
let response = client.send(request)
|
||||||
try {
|
try {
|
||||||
SimApiError.errorWhenFalse(response.isSuccessStatusCode, code: response.statusCode, message: "HTTP ERROR: ${response.statusCode}")
|
SimApiError.errorWhenFalse(isSuccess(response.status), code: Int64(response.status),
|
||||||
let result = response.content.readFromJson<SimApiResponse<T>>()
|
message: "HTTP ERROR: ${response.status}")
|
||||||
SimApiError.errorWhen(result._code != 200, code: result._code, message: result._message)
|
let json = readBodyText(response.body)
|
||||||
return result._data.getOrThrow()
|
let result = JsonSerializer.Deserialize<SimApiResponse<T>>(json)
|
||||||
|
SimApiError.errorWhen(result.code != 200, code: result.code, message: result.message)
|
||||||
|
return result.data.getOrThrow()
|
||||||
} finally {
|
} finally {
|
||||||
response.close()
|
response.close()
|
||||||
}
|
}
|
||||||
@@ -144,6 +139,34 @@ public open class SimApiHttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 2xx 视为成功
|
||||||
|
private static func isSuccess(status: UInt16): Bool {
|
||||||
|
status >= 200 && status < 300
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取响应体 InputStream 为字符串
|
||||||
|
private static func readBodyText(body: InputStream): String {
|
||||||
|
var buffer = Array<Byte>(4096, repeat: 0)
|
||||||
|
var sb = StringBuilder()
|
||||||
|
var read = body.read(buffer)
|
||||||
|
while (read > 0) {
|
||||||
|
sb.appendFromUtf8(buffer.slice(0, read))
|
||||||
|
read = body.read(buffer)
|
||||||
|
}
|
||||||
|
sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建 TLS 配置:信任所有证书 + SNI 域名
|
||||||
|
private static func buildTlsConfig(url: String): TlsClientConfig {
|
||||||
|
var tls = TlsClientConfig()
|
||||||
|
tls.verifyMode = CertificateVerifyMode.TrustAll
|
||||||
|
let host = extractHost(url)
|
||||||
|
if (!host.isEmpty()) {
|
||||||
|
tls.serverName = Some(host)
|
||||||
|
}
|
||||||
|
tls
|
||||||
|
}
|
||||||
|
|
||||||
private func aesEncrypt(plain: String): String {
|
private func aesEncrypt(plain: String): String {
|
||||||
// 对齐 C#:SimApiAesUtil.Encrypt(plain, AppKey)(AES-256-CBC + PKCS7,Base64(IV + 密文))
|
// 对齐 C#:SimApiAesUtil.Encrypt(plain, AppKey)(AES-256-CBC + PKCS7,Base64(IV + 密文))
|
||||||
SimApiAesUtil.encrypt(plain, appKey)
|
SimApiAesUtil.encrypt(plain, appKey)
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
* 自定义 IRequestDelegateFactory:在结果派发时自动封装响应,
|
||||||
|
* 对齐 C# SimApiResponseFilter(IResultFilter)的行为:
|
||||||
|
* - 返回 SimApiBaseResponse 或其子类 → 原样输出
|
||||||
|
* - 返回 null/void(Unit)→ SimApiBaseResponse()({code:200, message:成功})
|
||||||
|
* - 返回 String → SimApiResponse<String>(data 为字符串)
|
||||||
|
* - 返回其他对象 → SimApiResponse<Any>(data 为对象)
|
||||||
|
*
|
||||||
|
* 注册方式:必须在 soulsoft addControllers() 之前注册(tryAddSingleton 先到先得)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
|
import std.collection.*
|
||||||
|
import std.io.*
|
||||||
|
import std.reflect.*
|
||||||
|
import stdx.encoding.json.*
|
||||||
|
import stdx.serialization.serialization.*
|
||||||
|
import soulsoft_web_http.*
|
||||||
|
import soulsoft_web_mvc.annotations.*
|
||||||
|
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 simcu::serialization.*
|
||||||
|
import simcu::simapi.annotations.{SimApiAuth as SimApiAuthAttribute, OriginResponse, SimApiSign, AesBody}
|
||||||
|
import simcu::simapi.communications.*
|
||||||
|
import simcu::simapi.configurations.*
|
||||||
|
import simcu::simapi.interfaces.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义请求委托工厂:接管 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()
|
||||||
|
checkSimApiAuth()
|
||||||
|
checkSimApiSign()
|
||||||
|
// 预读并缓存请求体(body 流不可重读;若请求日志中间件已读,直接用其缓存)
|
||||||
|
if (!context.items.contains(BODY_CACHE_KEY)) {
|
||||||
|
context.items[BODY_CACHE_KEY] = readBody()
|
||||||
|
}
|
||||||
|
let modelBindingContext = ActionBindingContext(context, actionDescriptor.actionFunction.parameters)
|
||||||
|
// 自定义绑定:无注解参数(FromBody)用 simapi_serialization 反序列化,
|
||||||
|
// 显式注解参数(Query/Form/Route/Header/Services)委托 soulsoft binder。
|
||||||
|
let boundParameters = bindParameters(modelBindingContext)
|
||||||
|
if (!modelBindingContext.modelState.isValid) {
|
||||||
|
handleInvalidModelState(modelBindingContext)
|
||||||
|
} else {
|
||||||
|
let actionResult = actionDescriptor.actionFunction.apply(controller, boundParameters)
|
||||||
|
dispatchResult(actionResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 请求体缓存键(body 流不可重读,读一次后缓存)
|
||||||
|
private static let BODY_CACHE_KEY = "SimApi:BodyCache"
|
||||||
|
|
||||||
|
/// 绑定全部参数:FromBody 自实现(simapi_serialization),其余委托 soulsoft
|
||||||
|
private func bindParameters(context: ActionBindingContext): Array<Any> {
|
||||||
|
let params = context.parameters
|
||||||
|
if (params.size == 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let bound = Array<Any>(params.size, repeat: ())
|
||||||
|
// 存在显式参数时才委托 soulsoft 绑定(Query/Form/Route/Header/Services)
|
||||||
|
var hasExplicit = false
|
||||||
|
for (parameter in params) {
|
||||||
|
if (isExplicitlyBound(parameter)) {
|
||||||
|
hasExplicit = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let soulsoftBound = if (hasExplicit) { modelBinder.bind(context) } else { Array<Any>(params.size, repeat: ()) }
|
||||||
|
for ((index, parameter) in params |> enumerate) {
|
||||||
|
if (isExplicitlyBound(parameter)) {
|
||||||
|
// Query/Form/Route/Header/Services → soulsoft
|
||||||
|
bound[index] = soulsoftBound[index]
|
||||||
|
} else if (let Some(aes) <- parameter.findAnnotation<AesBody>()) {
|
||||||
|
// @AesBody → 解密 body 后按参数类型反序列化(对齐 C# AesBodyModelBinder)
|
||||||
|
bound[index] = bindAesBody(context, parameter, aes)
|
||||||
|
} else {
|
||||||
|
// FromBody → simapi_serialization 按运行时类型反序列化(免 @Serialization 宏)
|
||||||
|
bound[index] = bindFromBody(context, parameter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bound
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 参数是否显式指定绑定源(FromQuery/FromForm/FromRoute/FromHeader/FromServices)
|
||||||
|
private func isExplicitlyBound(parameter: ParameterInfo): Bool {
|
||||||
|
parameter.findAnnotation<FromQuery>().isSome() ||
|
||||||
|
parameter.findAnnotation<FromForm>().isSome() ||
|
||||||
|
parameter.findAnnotation<FromRoute>().isSome() ||
|
||||||
|
parameter.findAnnotation<FromHeader>().isSome() ||
|
||||||
|
parameter.findAnnotation<FromServices>().isSome()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从请求体反序列化(simapi_serialization 按运行时类型,DTO 免标注)
|
||||||
|
private func bindFromBody(context: ActionBindingContext, parameter: ParameterInfo): Any {
|
||||||
|
let body = match (context.httpContext.items.get(BODY_CACHE_KEY)) {
|
||||||
|
case Some(v) => if (let s: String <- v) { s } else { "" }
|
||||||
|
case None => ""
|
||||||
|
}
|
||||||
|
if (body.isEmpty()) {
|
||||||
|
SimApiError.error(code: 400, message: "请求体不能为空")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JsonSerializer.Deserialize(parameter.typeInfo, body)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
SimApiError.error(code: 400, message: "请求体反序列化失败: ${ex.message}")
|
||||||
|
}
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @AesBody 参数绑定:解密 body 后按参数类型反序列化(对齐 C# AesBodyModelBinder)
|
||||||
|
private func bindAesBody(context: ActionBindingContext, parameter: ParameterInfo, aes: AesBody): Any {
|
||||||
|
// 1. 从 DI 解析 keyProvider(AesBodyProviderBase 实现)
|
||||||
|
let provider = resolveAesProvider(aes.keyProvider)
|
||||||
|
// 2. 读取并解密 body(SimApiAesBodyChecker.decryptBody 内部读取原始 body 流)
|
||||||
|
let plain = SimApiAesBodyChecker.decryptBody(context.httpContext, provider)
|
||||||
|
// 3. 按参数类型反序列化明文 JSON
|
||||||
|
try {
|
||||||
|
return JsonSerializer.Deserialize(parameter.typeInfo, plain)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
SimApiError.error(code: 400, message: "AES body 反序列化失败: ${ex.message}")
|
||||||
|
}
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 DI 解析 AesBodyProviderBase 实现(注解未指定类型名时返回默认空实现)
|
||||||
|
private func resolveAesProvider(keyProvider: String): AesBodyProviderBase {
|
||||||
|
if (keyProvider.isEmpty()) {
|
||||||
|
return AesBodyProviderBase()
|
||||||
|
}
|
||||||
|
var typeInfo: ?TypeInfo = None
|
||||||
|
try {
|
||||||
|
typeInfo = Some(TypeInfo.get(keyProvider))
|
||||||
|
} catch (_: Exception) {
|
||||||
|
SimApiError.error(code: 400, message: "未找到 AES 密钥提供器 ${keyProvider}")
|
||||||
|
}
|
||||||
|
let instance = context.services.getOrThrow(typeInfo.getOrThrow())
|
||||||
|
if (let p: AesBodyProviderBase <- instance) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
SimApiError.error(code: 400, message: "密钥提供器 ${keyProvider} 未实现 AesBodyProviderBase")
|
||||||
|
AesBodyProviderBase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查 @SimApiSign 注解并执行验签(对齐 C# SimApiSignAttribute.OnActionExecuting)
|
||||||
|
private func checkSimApiSign() {
|
||||||
|
var sign: ?SimApiSign = None
|
||||||
|
for (item in actionDescriptor.endpointMetadata) {
|
||||||
|
if (let s: SimApiSign <- item) {
|
||||||
|
sign = Some(s)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (let Some(sign) <- sign) {
|
||||||
|
// 1. 从 DI 解析 keyProvider(SimApiSignProviderBase 实现)
|
||||||
|
let provider = resolveSignProvider(sign.keyProvider)
|
||||||
|
// 2. 解析缓存(nonce 去重;DI 有 SimApiCache 则用)
|
||||||
|
var cache: ?SimApiCache = None
|
||||||
|
try {
|
||||||
|
cache = Some(context.services.getOrThrow<SimApiCache>())
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// 未注册缓存 → 跳过 nonce 去重
|
||||||
|
}
|
||||||
|
// 3. 执行验签
|
||||||
|
SimApiSignChecker.verify(context, provider, cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 DI 解析 SimApiSignProviderBase 实现(注解未指定类型名时返回默认空实现)
|
||||||
|
private func resolveSignProvider(keyProvider: String): SimApiSignProviderBase {
|
||||||
|
if (keyProvider.isEmpty()) {
|
||||||
|
return SimApiSignProviderBase()
|
||||||
|
}
|
||||||
|
var typeInfo: ?TypeInfo = None
|
||||||
|
try {
|
||||||
|
typeInfo = Some(TypeInfo.get(keyProvider))
|
||||||
|
} catch (_: Exception) {
|
||||||
|
SimApiError.error(code: 400, message: "未找到签名提供器 ${keyProvider}")
|
||||||
|
}
|
||||||
|
let instance = context.services.getOrThrow(typeInfo.getOrThrow())
|
||||||
|
if (let p: SimApiSignProviderBase <- instance) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
SimApiError.error(code: 400, message: "签名提供器 ${keyProvider} 未实现 SimApiSignProviderBase")
|
||||||
|
SimApiSignProviderBase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取并重置请求体流(供后续业务读取)
|
||||||
|
private func readBody(): 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 text = sb.toString()
|
||||||
|
if (let seekable: Seekable <- context.request.body) {
|
||||||
|
seekable.seek(SeekPosition.Begin(0))
|
||||||
|
}
|
||||||
|
text
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
SimApiError.error(code: 400, message: "读取请求体失败: ${ex.message}")
|
||||||
|
}
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查 @SimApiAuth 注解并执行鉴权(对齐 C# SimApiAuthAttribute.OnActionExecuting):
|
||||||
|
/// 未登录 401 → 遍历执行 ISimApiAuthChecker → 类型权限 403
|
||||||
|
private func checkSimApiAuth() {
|
||||||
|
var auth: ?SimApiAuthAttribute = None
|
||||||
|
for (item in actionDescriptor.endpointMetadata) {
|
||||||
|
if (let a: SimApiAuthAttribute <- item) {
|
||||||
|
auth = Some(a)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (let Some(auth) <- auth) {
|
||||||
|
// 1. 未登录 → 401
|
||||||
|
var loginItem: SimApiLoginItem = SimApiLoginItem("")
|
||||||
|
match (context.items.get("LoginInfo")) {
|
||||||
|
case Some(v) =>
|
||||||
|
if (let l: SimApiLoginItem <- v) {
|
||||||
|
loginItem = l
|
||||||
|
} else {
|
||||||
|
SimApiError.error(code: 401, message: "需要登录")
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
SimApiError.error(code: 401, message: "需要登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 遍历执行 ISimApiAuthChecker(对齐 C# GetServices<ISimApiAuthChecker>():一次解析全部实现)
|
||||||
|
let token = match (context.items.get("LoginToken")) {
|
||||||
|
case Some(v) => if (let s: String <- v) { s } else { "" }
|
||||||
|
case None => ""
|
||||||
|
}
|
||||||
|
let checkers = context.services.getAll<ISimApiAuthChecker>()
|
||||||
|
for (checker in checkers) {
|
||||||
|
checker.run(loginItem, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 类型权限校验 → 403(对齐 C# Types.Intersect(loginInfo.Type).Any(),支持逗号分隔多类型)
|
||||||
|
if (!auth.`type`.isEmpty()) {
|
||||||
|
let requiredTypes = auth.`type`.split(",")
|
||||||
|
var matched = false
|
||||||
|
for (t in requiredTypes) {
|
||||||
|
if (loginItem.types.contains(t)) {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
SimApiError.error(code: 403, message: "无权访问")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模型绑定失败时写入 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)
|
||||||
|
}
|
||||||
|
// 与 soulsoft writeAsJson 的 Serializable<T> 分支一致:serialize().toJson().toString()
|
||||||
|
// (toJson 为 DataModel 接口扩展,需先上转);writeAsJson 内部会设置 contentType,此处补上
|
||||||
|
context.response.contentType = "application/json; charset=utf-8"
|
||||||
|
let dm: DataModel = details.serialize()
|
||||||
|
let jsonText = dm.toJson().toString()
|
||||||
|
SimApiResponseWriter.write(context, jsonText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 结果派发 + 自动封装(对齐 C# SimApiResponseFilter)
|
||||||
|
private func dispatchResult(actionResult: Any) {
|
||||||
|
// @OriginResponse:跳过统一封装,原样输出(对齐 C# OnResultExecuting 遇注解直接 return)
|
||||||
|
var originResponse = false
|
||||||
|
for (item in actionDescriptor.endpointMetadata) {
|
||||||
|
if (item is OriginResponse) {
|
||||||
|
originResponse = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (originResponse) {
|
||||||
|
if (let s: String <- actionResult) {
|
||||||
|
// String 原样输出文本(对齐 C# string 返回直接写入)
|
||||||
|
context.response.contentType = "application/json; charset=utf-8"
|
||||||
|
SimApiResponseWriter.write(context, s)
|
||||||
|
} else {
|
||||||
|
ObjectResult<Any>(actionResult).invoke(context)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 统一封装(IActionResult/SimApiBaseResponse/String/Unit/其他对象)
|
||||||
|
SimApiResultWriter.write(context, actionResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据 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
|
||||||
|
/// (通过 IBindRequestContext 接口而非 SimApiBaseController,避免 helpers↔controllers 循环依赖)
|
||||||
|
private func createControllerInstance(): Object {
|
||||||
|
let instance = ActivatorUtilities.createInstance(context.services, actionDescriptor.controllerType)
|
||||||
|
if (let controller: IBindRequestContext <- instance) {
|
||||||
|
controller.bindRequestContext(context)
|
||||||
|
}
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,17 +3,17 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.helpers
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
import simapi.communications.*
|
import simcu::serialization.*
|
||||||
|
import simcu::simapi.communications.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 响应封装:对写操作(Unit)返回统一成功响应,对已有 SimApiBaseResponse 透传。
|
* 响应封装:对写操作(Unit)返回统一成功响应,对已有 SimApiBaseResponse 透传。
|
||||||
* 在仓颉版中以中间件形式实现,对应 C# 的 SimApiResponseFilter。
|
* 在仓颉版中以中间件形式实现,对应 C# 的 SimApiResponseFilter。
|
||||||
*/
|
*/
|
||||||
public class SimApiResponseFilter {
|
public class SimApiResponseFilter {
|
||||||
public init() {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 包装响应委托:捕获下一级写入的响应内容。
|
* 包装响应委托:捕获下一级写入的响应内容。
|
||||||
@@ -25,7 +25,8 @@ public class SimApiResponseFilter {
|
|||||||
context =>
|
context =>
|
||||||
next(context)
|
next(context)
|
||||||
if (!context.response.hasStarted) {
|
if (!context.response.hasStarted) {
|
||||||
context.response.writeAsJson(SimApiBaseResponse())
|
context.response.contentType = "application/json; charset=utf-8"
|
||||||
|
SimApiResponseWriter.write(context, JsonSerializer.Serialize(SimApiBaseResponse()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
*/
|
||||||
|
|
||||||
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
|
import soulsoft_web_http.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应写出工具:写出响应体并缓存文本,供请求日志中间件读取。
|
||||||
|
*
|
||||||
|
* 背景:soulsoft 的 HttpResponse.body 为只写流(read 抛 UnsupportedException),
|
||||||
|
* 无法像 C# 那样用 MemoryStream 替换 Body 捕获响应内容;
|
||||||
|
* 故在统一写出入口缓存文本,请求日志中间件直接从 context.items 读取。
|
||||||
|
*/
|
||||||
|
public class SimApiResponseWriter {
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应体缓存键(写入 HttpContext.items)。
|
||||||
|
*/
|
||||||
|
public static let responseBodyCacheKey = "SimApi:ResponseBodyCache"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写出响应文本并缓存(供请求日志中间件读取)。
|
||||||
|
* @param context HTTP 上下文。
|
||||||
|
* @param text 响应正文文本。
|
||||||
|
*/
|
||||||
|
public static func write(context: HttpContext, text: String): Unit {
|
||||||
|
context.items[responseBodyCacheKey] = text
|
||||||
|
context.response.write(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,12 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.extensions
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
import soulsoft_web_mvc.core.*
|
import soulsoft_web_mvc.core.*
|
||||||
import simapi.communications.*
|
import simcu::serialization.*
|
||||||
|
import simcu::simapi.communications.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一响应封装工具(对齐 C# SimApiResponseFilter 的包装分支)。
|
* 统一响应封装工具(对齐 C# SimApiResponseFilter 的包装分支)。
|
||||||
@@ -16,6 +17,8 @@ import simapi.communications.*
|
|||||||
* - String → SimApiResponse<String>(data 为字符串)
|
* - String → SimApiResponse<String>(data 为字符串)
|
||||||
* - Unit(void)→ SimApiBaseResponse()({code:200, message:成功})
|
* - Unit(void)→ SimApiBaseResponse()({code:200, message:成功})
|
||||||
* - 其他对象(DTO/数组/动态结构)→ SimApiDataResponse(data 内嵌为对象)
|
* - 其他对象(DTO/数组/动态结构)→ SimApiDataResponse(data 内嵌为对象)
|
||||||
|
*
|
||||||
|
* 输出使用 simapi_serialization 序列化后直接写响应体(不走 soulsoft formatter)。
|
||||||
*/
|
*/
|
||||||
public class SimApiResultWriter {
|
public class SimApiResultWriter {
|
||||||
private init() {}
|
private init() {}
|
||||||
@@ -26,17 +29,22 @@ public class SimApiResultWriter {
|
|||||||
result.invoke(context)
|
result.invoke(context)
|
||||||
} else if (let result: SimApiBaseResponse <- actionResult) {
|
} else if (let result: SimApiBaseResponse <- actionResult) {
|
||||||
// 已是 SimApiBaseResponse(含子类)→ 原样输出
|
// 已是 SimApiBaseResponse(含子类)→ 原样输出
|
||||||
ObjectResult<Any>(result).invoke(context)
|
writeJson(context, result)
|
||||||
} else if (let result: String <- actionResult) {
|
} else if (let result: String <- actionResult) {
|
||||||
// String → SimApiResponse<String>(data 为字符串)
|
// String → SimApiResponse<String>(data 为字符串)
|
||||||
ObjectResult<Any>(SimApiResponse<String>(result)).invoke(context)
|
writeJson(context, SimApiResponse<String>(result))
|
||||||
} else if (let result: Unit <- actionResult) {
|
} else if (let result: Unit <- actionResult) {
|
||||||
// void/无返回 → SimApiBaseResponse()({code:200, message:成功})
|
// void/无返回 → SimApiBaseResponse()({code:200, message:成功})
|
||||||
context.response.writeAsJson(SimApiBaseResponse())
|
writeJson(context, SimApiBaseResponse())
|
||||||
} else {
|
} else {
|
||||||
// 其他对象(DTO/数组/动态结构)→ SimApiDataResponse
|
// 其他对象(DTO/数组/动态结构)→ SimApiDataResponse
|
||||||
// data 由 SimApiDataResponse.serializeObject 内嵌为对象
|
writeJson(context, SimApiDataResponse(actionResult))
|
||||||
ObjectResult<Any>(SimApiDataResponse(actionResult)).invoke(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// simapi_serialization 序列化后直接写响应体(经 SimApiResponseWriter 缓存,供请求日志读取)
|
||||||
|
private static func writeJson(context: HttpContext, obj: Any): Unit {
|
||||||
|
context.response.contentType = "application/json; charset=utf-8"
|
||||||
|
SimApiResponseWriter.write(context, JsonSerializer.Serialize(obj))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,55 +3,19 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.helpers
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import std.convert.*
|
import std.convert.*
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
import simapi.exceptions.*
|
import simcu::simapi.exceptions.*
|
||||||
|
import simcu::simapi.interfaces.*
|
||||||
/**
|
|
||||||
* 签名提供器(对齐 C# ModelBinders/SimApiSignProviderBase):
|
|
||||||
* 应用继承本类并实现 getKey(appId),返回 appId 对应的密钥。
|
|
||||||
*/
|
|
||||||
public open class SimApiSignProviderBase {
|
|
||||||
/// appId 字段名(None 表示签名中不包含 appId)
|
|
||||||
public var appIdName: ?String = Some("appId")
|
|
||||||
|
|
||||||
/// 时间戳字段名
|
|
||||||
public var timestampName: String = "timestamp"
|
|
||||||
|
|
||||||
/// 随机串字段名
|
|
||||||
public var nonceName: String = "nonce"
|
|
||||||
|
|
||||||
/// 签名字段名
|
|
||||||
public var signName: String = "sign"
|
|
||||||
|
|
||||||
/// 请求过期秒数(0 表示不校验 timestamp)
|
|
||||||
public var queryExpires: Int64 = 5
|
|
||||||
|
|
||||||
/// 是否开启 nonce 去重(需配置缓存)
|
|
||||||
public var duplicateRequestProtection: Bool = true
|
|
||||||
|
|
||||||
/// 参与签名的额外字段(与 appId/timestamp/nonce 一起拼入签名字符串)
|
|
||||||
public var signFields: Array<String> = []
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 appId 获取密钥。
|
|
||||||
* @param appId 应用 ID(未配置 appIdName 时为 None)。
|
|
||||||
* @return 密钥;返回 None 表示获取失败。
|
|
||||||
*/
|
|
||||||
public open func getKey(appId: ?String): ?String {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务端验签校验器(对齐 C# Attributes/SimApiSignAttribute.OnActionExecuting)。
|
* 服务端验签校验器(对齐 C# Attributes/SimApiSignAttribute.OnActionExecuting)。
|
||||||
*
|
*
|
||||||
* 仓颉无声明式 ActionFilter 机制,按项目惯例(同 requireLogin)由控制器在需要验签的方法开头调用:
|
* 仓颉无声明式 ActionFilter 机制,按项目惯例(同 requireLogin)由控制器在需要验签的方法开头调用:
|
||||||
* SimApiSignChecker.verify(context, provider, cache)
|
* SimApiSignChecker.verify(context, provider, cache)
|
||||||
|
* 或标注 @SimApiSign 注解自动执行(SimApiRequestDelegateFactory)。
|
||||||
*
|
*
|
||||||
* 校验流程(与 C# 完全一致):
|
* 校验流程(与 C# 完全一致):
|
||||||
* 1. 提取 appId(Query/Header)
|
* 1. 提取 appId(Query/Header)
|
||||||
|
|||||||
@@ -0,0 +1,552 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
* Helpers/SimApiStorage:S3/MinIO 兼容存储助手。
|
||||||
|
*
|
||||||
|
* 仓颉生态暂无 Minio SDK,此处自实现 AWS Signature V4(HMAC-SHA256 基于 stdx SHA256):
|
||||||
|
* - 预签名 URL(PUT 上传 / GET 下载)
|
||||||
|
* - 直接 PUT 上传(对象 + 建桶)
|
||||||
|
* - HEAD 检测桶是否存在
|
||||||
|
* 与 C# SimApiStorage(Minio SDK)的公开方法语义对齐:
|
||||||
|
* GetUploadUrl / GetDownloadUrl / UploadFile / FullUrl / GetUrl / GetPath
|
||||||
|
*/
|
||||||
|
|
||||||
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
|
import std.collection.*
|
||||||
|
import std.io.*
|
||||||
|
import std.time.*
|
||||||
|
import stdx.crypto.digest.*
|
||||||
|
import stdx.encoding.base64.*
|
||||||
|
import stdx.encoding.hex.*
|
||||||
|
import stdx.net.http.*
|
||||||
|
import stdx.net.tls.*
|
||||||
|
import stdx.net.tls.common.*
|
||||||
|
import soulsoft_web_http.*
|
||||||
|
import simcu::simapi.configurations.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传预签名 URL 响应(对齐 C# record GetUploadUrlResponse(UploadUrl, DownloadUrl, Path))。
|
||||||
|
*/
|
||||||
|
public class GetUploadUrlResponse {
|
||||||
|
public var uploadUrl: String = ""
|
||||||
|
public var downloadUrl: String = ""
|
||||||
|
public var path: String = ""
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public init(uploadUrl: String, downloadUrl: String, path: String) {
|
||||||
|
this.uploadUrl = uploadUrl
|
||||||
|
this.downloadUrl = downloadUrl
|
||||||
|
this.path = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S3/MinIO 存储助手(对齐 C# Helpers/SimApiStorage)。
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - 注册为 Scoped 以注入 IHttpContextAccessor(soulsoft DI 禁止 singleton 消费 scoped 服务);
|
||||||
|
* 桶的检测/创建由静态守卫保证整个进程只执行一次(对齐 C# 构造函数中 BucketExists+MakeBucket)。
|
||||||
|
* - fullUrl/getUrl 的 "~/" 分支依赖当前请求上下文(对齐 C# IHttpContextAccessor)。
|
||||||
|
*/
|
||||||
|
public class SimApiStorage {
|
||||||
|
private static var _bucketEnsured: Bool = false
|
||||||
|
|
||||||
|
private let _endpoint: String
|
||||||
|
private let _serveUrl: String
|
||||||
|
private let _bucket: String
|
||||||
|
private let _accessKey: String
|
||||||
|
private let _secretKey: String
|
||||||
|
private let _useSsl: Bool
|
||||||
|
private let _host: String
|
||||||
|
private let _region: String = "us-east-1"
|
||||||
|
private let _httpContextAccessor: IHttpContextAccessor
|
||||||
|
|
||||||
|
public init(options: SimApiOptions, httpContextAccessor: IHttpContextAccessor) {
|
||||||
|
let storage = options.simApiStorageOptions
|
||||||
|
if (storage.endpoint.isEmpty() || storage.serveUrl.isEmpty() || storage.bucket.isEmpty()) {
|
||||||
|
throw Exception("SimApiStorage: Endpoint/ServeUrl/Bucket 不能为空")
|
||||||
|
}
|
||||||
|
var useSsl = false
|
||||||
|
var host = ""
|
||||||
|
if (storage.endpoint.startsWith("http://")) {
|
||||||
|
host = storage.endpoint["http://".size..]
|
||||||
|
} else if (storage.endpoint.startsWith("https://")) {
|
||||||
|
useSsl = true
|
||||||
|
host = storage.endpoint["https://".size..]
|
||||||
|
} else {
|
||||||
|
throw Exception("SimApiStorage: Error Endpoint")
|
||||||
|
}
|
||||||
|
if (storage.serveUrl.endsWith("/")) {
|
||||||
|
throw Exception("SimApiStorage: ServeUrl must not end with /")
|
||||||
|
}
|
||||||
|
_endpoint = storage.endpoint
|
||||||
|
_serveUrl = storage.serveUrl
|
||||||
|
_bucket = storage.bucket
|
||||||
|
_accessKey = storage.accessKey
|
||||||
|
_secretKey = storage.secretKey
|
||||||
|
_useSsl = useSsl
|
||||||
|
_host = host
|
||||||
|
_httpContextAccessor = httpContextAccessor
|
||||||
|
// 桶不存在则创建(对齐 C# BucketExists + MakeBucket;静态守卫保证只执行一次)
|
||||||
|
ensureBucketOnce()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上传预签名 URL(对齐 C# GetUploadUrl,默认 7200 秒)。
|
||||||
|
*/
|
||||||
|
public func getUploadUrl(path: String, expire!: Int64 = 7200): GetUploadUrlResponse {
|
||||||
|
checkPath(path)
|
||||||
|
let obj = trimLeadingSlash(path)
|
||||||
|
let uploadUrl = presign("PUT", obj, expire)
|
||||||
|
GetUploadUrlResponse(uploadUrl, "${_serveUrl}${path}", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载预签名 URL(对齐 C# GetDownloadUrl,默认 600 秒)。
|
||||||
|
*/
|
||||||
|
public func getDownloadUrl(path: String, expire!: Int64 = 600): String {
|
||||||
|
checkPath(path)
|
||||||
|
let obj = trimLeadingSlash(path)
|
||||||
|
presign("GET", obj, expire)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接上传文件(对齐 C# UploadFile;data 为文件字节)。
|
||||||
|
*/
|
||||||
|
public func uploadFile(path: String, data: Array<Byte>, contentType!: String = "image/png"): Unit {
|
||||||
|
checkPath(path)
|
||||||
|
let obj = trimLeadingSlash(path)
|
||||||
|
putObject(obj, data, contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除对象(对齐 C# Minio RemoveObjectsAsync;S3 原生 DeleteObjects 接口:
|
||||||
|
* POST /{bucket}?delete,一次请求删除多个对象,无需逐个删除)。
|
||||||
|
* @param paths 对象路径数组(每个须以 / 开头)。
|
||||||
|
*/
|
||||||
|
public func deleteFiles(paths: Array<String>): Unit {
|
||||||
|
if (paths.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (p in paths) {
|
||||||
|
checkPath(p)
|
||||||
|
}
|
||||||
|
var sb = StringBuilder()
|
||||||
|
sb.append("<Delete xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">")
|
||||||
|
for (p in paths) {
|
||||||
|
sb.append("<Object><Key>${xmlEscape(trimLeadingSlash(p))}</Key></Object>")
|
||||||
|
}
|
||||||
|
sb.append("<Quiet>true</Quiet></Delete>")
|
||||||
|
deleteObjects(sb.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 path 获取完整的访问 URL(对齐 C# FullUrl)。
|
||||||
|
*/
|
||||||
|
public func fullUrl(path: ?String): ?String {
|
||||||
|
if (let Some(p) <- path) {
|
||||||
|
if (p.isEmpty() || p.startsWith("http://") || p.startsWith("https://")) {
|
||||||
|
return Some(p)
|
||||||
|
}
|
||||||
|
if (!(p.startsWith("/") || p.startsWith("~/"))) {
|
||||||
|
return Some(p)
|
||||||
|
}
|
||||||
|
if (p.startsWith("~/")) {
|
||||||
|
return Some("${requestBaseUrl()}${p[1..]}")
|
||||||
|
}
|
||||||
|
return Some("${_serveUrl}${p}")
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取一个 Path 的访问 URL(对齐 C# GetUrl)。
|
||||||
|
*/
|
||||||
|
public func getUrl(path: ?String): ?String {
|
||||||
|
if (let Some(p) <- path) {
|
||||||
|
if (p.isEmpty()) {
|
||||||
|
return Some(p)
|
||||||
|
}
|
||||||
|
if (p.startsWith("~/")) {
|
||||||
|
return Some("${requestBaseUrl()}${p[1..]}")
|
||||||
|
}
|
||||||
|
if (p.startsWith("/")) {
|
||||||
|
return Some("${_serveUrl}${p}")
|
||||||
|
}
|
||||||
|
return Some(p)
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 URL 中获取相对路径(对齐 C# GetPath;去掉 Endpoint/Bucket 或 ServeUrl 前缀)。
|
||||||
|
*/
|
||||||
|
public func getPath(url: ?String): ?String {
|
||||||
|
if (let Some(u) <- url) {
|
||||||
|
var r = u
|
||||||
|
let prefix = "${_endpoint}/${_bucket}"
|
||||||
|
if (r.startsWith(prefix)) {
|
||||||
|
r = r[prefix.size..]
|
||||||
|
}
|
||||||
|
if (r.startsWith(_serveUrl)) {
|
||||||
|
r = r[_serveUrl.size..]
|
||||||
|
}
|
||||||
|
return Some(r)
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 私有:SigV4 签名与 S3 请求 =====
|
||||||
|
|
||||||
|
/// 预签名 URL(对齐 Minio PresignedPutObject / PresignedGetObject)
|
||||||
|
private func presign(method: String, object: String, expireSeconds: Int64): String {
|
||||||
|
let now = DateTime.nowUTC()
|
||||||
|
let amzDate = formatAmzDate(now)
|
||||||
|
let dateStamp = amzDate[0..8]
|
||||||
|
let canonicalUri = "/${_bucket}/${uriEncode(object, false)}"
|
||||||
|
let credential = "${_accessKey}/${dateStamp}/${_region}/s3/aws4_request"
|
||||||
|
// 五个 X-Amz-* 参数按字典序排列(A<C<D<E<S)
|
||||||
|
let canonicalQuery = "X-Amz-Algorithm=${uriEncode("AWS4-HMAC-SHA256", true)}" +
|
||||||
|
"&X-Amz-Credential=${uriEncode(credential, true)}" +
|
||||||
|
"&X-Amz-Date=${amzDate}" +
|
||||||
|
"&X-Amz-Expires=${expireSeconds}" +
|
||||||
|
"&X-Amz-SignedHeaders=host"
|
||||||
|
let canonicalHeaders = "host:${_host}\n"
|
||||||
|
let signedHeaders = "host"
|
||||||
|
let canonicalRequest = "${method}\n${canonicalUri}\n${canonicalQuery}\n${canonicalHeaders}\n${signedHeaders}\nUNSIGNED-PAYLOAD"
|
||||||
|
let stringToSign = "AWS4-HMAC-SHA256\n${amzDate}\n${dateStamp}/${_region}/s3/aws4_request\n" +
|
||||||
|
"${toHexString(sha256Bytes(canonicalRequest.toArray()))}"
|
||||||
|
let signature = toHexString(hmacSha256(buildSigningKey(dateStamp), stringToSign.toArray()))
|
||||||
|
"${_endpoint}${canonicalUri}?${canonicalQuery}&X-Amz-Signature=${signature}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 直接 PUT 上传对象(Authorization 头签名,对齐 Minio PutObject)
|
||||||
|
private func putObject(object: String, data: Array<Byte>, contentType: String): Unit {
|
||||||
|
let now = DateTime.nowUTC()
|
||||||
|
let amzDate = formatAmzDate(now)
|
||||||
|
let dateStamp = amzDate[0..8]
|
||||||
|
let payloadHash = toHexString(sha256Bytes(data))
|
||||||
|
let canonicalUri = "/${_bucket}/${uriEncode(object, false)}"
|
||||||
|
let canonicalHeaders = "host:${_host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\n"
|
||||||
|
let signedHeaders = "host;x-amz-content-sha256;x-amz-date"
|
||||||
|
let canonicalRequest = "PUT\n${canonicalUri}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}"
|
||||||
|
let stringToSign = "AWS4-HMAC-SHA256\n${amzDate}\n${dateStamp}/${_region}/s3/aws4_request\n" +
|
||||||
|
"${toHexString(sha256Bytes(canonicalRequest.toArray()))}"
|
||||||
|
let authorization = buildAuthorization(dateStamp, signedHeaders,
|
||||||
|
toHexString(hmacSha256(buildSigningKey(dateStamp), stringToSign.toArray())))
|
||||||
|
let client = createClient()
|
||||||
|
try {
|
||||||
|
let request = HttpRequestBuilder().
|
||||||
|
put().
|
||||||
|
url("${_endpoint}${canonicalUri}").
|
||||||
|
header("x-amz-content-sha256", payloadHash).
|
||||||
|
header("x-amz-date", amzDate).
|
||||||
|
header("Authorization", authorization).
|
||||||
|
header("Content-Type", contentType).
|
||||||
|
body(data).
|
||||||
|
build()
|
||||||
|
let response = client.send(request)
|
||||||
|
try {
|
||||||
|
SimApiError.errorWhenFalse(isSuccess(response.status), code: Int64(response.status),
|
||||||
|
message: "SimApiStorage 上传失败: HTTP ${response.status}")
|
||||||
|
} finally {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HEAD 检测桶是否存在(对齐 Minio BucketExistsAsync)
|
||||||
|
private func bucketExists(): Bool {
|
||||||
|
let now = DateTime.nowUTC()
|
||||||
|
let amzDate = formatAmzDate(now)
|
||||||
|
let dateStamp = amzDate[0..8]
|
||||||
|
let payloadHash = toHexString(sha256Bytes(Array<Byte>(0, repeat: 0)))
|
||||||
|
let canonicalUri = "/${_bucket}"
|
||||||
|
let canonicalHeaders = "host:${_host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\n"
|
||||||
|
let signedHeaders = "host;x-amz-content-sha256;x-amz-date"
|
||||||
|
let canonicalRequest = "HEAD\n${canonicalUri}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}"
|
||||||
|
let stringToSign = "AWS4-HMAC-SHA256\n${amzDate}\n${dateStamp}/${_region}/s3/aws4_request\n" +
|
||||||
|
"${toHexString(sha256Bytes(canonicalRequest.toArray()))}"
|
||||||
|
let authorization = buildAuthorization(dateStamp, signedHeaders,
|
||||||
|
toHexString(hmacSha256(buildSigningKey(dateStamp), stringToSign.toArray())))
|
||||||
|
let client = createClient()
|
||||||
|
try {
|
||||||
|
let request = HttpRequestBuilder().
|
||||||
|
head().
|
||||||
|
url("${_endpoint}${canonicalUri}").
|
||||||
|
header("x-amz-content-sha256", payloadHash).
|
||||||
|
header("x-amz-date", amzDate).
|
||||||
|
header("Authorization", authorization).
|
||||||
|
build()
|
||||||
|
let response = client.send(request)
|
||||||
|
try {
|
||||||
|
response.status == 200
|
||||||
|
} finally {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建桶(对齐 Minio MakeBucketAsync)
|
||||||
|
private func makeBucket(): Unit {
|
||||||
|
let now = DateTime.nowUTC()
|
||||||
|
let amzDate = formatAmzDate(now)
|
||||||
|
let dateStamp = amzDate[0..8]
|
||||||
|
let payloadHash = toHexString(sha256Bytes(Array<Byte>(0, repeat: 0)))
|
||||||
|
let canonicalUri = "/${_bucket}"
|
||||||
|
let canonicalHeaders = "host:${_host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\n"
|
||||||
|
let signedHeaders = "host;x-amz-content-sha256;x-amz-date"
|
||||||
|
let canonicalRequest = "PUT\n${canonicalUri}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}"
|
||||||
|
let stringToSign = "AWS4-HMAC-SHA256\n${amzDate}\n${dateStamp}/${_region}/s3/aws4_request\n" +
|
||||||
|
"${toHexString(sha256Bytes(canonicalRequest.toArray()))}"
|
||||||
|
let authorization = buildAuthorization(dateStamp, signedHeaders,
|
||||||
|
toHexString(hmacSha256(buildSigningKey(dateStamp), stringToSign.toArray())))
|
||||||
|
let client = createClient()
|
||||||
|
try {
|
||||||
|
let request = HttpRequestBuilder().
|
||||||
|
put().
|
||||||
|
url("${_endpoint}${canonicalUri}").
|
||||||
|
header("x-amz-content-sha256", payloadHash).
|
||||||
|
header("x-amz-date", amzDate).
|
||||||
|
header("Authorization", authorization).
|
||||||
|
body(Array<UInt8>(0, repeat: 0u8)).
|
||||||
|
build()
|
||||||
|
let response = client.send(request)
|
||||||
|
try {
|
||||||
|
SimApiError.errorWhenFalse(isSuccess(response.status), code: Int64(response.status),
|
||||||
|
message: "SimApiStorage 创建桶失败: HTTP ${response.status}")
|
||||||
|
} finally {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ensureBucketOnce(): Unit {
|
||||||
|
if (!SimApiStorage._bucketEnsured) {
|
||||||
|
if (!bucketExists()) {
|
||||||
|
makeBucket()
|
||||||
|
}
|
||||||
|
SimApiStorage._bucketEnsured = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 批量删除请求(S3 DeleteObjects:POST /{bucket}?delete + XML)
|
||||||
|
private func deleteObjects(xml: String): Unit {
|
||||||
|
let now = DateTime.nowUTC()
|
||||||
|
let amzDate = formatAmzDate(now)
|
||||||
|
let dateStamp = amzDate[0..8]
|
||||||
|
let payloadHash = toHexString(sha256Bytes(xml.toArray()))
|
||||||
|
let canonicalUri = "/${_bucket}"
|
||||||
|
let canonicalQuery = "delete="
|
||||||
|
let canonicalHeaders = "host:${_host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\n"
|
||||||
|
let signedHeaders = "host;x-amz-content-sha256;x-amz-date"
|
||||||
|
let canonicalRequest = "POST\n${canonicalUri}\n${canonicalQuery}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}"
|
||||||
|
let stringToSign = "AWS4-HMAC-SHA256\n${amzDate}\n${dateStamp}/${_region}/s3/aws4_request\n" +
|
||||||
|
"${toHexString(sha256Bytes(canonicalRequest.toArray()))}"
|
||||||
|
let authorization = buildAuthorization(dateStamp, signedHeaders,
|
||||||
|
toHexString(hmacSha256(buildSigningKey(dateStamp), stringToSign.toArray())))
|
||||||
|
let client = createClient()
|
||||||
|
try {
|
||||||
|
let contentMd5 = md5Base64(xml.toArray())
|
||||||
|
let request = HttpRequestBuilder().
|
||||||
|
post().
|
||||||
|
url("${_endpoint}${canonicalUri}?${canonicalQuery}").
|
||||||
|
header("x-amz-content-sha256", payloadHash).
|
||||||
|
header("x-amz-date", amzDate).
|
||||||
|
header("Authorization", authorization).
|
||||||
|
header("Content-Type", "application/xml").
|
||||||
|
// MinIO 的 DeleteObjects 强制要求 Content-Md5(缺失返回 MissingContentMD5)
|
||||||
|
header("Content-Md5", contentMd5).
|
||||||
|
body(xml).
|
||||||
|
build()
|
||||||
|
let response = client.send(request)
|
||||||
|
try {
|
||||||
|
SimApiError.errorWhenFalse(isSuccess(response.status), code: Int64(response.status),
|
||||||
|
message: "SimApiStorage 批量删除失败: HTTP ${response.status}")
|
||||||
|
} finally {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildAuthorization(dateStamp: String, signedHeaders: String, signature: String): String {
|
||||||
|
"AWS4-HMAC-SHA256 Credential=${_accessKey}/${dateStamp}/${_region}/s3/aws4_request, " +
|
||||||
|
"SignedHeaders=${signedHeaders}, Signature=${signature}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildSigningKey(dateStamp: String): Array<Byte> {
|
||||||
|
let kDate = hmacSha256("AWS4${_secretKey}".toArray(), dateStamp.toArray())
|
||||||
|
let kRegion = hmacSha256(kDate, _region.toArray())
|
||||||
|
let kService = hmacSha256(kRegion, "s3".toArray())
|
||||||
|
hmacSha256(kService, "aws4_request".toArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createClient(): Client {
|
||||||
|
let builder = ClientBuilder().
|
||||||
|
noProxy().
|
||||||
|
readTimeout(Duration.second * 60)
|
||||||
|
if (_useSsl) {
|
||||||
|
var tls = TlsClientConfig()
|
||||||
|
tls.verifyMode = CertificateVerifyMode.TrustAll
|
||||||
|
let host = extractHost(_endpoint)
|
||||||
|
if (!host.isEmpty()) {
|
||||||
|
tls.serverName = Some(host)
|
||||||
|
}
|
||||||
|
return builder.tlsConfig(tls).build()
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 2xx 视为成功
|
||||||
|
private static func isSuccess(status: UInt16): Bool {
|
||||||
|
status >= 200 && status < 300
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkPath(path: String): Unit {
|
||||||
|
if (!path.startsWith("/")) {
|
||||||
|
throw Exception("path must start with /")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func trimLeadingSlash(path: String): String {
|
||||||
|
if (path.startsWith("/")) {
|
||||||
|
path[1..]
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 当前请求基础地址(scheme://host);无请求上下文时返回空串
|
||||||
|
private func requestBaseUrl(): String {
|
||||||
|
if (let Some(ctx) <- _httpContextAccessor.context) {
|
||||||
|
return "${ctx.request.scheme}://${ctx.request.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 }
|
||||||
|
rest[0..end]
|
||||||
|
case None => ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func formatAmzDate(dt: DateTime): String {
|
||||||
|
"${dt.year}${pad2(dt.monthValue)}${pad2(dt.dayOfMonth)}T${pad2(dt.hour)}${pad2(dt.minute)}${pad2(dt.second)}Z"
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func pad2(v: Int64): String {
|
||||||
|
if (v < 10) {
|
||||||
|
"0${v}"
|
||||||
|
} else {
|
||||||
|
"${v}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// XML 特殊字符转义(对象 Key 中可能含 & < > " ')
|
||||||
|
private static func xmlEscape(s: String): String {
|
||||||
|
var sb = StringBuilder()
|
||||||
|
for (c in s.runes()) {
|
||||||
|
match (c) {
|
||||||
|
case '&' => sb.append("&")
|
||||||
|
case '<' => sb.append("<")
|
||||||
|
case '>' => sb.append(">")
|
||||||
|
case '"' => sb.append(""")
|
||||||
|
case '\'' => sb.append("'")
|
||||||
|
case _ => sb.append(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RFC 3986 百分号编码(encodeSlash=false 时保留 '/')
|
||||||
|
private static func uriEncode(s: String, encodeSlash: Bool): String {
|
||||||
|
let bytes = s.toArray()
|
||||||
|
let hex = "0123456789ABCDEF"
|
||||||
|
var sb = StringBuilder()
|
||||||
|
for (b in bytes) {
|
||||||
|
let u = toU8(b)
|
||||||
|
let unreserved = (u >= 0x41u8 && u <= 0x5Au8) || (u >= 0x61u8 && u <= 0x7Au8) ||
|
||||||
|
(u >= 0x30u8 && u <= 0x39u8) || u == 0x2Du8 || u == 0x5Fu8 || u == 0x2Eu8 || u == 0x7Eu8
|
||||||
|
if (unreserved || (u == 0x2Fu8 && !encodeSlash)) {
|
||||||
|
sb.append(Rune(UInt32(u)))
|
||||||
|
} else {
|
||||||
|
sb.append("%")
|
||||||
|
sb.append(Rune(UInt32(hex[Int64((u >> 4u8) & 0x0Fu8)])))
|
||||||
|
sb.append(Rune(UInt32(hex[Int64(u & 0x0Fu8)])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func toU8(b: Byte): UInt8 {
|
||||||
|
if (b < 0) {
|
||||||
|
UInt8(Int64(b) + 256)
|
||||||
|
} else {
|
||||||
|
UInt8(Int64(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HMAC-SHA256(自实现,基于 stdx SHA256;stdx 静态库无现成 HMAC 封装)
|
||||||
|
private static func hmacSha256(key: Array<Byte>, data: Array<Byte>): Array<Byte> {
|
||||||
|
var k = key
|
||||||
|
if (k.size > 64) {
|
||||||
|
k = sha256Bytes(k)
|
||||||
|
}
|
||||||
|
var keyPadded = Array<Byte>(64, repeat: 0)
|
||||||
|
for (i in 0..k.size) {
|
||||||
|
keyPadded[i] = k[i]
|
||||||
|
}
|
||||||
|
var inner = ArrayList<Byte>()
|
||||||
|
for (i in 0..64) {
|
||||||
|
inner.add(bxor(keyPadded[i], 0x36u8))
|
||||||
|
}
|
||||||
|
for (b in data) {
|
||||||
|
inner.add(b)
|
||||||
|
}
|
||||||
|
let innerHash = sha256Bytes(inner.toArray())
|
||||||
|
var outer = ArrayList<Byte>()
|
||||||
|
for (i in 0..64) {
|
||||||
|
outer.add(bxor(keyPadded[i], 0x5Cu8))
|
||||||
|
}
|
||||||
|
for (b in innerHash) {
|
||||||
|
outer.add(b)
|
||||||
|
}
|
||||||
|
sha256Bytes(outer.toArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
@OverflowWrapping
|
||||||
|
private static func bxor(a: Byte, mask: UInt8): Byte {
|
||||||
|
let ua = toU8(a)
|
||||||
|
let r: Byte = ua ^ mask
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func sha256Bytes(data: Array<Byte>): Array<Byte> {
|
||||||
|
let sha = SHA256()
|
||||||
|
sha.write(data)
|
||||||
|
sha.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MD5 → Base64(Content-Md5 头,S3 DeleteObjects 必需)
|
||||||
|
private static func md5Base64(data: Array<Byte>): String {
|
||||||
|
let md = MD5()
|
||||||
|
md.write(data)
|
||||||
|
toBase64String(md.finish())
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
-22
@@ -3,7 +3,7 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.helpers
|
package simcu::simapi.helpers
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.time.*
|
import std.time.*
|
||||||
@@ -11,11 +11,10 @@ import std.random.*
|
|||||||
import stdx.crypto.digest.*
|
import stdx.crypto.digest.*
|
||||||
import stdx.encoding.hex.*
|
import stdx.encoding.hex.*
|
||||||
import stdx.encoding.base64.*
|
import stdx.encoding.base64.*
|
||||||
import stdx.encoding.json.*
|
|
||||||
import std.regex.*
|
import std.regex.*
|
||||||
import soulsoft_serialization.*
|
import simcu::serialization.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.macros.*
|
import simcu::simapi.macros.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具类:对应 C# 的 SimApi.Helpers.SimApiUtil。
|
* 工具类:对应 C# 的 SimApi.Helpers.SimApiUtil。
|
||||||
@@ -61,10 +60,10 @@ public class SimApiUtil {
|
|||||||
* @param source 源字符串。
|
* @param source 源字符串。
|
||||||
* @return 32 位十六进制小写。
|
* @return 32 位十六进制小写。
|
||||||
*/
|
*/
|
||||||
public static func md5(source: String): String {
|
public static func md5(source: String, mode!: String = "x2"): String {
|
||||||
let md = MD5()
|
let md = MD5()
|
||||||
md.write(source.toArray())
|
md.write(source.toArray())
|
||||||
toHexString(md.finish())
|
formatHex(md.finish(), mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,10 +71,10 @@ public class SimApiUtil {
|
|||||||
* @param source 源字符串。
|
* @param source 源字符串。
|
||||||
* @return 40 位十六进制小写。
|
* @return 40 位十六进制小写。
|
||||||
*/
|
*/
|
||||||
public static func sha1(source: String): String {
|
public static func sha1(source: String, mode!: String = "x2"): String {
|
||||||
let sha = SHA1()
|
let sha = SHA1()
|
||||||
sha.write(source.toArray())
|
sha.write(source.toArray())
|
||||||
toHexString(sha.finish())
|
formatHex(sha.finish(), mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,6 +88,28 @@ public class SimApiUtil {
|
|||||||
toHexString(sha.finish())
|
toHexString(sha.finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 按 .NET Md5/Sha1 的 mode 格式化:x2/x3/x4 → 每个字节 2/3/4 位十六进制
|
||||||
|
private static func formatHex(bytes: Array<Byte>, mode: String): String {
|
||||||
|
if (mode == "x2") {
|
||||||
|
return toHexString(bytes)
|
||||||
|
}
|
||||||
|
let two = toHexString(bytes)
|
||||||
|
var sb = StringBuilder()
|
||||||
|
for (i in 0..bytes.size) {
|
||||||
|
let pair = two[i * 2..i * 2 + 2]
|
||||||
|
if (mode == "x3") {
|
||||||
|
sb.append("0")
|
||||||
|
sb.append(pair)
|
||||||
|
} else if (mode == "x4") {
|
||||||
|
sb.append("00")
|
||||||
|
sb.append(pair)
|
||||||
|
} else {
|
||||||
|
sb.append(pair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字符串 Base64 编码。
|
* 字符串 Base64 编码。
|
||||||
*/
|
*/
|
||||||
@@ -112,7 +133,8 @@ public class SimApiUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断是否是 Email 地址(简化校验)。
|
* 判断是否是 Email 地址。
|
||||||
|
* 说明:.NET 使用 System.Net.Mail.MailAddress 校验,仓颉无等价 API,此处用正则近似。
|
||||||
*/
|
*/
|
||||||
public static func checkEmail(email: String): Bool {
|
public static func checkEmail(email: String): Bool {
|
||||||
Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$").matches(email)
|
Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$").matches(email)
|
||||||
@@ -150,20 +172,44 @@ public class SimApiUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将对象序列化为 JSON 字符串(委托给 SimApiJson.json 统一实现,对齐 C# SimApiUtil.Json)。
|
* 将对象序列化为 JSON 字符串(对齐 C# SimApiUtil.Json)。
|
||||||
|
* @param obj 任意对象(None 输出 null)。
|
||||||
*/
|
*/
|
||||||
public static func json(obj: ?Any): String {
|
public static func json(obj: ?Any): String {
|
||||||
SimApiJson.json(obj)
|
if (let Some(obj) <- obj) {
|
||||||
|
return JsonSerializer.Serialize(obj)
|
||||||
|
}
|
||||||
|
"null"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 字符串转义(对齐 C# 内部转义逻辑)。
|
||||||
|
* @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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 JSON 字符串反序列化为 T(对齐 C# SimApiUtil.FromJson<T>)。
|
* 从 JSON 字符串反序列化为 T(对齐 C# SimApiUtil.FromJson<T>)。
|
||||||
* @param T 目标类型(需实现 ISerialization<T>,如 @Serialization DTO、基础类型等)。
|
* @param T 目标类型(任意类,无需接口/宏约束)。
|
||||||
* @param jsonString JSON 字符串。
|
* @param jsonString JSON 字符串。
|
||||||
* @return 反序列化结果。
|
* @return 反序列化结果。
|
||||||
*/
|
*/
|
||||||
public static func fromJson<T>(jsonString: String): T where T <: ISerialization<T> {
|
public static func fromJson<T>(jsonString: String): T {
|
||||||
JsonSerializer.deserializeObject<T>(jsonString)
|
JsonSerializer.Deserialize<T>(jsonString)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,21 +218,34 @@ public class SimApiUtil {
|
|||||||
* @return Base64 字符串。
|
* @return Base64 字符串。
|
||||||
*/
|
*/
|
||||||
public static func base64Encode(obj: Any): String {
|
public static func base64Encode(obj: Any): String {
|
||||||
let json = if (let ser: ISerializable <- obj) {
|
let json = json(Some(obj))
|
||||||
ser.serializeObject().toJson().toString()
|
|
||||||
} else {
|
|
||||||
SimApiJson.json(Some(obj))
|
|
||||||
}
|
|
||||||
base64Encode(json)
|
base64Encode(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base64 → JSON → T 反序列化(对齐 C# Base64Decode<T>)。
|
* Base64 → JSON → T 反序列化(对齐 C# Base64Decode<T>)。
|
||||||
* @param T 目标类型(需实现 ISerialization<T>)。
|
* @param T 目标类型(任意类,无需接口/宏约束)。
|
||||||
* @param base64Str Base64 字符串。
|
* @param base64Str Base64 字符串。
|
||||||
* @return 反序列化结果。
|
* @return 反序列化结果。
|
||||||
*/
|
*/
|
||||||
public static func base64DecodeTo<T>(base64Str: String): T where T <: ISerialization<T> {
|
public static func base64DecodeTo<T>(base64Str: String): T {
|
||||||
fromJson<T>(base64Decode(base64Str))
|
fromJson<T>(base64Decode(base64Str))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页(对齐 C# Paginate 扩展;仓颉无 IQueryable,改为对 Array<T> 切片)。
|
||||||
|
*/
|
||||||
|
public static func paginate<T>(list: Array<T>, page: Int64, count: Int64): Array<T> {
|
||||||
|
let p = if (page < 1) { 1 } else { page }
|
||||||
|
let c = if (count <= 0) { 10 } else { count }
|
||||||
|
let skip = (p - 1) * c
|
||||||
|
let total = list.size
|
||||||
|
if (skip >= total) {
|
||||||
|
return Array<T>()
|
||||||
|
}
|
||||||
|
let end = if (skip + c > total) { total } else { skip + c }
|
||||||
|
list[skip..end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 说明:C# 的 XmlDeserialize<T> 依赖 System.Xml.Serialization,仓颉生态无 XML 序列化库,未移植。
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
* AES body 密钥提供器基类(对齐 C# ModelBinders/AesBodyProviderBase)。
|
||||||
|
*
|
||||||
|
* 说明:本类含配置字段(appIdName),故用 open class 而非 interface
|
||||||
|
* (Cangjie 接口不能声明字段),与 .NET 抽象类对应。应用继承本类并实现 getKey。
|
||||||
|
*/
|
||||||
|
|
||||||
|
package simcu::simapi.interfaces
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES body 密钥提供器基类:应用继承并实现 getKey(appId),返回 appId 对应的 AES 密钥。
|
||||||
|
*/
|
||||||
|
public open class AesBodyProviderBase {
|
||||||
|
/// appId 字段名(None 表示不带 appId)
|
||||||
|
public var appIdName: ?String = Some("appId")
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 appId 获取密钥。
|
||||||
|
* @param appId 应用 ID(未配置 appIdName 时为 None)。
|
||||||
|
* @return 密钥;返回 None 表示获取失败。
|
||||||
|
*/
|
||||||
|
public open func getKey(appId: ?String): ?String {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
* Interfaces/IBindRequestContext:请求上下文绑定接口。
|
||||||
|
*
|
||||||
|
* 说明:SimApiBaseController 实现本接口,由 SimApiRequestDelegateFactory(simapi.helpers)
|
||||||
|
* 在创建控制器后注入当前 HttpContext。放在 interfaces 包是为了避免
|
||||||
|
* simapi.helpers → simapi.controllers 的循环依赖(controllers 依赖 helpers 的 SimApiError/SimApiAuth/SimApiUtil)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
package simcu::simapi.interfaces
|
||||||
|
|
||||||
|
import soulsoft_web_http.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求上下文绑定接口:控制器实现后,框架在派发请求时把当前 HttpContext 注入。
|
||||||
|
*/
|
||||||
|
public interface IBindRequestContext {
|
||||||
|
/**
|
||||||
|
* 绑定当前请求上下文。
|
||||||
|
* @param context 当前请求的 HttpContext。
|
||||||
|
*/
|
||||||
|
func bindRequestContext(context: HttpContext): Unit
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.interfaces
|
package simcu::simapi.interfaces
|
||||||
|
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证后处理 Hook:实现后每次认证成功都会调用。
|
* 认证后处理 Hook:实现后每次认证成功都会调用。
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||||
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
|
* 签名提供器基类(对齐 C# ModelBinders/SimApiSignProviderBase)。
|
||||||
|
*
|
||||||
|
* 说明:本类含配置字段(appIdName/queryExpires 等),故用 open class 而非 interface
|
||||||
|
* (Cangjie 接口不能声明字段),与 .NET 抽象类对应。应用继承本类并实现 getKey。
|
||||||
|
*/
|
||||||
|
|
||||||
|
package simcu::simapi.interfaces
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名提供器基类:应用继承并实现 getKey(appId),返回 appId 对应的密钥。
|
||||||
|
*/
|
||||||
|
public open class SimApiSignProviderBase {
|
||||||
|
/// appId 字段名(None 表示签名中不包含 appId)
|
||||||
|
public var appIdName: ?String = Some("appId")
|
||||||
|
|
||||||
|
/// 时间戳字段名
|
||||||
|
public var timestampName: String = "timestamp"
|
||||||
|
|
||||||
|
/// 随机串字段名
|
||||||
|
public var nonceName: String = "nonce"
|
||||||
|
|
||||||
|
/// 签名字段名
|
||||||
|
public var signName: String = "sign"
|
||||||
|
|
||||||
|
/// 请求过期秒数(0 表示不校验 timestamp)
|
||||||
|
public var queryExpires: Int64 = 5
|
||||||
|
|
||||||
|
/// 是否开启 nonce 去重(需配置缓存)
|
||||||
|
public var duplicateRequestProtection: Bool = true
|
||||||
|
|
||||||
|
/// 参与签名的额外字段(与 appId/timestamp/nonce 一起拼入签名字符串)
|
||||||
|
public var signFields: Array<String> = []
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 appId 获取密钥。
|
||||||
|
* @param appId 应用 ID(未配置 appIdName 时为 None)。
|
||||||
|
* @return 密钥;返回 None 表示获取失败。
|
||||||
|
*/
|
||||||
|
public open func getKey(appId: ?String): ?String {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.logger
|
package simcu::simapi.logger
|
||||||
|
|
||||||
import std.collection.concurrent.*
|
import std.collection.concurrent.*
|
||||||
import std.env.*
|
import std.env.*
|
||||||
@@ -48,7 +48,7 @@ public class SimApiLogger <: ILogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func isEnabled(logLevel: LogLevel): Bool {
|
public func isEnabled(logLevel: LogLevel): Bool {
|
||||||
logLevel != LogLevel.Off
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,7 +96,7 @@ public class SimApiLogger <: ILogger {
|
|||||||
let hour = pad2(dt.hour)
|
let hour = pad2(dt.hour)
|
||||||
let minute = pad2(dt.minute)
|
let minute = pad2(dt.minute)
|
||||||
let second = pad2(dt.second)
|
let second = pad2(dt.second)
|
||||||
let millis = pad3(dt.nanosecond / 1000000)
|
let millis = pad4(dt.nanosecond / 100000)
|
||||||
"${year}-${month}-${day} ${hour}:${minute}:${second}:${millis}"
|
"${year}-${month}-${day} ${hour}:${minute}:${second}:${millis}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +107,14 @@ public class SimApiLogger <: ILogger {
|
|||||||
"${v}"
|
"${v}"
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func pad3(v: Int64): String {
|
private static func pad4(v: Int64): String {
|
||||||
if (v < 10) {
|
if (v < 10) {
|
||||||
return "00${v}"
|
return "000${v}"
|
||||||
}
|
}
|
||||||
if (v < 100) {
|
if (v < 100) {
|
||||||
|
return "00${v}"
|
||||||
|
}
|
||||||
|
if (v < 1000) {
|
||||||
return "0${v}"
|
return "0${v}"
|
||||||
}
|
}
|
||||||
"${v}"
|
"${v}"
|
||||||
@@ -124,8 +127,6 @@ public class SimApiLogger <: ILogger {
|
|||||||
public class SimApiLoggerProvider <: ILoggerProvider {
|
public class SimApiLoggerProvider <: ILoggerProvider {
|
||||||
private let _loggers = ConcurrentHashMap<String, SimApiLogger>()
|
private let _loggers = ConcurrentHashMap<String, SimApiLogger>()
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public func createLogger(categoryName: String): ILogger {
|
public func createLogger(categoryName: String): ILogger {
|
||||||
if (let Some(logger) <- _loggers.get(categoryName)) {
|
if (let Some(logger) <- _loggers.get(categoryName)) {
|
||||||
return logger
|
return logger
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
macro package simapi.macros
|
macro package simcu::simapi.macros
|
||||||
|
|
||||||
import std.ast.*
|
import std.ast.*
|
||||||
import std.fs.*
|
import std.fs.*
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.middlewares
|
package simcu::simapi.middlewares
|
||||||
|
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
import simapi.communications.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证信息获取中间件:从 Header Token 或 Query token 解析登录信息并注入上下文。
|
* 认证信息获取中间件:从 Header Token 或 Query token 解析登录信息并注入上下文。
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.middlewares
|
package simcu::simapi.middlewares
|
||||||
|
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
import soulsoft_extensions_logging.*
|
import soulsoft_extensions_logging.*
|
||||||
import simapi.communications.*
|
import simcu::serialization.*
|
||||||
import simapi.exceptions.*
|
import simcu::simapi.communications.*
|
||||||
import simapi.configurations.*
|
import simcu::simapi.exceptions.*
|
||||||
|
import simcu::simapi.configurations.*
|
||||||
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异常处理中间件:全异常捕获,统一输出 HTTP 200 + JSON 响应。
|
* 异常处理中间件:全异常捕获,统一输出 HTTP 200 + JSON 响应。
|
||||||
@@ -46,7 +48,7 @@ public class SimApiExceptionMiddleware <: IMiddleware {
|
|||||||
if (!context.response.hasStarted) {
|
if (!context.response.hasStarted) {
|
||||||
context.response.statusCode = 200
|
context.response.statusCode = 200
|
||||||
context.response.contentType = "application/json; charset=utf-8"
|
context.response.contentType = "application/json; charset=utf-8"
|
||||||
context.response.write(responseJson(response))
|
SimApiResponseWriter.write(context, responseJson(response))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,6 +71,6 @@ public class SimApiExceptionMiddleware <: IMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func responseJson(response: SimApiBaseResponse): String {
|
private func responseJson(response: SimApiBaseResponse): String {
|
||||||
response.toJsonString()
|
JsonSerializer.Serialize(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,31 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.middlewares
|
package simcu::simapi.middlewares
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.io.*
|
import std.io.*
|
||||||
import std.time.*
|
import std.time.*
|
||||||
import stdx.encoding.json.*
|
|
||||||
import soulsoft_web_http.*
|
import soulsoft_web_http.*
|
||||||
import soulsoft_extensions_logging.*
|
import soulsoft_extensions_logging.*
|
||||||
import simapi.communications.*
|
import simcu::serialization.*
|
||||||
import simapi.configurations.*
|
import simcu::simapi.communications.*
|
||||||
|
import simcu::simapi.configurations.*
|
||||||
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码、耗时与异常。
|
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码、响应体、耗时与异常。
|
||||||
* 对应 C# 的 SimApi.Middlewares.SimApiRequestLogMiddleware。
|
* 对应 C# 的 SimApi.Middlewares.SimApiRequestLogMiddleware。
|
||||||
*
|
*
|
||||||
* 对齐说明:
|
* 对齐说明:
|
||||||
* - 请求体按 JSON 字段级截断(对齐 C#:仅对超长字符串字段截断,保留结构)
|
* - 请求体按 JSON 字段级截断(对齐 C#:仅对超长字符串字段截断,保留结构)
|
||||||
* - 捕获下游异常并记录,随后重抛(对齐 C# ExceptionDispatchInfo + edi.Throw)
|
* - 捕获下游异常并记录,随后重抛(对齐 C# ExceptionDispatchInfo + edi.Throw)
|
||||||
* - 响应体:soulsoft HttpResponse.body 只读不可替换(C# 用 MemoryStream 替换捕获),
|
* - 响应体:soulsoft HttpResponse.body 只读且不可读回(read 抛 UnsupportedException),
|
||||||
* 此处以 Content-Length 作为替代信息;ShowFullResponse 选项因此暂不生效
|
* 无法像 C# 那样用 MemoryStream 替换捕获;改为在各统一写出入口
|
||||||
|
* (SimApiResponseWriter)缓存响应文本,此处直接读取。
|
||||||
|
* 响应行格式:*( Response [status] ) => [ N bytes ],随后换行输出响应体结构;
|
||||||
|
* ShowFullResponse=false 时截断到 200 字符(对齐 C#,长度仍显示完整字节数)
|
||||||
|
* - ShowFullUrl=false 时仅显示路径+查询串;ShowRunTime=true 时请求行显示 [POST] (xxxms)
|
||||||
*/
|
*/
|
||||||
public class SimApiRequestLogMiddleware <: IMiddleware {
|
public class SimApiRequestLogMiddleware <: IMiddleware {
|
||||||
private let _options: SimApiOptions
|
private let _options: SimApiOptions
|
||||||
@@ -38,13 +43,11 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
|||||||
*/
|
*/
|
||||||
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
|
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
|
||||||
let start = MonoTime.now()
|
let start = MonoTime.now()
|
||||||
let fullUrl = context.request.getDisplayUrl()
|
let options = _options.simApiRequestLogOptions
|
||||||
var sb = StringBuilder()
|
var sb = StringBuilder()
|
||||||
|
|
||||||
sb.append("[${context.request.method}] ${fullUrl}\n")
|
|
||||||
|
|
||||||
// 请求头
|
// 请求头
|
||||||
if (_options.simApiRequestLogOptions.showFullHeader) {
|
if (options.showFullHeader) {
|
||||||
sb.append("*( RequestHeaders [Full] ) =>\n")
|
sb.append("*( RequestHeaders [Full] ) =>\n")
|
||||||
sb.append(serializeHeaders(context))
|
sb.append(serializeHeaders(context))
|
||||||
} else {
|
} else {
|
||||||
@@ -66,20 +69,50 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
|||||||
exception = Some(ex)
|
exception = Some(ex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应信息
|
// 请求行(耗时需在 next 之后计算,故最后拼装)
|
||||||
let elapsed = MonoTime.now() - start
|
let elapsed = MonoTime.now() - start
|
||||||
let elapsedMs = elapsed / Duration.millisecond
|
let elapsedMs = elapsed / Duration.millisecond
|
||||||
sb.append("*( Response [${context.response.statusCode}] ) => ${elapsedMs}ms")
|
let url = if (options.showFullUrl) {
|
||||||
// 响应体捕获受 soulsoft 限制(body 只读不可替换),记录 Content-Length 作为替代
|
buildDisplayUrl(context)
|
||||||
if (let Some(len) <- context.response.contentLength) {
|
} else {
|
||||||
sb.append(" (响应体长度: ${len})")
|
context.request.path.toString() + context.request.queryString.toString()
|
||||||
|
}
|
||||||
|
var sbHead = StringBuilder()
|
||||||
|
sbHead.append("[${context.request.method}]")
|
||||||
|
if (options.showRunTime) {
|
||||||
|
sbHead.append(" (${elapsedMs}ms)")
|
||||||
|
}
|
||||||
|
sbHead.append(" ${url}\n")
|
||||||
|
|
||||||
|
// 响应信息:状态码 + 响应体长度(bytes),响应体结构换行显示
|
||||||
|
let responseBody = readResponseBody(context)
|
||||||
|
let bodyLen = if (responseBody.isEmpty()) {
|
||||||
|
context.response.contentLength ?? 0
|
||||||
|
} else {
|
||||||
|
responseBody.size
|
||||||
|
}
|
||||||
|
sb.append("*( Response [${context.response.statusCode}] ) => [ ${bodyLen} bytes ]\n")
|
||||||
|
if (!responseBody.isEmpty()) {
|
||||||
|
let display = if (_options.simApiRequestLogOptions.showFullResponse) {
|
||||||
|
responseBody
|
||||||
|
} else if (responseBody.size > 200) {
|
||||||
|
// 对齐 C#:ShowFullResponse=false 时截断到 200 字符(长度仍显示完整字节数)
|
||||||
|
responseBody[0..200] + "...(${responseBody.size})"
|
||||||
|
} else {
|
||||||
|
responseBody
|
||||||
|
}
|
||||||
|
// 去掉响应体末尾换行:结构体最后不多加换行(logger 收尾会补一个换行)
|
||||||
|
var text = display
|
||||||
|
while (text.endsWith("\n")) {
|
||||||
|
text = text[0..text.size - 1]
|
||||||
|
}
|
||||||
|
sb.append(text)
|
||||||
}
|
}
|
||||||
sb.append("\n")
|
|
||||||
if (let Some(ex) <- exception) {
|
if (let Some(ex) <- exception) {
|
||||||
sb.append("Exception: ${ex.toString()}\n")
|
sb.append("\nException: ${ex.toString()}\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.info(sb.toString())
|
_logger.info(sbHead.toString() + sb.toString())
|
||||||
|
|
||||||
// 重抛原异常(对齐 C# edi?.Throw()),由外层 ExceptionMiddleware 处理
|
// 重抛原异常(对齐 C# edi?.Throw()),由外层 ExceptionMiddleware 处理
|
||||||
if (let Some(ex) <- exception) {
|
if (let Some(ex) <- exception) {
|
||||||
@@ -87,13 +120,34 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 构造完整请求 URL:stdx 服务端请求 URL 只有路径(无 scheme/host),需手动拼接。
|
||||||
|
/// 对齐 C# 的 {Scheme}://{Host}{Path}{QueryString};Host 优先取 Host 请求头。
|
||||||
|
private func buildDisplayUrl(context: HttpContext): String {
|
||||||
|
var sb = StringBuilder()
|
||||||
|
let scheme = context.request.scheme
|
||||||
|
sb.append(if (scheme.isEmpty()) { "http" } else { scheme })
|
||||||
|
sb.append("://")
|
||||||
|
let hostHeader = context.request.headers.get("Host") ?? ""
|
||||||
|
if (!hostHeader.isEmpty()) {
|
||||||
|
sb.append(hostHeader)
|
||||||
|
} else {
|
||||||
|
let host = context.request.host.toString()
|
||||||
|
if (!host.isEmpty()) {
|
||||||
|
sb.append(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append(context.request.path.toString())
|
||||||
|
sb.append(context.request.queryString.toString())
|
||||||
|
sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
private func serializeHeaders(context: HttpContext): String {
|
private func serializeHeaders(context: HttpContext): String {
|
||||||
var sb = StringBuilder()
|
var sb = StringBuilder()
|
||||||
sb.append("{")
|
sb.append("{")
|
||||||
var first = true
|
var first = true
|
||||||
for ((name, values) in context.request.headers) {
|
for ((name, values) in context.request.headers) {
|
||||||
if (!first) { sb.append(",") }
|
if (!first) { sb.append(",") }
|
||||||
sb.append("\"${SimApiJson.escapeJson(name)}\":\"${SimApiJson.escapeJson(joinValues(values))}\"")
|
sb.append("\"${SimApiUtil.escapeJson(name)}\":\"${SimApiUtil.escapeJson(joinValues(values))}\"")
|
||||||
first = false
|
first = false
|
||||||
}
|
}
|
||||||
sb.append("}\n")
|
sb.append("}\n")
|
||||||
@@ -122,16 +176,26 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
|||||||
read = context.request.body.read(buffer)
|
read = context.request.body.read(buffer)
|
||||||
}
|
}
|
||||||
let bodyText = sb.toString()
|
let bodyText = sb.toString()
|
||||||
// 重置流位置,供后续业务读取
|
// 重置流位置,供后续业务读取;同时缓存 body(流可能不可重读)
|
||||||
if (let seekable: Seekable <- context.request.body) {
|
if (let seekable: Seekable <- context.request.body) {
|
||||||
seekable.seek(SeekPosition.Begin(0))
|
seekable.seek(SeekPosition.Begin(0))
|
||||||
}
|
}
|
||||||
|
context.items["SimApi:BodyCache"] = bodyText
|
||||||
return truncateBody(bodyText)
|
return truncateBody(bodyText)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
return "(读取请求体失败)\n"
|
return "(读取请求体失败)\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 响应体读取:从 SimApiResponseWriter 缓存取完整响应体;未捕获到时返回空串
|
||||||
|
/// (长度与截断由调用方处理)
|
||||||
|
private func readResponseBody(context: HttpContext): String {
|
||||||
|
match (context.items.get(SimApiResponseWriter.responseBodyCacheKey)) {
|
||||||
|
case Some(v) => if (let s: String <- v) { s } else { "" }
|
||||||
|
case None => ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 请求体截断:JSON 字段级截断(对齐 C#:仅对超长字符串字段截断),非 JSON 则整串截断
|
/// 请求体截断:JSON 字段级截断(对齐 C#:仅对超长字符串字段截断),非 JSON 则整串截断
|
||||||
private func truncateBody(body: String): String {
|
private func truncateBody(body: String): String {
|
||||||
let maxLen = _options.simApiRequestLogOptions.requestStringLogLength
|
let maxLen = _options.simApiRequestLogOptions.requestStringLogLength
|
||||||
@@ -139,26 +203,27 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
|||||||
return body + "\n"
|
return body + "\n"
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let jv = JsonValue.fromStr(body)
|
// 用 simapi_serialization 解析为动态结构,仅截断超长字符串字段(保持结构)
|
||||||
match (jv.kind()) {
|
let map = JsonSerializer.Deserialize<HashMap<String, Any>>(body)
|
||||||
case JsObject =>
|
var sb = StringBuilder()
|
||||||
let obj = jv.asObject()
|
sb.append("{")
|
||||||
let newObj = JsonObject()
|
var first = true
|
||||||
for ((k, v) in obj.getFields()) {
|
for ((k, v) in map) {
|
||||||
match (v.kind()) {
|
if (!first) { sb.append(",") }
|
||||||
case JsString =>
|
sb.append("\"${SimApiUtil.escapeJson(k)}\":")
|
||||||
let str = v.asString().getValue()
|
if (let s: String <- v) {
|
||||||
if (str.size > maxLen) {
|
if (s.size > maxLen) {
|
||||||
newObj.put(k, JsonString(str[0..maxLen] + "...(${str.size})"))
|
sb.append("\"${SimApiUtil.escapeJson(s[0..maxLen])}...(${s.size})\"")
|
||||||
} else {
|
} else {
|
||||||
newObj.put(k, v)
|
sb.append("\"${SimApiUtil.escapeJson(s)}\"")
|
||||||
}
|
|
||||||
case _ => newObj.put(k, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return newObj.toString() + "\n"
|
} else {
|
||||||
case _ => ()
|
sb.append(SimApiUtil.json(Some(v)))
|
||||||
|
}
|
||||||
|
first = false
|
||||||
}
|
}
|
||||||
|
sb.append("}")
|
||||||
|
return sb.toString() + "\n"
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
// 非 JSON 或解析失败:整串按长度截断
|
// 非 JSON 或解析失败:整串按长度截断
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package simapi.models
|
package simcu::simapi.models
|
||||||
|
|
||||||
import std.collection.*
|
import std.collection.*
|
||||||
import std.reflect.*
|
import std.reflect.*
|
||||||
import std.time.*
|
import std.time.*
|
||||||
import simapi.helpers.*
|
import simcu::simapi.helpers.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实体基类(对齐 C# Models/SimApiBaseModel)。
|
* 实体基类(对齐 C# Models/SimApiBaseModel)。
|
||||||
@@ -28,7 +28,6 @@ public open class SimApiBaseModel {
|
|||||||
/// UpdateTime 更新的字段名
|
/// UpdateTime 更新的字段名
|
||||||
protected var _updatedTimeField: String = "_updatedAt"
|
protected var _updatedTimeField: String = "_updatedAt"
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 反射映射:把 source 的同名同类型非忽略字段赋值到 this(对齐 C# MapData(source, mapAll))。
|
* 反射映射:把 source 的同名同类型非忽略字段赋值到 this(对齐 C# MapData(source, mapAll))。
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
|
||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
|
||||||
*/
|
|
||||||
|
|
||||||
package simapi
|
|
||||||
|
|
||||||
import simapi.communications.*
|
|
||||||
import simapi.configurations.*
|
|
||||||
import simapi.exceptions.*
|
|
||||||
import simapi.extensions.*
|
|
||||||
import simapi.helpers.*
|
|
||||||
import simapi.interfaces.*
|
|
||||||
import simapi.macros.*
|
|
||||||
import simapi.middlewares.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SimApi 仓颉版:ASP.NET Core 风格 API 基础框架。
|
|
||||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net)。
|
|
||||||
*/
|
|
||||||
public class SimApi {
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 包版本号(编译期从 simapi-cj/cjpm.toml 读取)。
|
|
||||||
*/
|
|
||||||
@ReadTomlVersion[path: "../simapi-cj/cjpm.toml"]
|
|
||||||
public static let version: String = ""
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user