multi-tenant-architecture.md 10 KB

多租户架构设计

版本信息

版本 日期 描述 作者
3.0 2025-10-20 集团AI智能进销存系统多租户架构 Winston

组织架构实体设计

公司实体设计

// 公司实体
interface Company {
  id: number;
  name: string;
  code: string; // 公司编码
  parentId: number | null; // 母公司ID
  level: number; // 层级深度
  path: string; // 层级路径
  status: 'active' | 'inactive';
  contactInfo: ContactInfo;
  businessInfo: BusinessInfo;
  createdAt: Date;
  updatedAt: Date;
}

// 用户实体扩展
interface User {
  id: number;
  username: string;
  email: string | null;
  password: string;
  companyId: number; // 所属公司
  roles: Role[];
  company: Company;
  createdAt: Date;
  updatedAt: Date;
}

// 联系信息
interface ContactInfo {
  phone: string | null;
  address: string | null;
  contactPerson: string | null;
  email: string | null;
}

// 业务信息
interface BusinessInfo {
  businessLicense: string | null;
  taxNumber: string | null;
  industry: string | null;
  scale: 'small' | 'medium' | 'large';
}

组织架构树形结构

// 组织架构树节点
interface OrganizationNode {
  id: number;
  name: string;
  code: string;
  level: number;
  parentId: number | null;
  children: OrganizationNode[];
  userCount: number;
  status: 'active' | 'inactive';
}

// 组织架构查询结果
interface OrganizationTree {
  root: OrganizationNode;
  totalCompanies: number;
  totalUsers: number;
}

数据隔离策略

数据库层面隔离

表结构设计:

-- 所有业务表添加 company_id 字段
ALTER TABLE suppliers ADD COLUMN company_id INTEGER NOT NULL;
ALTER TABLE customers ADD COLUMN company_id INTEGER NOT NULL;
ALTER TABLE sales_orders ADD COLUMN company_id INTEGER NOT NULL;
ALTER TABLE inventory ADD COLUMN company_id INTEGER NOT NULL;
ALTER TABLE purchase_orders ADD COLUMN company_id INTEGER NOT NULL;

