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

507
backend/test_tenant.py Normal file
View File

@@ -0,0 +1,507 @@
#!/usr/bin/env python3
"""
InsightFlow Phase 8 - Multi-Tenant SaaS Test Script
多租户 SaaS 架构测试脚本
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from tenant_manager import (
get_tenant_manager, TenantManager, Tenant, TenantDomain, TenantBranding,
TenantMember, TenantStatus, TenantTier, TenantRole, DomainStatus,
TenantContext
)
def test_tenant_management():
"""测试租户管理功能"""
print("=" * 60)
print("测试租户管理功能")
print("=" * 60)
# 使用测试数据库
test_db = "test_tenant.db"
if os.path.exists(test_db):
os.remove(test_db)
manager = get_tenant_manager(test_db)
# 1. 创建租户
print("\n1. 创建租户...")
try:
tenant = manager.create_tenant(
name="Test Company",
owner_id="user_001",
tier="pro",
description="A test tenant for validation",
settings={"theme": "dark"}
)
print(f" ✓ 租户创建成功: {tenant.id}")
print(f" - 名称: {tenant.name}")
print(f" - Slug: {tenant.slug}")
print(f" - 层级: {tenant.tier}")
print(f" - 状态: {tenant.status}")
print(f" - 资源限制: {tenant.resource_limits}")
except Exception as e:
print(f" ✗ 租户创建失败: {e}")
import traceback
traceback.print_exc()
return False
# 2. 获取租户
print("\n2. 获取租户...")
try:
fetched = manager.get_tenant(tenant.id)
assert fetched is not None
assert fetched.name == tenant.name
print(f" ✓ 通过 ID 获取租户成功")
fetched_by_slug = manager.get_tenant_by_slug(tenant.slug)
assert fetched_by_slug is not None
assert fetched_by_slug.id == tenant.id
print(f" ✓ 通过 Slug 获取租户成功")
except Exception as e:
print(f" ✗ 获取租户失败: {e}")
import traceback
traceback.print_exc()
return False
# 3. 更新租户
print("\n3. 更新租户...")
try:
updated = manager.update_tenant(
tenant.id,
name="Test Company Updated",
tier="enterprise"
)
assert updated is not None
assert updated.name == "Test Company Updated"
assert updated.tier == "enterprise"
print(f" ✓ 租户更新成功")
print(f" - 新名称: {updated.name}")
print(f" - 新层级: {updated.tier}")
except Exception as e:
print(f" ✗ 租户更新失败: {e}")
import traceback
traceback.print_exc()
return False
# 4. 列出租户
print("\n4. 列出租户...")
try:
tenants = manager.list_tenants()
assert len(tenants) >= 1
print(f" ✓ 列出租户成功,共 {len(tenants)} 个租户")
except Exception as e:
print(f" ✗ 列出租户失败: {e}")
return False
return tenant.id
def test_domain_management(tenant_id: str):
"""测试域名管理功能"""
print("\n" + "=" * 60)
print("测试域名管理功能")
print("=" * 60)
manager = get_tenant_manager("test_tenant.db")
# 1. 添加域名
print("\n1. 添加自定义域名...")
try:
domain = manager.add_domain(tenant_id, "app.example.com", is_primary=True)
print(f" ✓ 域名添加成功: {domain.id}")
print(f" - 域名: {domain.domain}")
print(f" - 状态: {domain.status}")
print(f" - 验证令牌: {domain.verification_token}")
print(f" - 是否主域名: {domain.is_primary}")
except Exception as e:
print(f" ✗ 域名添加失败: {e}")
import traceback
traceback.print_exc()
return False
# 2. 获取域名验证指导
print("\n2. 获取域名验证指导...")
try:
instructions = manager.get_domain_verification_instructions(domain.id)
assert instructions is not None
print(f" ✓ 获取验证指导成功")
print(f" - DNS 记录: {instructions['dns_record']}")
except Exception as e:
print(f" ✗ 获取验证指导失败: {e}")
return False
# 3. 验证域名
print("\n3. 验证域名...")
try:
success = manager.verify_domain(tenant_id, domain.id)
if success:
print(f" ✓ 域名验证成功")
else:
print(f" ! 域名验证返回 False可能是模拟验证")
except Exception as e:
print(f" ✗ 域名验证失败: {e}")
return False
# 4. 获取域名列表
print("\n4. 获取域名列表...")
try:
domains = manager.list_domains(tenant_id)
assert len(domains) >= 1
print(f" ✓ 获取域名列表成功,共 {len(domains)} 个域名")
for d in domains:
print(f" - {d.domain} ({d.status})")
except Exception as e:
print(f" ✗ 获取域名列表失败: {e}")
return False
# 5. 通过域名获取租户
print("\n5. 通过域名解析租户...")
try:
resolved = manager.get_tenant_by_domain("app.example.com")
if resolved:
assert resolved.id == tenant_id
print(f" ✓ 域名解析租户成功")
else:
print(f" ! 域名解析租户返回 None可能域名未激活")
except Exception as e:
print(f" ✗ 域名解析失败: {e}")
return False
return True
def test_branding_management(tenant_id: str):
"""测试品牌配置管理功能"""
print("\n" + "=" * 60)
print("测试品牌配置管理功能")
print("=" * 60)
manager = get_tenant_manager("test_tenant.db")
# 1. 更新品牌配置
print("\n1. 更新品牌配置...")
try:
branding = manager.update_branding(
tenant_id,
logo_url="https://example.com/logo.png",
favicon_url="https://example.com/favicon.ico",
primary_color="#FF5733",
secondary_color="#33FF57",
custom_css="body { font-size: 14px; }",
custom_js="console.log('Custom JS loaded');"
)
assert branding is not None
print(f" ✓ 品牌配置更新成功")
print(f" - Logo: {branding.logo_url}")
print(f" - 主色调: {branding.primary_color}")
print(f" - 次色调: {branding.secondary_color}")
except Exception as e:
print(f" ✗ 品牌配置更新失败: {e}")
import traceback
traceback.print_exc()
return False
# 2. 获取品牌配置
print("\n2. 获取品牌配置...")
try:
fetched = manager.get_branding(tenant_id)
assert fetched is not None
assert fetched.primary_color == "#FF5733"
print(f" ✓ 获取品牌配置成功")
except Exception as e:
print(f" ✗ 获取品牌配置失败: {e}")
return False
# 3. 生成品牌 CSS
print("\n3. 生成品牌 CSS...")
try:
css = manager.get_branding_css(tenant_id)
assert "--tenant-primary" in css
assert "#FF5733" in css
print(f" ✓ 品牌 CSS 生成成功")
print(f" - CSS 长度: {len(css)} 字符")
except Exception as e:
print(f" ✗ 品牌 CSS 生成失败: {e}")
return False
return True
def test_member_management(tenant_id: str):
"""测试成员管理功能"""
print("\n" + "=" * 60)
print("测试成员管理功能")
print("=" * 60)
manager = get_tenant_manager("test_tenant.db")
# 1. 邀请成员
print("\n1. 邀请成员...")
try:
member = manager.invite_member(
tenant_id=tenant_id,
email="user@example.com",
role="admin",
invited_by="user_001"
)
print(f" ✓ 成员邀请成功: {member.id}")
print(f" - 邮箱: {member.email}")
print(f" - 角色: {member.role}")
print(f" - 状态: {member.status}")
print(f" - 权限: {member.permissions}")
except Exception as e:
print(f" ✗ 成员邀请失败: {e}")
import traceback
traceback.print_exc()
return False
# 2. 获取成员列表
print("\n2. 获取成员列表...")
try:
members = manager.list_members(tenant_id)
assert len(members) >= 2 # owner + invited member
print(f" ✓ 获取成员列表成功,共 {len(members)} 个成员")
for m in members:
print(f" - {m.email} ({m.role}, {m.status})")
except Exception as e:
print(f" ✗ 获取成员列表失败: {e}")
return False
# 3. 接受邀请
print("\n3. 接受邀请...")
try:
# 注意accept_invitation 使用的是 member id 而不是 token
# 修正:查看源码后发现它接受的是 invitation_id即 member id
accepted = manager.accept_invitation(member.id, "user_002")
if accepted:
print(f" ✓ 邀请接受成功")
else:
print(f" ! 邀请接受返回 False可能是状态不对")
except Exception as e:
print(f" ✗ 邀请接受失败: {e}")
import traceback
traceback.print_exc()
return False
# 4. 更新成员角色
print("\n4. 更新成员角色...")
try:
success = manager.update_member_role(
tenant_id=tenant_id,
member_id=member.id,
role="member"
)
if success:
print(f" ✓ 成员角色更新成功")
else:
print(f" ! 成员角色更新返回 False")
except Exception as e:
print(f" ✗ 成员角色更新失败: {e}")
import traceback
traceback.print_exc()
return False
# 5. 检查权限
print("\n5. 检查用户权限...")
try:
# 检查 owner 权限
has_permission = manager.check_permission(
tenant_id=tenant_id,
user_id="user_001",
resource="project",
action="create"
)
print(f" ✓ 权限检查成功")
print(f" - Owner 是否有 project:create 权限: {has_permission}")
except Exception as e:
print(f" ✗ 权限检查失败: {e}")
return False
# 6. 获取用户租户列表
print("\n6. 获取用户租户列表...")
try:
user_tenants = manager.get_user_tenants("user_001")
assert len(user_tenants) >= 1
print(f" ✓ 获取用户租户列表成功,共 {len(user_tenants)} 个租户")
except Exception as e:
print(f" ✗ 获取用户租户列表失败: {e}")
return False
return True
def test_usage_stats(tenant_id: str):
"""测试使用统计功能"""
print("\n" + "=" * 60)
print("测试使用统计功能")
print("=" * 60)
manager = get_tenant_manager("test_tenant.db")
# 1. 记录使用
print("\n1. 记录资源使用...")
try:
manager.record_usage(
tenant_id=tenant_id,
storage_bytes=1024 * 1024 * 50, # 50MB
transcription_seconds=600, # 10分钟
api_calls=100,
projects_count=5,
entities_count=50,
members_count=3
)
print(f" ✓ 资源使用记录成功")
except Exception as e:
print(f" ✗ 资源使用记录失败: {e}")
import traceback
traceback.print_exc()
return False
# 2. 获取使用统计
print("\n2. 获取使用统计...")
try:
stats = manager.get_usage_stats(tenant_id)
print(f" ✓ 使用统计获取成功")
print(f" - 存储: {stats['storage_mb']:.2f} MB")
print(f" - 转录: {stats['transcription_minutes']:.2f} 分钟")
print(f" - API 调用: {stats['api_calls']}")
print(f" - 项目数: {stats['projects_count']}")
print(f" - 实体数: {stats['entities_count']}")
print(f" - 成员数: {stats['members_count']}")
print(f" - 配额: {stats['limits']}")
except Exception as e:
print(f" ✗ 使用统计获取失败: {e}")
import traceback
traceback.print_exc()
return False
# 3. 检查资源限制
print("\n3. 检查资源限制...")
try:
allowed, current, limit = manager.check_resource_limit(tenant_id, "storage")
print(f" ✓ 资源限制检查成功")
print(f" - 存储: {allowed}, 当前: {current}, 限制: {limit}")
except Exception as e:
print(f" ✗ 资源限制检查失败: {e}")
import traceback
traceback.print_exc()
return False
return True
def test_tenant_context():
"""测试租户上下文管理"""
print("\n" + "=" * 60)
print("测试租户上下文管理")
print("=" * 60)
# 1. 设置和获取租户上下文
print("\n1. 设置和获取租户上下文...")
try:
TenantContext.set_current_tenant("tenant_123")
tenant_id = TenantContext.get_current_tenant()
assert tenant_id == "tenant_123"
print(f" ✓ 租户上下文设置成功: {tenant_id}")
except Exception as e:
print(f" ✗ 租户上下文设置失败: {e}")
return False
# 2. 设置和获取用户上下文
print("\n2. 设置和获取用户上下文...")
try:
TenantContext.set_current_user("user_456")
user_id = TenantContext.get_current_user()
assert user_id == "user_456"
print(f" ✓ 用户上下文设置成功: {user_id}")
except Exception as e:
print(f" ✗ 用户上下文设置失败: {e}")
return False
# 3. 清除上下文
print("\n3. 清除上下文...")
try:
TenantContext.clear()
assert TenantContext.get_current_tenant() is None
assert TenantContext.get_current_user() is None
print(f" ✓ 上下文清除成功")
except Exception as e:
print(f" ✗ 上下文清除失败: {e}")
return False
return True
def cleanup():
"""清理测试数据"""
print("\n" + "=" * 60)
print("清理测试数据")
print("=" * 60)
test_db = "test_tenant.db"
if os.path.exists(test_db):
os.remove(test_db)
print(f"✓ 删除测试数据库: {test_db}")
def main():
"""主测试函数"""
print("\n" + "=" * 60)
print("InsightFlow Phase 8 - Multi-Tenant SaaS 测试")
print("=" * 60)
all_passed = True
tenant_id = None
try:
# 测试租户上下文
if not test_tenant_context():
all_passed = False
# 测试租户管理
tenant_id = test_tenant_management()
if not tenant_id:
all_passed = False
# 测试域名管理
if not test_domain_management(tenant_id):
all_passed = False
# 测试品牌配置
if not test_branding_management(tenant_id):
all_passed = False
# 测试成员管理
if not test_member_management(tenant_id):
all_passed = False
# 测试使用统计
if not test_usage_stats(tenant_id):
all_passed = False
except Exception as e:
print(f"\n测试过程中发生错误: {e}")
import traceback
traceback.print_exc()
all_passed = False
finally:
cleanup()
print("\n" + "=" * 60)
if all_passed:
print("✓ 所有测试通过!")
else:
print("✗ 部分测试失败")
print("=" * 60)
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())