Files
new-api-key-usage/docs/design/usage_logs表字段设计.md
T
skycurtain 45b312b357 docs: 设计文档体系落定(七篇定稿)
- docs/design:四张表字段设计 · 交互设计(方向层 + 细节层)· API 契约 · 同步引擎设计
- docs/upstream:sk-key 接口实证(430 行)· 接口范围(9 个接口)
- README(全景入口)· AGENTS.md(AI 协作者指引)
- 废弃旧稿已删除(不随仓库留存)
2026-09-14 23:54:52 +08:00

115 lines
7.0 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.
# usage_logs 表字段设计
> 数据模型第二张表:调用日志明细——增量同步的落点,本项目「任意时间范围用量」的唯一数据来源。字段清单依据 `/api/log/token` 实测响应(2026-09-11,1000 条样本)与上游源码(`model/log.go`)双重校对;上游只有 1000 条滚动窗口,**错过即永久丢失**。
| 项目 | 内容 |
|------|------|
| 状态 | 定稿(2026-09-11,逐字段讨论收敛) |
| 关联 | `docs/upstream/NewAPI-sk-key接口文档.md`(接口②字段依据)· `docs/design/api_keys表字段设计.md`(`key_id` 外键指向) |
| 定稿范围 | 列结构 + 去重与写入策略;同步引擎细节(调度、401 处理)不在此文 |
| 字段数 | 23 = 20 上游字段 + 3 系统字段(上游响应共 21 个字段,其中展示序号 `id` 不落表,见 3.2) |
---
## 1. 表定位
- **一行 = 一次上游调用记录**(消费 / 错误等类型全覆盖)
- 它是本项目最核心的数据资产:上游 `/api/log/token` 只保留最近 1000 条(实测当前速率下 ≈ 3 天),**错过即永久丢失**——本地全量累积是"任意时间范围用量"的唯一实现路径
- 与 `api_keys` 多对一:每条日志经 `key_id` 指向其所属 key
- 数据规模:实测约 300+ 条/天,量级为数十万行/年——单表 + 时间索引完全够用,无需分区或预聚合
## 2. 字段清单
### 2.1 系统字段(3 个,本系统产生)
| 列 | 类型 | 值来源 | 说明 |
|----|------|--------|------|
| `id` | bigserial PK | 自增 | 本地主键(与上游的展示序号 `id` 无关) |
| `key_id` | bigint FK | 同步引擎 | → `api_keys.id`;日志归属与索引锚点 |
| `dedupe_key` | text UNIQUE | 写入前生成 | **去重唯一键**:`${key_id}:${request_id}`;`request_id` 为空时退化为内容指纹(见 3.1) |
### 2.2 上游字段(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` 的落点) |
| 普通 | `(key_id, created_at DESC)` | 明细页 + 按 key 聚合 |
| 普通 | `(created_at DESC)` | 跨 key 合计与统计 |
## 3. 决策记录(含理由)
### 3.1 去重:单列 `dedupe_key`(即 `request_id` 的落库形态)
- 去重依据就是 `request_id`(上游为每次请求生成的唯一编号);`dedupe_key` 是它在库中的形态,99.9% 的情况下等于 `${key_id}:${request_id}`
- **为什么不把 UNIQUE 直接挂在 `request_id` 列上**:PostgreSQL 的空值语义两难——`NULL` 在唯一索引中互不冲突(空值行会重复入库);而把空值转成空串则过度冲突(第二条空值记录被静默丢弃)。单列 `dedupe_key` + 空值指纹兜底从根上规避这两种坏结局
- `key_id:` 前缀:跨 key 隔离的零成本保险
- 内容指纹(仅 `request_id` 为空时启用):`sha256(key_id, created_at, model_name, quota, prompt_tokens, completion_tokens)`——同一条日志的重复拉取必然同指纹(正确去重),真实不同的请求指纹不同(都保留)
- `request_id` 原值仍单独成列(2.2 之一)——两者并行,互不替代
- **为什么它不做主键**:主键是「行身份」(短、稳定、永不变化),`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 为稳定枚举),展示层翻译(未知值原样显示数字)
## 4. 写入时机与策略
| 时机 | 动作 |
|------|------|
| 登记首轮同步 | 拉最近 1000 条 → 逐条入库 |
| 每轮轮询 | 同上;`INSERT ... ON CONFLICT (dedupe_key) DO NOTHING`——幂等,只落新行 |
- 写入者唯一:同步引擎(不存在其他写入路径)
- 日志不可变:已入库的行不做 UPDATE
- 与本表相关的读取链:从 `api_keys` 取密文 → 解密 → 调上游 → 新日志写本表
## 5. 待定事项(关联后续环节)
| 事项 | 归属 |
|------|------|
| 明细/统计的查询口径(`type` 过滤策略) | API 契约 |
| `other` 内高频键是否提为独立列(如 `cache_tokens`) | 出现真实分析需求时再评估 |
| 数据保留策略(是否清理旧数据) | 后议 |