refactor: 系统权限模块重构,迁移至PostgreSQL并对齐新设计

- 切换数据库为PostgreSQL,更新依赖与配置
- 重构权限体系,使用sys_user/sys_role/sys_menu等新实体替代旧模型
- 移除旧的用户角色/权限/系统设置相关代码与实体
- 新增字典、菜单管理模块与基础CRUD接口
- 优化鉴权逻辑,基于菜单权限标识进行接口校验
- 前端同步更新API、路由与权限控制逻辑
- 删除冗余的v-permission指令与相关代码
This commit is contained in:
andyesfly committed 2026-08-19 14:11:34 +08:00
1 parent 98c22f6afb
commit 0a8103f33d
67 files changed
+3263 -1924

No files matched your search

+3
View File
@@ -5,6 +5,9 @@ import { TokenUtil } from '../util/token';
@Provide()
export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
// 中间件匹配范围:全局生效,但跳过公开接口
ignore = ['/api/auth/*', '/api/health'];
@Inject()
tokenUtil!: TokenUtil;
+42 -50
View File
@@ -2,56 +2,41 @@ import { Middleware, IMiddleware } from '@midwayjs/core';
import { Context, NextFunction } from '@midwayjs/koa';
import { Provide, Inject } from '@midwayjs/core';
import { InjectRepository } from '@midwayjs/sequelize';
import OperationLog from '../entity/operationLog.entity';
import SysOperationLog from '../entity/sysOperationLog.entity';
@Provide()
export class LogMiddleware implements IMiddleware<Context, NextFunction> {
// 中间件匹配范围:全局生效,跳过日志查询、健康检查与登录(登录由内部记录)
ignore = ['/api/logs/*', '/api/health', '/api/auth/login'];
@InjectRepository(OperationLog)
logModel!: typeof OperationLog;
@InjectRepository(SysOperationLog)
logModel!: typeof SysOperationLog;
resolve() {
return async (ctx: Context, next: NextFunction) => {
const startTime = Date.now();
let logData: any = {};
await next();
try {
logData = {
method: ctx.method,
url: ctx.url,
ipAddress: ctx.ip,
userAgent: ctx.header['user-agent'],
requestParams: JSON.stringify(ctx.request.body || {}),
};
if (ctx.state.user) {
logData.userId = ctx.state.user.userId;
logData.username = ctx.state.user.username;
}
await next();
logData.statusCode = ctx.status;
logData.duration = Date.now() - startTime;
if (['POST', 'PUT', 'DELETE'].includes(ctx.method)) {
logData.action = this.getActionDescription(ctx);
logData.module = this.getModuleFromUrl(ctx.url);
this.saveLog(logData).catch(err => {
console.error('保存操作日志失败:', err);
});
}
} catch (error: any) {
logData.statusCode = 500;
logData.duration = Date.now() - startTime;
logData.errorMessage = error.message;
this.saveLog(logData).catch(() => {});
throw error;
// 仅记录写操作,且要求登录用户存在
if (!['POST', 'PUT', 'DELETE'].includes(ctx.method)) {
return;
}
if (!ctx.state.user) {
return;
}
const logData = {
userId: ctx.state.user.userId,
module: this.getModuleFromUrl(ctx.url),
action: this.getActionDescription(ctx),
method: ctx.method,
params: JSON.stringify(ctx.request.body || {}),
ip: ctx.ip,
};
this.saveLog(logData).catch(err => {
console.error('保存操作日志失败:', err);
});
};
}
@@ -59,23 +44,30 @@ export class LogMiddleware implements IMiddleware<Context, NextFunction> {
const actionMap: Record<string, Record<string, string>> = {
'/api/auth/login': { POST: '用户登录' },
'/api/auth/logout': { POST: '用户登出' },
'/api/user': {
'/api/users': {
POST: '创建用户',
PUT: '更新用户',
DELETE: '删除用户',
},
'/api/role': {
'/api/roles': {
POST: '创建角色',
PUT: '更新角色',
DELETE: '删除角色',
},
'/api/settings': {
PUT: '修改系统设置',
'/api/menus': {
POST: '创建菜单',
PUT: '更新菜单',
DELETE: '删除菜单',
},
'/api/dicts': {
POST: '创建字典',
PUT: '更新字典',
DELETE: '删除字典',
},
};
const basePath = ctx.url.split('?')[0];
if (actionMap[basePath]?.[ctx.method]) {
return actionMap[basePath][ctx.method];
}
@@ -92,10 +84,10 @@ export class LogMiddleware implements IMiddleware<Context, NextFunction> {
private getModuleFromUrl(url: string): string {
const moduleMap: Record<string, string> = {
'/api/auth': '认证',
'/api/user': '用户管理',
'/api/role': '角色管理',
'/api/settings': '系统设置',
'/api/log': '日志管理',
'/api/users': '用户管理',
'/api/roles': '角色管理',
'/api/menus': '菜单管理',
'/api/dicts': '数据字典',
'/api/logs': '日志管理',
'/api/dashboard': '仪表盘',
};
+48 -24
View File
@@ -1,14 +1,37 @@
import { Middleware, IMiddleware } from '@midwayjs/core';
import { Context, NextFunction } from '@midwayjs/koa';
import { Provide, Inject } from '@midwayjs/core';
import { InjectRepository } from '@midwayjs/sequelize';
import { PermissionService } from '../service/permission.service';
import SysUserRole from '../entity/sysUserRole.entity';
import SysRole from '../entity/sysRole.entity';
// 请求路径前缀 → 菜单权限标识映射
const PERMISSION_MAP: Record<string, string> = {
'/api/users': 'system:user',
'/api/roles': 'system:role',
'/api/menus': 'system:menu',
'/api/dicts': 'system:dict',
'/api/logs': 'system:log',
};
// 超级管理员角色编码,拥有全部权限
const ADMIN_ROLE_CODE = 'ADMIN';
@Provide()
export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
// 中间件匹配范围:全局生效,跳过公开接口与仪表盘
ignore = ['/api/auth/*', '/api/health', '/api/dashboard/*'];
@Inject()
permissionService!: PermissionService;
@InjectRepository(SysUserRole)
userRoleModel!: typeof SysUserRole;
@InjectRepository(SysRole)
roleModel!: typeof SysRole;
resolve() {
return async (ctx: Context, next: NextFunction) => {
const user = ctx.state.user;
@@ -22,17 +45,21 @@ export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
return;
}
// 获取当前请求路径和方法,转换为权限码
// 例如:GET /api/user/list -> user:list
const permissionCode = this.extractPermissionCode(ctx);
// 超级管理员拥有所有权限
if (user.roles && user.roles.includes('super_admin')) {
// 路径前缀匹配,未匹配到任何权限映射的请求直接放行
const permissionCode = this.matchPermissionCode(ctx.path);
if (!permissionCode) {
await next();
return;
}
// 检查用户是否拥有该权限
// 查询用户角色编码,超级管理员直接放行
const roleCodes = await this.getUserRoleCodes(user.userId);
if (roleCodes.includes(ADMIN_ROLE_CODE)) {
await next();
return;
}
// 检查用户菜单中是否包含该权限标识
const hasPermission = await this.permissionService.checkUserPermission(
user.userId,
permissionCode
@@ -51,24 +78,21 @@ export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
};
}
private extractPermissionCode(ctx: Context): string {
const url = ctx.path.replace('/api/', '').replace(/^\//, '');
const method = ctx.method.toLowerCase();
const methodMap: Record<string, string> = {
post: 'create',
put: 'update',
delete: 'delete',
get: 'list',
};
const action = methodMap[method] || method;
const parts = url.split('/');
if (parts.length >= 2) {
return `${parts[0]}:${action}`;
private matchPermissionCode(path: string): string {
for (const [prefix, code] of Object.entries(PERMISSION_MAP)) {
if (path === prefix || path.startsWith(prefix + '/')) {
return code;
}
}
return '';
}
return `${url}:${action}`;
private async getUserRoleCodes(userId: number): Promise<string[]> {
const userRoles = await this.userRoleModel.findAll({
where: { userId },
include: [this.roleModel],
});
return userRoles.map(ur => ur.role?.roleCode).filter(Boolean) as string[];
}
}