feat: 添加操作日志、危险操作确认和业务关联功能
1. 新增操作日志记录功能,记录关键操作 2. 实现危险操作确认对话框,防止误删 3. 添加业务和库房管理模块 4. 支持设备标记为空闲状态 5. 完善API文档和健康检查 6. 优化前端删除操作的确认流程 7. 添加Swagger API文档支持 8. 实现设备与业务的关联功能 9. 改进设备模型,添加空闲相关字段 10. 优化用户、角色管理操作日志
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { logDangerousOperation, getDangerousOperationsLogs, cleanOldLogs, DANGEROUS_OPERATION_TYPES, RISK_LEVELS, calculateRiskLevel } = require('../utils/dangerousOperationLogger');
|
||||
|
||||
router.post('/log', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
operationType,
|
||||
operationName,
|
||||
targetType,
|
||||
targetId,
|
||||
targetName,
|
||||
beforeState,
|
||||
metadata = {},
|
||||
success = true,
|
||||
errorMessage = null,
|
||||
} = req.body;
|
||||
|
||||
if (!operationType || !operationName) {
|
||||
return res.status(400).json({ error: '缺少必需参数 operationType 或 operationName' });
|
||||
}
|
||||
|
||||
const riskLevel = metadata.riskLevel || calculateRiskLevel(operationType, metadata.itemCount || 1, {
|
||||
hasRelatedData: metadata.relatedDataCount > 0,
|
||||
isSystemLevel: metadata.isSystemLevel,
|
||||
});
|
||||
|
||||
await logDangerousOperation(req, {
|
||||
operationType,
|
||||
operationName,
|
||||
targetType,
|
||||
targetId,
|
||||
targetName,
|
||||
beforeState,
|
||||
metadata: {
|
||||
...metadata,
|
||||
riskLevel,
|
||||
},
|
||||
success,
|
||||
errorMessage,
|
||||
});
|
||||
|
||||
res.json({ success: true, riskLevel });
|
||||
} catch (error) {
|
||||
console.error('Failed to log dangerous operation:', error);
|
||||
res.status(500).json({ error: '日志记录失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/logs', async (req, res) => {
|
||||
try {
|
||||
const { operationType, targetType, success, startDate, endDate, username, riskLevel, page = 1, pageSize = 50 } = req.query;
|
||||
|
||||
const filters = {
|
||||
operationType,
|
||||
targetType,
|
||||
success: success !== undefined ? success === 'true' : undefined,
|
||||
startDate,
|
||||
endDate,
|
||||
username,
|
||||
riskLevel,
|
||||
};
|
||||
|
||||
const allLogs = getDangerousOperationsLogs(filters);
|
||||
const total = allLogs.length;
|
||||
const startIndex = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const logs = allLogs.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
logs,
|
||||
total,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(total / parseInt(pageSize)),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get dangerous operations logs:', error);
|
||||
res.status(500).json({ error: '获取日志失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/logs/clean', async (req, res) => {
|
||||
try {
|
||||
const { daysToKeep = 90 } = req.query;
|
||||
|
||||
if (!req.user || req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: '只有管理员才能清理日志' });
|
||||
}
|
||||
|
||||
const result = await cleanOldLogs(parseInt(daysToKeep));
|
||||
|
||||
await logDangerousOperation(req, {
|
||||
operationType: DANGEROUS_OPERATION_TYPES.PURGE,
|
||||
operationName: '清理危险操作日志',
|
||||
targetType: 'operation_logs',
|
||||
targetId: null,
|
||||
targetName: `清理 ${daysToKeep} 天前的日志`,
|
||||
metadata: {
|
||||
riskLevel: RISK_LEVELS.MEDIUM,
|
||||
deletedCount: result.deletedCount,
|
||||
remainingCount: result.remainingCount,
|
||||
daysToKeep: parseInt(daysToKeep),
|
||||
},
|
||||
success: true,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `已清理 ${result.deletedCount} 条过期日志,保留 ${result.remainingCount} 条日志`,
|
||||
deletedCount: result.deletedCount,
|
||||
remainingCount: result.remainingCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to clean logs:', error);
|
||||
res.status(500).json({ error: '清理日志失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/risk-assessment', async (req, res) => {
|
||||
try {
|
||||
const { operationType, itemCount, hasRelatedData, isSystemLevel } = req.query;
|
||||
|
||||
const riskLevel = calculateRiskLevel(
|
||||
operationType,
|
||||
parseInt(itemCount) || 1,
|
||||
{
|
||||
hasRelatedData: hasRelatedData === 'true',
|
||||
isSystemLevel: isSystemLevel === 'true',
|
||||
}
|
||||
);
|
||||
|
||||
const riskDescriptions = {
|
||||
[RISK_LEVELS.EXTREME]: '极高风险操作,需要输入确认关键词才能执行',
|
||||
[RISK_LEVELS.HIGH]: '高风险操作,需要详细确认信息',
|
||||
[RISK_LEVELS.MEDIUM]: '中等风险操作,需要明确确认',
|
||||
[RISK_LEVELS.LOW]: '低风险操作,使用标准确认即可',
|
||||
};
|
||||
|
||||
res.json({
|
||||
riskLevel,
|
||||
description: riskDescriptions[riskLevel],
|
||||
requiresKeyword: riskLevel === RISK_LEVELS.EXTREME,
|
||||
confirmationLevel: riskLevel === RISK_LEVELS.EXTREME ? 'KEYWORD' : riskLevel === RISK_LEVELS.HIGH ? 'ENHANCED' : 'STANDARD',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to assess risk:', error);
|
||||
res.status(500).json({ error: '风险评估失败' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+207
-37
@@ -16,6 +16,7 @@ const DevicePort = require('../models/DevicePort');
|
||||
const Cable = require('../models/Cable');
|
||||
const NetworkCard = require('../models/NetworkCard');
|
||||
const InventoryRecord = require('../models/InventoryRecord');
|
||||
const { logDeviceOperation } = require('../utils/operationLogger');
|
||||
const { validateBody, validateQuery } = require('../middleware/validation');
|
||||
const {
|
||||
createDeviceSchema,
|
||||
@@ -469,7 +470,7 @@ router.post('/import-preview', async (req, res) => {
|
||||
|
||||
router.get('/', validateQuery(queryDeviceSchema), async (req, res) => {
|
||||
try {
|
||||
const { keyword, status, type, rackId, roomId, page = 1, pageSize = 10 } = req.query;
|
||||
const { keyword, status, type, rackId, roomId, page = 1, pageSize = 10, isIdle } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// 构建查询条件
|
||||
@@ -544,6 +545,11 @@ router.get('/', validateQuery(queryDeviceSchema), async (req, res) => {
|
||||
where['$Rack.roomId$'] = roomId;
|
||||
}
|
||||
|
||||
// 空闲设备筛选
|
||||
if (isIdle !== undefined && isIdle !== '') {
|
||||
where.isIdle = isIdle === 'true' || isIdle === true;
|
||||
}
|
||||
|
||||
// 执行查询 - 优化:使用 JOIN 避免 N+1 查询问题
|
||||
const { count, rows } = await Device.findAndCountAll({
|
||||
where,
|
||||
@@ -626,14 +632,31 @@ router.post('/', validateBody(createDeviceSchema), async (req, res) => {
|
||||
}
|
||||
|
||||
const device = await Device.create(deviceData);
|
||||
|
||||
|
||||
const rack = await Rack.findByPk(deviceData.rackId);
|
||||
if (rack) {
|
||||
await rack.update({
|
||||
currentPower: rack.currentPower + deviceData.powerConsumption
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const createDetails = [
|
||||
`设备名称: ${device.name}`,
|
||||
`设备编号: ${device.deviceId}`,
|
||||
`设备类型: ${device.type}`,
|
||||
`所属机柜: ${rack ? rack.name : '未分配'}`,
|
||||
`安装位置: U${device.position}`,
|
||||
`功耗: ${device.powerConsumption}W`
|
||||
].join(';');
|
||||
|
||||
await logDeviceOperation('create', `创建设备【${device.name}】`, {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
afterState: device.toJSON(),
|
||||
req,
|
||||
metadata: { deviceType: device.type, rackName: rack?.name, powerConsumption: device.powerConsumption }
|
||||
});
|
||||
|
||||
res.status(201).json(device);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
@@ -1363,32 +1386,32 @@ router.put('/batch-offline', validateBody(batchDeviceIdsSchema), async (req, res
|
||||
router.put('/batch-status', async (req, res) => {
|
||||
try {
|
||||
const { deviceIds, status } = req.body;
|
||||
|
||||
|
||||
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
||||
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
||||
}
|
||||
|
||||
|
||||
// 检查数据库中是否存在这些设备
|
||||
const existingDevices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
attributes: ['deviceId']
|
||||
});
|
||||
|
||||
|
||||
// 检查是否有不存在的设备
|
||||
const existingIds = existingDevices.map(d => d.deviceId);
|
||||
const missingIds = deviceIds.filter(id => !existingIds.includes(id));
|
||||
|
||||
|
||||
if (missingIds.length > 0) {
|
||||
return res.status(404).json({ error: `设备不存在: ${missingIds.join(', ')}` });
|
||||
}
|
||||
|
||||
|
||||
const validStatus = ['running', 'maintenance', 'offline', 'fault'];
|
||||
if (!validStatus.includes(status)) {
|
||||
return res.status(400).json({
|
||||
error: `状态值无效,有效值为:${validStatus.join('、')}`
|
||||
return res.status(400).json({
|
||||
error: `状态值无效,有效值为:${validStatus.join('、')}`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 状态映射
|
||||
const statusText = {
|
||||
running: '运行中',
|
||||
@@ -1396,13 +1419,30 @@ router.put('/batch-status', async (req, res) => {
|
||||
offline: '离线',
|
||||
fault: '故障'
|
||||
};
|
||||
|
||||
|
||||
const beforeDevices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds } }
|
||||
});
|
||||
|
||||
const deviceNames = beforeDevices.map(d => d.name);
|
||||
|
||||
// 更新设备状态
|
||||
const [affectedCount] = await Device.update(
|
||||
{ status },
|
||||
{ where: { deviceId: { [Op.in]: deviceIds } } }
|
||||
);
|
||||
|
||||
|
||||
const statusChangeDesc = `批量变更${affectedCount}台设备状态:${deviceNames.join('、')} → ${statusText[status]}`;
|
||||
|
||||
await logDeviceOperation('status_change', statusChangeDesc, {
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${affectedCount}台设备`,
|
||||
beforeState: beforeDevices.map(d => ({ deviceId: d.deviceId, name: d.name, status: d.status })),
|
||||
afterState: beforeDevices.map(d => ({ deviceId: d.deviceId, name: d.name, status })),
|
||||
req,
|
||||
metadata: { status, statusText: statusText[status], count: affectedCount, deviceNames }
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `批量状态变更成功,已将 ${affectedCount} 个设备状态变更为"${statusText[status]}"`,
|
||||
affectedCount,
|
||||
@@ -1433,9 +1473,16 @@ router.put('/batch-move', async (req, res) => {
|
||||
|
||||
const devicesToMove = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
attributes: ['deviceId', 'position', 'height']
|
||||
attributes: ['deviceId', 'name', 'rackId', 'position', 'height']
|
||||
});
|
||||
|
||||
const beforeMoveState = devicesToMove.map(d => ({
|
||||
deviceId: d.deviceId,
|
||||
name: d.name,
|
||||
rackId: d.rackId,
|
||||
position: d.position
|
||||
}));
|
||||
|
||||
const deviceHeightMap = new Map(devicesToMove.map(d => [d.deviceId, d.height || 1]));
|
||||
|
||||
if (startPosition) {
|
||||
@@ -1511,6 +1558,20 @@ router.put('/batch-move', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
const deviceNames = devicesToMove.map(d => d.name);
|
||||
const moveDesc = startPosition
|
||||
? `批量移动${movedCount}台设备到机柜【${targetRack.name}】:${deviceNames.join('、')} → U${startPosition}起`
|
||||
: `批量移动${movedCount}台设备到机柜【${targetRack.name}】:${deviceNames.join('、')}`;
|
||||
|
||||
await logDeviceOperation('move', moveDesc, {
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${movedCount}台设备`,
|
||||
beforeState: beforeMoveState,
|
||||
afterState: { targetRackId, targetRackName: targetRack.name, startPosition },
|
||||
req,
|
||||
metadata: { count: movedCount, targetRackId, targetRackName: targetRack.name, startPosition, deviceNames }
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `批量移动成功,已将 ${movedCount} 个设备移动到机柜 ${targetRackId}`,
|
||||
movedCount
|
||||
@@ -1542,6 +1603,61 @@ router.get('/:deviceId', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 将设备标记为空闲
|
||||
router.put('/:deviceId/to-idle', async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { idleReason } = req.body;
|
||||
const { deviceId } = req.params;
|
||||
|
||||
const device = await Device.findByPk(deviceId, { transaction: t });
|
||||
if (!device) {
|
||||
await t.rollback();
|
||||
return res.status(404).json({ error: '设备不存在' });
|
||||
}
|
||||
|
||||
if (device.isIdle) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({ error: '设备已经标记为空闲设备' });
|
||||
}
|
||||
|
||||
await device.update({
|
||||
isIdle: true,
|
||||
idleDate: new Date(),
|
||||
idleReason: idleReason || `从设备管理转入`
|
||||
}, { transaction: t });
|
||||
|
||||
if (device.rackId) {
|
||||
const rack = await Rack.findByPk(device.rackId, { transaction: t });
|
||||
if (rack) {
|
||||
await rack.update({
|
||||
currentPower: Math.max(0, rack.currentPower - (device.powerConsumption || 0))
|
||||
}, { transaction: t });
|
||||
}
|
||||
}
|
||||
|
||||
await t.commit();
|
||||
|
||||
await logDeviceOperation('to_idle', `设备【${device.name}】转入空闲设备`, {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState: { ...device.toJSON(), isIdle: false },
|
||||
afterState: { ...device.toJSON(), isIdle: true },
|
||||
req,
|
||||
metadata: { idleReason, type: 'device_to_idle' }
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: '设备已转入空闲设备',
|
||||
device: device.toJSON()
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
console.error('设备转入空闲设备失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取设备的工单列表
|
||||
router.get('/:deviceId/tickets', async (req, res) => {
|
||||
try {
|
||||
@@ -1583,18 +1699,19 @@ router.get('/:deviceId/tickets', async (req, res) => {
|
||||
// 更新设备
|
||||
router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => {
|
||||
try {
|
||||
// 获取旧设备信息以更新功率
|
||||
const oldDevice = await Device.findByPk(req.params.deviceId);
|
||||
if (!oldDevice) {
|
||||
return res.status(404).json({ error: '设备不存在' });
|
||||
}
|
||||
|
||||
|
||||
const beforeState = oldDevice.toJSON();
|
||||
const changedFields = {};
|
||||
|
||||
const [updated] = await Device.update(req.body, {
|
||||
where: { deviceId: req.params.deviceId }
|
||||
});
|
||||
|
||||
|
||||
if (updated) {
|
||||
// 更新机柜当前功率
|
||||
const rack = await Rack.findByPk(oldDevice.rackId);
|
||||
if (rack) {
|
||||
const powerDiff = req.body.powerConsumption - oldDevice.powerConsumption;
|
||||
@@ -1602,7 +1719,7 @@ router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => {
|
||||
currentPower: rack.currentPower + powerDiff
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const updatedDevice = await Device.findByPk(req.params.deviceId, {
|
||||
include: [
|
||||
{
|
||||
@@ -1613,6 +1730,38 @@ router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => {
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const afterState = updatedDevice.toJSON();
|
||||
for (const key of Object.keys(req.body)) {
|
||||
if (JSON.stringify(beforeState[key]) !== JSON.stringify(afterState[key])) {
|
||||
changedFields[key] = { from: beforeState[key], to: afterState[key] };
|
||||
}
|
||||
}
|
||||
|
||||
const changeDetails = Object.entries(changedFields).map(([field, values]) => {
|
||||
const fieldNames = {
|
||||
name: '名称', deviceId: '设备编号', type: '类型', model: '型号',
|
||||
manufacturer: '制造商', serialNumber: '序列号', status: '状态',
|
||||
position: '安装位置(U)', height: '占用高度(U)', powerConsumption: '功耗(W)',
|
||||
ipAddress: 'IP地址', macAddress: 'MAC地址', managementIp: '管理IP'
|
||||
};
|
||||
const displayName = fieldNames[field] || field;
|
||||
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
||||
}).join(';');
|
||||
|
||||
const operationDesc = changeDetails
|
||||
? `更新设备【${updatedDevice.name}】:${changeDetails}`
|
||||
: `更新设备【${updatedDevice.name}】`;
|
||||
|
||||
await logDeviceOperation('update', operationDesc, {
|
||||
targetId: updatedDevice.deviceId,
|
||||
targetName: updatedDevice.name,
|
||||
beforeState,
|
||||
afterState,
|
||||
req,
|
||||
metadata: { changedFields }
|
||||
});
|
||||
|
||||
res.json(updatedDevice);
|
||||
} else {
|
||||
res.status(404).json({ error: '设备不存在' });
|
||||
@@ -1627,17 +1776,19 @@ router.post('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { deviceIds } = req.body;
|
||||
|
||||
|
||||
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({ error: '请提供有效的设备 ID 列表' });
|
||||
}
|
||||
|
||||
|
||||
const devices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
|
||||
const deviceNames = devices.map(d => d.name).join(', ');
|
||||
|
||||
// 1. 删除相关端口 (必须在网卡之前删除,因为端口依赖网卡)
|
||||
await DevicePort.destroy({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
@@ -1660,19 +1811,19 @@ router.post('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res
|
||||
},
|
||||
transaction: t
|
||||
});
|
||||
|
||||
|
||||
// 4. 解除工单关联
|
||||
await Ticket.update(
|
||||
{ deviceId: null },
|
||||
{ where: { deviceId: { [Op.in]: deviceIds } }, transaction: t }
|
||||
);
|
||||
|
||||
|
||||
// 5. 删除盘点记录
|
||||
await InventoryRecord.destroy({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
|
||||
// 6. 更新机柜功率
|
||||
for (const device of devices) {
|
||||
if (device.rackId) {
|
||||
@@ -1684,7 +1835,7 @@ router.post('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 7. 删除设备
|
||||
const deletedCount = await Device.destroy({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
@@ -1692,7 +1843,15 @@ router.post('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res
|
||||
});
|
||||
|
||||
await t.commit();
|
||||
|
||||
|
||||
await logDeviceOperation('batch_delete', `批量删除${deletedCount}台设备:${deviceNames}`, {
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${deletedCount}台设备`,
|
||||
beforeState: devices.map(d => d.toJSON()),
|
||||
req,
|
||||
metadata: { count: deletedCount, deviceNames }
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `批量删除成功,已删除 ${deletedCount} 个设备`,
|
||||
deletedCount
|
||||
@@ -1789,20 +1948,23 @@ router.delete('/:deviceId', async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { deviceId } = req.params;
|
||||
|
||||
|
||||
// 获取设备信息以更新功率
|
||||
const device = await Device.findByPk(deviceId, { transaction: t });
|
||||
if (!device) {
|
||||
await t.rollback();
|
||||
return res.status(404).json({ error: '设备不存在' });
|
||||
}
|
||||
|
||||
|
||||
const deviceName = device.name;
|
||||
const beforeState = device.toJSON();
|
||||
|
||||
// 1. 删除相关端口 (必须在网卡之前删除,因为端口依赖网卡)
|
||||
const deletedPorts = await DevicePort.destroy({
|
||||
where: { deviceId: deviceId },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
|
||||
// 2. 删除相关网卡
|
||||
const deletedNetworkCards = await NetworkCard.destroy({
|
||||
where: { deviceId: deviceId },
|
||||
@@ -1826,13 +1988,13 @@ router.delete('/:deviceId', async (req, res) => {
|
||||
{ deviceId: null },
|
||||
{ where: { deviceId: deviceId }, transaction: t }
|
||||
);
|
||||
|
||||
|
||||
// 5. 删除盘点记录
|
||||
await InventoryRecord.destroy({
|
||||
where: { deviceId: deviceId },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
|
||||
// 6. 更新机柜功率 (必须在删除设备之前)
|
||||
if (device.rackId) {
|
||||
try {
|
||||
@@ -1847,21 +2009,29 @@ router.delete('/:deviceId', async (req, res) => {
|
||||
throw err; // 重新抛出错误,触发事务回滚
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 7. 删除设备 (Delete Device)
|
||||
await Device.destroy({
|
||||
where: { deviceId: deviceId },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
|
||||
// 提交事务
|
||||
await t.commit();
|
||||
|
||||
|
||||
if (deletedCables > 0) {
|
||||
console.log(`已删除 ${deletedCables} 条相关接线`);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
|
||||
await logDeviceOperation('delete', `删除设备【${deviceName}】(编号:${deviceId},类型:${device.type},关联删除:${deletedCables}条接线、${deletedPorts}个端口、${deletedNetworkCards}张网卡)`, {
|
||||
targetId: deviceId,
|
||||
targetName: deviceName,
|
||||
beforeState,
|
||||
req,
|
||||
metadata: { deletedCables, deletedPorts, deletedNetworkCards, deviceType: device.type }
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
message: '删除成功',
|
||||
deviceId: deviceId,
|
||||
deletedCablesCount: deletedCables,
|
||||
|
||||
@@ -0,0 +1,862 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Op } = require('sequelize');
|
||||
const Device = require('../models/Device');
|
||||
const Rack = require('../models/Rack');
|
||||
const Room = require('../models/Room');
|
||||
const { logDeviceOperation } = require('../utils/operationLogger');
|
||||
|
||||
async function generateIdleDeviceId() {
|
||||
const devices = await Device.findAll({
|
||||
where: {
|
||||
deviceId: {
|
||||
[Op.like]: 'DEV%'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let maxNumber = 0;
|
||||
devices.forEach(device => {
|
||||
const match = device.deviceId.match(/^DEV(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1], 10);
|
||||
if (num > maxNumber) {
|
||||
maxNumber = num;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const newNumber = maxNumber + 1;
|
||||
return `DEV${String(newNumber).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { keyword, sourceType, idleReason, page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const where = { isIdle: true };
|
||||
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ deviceId: { [Op.like]: `%${keyword}%` } },
|
||||
{ name: { [Op.like]: `%${keyword}%` } },
|
||||
{ serialNumber: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
if (sourceType && sourceType !== 'all') {
|
||||
where.sourceType = sourceType;
|
||||
}
|
||||
|
||||
if (idleReason) {
|
||||
where.idleReason = { [Op.like]: `%${idleReason}%` };
|
||||
}
|
||||
|
||||
const { count, rows } = await Device.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Rack,
|
||||
attributes: ['rackId', 'name', 'roomId'],
|
||||
include: [{
|
||||
model: Room,
|
||||
attributes: ['roomId', 'name']
|
||||
}]
|
||||
}
|
||||
],
|
||||
offset: parseInt(offset),
|
||||
limit: parseInt(pageSize),
|
||||
order: [['idleDate', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
idleDevices: rows,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取空闲设备列表失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:deviceId', async (req, res) => {
|
||||
try {
|
||||
const device = await Device.findOne({
|
||||
where: { deviceId: req.params.deviceId, isIdle: true },
|
||||
include: [
|
||||
{
|
||||
model: Rack,
|
||||
attributes: ['rackId', 'name', 'roomId'],
|
||||
include: [{
|
||||
model: Room,
|
||||
attributes: ['roomId', 'name']
|
||||
}]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return res.status(404).json({ error: '空闲设备不存在' });
|
||||
}
|
||||
|
||||
res.json(device);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { name, type, model, serialNumber, powerConsumption, idleReason, warehouseId, description, rackId, position } = req.body;
|
||||
|
||||
let { deviceId } = req.body;
|
||||
|
||||
if (!deviceId || deviceId.trim() === '') {
|
||||
deviceId = await generateIdleDeviceId();
|
||||
}
|
||||
|
||||
const existingDevice = await Device.findByPk(deviceId);
|
||||
if (existingDevice) {
|
||||
return res.status(400).json({ error: '设备ID已存在' });
|
||||
}
|
||||
|
||||
if (rackId) {
|
||||
const rack = await Rack.findByPk(rackId);
|
||||
if (!rack) {
|
||||
return res.status(404).json({ error: '机柜不存在' });
|
||||
}
|
||||
}
|
||||
|
||||
const device = await Device.create({
|
||||
deviceId,
|
||||
name: name || '',
|
||||
type: type || 'other',
|
||||
model: model || '',
|
||||
serialNumber: serialNumber || '',
|
||||
powerConsumption: powerConsumption || 0,
|
||||
status: 'offline',
|
||||
isIdle: true,
|
||||
idleDate: new Date(),
|
||||
idleReason: idleReason || '',
|
||||
warehouseId: warehouseId || null,
|
||||
rackId: rackId || null,
|
||||
position: position || null,
|
||||
sourceType: warehouseId ? 'warehouse' : (rackId ? 'rack' : 'rack'),
|
||||
description: description || ''
|
||||
});
|
||||
|
||||
await logDeviceOperation('create', `新增空闲设备【${device.name || deviceId}】`, {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
afterState: device.toJSON(),
|
||||
req,
|
||||
metadata: { sourceType: device.sourceType, type: 'idle_device_create' }
|
||||
});
|
||||
|
||||
res.status(201).json(device);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/from-device/:deviceId', async (req, res) => {
|
||||
const t = await require('../db').sequelize.transaction();
|
||||
try {
|
||||
const { idleReason } = req.body;
|
||||
const { deviceId } = req.params;
|
||||
|
||||
const device = await Device.findByPk(deviceId, { transaction: t });
|
||||
if (!device) {
|
||||
await t.rollback();
|
||||
return res.status(404).json({ error: '设备不存在' });
|
||||
}
|
||||
|
||||
if (device.isIdle) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({ error: '设备已经标记为空闲设备' });
|
||||
}
|
||||
|
||||
await device.update({
|
||||
isIdle: true,
|
||||
idleDate: new Date(),
|
||||
idleReason: idleReason || `从设备管理转入`,
|
||||
sourceType: 'rack'
|
||||
}, { transaction: t });
|
||||
|
||||
await t.commit();
|
||||
|
||||
await logDeviceOperation('to_idle', `设备【${device.name}】转入空闲设备`, {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState: { ...device.toJSON(), isIdle: false },
|
||||
afterState: { ...device.toJSON(), isIdle: true },
|
||||
req,
|
||||
metadata: { idleReason, type: 'device_to_idle' }
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: '设备已转入空闲设备',
|
||||
device: device.toJSON()
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
console.error('设备转入空闲设备失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/batch-from-devices', async (req, res) => {
|
||||
const t = await require('../db').sequelize.transaction();
|
||||
try {
|
||||
const { deviceIds, idleReason } = req.body;
|
||||
|
||||
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
||||
}
|
||||
|
||||
const devices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
const notIdleDevices = devices.filter(d => !d.isIdle);
|
||||
const alreadyIdleDevices = devices.filter(d => d.isIdle);
|
||||
|
||||
if (notIdleDevices.length > 0) {
|
||||
await Device.update(
|
||||
{
|
||||
isIdle: true,
|
||||
idleDate: new Date(),
|
||||
idleReason: idleReason || `批量转入`
|
||||
},
|
||||
{
|
||||
where: { deviceId: { [Op.in]: notIdleDevices.map(d => d.deviceId) } },
|
||||
transaction: t
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await t.commit();
|
||||
|
||||
await logDeviceOperation('batch_to_idle', `批量将 ${notIdleDevices.length} 台设备转入空闲设备`, {
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${notIdleDevices.length}台设备`,
|
||||
req,
|
||||
metadata: { idleReason, type: 'batch_device_to_idle' }
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `成功将 ${notIdleDevices.length} 台设备转入空闲设备`,
|
||||
total: devices.length,
|
||||
updated: notIdleDevices.length,
|
||||
skipped: alreadyIdleDevices.length
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
console.error('批量转入空闲设备失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/batch-restore', async (req, res) => {
|
||||
const t = await require('../db').sequelize.transaction();
|
||||
try {
|
||||
const { devices } = req.body;
|
||||
|
||||
console.log('========== batch-restore 开始 ==========');
|
||||
console.log('原始请求 body:', JSON.stringify(req.body));
|
||||
console.log('devices 参数:', devices);
|
||||
|
||||
if (!devices || !Array.isArray(devices) || devices.length === 0) {
|
||||
await t.rollback();
|
||||
console.log('错误: devices 参数无效');
|
||||
return res.status(400).json({ error: '请提供有效的设备列表' });
|
||||
}
|
||||
|
||||
const deviceIds = devices.map(d => d.deviceId).filter(Boolean);
|
||||
console.log('提取的 deviceIds:', deviceIds);
|
||||
|
||||
if (deviceIds.length === 0) {
|
||||
await t.rollback();
|
||||
console.log('错误: deviceIds 为空');
|
||||
return res.status(400).json({ error: '设备ID不能为空' });
|
||||
}
|
||||
|
||||
console.log('开始查询设备,条件:', { deviceId: { [Op.in]: deviceIds }, isIdle: true });
|
||||
|
||||
const idleDevices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
console.log('查询到的空闲设备数量:', idleDevices.length);
|
||||
if (idleDevices.length > 0) {
|
||||
console.log('查询到的设备ID:', idleDevices.map(d => d.deviceId));
|
||||
}
|
||||
|
||||
if (idleDevices.length === 0) {
|
||||
console.log('没有找到空闲设备,检查设备是否存在:');
|
||||
const allDevices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
transaction: t
|
||||
});
|
||||
console.log('设备表中存在的设备数量:', allDevices.length);
|
||||
if (allDevices.length > 0) {
|
||||
console.log('存在的设备及其 isIdle 状态:', allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })));
|
||||
}
|
||||
|
||||
await t.rollback();
|
||||
return res.status(404).json({ error: '没有找到空闲设备' });
|
||||
}
|
||||
|
||||
let restoredCount = 0;
|
||||
const results = [];
|
||||
|
||||
for (const device of idleDevices) {
|
||||
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
|
||||
if (!deviceConfig) continue;
|
||||
|
||||
const targetRackId = deviceConfig.targetRackId;
|
||||
const targetPosition = deviceConfig.targetPosition;
|
||||
|
||||
if (!targetRackId) {
|
||||
results.push({
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'skipped',
|
||||
reason: '未指定目标机柜'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetRack = await Rack.findByPk(targetRackId, { transaction: t });
|
||||
if (!targetRack) {
|
||||
results.push({
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'failed',
|
||||
reason: '目标机柜不存在'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const height = device.height || 1;
|
||||
const position = targetPosition || 1;
|
||||
|
||||
const checkResult = await checkPositionAvailable(targetRackId, position, height, null, t);
|
||||
if (!checkResult.available) {
|
||||
results.push({
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'failed',
|
||||
reason: `U位${position}已被占用`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
await device.update({
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
rackId: targetRackId,
|
||||
position: position,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'offline'
|
||||
}, { transaction: t });
|
||||
|
||||
await targetRack.update({
|
||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
|
||||
}, { transaction: t });
|
||||
|
||||
restoredCount++;
|
||||
results.push({
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'success',
|
||||
targetRack: targetRack.name,
|
||||
targetPosition: position
|
||||
});
|
||||
}
|
||||
|
||||
await t.commit();
|
||||
|
||||
const successCount = results.filter(r => r.status === 'success').length;
|
||||
const failedCount = results.filter(r => r.status === 'failed').length;
|
||||
const skippedCount = results.filter(r => r.status === 'skipped').length;
|
||||
|
||||
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备`, {
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${successCount}台设备`,
|
||||
req,
|
||||
metadata: { results, type: 'batch_idle_device_restore' }
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `成功上架 ${successCount} 台设备`,
|
||||
total: idleDevices.length,
|
||||
restored: successCount,
|
||||
failed: failedCount,
|
||||
skipped: skippedCount,
|
||||
details: results
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
console.error('批量上架设备失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:deviceId/shelve', async (req, res) => {
|
||||
const t = await require('../db').sequelize.transaction();
|
||||
try {
|
||||
const { deviceId } = req.params;
|
||||
const { name, type, model, serialNumber, height, powerConsumption, rackId, position, description } = req.body;
|
||||
|
||||
const device = await Device.findOne({
|
||||
where: { deviceId, isIdle: true },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
await t.rollback();
|
||||
return res.status(404).json({ error: '空闲设备不存在' });
|
||||
}
|
||||
|
||||
if (!rackId) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({ error: '请选择目标机柜' });
|
||||
}
|
||||
|
||||
const targetRack = await Rack.findByPk(rackId, { transaction: t });
|
||||
if (!targetRack) {
|
||||
await t.rollback();
|
||||
return res.status(404).json({ error: '目标机柜不存在' });
|
||||
}
|
||||
|
||||
const deviceHeight = height || device.height || 1;
|
||||
const positionCheck = await checkPositionAvailable(rackId, position, deviceHeight, deviceId, t);
|
||||
if (!positionCheck.available) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({ error: positionCheck.reason });
|
||||
}
|
||||
|
||||
const beforeState = device.toJSON();
|
||||
|
||||
await device.update({
|
||||
name: name || device.name,
|
||||
type: type || device.type,
|
||||
model: model || device.model,
|
||||
serialNumber: serialNumber || device.serialNumber,
|
||||
height: deviceHeight,
|
||||
powerConsumption: powerConsumption || device.powerConsumption || 0,
|
||||
rackId: rackId,
|
||||
position: position,
|
||||
description: description || device.description,
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'running'
|
||||
}, { transaction: t });
|
||||
|
||||
await targetRack.update({
|
||||
currentPower: targetRack.currentPower + (powerConsumption || device.powerConsumption || 0)
|
||||
}, { transaction: t });
|
||||
|
||||
await t.commit();
|
||||
|
||||
const updatedDevice = await Device.findByPk(deviceId, {
|
||||
include: [
|
||||
{ model: Rack, include: [Room] }
|
||||
]
|
||||
});
|
||||
|
||||
await logDeviceOperation('shelve', `空闲设备【${device.name}】上架到机柜【${targetRack.name}】U${position}`, {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState: { ...beforeState, isIdle: true },
|
||||
afterState: updatedDevice.toJSON(),
|
||||
req,
|
||||
metadata: { rackId, position, type: 'idle_device_shelve' }
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: '设备上架成功',
|
||||
device: updatedDevice
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
console.error('设备上架失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:deviceId', async (req, res) => {
|
||||
try {
|
||||
const device = await Device.findOne({
|
||||
where: { deviceId: req.params.deviceId, isIdle: true }
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return res.status(404).json({ error: '空闲设备不存在' });
|
||||
}
|
||||
|
||||
const beforeState = device.toJSON();
|
||||
const allowedFields = ['name', 'type', 'model', 'idleReason', 'description', 'powerConsumption'];
|
||||
|
||||
allowedFields.forEach(field => {
|
||||
if (req.body[field] !== undefined) {
|
||||
device[field] = req.body[field];
|
||||
}
|
||||
});
|
||||
|
||||
if (req.body.warehouseId !== undefined) {
|
||||
device.warehouseId = req.body.warehouseId || null;
|
||||
device.sourceType = req.body.warehouseId ? 'warehouse' : 'rack';
|
||||
}
|
||||
|
||||
if (req.body.rackId !== undefined) {
|
||||
if (req.body.rackId) {
|
||||
const rack = await Rack.findByPk(req.body.rackId);
|
||||
if (!rack) {
|
||||
return res.status(404).json({ error: '机柜不存在' });
|
||||
}
|
||||
}
|
||||
device.rackId = req.body.rackId || null;
|
||||
if (!req.body.warehouseId) {
|
||||
device.sourceType = 'rack';
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.position !== undefined) {
|
||||
device.position = req.body.position || null;
|
||||
}
|
||||
|
||||
await device.save();
|
||||
|
||||
await logDeviceOperation('update', `更新空闲设备【${device.name}】`, {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState,
|
||||
afterState: device.toJSON(),
|
||||
req,
|
||||
metadata: { type: 'idle_device_update' }
|
||||
});
|
||||
|
||||
res.json(device);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:deviceId/restore', async (req, res) => {
|
||||
const t = await require('../db').sequelize.transaction();
|
||||
try {
|
||||
const { targetRackId, targetPosition } = req.body;
|
||||
const { deviceId } = req.params;
|
||||
|
||||
const device = await Device.findOne({
|
||||
where: { deviceId, isIdle: true },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
await t.rollback();
|
||||
return res.status(404).json({ error: '空闲设备不存在' });
|
||||
}
|
||||
|
||||
if (!targetRackId || !targetPosition) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({ error: '恢复设备需要指定目标机柜和位置' });
|
||||
}
|
||||
|
||||
const targetRack = await Rack.findByPk(targetRackId, { transaction: t });
|
||||
if (!targetRack) {
|
||||
await t.rollback();
|
||||
return res.status(404).json({ error: '目标机柜不存在' });
|
||||
}
|
||||
|
||||
const positionCheck = await checkPositionAvailable(targetRackId, targetPosition, device.height || 1, deviceId, t);
|
||||
if (!positionCheck.available) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({ error: positionCheck.reason });
|
||||
}
|
||||
|
||||
await device.update({
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
rackId: targetRackId,
|
||||
position: targetPosition,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'offline'
|
||||
}, { transaction: t });
|
||||
|
||||
await targetRack.update({
|
||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
|
||||
}, { transaction: t });
|
||||
|
||||
await t.commit();
|
||||
|
||||
const updatedDevice = await Device.findByPk(deviceId, {
|
||||
include: [
|
||||
{ model: Rack, include: [Room] }
|
||||
]
|
||||
});
|
||||
|
||||
await logDeviceOperation('restore', `空闲设备【${device.name}】恢复到机柜【${targetRack.name}】U${targetPosition}`, {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState: { ...device.toJSON(), isIdle: true },
|
||||
afterState: updatedDevice.toJSON(),
|
||||
req,
|
||||
metadata: { targetRackId, targetPosition, type: 'idle_device_restore' }
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: '设备已恢复到设备管理',
|
||||
device: updatedDevice
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
console.error('恢复设备失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/batch-restore', async (req, res) => {
|
||||
const t = await require('../db').sequelize.transaction();
|
||||
try {
|
||||
const { devices } = req.body;
|
||||
|
||||
console.log('========== batch-restore 开始 ==========');
|
||||
console.log('原始请求 body:', JSON.stringify(req.body));
|
||||
console.log('devices 参数:', devices);
|
||||
|
||||
if (!devices || !Array.isArray(devices) || devices.length === 0) {
|
||||
await t.rollback();
|
||||
console.log('错误: devices 参数无效');
|
||||
return res.status(400).json({ error: '请提供有效的设备列表' });
|
||||
}
|
||||
|
||||
const deviceIds = devices.map(d => d.deviceId).filter(Boolean);
|
||||
console.log('提取的 deviceIds:', deviceIds);
|
||||
console.log('deviceIds 类型:', typeof deviceIds, Array.isArray(deviceIds));
|
||||
|
||||
if (deviceIds.length === 0) {
|
||||
await t.rollback();
|
||||
console.log('错误: deviceIds 为空');
|
||||
return res.status(400).json({ error: '设备ID不能为空' });
|
||||
}
|
||||
|
||||
console.log('开始查询设备,条件:', { deviceId: { [Op.in]: deviceIds }, isIdle: true });
|
||||
|
||||
const idleDevices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
console.log('查询到的空闲设备数量:', idleDevices.length);
|
||||
if (idleDevices.length > 0) {
|
||||
console.log('查询到的设备ID:', idleDevices.map(d => d.deviceId));
|
||||
}
|
||||
|
||||
if (idleDevices.length === 0) {
|
||||
console.log('没有找到空闲设备,检查设备是否存在:');
|
||||
const allDevices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
transaction: t
|
||||
});
|
||||
console.log('设备表中存在的设备数量:', allDevices.length);
|
||||
if (allDevices.length > 0) {
|
||||
console.log('存在的设备及其 isIdle 状态:', allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })));
|
||||
}
|
||||
|
||||
await t.rollback();
|
||||
return res.status(404).json({ error: '没有找到空闲设备' });
|
||||
}
|
||||
|
||||
let restoredCount = 0;
|
||||
const results = [];
|
||||
|
||||
for (const device of idleDevices) {
|
||||
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
|
||||
if (!deviceConfig) continue;
|
||||
|
||||
const targetRackId = deviceConfig.targetRackId;
|
||||
const targetPosition = deviceConfig.targetPosition;
|
||||
|
||||
if (!targetRackId) {
|
||||
results.push({
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'skipped',
|
||||
reason: '未指定目标机柜'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetRack = await Rack.findByPk(targetRackId, { transaction: t });
|
||||
if (!targetRack) {
|
||||
results.push({
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'failed',
|
||||
reason: '目标机柜不存在'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const height = device.height || 1;
|
||||
const position = targetPosition || 1;
|
||||
|
||||
const checkResult = await checkPositionAvailable(targetRackId, position, height, null, t);
|
||||
if (!checkResult.available) {
|
||||
results.push({
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'failed',
|
||||
reason: `U位${position}已被占用`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
await device.update({
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
rackId: targetRackId,
|
||||
position: position,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'offline'
|
||||
}, { transaction: t });
|
||||
|
||||
await targetRack.update({
|
||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
|
||||
}, { transaction: t });
|
||||
|
||||
restoredCount++;
|
||||
results.push({
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'success',
|
||||
targetRack: targetRack.name,
|
||||
targetPosition: position
|
||||
});
|
||||
}
|
||||
|
||||
await t.commit();
|
||||
|
||||
const successCount = results.filter(r => r.status === 'success').length;
|
||||
const failedCount = results.filter(r => r.status === 'failed').length;
|
||||
const skippedCount = results.filter(r => r.status === 'skipped').length;
|
||||
|
||||
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备`, {
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${successCount}台设备`,
|
||||
req,
|
||||
metadata: { results, type: 'batch_idle_device_restore' }
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `成功上架 ${successCount} 台设备`,
|
||||
total: idleDevices.length,
|
||||
restored: successCount,
|
||||
failed: failedCount,
|
||||
skipped: skippedCount,
|
||||
details: results
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
console.error('批量上架设备失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:deviceId', async (req, res) => {
|
||||
const t = await require('../db').sequelize.transaction();
|
||||
try {
|
||||
const device = await Device.findOne({
|
||||
where: { deviceId: req.params.deviceId, isIdle: true },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
await t.rollback();
|
||||
return res.status(404).json({ error: '空闲设备不存在' });
|
||||
}
|
||||
|
||||
const beforeState = device.toJSON();
|
||||
|
||||
await device.destroy({ transaction: t });
|
||||
|
||||
await t.commit();
|
||||
|
||||
await logDeviceOperation('delete', `删除空闲设备【${device.name || device.deviceId}】`, {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState,
|
||||
req,
|
||||
metadata: { type: 'idle_device_delete' }
|
||||
});
|
||||
|
||||
res.json({ message: '空闲设备删除成功' });
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
console.error('删除空闲设备失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
async function checkPositionAvailable(rackId, position, height, excludeDeviceId = null, transaction = null) {
|
||||
if (!position || position <= 0) {
|
||||
return { available: true, reason: null };
|
||||
}
|
||||
|
||||
const deviceHeight = height || 1;
|
||||
const startU = position;
|
||||
const endU = position + deviceHeight - 1;
|
||||
|
||||
const queryOptions = {
|
||||
where: {
|
||||
rackId: rackId,
|
||||
position: { [Op.ne]: null },
|
||||
isIdle: false
|
||||
},
|
||||
attributes: ['deviceId', 'position', 'height']
|
||||
};
|
||||
|
||||
if (transaction) {
|
||||
queryOptions.transaction = transaction;
|
||||
}
|
||||
|
||||
const existingDevices = await Device.findAll(queryOptions);
|
||||
|
||||
for (const d of existingDevices) {
|
||||
if (excludeDeviceId && d.deviceId === excludeDeviceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existStart = d.position;
|
||||
const existEnd = d.position + (d.height || 1) - 1;
|
||||
|
||||
if (!(endU < existStart || startU > existEnd)) {
|
||||
return {
|
||||
available: false,
|
||||
reason: `U位冲突:机柜中已有设备 ${d.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { available: true, reason: null };
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,263 @@
|
||||
const express = require('express');
|
||||
const { Op } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
const OperationLog = require('../models/OperationLog');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.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: '获取操作日志失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/modules', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const modules = await OperationLog.findAll({
|
||||
attributes: ['module'],
|
||||
group: ['module']
|
||||
});
|
||||
|
||||
const moduleList = modules.map(m => ({
|
||||
value: m.module,
|
||||
label: getModuleName(m.module)
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: moduleList
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取模块列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取模块列表失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/types', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { module } = req.query;
|
||||
|
||||
const where = {};
|
||||
if (module && module !== 'all') {
|
||||
where.module = module;
|
||||
}
|
||||
|
||||
const types = await OperationLog.findAll({
|
||||
where,
|
||||
attributes: ['operationType'],
|
||||
group: ['operationType']
|
||||
});
|
||||
|
||||
const typeList = types.map(t => ({
|
||||
value: t.operationType,
|
||||
label: getOperationTypeName(t.operationType)
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: typeList
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作类型列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作类型列表失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/statistics', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
const where = {};
|
||||
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 [moduleStats, typeStats, dailyStats] = await Promise.all([
|
||||
OperationLog.findAll({
|
||||
where,
|
||||
attributes: ['module', [sequelize.fn('COUNT', sequelize.col('module')), 'count']],
|
||||
group: ['module']
|
||||
}),
|
||||
OperationLog.findAll({
|
||||
where,
|
||||
attributes: ['operationType', [sequelize.fn('COUNT', sequelize.col('operationType')), 'count']],
|
||||
group: ['operationType']
|
||||
}),
|
||||
OperationLog.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
[sequelize.fn('DATE', sequelize.col('createdAt')), 'date'],
|
||||
[sequelize.fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: [sequelize.fn('DATE', sequelize.col('createdAt'))],
|
||||
order: [[sequelize.fn('DATE', sequelize.col('createdAt')), 'DESC']],
|
||||
limit: 30
|
||||
})
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
byModule: moduleStats.map(s => ({ module: s.module, count: s.get('count') })),
|
||||
byType: typeStats.map(s => ({ type: s.operationType, count: s.get('count') })),
|
||||
byDay: dailyStats.map(s => ({ date: s.get('date'), count: s.get('count') }))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志统计失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作日志统计失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.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: '获取操作日志详情失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function getModuleName(module) {
|
||||
const moduleNames = {
|
||||
device: '设备管理',
|
||||
user: '用户管理',
|
||||
role: '角色管理',
|
||||
consumable: '耗材管理',
|
||||
rack: '机柜管理',
|
||||
room: '机房管理',
|
||||
ticket: '工单管理',
|
||||
backup: '备份管理'
|
||||
};
|
||||
return moduleNames[module] || module;
|
||||
}
|
||||
|
||||
function getOperationTypeName(type) {
|
||||
const typeNames = {
|
||||
create: '创建',
|
||||
update: '更新',
|
||||
delete: '删除',
|
||||
batch_delete: '批量删除',
|
||||
batch_update: '批量更新',
|
||||
status_change: '状态变更',
|
||||
move: '移动',
|
||||
permission_change: '权限变更',
|
||||
import: '导入',
|
||||
export: '导出'
|
||||
};
|
||||
return typeNames[type] || type;
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
@@ -4,6 +4,7 @@ const Permission = require('../models/Permission');
|
||||
const UserRole = require('../models/UserRole');
|
||||
const User = require('../models/User');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { logRoleOperation } = require('../utils/operationLogger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -134,6 +135,18 @@ router.post('/', authMiddleware, async (req, res) => {
|
||||
sort: sort || 0
|
||||
});
|
||||
|
||||
const permissionNames = permissions && permissions.length > 0
|
||||
? permissions.join('、')
|
||||
: '无';
|
||||
|
||||
await logRoleOperation('create', `创建角色【${roleName}】(编码:${roleCode},权限:${permissionNames})`, {
|
||||
targetId: role.roleId,
|
||||
targetName: roleName,
|
||||
afterState: role.toJSON(),
|
||||
req,
|
||||
metadata: { roleCode, permissions, permissionNames }
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '创建成功',
|
||||
@@ -160,6 +173,8 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const beforeState = role.toJSON();
|
||||
|
||||
if (roleName !== undefined) role.roleName = roleName;
|
||||
if (description !== undefined) role.description = description;
|
||||
if (permissions !== undefined) role.permissions = permissions;
|
||||
@@ -168,6 +183,53 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
|
||||
|
||||
await role.save();
|
||||
|
||||
const afterState = role.toJSON();
|
||||
const changedFields = {};
|
||||
|
||||
if (roleName !== undefined && beforeState.roleName !== roleName) {
|
||||
changedFields.roleName = { from: beforeState.roleName, to: roleName };
|
||||
}
|
||||
if (description !== undefined && beforeState.description !== description) {
|
||||
changedFields.description = { from: beforeState.description, to: description };
|
||||
}
|
||||
if (permissions !== undefined) {
|
||||
const oldPerms = (beforeState.permissions || []).sort().join(',');
|
||||
const newPerms = (permissions || []).sort().join(',');
|
||||
if (oldPerms !== newPerms) {
|
||||
changedFields.permissions = { from: beforeState.permissions, to: permissions };
|
||||
}
|
||||
}
|
||||
if (status !== undefined && beforeState.status !== status) {
|
||||
const statusText = { active: '启用', inactive: '禁用' };
|
||||
changedFields.status = { from: beforeState.status, to: status, fromText: statusText[beforeState.status], toText: statusText[status] };
|
||||
}
|
||||
|
||||
const changeDetails = Object.entries(changedFields).map(([field, values]) => {
|
||||
const fieldNames = { roleName: '角色名称', description: '描述', permissions: '权限', status: '状态' };
|
||||
const displayName = fieldNames[field] || field;
|
||||
|
||||
if (field === 'permissions') {
|
||||
return `权限: ${(values.from || []).join('、') || '无'} → ${(values.to || []).join('、') || '无'}`;
|
||||
}
|
||||
if (field === 'status') {
|
||||
return `状态: ${values.fromText} → ${values.toText}`;
|
||||
}
|
||||
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
||||
}).join(';');
|
||||
|
||||
const updateDesc = changeDetails
|
||||
? `更新角色【${role.roleName}】:${changeDetails}`
|
||||
: `更新角色【${role.roleName}】`;
|
||||
|
||||
await logRoleOperation('update', updateDesc, {
|
||||
targetId: role.roleId,
|
||||
targetName: role.roleName,
|
||||
beforeState,
|
||||
afterState,
|
||||
req,
|
||||
metadata: { changedFields, oldRoleName: beforeState.roleName, oldPermissions: beforeState.permissions }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '更新成功',
|
||||
@@ -208,8 +270,20 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const roleName = role.roleName;
|
||||
const roleCode = role.roleCode;
|
||||
const beforeState = role.toJSON();
|
||||
|
||||
await role.destroy();
|
||||
|
||||
await logRoleOperation('delete', `删除角色【${roleName}】(编码:${roleCode},权限:${(role.permissions || []).join('、') || '无'})`, {
|
||||
targetId: req.params.roleId,
|
||||
targetName: roleName,
|
||||
beforeState,
|
||||
req,
|
||||
metadata: { roleCode, userCount, permissions: role.permissions }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
|
||||
+13
-3
@@ -8,10 +8,20 @@ const { createRoomSchema, updateRoomSchema } = require('../validation/roomSchema
|
||||
// 获取所有机房
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const rooms = await Room.findAll({
|
||||
include: Rack
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const pageSize = parseInt(req.query.pageSize) || 100;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const { count, rows } = await Room.findAndCountAll({
|
||||
include: [{ model: Rack, attributes: ['rackId', 'name'] }],
|
||||
offset: offset,
|
||||
limit: pageSize
|
||||
});
|
||||
|
||||
res.json({
|
||||
rooms: rows,
|
||||
total: count
|
||||
});
|
||||
res.json(rooms);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
|
||||
+123
-7
@@ -7,6 +7,7 @@ const Role = require('../models/Role');
|
||||
const UserRole = require('../models/UserRole');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { SALT_ROUNDS, PASSWORD_MIN_LENGTH, FILE_UPLOAD, PAGINATION } = require('../config');
|
||||
const { logUserOperation } = require('../utils/operationLogger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -16,22 +17,30 @@ const generateId = () => {
|
||||
|
||||
const getWhereClause = (query) => {
|
||||
const where = {};
|
||||
|
||||
|
||||
if (query.username) {
|
||||
where.username = { [Op.like]: `%${query.username}%` };
|
||||
}
|
||||
|
||||
|
||||
if (query.status) {
|
||||
where.status = query.status;
|
||||
}
|
||||
|
||||
|
||||
if (query.realName) {
|
||||
where.realName = { [Op.like]: `%${query.realName}%` };
|
||||
}
|
||||
|
||||
|
||||
return where;
|
||||
};
|
||||
|
||||
const getUserRoleIds = async (userId) => {
|
||||
const userRoles = await UserRole.findAll({
|
||||
where: { UserId: userId },
|
||||
attributes: ['RoleId']
|
||||
});
|
||||
return userRoles.map(ur => ur.RoleId);
|
||||
};
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
@@ -204,6 +213,23 @@ router.post('/', authMiddleware, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
const roleNames = roleIds && roleIds.length > 0
|
||||
? (await Role.findAll({ where: { roleId: { [Op.in]: roleIds } } })).map(r => r.roleName).join('、')
|
||||
: '未分配角色';
|
||||
|
||||
await logUserOperation('create', `创建用户【${username}】(姓名:${realName || '未填写'},邮箱:${email || '未填写'},角色:${roleNames})`, {
|
||||
targetId: user.userId,
|
||||
targetName: username,
|
||||
afterState: {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName,
|
||||
status: user.status
|
||||
},
|
||||
req,
|
||||
metadata: { roleIds, roleNames }
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '创建成功',
|
||||
@@ -236,9 +262,20 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const beforeState = {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
realName: user.realName,
|
||||
status: user.status,
|
||||
remark: user.remark
|
||||
};
|
||||
|
||||
const oldRoleIds = roleIds !== undefined ? null : await getUserRoleIds(user.userId);
|
||||
|
||||
if (username !== undefined && username !== user.username) {
|
||||
const existingUser = await User.findOne({
|
||||
where: { username, userId: { [Op.ne]: user.userId } }
|
||||
const existingUser = await User.findOne({
|
||||
where: { username, userId: { [Op.ne]: user.userId } }
|
||||
});
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
@@ -261,21 +298,82 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
|
||||
await user.save();
|
||||
|
||||
let permissionChanged = false;
|
||||
let oldRoleNames = [];
|
||||
let newRoleNames = [];
|
||||
|
||||
if (roleIds !== undefined) {
|
||||
const oldRoles = await Role.findAll({ where: { roleId: { [Op.in]: oldRoleIds || [] } } });
|
||||
oldRoleNames = oldRoles.map(r => r.roleName);
|
||||
|
||||
await UserRole.destroy({ where: { UserId: user.userId } });
|
||||
|
||||
|
||||
for (const roleId of roleIds) {
|
||||
await UserRole.create({
|
||||
UserId: user.userId,
|
||||
RoleId: roleId
|
||||
});
|
||||
}
|
||||
|
||||
const newRoles = await Role.findAll({ where: { roleId: { [Op.in]: roleIds } } });
|
||||
newRoleNames = newRoles.map(r => r.roleName);
|
||||
permissionChanged = true;
|
||||
}
|
||||
|
||||
const updatedUser = await User.findByPk(req.params.userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
|
||||
if (permissionChanged) {
|
||||
const roleChangeDesc = `变更用户【${updatedUser.username}】的角色:${oldRoleNames.join('、') || '无'} → ${newRoleNames.join('、') || '无'}`;
|
||||
await logUserOperation('permission_change', roleChangeDesc, {
|
||||
targetId: updatedUser.userId,
|
||||
targetName: updatedUser.username,
|
||||
beforeState: { ...beforeState, roleIds: oldRoleIds, roleNames: oldRoleNames },
|
||||
afterState: { ...beforeState, roleIds, roleNames: newRoleNames },
|
||||
req,
|
||||
metadata: { oldRoleIds, newRoleIds: roleIds, oldRoleNames, newRoleNames }
|
||||
});
|
||||
} else {
|
||||
const afterState = {
|
||||
username: updatedUser.username,
|
||||
email: updatedUser.email,
|
||||
phone: updatedUser.phone,
|
||||
realName: updatedUser.realName,
|
||||
status: updatedUser.status,
|
||||
remark: updatedUser.remark
|
||||
};
|
||||
|
||||
const changedFields = {};
|
||||
for (const key of Object.keys(beforeState)) {
|
||||
if (JSON.stringify(beforeState[key]) !== JSON.stringify(afterState[key])) {
|
||||
changedFields[key] = { from: beforeState[key], to: afterState[key] };
|
||||
}
|
||||
}
|
||||
|
||||
const changeDetails = Object.entries(changedFields).map(([field, values]) => {
|
||||
const fieldNames = {
|
||||
username: '用户名', email: '邮箱', phone: '电话', realName: '姓名',
|
||||
status: '状态', remark: '备注'
|
||||
};
|
||||
const displayName = fieldNames[field] || field;
|
||||
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
||||
}).join(';');
|
||||
|
||||
const updateDesc = changeDetails
|
||||
? `更新用户【${updatedUser.username}】:${changeDetails}`
|
||||
: `更新用户【${updatedUser.username}】`;
|
||||
|
||||
await logUserOperation('update', updateDesc, {
|
||||
targetId: updatedUser.userId,
|
||||
targetName: updatedUser.username,
|
||||
beforeState,
|
||||
afterState,
|
||||
req,
|
||||
metadata: { changedFields }
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '更新成功',
|
||||
@@ -343,9 +441,27 @@ router.delete('/:userId', authMiddleware, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const userName = user.username;
|
||||
const userRealName = user.realName;
|
||||
const userEmail = user.email;
|
||||
const beforeState = {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName,
|
||||
status: user.status
|
||||
};
|
||||
|
||||
await UserRole.destroy({ where: { UserId: user.userId } });
|
||||
await user.destroy();
|
||||
|
||||
await logUserOperation('delete', `删除用户【${userName}】(姓名:${userRealName || '未填写'},邮箱:${userEmail || '未填写'})`, {
|
||||
targetId: req.params.userId,
|
||||
targetName: userName,
|
||||
beforeState,
|
||||
req,
|
||||
metadata: { deletedUsername: userName, realName: userRealName, email: userEmail }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Op } = require('sequelize');
|
||||
const Warehouse = require('../models/Warehouse');
|
||||
const Device = require('../models/Device');
|
||||
const { logDeviceOperation } = require('../utils/operationLogger');
|
||||
|
||||
async function generateWarehouseId() {
|
||||
const warehouses = await Warehouse.findAll({
|
||||
where: {
|
||||
warehouseId: { [Op.like]: 'WH%' }
|
||||
}
|
||||
});
|
||||
|
||||
let maxNumber = 0;
|
||||
warehouses.forEach(wh => {
|
||||
const match = wh.warehouseId.match(/^WH(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1], 10);
|
||||
if (num > maxNumber) {
|
||||
maxNumber = num;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const newNumber = maxNumber + 1;
|
||||
return `WH${String(newNumber).padStart(3, '0')}`;
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { keyword, status, page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ warehouseId: { [Op.like]: `%${keyword}%` } },
|
||||
{ name: { [Op.like]: `%${keyword}%` } },
|
||||
{ location: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const { count, rows } = await Warehouse.findAndCountAll({
|
||||
where,
|
||||
offset: parseInt(offset),
|
||||
limit: parseInt(pageSize),
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
const warehousesWithCount = await Promise.all(
|
||||
rows.map(async (warehouse) => {
|
||||
const deviceCount = await Device.count({
|
||||
where: { warehouseId: warehouse.warehouseId, isIdle: true }
|
||||
});
|
||||
return {
|
||||
...warehouse.toJSON(),
|
||||
deviceCount
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
warehouses: warehousesWithCount,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取库房列表失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:warehouseId', async (req, res) => {
|
||||
try {
|
||||
const warehouse = await Warehouse.findByPk(req.params.warehouseId);
|
||||
if (!warehouse) {
|
||||
return res.status(404).json({ error: '库房不存在' });
|
||||
}
|
||||
|
||||
const deviceCount = await Device.count({
|
||||
where: { warehouseId: warehouse.warehouseId, isIdle: true }
|
||||
});
|
||||
|
||||
res.json({
|
||||
...warehouse.toJSON(),
|
||||
deviceCount
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:warehouseId/devices', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const warehouse = await Warehouse.findByPk(req.params.warehouseId);
|
||||
if (!warehouse) {
|
||||
return res.status(404).json({ error: '库房不存在' });
|
||||
}
|
||||
|
||||
const { count, rows } = await Device.findAndCountAll({
|
||||
where: { warehouseId: req.params.warehouseId, isIdle: true },
|
||||
offset: parseInt(offset),
|
||||
limit: parseInt(pageSize),
|
||||
order: [['idleDate', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
devices: rows,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取库房设备失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { name, location, capacity, description } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: '库房名称不能为空' });
|
||||
}
|
||||
|
||||
const warehouseId = await generateWarehouseId();
|
||||
|
||||
const warehouse = await Warehouse.create({
|
||||
warehouseId,
|
||||
name,
|
||||
location: location || '',
|
||||
capacity: capacity || 100,
|
||||
status: 'active',
|
||||
description: description || ''
|
||||
});
|
||||
|
||||
await logDeviceOperation('create', `创建库房【${name}】`, {
|
||||
targetId: warehouse.warehouseId,
|
||||
targetName: name,
|
||||
afterState: warehouse.toJSON(),
|
||||
req,
|
||||
metadata: { type: 'warehouse_create' }
|
||||
});
|
||||
|
||||
res.status(201).json(warehouse);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:warehouseId', async (req, res) => {
|
||||
try {
|
||||
const warehouse = await Warehouse.findByPk(req.params.warehouseId);
|
||||
if (!warehouse) {
|
||||
return res.status(404).json({ error: '库房不存在' });
|
||||
}
|
||||
|
||||
const beforeState = warehouse.toJSON();
|
||||
const { name, location, capacity, status, description } = req.body;
|
||||
|
||||
if (name) warehouse.name = name;
|
||||
if (location !== undefined) warehouse.location = location;
|
||||
if (capacity !== undefined) warehouse.capacity = capacity;
|
||||
if (status) warehouse.status = status;
|
||||
if (description !== undefined) warehouse.description = description;
|
||||
|
||||
await warehouse.save();
|
||||
|
||||
await logDeviceOperation('update', `更新库房【${warehouse.name}】`, {
|
||||
targetId: warehouse.warehouseId,
|
||||
targetName: warehouse.name,
|
||||
beforeState,
|
||||
afterState: warehouse.toJSON(),
|
||||
req,
|
||||
metadata: { type: 'warehouse_update' }
|
||||
});
|
||||
|
||||
res.json(warehouse);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:warehouseId', async (req, res) => {
|
||||
try {
|
||||
const warehouse = await Warehouse.findByPk(req.params.warehouseId);
|
||||
if (!warehouse) {
|
||||
return res.status(404).json({ error: '库房不存在' });
|
||||
}
|
||||
|
||||
const idleDeviceCount = await Device.count({
|
||||
where: { warehouseId: req.params.warehouseId, isIdle: true }
|
||||
});
|
||||
|
||||
if (idleDeviceCount > 0) {
|
||||
return res.status(400).json({
|
||||
error: `库房中还有 ${idleDeviceCount} 台空闲设备,请先处理后再删除`
|
||||
});
|
||||
}
|
||||
|
||||
const warehouseName = warehouse.name;
|
||||
await warehouse.destroy();
|
||||
|
||||
await logDeviceOperation('delete', `删除库房【${warehouseName}】`, {
|
||||
targetId: req.params.warehouseId,
|
||||
targetName: warehouseName,
|
||||
req,
|
||||
metadata: { type: 'warehouse_delete' }
|
||||
});
|
||||
|
||||
res.json({ message: '库房删除成功' });
|
||||
} catch (error) {
|
||||
console.error('删除库房失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user