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