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:
60
README.md
60
README.md
@@ -205,16 +205,64 @@ MIT
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 8: 商业化与规模化 - 规划中 🚧
|
## Phase 8 开发进度
|
||||||
|
|
||||||
|
| 任务 | 状态 | 完成时间 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 1. 多租户 SaaS 架构 | ✅ 已完成 | 2026-02-25 |
|
||||||
|
| 2. 订阅与计费系统 | 🚧 进行中 | - |
|
||||||
|
| 3. 企业级功能 | ⏳ 待开始 | - |
|
||||||
|
| 4. AI 能力增强 | ⏳ 待开始 | - |
|
||||||
|
| 5. 运营与增长工具 | ⏳ 待开始 | - |
|
||||||
|
| 6. 开发者生态 | ⏳ 待开始 | - |
|
||||||
|
| 7. 全球化与本地化 | ⏳ 待开始 | - |
|
||||||
|
| 8. 运维与监控 | ⏳ 待开始 | - |
|
||||||
|
|
||||||
|
### Phase 8 任务 1 完成内容
|
||||||
|
|
||||||
|
**多租户 SaaS 架构** ✅
|
||||||
|
|
||||||
|
- ✅ 创建 tenant_manager.py - 多租户管理模块
|
||||||
|
- TenantManager: 租户管理主类
|
||||||
|
- Tenant: 租户数据模型(支持 Free/Pro/Enterprise 层级)
|
||||||
|
- TenantDomain: 自定义域名管理(DNS/文件验证)
|
||||||
|
- TenantBranding: 品牌白标配置(Logo、主题色、CSS)
|
||||||
|
- TenantMember: 租户成员管理(Owner/Admin/Member/Viewer 角色)
|
||||||
|
- TenantContext: 租户上下文管理器
|
||||||
|
- 租户隔离(数据、配置、资源完全隔离)
|
||||||
|
- 资源限制和用量统计
|
||||||
|
- ✅ 更新 schema.sql - 添加租户相关数据库表
|
||||||
|
- tenants: 租户主表
|
||||||
|
- tenant_domains: 租户域名绑定表
|
||||||
|
- tenant_branding: 租户品牌配置表
|
||||||
|
- tenant_members: 租户成员表
|
||||||
|
- tenant_permissions: 租户权限定义表
|
||||||
|
- tenant_usage: 租户资源使用统计表
|
||||||
|
- ✅ 更新 main.py - 添加租户相关 API 端点
|
||||||
|
- POST/GET /api/v1/tenants - 租户管理
|
||||||
|
- POST/GET /api/v1/tenants/{id}/domains - 域名管理
|
||||||
|
- POST /api/v1/tenants/{id}/domains/{id}/verify - 域名验证
|
||||||
|
- GET/PUT /api/v1/tenants/{id}/branding - 品牌配置
|
||||||
|
- GET /api/v1/tenants/{id}/branding.css - 品牌 CSS(公开)
|
||||||
|
- POST/GET /api/v1/tenants/{id}/members - 成员管理
|
||||||
|
- GET /api/v1/tenants/{id}/usage - 使用统计
|
||||||
|
- GET /api/v1/tenants/{id}/limits/{type} - 资源限制检查
|
||||||
|
- GET /api/v1/resolve-tenant - 域名解析租户
|
||||||
|
|
||||||
|
**预计 Phase 8 完成时间**: 6-8 周
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8: 商业化与规模化 - 进行中 🚧
|
||||||
|
|
||||||
基于 Phase 1-7 的完整功能,Phase 8 聚焦**商业化落地**和**规模化运营**:
|
基于 Phase 1-7 的完整功能,Phase 8 聚焦**商业化落地**和**规模化运营**:
|
||||||
|
|
||||||
### 1. 多租户 SaaS 架构 🏢
|
### 1. 多租户 SaaS 架构 🏢
|
||||||
**优先级: P0**
|
**优先级: P0** | **状态: ✅ 已完成**
|
||||||
- 租户隔离(数据、配置、资源完全隔离)
|
- ✅ 租户隔离(数据、配置、资源完全隔离)
|
||||||
- 自定义域名绑定(CNAME 支持)
|
- ✅ 自定义域名绑定(CNAME 支持)
|
||||||
- 品牌白标(Logo、主题色、自定义 CSS)
|
- ✅ 品牌白标(Logo、主题色、自定义 CSS)
|
||||||
- 租户级权限管理(超级管理员、管理员、成员)
|
- ✅ 租户级权限管理(超级管理员、管理员、成员)
|
||||||
|
|
||||||
### 2. 订阅与计费系统 💳
|
### 2. 订阅与计费系统 💳
|
||||||
**优先级: P0**
|
**优先级: P0**
|
||||||
|
|||||||
58
STATUS.md
58
STATUS.md
@@ -1,10 +1,10 @@
|
|||||||
# InsightFlow 开发状态
|
# InsightFlow 开发状态
|
||||||
|
|
||||||
**最后更新**: 2026-02-24 18:00
|
**最后更新**: 2026-02-25 12:00
|
||||||
|
|
||||||
## 当前阶段
|
## 当前阶段
|
||||||
|
|
||||||
Phase 7: 性能优化与扩展 - **已完成 ✅**
|
Phase 8: 商业化与规模化 - **进行中 🚧**
|
||||||
|
|
||||||
## 部署状态
|
## 部署状态
|
||||||
|
|
||||||
@@ -36,7 +36,59 @@ Phase 7: 性能优化与扩展 - **已完成 ✅**
|
|||||||
- 导出功能
|
- 导出功能
|
||||||
- API 开放平台
|
- API 开放平台
|
||||||
|
|
||||||
### Phase 7 - 任务 1: 工作流自动化 (已完成 ✅)
|
### Phase 7 - 全部任务 (已完成 ✅)
|
||||||
|
- ✅ 任务 1: 智能工作流自动化
|
||||||
|
- ✅ 任务 2: 多模态支持
|
||||||
|
- ✅ 任务 3: 数据安全与合规
|
||||||
|
- ✅ 任务 4: 协作与共享
|
||||||
|
- ✅ 任务 5: 智能报告生成
|
||||||
|
- ✅ 任务 6: 高级搜索与发现
|
||||||
|
- ✅ 任务 7: 插件与集成
|
||||||
|
- ✅ 任务 8: 性能优化与扩展
|
||||||
|
|
||||||
|
### Phase 8 - 任务 1: 多租户 SaaS 架构 (已完成 ✅)
|
||||||
|
- ✅ 创建 tenant_manager.py - 多租户管理模块
|
||||||
|
- TenantManager: 租户管理主类
|
||||||
|
- Tenant: 租户数据模型
|
||||||
|
- TenantDomain: 自定义域名管理
|
||||||
|
- TenantBranding: 品牌白标配置
|
||||||
|
- TenantMember: 租户成员管理
|
||||||
|
- TenantContext: 租户上下文管理器
|
||||||
|
- 租户隔离(数据、配置、资源完全隔离)
|
||||||
|
- 多层级订阅计划支持(Free/Pro/Enterprise)
|
||||||
|
- 资源限制和用量统计
|
||||||
|
- ✅ 更新 schema.sql - 添加租户相关数据库表
|
||||||
|
- tenants: 租户主表
|
||||||
|
- tenant_domains: 租户域名绑定表
|
||||||
|
- tenant_branding: 租户品牌配置表
|
||||||
|
- tenant_members: 租户成员表
|
||||||
|
- tenant_permissions: 租户权限定义表
|
||||||
|
- tenant_usage: 租户资源使用统计表
|
||||||
|
- ✅ 更新 main.py - 添加租户相关 API 端点
|
||||||
|
- POST/GET /api/v1/tenants - 租户管理
|
||||||
|
- POST/GET /api/v1/tenants/{id}/domains - 域名管理
|
||||||
|
- POST /api/v1/tenants/{id}/domains/{id}/verify - 域名验证
|
||||||
|
- GET/PUT /api/v1/tenants/{id}/branding - 品牌配置
|
||||||
|
- GET /api/v1/tenants/{id}/branding.css - 品牌 CSS
|
||||||
|
- POST/GET /api/v1/tenants/{id}/members - 成员管理
|
||||||
|
- GET /api/v1/tenants/{id}/usage - 使用统计
|
||||||
|
- GET /api/v1/tenants/{id}/limits/{type} - 资源限制检查
|
||||||
|
- GET /api/v1/resolve-tenant - 域名解析租户
|
||||||
|
|
||||||
|
## 待完成
|
||||||
|
|
||||||
|
### Phase 8 任务清单
|
||||||
|
|
||||||
|
| 任务 | 名称 | 优先级 | 状态 | 计划完成 |
|
||||||
|
|------|------|--------|------|----------|
|
||||||
|
| 1 | 多租户 SaaS 架构 | P0 | ✅ | 2026-02-25 |
|
||||||
|
| 2 | 订阅与计费系统 | P0 | 🚧 | 2026-02-26 |
|
||||||
|
| 3 | 企业级功能 | P1 | ⏳ | 2026-02-28 |
|
||||||
|
| 4 | AI 能力增强 | P1 | ⏳ | 2026-03-02 |
|
||||||
|
| 5 | 运营与增长工具 | P1 | ⏳ | 2026-03-04 |
|
||||||
|
| 6 | 开发者生态 | P2 | ⏳ | 2026-03-06 |
|
||||||
|
| 7 | 全球化与本地化 | P2 | ⏳ | 2026-03-08 |
|
||||||
|
| 8 | 运维与监控 | P2 | ⏳ | 2026-03-10 |
|
||||||
- ✅ 创建 workflow_manager.py - 工作流管理模块
|
- ✅ 创建 workflow_manager.py - 工作流管理模块
|
||||||
- WorkflowManager: 主管理类
|
- WorkflowManager: 主管理类
|
||||||
- WorkflowTask: 工作流任务定义
|
- WorkflowTask: 工作流任务定义
|
||||||
|
|||||||
BIN
backend/__pycache__/performance_manager.cpython-312.pyc
Normal file
BIN
backend/__pycache__/performance_manager.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/search_manager.cpython-312.pyc
Normal file
BIN
backend/__pycache__/search_manager.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/tenant_manager.cpython-312.pyc
Normal file
BIN
backend/__pycache__/tenant_manager.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/insightflow.db
Normal file
BIN
backend/insightflow.db
Normal file
Binary file not shown.
1189
backend/main.py
1189
backend/main.py
File diff suppressed because it is too large
Load Diff
@@ -60,3 +60,6 @@ sentence-transformers==2.5.1
|
|||||||
# Phase 7 Task 8: Performance Optimization & Scaling
|
# Phase 7 Task 8: Performance Optimization & Scaling
|
||||||
redis==5.0.1
|
redis==5.0.1
|
||||||
celery==5.3.6
|
celery==5.3.6
|
||||||
|
|
||||||
|
# Phase 8: Multi-Tenant SaaS
|
||||||
|
# (No additional dependencies required - uses built-in Python modules)
|
||||||
|
|||||||
@@ -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_project ON chrome_extension_tokens(project_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_chrome_tokens_hash ON chrome_extension_tokens(token_hash);
|
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: 插件与集成相关表
|
-- Phase 7: 插件与集成相关表
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
-- 插件表
|
-- 插件表
|
||||||
CREATE TABLE IF NOT EXISTS plugins (
|
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_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_project ON shard_mappings(project_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_shard_mappings_shard ON shard_mappings(shard_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);
|
||||||
|
|||||||
2146
backend/search_manager.py
Normal file
2146
backend/search_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
1381
backend/tenant_manager.py
Normal file
1381
backend/tenant_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
419
backend/test_phase7_task6_8.py
Normal file
419
backend/test_phase7_task6_8.py
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
InsightFlow Phase 7 Task 6 & 8 测试脚本
|
||||||
|
测试高级搜索与发现、性能优化与扩展功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
# 添加 backend 到路径
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from search_manager import (
|
||||||
|
get_search_manager, SearchManager,
|
||||||
|
FullTextSearch, SemanticSearch,
|
||||||
|
EntityPathDiscovery, KnowledgeGapDetection
|
||||||
|
)
|
||||||
|
|
||||||
|
from performance_manager import (
|
||||||
|
get_performance_manager, PerformanceManager,
|
||||||
|
CacheManager, DatabaseSharding, TaskQueue, PerformanceMonitor
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fulltext_search():
|
||||||
|
"""测试全文搜索"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("测试全文搜索 (FullTextSearch)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
search = FullTextSearch()
|
||||||
|
|
||||||
|
# 测试索引创建
|
||||||
|
print("\n1. 测试索引创建...")
|
||||||
|
success = search.index_content(
|
||||||
|
content_id="test_entity_1",
|
||||||
|
content_type="entity",
|
||||||
|
project_id="test_project",
|
||||||
|
text="这是一个测试实体,用于验证全文搜索功能。支持关键词高亮显示。"
|
||||||
|
)
|
||||||
|
print(f" 索引创建: {'✓ 成功' if success else '✗ 失败'}")
|
||||||
|
|
||||||
|
# 测试搜索
|
||||||
|
print("\n2. 测试关键词搜索...")
|
||||||
|
results = search.search("测试", project_id="test_project")
|
||||||
|
print(f" 搜索结果数量: {len(results)}")
|
||||||
|
if results:
|
||||||
|
print(f" 第一个结果: {results[0].content[:50]}...")
|
||||||
|
print(f" 相关分数: {results[0].score}")
|
||||||
|
|
||||||
|
# 测试布尔搜索
|
||||||
|
print("\n3. 测试布尔搜索...")
|
||||||
|
results = search.search("测试 AND 全文", project_id="test_project")
|
||||||
|
print(f" AND 搜索结果: {len(results)}")
|
||||||
|
|
||||||
|
results = search.search("测试 OR 关键词", project_id="test_project")
|
||||||
|
print(f" OR 搜索结果: {len(results)}")
|
||||||
|
|
||||||
|
# 测试高亮
|
||||||
|
print("\n4. 测试文本高亮...")
|
||||||
|
highlighted = search.highlight_text(
|
||||||
|
"这是一个测试实体,用于验证全文搜索功能。",
|
||||||
|
"测试 全文"
|
||||||
|
)
|
||||||
|
print(f" 高亮结果: {highlighted}")
|
||||||
|
|
||||||
|
print("\n✓ 全文搜索测试完成")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_semantic_search():
|
||||||
|
"""测试语义搜索"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("测试语义搜索 (SemanticSearch)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
semantic = SemanticSearch()
|
||||||
|
|
||||||
|
# 检查可用性
|
||||||
|
print(f"\n1. 语义搜索可用性: {'✓ 可用' if semantic.is_available() else '✗ 不可用'}")
|
||||||
|
|
||||||
|
if not semantic.is_available():
|
||||||
|
print(" (需要安装 sentence-transformers 库)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 测试 embedding 生成
|
||||||
|
print("\n2. 测试 embedding 生成...")
|
||||||
|
embedding = semantic.generate_embedding("这是一个测试句子")
|
||||||
|
if embedding:
|
||||||
|
print(f" Embedding 维度: {len(embedding)}")
|
||||||
|
print(f" 前5个值: {embedding[:5]}")
|
||||||
|
|
||||||
|
# 测试索引
|
||||||
|
print("\n3. 测试语义索引...")
|
||||||
|
success = semantic.index_embedding(
|
||||||
|
content_id="test_content_1",
|
||||||
|
content_type="transcript",
|
||||||
|
project_id="test_project",
|
||||||
|
text="这是用于语义搜索测试的文本内容。"
|
||||||
|
)
|
||||||
|
print(f" 索引创建: {'✓ 成功' if success else '✗ 失败'}")
|
||||||
|
|
||||||
|
print("\n✓ 语义搜索测试完成")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_entity_path_discovery():
|
||||||
|
"""测试实体路径发现"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("测试实体路径发现 (EntityPathDiscovery)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
discovery = EntityPathDiscovery()
|
||||||
|
|
||||||
|
print("\n1. 测试路径发现初始化...")
|
||||||
|
print(f" 数据库路径: {discovery.db_path}")
|
||||||
|
|
||||||
|
print("\n2. 测试多跳关系发现...")
|
||||||
|
# 注意:这需要在数据库中有实际数据
|
||||||
|
print(" (需要实际实体数据才能测试)")
|
||||||
|
|
||||||
|
print("\n✓ 实体路径发现测试完成")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_knowledge_gap_detection():
|
||||||
|
"""测试知识缺口识别"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("测试知识缺口识别 (KnowledgeGapDetection)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
detection = KnowledgeGapDetection()
|
||||||
|
|
||||||
|
print("\n1. 测试缺口检测初始化...")
|
||||||
|
print(f" 数据库路径: {detection.db_path}")
|
||||||
|
|
||||||
|
print("\n2. 测试完整性报告生成...")
|
||||||
|
# 注意:这需要在数据库中有实际项目数据
|
||||||
|
print(" (需要实际项目数据才能测试)")
|
||||||
|
|
||||||
|
print("\n✓ 知识缺口识别测试完成")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_cache_manager():
|
||||||
|
"""测试缓存管理器"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("测试缓存管理器 (CacheManager)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
cache = CacheManager()
|
||||||
|
|
||||||
|
print(f"\n1. 缓存后端: {'Redis' if cache.use_redis else '内存 LRU'}")
|
||||||
|
|
||||||
|
print("\n2. 测试缓存操作...")
|
||||||
|
# 设置缓存
|
||||||
|
cache.set("test_key_1", {"name": "测试数据", "value": 123}, ttl=60)
|
||||||
|
print(" ✓ 设置缓存 test_key_1")
|
||||||
|
|
||||||
|
# 获取缓存
|
||||||
|
value = cache.get("test_key_1")
|
||||||
|
print(f" ✓ 获取缓存: {value}")
|
||||||
|
|
||||||
|
# 批量操作
|
||||||
|
cache.set_many({
|
||||||
|
"batch_key_1": "value1",
|
||||||
|
"batch_key_2": "value2",
|
||||||
|
"batch_key_3": "value3"
|
||||||
|
}, ttl=60)
|
||||||
|
print(" ✓ 批量设置缓存")
|
||||||
|
|
||||||
|
values = cache.get_many(["batch_key_1", "batch_key_2", "batch_key_3"])
|
||||||
|
print(f" ✓ 批量获取缓存: {len(values)} 个")
|
||||||
|
|
||||||
|
# 删除缓存
|
||||||
|
cache.delete("test_key_1")
|
||||||
|
print(" ✓ 删除缓存 test_key_1")
|
||||||
|
|
||||||
|
# 获取统计
|
||||||
|
stats = cache.get_stats()
|
||||||
|
print(f"\n3. 缓存统计:")
|
||||||
|
print(f" 总请求数: {stats['total_requests']}")
|
||||||
|
print(f" 命中数: {stats['hits']}")
|
||||||
|
print(f" 未命中数: {stats['misses']}")
|
||||||
|
print(f" 命中率: {stats['hit_rate']:.2%}")
|
||||||
|
|
||||||
|
if not cache.use_redis:
|
||||||
|
print(f" 内存使用: {stats.get('memory_size_bytes', 0)} bytes")
|
||||||
|
print(f" 缓存条目数: {stats.get('cache_entries', 0)}")
|
||||||
|
|
||||||
|
print("\n✓ 缓存管理器测试完成")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_queue():
|
||||||
|
"""测试任务队列"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("测试任务队列 (TaskQueue)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
queue = TaskQueue()
|
||||||
|
|
||||||
|
print(f"\n1. 任务队列可用性: {'✓ 可用' if queue.is_available() else '✗ 不可用'}")
|
||||||
|
print(f" 后端: {'Celery' if queue.use_celery else '内存'}")
|
||||||
|
|
||||||
|
print("\n2. 测试任务提交...")
|
||||||
|
|
||||||
|
# 定义测试任务处理器
|
||||||
|
def test_task_handler(payload):
|
||||||
|
print(f" 执行任务: {payload}")
|
||||||
|
return {"status": "success", "processed": True}
|
||||||
|
|
||||||
|
queue.register_handler("test_task", test_task_handler)
|
||||||
|
|
||||||
|
# 提交任务
|
||||||
|
task_id = queue.submit(
|
||||||
|
task_type="test_task",
|
||||||
|
payload={"test": "data", "timestamp": time.time()}
|
||||||
|
)
|
||||||
|
print(f" ✓ 提交任务: {task_id}")
|
||||||
|
|
||||||
|
# 获取任务状态
|
||||||
|
task_info = queue.get_status(task_id)
|
||||||
|
if task_info:
|
||||||
|
print(f" ✓ 任务状态: {task_info.status}")
|
||||||
|
|
||||||
|
# 获取统计
|
||||||
|
stats = queue.get_stats()
|
||||||
|
print(f"\n3. 任务队列统计:")
|
||||||
|
print(f" 后端: {stats['backend']}")
|
||||||
|
print(f" 按状态统计: {stats.get('by_status', {})}")
|
||||||
|
|
||||||
|
print("\n✓ 任务队列测试完成")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_performance_monitor():
|
||||||
|
"""测试性能监控"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("测试性能监控 (PerformanceMonitor)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
monitor = PerformanceMonitor()
|
||||||
|
|
||||||
|
print("\n1. 测试指标记录...")
|
||||||
|
|
||||||
|
# 记录一些测试指标
|
||||||
|
for i in range(5):
|
||||||
|
monitor.record_metric(
|
||||||
|
metric_type="api_response",
|
||||||
|
duration_ms=50 + i * 10,
|
||||||
|
endpoint="/api/v1/test",
|
||||||
|
metadata={"test": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
monitor.record_metric(
|
||||||
|
metric_type="db_query",
|
||||||
|
duration_ms=20 + i * 5,
|
||||||
|
endpoint="SELECT test",
|
||||||
|
metadata={"test": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(" ✓ 记录了 8 个测试指标")
|
||||||
|
|
||||||
|
# 获取统计
|
||||||
|
print("\n2. 获取性能统计...")
|
||||||
|
stats = monitor.get_stats(hours=1)
|
||||||
|
print(f" 总请求数: {stats['overall']['total_requests']}")
|
||||||
|
print(f" 平均响应时间: {stats['overall']['avg_duration_ms']} ms")
|
||||||
|
print(f" 最大响应时间: {stats['overall']['max_duration_ms']} ms")
|
||||||
|
|
||||||
|
print("\n3. 按类型统计:")
|
||||||
|
for type_stat in stats.get('by_type', []):
|
||||||
|
print(f" {type_stat['type']}: {type_stat['count']} 次, "
|
||||||
|
f"平均 {type_stat['avg_duration_ms']} ms")
|
||||||
|
|
||||||
|
print("\n✓ 性能监控测试完成")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_manager():
|
||||||
|
"""测试搜索管理器"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("测试搜索管理器 (SearchManager)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
manager = get_search_manager()
|
||||||
|
|
||||||
|
print("\n1. 搜索管理器初始化...")
|
||||||
|
print(f" ✓ 搜索管理器已初始化")
|
||||||
|
|
||||||
|
print("\n2. 获取搜索统计...")
|
||||||
|
stats = manager.get_search_stats()
|
||||||
|
print(f" 全文索引数: {stats['fulltext_indexed']}")
|
||||||
|
print(f" 语义索引数: {stats['semantic_indexed']}")
|
||||||
|
print(f" 语义搜索可用: {stats['semantic_search_available']}")
|
||||||
|
|
||||||
|
print("\n✓ 搜索管理器测试完成")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_performance_manager():
|
||||||
|
"""测试性能管理器"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("测试性能管理器 (PerformanceManager)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
manager = get_performance_manager()
|
||||||
|
|
||||||
|
print("\n1. 性能管理器初始化...")
|
||||||
|
print(f" ✓ 性能管理器已初始化")
|
||||||
|
|
||||||
|
print("\n2. 获取系统健康状态...")
|
||||||
|
health = manager.get_health_status()
|
||||||
|
print(f" 缓存后端: {health['cache']['backend']}")
|
||||||
|
print(f" 任务队列后端: {health['task_queue']['backend']}")
|
||||||
|
|
||||||
|
print("\n3. 获取完整统计...")
|
||||||
|
stats = manager.get_full_stats()
|
||||||
|
print(f" 缓存统计: {stats['cache']['total_requests']} 请求")
|
||||||
|
print(f" 任务队列统计: {stats['task_queue']}")
|
||||||
|
|
||||||
|
print("\n✓ 性能管理器测试完成")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""运行所有测试"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("InsightFlow Phase 7 Task 6 & 8 测试")
|
||||||
|
print("高级搜索与发现 + 性能优化与扩展")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# 搜索模块测试
|
||||||
|
try:
|
||||||
|
results.append(("全文搜索", test_fulltext_search()))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ 全文搜索测试失败: {e}")
|
||||||
|
results.append(("全文搜索", False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
results.append(("语义搜索", test_semantic_search()))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ 语义搜索测试失败: {e}")
|
||||||
|
results.append(("语义搜索", False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
results.append(("实体路径发现", test_entity_path_discovery()))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ 实体路径发现测试失败: {e}")
|
||||||
|
results.append(("实体路径发现", False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
results.append(("知识缺口识别", test_knowledge_gap_detection()))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ 知识缺口识别测试失败: {e}")
|
||||||
|
results.append(("知识缺口识别", False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
results.append(("搜索管理器", test_search_manager()))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ 搜索管理器测试失败: {e}")
|
||||||
|
results.append(("搜索管理器", False))
|
||||||
|
|
||||||
|
# 性能模块测试
|
||||||
|
try:
|
||||||
|
results.append(("缓存管理器", test_cache_manager()))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ 缓存管理器测试失败: {e}")
|
||||||
|
results.append(("缓存管理器", False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
results.append(("任务队列", test_task_queue()))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ 任务队列测试失败: {e}")
|
||||||
|
results.append(("任务队列", False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
results.append(("性能监控", test_performance_monitor()))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ 性能监控测试失败: {e}")
|
||||||
|
results.append(("性能监控", False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
results.append(("性能管理器", test_performance_manager()))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ 性能管理器测试失败: {e}")
|
||||||
|
results.append(("性能管理器", False))
|
||||||
|
|
||||||
|
# 打印测试汇总
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("测试汇总")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
passed = sum(1 for _, result in results if result)
|
||||||
|
total = len(results)
|
||||||
|
|
||||||
|
for name, result in results:
|
||||||
|
status = "✓ 通过" if result else "✗ 失败"
|
||||||
|
print(f" {status} - {name}")
|
||||||
|
|
||||||
|
print(f"\n总计: {passed}/{total} 测试通过")
|
||||||
|
|
||||||
|
if passed == total:
|
||||||
|
print("\n🎉 所有测试通过!")
|
||||||
|
else:
|
||||||
|
print(f"\n⚠️ 有 {total - passed} 个测试失败")
|
||||||
|
|
||||||
|
return passed == total
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = run_all_tests()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
331
backend/test_phase8_task1.py
Normal file
331
backend/test_phase8_task1.py
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
InsightFlow Phase 8 Task 1 - 多租户 SaaS 架构测试脚本
|
||||||
|
|
||||||
|
测试内容:
|
||||||
|
1. 租户创建和管理
|
||||||
|
2. 自定义域名绑定和验证
|
||||||
|
3. 品牌白标配置
|
||||||
|
4. 成员邀请和权限管理
|
||||||
|
5. 资源使用统计
|
||||||
|
"""
|
||||||
|
|
||||||
|
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, TenantRole, TenantStatus, TenantTier
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tenant_management():
|
||||||
|
"""测试租户管理功能"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("测试 1: 租户管理")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
|
# 1. 创建租户
|
||||||
|
print("\n1.1 创建租户...")
|
||||||
|
tenant = manager.create_tenant(
|
||||||
|
name="Test Company",
|
||||||
|
owner_id="user_001",
|
||||||
|
tier="pro",
|
||||||
|
description="A test company tenant"
|
||||||
|
)
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# 2. 获取租户
|
||||||
|
print("\n1.2 获取租户信息...")
|
||||||
|
fetched = manager.get_tenant(tenant.id)
|
||||||
|
assert fetched is not None, "获取租户失败"
|
||||||
|
print(f"✅ 获取租户成功: {fetched.name}")
|
||||||
|
|
||||||
|
# 3. 通过 slug 获取
|
||||||
|
print("\n1.3 通过 slug 获取租户...")
|
||||||
|
by_slug = manager.get_tenant_by_slug(tenant.slug)
|
||||||
|
assert by_slug is not None, "通过 slug 获取失败"
|
||||||
|
print(f"✅ 通过 slug 获取成功: {by_slug.name}")
|
||||||
|
|
||||||
|
# 4. 更新租户
|
||||||
|
print("\n1.4 更新租户信息...")
|
||||||
|
updated = manager.update_tenant(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
name="Test Company Updated",
|
||||||
|
tier="enterprise"
|
||||||
|
)
|
||||||
|
assert updated is not None, "更新租户失败"
|
||||||
|
print(f"✅ 租户更新成功: {updated.name}, 层级: {updated.tier}")
|
||||||
|
|
||||||
|
# 5. 列出租户
|
||||||
|
print("\n1.5 列出租户...")
|
||||||
|
tenants = manager.list_tenants(limit=10)
|
||||||
|
print(f"✅ 找到 {len(tenants)} 个租户")
|
||||||
|
|
||||||
|
return tenant.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_domain_management(tenant_id: str):
|
||||||
|
"""测试域名管理功能"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("测试 2: 域名管理")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
|
# 1. 添加域名
|
||||||
|
print("\n2.1 添加自定义域名...")
|
||||||
|
domain = manager.add_domain(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
domain="test.example.com",
|
||||||
|
is_primary=True
|
||||||
|
)
|
||||||
|
print(f"✅ 域名添加成功: {domain.domain}")
|
||||||
|
print(f" - ID: {domain.id}")
|
||||||
|
print(f" - 状态: {domain.status}")
|
||||||
|
print(f" - 验证令牌: {domain.verification_token}")
|
||||||
|
|
||||||
|
# 2. 获取验证指导
|
||||||
|
print("\n2.2 获取域名验证指导...")
|
||||||
|
instructions = manager.get_domain_verification_instructions(domain.id)
|
||||||
|
print(f"✅ 验证指导:")
|
||||||
|
print(f" - DNS 记录: {instructions['dns_record']}")
|
||||||
|
print(f" - 文件验证: {instructions['file_verification']}")
|
||||||
|
|
||||||
|
# 3. 验证域名
|
||||||
|
print("\n2.3 验证域名...")
|
||||||
|
verified = manager.verify_domain(tenant_id, domain.id)
|
||||||
|
print(f"✅ 域名验证结果: {verified}")
|
||||||
|
|
||||||
|
# 4. 通过域名获取租户
|
||||||
|
print("\n2.4 通过域名获取租户...")
|
||||||
|
by_domain = manager.get_tenant_by_domain("test.example.com")
|
||||||
|
if by_domain:
|
||||||
|
print(f"✅ 通过域名获取租户成功: {by_domain.name}")
|
||||||
|
else:
|
||||||
|
print("⚠️ 通过域名获取租户失败(验证可能未通过)")
|
||||||
|
|
||||||
|
# 5. 列出域名
|
||||||
|
print("\n2.5 列出所有域名...")
|
||||||
|
domains = manager.list_domains(tenant_id)
|
||||||
|
print(f"✅ 找到 {len(domains)} 个域名")
|
||||||
|
for d in domains:
|
||||||
|
print(f" - {d.domain} ({d.status})")
|
||||||
|
|
||||||
|
return domain.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_branding_management(tenant_id: str):
|
||||||
|
"""测试品牌白标功能"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("测试 3: 品牌白标")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
|
# 1. 更新品牌配置
|
||||||
|
print("\n3.1 更新品牌配置...")
|
||||||
|
branding = manager.update_branding(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
logo_url="https://example.com/logo.png",
|
||||||
|
favicon_url="https://example.com/favicon.ico",
|
||||||
|
primary_color="#1890ff",
|
||||||
|
secondary_color="#52c41a",
|
||||||
|
custom_css=".header { background: #1890ff; }",
|
||||||
|
custom_js="console.log('Custom JS loaded');",
|
||||||
|
login_page_bg="https://example.com/bg.jpg"
|
||||||
|
)
|
||||||
|
print(f"✅ 品牌配置更新成功")
|
||||||
|
print(f" - Logo: {branding.logo_url}")
|
||||||
|
print(f" - 主色: {branding.primary_color}")
|
||||||
|
print(f" - 次色: {branding.secondary_color}")
|
||||||
|
|
||||||
|
# 2. 获取品牌配置
|
||||||
|
print("\n3.2 获取品牌配置...")
|
||||||
|
fetched = manager.get_branding(tenant_id)
|
||||||
|
assert fetched is not None, "获取品牌配置失败"
|
||||||
|
print(f"✅ 获取品牌配置成功")
|
||||||
|
|
||||||
|
# 3. 生成品牌 CSS
|
||||||
|
print("\n3.3 生成品牌 CSS...")
|
||||||
|
css = manager.get_branding_css(tenant_id)
|
||||||
|
print(f"✅ 生成 CSS 成功 ({len(css)} 字符)")
|
||||||
|
print(f" CSS 预览:\n{css[:200]}...")
|
||||||
|
|
||||||
|
return branding.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_member_management(tenant_id: str):
|
||||||
|
"""测试成员管理功能"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("测试 4: 成员管理")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
|
# 1. 邀请成员
|
||||||
|
print("\n4.1 邀请成员...")
|
||||||
|
member1 = manager.invite_member(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
email="admin@test.com",
|
||||||
|
role="admin",
|
||||||
|
invited_by="user_001"
|
||||||
|
)
|
||||||
|
print(f"✅ 成员邀请成功: {member1.email}")
|
||||||
|
print(f" - ID: {member1.id}")
|
||||||
|
print(f" - 角色: {member1.role}")
|
||||||
|
print(f" - 权限: {member1.permissions}")
|
||||||
|
|
||||||
|
member2 = manager.invite_member(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
email="member@test.com",
|
||||||
|
role="member",
|
||||||
|
invited_by="user_001"
|
||||||
|
)
|
||||||
|
print(f"✅ 成员邀请成功: {member2.email}")
|
||||||
|
|
||||||
|
# 2. 接受邀请
|
||||||
|
print("\n4.2 接受邀请...")
|
||||||
|
accepted = manager.accept_invitation(member1.id, "user_002")
|
||||||
|
print(f"✅ 邀请接受结果: {accepted}")
|
||||||
|
|
||||||
|
# 3. 列出成员
|
||||||
|
print("\n4.3 列出所有成员...")
|
||||||
|
members = manager.list_members(tenant_id)
|
||||||
|
print(f"✅ 找到 {len(members)} 个成员")
|
||||||
|
for m in members:
|
||||||
|
print(f" - {m.email} ({m.role}) - {m.status}")
|
||||||
|
|
||||||
|
# 4. 检查权限
|
||||||
|
print("\n4.4 检查权限...")
|
||||||
|
can_manage = manager.check_permission(tenant_id, "user_002", "project", "create")
|
||||||
|
print(f"✅ user_002 可以创建项目: {can_manage}")
|
||||||
|
|
||||||
|
# 5. 更新成员角色
|
||||||
|
print("\n4.5 更新成员角色...")
|
||||||
|
updated = manager.update_member_role(tenant_id, member2.id, "viewer")
|
||||||
|
print(f"✅ 角色更新结果: {updated}")
|
||||||
|
|
||||||
|
# 6. 获取用户所属租户
|
||||||
|
print("\n4.6 获取用户所属租户...")
|
||||||
|
user_tenants = manager.get_user_tenants("user_002")
|
||||||
|
print(f"✅ user_002 属于 {len(user_tenants)} 个租户")
|
||||||
|
for t in user_tenants:
|
||||||
|
print(f" - {t['name']} ({t['member_role']})")
|
||||||
|
|
||||||
|
return member1.id, member2.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_usage_tracking(tenant_id: str):
|
||||||
|
"""测试资源使用统计功能"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("测试 5: 资源使用统计")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
|
# 1. 记录使用
|
||||||
|
print("\n5.1 记录资源使用...")
|
||||||
|
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("✅ 资源使用记录成功")
|
||||||
|
|
||||||
|
# 2. 获取使用统计
|
||||||
|
print("\n5.2 获取使用统计...")
|
||||||
|
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['usage_percentages']}")
|
||||||
|
|
||||||
|
# 3. 检查资源限制
|
||||||
|
print("\n5.3 检查资源限制...")
|
||||||
|
for resource in ["storage", "transcription", "api_calls", "projects", "entities", "members"]:
|
||||||
|
allowed, current, limit = manager.check_resource_limit(tenant_id, resource)
|
||||||
|
print(f" - {resource}: {current}/{limit} ({'✅' if allowed else '❌'})")
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup(tenant_id: str, domain_id: str, member_ids: list):
|
||||||
|
"""清理测试数据"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("清理测试数据")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
|
# 移除成员
|
||||||
|
for member_id in member_ids:
|
||||||
|
if member_id:
|
||||||
|
manager.remove_member(tenant_id, member_id)
|
||||||
|
print(f"✅ 成员已移除: {member_id}")
|
||||||
|
|
||||||
|
# 移除域名
|
||||||
|
if domain_id:
|
||||||
|
manager.remove_domain(tenant_id, domain_id)
|
||||||
|
print(f"✅ 域名已移除: {domain_id}")
|
||||||
|
|
||||||
|
# 删除租户
|
||||||
|
manager.delete_tenant(tenant_id)
|
||||||
|
print(f"✅ 租户已删除: {tenant_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主测试函数"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("InsightFlow Phase 8 Task 1 - 多租户 SaaS 架构测试")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
tenant_id = None
|
||||||
|
domain_id = None
|
||||||
|
member_ids = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 运行所有测试
|
||||||
|
tenant_id = test_tenant_management()
|
||||||
|
domain_id = test_domain_management(tenant_id)
|
||||||
|
test_branding_management(tenant_id)
|
||||||
|
m1, m2 = test_member_management(tenant_id)
|
||||||
|
member_ids = [m1, m2]
|
||||||
|
test_usage_tracking(tenant_id)
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("✅ 所有测试通过!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 测试失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 清理
|
||||||
|
if tenant_id:
|
||||||
|
try:
|
||||||
|
cleanup(tenant_id, domain_id, member_ids)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 清理失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
507
backend/test_tenant.py
Normal file
507
backend/test_tenant.py
Normal 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())
|
||||||
Reference in New Issue
Block a user