-- 创建公司表
CREATE TABLE companies (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  code VARCHAR(50) UNIQUE NOT NULL,
  parent_id INTEGER REFERENCES companies(id),
  level INTEGER NOT NULL DEFAULT 1,
  path VARCHAR(1000) NOT NULL,
  status VARCHAR(20) DEFAULT 'active',
  contact_info JSONB,
  business_info JSONB,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

数据库视图:

-- 创建数据隔离视图
CREATE VIEW vw_suppliers AS
SELECT s.*
FROM suppliers s
JOIN companies c ON s.company_id = c.id
WHERE c.status = 'active';

-- 创建层级数据视图(母公司可访问子公司数据)
CREATE VIEW vw_hierarchical_suppliers AS
SELECT s.*
FROM suppliers s
JOIN companies c ON s.company_id = c.id
JOIN companies parent ON c.path LIKE parent.path || '%'
WHERE parent.id = :current_company_id
AND c.status = 'active';

行级安全策略(RLS):

-- 启用行级安全
ALTER TABLE suppliers ENABLE ROW LEVEL SECURITY;

-- 创建策略:用户只能访问自己公司的数据
CREATE POLICY company_isolation_policy ON suppliers
FOR ALL USING (company_id = current_setting('app.current_company_id')::integer);

应用层面隔离

中间件自动注入:

// 多租户数据过滤中间件
const multiTenantMiddleware = async (c: Context, next: Next) => {
  const user = c.get('user') as User;

  // 设置当前公司ID
  c.set('currentCompanyId', user.companyId);

  // 对于查询操作,自动添加公司过滤条件
  const originalQuery = c.req.query();
  if (originalQuery) {
    c.req.query = {
      ...originalQuery,
      companyId: user.companyId.toString()
    };
  }

  await next();
};

// 数据访问服务
class MultiTenantService<T> {
  constructor(private repository: Repository<T>) {}

  async findAll(user: User, options?: FindOptions<T>): Promise<T[]> {
    return this.repository.find({
      ...options,
      where: {
        ...options?.where,
        companyId: user.companyId
      }
    });
  }

  async findOne(user: User, id: number, options?: FindOptions<T>): Promise<T | null> {
    return this.repository.findOne({
      ...options,
      where: {
        id,
        companyId: user.companyId,
        ...options?.where
      }
    });
  }
}

权限穿透机制

权限检查逻辑

// 权限检查服务
class PermissionService {
  // 检查数据访问权限
  async checkDataAccess(user: User, targetCompanyId: number): Promise<boolean> {
    // 超级管理员可以访问所有数据
    if (user.roles.some(role => role.name === 'super_admin')) {
      return true;
    }

    // 母公司管理员可以访问子公司数据
    if (user.roles.some(role => role.name === 'parent_admin')) {
      return await this.isChildCompany(user.companyId, targetCompanyId);
    }

    // 普通用户只能访问本公司数据
    return user.companyId === targetCompanyId;
  }

  // 检查是否是子公司
  async isChildCompany(parentCompanyId: number, childCompanyId: number): Promise<boolean> {
    const childCompany = await this.companyRepository.findOne({
      where: { id: childCompanyId }
    });

    if (!childCompany) return false;

    // 检查路径是否包含父公司路径
    return childCompany.path.startsWith(
      await this.getCompanyPath(parentCompanyId)
    );
  }

  // 获取公司路径
  async getCompanyPath(companyId: number): Promise<string> {
    const company = await this.companyRepository.findOne({
      where: { id: companyId }
    });
    return company?.path || '';
  }

  // 获取用户可访问的公司列表
  async getAccessibleCompanies(user: User): Promise<number[]> {
    if (user.roles.some(role => role.name === 'super_admin')) {
      // 超级管理员可以访问所有公司
      const allCompanies = await this.companyRepository.find({
        where: { status: 'active' }
      });
      return allCompanies.map(c => c.id);
    }

    if (user.roles.some(role => role.name === 'parent_admin')) {
      // 母公司管理员可以访问所有子公司
      const childCompanies = await this.companyRepository.find({
        where: {
          path: Like(`${await this.getCompanyPath(user.companyId)}%`),
          status: 'active'
        }
      });
      return [user.companyId, ...childCompanies.map(c => c.id)];
    }

    // 普通用户只能访问本公司
    return [user.companyId];
  }
}

数据查询优化

// 多租户数据查询服务
class MultiTenantQueryService {
  constructor(private permissionService: PermissionService) {}

  // 构建多租户查询条件
  async buildQueryConditions(user: User, targetCompanyId?: number): Promise<any> {
    const accessibleCompanies = await this.permissionService.getAccessibleCompanies(user);

    if (targetCompanyId) {
      // 检查特定公司的访问权限
      const hasAccess = await this.permissionService.checkDataAccess(user, targetCompanyId);
      if (!hasAccess) {
        throw new Error('无权访问该公司数据');
      }
      return { companyId: targetCompanyId };
    }

    // 返回用户可访问的所有公司
    return { companyId: In(accessibleCompanies) };
  }

  // 分页查询带权限检查
  async findWithPagination<T>(
    user: User,
    repository: Repository<T>,
    options: FindManyOptions<T> & { targetCompanyId?: number }
  ): Promise<PaginatedResponse<T>> {
    const conditions = await this.buildQueryConditions(user, options.targetCompanyId);

    const [data, total] = await repository.findAndCount({
      ...options,
      where: {
        ...options.where,
        ...conditions
      }
    });

    return {
      data,
      pagination: {
        total,
        current: options.skip ? Math.floor(options.skip / (options.take || 10)) + 1 : 1,
        pageSize: options.take || 10,
        totalPages: Math.ceil(total / (options.take || 10))
      }
    };
  }
}

审计日志

// 数据访问审计
interface DataAccessAudit {
  id: number;
  userId: number;
  userCompanyId: number;
  targetCompanyId: number;
  resourceType: string; // 'supplier', 'customer', 'order', etc.
  resourceId: number;
  action: 'read' | 'create' | 'update' | 'delete';
  timestamp: Date;
  ipAddress: string;
  userAgent: string;
  success: boolean;
  errorMessage?: string;
}

// 审计服务
class AuditService {
  async logDataAccess(
    user: User,
    targetCompanyId: number,
    resourceType: string,
    resourceId: number,
    action: string,
    success: boolean,
    errorMessage?: string
  ): Promise<void> {
    const auditLog: Partial<DataAccessAudit> = {
      userId: user.id,
      userCompanyId: user.companyId,
      targetCompanyId,
      resourceType,
      resourceId,
      action,
      timestamp: new Date(),
      ipAddress: this.getClientIP(),
      userAgent: this.getUserAgent(),
      success,
      errorMessage
    };

    await this.auditRepository.save(auditLog);
  }
}

性能优化

数据库索引

-- 为多租户查询创建复合索引
CREATE INDEX idx_suppliers_company_id ON suppliers(company_id);
CREATE INDEX idx_customers_company_id ON customers(company_id);
CREATE INDEX idx_orders_company_id ON sales_orders(company_id);
CREATE INDEX idx_inventory_company_id ON inventory(company_id);

-- 为层级查询创建索引
CREATE INDEX idx_companies_path ON companies(path);
CREATE INDEX idx_companies_parent_id ON companies(parent_id);

缓存策略

// 多租户缓存服务
class MultiTenantCacheService {
  constructor(private redis: Redis) {}

  // 公司数据缓存
  async cacheCompanyData(companyId: number, data: any): Promise<void> {
    const key = `company:${companyId}:data`;
    await this.redis.setex(key, 3600, JSON.stringify(data)); // 缓存1小时
  }

  // 获取公司缓存数据
  async getCachedCompanyData(companyId: number): Promise<any> {
    const key = `company:${companyId}:data`;
    const cached = await this.redis.get(key);
    return cached ? JSON.parse(cached) : null;
  }

  // 清除公司缓存
  async clearCompanyCache(companyId: number): Promise<void> {
    const pattern = `company:${companyId}:*`;
    const keys = await this.redis.keys(pattern);
    if (keys.length > 0) {
      await this.redis.del(...keys);
    }
  }
}

这个多租户架构设计确保了集团级进销存系统的数据安全性和隔离性,同时提供了灵活的权限穿透机制,满足母子公司管理的复杂业务需求。