Files
new-api-key-usage/docs/design/usage_logs表字段设计.md
T
skycurtain 0e9db439ba docs: 收尾(阶段 8)——部署文档 + 端到端走查 + 文档状态
- docs/delivery/部署文档.md:前置条件 / 环境变量 / 一条命令链 / systemd 与 Nginx 示例 / 首次运行检查 / 运行约束(后端单实例 · 3 天窗口 · 主密钥 · 401 摘除)/ 升级 / 排查
- 从零端到端走查:清库 → db:push → 启动(空转 + QPU 基线)→ 管理面登记 → 引擎立即补一轮(984 条)→ 查看面查询(未折算「—」)→ 补录购买记录 → 金额出现 → 换算参数时间线;无 pageerror
- 8 篇设计文档状态 → 已实现(2026-09-15)
- README:状态行 / 文档地图(含 delivery)/ 下一步(部署 + 待补验与裁决清单)
- AGENTS.md:项目速览更新
2026-09-15 01:10:55 +08:00

6.9 KiB
Raw Blame History

usage_logs 表字段设计

数据模型第二张表:调用日志明细——增量同步的落点,本项目「任意时间范围用量」的唯一数据来源。字段清单依据 /api/log/token 实测响应(2026-09-11,1000 条样本)与上游源码(model/log.go)双重校对;上游只有 1000 条滚动窗口,错过即永久丢失。

项目 内容
状态 已实现(2026-09-15)
关联 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) 出现真实分析需求时再评估
数据保留策略(是否清理旧数据) 后议