卓信SAAS平台

目录

项目介绍

为卓信ID、推必安业务开发的SAAS平台,包含开发者管理、服务商管理、SDK版本管理、权限管理、多重配置管理、设备命中策略等功能。

卓信SAAS平台-从微前端到BFF层的全方位技术架构升级

项目概述

卓信业务SaaS平台是一个集成了卓信ID反欺诈推必安推送内容审核两大核心业务的综合性平台。该平台主要为广告商、服务商等B端客户提供客制化的数据业务服务,需要具备高度的可扩展性、安全性和性能表现。

核心功能模块

  • 开发者管理-开发者账户管理、权限分配
  • 服务商管理-第三方服务商接入与管理
  • SDK版本管理-多版本SDK发布与维护
  • 权限管理-细粒度权限控制系统
  • 多重配置管理-灵活的配置管理机制
  • 设备命中策略-智能设备识别与策略配置

项目背景与挑战

在项目初期,我们面临着单体应用架构带来的一系列挑战-

核心痛点

问题类型具体表现影响程度
性能问题首屏加载时间长达7秒严重影响用户体验
架构问题不同业务模块耦合度高迭代效率低下
集成问题第三方服务商接入复杂API调用成本高
安全问题缺乏统一的权限控制租户隔离机制不完善

解决方案

为了解决这些问题,我们决定进行全面的技术架构升级,采用微前端BFF层等现代化技术方案。

技术栈选型

在项目重构过程中,我们选择了以下技术栈-

前端技术栈

技术类型选择方案选择理由
前端框架Nuxt.js 3 + Vue 3 + TypeScriptSSR支持,开发体验优秀
UI组件库ArcoDesign企业级组件库,定制性强
微前端框架Wujie (无界)性能优秀,Vue生态友好
构建工具Vite + Rollup快速构建,插件生态丰富
图表库ECharts功能强大,可视化效果好
测试工具Vitest与Vite集成度高

后端技术栈

  • 后端框架-NestJS - 企业级Node.js框架
  • 数据分析-Python - 强大的数据处理能力

一、微前端架构设计与实践

1.1 微前端选型分析

在微前端框架选型过程中,我们对比了主流的几种方案-

方案优点缺点适用场景
Single-spa轻量级,灵活性高配置复杂,需要手动处理样式隔离简单场景
Qiankun功能完善,开箱即用体积较大,对老项目兼容性一般,对Vue3的支持不友好中大型应用
Wujie性能优秀,API简洁生态相对较新现代前端项目

最终选择Wujie的主要原因-

  • 性能优势-基于Web Component和Shadow DOM,沙箱隔离更彻底
  • API简洁-接入成本低,学习曲线平缓
  • Nuxt.js友好-对Vue 3和Nuxt.js支持良好
  • 体积小巧-核心包体积小,对主应用性能影响小

1.2 微前端架构设计

我们采用了**“基座管理 + 子应用独立运行 + 共享模块按需加载”**的架构方案-

// 主应用微前端配置 (main.ts)
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { setupApp } from 'wujie'
import App from './App.vue'
import routes from './routes'
 
// 初始化无界微前端
setupApp({
  preloadApps: ['user', 'payment', 'data'],
  lifeCycles: {
    beforeLoad: (app) => {
      console.log('before load:', app.name)
    },
    afterMount: (app) => {
      console.log('after mount:', app.name)
    }
  }
})
 
const router = createRouter({
  history: createWebHistory(),
  routes
})
 
createApp(App)
  .use(router)
  .mount('#app')

1.3 子应用注册与配置

// 子应用注册配置 (apps.ts)
export const microApps = [
  {
    name: 'user',
    entry: import.meta.env.VITE_USER_APP_URL,
    container: '#micro-user',
    activeRule: '/user',
    props: {
      baseRoute: '/user'
    }
  },
  {
    name: 'payment',
    entry: import.meta.env.VITE_PAYMENT_APP_URL,
    container: '#micro-payment',
    activeRule: '/payment',
    props: {
      baseRoute: '/payment'
    }
  },
  {
    name: 'data',
    entry: import.meta.env.VITE_DATA_APP_URL,
    container: '#micro-data',
    activeRule: '/data',
    props: {
      baseRoute: '/data'
    }
  }
]

