refactor(dashboard): 重构用户管理页面和路由结构 feat(server): 实现部门管理API和RBAC增强功能 docs: 更新用户手册和管理员指南文档 style: 统一图标使用和组件命名规范 test: 添加部门服务和数据隔离测试用例 chore: 更新依赖和配置文件
838 lines
20 KiB
TypeScript
838 lines
20 KiB
TypeScript
/**
|
|
* [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<void> {
|
|
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<Department> = {
|
|
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<Department> {
|
|
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<Department> = {
|
|
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<Department>
|
|
): Promise<Department> {
|
|
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<void> {
|
|
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<Department> {
|
|
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<any> {
|
|
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<any> {
|
|
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<Shop> {
|
|
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<Shop> = {
|
|
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<Shop>
|
|
): Promise<Shop> {
|
|
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<void> {
|
|
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<HierarchyStats> {
|
|
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<HierarchyNode[]> {
|
|
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<string, HierarchyNode>();
|
|
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<Shop[]> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
await RedisService.deletePattern(`${this.CACHE_PREFIX}*:${tenantId}*`);
|
|
logger.debug(`[Hierarchy] Cleared cache for tenant ${tenantId}`);
|
|
}
|
|
}
|