- docs/design:四张表字段设计 · 交互设计(方向层 + 细节层)· API 契约 · 同步引擎设计 - docs/upstream:sk-key 接口实证(430 行)· 接口范围(9 个接口) - README(全景入口)· AGENTS.md(AI 协作者指引) - 废弃旧稿已删除(不随仓库留存)
115 lines
7.0 KiB
Markdown
115 lines
7.0 KiB
Markdown
# 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`) | 出现真实分析需求时再评估 |
|
||
| 数据保留策略(是否清理旧数据) | 后议 |
|