first version

This commit is contained in:
2026-08-16 12:46:15 +08:00
commit 932fce1b9e
40 changed files with 4005 additions and 0 deletions
+442
View File
@@ -0,0 +1,442 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
*/
package simapi.helpers
import std.collection.*
import std.random.*
import stdx.crypto.digest.*
import stdx.encoding.base64.*
/**
* AES-256-CBC + PKCS7 加解密工具(对齐 C# SimApiAesUtil)。
*
* 约定(与 C# 完全一致):
* - 密钥:SHA256(key 字符串) → 32 字节
* - 模式:AES-256-CBCPKCS7 填充
* - IV:每次加密随机生成 16 字节,前置在密文前
* - 输出:Base64(IV(16) + 密文)
*
* 仓颉生态(stdx / soulsoft)均无现成 AES 实现,此处纯仓颉实现 FIPS-197 AES-256。
*/
public class SimApiAesUtil {
private init() {}
/**
* AES 加密:Base64(随机IV + 密文)。
* @param plainText 明文。
* @param key 字符串密钥(SHA256 处理后为 256 位)。
* @return Base64(IV + 密文)。
*/
public static func encrypt(plainText: String, key: String): String {
if (plainText.isEmpty()) {
throw Exception("plainText 不能为空")
}
if (key.isEmpty()) {
throw Exception("key 不能为空")
}
let keyBytes = processKey(key)
let iv = generateIv()
let padded = pkcs7Pad(plainText.toArray())
let cipher = cbcEncrypt(padded, keyBytes, iv)
// IV + 密文 → Base64
var out = ArrayList<Byte>()
for (b in iv) {
out.add(toB(b))
}
for (b in cipher) {
out.add(toB(b))
}
toBase64String(out.toArray())
}
/**
* AES 解密。
* @param cipherText Base64(IV + 密文)。
* @param key 字符串密钥(与加密时相同)。
* @return 明文。
*/
public static func decrypt(cipherText: String, key: String): String {
if (cipherText.isEmpty()) {
throw Exception("cipherText 不能为空")
}
if (key.isEmpty()) {
throw Exception("key 不能为空")
}
let all = fromBase64String(cipherText).getOrThrow { Exception("Base64 解码失败") }
if (all.size < 32) {
throw Exception("密文长度非法")
}
var iv = Array<UInt8>(16, repeat: 0)
var cipher = Array<UInt8>(all.size - 16, repeat: 0)
for (i in 0..16) {
iv[i] = toU8(all[i])
}
for (i in 0..cipher.size) {
cipher[i] = toU8(all[i + 16])
}
let keyBytes = processKey(key)
let padded = cbcDecrypt(cipher, keyBytes, iv)
let plain = pkcs7Unpad(padded)
String.fromUtf8(toBytes(plain))
}
// ===== 密钥与 IV =====
/// SHA256(key) → 32 字节密钥
@OverflowWrapping
private static func processKey(key: String): Array<UInt8> {
let md = SHA256()
md.write(key.toArray())
let digest = md.finish()
var out = Array<UInt8>(digest.size, repeat: 0)
for (i in 0..digest.size) {
out[i] = toU8(digest[i])
}
out
}
/// 16 字节随机 IV
private static func generateIv(): Array<UInt8> {
let rnd = Random()
var iv = Array<UInt8>(16, repeat: 0)
for (i in 0..16) {
iv[i] = UInt8(rnd.nextUInt64() & 0xFFu64)
}
iv
}
// ===== PKCS7 填充 =====
@OverflowWrapping
private static func pkcs7Pad(data: Array<Byte>): Array<UInt8> {
let padLen = 16 - (data.size % 16)
var out = ArrayList<UInt8>()
for (b in data) {
out.add(toU8(b))
}
for (i in 0..padLen) {
out.add(UInt8(padLen))
}
out.toArray()
}
@OverflowWrapping
private static func pkcs7Unpad(data: Array<UInt8>): Array<UInt8> {
if (data.size == 0) {
return Array<UInt8>(0, repeat: 0)
}
let padLen = Int64(data[data.size - 1])
if (padLen < 1 || padLen > 16) {
throw Exception("PKCS7 填充非法")
}
data[0..data.size - padLen]
}
// ===== CBC 模式 =====
@OverflowWrapping
private static func cbcEncrypt(padded: Array<UInt8>, key: Array<UInt8>, iv: Array<UInt8>): Array<UInt8> {
let roundKeys = keyExpansion(key)
var prev = Array<UInt8>(16, repeat: 0)
for (i in 0..16) {
prev[i] = iv[i]
}
var out = ArrayList<UInt8>()
let n = padded.size / 16
for (block in 0..n) {
var state = Array<UInt8>(16, repeat: 0)
for (i in 0..16) {
state[i] = padded[block * 16 + i] ^ prev[i]
}
let enc = aesEncryptBlock(state, roundKeys)
for (b in enc) {
out.add(b)
}
prev = enc
}
out.toArray()
}
@OverflowWrapping
private static func cbcDecrypt(cipher: Array<UInt8>, key: Array<UInt8>, iv: Array<UInt8>): Array<UInt8> {
let roundKeys = keyExpansion(key)
var prev = Array<UInt8>(16, repeat: 0)
for (i in 0..16) {
prev[i] = iv[i]
}
var out = ArrayList<UInt8>()
let n = cipher.size / 16
for (block in 0..n) {
var blockBytes = Array<UInt8>(16, repeat: 0)
for (i in 0..16) {
blockBytes[i] = cipher[block * 16 + i]
}
let dec = aesDecryptBlock(blockBytes, roundKeys)
for (i in 0..16) {
out.add(dec[i] ^ prev[i])
}
prev = blockBytes
}
out.toArray()
}
// ===== AES-256 核心(FIPS-197 =====
/// S-box / 逆 S-box(运行时生成,避免手写 256 字节表出错)
private static let sbox: Array<UInt8> = generateSbox()
private static let invSbox: Array<UInt8> = generateInvSbox()
@OverflowWrapping
private static func generateSbox(): Array<UInt8> {
var s = Array<UInt8>(256, repeat: 0)
var p = 1u8
var q = 1u8
while (true) {
// p *= 3GF(2^8) 生成元遍历)
p = p ^ (p << 1u8) ^ (if ((p & 0x80u8) != 0u8) { 0x1Bu8 } else { 0u8 })
// q /= 3(等价乘以 0xF6
q = q ^ (q << 1u8)
q = q ^ (q << 2u8)
q = q ^ (q << 4u8)
q = q ^ (if ((q & 0x80u8) != 0u8) { 0x09u8 } else { 0u8 })
// 仿射变换
let x = q ^ rotl8(q, 1) ^ rotl8(q, 2) ^ rotl8(q, 3) ^ rotl8(q, 4)
s[Int64(p)] = x ^ 0x63u8
if (p == 1u8) {
break
}
}
// p 序列遍历非零元素,s[0] 从未赋值:AES 规定 S(0) = 0x63(0 的逆为 0,仿射变换结果)
s[0] = 0x63u8
s
}
@OverflowWrapping
private static func generateInvSbox(): Array<UInt8> {
var inv = Array<UInt8>(256, repeat: 0)
for (i in 0..256) {
inv[Int64(sbox[i])] = UInt8(i)
}
inv
}
/// 8 位循环左移
private static func rotl8(v: UInt8, n: Int64): UInt8 {
UInt8(((UInt16(v) << n) | (UInt16(v) >> (8 - n))) & 0xFFu16)
}
/// GF(2^8) 乘以 2xtime
@OverflowWrapping
private static func xtime(a: UInt8): UInt8 {
if ((a & 0x80u8) != 0u8) {
(a << 1u8) ^ 0x1Bu8
} else {
a << 1u8
}
}
/// GF(2^8) 通用乘法
@OverflowWrapping
private static func gfMul(a: UInt8, b: UInt8): UInt8 {
var result = 0u8
var aa = a
var bb = b
for (i in 0..8) {
if ((bb & 1u8) != 0u8) {
result = result ^ aa
}
let hi = (aa & 0x80u8) != 0u8
aa = aa << 1u8
if (hi) {
aa = aa ^ 0x1Bu8
}
bb = bb >> 1u8
}
result
}
/// 密钥扩展:32 字节密钥 → 240 字节轮密钥(60 wordsAES-256 共 15 轮)
@OverflowWrapping
private static func keyExpansion(key: Array<UInt8>): Array<UInt8> {
var w = Array<UInt8>(240, repeat: 0)
for (i in 0..32) {
w[i] = key[i]
}
var rcon = 1u8
for (i in 8..60) {
var temp = Array<UInt8>(4, repeat: 0)
for (j in 0..4) {
temp[j] = w[(i - 1) * 4 + j]
}
if (i % 8 == 0) {
// RotWord
let t0 = temp[0]
temp[0] = temp[1]
temp[1] = temp[2]
temp[2] = temp[3]
temp[3] = t0
// SubWord
for (j in 0..4) {
temp[j] = sbox[Int64(temp[j])]
}
temp[0] = temp[0] ^ rcon
rcon = xtime(rcon)
} else if (i % 8 == 4) {
for (j in 0..4) {
temp[j] = sbox[Int64(temp[j])]
}
}
for (j in 0..4) {
w[i * 4 + j] = w[(i - 8) * 4 + j] ^ temp[j]
}
}
w
}
@OverflowWrapping
private static func aesEncryptBlock(input: Array<UInt8>, roundKeys: Array<UInt8>): Array<UInt8> {
var state = Array<UInt8>(16, repeat: 0)
for (i in 0..16) {
state[i] = input[i]
}
addRoundKey(state, roundKeys, 0)
for (round in 1..14) {
subBytes(state)
shiftRows(state)
mixColumns(state)
addRoundKey(state, roundKeys, round)
}
subBytes(state)
shiftRows(state)
addRoundKey(state, roundKeys, 14)
state
}
@OverflowWrapping
private static func aesDecryptBlock(input: Array<UInt8>, roundKeys: Array<UInt8>): Array<UInt8> {
var state = Array<UInt8>(16, repeat: 0)
for (i in 0..16) {
state[i] = input[i]
}
addRoundKey(state, roundKeys, 14)
// 轮 13..1(逆序)
for (i in 0..13) {
let round = 13 - i
invShiftRows(state)
invSubBytes(state)
addRoundKey(state, roundKeys, round)
invMixColumns(state)
}
invShiftRows(state)
invSubBytes(state)
addRoundKey(state, roundKeys, 0)
state
}
@OverflowWrapping
private static func addRoundKey(state: Array<UInt8>, roundKeys: Array<UInt8>, round: Int64): Unit {
for (i in 0..16) {
state[i] = state[i] ^ roundKeys[round * 16 + i]
}
}
@OverflowWrapping
private static func subBytes(state: Array<UInt8>): Unit {
for (i in 0..16) {
state[i] = sbox[Int64(state[i])]
}
}
@OverflowWrapping
private static func invSubBytes(state: Array<UInt8>): Unit {
for (i in 0..16) {
state[i] = invSbox[Int64(state[i])]
}
}
/// ShiftRows:行 r 循环左移 r 字节(列主序 state[i] = s[r + 4*c]
@OverflowWrapping
private static func shiftRows(state: Array<UInt8>): Unit {
var tmp = Array<UInt8>(16, repeat: 0)
for (r in 0..4) {
for (c in 0..4) {
tmp[r + 4 * c] = state[r + 4 * ((c + r) % 4)]
}
}
for (i in 0..16) {
state[i] = tmp[i]
}
}
/// InvShiftRows:行 r 循环右移 r 字节
@OverflowWrapping
private static func invShiftRows(state: Array<UInt8>): Unit {
var tmp = Array<UInt8>(16, repeat: 0)
for (r in 0..4) {
for (c in 0..4) {
tmp[r + 4 * c] = state[r + 4 * ((c - r % 4 + 4) % 4)]
}
}
for (i in 0..16) {
state[i] = tmp[i]
}
}
/// MixColumns:每列乘固定矩阵 [[2,3,1,1],[1,2,3,1],[1,1,2,3],[3,1,1,2]]
@OverflowWrapping
private static func mixColumns(state: Array<UInt8>): Unit {
for (c in 0..4) {
let a0 = state[0 + 4 * c]
let a1 = state[1 + 4 * c]
let a2 = state[2 + 4 * c]
let a3 = state[3 + 4 * c]
state[0 + 4 * c] = gfMul(a0, 2u8) ^ gfMul(a1, 3u8) ^ a2 ^ a3
state[1 + 4 * c] = a0 ^ gfMul(a1, 2u8) ^ gfMul(a2, 3u8) ^ a3
state[2 + 4 * c] = a0 ^ a1 ^ gfMul(a2, 2u8) ^ gfMul(a3, 3u8)
state[3 + 4 * c] = gfMul(a0, 3u8) ^ a1 ^ a2 ^ gfMul(a3, 2u8)
}
}
/// InvMixColumns:每列乘逆矩阵 [[14,11,13,9],[9,14,11,13],[13,9,14,11],[11,13,9,14]]
@OverflowWrapping
private static func invMixColumns(state: Array<UInt8>): Unit {
for (c in 0..4) {
let a0 = state[0 + 4 * c]
let a1 = state[1 + 4 * c]
let a2 = state[2 + 4 * c]
let a3 = state[3 + 4 * c]
state[0 + 4 * c] = gfMul(a0, 14u8) ^ gfMul(a1, 11u8) ^ gfMul(a2, 13u8) ^ gfMul(a3, 9u8)
state[1 + 4 * c] = gfMul(a0, 9u8) ^ gfMul(a1, 14u8) ^ gfMul(a2, 11u8) ^ gfMul(a3, 13u8)
state[2 + 4 * c] = gfMul(a0, 13u8) ^ gfMul(a1, 9u8) ^ gfMul(a2, 14u8) ^ gfMul(a3, 11u8)
state[3 + 4 * c] = gfMul(a0, 11u8) ^ gfMul(a1, 13u8) ^ gfMul(a2, 9u8) ^ gfMul(a3, 14u8)
}
}
// ===== 字节转换 =====
/// Byte → UInt8:负 Byte>127 字节的补码)按位重解释为 0-255
private static func toU8(b: Byte): UInt8 {
if (b < 0) {
UInt8(Int64(b) + 256)
} else {
UInt8(Int64(b))
}
}
private static func toB(u: UInt8): Byte {
let b: Byte = u
b
}
private static func toBytes(data: Array<UInt8>): Array<Byte> {
var out = Array<Byte>(data.size, repeat: 0)
for (i in 0..data.size) {
out[i] = toB(data[i])
}
out
}
}
+222
View File
@@ -0,0 +1,222 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
*/
package simapi.helpers
import std.collection.*
import std.collection.concurrent.*
import std.convert.*
import stdx.encoding.json.*
import soulsoft_serialization.*
import redis.client.*
import simapi.communications.*
import simapi.configurations.*
import simapi.exceptions.*
/**
* 认证助手:基于 Header Token 的登录态管理。
* 支持两种存储模式:
* - Redis 模式:配置了 RedisConfiguration 时使用,支持多实例共享。
* - InMemory 模式:未配置 Redis 时自动使用,重启后登录态丢失。
*/
public class SimApiAuth {
private static let tokenCachePrefix = "SimApi:Auth:Token:"
private static let tokenSetCachePrefix = "SimApi:Auth:User:"
private var _redis: ?RedisClient = None
private var _redisHost: String = ""
private var _redisPort: UInt16 = 6379
// InMemory 模式:token → 登录信息 JSON
private let _tokenStore = ConcurrentHashMap<String, String>()
// InMemory 模式:userId → token 集合
private let _userTokens = ConcurrentHashMap<String, HashSet<String>>()
/**
* 创建认证助手(依赖注入 SimApiOptions)。
* @param options SimApi 配置(redisConfiguration 非空时使用 Redis,否则 InMemory)。
*/
public init(options: SimApiOptions) {
let redisConfiguration = options.redisConfiguration
if (!redisConfiguration.isEmpty()) {
let (host, port) = parseRedisConfig(redisConfiguration)
_redisHost = host
_redisPort = port
_redis = Some(RedisClient(host, port))
}
}
/**
* 登录:生成 token 并保存登录信息。
* @param loginItem 登录信息。
* @param expireSeconds 过期秒数,默认 7 天。
* @param token 指定 token(可选,默认随机生成)。
* @return 登录 token。
*/
public func login(loginItem: SimApiLoginItem, expireSeconds!: Int64 = 604800, token!: String = ""): String {
let newToken = if (token.isEmpty()) { generateToken() } else { token }
let tokenKey = "${tokenCachePrefix}${newToken}"
let setKey = "${tokenSetCachePrefix}${loginItem._id}"
let json = loginItemJson(loginItem)
if (let Some(redis) <- _redis) {
redis.set(tokenKey, Blob.fromUtf8(json), ex: Some(expireSeconds))
redis.sadd(setKey, [Blob.fromUtf8(newToken)])
redis.expire(setKey, expireSeconds)
} else {
_tokenStore[newToken] = json
var tokens = _userTokens.get(loginItem._id)
if (tokens == None) {
tokens = HashSet<String>()
_userTokens[loginItem._id] = tokens.getOrThrow()
}
tokens.getOrThrow().add(newToken)
}
return newToken
}
/**
* 更新登录信息(token 不变)。
* @param loginItem 新的登录信息。
* @param token 已有 token。
* @return 原 token。
*/
public func update(loginItem: SimApiLoginItem, token: String): String {
let tokenKey = "${tokenCachePrefix}${token}"
let json = loginItemJson(loginItem)
if (let Some(redis) <- _redis) {
redis.set(tokenKey, Blob.fromUtf8(json))
} else {
_tokenStore[token] = json
}
return token
}
/**
* 获取登录信息。
* @param token 登录 token。
* @return 登录信息;token 无效返回 None。
*/
public func getLogin(token: String): ?SimApiLoginItem {
let tokenKey = "${tokenCachePrefix}${token}"
let json: ?String
if (let Some(redis) <- _redis) {
json = match (redis.get(tokenKey)) {
case Some(blob) => Some(blob.toUtf8())
case None => None
}
} else {
json = _tokenStore.get(token)
}
return match (json) {
case Some(j) => Some(parseLoginItem(j))
case None => None
}
}
/**
* 获取某用户全部登录信息。
* @param userId 用户 ID。
* @return 登录信息数组。
*/
public func getAllLogins(userId: String): Array<SimApiLoginItem> {
let tokens = getTokensOfUser(userId)
var result = ArrayList<SimApiLoginItem>()
for (token in tokens) {
if (let Some(item) <- getLogin(token)) {
result.add(item)
} else {
removeTokenOfUser(userId, token)
}
}
return result.toArray()
}
/**
* 退出登录。
* @param token 登录 token。
*/
public func logout(token: String): Unit {
let item = getLogin(token)
if (let Some(item) <- item) {
removeTokenOfUser(item._id, token)
}
let tokenKey = "${tokenCachePrefix}${token}"
if (let Some(redis) <- _redis) {
redis.del([tokenKey])
} else {
_tokenStore.remove(token)
}
}
/**
* 退出某用户全部登录。
* @param userId 用户 ID。
*/
public func logoutAll(userId: String): Unit {
let tokens = getTokensOfUser(userId)
if (let Some(redis) <- _redis) {
for (token in tokens) {
redis.del(["${tokenCachePrefix}${token}"])
}
redis.del(["${tokenSetCachePrefix}${userId}"])
} else {
_userTokens.remove(userId)
for (token in tokens) {
_tokenStore.remove(token)
}
}
}
private func getTokensOfUser(userId: String): ArrayList<String> {
var result = ArrayList<String>()
if (let Some(redis) <- _redis) {
let members = redis.smembers("${tokenSetCachePrefix}${userId}")
for (m in members) {
result.add(m.toUtf8())
}
} else {
if (let Some(tokens) <- _userTokens.get(userId)) {
for (t in tokens) {
result.add(t)
}
}
}
return result
}
private func removeTokenOfUser(userId: String, token: String): Unit {
if (let Some(redis) <- _redis) {
redis.srem("${tokenSetCachePrefix}${userId}", [Blob.fromUtf8(token)])
} else {
if (let Some(tokens) <- _userTokens.get(userId)) {
tokens.remove(token)
}
}
}
private static func generateToken(): String {
// 对齐 C#token = Guid.NewGuid().ToString()(小写、8-4-4-4-12
SimApiUtil.newGuid()
}
private static func parseRedisConfig(config: String): (String, UInt16) {
let parts = config.split(":")
if (parts.size == 2) {
return (parts[0], UInt16.parse(parts[1]))
}
return (config, 6379u16)
}
private static func loginItemJson(item: SimApiLoginItem): String {
// 统一 JSON 序列化:对齐 .NET JsonSerializer.Serialize(item)
JsonSerializer.serializeObject<SimApiLoginItem>(item)
}
private static func parseLoginItem(json: String): SimApiLoginItem {
// 统一 JSON 反序列化:对齐 .NET JsonSerializer.Deserialize<SimApiLoginItem>(json)
JsonSerializer.deserializeObject<SimApiLoginItem>(json)
}
}
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
*/
package simapi.helpers
import std.collection.*
import std.collection.concurrent.*
import std.convert.*
import redis.client.*
import simapi.configurations.*
import simapi.exceptions.*
/**
* 缓存助手:Key 自动加前缀 "SimApi:Cache:"。
* 存储后端与 SimApiAuth 一致:配置了 Redis 用 Redis,否则 InMemory。
*/
public class SimApiCache {
private static let prefix = "SimApi:Cache:"
private var _redis: ?RedisClient = None
private let _store = ConcurrentHashMap<String, String>()
/**
* 创建缓存(依赖注入 SimApiOptions)。
* @param options SimApi 配置(redisConfiguration 非空时使用 Redis,否则 InMemory)。
*/
public init(options: SimApiOptions) {
let redisConfiguration = options.redisConfiguration
if (!redisConfiguration.isEmpty()) {
let (host, port) = parseRedisConfig(redisConfiguration)
_redis = Some(RedisClient(host, port))
}
}
/**
* 设置缓存。
* @param key 缓存键。
* @param value 缓存值(不能为 null)。
* @param expireSeconds 过期秒数(可选)。
*/
public func set(key: String, value: String, expireSeconds!: Int64 = -1): Unit {
if (let Some(redis) <- _redis) {
if (expireSeconds > 0) {
redis.set("${prefix}${key}", Blob.fromUtf8(value), ex: Some(expireSeconds))
} else {
redis.set("${prefix}${key}", Blob.fromUtf8(value))
}
} else {
_store["${prefix}${key}"] = value
}
}
/**
* 移除缓存。
*/
public func remove(key: String): Unit {
if (let Some(redis) <- _redis) {
redis.del(["${prefix}${key}"])
} else {
_store.remove("${prefix}${key}")
}
}
/**
* 缓存 Key 是否存在。
*/
public func hasKey(key: String): Bool {
get(key) != None
}
/**
* 获取 string 类型缓存。
*/
public func get(key: String): ?String {
if (let Some(redis) <- _redis) {
return match (redis.get("${prefix}${key}")) {
case Some(blob) => Some(blob.toUtf8())
case None => None
}
}
return _store.get("${prefix}${key}")
}
private static func parseRedisConfig(config: String): (String, UInt16) {
let parts = config.split(":")
if (parts.size == 2) {
return (parts[0], UInt16.parse(parts[1]))
}
return (config, 6379u16)
}
}
+104
View File
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
* Helpers/SimApiControllerScanner:自动发现调用者包中的 MVC 控制器。
*
* 对齐 C# 的控制器发现机制:
* - C# 通过 StackTrace 获取调用程序集,再 Assembly.GetTypes() 扫描所有类型
* - 仓颉版通过 Error.getStackTrace() 获取调用者包名,再 PackageInfo 枚举类型,
* 过滤出继承 Controller 的类型(含子包)
*/
package simapi.helpers
import std.collection.*
import std.core.*
import std.reflect.*
import soulsoft_web_mvc.core.*
/**
* 控制器自动扫描器:从调用栈定位调用者包,枚举该包(含子包)中继承 Controller 的类型。
*/
public class SimApiControllerScanner {
private init() {}
/**
* 扫描调用者包及其所有子包中的控制器类型。
* @return 找到的控制器类型列表(不含抽象类型与 Controller 基类本身)。
*/
public static func scan(): Array<TypeInfo> {
let callerPackage = getCallerPackage()
var result = ArrayList<TypeInfo>()
collectControllers(callerPackage, result)
result.toArray()
}
/**
* 获取调用者(应用)包名:遍历栈帧,跳过 simapi/soulsoft/std 等框架包,
* 返回第一个应用包的 declaringClass(对齐 C# 通过 StackTrace 找调用程序集)。
*/
public static func getCallerPackage(): String {
try {
throw Exception("probe")
} catch (ex: Exception) {
let st = ex.getStackTrace()
for (el in st) {
let name = el.declaringClass
// 跳过本类及框架包
if (name.isEmpty()) {
continue
}
if (name.startsWith("simapi.") || name == "simapi") {
continue
}
if (name.startsWith("soulsoft_") || name.startsWith("std.") || name.startsWith("stdx.")) {
continue
}
return name
}
return ""
}
}
/**
* 收集指定包及其子包中的控制器类型。
*/
private static func collectControllers(packageName: String, result: ArrayList<TypeInfo>): Unit {
if (packageName.isEmpty()) {
return
}
try {
let info = PackageInfo.get(packageName)
for (ti in info.typeInfos) {
if (isController(ti)) {
result.add(ti)
}
}
// 递归扫描子包(subPackages 的 name 是短名,需拼接全限定名)
for (sub in info.subPackages) {
collectControllers("${packageName}.${sub.name}", result)
}
} catch (_: Exception) {
// 包不存在时跳过
}
}
/**
* 判断类型是否为控制器:
* - 继承 Controllersoulsoft_web_mvc.core.Controller
* - 非抽象类
* - 不是 Controller 基类本身
*/
private static func isController(typeInfo: TypeInfo): Bool {
if (let classTypeInfo: ClassTypeInfo <- typeInfo) {
if (classTypeInfo.isAbstract()) {
return false
}
if (typeInfo == TypeInfo.of<Controller>()) {
return false
}
return typeInfo.isSubtypeOf(TypeInfo.of<Controller>())
}
false
}
}
+64
View File
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
*/
package simapi.helpers
import simapi.exceptions.*
/**
* 错误抛出辅助类:所有业务错误统一通过这里抛出 SimApiException。
* 对应 C# 的 SimApi.Helpers.SimApiError。
*/
public class SimApiError {
private init() {}
/**
* 直接抛错。
* @param code 错误代码,默认 500。
* @param message 错误描述,默认空(由 code 自动带取描述)。
*/
public static func error(code!: Int64 = 500, message!: String = ""): Unit {
throw SimApiException(code, message: message)
}
/**
* 条件为 true 时抛错。
* @param condition 检测条件。
* @param code 错误代码,默认 400。
* @param message 错误描述。
*/
public static func errorWhen(condition: Bool, code!: Int64 = 400, message!: String = ""): Unit {
if (condition) {
error(code: code, message: message)
}
}
/**
* 条件为 true 时抛错(别名)。
*/
public static func errorWhenTrue(condition: Bool, code!: Int64 = 400, message!: String = ""): Unit {
errorWhen(condition, code: code, message: message)
}
/**
* 条件为 false 时抛错。
*/
public static func errorWhenFalse(condition: Bool, code!: Int64 = 400, message!: String = ""): Unit {
errorWhen(!condition, code: code, message: message)
}
/**
* 给定的可选值为 None 时抛错。
* @param condition 检测的可选值。
* @param code 错误代码,默认 404。
* @param message 错误描述。
*/
public static func errorWhenNone(condition: ?Any, code!: Int64 = 404, message!: String = ""): Unit {
match (condition) {
case None => error(code: code, message: message)
case _ => ()
}
}
}
+169
View File
@@ -0,0 +1,169 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
*/
package simapi.helpers
import std.collection.*
import std.io.*
import stdx.net.tls.*
import stdx.net.tls.common.*
import soulsoft_net_http.{HttpClient, HttpRequestMessage, JsonContent}
import soulsoft_net_http.{HttpMethod as NetHttpMethod}
import soulsoft_serialization.*
import simapi.communications.*
import simapi.configurations.*
import simapi.exceptions.*
/**
* HTTP 客户端:用于调用其他带签名/AES 的 SimApi 服务。
* 对齐 C# 的 SimApi.Helpers.SimApiHttpClient
* - 内部使用 soulsoft_net_http 的 HttpClient(等价 .NET 的 System.Net.Http.HttpClient
* - 返回泛型 T(反序列化响应 body 的 data 字段),不再返回 String
* @param T 响应 data 的数据类型(需实现 ISerialization<T>,如 SimApiLoginItem、String、Int64 等)。
*/
public class SimApiHttpClient {
public var server: String
public var appId: String
public var appKey: String
public var signName: String = "sign"
public var timestampName: String = "timestamp"
public var nonceName: String = "nonce"
public var appIdName: ?String = Some("appId")
public var signFields: Array<String> = []
public init(options!: SimApiOptions = SimApiOptions()) {
let httpOptions = options.simApiHttpClientOptions
server = httpOptions.server
appId = httpOptions.appId
appKey = httpOptions.appKey
signName = httpOptions.signName
timestampName = httpOptions.timestampName
nonceName = httpOptions.nonceName
appIdName = httpOptions.appIdName
signFields = httpOptions.signFields
}
/**
* 发起签名请求(GET query 签名 + POST body)。
* 对齐 C# SignQuery<T>query 串 = SignFields + AppId + timestamp + nonce,整体拼 AppKey 取 MD5 作为 sign。
* @param url 请求路径(相对路径,自动拼接 server)。
* @param body 请求体 JSON 字符串(可选)。
* @param queries 额外查询参数(可选)。
* @return 响应 data 字段反序列化后的 T。
*/
public func signQuery<T>(url: String, body!: String = "", queries!: HashMap<String, String> = HashMap<String, String>()): T where T <: ISerialization<T> {
var queryUrl = StringBuilder()
for (field in signFields) {
queryUrl.append("${field}=")
if (let Some(v) <- queries.get(field)) {
queryUrl.append(v)
}
queryUrl.append("&")
}
if (let Some(name) <- appIdName) {
queryUrl.append("${name}=${appId}&")
}
let timestamp = Int64(SimApiUtil.timestampNow)
let nonce = generateNonce()
queryUrl.append("${timestampName}=${timestamp}&${nonceName}=${nonce}")
let signStr = "${queryUrl.toString()}&${appKey}"
var path = "${server}${url}?${queryUrl.toString()}&${signName}=${SimApiUtil.md5(signStr)}"
for ((k, v) in queries) {
if (!signFields.contains(k)) {
path = "${path}&${k}=${v}"
}
}
return query<T>(path, body)
}
/**
* 发起 AES 加密请求:body 加密后放入 {"data": "..."} 提交。
* 对齐 C# AesQuery<T>SimApiOneFieldRequest<string> { Data = Encrypt(body, AppKey) })。
* @param url 请求路径(相对路径,自动拼接 server)。
* @param body 请求体 JSON 字符串。
* @return 响应 data 字段反序列化后的 T。
*/
public func aesQuery<T>(url: String, body: String): T where T <: ISerialization<T> {
var target = "${server}${url}"
if (let Some(name) <- appIdName) {
target = "${target}?${name}=${appId}"
}
let encrypted = aesEncrypt(body)
let req = "{\"data\":\"${encrypted}\"}"
return query<T>(target, req)
}
/**
* 发起 AES 加密 + 签名请求。
* 对齐 C# AesSignQuery<T>。
* @param url 请求路径(相对路径,自动拼接 server)。
* @param body 请求体 JSON 字符串。
* @param queries 额外查询参数(可选)。
* @return 响应 data 字段反序列化后的 T。
*/
public func aesSignQuery<T>(url: String, body: String, queries!: HashMap<String, String> = HashMap<String, String>()): T where T <: ISerialization<T> {
let encrypted = aesEncrypt(body)
let req = "{\"data\":\"${encrypted}\"}"
return signQuery<T>(url, body: req, queries: queries)
}
/**
* 发起 POST 请求并反序列化 SimApiBaseResponse<T>,返回 data 字段。
* 对齐 C# Query<T>
* ErrorWhenFalse(IsSuccessStatusCode) → ReadFromJsonAsync<SimApiBaseResponse<T>> → ErrorWhen(Code != 200) → return Data。
* 注意:必须 noProxy(),否则会走系统代理(192.168.0.250:8118)导致连接被拒。
*/
private func query<T>(url: String, body: String): T where T <: ISerialization<T> {
let client = HttpClient.create { builder =>
builder.noProxy()
// 支持 https:配置 TLS(信任所有证书 + SNI 域名)
var tls = TlsClientConfig()
tls.verifyMode = CertificateVerifyMode.TrustAll
let host = extractHost(url)
if (!host.isEmpty()) {
tls.serverName = Some(host)
}
builder.tlsConfig(tls)
}
try {
let request = HttpRequestMessage(NetHttpMethod.Post, url)
request.content = JsonContent.create(body)
let response = client.send(request)
try {
SimApiError.errorWhenFalse(response.isSuccessStatusCode, code: response.statusCode, message: "HTTP ERROR: ${response.statusCode}")
let result = response.content.readFromJson<SimApiResponse<T>>()
SimApiError.errorWhen(result._code != 200, code: result._code, message: result._message)
return result._data.getOrThrow()
} finally {
response.close()
}
} finally {
client.close()
}
}
private func aesEncrypt(plain: String): String {
// 对齐 C#SimApiAesUtil.Encrypt(plain, AppKey)AES-256-CBC + PKCS7Base64(IV + 密文)
SimApiAesUtil.encrypt(plain, appKey)
}
private static func generateNonce(): String {
// 对齐 C#nonce 直接用 Guid.NewGuid()
SimApiUtil.newGuid()
}
/// 从完整 URL 提取 hosthttps://host[:port]/path → host)。
private static func extractHost(fullUrl: String): String {
match (fullUrl.indexOf("://")) {
case Some(i) =>
let rest = fullUrl[i + 3..]
let slash = rest.indexOf("/") ?? rest.size
let q = rest.indexOf("?") ?? rest.size
let end = if (slash < q) { slash } else { q }
return rest[0..end]
case None => return ""
}
}
}
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
*/
package simapi.helpers
import soulsoft_web_http.*
import simapi.communications.*
/**
* 响应封装:对写操作(Unit)返回统一成功响应,对已有 SimApiBaseResponse 透传。
* 在仓颉版中以中间件形式实现,对应 C# 的 SimApiResponseFilter。
*/
public class SimApiResponseFilter {
public init() {}
/**
* 包装响应委托:捕获下一级写入的响应内容。
* 说明:仓颉版约定各路由处理器直接返回 SimApiBaseResponse
* 由 SimApiExtensions 统一写入,本类保留供扩展使用。
*/
public func wrap(next: RequestDelegate): RequestDelegate {
return {
context =>
next(context)
if (!context.response.hasStarted) {
context.response.writeAsJson(SimApiBaseResponse())
}
}
}
}
+156
View File
@@ -0,0 +1,156 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
*/
package simapi.helpers
import std.collection.*
import std.time.*
import std.random.*
import stdx.crypto.digest.*
import stdx.encoding.hex.*
import stdx.encoding.base64.*
import std.regex.*
import simapi.communications.*
import simapi.macros.*
/**
* 工具类:对应 C# 的 SimApi.Helpers.SimApiUtil。
* 提供时间、哈希、Base64、JSON、校验等常用能力。
*/
public class SimApiUtil {
private init() {}
/**
* 当前 CST 时间(UTC+8)。
*/
public static prop cstNow: DateTime {
get() {
DateTime.nowUTC().addHours(8)
}
}
/**
* 当前秒级 Unix 时间戳(Double)。
*/
public static prop timestampNow: Float64 {
get() {
let ts = DateTime.nowUTC().toUnixTimeStamp()
Float64(ts / Duration.second)
}
}
/**
* SimApi 包版本号(编译期从 simapi-cj/cjpm.toml 读取。
* 宏在调用方项目编译时展开,cwd 为应用根,故用 ../simapi-cj 相对路径)。
*/
@ReadTomlVersion[path: "../simapi-cj/cjpm.toml"]
public static let simApiVersion: String = ""
/**
* 应用版本号(编译期从调用方项目 cjpm.toml 读取)。
*/
@ReadTomlVersion[path: "cjpm.toml"]
public static let appVersion: String = ""
/**
* MD5 加密字符串。
* @param source 源字符串。
* @return 32 位十六进制小写。
*/
public static func md5(source: String): String {
let md = MD5()
md.write(source.toArray())
toHexString(md.finish())
}
/**
* SHA1 加密字符串。
* @param source 源字符串。
* @return 40 位十六进制小写。
*/
public static func sha1(source: String): String {
let sha = SHA1()
sha.write(source.toArray())
toHexString(sha.finish())
}
/**
* SHA256 加密字符串。
* @param source 源字符串。
* @return 64 位十六进制小写。
*/
public static func sha256(source: String): String {
let sha = SHA256()
sha.write(source.toArray())
toHexString(sha.finish())
}
/**
* 字符串 Base64 编码。
*/
public static func base64Encode(str: String): String {
toBase64String(str.toArray())
}
/**
* 从 Base64 解码字符串。
*/
public static func base64Decode(base64Str: String): String {
let decoded = fromBase64String(base64Str).getOrThrow { Exception("Base64 解码失败") }
String.fromUtf8(decoded)
}
/**
* 检测手机号是否正确(中国大陆 11 位手机号)。
*/
public static func checkCell(cell: String): Bool {
Regex("^1[3456789]\\d{9}$").matches(cell)
}
/**
* 判断是否是 Email 地址(简化校验)。
*/
public static func checkEmail(email: String): Bool {
Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$").matches(email)
}
/**
* 生成 UUID v4 字符串(对齐 C# Guid.NewGuid().ToString():小写、8-4-4-4-12 连字符格式)。
* 仓颉标准库没有 GUID 生成器(stdx 的 GUID 是 stdx.net.http 内部类型),
* 此处用随机数自行构造:16 字节随机数 + 版本位(4)+ 变体位(10)。
*/
public static func newGuid(): String {
let rnd = Random()
var bytes = Array<UInt8>(16, repeat: 0u8)
let h = rnd.nextUInt64()
let l = rnd.nextUInt64()
for (i in 0..8) {
bytes[i] = UInt8((h >> UInt64(i * 8)) & 0xFFu64)
bytes[8 + i] = UInt8((l >> UInt64(i * 8)) & 0xFFu64)
}
// UUID v4:版本位(第 7 字节高 4 位 = 4),变体位(第 9 字节高 2 位 = 10)
bytes[6] = (bytes[6] & 0x0Fu8) | 0x40u8
bytes[8] = (bytes[8] & 0x3Fu8) | 0x80u8
let hex = "0123456789abcdef"
var sb = StringBuilder()
for (i in 0..16) {
if (i == 4 || i == 6 || i == 8 || i == 10) {
sb.append("-")
}
let b = bytes[i]
// 注意:String 索引返回 UInt8(字节),必须转 Rune 再 append,否则输出十进制 ASCII 码
sb.append(Rune(UInt32(hex[Int64((b >> 4u8) & 0x0Fu8)])))
sb.append(Rune(UInt32(hex[Int64(b & 0x0Fu8)])))
}
sb.toString()
}
/**
* 将对象序列化为 JSON 字符串(委托给 SimApiJson.json 统一实现,对齐 C# SimApiUtil.Json)。
*/
public static func json(obj: ?Any): String {
SimApiJson.json(obj)
}
}