api_keys 表字段设计
数据模型第一张表:API key 登记表——每个被登记的 API key 一行,由管理通道登记(2026-09-13 起,取代原「输入即注册」)。本文记录字段清单、语义与决策理由;usage_logs 表待对齐后另文记录。
| 项目 |
内容 |
| 状态 |
定稿(2026-09-11,逐字段讨论收敛;2026-09-13 修订:key 登记改由管理通道执行、表名更改为 api_keys、新增 disabled_source 列) |
| 关联 |
docs/upstream/NewAPI-sk-key接口文档.md(接口①字段依据)· docs/upstream/NewAPI-接口范围.md · docs/design/API契约.md §2.2(登记 / 启停端点) |
| 定稿范围 |
列结构 + 语义 + 写入时机;同步引擎细节(401 阈值等)不在此文 |
| 字段数 |
17 = 9 本系统管理字段 + 8 上游接口字段 |
1. 表定位
- 一行 = 一个登记过的 API key:在管理通道登记一次,这里多一行;之后所有轮询同步、查询都围绕它转
- 它是三个基本问题的答案所在地:
- 「这个 key 登记过吗」→
key_hash 查询命中(支撑「本地查库、未命中即提示未收录」的判定流程)
- 「该轮询哪些 key」→
status = active 的行
- 「凭证安全存哪」→
key_ciphertext(加密存储)
- 与
usage_logs 一对多:每条日志经 key_id 指向本表
2. 字段清单
2.1 管理字段(9 个,本系统产生)
| 列 |
类型 |
值来源 |
消费方 / 用途 |
id |
bigserial PK |
自增 |
usage_logs.key_id 外键指向 |
key_hash |
text UNIQUE |
登记时 HMAC-SHA256(管理员粘贴输入) |
查询命中判定(「查库优先」);不可逆 |
key_ciphertext |
bytea |
登记时 AES-256-GCM(管理员粘贴输入),nonce + 密文 + tag 打包 |
调上游时解密原文(唯一用途) |
status |
text |
401 摘除 / 手动启停 |
active / disabled;「key 已失效」提示与轮询范围 |
disabled_source |
text NULL |
401 摘除 → auto_401;手动停用 → manual;启用 / 登记时清 NULL |
管理面区分「已失效」与「已停用」(见 3.8) |
consecutive_401s |
int |
每轮同步:401 时 +1,成功清零 |
「连续 401 自动摘除」的判定状态 |
registered_at |
timestamptz |
登记时写入 |
空态文案「数据自该 key 登记起累积」 |
log_synced_at |
timestamptz NULL |
日志同步成功时写入 |
「数据截止 HH:mm」的唯一来源 |
usage_synced_at |
timestamptz NULL |
额度采集成功时写入 |
额度信息的新鲜度(UI 若做)、排查 |
2.2 上游字段(8 个,取自 GET /api/usage/token,除 object 外全部落库)
| 列 |
类型 |
上游字段 |
说明 |
token_name |
text |
name |
展示名(约定:展示名 = token_name) |
total_granted |
bigint |
total_granted |
总额度(上游原值;无限额度时无意义,照存) |
total_used |
bigint |
total_used |
累计已用 →「累计已用」列 |
total_available |
bigint |
total_available |
按上游原值存(数字上等于 granted − used,但保留原值,含无限额度时的负数行为) |
unlimited_quota |
bool |
unlimited_quota |
驱动展示分支(「累计剩余」显示「不限」而非数字) |
model_limits |
jsonb |
model_limits |
map 结构的自然落法 |
model_limits_enabled |
bool |
model_limits_enabled |
|
expires_at |
timestamptz NULL |
expires_at |
上游 0 = 永不过期 → 存 NULL(见 3.5) |
3. 决策记录(含理由)
3.1 凭证双列分离:key_hash + key_ciphertext
key_hash(HMAC-SHA256,不可逆):只服务「命中判定」——没有它,每次查询都要把全部 key 送去上游验证
key_ciphertext(AES-256-GCM,可解密):唯一用途是调上游
- 密钥
KEYS_MASTER_KEY 经 env 注入(HKDF 派生 hmac / aes 子密钥);密钥丢失 = 全部 key 需重新登记(记入部署文档)
注解:为什么是两个字段(给非密码学背景的读者)
两列各管一件事——key_hash 管「找得到」,key_ciphertext 管「用得起来」:
key_hash 像单向粉碎机:同样的输入永远产出同样的指纹,指纹却反推不出原文。输入 key 时(登记或查询),服务端算出的指纹与库中比对,命中即「已登记」,零上游调用;即便整库泄露,外人拿到的也只是指纹。选用 HMAC(而非裸哈希)是多掺一味只有服务器持有的「调料」:没有它,就无法用「猜测输入 → 对照指纹」的方式离线验证。
key_ciphertext 像保险箱:用 KEYS_MASTER_KEY 加密锁存,轮询调上游时开锁取明文;AES-GCM 自带防篡改,密文被动过则解密直接报错。
- 两列并存的原因:加密每次都掺随机数(同一 key 两次密文不同)→ 密文不能用于查找;哈希不可逆 → 指纹还原不出明文。「可查找」与「可还原」两个需求互斥,只能分存两份形态。
- 防线边界:能同时接触数据库与环境变量的人(服务器运营方)防不住——这是已接受的信任模型前提;本设计防的是「数据库单独落到第三方手里」的常见拖库场景(SQL 注入、备份泄露)。
- 实现提示:两种算法均为标准件,Bun 内置
crypto 直接提供,无需第三方库。
3.2 命名:与上游对齐
- 上游字段直接采用原名(
total_granted 等),对照 docs/upstream/NewAPI-sk-key接口文档.md 看表即懂
- 例外:
name → token_name(name 太泛;本工具以 token_name 指代展示名)
3.3 两个同步时间戳,不合并
- 两个上游调用(日志 / 额度)独立成功、独立失败——如实建模、数据溯源
- 「数据截止」语义钉死:只随日志同步成功前进;合并会导致额度接口故障时反向失真(日志已新,却报旧时间)
- 为未来的额度信息展示留新鲜度表达位(UI 方案未稳定,不轻易断言其无消费方)
3.4 上游字段「全体落库」
/api/usage/token 除 object 外 8 个字段全部入列(含当前无消费方的 model_limits 等)
- 这类数据随时可从上游重新获取,入列本质是「本地最新快照」;当前策略为宁多勿缺,待 UI 稳定后回审
3.5 expires_at 的 0 → NULL 转换
- 上游 0 = 永不过期;转 NULL 表达该语义
- 安全性依据:8 个上游字段整组一起写入(同一条 UPDATE +
usage_synced_at),「未获取」由 usage_synced_at IS NULL 承载——expires_at 的 NULL 专用于「永不过期」,语义不重叠
3.6 生命周期
active →(连续 N 次 401)→ disabled + disabled_source = auto_401(数据保留、不再轮询)→(管理通道手动启用、上游验证通过)→ active + 清 disabled_source
- 管理通道也可手动停用(
disabled_source = manual)——与 401 摘除是同一状态、不同来源(见 docs/design/交互设计.md §3.2)
- N 的取值属同步引擎设计(不在此文)
3.7 表名:从 sk_keys 更名 api_keys(2026-09-13)
- 理由:与页面
/manage/api-keys 同词对齐;sk- 只是 key 的实际格式前缀(不属表名职责),api_keys 为领域通用词,与其余三表(usage_logs / purchases / qpu_history)风格一致
3.8 新增 disabled_source:停用来源可辨(2026-09-13)
- 起因:「已失效(401 自动摘除)」与「已停用(手动)」需要在管理面区分——
status 只有 active / disabled 两值,401 自动摘除与手动停用落在同一个值上,事后无法区分
- 方案对比:
- ① 用
consecutive_401s 推断——不可靠(手动停用会保留停用前的任意值,成功同步又会清零)
- ② 拆分
status 取值(如 auto_disabled / manual_disabled)——污染状态机,启停与轮询逻辑分叉
- ③ 新增一列记录来源——最直接;取值
manual / auto_401,active 时为 NULL
- 代价:字段数 16 → 17(2026-09-13 由契约推导回头补列)
4. 写入时机(两个写入者:管理通道 / 同步引擎)
| 时机 |
动作 |
| 登记(管理通道,上游验证通过) |
INSERT 一行:管理字段(HMAC / 加密 / registered_at,disabled_source 为 NULL)+ 上游字段全组(一次 /api/usage/token 拿全)+ 两个 synced_at;首轮日志同步不阻塞登记响应(由同步引擎立即异步补一轮,见 docs/design/API契约.md §2.2) |
| 启停(管理通道) |
UPDATE status + disabled_source:启用(上游验证通过 → active + 清 NULL + consecutive_401s 清零——验证调用本身即一次成功调用)/ 停用(→ disabled + manual)——数据保留、不删除 |
| 每轮轮询 |
UPDATE:上游字段全组 + usage_synced_at;日志同步成功后写 log_synced_at;401 计数随结果更新;连续 N 次 401 → status = disabled + disabled_source = auto_401 |
- 两个上游调用各自独立:各自的成功写各自的时间戳,互不阻塞、互不污染
5. 明确不存的(已否决字段与理由)
| 字段 |
否决理由 |
key_hint(sk-…a1b2 尾号) |
登记即有 token_name,回显价值归零(注:查看面响应里的 key_hint 是即时生成的,与本列否决无关) |
| 别名 / 备注名 |
上游 token_name 已提供展示名(本地已同步该字段),再记一份是平行冗余;要改名去上游站点改(2026-09-13 定) |
upstream_token_id |
本地 id 足够;对账时日志行自有 token_id |
last_sync_error |
错误详情进服务器日志即可;UI 无展示位(有需求再加) |
object / code / message |
固定值 / 传输层包装,无信息量 |
6. 待定事项(关联后续环节)
| 事项 |
归属 |
| 401 摘除阈值 N |
同步引擎设计 |
| 多 key 时「数据截止」的聚合语义 |
已定:取最慢(docs/design/API契约.md 约定 22) |
| 「累计已用/剩余」展示格式与无限额度分支 |
交互细节层 |
usage_logs 表字段清单 |
已定稿:见 docs/design/usage_logs表字段设计.md |