第278集:Vue 3低代码平台多租户架构设计
一、多租户架构概述
多租户架构是一种软件架构模式,允许单个软件实例同时为多个独立的租户(组织或用户组)提供服务。在低代码平台中,多租户架构可以帮助平台提供商高效地管理多个客户,降低运维成本,同时确保租户之间的数据隔离和安全性。
1.1 核心概念
- 租户(Tenant):使用低代码平台的组织或用户组,每个租户拥有独立的数据和配置
- 实例(Instance):运行低代码平台的软件实例
- 数据隔离:确保不同租户的数据相互隔离,不能互相访问
- 资源共享:多个租户共享同一套软件实例和硬件资源
- 自定义配置:每个租户可以有自己的自定义配置和主题
1.2 多租户架构模式
1.2.1 共享数据库,共享Schema
- 特点:所有租户共享同一个数据库和数据表,通过租户ID字段区分不同租户的数据
- 优点:部署简单,资源利用率高,成本低
- 缺点:数据隔离性较差,查询性能可能受影响,迁移和备份复杂
1.2.2 共享数据库,独立Schema
- 特点:所有租户共享同一个数据库,但每个租户有独立的Schema(数据表集合)
- 优点:数据隔离性较好,查询性能较高,迁移和备份相对简单
- 缺点:部署相对复杂,资源利用率中等
1.2.3 独立数据库
- 特点:每个租户有独立的数据库
- 优点:数据隔离性最好,查询性能最高,迁移和备份简单,故障隔离
- 缺点:部署复杂,资源利用率低,成本高
1.3 低代码平台多租户需求
- 数据隔离:确保不同租户的数据完全隔离
- 资源隔离:限制每个租户的资源使用,防止单个租户影响其他租户
- 自定义能力:支持每个租户的自定义配置、主题和组件
- 扩展性:支持快速添加新租户
- 安全性:确保租户数据的安全性和隐私保护
- 可监控性:能够监控每个租户的使用情况和性能
二、核心实现
2.1 数据模型设计
// 租户信息接口
export interface Tenant {
id: string;
name: string; // 租户名称
slug: string; // 租户唯一标识(用于URL)
description?: string; // 租户描述
status: TenantStatus; // 租户状态
createdBy: string; // 创建人
createdAt: Date; // 创建时间
updatedAt: Date; // 更新时间
config: TenantConfig; // 租户配置
quota: TenantQuota; // 租户配额
domains?: string[]; // 租户自定义域名
}
// 租户状态枚举
export enum TenantStatus {
ACTIVE = 'active', // 激活
INACTIVE = 'inactive', // 未激活
SUSPENDED = 'suspended', // 暂停
DELETED = 'deleted' // 已删除
}
// 租户配置接口
export interface TenantConfig {
theme: string; // 主题
logo?: string; // 租户Logo
favicon?: string; // 租户图标
colors?: {
primary: string;
secondary: string;
accent: string;
};
features: {
[key: string]: boolean;
};
customCSS?: string; // 自定义CSS
customJS?: string; // 自定义JavaScript
}
// 租户配额接口
export interface TenantQuota {
maxApps: number; // 最大应用数量
maxComponents: number; // 最大组件数量
maxDataSources: number; // 最大数据源数量
maxUsers: number; // 最大用户数量
storageQuota: number; // 存储配额(MB)
apiRateLimit: number; // API速率限制(每分钟请求数)
}
// 租户用户接口
export interface TenantUser {
id: string;
tenantId: string; // 租户ID
userId: string; // 用户ID
role: TenantRole; // 租户内角色
status: UserStatus; // 用户状态
createdAt: Date; // 创建时间
updatedAt: Date; // 更新时间
}
// 租户角色枚举
export enum TenantRole {
ADMIN = 'admin', // 管理员
DEVELOPER = 'developer', // 开发者
DESIGNER = 'designer', // 设计师
VIEWER = 'viewer' // 查看者
}
// 用户状态枚举
export enum UserStatus {
ACTIVE = 'active', // 激活
INACTIVE = 'inactive', // 未激活
SUSPENDED = 'suspended', // 暂停
DELETED = 'deleted' // 已删除
}
// 应用信息接口(增加租户ID字段)
export interface App {
id: string;
tenantId: string; // 租户ID
name: string;
description?: string;
status: AppStatus;
createdBy: string;
createdAt: Date;
updatedAt: Date;
// 其他应用字段...
}2.2 租户管理服务
import type { Tenant, TenantStatus, TenantConfig, TenantQuota, TenantUser } from './types';
// 租户管理服务
export class TenantManager {
private tenants: Map<string, Tenant> = new Map();
private tenantUsers: Map<string, TenantUser[]> = new Map();
private storageKey = 'lowcode_tenants';
private tenantUsersKey = 'lowcode_tenant_users';
constructor() {
this.loadFromStorage();
}
// 创建租户
createTenant(name: string, slug: string, createdBy: string, config?: Partial<TenantConfig>, quota?: Partial<TenantQuota>): Tenant {
// 检查slug是否已存在
if (Array.from(this.tenants.values()).some(t => t.slug === slug)) {
throw new Error(`租户标识"${slug}"已存在`);
}
const newTenant: Tenant = {
id: `tenant_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
name,
slug,
description: '',
status: TenantStatus.ACTIVE,
createdBy,
createdAt: new Date(),
updatedAt: new Date(),
config: {
theme: 'default',
features: {},
...config
},
quota: {
maxApps: 10,
maxComponents: 100,
maxDataSources: 20,
maxUsers: 50,
storageQuota: 1024, // 1GB
apiRateLimit: 1000,
...quota
},
domains: []
};
this.tenants.set(newTenant.id, newTenant);
this.tenantUsers.set(newTenant.id, []);
this.saveToStorage();
return newTenant;
}
// 获取所有租户
getTenants(): Tenant[] {
return Array.from(this.tenants.values()).filter(t => t.status !== TenantStatus.DELETED);
}
// 根据ID获取租户
getTenantById(id: string): Tenant | undefined {
const tenant = this.tenants.get(id);
return tenant?.status !== TenantStatus.DELETED ? tenant : undefined;
}
// 根据slug获取租户
getTenantBySlug(slug: string): Tenant | undefined {
return Array.from(this.tenants.values()).find(t => t.slug === slug && t.status !== TenantStatus.DELETED);
}
// 更新租户信息
updateTenant(id: string, updates: Partial<Tenant>): Tenant | undefined {
const tenant = this.tenants.get(id);
if (!tenant || tenant.status === TenantStatus.DELETED) {
return undefined;
}
const updatedTenant = {
...tenant,
...updates,
updatedAt: new Date()
};
this.tenants.set(id, updatedTenant);
this.saveToStorage();
return updatedTenant;
}
// 更新租户状态
updateTenantStatus(id: string, status: TenantStatus): boolean {
const tenant = this.tenants.get(id);
if (!tenant) {
return false;
}
tenant.status = status;
tenant.updatedAt = new Date();
this.saveToStorage();
return true;
}
// 删除租户(软删除)
deleteTenant(id: string): boolean {
return this.updateTenantStatus(id, TenantStatus.DELETED);
}
// 添加租户用户
addTenantUser(tenantId: string, userId: string, role: string): TenantUser {
const tenant = this.tenants.get(tenantId);
if (!tenant || tenant.status === TenantStatus.DELETED) {
throw new Error('租户不存在或已删除');
}
const tenantUsers = this.tenantUsers.get(tenantId) || [];
// 检查用户是否已在租户中
if (tenantUsers.some(tu => tu.userId === userId)) {
throw new Error('用户已在该租户中');
}
const newTenantUser: TenantUser = {
id: `tenant_user_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
tenantId,
userId,
role: role as any,
status: UserStatus.ACTIVE,
createdAt: new Date(),
updatedAt: new Date()
};
tenantUsers.push(newTenantUser);
this.tenantUsers.set(tenantId, tenantUsers);
this.saveToStorage();
return newTenantUser;
}
// 获取租户用户列表
getTenantUsers(tenantId: string): TenantUser[] {
return this.tenantUsers.get(tenantId) || [];
}
// 从本地存储加载数据
private loadFromStorage(): void {
// 加载租户数据
const savedTenants = localStorage.getItem(this.storageKey);
if (savedTenants) {
try {
const data = JSON.parse(savedTenants);
for (const [id, tenant] of Object.entries(data)) {
this.tenants.set(id, {
...tenant as any,
createdAt: new Date(tenant.createdAt),
updatedAt: new Date(tenant.updatedAt)
});
}
} catch (e) {
console.error('Failed to load tenants from storage:', e);
}
}
// 加载租户用户数据
const savedTenantUsers = localStorage.getItem(this.tenantUsersKey);
if (savedTenantUsers) {
try {
const data = JSON.parse(savedTenantUsers);
for (const [tenantId, users] of Object.entries(data)) {
this.tenantUsers.set(tenantId, (users as any[]).map(user => ({
...user,
createdAt: new Date(user.createdAt),
updatedAt: new Date(user.updatedAt)
})));
}
} catch (e) {
console.error('Failed to load tenant users from storage:', e);
}
}
}
// 保存数据到本地存储
private saveToStorage(): void {
localStorage.setItem(this.storageKey, JSON.stringify(Object.fromEntries(this.tenants)));
localStorage.setItem(this.tenantUsersKey, JSON.stringify(Object.fromEntries(this.tenantUsers)));
}
}2.3 租户上下文管理
import type { Tenant } from './types';
import { TenantManager } from './services/TenantManager';
// 租户上下文类
export class TenantContext {
private static currentTenant: Tenant | null = null;
private static tenantManager: TenantManager = new TenantManager();
// 设置当前租户
static setCurrentTenant(tenant: Tenant): void {
this.currentTenant = tenant;
}
// 获取当前租户
static getCurrentTenant(): Tenant | null {
return this.currentTenant;
}
// 清除当前租户
static clearCurrentTenant(): void {
this.currentTenant = null;
}
// 根据请求获取租户(从URL、域名或请求头中获取)
static async getTenantFromRequest(request: any): Promise<Tenant | null> {
// 从URL路径获取租户slug(如:https://platform.com/tenant-slug/apps)
const urlPath = request.url || '';
const pathParts = urlPath.split('/').filter(part => part);
let tenantSlug: string | undefined;
// 假设URL格式为:/tenant-slug/...
if (pathParts.length > 0) {
tenantSlug = pathParts[0];
}
// 或者从请求头获取
if (!tenantSlug) {
tenantSlug = request.headers?.['x-tenant-slug'];
}
// 或者从域名获取(如:tenant-slug.platform.com)
if (!tenantSlug && request.headers?.host) {
const host = request.headers.host;
const subdomain = host.split('.')[0];
if (subdomain && subdomain !== 'www' && subdomain !== 'platform') {
tenantSlug = subdomain;
}
}
if (tenantSlug) {
const tenant = this.tenantManager.getTenantBySlug(tenantSlug);
if (tenant) {
this.setCurrentTenant(tenant);
return tenant;
}
}
return null;
}
// 检查当前租户是否有权限访问资源
static hasPermission(resource: string, action: string): boolean {
const tenant = this.getCurrentTenant();
if (!tenant) {
return false;
}
// 实现权限检查逻辑
// 这里可以根据租户配置和用户角色进行权限检查
return true;
}
// 获取租户配额信息
static getTenantQuota(): any {
const tenant = this.getCurrentTenant();
return tenant?.quota || null;
}
}2.4 多租户路由设计
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { TenantContext } from './TenantContext';
// 路由配置
const routes: RouteRecordRaw[] = [
// 公共路由(不需要租户上下文)
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/register',
name: 'Register',
component: () => import('../views/Register.vue')
},
// 租户路由(需要租户上下文)
{
path: '/:tenantSlug',
name: 'TenantRoot',
component: () => import('../layouts/TenantLayout.vue'),
beforeEnter: async (to, from, next) => {
// 从URL参数获取租户slug
const tenantSlug = to.params.tenantSlug as string;
const tenantManager = (window as any).tenantManager || new (await import('./services/TenantManager')).TenantManager();
// 获取租户信息
const tenant = tenantManager.getTenantBySlug(tenantSlug);
if (tenant) {
// 设置当前租户上下文
TenantContext.setCurrentTenant(tenant);
next();
} else {
// 租户不存在,跳转到错误页面
next({ name: 'NotFound' });
}
},
children: [
{
path: 'dashboard',
name: 'TenantDashboard',
component: () => import('../views/tenant/Dashboard.vue')
},
{
path: 'apps',
name: 'TenantApps',
component: () => import('../views/tenant/Apps.vue')
},
{
path: 'apps/:appId',
name: 'TenantAppDetail',
component: () => import('../views/tenant/AppDetail.vue')
},
{
path: 'components',
name: 'TenantComponents',
component: () => import('../views/tenant/Components.vue')
},
{
path: 'datasources',
name: 'TenantDataSources',
component: () => import('../views/tenant/DataSources.vue')
},
{
path: 'settings',
name: 'TenantSettings',
component: () => import('../views/tenant/Settings.vue')
}
]
},
// 404页面
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound.vue')
}
];
// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;2.5 租户布局组件
实现租户专用的布局组件,根据租户配置动态加载主题和样式:
<template>
<div class="tenant-layout" :class="`theme-${tenantConfig.theme}`" :style="tenantStyles">
<!-- 租户Logo和导航 -->
<header class="tenant-header">
<div class="header-left">
<img
v-if="tenantConfig.logo"
:src="tenantConfig.logo"
:alt="tenant?.name"
class="tenant-logo"
/>
<h1 class="tenant-name">{{ tenant?.name }}</h1>
</div>
<nav class="header-nav">
<router-link to="dashboard" class="nav-item">仪表盘</router-link>
<router-link to="apps" class="nav-item">应用管理</router-link>
<router-link to="components" class="nav-item">组件库</router-link>
<router-link to="datasources" class="nav-item">数据源</router-link>
<router-link to="settings" class="nav-item">设置</router-link>
</nav>
<div class="header-right">
<!-- 用户菜单 -->
</div>
</header>
<!-- 主要内容区域 -->
<main class="tenant-main">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</main>
<!-- 租户页脚 -->
<footer class="tenant-footer">
<p>{{ tenant?.name }} - Vue 3低代码平台</p>
</footer>
<!-- 动态加载租户自定义CSS -->
<style v-if="tenantConfig.customCSS" scoped>
{{ tenantConfig.customCSS }}
</style>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue';
import { TenantContext } from '../utils/TenantContext';
// 获取当前租户信息
const tenant = computed(() => TenantContext.getCurrentTenant());
// 获取租户配置
const tenantConfig = computed(() => tenant.value?.config || {
theme: 'default',
features: {}
});
// 计算租户样式
const tenantStyles = computed(() => {
const colors = tenantConfig.value.colors;
if (colors) {
return {
'--primary-color': colors.primary,
'--secondary-color': colors.secondary,
'--accent-color': colors.accent
};
}
return {};
});
// 加载租户自定义JavaScript
onMounted(() => {
if (tenantConfig.value.customJS) {
try {
// 动态执行租户自定义JS(实际项目中应进行安全验证)
// eslint-disable-next-line no-eval
eval(tenantConfig.value.customJS);
} catch (error) {
console.error('Failed to execute tenant custom JS:', error);
}
}
});
// 清理资源
onUnmounted(() => {
// 清理租户相关资源
});
</script>
<style scoped>
.tenant-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
font-family: Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
transition: all 0.3s ease;
}
/* 主题变量 */
.tenant-layout {
--primary-color: #1976d2;
--secondary-color: #424242;
--accent-color: #ff9800;
--background-color: #f5f5f5;
--header-background: white;
--footer-background: #fafafa;
--text-color: #333;
--border-color: #e0e0e0;
}
/* 头部样式 */
.tenant-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
background-color: var(--header-background);
border-bottom: 1px solid var(--border-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
height: 64px;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.tenant-logo {
height: 40px;
width: auto;
}
.tenant-name {
font-size: 20px;
font-weight: 600;
color: var(--primary-color);
margin: 0;
}
/* 导航样式 */
.header-nav {
display: flex;
gap: 24px;
}
.nav-item {
text-decoration: none;
color: var(--text-color);
font-size: 14px;
font-weight: 500;
padding: 8px 0;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
}
.nav-item:hover, .nav-item.router-link-active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
/* 主要内容区域 */
.tenant-main {
flex: 1;
padding: 20px;
background-color: var(--background-color);
}
/* 页脚样式 */
.tenant-footer {
padding: 16px 20px;
background-color: var(--footer-background);
border-top: 1px solid var(--border-color);
text-align: center;
font-size: 14px;
color: #666;
}
/* 过渡动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>2.6 租户管理组件
实现租户管理的可视化组件,用于管理租户信息、用户和配额:
<template>
<div class="tenant-management">
<div class="management-header">
<h2>租户管理</h2>
<button class="create-btn" @click="showCreateModal = true">创建租户</button>
</div>
<!-- 租户列表 -->
<div class="tenant-list">
<div class="list-header">
<div class="header-item">租户名称</div>
<div class="header-item">租户标识</div>
<div class="header-item">状态</div>
<div class="header-item">创建时间</div>
<div class="header-item">操作</div>
</div>
<div
v-for="tenant in tenants"
:key="tenant.id"
class="tenant-item"
>
<div class="item-cell">{{ tenant.name }}</div>
<div class="item-cell">{{ tenant.slug }}</div>
<div class="item-cell">
<span class="status-badge" :class="tenant.status">{{ getStatusText(tenant.status) }}</span>
</div>
<div class="item-cell">{{ formatDate(tenant.createdAt) }}</div>
<div class="item-cell actions">
<button class="action-btn view-btn" @click="viewTenant(tenant)">查看</button>
<button class="action-btn edit-btn" @click="editTenant(tenant)">编辑</button>
<button class="action-btn" :class="tenant.status === 'active' ? 'suspend-btn' : 'activate-btn'" @click="toggleTenantStatus(tenant)">
{{ tenant.status === 'active' ? '暂停' : '激活' }}
</button>
<button class="action-btn delete-btn" @click="deleteTenant(tenant)">删除</button>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="tenants.length === 0">
<p>暂无租户,请点击"创建租户"按钮创建第一个租户</p>
</div>
</div>
<!-- 创建/编辑租户模态框 -->
<div class="modal" v-if="showCreateModal || showEditModal">
<div class="modal-overlay" @click="closeModal"></div>
<div class="modal-content large">
<div class="modal-header">
<h3>{{ showEditModal ? '编辑租户' : '创建租户' }}</h3>
<button class="close-btn" @click="closeModal">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="saveTenant">
<div class="form-row">
<div class="form-group">
<label>租户名称 *</label>
<input
type="text"
v-model="formData.name"
required
placeholder="请输入租户名称"
/>
</div>
<div class="form-group">
<label>租户标识 *</label>
<input
type="text"
v-model="formData.slug"
required
placeholder="请输入租户唯一标识(用于URL)"
:disabled="showEditModal"
/>
</div>
</div>
<div class="form-group">
<label>租户描述</label>
<textarea
v-model="formData.description"
placeholder="请输入租户描述"
rows="3"
></textarea>
</div>
<div class="form-section">
<h4>租户配额</h4>
<div class="form-row">
<div class="form-group">
<label>最大应用数量</label>
<input
type="number"
v-model.number="formData.quota.maxApps"
min="1"
/>
</div>
<div class="form-group">
<label>最大组件数量</label>
<input
type="number"
v-model.number="formData.quota.maxComponents"
min="1"
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>最大数据源数量</label>
<input
type="number"
v-model.number="formData.quota.maxDataSources"
min="1"
/>
</div>
<div class="form-group">
<label>最大用户数量</label>
<input
type="number"
v-model.number="formData.quota.maxUsers"
min="1"
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>存储配额(MB)</label>
<input
type="number"
v-model.number="formData.quota.storageQuota"
min="100"
/>
</div>
<div class="form-group">
<label>API速率限制</label>
<input
type="number"
v-model.number="formData.quota.apiRateLimit"
min="100"
/>
</div>
</div>
</div>
<div class="form-section">
<h4>自定义域名</h4>
<div class="domain-inputs">
<div
v-for="(domain, index) in formData.domains"
:key="index"
class="domain-item"
>
<input
type="text"
v-model="formData.domains[index]"
placeholder="输入域名(如:tenant.example.com)"
/>
<button type="button" class="remove-domain-btn" @click="removeDomain(index)">×</button>
</div>
<button type="button" class="add-domain-btn" @click="addDomain">+ 添加域名</button>
</div>
</div>
<div class="form-actions">
<button type="button" class="cancel-btn" @click="closeModal">取消</button>
<button type="submit" class="save-btn" :disabled="isSaving">
{{ isSaving ? '保存中...' : '保存' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import type { Tenant, TenantStatus, TenantConfig, TenantQuota } from './types';
import { TenantManager } from './services/TenantManager';
import { TenantStatus as TenantStatusEnum } from './types';
// 租户管理器实例
const tenantManager = new TenantManager();
// 租户列表
const tenants = ref<Tenant[]>([]);
// 显示创建租户模态框
const showCreateModal = ref(false);
// 显示编辑租户模态框
const showEditModal = ref(false);
// 是否正在保存
const isSaving = ref(false);
// 当前编辑的租户
const currentTenant = ref<Tenant | null>(null);
// 表单数据
const formData = reactive({
name: '',
slug: '',
description: '',
status: TenantStatusEnum.ACTIVE,
config: {
theme: 'default',
features: {}
} as TenantConfig,
quota: {
maxApps: 10,
maxComponents: 100,
maxDataSources: 20,
maxUsers: 50,
storageQuota: 1024,
apiRateLimit: 1000
} as TenantQuota,
domains: [] as string[]
});
// 初始化
function init() {
tenants.value = tenantManager.getTenants();
}
// 格式化日期
function formatDate(date: Date): string {
return new Date(date).toLocaleString('zh-CN');
}
// 获取状态文本
function getStatusText(status: TenantStatus): string {
const statusMap: Record<TenantStatus, string> = {
[TenantStatusEnum.ACTIVE]: '激活',
[TenantStatusEnum.INACTIVE]: '未激活',
[TenantStatusEnum.SUSPENDED]: '已暂停',
[TenantStatusEnum.DELETED]: '已删除'
};
return statusMap[status] || status;
}
// 查看租户
function viewTenant(tenant: Tenant) {
// 跳转到租户详情页面
console.log('View tenant:', tenant);
}
// 编辑租户
function editTenant(tenant: Tenant) {
currentTenant.value = tenant;
// 填充表单数据
formData.name = tenant.name;
formData.slug = tenant.slug;
formData.description = tenant.description || '';
formData.status = tenant.status;
formData.config = { ...tenant.config };
formData.quota = { ...tenant.quota };
formData.domains = [...(tenant.domains || [])];
showEditModal.value = true;
}
// 保存租户
async function saveTenant() {
try {
isSaving.value = true;
if (showEditModal.value && currentTenant.value) {
// 更新现有租户
tenantManager.updateTenant(currentTenant.value.id, {
name: formData.name,
description: formData.description,
quota: formData.quota,
domains: formData.domains.filter(d => d.trim())
});
} else {
// 创建新租户
tenantManager.createTenant(
formData.name,
formData.slug,
'admin',
formData.config,
formData.quota
);
}
// 刷新租户列表
init();
// 关闭模态框
closeModal();
alert('租户保存成功!');
} catch (error) {
console.error('Failed to save tenant:', error);
alert(`租户保存失败:${error instanceof Error ? error.message : '未知错误'}`);
} finally {
isSaving.value = false;
}
}
// 切换租户状态
function toggleTenantStatus(tenant: Tenant) {
const newStatus = tenant.status === TenantStatusEnum.ACTIVE
? TenantStatusEnum.SUSPENDED
: TenantStatusEnum.ACTIVE;
if (confirm(`确定要${newStatus === TenantStatusEnum.ACTIVE ? '激活' : '暂停'}租户"${tenant.name}"吗?`)) {
tenantManager.updateTenantStatus(tenant.id, newStatus);
init();
alert(`租户已${newStatus === TenantStatusEnum.ACTIVE ? '激活' : '暂停'}!`);
}
}
// 删除租户
function deleteTenant(tenant: Tenant) {
if (confirm(`确定要删除租户"${tenant.name}"吗?此操作不可恢复。`)) {
tenantManager.deleteTenant(tenant.id);
init();
alert('租户已删除!');
}
}
// 添加域名
function addDomain() {
formData.domains.push('');
}
// 删除域名
function removeDomain(index: number) {
formData.domains.splice(index, 1);
}
// 关闭模态框
function closeModal() {
showCreateModal.value = false;
showEditModal.value = false;
currentTenant.value = null;
// 重置表单
Object.assign(formData, {
name: '',
slug: '',
description: '',
status: TenantStatusEnum.ACTIVE,
config: {
theme: 'default',
features: {}
},
quota: {
maxApps: 10,
maxComponents: 100,
maxDataSources: 20,
maxUsers: 50,
storageQuota: 1024,
apiRateLimit: 1000
},
domains: []
});
}
onMounted(() => {
init();
});
</script>
<style scoped>
.tenant-management {
padding: 20px;
font-family: Arial, sans-serif;
background-color: #f5f5f5;
min-height: 100vh;
}
/* 头部样式 */
.management-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
background-color: white;
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.management-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
.create-btn {
padding: 8px 16px;
background-color: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.create-btn:hover {
background-color: #1565c0;
}
/* 租户列表样式 */
.tenant-list {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.list-header {
display: flex;
background-color: #fafafa;
border-bottom: 1px solid #e0e0e0;
font-weight: 600;
color: #666;
font-size: 14px;
}
.header-item {
padding: 12px 16px;
flex: 1;
}
.header-item:last-child {
flex: 0 0 200px;
}
.tenant-item {
display: flex;
border-bottom: 1px solid #e0e0e0;
transition: all 0.3s ease;
}
.tenant-item:hover {
background-color: #fafafa;
}
.item-cell {
padding: 12px 16px;
flex: 1;
font-size: 14px;
color: #333;
align-self: center;
}
.item-cell.actions {
flex: 0 0 200px;
display: flex;
gap: 8px;
}
/* 状态徽章样式 */
.status-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-badge.active {
background-color: #d4edda;
color: #155724;
}
.status-badge.inactive,
.status-badge.suspended {
background-color: #fff3cd;
color: #856404;
}
.status-badge.deleted {
background-color: #f8d7da;
color: #721c24;
}
/* 操作按钮样式 */
.action-btn {
padding: 4px 8px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
}
.view-btn {
background-color: #2196f3;
color: white;
}
.view-btn:hover {
background-color: #1976d2;
}
.edit-btn {
background-color: #4caf50;
color: white;
}
.edit-btn:hover {
background-color: #388e3c;
}
.suspend-btn {
background-color: #ff9800;
color: white;
}
.suspend-btn:hover {
background-color: #f57c00;
}
.activate-btn {
background-color: #4caf50;
color: white;
}
.activate-btn:hover {
background-color: #388e3c;
}
.delete-btn {
background-color: #f44336;
color: white;
}
.delete-btn:hover {
background-color: #d32f2f;
}
/* 空状态样式 */
.empty-state {
padding: 40px 20px;
text-align: center;
color: #999;
}
/* 模态框样式 */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
width: 600px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.modal-content.large {
width: 800px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.3s ease;
}
.close-btn:hover {
background-color: #f5f5f5;
color: #333;
}
.modal-body {
padding: 20px;
}
/* 表单样式 */
.form-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.form-group {
flex: 1;
min-width: 0;
}
.form-section {
margin: 20px 0;
padding: 16px;
background-color: #fafafa;
border-radius: 8px;
}
.form-section h4 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #333;
font-size: 14px;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
transition: all 0.3s ease;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
}
.form-group input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
/* 域名输入样式 */
.domain-inputs {
display: flex;
flex-direction: column;
gap: 8px;
}
.domain-item {
display: flex;
gap: 8px;
align-items: center;
}
.remove-domain-btn {
background: none;
border: 1px solid #e0e0e0;
border-radius: 4px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 16px;
color: #666;
transition: all 0.3s ease;
}
.remove-domain-btn:hover {
background-color: #f44336;
color: white;
border-color: #f44336;
}
.add-domain-btn {
padding: 8px 12px;
background-color: #f5f5f5;
color: #333;
border: 1px dashed #e0e0e0;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
align-self: flex-start;
}
.add-domain-btn:hover {
background-color: #e0e0e0;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.cancel-btn, .save-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.cancel-btn {
background-color: #f5f5f5;
color: #333;
}
.cancel-btn:hover {
background-color: #e0e0e0;
}
.save-btn {
background-color: #1976d2;
color: white;
}
.save-btn:hover:not(:disabled) {
background-color: #1565c0;
}
.save-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 40px 20px;
color: #999;
}
</style>三、使用示例
3.1 基本使用
创建租户:
- 登录管理员账户
- 进入租户管理页面
- 点击"创建租户"按钮
- 填写租户信息和配额
- 点击"保存"按钮
访问租户应用:
- 通过URL访问租户应用:
https://platform.com/tenant-slug/apps - 或通过自定义域名访问:
https://tenant.example.com/apps
- 通过URL访问租户应用:
管理租户:
- 查看租户信息和状态
- 编辑租户配置和配额
- 暂停/激活租户
- 删除租户
3.2 租户自定义配置
每个租户可以自定义主题、颜色、Logo等:
// 租户配置示例
const tenantConfig = {
theme: 'dark',
logo: 'https://tenant.example.com/logo.png',
favicon: 'https://tenant.example.com/favicon.ico',
colors: {
primary: '#ff5722',
secondary: '#4caf50',
accent: '#2196f3'
},
features: {
advancedComponents: true,
customCode: false,
teamCollaboration: true
},
customCSS: '.tenant-header { background-color: #1a1a1a; }',
customJS: 'console.log("Custom JS for tenant");'
};3.3 多租户数据隔离
在应用开发中,确保所有数据操作都包含租户ID:
// 获取当前租户ID
const tenantId = TenantContext.getCurrentTenant()?.id;
// 创建应用时添加租户ID
const newApp = {
id: `app_${Date.now()}`,
tenantId,
name: '新应用',
// 其他应用字段...
};
// 查询应用时过滤租户ID
const apps = await appService.getApps({ tenantId });
// 删除应用时验证租户ID
await appService.deleteApp(appId, { tenantId });四、性能优化
4.1 租户数据缓存
- 租户信息缓存:将常用的租户信息缓存到内存中,减少数据库查询
- 配置缓存:缓存租户配置,避免每次请求都加载配置
- 查询缓存:为每个租户提供独立的查询缓存空间
4.2 资源隔离
- API速率限制:为每个租户设置API请求速率限制
- 存储配额:限制每个租户的存储使用量
- 计算资源隔离:使用容器化技术隔离不同租户的计算资源
4.3 数据库优化
- 索引优化:为租户ID字段添加索引,提高查询性能
- 分区表:对于大型表,按租户ID进行分区
- 读写分离:实现读写分离,提高数据库吞吐量
4.4 应用优化
- 懒加载:按需加载租户相关资源
- CDN加速:使用CDN加速静态资源访问
- 异步处理:将耗时操作异步化,提高响应速度
五、最佳实践
5.1 数据安全
- 严格的数据隔离:确保所有数据操作都包含租户ID过滤
- 加密存储:对敏感数据进行加密存储
- 访问控制:实现细粒度的访问控制,限制用户只能访问自己租户的数据
- 审计日志:记录所有重要操作的审计日志
5.2 租户管理
- 自动化部署:实现租户的自动化创建和部署
- 监控告警:监控每个租户的使用情况和性能指标
- 生命周期管理:实现租户的完整生命周期管理(创建、激活、暂停、删除)
- 配额管理:合理设置和管理租户配额
5.3 扩展性设计
- 模块化设计:采用模块化设计,便于添加新功能
- 插件系统:支持插件扩展,允许租户自定义功能
- API设计:设计RESTful API,支持外部系统集成
- 事件驱动:采用事件驱动架构,提高系统扩展性
5.4 运营管理
- 租户自助服务:提供租户自助服务门户
- 计费系统:集成计费系统,实现按使用量计费
- 统计分析:提供租户使用情况的统计分析
- 客户支持:为租户提供技术支持和培训
六、总结
多租户架构是低代码平台的重要组成部分,它允许平台提供商高效地管理多个客户,降低运维成本,同时确保租户之间的数据隔离和安全性。在本集中,我们学习了:
- 多租户架构的核心概念和模式
- 低代码平台多租户的需求和挑战
- 多租户数据模型设计
- 租户管理服务的实现
- 租户上下文管理
- 多租户路由设计
- 租户布局和主题定制
- 租户管理可视化组件
- 多租户使用示例
- 性能优化策略
- 最佳实践
通过合理设计和实现多租户架构,我们可以构建出高效、安全、可扩展的低代码平台,为多个租户提供优质的服务。
在下一集中,我们将学习如何实现低代码平台的性能与扩展性优化,确保平台在高并发环境下的稳定运行。
```