Skip to content

架构概览

text
相关代码: server/index.ts, server/proxyRoutes.ts, server/tokenManager.ts

系统上下文

双层代理设计

这是本系统最核心的设计。理解它需要先理解一个约束:

Leopard API 没有管理员代查功能 - 每个用户只能查看自己的订单、账单、钱包

解决方案:动态 Token 模拟用户身份

两类 API 路由

路由前缀Token 类型用途代码位置
/api/leopard/*管理员 TokenSKU 管理、产品配置server/index.ts:145
/api/ghippo/*管理员 Token用户列表查询server/index.ts:151
/proxy/users/:userId/*动态用户 Token订单/账单/钱包查询server/proxyRoutes.ts

Token 管理机制

生命周期

阶段触发条件操作
创建首次查询用户数据调用 Ghippo 创建 AccessToken
缓存创建成功后内存 Map + data/tokens.json
使用后续查询直接从内存获取
过期超过 TTL(默认 1 小时)定时任务清理
重启服务重启读取文件,调用 Ghippo 删除残留 Token

为什么要文件持久化?

Token 在 Ghippo 侧是真实资源。服务重启后内存丢失,但 Ghippo 侧的 Token 仍然存在。

文件存储 aid(Token ID)的目的是:重启时读取文件,逐个调用 DELETE API 清理,避免 Token 泄漏。

安全设计

文件只存 aid,不存实际 token。即使文件泄漏,攻击者也无法获取有效凭证。

认证机制

Session Token 结构

{payload}.{signature}
  • payload: Base64URL 编码的 JSON { username, exp }
  • signature: HMAC-SHA256(payload, SECRET)

无状态验证:服务端不存储 session,通过签名验证有效性,通过 exp 检查过期。

数据转换层

单位转换

字段前端显示API 存储转换公式
price微元× 1,000,000
memoryGBMB× 1024
gpuMemoryGBMB× 1024
timestampDate 对象Unix ms (string)Date.now().toString()

EAV → 扁平结构

typescript
// server/services/api.ts
const mapLeopardSkuToFrontend = (item: any): SKU => {
  const fields = item.specFields || []
  const fieldMap = new Map(fields.map((f: any) => [f.key, f.value]))

  return {
    gpuModel: fieldMap.get('gpu-model') || '',
    gpuType: fieldMap.get('gpu-type') || '',
    // ...
  }
}

架构权衡

优点

  1. 安全隔离 - 用户 Token 短生命周期,泄漏影响有限
  2. 灵活扩展 - 新增用户 API 只需在 proxyRoutes.ts 添加路由
  3. 无状态认证 - Session 不依赖外部存储

技术债务

  1. N+1 查询 - SKU 列表需逐个获取 availabledisplayOrder 字段
  2. 手动代理 - 未使用 http-proxy-middleware,代码量较大
  3. 单点故障 - Node 层无集群部署方案

下一步