Phase 8 Task 1: 多租户 SaaS 架构

- 创建 tenant_manager.py 多租户管理模块
  - 租户管理(CRUD、slug、状态管理)
  - 自定义域名绑定(DNS/文件验证)
  - 品牌白标(Logo、主题色、自定义 CSS/JS)
  - 成员管理(邀请、角色、权限)
  - 资源使用统计和限制检查
  - 租户上下文管理器

- 更新 schema.sql 添加租户相关表
  - tenants, tenant_domains, tenant_branding
  - tenant_members, tenant_permissions, tenant_usage

- 更新 main.py 添加租户 API 端点
  - /api/v1/tenants/* 租户管理
  - /api/v1/tenants/{id}/domains 域名管理
  - /api/v1/tenants/{id}/branding 品牌配置
  - /api/v1/tenants/{id}/members 成员管理
  - /api/v1/tenants/{id}/usage 使用统计
  - /api/v1/resolve-tenant 域名解析

- 创建 test_phase8_task1.py 测试脚本
This commit is contained in:
OpenClaw Bot
2026-02-25 12:12:50 +08:00
parent 1e74d94e11
commit e3d7794ae7
14 changed files with 6421 additions and 10 deletions

View File

@@ -433,7 +433,106 @@ CREATE INDEX IF NOT EXISTS idx_webdav_syncs_project ON webdav_syncs(project_id);
CREATE INDEX IF NOT EXISTS idx_chrome_tokens_project ON chrome_extension_tokens(project_id);
CREATE INDEX IF NOT EXISTS idx_chrome_tokens_hash ON chrome_extension_tokens(token_hash);
-- ============================================
-- Phase 7 Task 6: 高级搜索与发现
-- ============================================
-- 搜索索引表
CREATE TABLE IF NOT EXISTS search_indexes (
id TEXT PRIMARY KEY,
content_id TEXT NOT NULL,
content_type TEXT NOT NULL, -- transcript, entity, relation
project_id TEXT NOT NULL,
tokens TEXT, -- JSON 数组
token_positions TEXT, -- JSON 对象
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(content_id, content_type)
);
-- 搜索词频统计表
CREATE TABLE IF NOT EXISTS search_term_freq (
term TEXT NOT NULL,
content_id TEXT NOT NULL,
content_type TEXT NOT NULL,
project_id TEXT NOT NULL,
frequency INTEGER DEFAULT 1,
positions TEXT, -- JSON 数组
PRIMARY KEY (term, content_id, content_type)
);
-- 文本 Embedding 表
CREATE TABLE IF NOT EXISTS embeddings (
id TEXT PRIMARY KEY,
content_id TEXT NOT NULL,
content_type TEXT NOT NULL,
project_id TEXT NOT NULL,
embedding TEXT, -- JSON 数组
model_name TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(content_id, content_type)
);
-- 搜索相关索引
CREATE INDEX IF NOT EXISTS idx_search_content ON search_indexes(content_id, content_type);
CREATE INDEX IF NOT EXISTS idx_search_project ON search_indexes(project_id);
CREATE INDEX IF NOT EXISTS idx_term_freq_term ON search_term_freq(term);
CREATE INDEX IF NOT EXISTS idx_term_freq_project ON search_term_freq(project_id);
CREATE INDEX IF NOT EXISTS idx_embedding_content ON embeddings(content_id, content_type);
CREATE INDEX IF NOT EXISTS idx_embedding_project ON embeddings(project_id);
-- ============================================
-- Phase 7 Task 8: 性能优化与扩展
-- ============================================
-- 缓存统计表
CREATE TABLE IF NOT EXISTS cache_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
total_requests INTEGER DEFAULT 0,
hits INTEGER DEFAULT 0,
misses INTEGER DEFAULT 0,
hit_rate REAL DEFAULT 0.0,
memory_usage INTEGER DEFAULT 0
);
-- 任务队列表
CREATE TABLE IF NOT EXISTS task_queue (
id TEXT PRIMARY KEY,
task_type TEXT NOT NULL,
status TEXT DEFAULT 'pending', -- pending, running, success, failed, retrying, cancelled
payload TEXT, -- JSON
result TEXT, -- JSON
error_message TEXT,
retry_count INTEGER DEFAULT 0,
max_retries INTEGER DEFAULT 3,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
started_at TIMESTAMP,
completed_at TIMESTAMP
);
-- 性能指标表
CREATE TABLE IF NOT EXISTS performance_metrics (
id TEXT PRIMARY KEY,
metric_type TEXT NOT NULL, -- api_response, db_query, cache_operation
endpoint TEXT,
duration_ms REAL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
metadata TEXT -- JSON
);
-- 性能相关索引
CREATE INDEX IF NOT EXISTS idx_cache_stats_time ON cache_stats(timestamp);
CREATE INDEX IF NOT EXISTS idx_task_status ON task_queue(status);
CREATE INDEX IF NOT EXISTS idx_task_type ON task_queue(task_type);
CREATE INDEX IF NOT EXISTS idx_task_created ON task_queue(created_at);
CREATE INDEX IF NOT EXISTS idx_metrics_type ON performance_metrics(metric_type);
CREATE INDEX IF NOT EXISTS idx_metrics_endpoint ON performance_metrics(endpoint);
CREATE INDEX IF NOT EXISTS idx_metrics_time ON performance_metrics(timestamp);
-- ============================================
-- Phase 7: 插件与集成相关表
-- ============================================
-- 插件表
CREATE TABLE IF NOT EXISTS plugins (
@@ -845,3 +944,241 @@ CREATE INDEX IF NOT EXISTS idx_metrics_endpoint ON performance_metrics(endpoint)
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON performance_metrics(timestamp);
CREATE INDEX IF NOT EXISTS idx_shard_mappings_project ON shard_mappings(project_id);
CREATE INDEX IF NOT EXISTS idx_shard_mappings_shard ON shard_mappings(shard_id);
-- ============================================
-- Phase 8 Task 1: 多租户 SaaS 架构
-- ============================================
-- 租户主表
CREATE TABLE IF NOT EXISTS tenants (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
description TEXT,
tier TEXT DEFAULT 'free',
status TEXT DEFAULT 'pending',
owner_id TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
settings TEXT DEFAULT '{}',
resource_limits TEXT DEFAULT '{}',
metadata TEXT DEFAULT '{}'
);
-- 租户域名表
CREATE TABLE IF NOT EXISTS tenant_domains (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
domain TEXT UNIQUE NOT NULL,
status TEXT DEFAULT 'pending',
verification_token TEXT NOT NULL,
verification_method TEXT DEFAULT 'dns',
verified_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_primary INTEGER DEFAULT 0,
ssl_enabled INTEGER DEFAULT 0,
ssl_expires_at TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- 租户品牌配置表
CREATE TABLE IF NOT EXISTS tenant_branding (
id TEXT PRIMARY KEY,
tenant_id TEXT UNIQUE NOT NULL,
logo_url TEXT,
favicon_url TEXT,
primary_color TEXT,
secondary_color TEXT,
custom_css TEXT,
custom_js TEXT,
login_page_bg TEXT,
email_template TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- 租户成员表
CREATE TABLE IF NOT EXISTS tenant_members (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
user_id TEXT, -- NULL for pending invitations
email TEXT NOT NULL,
role TEXT DEFAULT 'member',
permissions TEXT DEFAULT '[]',
invited_by TEXT,
invited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
joined_at TIMESTAMP,
last_active_at TIMESTAMP,
status TEXT DEFAULT 'pending',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- 租户权限定义表
CREATE TABLE IF NOT EXISTS tenant_permissions (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
name TEXT NOT NULL,
code TEXT NOT NULL,
description TEXT,
resource_type TEXT NOT NULL,
actions TEXT NOT NULL,
conditions TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
UNIQUE(tenant_id, code)
);
-- 租户资源使用统计表
CREATE TABLE IF NOT EXISTS tenant_usage (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
date DATE NOT NULL,
storage_bytes INTEGER DEFAULT 0,
transcription_seconds INTEGER DEFAULT 0,
api_calls INTEGER DEFAULT 0,
projects_count INTEGER DEFAULT 0,
entities_count INTEGER DEFAULT 0,
members_count INTEGER DEFAULT 0,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
UNIQUE(tenant_id, date)
);
-- 租户相关索引
CREATE INDEX IF NOT EXISTS idx_tenants_slug ON tenants(slug);
CREATE INDEX IF NOT EXISTS idx_tenants_owner ON tenants(owner_id);
CREATE INDEX IF NOT EXISTS idx_tenants_status ON tenants(status);
CREATE INDEX IF NOT EXISTS idx_domains_tenant ON tenant_domains(tenant_id);
CREATE INDEX IF NOT EXISTS idx_domains_domain ON tenant_domains(domain);
CREATE INDEX IF NOT EXISTS idx_domains_status ON tenant_domains(status);
CREATE INDEX IF NOT EXISTS idx_members_tenant ON tenant_members(tenant_id);
CREATE INDEX IF NOT EXISTS idx_members_user ON tenant_members(user_id);
CREATE INDEX IF NOT EXISTS idx_usage_tenant ON tenant_usage(tenant_id);
CREATE INDEX IF NOT EXISTS idx_usage_date ON tenant_usage(date);
-- ============================================
-- Phase 8: Multi-Tenant SaaS Architecture
-- ============================================
-- 租户主表
CREATE TABLE IF NOT EXISTS tenants (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL, -- URL 友好的唯一标识
description TEXT DEFAULT '',
status TEXT DEFAULT 'active', -- active, suspended, trial, expired, pending
plan TEXT DEFAULT 'free', -- free, starter, professional, enterprise
max_projects INTEGER DEFAULT 5,
max_members INTEGER DEFAULT 10,
max_storage_gb REAL DEFAULT 1.0,
max_api_calls_per_day INTEGER DEFAULT 1000,
billing_email TEXT DEFAULT '',
subscription_start TEXT,
subscription_end TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT '', -- 创建者用户ID
db_schema TEXT DEFAULT '', -- 数据库 schema 名称
table_prefix TEXT DEFAULT '' -- 表前缀
);
-- 租户域名绑定表
CREATE TABLE IF NOT EXISTS tenant_domains (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
domain TEXT NOT NULL, -- 自定义域名
status TEXT DEFAULT 'pending', -- pending, verified, active, failed, expired
verification_record TEXT DEFAULT '', -- DNS TXT 记录值
verification_expires_at TEXT,
ssl_enabled INTEGER DEFAULT 0,
ssl_cert_path TEXT,
ssl_key_path TEXT,
ssl_expires_at TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
verified_at TEXT,
UNIQUE(tenant_id, domain),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- 租户品牌配置表(白标)
CREATE TABLE IF NOT EXISTS tenant_branding (
id TEXT PRIMARY KEY,
tenant_id TEXT UNIQUE NOT NULL,
logo_url TEXT,
logo_dark_url TEXT, -- 深色模式 Logo
favicon_url TEXT,
primary_color TEXT DEFAULT '#3B82F6',
secondary_color TEXT DEFAULT '#10B981',
accent_color TEXT DEFAULT '#F59E0B',
background_color TEXT DEFAULT '#FFFFFF',
text_color TEXT DEFAULT '#1F2937',
dark_primary_color TEXT DEFAULT '#60A5FA',
dark_background_color TEXT DEFAULT '#111827',
dark_text_color TEXT DEFAULT '#F9FAFB',
font_family TEXT DEFAULT 'Inter, system-ui, sans-serif',
heading_font_family TEXT,
custom_css TEXT DEFAULT '',
custom_js TEXT DEFAULT '',
app_name TEXT DEFAULT 'InsightFlow',
login_page_title TEXT DEFAULT '登录到 InsightFlow',
login_page_description TEXT DEFAULT '',
footer_text TEXT DEFAULT '© 2024 InsightFlow',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- 租户成员表
CREATE TABLE IF NOT EXISTS tenant_members (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
user_id TEXT NOT NULL,
email TEXT NOT NULL,
name TEXT DEFAULT '',
role TEXT DEFAULT 'viewer', -- owner, admin, editor, viewer, guest
status TEXT DEFAULT 'invited', -- active, invited, suspended, removed
invited_by TEXT,
invited_at TEXT,
invitation_token TEXT,
invitation_expires_at TEXT,
joined_at TEXT,
last_active_at TEXT,
custom_permissions TEXT DEFAULT '[]', -- JSON 数组
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(tenant_id, user_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- 租户角色表
CREATE TABLE IF NOT EXISTS tenant_roles (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT DEFAULT '',
permissions TEXT DEFAULT '[]', -- JSON 数组
is_system INTEGER DEFAULT 0, -- 1=系统预设, 0=自定义
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- 租户相关索引
CREATE INDEX IF NOT EXISTS idx_tenants_slug ON tenants(slug);
CREATE INDEX IF NOT EXISTS idx_tenants_status ON tenants(status);
CREATE INDEX IF NOT EXISTS idx_domains_tenant ON tenant_domains(tenant_id);
CREATE INDEX IF NOT EXISTS idx_domains_domain ON tenant_domains(domain);
CREATE INDEX IF NOT EXISTS idx_domains_status ON tenant_domains(status);
CREATE INDEX IF NOT EXISTS idx_members_tenant ON tenant_members(tenant_id);
CREATE INDEX IF NOT EXISTS idx_members_user ON tenant_members(user_id);
CREATE INDEX IF NOT EXISTS idx_members_role ON tenant_members(role);
CREATE INDEX IF NOT EXISTS idx_members_status ON tenant_members(status);
CREATE INDEX IF NOT EXISTS idx_members_token ON tenant_members(invitation_token);
CREATE INDEX IF NOT EXISTS idx_roles_tenant ON tenant_roles(tenant_id);
-- 更新项目表,添加租户关联(可选,支持租户隔离)
ALTER TABLE projects ADD COLUMN tenant_id TEXT;
CREATE INDEX IF NOT EXISTS idx_projects_tenant ON projects(tenant_id);