feat: 添加操作日志、危险操作确认和业务关联功能
1. 新增操作日志记录功能,记录关键操作 2. 实现危险操作确认对话框,防止误删 3. 添加业务和库房管理模块 4. 支持设备标记为空闲状态 5. 完善API文档和健康检查 6. 优化前端删除操作的确认流程 7. 添加Swagger API文档支持 8. 实现设备与业务的关联功能 9. 改进设备模型,添加空闲相关字段 10. 优化用户、角色管理操作日志
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
const OperationLog = require('../models/OperationLog');
|
||||
|
||||
describe('OperationLog 模型测试', () => {
|
||||
beforeAll(async () => {
|
||||
await sequelize.sync({ force: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await OperationLog.destroy({ where: {} });
|
||||
});
|
||||
|
||||
describe('模型定义', () => {
|
||||
test('OperationLog 模型应该正确定义', () => {
|
||||
expect(OperationLog).toBeDefined();
|
||||
expect(typeof OperationLog.create).toBe('function');
|
||||
expect(typeof OperationLog.findAll).toBe('function');
|
||||
expect(typeof OperationLog.findOne).toBe('function');
|
||||
});
|
||||
|
||||
test('模型应该有正确的表名', () => {
|
||||
expect(OperationLog.options.tableName).toBe('operation_logs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('创建日志记录', () => {
|
||||
test('应该能够创建基本的操作日志', async () => {
|
||||
const logData = {
|
||||
recordId: 'OPLOG_TEST_001',
|
||||
module: 'device',
|
||||
operationType: 'create',
|
||||
operationDescription: '创建设备 TEST_SERVER',
|
||||
targetId: 'DEV001',
|
||||
targetName: 'TEST_SERVER',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '测试用户',
|
||||
operatorRole: '管理员',
|
||||
result: 'success'
|
||||
};
|
||||
|
||||
const log = await OperationLog.create(logData);
|
||||
|
||||
expect(log.recordId).toBe(logData.recordId);
|
||||
expect(log.module).toBe(logData.module);
|
||||
expect(log.operationType).toBe(logData.operationType);
|
||||
expect(log.operationDescription).toBe(logData.operationDescription);
|
||||
expect(log.targetId).toBe(logData.targetId);
|
||||
expect(log.targetName).toBe(logData.targetName);
|
||||
expect(log.operatorId).toBe(logData.operatorId);
|
||||
expect(log.operatorName).toBe(logData.operatorName);
|
||||
expect(log.result).toBe(logData.result);
|
||||
expect(log.createdAt).toBeDefined();
|
||||
expect(log.updatedAt).toBeDefined();
|
||||
});
|
||||
|
||||
test('应该能够创建带有 beforeState 和 afterState 的日志', async () => {
|
||||
const beforeState = { name: '旧名称', status: 'offline' };
|
||||
const afterState = { name: '新名称', status: 'running' };
|
||||
|
||||
const log = await OperationLog.create({
|
||||
recordId: 'OPLOG_TEST_002',
|
||||
module: 'device',
|
||||
operationType: 'update',
|
||||
operationDescription: '更新设备 TEST_DEVICE',
|
||||
targetId: 'DEV002',
|
||||
targetName: 'TEST_DEVICE',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '测试用户',
|
||||
beforeState,
|
||||
afterState,
|
||||
result: 'success'
|
||||
});
|
||||
|
||||
expect(log.beforeState).toEqual(beforeState);
|
||||
expect(log.afterState).toEqual(afterState);
|
||||
});
|
||||
|
||||
test('应该能够创建带有 metadata 的日志', async () => {
|
||||
const metadata = {
|
||||
count: 5,
|
||||
source: 'batch_operation',
|
||||
extraInfo: '额外信息'
|
||||
};
|
||||
|
||||
const log = await OperationLog.create({
|
||||
recordId: 'OPLOG_TEST_003',
|
||||
module: 'device',
|
||||
operationType: 'batch_delete',
|
||||
operationDescription: '批量删除设备',
|
||||
targetId: 'DEV001,DEV002,DEV003',
|
||||
targetName: '3台设备',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '测试用户',
|
||||
metadata,
|
||||
result: 'success'
|
||||
});
|
||||
|
||||
expect(log.metadata).toEqual(metadata);
|
||||
});
|
||||
|
||||
test('应该能够创建带有 IP 地址和 UserAgent 的日志', async () => {
|
||||
const log = await OperationLog.create({
|
||||
recordId: 'OPLOG_TEST_004',
|
||||
module: 'user',
|
||||
operationType: 'create',
|
||||
operationDescription: '创建用户 test_user',
|
||||
targetId: 'user_test',
|
||||
targetName: 'test_user',
|
||||
operatorId: 'admin_user',
|
||||
operatorName: '管理员',
|
||||
ipAddress: '192.168.1.100',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
||||
result: 'success'
|
||||
});
|
||||
|
||||
expect(log.ipAddress).toBe('192.168.1.100');
|
||||
expect(log.userAgent).toBe('Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
|
||||
});
|
||||
|
||||
test('result 字段默认为 success', async () => {
|
||||
const log = await OperationLog.create({
|
||||
recordId: 'OPLOG_TEST_005',
|
||||
module: 'device',
|
||||
operationType: 'delete',
|
||||
operationDescription: '删除设备',
|
||||
targetId: 'DEV005',
|
||||
targetName: 'TEST_DEVICE',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '测试用户'
|
||||
});
|
||||
|
||||
expect(log.result).toBe('success');
|
||||
});
|
||||
|
||||
test('metadata 字段默认为空对象', async () => {
|
||||
const log = await OperationLog.create({
|
||||
recordId: 'OPLOG_TEST_006',
|
||||
module: 'role',
|
||||
operationType: 'create',
|
||||
operationDescription: '创建角色',
|
||||
targetId: 'role_test',
|
||||
targetName: '测试角色',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '测试用户'
|
||||
});
|
||||
|
||||
expect(log.metadata).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('查询日志记录', () => {
|
||||
beforeEach(async () => {
|
||||
await OperationLog.bulkCreate([
|
||||
{
|
||||
recordId: 'OPLOG_QUERY_001',
|
||||
module: 'device',
|
||||
operationType: 'create',
|
||||
operationDescription: '创建设备 设备A',
|
||||
targetId: 'DEV_A',
|
||||
targetName: '设备A',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '用户A',
|
||||
result: 'success'
|
||||
},
|
||||
{
|
||||
recordId: 'OPLOG_QUERY_002',
|
||||
module: 'device',
|
||||
operationType: 'update',
|
||||
operationDescription: '更新设备 设备B',
|
||||
targetId: 'DEV_B',
|
||||
targetName: '设备B',
|
||||
operatorId: 'user_002',
|
||||
operatorName: '用户B',
|
||||
result: 'success'
|
||||
},
|
||||
{
|
||||
recordId: 'OPLOG_QUERY_003',
|
||||
module: 'user',
|
||||
operationType: 'create',
|
||||
operationDescription: '创建用户 用户C',
|
||||
targetId: 'user_C',
|
||||
targetName: '用户C',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '用户A',
|
||||
result: 'success'
|
||||
},
|
||||
{
|
||||
recordId: 'OPLOG_QUERY_004',
|
||||
module: 'device',
|
||||
operationType: 'delete',
|
||||
operationDescription: '删除设备 设备D',
|
||||
targetId: 'DEV_D',
|
||||
targetName: '设备D',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '用户A',
|
||||
result: 'failed'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('应该能够按 module 查询', async () => {
|
||||
const deviceLogs = await OperationLog.findAll({
|
||||
where: { module: 'device' }
|
||||
});
|
||||
expect(deviceLogs.length).toBe(3);
|
||||
});
|
||||
|
||||
test('应该能够按 operationType 查询', async () => {
|
||||
const createLogs = await OperationLog.findAll({
|
||||
where: { operationType: 'create' }
|
||||
});
|
||||
expect(createLogs.length).toBe(2);
|
||||
});
|
||||
|
||||
test('应该能够按 operatorId 查询', async () => {
|
||||
const user001Logs = await OperationLog.findAll({
|
||||
where: { operatorId: 'user_001' }
|
||||
});
|
||||
expect(user001Logs.length).toBe(3);
|
||||
});
|
||||
|
||||
test('应该能够按 result 查询', async () => {
|
||||
const failedLogs = await OperationLog.findAll({
|
||||
where: { result: 'failed' }
|
||||
});
|
||||
expect(failedLogs.length).toBe(1);
|
||||
});
|
||||
|
||||
test('应该能够按 targetId 模糊查询', async () => {
|
||||
const { Op } = require('sequelize');
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
targetId: { [Op.like]: '%DEV%' }
|
||||
}
|
||||
});
|
||||
expect(logs.length).toBe(3);
|
||||
});
|
||||
|
||||
test('应该支持分页查询', async () => {
|
||||
const { count, rows } = await OperationLog.findAndCountAll({
|
||||
limit: 2,
|
||||
offset: 0,
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
expect(count).toBe(4);
|
||||
expect(rows.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('索引测试', () => {
|
||||
test('应该有 module 索引', () => {
|
||||
const indexes = OperationLog.options.indexes;
|
||||
expect(indexes.some(idx => idx.fields.includes('module'))).toBe(true);
|
||||
});
|
||||
|
||||
test('应该有 operationType 索引', () => {
|
||||
const indexes = OperationLog.options.indexes;
|
||||
expect(indexes.some(idx => idx.fields.includes('operationType'))).toBe(true);
|
||||
});
|
||||
|
||||
test('应该有 targetId 索引', () => {
|
||||
const indexes = OperationLog.options.indexes;
|
||||
expect(indexes.some(idx => idx.fields.includes('targetId'))).toBe(true);
|
||||
});
|
||||
|
||||
test('应该有 operatorId 索引', () => {
|
||||
const indexes = OperationLog.options.indexes;
|
||||
expect(indexes.some(idx => idx.fields.includes('operatorId'))).toBe(true);
|
||||
});
|
||||
|
||||
test('应该有 createdAt 索引', () => {
|
||||
const indexes = OperationLog.options.indexes;
|
||||
expect(indexes.some(idx => idx.fields.includes('createdAt'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user