573 lines
15 KiB
TypeScript
573 lines
15 KiB
TypeScript
import { Request, Response } from 'express';
|
||
import type {
|
||
LogItem,
|
||
OperationModule,
|
||
OperationStatus,
|
||
OperationType,
|
||
} from '../src/pages/LogAudit/data.d';
|
||
|
||
// 操作人姓名列表
|
||
const operatorNames = [
|
||
'张伟',
|
||
'李娜',
|
||
'王强',
|
||
'刘洋',
|
||
'陈静',
|
||
'杨帆',
|
||
'赵敏',
|
||
'黄涛',
|
||
'周丽',
|
||
'吴刚',
|
||
'徐静',
|
||
'孙鹏',
|
||
'马丽',
|
||
'朱杰',
|
||
'胡军',
|
||
'林娜',
|
||
'郭明',
|
||
'何伟',
|
||
'高峰',
|
||
'梁雨',
|
||
'system',
|
||
'admin',
|
||
'root',
|
||
];
|
||
|
||
// 角色列表
|
||
const operatorRoles = [
|
||
'超级管理员',
|
||
'系统管理员',
|
||
'运营专员',
|
||
'内容编辑',
|
||
'普通用户',
|
||
'审计员',
|
||
];
|
||
|
||
// 操作类型
|
||
const operationTypes: OperationType[] = [
|
||
'CREATE',
|
||
'UPDATE',
|
||
'DELETE',
|
||
'QUERY',
|
||
'LOGIN',
|
||
'LOGOUT',
|
||
'EXPORT',
|
||
'IMPORT',
|
||
'ENABLE',
|
||
'DISABLE',
|
||
'OTHER',
|
||
];
|
||
|
||
// 操作模块
|
||
const operationModules: OperationModule[] = [
|
||
'USER',
|
||
'PRODUCT',
|
||
'ORDER',
|
||
'ARTICLE',
|
||
'SYSTEM',
|
||
'AUTH',
|
||
'OTHER',
|
||
];
|
||
|
||
// 操作描述模板
|
||
const operationDescMap: Record<
|
||
OperationModule,
|
||
Record<OperationType, string[]>
|
||
> = {
|
||
USER: {
|
||
CREATE: ['创建新用户账号', '批量导入用户数据', '注册新会员'],
|
||
UPDATE: ['修改用户信息', '更新用户头像', '修改用户邮箱'],
|
||
DELETE: ['删除用户账号', '批量删除用户', '注销用户'],
|
||
QUERY: ['查询用户列表', '查看用户详情', '搜索用户'],
|
||
LOGIN: ['用户登录系统', '扫码登录', '短信验证码登录'],
|
||
LOGOUT: ['用户退出登录', '会话超时退出', '强制退出'],
|
||
EXPORT: ['导出用户列表', '导出用户报表'],
|
||
IMPORT: ['导入用户数据', '批量导入用户'],
|
||
ENABLE: ['启用用户账号', '激活用户'],
|
||
DISABLE: ['禁用用户账号', '冻结用户'],
|
||
OTHER: ['重置用户密码', '发送验证邮件'],
|
||
},
|
||
PRODUCT: {
|
||
CREATE: ['创建商品', '新增商品信息', '批量添加商品'],
|
||
UPDATE: ['修改商品信息', '更新商品价格', '修改商品库存'],
|
||
DELETE: ['删除商品', '批量删除商品'],
|
||
QUERY: ['查询商品列表', '查看商品详情', '搜索商品'],
|
||
LOGIN: [],
|
||
LOGOUT: [],
|
||
EXPORT: ['导出商品列表', '导出商品库存报表'],
|
||
IMPORT: ['导入商品数据', '批量导入商品信息'],
|
||
ENABLE: ['上架商品', '启用商品'],
|
||
DISABLE: ['下架商品', '禁用商品'],
|
||
OTHER: ['修改商品图片', '更新商品分类'],
|
||
},
|
||
ORDER: {
|
||
CREATE: ['创建订单', '生成新订单'],
|
||
UPDATE: ['修改订单状态', '更新订单信息', '修改收货地址'],
|
||
DELETE: ['删除订单', '取消订单'],
|
||
QUERY: ['查询订单列表', '查看订单详情', '搜索订单'],
|
||
LOGIN: [],
|
||
LOGOUT: [],
|
||
EXPORT: ['导出订单列表', '导出销售报表'],
|
||
IMPORT: ['导入订单数据'],
|
||
ENABLE: [],
|
||
DISABLE: [],
|
||
OTHER: ['订单退款', '订单发货', '订单备注'],
|
||
},
|
||
ARTICLE: {
|
||
CREATE: ['发布文章', '创建新文章', '保存草稿'],
|
||
UPDATE: ['修改文章内容', '更新文章标题', '编辑文章'],
|
||
DELETE: ['删除文章', '批量删除文章'],
|
||
QUERY: ['查询文章列表', '查看文章详情', '搜索文章'],
|
||
LOGIN: [],
|
||
LOGOUT: [],
|
||
EXPORT: ['导出文章内容'],
|
||
IMPORT: ['导入文章'],
|
||
ENABLE: ['发布文章', '启用文章'],
|
||
DISABLE: ['隐藏文章', '禁用文章'],
|
||
OTHER: ['文章审核', '文章置顶'],
|
||
},
|
||
SYSTEM: {
|
||
CREATE: ['创建系统配置', '添加系统参数'],
|
||
UPDATE: ['修改系统配置', '更新系统参数'],
|
||
DELETE: ['删除系统配置', '清除缓存'],
|
||
QUERY: ['查询系统日志', '查看系统状态'],
|
||
LOGIN: [],
|
||
LOGOUT: [],
|
||
EXPORT: ['导出系统日志', '导出配置备份'],
|
||
IMPORT: ['导入系统配置'],
|
||
ENABLE: ['启用功能模块'],
|
||
DISABLE: ['禁用功能模块'],
|
||
OTHER: ['系统备份', '系统恢复', '清理日志'],
|
||
},
|
||
AUTH: {
|
||
CREATE: ['创建角色', '添加权限', '新建菜单'],
|
||
UPDATE: ['修改角色权限', '更新菜单', '编辑权限'],
|
||
DELETE: ['删除角色', '移除权限', '删除菜单'],
|
||
QUERY: ['查询角色列表', '查看权限树'],
|
||
LOGIN: ['用户认证', '令牌刷新'],
|
||
LOGOUT: ['令牌失效'],
|
||
EXPORT: ['导出权限配置'],
|
||
IMPORT: ['导入权限配置'],
|
||
ENABLE: ['启用角色'],
|
||
DISABLE: ['禁用角色'],
|
||
OTHER: ['密码修改', '权限分配'],
|
||
},
|
||
OTHER: {
|
||
CREATE: ['创建数据'],
|
||
UPDATE: ['更新数据'],
|
||
DELETE: ['删除数据'],
|
||
QUERY: ['查询数据'],
|
||
LOGIN: [],
|
||
LOGOUT: [],
|
||
EXPORT: ['导出数据'],
|
||
IMPORT: ['导入数据'],
|
||
ENABLE: ['启用功能'],
|
||
DISABLE: ['禁用功能'],
|
||
OTHER: ['其他操作'],
|
||
},
|
||
};
|
||
|
||
// 请求方法
|
||
const requestMethods: Array<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'> = [
|
||
'GET',
|
||
'POST',
|
||
'PUT',
|
||
'DELETE',
|
||
'PATCH',
|
||
];
|
||
|
||
// 请求 URL 模板
|
||
const urlTemplates: Record<OperationModule, string[]> = {
|
||
USER: [
|
||
'/api/users',
|
||
'/api/users/list',
|
||
'/api/users/detail',
|
||
'/api/auth/login',
|
||
'/api/auth/logout',
|
||
],
|
||
PRODUCT: [
|
||
'/api/products',
|
||
'/api/products/list',
|
||
'/api/products/detail',
|
||
'/api/products/stock',
|
||
],
|
||
ORDER: [
|
||
'/api/orders',
|
||
'/api/orders/list',
|
||
'/api/orders/detail',
|
||
'/api/orders/status',
|
||
],
|
||
ARTICLE: [
|
||
'/api/articles',
|
||
'/api/articles/list',
|
||
'/api/articles/detail',
|
||
'/api/articles/publish',
|
||
],
|
||
SYSTEM: [
|
||
'/api/system/config',
|
||
'/api/system/logs',
|
||
'/api/system/status',
|
||
'/api/system/backup',
|
||
],
|
||
AUTH: [
|
||
'/api/auth/roles',
|
||
'/api/auth/permissions',
|
||
'/api/auth/menus',
|
||
'/api/auth/login',
|
||
],
|
||
OTHER: ['/api/misc', '/api/utils', '/api/common'],
|
||
};
|
||
|
||
// IP 地址池
|
||
const ipPrefixes = [
|
||
'192.168.1',
|
||
'192.168.0',
|
||
'10.0.0',
|
||
'172.16.0',
|
||
'113.45',
|
||
'36.110',
|
||
'223.5.5',
|
||
];
|
||
|
||
// User Agent 列表
|
||
const userAgents = [
|
||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0 Safari/537.36',
|
||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1',
|
||
'Mozilla/5.0 (Linux; Android 13; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
|
||
];
|
||
|
||
// 错误信息模板
|
||
const errorMessages = [
|
||
'参数校验失败:必填字段为空',
|
||
'权限不足:当前用户无操作权限',
|
||
'数据不存在:记录已被删除或不存在',
|
||
'数据库连接超时',
|
||
'系统繁忙,请稍后再试',
|
||
'请求参数格式错误',
|
||
'接口限流,请求过于频繁',
|
||
'Token 已过期,请重新登录',
|
||
'密码错误,请重新输入',
|
||
'账号已被锁定',
|
||
];
|
||
|
||
// 生成随机整数
|
||
function randomInt(min: number, max: number): number {
|
||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||
}
|
||
|
||
// 生成随机日期(近30天)
|
||
function randomDate(): string {
|
||
const now = new Date();
|
||
const daysAgo = randomInt(0, 30);
|
||
const hoursAgo = randomInt(0, 23);
|
||
const minutesAgo = randomInt(0, 59);
|
||
const secondsAgo = randomInt(0, 59);
|
||
|
||
const date = new Date(
|
||
now.getTime() -
|
||
daysAgo * 24 * 60 * 60 * 1000 -
|
||
hoursAgo * 60 * 60 * 1000 -
|
||
minutesAgo * 60 * 1000 -
|
||
secondsAgo * 1000,
|
||
);
|
||
|
||
return date.toISOString();
|
||
}
|
||
|
||
// 生成随机 IP
|
||
function randomIP(): string {
|
||
const prefix = ipPrefixes[randomInt(0, ipPrefixes.length - 1)];
|
||
return `${prefix}.${randomInt(1, 254)}`;
|
||
}
|
||
|
||
// 生成单条日志数据
|
||
function generateLog(id: number): LogItem {
|
||
const module = operationModules[randomInt(0, operationModules.length - 1)];
|
||
const type = operationTypes[randomInt(0, operationTypes.length - 1)];
|
||
const descList = operationDescMap[module][type];
|
||
const desc =
|
||
descList.length > 0
|
||
? descList[randomInt(0, descList.length - 1)]
|
||
: `${type}操作`;
|
||
|
||
// 90% 成功,10% 失败
|
||
const isSuccess = Math.random() > 0.1;
|
||
const status: OperationStatus = isSuccess ? 'success' : 'failure';
|
||
|
||
const operatorName = operatorNames[randomInt(0, operatorNames.length - 1)];
|
||
const operatorId =
|
||
operatorName === 'system' ? '0' : randomInt(1000, 9999).toString();
|
||
|
||
return {
|
||
id: id.toString(),
|
||
operatorId,
|
||
operatorName,
|
||
operatorRole: operatorRoles[randomInt(0, operatorRoles.length - 1)],
|
||
operationType: type,
|
||
operationModule: module,
|
||
operationDesc: desc,
|
||
requestMethod: requestMethods[randomInt(0, requestMethods.length - 1)],
|
||
requestUrl:
|
||
urlTemplates[module][randomInt(0, urlTemplates[module].length - 1)],
|
||
requestParams:
|
||
Math.random() > 0.5
|
||
? JSON.stringify({ id: randomInt(1, 1000), page: 1, size: 20 })
|
||
: undefined,
|
||
responseData:
|
||
isSuccess && Math.random() > 0.7
|
||
? JSON.stringify({ code: 200, message: 'success', data: {} })
|
||
: undefined,
|
||
ipAddress: randomIP(),
|
||
userAgent:
|
||
Math.random() > 0.3
|
||
? userAgents[randomInt(0, userAgents.length - 1)]
|
||
: undefined,
|
||
status,
|
||
errorMessage: !isSuccess
|
||
? errorMessages[randomInt(0, errorMessages.length - 1)]
|
||
: undefined,
|
||
executionTime: randomInt(10, 5000),
|
||
createdAt: randomDate(),
|
||
};
|
||
}
|
||
|
||
// 生成 150 条日志数据
|
||
const logs: LogItem[] = Array.from({ length: 150 }, (_, i) =>
|
||
generateLog(i + 1),
|
||
);
|
||
|
||
// 按时间倒序排序
|
||
logs.sort(
|
||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||
);
|
||
|
||
export default {
|
||
// 获取日志列表 GET /api/logs
|
||
'GET /api/logs': (req: Request, res: Response) => {
|
||
const {
|
||
current = 1,
|
||
pageSize = 20,
|
||
operatorName,
|
||
operationType,
|
||
operationModule,
|
||
status,
|
||
startTime,
|
||
endTime,
|
||
ipAddress,
|
||
sortField = 'createdAt',
|
||
sortOrder = 'descend',
|
||
} = req.query;
|
||
|
||
let filteredData = [...logs];
|
||
|
||
// 操作人姓名筛选
|
||
if (operatorName) {
|
||
filteredData = filteredData.filter((item) =>
|
||
item.operatorName.includes(operatorName as string),
|
||
);
|
||
}
|
||
|
||
// 操作类型筛选
|
||
if (operationType) {
|
||
filteredData = filteredData.filter(
|
||
(item) => item.operationType === operationType,
|
||
);
|
||
}
|
||
|
||
// 操作模块筛选
|
||
if (operationModule) {
|
||
filteredData = filteredData.filter(
|
||
(item) => item.operationModule === operationModule,
|
||
);
|
||
}
|
||
|
||
// 状态筛选
|
||
if (status) {
|
||
filteredData = filteredData.filter((item) => item.status === status);
|
||
}
|
||
|
||
// 时间范围筛选
|
||
if (startTime) {
|
||
const start = new Date(startTime as string).getTime();
|
||
filteredData = filteredData.filter(
|
||
(item) => new Date(item.createdAt).getTime() >= start,
|
||
);
|
||
}
|
||
|
||
if (endTime) {
|
||
const end = new Date(endTime as string).getTime();
|
||
filteredData = filteredData.filter(
|
||
(item) => new Date(item.createdAt).getTime() <= end,
|
||
);
|
||
}
|
||
|
||
// IP 地址筛选
|
||
if (ipAddress) {
|
||
filteredData = filteredData.filter((item) =>
|
||
item.ipAddress.includes(ipAddress as string),
|
||
);
|
||
}
|
||
|
||
// 排序
|
||
if (sortField && sortOrder) {
|
||
filteredData.sort((a, b) => {
|
||
const aValue = a[sortField as keyof LogItem];
|
||
const bValue = b[sortField as keyof LogItem];
|
||
const isAscend = sortOrder === 'ascend';
|
||
|
||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||
return isAscend
|
||
? aValue.localeCompare(bValue)
|
||
: bValue.localeCompare(aValue);
|
||
}
|
||
|
||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||
return isAscend ? aValue - bValue : bValue - aValue;
|
||
}
|
||
|
||
return 0;
|
||
});
|
||
}
|
||
|
||
// 分页
|
||
const currentNum = parseInt(current as string, 10);
|
||
const pageSizeNum = parseInt(pageSize as string, 10);
|
||
const startIndex = (currentNum - 1) * pageSizeNum;
|
||
const endIndex = startIndex + pageSizeNum;
|
||
const pagedData = filteredData.slice(startIndex, endIndex);
|
||
|
||
// 模拟网络延迟
|
||
setTimeout(() => {
|
||
res.json({
|
||
data: pagedData,
|
||
total: filteredData.length,
|
||
success: true,
|
||
});
|
||
}, 300);
|
||
},
|
||
|
||
// 获取日志详情 GET /api/logs/:id
|
||
'GET /api/logs/:id': (req: Request, res: Response) => {
|
||
const log = logs.find((item) => item.id === req.params.id);
|
||
|
||
if (!log) {
|
||
res.status(404).json({
|
||
success: false,
|
||
message: '日志记录不存在',
|
||
});
|
||
return;
|
||
}
|
||
|
||
res.json({
|
||
data: log,
|
||
success: true,
|
||
});
|
||
},
|
||
|
||
// 导出日志 POST /api/logs/export
|
||
'POST /api/logs/export': (req: Request, res: Response) => {
|
||
const {
|
||
operatorName,
|
||
operationType,
|
||
operationModule,
|
||
status,
|
||
startTime,
|
||
endTime,
|
||
ipAddress,
|
||
} = req.body;
|
||
|
||
let filteredData = [...logs];
|
||
|
||
// 应用相同的筛选逻辑
|
||
if (operatorName) {
|
||
filteredData = filteredData.filter((item) =>
|
||
item.operatorName.includes(operatorName),
|
||
);
|
||
}
|
||
|
||
if (operationType) {
|
||
filteredData = filteredData.filter(
|
||
(item) => item.operationType === operationType,
|
||
);
|
||
}
|
||
|
||
if (operationModule) {
|
||
filteredData = filteredData.filter(
|
||
(item) => item.operationModule === operationModule,
|
||
);
|
||
}
|
||
|
||
if (status) {
|
||
filteredData = filteredData.filter((item) => item.status === status);
|
||
}
|
||
|
||
if (startTime) {
|
||
const start = new Date(startTime).getTime();
|
||
filteredData = filteredData.filter(
|
||
(item) => new Date(item.createdAt).getTime() >= start,
|
||
);
|
||
}
|
||
|
||
if (endTime) {
|
||
const end = new Date(endTime).getTime();
|
||
filteredData = filteredData.filter(
|
||
(item) => new Date(item.createdAt).getTime() <= end,
|
||
);
|
||
}
|
||
|
||
if (ipAddress) {
|
||
filteredData = filteredData.filter((item) =>
|
||
item.ipAddress.includes(ipAddress),
|
||
);
|
||
}
|
||
|
||
// 生成 CSV 内容
|
||
const headers = [
|
||
'ID',
|
||
'操作人',
|
||
'操作类型',
|
||
'操作模块',
|
||
'操作描述',
|
||
'请求方法',
|
||
'请求URL',
|
||
'IP地址',
|
||
'状态',
|
||
'执行时间(ms)',
|
||
'操作时间',
|
||
];
|
||
const rows = filteredData.map((log) => [
|
||
log.id,
|
||
log.operatorName,
|
||
log.operationType,
|
||
log.operationModule,
|
||
log.operationDesc,
|
||
log.requestMethod,
|
||
log.requestUrl,
|
||
log.ipAddress,
|
||
log.status,
|
||
log.executionTime,
|
||
new Date(log.createdAt).toLocaleString('zh-CN'),
|
||
]);
|
||
|
||
const csvContent = [
|
||
headers.join(','),
|
||
...rows.map((row) => row.map((cell) => `"${cell}"`).join(',')),
|
||
].join('\n');
|
||
|
||
// 设置响应头,触发浏览器下载
|
||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||
res.setHeader(
|
||
'Content-Disposition',
|
||
`attachment; filename=logs_export_${new Date()
|
||
.toISOString()
|
||
.slice(0, 10)}.csv`,
|
||
);
|
||
|
||
// 添加 BOM 以支持中文显示
|
||
const bom = '\uFEFF';
|
||
res.send(bom + csvContent);
|
||
},
|
||
};
|