个人编制软件展示

PSI - Purchase Sale Inventory 进销存软件

PSI进销存系统多租户架构设计

多租户概述

多租户(Multi-tenancy)是一种软件架构模式,多个租户共享同一个应用实例,但数据相互隔离。PSI进销存系统支持多租户模式,可为不同客户提供独立的SaaS服务,实现资源复用和成本优化。

多租户模式选择

常见的的多租户实现模式:

模式 优点 缺点 适用场景
独立数据库 数据隔离好,安全性高 资源占用高,维护成本大 大型客户
共享数据库独立Schema 平衡成本与隔离 需要Schema管理 中型客户
共享数据库共享Schema 成本最低,易于维护 数据隔离依赖应用层 小型客户

数据层设计

PSI系统采用共享数据库共享Schema模式,通过租户ID实现数据隔离:

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

  // 设置当前租户
  setTenant(tenantId) {
    this.currentTenant = tenantId;
  }

  // 获取当前租户
  getTenant() {
    return this.currentTenant;
  }

  // 获取租户ID(用于数据库查询)
  getTenantId() {
    return this.currentTenant || 'default';
  }
}

// 租户中间件
const tenantContext = new TenantContext();

const tenantMiddleware = async (ctx, next) => {
  // 从请求头、Cookie或参数获取租户ID
  const tenantId = ctx.headers['x-tenant-id']
    || ctx.query.tenantId
    || ctx.cookies.get('tenantId')
    || 'default';

  tenantContext.setTenant(tenantId);
  ctx.tenantId = tenantId;

  try {
    await next();
  } finally {
    tenantContext.setTenant(null);
  }
};

// 多租户数据查询基础类
class TenantQuery {
  constructor(tableName) {
    this.tableName = tableName;
    this.tenantId = tenantContext.getTenantId();
  }

  // 添加租户过滤条件
  addTenantCondition(where) {
    return {
      ...where,
      tenant_id: this.tenantId
    };
  }

  // 查询
  async find(where = {}) {
    const query = this.addTenantCondition(where);
    return await db.query(this.tableName, query);
  }

  // 创建
  async create(data) {
    return await db.query(this.tableName, {
      ...data,
      tenant_id: this.tenantId,
      created_at: new Date(),
      updated_at: new Date()
    });
  }

  // 更新
  async update(id, data) {
    return await db.query(this.tableName, data, {
      id,
      tenant_id: this.tenantId
    });
  }

  // 删除
  async delete(id) {
    return await db.query(this.tableName, null, {
      id,
      tenant_id: this.tenantId
    });
  }
}

数据隔离实现

使用数据库视图和行级安全策略实现数据隔离:

-- 创建租户隔离视图
CREATE VIEW v_products AS
SELECT
  id,
  tenant_id,
  product_code,
  product_name,
  category,
  unit,
  price,
  cost_price,
  stock_quantity,
  created_at,
  updated_at
FROM products
WHERE tenant_id = current_setting('app.tenant_id', true);

-- 使用RLS(行级安全策略)
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

-- 创建租户隔离策略
CREATE POLICY tenant_isolation_policy ON products
  FOR ALL
  USING (tenant_id = current_setting('app.tenant_id', true));

-- 数据库连接时设置租户上下文
const pool = new Pool({
  database: 'psi_db'
});

// 每次查询前设置租户ID
pool.on('connection', (client) => {
  client.query(`SET app.tenant_id = '${tenantId}'`);
});

租户配置管理

多租户系统的租户配置和套餐管理:

// 租户服务
class TenantService {
  // 获取租户配置
  async getTenantConfig(tenantId) {
    return await db.tenants.findOne({ id: tenantId });
  }

  // 获取租户套餐信息
  async getTenantPlan(tenantId) {
    const tenant = await this.getTenantConfig(tenantId);
    return await db.plans.findOne({ id: tenant.plan_id });
  }

  // 检查租户功能权限
  async checkFeaturePermission(tenantId, feature) {
    const plan = await this.getTenantPlan(tenantId);
    const features = plan.features.split(',');
    return features.includes(feature);
  }

