Files
ai-paas-comparison/docs/03-数据库设计.md
T
qyyfw d9a8a0e93c docs: 补全基础设施文档 (DB设计/API规范/MiMoCode配置)
- 03-数据库设计.md: ER图 + 12张表定义 + Redis数据结构 + Migration策略
- 04-API接口规范.md: OpenAPI 3.0 约定 + 全部接口定义 (Auth/APIKey/LLM/Billing/Registry/Skills/IM/Admin)
- 05-AGENTS-MiMoCode配置.md: MiMo Code 核心配置文件,包含项目结构/技术栈/开发规范/模块说明

至此 docs/ 目录包含完整的6份基础设计文档,可作为 MiMo Code 的上下文输入
2026-06-27 10:51:13 +00:00

444 lines
17 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.
# AI PaaS 商业平台 — 数据库设计
> 版本: 1.0 | 日期: 2026-06-27
> 数据库: PostgreSQL 16+ | 缓存: Redis 7+
---
## 1. ER 关系图
```
tenants ──1:N──→ users
tenants ──1:N──→ capabilities
tenants ──1:N──→ subscriptions
tenants ──1:N──→ credit_transactions
tenants ──1:N──→ token_usage
tenants ──1:N──→ skills
tenants ──1:N──→ im_sessions
users ──1:N──→ api_keys
users ──1:N──→ skill_purchases
capabilities ──1:N──→ capability_versions
skills ──1:N──→ skill_purchases
im_sessions ──1:N──→ im_messages
```
---
## 2. 表定义
### 2.1 tenants(租户)
```sql
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE, -- URL 友好标识
plan VARCHAR(20) NOT NULL DEFAULT 'free', -- free/pro/team/enterprise
credit_balance BIGINT NOT NULL DEFAULT 0, -- 毫 Credit (1 Credit = 1000)
credit_included BIGINT NOT NULL DEFAULT 100000, -- 月包含 Credit (毫)
credit_used BIGINT NOT NULL DEFAULT 0, -- 本月已用 (毫)
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/suspended/deleted
settings JSONB DEFAULT '{}', -- 租户配置
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_tenants_slug ON tenants(slug);
CREATE INDEX idx_tenants_status ON tenants(status);
-- RLS
ALTER TABLE tenants ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON tenants
USING (id = current_setting('app.tenant_id')::uuid);
```
### 2.2 users(用户)
```sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255), -- Keycloak 管理时可为空
name VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'user', -- owner/admin/developer/user
avatar_url VARCHAR(500),
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/suspended/deleted
last_login_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(tenant_id, email)
);
CREATE INDEX idx_users_tenant ON users(tenant_id);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON users
USING (tenant_id = current_setting('app.tenant_id')::uuid);
```
### 2.3 api_keys(API 密钥)
```sql
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL, -- 用途标识
key_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 哈希
key_prefix VARCHAR(10) NOT NULL, -- 前缀 aip_xxxx
scopes TEXT[] DEFAULT '{}', -- 权限范围
last_used_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE, -- 可选过期时间
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/revoked
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_api_keys_tenant ON api_keys(tenant_id);
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash);
CREATE INDEX idx_api_keys_user ON api_keys(user_id);
ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON api_keys
USING (tenant_id = current_setting('app.tenant_id')::uuid);
```
### 2.4 subscriptions(订阅)
```sql
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
plan VARCHAR(20) NOT NULL, -- free/pro/team/enterprise
credits_included BIGINT NOT NULL, -- 月包含 Credit (毫)
credits_used BIGINT NOT NULL DEFAULT 0, -- 本月已用
price_cents INTEGER NOT NULL DEFAULT 0, -- 月费 (分)
billing_cycle VARCHAR(10) NOT NULL DEFAULT 'monthly', -- monthly/yearly
period_start TIMESTAMP WITH TIME ZONE NOT NULL,
period_end TIMESTAMP WITH TIME ZONE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/cancelled/expired
stripe_sub_id VARCHAR(255), -- Stripe 订阅 ID
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_subscriptions_tenant ON subscriptions(tenant_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
CREATE INDEX idx_subscriptions_period ON subscriptions(period_start, period_end);
ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON subscriptions
USING (tenant_id = current_setting('app.tenant_id')::uuid);
```
### 2.5 credit_transactions(Credit 流水 — 不可变)
```sql
CREATE TABLE credit_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
type VARCHAR(30) NOT NULL, -- usage/topup/subscription/bonus/refund
amount BIGINT NOT NULL, -- 变动量 (毫 Credit),正=加,负=减
balance_after BIGINT NOT NULL, -- 变动后余额
description TEXT, -- 描述
trace_id VARCHAR(64), -- 关联的调用 trace
idempotency_key VARCHAR(64) UNIQUE, -- 幂等键 (防重复扣费)
reference_type VARCHAR(30), -- llm_call/skill_purchase/topup/subscription
reference_id UUID, -- 关联 ID
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
-- 不可变: 无 updated_at, 无 DELETE
);
CREATE INDEX idx_credit_tx_tenant ON credit_transactions(tenant_id);
CREATE INDEX idx_credit_tx_type ON credit_transactions(type);
CREATE INDEX idx_credit_tx_created ON credit_transactions(created_at);
CREATE INDEX idx_credit_tx_trace ON credit_transactions(trace_id);
-- 不可变表: 禁用 UPDATE 和 DELETE
CREATE RULE credit_tx_no_update AS ON UPDATE TO credit_transactions DO INSTEAD NOTHING;
CREATE RULE credit_tx_no_delete AS ON DELETE TO credit_transactions DO INSTEAD NOTHING;
ALTER TABLE credit_transactions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON credit_transactions
USING (tenant_id = current_setting('app.tenant_id')::uuid);
```
### 2.6 token_usage(Token 用量 — 按月分区)
```sql
-- 主表 (自动分区)
CREATE TABLE token_usage (
id UUID NOT NULL DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
capability_id UUID,
user_id UUID,
model VARCHAR(100) NOT NULL, -- deepseek-chat, qwen-turbo
provider VARCHAR(50) NOT NULL, -- deepseek, qianwen, doubao
prompt_tokens INTEGER NOT NULL,
completion_tokens INTEGER NOT NULL,
total_tokens INTEGER NOT NULL,
cost_credits BIGINT NOT NULL, -- 消耗 Credit (毫)
trace_id VARCHAR(64),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- 按月创建分区
CREATE TABLE token_usage_2026_07 PARTITION OF token_usage
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
CREATE TABLE token_usage_2026_08 PARTITION OF token_usage
FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
-- ... 自动创建未来分区
CREATE INDEX idx_token_usage_tenant ON token_usage(tenant_id, created_at);
CREATE INDEX idx_token_usage_model ON token_usage(model, created_at);
CREATE INDEX idx_token_usage_trace ON token_usage(trace_id);
-- 不可变表
CREATE RULE token_usage_no_update AS ON UPDATE TO token_usage DO INSTEAD NOTHING;
CREATE RULE token_usage_no_delete AS ON DELETE TO token_usage DO INSTEAD NOTHING;
```
### 2.7 capabilities(能力)
```sql
CREATE TABLE capabilities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
type VARCHAR(30) NOT NULL, -- agent/mcp/skill/llm/custom
version VARCHAR(50) NOT NULL, -- SemVer
description TEXT,
tags TEXT[] DEFAULT '{}',
endpoint VARCHAR(500), -- 调用地址
protocol VARCHAR(20) DEFAULT 'rest', -- rest/grpc/websocket
pricing_config JSONB DEFAULT '{}', -- 计费配置
dependencies JSONB DEFAULT '[]', -- 依赖的其他能力
owner_id UUID REFERENCES users(id),
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- draft/pending_review/active/deprecated/rejected
review_note TEXT, -- 审核备注
view_count INTEGER DEFAULT 0, -- 浏览次数
invoke_count BIGINT DEFAULT 0, -- 调用次数
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(tenant_id, name, version)
);
CREATE INDEX idx_capabilities_tenant ON capabilities(tenant_id);
CREATE INDEX idx_capabilities_type ON capabilities(type);
CREATE INDEX idx_capabilities_status ON capabilities(status);
CREATE INDEX idx_capabilities_tags ON capabilities USING GIN(tags);
ALTER TABLE capabilities ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON capabilities
USING (tenant_id = current_setting('app.tenant_id')::uuid);
```
### 2.8 capability_versions(能力版本)
```sql
CREATE TABLE capability_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
capability_id UUID NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
version VARCHAR(50) NOT NULL,
endpoint VARCHAR(500),
changelog TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/deprecated
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(capability_id, version)
);
CREATE INDEX idx_cap_versions_cap ON capability_versions(capability_id);
```
### 2.9 skills(技能)
```sql
CREATE TABLE skills (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
category VARCHAR(50), -- 分类
tags TEXT[] DEFAULT '{}',
pricing_model VARCHAR(20) NOT NULL DEFAULT 'per_call', -- per_call/per_token/subscription/free
price_credits BIGINT NOT NULL DEFAULT 0, -- 价格 (毫 Credit)
developer_share NUMERIC(3,2) DEFAULT 0.75, -- 开发者分成比例
endpoint VARCHAR(500), -- API 地址
documentation TEXT, -- 使用文档
icon_url VARCHAR(500),
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- draft/active/suspended/deleted
rating NUMERIC(3,2) DEFAULT 0, -- 评分
rating_count INTEGER DEFAULT 0, -- 评分人数
view_count INTEGER DEFAULT 0, -- 浏览次数
purchase_count INTEGER DEFAULT 0, -- 购买次数
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_skills_tenant ON skills(tenant_id);
CREATE INDEX idx_skills_category ON skills(category);
CREATE INDEX idx_skills_status ON skills(status);
CREATE INDEX idx_skills_tags ON skills USING GIN(tags);
CREATE INDEX idx_skills_rating ON skills(rating DESC);
CREATE INDEX idx_skills_name_search ON skills USING GIN(to_tsvector('simple', name || ' ' || COALESCE(description, '')));
ALTER TABLE skills ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON skills
USING (tenant_id = current_setting('app.tenant_id')::uuid);
```
### 2.10 skill_purchases(技能购买)
```sql
CREATE TABLE skill_purchases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
user_id UUID NOT NULL REFERENCES users(id),
skill_id UUID NOT NULL REFERENCES skills(id),
developer_id UUID NOT NULL, -- 开发者 user_id
amount_credits BIGINT NOT NULL, -- 支付金额 (毫 Credit)
platform_share BIGINT NOT NULL, -- 平台分成
developer_share BIGINT NOT NULL, -- 开发者分成
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/refunded
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_skill_purchases_user ON skill_purchases(user_id);
CREATE INDEX idx_skill_purchases_skill ON skill_purchases(skill_id);
CREATE INDEX idx_skill_purchases_tenant ON skill_purchases(tenant_id);
```
### 2.11 im_sessions(IM 会话)
```sql
CREATE TABLE im_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
platform VARCHAR(20) NOT NULL, -- wechat/qq/dingtalk/feishu
user_identifier VARCHAR(255) NOT NULL, -- 平台用户标识
user_name VARCHAR(255), -- 平台用户名
bot_id VARCHAR(255), -- 机器人标识
model VARCHAR(100) DEFAULT 'deepseek-chat', -- 当前使用模型
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/closed
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_active_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_im_sessions_tenant ON im_sessions(tenant_id);
CREATE INDEX idx_im_sessions_platform ON im_sessions(platform, user_identifier);
ALTER TABLE im_sessions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON im_sessions
USING (tenant_id = current_setting('app.tenant_id')::uuid);
```
### 2.12 im_messages(IM 消息)
```sql
CREATE TABLE im_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES im_sessions(id),
role VARCHAR(10) NOT NULL, -- user/assistant/system
content TEXT NOT NULL,
model VARCHAR(100), -- 使用的模型
tokens_used INTEGER,
cost_credits BIGINT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_im_messages_session ON im_messages(session_id, created_at);
```
---
## 3. Redis 数据结构
### 3.1 计费相关
```
# Credit 余额 (毫 Credit)
billing:balance:{tenant_id} → "12580000"
# 类型: String
# 操作: GET / DECRBY (Lua 原子)
# TTL: 永不过期
# 幂等键
billing:idempotency:{tenant_id}:{idempotency_key} → {
"result": "success",
"amount": -1280,
"trace_id": "abc-123"
}
# 类型: Hash
# TTL: 24 小时
# 每日统计
billing:daily:{tenant_id}:{YYYY-MM-DD} → {
"calls": "342",
"tokens": "456000",
"credits": "1280000"
}
# 类型: Hash
# TTL: 7 天
# 订阅信息
billing:subscription:{tenant_id} → {
"plan": "pro",
"credits_included": "50000000",
"credits_used": "12800000",
"period_end": "2026-07-27T00:00:00Z"
}
# 类型: Hash
# TTL: 永不过期
```
### 3.2 限流相关
```
# 滑动窗口限流
ratelimit:{tenant_id}:{window_key} → "1" (INCR)
# 类型: String
# TTL: 窗口大小 (如 60s)
```
### 3.3 IM 会话
```
# 会话上下文
im:session:{platform}:{user_id} → {
"tenant_id": "xxx",
"model": "deepseek-chat",
"messages": "[{role,content}, ...]" # 最近 20 条
}
# 类型: Hash
# TTL: 24 小时
```
---
## 4. Migration 策略
```
migrations/
├── 001_init.sql # 所有表 + RLS + 索引
├── 002_seed_data.sql # 开发环境种子数据
├── 003_add_partitions.sql # token_usage 分区
└── 004_add_fulltext.sql # 全文搜索索引
```
**规则**:
- Migration 文件名: `{序号}_{描述}.sql`
- 不允许修改已执行的 Migration
- 每个 Migration 必须可回滚
- 生产环境 Migration 必须在低峰期执行
---
*文档结束*