个人编程网站

进销存(JXC)软件开发技术积累与分享

进销存系统多租户架构设计与数据隔离方案

引言

SaaS 模式的进销存系统需要支撑多个企业(租户)同时使用,每个租户的数据必须严格隔离,同时要保障系统资源的高效利用。本文将介绍如何设计一套完整的多租户架构,实现数据隔离、资源管理和成本优化。

多租户架构模式

常见的多租户架构模式对比:

模式 优点 缺点 适用场景
独立数据库 数据隔离严格,安全性高 资源成本高,维护复杂 大型企业客户
独立 Schema 隔离性较好,成本适中 数据库连接数限制 中型企业客户
共享数据库 成本最低,运维简单 隔离性较弱,需应用层加固 小型客户

核心代码实现

多租户数据访问层实现:

// 多租户上下文管理器
class TenantContext {
  constructor() {
    this.currentTenant = null;
    this.tenantCache = new Map();
  }

  // 设置当前租户
  setCurrentTenant(tenantId) {
    this.currentTenant = tenantId;
    // 设置到请求上下文
    async_context.set('tenantId', tenantId);
  }

  // 获取当前租户
  getCurrentTenant() {
    if (this.currentTenant) {
      return this.currentTenant;
    }
    return async_context.get('tenantId');
  }

  // 清除租户上下文
  clear() {
    this.currentTenant = null;
    async_context.delete('tenantId');
  }
}

// 租户解析器
class TenantResolver {
  constructor(config) {
    this.config = config;
    this.resolvers = [
      this.resolveFromHeader.bind(this),
      this.resolveFromCookie.bind(this),
      this.resolveFromSubdomain.bind(this),
      this.resolveFromPath.bind(this)
    ];
  }

  // 从请求头解析租户
  resolveFromHeader(request) {
    const tenantId = request.headers['x-tenant-id'];
    if (tenantId && this.validateTenant(tenantId)) {
      return tenantId;
    }
    return null;
  }

  // 从 Cookie 解析租户
  resolveFromCookie(request) {
    const tenantId = request.cookies['tenant_id'];
    if (tenantId && this.validateTenant(tenantId)) {
      return tenantId;
    }
    return null;
  }

  // 从子域名解析租户
  resolveFromSubdomain(request) {
    const host = request.headers.host;
    const match = host.match(/^(\w+)\./);

    if (match) {
      const subdomain = match[1];
      if (subdomain !== 'www' && subdomain !== 'api') {
        return this.lookupTenantBySubdomain(subdomain);
      }
    }
    return null;
  }

  // 从路径解析租户
  resolveFromPath(request) {
    const match = request.path.match(/^\/tenant\/([^\/]+)/);
    if (match) {
      return match[1];
    }
    return null;
  }

  // 解析租户
  async resolve(request) {
    for (const resolver of this.resolvers) {
      const tenantId = await resolver(request);
      if (tenantId) {
        return tenantId;
      }
    }

    // 返回默认租户
    return this.config.defaultTenant;
  }

  // 验证租户
  async validateTenant(tenantId) {
    const tenant = await this.getTenant(tenantId);
    return tenant && tenant.status === 'active';
  }

  // 获取租户信息
  async getTenant(tenantId) {
    return await TenantCache.get(tenantId);
  }
}

// 数据路由策略
class DataRouter {
  constructor(tenantContext, config) {
    this.tenantContext = tenantContext;
    this.config = config;
  }

  // 获取数据源配置
  getDataSourceConfig(tenantId) {
    const tenant = this.tenantContext.getTenant(tenantId);

    switch (this.config.isolationMode) {
      case 'database':
        return {
          host: `${tenantId}.database.host`,
          database: tenantId,
          username: `${tenantId}_user`,
          password: tenant.dbPassword
        };

      case 'schema':
        return {
          host: this.config.sharedDatabase.host,
          database: this.config.sharedDatabase.name,
          username: `${tenantId}_user`,
          password: tenant.dbPassword,
          schema: tenantId
        };

      case 'shared':
        return {
          host: this.config.sharedDatabase.host,
          database: this.config.sharedDatabase.name,
          username: this.config.sharedDatabase.username,
          password: this.config.sharedDatabase.password,
          options: {
            tenantId: tenantId
          }
        };

      default:
        throw new Error('Unknown isolation mode');
    }
  }

