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

style(backend): 格式化模型文件代码
style(frontend): 调整组件代码格式
chore: 删除旧 ESLint 配置并添加新配置
refactor(backend): 重构模型定义语法
style: 统一箭头函数和对象属性简写
This commit is contained in:
zhang1106
2026-03-27 19:12:16 +08:00
parent 6a8d4144ff
commit 63f0cb570e
166 changed files with 15483 additions and 11170 deletions
-20
View File
@@ -1,20 +0,0 @@
# 构建输出
dist/
build/
# 依赖
node_modules/
# 日志
logs/
*.log
# 数据库
*.db
*.sqlite
# 上传文件
uploads/
# 其他
.DS_Store
-27
View File
@@ -1,27 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
es2021: true,
jest: true
},
extends: [
'eslint:recommended',
'plugin:prettier/recommended'
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
'no-undef': 'error',
'no-unreachable': 'error',
'no-unused-expressions': 'error',
'eqeqeq': ['error', 'always'],
'curly': ['error', 'all'],
'no-var': 'error',
'prefer-const': 'error'
}
}
+4 -4
View File
@@ -9,7 +9,7 @@ module.exports = {
MAX_PAGE_SIZE: parseInt(process.env.MAX_PAGE_SIZE, 10) || 1000,
PAGE_SIZE_OPTIONS: [10, 20, 30, 50, 100],
},
FILE_UPLOAD: {
MAX_FILE_SIZE: (parseInt(process.env.MAX_FILE_SIZE_MB, 10) || 500) * 1024 * 1024,
MAX_AVATAR_SIZE: (parseInt(process.env.MAX_AVATAR_SIZE_MB, 10) || 5) * 1024 * 1024,
@@ -22,17 +22,17 @@ module.exports = {
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
],
},
RETRY: {
MAX_RETRIES: parseInt(process.env.MAX_RETRIES, 10) || 3,
RETRY_DELAY: parseInt(process.env.RETRY_DELAY, 10) || 1000,
},
TIMEOUT: {
API_TIMEOUT: parseInt(process.env.API_TIMEOUT, 10) || 30000,
DB_QUERY_TIMEOUT: parseInt(process.env.DB_QUERY_TIMEOUT, 10) || 30000,
},
FRONTEND: {
DEFAULT_PORT: parseInt(process.env.FRONTEND_PORT, 10) || 3000,
},
+5 -5
View File
@@ -5,15 +5,15 @@
module.exports = {
SALT_ROUNDS: parseInt(process.env.SALT_ROUNDS, 10) || 10,
MAX_LOGIN_ATTEMPTS: parseInt(process.env.MAX_LOGIN_ATTEMPTS, 10) || 5,
LOCK_TIME: (parseInt(process.env.LOCK_TIME_MINUTES, 10) || 30) * 60 * 1000,
TOKEN_EXPIRY: process.env.TOKEN_EXPIRY || '24h',
PASSWORD_MIN_LENGTH: parseInt(process.env.PASSWORD_MIN_LENGTH, 10) || 6,
USERNAME_MIN_LENGTH: parseInt(process.env.USERNAME_MIN_LENGTH, 10) || 3,
USERNAME_MAX_LENGTH: parseInt(process.env.USERNAME_MAX_LENGTH, 10) || 50,
};
+18 -7
View File
@@ -59,7 +59,6 @@ const createIndexes = async () => {
console.log(' ✓ rooms 表索引创建完成');
console.log('\n✅ 所有索引创建完成!');
} catch (error) {
console.error('创建索引失败:', error.message);
throw error;
@@ -69,8 +68,13 @@ const createIndexes = async () => {
const checkIndexes = async () => {
const queryInterface = sequelize.getQueryInterface();
const tables = [
'devices', 'users', 'consumables', 'consumable_records',
'consumable_logs', 'racks', 'rooms'
'devices',
'users',
'consumables',
'consumable_records',
'consumable_logs',
'racks',
'rooms',
];
console.log('\n检查现有索引...');
@@ -94,13 +98,19 @@ const dropIndexes = async () => {
try {
const indexDefinitions = [
{ table: 'devices', indexes: ['status', 'type', 'rackId', 'createdAt', 'status_type', 'name'] },
{
table: 'devices',
indexes: ['status', 'type', 'rackId', 'createdAt', 'status_type', 'name'],
},
{ table: 'users', indexes: ['status', 'username', 'email'] },
{ table: 'consumables', indexes: ['category', 'status', 'category_status'] },
{ table: 'consumable_records', indexes: ['consumableId', 'type', 'createdAt'] },
{ table: 'consumable_logs', indexes: ['consumableId', 'operationType', 'createdAt', 'consumableId_createdAt'] },
{
table: 'consumable_logs',
indexes: ['consumableId', 'operationType', 'createdAt', 'consumableId_createdAt'],
},
{ table: 'racks', indexes: ['roomId', 'status', 'roomId_status'] },
{ table: 'rooms', indexes: ['status', 'name'] }
{ table: 'rooms', indexes: ['status', 'name'] },
];
for (const def of indexDefinitions) {
@@ -126,7 +136,8 @@ module.exports = { createIndexes, checkIndexes, dropIndexes };
if (require.main === module) {
const command = process.argv[2] || 'create';
sequelize.authenticate()
sequelize
.authenticate()
.then(async () => {
console.log('数据库连接成功\n');
if (command === 'check') {
+7 -7
View File
@@ -20,11 +20,11 @@ if (DB_TYPE === 'mysql') {
logging: process.env.NODE_ENV === 'development' ? console.log : false,
// 连接池配置 - 提升并发处理能力
pool: {
max: 10, // 最大连接数
min: 2, // 最小连接数
max: 10, // 最大连接数
min: 2, // 最小连接数
acquire: 30000, // 获取连接超时时间(ms)
idle: 10000 // 连接空闲时间(ms)
}
idle: 10000, // 连接空闲时间(ms)
},
}
);
dbDialect = 'mysql';
@@ -38,10 +38,10 @@ if (DB_TYPE === 'mysql') {
max: 5,
min: 1,
acquire: 30000,
idle: 10000
}
idle: 10000,
},
});
dbDialect = 'sqlite';
}
module.exports = { sequelize, DB_TYPE, dbDialect };
module.exports = { sequelize, DB_TYPE, dbDialect };
+70
View File
@@ -0,0 +1,70 @@
import js from '@eslint/js';
import globals from 'globals';
export default [
{
ignores: [
'dist',
'build',
'node_modules',
'logs',
'*.log',
'*.db',
'*.sqlite',
'uploads',
'.DS_Store',
],
},
{
files: ['**/*.{js,mjs,cjs}'],
ignores: ['tests/**'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.node,
...globals.es2021,
},
},
plugins: {
'@eslint/js': js,
},
rules: {
...js.configs.recommended.rules,
'no-unused-vars': 'off',
'no-console': 'off',
'no-undef': 'off',
'no-unreachable': 'off',
'no-unused-expressions': 'off',
'no-prototype-builtins': 'off',
'no-useless-escape': 'off',
'no-fallthrough': 'off',
eqeqeq: 'off',
curly: 'off',
'no-var': 'error',
'prefer-const': 'off',
},
},
{
files: ['tests/**'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.jest,
describe: 'readonly',
it: 'readonly',
test: 'readonly',
expect: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
},
},
rules: {
'no-undef': 'off',
'no-unused-vars': 'off',
},
},
];
Binary file not shown.
+18 -16
View File
@@ -11,11 +11,13 @@ function generateSecret(length = 64) {
function parseEnvContent(content) {
const lines = content.split('\n');
const result = {};
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
const equalIndex = trimmed.indexOf('=');
if (equalIndex > 0) {
const key = trimmed.substring(0, equalIndex).trim();
@@ -23,26 +25,26 @@ function parseEnvContent(content) {
result[key] = value;
}
}
return result;
}
function stringifyEnvContent(envObj, originalContent) {
const lines = originalContent.split('\n');
const updatedLines = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
updatedLines.push(line);
continue;
}
const equalIndex = trimmed.indexOf('=');
if (equalIndex > 0) {
const key = trimmed.substring(0, equalIndex).trim();
if (key === 'JWT_SECRET' && envObj.hasOwnProperty('JWT_SECRET')) {
updatedLines.push(`JWT_SECRET=${envObj.JWT_SECRET}`);
} else {
@@ -52,29 +54,29 @@ function stringifyEnvContent(envObj, originalContent) {
updatedLines.push(line);
}
}
return updatedLines.join('\n');
}
function ensureJwtSecret() {
const currentSecret = process.env.JWT_SECRET;
if (currentSecret && currentSecret.length >= 32) {
console.log('✓ JWT_SECRET 已配置');
return currentSecret;
}
if (process.env.NODE_ENV === 'production') {
throw new Error(
'[致命错误] 生产环境未设置 JWT_SECRET 环境变量!\n' +
'请在服务器环境变量中设置强密钥(至少32位随机字符)。\n' +
'生成命令(PowerShell):-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | ForEach-Object { [char]$_ })'
'请在服务器环境变量中设置强密钥(至少32位随机字符)。\n' +
'生成命令(PowerShell):-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | ForEach-Object { [char]$_ })'
);
}
const newSecret = generateSecret(64);
console.log('⚠️ JWT_SECRET 未配置或长度不足,正在自动生成...');
try {
if (fs.existsSync(ENV_FILE_PATH)) {
const originalContent = fs.readFileSync(ENV_FILE_PATH, 'utf-8');
@@ -90,7 +92,7 @@ JWT_SECRET=${newSecret}
fs.writeFileSync(ENV_FILE_PATH, defaultContent, 'utf-8');
console.log('✓ JWT_SECRET 已自动生成并创建 .env 文件');
}
process.env.JWT_SECRET = newSecret;
return newSecret;
} catch (err) {
+25 -23
View File
@@ -11,7 +11,7 @@ const defaultDeviceFields = [
required: false,
order: 1,
visible: false,
isSystem: true
isSystem: true,
},
{
fieldName: 'name',
@@ -20,7 +20,7 @@ const defaultDeviceFields = [
required: true,
order: 2,
visible: true,
isSystem: true
isSystem: true,
},
{
fieldName: 'type',
@@ -35,8 +35,8 @@ const defaultDeviceFields = [
{ value: 'switch', label: '交换机' },
{ value: 'router', label: '路由器' },
{ value: 'storage', label: '存储设备' },
{ value: 'other', label: '其他设备' }
]
{ value: 'other', label: '其他设备' },
],
},
{
fieldName: 'model',
@@ -45,7 +45,7 @@ const defaultDeviceFields = [
required: false,
order: 4,
visible: true,
isSystem: true
isSystem: true,
},
{
fieldName: 'serialNumber',
@@ -54,7 +54,7 @@ const defaultDeviceFields = [
required: true,
order: 5,
visible: true,
isSystem: true
isSystem: true,
},
{
fieldName: 'rackId',
@@ -63,7 +63,7 @@ const defaultDeviceFields = [
required: true,
order: 6,
visible: true,
isSystem: true
isSystem: true,
},
{
fieldName: 'position',
@@ -72,7 +72,7 @@ const defaultDeviceFields = [
required: true,
order: 7,
visible: true,
isSystem: true
isSystem: true,
},
{
fieldName: 'height',
@@ -81,7 +81,7 @@ const defaultDeviceFields = [
required: true,
order: 8,
visible: true,
isSystem: true
isSystem: true,
},
{
fieldName: 'powerConsumption',
@@ -90,7 +90,7 @@ const defaultDeviceFields = [
required: true,
order: 9,
visible: true,
isSystem: true
isSystem: true,
},
{
fieldName: 'status',
@@ -105,8 +105,8 @@ const defaultDeviceFields = [
{ value: 'maintenance', label: '维护中' },
{ value: 'offline', label: '离线' },
{ value: 'fault', label: '故障' },
{ value: 'idle', label: '空闲' }
]
{ value: 'idle', label: '空闲' },
],
},
{
fieldName: 'purchaseDate',
@@ -115,7 +115,7 @@ const defaultDeviceFields = [
required: false,
order: 11,
visible: true,
isSystem: true
isSystem: true,
},
{
fieldName: 'warrantyExpiry',
@@ -124,7 +124,7 @@ const defaultDeviceFields = [
required: false,
order: 12,
visible: true,
isSystem: true
isSystem: true,
},
{
fieldName: 'ipAddress',
@@ -133,7 +133,7 @@ const defaultDeviceFields = [
required: false,
order: 13,
visible: true,
isSystem: false
isSystem: false,
},
{
fieldName: 'description',
@@ -142,7 +142,7 @@ const defaultDeviceFields = [
required: false,
order: 14,
visible: true,
isSystem: false
isSystem: false,
},
{
fieldName: 'brand',
@@ -151,8 +151,8 @@ const defaultDeviceFields = [
required: false,
order: 15,
visible: true,
isSystem: false
}
isSystem: false,
},
];
// 初始化设备字段
@@ -165,9 +165,9 @@ async function initDeviceFields() {
for (const field of defaultDeviceFields) {
// 检查字段是否已存在
const existingField = await DeviceField.findOne({
where: { fieldName: field.fieldName }
where: { fieldName: field.fieldName },
});
if (!existingField) {
// 只创建新字段,不更新已存在的字段
await DeviceField.create(field);
@@ -185,7 +185,9 @@ async function initDeviceFields() {
if (missingOptions.length > 0) {
const updatedOptions = [...existingField.options, ...missingOptions];
await existingField.update({ options: updatedOptions });
console.log(`补充缺失的 options: ${field.displayName},新增: ${missingOptions.map(o => o.label).join(', ')}`);
console.log(
`补充缺失的 options: ${field.displayName},新增: ${missingOptions.map(o => o.label).join(', ')}`
);
} else {
console.log(`跳过已存在字段: ${field.displayName}`);
}
@@ -194,7 +196,7 @@ async function initDeviceFields() {
}
}
}
console.log('设备字段配置初始化完成');
} catch (error) {
console.error('设备字段配置初始化失败:', error);
@@ -202,4 +204,4 @@ async function initDeviceFields() {
}
// 导出初始化函数
module.exports = initDeviceFields;
module.exports = initDeviceFields;
+17 -17
View File
@@ -8,7 +8,7 @@ const defaultTicketFields = [
fieldType: 'string',
required: true,
order: 1,
visible: true
visible: true,
},
{
fieldName: 'title',
@@ -16,7 +16,7 @@ const defaultTicketFields = [
fieldType: 'string',
required: true,
order: 2,
visible: true
visible: true,
},
{
fieldName: 'deviceName',
@@ -24,7 +24,7 @@ const defaultTicketFields = [
fieldType: 'string',
required: false,
order: 3,
visible: true
visible: true,
},
{
fieldName: 'serialNumber',
@@ -32,7 +32,7 @@ const defaultTicketFields = [
fieldType: 'string',
required: false,
order: 4,
visible: true
visible: true,
},
{
fieldName: 'faultCategory',
@@ -41,7 +41,7 @@ const defaultTicketFields = [
required: true,
order: 5,
visible: true,
options: []
options: [],
},
{
fieldName: 'priority',
@@ -54,8 +54,8 @@ const defaultTicketFields = [
{ value: 'low', label: '低' },
{ value: 'medium', label: '中' },
{ value: 'high', label: '高' },
{ value: 'urgent', label: '紧急' }
]
{ value: 'urgent', label: '紧急' },
],
},
{
fieldName: 'status',
@@ -68,8 +68,8 @@ const defaultTicketFields = [
{ value: 'pending', label: '待处理' },
{ value: 'in_progress', label: '处理中' },
{ value: 'completed', label: '已完成' },
{ value: 'closed', label: '已关闭' }
]
{ value: 'closed', label: '已关闭' },
],
},
{
fieldName: 'reporterName',
@@ -77,7 +77,7 @@ const defaultTicketFields = [
fieldType: 'string',
required: false,
order: 8,
visible: true
visible: true,
},
{
fieldName: 'createdAt',
@@ -85,7 +85,7 @@ const defaultTicketFields = [
fieldType: 'datetime',
required: false,
order: 9,
visible: true
visible: true,
},
{
fieldName: 'expectedCompletionDate',
@@ -93,7 +93,7 @@ const defaultTicketFields = [
fieldType: 'datetime',
required: false,
order: 10,
visible: true
visible: true,
},
{
fieldName: 'completionDate',
@@ -101,7 +101,7 @@ const defaultTicketFields = [
fieldType: 'datetime',
required: false,
order: 11,
visible: true
visible: true,
},
{
fieldName: 'description',
@@ -110,7 +110,7 @@ const defaultTicketFields = [
required: false,
order: 12,
visible: true,
placeholder: '请详细描述故障情况'
placeholder: '请详细描述故障情况',
},
{
fieldName: 'resolution',
@@ -119,7 +119,7 @@ const defaultTicketFields = [
required: false,
order: 13,
visible: true,
placeholder: '请输入解决方案'
placeholder: '请输入解决方案',
},
{
fieldName: 'location',
@@ -127,8 +127,8 @@ const defaultTicketFields = [
fieldType: 'string',
required: false,
order: 14,
visible: false
}
visible: false,
},
];
async function initializeTicketFields() {
+2 -2
View File
@@ -5,11 +5,11 @@ module.exports = {
'models/**/*.js',
'utils/**/*.js',
'routes/**/*.js',
'!models/ticketIndex.js'
'!models/ticketIndex.js',
],
coverageDirectory: 'coverage',
verbose: true,
testTimeout: 30000,
setupFiles: ['./tests/setupEnv.js'],
setupFilesAfterEnv: ['./tests/setup.js']
setupFilesAfterEnv: ['./tests/setup.js'],
};
+36 -34
View File
@@ -8,20 +8,20 @@ function getJwtSecret() {
if (!envSecret) {
throw new Error(
'[致命错误] 生产环境未设置 JWT_SECRET 环境变量!\n' +
'请在服务器环境变量中设置强密钥(至少32位随机字符)。\n' +
'生成命令(PowerShell):-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | ForEach-Object { [char]$_ })'
'请在服务器环境变量中设置强密钥(至少32位随机字符)。\n' +
'生成命令(PowerShell):-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | ForEach-Object { [char]$_ })'
);
}
if (envSecret.length < 32) {
throw new Error('[致命错误] 生产环境 JWT_SECRET 长度必须至少32位!当前长度:' + envSecret.length);
throw new Error(
'[致命错误] 生产环境 JWT_SECRET 长度必须至少32位!当前长度:' + envSecret.length
);
}
return envSecret;
}
if (!envSecret) {
throw new Error(
'[错误] JWT_SECRET 未配置,请检查 initConfig.js 是否正确执行'
);
throw new Error('[错误] JWT_SECRET 未配置,请检查 initConfig.js 是否正确执行');
}
return envSecret;
@@ -30,14 +30,16 @@ function getJwtSecret() {
const JWT_SECRET = getJwtSecret();
const TOKEN_EXPIRY = process.env.TOKEN_EXPIRY || '24h';
const getBrowserInfo = (userAgent) => {
const getBrowserInfo = userAgent => {
let device = 'Desktop';
let browser = 'Unknown';
let os = 'Unknown';
if (/Mobile|Android|iPhone|iPad|iPod/i.test(userAgent)) {
device = 'Mobile';
if (/iPad/i.test(userAgent)) device = 'Tablet';
if (/iPad/i.test(userAgent)) {
device = 'Tablet';
}
}
if (/Firefox/i.test(userAgent)) {
@@ -67,19 +69,19 @@ const getBrowserInfo = (userAgent) => {
return { device, browser, os };
};
const generateToken = (user) => {
const generateToken = user => {
return jwt.sign(
{
userId: user.userId,
username: user.username,
roleId: user.roleId
roleId: user.roleId,
},
JWT_SECRET,
{ expiresIn: TOKEN_EXPIRY }
);
};
const verifyToken = (token) => {
const verifyToken = token => {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
@@ -90,20 +92,20 @@ const verifyToken = (token) => {
const authMiddleware = async (req, res, next) => {
try {
let token = null;
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7);
}
if (!token && req.query.token) {
token = req.query.token;
}
if (!token) {
return res.status(401).json({
success: false,
message: '未提供认证令牌'
message: '未提供认证令牌',
});
}
@@ -112,7 +114,7 @@ const authMiddleware = async (req, res, next) => {
if (!decoded) {
return res.status(401).json({
success: false,
message: '令牌无效或已过期'
message: '令牌无效或已过期',
});
}
@@ -126,23 +128,23 @@ const authMiddleware = async (req, res, next) => {
userId: decoded.userId,
error: dbError.message,
stack: dbError.stack,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
return res.status(500).json({
success: false,
message: '数据库查询失败,请稍后重试'
message: '数据库查询失败,请稍后重试',
});
}
if (!user) {
console.warn('[认证中间件] 用户不存在:', {
userId: decoded.userId,
username: decoded.username,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
return res.status(401).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
@@ -150,11 +152,11 @@ const authMiddleware = async (req, res, next) => {
console.warn('[认证中间件] 账户已被锁定:', {
userId: user.userId,
username: user.username,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
return res.status(403).json({
success: false,
message: '账户已被锁定'
message: '账户已被锁定',
});
}
@@ -162,11 +164,11 @@ const authMiddleware = async (req, res, next) => {
console.warn('[认证中间件] 账户已禁用:', {
userId: user.userId,
username: user.username,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
return res.status(403).json({
success: false,
message: '账户已禁用'
message: '账户已禁用',
});
}
@@ -181,11 +183,11 @@ const authMiddleware = async (req, res, next) => {
url: req?.url,
method: req?.method,
ip: req?.ip,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
return res.status(500).json({
success: false,
message: '认证失败,请稍后重试'
message: '认证失败,请稍后重试',
});
}
};
@@ -193,11 +195,11 @@ const authMiddleware = async (req, res, next) => {
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
const decoded = verifyToken(token);
if (decoded) {
let user;
try {
@@ -207,27 +209,27 @@ const optionalAuth = async (req, res, next) => {
console.warn('[可选认证] 数据库查询失败:', {
userId: decoded.userId,
error: dbError.message,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
// 继续执行,不设置用户信息
next();
return;
}
if (user && user.status === 'active') {
req.user = decoded;
req.userModel = user;
}
}
}
next();
} catch (error) {
// 可选认证失败不影响主流程,仅记录日志
console.warn('[可选认证] 认证失败(已忽略):', {
error: error.message,
url: req?.url,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
next();
}
@@ -239,5 +241,5 @@ module.exports = {
authMiddleware,
optionalAuth,
JWT_SECRET,
TOKEN_EXPIRY
TOKEN_EXPIRY,
};
+16 -16
View File
@@ -1,27 +1,27 @@
const validate = (schema, source = 'body') => {
return async (req, res, next) => {
const data = source === 'query' ? req.query : req.body;
try {
let value;
if (schema.validate && typeof schema.validate === 'function') {
const result = schema.validate(data, {
abortEarly: false,
stripUnknown: true,
allowUnknown: source === 'query'
allowUnknown: source === 'query',
});
if (result && typeof result.then === 'function') {
value = await result;
} else if (result && result.error) {
const errorMessages = result.error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
message: detail.message,
}));
return res.status(400).json({
error: '参数验证失败',
details: errorMessages
details: errorMessages,
});
} else if (result && result.value !== undefined) {
value = result.value;
@@ -32,22 +32,22 @@ const validate = (schema, source = 'body') => {
value = await schema.validateAsync(data, {
abortEarly: false,
stripUnknown: true,
allowUnknown: source === 'query'
allowUnknown: source === 'query',
});
} else {
const result = schema.validate(data, {
abortEarly: false,
stripUnknown: true,
allowUnknown: source === 'query'
allowUnknown: source === 'query',
});
if (result.error) {
const errorMessages = result.error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
message: detail.message,
}));
return res.status(400).json({
error: '参数验证失败',
details: errorMessages
details: errorMessages,
});
}
value = result.value;
@@ -64,30 +64,30 @@ const validate = (schema, source = 'body') => {
if (error.details) {
const errorMessages = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
message: detail.message,
}));
return res.status(400).json({
error: '参数验证失败',
details: errorMessages
details: errorMessages,
});
}
console.error('验证中间件错误:', error);
return res.status(500).json({
error: '验证过程发生错误',
message: error.message
message: error.message,
});
}
};
};
const validateQuery = (schema) => validate(schema, 'query');
const validateQuery = schema => validate(schema, 'query');
const validateBody = (schema) => validate(schema, 'body');
const validateBody = schema => validate(schema, 'body');
module.exports = {
validate,
validateQuery,
validateBody
validateBody,
};
+90 -87
View File
@@ -1,94 +1,97 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const BackupLog = sequelize.define('BackupLog', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
const BackupLog = sequelize.define(
'BackupLog',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
logType: {
type: DataTypes.ENUM('auto', 'manual'),
allowNull: false,
comment: '备份类型:auto自动,manual手动',
},
status: {
type: DataTypes.ENUM('pending', 'running', 'success', 'failed'),
allowNull: false,
defaultValue: 'pending',
comment: '状态:pending待执行,running执行中,success成功,failed失败',
},
description: {
type: DataTypes.STRING,
allowNull: true,
comment: '备份描述',
},
backupType: {
type: DataTypes.ENUM('full', 'incremental'),
allowNull: true,
comment: '备份类型:full全量,incremental增量',
},
filename: {
type: DataTypes.STRING,
allowNull: true,
comment: '备份文件名',
},
filePath: {
type: DataTypes.STRING,
allowNull: true,
comment: '备份文件路径',
},
fileSize: {
type: DataTypes.BIGINT,
allowNull: true,
comment: '文件大小(字节)',
},
errorMessage: {
type: DataTypes.TEXT,
allowNull: true,
comment: '错误信息',
},
startTime: {
type: DataTypes.DATE,
allowNull: true,
comment: '开始时间',
},
endTime: {
type: DataTypes.DATE,
allowNull: true,
comment: '结束时间',
},
duration: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '执行时长(毫秒)',
},
includeFiles: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: '是否包含文件',
},
compressed: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: '是否压缩',
},
remoteUploads: {
type: DataTypes.JSON,
allowNull: true,
comment: '远端上传结果',
},
},
logType: {
type: DataTypes.ENUM('auto', 'manual'),
allowNull: false,
comment: '备份类型:auto自动,manual手动'
},
status: {
type: DataTypes.ENUM('pending', 'running', 'success', 'failed'),
allowNull: false,
defaultValue: 'pending',
comment: '状态:pending待执行,running执行中,success成功,failed失败'
},
description: {
type: DataTypes.STRING,
allowNull: true,
comment: '备份描述'
},
backupType: {
type: DataTypes.ENUM('full', 'incremental'),
allowNull: true,
comment: '备份类型:full全量,incremental增量'
},
filename: {
type: DataTypes.STRING,
allowNull: true,
comment: '备份文件名'
},
filePath: {
type: DataTypes.STRING,
allowNull: true,
comment: '备份文件路径'
},
fileSize: {
type: DataTypes.BIGINT,
allowNull: true,
comment: '文件大小(字节)'
},
errorMessage: {
type: DataTypes.TEXT,
allowNull: true,
comment: '错误信息'
},
startTime: {
type: DataTypes.DATE,
allowNull: true,
comment: '开始时间'
},
endTime: {
type: DataTypes.DATE,
allowNull: true,
comment: '结束时间'
},
duration: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '执行时长(毫秒)'
},
includeFiles: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: '是否包含文件'
},
compressed: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: '是否压缩'
},
remoteUploads: {
type: DataTypes.JSON,
allowNull: true,
comment: '远端上传结果'
{
tableName: 'backup_logs',
timestamps: true,
comment: '备份日志表',
indexes: [
{ fields: ['logType'] },
{ fields: ['status'] },
{ fields: ['createdAt'] },
{ fields: ['logType', 'createdAt'] },
],
}
}, {
tableName: 'backup_logs',
timestamps: true,
comment: '备份日志表',
indexes: [
{ fields: ['logType'] },
{ fields: ['status'] },
{ fields: ['createdAt'] },
{ fields: ['logType', 'createdAt'] }
]
});
);
module.exports = BackupLog;
+34 -33
View File
@@ -1,40 +1,41 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const Business = sequelize.define('Business', {
businessId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const Business = sequelize.define(
'Business',
{
businessId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
status: {
type: DataTypes.ENUM('active', 'offline'),
defaultValue: 'active',
},
offlineDate: {
type: DataTypes.DATE,
allowNull: true,
},
offlineReason: {
type: DataTypes.STRING,
allowNull: true,
},
},
name: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
status: {
type: DataTypes.ENUM('active', 'offline'),
defaultValue: 'active'
},
offlineDate: {
type: DataTypes.DATE,
allowNull: true
},
offlineReason: {
type: DataTypes.STRING,
allowNull: true
{
tableName: 'businesses',
timestamps: true,
indexes: [{ fields: ['status'] }, { fields: ['name'] }],
}
}, {
tableName: 'businesses',
timestamps: true,
indexes: [
{ fields: ['status'] },
{ fields: ['name'] }
]
});
);
module.exports = Business;
+62 -58
View File
@@ -2,66 +2,70 @@ const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const Device = require('./Device');
const Cable = sequelize.define('Cable', {
cableId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const Cable = sequelize.define(
'Cable',
{
cableId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
sourceDeviceId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: 'devices',
key: 'deviceId',
},
},
sourcePort: {
type: DataTypes.STRING,
allowNull: false,
},
targetDeviceId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: 'devices',
key: 'deviceId',
},
},
targetPort: {
type: DataTypes.STRING,
allowNull: false,
},
cableType: {
type: DataTypes.ENUM('ethernet', 'fiber', 'copper'),
defaultValue: 'ethernet',
allowNull: false,
},
cableLength: {
type: DataTypes.DECIMAL(5, 2),
allowNull: true,
},
status: {
type: DataTypes.ENUM('normal', 'fault', 'disconnected'),
defaultValue: 'normal',
allowNull: false,
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
},
sourceDeviceId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: 'devices',
key: 'deviceId'
}
},
sourcePort: {
type: DataTypes.STRING,
allowNull: false
},
targetDeviceId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: 'devices',
key: 'deviceId'
}
},
targetPort: {
type: DataTypes.STRING,
allowNull: false
},
cableType: {
type: DataTypes.ENUM('ethernet', 'fiber', 'copper'),
defaultValue: 'ethernet',
allowNull: false
},
cableLength: {
type: DataTypes.DECIMAL(5, 2),
allowNull: true
},
status: {
type: DataTypes.ENUM('normal', 'fault', 'disconnected'),
defaultValue: 'normal',
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
{
tableName: 'cables',
timestamps: true,
indexes: [
{ fields: ['sourceDeviceId'] },
{ fields: ['targetDeviceId'] },
{ fields: ['status'] },
{ fields: ['cableType'] },
{ fields: ['sourceDeviceId', 'targetDeviceId'] },
],
}
}, {
tableName: 'cables',
timestamps: true,
indexes: [
{ fields: ['sourceDeviceId'] },
{ fields: ['targetDeviceId'] },
{ fields: ['status'] },
{ fields: ['cableType'] },
{ fields: ['sourceDeviceId', 'targetDeviceId'] }
]
});
);
Cable.belongsTo(Device, { foreignKey: 'sourceDeviceId', as: 'sourceDevice' });
Cable.belongsTo(Device, { foreignKey: 'targetDeviceId', as: 'targetDevice' });
+78 -74
View File
@@ -1,81 +1,85 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const Consumable = sequelize.define('Consumable', {
consumableId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false
const Consumable = sequelize.define(
'Consumable',
{
consumableId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
category: {
type: DataTypes.STRING,
allowNull: false,
},
unit: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: '个',
},
currentStock: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
minStock: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 10,
},
maxStock: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '最大库存,0表示无限制',
},
unitPrice: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
defaultValue: 0,
},
supplier: {
type: DataTypes.STRING,
},
location: {
type: DataTypes.STRING,
comment: '存放位置',
},
description: {
type: DataTypes.TEXT,
},
snList: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'SN序列号列表,JSON数组格式',
},
status: {
type: DataTypes.STRING,
defaultValue: 'active',
},
version: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '乐观锁版本号',
},
},
name: {
type: DataTypes.STRING,
allowNull: false
},
category: {
type: DataTypes.STRING,
allowNull: false
},
unit: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: '个'
},
currentStock: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
minStock: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 10
},
maxStock: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '最大库存,0表示无限制'
},
unitPrice: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
defaultValue: 0
},
supplier: {
type: DataTypes.STRING
},
location: {
type: DataTypes.STRING,
comment: '存放位置'
},
description: {
type: DataTypes.TEXT
},
snList: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: 'SN序列号列表,JSON数组格式'
},
status: {
type: DataTypes.STRING,
defaultValue: 'active'
},
version: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '乐观锁版本号'
{
tableName: 'consumables',
timestamps: true,
indexes: [
{ fields: ['category'] },
{ fields: ['status'] },
{ fields: ['category', 'status'] },
{ fields: ['updatedAt'] },
],
}
}, {
tableName: 'consumables',
timestamps: true,
indexes: [
{ fields: ['category'] },
{ fields: ['status'] },
{ fields: ['category', 'status'] },
{ fields: ['updatedAt'] }
]
});
);
module.exports = Consumable;
+32 -28
View File
@@ -1,35 +1,39 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const ConsumableCategory = sequelize.define('ConsumableCategory', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
const ConsumableCategory = sequelize.define(
'ConsumableCategory',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
comment: '分类名称',
},
description: {
type: DataTypes.STRING,
comment: '分类描述',
},
sortOrder: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '排序顺序',
},
status: {
type: DataTypes.STRING,
defaultValue: 'active',
comment: '状态: active-启用, inactive-停用',
},
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
comment: '分类名称'
},
description: {
type: DataTypes.STRING,
comment: '分类描述'
},
sortOrder: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '排序顺序'
},
status: {
type: DataTypes.STRING,
defaultValue: 'active',
comment: '状态: active-启用, inactive-停用'
{
tableName: 'consumable_categories',
timestamps: true,
}
}, {
tableName: 'consumable_categories',
timestamps: true
});
);
module.exports = ConsumableCategory;
+109 -105
View File
@@ -1,112 +1,116 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const ConsumableLog = sequelize.define('ConsumableLog', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
const ConsumableLog = sequelize.define(
'ConsumableLog',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
consumableId: {
type: DataTypes.STRING,
allowNull: false,
comment: '耗材ID',
},
consumableName: {
type: DataTypes.STRING,
allowNull: false,
comment: '耗材名称',
},
operationType: {
type: DataTypes.ENUM('in', 'out', 'create', 'update', 'delete', 'adjust', 'import'),
allowNull: false,
comment: '操作类型',
},
quantity: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '变动数量(入库为正,出库为负)',
},
previousStock: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '操作前库存',
},
currentStock: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '操作后库存',
},
operator: {
type: DataTypes.STRING,
comment: '操作人',
},
reason: {
type: DataTypes.STRING,
comment: '操作原因',
},
notes: {
type: DataTypes.TEXT,
comment: '备注',
},
relatedId: {
type: DataTypes.STRING,
comment: '关联ID(如订单号、盘点ID等)',
},
isEditable: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: '是否可编辑(创建、导入的记录可编辑,系统生成的出入库记录不可编辑)',
},
originalLogId: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '原始日志ID(用于追踪修改历史链)',
},
modifiedBy: {
type: DataTypes.STRING,
allowNull: true,
comment: '修改人',
},
modifiedAt: {
type: DataTypes.DATE,
allowNull: true,
comment: '修改时间',
},
modificationReason: {
type: DataTypes.STRING,
allowNull: true,
comment: '修改原因',
},
isConsumableDeleted: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: '关联耗材是否已被删除',
},
consumableSnapshot: {
type: DataTypes.JSON,
allowNull: true,
comment: '耗材快照信息(分类、单位、供应商等),用于耗材删除后追溯',
},
snList: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: '本次操作的SN序列号列表',
},
},
consumableId: {
type: DataTypes.STRING,
allowNull: false,
comment: '耗材ID'
},
consumableName: {
type: DataTypes.STRING,
allowNull: false,
comment: '耗材名称'
},
operationType: {
type: DataTypes.ENUM('in', 'out', 'create', 'update', 'delete', 'adjust', 'import'),
allowNull: false,
comment: '操作类型'
},
quantity: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '变动数量(入库为正,出库为负)'
},
previousStock: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '操作前库存'
},
currentStock: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '操作后库存'
},
operator: {
type: DataTypes.STRING,
comment: '操作人'
},
reason: {
type: DataTypes.STRING,
comment: '操作原因'
},
notes: {
type: DataTypes.TEXT,
comment: '备注'
},
relatedId: {
type: DataTypes.STRING,
comment: '关联ID(如订单号、盘点ID等)'
},
isEditable: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: '是否可编辑(创建、导入的记录可编辑,系统生成的出入库记录不可编辑)'
},
originalLogId: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '原始日志ID(用于追踪修改历史链)'
},
modifiedBy: {
type: DataTypes.STRING,
allowNull: true,
comment: '修改人'
},
modifiedAt: {
type: DataTypes.DATE,
allowNull: true,
comment: '修改时间'
},
modificationReason: {
type: DataTypes.STRING,
allowNull: true,
comment: '修改原因'
},
isConsumableDeleted: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: '关联耗材是否已被删除'
},
consumableSnapshot: {
type: DataTypes.JSON,
allowNull: true,
comment: '耗材快照信息(分类、单位、供应商等),用于耗材删除后追溯'
},
snList: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: '本次操作的SN序列号列表'
{
tableName: 'consumable_logs',
timestamps: true,
comment: '耗材操作日志表',
indexes: [
{ fields: ['consumableId'] },
{ fields: ['operationType'] },
{ fields: ['createdAt'] },
{ fields: ['consumableId', 'createdAt'] },
{ fields: ['originalLogId'] },
{ fields: ['isEditable'] },
{ fields: ['isConsumableDeleted'] },
],
}
}, {
tableName: 'consumable_logs',
timestamps: true,
comment: '耗材操作日志表',
indexes: [
{ fields: ['consumableId'] },
{ fields: ['operationType'] },
{ fields: ['createdAt'] },
{ fields: ['consumableId', 'createdAt'] },
{ fields: ['originalLogId'] },
{ fields: ['isEditable'] },
{ fields: ['isConsumableDeleted'] }
]
});
);
module.exports = ConsumableLog;
+79 -75
View File
@@ -5,82 +5,86 @@ const { sequelize } = require('../db');
* 耗材操作日志归档表
* 用于存储被删除耗材的历史操作记录
*/
const ConsumableLogArchive = sequelize.define('ConsumableLogArchive', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
const ConsumableLogArchive = sequelize.define(
'ConsumableLogArchive',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
archiveId: {
type: DataTypes.STRING,
allowNull: false,
comment: '归档记录唯一标识',
},
consumableId: {
type: DataTypes.STRING,
allowNull: false,
comment: '被删除的耗材ID',
},
consumableName: {
type: DataTypes.STRING,
allowNull: false,
comment: '耗材名称',
},
consumableSnapshot: {
type: DataTypes.JSON,
allowNull: true,
comment: '耗材快照信息',
},
totalOperations: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '操作记录总数',
},
firstOperationAt: {
type: DataTypes.DATE,
comment: '首次操作时间',
},
lastOperationAt: {
type: DataTypes.DATE,
comment: '最后操作时间',
},
totalInQuantity: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '总入库数量',
},
totalOutQuantity: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '总出库数量',
},
finalStock: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '删除时库存',
},
deletedBy: {
type: DataTypes.STRING,
comment: '删除人',
},
deletedAt: {
type: DataTypes.DATE,
comment: '删除时间',
},
deleteReason: {
type: DataTypes.STRING,
comment: '删除原因',
},
},
archiveId: {
type: DataTypes.STRING,
allowNull: false,
comment: '归档记录唯一标识'
},
consumableId: {
type: DataTypes.STRING,
allowNull: false,
comment: '被删除的耗材ID'
},
consumableName: {
type: DataTypes.STRING,
allowNull: false,
comment: '耗材名称'
},
consumableSnapshot: {
type: DataTypes.JSON,
allowNull: true,
comment: '耗材快照信息'
},
totalOperations: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '操作记录总数'
},
firstOperationAt: {
type: DataTypes.DATE,
comment: '首次操作时间'
},
lastOperationAt: {
type: DataTypes.DATE,
comment: '最后操作时间'
},
totalInQuantity: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '总入库数量'
},
totalOutQuantity: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '总出库数量'
},
finalStock: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '删除时库存'
},
deletedBy: {
type: DataTypes.STRING,
comment: '删除人'
},
deletedAt: {
type: DataTypes.DATE,
comment: '删除时间'
},
deleteReason: {
type: DataTypes.STRING,
comment: '删除原因'
{
tableName: 'consumable_log_archives',
timestamps: true,
comment: '耗材操作日志归档表',
indexes: [
{ fields: ['consumableId'] },
{ fields: ['archiveId'] },
{ fields: ['deletedAt'] },
{ fields: ['consumableId', 'deletedAt'] },
],
}
}, {
tableName: 'consumable_log_archives',
timestamps: true,
comment: '耗材操作日志归档表',
indexes: [
{ fields: ['consumableId'] },
{ fields: ['archiveId'] },
{ fields: ['deletedAt'] },
{ fields: ['consumableId', 'deletedAt'] }
]
});
);
module.exports = ConsumableLogArchive;
+61 -61
View File
@@ -2,75 +2,75 @@ const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const Consumable = require('./Consumable');
const ConsumableRecord = sequelize.define('ConsumableRecord', {
recordId: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
allowNull: false
const ConsumableRecord = sequelize.define(
'ConsumableRecord',
{
recordId: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
allowNull: false,
},
consumableId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: Consumable,
key: 'consumableId',
},
},
type: {
type: DataTypes.ENUM('in', 'out'),
allowNull: false,
},
quantity: {
type: DataTypes.INTEGER,
allowNull: false,
},
previousStock: {
type: DataTypes.INTEGER,
allowNull: false,
},
currentStock: {
type: DataTypes.INTEGER,
allowNull: false,
},
operator: {
type: DataTypes.STRING,
allowNull: false,
},
reason: {
type: DataTypes.STRING,
},
recipient: {
type: DataTypes.STRING,
},
notes: {
type: DataTypes.TEXT,
},
snList: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: '本次操作的SN序列号列表',
},
},
consumableId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: Consumable,
key: 'consumableId'
}
},
type: {
type: DataTypes.ENUM('in', 'out'),
allowNull: false
},
quantity: {
type: DataTypes.INTEGER,
allowNull: false
},
previousStock: {
type: DataTypes.INTEGER,
allowNull: false
},
currentStock: {
type: DataTypes.INTEGER,
allowNull: false
},
operator: {
type: DataTypes.STRING,
allowNull: false
},
reason: {
type: DataTypes.STRING
},
recipient: {
type: DataTypes.STRING
},
notes: {
type: DataTypes.TEXT
},
snList: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: [],
comment: '本次操作的SN序列号列表'
{
tableName: 'consumable_records',
timestamps: true,
indexes: [{ fields: ['consumableId'] }, { fields: ['type'] }, { fields: ['createdAt'] }],
}
}, {
tableName: 'consumable_records',
timestamps: true,
indexes: [
{ fields: ['consumableId'] },
{ fields: ['type'] },
{ fields: ['createdAt'] }
]
});
);
ConsumableRecord.belongsTo(Consumable, {
ConsumableRecord.belongsTo(Consumable, {
foreignKey: 'consumableId',
as: 'consumable',
onDelete: 'CASCADE'
onDelete: 'CASCADE',
});
Consumable.hasMany(ConsumableRecord, {
Consumable.hasMany(ConsumableRecord, {
foreignKey: 'consumableId',
as: 'records',
onDelete: 'CASCADE'
onDelete: 'CASCADE',
});
module.exports = ConsumableRecord;
+101 -97
View File
@@ -3,105 +3,109 @@ const { sequelize } = require('../db');
const Rack = require('./Rack');
const Warehouse = require('./Warehouse');
const Device = sequelize.define('Device', {
deviceId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const Device = sequelize.define(
'Device',
{
deviceId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: {
type: DataTypes.STRING,
allowNull: true,
},
type: {
type: DataTypes.STRING,
allowNull: true,
},
model: {
type: DataTypes.STRING,
allowNull: true,
},
serialNumber: {
type: DataTypes.STRING,
allowNull: true,
unique: true,
},
rackId: {
type: DataTypes.STRING,
allowNull: true,
},
position: {
type: DataTypes.INTEGER,
allowNull: true,
},
height: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 1,
},
powerConsumption: {
type: DataTypes.FLOAT,
allowNull: true,
defaultValue: 0,
},
status: {
type: DataTypes.STRING,
defaultValue: 'offline',
},
isIdle: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
idleDate: {
type: DataTypes.DATE,
allowNull: true,
},
idleReason: {
type: DataTypes.STRING,
allowNull: true,
},
warehouseId: {
type: DataTypes.STRING,
allowNull: true,
},
sourceType: {
type: DataTypes.ENUM('rack', 'warehouse'),
defaultValue: 'rack',
},
purchaseDate: {
type: DataTypes.DATE,
allowNull: true,
},
warrantyExpiry: {
type: DataTypes.DATE,
allowNull: true,
},
ipAddress: {
type: DataTypes.STRING,
allowNull: true,
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
customFields: {
type: DataTypes.JSON,
defaultValue: {},
allowNull: true,
},
},
name: {
type: DataTypes.STRING,
allowNull: true
},
type: {
type: DataTypes.STRING,
allowNull: true
},
model: {
type: DataTypes.STRING,
allowNull: true
},
serialNumber: {
type: DataTypes.STRING,
allowNull: true,
unique: true
},
rackId: {
type: DataTypes.STRING,
allowNull: true
},
position: {
type: DataTypes.INTEGER,
allowNull: true
},
height: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 1
},
powerConsumption: {
type: DataTypes.FLOAT,
allowNull: true,
defaultValue: 0
},
status: {
type: DataTypes.STRING,
defaultValue: 'offline'
},
isIdle: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
idleDate: {
type: DataTypes.DATE,
allowNull: true
},
idleReason: {
type: DataTypes.STRING,
allowNull: true
},
warehouseId: {
type: DataTypes.STRING,
allowNull: true
},
sourceType: {
type: DataTypes.ENUM('rack', 'warehouse'),
defaultValue: 'rack'
},
purchaseDate: {
type: DataTypes.DATE,
allowNull: true
},
warrantyExpiry: {
type: DataTypes.DATE,
allowNull: true
},
ipAddress: {
type: DataTypes.STRING,
allowNull: true
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
customFields: {
type: DataTypes.JSON,
defaultValue: {},
allowNull: true
{
tableName: 'devices',
timestamps: true,
indexes: [
{ fields: ['status'] },
{ fields: ['type'] },
{ fields: ['rackId'] },
{ fields: ['createdAt'] },
{ fields: ['status', 'type'] },
{ fields: ['name'] },
],
}
}, {
tableName: 'devices',
timestamps: true,
indexes: [
{ fields: ['status'] },
{ fields: ['type'] },
{ fields: ['rackId'] },
{ fields: ['createdAt'] },
{ fields: ['status', 'type'] },
{ fields: ['name'] }
]
});
);
Device.belongsTo(Rack, { foreignKey: 'rackId' });
Device.belongsTo(Warehouse, { foreignKey: 'warehouseId' });
+39 -35
View File
@@ -3,52 +3,56 @@ const { sequelize } = require('../db');
const Device = require('./Device');
const Business = require('./Business');
const DeviceBusiness = sequelize.define('DeviceBusiness', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
const DeviceBusiness = sequelize.define(
'DeviceBusiness',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
deviceId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: Device,
key: 'deviceId',
},
},
businessId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: Business,
key: 'businessId',
},
},
isPrimary: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
},
deviceId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: Device,
key: 'deviceId'
}
},
businessId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: Business,
key: 'businessId'
}
},
isPrimary: {
type: DataTypes.BOOLEAN,
defaultValue: false
{
tableName: 'device_business',
timestamps: true,
indexes: [
{ fields: ['deviceId'] },
{ fields: ['businessId'] },
{ unique: true, fields: ['deviceId', 'businessId'] },
],
}
}, {
tableName: 'device_business',
timestamps: true,
indexes: [
{ fields: ['deviceId'] },
{ fields: ['businessId'] },
{ unique: true, fields: ['deviceId', 'businessId'] }
]
});
);
Device.belongsToMany(Business, {
through: DeviceBusiness,
foreignKey: 'deviceId',
otherKey: 'businessId'
otherKey: 'businessId',
});
Business.belongsToMany(Device, {
through: DeviceBusiness,
foreignKey: 'businessId',
otherKey: 'deviceId'
otherKey: 'deviceId',
});
DeviceBusiness.belongsTo(Business, { foreignKey: 'businessId' });
+53 -49
View File
@@ -1,55 +1,59 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const DeviceField = sequelize.define('DeviceField', {
fieldId: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
const DeviceField = sequelize.define(
'DeviceField',
{
fieldId: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
fieldName: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
displayName: {
type: DataTypes.STRING,
allowNull: false,
},
fieldType: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'string',
},
required: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
options: {
type: DataTypes.JSON,
allowNull: true,
},
order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
visible: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
isSystem: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: '是否为系统字段,系统字段不可删除',
},
},
fieldName: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
displayName: {
type: DataTypes.STRING,
allowNull: false
},
fieldType: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'string'
},
required: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
options: {
type: DataTypes.JSON,
allowNull: true
},
order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
visible: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
},
isSystem: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: '是否为系统字段,系统字段不可删除'
{
tableName: 'deviceFields',
timestamps: true,
}
}, {
tableName: 'deviceFields',
timestamps: true
});
);
module.exports = DeviceField;
module.exports = DeviceField;
+64 -60
View File
@@ -1,68 +1,72 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const DevicePort = sequelize.define('DevicePort', {
portId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
},
deviceId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: 'devices',
key: 'deviceId'
}
},
nicId: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: 'network_cards',
key: 'nicId'
const DevicePort = sequelize.define(
'DevicePort',
{
portId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
deviceId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: 'devices',
key: 'deviceId',
},
},
nicId: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: 'network_cards',
key: 'nicId',
},
comment: '所属网卡ID,可为空(向后兼容)',
},
portName: {
type: DataTypes.STRING,
allowNull: false,
},
portType: {
type: DataTypes.ENUM('RJ45', 'SFP', 'SFP+', 'SFP28', 'QSFP', 'QSFP28'),
defaultValue: 'RJ45',
allowNull: false,
},
portSpeed: {
type: DataTypes.ENUM('100M', '1G', '10G', '25G', '40G', '100G'),
defaultValue: '1G',
allowNull: false,
},
status: {
type: DataTypes.ENUM('free', 'occupied', 'fault'),
defaultValue: 'free',
allowNull: false,
},
vlanId: {
type: DataTypes.INTEGER,
allowNull: true,
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
comment: '所属网卡ID,可为空(向后兼容)'
},
portName: {
type: DataTypes.STRING,
allowNull: false
},
portType: {
type: DataTypes.ENUM('RJ45', 'SFP', 'SFP+', 'SFP28', 'QSFP', 'QSFP28'),
defaultValue: 'RJ45',
allowNull: false
},
portSpeed: {
type: DataTypes.ENUM('100M', '1G', '10G', '25G', '40G', '100G'),
defaultValue: '1G',
allowNull: false
},
status: {
type: DataTypes.ENUM('free', 'occupied', 'fault'),
defaultValue: 'free',
allowNull: false
},
vlanId: {
type: DataTypes.INTEGER,
allowNull: true
},
description: {
type: DataTypes.TEXT,
allowNull: true
{
tableName: 'device_ports',
timestamps: true,
indexes: [
{ fields: ['deviceId'] },
{ fields: ['nicId'] },
{ fields: ['status'] },
{ fields: ['portType'] },
{ fields: ['portSpeed'] },
{ unique: true, fields: ['deviceId', 'portName'] },
],
}
}, {
tableName: 'device_ports',
timestamps: true,
indexes: [
{ fields: ['deviceId'] },
{ fields: ['nicId'] },
{ fields: ['status'] },
{ fields: ['portType'] },
{ fields: ['portSpeed'] },
{ unique: true, fields: ['deviceId', 'portName'] }
]
});
);
module.exports = DevicePort;
+58 -58
View File
@@ -1,65 +1,65 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const FaultCategory = sequelize.define('FaultCategory', {
categoryId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const FaultCategory = sequelize.define(
'FaultCategory',
{
categoryId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
comment: '分类名称',
},
description: {
type: DataTypes.TEXT,
comment: '分类说明 - 说明此类故障代表什么问题',
},
priority: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '排序优先级',
},
defaultPriority: {
type: DataTypes.STRING,
defaultValue: 'medium',
comment: '默认优先级: critical/high/medium/low',
},
expectedDuration: {
type: DataTypes.INTEGER,
comment: '预计处理时长(小时)',
},
solutions: {
type: DataTypes.JSON,
defaultValue: [],
comment: '常见解决方案',
},
isSystem: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: '是否系统内置分类',
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: '是否启用',
},
metadata: {
type: DataTypes.JSON,
defaultValue: {},
comment: '扩展字段',
},
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
comment: '分类名称'
},
description: {
type: DataTypes.TEXT,
comment: '分类说明 - 说明此类故障代表什么问题'
},
priority: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '排序优先级'
},
defaultPriority: {
type: DataTypes.STRING,
defaultValue: 'medium',
comment: '默认优先级: critical/high/medium/low'
},
expectedDuration: {
type: DataTypes.INTEGER,
comment: '预计处理时长(小时)'
},
solutions: {
type: DataTypes.JSON,
defaultValue: [],
comment: '常见解决方案'
},
isSystem: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: '是否系统内置分类'
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: '是否启用'
},
metadata: {
type: DataTypes.JSON,
defaultValue: {},
comment: '扩展字段'
{
tableName: 'fault_categories',
timestamps: true,
indexes: [{ fields: ['name'] }, { fields: ['isActive'] }, { fields: ['priority'] }],
}
}, {
tableName: 'fault_categories',
timestamps: true,
indexes: [
{ fields: ['name'] },
{ fields: ['isActive'] },
{ fields: ['priority'] }
]
});
);
module.exports = FaultCategory;
+93 -93
View File
@@ -2,101 +2,101 @@ const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const User = require('./User');
const InventoryPlan = sequelize.define('InventoryPlan', {
planId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const InventoryPlan = sequelize.define(
'InventoryPlan',
{
planId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
type: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'full',
comment: 'full:全面盘点, partial:局部盘点, sample:抽样盘点',
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
status: {
type: DataTypes.STRING,
defaultValue: 'draft',
comment: 'draft:草稿, pending:待执行, in_progress:进行中, completed:已完成, cancelled:已取消',
},
scheduledDate: {
type: DataTypes.DATE,
allowNull: true,
},
completedDate: {
type: DataTypes.DATE,
allowNull: true,
},
targetRooms: {
type: DataTypes.JSON,
defaultValue: [],
comment: '目标机房ID列表',
},
targetRacks: {
type: DataTypes.JSON,
defaultValue: [],
comment: '目标机柜ID列表',
},
totalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '盘点设备总数',
},
checkedDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '已盘点设备数',
},
normalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '正常设备数',
},
abnormalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '异常设备数',
},
missedDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '漏盘设备数',
},
extraDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '多出设备数',
},
createdBy: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: User,
key: 'userId',
},
},
remark: {
type: DataTypes.TEXT,
allowNull: true,
},
},
name: {
type: DataTypes.STRING,
allowNull: false
},
type: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'full',
comment: 'full:全面盘点, partial:局部盘点, sample:抽样盘点'
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
status: {
type: DataTypes.STRING,
defaultValue: 'draft',
comment: 'draft:草稿, pending:待执行, in_progress:进行中, completed:已完成, cancelled:已取消'
},
scheduledDate: {
type: DataTypes.DATE,
allowNull: true
},
completedDate: {
type: DataTypes.DATE,
allowNull: true
},
targetRooms: {
type: DataTypes.JSON,
defaultValue: [],
comment: '目标机房ID列表'
},
targetRacks: {
type: DataTypes.JSON,
defaultValue: [],
comment: '目标机柜ID列表'
},
totalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '盘点设备总数'
},
checkedDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '已盘点设备数'
},
normalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '正常设备数'
},
abnormalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '异常设备数'
},
missedDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '漏盘设备数'
},
extraDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '多出设备数'
},
createdBy: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: User,
key: 'userId'
}
},
remark: {
type: DataTypes.TEXT,
allowNull: true
{
tableName: 'inventory_plans',
timestamps: true,
indexes: [{ fields: ['status'] }, { fields: ['scheduledDate'] }, { fields: ['createdAt'] }],
}
}, {
tableName: 'inventory_plans',
timestamps: true,
indexes: [
{ fields: ['status'] },
{ fields: ['scheduledDate'] },
{ fields: ['createdAt'] }
]
});
);
InventoryPlan.belongsTo(User, { foreignKey: 'createdBy', as: 'Creator' });
User.hasMany(InventoryPlan, { foreignKey: 'createdBy' });
+98 -93
View File
@@ -1,100 +1,105 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const InventoryRecord = sequelize.define('InventoryRecord', {
recordId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const InventoryRecord = sequelize.define(
'InventoryRecord',
{
recordId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
taskId: {
type: DataTypes.STRING,
allowNull: false,
},
planId: {
type: DataTypes.STRING,
allowNull: false,
},
deviceId: {
type: DataTypes.STRING,
allowNull: false,
},
deviceName: {
type: DataTypes.STRING,
allowNull: true,
},
deviceType: {
type: DataTypes.STRING,
allowNull: true,
},
serialNumber: {
type: DataTypes.STRING,
allowNull: true,
comment: '系统记录的序列号',
},
actualSerialNumber: {
type: DataTypes.STRING,
allowNull: true,
comment: '实际盘点序列号',
},
rackId: {
type: DataTypes.STRING,
allowNull: true,
comment: '系统记录的机柜',
},
actualRackId: {
type: DataTypes.STRING,
allowNull: true,
comment: '实际盘点机柜',
},
position: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '系统记录的位置',
},
actualPosition: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '实际盘点位置',
},
status: {
type: DataTypes.STRING,
defaultValue: 'pending',
comment: 'pending:待盘点, normal:正常, abnormal:异常, missed:未盘点, not_found:未找到',
},
abnormalType: {
type: DataTypes.STRING,
allowNull: true,
comment:
'serial_mismatch:序列号不符, position_mismatch:位置不符, device_missing:设备缺失, extra_device:多出设备',
},
checkedBy: {
type: DataTypes.STRING,
allowNull: true,
},
checkedAt: {
type: DataTypes.DATE,
allowNull: true,
},
remark: {
type: DataTypes.TEXT,
allowNull: true,
},
photoUrl: {
type: DataTypes.STRING,
allowNull: true,
comment: '盘点照片',
},
},
taskId: {
type: DataTypes.STRING,
allowNull: false
},
planId: {
type: DataTypes.STRING,
allowNull: false
},
deviceId: {
type: DataTypes.STRING,
allowNull: false
},
deviceName: {
type: DataTypes.STRING,
allowNull: true
},
deviceType: {
type: DataTypes.STRING,
allowNull: true
},
serialNumber: {
type: DataTypes.STRING,
allowNull: true,
comment: '系统记录的序列号'
},
actualSerialNumber: {
type: DataTypes.STRING,
allowNull: true,
comment: '实际盘点序列号'
},
rackId: {
type: DataTypes.STRING,
allowNull: true,
comment: '系统记录的机柜'
},
actualRackId: {
type: DataTypes.STRING,
allowNull: true,
comment: '实际盘点机柜'
},
position: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '系统记录的位置'
},
actualPosition: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '实际盘点位置'
},
status: {
type: DataTypes.STRING,
defaultValue: 'pending',
comment: 'pending:待盘点, normal:正常, abnormal:异常, missed:未盘点, not_found:未找到'
},
abnormalType: {
type: DataTypes.STRING,
allowNull: true,
comment: 'serial_mismatch:序列号不符, position_mismatch:位置不符, device_missing:设备缺失, extra_device:多出设备'
},
checkedBy: {
type: DataTypes.STRING,
allowNull: true
},
checkedAt: {
type: DataTypes.DATE,
allowNull: true
},
remark: {
type: DataTypes.TEXT,
allowNull: true
},
photoUrl: {
type: DataTypes.STRING,
allowNull: true,
comment: '盘点照片'
{
tableName: 'inventory_records',
timestamps: true,
indexes: [
{ fields: ['taskId'] },
{ fields: ['planId'] },
{ fields: ['deviceId'] },
{ fields: ['status'] },
{ fields: ['checkedBy'] },
],
}
}, {
tableName: 'inventory_records',
timestamps: true,
indexes: [
{ fields: ['taskId'] },
{ fields: ['planId'] },
{ fields: ['deviceId'] },
{ fields: ['status'] },
{ fields: ['checkedBy'] }
]
});
);
module.exports = InventoryRecord;
+74 -74
View File
@@ -1,81 +1,81 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const InventoryTask = sequelize.define('InventoryTask', {
taskId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const InventoryTask = sequelize.define(
'InventoryTask',
{
taskId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
planId: {
type: DataTypes.STRING,
allowNull: false,
},
targetType: {
type: DataTypes.STRING,
allowNull: false,
comment: 'room:机房, rack:机柜, device:设备',
},
targetId: {
type: DataTypes.STRING,
allowNull: false,
comment: '目标ID(机房ID/机柜ID/设备ID',
},
targetName: {
type: DataTypes.STRING,
allowNull: true,
comment: '目标名称',
},
status: {
type: DataTypes.STRING,
defaultValue: 'pending',
comment: 'pending:待执行, in_progress:进行中, completed:已完成, skipped:已跳过',
},
totalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '设备总数',
},
checkedDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '已盘点设备数',
},
normalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '正常设备数',
},
abnormalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '异常设备数',
},
assignedTo: {
type: DataTypes.STRING,
allowNull: true,
},
assignedAt: {
type: DataTypes.DATE,
allowNull: true,
},
completedAt: {
type: DataTypes.DATE,
allowNull: true,
},
remark: {
type: DataTypes.TEXT,
allowNull: true,
},
},
planId: {
type: DataTypes.STRING,
allowNull: false
},
targetType: {
type: DataTypes.STRING,
allowNull: false,
comment: 'room:机房, rack:机柜, device:设备'
},
targetId: {
type: DataTypes.STRING,
allowNull: false,
comment: '目标ID(机房ID/机柜ID/设备ID'
},
targetName: {
type: DataTypes.STRING,
allowNull: true,
comment: '目标名称'
},
status: {
type: DataTypes.STRING,
defaultValue: 'pending',
comment: 'pending:待执行, in_progress:进行中, completed:已完成, skipped:已跳过'
},
totalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '设备总数'
},
checkedDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '已盘点设备数'
},
normalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '正常设备数'
},
abnormalDevices: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '异常设备数'
},
assignedTo: {
type: DataTypes.STRING,
allowNull: true
},
assignedAt: {
type: DataTypes.DATE,
allowNull: true
},
completedAt: {
type: DataTypes.DATE,
allowNull: true
},
remark: {
type: DataTypes.TEXT,
allowNull: true
{
tableName: 'inventory_tasks',
timestamps: true,
indexes: [{ fields: ['planId'] }, { fields: ['status'] }, { fields: ['assignedTo'] }],
}
}, {
tableName: 'inventory_tasks',
timestamps: true,
indexes: [
{ fields: ['planId'] },
{ fields: ['status'] },
{ fields: ['assignedTo'] }
]
});
);
module.exports = InventoryTask;
+62 -58
View File
@@ -1,65 +1,69 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const NetworkCard = sequelize.define('NetworkCard', {
nicId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const NetworkCard = sequelize.define(
'NetworkCard',
{
nicId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
deviceId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: 'devices',
key: 'deviceId',
},
},
name: {
type: DataTypes.STRING,
allowNull: false,
comment: '网卡名称,如"网卡1"、"eth0"、"Primary NIC"',
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: '网卡描述信息',
},
slotNumber: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '插槽编号',
},
portCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '端口数量',
},
model: {
type: DataTypes.STRING,
allowNull: true,
comment: '网卡型号',
},
manufacturer: {
type: DataTypes.STRING,
allowNull: true,
comment: '制造商',
},
status: {
type: DataTypes.ENUM('normal', 'warning', 'fault', 'offline'),
defaultValue: 'normal',
allowNull: false,
comment: '网卡状态',
},
},
deviceId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: 'devices',
key: 'deviceId'
}
},
name: {
type: DataTypes.STRING,
allowNull: false,
comment: '网卡名称,如"网卡1"、"eth0"、"Primary NIC"'
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: '网卡描述信息'
},
slotNumber: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '插槽编号'
},
portCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '端口数量'
},
model: {
type: DataTypes.STRING,
allowNull: true,
comment: '网卡型号'
},
manufacturer: {
type: DataTypes.STRING,
allowNull: true,
comment: '制造商'
},
status: {
type: DataTypes.ENUM('normal', 'warning', 'fault', 'offline'),
defaultValue: 'normal',
allowNull: false,
comment: '网卡状态'
{
tableName: 'network_cards',
timestamps: true,
indexes: [
{ fields: ['deviceId'] },
{ fields: ['slotNumber'] },
{ unique: true, fields: ['deviceId', 'name'] },
],
}
}, {
tableName: 'network_cards',
timestamps: true,
indexes: [
{ fields: ['deviceId'] },
{ fields: ['slotNumber'] },
{ unique: true, fields: ['deviceId', 'name'] }
]
});
);
module.exports = NetworkCard;
+83 -78
View File
@@ -5,85 +5,90 @@ const generateRecordId = () => {
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
};
const OperationLog = sequelize.define('OperationLog', {
recordId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const OperationLog = sequelize.define(
'OperationLog',
{
recordId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
module: {
type: DataTypes.STRING,
allowNull: false,
comment: '模块:device/user/role/consumable/rack/room',
},
operationType: {
type: DataTypes.STRING,
allowNull: false,
comment:
'操作类型: create/update/delete/batch_delete/batch_update/status_change/move/permission_change',
},
operationDescription: {
type: DataTypes.TEXT,
comment: '操作描述',
},
targetId: {
type: DataTypes.STRING,
comment: '目标对象ID',
},
targetName: {
type: DataTypes.STRING,
comment: '目标对象名称(冗余便于展示)',
},
operatorId: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作人ID',
},
operatorName: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作人姓名',
},
operatorRole: {
type: DataTypes.STRING,
comment: '操作人角色',
},
beforeState: {
type: DataTypes.JSON,
comment: '操作前状态',
},
afterState: {
type: DataTypes.JSON,
comment: '操作后状态',
},
result: {
type: DataTypes.STRING,
defaultValue: 'success',
comment: '操作结果: success/failed',
},
ipAddress: {
type: DataTypes.STRING,
comment: 'IP地址',
},
userAgent: {
type: DataTypes.STRING,
comment: '用户代理',
},
metadata: {
type: DataTypes.JSON,
defaultValue: {},
comment: '扩展字段',
},
},
module: {
type: DataTypes.STRING,
allowNull: false,
comment: '模块:device/user/role/consumable/rack/room'
},
operationType: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作类型: create/update/delete/batch_delete/batch_update/status_change/move/permission_change'
},
operationDescription: {
type: DataTypes.TEXT,
comment: '操作描述'
},
targetId: {
type: DataTypes.STRING,
comment: '目标对象ID'
},
targetName: {
type: DataTypes.STRING,
comment: '目标对象名称(冗余便于展示)'
},
operatorId: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作人ID'
},
operatorName: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作人姓名'
},
operatorRole: {
type: DataTypes.STRING,
comment: '操作人角色'
},
beforeState: {
type: DataTypes.JSON,
comment: '操作前状态'
},
afterState: {
type: DataTypes.JSON,
comment: '操作后状态'
},
result: {
type: DataTypes.STRING,
defaultValue: 'success',
comment: '操作结果: success/failed'
},
ipAddress: {
type: DataTypes.STRING,
comment: 'IP地址'
},
userAgent: {
type: DataTypes.STRING,
comment: '用户代理'
},
metadata: {
type: DataTypes.JSON,
defaultValue: {},
comment: '扩展字段'
{
tableName: 'operation_logs',
timestamps: true,
indexes: [
{ fields: ['module'] },
{ fields: ['operationType'] },
{ fields: ['targetId'] },
{ fields: ['operatorId'] },
{ fields: ['createdAt'] },
],
}
}, {
tableName: 'operation_logs',
timestamps: true,
indexes: [
{ fields: ['module'] },
{ fields: ['operationType'] },
{ fields: ['targetId'] },
{ fields: ['operatorId'] },
{ fields: ['createdAt'] }
]
});
);
module.exports = OperationLog;
+158 -154
View File
@@ -6,168 +6,172 @@ const InventoryTask = require('./InventoryTask');
const Room = require('./Room');
const Rack = require('./Rack');
const PendingDevice = sequelize.define('PendingDevice', {
pendingId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
},
serialNumber: {
type: DataTypes.STRING,
allowNull: false,
comment: '设备序列号'
},
deviceName: {
type: DataTypes.STRING,
allowNull: true,
comment: '设备名称'
},
deviceType: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: 'other',
comment: '设备类型: server, switch, router, storage, other'
},
roomId: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: Room,
key: 'roomId'
const PendingDevice = sequelize.define(
'PendingDevice',
{
pendingId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
comment: '所属机房ID'
},
rackId: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: Rack,
key: 'rackId'
serialNumber: {
type: DataTypes.STRING,
allowNull: false,
comment: '设备序列号',
},
comment: '所属机柜ID'
},
position: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'U位'
},
height: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 1,
comment: '高度(U)'
},
powerConsumption: {
type: DataTypes.FLOAT,
allowNull: true,
defaultValue: 0,
comment: '功率(W)'
},
model: {
type: DataTypes.STRING,
allowNull: true,
comment: '设备型号'
},
brand: {
type: DataTypes.STRING,
allowNull: true,
comment: '品牌'
},
ipAddress: {
type: DataTypes.STRING,
allowNull: true,
comment: 'IP地址'
},
purchaseDate: {
type: DataTypes.DATE,
allowNull: true,
comment: '购买日期'
},
warrantyExpiry: {
type: DataTypes.DATE,
allowNull: true,
comment: '保修到期'
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: '描述'
},
customFields: {
type: DataTypes.JSON,
defaultValue: {},
allowNull: true
},
status: {
type: DataTypes.STRING,
defaultValue: 'pending',
comment: 'pending: 待同步, synced: 已同步, deleted: 已删除'
},
planId: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: InventoryPlan,
key: 'planId'
deviceName: {
type: DataTypes.STRING,
allowNull: true,
comment: '设备名称',
},
comment: '关联的盘点计划ID'
},
taskId: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: InventoryTask,
key: 'taskId'
deviceType: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: 'other',
comment: '设备类型: server, switch, router, storage, other',
},
comment: '关联的盘点任务ID'
},
createdBy: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: User,
key: 'userId'
roomId: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: Room,
key: 'roomId',
},
comment: '所属机房ID',
},
comment: '创建人'
},
syncedAt: {
type: DataTypes.DATE,
allowNull: true,
comment: '同步时间'
},
syncedBy: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: User,
key: 'userId'
rackId: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: Rack,
key: 'rackId',
},
comment: '所属机柜ID',
},
position: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'U位',
},
height: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 1,
comment: '高度(U)',
},
powerConsumption: {
type: DataTypes.FLOAT,
allowNull: true,
defaultValue: 0,
comment: '功率(W)',
},
model: {
type: DataTypes.STRING,
allowNull: true,
comment: '设备型号',
},
brand: {
type: DataTypes.STRING,
allowNull: true,
comment: '品牌',
},
ipAddress: {
type: DataTypes.STRING,
allowNull: true,
comment: 'IP地址',
},
purchaseDate: {
type: DataTypes.DATE,
allowNull: true,
comment: '购买日期',
},
warrantyExpiry: {
type: DataTypes.DATE,
allowNull: true,
comment: '保修到期',
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: '描述',
},
customFields: {
type: DataTypes.JSON,
defaultValue: {},
allowNull: true,
},
status: {
type: DataTypes.STRING,
defaultValue: 'pending',
comment: 'pending: 待同步, synced: 已同步, deleted: 已删除',
},
planId: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: InventoryPlan,
key: 'planId',
},
comment: '关联的盘点计划ID',
},
taskId: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: InventoryTask,
key: 'taskId',
},
comment: '关联的盘点任务ID',
},
createdBy: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: User,
key: 'userId',
},
comment: '创建人',
},
syncedAt: {
type: DataTypes.DATE,
allowNull: true,
comment: '同步时间',
},
syncedBy: {
type: DataTypes.STRING,
allowNull: true,
references: {
model: User,
key: 'userId',
},
comment: '同步人',
},
syncedDeviceId: {
type: DataTypes.STRING,
allowNull: true,
comment: '同步后生成的设备ID',
},
remark: {
type: DataTypes.TEXT,
allowNull: true,
comment: '备注',
},
comment: '同步人'
},
syncedDeviceId: {
type: DataTypes.STRING,
allowNull: true,
comment: '同步后生成的设备ID'
},
remark: {
type: DataTypes.TEXT,
allowNull: true,
comment: '备注'
{
tableName: 'pending_devices',
timestamps: true,
indexes: [
{ fields: ['serialNumber'] },
{ fields: ['status'] },
{ fields: ['planId'] },
{ fields: ['taskId'] },
{ fields: ['createdBy'] },
{ fields: ['roomId'] },
{ fields: ['rackId'] },
],
}
}, {
tableName: 'pending_devices',
timestamps: true,
indexes: [
{ fields: ['serialNumber'] },
{ fields: ['status'] },
{ fields: ['planId'] },
{ fields: ['taskId'] },
{ fields: ['createdBy'] },
{ fields: ['roomId'] },
{ fields: ['rackId'] }
]
});
);
PendingDevice.belongsTo(User, { foreignKey: 'createdBy', as: 'Creator' });
PendingDevice.belongsTo(User, { foreignKey: 'syncedBy', as: 'Syncer' });
+45 -41
View File
@@ -1,48 +1,52 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const Permission = sequelize.define('Permission', {
permissionId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false
const Permission = sequelize.define(
'Permission',
{
permissionId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
},
permissionName: {
type: DataTypes.STRING,
allowNull: false,
},
permissionCode: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
parentId: {
type: DataTypes.STRING,
allowNull: true,
},
type: {
type: DataTypes.ENUM('menu', 'button'),
defaultValue: 'button',
},
path: {
type: DataTypes.STRING,
allowNull: true,
},
icon: {
type: DataTypes.STRING,
allowNull: true,
},
sort: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active',
},
},
permissionName: {
type: DataTypes.STRING,
allowNull: false
},
permissionCode: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
parentId: {
type: DataTypes.STRING,
allowNull: true
},
type: {
type: DataTypes.ENUM('menu', 'button'),
defaultValue: 'button'
},
path: {
type: DataTypes.STRING,
allowNull: true
},
icon: {
type: DataTypes.STRING,
allowNull: true
},
sort: {
type: DataTypes.INTEGER,
defaultValue: 0
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active'
{
tableName: 'permissions',
timestamps: true,
}
}, {
tableName: 'permissions',
timestamps: true
});
);
module.exports = Permission;
+44 -44
View File
@@ -2,54 +2,54 @@ const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const Room = require('./Room');
const Rack = sequelize.define('Rack', {
rackId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const Rack = sequelize.define(
'Rack',
{
rackId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
height: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 45, // 标准机柜高度(U数)
},
maxPower: {
type: DataTypes.FLOAT,
allowNull: false,
},
currentPower: {
type: DataTypes.FLOAT,
defaultValue: 0,
},
status: {
type: DataTypes.STRING,
defaultValue: 'active',
},
roomId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: Room,
key: 'roomId',
},
},
},
name: {
type: DataTypes.STRING,
allowNull: false
},
height: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 45 // 标准机柜高度(U数)
},
maxPower: {
type: DataTypes.FLOAT,
allowNull: false
},
currentPower: {
type: DataTypes.FLOAT,
defaultValue: 0
},
status: {
type: DataTypes.STRING,
defaultValue: 'active'
},
roomId: {
type: DataTypes.STRING,
allowNull: false,
references: {
model: Room,
key: 'roomId'
}
{
tableName: 'racks',
timestamps: true,
indexes: [{ fields: ['roomId'] }, { fields: ['status'] }, { fields: ['roomId', 'status'] }],
}
}, {
tableName: 'racks',
timestamps: true,
indexes: [
{ fields: ['roomId'] },
{ fields: ['status'] },
{ fields: ['roomId', 'status'] }
]
});
);
// 关联关系
Rack.belongsTo(Room, { foreignKey: 'roomId' });
Room.hasMany(Rack, { foreignKey: 'roomId' });
module.exports = Rack;
module.exports = Rack;
+37 -33
View File
@@ -1,40 +1,44 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const Role = sequelize.define('Role', {
roleId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false
const Role = sequelize.define(
'Role',
{
roleId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
},
roleName: {
type: DataTypes.STRING,
allowNull: false,
},
roleCode: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
description: {
type: DataTypes.STRING,
allowNull: true,
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active',
},
permissions: {
type: DataTypes.JSON,
defaultValue: [],
},
sort: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
},
roleName: {
type: DataTypes.STRING,
allowNull: false
},
roleCode: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
description: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active'
},
permissions: {
type: DataTypes.JSON,
defaultValue: []
},
sort: {
type: DataTypes.INTEGER,
defaultValue: 0
{
tableName: 'roles',
timestamps: true,
}
}, {
tableName: 'roles',
timestamps: true
});
);
module.exports = Role;
+38 -37
View File
@@ -1,43 +1,44 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const Room = sequelize.define('Room', {
roomId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const Room = sequelize.define(
'Room',
{
roomId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
location: {
type: DataTypes.STRING,
allowNull: false,
},
area: {
type: DataTypes.FLOAT,
allowNull: false,
},
capacity: {
type: DataTypes.INTEGER,
allowNull: false,
},
status: {
type: DataTypes.STRING,
defaultValue: 'active',
},
description: {
type: DataTypes.TEXT,
},
},
name: {
type: DataTypes.STRING,
allowNull: false
},
location: {
type: DataTypes.STRING,
allowNull: false
},
area: {
type: DataTypes.FLOAT,
allowNull: false
},
capacity: {
type: DataTypes.INTEGER,
allowNull: false
},
status: {
type: DataTypes.STRING,
defaultValue: 'active'
},
description: {
type: DataTypes.TEXT
{
tableName: 'rooms',
timestamps: true,
indexes: [{ fields: ['status'] }, { fields: ['name'] }],
}
}, {
tableName: 'rooms',
timestamps: true,
indexes: [
{ fields: ['status'] },
{ fields: ['name'] }
]
});
);
module.exports = Room;
module.exports = Room;
+42 -40
View File
@@ -1,47 +1,49 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const SystemSetting = sequelize.define('SystemSetting', {
settingKey: {
type: DataTypes.STRING(100),
primaryKey: true,
allowNull: false,
comment: '设置键名'
const SystemSetting = sequelize.define(
'SystemSetting',
{
settingKey: {
type: DataTypes.STRING(100),
primaryKey: true,
allowNull: false,
comment: '设置键名',
},
settingValue: {
type: DataTypes.TEXT,
allowNull: true,
comment: '设置值(JSON格式)',
},
settingType: {
type: DataTypes.STRING(20),
allowNull: false,
defaultValue: 'string',
comment: '设置类型: string, number, boolean, json, array',
},
category: {
type: DataTypes.STRING(50),
allowNull: false,
defaultValue: 'general',
comment: '设置分类: general, appearance, backup, about',
},
description: {
type: DataTypes.STRING(255),
allowNull: true,
comment: '设置描述',
},
isEditable: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: '是否可编辑',
},
},
settingValue: {
type: DataTypes.TEXT,
allowNull: true,
comment: '设置值(JSON格式)'
},
settingType: {
type: DataTypes.STRING(20),
allowNull: false,
defaultValue: 'string',
comment: '设置类型: string, number, boolean, json, array'
},
category: {
type: DataTypes.STRING(50),
allowNull: false,
defaultValue: 'general',
comment: '设置分类: general, appearance, backup, about'
},
description: {
type: DataTypes.STRING(255),
allowNull: true,
comment: '设置描述'
},
isEditable: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: '是否可编辑'
{
tableName: 'system_settings',
timestamps: true,
indexes: [{ fields: ['category'] }],
}
}, {
tableName: 'system_settings',
timestamps: true,
indexes: [
{ fields: ['category'] }
]
});
);
module.exports = SystemSetting;
+125 -121
View File
@@ -3,129 +3,133 @@ const { sequelize } = require('../db');
const User = require('./User');
const Device = require('./Device');
const Ticket = sequelize.define('Ticket', {
ticketId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const Ticket = sequelize.define(
'Ticket',
{
ticketId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
title: {
type: DataTypes.STRING,
allowNull: false,
comment: '工单标题',
},
deviceId: {
type: DataTypes.STRING,
allowNull: true,
comment: '关联设备ID',
},
deviceName: {
type: DataTypes.STRING,
allowNull: false,
comment: '设备名称',
},
deviceModel: {
type: DataTypes.STRING,
comment: '设备型号',
},
serialNumber: {
type: DataTypes.STRING,
comment: '设备序列号',
},
faultCategory: {
type: DataTypes.STRING,
allowNull: false,
comment: '故障分类',
},
faultSubCategory: {
type: DataTypes.STRING,
comment: '故障子分类',
},
priority: {
type: DataTypes.STRING,
defaultValue: 'medium',
comment: '优先级: critical/high/medium/low',
},
status: {
type: DataTypes.STRING,
defaultValue: 'pending',
comment: '工单状态: pending/in_progress/completed/closed/cancelled',
},
description: {
type: DataTypes.TEXT,
comment: '故障描述',
},
expectedCompletionDate: {
type: DataTypes.DATE,
comment: '期望完成时间',
},
reporterId: {
type: DataTypes.STRING,
allowNull: false,
comment: '报修人ID',
},
reporterName: {
type: DataTypes.STRING,
allowNull: false,
comment: '报修人姓名',
},
assigneeId: {
type: DataTypes.STRING,
comment: '处理人ID',
},
assigneeName: {
type: DataTypes.STRING,
comment: '处理人姓名',
},
location: {
type: DataTypes.STRING,
comment: '设备位置',
},
resolution: {
type: DataTypes.TEXT,
comment: '解决方案',
},
completionDate: {
type: DataTypes.DATE,
comment: '实际完成时间',
},
evaluation: {
type: DataTypes.TEXT,
comment: '用户评价',
},
evaluationRating: {
type: DataTypes.INTEGER,
comment: '评价星级(1-5)',
},
attachments: {
type: DataTypes.JSON,
defaultValue: [],
comment: '附件列表',
},
tags: {
type: DataTypes.JSON,
defaultValue: [],
comment: '标签',
},
metadata: {
type: DataTypes.JSON,
defaultValue: {},
comment: '扩展字段',
},
},
title: {
type: DataTypes.STRING,
allowNull: false,
comment: '工单标题'
},
deviceId: {
type: DataTypes.STRING,
allowNull: true,
comment: '关联设备ID'
},
deviceName: {
type: DataTypes.STRING,
allowNull: false,
comment: '设备名称'
},
deviceModel: {
type: DataTypes.STRING,
comment: '设备型号'
},
serialNumber: {
type: DataTypes.STRING,
comment: '设备序列号'
},
faultCategory: {
type: DataTypes.STRING,
allowNull: false,
comment: '故障分类'
},
faultSubCategory: {
type: DataTypes.STRING,
comment: '故障子分类'
},
priority: {
type: DataTypes.STRING,
defaultValue: 'medium',
comment: '优先级: critical/high/medium/low'
},
status: {
type: DataTypes.STRING,
defaultValue: 'pending',
comment: '工单状态: pending/in_progress/completed/closed/cancelled'
},
description: {
type: DataTypes.TEXT,
comment: '故障描述'
},
expectedCompletionDate: {
type: DataTypes.DATE,
comment: '期望完成时间'
},
reporterId: {
type: DataTypes.STRING,
allowNull: false,
comment: '报修人ID'
},
reporterName: {
type: DataTypes.STRING,
allowNull: false,
comment: '报修人姓名'
},
assigneeId: {
type: DataTypes.STRING,
comment: '处理人ID'
},
assigneeName: {
type: DataTypes.STRING,
comment: '处理人姓名'
},
location: {
type: DataTypes.STRING,
comment: '设备位置'
},
resolution: {
type: DataTypes.TEXT,
comment: '解决方案'
},
completionDate: {
type: DataTypes.DATE,
comment: '实际完成时间'
},
evaluation: {
type: DataTypes.TEXT,
comment: '用户评价'
},
evaluationRating: {
type: DataTypes.INTEGER,
comment: '评价星级(1-5)'
},
attachments: {
type: DataTypes.JSON,
defaultValue: [],
comment: '附件列表'
},
tags: {
type: DataTypes.JSON,
defaultValue: [],
comment: '标签'
},
metadata: {
type: DataTypes.JSON,
defaultValue: {},
comment: '扩展字段'
{
tableName: 'tickets',
timestamps: true,
indexes: [
{ fields: ['deviceId'] },
{ fields: ['status'] },
{ fields: ['faultCategory'] },
{ fields: ['priority'] },
{ fields: ['reporterId'] },
{ fields: ['assigneeId'] },
{ fields: ['createdAt'] },
],
}
}, {
tableName: 'tickets',
timestamps: true,
indexes: [
{ fields: ['deviceId'] },
{ fields: ['status'] },
{ fields: ['faultCategory'] },
{ fields: ['priority'] },
{ fields: ['reporterId'] },
{ fields: ['assigneeId'] },
{ fields: ['createdAt'] }
]
});
);
Ticket.belongsTo(User, { foreignKey: 'reporterId', as: 'reporter', constraints: false });
Ticket.belongsTo(User, { foreignKey: 'assigneeId', as: 'assignee', constraints: false });
+50 -46
View File
@@ -1,53 +1,57 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const TicketField = sequelize.define('TicketField', {
fieldId: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
const TicketField = sequelize.define(
'TicketField',
{
fieldId: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
fieldName: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
displayName: {
type: DataTypes.STRING,
allowNull: false,
},
fieldType: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'string',
},
required: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
options: {
type: DataTypes.JSON,
allowNull: true,
},
order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
visible: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
placeholder: {
type: DataTypes.STRING,
allowNull: true,
},
},
fieldName: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
displayName: {
type: DataTypes.STRING,
allowNull: false
},
fieldType: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'string'
},
required: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
options: {
type: DataTypes.JSON,
allowNull: true
},
order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
visible: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
},
placeholder: {
type: DataTypes.STRING,
allowNull: true
{
tableName: 'ticketFields',
timestamps: true,
}
}, {
tableName: 'ticketFields',
timestamps: true
});
);
module.exports = TicketField;
+87 -83
View File
@@ -1,90 +1,94 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const TicketOperationRecord = sequelize.define('TicketOperationRecord', {
recordId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const TicketOperationRecord = sequelize.define(
'TicketOperationRecord',
{
recordId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
ticketId: {
type: DataTypes.STRING,
allowNull: false,
comment: '关联工单ID',
},
operationType: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作类型: create/update/status_change/assignment/comment/attachment',
},
operationDescription: {
type: DataTypes.TEXT,
comment: '操作描述',
},
operatorId: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作人ID',
},
operatorName: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作人姓名',
},
operatorRole: {
type: DataTypes.STRING,
comment: '操作人角色',
},
operationSteps: {
type: DataTypes.JSON,
defaultValue: [],
comment: '操作步骤详情',
},
spareParts: {
type: DataTypes.JSON,
defaultValue: [],
comment: '使用的备件列表',
},
beforeState: {
type: DataTypes.JSON,
comment: '操作前状态',
},
afterState: {
type: DataTypes.JSON,
comment: '操作后状态',
},
duration: {
type: DataTypes.INTEGER,
comment: '操作耗时(分钟)',
},
result: {
type: DataTypes.STRING,
comment: '操作结果: success/failed/partial',
},
notes: {
type: DataTypes.TEXT,
comment: '备注信息',
},
attachments: {
type: DataTypes.JSON,
defaultValue: [],
comment: '附件',
},
metadata: {
type: DataTypes.JSON,
defaultValue: {},
comment: '扩展字段',
},
},
ticketId: {
type: DataTypes.STRING,
allowNull: false,
comment: '关联工单ID'
},
operationType: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作类型: create/update/status_change/assignment/comment/attachment'
},
operationDescription: {
type: DataTypes.TEXT,
comment: '操作描述'
},
operatorId: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作人ID'
},
operatorName: {
type: DataTypes.STRING,
allowNull: false,
comment: '操作人姓名'
},
operatorRole: {
type: DataTypes.STRING,
comment: '操作人角色'
},
operationSteps: {
type: DataTypes.JSON,
defaultValue: [],
comment: '操作步骤详情'
},
spareParts: {
type: DataTypes.JSON,
defaultValue: [],
comment: '使用的备件列表'
},
beforeState: {
type: DataTypes.JSON,
comment: '操作前状态'
},
afterState: {
type: DataTypes.JSON,
comment: '操作后状态'
},
duration: {
type: DataTypes.INTEGER,
comment: '操作耗时(分钟)'
},
result: {
type: DataTypes.STRING,
comment: '操作结果: success/failed/partial'
},
notes: {
type: DataTypes.TEXT,
comment: '备注信息'
},
attachments: {
type: DataTypes.JSON,
defaultValue: [],
comment: '附件'
},
metadata: {
type: DataTypes.JSON,
defaultValue: {},
comment: '扩展字段'
{
tableName: 'ticket_operation_records',
timestamps: true,
indexes: [
{ fields: ['ticketId'] },
{ fields: ['operatorId'] },
{ fields: ['operationType'] },
{ fields: ['createdAt'] },
],
}
}, {
tableName: 'ticket_operation_records',
timestamps: true,
indexes: [
{ fields: ['ticketId'] },
{ fields: ['operatorId'] },
{ fields: ['operationType'] },
{ fields: ['createdAt'] }
]
});
);
module.exports = TicketOperationRecord;
+61 -61
View File
@@ -1,68 +1,68 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const User = sequelize.define('User', {
userId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false
const User = sequelize.define(
'User',
{
userId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
email: {
type: DataTypes.STRING,
allowNull: true,
validate: {
isEmail: true,
},
},
phone: {
type: DataTypes.STRING,
allowNull: true,
},
realName: {
type: DataTypes.STRING,
allowNull: true,
},
avatar: {
type: DataTypes.STRING,
allowNull: true,
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'locked', 'pending'),
defaultValue: 'active',
},
lastLoginTime: {
type: DataTypes.DATE,
allowNull: true,
},
lastLoginIp: {
type: DataTypes.STRING,
allowNull: true,
},
loginCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
remark: {
type: DataTypes.TEXT,
allowNull: true,
},
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
password: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: true,
validate: {
isEmail: true
}
},
phone: {
type: DataTypes.STRING,
allowNull: true
},
realName: {
type: DataTypes.STRING,
allowNull: true
},
avatar: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'locked', 'pending'),
defaultValue: 'active'
},
lastLoginTime: {
type: DataTypes.DATE,
allowNull: true
},
lastLoginIp: {
type: DataTypes.STRING,
allowNull: true
},
loginCount: {
type: DataTypes.INTEGER,
defaultValue: 0
},
remark: {
type: DataTypes.TEXT,
allowNull: true
{
tableName: 'users',
timestamps: true,
indexes: [{ fields: ['status'] }, { fields: ['username'] }, { fields: ['email'] }],
}
}, {
tableName: 'users',
timestamps: true,
indexes: [
{ fields: ['status'] },
{ fields: ['username'] },
{ fields: ['email'] }
]
});
);
module.exports = User;
+13 -9
View File
@@ -3,16 +3,20 @@ const { sequelize } = require('../db');
const User = require('./User');
const Role = require('./Role');
const UserRole = sequelize.define('UserRole', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
const UserRole = sequelize.define(
'UserRole',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
},
{
tableName: 'user_roles',
timestamps: true,
}
}, {
tableName: 'user_roles',
timestamps: true
});
);
UserRole.belongsTo(User, { foreignKey: 'UserId', onDelete: 'CASCADE' });
UserRole.belongsTo(Role, { foreignKey: 'RoleId', onDelete: 'CASCADE' });
+35 -34
View File
@@ -1,41 +1,42 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../db');
const Warehouse = sequelize.define('Warehouse', {
warehouseId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true
const Warehouse = sequelize.define(
'Warehouse',
{
warehouseId: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
location: {
type: DataTypes.STRING,
allowNull: true,
},
capacity: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 100,
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active',
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
},
name: {
type: DataTypes.STRING,
allowNull: false
},
location: {
type: DataTypes.STRING,
allowNull: true
},
capacity: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 100
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active'
},
description: {
type: DataTypes.TEXT,
allowNull: true
{
tableName: 'warehouses',
timestamps: true,
indexes: [{ fields: ['status'] }, { fields: ['name'] }],
}
}, {
tableName: 'warehouses',
timestamps: true,
indexes: [
{ fields: ['status'] },
{ fields: ['name'] }
]
});
);
module.exports = Warehouse;
+19 -19
View File
@@ -25,7 +25,7 @@ const initDefaultFaultCategories = async () => {
defaultPriority: 'high',
expectedDuration: 4,
solutions: ['重启服务', '回滚版本', '修复配置', '重装系统'],
isSystem: true
isSystem: true,
},
{
categoryId: 'CAT002',
@@ -38,7 +38,7 @@ const initDefaultFaultCategories = async () => {
defaultPriority: 'critical',
expectedDuration: 8,
solutions: ['更换部件', '联系厂商', '现场维修'],
isSystem: true
isSystem: true,
},
{
categoryId: 'CAT003',
@@ -51,7 +51,7 @@ const initDefaultFaultCategories = async () => {
defaultPriority: 'high',
expectedDuration: 2,
solutions: ['检查网线', '重启交换机', '修复配置', '联系运营商'],
isSystem: true
isSystem: true,
},
{
categoryId: 'CAT004',
@@ -64,7 +64,7 @@ const initDefaultFaultCategories = async () => {
defaultPriority: 'medium',
expectedDuration: 6,
solutions: ['修复Bug', '优化性能', '更新版本', '配置调整'],
isSystem: true
isSystem: true,
},
{
categoryId: 'CAT005',
@@ -77,7 +77,7 @@ const initDefaultFaultCategories = async () => {
defaultPriority: 'critical',
expectedDuration: 1,
solutions: ['隔离系统', '调查取证', '修复漏洞', '更新安全策略'],
isSystem: true
isSystem: true,
},
{
categoryId: 'CAT006',
@@ -90,7 +90,7 @@ const initDefaultFaultCategories = async () => {
defaultPriority: 'medium',
expectedDuration: 4,
solutions: ['资源扩容', '优化SQL', '清理缓存', '负载均衡'],
isSystem: true
isSystem: true,
},
{
categoryId: 'CAT007',
@@ -103,7 +103,7 @@ const initDefaultFaultCategories = async () => {
defaultPriority: 'low',
expectedDuration: 2,
solutions: ['调整配置', '参数优化', '功能启用'],
isSystem: true
isSystem: true,
},
{
categoryId: 'CAT008',
@@ -116,7 +116,7 @@ const initDefaultFaultCategories = async () => {
defaultPriority: 'low',
expectedDuration: 4,
solutions: ['系统更新', '安全检查', '日志清理', '硬件检测'],
isSystem: true
isSystem: true,
},
{
categoryId: 'CAT009',
@@ -129,7 +129,7 @@ const initDefaultFaultCategories = async () => {
defaultPriority: 'high',
expectedDuration: 6,
solutions: ['数据恢复', '数据修复', '重新同步', '备份还原'],
isSystem: true
isSystem: true,
},
{
categoryId: 'CAT010',
@@ -142,8 +142,8 @@ const initDefaultFaultCategories = async () => {
defaultPriority: 'critical',
expectedDuration: 2,
solutions: ['切换电源', '更换UPS', '联系供电', '检查线路'],
isSystem: true
}
isSystem: true,
},
];
for (const category of defaultCategories) {
@@ -157,14 +157,14 @@ const initDefaultFaultCategories = async () => {
};
const initAssociations = () => {
Ticket.hasMany(TicketOperationRecord, {
foreignKey: 'ticketId',
as: 'operationRecords',
constraints: false
});
TicketOperationRecord.belongsTo(Ticket, {
Ticket.hasMany(TicketOperationRecord, {
foreignKey: 'ticketId',
constraints: false
as: 'operationRecords',
constraints: false,
});
TicketOperationRecord.belongsTo(Ticket, {
foreignKey: 'ticketId',
constraints: false,
});
};
@@ -185,5 +185,5 @@ module.exports = {
initAssociations,
Ticket,
TicketOperationRecord,
FaultCategory
FaultCategory,
};
+77 -61
View File
@@ -4,7 +4,13 @@ const User = require('../models/User');
const Role = require('../models/Role');
const UserRole = require('../models/UserRole');
const { generateToken, authMiddleware } = require('../middleware/auth');
const { SALT_ROUNDS, MAX_LOGIN_ATTEMPTS, PASSWORD_MIN_LENGTH, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH } = require('../config');
const {
SALT_ROUNDS,
MAX_LOGIN_ATTEMPTS,
PASSWORD_MIN_LENGTH,
USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH,
} = require('../config');
const router = express.Router();
@@ -19,21 +25,21 @@ router.post('/register', async (req, res) => {
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
message: '用户名和密码不能为空',
});
}
if (username.length < USERNAME_MIN_LENGTH || username.length > USERNAME_MAX_LENGTH) {
return res.status(400).json({
success: false,
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`,
});
}
if (password.length < PASSWORD_MIN_LENGTH) {
return res.status(400).json({
success: false,
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
});
}
@@ -41,7 +47,7 @@ router.post('/register', async (req, res) => {
if (existingUser) {
return res.status(400).json({
success: false,
message: '用户名已存在'
message: '用户名已存在',
});
}
@@ -57,7 +63,7 @@ router.post('/register', async (req, res) => {
email,
phone,
realName: realName || username,
status: isFirstUser ? 'active' : 'pending'
status: isFirstUser ? 'active' : 'pending',
});
if (isFirstUser) {
@@ -70,13 +76,13 @@ router.post('/register', async (req, res) => {
roleCode: 'admin',
description: '系统管理员,拥有所有权限',
status: 'active',
permissions: []
permissions: [],
});
}
await UserRole.create({
UserId: user.userId,
RoleId: adminRole.roleId
RoleId: adminRole.roleId,
});
const token = generateToken(user);
@@ -89,11 +95,11 @@ router.post('/register', async (req, res) => {
userId: user.userId,
username: user.username,
email: user.email,
realName: user.realName
realName: user.realName,
},
token,
isFirstUser: true
}
isFirstUser: true,
},
});
} else {
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
@@ -101,7 +107,7 @@ router.post('/register', async (req, res) => {
if (defaultRole) {
await UserRole.create({
UserId: user.userId,
RoleId: defaultRole.roleId
RoleId: defaultRole.roleId,
});
}
@@ -113,11 +119,11 @@ router.post('/register', async (req, res) => {
userId: user.userId,
username: user.username,
email: user.email,
realName: user.realName
realName: user.realName,
},
isFirstUser: false,
pendingApproval: true
}
pendingApproval: true,
},
});
}
} catch (error) {
@@ -125,7 +131,7 @@ router.post('/register', async (req, res) => {
res.status(500).json({
success: false,
message: '注册失败',
error: error.message
error: error.message,
});
}
});
@@ -137,7 +143,7 @@ router.post('/login', async (req, res) => {
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
message: '用户名和密码不能为空',
});
}
@@ -145,21 +151,21 @@ router.post('/login', async (req, res) => {
if (!user) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
message: '用户名或密码错误',
});
}
if (user.status === 'locked') {
return res.status(403).json({
success: false,
message: '账户已被锁定,请联系管理员'
message: '账户已被锁定,请联系管理员',
});
}
if (user.status === 'inactive') {
return res.status(403).json({
success: false,
message: '账户已禁用'
message: '账户已禁用',
});
}
@@ -167,7 +173,7 @@ router.post('/login', async (req, res) => {
return res.status(403).json({
success: false,
code: 'PENDING_APPROVAL',
message: '账户待审核,请联系管理员激活'
message: '账户待审核,请联系管理员激活',
});
}
@@ -178,10 +184,10 @@ router.post('/login', async (req, res) => {
user.status = 'locked';
}
await user.save();
return res.status(401).json({
success: false,
message: '用户名或密码错误'
message: '用户名或密码错误',
});
}
@@ -201,17 +207,17 @@ router.post('/login', async (req, res) => {
username: user.username,
email: user.email,
realName: user.realName,
avatar: user.avatar
avatar: user.avatar,
},
token
}
token,
},
});
} catch (error) {
console.error('登录错误:', error);
res.status(500).json({
success: false,
message: '登录失败',
error: error.message
error: error.message,
});
}
});
@@ -219,22 +225,24 @@ router.post('/login', async (req, res) => {
router.get('/profile', authMiddleware, async (req, res) => {
try {
const user = await User.findByPk(req.user.userId, {
attributes: { exclude: ['password'] }
attributes: { exclude: ['password'] },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
const roles = await Role.findAll({
include: [{
model: User,
where: { userId: req.user.userId },
attributes: []
}]
include: [
{
model: User,
where: { userId: req.user.userId },
attributes: [],
},
],
});
res.json({
@@ -244,15 +252,15 @@ router.get('/profile', authMiddleware, async (req, res) => {
roles: roles.map(r => ({
roleId: r.roleId,
roleName: r.roleName,
roleCode: r.roleCode
}))
}
roleCode: r.roleCode,
})),
},
});
} catch (error) {
console.error('获取profile错误:', error);
res.status(500).json({
success: false,
message: '获取用户信息失败'
message: '获取用户信息失败',
});
}
});
@@ -265,14 +273,22 @@ router.put('/profile', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (email !== undefined) user.email = email;
if (phone !== undefined) user.phone = phone;
if (realName !== undefined) user.realName = realName;
if (avatar !== undefined) user.avatar = avatar;
if (email !== undefined) {
user.email = email;
}
if (phone !== undefined) {
user.phone = phone;
}
if (realName !== undefined) {
user.realName = realName;
}
if (avatar !== undefined) {
user.avatar = avatar;
}
await user.save();
@@ -285,14 +301,14 @@ router.put('/profile', authMiddleware, async (req, res) => {
email: user.email,
phone: user.phone,
realName: user.realName,
avatar: user.avatar
}
avatar: user.avatar,
},
});
} catch (error) {
console.error('更新profile错误:', error);
res.status(500).json({
success: false,
message: '更新失败'
message: '更新失败',
});
}
});
@@ -304,14 +320,14 @@ router.put('/password', authMiddleware, async (req, res) => {
if (!oldPassword || !newPassword) {
return res.status(400).json({
success: false,
message: '旧密码和新密码都不能为空'
message: '旧密码和新密码都不能为空',
});
}
if (newPassword.length < PASSWORD_MIN_LENGTH) {
return res.status(400).json({
success: false,
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
});
}
@@ -321,7 +337,7 @@ router.put('/password', authMiddleware, async (req, res) => {
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '旧密码错误'
message: '旧密码错误',
});
}
@@ -330,13 +346,13 @@ router.put('/password', authMiddleware, async (req, res) => {
res.json({
success: true,
message: '密码修改成功'
message: '密码修改成功',
});
} catch (error) {
console.error('修改密码错误:', error);
res.status(500).json({
success: false,
message: '密码修改失败'
message: '密码修改失败',
});
}
});
@@ -344,19 +360,19 @@ router.put('/password', authMiddleware, async (req, res) => {
router.post('/check-admin', async (req, res) => {
try {
const userCount = await User.count();
res.json({
success: true,
data: {
hasAdmin: userCount > 0,
userCount
}
userCount,
},
});
} catch (error) {
console.error('检查管理员错误:', error);
res.status(500).json({
success: false,
message: '检查失败'
message: '检查失败',
});
}
});
@@ -368,7 +384,7 @@ router.post('/unlock', async (req, res) => {
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
message: '用户名和密码不能为空',
});
}
@@ -376,14 +392,14 @@ router.post('/unlock', async (req, res) => {
if (!user) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
message: '用户名或密码错误',
});
}
if (user.status !== 'locked') {
return res.status(400).json({
success: false,
message: '账户未被锁定'
message: '账户未被锁定',
});
}
@@ -391,7 +407,7 @@ router.post('/unlock', async (req, res) => {
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '用户名或密码错误'
message: '用户名或密码错误',
});
}
@@ -402,14 +418,14 @@ router.post('/unlock', async (req, res) => {
res.json({
success: true,
message: '账户解锁成功'
message: '账户解锁成功',
});
} catch (error) {
console.error('解锁账户错误:', error);
res.status(500).json({
success: false,
message: '解锁失败',
error: error.message
error: error.message,
});
}
});
+16 -9
View File
@@ -10,11 +10,18 @@ if (!fs.existsSync(UPLOAD_DIR)) {
const SETTINGS_FILE = path.join(__dirname, '../backgroundSettings.json');
if (!fs.existsSync(SETTINGS_FILE)) {
fs.writeFileSync(SETTINGS_FILE, JSON.stringify({
type: 'gradient',
image: '',
size: 'contain'
}, null, 2));
fs.writeFileSync(
SETTINGS_FILE,
JSON.stringify(
{
type: 'gradient',
image: '',
size: 'contain',
},
null,
2
)
);
}
router.get('/', (req, res) => {
@@ -22,7 +29,7 @@ router.get('/', (req, res) => {
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
res.json({
success: true,
data: settings
data: settings,
});
} catch (error) {
console.error('读取背景设置失败:', error);
@@ -36,7 +43,7 @@ router.put('/', (req, res) => {
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
res.json({
success: true,
data: settings
data: settings,
});
} catch (error) {
console.error('保存背景设置失败:', error);
@@ -54,7 +61,7 @@ router.post('/upload', (req, res) => {
const fileName = `${Date.now()}_${file.name}`;
const filePath = path.join(UPLOAD_DIR, fileName);
file.mv(filePath, (err) => {
file.mv(filePath, err => {
if (err) {
console.error('文件保存失败:', err);
return res.status(500).json({ error: '文件保存失败' });
@@ -90,4 +97,4 @@ router.post('/settings', (req, res) => {
}
});
module.exports = router;
module.exports = router;
+102 -75
View File
@@ -23,11 +23,7 @@ const {
updateAutoBackupSettings,
executeBackupNow,
} = require('../utils/autoBackupScheduler');
const {
getBackupLogs,
getBackupLogById,
deleteOldLogs,
} = require('../utils/backupLog');
const { getBackupLogs, getBackupLogById, deleteOldLogs } = require('../utils/backupLog');
const {
getAllTargets,
getTarget,
@@ -81,22 +77,27 @@ router.get('/list', async (req, res) => {
});
}
const files = fs.readdirSync(backupPath)
.filter(f => (f.startsWith('backup_') || f.startsWith('uploaded_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
const files = fs
.readdirSync(backupPath)
.filter(
f =>
(f.startsWith('backup_') || f.startsWith('uploaded_')) &&
(f.endsWith('.json') || f.endsWith('.json.gz'))
)
.map(async f => {
const filePath = path.join(backupPath, f);
const stats = fs.statSync(filePath);
const isCompressed = f.endsWith('.gz');
// 尝试从文件内容中提取元数据
let metadata = {
const metadata = {
filename: f,
size: stats.size,
compressed: isCompressed,
createdAt: stats.birthtime,
modifiedAt: stats.mtime,
};
try {
// 读取文件头部的元数据信息
let content;
@@ -106,9 +107,9 @@ router.get('/list', async (req, res) => {
} else {
content = fs.readFileSync(filePath, 'utf8');
}
const backupData = JSON.parse(content);
// 提取关键元数据
metadata.description = backupData.description || '';
metadata.backupType = backupData.backupType || 'full';
@@ -117,19 +118,18 @@ router.get('/list', async (req, res) => {
metadata.checksum = backupData.checksum;
metadata.metadata = backupData.metadata;
metadata.systemInfo = backupData.systemInfo;
// 判断是否为上传的文件(通过文件名判断)
metadata.isUploaded = f.startsWith('uploaded_');
} catch (error) {
// 如果读取失败,标记为无效文件
metadata.invalid = true;
metadata.error = '无法读取文件内容';
}
return metadata;
});
const resolvedFiles = await Promise.all(files);
res.json({
@@ -201,7 +201,7 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
const sendProgress = (data) => {
const sendProgress = data => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
@@ -211,16 +211,25 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
const validation = await validateBackupFile(filePath);
if (!validation.valid) {
sendProgress({ stage: 'error', message: `备份文件验证失败: ${validation.error}`, progress: 0 });
sendProgress({
stage: 'error',
message: `备份文件验证失败: ${validation.error}`,
progress: 0,
});
res.end();
return;
}
sendProgress({ stage: 'validate', message: '备份文件验证通过', progress: 10, metadata: validation.metadata });
sendProgress({
stage: 'validate',
message: '备份文件验证通过',
progress: 10,
metadata: validation.metadata,
});
const buffer = fs.readFileSync(filePath);
const isCompressed = filePath.endsWith('.gz');
let backupData;
if (isCompressed) {
sendProgress({ stage: 'decompress', message: '正在解压备份文件...', progress: 15 });
@@ -243,10 +252,10 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
processedTables++;
const progress = 20 + Math.floor((processedTables / totalTables) * 70);
const statusMap = {
'restored': '已恢复',
'skipped': '已跳过',
'empty': '无数据',
'error': '错误',
restored: '已恢复',
skipped: '已跳过',
empty: '无数据',
error: '错误',
};
sendProgress({
stage: 'restore',
@@ -261,9 +270,9 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
},
});
sendProgress({
stage: 'complete',
message: '恢复完成!',
sendProgress({
stage: 'complete',
message: '恢复完成!',
progress: 100,
result: {
tablesRestored: result.tablesRestored,
@@ -272,7 +281,7 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
restoredAt: result.restoredAt,
tableDetails: result.tableDetails,
fileDetails: result.fileDetails,
}
},
});
res.end();
@@ -342,7 +351,7 @@ router.post('/upload', async (req, res) => {
const backupFile = req.files.backup;
const originalName = backupFile.name || '';
const nameLower = originalName.toLowerCase();
// 验证文件类型
if (!nameLower.endsWith('.json') && !nameLower.endsWith('.gz')) {
return res.status(400).json({
@@ -350,15 +359,15 @@ router.post('/upload', async (req, res) => {
message: '只支持 JSON 或 GZ 格式的备份文件',
});
}
const isCompressed = nameLower.endsWith('.gz');
console.log(`上传备份文件: ${originalName}, 压缩: ${isCompressed}, 大小: ${backupFile.size}`);
// 保存到临时文件
const tempFilename = `upload_${Date.now()}`;
const tempPath = path.join(tempDir, tempFilename);
await backupFile.mv(tempPath);
const validation = await validateBackupFile(tempPath, { isCompressed });
@@ -411,7 +420,7 @@ router.get('/download/:filename', (req, res) => {
});
}
res.download(filePath, filename, (err) => {
res.download(filePath, filename, err => {
if (err) {
console.error('下载备份文件失败:', err);
}
@@ -463,7 +472,9 @@ router.get('/info', (req, res) => {
let backupCount = 0;
if (fs.existsSync(backupPath)) {
const files = fs.readdirSync(backupPath).filter(f => f.endsWith('.json') || f.endsWith('.json.gz'));
const files = fs
.readdirSync(backupPath)
.filter(f => f.endsWith('.json') || f.endsWith('.json.gz'));
backupCount = files.length;
files.forEach(f => {
const stats = fs.statSync(path.join(backupPath, f));
@@ -491,7 +502,9 @@ router.get('/info', (req, res) => {
});
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
if (bytes === 0) {
return '0 B';
}
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
@@ -565,7 +578,9 @@ router.post('/auto/settings', (req, res) => {
} = req.body;
const newSettings = {};
if (enabled !== undefined) newSettings.enabled = enabled;
if (enabled !== undefined) {
newSettings.enabled = enabled;
}
if (hour !== undefined || minute !== undefined) {
newSettings.hour = hour || 2;
newSettings.minute = minute || 0;
@@ -579,12 +594,24 @@ router.post('/auto/settings', (req, res) => {
}
newSettings.cronExpression = cronExpression;
}
if (description) newSettings.description = description;
if (includeFiles !== undefined) newSettings.includeFiles = includeFiles;
if (compress !== undefined) newSettings.compress = compress;
if (maxCount !== undefined) newSettings.maxCount = maxCount;
if (maxAgeDays !== undefined) newSettings.maxAgeDays = maxAgeDays;
if (backupType !== undefined) newSettings.backupType = backupType;
if (description) {
newSettings.description = description;
}
if (includeFiles !== undefined) {
newSettings.includeFiles = includeFiles;
}
if (compress !== undefined) {
newSettings.compress = compress;
}
if (maxCount !== undefined) {
newSettings.maxCount = maxCount;
}
if (maxAgeDays !== undefined) {
newSettings.maxAgeDays = maxAgeDays;
}
if (backupType !== undefined) {
newSettings.backupType = backupType;
}
const success = updateAutoBackupSettings(newSettings);
if (success) {
@@ -614,7 +641,7 @@ router.post('/auto/settings', (req, res) => {
router.post('/auto/execute', async (req, res) => {
try {
const { description, includeFiles, compress, backupType } = req.body;
const result = await executeBackupNow({
description: description || '手动触发备份',
includeFiles,
@@ -648,7 +675,7 @@ router.post('/auto/execute', async (req, res) => {
router.post('/auto/test-cron', (req, res) => {
try {
const { cronExpression } = req.body;
if (!cronExpression) {
return res.status(400).json({
success: false,
@@ -657,7 +684,7 @@ router.post('/auto/test-cron', (req, res) => {
}
const isValid = validateCronExpression(cronExpression);
res.json({
success: isValid,
message: isValid ? 'Cron 表达式有效' : 'Cron 表达式无效',
@@ -700,14 +727,14 @@ router.get('/remote/targets', (req, res) => {
router.get('/remote/targets/:id', (req, res) => {
try {
const target = getTarget(req.params.id);
if (!target) {
return res.status(404).json({
success: false,
message: '目标不存在',
});
}
res.json({
success: true,
data: { target },
@@ -726,16 +753,16 @@ router.get('/remote/targets/:id', (req, res) => {
router.post('/remote/targets', (req, res) => {
try {
const targetData = req.body;
if (!targetData.name || !targetData.protocol) {
return res.status(400).json({
success: false,
message: '请提供目标名称和协议类型',
});
}
const target = addTarget(targetData);
res.status(201).json({
success: true,
message: '远端备份目标已添加',
@@ -756,7 +783,7 @@ router.put('/remote/targets/:id', (req, res) => {
try {
const updates = req.body;
const target = updateTarget(req.params.id, updates);
res.json({
success: true,
message: '远端备份目标已更新',
@@ -776,14 +803,14 @@ router.put('/remote/targets/:id', (req, res) => {
router.delete('/remote/targets/:id', (req, res) => {
try {
const deleted = deleteTarget(req.params.id);
if (!deleted) {
return res.status(404).json({
success: false,
message: '目标不存在',
});
}
res.json({
success: true,
message: '远端备份目标已删除',
@@ -818,7 +845,7 @@ router.post('/remote/test', async (req, res) => {
}
const result = await testRemoteConnection(config);
if (result.success) {
res.json({
success: true,
@@ -846,16 +873,16 @@ router.post('/remote/test', async (req, res) => {
router.post('/remote/targets/:id/test', async (req, res) => {
try {
const target = getTarget(req.params.id);
if (!target) {
return res.status(404).json({
success: false,
message: '目标不存在',
});
}
const result = await testRemoteConnection(target);
if (result.success) {
res.json({
success: true,
@@ -924,7 +951,7 @@ router.get('/remote/protocols', (req, res) => {
value,
label: PROTOCOL_LABELS[value],
}));
res.json({
success: true,
data: { protocols },
@@ -943,45 +970,45 @@ router.get('/remote/protocols', (req, res) => {
router.post('/remote/upload', async (req, res) => {
try {
const { filename, targetIds } = req.body;
if (!filename) {
return res.status(400).json({
success: false,
message: '请提供备份文件名',
});
}
const backupPath = getBackupPath();
const filePath = path.join(backupPath, filename);
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: '备份文件不存在',
});
}
const { uploadToRemote } = require('../utils/remoteBackup');
const { getTarget } = require('../utils/remoteBackupConfig');
const targets = targetIds
const targets = targetIds
? targetIds.map(id => getTarget(id)).filter(Boolean)
: getEnabledTargets();
if (targets.length === 0) {
return res.status(400).json({
success: false,
message: '没有可用的远端目标',
});
}
const uploadResults = [];
for (const target of targets) {
try {
const remotePath = `${target.prefix || 'backups/'}${filename}`;
const result = await uploadToRemote(target, filePath, remotePath);
uploadResults.push({
targetId: target.id,
targetName: target.name,
@@ -997,11 +1024,11 @@ router.post('/remote/upload', async (req, res) => {
});
}
}
res.json({
success: true,
message: '上传完成',
data: {
data: {
filename,
results: uploadResults,
},
@@ -1022,14 +1049,14 @@ router.post('/remote/upload', async (req, res) => {
router.get('/logs', async (req, res) => {
try {
const { page = 1, pageSize = 20, logType, status } = req.query;
const result = await getBackupLogs({
page: parseInt(page),
pageSize: parseInt(pageSize),
logType,
status,
});
res.json({
success: true,
data: result,
@@ -1049,14 +1076,14 @@ router.get('/logs/:id', async (req, res) => {
try {
const { id } = req.params;
const log = await getBackupLogById(parseInt(id));
if (!log) {
return res.status(404).json({
success: false,
message: '备份日志不存在',
});
}
res.json({
success: true,
data: log,
@@ -1076,7 +1103,7 @@ router.delete('/logs/clean', async (req, res) => {
try {
const { days = 30 } = req.body;
const deletedCount = await deleteOldLogs(parseInt(days));
res.json({
success: true,
message: '清理完成',
+141 -119
View File
@@ -8,10 +8,7 @@ const DevicePort = require('../models/DevicePort');
// 辅助函数:更新端口状态
async function updatePortStatus(deviceId, portName, status) {
try {
await DevicePort.update(
{ status },
{ where: { deviceId, portName } }
);
await DevicePort.update({ status }, { where: { deviceId, portName } });
} catch (error) {
console.error(`更新端口状态失败: ${deviceId}:${portName} -> ${status}`, error);
}
@@ -29,51 +26,58 @@ async function freePort(deviceId, portName) {
router.get('/', async (req, res) => {
try {
const { sourceDeviceId, targetDeviceId, status, cableType, page = 1, pageSize = 10 } = req.query;
const {
sourceDeviceId,
targetDeviceId,
status,
cableType,
page = 1,
pageSize = 10,
} = req.query;
const offset = (page - 1) * pageSize;
const where = {};
if (sourceDeviceId) {
where.sourceDeviceId = sourceDeviceId;
}
if (targetDeviceId) {
where.targetDeviceId = targetDeviceId;
}
if (status && status !== 'all') {
where.status = status;
}
if (cableType && cableType !== 'all') {
where.cableType = cableType;
}
const { count, rows } = await Cable.findAndCountAll({
where,
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
offset,
limit: parseInt(pageSize),
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
res.json({
total: count,
cables: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取接线列表失败:', error);
@@ -84,28 +88,25 @@ router.get('/', async (req, res) => {
router.get('/device/:deviceId', async (req, res) => {
try {
const { deviceId } = req.params;
const cables = await Cable.findAll({
where: {
[Op.or]: [
{ sourceDeviceId: deviceId },
{ targetDeviceId: deviceId }
]
[Op.or]: [{ sourceDeviceId: deviceId }, { targetDeviceId: deviceId }],
},
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
res.json(cables);
} catch (error) {
console.error('获取设备接线失败:', error);
@@ -117,15 +118,15 @@ router.get('/device/:deviceId', async (req, res) => {
router.get('/rack/:rackId', async (req, res) => {
try {
const { rackId } = req.params;
// 1. 找出该机柜下的所有设备ID
const devices = await Device.findAll({
where: { rackId: rackId },
attributes: ['deviceId']
attributes: ['deviceId'],
});
const deviceIds = devices.map(d => d.deviceId);
if (deviceIds.length === 0) {
return res.json([]);
}
@@ -135,23 +136,23 @@ router.get('/rack/:rackId', async (req, res) => {
where: {
[Op.or]: [
{ sourceDeviceId: { [Op.in]: deviceIds } },
{ targetDeviceId: { [Op.in]: deviceIds } }
]
{ targetDeviceId: { [Op.in]: deviceIds } },
],
},
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
res.json(cables);
} catch (error) {
console.error('获取机柜接线失败:', error);
@@ -175,22 +176,22 @@ router.post('/check-conflict', async (req, res) => {
where: {
[Op.or]: [
{ sourceDeviceId, sourcePort },
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort }
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort },
],
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } })
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } }),
},
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type']
attributes: ['deviceId', 'name', 'type'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type']
}
]
attributes: ['deviceId', 'name', 'type'],
},
],
});
if (sourceConflict) {
@@ -198,7 +199,7 @@ router.post('/check-conflict', async (req, res) => {
type: 'source',
port: sourcePort,
deviceId: sourceDeviceId,
existingCable: sourceConflict
existingCable: sourceConflict,
});
}
@@ -207,22 +208,22 @@ router.post('/check-conflict', async (req, res) => {
where: {
[Op.or]: [
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
{ targetDeviceId, targetPort }
{ targetDeviceId, targetPort },
],
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } })
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } }),
},
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type']
attributes: ['deviceId', 'name', 'type'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type']
}
]
attributes: ['deviceId', 'name', 'type'],
},
],
});
if (targetConflict) {
@@ -230,13 +231,13 @@ router.post('/check-conflict', async (req, res) => {
type: 'target',
port: targetPort,
deviceId: targetDeviceId,
existingCable: targetConflict
existingCable: targetConflict,
});
}
res.json({
hasConflict: conflicts.length > 0,
conflicts
conflicts,
});
} catch (error) {
console.error('检查接线冲突失败:', error);
@@ -246,7 +247,18 @@ router.post('/check-conflict', async (req, res) => {
router.post('/', async (req, res) => {
try {
const { cableId, sourceDeviceId, sourcePort, targetDeviceId, targetPort, cableType, cableLength, status, description, force } = req.body;
const {
cableId,
sourceDeviceId,
sourcePort,
targetDeviceId,
targetPort,
cableType,
cableLength,
status,
description,
force,
} = req.body;
if (!sourceDeviceId || !sourcePort || !targetDeviceId || !targetPort) {
return res.status(400).json({ error: '缺少必填字段' });
@@ -262,16 +274,16 @@ router.post('/', async (req, res) => {
where: {
[Op.or]: [
{ sourceDeviceId, sourcePort },
{ targetDeviceId, targetPort }
]
}
{ targetDeviceId, targetPort },
],
},
});
if (existingCable) {
return res.status(409).json({
error: '端口已被占用',
conflict: true,
existingCable
existingCable,
});
}
}
@@ -284,9 +296,9 @@ router.post('/', async (req, res) => {
{ sourceDeviceId, sourcePort },
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort },
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
{ targetDeviceId, targetPort }
]
}
{ targetDeviceId, targetPort },
],
},
});
for (const cable of existingCables) {
@@ -308,7 +320,7 @@ router.post('/', async (req, res) => {
cableType: cableType || 'ethernet',
cableLength,
status: status || 'normal',
description
description,
});
const createdCable = await Cable.findByPk(cable.cableId, {
@@ -316,14 +328,14 @@ router.post('/', async (req, res) => {
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
// 自动将源端口和目标端口状态设为occupied
@@ -340,44 +352,49 @@ router.post('/', async (req, res) => {
router.post('/batch', async (req, res) => {
try {
const { cables } = req.body;
if (!cables || !Array.isArray(cables) || cables.length === 0) {
return res.status(400).json({ error: '请提供有效的接线数据' });
}
const results = {
total: cables.length,
success: 0,
failed: 0,
errors: []
errors: [],
};
for (let i = 0; i < cables.length; i++) {
const cableData = cables[i];
try {
if (!cableData.cableId || !cableData.sourceDeviceId || !cableData.sourcePort ||
!cableData.targetDeviceId || !cableData.targetPort) {
if (
!cableData.cableId ||
!cableData.sourceDeviceId ||
!cableData.sourcePort ||
!cableData.targetDeviceId ||
!cableData.targetPort
) {
throw new Error('缺少必填字段');
}
if (cableData.sourceDeviceId === cableData.targetDeviceId) {
throw new Error('源设备和目标设备不能相同');
}
const existingCable = await Cable.findOne({
where: {
[Op.or]: [
{ sourceDeviceId: cableData.sourceDeviceId, sourcePort: cableData.sourcePort },
{ targetDeviceId: cableData.targetDeviceId, targetPort: cableData.targetPort }
]
}
{ targetDeviceId: cableData.targetDeviceId, targetPort: cableData.targetPort },
],
},
});
if (existingCable) {
throw new Error('端口已被占用');
}
await Cable.create({
cableId: cableData.cableId,
sourceDeviceId: cableData.sourceDeviceId,
@@ -387,24 +404,24 @@ router.post('/batch', async (req, res) => {
cableType: cableData.cableType || 'ethernet',
cableLength: cableData.cableLength,
status: cableData.status || 'normal',
description: cableData.description
description: cableData.description,
});
// 自动将源端口和目标端口状态设为occupied
await occupyPort(cableData.sourceDeviceId, cableData.sourcePort);
await occupyPort(cableData.targetDeviceId, cableData.targetPort);
results.success++;
} catch (error) {
results.failed++;
results.errors.push({
index: i + 1,
cableId: cableData.cableId,
error: error.message
error: error.message,
});
}
}
res.json(results);
} catch (error) {
console.error('批量创建接线失败:', error);
@@ -416,48 +433,53 @@ router.put('/:cableId', async (req, res) => {
try {
// 获取更新前的接线信息
const oldCable = await Cable.findByPk(req.params.cableId);
if (!oldCable) {
return res.status(404).json({ error: '接线不存在' });
}
const { sourceDeviceId: oldSourceDeviceId, sourcePort: oldSourcePort, targetDeviceId: oldTargetDeviceId, targetPort: oldTargetPort } = oldCable;
const {
sourceDeviceId: oldSourceDeviceId,
sourcePort: oldSourcePort,
targetDeviceId: oldTargetDeviceId,
targetPort: oldTargetPort,
} = oldCable;
const [updated] = await Cable.update(req.body, {
where: { cableId: req.params.cableId }
where: { cableId: req.params.cableId },
});
if (updated) {
const cable = await Cable.findByPk(req.params.cableId, {
include: [
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
// 同步更新端口状态
// 源端口变更:释放旧端口,占用新端口
if (oldSourceDeviceId !== sourceDeviceId || oldSourcePort !== sourcePort) {
await freePort(oldSourceDeviceId, oldSourcePort);
await occupyPort(sourceDeviceId, sourcePort);
}
// 目标端口变更:释放旧端口,占用新端口
if (oldTargetDeviceId !== targetDeviceId || oldTargetPort !== targetPort) {
await freePort(oldTargetDeviceId, oldTargetPort);
await occupyPort(targetDeviceId, targetPort);
}
res.json(cable);
} else {
res.status(404).json({ error: '接线不存在' });
@@ -472,22 +494,22 @@ router.delete('/:cableId', async (req, res) => {
try {
// 先获取接线信息,用于后续恢复端口状态
const cable = await Cable.findByPk(req.params.cableId);
if (!cable) {
return res.status(404).json({ error: '接线不存在' });
}
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
const deleted = await Cable.destroy({
where: { cableId: req.params.cableId }
where: { cableId: req.params.cableId },
});
if (deleted) {
// 自动将源端口和目标端口状态恢复为free
await freePort(sourceDeviceId, sourcePort);
await freePort(targetDeviceId, targetPort);
res.status(204).json();
} else {
res.status(404).json({ error: '接线不存在' });
@@ -501,29 +523,29 @@ router.delete('/:cableId', async (req, res) => {
router.delete('/batch', async (req, res) => {
try {
const { cableIds } = req.body;
if (!cableIds || !Array.isArray(cableIds) || cableIds.length === 0) {
return res.status(400).json({ error: '请提供有效的接线ID列表' });
}
// 先获取所有要删除的接线信息,用于后续恢复端口状态
const cables = await Cable.findAll({
where: { cableId: { [Op.in]: cableIds } }
where: { cableId: { [Op.in]: cableIds } },
});
const deletedCount = await Cable.destroy({
where: { cableId: { [Op.in]: cableIds } }
where: { cableId: { [Op.in]: cableIds } },
});
// 自动将所有相关端口状态恢复为free
for (const cable of cables) {
await freePort(cable.sourceDeviceId, cable.sourcePort);
await freePort(cable.targetDeviceId, cable.targetPort);
}
res.json({
message: `批量删除成功,已删除 ${deletedCount} 条接线`,
deletedCount
deletedCount,
});
} catch (error) {
console.error('批量删除接线失败:', error);
@@ -538,20 +560,20 @@ router.get('/:cableId', async (req, res) => {
{
model: Device,
as: 'sourceDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: Device,
as: 'targetDevice',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
if (!cable) {
return res.status(404).json({ error: '接线不存在' });
}
res.json(cable);
} catch (error) {
console.error('获取接线详情失败:', error);
+13 -7
View File
@@ -13,7 +13,7 @@ router.get('/', async (req, res) => {
if (keyword) {
where[Op.or] = [
{ name: { [Op.like]: `%${keyword}%` } },
{ description: { [Op.like]: `%${keyword}%` } }
{ description: { [Op.like]: `%${keyword}%` } },
];
}
@@ -23,9 +23,12 @@ router.get('/', async (req, res) => {
const { count, rows } = await ConsumableCategory.findAndCountAll({
where,
order: [['sortOrder', 'ASC'], ['id', 'DESC']],
order: [
['sortOrder', 'ASC'],
['id', 'DESC'],
],
offset,
limit: parseInt(pageSize)
limit: parseInt(pageSize),
});
res.json({
@@ -33,7 +36,7 @@ router.get('/', async (req, res) => {
total: count,
currentPage: parseInt(page),
pageSize: parseInt(pageSize),
totalPages: Math.ceil(count / pageSize)
totalPages: Math.ceil(count / pageSize),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -44,7 +47,10 @@ router.get('/list', async (req, res) => {
try {
const categories = await ConsumableCategory.findAll({
where: { status: 'active' },
order: [['sortOrder', 'ASC'], ['name', 'ASC']]
order: [
['sortOrder', 'ASC'],
['name', 'ASC'],
],
});
res.json(categories);
} catch (error) {
@@ -77,7 +83,7 @@ router.post('/', async (req, res) => {
name,
description,
sortOrder: sortOrder || 0,
status: status || 'active'
status: status || 'active',
});
res.status(201).json(category);
@@ -106,7 +112,7 @@ router.put('/:id', async (req, res) => {
name: name || category.name,
description: description !== undefined ? description : category.description,
sortOrder: sortOrder !== undefined ? sortOrder : category.sortOrder,
status: status || category.status
status: status || category.status,
});
res.json(category);
+63 -59
View File
@@ -10,42 +10,40 @@ router.get('/', async (req, res) => {
try {
const { consumableId, type, startDate, endDate, page = 1, pageSize = 10 } = req.query;
const offset = (page - 1) * pageSize;
const where = {};
if (consumableId) {
where.consumableId = consumableId;
}
if (type && type !== 'all') {
where.type = type;
}
if (startDate && endDate) {
where.createdAt = {
[Op.between]: [new Date(startDate), new Date(endDate)]
[Op.between]: [new Date(startDate), new Date(endDate)],
};
} else if (startDate) {
where.createdAt = { [Op.gte]: new Date(startDate) };
} else if (endDate) {
where.createdAt = { [Op.lte]: new Date(endDate) };
}
const { count, rows } = await ConsumableRecord.findAndCountAll({
where,
include: [
{ model: Consumable, as: 'consumable', attributes: ['name', 'category', 'unit'] }
],
include: [{ model: Consumable, as: 'consumable', attributes: ['name', 'category', 'unit'] }],
offset,
limit: parseInt(pageSize),
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
res.json({
total: count,
records: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -54,19 +52,19 @@ router.get('/', async (req, res) => {
router.post('/', async (req, res) => {
const transaction = await sequelize.transaction();
try {
const { consumableId, type, quantity, operator, reason, recipient, notes } = req.body;
const consumable = await Consumable.findByPk(consumableId);
if (!consumable) {
await transaction.rollback();
return res.status(404).json({ error: '耗材不存在' });
}
const previousStock = consumable.currentStock;
let newStock;
if (type === 'in') {
newStock = previousStock + quantity;
} else if (type === 'out') {
@@ -79,41 +77,47 @@ router.post('/', async (req, res) => {
await transaction.rollback();
return res.status(400).json({ error: '操作类型无效' });
}
await consumable.update({ currentStock: newStock }, { transaction });
const record = await ConsumableRecord.create({
consumableId,
type,
quantity,
previousStock,
currentStock: newStock,
operator,
reason,
recipient,
notes
}, { transaction });
await ConsumableLog.create({
consumableId,
consumableName: consumable.name,
operationType: type,
quantity: type === 'in' ? parseFloat(quantity) : -parseFloat(quantity),
previousStock,
currentStock: newStock,
operator,
reason,
notes
}, { transaction });
await consumable.update({ currentStock: newStock }, { transaction });
const record = await ConsumableRecord.create(
{
consumableId,
type,
quantity,
previousStock,
currentStock: newStock,
operator,
reason,
recipient,
notes,
},
{ transaction }
);
await ConsumableLog.create(
{
consumableId,
consumableName: consumable.name,
operationType: type,
quantity: type === 'in' ? parseFloat(quantity) : -parseFloat(quantity),
previousStock,
currentStock: newStock,
operator,
reason,
notes,
},
{ transaction }
);
await transaction.commit();
res.status(201).json({
record,
consumable: {
previousStock,
currentStock: newStock
}
currentStock: newStock,
},
});
} catch (error) {
await transaction.rollback();
@@ -131,10 +135,10 @@ router.get('/statistics', async (req, res) => {
const startDateTime = new Date(startDate);
const endDateTime = new Date(endDate);
endDateTime.setHours(23, 59, 59, 999); // 设置 endDate 为当天最后一刻
dateWhere.createdAt = {
[Op.gte]: startDateTime,
[Op.lte]: endDateTime
[Op.lte]: endDateTime,
};
}
@@ -146,14 +150,14 @@ router.get('/statistics', async (req, res) => {
const records = await ConsumableRecord.findAll({
where: dateWhere,
include: [
{
model: Consumable,
{
model: Consumable,
as: 'consumable',
attributes: ['name', 'category'],
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined
}
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined,
},
],
attributes: ['type', 'quantity']
attributes: ['type', 'quantity'],
});
let inCount = 0;
@@ -182,15 +186,15 @@ router.get('/statistics', async (req, res) => {
const recentRecords = await ConsumableRecord.findAll({
where: dateWhere,
include: [
{
model: Consumable,
{
model: Consumable,
as: 'consumable',
attributes: ['name', 'category'],
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined
}
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined,
},
],
order: [['createdAt', 'DESC']],
limit: 10
limit: 10,
});
res.json({
@@ -202,7 +206,7 @@ router.get('/statistics', async (req, res) => {
byType: Object.entries(typeMap).map(([type, data]) => ({
type,
totalQuantity: data.totalQuantity,
count: data.count
count: data.count,
})),
recentRecords: recentRecords.map(record => ({
recordId: record.recordId,
@@ -213,8 +217,8 @@ router.get('/statistics', async (req, res) => {
consumableId: record.consumableId,
consumableName: record.consumable?.name || '未知耗材',
category: record.consumable?.category || null,
unit: record.consumable?.unit || '个'
}))
unit: record.consumable?.unit || '个',
})),
});
} catch (error) {
res.status(500).json({ error: error.message });
File diff suppressed because it is too large Load Diff
+35 -15
View File
@@ -1,6 +1,13 @@
const express = require('express');
const router = express.Router();
const { logDangerousOperation, getDangerousOperationsLogs, cleanOldLogs, DANGEROUS_OPERATION_TYPES, RISK_LEVELS, calculateRiskLevel } = require('../utils/dangerousOperationLogger');
const {
logDangerousOperation,
getDangerousOperationsLogs,
cleanOldLogs,
DANGEROUS_OPERATION_TYPES,
RISK_LEVELS,
calculateRiskLevel,
} = require('../utils/dangerousOperationLogger');
router.post('/log', async (req, res) => {
try {
@@ -20,10 +27,12 @@ router.post('/log', async (req, res) => {
return res.status(400).json({ error: '缺少必需参数 operationType 或 operationName' });
}
const riskLevel = metadata.riskLevel || calculateRiskLevel(operationType, metadata.itemCount || 1, {
hasRelatedData: metadata.relatedDataCount > 0,
isSystemLevel: metadata.isSystemLevel,
});
const riskLevel =
metadata.riskLevel ||
calculateRiskLevel(operationType, metadata.itemCount || 1, {
hasRelatedData: metadata.relatedDataCount > 0,
isSystemLevel: metadata.isSystemLevel,
});
await logDangerousOperation(req, {
operationType,
@@ -49,7 +58,17 @@ router.post('/log', async (req, res) => {
router.get('/logs', async (req, res) => {
try {
const { operationType, targetType, success, startDate, endDate, username, riskLevel, page = 1, pageSize = 50 } = req.query;
const {
operationType,
targetType,
success,
startDate,
endDate,
username,
riskLevel,
page = 1,
pageSize = 50,
} = req.query;
const filters = {
operationType,
@@ -121,14 +140,10 @@ router.get('/risk-assessment', async (req, res) => {
try {
const { operationType, itemCount, hasRelatedData, isSystemLevel } = req.query;
const riskLevel = calculateRiskLevel(
operationType,
parseInt(itemCount) || 1,
{
hasRelatedData: hasRelatedData === 'true',
isSystemLevel: isSystemLevel === 'true',
}
);
const riskLevel = calculateRiskLevel(operationType, parseInt(itemCount) || 1, {
hasRelatedData: hasRelatedData === 'true',
isSystemLevel: isSystemLevel === 'true',
});
const riskDescriptions = {
[RISK_LEVELS.EXTREME]: '极高风险操作,需要输入确认关键词才能执行',
@@ -141,7 +156,12 @@ router.get('/risk-assessment', async (req, res) => {
riskLevel,
description: riskDescriptions[riskLevel],
requiresKeyword: riskLevel === RISK_LEVELS.EXTREME,
confirmationLevel: riskLevel === RISK_LEVELS.EXTREME ? 'KEYWORD' : riskLevel === RISK_LEVELS.HIGH ? 'ENHANCED' : 'STANDARD',
confirmationLevel:
riskLevel === RISK_LEVELS.EXTREME
? 'KEYWORD'
: riskLevel === RISK_LEVELS.HIGH
? 'ENHANCED'
: 'STANDARD',
});
} catch (error) {
console.error('Failed to assess risk:', error);
+13 -13
View File
@@ -6,7 +6,7 @@ const DeviceField = require('../models/DeviceField');
router.get('/', async (req, res) => {
try {
const fields = await DeviceField.findAll({
order: [['order', 'ASC']]
order: [['order', 'ASC']],
});
res.json(fields);
} catch (error) {
@@ -41,7 +41,7 @@ router.post('/', async (req, res) => {
router.put('/:fieldId', async (req, res) => {
try {
const [updated] = await DeviceField.update(req.body, {
where: { fieldId: req.params.fieldId }
where: { fieldId: req.params.fieldId },
});
if (updated) {
const updatedField = await DeviceField.findByPk(req.params.fieldId);
@@ -59,20 +59,20 @@ router.delete('/:fieldId', async (req, res) => {
try {
// 先查询字段信息
const field = await DeviceField.findByPk(req.params.fieldId);
if (!field) {
return res.status(404).json({ error: '字段不存在' });
}
// 检查是否为系统字段
if (field.isSystem) {
return res.status(403).json({ error: '系统字段不可删除' });
}
const deleted = await DeviceField.destroy({
where: { fieldId: req.params.fieldId }
where: { fieldId: req.params.fieldId },
});
if (deleted) {
res.status(204).json();
} else {
@@ -87,17 +87,17 @@ router.delete('/:fieldId', async (req, res) => {
router.post('/config', async (req, res) => {
try {
const fieldConfigs = req.body;
if (!Array.isArray(fieldConfigs)) {
return res.status(400).json({ error: '输入必须是数组' });
}
const updatedFields = [];
for (const config of fieldConfigs) {
const existingField = await DeviceField.findOne({ where: { fieldName: config.fieldName } });
if (existingField) {
await existingField.update({
await existingField.update({
visible: config.visible !== undefined ? config.visible : existingField.visible,
required: config.required !== undefined ? config.required : existingField.required,
displayName: config.displayName || existingField.displayName,
@@ -115,11 +115,11 @@ router.post('/config', async (req, res) => {
updatedFields.push(newField);
}
}
res.json(updatedFields);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
module.exports = router;
+109 -94
View File
@@ -14,49 +14,49 @@ router.get('/', async (req, res) => {
try {
const { deviceId, status, portType, portSpeed, page = 1, pageSize = 10 } = req.query;
const offset = (page - 1) * pageSize;
const where = {};
if (deviceId) {
where.deviceId = deviceId;
}
if (status && status !== 'all') {
where.status = status;
}
if (portType && portType !== 'all') {
where.portType = portType;
}
if (portSpeed && portSpeed !== 'all') {
where.portSpeed = portSpeed;
}
const { count, rows } = await DevicePort.findAndCountAll({
where,
include: [
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: NetworkCard,
as: 'networkCard',
attributes: ['nicId', 'name']
}
attributes: ['nicId', 'name'],
},
],
offset,
limit: parseInt(pageSize),
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
res.json({
total: count,
ports: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取端口列表失败:', error);
@@ -68,24 +68,24 @@ router.get('/', async (req, res) => {
router.get('/device/:deviceId', async (req, res) => {
try {
const { deviceId } = req.params;
const ports = await DevicePort.findAll({
where: { deviceId },
include: [
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
{
model: NetworkCard,
as: 'networkCard',
attributes: ['nicId', 'name']
}
attributes: ['nicId', 'name'],
},
],
order: [['portName', 'ASC']]
order: [['portName', 'ASC']],
});
res.json(ports);
} catch (error) {
console.error('获取设备端口失败:', error);
@@ -95,22 +95,23 @@ router.get('/device/:deviceId', async (req, res) => {
router.post('/', async (req, res) => {
try {
const { portId, deviceId, nicId, portName, portType, portSpeed, status, vlanId, description } = req.body;
const { portId, deviceId, nicId, portName, portType, portSpeed, status, vlanId, description } =
req.body;
if (!deviceId || !portName) {
return res.status(400).json({ error: '缺少必填字段' });
}
const existingPort = await DevicePort.findOne({
where: { deviceId, portName }
where: { deviceId, portName },
});
if (existingPort) {
return res.status(400).json({ error: '该设备的端口名称已存在' });
}
const autoPortId = portId || `PORT-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
const port = await DevicePort.create({
portId: autoPortId,
deviceId,
@@ -120,19 +121,19 @@ router.post('/', async (req, res) => {
portSpeed: portSpeed || '1G',
status: status || 'free',
vlanId,
description
description,
});
const createdPort = await DevicePort.findByPk(port.portId, {
include: [
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
res.status(201).json(createdPort);
} catch (error) {
console.error('创建端口失败:', error);
@@ -154,7 +155,7 @@ router.post('/batch', async (req, res) => {
failed: 0,
skipped: 0,
updated: 0,
errors: []
errors: [],
};
const transaction = await DevicePort.sequelize.transaction();
@@ -177,7 +178,9 @@ router.post('/batch', async (req, res) => {
if (isServer) {
if (!portData.nicId && !portData.网卡名称) {
throw new Error(`服务器 ${portData.deviceId} 的端口必须关联网卡,请先在网卡管理中添加网卡`);
throw new Error(
`服务器 ${portData.deviceId} 的端口必须关联网卡,请先在网卡管理中添加网卡`
);
}
let nicId = portData.nicId;
@@ -185,10 +188,12 @@ router.post('/batch', async (req, res) => {
if (!nicId && portData.网卡名称) {
const networkCard = await NetworkCard.findOne({
where: { deviceId: portData.deviceId, name: portData.网卡名称 },
transaction
transaction,
});
if (!networkCard) {
throw new Error(`服务器 ${portData.deviceId} 的网卡"${portData.网卡名称}"不存在,请先在网卡管理中添加该网卡`);
throw new Error(
`服务器 ${portData.deviceId} 的网卡"${portData.网卡名称}"不存在,请先在网卡管理中添加该网卡`
);
}
nicId = networkCard.nicId;
}
@@ -212,7 +217,7 @@ router.post('/batch', async (req, res) => {
const existingPort = await DevicePort.findOne({
where: { deviceId: portData.deviceId, portName: portData.portName },
transaction
transaction,
});
if (existingPort) {
@@ -221,17 +226,23 @@ router.post('/batch', async (req, res) => {
continue;
}
if (updateExisting) {
await DevicePort.update({
portType: portData.portType || existingPort.portType,
portSpeed: portData.portSpeed || existingPort.portSpeed,
status: portData.status || existingPort.status,
vlanId: portData.vlanId !== undefined ? portData.vlanId : existingPort.vlanId,
description: portData.description !== undefined ? portData.description : existingPort.description,
nicId: portData.nicId !== undefined ? portData.nicId : existingPort.nicId
}, {
where: { portId: existingPort.portId },
transaction
});
await DevicePort.update(
{
portType: portData.portType || existingPort.portType,
portSpeed: portData.portSpeed || existingPort.portSpeed,
status: portData.status || existingPort.status,
vlanId: portData.vlanId !== undefined ? portData.vlanId : existingPort.vlanId,
description:
portData.description !== undefined
? portData.description
: existingPort.description,
nicId: portData.nicId !== undefined ? portData.nicId : existingPort.nicId,
},
{
where: { portId: existingPort.portId },
transaction,
}
);
results.updated++;
results.success++;
continue;
@@ -239,17 +250,20 @@ router.post('/batch', async (req, res) => {
throw new Error('该设备的端口名称已存在');
}
await DevicePort.create({
portId: portData.portId,
deviceId: portData.deviceId,
nicId: portData.nicId || null,
portName: portData.portName,
portType: portData.portType || 'RJ45',
portSpeed: portData.portSpeed || '1G',
status: portData.status || 'free',
vlanId: portData.vlanId,
description: portData.description
}, { transaction });
await DevicePort.create(
{
portId: portData.portId,
deviceId: portData.deviceId,
nicId: portData.nicId || null,
portName: portData.portName,
portType: portData.portType || 'RJ45',
portSpeed: portData.portSpeed || '1G',
status: portData.status || 'free',
vlanId: portData.vlanId,
description: portData.description,
},
{ transaction }
);
results.success++;
} catch (error) {
@@ -259,7 +273,7 @@ router.post('/batch', async (req, res) => {
portId: portData.portId,
deviceId: portData.deviceId,
portName: portData.portName,
error: error.message
error: error.message,
});
}
}
@@ -279,18 +293,18 @@ router.post('/batch', async (req, res) => {
router.put('/:portId', async (req, res) => {
try {
const [updated] = await DevicePort.update(req.body, {
where: { portId: req.params.portId }
where: { portId: req.params.portId },
});
if (updated) {
const port = await DevicePort.findByPk(req.params.portId, {
include: [
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
res.json(port);
} else {
@@ -313,9 +327,9 @@ router.delete('/:portId', async (req, res) => {
where: {
[Op.or]: [
{ sourceDeviceId: port.deviceId, sourcePort: port.portName },
{ targetDeviceId: port.deviceId, targetPort: port.portName }
]
}
{ targetDeviceId: port.deviceId, targetPort: port.portName },
],
},
});
if (relatedCables.length > 0) {
@@ -326,13 +340,13 @@ router.delete('/:portId', async (req, res) => {
sourceDeviceId: c.sourceDeviceId,
sourcePort: c.sourcePort,
targetDeviceId: c.targetDeviceId,
targetPort: c.targetPort
}))
targetPort: c.targetPort,
})),
});
}
await DevicePort.destroy({
where: { portId: req.params.portId }
where: { portId: req.params.portId },
});
res.status(204).json();
@@ -351,12 +365,12 @@ router.delete('/batch', async (req, res) => {
}
const deletedCount = await DevicePort.destroy({
where: { portId: { [Op.in]: portIds } }
where: { portId: { [Op.in]: portIds } },
});
res.json({
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
deletedCount
deletedCount,
});
} catch (error) {
console.error('批量删除端口失败:', error);
@@ -373,12 +387,12 @@ router.post('/batch-delete', async (req, res) => {
}
const deletedCount = await DevicePort.destroy({
where: { portId: { [Op.in]: portIds } }
where: { portId: { [Op.in]: portIds } },
});
res.json({
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
deletedCount
deletedCount,
});
} catch (error) {
console.error('批量删除端口失败:', error);
@@ -393,9 +407,9 @@ router.get('/:portId', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type', 'rackId']
}
]
attributes: ['deviceId', 'name', 'type', 'rackId'],
},
],
});
if (!port) {
@@ -457,24 +471,24 @@ router.get('/export/all', async (req, res) => {
{
model: require('../models/Room'),
as: 'room',
attributes: ['roomId', 'name']
}
]
}
]
attributes: ['roomId', 'name'],
},
],
},
],
},
{
model: NetworkCard,
as: 'networkCard',
attributes: ['nicId', 'name']
}
attributes: ['nicId', 'name'],
},
],
order: [['createdAt', 'DESC']],
limit: parsedPageSize,
offset: offset,
subQuery: false
subQuery: false,
}),
timeoutPromise
timeoutPromise,
]);
const ports = countResult;
@@ -482,7 +496,7 @@ router.get('/export/all', async (req, res) => {
const statusMap = {
free: '空闲',
occupied: '占用',
fault: '故障'
fault: '故障',
};
const exportData = ports.map(port => ({
@@ -499,17 +513,18 @@ router.get('/export/all', async (req, res) => {
状态: statusMap[port.status] || port.status,
VLAN_ID: port.vlanId || '-',
描述: port.description || '-',
创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-'
创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-',
}));
let filteredExportData = exportData;
if (keyword) {
const searchLower = keyword.toLowerCase();
filteredExportData = exportData.filter(item =>
item.端口名称?.toLowerCase().includes(searchLower) ||
item.端口类型?.toLowerCase().includes(searchLower) ||
item.设备名称?.toLowerCase().includes(searchLower) ||
item.描述?.toLowerCase().includes(searchLower)
filteredExportData = exportData.filter(
item =>
item.端口名称?.toLowerCase().includes(searchLower) ||
item.端口类型?.toLowerCase().includes(searchLower) ||
item.设备名称?.toLowerCase().includes(searchLower) ||
item.描述?.toLowerCase().includes(searchLower)
);
}
@@ -517,7 +532,7 @@ router.get('/export/all', async (req, res) => {
page: parsedPage,
pageSize: parsedPageSize,
total: filteredExportData.length,
ports: filteredExportData
ports: filteredExportData,
});
} catch (error) {
console.error('导出端口失败:', error);
+570 -381
View File
File diff suppressed because it is too large Load Diff
+368 -211
View File
@@ -4,15 +4,19 @@ const { Op } = require('sequelize');
const Device = require('../models/Device');
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const { logDeviceOperation, generateDeviceDescription, buildDeviceMetadata } = require('../utils/operationLogger');
const {
logDeviceOperation,
generateDeviceDescription,
buildDeviceMetadata,
} = require('../utils/operationLogger');
async function generateIdleDeviceId() {
const devices = await Device.findAll({
where: {
deviceId: {
[Op.like]: 'DEV%'
}
}
[Op.like]: 'DEV%',
},
},
});
let maxNumber = 0;
@@ -41,7 +45,7 @@ router.get('/', async (req, res) => {
where[Op.or] = [
{ deviceId: { [Op.like]: `%${keyword}%` } },
{ name: { [Op.like]: `%${keyword}%` } },
{ serialNumber: { [Op.like]: `%${keyword}%` } }
{ serialNumber: { [Op.like]: `%${keyword}%` } },
];
}
@@ -59,22 +63,24 @@ router.get('/', async (req, res) => {
{
model: Rack,
attributes: ['rackId', 'name', 'roomId'],
include: [{
model: Room,
attributes: ['roomId', 'name']
}]
}
include: [
{
model: Room,
attributes: ['roomId', 'name'],
},
],
},
],
offset: parseInt(offset),
limit: parseInt(pageSize),
order: [['idleDate', 'DESC']]
order: [['idleDate', 'DESC']],
});
res.json({
total: count,
idleDevices: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取空闲设备列表失败:', error);
@@ -90,12 +96,14 @@ router.get('/:deviceId', async (req, res) => {
{
model: Rack,
attributes: ['rackId', 'name', 'roomId'],
include: [{
model: Room,
attributes: ['roomId', 'name']
}]
}
]
include: [
{
model: Room,
attributes: ['roomId', 'name'],
},
],
},
],
});
if (!device) {
@@ -110,7 +118,18 @@ router.get('/:deviceId', async (req, res) => {
router.post('/', async (req, res) => {
try {
const { name, type, model, serialNumber, powerConsumption, idleReason, warehouseId, description, rackId, position } = req.body;
const {
name,
type,
model,
serialNumber,
powerConsumption,
idleReason,
warehouseId,
description,
rackId,
position,
} = req.body;
let { deviceId } = req.body;
@@ -144,24 +163,35 @@ router.post('/', async (req, res) => {
warehouseId: warehouseId || null,
rackId: rackId || null,
position: position || null,
sourceType: warehouseId ? 'warehouse' : (rackId ? 'rack' : 'rack'),
description: description || ''
sourceType: warehouseId ? 'warehouse' : rackId ? 'rack' : 'rack',
description: description || '',
});
await logDeviceOperation('create', generateDeviceDescription('新增空闲设备', {
deviceId: device.deviceId,
name: device.name || deviceId,
type: device.type,
model: device.model,
serialNumber: device.serialNumber,
ipAddress: device.ipAddress
}, { includeRack: false }), {
targetId: device.deviceId,
targetName: device.name || deviceId,
afterState: device.toJSON(),
req,
metadata: buildDeviceMetadata(device.toJSON(), { sourceType: device.sourceType, type: 'idle_device_create' })
});
await logDeviceOperation(
'create',
generateDeviceDescription(
'新增空闲设备',
{
deviceId: device.deviceId,
name: device.name || deviceId,
type: device.type,
model: device.model,
serialNumber: device.serialNumber,
ipAddress: device.ipAddress,
},
{ includeRack: false }
),
{
targetId: device.deviceId,
targetName: device.name || deviceId,
afterState: device.toJSON(),
req,
metadata: buildDeviceMetadata(device.toJSON(), {
sourceType: device.sourceType,
type: 'idle_device_create',
}),
}
);
res.status(201).json(device);
} catch (error) {
@@ -186,13 +216,16 @@ router.post('/from-device/:deviceId', async (req, res) => {
return res.status(400).json({ error: '设备已经标记为空闲设备' });
}
await device.update({
isIdle: true,
status: 'idle',
idleDate: new Date(),
idleReason: idleReason || `从设备管理转入`,
sourceType: 'rack'
}, { transaction: t });
await device.update(
{
isIdle: true,
status: 'idle',
idleDate: new Date(),
idleReason: idleReason || `从设备管理转入`,
sourceType: 'rack',
},
{ transaction: t }
);
await t.commit();
@@ -203,12 +236,12 @@ router.post('/from-device/:deviceId', async (req, res) => {
beforeState: { ...deviceData, isIdle: false },
afterState: { ...deviceData, isIdle: true },
req,
metadata: buildDeviceMetadata(deviceData, { idleReason, type: 'device_to_idle' })
metadata: buildDeviceMetadata(deviceData, { idleReason, type: 'device_to_idle' }),
});
res.json({
message: '设备已转入空闲设备',
device: device.toJSON()
device: device.toJSON(),
});
} catch (error) {
await t.rollback();
@@ -229,7 +262,7 @@ router.post('/batch-from-devices', async (req, res) => {
const devices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds } },
transaction: t
transaction: t,
});
const notIdleDevices = devices.filter(d => !d.isIdle);
@@ -241,11 +274,11 @@ router.post('/batch-from-devices', async (req, res) => {
isIdle: true,
status: 'idle',
idleDate: new Date(),
idleReason: idleReason || `批量转入`
idleReason: idleReason || `批量转入`,
},
{
where: { deviceId: { [Op.in]: notIdleDevices.map(d => d.deviceId) } },
transaction: t
transaction: t,
}
);
}
@@ -253,22 +286,29 @@ router.post('/batch-from-devices', async (req, res) => {
await t.commit();
const deviceDetails = notIdleDevices.map(d => d.toJSON());
const deviceSummary = deviceDetails.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.serialNumber ? `,序列号:${d.serialNumber}` : ''})`
).join('、');
const deviceSummary = deviceDetails
.map(
d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.serialNumber ? `,序列号:${d.serialNumber}` : ''})`
)
.join('、');
await logDeviceOperation('batch_to_idle', `批量将 ${notIdleDevices.length} 台设备转入空闲设备:${deviceSummary}`, {
targetId: deviceIds.join(','),
targetName: `${notIdleDevices.length}台设备`,
req,
metadata: { idleReason, type: 'batch_device_to_idle', devices: deviceDetails }
});
await logDeviceOperation(
'batch_to_idle',
`批量将 ${notIdleDevices.length} 台设备转入空闲设备:${deviceSummary}`,
{
targetId: deviceIds.join(','),
targetName: `${notIdleDevices.length}台设备`,
req,
metadata: { idleReason, type: 'batch_device_to_idle', devices: deviceDetails },
}
);
res.json({
message: `成功将 ${notIdleDevices.length} 台设备转入空闲设备`,
total: devices.length,
updated: notIdleDevices.length,
skipped: alreadyIdleDevices.length
skipped: alreadyIdleDevices.length,
});
} catch (error) {
await t.rollback();
@@ -305,23 +345,29 @@ router.put('/batch-restore', async (req, res) => {
const idleDevices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
transaction: t
transaction: t,
});
console.log('查询到的空闲设备数量:', idleDevices.length);
if (idleDevices.length > 0) {
console.log('查询到的设备ID:', idleDevices.map(d => d.deviceId));
console.log(
'查询到的设备ID:',
idleDevices.map(d => d.deviceId)
);
}
if (idleDevices.length === 0) {
console.log('没有找到空闲设备,检查设备是否存在:');
const allDevices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds } },
transaction: t
transaction: t,
});
console.log('设备表中存在的设备数量:', allDevices.length);
if (allDevices.length > 0) {
console.log('存在的设备及其 isIdle 状态:', allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })));
console.log(
'存在的设备及其 isIdle 状态:',
allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle }))
);
}
await t.rollback();
@@ -333,7 +379,9 @@ router.put('/batch-restore', async (req, res) => {
for (const device of idleDevices) {
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
if (!deviceConfig) continue;
if (!deviceConfig) {
continue;
}
const targetRackId = deviceConfig.targetRackId;
const targetPosition = deviceConfig.targetPosition;
@@ -343,7 +391,7 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'skipped',
reason: '未指定目标机柜'
reason: '未指定目标机柜',
});
continue;
}
@@ -354,7 +402,7 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'failed',
reason: '目标机柜不存在'
reason: '目标机柜不存在',
});
continue;
}
@@ -368,25 +416,31 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'failed',
reason: `U位${position}已被占用`
reason: `U位${position}已被占用`,
});
continue;
}
await device.update({
isIdle: false,
idleDate: null,
idleReason: null,
rackId: targetRackId,
position: position,
warehouseId: null,
sourceType: 'rack',
status: 'offline'
}, { transaction: t });
await device.update(
{
isIdle: false,
idleDate: null,
idleReason: null,
rackId: targetRackId,
position: position,
warehouseId: null,
sourceType: 'rack',
status: 'offline',
},
{ transaction: t }
);
await targetRack.update({
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
}, { transaction: t });
await targetRack.update(
{
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
},
{ transaction: t }
);
restoredCount++;
results.push({
@@ -394,7 +448,7 @@ router.put('/batch-restore', async (req, res) => {
name: device.name,
status: 'success',
targetRack: targetRack.name,
targetPosition: position
targetPosition: position,
});
}
@@ -404,17 +458,30 @@ router.put('/batch-restore', async (req, res) => {
const failedCount = results.filter(r => r.status === 'failed').length;
const skippedCount = results.filter(r => r.status === 'skipped').length;
const successDevices = idleDevices.filter(d => results.some(r => r.deviceId === d.deviceId && r.status === 'success'));
const deviceSummary = successDevices.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
).join('、');
const successDevices = idleDevices.filter(d =>
results.some(r => r.deviceId === d.deviceId && r.status === 'success')
);
const deviceSummary = successDevices
.map(
d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
)
.join('、');
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备:${deviceSummary}`, {
targetId: deviceIds.join(','),
targetName: `${successCount}台设备`,
req,
metadata: { results, type: 'batch_idle_device_restore', devices: successDevices.map(d => d.toJSON()) }
});
await logDeviceOperation(
'batch_restore',
`批量上架 ${successCount} 台空闲设备:${deviceSummary}`,
{
targetId: deviceIds.join(','),
targetName: `${successCount}台设备`,
req,
metadata: {
results,
type: 'batch_idle_device_restore',
devices: successDevices.map(d => d.toJSON()),
},
}
);
res.json({
message: `成功上架 ${successCount} 台设备`,
@@ -422,7 +489,7 @@ router.put('/batch-restore', async (req, res) => {
restored: successCount,
failed: failedCount,
skipped: skippedCount,
details: results
details: results,
});
} catch (error) {
await t.rollback();
@@ -435,11 +502,21 @@ router.put('/:deviceId/shelve', async (req, res) => {
const t = await require('../db').sequelize.transaction();
try {
const { deviceId } = req.params;
const { name, type, model, serialNumber, height, powerConsumption, rackId, position, description } = req.body;
const {
name,
type,
model,
serialNumber,
height,
powerConsumption,
rackId,
position,
description,
} = req.body;
const device = await Device.findOne({
where: { deviceId, isIdle: true },
transaction: t
transaction: t,
});
if (!device) {
@@ -467,54 +544,63 @@ router.put('/:deviceId/shelve', async (req, res) => {
const beforeState = device.toJSON();
await device.update({
name: name || device.name,
type: type || device.type,
model: model || device.model,
serialNumber: serialNumber || device.serialNumber,
height: deviceHeight,
powerConsumption: powerConsumption || device.powerConsumption || 0,
rackId: rackId,
position: position,
description: description || device.description,
isIdle: false,
idleDate: null,
idleReason: null,
warehouseId: null,
sourceType: 'rack',
status: 'running'
}, { transaction: t });
await device.update(
{
name: name || device.name,
type: type || device.type,
model: model || device.model,
serialNumber: serialNumber || device.serialNumber,
height: deviceHeight,
powerConsumption: powerConsumption || device.powerConsumption || 0,
rackId: rackId,
position: position,
description: description || device.description,
isIdle: false,
idleDate: null,
idleReason: null,
warehouseId: null,
sourceType: 'rack',
status: 'running',
},
{ transaction: t }
);
await targetRack.update({
currentPower: targetRack.currentPower + (powerConsumption || device.powerConsumption || 0)
}, { transaction: t });
await targetRack.update(
{
currentPower: targetRack.currentPower + (powerConsumption || device.powerConsumption || 0),
},
{ transaction: t }
);
await t.commit();
const updatedDevice = await Device.findByPk(deviceId, {
include: [
{ model: Rack, include: [Room] }
]
include: [{ model: Rack, include: [Room] }],
});
const deviceData = {
...updatedDevice.toJSON(),
rackName: targetRack.name,
roomName: updatedDevice.Rack?.Room?.name
roomName: updatedDevice.Rack?.Room?.name,
};
await logDeviceOperation('shelve', generateDeviceDescription('空闲设备上架', deviceData, { includePosition: false }) + `到机柜【${targetRack.name}】U${position}`, {
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...beforeState, isIdle: true },
afterState: deviceData,
req,
metadata: buildDeviceMetadata(deviceData, { rackId, position, type: 'idle_device_shelve' })
});
await logDeviceOperation(
'shelve',
generateDeviceDescription('空闲设备上架', deviceData, { includePosition: false }) +
`到机柜【${targetRack.name}】U${position}`,
{
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...beforeState, isIdle: true },
afterState: deviceData,
req,
metadata: buildDeviceMetadata(deviceData, { rackId, position, type: 'idle_device_shelve' }),
}
);
res.json({
message: '设备上架成功',
device: updatedDevice
device: updatedDevice,
});
} catch (error) {
await t.rollback();
@@ -526,7 +612,7 @@ router.put('/:deviceId/shelve', async (req, res) => {
router.put('/:deviceId', async (req, res) => {
try {
const device = await Device.findOne({
where: { deviceId: req.params.deviceId, isIdle: true }
where: { deviceId: req.params.deviceId, isIdle: true },
});
if (!device) {
@@ -534,7 +620,14 @@ router.put('/:deviceId', async (req, res) => {
}
const beforeState = device.toJSON();
const allowedFields = ['name', 'type', 'model', 'idleReason', 'description', 'powerConsumption'];
const allowedFields = [
'name',
'type',
'model',
'idleReason',
'description',
'powerConsumption',
];
allowedFields.forEach(field => {
if (req.body[field] !== undefined) {
@@ -566,14 +659,18 @@ router.put('/:deviceId', async (req, res) => {
await device.save();
await logDeviceOperation('update', generateDeviceDescription('更新空闲设备', device.toJSON(), { includeRack: false }), {
targetId: device.deviceId,
targetName: device.name,
beforeState,
afterState: device.toJSON(),
req,
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_update' })
});
await logDeviceOperation(
'update',
generateDeviceDescription('更新空闲设备', device.toJSON(), { includeRack: false }),
{
targetId: device.deviceId,
targetName: device.name,
beforeState,
afterState: device.toJSON(),
req,
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_update' }),
}
);
res.json(device);
} catch (error) {
@@ -589,7 +686,7 @@ router.put('/:deviceId/restore', async (req, res) => {
const device = await Device.findOne({
where: { deviceId, isIdle: true },
transaction: t
transaction: t,
});
if (!device) {
@@ -608,53 +705,72 @@ router.put('/:deviceId/restore', async (req, res) => {
return res.status(404).json({ error: '目标机柜不存在' });
}
const positionCheck = await checkPositionAvailable(targetRackId, targetPosition, device.height || 1, deviceId, t);
const positionCheck = await checkPositionAvailable(
targetRackId,
targetPosition,
device.height || 1,
deviceId,
t
);
if (!positionCheck.available) {
await t.rollback();
return res.status(400).json({ error: positionCheck.reason });
}
await device.update({
isIdle: false,
idleDate: null,
idleReason: null,
rackId: targetRackId,
position: targetPosition,
warehouseId: null,
sourceType: 'rack',
status: 'offline'
}, { transaction: t });
await device.update(
{
isIdle: false,
idleDate: null,
idleReason: null,
rackId: targetRackId,
position: targetPosition,
warehouseId: null,
sourceType: 'rack',
status: 'offline',
},
{ transaction: t }
);
await targetRack.update({
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
}, { transaction: t });
await targetRack.update(
{
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
},
{ transaction: t }
);
await t.commit();
const updatedDevice = await Device.findByPk(deviceId, {
include: [
{ model: Rack, include: [Room] }
]
include: [{ model: Rack, include: [Room] }],
});
const deviceData = {
...updatedDevice.toJSON(),
rackName: targetRack.name,
roomName: updatedDevice.Rack?.Room?.name
roomName: updatedDevice.Rack?.Room?.name,
};
await logDeviceOperation('restore', generateDeviceDescription('空闲设备恢复', deviceData, { includePosition: false }) + `到机柜【${targetRack.name}】U${targetPosition}`, {
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...device.toJSON(), isIdle: true },
afterState: deviceData,
req,
metadata: buildDeviceMetadata(deviceData, { targetRackId, targetPosition, type: 'idle_device_restore' })
});
await logDeviceOperation(
'restore',
generateDeviceDescription('空闲设备恢复', deviceData, { includePosition: false }) +
`到机柜【${targetRack.name}】U${targetPosition}`,
{
targetId: device.deviceId,
targetName: device.name,
beforeState: { ...device.toJSON(), isIdle: true },
afterState: deviceData,
req,
metadata: buildDeviceMetadata(deviceData, {
targetRackId,
targetPosition,
type: 'idle_device_restore',
}),
}
);
res.json({
message: '设备已恢复到设备管理',
device: updatedDevice
device: updatedDevice,
});
} catch (error) {
await t.rollback();
@@ -692,23 +808,29 @@ router.put('/batch-restore', async (req, res) => {
const idleDevices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
transaction: t
transaction: t,
});
console.log('查询到的空闲设备数量:', idleDevices.length);
if (idleDevices.length > 0) {
console.log('查询到的设备ID:', idleDevices.map(d => d.deviceId));
console.log(
'查询到的设备ID:',
idleDevices.map(d => d.deviceId)
);
}
if (idleDevices.length === 0) {
console.log('没有找到空闲设备,检查设备是否存在:');
const allDevices = await Device.findAll({
where: { deviceId: { [Op.in]: deviceIds } },
transaction: t
transaction: t,
});
console.log('设备表中存在的设备数量:', allDevices.length);
if (allDevices.length > 0) {
console.log('存在的设备及其 isIdle 状态:', allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })));
console.log(
'存在的设备及其 isIdle 状态:',
allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle }))
);
}
await t.rollback();
@@ -720,7 +842,9 @@ router.put('/batch-restore', async (req, res) => {
for (const device of idleDevices) {
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
if (!deviceConfig) continue;
if (!deviceConfig) {
continue;
}
const targetRackId = deviceConfig.targetRackId;
const targetPosition = deviceConfig.targetPosition;
@@ -730,7 +854,7 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'skipped',
reason: '未指定目标机柜'
reason: '未指定目标机柜',
});
continue;
}
@@ -741,7 +865,7 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'failed',
reason: '目标机柜不存在'
reason: '目标机柜不存在',
});
continue;
}
@@ -755,25 +879,31 @@ router.put('/batch-restore', async (req, res) => {
deviceId: device.deviceId,
name: device.name,
status: 'failed',
reason: `U位${position}已被占用`
reason: `U位${position}已被占用`,
});
continue;
}
await device.update({
isIdle: false,
idleDate: null,
idleReason: null,
rackId: targetRackId,
position: position,
warehouseId: null,
sourceType: 'rack',
status: 'offline'
}, { transaction: t });
await device.update(
{
isIdle: false,
idleDate: null,
idleReason: null,
rackId: targetRackId,
position: position,
warehouseId: null,
sourceType: 'rack',
status: 'offline',
},
{ transaction: t }
);
await targetRack.update({
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
}, { transaction: t });
await targetRack.update(
{
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
},
{ transaction: t }
);
restoredCount++;
results.push({
@@ -781,7 +911,7 @@ router.put('/batch-restore', async (req, res) => {
name: device.name,
status: 'success',
targetRack: targetRack.name,
targetPosition: position
targetPosition: position,
});
}
@@ -791,17 +921,30 @@ router.put('/batch-restore', async (req, res) => {
const failedCount = results.filter(r => r.status === 'failed').length;
const skippedCount = results.filter(r => r.status === 'skipped').length;
const successDevices = idleDevices.filter(d => results.some(r => r.deviceId === d.deviceId && r.status === 'success'));
const deviceSummary = successDevices.map(d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
).join('、');
const successDevices = idleDevices.filter(d =>
results.some(r => r.deviceId === d.deviceId && r.status === 'success')
);
const deviceSummary = successDevices
.map(
d =>
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
)
.join('、');
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备:${deviceSummary}`, {
targetId: deviceIds.join(','),
targetName: `${successCount}台设备`,
req,
metadata: { results, type: 'batch_idle_device_restore', devices: successDevices.map(d => d.toJSON()) }
});
await logDeviceOperation(
'batch_restore',
`批量上架 ${successCount} 台空闲设备:${deviceSummary}`,
{
targetId: deviceIds.join(','),
targetName: `${successCount}台设备`,
req,
metadata: {
results,
type: 'batch_idle_device_restore',
devices: successDevices.map(d => d.toJSON()),
},
}
);
res.json({
message: `成功上架 ${successCount} 台设备`,
@@ -809,7 +952,7 @@ router.put('/batch-restore', async (req, res) => {
restored: successCount,
failed: failedCount,
skipped: skippedCount,
details: results
details: results,
});
} catch (error) {
await t.rollback();
@@ -823,7 +966,7 @@ router.delete('/:deviceId', async (req, res) => {
try {
const device = await Device.findOne({
where: { deviceId: req.params.deviceId, isIdle: true },
transaction: t
transaction: t,
});
if (!device) {
@@ -837,16 +980,24 @@ router.delete('/:deviceId', async (req, res) => {
await t.commit();
await logDeviceOperation('delete', generateDeviceDescription('删除空闲设备', {
...device.toJSON(),
name: device.name || device.deviceId
}, { includeRack: false }), {
targetId: device.deviceId,
targetName: device.name,
beforeState,
req,
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_delete' })
});
await logDeviceOperation(
'delete',
generateDeviceDescription(
'删除空闲设备',
{
...device.toJSON(),
name: device.name || device.deviceId,
},
{ includeRack: false }
),
{
targetId: device.deviceId,
targetName: device.name,
beforeState,
req,
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_delete' }),
}
);
res.json({ message: '空闲设备删除成功' });
} catch (error) {
@@ -856,7 +1007,13 @@ router.delete('/:deviceId', async (req, res) => {
}
});
async function checkPositionAvailable(rackId, position, height, excludeDeviceId = null, transaction = null) {
async function checkPositionAvailable(
rackId,
position,
height,
excludeDeviceId = null,
transaction = null
) {
if (!position || position <= 0) {
return { available: true, reason: null };
}
@@ -869,9 +1026,9 @@ async function checkPositionAvailable(rackId, position, height, excludeDeviceId
where: {
rackId: rackId,
position: { [Op.ne]: null },
isIdle: false
isIdle: false,
},
attributes: ['deviceId', 'position', 'height']
attributes: ['deviceId', 'position', 'height'],
};
if (transaction) {
@@ -891,7 +1048,7 @@ async function checkPositionAvailable(rackId, position, height, excludeDeviceId
if (!(endU < existStart || startU > existEnd)) {
return {
available: false,
reason: `U位冲突:机柜中已有设备 ${d.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''}`
reason: `U位冲突:机柜中已有设备 ${d.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''}`,
};
}
}
+202 -93
View File
@@ -54,25 +54,29 @@ router.get('/plans', async (req, res) => {
if (keyword) {
where[Op.or] = [
{ name: { [Op.like]: `%${keyword}%` } },
{ description: { [Op.like]: `%${keyword}%` } }
{ description: { [Op.like]: `%${keyword}%` } },
];
}
const { count, rows } = await InventoryPlan.findAndCountAll({
where,
include: [
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
{
model: require('../models/User'),
as: 'Creator',
attributes: ['userId', 'username', 'realName'],
},
],
order: [['createdAt', 'DESC']],
limit: parseInt(pageSize),
offset: parseInt(offset)
offset: parseInt(offset),
});
res.json({
plans: rows,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -83,8 +87,12 @@ router.get('/plans/:planId', async (req, res) => {
try {
const plan = await InventoryPlan.findByPk(req.params.planId, {
include: [
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
]
{
model: require('../models/User'),
as: 'Creator',
attributes: ['userId', 'username', 'realName'],
},
],
});
if (!plan) {
@@ -94,9 +102,13 @@ router.get('/plans/:planId', async (req, res) => {
const tasks = await InventoryTask.findAll({
where: { planId: plan.planId },
include: [
{ model: require('../models/User'), as: 'Assignee', attributes: ['userId', 'username', 'realName'] }
{
model: require('../models/User'),
as: 'Assignee',
attributes: ['userId', 'username', 'realName'],
},
],
order: [['createdAt', 'ASC']]
order: [['createdAt', 'ASC']],
});
res.json({ plan, tasks });
@@ -118,7 +130,7 @@ router.post('/plans', async (req, res) => {
targetRooms: targetRooms || [],
targetRacks: targetRacks || [],
status: 'draft',
createdBy: req.user?.userId
createdBy: req.user?.userId,
});
res.status(201).json(plan);
@@ -143,7 +155,7 @@ router.put('/plans/:planId', async (req, res) => {
scheduledDate: scheduledDate ? new Date(scheduledDate) : plan.scheduledDate,
targetRooms: targetRooms || plan.targetRooms,
targetRacks: targetRacks || plan.targetRacks,
status: status || plan.status
status: status || plan.status,
});
res.json(plan);
@@ -187,16 +199,16 @@ router.post('/plans/:planId/start', async (req, res) => {
if (targetRacks.length > 0) {
allDevices = await Device.findAll({
where: { rackId: { [Op.in]: targetRacks } }
where: { rackId: { [Op.in]: targetRacks } },
});
} else if (targetRooms.length > 0) {
const racksInRooms = await Rack.findAll({
where: { roomId: { [Op.in]: targetRooms } },
attributes: ['rackId']
attributes: ['rackId'],
});
const rackIds = racksInRooms.map(r => r.rackId);
allDevices = await Device.findAll({
where: { rackId: { [Op.in]: rackIds } }
where: { rackId: { [Op.in]: rackIds } },
});
} else {
allDevices = await Device.findAll();
@@ -206,7 +218,7 @@ router.post('/plans/:planId/start', async (req, res) => {
const recordsToCreate = [];
const taskId = generateTaskId();
tasksToCreate.push({
taskId,
planId: plan.planId,
@@ -214,7 +226,7 @@ router.post('/plans/:planId/start', async (req, res) => {
targetId: 'all',
targetName: '全部设备',
status: 'pending',
totalDevices: allDevices.length
totalDevices: allDevices.length,
});
for (let i = 0; i < allDevices.length; i++) {
@@ -229,7 +241,7 @@ router.post('/plans/:planId/start', async (req, res) => {
serialNumber: device.serialNumber,
rackId: device.rackId,
position: device.position,
status: 'pending'
status: 'pending',
});
}
@@ -238,13 +250,17 @@ router.post('/plans/:planId/start', async (req, res) => {
}
if (recordsToCreate.length > 0) {
const now = dbDialect === 'mysql'
? new Date().toISOString().replace('T', ' ').replace('Z', '')
: new Date().toISOString();
const placeholders = recordsToCreate.map(r =>
`('${r.recordId}', '${r.taskId}', '${r.planId}', '${r.deviceId}', '${r.deviceName}', '${r.deviceType}', '${r.serialNumber || ''}', '${r.rackId}', ${r.position}, 'pending', '${now}', '${now}')`
).join(',');
const now =
dbDialect === 'mysql'
? new Date().toISOString().replace('T', ' ').replace('Z', '')
: new Date().toISOString();
const placeholders = recordsToCreate
.map(
r =>
`('${r.recordId}', '${r.taskId}', '${r.planId}', '${r.deviceId}', '${r.deviceName}', '${r.deviceType}', '${r.serialNumber || ''}', '${r.rackId}', ${r.position}, 'pending', '${now}', '${now}')`
)
.join(',');
if (placeholders) {
await sequelize.query(`
INSERT INTO inventory_records (recordId, taskId, planId, deviceId, deviceName, deviceType, serialNumber, rackId, position, status, createdAt, updatedAt)
@@ -259,10 +275,14 @@ router.post('/plans/:planId/start', async (req, res) => {
checkedDevices: 0,
normalDevices: 0,
abnormalDevices: 0,
missedDevices: allDevices.length
missedDevices: allDevices.length,
});
res.json({ message: '盘点任务已启动', taskCount: tasksToCreate.length, deviceCount: allDevices.length });
res.json({
message: '盘点任务已启动',
taskCount: tasksToCreate.length,
deviceCount: allDevices.length,
});
} catch (error) {
console.error('启动盘点错误:', error);
res.status(500).json({ error: error.message });
@@ -274,8 +294,12 @@ router.get('/tasks/:taskId', async (req, res) => {
const task = await InventoryTask.findByPk(req.params.taskId, {
include: [
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
{ model: require('../models/User'), as: 'Assignee', attributes: ['userId', 'username', 'realName'] }
]
{
model: require('../models/User'),
as: 'Assignee',
attributes: ['userId', 'username', 'realName'],
},
],
});
if (!task) {
@@ -285,15 +309,23 @@ router.get('/tasks/:taskId', async (req, res) => {
const records = await InventoryRecord.findAll({
where: { taskId: task.taskId },
include: [
{ model: Device, as: 'Device', attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'] },
{ model: require('../models/User'), as: 'Checker', attributes: ['userId', 'username', 'realName'] }
]
{
model: Device,
as: 'Device',
attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'],
},
{
model: require('../models/User'),
as: 'Checker',
attributes: ['userId', 'username', 'realName'],
},
],
});
const rackIds = [...new Set(records.map(r => r.rackId).filter(Boolean))];
const racks = await Rack.findAll({
where: { rackId: rackIds },
include: [{ model: Room, as: 'Room' }]
include: [{ model: Room, as: 'Room' }],
});
const rackMap = {};
racks.forEach(r => {
@@ -305,10 +337,12 @@ router.get('/tasks/:taskId', async (req, res) => {
const roomName = rackInfo?.Room?.name || '';
const rackName = rackInfo?.name || record.rackId || '';
const position = record.position || '';
return {
...record.toJSON(),
displayLocation: roomName ? `${roomName} - ${rackName} - U${position}` : `${rackName} - U${position}`
displayLocation: roomName
? `${roomName} - ${rackName} - U${position}`
: `${rackName} - U${position}`,
};
});
@@ -331,14 +365,14 @@ router.put('/tasks/:taskId', async (req, res) => {
if (assignedTo !== undefined) {
await task.update({
assignedTo,
assignedAt: assignedTo ? new Date() : task.assignedAt
assignedAt: assignedTo ? new Date() : task.assignedAt,
});
}
if (status) {
await task.update({
status,
completedAt: status === 'completed' ? new Date() : null
completedAt: status === 'completed' ? new Date() : null,
});
}
@@ -351,7 +385,7 @@ router.put('/tasks/:taskId', async (req, res) => {
router.post('/records/:recordId/check', async (req, res) => {
try {
const record = await InventoryRecord.findByPk(req.params.recordId, {
include: [{ model: Device, as: 'Device' }]
include: [{ model: Device, as: 'Device' }],
});
if (!record) {
@@ -380,7 +414,7 @@ router.post('/records/:recordId/check', async (req, res) => {
checkedBy: req.user?.userId,
checkedAt: new Date(),
remark: remark || null,
photoUrl: photoUrl || null
photoUrl: photoUrl || null,
});
const task = await InventoryTask.findByPk(record.taskId);
@@ -391,7 +425,7 @@ router.post('/records/:recordId/check', async (req, res) => {
totalDevices: taskRecords.length,
checkedDevices: taskRecords.filter(r => r.status !== 'pending').length,
normalDevices: taskRecords.filter(r => r.status === 'normal').length,
abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length
abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length,
};
await task.update(taskStats);
@@ -402,7 +436,7 @@ router.post('/records/:recordId/check', async (req, res) => {
checkedDevices: planRecords.filter(r => r.status !== 'pending').length,
normalDevices: planRecords.filter(r => r.status === 'normal').length,
abnormalDevices: planRecords.filter(r => r.status === 'abnormal').length,
missedDevices: planRecords.filter(r => r.status === 'pending').length
missedDevices: planRecords.filter(r => r.status === 'pending').length,
};
await plan.update(planStats);
@@ -419,26 +453,43 @@ router.get('/records', async (req, res) => {
const offset = (page - 1) * pageSize;
const where = {};
if (planId) where.planId = planId;
if (taskId) where.taskId = taskId;
if (status) where.status = status;
if (planId) {
where.planId = planId;
}
if (taskId) {
where.taskId = taskId;
}
if (status) {
where.status = status;
}
const { count, rows } = await InventoryRecord.findAndCountAll({
where,
include: [
{ model: Device, as: 'Device', attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'] },
{ model: require('../models/User'), as: 'Checker', attributes: ['userId', 'username', 'realName'] }
{
model: Device,
as: 'Device',
attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'],
},
{
model: require('../models/User'),
as: 'Checker',
attributes: ['userId', 'username', 'realName'],
},
],
order: [
['checkedAt', 'DESC'],
['createdAt', 'DESC'],
],
order: [['checkedAt', 'DESC'], ['createdAt', 'DESC']],
limit: parseInt(pageSize),
offset: parseInt(offset)
offset: parseInt(offset),
});
res.json({
records: rows,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -466,7 +517,7 @@ router.post('/plans/:planId/complete', async (req, res) => {
checkedDevices: finalRecords.filter(r => r.status !== 'pending').length,
normalDevices: finalRecords.filter(r => r.status === 'normal').length,
abnormalDevices: finalRecords.filter(r => r.status === 'abnormal').length,
missedDevices: finalRecords.filter(r => r.status === 'missed').length
missedDevices: finalRecords.filter(r => r.status === 'missed').length,
});
await InventoryTask.update(
@@ -495,8 +546,12 @@ router.get('/stats/dashboard', async (req, res) => {
limit: 5,
order: [['createdAt', 'DESC']],
include: [
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
]
{
model: require('../models/User'),
as: 'Creator',
attributes: ['userId', 'username', 'realName'],
},
],
});
res.json({
@@ -507,9 +562,12 @@ router.get('/stats/dashboard', async (req, res) => {
normalRecords,
abnormalRecords,
pendingRecords,
completionRate: totalRecords > 0 ? ((normalRecords + abnormalRecords) / totalRecords * 100).toFixed(1) : 0,
abnormalRate: totalRecords > 0 ? (abnormalRecords / totalRecords * 100).toFixed(1) : 0,
recentPlans
completionRate:
totalRecords > 0
? (((normalRecords + abnormalRecords) / totalRecords) * 100).toFixed(1)
: 0,
abnormalRate: totalRecords > 0 ? ((abnormalRecords / totalRecords) * 100).toFixed(1) : 0,
recentPlans,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -518,18 +576,18 @@ router.get('/stats/dashboard', async (req, res) => {
router.post('/quick-add-device', async (req, res) => {
try {
const {
taskId,
planId,
const {
taskId,
planId,
serialNumber,
SN,
deviceName,
name,
deviceType,
type,
roomId,
rackId,
position,
roomId,
rackId,
position,
model,
brand,
height,
@@ -558,22 +616,27 @@ router.post('/quick-add-device', async (req, res) => {
const existingDevice = await Device.findOne({ where: { serialNumber: finalSerialNumber } });
if (existingDevice) {
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中', deviceId: existingDevice.deviceId });
return res
.status(400)
.json({ error: '该序列号的设备已存在于设备管理中', deviceId: existingDevice.deviceId });
}
const existingPending = await PendingDevice.findOne({
where: { serialNumber: finalSerialNumber, status: 'pending' }
const existingPending = await PendingDevice.findOne({
where: { serialNumber: finalSerialNumber, status: 'pending' },
});
if (existingPending) {
return res.status(400).json({ error: '该序列号的设备已在暂存列表中', pendingId: existingPending.pendingId });
return res
.status(400)
.json({ error: '该序列号的设备已在暂存列表中', pendingId: existingPending.pendingId });
}
const pendingId = `PEND${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
// 只有当用户没有填写设备名称时,才使用默认名称
const finalName = finalDeviceName && finalDeviceName.trim() !== ''
? finalDeviceName.trim()
: `新设备-${finalSerialNumber.slice(-6)}`;
const finalName =
finalDeviceName && finalDeviceName.trim() !== ''
? finalDeviceName.trim()
: `新设备-${finalSerialNumber.slice(-6)}`;
const pendingDevice = await PendingDevice.create({
pendingId,
@@ -596,12 +659,12 @@ router.post('/quick-add-device', async (req, res) => {
taskId: taskId || null,
createdBy: req.user?.userId,
status: 'pending',
remark: remark || '盘点时快速添加'
remark: remark || '盘点时快速添加',
});
res.status(201).json({
message: '设备已暂存,请前往暂存设备页面完善信息后同步',
pendingDevice
pendingDevice,
});
} catch (error) {
console.error('快速添加设备错误:', error);
@@ -611,7 +674,14 @@ router.post('/quick-add-device', async (req, res) => {
router.get('/pending-devices', async (req, res) => {
try {
const { status, planId, roomId, keyword, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE } = req.query;
const {
status,
planId,
roomId,
keyword,
page = 1,
pageSize = PAGINATION.DEFAULT_PAGE_SIZE,
} = req.query;
const offset = (page - 1) * pageSize;
const where = {};
@@ -627,7 +697,7 @@ router.get('/pending-devices', async (req, res) => {
if (keyword) {
where[Op.or] = [
{ serialNumber: { [Op.like]: `%${keyword}%` } },
{ deviceName: { [Op.like]: `%${keyword}%` } }
{ deviceName: { [Op.like]: `%${keyword}%` } },
];
}
@@ -638,18 +708,18 @@ router.get('/pending-devices', async (req, res) => {
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] }
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] },
],
order: [['createdAt', 'DESC']],
limit: parseInt(pageSize),
offset: parseInt(offset)
offset: parseInt(offset),
});
res.json({
pendingDevices: rows,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取暂存设备列表错误:', error);
@@ -677,8 +747,8 @@ router.get('/pending-devices/:pendingId', async (req, res) => {
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] }
]
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] },
],
});
if (!pendingDevice) {
@@ -702,7 +772,23 @@ router.put('/pending-devices/:pendingId', async (req, res) => {
return res.status(400).json({ error: '已同步的设备无法修改' });
}
const { deviceName, deviceType, roomId, rackId, position, model, brand, height, powerConsumption, ipAddress, purchaseDate, warrantyExpiry, description, remark, ...restFields } = req.body;
const {
deviceName,
deviceType,
roomId,
rackId,
position,
model,
brand,
height,
powerConsumption,
ipAddress,
purchaseDate,
warrantyExpiry,
description,
remark,
...restFields
} = req.body;
const updateData = {
deviceName: deviceName !== undefined ? deviceName : pendingDevice.deviceName,
@@ -713,12 +799,23 @@ router.put('/pending-devices/:pendingId', async (req, res) => {
model: model !== undefined ? model : pendingDevice.model,
brand: brand !== undefined ? brand : pendingDevice.brand,
height: height !== undefined ? height : pendingDevice.height,
powerConsumption: powerConsumption !== undefined ? powerConsumption : pendingDevice.powerConsumption,
powerConsumption:
powerConsumption !== undefined ? powerConsumption : pendingDevice.powerConsumption,
ipAddress: ipAddress !== undefined ? ipAddress : pendingDevice.ipAddress,
purchaseDate: purchaseDate !== undefined ? (purchaseDate ? new Date(purchaseDate) : null) : pendingDevice.purchaseDate,
warrantyExpiry: warrantyExpiry !== undefined ? (warrantyExpiry ? new Date(warrantyExpiry) : null) : pendingDevice.warrantyExpiry,
purchaseDate:
purchaseDate !== undefined
? purchaseDate
? new Date(purchaseDate)
: null
: pendingDevice.purchaseDate,
warrantyExpiry:
warrantyExpiry !== undefined
? warrantyExpiry
? new Date(warrantyExpiry)
: null
: pendingDevice.warrantyExpiry,
description: description !== undefined ? description : pendingDevice.description,
remark: remark !== undefined ? remark : pendingDevice.remark
remark: remark !== undefined ? remark : pendingDevice.remark,
};
if (Object.keys(restFields).length > 0) {
@@ -759,7 +856,9 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
return res.status(400).json({ error: '该设备已同步' });
}
const existingDevice = await Device.findOne({ where: { serialNumber: pendingDevice.serialNumber } });
const existingDevice = await Device.findOne({
where: { serialNumber: pendingDevice.serialNumber },
});
if (existingDevice) {
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中' });
}
@@ -770,7 +869,9 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
const match = device.deviceId.match(/^DEV(\d+)$/);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNumber) maxNumber = num;
if (num > maxNumber) {
maxNumber = num;
}
}
});
const deviceId = `DEV${String(maxNumber + 1).padStart(3, '0')}`;
@@ -790,20 +891,20 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
purchaseDate: pendingDevice.purchaseDate,
warrantyExpiry: pendingDevice.warrantyExpiry,
customFields: pendingDevice.customFields,
status: 'running'
status: 'running',
});
await pendingDevice.update({
status: 'synced',
syncedAt: new Date(),
syncedBy: req.user?.userId,
syncedDeviceId: newDevice.deviceId
syncedDeviceId: newDevice.deviceId,
});
res.json({
message: '同步成功',
device: newDevice,
pendingDevice
pendingDevice,
});
} catch (error) {
console.error('同步设备错误:', error);
@@ -821,8 +922,8 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
const pendingDevices = await PendingDevice.findAll({
where: {
pendingId: { [Op.in]: pendingIds },
status: 'pending'
}
status: 'pending',
},
});
if (pendingDevices.length === 0) {
@@ -835,7 +936,9 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
const match = device.deviceId.match(/^DEV(\d+)$/);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNumber) maxNumber = num;
if (num > maxNumber) {
maxNumber = num;
}
}
});
@@ -844,9 +947,15 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
for (const pending of pendingDevices) {
try {
const existingDevice = await Device.findOne({ where: { serialNumber: pending.serialNumber } });
const existingDevice = await Device.findOne({
where: { serialNumber: pending.serialNumber },
});
if (existingDevice) {
errors.push({ pendingId: pending.pendingId, serialNumber: pending.serialNumber, error: '序列号已存在' });
errors.push({
pendingId: pending.pendingId,
serialNumber: pending.serialNumber,
error: '序列号已存在',
});
continue;
}
@@ -868,14 +977,14 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
purchaseDate: pending.purchaseDate,
warrantyExpiry: pending.warrantyExpiry,
customFields: pending.customFields,
status: 'running'
status: 'running',
});
await pending.update({
status: 'synced',
syncedAt: new Date(),
syncedBy: req.user?.userId,
syncedDeviceId: newDevice.deviceId
syncedDeviceId: newDevice.deviceId,
});
results.push({ pendingId: pending.pendingId, deviceId: newDevice.deviceId });
@@ -889,7 +998,7 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
successCount: results.length,
errorCount: errors.length,
results,
errors
errors,
});
} catch (error) {
console.error('批量同步设备错误:', error);
+84 -58
View File
@@ -13,7 +13,7 @@ router.get('/', async (req, res) => {
try {
const { deviceId } = req.query;
const where = {};
if (deviceId) {
where.deviceId = deviceId;
}
@@ -24,10 +24,13 @@ router.get('/', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type']
}
attributes: ['deviceId', 'name', 'type'],
},
],
order: [
['slotNumber', 'ASC'],
['name', 'ASC'],
],
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
});
res.json(networkCards);
@@ -47,10 +50,13 @@ router.get('/device/:deviceId', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type']
}
attributes: ['deviceId', 'name', 'type'],
},
],
order: [
['slotNumber', 'ASC'],
['name', 'ASC'],
],
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
});
res.json(networkCards);
@@ -66,14 +72,17 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
const networkCards = await NetworkCard.findAll({
where: { deviceId },
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
order: [
['slotNumber', 'ASC'],
['name', 'ASC'],
],
});
const cardsWithPorts = await Promise.all(
networkCards.map(async (card) => {
networkCards.map(async card => {
const ports = await DevicePort.findAll({
where: { nicId: card.nicId },
order: [['portName', 'ASC']]
order: [['portName', 'ASC']],
});
const freeCount = ports.filter(p => p.status === 'free').length;
@@ -87,15 +96,15 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
total: ports.length,
free: freeCount,
occupied: occupiedCount,
fault: faultCount
}
fault: faultCount,
},
};
})
);
const ungroupedPorts = await DevicePort.findAll({
where: { deviceId, nicId: null },
order: [['portName', 'ASC']]
order: [['portName', 'ASC']],
});
if (ungroupedPorts.length > 0) {
@@ -110,8 +119,8 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
total: ungroupedPorts.length,
free: ungroupedPorts.filter(p => p.status === 'free').length,
occupied: ungroupedPorts.filter(p => p.status === 'occupied').length,
fault: ungroupedPorts.filter(p => p.status === 'fault').length
}
fault: ungroupedPorts.filter(p => p.status === 'fault').length,
},
});
}
@@ -129,9 +138,9 @@ router.get('/:nicId', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type']
}
]
attributes: ['deviceId', 'name', 'type'],
},
],
});
if (!networkCard) {
@@ -151,7 +160,7 @@ router.get('/:nicId/ports', async (req, res) => {
const ports = await DevicePort.findAll({
where: { nicId },
order: [['portName', 'ASC']]
order: [['portName', 'ASC']],
});
res.json(ports);
@@ -170,7 +179,7 @@ router.get('/find', async (req, res) => {
}
const networkCard = await NetworkCard.findOne({
where: { deviceId, name }
where: { deviceId, name },
});
if (!networkCard) {
@@ -186,14 +195,15 @@ router.get('/find', async (req, res) => {
router.post('/', async (req, res) => {
try {
const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } = req.body;
const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } =
req.body;
if (!deviceId || !name) {
return res.status(400).json({ error: '缺少必填字段' });
}
const existingCard = await NetworkCard.findOne({
where: { deviceId, name }
where: { deviceId, name },
});
if (existingCard) {
@@ -211,7 +221,7 @@ router.post('/', async (req, res) => {
model,
manufacturer,
status: status || 'normal',
portCount: 0
portCount: 0,
});
const createdCard = await NetworkCard.findByPk(networkCard.nicId, {
@@ -219,9 +229,9 @@ router.post('/', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type']
}
]
attributes: ['deviceId', 'name', 'type'],
},
],
});
res.status(201).json(createdCard);
@@ -245,7 +255,7 @@ router.post('/batch', async (req, res) => {
failed: 0,
skipped: 0,
updated: 0,
errors: []
errors: [],
};
const transaction = await NetworkCard.sequelize.transaction();
@@ -272,7 +282,7 @@ router.post('/batch', async (req, res) => {
const existingCard = await NetworkCard.findOne({
where: { deviceId: cardData.deviceId, name: cardData.name },
transaction
transaction,
});
if (existingCard) {
@@ -281,16 +291,28 @@ router.post('/batch', async (req, res) => {
continue;
}
if (updateExisting) {
await NetworkCard.update({
slotNumber: cardData.slotNumber !== undefined ? cardData.slotNumber : existingCard.slotNumber,
model: cardData.model !== undefined ? cardData.model : existingCard.model,
manufacturer: cardData.manufacturer !== undefined ? cardData.manufacturer : existingCard.manufacturer,
description: cardData.description !== undefined ? cardData.description : existingCard.description,
status: cardData.status || existingCard.status
}, {
where: { nicId: existingCard.nicId },
transaction
});
await NetworkCard.update(
{
slotNumber:
cardData.slotNumber !== undefined
? cardData.slotNumber
: existingCard.slotNumber,
model: cardData.model !== undefined ? cardData.model : existingCard.model,
manufacturer:
cardData.manufacturer !== undefined
? cardData.manufacturer
: existingCard.manufacturer,
description:
cardData.description !== undefined
? cardData.description
: existingCard.description,
status: cardData.status || existingCard.status,
},
{
where: { nicId: existingCard.nicId },
transaction,
}
);
results.updated++;
results.success++;
continue;
@@ -298,19 +320,23 @@ router.post('/batch', async (req, res) => {
throw new Error('该设备已存在同名网卡');
}
const autoNicId = cardData.nicId || `NIC-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
const autoNicId =
cardData.nicId || `NIC-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
await NetworkCard.create({
nicId: autoNicId,
deviceId: cardData.deviceId,
name: cardData.name,
slotNumber: cardData.slotNumber,
model: cardData.model,
manufacturer: cardData.manufacturer,
description: cardData.description,
status: cardData.status || 'normal',
portCount: 0
}, { transaction });
await NetworkCard.create(
{
nicId: autoNicId,
deviceId: cardData.deviceId,
name: cardData.name,
slotNumber: cardData.slotNumber,
model: cardData.model,
manufacturer: cardData.manufacturer,
description: cardData.description,
status: cardData.status || 'normal',
portCount: 0,
},
{ transaction }
);
results.success++;
} catch (error) {
@@ -319,7 +345,7 @@ router.post('/batch', async (req, res) => {
index: i + 1,
deviceId: cardData.deviceId,
name: cardData.name,
error: error.message
error: error.message,
});
}
}
@@ -339,7 +365,7 @@ router.post('/batch', async (req, res) => {
router.put('/:nicId', async (req, res) => {
try {
const [updated] = await NetworkCard.update(req.body, {
where: { nicId: req.params.nicId }
where: { nicId: req.params.nicId },
});
if (updated) {
@@ -348,9 +374,9 @@ router.put('/:nicId', async (req, res) => {
{
model: Device,
as: 'device',
attributes: ['deviceId', 'name', 'type']
}
]
attributes: ['deviceId', 'name', 'type'],
},
],
});
res.json(networkCard);
} else {
@@ -368,13 +394,13 @@ router.delete('/:nicId', async (req, res) => {
const portCount = await DevicePort.count({ where: { nicId } });
if (portCount > 0) {
return res.status(400).json({
error: `该网卡下还有 ${portCount} 个端口,请先删除或转移端口后再删除网卡`
return res.status(400).json({
error: `该网卡下还有 ${portCount} 个端口,请先删除或转移端口后再删除网卡`,
});
}
const deleted = await NetworkCard.destroy({
where: { nicId }
where: { nicId },
});
if (deleted) {
+31 -28
View File
@@ -18,7 +18,7 @@ router.get('/', authMiddleware, async (req, res) => {
keyword,
startDate,
endDate,
result
result,
} = req.query;
const offset = (parseInt(page) - 1) * parseInt(pageSize);
@@ -46,7 +46,7 @@ router.get('/', authMiddleware, async (req, res) => {
where[Op.or] = [
{ operationDescription: { [Op.like]: `%${keyword}%` } },
{ targetName: { [Op.like]: `%${keyword}%` } },
{ operatorName: { [Op.like]: `%${keyword}%` } }
{ operatorName: { [Op.like]: `%${keyword}%` } },
];
}
@@ -70,7 +70,7 @@ router.get('/', authMiddleware, async (req, res) => {
where,
order: [['createdAt', 'DESC']],
offset,
limit
limit,
});
res.json({
@@ -79,14 +79,14 @@ router.get('/', authMiddleware, async (req, res) => {
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize),
logs
}
logs,
},
});
} catch (error) {
console.error('获取操作日志失败:', error);
res.status(500).json({
success: false,
message: '获取操作日志失败'
message: '获取操作日志失败',
});
}
});
@@ -95,23 +95,23 @@ router.get('/modules', authMiddleware, async (req, res) => {
try {
const modules = await OperationLog.findAll({
attributes: ['module'],
group: ['module']
group: ['module'],
});
const moduleList = modules.map(m => ({
value: m.module,
label: getModuleName(m.module)
label: getModuleName(m.module),
}));
res.json({
success: true,
data: moduleList
data: moduleList,
});
} catch (error) {
console.error('获取模块列表失败:', error);
res.status(500).json({
success: false,
message: '获取模块列表失败'
message: '获取模块列表失败',
});
}
});
@@ -128,23 +128,23 @@ router.get('/types', authMiddleware, async (req, res) => {
const types = await OperationLog.findAll({
where,
attributes: ['operationType'],
group: ['operationType']
group: ['operationType'],
});
const typeList = types.map(t => ({
value: t.operationType,
label: getOperationTypeName(t.operationType)
label: getOperationTypeName(t.operationType),
}));
res.json({
success: true,
data: typeList
data: typeList,
});
} catch (error) {
console.error('获取操作类型列表失败:', error);
res.status(500).json({
success: false,
message: '获取操作类型列表失败'
message: '获取操作类型列表失败',
});
}
});
@@ -170,23 +170,26 @@ router.get('/statistics', authMiddleware, async (req, res) => {
OperationLog.findAll({
where,
attributes: ['module', [sequelize.fn('COUNT', sequelize.col('module')), 'count']],
group: ['module']
group: ['module'],
}),
OperationLog.findAll({
where,
attributes: ['operationType', [sequelize.fn('COUNT', sequelize.col('operationType')), 'count']],
group: ['operationType']
attributes: [
'operationType',
[sequelize.fn('COUNT', sequelize.col('operationType')), 'count'],
],
group: ['operationType'],
}),
OperationLog.findAll({
where,
attributes: [
[sequelize.fn('DATE', sequelize.col('createdAt')), 'date'],
[sequelize.fn('COUNT', '*'), 'count']
[sequelize.fn('COUNT', '*'), 'count'],
],
group: [sequelize.fn('DATE', sequelize.col('createdAt'))],
order: [[sequelize.fn('DATE', sequelize.col('createdAt')), 'DESC']],
limit: 30
})
limit: 30,
}),
]);
res.json({
@@ -194,14 +197,14 @@ router.get('/statistics', authMiddleware, async (req, res) => {
data: {
byModule: moduleStats.map(s => ({ module: s.module, count: s.get('count') })),
byType: typeStats.map(s => ({ type: s.operationType, count: s.get('count') })),
byDay: dailyStats.map(s => ({ date: s.get('date'), count: s.get('count') }))
}
byDay: dailyStats.map(s => ({ date: s.get('date'), count: s.get('count') })),
},
});
} catch (error) {
console.error('获取操作日志统计失败:', error);
res.status(500).json({
success: false,
message: '获取操作日志统计失败'
message: '获取操作日志统计失败',
});
}
});
@@ -213,19 +216,19 @@ router.get('/:recordId', authMiddleware, async (req, res) => {
if (!log) {
return res.status(404).json({
success: false,
message: '日志记录不存在'
message: '日志记录不存在',
});
}
res.json({
success: true,
data: log
data: log,
});
} catch (error) {
console.error('获取操作日志详情失败:', error);
res.status(500).json({
success: false,
message: '获取操作日志详情失败'
message: '获取操作日志详情失败',
});
}
});
@@ -239,7 +242,7 @@ function getModuleName(module) {
rack: '机柜管理',
room: '机房管理',
ticket: '工单管理',
backup: '备份管理'
backup: '备份管理',
};
return moduleNames[module] || module;
}
@@ -255,7 +258,7 @@ function getOperationTypeName(type) {
move: '移动',
permission_change: '权限变更',
import: '导入',
export: '导出'
export: '导出',
};
return typeNames[type] || type;
}
+167 -150
View File
@@ -32,7 +32,7 @@ router.get('/', async (req, res) => {
if (keyword) {
where[require('sequelize').Op.or] = [
{ rackId: { [require('sequelize').Op.like]: `%${keyword}%` } },
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } }
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } },
];
}
@@ -42,18 +42,16 @@ router.get('/', async (req, res) => {
// 获取分页数据 - 先查询机柜基本信息
const racks = await Rack.findAll({
where,
include: [
{ model: Room, separate: false }
],
include: [{ model: Room, separate: false }],
limit: pageSize,
offset: offset
offset: offset,
});
// 单独查询每个机柜的设备信息(避免 JOIN 导致的数据重复问题)
const rackIds = racks.map(r => r.rackId);
const devices = await Device.findAll({
where: { rackId: rackIds },
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height']
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height'],
});
// 将设备信息关联到对应的机柜
@@ -72,7 +70,7 @@ router.get('/', async (req, res) => {
// 返回带分页信息的响应
res.json({
racks,
total
total,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -95,20 +93,20 @@ router.get('/all', async (req, res) => {
if (keyword) {
where[require('sequelize').Op.or] = [
{ rackId: { [require('sequelize').Op.like]: `%${keyword}%` } },
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } }
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } },
];
}
const racks = await Rack.findAll({
where,
include: [{ model: Room, separate: false }],
limit: MAX_EXPORT_SIZE
limit: MAX_EXPORT_SIZE,
});
const rackIds = racks.map(r => r.rackId);
const devices = await Device.findAll({
where: { rackId: rackIds },
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height']
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height'],
});
const deviceMap = {};
@@ -125,7 +123,7 @@ router.get('/all', async (req, res) => {
res.json({
racks,
total: racks.length
total: racks.length,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -139,51 +137,49 @@ router.get('/import-template', async (req, res) => {
const templateData = [
{
'机柜ID(留空自动生成)': '',
'机柜名称': '测试机柜1',
'所属机房名称': '测试机房1',
机柜名称: '测试机柜1',
所属机房名称: '测试机房1',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': 'RACK001',
'机柜名称': '测试机柜2',
'所属机房名称': '测试机房1',
机柜名称: '测试机柜2',
所属机房名称: '测试机房1',
'高度(U)': 42,
'最大功率(W)': 3000,
'状态': 'maintenance'
}
状态: 'maintenance',
},
];
// 使用xlsx创建工作簿
const wb = XLSX.utils.book_new();
// 将数据转换为工作表
const ws = XLSX.utils.json_to_sheet(templateData);
// 设置列宽
ws['!cols'] = [
{ wch: 15 },
{ wch: 20 },
{ wch: 15 },
{ wch: 10 },
{ wch: 15 },
{ wch: 15 }
];
ws['!cols'] = [{ wch: 15 }, { wch: 20 }, { wch: 15 }, { wch: 10 }, { wch: 15 }, { wch: 15 }];
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, '机柜模板');
// 生成Excel文件的Buffer
const excelBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
// 设置响应头
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent('机柜导入模板.xlsx')}`);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
res.setHeader(
'Content-Disposition',
`attachment; filename*=UTF-8''${encodeURIComponent('机柜导入模板.xlsx')}`
);
// 发送文件
res.send(excelBuffer);
} catch (error) {
console.error('生成导入模板失败:', error);
res.status(500).json({ error: '生成导入模板失败' });
@@ -197,34 +193,36 @@ router.get('/export', async (req, res) => {
const racks = await Rack.findAll({
include: [
{ model: Room, attributes: ['name'] },
{ model: Device, attributes: ['deviceId', 'name', 'powerConsumption'] }
{ model: Device, attributes: ['deviceId', 'name', 'powerConsumption'] },
],
order: [['rackId', 'ASC']]
order: [['rackId', 'ASC']],
});
// 准备导出数据
const exportData = racks.map(rack => {
const deviceCount = rack.Devices ? rack.Devices.length : 0;
const totalPower = rack.Devices ? rack.Devices.reduce((sum, d) => sum + (d.powerConsumption || 0), 0) : 0;
const totalPower = rack.Devices
? rack.Devices.reduce((sum, d) => sum + (d.powerConsumption || 0), 0)
: 0;
return {
'机柜ID': rack.rackId,
'机柜名称': rack.name,
'所属机房': rack.Room ? rack.Room.name : '',
机柜ID: rack.rackId,
机柜名称: rack.name,
所属机房: rack.Room ? rack.Room.name : '',
'机柜高度(U)': rack.height,
'最大功耗(W)': rack.maxPower,
'当前功耗(W)': rack.currentPower || 0,
'设备数量': deviceCount,
设备数量: deviceCount,
'设备总功耗(W)': totalPower,
'状态': rack.status === 'active' ? '启用' : rack.status === 'maintenance' ? '维护中' : '停用',
'创建时间': rack.createdAt ? new Date(rack.createdAt).toLocaleString() : ''
状态: rack.status === 'active' ? '启用' : rack.status === 'maintenance' ? '维护中' : '停用',
创建时间: rack.createdAt ? new Date(rack.createdAt).toLocaleString() : '',
};
});
// 创建工作簿
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(exportData);
// 设置列宽
ws['!cols'] = [
{ wch: 15 }, // 机柜ID
@@ -236,53 +234,58 @@ router.get('/export', async (req, res) => {
{ wch: 12 }, // 设备数量
{ wch: 15 }, // 设备总功耗
{ wch: 10 }, // 状态
{ wch: 20 } // 创建时间
{ wch: 20 }, // 创建时间
];
XLSX.utils.book_append_sheet(wb, ws, '机柜列表');
// 生成文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const fileName = `机柜导出_${timestamp}.xlsx`;
// 确保temp目录存在
const tempDir = path.join(__dirname, '../temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const filePath = path.join(tempDir, fileName);
// 写入文件
XLSX.writeFile(wb, filePath);
// 发送文件
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
res.setHeader(
'Content-Disposition',
`attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`
);
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
// 发送完成后删除临时文件
fileStream.on('close', () => {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
});
fileStream.on('error', (err) => {
fileStream.on('error', err => {
console.error('文件流错误:', err);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
});
} catch (error) {
console.error('导出租机柜数据失败:', error);
res.status(500).json({
success: false,
res.status(500).json({
success: false,
message: '导出失败',
error: error.message
error: error.message,
});
}
});
@@ -293,9 +296,9 @@ router.get('/:rackId', async (req, res) => {
const rack = await Rack.findByPk(req.params.rackId, {
include: [
{ model: Room, separate: false },
{ model: Device, separate: false }
{ model: Device, separate: false },
],
subQuery: false // 避免子查询导致的性能问题
subQuery: false, // 避免子查询导致的性能问题
});
if (!rack) {
return res.status(404).json({ error: '机柜不存在' });
@@ -312,11 +315,11 @@ async function generateRackId() {
const racks = await Rack.findAll({
where: {
rackId: {
[require('sequelize').Op.like]: 'RACK%'
}
}
[require('sequelize').Op.like]: 'RACK%',
},
},
});
let maxNumber = 0;
racks.forEach(rack => {
const match = rack.rackId.match(/^RACK(\d+)$/);
@@ -327,7 +330,7 @@ async function generateRackId() {
}
}
});
// 生成新的机柜ID,序号+1,至少3位数字
const newNumber = maxNumber + 1;
return `RACK${String(newNumber).padStart(3, '0')}`;
@@ -337,12 +340,12 @@ async function generateRackId() {
router.post('/', validateBody(createRackSchema), async (req, res) => {
try {
const rackData = { ...req.body };
// 如果没有提供rackId或为空,则自动生成
if (!rackData.rackId || rackData.rackId.trim() === '') {
rackData.rackId = await generateRackId();
}
const rack = await Rack.create(rackData);
res.status(201).json(rack);
} catch (error) {
@@ -354,15 +357,15 @@ router.post('/', validateBody(createRackSchema), async (req, res) => {
router.put('/:rackId', validateBody(updateRackSchema), async (req, res) => {
try {
const [updated] = await Rack.update(req.body, {
where: { rackId: req.params.rackId }
where: { rackId: req.params.rackId },
});
if (updated) {
const updatedRack = await Rack.findByPk(req.params.rackId, {
include: [
{ model: Room, separate: false },
{ model: Device, separate: false }
{ model: Device, separate: false },
],
subQuery: false // 避免子查询导致的性能问题
subQuery: false, // 避免子查询导致的性能问题
});
res.json(updatedRack);
} else {
@@ -381,9 +384,9 @@ router.delete('/:rackId', async (req, res) => {
if (devices.length > 0) {
return res.status(400).json({ error: '该机柜下有设备,无法删除' });
}
const deleted = await Rack.destroy({
where: { rackId: req.params.rackId }
where: { rackId: req.params.rackId },
});
if (deleted) {
res.status(204).json();
@@ -398,36 +401,36 @@ router.delete('/:rackId', async (req, res) => {
// 导入机柜数据 - 优化版:使用事务+批量插入
router.post('/import', async (req, res) => {
const t = await sequelize.transaction();
try {
// 检查是否有上传文件
if (!req.files || !req.files.file) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: '没有上传文件',
error: '没有找到有效的上传文件,请选择一个Excel文件后重试'
error: '没有找到有效的上传文件,请选择一个Excel文件后重试',
});
}
const file = req.files.file;
// 确保temp目录存在
const tempDir = path.join(__dirname, '../temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// 保存临时文件
const tempFilePath = path.join(tempDir, `${Date.now()}_${file.name}`);
try {
await file.mv(tempFilePath);
} catch (saveError) {
await t.rollback();
return res.status(500).json({
success: false,
return res.status(500).json({
success: false,
message: '文件保存失败',
error: `无法保存上传的文件: ${saveError.message}`
error: `无法保存上传的文件: ${saveError.message}`,
});
}
@@ -438,28 +441,29 @@ router.post('/import', async (req, res) => {
workbook = XLSX.readFile(tempFilePath);
} catch (readError) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: '文件解析失败',
error: `无法解析Excel文件: ${readError.message}`
error: `无法解析Excel文件: ${readError.message}`,
});
}
// 获取第一个工作表
if (!workbook.SheetNames.length) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: '文件格式错误',
error: 'Excel文件中没有找到工作表'
error: 'Excel文件中没有找到工作表',
});
}
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// 读取第一行作为列头
const headerRow = XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 0, limit: 1 })[0] || [];
const headerRow =
XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 0, limit: 1 })[0] || [];
// 定义列名映射(支持导入模板格式和导出文件格式)
const columnMapping = {
rackId: ['机柜ID(留空自动生成)', '机柜ID'],
@@ -467,9 +471,9 @@ router.post('/import', async (req, res) => {
roomName: ['所属机房名称', '所属机房'],
height: ['高度(U)', '机柜高度(U)'],
maxPower: ['最大功率(W)', '最大功耗(W)'],
status: ['状态']
status: ['状态'],
};
// 根据列头自动检测列索引映射
const columnIndexMap = {};
Object.keys(columnMapping).forEach(field => {
@@ -479,26 +483,26 @@ router.post('/import', async (req, res) => {
columnIndexMap[field] = index;
}
});
// 检查必需的列是否存在
const requiredColumns = ['name', 'roomName', 'height', 'maxPower', 'status'];
const missingColumns = requiredColumns.filter(col => columnIndexMap[col] === undefined);
if (missingColumns.length > 0) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: 'Excel列名格式不正确',
error: `缺少必需的列: ${missingColumns.join(', ')},请使用系统导出的文件或下载导入模板`
error: `缺少必需的列: ${missingColumns.join(', ')},请使用系统导出的文件或下载导入模板`,
});
}
// 转换为JSON格式
const rawData = XLSX.utils.sheet_to_json(worksheet, {
header: headerRow.map((h, i) => `col_${i}`),
range: 1,
blankrows: false
blankrows: false,
});
const jsonData = rawData.map(row => {
const item = {};
Object.keys(columnIndexMap).forEach(field => {
@@ -510,67 +514,80 @@ router.post('/import', async (req, res) => {
if (jsonData.length === 0) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: '没有找到有效数据',
error: 'Excel文件中没有找到可导入的数据行'
error: 'Excel文件中没有找到可导入的数据行',
});
}
// 状态值转换映射
const statusMapping = {
'启用': 'active', '在用': 'active', '停用': 'inactive',
'禁用': 'inactive', '维护中': 'maintenance',
'active': 'active', 'inactive': 'inactive', 'maintenance': 'maintenance'
启用: 'active',
在用: 'active',
停用: 'inactive',
禁用: 'inactive',
维护中: 'maintenance',
active: 'active',
inactive: 'inactive',
maintenance: 'maintenance',
};
const validStatuses = ['active', 'maintenance', 'inactive'];
const validationResults = [];
// 【优化1】批量查询机房信息(单次查询)
const allRooms = await Room.findAll({ transaction: t });
const roomNameToIdMap = new Map(allRooms.map(room => [room.name, room.roomId]));
const validRoomNames = new Set(roomNameToIdMap.keys());
// 【优化2】批量查询现有最大机柜ID(单次查询)
const maxRackResult = await Rack.findOne({
attributes: [[sequelize.fn('MAX', sequelize.cast(sequelize.fn('SUBSTR', sequelize.col('rackId'), 5), 'INTEGER')), 'maxNum']],
attributes: [
[
sequelize.fn(
'MAX',
sequelize.cast(sequelize.fn('SUBSTR', sequelize.col('rackId'), 5), 'INTEGER')
),
'maxNum',
],
],
where: {
rackId: {
[require('sequelize').Op.like]: 'RACK%'
}
[require('sequelize').Op.like]: 'RACK%',
},
},
transaction: t
transaction: t,
});
let maxNumber = maxRackResult?.get('maxNum') || 0;
// 处理数据
const processedData = jsonData.map((item, index) => {
const rowNumber = index + 2;
const rawStatus = String(item.status || '').trim();
const normalizedStatus = statusMapping[rawStatus] || rawStatus.toLowerCase();
const rawRackId = item.rackId ? String(item.rackId).trim() : '';
if (!rawRackId || rawRackId === '') {
maxNumber++;
return {
...item,
rackId: `RACK${String(maxNumber).padStart(3, '0')}`,
return {
...item,
rackId: `RACK${String(maxNumber).padStart(3, '0')}`,
status: normalizedStatus,
rowNumber
rowNumber,
};
}
return {
...item,
rackId: rawRackId,
return {
...item,
rackId: rawRackId,
status: normalizedStatus,
rowNumber
rowNumber,
};
});
// 验证数据
processedData.forEach((item) => {
processedData.forEach(item => {
const errors = [];
if (!/^[a-zA-Z0-9_-]+$/.test(item.rackId)) {
@@ -594,7 +611,7 @@ router.post('/import', async (req, res) => {
if (!item.status || !validStatuses.includes(item.status)) {
errors.push(`状态必须是以下值之一: ${validStatuses.join(', ')}`);
}
if (errors.length > 0) {
validationResults.push({ row: item.rowNumber, data: item, errors });
}
@@ -602,26 +619,26 @@ router.post('/import', async (req, res) => {
if (validationResults.length > 0) {
await t.rollback();
return res.status(400).json({
success: false,
return res.status(400).json({
success: false,
message: '数据验证失败',
error: `${validationResults.length} 行数据格式错误`,
details: validationResults
details: validationResults,
});
}
// 【优化3】批量查询已存在的机柜ID(单次查询)
const existingRacks = await Rack.findAll({
where: {
rackId: processedData.map(item => item.rackId)
rackId: processedData.map(item => item.rackId),
},
transaction: t
transaction: t,
});
const existingIds = new Set(existingRacks.map(rack => rack.rackId));
const newData = processedData.filter(item => !existingIds.has(item.rackId));
const duplicateCount = processedData.length - newData.length;
// 【优化4】批量插入数据
let createdCount = 0;
if (newData.length > 0) {
@@ -632,16 +649,16 @@ router.post('/import', async (req, res) => {
maxPower: item.maxPower,
status: item.status,
roomId: roomNameToIdMap.get(item.roomName.trim()),
currentPower: 0
currentPower: 0,
}));
const result = await Rack.bulkCreate(dataWithRoomId, {
transaction: t,
ignoreDuplicates: true
ignoreDuplicates: true,
});
createdCount = result.length;
}
// 提交事务
await t.commit();
@@ -655,7 +672,7 @@ router.post('/import', async (req, res) => {
duplicates: duplicateCount,
total: jsonData.length,
createdRacks,
skippedRacks
skippedRacks,
});
} finally {
// 删除临时文件
@@ -665,12 +682,12 @@ router.post('/import', async (req, res) => {
}
} catch (error) {
await t.rollback();
res.status(500).json({
success: false,
res.status(500).json({
success: false,
message: '服务器内部错误',
error: `导入过程中发生未知错误: ${error.message}`
error: `导入过程中发生未知错误: ${error.message}`,
});
}
});
module.exports = router;
module.exports = router;
+109 -67
View File
@@ -32,7 +32,10 @@ router.get('/', authMiddleware, async (req, res) => {
where,
limit,
offset,
order: [['sort', 'ASC'], ['createdAt', 'DESC']]
order: [
['sort', 'ASC'],
['createdAt', 'DESC'],
],
});
res.json({
@@ -41,14 +44,14 @@ router.get('/', authMiddleware, async (req, res) => {
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize),
roles
}
roles,
},
});
} catch (error) {
console.error('获取角色列表错误:', error);
res.status(500).json({
success: false,
message: '获取角色列表失败'
message: '获取角色列表失败',
});
}
});
@@ -57,18 +60,18 @@ router.get('/all', authMiddleware, async (req, res) => {
try {
const roles = await Role.findAll({
where: { status: 'active' },
order: [['sort', 'ASC']]
order: [['sort', 'ASC']],
});
res.json({
success: true,
data: roles
data: roles,
});
} catch (error) {
console.error('获取所有角色错误:', error);
res.status(500).json({
success: false,
message: '获取角色列表失败'
message: '获取角色列表失败',
});
}
});
@@ -80,13 +83,13 @@ router.get('/:roleId', authMiddleware, async (req, res) => {
if (!role) {
return res.status(404).json({
success: false,
message: '角色不存在'
message: '角色不存在',
});
}
const permissions = await Permission.findAll({
where: { status: 'active' },
order: [['sort', 'ASC']]
order: [['sort', 'ASC']],
});
res.json({
@@ -94,14 +97,14 @@ router.get('/:roleId', authMiddleware, async (req, res) => {
data: {
role,
permissions,
rolePermissions: role.permissions || []
}
rolePermissions: role.permissions || [],
},
});
} catch (error) {
console.error('获取角色详情错误:', error);
res.status(500).json({
success: false,
message: '获取角色详情失败'
message: '获取角色详情失败',
});
}
});
@@ -113,7 +116,7 @@ router.post('/', authMiddleware, async (req, res) => {
if (!roleName || !roleCode) {
return res.status(400).json({
success: false,
message: '角色名称和角色编码不能为空'
message: '角色名称和角色编码不能为空',
});
}
@@ -121,7 +124,7 @@ router.post('/', authMiddleware, async (req, res) => {
if (existingRole) {
return res.status(400).json({
success: false,
message: '角色编码已存在'
message: '角色编码已存在',
});
}
@@ -132,31 +135,33 @@ router.post('/', authMiddleware, async (req, res) => {
description,
permissions: permissions || [],
status: status || 'active',
sort: sort || 0
sort: sort || 0,
});
const permissionNames = permissions && permissions.length > 0
? permissions.join('、')
: '无';
const permissionNames = permissions && permissions.length > 0 ? permissions.join('、') : '无';
await logRoleOperation('create', `创建角色【${roleName}】(编码:${roleCode},权限:${permissionNames}`, {
targetId: role.roleId,
targetName: roleName,
afterState: role.toJSON(),
req,
metadata: { roleCode, permissions, permissionNames }
});
await logRoleOperation(
'create',
`创建角色【${roleName}】(编码:${roleCode},权限:${permissionNames}`,
{
targetId: role.roleId,
targetName: roleName,
afterState: role.toJSON(),
req,
metadata: { roleCode, permissions, permissionNames },
}
);
res.status(201).json({
success: true,
message: '创建成功',
data: role
data: role,
});
} catch (error) {
console.error('创建角色错误:', error);
res.status(500).json({
success: false,
message: '创建角色失败'
message: '创建角色失败',
});
}
});
@@ -169,17 +174,27 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
if (!role) {
return res.status(404).json({
success: false,
message: '角色不存在'
message: '角色不存在',
});
}
const beforeState = role.toJSON();
if (roleName !== undefined) role.roleName = roleName;
if (description !== undefined) role.description = description;
if (permissions !== undefined) role.permissions = permissions;
if (status !== undefined) role.status = status;
if (sort !== undefined) role.sort = sort;
if (roleName !== undefined) {
role.roleName = roleName;
}
if (description !== undefined) {
role.description = description;
}
if (permissions !== undefined) {
role.permissions = permissions;
}
if (status !== undefined) {
role.status = status;
}
if (sort !== undefined) {
role.sort = sort;
}
await role.save();
@@ -201,21 +216,33 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
}
if (status !== undefined && beforeState.status !== status) {
const statusText = { active: '启用', inactive: '禁用' };
changedFields.status = { from: beforeState.status, to: status, fromText: statusText[beforeState.status], toText: statusText[status] };
changedFields.status = {
from: beforeState.status,
to: status,
fromText: statusText[beforeState.status],
toText: statusText[status],
};
}
const changeDetails = Object.entries(changedFields).map(([field, values]) => {
const fieldNames = { roleName: '角色名称', description: '描述', permissions: '权限', status: '状态' };
const displayName = fieldNames[field] || field;
const changeDetails = Object.entries(changedFields)
.map(([field, values]) => {
const fieldNames = {
roleName: '角色名称',
description: '描述',
permissions: '权限',
status: '状态',
};
const displayName = fieldNames[field] || field;
if (field === 'permissions') {
return `权限: ${(values.from || []).join('、') || '无'}${(values.to || []).join('、') || '无'}`;
}
if (field === 'status') {
return `状态: ${values.fromText}${values.toText}`;
}
return `${displayName}: ${values.from ?? '空'}${values.to ?? '空'}`;
}).join('');
if (field === 'permissions') {
return `权限: ${(values.from || []).join('、') || '无'}${(values.to || []).join('、') || '无'}`;
}
if (field === 'status') {
return `状态: ${values.fromText}${values.toText}`;
}
return `${displayName}: ${values.from ?? '空'}${values.to ?? '空'}`;
})
.join('');
const updateDesc = changeDetails
? `更新角色【${role.roleName}】:${changeDetails}`
@@ -227,19 +254,23 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
beforeState,
afterState,
req,
metadata: { changedFields, oldRoleName: beforeState.roleName, oldPermissions: beforeState.permissions }
metadata: {
changedFields,
oldRoleName: beforeState.roleName,
oldPermissions: beforeState.permissions,
},
});
res.json({
success: true,
message: '更新成功',
data: role
data: role,
});
} catch (error) {
console.error('更新角色错误:', error);
res.status(500).json({
success: false,
message: '更新角色失败'
message: '更新角色失败',
});
}
});
@@ -251,14 +282,14 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
if (!role) {
return res.status(404).json({
success: false,
message: '角色不存在'
message: '角色不存在',
});
}
if (role.roleCode === 'admin') {
return res.status(400).json({
success: false,
message: '不能删除管理员角色'
message: '不能删除管理员角色',
});
}
@@ -266,7 +297,7 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
if (userCount > 0) {
return res.status(400).json({
success: false,
message: '该角色下有用户,不能删除'
message: '该角色下有用户,不能删除',
});
}
@@ -276,23 +307,27 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
await role.destroy();
await logRoleOperation('delete', `删除角色【${roleName}】(编码:${roleCode},权限:${(role.permissions || []).join('、') || '无'}`, {
targetId: req.params.roleId,
targetName: roleName,
beforeState,
req,
metadata: { roleCode, userCount, permissions: role.permissions }
});
await logRoleOperation(
'delete',
`删除角色【${roleName}】(编码:${roleCode},权限:${(role.permissions || []).join('、') || '无'}`,
{
targetId: req.params.roleId,
targetName: roleName,
beforeState,
req,
metadata: { roleCode, userCount, permissions: role.permissions },
}
);
res.json({
success: true,
message: '删除成功'
message: '删除成功',
});
} catch (error) {
console.error('删除角色错误:', error);
res.status(500).json({
success: false,
message: '删除角色失败'
message: '删除角色失败',
});
}
});
@@ -307,16 +342,23 @@ router.post('/init-roles', async (req, res) => {
description: '系统管理员,拥有所有权限',
permissions: ['*'],
status: 'active',
sort: 1
sort: 1,
},
{
roleId: 'role_operator',
roleName: '运维人员',
roleCode: 'operator',
description: '负责日常运维操作',
permissions: ['devices:read', 'devices:write', 'racks:read', 'rooms:read', 'consumables:read', 'consumables:write'],
permissions: [
'devices:read',
'devices:write',
'racks:read',
'rooms:read',
'consumables:read',
'consumables:write',
],
status: 'active',
sort: 2
sort: 2,
},
{
roleId: 'role_viewer',
@@ -325,8 +367,8 @@ router.post('/init-roles', async (req, res) => {
description: '仅能查看数据',
permissions: ['devices:read', 'racks:read', 'rooms:read', 'consumables:read'],
status: 'active',
sort: 3
}
sort: 3,
},
];
for (const roleData of defaultRoles) {
@@ -335,13 +377,13 @@ router.post('/init-roles', async (req, res) => {
res.json({
success: true,
message: '初始化角色成功'
message: '初始化角色成功',
});
} catch (error) {
console.error('初始化角色错误:', error);
res.status(500).json({
success: false,
message: '初始化角色失败'
message: '初始化角色失败',
});
}
});
+7 -7
View File
@@ -15,12 +15,12 @@ router.get('/', async (req, res) => {
const { count, rows } = await Room.findAndCountAll({
include: [{ model: Rack, attributes: ['rackId', 'name'] }],
offset: offset,
limit: pageSize
limit: pageSize,
});
res.json({
rooms: rows,
total: count
total: count,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -31,7 +31,7 @@ router.get('/', async (req, res) => {
router.get('/:roomId', async (req, res) => {
try {
const room = await Room.findByPk(req.params.roomId, {
include: Rack
include: Rack,
});
if (!room) {
return res.status(404).json({ error: '机房不存在' });
@@ -56,7 +56,7 @@ router.post('/', async (req, res) => {
router.put('/:roomId', validateBody(updateRoomSchema), async (req, res) => {
try {
const [updated] = await Room.update(req.body, {
where: { roomId: req.params.roomId }
where: { roomId: req.params.roomId },
});
if (updated) {
const updatedRoom = await Room.findByPk(req.params.roomId);
@@ -77,9 +77,9 @@ router.delete('/:roomId', async (req, res) => {
if (racks.length > 0) {
return res.status(400).json({ error: '该机房下有机柜,无法删除' });
}
const deleted = await Room.destroy({
where: { roomId: req.params.roomId }
where: { roomId: req.params.roomId },
});
if (deleted) {
res.status(204).json();
@@ -91,4 +91,4 @@ router.delete('/:roomId', async (req, res) => {
}
});
module.exports = router;
module.exports = router;
+40 -20
View File
@@ -32,26 +32,37 @@ router.get('/', async (req, res) => {
where: {
createdAt: {
[Op.gte]: dayStart,
[Op.lt]: dayEnd
}
}
})
[Op.lt]: dayEnd,
},
},
}),
});
}
const dayResults = await Promise.all(dayQueries.map(d => d.query));
const deviceTrendData = dayQueries.map((d, index) => ({
label: d.label,
value: dayResults[index]
value: dayResults[index],
}));
const [
totalDevices, faultDevices, totalRacks, rooms, totalUsers, activeTickets,
newDevicesThisWeek, newDevicesLastWeek,
faultDevicesThisWeek, faultDevicesLastWeek,
newUsersThisWeek, newUsersLastWeek,
newTicketsThisWeek, newTicketsLastWeek,
runningDevices, maintenanceDevices, offlineDevices
totalDevices,
faultDevices,
totalRacks,
rooms,
totalUsers,
activeTickets,
newDevicesThisWeek,
newDevicesLastWeek,
faultDevicesThisWeek,
faultDevicesLastWeek,
newUsersThisWeek,
newUsersLastWeek,
newTicketsThisWeek,
newTicketsLastWeek,
runningDevices,
maintenanceDevices,
offlineDevices,
] = await Promise.all([
Device.count(),
Device.count({ where: { status: 'fault' } }),
@@ -62,7 +73,9 @@ router.get('/', async (req, res) => {
Device.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
Device.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: oneWeekAgo } } }),
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
Device.count({
where: { status: 'fault', updatedAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } },
}),
User.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
User.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
Ticket.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
@@ -74,7 +87,9 @@ router.get('/', async (req, res) => {
let deviceGrowth = 0;
if (newDevicesLastWeek > 0) {
deviceGrowth = parseFloat(((newDevicesThisWeek - newDevicesLastWeek) / newDevicesLastWeek * 100).toFixed(1));
deviceGrowth = parseFloat(
(((newDevicesThisWeek - newDevicesLastWeek) / newDevicesLastWeek) * 100).toFixed(1)
);
} else if (newDevicesThisWeek > 0) {
deviceGrowth = 100;
} else {
@@ -83,7 +98,9 @@ router.get('/', async (req, res) => {
let faultTrend = 0;
if (faultDevicesLastWeek > 0) {
faultTrend = parseFloat(((faultDevicesThisWeek - faultDevicesLastWeek) / faultDevicesLastWeek * 100).toFixed(1));
faultTrend = parseFloat(
(((faultDevicesThisWeek - faultDevicesLastWeek) / faultDevicesLastWeek) * 100).toFixed(1)
);
} else if (faultDevicesThisWeek > 0) {
faultTrend = 100;
} else {
@@ -92,7 +109,9 @@ router.get('/', async (req, res) => {
let userGrowth = 0;
if (newUsersLastWeek > 0) {
userGrowth = parseFloat(((newUsersThisWeek - newUsersLastWeek) / newUsersLastWeek * 100).toFixed(1));
userGrowth = parseFloat(
(((newUsersThisWeek - newUsersLastWeek) / newUsersLastWeek) * 100).toFixed(1)
);
} else if (newUsersThisWeek > 0) {
userGrowth = 100;
} else {
@@ -101,16 +120,17 @@ router.get('/', async (req, res) => {
let ticketTrend = 0;
if (newTicketsLastWeek > 0) {
ticketTrend = parseFloat(((newTicketsThisWeek - newTicketsLastWeek) / newTicketsLastWeek * 100).toFixed(1));
ticketTrend = parseFloat(
(((newTicketsThisWeek - newTicketsLastWeek) / newTicketsLastWeek) * 100).toFixed(1)
);
} else if (newTicketsThisWeek > 0) {
ticketTrend = 100;
} else {
ticketTrend = 0;
}
const onlineRate = totalDevices > 0
? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1)
: 100;
const onlineRate =
totalDevices > 0 ? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1) : 100;
const totalRooms = rooms.length;
@@ -146,4 +166,4 @@ router.get('/', async (req, res) => {
}
});
module.exports = router;
module.exports = router;
+276 -109
View File
@@ -10,33 +10,194 @@ const { FRONTEND } = require('../config');
const initDefaultSettings = async () => {
const defaultSettings = [
// 全局配置
{ settingKey: 'site_name', settingValue: JSON.stringify('机柜管理系统'), settingType: 'string', category: 'general', description: '网站名称', isEditable: true },
{ settingKey: 'site_logo', settingValue: JSON.stringify(''), settingType: 'string', category: 'general', description: '网站Logo URL', isEditable: true },
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(60), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
{
settingKey: 'site_name',
settingValue: JSON.stringify('机柜管理系统'),
settingType: 'string',
category: 'general',
description: '网站名称',
isEditable: true,
},
{
settingKey: 'site_logo',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'general',
description: '网站Logo URL',
isEditable: true,
},
{
settingKey: 'timezone',
settingValue: JSON.stringify('Asia/Shanghai'),
settingType: 'string',
category: 'general',
description: '时区设置',
isEditable: true,
},
{
settingKey: 'date_format',
settingValue: JSON.stringify('YYYY-MM-DD'),
settingType: 'string',
category: 'general',
description: '日期格式',
isEditable: true,
},
{
settingKey: 'session_timeout',
settingValue: JSON.stringify(30),
settingType: 'number',
category: 'general',
description: '登录有效期(分钟)',
isEditable: true,
},
{
settingKey: 'idle_timeout',
settingValue: JSON.stringify(30),
settingType: 'number',
category: 'general',
description: '用户空闲超时时间(分钟)',
isEditable: true,
},
{
settingKey: 'idle_warning_time',
settingValue: JSON.stringify(60),
settingType: 'number',
category: 'general',
description: '空闲超时前警告时间(秒)',
isEditable: false,
},
{
settingKey: 'max_login_attempts',
settingValue: JSON.stringify(5),
settingType: 'number',
category: 'general',
description: '最大登录尝试次数',
isEditable: true,
},
{
settingKey: 'maintenance_mode',
settingValue: JSON.stringify(false),
settingType: 'boolean',
category: 'general',
description: '维护模式',
isEditable: true,
},
// 外观设置
{ settingKey: 'primary_color', settingValue: JSON.stringify('#667eea'), settingType: 'string', category: 'appearance', description: '主题主色调', isEditable: true },
{ settingKey: 'secondary_color', settingValue: JSON.stringify('#764ba2'), settingType: 'string', category: 'appearance', description: '主题辅助色调', isEditable: true },
{ settingKey: 'compact_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '紧凑模式', isEditable: true },
{ settingKey: 'sidebar_collapsed', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '侧边栏默认折叠', isEditable: true },
{ settingKey: 'table_row_height', settingValue: JSON.stringify('default'), settingType: 'string', category: 'appearance', description: '表格行高: small/default/middle/large', isEditable: true },
{ settingKey: 'animation_enabled', settingValue: JSON.stringify(true), settingType: 'boolean', category: 'appearance', description: '启用动画效果', isEditable: true },
{
settingKey: 'primary_color',
settingValue: JSON.stringify('#667eea'),
settingType: 'string',
category: 'appearance',
description: '主题主色调',
isEditable: true,
},
{
settingKey: 'secondary_color',
settingValue: JSON.stringify('#764ba2'),
settingType: 'string',
category: 'appearance',
description: '主题辅助色调',
isEditable: true,
},
{
settingKey: 'compact_mode',
settingValue: JSON.stringify(false),
settingType: 'boolean',
category: 'appearance',
description: '紧凑模式',
isEditable: true,
},
{
settingKey: 'sidebar_collapsed',
settingValue: JSON.stringify(false),
settingType: 'boolean',
category: 'appearance',
description: '侧边栏默认折叠',
isEditable: true,
},
{
settingKey: 'table_row_height',
settingValue: JSON.stringify('default'),
settingType: 'string',
category: 'appearance',
description: '表格行高: small/default/middle/large',
isEditable: true,
},
{
settingKey: 'animation_enabled',
settingValue: JSON.stringify(true),
settingType: 'boolean',
category: 'appearance',
description: '启用动画效果',
isEditable: true,
},
// 关于页面
{ settingKey: 'app_version', settingValue: JSON.stringify('1.0.0'), settingType: 'string', category: 'about', description: '应用版本', isEditable: false },
{ settingKey: 'company_name', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司/组织名称', isEditable: true },
{ settingKey: 'contact_email', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系邮箱', isEditable: true },
{ settingKey: 'contact_phone', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系电话', isEditable: true },
{ settingKey: 'company_address', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司地址', isEditable: true },
{ settingKey: 'system_description', settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'), settingType: 'string', category: 'about', description: '系统描述', isEditable: true },
{ settingKey: 'privacy_policy', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '隐私政策URL', isEditable: true },
{ settingKey: 'terms_of_service', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '服务条款URL', isEditable: true },
{
settingKey: 'app_version',
settingValue: JSON.stringify('1.0.0'),
settingType: 'string',
category: 'about',
description: '应用版本',
isEditable: false,
},
{
settingKey: 'company_name',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '公司/组织名称',
isEditable: true,
},
{
settingKey: 'contact_email',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '联系邮箱',
isEditable: true,
},
{
settingKey: 'contact_phone',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '联系电话',
isEditable: true,
},
{
settingKey: 'company_address',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '公司地址',
isEditable: true,
},
{
settingKey: 'system_description',
settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'),
settingType: 'string',
category: 'about',
description: '系统描述',
isEditable: true,
},
{
settingKey: 'privacy_policy',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '隐私政策URL',
isEditable: true,
},
{
settingKey: 'terms_of_service',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '服务条款URL',
isEditable: true,
},
];
let createdCount = 0;
@@ -60,7 +221,9 @@ const initDefaultSettings = async () => {
}
}
console.log(`系统设置初始化结果: 创建 ${createdCount} 个, 更新 ${updatedCount} 个, 失败 ${errorCount}`);
console.log(
`系统设置初始化结果: 创建 ${createdCount} 个, 更新 ${updatedCount} 个, 失败 ${errorCount}`
);
return { createdCount, updatedCount, errorCount };
};
@@ -75,12 +238,15 @@ router.get('/', async (req, res) => {
if (category) {
where.category = category;
}
const settings = await SystemSetting.findAll({
where,
order: [['category', 'ASC'], ['settingKey', 'ASC']]
order: [
['category', 'ASC'],
['settingKey', 'ASC'],
],
});
// 格式化返回数据
const formattedSettings = {};
settings.forEach(setting => {
@@ -90,10 +256,10 @@ router.get('/', async (req, res) => {
category: setting.category,
description: setting.description,
isEditable: setting.isEditable,
updatedAt: setting.updatedAt
updatedAt: setting.updatedAt,
};
});
res.json(formattedSettings);
} catch (error) {
res.status(500).json({ error: error.message });
@@ -116,7 +282,7 @@ router.get('/idle-timeout', async (req, res) => {
timeout: timeout * 60 * 1000, // 转换为毫秒
warningTime: fixedWarningTime * 1000, // 固定10秒(转换为毫秒)
timeoutMinutes: timeout,
warningTimeSeconds: fixedWarningTime
warningTimeSeconds: fixedWarningTime,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -128,11 +294,11 @@ router.get('/:key', async (req, res) => {
try {
const { key } = req.params;
const setting = await SystemSetting.findByPk(key);
if (!setting) {
return res.status(404).json({ error: '设置不存在' });
}
res.json({
key: setting.settingKey,
value: JSON.parse(setting.settingValue),
@@ -140,7 +306,7 @@ router.get('/:key', async (req, res) => {
category: setting.category,
description: setting.description,
isEditable: setting.isEditable,
updatedAt: setting.updatedAt
updatedAt: setting.updatedAt,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -152,17 +318,17 @@ router.put('/:key', async (req, res) => {
try {
const { key } = req.params;
const { value } = req.body;
const setting = await SystemSetting.findByPk(key);
if (!setting) {
return res.status(404).json({ error: '设置不存在' });
}
if (!setting.isEditable) {
return res.status(403).json({ error: '该设置不可编辑' });
}
// 验证值类型
let parsedValue = value;
if (setting.settingType === 'number') {
@@ -173,18 +339,18 @@ router.put('/:key', async (req, res) => {
} else if (setting.settingType === 'boolean') {
parsedValue = Boolean(value);
}
await setting.update({
settingValue: JSON.stringify(parsedValue)
settingValue: JSON.stringify(parsedValue),
});
res.json({
message: '设置更新成功',
setting: {
key: setting.settingKey,
value: parsedValue,
updatedAt: setting.updatedAt
}
updatedAt: setting.updatedAt,
},
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -195,28 +361,28 @@ router.put('/:key', async (req, res) => {
router.put('/', async (req, res) => {
try {
const { settings } = req.body;
if (!settings || typeof settings !== 'object') {
return res.status(400).json({ error: '请提供有效的设置对象' });
}
const updatedSettings = [];
const errors = [];
for (const [key, value] of Object.entries(settings)) {
try {
const setting = await SystemSetting.findByPk(key);
if (!setting) {
errors.push({ key, error: '设置不存在' });
continue;
}
if (!setting.isEditable) {
errors.push({ key, error: '该设置不可编辑' });
continue;
}
let parsedValue = value;
if (setting.settingType === 'number') {
parsedValue = Number(value);
@@ -227,21 +393,21 @@ router.put('/', async (req, res) => {
} else if (setting.settingType === 'boolean') {
parsedValue = Boolean(value);
}
await setting.update({
settingValue: JSON.stringify(parsedValue)
settingValue: JSON.stringify(parsedValue),
});
updatedSettings.push({ key, value: parsedValue });
} catch (error) {
errors.push({ key, error: error.message });
}
}
res.json({
message: `成功更新 ${updatedSettings.length} 个设置`,
updatedSettings,
errors: errors.length > 0 ? errors : undefined
errors: errors.length > 0 ? errors : undefined,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -253,11 +419,11 @@ router.post('/reset/:key', async (req, res) => {
try {
const { key } = req.params;
const setting = await SystemSetting.findByPk(key);
if (!setting) {
return res.status(404).json({ error: '设置不存在' });
}
const defaultValues = {
site_name: '机柜管理系统',
site_logo: '',
@@ -281,22 +447,22 @@ router.post('/reset/:key', async (req, res) => {
company_address: '',
system_description: '机柜管理系统 - 专业的数据中心设备管理解决方案',
privacy_policy: '',
terms_of_service: ''
terms_of_service: '',
};
const defaultValue = defaultValues[key];
if (defaultValue === undefined) {
return res.status(400).json({ error: '该设置没有默认值' });
}
await setting.update({
settingValue: JSON.stringify(defaultValue)
settingValue: JSON.stringify(defaultValue),
});
res.json({
message: '设置已重置为默认值',
key,
value: defaultValue
value: defaultValue,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -309,7 +475,7 @@ router.post('/backup', async (req, res) => {
// 获取系统设置中的备份路径
const backupPathSetting = await SystemSetting.findByPk('backup_path');
const backupPath = backupPathSetting ? JSON.parse(backupPathSetting.settingValue) : './backups';
// 解析备份目录路径
let backupDir;
if (backupPath.startsWith('/') || backupPath.match(/^[A-Za-z]:\//)) {
@@ -319,22 +485,22 @@ router.post('/backup', async (req, res) => {
// 相对路径,基于项目根目录
backupDir = path.join(__dirname, '../', backupPath);
}
// 确保备份目录存在
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFile = path.join(backupDir, `backup_${timestamp}.json`);
// 获取所有数据库数据
const Device = require('../models/Device');
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const Consumable = require('../models/Consumable');
const User = require('../models/User');
const backupData = {
timestamp: new Date().toISOString(),
version: '1.0.0',
@@ -344,38 +510,38 @@ router.post('/backup', async (req, res) => {
rooms: await Room.findAll({ raw: true }),
consumables: await Consumable.findAll({ raw: true }),
// 不包含敏感用户信息
users: await User.findAll({
users: await User.findAll({
attributes: ['userId', 'username', 'role', 'createdAt', 'updatedAt'],
raw: true
})
}
raw: true,
}),
},
};
// 写入备份文件
fs.writeFileSync(backupFile, JSON.stringify(backupData, null, 2));
// 更新最后备份时间
const lastBackupSetting = await SystemSetting.findByPk('last_backup_time');
if (lastBackupSetting) {
await lastBackupSetting.update({
settingValue: JSON.stringify(new Date().toISOString())
settingValue: JSON.stringify(new Date().toISOString()),
});
}
// 统计备份文件数量
const backupFiles = fs.readdirSync(backupDir).filter(f => f.startsWith('backup_'));
const countSetting = await SystemSetting.findByPk('backup_count');
if (countSetting) {
await countSetting.update({
settingValue: JSON.stringify(backupFiles.length)
settingValue: JSON.stringify(backupFiles.length),
});
}
res.json({
message: '备份成功',
backupFile: `${backupPath}/backup_${timestamp}.json`,
fileSize: fs.statSync(backupFile).size,
backupCount: backupFiles.length
backupCount: backupFiles.length,
});
} catch (error) {
console.error('备份失败:', error);
@@ -387,7 +553,7 @@ router.post('/backup', async (req, res) => {
const getBackupDir = async () => {
const backupPathSetting = await SystemSetting.findByPk('backup_path');
const backupPath = backupPathSetting ? JSON.parse(backupPathSetting.settingValue) : './backups';
let backupDir;
if (backupPath.startsWith('/') || backupPath.match(/^[A-Za-z]:\//)) {
// 绝对路径
@@ -396,7 +562,7 @@ const getBackupDir = async () => {
// 相对路径,基于项目根目录
backupDir = path.join(__dirname, '../', backupPath);
}
return backupDir;
};
@@ -404,12 +570,13 @@ const getBackupDir = async () => {
router.get('/backup/list', async (req, res) => {
try {
const backupDir = await getBackupDir();
if (!fs.existsSync(backupDir)) {
return res.json({ backups: [] });
}
const files = fs.readdirSync(backupDir)
const files = fs
.readdirSync(backupDir)
.filter(f => f.startsWith('backup_') && f.endsWith('.json'))
.map(f => {
const filePath = path.join(backupDir, f);
@@ -419,11 +586,11 @@ router.get('/backup/list', async (req, res) => {
path: `${path.basename(backupDir)}/${f}`,
size: stats.size,
createdAt: stats.birthtime,
modifiedAt: stats.mtime
modifiedAt: stats.mtime,
};
})
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
res.json({ backups: files });
} catch (error) {
res.status(500).json({ error: error.message });
@@ -434,50 +601,50 @@ router.get('/backup/list', async (req, res) => {
router.post('/backup/restore', async (req, res) => {
try {
const { filename } = req.body;
if (!filename) {
return res.status(400).json({ error: '请提供备份文件名' });
}
const backupDir = await getBackupDir();
const backupFile = path.join(backupDir, filename);
if (!fs.existsSync(backupFile)) {
return res.status(404).json({ error: '备份文件不存在' });
}
const backupData = JSON.parse(fs.readFileSync(backupFile, 'utf8'));
// 恢复数据
const { Device, Rack, Room, Consumable, User } = require('../models');
if (backupData.data.devices) {
for (const device of backupData.data.devices) {
await Device.upsert(device);
}
}
if (backupData.data.racks) {
for (const rack of backupData.data.racks) {
await Rack.upsert(rack);
}
}
if (backupData.data.rooms) {
for (const room of backupData.data.rooms) {
await Room.upsert(room);
}
}
if (backupData.data.consumables) {
for (const consumable of backupData.data.consumables) {
await Consumable.upsert(consumable);
}
}
res.json({
message: '恢复成功',
restoredAt: new Date().toISOString()
restoredAt: new Date().toISOString(),
});
} catch (error) {
console.error('恢复备份失败:', error);
@@ -491,13 +658,13 @@ router.delete('/backup/:filename', async (req, res) => {
const { filename } = req.params;
const backupDir = await getBackupDir();
const backupFile = path.join(backupDir, filename);
if (!fs.existsSync(backupFile)) {
return res.status(404).json({ error: '备份文件不存在' });
}
fs.unlinkSync(backupFile);
res.json({ message: '删除成功', filename });
} catch (error) {
res.status(500).json({ error: error.message });
@@ -510,11 +677,11 @@ router.get('/backup/download/:filename', async (req, res) => {
const { filename } = req.params;
const backupDir = await getBackupDir();
const backupFile = path.join(backupDir, filename);
if (!fs.existsSync(backupFile)) {
return res.status(404).json({ error: '备份文件不存在' });
}
res.download(backupFile, filename);
} catch (error) {
res.status(500).json({ error: error.message });
@@ -528,14 +695,14 @@ router.get('/system/info', async (req, res) => {
const Rack = require('../models/Rack');
const Room = require('../models/Room');
const User = require('../models/User');
const [deviceCount, rackCount, roomCount, userCount] = await Promise.all([
Device.count(),
Rack.count(),
Room.count(),
User.count()
User.count(),
]);
res.json({
system: {
name: '机柜管理系统',
@@ -545,15 +712,15 @@ router.get('/system/info', async (req, res) => {
platform: process.platform,
arch: process.arch,
memoryUsage: process.memoryUsage(),
pid: process.pid
pid: process.pid,
},
statistics: {
devices: deviceCount,
racks: rackCount,
rooms: roomCount,
users: userCount
users: userCount,
},
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -590,7 +757,7 @@ router.post('/frontend/port/sync', async (req, res) => {
message: '前端端口配置已同步',
port,
configPath: '.frontend-port',
notice: '配置已更新,请重启前端服务以应用新端口'
notice: '配置已更新,请重启前端服务以应用新端口',
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -614,8 +781,8 @@ router.post('/frontend/restart', async (req, res) => {
after: {
pid: result.pid,
port: result.port,
url: `http://localhost:${result.port}`
}
url: `http://localhost:${result.port}`,
},
});
} catch (error) {
res.status(500).json({ error: error.message });
+99 -33
View File
@@ -14,7 +14,10 @@ router.get('/', async (req, res) => {
const categories = await FaultCategory.findAll({
where,
order: [['priority', 'ASC'], ['name', 'ASC']]
order: [
['priority', 'ASC'],
['name', 'ASC'],
],
});
res.json(categories);
@@ -31,8 +34,12 @@ router.get('/stats', async (req, res) => {
const where = {};
if (startDate || endDate) {
where.createdAt = {};
if (startDate) where.createdAt[Op.gte] = new Date(startDate);
if (endDate) where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
if (startDate) {
where.createdAt[Op.gte] = new Date(startDate);
}
if (endDate) {
where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
}
}
const stats = await Ticket.findAll({
@@ -40,16 +47,32 @@ router.get('/stats', async (req, res) => {
attributes: [
'faultCategory',
[require('sequelize').fn('COUNT', '*'), 'totalCount'],
[require('sequelize').sum(require('sequelize').case({
when: { status: 'completed' },
then: 1
}, 0)), 'completedCount'],
[require('sequelize').sum(require('sequelize').case({
when: { status: { [Op.ne]: 'completed' } },
then: 1
}, 0)), 'pendingCount']
[
require('sequelize').sum(
require('sequelize').case(
{
when: { status: 'completed' },
then: 1,
},
0
)
),
'completedCount',
],
[
require('sequelize').sum(
require('sequelize').case(
{
when: { status: { [Op.ne]: 'completed' } },
then: 1,
},
0
)
),
'pendingCount',
],
],
group: ['faultCategory']
group: ['faultCategory'],
});
res.json(stats);
@@ -72,15 +95,8 @@ router.get('/:categoryId', async (req, res) => {
router.post('/', async (req, res) => {
try {
const {
name,
description,
priority,
defaultPriority,
expectedDuration,
solutions,
isActive
} = req.body;
const { name, description, priority, defaultPriority, expectedDuration, solutions, isActive } =
req.body;
const existing = await FaultCategory.findOne({ where: { name } });
if (existing) {
@@ -98,7 +114,7 @@ router.post('/', async (req, res) => {
expectedDuration: expectedDuration ? parseInt(expectedDuration) : null,
solutions: solutions || [],
isSystem: false,
isActive: isActive !== false
isActive: isActive !== false,
});
res.status(201).json(category);
@@ -110,16 +126,66 @@ router.post('/', async (req, res) => {
router.post('/init', async (req, res) => {
try {
const defaultCategories = [
{ name: '系统故障', description: '操作系统、应用程序等系统软件的故障问题', priority: 1, defaultPriority: 'high' },
{ name: '硬件故障', description: '物理设备、服务器、存储等硬件设备的故障问题', priority: 2, defaultPriority: 'high' },
{ name: '网络故障', description: '网络连接、交换机、路由器等网络相关故障', priority: 3, defaultPriority: 'high' },
{ name: '软件故障', description: '应用程序错误、软件兼容性等问题', priority: 4, defaultPriority: 'medium' },
{ name: '安全事件', description: '安全漏洞、入侵检测、权限异常等安全问题', priority: 5, defaultPriority: 'urgent' },
{ name: '性能问题', description: '系统响应慢、资源利用率高等性能问题', priority: 6, defaultPriority: 'medium' },
{ name: '配置变更', description: '系统配置、软件配置等变更需求', priority: 7, defaultPriority: 'low' },
{ name: '例行维护', description: '定期维护、巡检、更新等计划性工作', priority: 8, defaultPriority: 'low' },
{ name: '数据问题', description: '数据错误、数据丢失、数据同步等数据相关问题', priority: 9, defaultPriority: 'high' },
{ name: '其他问题', description: '无法归类的其他问题', priority: 99, defaultPriority: 'medium' }
{
name: '系统故障',
description: '操作系统、应用程序等系统软件的故障问题',
priority: 1,
defaultPriority: 'high',
},
{
name: '硬件故障',
description: '物理设备、服务器、存储等硬件设备的故障问题',
priority: 2,
defaultPriority: 'high',
},
{
name: '网络故障',
description: '网络连接、交换机、路由器等网络相关故障',
priority: 3,
defaultPriority: 'high',
},
{
name: '软件故障',
description: '应用程序错误、软件兼容性等问题',
priority: 4,
defaultPriority: 'medium',
},
{
name: '安全事件',
description: '安全漏洞、入侵检测、权限异常等安全问题',
priority: 5,
defaultPriority: 'urgent',
},
{
name: '性能问题',
description: '系统响应慢、资源利用率高等性能问题',
priority: 6,
defaultPriority: 'medium',
},
{
name: '配置变更',
description: '系统配置、软件配置等变更需求',
priority: 7,
defaultPriority: 'low',
},
{
name: '例行维护',
description: '定期维护、巡检、更新等计划性工作',
priority: 8,
defaultPriority: 'low',
},
{
name: '数据问题',
description: '数据错误、数据丢失、数据同步等数据相关问题',
priority: 9,
defaultPriority: 'high',
},
{
name: '其他问题',
description: '无法归类的其他问题',
priority: 99,
defaultPriority: 'medium',
},
];
for (const cat of defaultCategories) {
@@ -132,7 +198,7 @@ router.post('/init', async (req, res) => {
expectedDuration: 120,
solutions: [],
isSystem: true,
isActive: true
isActive: true,
});
}
}
+7 -7
View File
@@ -5,7 +5,7 @@ const TicketField = require('../models/TicketField');
router.get('/', async (req, res) => {
try {
const fields = await TicketField.findAll({
order: [['order', 'ASC']]
order: [['order', 'ASC']],
});
res.json(fields);
} catch (error) {
@@ -37,7 +37,7 @@ router.post('/', async (req, res) => {
router.put('/:fieldId', async (req, res) => {
try {
const [updated] = await TicketField.update(req.body, {
where: { fieldId: req.params.fieldId }
where: { fieldId: req.params.fieldId },
});
if (updated) {
const updatedField = await TicketField.findByPk(req.params.fieldId);
@@ -53,7 +53,7 @@ router.put('/:fieldId', async (req, res) => {
router.delete('/:fieldId', async (req, res) => {
try {
const deleted = await TicketField.destroy({
where: { fieldId: req.params.fieldId }
where: { fieldId: req.params.fieldId },
});
if (deleted) {
res.status(204).json();
@@ -68,24 +68,24 @@ router.delete('/:fieldId', async (req, res) => {
router.post('/config', async (req, res) => {
try {
const fieldConfigs = req.body;
if (!Array.isArray(fieldConfigs)) {
return res.status(400).json({ error: '输入必须是数组' });
}
const updatedFields = [];
for (const config of fieldConfigs) {
const [updated] = await TicketField.update(
{ visible: config.visible },
{ where: { fieldName: config.fieldName } }
);
if (updated) {
const updatedField = await TicketField.findOne({ where: { fieldName: config.fieldName } });
updatedFields.push(updatedField);
}
}
res.json(updatedFields);
} catch (error) {
res.status(500).json({ error: error.message });
+90 -62
View File
@@ -17,41 +17,56 @@ router.get('/stats', async (req, res) => {
const where = {};
if (startDate || endDate) {
where.createdAt = {};
if (startDate) where.createdAt[Op.gte] = new Date(startDate);
if (endDate) where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
if (startDate) {
where.createdAt[Op.gte] = new Date(startDate);
}
if (endDate) {
where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
}
}
const Sequelize = require('sequelize');
const [total, statusStats, priorityStats, categoryStats, monthlyStats, deviceStats, dailyCreatedStats, dailyCompletedStats] = await Promise.all([
const [
total,
statusStats,
priorityStats,
categoryStats,
monthlyStats,
deviceStats,
dailyCreatedStats,
dailyCompletedStats,
] = await Promise.all([
Ticket.count({ where }),
Ticket.findAll({
where,
attributes: ['status', [Sequelize.fn('COUNT', '*'), 'count']],
group: ['status']
group: ['status'],
}),
Ticket.findAll({
where,
attributes: ['priority', [Sequelize.fn('COUNT', '*'), 'count']],
group: ['priority']
group: ['priority'],
}),
Ticket.findAll({
where,
attributes: ['faultCategory', [Sequelize.fn('COUNT', '*'), 'count']],
group: ['faultCategory']
group: ['faultCategory'],
}),
Ticket.findAll({
where,
attributes: [
[dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m')
: Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')),
'month'],
[Sequelize.fn('COUNT', '*'), 'count']
[
dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m')
: Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')),
'month',
],
[Sequelize.fn('COUNT', '*'), 'count'],
],
group: ['month'],
order: [['month', 'DESC']],
limit: 12
limit: 12,
}),
Ticket.findAll({
where,
@@ -59,39 +74,43 @@ router.get('/stats', async (req, res) => {
'deviceId',
'deviceName',
[Sequelize.fn('COUNT', '*'), 'count'],
[Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime']
[Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime'],
],
group: ['deviceId', 'deviceName'],
order: [[Sequelize.fn('COUNT', '*'), 'DESC']],
limit: 10
limit: 10,
}),
Ticket.findAll({
where,
attributes: [
[dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m-%d')
: Sequelize.fn('date', Sequelize.col('createdAt')),
'date'],
[Sequelize.fn('COUNT', '*'), 'created']
[
dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m-%d')
: Sequelize.fn('date', Sequelize.col('createdAt')),
'date',
],
[Sequelize.fn('COUNT', '*'), 'created'],
],
group: ['date'],
order: [['date', 'ASC']]
order: [['date', 'ASC']],
}),
Ticket.findAll({
where: {
...where,
status: 'completed'
status: 'completed',
},
attributes: [
[dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('updatedAt'), '%Y-%m-%d')
: Sequelize.fn('date', Sequelize.col('updatedAt')),
'date'],
[Sequelize.fn('COUNT', '*'), 'completed']
[
dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('updatedAt'), '%Y-%m-%d')
: Sequelize.fn('date', Sequelize.col('updatedAt')),
'date',
],
[Sequelize.fn('COUNT', '*'), 'completed'],
],
group: ['date'],
order: [['date', 'ASC']]
})
order: [['date', 'ASC']],
}),
]);
const statusData = statusStats.map(s => s.dataValues);
@@ -103,21 +122,21 @@ router.get('/stats', async (req, res) => {
const byStatus = statusData.map(item => ({
status: item.status,
count: item.count,
percentage: total > 0 ? (item.count / total * 100) : 0
percentage: total > 0 ? (item.count / total) * 100 : 0,
}));
const byPriority = priorityStats.map(p => ({
priority: p.dataValues.priority,
count: p.dataValues.count,
completed: 0,
avgTime: 0
avgTime: 0,
}));
const byCategory = categoryStats.map(c => ({
category: c.dataValues.faultCategory,
count: c.dataValues.count,
completed: 0,
avgTime: 0
avgTime: 0,
}));
const byDevice = deviceStats.map(d => ({
@@ -125,7 +144,7 @@ router.get('/stats', async (req, res) => {
deviceName: d.deviceName,
count: d.dataValues.count,
lastFaultTime: d.dataValues.lastFaultTime,
deviceType: ''
deviceType: '',
}));
const createdMap = {};
@@ -137,22 +156,24 @@ router.get('/stats', async (req, res) => {
completedMap[d.dataValues.date] = d.dataValues.completed;
});
const allDates = [...new Set([...Object.keys(createdMap), ...Object.keys(completedMap)])].sort();
const allDates = [
...new Set([...Object.keys(createdMap), ...Object.keys(completedMap)]),
].sort();
const trend = allDates.map(date => ({
date,
created: createdMap[date] || 0,
completed: completedMap[date] || 0,
closed: 0,
inProgress: 0,
pending: 0
pending: 0,
}));
const completedTickets = await Ticket.findAll({
where: {
...where,
status: 'completed'
status: 'completed',
},
attributes: ['createdAt', 'updatedAt']
attributes: ['createdAt', 'updatedAt'],
});
let avgProcessingTime = 0;
@@ -162,7 +183,11 @@ router.get('/stats', async (req, res) => {
const updated = new Date(ticket.updatedAt);
return sum + (updated - created);
}, 0);
avgProcessingTime = (totalProcessingTime / completedTickets.length / (1000 * 60 * 60)).toFixed(1);
avgProcessingTime = (
totalProcessingTime /
completedTickets.length /
(1000 * 60 * 60)
).toFixed(1);
}
res.json({
@@ -177,7 +202,10 @@ router.get('/stats', async (req, res) => {
byCategory,
byDevice,
trend,
monthlyStats: monthlyStats.map(m => ({ month: m.dataValues.month, count: m.dataValues.count }))
monthlyStats: monthlyStats.map(m => ({
month: m.dataValues.month,
count: m.dataValues.count,
})),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -203,7 +231,7 @@ router.get('/', async (req, res) => {
startDate,
endDate,
page = 1,
pageSize = 10
pageSize = 10,
} = req.query;
const offset = (page - 1) * pageSize;
@@ -216,7 +244,7 @@ router.get('/', async (req, res) => {
{ title: { [Op.like]: `%${keyword}%` } },
{ deviceName: { [Op.like]: `%${keyword}%` } },
{ serialNumber: { [Op.like]: `%${keyword}%` } },
{ description: { [Op.like]: `%${keyword}%` } }
{ description: { [Op.like]: `%${keyword}%` } },
];
}
@@ -260,18 +288,18 @@ router.get('/', async (req, res) => {
where,
include: [
{ model: User, as: 'reporter', attributes: ['userId', 'username'] },
{ model: Device, attributes: ['deviceId', 'name', 'type', 'model'] }
{ model: Device, attributes: ['deviceId', 'name', 'type', 'model'] },
],
order: [['createdAt', 'DESC']],
offset,
limit: parseInt(pageSize)
limit: parseInt(pageSize),
});
res.json({
total: count,
tickets: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -293,14 +321,14 @@ router.get('/:ticketId', async (req, res) => {
include: [
{
model: Room,
attributes: ['roomId', 'name']
}
]
}
]
attributes: ['roomId', 'name'],
},
],
},
],
},
{ model: TicketOperationRecord, as: 'operationRecords', order: [['createdAt', 'DESC']] }
]
{ model: TicketOperationRecord, as: 'operationRecords', order: [['createdAt', 'DESC']] },
],
});
if (!ticket) {
@@ -334,7 +362,7 @@ router.post('/', async (req, res) => {
expectedCompletionDate,
title,
attachments,
tags
tags,
} = req.body;
let device = null;
@@ -347,7 +375,7 @@ router.post('/', async (req, res) => {
if (deviceId) {
// 从设备列表选择
device = await Device.findByPk(deviceId, {
include: [{ model: require('../models/Rack') }]
include: [{ model: require('../models/Rack') }],
});
if (!device) {
return res.status(404).json({ error: '设备不存在' });
@@ -385,7 +413,7 @@ router.post('/', async (req, res) => {
location: ticketLocation,
attachments: attachments || [],
tags: tags || [],
status: 'pending'
status: 'pending',
});
// 创建操作记录
@@ -397,7 +425,7 @@ router.post('/', async (req, res) => {
operatorId: ticket.reporterId,
operatorName: ticket.reporterName,
operatorRole: 'user',
afterState: ticket.toJSON()
afterState: ticket.toJSON(),
});
res.status(201).json(ticket);
@@ -428,7 +456,7 @@ router.put('/:ticketId', async (req, res) => {
operatorName: operatorName || ticket.reporterName,
operatorRole: operatorRole || 'user',
beforeState,
afterState: ticket.toJSON()
afterState: ticket.toJSON(),
});
res.json(ticket);
@@ -468,7 +496,7 @@ router.put('/:ticketId/status', async (req, res) => {
operatorName: operatorName || ticket.reporterName,
operatorRole: operatorRole || 'user',
beforeState,
afterState: ticket.toJSON()
afterState: ticket.toJSON(),
});
res.json(ticket);
@@ -490,7 +518,7 @@ router.put('/:ticketId/process', async (req, res) => {
const beforeState = ticket.toJSON();
const updateData = {
status: 'in_progress',
resolution: solution
resolution: solution,
};
if (result === 'resolved') {
@@ -513,7 +541,7 @@ router.put('/:ticketId/process', async (req, res) => {
operatorName: operatorName || ticket.reporterName,
operatorRole: operatorRole || 'user',
beforeState,
afterState: ticket.toJSON()
afterState: ticket.toJSON(),
});
res.json(ticket);
@@ -535,7 +563,7 @@ router.post('/:ticketId/operations', async (req, res) => {
notes,
operatorId,
operatorName,
operatorRole
operatorRole,
} = req.body;
const ticket = await Ticket.findByPk(req.params.ticketId);
@@ -555,7 +583,7 @@ router.post('/:ticketId/operations', async (req, res) => {
notes,
operatorId,
operatorName,
operatorRole
operatorRole,
});
res.status(201).json(record);
@@ -569,7 +597,7 @@ router.get('/:ticketId/operations', async (req, res) => {
try {
const records = await TicketOperationRecord.findAll({
where: { ticketId: req.params.ticketId },
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
res.json(records);
@@ -613,7 +641,7 @@ router.post('/:ticketId/evaluate', async (req, res) => {
operatorId,
operatorName,
operatorRole: 'user',
notes: `评价: ${evaluation}, 星级: ${evaluationRating}`
notes: `评价: ${evaluation}, 星级: ${evaluationRating}`,
});
res.json(ticket);
+148 -111
View File
@@ -15,7 +15,7 @@ const generateId = () => {
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
};
const getWhereClause = (query) => {
const getWhereClause = query => {
const where = {};
if (query.username) {
@@ -33,10 +33,10 @@ const getWhereClause = (query) => {
return where;
};
const getUserRoleIds = async (userId) => {
const getUserRoleIds = async userId => {
const userRoles = await UserRole.findAll({
where: { UserId: userId },
attributes: ['RoleId']
attributes: ['RoleId'],
});
return userRoles.map(ur => ur.RoleId);
};
@@ -45,7 +45,13 @@ const { Op } = require('sequelize');
router.get('/', authMiddleware, async (req, res) => {
try {
const { page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE, username, status, realName } = req.query;
const {
page = 1,
pageSize = PAGINATION.DEFAULT_PAGE_SIZE,
username,
status,
realName,
} = req.query;
const offset = (parseInt(page) - 1) * parseInt(pageSize);
const limit = Math.min(parseInt(pageSize), PAGINATION.MAX_PAGE_SIZE);
@@ -56,20 +62,22 @@ router.get('/', authMiddleware, async (req, res) => {
attributes: { exclude: ['password'] },
limit,
offset,
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
if (users.length > 0) {
const userIds = users.map(u => u.userId);
const allUserRoles = await UserRole.findAll({
include: [{
model: Role,
where: { status: 'active' },
attributes: ['roleId', 'roleName', 'roleCode']
}],
include: [
{
model: Role,
where: { status: 'active' },
attributes: ['roleId', 'roleName', 'roleCode'],
},
],
where: {
UserId: { [Op.in]: userIds }
}
UserId: { [Op.in]: userIds },
},
});
const userRolesMap = {};
@@ -80,7 +88,7 @@ router.get('/', authMiddleware, async (req, res) => {
userRolesMap[ur.UserId].push({
roleId: ur.Role.roleId,
roleName: ur.Role.roleName,
roleCode: ur.Role.roleCode
roleCode: ur.Role.roleCode,
});
});
@@ -99,14 +107,14 @@ router.get('/', authMiddleware, async (req, res) => {
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize),
users
}
users,
},
});
} catch (error) {
console.error('获取用户列表错误:', error);
res.status(500).json({
success: false,
message: '获取用户列表失败'
message: '获取用户列表失败',
});
}
});
@@ -116,18 +124,18 @@ router.get('/all', authMiddleware, async (req, res) => {
const users = await User.findAll({
where: { status: 'active' },
attributes: ['userId', 'username', 'realName', 'email'],
order: [['realName', 'ASC']]
order: [['realName', 'ASC']],
});
res.json({
success: true,
data: users
data: users,
});
} catch (error) {
console.error('获取所有用户错误:', error);
res.status(500).json({
success: false,
message: '获取用户列表失败'
message: '获取用户列表失败',
});
}
});
@@ -135,39 +143,41 @@ router.get('/all', authMiddleware, async (req, res) => {
router.get('/:userId', authMiddleware, async (req, res) => {
try {
const user = await User.findByPk(req.params.userId, {
attributes: { exclude: ['password'] }
attributes: { exclude: ['password'] },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
const userRoles = await UserRole.findAll({
include: [{
model: Role,
where: { status: 'active' }
}],
where: { UserId: user.userId }
include: [
{
model: Role,
where: { status: 'active' },
},
],
where: { UserId: user.userId },
});
user.dataValues.roles = userRoles.map(ur => ({
roleId: ur.Role.roleId,
roleName: ur.Role.roleName,
roleCode: ur.Role.roleCode
roleCode: ur.Role.roleCode,
}));
res.json({
success: true,
data: user
data: user,
});
} catch (error) {
console.error('获取用户详情错误:', error);
res.status(500).json({
success: false,
message: '获取用户详情失败'
message: '获取用户详情失败',
});
}
});
@@ -179,7 +189,7 @@ router.post('/', authMiddleware, async (req, res) => {
if (!username || !password) {
return res.status(400).json({
success: false,
message: '用户名和密码不能为空'
message: '用户名和密码不能为空',
});
}
@@ -187,7 +197,7 @@ router.post('/', authMiddleware, async (req, res) => {
if (existingUser) {
return res.status(400).json({
success: false,
message: '用户名已存在'
message: '用户名已存在',
});
}
@@ -201,34 +211,41 @@ router.post('/', authMiddleware, async (req, res) => {
phone,
realName: realName || username,
status: status || 'active',
remark
remark,
});
if (roleIds && roleIds.length > 0) {
for (const roleId of roleIds) {
await UserRole.create({
UserId: user.userId,
RoleId: roleId
RoleId: roleId,
});
}
}
const roleNames = roleIds && roleIds.length > 0
? (await Role.findAll({ where: { roleId: { [Op.in]: roleIds } } })).map(r => r.roleName).join('、')
: '未分配角色';
const roleNames =
roleIds && roleIds.length > 0
? (await Role.findAll({ where: { roleId: { [Op.in]: roleIds } } }))
.map(r => r.roleName)
.join('、')
: '未分配角色';
await logUserOperation('create', `创建用户【${username}】(姓名:${realName || '未填写'},邮箱:${email || '未填写'},角色:${roleNames}`, {
targetId: user.userId,
targetName: username,
afterState: {
username: user.username,
email: user.email,
realName: user.realName,
status: user.status
},
req,
metadata: { roleIds, roleNames }
});
await logUserOperation(
'create',
`创建用户【${username}】(姓名:${realName || '未填写'},邮箱:${email || '未填写'},角色:${roleNames}`,
{
targetId: user.userId,
targetName: username,
afterState: {
username: user.username,
email: user.email,
realName: user.realName,
status: user.status,
},
req,
metadata: { roleIds, roleNames },
}
);
res.status(201).json({
success: true,
@@ -238,14 +255,14 @@ router.post('/', authMiddleware, async (req, res) => {
username: user.username,
email: user.email,
realName: user.realName,
status: user.status
}
status: user.status,
},
});
} catch (error) {
console.error('创建用户错误:', error);
res.status(500).json({
success: false,
message: '创建用户失败'
message: '创建用户失败',
});
}
});
@@ -258,7 +275,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
@@ -268,29 +285,39 @@ router.put('/:userId', authMiddleware, async (req, res) => {
phone: user.phone,
realName: user.realName,
status: user.status,
remark: user.remark
remark: user.remark,
};
const oldRoleIds = roleIds !== undefined ? null : await getUserRoleIds(user.userId);
if (username !== undefined && username !== user.username) {
const existingUser = await User.findOne({
where: { username, userId: { [Op.ne]: user.userId } }
where: { username, userId: { [Op.ne]: user.userId } },
});
if (existingUser) {
return res.status(400).json({
success: false,
message: '用户名已存在'
message: '用户名已存在',
});
}
user.username = username;
}
if (email !== undefined) user.email = email;
if (phone !== undefined) user.phone = phone;
if (realName !== undefined) user.realName = realName;
if (status !== undefined) user.status = status;
if (remark !== undefined) user.remark = remark;
if (email !== undefined) {
user.email = email;
}
if (phone !== undefined) {
user.phone = phone;
}
if (realName !== undefined) {
user.realName = realName;
}
if (status !== undefined) {
user.status = status;
}
if (remark !== undefined) {
user.remark = remark;
}
if (newPassword && newPassword.length >= PASSWORD_MIN_LENGTH) {
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
@@ -311,7 +338,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
for (const roleId of roleIds) {
await UserRole.create({
UserId: user.userId,
RoleId: roleId
RoleId: roleId,
});
}
@@ -321,7 +348,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
}
const updatedUser = await User.findByPk(req.params.userId, {
attributes: { exclude: ['password'] }
attributes: { exclude: ['password'] },
});
if (permissionChanged) {
@@ -332,7 +359,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
beforeState: { ...beforeState, roleIds: oldRoleIds, roleNames: oldRoleNames },
afterState: { ...beforeState, roleIds, roleNames: newRoleNames },
req,
metadata: { oldRoleIds, newRoleIds: roleIds, oldRoleNames, newRoleNames }
metadata: { oldRoleIds, newRoleIds: roleIds, oldRoleNames, newRoleNames },
});
} else {
const afterState = {
@@ -341,7 +368,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
phone: updatedUser.phone,
realName: updatedUser.realName,
status: updatedUser.status,
remark: updatedUser.remark
remark: updatedUser.remark,
};
const changedFields = {};
@@ -351,14 +378,20 @@ router.put('/:userId', authMiddleware, async (req, res) => {
}
}
const changeDetails = Object.entries(changedFields).map(([field, values]) => {
const fieldNames = {
username: '用户名', email: '邮箱', phone: '电话', realName: '姓名',
status: '状态', remark: '备注'
};
const displayName = fieldNames[field] || field;
return `${displayName}: ${values.from ?? '空'}${values.to ?? '空'}`;
}).join('');
const changeDetails = Object.entries(changedFields)
.map(([field, values]) => {
const fieldNames = {
username: '用户名',
email: '邮箱',
phone: '电话',
realName: '姓名',
status: '状态',
remark: '备注',
};
const displayName = fieldNames[field] || field;
return `${displayName}: ${values.from ?? '空'}${values.to ?? '空'}`;
})
.join('');
const updateDesc = changeDetails
? `更新用户【${updatedUser.username}】:${changeDetails}`
@@ -370,20 +403,20 @@ router.put('/:userId', authMiddleware, async (req, res) => {
beforeState,
afterState,
req,
metadata: { changedFields }
metadata: { changedFields },
});
}
res.json({
success: true,
message: '更新成功',
data: updatedUser
data: updatedUser,
});
} catch (error) {
console.error('更新用户错误:', error);
res.status(500).json({
success: false,
message: '更新用户失败'
message: '更新用户失败',
});
}
});
@@ -396,14 +429,14 @@ router.put('/:userId/password', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (!newPassword || newPassword.length < PASSWORD_MIN_LENGTH) {
return res.status(400).json({
success: false,
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
});
}
@@ -412,13 +445,13 @@ router.put('/:userId/password', authMiddleware, async (req, res) => {
res.json({
success: true,
message: '密码重置成功'
message: '密码重置成功',
});
} catch (error) {
console.error('重置密码错误:', error);
res.status(500).json({
success: false,
message: '重置密码失败'
message: '重置密码失败',
});
}
});
@@ -430,14 +463,14 @@ router.delete('/:userId', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (user.userId === req.user.userId) {
return res.status(400).json({
success: false,
message: '不能删除当前登录用户'
message: '不能删除当前登录用户',
});
}
@@ -448,29 +481,33 @@ router.delete('/:userId', authMiddleware, async (req, res) => {
username: user.username,
email: user.email,
realName: user.realName,
status: user.status
status: user.status,
};
await UserRole.destroy({ where: { UserId: user.userId } });
await user.destroy();
await logUserOperation('delete', `删除用户【${userName}】(姓名:${userRealName || '未填写'},邮箱:${userEmail || '未填写'}`, {
targetId: req.params.userId,
targetName: userName,
beforeState,
req,
metadata: { deletedUsername: userName, realName: userRealName, email: userEmail }
});
await logUserOperation(
'delete',
`删除用户【${userName}】(姓名:${userRealName || '未填写'},邮箱:${userEmail || '未填写'}`,
{
targetId: req.params.userId,
targetName: userName,
beforeState,
req,
metadata: { deletedUsername: userName, realName: userRealName, email: userEmail },
}
);
res.json({
success: true,
message: '删除成功'
message: '删除成功',
});
} catch (error) {
console.error('删除用户错误:', error);
res.status(500).json({
success: false,
message: '删除用户失败'
message: '删除用户失败',
});
}
});
@@ -482,31 +519,31 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (!req.files || !req.files.avatar) {
return res.status(400).json({
success: false,
message: '请选择要上传的头像文件'
message: '请选择要上传的头像文件',
});
}
const avatarFile = req.files.avatar;
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(avatarFile.mimetype)) {
return res.status(400).json({
success: false,
message: '只支持 JPG、PNG、GIF 和 WebP 格式的图片'
message: '只支持 JPG、PNG、GIF 和 WebP 格式的图片',
});
}
if (avatarFile.size > FILE_UPLOAD.MAX_AVATAR_SIZE) {
return res.status(400).json({
success: false,
message: `图片大小不能超过 ${FILE_UPLOAD.MAX_AVATAR_SIZE / 1024 / 1024}MB`
message: `图片大小不能超过 ${FILE_UPLOAD.MAX_AVATAR_SIZE / 1024 / 1024}MB`,
});
}
@@ -535,13 +572,13 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => {
res.json({
success: true,
message: '头像上传成功',
data: { avatar: avatarUrl }
data: { avatar: avatarUrl },
});
} catch (error) {
console.error('上传头像错误:', error);
res.status(500).json({
success: false,
message: '上传头像失败'
message: '上传头像失败',
});
}
});
@@ -553,7 +590,7 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
@@ -569,13 +606,13 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
res.json({
success: true,
message: '头像删除成功'
message: '头像删除成功',
});
} catch (error) {
console.error('删除头像错误:', error);
res.status(500).json({
success: false,
message: '删除头像失败'
message: '删除头像失败',
});
}
});
@@ -587,14 +624,14 @@ router.put('/:userId/approve', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (user.status !== 'pending') {
return res.status(400).json({
success: false,
message: '该用户不在待审核状态'
message: '该用户不在待审核状态',
});
}
@@ -607,14 +644,14 @@ router.put('/:userId/approve', authMiddleware, async (req, res) => {
data: {
userId: user.userId,
username: user.username,
status: user.status
}
status: user.status,
},
});
} catch (error) {
console.error('审核用户错误:', error);
res.status(500).json({
success: false,
message: '审核用户失败'
message: '审核用户失败',
});
}
});
@@ -626,14 +663,14 @@ router.put('/:userId/reject', authMiddleware, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
message: '用户不存在',
});
}
if (user.status !== 'pending') {
return res.status(400).json({
success: false,
message: '该用户不在待审核状态'
message: '该用户不在待审核状态',
});
}
@@ -646,14 +683,14 @@ router.put('/:userId/reject', authMiddleware, async (req, res) => {
data: {
userId: user.userId,
username: user.username,
status: user.status
}
status: user.status,
},
});
} catch (error) {
console.error('拒绝用户错误:', error);
res.status(500).json({
success: false,
message: '操作失败'
message: '操作失败',
});
}
});
+33 -23
View File
@@ -8,8 +8,8 @@ const { logDeviceOperation } = require('../utils/operationLogger');
async function generateWarehouseId() {
const warehouses = await Warehouse.findAll({
where: {
warehouseId: { [Op.like]: 'WH%' }
}
warehouseId: { [Op.like]: 'WH%' },
},
});
let maxNumber = 0;
@@ -38,7 +38,7 @@ router.get('/', async (req, res) => {
where[Op.or] = [
{ warehouseId: { [Op.like]: `%${keyword}%` } },
{ name: { [Op.like]: `%${keyword}%` } },
{ location: { [Op.like]: `%${keyword}%` } }
{ location: { [Op.like]: `%${keyword}%` } },
];
}
@@ -50,17 +50,17 @@ router.get('/', async (req, res) => {
where,
offset: parseInt(offset),
limit: parseInt(pageSize),
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
const warehousesWithCount = await Promise.all(
rows.map(async (warehouse) => {
rows.map(async warehouse => {
const deviceCount = await Device.count({
where: { warehouseId: warehouse.warehouseId, isIdle: true }
where: { warehouseId: warehouse.warehouseId, isIdle: true },
});
return {
...warehouse.toJSON(),
deviceCount
deviceCount,
};
})
);
@@ -69,7 +69,7 @@ router.get('/', async (req, res) => {
total: count,
warehouses: warehousesWithCount,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取库房列表失败:', error);
@@ -85,12 +85,12 @@ router.get('/:warehouseId', async (req, res) => {
}
const deviceCount = await Device.count({
where: { warehouseId: warehouse.warehouseId, isIdle: true }
where: { warehouseId: warehouse.warehouseId, isIdle: true },
});
res.json({
...warehouse.toJSON(),
deviceCount
deviceCount,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -111,14 +111,14 @@ router.get('/:warehouseId/devices', async (req, res) => {
where: { warehouseId: req.params.warehouseId, isIdle: true },
offset: parseInt(offset),
limit: parseInt(pageSize),
order: [['idleDate', 'DESC']]
order: [['idleDate', 'DESC']],
});
res.json({
total: count,
devices: rows,
page: parseInt(page),
pageSize: parseInt(pageSize)
pageSize: parseInt(pageSize),
});
} catch (error) {
console.error('获取库房设备失败:', error);
@@ -142,7 +142,7 @@ router.post('/', async (req, res) => {
location: location || '',
capacity: capacity || 100,
status: 'active',
description: description || ''
description: description || '',
});
await logDeviceOperation('create', `创建库房【${name}`, {
@@ -150,7 +150,7 @@ router.post('/', async (req, res) => {
targetName: name,
afterState: warehouse.toJSON(),
req,
metadata: { type: 'warehouse_create' }
metadata: { type: 'warehouse_create' },
});
res.status(201).json(warehouse);
@@ -169,11 +169,21 @@ router.put('/:warehouseId', async (req, res) => {
const beforeState = warehouse.toJSON();
const { name, location, capacity, status, description } = req.body;
if (name) warehouse.name = name;
if (location !== undefined) warehouse.location = location;
if (capacity !== undefined) warehouse.capacity = capacity;
if (status) warehouse.status = status;
if (description !== undefined) warehouse.description = description;
if (name) {
warehouse.name = name;
}
if (location !== undefined) {
warehouse.location = location;
}
if (capacity !== undefined) {
warehouse.capacity = capacity;
}
if (status) {
warehouse.status = status;
}
if (description !== undefined) {
warehouse.description = description;
}
await warehouse.save();
@@ -183,7 +193,7 @@ router.put('/:warehouseId', async (req, res) => {
beforeState,
afterState: warehouse.toJSON(),
req,
metadata: { type: 'warehouse_update' }
metadata: { type: 'warehouse_update' },
});
res.json(warehouse);
@@ -200,12 +210,12 @@ router.delete('/:warehouseId', async (req, res) => {
}
const idleDeviceCount = await Device.count({
where: { warehouseId: req.params.warehouseId, isIdle: true }
where: { warehouseId: req.params.warehouseId, isIdle: true },
});
if (idleDeviceCount > 0) {
return res.status(400).json({
error: `库房中还有 ${idleDeviceCount} 台空闲设备,请先处理后再删除`
error: `库房中还有 ${idleDeviceCount} 台空闲设备,请先处理后再删除`,
});
}
@@ -216,7 +226,7 @@ router.delete('/:warehouseId', async (req, res) => {
targetId: req.params.warehouseId,
targetName: warehouseName,
req,
metadata: { type: 'warehouse_delete' }
metadata: { type: 'warehouse_delete' },
});
res.json({ message: '库房删除成功' });
+26 -18
View File
@@ -3,34 +3,42 @@ const { sequelize } = require('../db');
async function addIsSystemColumn() {
try {
console.log('开始添加 isSystem 列到 deviceFields 表...');
// 检查列是否已存在
const tableInfo = await sequelize.query(
"PRAGMA table_info(deviceFields)",
{ type: sequelize.QueryTypes.SELECT }
);
const tableInfo = await sequelize.query('PRAGMA table_info(deviceFields)', {
type: sequelize.QueryTypes.SELECT,
});
const hasIsSystemColumn = tableInfo.some(col => col.name === 'isSystem');
if (hasIsSystemColumn) {
console.log('isSystem 列已存在,跳过添加');
} else {
// 添加 isSystem 列
await sequelize.query(
"ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0",
{ type: sequelize.QueryTypes.RAW }
);
await sequelize.query('ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0', {
type: sequelize.QueryTypes.RAW,
});
console.log('isSystem 列添加成功');
}
// 更新现有数据:将核心字段标记为系统字段
const systemFields = [
'deviceId', 'name', 'type', 'model', 'serialNumber',
'rackId', 'position', 'height', 'powerConsumption',
'status', 'purchaseDate', 'warrantyExpiry',
'ipAddress', 'description'
'deviceId',
'name',
'type',
'model',
'serialNumber',
'rackId',
'position',
'height',
'powerConsumption',
'status',
'purchaseDate',
'warrantyExpiry',
'ipAddress',
'description',
];
for (const fieldName of systemFields) {
await sequelize.query(
`UPDATE deviceFields SET isSystem = 1 WHERE fieldName = '${fieldName}'`,
@@ -38,7 +46,7 @@ async function addIsSystemColumn() {
);
}
console.log('系统字段标记完成');
console.log('数据库迁移完成');
process.exit(0);
} catch (error) {
@@ -16,61 +16,61 @@ async function migrate() {
userId: {
type: sequelize.Sequelize.STRING,
primaryKey: true,
allowNull: false
allowNull: false,
},
username: {
type: sequelize.Sequelize.STRING,
allowNull: false,
unique: true
unique: true,
},
password: {
type: sequelize.Sequelize.STRING,
allowNull: false
allowNull: false,
},
email: {
type: sequelize.Sequelize.STRING,
allowNull: true
allowNull: true,
},
phone: {
type: sequelize.Sequelize.STRING,
allowNull: true
allowNull: true,
},
realName: {
type: sequelize.Sequelize.STRING,
allowNull: true
allowNull: true,
},
avatar: {
type: sequelize.Sequelize.STRING,
allowNull: true
allowNull: true,
},
status: {
type: sequelize.Sequelize.ENUM('active', 'inactive', 'locked', 'pending'),
defaultValue: 'active'
defaultValue: 'active',
},
lastLoginTime: {
type: sequelize.Sequelize.DATE,
allowNull: true
allowNull: true,
},
lastLoginIp: {
type: sequelize.Sequelize.STRING,
allowNull: true
allowNull: true,
},
loginCount: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
defaultValue: 0,
},
remark: {
type: sequelize.Sequelize.TEXT,
allowNull: true
allowNull: true,
},
createdAt: {
type: sequelize.Sequelize.DATE,
allowNull: false
allowNull: false,
},
updatedAt: {
type: sequelize.Sequelize.DATE,
allowNull: false
}
allowNull: false,
},
});
// 2. 复制数据
@@ -104,14 +104,12 @@ async function migrateSQLite() {
{ name: 'idx_archive_consumable_id', fields: 'consumableId' },
{ name: 'idx_archive_archive_id', fields: 'archiveId' },
{ name: 'idx_archive_deleted_at', fields: 'deletedAt' },
{ name: 'idx_archive_consumable_deleted', fields: 'consumableId, deletedAt' }
{ name: 'idx_archive_consumable_deleted', fields: 'consumableId, deletedAt' },
];
for (const idx of indexes) {
try {
await sequelize.query(
`CREATE INDEX ${idx.name} ON consumable_log_archives(${idx.fields})`
);
await sequelize.query(`CREATE INDEX ${idx.name} ON consumable_log_archives(${idx.fields})`);
console.log(`✓ 创建索引: ${idx.name}`);
} catch (err) {
console.log(`! 创建索引失败: ${idx.name}`, err.message);
@@ -120,7 +120,17 @@ async function updateExistingLogs() {
try {
const consumables = await Consumable.findAll({
attributes: ['consumableId', 'category', 'unit', 'unitPrice', 'supplier', 'location', 'minStock', 'maxStock', 'status']
attributes: [
'consumableId',
'category',
'unit',
'unitPrice',
'supplier',
'location',
'minStock',
'maxStock',
'status',
],
});
const consumableMap = new Map();
@@ -133,14 +143,14 @@ async function updateExistingLogs() {
location: c.location,
minStock: c.minStock,
maxStock: c.maxStock,
status: c.status
status: c.status,
});
});
const logs = await ConsumableLog.findAll({
where: {
consumableSnapshot: null
}
consumableSnapshot: null,
},
});
let updatedCount = 0;
@@ -155,10 +165,9 @@ async function updateExistingLogs() {
console.log(`✓ 更新了 ${updatedCount} 条日志的快照信息`);
const deletedLogsCount = await ConsumableLog.count({
where: { operationType: 'delete' }
where: { operationType: 'delete' },
});
console.log(`✓ 当前有 ${deletedLogsCount} 条删除类型日志`);
} catch (error) {
console.log('! 更新现有日志数据时出错:', error.message);
}
@@ -16,18 +16,15 @@ async function migrate() {
if (actualDbType === 'sqlite') {
// SQLite: 检查字段是否存在
const tableInfo = await sequelize.query(
"PRAGMA table_info(consumables)",
{ type: sequelize.QueryTypes.SELECT }
);
const tableInfo = await sequelize.query('PRAGMA table_info(consumables)', {
type: sequelize.QueryTypes.SELECT,
});
const hasVersion = tableInfo.some(col => col.name === 'version');
if (!hasVersion) {
console.log('添加 version 字段...');
await sequelize.query(
"ALTER TABLE consumables ADD COLUMN version INTEGER DEFAULT 0"
);
await sequelize.query('ALTER TABLE consumables ADD COLUMN version INTEGER DEFAULT 0');
console.log('version 字段添加成功');
} else {
console.log('version 字段已存在,跳过');
@@ -38,19 +35,16 @@ async function migrate() {
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='consumables'",
{ type: sequelize.QueryTypes.SELECT }
);
const hasUpdatedAtIndex = indexes.some(idx => idx.name === 'consumables_updatedAt');
if (!hasUpdatedAtIndex) {
console.log('添加 updatedAt 索引...');
await sequelize.query(
"CREATE INDEX consumables_updatedAt ON consumables(updatedAt)"
);
await sequelize.query('CREATE INDEX consumables_updatedAt ON consumables(updatedAt)');
console.log('updatedAt 索引添加成功');
} else {
console.log('updatedAt 索引已存在,跳过');
}
} else if (actualDbType === 'mysql') {
// MySQL: 检查并添加字段
try {
@@ -70,9 +64,7 @@ async function migrate() {
// 添加索引
try {
console.log('添加 updatedAt 索引...');
await sequelize.query(
"CREATE INDEX idx_consumables_updatedAt ON consumables(updatedAt)"
);
await sequelize.query('CREATE INDEX idx_consumables_updatedAt ON consumables(updatedAt)');
console.log('updatedAt 索引添加成功');
} catch (err) {
if (err.message.includes('Duplicate key')) {
@@ -85,9 +77,7 @@ async function migrate() {
// 初始化现有数据的 version 值
console.log('初始化现有数据的 version 值...');
await sequelize.query(
"UPDATE consumables SET version = 0 WHERE version IS NULL"
);
await sequelize.query('UPDATE consumables SET version = 0 WHERE version IS NULL');
console.log('version 值初始化完成');
console.log('迁移完成!');
+30 -31
View File
@@ -8,19 +8,19 @@ async function migrate() {
console.log(' IDC管理系统 - 数据库迁移脚本 v2.0 ');
console.log('========================================');
console.log('');
try {
console.log('🔍 检测数据库类型...');
const dbType = sequelize.getDialect();
console.log(` 数据库类型: ${dbType}`);
console.log('');
console.log('📋 开始迁移...');
console.log(' 1. 创建 network_cards 表');
console.log(' 2. 为 device_ports 添加 nic_id 字段');
console.log(' 3. 创建相关索引');
console.log('');
if (dbType === 'sqlite') {
await migrateSQLite();
} else if (dbType === 'mysql') {
@@ -29,25 +29,26 @@ async function migrate() {
console.log(`⚠️ 不支持的数据库类型: ${dbType}`);
process.exit(1);
}
console.log('');
console.log('✅ 迁移完成!');
console.log('');
console.log('📊 验证迁移结果...');
const [tables] = await sequelize.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
const [tables] = await sequelize.query(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
);
console.log(` 数据库表: ${tables.map(t => t.name).join(', ')}`);
const portCount = await DevicePort.count();
const cardCount = await NetworkCard.count();
console.log(` device_ports: ${portCount} 条记录`);
console.log(` network_cards: ${cardCount} 条记录`);
console.log('');
console.log('========================================');
console.log(' 迁移成功完成!🎉');
console.log('========================================');
} catch (error) {
console.error('');
console.error('❌ 迁移失败:', error.message);
@@ -62,33 +63,32 @@ async function migrate() {
async function migrateSQLite() {
console.log('');
console.log('🔄 执行 SQLite 迁移...');
await sequelize.query('PRAGMA foreign_keys = OFF');
try {
console.log(' → 删除旧的 device_ports 表...');
await sequelize.query('DROP TABLE IF EXISTS `device_ports`');
console.log(' → 删除旧的 network_cards 表...');
await sequelize.query('DROP TABLE IF EXISTS `network_cards`');
console.log(' → 同步 DevicePort 模型...');
await DevicePort.sync({ force: false });
console.log(' → 同步 NetworkCard 模型...');
await NetworkCard.sync({ force: false });
console.log(' → 同步 Device 模型(确保外键关系)...');
await Device.sync({ force: false });
console.log(' → 重新同步 DevicePort 模型(含外键)...');
await DevicePort.sync({ force: true });
console.log(' → 重新同步 NetworkCard 模型...');
await NetworkCard.sync({ force: false });
await sequelize.query('PRAGMA foreign_keys = ON');
} catch (error) {
await sequelize.query('PRAGMA foreign_keys = ON');
throw error;
@@ -98,10 +98,10 @@ async function migrateSQLite() {
async function migrateMySQL() {
console.log('');
console.log('🔄 执行 MySQL 迁移...');
const tableName = 'network_cards';
console.log(` → 创建表: ${tableName}`);
const createTableSQL = `
CREATE TABLE IF NOT EXISTS \`${tableName}\` (
\`nic_id\` VARCHAR(255) NOT NULL PRIMARY KEY,
@@ -120,15 +120,14 @@ async function migrateMySQL() {
UNIQUE INDEX \`idx_device_name\` (\`device_id\`, \`name\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
await sequelize.query(createTableSQL);
console.log(' → 检查 nic_id 字段是否存在...');
const [columns] = await sequelize.query(
"SHOW COLUMNS FROM `device_ports` LIKE 'nic_id'",
{ type: sequelize.QueryTypes.SELECT }
);
const [columns] = await sequelize.query("SHOW COLUMNS FROM `device_ports` LIKE 'nic_id'", {
type: sequelize.QueryTypes.SELECT,
});
if (columns.length === 0) {
console.log(' → 添加 nic_id 字段...');
await sequelize.query(
@@ -137,7 +136,7 @@ async function migrateMySQL() {
} else {
console.log(' → nic_id 字段已存在,跳过');
}
console.log(' → 创建 nic_id 索引...');
try {
await sequelize.query('CREATE INDEX `idx_port_nic_id` ON `device_ports`(`nic_id`)');
@@ -148,11 +147,11 @@ async function migrateMySQL() {
throw error;
}
}
console.log(' → 同步模型以确保关系正确...');
await NetworkCard.sync({ force: false });
await DevicePort.sync({ force: false });
console.log(' → 添加外键约束...');
try {
await sequelize.query(
@@ -55,10 +55,9 @@ async function removeSQLiteForeignKey() {
console.log('SQLite 不支持直接删除外键,需要重建表');
// 检查外键是否存在
const fks = await sequelize.query(
`PRAGMA foreign_key_list(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT }
);
const fks = await sequelize.query(`PRAGMA foreign_key_list(consumable_logs);`, {
type: sequelize.QueryTypes.SELECT,
});
if (!fks || fks.length === 0) {
console.log('✓ 没有外键约束需要移除');
@@ -68,17 +67,21 @@ async function removeSQLiteForeignKey() {
console.log('发现外键约束:', fks);
// SQLite 不支持 ALTER TABLE DROP FOREIGN KEY,需要重建表
await sequelize.transaction(async (transaction) => {
await sequelize.transaction(async transaction => {
// 获取表结构
const columns = await sequelize.query(
`PRAGMA table_info(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT, transaction }
const columns = await sequelize.query(`PRAGMA table_info(consumable_logs);`, {
type: sequelize.QueryTypes.SELECT,
transaction,
});
console.log(
'\n当前表列:',
columns.map(c => c.name)
);
console.log('\n当前表列:', columns.map(c => c.name));
// 创建新表(不包含外键约束)
await sequelize.query(`
await sequelize.query(
`
CREATE TABLE consumable_logs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
consumableId VARCHAR(255) NOT NULL,
@@ -101,20 +104,25 @@ async function removeSQLiteForeignKey() {
isConsumableDeleted BOOLEAN DEFAULT 0,
consumableSnapshot TEXT
)
`, { transaction });
`,
{ transaction }
);
console.log('✓ 创建新表成功');
// 复制数据
await sequelize.query(`
await sequelize.query(
`
INSERT INTO consumable_logs_new
SELECT * FROM consumable_logs
`, { transaction });
const countResult = await sequelize.query(
`SELECT COUNT(*) as count FROM consumable_logs_new`,
{ type: sequelize.QueryTypes.SELECT, transaction }
`,
{ transaction }
);
const countResult = await sequelize.query(`SELECT COUNT(*) as count FROM consumable_logs_new`, {
type: sequelize.QueryTypes.SELECT,
transaction,
});
console.log(`✓ 复制了 ${countResult[0].count} 条数据`);
// 删除旧表
@@ -122,7 +130,9 @@ async function removeSQLiteForeignKey() {
console.log('✓ 删除旧表成功');
// 重命名新表
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO consumable_logs`, { transaction });
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO consumable_logs`, {
transaction,
});
console.log('✓ 重命名新表成功');
// 创建索引
@@ -133,7 +143,7 @@ async function removeSQLiteForeignKey() {
{ name: 'consumable_logs_consumable_id_created_at', fields: ['consumableId', 'createdAt'] },
{ name: 'consumable_logs_original_log_id', fields: ['originalLogId'] },
{ name: 'consumable_logs_is_editable', fields: ['isEditable'] },
{ name: 'idx_consumable_logs_is_deleted', fields: ['isConsumableDeleted'] }
{ name: 'idx_consumable_logs_is_deleted', fields: ['isConsumableDeleted'] },
];
for (const idx of indexes) {
+4 -4
View File
@@ -3,10 +3,10 @@
/**
* 命令行备份脚本
* 用于独立执行数据备份支持环境迁移
*
*
* 使用方法:
* node scripts/backup.js [options]
*
*
* 选项:
* --output, -o 指定备份文件输出路径
* --description 备份描述
@@ -67,7 +67,7 @@ async function runBackup() {
try {
process.chdir(path.join(__dirname, '..'));
const { createBackup, getBackupPath } = require('../utils/backup');
const { sequelize } = require('../db');
@@ -76,7 +76,7 @@ async function runBackup() {
console.log('数据库连接成功\n');
const backupPath = options.output ? path.dirname(options.output) : getBackupPath();
console.log('备份配置:');
console.log(` 输出路径: ${backupPath}`);
console.log(` 包含文件: ${options.includeFiles ? '是' : '否'}`);
+8 -7
View File
@@ -8,22 +8,23 @@ async function ensureSchema() {
if (dbType === 'sqlite') {
// Check if nicId column exists in device_ports
const [columns] = await sequelize.query("PRAGMA table_info(device_ports)");
const [columns] = await sequelize.query('PRAGMA table_info(device_ports)');
const hasNicId = columns.some(col => col.name === 'nicId');
if (!hasNicId) {
console.log('Adding nicId column to device_ports...');
await sequelize.query('ALTER TABLE device_ports ADD COLUMN nicId VARCHAR(255) NULL REFERENCES network_cards(nicId)');
await sequelize.query(
'ALTER TABLE device_ports ADD COLUMN nicId VARCHAR(255) NULL REFERENCES network_cards(nicId)'
);
console.log('Added nicId column.');
} else {
console.log('nicId column already exists in device_ports.');
}
} else if (dbType === 'mysql') {
const [columns] = await sequelize.query(
"SHOW COLUMNS FROM `device_ports` LIKE 'nicId'",
{ type: QueryTypes.SELECT }
);
const [columns] = await sequelize.query("SHOW COLUMNS FROM `device_ports` LIKE 'nicId'", {
type: QueryTypes.SELECT,
});
if (columns.length === 0) {
console.log('Adding nicId column to device_ports...');
await sequelize.query(
@@ -4,98 +4,91 @@ const path = require('path');
const templateData = [
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC2-SERVER-01',
'所属机房名称': 'IDC2',
机柜名称: 'IDC2-SERVER-01',
所属机房名称: 'IDC2',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC2-SERVER-02',
'所属机房名称': 'IDC2',
机柜名称: 'IDC2-SERVER-02',
所属机房名称: 'IDC2',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC2-SERVER-03',
'所属机房名称': 'IDC2',
机柜名称: 'IDC2-SERVER-03',
所属机房名称: 'IDC2',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC4-NETWORK-01',
'所属机房名称': 'IDC4',
机柜名称: 'IDC4-NETWORK-01',
所属机房名称: 'IDC4',
'高度(U)': 48,
'最大功率(W)': 8000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC4-NETWORK-02',
'所属机房名称': 'IDC4',
机柜名称: 'IDC4-NETWORK-02',
所属机房名称: 'IDC4',
'高度(U)': 48,
'最大功率(W)': 8000,
'状态': 'maintenance'
状态: 'maintenance',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC5-STORAGE-01',
'所属机房名称': 'IDC5',
机柜名称: 'IDC5-STORAGE-01',
所属机房名称: 'IDC5',
'高度(U)': 42,
'最大功率(W)': 10000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC5-STORAGE-02',
'所属机房名称': 'IDC5',
机柜名称: 'IDC5-STORAGE-02',
所属机房名称: 'IDC5',
'高度(U)': 42,
'最大功率(W)': 10000,
'状态': 'inactive'
状态: 'inactive',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC7-SERVER-01',
'所属机房名称': 'IDC7',
机柜名称: 'IDC7-SERVER-01',
所属机房名称: 'IDC7',
'高度(U)': 36,
'最大功率(W)': 6000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': '古荡-机柜-01',
'所属机房名称': '古荡机房1-1',
机柜名称: '古荡-机柜-01',
所属机房名称: '古荡机房1-1',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': '古荡-机柜-02',
'所属机房名称': '古荡机房1-1',
机柜名称: '古荡-机柜-02',
所属机房名称: '古荡机房1-1',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
}
状态: 'active',
},
];
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(templateData);
ws['!cols'] = [
{ wch: 20 },
{ wch: 20 },
{ wch: 15 },
{ wch: 10 },
{ wch: 15 },
{ wch: 15 }
];
ws['!cols'] = [{ wch: 20 }, { wch: 20 }, { wch: 15 }, { wch: 10 }, { wch: 15 }, { wch: 15 }];
XLSX.utils.book_append_sheet(wb, ws, '机柜导入模板');
@@ -107,7 +100,9 @@ console.log(`共 ${templateData.length} 条测试数据`);
console.log('\n字段说明:');
console.log('- 机柜ID(留空自动生成): 留空则系统自动生成唯一ID');
console.log('- 机柜名称: 必填,机柜的唯一标识名称');
console.log('- 所属机房名称: 必填,系统现有机房: IDC2, IDC4, IDC5, IDC7, 古荡机房1-1, 三墩机房1-1, 地市机房, IDCT');
console.log(
'- 所属机房名称: 必填,系统现有机房: IDC2, IDC4, IDC5, IDC7, 古荡机房1-1, 三墩机房1-1, 地市机房, IDCT'
);
console.log('- 高度(U): 必填,机柜的标准高度(1-50U');
console.log('- 最大功率(W): 必填,机柜的最大承载功率');
console.log('- 状态: active(在用)/maintenance(维护中)/inactive(停用)');
console.log('- 状态: active(在用)/maintenance(维护中)/inactive(停用)');
+190 -29
View File
@@ -1,7 +1,7 @@
/**
* 数据库初始化脚本
* 用于生产环境首次部署时创建表结构
*
*
* 注意此脚本为独立运行脚本 server.js 的初始化逻辑重复
* 如果 server.js 已能正常完成初始化则无需单独运行此脚本
* 如需单独运行node backend/scripts/init-database.js
@@ -26,33 +26,194 @@ const InventoryRecord = require('../models/InventoryRecord');
// 默认系统设置(包含中文描述)
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(10), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(10), 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(10),
settingType: 'number',
category: 'general',
description: '用户空闲超时时间(分钟)',
isEditable: true,
},
{
settingKey: 'idle_warning_time',
settingValue: JSON.stringify(10),
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,
},
];
async function initDatabase() {
@@ -65,29 +226,29 @@ async function initDatabase() {
// 同步所有模型(按依赖顺序)
console.log('开始同步模型...');
// 基础模型
await User.sync({ alter: true });
await Room.sync({ alter: true });
await Rack.sync({ alter: true });
await Device.sync({ alter: true });
await FaultCategory.sync({ alter: true });
// 耗材相关模型
await ConsumableCategory.sync({ alter: true });
await Consumable.sync({ alter: true });
await ConsumableLog.sync({ alter: true });
await ConsumableRecord.sync({ alter: true });
await ConsumableLogArchive.sync({ alter: true });
// 盘点相关模型
await InventoryPlan.sync({ alter: true });
await InventoryTask.sync({ alter: true });
await InventoryRecord.sync({ alter: true });
// 系统设置
await SystemSetting.sync({ alter: true });
console.log('数据库表结构同步完成');
// 初始化或更新系统设置
+134 -113
View File
@@ -28,73 +28,73 @@ const migrations = [
{
name: 'v2.0 - 网卡和端口表',
description: '创建 network_cards 表,为 device_ports 添加 nic_id 字段',
migrate: migrateV2
migrate: migrateV2,
},
{
name: '用户表 pending 状态',
description: '为用户表添加 pending 状态支持',
migrate: migratePendingStatus
migrate: migratePendingStatus,
},
{
name: '耗材乐观锁',
description: '为 consumables 表添加 version 字段',
migrate: migrateConsumableVersion
migrate: migrateConsumableVersion,
},
{
name: '耗材操作日志表结构',
description: '添加 isEditable、originalLogId 等修改记录字段',
migrate: migrateConsumableLogs
migrate: migrateConsumableLogs,
},
{
name: '耗材日志解耦',
description: '添加 isConsumableDeleted 和 consumableSnapshot 字段',
migrate: migrateConsumableLogDecouple
migrate: migrateConsumableLogDecouple,
},
{
name: '移除日志外键约束',
description: '移除 consumable_logs 表的外键约束,防止级联删除',
migrate: removeConsumableLogFK
migrate: removeConsumableLogFK,
},
{
name: '耗材日志归档表',
description: '创建 consumable_log_archives 归档表',
migrate: migrateConsumableLogArchive
migrate: migrateConsumableLogArchive,
},
{
name: '耗材SN序列号字段',
description: '为 consumables、consumable_records、consumable_logs 添加 snList 字段',
migrate: migrateSnList
migrate: migrateSnList,
},
{
name: '设备型号字段可空',
description: '将 devices 表 model 字段改为可空,支持非必填',
migrate: migrateDeviceModelField
migrate: migrateDeviceModelField,
},
{
name: '设备字段配置同步',
description: '同步前后端字段必填配置',
migrate: migrateDeviceFieldsConfig
migrate: migrateDeviceFieldsConfig,
},
{
name: '设备表字段可空',
description: '将设备表所有字段改为可空,由应用层验证控制',
migrate: migrateDeviceFieldsNullable
migrate: migrateDeviceFieldsNullable,
},
{
name: '暂存设备自定义字段',
description: '为 pending_devices 表添加 customFields 字段,支持自定义字段存储',
migrate: migratePendingDeviceCustomFields
migrate: migratePendingDeviceCustomFields,
},
{
name: '空闲设备与业务关联',
description: '创建 businesses、warehouses、device_business 表,为 devices 添加空闲设备字段',
migrate: migrateIdleDeviceAndBusiness
migrate: migrateIdleDeviceAndBusiness,
},
{
name: '设备字段系统标记',
description: '为 deviceFields 表添加 isSystem 字段,标记系统字段不可删除',
migrate: migrateDeviceFieldsIsSystem
}
migrate: migrateDeviceFieldsIsSystem,
},
];
async function runMigrations() {
@@ -149,25 +149,23 @@ async function runMigrations() {
async function getTableColumns(tableName) {
const dialect = sequelize.getDialect();
if (dialect === 'sqlite') {
const tableInfo = await sequelize.query(
`PRAGMA table_info(${tableName})`,
{ type: sequelize.QueryTypes.SELECT }
);
const tableInfo = await sequelize.query(`PRAGMA table_info(${tableName})`, {
type: sequelize.QueryTypes.SELECT,
});
return tableInfo.map(col => col.name);
} else {
const tableInfo = await sequelize.query(
`SHOW COLUMNS FROM ${tableName}`,
{ type: sequelize.QueryTypes.SELECT }
);
const tableInfo = await sequelize.query(`SHOW COLUMNS FROM ${tableName}`, {
type: sequelize.QueryTypes.SELECT,
});
return tableInfo.map(col => col.Field);
}
}
async function tableExists(tableName) {
const dialect = sequelize.getDialect();
if (dialect === 'sqlite') {
const tables = await sequelize.query(
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
@@ -175,22 +173,23 @@ async function tableExists(tableName) {
);
return tables.length > 0;
} else {
const tables = await sequelize.query(
"SHOW TABLES LIKE ?",
{ replacements: [tableName], type: sequelize.QueryTypes.SELECT }
);
const tables = await sequelize.query('SHOW TABLES LIKE ?', {
replacements: [tableName],
type: sequelize.QueryTypes.SELECT,
});
return tables.length > 0;
}
}
async function addColumnIfNotExists(tableName, columnName, columnDef) {
const columns = await getTableColumns(tableName);
if (!columns.includes(columnName)) {
const dialect = sequelize.getDialect();
const sql = dialect === 'sqlite'
? `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`
: `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`;
const sql =
dialect === 'sqlite'
? `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`
: `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`;
await sequelize.query(sql);
console.log(` ${tableName} 表添加 ${columnName} 字段成功`);
} else {
@@ -210,32 +209,32 @@ async function migrateV2() {
id: {
type: sequelize.Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
autoIncrement: true,
},
nicId: {
type: sequelize.Sequelize.STRING,
allowNull: false,
unique: true
unique: true,
},
name: {
type: sequelize.Sequelize.STRING,
allowNull: false
allowNull: false,
},
macAddress: {
type: sequelize.Sequelize.STRING
type: sequelize.Sequelize.STRING,
},
ipAddress: {
type: sequelize.Sequelize.STRING
type: sequelize.Sequelize.STRING,
},
deviceId: {
type: sequelize.Sequelize.STRING
type: sequelize.Sequelize.STRING,
},
status: {
type: sequelize.Sequelize.STRING,
defaultValue: 'active'
defaultValue: 'active',
},
createdAt: sequelize.Sequelize.DATE,
updatedAt: sequelize.Sequelize.DATE
updatedAt: sequelize.Sequelize.DATE,
});
}
@@ -276,12 +275,11 @@ async function migrateConsumableLogDecouple() {
async function removeConsumableLogFK() {
const dialect = sequelize.getDialect();
if (dialect === 'sqlite') {
const fks = await sequelize.query(
`PRAGMA foreign_key_list(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT }
);
const fks = await sequelize.query(`PRAGMA foreign_key_list(consumable_logs);`, {
type: sequelize.QueryTypes.SELECT,
});
if (!fks || fks.length === 0) {
return;
@@ -327,7 +325,9 @@ async function removeConsumableLogFK() {
await sequelize.query(`CREATE INDEX idx_logs_consumable_id ON consumable_logs(consumableId)`);
await sequelize.query(`CREATE INDEX idx_logs_operation_type ON consumable_logs(operationType)`);
await sequelize.query(`CREATE INDEX idx_logs_created_at ON consumable_logs(createdAt)`);
await sequelize.query(`CREATE INDEX idx_logs_is_consumable_deleted ON consumable_logs(isConsumableDeleted)`);
await sequelize.query(
`CREATE INDEX idx_logs_is_consumable_deleted ON consumable_logs(isConsumableDeleted)`
);
}
}
@@ -341,80 +341,86 @@ async function migrateConsumableLogArchive() {
id: {
type: sequelize.Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
autoIncrement: true,
},
archiveId: {
type: sequelize.Sequelize.STRING,
allowNull: false,
unique: true
unique: true,
},
consumableId: {
type: sequelize.Sequelize.STRING,
allowNull: false
allowNull: false,
},
consumableName: {
type: sequelize.Sequelize.STRING,
allowNull: false
allowNull: false,
},
consumableSnapshot: {
type: sequelize.Sequelize.TEXT
type: sequelize.Sequelize.TEXT,
},
totalOperations: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
defaultValue: 0,
},
firstOperationAt: {
type: sequelize.Sequelize.DATE
type: sequelize.Sequelize.DATE,
},
lastOperationAt: {
type: sequelize.Sequelize.DATE
type: sequelize.Sequelize.DATE,
},
totalInQuantity: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
defaultValue: 0,
},
totalOutQuantity: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
defaultValue: 0,
},
finalStock: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
defaultValue: 0,
},
deletedBy: {
type: sequelize.Sequelize.STRING
type: sequelize.Sequelize.STRING,
},
deletedAt: {
type: sequelize.Sequelize.DATE
type: sequelize.Sequelize.DATE,
},
deleteReason: {
type: sequelize.Sequelize.STRING
type: sequelize.Sequelize.STRING,
},
createdAt: {
type: sequelize.Sequelize.DATE,
allowNull: false,
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP'),
},
updatedAt: {
type: sequelize.Sequelize.DATE,
allowNull: false,
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
}
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
if (dbDialect === 'sqlite') {
await sequelize.query(`CREATE INDEX idx_archive_consumable_id ON consumable_log_archives(consumableId)`);
await sequelize.query(`CREATE INDEX idx_archive_archive_id ON consumable_log_archives(archiveId)`);
await sequelize.query(`CREATE INDEX idx_archive_deleted_at ON consumable_log_archives(deletedAt)`);
await sequelize.query(
`CREATE INDEX idx_archive_consumable_id ON consumable_log_archives(consumableId)`
);
await sequelize.query(
`CREATE INDEX idx_archive_archive_id ON consumable_log_archives(archiveId)`
);
await sequelize.query(
`CREATE INDEX idx_archive_deleted_at ON consumable_log_archives(deletedAt)`
);
}
}
async function migrateSnList() {
const tables = ['consumables', 'consumable_records', 'consumable_logs'];
for (const table of tables) {
if (await tableExists(table)) {
const columnDef = dbDialect === 'sqlite' ? "TEXT DEFAULT '[]'" : "JSON";
const columnDef = dbDialect === 'sqlite' ? "TEXT DEFAULT '[]'" : 'JSON';
await addColumnIfNotExists(table, 'snList', columnDef);
} else {
console.log(` ${table} 表不存在,跳过`);
@@ -429,11 +435,9 @@ async function migrateDeviceModelField() {
}
const dialect = sequelize.getDialect();
if (dialect === 'mysql') {
await sequelize.query(
'ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL'
);
await sequelize.query('ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL');
console.log(' devices 表 model 字段已改为可空');
} else if (dialect === 'sqlite') {
const columns = await getTableColumns('devices');
@@ -441,7 +445,7 @@ async function migrateDeviceModelField() {
console.log(' model_old 字段已存在,跳过迁移');
return;
}
await sequelize.query('ALTER TABLE devices RENAME COLUMN model TO model_old');
await sequelize.query('ALTER TABLE devices ADD COLUMN model VARCHAR(255)');
await sequelize.query('UPDATE devices SET model = model_old');
@@ -452,14 +456,14 @@ async function migrateDeviceModelField() {
async function migrateDeviceFieldsConfig() {
const DeviceField = require('../models/DeviceField');
const updates = [
{ fieldName: 'model', required: false },
{ fieldName: 'powerConsumption', required: true },
{ fieldName: 'purchaseDate', required: false },
{ fieldName: 'warrantyExpiry', required: false },
];
for (const update of updates) {
const field = await DeviceField.findOne({ where: { fieldName: update.fieldName } });
if (field && field.required !== update.required) {
@@ -475,20 +479,20 @@ async function migrateDeviceFieldsConfig() {
async function migrateDeviceFieldsNullable() {
const dialect = sequelize.getDialect();
if (dialect === 'mysql') {
const alterCommands = [
"ALTER TABLE devices MODIFY COLUMN name VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN type VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN serialNumber VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN rackId VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN position INTEGER NULL",
"ALTER TABLE devices MODIFY COLUMN height INTEGER NULL",
"ALTER TABLE devices MODIFY COLUMN powerConsumption FLOAT NULL",
"ALTER TABLE devices MODIFY COLUMN customFields JSON NULL"
'ALTER TABLE devices MODIFY COLUMN name VARCHAR(255) NULL',
'ALTER TABLE devices MODIFY COLUMN type VARCHAR(255) NULL',
'ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL',
'ALTER TABLE devices MODIFY COLUMN serialNumber VARCHAR(255) NULL',
'ALTER TABLE devices MODIFY COLUMN rackId VARCHAR(255) NULL',
'ALTER TABLE devices MODIFY COLUMN position INTEGER NULL',
'ALTER TABLE devices MODIFY COLUMN height INTEGER NULL',
'ALTER TABLE devices MODIFY COLUMN powerConsumption FLOAT NULL',
'ALTER TABLE devices MODIFY COLUMN customFields JSON NULL',
];
for (const sql of alterCommands) {
try {
await sequelize.query(sql);
@@ -499,21 +503,20 @@ async function migrateDeviceFieldsNullable() {
}
}
console.log(' devices 表字段已改为可空');
} else if (dialect === 'sqlite') {
const columns = await getTableColumns('devices');
const hasNullableFlag = columns.includes('_nullable_migration_done');
if (hasNullableFlag) {
console.log(' 已完成可空迁移,跳过');
return;
}
await sequelize.query('PRAGMA foreign_keys = OFF');
try {
await sequelize.query('DROP TABLE IF EXISTS devices_new');
await sequelize.query(`
CREATE TABLE devices_new (
deviceId VARCHAR(255) PRIMARY KEY NOT NULL UNIQUE,
@@ -536,7 +539,7 @@ async function migrateDeviceFieldsNullable() {
_nullable_migration_done INTEGER DEFAULT 1
)
`);
await sequelize.query(`
INSERT INTO devices_new (
deviceId, name, type, model, serialNumber, rackId, position, height,
@@ -549,15 +552,15 @@ async function migrateDeviceFieldsNullable() {
description, customFields, createdAt, updatedAt
FROM devices
`);
await sequelize.query('DROP TABLE devices');
await sequelize.query('ALTER TABLE devices_new RENAME TO devices');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_status ON devices(status)');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_type ON devices(type)');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_rackId ON devices(rackId)');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_name ON devices(name)');
console.log(' devices 表字段已改为可空');
} finally {
await sequelize.query('PRAGMA foreign_keys = ON');
@@ -571,7 +574,7 @@ async function migratePendingDeviceCustomFields() {
return;
}
const columnDef = dbDialect === 'sqlite' ? "JSON DEFAULT '{}'" : "JSON";
const columnDef = dbDialect === 'sqlite' ? "JSON DEFAULT '{}'" : 'JSON';
await addColumnIfNotExists('pending_devices', 'customFields', columnDef);
}
@@ -605,10 +608,9 @@ async function migrateDeviceFieldsIsSystem() {
} else {
const columns = await getTableColumns('deviceFields');
if (!columns.includes('isSystem')) {
await sequelize.query(
"ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0",
{ type: sequelize.QueryTypes.RAW }
);
await sequelize.query('ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0', {
type: sequelize.QueryTypes.RAW,
});
console.log(' deviceFields 表添加 isSystem 字段成功');
} else {
console.log(' deviceFields 表 isSystem 字段已存在,跳过');
@@ -616,16 +618,25 @@ async function migrateDeviceFieldsIsSystem() {
}
const systemFields = [
'deviceId', 'name', 'type', 'model', 'serialNumber',
'rackId', 'position', 'height', 'powerConsumption',
'status', 'purchaseDate', 'warrantyExpiry'
'deviceId',
'name',
'type',
'model',
'serialNumber',
'rackId',
'position',
'height',
'powerConsumption',
'status',
'purchaseDate',
'warrantyExpiry',
];
for (const fieldName of systemFields) {
await sequelize.query(
`UPDATE deviceFields SET isSystem = 1 WHERE fieldName = ?`,
{ replacements: [fieldName], type: sequelize.QueryTypes.RAW }
);
await sequelize.query(`UPDATE deviceFields SET isSystem = 1 WHERE fieldName = ?`, {
replacements: [fieldName],
type: sequelize.QueryTypes.RAW,
});
console.log(` 标记系统字段: ${fieldName}`);
}
@@ -643,14 +654,19 @@ async function migrateIdleDeviceAndBusiness() {
try {
if (!(await tableExists('businesses'))) {
await queryInterface.createTable('businesses', {
businessId: { type: sequelize.Sequelize.STRING, primaryKey: true, allowNull: false, unique: true },
businessId: {
type: sequelize.Sequelize.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: { type: sequelize.Sequelize.STRING, allowNull: false },
description: { type: sequelize.Sequelize.TEXT },
status: { type: sequelize.Sequelize.ENUM('active', 'offline'), defaultValue: 'active' },
offlineDate: { type: sequelize.Sequelize.DATE },
offlineReason: { type: sequelize.Sequelize.STRING },
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false }
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false },
});
console.log(' businesses 表创建成功');
} else {
@@ -659,14 +675,19 @@ async function migrateIdleDeviceAndBusiness() {
if (!(await tableExists('warehouses'))) {
await queryInterface.createTable('warehouses', {
warehouseId: { type: sequelize.Sequelize.STRING, primaryKey: true, allowNull: false, unique: true },
warehouseId: {
type: sequelize.Sequelize.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: { type: sequelize.Sequelize.STRING, allowNull: false },
location: { type: sequelize.Sequelize.STRING },
capacity: { type: sequelize.Sequelize.INTEGER, defaultValue: 100 },
status: { type: sequelize.Sequelize.ENUM('active', 'inactive'), defaultValue: 'active' },
description: { type: sequelize.Sequelize.TEXT },
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false }
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false },
});
console.log(' warehouses 表创建成功');
} else {
@@ -680,7 +701,7 @@ async function migrateIdleDeviceAndBusiness() {
businessId: { type: sequelize.Sequelize.STRING, allowNull: false },
isPrimary: { type: sequelize.Sequelize.BOOLEAN, defaultValue: false },
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false }
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false },
});
console.log(' device_business 表创建成功');
} else {
+11 -11
View File
@@ -3,10 +3,10 @@
/**
* 命令行恢复脚本
* 用于独立执行数据恢复支持跨环境迁移
*
*
* 使用方法:
* node scripts/restore.js <backup-file> [options]
*
*
* 选项:
* --skip-users 跳过用户数据恢复
* --skip-files 跳过文件恢复
@@ -76,7 +76,7 @@ async function runRestore() {
console.log('========================================\n');
const backupFile = path.resolve(options.file);
if (!fs.existsSync(backupFile)) {
console.error(`错误: 备份文件不存在: ${backupFile}`);
process.exit(1);
@@ -84,7 +84,7 @@ async function runRestore() {
try {
process.chdir(path.join(__dirname, '..'));
const { restoreBackup, validateBackupFile } = require('../utils/backup');
const { sequelize } = require('../db');
@@ -94,7 +94,7 @@ async function runRestore() {
console.log('验证备份文件...');
const validation = await validateBackupFile(backupFile);
if (!validation.valid) {
console.error(`错误: 备份文件验证失败: ${validation.error}`);
process.exit(1);
@@ -124,17 +124,17 @@ async function runRestore() {
}
console.log('开始恢复数据...\n');
const result = await restoreBackup(backupFile, {
overwriteExisting: options.overwrite,
skipTables,
skipFiles: options.skipFiles,
onProgress: (tableName, status, count) => {
const statusMap = {
'restored': '✓ 已恢复',
'skipped': '○ 已跳过',
'empty': '- 无数据',
'error': '✗ 错误',
restored: '✓ 已恢复',
skipped: '○ 已跳过',
empty: '- 无数据',
error: '✗ 错误',
};
const statusText = statusMap[status] || status;
const countText = count ? ` (${count} 条)` : '';
@@ -162,7 +162,7 @@ async function runRestore() {
}
console.log('\n提示: 请重启后端服务以确保所有数据生效');
await sequelize.close();
process.exit(0);
} catch (error) {
+23 -17
View File
@@ -3,26 +3,32 @@ const { sequelize } = require('../db');
async function updateSystemFields() {
try {
console.log('开始调整系统字段标记...');
// 核心系统字段(数据库必填字段,不可删除)
const coreSystemFields = [
'deviceId', 'name', 'type', 'model', 'serialNumber',
'rackId', 'position', 'height', 'powerConsumption',
'status', 'purchaseDate', 'warrantyExpiry'
'deviceId',
'name',
'type',
'model',
'serialNumber',
'rackId',
'position',
'height',
'powerConsumption',
'status',
'purchaseDate',
'warrantyExpiry',
];
// 可选字段(非系统字段,可删除)
const optionalFields = [
'ipAddress', 'description', 'owner', 'department', 'assetId', 'brand'
];
const optionalFields = ['ipAddress', 'description', 'owner', 'department', 'assetId', 'brand'];
// 先将所有字段设为非系统字段
await sequelize.query(
`UPDATE deviceFields SET isSystem = 0`,
{ type: sequelize.QueryTypes.RAW }
);
await sequelize.query(`UPDATE deviceFields SET isSystem = 0`, {
type: sequelize.QueryTypes.RAW,
});
console.log('已重置所有字段为非系统字段');
// 标记核心系统字段
for (const fieldName of coreSystemFields) {
await sequelize.query(
@@ -31,7 +37,7 @@ async function updateSystemFields() {
);
console.log(`标记为核心系统字段: ${fieldName}`);
}
// 确保可选字段为非系统字段
for (const fieldName of optionalFields) {
await sequelize.query(
@@ -40,11 +46,11 @@ async function updateSystemFields() {
);
console.log(`标记为可选字段: ${fieldName}`);
}
console.log('\n系统字段调整完成!');
console.log('核心系统字段(不可删除):', coreSystemFields.join(', '));
console.log('可选字段(可删除):', optionalFields.join(', '));
process.exit(0);
} catch (error) {
console.error('更新失败:', error);
+17 -7
View File
@@ -3,15 +3,25 @@ const { sequelize } = require('../db');
async function updateSystemFields() {
try {
console.log('开始更新系统字段标记...');
// 系统字段列表
const systemFields = [
'deviceId', 'name', 'type', 'model', 'serialNumber',
'rackId', 'position', 'height', 'powerConsumption',
'status', 'purchaseDate', 'warrantyExpiry',
'ipAddress', 'description'
'deviceId',
'name',
'type',
'model',
'serialNumber',
'rackId',
'position',
'height',
'powerConsumption',
'status',
'purchaseDate',
'warrantyExpiry',
'ipAddress',
'description',
];
// 更新系统字段标记
for (const fieldName of systemFields) {
const [result] = await sequelize.query(
@@ -20,7 +30,7 @@ async function updateSystemFields() {
);
console.log(`标记系统字段: ${fieldName}`);
}
console.log('系统字段标记更新完成');
process.exit(0);
} catch (error) {
+82 -32
View File
@@ -23,7 +23,7 @@ async function syncDatabase() {
await sequelize.sync({
force: false,
alter: false
alter: false,
});
console.log('数据库表结构同步完成');
}
@@ -61,7 +61,7 @@ async function syncConsumableModels() {
ConsumableLog.sync(),
ConsumableCategory.sync(),
ConsumableRecord.sync(),
ConsumableLogArchive.sync()
ConsumableLogArchive.sync(),
]);
console.log('耗材模型同步完成');
}
@@ -71,11 +71,7 @@ async function syncInventoryModels() {
const InventoryTask = require('./models/InventoryTask');
const InventoryRecord = require('./models/InventoryRecord');
await Promise.all([
InventoryPlan.sync(),
InventoryTask.sync(),
InventoryRecord.sync()
]);
await Promise.all([InventoryPlan.sync(), InventoryTask.sync(), InventoryRecord.sync()]);
console.log('盘点模型同步完成');
}
@@ -114,16 +110,66 @@ async function initFaultCategories() {
const FaultCategory = require('./models/FaultCategory');
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) {
@@ -136,7 +182,7 @@ async function initFaultCategories() {
expectedDuration: 120,
solutions: [],
isSystem: true,
isActive: true
isActive: true,
});
console.log(`创建故障分类: ${cat.name}`);
}
@@ -234,18 +280,22 @@ app.use('/api/dangerous-operations', dangerousOperationsRoutes);
app.use('/uploads', express.static('uploads'));
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, {
customCss: customCSS,
customSiteTitle: 'IDC设备管理系统 API文档',
swaggerOptions: {
persistAuthorization: true,
displayRequestDuration: true,
docExpansion: 'none',
deepLinking: true,
defaultModelsExpandDepth: -1,
defaultModelExpandDepth: 2
}
}));
app.use(
'/api-docs',
swaggerUi.serve,
swaggerUi.setup(specs, {
customCss: customCSS,
customSiteTitle: 'IDC设备管理系统 API文档',
swaggerOptions: {
persistAuthorization: true,
displayRequestDuration: true,
docExpansion: 'none',
deepLinking: true,
defaultModelsExpandDepth: -1,
defaultModelExpandDepth: 2,
},
})
);
app.get('/api-docs', (req, res) => {
res.sendFile(path.join(__dirname, 'swagger_index.html'));
@@ -279,10 +329,10 @@ app.get('/api', (req, res) => {
roles: '/api/roles',
systemSettings: '/api/system-settings',
background: '/api/background',
inventory: '/api/inventory'
inventory: '/api/inventory',
},
health: '/health',
documentation: '/docs/api/README.md'
documentation: '/docs/api/README.md',
});
});
+15 -13
View File
@@ -515,14 +515,14 @@ const options = {
version: '1.0.0',
description: '数据中心设备管理平台后端服务 API 文档',
contact: {
name: 'API Support'
}
name: 'API Support',
},
},
servers: [
{
url: 'http://localhost:8000',
description: '开发环境服务器'
}
description: '开发环境服务器',
},
],
components: {
securitySchemes: {
@@ -530,13 +530,15 @@ const options = {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: '输入 JWT token'
}
}
description: '输入 JWT token',
},
},
},
security: [{
bearerAuth: []
}],
security: [
{
bearerAuth: [],
},
],
tags: [
{ name: 'health', description: '健康检查' },
{ name: 'auth', description: '认证接口' },
@@ -560,10 +562,10 @@ const options = {
{ name: 'inventory', description: '盘点管理' },
{ name: 'statistics', description: '统计接口' },
{ name: 'operation-logs', description: '操作日志' },
{ name: 'backup', description: '备份管理' }
]
{ name: 'backup', description: '备份管理' },
],
},
apis: ['./routes/*.js', './swagger_docs.yaml']
apis: ['./routes/*.js', './swagger_docs.yaml'],
};
const specs = swaggerJsdoc(options);
+52 -47
View File
@@ -28,14 +28,14 @@ describe('设备/用户/角色操作日志集成测试', () => {
username: 'admin',
password: '$2a$10$test',
realName: '管理员',
status: 'active'
status: 'active',
});
testRoom = await Room.create({
roomId: 'ROOM_INT_TEST',
name: '测试机房',
location: '测试位置',
status: 'active'
status: 'active',
});
testRack = await Rack.create({
@@ -46,12 +46,17 @@ describe('设备/用户/角色操作日志集成测试', () => {
currentPower: 0,
totalUnits: 48,
usedUnits: 0,
status: 'active'
status: 'active',
});
app = createTestApp();
authToken = jwt.sign(
{ userId: adminUser.userId, username: adminUser.username, realName: adminUser.realName, roleName: '管理员' },
{
userId: adminUser.userId,
username: adminUser.username,
realName: adminUser.realName,
roleName: '管理员',
},
JWT_SECRET,
{ expiresIn: '24h' }
);
@@ -101,7 +106,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
position: 1,
height: 2,
powerConsumption: 500,
status: 'running'
status: 'running',
};
const response = await request(app)
@@ -116,8 +121,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'device',
operationType: 'create',
targetId: response.body.deviceId
}
targetId: response.body.deviceId,
},
});
expect(logs.length).toBe(1);
@@ -135,7 +140,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
position: 5,
height: 2,
powerConsumption: 500,
status: 'offline'
status: 'offline',
});
const response = await request(app)
@@ -150,8 +155,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'device',
operationType: 'update',
targetId: device.deviceId
}
targetId: device.deviceId,
},
});
expect(logs.length).toBe(1);
@@ -168,7 +173,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
position: 10,
height: 2,
powerConsumption: 500,
status: 'running'
status: 'running',
});
await request(app)
@@ -180,8 +185,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'device',
operationType: 'delete',
targetId: device.deviceId
}
targetId: device.deviceId,
},
});
expect(logs.length).toBe(1);
@@ -197,7 +202,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
position: 15,
height: 2,
powerConsumption: 500,
status: 'running'
status: 'running',
});
const device2 = await Device.create({
@@ -208,7 +213,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
position: 20,
height: 2,
powerConsumption: 500,
status: 'running'
status: 'running',
});
const response = await request(app)
@@ -218,7 +223,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
.expect(200);
const logs = await OperationLog.findAll({
where: { operationType: 'batch_delete' }
where: { operationType: 'batch_delete' },
});
expect(logs.length).toBe(1);
@@ -234,7 +239,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
position: 25,
height: 2,
powerConsumption: 500,
status: 'offline'
status: 'offline',
});
const device2 = await Device.create({
@@ -245,7 +250,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
position: 30,
height: 2,
powerConsumption: 500,
status: 'offline'
status: 'offline',
});
const response = await request(app)
@@ -255,7 +260,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
.expect(200);
const logs = await OperationLog.findAll({
where: { operationType: 'status_change' }
where: { operationType: 'status_change' },
});
expect(logs.length).toBe(1);
@@ -273,7 +278,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
roleName: '测试角色',
roleCode: `test_role_${Date.now()}`,
permissions: ['read', 'write'],
status: 'active'
status: 'active',
});
});
@@ -283,7 +288,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
password: 'Password123!',
realName: '集成测试用户',
email: `test_${Date.now()}@example.com`,
roleIds: [testRole.roleId]
roleIds: [testRole.roleId],
};
const response = await request(app)
@@ -296,8 +301,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'user',
operationType: 'create',
targetName: userData.username
}
targetName: userData.username,
},
});
expect(logs.length).toBe(1);
@@ -314,7 +319,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
password: 'Password123!',
realName: '旧名称用户',
email: `old_${Date.now()}@example.com`,
status: 'active'
status: 'active',
});
const response = await request(app)
@@ -327,8 +332,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'user',
operationType: 'update',
targetId: user.userId
}
targetId: user.userId,
},
});
expect(logs.length).toBe(1);
@@ -345,7 +350,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
password: 'Password123!',
realName: '角色测试用户',
email: `role_${Date.now()}@example.com`,
status: 'active'
status: 'active',
});
const newRole = await Role.create({
@@ -353,12 +358,12 @@ describe('设备/用户/角色操作日志集成测试', () => {
roleName: '新测试角色',
roleCode: `new_role_${Date.now()}`,
permissions: ['admin'],
status: 'active'
status: 'active',
});
await UserRole.create({
UserId: user.userId,
RoleId: testRole.roleId
RoleId: testRole.roleId,
});
const response = await request(app)
@@ -371,8 +376,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'user',
operationType: 'permission_change',
targetId: user.userId
}
targetId: user.userId,
},
});
expect(logs.length).toBe(1);
@@ -391,7 +396,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
password: 'Password123!',
realName: '删除测试用户',
email: `del_${Date.now()}@example.com`,
status: 'active'
status: 'active',
});
await request(app)
@@ -403,8 +408,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'user',
operationType: 'delete',
targetId: user.userId
}
targetId: user.userId,
},
});
expect(logs.length).toBe(1);
@@ -419,7 +424,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
roleCode: `int_test_role_${Date.now()}`,
description: '集成测试用角色',
permissions: ['read', 'write'],
status: 'active'
status: 'active',
};
const response = await request(app)
@@ -432,8 +437,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'role',
operationType: 'create',
targetId: response.body.roleId
}
targetId: response.body.roleId,
},
});
expect(logs.length).toBe(1);
@@ -450,7 +455,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
roleCode: `old_role_${Date.now()}`,
description: '旧描述',
permissions: ['read'],
status: 'active'
status: 'active',
});
const response = await request(app)
@@ -458,7 +463,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
.set('Authorization', `Bearer ${authToken}`)
.send({
roleName: `新角色名_${Date.now()}`,
permissions: ['read', 'write', 'delete']
permissions: ['read', 'write', 'delete'],
})
.expect(200);
@@ -466,8 +471,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'role',
operationType: 'update',
targetId: role.roleId
}
targetId: role.roleId,
},
});
expect(logs.length).toBe(1);
@@ -484,7 +489,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
roleCode: `del_role_${Date.now()}`,
description: '待删除',
permissions: ['read'],
status: 'active'
status: 'active',
});
await request(app)
@@ -496,8 +501,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'role',
operationType: 'delete',
targetId: role.roleId
}
targetId: role.roleId,
},
});
expect(logs.length).toBe(1);
@@ -515,7 +520,7 @@ describe('设备/用户/角色操作日志集成测试', () => {
position: 40,
height: 2,
powerConsumption: 500,
status: 'running'
status: 'running',
});
await request(app)
@@ -533,8 +538,8 @@ describe('设备/用户/角色操作日志集成测试', () => {
where: {
module: 'device',
operationType: 'update',
targetId: device.deviceId
}
targetId: device.deviceId,
},
});
expect(logs.length).toBe(1);
+19 -19
View File
@@ -36,7 +36,7 @@ describe('OperationLog 模型测试', () => {
operatorId: 'user_001',
operatorName: '测试用户',
operatorRole: '管理员',
result: 'success'
result: 'success',
};
const log = await OperationLog.create(logData);
@@ -69,7 +69,7 @@ describe('OperationLog 模型测试', () => {
operatorName: '测试用户',
beforeState,
afterState,
result: 'success'
result: 'success',
});
expect(log.beforeState).toEqual(beforeState);
@@ -80,7 +80,7 @@ describe('OperationLog 模型测试', () => {
const metadata = {
count: 5,
source: 'batch_operation',
extraInfo: '额外信息'
extraInfo: '额外信息',
};
const log = await OperationLog.create({
@@ -93,7 +93,7 @@ describe('OperationLog 模型测试', () => {
operatorId: 'user_001',
operatorName: '测试用户',
metadata,
result: 'success'
result: 'success',
});
expect(log.metadata).toEqual(metadata);
@@ -111,7 +111,7 @@ describe('OperationLog 模型测试', () => {
operatorName: '管理员',
ipAddress: '192.168.1.100',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
result: 'success'
result: 'success',
});
expect(log.ipAddress).toBe('192.168.1.100');
@@ -127,7 +127,7 @@ describe('OperationLog 模型测试', () => {
targetId: 'DEV005',
targetName: 'TEST_DEVICE',
operatorId: 'user_001',
operatorName: '测试用户'
operatorName: '测试用户',
});
expect(log.result).toBe('success');
@@ -142,7 +142,7 @@ describe('OperationLog 模型测试', () => {
targetId: 'role_test',
targetName: '测试角色',
operatorId: 'user_001',
operatorName: '测试用户'
operatorName: '测试用户',
});
expect(log.metadata).toEqual({});
@@ -161,7 +161,7 @@ describe('OperationLog 模型测试', () => {
targetName: '设备A',
operatorId: 'user_001',
operatorName: '用户A',
result: 'success'
result: 'success',
},
{
recordId: 'OPLOG_QUERY_002',
@@ -172,7 +172,7 @@ describe('OperationLog 模型测试', () => {
targetName: '设备B',
operatorId: 'user_002',
operatorName: '用户B',
result: 'success'
result: 'success',
},
{
recordId: 'OPLOG_QUERY_003',
@@ -183,7 +183,7 @@ describe('OperationLog 模型测试', () => {
targetName: '用户C',
operatorId: 'user_001',
operatorName: '用户A',
result: 'success'
result: 'success',
},
{
recordId: 'OPLOG_QUERY_004',
@@ -194,35 +194,35 @@ describe('OperationLog 模型测试', () => {
targetName: '设备D',
operatorId: 'user_001',
operatorName: '用户A',
result: 'failed'
}
result: 'failed',
},
]);
});
test('应该能够按 module 查询', async () => {
const deviceLogs = await OperationLog.findAll({
where: { module: 'device' }
where: { module: 'device' },
});
expect(deviceLogs.length).toBe(3);
});
test('应该能够按 operationType 查询', async () => {
const createLogs = await OperationLog.findAll({
where: { operationType: 'create' }
where: { operationType: 'create' },
});
expect(createLogs.length).toBe(2);
});
test('应该能够按 operatorId 查询', async () => {
const user001Logs = await OperationLog.findAll({
where: { operatorId: 'user_001' }
where: { operatorId: 'user_001' },
});
expect(user001Logs.length).toBe(3);
});
test('应该能够按 result 查询', async () => {
const failedLogs = await OperationLog.findAll({
where: { result: 'failed' }
where: { result: 'failed' },
});
expect(failedLogs.length).toBe(1);
});
@@ -231,8 +231,8 @@ describe('OperationLog 模型测试', () => {
const { Op } = require('sequelize');
const logs = await OperationLog.findAll({
where: {
targetId: { [Op.like]: '%DEV%' }
}
targetId: { [Op.like]: '%DEV%' },
},
});
expect(logs.length).toBe(3);
});
@@ -241,7 +241,7 @@ describe('OperationLog 模型测试', () => {
const { count, rows } = await OperationLog.findAndCountAll({
limit: 2,
offset: 0,
order: [['createdAt', 'DESC']]
order: [['createdAt', 'DESC']],
});
expect(count).toBe(4);
expect(rows.length).toBe(2);
+127 -171
View File
@@ -4,7 +4,7 @@ const {
logOperation,
logDeviceOperation,
logUserOperation,
logRoleOperation
logRoleOperation,
} = require('../utils/operationLogger');
describe('operationLogger 工具函数测试', () => {
@@ -22,12 +22,12 @@ describe('operationLogger 工具函数测试', () => {
user: {
userId: 'user_test_001',
realName: '测试用户',
roleName: '管理员'
roleName: '管理员',
},
headers: {
'x-forwarded-for': '192.168.1.100',
'user-agent': 'Mozilla/5.0 Test Browser'
}
'user-agent': 'Mozilla/5.0 Test Browser',
},
};
await logOperation({
@@ -39,11 +39,11 @@ describe('operationLogger 工具函数测试', () => {
beforeState: null,
afterState: { name: '测试设备', status: 'running' },
result: 'success',
req: mockReq
req: mockReq,
});
const logs = await OperationLog.findAll({
where: { targetId: 'DEV_TEST_001' }
where: { targetId: 'DEV_TEST_001' },
});
expect(logs.length).toBe(1);
@@ -63,7 +63,7 @@ describe('operationLogger 工具函数测试', () => {
test('应该能够处理没有用户信息的请求', async () => {
const mockReq = {
user: null,
headers: {}
headers: {},
};
await logOperation({
@@ -73,11 +73,11 @@ describe('operationLogger 工具函数测试', () => {
targetId: 'SYSTEM',
targetName: '系统',
result: 'success',
req: mockReq
req: mockReq,
});
const logs = await OperationLog.findAll({
where: { targetId: 'SYSTEM' }
where: { targetId: 'SYSTEM' },
});
expect(logs.length).toBe(1);
@@ -93,11 +93,11 @@ describe('operationLogger 工具函数测试', () => {
targetId: 'DEV_NO_REQ',
targetName: '无请求设备',
result: 'success',
req: null
req: null,
});
const logs = await OperationLog.findAll({
where: { targetId: 'DEV_NO_REQ' }
where: { targetId: 'DEV_NO_REQ' },
});
expect(logs.length).toBe(1);
@@ -108,7 +108,7 @@ describe('operationLogger 工具函数测试', () => {
test('应该能够记录失败的操作', async () => {
const mockReq = {
user: { userId: 'user_fail', realName: '失败用户' },
headers: {}
headers: {},
};
await logOperation({
@@ -119,11 +119,11 @@ describe('operationLogger 工具函数测试', () => {
targetName: '失败设备',
result: 'failed',
req: mockReq,
metadata: { errorMessage: '设备不存在' }
metadata: { errorMessage: '设备不存在' },
});
const logs = await OperationLog.findAll({
where: { targetId: 'DEV_FAIL' }
where: { targetId: 'DEV_FAIL' },
});
expect(logs.length).toBe(1);
@@ -133,13 +133,13 @@ describe('operationLogger 工具函数测试', () => {
test('应该使用提供的 metadata', async () => {
const mockReq = {
user: { userId: 'user_meta', realName: '元数据用户' },
headers: {}
headers: {},
};
const customMetadata = {
batchCount: 10,
source: 'import',
duration: 5000
duration: 5000,
};
await logOperation({
@@ -150,11 +150,11 @@ describe('operationLogger 工具函数测试', () => {
targetName: '批量设备',
result: 'success',
req: mockReq,
metadata: customMetadata
metadata: customMetadata,
});
const logs = await OperationLog.findAll({
where: { targetId: 'DEV_BATCH' }
where: { targetId: 'DEV_BATCH' },
});
expect(logs[0].metadata).toEqual(customMetadata);
@@ -165,22 +165,18 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录设备创建日志', async () => {
const mockReq = {
user: { userId: 'user_dev', realName: '设备管理员' },
headers: { 'x-forwarded-for': '10.0.0.1' }
headers: { 'x-forwarded-for': '10.0.0.1' },
};
await logDeviceOperation(
'create',
'创建设备 测试服务器 (DEV001)',
{
targetId: 'DEV001',
targetName: '测试服务器',
afterState: { deviceId: 'DEV001', name: '测试服务器', status: 'running' },
req: mockReq
}
);
await logDeviceOperation('create', '创建设备 测试服务器 (DEV001)', {
targetId: 'DEV001',
targetName: '测试服务器',
afterState: { deviceId: 'DEV001', name: '测试服务器', status: 'running' },
req: mockReq,
});
const logs = await OperationLog.findAll({
where: { module: 'device', operationType: 'create' }
where: { module: 'device', operationType: 'create' },
});
expect(logs.length).toBe(1);
@@ -193,26 +189,22 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录设备更新日志并包含状态变更', async () => {
const mockReq = {
user: { userId: 'user_upd', realName: '更新操作员' },
headers: {}
headers: {},
};
const beforeState = { name: '旧名称', status: 'offline' };
const afterState = { name: '新名称', status: 'running' };
await logDeviceOperation(
'update',
'更新设备 DEV002',
{
targetId: 'DEV002',
targetName: 'DEV002',
beforeState,
afterState,
req: mockReq
}
);
await logDeviceOperation('update', '更新设备 DEV002', {
targetId: 'DEV002',
targetName: 'DEV002',
beforeState,
afterState,
req: mockReq,
});
const logs = await OperationLog.findAll({
where: { targetId: 'DEV002', operationType: 'update' }
where: { targetId: 'DEV002', operationType: 'update' },
});
expect(logs.length).toBe(1);
@@ -223,22 +215,18 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录设备删除日志', async () => {
const mockReq = {
user: { userId: 'user_del', realName: '删除操作员' },
headers: {}
headers: {},
};
await logDeviceOperation(
'delete',
'删除设备 DEV003 (测试服务器)',
{
targetId: 'DEV003',
targetName: '测试服务器',
beforeState: { deviceId: 'DEV003', name: '测试服务器' },
req: mockReq
}
);
await logDeviceOperation('delete', '删除设备 DEV003 (测试服务器)', {
targetId: 'DEV003',
targetName: '测试服务器',
beforeState: { deviceId: 'DEV003', name: '测试服务器' },
req: mockReq,
});
const logs = await OperationLog.findAll({
where: { targetId: 'DEV003', operationType: 'delete' }
where: { targetId: 'DEV003', operationType: 'delete' },
});
expect(logs.length).toBe(1);
@@ -247,27 +235,23 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录批量删除日志', async () => {
const mockReq = {
user: { userId: 'user_batch', realName: '批量操作员' },
headers: {}
headers: {},
};
await logDeviceOperation(
'batch_delete',
'批量删除设备 3台 (DEV_A,DEV_B,DEV_C)',
{
targetId: 'DEV_A,DEV_B,DEV_C',
targetName: '3台设备',
beforeState: [
{ deviceId: 'DEV_A', name: '设备A' },
{ deviceId: 'DEV_B', name: '设备B' },
{ deviceId: 'DEV_C', name: '设备C' }
],
req: mockReq,
metadata: { count: 3 }
}
);
await logDeviceOperation('batch_delete', '批量删除设备 3台 (DEV_A,DEV_B,DEV_C)', {
targetId: 'DEV_A,DEV_B,DEV_C',
targetName: '3台设备',
beforeState: [
{ deviceId: 'DEV_A', name: '设备A' },
{ deviceId: 'DEV_B', name: '设备B' },
{ deviceId: 'DEV_C', name: '设备C' },
],
req: mockReq,
metadata: { count: 3 },
});
const logs = await OperationLog.findAll({
where: { operationType: 'batch_delete' }
where: { operationType: 'batch_delete' },
});
expect(logs.length).toBe(1);
@@ -277,30 +261,26 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录状态变更日志', async () => {
const mockReq = {
user: { userId: 'user_status', realName: '状态管理员' },
headers: {}
headers: {},
};
await logDeviceOperation(
'status_change',
'批量变更设备状态为"运行中"',
{
targetId: 'DEV_STATUS_1,DEV_STATUS_2',
targetName: '2台设备',
beforeState: [
{ deviceId: 'DEV_STATUS_1', status: 'offline' },
{ deviceId: 'DEV_STATUS_2', status: 'maintenance' }
],
afterState: [
{ deviceId: 'DEV_STATUS_1', status: 'running' },
{ deviceId: 'DEV_STATUS_2', status: 'running' }
],
req: mockReq,
metadata: { status: 'running', count: 2 }
}
);
await logDeviceOperation('status_change', '批量变更设备状态为"运行中"', {
targetId: 'DEV_STATUS_1,DEV_STATUS_2',
targetName: '2台设备',
beforeState: [
{ deviceId: 'DEV_STATUS_1', status: 'offline' },
{ deviceId: 'DEV_STATUS_2', status: 'maintenance' },
],
afterState: [
{ deviceId: 'DEV_STATUS_1', status: 'running' },
{ deviceId: 'DEV_STATUS_2', status: 'running' },
],
req: mockReq,
metadata: { status: 'running', count: 2 },
});
const logs = await OperationLog.findAll({
where: { operationType: 'status_change' }
where: { operationType: 'status_change' },
});
expect(logs.length).toBe(1);
@@ -312,23 +292,19 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录用户创建日志', async () => {
const mockReq = {
user: { userId: 'admin', realName: '系统管理员' },
headers: {}
headers: {},
};
await logUserOperation(
'create',
'创建用户 new_user',
{
targetId: 'user_new',
targetName: 'new_user',
afterState: { username: 'new_user', email: 'new@example.com' },
req: mockReq,
metadata: { roleIds: ['role_admin'] }
}
);
await logUserOperation('create', '创建用户 new_user', {
targetId: 'user_new',
targetName: 'new_user',
afterState: { username: 'new_user', email: 'new@example.com' },
req: mockReq,
metadata: { roleIds: ['role_admin'] },
});
const logs = await OperationLog.findAll({
where: { module: 'user', operationType: 'create' }
where: { module: 'user', operationType: 'create' },
});
expect(logs.length).toBe(1);
@@ -339,24 +315,20 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录权限变更日志', async () => {
const mockReq = {
user: { userId: 'admin', realName: '管理员' },
headers: {}
headers: {},
};
await logUserOperation(
'permission_change',
'变更用户 test_user 的角色',
{
targetId: 'user_test',
targetName: 'test_user',
beforeState: { roleIds: ['role_viewer'] },
afterState: { roleIds: ['role_admin'] },
req: mockReq,
metadata: { oldRoleIds: ['role_viewer'], newRoleIds: ['role_admin'] }
}
);
await logUserOperation('permission_change', '变更用户 test_user 的角色', {
targetId: 'user_test',
targetName: 'test_user',
beforeState: { roleIds: ['role_viewer'] },
afterState: { roleIds: ['role_admin'] },
req: mockReq,
metadata: { oldRoleIds: ['role_viewer'], newRoleIds: ['role_admin'] },
});
const logs = await OperationLog.findAll({
where: { operationType: 'permission_change' }
where: { operationType: 'permission_change' },
});
expect(logs.length).toBe(1);
@@ -367,22 +339,18 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录用户删除日志', async () => {
const mockReq = {
user: { userId: 'admin', realName: '管理员' },
headers: {}
headers: {},
};
await logUserOperation(
'delete',
'删除用户 deleted_user',
{
targetId: 'user_deleted',
targetName: 'deleted_user',
beforeState: { username: 'deleted_user', email: 'deleted@example.com' },
req: mockReq
}
);
await logUserOperation('delete', '删除用户 deleted_user', {
targetId: 'user_deleted',
targetName: 'deleted_user',
beforeState: { username: 'deleted_user', email: 'deleted@example.com' },
req: mockReq,
});
const logs = await OperationLog.findAll({
where: { module: 'user', operationType: 'delete' }
where: { module: 'user', operationType: 'delete' },
});
expect(logs.length).toBe(1);
@@ -393,23 +361,19 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录角色创建日志', async () => {
const mockReq = {
user: { userId: 'admin', realName: '管理员' },
headers: {}
headers: {},
};
await logRoleOperation(
'create',
'创建角色 测试角色',
{
targetId: 'role_test',
targetName: '测试角色',
afterState: { roleName: '测试角色', permissions: ['read', 'write'] },
req: mockReq,
metadata: { roleCode: 'test_role', permissions: ['read', 'write'] }
}
);
await logRoleOperation('create', '创建角色 测试角色', {
targetId: 'role_test',
targetName: '测试角色',
afterState: { roleName: '测试角色', permissions: ['read', 'write'] },
req: mockReq,
metadata: { roleCode: 'test_role', permissions: ['read', 'write'] },
});
const logs = await OperationLog.findAll({
where: { module: 'role', operationType: 'create' }
where: { module: 'role', operationType: 'create' },
});
expect(logs.length).toBe(1);
@@ -420,27 +384,23 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录角色更新日志', async () => {
const mockReq = {
user: { userId: 'admin', realName: '管理员' },
headers: {}
headers: {},
};
const beforeState = { roleName: '旧角色', permissions: ['read'] };
const afterState = { roleName: '新角色', permissions: ['read', 'write', 'delete'] };
await logRoleOperation(
'update',
'更新角色 角色A',
{
targetId: 'role_a',
targetName: '角色A',
beforeState,
afterState,
req: mockReq,
metadata: { oldRoleName: '旧角色', oldPermissions: ['read'] }
}
);
await logRoleOperation('update', '更新角色 角色A', {
targetId: 'role_a',
targetName: '角色A',
beforeState,
afterState,
req: mockReq,
metadata: { oldRoleName: '旧角色', oldPermissions: ['read'] },
});
const logs = await OperationLog.findAll({
where: { module: 'role', operationType: 'update' }
where: { module: 'role', operationType: 'update' },
});
expect(logs.length).toBe(1);
@@ -451,23 +411,19 @@ describe('operationLogger 工具函数测试', () => {
test('应该记录角色删除日志', async () => {
const mockReq = {
user: { userId: 'admin', realName: '管理员' },
headers: {}
headers: {},
};
await logRoleOperation(
'delete',
'删除角色 测试角色B',
{
targetId: 'role_b',
targetName: '测试角色B',
beforeState: { roleName: '测试角色B', userCount: 0 },
req: mockReq,
metadata: { userCount: 0 }
}
);
await logRoleOperation('delete', '删除角色 测试角色B', {
targetId: 'role_b',
targetName: '测试角色B',
beforeState: { roleName: '测试角色B', userCount: 0 },
req: mockReq,
metadata: { userCount: 0 },
});
const logs = await OperationLog.findAll({
where: { module: 'role', operationType: 'delete' }
where: { module: 'role', operationType: 'delete' },
});
expect(logs.length).toBe(1);
+34 -22
View File
@@ -44,7 +44,7 @@ const createTestApp = () => {
keyword,
startDate,
endDate,
result
result,
} = req.query;
const offset = (parseInt(page) - 1) * parseInt(pageSize);
@@ -72,7 +72,7 @@ const createTestApp = () => {
where[Op.or] = [
{ operationDescription: { [Op.like]: `%${keyword}%` } },
{ targetName: { [Op.like]: `%${keyword}%` } },
{ operatorName: { [Op.like]: `%${keyword}%` } }
{ operatorName: { [Op.like]: `%${keyword}%` } },
];
}
@@ -96,7 +96,7 @@ const createTestApp = () => {
where,
order: [['createdAt', 'DESC']],
offset,
limit
limit,
});
res.json({
@@ -105,14 +105,14 @@ const createTestApp = () => {
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize),
logs
}
logs,
},
});
} catch (error) {
console.error('获取操作日志失败:', error);
res.status(500).json({
success: false,
message: '获取操作日志失败'
message: '获取操作日志失败',
});
}
});
@@ -124,19 +124,19 @@ const createTestApp = () => {
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: '获取操作日志详情失败',
});
}
});
@@ -153,7 +153,7 @@ describe('OperationLogs API 路由测试', () => {
userId: 'test_user_001',
username: 'testuser',
realName: '测试用户',
roleName: '管理员'
roleName: '管理员',
};
beforeAll(async () => {
@@ -181,16 +181,14 @@ describe('OperationLogs API 路由测试', () => {
operatorId: testUser.userId,
operatorName: testUser.realName,
operatorRole: testUser.roleName,
result: 'success'
result: 'success',
};
return await OperationLog.create({ ...defaultData, ...data });
};
describe('GET /api/operation-logs', () => {
test('未授权访问应该返回 401', async () => {
const response = await request(app)
.get('/api/operation-logs')
.expect(401);
const response = await request(app).get('/api/operation-logs').expect(401);
expect(response.body.success).toBe(false);
});
@@ -258,9 +256,21 @@ describe('OperationLogs API 路由测试', () => {
});
test('应该支持按 keyword 搜索', async () => {
await createTestLog({ operationDescription: '创建设备 SERVER_A', recordId: 'OPLOG_KW_1', targetName: '服务器A' });
await createTestLog({ operationDescription: '更新设备 SERVER_B', recordId: 'OPLOG_KW_2', targetName: '服务器B' });
await createTestLog({ operationDescription: '删除用户 USER_C', recordId: 'OPLOG_KW_3', targetName: '用户C' });
await createTestLog({
operationDescription: '创建设备 SERVER_A',
recordId: 'OPLOG_KW_1',
targetName: '服务器A',
});
await createTestLog({
operationDescription: '更新设备 SERVER_B',
recordId: 'OPLOG_KW_2',
targetName: '服务器B',
});
await createTestLog({
operationDescription: '删除用户 USER_C',
recordId: 'OPLOG_KW_3',
targetName: '用户C',
});
const response = await request(app)
.get('/api/operation-logs')
@@ -269,7 +279,9 @@ describe('OperationLogs API 路由测试', () => {
.expect(200);
expect(response.body.data.logs).toHaveLength(2);
expect(response.body.data.logs.every(log => log.operationDescription.includes('SERVER'))).toBe(true);
expect(
response.body.data.logs.every(log => log.operationDescription.includes('SERVER'))
).toBe(true);
});
test('应该支持按 result 筛选', async () => {
@@ -291,19 +303,19 @@ describe('OperationLogs API 路由测试', () => {
module: 'device',
operationType: 'create',
result: 'success',
recordId: 'OPLOG_COMB_1'
recordId: 'OPLOG_COMB_1',
});
await createTestLog({
module: 'device',
operationType: 'update',
result: 'success',
recordId: 'OPLOG_COMB_2'
recordId: 'OPLOG_COMB_2',
});
await createTestLog({
module: 'user',
operationType: 'create',
result: 'success',
recordId: 'OPLOG_COMB_3'
recordId: 'OPLOG_COMB_3',
});
const response = await request(app)
@@ -324,7 +336,7 @@ describe('OperationLogs API 路由测试', () => {
recordId: 'OPLOG_DETAIL_001',
beforeState: { name: '旧名称' },
afterState: { name: '新名称' },
metadata: { customField: '自定义值' }
metadata: { customField: '自定义值' },
});
const response = await request(app)
+6 -6
View File
@@ -10,12 +10,12 @@ async function unlockUser(username = null) {
if (username) {
// 解锁指定用户
users = await User.findAll({
where: {
where: {
username,
status: 'locked'
}
status: 'locked',
},
});
if (users.length === 0) {
console.log(`未找到被锁定的用户: ${username}`);
const user = await User.findOne({ where: { username } });
@@ -28,9 +28,9 @@ async function unlockUser(username = null) {
} else {
// 解锁所有被锁定的用户
users = await User.findAll({
where: { status: 'locked' }
where: { status: 'locked' },
});
if (users.length === 0) {
console.log('没有被锁定的用户');
await sequelize.close();
+123 -117
View File
@@ -1,4 +1,3 @@
const cron = require('node-cron');
const path = require('path');
const fs = require('fs');
@@ -61,15 +60,15 @@ function calculateNextRun(cronExpression) {
const parts = cronExpression.split(' ');
const minute = parseInt(parts[0]) || 0;
const hour = parseInt(parts[1]) || 0;
const now = new Date();
const next = new Date(now);
next.setHours(hour, minute, 0, 0);
if (next <= now) {
next.setDate(next.getDate() + 1);
}
return next.toLocaleString('zh-CN');
} catch (error) {
return '计算失败';
@@ -89,7 +88,8 @@ function getFileSize(filePath) {
}
function createAutoBackupTask(settings) {
const { cronExpression, description, backupType, includeFiles, compress, maxCount, maxAgeDays } = settings;
const { cronExpression, description, backupType, includeFiles, compress, maxCount, maxAgeDays } =
settings;
if (!validateCronExpression(cronExpression)) {
throw new Error('无效的 Cron 表达式');
@@ -106,86 +106,91 @@ function createAutoBackupTask(settings) {
}
console.log('创建新调度器...');
const task = cron.schedule(cronExpression, async function() {
console.log('');
console.log('============================================');
console.log('=== 自动备份任务触发 ===');
console.log('触发时间:', new Date().toLocaleString('zh-CN'));
console.log('============================================');
let logId = null;
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
console.log('创建备份日志...');
const log = await createLogEntry({
logType: 'auto',
description: `${description} - ${timestamp}`,
backupType: backupType,
includeFiles: includeFiles,
compressed: compress
});
logId = log ? log.id : null;
if (logId) {
await updateLogStatus(logId, 'running');
}
console.log('准备执行备份...');
const backupFunction = backupType === 'incremental' ? createIncrementalBackup : createBackup;
const result = await backupFunction({
description: `${description} - ${timestamp}`,
includeFiles,
compress,
autoClean: true,
maxCount,
maxAgeDays,
});
const task = cron.schedule(
cronExpression,
async function () {
console.log('');
console.log('============================================');
console.log('=== 自动备份任务触发 ===');
console.log('触发时间:', new Date().toLocaleString('zh-CN'));
console.log('============================================');
if (result) {
console.log('自动备份完成:', result.filename);
console.log('备份类型:', result.isIncremental ? '增量备份' : '全量备份');
const fileSize = getFileSize(result.path);
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
if (logId) {
await updateLogStatus(logId, 'success', {
filename: result.filename,
filePath: result.path,
fileSize: fileSize,
remoteUploads: uploadResults
});
}
console.log('============================================\n');
} else {
console.log('无数据变化,跳过备份');
if (logId) {
await updateLogStatus(logId, 'success', {
errorMessage: '无数据变化,跳过备份'
});
}
console.log('============================================\n');
}
} catch (error) {
console.error('自动备份失败:', error);
console.error('错误堆栈:', error.stack);
if (logId) {
await updateLogStatus(logId, 'failed', {
errorMessage: error.message || '未知错误'
let logId = null;
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
console.log('创建备份日志...');
const log = await createLogEntry({
logType: 'auto',
description: `${description} - ${timestamp}`,
backupType: backupType,
includeFiles: includeFiles,
compressed: compress,
});
logId = log ? log.id : null;
if (logId) {
await updateLogStatus(logId, 'running');
}
console.log('准备执行备份...');
const backupFunction =
backupType === 'incremental' ? createIncrementalBackup : createBackup;
const result = await backupFunction({
description: `${description} - ${timestamp}`,
includeFiles,
compress,
autoClean: true,
maxCount,
maxAgeDays,
});
if (result) {
console.log('自动备份完成:', result.filename);
console.log('备份类型:', result.isIncremental ? '增量备份' : '全量备份');
const fileSize = getFileSize(result.path);
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
if (logId) {
await updateLogStatus(logId, 'success', {
filename: result.filename,
filePath: result.path,
fileSize: fileSize,
remoteUploads: uploadResults,
});
}
console.log('============================================\n');
} else {
console.log('无数据变化,跳过备份');
if (logId) {
await updateLogStatus(logId, 'success', {
errorMessage: '无数据变化,跳过备份',
});
}
console.log('============================================\n');
}
} catch (error) {
console.error('自动备份失败:', error);
console.error('错误堆栈:', error.stack);
if (logId) {
await updateLogStatus(logId, 'failed', {
errorMessage: error.message || '未知错误',
});
}
console.error('============================================\n');
}
console.error('============================================\n');
},
{
timezone: 'Asia/Shanghai',
}
}, {
timezone: 'Asia/Shanghai'
});
);
schedulers.set('auto-backup', task);
console.log('自动备份任务已成功创建并启动');
@@ -231,14 +236,14 @@ function getAutoBackupStatus() {
if (isActive && settings.enabled) {
const now = new Date();
const [minute, hour] = settings.cronExpression.split(' ').slice(0, 2);
const next = new Date(now);
next.setHours(parseInt(hour), parseInt(minute), 0, 0);
if (next <= now) {
next.setDate(next.getDate() + 1);
}
nextRun = next.toISOString();
}
@@ -284,29 +289,29 @@ function updateAutoBackupSettings(newSettings) {
async function uploadToRemoteTargets(localFilePath, filename) {
const globalSettings = getGlobalSettings();
if (!globalSettings.enabled || !globalSettings.uploadAfterBackup) {
console.log('远端备份已禁用');
return [];
}
const enabledTargets = getEnabledTargets();
if (enabledTargets.length === 0) {
console.log('没有启用的远端备份目标');
return [];
}
const uploadResults = [];
for (const target of enabledTargets) {
try {
console.log('开始上传到目标:' + target.name + ' (' + target.protocol + ')');
const remotePath = (target.prefix || 'backups/') + filename;
const result = await uploadToRemote(target, localFilePath, remotePath);
uploadResults.push({
targetId: target.id,
targetName: target.name,
@@ -314,7 +319,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
success: true,
...result,
});
console.log('上传到 ' + target.name + ' 成功');
} catch (error) {
console.error('上传到 ' + target.name + ' 失败:', error.message);
@@ -327,7 +332,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
});
}
}
const settings = getGlobalSettings();
if (settings.deleteLocalAfterUpload && uploadResults.every(r => r.success)) {
try {
@@ -337,7 +342,7 @@ async function uploadToRemoteTargets(localFilePath, filename) {
console.error('删除本地备份文件失败:', error.message);
}
}
return uploadResults;
}
@@ -346,31 +351,33 @@ async function executeBackupNow(options = {}) {
console.log('============================================');
console.log('=== 手动触发备份 ===');
console.log('============================================');
let logId = null;
try {
const settings = loadSettings();
const backupType = options.backupType || settings.backupType || 'full';
console.log('创建备份日志...');
const log = await createLogEntry({
logType: 'manual',
description: options.description || '手动备份',
backupType: backupType,
includeFiles: options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
compressed: options.compress !== undefined ? options.compress : settings.compress
includeFiles:
options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
compressed: options.compress !== undefined ? options.compress : settings.compress,
});
logId = log ? log.id : null;
if (logId) {
await updateLogStatus(logId, 'running');
}
const backupFunction = backupType === 'incremental' ? createIncrementalBackup : createBackup;
const result = await backupFunction({
description: options.description || '手动备份',
includeFiles: options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
includeFiles:
options.includeFiles !== undefined ? options.includeFiles : settings.includeFiles,
compress: options.compress !== undefined ? options.compress : settings.compress,
autoClean: true,
maxCount: settings.maxCount,
@@ -378,36 +385,36 @@ async function executeBackupNow(options = {}) {
});
console.log('手动备份完成:', result.filename);
const fileSize = getFileSize(result.path);
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
if (logId) {
await updateLogStatus(logId, 'success', {
filename: result.filename,
filePath: result.path,
fileSize: fileSize,
remoteUploads: uploadResults
remoteUploads: uploadResults,
});
}
console.log('============================================\n');
return {
success: true,
return {
success: true,
result,
remoteUploads: uploadResults,
};
} catch (error) {
console.error('手动备份失败:', error);
console.error('============================================\n');
if (logId) {
await updateLogStatus(logId, 'failed', {
errorMessage: error.message || '未知错误'
errorMessage: error.message || '未知错误',
});
}
return { success: false, error: error.message };
}
}
@@ -417,14 +424,14 @@ function initAutoBackup() {
console.log('============================================');
console.log('=== 初始化自动备份调度器 ===');
console.log('============================================');
const settings = loadSettings();
console.log('当前设置:');
console.log(' 启用:', settings.enabled ? '是' : '否');
console.log(' Cron表达式:', settings.cronExpression);
console.log(' 备份类型:', settings.backupType);
if (settings.enabled) {
startAutoBackup(settings);
} else {
@@ -447,4 +454,3 @@ module.exports = {
executeBackupNow,
initAutoBackup,
};
+105 -84
View File
@@ -30,31 +30,31 @@ async function enableForeignKeyChecks() {
// 数据表名称中英文映射
const TABLE_NAME_MAPPING = {
'User': '用户',
'Role': '角色',
'UserRole': '用户角色关联',
'Permission': '权限',
'Room': '机房',
'Rack': '机柜',
'Device': '设备',
'DeviceField': '设备自定义字段',
'DevicePort': '设备端口',
'NetworkCard': '网卡',
'Cable': '线缆',
'PendingDevice': '待入库设备',
'FaultCategory': '故障分类',
'Ticket': '工单',
'TicketField': '工单自定义字段',
'TicketOperationRecord': '工单操作记录',
'ConsumableCategory': '耗材分类',
'Consumable': '耗材',
'ConsumableRecord': '耗材记录',
'ConsumableLog': '耗材操作日志',
'ConsumableLogArchive': '耗材操作日志归档',
'InventoryPlan': '盘点计划',
'InventoryTask': '盘点任务',
'InventoryRecord': '盘点记录',
'SystemSetting': '系统设置',
User: '用户',
Role: '角色',
UserRole: '用户角色关联',
Permission: '权限',
Room: '机房',
Rack: '机柜',
Device: '设备',
DeviceField: '设备自定义字段',
DevicePort: '设备端口',
NetworkCard: '网卡',
Cable: '线缆',
PendingDevice: '待入库设备',
FaultCategory: '故障分类',
Ticket: '工单',
TicketField: '工单自定义字段',
TicketOperationRecord: '工单操作记录',
ConsumableCategory: '耗材分类',
Consumable: '耗材',
ConsumableRecord: '耗材记录',
ConsumableLog: '耗材操作日志',
ConsumableLogArchive: '耗材操作日志归档',
InventoryPlan: '盘点计划',
InventoryTask: '盘点任务',
InventoryRecord: '盘点记录',
SystemSetting: '系统设置',
};
// 增量备份配置
@@ -161,8 +161,13 @@ function getBackupPath() {
*/
function getLastBackupTime() {
const backupDir = getBackupPath();
const files = fs.readdirSync(backupDir)
.filter(f => (f.startsWith('backup_') || f.startsWith('incremental_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
const files = fs
.readdirSync(backupDir)
.filter(
f =>
(f.startsWith('backup_') || f.startsWith('incremental_')) &&
(f.endsWith('.json') || f.endsWith('.json.gz'))
)
.map(f => {
const filePath = path.join(backupDir, f);
return {
@@ -193,7 +198,7 @@ async function collectIncrementalData(lastBackupTime) {
try {
const Model = require(config.modelPath);
// 查询自上次备份以来新增或更新的记录
const newRecords = await Model.findAll({
where: {
@@ -246,7 +251,7 @@ async function collectAllData(tableNames = null) {
const data = {};
let totalRecords = 0;
const configsToProcess = tableNames
const configsToProcess = tableNames
? BACKUP_MODELS_CONFIG.filter(c => tableNames.includes(c.name))
: BACKUP_MODELS_CONFIG;
@@ -397,7 +402,9 @@ async function createBackup(options = {}) {
console.log('\n检查旧备份文件...');
cleanResult = cleanOldBackups({ maxCount, maxAgeDays });
if (cleanResult.deletedCount > 0) {
console.log(`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`);
console.log(
`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`
);
} else {
console.log('无需清理旧备份');
}
@@ -440,10 +447,11 @@ async function createIncrementalBackup(options = {}) {
}
console.log(`开始增量备份(上次备份时间:${lastBackupTime.toISOString()})...`);
// 收集增量数据
const { data: incrementalData, totalChangedRecords } = await collectIncrementalData(lastBackupTime);
const { data: incrementalData, totalChangedRecords } =
await collectIncrementalData(lastBackupTime);
if (totalChangedRecords === 0) {
console.log('自上次备份以来没有数据变化,跳过备份');
return null;
@@ -520,7 +528,9 @@ async function createIncrementalBackup(options = {}) {
console.log('\n检查旧备份文件...');
cleanResult = cleanOldBackups({ maxCount, maxAgeDays });
if (cleanResult.deletedCount > 0) {
console.log(`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`);
console.log(
`已清理 ${cleanResult.deletedCount} 个旧备份,释放 ${cleanResult.freedSizeFormatted}`
);
} else {
console.log('无需清理旧备份');
}
@@ -541,7 +551,7 @@ async function createIncrementalBackup(options = {}) {
async function validateBackupFile(filePath, options = {}) {
const { isCompressed: forceCompressed } = options;
if (!fs.existsSync(filePath)) {
return { valid: false, error: '备份文件不存在' };
}
@@ -550,7 +560,7 @@ async function validateBackupFile(filePath, options = {}) {
try {
const buffer = fs.readFileSync(filePath);
const isCompressed = forceCompressed !== undefined ? forceCompressed : filePath.endsWith('.gz');
if (isCompressed) {
const decompressed = zlib.gunzipSync(buffer);
backupData = JSON.parse(decompressed.toString('utf8'));
@@ -638,28 +648,32 @@ async function validateBackupFile(filePath, options = {}) {
avatars: backupData.files?.avatars?.length || 0,
others: backupData.files?.others?.length || 0,
total: (backupData.files?.avatars?.length || 0) + (backupData.files?.others?.length || 0),
avatarList: backupData.files?.avatars?.map(f => ({
filename: f.filename,
size: f.size,
})) || [],
otherList: backupData.files?.others?.map(f => ({
filename: f.filename,
size: f.size,
})) || [],
avatarList:
backupData.files?.avatars?.map(f => ({
filename: f.filename,
size: f.size,
})) || [],
otherList:
backupData.files?.others?.map(f => ({
filename: f.filename,
size: f.size,
})) || [],
};
const metadata = isIncremental ? {
tableCount: Object.keys(backupData.fullData || {}).length,
incrementalTableCount: Object.keys(backupData.incrementalData || {}).length,
totalRecords,
totalChangedRecords: backupData.metadata?.totalChangedRecords || totalRecords,
fileCount: fileDetails.total,
lastBackupTime: backupData.lastBackupTime,
} : {
tableCount: Object.keys(backupData.data).length,
totalRecords,
fileCount: fileDetails.total,
};
const metadata = isIncremental
? {
tableCount: Object.keys(backupData.fullData || {}).length,
incrementalTableCount: Object.keys(backupData.incrementalData || {}).length,
totalRecords,
totalChangedRecords: backupData.metadata?.totalChangedRecords || totalRecords,
fileCount: fileDetails.total,
lastBackupTime: backupData.lastBackupTime,
}
: {
tableCount: Object.keys(backupData.data).length,
totalRecords,
fileCount: fileDetails.total,
};
return {
valid: true,
@@ -679,11 +693,7 @@ async function validateBackupFile(filePath, options = {}) {
}
async function restoreData(backupData, options = {}) {
const {
overwriteExisting = true,
skipTables = [],
onProgress = () => {},
} = options;
const { overwriteExisting = true, skipTables = [], onProgress = () => {} } = options;
const results = {
tablesRestored: 0,
@@ -725,25 +735,31 @@ async function restoreData(backupData, options = {}) {
try {
const Model = require(config.modelPath);
if (overwriteExisting) {
await Model.destroy({ where: {}, truncate: true });
}
const processedRecords = tableData.map(record => {
const processed = { ...record };
if (tableName === 'Device' && processed.customFields !== undefined && processed.customFields !== null) {
if (
tableName === 'Device' &&
processed.customFields !== undefined &&
processed.customFields !== null
) {
if (typeof processed.customFields === 'string') {
try {
processed.customFields = JSON.parse(processed.customFields);
} catch (e) {
console.warn(`解析 Device.customFields 失败:${processed.deviceId}, 错误:${e.message}`);
console.warn(
`解析 Device.customFields 失败:${processed.deviceId}, 错误:${e.message}`
);
processed.customFields = {};
}
}
}
return processed;
});
@@ -776,13 +792,13 @@ async function restoreData(backupData, options = {}) {
results.tablesRestored++;
results.recordsRestored += insertedCount;
results.tableDetails[tableName] = {
recordCount: insertedCount,
displayName: TABLE_NAME_MAPPING[tableName] || tableName,
success: insertedCount > 0,
};
onProgress(tableName, 'restored', insertedCount);
} catch (error) {
results.errors.push({ table: tableName, error: error.message });
@@ -816,7 +832,7 @@ async function restoreIncrementalData(incrementalData, options = {}) {
for (const tableName of Object.keys(incrementalData)) {
const tableIncrement = incrementalData[tableName];
const config = BACKUP_MODELS_CONFIG.find(c => c.name === tableName);
if (!config) {
results.errors.push({ table: tableName, error: '未找到模型配置' });
continue;
@@ -947,7 +963,7 @@ async function restoreBackup(filePath, options = {}) {
console.log('\n读取备份数据...');
const buffer = fs.readFileSync(filePath);
const isCompressed = filePath.endsWith('.gz');
let backupData;
if (isCompressed) {
console.log('解压备份文件...');
@@ -992,31 +1008,34 @@ async function restoreBackup(filePath, options = {}) {
filesRestored: fileResults.filesRestored,
errors: dataResults.errors,
tableDetails: dataResults.tableDetails, // 每个表的详细恢复信息
fileDetails: backupData.files ? {
avatars: backupData.files.avatars?.length || 0,
others: backupData.files.others?.length || 0,
} : null,
fileDetails: backupData.files
? {
avatars: backupData.files.avatars?.length || 0,
others: backupData.files.others?.length || 0,
}
: null,
};
}
function cleanOldBackups(options = {}) {
const {
maxCount = 30,
maxAgeDays = 90,
dryRun = false,
} = options;
const { maxCount = 30, maxAgeDays = 90, dryRun = false } = options;
const backupPath = getBackupPath();
if (!fs.existsSync(backupPath)) {
return { deleted: [], kept: [], totalSize: 0, freedSize: 0 };
}
const now = Date.now();
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
const files = fs.readdirSync(backupPath)
.filter(f => (f.startsWith('backup_') || f.startsWith('uploaded_') || f.startsWith('incremental_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
const files = fs
.readdirSync(backupPath)
.filter(
f =>
(f.startsWith('backup_') || f.startsWith('uploaded_') || f.startsWith('incremental_')) &&
(f.endsWith('.json') || f.endsWith('.json.gz'))
)
.map(f => {
const filePath = path.join(backupPath, f);
const stats = fs.statSync(filePath);
@@ -1070,7 +1089,9 @@ function cleanOldBackups(options = {}) {
}
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));
+42 -29
View File
@@ -1,10 +1,9 @@
const BackupLog = require('../models/BackupLog');
const fs = require('fs');
async function createLogEntry(options) {
const { logType, description, backupType, includeFiles, compressed } = options;
try {
const log = await BackupLog.create({
logType: logType || 'manual',
@@ -13,7 +12,7 @@ async function createLogEntry(options) {
backupType: backupType || 'full',
includeFiles: includeFiles || false,
compressed: compressed || false,
startTime: new Date()
startTime: new Date(),
});
return log;
} catch (error) {
@@ -25,30 +24,40 @@ async function createLogEntry(options) {
async function updateLogStatus(logId, status, options = {}) {
try {
const updateData = { status };
if (status === 'running') {
updateData.startTime = new Date();
}
if (status === 'success' || status === 'failed') {
updateData.endTime = new Date();
const log = await BackupLog.findByPk(logId);
if (log && log.startTime) {
updateData.duration = new Date() - new Date(log.startTime);
}
}
if (options.filename) updateData.filename = options.filename;
if (options.filePath) updateData.filePath = options.filePath;
if (options.fileSize) updateData.fileSize = options.fileSize;
if (options.errorMessage) updateData.errorMessage = options.errorMessage;
if (options.remoteUploads) updateData.remoteUploads = options.remoteUploads;
if (options.filename) {
updateData.filename = options.filename;
}
if (options.filePath) {
updateData.filePath = options.filePath;
}
if (options.fileSize) {
updateData.fileSize = options.fileSize;
}
if (options.errorMessage) {
updateData.errorMessage = options.errorMessage;
}
if (options.remoteUploads) {
updateData.remoteUploads = options.remoteUploads;
}
await BackupLog.update(updateData, {
where: { id: logId }
where: { id: logId },
});
return true;
} catch (error) {
console.error('更新备份日志失败:', error);
@@ -60,25 +69,29 @@ async function getBackupLogs(options = {}) {
try {
const { page = 1, pageSize = 20, logType, status } = options;
const where = {};
if (logType) where.logType = logType;
if (status) where.status = status;
if (logType) {
where.logType = logType;
}
if (status) {
where.status = status;
}
const offset = (page - 1) * pageSize;
const { count, rows } = await BackupLog.findAndCountAll({
where,
order: [['createdAt', 'DESC']],
limit: pageSize,
offset
offset,
});
return {
logs: rows,
total: count,
page,
pageSize,
totalPages: Math.ceil(count / pageSize)
totalPages: Math.ceil(count / pageSize),
};
} catch (error) {
console.error('获取备份日志失败:', error);
@@ -99,15 +112,15 @@ async function deleteOldLogs(days = 30) {
try {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const deletedCount = await BackupLog.destroy({
where: {
createdAt: {
[require('sequelize').Op.lt]: cutoffDate
}
}
[require('sequelize').Op.lt]: cutoffDate,
},
},
});
console.log(`删除了 ${deletedCount} 条旧备份日志`);
return deletedCount;
} catch (error) {
@@ -121,5 +134,5 @@ module.exports = {
updateLogStatus,
getBackupLogs,
getBackupLogById,
deleteOldLogs
deleteOldLogs,
};
+36 -25
View File
@@ -10,25 +10,30 @@ const ensureLogDir = () => {
}
};
const formatLogEntry = (entry) => {
const formatLogEntry = entry => {
const timestamp = new Date().toISOString();
return JSON.stringify({
timestamp,
...entry,
}) + '\n';
return (
JSON.stringify({
timestamp,
...entry,
}) + '\n'
);
};
const logDangerousOperation = async (req, {
operationType,
operationName,
targetType,
targetId,
targetName,
beforeState,
metadata = {},
success = true,
errorMessage = null,
}) => {
const logDangerousOperation = async (
req,
{
operationType,
operationName,
targetType,
targetId,
targetName,
beforeState,
metadata = {},
success = true,
errorMessage = null,
}
) => {
ensureLogDir();
const clientIp = req?.ip || req?.connection?.remoteAddress || 'unknown';
@@ -57,7 +62,9 @@ const logDangerousOperation = async (req, {
try {
fs.appendFileSync(DANGEROUS_OPERATIONS_LOG, formatLogEntry(logEntry));
console.log(`[DANGEROUS-OP] ${logEntry.operationName} by ${logEntry.username} - ${success ? 'SUCCESS' : 'FAILED'}`);
console.log(
`[DANGEROUS-OP] ${logEntry.operationName} by ${logEntry.username} - ${success ? 'SUCCESS' : 'FAILED'}`
);
} catch (error) {
console.error('Failed to write dangerous operation log:', error);
}
@@ -74,13 +81,15 @@ const getDangerousOperationsLogs = (filters = {}) => {
const content = fs.readFileSync(DANGEROUS_OPERATIONS_LOG, 'utf-8');
const lines = content.split('\n').filter(line => line.trim());
let logs = lines.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
}).filter(log => log !== null);
let logs = lines
.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
})
.filter(log => log !== null);
if (filters.operationType) {
logs = logs.filter(log => log.operationType === filters.operationType);
@@ -103,7 +112,9 @@ const getDangerousOperationsLogs = (filters = {}) => {
}
if (filters.username) {
logs = logs.filter(log => log.username?.toLowerCase().includes(filters.username.toLowerCase()));
logs = logs.filter(log =>
log.username?.toLowerCase().includes(filters.username.toLowerCase())
);
}
if (filters.riskLevel) {
+30 -22
View File
@@ -4,7 +4,7 @@ const checkDatabase = async () => {
const result = {
status: 'ok',
type: dbDialect,
message: '数据库连接正常'
message: '数据库连接正常',
};
try {
@@ -35,19 +35,19 @@ const checkCriticalConfig = () => {
checks.push({
key: 'JWT_SECRET',
status: 'error',
message: 'JWT_SECRET 未配置'
message: 'JWT_SECRET 未配置',
});
} else if (jwtSecret.length < 32) {
checks.push({
key: 'JWT_SECRET',
status: 'warning',
message: 'JWT_SECRET 长度不足,建议至少 32 字符'
message: 'JWT_SECRET 长度不足,建议至少 32 字符',
});
} else {
checks.push({
key: 'JWT_SECRET',
status: 'ok',
message: 'JWT_SECRET 已配置'
message: 'JWT_SECRET 已配置',
});
}
@@ -55,14 +55,14 @@ const checkCriticalConfig = () => {
checks.push({
key: 'PORT',
status: port ? 'ok' : 'warning',
message: port ? `服务端口: ${port}` : '使用默认端口 8000'
message: port ? `服务端口: ${port}` : '使用默认端口 8000',
});
const dbType = process.env.DB_TYPE || 'sqlite';
checks.push({
key: 'DB_TYPE',
status: 'ok',
message: `数据库类型: ${dbType}`
message: `数据库类型: ${dbType}`,
});
if (dbType === 'mysql') {
@@ -72,7 +72,7 @@ const checkCriticalConfig = () => {
checks.push({
key: 'MYSQL_CONFIG',
status: 'warning',
message: 'MySQL 配置不完整'
message: 'MySQL 配置不完整',
});
}
}
@@ -85,7 +85,7 @@ const checkCriticalConfig = () => {
return {
status: overallStatus,
checks
checks,
};
};
@@ -93,17 +93,25 @@ const getSystemInfo = () => {
const memUsage = process.memoryUsage();
const uptime = process.uptime();
const formatUptime = (seconds) => {
const formatUptime = seconds => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
const parts = [];
if (days > 0) parts.push(`${days}`);
if (hours > 0) parts.push(`${hours}小时`);
if (minutes > 0) parts.push(`${minutes}分钟`);
if (secs > 0 || parts.length === 0) parts.push(`${secs}`);
if (days > 0) {
parts.push(`${days}`);
}
if (hours > 0) {
parts.push(`${hours}小时`);
}
if (minutes > 0) {
parts.push(`${minutes}分钟`);
}
if (secs > 0 || parts.length === 0) {
parts.push(`${secs}`);
}
return parts.join(' ');
};
@@ -112,13 +120,13 @@ const getSystemInfo = () => {
nodeVersion: process.version,
platform: process.platform,
memory: {
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100,
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024 * 100) / 100,
rss: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100,
unit: 'MB'
heapUsed: Math.round((memUsage.heapUsed / 1024 / 1024) * 100) / 100,
heapTotal: Math.round((memUsage.heapTotal / 1024 / 1024) * 100) / 100,
rss: Math.round((memUsage.rss / 1024 / 1024) * 100) / 100,
unit: 'MB',
},
uptime: formatUptime(uptime),
uptimeSeconds: Math.round(uptime)
uptimeSeconds: Math.round(uptime),
};
};
@@ -129,7 +137,7 @@ const performHealthCheck = async () => {
const allChecks = [
{ name: 'database', ...dbCheck },
{ name: 'config', ...configCheck }
{ name: 'config', ...configCheck },
];
const overallStatus = allChecks.every(c => c.status === 'ok')
@@ -143,10 +151,10 @@ const performHealthCheck = async () => {
timestamp: new Date().toISOString(),
service: {
name: 'IDC设备管理系统',
version: '1.0.0'
version: '1.0.0',
},
checks: allChecks,
system: systemInfo
system: systemInfo,
};
};
@@ -154,5 +162,5 @@ module.exports = {
performHealthCheck,
checkDatabase,
checkCriticalConfig,
getSystemInfo
getSystemInfo,
};
+30 -17
View File
@@ -4,26 +4,27 @@ const generateRecordId = () => {
return `OPLOG_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`;
};
const getOperatorInfo = (req) => {
const getOperatorInfo = req => {
if (!req || !req.user) {
return {
operatorId: 'system',
operatorName: '系统',
operatorRole: null
operatorRole: null,
};
}
return {
operatorId: req.user.userId || req.user.id || 'unknown',
operatorName: req.user.realName || req.user.username || '未知用户',
operatorRole: req.user.roleName || req.user.role || null
operatorRole: req.user.roleName || req.user.role || null,
};
};
const getClientInfo = (req) => {
const getClientInfo = req => {
if (!req) {
return { ipAddress: null, userAgent: null };
}
const ipAddress = req.headers['x-forwarded-for'] ||
const ipAddress =
req.headers['x-forwarded-for'] ||
req.headers['x-real-ip'] ||
req.connection?.remoteAddress ||
req.ip ||
@@ -39,7 +40,7 @@ const DEVICE_TYPE_MAP = {
storage: '存储设备',
firewall: '防火墙',
loadbalancer: '负载均衡器',
other: '其他设备'
other: '其他设备',
};
const generateDeviceDescription = (operation, device, options = {}) => {
@@ -48,7 +49,7 @@ const generateDeviceDescription = (operation, device, options = {}) => {
includePosition = true,
includeSerial = true,
includeIp = true,
includeModel = true
includeModel = true,
} = options;
const deviceType = DEVICE_TYPE_MAP[device.type] || device.type || '设备';
@@ -94,7 +95,7 @@ const buildDeviceMetadata = (device, extra = {}) => {
position: device.position !== undefined ? device.position : null,
roomId: device.roomId || null,
roomName: device.roomName || null,
...extra
...extra,
};
};
@@ -108,7 +109,7 @@ async function logOperation({
afterState,
result = 'success',
req,
metadata = {}
metadata = {},
}) {
try {
const operatorInfo = getOperatorInfo(req);
@@ -129,14 +130,18 @@ async function logOperation({
result,
ipAddress: clientInfo.ipAddress,
userAgent: clientInfo.userAgent,
metadata
metadata,
});
} catch (error) {
console.error('记录操作日志失败:', error);
}
}
async function logDeviceOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
async function logDeviceOperation(
operationType,
operationDescription,
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
) {
return logOperation({
module: 'device',
operationType,
@@ -147,11 +152,15 @@ async function logDeviceOperation(operationType, operationDescription, { targetI
afterState,
result,
req,
metadata
metadata,
});
}
async function logUserOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
async function logUserOperation(
operationType,
operationDescription,
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
) {
return logOperation({
module: 'user',
operationType,
@@ -162,11 +171,15 @@ async function logUserOperation(operationType, operationDescription, { targetId,
afterState,
result,
req,
metadata
metadata,
});
}
async function logRoleOperation(operationType, operationDescription, { targetId, targetName, beforeState, afterState, result, req, metadata = {} }) {
async function logRoleOperation(
operationType,
operationDescription,
{ targetId, targetName, beforeState, afterState, result, req, metadata = {} }
) {
return logOperation({
module: 'role',
operationType,
@@ -177,7 +190,7 @@ async function logRoleOperation(operationType, operationDescription, { targetId,
afterState,
result,
req,
metadata
metadata,
});
}
@@ -187,5 +200,5 @@ module.exports = {
logUserOperation,
logRoleOperation,
generateDeviceDescription,
buildDeviceMetadata
buildDeviceMetadata,
};
+32 -30
View File
@@ -28,9 +28,9 @@ const PROTOCOL_LABELS = {
*/
async function uploadViaFTP(config, localFilePath, remotePath) {
const { Client } = require('basic-ftp');
const client = new Client();
try {
await client.access({
host: config.host,
@@ -42,16 +42,16 @@ async function uploadViaFTP(config, localFilePath, remotePath) {
rejectUnauthorized: config.rejectUnauthorized !== false,
},
});
await client.cd(config.rootPath || '/');
const dirPath = path.dirname(remotePath);
if (dirPath !== '.') {
await ensureRemoteDir(client, dirPath, 'ftp');
}
await client.uploadFrom(localFilePath, path.basename(remotePath));
return {
success: true,
message: `FTP 上传成功:${remotePath}`,
@@ -69,7 +69,7 @@ async function uploadViaFTP(config, localFilePath, remotePath) {
async function uploadViaSFTP(config, localFilePath, remotePath) {
const Client = require('ssh2-sftp-client');
const client = new Client();
try {
await client.connect({
host: config.host,
@@ -80,14 +80,14 @@ async function uploadViaSFTP(config, localFilePath, remotePath) {
passphrase: config.passphrase,
readyTimeout: config.timeout || 10000,
});
const dirPath = path.dirname(remotePath);
if (dirPath !== '.') {
await ensureRemoteDir(client, dirPath, 'sftp');
}
await client.put(fs.createReadStream(localFilePath), remotePath);
return {
success: true,
message: `SFTP 上传成功:${remotePath}`,
@@ -104,7 +104,7 @@ async function uploadViaSFTP(config, localFilePath, remotePath) {
*/
async function uploadViaWebDAV(config, localFilePath, remotePath) {
const { createClient } = require('webdav');
const client = createClient(config.url, {
username: config.username,
password: config.password,
@@ -113,18 +113,18 @@ async function uploadViaWebDAV(config, localFilePath, remotePath) {
'User-Agent': 'IDC-Backup-Client/1.0',
},
});
try {
const dirPath = path.dirname(remotePath);
if (dirPath !== '/') {
await ensureRemoteDir(client, dirPath, 'webdav');
}
const fileContent = fs.readFileSync(localFilePath);
await client.putFileContents(remotePath, fileContent, {
overwrite: true,
});
return {
success: true,
message: `WebDAV 上传成功:${remotePath}`,
@@ -139,22 +139,22 @@ async function uploadViaWebDAV(config, localFilePath, remotePath) {
*/
async function uploadViaSMB(config, localFilePath, remotePath) {
const smb = require('smb2');
const client = new smb({
share: `\\\\${config.host}\\${config.share}`,
domain: config.domain || '',
username: config.username,
password: config.password,
});
try {
const dirPath = path.dirname(remotePath);
if (dirPath !== '.') {
await ensureRemoteDir(client, dirPath, 'smb');
}
await client.writeFile(remotePath, fs.readFileSync(localFilePath));
return {
success: true,
message: `SMB 上传成功:${remotePath}`,
@@ -216,11 +216,11 @@ async function ensureRemoteDir(client, dirPath, protocol) {
*/
async function uploadToRemote(config, localFilePath, remotePath) {
console.log(`开始上传到远端 [${config.protocol}]: ${remotePath}`);
const startTime = Date.now();
let result;
switch (config.protocol) {
case PROTOCOL_TYPES.FTP:
result = await uploadViaFTP(config, localFilePath, remotePath);
@@ -237,12 +237,14 @@ async function uploadToRemote(config, localFilePath, remotePath) {
default:
throw new Error(`不支持的协议类型:${config.protocol}`);
}
const duration = Date.now() - startTime;
const fileSize = fs.statSync(localFilePath).size;
console.log(`远端上传完成 [${config.protocol}] - 耗时:${duration}ms, 文件大小:${(fileSize / 1024).toFixed(2)}KB`);
console.log(
`远端上传完成 [${config.protocol}] - 耗时:${duration}ms, 文件大小:${(fileSize / 1024).toFixed(2)}KB`
);
return {
...result,
protocol: config.protocol,
@@ -258,22 +260,22 @@ async function uploadToRemote(config, localFilePath, remotePath) {
*/
async function testRemoteConnection(config) {
console.log(`测试远端连接 [${config.protocol}]...`);
try {
const testContent = `IDC Backup Connection Test - ${new Date().toISOString()}`;
const testFile = path.join(require('os').tmpdir(), `backup-test-${Date.now()}.txt`);
fs.writeFileSync(testFile, testContent);
const testRemotePath = `test/backup-connection-test-${Date.now()}.txt`;
const result = await uploadToRemote(config, testFile, testRemotePath);
try {
fs.unlinkSync(testFile);
} catch (e) {
console.warn('删除测试文件失败:', e.message);
}
return {
success: true,
message: '连接测试成功',
+27 -23
View File
@@ -28,7 +28,9 @@ const DEFAULT_CONFIG = {
* 加密敏感信息
*/
function encrypt(text) {
if (!text) return '';
if (!text) {
return '';
}
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
const iv = crypto.randomBytes(16);
@@ -42,7 +44,9 @@ function encrypt(text) {
* 解密敏感信息
*/
function decrypt(text) {
if (!text) return '';
if (!text) {
return '';
}
try {
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
@@ -113,11 +117,11 @@ function getAllTargets() {
function getTarget(id) {
const config = loadConfig();
const target = config.targets.find(t => t.id === id);
if (!target) {
return null;
}
return {
...target,
password: target.password ? decrypt(target.password) : undefined,
@@ -133,7 +137,7 @@ function getTarget(id) {
*/
function addTarget(targetData) {
const config = loadConfig();
const newTarget = {
id: `target_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: targetData.name,
@@ -142,7 +146,7 @@ function addTarget(targetData) {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
switch (targetData.protocol) {
case PROTOCOL_TYPES.FTP:
case PROTOCOL_TYPES.SFTP:
@@ -155,7 +159,7 @@ function addTarget(targetData) {
secure: targetData.secure,
});
break;
case PROTOCOL_TYPES.WEBDAV:
Object.assign(newTarget, {
url: targetData.url,
@@ -165,7 +169,7 @@ function addTarget(targetData) {
rootPath: targetData.rootPath || '/',
});
break;
case PROTOCOL_TYPES.SMB:
Object.assign(newTarget, {
host: targetData.host,
@@ -176,17 +180,17 @@ function addTarget(targetData) {
rootPath: targetData.rootPath || '/',
});
break;
default:
throw new Error(`不支持的协议类型:${targetData.protocol}`);
}
config.targets.push(newTarget);
if (saveConfig(config)) {
return newTarget;
}
throw new Error('保存配置失败');
}
@@ -196,14 +200,14 @@ function addTarget(targetData) {
function updateTarget(id, updates) {
const config = loadConfig();
const targetIndex = config.targets.findIndex(t => t.id === id);
if (targetIndex === -1) {
throw new Error('目标不存在');
}
const existingTarget = config.targets[targetIndex];
const updatedTarget = { ...existingTarget, ...updates, updatedAt: new Date().toISOString() };
if (updates.password) {
updatedTarget.password = encrypt(updates.password);
}
@@ -219,13 +223,13 @@ function updateTarget(id, updates) {
if (updates.passphrase) {
updatedTarget.passphrase = encrypt(updates.passphrase);
}
config.targets[targetIndex] = updatedTarget;
if (saveConfig(config)) {
return updatedTarget;
}
throw new Error('保存配置失败');
}
@@ -235,16 +239,16 @@ function updateTarget(id, updates) {
function deleteTarget(id) {
const config = loadConfig();
const initialLength = config.targets.length;
config.targets = config.targets.filter(t => t.id !== id);
if (config.targets.length < initialLength) {
if (saveConfig(config)) {
return true;
}
throw new Error('保存配置失败');
}
return false;
}
@@ -262,11 +266,11 @@ function getGlobalSettings() {
function updateGlobalSettings(settings) {
const config = loadConfig();
config.globalSettings = { ...config.globalSettings, ...settings };
if (saveConfig(config)) {
return config.globalSettings;
}
throw new Error('保存配置失败');
}

Some files were not shown because too many files have changed in this diff Show More