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

17 KiB
Raw Blame History

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(租户)

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(用户)

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 密钥)

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(订阅)

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 流水 — 不可变)

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 用量 — 按月分区)

-- 主表 (自动分区)
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(能力)

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(能力版本)

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(技能)

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(技能购买)

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 会话)

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 消息)

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 必须在低峰期执行

文档结束