第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">&times;</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)">&times;</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 基本使用

  1. 创建租户

    • 登录管理员账户
    • 进入租户管理页面
    • 点击"创建租户"按钮
    • 填写租户信息和配额
    • 点击"保存"按钮
  2. 访问租户应用

    • 通过URL访问租户应用:https://platform.com/tenant-slug/apps
    • 或通过自定义域名访问:https://tenant.example.com/apps
  3. 管理租户

    • 查看租户信息和状态
    • 编辑租户配置和配额
    • 暂停/激活租户
    • 删除租户

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 运营管理

  • 租户自助服务:提供租户自助服务门户
  • 计费系统:集成计费系统,实现按使用量计费
  • 统计分析:提供租户使用情况的统计分析
  • 客户支持:为租户提供技术支持和培训

六、总结

多租户架构是低代码平台的重要组成部分,它允许平台提供商高效地管理多个客户,降低运维成本,同时确保租户之间的数据隔离和安全性。在本集中,我们学习了:

  1. 多租户架构的核心概念和模式
  2. 低代码平台多租户的需求和挑战
  3. 多租户数据模型设计
  4. 租户管理服务的实现
  5. 租户上下文管理
  6. 多租户路由设计
  7. 租户布局和主题定制
  8. 租户管理可视化组件
  9. 多租户使用示例
  10. 性能优化策略
  11. 最佳实践

通过合理设计和实现多租户架构,我们可以构建出高效、安全、可扩展的低代码平台,为多个租户提供优质的服务。

在下一集中,我们将学习如何实现低代码平台的性能与扩展性优化,确保平台在高并发环境下的稳定运行。
```

« 上一篇 Vue 3低代码平台版本管理与回滚 下一篇 » Vue 3低代码平台性能与扩展性优化