  // 多数据源管理
  async getDataSource(tenantId) {
    const cacheKey = `datasource_${tenantId}`;
    let dataSource = this.dataSourceCache.get(cacheKey);

    if (!dataSource) {
      const config = this.getDataSourceConfig(tenantId);
      dataSource = await this.createDataSource(config);
      this.dataSourceCache.set(cacheKey, dataSource);
    }

    return dataSource;
  }
}

// SQL 拦截器 - 添加租户过滤
class TenantSQLInterceptor {
  constructor(tenantContext) {
    this.tenantContext = tenantContext;
  }

  // 拦截 SQL 添加租户条件
  intercept(sql, params) {
    const tenantId = this.tenantContext.getCurrentTenant();
    if (!tenantId) {
      throw new Error('Tenant context not set');
    }

    // 分析 SQL 类型
    const sqlType = this.getSQLType(sql);

    if (sqlType === 'SELECT') {
      return this.addTenantCondition(sql, params, 'tenant_id', tenantId);
    } else if (sqlType === 'INSERT') {
      return this.addTenantField(sql, params, 'tenant_id', tenantId);
    } else if (sqlType === 'UPDATE' || sqlType === 'DELETE') {
      return this.addTenantCondition(sql, params, 'tenant_id', tenantId);
    }

    return { sql, params };
  }

  // 添加租户条件
  addTenantCondition(sql, params, tenantField, tenantId) {
    // 简单处理:追加 WHERE 条件
    if (sql.toLowerCase().includes('where')) {
      sql = `${sql} AND ${tenantField} = ?`;
    } else {
      sql = `${sql} WHERE ${tenantField} = ?`;
    }
    params.push(tenantId);

    return { sql, params };
  }

  // 添加租户字段
  addTenantField(sql, params, tenantField, tenantId) {
    // INSERT INTO table (...) VALUES (...)
    const match = sql.match(/INSERT\s+INTO\s+(\w+)\s*\(([^)]+)\)\s*VALUES/i);
    if (match) {
      const fields = match[2].split(',').map(f => f.trim());
      fields.push(tenantField);
      const newFields = fields.join(', ');

      const valuesPlaceholder = params.map(() => '?').join(', ');
      sql = sql.replace(
        `(${match[2]})`,
        `(${newFields})`
      ).replace(
        `VALUES (${valuesPlaceholder})`,
        `VALUES (${valuesPlaceholder}, ?)`
      );
      params.push(tenantId);
    }

    return { sql, params };
  }
}

// 租户资源配额管理
class TenantQuotaManager {
  constructor() {
    this.quotas = new Map();
    this.usage = new Map();
  }

  // 初始化租户配额
  initQuota(tenantId, quota) {
    this.quotas.set(tenantId, {
      storage: quota.storage || 1024 * 1024 * 1024, // 默认 1GB
      apiCalls: quota.apiCalls || 10000,
      users: quota.users || 10,
      warehouses: quota.warehouses || 3,
      products: quota.products || 1000
    });

    this.usage.set(tenantId, {
      storage: 0,
      apiCalls: 0,
      users: 0,
      warehouses: 0,
      products: 0
    });
  }

  // 检查配额
  checkQuota(tenantId, resource, amount = 1) {
    const quota = this.quotas.get(tenantId);
    const usage = this.usage.get(tenantId);

    if (!quota || !usage) {
      return { allowed: false, reason: 'Tenant not found' };
    }

    const currentUsage = usage[resource] || 0;
    const limit = quota[resource];

    return {
      allowed: currentUsage + amount <= limit,
      current: currentUsage,
      limit: limit,
      remaining: limit - currentUsage
    };
  }

  // 消耗配额
  async consumeQuota(tenantId, resource, amount = 1) {
    const check = this.checkQuota(tenantId, resource, amount);

    if (!check.allowed) {
      throw new Error(`Quota exceeded for ${resource}`);
    }

    const usage = this.usage.get(tenantId);
    usage[resource] = (usage[resource] || 0) + amount;

    return check;
  }

  // 释放配额
  releaseQuota(tenantId, resource, amount = 1) {
    const usage = this.usage.get(tenantId);
    if (usage) {
      usage[resource] = Math.max(0, usage[resource] - amount);
    }
  }

  // 获取配额使用报告
  getQuotaReport(tenantId) {
    const quota = this.quotas.get(tenantId);
    const usage = this.usage.get(tenantId);

    if (!quota || !usage) {
      return null;
    }

    const report = {};
    for (const [resource, limit] of Object.entries(quota)) {
      const used = usage[resource] || 0;
      report[resource] = {
        used,
        limit,
        percentage: ((used / limit) * 100).toFixed(2) + '%',
        remaining: limit - used
      };
    }

    return report;
  }
}