1.4 沙箱通信机制实现

为了保障数据安全和通信效率,我们设计了一套完整的沙箱通信机制-

// 主应用通信工具 (event-bus.ts)
import { EventEmitter } from 'events'
 
class MicroAppEventBus extends EventEmitter {
  constructor() {
    super()
    this.setMaxListeners(100)
  }
 
  // 主应用发送消息到子应用
  sendToApp(appName: string, eventName: string, data: any) {
    this.emit(`${appName}:${eventName}`, data)
  }
 
  // 子应用发送消息到主应用
  sendToMain(eventName: string, data: any) {
    this.emit(`main:${eventName}`, data)
  }
 
  // 主应用监听子应用消息
  onMainEvent(eventName: string, callback: (...args: any[]) => void) {
    this.on(`main:${eventName}`, callback)
  }
 
  // 子应用监听主应用消息
  onAppEvent(appName: string, eventName: string, callback: (...args: any[]) => void) {
    this.on(`${appName}:${eventName}`, callback)
  }
}
 
export const eventBus = new MicroAppEventBus()

1.5 共享模块设计

为了避免代码重复和资源浪费,我们设计了共享模块机制-

// 共享模块配置 (shared.ts)
export const sharedModules = {
  'vue': () => import('vue'),
  'vue-router': () => import('vue-router'),
  'pinia': () => import('pinia'),
  '@arco-design/web-vue': () => import('@arco-design/web-vue'),
  'axios': () => import('axios')
}
 
// 主应用共享模块注册
setupApp({
  share: {
    deps: sharedModules,
    scope: 'global'
  }
})

1.5.1 共享API接口设计

为了统一管理各个子应用的API调用,我们设计了共享API接口机制-

// 共享API服务 (shared-api.ts)
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
import { eventBus } from './event-bus'
 
class SharedApiService {
  private axiosInstance: AxiosInstance
  private baseURL: string
  private tenantId: string
 
  constructor() {
    this.baseURL = import.meta.env.VITE_API_BASE_URL
    this.axiosInstance = axios.create({
      baseURL: this.baseURL,
      timeout: 10000
    })
 
    this.setupInterceptors()
  }
 
  // 设置租户ID
  setTenantId(tenantId: string) {
    this.tenantId = tenantId
  }
 
  // 请求拦截器
  private setupInterceptors() {
    this.axiosInstance.interceptors.request.use(
      (config) => {
        // 添加租户头信息
        if (this.tenantId) {
          config.headers['X-Tenant-Id'] = this.tenantId
        }
 
        // 添加认证token
        const token = localStorage.getItem('access_token')
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
 
        return config
      },
      (error) => Promise.reject(error)
    )
 
    // 响应拦截器
    this.axiosInstance.interceptors.response.use(
      (response) => response,
      (error) => {
        if (error.response?.status === 401) {
          // 通知所有子应用token过期
          eventBus.emit('auth:token-expired')
        }
        return Promise.reject(error)
      }
    )
  }
 
  // 通用API方法
  async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.axiosInstance.get(url, config)
    return response.data
  }
 
  async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.axiosInstance.post(url, data, config)
    return response.data
  }
 
  async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.axiosInstance.put(url, data, config)
    return response.data
  }
 
  async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.axiosInstance.delete(url, config)
    return response.data
  }
}
 
// 导出单例实例
export const sharedApi = new SharedApiService()

1.5.2 业务API模块封装

// 用户相关API (shared-user-api.ts)
import { sharedApi } from './shared-api'
 
export interface User {
  id: string
  name: string
  email: string
  role: string
  tenantId: string
}
 
