Files
new-api-key-usage/docs/design/api_keys表字段设计.md
T
skycurtain 867bb7d566 docs: 命令登记与 SK_KEY_ENCRYPTION_SECRET 命名对齐
AGENTS 命令表、会话留档约定;骨架搭建指南与设计文档键名对齐代码。
2026-10-06 01:30:52 +08:00

137 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# api_keys 表字段设计
> 数据模型第一张表:API key 登记表——每个被登记的 API key 一行,**由管理通道登记**。本文记录字段清单、语义与决策理由。
| 项目 | 内容 |
|------|------|
| 状态 | 定稿(2026-10-01) |
| 关联 | `docs/references/NewAPI-接口范围.md` §2.⑧(`/api/usage/token` 字段依据)· 同文 §1(鉴权三态)· `docs/design/members表字段设计.md`(`member_id` 指向) |
| 定稿范围 | 列结构 + 语义 + 写入时机;同步引擎细节不在此文;方言映射归数据访问层设计 |
| 字段数 | 16 = 8 本系统管理字段 + 8 上游接口字段 |
---
## 1. 表定位
- **一行 = 一个登记过的 API key**:在管理通道登记一次,这里多一行;之后所有轮询同步、查询都围绕它转
- 它是四个基本问题的答案所在地:
1. 「这个 key 登记过吗」→ `key_hash` 查询命中(支撑「本地查库、未命中即提示未收录」的判定流程)
2. 「该轮询哪些 key」→ `status = active` 的行
3. 「凭证安全存哪」→ `key_ciphertext`(加密存储)
4. 「这个 key 归属哪个成员」→ `member_id`(用量统计与成员视图的归属锚点;空 = 未归属,用量归作者)
- 与 `usage_logs` 一对多:每条日志经 `api_key_id` 指向本表;与 `members` 多对一:归属列在本表(见 3.9)
## 2. 字段清单
### 2.1 管理字段(8 个,本系统产生)
| 列 | 类型 | 值来源 | 消费方 / 用途 |
|----|------|--------|---------------|
| `id` | bigserial PK | 自增 | `usage_logs.api_key_id` 外键指向 |
| `key_hash` | text UNIQUE | 登记时 HMAC-SHA256(管理员粘贴输入) | 查询命中判定(「查库优先」);不可逆 |
| `key_ciphertext` | bytea | 登记时 AES-256-GCM(管理员粘贴输入),nonce + 密文 + tag 打包 | 调上游时解密原文(唯一用途) |
| `status` | text | 管理面启停(引擎只读) | `active` / `disabled`;轮询清单 = active;disabled 数据保留、仅移出轮询 |
| `member_id` | bigint FK NULL | 归属登记(管理通道) | → `members.id`;成员视图与按成员统计的锚点;空 = 未归属 |
| `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,可解密):唯一用途是调上游
- 密钥 `SK_KEY_ENCRYPTION_SECRET` 经 env 注入(HKDF 派生 hmac / aes 子密钥);**密钥丢失 = 全部 key 需重新登记**(记入部署文档)
> **注解:为什么是两个字段(给非密码学背景的读者)**
>
> 两列各管一件事——`key_hash` 管「找得到」,`key_ciphertext` 管「用得起来」:
>
> - `key_hash` 像**单向粉碎机**:同样的输入永远产出同样的指纹,指纹却反推不出原文。输入 key 时(登记或查询),服务端算出的指纹与库中比对,命中即「已登记」,零上游调用;即便整库泄露,外人拿到的也只是指纹。选用 HMAC(而非裸哈希)是多掺一味只有服务器持有的「调料」:没有它,就无法用「猜测输入 → 对照指纹」的方式离线验证。
> - `key_ciphertext` 像**保险箱**:用 `SK_KEY_ENCRYPTION_SECRET` 加密锁存,轮询调上游时开锁取明文;AES-GCM 自带防篡改,密文被动过则解密直接报错。
> - **两列并存的原因**:加密每次都掺随机数(同一 key 两次密文不同)→ 密文不能用于查找;哈希不可逆 → 指纹还原不出明文。「可查找」与「可还原」两个需求互斥,只能分存两份形态。
> - **防线边界**:能同时接触数据库与环境变量的人(服务器运营方)防不住——这是已接受的信任模型前提;本设计防的是「数据库单独落到第三方手里」的常见拖库场景(SQL 注入、备份泄露)。
> - 实现提示:两种算法均为标准件,Bun 内置 `crypto` 直接提供,无需第三方库。
### 3.2 命名:与上游对齐
- 上游字段直接采用原名(`total_granted` 等),对照 `docs/references/NewAPI-接口范围.md` §2.⑧ 看表即懂
- 例外:`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 生命周期:纯手动开关,无 401 自动摘除
- `active` / `disabled` 两值,**唯一写入者是管理面的启停操作**——引擎对 `status` 只读
- 停用:数据保留、移出轮询清单;启用:回清单,**下一轮同步即自然验证**(成败经数据截止与引擎日志立即显形,无需人工预验证环节)
- **引擎对失败(401 / 403 / 超时等)只记录、不决策**:失败不写行——`log_synced_at` 停滞即管理面可见信号,失败明细逐轮进引擎运行日志(母体 journald / CFW Workers Logs);持续 401 的处置(停用)是作者的管理动作
- **为什么不做自动摘除**:管理决策归人(平台呈现事实——与不做分摊计算同构);作者即运维者(自动化的受众唯一);对上游瞬时误报免疫(不杀就无误杀);代价是作者不看平台期间死 key 持续消耗轮询预算(CFW Free 档有感),可见且可控
### 3.7 表名:`api_keys`
- 理由:与页面 `/manage/api-keys` 同词对齐;`sk-` 只是 key 的实际格式前缀(不属表名职责),`api_keys` 为领域通用词,与其余各表(`usage_logs` / `payments` / `qpu_history`)风格一致
### 3.8 归属列 `member_id`:外键在本表,且可空
- **为什么外键在 key 侧**:成员与 key 是 1:N——外键由 N 侧持有是标准做法;成员表的行数不随 key 数量膨胀
- **为什么可空**:未归属的 key 是真实存在的状态(测试 key、暂未分配的 key)——空值即「用量归作者」的默认归属,无需哨兵行
- **归属变更即时生效**:`member_id` 是管理通道的 UPDATE,不影响日志行(历史日志按行级归属自然分组——见 `usage_logs表字段设计.md` 3.6)
## 4. 写入时机(两个写入者:管理通道 / 同步引擎)
| 时机 | 动作 |
|------|------|
| **登记**(管理通道,上游验证通过) | INSERT 一行:管理字段(HMAC / 加密 / registered_at / 归属 `member_id`)+ 上游字段全组(一次 `/api/usage/token` 拿全)+ 两个 synced_at;**首轮日志同步不阻塞登记响应**(由同步引擎立即异步补一轮) |
| **启停**(管理通道) | UPDATE `status`:启用 → `active`(回轮询清单,下一轮同步即自然验证)/ 停用 → `disabled`(移出轮询清单)——数据保留、不删除 |
| **归属变更**(管理通道) | UPDATE `member_id`——即时生效,不触碰日志行(统计按行级归属分组,见 3.8) |
| **每轮轮询** | 仅写成功结果:UPDATE 上游字段全组 + `usage_synced_at`;日志同步成功后写 `log_synced_at`;任何失败(401 / 403 / 超时等)**不写行**——`log_synced_at` 停滞即管理面可见信号,失败明细进引擎运行日志 |
- 两个上游调用各自独立:**各自的成功写各自的时间戳,互不阻塞、互不污染**
## 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. 待定事项(关联后续环节)
| 事项 | 归属 |
|------|------|
| 轮次摘要与失败明细的观测呈现(引擎日志格式) | 同步引擎设计(待立) |
| 多 key 时「数据截止」的聚合语义 | 已定:取最慢 |
| 「累计已用/剩余」展示格式与无限额度分支 | 交互细节层 |
| `usage_logs` 表字段清单 | 见 `docs/design/usage_logs表字段设计.md` |