docs: 设计阶段定稿——需求清单(15 项全定案)与七篇表设计定稿
- 需求清单:独立计费平台定位,R1-R15 全部定案(12 项裁决 + R10 裁撤 + R3/R4 延后),裁决档案含访问主体模型(作者登录体系 + 成员长期令牌)、分摊哲学(平台不做价值判断只提供事实服务)、双形态交付(完整前后端先行,CFW 抽取版后续) - 数据模型定稿(一表一文档):api_keys(16 列,401 状态机裁撤——引擎记录不决策)/ usage_logs(25 列,billing_source 与 subscription_id 提列,非消费行去重含 content 指纹)/ payments(币种无关支付记录,只存事实)/ qpu_history(按日志时刻取值)/ members(成员实体与长期令牌)/ users(空库双通道初始化与重置)/ sessions(服务端会话,否决 JWT) - 接口参考:NewAPI-接口范围融合字段级实证,定位 docs/references/(计费平台可用接口参考,唯一上游参考) - README / AGENTS:新版设计语境(项目沿革、重开设计动因、协作约定、文档自立原则)
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
给 AI 协作者的仓库指引。**开始任何工作前,先读 [`README.md`](README.md)**——它是全景入口:项目是什么、为什么长这样、走到哪了。
|
||||||
|
|
||||||
|
## 协作约定(长期有效)
|
||||||
|
|
||||||
|
- **Git**:仅当项目作者明确说"提交 / commit"时才执行提交;`git push` 永远由项目作者手工执行,AI 绝不推送
|
||||||
|
- **命名**:需求 / 设计 / 交互 / 技术 / 计划等**文档名**用中文(`README` / `AGENTS` / `LICENSE` 等约定俗成除外)、**目录**用英文;新文档按性质放入 `docs/design/`(我们定的方案与决策)或 `docs/references/`(平台可用接口参考——实证事实,不做设计决策),并同步更新 README 的「文档地图」;`docs/deprecated/` 只归档已废弃文档——**勿引用、勿作依据**(本分支暂无此目录,需要时再建)
|
||||||
|
- **文档状态**:头部表格标「状态」(草案 → 讨论中 → 待审阅 → 定稿(日期) → 已实现);设计文档定稿后不搬家、只改状态;被取代时删除(git 历史即考古)
|
||||||
|
- **部署**:交付物必须保证「一条命令链」可部署;**双形态并行已裁决**——完整前后端(运营方服务器)先行,Cloudflare Workers 抽取版(作者自部署)后续(见 `docs/design/需求清单.md` R13)
|
||||||
|
- **不引入版本叙事**:不使用 v1/v2/v3 之类版本编号,不以「MVP」等标签或指称显式描述任何历史阶段;文档内容必须**自立**——不得引用、描述任何当前分支之外、可能被删除的分支内容
|
||||||
|
|
||||||
|
## 项目速览(速记)
|
||||||
|
|
||||||
|
- **`design`(本分支)= 新版设计文档中心,无代码**
|
||||||
|
- 数据模型:四张基线表(`api_keys` · `usage_logs` · `payments` · `qpu_history`)+ 三张访问主体表(`members` · `users` · `sessions`)设计见本目录,**七篇已定稿(2026-10-01,经作者逐篇审阅)**;同步架构(轮询 + `request_id` 去重 + 攒库)的动因由上游物理约束决定,架构结论不变
|
||||||
|
- 需求总集与待裁决项(平台身份体系、费用分摊与账单、充值折算、多订阅、数据资产保护)见 `docs/design/需求清单.md`;交付形态已裁决(双形态并行,完整形态先行)
|
||||||
|
- 上游约束速记:只有 sk-key、无时间过滤接口 → 轮询攒库是唯一路径(README「架构的根本动因」)
|
||||||
|
|
||||||
|
## 命令
|
||||||
|
|
||||||
|
本分支为纯文档分支,无构建 / 测试命令。
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# new-api-key-usage
|
||||||
|
|
||||||
|
> 针对 NewAPI 平台的 key 用量统计工具
|
||||||
|
|
||||||
|
定时用每个 sk-key 轮询网关日志接口,按 `request_id` 去重,攒全量日志到数据库;任意时间范围的用量查询,即一条本地聚合查询。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 当前状态 | **新版设计阶段**(`design` 分支,文档中心)——详细设计推进中 |
|
||||||
|
| 技术栈 | 服务器形态栈待架构设计评估(双形态交付已裁决,见 [`docs/design/需求清单.md`](docs/design/需求清单.md) R13/R14) |
|
||||||
|
|
||||||
|
## 项目沿革
|
||||||
|
|
||||||
|
| 阶段 | 形态 | 结局 |
|
||||||
|
|------|------|------|
|
||||||
|
| 初版方案 | Cloudflare Workers | 弃——当时运营方愿意提供服务器资源 |
|
||||||
|
| 新版设计(当前) | **本分支,设计进行中** | 捡回 Cloudflare Workers(**serverless 后端**:Cron 定时轮询 + 云端数据库攒库,非初版原案复刻)与完整前后端方案并列评估;同时解决管理面鉴权、充值折算、多订阅等设计问题 |
|
||||||
|
|
||||||
|
**当前设计阶段的直接动因**:服务器部署形态下,每次迭代都需实例运营方在服务器上手工升级,测试阶段迭代频繁而运营方无暇跟随;若部署到作者自控的 Cloudflare 账号(Workers + Pages),迭代部署权回到作者手里。
|
||||||
|
|
||||||
|
## 文档地图
|
||||||
|
|
||||||
|
| 文档 | 什么时候读 | 状态 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| [`docs/design/需求清单.md`](docs/design/需求清单.md) | **需求权威索引与裁决档案**——R1–R15 全部定案(含裁撤与延后),R 编号体系为后续设计文档的引用锚点 | 定稿(2026-10-01) |
|
||||||
|
| [`docs/design/api_keys表字段设计.md`](docs/design/api_keys表字段设计.md) | 数据模型第一张表:key 登记、生命周期与成员归属 | 定稿(2026-10-01) |
|
||||||
|
| [`docs/design/usage_logs表字段设计.md`](docs/design/usage_logs表字段设计.md) | 数据模型第二张表:日志明细、来源提列、去重与写入策略 | 同上 |
|
||||||
|
| [`docs/design/payments表字段设计.md`](docs/design/payments表字段设计.md) | 数据模型第三张表:支付记录(订阅/充值同构,币种无关,只存事实) | 同上 |
|
||||||
|
| [`docs/design/qpu_history表字段设计.md`](docs/design/qpu_history表字段设计.md) | 数据模型第四张表:换算参数时间线(自动采样、只读) | 同上 |
|
||||||
|
| [`docs/design/members表字段设计.md`](docs/design/members表字段设计.md) | 数据模型新增表:成员实体——key 归属、成员视图身份与按成员统计的锚点 | 定稿(2026-10-01) |
|
||||||
|
| [`docs/design/users表字段设计.md`](docs/design/users表字段设计.md) | 数据模型新增表:管理员(单行)——登录体系的身份落点 | 同上 |
|
||||||
|
| [`docs/design/sessions表字段设计.md`](docs/design/sessions表字段设计.md) | 数据模型新增表:管理面会话——cookie 令牌的服务端对应物 | 同上 |
|
||||||
|
| [`docs/references/NewAPI-接口范围.md`](docs/references/NewAPI-接口范围.md) | 计费平台在 NewAPI 中可用的接口参考:鉴权档位、请求与返回结构、字段语义、实测行为、源码出处(**唯一上游参考**) | 事实依据 |
|
||||||
|
|
||||||
|
- 命名与分区:**目录用英文、文档名用中文**;`docs/design/`(我们定的方案与决策)· `docs/references/`(平台可用接口参考——实证事实,不做设计决策)
|
||||||
|
- 生命周期:设计文档定稿后不搬家、只改状态(草案 → 讨论中 → 待审阅 → 定稿(日期) → 已实现);被取代时删除(git 历史即考古)
|
||||||
|
|
||||||
|
## 这是什么项目
|
||||||
|
|
||||||
|
- 名字直述用途:NewAPI 网关下按 key 查询用量(key usage)
|
||||||
|
- 全场景统一小写 kebab-case:仓库名 / 包名 / 文档标题一致,无大写变体
|
||||||
|
|
||||||
|
## 背景故事
|
||||||
|
|
||||||
|
- 本工具源于一个真实场景:[NewAPI](https://github.com/QuantumNous/new-api)(Go + GORM 的开源 LLM 网关)实例提供订阅服务,多个使用者通过分发 API key 共享同一份订阅额度
|
||||||
|
- 项目作者是该实例的**普通用户**:购买了订阅额度,创建多个 sk- API key 分发给多名使用者分摊使用成本——**非商业,纯成本分担**
|
||||||
|
- 关键身份约束:项目作者**只有 sk-key,没有实例的管理员账号**。因此:
|
||||||
|
- 一切数据获取只能走 key 级接口(账号级接口用 sk-key 访问会 401)
|
||||||
|
- 部署形态已裁决为**双形态并行**:完整前后端部署在实例运营方提供的服务器上(作者不接触服务器)+ Cloudflare Workers 抽取版部署在作者自控账号(见 [`docs/design/需求清单.md`](docs/design/需求清单.md) R13)
|
||||||
|
|
||||||
|
## 核心需求(基线草案,待复审;完整范围见需求清单)
|
||||||
|
|
||||||
|
| # | 需求 | 说明 |
|
||||||
|
|---|------|------|
|
||||||
|
| 1 | key 用量查询 | 按时间范围查模型请求日志与用量;作者的全员视角经管理面总览实现——「凭 sk-key 的开放查询」与「多 key 合并查询」机制已裁撤(见需求清单裁决记录) |
|
||||||
|
| 2 | key 登记 | 管理通道手动登记:登记后入库并加入轮询;查看面凭已登记的 key 查询(纯只读) |
|
||||||
|
|
||||||
|
基线刻意收缩的部分(费用分摊计算、计费周期管理、多组支持、登录页、admin 角色、会话记忆)与新增域(数据资产保护等)的完整需求盘点见 [`docs/design/需求清单.md`](docs/design/需求清单.md)。
|
||||||
|
|
||||||
|
## 架构的根本动因:为什么必须攒数据
|
||||||
|
|
||||||
|
这不是优化选项,是上游接口的物理约束决定的**唯一路径**(不随部署形态改变):
|
||||||
|
|
||||||
|
1. sk-key 可用的接口(`/api/usage/token`、`/api/log/token` 等)返回的都是**累计值**或**最近 1000 条**,没有任何接口支持时间范围过滤
|
||||||
|
2. 真正适合算“当月用量”的接口(`/api/log/self/stat`,支持时间范围 + quota 汇总)需要账号登录态,sk-key 访问 401——这是“按时间段统计用量”无法纯客户端实现的**核心障碍**
|
||||||
|
3. `/api/log/token` 硬上限 1000 条、不支持任何参数(分页 / limit / 时间过滤全被忽略)
|
||||||
|
|
||||||
|
→ 结论:轮询同步 + `request_id` 去重入库 + 数据库。**部署形态可以变(运营方服务器 / Cloudflare Workers),攒库架构不变。**上游接口完整实证(含源码出处)见 [`docs/references/NewAPI-接口范围.md`](docs/references/NewAPI-接口范围.md)。
|
||||||
|
|
||||||
|
## 关键事实清单(实测 / 源码确认,设计时不得违反)
|
||||||
|
|
||||||
|
> 出处均见 [`docs/references/NewAPI-接口范围.md`](docs/references/NewAPI-接口范围.md),此处仅索引。
|
||||||
|
|
||||||
|
1. **`QuotaPerUnit = 500000`**:quota ÷ 500000 = 额度美元——**它不是实付金额**:折算到实付人民币还需乘「购买汇率」(由运营方定价、可变,历史上调整过一次)
|
||||||
|
2. **去重必须用 `request_id`**:`/api/log/token` 返回的 `id` 是每次从 1 重编的展示序号,绝不能用于去重
|
||||||
|
3. **`created_at` 是 UTC Unix 秒**(三字段交叉实测),北京时间输入需转换
|
||||||
|
4. **1000 条硬上限**:窗口只保留最近 1000 条——实测当前站点的日志速率下约等于 3 天;5 分钟级同步余量充分,但同步停摆超过约 3 天会永久丢数据
|
||||||
|
5. **`TokenAuthReadOnly`**:用量/日志接口用宽松鉴权,key 过期 / 额度耗尽仍可查询(用完也能对账);**显式禁用会被 401 拒绝**
|
||||||
|
6. **`DisplayTokenStatEnabled` 已开启**:billing 接口返回 token 级额度
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# 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,可解密):唯一用途是调上游
|
||||||
|
- 密钥 `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/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` |
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# members 表字段设计
|
||||||
|
|
||||||
|
> 数据模型新增表之一:成员实体——「成员」维度的锚点。key 归属(经 `api_keys.member_id` 指向本表)、成员视图的身份、按成员统计的分组键。裁决背景见 `docs/design/需求清单.md` §9「访问主体模型」。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 状态 | 定稿(2026-10-01) |
|
||||||
|
| 关联 | `docs/design/api_keys表字段设计.md` 3.9(归属列指向本表)· `docs/design/需求清单.md` §9 |
|
||||||
|
| 范围 | 列结构 + 语义 + 写入时机;令牌生成细节归技术稿;方言映射归数据访问层设计 |
|
||||||
|
| 字段数 | 7 = 1 主键 + 6 业务字段 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 表定位
|
||||||
|
|
||||||
|
- **一行 = 一个分摊成员**
|
||||||
|
- 三个角色:`api_keys.member_id` 的归属目标;成员视图的访问主体(令牌校验命中本表行 + `status = active`);按成员统计的分组键
|
||||||
|
- 数据规模:个位数行
|
||||||
|
|
||||||
|
## 2. 字段清单
|
||||||
|
|
||||||
|
| 列 | 类型 | 值来源 | 说明 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| `id` | bigserial PK | 自增 | |
|
||||||
|
| `name` | text UNIQUE | 管理面录入 | 显示名——账目与视图中的署名 |
|
||||||
|
| `remark` | text | 管理面录入 | 备注(联系方式、是谁等;可空) |
|
||||||
|
| `status` | text | 管理面 | `active` / `disabled`——停用即成员视图访问立即失效(令牌校验含状态判断) |
|
||||||
|
| `token_hash` | text | 管理面生成令牌时 | 成员视图的访问凭证(哈希存储,明文只在生成时展示一次) |
|
||||||
|
| `token_issued_at` | timestamptz | 同上 | 当前令牌的签发时间(审计线索) |
|
||||||
|
| `created_at` | timestamptz | 开户时 | |
|
||||||
|
|
||||||
|
## 3. 决策记录(含理由)
|
||||||
|
|
||||||
|
### 3.1 令牌哈希存储
|
||||||
|
|
||||||
|
拖库不泄令牌(与 `api_keys.key_hash` 同思路);副作用是明文不可找回——丢了只能重发(`token_hash` + `token_issued_at` 一起更新),这正是「凭证事务经作者」的裁决语义。
|
||||||
|
|
||||||
|
### 3.2 令牌不过期(无 `token_expires_at`),且与 `sessions` 无关
|
||||||
|
|
||||||
|
- 令牌是**长期访问凭证**(access token,近 GitHub PAT 形态),不是「登录令牌」:没有会话语义——不过期、无登出、无「验证持久身份 → 换发短时会话」的过程,吊销与重发是唯一的生命周期操作,均经作者(管理面)
|
||||||
|
- **与 `sessions` 表零关系**:sessions 服务于管理面(作者)的登录体系(密码验证 → 短时会话);本表令牌是成员链路唯一的凭证,成员请求直接携带、服务端直查本表(`token_hash` + `status`)——两条链路各自独立、物理隔离
|
||||||
|
- 成员低频只读,过期只会制造无意义的重发摩擦
|
||||||
|
|
||||||
|
### 3.3 `name` UNIQUE
|
||||||
|
|
||||||
|
账目署名不可歧义。
|
||||||
|
|
||||||
|
### 3.4 软删除(`status`)而非物理删除
|
||||||
|
|
||||||
|
成员名下的 key 与历史用量是账本的一部分——「退拼」是停用(保留行、停访问),不是抹掉事实。
|
||||||
|
|
||||||
|
## 4. 写入时机
|
||||||
|
|
||||||
|
| 时机 | 动作 |
|
||||||
|
|------|------|
|
||||||
|
| 开户(管理面) | INSERT(`name` / `remark`;生成令牌 → `token_hash` + `token_issued_at`) |
|
||||||
|
| 停用 / 启用 | UPDATE `status` |
|
||||||
|
| 重发令牌 | UPDATE `token_hash` + `token_issued_at`(旧令牌即失效) |
|
||||||
|
|
||||||
|
以上全部为管理面写操作,均落**审计日志**(审计为独立需求,表结构归技术稿)。
|
||||||
|
|
||||||
|
## 5. 待定事项
|
||||||
|
|
||||||
|
| 事项 | 归属 |
|
||||||
|
|------|------|
|
||||||
|
| 令牌生成的随机源与长度(256bit 起步) | 技术稿 |
|
||||||
|
| 审计日志表结构 | 技术稿 |
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# payments 表字段设计
|
||||||
|
|
||||||
|
> 数据模型第三张表:支付记录——每笔支付事件(订阅购买 / 充值)一行。平台只存**事实**(支付金额、换得额度、来源参数),「支付金额与额度之间的比率」是查询时的派生视图,不存储、不做任何分摊计算。本文记录字段清单、语义与决策理由。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 状态 | 定稿(2026-10-01) |
|
||||||
|
| 关联 | `docs/design/usage_logs表字段设计.md` 3.6(`subscription_id` 直连与本表)· `docs/design/qpu_history表字段设计.md`(quota → 额度美元的换算时间线)· `docs/references/NewAPI-接口范围.md` §2.⑨(`other.subscription_*` 字段依据) |
|
||||||
|
| 范围 | 列结构 + 语义 + 写入时机;方言映射归数据访问层设计 |
|
||||||
|
| 字段数 | 9 = 1 主键 + 8 人工录入(支付 4 · 期间 2 · 订阅 2) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 表定位
|
||||||
|
|
||||||
|
- **一行 = 一笔支付事件**(订阅购买 / 充值)——同一模型兼容两种形态(`kind` 列区分)
|
||||||
|
- **本表是「只有作者知道」的信息的唯一落点**:支付金额、额度、订阅 ID——上游没有免 auth 接口可查,全靠作者手工抄录
|
||||||
|
- **币种无关**:`paid_amount` 是支付金额数值,**系统不预设币种**——币种是使用者之间的约定;「金额 ↔ 额度」的比率同样是派生视图(查询时相除即得)
|
||||||
|
- **不做分摊计算**:平台不把支付金额按用量比例分摊到日志或成员——分账是社交层问题;本表与日志的交汇只发生在**事实关联**(`subscription_id` 直连)与**参数展示**(订阅信息对使用者完全公开透明)
|
||||||
|
- 数据规模:年个位数到十位数行——无需索引设计
|
||||||
|
|
||||||
|
## 2. 字段清单
|
||||||
|
|
||||||
|
### 2.1 主键与支付字段(5 个)
|
||||||
|
|
||||||
|
| 列 | 类型 | 值来源 | 说明 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| `id` | bigserial PK | 自增 | |
|
||||||
|
| `kind` | text | 人工选择 | `subscription`(订阅)/ `topup`(充值) |
|
||||||
|
| `paid_at` | timestamptz | 人工填写 | 支付时间 |
|
||||||
|
| `paid_amount` | numeric | **人工必填** | **支付金额**(币种无关——数值本身,币种由使用者约定) |
|
||||||
|
| `remark` | text | 人工填写 | 备注 |
|
||||||
|
|
||||||
|
### 2.2 期间与订阅字段(4 个)
|
||||||
|
|
||||||
|
| 列 | 类型 | 值来源 | 说明 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| `period_start` | timestamptz? | **人工录入**(钱包页面) | 仅订阅:该笔覆盖的用量期间起点;左闭 |
|
||||||
|
| `period_end` | timestamptz? | **人工录入**(钱包页面) | 期间终点;左开;未结束可空 |
|
||||||
|
| `subscription_id` | bigint? | **人工录入**(钱包页面「订阅 #N」) | 上游订阅实例 ID;与日志 `subscription_id` 直连(事实关联,不推算) |
|
||||||
|
| `quota_granted` | bigint? | **人工录入**(钱包页面「总额度」/ 充值记录) | 本笔换得的额度——**单位钉死为原始 quota 整数**(如 $25 额度 = 12,500,000;见 3.6) |
|
||||||
|
|
||||||
|
## 3. 决策记录(含理由)
|
||||||
|
|
||||||
|
### 3.1 登记 = 手工抄录(来源只有两处)
|
||||||
|
|
||||||
|
支付记录全部由作者手工录入,来源仅两处:**支付凭证**(`kind` / `paid_at` / `paid_amount`)与**钱包页面**(订阅 ID / 期间 / 总额度)。订阅关键信息只有作者可见(没有免 auth 接口可查)——系统不做回填与推断,日志观测仅用于核对提示(见 §4);必填 / 可空的校验细节归技术稿。
|
||||||
|
|
||||||
|
### 3.2 只存事实,不存派生数
|
||||||
|
|
||||||
|
- `paid_amount` 与 `quota_granted` 是不可替代的事实;「金额 ↔ 额度」的比率是派生数——查询时相除即得,事后核对永远可用;只存比率会丢失核对依据
|
||||||
|
- **日志同样不加任何金额 / 比率列**(`usage_logs` 零分摊字段):把支付金额按用量分摊到日志行的任何计算都不做——分摊口径是社交层问题,账本上只有客观事实
|
||||||
|
- 关联维度(`billing_source` / `subscription_id`)已在日志侧提为独立列(见 `usage_logs表字段设计.md` 3.6),统计按事实关联
|
||||||
|
|
||||||
|
### 3.3 为什么额度也手工录入(不采用日志感知)
|
||||||
|
|
||||||
|
日志逐条自带 `subscription_total`(实测),但回填它需要「订阅 ID → 该 ID 日志 total」的关联逻辑;钱包页面已直接给出总额度——手工抄录一步到位。**系统对支付记录零回填、零推断**,观测仅用于核对(见 §4)。
|
||||||
|
|
||||||
|
### 3.4 币种无关(不预设人民币)
|
||||||
|
|
||||||
|
`paid_amount` 是纯数值。作者记账用什么货币是他的事,展示层的货币符号是界面约定——数据层与币种彻底解耦,更换支付渠道 / 币种时零 schema 变化。
|
||||||
|
|
||||||
|
### 3.5 命名与类型
|
||||||
|
|
||||||
|
- 表名 `payments`(支付记录):订阅与充值同构,`purchases`(购买)只覆盖前者,`payments` 是两者的上位词
|
||||||
|
- 金额列 `numeric`(金额不用浮点);时间列统一 `timestamptz`
|
||||||
|
- 期间左闭右开(`[period_start, period_end)`)——与全项目期间惯例一致
|
||||||
|
- 订阅实例 ID 与日志同名 `subscription_id`——同一体系、同一含义(直连即用此列)
|
||||||
|
|
||||||
|
### 3.6 `quota_granted` 的单位:原始 quota,不是美元
|
||||||
|
|
||||||
|
- **存真值,不存换算快照**:上游订阅套餐的额度本体是 quota 整数,「$25」只是界面显示形态(quota ÷ `quota_per_unit`,随 `quota_display_type` 而变)——本列存本体
|
||||||
|
- **域一致性**:日志的 `quota` 同为原始整数——`SUM(usage_logs.quota)` 与 `quota_granted` 同域直接可比,零换算;若存美元则每次比较都要过 ÷QPU,而 QPU 是时间线变量(`qpu_history`),凭空引入取值时刻问题
|
||||||
|
- **类型精确**:quota 恒为整数;美元额度可能有小数,`bigint` 装不下
|
||||||
|
- **录入体验归交互层**:钱包页面显示美元形态——管理面表单支持输入美元、按当时 QPU 换算落库(或展示两态),换算逻辑不进数据模型
|
||||||
|
- 美元形态随时可派生:`quota_granted ÷ QPU`(展示层)
|
||||||
|
- **与 qpu_history 无概念重复**:quota 是**量**,QPU 是**换算率**——记录量不等于记录单位定义(记「¥170」不涵盖「1 美元 = 6.8 人民币」)。真值域(quota)不携带任何换算,换算率只在一处(`qpu_history` 时间线),美元永远现算现用;反过来若直接存美元,每个值都是某次换算的快照、各自隐含一个换算时刻——那才是真正的冗余,且上游改 QPU 后与日志统计失去同域可比性
|
||||||
|
|
||||||
|
## 4. 写入时机与维护
|
||||||
|
|
||||||
|
| 时机 | 动作 |
|
||||||
|
|------|------|
|
||||||
|
| 人工登记(管理通道) | INSERT:录入支付信息(时间 / 金额)与订阅信息(ID / 期间 / 总额度,抄自钱包页面)→ 确认 |
|
||||||
|
| 系统观测(仅核对) | 日志观测到的订阅 ID / `subscription_total` 仅作核对提示(如「观测到 #N 未对应任何支付记录」——即「未关联」桶的健康信号),不参与回填 |
|
||||||
|
| 编辑 / 删除 | 数据为人工登记,改错即修 |
|
||||||
|
|
||||||
|
## 5. 展示层边界(界面设计的输入)
|
||||||
|
|
||||||
|
- **订阅与支付参数对使用者完全公开透明**(访问主体模型裁决):期间、额度、支付金额、订阅 ID——成员视图与管理面同源可见;不存在「管理面独占的金额信息」
|
||||||
|
- 「按订阅维度统计」(某笔订阅的消耗占比等)数据天然支持——按 `subscription_id` 直连聚合即可
|
||||||
|
- 成员视角的日志行携带所属订阅/充值信息的具体形态,交互设计期定(见 `docs/design/需求清单.md` R12)
|
||||||
|
|
||||||
|
## 6. 待定事项(关联后续环节)
|
||||||
|
|
||||||
|
| 事项 | 归属 |
|
||||||
|
|------|------|
|
||||||
|
| 核对提示(观测到的订阅 ID ↔ 已录入记录比对)的交互 | 交互设计(待立) |
|
||||||
|
| 充值(`topup`)的 `quota_granted` 抄录来源(充值记录页)核实 | 实现期真实数据核查 |
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# qpu_history 表字段设计
|
||||||
|
|
||||||
|
> 数据模型第四张表:换算参数时间线(表名 `qpu_history`,QPU = QuotaPerUnit 缩写)——`quota_per_unit`(quota → 额度美元)的历史取值。历史值在 sk-key 视角无从观测,本表由同步引擎自动采样、只记变化点;**纯只读**(唯一写入者是同步引擎)。本文记录字段清单、语义与决策理由。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 状态 | 定稿(2026-10-01) |
|
||||||
|
| 关联 | `docs/design/usage_logs表字段设计.md`(消费方:quota → 美元的展示换算)· `docs/references/NewAPI-接口范围.md`(采集来源 `/api/status`) |
|
||||||
|
| 范围 | 列结构 + 语义 + 写入规则;采样调度(同步引擎)不在此文;方言映射归数据访问层设计 |
|
||||||
|
| 字段数 | 3 = 1 主键 + 2 业务字段 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 表定位
|
||||||
|
|
||||||
|
- **一行 = 一次「换算单位取值变化」事件**(值 + 生效时间)——正常情况全表仅 1 行(当前站点为出厂默认值,从未变过)
|
||||||
|
- 它是 **quota → 额度美元**这一单位换算的**时间线**:任何时刻的换算取值 = 「该时刻之前最近一条」
|
||||||
|
- **为什么要自己攒**:`/api/status` 只能看到当前值,历史值无从观测(上游不留历史、管理审计不记值)——没有本表,历史日志只能用今天的单位换算,**账会串**
|
||||||
|
- 与日志零耦合:换算全部发生在展示 / 聚合层——本表任何变动都不触发 `usage_logs` 写入
|
||||||
|
|
||||||
|
## 2. 字段清单
|
||||||
|
|
||||||
|
| 列 | 类型 | 值来源 | 说明 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| `id` | bigserial PK | 自增 | |
|
||||||
|
| `qpu` | numeric | `/api/status.quota_per_unit` | 当时的 `quota_per_unit` 值 |
|
||||||
|
| `effective_from` | timestamptz | 首次采样或检测到变化的时间 | 生效时间;采样误差 ≤ 同步周期 |
|
||||||
|
|
||||||
|
## 3. 决策记录(含理由)
|
||||||
|
|
||||||
|
### 3.1 为什么自动采样(而非人工登记)
|
||||||
|
|
||||||
|
它是「可登记的事实」的**反面**——历史值无从观测(只能看到当前值);自动采样(每轮同步)是唯一能沿时间轴捕获它的方式。采样误差 ≤ 同步周期(边界影响可忽略)。
|
||||||
|
|
||||||
|
### 3.2 只记变化点,不记每次采样
|
||||||
|
|
||||||
|
部署时插基线;此后每轮同步顺带读 `/api/status`,**值变化才追加一行**——行数 = 调价次数 + 1(正常为 1)。
|
||||||
|
|
||||||
|
### 3.3 取值语义:按日志自身时刻取值
|
||||||
|
|
||||||
|
- **一条日志的美元换算 = 该日志 `created_at` 时刻的当值 QPU**(「该时刻之前最近一条」)——这是纯粹的历史单位换算:这条日志发生时一单位 quota 值多少美元额度,就用当时的换算率
|
||||||
|
- **为什么按日志时刻**:换算的唯一目的是「这条日志当时值多少美元额度」——历史正确性要求用历史当值;不存在「一笔订阅一把尺」的分摊自洽诉求(分摊计算已整体裁撤)
|
||||||
|
- **边界**:日志时刻早于本表最早记录时(如补录历史数据),回退用「最早已知值」(近似规则,正常部署起点的日志不会触发)
|
||||||
|
|
||||||
|
### 3.4 `quota_per_unit` 的可变性说明(本表存在的理由)
|
||||||
|
|
||||||
|
- 它是「全站计价单位的定义」——改它 ≈ 重定义货币单位(全站一致重定标);管理员极少动(当前站点为出厂默认值)。本设计以「按时间线换算」从根上免疫它的任何变更
|
||||||
|
- **存在必要性审视(2026-09-29,作者裁决保留)**:改动概率极低(历史上零次)× 权限极高(Root,深思熟虑的定价动作而非误操作)× **不可逆**(一旦发生且未被捕捉,「当时的率」永久不存在)——风险结构与日志 1000 条窗口同构;备选方案「砍表 + 保留观测告警、事后人工重建」被否决——不可逆的历史由自动化保障,不依赖人工响应;全部代价 = 每轮一次 `/api/status` 采样 + 十行时点查询(`qpuAt(t)`,3 行内存缓存 + 区间判断)
|
||||||
|
|
||||||
|
### 3.5 为什么不做人工修正(只读设计)
|
||||||
|
|
||||||
|
- 人工介入的场景不成立——`quota_per_unit` 在上游**无编辑入口(前台默认隐藏且只读)、管理审计不记值、历史上从未变化**(见 `docs/references/NewAPI-接口范围.md`)
|
||||||
|
- 本表由此定型为**纯只读时间线**:唯一写入者是同步引擎(值变化才追加);管理通道仅提供只读查看
|
||||||
|
- 未来若真出现需要人工介入的场景(如运营方主动告知一次精确切换时刻),再评估启用
|
||||||
|
|
||||||
|
### 3.6 命名:表名 `qpu_history`、值列 `qpu`
|
||||||
|
|
||||||
|
- **表名**:`quota_per_unit_history` 过长、逐处引用啰嗦 → `qpu_history`——QPU 是 QuotaPerUnit 的通用缩写,短且醒目
|
||||||
|
- **值列**:`value` 过泛("什么的值"不自明)→ 改为 `qpu`,与表名一体、一眼即知
|
||||||
|
- 通用约定:**表名 / 列名用 QPU 缩写,引用上游字段 `quota_per_unit` 时保持全称**
|
||||||
|
|
||||||
|
## 4. 写入时机(唯一写入者:同步引擎)
|
||||||
|
|
||||||
|
| 时机 | 动作 |
|
||||||
|
|------|------|
|
||||||
|
| 部署(基线) | INSERT 基线一行(当时 `/api/status` 取值) |
|
||||||
|
| 每轮同步 | 读 `/api/status`:**值变化才追加一行** |
|
||||||
|
|
||||||
|
- 除追加外无 UPDATE / DELETE——数据不可变
|
||||||
|
|
||||||
|
## 5. 待定事项(关联后续环节)
|
||||||
|
|
||||||
|
| 事项 | 归属 |
|
||||||
|
|------|------|
|
||||||
|
| 采样调度(读取频率、失败重试) | 同步引擎设计(待立) |
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# sessions 表字段设计
|
||||||
|
|
||||||
|
> 数据模型新增表之三:管理面会话——HttpOnly cookie 里的令牌与服务端状态的对应物。裁决背景见 `docs/design/需求清单.md` §9「访问主体模型」。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 状态 | 定稿(2026-10-01) |
|
||||||
|
| 关联 | `docs/design/users表字段设计.md`(`user_id` 指向)· `docs/design/需求清单.md` §9 |
|
||||||
|
| 范围 | 列结构 + 语义 + 写入时机;cookie 参数(SameSite 等)归技术稿;方言映射归数据访问层设计 |
|
||||||
|
| 字段数 | 5 = 1 主键 + 4 业务字段 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 表定位
|
||||||
|
|
||||||
|
- **一行 = 一次活跃登录**——登录成功 INSERT,登出 / 过期 DELETE
|
||||||
|
- 浏览器侧只持有令牌明文(HttpOnly cookie),服务端只存哈希
|
||||||
|
- **唯一服务对象是管理面(作者)登录**——`user_id` 指向单行的 `users` 表;成员链路**不经此表**(成员凭证是 `members.token_hash` 长期令牌,无会话语义,辨析见 `members表字段设计.md` 3.2)
|
||||||
|
|
||||||
|
## 2. 字段清单
|
||||||
|
|
||||||
|
| 列 | 类型 | 值来源 | 说明 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| `id` | bigserial PK | 自增 | |
|
||||||
|
| `user_id` | bigint FK | 登录成功时 | → `users.id` |
|
||||||
|
| `token_hash` | text UNIQUE | 登录成功时 | 会话令牌(随机 256bit)的哈希——cookie 存明文、库存哈希 |
|
||||||
|
| `expires_at` | timestamptz | 登录成功时 | 过期时间(如 7 天);过期即拒绝 |
|
||||||
|
| `created_at` | timestamptz | 登录成功时 | |
|
||||||
|
|
||||||
|
## 3. 决策记录(含理由)
|
||||||
|
|
||||||
|
### 3.1 服务端会话(查库校验),明确否决 JWT
|
||||||
|
|
||||||
|
- **JWT 的问题域我们没有**:JWT(无状态令牌)的收益在多服务共享认证、水平扩展免共享存储、海量会话——本项目是单后端、单实例、单管理员,收益一项不占,负担全额缴纳
|
||||||
|
- **不可吊销是致命伤**:JWT 签发后、过期前无法作废——登出仅是客户端删 cookie(令牌仍有效)、踢下线做不到、改密后旧会话无法失效;业界绕法「短过期 + refresh token」需要服务端存储 refresh token,绕回有状态,JWT 的唯一优势随之蒸发
|
||||||
|
- **多一个要管理的秘密**:签名密钥的保管、轮换、泄露处置,凭空多一项运维(CFW 形态下多一处 secret 配置)
|
||||||
|
- **本设计即业界最佳实践的翻译**:随机 256bit 令牌、哈希存储、`HttpOnly`/`Secure`/`SameSite`、服务端校验、过期拒绝、登出即吊销——对应 OWASP Session Management Cheat Sheet 逐条要求;「成熟」体现在模式本身。现成认证库的评估归实现期技术稿,模式不变
|
||||||
|
|
||||||
|
### 3.2 令牌哈希存储
|
||||||
|
|
||||||
|
拖库不泄会话令牌。
|
||||||
|
|
||||||
|
### 3.3 过期清理为惰性删除
|
||||||
|
|
||||||
|
校验命中过期行即删 + 拒绝;不建定时清理任务——过期行占用量级:个位数/月,无清理压力。
|
||||||
|
|
||||||
|
### 3.4 单设备多会话不互斥
|
||||||
|
|
||||||
|
同一账号可多处登录(各自行)——单人场景无需「顶号」语义。
|
||||||
|
|
||||||
|
## 4. 写入时机
|
||||||
|
|
||||||
|
| 时机 | 动作 |
|
||||||
|
|------|------|
|
||||||
|
| 登录成功 | INSERT(用户名 + PBKDF2 验证密码通过) |
|
||||||
|
| 登出 | DELETE 该行 |
|
||||||
|
| 校验命中过期行 | DELETE + 拒绝(惰性清理) |
|
||||||
|
|
||||||
|
## 5. 待定事项
|
||||||
|
|
||||||
|
| 事项 | 归属 |
|
||||||
|
|------|------|
|
||||||
|
| cookie 参数(`HttpOnly` / `Secure` / `SameSite=Lax` / Path 限管理面)、会话时长取值 | 技术稿 |
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# usage_logs 表字段设计
|
||||||
|
|
||||||
|
> 数据模型第二张表:调用日志明细——增量同步的落点,本项目「任意时间范围用量」的唯一数据来源。字段清单依据 `/api/log/token` 实测响应(1000 条样本)与上游源码(`model/log.go`)双重校对;上游只有 1000 条滚动窗口,**错过即永久丢失**。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 状态 | 定稿(2026-10-01) |
|
||||||
|
| 关联 | `docs/references/NewAPI-接口范围.md` §2.⑨(`/api/log/token` 字段与限制依据)· `docs/design/api_keys表字段设计.md`(`api_key_id` 外键指向) |
|
||||||
|
| 定稿范围 | 列结构 + 去重与写入策略;同步引擎细节(调度、401 处理)不在此文;方言映射归数据访问层设计 |
|
||||||
|
| 字段数 | 25 = 3 系统字段 + 2 来源提列 + 20 上游字段(上游响应共 21 个字段,其中展示序号 `id` 不落表,见 3.2) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 表定位
|
||||||
|
|
||||||
|
- **一行 = 一次上游调用记录**(消费 / 错误等类型全覆盖)
|
||||||
|
- 它是本项目最核心的数据资产:上游 `/api/log/token` 只保留最近 1000 条(实测当前速率下 ≈ 3 天),**错过即永久丢失**——本地全量累积是"任意时间范围用量"的唯一实现路径
|
||||||
|
- 与 `api_keys` 多对一:每条日志经 `api_key_id` 指向其所属 key
|
||||||
|
- 计费来源(`billing_source` / `subscription_id`)是**一等维度**:按来源统计、日志与支付记录的 `subscription_id` 直连(见 3.6)
|
||||||
|
- 数据规模:实测约 300+ 条/天,量级为数十万行/年——单表 + 时间索引完全够用,无需分区或预聚合
|
||||||
|
|
||||||
|
## 2. 字段清单
|
||||||
|
|
||||||
|
### 2.1 系统字段(3 个,本系统产生)
|
||||||
|
|
||||||
|
| 列 | 类型 | 值来源 | 说明 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| `id` | bigserial PK | 自增 | 本地主键(与上游的展示序号 `id` 无关) |
|
||||||
|
| `api_key_id` | bigint FK | 同步引擎 | → `api_keys.id`;日志归属与索引锚点 |
|
||||||
|
| `dedupe_key` | text UNIQUE | 写入前生成 | **去重唯一键**,规则见 3.1 |
|
||||||
|
|
||||||
|
### 2.2 来源提列字段(2 个,解析自 `other`、提升为独立列)
|
||||||
|
|
||||||
|
| 列 | 类型 | 上游来源 | 说明 |
|
||||||
|
|----|------|---------|------|
|
||||||
|
| `billing_source` | text | `other.billing_source` | 计费来源:`subscription`(订阅)/ `wallet`(充值)——按来源统计的维度 |
|
||||||
|
| `subscription_id` | bigint NULL | `other.subscription_id` | 该次消耗所属的上游订阅实例 ID——与 `payments.subscription_id` 直连;缺失时 NULL(归「未关联」桶,见 3.6) |
|
||||||
|
|
||||||
|
### 2.3 上游字段(20 个,`/api/log/token` 响应照收)
|
||||||
|
|
||||||
|
| 列 | 类型 | 上游字段 | 说明 |
|
||||||
|
|----|------|---------|------|
|
||||||
|
| `user_id` | bigint | `user_id` | 账号 ID(同一实例内恒定) |
|
||||||
|
| `created_at` | timestamptz | `created_at` | 上游 UTC Unix 秒,入库转换;范围查询主维度 |
|
||||||
|
| `type` | smallint | `type` | 日志类型 0-7 枚举(完整表见接口文档);照存不过滤 |
|
||||||
|
| `content` | text | `content` | 消费记录为空串;错误记录为错误详情 |
|
||||||
|
| `username` | text | `username` | 账号用户名 |
|
||||||
|
| `token_name` | text | `token_name` | 快照存储(保真;展示取当前名口径由 API 契约(待立)另定) |
|
||||||
|
| `model_name` | text | `model_name` | |
|
||||||
|
| `quota` | bigint | `quota` | 原始 quota |
|
||||||
|
| `prompt_tokens` | int | `prompt_tokens` | |
|
||||||
|
| `completion_tokens` | int | `completion_tokens` | |
|
||||||
|
| `use_time` | int | `use_time` | 秒 |
|
||||||
|
| `is_stream` | bool | `is_stream` | |
|
||||||
|
| `channel` | int | `channel` | 渠道 ID |
|
||||||
|
| `channel_name` | text | `channel_name` | 普通用户视角为空串 |
|
||||||
|
| `token_id` | bigint | `token_id` | 上游 token ID(对账用) |
|
||||||
|
| `group_name` | text | `group` | PG 保留字改名 |
|
||||||
|
| `ip` | text | `ip` | 普通用户视角为空串 |
|
||||||
|
| `request_id` | text | `request_id` | 原值保留;JSON tag 为 `omitempty`,源码写入时有兜底 |
|
||||||
|
| `upstream_request_id` | text NULL | `upstream_request_id` | **可选字段**:`omitempty`——空值时不出现、有值才出现(实测样本中缺席) |
|
||||||
|
| `other` | jsonb | `other` | JSON 字符串解析入库(可查询;含缓存/倍率/延迟等扩展信息) |
|
||||||
|
|
||||||
|
> 上游响应中的展示序号 `id`(每次从 1 重编)不落表——见 3.2。
|
||||||
|
|
||||||
|
### 2.3 索引
|
||||||
|
|
||||||
|
| 索引 | 列 | 用途 |
|
||||||
|
|------|-----|------|
|
||||||
|
| PRIMARY KEY | `id` | |
|
||||||
|
| UNIQUE | `dedupe_key` | 去重(`ON CONFLICT` 的落点) |
|
||||||
|
| 普通 | `(api_key_id, created_at DESC)` | 明细页 + 按 key 聚合 |
|
||||||
|
| 普通 | `(created_at DESC)` | 跨 key 合计与统计 |
|
||||||
|
|
||||||
|
## 3. 决策记录(含理由)
|
||||||
|
|
||||||
|
### 3.1 去重:单列 `dedupe_key`,按类型分规则
|
||||||
|
|
||||||
|
- `request_id` 是去重依据(上游为每次请求生成的编号);但**错误行上不唯一**——实测样本中同一 `request_id` 可对应多条 `type=5` 错误记录(同一请求的多次失败 / 重试,`content` 的 `status_code` 可不同)。按 `request_id` 一律去重会把它们合并——**失败次数被少算,事实被丢**。规则按类型分:
|
||||||
|
|
||||||
|
- **消费行(`type = 2`)**:`dedupe_key = ${api_key_id}:${request_id}`(实测未观察到消费行 `request_id` 重复;重复拉取幂等合并)
|
||||||
|
- **非消费行(`type ≠ 2`,如实测的错误行)**:`dedupe_key = ${api_key_id}:${request_id}:${content指纹}`,指纹 = `sha256(api_key_id, created_at, type, model_name, quota, prompt_tokens, completion_tokens, use_time, content)`——把 `content` 纳入:同一请求多次失败、错误详情不同即指纹不同,**各自保留**;全部字段完全一致的两行在「无法区分」的意义上视作重复拉取,合并无害
|
||||||
|
- `request_id` 为空时:退化为纯内容指纹(同上字段集,消费行不含 `content`)
|
||||||
|
|
||||||
|
- **为什么不把 UNIQUE 直接挂在 `request_id` 列上**:空值语义两难——`NULL` 在唯一索引中互不冲突(空值行会重复入库);转空串则过度冲突(第二条空值记录被静默丢弃)。单列 `dedupe_key` + 指纹兜底从根上规避
|
||||||
|
- `api_key_id:` 前缀:跨 key 隔离的零成本保险
|
||||||
|
- `request_id` 原值仍单独成列——两者并行,互不替代
|
||||||
|
- **为什么它不做主键**:主键是「行身份」(短、稳定、永不变化),`dedupe_key` 是「防重签名」(长、由业务规则拼接、可能随上游演化)——让可变规则决定行身份是耦合隐患;代理键(自增 `id`)+ 独立唯一约束是标准做法
|
||||||
|
|
||||||
|
### 3.2 上游展示序号 `id` 不落表
|
||||||
|
|
||||||
|
它是每次响应从 1 重编的展示序号(源码 `assignDisplayLogIds`),跨请求无意义——绝不能用于去重,也没有存储价值。
|
||||||
|
|
||||||
|
### 3.3 `other` 用 jsonb
|
||||||
|
|
||||||
|
- 原样保真(解析后完整存储)+ 查询能力(`other->>'cache_tokens'`);jsonb 是 PostgreSQL 内置类型
|
||||||
|
- 普通用户视角的 `other` 已由服务端做过一层「可见性过滤」——我们存的就是用户实际可见的部分
|
||||||
|
|
||||||
|
### 3.4 `raw` 兜底列:不加
|
||||||
|
|
||||||
|
21 个候选字段(响应理论全集)已由实测 + 源码双重校对齐全,且 `other` 已单独 jsonb 保真(信息富集区);字段集变化的风险由「同步持续运行 + 代码随上游更新」覆盖,接受历史缺值的可能性。
|
||||||
|
|
||||||
|
### 3.5 命名与类型
|
||||||
|
|
||||||
|
- 上游字段采用原名;例外:`group` → `group_name`(PG 保留字)
|
||||||
|
- 时间列统一 `timestamptz`(应用层做 UTC ↔ 北京时间转换);`quota` 用 `bigint`
|
||||||
|
- `type` 用 `smallint` 存原值(0-7 为稳定枚举),展示层翻译(未知值原样显示数字)
|
||||||
|
|
||||||
|
### 3.6 来源提列:`billing_source` 与 `subscription_id` 升为独立列
|
||||||
|
|
||||||
|
- **为什么提列**:两者是按来源统计与日志-支付记录直连的核心维度——`other` jsonb 的键查询无法走索引(SQLite/D1 的 `json_extract` 同理),高频过滤维度应独立成列
|
||||||
|
- **`subscription_id` 直连语义**(事实关联,非推算):日志按 `subscription_id = payments.subscription_id` 关联支付记录;**不做期间推定**——订阅来源但该列缺失(NULL)的行归「未关联订阅」桶,统计中透明显示(正常应恒为零,非零即「缺登记」或「上游行为变化」的异常信号)
|
||||||
|
- `billing_source = wallet`(充值)的日志不挂订阅(余额扣款与订阅实例无关),`subscription_id` 为 NULL 是**正常状态**而非异常——按来源统计时它们在「充值」桶,不进「未关联订阅」桶
|
||||||
|
- **其他键留在 `other`**:提列只针对这两个一等维度;`cache_tokens`、倍率等仍留在 jsonb,出现真实分析需求再评估(见 §5)
|
||||||
|
|
||||||
|
### 3.7 行级归属
|
||||||
|
|
||||||
|
- key 的成员归属是**管理面可变的**(key 中途转给另一个成员),日志行不复制归属——统计时经 `api_key_id → api_keys.member_id` 联查取**当前归属**
|
||||||
|
- 归属变更不重写历史:变更后的统计自然反映新归属;「变更前谁用了多少」不是平台的账目职责(分账是社交层问题——平台只保证当前事实的正确)
|
||||||
|
|
||||||
|
## 4. 写入时机与策略
|
||||||
|
|
||||||
|
| 时机 | 动作 |
|
||||||
|
|------|------|
|
||||||
|
| 登记首轮同步 | 拉最近 1000 条 → 逐条入库 |
|
||||||
|
| 每轮轮询 | 同上;`INSERT ... ON CONFLICT (dedupe_key) DO NOTHING`——幂等,只落新行 |
|
||||||
|
|
||||||
|
- 写入者唯一:同步引擎(不存在其他写入路径)
|
||||||
|
- 日志不可变:已入库的行不做 UPDATE
|
||||||
|
- 与本表相关的读取链:从 `api_keys` 取密文 → 解密 → 调上游 → 新日志写本表
|
||||||
|
|
||||||
|
## 5. 待定事项(关联后续环节)
|
||||||
|
|
||||||
|
| 事项 | 归属 |
|
||||||
|
|------|------|
|
||||||
|
| 明细/统计的查询口径(`type` 过滤策略) | API 契约(待立) |
|
||||||
|
| `other` 内其余高频键是否提为独立列(如 `cache_tokens`) | 出现真实分析需求时再评估(`billing_source` / `subscription_id` 已提列,见 3.6) |
|
||||||
|
| 数据保留策略(是否清理旧数据) | 后议 |
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# users 表字段设计
|
||||||
|
|
||||||
|
> 数据模型新增表之二:管理员——登录体系的身份落点,**实际只有一行**(作者,唯一管理员)。裁决背景见 `docs/design/需求清单.md` §9「访问主体模型」。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 状态 | 定稿(2026-10-01) |
|
||||||
|
| 关联 | `docs/design/sessions表字段设计.md`(登录会话指向本表)· `docs/design/需求清单.md` §9 |
|
||||||
|
| 范围 | 列结构 + 语义 + 写入时机;登录流程细节(PBKDF2 参数、节流)归技术稿;方言映射归数据访问层设计 |
|
||||||
|
| 字段数 | 4 = 1 主键 + 3 业务字段 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 表定位
|
||||||
|
|
||||||
|
- **一行 = 一个管理员账号**——本表设计为单行(作者),不存在注册路径
|
||||||
|
- 凭证经**部署期初始化**创建(env / wrangler secret 的引导凭证,空库幂等落库)——部署完成即就绪;空库且 env 无凭证时由**引导页**完成初始化(重置兜底通道,见 3.1)
|
||||||
|
|
||||||
|
## 2. 字段清单
|
||||||
|
|
||||||
|
| 列 | 类型 | 值来源 | 说明 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| `id` | bigserial PK | 自增 | |
|
||||||
|
| `username` | text UNIQUE | 部署期初始化 | 登录名 |
|
||||||
|
| `password_hash` | text | 部署期初始化 / 改密 | PBKDF2-SHA256 哈希(WebCrypto 原生,两形态统一——Workers 无 argon2/bcrypt) |
|
||||||
|
| `created_at` | timestamptz | 初始化时 | |
|
||||||
|
|
||||||
|
## 3. 决策记录(含理由)
|
||||||
|
|
||||||
|
### 3.1 单行表,不做注册;初始化与重置 = 空库双通道
|
||||||
|
|
||||||
|
管理员账号由空库初始化创建;改密走登录后的表单(旧密码验证 → 落库)。「单行」是**部署行为约定,不是 schema 约束**——表结构天然支持多行,未来若需要多管理员 + 角色,扩展为增量动作(加 `role` 列 + 中间件分支 + 管理页),无需推倒任何东西。
|
||||||
|
|
||||||
|
**管理面入口的幂等三态判断**:
|
||||||
|
|
||||||
|
- `users` 非空 → 登录页(如常)
|
||||||
|
- 空库 + env 凭证在 → **静默重建**(部署与重置共用此通道;密码即 env 凭证)
|
||||||
|
- 空库 + env 凭证无 → **引导页**(设置新用户名密码,直接进入登录态;`users` 非空后永不复现)
|
||||||
|
|
||||||
|
**忘记密码的重置**:删除 `users` 行(母体:请运营方执行一条 SQL;CFW:自助 `wrangler d1 execute`)→ 下一次请求触发三态判断 → 有 env 静默重建后改密,无 env 引导页当场设新密码。**env 凭证可留可删、无依赖**——留着享受静默重建,删了有页面兜底;抢注窗口仅存在于「删 env + 删行」的重置瞬间(作者主导、分钟级),部署路径(env 在)永远无窗口。
|
||||||
|
|
||||||
|
### 3.2 密码哈希选 PBKDF2-SHA256
|
||||||
|
|
||||||
|
WebCrypto 原生支持——母体与 CFW 抽取版同一实现(Workers runtime 无 argon2/bcrypt,选它们会造成两形态分叉);迭代参数归技术稿。
|
||||||
|
|
||||||
|
### 3.3 不建多用户 / 角色引擎
|
||||||
|
|
||||||
|
管理操作只有作者一人——RBAC 无用武之地;权限边界由访问面物理隔离(管理面 API 与成员 API 两套路由)。
|
||||||
|
|
||||||
|
## 4. 写入时机
|
||||||
|
|
||||||
|
| 时机 | 动作 |
|
||||||
|
|------|------|
|
||||||
|
| 部署期初始化(双通道) | 空库 + env 凭证在 → 静默 INSERT(幂等,已初始化则永远跳过);空库 + env 凭证无 → 引导页设新密码后 INSERT(重置兜底) |
|
||||||
|
| 改密(管理面表单) | UPDATE `password_hash` |
|
||||||
|
|
||||||
|
改密落**审计日志**(审计为独立需求,表结构归技术稿)。
|
||||||
|
|
||||||
|
## 5. 待定事项
|
||||||
|
|
||||||
|
| 事项 | 归属 |
|
||||||
|
|------|------|
|
||||||
|
| PBKDF2 迭代参数、登录失败节流策略 | 技术稿 |
|
||||||
|
| 审计日志表结构 | 技术稿 |
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# 需求清单
|
||||||
|
|
||||||
|
> 平台的需求权威索引与裁决档案:以「**不依赖 NewAPI 平台管理员与用户体系的独立计费平台**」为定位,按功能域记录平台做什么、不做什么、为什么(裁决脉络见 §9)。后续专门设计文档(API 契约 / 交互 / 引擎)回答「怎么做」;R 编号体系是被引用的锚点。R3 / R4 延后项存档于 §9,核心功能实现后重启;新需求按 R 体系增补。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 状态 | 定稿(2026-10-01) |
|
||||||
|
| 关联 | `README.md`(项目沿革与动因)· 七篇表字段设计(定稿)· `docs/references/NewAPI-接口范围.md`(可用接口参考) |
|
||||||
|
| 取代 | 《设计议题》(2026-09-29 由本清单取代;其中双形态裁决与 CFW 调研结论已并入本文 §5/§7) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 平台定位与背景
|
||||||
|
|
||||||
|
**定位**:不依赖 NewAPI 平台管理员与用户体系的**独立计费平台**。上游只提供 sk-key 级接口——没有账单、没有分摊、没有历史回填,一切计费性质的能力(采集、归集、折算、分摊、账单)都必须由平台自身承载。上游接口的完整实证见 `docs/references/NewAPI-接口范围.md`。
|
||||||
|
|
||||||
|
**平台哲学(2026-09-29 裁决)**:**平台不做价值判断,只提供事实服务**——「谁该付谁多少钱」的分摊口径是人和人之间的事;平台提供分账所需的全部原材料(用量事实、来源标注、支付记录、多维统计),数字全部客观中立。
|
||||||
|
|
||||||
|
**背景**(三类问题促成当前设计阶段):
|
||||||
|
|
||||||
|
1. **迭代部署成本**:服务器部署形态下每次迭代都需实例运营方手工升级,测试阶段迭代频繁而运营方无暇跟随 → 双形态交付(R13,已裁决)
|
||||||
|
2. **初版设计刻意收缩的部分**:管理面固定口令不妥(R11)、充值折算(R7)、多订阅处理(R8)
|
||||||
|
3. **已知设计问题**:错误行 `request_id` 去重(R15 必修项)
|
||||||
|
|
||||||
|
## 1. 数据采集域
|
||||||
|
|
||||||
|
| # | 需求 | 说明 | 状态 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| R1 | sk-key 轮询采集与攒库 | 定时轮询 key 级接口、`request_id` 去重、全量日志入库——架构动因(上游无时间范围过滤接口)是物理约束,实现细节开放讨论 | **已裁决**(2026-10-01,随 R15 表设计审阅定稿) |
|
||||||
|
| R2 | key 登记与生命周期 | 管理通道登记、手动启停(`status` 纯手动开关,引擎只读);401 等持续失败经引擎日志与数据截止**显形**,停用由作者手动执行(**不做自动摘除**——引擎记录、不决策) | **已裁决**(2026-09-29,随 R15 审阅) |
|
||||||
|
|
||||||
|
## 2. 数据资产域(新增)
|
||||||
|
|
||||||
|
> 本地库是全量日志的**唯一副本**(上游不提供回填);上游窗口仅最近 1000 条(实测当前速率 ≈ 3 天),同步停摆超过窗口即**永久丢数据**。「不依赖上游」的直接推论:资产保护是平台自己的责任。
|
||||||
|
|
||||||
|
| # | 需求 | 说明 | 状态 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| R3 | 数据资产保护 | 备份 / 导出 / 恢复策略——平台核心资产不能只有单副本 | **延后**(2026-09-29 裁决:核心功能实现后再议;提案存档见裁决记录) |
|
||||||
|
| R4 | 采集健康与告警 | 同步停摆告警、数据连续性校验——3 天窗口的不可逆风险需要可观测兜底 | **延后**(2026-09-29 裁决:核心功能实现后再议;提案存档见裁决记录) |
|
||||||
|
|
||||||
|
## 3. 计费域
|
||||||
|
|
||||||
|
| # | 需求 | 说明 | 状态 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| R5 | 用量查询 | 时间范围 / 明细与汇总;查询入口已归位——成员视图(令牌)与管理面总览/下钻(口令),「凭 sk-key 的开放查询」已裁撤(见裁决记录 2026-09-29);统计维度含按 key / 成员 / 计费来源 / 模型(见 R9) | **已裁决**(2026-10-01,数据层随表设计定稿;查询口径细节归 API 契约) |
|
||||||
|
| R6 | 美元换算与支付记录 | **重定义**(2026-09-29):① quota → 额度美元换算(÷ `quota_per_unit`,`qpu_history` 时间线服务它)——纯客观单位换算,保留;② **裁撤**每行日志的实付分摊金额(`amount_rmb`,「quota 占比 × 实付」)——它是分摊口径的派生值,随购买记录漂移,违反事实服务原则;③ 支付记录登记:支付金额 ↔ 额度的**比率关系**(币种无关——系统不预设币种,见 R9) | **已裁决**(2026-09-29,重定义) |
|
||||||
|
| R7 | 充值登记 | **重定义**(2026-09-29):充值与订阅同构——支付记录(金额 + 额度 + 比率)+ 日志来源标注(`billing_source`)+ 按来源统计;**同样不做**充值用量的金额分摊(原「池子加权平均」方案裁撤) | **已裁决**(2026-09-29,重定义) |
|
||||||
|
| R8 | 多订阅并行处理 | **重定义**(2026-09-29):① 支付记录允许多订阅期间重叠(不设互斥约束)② 日志-支付记录按 `subscription_id` **直连**(上游标注的客观事实,不推算)——「整笔订阅一把尺」原则已随 `amount_rmb` 裁撤消亡,订阅维度无需任何「尺」③ 按订阅分组的统计为查询时聚合输出(**非物理表**——账本只存事实行,统计是随查随算的派生视图);「未关联订阅」为 LEFT JOIN 未匹配集的透明桶(无新字段,正常时恒为零——非零即异常信号:缺登记或上游行为变化),兼作数据健康指示器;扣费顺序(先旧后新等)是上游策略,日志忠实记录,平台不管 | **已裁决**(2026-09-29,重定义) |
|
||||||
|
| R9 | 分摊原材料与多维统计 | **重定义**(2026-09-29):平台**不做**应付款计算、**不做**分摊口径(闲置额度归谁之类是社交层问题)——只提供原材料:计费来源标注(`billing_source`:订阅 / 充值)、支付记录(**币种无关**:支付金额与额度之间的比率关系,不预设币种)、任意时段多维聚合(按 key / 成员 / 来源 / 模型) | **已裁决**(2026-09-29,重定义) |
|
||||||
|
| R10 | ~~计费周期与账单~~ | **已裁撤**(2026-09-29):周期 = 任意查询时段(订阅期间为预置选项);无平台侧账单(应付款不计算,期末快照无对象)。保留行以维持编号连续 | **已裁撤** |
|
||||||
|
|
||||||
|
## 4. 使用者域
|
||||||
|
|
||||||
|
> 平台自身的身份体系——实际使用者 ≈ 作者(管理)+ 若干分摊成员(查询)+ 运营方(信任模型内,本就能看到一切)。这不是多租户产品。
|
||||||
|
|
||||||
|
| # | 需求 | 说明 | 状态 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| R11 | 平台身份体系与鉴权 | 访问主体 = 作者 + 成员(运营方完全信任,不设身份)。**成员身份与管理面鉴权均已裁决**(2026-09-29,见裁决记录) | **已裁决**(2026-09-29) |
|
||||||
|
| R12 | 成员自助视图 | 令牌进成员视图:**名下 key 的模型调用日志**——每条记录额外携带所属订阅/充值的信息(具体携带形态待核查后设计)+ 用量聚合(quota / 美元,按来源分解);**完全公开透明**(订阅与支付参数对成员可见);无「应付」概念(分摊是社交层问题);登录壳交互(输令牌 → sessionStorage,关标签失效) | **已裁决**(2026-09-29) |
|
||||||
|
|
||||||
|
## 5. 交付域(已裁决)
|
||||||
|
|
||||||
|
| # | 需求 | 说明 | 状态 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| R13 | 双形态交付 | **完整前后端(先行,母体)**:服务器部署的前后端分离应用 + PostgreSQL(后端单进程承载 API 路由与同步引擎),功能全集在此形态上设计与实现;**CFW 抽取版(后续,衍生)**:Cloudflare Workers(API + Cron Triggers 轮询)+ D1 攒库 + 前端静态资产托管,从母体中抽取业务核心、替换基础设施层——承载「部署权归作者」的目标形态 | **已裁决**(2026-09-29) |
|
||||||
|
| R14 | 可移植性分层 | R13 的横切约束:业务核心(轮询逻辑 / `request_id` 去重 / 折算链计算 / 加密体系 / API 语义)与形态耦合面(HTTP 框架 / 调度器 / 数据库驱动 / 托管部署)分离;数据访问层隔离 PostgreSQL 与 SQLite/D1 方言(`jsonb` → TEXT + SQLite JSON 函数、类型与索引映射);轮次语义统一(幂等、错过即补——每轮全量拉最近 1000 条天然补偿) | **已裁决**(2026-09-29) |
|
||||||
|
|
||||||
|
## 6. 数据模型
|
||||||
|
|
||||||
|
| # | 需求 | 说明 | 状态 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| R15 | 数据模型修订 | 七篇表设计(`api_keys` · `usage_logs` · `payments` · `qpu_history` · `members` · `users` · `sessions`)经作者逐篇审阅**定稿**:四张基线表复审落点 + `payments` 重定义(订阅/充值同构、币种无关、只存事实)+ 访问主体三新表 + `usage_logs` 来源提列(`billing_source` / `subscription_id`)与错误行去重修订(非消费行 `dedupe_key` 追加含 `content` 的指纹)+ `api_keys.member_id` 归属列 + 401 自动摘除状态机裁撤(引擎记录不决策);方言映射归数据访问层设计 | **定稿**(2026-10-01) |
|
||||||
|
|
||||||
|
## 7. R13 的支撑材料:CFW 可行性调研(2026-09-29,developers.cloudflare.com 官方文档核实,五项全部无阻断)
|
||||||
|
|
||||||
|
| # | 调研点 | 结论(Workers Free / Paid) |
|
||||||
|
|---|--------|---------------------------|
|
||||||
|
| 1 | Cron 粒度与补偿 | `*/5 * * * *` 支持(最小每分钟);UTC 执行;**单次执行上限 15 分钟**;`controller.scheduledTime` 可检测延迟;失败重试机制存在(`noRetry()` 可关);Cron Events 留存最近 100 次调用(可观测) |
|
||||||
|
| 2 | D1 限制 | 单库 **500 MB / 10 GB**;每 Worker 调用查询数 **50 / 1000**;单条 SQL ≤ 100 KB、绑定参数 ≤ 100/语句;库为单线程(写入串行)——本项目低频批量写入无碍 |
|
||||||
|
| 3 | Workers 运行时 | CPU:**10 ms**(网络等待不计)/ 30 s(<1h 间隔 cron;≥1h 为 15 min);外网子请求 **50 / 1000** 每调用;出站并发 6;Cron Triggers 每账号 5 / 250 |
|
||||||
|
| 4 | 免费额度 | Workers Free:10 万请求/天;D1 Free:读 500 万行/天、写 **10 万行/天**、存储 5 GB;**静态资产请求免费无限**(前端托管零成本) |
|
||||||
|
| 5 | 加密体系 | Workers 完整实现 Web Crypto:**HKDF ✓ / AES-256-GCM ✓ / HMAC-SHA256 ✓**(原生加速)——`KEYS_MASTER_KEY` 体系可平移 |
|
||||||
|
|
||||||
|
**负载画像对照**(key 数按 10–25 估):
|
||||||
|
|
||||||
|
- 每轮 cron:外网子请求 = 2 × key 数(Free 上限 50 → 最多 25 key);CPU 主要花在 1000 条 JSON 解析 + SHA-256 指纹 + AES-GCM 解密——**Free 10 ms 是唯一贴线风险点**(实现期实测,超限则升 Paid)
|
||||||
|
- D1 写入:5 分钟一批、日增千行级 ≪ 10 万行/天;存储日增 MB 级 → Free 500 MB 可撑约两年,Paid 10 GB 富余
|
||||||
|
- 查询面:个人低频聚合查询 ≪ 500 万行读/天(时间范围需走索引避免全表扫描)
|
||||||
|
- 同步引擎防重入:D1 单线程天然串行化写入,多 cron 并发触发无竞态风险;每轮幂等(`request_id` 去重),失败重试零副作用
|
||||||
|
|
||||||
|
## 8. 推进方式
|
||||||
|
|
||||||
|
**需求阶段收官(2026-10-01)**:15 项全部有归宿——R1 / R2 / R5 / R6 / R7 / R8 / R9 / R11 / R12 / R13 / R14 / R15 已裁决关闭,R10 裁撤,R3 / R4 延后(提案存档见 §9)。七篇表设计经作者逐篇审阅**定稿**。**下一批专门设计文档:API 契约 → 交互设计 → 同步引擎设计**(双形态架构下,逐篇立稿、逐篇审阅)。
|
||||||
|
|
||||||
|
## 9. 裁决记录
|
||||||
|
|
||||||
|
### 2026-09-29 · 访问主体模型(R11 · R12 · 关联 R5)
|
||||||
|
|
||||||
|
**访问主体 = 作者 + 成员,一个模型两种凭证**(运营方完全信任,不设身份;CFW 版可将来叠加 Cloudflare Access 作为前置纵深,不作鉴权主体)。
|
||||||
|
|
||||||
|
**成员侧(成员视图)**:
|
||||||
|
|
||||||
|
- **成员实体(新增)**:基本用户字段(`id` / `name` 显示名 / `remark` 备注 / `status` 启停 / `created_at`)+ 受管理凭证(`token_hash` 哈希存储 + `token_issued_at`;生成时明文展示一次,可吊销、可重发)+ key 归属(`api_keys.member_id`,**1:N**,允许空——未归属 key 存在)
|
||||||
|
- **一层成员模型**:key 创建权始终在作者(唯一 NewAPI 账号持有人);**归属登记即层级**——成员向下拼车 = 作者开新 key 挂其名下,平台不感知下级;算账口径 = 成员名下全部 key 的计费总和
|
||||||
|
- **成员视图**:登录壳(输入令牌 → sessionStorage,关标签失效)→ 自己关联的 key 列表 + 每 key 计费 + 聚合(应付部分随 R9 裁决展开)
|
||||||
|
- 成员侧无注册、无密码、无会话;令牌泄露处置 = 作者吊销重发(朋友信任关系下凭证事务经作者,路径短于自助改密)
|
||||||
|
|
||||||
|
**管理面侧(作者,唯一管理员)——最小登录体系**:
|
||||||
|
|
||||||
|
- **users 表**(单行:作者 `username` + `password_hash`)+ **sessions 表**(随机 256bit 令牌哈希存储、`expires_at` 如 7 天、登出即删,随时可踢)
|
||||||
|
- **密码哈希 = PBKDF2-SHA256**(WebCrypto 原生,高迭代)——Workers runtime 无 argon2/bcrypt,PBKDF2 是两形态统一实现(R14 的落地细节)
|
||||||
|
- **浏览器侧**:`HttpOnly + Secure + SameSite=Lax` cookie,路径限管理面——替代手动凭证管理
|
||||||
|
- **部署期初始化(空库双通道)**:初始用户名/密码经 env(母体)/ wrangler secret(CFW)传入;管理面入口幂等**三态判断**——非空 → 登录页;空库 + env 凭证在 → **静默落库**(部署完成即就绪,无抢注窗口;母体启动 / 首次请求检查,CFW 首次请求 / 首次 Cron 检查);空库 + env 凭证无 → **引导页**设新密码(重置兜底通道)
|
||||||
|
- **初始凭证为引导性质**:仅空库时生效,此后改 env 不改密;**改密走登录后的表单**(旧密码验证 → 落库,登录体系的基本配套);**env 凭证可留可删、无依赖**——留着:删 `users` 行即静默重建;删了:引导页当场设新密码(见 `users表字段设计.md` 3.1);抢注窗口仅存在于「删 env + 删行」的重置瞬间
|
||||||
|
- 无注册、无找回(忘记密码 = 服务器改库,管理员特权);登录节流为最小实现(失败延迟/计数)
|
||||||
|
- **基线三件套状态修正**:作者侧登录页与会话**解禁**(最小形态);admin 角色不做(单管理员无角色引擎用武之地);成员侧维持令牌无会话
|
||||||
|
|
||||||
|
**开放查看面裁撤**(同日裁决):「凭 sk-key 查」入口取消——sk-key 明文从 web 界面消失(仅登记时输入一次、随即加密);场景归位:**成员 → 令牌视图;作者 → 管理面**(key 清单下钻 + 全员总览面板,总览为新增设计项)
|
||||||
|
|
||||||
|
**审计日志(独立需求,与鉴权方案正交)**:管理面写操作(key 登记/启停、购买记录增删改、成员开户/停用、令牌签发/吊销、改密)逐条留痕:操作类型、时间、关键内容摘要;属业务核心层,两形态共有。同理正交的还有同步引擎运行日志——记录语义两形态一致,落盘通道随形态(母体 stdout/journald,CFW Workers Logs)
|
||||||
|
|
||||||
|
### 2026-09-29 · 分摊哲学与原材料(R9 重定义 · R10 裁撤 · R6/R7 重定义 · R12 形态修正)
|
||||||
|
|
||||||
|
**平台不做价值判断,只提供事实服务**——「谁该付谁多少钱」是人和人之间的社交问题;平台提供分账所需的全部原材料,数字全部客观中立。
|
||||||
|
|
||||||
|
**裁掉**:
|
||||||
|
|
||||||
|
- **应付款计算与分摊口径**——「闲置额度归谁」这类没有客观答案的问题,平台不回答(「按占额度分摊」vs「按占总用量分摊」的口径之争随之消失)
|
||||||
|
- **期末账单快照与周期管理**(R10 裁撤)——周期 = 任意查询时段(订阅期间为预置选项),无平台侧账单
|
||||||
|
- **每行日志的实付分摊金额(`amount_rmb`)**——「quota 占比 × 实付」是分摊口径的派生值而非事实,且随购买记录修订漂移;明细行金额回归 **quota + 额度美元**(÷ `quota_per_unit`,`qpu_history` 时间线,纯客观单位换算);基线「查看面金额口径 = 折算实付人民币」的设计作废
|
||||||
|
|
||||||
|
**保留与新增**:
|
||||||
|
|
||||||
|
- **支付记录**(订阅与充值同构):支付金额 ↔ 额度的**比率关系**——**币种无关**(系统不预设人民币或其他币种,币种是使用者之间的约定);订阅与充值两类支付事件统一建模(表结构与命名随 R15)
|
||||||
|
- **计费来源标注为一等维度**:`billing_source`(订阅 / 充值)随日志解析存储;按来源 / 成员 / key / 模型的任意时段多维聚合
|
||||||
|
- **充值登记**(R7 重定义):与订阅同构——不做充值用量的金额分摊(原「池子加权平均」方案裁撤)
|
||||||
|
- **成员视角(R12 形态修正)**:名下 key 的模型调用日志,**每条记录额外携带所属订阅/充值的信息**(具体携带形态待核查后设计)、**完全公开透明**(订阅与支付参数对成员可见)、无「应付」概念
|
||||||
|
|
||||||
|
### 2026-10-01 · R15 审阅定稿
|
||||||
|
|
||||||
|
七篇表设计经作者逐篇审阅,全部**定稿**。审阅期实质修订:`key_id` → `api_key_id`、`note` → `remark`、**401 自动摘除状态机裁撤**(api_keys 3.6——引擎记录不决策,`status` 纯手动开关)、**管理员初始化与重置改为空库双通道**(users 3.1——env 静默重建 / 引导页兜底,env 凭证降级为可弃);另有多处语义辨析随文入档(错误行去重指纹、量与换算率、sessions 与 members 零关系、QPU 存在必要性裁决保留)。
|
||||||
|
|
||||||
|
### 2026-09-29 · 多订阅并行(R8 重定义)
|
||||||
|
|
||||||
|
- **「一把尺」原则已随 `amount_rmb` 裁撤消亡**——其唯一客户是分摊金额计算;quota → 美元走 QPU(站点参数),订阅维度无需任何「尺」
|
||||||
|
- **日志归属 = 上游事实,非推算**:日志 `other.subscription_id` 直接关联支付记录(实现期核查项:登录态钱包页的订阅编号与日志 `subscription_id` 是否同一体系——基线草案既有推测,与 R12 携带形态并作一批真实数据核查)
|
||||||
|
- **未关联桶 = 查询层产物**(LEFT JOIN 未匹配集,无新字段),正常时恒为零,非零即异常信号;两种未关联(日志缺 ID / 有 ID 无登记)的展示区分留设计期
|
||||||
|
- **统计 = 派生视图**:数据库只存事实行,一切分组/分解/汇总为查询时聚合,无预计算存储
|
||||||
|
|
||||||
|
### 2026-09-29 · 数据资产域延后(R3 / R4)
|
||||||
|
|
||||||
|
**裁决:不纳入现阶段**(锦上添花能力,核心功能实现后再议)。提案存档如下,供重启讨论时作起点:
|
||||||
|
|
||||||
|
- **R4 采集健康与告警(三层提案)**:① 同步健康面板(数据截止 / 轮次成败 / 引擎状态——即管理面总览的一部分)② 缺口检测与留痕(停摆恢复轮检测时间缝隙,落「数据缺口事件」记录——丢了多少无法精确知道,但「丢过」可精确留痕)③ 主动告警(webhook 推送,作者自配钉钉/企微/TG bot;停摆判定 = 连续 N 轮失败或数据截止落后超阈值;不做邮件、不做分级路由)
|
||||||
|
- **R3 资产保护(导出 + 备份 + 异地性提案)**:① 管理面一键导出(全量日志 JSONL + 支付记录 + 归属)② 形态差异——母体 pg_dump 定时 + 作者定期拉取;CFW 版 D1 Time Travel(PITR:付费 30 天 / 免费 7 天)+ wrangler d1 export ③ 异地性三选(作者手动拉起步 / 推 R2 对象存储 / 只信 Time Travel),倾向手动拉起步、导出与推送逻辑分离留升级口子
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
# NewAPI 接口范围
|
||||||
|
|
||||||
|
> 计费平台在 NewAPI 中可用的接口参考:每个接口的鉴权档位、请求与返回结构、字段语义、实测行为与对平台的用途——**平台所有上游调用都出自这份文档**。
|
||||||
|
>
|
||||||
|
> 依据:NewAPI 源码考证 + 接口实测 · 实测环境:NewAPI 实例 · 上游仓库:https://github.com/QuantumNous/new-api
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 通用说明
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
所有需要凭证的接口均使用 API key 鉴权,在请求头中携带:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer sk-xxxxxxxxxxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
**不需要**账号登录态(access token / session)——这是仅凭 sk-key 即可查询的关键前提。BaseURL 即 NewAPI 站点地址,路径直接拼接、无多余前缀。
|
||||||
|
|
||||||
|
### 关键开关:`DisplayTokenStatEnabled`
|
||||||
|
|
||||||
|
- **billing 接口(⑤⑥)受影响**:开启 → 返回 token 级额度;关闭 → 回退账号级额度(会暴露整个账号余额)
|
||||||
|
- `/api/usage/token`、`/api/log/token` **不受影响**,恒返回 token 级数据
|
||||||
|
- **实测已确认当前站点开关为开启状态**;开关位于后台设置 → 计费设置(Billing)→ 货币与显示(Currency & Display)→ `Display Token Statistics`,仅管理员可改
|
||||||
|
|
||||||
|
### 换算参数:`QuotaPerUnit`
|
||||||
|
|
||||||
|
- 默认 `500000`(源码 `common/constants.go`,锚点 `$0.002 / 1K tokens`),**管理员可改且改后立即全站生效**(详见 ④)
|
||||||
|
- 实现时必须从 `GET /api/status` 实时读取,**不得硬编码**
|
||||||
|
- 实测交叉验证:`/api/usage/token` 的 `total_used = 673,419,457` ÷ 500000 = **1346.84 美元**;`/v1/dashboard/billing/usage` 的 `total_usage = 134,683.8914` 美分 ÷ 100 = **1346.84 美元**——完全吻合
|
||||||
|
|
||||||
|
## 1. 鉴权三态总览
|
||||||
|
|
||||||
|
本项目对接的接口按鉴权中间件分三档:
|
||||||
|
|
||||||
|
| 鉴权档 | 中间件 | 语义 | 本项目的请求方式 |
|
||||||
|
|--------|--------|------|----------------|
|
||||||
|
| **公开**(none) | 无 / `HeaderNavModulePublicOrUserAuth` | 不持有任何凭证即可访问 | 直接请求,无需 key |
|
||||||
|
| **严格**(strict) | `TokenAuth` | 校验 key 有效、未过期、有剩余额度;无效直接被拒 | 携带 sk-key 请求 |
|
||||||
|
| **宽松**(lenient) | `TokenAuthReadOnly` | 只验 key 存在且用户未封禁;**过期/额度耗尽仍可查**,但**显式禁用(Disabled)会被 401 拒绝**(`middleware/auth.go` 注释原文:only explicitly disabled tokens are denied) | 携带 sk-key 请求;**本项目主要依赖此档** |
|
||||||
|
|
||||||
|
> 失败状态码全集(2026-09-13 源码复核):**401** = key 不存在(`MsgTokenInvalid`)或 token 被显式禁用(`MsgTokenStatusUnavailable`);**403** = 账号被封禁;**500** = 数据库错误。→ 摘除计数只认 401;403 / 500 视为其他故障,不计入。宽松档**允许** key 过期(Expired)与额度耗尽(Exhausted)状态查询。
|
||||||
|
|
||||||
|
## 2. 接口清单
|
||||||
|
|
||||||
|
### 公开接口组(无需 sk-key)
|
||||||
|
|
||||||
|
#### ① `GET /api/user/groups` — 用户分组与倍率
|
||||||
|
|
||||||
|
- **返回**:站点配置的用户分组(default / vip 等)与倍率;**未启用分组时 `data` 为空对象**(当前站点实测即此状态)
|
||||||
|
- **用途**:解释「同一模型为什么计费不同」——分组倍率影响同一模型的实际消耗
|
||||||
|
- **本项目用途**:低优先级;仅当站点启用分组倍率后才有内容
|
||||||
|
|
||||||
|
#### ② `GET /api/perf-metrics/summary` — 全站模型服务质量
|
||||||
|
|
||||||
|
- **参数**:`hours`(统计窗口小时数,默认 24)
|
||||||
|
- **返回**:每个模型的 `avg_latency_ms`(平均延迟)、`success_rate`(成功率 %)、`avg_tps`(每秒 token 数)、`recent_success_series`(按小时采样的成功率时间序列);明细版 `/api/perf-metrics?model=xxx`(model 必填,另支持 group/hours)
|
||||||
|
- **鉴权细节**:`HeaderNavModulePublicOrUserAuth("pricing")`——默认公开,管理员可为 pricing 模块开启 requireAuth
|
||||||
|
- **实测**:HTTP 200,含站点真实模型的服务质量数据
|
||||||
|
- **本项目用途**:**高**——本工具独有视角。单个 sk-key 日志看不到全局服务质量;使用者反馈「慢/报错多」时,此接口能定位是模型问题还是自身问题。技术设计时可作为「模型健康度」功能的数据源
|
||||||
|
|
||||||
|
#### ③ `GET /api/rankings` — 全站模型用量排行
|
||||||
|
|
||||||
|
- **参数**:`period`(统计周期,默认 week)
|
||||||
|
- **返回**:每个模型的 `rank`(名次)、`previous_rank`(上期名次)、`total_tokens`(总用量)、`share`(份额)、`growth_pct`(环比增长 %)
|
||||||
|
- **实测**:HTTP 200,14KB 数据
|
||||||
|
- **本项目用途**:中——项目作者选「主推模型」(便宜且稳定)的数据依据;配合②的成功率可识别衰退模型
|
||||||
|
|
||||||
|
#### ④ `GET /api/status` — 站点配置与计费参数
|
||||||
|
|
||||||
|
- **返回**:60+ 字段,与本项目相关的计费字段:`quota_per_unit`(**换算比率当前生效值,管理员可改**)、`quota_display_type`(USD/CNY/TOKENS,影响 billing 接口语义)、`usd_exchange_rate`、`price` 等
|
||||||
|
- **本项目用途**:**关键**——换算比率的唯一权威来源,实现时必须实时读取、不得硬编码;同时感知 `quota_display_type` 切换(若切到 CNY/TOKENS,billing 接口数字语义会变)。实测当前站点:`quota_per_unit = 500000`、`quota_display_type = USD`、`usd_exchange_rate = 6.8`
|
||||||
|
- **`quota_per_unit` 的行为事实(2026-09-13 源码评估)**:由全局变量承载,改设置是「先写库、再热更新内存」(`model/option.go` 的 `UpdateOption` → `updateOptionMap`)——改后**立即全站生效、无需重启**。三条关键行为:
|
||||||
|
- **无历史**:`options` 表一 key 一行、改即覆盖——任何接口都读不到过去的取值;管理审计只记"键名被改过"、**不记值**
|
||||||
|
- **无编辑入口**:后台设置页该字段默认隐藏;当显示类型为 TOKENS 或取值非默认(≠500000)时才显示,且显示时仍为只读(`disabled`)——源码 `web/src/features/system-settings/general/pricing-section.tsx`(2026-09-29 main 分支复核);修改只能走 API(`PUT /api/option/`,Root 权限,热更新内存立即生效)
|
||||||
|
- **解读跟随当前值**:一切对外的金额换算(历史记录解读、billing 接口、界面显示)都在读取时实时使用当前值——改它 = 全站金额表达整体平移(数据库里的 quota 整数不受影响)
|
||||||
|
|
||||||
|
### 严格鉴权组(TokenAuth,sk-key)
|
||||||
|
|
||||||
|
#### ⑤ `GET /v1/dashboard/billing/subscription` — 总额度上限(美元)
|
||||||
|
|
||||||
|
- **返回结构**(源码 `controller/billing.go:13` → `GetSubscription`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"object": "billing_subscription",
|
||||||
|
"has_payment_method": true,
|
||||||
|
"soft_limit_usd": 100000000,
|
||||||
|
"hard_limit_usd": 100000000,
|
||||||
|
"system_hard_limit_usd": 100000000,
|
||||||
|
"access_until": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **字段**:`hard_limit_usd`(总额度上限,美元;三个 limit 字段同值)、`access_until`(token 过期时间,Unix 秒,0 = 永不过期)
|
||||||
|
- **注意**:无限额度 key 返回 `100000000`(一亿占位值,**无参考意义**);受 `DisplayTokenStatEnabled` 与 `quota_display_type` 影响(USD 模式直接是美元,CNY 模式做货币转换,Tokens 模式是原始 token 数)
|
||||||
|
- **路径变体**:`/dashboard/billing/subscription`(不带 `/v1` 前缀)是同一接口,为兼容不同客户端写法——测一组即可
|
||||||
|
- **本项目用途**:中——按 key 统计的「累计剩余」列候选数据源;与⑥相减可得剩余额度
|
||||||
|
|
||||||
|
#### ⑥ `GET /v1/dashboard/billing/usage` — 累计已用(美分)
|
||||||
|
|
||||||
|
- **返回结构**(源码 `controller/billing.go:75` → `GetUsage`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"object": "list",
|
||||||
|
"total_usage": 134683.8914
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **字段**:`total_usage`(**累计已用,单位美分**,÷100 = 美元;从 key 创建至今累计,非当月)
|
||||||
|
- **注意**:受 `DisplayTokenStatEnabled` 与 `quota_display_type` 影响(USD 模式才是美元);`hard_limit_usd`(⑤)− `total_usage ÷ 100`(⑥)= 剩余额度(仅有限额 key 有意义);可与⑧的 `total_used ÷ quota_per_unit` 交叉对账(不一致 = 站点改过比率,见④)
|
||||||
|
- **路径变体**:`/dashboard/billing/usage` 同⑥
|
||||||
|
- **本项目用途**:中——对账与校验用途
|
||||||
|
|
||||||
|
#### ⑦ `GET /v1/models` — 可用模型列表
|
||||||
|
|
||||||
|
- **返回**:当前 key 可用的模型列表,OpenAI 兼容格式(`data[].id` 为模型名)
|
||||||
|
- **变体**(与用量统计无关,列出仅为完整覆盖):`/v1/models/:model` 单模型详情、`/v1beta/models`(Gemini 格式)、`/v1beta/openai/models`(OpenAI 格式,Gemini 路由下)
|
||||||
|
- **已知限制**:`/v1/models` 查数据库 `abilities` 表(渠道实际配置),而 `/v1/models/:model` 查代码内硬编码的静态模型列表(`openAIModelsMap`)——渠道配了新模型或自定义模型名但代码未收录时,会出现"列表里有但单查返回 `model_not_found`"的现象;不影响实际模型调用
|
||||||
|
- **本项目用途**:低——调试期验证渠道配置用
|
||||||
|
|
||||||
|
### 宽松鉴权组(TokenAuthReadOnly,sk-key)
|
||||||
|
|
||||||
|
#### ⑧ `GET /api/usage/token` — token 级用量汇总(原始 quota)
|
||||||
|
|
||||||
|
- **返回结构**(源码 `controller/token.go` → `GetTokenUsage`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": true,
|
||||||
|
"message": "ok",
|
||||||
|
"data": {
|
||||||
|
"object": "token_usage",
|
||||||
|
"name": "令牌名称",
|
||||||
|
"total_granted": 6012,
|
||||||
|
"total_used": 673419457,
|
||||||
|
"total_available": -673413445,
|
||||||
|
"unlimited_quota": true,
|
||||||
|
"model_limits": {},
|
||||||
|
"model_limits_enabled": false,
|
||||||
|
"expires_at": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **字段**:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `name` | string | 令牌名称 |
|
||||||
|
| `total_granted` | int | 总额度 = RemainQuota + UsedQuota(原始 quota 单位) |
|
||||||
|
| `total_used` | int | **累计已用**(原始 quota 单位)。从 key 创建以来累计,非当月;换算美元 ÷ `quota_per_unit` |
|
||||||
|
| `total_available` | int | 剩余 = granted − used。无限额度时为负数,无实际意义 |
|
||||||
|
| `unlimited_quota` | bool | 是否无限额度。true 时 granted/available 无实际意义 |
|
||||||
|
| `model_limits` / `model_limits_enabled` | object / bool | 模型限制 |
|
||||||
|
| `expires_at` | int | 过期时间(Unix 秒),0 = 永不过期 |
|
||||||
|
|
||||||
|
- **注意**:额度单位是**原始 quota 整数**,需自行换算美元;禁用再解禁不会重置 `total_used`(源码验证:`Update()` 的 `.Select(...)` 列表不含 `used_quota`);key 耗尽后仍可查(用完也能对账)
|
||||||
|
- **本项目用途**:**高**——按 key 统计「全周期累计(已用/剩余)」的直接数据源
|
||||||
|
|
||||||
|
#### ⑨ `GET /api/log/token` — 最近 1000 条调用日志
|
||||||
|
|
||||||
|
- **返回结构**(源码 `controller/log.go:62` → `GetLogByKey`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 123456, "user_id": 12, "created_at": 1751328000, "type": 2,
|
||||||
|
"content": "模型倍率 1.0,分组倍率 1.0", "username": "用户名", "token_name": "令牌名",
|
||||||
|
"model_name": "gpt-4o", "quota": 2500, "prompt_tokens": 1500, "completion_tokens": 500,
|
||||||
|
"use_time": 2, "is_stream": true, "channel": 0, "channel_name": "", "token_id": 100,
|
||||||
|
"group": "default", "ip": "",
|
||||||
|
"request_id": "20260911113652…",
|
||||||
|
"other": "{\"billing_source\":\"subscription\",\"cache_tokens\":29056,\"frt\":3931,\"model_ratio\":0.075,\"request_path\":\"/v1/messages\",\"stream_status\":{\"status\":\"ok\"},…}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **字段说明**(共 21 个字段;按实测响应 + 上游源码校正,2026-09-11):
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | int | 展示序号,**每次从 1 重编**——跨请求无意义,绝不能用于去重 |
|
||||||
|
| `user_id` | int | 账号 ID(同一实例内恒定) |
|
||||||
|
| `created_at` | int | 调用时间(**UTC Unix 秒**,三字段交叉实测) |
|
||||||
|
| `type` | int | 日志类型(取值见下);token 日志实见 2=消费、5=错误 |
|
||||||
|
| `content` | string | 消费记录为空串;错误记录为错误详情文本 |
|
||||||
|
| `username` / `token_name` | string | 账号用户名 / 令牌名 |
|
||||||
|
| `model_name` | string | 调用的模型名 |
|
||||||
|
| `quota` | int | 本次消耗的 quota(换算美元 ÷ `quota_per_unit`);错误记录为 0 |
|
||||||
|
| `prompt_tokens` / `completion_tokens` | int | 输入 / 输出 token 数;错误记录为 0 |
|
||||||
|
| `use_time` | int | 耗时(秒);错误记录为 0 |
|
||||||
|
| `is_stream` | bool | 是否流式 |
|
||||||
|
| `channel` / `channel_name` | int / string | 渠道 ID / 渠道名(普通用户视角后者为空串) |
|
||||||
|
| `token_id` | int | 上游 token ID(对账用) |
|
||||||
|
| `group` | string | 分组名(默认 `default`) |
|
||||||
|
| `ip` | string | 请求 IP(普通用户视角为空串) |
|
||||||
|
| `request_id` | string | 请求 ID(`yyyyMMddHHmmss` 前缀 + 随机串);源码写入有兜底保证非空——**去重唯一依据**。**补充实测(2026-09-15)**:错误场景下**不保证行级唯一**——1000 条实测样本中 8 个 `request_id` 各对应 3 条 `type=5` 错误记录(同一请求的多次失败 / 重试,`content` 的 `status_code` 可不同);按 `request_id` 去重会把它们合并为 1 条(消费类 `type=2` 样本中未观察到重复) |
|
||||||
|
| `upstream_request_id` | string | 上游请求 ID;JSON tag 为 `omitempty`——**空值时不出现、有值时才出现**,消费方需按「可选字段」处理 |
|
||||||
|
| `other` | string | JSON 字符串,扩展信息(结构见下) |
|
||||||
|
|
||||||
|
- **`type` 完整取值**(上游源码 `model/log.go`;常量注释明确「刻意不用 iota,以保证枚举值永不改变」):
|
||||||
|
|
||||||
|
| 值 | 常量 | 含义 | 可能出现在 token 日志? |
|
||||||
|
|----|------|------|------------------------|
|
||||||
|
| 0 | `LogTypeUnknown` | 未知 | 兜底值 |
|
||||||
|
| 1 | `LogTypeTopup` | 充值 | 否(账号级,无 token_id) |
|
||||||
|
| 2 | `LogTypeConsume` | 消费 | **是**(实测) |
|
||||||
|
| 3 | `LogTypeManage` | 管理操作 | 否(审计类) |
|
||||||
|
| 4 | `LogTypeSystem` | 系统 | 否 |
|
||||||
|
| 5 | `LogTypeError` | 错误 | **是**(实测) |
|
||||||
|
| 6 | `LogTypeRefund` | 退款 | 可能(任务计费链路带 token_id) |
|
||||||
|
| 7 | `LogTypeLogin` | 登录 | 否(账号级) |
|
||||||
|
|
||||||
|
> 本接口按 `token_id` 过滤、不按 `type` 过滤——凡是「带 token_id 写入」的类型都可能返回。
|
||||||
|
|
||||||
|
- **`other` 字段**(JSON 字符串,内容随 `type` 变化,实测 2026-09-11):
|
||||||
|
- 消费记录(`type=2`):`billing_source`、`cache_ratio`、`cache_tokens`、`completion_ratio`、`frt`、`group_ratio`、`model_price`、`model_ratio`、`reasoning_effort`、`request_conversion`、`request_path`、`stream_status`(对象)、`subscription_*`(订阅维度,共 8 个)、`user_group_ratio`、`wallet_quota_deducted`
|
||||||
|
- 错误记录(`type=5`):`error_code`、`error_type`、`request_path`、`status_code`
|
||||||
|
- 注:普通用户视角下,服务端会先对 `other` 做「可见性过滤」再返回——上表为过滤后实际可见的键
|
||||||
|
|
||||||
|
- **关键限制**:
|
||||||
|
- **硬上限 1000 条**:固定返回最近 1000 条(`Limit(common.MaxRecentItems)`,`MaxRecentItems = 1000`,`common/constants.go`);**不支持任何请求参数**(源码不读任何 `c.Query()`,无分页 / limit / 时间过滤);固定按 `id desc` 排序(最新 1000 条)
|
||||||
|
- **超过 1000 条的旧记录无法获取**:无法翻页、无法扩大 limit;实测当前站点日志速率下 1000 条 ≈ 3 天窗口
|
||||||
|
- **大响应**:pretty-print 后 2~4MB——同步引擎需直面的单次响应体积
|
||||||
|
- **本项目用途**:**核心**——增量同步引擎的唯一数据源;本地攒全量后,任意时间范围的用量/明细/模型统计都是本地聚合查询
|
||||||
|
|
||||||
|
## 3. 换算与 quota 计算
|
||||||
|
|
||||||
|
### 核心换算公式
|
||||||
|
|
||||||
|
```
|
||||||
|
原始 quota ÷ quota_per_unit(默认 500000) = 美元
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消费记录的 `quota` 是怎么算出来的(实测反推,2026-09-13)
|
||||||
|
|
||||||
|
按 tokens 计费的模型,`quota` 在**请求时**由「倍率 × 用量」算出并固化进日志(此后不再变化):
|
||||||
|
|
||||||
|
```
|
||||||
|
quota = ( (prompt_tokens − cache_tokens) ← 提示 tokens(未命中缓存部分)
|
||||||
|
+ cache_tokens × cache_ratio ← 缓存命中部分按折扣
|
||||||
|
+ completion_tokens × completion_ratio ) ← 补全 tokens 按倍率
|
||||||
|
× model_ratio ← 模型倍率
|
||||||
|
× group_ratio ← 分组倍率
|
||||||
|
```
|
||||||
|
|
||||||
|
- 验证:6/6 条实测日志逐条吻合(差值仅为四舍五入)
|
||||||
|
- 按次计费模型走 `model_price` 分支:`quota = model_price × QuotaPerUnit × group_ratio`(源码 `relay/helper/price.go`)
|
||||||
|
- **公式中没有 `QuotaPerUnit`**:倍率与美元的锚定(`model_ratio = 1` ⇔ `$0.002 / 1K tokens`)已隐含它——倍率制下改 `QuotaPerUnit` 等于全站重定标;美元定价路径(`model_price` / 表达式 `$/1M`)才显式乘 `QuotaPerUnit`
|
||||||
|
|
||||||
|
## 4. 范围之外(sk-key 不可达)
|
||||||
|
|
||||||
|
### 4.1 不存在的接口(404)
|
||||||
|
|
||||||
|
| 路径 | 状态 |
|
||||||
|
|------|------|
|
||||||
|
| `/v1/usage`、`/v1/quota`、`/api/token/self` | 不存在 |
|
||||||
|
|
||||||
|
### 4.2 存在但需要登录态的接口(sk-key 访问 401)
|
||||||
|
|
||||||
|
支持时间范围查询与过滤,但**必须用账号登录态**(access token + `New-Api-User` header):
|
||||||
|
|
||||||
|
| 路径 | 鉴权 | 能力 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/log/self` | `UserAuth()` | 支持 `start_timestamp`、`end_timestamp`、`token_name` 过滤,返回日志明细 |
|
||||||
|
| `/api/log/self/stat` | `UserAuth()` | **支持时间范围 + token_name,返回 quota 汇总**(调 `SumUsedQuota`)——NewAPI 中最适合算"当月用量"的接口,是「按时间段统计用量」无法纯 sk-key 实现的**核心障碍** |
|
||||||
|
| `/api/data/self` | `UserAuth()` | 按日聚合的看板数据 |
|
||||||
|
| `/api/token/` (GET/POST/PUT/DELETE) | `UserAuth()` | token 的增删改查 |
|
||||||
|
| `/api/user/self` | `UserAuth()` | 用户自身信息 |
|
||||||
|
|
||||||
|
### 4.3 订阅信息
|
||||||
|
|
||||||
|
- **没有免 auth 的接口可直接查询**(作者实测,2026-09-13);登录态的「钱包」页面可见——显示订阅编号(形如「订阅 #N」)、状态(生效中 / 已过期 + 过期时间)、总额度与已用 / 剩余
|
||||||
|
- 订阅编号**推测**与日志 `other.subscription_id` 为同一体系(实现期以真实数据核对)
|
||||||
|
- 对应设计:订阅信息由购买者手工录入(见 `docs/design/payments表字段设计.md`)
|
||||||
|
|
||||||
|
### 4.4 参数被忽略的情况(实测)
|
||||||
|
|
||||||
|
| 请求 | 实际行为 |
|
||||||
|
|------|---------|
|
||||||
|
| `/api/log/token?p=2` | 参数被忽略,仍返回最近 1000 条 |
|
||||||
|
| `/api/log/token?limit=5000` | 参数被忽略,仍返回最近 1000 条 |
|
||||||
|
| `/api/log/token?start_timestamp=1751328000` | 参数被忽略,仍返回最近 1000 条 |
|
||||||
|
| `/v1/dashboard/billing/usage?start_date=2025-07-01` | 参数被忽略 |
|
||||||
|
|
||||||
|
## 5. 行为事实:写权限与 key 生命周期
|
||||||
|
|
||||||
|
- **sk-key 没有任何写接口**:没有 POST/PUT/DELETE 能改 token 自身;无法通过 sk-key 重置 `used_quota`(没有"重置用量"接口)
|
||||||
|
- **禁用 token 只改 `status` 字段**:不重置 `used_quota`、不清零 `remain_quota`、不丢历史日志;解禁后三者原封不动
|
||||||
|
- **唯一让 `used_quota` 归零的途径**:删除 token 后新建(产生全新 Id/Key,旧日志按旧 token_id 关联,新 token 查不到旧日志)
|
||||||
|
- **解禁保护**:额度耗尽(`Exhausted`)且 `RemainQuota ≤ 0` 且非无限额度的 token 不能直接解禁,需先充值;已过期的 token 也不能直接解禁,需改过期时间
|
||||||
|
|
||||||
|
## 6. 源码参考
|
||||||
|
|
||||||
|
| 关注点 | 文件路径 | 关键函数/位置 |
|
||||||
|
|--------|---------|--------------|
|
||||||
|
| 路由注册 | `router/api-router.go` | usageRoute、logRoute 组 |
|
||||||
|
| 路由注册 | `router/dashboard.go:14-23` | billing 路由组 |
|
||||||
|
| 路由注册 | `router/relay-router.go` | models 路由 |
|
||||||
|
| API key 鉴权 | `middleware/auth.go:171` | `TokenAuthReadOnly()` |
|
||||||
|
| API key 鉴权 | `middleware/auth.go` | `TokenAuth()` / `SetupContextForToken` |
|
||||||
|
| token 用量查询 | `controller/token.go` | `GetTokenUsage` |
|
||||||
|
| 日志查询 | `controller/log.go:62` | `GetLogByKey` |
|
||||||
|
| 日志查询(model) | `model/log.go` | `GetLogByTokenId`(Limit 1000) |
|
||||||
|
| billing 查询 | `controller/billing.go:13 / :75` | `GetSubscription` / `GetUsage` |
|
||||||
|
| 显示开关 | `common/constants.go` | `DisplayTokenStatEnabled` |
|
||||||
|
| 日志上限 | `common/constants.go` | `MaxRecentItems = 1000` |
|
||||||
|
| 配额单位 | `common/constants.go` | `QuotaPerUnit`(默认 500000) |
|
||||||
|
| token 更新 | `model/token.go` | `Update()`(Select 不含 used_quota) |
|
||||||
|
| 额度增减 | `model/token.go` | `increaseTokenQuota` / `decreaseTokenQuota` |
|
||||||
|
| 日志类型常量 | `model/log.go` | `LogTypeConsume = 2`、`LogTypeError = 5` 等(刻意不用 iota) |
|
||||||
Reference in New Issue
Block a user