export interface UserListParams {
  page?: number
  pageSize?: number
  keyword?: string
  role?: string
}
 
export class UserApiService {
  // 获取用户列表
  static async getUserList(params: UserListParams = {}) {
    return sharedApi.get<{
      data: User[]
      total: number
      page: number
      pageSize: number
    }>('/api/users', { params })
  }
 
  // 获取用户详情
  static async getUserDetail(userId: string) {
    return sharedApi.get<User>(`/api/users/${userId}`)
  }
 
  // 创建用户
  static async createUser(userData: Omit<User, 'id'>) {
    return sharedApi.post<User>('/api/users', userData)
  }
 
  // 更新用户
  static async updateUser(userId: string, userData: Partial<User>) {
    return sharedApi.put<User>(`/api/users/${userId}`, userData)
  }
 
  // 删除用户
  static async deleteUser(userId: string) {
    return sharedApi.delete(`/api/users/${userId}`)
  }
}

1.5.3 权限管理API

// 权限相关API (shared-permission-api.ts)
import { sharedApi } from './shared-api'
 
export interface Permission {
  id: string
  name: string
  code: string
  resource: string
  action: string
}
 
export interface Role {
  id: string
  name: string
  code: string
  permissions: Permission[]
}
 
export class PermissionApiService {
  // 获取当前用户权限
  static async getCurrentUserPermissions() {
    return sharedApi.get<Permission[]>('/api/auth/permissions')
  }
 
  // 获取角色列表
  static async getRoleList() {
    return sharedApi.get<Role[]>('/api/roles')
  }
 
  // 检查权限
  static async checkPermission(resource: string, action: string) {
    return sharedApi.post<{ hasPermission: boolean }>('/api/auth/check-permission', {
      resource,
      action
    })
  }
 
  // 获取用户角色
  static async getUserRoles(userId: string) {
    return sharedApi.get<Role[]>(`/api/users/${userId}/roles`)
  }
}

1.5.4 子应用API使用示例

// 子应用中使用共享API (user-management-app.vue)
<template>
  <div class="user-management">
    <a-table 
      :columns="columns" 
      :data="userList" 
      :loading="loading"
      @page-change="handlePageChange"
    >
      <template #action="{ record }">
        <a-button @click="editUser(record)">编辑</a-button>
        <a-button status="danger" @click="deleteUser(record.id)">删除</a-button>
      </template>
    </a-table>
  </div>
</template>
 
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { UserApiService, type User } from '@shared/user-api'
import { PermissionApiService } from '@shared/permission-api'
 
const userList = ref<User[]>([])
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
 
// 获取用户列表
const fetchUserList = async () => {
  loading.value = true
  try {
    const response = await UserApiService.getUserList({
      page: currentPage.value,
      pageSize: pageSize.value
    })
    userList.value = response.data
  } catch (error) {
    console.error('获取用户列表失败:', error)
  } finally {
    loading.value = false
  }
}
 
// 编辑用户
const editUser = async (user: User) => {
  // 检查编辑权限
  const permissionCheck = await PermissionApiService.checkPermission('user', 'update')
  if (!permissionCheck.hasPermission) {
    console.warn('没有编辑用户的权限')
    return
  }
  
  // 执行编辑逻辑
  console.log('编辑用户:', user)
}
 
// 删除用户
const deleteUser = async (userId: string) => {
  // 检查删除权限
  const permissionCheck = await PermissionApiService.checkPermission('user', 'delete')
  if (!permissionCheck.hasPermission) {
    console.warn('没有删除用户的权限')
    return
  }
 
  try {
    await UserApiService.deleteUser(userId)
    await fetchUserList() // 重新获取列表
  } catch (error) {
    console.error('删除用户失败:', error)
  }
}
 
// 分页处理
const handlePageChange = (page: number) => {
  currentPage.value = page
  fetchUserList()
}
 
onMounted(() => {
  fetchUserList()
})
</script>

二、Vite定制构建流程优化

2.1 Vite插件开发

