refactor: 统一代码风格并迁移至 ESLint 新配置
style(backend): 格式化模型文件代码 style(frontend): 调整组件代码格式 chore: 删除旧 ESLint 配置并添加新配置 refactor(backend): 重构模型定义语法 style: 统一箭头函数和对象属性简写
This commit is contained in:
+77
-61
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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 +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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
+451
-374
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+368
-211
@@ -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
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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: '初始化角色失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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: '操作失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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: '库房删除成功' });
|
||||
|
||||
Reference in New Issue
Block a user