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服务,同时控制运营成本。