为了满足项目的特殊需求,我们开发了多个Vite插件-

// 自定义Vite插件 (vite-plugin-custom.ts)
import { Plugin } from 'vite'
 
export function customVitePlugin(options: any = {}): Plugin {
  return {
    name: 'custom-vite-plugin',
    configResolved(config) {
      // 配置解析完成后的处理
      console.log('Vite config resolved')
    },
    buildStart(opts) {
      // 构建开始时的处理
      console.log('Build started')
    },
    transform(code, id) {
      // 代码转换
      if (id.endsWith('.special')) {
        return {
          code: transformSpecialFile(code),
          map: null
        }
      }
    },
    generateBundle(options, bundle) {
      // 生成bundle后的处理
      console.log('Bundle generated')
    }
  }
}
 
function transformSpecialFile(code: string): string {
  // 自定义文件转换逻辑
  return code.replace(/{{(\w+)}}/g, (_, key) => {
    return process.env[key] || ''
  })
}

2.2 资源加载策略优化

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { customVitePlugin } from './plugins/custom-vite-plugin'
 
export default defineConfig({
  plugins: [
    vue(),
    customVitePlugin()
  ],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // 精细化代码分割
          vendor: ['vue', 'vue-router', 'pinia'],
          arco: ['@arco-design/web-vue'],
          utils: ['lodash-es', 'dayjs'],
          charts: ['echarts']
        },
        chunkFileNames: (chunkInfo) => {
          const facadeModuleId = chunkInfo.facadeModuleId
          if (facadeModuleId) {
            if (facadeModuleId.includes('node_modules')) {
              return 'vendor/[name].[hash].js'
            }
            return 'js/[name].[hash].js'
          }
          return 'js/[name].[hash].js'
        }
      }
    }
  },
  optimizeDeps: {
    // 预构建优化
    include: [
      'vue',
      'vue-router',
      'pinia',
      '@arco-design/web-vue',
      'axios'
    ],
    exclude: ['@my-lib/shared']
  }
})

2.3 预加载策略配置

// 预加载插件 (vite-plugin-preload.ts)
import { Plugin } from 'vite'
 
export function preloadPlugin(): Plugin {
  return {
    name: 'preload-plugin',
    generateBundle(options, bundle) {
      const htmlFiles = Object.entries(bundle).filter(
        ([fileName]) => fileName.endsWith('.html')
      )
 
      htmlFiles.forEach(([fileName, chunk]) => {
        if (chunk.type === 'asset' && typeof chunk.source === 'string') {
          // 添加预加载链接
          const preloadLinks = `
            <link rel="preload" href="/vendor/vendor.[hash].js" as="script">
            <link rel="preload" href="/vendor/arco.[hash].js" as="script">
            <link rel="preload" href="/css/main.[hash].css" as="style">
          `
          chunk.source = chunk.source.replace(
            '</head>',
            `${preloadLinks}</head>`
          )
        }
      })
    }
  }
}

2.4 Tree-shaking优化

package.json配置-

{
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/polyfills.js"
  ]
}

vite.config.ts中启用生产环境优化-

export default defineConfig({
  build: {
    minify: 'esbuild',
    target: 'es2015',
    rollupOptions: {
      output: {
        compact: true
      }
    }
  }
})

三、NestJS BFF层架构设计

3.1 BFF层架构概述

BFF(Backend For Frontend)层作为前端和后端服务之间的中间层,主要承担以下职责-

核心职责

职责类型具体功能业务价值
请求转发将前端请求转发到对应的微服务简化前端调用复杂度
数据聚合聚合多个微服务的数据,减少前端请求次数提升性能,减少网络开销
权限控制统一的身份认证和权限校验增强安全性
租户隔离实现多租户数据隔离保障数据安全
缓存管理提供数据缓存能力提升响应速度

3.2 租户架构设计

3.2.1 租户识别中间件