  // 检查租户资源配额
  async checkQuota(tenantId, resourceType) {
    const tenant = await this.getTenantConfig(tenantId);
    const plan = await this.getTenantPlan(tenantId);

    const quotas = {
      users: { current: tenant.user_count, max: plan.max_users },
      products: { current: tenant.product_count, max: plan.max_products },
      orders: { current: tenant.order_count, max: plan.max_orders },
      storage: { current: tenant.storage_used, max: plan.max_storage }
    };

    const quota = quotas[resourceType];
    if (quota.current >= quota.max) {
      throw new Error(`已达${resourceType}数量上限,请升级套餐`);
    }
    return quota;
  }

  // SaaS套餐定义
  getPlanFeatures() {
    return {
      free: {
        name: '免费版',
        price: 0,
        max_users: 3,
        max_products: 100,
        max_orders: 500,
        max_storage: 1024, // MB
        features: ['basic', 'report']
      },
      standard: {
        name: '标准版',
        price: 99,
        max_users: 10,
        max_products: 1000,
        max_orders: 10000,
        max_storage: 10240,
        features: ['basic', 'report', 'api', 'multi_store']
      },
      enterprise: {
        name: '企业版',
        price: 299,
        max_users: 100,
        max_products: 10000,
        max_orders: 100000,
        max_storage: 102400,
        features: ['basic', 'report', 'api', 'multi_store', 'custom', 'support']
      }
    };
  }
}

租户路由设计

支持三级域名或子目录方式的租户访问:

// 租户路由器
const Router = require('koa-router');

// 方式1:子域名方式 tenant.psi.com
const subdomainRouter = new Router({ prefix: '' });

subdomainRouter.subdomain('tenant', (router) => {
  router.get('/', async (ctx) => {
    const tenantSubdomain = ctx.params.tenant;
    const tenant = await tenantService.getTenantBySubdomain(tenantSubdomain);

    if (!tenant) {
      ctx.status = 404;
      return;
    }

    ctx.state.tenantId = tenant.id;
    await next();
  });
});

// 方式2:路径方式 psi.com/t/tenant
const pathRouter = new Router();

pathRouter.get('/t/:tenant', async (ctx) => {
  const { tenant } = ctx.params;
  const tenantInfo = await tenantService.getTenantByAlias(tenant);

  if (!tenantInfo) {
    ctx.status = 404;
    return;
    ctx.redirect('/not-found');
  }

  // 重定向到租户空间
  ctx.redirect(`/${tenantInfo.alias}/dashboard`);
});

// 租户空间路由
const workspaceRouter = new Router();

workspaceRouter.get('/:tenantId/*', async (ctx, next) => {
  const { tenantId } = ctx.params;

  // 验证租户是否存在
  const tenant = await tenantService.getTenantConfig(tenantId);
  if (!tenant) {
    ctx.status = 404;
    return;
  }

  // 检查租户状态
  if (tenant.status === 'suspended') {
    ctx.status = 403;
    ctx.body = '该租户已被暂停使用';
    return;
  }

  await next();
});

跨租户数据共享

支持租户间的数据协作与共享:

// 租户数据共享服务
class CrossTenantService {
  // 授权另一个租户访问自己的数据
  async shareData(sourceTenantId, targetTenantId, dataTypes) {
    // 检查授权
    const share = await db.tenant_shares.create({
      source_tenant_id: sourceTenantId,
      target_tenant_id: targetTenantId,
      data_types: dataTypes.join(','),
      created_at: new Date()
    });

    return share;
  }

  // 查询跨租户数据
  async querySharedData(tenantId, dataType) {
    // 查询授权给我的数据
    const shares = await db.tenant_shares.find({
      target_tenant_id: tenantId,
      data_types: { like: `%${dataType}%` }
    });

    const results = [];
    for (const share of shares) {
      const data = await db.query(dataType, {
        tenant_id: share.source_tenant_id
      });
      results.push(...data.map(item => ({
        ...item,
        _source_tenant: share.source_tenant_id
      })));
    }

    return results;
  }
}

总结

PSI进销存系统的多租户架构设计充分考虑了数据隔离、资源配额和扩展性。通过合理的架构设计,可以为不同规模的客户提供灵活的SaaS服务,同时控制运营成本。

← 下一篇:PSI进销存软件自动化部署与持续集成 上篇:PSI进销存软件数据备份与灾难恢复 →