feat: 添加操作日志、危险操作确认和业务关联功能
1. 新增操作日志记录功能,记录关键操作 2. 实现危险操作确认对话框,防止误删 3. 添加业务和库房管理模块 4. 支持设备标记为空闲状态 5. 完善API文档和健康检查 6. 优化前端删除操作的确认流程 7. 添加Swagger API文档支持 8. 实现设备与业务的关联功能 9. 改进设备模型,添加空闲相关字段 10. 优化用户、角色管理操作日志
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
process.env.JWT_SECRET = 'test-secret-key-for-jest-testing-minimum-32-chars-long';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.DB_DIALECT = 'sqlite';
|
||||
process.env.DB_STORAGE = ':memory:';
|
||||
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { Op } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
const OperationLog = require('../models/OperationLog');
|
||||
|
||||
const JWT_SECRET = 'test-secret-key-for-jest-testing-minimum-32-chars-long';
|
||||
|
||||
const createTestApp = () => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const authMiddleware = (req, res, next) => {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, message: '未授权' });
|
||||
}
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ success: false, message: '无效的令牌' });
|
||||
}
|
||||
};
|
||||
|
||||
const operationLogsRouter = express.Router();
|
||||
|
||||
operationLogsRouter.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
module,
|
||||
operationType,
|
||||
targetId,
|
||||
operatorId,
|
||||
keyword,
|
||||
startDate,
|
||||
endDate,
|
||||
result
|
||||
} = req.query;
|
||||
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = Math.min(parseInt(pageSize), 100);
|
||||
|
||||
const where = {};
|
||||
|
||||
if (module && module !== 'all') {
|
||||
where.module = module;
|
||||
}
|
||||
|
||||
if (operationType && operationType !== 'all') {
|
||||
where.operationType = operationType;
|
||||
}
|
||||
|
||||
if (targetId) {
|
||||
where.targetId = { [Op.like]: `%${targetId}%` };
|
||||
}
|
||||
|
||||
if (operatorId) {
|
||||
where.operatorId = operatorId;
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ operationDescription: { [Op.like]: `%${keyword}%` } },
|
||||
{ targetName: { [Op.like]: `%${keyword}%` } },
|
||||
{ operatorName: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
if (result && result !== 'all') {
|
||||
where.result = result;
|
||||
}
|
||||
|
||||
if (startDate || endDate) {
|
||||
where.createdAt = {};
|
||||
if (startDate) {
|
||||
where.createdAt[Op.gte] = new Date(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
const endOfDay = new Date(endDate);
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
where.createdAt[Op.lte] = endOfDay;
|
||||
}
|
||||
}
|
||||
|
||||
const { count, rows: logs } = await OperationLog.findAndCountAll({
|
||||
where,
|
||||
order: [['createdAt', 'DESC']],
|
||||
offset,
|
||||
limit
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
logs
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作日志失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
operationLogsRouter.get('/:recordId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const log = await OperationLog.findByPk(req.params.recordId);
|
||||
|
||||
if (!log) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '日志记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: log
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作日志详情失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/api/operation-logs', operationLogsRouter);
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('OperationLogs API 路由测试', () => {
|
||||
let app;
|
||||
let authToken;
|
||||
const testUser = {
|
||||
userId: 'test_user_001',
|
||||
username: 'testuser',
|
||||
realName: '测试用户',
|
||||
roleName: '管理员'
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await sequelize.sync({ force: true });
|
||||
app = createTestApp();
|
||||
authToken = jwt.sign(testUser, JWT_SECRET, { expiresIn: '24h' });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await OperationLog.destroy({ where: {} });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await sequelize.close();
|
||||
});
|
||||
|
||||
const createTestLog = async (data = {}) => {
|
||||
const defaultData = {
|
||||
recordId: `OPLOG_API_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
module: 'device',
|
||||
operationType: 'create',
|
||||
operationDescription: '测试日志',
|
||||
targetId: 'DEV_TEST',
|
||||
targetName: '测试设备',
|
||||
operatorId: testUser.userId,
|
||||
operatorName: testUser.realName,
|
||||
operatorRole: testUser.roleName,
|
||||
result: 'success'
|
||||
};
|
||||
return await OperationLog.create({ ...defaultData, ...data });
|
||||
};
|
||||
|
||||
describe('GET /api/operation-logs', () => {
|
||||
test('未授权访问应该返回 401', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
|
||||
test('应该能够获取日志列表', async () => {
|
||||
await createTestLog({ recordId: 'OPLOG_LST_001' });
|
||||
await createTestLog({ recordId: 'OPLOG_LST_002' });
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.logs).toHaveLength(2);
|
||||
expect(response.body.data.total).toBe(2);
|
||||
});
|
||||
|
||||
test('应该支持分页参数', async () => {
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await createTestLog({ recordId: `OPLOG_PAGE_${i}` });
|
||||
}
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs')
|
||||
.query({ page: 1, pageSize: 10 })
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.logs).toHaveLength(10);
|
||||
expect(response.body.data.total).toBe(15);
|
||||
expect(response.body.data.page).toBe(1);
|
||||
expect(response.body.data.pageSize).toBe(10);
|
||||
});
|
||||
|
||||
test('应该支持按 module 筛选', async () => {
|
||||
await createTestLog({ module: 'device', recordId: 'OPLOG_MOD_1' });
|
||||
await createTestLog({ module: 'user', recordId: 'OPLOG_MOD_2' });
|
||||
await createTestLog({ module: 'role', recordId: 'OPLOG_MOD_3' });
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs')
|
||||
.query({ module: 'device' })
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.logs).toHaveLength(1);
|
||||
expect(response.body.data.logs[0].module).toBe('device');
|
||||
});
|
||||
|
||||
test('应该支持按 operationType 筛选', async () => {
|
||||
await createTestLog({ operationType: 'create', recordId: 'OPLOG_TYPE_1' });
|
||||
await createTestLog({ operationType: 'update', recordId: 'OPLOG_TYPE_2' });
|
||||
await createTestLog({ operationType: 'delete', recordId: 'OPLOG_TYPE_3' });
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs')
|
||||
.query({ operationType: 'create' })
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.logs).toHaveLength(1);
|
||||
expect(response.body.data.logs[0].operationType).toBe('create');
|
||||
});
|
||||
|
||||
test('应该支持按 keyword 搜索', async () => {
|
||||
await createTestLog({ operationDescription: '创建设备 SERVER_A', recordId: 'OPLOG_KW_1', targetName: '服务器A' });
|
||||
await createTestLog({ operationDescription: '更新设备 SERVER_B', recordId: 'OPLOG_KW_2', targetName: '服务器B' });
|
||||
await createTestLog({ operationDescription: '删除用户 USER_C', recordId: 'OPLOG_KW_3', targetName: '用户C' });
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs')
|
||||
.query({ keyword: 'SERVER' })
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.logs).toHaveLength(2);
|
||||
expect(response.body.data.logs.every(log => log.operationDescription.includes('SERVER'))).toBe(true);
|
||||
});
|
||||
|
||||
test('应该支持按 result 筛选', async () => {
|
||||
await createTestLog({ result: 'success', recordId: 'OPLOG_RES_1' });
|
||||
await createTestLog({ result: 'failed', recordId: 'OPLOG_RES_2' });
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs')
|
||||
.query({ result: 'failed' })
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.logs).toHaveLength(1);
|
||||
expect(response.body.data.logs[0].result).toBe('failed');
|
||||
});
|
||||
|
||||
test('应该支持多条件组合筛选', async () => {
|
||||
await createTestLog({
|
||||
module: 'device',
|
||||
operationType: 'create',
|
||||
result: 'success',
|
||||
recordId: 'OPLOG_COMB_1'
|
||||
});
|
||||
await createTestLog({
|
||||
module: 'device',
|
||||
operationType: 'update',
|
||||
result: 'success',
|
||||
recordId: 'OPLOG_COMB_2'
|
||||
});
|
||||
await createTestLog({
|
||||
module: 'user',
|
||||
operationType: 'create',
|
||||
result: 'success',
|
||||
recordId: 'OPLOG_COMB_3'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs')
|
||||
.query({ module: 'device', operationType: 'create' })
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.logs).toHaveLength(1);
|
||||
expect(response.body.data.logs[0].module).toBe('device');
|
||||
expect(response.body.data.logs[0].operationType).toBe('create');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/operation-logs/:recordId', () => {
|
||||
test('应该能够获取单个日志详情', async () => {
|
||||
const log = await createTestLog({
|
||||
recordId: 'OPLOG_DETAIL_001',
|
||||
beforeState: { name: '旧名称' },
|
||||
afterState: { name: '新名称' },
|
||||
metadata: { customField: '自定义值' }
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs/OPLOG_DETAIL_001')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.recordId).toBe('OPLOG_DETAIL_001');
|
||||
});
|
||||
|
||||
test('不存在的日志应该返回 404', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs/NON_EXISTENT_LOG')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user