// tenant.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { TenantService } from './tenant.service';
 
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(private readonly tenantService: TenantService) {}
 
  async use(req: Request, res: Response, next: NextFunction) {
    try {
      // 从请求头或域名中获取租户标识
      const tenantId = req.headers['x-tenant-id'] || this.extractTenantFromHost(req.hostname);
      
      if (!tenantId) {
        return res.status(400).json({
          statusCode: 400,
          message: 'Tenant ID is required'
        });
      }
 
      // 验证租户合法性
      const tenant = await this.tenantService.validateTenant(tenantId as string);
      
      if (!tenant) {
        return res.status(403).json({
          statusCode: 403,
          message: 'Invalid tenant'
        });
      }
 
      // 将租户信息添加到请求对象
      req.tenant = tenant;
      next();
    } catch (error) {
      next(error);
    }
  }
 
  private extractTenantFromHost(hostname: string): string | null {
    // 从域名中提取租户信息,如 tenant1.example.com
    const parts = hostname.split('.');
    if (parts.length >= 3) {
      return parts[0];
    }
    return null;
  }
}

3.2.2 请求作用域租户上下文

// tenant.context.ts
import { Injectable, Scope } from '@nestjs/common';
 
@Injectable({ scope: Scope.REQUEST })
export class TenantContext {
  private tenantId: string;
  private tenantConfig: any;
 
  setTenant(tenantId: string, config: any) {
    this.tenantId = tenantId;
    this.tenantConfig = config;
  }
 
  getTenantId(): string {
    return this.tenantId;
  }
 
  getTenantConfig(): any {
    return this.tenantConfig;
  }
 
  // 获取租户特定的数据库连接
  getDatabaseConnection() {
    return `tenant_${this.tenantId}`;
  }
}

3.2.3 JWT鉴权与租户权限控制

// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Reflector } from '@nestjs/core';
 
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly jwtService: JwtService,
    private readonly reflector: Reflector
  ) {}
 
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
 
    if (!token) {
      return false;
    }
 
    try {
      const payload = await this.jwtService.verifyAsync(token);
      
      // 验证租户权限
      const requiredTenantRoles = this.reflector.get<string[]>(
        'tenant_roles',
        context.getHandler()
      );
 
      if (requiredTenantRoles && !requiredTenantRoles.includes(payload.role)) {
        return false;
      }
 
      // 将用户信息添加到请求对象
      request.user = payload;
      return true;
    } catch {
      return false;
    }
  }
 
  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

3.3 BFF层数据聚合实现

// user.bff.service.ts
import { Injectable, HttpService } from '@nestjs/common';
import { firstValueFrom } from 'rxjs';
import { TenantContext } from '../tenant/tenant.context';
 
@Injectable()
export class UserBffService {
  constructor(
    private readonly httpService: HttpService,
    private readonly tenantContext: TenantContext
  ) {}
 
  async getUserWithDetails(userId: string) {
    // 并行调用多个微服务接口
    const [user, orders, profile] = await Promise.all([
      this.getUserBaseInfo(userId),
      this.getUserOrders(userId),
      this.getUserProfile(userId)
    ]);
 
    // 数据聚合
    return {
      ...user,
      orders: orders.data,
      profile: profile.data,
      tenantSpecificData: this.getTenantSpecificData(user)
    };
  }
 
  private async getUserBaseInfo(userId: string) {
    const response = await firstValueFrom(
      this.httpService.get(`${process.env.USER_SERVICE_URL}/users/${userId}`, {
        headers: {
          'X-Tenant-Id': this.tenantContext.getTenantId()
        }
      })
    );
    return response.data;
  }
 
  private async getUserOrders(userId: string) {
    return firstValueFrom(
      this.httpService.get(`${process.env.ORDER_SERVICE_URL}/users/${userId}/orders`)
    );
  }
 
  private async getUserProfile(userId: string) {
    return firstValueFrom(
      this.httpService.get(`${process.env.PROFILE_SERVICE_URL}/users/${userId}/profile`)
    );
  }
 
