refactor: 统一代码风格并迁移至 ESLint 新配置

style(backend): 格式化模型文件代码
style(frontend): 调整组件代码格式
chore: 删除旧 ESLint 配置并添加新配置
refactor(backend): 重构模型定义语法
style: 统一箭头函数和对象属性简写
This commit is contained in:
zhang1106
2026-03-27 19:12:16 +08:00
parent 6a8d4144ff
commit 63f0cb570e
166 changed files with 15483 additions and 11170 deletions
+77 -61
View File
@@ -4,7 +4,13 @@ const User = require('../models/User');
const Role = require('../models/Role');
const UserRole = require('../models/UserRole');
const { generateToken, authMiddleware } = require('../middleware/auth');
const { SALT_ROUNDS, MAX_LOGIN_ATTEMPTS, PASSWORD_MIN_LENGTH, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH } = require('../config');
const {
SALT_ROUNDS,
MAX_LOGIN_ATTEMPTS,
PASSWORD_MIN_LENGTH,
USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH,
} = require('../config');
const router = express.Router();
@@ -19,21 +25,21 @@ router.post('/register', async (req, res) => {
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
message: '用户名和密码不能为空',
});
}
if (username.length < USERNAME_MIN_LENGTH || username.length > USERNAME_MAX_LENGTH) {
return res.status(400).json({
success: false,
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`,
});
}
if (password.length < PASSWORD_MIN_LENGTH) {
return res.status(400).json({
success: false,
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
});
}
@@ -41,7 +47,7 @@ router.post('/register', async (req, res) => {
if (existingUser) {
return res.status(400).json({
success: false,
message: '用户名已存在'
message: '用户名已存在',
});
}
@@ -57,7 +63,7 @@ router.post('/register', async (req, res) => {
email,
phone,
realName: realName || username,
status: isFirstUser ? 'active' : 'pending'
status: isFirstUser ? 'active' : 'pending',
});
if (isFirstUser) {
@@ -70,13 +76,13 @@ router.post('/register', async (req, res) => {
roleCode: 'admin',
description: '系统管理员,拥有所有权限',
status: 'active',
permissions: []
permissions: [],
});
}
await UserRole.create({
UserId: user.userId,
RoleId: adminRole.roleId
RoleId: adminRole.roleId,
});
const token = generateToken(user);
@@ -89,11 +95,11 @@ router.post('/register', async (req, res) => {
userId: user.userId,
username: user.username,
email: user.email,
realName: user.realName
realName: user.realName,
},
token,
isFirstUser: true
}
isFirstUser: true,
},
});
} else {
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
@@ -101,7 +107,7 @@ router.post('/register', async (req, res) => {
if (defaultRole) {
await UserRole.create({
UserId: user.userId,
RoleId: defaultRole.roleId
RoleId: defaultRole.roleId,
});
}
@@ -113,11 +119,11 @@ router.post('/register', async (req, res) => {
userId: user.userId,
username: user.username,
email: user.email,
realName: user.realName
realName: user.realName,
},
isFirstUser: false,
pendingApproval: true
}
pendingApproval: true,
},
});
}
} catch (error) {
@@ -125,7 +131,7 @@ router.post('/register', async (req, res) => {
res.status(500).json({
success: false,
message: '注册失败',
error: error.message
error: error.message,
});
}
});
@@ -137,7 +143,7 @@ router.post('/login', async (req, res) => {
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
message: '用户名和密码不能为空',
});
}
@@ -145,21 +151,21 @@ router.post('/login', async (req, res) => {
if (!user) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
message: '用户名或密码错误',
});
}
if (user.status === 'locked') {
return res.status(403).json({
success: false,
message: '账户已被锁定,请联系管理员'
message: '账户已被锁定,请联系管理员',
});
}
if (user.status === 'inactive') {
return res.status(403).json({
success: false,
message: '账户已禁用'
message: '账户已禁用',
});
}
@@ -167,7 +173,7 @@ router.post('/login', async (req, res) => {
return res.status(403).json({
success: false,
code: 'PENDING_APPROVAL',
message: '账户待审核,请联系管理员激活'
message: '账户待审核,请联系管理员激活',
});
}
@@ -178,10 +184,10 @@ router.post('/login', async (req, res) => {
user.status = 'locked';
}
await user.save();
return res.status(401).json({
success: false,
message: '用户名或密码错误'
message: '用户名或密码错误',
});
}
@@ -201,17 +207,17 @@ router.post('/login', async (req, res) => {
username: user.username,
email: user.email,
realName: user.realName,
avatar: user.avatar
avatar: user.avatar,
},
token
}
token,
},
});
} catch (error) {
console.error('登录错误:', error);
res.status(500).json({
success: false,
message: '登录失败',
error: error.message
error: error.message,
});
}
});
@@ -219,22 +225,24 @@ router.post('/login', async (req, res) => {
router.get('/profile', authMiddleware, async (req, res) => {
try {
const user = await User.findByPk(req.user.userId, {
attributes: { exclude: ['password'] }
attributes: { exclude: ['password'] },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
const roles = await Role.findAll({
include: [{
model: User,
where: { userId: req.user.userId },
attributes: []
}]
include: [
{
model: User,
where: { userId: req.user.userId },
attributes: [],
},
],
});
res.json({
@@ -244,15 +252,15 @@ router.get('/profile', authMiddleware, async (req, res) => {
roles: roles.map(r => ({
roleId: r.roleId,
roleName: r.roleName,
roleCode: r.roleCode
}))
}
roleCode: r.roleCode,
})),
},
});
} catch (error) {
console.error('获取profile错误:', error);
res.status(500).json({
success: false,
message: '获取用户信息失败'
message: '获取用户信息失败',
});
}
});
@@ -265,14 +273,22 @@ router.put('/profile', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (email !== undefined) user.email = email;
if (phone !== undefined) user.phone = phone;
if (realName !== undefined) user.realName = realName;
if (avatar !== undefined) user.avatar = avatar;
if (email !== undefined) {
user.email = email;
}
if (phone !== undefined) {
user.phone = phone;
}
if (realName !== undefined) {
user.realName = realName;
}
if (avatar !== undefined) {
user.avatar = avatar;
}
await user.save();
@@ -285,14 +301,14 @@ router.put('/profile', authMiddleware, async (req, res) => {
email: user.email,
phone: user.phone,
realName: user.realName,
avatar: user.avatar
}
avatar: user.avatar,
},
});
} catch (error) {
console.error('更新profile错误:', error);
res.status(500).json({
success: false,
message: '更新失败'
message: '更新失败',
});
}
});
@@ -304,14 +320,14 @@ router.put('/password', authMiddleware, async (req, res) => {
if (!oldPassword || !newPassword) {
return res.status(400).json({
success: false,
message: '旧密码和新密码都不能为空'
message: '旧密码和新密码都不能为空',
});
}
if (newPassword.length < PASSWORD_MIN_LENGTH) {
return res.status(400).json({
success: false,
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
});
}
@@ -321,7 +337,7 @@ router.put('/password', authMiddleware, async (req, res) => {
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '旧密码错误'
message: '旧密码错误',
});
}
@@ -330,13 +346,13 @@ router.put('/password', authMiddleware, async (req, res) => {
res.json({
success: true,
message: '密码修改成功'
message: '密码修改成功',
});
} catch (error) {
console.error('修改密码错误:', error);
res.status(500).json({
success: false,
message: '密码修改失败'
message: '密码修改失败',
});
}
});
@@ -344,19 +360,19 @@ router.put('/password', authMiddleware, async (req, res) => {
router.post('/check-admin', async (req, res) => {
try {
const userCount = await User.count();
res.json({
success: true,
data: {
hasAdmin: userCount > 0,
userCount
}
userCount,
},
});
} catch (error) {
console.error('检查管理员错误:', error);
res.status(500).json({
success: false,
message: '检查失败'
message: '检查失败',
});
}
});
@@ -368,7 +384,7 @@ router.post('/unlock', async (req, res) => {
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
message: '用户名和密码不能为空',
});
}
@@ -376,14 +392,14 @@ router.post('/unlock', async (req, res) => {
if (!user) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
message: '用户名或密码错误',
});
}
if (user.status !== 'locked') {
return res.status(400).json({
success: false,
message: '账户未被锁定'
message: '账户未被锁定',
});
}
@@ -391,7 +407,7 @@ router.post('/unlock', async (req, res) => {
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
message: '用户名或密码错误',
});
}
@@ -402,14 +418,14 @@ router.post('/unlock', async (req, res) => {
res.json({
success: true,
message: '账户解锁成功'
message: '账户解锁成功',
});
} catch (error) {
console.error('解锁账户错误:', error);
res.status(500).json({
success: false,
message: '解锁失败',
error: error.message
error: error.message,
});
}
});
+16 -9
View File
@@ -10,11 +10,18 @@ if (!fs.existsSync(UPLOAD_DIR)) {
const SETTINGS_FILE = path.join(__dirname, '../backgroundSettings.json');
if (!fs.existsSync(SETTINGS_FILE)) {
fs.writeFileSync(SETTINGS_FILE, JSON.stringify({
type: 'gradient',
image: '',
size: 'contain'
}, null, 2));
fs.writeFileSync(
SETTINGS_FILE,
JSON.stringify(
{
type: 'gradient',
image: '',
size: 'contain',
},
null,
2
)
);
}
router.get('/', (req, res) => {
@@ -22,7 +29,7 @@ router.get('/', (req, res) => {
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
res.json({
success: true,
data: settings
data: settings,
});
} catch (error) {
console.error('读取背景设置失败:', error);
@@ -36,7 +43,7 @@ router.put('/', (req, res) => {
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
res.json({
success: true,
data: settings
data: settings,
});
} catch (error) {
console.error('保存背景设置失败:', error);
@@ -54,7 +61,7 @@ router.post('/upload', (req, res) => {
const fileName = `${Date.now()}_${file.name}`;
const filePath = path.join(UPLOAD_DIR, fileName);
file.mv(filePath, (err) => {
file.mv(filePath, err => {
if (err) {
console.error('文件保存失败:', err);
return res.status(500).json({ error: '文件保存失败' });
@@ -90,4 +97,4 @@ router.post('/settings', (req, res) => {
}
});
module.exports = router;
module.exports = router;
+102 -75
View File
@@ -23,11 +23,7 @@ const {
updateAutoBackupSettings,
executeBackupNow,
} = require('../utils/autoBackupScheduler');
const {
getBackupLogs,
getBackupLogById,
deleteOldLogs,
} = require('../utils/backupLog');
const { getBackupLogs, getBackupLogById, deleteOldLogs } = require('../utils/backupLog');
const {
getAllTargets,
getTarget,
@@ -81,22 +77,27 @@ router.get('/list', async (req, res) => {
});
}
const files = fs.readdirSync(backupPath)
.filter(f => (f.startsWith('backup_') || f.startsWith('uploaded_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
const files = fs
.readdirSync(backupPath)
.filter(
f =>
(f.startsWith('backup_') || f.startsWith('uploaded_')) &&
(f.endsWith('.json') || f.endsWith('.json.gz'))
)
.map(async f => {
const filePath = path.join(backupPath, f);
const stats = fs.statSync(filePath);
const isCompressed = f.endsWith('.gz');
// 尝试从文件内容中提取元数据
let metadata = {
const metadata = {
filename: f,
size: stats.size,
compressed: isCompressed,
createdAt: stats.birthtime,
modifiedAt: stats.mtime,
};
try {
// 读取文件头部的元数据信息
let content;
@@ -106,9 +107,9 @@ router.get('/list', async (req, res) => {
} else {
content = fs.readFileSync(filePath, 'utf8');
}
const backupData = JSON.parse(content);
// 提取关键元数据
metadata.description = backupData.description || '';
metadata.backupType = backupData.backupType || 'full';
@@ -117,19 +118,18 @@ router.get('/list', async (req, res) => {
metadata.checksum = backupData.checksum;
metadata.metadata = backupData.metadata;
metadata.systemInfo = backupData.systemInfo;
// 判断是否为上传的文件(通过文件名判断)
metadata.isUploaded = f.startsWith('uploaded_');
} catch (error) {
// 如果读取失败,标记为无效文件
metadata.invalid = true;
metadata.error = '无法读取文件内容';
}
return metadata;
});
const resolvedFiles = await Promise.all(files);
res.json({
@@ -201,7 +201,7 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
const sendProgress = (data) => {
const sendProgress = data => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
@@ -211,16 +211,25 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
const validation = await validateBackupFile(filePath);
if (!validation.valid) {
sendProgress({ stage: 'error', message: `备份文件验证失败: ${validation.error}`, progress: 0 });
sendProgress({
stage: 'error',
message: `备份文件验证失败: ${validation.error}`,
progress: 0,
});
res.end();
return;
}
sendProgress({ stage: 'validate', message: '备份文件验证通过', progress: 10, metadata: validation.metadata });
sendProgress({
stage: 'validate',
message: '备份文件验证通过',
progress: 10,
metadata: validation.metadata,
});
const buffer = fs.readFileSync(filePath);
const isCompressed = filePath.endsWith('.gz');
let backupData;
if (isCompressed) {
sendProgress({ stage: 'decompress', message: '正在解压备份文件...', progress: 15 });
@@ -243,10 +252,10 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
processedTables++;
const progress = 20 + Math.floor((processedTables / totalTables) * 70);
const statusMap = {
'restored': '已恢复',
'skipped': '已跳过',
'empty': '无数据',
'error': '错误',
restored: '已恢复',
skipped: '已跳过',
empty: '无数据',
error: '错误',
};
sendProgress({
stage: 'restore',
@@ -261,9 +270,9 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
},
});
sendProgress({
stage: 'complete',
message: '恢复完成!',
sendProgress({
stage: 'complete',
message: '恢复完成!',
progress: 100,
result: {
tablesRestored: result.tablesRestored,
@@ -272,7 +281,7 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
restoredAt: result.restoredAt,
tableDetails: result.tableDetails,
fileDetails: result.fileDetails,
}
},
});
res.end();
@@ -342,7 +351,7 @@ router.post('/upload', async (req, res) => {
const backupFile = req.files.backup;
const originalName = backupFile.name || '';
const nameLower = originalName.toLowerCase();
// 验证文件类型
if (!nameLower.endsWith('.json') && !nameLower.endsWith('.gz')) {
return res.status(400).json({
@@ -350,15 +359,15 @@ router.post('/upload', async (req, res) => {
message: '只支持 JSON 或 GZ 格式的备份文件',
});
}
const isCompressed = nameLower.endsWith('.gz');
console.log(`上传备份文件: ${originalName}, 压缩: ${isCompressed}, 大小: ${backupFile.size}`);
// 保存到临时文件
const tempFilename = `upload_${Date.now()}`;
const tempPath = path.join(tempDir, tempFilename);
await backupFile.mv(tempPath);
const validation = await validateBackupFile(tempPath, { isCompressed });
@@ -411,7 +420,7 @@ router.get('/download/:filename', (req, res) => {
});
}
res.download(filePath, filename, (err) => {
res.download(filePath, filename, err => {
if (err) {
console.error('下载备份文件失败:', err);
}
@@ -463,7 +472,9 @@ router.get('/info', (req, res) => {
let backupCount = 0;
if (fs.existsSync(backupPath)) {
const files = fs.readdirSync(backupPath).filter(f => f.endsWith('.json') || f.endsWith('.json.gz'));
const files = fs
.readdirSync(backupPath)
.filter(f => f.endsWith('.json') || f.endsWith('.json.gz'));
backupCount = files.length;
files.forEach(f => {
const stats = fs.statSync(path.join(backupPath, f));
@@ -491,7 +502,9 @@ router.get('/info', (req, res) => {
});
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
if (bytes === 0) {
return '0 B';
}
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
@@ -565,7 +578,9 @@ router.post('/auto/settings', (req, res) => {
} = req.body;
const newSettings = {};
if (enabled !== undefined) newSettings.enabled = enabled;
if (enabled !== undefined) {
newSettings.enabled = enabled;
}
if (hour !== undefined || minute !== undefined) {
newSettings.hour = hour || 2;
newSettings.minute = minute || 0;
@@ -579,12 +594,24 @@ router.post('/auto/settings', (req, res) => {
}
newSettings.cronExpression = cronExpression;
}
if (description) newSettings.description = description;
if (includeFiles !== undefined) newSettings.includeFiles = includeFiles;
if (compress !== undefined) newSettings.compress = compress;
if (maxCount !== undefined) newSettings.maxCount = maxCount;
if (maxAgeDays !== undefined) newSettings.maxAgeDays = maxAgeDays;
if (backupType !== undefined) newSettings.backupType = backupType;
if (description) {
newSettings.description = description;
}
if (includeFiles !== undefined) {
newSettings.includeFiles = includeFiles;
}
if (compress !== undefined) {
newSettings.compress = compress;
}
if (maxCount !== undefined) {
newSettings.maxCount = maxCount;
}
if (maxAgeDays !== undefined) {
newSettings.maxAgeDays = maxAgeDays;
}
if (backupType !== undefined) {
newSettings.backupType = backupType;
}
const success = updateAutoBackupSettings(newSettings);
if (success) {
@@ -614,7 +641,7 @@ router.post('/auto/settings', (req, res) => {
router.post('/auto/execute', async (req, res) => {
try {
const { description, includeFiles, compress, backupType } = req.body;
const result = await executeBackupNow({
description: description || '手动触发备份',
includeFiles,
@@ -648,7 +675,7 @@ router.post('/auto/execute', async (req, res) => {
router.post('/auto/test-cron', (req, res) => {
try {
const { cronExpression } = req.body;
if (!cronExpression) {
return res.status(400).json({
success: false,
@@ -657,7 +684,7 @@ router.post('/auto/test-cron', (req, res) => {
}
const isValid = validateCronExpression(cronExpression);
res.json({
success: isValid,
message: isValid ? 'Cron 表达式有效' : 'Cron 表达式无效',
@@ -700,14 +727,14 @@ router.get('/remote/targets', (req, res) => {
router.get('/remote/targets/:id', (req, res) => {
try {
const target = getTarget(req.params.id);
if (!target) {
return res.status(404).json({
success: false,
message: '目标不存在',
});
}
res.json({
success: true,
data: { target },
@@ -726,16 +753,16 @@ router.get('/remote/targets/:id', (req, res) => {
router.post('/remote/targets', (req, res) => {
try {
const targetData = req.body;
if (!targetData.name || !targetData.protocol) {
return res.status(400).json({
success: false,
message: '请提供目标名称和协议类型',
});
}
const target = addTarget(targetData);
res.status(201).json({
success: true,
message: '远端备份目标已添加',
@@ -756,7 +783,7 @@ router.put('/remote/targets/:id', (req, res) => {
try {
const updates = req.body;
const target = updateTarget(req.params.id, updates);
res.json({
success: true,
message: '远端备份目标已更新',
@@ -776,14 +803,14 @@ router.put('/remote/targets/:id', (req, res) => {
router.delete('/remote/targets/:id', (req, res) => {
try {
const deleted = deleteTarget(req.params.id);
if (!deleted) {
return res.status(404).json({
success: false,
message: '目标不存在',
});
}
res.json({
success: true,
message: '远端备份目标已删除',
@@ -818,7 +845,7 @@ router.post('/remote/test', async (req, res) => {
}
const result = await testRemoteConnection(config);
if (result.success) {
res.json({
success: true,
@@ -846,16 +873,16 @@ router.post('/remote/test', async (req, res) => {
router.post('/remote/targets/:id/test', async (req, res) => {
try {
const target = getTarget(req.params.id);
if (!target) {
return res.status(404).json({
success: false,
message: '目标不存在',
});
}
const result = await testRemoteConnection(target);
if (result.success) {
res.json({
success: true,
@@ -924,7 +951,7 @@ router.get('/remote/protocols', (req, res) => {
value,
label: PROTOCOL_LABELS[value],
}));
res.json({
success: true,
data: { protocols },
@@ -943,45 +970,45 @@ router.get('/remote/protocols', (req, res) => {
router.post('/remote/upload', async (req, res) => {
try {
const { filename, targetIds } = req.body;
if (!filename) {
return res.status(400).json({
success: false,
message: '请提供备份文件名',
});
}
const backupPath = getBackupPath();
const filePath = path.join(backupPath, filename);
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: '备份文件不存在',
});
}
const { uploadToRemote } = require('../utils/remoteBackup');
const { getTarget } = require('../utils/remoteBackupConfig');
const targets = targetIds
const targets = targetIds
? targetIds.map(id => getTarget(id)).filter(Boolean)
: getEnabledTargets();
if (targets.length === 0) {
return res.status(400).json({
success: false,
message: '没有可用的远端目标',
});
}
const uploadResults = [];
for (const target of targets) {
try {
const remotePath = `${target.prefix || 'backups/'}${filename}`;
const result = await uploadToRemote(target, filePath, remotePath);
uploadResults.push({
targetId: target.id,
targetName: target.name,
@@ -997,11 +1024,11 @@ router.post('/remote/upload', async (req, res) => {
});
}
}
res.json({
success: true,
message: '上传完成',
data: {
data: {
filename,
results: uploadResults,
},
@@ -1022,14 +1049,14 @@ router.post('/remote/upload', async (req, res) => {
router.get('/logs', async (req, res) => {
try {
const { page = 1, pageSize = 20, logType, status } = req.query;
const result = await getBackupLogs({
page: parseInt(page),
pageSize: parseInt(pageSize),
logType,
status,
});
res.json({
success: true,
data: result,
@@ -1049,14 +1076,14 @@ router.get('/logs/:id', async (req, res) => {
try {
const { id } = req.params;
const log = await getBackupLogById(parseInt(id));
if (!log) {
return res.status(404).json({
success: false,
message: '备份日志不存在',
});
}
res.json({
success: true,
data: log,
@@ -1076,7 +1103,7 @@ router.delete('/logs/clean', async (req, res) => {
try {
const { days = 30 } = req.body;
const deletedCount = await deleteOldLogs(parseInt(days));
res.json({
success: true,
message: '清理完成',
+141 -119
View File
@@ -8,10 +8,7 @@ const DevicePort = require('../models/DevicePort');
// 辅助函数:更新端口状态
async function updatePortStatus(deviceId, portName, status) {
try {
await DevicePort.update(
{ status },
{ where: { deviceId, portName } }
);
await DevicePort.update({ status }, { where: { deviceId, portName } });
} catch (error) {
console.error(`更新端口状态失败: ${deviceId}:${portName} -> ${status}`, error);
}
@@ -29,51 +26,58 @@ async function freePort(deviceId, portName) {
router.get('/', async (req, res) => {
try {
const { sourceDeviceId, targetDeviceId, status, cableType, page = 1, pageSize = 10 } = req.query;
const {
sourceDeviceId,
targetDeviceId,
status,
cableType,
page = 1,
pageSize = 10,
} = req.query;
const offset = (page - 1) * pageSize;
const where = {};
if (sourceDeviceId) {
where.sourceDeviceId = sourceDeviceId;
}
if (targetDeviceId) {
where.targetDeviceId = targetDeviceId;
}
if (status && status !== 'all') {
where.status = status;
}
if (cableType && cableType !== 'all') {
where.cableType = cableType;
}
const { count, rows } = await Cable.findAndCountAll({
where,
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
offset,
limit: parseInt(pageSize),
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
res.json({
total: count,
cables: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取接线列表失败:', error);
@@ -84,28 +88,25 @@ router.get('/', async (req, res) => {
router.get('/device/:deviceId', async (req, res) => {
try {
const { deviceId } = req.params;
const cables = await Cable.findAll({
where: {
[Op.or]: [
{ sourceDeviceId: deviceId },
{ targetDeviceId: deviceId }
]
[Op.or]: [{ sourceDeviceId: deviceId }, { targetDeviceId: deviceId }],
},
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
res.json(cables);
} catch (error) {
console.error('获取设备接线失败:', error);
@@ -117,15 +118,15 @@ router.get('/device/:deviceId', async (req, res) => {
router.get('/rack/:rackId', async (req, res) => {
try {
const { rackId } = req.params;
// 1. 找出该机柜下的所有设备ID
const devices = await Device.findAll({
where: { rackId: rackId },
attributes: ['deviceId']
attributes: ['deviceId'],
});
const deviceIds = devices.map(d => d.deviceId);
if (deviceIds.length === 0) {
return res.json([]);
}
@@ -135,23 +136,23 @@ router.get('/rack/:rackId', async (req, res) => {
where: {
[Op.or]: [
{ sourceDeviceId: { [Op.in]: deviceIds } },
{ targetDeviceId: { [Op.in]: deviceIds } }
]
{ targetDeviceId: { [Op.in]: deviceIds } },
],
},
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
res.json(cables);
} catch (error) {
console.error('获取机柜接线失败:', error);
@@ -175,22 +176,22 @@ router.post('/check-conflict', async (req, res) => {
where: {
[Op.or]: [
{ sourceDeviceId, sourcePort },
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort }
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort },
],
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } })
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } }),
},
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type']
attributes: ['deviceId', 'name', 'type'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type']
}
]
attributes: ['deviceId', 'name', 'type'],
},
],
});
if (sourceConflict) {
@@ -198,7 +199,7 @@ router.post('/check-conflict', async (req, res) => {
type: 'source',
port: sourcePort,
deviceId: sourceDeviceId,
existingCable: sourceConflict
existingCable: sourceConflict,
});
}
@@ -207,22 +208,22 @@ router.post('/check-conflict', async (req, res) => {
where: {
[Op.or]: [
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
{ targetDeviceId, targetPort }
{ targetDeviceId, targetPort },
],
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } })
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } }),
},
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type']
attributes: ['deviceId', 'name', 'type'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type']
}
]
attributes: ['deviceId', 'name', 'type'],
},
],
});
if (targetConflict) {
@@ -230,13 +231,13 @@ router.post('/check-conflict', async (req, res) => {
type: 'target',
port: targetPort,
deviceId: targetDeviceId,
existingCable: targetConflict
existingCable: targetConflict,
});
}
res.json({
hasConflict: conflicts.length > 0,
conflicts
conflicts,
});
} catch (error) {
console.error('检查接线冲突失败:', error);
@@ -246,7 +247,18 @@ router.post('/check-conflict', async (req, res) => {
router.post('/', async (req, res) => {
try {
const { cableId, sourceDeviceId, sourcePort, targetDeviceId, targetPort, cableType, cableLength, status, description, force } = req.body;
const {
cableId,
sourceDeviceId,
sourcePort,
targetDeviceId,
targetPort,
cableType,
cableLength,
status,
description,
force,
} = req.body;
if (!sourceDeviceId || !sourcePort || !targetDeviceId || !targetPort) {
return res.status(400).json({ error: '缺少必填字段' });
@@ -262,16 +274,16 @@ router.post('/', async (req, res) => {
where: {
[Op.or]: [
{ sourceDeviceId, sourcePort },
{ targetDeviceId, targetPort }
]
}
{ targetDeviceId, targetPort },
],
},
});
if (existingCable) {
return res.status(409).json({
error: '端口已被占用',
conflict: true,
existingCable
existingCable,
});
}
}
@@ -284,9 +296,9 @@ router.post('/', async (req, res) => {
{ sourceDeviceId, sourcePort },
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort },
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
{ targetDeviceId, targetPort }
]
}
{ targetDeviceId, targetPort },
],
},
});
for (const cable of existingCables) {
@@ -308,7 +320,7 @@ router.post('/', async (req, res) => {
cableType: cableType || 'ethernet',
cableLength,
status: status || 'normal',
description
description,
});
const createdCable = await Cable.findByPk(cable.cableId, {
@@ -316,14 +328,14 @@ router.post('/', async (req, res) => {
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
// 自动将源端口和目标端口状态设为occupied
@@ -340,44 +352,49 @@ router.post('/', async (req, res) => {
router.post('/batch', async (req, res) => {
try {
const { cables } = req.body;
if (!cables || !Array.isArray(cables) || cables.length === 0) {
return res.status(400).json({ error: '请提供有效的接线数据' });
}
const results = {
total: cables.length,
success: 0,
failed: 0,
errors: []
errors: [],
};
for (let i = 0; i < cables.length; i++) {
const cableData = cables[i];
try {
if (!cableData.cableId || !cableData.sourceDeviceId || !cableData.sourcePort ||
!cableData.targetDeviceId || !cableData.targetPort) {
if (
!cableData.cableId ||
!cableData.sourceDeviceId ||
!cableData.sourcePort ||
!cableData.targetDeviceId ||
!cableData.targetPort
) {
throw new Error('缺少必填字段');
}
if (cableData.sourceDeviceId === cableData.targetDeviceId) {
throw new Error('源设备和目标设备不能相同');
}
const existingCable = await Cable.findOne({
where: {
[Op.or]: [
{ sourceDeviceId: cableData.sourceDeviceId, sourcePort: cableData.sourcePort },
{ targetDeviceId: cableData.targetDeviceId, targetPort: cableData.targetPort }
]
}
{ targetDeviceId: cableData.targetDeviceId, targetPort: cableData.targetPort },
],
},
});
if (existingCable) {
throw new Error('端口已被占用');
}
await Cable.create({
cableId: cableData.cableId,
sourceDeviceId: cableData.sourceDeviceId,
@@ -387,24 +404,24 @@ router.post('/batch', async (req, res) => {
cableType: cableData.cableType || 'ethernet',
cableLength: cableData.cableLength,
status: cableData.status || 'normal',
description: cableData.description
description: cableData.description,
});
// 自动将源端口和目标端口状态设为occupied
await occupyPort(cableData.sourceDeviceId, cableData.sourcePort);
await occupyPort(cableData.targetDeviceId, cableData.targetPort);
results.success++;
} catch (error) {
results.failed++;
results.errors.push({
index: i + 1,
cableId: cableData.cableId,
error: error.message
error: error.message,
});
}
}
res.json(results);
} catch (error) {
console.error('批量创建接线失败:', error);
@@ -416,48 +433,53 @@ router.put('/:cableId', async (req, res) => {
try {
// 获取更新前的接线信息
const oldCable = await Cable.findByPk(req.params.cableId);
if (!oldCable) {
return res.status(404).json({ error: '接线不存在' });
}
const { sourceDeviceId: oldSourceDeviceId, sourcePort: oldSourcePort, targetDeviceId: oldTargetDeviceId, targetPort: oldTargetPort } = oldCable;
const {
sourceDeviceId: oldSourceDeviceId,
sourcePort: oldSourcePort,
targetDeviceId: oldTargetDeviceId,
targetPort: oldTargetPort,
} = oldCable;
const [updated] = await Cable.update(req.body, {
where: { cableId: req.params.cableId }
where: { cableId: req.params.cableId },
});
if (updated) {
const cable = await Cable.findByPk(req.params.cableId, {
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
// 同步更新端口状态
// 源端口变更:释放旧端口,占用新端口
if (oldSourceDeviceId !== sourceDeviceId || oldSourcePort !== sourcePort) {
await freePort(oldSourceDeviceId, oldSourcePort);
await occupyPort(sourceDeviceId, sourcePort);
}
// 目标端口变更:释放旧端口,占用新端口
if (oldTargetDeviceId !== targetDeviceId || oldTargetPort !== targetPort) {
await freePort(oldTargetDeviceId, oldTargetPort);
await occupyPort(targetDeviceId, targetPort);
}
res.json(cable);
} else {
res.status(404).json({ error: '接线不存在' });
@@ -472,22 +494,22 @@ router.delete('/:cableId', async (req, res) => {
try {
// 先获取接线信息,用于后续恢复端口状态
const cable = await Cable.findByPk(req.params.cableId);
if (!cable) {
return res.status(404).json({ error: '接线不存在' });
}
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
const deleted = await Cable.destroy({
where: { cableId: req.params.cableId }
where: { cableId: req.params.cableId },
});
if (deleted) {
// 自动将源端口和目标端口状态恢复为free
await freePort(sourceDeviceId, sourcePort);
await freePort(targetDeviceId, targetPort);
res.status(204).json();
} else {
res.status(404).json({ error: '接线不存在' });
@@ -501,29 +523,29 @@ router.delete('/:cableId', async (req, res) => {
router.delete('/batch', async (req, res) => {
try {
const { cableIds } = req.body;
if (!cableIds || !Array.isArray(cableIds) || cableIds.length === 0) {
return res.status(400).json({ error: '请提供有效的接线ID列表' });
}
// 先获取所有要删除的接线信息,用于后续恢复端口状态
const cables = await Cable.findAll({
where: { cableId: { [Op.in]: cableIds } }
where: { cableId: { [Op.in]: cableIds } },
});
const deletedCount = await Cable.destroy({
where: { cableId: { [Op.in]: cableIds } }
where: { cableId: { [Op.in]: cableIds } },
});
// 自动将所有相关端口状态恢复为free
for (const cable of cables) {
await freePort(cable.sourceDeviceId, cable.sourcePort);
await freePort(cable.targetDeviceId, cable.targetPort);
}
res.json({
message: `批量删除成功,已删除 ${deletedCount} 条接线`,
deletedCount
deletedCount,
});
} catch (error) {
console.error('批量删除接线失败:', error);
@@ -538,20 +560,20 @@ router.get('/:cableId', async (req, res) => {
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
if (!cable) {
return res.status(404).json({ error: '接线不存在' });
}
res.json(cable);
} catch (error) {
console.error('获取接线详情失败:', error);
+13 -7
View File
@@ -13,7 +13,7 @@ router.get('/', async (req, res) => {
if (keyword) {
where[Op.or] = [
{ name: { [Op.like]: `%${keyword}%` } },
{ description: { [Op.like]: `%${keyword}%` } }
{ description: { [Op.like]: `%${keyword}%` } },
];
}
@@ -23,9 +23,12 @@ router.get('/', async (req, res) => {
const { count, rows } = await ConsumableCategory.findAndCountAll({
where,
order: [['sortOrder', 'ASC'], ['id', 'DESC']],
order: [
['sortOrder', 'ASC'],
['id', 'DESC'],
],
offset,
limit: parseInt(pageSize)
limit: parseInt(pageSize),
});
res.json({
@@ -33,7 +36,7 @@ router.get('/', async (req, res) => {
total: count,
currentPage: parseInt(page),
pageSize: parseInt(pageSize),
totalPages: Math.ceil(count / pageSize)
totalPages: Math.ceil(count / pageSize),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -44,7 +47,10 @@ router.get('/list', async (req, res) => {
try {
const categories = await ConsumableCategory.findAll({
where: { status: 'active' },
order: [['sortOrder', 'ASC'], ['name', 'ASC']]
order: [
['sortOrder', 'ASC'],
['name', 'ASC'],
],
});
res.json(categories);
} catch (error) {
@@ -77,7 +83,7 @@ router.post('/', async (req, res) => {
name,
description,
sortOrder: sortOrder || 0,
status: status || 'active'
status: status || 'active',
});
res.status(201).json(category);
@@ -106,7 +112,7 @@ router.put('/:id', async (req, res) => {
name: name || category.name,
description: description !== undefined ? description : category.description,
sortOrder: sortOrder !== undefined ? sortOrder : category.sortOrder,
status: status || category.status
status: status || category.status,
});
res.json(category);
+63 -59
View File
@@ -10,42 +10,40 @@ router.get('/', async (req, res) => {
try {
const { consumableId, type, startDate, endDate, page = 1, pageSize = 10 } = req.query;
const offset = (page - 1) * pageSize;
const where = {};
if (consumableId) {
where.consumableId = consumableId;
}
if (type && type !== 'all') {
where.type = type;
}
if (startDate && endDate) {
where.createdAt = {
[Op.between]: [new Date(startDate), new Date(endDate)]
[Op.between]: [new Date(startDate), new Date(endDate)],
};
} else if (startDate) {
where.createdAt = { [Op.gte]: new Date(startDate) };
} else if (endDate) {
where.createdAt = { [Op.lte]: new Date(endDate) };
}
const { count, rows } = await ConsumableRecord.findAndCountAll({
where,
include: [
{ model: Consumable, as: 'consumable', attributes: ['name', 'category', 'unit'] }
],
include: [{ model: Consumable, as: 'consumable', attributes: ['name', 'category', 'unit'] }],
offset,
limit: parseInt(pageSize),
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
res.json({
total: count,
records: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -54,19 +52,19 @@ router.get('/', async (req, res) => {
router.post('/', async (req, res) => {
const transaction = await sequelize.transaction();
try {
const { consumableId, type, quantity, operator, reason, recipient, notes } = req.body;
const consumable = await Consumable.findByPk(consumableId);
if (!consumable) {
await transaction.rollback();
return res.status(404).json({ error: '耗材不存在' });
}
const previousStock = consumable.currentStock;
let newStock;
if (type === 'in') {
newStock = previousStock + quantity;
} else if (type === 'out') {
@@ -79,41 +77,47 @@ router.post('/', async (req, res) => {
await transaction.rollback();
return res.status(400).json({ error: '操作类型无效' });
}
await consumable.update({ currentStock: newStock }, { transaction });
const record = await ConsumableRecord.create({
consumableId,
type,
quantity,
previousStock,
currentStock: newStock,
operator,
reason,
recipient,
notes
}, { transaction });
await ConsumableLog.create({
consumableId,
consumableName: consumable.name,
operationType: type,
quantity: type === 'in' ? parseFloat(quantity) : -parseFloat(quantity),
previousStock,
currentStock: newStock,
operator,
reason,
notes
}, { transaction });
await consumable.update({ currentStock: newStock }, { transaction });
const record = await ConsumableRecord.create(
{
consumableId,
type,
quantity,
previousStock,
currentStock: newStock,
operator,
reason,
recipient,
notes,
},
{ transaction }
);
await ConsumableLog.create(
{
consumableId,
consumableName: consumable.name,
operationType: type,
quantity: type === 'in' ? parseFloat(quantity) : -parseFloat(quantity),
previousStock,
currentStock: newStock,
operator,
reason,
notes,
},
{ transaction }
);
await transaction.commit();
res.status(201).json({
record,
consumable: {
previousStock,
currentStock: newStock
}
currentStock: newStock,
},
});
} catch (error) {
await transaction.rollback();
@@ -131,10 +135,10 @@ router.get('/statistics', async (req, res) => {
const startDateTime = new Date(startDate);
const endDateTime = new Date(endDate);
endDateTime.setHours(23, 59, 59, 999); // 设置 endDate 为当天最后一刻
dateWhere.createdAt = {
[Op.gte]: startDateTime,
[Op.lte]: endDateTime
[Op.lte]: endDateTime,
};
}
@@ -146,14 +150,14 @@ router.get('/statistics', async (req, res) => {
const records = await ConsumableRecord.findAll({
where: dateWhere,
include: [
{
model: Consumable,
{
model: Consumable,
as: 'consumable',
attributes: ['name', 'category'],
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined
}
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined,
},
],
attributes: ['type', 'quantity']
attributes: ['type', 'quantity'],
});
let inCount = 0;
@@ -182,15 +186,15 @@ router.get('/statistics', async (req, res) => {
const recentRecords = await ConsumableRecord.findAll({
where: dateWhere,
include: [
{
model: Consumable,
{
model: Consumable,
as: 'consumable',
attributes: ['name', 'category'],
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined
}
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined,
},
],
order: [['createdAt', 'DESC']],
limit: 10
limit: 10,
});
res.json({
@@ -202,7 +206,7 @@ router.get('/statistics', async (req, res) => {
byType: Object.entries(typeMap).map(([type, data]) => ({
type,
totalQuantity: data.totalQuantity,
count: data.count
count: data.count,
})),
recentRecords: recentRecords.map(record => ({
recordId: record.recordId,
@@ -213,8 +217,8 @@ router.get('/statistics', async (req, res) => {
consumableId: record.consumableId,
consumableName: record.consumable?.name || '未知耗材',
category: record.consumable?.category || null,
unit: record.consumable?.unit || '个'
}))
unit: record.consumable?.unit || '个',
})),
});
} catch (error) {
res.status(500).json({ error: error.message });
File diff suppressed because it is too large Load Diff
+35 -15
View File
@@ -1,6 +1,13 @@
const express = require('express');
const router = express.Router();
const { logDangerousOperation, getDangerousOperationsLogs, cleanOldLogs, DANGEROUS_OPERATION_TYPES, RISK_LEVELS, calculateRiskLevel } = require('../utils/dangerousOperationLogger');
const {
logDangerousOperation,
getDangerousOperationsLogs,
cleanOldLogs,
DANGEROUS_OPERATION_TYPES,
RISK_LEVELS,
calculateRiskLevel,
} = require('../utils/dangerousOperationLogger');
router.post('/log', async (req, res) => {
try {
@@ -20,10 +27,12 @@ router.post('/log', async (req, res) => {
return res.status(400).json({ error: '缺少必需参数 operationType 或 operationName' });
}
const riskLevel = metadata.riskLevel || calculateRiskLevel(operationType, metadata.itemCount || 1, {
hasRelatedData: metadata.relatedDataCount > 0,
isSystemLevel: metadata.isSystemLevel,
});
const riskLevel =
metadata.riskLevel ||
calculateRiskLevel(operationType, metadata.itemCount || 1, {
hasRelatedData: metadata.relatedDataCount > 0,
isSystemLevel: metadata.isSystemLevel,
});
await logDangerousOperation(req, {
operationType,
@@ -49,7 +58,17 @@ router.post('/log', async (req, res) => {
router.get('/logs', async (req, res) => {
try {
const { operationType, targetType, success, startDate, endDate, username, riskLevel, page = 1, pageSize = 50 } = req.query;
const {
operationType,
targetType,
success,
startDate,
endDate,
username,
riskLevel,
page = 1,
pageSize = 50,
} = req.query;
const filters = {
operationType,
@@ -121,14 +140,10 @@ 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 riskLevel = calculateRiskLevel(operationType, parseInt(itemCount) || 1, {
hasRelatedData: hasRelatedData === 'true',
isSystemLevel: isSystemLevel === 'true',
});
const riskDescriptions = {
[RISK_LEVELS.EXTREME]: '极高风险操作,需要输入确认关键词才能执行',
@@ -141,7 +156,12 @@ router.get('/risk-assessment', async (req, res) => {
riskLevel,
description: riskDescriptions[riskLevel],
requiresKeyword: riskLevel === RISK_LEVELS.EXTREME,
confirmationLevel: riskLevel === RISK_LEVELS.EXTREME ? 'KEYWORD' : riskLevel === RISK_LEVELS.HIGH ? 'ENHANCED' : 'STANDARD',
confirmationLevel:
riskLevel === RISK_LEVELS.EXTREME
? 'KEYWORD'
: riskLevel === RISK_LEVELS.HIGH
? 'ENHANCED'
: 'STANDARD',
});
} catch (error) {
console.error('Failed to assess risk:', error);
+13 -13
View File
@@ -6,7 +6,7 @@ const DeviceField = require('../models/DeviceField');
router.get('/', async (req, res) => {
try {
const fields = await DeviceField.findAll({
order: [['order', 'ASC']]
order: [['order', 'ASC']],
});
res.json(fields);
} catch (error) {
@@ -41,7 +41,7 @@ router.post('/', async (req, res) => {
router.put('/:fieldId', async (req, res) => {
try {
const [updated] = await DeviceField.update(req.body, {
where: { fieldId: req.params.fieldId }
where: { fieldId: req.params.fieldId },
});
if (updated) {
const updatedField = await DeviceField.findByPk(req.params.fieldId);
@@ -59,20 +59,20 @@ router.delete('/:fieldId', async (req, res) => {
try {
// 先查询字段信息
const field = await DeviceField.findByPk(req.params.fieldId);
if (!field) {
return res.status(404).json({ error: '字段不存在' });
}
// 检查是否为系统字段
if (field.isSystem) {
return res.status(403).json({ error: '系统字段不可删除' });
}
const deleted = await DeviceField.destroy({
where: { fieldId: req.params.fieldId }
where: { fieldId: req.params.fieldId },
});
if (deleted) {
res.status(204).json();
} else {
@@ -87,17 +87,17 @@ router.delete('/:fieldId', async (req, res) => {
router.post('/config', async (req, res) => {
try {
const fieldConfigs = req.body;
if (!Array.isArray(fieldConfigs)) {
return res.status(400).json({ error: '输入必须是数组' });
}
const updatedFields = [];
for (const config of fieldConfigs) {
const existingField = await DeviceField.findOne({ where: { fieldName: config.fieldName } });
if (existingField) {
await existingField.update({
await existingField.update({
visible: config.visible !== undefined ? config.visible : existingField.visible,
required: config.required !== undefined ? config.required : existingField.required,
displayName: config.displayName || existingField.displayName,
@@ -115,11 +115,11 @@ router.post('/config', async (req, res) => {
updatedFields.push(newField);
}
}
res.json(updatedFields);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
module.exports = router;
+109 -94
View File
@@ -14,49 +14,49 @@ router.get('/', async (req, res) => {
try {
const { deviceId, status, portType, portSpeed, page = 1, pageSize = 10 } = req.query;
const offset = (page - 1) * pageSize;
const where = {};
if (deviceId) {
where.deviceId = deviceId;
}
if (status && status !== 'all') {
where.status = status;
}
if (portType && portType !== 'all') {
where.portType = portType;
}
if (portSpeed && portSpeed !== 'all') {
where.portSpeed = portSpeed;
}
const { count, rows } = await DevicePort.findAndCountAll({
where,
include: [
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: NetworkCard,
as: 'networkCard',
attributes: ['nicId', 'name']
}
attributes: ['nicId', 'name'],
},
],
offset,
limit: parseInt(pageSize),
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
res.json({
total: count,
ports: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取端口列表失败:', error);
@@ -68,24 +68,24 @@ router.get('/', async (req, res) => {
router.get('/device/:deviceId', async (req, res) => {
try {
const { deviceId } = req.params;
const ports = await DevicePort.findAll({
where: { deviceId },
include: [
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: NetworkCard,
as: 'networkCard',
attributes: ['nicId', 'name']
}
attributes: ['nicId', 'name'],
},
],
order: [['portName', 'ASC']]
order: [['portName', 'ASC']],
});
res.json(ports);
} catch (error) {
console.error('获取设备端口失败:', error);
@@ -95,22 +95,23 @@ router.get('/device/:deviceId', async (req, res) => {
router.post('/', async (req, res) => {
try {
const { portId, deviceId, nicId, portName, portType, portSpeed, status, vlanId, description } = req.body;
const { portId, deviceId, nicId, portName, portType, portSpeed, status, vlanId, description } =
req.body;
if (!deviceId || !portName) {
return res.status(400).json({ error: '缺少必填字段' });
}
const existingPort = await DevicePort.findOne({
where: { deviceId, portName }
where: { deviceId, portName },
});
if (existingPort) {
return res.status(400).json({ error: '该设备的端口名称已存在' });
}
const autoPortId = portId || `PORT-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
const port = await DevicePort.create({
portId: autoPortId,
deviceId,
@@ -120,19 +121,19 @@ router.post('/', async (req, res) => {
portSpeed: portSpeed || '1G',
status: status || 'free',
vlanId,
description
description,
});
const createdPort = await DevicePort.findByPk(port.portId, {
include: [
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
res.status(201).json(createdPort);
} catch (error) {
console.error('创建端口失败:', error);
@@ -154,7 +155,7 @@ router.post('/batch', async (req, res) => {
failed: 0,
skipped: 0,
updated: 0,
errors: []
errors: [],
};
const transaction = await DevicePort.sequelize.transaction();
@@ -177,7 +178,9 @@ router.post('/batch', async (req, res) => {
if (isServer) {
if (!portData.nicId && !portData.网卡名称) {
throw new Error(`服务器 ${portData.deviceId} 的端口必须关联网卡,请先在网卡管理中添加网卡`);
throw new Error(
`服务器 ${portData.deviceId} 的端口必须关联网卡,请先在网卡管理中添加网卡`
);
}
let nicId = portData.nicId;
@@ -185,10 +188,12 @@ router.post('/batch', async (req, res) => {
if (!nicId && portData.网卡名称) {
const networkCard = await NetworkCard.findOne({
where: { deviceId: portData.deviceId, name: portData.网卡名称 },
transaction
transaction,
});
if (!networkCard) {
throw new Error(`服务器 ${portData.deviceId} 的网卡"${portData.网卡名称}"不存在,请先在网卡管理中添加该网卡`);
throw new Error(
`服务器 ${portData.deviceId} 的网卡"${portData.网卡名称}"不存在,请先在网卡管理中添加该网卡`
);
}
nicId = networkCard.nicId;
}
@@ -212,7 +217,7 @@ router.post('/batch', async (req, res) => {
const existingPort = await DevicePort.findOne({
where: { deviceId: portData.deviceId, portName: portData.portName },
transaction
transaction,
});
if (existingPort) {
@@ -221,17 +226,23 @@ router.post('/batch', async (req, res) => {
continue;
}
if (updateExisting) {
await DevicePort.update({
portType: portData.portType || existingPort.portType,
portSpeed: portData.portSpeed || existingPort.portSpeed,
status: portData.status || existingPort.status,
vlanId: portData.vlanId !== undefined ? portData.vlanId : existingPort.vlanId,
description: portData.description !== undefined ? portData.description : existingPort.description,
nicId: portData.nicId !== undefined ? portData.nicId : existingPort.nicId
}, {
where: { portId: existingPort.portId },
transaction
});
await DevicePort.update(
{
portType: portData.portType || existingPort.portType,
portSpeed: portData.portSpeed || existingPort.portSpeed,
status: portData.status || existingPort.status,
vlanId: portData.vlanId !== undefined ? portData.vlanId : existingPort.vlanId,
description:
portData.description !== undefined
? portData.description
: existingPort.description,
nicId: portData.nicId !== undefined ? portData.nicId : existingPort.nicId,
},
{
where: { portId: existingPort.portId },
transaction,
}
);
results.updated++;
results.success++;
continue;
@@ -239,17 +250,20 @@ router.post('/batch', async (req, res) => {
throw new Error('该设备的端口名称已存在');
}
await DevicePort.create({
portId: portData.portId,
deviceId: portData.deviceId,
nicId: portData.nicId || null,
portName: portData.portName,
portType: portData.portType || 'RJ45',
portSpeed: portData.portSpeed || '1G',
status: portData.status || 'free',
vlanId: portData.vlanId,
description: portData.description
}, { transaction });
await DevicePort.create(
{
portId: portData.portId,
deviceId: portData.deviceId,
nicId: portData.nicId || null,
portName: portData.portName,
portType: portData.portType || 'RJ45',
portSpeed: portData.portSpeed || '1G',
status: portData.status || 'free',
vlanId: portData.vlanId,
description: portData.description,
},
{ transaction }
);
results.success++;
} catch (error) {
@@ -259,7 +273,7 @@ router.post('/batch', async (req, res) => {
portId: portData.portId,
deviceId: portData.deviceId,
portName: portData.portName,
error: error.message
error: error.message,
});
}
}
@@ -279,18 +293,18 @@ router.post('/batch', async (req, res) => {
router.put('/:portId', async (req, res) => {
try {
const [updated] = await DevicePort.update(req.body, {
where: { portId: req.params.portId }
where: { portId: req.params.portId },
});
if (updated) {
const port = await DevicePort.findByPk(req.params.portId, {
include: [
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
res.json(port);
} else {
@@ -313,9 +327,9 @@ router.delete('/:portId', async (req, res) => {
where: {
[Op.or]: [
{ sourceDeviceId: port.deviceId, sourcePort: port.portName },
{ targetDeviceId: port.deviceId, targetPort: port.portName }
]
}
{ targetDeviceId: port.deviceId, targetPort: port.portName },
],
},
});
if (relatedCables.length > 0) {
@@ -326,13 +340,13 @@ router.delete('/:portId', async (req, res) => {
sourceDeviceId: c.sourceDeviceId,
sourcePort: c.sourcePort,
targetDeviceId: c.targetDeviceId,
targetPort: c.targetPort
}))
targetPort: c.targetPort,
})),
});
}
await DevicePort.destroy({
where: { portId: req.params.portId }
where: { portId: req.params.portId },
});
res.status(204).json();
@@ -351,12 +365,12 @@ router.delete('/batch', async (req, res) => {
}
const deletedCount = await DevicePort.destroy({
where: { portId: { [Op.in]: portIds } }
where: { portId: { [Op.in]: portIds } },
});
res.json({
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
deletedCount
deletedCount,
});
} catch (error) {
console.error('批量删除端口失败:', error);
@@ -373,12 +387,12 @@ router.post('/batch-delete', async (req, res) => {
}
const deletedCount = await DevicePort.destroy({
where: { portId: { [Op.in]: portIds } }
where: { portId: { [Op.in]: portIds } },
});
res.json({
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
deletedCount
deletedCount,
});
} catch (error) {
console.error('批量删除端口失败:', error);
@@ -393,9 +407,9 @@ router.get('/:portId', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
if (!port) {
@@ -457,24 +471,24 @@ router.get('/export/all', async (req, res) => {
{
model: require('../models/Room'),
as: 'room',
attributes: ['roomId', 'name']
}
]
}
]
attributes: ['roomId', 'name'],
},
],
},
],
},
{
model: NetworkCard,
as: 'networkCard',
attributes: ['nicId', 'name']
}
attributes: ['nicId', 'name'],
},
],
order: [['createdAt', 'DESC']],
limit: parsedPageSize,
offset: offset,
subQuery: false
subQuery: false,
}),
timeoutPromise
timeoutPromise,
]);
const ports = countResult;
@@ -482,7 +496,7 @@ router.get('/export/all', async (req, res) => {
const statusMap = {
free: '空闲',
occupied: '占用',
fault: '故障'
fault: '故障',
};
const exportData = ports.map(port => ({
@@ -499,17 +513,18 @@ router.get('/export/all', async (req, res) => {
状态: statusMap[port.status] || port.status,
VLAN_ID: port.vlanId || '-',
描述: port.description || '-',
创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-'
创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-',
}));
let filteredExportData = exportData;
if (keyword) {
const searchLower = keyword.toLowerCase();
filteredExportData = exportData.filter(item =>
item.端口名称?.toLowerCase().includes(searchLower) ||
item.端口类型?.toLowerCase().includes(searchLower) ||
item.设备名称?.toLowerCase().includes(searchLower) ||
item.描述?.toLowerCase().includes(searchLower)
filteredExportData = exportData.filter(
item =>
item.端口名称?.toLowerCase().includes(searchLower) ||
item.端口类型?.toLowerCase().includes(searchLower) ||
item.设备名称?.toLowerCase().includes(searchLower) ||
item.描述?.toLowerCase().includes(searchLower)
);
}
@@ -517,7 +532,7 @@ router.get('/export/all', async (req, res) => {
page: parsedPage,
pageSize: parsedPageSize,
total: filteredExportData.length,
ports: filteredExportData
ports: filteredExportData,
});
} catch (error) {
console.error('导出端口失败:', error);
+570 -381
View File
File diff suppressed because it is too large Load Diff
+368 -211
View File
@@ -4,15 +4,19 @@ const { Op } = require('sequelize');
const Device = require('../models/Device');
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const { logDeviceOperation, generateDeviceDescription, buildDeviceMetadata } = require('../utils/operationLogger');
const {
logDeviceOperation,
generateDeviceDescription,
buildDeviceMetadata,
} = require('../utils/operationLogger');
async function generateIdleDeviceId() {
const devices = await Device.findAll({
where: {
deviceId: {
[Op.like]: 'DEV%'
}
}
[Op.like]: 'DEV%',
},
},
});
let maxNumber = 0;
@@ -41,7 +45,7 @@ router.get('/', async (req, res) => {
where[Op.or] = [
{ deviceId: { [Op.like]: `%${keyword}%` } },
{ name: { [Op.like]: `%${keyword}%` } },
{ serialNumber: { [Op.like]: `%${keyword}%` } }
{ serialNumber: { [Op.like]: `%${keyword}%` } },
];
}
@@ -59,22 +63,24 @@ router.get('/', async (req, res) => {
{
model: Rack,
attributes: ['rackId', 'name', 'roomId'],
include: [{
model: Room,
attributes: ['roomId', 'name']
}]
}
include: [
{
model: Room,
attributes: ['roomId', 'name'],
},
],
},
],
offset: parseInt(offset),
limit: parseInt(pageSize),
order: [['idleDate', 'DESC']]
order: [['idleDate', 'DESC']],
});
res.json({
total: count,
idleDevices: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取空闲设备列表失败:', error);
@@ -90,12 +96,14 @@ router.get('/:deviceId', async (req, res) => {
{
model: Rack,
attributes: ['rackId', 'name', 'roomId'],
include: [{
model: Room,
attributes: ['roomId', 'name']
}]
}
]
include: [
{
model: Room,
attributes: ['roomId', 'name'],
},
],
},
],
});
if (!device) {
@@ -110,7 +118,18 @@ router.get('/:deviceId', async (req, res) => {
router.post('/', async (req, res) => {
try {
const { name, type, model, serialNumber, powerConsumption, idleReason, warehouseId, description, rackId, position } = req.body;
const {
name,
type,
model,
serialNumber,
powerConsumption,
idleReason,
warehouseId,
description,
rackId,
position,
} = req.body;
let { deviceId } = req.body;
@@ -144,24 +163,35 @@ router.post('/', async (req, res) => {
warehouseId: warehouseId || null,
rackId: rackId || null,
position: position || null,
sourceType: warehouseId ? 'warehouse' : (rackId ? 'rack' : 'rack'),
description: description || ''
sourceType: warehouseId ? 'warehouse' : rackId ? 'rack' : 'rack',
description: description || '',
});
await logDeviceOperation('create', generateDeviceDescription('新增空闲设备', {
deviceId: device.deviceId,
name: device.name || deviceId,
type: device.type,
model: device.model,
serialNumber: device.serialNumber,
ipAddress: device.ipAddress
}, { includeRack: false }), {
targetId: device.deviceId,
targetName: device.name || deviceId,
afterState: device.toJSON(),
req,
metadata: buildDeviceMetadata(device.toJSON(), { sourceType: device.sourceType, type: 'idle_device_create' })
});
await logDeviceOperation(
'create',
generateDeviceDescription(
'新增空闲设备',
{
deviceId: device.deviceId,
name: device.name || deviceId,
type: device.type,
model: device.model,
serialNumber: device.serialNumber,
ipAddress: device.ipAddress,
},
{ includeRack: false }
),
{
targetId: device.deviceId,
targetName: device.name || deviceId,
afterState: device.toJSON(),
req,
metadata: buildDeviceMetadata(device.toJSON(), {
sourceType: device.sourceType,
type: 'idle_device_create',
}),
}
);
res.status(201).json(device);
} catch (error) {
@@ -186,13 +216,16 @@ router.post('/from-device/:deviceId', async (req, res) => {
return res.status(400).json({ error: '设备已经标记为空闲设备' });
}
await device.update({
isIdle: true,
status: 'idle',
idleDate: new Date(),
idleReason: idleReason || `从设备管理转入`,
sourceType: 'rack'
}, { transaction: t });
await device.update(
{
isIdle: true,
status: 'idle',
idleDate: new Date(),
idleReason: idleReason || `从设备管理转入`,
sourceType: 'rack',
},
{ transaction: t }
);
await t.commit();
@@ -203,12 +236,12 @@ router.post('/from-device/:deviceId', async (req, res) => {
beforeState: { ...deviceData, isIdle: false },
afterState: { ...deviceData, isIdle: true },
req,
metadata: buildDeviceMetadata(deviceData, { idleReason, type: 'device_to_idle' })
metadata: buildDeviceMetadata(deviceData, { idleReason, type: 'device_to_idle' }),
});
res.json({
message: '设备已转入空闲设备',
device: device.toJSON()
device: device.toJSON(),
});
} catch (error) {
await t.rollback();
@@ -229,7 +262,7 @@ router.post('/batch-from-devices', async (req, res) => {
const devices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds } },
transaction: t
transaction: t,
});
const notIdleDevices = devices.filter(d => !d.isIdle);
@@ -241,11 +274,11 @@ router.post('/batch-from-devices', async (req, res) => {
isIdle: true,
status: 'idle',
idleDate: new Date(),
idleReason: idleReason || `批量转入`
idleReason: idleReason || `批量转入`,
},
{
where: { deviceId: { [Op.in]: notIdleDevices.map(d => d.deviceId) } },
transaction: t
transaction: t,
}
);
}
@@ -253,22 +286,29 @@ router.post('/batch-from-devices', async (req, res) => {
await t.commit();
const deviceDetails = notIdleDevices.map(d => d.toJSON());
const deviceSummary = deviceDetails.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.serialNumber ? `,序列号:${d.serialNumber}` : ''})`
).join('、');
const deviceSummary = deviceDetails
.map(
d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.serialNumber ? `,序列号:${d.serialNumber}` : ''})`
)
.join('、');
await logDeviceOperation('batch_to_idle', `批量将 ${notIdleDevices.length} 台设备转入空闲设备:${deviceSummary}`, {
targetId: deviceIds.join(','),
targetName: `${notIdleDevices.length}台设备`,
req,
metadata: { idleReason, type: 'batch_device_to_idle', devices: deviceDetails }
});
await logDeviceOperation(
'batch_to_idle',
`批量将 ${notIdleDevices.length} 台设备转入空闲设备:${deviceSummary}`,
{
targetId: deviceIds.join(','),
targetName: `${notIdleDevices.length}台设备`,
req,
metadata: { idleReason, type: 'batch_device_to_idle', devices: deviceDetails },
}
);
res.json({
message: `成功将 ${notIdleDevices.length} 台设备转入空闲设备`,
total: devices.length,
updated: notIdleDevices.length,
skipped: alreadyIdleDevices.length
skipped: alreadyIdleDevices.length,
});
} catch (error) {
await t.rollback();
@@ -305,23 +345,29 @@ router.put('/batch-restore', async (req, res) => {
const idleDevices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
transaction: t
transaction: t,
});
console.log('查询到的空闲设备数量:', idleDevices.length);
if (idleDevices.length > 0) {
console.log('查询到的设备ID:', idleDevices.map(d => d.deviceId));
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
transaction: t,
});
console.log('设备表中存在的设备数量:', allDevices.length);
if (allDevices.length > 0) {
console.log('存在的设备及其 isIdle 状态:', allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })));
console.log(
'存在的设备及其 isIdle 状态:',
allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle }))
);
}
await t.rollback();
@@ -333,7 +379,9 @@ router.put('/batch-restore', async (req, res) => {
for (const device of idleDevices) {
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
if (!deviceConfig) continue;
if (!deviceConfig) {
continue;
}
const targetRackId = deviceConfig.targetRackId;
const targetPosition = deviceConfig.targetPosition;
@@ -343,7 +391,7 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'skipped',
reason: '未指定目标机柜'
reason: '未指定目标机柜',
});
continue;
}
@@ -354,7 +402,7 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'failed',
reason: '目标机柜不存在'
reason: '目标机柜不存在',
});
continue;
}
@@ -368,25 +416,31 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'failed',
reason: `U位${position}已被占用`
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 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 });
await targetRack.update(
{
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
},
{ transaction: t }
);
restoredCount++;
results.push({
@@ -394,7 +448,7 @@ router.put('/batch-restore', async (req, res) => {
name: device.name,
status: 'success',
targetRack: targetRack.name,
targetPosition: position
targetPosition: position,
});
}
@@ -404,17 +458,30 @@ router.put('/batch-restore', async (req, res) => {
const failedCount = results.filter(r => r.status === 'failed').length;
const skippedCount = results.filter(r => r.status === 'skipped').length;
const successDevices = idleDevices.filter(d => results.some(r => r.deviceId === d.deviceId && r.status === 'success'));
const deviceSummary = successDevices.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
).join('、');
const successDevices = idleDevices.filter(d =>
results.some(r => r.deviceId === d.deviceId && r.status === 'success')
);
const deviceSummary = successDevices
.map(
d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
)
.join('、');
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备:${deviceSummary}`, {
targetId: deviceIds.join(','),
targetName: `${successCount}台设备`,
req,
metadata: { results, type: 'batch_idle_device_restore', devices: successDevices.map(d => d.toJSON()) }
});
await logDeviceOperation(
'batch_restore',
`批量上架 ${successCount} 台空闲设备:${deviceSummary}`,
{
targetId: deviceIds.join(','),
targetName: `${successCount}台设备`,
req,
metadata: {
results,
type: 'batch_idle_device_restore',
devices: successDevices.map(d => d.toJSON()),
},
}
);
res.json({
message: `成功上架 ${successCount} 台设备`,
@@ -422,7 +489,7 @@ router.put('/batch-restore', async (req, res) => {
restored: successCount,
failed: failedCount,
skipped: skippedCount,
details: results
details: results,
});
} catch (error) {
await t.rollback();
@@ -435,11 +502,21 @@ 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 {
name,
type,
model,
serialNumber,
height,
powerConsumption,
rackId,
position,
description,
} = req.body;
const device = await Device.findOne({
where: { deviceId, isIdle: true },
transaction: t
transaction: t,
});
if (!device) {
@@ -467,54 +544,63 @@ router.put('/:deviceId/shelve', async (req, res) => {
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 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 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] }
]
include: [{ model: Rack, include: [Room] }],
});
const deviceData = {
...updatedDevice.toJSON(),
rackName: targetRack.name,
roomName: updatedDevice.Rack?.Room?.name
roomName: updatedDevice.Rack?.Room?.name,
};
await logDeviceOperation('shelve', generateDeviceDescription('空闲设备上架', deviceData, { includePosition: false }) + `到机柜【${targetRack.name}】U${position}`, {
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...beforeState, isIdle: true },
afterState: deviceData,
req,
metadata: buildDeviceMetadata(deviceData, { rackId, position, type: 'idle_device_shelve' })
});
await logDeviceOperation(
'shelve',
generateDeviceDescription('空闲设备上架', deviceData, { includePosition: false }) +
`到机柜【${targetRack.name}】U${position}`,
{
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...beforeState, isIdle: true },
afterState: deviceData,
req,
metadata: buildDeviceMetadata(deviceData, { rackId, position, type: 'idle_device_shelve' }),
}
);
res.json({
message: '设备上架成功',
device: updatedDevice
device: updatedDevice,
});
} catch (error) {
await t.rollback();
@@ -526,7 +612,7 @@ router.put('/:deviceId/shelve', async (req, res) => {
router.put('/:deviceId', async (req, res) => {
try {
const device = await Device.findOne({
where: { deviceId: req.params.deviceId, isIdle: true }
where: { deviceId: req.params.deviceId, isIdle: true },
});
if (!device) {
@@ -534,7 +620,14 @@ router.put('/:deviceId', async (req, res) => {
}
const beforeState = device.toJSON();
const allowedFields = ['name', 'type', 'model', 'idleReason', 'description', 'powerConsumption'];
const allowedFields = [
'name',
'type',
'model',
'idleReason',
'description',
'powerConsumption',
];
allowedFields.forEach(field => {
if (req.body[field] !== undefined) {
@@ -566,14 +659,18 @@ router.put('/:deviceId', async (req, res) => {
await device.save();
await logDeviceOperation('update', generateDeviceDescription('更新空闲设备', device.toJSON(), { includeRack: false }), {
targetId: device.deviceId,
targetName: device.name,
beforeState,
afterState: device.toJSON(),
req,
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_update' })
});
await logDeviceOperation(
'update',
generateDeviceDescription('更新空闲设备', device.toJSON(), { includeRack: false }),
{
targetId: device.deviceId,
targetName: device.name,
beforeState,
afterState: device.toJSON(),
req,
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_update' }),
}
);
res.json(device);
} catch (error) {
@@ -589,7 +686,7 @@ router.put('/:deviceId/restore', async (req, res) => {
const device = await Device.findOne({
where: { deviceId, isIdle: true },
transaction: t
transaction: t,
});
if (!device) {
@@ -608,53 +705,72 @@ router.put('/:deviceId/restore', async (req, res) => {
return res.status(404).json({ error: '目标机柜不存在' });
}
const positionCheck = await checkPositionAvailable(targetRackId, targetPosition, device.height || 1, deviceId, t);
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 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 targetRack.update(
{
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
},
{ transaction: t }
);
await t.commit();
const updatedDevice = await Device.findByPk(deviceId, {
include: [
{ model: Rack, include: [Room] }
]
include: [{ model: Rack, include: [Room] }],
});
const deviceData = {
...updatedDevice.toJSON(),
rackName: targetRack.name,
roomName: updatedDevice.Rack?.Room?.name
roomName: updatedDevice.Rack?.Room?.name,
};
await logDeviceOperation('restore', generateDeviceDescription('空闲设备恢复', deviceData, { includePosition: false }) + `到机柜【${targetRack.name}】U${targetPosition}`, {
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...device.toJSON(), isIdle: true },
afterState: deviceData,
req,
metadata: buildDeviceMetadata(deviceData, { targetRackId, targetPosition, type: 'idle_device_restore' })
});
await logDeviceOperation(
'restore',
generateDeviceDescription('空闲设备恢复', deviceData, { includePosition: false }) +
`到机柜【${targetRack.name}】U${targetPosition}`,
{
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...device.toJSON(), isIdle: true },
afterState: deviceData,
req,
metadata: buildDeviceMetadata(deviceData, {
targetRackId,
targetPosition,
type: 'idle_device_restore',
}),
}
);
res.json({
message: '设备已恢复到设备管理',
device: updatedDevice
device: updatedDevice,
});
} catch (error) {
await t.rollback();
@@ -692,23 +808,29 @@ router.put('/batch-restore', async (req, res) => {
const idleDevices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
transaction: t
transaction: t,
});
console.log('查询到的空闲设备数量:', idleDevices.length);
if (idleDevices.length > 0) {
console.log('查询到的设备ID:', idleDevices.map(d => d.deviceId));
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
transaction: t,
});
console.log('设备表中存在的设备数量:', allDevices.length);
if (allDevices.length > 0) {
console.log('存在的设备及其 isIdle 状态:', allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })));
console.log(
'存在的设备及其 isIdle 状态:',
allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle }))
);
}
await t.rollback();
@@ -720,7 +842,9 @@ router.put('/batch-restore', async (req, res) => {
for (const device of idleDevices) {
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
if (!deviceConfig) continue;
if (!deviceConfig) {
continue;
}
const targetRackId = deviceConfig.targetRackId;
const targetPosition = deviceConfig.targetPosition;
@@ -730,7 +854,7 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'skipped',
reason: '未指定目标机柜'
reason: '未指定目标机柜',
});
continue;
}
@@ -741,7 +865,7 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'failed',
reason: '目标机柜不存在'
reason: '目标机柜不存在',
});
continue;
}
@@ -755,25 +879,31 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'failed',
reason: `U位${position}已被占用`
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 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 });
await targetRack.update(
{
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
},
{ transaction: t }
);
restoredCount++;
results.push({
@@ -781,7 +911,7 @@ router.put('/batch-restore', async (req, res) => {
name: device.name,
status: 'success',
targetRack: targetRack.name,
targetPosition: position
targetPosition: position,
});
}
@@ -791,17 +921,30 @@ router.put('/batch-restore', async (req, res) => {
const failedCount = results.filter(r => r.status === 'failed').length;
const skippedCount = results.filter(r => r.status === 'skipped').length;
const successDevices = idleDevices.filter(d => results.some(r => r.deviceId === d.deviceId && r.status === 'success'));
const deviceSummary = successDevices.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
).join('、');
const successDevices = idleDevices.filter(d =>
results.some(r => r.deviceId === d.deviceId && r.status === 'success')
);
const deviceSummary = successDevices
.map(
d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
)
.join('、');
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备:${deviceSummary}`, {
targetId: deviceIds.join(','),
targetName: `${successCount}台设备`,
req,
metadata: { results, type: 'batch_idle_device_restore', devices: successDevices.map(d => d.toJSON()) }
});
await logDeviceOperation(
'batch_restore',
`批量上架 ${successCount} 台空闲设备:${deviceSummary}`,
{
targetId: deviceIds.join(','),
targetName: `${successCount}台设备`,
req,
metadata: {
results,
type: 'batch_idle_device_restore',
devices: successDevices.map(d => d.toJSON()),
},
}
);
res.json({
message: `成功上架 ${successCount} 台设备`,
@@ -809,7 +952,7 @@ router.put('/batch-restore', async (req, res) => {
restored: successCount,
failed: failedCount,
skipped: skippedCount,
details: results
details: results,
});
} catch (error) {
await t.rollback();
@@ -823,7 +966,7 @@ router.delete('/:deviceId', async (req, res) => {
try {
const device = await Device.findOne({
where: { deviceId: req.params.deviceId, isIdle: true },
transaction: t
transaction: t,
});
if (!device) {
@@ -837,16 +980,24 @@ router.delete('/:deviceId', async (req, res) => {
await t.commit();
await logDeviceOperation('delete', generateDeviceDescription('删除空闲设备', {
...device.toJSON(),
name: device.name || device.deviceId
}, { includeRack: false }), {
targetId: device.deviceId,
targetName: device.name,
beforeState,
req,
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_delete' })
});
await logDeviceOperation(
'delete',
generateDeviceDescription(
'删除空闲设备',
{
...device.toJSON(),
name: device.name || device.deviceId,
},
{ includeRack: false }
),
{
targetId: device.deviceId,
targetName: device.name,
beforeState,
req,
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_delete' }),
}
);
res.json({ message: '空闲设备删除成功' });
} catch (error) {
@@ -856,7 +1007,13 @@ router.delete('/:deviceId', async (req, res) => {
}
});
async function checkPositionAvailable(rackId, position, height, excludeDeviceId = null, transaction = null) {
async function checkPositionAvailable(
rackId,
position,
height,
excludeDeviceId = null,
transaction = null
) {
if (!position || position <= 0) {
return { available: true, reason: null };
}
@@ -869,9 +1026,9 @@ async function checkPositionAvailable(rackId, position, height, excludeDeviceId
where: {
rackId: rackId,
position: { [Op.ne]: null },
isIdle: false
isIdle: false,
},
attributes: ['deviceId', 'position', 'height']
attributes: ['deviceId', 'position', 'height'],
};
if (transaction) {
@@ -891,7 +1048,7 @@ async function checkPositionAvailable(rackId, position, height, excludeDeviceId
if (!(endU < existStart || startU > existEnd)) {
return {
available: false,
reason: `U位冲突:机柜中已有设备 ${d.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''}`
reason: `U位冲突:机柜中已有设备 ${d.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''}`,
};
}
}
+202 -93
View File
@@ -54,25 +54,29 @@ router.get('/plans', async (req, res) => {
if (keyword) {
where[Op.or] = [
{ name: { [Op.like]: `%${keyword}%` } },
{ description: { [Op.like]: `%${keyword}%` } }
{ description: { [Op.like]: `%${keyword}%` } },
];
}
const { count, rows } = await InventoryPlan.findAndCountAll({
where,
include: [
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
{
model: require('../models/User'),
as: 'Creator',
attributes: ['userId', 'username', 'realName'],
},
],
order: [['createdAt', 'DESC']],
limit: parseInt(pageSize),
offset: parseInt(offset)
offset: parseInt(offset),
});
res.json({
plans: rows,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -83,8 +87,12 @@ router.get('/plans/:planId', async (req, res) => {
try {
const plan = await InventoryPlan.findByPk(req.params.planId, {
include: [
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
]
{
model: require('../models/User'),
as: 'Creator',
attributes: ['userId', 'username', 'realName'],
},
],
});
if (!plan) {
@@ -94,9 +102,13 @@ router.get('/plans/:planId', async (req, res) => {
const tasks = await InventoryTask.findAll({
where: { planId: plan.planId },
include: [
{ model: require('../models/User'), as: 'Assignee', attributes: ['userId', 'username', 'realName'] }
{
model: require('../models/User'),
as: 'Assignee',
attributes: ['userId', 'username', 'realName'],
},
],
order: [['createdAt', 'ASC']]
order: [['createdAt', 'ASC']],
});
res.json({ plan, tasks });
@@ -118,7 +130,7 @@ router.post('/plans', async (req, res) => {
targetRooms: targetRooms || [],
targetRacks: targetRacks || [],
status: 'draft',
createdBy: req.user?.userId
createdBy: req.user?.userId,
});
res.status(201).json(plan);
@@ -143,7 +155,7 @@ router.put('/plans/:planId', async (req, res) => {
scheduledDate: scheduledDate ? new Date(scheduledDate) : plan.scheduledDate,
targetRooms: targetRooms || plan.targetRooms,
targetRacks: targetRacks || plan.targetRacks,
status: status || plan.status
status: status || plan.status,
});
res.json(plan);
@@ -187,16 +199,16 @@ router.post('/plans/:planId/start', async (req, res) => {
if (targetRacks.length > 0) {
allDevices = await Device.findAll({
where: { rackId: { [Op.in]: targetRacks } }
where: { rackId: { [Op.in]: targetRacks } },
});
} else if (targetRooms.length > 0) {
const racksInRooms = await Rack.findAll({
where: { roomId: { [Op.in]: targetRooms } },
attributes: ['rackId']
attributes: ['rackId'],
});
const rackIds = racksInRooms.map(r => r.rackId);
allDevices = await Device.findAll({
where: { rackId: { [Op.in]: rackIds } }
where: { rackId: { [Op.in]: rackIds } },
});
} else {
allDevices = await Device.findAll();
@@ -206,7 +218,7 @@ router.post('/plans/:planId/start', async (req, res) => {
const recordsToCreate = [];
const taskId = generateTaskId();
tasksToCreate.push({
taskId,
planId: plan.planId,
@@ -214,7 +226,7 @@ router.post('/plans/:planId/start', async (req, res) => {
targetId: 'all',
targetName: '全部设备',
status: 'pending',
totalDevices: allDevices.length
totalDevices: allDevices.length,
});
for (let i = 0; i < allDevices.length; i++) {
@@ -229,7 +241,7 @@ router.post('/plans/:planId/start', async (req, res) => {
serialNumber: device.serialNumber,
rackId: device.rackId,
position: device.position,
status: 'pending'
status: 'pending',
});
}
@@ -238,13 +250,17 @@ router.post('/plans/:planId/start', async (req, res) => {
}
if (recordsToCreate.length > 0) {
const now = dbDialect === 'mysql'
? new Date().toISOString().replace('T', ' ').replace('Z', '')
: new Date().toISOString();
const placeholders = recordsToCreate.map(r =>
`('${r.recordId}', '${r.taskId}', '${r.planId}', '${r.deviceId}', '${r.deviceName}', '${r.deviceType}', '${r.serialNumber || ''}', '${r.rackId}', ${r.position}, 'pending', '${now}', '${now}')`
).join(',');
const now =
dbDialect === 'mysql'
? new Date().toISOString().replace('T', ' ').replace('Z', '')
: new Date().toISOString();
const placeholders = recordsToCreate
.map(
r =>
`('${r.recordId}', '${r.taskId}', '${r.planId}', '${r.deviceId}', '${r.deviceName}', '${r.deviceType}', '${r.serialNumber || ''}', '${r.rackId}', ${r.position}, 'pending', '${now}', '${now}')`
)
.join(',');
if (placeholders) {
await sequelize.query(`
INSERT INTO inventory_records (recordId, taskId, planId, deviceId, deviceName, deviceType, serialNumber, rackId, position, status, createdAt, updatedAt)
@@ -259,10 +275,14 @@ router.post('/plans/:planId/start', async (req, res) => {
checkedDevices: 0,
normalDevices: 0,
abnormalDevices: 0,
missedDevices: allDevices.length
missedDevices: allDevices.length,
});
res.json({ message: '盘点任务已启动', taskCount: tasksToCreate.length, deviceCount: allDevices.length });
res.json({
message: '盘点任务已启动',
taskCount: tasksToCreate.length,
deviceCount: allDevices.length,
});
} catch (error) {
console.error('启动盘点错误:', error);
res.status(500).json({ error: error.message });
@@ -274,8 +294,12 @@ router.get('/tasks/:taskId', async (req, res) => {
const task = await InventoryTask.findByPk(req.params.taskId, {
include: [
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
{ model: require('../models/User'), as: 'Assignee', attributes: ['userId', 'username', 'realName'] }
]
{
model: require('../models/User'),
as: 'Assignee',
attributes: ['userId', 'username', 'realName'],
},
],
});
if (!task) {
@@ -285,15 +309,23 @@ router.get('/tasks/:taskId', async (req, res) => {
const records = await InventoryRecord.findAll({
where: { taskId: task.taskId },
include: [
{ model: Device, as: 'Device', attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'] },
{ model: require('../models/User'), as: 'Checker', attributes: ['userId', 'username', 'realName'] }
]
{
model: Device,
as: 'Device',
attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'],
},
{
model: require('../models/User'),
as: 'Checker',
attributes: ['userId', 'username', 'realName'],
},
],
});
const rackIds = [...new Set(records.map(r => r.rackId).filter(Boolean))];
const racks = await Rack.findAll({
where: { rackId: rackIds },
include: [{ model: Room, as: 'Room' }]
include: [{ model: Room, as: 'Room' }],
});
const rackMap = {};
racks.forEach(r => {
@@ -305,10 +337,12 @@ router.get('/tasks/:taskId', async (req, res) => {
const roomName = rackInfo?.Room?.name || '';
const rackName = rackInfo?.name || record.rackId || '';
const position = record.position || '';
return {
...record.toJSON(),
displayLocation: roomName ? `${roomName} - ${rackName} - U${position}` : `${rackName} - U${position}`
displayLocation: roomName
? `${roomName} - ${rackName} - U${position}`
: `${rackName} - U${position}`,
};
});
@@ -331,14 +365,14 @@ router.put('/tasks/:taskId', async (req, res) => {
if (assignedTo !== undefined) {
await task.update({
assignedTo,
assignedAt: assignedTo ? new Date() : task.assignedAt
assignedAt: assignedTo ? new Date() : task.assignedAt,
});
}
if (status) {
await task.update({
status,
completedAt: status === 'completed' ? new Date() : null
completedAt: status === 'completed' ? new Date() : null,
});
}
@@ -351,7 +385,7 @@ router.put('/tasks/:taskId', async (req, res) => {
router.post('/records/:recordId/check', async (req, res) => {
try {
const record = await InventoryRecord.findByPk(req.params.recordId, {
include: [{ model: Device, as: 'Device' }]
include: [{ model: Device, as: 'Device' }],
});
if (!record) {
@@ -380,7 +414,7 @@ router.post('/records/:recordId/check', async (req, res) => {
checkedBy: req.user?.userId,
checkedAt: new Date(),
remark: remark || null,
photoUrl: photoUrl || null
photoUrl: photoUrl || null,
});
const task = await InventoryTask.findByPk(record.taskId);
@@ -391,7 +425,7 @@ router.post('/records/:recordId/check', async (req, res) => {
totalDevices: taskRecords.length,
checkedDevices: taskRecords.filter(r => r.status !== 'pending').length,
normalDevices: taskRecords.filter(r => r.status === 'normal').length,
abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length
abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length,
};
await task.update(taskStats);
@@ -402,7 +436,7 @@ router.post('/records/:recordId/check', async (req, res) => {
checkedDevices: planRecords.filter(r => r.status !== 'pending').length,
normalDevices: planRecords.filter(r => r.status === 'normal').length,
abnormalDevices: planRecords.filter(r => r.status === 'abnormal').length,
missedDevices: planRecords.filter(r => r.status === 'pending').length
missedDevices: planRecords.filter(r => r.status === 'pending').length,
};
await plan.update(planStats);
@@ -419,26 +453,43 @@ router.get('/records', async (req, res) => {
const offset = (page - 1) * pageSize;
const where = {};
if (planId) where.planId = planId;
if (taskId) where.taskId = taskId;
if (status) where.status = status;
if (planId) {
where.planId = planId;
}
if (taskId) {
where.taskId = taskId;
}
if (status) {
where.status = status;
}
const { count, rows } = await InventoryRecord.findAndCountAll({
where,
include: [
{ model: Device, as: 'Device', attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'] },
{ model: require('../models/User'), as: 'Checker', attributes: ['userId', 'username', 'realName'] }
{
model: Device,
as: 'Device',
attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'],
},
{
model: require('../models/User'),
as: 'Checker',
attributes: ['userId', 'username', 'realName'],
},
],
order: [
['checkedAt', 'DESC'],
['createdAt', 'DESC'],
],
order: [['checkedAt', 'DESC'], ['createdAt', 'DESC']],
limit: parseInt(pageSize),
offset: parseInt(offset)
offset: parseInt(offset),
});
res.json({
records: rows,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -466,7 +517,7 @@ router.post('/plans/:planId/complete', async (req, res) => {
checkedDevices: finalRecords.filter(r => r.status !== 'pending').length,
normalDevices: finalRecords.filter(r => r.status === 'normal').length,
abnormalDevices: finalRecords.filter(r => r.status === 'abnormal').length,
missedDevices: finalRecords.filter(r => r.status === 'missed').length
missedDevices: finalRecords.filter(r => r.status === 'missed').length,
});
await InventoryTask.update(
@@ -495,8 +546,12 @@ router.get('/stats/dashboard', async (req, res) => {
limit: 5,
order: [['createdAt', 'DESC']],
include: [
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
]
{
model: require('../models/User'),
as: 'Creator',
attributes: ['userId', 'username', 'realName'],
},
],
});
res.json({
@@ -507,9 +562,12 @@ router.get('/stats/dashboard', async (req, res) => {
normalRecords,
abnormalRecords,
pendingRecords,
completionRate: totalRecords > 0 ? ((normalRecords + abnormalRecords) / totalRecords * 100).toFixed(1) : 0,
abnormalRate: totalRecords > 0 ? (abnormalRecords / totalRecords * 100).toFixed(1) : 0,
recentPlans
completionRate:
totalRecords > 0
? (((normalRecords + abnormalRecords) / totalRecords) * 100).toFixed(1)
: 0,
abnormalRate: totalRecords > 0 ? ((abnormalRecords / totalRecords) * 100).toFixed(1) : 0,
recentPlans,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -518,18 +576,18 @@ router.get('/stats/dashboard', async (req, res) => {
router.post('/quick-add-device', async (req, res) => {
try {
const {
taskId,
planId,
const {
taskId,
planId,
serialNumber,
SN,
deviceName,
name,
deviceType,
type,
roomId,
rackId,
position,
roomId,
rackId,
position,
model,
brand,
height,
@@ -558,22 +616,27 @@ router.post('/quick-add-device', async (req, res) => {
const existingDevice = await Device.findOne({ where: { serialNumber: finalSerialNumber } });
if (existingDevice) {
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中', deviceId: existingDevice.deviceId });
return res
.status(400)
.json({ error: '该序列号的设备已存在于设备管理中', deviceId: existingDevice.deviceId });
}
const existingPending = await PendingDevice.findOne({
where: { serialNumber: finalSerialNumber, status: 'pending' }
const existingPending = await PendingDevice.findOne({
where: { serialNumber: finalSerialNumber, status: 'pending' },
});
if (existingPending) {
return res.status(400).json({ error: '该序列号的设备已在暂存列表中', pendingId: existingPending.pendingId });
return res
.status(400)
.json({ error: '该序列号的设备已在暂存列表中', pendingId: existingPending.pendingId });
}
const pendingId = `PEND${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
// 只有当用户没有填写设备名称时,才使用默认名称
const finalName = finalDeviceName && finalDeviceName.trim() !== ''
? finalDeviceName.trim()
: `新设备-${finalSerialNumber.slice(-6)}`;
const finalName =
finalDeviceName && finalDeviceName.trim() !== ''
? finalDeviceName.trim()
: `新设备-${finalSerialNumber.slice(-6)}`;
const pendingDevice = await PendingDevice.create({
pendingId,
@@ -596,12 +659,12 @@ router.post('/quick-add-device', async (req, res) => {
taskId: taskId || null,
createdBy: req.user?.userId,
status: 'pending',
remark: remark || '盘点时快速添加'
remark: remark || '盘点时快速添加',
});
res.status(201).json({
message: '设备已暂存,请前往暂存设备页面完善信息后同步',
pendingDevice
pendingDevice,
});
} catch (error) {
console.error('快速添加设备错误:', error);
@@ -611,7 +674,14 @@ router.post('/quick-add-device', async (req, res) => {
router.get('/pending-devices', async (req, res) => {
try {
const { status, planId, roomId, keyword, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE } = req.query;
const {
status,
planId,
roomId,
keyword,
page = 1,
pageSize = PAGINATION.DEFAULT_PAGE_SIZE,
} = req.query;
const offset = (page - 1) * pageSize;
const where = {};
@@ -627,7 +697,7 @@ router.get('/pending-devices', async (req, res) => {
if (keyword) {
where[Op.or] = [
{ serialNumber: { [Op.like]: `%${keyword}%` } },
{ deviceName: { [Op.like]: `%${keyword}%` } }
{ deviceName: { [Op.like]: `%${keyword}%` } },
];
}
@@ -638,18 +708,18 @@ router.get('/pending-devices', async (req, res) => {
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] }
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] },
],
order: [['createdAt', 'DESC']],
limit: parseInt(pageSize),
offset: parseInt(offset)
offset: parseInt(offset),
});
res.json({
pendingDevices: rows,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取暂存设备列表错误:', error);
@@ -677,8 +747,8 @@ router.get('/pending-devices/:pendingId', async (req, res) => {
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] }
]
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] },
],
});
if (!pendingDevice) {
@@ -702,7 +772,23 @@ router.put('/pending-devices/:pendingId', async (req, res) => {
return res.status(400).json({ error: '已同步的设备无法修改' });
}
const { deviceName, deviceType, roomId, rackId, position, model, brand, height, powerConsumption, ipAddress, purchaseDate, warrantyExpiry, description, remark, ...restFields } = req.body;
const {
deviceName,
deviceType,
roomId,
rackId,
position,
model,
brand,
height,
powerConsumption,
ipAddress,
purchaseDate,
warrantyExpiry,
description,
remark,
...restFields
} = req.body;
const updateData = {
deviceName: deviceName !== undefined ? deviceName : pendingDevice.deviceName,
@@ -713,12 +799,23 @@ router.put('/pending-devices/:pendingId', async (req, res) => {
model: model !== undefined ? model : pendingDevice.model,
brand: brand !== undefined ? brand : pendingDevice.brand,
height: height !== undefined ? height : pendingDevice.height,
powerConsumption: powerConsumption !== undefined ? powerConsumption : pendingDevice.powerConsumption,
powerConsumption:
powerConsumption !== undefined ? powerConsumption : pendingDevice.powerConsumption,
ipAddress: ipAddress !== undefined ? ipAddress : pendingDevice.ipAddress,
purchaseDate: purchaseDate !== undefined ? (purchaseDate ? new Date(purchaseDate) : null) : pendingDevice.purchaseDate,
warrantyExpiry: warrantyExpiry !== undefined ? (warrantyExpiry ? new Date(warrantyExpiry) : null) : pendingDevice.warrantyExpiry,
purchaseDate:
purchaseDate !== undefined
? purchaseDate
? new Date(purchaseDate)
: null
: pendingDevice.purchaseDate,
warrantyExpiry:
warrantyExpiry !== undefined
? warrantyExpiry
? new Date(warrantyExpiry)
: null
: pendingDevice.warrantyExpiry,
description: description !== undefined ? description : pendingDevice.description,
remark: remark !== undefined ? remark : pendingDevice.remark
remark: remark !== undefined ? remark : pendingDevice.remark,
};
if (Object.keys(restFields).length > 0) {
@@ -759,7 +856,9 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
return res.status(400).json({ error: '该设备已同步' });
}
const existingDevice = await Device.findOne({ where: { serialNumber: pendingDevice.serialNumber } });
const existingDevice = await Device.findOne({
where: { serialNumber: pendingDevice.serialNumber },
});
if (existingDevice) {
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中' });
}
@@ -770,7 +869,9 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
const match = device.deviceId.match(/^DEV(\d+)$/);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNumber) maxNumber = num;
if (num > maxNumber) {
maxNumber = num;
}
}
});
const deviceId = `DEV${String(maxNumber + 1).padStart(3, '0')}`;
@@ -790,20 +891,20 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
purchaseDate: pendingDevice.purchaseDate,
warrantyExpiry: pendingDevice.warrantyExpiry,
customFields: pendingDevice.customFields,
status: 'running'
status: 'running',
});
await pendingDevice.update({
status: 'synced',
syncedAt: new Date(),
syncedBy: req.user?.userId,
syncedDeviceId: newDevice.deviceId
syncedDeviceId: newDevice.deviceId,
});
res.json({
message: '同步成功',
device: newDevice,
pendingDevice
pendingDevice,
});
} catch (error) {
console.error('同步设备错误:', error);
@@ -821,8 +922,8 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
const pendingDevices = await PendingDevice.findAll({
where: {
pendingId: { [Op.in]: pendingIds },
status: 'pending'
}
status: 'pending',
},
});
if (pendingDevices.length === 0) {
@@ -835,7 +936,9 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
const match = device.deviceId.match(/^DEV(\d+)$/);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNumber) maxNumber = num;
if (num > maxNumber) {
maxNumber = num;
}
}
});
@@ -844,9 +947,15 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
for (const pending of pendingDevices) {
try {
const existingDevice = await Device.findOne({ where: { serialNumber: pending.serialNumber } });
const existingDevice = await Device.findOne({
where: { serialNumber: pending.serialNumber },
});
if (existingDevice) {
errors.push({ pendingId: pending.pendingId, serialNumber: pending.serialNumber, error: '序列号已存在' });
errors.push({
pendingId: pending.pendingId,
serialNumber: pending.serialNumber,
error: '序列号已存在',
});
continue;
}
@@ -868,14 +977,14 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
purchaseDate: pending.purchaseDate,
warrantyExpiry: pending.warrantyExpiry,
customFields: pending.customFields,
status: 'running'
status: 'running',
});
await pending.update({
status: 'synced',
syncedAt: new Date(),
syncedBy: req.user?.userId,
syncedDeviceId: newDevice.deviceId
syncedDeviceId: newDevice.deviceId,
});
results.push({ pendingId: pending.pendingId, deviceId: newDevice.deviceId });
@@ -889,7 +998,7 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
successCount: results.length,
errorCount: errors.length,
results,
errors
errors,
});
} catch (error) {
console.error('批量同步设备错误:', error);
+84 -58
View File
@@ -13,7 +13,7 @@ router.get('/', async (req, res) => {
try {
const { deviceId } = req.query;
const where = {};
if (deviceId) {
where.deviceId = deviceId;
}
@@ -24,10 +24,13 @@ router.get('/', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type']
}
attributes: ['deviceId', 'name', 'type'],
},
],
order: [
['slotNumber', 'ASC'],
['name', 'ASC'],
],
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
});
res.json(networkCards);
@@ -47,10 +50,13 @@ router.get('/device/:deviceId', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type']
}
attributes: ['deviceId', 'name', 'type'],
},
],
order: [
['slotNumber', 'ASC'],
['name', 'ASC'],
],
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
});
res.json(networkCards);
@@ -66,14 +72,17 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
const networkCards = await NetworkCard.findAll({
where: { deviceId },
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
order: [
['slotNumber', 'ASC'],
['name', 'ASC'],
],
});
const cardsWithPorts = await Promise.all(
networkCards.map(async (card) => {
networkCards.map(async card => {
const ports = await DevicePort.findAll({
where: { nicId: card.nicId },
order: [['portName', 'ASC']]
order: [['portName', 'ASC']],
});
const freeCount = ports.filter(p => p.status === 'free').length;
@@ -87,15 +96,15 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
total: ports.length,
free: freeCount,
occupied: occupiedCount,
fault: faultCount
}
fault: faultCount,
},
};
})
);
const ungroupedPorts = await DevicePort.findAll({
where: { deviceId, nicId: null },
order: [['portName', 'ASC']]
order: [['portName', 'ASC']],
});
if (ungroupedPorts.length > 0) {
@@ -110,8 +119,8 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
total: ungroupedPorts.length,
free: ungroupedPorts.filter(p => p.status === 'free').length,
occupied: ungroupedPorts.filter(p => p.status === 'occupied').length,
fault: ungroupedPorts.filter(p => p.status === 'fault').length
}
fault: ungroupedPorts.filter(p => p.status === 'fault').length,
},
});
}
@@ -129,9 +138,9 @@ router.get('/:nicId', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type']
}
]
attributes: ['deviceId', 'name', 'type'],
},
],
});
if (!networkCard) {
@@ -151,7 +160,7 @@ router.get('/:nicId/ports', async (req, res) => {
const ports = await DevicePort.findAll({
where: { nicId },
order: [['portName', 'ASC']]
order: [['portName', 'ASC']],
});
res.json(ports);
@@ -170,7 +179,7 @@ router.get('/find', async (req, res) => {
}
const networkCard = await NetworkCard.findOne({
where: { deviceId, name }
where: { deviceId, name },
});
if (!networkCard) {
@@ -186,14 +195,15 @@ router.get('/find', async (req, res) => {
router.post('/', async (req, res) => {
try {
const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } = req.body;
const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } =
req.body;
if (!deviceId || !name) {
return res.status(400).json({ error: '缺少必填字段' });
}
const existingCard = await NetworkCard.findOne({
where: { deviceId, name }
where: { deviceId, name },
});
if (existingCard) {
@@ -211,7 +221,7 @@ router.post('/', async (req, res) => {
model,
manufacturer,
status: status || 'normal',
portCount: 0
portCount: 0,
});
const createdCard = await NetworkCard.findByPk(networkCard.nicId, {
@@ -219,9 +229,9 @@ router.post('/', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type']
}
]
attributes: ['deviceId', 'name', 'type'],
},
],
});
res.status(201).json(createdCard);
@@ -245,7 +255,7 @@ router.post('/batch', async (req, res) => {
failed: 0,
skipped: 0,
updated: 0,
errors: []
errors: [],
};
const transaction = await NetworkCard.sequelize.transaction();
@@ -272,7 +282,7 @@ router.post('/batch', async (req, res) => {
const existingCard = await NetworkCard.findOne({
where: { deviceId: cardData.deviceId, name: cardData.name },
transaction
transaction,
});
if (existingCard) {
@@ -281,16 +291,28 @@ router.post('/batch', async (req, res) => {
continue;
}
if (updateExisting) {
await NetworkCard.update({
slotNumber: cardData.slotNumber !== undefined ? cardData.slotNumber : existingCard.slotNumber,
model: cardData.model !== undefined ? cardData.model : existingCard.model,
manufacturer: cardData.manufacturer !== undefined ? cardData.manufacturer : existingCard.manufacturer,
description: cardData.description !== undefined ? cardData.description : existingCard.description,
status: cardData.status || existingCard.status
}, {
where: { nicId: existingCard.nicId },
transaction
});
await NetworkCard.update(
{
slotNumber:
cardData.slotNumber !== undefined
? cardData.slotNumber
: existingCard.slotNumber,
model: cardData.model !== undefined ? cardData.model : existingCard.model,
manufacturer:
cardData.manufacturer !== undefined
? cardData.manufacturer
: existingCard.manufacturer,
description:
cardData.description !== undefined
? cardData.description
: existingCard.description,
status: cardData.status || existingCard.status,
},
{
where: { nicId: existingCard.nicId },
transaction,
}
);
results.updated++;
results.success++;
continue;
@@ -298,19 +320,23 @@ router.post('/batch', async (req, res) => {
throw new Error('该设备已存在同名网卡');
}
const autoNicId = cardData.nicId || `NIC-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
const autoNicId =
cardData.nicId || `NIC-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
await NetworkCard.create({
nicId: autoNicId,
deviceId: cardData.deviceId,
name: cardData.name,
slotNumber: cardData.slotNumber,
model: cardData.model,
manufacturer: cardData.manufacturer,
description: cardData.description,
status: cardData.status || 'normal',
portCount: 0
}, { transaction });
await NetworkCard.create(
{
nicId: autoNicId,
deviceId: cardData.deviceId,
name: cardData.name,
slotNumber: cardData.slotNumber,
model: cardData.model,
manufacturer: cardData.manufacturer,
description: cardData.description,
status: cardData.status || 'normal',
portCount: 0,
},
{ transaction }
);
results.success++;
} catch (error) {
@@ -319,7 +345,7 @@ router.post('/batch', async (req, res) => {
index: i + 1,
deviceId: cardData.deviceId,
name: cardData.name,
error: error.message
error: error.message,
});
}
}
@@ -339,7 +365,7 @@ router.post('/batch', async (req, res) => {
router.put('/:nicId', async (req, res) => {
try {
const [updated] = await NetworkCard.update(req.body, {
where: { nicId: req.params.nicId }
where: { nicId: req.params.nicId },
});
if (updated) {
@@ -348,9 +374,9 @@ router.put('/:nicId', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type']
}
]
attributes: ['deviceId', 'name', 'type'],
},
],
});
res.json(networkCard);
} else {
@@ -368,13 +394,13 @@ router.delete('/:nicId', async (req, res) => {
const portCount = await DevicePort.count({ where: { nicId } });
if (portCount > 0) {
return res.status(400).json({
error: `该网卡下还有 ${portCount} 个端口,请先删除或转移端口后再删除网卡`
return res.status(400).json({
error: `该网卡下还有 ${portCount} 个端口,请先删除或转移端口后再删除网卡`,
});
}
const deleted = await NetworkCard.destroy({
where: { nicId }
where: { nicId },
});
if (deleted) {
+31 -28
View File
@@ -18,7 +18,7 @@ router.get('/', authMiddleware, async (req, res) => {
keyword,
startDate,
endDate,
result
result,
} = req.query;
const offset = (parseInt(page) - 1) * parseInt(pageSize);
@@ -46,7 +46,7 @@ router.get('/', authMiddleware, async (req, res) => {
where[Op.or] = [
{ operationDescription: { [Op.like]: `%${keyword}%` } },
{ targetName: { [Op.like]: `%${keyword}%` } },
{ operatorName: { [Op.like]: `%${keyword}%` } }
{ operatorName: { [Op.like]: `%${keyword}%` } },
];
}
@@ -70,7 +70,7 @@ router.get('/', authMiddleware, async (req, res) => {
where,
order: [['createdAt', 'DESC']],
offset,
limit
limit,
});
res.json({
@@ -79,14 +79,14 @@ router.get('/', authMiddleware, async (req, res) => {
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize),
logs
}
logs,
},
});
} catch (error) {
console.error('获取操作日志失败:', error);
res.status(500).json({
success: false,
message: '获取操作日志失败'
message: '获取操作日志失败',
});
}
});
@@ -95,23 +95,23 @@ router.get('/modules', authMiddleware, async (req, res) => {
try {
const modules = await OperationLog.findAll({
attributes: ['module'],
group: ['module']
group: ['module'],
});
const moduleList = modules.map(m => ({
value: m.module,
label: getModuleName(m.module)
label: getModuleName(m.module),
}));
res.json({
success: true,
data: moduleList
data: moduleList,
});
} catch (error) {
console.error('获取模块列表失败:', error);
res.status(500).json({
success: false,
message: '获取模块列表失败'
message: '获取模块列表失败',
});
}
});
@@ -128,23 +128,23 @@ router.get('/types', authMiddleware, async (req, res) => {
const types = await OperationLog.findAll({
where,
attributes: ['operationType'],
group: ['operationType']
group: ['operationType'],
});
const typeList = types.map(t => ({
value: t.operationType,
label: getOperationTypeName(t.operationType)
label: getOperationTypeName(t.operationType),
}));
res.json({
success: true,
data: typeList
data: typeList,
});
} catch (error) {
console.error('获取操作类型列表失败:', error);
res.status(500).json({
success: false,
message: '获取操作类型列表失败'
message: '获取操作类型列表失败',
});
}
});
@@ -170,23 +170,26 @@ router.get('/statistics', authMiddleware, async (req, res) => {
OperationLog.findAll({
where,
attributes: ['module', [sequelize.fn('COUNT', sequelize.col('module')), 'count']],
group: ['module']
group: ['module'],
}),
OperationLog.findAll({
where,
attributes: ['operationType', [sequelize.fn('COUNT', sequelize.col('operationType')), 'count']],
group: ['operationType']
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']
[sequelize.fn('COUNT', '*'), 'count'],
],
group: [sequelize.fn('DATE', sequelize.col('createdAt'))],
order: [[sequelize.fn('DATE', sequelize.col('createdAt')), 'DESC']],
limit: 30
})
limit: 30,
}),
]);
res.json({
@@ -194,14 +197,14 @@ router.get('/statistics', authMiddleware, async (req, res) => {
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') }))
}
byDay: dailyStats.map(s => ({ date: s.get('date'), count: s.get('count') })),
},
});
} catch (error) {
console.error('获取操作日志统计失败:', error);
res.status(500).json({
success: false,
message: '获取操作日志统计失败'
message: '获取操作日志统计失败',
});
}
});
@@ -213,19 +216,19 @@ router.get('/:recordId', authMiddleware, async (req, res) => {
if (!log) {
return res.status(404).json({
success: false,
message: '日志记录不存在'
message: '日志记录不存在',
});
}
res.json({
success: true,
data: log
data: log,
});
} catch (error) {
console.error('获取操作日志详情失败:', error);
res.status(500).json({
success: false,
message: '获取操作日志详情失败'
message: '获取操作日志详情失败',
});
}
});
@@ -239,7 +242,7 @@ function getModuleName(module) {
rack: '机柜管理',
room: '机房管理',
ticket: '工单管理',
backup: '备份管理'
backup: '备份管理',
};
return moduleNames[module] || module;
}
@@ -255,7 +258,7 @@ function getOperationTypeName(type) {
move: '移动',
permission_change: '权限变更',
import: '导入',
export: '导出'
export: '导出',
};
return typeNames[type] || type;
}
+167 -150
View File
@@ -32,7 +32,7 @@ router.get('/', async (req, res) => {
if (keyword) {
where[require('sequelize').Op.or] = [
{ rackId: { [require('sequelize').Op.like]: `%${keyword}%` } },
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } }
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } },
];
}
@@ -42,18 +42,16 @@ router.get('/', async (req, res) => {
// 获取分页数据 - 先查询机柜基本信息
const racks = await Rack.findAll({
where,
include: [
{ model: Room, separate: false }
],
include: [{ model: Room, separate: false }],
limit: pageSize,
offset: offset
offset: offset,
});
// 单独查询每个机柜的设备信息(避免 JOIN 导致的数据重复问题)
const rackIds = racks.map(r => r.rackId);
const devices = await Device.findAll({
where: { rackId: rackIds },
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height']
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height'],
});
// 将设备信息关联到对应的机柜
@@ -72,7 +70,7 @@ router.get('/', async (req, res) => {
// 返回带分页信息的响应
res.json({
racks,
total
total,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -95,20 +93,20 @@ router.get('/all', async (req, res) => {
if (keyword) {
where[require('sequelize').Op.or] = [
{ rackId: { [require('sequelize').Op.like]: `%${keyword}%` } },
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } }
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } },
];
}
const racks = await Rack.findAll({
where,
include: [{ model: Room, separate: false }],
limit: MAX_EXPORT_SIZE
limit: MAX_EXPORT_SIZE,
});
const rackIds = racks.map(r => r.rackId);
const devices = await Device.findAll({
where: { rackId: rackIds },
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height']
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height'],
});
const deviceMap = {};
@@ -125,7 +123,7 @@ router.get('/all', async (req, res) => {
res.json({
racks,
total: racks.length
total: racks.length,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -139,51 +137,49 @@ router.get('/import-template', async (req, res) => {
const templateData = [
{
'机柜ID(留空自动生成)': '',
'机柜名称': '测试机柜1',
'所属机房名称': '测试机房1',
机柜名称: '测试机柜1',
所属机房名称: '测试机房1',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': 'RACK001',
'机柜名称': '测试机柜2',
'所属机房名称': '测试机房1',
机柜名称: '测试机柜2',
所属机房名称: '测试机房1',
'高度(U)': 42,
'最大功率(W)': 3000,
'状态': 'maintenance'
}
状态: 'maintenance',
},
];
// 使用xlsx创建工作簿
const wb = XLSX.utils.book_new();
// 将数据转换为工作表
const ws = XLSX.utils.json_to_sheet(templateData);
// 设置列宽
ws['!cols'] = [
{ wch: 15 },
{ wch: 20 },
{ wch: 15 },
{ wch: 10 },
{ wch: 15 },
{ wch: 15 }
];
ws['!cols'] = [{ wch: 15 }, { wch: 20 }, { wch: 15 }, { wch: 10 }, { wch: 15 }, { wch: 15 }];
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, '机柜模板');
// 生成Excel文件的Buffer
const excelBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
// 设置响应头
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent('机柜导入模板.xlsx')}`);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
res.setHeader(
'Content-Disposition',
`attachment; filename*=UTF-8''${encodeURIComponent('机柜导入模板.xlsx')}`
);
// 发送文件
res.send(excelBuffer);
} catch (error) {
console.error('生成导入模板失败:', error);
res.status(500).json({ error: '生成导入模板失败' });
@@ -197,34 +193,36 @@ router.get('/export', async (req, res) => {
const racks = await Rack.findAll({
include: [
{ model: Room, attributes: ['name'] },
{ model: Device, attributes: ['deviceId', 'name', 'powerConsumption'] }
{ model: Device, attributes: ['deviceId', 'name', 'powerConsumption'] },
],
order: [['rackId', 'ASC']]
order: [['rackId', 'ASC']],
});
// 准备导出数据
const exportData = racks.map(rack => {
const deviceCount = rack.Devices ? rack.Devices.length : 0;
const totalPower = rack.Devices ? rack.Devices.reduce((sum, d) => sum + (d.powerConsumption || 0), 0) : 0;
const totalPower = rack.Devices
? rack.Devices.reduce((sum, d) => sum + (d.powerConsumption || 0), 0)
: 0;
return {
'机柜ID': rack.rackId,
'机柜名称': rack.name,
'所属机房': rack.Room ? rack.Room.name : '',
机柜ID: rack.rackId,
机柜名称: rack.name,
所属机房: rack.Room ? rack.Room.name : '',
'机柜高度(U)': rack.height,
'最大功耗(W)': rack.maxPower,
'当前功耗(W)': rack.currentPower || 0,
'设备数量': deviceCount,
设备数量: deviceCount,
'设备总功耗(W)': totalPower,
'状态': rack.status === 'active' ? '启用' : rack.status === 'maintenance' ? '维护中' : '停用',
'创建时间': rack.createdAt ? new Date(rack.createdAt).toLocaleString() : ''
状态: rack.status === 'active' ? '启用' : rack.status === 'maintenance' ? '维护中' : '停用',
创建时间: rack.createdAt ? new Date(rack.createdAt).toLocaleString() : '',
};
});
// 创建工作簿
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(exportData);
// 设置列宽
ws['!cols'] = [
{ wch: 15 }, // 机柜ID
@@ -236,53 +234,58 @@ router.get('/export', async (req, res) => {
{ wch: 12 }, // 设备数量
{ wch: 15 }, // 设备总功耗
{ wch: 10 }, // 状态
{ wch: 20 } // 创建时间
{ wch: 20 }, // 创建时间
];
XLSX.utils.book_append_sheet(wb, ws, '机柜列表');
// 生成文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const fileName = `机柜导出_${timestamp}.xlsx`;
// 确保temp目录存在
const tempDir = path.join(__dirname, '../temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const filePath = path.join(tempDir, fileName);
// 写入文件
XLSX.writeFile(wb, filePath);
// 发送文件
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
res.setHeader(
'Content-Disposition',
`attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`
);
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
// 发送完成后删除临时文件
fileStream.on('close', () => {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
});
fileStream.on('error', (err) => {
fileStream.on('error', err => {
console.error('文件流错误:', err);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
});
} catch (error) {
console.error('导出租机柜数据失败:', error);
res.status(500).json({
success: false,
res.status(500).json({
success: false,
message: '导出失败',
error: error.message
error: error.message,
});
}
});
@@ -293,9 +296,9 @@ router.get('/:rackId', async (req, res) => {
const rack = await Rack.findByPk(req.params.rackId, {
include: [
{ model: Room, separate: false },
{ model: Device, separate: false }
{ model: Device, separate: false },
],
subQuery: false // 避免子查询导致的性能问题
subQuery: false, // 避免子查询导致的性能问题
});
if (!rack) {
return res.status(404).json({ error: '机柜不存在' });
@@ -312,11 +315,11 @@ async function generateRackId() {
const racks = await Rack.findAll({
where: {
rackId: {
[require('sequelize').Op.like]: 'RACK%'
}
}
[require('sequelize').Op.like]: 'RACK%',
},
},
});
let maxNumber = 0;
racks.forEach(rack => {
const match = rack.rackId.match(/^RACK(\d+)$/);
@@ -327,7 +330,7 @@ async function generateRackId() {
}
}
});
// 生成新的机柜ID,序号+1,至少3位数字
const newNumber = maxNumber + 1;
return `RACK${String(newNumber).padStart(3, '0')}`;
@@ -337,12 +340,12 @@ async function generateRackId() {
router.post('/', validateBody(createRackSchema), async (req, res) => {
try {
const rackData = { ...req.body };
// 如果没有提供rackId或为空,则自动生成
if (!rackData.rackId || rackData.rackId.trim() === '') {
rackData.rackId = await generateRackId();
}
const rack = await Rack.create(rackData);
res.status(201).json(rack);
} catch (error) {
@@ -354,15 +357,15 @@ router.post('/', validateBody(createRackSchema), async (req, res) => {
router.put('/:rackId', validateBody(updateRackSchema), async (req, res) => {
try {
const [updated] = await Rack.update(req.body, {
where: { rackId: req.params.rackId }
where: { rackId: req.params.rackId },
});
if (updated) {
const updatedRack = await Rack.findByPk(req.params.rackId, {
include: [
{ model: Room, separate: false },
{ model: Device, separate: false }
{ model: Device, separate: false },
],
subQuery: false // 避免子查询导致的性能问题
subQuery: false, // 避免子查询导致的性能问题
});
res.json(updatedRack);
} else {
@@ -381,9 +384,9 @@ router.delete('/:rackId', async (req, res) => {
if (devices.length > 0) {
return res.status(400).json({ error: '该机柜下有设备,无法删除' });
}
const deleted = await Rack.destroy({
where: { rackId: req.params.rackId }
where: { rackId: req.params.rackId },
});
if (deleted) {
res.status(204).json();
@@ -398,36 +401,36 @@ router.delete('/:rackId', async (req, res) => {
// 导入机柜数据 - 优化版:使用事务+批量插入
router.post('/import', async (req, res) => {
const t = await sequelize.transaction();
try {
// 检查是否有上传文件
if (!req.files || !req.files.file) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: '没有上传文件',
error: '没有找到有效的上传文件,请选择一个Excel文件后重试'
error: '没有找到有效的上传文件,请选择一个Excel文件后重试',
});
}
const file = req.files.file;
// 确保temp目录存在
const tempDir = path.join(__dirname, '../temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// 保存临时文件
const tempFilePath = path.join(tempDir, `${Date.now()}_${file.name}`);
try {
await file.mv(tempFilePath);
} catch (saveError) {
await t.rollback();
return res.status(500).json({
success: false,
return res.status(500).json({
success: false,
message: '文件保存失败',
error: `无法保存上传的文件: ${saveError.message}`
error: `无法保存上传的文件: ${saveError.message}`,
});
}
@@ -438,28 +441,29 @@ router.post('/import', async (req, res) => {
workbook = XLSX.readFile(tempFilePath);
} catch (readError) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: '文件解析失败',
error: `无法解析Excel文件: ${readError.message}`
error: `无法解析Excel文件: ${readError.message}`,
});
}
// 获取第一个工作表
if (!workbook.SheetNames.length) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: '文件格式错误',
error: 'Excel文件中没有找到工作表'
error: 'Excel文件中没有找到工作表',
});
}
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// 读取第一行作为列头
const headerRow = XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 0, limit: 1 })[0] || [];
const headerRow =
XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 0, limit: 1 })[0] || [];
// 定义列名映射(支持导入模板格式和导出文件格式)
const columnMapping = {
rackId: ['机柜ID(留空自动生成)', '机柜ID'],
@@ -467,9 +471,9 @@ router.post('/import', async (req, res) => {
roomName: ['所属机房名称', '所属机房'],
height: ['高度(U)', '机柜高度(U)'],
maxPower: ['最大功率(W)', '最大功耗(W)'],
status: ['状态']
status: ['状态'],
};
// 根据列头自动检测列索引映射
const columnIndexMap = {};
Object.keys(columnMapping).forEach(field => {
@@ -479,26 +483,26 @@ router.post('/import', async (req, res) => {
columnIndexMap[field] = index;
}
});
// 检查必需的列是否存在
const requiredColumns = ['name', 'roomName', 'height', 'maxPower', 'status'];
const missingColumns = requiredColumns.filter(col => columnIndexMap[col] === undefined);
if (missingColumns.length > 0) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: 'Excel列名格式不正确',
error: `缺少必需的列: ${missingColumns.join(', ')},请使用系统导出的文件或下载导入模板`
error: `缺少必需的列: ${missingColumns.join(', ')},请使用系统导出的文件或下载导入模板`,
});
}
// 转换为JSON格式
const rawData = XLSX.utils.sheet_to_json(worksheet, {
header: headerRow.map((h, i) => `col_${i}`),
range: 1,
blankrows: false
blankrows: false,
});
const jsonData = rawData.map(row => {
const item = {};
Object.keys(columnIndexMap).forEach(field => {
@@ -510,67 +514,80 @@ router.post('/import', async (req, res) => {
if (jsonData.length === 0) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: '没有找到有效数据',
error: 'Excel文件中没有找到可导入的数据行'
error: 'Excel文件中没有找到可导入的数据行',
});
}
// 状态值转换映射
const statusMapping = {
'启用': 'active', '在用': 'active', '停用': 'inactive',
'禁用': 'inactive', '维护中': 'maintenance',
'active': 'active', 'inactive': 'inactive', 'maintenance': 'maintenance'
启用: 'active',
在用: 'active',
停用: 'inactive',
禁用: 'inactive',
维护中: 'maintenance',
active: 'active',
inactive: 'inactive',
maintenance: 'maintenance',
};
const validStatuses = ['active', 'maintenance', 'inactive'];
const validationResults = [];
// 【优化1】批量查询机房信息(单次查询)
const allRooms = await Room.findAll({ transaction: t });
const roomNameToIdMap = new Map(allRooms.map(room => [room.name, room.roomId]));
const validRoomNames = new Set(roomNameToIdMap.keys());
// 【优化2】批量查询现有最大机柜ID(单次查询)
const maxRackResult = await Rack.findOne({
attributes: [[sequelize.fn('MAX', sequelize.cast(sequelize.fn('SUBSTR', sequelize.col('rackId'), 5), 'INTEGER')), 'maxNum']],
attributes: [
[
sequelize.fn(
'MAX',
sequelize.cast(sequelize.fn('SUBSTR', sequelize.col('rackId'), 5), 'INTEGER')
),
'maxNum',
],
],
where: {
rackId: {
[require('sequelize').Op.like]: 'RACK%'
}
[require('sequelize').Op.like]: 'RACK%',
},
},
transaction: t
transaction: t,
});
let maxNumber = maxRackResult?.get('maxNum') || 0;
// 处理数据
const processedData = jsonData.map((item, index) => {
const rowNumber = index + 2;
const rawStatus = String(item.status || '').trim();
const normalizedStatus = statusMapping[rawStatus] || rawStatus.toLowerCase();
const rawRackId = item.rackId ? String(item.rackId).trim() : '';
if (!rawRackId || rawRackId === '') {
maxNumber++;
return {
...item,
rackId: `RACK${String(maxNumber).padStart(3, '0')}`,
return {
...item,
rackId: `RACK${String(maxNumber).padStart(3, '0')}`,
status: normalizedStatus,
rowNumber
rowNumber,
};
}
return {
...item,
rackId: rawRackId,
return {
...item,
rackId: rawRackId,
status: normalizedStatus,
rowNumber
rowNumber,
};
});
// 验证数据
processedData.forEach((item) => {
processedData.forEach(item => {
const errors = [];
if (!/^[a-zA-Z0-9_-]+$/.test(item.rackId)) {
@@ -594,7 +611,7 @@ router.post('/import', async (req, res) => {
if (!item.status || !validStatuses.includes(item.status)) {
errors.push(`状态必须是以下值之一: ${validStatuses.join(', ')}`);
}
if (errors.length > 0) {
validationResults.push({ row: item.rowNumber, data: item, errors });
}
@@ -602,26 +619,26 @@ router.post('/import', async (req, res) => {
if (validationResults.length > 0) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: '数据验证失败',
error: `${validationResults.length} 行数据格式错误`,
details: validationResults
details: validationResults,
});
}
// 【优化3】批量查询已存在的机柜ID(单次查询)
const existingRacks = await Rack.findAll({
where: {
rackId: processedData.map(item => item.rackId)
rackId: processedData.map(item => item.rackId),
},
transaction: t
transaction: t,
});
const existingIds = new Set(existingRacks.map(rack => rack.rackId));
const newData = processedData.filter(item => !existingIds.has(item.rackId));
const duplicateCount = processedData.length - newData.length;
// 【优化4】批量插入数据
let createdCount = 0;
if (newData.length > 0) {
@@ -632,16 +649,16 @@ router.post('/import', async (req, res) => {
maxPower: item.maxPower,
status: item.status,
roomId: roomNameToIdMap.get(item.roomName.trim()),
currentPower: 0
currentPower: 0,
}));
const result = await Rack.bulkCreate(dataWithRoomId, {
transaction: t,
ignoreDuplicates: true
ignoreDuplicates: true,
});
createdCount = result.length;
}
// 提交事务
await t.commit();
@@ -655,7 +672,7 @@ router.post('/import', async (req, res) => {
duplicates: duplicateCount,
total: jsonData.length,
createdRacks,
skippedRacks
skippedRacks,
});
} finally {
// 删除临时文件
@@ -665,12 +682,12 @@ router.post('/import', async (req, res) => {
}
} catch (error) {
await t.rollback();
res.status(500).json({
success: false,
res.status(500).json({
success: false,
message: '服务器内部错误',
error: `导入过程中发生未知错误: ${error.message}`
error: `导入过程中发生未知错误: ${error.message}`,
});
}
});
module.exports = router;
module.exports = router;
+109 -67
View File
@@ -32,7 +32,10 @@ router.get('/', authMiddleware, async (req, res) => {
where,
limit,
offset,
order: [['sort', 'ASC'], ['createdAt', 'DESC']]
order: [
['sort', 'ASC'],
['createdAt', 'DESC'],
],
});
res.json({
@@ -41,14 +44,14 @@ router.get('/', authMiddleware, async (req, res) => {
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize),
roles
}
roles,
},
});
} catch (error) {
console.error('获取角色列表错误:', error);
res.status(500).json({
success: false,
message: '获取角色列表失败'
message: '获取角色列表失败',
});
}
});
@@ -57,18 +60,18 @@ router.get('/all', authMiddleware, async (req, res) => {
try {
const roles = await Role.findAll({
where: { status: 'active' },
order: [['sort', 'ASC']]
order: [['sort', 'ASC']],
});
res.json({
success: true,
data: roles
data: roles,
});
} catch (error) {
console.error('获取所有角色错误:', error);
res.status(500).json({
success: false,
message: '获取角色列表失败'
message: '获取角色列表失败',
});
}
});
@@ -80,13 +83,13 @@ router.get('/:roleId', authMiddleware, async (req, res) => {
if (!role) {
return res.status(404).json({
success: false,
message: '角色不存在'
message: '角色不存在',
});
}
const permissions = await Permission.findAll({
where: { status: 'active' },
order: [['sort', 'ASC']]
order: [['sort', 'ASC']],
});
res.json({
@@ -94,14 +97,14 @@ router.get('/:roleId', authMiddleware, async (req, res) => {
data: {
role,
permissions,
rolePermissions: role.permissions || []
}
rolePermissions: role.permissions || [],
},
});
} catch (error) {
console.error('获取角色详情错误:', error);
res.status(500).json({
success: false,
message: '获取角色详情失败'
message: '获取角色详情失败',
});
}
});
@@ -113,7 +116,7 @@ router.post('/', authMiddleware, async (req, res) => {
if (!roleName || !roleCode) {
return res.status(400).json({
success: false,
message: '角色名称和角色编码不能为空'
message: '角色名称和角色编码不能为空',
});
}
@@ -121,7 +124,7 @@ router.post('/', authMiddleware, async (req, res) => {
if (existingRole) {
return res.status(400).json({
success: false,
message: '角色编码已存在'
message: '角色编码已存在',
});
}
@@ -132,31 +135,33 @@ router.post('/', authMiddleware, async (req, res) => {
description,
permissions: permissions || [],
status: status || 'active',
sort: sort || 0
sort: sort || 0,
});
const permissionNames = permissions && permissions.length > 0
? permissions.join('、')
: '无';
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 }
});
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: '创建成功',
data: role
data: role,
});
} catch (error) {
console.error('创建角色错误:', error);
res.status(500).json({
success: false,
message: '创建角色失败'
message: '创建角色失败',
});
}
});
@@ -169,17 +174,27 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
if (!role) {
return res.status(404).json({
success: false,
message: '角色不存在'
message: '角色不存在',
});
}
const beforeState = role.toJSON();
if (roleName !== undefined) role.roleName = roleName;
if (description !== undefined) role.description = description;
if (permissions !== undefined) role.permissions = permissions;
if (status !== undefined) role.status = status;
if (sort !== undefined) role.sort = sort;
if (roleName !== undefined) {
role.roleName = roleName;
}
if (description !== undefined) {
role.description = description;
}
if (permissions !== undefined) {
role.permissions = permissions;
}
if (status !== undefined) {
role.status = status;
}
if (sort !== undefined) {
role.sort = sort;
}
await role.save();
@@ -201,21 +216,33 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
}
if (status !== undefined && beforeState.status !== status) {
const statusText = { active: '启用', inactive: '禁用' };
changedFields.status = { from: beforeState.status, to: status, fromText: statusText[beforeState.status], toText: statusText[status] };
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;
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('');
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}`
@@ -227,19 +254,23 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
beforeState,
afterState,
req,
metadata: { changedFields, oldRoleName: beforeState.roleName, oldPermissions: beforeState.permissions }
metadata: {
changedFields,
oldRoleName: beforeState.roleName,
oldPermissions: beforeState.permissions,
},
});
res.json({
success: true,
message: '更新成功',
data: role
data: role,
});
} catch (error) {
console.error('更新角色错误:', error);
res.status(500).json({
success: false,
message: '更新角色失败'
message: '更新角色失败',
});
}
});
@@ -251,14 +282,14 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
if (!role) {
return res.status(404).json({
success: false,
message: '角色不存在'
message: '角色不存在',
});
}
if (role.roleCode === 'admin') {
return res.status(400).json({
success: false,
message: '不能删除管理员角色'
message: '不能删除管理员角色',
});
}
@@ -266,7 +297,7 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
if (userCount > 0) {
return res.status(400).json({
success: false,
message: '该角色下有用户,不能删除'
message: '该角色下有用户,不能删除',
});
}
@@ -276,23 +307,27 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
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 }
});
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: '删除成功'
message: '删除成功',
});
} catch (error) {
console.error('删除角色错误:', error);
res.status(500).json({
success: false,
message: '删除角色失败'
message: '删除角色失败',
});
}
});
@@ -307,16 +342,23 @@ router.post('/init-roles', async (req, res) => {
description: '系统管理员,拥有所有权限',
permissions: ['*'],
status: 'active',
sort: 1
sort: 1,
},
{
roleId: 'role_operator',
roleName: '运维人员',
roleCode: 'operator',
description: '负责日常运维操作',
permissions: ['devices:read', 'devices:write', 'racks:read', 'rooms:read', 'consumables:read', 'consumables:write'],
permissions: [
'devices:read',
'devices:write',
'racks:read',
'rooms:read',
'consumables:read',
'consumables:write',
],
status: 'active',
sort: 2
sort: 2,
},
{
roleId: 'role_viewer',
@@ -325,8 +367,8 @@ router.post('/init-roles', async (req, res) => {
description: '仅能查看数据',
permissions: ['devices:read', 'racks:read', 'rooms:read', 'consumables:read'],
status: 'active',
sort: 3
}
sort: 3,
},
];
for (const roleData of defaultRoles) {
@@ -335,13 +377,13 @@ router.post('/init-roles', async (req, res) => {
res.json({
success: true,
message: '初始化角色成功'
message: '初始化角色成功',
});
} catch (error) {
console.error('初始化角色错误:', error);
res.status(500).json({
success: false,
message: '初始化角色失败'
message: '初始化角色失败',
});
}
});
+7 -7
View File
@@ -15,12 +15,12 @@ router.get('/', async (req, res) => {
const { count, rows } = await Room.findAndCountAll({
include: [{ model: Rack, attributes: ['rackId', 'name'] }],
offset: offset,
limit: pageSize
limit: pageSize,
});
res.json({
rooms: rows,
total: count
total: count,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -31,7 +31,7 @@ router.get('/', async (req, res) => {
router.get('/:roomId', async (req, res) => {
try {
const room = await Room.findByPk(req.params.roomId, {
include: Rack
include: Rack,
});
if (!room) {
return res.status(404).json({ error: '机房不存在' });
@@ -56,7 +56,7 @@ router.post('/', async (req, res) => {
router.put('/:roomId', validateBody(updateRoomSchema), async (req, res) => {
try {
const [updated] = await Room.update(req.body, {
where: { roomId: req.params.roomId }
where: { roomId: req.params.roomId },
});
if (updated) {
const updatedRoom = await Room.findByPk(req.params.roomId);
@@ -77,9 +77,9 @@ router.delete('/:roomId', async (req, res) => {
if (racks.length > 0) {
return res.status(400).json({ error: '该机房下有机柜,无法删除' });
}
const deleted = await Room.destroy({
where: { roomId: req.params.roomId }
where: { roomId: req.params.roomId },
});
if (deleted) {
res.status(204).json();
@@ -91,4 +91,4 @@ router.delete('/:roomId', async (req, res) => {
}
});
module.exports = router;
module.exports = router;
+40 -20
View File
@@ -32,26 +32,37 @@ router.get('/', async (req, res) => {
where: {
createdAt: {
[Op.gte]: dayStart,
[Op.lt]: dayEnd
}
}
})
[Op.lt]: dayEnd,
},
},
}),
});
}
const dayResults = await Promise.all(dayQueries.map(d => d.query));
const deviceTrendData = dayQueries.map((d, index) => ({
label: d.label,
value: dayResults[index]
value: dayResults[index],
}));
const [
totalDevices, faultDevices, totalRacks, rooms, totalUsers, activeTickets,
newDevicesThisWeek, newDevicesLastWeek,
faultDevicesThisWeek, faultDevicesLastWeek,
newUsersThisWeek, newUsersLastWeek,
newTicketsThisWeek, newTicketsLastWeek,
runningDevices, maintenanceDevices, offlineDevices
totalDevices,
faultDevices,
totalRacks,
rooms,
totalUsers,
activeTickets,
newDevicesThisWeek,
newDevicesLastWeek,
faultDevicesThisWeek,
faultDevicesLastWeek,
newUsersThisWeek,
newUsersLastWeek,
newTicketsThisWeek,
newTicketsLastWeek,
runningDevices,
maintenanceDevices,
offlineDevices,
] = await Promise.all([
Device.count(),
Device.count({ where: { status: 'fault' } }),
@@ -62,7 +73,9 @@ router.get('/', async (req, res) => {
Device.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
Device.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: oneWeekAgo } } }),
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
Device.count({
where: { status: 'fault', updatedAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } },
}),
User.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
User.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
Ticket.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
@@ -74,7 +87,9 @@ router.get('/', async (req, res) => {
let deviceGrowth = 0;
if (newDevicesLastWeek > 0) {
deviceGrowth = parseFloat(((newDevicesThisWeek - newDevicesLastWeek) / newDevicesLastWeek * 100).toFixed(1));
deviceGrowth = parseFloat(
(((newDevicesThisWeek - newDevicesLastWeek) / newDevicesLastWeek) * 100).toFixed(1)
);
} else if (newDevicesThisWeek > 0) {
deviceGrowth = 100;
} else {
@@ -83,7 +98,9 @@ router.get('/', async (req, res) => {
let faultTrend = 0;
if (faultDevicesLastWeek > 0) {
faultTrend = parseFloat(((faultDevicesThisWeek - faultDevicesLastWeek) / faultDevicesLastWeek * 100).toFixed(1));
faultTrend = parseFloat(
(((faultDevicesThisWeek - faultDevicesLastWeek) / faultDevicesLastWeek) * 100).toFixed(1)
);
} else if (faultDevicesThisWeek > 0) {
faultTrend = 100;
} else {
@@ -92,7 +109,9 @@ router.get('/', async (req, res) => {
let userGrowth = 0;
if (newUsersLastWeek > 0) {
userGrowth = parseFloat(((newUsersThisWeek - newUsersLastWeek) / newUsersLastWeek * 100).toFixed(1));
userGrowth = parseFloat(
(((newUsersThisWeek - newUsersLastWeek) / newUsersLastWeek) * 100).toFixed(1)
);
} else if (newUsersThisWeek > 0) {
userGrowth = 100;
} else {
@@ -101,16 +120,17 @@ router.get('/', async (req, res) => {
let ticketTrend = 0;
if (newTicketsLastWeek > 0) {
ticketTrend = parseFloat(((newTicketsThisWeek - newTicketsLastWeek) / newTicketsLastWeek * 100).toFixed(1));
ticketTrend = parseFloat(
(((newTicketsThisWeek - newTicketsLastWeek) / newTicketsLastWeek) * 100).toFixed(1)
);
} else if (newTicketsThisWeek > 0) {
ticketTrend = 100;
} else {
ticketTrend = 0;
}
const onlineRate = totalDevices > 0
? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1)
: 100;
const onlineRate =
totalDevices > 0 ? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1) : 100;
const totalRooms = rooms.length;
@@ -146,4 +166,4 @@ router.get('/', async (req, res) => {
}
});
module.exports = router;
module.exports = router;
+276 -109
View File
@@ -10,33 +10,194 @@ const { FRONTEND } = require('../config');
const initDefaultSettings = async () => {
const defaultSettings = [
// 全局配置
{ settingKey: 'site_name', settingValue: JSON.stringify('机柜管理系统'), settingType: 'string', category: 'general', description: '网站名称', isEditable: true },
{ settingKey: 'site_logo', settingValue: JSON.stringify(''), settingType: 'string', category: 'general', description: '网站Logo URL', isEditable: true },
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(60), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
{
settingKey: 'site_name',
settingValue: JSON.stringify('机柜管理系统'),
settingType: 'string',
category: 'general',
description: '网站名称',
isEditable: true,
},
{
settingKey: 'site_logo',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'general',
description: '网站Logo URL',
isEditable: true,
},
{
settingKey: 'timezone',
settingValue: JSON.stringify('Asia/Shanghai'),
settingType: 'string',
category: 'general',
description: '时区设置',
isEditable: true,
},
{
settingKey: 'date_format',
settingValue: JSON.stringify('YYYY-MM-DD'),
settingType: 'string',
category: 'general',
description: '日期格式',
isEditable: true,
},
{
settingKey: 'session_timeout',
settingValue: JSON.stringify(30),
settingType: 'number',
category: 'general',
description: '登录有效期(分钟)',
isEditable: true,
},
{
settingKey: 'idle_timeout',
settingValue: JSON.stringify(30),
settingType: 'number',
category: 'general',
description: '用户空闲超时时间(分钟)',
isEditable: true,
},
{
settingKey: 'idle_warning_time',
settingValue: JSON.stringify(60),
settingType: 'number',
category: 'general',
description: '空闲超时前警告时间(秒)',
isEditable: false,
},
{
settingKey: 'max_login_attempts',
settingValue: JSON.stringify(5),
settingType: 'number',
category: 'general',
description: '最大登录尝试次数',
isEditable: true,
},
{
settingKey: 'maintenance_mode',
settingValue: JSON.stringify(false),
settingType: 'boolean',
category: 'general',
description: '维护模式',
isEditable: true,
},
// 外观设置
{ settingKey: 'primary_color', settingValue: JSON.stringify('#667eea'), settingType: 'string', category: 'appearance', description: '主题主色调', isEditable: true },
{ settingKey: 'secondary_color', settingValue: JSON.stringify('#764ba2'), settingType: 'string', category: 'appearance', description: '主题辅助色调', isEditable: true },
{ settingKey: 'compact_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '紧凑模式', isEditable: true },
{ settingKey: 'sidebar_collapsed', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '侧边栏默认折叠', isEditable: true },
{ settingKey: 'table_row_height', settingValue: JSON.stringify('default'), settingType: 'string', category: 'appearance', description: '表格行高: small/default/middle/large', isEditable: true },
{ settingKey: 'animation_enabled', settingValue: JSON.stringify(true), settingType: 'boolean', category: 'appearance', description: '启用动画效果', isEditable: true },
{
settingKey: 'primary_color',
settingValue: JSON.stringify('#667eea'),
settingType: 'string',
category: 'appearance',
description: '主题主色调',
isEditable: true,
},
{
settingKey: 'secondary_color',
settingValue: JSON.stringify('#764ba2'),
settingType: 'string',
category: 'appearance',
description: '主题辅助色调',
isEditable: true,
},
{
settingKey: 'compact_mode',
settingValue: JSON.stringify(false),
settingType: 'boolean',
category: 'appearance',
description: '紧凑模式',
isEditable: true,
},
{
settingKey: 'sidebar_collapsed',
settingValue: JSON.stringify(false),
settingType: 'boolean',
category: 'appearance',
description: '侧边栏默认折叠',
isEditable: true,
},
{
settingKey: 'table_row_height',
settingValue: JSON.stringify('default'),
settingType: 'string',
category: 'appearance',
description: '表格行高: small/default/middle/large',
isEditable: true,
},
{
settingKey: 'animation_enabled',
settingValue: JSON.stringify(true),
settingType: 'boolean',
category: 'appearance',
description: '启用动画效果',
isEditable: true,
},
// 关于页面
{ settingKey: 'app_version', settingValue: JSON.stringify('1.0.0'), settingType: 'string', category: 'about', description: '应用版本', isEditable: false },
{ settingKey: 'company_name', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司/组织名称', isEditable: true },
{ settingKey: 'contact_email', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系邮箱', isEditable: true },
{ settingKey: 'contact_phone', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系电话', isEditable: true },
{ settingKey: 'company_address', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司地址', isEditable: true },
{ settingKey: 'system_description', settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'), settingType: 'string', category: 'about', description: '系统描述', isEditable: true },
{ settingKey: 'privacy_policy', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '隐私政策URL', isEditable: true },
{ settingKey: 'terms_of_service', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '服务条款URL', isEditable: true },
{
settingKey: 'app_version',
settingValue: JSON.stringify('1.0.0'),
settingType: 'string',
category: 'about',
description: '应用版本',
isEditable: false,
},
{
settingKey: 'company_name',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '公司/组织名称',
isEditable: true,
},
{
settingKey: 'contact_email',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '联系邮箱',
isEditable: true,
},
{
settingKey: 'contact_phone',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '联系电话',
isEditable: true,
},
{
settingKey: 'company_address',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '公司地址',
isEditable: true,
},
{
settingKey: 'system_description',
settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'),
settingType: 'string',
category: 'about',
description: '系统描述',
isEditable: true,
},
{
settingKey: 'privacy_policy',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '隐私政策URL',
isEditable: true,
},
{
settingKey: 'terms_of_service',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '服务条款URL',
isEditable: true,
},
];
let createdCount = 0;
@@ -60,7 +221,9 @@ const initDefaultSettings = async () => {
}
}
console.log(`系统设置初始化结果: 创建 ${createdCount} 个, 更新 ${updatedCount} 个, 失败 ${errorCount}`);
console.log(
`系统设置初始化结果: 创建 ${createdCount} 个, 更新 ${updatedCount} 个, 失败 ${errorCount}`
);
return { createdCount, updatedCount, errorCount };
};
@@ -75,12 +238,15 @@ router.get('/', async (req, res) => {
if (category) {
where.category = category;
}
const settings = await SystemSetting.findAll({
where,
order: [['category', 'ASC'], ['settingKey', 'ASC']]
order: [
['category', 'ASC'],
['settingKey', 'ASC'],
],
});
// 格式化返回数据
const formattedSettings = {};
settings.forEach(setting => {
@@ -90,10 +256,10 @@ router.get('/', async (req, res) => {
category: setting.category,
description: setting.description,
isEditable: setting.isEditable,
updatedAt: setting.updatedAt
updatedAt: setting.updatedAt,
};
});
res.json(formattedSettings);
} catch (error) {
res.status(500).json({ error: error.message });
@@ -116,7 +282,7 @@ router.get('/idle-timeout', async (req, res) => {
timeout: timeout * 60 * 1000, // 转换为毫秒
warningTime: fixedWarningTime * 1000, // 固定10秒(转换为毫秒)
timeoutMinutes: timeout,
warningTimeSeconds: fixedWarningTime
warningTimeSeconds: fixedWarningTime,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -128,11 +294,11 @@ router.get('/:key', async (req, res) => {
try {
const { key } = req.params;
const setting = await SystemSetting.findByPk(key);
if (!setting) {
return res.status(404).json({ error: '设置不存在' });
}
res.json({
key: setting.settingKey,
value: JSON.parse(setting.settingValue),
@@ -140,7 +306,7 @@ router.get('/:key', async (req, res) => {
category: setting.category,
description: setting.description,
isEditable: setting.isEditable,
updatedAt: setting.updatedAt
updatedAt: setting.updatedAt,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -152,17 +318,17 @@ router.put('/:key', async (req, res) => {
try {
const { key } = req.params;
const { value } = req.body;
const setting = await SystemSetting.findByPk(key);
if (!setting) {
return res.status(404).json({ error: '设置不存在' });
}
if (!setting.isEditable) {
return res.status(403).json({ error: '该设置不可编辑' });
}
// 验证值类型
let parsedValue = value;
if (setting.settingType === 'number') {
@@ -173,18 +339,18 @@ router.put('/:key', async (req, res) => {
} else if (setting.settingType === 'boolean') {
parsedValue = Boolean(value);
}
await setting.update({
settingValue: JSON.stringify(parsedValue)
settingValue: JSON.stringify(parsedValue),
});
res.json({
message: '设置更新成功',
setting: {
key: setting.settingKey,
value: parsedValue,
updatedAt: setting.updatedAt
}
updatedAt: setting.updatedAt,
},
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -195,28 +361,28 @@ router.put('/:key', async (req, res) => {
router.put('/', async (req, res) => {
try {
const { settings } = req.body;
if (!settings || typeof settings !== 'object') {
return res.status(400).json({ error: '请提供有效的设置对象' });
}
const updatedSettings = [];
const errors = [];
for (const [key, value] of Object.entries(settings)) {
try {
const setting = await SystemSetting.findByPk(key);
if (!setting) {
errors.push({ key, error: '设置不存在' });
continue;
}
if (!setting.isEditable) {
errors.push({ key, error: '该设置不可编辑' });
continue;
}
let parsedValue = value;
if (setting.settingType === 'number') {
parsedValue = Number(value);
@@ -227,21 +393,21 @@ router.put('/', async (req, res) => {
} else if (setting.settingType === 'boolean') {
parsedValue = Boolean(value);
}
await setting.update({
settingValue: JSON.stringify(parsedValue)
settingValue: JSON.stringify(parsedValue),
});
updatedSettings.push({ key, value: parsedValue });
} catch (error) {
errors.push({ key, error: error.message });
}
}
res.json({
message: `成功更新 ${updatedSettings.length} 个设置`,
updatedSettings,
errors: errors.length > 0 ? errors : undefined
errors: errors.length > 0 ? errors : undefined,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -253,11 +419,11 @@ router.post('/reset/:key', async (req, res) => {
try {
const { key } = req.params;
const setting = await SystemSetting.findByPk(key);
if (!setting) {
return res.status(404).json({ error: '设置不存在' });
}
const defaultValues = {
site_name: '机柜管理系统',
site_logo: '',
@@ -281,22 +447,22 @@ router.post('/reset/:key', async (req, res) => {
company_address: '',
system_description: '机柜管理系统 - 专业的数据中心设备管理解决方案',
privacy_policy: '',
terms_of_service: ''
terms_of_service: '',
};
const defaultValue = defaultValues[key];
if (defaultValue === undefined) {
return res.status(400).json({ error: '该设置没有默认值' });
}
await setting.update({
settingValue: JSON.stringify(defaultValue)
settingValue: JSON.stringify(defaultValue),
});
res.json({
message: '设置已重置为默认值',
key,
value: defaultValue
value: defaultValue,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -309,7 +475,7 @@ router.post('/backup', async (req, res) => {
// 获取系统设置中的备份路径
const backupPathSetting = await SystemSetting.findByPk('backup_path');
const backupPath = backupPathSetting ? JSON.parse(backupPathSetting.settingValue) : './backups';
// 解析备份目录路径
let backupDir;
if (backupPath.startsWith('/') || backupPath.match(/^[A-Za-z]:\//)) {
@@ -319,22 +485,22 @@ router.post('/backup', async (req, res) => {
// 相对路径,基于项目根目录
backupDir = path.join(__dirname, '../', backupPath);
}
// 确保备份目录存在
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFile = path.join(backupDir, `backup_${timestamp}.json`);
// 获取所有数据库数据
const Device = require('../models/Device');
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const Consumable = require('../models/Consumable');
const User = require('../models/User');
const backupData = {
timestamp: new Date().toISOString(),
version: '1.0.0',
@@ -344,38 +510,38 @@ router.post('/backup', async (req, res) => {
rooms: await Room.findAll({ raw: true }),
consumables: await Consumable.findAll({ raw: true }),
// 不包含敏感用户信息
users: await User.findAll({
users: await User.findAll({
attributes: ['userId', 'username', 'role', 'createdAt', 'updatedAt'],
raw: true
})
}
raw: true,
}),
},
};
// 写入备份文件
fs.writeFileSync(backupFile, JSON.stringify(backupData, null, 2));
// 更新最后备份时间
const lastBackupSetting = await SystemSetting.findByPk('last_backup_time');
if (lastBackupSetting) {
await lastBackupSetting.update({
settingValue: JSON.stringify(new Date().toISOString())
settingValue: JSON.stringify(new Date().toISOString()),
});
}
// 统计备份文件数量
const backupFiles = fs.readdirSync(backupDir).filter(f => f.startsWith('backup_'));
const countSetting = await SystemSetting.findByPk('backup_count');
if (countSetting) {
await countSetting.update({
settingValue: JSON.stringify(backupFiles.length)
settingValue: JSON.stringify(backupFiles.length),
});
}
res.json({
message: '备份成功',
backupFile: `${backupPath}/backup_${timestamp}.json`,
fileSize: fs.statSync(backupFile).size,
backupCount: backupFiles.length
backupCount: backupFiles.length,
});
} catch (error) {
console.error('备份失败:', error);
@@ -387,7 +553,7 @@ router.post('/backup', async (req, res) => {
const getBackupDir = async () => {
const backupPathSetting = await SystemSetting.findByPk('backup_path');
const backupPath = backupPathSetting ? JSON.parse(backupPathSetting.settingValue) : './backups';
let backupDir;
if (backupPath.startsWith('/') || backupPath.match(/^[A-Za-z]:\//)) {
// 绝对路径
@@ -396,7 +562,7 @@ const getBackupDir = async () => {
// 相对路径,基于项目根目录
backupDir = path.join(__dirname, '../', backupPath);
}
return backupDir;
};
@@ -404,12 +570,13 @@ const getBackupDir = async () => {
router.get('/backup/list', async (req, res) => {
try {
const backupDir = await getBackupDir();
if (!fs.existsSync(backupDir)) {
return res.json({ backups: [] });
}
const files = fs.readdirSync(backupDir)
const files = fs
.readdirSync(backupDir)
.filter(f => f.startsWith('backup_') && f.endsWith('.json'))
.map(f => {
const filePath = path.join(backupDir, f);
@@ -419,11 +586,11 @@ router.get('/backup/list', async (req, res) => {
path: `${path.basename(backupDir)}/${f}`,
size: stats.size,
createdAt: stats.birthtime,
modifiedAt: stats.mtime
modifiedAt: stats.mtime,
};
})
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
res.json({ backups: files });
} catch (error) {
res.status(500).json({ error: error.message });
@@ -434,50 +601,50 @@ router.get('/backup/list', async (req, res) => {
router.post('/backup/restore', async (req, res) => {
try {
const { filename } = req.body;
if (!filename) {
return res.status(400).json({ error: '请提供备份文件名' });
}
const backupDir = await getBackupDir();
const backupFile = path.join(backupDir, filename);
if (!fs.existsSync(backupFile)) {
return res.status(404).json({ error: '备份文件不存在' });
}
const backupData = JSON.parse(fs.readFileSync(backupFile, 'utf8'));
// 恢复数据
const { Device, Rack, Room, Consumable, User } = require('../models');
if (backupData.data.devices) {
for (const device of backupData.data.devices) {
await Device.upsert(device);
}
}
if (backupData.data.racks) {
for (const rack of backupData.data.racks) {
await Rack.upsert(rack);
}
}
if (backupData.data.rooms) {
for (const room of backupData.data.rooms) {
await Room.upsert(room);
}
}
if (backupData.data.consumables) {
for (const consumable of backupData.data.consumables) {
await Consumable.upsert(consumable);
}
}
res.json({
message: '恢复成功',
restoredAt: new Date().toISOString()
restoredAt: new Date().toISOString(),
});
} catch (error) {
console.error('恢复备份失败:', error);
@@ -491,13 +658,13 @@ router.delete('/backup/:filename', async (req, res) => {
const { filename } = req.params;
const backupDir = await getBackupDir();
const backupFile = path.join(backupDir, filename);
if (!fs.existsSync(backupFile)) {
return res.status(404).json({ error: '备份文件不存在' });
}
fs.unlinkSync(backupFile);
res.json({ message: '删除成功', filename });
} catch (error) {
res.status(500).json({ error: error.message });
@@ -510,11 +677,11 @@ router.get('/backup/download/:filename', async (req, res) => {
const { filename } = req.params;
const backupDir = await getBackupDir();
const backupFile = path.join(backupDir, filename);
if (!fs.existsSync(backupFile)) {
return res.status(404).json({ error: '备份文件不存在' });
}
res.download(backupFile, filename);
} catch (error) {
res.status(500).json({ error: error.message });
@@ -528,14 +695,14 @@ router.get('/system/info', async (req, res) => {
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const User = require('../models/User');
const [deviceCount, rackCount, roomCount, userCount] = await Promise.all([
Device.count(),
Rack.count(),
Room.count(),
User.count()
User.count(),
]);
res.json({
system: {
name: '机柜管理系统',
@@ -545,15 +712,15 @@ router.get('/system/info', async (req, res) => {
platform: process.platform,
arch: process.arch,
memoryUsage: process.memoryUsage(),
pid: process.pid
pid: process.pid,
},
statistics: {
devices: deviceCount,
racks: rackCount,
rooms: roomCount,
users: userCount
users: userCount,
},
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -590,7 +757,7 @@ router.post('/frontend/port/sync', async (req, res) => {
message: '前端端口配置已同步',
port,
configPath: '.frontend-port',
notice: '配置已更新,请重启前端服务以应用新端口'
notice: '配置已更新,请重启前端服务以应用新端口',
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -614,8 +781,8 @@ router.post('/frontend/restart', async (req, res) => {
after: {
pid: result.pid,
port: result.port,
url: `http://localhost:${result.port}`
}
url: `http://localhost:${result.port}`,
},
});
} catch (error) {
res.status(500).json({ error: error.message });
+99 -33
View File
@@ -14,7 +14,10 @@ router.get('/', async (req, res) => {
const categories = await FaultCategory.findAll({
where,
order: [['priority', 'ASC'], ['name', 'ASC']]
order: [
['priority', 'ASC'],
['name', 'ASC'],
],
});
res.json(categories);
@@ -31,8 +34,12 @@ router.get('/stats', async (req, res) => {
const where = {};
if (startDate || endDate) {
where.createdAt = {};
if (startDate) where.createdAt[Op.gte] = new Date(startDate);
if (endDate) where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
if (startDate) {
where.createdAt[Op.gte] = new Date(startDate);
}
if (endDate) {
where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
}
}
const stats = await Ticket.findAll({
@@ -40,16 +47,32 @@ router.get('/stats', async (req, res) => {
attributes: [
'faultCategory',
[require('sequelize').fn('COUNT', '*'), 'totalCount'],
[require('sequelize').sum(require('sequelize').case({
when: { status: 'completed' },
then: 1
}, 0)), 'completedCount'],
[require('sequelize').sum(require('sequelize').case({
when: { status: { [Op.ne]: 'completed' } },
then: 1
}, 0)), 'pendingCount']
[
require('sequelize').sum(
require('sequelize').case(
{
when: { status: 'completed' },
then: 1,
},
0
)
),
'completedCount',
],
[
require('sequelize').sum(
require('sequelize').case(
{
when: { status: { [Op.ne]: 'completed' } },
then: 1,
},
0
)
),
'pendingCount',
],
],
group: ['faultCategory']
group: ['faultCategory'],
});
res.json(stats);
@@ -72,15 +95,8 @@ router.get('/:categoryId', async (req, res) => {
router.post('/', async (req, res) => {
try {
const {
name,
description,
priority,
defaultPriority,
expectedDuration,
solutions,
isActive
} = req.body;
const { name, description, priority, defaultPriority, expectedDuration, solutions, isActive } =
req.body;
const existing = await FaultCategory.findOne({ where: { name } });
if (existing) {
@@ -98,7 +114,7 @@ router.post('/', async (req, res) => {
expectedDuration: expectedDuration ? parseInt(expectedDuration) : null,
solutions: solutions || [],
isSystem: false,
isActive: isActive !== false
isActive: isActive !== false,
});
res.status(201).json(category);
@@ -110,16 +126,66 @@ router.post('/', async (req, res) => {
router.post('/init', async (req, res) => {
try {
const defaultCategories = [
{ name: '系统故障', description: '操作系统、应用程序等系统软件的故障问题', priority: 1, defaultPriority: 'high' },
{ name: '硬件故障', description: '物理设备、服务器、存储等硬件设备的故障问题', priority: 2, defaultPriority: 'high' },
{ name: '网络故障', description: '网络连接、交换机、路由器等网络相关故障', priority: 3, defaultPriority: 'high' },
{ name: '软件故障', description: '应用程序错误、软件兼容性等问题', priority: 4, defaultPriority: 'medium' },
{ name: '安全事件', description: '安全漏洞、入侵检测、权限异常等安全问题', priority: 5, defaultPriority: 'urgent' },
{ name: '性能问题', description: '系统响应慢、资源利用率高等性能问题', priority: 6, defaultPriority: 'medium' },
{ name: '配置变更', description: '系统配置、软件配置等变更需求', priority: 7, defaultPriority: 'low' },
{ name: '例行维护', description: '定期维护、巡检、更新等计划性工作', priority: 8, defaultPriority: 'low' },
{ name: '数据问题', description: '数据错误、数据丢失、数据同步等数据相关问题', priority: 9, defaultPriority: 'high' },
{ name: '其他问题', description: '无法归类的其他问题', priority: 99, defaultPriority: 'medium' }
{
name: '系统故障',
description: '操作系统、应用程序等系统软件的故障问题',
priority: 1,
defaultPriority: 'high',
},
{
name: '硬件故障',
description: '物理设备、服务器、存储等硬件设备的故障问题',
priority: 2,
defaultPriority: 'high',
},
{
name: '网络故障',
description: '网络连接、交换机、路由器等网络相关故障',
priority: 3,
defaultPriority: 'high',
},
{
name: '软件故障',
description: '应用程序错误、软件兼容性等问题',
priority: 4,
defaultPriority: 'medium',
},
{
name: '安全事件',
description: '安全漏洞、入侵检测、权限异常等安全问题',
priority: 5,
defaultPriority: 'urgent',
},
{
name: '性能问题',
description: '系统响应慢、资源利用率高等性能问题',
priority: 6,
defaultPriority: 'medium',
},
{
name: '配置变更',
description: '系统配置、软件配置等变更需求',
priority: 7,
defaultPriority: 'low',
},
{
name: '例行维护',
description: '定期维护、巡检、更新等计划性工作',
priority: 8,
defaultPriority: 'low',
},
{
name: '数据问题',
description: '数据错误、数据丢失、数据同步等数据相关问题',
priority: 9,
defaultPriority: 'high',
},
{
name: '其他问题',
description: '无法归类的其他问题',
priority: 99,
defaultPriority: 'medium',
},
];
for (const cat of defaultCategories) {
@@ -132,7 +198,7 @@ router.post('/init', async (req, res) => {
expectedDuration: 120,
solutions: [],
isSystem: true,
isActive: true
isActive: true,
});
}
}
+7 -7
View File
@@ -5,7 +5,7 @@ const TicketField = require('../models/TicketField');
router.get('/', async (req, res) => {
try {
const fields = await TicketField.findAll({
order: [['order', 'ASC']]
order: [['order', 'ASC']],
});
res.json(fields);
} catch (error) {
@@ -37,7 +37,7 @@ router.post('/', async (req, res) => {
router.put('/:fieldId', async (req, res) => {
try {
const [updated] = await TicketField.update(req.body, {
where: { fieldId: req.params.fieldId }
where: { fieldId: req.params.fieldId },
});
if (updated) {
const updatedField = await TicketField.findByPk(req.params.fieldId);
@@ -53,7 +53,7 @@ router.put('/:fieldId', async (req, res) => {
router.delete('/:fieldId', async (req, res) => {
try {
const deleted = await TicketField.destroy({
where: { fieldId: req.params.fieldId }
where: { fieldId: req.params.fieldId },
});
if (deleted) {
res.status(204).json();
@@ -68,24 +68,24 @@ router.delete('/:fieldId', async (req, res) => {
router.post('/config', async (req, res) => {
try {
const fieldConfigs = req.body;
if (!Array.isArray(fieldConfigs)) {
return res.status(400).json({ error: '输入必须是数组' });
}
const updatedFields = [];
for (const config of fieldConfigs) {
const [updated] = await TicketField.update(
{ visible: config.visible },
{ where: { fieldName: config.fieldName } }
);
if (updated) {
const updatedField = await TicketField.findOne({ where: { fieldName: config.fieldName } });
updatedFields.push(updatedField);
}
}
res.json(updatedFields);
} catch (error) {
res.status(500).json({ error: error.message });
+90 -62
View File
@@ -17,41 +17,56 @@ router.get('/stats', async (req, res) => {
const where = {};
if (startDate || endDate) {
where.createdAt = {};
if (startDate) where.createdAt[Op.gte] = new Date(startDate);
if (endDate) where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
if (startDate) {
where.createdAt[Op.gte] = new Date(startDate);
}
if (endDate) {
where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
}
}
const Sequelize = require('sequelize');
const [total, statusStats, priorityStats, categoryStats, monthlyStats, deviceStats, dailyCreatedStats, dailyCompletedStats] = await Promise.all([
const [
total,
statusStats,
priorityStats,
categoryStats,
monthlyStats,
deviceStats,
dailyCreatedStats,
dailyCompletedStats,
] = await Promise.all([
Ticket.count({ where }),
Ticket.findAll({
where,
attributes: ['status', [Sequelize.fn('COUNT', '*'), 'count']],
group: ['status']
group: ['status'],
}),
Ticket.findAll({
where,
attributes: ['priority', [Sequelize.fn('COUNT', '*'), 'count']],
group: ['priority']
group: ['priority'],
}),
Ticket.findAll({
where,
attributes: ['faultCategory', [Sequelize.fn('COUNT', '*'), 'count']],
group: ['faultCategory']
group: ['faultCategory'],
}),
Ticket.findAll({
where,
attributes: [
[dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m')
: Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')),
'month'],
[Sequelize.fn('COUNT', '*'), 'count']
[
dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m')
: Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')),
'month',
],
[Sequelize.fn('COUNT', '*'), 'count'],
],
group: ['month'],
order: [['month', 'DESC']],
limit: 12
limit: 12,
}),
Ticket.findAll({
where,
@@ -59,39 +74,43 @@ router.get('/stats', async (req, res) => {
'deviceId',
'deviceName',
[Sequelize.fn('COUNT', '*'), 'count'],
[Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime']
[Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime'],
],
group: ['deviceId', 'deviceName'],
order: [[Sequelize.fn('COUNT', '*'), 'DESC']],
limit: 10
limit: 10,
}),
Ticket.findAll({
where,
attributes: [
[dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m-%d')
: Sequelize.fn('date', Sequelize.col('createdAt')),
'date'],
[Sequelize.fn('COUNT', '*'), 'created']
[
dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m-%d')
: Sequelize.fn('date', Sequelize.col('createdAt')),
'date',
],
[Sequelize.fn('COUNT', '*'), 'created'],
],
group: ['date'],
order: [['date', 'ASC']]
order: [['date', 'ASC']],
}),
Ticket.findAll({
where: {
...where,
status: 'completed'
status: 'completed',
},
attributes: [
[dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('updatedAt'), '%Y-%m-%d')
: Sequelize.fn('date', Sequelize.col('updatedAt')),
'date'],
[Sequelize.fn('COUNT', '*'), 'completed']
[
dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('updatedAt'), '%Y-%m-%d')
: Sequelize.fn('date', Sequelize.col('updatedAt')),
'date',
],
[Sequelize.fn('COUNT', '*'), 'completed'],
],
group: ['date'],
order: [['date', 'ASC']]
})
order: [['date', 'ASC']],
}),
]);
const statusData = statusStats.map(s => s.dataValues);
@@ -103,21 +122,21 @@ router.get('/stats', async (req, res) => {
const byStatus = statusData.map(item => ({
status: item.status,
count: item.count,
percentage: total > 0 ? (item.count / total * 100) : 0
percentage: total > 0 ? (item.count / total) * 100 : 0,
}));
const byPriority = priorityStats.map(p => ({
priority: p.dataValues.priority,
count: p.dataValues.count,
completed: 0,
avgTime: 0
avgTime: 0,
}));
const byCategory = categoryStats.map(c => ({
category: c.dataValues.faultCategory,
count: c.dataValues.count,
completed: 0,
avgTime: 0
avgTime: 0,
}));
const byDevice = deviceStats.map(d => ({
@@ -125,7 +144,7 @@ router.get('/stats', async (req, res) => {
deviceName: d.deviceName,
count: d.dataValues.count,
lastFaultTime: d.dataValues.lastFaultTime,
deviceType: ''
deviceType: '',
}));
const createdMap = {};
@@ -137,22 +156,24 @@ router.get('/stats', async (req, res) => {
completedMap[d.dataValues.date] = d.dataValues.completed;
});
const allDates = [...new Set([...Object.keys(createdMap), ...Object.keys(completedMap)])].sort();
const allDates = [
...new Set([...Object.keys(createdMap), ...Object.keys(completedMap)]),
].sort();
const trend = allDates.map(date => ({
date,
created: createdMap[date] || 0,
completed: completedMap[date] || 0,
closed: 0,
inProgress: 0,
pending: 0
pending: 0,
}));
const completedTickets = await Ticket.findAll({
where: {
...where,
status: 'completed'
status: 'completed',
},
attributes: ['createdAt', 'updatedAt']
attributes: ['createdAt', 'updatedAt'],
});
let avgProcessingTime = 0;
@@ -162,7 +183,11 @@ router.get('/stats', async (req, res) => {
const updated = new Date(ticket.updatedAt);
return sum + (updated - created);
}, 0);
avgProcessingTime = (totalProcessingTime / completedTickets.length / (1000 * 60 * 60)).toFixed(1);
avgProcessingTime = (
totalProcessingTime /
completedTickets.length /
(1000 * 60 * 60)
).toFixed(1);
}
res.json({
@@ -177,7 +202,10 @@ router.get('/stats', async (req, res) => {
byCategory,
byDevice,
trend,
monthlyStats: monthlyStats.map(m => ({ month: m.dataValues.month, count: m.dataValues.count }))
monthlyStats: monthlyStats.map(m => ({
month: m.dataValues.month,
count: m.dataValues.count,
})),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -203,7 +231,7 @@ router.get('/', async (req, res) => {
startDate,
endDate,
page = 1,
pageSize = 10
pageSize = 10,
} = req.query;
const offset = (page - 1) * pageSize;
@@ -216,7 +244,7 @@ router.get('/', async (req, res) => {
{ title: { [Op.like]: `%${keyword}%` } },
{ deviceName: { [Op.like]: `%${keyword}%` } },
{ serialNumber: { [Op.like]: `%${keyword}%` } },
{ description: { [Op.like]: `%${keyword}%` } }
{ description: { [Op.like]: `%${keyword}%` } },
];
}
@@ -260,18 +288,18 @@ router.get('/', async (req, res) => {
where,
include: [
{ model: User, as: 'reporter', attributes: ['userId', 'username'] },
{ model: Device, attributes: ['deviceId', 'name', 'type', 'model'] }
{ model: Device, attributes: ['deviceId', 'name', 'type', 'model'] },
],
order: [['createdAt', 'DESC']],
offset,
limit: parseInt(pageSize)
limit: parseInt(pageSize),
});
res.json({
total: count,
tickets: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -293,14 +321,14 @@ router.get('/:ticketId', async (req, res) => {
include: [
{
model: Room,
attributes: ['roomId', 'name']
}
]
}
]
attributes: ['roomId', 'name'],
},
],
},
],
},
{ model: TicketOperationRecord, as: 'operationRecords', order: [['createdAt', 'DESC']] }
]
{ model: TicketOperationRecord, as: 'operationRecords', order: [['createdAt', 'DESC']] },
],
});
if (!ticket) {
@@ -334,7 +362,7 @@ router.post('/', async (req, res) => {
expectedCompletionDate,
title,
attachments,
tags
tags,
} = req.body;
let device = null;
@@ -347,7 +375,7 @@ router.post('/', async (req, res) => {
if (deviceId) {
// 从设备列表选择
device = await Device.findByPk(deviceId, {
include: [{ model: require('../models/Rack') }]
include: [{ model: require('../models/Rack') }],
});
if (!device) {
return res.status(404).json({ error: '设备不存在' });
@@ -385,7 +413,7 @@ router.post('/', async (req, res) => {
location: ticketLocation,
attachments: attachments || [],
tags: tags || [],
status: 'pending'
status: 'pending',
});
// 创建操作记录
@@ -397,7 +425,7 @@ router.post('/', async (req, res) => {
operatorId: ticket.reporterId,
operatorName: ticket.reporterName,
operatorRole: 'user',
afterState: ticket.toJSON()
afterState: ticket.toJSON(),
});
res.status(201).json(ticket);
@@ -428,7 +456,7 @@ router.put('/:ticketId', async (req, res) => {
operatorName: operatorName || ticket.reporterName,
operatorRole: operatorRole || 'user',
beforeState,
afterState: ticket.toJSON()
afterState: ticket.toJSON(),
});
res.json(ticket);
@@ -468,7 +496,7 @@ router.put('/:ticketId/status', async (req, res) => {
operatorName: operatorName || ticket.reporterName,
operatorRole: operatorRole || 'user',
beforeState,
afterState: ticket.toJSON()
afterState: ticket.toJSON(),
});
res.json(ticket);
@@ -490,7 +518,7 @@ router.put('/:ticketId/process', async (req, res) => {
const beforeState = ticket.toJSON();
const updateData = {
status: 'in_progress',
resolution: solution
resolution: solution,
};
if (result === 'resolved') {
@@ -513,7 +541,7 @@ router.put('/:ticketId/process', async (req, res) => {
operatorName: operatorName || ticket.reporterName,
operatorRole: operatorRole || 'user',
beforeState,
afterState: ticket.toJSON()
afterState: ticket.toJSON(),
});
res.json(ticket);
@@ -535,7 +563,7 @@ router.post('/:ticketId/operations', async (req, res) => {
notes,
operatorId,
operatorName,
operatorRole
operatorRole,
} = req.body;
const ticket = await Ticket.findByPk(req.params.ticketId);
@@ -555,7 +583,7 @@ router.post('/:ticketId/operations', async (req, res) => {
notes,
operatorId,
operatorName,
operatorRole
operatorRole,
});
res.status(201).json(record);
@@ -569,7 +597,7 @@ router.get('/:ticketId/operations', async (req, res) => {
try {
const records = await TicketOperationRecord.findAll({
where: { ticketId: req.params.ticketId },
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
res.json(records);
@@ -613,7 +641,7 @@ router.post('/:ticketId/evaluate', async (req, res) => {
operatorId,
operatorName,
operatorRole: 'user',
notes: `评价: ${evaluation}, 星级: ${evaluationRating}`
notes: `评价: ${evaluation}, 星级: ${evaluationRating}`,
});
res.json(ticket);
+148 -111
View File
@@ -15,7 +15,7 @@ const generateId = () => {
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
};
const getWhereClause = (query) => {
const getWhereClause = query => {
const where = {};
if (query.username) {
@@ -33,10 +33,10 @@ const getWhereClause = (query) => {
return where;
};
const getUserRoleIds = async (userId) => {
const getUserRoleIds = async userId => {
const userRoles = await UserRole.findAll({
where: { UserId: userId },
attributes: ['RoleId']
attributes: ['RoleId'],
});
return userRoles.map(ur => ur.RoleId);
};
@@ -45,7 +45,13 @@ const { Op } = require('sequelize');
router.get('/', authMiddleware, async (req, res) => {
try {
const { page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE, username, status, realName } = req.query;
const {
page = 1,
pageSize = PAGINATION.DEFAULT_PAGE_SIZE,
username,
status,
realName,
} = req.query;
const offset = (parseInt(page) - 1) * parseInt(pageSize);
const limit = Math.min(parseInt(pageSize), PAGINATION.MAX_PAGE_SIZE);
@@ -56,20 +62,22 @@ router.get('/', authMiddleware, async (req, res) => {
attributes: { exclude: ['password'] },
limit,
offset,
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
if (users.length > 0) {
const userIds = users.map(u => u.userId);
const allUserRoles = await UserRole.findAll({
include: [{
model: Role,
where: { status: 'active' },
attributes: ['roleId', 'roleName', 'roleCode']
}],
include: [
{
model: Role,
where: { status: 'active' },
attributes: ['roleId', 'roleName', 'roleCode'],
},
],
where: {
UserId: { [Op.in]: userIds }
}
UserId: { [Op.in]: userIds },
},
});
const userRolesMap = {};
@@ -80,7 +88,7 @@ router.get('/', authMiddleware, async (req, res) => {
userRolesMap[ur.UserId].push({
roleId: ur.Role.roleId,
roleName: ur.Role.roleName,
roleCode: ur.Role.roleCode
roleCode: ur.Role.roleCode,
});
});
@@ -99,14 +107,14 @@ router.get('/', authMiddleware, async (req, res) => {
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize),
users
}
users,
},
});
} catch (error) {
console.error('获取用户列表错误:', error);
res.status(500).json({
success: false,
message: '获取用户列表失败'
message: '获取用户列表失败',
});
}
});
@@ -116,18 +124,18 @@ router.get('/all', authMiddleware, async (req, res) => {
const users = await User.findAll({
where: { status: 'active' },
attributes: ['userId', 'username', 'realName', 'email'],
order: [['realName', 'ASC']]
order: [['realName', 'ASC']],
});
res.json({
success: true,
data: users
data: users,
});
} catch (error) {
console.error('获取所有用户错误:', error);
res.status(500).json({
success: false,
message: '获取用户列表失败'
message: '获取用户列表失败',
});
}
});
@@ -135,39 +143,41 @@ router.get('/all', authMiddleware, async (req, res) => {
router.get('/:userId', authMiddleware, async (req, res) => {
try {
const user = await User.findByPk(req.params.userId, {
attributes: { exclude: ['password'] }
attributes: { exclude: ['password'] },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
const userRoles = await UserRole.findAll({
include: [{
model: Role,
where: { status: 'active' }
}],
where: { UserId: user.userId }
include: [
{
model: Role,
where: { status: 'active' },
},
],
where: { UserId: user.userId },
});
user.dataValues.roles = userRoles.map(ur => ({
roleId: ur.Role.roleId,
roleName: ur.Role.roleName,
roleCode: ur.Role.roleCode
roleCode: ur.Role.roleCode,
}));
res.json({
success: true,
data: user
data: user,
});
} catch (error) {
console.error('获取用户详情错误:', error);
res.status(500).json({
success: false,
message: '获取用户详情失败'
message: '获取用户详情失败',
});
}
});
@@ -179,7 +189,7 @@ router.post('/', authMiddleware, async (req, res) => {
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
message: '用户名和密码不能为空',
});
}
@@ -187,7 +197,7 @@ router.post('/', authMiddleware, async (req, res) => {
if (existingUser) {
return res.status(400).json({
success: false,
message: '用户名已存在'
message: '用户名已存在',
});
}
@@ -201,34 +211,41 @@ router.post('/', authMiddleware, async (req, res) => {
phone,
realName: realName || username,
status: status || 'active',
remark
remark,
});
if (roleIds && roleIds.length > 0) {
for (const roleId of roleIds) {
await UserRole.create({
UserId: user.userId,
RoleId: roleId
RoleId: roleId,
});
}
}
const roleNames = roleIds && roleIds.length > 0
? (await Role.findAll({ where: { roleId: { [Op.in]: roleIds } } })).map(r => r.roleName).join('、')
: '未分配角色';
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 }
});
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,
@@ -238,14 +255,14 @@ router.post('/', authMiddleware, async (req, res) => {
username: user.username,
email: user.email,
realName: user.realName,
status: user.status
}
status: user.status,
},
});
} catch (error) {
console.error('创建用户错误:', error);
res.status(500).json({
success: false,
message: '创建用户失败'
message: '创建用户失败',
});
}
});
@@ -258,7 +275,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
@@ -268,29 +285,39 @@ router.put('/:userId', authMiddleware, async (req, res) => {
phone: user.phone,
realName: user.realName,
status: user.status,
remark: user.remark
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 } }
where: { username, userId: { [Op.ne]: user.userId } },
});
if (existingUser) {
return res.status(400).json({
success: false,
message: '用户名已存在'
message: '用户名已存在',
});
}
user.username = username;
}
if (email !== undefined) user.email = email;
if (phone !== undefined) user.phone = phone;
if (realName !== undefined) user.realName = realName;
if (status !== undefined) user.status = status;
if (remark !== undefined) user.remark = remark;
if (email !== undefined) {
user.email = email;
}
if (phone !== undefined) {
user.phone = phone;
}
if (realName !== undefined) {
user.realName = realName;
}
if (status !== undefined) {
user.status = status;
}
if (remark !== undefined) {
user.remark = remark;
}
if (newPassword && newPassword.length >= PASSWORD_MIN_LENGTH) {
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
@@ -311,7 +338,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
for (const roleId of roleIds) {
await UserRole.create({
UserId: user.userId,
RoleId: roleId
RoleId: roleId,
});
}
@@ -321,7 +348,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
}
const updatedUser = await User.findByPk(req.params.userId, {
attributes: { exclude: ['password'] }
attributes: { exclude: ['password'] },
});
if (permissionChanged) {
@@ -332,7 +359,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
beforeState: { ...beforeState, roleIds: oldRoleIds, roleNames: oldRoleNames },
afterState: { ...beforeState, roleIds, roleNames: newRoleNames },
req,
metadata: { oldRoleIds, newRoleIds: roleIds, oldRoleNames, newRoleNames }
metadata: { oldRoleIds, newRoleIds: roleIds, oldRoleNames, newRoleNames },
});
} else {
const afterState = {
@@ -341,7 +368,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
phone: updatedUser.phone,
realName: updatedUser.realName,
status: updatedUser.status,
remark: updatedUser.remark
remark: updatedUser.remark,
};
const changedFields = {};
@@ -351,14 +378,20 @@ router.put('/:userId', authMiddleware, async (req, res) => {
}
}
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 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}`
@@ -370,20 +403,20 @@ router.put('/:userId', authMiddleware, async (req, res) => {
beforeState,
afterState,
req,
metadata: { changedFields }
metadata: { changedFields },
});
}
res.json({
success: true,
message: '更新成功',
data: updatedUser
data: updatedUser,
});
} catch (error) {
console.error('更新用户错误:', error);
res.status(500).json({
success: false,
message: '更新用户失败'
message: '更新用户失败',
});
}
});
@@ -396,14 +429,14 @@ router.put('/:userId/password', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (!newPassword || newPassword.length < PASSWORD_MIN_LENGTH) {
return res.status(400).json({
success: false,
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
});
}
@@ -412,13 +445,13 @@ router.put('/:userId/password', authMiddleware, async (req, res) => {
res.json({
success: true,
message: '密码重置成功'
message: '密码重置成功',
});
} catch (error) {
console.error('重置密码错误:', error);
res.status(500).json({
success: false,
message: '重置密码失败'
message: '重置密码失败',
});
}
});
@@ -430,14 +463,14 @@ router.delete('/:userId', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (user.userId === req.user.userId) {
return res.status(400).json({
success: false,
message: '不能删除当前登录用户'
message: '不能删除当前登录用户',
});
}
@@ -448,29 +481,33 @@ router.delete('/:userId', authMiddleware, async (req, res) => {
username: user.username,
email: user.email,
realName: user.realName,
status: user.status
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 }
});
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: '删除成功'
message: '删除成功',
});
} catch (error) {
console.error('删除用户错误:', error);
res.status(500).json({
success: false,
message: '删除用户失败'
message: '删除用户失败',
});
}
});
@@ -482,31 +519,31 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (!req.files || !req.files.avatar) {
return res.status(400).json({
success: false,
message: '请选择要上传的头像文件'
message: '请选择要上传的头像文件',
});
}
const avatarFile = req.files.avatar;
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(avatarFile.mimetype)) {
return res.status(400).json({
success: false,
message: '只支持 JPG、PNG、GIF 和 WebP 格式的图片'
message: '只支持 JPG、PNG、GIF 和 WebP 格式的图片',
});
}
if (avatarFile.size > FILE_UPLOAD.MAX_AVATAR_SIZE) {
return res.status(400).json({
success: false,
message: `图片大小不能超过 ${FILE_UPLOAD.MAX_AVATAR_SIZE / 1024 / 1024}MB`
message: `图片大小不能超过 ${FILE_UPLOAD.MAX_AVATAR_SIZE / 1024 / 1024}MB`,
});
}
@@ -535,13 +572,13 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => {
res.json({
success: true,
message: '头像上传成功',
data: { avatar: avatarUrl }
data: { avatar: avatarUrl },
});
} catch (error) {
console.error('上传头像错误:', error);
res.status(500).json({
success: false,
message: '上传头像失败'
message: '上传头像失败',
});
}
});
@@ -553,7 +590,7 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
@@ -569,13 +606,13 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
res.json({
success: true,
message: '头像删除成功'
message: '头像删除成功',
});
} catch (error) {
console.error('删除头像错误:', error);
res.status(500).json({
success: false,
message: '删除头像失败'
message: '删除头像失败',
});
}
});
@@ -587,14 +624,14 @@ router.put('/:userId/approve', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (user.status !== 'pending') {
return res.status(400).json({
success: false,
message: '该用户不在待审核状态'
message: '该用户不在待审核状态',
});
}
@@ -607,14 +644,14 @@ router.put('/:userId/approve', authMiddleware, async (req, res) => {
data: {
userId: user.userId,
username: user.username,
status: user.status
}
status: user.status,
},
});
} catch (error) {
console.error('审核用户错误:', error);
res.status(500).json({
success: false,
message: '审核用户失败'
message: '审核用户失败',
});
}
});
@@ -626,14 +663,14 @@ router.put('/:userId/reject', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (user.status !== 'pending') {
return res.status(400).json({
success: false,
message: '该用户不在待审核状态'
message: '该用户不在待审核状态',
});
}
@@ -646,14 +683,14 @@ router.put('/:userId/reject', authMiddleware, async (req, res) => {
data: {
userId: user.userId,
username: user.username,
status: user.status
}
status: user.status,
},
});
} catch (error) {
console.error('拒绝用户错误:', error);
res.status(500).json({
success: false,
message: '操作失败'
message: '操作失败',
});
}
});
+33 -23
View File
@@ -8,8 +8,8 @@ const { logDeviceOperation } = require('../utils/operationLogger');
async function generateWarehouseId() {
const warehouses = await Warehouse.findAll({
where: {
warehouseId: { [Op.like]: 'WH%' }
}
warehouseId: { [Op.like]: 'WH%' },
},
});
let maxNumber = 0;
@@ -38,7 +38,7 @@ router.get('/', async (req, res) => {
where[Op.or] = [
{ warehouseId: { [Op.like]: `%${keyword}%` } },
{ name: { [Op.like]: `%${keyword}%` } },
{ location: { [Op.like]: `%${keyword}%` } }
{ location: { [Op.like]: `%${keyword}%` } },
];
}
@@ -50,17 +50,17 @@ router.get('/', async (req, res) => {
where,
offset: parseInt(offset),
limit: parseInt(pageSize),
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
const warehousesWithCount = await Promise.all(
rows.map(async (warehouse) => {
rows.map(async warehouse => {
const deviceCount = await Device.count({
where: { warehouseId: warehouse.warehouseId, isIdle: true }
where: { warehouseId: warehouse.warehouseId, isIdle: true },
});
return {
...warehouse.toJSON(),
deviceCount
deviceCount,
};
})
);
@@ -69,7 +69,7 @@ router.get('/', async (req, res) => {
total: count,
warehouses: warehousesWithCount,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取库房列表失败:', error);
@@ -85,12 +85,12 @@ router.get('/:warehouseId', async (req, res) => {
}
const deviceCount = await Device.count({
where: { warehouseId: warehouse.warehouseId, isIdle: true }
where: { warehouseId: warehouse.warehouseId, isIdle: true },
});
res.json({
...warehouse.toJSON(),
deviceCount
deviceCount,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -111,14 +111,14 @@ router.get('/:warehouseId/devices', async (req, res) => {
where: { warehouseId: req.params.warehouseId, isIdle: true },
offset: parseInt(offset),
limit: parseInt(pageSize),
order: [['idleDate', 'DESC']]
order: [['idleDate', 'DESC']],
});
res.json({
total: count,
devices: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取库房设备失败:', error);
@@ -142,7 +142,7 @@ router.post('/', async (req, res) => {
location: location || '',
capacity: capacity || 100,
status: 'active',
description: description || ''
description: description || '',
});
await logDeviceOperation('create', `创建库房【${name}`, {
@@ -150,7 +150,7 @@ router.post('/', async (req, res) => {
targetName: name,
afterState: warehouse.toJSON(),
req,
metadata: { type: 'warehouse_create' }
metadata: { type: 'warehouse_create' },
});
res.status(201).json(warehouse);
@@ -169,11 +169,21 @@ router.put('/:warehouseId', async (req, res) => {
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;
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();
@@ -183,7 +193,7 @@ router.put('/:warehouseId', async (req, res) => {
beforeState,
afterState: warehouse.toJSON(),
req,
metadata: { type: 'warehouse_update' }
metadata: { type: 'warehouse_update' },
});
res.json(warehouse);
@@ -200,12 +210,12 @@ router.delete('/:warehouseId', async (req, res) => {
}
const idleDeviceCount = await Device.count({
where: { warehouseId: req.params.warehouseId, isIdle: true }
where: { warehouseId: req.params.warehouseId, isIdle: true },
});
if (idleDeviceCount > 0) {
return res.status(400).json({
error: `库房中还有 ${idleDeviceCount} 台空闲设备,请先处理后再删除`
error: `库房中还有 ${idleDeviceCount} 台空闲设备,请先处理后再删除`,
});
}
@@ -216,7 +226,7 @@ router.delete('/:warehouseId', async (req, res) => {
targetId: req.params.warehouseId,
targetName: warehouseName,
req,
metadata: { type: 'warehouse_delete' }
metadata: { type: 'warehouse_delete' },
});
res.json({ message: '库房删除成功' });