数据库设计
1. 数据库表设计原则
1.1 基础要求
所有表都使用 PostgreSQL 默认的
publicschema所有表名都使用
snake_case命名风格,采用复数名词所有表都存在主键,当使用非联合主键时 id 应当为
<prefix>_<ulid>的形式所有表都使用
timestamptz作为时间字段所有表统一包含:
created_at timestamptz not nullupdated_at timestamptz not null
原则上任何直接或间接归属于 Tenant 的表都必须存在
tenant_id、任何直接或间接归属于 Route 的表都必须存在route_id,避免查询时过多联查
1.2 表的分类
schema_migrations表用于存储迁移的执行情况routes表为路由的入口表,绝大多数功能紧贴routes设计tenants表为租户表,是routes的上层表upstreamproviders等表为上游配置表,consumersconsumer_api_keys等表为辅助配置表,这些都是归属于租户的资源plugin_configs表为统一插件配置表,对于无需复杂查询的插件而言将数据存储至其中即足够,这些都是归属于路由的资源
1.3 ID 规范与来源字段
该定义仅供 text 类型主键使用,部分情况下表可能需要选用联合主键或自增数字主键
所有实体主键及其引用字段统一使用 text,格式固定为 <prefix>_<ulid>
约束:
<prefix>尽量控制在 2-3 个字符,极端不超过 5 个字符;<prefix>必须全局唯一,避免不同表之间产生歧义;<prefix>应尽量与实体原始单词或词组对应;- 同一张表允许存在多种 prefix,只要这些 prefix 能清楚表达差异。
- 程序侧所有 ID 字段统一使用完整字符串,不拆分保存裸 ULID。
prefix 定义:
| 实体 / 表 | prefix | 说明 |
|---|---|---|
tenants | tn_ | tenant |
routes | rt_ | route |
providers | gp_ / tp_ | global provider / tenant provider |
upstreams | ups_ | upstream |
provider_pricings | ppr_ | provider pricing |
consumers | cs_ | consumer |
consumer_api_keys | cak_ | consumer api key |
credit_ledger_entries | cle_ | credit ledger entry |
request_logs | rql_ | request log |
对于同一张表中可能同时承载 global / tenant / route / provider 等多个来源的数据,除了在 id prefix 上体现来源外,还统一增加两个专门字段:
source_scope text not nullsource_ref_id text not null default ''
示例:
- 全局数据:
source_scope='global',source_ref_id='' - tenant 数据:
source_scope='tenant',source_ref_id='tn_xxx' - route 数据:
source_scope='route',source_ref_id='rt_xxx' - provider 数据:
source_scope='provider',source_ref_id='gp_xxx'或tp_xxx
1.4 复杂策略使用 JSONB
以下内容不建议一开始强行拆成高度规范化小表:
- Guard 规则 DSL
- 脱敏规则 DSL
- DLP / egress 规则 DSL
- protocol adapter 的细节参数
- fallback 条件细节
这类结构变化快,建议:
- 元信息关系化;
- 规则内容用
jsonb; - 写入时直接落到主业务表中的当前生效结构,必要时做适度冗余。
1.5 Secret 处理
- 适用范围包括 upstream 凭证,以及 consumer API key 等;
- 首版本为降低复杂性,数据库中先直接存放明文凭证;
- 后续版本再引入 KMS / 加密存储;
1.6 高频计数不要直接写 PG
以下运行时高频数据不建议由 router 直接写 PG:
- QPS/CPS 限速计数
建议:
- 实时判定用 Redis;
- usage/event 在主流程写入 PG;
- Consumer 与 API Key 的 Credit 余额由于要求强一致性,直接存于 RW PG 并在请求结束时扣减;
- RO PG 负责配置读取/监听;RW PG 负责 Credit 配额余额、usage、汇总、审计、账单;Redis 仅负责 rate limit。
当前后结算 Credit 语义:
- 不做预扣;
- 准入只检查
remaining_credit > 0; - 不预判本次请求最终会不会欠费;
- 请求完成后按实际 usage 与
provider_pricings.pricing.basePricing直接扣减; - 结算后允许
remaining_credit变成负数; - 请求级 billing 结果写入
request_logs.ext_fields.billing; - Consumer 余额经管理 API 调整时会写入账本;API Key 余额经管理 API 调整时当前不写账本。
1.7 Credit 记账单位
对外暴露费用统一使用 Credit,内部所有计费、配额、余额、账单数据结构也统一使用 Credit,并且必须为整数。
设计约束:
- 所有价格、余额、quota、账单金额字段统一使用
bigint; - 程序中无论是配额还是计费,都只使用 Credit 作为单位,不涉及法币转换;
- router 和所有数据库表内部不保存浮点金额或法币币种;
- 法币与 Credit 的换算由业务侧负责,不属于 router 核心职责;
- 建议默认采用
1 USD = 1_000_000 Credit、1 CNY = 1_000_000 Credit; - 上述换算只是业务接入建议,router 内部只消费和产出整数 Credit。
2. 核心业务表设计
2.1 tenants
租户主表。
| 字段 | 类型 | 说明 |
|---|---|---|
id | text pk | 主键,格式 tn_<ulid> |
name | text not null | 租户名称 |
status | text not null | active / disabled |
inbound_rate_limit_qps | int not null default 0 | 当前 tenant 级入口限速字段。[TODO: new plugin system] |
detect_rate_limit_cpm | int not null default 0 | 当前 Guard detect 侧限速字段。[TODO: new plugin system] |
metadata | jsonb not null default '{}' | 扩展字段 |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
索引:
index (status)
2.2 routes
路由主表,只负责入口与治理,不承载唯一 upstream。
| 字段 | 类型 | 说明 |
|---|---|---|
id | text pk | 主键,格式 rt_<ulid> |
tenant_id | text not null fk -> tenants.id | 所属 tenant |
name | text not null | 路由名称 |
path_prefix | text not null | 路由前缀,如 /demo |
max_attempts | int not null default 2 | 单次请求最多尝试的候选数,最小为 1 |
disabled_at | timestamptz null | 非空表示当前 route 禁用 |
disable_auth | boolean not null default false | 为 true 时直接跳过认证 |
protocol_transformation_type | text not null default 'if-different-protocol' | 协议转换模式:enabled / if-different-protocol / disabled |
legacy_bearer_auth_tokens | text[] not null default '{}' | route 级 legacy bearer token 列表 |
passthrough_auth_token | boolean not null default false | 当前仍保留;仅作为 deprecated 的 legacy 兼容开关;后续计划移除 |
groups | text[] not null default '{}' | route 级分组约束;空数组表示不限制任何组 |
labels | jsonb not null default '{}' | 当前 route 级自定义标签 |
metadata | jsonb not null default '{}' | 扩展字段 |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
约束/索引:
unique (path_prefix)index (tenant_id, disabled_at)index using gin (groups)check (path_prefix ~ '^/[a-zA-Z0-9_-]+$')check (protocol_transformation_type in ('enabled', 'if-different-protocol', 'disabled'))
2.3 providers
Provider 模板表。
Provider 可来源于全局默认或 tenant 自定义。运行时按
protocol、compat与request_header_overrides分层工作:compat只提供协议层配置,request_header_overrides只提供网关层 request header 覆盖。
其中protocol的语义对应aidy-models中Provider.api。
| 字段 | 类型 | 说明 |
|---|---|---|
id | text pk | 主键,格式 gp_<ulid> 或 tp_<ulid> |
source_scope | text not null | global / tenant |
source_ref_id | text not null default '' | 全局为空字符串;tenant 级为 tn_xxx |
name | text not null | 展示名称 |
description | text null | 描述 |
protocol | text not null | 运行时协议,语义对应 aidy-models.Provider.api |
base_url | text null | 默认 base URL |
headers | jsonb not null default '{}' | 默认静态 headers;语义上必须是扁平 string -> string 映射 |
compat | jsonb not null default '{}' | 默认兼容性配置 |
request_header_overrides | jsonb not null default '{}' | 默认 request header override 迷你 DSL |
capabilities | text[] not null default '{}' | provider 默认 forwarder 能力集合;空数组表示按 protocol 推导默认集 |
check_model | text null | provider 自检时使用的模型名 |
disabled_at | timestamptz null | 非空表示当前 provider 禁用 |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
约束/索引:
unique (source_scope, source_ref_id, name)index (source_scope, source_ref_id, disabled_at)
2.4 upstreams
tenant 下的上游目标。
创建 upstream 时必须选择一个 provider。
upstream 保存当前实际生效的base_url/headers/compat/request_header_overrides和内嵌api_keys;运行时仍以 provider 的protocol决定协议实现。
| 字段 | 类型 | 说明 |
|---|---|---|
id | text pk | 主键,格式 ups_<ulid> |
tenant_id | text not null fk -> tenants.id | 所属 tenant |
provider_id | text not null fk -> providers.id | 绑定的 provider 模板 |
name | text not null | 上游名称 |
group | text not null default 'default' | upstream 所属分组 |
base_url | text null | upstream 侧实际使用的 base URL |
headers | jsonb not null default '{}' | upstream 侧实际使用的 headers;语义上必须是扁平 string -> string 映射 |
compat | jsonb not null default '{}' | upstream 侧实际使用的 compat 配置 |
request_header_overrides | jsonb not null default '{}' | upstream 侧实际使用的 request header override 迷你 DSL |
capabilities | text[] not null default '{}' | upstream forwarder 能力覆盖;空数组表示继承 provider 默认值 |
api_keys | jsonb not null default '[]' | 内嵌的 upstream API key 数组 |
check_model | text null | upstream 自检时使用的模型名 |
max_idle_conns_per_host | int null | 连接池参数 |
lb_weight | int not null default 100 | 默认权重 |
priority | int not null default 100 | 默认优先级 |
disabled_at | timestamptz null | 非空表示当前 upstream 禁用 |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
约束/索引:
index (tenant_id, disabled_at)index (tenant_id, provider_id)index (tenant_id, group, disabled_at)
说明:
upstreams直接表达 endpoint、headers、compat、request_header_overrides、连接池参数、forwarder 能力覆盖与内嵌 API key; 其中headers只表示静态单值 header map,不承载任意 JSON 结构;- 若
api_keys为空,则表示该 upstream 不需要额外出网凭证; - 若
api_keys非空,运行时当前按稳定顺序选择首个可用 key。 request_header_overrides当前不支持覆盖或透传Host;Host始终由最终 upstream URL 决定。
2.5 upstream_models
定义某个 upstream 上可用的模型、映射与别名。
| 字段 | 类型 | 说明 |
|---|---|---|
tenant_id | text not null fk -> tenants.id | 所属 tenant |
upstream_id | text not null fk -> upstreams.id | 所属 upstream |
model | text not null | 对外暴露并统一用于路由/计费的模型名 |
is_alias | boolean not null default false | 是否为 alias 行 |
upstream_model | text not null | upstream 实际调用的模型名 |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
约束/索引:
primary key (upstream_id, model)index (tenant_id)index (model)
说明:
upstream_models直接承担“模型到 upstream”的映射,不再经过独立的model_routing_*表;- 若要暴露 alias,就直接再写一行
is_alias = true的记录。
2.6 provider_pricings
Provider 级定价表。
定价现在直接来源于 provider。
使用内置 provider 时,读取该 global provider 的定价;使用 tenant 自定义 provider 时,读取该 tenant provider 的定价。
| 字段 | 类型 | 说明 |
|---|---|---|
id | text pk | 主键,格式 ppr_<ulid> |
tenant_id | text null | tenant 自定义 provider 时为对应 tn_xxx;global provider 时为空 |
provider_id | text not null fk -> providers.id | 所属 provider |
model | text not null | 对外模型名 |
pricing | jsonb not null default '{}' | 定价结构,原样存储 |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
约束/索引:
unique (provider_id, model)index (tenant_id)index (provider_id)
pricing 当前约定为如下 JSON:
{
"basePricing": {
"textInput": 500,
"textOutput": 1500
},
"adjustments": [
{
"mode": "ADJUSTMENT_MODE_MULTIPLIER",
"when": {
"serviceTier": "priority"
},
"values": {
"textInput": 2,
"textOutput": 2
}
}
]
}
- 固定只支持 millionTokens 口径,因此不再写
unit - 所有数字都表示每 1,000,000 tokens 的 credit
pricing使用aidy.v2.ext.billing.Pricing的 canonical protobuf JSONbasePricing/adjustments.values只支持文本计费四项:textInput、textOutput、textInputCacheRead、textInputCacheWrite- 当前运行时结算只读取
basePricing,完全忽略adjustments textInput的运行时计费口径为max(input_tokens - cached_read_tokens - cached_creation_tokens, 0)textOutput直接使用output_tokenstextInputCacheRead直接使用cached_read_tokenstextInputCacheWrite直接使用cached_creation_tokens- 四项原始费用求和后统一做四舍五入,
0.5向上,得到最终整数Credit
2.7 consumers
tenant 下的调用方实体。
| 字段 | 类型 | 说明 |
|---|---|---|
id | text pk | 主键,格式 cs_<ulid> |
tenant_id | text not null fk -> tenants.id | 所属 tenant |
name | text not null | 名称 |
status | text not null | active / disabled |
unlimited_credit | boolean not null default false | 为 true 时表示不限制使用额度 |
remaining_credit | bigint not null default 0 | 当前剩余 Credit |
used_credit | bigint not null default 0 | 历史累计总用量 |
metadata | jsonb not null default '{}' | 扩展字段 |
groups | text[] not null default '{}' | 可用组约束;空数组表示不限组 |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
约束/索引:
index (tenant_id, status)index using gin (groups)
2.8 consumer_api_keys
API Key 明细表。
| 字段 | 类型 | 说明 |
|---|---|---|
id | text pk | 主键,格式 cak_<ulid> |
tenant_id | text not null fk -> tenants.id | 所属 tenant |
consumer_id | text not null fk -> consumers.id | 所属 consumer |
name | text not null | 自定义名称 |
key | text not null | 完整 API key,运行时直接按该值查找 |
disabled_at | timestamptz null | 置非空后,该 key 立即视为禁用 |
expires_at | timestamptz null | 过期时间 |
revoked_at | timestamptz null | 吊销时间 |
unlimited_credit | boolean not null default false | 为 true 时表示不限制使用额度 |
remaining_credit | bigint not null default 0 | 当前剩余 Credit |
used_credit | bigint not null default 0 | 历史累计总用量 |
groups | text[] not null default '{}' | 可用组约束;空数组表示不限组 |
last_used_at | timestamptz null | 最近使用时间 |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
索引:
unique (key)unique (consumer_id, name)index (tenant_id)index (consumer_id)index using gin (groups)index (disabled_at)index (expires_at)index (revoked_at)
2.9 plugin_configs
| 字段 | 类型 | 说明 |
|---|---|---|
tenant_id | text not null | 所属 tenant |
route_id | text not null | 所属 route |
plugin_key | text not null | 插件主键,如 guard / inbound_rate_limit |
plugin_sub_key | text not null default '' | 插件子键;空字符串表示无子键 |
config | jsonb not null default '{}' | 当前插件配置 JSON |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
约束/索引:
primary key (route_id, plugin_key, plugin_sub_key)index (tenant_id)index (route_id)index (route_id, plugin_key)
2.10 request_logs
请求日志摘要表。
该表只存摘要,不存 request / response body。
route_labels也不落库,只保留在事件流里。
可选的raw列只在显式开启plugins.log.store_full_body_in_pg时保存完整 protojson。
| 字段 | 类型 | 说明 |
|---|---|---|
id | text pk | 主键,格式 rql_<ulid> |
tenant_id | text not null default '' | 所属 tenant;未知 route / 无 tenant 时为空字符串 |
route_id | text not null | route id |
route_name | text not null | route name |
request_id | text not null | 请求 ID |
requested_model | text null | 客户端请求体中的原始 model 字段 |
remote_addr | text not null default '' | 请求来源地址 |
request | jsonb not null default '{}' | 入口请求摘要,仅含 url/path/method/header |
response | jsonb not null default '{}' | 出口响应摘要,仅含 code/header |
upstream_requests | jsonb not null default '[]' | 上游尝试数组;每项包含 request、response、meta;其中 request.model 为真实出站模型 |
timing | jsonb not null default '{}' | 时间点信息 |
duration | jsonb not null default '{}' | 耗时信息 |
error | text null | 内部错误字符串 |
ext_fields | jsonb not null default '{}' | 扩展字段;当前已知字段定义见 日志扩展字段 |
raw | jsonb null | 可选完整日志 protojson;仅在 plugins.log.store_full_body_in_pg = true 时写入 |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
说明:
upstream_requests按真实尝试顺序保存多个上游 request/response 对requested_model保留客户端原始请求模型;真实发往上游的模型记录在upstream_requests[*].request.modelupstream_requests[*].meta在数据库中仍以 JSON 对象存储;aidy.v2.logging.v1的 proto 契约已使用强类型UpstreamRequestMetameta当前至少包括:attempt_index、upstream_id、upstream_name、upstream_api_key_id、provider_protocol、final、errorraw为null表示未开启完整日志落库ext_fields的当前已知字段说明统一收口在 日志扩展字段ext_fields.billing为请求级后结算结果,当前最小结构为:
{
"status": "settled",
"consumer_id": "cs_xxx",
"consumer_api_key_id": "cak_xxx",
"charged_credit": 12,
"ledger_entry_ids": [
"cle_xxx",
"cle_yyy"
],
"error": null
}
- 成功时
status='settled',charged_credit为本次最终扣费整数值 - 失败时
status='settle_failed',charged_credit=0,error必填 - 若 consumer 与 API key 同时参与扣费,它们本次扣减金额默认相同,因此只保留一个
charged_credit request_logs现在同时承担“请求日志摘要 + 请求级 billing 事实”角色,因此(tenant_id, request_id)必须唯一
索引:
index (tenant_id, created_at)index (route_id, created_at)unique (tenant_id, request_id)index (requested_model)
2.11 credit_ledger_entries
Credit 不可变账本表。
consumers.remaining_credit/used_credit与consumer_api_keys.remaining_credit/used_credit只是余额快照。
真正用于审计、追溯与请求级去重的是credit_ledger_entries。
| 字段 | 类型 | 说明 |
|---|---|---|
id | text pk | 主键,格式 cle_<ulid> |
tenant_id | text not null | 所属 tenant |
subject_type | text not null | consumer / consumer_api_key |
subject_id | text not null | 计费主体 ID |
consumer_id | text not null | 对应 consumer |
consumer_api_key_id | text null | 对应 API key;consumer 级账项可为空 |
request_id | text null | 请求级幂等键;仅请求结算类账项使用 |
request_log_id | text null | 对应请求日志;当前可为空 |
entry_type | text not null | settle / admin_adjustment / correction |
amount_delta | bigint not null | 有符号 Credit 变化量;扣费为负,加额为正 |
balance_after | bigint null | 该账项落地后的 remaining_credit 快照 |
used_after | bigint null | 该账项落地后的 used_credit 快照 |
metadata | jsonb not null default '{}' | 扩展字段 |
created_at | timestamptz not null | 创建时间 |
updated_at | timestamptz not null | 更新时间 |
约束/索引:
check (subject_type in ('consumer', 'consumer_api_key'))check (entry_type in ('settle', 'admin_adjustment', 'correction'))index (tenant_id, subject_type, subject_id, created_at)index (tenant_id, request_id)index (request_log_id)unique (tenant_id, request_id, subject_type, entry_type) where request_id is not null
说明:
- 请求后结算时:
- consumer 扣费写
entry_type='settle'、subject_type='consumer' - API key 也参与扣费时,再写一条
entry_type='settle'、subject_type='consumer_api_key'
- consumer 扣费写
- Consumer 余额通过管理 API 调整时,必须写
entry_type='admin_adjustment' - API Key 余额通过管理 API 调整时,当前不写账本,这是有意保留的不对称行为