refactor(services): 重构服务模块结构,按功能分类移动文件

将服务文件按功能分类移动到对应子目录,包括财务、营销、订单等模块
更新相关路由和导入路径,修复文件引用错误
归档旧版任务文档,更新README和任务统计信息
This commit is contained in:
2026-03-23 15:41:50 +08:00
parent 2b86715c09
commit e59d7c6620
156 changed files with 14658 additions and 7774 deletions

View File

@@ -0,0 +1,711 @@
/**
* [BE-MT002] 层级管理服务
* 负责商户→部门→店铺三层层级结构的管理
* AI注意: 所有层级操作必须通过此服务进行
*/
import db from '../config/database';
import { logger } from '../utils/logger';
import { EventBusService } from './EventBusService';
import RedisService from './RedisService';
import { DataIsolationService, HierarchyLevel, HierarchyNode, DataIsolationContext } from './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-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}`);
}
}