资源隔离与安全

租户资源隔离配置:

// 租户认证与授权
class TenantAuthService {
  constructor(tenantContext) {
    this.tenantContext = tenantContext;
  }

  // 租户登录
  async login(tenantDomain, username, password) {
    const tenant = await this.getTenantByDomain(tenantDomain);
    if (!tenant) {
      throw new Error('Tenant not found');
    }

    // 验证用户
    const user = await this.verifyUser(tenant.id, username, password);
    if (!user) {
      throw new Error('Invalid credentials');
    }

    // 生成令牌
    const token = await this.generateToken({
      tenantId: tenant.id,
      userId: user.id,
      roles: user.roles
    });

    return { token, tenant, user };
  }

  // 验证用户
  async verifyUser(tenantId, username, password) {
    const user = await this.db.query(
      `SELECT * FROM tenant_${tenantId}.users
       WHERE username = ? AND password = ?`,
      [username, this.hashPassword(password)]
    );

    return user[0];
  }

  // 生成令牌
  async generateToken(payload) {
    return jwt.sign(payload, this.secret, {
      expiresIn: '24h'
    });
  }

  // 中间件:验证租户权限
  middleware() {
    return async (ctx, next) => {
      const token = ctx.headers.authorization?.replace('Bearer ', '');
      if (!token) {
        ctx.status = 401;
        ctx.body = { error: 'No token provided' };
        return;
      }

      try {
        const decoded = jwt.verify(token, this.secret);
        this.tenantContext.setCurrentTenant(decoded.tenantId);
        ctx.user = decoded;
        await next();
      } catch (error) {
        ctx.status = 401;
        ctx.body = { error: 'Invalid token' };
      }
    };
  }
}

// 数据加密服务
class TenantEncryptionService {
  constructor() {
    this.algorithms = {
      'aes-256-gcm': { keySize: 32, ivSize: 16 },
      'aes-128-gcm': { keySize: 16, ivSize: 16 }
    };
  }

  // 生成租户密钥
  generateTenantKey(tenantId) {
    const config = this.algorithms['aes-256-gcm'];
    const key = crypto.randomBytes(config.keySize);
    this.storeKey(tenantId, key);
    return key;
  }

  // 加密敏感数据
  encrypt(tenantId, data) {
    const key = this.getKey(tenantId);
    const config = this.algorithms['aes-256-gcm'];
    const iv = crypto.randomBytes(config.ivSize);

    const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
    const encrypted = Buffer.concat([
      cipher.update(JSON.stringify(data), 'utf8'),
      cipher.final()
    ]);

    const authTag = cipher.getAuthTag();

    return {
      iv: iv.toString('base64'),
      data: encrypted.toString('base64'),
      authTag: authTag.toString('base64')
    };
  }

  // 解密敏感数据
  decrypt(tenantId, encryptedData) {
    const key = this.getKey(tenantId);
    const iv = Buffer.from(encryptedData.iv, 'base64');
    const encrypted = Buffer.from(encryptedData.data, 'base64');
    const authTag = Buffer.from(encryptedData.authTag, 'base64');

    const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
    decipher.setAuthTag(authTag);

    const decrypted = Buffer.concat([
      decipher.update(encrypted),
      decipher.final()
    ]);

    return JSON.parse(decrypted.toString('utf8'));
  }
}

// 审计日志
class TenantAuditLogger {
  constructor(tenantContext) {
    this.tenantContext = tenantContext;
  }

  // 记录操作日志
  async log(action, resource, details = {}) {
    const tenantId = this.tenantContext.getCurrentTenant();
    const userId = details.userId || 'system';

    const log = {
      tenantId,
      userId,
      action,
      resource,
      details,
      timestamp: new Date(),
      ip: details.ip || 'unknown',
      userAgent: details.userAgent || 'unknown'
    };

    // 异步写入日志
    await this.writeLog(log);
  }

  // 查询租户日志
  async queryLogs(tenantId, filters = {}) {
    const query = {
      tenantId,
      startDate: filters.startDate,
      endDate: filters.endDate,
      action: filters.action,
      userId: filters.userId,
      limit: filters.limit || 100
    };

    return await this.db.queryLogs(query);
  }
}

最佳实践建议

总结

多租户架构是 SaaS 进销存系统的核心技术:

实际落地需要根据业务规模和发展阶段选择合适的架构方案。

← 下一篇:进销存系统智能补货预测模型与库存优化策略