feat: 添加汇率服务和缓存服务,优化数据源和日志服务

refactor: 重构数据源工厂和类型定义,提升代码可维护性

fix: 修复类型转换和状态机文档中的错误

docs: 更新服务架构文档,添加新的服务闭环流程

test: 添加汇率服务单元测试

chore: 清理无用代码和注释,优化代码结构
This commit is contained in:
2026-03-19 14:19:01 +08:00
parent 0dac26d781
commit aa2cf560c6
120 changed files with 33383 additions and 4347 deletions

View File

@@ -0,0 +1,359 @@
/**
* [BE-MT002] 层级权限认证中间件
* 负责验证用户层级权限、构建数据隔离上下文
* AI注意: 所有需要数据隔离的路由必须使用此中间件
*/
import { Request, Response, NextFunction } from 'express';
import { logger } from '../utils/logger';
import { DataIsolationService, DataIsolationContext, HierarchyLevel } from './DataIsolationService';
import { RedisService } from './RedisService';
declare global {
namespace Express {
interface Request {
isolationContext?: DataIsolationContext;
}
}
}
// 权限级别
export type PermissionLevel = 'TENANT' | 'DEPARTMENT' | 'SHOP' | 'OWN';
// 中间件配置
interface HierarchyAuthConfig {
requiredLevel?: PermissionLevel;
resourceType?: string;
action?: 'read' | 'write' | 'delete';
allowAdmin?: boolean;
}
export class HierarchyAuthMiddleware {
private static readonly CONTEXT_CACHE_PREFIX = 'hierarchy_context:';
private static readonly CONTEXT_CACHE_TTL = 1800;
/**
* [BE-MT002-01] 创建层级认证中间件
*/
static create(config: HierarchyAuthConfig = {}) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
if (!req.user) {
return res.status(401).json({ error: '未授权访问' });
}
const context = await this.buildContext(req.user.id);
if (!context) {
return res.status(403).json({ error: '无法构建权限上下文' });
}
req.isolationContext = context;
if (config.requiredLevel) {
const hasPermission = await this.checkPermissionLevel(
context,
config.requiredLevel,
config.allowAdmin !== false
);
if (!hasPermission) {
logger.warn(`[HierarchyAuth] Permission denied for user ${req.user.id}`, {
requiredLevel: config.requiredLevel,
userLevel: context.role,
});
return res.status(403).json({ error: '权限不足' });
}
}
if (config.resourceType && config.action) {
const resourceId = req.params.id || req.body.id;
if (resourceId) {
const hasAccess = await DataIsolationService.validateDataAccess(
config.resourceType,
resourceId,
context
);
if (!hasAccess) {
logger.warn(`[HierarchyAuth] Access denied to resource ${resourceId}`, {
userId: req.user.id,
resourceType: config.resourceType,
});
return res.status(403).json({ error: '无权访问此资源' });
}
}
}
next();
} catch (error) {
logger.error('[HierarchyAuth] Middleware error:', error);
return res.status(500).json({ error: '权限验证失败' });
}
};
}
/**
* [BE-MT002-02] 构建数据隔离上下文
*/
private static async buildContext(userId: string): Promise<DataIsolationContext | null> {
const cacheKey = `${this.CONTEXT_CACHE_PREFIX}${userId}`;
const cached = await RedisService.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const user = await this.getUserHierarchy(userId);
if (!user) {
return null;
}
const permissions = await this.getUserPermissions(userId);
const context: DataIsolationContext = {
tenantId: user.tenant_id,
departmentId: user.department_id,
shopId: user.shop_id,
userId: user.id,
role: user.role,
permissions,
hierarchyPath: user.hierarchy_path || '',
};
await RedisService.set(cacheKey, JSON.stringify(context), this.CONTEXT_CACHE_TTL);
return context;
}
/**
* [BE-MT002-03] 获取用户层级信息
*/
private static async getUserHierarchy(userId: string): Promise<any> {
const user = await require('../config/database').default('cf_user')
.where('id', userId)
.whereNull('deleted_at')
.first(
'id',
'tenant_id',
'department_id',
'shop_id',
'role',
'hierarchy_path'
);
return user;
}
/**
* [BE-MT002-04] 获取用户权限列表
*/
private static async getUserPermissions(userId: string): Promise<string[]> {
const permissions = await require('../config/database').default('cf_user_permission')
.where('user_id', userId)
.pluck('permission_code');
return permissions;
}
/**
* [BE-MT002-05] 检查权限级别
*/
private static async checkPermissionLevel(
context: DataIsolationContext,
requiredLevel: PermissionLevel,
allowAdmin: boolean
): Promise<boolean> {
if (allowAdmin && context.role === 'ADMIN') {
return true;
}
switch (requiredLevel) {
case 'TENANT':
return !!context.tenantId;
case 'DEPARTMENT':
return !!context.departmentId || context.role === 'MANAGER';
case 'SHOP':
return !!context.shopId || context.role === 'MANAGER' || context.role === 'OPERATOR';
case 'OWN':
return true;
default:
return false;
}
}
/**
* [BE-MT002-06] 验证层级选择器
*/
static validateHierarchySelector(requiredLevel: PermissionLevel) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
if (!req.isolationContext) {
return res.status(401).json({ error: '未授权访问' });
}
const { tenantId, departmentId, shopId } = req.body;
if (requiredLevel === 'SHOP' && !shopId) {
return res.status(400).json({ error: '必须指定店铺' });
}
if (requiredLevel === 'DEPARTMENT' && !departmentId) {
return res.status(400).json({ error: '必须指定部门' });
}
if (tenantId && tenantId !== req.isolationContext.tenantId) {
return res.status(403).json({ error: '无权访问此租户数据' });
}
if (departmentId && req.isolationContext.role !== 'ADMIN') {
if (req.isolationContext.departmentId !== departmentId) {
return res.status(403).json({ error: '无权访问此部门数据' });
}
}
if (shopId && req.isolationContext.role !== 'ADMIN') {
if (req.isolationContext.shopId && req.isolationContext.shopId !== shopId) {
return res.status(403).json({ error: '无权访问此店铺数据' });
}
}
next();
} catch (error) {
logger.error('[HierarchyAuth] Selector validation error:', error);
return res.status(500).json({ error: '权限验证失败' });
}
};
}
/**
* [BE-MT002-07] 清除用户上下文缓存
*/
static async clearUserContextCache(userId: string): Promise<void> {
await RedisService.delete(`${this.CONTEXT_CACHE_PREFIX}${userId}`);
logger.info(`[HierarchyAuth] Cleared context cache for user ${userId}`);
}
/**
* [BE-MT002-08] 数据隔离查询包装器
*/
static withIsolation(tableName: string) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
if (!req.isolationContext) {
return res.status(401).json({ error: '未授权访问' });
}
req.isolatedQuery = DataIsolationService.buildIsolationQuery(
tableName,
req.isolationContext
);
next();
} catch (error) {
logger.error('[HierarchyAuth] Isolation wrapper error:', error);
return res.status(500).json({ error: '数据隔离失败' });
}
};
}
/**
* [BE-MT002-09] 批量数据访问验证
*/
static batchAccessCheck(tableName: string, idParam: string = 'ids') {
return async (req: Request, res: Response, next: NextFunction) => {
try {
if (!req.isolationContext) {
return res.status(401).json({ error: '未授权访问' });
}
const ids = req.body[idParam] || req.query[idParam];
if (!ids || !Array.isArray(ids)) {
return res.status(400).json({ error: '必须提供资源ID列表' });
}
const accessMap = await DataIsolationService.batchValidateAccess(
tableName,
ids,
req.isolationContext
);
const deniedIds = ids.filter(id => !accessMap[id]);
if (deniedIds.length > 0) {
logger.warn(`[HierarchyAuth] Batch access denied for user ${req.user?.id}`, {
deniedIds,
tableName,
});
return res.status(403).json({
error: '部分资源无权访问',
deniedIds,
});
}
req.accessMap = accessMap;
next();
} catch (error) {
logger.error('[HierarchyAuth] Batch access check error:', error);
return res.status(500).json({ error: '权限验证失败' });
}
};
}
/**
* [BE-MT002-10] 角色检查中间件
*/
static requireRole(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.isolationContext) {
return res.status(401).json({ error: '未授权访问' });
}
if (!roles.includes(req.isolationContext.role)) {
logger.warn(`[HierarchyAuth] Role check failed for user ${req.user?.id}`, {
requiredRoles: roles,
userRole: req.isolationContext.role,
});
return res.status(403).json({ error: '角色权限不足' });
}
next();
};
}
/**
* [BE-MT002-11] 权限检查中间件
*/
static requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.isolationContext) {
return res.status(401).json({ error: '未授权访问' });
}
if (req.isolationContext.role === 'ADMIN') {
return next();
}
if (!req.isolationContext.permissions.includes(permission)) {
logger.warn(`[HierarchyAuth] Permission check failed for user ${req.user?.id}`, {
requiredPermission: permission,
userPermissions: req.isolationContext.permissions,
});
return res.status(403).json({ error: '缺少必要权限' });
}
next();
};
}
}
declare global {
namespace Express {
interface Request {
isolatedQuery?: any;
accessMap?: Record<string, boolean>;
}
}
}