/** * [BE-MT002] 层级管理服务 * 负责商户→部门→店铺三层层级结构的管理 * AI注意: 所有层级操作必须通过此服务进行 */ import db from '../../config/database'; import { logger } from '../../utils/logger'; import { EventBusService } from '../utils/EventBusService'; import RedisService from '../utils/RedisService'; import { DataIsolationService, HierarchyLevel, HierarchyNode, DataIsolationContext } from '../integration/DataIsolationService'; // 部门接口 export interface Department { id: string; tenant_id: string; name: string; parent_id: string | null; path: string; depth: number; manager_id?: string; status: 'ACTIVE' | 'INACTIVE'; created_at: Date; updated_at: Date; } // 店铺接口 export interface Shop { id: string; tenant_id: string; department_id: string; name: string; platform: string; shop_code: string; path: string; depth: number; status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED'; created_at: Date; updated_at: Date; } // 层级统计 export interface HierarchyStats { totalDepartments: number; totalShops: number; activeShops: number; inactiveShops: number; maxDepth: number; } export class HierarchyService { private static readonly CACHE_PREFIX = 'hierarchy:'; private static readonly CACHE_TTL = 3600; /** * [BE-MT002-01] 初始化租户层级结构 */ static async initializeTenantHierarchy(tenantId: string): Promise { const exists = await db('cf_department') .where('tenant_id', tenantId) .first(); if (exists) { logger.info(`[Hierarchy] Tenant ${tenantId} hierarchy already initialized`); return; } const defaultDepartment: Partial = { id: `DEPT-${tenantId}-DEFAULT`, tenant_id: tenantId, name: '默认部门', parent_id: null, path: `/DEPT-${tenantId}-DEFAULT`, depth: 1, status: 'ACTIVE', created_at: new Date(), updated_at: new Date(), }; await db('cf_department').insert(defaultDepartment); await EventBusService.publish({ type: 'hierarchy.department.created', data: { departmentId: defaultDepartment.id, tenantId, isDefault: true, timestamp: new Date(), } }); logger.info(`[Hierarchy] Initialized default department for tenant ${tenantId}`); } /** * [BE-MT002-02] 创建部门 */ static async createDepartment( tenantId: string, name: string, parentId: string | null = null, managerId?: string ): Promise { let depth = 1; let parentPath = ''; if (parentId) { const parent = await db('cf_department') .where('id', parentId) .where('tenant_id', tenantId) .first(); if (!parent) { throw new Error('父部门不存在'); } depth = parent.depth + 1; parentPath = parent.path; } const id = `DEPT-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const path = `${parentPath}/${id}`; const department: Partial = { id, tenant_id: tenantId, name, parent_id: parentId, path, depth, manager_id: managerId, status: 'ACTIVE', created_at: new Date(), updated_at: new Date(), }; await db('cf_department').insert(department); await this.clearHierarchyCache(tenantId); await EventBusService.publish({ type: 'hierarchy.department.created', data: { departmentId: id, tenantId, parentId, timestamp: new Date(), } }); logger.info(`[Hierarchy] Created department ${id} for tenant ${tenantId}`); return department as Department; } /** * [BE-MT002-03] 更新部门 */ static async updateDepartment( departmentId: string, tenantId: string, updates: Partial ): Promise { const department = await db('cf_department') .where('id', departmentId) .where('tenant_id', tenantId) .first(); if (!department) { throw new Error('部门不存在'); } if (updates.parent_id !== undefined && updates.parent_id !== department.parent_id) { await this.validateParentChange(departmentId, updates.parent_id, tenantId); if (updates.parent_id) { const newParent = await db('cf_department') .where('id', updates.parent_id) .where('tenant_id', tenantId) .first(); updates.depth = newParent.depth + 1; updates.path = `${newParent.path}/${departmentId}`; } else { updates.depth = 1; updates.path = `/${departmentId}`; } await this.updateChildrenPaths(departmentId, updates.path as string, tenantId); } await db('cf_department') .where('id', departmentId) .update({ ...updates, updated_at: new Date(), }); await this.clearHierarchyCache(tenantId); await EventBusService.publish({ type: 'hierarchy.department.updated', data: { departmentId, tenantId, updates, timestamp: new Date(), } }); logger.info(`[Hierarchy] Updated department ${departmentId}`); return await db('cf_department') .where('id', departmentId) .first(); } /** * [BE-MT002-04] 删除部门 */ static async deleteDepartment(departmentId: string, tenantId: string): Promise { const department = await db('cf_department') .where('id', departmentId) .where('tenant_id', tenantId) .first(); if (!department) { throw new Error('部门不存在'); } const childCount = await db('cf_department') .where('parent_id', departmentId) .count('id as count') .first(); if (childCount && Number(childCount.count) > 0) { throw new Error('该部门下存在子部门,无法删除'); } const shopCount = await db('cf_shop') .where('department_id', departmentId) .count('id as count') .first(); if (shopCount && Number(shopCount.count) > 0) { throw new Error('该部门下存在店铺,无法删除'); } await db('cf_department') .where('id', departmentId) .delete(); await this.clearHierarchyCache(tenantId); await EventBusService.publish({ type: 'hierarchy.department.deleted', data: { departmentId, tenantId, timestamp: new Date(), } }); logger.info(`[Hierarchy] Deleted department ${departmentId}`); } /** * [BE-MT002-04-01] 设置部门负责人 */ static async assignDepartmentManager( departmentId: string, tenantId: string, managerId: string, assignedBy: string ): Promise { const department = await db('cf_department') .where('id', departmentId) .where('tenant_id', tenantId) .first(); if (!department) { throw new Error('部门不存在'); } const user = await db('cf_user') .where('id', managerId) .where('tenant_id', tenantId) .where('status', 'ACTIVE') .first(); if (!user) { throw new Error('用户不存在或未激活'); } await db('cf_department') .where('id', departmentId) .where('tenant_id', tenantId) .update({ manager_id: managerId, updated_at: new Date(), }); const updatedDepartment = await db('cf_department') .where('id', departmentId) .where('tenant_id', tenantId) .first(); await this.clearHierarchyCache(tenantId); await EventBusService.publish({ type: 'hierarchy.department.manager_assigned', data: { departmentId, tenantId, managerId, assignedBy, timestamp: new Date(), } }); logger.info(`[Hierarchy] Assigned manager ${managerId} to department ${departmentId}`); return updatedDepartment; } /** * [BE-MT002-04-02] 获取部门负责人 */ static async getDepartmentManager(departmentId: string, tenantId: string): Promise { const department = await db('cf_department') .where('id', departmentId) .where('tenant_id', tenantId) .first(); if (!department || !department.manager_id) { return null; } const manager = await db('cf_user') .where('id', department.manager_id) .where('tenant_id', tenantId) .select('id', 'username', 'email', 'role', 'status') .first(); return manager; } /** * [BE-MT002-04-03] 获取部门统计信息 */ static async getDepartmentStats(departmentId: string, tenantId: string): Promise { const department = await db('cf_department') .where('id', departmentId) .where('tenant_id', tenantId) .first(); if (!department) { throw new Error('部门不存在'); } const [userCount, shopCount, subDepartmentCount] = await Promise.all([ db('cf_user') .where('department_id', departmentId) .where('tenant_id', tenantId) .count('id as count') .first(), db('cf_shop') .where('department_id', departmentId) .where('tenant_id', tenantId) .count('id as count') .first(), db('cf_department') .where('parent_id', departmentId) .where('tenant_id', tenantId) .count('id as count') .first(), ]); return { departmentId, departmentName: department.name, userCount: Number(userCount?.count || 0), shopCount: Number(shopCount?.count || 0), subDepartmentCount: Number(subDepartmentCount?.count || 0), managerId: department.manager_id, status: department.status, createdAt: department.created_at, }; } /** * [BE-MT002-05] 创建店铺 */ static async createShop( tenantId: string, departmentId: string, name: string, platform: string, shopCode: string ): Promise { const department = await db('cf_department') .where('id', departmentId) .where('tenant_id', tenantId) .first(); if (!department) { throw new Error('部门不存在'); } const existingShop = await db('cf_shop') .where('tenant_id', tenantId) .where('shop_code', shopCode) .first(); if (existingShop) { throw new Error('店铺编码已存在'); } const id = `SHOP-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const path = `${department.path}/${id}`; const shop: Partial = { id, tenant_id: tenantId, department_id: departmentId, name, platform, shop_code: shopCode, path, depth: department.depth + 1, status: 'ACTIVE', created_at: new Date(), updated_at: new Date(), }; await db('cf_shop').insert(shop); await this.clearHierarchyCache(tenantId); await EventBusService.publish({ type: 'hierarchy.shop.created', data: { shopId: id, tenantId, departmentId, platform, timestamp: new Date(), } }); logger.info(`[Hierarchy] Created shop ${id} for tenant ${tenantId}`); return shop as Shop; } /** * [BE-MT002-06] 更新店铺 */ static async updateShop( shopId: string, tenantId: string, updates: Partial ): Promise { const shop = await db('cf_shop') .where('id', shopId) .where('tenant_id', tenantId) .first(); if (!shop) { throw new Error('店铺不存在'); } if (updates.department_id && updates.department_id !== shop.department_id) { const newDepartment = await db('cf_department') .where('id', updates.department_id) .where('tenant_id', tenantId) .first(); if (!newDepartment) { throw new Error('目标部门不存在'); } updates.path = `${newDepartment.path}/${shopId}`; updates.depth = newDepartment.depth + 1; } if (updates.shop_code && updates.shop_code !== shop.shop_code) { const existingShop = await db('cf_shop') .where('tenant_id', tenantId) .where('shop_code', updates.shop_code) .whereNot('id', shopId) .first(); if (existingShop) { throw new Error('店铺编码已存在'); } } await db('cf_shop') .where('id', shopId) .update({ ...updates, updated_at: new Date(), }); await this.clearHierarchyCache(tenantId); await EventBusService.publish({ type: 'hierarchy.shop.updated', data: { shopId, tenantId, updates, timestamp: new Date(), } }); logger.info(`[Hierarchy] Updated shop ${shopId}`); return await db('cf_shop') .where('id', shopId) .first(); } /** * [BE-MT002-07] 删除店铺 */ static async deleteShop(shopId: string, tenantId: string): Promise { const shop = await db('cf_shop') .where('id', shopId) .where('tenant_id', tenantId) .first(); if (!shop) { throw new Error('店铺不存在'); } await db('cf_shop') .where('id', shopId) .delete(); await this.clearHierarchyCache(tenantId); await EventBusService.publish({ type: 'hierarchy.shop.deleted', data: { shopId, tenantId, timestamp: new Date(), } }); logger.info(`[Hierarchy] Deleted shop ${shopId}`); } /** * [BE-MT002-08] 获取层级统计 */ static async getHierarchyStats(tenantId: string): Promise { const cacheKey = `${this.CACHE_PREFIX}stats:${tenantId}`; const cached = await RedisService.get(cacheKey); if (cached) { return JSON.parse(cached); } const [deptCount, shopStats, maxDepth] = await Promise.all([ db('cf_department') .where('tenant_id', tenantId) .count('id as count') .first(), db('cf_shop') .where('tenant_id', tenantId) .select('status') .count('id as count') .groupBy('status'), db('cf_department') .where('tenant_id', tenantId) .max('depth as maxDepth') .first(), ]); const stats: HierarchyStats = { totalDepartments: Number(deptCount?.count || 0), totalShops: shopStats.reduce((sum, s) => sum + Number(s.count), 0), activeShops: Number(shopStats.find(s => s.status === 'ACTIVE')?.count || 0), inactiveShops: Number(shopStats.find(s => s.status === 'INACTIVE')?.count || 0), maxDepth: Number(maxDepth?.maxDepth || 0), }; await RedisService.set(cacheKey, JSON.stringify(stats), this.CACHE_TTL); return stats; } /** * [BE-MT002-09] 获取部门树 */ static async getDepartmentTree(tenantId: string): Promise { const cacheKey = `${this.CACHE_PREFIX}dept_tree:${tenantId}`; const cached = await RedisService.get(cacheKey); if (cached) { return JSON.parse(cached); } const departments = await db('cf_department') .where('tenant_id', tenantId) .where('status', 'ACTIVE') .orderBy('depth') .orderBy('name') .select('id', 'name', 'parent_id', 'path', 'depth'); const nodeMap = new Map(); const rootNodes: HierarchyNode[] = []; departments.forEach(dept => { nodeMap.set(dept.id, { id: dept.id, type: 'DEPARTMENT', parentId: dept.parent_id, name: dept.name, path: dept.path, depth: dept.depth, children: [], }); }); nodeMap.forEach(node => { if (node.parentId && nodeMap.has(node.parentId)) { const parent = nodeMap.get(node.parentId)!; parent.children = parent.children || []; parent.children.push(node); } else { rootNodes.push(node); } }); await RedisService.set(cacheKey, JSON.stringify(rootNodes), this.CACHE_TTL); return rootNodes; } /** * [BE-MT002-10] 获取部门下的店铺 */ static async getDepartmentShops( departmentId: string, tenantId: string, includeChildren: boolean = false ): Promise { let query = db('cf_shop') .where('tenant_id', tenantId); if (includeChildren) { const department = await db('cf_department') .where('id', departmentId) .first('path'); if (!department) { return []; } const childDeptIds = await db('cf_department') .where('tenant_id', tenantId) .where('path', 'like', `${department.path}%`) .pluck('id'); query = query.whereIn('department_id', childDeptIds); } else { query = query.where('department_id', departmentId); } return query.orderBy('name'); } /** * [BE-MT002-11] 获取用户层级上下文 */ static async getUserHierarchyContext(userId: string): Promise<{ tenantId: string; departmentId?: string; shopId?: string; hierarchyPath: string; }> { const user = await db('cf_user') .where('id', userId) .first('tenant_id', 'department_id', 'shop_id', 'hierarchy_path'); if (!user) { throw new Error('用户不存在'); } return { tenantId: user.tenant_id, departmentId: user.department_id, shopId: user.shop_id, hierarchyPath: user.hierarchy_path || '', }; } /** * [BE-MT002-12] 更新用户层级绑定 */ static async updateUserHierarchy( userId: string, tenantId: string, departmentId?: string, shopId?: string ): Promise { let hierarchyPath = `/${tenantId}`; if (departmentId) { const department = await db('cf_department') .where('id', departmentId) .where('tenant_id', tenantId) .first('path'); if (department) { hierarchyPath = department.path; } } if (shopId) { const shop = await db('cf_shop') .where('id', shopId) .where('tenant_id', tenantId) .first('path'); if (shop) { hierarchyPath = shop.path; } } await db('cf_user') .where('id', userId) .update({ tenant_id: tenantId, department_id: departmentId || null, shop_id: shopId || null, hierarchy_path: hierarchyPath, updated_at: new Date(), }); await EventBusService.publish({ type: 'hierarchy.user.updated', data: { userId, tenantId, departmentId, shopId, timestamp: new Date(), } }); logger.info(`[Hierarchy] Updated user ${userId} hierarchy binding`); } /** * 验证父节点变更 */ private static async validateParentChange( departmentId: string, newParentId: string | null, tenantId: string ): Promise { if (!newParentId) return; if (departmentId === newParentId) { throw new Error('不能将部门设为自己的子部门'); } const newParent = await db('cf_department') .where('id', newParentId) .where('tenant_id', tenantId) .first('path'); if (!newParent) { throw new Error('父部门不存在'); } if (newParent.path.includes(departmentId)) { throw new Error('不能将部门移动到自己的子部门下'); } } /** * 更新子节点路径 */ private static async updateChildrenPaths( departmentId: string, newPath: string, tenantId: string ): Promise { const children = await db('cf_department') .where('tenant_id', tenantId) .where('path', 'like', `%/${departmentId}/%`) .select('id', 'path'); for (const child of children) { const updatedPath = child.path.replace( new RegExp(`.*${departmentId}`), newPath ); await db('cf_department') .where('id', child.id) .update({ path: updatedPath }); } const shops = await db('cf_shop') .where('tenant_id', tenantId) .where('path', 'like', `%/${departmentId}/%`) .select('id', 'path'); for (const shop of shops) { const updatedPath = shop.path.replace( new RegExp(`.*${departmentId}`), newPath ); await db('cf_shop') .where('id', shop.id) .update({ path: updatedPath }); } } /** * 清除层级缓存 */ private static async clearHierarchyCache(tenantId: string): Promise { await RedisService.deletePattern(`${this.CACHE_PREFIX}*:${tenantId}*`); logger.debug(`[Hierarchy] Cleared cache for tenant ${tenantId}`); } }