本次提交实现了完整的客户拜访业务系统: 1. 新增对象存储上传服务,支持S3兼容协议的RustFS存储 2. 完善客户拜访实体与数据库表结构,补充全业务字段 3. 开发拜访台账与记录的前后端接口,包含增删改查、批量创建、状态流转等能力 4. 新增拜访表单页面与路由,支持父级菜单补全面包屑 5. 新增勘察列表页预填客户ID跳转逻辑 6. 补充UI设计文档与页面截图 7. 调整日志中间件跳过上传路由,优化RBAC权限配置
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import { Middleware, IMiddleware } from '@midwayjs/core';
|
|
import { Context, NextFunction } from '@midwayjs/koa';
|
|
import { Provide, Inject } from '@midwayjs/core';
|
|
import { InjectRepository } from '@midwayjs/sequelize';
|
|
import SysOperationLog from '../entity/sysOperationLog.entity';
|
|
|
|
@Provide()
|
|
export class LogMiddleware implements IMiddleware<Context, NextFunction> {
|
|
// 中间件匹配范围:全局生效,跳过日志查询、健康检查、登录(登录由内部记录)与文件上传(multipart 无记录价值)
|
|
ignore = ['/api/logs/*', '/api/health', '/api/auth/login', '/api/upload/*'];
|
|
|
|
@InjectRepository(SysOperationLog)
|
|
logModel!: typeof SysOperationLog;
|
|
|
|
resolve() {
|
|
return async (ctx: Context, next: NextFunction) => {
|
|
await next();
|
|
|
|
// 仅记录写操作,且要求登录用户存在
|
|
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);
|
|
});
|
|
};
|
|
}
|
|
|
|
private getActionDescription(ctx: Context): string {
|
|
const actionMap: Record<string, Record<string, string>> = {
|
|
'/api/auth/login': { POST: '用户登录' },
|
|
'/api/auth/logout': { POST: '用户登出' },
|
|
'/api/users': {
|
|
POST: '创建用户',
|
|
PUT: '更新用户',
|
|
DELETE: '删除用户',
|
|
},
|
|
'/api/roles': {
|
|
POST: '创建角色',
|
|
PUT: '更新角色',
|
|
DELETE: '删除角色',
|
|
},
|
|
'/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];
|
|
}
|
|
|
|
for (const [path, actions] of Object.entries(actionMap)) {
|
|
if (basePath.startsWith(path) && actions[ctx.method]) {
|
|
return actions[ctx.method];
|
|
}
|
|
}
|
|
|
|
return `${ctx.method} ${basePath}`;
|
|
}
|
|
|
|
private getModuleFromUrl(url: string): string {
|
|
const moduleMap: Record<string, string> = {
|
|
'/api/auth': '认证',
|
|
'/api/users': '用户管理',
|
|
'/api/roles': '角色管理',
|
|
'/api/menus': '菜单管理',
|
|
'/api/dicts': '数据字典',
|
|
'/api/logs': '日志管理',
|
|
'/api/dashboard': '仪表盘',
|
|
};
|
|
|
|
for (const [path, module] of Object.entries(moduleMap)) {
|
|
if (url.startsWith(path)) {
|
|
return module;
|
|
}
|
|
}
|
|
|
|
return '其他';
|
|
}
|
|
|
|
private async saveLog(logData: any): Promise<void> {
|
|
try {
|
|
await this.logModel.create(logData);
|
|
} catch (error) {
|
|
console.error('写入操作日志失败:', error);
|
|
}
|
|
}
|
|
}
|