refactor: 对齐 simapi-net 的 20 项差异

- SimApiLoginItem._types 默认 [user]
- 新增 SimApiIdOnlyRequest
- errorWhenNone 改名 errorWhenNull
- SimApiAuth.update 保留 TTL(Redis ttl/expire)
- SimApiCache: set 接受 Any + 新增 get<T>/getString
- SimApiHttpClient 签名字段移出 Options
- SimApiUtil: md5/sha1 支持 mode + 新增 paginate
- @SimApiAuth 支持逗号分隔多类型
- Logger: 4 位毫秒 + IsEnabled 恒 true
- 删除 /versions 端点;exceptionHandler 改为抛错
- UseSimApi 中间件顺序对齐 .NET
- README 同步更新
This commit is contained in:
2026-08-16 23:44:52 +08:00
parent 40e0726087
commit 92eea3108a
17 changed files with 142 additions and 76 deletions
+5
View File
@@ -114,7 +114,12 @@ public class SimApiAuth {
let tokenKey = "${tokenCachePrefix}${token}"
let json = loginItemJson(loginItem)
if (let Some(redis) <- _redis) {
// 保留原过期时间(对齐 C# update 不刷新 TTL):先读旧 TTL,SET 后重新续期
let ttl = redis.ttl(tokenKey)
redis.set(tokenKey, Blob.fromUtf8(json))
if (ttl > 0) {
redis.expire(tokenKey, ttl)
}
} else {
// 保留原过期时间(对齐 C# update 不刷新 TTL
let expireAt = match (_tokenStore.get(token)) {
+19 -7
View File
@@ -10,6 +10,7 @@ import std.collection.concurrent.*
import std.convert.*
import std.time.*
import redis.client.*
import soulsoft_serialization.*
import simapi.configurations.*
import simapi.exceptions.*
@@ -66,16 +67,17 @@ public class SimApiCache {
* @param value 缓存值(不能为 null)。
* @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 (expireSeconds > 0) {
redis.set("${prefix}${key}", Blob.fromUtf8(value), ex: Some(expireSeconds))
redis.set("${prefix}${key}", Blob.fromUtf8(json), ex: Some(expireSeconds))
} else {
redis.set("${prefix}${key}", Blob.fromUtf8(value))
redis.set("${prefix}${key}", Blob.fromUtf8(json))
}
} else {
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 是否存在。
*/
public func hasKey(key: String): Bool {
get(key) != None
getString(key) != None
}
/**
* 获取 string 类型缓存
* 获取特定类型缓存(对齐 C# Get<T>:从 JSON 反序列化)
*/
public func get(key: String): ?String {
public func get<T>(key: String): ?T where T <: ISerialization<T> {
match (getString(key)) {
case Some(json) => Some(JsonSerializer.deserializeObject<T>(json))
case None => None
}
}
/**
* 获取 string 类型缓存(对应 C# Get(string)Cangjie 不支持按泛型重载,故拆分为 getString/get<T>)。
*/
public func getString(key: String): ?String {
if (let Some(redis) <- _redis) {
return match (redis.get("${prefix}${key}")) {
case Some(blob) => Some(blob.toUtf8())
+1 -1
View File
@@ -55,7 +55,7 @@ public class SimApiError {
* @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) {
case None => error(code: code, message: message)
case _ => ()
-5
View File
@@ -38,11 +38,6 @@ public open class SimApiHttpClient {
server = httpOptions.server
appId = httpOptions.appId
appKey = httpOptions.appKey
signName = httpOptions.signName
timestampName = httpOptions.timestampName
nonceName = httpOptions.nonceName
appIdName = httpOptions.appIdName
signFields = httpOptions.signFields
}
/**
+45 -5
View File
@@ -61,10 +61,10 @@ public class SimApiUtil {
* @param source 源字符串。
* @return 32 位十六进制小写。
*/
public static func md5(source: String): String {
public static func md5(source: String, mode!: String = "x2"): String {
let md = MD5()
md.write(source.toArray())
toHexString(md.finish())
formatHex(md.finish(), mode)
}
/**
@@ -72,10 +72,10 @@ public class SimApiUtil {
* @param source 源字符串。
* @return 40 位十六进制小写。
*/
public static func sha1(source: String): String {
public static func sha1(source: String, mode!: String = "x2"): String {
let sha = SHA1()
sha.write(source.toArray())
toHexString(sha.finish())
formatHex(sha.finish(), mode)
}
/**
@@ -89,6 +89,28 @@ public class SimApiUtil {
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 编码。
*/
@@ -112,7 +134,8 @@ public class SimApiUtil {
}
/**
* 判断是否是 Email 地址(简化校验)
* 判断是否是 Email 地址。
* 说明:.NET 使用 System.Net.Mail.MailAddress 校验,仓颉无等价 API,此处用正则近似。
*/
public static func checkEmail(email: String): Bool {
Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$").matches(email)
@@ -189,4 +212,21 @@ public class SimApiUtil {
public static func base64DecodeTo<T>(base64Str: String): T where T <: ISerialization<T> {
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 序列化库,未移植。
}