主题
架构概览
text
相关代码: server/index.ts, server/proxyRoutes.ts, server/tokenManager.ts系统上下文
双层代理设计
这是本系统最核心的设计。理解它需要先理解一个约束:
Leopard API 没有管理员代查功能 - 每个用户只能查看自己的订单、账单、钱包
解决方案:动态 Token 模拟用户身份
两类 API 路由
| 路由前缀 | Token 类型 | 用途 | 代码位置 |
|---|---|---|---|
/api/leopard/* | 管理员 Token | SKU 管理、产品配置 | 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 |
| memory | GB | MB | × 1024 |
| gpuMemory | GB | MB | × 1024 |
| timestamp | Date 对象 | 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') || '',
// ...
}
}架构权衡
优点
- 安全隔离 - 用户 Token 短生命周期,泄漏影响有限
- 灵活扩展 - 新增用户 API 只需在
proxyRoutes.ts添加路由 - 无状态认证 - Session 不依赖外部存储
技术债务
- N+1 查询 - SKU 列表需逐个获取
available和displayOrder字段 - 手动代理 - 未使用
http-proxy-middleware,代码量较大 - 单点故障 - Node 层无集群部署方案
下一步
- Token 管理详解 - 深入理解缓存策略
- Session 管理详解 - HMAC 签名实现