  private getTenantSpecificData(user: any) {
    const tenantConfig = this.tenantContext.getTenantConfig();
    
    // 根据租户配置返回不同的数据结构
    return {
      customFields: tenantConfig.customFields?.map(field => ({
        name: field,
        value: user[field] || null
      })) || []
    };
  }
}

3.4 缓存策略实现

// cache.service.ts
import { Injectable, CACHE_MANAGER, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { TenantContext } from '../tenant/tenant.context';
 
@Injectable()
export class CacheService {
  constructor(
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    private readonly tenantContext: TenantContext
  ) {}
 
  private getTenantCacheKey(key: string): string {
    return `${this.tenantContext.getTenantId()}:${key}`;
  }
 
  async get<T>(key: string): Promise<T | undefined> {
    return this.cacheManager.get<T>(this.getTenantCacheKey(key));
  }
 
  async set<T>(key: string, value: T, ttl = 3600): Promise<void> {
    await this.cacheManager.set(
      this.getTenantCacheKey(key),
      value,
      ttl * 1000
    );
  }
 
  async del(key: string): Promise<void> {
    await this.cacheManager.del(this.getTenantCacheKey(key));
  }
 
  async wrap<T>(key: string, fn: () => Promise<T>, ttl = 3600): Promise<T> {
    const cacheKey = this.getTenantCacheKey(key);
    
    const cachedValue = await this.cacheManager.get<T>(cacheKey);
    if (cachedValue) {
      return cachedValue;
    }
 
    const result = await fn();
    await this.cacheManager.set(cacheKey, result, ttl * 1000);
    return result;
  }
}

四、性能优化成果

4.1 前端性能优化结果

通过微前端架构和Vite构建优化,我们取得了显著的性能提升-

指标优化前优化后提升幅度
首屏加载时间7s1s85.7%
LCP (最大内容绘制)6.8s0.7s90%
TTI (交互时间)8.2s1.5s81.7%
包体积2.8MB1.2MB57.1%

4.2 后端性能优化结果

BFF层架构的实施也带来了后端性能的显著提升-

指标优化前优化后提升幅度
请求延迟500ms+<200ms60%
并发处理能力5k QPS20k QPS300%
部署时间36min14min61.1%
API调用复杂度30%+ 减少

五、项目经验总结

5.1 技术选型经验

  • 适合的才是最好的-技术选型要结合项目实际需求,而不是盲目追求新技术
  • 渐进式迁移-对于大型项目,采用渐进式微前端迁移策略,降低风险
  • 性能优先-在架构设计中始终将性能作为重要考量因素

5.2 开发实践经验

  • 标准化开发-建立统一的开发规范和代码质量标准
  • 自动化测试-完善的单元测试和E2E测试保障代码质量
  • CI/CD流水线-自动化部署流程提高开发效率

5.3 架构设计经验

  • 模块化设计-合理的模块划分提高代码复用性和维护性
  • 缓存策略-多级缓存机制显著提升系统性能
  • 监控告警-完善的监控体系保障系统稳定运行

5.4 遇到的挑战与解决方案

挑战1-微前端样式隔离问题

  • 解决方案-使用Shadow DOM和CSS Modules双重隔离机制

挑战2-多租户数据安全

  • 解决方案-请求作用域的租户上下文和严格的权限控制

挑战3-构建性能优化

  • 解决方案-Vite插件开发和精细化的代码分割策略

结语

卓信业务SaaS平台的技术架构升级是一个系统性工程,涉及微前端、BFF层、构建优化等多个技术领域。通过这次架构升级,我们不仅解决了现有系统的性能和扩展性问题,也为未来的业务发展奠定了坚实的技术基础。

在这个过程中,深刻体会到-

  • 架构设计要面向未来-技术选型和架构设计需要考虑业务的长期发展
  • 性能优化是持续过程-性能优化不是一次性工作,需要持续监控和改进
  • 团队协作是关键-复杂项目的成功离不开团队成员的密切配合