# 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 必须在低峰期执行 --- *文档结束*