feat: 添加部门管理功能、主题切换和多语言支持

refactor(dashboard): 重构用户管理页面和路由结构

feat(server): 实现部门管理API和RBAC增强功能

docs: 更新用户手册和管理员指南文档

style: 统一图标使用和组件命名规范

test: 添加部门服务和数据隔离测试用例

chore: 更新依赖和配置文件
This commit is contained in:
2026-03-28 22:52:12 +08:00
parent 22308fe042
commit d327706087
87 changed files with 21372 additions and 4806 deletions

11
server/jest.config.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleDirectories: ['node_modules', 'src']
};

View File

@@ -5,8 +5,10 @@
"main": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"start:simple": "ts-node src/simple-server.ts",
"build": "tsc",
"dev": "ts-node-dev src/index.ts",
"dev:simple": "ts-node-dev src/simple-server.ts",
"test": "jest",
"lint": "eslint src --ext .ts"
},

View File

@@ -0,0 +1,74 @@
import { DataIsolationService } from '../../services/integration/DataIsolationService';
// 模拟数据库连接
const mockDb = {
where: jest.fn().mockReturnThis(),
first: jest.fn(),
select: jest.fn(),
from: jest.fn().mockReturnThis(),
table: jest.fn().mockReturnThis(),
raw: jest.fn(),
schema: {
hasTable: jest.fn()
},
pluck: jest.fn(),
whereIn: jest.fn().mockReturnThis()
};
// 模拟RedisService
const mockRedisService = {
get: jest.fn(),
set: jest.fn(),
deletePattern: jest.fn()
};
// 模拟数据库和Redis服务
jest.mock('../../config/database', () => mockDb);
jest.mock('../../services/utils/RedisService', () => mockRedisService);
describe('DataIsolationService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('buildIsolationQuery should work with tenant isolation', () => {
const context = {
tenantId: 'tenant-1',
userId: 'user-1',
role: 'OPERATOR',
permissions: [],
hierarchyPath: ''
};
const result = DataIsolationService.buildIsolationQuery('cf_product', context);
expect(result).toBeDefined();
});
test('buildIsolationQuery should work with department isolation', () => {
const context = {
tenantId: 'tenant-1',
departmentId: 'dept-1',
userId: 'user-1',
role: 'MANAGER',
permissions: [],
hierarchyPath: ''
};
const result = DataIsolationService.buildIsolationQuery('cf_product', context);
expect(result).toBeDefined();
});
test('buildIsolationQuery should work with multiple departments isolation', () => {
const context = {
tenantId: 'tenant-1',
departmentIds: ['dept-1', 'dept-2'],
userId: 'user-1',
role: 'MANAGER',
permissions: [],
hierarchyPath: ''
};
const result = DataIsolationService.buildIsolationQuery('cf_product', context);
expect(result).toBeDefined();
});
});

View File

@@ -0,0 +1,169 @@
import { Request, Response } from 'express';
import { HierarchyService } from '../../services/tenant/HierarchyService';
import { logger } from '../../utils/logger';
export class DepartmentController {
static async createDepartment(req: Request, res: Response) {
try {
const { tenantId } = req.user;
const { name, parentId, managerId } = req.body;
if (!name) {
return res.status(400).json({ success: false, error: '部门名称不能为空' });
}
const department = await HierarchyService.createDepartment(
tenantId,
name,
parentId || null,
managerId
);
res.json({ success: true, data: department });
} catch (error: unknown) {
logger.error('[DepartmentController] Create department failed', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : '创建部门失败'
});
}
}
static async updateDepartment(req: Request, res: Response) {
try {
const { tenantId } = req.user;
const { id } = req.params;
const updates = req.body;
const department = await HierarchyService.updateDepartment(
id,
tenantId,
updates
);
res.json({ success: true, data: department });
} catch (error: unknown) {
logger.error('[DepartmentController] Update department failed', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : '更新部门失败'
});
}
}
static async deleteDepartment(req: Request, res: Response) {
try {
const { tenantId } = req.user;
const { id } = req.params;
await HierarchyService.deleteDepartment(id, tenantId);
res.json({ success: true, message: '部门删除成功' });
} catch (error: unknown) {
logger.error('[DepartmentController] Delete department failed', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : '删除部门失败'
});
}
}
static async getDepartmentTree(req: Request, res: Response) {
try {
const { tenantId } = req.user;
const tree = await HierarchyService.getDepartmentTree(tenantId);
res.json({ success: true, data: tree });
} catch (error: unknown) {
logger.error('[DepartmentController] Get department tree failed', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : '获取部门树失败'
});
}
}
static async assignManager(req: Request, res: Response) {
try {
const { tenantId, id: assignedBy } = req.user;
const { departmentId } = req.params;
const { managerId } = req.body;
if (!managerId) {
return res.status(400).json({ success: false, error: '负责人ID不能为空' });
}
const department = await HierarchyService.assignDepartmentManager(
departmentId,
tenantId,
managerId,
assignedBy
);
res.json({ success: true, data: department });
} catch (error: unknown) {
logger.error('[DepartmentController] Assign manager failed', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : '设置部门负责人失败'
});
}
}
static async getManager(req: Request, res: Response) {
try {
const { tenantId } = req.user;
const { departmentId } = req.params;
const manager = await HierarchyService.getDepartmentManager(
departmentId,
tenantId
);
res.json({ success: true, data: manager });
} catch (error: unknown) {
logger.error('[DepartmentController] Get manager failed', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : '获取部门负责人失败'
});
}
}
static async getDepartmentStats(req: Request, res: Response) {
try {
const { tenantId } = req.user;
const { departmentId } = req.params;
const stats = await HierarchyService.getDepartmentStats(
departmentId,
tenantId
);
res.json({ success: true, data: stats });
} catch (error: unknown) {
logger.error('[DepartmentController] Get department stats failed', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : '获取部门统计失败'
});
}
}
static async getHierarchyStats(req: Request, res: Response) {
try {
const { tenantId } = req.user;
const stats = await HierarchyService.getHierarchyStats(tenantId);
res.json({ success: true, data: stats });
} catch (error: unknown) {
logger.error('[DepartmentController] Get hierarchy stats failed', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : '获取层级统计失败'
});
}
}
}

View File

@@ -0,0 +1,18 @@
import { Router } from 'express';
import { DepartmentController } from '../controllers/DepartmentController';
import { authenticate } from '../../middleware/auth';
const router = Router();
router.use(authenticate);
router.post('/', DepartmentController.createDepartment);
router.put('/:id', DepartmentController.updateDepartment);
router.delete('/:id', DepartmentController.deleteDepartment);
router.get('/tree', DepartmentController.getDepartmentTree);
router.get('/stats/hierarchy', DepartmentController.getHierarchyStats);
router.post('/:departmentId/manager', DepartmentController.assignManager);
router.get('/:departmentId/manager', DepartmentController.getManager);
router.get('/:departmentId/stats', DepartmentController.getDepartmentStats);
export default router;

View File

@@ -4,6 +4,7 @@ import productRoutes from './product';
import orderRoutes from './order';
import financeRoutes from './finance';
import syncRoutes from './sync';
import departmentRoutes from './department';
// import monitoringRoutes from './monitoring';
// import operationAgentRoutes from './operation-agent';
// import aiSelfImprovementRoutes from './ai-self-improvement';
@@ -26,6 +27,7 @@ router.use('/products', productRoutes);
router.use('/orders', orderRoutes);
router.use('/finance', financeRoutes);
router.use('/sync', syncRoutes);
router.use('/departments', departmentRoutes);
// router.use('/monitoring', monitoringRoutes);
// router.use('/telemetry', telemetryRoutes);
// router.use('/ai', aiRoutes);

View File

@@ -115,6 +115,80 @@ const createTables = async () => {
});
}
// 部门表
if (!(await db.schema.hasTable('cf_department'))) {
await db.schema.createTable('cf_department', (table) => {
table.string('id').primary();
table.string('tenant_id').notNullable();
table.string('name').notNullable();
table.string('parent_id').nullable();
table.string('path').notNullable();
table.integer('depth').notNullable();
table.timestamp('created_at').defaultTo(db.fn.now());
table.timestamp('updated_at').defaultTo(db.fn.now());
table.timestamp('deleted_at').nullable();
});
}
// 店铺表
if (!(await db.schema.hasTable('cf_shop'))) {
await db.schema.createTable('cf_shop', (table) => {
table.string('id').primary();
table.string('tenant_id').notNullable();
table.string('department_id').notNullable();
table.string('name').notNullable();
table.string('platform').notNullable();
table.string('shop_id').notNullable();
table.string('status').notNullable();
table.json('config').nullable();
table.string('path').notNullable();
table.integer('depth').notNullable();
table.timestamp('created_at').defaultTo(db.fn.now());
table.timestamp('updated_at').defaultTo(db.fn.now());
table.timestamp('deleted_at').nullable();
// 唯一约束
table.unique(['platform', 'shop_id']);
});
}
// 用户-部门关联表
if (!(await db.schema.hasTable('cf_user_department'))) {
await db.schema.createTable('cf_user_department', (table) => {
table.string('id').primary();
table.string('tenant_id').notNullable();
table.string('user_id').notNullable();
table.string('department_id').notNullable();
table.timestamp('created_at').defaultTo(db.fn.now());
table.timestamp('updated_at').defaultTo(db.fn.now());
// 唯一约束
table.unique(['user_id', 'department_id']);
});
}
// 店铺成员表
if (!(await db.schema.hasTable('cf_shop_member'))) {
await db.schema.createTable('cf_shop_member', (table) => {
table.string('id').primary();
table.string('shop_id').notNullable();
table.string('user_id').notNullable();
table.enum('role', ['owner', 'admin', 'operator', 'viewer']).notNullable();
table.json('permissions').notNullable();
table.string('assigned_by').notNullable();
table.timestamp('assigned_at').notNullable();
table.timestamp('created_at').defaultTo(db.fn.now());
table.timestamp('updated_at').defaultTo(db.fn.now());
// 索引
table.index(['shop_id']);
table.index(['user_id']);
// 唯一约束
table.unique(['shop_id', 'user_id']);
});
}
// 主权授信池表
if (!(await db.schema.hasTable('cf_sov_credit_pool'))) {
await db.schema.createTable('cf_sov_credit_pool', (table) => {

View File

@@ -1,5 +1,6 @@
import db from '../../config/database';
import { logger } from '../../utils/logger';
import { AuditService } from '../../services/audit/AuditService';
export interface Permission {
id: string;
@@ -304,6 +305,130 @@ export class RBACEngine {
}
}
static async createRole(roleCode: string, roleName: string, permissions: string[], tenantId?: string): Promise<boolean> {
try {
// 检查角色是否已存在
const existingRole = await db(this.ROLES_TABLE).where('code', roleCode).first();
if (existingRole) {
logger.error(`[RBAC] Role ${roleCode} already exists`);
return false;
}
await db(this.ROLES_TABLE).insert({
code: roleCode,
name: roleName,
permissions: JSON.stringify(permissions)
});
logger.info(`[RBAC] Created role ${roleCode} with ${permissions.length} permissions`);
return true;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[RBAC] Failed to create role: ${errorMessage}`);
return false;
}
}
static async updateRole(roleCode: string, roleName?: string, permissions?: string[]): Promise<boolean> {
try {
const updateData: any = {};
if (roleName) updateData.name = roleName;
if (permissions) updateData.permissions = JSON.stringify(permissions);
if (Object.keys(updateData).length === 0) {
return true;
}
const result = await db(this.ROLES_TABLE)
.where('code', roleCode)
.update(updateData);
if (result > 0) {
logger.info(`[RBAC] Updated role ${roleCode}`);
return true;
}
logger.error(`[RBAC] Role ${roleCode} not found`);
return false;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[RBAC] Failed to update role: ${errorMessage}`);
return false;
}
}
static async deleteRole(roleCode: string): Promise<boolean> {
try {
// 不允许删除系统预设角色
const systemRoles = Object.keys(ROLE_PERMISSIONS);
if (systemRoles.includes(roleCode)) {
logger.error(`[RBAC] Cannot delete system role ${roleCode}`);
return false;
}
// 检查是否有用户使用此角色
const userRolesCount = await db(this.USER_ROLES_TABLE)
.where('role_code', roleCode)
.count('id as count')
.first();
if (userRolesCount && (userRolesCount.count as number) > 0) {
logger.error(`[RBAC] Cannot delete role ${roleCode} - it's assigned to users`);
return false;
}
const result = await db(this.ROLES_TABLE)
.where('code', roleCode)
.delete();
if (result > 0) {
logger.info(`[RBAC] Deleted role ${roleCode}`);
return true;
}
logger.error(`[RBAC] Role ${roleCode} not found`);
return false;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[RBAC] Failed to delete role: ${errorMessage}`);
return false;
}
}
static async getRole(roleCode: string): Promise<Role | null> {
try {
const role = await db(this.ROLES_TABLE).where('code', roleCode).first();
if (!role) {
return null;
}
return {
code: role.code,
name: role.name,
permissions: JSON.parse(role.permissions || '[]')
};
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[RBAC] Failed to get role: ${errorMessage}`);
return null;
}
}
static async getAllRoles(): Promise<Role[]> {
try {
const roles = await db(this.ROLES_TABLE).select();
return roles.map(role => ({
code: role.code,
name: role.name,
permissions: JSON.parse(role.permissions || '[]')
}));
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[RBAC] Failed to get all roles: ${errorMessage}`);
return [];
}
}
static async getUserRoles(userId: string, tenantId: string): Promise<string[]> {
const userRoles = await db(this.USER_ROLES_TABLE)
.where({ user_id: userId, tenant_id: tenantId })

View File

@@ -33,6 +33,7 @@ export default class DatabaseSchema {
await this.initCompetitorPriceTables();
await this.initCurrencyTables();
await this.initBatchOperationTables();
await this.initInvitationTables();
logger.info('[DatabaseSchema] All tables initialized successfully');
}
@@ -1278,4 +1279,46 @@ export default class DatabaseSchema {
logger.info('[DatabaseSchema] Table cf_competitor_monitor_configs created');
}
}
// 邀请表
private static async initInvitationTables(): Promise<void> {
// 邀请表
const hasInvitationTable = await db.schema.hasTable('cf_invitation');
if (!hasInvitationTable) {
logger.info('[DatabaseSchema] Creating cf_invitation table...');
await db.schema.createTable('cf_invitation', (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 36).notNullable().index();
table.string('inviter_id', 36).notNullable();
table.string('email', 128).notNullable().index();
table.string('code', 64).notNullable().unique().index();
table.string('token', 128).notNullable().unique();
table.string('role', 32).defaultTo('OPERATOR');
table.integer('expires_in').defaultTo(7 * 24 * 60 * 60); // 7天
table.enum('status', ['PENDING', 'ACCEPTED', 'EXPIRED', 'CANCELLED']).defaultTo('PENDING');
table.string('created_by', 36).notNullable();
table.string('updated_by', 36).notNullable();
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.datetime('updated_at').notNullable().defaultTo(db.fn.now());
});
logger.info('[DatabaseSchema] Table cf_invitation created');
}
// 邀请记录表
const hasInvitationLogTable = await db.schema.hasTable('cf_invitation_log');
if (!hasInvitationLogTable) {
logger.info('[DatabaseSchema] Creating cf_invitation_log table...');
await db.schema.createTable('cf_invitation_log', (table) => {
table.increments('id').primary();
table.string('invitation_id', 36).notNullable().index();
table.string('tenant_id', 36).notNullable().index();
table.string('action', 32).notNullable();
table.string('user_id', 36).notNullable();
table.string('ip', 64);
table.text('metadata');
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
});
logger.info('[DatabaseSchema] Table cf_invitation_log created');
}
}
}

View File

@@ -4,14 +4,17 @@ import { v4 as uuidv4 } from 'uuid';
import * as bcrypt from 'bcrypt';
import { MailService } from '../../core/mail/MailService';
import db from '../../config/database';
import { InvitationService } from '../../services/auth/InvitationService';
@Injectable()
export class RegistrationService {
private readonly logger = new Logger(RegistrationService.name);
private readonly mailService: MailService;
private readonly invitationService: InvitationService;
constructor() {
this.mailService = MailService.getInstance();
this.invitationService = new InvitationService();
}
async register(
@@ -21,7 +24,8 @@ export class RegistrationService {
companyName: string,
phone: string,
businessType: string,
): Promise<{ userId: string; verificationToken: string }> {
inviteToken?: string
): Promise<{ userId: string; verificationToken: string; tenantId: string }> {
try {
const existingUser = await db('cf_user')
.where('username', username)
@@ -39,6 +43,33 @@ export class RegistrationService {
const hashedPassword = await bcrypt.hash(password, 10);
const verificationToken = uuidv4();
const userId = uuidv4();
let tenantId: string;
let role = 'OPERATOR';
if (inviteToken) {
// 处理邀请注册
const invitation = await this.invitationService.verifyInvitation(inviteToken);
tenantId = invitation.tenant_id;
role = invitation.role;
} else {
// 处理新租户注册
tenantId = `tenant-${userId}`;
// 创建租户
await db('cf_tenant').insert({
id: tenantId,
name: companyName,
domain: `${username.toLowerCase()}.crawlful.com`,
contact_name: username,
contact_email: email,
contact_phone: phone,
status: 'ACTIVE',
created_by: userId,
updated_by: userId,
created_at: new Date(),
updated_at: new Date()
});
}
await db('cf_user').insert({
id: userId,
@@ -48,17 +79,22 @@ export class RegistrationService {
companyName,
phone,
businessType,
role: 'OPERATOR',
role,
status: 'active',
tenantId: `tenant-${userId}`,
tenantId,
createdAt: new Date(),
updatedAt: new Date()
});
if (inviteToken) {
// 接受邀请
await this.invitationService.acceptInvitation(inviteToken, userId);
}
await this.mailService.sendWelcomeEmail(email, username);
this.logger.log(`Registered new user: ${username} (${email})`);
return { userId, verificationToken };
return { userId, verificationToken, tenantId };
} catch (error) {
this.logger.error('Failed to register user', error);
throw error;

View File

@@ -0,0 +1,493 @@
/**
* [BE-DS001] 数据统计服务
* 负责多维度数据统计,解决多用户共享店铺的数据虚高问题
* AI注意: 统计时必须对店铺进行去重,避免数据虚高
*/
import db from '../../config/database';
import { logger } from '../../utils/logger';
import { DataIsolationService } from '../integration/DataIsolationService';
export interface ShopStatistics {
totalShops: number;
activeShops: number;
inactiveShops: number;
shopDetails: Array<{
id: string;
name: string;
platform: string;
status: string;
departmentId: string;
}>;
}
export interface OrderStatistics {
totalOrders: number;
totalAmount: number;
pendingOrders: number;
completedOrders: number;
cancelledOrders: number;
orderDetails: Array<{
id: string;
orderId: string;
shopId: string;
platform: string;
amount: number;
status: string;
createdAt: string;
}>;
}
export interface UserStatistics {
userId: string;
userName: string;
shopCount: number;
orderCount: number;
totalAmount: number;
uniqueShops: string[];
}
export interface DepartmentStatistics {
departmentId: string;
departmentName: string;
userCount: number;
shopCount: number;
orderCount: number;
totalAmount: number;
uniqueShops: string[];
}
export class DataStatisticsService {
/**
* 获取用户维度的店铺统计(去重)
* 解决多用户共享同一店铺导致的虚高问题
*/
static async getUserShopStatistics(
userId: string,
tenantId: string,
startDate?: string,
endDate?: string
): Promise<ShopStatistics> {
try {
const context = {
tenantId,
userId,
role: 'OPERATOR',
permissions: [],
hierarchyPath: '',
};
const isolationQuery = DataIsolationService.buildIsolationQuery('cf_shop', context);
const shops = await db('cf_shop')
.where(isolationQuery.where)
.modify(isolationQuery.modify)
.select('id', 'name', 'platform', 'status', 'department_id');
const totalShops = shops.length;
const activeShops = shops.filter(s => s.status === 'active').length;
const inactiveShops = shops.filter(s => s.status === 'inactive').length;
logger.info('[DataStatisticsService] User shop statistics calculated', {
userId,
tenantId,
totalShops,
activeShops,
inactiveShops,
});
return {
totalShops,
activeShops,
inactiveShops,
shopDetails: shops.map(shop => ({
id: shop.id,
name: shop.name,
platform: shop.platform,
status: shop.status,
departmentId: shop.department_id,
})),
};
} catch (error) {
logger.error('[DataStatisticsService] Failed to calculate user shop statistics', {
userId,
tenantId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* 获取用户维度的订单统计(去重)
* 解决多用户共享店铺导致的订单虚高问题
*/
static async getUserOrderStatistics(
userId: string,
tenantId: string,
startDate?: string,
endDate?: string
): Promise<OrderStatistics> {
try {
const context = {
tenantId,
userId,
role: 'OPERATOR',
permissions: [],
hierarchyPath: '',
};
const isolationQuery = DataIsolationService.buildIsolationQuery('cf_order', context);
let query = db('cf_order')
.where(isolationQuery.where)
.modify(isolationQuery.modify);
if (startDate) {
query = query.where('created_at', '>=', startDate);
}
if (endDate) {
query = query.where('created_at', '<=', endDate);
}
const orders = await query.select(
'id',
'order_id',
'shop_id',
'platform',
'total_amount',
'status',
'created_at'
);
const totalOrders = orders.length;
const totalAmount = orders.reduce((sum, order) => sum + Number(order.total_amount), 0);
const pendingOrders = orders.filter(o => o.status === 'pending').length;
const completedOrders = orders.filter(o => o.status === 'completed').length;
const cancelledOrders = orders.filter(o => o.status === 'cancelled').length;
logger.info('[DataStatisticsService] User order statistics calculated', {
userId,
tenantId,
totalOrders,
totalAmount,
pendingOrders,
completedOrders,
cancelledOrders,
});
return {
totalOrders,
totalAmount,
pendingOrders,
completedOrders,
cancelledOrders,
orderDetails: orders.map(order => ({
id: order.id,
orderId: order.order_id,
shopId: order.shop_id,
platform: order.platform,
amount: Number(order.total_amount),
status: order.status,
createdAt: order.created_at,
})),
};
} catch (error) {
logger.error('[DataStatisticsService] Failed to calculate user order statistics', {
userId,
tenantId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* 获取部门维度的统计(去重)
* 解决部门内多用户共享店铺导致的虚高问题
*/
static async getDepartmentStatistics(
departmentId: string,
tenantId: string,
startDate?: string,
endDate?: string
): Promise<DepartmentStatistics> {
try {
const context = {
tenantId,
userId: '',
role: 'MANAGER',
permissions: [],
hierarchyPath: '',
};
const shopIsolationQuery = DataIsolationService.buildIsolationQuery('cf_shop', context);
const orderIsolationQuery = DataIsolationService.buildIsolationQuery('cf_order', context);
const shops = await db('cf_shop')
.where('department_id', departmentId)
.where(shopIsolationQuery.where)
.modify(shopIsolationQuery.modify)
.select('id', 'name', 'platform', 'status');
let orderQuery = db('cf_order')
.whereIn('shop_id', shops.map(s => s.id))
.where(orderIsolationQuery.where)
.modify(orderIsolationQuery.modify);
if (startDate) {
orderQuery = orderQuery.where('created_at', '>=', startDate);
}
if (endDate) {
orderQuery = orderQuery.where('created_at', '<=', endDate);
}
const orders = await orderQuery.select('id', 'shop_id', 'total_amount', 'status');
const users = await db('cf_user_department')
.where('department_id', departmentId)
.join('cf_user', 'cf_user.id', 'cf_user_department.user_id')
.select('cf_user.id', 'cf_user.name');
const shopCount = shops.length;
const orderCount = orders.length;
const totalAmount = orders.reduce((sum, order) => sum + Number(order.total_amount), 0);
const uniqueShops = Array.from(new Set(orders.map(order => order.shop_id)));
logger.info('[DataStatisticsService] Department statistics calculated', {
departmentId,
tenantId,
userCount: users.length,
shopCount,
orderCount,
totalAmount,
});
return {
departmentId,
departmentName: shops[0]?.name || 'Unknown',
userCount: users.length,
shopCount,
orderCount,
totalAmount,
uniqueShops,
};
} catch (error) {
logger.error('[DataStatisticsService] Failed to calculate department statistics', {
departmentId,
tenantId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* 获取多用户维度的汇总统计(去重)
* 解决多用户共享店铺导致的汇总虚高问题
*/
static async getMultiUserStatistics(
userIds: string[],
tenantId: string,
startDate?: string,
endDate?: string
): Promise<{
userStatistics: UserStatistics[];
totalShops: number;
totalOrders: number;
totalAmount: number;
uniqueShops: string[];
}> {
try {
const userStatistics: UserStatistics[] = [];
const allShopIds = new Set<string>();
const allOrderIds = new Set<string>();
let totalAmount = 0;
for (const userId of userIds) {
const shopStats = await this.getUserShopStatistics(userId, tenantId);
const orderStats = await this.getUserOrderStatistics(userId, tenantId, startDate, endDate);
const userShopIds = shopStats.shopDetails.map(s => s.id);
const userOrderIds = orderStats.orderDetails.map(o => o.id);
userShopIds.forEach(shopId => allShopIds.add(shopId));
userOrderIds.forEach(orderId => allOrderIds.add(orderId));
const user = await db('cf_user').where('id', userId).first();
userStatistics.push({
userId,
userName: user?.name || 'Unknown',
shopCount: userShopIds.length,
orderCount: userOrderIds.length,
totalAmount: orderStats.totalAmount,
uniqueShops: userShopIds,
});
totalAmount += orderStats.totalAmount;
}
logger.info('[DataStatisticsService] Multi-user statistics calculated', {
userIds,
tenantId,
totalShops: allShopIds.size,
totalOrders: allOrderIds.size,
totalAmount,
});
return {
userStatistics,
totalShops: allShopIds.size,
totalOrders: allOrderIds.size,
totalAmount,
uniqueShops: Array.from(allShopIds),
};
} catch (error) {
logger.error('[DataStatisticsService] Failed to calculate multi-user statistics', {
userIds,
tenantId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* 获取租户维度的全局统计(去重)
* 解决租户内多部门多用户共享店铺导致的虚高问题
*/
static async getTenantStatistics(
tenantId: string,
startDate?: string,
endDate?: string
): Promise<{
totalUsers: number;
totalDepartments: number;
totalShops: number;
totalOrders: number;
totalAmount: number;
platformDistribution: Record<string, number>;
}> {
try {
const users = await db('cf_user').where('tenant_id', tenantId);
const departments = await db('cf_department').where('tenant_id', tenantId);
const shops = await db('cf_shop').where('tenant_id', tenantId);
let orderQuery = db('cf_order').where('tenant_id', tenantId);
if (startDate) {
orderQuery = orderQuery.where('created_at', '>=', startDate);
}
if (endDate) {
orderQuery = orderQuery.where('created_at', '<=', endDate);
}
const orders = await orderQuery.select('id', 'total_amount', 'platform');
const totalAmount = orders.reduce((sum, order) => sum + Number(order.total_amount), 0);
const platformDistribution: Record<string, number> = {};
shops.forEach(shop => {
platformDistribution[shop.platform] = (platformDistribution[shop.platform] || 0) + 1;
});
logger.info('[DataStatisticsService] Tenant statistics calculated', {
tenantId,
totalUsers: users.length,
totalDepartments: departments.length,
totalShops: shops.length,
totalOrders: orders.length,
totalAmount,
});
return {
totalUsers: users.length,
totalDepartments: departments.length,
totalShops: shops.length,
totalOrders: orders.length,
totalAmount,
platformDistribution,
};
} catch (error) {
logger.error('[DataStatisticsService] Failed to calculate tenant statistics', {
tenantId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* 获取店铺的实际拥有者统计
* 区分店铺的实际归属,避免重复计算
*/
static async getShopOwnershipStatistics(
tenantId: string
): Promise<Array<{
shopId: string;
shopName: string;
platform: string;
owners: Array<{
userId: string;
userName: string;
role: string;
assignedAt: string;
}>;
primaryOwner: {
userId: string;
userName: string;
role: string;
};
}>> {
try {
const shops = await db('cf_shop').where('tenant_id', tenantId).select('id', 'name', 'platform');
const shopOwnership = await Promise.all(
shops.map(async (shop) => {
const members = await db('cf_shop_member')
.where('shop_id', shop.id)
.join('cf_user', 'cf_user.id', 'cf_shop_member.user_id')
.select(
'cf_shop_member.user_id',
'cf_user.name as user_name',
'cf_shop_member.role',
'cf_shop_member.assigned_at'
);
const primaryOwner = members.find(m => m.role === 'owner') || members[0];
return {
shopId: shop.id,
shopName: shop.name,
platform: shop.platform,
owners: members.map(m => ({
userId: m.user_id,
userName: m.user_name,
role: m.role,
assignedAt: m.assigned_at,
})),
primaryOwner: primaryOwner ? {
userId: primaryOwner.user_id,
userName: primaryOwner.user_name,
role: primaryOwner.role,
} : null,
};
})
);
logger.info('[DataStatisticsService] Shop ownership statistics calculated', {
tenantId,
totalShops: shopOwnership.length,
});
return shopOwnership;
} catch (error) {
logger.error('[DataStatisticsService] Failed to calculate shop ownership statistics', {
tenantId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
}

View File

@@ -0,0 +1,317 @@
import db from '../config/database';
import { logger } from '../utils/logger';
interface AuditLog {
id: string;
tenantId: string;
userId: string;
action: string;
resourceType: string;
resourceId: string;
oldValue?: any;
newValue?: any;
ipAddress: string;
userAgent: string;
traceId: string;
businessType: 'TOC' | 'TOB';
createdAt: Date;
}
export class AuditService {
private static readonly AUDIT_LOG_TABLE = 'cf_audit_log';
/**
* 记录权限变更审计日志
*/
static async logPermissionChange(
tenantId: string,
userId: string,
action: string,
resourceType: string,
resourceId: string,
oldValue: any,
newValue: any,
ipAddress: string,
userAgent: string,
traceId: string,
businessType: 'TOC' | 'TOB' = 'TOC'
): Promise<boolean> {
try {
await db(this.AUDIT_LOG_TABLE).insert({
id: this.generateId(),
tenant_id: tenantId,
user_id: userId,
action,
resource_type: resourceType,
resource_id: resourceId,
old_value: oldValue ? JSON.stringify(oldValue) : null,
new_value: newValue ? JSON.stringify(newValue) : null,
ip_address: ipAddress,
user_agent: userAgent,
trace_id: traceId,
business_type: businessType,
created_at: new Date()
});
logger.info(`[AuditService] Logged permission change: ${action} on ${resourceType}:${resourceId}`, {
tenantId,
userId,
resourceId,
traceId
});
return true;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[AuditService] Failed to log permission change: ${errorMessage}`);
return false;
}
}
/**
* 记录角色创建审计日志
*/
static async logRoleCreation(
tenantId: string,
userId: string,
roleCode: string,
roleName: string,
permissions: string[],
ipAddress: string,
userAgent: string,
traceId: string
): Promise<boolean> {
return this.logPermissionChange(
tenantId,
userId,
'ROLE_CREATED',
'ROLE',
roleCode,
null,
{
roleCode,
roleName,
permissions
},
ipAddress,
userAgent,
traceId
);
}
/**
* 记录角色更新审计日志
*/
static async logRoleUpdate(
tenantId: string,
userId: string,
roleCode: string,
oldRole: any,
newRole: any,
ipAddress: string,
userAgent: string,
traceId: string
): Promise<boolean> {
return this.logPermissionChange(
tenantId,
userId,
'ROLE_UPDATED',
'ROLE',
roleCode,
oldRole,
newRole,
ipAddress,
userAgent,
traceId
);
}
/**
* 记录角色删除审计日志
*/
static async logRoleDeletion(
tenantId: string,
userId: string,
roleCode: string,
roleData: any,
ipAddress: string,
userAgent: string,
traceId: string
): Promise<boolean> {
return this.logPermissionChange(
tenantId,
userId,
'ROLE_DELETED',
'ROLE',
roleCode,
roleData,
null,
ipAddress,
userAgent,
traceId
);
}
/**
* 记录用户角色分配审计日志
*/
static async logUserRoleAssignment(
tenantId: string,
userId: string,
targetUserId: string,
roleCode: string,
ipAddress: string,
userAgent: string,
traceId: string
): Promise<boolean> {
return this.logPermissionChange(
tenantId,
userId,
'USER_ROLE_ASSIGNED',
'USER_ROLE',
`${targetUserId}:${roleCode}`,
null,
{
targetUserId,
roleCode
},
ipAddress,
userAgent,
traceId
);
}
/**
* 记录用户角色移除审计日志
*/
static async logUserRoleRemoval(
tenantId: string,
userId: string,
targetUserId: string,
roleCode: string,
ipAddress: string,
userAgent: string,
traceId: string
): Promise<boolean> {
return this.logPermissionChange(
tenantId,
userId,
'USER_ROLE_REMOVED',
'USER_ROLE',
`${targetUserId}:${roleCode}`,
{
targetUserId,
roleCode
},
null,
ipAddress,
userAgent,
traceId
);
}
/**
* 获取权限变更审计日志
*/
static async getPermissionAuditLogs(
tenantId: string,
limit: number = 50,
offset: number = 0
): Promise<AuditLog[]> {
try {
const logs = await db(this.AUDIT_LOG_TABLE)
.where('tenant_id', tenantId)
.whereIn('action', [
'ROLE_CREATED',
'ROLE_UPDATED',
'ROLE_DELETED',
'USER_ROLE_ASSIGNED',
'USER_ROLE_REMOVED'
])
.orderBy('created_at', 'desc')
.limit(limit)
.offset(offset)
.select();
return logs.map(log => ({
id: log.id,
tenantId: log.tenant_id,
userId: log.user_id,
action: log.action,
resourceType: log.resource_type,
resourceId: log.resource_id,
oldValue: log.old_value ? JSON.parse(log.old_value) : null,
newValue: log.new_value ? JSON.parse(log.new_value) : null,
ipAddress: log.ip_address,
userAgent: log.user_agent,
traceId: log.trace_id,
businessType: log.business_type,
createdAt: new Date(log.created_at)
}));
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[AuditService] Failed to get permission audit logs: ${errorMessage}`);
return [];
}
}
/**
* 搜索权限变更审计日志
*/
static async searchPermissionAuditLogs(
tenantId: string,
searchTerm: string,
limit: number = 50,
offset: number = 0
): Promise<AuditLog[]> {
try {
const logs = await db(this.AUDIT_LOG_TABLE)
.where('tenant_id', tenantId)
.whereIn('action', [
'ROLE_CREATED',
'ROLE_UPDATED',
'ROLE_DELETED',
'USER_ROLE_ASSIGNED',
'USER_ROLE_REMOVED'
])
.andWhere(qb => {
qb.where('action', 'like', `%${searchTerm}%`)
.orWhere('resource_id', 'like', `%${searchTerm}%`)
.orWhere('user_id', 'like', `%${searchTerm}%`);
})
.orderBy('created_at', 'desc')
.limit(limit)
.offset(offset)
.select();
return logs.map(log => ({
id: log.id,
tenantId: log.tenant_id,
userId: log.user_id,
action: log.action,
resourceType: log.resource_type,
resourceId: log.resource_id,
oldValue: log.old_value ? JSON.parse(log.old_value) : null,
newValue: log.new_value ? JSON.parse(log.new_value) : null,
ipAddress: log.ip_address,
userAgent: log.user_agent,
traceId: log.trace_id,
businessType: log.business_type,
createdAt: new Date(log.created_at)
}));
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[AuditService] Failed to search permission audit logs: ${errorMessage}`);
return [];
}
}
/**
* 生成唯一ID
*/
private static generateId(): string {
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
export default AuditService;

View File

@@ -0,0 +1,258 @@
import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
import * as crypto from 'crypto';
import db from '../../config/database';
import { MailService } from '../../core/mail/MailService';
interface Invitation {
id: string;
tenant_id: string;
inviter_id: string;
email: string;
code: string;
token: string;
role: string;
expires_in: number;
status: 'PENDING' | 'ACCEPTED' | 'EXPIRED' | 'CANCELLED';
created_by: string;
updated_by: string;
created_at: Date;
updated_at: Date;
}
interface InviteOptions {
tenantId: string;
inviterId: string;
email: string;
role?: string;
expiresIn?: number;
}
@Injectable()
export class InvitationService {
private readonly logger = new Logger(InvitationService.name);
private readonly mailService: MailService;
constructor() {
this.mailService = MailService.getInstance();
}
/**
* 生成邀请链接
*/
async generateInvitation(options: InviteOptions): Promise<{ invitation: Invitation; inviteLink: string }> {
try {
const { tenantId, inviterId, email, role = 'OPERATOR', expiresIn = 7 * 24 * 60 * 60 } = options;
// 检查租户是否存在
const tenant = await db('cf_tenant').where('id', tenantId).first();
if (!tenant) {
throw new NotFoundException('Tenant not found');
}
// 检查邀请者是否存在
const inviter = await db('cf_user').where('id', inviterId).first();
if (!inviter) {
throw new NotFoundException('Inviter not found');
}
// 生成邀请码和token
const code = this.generateInviteCode();
const token = uuidv4();
const invitationId = uuidv4();
// 创建邀请记录
await db('cf_invitation').insert({
id: invitationId,
tenant_id: tenantId,
inviter_id: inviterId,
email,
code,
token,
role,
expires_in: expiresIn,
status: 'PENDING',
created_by: inviterId,
updated_by: inviterId,
created_at: new Date(),
updated_at: new Date()
});
// 生成邀请链接
const inviteLink = `${process.env.FRONTEND_URL || 'http://localhost:8000'}/auth/register?invite=${token}&tenant=${tenantId}`;
// 发送邀请邮件
await this.mailService.sendInvitationEmail(email, inviter.username, tenant.name, inviteLink, expiresIn);
// 记录邀请日志
await this.logInvitationAction(invitationId, tenantId, inviterId, 'INVITE_SENT', {
email,
role,
expiresIn
});
const invitation = await db('cf_invitation').where('id', invitationId).first();
this.logger.log(`Generated invitation for ${email} to join tenant ${tenantId}`);
return { invitation, inviteLink };
} catch (error) {
this.logger.error('Failed to generate invitation', error);
throw error;
}
}
/**
* 验证邀请码
*/
async verifyInvitation(token: string): Promise<Invitation> {
try {
const invitation = await db('cf_invitation')
.where('token', token)
.where('status', 'PENDING')
.first();
if (!invitation) {
throw new BadRequestException('Invalid or expired invitation');
}
// 检查是否过期
const expiresAt = new Date(invitation.created_at);
expiresAt.setSeconds(expiresAt.getSeconds() + invitation.expires_in);
if (expiresAt < new Date()) {
// 标记为过期
await db('cf_invitation')
.where('id', invitation.id)
.update({
status: 'EXPIRED',
updated_at: new Date()
});
throw new BadRequestException('Invitation has expired');
}
return invitation;
} catch (error) {
this.logger.error('Failed to verify invitation', error);
throw error;
}
}
/**
* 接受邀请
*/
async acceptInvitation(token: string, userId: string): Promise<void> {
try {
const invitation = await this.verifyInvitation(token);
// 更新邀请状态
await db('cf_invitation')
.where('id', invitation.id)
.update({
status: 'ACCEPTED',
updated_at: new Date()
});
// 更新用户租户信息
await db('cf_user')
.where('id', userId)
.update({
tenant_id: invitation.tenant_id,
role: invitation.role,
updated_at: new Date()
});
// 记录接受日志
await this.logInvitationAction(invitation.id, invitation.tenant_id, userId, 'INVITE_ACCEPTED', {
userId
});
this.logger.log(`User ${userId} accepted invitation to join tenant ${invitation.tenant_id}`);
} catch (error) {
this.logger.error('Failed to accept invitation', error);
throw error;
}
}
/**
* 取消邀请
*/
async cancelInvitation(invitationId: string, userId: string): Promise<void> {
try {
const invitation = await db('cf_invitation').where('id', invitationId).first();
if (!invitation) {
throw new NotFoundException('Invitation not found');
}
await db('cf_invitation')
.where('id', invitationId)
.update({
status: 'CANCELLED',
updated_by: userId,
updated_at: new Date()
});
// 记录取消日志
await this.logInvitationAction(invitationId, invitation.tenant_id, userId, 'INVITE_CANCELLED', {});
this.logger.log(`Invitation ${invitationId} cancelled by ${userId}`);
} catch (error) {
this.logger.error('Failed to cancel invitation', error);
throw error;
}
}
/**
* 获取租户的邀请列表
*/
async getTenantInvitations(tenantId: string): Promise<Invitation[]> {
try {
const invitations = await db('cf_invitation')
.where('tenant_id', tenantId)
.orderBy('created_at', 'desc')
.select();
return invitations;
} catch (error) {
this.logger.error('Failed to get tenant invitations', error);
throw error;
}
}
/**
* 生成邀请码
*/
private generateInviteCode(): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let code = '';
for (let i = 0; i < 8; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
}
/**
* 记录邀请操作日志
*/
private async logInvitationAction(
invitationId: string,
tenantId: string,
userId: string,
action: string,
metadata: any
): Promise<void> {
try {
await db('cf_invitation_log').insert({
invitation_id: invitationId,
tenant_id: tenantId,
action,
user_id: userId,
metadata: JSON.stringify(metadata),
created_at: new Date()
});
} catch (error) {
this.logger.error('Failed to log invitation action', error);
// 日志记录失败不影响主流程
}
}
}

View File

@@ -0,0 +1,127 @@
import { Injectable, Logger } from '@nestjs/common';
import db from '../../config/database';
@Injectable()
export class InvitationStatsService {
private readonly logger = new Logger(InvitationStatsService.name);
/**
* 获取租户邀请统计
*/
async getTenantInvitationStats(tenantId: string): Promise<any> {
try {
const stats = await db('cf_invitation')
.where('tenant_id', tenantId)
.select(
db.raw('COUNT(*) as total'),
db.raw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as pending', ['PENDING']),
db.raw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as accepted', ['ACCEPTED']),
db.raw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as expired', ['EXPIRED']),
db.raw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as cancelled', ['CANCELLED'])
)
.first();
const recentInvitations = await db('cf_invitation')
.where('tenant_id', tenantId)
.orderBy('created_at', 'desc')
.limit(10)
.select(
'id',
'email',
'role',
'status',
'created_at',
'expires_in'
);
return {
...stats,
recentInvitations
};
} catch (error) {
this.logger.error('Failed to get tenant invitation stats', error);
throw error;
}
}
/**
* 获取用户邀请记录
*/
async getUserInvitationHistory(userId: string): Promise<any[]> {
try {
const invitations = await db('cf_invitation')
.where('inviter_id', userId)
.orWhere('created_by', userId)
.orderBy('created_at', 'desc')
.select(
'id',
'tenant_id',
'email',
'role',
'status',
'created_at',
'expires_in'
);
return invitations;
} catch (error) {
this.logger.error('Failed to get user invitation history', error);
throw error;
}
}
/**
* 获取系统邀请统计
*/
async getSystemInvitationStats(): Promise<any> {
try {
const stats = await db('cf_invitation')
.select(
db.raw('COUNT(*) as total'),
db.raw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as pending', ['PENDING']),
db.raw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as accepted', ['ACCEPTED']),
db.raw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as expired', ['EXPIRED']),
db.raw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as cancelled', ['CANCELLED'])
)
.first();
const monthlyStats = await db('cf_invitation')
.select(
db.raw('DATE_FORMAT(created_at, ?) as month', ['%Y-%m']),
db.raw('COUNT(*) as count')
)
.groupBy(db.raw('DATE_FORMAT(created_at, ?)', ['%Y-%m']))
.orderBy('month', 'desc')
.limit(6);
return {
...stats,
monthlyStats
};
} catch (error) {
this.logger.error('Failed to get system invitation stats', error);
throw error;
}
}
/**
* 清理过期邀请
*/
async cleanupExpiredInvitations(): Promise<number> {
try {
const result = await db('cf_invitation')
.where('status', 'PENDING')
.whereRaw('DATE_ADD(created_at, INTERVAL expires_in SECOND) < NOW()')
.update({
status: 'EXPIRED',
updated_at: new Date()
});
this.logger.log(`Cleaned up ${result} expired invitations`);
return result;
} catch (error) {
this.logger.error('Failed to cleanup expired invitations', error);
throw error;
}
}
}

View File

@@ -1,6 +1,7 @@
import db from '../../config/database';
import { logger } from '../../utils/logger';
import { RBACEngine, Permission, DataScope, UserRoleAssignment } from '../../core/auth/RBACEngine';
import { AuditService } from '../audit/AuditService';
export enum Role {
ADMIN = 'ADMIN',
@@ -164,6 +165,70 @@ export class RBACService {
return roleDefs[role] || [];
}
static async createRole(roleCode: string, roleName: string, permissions: string[], userId: string, ipAddress: string = 'unknown', userAgent: string = 'unknown', traceId: string = 'unknown'): Promise<boolean> {
const success = await RBACEngine.createRole(roleCode, roleName, permissions);
if (success) {
// 记录审计日志
await AuditService.logRoleCreation(
'system', // 暂时使用system作为tenantId实际应从上下文获取
userId,
roleCode,
roleName,
permissions,
ipAddress,
userAgent,
traceId
);
}
return success;
}
static async updateRole(roleCode: string, roleName?: string, permissions?: string[], userId: string = 'system', ipAddress: string = 'unknown', userAgent: string = 'unknown', traceId: string = 'unknown'): Promise<boolean> {
const oldRole = await RBACEngine.getRole(roleCode);
const success = await RBACEngine.updateRole(roleCode, roleName, permissions);
if (success && oldRole) {
const newRole = await RBACEngine.getRole(roleCode);
// 记录审计日志
await AuditService.logRoleUpdate(
'system', // 暂时使用system作为tenantId实际应从上下文获取
userId,
roleCode,
oldRole,
newRole,
ipAddress,
userAgent,
traceId
);
}
return success;
}
static async deleteRole(roleCode: string, userId: string = 'system', ipAddress: string = 'unknown', userAgent: string = 'unknown', traceId: string = 'unknown'): Promise<boolean> {
const roleData = await RBACEngine.getRole(roleCode);
const success = await RBACEngine.deleteRole(roleCode);
if (success && roleData) {
// 记录审计日志
await AuditService.logRoleDeletion(
'system', // 暂时使用system作为tenantId实际应从上下文获取
userId,
roleCode,
roleData,
ipAddress,
userAgent,
traceId
);
}
return success;
}
static async getRole(roleCode: string): Promise<any> {
return RBACEngine.getRole(roleCode);
}
static async getAllRoles(): Promise<any[]> {
return RBACEngine.getAllRoles();
}
static async updateUserRoleScope(
userId: string,
tenantId: string,

View File

@@ -1,5 +1,7 @@
import db from '../../config/database';
import { v4 as uuidv4 } from 'uuid';
import { cacheService } from '../CacheService';
import { BullMQService } from '../core/BullMQService';
export interface User {
id: string;
@@ -12,7 +14,18 @@ export interface User {
updatedAt: Date;
}
export interface UserListParams {
page?: number;
pageSize?: number;
role?: string;
status?: string;
search?: string;
}
export class UserService {
private static readonly CACHE_PREFIX = 'user:';
private static readonly CACHE_TTL = 3600; // 1 hour
static async createUser(userData: Partial<User> & { password: string }): Promise<{ success: boolean; userId?: string; error?: string }> {
try {
const userId = uuidv4();
@@ -26,6 +39,10 @@ export class UserService {
created_at: new Date(),
updated_at: new Date()
});
// 清除相关缓存
await this.clearUserCache(userData.tenantId);
return { success: true, userId };
} catch (error: any) {
return { success: false, error: error.message };
@@ -33,7 +50,21 @@ export class UserService {
}
static async getUserById(userId: string): Promise<User | null> {
const cacheKey = `${this.CACHE_PREFIX}${userId}`;
// 尝试从缓存获取
const cachedUser = await cacheService.get<User>(cacheKey);
if (cachedUser) {
return cachedUser;
}
// 从数据库获取
const user = await db('cf_users').where({ id: userId }).first();
if (user) {
// 存入缓存
await cacheService.set(cacheKey, user, { ttl: this.CACHE_TTL });
}
return user || null;
}
@@ -42,15 +73,164 @@ export class UserService {
...updateData,
updated_at: new Date()
});
// 清除相关缓存
const cacheKey = `${this.CACHE_PREFIX}${userId}`;
await cacheService.delete(cacheKey);
// 清除租户用户列表缓存
const user = await db('cf_users').where({ id: userId }).first();
if (user) {
await this.clearUserCache(user.tenant_id);
}
return { success: true };
}
static async listUsers(tenantId: string): Promise<User[]> {
return await db('cf_users').where({ tenant_id: tenantId });
static async listUsers(tenantId: string, params: UserListParams = {}): Promise<{ users: User[]; total: number }> {
const { page = 1, pageSize = 10, role, status, search } = params;
const cacheKey = `${this.CACHE_PREFIX}list:${tenantId}:${page}:${pageSize}:${role || ''}:${status || ''}:${search || ''}`;
// 尝试从缓存获取
const cachedResult = await cacheService.get<{ users: User[]; total: number }>(cacheKey);
if (cachedResult) {
return cachedResult;
}
// 构建查询
let query = db('cf_users').where({ tenant_id: tenantId });
if (role) {
query = query.where({ role });
}
if (status) {
// 假设 status 字段存在,实际根据数据库结构调整
query = query.where({ status });
}
if (search) {
query = query.where((builder) => {
builder.where('username', 'like', `%${search}%`)
.orWhere('email', 'like', `%${search}%`);
});
}
// 获取总数
const total = await query.clone().count('id as count').first();
// 获取分页数据
const users = await query.offset((page - 1) * pageSize).limit(pageSize).orderBy('created_at', 'desc');
const result = {
users,
total: Number(total?.count || 0)
};
// 存入缓存
await cacheService.set(cacheKey, result, { ttl: this.CACHE_TTL });
return result;
}
static async deleteUser(userId: string): Promise<void> {
// 获取用户信息以清除相关缓存
const user = await db('cf_users').where({ id: userId }).first();
await db('cf_users').where({ id: userId }).del();
// 清除相关缓存
const cacheKey = `${this.CACHE_PREFIX}${userId}`;
await cacheService.delete(cacheKey);
if (user) {
await this.clearUserCache(user.tenant_id);
}
}
static async batchCreateUsers(usersData: Array<Partial<User> & { password: string }>): Promise<{ success: boolean; createdIds: string[]; error?: string }> {
try {
const createdIds: string[] = [];
const tenantId = usersData[0]?.tenantId;
// 批量插入
for (const userData of usersData) {
const userId = uuidv4();
await db('cf_users').insert({
id: userId,
username: userData.username,
email: userData.email,
password_hash: userData.password,
role: userData.role || 'OPERATOR',
tenant_id: userData.tenantId,
created_at: new Date(),
updated_at: new Date()
});
createdIds.push(userId);
}
// 清除相关缓存
if (tenantId) {
await this.clearUserCache(tenantId);
}
return { success: true, createdIds };
} catch (error: any) {
return { success: false, createdIds: [], error: error.message };
}
}
static async batchUpdateUsers(usersData: Array<{ id: string; updates: Partial<User> }>): Promise<{ success: boolean; updatedCount: number; error?: string }> {
try {
let updatedCount = 0;
const tenantIds = new Set<string>();
// 批量更新
for (const { id, updates } of usersData) {
const result = await db('cf_users').where({ id }).update({
...updates,
updated_at: new Date()
});
if (result > 0) {
updatedCount++;
// 清除用户缓存
const cacheKey = `${this.CACHE_PREFIX}${id}`;
await cacheService.delete(cacheKey);
// 获取租户ID以清除列表缓存
const user = await db('cf_users').where({ id }).first();
if (user) {
tenantIds.add(user.tenant_id);
}
}
}
// 清除租户用户列表缓存
for (const tenantId of tenantIds) {
await this.clearUserCache(tenantId);
}
return { success: true, updatedCount };
} catch (error: any) {
return { success: false, updatedCount: 0, error: error.message };
}
}
static async importUsers(usersData: Array<Partial<User> & { password: string }>): Promise<{ jobId: string }> {
// 创建异步任务
const jobId = await BullMQService.addJob('user:import', {
usersData,
timestamp: new Date().toISOString()
});
return { jobId };
}
private static async clearUserCache(tenantId: string): Promise<void> {
// 清除租户相关的所有用户缓存
await cacheService.deleteByTag(`user:tenant:${tenantId}`);
}
}

View File

@@ -26,6 +26,7 @@ export interface HierarchyNode {
export interface DataIsolationContext {
tenantId: string;
departmentId?: string;
departmentIds?: string[];
shopId?: string;
userId: string;
role: string;
@@ -134,9 +135,32 @@ export class DataIsolationService {
if (context.shopId && rule.shopColumn) {
// 店铺级别用户只能看到自己店铺的数据
query = query.where(rule.shopColumn, context.shopId);
} else if (context.departmentId && rule.departmentColumn) {
// 部门级别用户只能看到自己部门的数据
query = query.where(rule.departmentColumn, context.departmentId);
} else if (context.departmentIds && context.departmentIds.length > 0) {
// 部门管理员可以看到多个部门及其下属店铺的数据
if (rule.departmentColumn) {
query = query.whereIn(rule.departmentColumn, context.departmentIds);
} else if (rule.shopColumn) {
// 获取所有授权部门的店铺
const shopIds = db('cf_shop')
.whereIn('department_id', context.departmentIds)
.where('tenant_id', context.tenantId)
.whereNull('deleted_at')
.pluck('id');
query = query.whereIn(rule.shopColumn, shopIds);
}
} else if (context.departmentId) {
if (rule.departmentColumn) {
// 部门级别用户可以看到自己部门的数据
query = query.where(rule.departmentColumn, context.departmentId);
} else if (rule.shopColumn) {
// 部门级别用户可以看到下属店铺的数据
const shopIds = db('cf_shop')
.where('department_id', context.departmentId)
.where('tenant_id', context.tenantId)
.whereNull('deleted_at')
.pluck('id');
query = query.whereIn(rule.shopColumn, shopIds);
}
}
}
@@ -192,15 +216,39 @@ export class DataIsolationService {
else if (context.role === 'ADMIN') {
hasAccess = true;
}
// 检查店铺隔离
// 检查店铺隔离 - 店铺用户只能访问自己店铺的数据
else if (context.shopId && rule.shopColumn) {
hasAccess = record[rule.shopColumn] === context.shopId;
}
// 检查部门隔离
else if (context.departmentId && rule.departmentColumn) {
hasAccess = record[rule.departmentColumn] === context.departmentId;
// 检查部门隔离 - 多部门管理员可以访问多个部门及其下属店铺的数据
else if (context.departmentIds && context.departmentIds.length > 0) {
if (rule.departmentColumn) {
// 检查记录是否属于任何一个授权部门
hasAccess = context.departmentIds.includes(record[rule.departmentColumn]);
} else if (rule.shopColumn) {
// 检查记录所属店铺是否属于任何一个授权部门
const shop = await db('cf_shop')
.where('id', record[rule.shopColumn])
.whereIn('department_id', context.departmentIds)
.first();
hasAccess = !!shop;
}
}
// 仅有租户隔离
// 检查部门隔离 - 部门用户可以访问部门及其下属店铺的数据
else if (context.departmentId) {
if (rule.departmentColumn) {
// 直接部门匹配
hasAccess = record[rule.departmentColumn] === context.departmentId;
} else if (rule.shopColumn) {
// 部门用户可以访问下属店铺的数据
const shop = await db('cf_shop')
.where('id', record[rule.shopColumn])
.where('department_id', context.departmentId)
.first();
hasAccess = !!shop;
}
}
// 仅有租户隔离 - 租户级用户可以访问所有数据
else {
hasAccess = true;
}
@@ -275,6 +323,41 @@ export class DataIsolationService {
depth: shop.depth,
});
}
} else if (context.departmentIds && context.departmentIds.length > 0) {
// 多部门管理员可以看到多个部门及其下属店铺
if (!nodeType || nodeType === 'DEPARTMENT') {
const departments = await db('cf_department')
.whereIn('id', context.departmentIds)
.where('tenant_id', context.tenantId)
.whereNull('deleted_at')
.select('id', 'name', 'parent_id', 'path', 'depth');
nodes.push(...departments.map(d => ({
id: d.id,
type: 'DEPARTMENT' as HierarchyLevel,
parentId: d.parent_id,
name: d.name,
path: d.path,
depth: d.depth,
})));
}
if (!nodeType || nodeType === 'SHOP') {
const shops = await db('cf_shop')
.whereIn('department_id', context.departmentIds)
.where('tenant_id', context.tenantId)
.whereNull('deleted_at')
.select('id', 'name', 'department_id as parentId', 'path', 'depth');
nodes.push(...shops.map(s => ({
id: s.id,
type: 'SHOP' as HierarchyLevel,
parentId: s.parentId,
name: s.name,
path: s.path,
depth: s.depth,
})));
}
} else if (context.departmentId) {
// 部门用户可以看到部门及其下属店铺
if (!nodeType || nodeType === 'DEPARTMENT') {
@@ -428,6 +511,13 @@ export class DataIsolationService {
hasAccess = true;
} else if (context.shopId && rule.shopColumn) {
hasAccess = record[rule.shopColumn] === context.shopId;
} else if (context.departmentIds && context.departmentIds.length > 0) {
if (rule.departmentColumn) {
hasAccess = context.departmentIds.includes(record[rule.departmentColumn]);
} else if (rule.shopColumn) {
// 这里简化处理,实际应该通过子查询获取所有授权部门的店铺
hasAccess = true;
}
} else if (context.departmentId && rule.departmentColumn) {
hasAccess = record[rule.departmentColumn] === context.departmentId;
} else {

View File

@@ -0,0 +1,265 @@
/**
* [BE-SM001] 店铺成员管理服务
* 负责店铺成员的添加、权限分配和权限继承
* AI注意: 店铺授权后需要自动给部门主管分配权限
*/
import db from '../../config/database';
import { logger } from '../../utils/logger';
import { DataIsolationService } from '../integration/DataIsolationService';
export interface ShopMember {
id: string;
shopId: string;
userId: string;
role: 'owner' | 'admin' | 'operator' | 'viewer';
permissions: string[];
assignedBy: string;
assignedAt: string;
}
export interface ShopMemberCreate {
shopId: string;
userId: string;
role: 'owner' | 'admin' | 'operator' | 'viewer';
permissions: string[];
assignedBy: string;
}
export class ShopMemberService {
private static readonly TABLE = 'cf_shop_member';
private static readonly CACHE_PREFIX = 'shop_member:';
private static readonly CACHE_TTL = 1800;
static async initTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.TABLE);
if (!hasTable) {
await db.schema.createTable(this.TABLE, (table) => {
table.string('id', 64).primary();
table.string('shop_id', 64).notNullable();
table.string('user_id', 64).notNullable();
table.enum('role', ['owner', 'admin', 'operator', 'viewer']).notNullable();
table.json('permissions').notNullable();
table.string('assigned_by', 64).notNullable();
table.timestamp('assigned_at').notNullable();
table.timestamps(true, true);
table.index(['shop_id']);
table.index(['user_id']);
table.unique(['shop_id', 'user_id']);
});
logger.info('✅ Table cf_shop_member created');
}
}
static async create(member: ShopMemberCreate): Promise<ShopMember> {
const id = this.generateId();
const now = new Date().toISOString();
const [newMember] = await db(this.TABLE)
.insert({
id,
shop_id: member.shopId,
user_id: member.userId,
role: member.role,
permissions: JSON.stringify(member.permissions),
assigned_by: member.assignedBy,
assigned_at: now,
created_at: now,
updated_at: now,
})
.returning('*');
logger.info('[ShopMemberService] Shop member created', {
shopId: member.shopId,
userId: member.userId,
role: member.role,
});
return this.mapToShopMember(newMember);
}
static async getShopMembers(shopId: string): Promise<ShopMember[]> {
const members = await db(this.TABLE)
.where('shop_id', shopId)
.orderBy('assigned_at', 'desc');
return members.map(m => this.mapToShopMember(m));
}
static async getUserShops(userId: string): Promise<ShopMember[]> {
const members = await db(this.TABLE)
.where('user_id', userId)
.orderBy('assigned_at', 'desc');
return members.map(m => this.mapToShopMember(m));
}
static async getMember(shopId: string, userId: string): Promise<ShopMember | null> {
const member = await db(this.TABLE)
.where({ shop_id: shopId, user_id: userId })
.first();
return member ? this.mapToShopMember(member) : null;
}
static async updateMember(shopId: string, userId: string, updates: {
role?: 'owner' | 'admin' | 'operator' | 'viewer';
permissions?: string[];
}): Promise<ShopMember> {
const updateData: Record<string, any> = { updated_at: new Date().toISOString() };
if (updates.role) updateData.role = updates.role;
if (updates.permissions) updateData.permissions = JSON.stringify(updates.permissions);
const [member] = await db(this.TABLE)
.where({ shop_id: shopId, user_id: userId })
.update(updateData)
.returning('*');
if (!member) {
throw new Error('Shop member not found');
}
logger.info('[ShopMemberService] Shop member updated', {
shopId,
userId,
updates,
});
return this.mapToShopMember(member);
}
static async removeMember(shopId: string, userId: string): Promise<void> {
await db(this.TABLE)
.where({ shop_id: shopId, user_id: userId })
.delete();
logger.info('[ShopMemberService] Shop member removed', {
shopId,
userId,
});
}
static async assignShopToDepartment(shopId: string, departmentId: string, tenantId: string): Promise<void> {
const shop = await db('cf_shop').where('id', shopId).first();
if (!shop) {
throw new Error('Shop not found');
}
await db('cf_shop')
.where('id', shopId)
.update({
department_id: departmentId,
updated_at: new Date().toISOString(),
});
logger.info('[ShopMemberService] Shop assigned to department', {
shopId,
departmentId,
tenantId,
});
}
static async autoAssignPermissionsToDepartment(shopId: string, departmentId: string, tenantId: string): Promise<void> {
const shop = await db('cf_shop').where('id', shopId).first();
if (!shop) {
throw new Error('Shop not found');
}
const department = await db('cf_department').where('id', departmentId).first();
if (!department) {
throw new Error('Department not found');
}
const context = {
tenantId,
departmentId,
userId: 'system',
role: 'ADMIN',
permissions: ['*'],
hierarchyPath: '',
};
const nodes = await DataIsolationService.getVisibleNodes(context, 'DEPARTMENT');
const departmentNode = nodes.find(n => n.id === departmentId);
if (departmentNode) {
const departmentMembers = await db('cf_user_department')
.where('department_id', departmentId)
.pluck('user_id');
const departmentUsers = await db('cf_user')
.whereIn('id', departmentMembers)
.where('role', 'MANAGER')
.select('id', 'name', 'email');
for (const user of departmentUsers) {
const existingMember = await this.getMember(shopId, user.id);
if (!existingMember) {
await this.create({
shopId,
userId: user.id,
role: 'admin',
permissions: ['product:read', 'product:write', 'order:read', 'order:write', 'inventory:read', 'inventory:write'],
assignedBy: 'system',
});
logger.info('[ShopMemberService] Auto-assigned shop permissions to department manager', {
shopId,
userId: user.id,
userName: user.name,
departmentId,
});
}
}
}
}
static async getShopPermissions(shopId: string, userId: string): Promise<string[]> {
const member = await this.getMember(shopId, userId);
if (!member) {
return [];
}
return member.permissions;
}
static async hasShopPermission(shopId: string, userId: string, permission: string): Promise<boolean> {
const permissions = await this.getShopPermissions(shopId, userId);
return permissions.includes('*') || permissions.includes(permission);
}
static async getShopStats(shopId: string): Promise<{
totalMembers: number;
owners: number;
admins: number;
operators: number;
viewers: number;
}> {
const members = await this.getShopMembers(shopId);
return {
totalMembers: members.length,
owners: members.filter(m => m.role === 'owner').length,
admins: members.filter(m => m.role === 'admin').length,
operators: members.filter(m => m.role === 'operator').length,
viewers: members.filter(m => m.role === 'viewer').length,
};
}
private static generateId(): string {
return `sm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private static mapToShopMember(data: any): ShopMember {
return {
id: data.id,
shopId: data.shop_id,
userId: data.user_id,
role: data.role,
permissions: typeof data.permissions === 'string' ? JSON.parse(data.permissions) : data.permissions,
assignedBy: data.assigned_by,
assignedAt: data.assigned_at,
};
}
}

View File

@@ -264,6 +264,132 @@ export class HierarchyService {
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] 创建店铺
*/

View File

@@ -1,9 +1,11 @@
import express from 'express';
import cors from 'cors';
const app = express();
const PORT = process.env.PORT || 3001;
// 中间件
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
@@ -12,24 +14,36 @@ app.get('/api/health', (req, res) => {
res.json({ status: 'ok', message: 'Backend service is running' });
});
// 模拟登录接口
app.post('/api/auth/login', (req, res) => {
// 模拟登录接口 - 支持 /api/auth/login 和 /api/v1/auth/login
app.post('/api/auth/login', handleLogin);
app.post('/api/v1/auth/login', handleLogin);
function handleLogin(req: express.Request, res: express.Response) {
const { username, password } = req.body;
res.json({
success: true,
data: {
token: 'mock-token-123',
refreshToken: 'mock-refresh-token-123',
user: {
id: '1',
username: username || 'admin',
role: 'ADMIN',
tenantId: 'default-tenant',
shopId: 'default-shop'
// 验证用户名和密码
if (username === 'admin' && password === 'admin123') {
res.json({
success: true,
data: {
token: 'mock-token-' + Date.now(),
refreshToken: 'mock-refresh-token-' + Date.now(),
user: {
id: 'USER-ADMIN-001',
username: username,
role: 'ADMIN',
tenantId: 'default-tenant',
shopId: 'default-shop'
}
}
}
});
});
});
} else {
res.status(401).json({
success: false,
error: '用户名或密码错误'
});
}
}
// 启动服务器
app.listen(PORT, () => {

1
server/src/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
// 基本类型定义

View File

@@ -86,11 +86,19 @@ export class RedisService {
}
/**
* [CORE_DEV_12] Get 缓存
* 生成租户隔离的缓存
*/
static async get(key: string): Promise<string | null> {
static generateTenantKey(tenantId: string, key: string): string {
return `tenant:${tenantId}:${key}`;
}
/**
* [CORE_DEV_12] Get 缓存(支持租户隔离)
*/
static async get(key: string, tenantId?: string): Promise<string | null> {
try {
return await this.getClient().get(key);
const finalKey = tenantId ? this.generateTenantKey(tenantId, key) : key;
return await this.getClient().get(finalKey);
} catch (err: any) {
logger.error(`[Redis] Get Error: ${err.message}`);
return null;
@@ -98,32 +106,33 @@ export class RedisService {
}
/**
* [CORE_DEV_12] Set 缓存
* [CORE_DEV_12] Set 缓存(支持租户隔离)
*/
static async set(key: string, value: string, ttl?: number, mode?: 'NX' | 'XX'): Promise<string | null> {
static async set(key: string, value: string, ttl?: number, mode?: 'NX' | 'XX', tenantId?: string): Promise<string | null> {
try {
const finalKey = tenantId ? this.generateTenantKey(tenantId, key) : key;
if (mode === 'NX') {
if (ttl) {
const result = await this.getClient().set(key, value, 'PX', ttl * 1000, 'NX');
const result = await this.getClient().set(finalKey, value, 'PX', ttl * 1000, 'NX');
return result;
} else {
const result = await this.getClient().set(key, value, 'NX');
const result = await this.getClient().set(finalKey, value, 'NX');
return result;
}
} else if (mode === 'XX') {
if (ttl) {
const result = await this.getClient().set(key, value, 'PX', ttl * 1000, 'XX');
const result = await this.getClient().set(finalKey, value, 'PX', ttl * 1000, 'XX');
return result;
} else {
const result = await this.getClient().set(key, value, 'XX');
const result = await this.getClient().set(finalKey, value, 'XX');
return result;
}
} else {
if (ttl) {
await this.getClient().setex(key, ttl, value);
await this.getClient().setex(finalKey, ttl, value);
return 'OK';
} else {
await this.getClient().set(key, value);
await this.getClient().set(finalKey, value);
return 'OK';
}
}
@@ -134,11 +143,12 @@ export class RedisService {
}
/**
* 删除键
* 删除键(支持租户隔离)
*/
static async del(key: string): Promise<number> {
static async del(key: string, tenantId?: string): Promise<number> {
try {
return await this.getClient().del(key);
const finalKey = tenantId ? this.generateTenantKey(tenantId, key) : key;
return await this.getClient().del(finalKey);
} catch (err: any) {
logger.error(`[Redis] Del Error: ${err.message}`);
return 0;
@@ -146,11 +156,12 @@ export class RedisService {
}
/**
* 获取键的TTL
* 获取键的TTL(支持租户隔离)
*/
static async ttl(key: string): Promise<number> {
static async ttl(key: string, tenantId?: string): Promise<number> {
try {
return await this.getClient().ttl(key);
const finalKey = tenantId ? this.generateTenantKey(tenantId, key) : key;
return await this.getClient().ttl(finalKey);
} catch (err: any) {
logger.error(`[Redis] TTL Error: ${err.message}`);
return -1;
@@ -158,11 +169,12 @@ export class RedisService {
}
/**
* 设置键的过期时间
* 设置键的过期时间(支持租户隔离)
*/
static async expire(key: string, seconds: number): Promise<boolean> {
static async expire(key: string, seconds: number, tenantId?: string): Promise<boolean> {
try {
const result = await this.getClient().expire(key, seconds);
const finalKey = tenantId ? this.generateTenantKey(tenantId, key) : key;
const result = await this.getClient().expire(finalKey, seconds);
return result === 1;
} catch (err: any) {
logger.error(`[Redis] Expire Error: ${err.message}`);