refactor: 统一代码风格并迁移至 ESLint 新配置
style(backend): 格式化模型文件代码 style(frontend): 调整组件代码格式 chore: 删除旧 ESLint 配置并添加新配置 refactor(backend): 重构模型定义语法 style: 统一箭头函数和对象属性简写
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
# 构建输出
|
||||
dist/
|
||||
build/
|
||||
|
||||
# 依赖
|
||||
node_modules/
|
||||
|
||||
# 日志
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# 数据库
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
# 上传文件
|
||||
uploads/
|
||||
|
||||
# 其他
|
||||
.DS_Store
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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,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,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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' });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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' });
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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' });
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -4,7 +4,13 @@ const User = require('../models/User');
|
||||
const Role = require('../models/Role');
|
||||
const UserRole = require('../models/UserRole');
|
||||
const { generateToken, authMiddleware } = require('../middleware/auth');
|
||||
const { SALT_ROUNDS, MAX_LOGIN_ATTEMPTS, PASSWORD_MIN_LENGTH, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH } = require('../config');
|
||||
const {
|
||||
SALT_ROUNDS,
|
||||
MAX_LOGIN_ATTEMPTS,
|
||||
PASSWORD_MIN_LENGTH,
|
||||
USERNAME_MIN_LENGTH,
|
||||
USERNAME_MAX_LENGTH,
|
||||
} = require('../config');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -19,21 +25,21 @@ router.post('/register', async (req, res) => {
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名和密码不能为空'
|
||||
message: '用户名和密码不能为空',
|
||||
});
|
||||
}
|
||||
|
||||
if (username.length < USERNAME_MIN_LENGTH || username.length > USERNAME_MAX_LENGTH) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`
|
||||
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`,
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < PASSWORD_MIN_LENGTH) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
||||
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,7 +47,7 @@ router.post('/register', async (req, res) => {
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名已存在'
|
||||
message: '用户名已存在',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,7 +63,7 @@ router.post('/register', async (req, res) => {
|
||||
email,
|
||||
phone,
|
||||
realName: realName || username,
|
||||
status: isFirstUser ? 'active' : 'pending'
|
||||
status: isFirstUser ? 'active' : 'pending',
|
||||
});
|
||||
|
||||
if (isFirstUser) {
|
||||
@@ -70,13 +76,13 @@ router.post('/register', async (req, res) => {
|
||||
roleCode: 'admin',
|
||||
description: '系统管理员,拥有所有权限',
|
||||
status: 'active',
|
||||
permissions: []
|
||||
permissions: [],
|
||||
});
|
||||
}
|
||||
|
||||
await UserRole.create({
|
||||
UserId: user.userId,
|
||||
RoleId: adminRole.roleId
|
||||
RoleId: adminRole.roleId,
|
||||
});
|
||||
|
||||
const token = generateToken(user);
|
||||
@@ -89,11 +95,11 @@ router.post('/register', async (req, res) => {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName
|
||||
realName: user.realName,
|
||||
},
|
||||
token,
|
||||
isFirstUser: true
|
||||
}
|
||||
isFirstUser: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
|
||||
@@ -101,7 +107,7 @@ router.post('/register', async (req, res) => {
|
||||
if (defaultRole) {
|
||||
await UserRole.create({
|
||||
UserId: user.userId,
|
||||
RoleId: defaultRole.roleId
|
||||
RoleId: defaultRole.roleId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -113,11 +119,11 @@ router.post('/register', async (req, res) => {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName
|
||||
realName: user.realName,
|
||||
},
|
||||
isFirstUser: false,
|
||||
pendingApproval: true
|
||||
}
|
||||
pendingApproval: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -125,7 +131,7 @@ router.post('/register', async (req, res) => {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '注册失败',
|
||||
error: error.message
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -137,7 +143,7 @@ router.post('/login', async (req, res) => {
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名和密码不能为空'
|
||||
message: '用户名和密码不能为空',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,21 +151,21 @@ router.post('/login', async (req, res) => {
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
message: '用户名或密码错误',
|
||||
});
|
||||
}
|
||||
|
||||
if (user.status === 'locked') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '账户已被锁定,请联系管理员'
|
||||
message: '账户已被锁定,请联系管理员',
|
||||
});
|
||||
}
|
||||
|
||||
if (user.status === 'inactive') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '账户已禁用'
|
||||
message: '账户已禁用',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,7 +173,7 @@ router.post('/login', async (req, res) => {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
code: 'PENDING_APPROVAL',
|
||||
message: '账户待审核,请联系管理员激活'
|
||||
message: '账户待审核,请联系管理员激活',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,10 +184,10 @@ router.post('/login', async (req, res) => {
|
||||
user.status = 'locked';
|
||||
}
|
||||
await user.save();
|
||||
|
||||
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
message: '用户名或密码错误',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -201,17 +207,17 @@ router.post('/login', async (req, res) => {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName,
|
||||
avatar: user.avatar
|
||||
avatar: user.avatar,
|
||||
},
|
||||
token
|
||||
}
|
||||
token,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '登录失败',
|
||||
error: error.message
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -219,22 +225,24 @@ router.post('/login', async (req, res) => {
|
||||
router.get('/profile', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
attributes: { exclude: ['password'] },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
const roles = await Role.findAll({
|
||||
include: [{
|
||||
model: User,
|
||||
where: { userId: req.user.userId },
|
||||
attributes: []
|
||||
}]
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
where: { userId: req.user.userId },
|
||||
attributes: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -244,15 +252,15 @@ router.get('/profile', authMiddleware, async (req, res) => {
|
||||
roles: roles.map(r => ({
|
||||
roleId: r.roleId,
|
||||
roleName: r.roleName,
|
||||
roleCode: r.roleCode
|
||||
}))
|
||||
}
|
||||
roleCode: r.roleCode,
|
||||
})),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取profile错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户信息失败'
|
||||
message: '获取用户信息失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -265,14 +273,22 @@ router.put('/profile', authMiddleware, async (req, res) => {
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
if (email !== undefined) user.email = email;
|
||||
if (phone !== undefined) user.phone = phone;
|
||||
if (realName !== undefined) user.realName = realName;
|
||||
if (avatar !== undefined) user.avatar = avatar;
|
||||
if (email !== undefined) {
|
||||
user.email = email;
|
||||
}
|
||||
if (phone !== undefined) {
|
||||
user.phone = phone;
|
||||
}
|
||||
if (realName !== undefined) {
|
||||
user.realName = realName;
|
||||
}
|
||||
if (avatar !== undefined) {
|
||||
user.avatar = avatar;
|
||||
}
|
||||
|
||||
await user.save();
|
||||
|
||||
@@ -285,14 +301,14 @@ router.put('/profile', authMiddleware, async (req, res) => {
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
realName: user.realName,
|
||||
avatar: user.avatar
|
||||
}
|
||||
avatar: user.avatar,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新profile错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新失败'
|
||||
message: '更新失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -304,14 +320,14 @@ router.put('/password', authMiddleware, async (req, res) => {
|
||||
if (!oldPassword || !newPassword) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '旧密码和新密码都不能为空'
|
||||
message: '旧密码和新密码都不能为空',
|
||||
});
|
||||
}
|
||||
|
||||
if (newPassword.length < PASSWORD_MIN_LENGTH) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
||||
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,7 +337,7 @@ router.put('/password', authMiddleware, async (req, res) => {
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '旧密码错误'
|
||||
message: '旧密码错误',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -330,13 +346,13 @@ router.put('/password', authMiddleware, async (req, res) => {
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '密码修改成功'
|
||||
message: '密码修改成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('修改密码错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '密码修改失败'
|
||||
message: '密码修改失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -344,19 +360,19 @@ router.put('/password', authMiddleware, async (req, res) => {
|
||||
router.post('/check-admin', async (req, res) => {
|
||||
try {
|
||||
const userCount = await User.count();
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hasAdmin: userCount > 0,
|
||||
userCount
|
||||
}
|
||||
userCount,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('检查管理员错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '检查失败'
|
||||
message: '检查失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -368,7 +384,7 @@ router.post('/unlock', async (req, res) => {
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名和密码不能为空'
|
||||
message: '用户名和密码不能为空',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -376,14 +392,14 @@ router.post('/unlock', async (req, res) => {
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
message: '用户名或密码错误',
|
||||
});
|
||||
}
|
||||
|
||||
if (user.status !== 'locked') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '账户未被锁定'
|
||||
message: '账户未被锁定',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -391,7 +407,7 @@ router.post('/unlock', async (req, res) => {
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
message: '用户名或密码错误',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -402,14 +418,14 @@ router.post('/unlock', async (req, res) => {
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '账户解锁成功'
|
||||
message: '账户解锁成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('解锁账户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '解锁失败',
|
||||
error: error.message
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,11 +10,18 @@ if (!fs.existsSync(UPLOAD_DIR)) {
|
||||
|
||||
const SETTINGS_FILE = path.join(__dirname, '../backgroundSettings.json');
|
||||
if (!fs.existsSync(SETTINGS_FILE)) {
|
||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify({
|
||||
type: 'gradient',
|
||||
image: '',
|
||||
size: 'contain'
|
||||
}, null, 2));
|
||||
fs.writeFileSync(
|
||||
SETTINGS_FILE,
|
||||
JSON.stringify(
|
||||
{
|
||||
type: 'gradient',
|
||||
image: '',
|
||||
size: 'contain',
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
@@ -22,7 +29,7 @@ router.get('/', (req, res) => {
|
||||
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
||||
res.json({
|
||||
success: true,
|
||||
data: settings
|
||||
data: settings,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('读取背景设置失败:', error);
|
||||
@@ -36,7 +43,7 @@ router.put('/', (req, res) => {
|
||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
||||
res.json({
|
||||
success: true,
|
||||
data: settings
|
||||
data: settings,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('保存背景设置失败:', error);
|
||||
@@ -54,7 +61,7 @@ router.post('/upload', (req, res) => {
|
||||
const fileName = `${Date.now()}_${file.name}`;
|
||||
const filePath = path.join(UPLOAD_DIR, fileName);
|
||||
|
||||
file.mv(filePath, (err) => {
|
||||
file.mv(filePath, err => {
|
||||
if (err) {
|
||||
console.error('文件保存失败:', err);
|
||||
return res.status(500).json({ error: '文件保存失败' });
|
||||
@@ -90,4 +97,4 @@ router.post('/settings', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
+102
-75
@@ -23,11 +23,7 @@ const {
|
||||
updateAutoBackupSettings,
|
||||
executeBackupNow,
|
||||
} = require('../utils/autoBackupScheduler');
|
||||
const {
|
||||
getBackupLogs,
|
||||
getBackupLogById,
|
||||
deleteOldLogs,
|
||||
} = require('../utils/backupLog');
|
||||
const { getBackupLogs, getBackupLogById, deleteOldLogs } = require('../utils/backupLog');
|
||||
const {
|
||||
getAllTargets,
|
||||
getTarget,
|
||||
@@ -81,22 +77,27 @@ router.get('/list', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(backupPath)
|
||||
.filter(f => (f.startsWith('backup_') || f.startsWith('uploaded_')) && (f.endsWith('.json') || f.endsWith('.json.gz')))
|
||||
const files = fs
|
||||
.readdirSync(backupPath)
|
||||
.filter(
|
||||
f =>
|
||||
(f.startsWith('backup_') || f.startsWith('uploaded_')) &&
|
||||
(f.endsWith('.json') || f.endsWith('.json.gz'))
|
||||
)
|
||||
.map(async f => {
|
||||
const filePath = path.join(backupPath, f);
|
||||
const stats = fs.statSync(filePath);
|
||||
const isCompressed = f.endsWith('.gz');
|
||||
|
||||
|
||||
// 尝试从文件内容中提取元数据
|
||||
let metadata = {
|
||||
const metadata = {
|
||||
filename: f,
|
||||
size: stats.size,
|
||||
compressed: isCompressed,
|
||||
createdAt: stats.birthtime,
|
||||
modifiedAt: stats.mtime,
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
// 读取文件头部的元数据信息
|
||||
let content;
|
||||
@@ -106,9 +107,9 @@ router.get('/list', async (req, res) => {
|
||||
} else {
|
||||
content = fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
|
||||
|
||||
const backupData = JSON.parse(content);
|
||||
|
||||
|
||||
// 提取关键元数据
|
||||
metadata.description = backupData.description || '';
|
||||
metadata.backupType = backupData.backupType || 'full';
|
||||
@@ -117,19 +118,18 @@ router.get('/list', async (req, res) => {
|
||||
metadata.checksum = backupData.checksum;
|
||||
metadata.metadata = backupData.metadata;
|
||||
metadata.systemInfo = backupData.systemInfo;
|
||||
|
||||
|
||||
// 判断是否为上传的文件(通过文件名判断)
|
||||
metadata.isUploaded = f.startsWith('uploaded_');
|
||||
|
||||
} catch (error) {
|
||||
// 如果读取失败,标记为无效文件
|
||||
metadata.invalid = true;
|
||||
metadata.error = '无法读取文件内容';
|
||||
}
|
||||
|
||||
|
||||
return metadata;
|
||||
});
|
||||
|
||||
|
||||
const resolvedFiles = await Promise.all(files);
|
||||
|
||||
res.json({
|
||||
@@ -201,7 +201,7 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
|
||||
const sendProgress = (data) => {
|
||||
const sendProgress = data => {
|
||||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
};
|
||||
|
||||
@@ -211,16 +211,25 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
|
||||
|
||||
const validation = await validateBackupFile(filePath);
|
||||
if (!validation.valid) {
|
||||
sendProgress({ stage: 'error', message: `备份文件验证失败: ${validation.error}`, progress: 0 });
|
||||
sendProgress({
|
||||
stage: 'error',
|
||||
message: `备份文件验证失败: ${validation.error}`,
|
||||
progress: 0,
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
sendProgress({ stage: 'validate', message: '备份文件验证通过', progress: 10, metadata: validation.metadata });
|
||||
sendProgress({
|
||||
stage: 'validate',
|
||||
message: '备份文件验证通过',
|
||||
progress: 10,
|
||||
metadata: validation.metadata,
|
||||
});
|
||||
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const isCompressed = filePath.endsWith('.gz');
|
||||
|
||||
|
||||
let backupData;
|
||||
if (isCompressed) {
|
||||
sendProgress({ stage: 'decompress', message: '正在解压备份文件...', progress: 15 });
|
||||
@@ -243,10 +252,10 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
|
||||
processedTables++;
|
||||
const progress = 20 + Math.floor((processedTables / totalTables) * 70);
|
||||
const statusMap = {
|
||||
'restored': '已恢复',
|
||||
'skipped': '已跳过',
|
||||
'empty': '无数据',
|
||||
'error': '错误',
|
||||
restored: '已恢复',
|
||||
skipped: '已跳过',
|
||||
empty: '无数据',
|
||||
error: '错误',
|
||||
};
|
||||
sendProgress({
|
||||
stage: 'restore',
|
||||
@@ -261,9 +270,9 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
|
||||
},
|
||||
});
|
||||
|
||||
sendProgress({
|
||||
stage: 'complete',
|
||||
message: '恢复完成!',
|
||||
sendProgress({
|
||||
stage: 'complete',
|
||||
message: '恢复完成!',
|
||||
progress: 100,
|
||||
result: {
|
||||
tablesRestored: result.tablesRestored,
|
||||
@@ -272,7 +281,7 @@ router.get('/restore-progress/:filename', authMiddleware, async (req, res) => {
|
||||
restoredAt: result.restoredAt,
|
||||
tableDetails: result.tableDetails,
|
||||
fileDetails: result.fileDetails,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
res.end();
|
||||
@@ -342,7 +351,7 @@ router.post('/upload', async (req, res) => {
|
||||
const backupFile = req.files.backup;
|
||||
const originalName = backupFile.name || '';
|
||||
const nameLower = originalName.toLowerCase();
|
||||
|
||||
|
||||
// 验证文件类型
|
||||
if (!nameLower.endsWith('.json') && !nameLower.endsWith('.gz')) {
|
||||
return res.status(400).json({
|
||||
@@ -350,15 +359,15 @@ router.post('/upload', async (req, res) => {
|
||||
message: '只支持 JSON 或 GZ 格式的备份文件',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const isCompressed = nameLower.endsWith('.gz');
|
||||
|
||||
|
||||
console.log(`上传备份文件: ${originalName}, 压缩: ${isCompressed}, 大小: ${backupFile.size}`);
|
||||
|
||||
// 保存到临时文件
|
||||
const tempFilename = `upload_${Date.now()}`;
|
||||
const tempPath = path.join(tempDir, tempFilename);
|
||||
|
||||
|
||||
await backupFile.mv(tempPath);
|
||||
|
||||
const validation = await validateBackupFile(tempPath, { isCompressed });
|
||||
@@ -411,7 +420,7 @@ router.get('/download/:filename', (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
res.download(filePath, filename, (err) => {
|
||||
res.download(filePath, filename, err => {
|
||||
if (err) {
|
||||
console.error('下载备份文件失败:', err);
|
||||
}
|
||||
@@ -463,7 +472,9 @@ router.get('/info', (req, res) => {
|
||||
let backupCount = 0;
|
||||
|
||||
if (fs.existsSync(backupPath)) {
|
||||
const files = fs.readdirSync(backupPath).filter(f => f.endsWith('.json') || f.endsWith('.json.gz'));
|
||||
const files = fs
|
||||
.readdirSync(backupPath)
|
||||
.filter(f => f.endsWith('.json') || f.endsWith('.json.gz'));
|
||||
backupCount = files.length;
|
||||
files.forEach(f => {
|
||||
const stats = fs.statSync(path.join(backupPath, f));
|
||||
@@ -491,7 +502,9 @@ router.get('/info', (req, res) => {
|
||||
});
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
if (bytes === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
@@ -565,7 +578,9 @@ router.post('/auto/settings', (req, res) => {
|
||||
} = req.body;
|
||||
|
||||
const newSettings = {};
|
||||
if (enabled !== undefined) newSettings.enabled = enabled;
|
||||
if (enabled !== undefined) {
|
||||
newSettings.enabled = enabled;
|
||||
}
|
||||
if (hour !== undefined || minute !== undefined) {
|
||||
newSettings.hour = hour || 2;
|
||||
newSettings.minute = minute || 0;
|
||||
@@ -579,12 +594,24 @@ router.post('/auto/settings', (req, res) => {
|
||||
}
|
||||
newSettings.cronExpression = cronExpression;
|
||||
}
|
||||
if (description) newSettings.description = description;
|
||||
if (includeFiles !== undefined) newSettings.includeFiles = includeFiles;
|
||||
if (compress !== undefined) newSettings.compress = compress;
|
||||
if (maxCount !== undefined) newSettings.maxCount = maxCount;
|
||||
if (maxAgeDays !== undefined) newSettings.maxAgeDays = maxAgeDays;
|
||||
if (backupType !== undefined) newSettings.backupType = backupType;
|
||||
if (description) {
|
||||
newSettings.description = description;
|
||||
}
|
||||
if (includeFiles !== undefined) {
|
||||
newSettings.includeFiles = includeFiles;
|
||||
}
|
||||
if (compress !== undefined) {
|
||||
newSettings.compress = compress;
|
||||
}
|
||||
if (maxCount !== undefined) {
|
||||
newSettings.maxCount = maxCount;
|
||||
}
|
||||
if (maxAgeDays !== undefined) {
|
||||
newSettings.maxAgeDays = maxAgeDays;
|
||||
}
|
||||
if (backupType !== undefined) {
|
||||
newSettings.backupType = backupType;
|
||||
}
|
||||
|
||||
const success = updateAutoBackupSettings(newSettings);
|
||||
if (success) {
|
||||
@@ -614,7 +641,7 @@ router.post('/auto/settings', (req, res) => {
|
||||
router.post('/auto/execute', async (req, res) => {
|
||||
try {
|
||||
const { description, includeFiles, compress, backupType } = req.body;
|
||||
|
||||
|
||||
const result = await executeBackupNow({
|
||||
description: description || '手动触发备份',
|
||||
includeFiles,
|
||||
@@ -648,7 +675,7 @@ router.post('/auto/execute', async (req, res) => {
|
||||
router.post('/auto/test-cron', (req, res) => {
|
||||
try {
|
||||
const { cronExpression } = req.body;
|
||||
|
||||
|
||||
if (!cronExpression) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -657,7 +684,7 @@ router.post('/auto/test-cron', (req, res) => {
|
||||
}
|
||||
|
||||
const isValid = validateCronExpression(cronExpression);
|
||||
|
||||
|
||||
res.json({
|
||||
success: isValid,
|
||||
message: isValid ? 'Cron 表达式有效' : 'Cron 表达式无效',
|
||||
@@ -700,14 +727,14 @@ router.get('/remote/targets', (req, res) => {
|
||||
router.get('/remote/targets/:id', (req, res) => {
|
||||
try {
|
||||
const target = getTarget(req.params.id);
|
||||
|
||||
|
||||
if (!target) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '目标不存在',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { target },
|
||||
@@ -726,16 +753,16 @@ router.get('/remote/targets/:id', (req, res) => {
|
||||
router.post('/remote/targets', (req, res) => {
|
||||
try {
|
||||
const targetData = req.body;
|
||||
|
||||
|
||||
if (!targetData.name || !targetData.protocol) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供目标名称和协议类型',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const target = addTarget(targetData);
|
||||
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '远端备份目标已添加',
|
||||
@@ -756,7 +783,7 @@ router.put('/remote/targets/:id', (req, res) => {
|
||||
try {
|
||||
const updates = req.body;
|
||||
const target = updateTarget(req.params.id, updates);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '远端备份目标已更新',
|
||||
@@ -776,14 +803,14 @@ router.put('/remote/targets/:id', (req, res) => {
|
||||
router.delete('/remote/targets/:id', (req, res) => {
|
||||
try {
|
||||
const deleted = deleteTarget(req.params.id);
|
||||
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '目标不存在',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '远端备份目标已删除',
|
||||
@@ -818,7 +845,7 @@ router.post('/remote/test', async (req, res) => {
|
||||
}
|
||||
|
||||
const result = await testRemoteConnection(config);
|
||||
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -846,16 +873,16 @@ router.post('/remote/test', async (req, res) => {
|
||||
router.post('/remote/targets/:id/test', async (req, res) => {
|
||||
try {
|
||||
const target = getTarget(req.params.id);
|
||||
|
||||
|
||||
if (!target) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '目标不存在',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const result = await testRemoteConnection(target);
|
||||
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -924,7 +951,7 @@ router.get('/remote/protocols', (req, res) => {
|
||||
value,
|
||||
label: PROTOCOL_LABELS[value],
|
||||
}));
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { protocols },
|
||||
@@ -943,45 +970,45 @@ router.get('/remote/protocols', (req, res) => {
|
||||
router.post('/remote/upload', async (req, res) => {
|
||||
try {
|
||||
const { filename, targetIds } = req.body;
|
||||
|
||||
|
||||
if (!filename) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供备份文件名',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const backupPath = getBackupPath();
|
||||
const filePath = path.join(backupPath, filename);
|
||||
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '备份文件不存在',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const { uploadToRemote } = require('../utils/remoteBackup');
|
||||
const { getTarget } = require('../utils/remoteBackupConfig');
|
||||
|
||||
const targets = targetIds
|
||||
|
||||
const targets = targetIds
|
||||
? targetIds.map(id => getTarget(id)).filter(Boolean)
|
||||
: getEnabledTargets();
|
||||
|
||||
|
||||
if (targets.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '没有可用的远端目标',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const uploadResults = [];
|
||||
|
||||
|
||||
for (const target of targets) {
|
||||
try {
|
||||
const remotePath = `${target.prefix || 'backups/'}${filename}`;
|
||||
const result = await uploadToRemote(target, filePath, remotePath);
|
||||
|
||||
|
||||
uploadResults.push({
|
||||
targetId: target.id,
|
||||
targetName: target.name,
|
||||
@@ -997,11 +1024,11 @@ router.post('/remote/upload', async (req, res) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '上传完成',
|
||||
data: {
|
||||
data: {
|
||||
filename,
|
||||
results: uploadResults,
|
||||
},
|
||||
@@ -1022,14 +1049,14 @@ router.post('/remote/upload', async (req, res) => {
|
||||
router.get('/logs', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, logType, status } = req.query;
|
||||
|
||||
|
||||
const result = await getBackupLogs({
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
logType,
|
||||
status,
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
@@ -1049,14 +1076,14 @@ router.get('/logs/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const log = await getBackupLogById(parseInt(id));
|
||||
|
||||
|
||||
if (!log) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '备份日志不存在',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: log,
|
||||
@@ -1076,7 +1103,7 @@ router.delete('/logs/clean', async (req, res) => {
|
||||
try {
|
||||
const { days = 30 } = req.body;
|
||||
const deletedCount = await deleteOldLogs(parseInt(days));
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '清理完成',
|
||||
|
||||
+141
-119
@@ -8,10 +8,7 @@ const DevicePort = require('../models/DevicePort');
|
||||
// 辅助函数:更新端口状态
|
||||
async function updatePortStatus(deviceId, portName, status) {
|
||||
try {
|
||||
await DevicePort.update(
|
||||
{ status },
|
||||
{ where: { deviceId, portName } }
|
||||
);
|
||||
await DevicePort.update({ status }, { where: { deviceId, portName } });
|
||||
} catch (error) {
|
||||
console.error(`更新端口状态失败: ${deviceId}:${portName} -> ${status}`, error);
|
||||
}
|
||||
@@ -29,51 +26,58 @@ async function freePort(deviceId, portName) {
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { sourceDeviceId, targetDeviceId, status, cableType, page = 1, pageSize = 10 } = req.query;
|
||||
const {
|
||||
sourceDeviceId,
|
||||
targetDeviceId,
|
||||
status,
|
||||
cableType,
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
} = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
|
||||
const where = {};
|
||||
|
||||
|
||||
if (sourceDeviceId) {
|
||||
where.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
|
||||
if (targetDeviceId) {
|
||||
where.targetDeviceId = targetDeviceId;
|
||||
}
|
||||
|
||||
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
|
||||
if (cableType && cableType !== 'all') {
|
||||
where.cableType = cableType;
|
||||
}
|
||||
|
||||
|
||||
const { count, rows } = await Cable.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'sourceDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
{
|
||||
model: Device,
|
||||
as: 'targetDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
}
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
],
|
||||
offset,
|
||||
limit: parseInt(pageSize),
|
||||
order: [['createdAt', 'DESC']]
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
cables: rows,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取接线列表失败:', error);
|
||||
@@ -84,28 +88,25 @@ router.get('/', async (req, res) => {
|
||||
router.get('/device/:deviceId', async (req, res) => {
|
||||
try {
|
||||
const { deviceId } = req.params;
|
||||
|
||||
|
||||
const cables = await Cable.findAll({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ sourceDeviceId: deviceId },
|
||||
{ targetDeviceId: deviceId }
|
||||
]
|
||||
[Op.or]: [{ sourceDeviceId: deviceId }, { targetDeviceId: deviceId }],
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'sourceDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
{
|
||||
model: Device,
|
||||
as: 'targetDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
res.json(cables);
|
||||
} catch (error) {
|
||||
console.error('获取设备接线失败:', error);
|
||||
@@ -117,15 +118,15 @@ router.get('/device/:deviceId', async (req, res) => {
|
||||
router.get('/rack/:rackId', async (req, res) => {
|
||||
try {
|
||||
const { rackId } = req.params;
|
||||
|
||||
|
||||
// 1. 找出该机柜下的所有设备ID
|
||||
const devices = await Device.findAll({
|
||||
where: { rackId: rackId },
|
||||
attributes: ['deviceId']
|
||||
attributes: ['deviceId'],
|
||||
});
|
||||
|
||||
|
||||
const deviceIds = devices.map(d => d.deviceId);
|
||||
|
||||
|
||||
if (deviceIds.length === 0) {
|
||||
return res.json([]);
|
||||
}
|
||||
@@ -135,23 +136,23 @@ router.get('/rack/:rackId', async (req, res) => {
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ sourceDeviceId: { [Op.in]: deviceIds } },
|
||||
{ targetDeviceId: { [Op.in]: deviceIds } }
|
||||
]
|
||||
{ targetDeviceId: { [Op.in]: deviceIds } },
|
||||
],
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'sourceDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
{
|
||||
model: Device,
|
||||
as: 'targetDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
res.json(cables);
|
||||
} catch (error) {
|
||||
console.error('获取机柜接线失败:', error);
|
||||
@@ -175,22 +176,22 @@ router.post('/check-conflict', async (req, res) => {
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ sourceDeviceId, sourcePort },
|
||||
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort }
|
||||
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort },
|
||||
],
|
||||
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } })
|
||||
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } }),
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'sourceDevice',
|
||||
attributes: ['deviceId', 'name', 'type']
|
||||
attributes: ['deviceId', 'name', 'type'],
|
||||
},
|
||||
{
|
||||
model: Device,
|
||||
as: 'targetDevice',
|
||||
attributes: ['deviceId', 'name', 'type']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (sourceConflict) {
|
||||
@@ -198,7 +199,7 @@ router.post('/check-conflict', async (req, res) => {
|
||||
type: 'source',
|
||||
port: sourcePort,
|
||||
deviceId: sourceDeviceId,
|
||||
existingCable: sourceConflict
|
||||
existingCable: sourceConflict,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -207,22 +208,22 @@ router.post('/check-conflict', async (req, res) => {
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
|
||||
{ targetDeviceId, targetPort }
|
||||
{ targetDeviceId, targetPort },
|
||||
],
|
||||
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } })
|
||||
...(excludeCableId && { cableId: { [Op.ne]: excludeCableId } }),
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'sourceDevice',
|
||||
attributes: ['deviceId', 'name', 'type']
|
||||
attributes: ['deviceId', 'name', 'type'],
|
||||
},
|
||||
{
|
||||
model: Device,
|
||||
as: 'targetDevice',
|
||||
attributes: ['deviceId', 'name', 'type']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (targetConflict) {
|
||||
@@ -230,13 +231,13 @@ router.post('/check-conflict', async (req, res) => {
|
||||
type: 'target',
|
||||
port: targetPort,
|
||||
deviceId: targetDeviceId,
|
||||
existingCable: targetConflict
|
||||
existingCable: targetConflict,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
hasConflict: conflicts.length > 0,
|
||||
conflicts
|
||||
conflicts,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('检查接线冲突失败:', error);
|
||||
@@ -246,7 +247,18 @@ router.post('/check-conflict', async (req, res) => {
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { cableId, sourceDeviceId, sourcePort, targetDeviceId, targetPort, cableType, cableLength, status, description, force } = req.body;
|
||||
const {
|
||||
cableId,
|
||||
sourceDeviceId,
|
||||
sourcePort,
|
||||
targetDeviceId,
|
||||
targetPort,
|
||||
cableType,
|
||||
cableLength,
|
||||
status,
|
||||
description,
|
||||
force,
|
||||
} = req.body;
|
||||
|
||||
if (!sourceDeviceId || !sourcePort || !targetDeviceId || !targetPort) {
|
||||
return res.status(400).json({ error: '缺少必填字段' });
|
||||
@@ -262,16 +274,16 @@ router.post('/', async (req, res) => {
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ sourceDeviceId, sourcePort },
|
||||
{ targetDeviceId, targetPort }
|
||||
]
|
||||
}
|
||||
{ targetDeviceId, targetPort },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCable) {
|
||||
return res.status(409).json({
|
||||
error: '端口已被占用',
|
||||
conflict: true,
|
||||
existingCable
|
||||
existingCable,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -284,9 +296,9 @@ router.post('/', async (req, res) => {
|
||||
{ sourceDeviceId, sourcePort },
|
||||
{ targetDeviceId: sourceDeviceId, targetPort: sourcePort },
|
||||
{ sourceDeviceId: targetDeviceId, sourcePort: targetPort },
|
||||
{ targetDeviceId, targetPort }
|
||||
]
|
||||
}
|
||||
{ targetDeviceId, targetPort },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
for (const cable of existingCables) {
|
||||
@@ -308,7 +320,7 @@ router.post('/', async (req, res) => {
|
||||
cableType: cableType || 'ethernet',
|
||||
cableLength,
|
||||
status: status || 'normal',
|
||||
description
|
||||
description,
|
||||
});
|
||||
|
||||
const createdCable = await Cable.findByPk(cable.cableId, {
|
||||
@@ -316,14 +328,14 @@ router.post('/', async (req, res) => {
|
||||
{
|
||||
model: Device,
|
||||
as: 'sourceDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
{
|
||||
model: Device,
|
||||
as: 'targetDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 自动将源端口和目标端口状态设为occupied
|
||||
@@ -340,44 +352,49 @@ router.post('/', async (req, res) => {
|
||||
router.post('/batch', async (req, res) => {
|
||||
try {
|
||||
const { cables } = req.body;
|
||||
|
||||
|
||||
if (!cables || !Array.isArray(cables) || cables.length === 0) {
|
||||
return res.status(400).json({ error: '请提供有效的接线数据' });
|
||||
}
|
||||
|
||||
|
||||
const results = {
|
||||
total: cables.length,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
errors: []
|
||||
errors: [],
|
||||
};
|
||||
|
||||
|
||||
for (let i = 0; i < cables.length; i++) {
|
||||
const cableData = cables[i];
|
||||
|
||||
|
||||
try {
|
||||
if (!cableData.cableId || !cableData.sourceDeviceId || !cableData.sourcePort ||
|
||||
!cableData.targetDeviceId || !cableData.targetPort) {
|
||||
if (
|
||||
!cableData.cableId ||
|
||||
!cableData.sourceDeviceId ||
|
||||
!cableData.sourcePort ||
|
||||
!cableData.targetDeviceId ||
|
||||
!cableData.targetPort
|
||||
) {
|
||||
throw new Error('缺少必填字段');
|
||||
}
|
||||
|
||||
|
||||
if (cableData.sourceDeviceId === cableData.targetDeviceId) {
|
||||
throw new Error('源设备和目标设备不能相同');
|
||||
}
|
||||
|
||||
|
||||
const existingCable = await Cable.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ sourceDeviceId: cableData.sourceDeviceId, sourcePort: cableData.sourcePort },
|
||||
{ targetDeviceId: cableData.targetDeviceId, targetPort: cableData.targetPort }
|
||||
]
|
||||
}
|
||||
{ targetDeviceId: cableData.targetDeviceId, targetPort: cableData.targetPort },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (existingCable) {
|
||||
throw new Error('端口已被占用');
|
||||
}
|
||||
|
||||
|
||||
await Cable.create({
|
||||
cableId: cableData.cableId,
|
||||
sourceDeviceId: cableData.sourceDeviceId,
|
||||
@@ -387,24 +404,24 @@ router.post('/batch', async (req, res) => {
|
||||
cableType: cableData.cableType || 'ethernet',
|
||||
cableLength: cableData.cableLength,
|
||||
status: cableData.status || 'normal',
|
||||
description: cableData.description
|
||||
description: cableData.description,
|
||||
});
|
||||
|
||||
|
||||
// 自动将源端口和目标端口状态设为occupied
|
||||
await occupyPort(cableData.sourceDeviceId, cableData.sourcePort);
|
||||
await occupyPort(cableData.targetDeviceId, cableData.targetPort);
|
||||
|
||||
|
||||
results.success++;
|
||||
} catch (error) {
|
||||
results.failed++;
|
||||
results.errors.push({
|
||||
index: i + 1,
|
||||
cableId: cableData.cableId,
|
||||
error: error.message
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
console.error('批量创建接线失败:', error);
|
||||
@@ -416,48 +433,53 @@ router.put('/:cableId', async (req, res) => {
|
||||
try {
|
||||
// 获取更新前的接线信息
|
||||
const oldCable = await Cable.findByPk(req.params.cableId);
|
||||
|
||||
|
||||
if (!oldCable) {
|
||||
return res.status(404).json({ error: '接线不存在' });
|
||||
}
|
||||
|
||||
const { sourceDeviceId: oldSourceDeviceId, sourcePort: oldSourcePort, targetDeviceId: oldTargetDeviceId, targetPort: oldTargetPort } = oldCable;
|
||||
|
||||
|
||||
const {
|
||||
sourceDeviceId: oldSourceDeviceId,
|
||||
sourcePort: oldSourcePort,
|
||||
targetDeviceId: oldTargetDeviceId,
|
||||
targetPort: oldTargetPort,
|
||||
} = oldCable;
|
||||
|
||||
const [updated] = await Cable.update(req.body, {
|
||||
where: { cableId: req.params.cableId }
|
||||
where: { cableId: req.params.cableId },
|
||||
});
|
||||
|
||||
|
||||
if (updated) {
|
||||
const cable = await Cable.findByPk(req.params.cableId, {
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'sourceDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
{
|
||||
model: Device,
|
||||
as: 'targetDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
|
||||
|
||||
|
||||
// 同步更新端口状态
|
||||
// 源端口变更:释放旧端口,占用新端口
|
||||
if (oldSourceDeviceId !== sourceDeviceId || oldSourcePort !== sourcePort) {
|
||||
await freePort(oldSourceDeviceId, oldSourcePort);
|
||||
await occupyPort(sourceDeviceId, sourcePort);
|
||||
}
|
||||
|
||||
|
||||
// 目标端口变更:释放旧端口,占用新端口
|
||||
if (oldTargetDeviceId !== targetDeviceId || oldTargetPort !== targetPort) {
|
||||
await freePort(oldTargetDeviceId, oldTargetPort);
|
||||
await occupyPort(targetDeviceId, targetPort);
|
||||
}
|
||||
|
||||
|
||||
res.json(cable);
|
||||
} else {
|
||||
res.status(404).json({ error: '接线不存在' });
|
||||
@@ -472,22 +494,22 @@ router.delete('/:cableId', async (req, res) => {
|
||||
try {
|
||||
// 先获取接线信息,用于后续恢复端口状态
|
||||
const cable = await Cable.findByPk(req.params.cableId);
|
||||
|
||||
|
||||
if (!cable) {
|
||||
return res.status(404).json({ error: '接线不存在' });
|
||||
}
|
||||
|
||||
|
||||
const { sourceDeviceId, sourcePort, targetDeviceId, targetPort } = cable;
|
||||
|
||||
|
||||
const deleted = await Cable.destroy({
|
||||
where: { cableId: req.params.cableId }
|
||||
where: { cableId: req.params.cableId },
|
||||
});
|
||||
|
||||
|
||||
if (deleted) {
|
||||
// 自动将源端口和目标端口状态恢复为free
|
||||
await freePort(sourceDeviceId, sourcePort);
|
||||
await freePort(targetDeviceId, targetPort);
|
||||
|
||||
|
||||
res.status(204).json();
|
||||
} else {
|
||||
res.status(404).json({ error: '接线不存在' });
|
||||
@@ -501,29 +523,29 @@ router.delete('/:cableId', async (req, res) => {
|
||||
router.delete('/batch', async (req, res) => {
|
||||
try {
|
||||
const { cableIds } = req.body;
|
||||
|
||||
|
||||
if (!cableIds || !Array.isArray(cableIds) || cableIds.length === 0) {
|
||||
return res.status(400).json({ error: '请提供有效的接线ID列表' });
|
||||
}
|
||||
|
||||
|
||||
// 先获取所有要删除的接线信息,用于后续恢复端口状态
|
||||
const cables = await Cable.findAll({
|
||||
where: { cableId: { [Op.in]: cableIds } }
|
||||
where: { cableId: { [Op.in]: cableIds } },
|
||||
});
|
||||
|
||||
|
||||
const deletedCount = await Cable.destroy({
|
||||
where: { cableId: { [Op.in]: cableIds } }
|
||||
where: { cableId: { [Op.in]: cableIds } },
|
||||
});
|
||||
|
||||
|
||||
// 自动将所有相关端口状态恢复为free
|
||||
for (const cable of cables) {
|
||||
await freePort(cable.sourceDeviceId, cable.sourcePort);
|
||||
await freePort(cable.targetDeviceId, cable.targetPort);
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
message: `批量删除成功,已删除 ${deletedCount} 条接线`,
|
||||
deletedCount
|
||||
deletedCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除接线失败:', error);
|
||||
@@ -538,20 +560,20 @@ router.get('/:cableId', async (req, res) => {
|
||||
{
|
||||
model: Device,
|
||||
as: 'sourceDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
{
|
||||
model: Device,
|
||||
as: 'targetDevice',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
if (!cable) {
|
||||
return res.status(404).json({ error: '接线不存在' });
|
||||
}
|
||||
|
||||
|
||||
res.json(cable);
|
||||
} catch (error) {
|
||||
console.error('获取接线详情失败:', error);
|
||||
|
||||
@@ -13,7 +13,7 @@ router.get('/', async (req, res) => {
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ name: { [Op.like]: `%${keyword}%` } },
|
||||
{ description: { [Op.like]: `%${keyword}%` } }
|
||||
{ description: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -23,9 +23,12 @@ router.get('/', async (req, res) => {
|
||||
|
||||
const { count, rows } = await ConsumableCategory.findAndCountAll({
|
||||
where,
|
||||
order: [['sortOrder', 'ASC'], ['id', 'DESC']],
|
||||
order: [
|
||||
['sortOrder', 'ASC'],
|
||||
['id', 'DESC'],
|
||||
],
|
||||
offset,
|
||||
limit: parseInt(pageSize)
|
||||
limit: parseInt(pageSize),
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -33,7 +36,7 @@ router.get('/', async (req, res) => {
|
||||
total: count,
|
||||
currentPage: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(count / pageSize)
|
||||
totalPages: Math.ceil(count / pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -44,7 +47,10 @@ router.get('/list', async (req, res) => {
|
||||
try {
|
||||
const categories = await ConsumableCategory.findAll({
|
||||
where: { status: 'active' },
|
||||
order: [['sortOrder', 'ASC'], ['name', 'ASC']]
|
||||
order: [
|
||||
['sortOrder', 'ASC'],
|
||||
['name', 'ASC'],
|
||||
],
|
||||
});
|
||||
res.json(categories);
|
||||
} catch (error) {
|
||||
@@ -77,7 +83,7 @@ router.post('/', async (req, res) => {
|
||||
name,
|
||||
description,
|
||||
sortOrder: sortOrder || 0,
|
||||
status: status || 'active'
|
||||
status: status || 'active',
|
||||
});
|
||||
|
||||
res.status(201).json(category);
|
||||
@@ -106,7 +112,7 @@ router.put('/:id', async (req, res) => {
|
||||
name: name || category.name,
|
||||
description: description !== undefined ? description : category.description,
|
||||
sortOrder: sortOrder !== undefined ? sortOrder : category.sortOrder,
|
||||
status: status || category.status
|
||||
status: status || category.status,
|
||||
});
|
||||
|
||||
res.json(category);
|
||||
|
||||
@@ -10,42 +10,40 @@ router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { consumableId, type, startDate, endDate, page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
|
||||
const where = {};
|
||||
|
||||
|
||||
if (consumableId) {
|
||||
where.consumableId = consumableId;
|
||||
}
|
||||
|
||||
|
||||
if (type && type !== 'all') {
|
||||
where.type = type;
|
||||
}
|
||||
|
||||
|
||||
if (startDate && endDate) {
|
||||
where.createdAt = {
|
||||
[Op.between]: [new Date(startDate), new Date(endDate)]
|
||||
[Op.between]: [new Date(startDate), new Date(endDate)],
|
||||
};
|
||||
} else if (startDate) {
|
||||
where.createdAt = { [Op.gte]: new Date(startDate) };
|
||||
} else if (endDate) {
|
||||
where.createdAt = { [Op.lte]: new Date(endDate) };
|
||||
}
|
||||
|
||||
|
||||
const { count, rows } = await ConsumableRecord.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{ model: Consumable, as: 'consumable', attributes: ['name', 'category', 'unit'] }
|
||||
],
|
||||
include: [{ model: Consumable, as: 'consumable', attributes: ['name', 'category', 'unit'] }],
|
||||
offset,
|
||||
limit: parseInt(pageSize),
|
||||
order: [['createdAt', 'DESC']]
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
records: rows,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -54,19 +52,19 @@ router.get('/', async (req, res) => {
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
|
||||
try {
|
||||
const { consumableId, type, quantity, operator, reason, recipient, notes } = req.body;
|
||||
|
||||
|
||||
const consumable = await Consumable.findByPk(consumableId);
|
||||
if (!consumable) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json({ error: '耗材不存在' });
|
||||
}
|
||||
|
||||
|
||||
const previousStock = consumable.currentStock;
|
||||
let newStock;
|
||||
|
||||
|
||||
if (type === 'in') {
|
||||
newStock = previousStock + quantity;
|
||||
} else if (type === 'out') {
|
||||
@@ -79,41 +77,47 @@ router.post('/', async (req, res) => {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({ error: '操作类型无效' });
|
||||
}
|
||||
|
||||
await consumable.update({ currentStock: newStock }, { transaction });
|
||||
|
||||
const record = await ConsumableRecord.create({
|
||||
consumableId,
|
||||
type,
|
||||
quantity,
|
||||
previousStock,
|
||||
currentStock: newStock,
|
||||
operator,
|
||||
reason,
|
||||
recipient,
|
||||
notes
|
||||
}, { transaction });
|
||||
|
||||
await ConsumableLog.create({
|
||||
consumableId,
|
||||
consumableName: consumable.name,
|
||||
operationType: type,
|
||||
quantity: type === 'in' ? parseFloat(quantity) : -parseFloat(quantity),
|
||||
previousStock,
|
||||
currentStock: newStock,
|
||||
operator,
|
||||
reason,
|
||||
notes
|
||||
}, { transaction });
|
||||
await consumable.update({ currentStock: newStock }, { transaction });
|
||||
|
||||
const record = await ConsumableRecord.create(
|
||||
{
|
||||
consumableId,
|
||||
type,
|
||||
quantity,
|
||||
previousStock,
|
||||
currentStock: newStock,
|
||||
operator,
|
||||
reason,
|
||||
recipient,
|
||||
notes,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
await ConsumableLog.create(
|
||||
{
|
||||
consumableId,
|
||||
consumableName: consumable.name,
|
||||
operationType: type,
|
||||
quantity: type === 'in' ? parseFloat(quantity) : -parseFloat(quantity),
|
||||
previousStock,
|
||||
currentStock: newStock,
|
||||
operator,
|
||||
reason,
|
||||
notes,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
|
||||
res.status(201).json({
|
||||
record,
|
||||
consumable: {
|
||||
previousStock,
|
||||
currentStock: newStock
|
||||
}
|
||||
currentStock: newStock,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
@@ -131,10 +135,10 @@ router.get('/statistics', async (req, res) => {
|
||||
const startDateTime = new Date(startDate);
|
||||
const endDateTime = new Date(endDate);
|
||||
endDateTime.setHours(23, 59, 59, 999); // 设置 endDate 为当天最后一刻
|
||||
|
||||
|
||||
dateWhere.createdAt = {
|
||||
[Op.gte]: startDateTime,
|
||||
[Op.lte]: endDateTime
|
||||
[Op.lte]: endDateTime,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,14 +150,14 @@ router.get('/statistics', async (req, res) => {
|
||||
const records = await ConsumableRecord.findAll({
|
||||
where: dateWhere,
|
||||
include: [
|
||||
{
|
||||
model: Consumable,
|
||||
{
|
||||
model: Consumable,
|
||||
as: 'consumable',
|
||||
attributes: ['name', 'category'],
|
||||
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined
|
||||
}
|
||||
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined,
|
||||
},
|
||||
],
|
||||
attributes: ['type', 'quantity']
|
||||
attributes: ['type', 'quantity'],
|
||||
});
|
||||
|
||||
let inCount = 0;
|
||||
@@ -182,15 +186,15 @@ router.get('/statistics', async (req, res) => {
|
||||
const recentRecords = await ConsumableRecord.findAll({
|
||||
where: dateWhere,
|
||||
include: [
|
||||
{
|
||||
model: Consumable,
|
||||
{
|
||||
model: Consumable,
|
||||
as: 'consumable',
|
||||
attributes: ['name', 'category'],
|
||||
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined
|
||||
}
|
||||
where: Object.keys(consumableWhere).length > 0 ? consumableWhere : undefined,
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: 10
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -202,7 +206,7 @@ router.get('/statistics', async (req, res) => {
|
||||
byType: Object.entries(typeMap).map(([type, data]) => ({
|
||||
type,
|
||||
totalQuantity: data.totalQuantity,
|
||||
count: data.count
|
||||
count: data.count,
|
||||
})),
|
||||
recentRecords: recentRecords.map(record => ({
|
||||
recordId: record.recordId,
|
||||
@@ -213,8 +217,8 @@ router.get('/statistics', async (req, res) => {
|
||||
consumableId: record.consumableId,
|
||||
consumableName: record.consumable?.name || '未知耗材',
|
||||
category: record.consumable?.category || null,
|
||||
unit: record.consumable?.unit || '个'
|
||||
}))
|
||||
unit: record.consumable?.unit || '个',
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
|
||||
+451
-374
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,13 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { logDangerousOperation, getDangerousOperationsLogs, cleanOldLogs, DANGEROUS_OPERATION_TYPES, RISK_LEVELS, calculateRiskLevel } = require('../utils/dangerousOperationLogger');
|
||||
const {
|
||||
logDangerousOperation,
|
||||
getDangerousOperationsLogs,
|
||||
cleanOldLogs,
|
||||
DANGEROUS_OPERATION_TYPES,
|
||||
RISK_LEVELS,
|
||||
calculateRiskLevel,
|
||||
} = require('../utils/dangerousOperationLogger');
|
||||
|
||||
router.post('/log', async (req, res) => {
|
||||
try {
|
||||
@@ -20,10 +27,12 @@ router.post('/log', async (req, res) => {
|
||||
return res.status(400).json({ error: '缺少必需参数 operationType 或 operationName' });
|
||||
}
|
||||
|
||||
const riskLevel = metadata.riskLevel || calculateRiskLevel(operationType, metadata.itemCount || 1, {
|
||||
hasRelatedData: metadata.relatedDataCount > 0,
|
||||
isSystemLevel: metadata.isSystemLevel,
|
||||
});
|
||||
const riskLevel =
|
||||
metadata.riskLevel ||
|
||||
calculateRiskLevel(operationType, metadata.itemCount || 1, {
|
||||
hasRelatedData: metadata.relatedDataCount > 0,
|
||||
isSystemLevel: metadata.isSystemLevel,
|
||||
});
|
||||
|
||||
await logDangerousOperation(req, {
|
||||
operationType,
|
||||
@@ -49,7 +58,17 @@ router.post('/log', async (req, res) => {
|
||||
|
||||
router.get('/logs', async (req, res) => {
|
||||
try {
|
||||
const { operationType, targetType, success, startDate, endDate, username, riskLevel, page = 1, pageSize = 50 } = req.query;
|
||||
const {
|
||||
operationType,
|
||||
targetType,
|
||||
success,
|
||||
startDate,
|
||||
endDate,
|
||||
username,
|
||||
riskLevel,
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
} = req.query;
|
||||
|
||||
const filters = {
|
||||
operationType,
|
||||
@@ -121,14 +140,10 @@ router.get('/risk-assessment', async (req, res) => {
|
||||
try {
|
||||
const { operationType, itemCount, hasRelatedData, isSystemLevel } = req.query;
|
||||
|
||||
const riskLevel = calculateRiskLevel(
|
||||
operationType,
|
||||
parseInt(itemCount) || 1,
|
||||
{
|
||||
hasRelatedData: hasRelatedData === 'true',
|
||||
isSystemLevel: isSystemLevel === 'true',
|
||||
}
|
||||
);
|
||||
const riskLevel = calculateRiskLevel(operationType, parseInt(itemCount) || 1, {
|
||||
hasRelatedData: hasRelatedData === 'true',
|
||||
isSystemLevel: isSystemLevel === 'true',
|
||||
});
|
||||
|
||||
const riskDescriptions = {
|
||||
[RISK_LEVELS.EXTREME]: '极高风险操作,需要输入确认关键词才能执行',
|
||||
@@ -141,7 +156,12 @@ router.get('/risk-assessment', async (req, res) => {
|
||||
riskLevel,
|
||||
description: riskDescriptions[riskLevel],
|
||||
requiresKeyword: riskLevel === RISK_LEVELS.EXTREME,
|
||||
confirmationLevel: riskLevel === RISK_LEVELS.EXTREME ? 'KEYWORD' : riskLevel === RISK_LEVELS.HIGH ? 'ENHANCED' : 'STANDARD',
|
||||
confirmationLevel:
|
||||
riskLevel === RISK_LEVELS.EXTREME
|
||||
? 'KEYWORD'
|
||||
: riskLevel === RISK_LEVELS.HIGH
|
||||
? 'ENHANCED'
|
||||
: 'STANDARD',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to assess risk:', error);
|
||||
|
||||
@@ -6,7 +6,7 @@ const DeviceField = require('../models/DeviceField');
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const fields = await DeviceField.findAll({
|
||||
order: [['order', 'ASC']]
|
||||
order: [['order', 'ASC']],
|
||||
});
|
||||
res.json(fields);
|
||||
} catch (error) {
|
||||
@@ -41,7 +41,7 @@ router.post('/', async (req, res) => {
|
||||
router.put('/:fieldId', async (req, res) => {
|
||||
try {
|
||||
const [updated] = await DeviceField.update(req.body, {
|
||||
where: { fieldId: req.params.fieldId }
|
||||
where: { fieldId: req.params.fieldId },
|
||||
});
|
||||
if (updated) {
|
||||
const updatedField = await DeviceField.findByPk(req.params.fieldId);
|
||||
@@ -59,20 +59,20 @@ router.delete('/:fieldId', async (req, res) => {
|
||||
try {
|
||||
// 先查询字段信息
|
||||
const field = await DeviceField.findByPk(req.params.fieldId);
|
||||
|
||||
|
||||
if (!field) {
|
||||
return res.status(404).json({ error: '字段不存在' });
|
||||
}
|
||||
|
||||
|
||||
// 检查是否为系统字段
|
||||
if (field.isSystem) {
|
||||
return res.status(403).json({ error: '系统字段不可删除' });
|
||||
}
|
||||
|
||||
|
||||
const deleted = await DeviceField.destroy({
|
||||
where: { fieldId: req.params.fieldId }
|
||||
where: { fieldId: req.params.fieldId },
|
||||
});
|
||||
|
||||
|
||||
if (deleted) {
|
||||
res.status(204).json();
|
||||
} else {
|
||||
@@ -87,17 +87,17 @@ router.delete('/:fieldId', async (req, res) => {
|
||||
router.post('/config', async (req, res) => {
|
||||
try {
|
||||
const fieldConfigs = req.body;
|
||||
|
||||
|
||||
if (!Array.isArray(fieldConfigs)) {
|
||||
return res.status(400).json({ error: '输入必须是数组' });
|
||||
}
|
||||
|
||||
|
||||
const updatedFields = [];
|
||||
for (const config of fieldConfigs) {
|
||||
const existingField = await DeviceField.findOne({ where: { fieldName: config.fieldName } });
|
||||
|
||||
|
||||
if (existingField) {
|
||||
await existingField.update({
|
||||
await existingField.update({
|
||||
visible: config.visible !== undefined ? config.visible : existingField.visible,
|
||||
required: config.required !== undefined ? config.required : existingField.required,
|
||||
displayName: config.displayName || existingField.displayName,
|
||||
@@ -115,11 +115,11 @@ router.post('/config', async (req, res) => {
|
||||
updatedFields.push(newField);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
res.json(updatedFields);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
+109
-94
@@ -14,49 +14,49 @@ router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { deviceId, status, portType, portSpeed, page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
|
||||
const where = {};
|
||||
|
||||
|
||||
if (deviceId) {
|
||||
where.deviceId = deviceId;
|
||||
}
|
||||
|
||||
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
|
||||
if (portType && portType !== 'all') {
|
||||
where.portType = portType;
|
||||
}
|
||||
|
||||
|
||||
if (portSpeed && portSpeed !== 'all') {
|
||||
where.portSpeed = portSpeed;
|
||||
}
|
||||
|
||||
|
||||
const { count, rows } = await DevicePort.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
{
|
||||
model: NetworkCard,
|
||||
as: 'networkCard',
|
||||
attributes: ['nicId', 'name']
|
||||
}
|
||||
attributes: ['nicId', 'name'],
|
||||
},
|
||||
],
|
||||
offset,
|
||||
limit: parseInt(pageSize),
|
||||
order: [['createdAt', 'DESC']]
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
ports: rows,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取端口列表失败:', error);
|
||||
@@ -68,24 +68,24 @@ router.get('/', async (req, res) => {
|
||||
router.get('/device/:deviceId', async (req, res) => {
|
||||
try {
|
||||
const { deviceId } = req.params;
|
||||
|
||||
|
||||
const ports = await DevicePort.findAll({
|
||||
where: { deviceId },
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
{
|
||||
model: NetworkCard,
|
||||
as: 'networkCard',
|
||||
attributes: ['nicId', 'name']
|
||||
}
|
||||
attributes: ['nicId', 'name'],
|
||||
},
|
||||
],
|
||||
order: [['portName', 'ASC']]
|
||||
order: [['portName', 'ASC']],
|
||||
});
|
||||
|
||||
|
||||
res.json(ports);
|
||||
} catch (error) {
|
||||
console.error('获取设备端口失败:', error);
|
||||
@@ -95,22 +95,23 @@ router.get('/device/:deviceId', async (req, res) => {
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { portId, deviceId, nicId, portName, portType, portSpeed, status, vlanId, description } = req.body;
|
||||
|
||||
const { portId, deviceId, nicId, portName, portType, portSpeed, status, vlanId, description } =
|
||||
req.body;
|
||||
|
||||
if (!deviceId || !portName) {
|
||||
return res.status(400).json({ error: '缺少必填字段' });
|
||||
}
|
||||
|
||||
|
||||
const existingPort = await DevicePort.findOne({
|
||||
where: { deviceId, portName }
|
||||
where: { deviceId, portName },
|
||||
});
|
||||
|
||||
|
||||
if (existingPort) {
|
||||
return res.status(400).json({ error: '该设备的端口名称已存在' });
|
||||
}
|
||||
|
||||
|
||||
const autoPortId = portId || `PORT-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
|
||||
|
||||
const port = await DevicePort.create({
|
||||
portId: autoPortId,
|
||||
deviceId,
|
||||
@@ -120,19 +121,19 @@ router.post('/', async (req, res) => {
|
||||
portSpeed: portSpeed || '1G',
|
||||
status: status || 'free',
|
||||
vlanId,
|
||||
description
|
||||
description,
|
||||
});
|
||||
|
||||
|
||||
const createdPort = await DevicePort.findByPk(port.portId, {
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
res.status(201).json(createdPort);
|
||||
} catch (error) {
|
||||
console.error('创建端口失败:', error);
|
||||
@@ -154,7 +155,7 @@ router.post('/batch', async (req, res) => {
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
updated: 0,
|
||||
errors: []
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const transaction = await DevicePort.sequelize.transaction();
|
||||
@@ -177,7 +178,9 @@ router.post('/batch', async (req, res) => {
|
||||
|
||||
if (isServer) {
|
||||
if (!portData.nicId && !portData.网卡名称) {
|
||||
throw new Error(`服务器 ${portData.deviceId} 的端口必须关联网卡,请先在网卡管理中添加网卡`);
|
||||
throw new Error(
|
||||
`服务器 ${portData.deviceId} 的端口必须关联网卡,请先在网卡管理中添加网卡`
|
||||
);
|
||||
}
|
||||
|
||||
let nicId = portData.nicId;
|
||||
@@ -185,10 +188,12 @@ router.post('/batch', async (req, res) => {
|
||||
if (!nicId && portData.网卡名称) {
|
||||
const networkCard = await NetworkCard.findOne({
|
||||
where: { deviceId: portData.deviceId, name: portData.网卡名称 },
|
||||
transaction
|
||||
transaction,
|
||||
});
|
||||
if (!networkCard) {
|
||||
throw new Error(`服务器 ${portData.deviceId} 的网卡"${portData.网卡名称}"不存在,请先在网卡管理中添加该网卡`);
|
||||
throw new Error(
|
||||
`服务器 ${portData.deviceId} 的网卡"${portData.网卡名称}"不存在,请先在网卡管理中添加该网卡`
|
||||
);
|
||||
}
|
||||
nicId = networkCard.nicId;
|
||||
}
|
||||
@@ -212,7 +217,7 @@ router.post('/batch', async (req, res) => {
|
||||
|
||||
const existingPort = await DevicePort.findOne({
|
||||
where: { deviceId: portData.deviceId, portName: portData.portName },
|
||||
transaction
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (existingPort) {
|
||||
@@ -221,17 +226,23 @@ router.post('/batch', async (req, res) => {
|
||||
continue;
|
||||
}
|
||||
if (updateExisting) {
|
||||
await DevicePort.update({
|
||||
portType: portData.portType || existingPort.portType,
|
||||
portSpeed: portData.portSpeed || existingPort.portSpeed,
|
||||
status: portData.status || existingPort.status,
|
||||
vlanId: portData.vlanId !== undefined ? portData.vlanId : existingPort.vlanId,
|
||||
description: portData.description !== undefined ? portData.description : existingPort.description,
|
||||
nicId: portData.nicId !== undefined ? portData.nicId : existingPort.nicId
|
||||
}, {
|
||||
where: { portId: existingPort.portId },
|
||||
transaction
|
||||
});
|
||||
await DevicePort.update(
|
||||
{
|
||||
portType: portData.portType || existingPort.portType,
|
||||
portSpeed: portData.portSpeed || existingPort.portSpeed,
|
||||
status: portData.status || existingPort.status,
|
||||
vlanId: portData.vlanId !== undefined ? portData.vlanId : existingPort.vlanId,
|
||||
description:
|
||||
portData.description !== undefined
|
||||
? portData.description
|
||||
: existingPort.description,
|
||||
nicId: portData.nicId !== undefined ? portData.nicId : existingPort.nicId,
|
||||
},
|
||||
{
|
||||
where: { portId: existingPort.portId },
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
results.updated++;
|
||||
results.success++;
|
||||
continue;
|
||||
@@ -239,17 +250,20 @@ router.post('/batch', async (req, res) => {
|
||||
throw new Error('该设备的端口名称已存在');
|
||||
}
|
||||
|
||||
await DevicePort.create({
|
||||
portId: portData.portId,
|
||||
deviceId: portData.deviceId,
|
||||
nicId: portData.nicId || null,
|
||||
portName: portData.portName,
|
||||
portType: portData.portType || 'RJ45',
|
||||
portSpeed: portData.portSpeed || '1G',
|
||||
status: portData.status || 'free',
|
||||
vlanId: portData.vlanId,
|
||||
description: portData.description
|
||||
}, { transaction });
|
||||
await DevicePort.create(
|
||||
{
|
||||
portId: portData.portId,
|
||||
deviceId: portData.deviceId,
|
||||
nicId: portData.nicId || null,
|
||||
portName: portData.portName,
|
||||
portType: portData.portType || 'RJ45',
|
||||
portSpeed: portData.portSpeed || '1G',
|
||||
status: portData.status || 'free',
|
||||
vlanId: portData.vlanId,
|
||||
description: portData.description,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
results.success++;
|
||||
} catch (error) {
|
||||
@@ -259,7 +273,7 @@ router.post('/batch', async (req, res) => {
|
||||
portId: portData.portId,
|
||||
deviceId: portData.deviceId,
|
||||
portName: portData.portName,
|
||||
error: error.message
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -279,18 +293,18 @@ router.post('/batch', async (req, res) => {
|
||||
router.put('/:portId', async (req, res) => {
|
||||
try {
|
||||
const [updated] = await DevicePort.update(req.body, {
|
||||
where: { portId: req.params.portId }
|
||||
where: { portId: req.params.portId },
|
||||
});
|
||||
|
||||
|
||||
if (updated) {
|
||||
const port = await DevicePort.findByPk(req.params.portId, {
|
||||
include: [
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
],
|
||||
});
|
||||
res.json(port);
|
||||
} else {
|
||||
@@ -313,9 +327,9 @@ router.delete('/:portId', async (req, res) => {
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ sourceDeviceId: port.deviceId, sourcePort: port.portName },
|
||||
{ targetDeviceId: port.deviceId, targetPort: port.portName }
|
||||
]
|
||||
}
|
||||
{ targetDeviceId: port.deviceId, targetPort: port.portName },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (relatedCables.length > 0) {
|
||||
@@ -326,13 +340,13 @@ router.delete('/:portId', async (req, res) => {
|
||||
sourceDeviceId: c.sourceDeviceId,
|
||||
sourcePort: c.sourcePort,
|
||||
targetDeviceId: c.targetDeviceId,
|
||||
targetPort: c.targetPort
|
||||
}))
|
||||
targetPort: c.targetPort,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
await DevicePort.destroy({
|
||||
where: { portId: req.params.portId }
|
||||
where: { portId: req.params.portId },
|
||||
});
|
||||
|
||||
res.status(204).json();
|
||||
@@ -351,12 +365,12 @@ router.delete('/batch', async (req, res) => {
|
||||
}
|
||||
|
||||
const deletedCount = await DevicePort.destroy({
|
||||
where: { portId: { [Op.in]: portIds } }
|
||||
where: { portId: { [Op.in]: portIds } },
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
|
||||
deletedCount
|
||||
deletedCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除端口失败:', error);
|
||||
@@ -373,12 +387,12 @@ router.post('/batch-delete', async (req, res) => {
|
||||
}
|
||||
|
||||
const deletedCount = await DevicePort.destroy({
|
||||
where: { portId: { [Op.in]: portIds } }
|
||||
where: { portId: { [Op.in]: portIds } },
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: `批量删除成功,已删除 ${deletedCount} 个端口`,
|
||||
deletedCount
|
||||
deletedCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除端口失败:', error);
|
||||
@@ -393,9 +407,9 @@ router.get('/:portId', async (req, res) => {
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type', 'rackId'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!port) {
|
||||
@@ -457,24 +471,24 @@ router.get('/export/all', async (req, res) => {
|
||||
{
|
||||
model: require('../models/Room'),
|
||||
as: 'room',
|
||||
attributes: ['roomId', 'name']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
attributes: ['roomId', 'name'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
model: NetworkCard,
|
||||
as: 'networkCard',
|
||||
attributes: ['nicId', 'name']
|
||||
}
|
||||
attributes: ['nicId', 'name'],
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: parsedPageSize,
|
||||
offset: offset,
|
||||
subQuery: false
|
||||
subQuery: false,
|
||||
}),
|
||||
timeoutPromise
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
const ports = countResult;
|
||||
@@ -482,7 +496,7 @@ router.get('/export/all', async (req, res) => {
|
||||
const statusMap = {
|
||||
free: '空闲',
|
||||
occupied: '占用',
|
||||
fault: '故障'
|
||||
fault: '故障',
|
||||
};
|
||||
|
||||
const exportData = ports.map(port => ({
|
||||
@@ -499,17 +513,18 @@ router.get('/export/all', async (req, res) => {
|
||||
状态: statusMap[port.status] || port.status,
|
||||
VLAN_ID: port.vlanId || '-',
|
||||
描述: port.description || '-',
|
||||
创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-'
|
||||
创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-',
|
||||
}));
|
||||
|
||||
let filteredExportData = exportData;
|
||||
if (keyword) {
|
||||
const searchLower = keyword.toLowerCase();
|
||||
filteredExportData = exportData.filter(item =>
|
||||
item.端口名称?.toLowerCase().includes(searchLower) ||
|
||||
item.端口类型?.toLowerCase().includes(searchLower) ||
|
||||
item.设备名称?.toLowerCase().includes(searchLower) ||
|
||||
item.描述?.toLowerCase().includes(searchLower)
|
||||
filteredExportData = exportData.filter(
|
||||
item =>
|
||||
item.端口名称?.toLowerCase().includes(searchLower) ||
|
||||
item.端口类型?.toLowerCase().includes(searchLower) ||
|
||||
item.设备名称?.toLowerCase().includes(searchLower) ||
|
||||
item.描述?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -517,7 +532,7 @@ router.get('/export/all', async (req, res) => {
|
||||
page: parsedPage,
|
||||
pageSize: parsedPageSize,
|
||||
total: filteredExportData.length,
|
||||
ports: filteredExportData
|
||||
ports: filteredExportData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('导出端口失败:', error);
|
||||
|
||||
+570
-381
File diff suppressed because it is too large
Load Diff
+368
-211
@@ -4,15 +4,19 @@ const { Op } = require('sequelize');
|
||||
const Device = require('../models/Device');
|
||||
const Rack = require('../models/Rack');
|
||||
const Room = require('../models/Room');
|
||||
const { logDeviceOperation, generateDeviceDescription, buildDeviceMetadata } = require('../utils/operationLogger');
|
||||
const {
|
||||
logDeviceOperation,
|
||||
generateDeviceDescription,
|
||||
buildDeviceMetadata,
|
||||
} = require('../utils/operationLogger');
|
||||
|
||||
async function generateIdleDeviceId() {
|
||||
const devices = await Device.findAll({
|
||||
where: {
|
||||
deviceId: {
|
||||
[Op.like]: 'DEV%'
|
||||
}
|
||||
}
|
||||
[Op.like]: 'DEV%',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let maxNumber = 0;
|
||||
@@ -41,7 +45,7 @@ router.get('/', async (req, res) => {
|
||||
where[Op.or] = [
|
||||
{ deviceId: { [Op.like]: `%${keyword}%` } },
|
||||
{ name: { [Op.like]: `%${keyword}%` } },
|
||||
{ serialNumber: { [Op.like]: `%${keyword}%` } }
|
||||
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -59,22 +63,24 @@ router.get('/', async (req, res) => {
|
||||
{
|
||||
model: Rack,
|
||||
attributes: ['rackId', 'name', 'roomId'],
|
||||
include: [{
|
||||
model: Room,
|
||||
attributes: ['roomId', 'name']
|
||||
}]
|
||||
}
|
||||
include: [
|
||||
{
|
||||
model: Room,
|
||||
attributes: ['roomId', 'name'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
offset: parseInt(offset),
|
||||
limit: parseInt(pageSize),
|
||||
order: [['idleDate', 'DESC']]
|
||||
order: [['idleDate', 'DESC']],
|
||||
});
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
idleDevices: rows,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取空闲设备列表失败:', error);
|
||||
@@ -90,12 +96,14 @@ router.get('/:deviceId', async (req, res) => {
|
||||
{
|
||||
model: Rack,
|
||||
attributes: ['rackId', 'name', 'roomId'],
|
||||
include: [{
|
||||
model: Room,
|
||||
attributes: ['roomId', 'name']
|
||||
}]
|
||||
}
|
||||
]
|
||||
include: [
|
||||
{
|
||||
model: Room,
|
||||
attributes: ['roomId', 'name'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
@@ -110,7 +118,18 @@ router.get('/:deviceId', async (req, res) => {
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { name, type, model, serialNumber, powerConsumption, idleReason, warehouseId, description, rackId, position } = req.body;
|
||||
const {
|
||||
name,
|
||||
type,
|
||||
model,
|
||||
serialNumber,
|
||||
powerConsumption,
|
||||
idleReason,
|
||||
warehouseId,
|
||||
description,
|
||||
rackId,
|
||||
position,
|
||||
} = req.body;
|
||||
|
||||
let { deviceId } = req.body;
|
||||
|
||||
@@ -144,24 +163,35 @@ router.post('/', async (req, res) => {
|
||||
warehouseId: warehouseId || null,
|
||||
rackId: rackId || null,
|
||||
position: position || null,
|
||||
sourceType: warehouseId ? 'warehouse' : (rackId ? 'rack' : 'rack'),
|
||||
description: description || ''
|
||||
sourceType: warehouseId ? 'warehouse' : rackId ? 'rack' : 'rack',
|
||||
description: description || '',
|
||||
});
|
||||
|
||||
await logDeviceOperation('create', generateDeviceDescription('新增空闲设备', {
|
||||
deviceId: device.deviceId,
|
||||
name: device.name || deviceId,
|
||||
type: device.type,
|
||||
model: device.model,
|
||||
serialNumber: device.serialNumber,
|
||||
ipAddress: device.ipAddress
|
||||
}, { includeRack: false }), {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name || deviceId,
|
||||
afterState: device.toJSON(),
|
||||
req,
|
||||
metadata: buildDeviceMetadata(device.toJSON(), { sourceType: device.sourceType, type: 'idle_device_create' })
|
||||
});
|
||||
await logDeviceOperation(
|
||||
'create',
|
||||
generateDeviceDescription(
|
||||
'新增空闲设备',
|
||||
{
|
||||
deviceId: device.deviceId,
|
||||
name: device.name || deviceId,
|
||||
type: device.type,
|
||||
model: device.model,
|
||||
serialNumber: device.serialNumber,
|
||||
ipAddress: device.ipAddress,
|
||||
},
|
||||
{ includeRack: false }
|
||||
),
|
||||
{
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name || deviceId,
|
||||
afterState: device.toJSON(),
|
||||
req,
|
||||
metadata: buildDeviceMetadata(device.toJSON(), {
|
||||
sourceType: device.sourceType,
|
||||
type: 'idle_device_create',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
res.status(201).json(device);
|
||||
} catch (error) {
|
||||
@@ -186,13 +216,16 @@ router.post('/from-device/:deviceId', async (req, res) => {
|
||||
return res.status(400).json({ error: '设备已经标记为空闲设备' });
|
||||
}
|
||||
|
||||
await device.update({
|
||||
isIdle: true,
|
||||
status: 'idle',
|
||||
idleDate: new Date(),
|
||||
idleReason: idleReason || `从设备管理转入`,
|
||||
sourceType: 'rack'
|
||||
}, { transaction: t });
|
||||
await device.update(
|
||||
{
|
||||
isIdle: true,
|
||||
status: 'idle',
|
||||
idleDate: new Date(),
|
||||
idleReason: idleReason || `从设备管理转入`,
|
||||
sourceType: 'rack',
|
||||
},
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
await t.commit();
|
||||
|
||||
@@ -203,12 +236,12 @@ router.post('/from-device/:deviceId', async (req, res) => {
|
||||
beforeState: { ...deviceData, isIdle: false },
|
||||
afterState: { ...deviceData, isIdle: true },
|
||||
req,
|
||||
metadata: buildDeviceMetadata(deviceData, { idleReason, type: 'device_to_idle' })
|
||||
metadata: buildDeviceMetadata(deviceData, { idleReason, type: 'device_to_idle' }),
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: '设备已转入空闲设备',
|
||||
device: device.toJSON()
|
||||
device: device.toJSON(),
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
@@ -229,7 +262,7 @@ router.post('/batch-from-devices', async (req, res) => {
|
||||
|
||||
const devices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
transaction: t
|
||||
transaction: t,
|
||||
});
|
||||
|
||||
const notIdleDevices = devices.filter(d => !d.isIdle);
|
||||
@@ -241,11 +274,11 @@ router.post('/batch-from-devices', async (req, res) => {
|
||||
isIdle: true,
|
||||
status: 'idle',
|
||||
idleDate: new Date(),
|
||||
idleReason: idleReason || `批量转入`
|
||||
idleReason: idleReason || `批量转入`,
|
||||
},
|
||||
{
|
||||
where: { deviceId: { [Op.in]: notIdleDevices.map(d => d.deviceId) } },
|
||||
transaction: t
|
||||
transaction: t,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -253,22 +286,29 @@ router.post('/batch-from-devices', async (req, res) => {
|
||||
await t.commit();
|
||||
|
||||
const deviceDetails = notIdleDevices.map(d => d.toJSON());
|
||||
const deviceSummary = deviceDetails.map(d =>
|
||||
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.serialNumber ? `,序列号:${d.serialNumber}` : ''})`
|
||||
).join('、');
|
||||
const deviceSummary = deviceDetails
|
||||
.map(
|
||||
d =>
|
||||
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.serialNumber ? `,序列号:${d.serialNumber}` : ''})`
|
||||
)
|
||||
.join('、');
|
||||
|
||||
await logDeviceOperation('batch_to_idle', `批量将 ${notIdleDevices.length} 台设备转入空闲设备:${deviceSummary}`, {
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${notIdleDevices.length}台设备`,
|
||||
req,
|
||||
metadata: { idleReason, type: 'batch_device_to_idle', devices: deviceDetails }
|
||||
});
|
||||
await logDeviceOperation(
|
||||
'batch_to_idle',
|
||||
`批量将 ${notIdleDevices.length} 台设备转入空闲设备:${deviceSummary}`,
|
||||
{
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${notIdleDevices.length}台设备`,
|
||||
req,
|
||||
metadata: { idleReason, type: 'batch_device_to_idle', devices: deviceDetails },
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: `成功将 ${notIdleDevices.length} 台设备转入空闲设备`,
|
||||
total: devices.length,
|
||||
updated: notIdleDevices.length,
|
||||
skipped: alreadyIdleDevices.length
|
||||
skipped: alreadyIdleDevices.length,
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
@@ -305,23 +345,29 @@ router.put('/batch-restore', async (req, res) => {
|
||||
|
||||
const idleDevices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
|
||||
transaction: t
|
||||
transaction: t,
|
||||
});
|
||||
|
||||
console.log('查询到的空闲设备数量:', idleDevices.length);
|
||||
if (idleDevices.length > 0) {
|
||||
console.log('查询到的设备ID:', idleDevices.map(d => d.deviceId));
|
||||
console.log(
|
||||
'查询到的设备ID:',
|
||||
idleDevices.map(d => d.deviceId)
|
||||
);
|
||||
}
|
||||
|
||||
if (idleDevices.length === 0) {
|
||||
console.log('没有找到空闲设备,检查设备是否存在:');
|
||||
const allDevices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
transaction: t
|
||||
transaction: t,
|
||||
});
|
||||
console.log('设备表中存在的设备数量:', allDevices.length);
|
||||
if (allDevices.length > 0) {
|
||||
console.log('存在的设备及其 isIdle 状态:', allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })));
|
||||
console.log(
|
||||
'存在的设备及其 isIdle 状态:',
|
||||
allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle }))
|
||||
);
|
||||
}
|
||||
|
||||
await t.rollback();
|
||||
@@ -333,7 +379,9 @@ router.put('/batch-restore', async (req, res) => {
|
||||
|
||||
for (const device of idleDevices) {
|
||||
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
|
||||
if (!deviceConfig) continue;
|
||||
if (!deviceConfig) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetRackId = deviceConfig.targetRackId;
|
||||
const targetPosition = deviceConfig.targetPosition;
|
||||
@@ -343,7 +391,7 @@ router.put('/batch-restore', async (req, res) => {
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'skipped',
|
||||
reason: '未指定目标机柜'
|
||||
reason: '未指定目标机柜',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -354,7 +402,7 @@ router.put('/batch-restore', async (req, res) => {
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'failed',
|
||||
reason: '目标机柜不存在'
|
||||
reason: '目标机柜不存在',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -368,25 +416,31 @@ router.put('/batch-restore', async (req, res) => {
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'failed',
|
||||
reason: `U位${position}已被占用`
|
||||
reason: `U位${position}已被占用`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
await device.update({
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
rackId: targetRackId,
|
||||
position: position,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'offline'
|
||||
}, { transaction: t });
|
||||
await device.update(
|
||||
{
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
rackId: targetRackId,
|
||||
position: position,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'offline',
|
||||
},
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
await targetRack.update({
|
||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
|
||||
}, { transaction: t });
|
||||
await targetRack.update(
|
||||
{
|
||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
|
||||
},
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
restoredCount++;
|
||||
results.push({
|
||||
@@ -394,7 +448,7 @@ router.put('/batch-restore', async (req, res) => {
|
||||
name: device.name,
|
||||
status: 'success',
|
||||
targetRack: targetRack.name,
|
||||
targetPosition: position
|
||||
targetPosition: position,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -404,17 +458,30 @@ router.put('/batch-restore', async (req, res) => {
|
||||
const failedCount = results.filter(r => r.status === 'failed').length;
|
||||
const skippedCount = results.filter(r => r.status === 'skipped').length;
|
||||
|
||||
const successDevices = idleDevices.filter(d => results.some(r => r.deviceId === d.deviceId && r.status === 'success'));
|
||||
const deviceSummary = successDevices.map(d =>
|
||||
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
|
||||
).join('、');
|
||||
const successDevices = idleDevices.filter(d =>
|
||||
results.some(r => r.deviceId === d.deviceId && r.status === 'success')
|
||||
);
|
||||
const deviceSummary = successDevices
|
||||
.map(
|
||||
d =>
|
||||
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
|
||||
)
|
||||
.join('、');
|
||||
|
||||
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备:${deviceSummary}`, {
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${successCount}台设备`,
|
||||
req,
|
||||
metadata: { results, type: 'batch_idle_device_restore', devices: successDevices.map(d => d.toJSON()) }
|
||||
});
|
||||
await logDeviceOperation(
|
||||
'batch_restore',
|
||||
`批量上架 ${successCount} 台空闲设备:${deviceSummary}`,
|
||||
{
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${successCount}台设备`,
|
||||
req,
|
||||
metadata: {
|
||||
results,
|
||||
type: 'batch_idle_device_restore',
|
||||
devices: successDevices.map(d => d.toJSON()),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: `成功上架 ${successCount} 台设备`,
|
||||
@@ -422,7 +489,7 @@ router.put('/batch-restore', async (req, res) => {
|
||||
restored: successCount,
|
||||
failed: failedCount,
|
||||
skipped: skippedCount,
|
||||
details: results
|
||||
details: results,
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
@@ -435,11 +502,21 @@ router.put('/:deviceId/shelve', async (req, res) => {
|
||||
const t = await require('../db').sequelize.transaction();
|
||||
try {
|
||||
const { deviceId } = req.params;
|
||||
const { name, type, model, serialNumber, height, powerConsumption, rackId, position, description } = req.body;
|
||||
const {
|
||||
name,
|
||||
type,
|
||||
model,
|
||||
serialNumber,
|
||||
height,
|
||||
powerConsumption,
|
||||
rackId,
|
||||
position,
|
||||
description,
|
||||
} = req.body;
|
||||
|
||||
const device = await Device.findOne({
|
||||
where: { deviceId, isIdle: true },
|
||||
transaction: t
|
||||
transaction: t,
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
@@ -467,54 +544,63 @@ router.put('/:deviceId/shelve', async (req, res) => {
|
||||
|
||||
const beforeState = device.toJSON();
|
||||
|
||||
await device.update({
|
||||
name: name || device.name,
|
||||
type: type || device.type,
|
||||
model: model || device.model,
|
||||
serialNumber: serialNumber || device.serialNumber,
|
||||
height: deviceHeight,
|
||||
powerConsumption: powerConsumption || device.powerConsumption || 0,
|
||||
rackId: rackId,
|
||||
position: position,
|
||||
description: description || device.description,
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'running'
|
||||
}, { transaction: t });
|
||||
await device.update(
|
||||
{
|
||||
name: name || device.name,
|
||||
type: type || device.type,
|
||||
model: model || device.model,
|
||||
serialNumber: serialNumber || device.serialNumber,
|
||||
height: deviceHeight,
|
||||
powerConsumption: powerConsumption || device.powerConsumption || 0,
|
||||
rackId: rackId,
|
||||
position: position,
|
||||
description: description || device.description,
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'running',
|
||||
},
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
await targetRack.update({
|
||||
currentPower: targetRack.currentPower + (powerConsumption || device.powerConsumption || 0)
|
||||
}, { transaction: t });
|
||||
await targetRack.update(
|
||||
{
|
||||
currentPower: targetRack.currentPower + (powerConsumption || device.powerConsumption || 0),
|
||||
},
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
await t.commit();
|
||||
|
||||
const updatedDevice = await Device.findByPk(deviceId, {
|
||||
include: [
|
||||
{ model: Rack, include: [Room] }
|
||||
]
|
||||
include: [{ model: Rack, include: [Room] }],
|
||||
});
|
||||
|
||||
const deviceData = {
|
||||
...updatedDevice.toJSON(),
|
||||
rackName: targetRack.name,
|
||||
roomName: updatedDevice.Rack?.Room?.name
|
||||
roomName: updatedDevice.Rack?.Room?.name,
|
||||
};
|
||||
|
||||
await logDeviceOperation('shelve', generateDeviceDescription('空闲设备上架', deviceData, { includePosition: false }) + `到机柜【${targetRack.name}】U${position}`, {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState: { ...beforeState, isIdle: true },
|
||||
afterState: deviceData,
|
||||
req,
|
||||
metadata: buildDeviceMetadata(deviceData, { rackId, position, type: 'idle_device_shelve' })
|
||||
});
|
||||
await logDeviceOperation(
|
||||
'shelve',
|
||||
generateDeviceDescription('空闲设备上架', deviceData, { includePosition: false }) +
|
||||
`到机柜【${targetRack.name}】U${position}`,
|
||||
{
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState: { ...beforeState, isIdle: true },
|
||||
afterState: deviceData,
|
||||
req,
|
||||
metadata: buildDeviceMetadata(deviceData, { rackId, position, type: 'idle_device_shelve' }),
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: '设备上架成功',
|
||||
device: updatedDevice
|
||||
device: updatedDevice,
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
@@ -526,7 +612,7 @@ router.put('/:deviceId/shelve', async (req, res) => {
|
||||
router.put('/:deviceId', async (req, res) => {
|
||||
try {
|
||||
const device = await Device.findOne({
|
||||
where: { deviceId: req.params.deviceId, isIdle: true }
|
||||
where: { deviceId: req.params.deviceId, isIdle: true },
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
@@ -534,7 +620,14 @@ router.put('/:deviceId', async (req, res) => {
|
||||
}
|
||||
|
||||
const beforeState = device.toJSON();
|
||||
const allowedFields = ['name', 'type', 'model', 'idleReason', 'description', 'powerConsumption'];
|
||||
const allowedFields = [
|
||||
'name',
|
||||
'type',
|
||||
'model',
|
||||
'idleReason',
|
||||
'description',
|
||||
'powerConsumption',
|
||||
];
|
||||
|
||||
allowedFields.forEach(field => {
|
||||
if (req.body[field] !== undefined) {
|
||||
@@ -566,14 +659,18 @@ router.put('/:deviceId', async (req, res) => {
|
||||
|
||||
await device.save();
|
||||
|
||||
await logDeviceOperation('update', generateDeviceDescription('更新空闲设备', device.toJSON(), { includeRack: false }), {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState,
|
||||
afterState: device.toJSON(),
|
||||
req,
|
||||
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_update' })
|
||||
});
|
||||
await logDeviceOperation(
|
||||
'update',
|
||||
generateDeviceDescription('更新空闲设备', device.toJSON(), { includeRack: false }),
|
||||
{
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState,
|
||||
afterState: device.toJSON(),
|
||||
req,
|
||||
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_update' }),
|
||||
}
|
||||
);
|
||||
|
||||
res.json(device);
|
||||
} catch (error) {
|
||||
@@ -589,7 +686,7 @@ router.put('/:deviceId/restore', async (req, res) => {
|
||||
|
||||
const device = await Device.findOne({
|
||||
where: { deviceId, isIdle: true },
|
||||
transaction: t
|
||||
transaction: t,
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
@@ -608,53 +705,72 @@ router.put('/:deviceId/restore', async (req, res) => {
|
||||
return res.status(404).json({ error: '目标机柜不存在' });
|
||||
}
|
||||
|
||||
const positionCheck = await checkPositionAvailable(targetRackId, targetPosition, device.height || 1, deviceId, t);
|
||||
const positionCheck = await checkPositionAvailable(
|
||||
targetRackId,
|
||||
targetPosition,
|
||||
device.height || 1,
|
||||
deviceId,
|
||||
t
|
||||
);
|
||||
if (!positionCheck.available) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({ error: positionCheck.reason });
|
||||
}
|
||||
|
||||
await device.update({
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
rackId: targetRackId,
|
||||
position: targetPosition,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'offline'
|
||||
}, { transaction: t });
|
||||
await device.update(
|
||||
{
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
rackId: targetRackId,
|
||||
position: targetPosition,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'offline',
|
||||
},
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
await targetRack.update({
|
||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
|
||||
}, { transaction: t });
|
||||
await targetRack.update(
|
||||
{
|
||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
|
||||
},
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
await t.commit();
|
||||
|
||||
const updatedDevice = await Device.findByPk(deviceId, {
|
||||
include: [
|
||||
{ model: Rack, include: [Room] }
|
||||
]
|
||||
include: [{ model: Rack, include: [Room] }],
|
||||
});
|
||||
|
||||
const deviceData = {
|
||||
...updatedDevice.toJSON(),
|
||||
rackName: targetRack.name,
|
||||
roomName: updatedDevice.Rack?.Room?.name
|
||||
roomName: updatedDevice.Rack?.Room?.name,
|
||||
};
|
||||
|
||||
await logDeviceOperation('restore', generateDeviceDescription('空闲设备恢复', deviceData, { includePosition: false }) + `到机柜【${targetRack.name}】U${targetPosition}`, {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState: { ...device.toJSON(), isIdle: true },
|
||||
afterState: deviceData,
|
||||
req,
|
||||
metadata: buildDeviceMetadata(deviceData, { targetRackId, targetPosition, type: 'idle_device_restore' })
|
||||
});
|
||||
await logDeviceOperation(
|
||||
'restore',
|
||||
generateDeviceDescription('空闲设备恢复', deviceData, { includePosition: false }) +
|
||||
`到机柜【${targetRack.name}】U${targetPosition}`,
|
||||
{
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState: { ...device.toJSON(), isIdle: true },
|
||||
afterState: deviceData,
|
||||
req,
|
||||
metadata: buildDeviceMetadata(deviceData, {
|
||||
targetRackId,
|
||||
targetPosition,
|
||||
type: 'idle_device_restore',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: '设备已恢复到设备管理',
|
||||
device: updatedDevice
|
||||
device: updatedDevice,
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
@@ -692,23 +808,29 @@ router.put('/batch-restore', async (req, res) => {
|
||||
|
||||
const idleDevices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds }, isIdle: true },
|
||||
transaction: t
|
||||
transaction: t,
|
||||
});
|
||||
|
||||
console.log('查询到的空闲设备数量:', idleDevices.length);
|
||||
if (idleDevices.length > 0) {
|
||||
console.log('查询到的设备ID:', idleDevices.map(d => d.deviceId));
|
||||
console.log(
|
||||
'查询到的设备ID:',
|
||||
idleDevices.map(d => d.deviceId)
|
||||
);
|
||||
}
|
||||
|
||||
if (idleDevices.length === 0) {
|
||||
console.log('没有找到空闲设备,检查设备是否存在:');
|
||||
const allDevices = await Device.findAll({
|
||||
where: { deviceId: { [Op.in]: deviceIds } },
|
||||
transaction: t
|
||||
transaction: t,
|
||||
});
|
||||
console.log('设备表中存在的设备数量:', allDevices.length);
|
||||
if (allDevices.length > 0) {
|
||||
console.log('存在的设备及其 isIdle 状态:', allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle })));
|
||||
console.log(
|
||||
'存在的设备及其 isIdle 状态:',
|
||||
allDevices.map(d => ({ deviceId: d.deviceId, isIdle: d.isIdle }))
|
||||
);
|
||||
}
|
||||
|
||||
await t.rollback();
|
||||
@@ -720,7 +842,9 @@ router.put('/batch-restore', async (req, res) => {
|
||||
|
||||
for (const device of idleDevices) {
|
||||
const deviceConfig = devices.find(d => d.deviceId === device.deviceId);
|
||||
if (!deviceConfig) continue;
|
||||
if (!deviceConfig) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetRackId = deviceConfig.targetRackId;
|
||||
const targetPosition = deviceConfig.targetPosition;
|
||||
@@ -730,7 +854,7 @@ router.put('/batch-restore', async (req, res) => {
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'skipped',
|
||||
reason: '未指定目标机柜'
|
||||
reason: '未指定目标机柜',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -741,7 +865,7 @@ router.put('/batch-restore', async (req, res) => {
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'failed',
|
||||
reason: '目标机柜不存在'
|
||||
reason: '目标机柜不存在',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -755,25 +879,31 @@ router.put('/batch-restore', async (req, res) => {
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
status: 'failed',
|
||||
reason: `U位${position}已被占用`
|
||||
reason: `U位${position}已被占用`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
await device.update({
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
rackId: targetRackId,
|
||||
position: position,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'offline'
|
||||
}, { transaction: t });
|
||||
await device.update(
|
||||
{
|
||||
isIdle: false,
|
||||
idleDate: null,
|
||||
idleReason: null,
|
||||
rackId: targetRackId,
|
||||
position: position,
|
||||
warehouseId: null,
|
||||
sourceType: 'rack',
|
||||
status: 'offline',
|
||||
},
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
await targetRack.update({
|
||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0)
|
||||
}, { transaction: t });
|
||||
await targetRack.update(
|
||||
{
|
||||
currentPower: targetRack.currentPower + (device.powerConsumption || 0),
|
||||
},
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
restoredCount++;
|
||||
results.push({
|
||||
@@ -781,7 +911,7 @@ router.put('/batch-restore', async (req, res) => {
|
||||
name: device.name,
|
||||
status: 'success',
|
||||
targetRack: targetRack.name,
|
||||
targetPosition: position
|
||||
targetPosition: position,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -791,17 +921,30 @@ router.put('/batch-restore', async (req, res) => {
|
||||
const failedCount = results.filter(r => r.status === 'failed').length;
|
||||
const skippedCount = results.filter(r => r.status === 'skipped').length;
|
||||
|
||||
const successDevices = idleDevices.filter(d => results.some(r => r.deviceId === d.deviceId && r.status === 'success'));
|
||||
const deviceSummary = successDevices.map(d =>
|
||||
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
|
||||
).join('、');
|
||||
const successDevices = idleDevices.filter(d =>
|
||||
results.some(r => r.deviceId === d.deviceId && r.status === 'success')
|
||||
);
|
||||
const deviceSummary = successDevices
|
||||
.map(
|
||||
d =>
|
||||
`${d.name}(编号:${d.deviceId}${d.type ? `,类型:${d.type}` : ''}${d.ipAddress ? `,IP:${d.ipAddress}` : ''})`
|
||||
)
|
||||
.join('、');
|
||||
|
||||
await logDeviceOperation('batch_restore', `批量上架 ${successCount} 台空闲设备:${deviceSummary}`, {
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${successCount}台设备`,
|
||||
req,
|
||||
metadata: { results, type: 'batch_idle_device_restore', devices: successDevices.map(d => d.toJSON()) }
|
||||
});
|
||||
await logDeviceOperation(
|
||||
'batch_restore',
|
||||
`批量上架 ${successCount} 台空闲设备:${deviceSummary}`,
|
||||
{
|
||||
targetId: deviceIds.join(','),
|
||||
targetName: `${successCount}台设备`,
|
||||
req,
|
||||
metadata: {
|
||||
results,
|
||||
type: 'batch_idle_device_restore',
|
||||
devices: successDevices.map(d => d.toJSON()),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: `成功上架 ${successCount} 台设备`,
|
||||
@@ -809,7 +952,7 @@ router.put('/batch-restore', async (req, res) => {
|
||||
restored: successCount,
|
||||
failed: failedCount,
|
||||
skipped: skippedCount,
|
||||
details: results
|
||||
details: results,
|
||||
});
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
@@ -823,7 +966,7 @@ router.delete('/:deviceId', async (req, res) => {
|
||||
try {
|
||||
const device = await Device.findOne({
|
||||
where: { deviceId: req.params.deviceId, isIdle: true },
|
||||
transaction: t
|
||||
transaction: t,
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
@@ -837,16 +980,24 @@ router.delete('/:deviceId', async (req, res) => {
|
||||
|
||||
await t.commit();
|
||||
|
||||
await logDeviceOperation('delete', generateDeviceDescription('删除空闲设备', {
|
||||
...device.toJSON(),
|
||||
name: device.name || device.deviceId
|
||||
}, { includeRack: false }), {
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState,
|
||||
req,
|
||||
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_delete' })
|
||||
});
|
||||
await logDeviceOperation(
|
||||
'delete',
|
||||
generateDeviceDescription(
|
||||
'删除空闲设备',
|
||||
{
|
||||
...device.toJSON(),
|
||||
name: device.name || device.deviceId,
|
||||
},
|
||||
{ includeRack: false }
|
||||
),
|
||||
{
|
||||
targetId: device.deviceId,
|
||||
targetName: device.name,
|
||||
beforeState,
|
||||
req,
|
||||
metadata: buildDeviceMetadata(device.toJSON(), { type: 'idle_device_delete' }),
|
||||
}
|
||||
);
|
||||
|
||||
res.json({ message: '空闲设备删除成功' });
|
||||
} catch (error) {
|
||||
@@ -856,7 +1007,13 @@ router.delete('/:deviceId', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
async function checkPositionAvailable(rackId, position, height, excludeDeviceId = null, transaction = null) {
|
||||
async function checkPositionAvailable(
|
||||
rackId,
|
||||
position,
|
||||
height,
|
||||
excludeDeviceId = null,
|
||||
transaction = null
|
||||
) {
|
||||
if (!position || position <= 0) {
|
||||
return { available: true, reason: null };
|
||||
}
|
||||
@@ -869,9 +1026,9 @@ async function checkPositionAvailable(rackId, position, height, excludeDeviceId
|
||||
where: {
|
||||
rackId: rackId,
|
||||
position: { [Op.ne]: null },
|
||||
isIdle: false
|
||||
isIdle: false,
|
||||
},
|
||||
attributes: ['deviceId', 'position', 'height']
|
||||
attributes: ['deviceId', 'position', 'height'],
|
||||
};
|
||||
|
||||
if (transaction) {
|
||||
@@ -891,7 +1048,7 @@ async function checkPositionAvailable(rackId, position, height, excludeDeviceId
|
||||
if (!(endU < existStart || startU > existEnd)) {
|
||||
return {
|
||||
available: false,
|
||||
reason: `U位冲突:机柜中已有设备 ${d.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''}`
|
||||
reason: `U位冲突:机柜中已有设备 ${d.deviceId} 占用 U${existStart}${existEnd !== existStart ? '-' + existEnd : ''}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+202
-93
@@ -54,25 +54,29 @@ router.get('/plans', async (req, res) => {
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ name: { [Op.like]: `%${keyword}%` } },
|
||||
{ description: { [Op.like]: `%${keyword}%` } }
|
||||
{ description: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
const { count, rows } = await InventoryPlan.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
|
||||
{
|
||||
model: require('../models/User'),
|
||||
as: 'Creator',
|
||||
attributes: ['userId', 'username', 'realName'],
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: parseInt(pageSize),
|
||||
offset: parseInt(offset)
|
||||
offset: parseInt(offset),
|
||||
});
|
||||
|
||||
res.json({
|
||||
plans: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -83,8 +87,12 @@ router.get('/plans/:planId', async (req, res) => {
|
||||
try {
|
||||
const plan = await InventoryPlan.findByPk(req.params.planId, {
|
||||
include: [
|
||||
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
|
||||
]
|
||||
{
|
||||
model: require('../models/User'),
|
||||
as: 'Creator',
|
||||
attributes: ['userId', 'username', 'realName'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
@@ -94,9 +102,13 @@ router.get('/plans/:planId', async (req, res) => {
|
||||
const tasks = await InventoryTask.findAll({
|
||||
where: { planId: plan.planId },
|
||||
include: [
|
||||
{ model: require('../models/User'), as: 'Assignee', attributes: ['userId', 'username', 'realName'] }
|
||||
{
|
||||
model: require('../models/User'),
|
||||
as: 'Assignee',
|
||||
attributes: ['userId', 'username', 'realName'],
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'ASC']]
|
||||
order: [['createdAt', 'ASC']],
|
||||
});
|
||||
|
||||
res.json({ plan, tasks });
|
||||
@@ -118,7 +130,7 @@ router.post('/plans', async (req, res) => {
|
||||
targetRooms: targetRooms || [],
|
||||
targetRacks: targetRacks || [],
|
||||
status: 'draft',
|
||||
createdBy: req.user?.userId
|
||||
createdBy: req.user?.userId,
|
||||
});
|
||||
|
||||
res.status(201).json(plan);
|
||||
@@ -143,7 +155,7 @@ router.put('/plans/:planId', async (req, res) => {
|
||||
scheduledDate: scheduledDate ? new Date(scheduledDate) : plan.scheduledDate,
|
||||
targetRooms: targetRooms || plan.targetRooms,
|
||||
targetRacks: targetRacks || plan.targetRacks,
|
||||
status: status || plan.status
|
||||
status: status || plan.status,
|
||||
});
|
||||
|
||||
res.json(plan);
|
||||
@@ -187,16 +199,16 @@ router.post('/plans/:planId/start', async (req, res) => {
|
||||
|
||||
if (targetRacks.length > 0) {
|
||||
allDevices = await Device.findAll({
|
||||
where: { rackId: { [Op.in]: targetRacks } }
|
||||
where: { rackId: { [Op.in]: targetRacks } },
|
||||
});
|
||||
} else if (targetRooms.length > 0) {
|
||||
const racksInRooms = await Rack.findAll({
|
||||
where: { roomId: { [Op.in]: targetRooms } },
|
||||
attributes: ['rackId']
|
||||
attributes: ['rackId'],
|
||||
});
|
||||
const rackIds = racksInRooms.map(r => r.rackId);
|
||||
allDevices = await Device.findAll({
|
||||
where: { rackId: { [Op.in]: rackIds } }
|
||||
where: { rackId: { [Op.in]: rackIds } },
|
||||
});
|
||||
} else {
|
||||
allDevices = await Device.findAll();
|
||||
@@ -206,7 +218,7 @@ router.post('/plans/:planId/start', async (req, res) => {
|
||||
const recordsToCreate = [];
|
||||
|
||||
const taskId = generateTaskId();
|
||||
|
||||
|
||||
tasksToCreate.push({
|
||||
taskId,
|
||||
planId: plan.planId,
|
||||
@@ -214,7 +226,7 @@ router.post('/plans/:planId/start', async (req, res) => {
|
||||
targetId: 'all',
|
||||
targetName: '全部设备',
|
||||
status: 'pending',
|
||||
totalDevices: allDevices.length
|
||||
totalDevices: allDevices.length,
|
||||
});
|
||||
|
||||
for (let i = 0; i < allDevices.length; i++) {
|
||||
@@ -229,7 +241,7 @@ router.post('/plans/:planId/start', async (req, res) => {
|
||||
serialNumber: device.serialNumber,
|
||||
rackId: device.rackId,
|
||||
position: device.position,
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -238,13 +250,17 @@ router.post('/plans/:planId/start', async (req, res) => {
|
||||
}
|
||||
|
||||
if (recordsToCreate.length > 0) {
|
||||
const now = dbDialect === 'mysql'
|
||||
? new Date().toISOString().replace('T', ' ').replace('Z', '')
|
||||
: new Date().toISOString();
|
||||
const placeholders = recordsToCreate.map(r =>
|
||||
`('${r.recordId}', '${r.taskId}', '${r.planId}', '${r.deviceId}', '${r.deviceName}', '${r.deviceType}', '${r.serialNumber || ''}', '${r.rackId}', ${r.position}, 'pending', '${now}', '${now}')`
|
||||
).join(',');
|
||||
|
||||
const now =
|
||||
dbDialect === 'mysql'
|
||||
? new Date().toISOString().replace('T', ' ').replace('Z', '')
|
||||
: new Date().toISOString();
|
||||
const placeholders = recordsToCreate
|
||||
.map(
|
||||
r =>
|
||||
`('${r.recordId}', '${r.taskId}', '${r.planId}', '${r.deviceId}', '${r.deviceName}', '${r.deviceType}', '${r.serialNumber || ''}', '${r.rackId}', ${r.position}, 'pending', '${now}', '${now}')`
|
||||
)
|
||||
.join(',');
|
||||
|
||||
if (placeholders) {
|
||||
await sequelize.query(`
|
||||
INSERT INTO inventory_records (recordId, taskId, planId, deviceId, deviceName, deviceType, serialNumber, rackId, position, status, createdAt, updatedAt)
|
||||
@@ -259,10 +275,14 @@ router.post('/plans/:planId/start', async (req, res) => {
|
||||
checkedDevices: 0,
|
||||
normalDevices: 0,
|
||||
abnormalDevices: 0,
|
||||
missedDevices: allDevices.length
|
||||
missedDevices: allDevices.length,
|
||||
});
|
||||
|
||||
res.json({ message: '盘点任务已启动', taskCount: tasksToCreate.length, deviceCount: allDevices.length });
|
||||
res.json({
|
||||
message: '盘点任务已启动',
|
||||
taskCount: tasksToCreate.length,
|
||||
deviceCount: allDevices.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('启动盘点错误:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -274,8 +294,12 @@ router.get('/tasks/:taskId', async (req, res) => {
|
||||
const task = await InventoryTask.findByPk(req.params.taskId, {
|
||||
include: [
|
||||
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
||||
{ model: require('../models/User'), as: 'Assignee', attributes: ['userId', 'username', 'realName'] }
|
||||
]
|
||||
{
|
||||
model: require('../models/User'),
|
||||
as: 'Assignee',
|
||||
attributes: ['userId', 'username', 'realName'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
@@ -285,15 +309,23 @@ router.get('/tasks/:taskId', async (req, res) => {
|
||||
const records = await InventoryRecord.findAll({
|
||||
where: { taskId: task.taskId },
|
||||
include: [
|
||||
{ model: Device, as: 'Device', attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'] },
|
||||
{ model: require('../models/User'), as: 'Checker', attributes: ['userId', 'username', 'realName'] }
|
||||
]
|
||||
{
|
||||
model: Device,
|
||||
as: 'Device',
|
||||
attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'],
|
||||
},
|
||||
{
|
||||
model: require('../models/User'),
|
||||
as: 'Checker',
|
||||
attributes: ['userId', 'username', 'realName'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const rackIds = [...new Set(records.map(r => r.rackId).filter(Boolean))];
|
||||
const racks = await Rack.findAll({
|
||||
where: { rackId: rackIds },
|
||||
include: [{ model: Room, as: 'Room' }]
|
||||
include: [{ model: Room, as: 'Room' }],
|
||||
});
|
||||
const rackMap = {};
|
||||
racks.forEach(r => {
|
||||
@@ -305,10 +337,12 @@ router.get('/tasks/:taskId', async (req, res) => {
|
||||
const roomName = rackInfo?.Room?.name || '';
|
||||
const rackName = rackInfo?.name || record.rackId || '';
|
||||
const position = record.position || '';
|
||||
|
||||
|
||||
return {
|
||||
...record.toJSON(),
|
||||
displayLocation: roomName ? `${roomName} - ${rackName} - U${position}` : `${rackName} - U${position}`
|
||||
displayLocation: roomName
|
||||
? `${roomName} - ${rackName} - U${position}`
|
||||
: `${rackName} - U${position}`,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -331,14 +365,14 @@ router.put('/tasks/:taskId', async (req, res) => {
|
||||
if (assignedTo !== undefined) {
|
||||
await task.update({
|
||||
assignedTo,
|
||||
assignedAt: assignedTo ? new Date() : task.assignedAt
|
||||
assignedAt: assignedTo ? new Date() : task.assignedAt,
|
||||
});
|
||||
}
|
||||
|
||||
if (status) {
|
||||
await task.update({
|
||||
status,
|
||||
completedAt: status === 'completed' ? new Date() : null
|
||||
completedAt: status === 'completed' ? new Date() : null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,7 +385,7 @@ router.put('/tasks/:taskId', async (req, res) => {
|
||||
router.post('/records/:recordId/check', async (req, res) => {
|
||||
try {
|
||||
const record = await InventoryRecord.findByPk(req.params.recordId, {
|
||||
include: [{ model: Device, as: 'Device' }]
|
||||
include: [{ model: Device, as: 'Device' }],
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
@@ -380,7 +414,7 @@ router.post('/records/:recordId/check', async (req, res) => {
|
||||
checkedBy: req.user?.userId,
|
||||
checkedAt: new Date(),
|
||||
remark: remark || null,
|
||||
photoUrl: photoUrl || null
|
||||
photoUrl: photoUrl || null,
|
||||
});
|
||||
|
||||
const task = await InventoryTask.findByPk(record.taskId);
|
||||
@@ -391,7 +425,7 @@ router.post('/records/:recordId/check', async (req, res) => {
|
||||
totalDevices: taskRecords.length,
|
||||
checkedDevices: taskRecords.filter(r => r.status !== 'pending').length,
|
||||
normalDevices: taskRecords.filter(r => r.status === 'normal').length,
|
||||
abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length
|
||||
abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length,
|
||||
};
|
||||
|
||||
await task.update(taskStats);
|
||||
@@ -402,7 +436,7 @@ router.post('/records/:recordId/check', async (req, res) => {
|
||||
checkedDevices: planRecords.filter(r => r.status !== 'pending').length,
|
||||
normalDevices: planRecords.filter(r => r.status === 'normal').length,
|
||||
abnormalDevices: planRecords.filter(r => r.status === 'abnormal').length,
|
||||
missedDevices: planRecords.filter(r => r.status === 'pending').length
|
||||
missedDevices: planRecords.filter(r => r.status === 'pending').length,
|
||||
};
|
||||
|
||||
await plan.update(planStats);
|
||||
@@ -419,26 +453,43 @@ router.get('/records', async (req, res) => {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const where = {};
|
||||
|
||||
if (planId) where.planId = planId;
|
||||
if (taskId) where.taskId = taskId;
|
||||
if (status) where.status = status;
|
||||
if (planId) {
|
||||
where.planId = planId;
|
||||
}
|
||||
if (taskId) {
|
||||
where.taskId = taskId;
|
||||
}
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const { count, rows } = await InventoryRecord.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{ model: Device, as: 'Device', attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'] },
|
||||
{ model: require('../models/User'), as: 'Checker', attributes: ['userId', 'username', 'realName'] }
|
||||
{
|
||||
model: Device,
|
||||
as: 'Device',
|
||||
attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'],
|
||||
},
|
||||
{
|
||||
model: require('../models/User'),
|
||||
as: 'Checker',
|
||||
attributes: ['userId', 'username', 'realName'],
|
||||
},
|
||||
],
|
||||
order: [
|
||||
['checkedAt', 'DESC'],
|
||||
['createdAt', 'DESC'],
|
||||
],
|
||||
order: [['checkedAt', 'DESC'], ['createdAt', 'DESC']],
|
||||
limit: parseInt(pageSize),
|
||||
offset: parseInt(offset)
|
||||
offset: parseInt(offset),
|
||||
});
|
||||
|
||||
res.json({
|
||||
records: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -466,7 +517,7 @@ router.post('/plans/:planId/complete', async (req, res) => {
|
||||
checkedDevices: finalRecords.filter(r => r.status !== 'pending').length,
|
||||
normalDevices: finalRecords.filter(r => r.status === 'normal').length,
|
||||
abnormalDevices: finalRecords.filter(r => r.status === 'abnormal').length,
|
||||
missedDevices: finalRecords.filter(r => r.status === 'missed').length
|
||||
missedDevices: finalRecords.filter(r => r.status === 'missed').length,
|
||||
});
|
||||
|
||||
await InventoryTask.update(
|
||||
@@ -495,8 +546,12 @@ router.get('/stats/dashboard', async (req, res) => {
|
||||
limit: 5,
|
||||
order: [['createdAt', 'DESC']],
|
||||
include: [
|
||||
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
|
||||
]
|
||||
{
|
||||
model: require('../models/User'),
|
||||
as: 'Creator',
|
||||
attributes: ['userId', 'username', 'realName'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -507,9 +562,12 @@ router.get('/stats/dashboard', async (req, res) => {
|
||||
normalRecords,
|
||||
abnormalRecords,
|
||||
pendingRecords,
|
||||
completionRate: totalRecords > 0 ? ((normalRecords + abnormalRecords) / totalRecords * 100).toFixed(1) : 0,
|
||||
abnormalRate: totalRecords > 0 ? (abnormalRecords / totalRecords * 100).toFixed(1) : 0,
|
||||
recentPlans
|
||||
completionRate:
|
||||
totalRecords > 0
|
||||
? (((normalRecords + abnormalRecords) / totalRecords) * 100).toFixed(1)
|
||||
: 0,
|
||||
abnormalRate: totalRecords > 0 ? ((abnormalRecords / totalRecords) * 100).toFixed(1) : 0,
|
||||
recentPlans,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -518,18 +576,18 @@ router.get('/stats/dashboard', async (req, res) => {
|
||||
|
||||
router.post('/quick-add-device', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
taskId,
|
||||
planId,
|
||||
const {
|
||||
taskId,
|
||||
planId,
|
||||
serialNumber,
|
||||
SN,
|
||||
deviceName,
|
||||
name,
|
||||
deviceType,
|
||||
type,
|
||||
roomId,
|
||||
rackId,
|
||||
position,
|
||||
roomId,
|
||||
rackId,
|
||||
position,
|
||||
model,
|
||||
brand,
|
||||
height,
|
||||
@@ -558,22 +616,27 @@ router.post('/quick-add-device', async (req, res) => {
|
||||
|
||||
const existingDevice = await Device.findOne({ where: { serialNumber: finalSerialNumber } });
|
||||
if (existingDevice) {
|
||||
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中', deviceId: existingDevice.deviceId });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: '该序列号的设备已存在于设备管理中', deviceId: existingDevice.deviceId });
|
||||
}
|
||||
|
||||
const existingPending = await PendingDevice.findOne({
|
||||
where: { serialNumber: finalSerialNumber, status: 'pending' }
|
||||
const existingPending = await PendingDevice.findOne({
|
||||
where: { serialNumber: finalSerialNumber, status: 'pending' },
|
||||
});
|
||||
if (existingPending) {
|
||||
return res.status(400).json({ error: '该序列号的设备已在暂存列表中', pendingId: existingPending.pendingId });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: '该序列号的设备已在暂存列表中', pendingId: existingPending.pendingId });
|
||||
}
|
||||
|
||||
const pendingId = `PEND${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
||||
|
||||
// 只有当用户没有填写设备名称时,才使用默认名称
|
||||
const finalName = finalDeviceName && finalDeviceName.trim() !== ''
|
||||
? finalDeviceName.trim()
|
||||
: `新设备-${finalSerialNumber.slice(-6)}`;
|
||||
const finalName =
|
||||
finalDeviceName && finalDeviceName.trim() !== ''
|
||||
? finalDeviceName.trim()
|
||||
: `新设备-${finalSerialNumber.slice(-6)}`;
|
||||
|
||||
const pendingDevice = await PendingDevice.create({
|
||||
pendingId,
|
||||
@@ -596,12 +659,12 @@ router.post('/quick-add-device', async (req, res) => {
|
||||
taskId: taskId || null,
|
||||
createdBy: req.user?.userId,
|
||||
status: 'pending',
|
||||
remark: remark || '盘点时快速添加'
|
||||
remark: remark || '盘点时快速添加',
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
message: '设备已暂存,请前往暂存设备页面完善信息后同步',
|
||||
pendingDevice
|
||||
pendingDevice,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('快速添加设备错误:', error);
|
||||
@@ -611,7 +674,14 @@ router.post('/quick-add-device', async (req, res) => {
|
||||
|
||||
router.get('/pending-devices', async (req, res) => {
|
||||
try {
|
||||
const { status, planId, roomId, keyword, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE } = req.query;
|
||||
const {
|
||||
status,
|
||||
planId,
|
||||
roomId,
|
||||
keyword,
|
||||
page = 1,
|
||||
pageSize = PAGINATION.DEFAULT_PAGE_SIZE,
|
||||
} = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
const where = {};
|
||||
|
||||
@@ -627,7 +697,7 @@ router.get('/pending-devices', async (req, res) => {
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
||||
{ deviceName: { [Op.like]: `%${keyword}%` } }
|
||||
{ deviceName: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -638,18 +708,18 @@ router.get('/pending-devices', async (req, res) => {
|
||||
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
|
||||
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
||||
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
|
||||
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] }
|
||||
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] },
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: parseInt(pageSize),
|
||||
offset: parseInt(offset)
|
||||
offset: parseInt(offset),
|
||||
});
|
||||
|
||||
res.json({
|
||||
pendingDevices: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取暂存设备列表错误:', error);
|
||||
@@ -677,8 +747,8 @@ router.get('/pending-devices/:pendingId', async (req, res) => {
|
||||
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
|
||||
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
||||
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
|
||||
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] }
|
||||
]
|
||||
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] },
|
||||
],
|
||||
});
|
||||
|
||||
if (!pendingDevice) {
|
||||
@@ -702,7 +772,23 @@ router.put('/pending-devices/:pendingId', async (req, res) => {
|
||||
return res.status(400).json({ error: '已同步的设备无法修改' });
|
||||
}
|
||||
|
||||
const { deviceName, deviceType, roomId, rackId, position, model, brand, height, powerConsumption, ipAddress, purchaseDate, warrantyExpiry, description, remark, ...restFields } = req.body;
|
||||
const {
|
||||
deviceName,
|
||||
deviceType,
|
||||
roomId,
|
||||
rackId,
|
||||
position,
|
||||
model,
|
||||
brand,
|
||||
height,
|
||||
powerConsumption,
|
||||
ipAddress,
|
||||
purchaseDate,
|
||||
warrantyExpiry,
|
||||
description,
|
||||
remark,
|
||||
...restFields
|
||||
} = req.body;
|
||||
|
||||
const updateData = {
|
||||
deviceName: deviceName !== undefined ? deviceName : pendingDevice.deviceName,
|
||||
@@ -713,12 +799,23 @@ router.put('/pending-devices/:pendingId', async (req, res) => {
|
||||
model: model !== undefined ? model : pendingDevice.model,
|
||||
brand: brand !== undefined ? brand : pendingDevice.brand,
|
||||
height: height !== undefined ? height : pendingDevice.height,
|
||||
powerConsumption: powerConsumption !== undefined ? powerConsumption : pendingDevice.powerConsumption,
|
||||
powerConsumption:
|
||||
powerConsumption !== undefined ? powerConsumption : pendingDevice.powerConsumption,
|
||||
ipAddress: ipAddress !== undefined ? ipAddress : pendingDevice.ipAddress,
|
||||
purchaseDate: purchaseDate !== undefined ? (purchaseDate ? new Date(purchaseDate) : null) : pendingDevice.purchaseDate,
|
||||
warrantyExpiry: warrantyExpiry !== undefined ? (warrantyExpiry ? new Date(warrantyExpiry) : null) : pendingDevice.warrantyExpiry,
|
||||
purchaseDate:
|
||||
purchaseDate !== undefined
|
||||
? purchaseDate
|
||||
? new Date(purchaseDate)
|
||||
: null
|
||||
: pendingDevice.purchaseDate,
|
||||
warrantyExpiry:
|
||||
warrantyExpiry !== undefined
|
||||
? warrantyExpiry
|
||||
? new Date(warrantyExpiry)
|
||||
: null
|
||||
: pendingDevice.warrantyExpiry,
|
||||
description: description !== undefined ? description : pendingDevice.description,
|
||||
remark: remark !== undefined ? remark : pendingDevice.remark
|
||||
remark: remark !== undefined ? remark : pendingDevice.remark,
|
||||
};
|
||||
|
||||
if (Object.keys(restFields).length > 0) {
|
||||
@@ -759,7 +856,9 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
|
||||
return res.status(400).json({ error: '该设备已同步' });
|
||||
}
|
||||
|
||||
const existingDevice = await Device.findOne({ where: { serialNumber: pendingDevice.serialNumber } });
|
||||
const existingDevice = await Device.findOne({
|
||||
where: { serialNumber: pendingDevice.serialNumber },
|
||||
});
|
||||
if (existingDevice) {
|
||||
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中' });
|
||||
}
|
||||
@@ -770,7 +869,9 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
|
||||
const match = device.deviceId.match(/^DEV(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1], 10);
|
||||
if (num > maxNumber) maxNumber = num;
|
||||
if (num > maxNumber) {
|
||||
maxNumber = num;
|
||||
}
|
||||
}
|
||||
});
|
||||
const deviceId = `DEV${String(maxNumber + 1).padStart(3, '0')}`;
|
||||
@@ -790,20 +891,20 @@ router.post('/pending-devices/:pendingId/sync', async (req, res) => {
|
||||
purchaseDate: pendingDevice.purchaseDate,
|
||||
warrantyExpiry: pendingDevice.warrantyExpiry,
|
||||
customFields: pendingDevice.customFields,
|
||||
status: 'running'
|
||||
status: 'running',
|
||||
});
|
||||
|
||||
await pendingDevice.update({
|
||||
status: 'synced',
|
||||
syncedAt: new Date(),
|
||||
syncedBy: req.user?.userId,
|
||||
syncedDeviceId: newDevice.deviceId
|
||||
syncedDeviceId: newDevice.deviceId,
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: '同步成功',
|
||||
device: newDevice,
|
||||
pendingDevice
|
||||
pendingDevice,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('同步设备错误:', error);
|
||||
@@ -821,8 +922,8 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
|
||||
const pendingDevices = await PendingDevice.findAll({
|
||||
where: {
|
||||
pendingId: { [Op.in]: pendingIds },
|
||||
status: 'pending'
|
||||
}
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
if (pendingDevices.length === 0) {
|
||||
@@ -835,7 +936,9 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
|
||||
const match = device.deviceId.match(/^DEV(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1], 10);
|
||||
if (num > maxNumber) maxNumber = num;
|
||||
if (num > maxNumber) {
|
||||
maxNumber = num;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -844,9 +947,15 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
|
||||
|
||||
for (const pending of pendingDevices) {
|
||||
try {
|
||||
const existingDevice = await Device.findOne({ where: { serialNumber: pending.serialNumber } });
|
||||
const existingDevice = await Device.findOne({
|
||||
where: { serialNumber: pending.serialNumber },
|
||||
});
|
||||
if (existingDevice) {
|
||||
errors.push({ pendingId: pending.pendingId, serialNumber: pending.serialNumber, error: '序列号已存在' });
|
||||
errors.push({
|
||||
pendingId: pending.pendingId,
|
||||
serialNumber: pending.serialNumber,
|
||||
error: '序列号已存在',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -868,14 +977,14 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
|
||||
purchaseDate: pending.purchaseDate,
|
||||
warrantyExpiry: pending.warrantyExpiry,
|
||||
customFields: pending.customFields,
|
||||
status: 'running'
|
||||
status: 'running',
|
||||
});
|
||||
|
||||
await pending.update({
|
||||
status: 'synced',
|
||||
syncedAt: new Date(),
|
||||
syncedBy: req.user?.userId,
|
||||
syncedDeviceId: newDevice.deviceId
|
||||
syncedDeviceId: newDevice.deviceId,
|
||||
});
|
||||
|
||||
results.push({ pendingId: pending.pendingId, deviceId: newDevice.deviceId });
|
||||
@@ -889,7 +998,7 @@ router.post('/pending-devices/batch-sync', async (req, res) => {
|
||||
successCount: results.length,
|
||||
errorCount: errors.length,
|
||||
results,
|
||||
errors
|
||||
errors,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量同步设备错误:', error);
|
||||
|
||||
@@ -13,7 +13,7 @@ router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { deviceId } = req.query;
|
||||
const where = {};
|
||||
|
||||
|
||||
if (deviceId) {
|
||||
where.deviceId = deviceId;
|
||||
}
|
||||
@@ -24,10 +24,13 @@ router.get('/', async (req, res) => {
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type']
|
||||
}
|
||||
attributes: ['deviceId', 'name', 'type'],
|
||||
},
|
||||
],
|
||||
order: [
|
||||
['slotNumber', 'ASC'],
|
||||
['name', 'ASC'],
|
||||
],
|
||||
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
|
||||
});
|
||||
|
||||
res.json(networkCards);
|
||||
@@ -47,10 +50,13 @@ router.get('/device/:deviceId', async (req, res) => {
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type']
|
||||
}
|
||||
attributes: ['deviceId', 'name', 'type'],
|
||||
},
|
||||
],
|
||||
order: [
|
||||
['slotNumber', 'ASC'],
|
||||
['name', 'ASC'],
|
||||
],
|
||||
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
|
||||
});
|
||||
|
||||
res.json(networkCards);
|
||||
@@ -66,14 +72,17 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
|
||||
|
||||
const networkCards = await NetworkCard.findAll({
|
||||
where: { deviceId },
|
||||
order: [['slotNumber', 'ASC'], ['name', 'ASC']]
|
||||
order: [
|
||||
['slotNumber', 'ASC'],
|
||||
['name', 'ASC'],
|
||||
],
|
||||
});
|
||||
|
||||
const cardsWithPorts = await Promise.all(
|
||||
networkCards.map(async (card) => {
|
||||
networkCards.map(async card => {
|
||||
const ports = await DevicePort.findAll({
|
||||
where: { nicId: card.nicId },
|
||||
order: [['portName', 'ASC']]
|
||||
order: [['portName', 'ASC']],
|
||||
});
|
||||
|
||||
const freeCount = ports.filter(p => p.status === 'free').length;
|
||||
@@ -87,15 +96,15 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
|
||||
total: ports.length,
|
||||
free: freeCount,
|
||||
occupied: occupiedCount,
|
||||
fault: faultCount
|
||||
}
|
||||
fault: faultCount,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const ungroupedPorts = await DevicePort.findAll({
|
||||
where: { deviceId, nicId: null },
|
||||
order: [['portName', 'ASC']]
|
||||
order: [['portName', 'ASC']],
|
||||
});
|
||||
|
||||
if (ungroupedPorts.length > 0) {
|
||||
@@ -110,8 +119,8 @@ router.get('/device/:deviceId/with-ports', async (req, res) => {
|
||||
total: ungroupedPorts.length,
|
||||
free: ungroupedPorts.filter(p => p.status === 'free').length,
|
||||
occupied: ungroupedPorts.filter(p => p.status === 'occupied').length,
|
||||
fault: ungroupedPorts.filter(p => p.status === 'fault').length
|
||||
}
|
||||
fault: ungroupedPorts.filter(p => p.status === 'fault').length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,9 +138,9 @@ router.get('/:nicId', async (req, res) => {
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!networkCard) {
|
||||
@@ -151,7 +160,7 @@ router.get('/:nicId/ports', async (req, res) => {
|
||||
|
||||
const ports = await DevicePort.findAll({
|
||||
where: { nicId },
|
||||
order: [['portName', 'ASC']]
|
||||
order: [['portName', 'ASC']],
|
||||
});
|
||||
|
||||
res.json(ports);
|
||||
@@ -170,7 +179,7 @@ router.get('/find', async (req, res) => {
|
||||
}
|
||||
|
||||
const networkCard = await NetworkCard.findOne({
|
||||
where: { deviceId, name }
|
||||
where: { deviceId, name },
|
||||
});
|
||||
|
||||
if (!networkCard) {
|
||||
@@ -186,14 +195,15 @@ router.get('/find', async (req, res) => {
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } = req.body;
|
||||
const { nicId, deviceId, name, description, slotNumber, model, manufacturer, status } =
|
||||
req.body;
|
||||
|
||||
if (!deviceId || !name) {
|
||||
return res.status(400).json({ error: '缺少必填字段' });
|
||||
}
|
||||
|
||||
const existingCard = await NetworkCard.findOne({
|
||||
where: { deviceId, name }
|
||||
where: { deviceId, name },
|
||||
});
|
||||
|
||||
if (existingCard) {
|
||||
@@ -211,7 +221,7 @@ router.post('/', async (req, res) => {
|
||||
model,
|
||||
manufacturer,
|
||||
status: status || 'normal',
|
||||
portCount: 0
|
||||
portCount: 0,
|
||||
});
|
||||
|
||||
const createdCard = await NetworkCard.findByPk(networkCard.nicId, {
|
||||
@@ -219,9 +229,9 @@ router.post('/', async (req, res) => {
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
res.status(201).json(createdCard);
|
||||
@@ -245,7 +255,7 @@ router.post('/batch', async (req, res) => {
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
updated: 0,
|
||||
errors: []
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const transaction = await NetworkCard.sequelize.transaction();
|
||||
@@ -272,7 +282,7 @@ router.post('/batch', async (req, res) => {
|
||||
|
||||
const existingCard = await NetworkCard.findOne({
|
||||
where: { deviceId: cardData.deviceId, name: cardData.name },
|
||||
transaction
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (existingCard) {
|
||||
@@ -281,16 +291,28 @@ router.post('/batch', async (req, res) => {
|
||||
continue;
|
||||
}
|
||||
if (updateExisting) {
|
||||
await NetworkCard.update({
|
||||
slotNumber: cardData.slotNumber !== undefined ? cardData.slotNumber : existingCard.slotNumber,
|
||||
model: cardData.model !== undefined ? cardData.model : existingCard.model,
|
||||
manufacturer: cardData.manufacturer !== undefined ? cardData.manufacturer : existingCard.manufacturer,
|
||||
description: cardData.description !== undefined ? cardData.description : existingCard.description,
|
||||
status: cardData.status || existingCard.status
|
||||
}, {
|
||||
where: { nicId: existingCard.nicId },
|
||||
transaction
|
||||
});
|
||||
await NetworkCard.update(
|
||||
{
|
||||
slotNumber:
|
||||
cardData.slotNumber !== undefined
|
||||
? cardData.slotNumber
|
||||
: existingCard.slotNumber,
|
||||
model: cardData.model !== undefined ? cardData.model : existingCard.model,
|
||||
manufacturer:
|
||||
cardData.manufacturer !== undefined
|
||||
? cardData.manufacturer
|
||||
: existingCard.manufacturer,
|
||||
description:
|
||||
cardData.description !== undefined
|
||||
? cardData.description
|
||||
: existingCard.description,
|
||||
status: cardData.status || existingCard.status,
|
||||
},
|
||||
{
|
||||
where: { nicId: existingCard.nicId },
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
results.updated++;
|
||||
results.success++;
|
||||
continue;
|
||||
@@ -298,19 +320,23 @@ router.post('/batch', async (req, res) => {
|
||||
throw new Error('该设备已存在同名网卡');
|
||||
}
|
||||
|
||||
const autoNicId = cardData.nicId || `NIC-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
const autoNicId =
|
||||
cardData.nicId || `NIC-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
|
||||
await NetworkCard.create({
|
||||
nicId: autoNicId,
|
||||
deviceId: cardData.deviceId,
|
||||
name: cardData.name,
|
||||
slotNumber: cardData.slotNumber,
|
||||
model: cardData.model,
|
||||
manufacturer: cardData.manufacturer,
|
||||
description: cardData.description,
|
||||
status: cardData.status || 'normal',
|
||||
portCount: 0
|
||||
}, { transaction });
|
||||
await NetworkCard.create(
|
||||
{
|
||||
nicId: autoNicId,
|
||||
deviceId: cardData.deviceId,
|
||||
name: cardData.name,
|
||||
slotNumber: cardData.slotNumber,
|
||||
model: cardData.model,
|
||||
manufacturer: cardData.manufacturer,
|
||||
description: cardData.description,
|
||||
status: cardData.status || 'normal',
|
||||
portCount: 0,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
results.success++;
|
||||
} catch (error) {
|
||||
@@ -319,7 +345,7 @@ router.post('/batch', async (req, res) => {
|
||||
index: i + 1,
|
||||
deviceId: cardData.deviceId,
|
||||
name: cardData.name,
|
||||
error: error.message
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -339,7 +365,7 @@ router.post('/batch', async (req, res) => {
|
||||
router.put('/:nicId', async (req, res) => {
|
||||
try {
|
||||
const [updated] = await NetworkCard.update(req.body, {
|
||||
where: { nicId: req.params.nicId }
|
||||
where: { nicId: req.params.nicId },
|
||||
});
|
||||
|
||||
if (updated) {
|
||||
@@ -348,9 +374,9 @@ router.put('/:nicId', async (req, res) => {
|
||||
{
|
||||
model: Device,
|
||||
as: 'device',
|
||||
attributes: ['deviceId', 'name', 'type']
|
||||
}
|
||||
]
|
||||
attributes: ['deviceId', 'name', 'type'],
|
||||
},
|
||||
],
|
||||
});
|
||||
res.json(networkCard);
|
||||
} else {
|
||||
@@ -368,13 +394,13 @@ router.delete('/:nicId', async (req, res) => {
|
||||
|
||||
const portCount = await DevicePort.count({ where: { nicId } });
|
||||
if (portCount > 0) {
|
||||
return res.status(400).json({
|
||||
error: `该网卡下还有 ${portCount} 个端口,请先删除或转移端口后再删除网卡`
|
||||
return res.status(400).json({
|
||||
error: `该网卡下还有 ${portCount} 个端口,请先删除或转移端口后再删除网卡`,
|
||||
});
|
||||
}
|
||||
|
||||
const deleted = await NetworkCard.destroy({
|
||||
where: { nicId }
|
||||
where: { nicId },
|
||||
});
|
||||
|
||||
if (deleted) {
|
||||
|
||||
@@ -18,7 +18,7 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
keyword,
|
||||
startDate,
|
||||
endDate,
|
||||
result
|
||||
result,
|
||||
} = req.query;
|
||||
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
@@ -46,7 +46,7 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
where[Op.or] = [
|
||||
{ operationDescription: { [Op.like]: `%${keyword}%` } },
|
||||
{ targetName: { [Op.like]: `%${keyword}%` } },
|
||||
{ operatorName: { [Op.like]: `%${keyword}%` } }
|
||||
{ operatorName: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
where,
|
||||
order: [['createdAt', 'DESC']],
|
||||
offset,
|
||||
limit
|
||||
limit,
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -79,14 +79,14 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
logs
|
||||
}
|
||||
logs,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作日志失败'
|
||||
message: '获取操作日志失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -95,23 +95,23 @@ router.get('/modules', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const modules = await OperationLog.findAll({
|
||||
attributes: ['module'],
|
||||
group: ['module']
|
||||
group: ['module'],
|
||||
});
|
||||
|
||||
const moduleList = modules.map(m => ({
|
||||
value: m.module,
|
||||
label: getModuleName(m.module)
|
||||
label: getModuleName(m.module),
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: moduleList
|
||||
data: moduleList,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取模块列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取模块列表失败'
|
||||
message: '获取模块列表失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -128,23 +128,23 @@ router.get('/types', authMiddleware, async (req, res) => {
|
||||
const types = await OperationLog.findAll({
|
||||
where,
|
||||
attributes: ['operationType'],
|
||||
group: ['operationType']
|
||||
group: ['operationType'],
|
||||
});
|
||||
|
||||
const typeList = types.map(t => ({
|
||||
value: t.operationType,
|
||||
label: getOperationTypeName(t.operationType)
|
||||
label: getOperationTypeName(t.operationType),
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: typeList
|
||||
data: typeList,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作类型列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作类型列表失败'
|
||||
message: '获取操作类型列表失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -170,23 +170,26 @@ router.get('/statistics', authMiddleware, async (req, res) => {
|
||||
OperationLog.findAll({
|
||||
where,
|
||||
attributes: ['module', [sequelize.fn('COUNT', sequelize.col('module')), 'count']],
|
||||
group: ['module']
|
||||
group: ['module'],
|
||||
}),
|
||||
OperationLog.findAll({
|
||||
where,
|
||||
attributes: ['operationType', [sequelize.fn('COUNT', sequelize.col('operationType')), 'count']],
|
||||
group: ['operationType']
|
||||
attributes: [
|
||||
'operationType',
|
||||
[sequelize.fn('COUNT', sequelize.col('operationType')), 'count'],
|
||||
],
|
||||
group: ['operationType'],
|
||||
}),
|
||||
OperationLog.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
[sequelize.fn('DATE', sequelize.col('createdAt')), 'date'],
|
||||
[sequelize.fn('COUNT', '*'), 'count']
|
||||
[sequelize.fn('COUNT', '*'), 'count'],
|
||||
],
|
||||
group: [sequelize.fn('DATE', sequelize.col('createdAt'))],
|
||||
order: [[sequelize.fn('DATE', sequelize.col('createdAt')), 'DESC']],
|
||||
limit: 30
|
||||
})
|
||||
limit: 30,
|
||||
}),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
@@ -194,14 +197,14 @@ router.get('/statistics', authMiddleware, async (req, res) => {
|
||||
data: {
|
||||
byModule: moduleStats.map(s => ({ module: s.module, count: s.get('count') })),
|
||||
byType: typeStats.map(s => ({ type: s.operationType, count: s.get('count') })),
|
||||
byDay: dailyStats.map(s => ({ date: s.get('date'), count: s.get('count') }))
|
||||
}
|
||||
byDay: dailyStats.map(s => ({ date: s.get('date'), count: s.get('count') })),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志统计失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作日志统计失败'
|
||||
message: '获取操作日志统计失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -213,19 +216,19 @@ router.get('/:recordId', authMiddleware, async (req, res) => {
|
||||
if (!log) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '日志记录不存在'
|
||||
message: '日志记录不存在',
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: log
|
||||
data: log,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作日志详情失败'
|
||||
message: '获取操作日志详情失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -239,7 +242,7 @@ function getModuleName(module) {
|
||||
rack: '机柜管理',
|
||||
room: '机房管理',
|
||||
ticket: '工单管理',
|
||||
backup: '备份管理'
|
||||
backup: '备份管理',
|
||||
};
|
||||
return moduleNames[module] || module;
|
||||
}
|
||||
@@ -255,7 +258,7 @@ function getOperationTypeName(type) {
|
||||
move: '移动',
|
||||
permission_change: '权限变更',
|
||||
import: '导入',
|
||||
export: '导出'
|
||||
export: '导出',
|
||||
};
|
||||
return typeNames[type] || type;
|
||||
}
|
||||
|
||||
+167
-150
@@ -32,7 +32,7 @@ router.get('/', async (req, res) => {
|
||||
if (keyword) {
|
||||
where[require('sequelize').Op.or] = [
|
||||
{ rackId: { [require('sequelize').Op.like]: `%${keyword}%` } },
|
||||
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } }
|
||||
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -42,18 +42,16 @@ router.get('/', async (req, res) => {
|
||||
// 获取分页数据 - 先查询机柜基本信息
|
||||
const racks = await Rack.findAll({
|
||||
where,
|
||||
include: [
|
||||
{ model: Room, separate: false }
|
||||
],
|
||||
include: [{ model: Room, separate: false }],
|
||||
limit: pageSize,
|
||||
offset: offset
|
||||
offset: offset,
|
||||
});
|
||||
|
||||
// 单独查询每个机柜的设备信息(避免 JOIN 导致的数据重复问题)
|
||||
const rackIds = racks.map(r => r.rackId);
|
||||
const devices = await Device.findAll({
|
||||
where: { rackId: rackIds },
|
||||
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height']
|
||||
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height'],
|
||||
});
|
||||
|
||||
// 将设备信息关联到对应的机柜
|
||||
@@ -72,7 +70,7 @@ router.get('/', async (req, res) => {
|
||||
// 返回带分页信息的响应
|
||||
res.json({
|
||||
racks,
|
||||
total
|
||||
total,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -95,20 +93,20 @@ router.get('/all', async (req, res) => {
|
||||
if (keyword) {
|
||||
where[require('sequelize').Op.or] = [
|
||||
{ rackId: { [require('sequelize').Op.like]: `%${keyword}%` } },
|
||||
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } }
|
||||
{ name: { [require('sequelize').Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
const racks = await Rack.findAll({
|
||||
where,
|
||||
include: [{ model: Room, separate: false }],
|
||||
limit: MAX_EXPORT_SIZE
|
||||
limit: MAX_EXPORT_SIZE,
|
||||
});
|
||||
|
||||
const rackIds = racks.map(r => r.rackId);
|
||||
const devices = await Device.findAll({
|
||||
where: { rackId: rackIds },
|
||||
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height']
|
||||
attributes: ['deviceId', 'rackId', 'name', 'powerConsumption', 'height'],
|
||||
});
|
||||
|
||||
const deviceMap = {};
|
||||
@@ -125,7 +123,7 @@ router.get('/all', async (req, res) => {
|
||||
|
||||
res.json({
|
||||
racks,
|
||||
total: racks.length
|
||||
total: racks.length,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -139,51 +137,49 @@ router.get('/import-template', async (req, res) => {
|
||||
const templateData = [
|
||||
{
|
||||
'机柜ID(留空自动生成)': '',
|
||||
'机柜名称': '测试机柜1',
|
||||
'所属机房名称': '测试机房1',
|
||||
机柜名称: '测试机柜1',
|
||||
所属机房名称: '测试机房1',
|
||||
'高度(U)': 42,
|
||||
'最大功率(W)': 5000,
|
||||
'状态': 'active'
|
||||
状态: 'active',
|
||||
},
|
||||
{
|
||||
'机柜ID(留空自动生成)': 'RACK001',
|
||||
'机柜名称': '测试机柜2',
|
||||
'所属机房名称': '测试机房1',
|
||||
机柜名称: '测试机柜2',
|
||||
所属机房名称: '测试机房1',
|
||||
'高度(U)': 42,
|
||||
'最大功率(W)': 3000,
|
||||
'状态': 'maintenance'
|
||||
}
|
||||
状态: 'maintenance',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
// 使用xlsx创建工作簿
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
|
||||
// 将数据转换为工作表
|
||||
const ws = XLSX.utils.json_to_sheet(templateData);
|
||||
|
||||
|
||||
// 设置列宽
|
||||
ws['!cols'] = [
|
||||
{ wch: 15 },
|
||||
{ wch: 20 },
|
||||
{ wch: 15 },
|
||||
{ wch: 10 },
|
||||
{ wch: 15 },
|
||||
{ wch: 15 }
|
||||
];
|
||||
|
||||
ws['!cols'] = [{ wch: 15 }, { wch: 20 }, { wch: 15 }, { wch: 10 }, { wch: 15 }, { wch: 15 }];
|
||||
|
||||
// 添加工作表到工作簿
|
||||
XLSX.utils.book_append_sheet(wb, ws, '机柜模板');
|
||||
|
||||
|
||||
// 生成Excel文件的Buffer
|
||||
const excelBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
|
||||
// 设置响应头
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent('机柜导入模板.xlsx')}`);
|
||||
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`attachment; filename*=UTF-8''${encodeURIComponent('机柜导入模板.xlsx')}`
|
||||
);
|
||||
|
||||
// 发送文件
|
||||
res.send(excelBuffer);
|
||||
|
||||
} catch (error) {
|
||||
console.error('生成导入模板失败:', error);
|
||||
res.status(500).json({ error: '生成导入模板失败' });
|
||||
@@ -197,34 +193,36 @@ router.get('/export', async (req, res) => {
|
||||
const racks = await Rack.findAll({
|
||||
include: [
|
||||
{ model: Room, attributes: ['name'] },
|
||||
{ model: Device, attributes: ['deviceId', 'name', 'powerConsumption'] }
|
||||
{ model: Device, attributes: ['deviceId', 'name', 'powerConsumption'] },
|
||||
],
|
||||
order: [['rackId', 'ASC']]
|
||||
order: [['rackId', 'ASC']],
|
||||
});
|
||||
|
||||
// 准备导出数据
|
||||
const exportData = racks.map(rack => {
|
||||
const deviceCount = rack.Devices ? rack.Devices.length : 0;
|
||||
const totalPower = rack.Devices ? rack.Devices.reduce((sum, d) => sum + (d.powerConsumption || 0), 0) : 0;
|
||||
|
||||
const totalPower = rack.Devices
|
||||
? rack.Devices.reduce((sum, d) => sum + (d.powerConsumption || 0), 0)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
'机柜ID': rack.rackId,
|
||||
'机柜名称': rack.name,
|
||||
'所属机房': rack.Room ? rack.Room.name : '',
|
||||
机柜ID: rack.rackId,
|
||||
机柜名称: rack.name,
|
||||
所属机房: rack.Room ? rack.Room.name : '',
|
||||
'机柜高度(U)': rack.height,
|
||||
'最大功耗(W)': rack.maxPower,
|
||||
'当前功耗(W)': rack.currentPower || 0,
|
||||
'设备数量': deviceCount,
|
||||
设备数量: deviceCount,
|
||||
'设备总功耗(W)': totalPower,
|
||||
'状态': rack.status === 'active' ? '启用' : rack.status === 'maintenance' ? '维护中' : '停用',
|
||||
'创建时间': rack.createdAt ? new Date(rack.createdAt).toLocaleString() : ''
|
||||
状态: rack.status === 'active' ? '启用' : rack.status === 'maintenance' ? '维护中' : '停用',
|
||||
创建时间: rack.createdAt ? new Date(rack.createdAt).toLocaleString() : '',
|
||||
};
|
||||
});
|
||||
|
||||
// 创建工作簿
|
||||
const wb = XLSX.utils.book_new();
|
||||
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||
|
||||
|
||||
// 设置列宽
|
||||
ws['!cols'] = [
|
||||
{ wch: 15 }, // 机柜ID
|
||||
@@ -236,53 +234,58 @@ router.get('/export', async (req, res) => {
|
||||
{ wch: 12 }, // 设备数量
|
||||
{ wch: 15 }, // 设备总功耗
|
||||
{ wch: 10 }, // 状态
|
||||
{ wch: 20 } // 创建时间
|
||||
{ wch: 20 }, // 创建时间
|
||||
];
|
||||
|
||||
|
||||
XLSX.utils.book_append_sheet(wb, ws, '机柜列表');
|
||||
|
||||
|
||||
// 生成文件名
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
const fileName = `机柜导出_${timestamp}.xlsx`;
|
||||
|
||||
|
||||
// 确保temp目录存在
|
||||
const tempDir = path.join(__dirname, '../temp');
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
const filePath = path.join(tempDir, fileName);
|
||||
|
||||
|
||||
// 写入文件
|
||||
XLSX.writeFile(wb, filePath);
|
||||
|
||||
|
||||
// 发送文件
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
|
||||
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`
|
||||
);
|
||||
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
fileStream.pipe(res);
|
||||
|
||||
|
||||
// 发送完成后删除临时文件
|
||||
fileStream.on('close', () => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
fileStream.on('error', (err) => {
|
||||
|
||||
fileStream.on('error', err => {
|
||||
console.error('文件流错误:', err);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('导出租机柜数据失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '导出失败',
|
||||
error: error.message
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -293,9 +296,9 @@ router.get('/:rackId', async (req, res) => {
|
||||
const rack = await Rack.findByPk(req.params.rackId, {
|
||||
include: [
|
||||
{ model: Room, separate: false },
|
||||
{ model: Device, separate: false }
|
||||
{ model: Device, separate: false },
|
||||
],
|
||||
subQuery: false // 避免子查询导致的性能问题
|
||||
subQuery: false, // 避免子查询导致的性能问题
|
||||
});
|
||||
if (!rack) {
|
||||
return res.status(404).json({ error: '机柜不存在' });
|
||||
@@ -312,11 +315,11 @@ async function generateRackId() {
|
||||
const racks = await Rack.findAll({
|
||||
where: {
|
||||
rackId: {
|
||||
[require('sequelize').Op.like]: 'RACK%'
|
||||
}
|
||||
}
|
||||
[require('sequelize').Op.like]: 'RACK%',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
let maxNumber = 0;
|
||||
racks.forEach(rack => {
|
||||
const match = rack.rackId.match(/^RACK(\d+)$/);
|
||||
@@ -327,7 +330,7 @@ async function generateRackId() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 生成新的机柜ID,序号+1,至少3位数字
|
||||
const newNumber = maxNumber + 1;
|
||||
return `RACK${String(newNumber).padStart(3, '0')}`;
|
||||
@@ -337,12 +340,12 @@ async function generateRackId() {
|
||||
router.post('/', validateBody(createRackSchema), async (req, res) => {
|
||||
try {
|
||||
const rackData = { ...req.body };
|
||||
|
||||
|
||||
// 如果没有提供rackId或为空,则自动生成
|
||||
if (!rackData.rackId || rackData.rackId.trim() === '') {
|
||||
rackData.rackId = await generateRackId();
|
||||
}
|
||||
|
||||
|
||||
const rack = await Rack.create(rackData);
|
||||
res.status(201).json(rack);
|
||||
} catch (error) {
|
||||
@@ -354,15 +357,15 @@ router.post('/', validateBody(createRackSchema), async (req, res) => {
|
||||
router.put('/:rackId', validateBody(updateRackSchema), async (req, res) => {
|
||||
try {
|
||||
const [updated] = await Rack.update(req.body, {
|
||||
where: { rackId: req.params.rackId }
|
||||
where: { rackId: req.params.rackId },
|
||||
});
|
||||
if (updated) {
|
||||
const updatedRack = await Rack.findByPk(req.params.rackId, {
|
||||
include: [
|
||||
{ model: Room, separate: false },
|
||||
{ model: Device, separate: false }
|
||||
{ model: Device, separate: false },
|
||||
],
|
||||
subQuery: false // 避免子查询导致的性能问题
|
||||
subQuery: false, // 避免子查询导致的性能问题
|
||||
});
|
||||
res.json(updatedRack);
|
||||
} else {
|
||||
@@ -381,9 +384,9 @@ router.delete('/:rackId', async (req, res) => {
|
||||
if (devices.length > 0) {
|
||||
return res.status(400).json({ error: '该机柜下有设备,无法删除' });
|
||||
}
|
||||
|
||||
|
||||
const deleted = await Rack.destroy({
|
||||
where: { rackId: req.params.rackId }
|
||||
where: { rackId: req.params.rackId },
|
||||
});
|
||||
if (deleted) {
|
||||
res.status(204).json();
|
||||
@@ -398,36 +401,36 @@ router.delete('/:rackId', async (req, res) => {
|
||||
// 导入机柜数据 - 优化版:使用事务+批量插入
|
||||
router.post('/import', async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
|
||||
|
||||
try {
|
||||
// 检查是否有上传文件
|
||||
if (!req.files || !req.files.file) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '没有上传文件',
|
||||
error: '没有找到有效的上传文件,请选择一个Excel文件后重试'
|
||||
error: '没有找到有效的上传文件,请选择一个Excel文件后重试',
|
||||
});
|
||||
}
|
||||
|
||||
const file = req.files.file;
|
||||
|
||||
|
||||
// 确保temp目录存在
|
||||
const tempDir = path.join(__dirname, '../temp');
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
// 保存临时文件
|
||||
const tempFilePath = path.join(tempDir, `${Date.now()}_${file.name}`);
|
||||
try {
|
||||
await file.mv(tempFilePath);
|
||||
} catch (saveError) {
|
||||
await t.rollback();
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '文件保存失败',
|
||||
error: `无法保存上传的文件: ${saveError.message}`
|
||||
error: `无法保存上传的文件: ${saveError.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -438,28 +441,29 @@ router.post('/import', async (req, res) => {
|
||||
workbook = XLSX.readFile(tempFilePath);
|
||||
} catch (readError) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '文件解析失败',
|
||||
error: `无法解析Excel文件: ${readError.message}`
|
||||
error: `无法解析Excel文件: ${readError.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 获取第一个工作表
|
||||
if (!workbook.SheetNames.length) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '文件格式错误',
|
||||
error: 'Excel文件中没有找到工作表'
|
||||
error: 'Excel文件中没有找到工作表',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
|
||||
|
||||
// 读取第一行作为列头
|
||||
const headerRow = XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 0, limit: 1 })[0] || [];
|
||||
|
||||
const headerRow =
|
||||
XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 0, limit: 1 })[0] || [];
|
||||
|
||||
// 定义列名映射(支持导入模板格式和导出文件格式)
|
||||
const columnMapping = {
|
||||
rackId: ['机柜ID(留空自动生成)', '机柜ID'],
|
||||
@@ -467,9 +471,9 @@ router.post('/import', async (req, res) => {
|
||||
roomName: ['所属机房名称', '所属机房'],
|
||||
height: ['高度(U)', '机柜高度(U)'],
|
||||
maxPower: ['最大功率(W)', '最大功耗(W)'],
|
||||
status: ['状态']
|
||||
status: ['状态'],
|
||||
};
|
||||
|
||||
|
||||
// 根据列头自动检测列索引映射
|
||||
const columnIndexMap = {};
|
||||
Object.keys(columnMapping).forEach(field => {
|
||||
@@ -479,26 +483,26 @@ router.post('/import', async (req, res) => {
|
||||
columnIndexMap[field] = index;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 检查必需的列是否存在
|
||||
const requiredColumns = ['name', 'roomName', 'height', 'maxPower', 'status'];
|
||||
const missingColumns = requiredColumns.filter(col => columnIndexMap[col] === undefined);
|
||||
if (missingColumns.length > 0) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Excel列名格式不正确',
|
||||
error: `缺少必需的列: ${missingColumns.join(', ')},请使用系统导出的文件或下载导入模板`
|
||||
error: `缺少必需的列: ${missingColumns.join(', ')},请使用系统导出的文件或下载导入模板`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 转换为JSON格式
|
||||
const rawData = XLSX.utils.sheet_to_json(worksheet, {
|
||||
header: headerRow.map((h, i) => `col_${i}`),
|
||||
range: 1,
|
||||
blankrows: false
|
||||
blankrows: false,
|
||||
});
|
||||
|
||||
|
||||
const jsonData = rawData.map(row => {
|
||||
const item = {};
|
||||
Object.keys(columnIndexMap).forEach(field => {
|
||||
@@ -510,67 +514,80 @@ router.post('/import', async (req, res) => {
|
||||
|
||||
if (jsonData.length === 0) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '没有找到有效数据',
|
||||
error: 'Excel文件中没有找到可导入的数据行'
|
||||
error: 'Excel文件中没有找到可导入的数据行',
|
||||
});
|
||||
}
|
||||
|
||||
// 状态值转换映射
|
||||
const statusMapping = {
|
||||
'启用': 'active', '在用': 'active', '停用': 'inactive',
|
||||
'禁用': 'inactive', '维护中': 'maintenance',
|
||||
'active': 'active', 'inactive': 'inactive', 'maintenance': 'maintenance'
|
||||
启用: 'active',
|
||||
在用: 'active',
|
||||
停用: 'inactive',
|
||||
禁用: 'inactive',
|
||||
维护中: 'maintenance',
|
||||
active: 'active',
|
||||
inactive: 'inactive',
|
||||
maintenance: 'maintenance',
|
||||
};
|
||||
|
||||
|
||||
const validStatuses = ['active', 'maintenance', 'inactive'];
|
||||
const validationResults = [];
|
||||
|
||||
|
||||
// 【优化1】批量查询机房信息(单次查询)
|
||||
const allRooms = await Room.findAll({ transaction: t });
|
||||
const roomNameToIdMap = new Map(allRooms.map(room => [room.name, room.roomId]));
|
||||
const validRoomNames = new Set(roomNameToIdMap.keys());
|
||||
|
||||
|
||||
// 【优化2】批量查询现有最大机柜ID(单次查询)
|
||||
const maxRackResult = await Rack.findOne({
|
||||
attributes: [[sequelize.fn('MAX', sequelize.cast(sequelize.fn('SUBSTR', sequelize.col('rackId'), 5), 'INTEGER')), 'maxNum']],
|
||||
attributes: [
|
||||
[
|
||||
sequelize.fn(
|
||||
'MAX',
|
||||
sequelize.cast(sequelize.fn('SUBSTR', sequelize.col('rackId'), 5), 'INTEGER')
|
||||
),
|
||||
'maxNum',
|
||||
],
|
||||
],
|
||||
where: {
|
||||
rackId: {
|
||||
[require('sequelize').Op.like]: 'RACK%'
|
||||
}
|
||||
[require('sequelize').Op.like]: 'RACK%',
|
||||
},
|
||||
},
|
||||
transaction: t
|
||||
transaction: t,
|
||||
});
|
||||
let maxNumber = maxRackResult?.get('maxNum') || 0;
|
||||
|
||||
|
||||
// 处理数据
|
||||
const processedData = jsonData.map((item, index) => {
|
||||
const rowNumber = index + 2;
|
||||
const rawStatus = String(item.status || '').trim();
|
||||
const normalizedStatus = statusMapping[rawStatus] || rawStatus.toLowerCase();
|
||||
const rawRackId = item.rackId ? String(item.rackId).trim() : '';
|
||||
|
||||
|
||||
if (!rawRackId || rawRackId === '') {
|
||||
maxNumber++;
|
||||
return {
|
||||
...item,
|
||||
rackId: `RACK${String(maxNumber).padStart(3, '0')}`,
|
||||
return {
|
||||
...item,
|
||||
rackId: `RACK${String(maxNumber).padStart(3, '0')}`,
|
||||
status: normalizedStatus,
|
||||
rowNumber
|
||||
rowNumber,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
rackId: rawRackId,
|
||||
|
||||
return {
|
||||
...item,
|
||||
rackId: rawRackId,
|
||||
status: normalizedStatus,
|
||||
rowNumber
|
||||
rowNumber,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// 验证数据
|
||||
processedData.forEach((item) => {
|
||||
processedData.forEach(item => {
|
||||
const errors = [];
|
||||
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(item.rackId)) {
|
||||
@@ -594,7 +611,7 @@ router.post('/import', async (req, res) => {
|
||||
if (!item.status || !validStatuses.includes(item.status)) {
|
||||
errors.push(`状态必须是以下值之一: ${validStatuses.join(', ')}`);
|
||||
}
|
||||
|
||||
|
||||
if (errors.length > 0) {
|
||||
validationResults.push({ row: item.rowNumber, data: item, errors });
|
||||
}
|
||||
@@ -602,26 +619,26 @@ router.post('/import', async (req, res) => {
|
||||
|
||||
if (validationResults.length > 0) {
|
||||
await t.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '数据验证失败',
|
||||
error: `${validationResults.length} 行数据格式错误`,
|
||||
details: validationResults
|
||||
details: validationResults,
|
||||
});
|
||||
}
|
||||
|
||||
// 【优化3】批量查询已存在的机柜ID(单次查询)
|
||||
const existingRacks = await Rack.findAll({
|
||||
where: {
|
||||
rackId: processedData.map(item => item.rackId)
|
||||
rackId: processedData.map(item => item.rackId),
|
||||
},
|
||||
transaction: t
|
||||
transaction: t,
|
||||
});
|
||||
|
||||
|
||||
const existingIds = new Set(existingRacks.map(rack => rack.rackId));
|
||||
const newData = processedData.filter(item => !existingIds.has(item.rackId));
|
||||
const duplicateCount = processedData.length - newData.length;
|
||||
|
||||
|
||||
// 【优化4】批量插入数据
|
||||
let createdCount = 0;
|
||||
if (newData.length > 0) {
|
||||
@@ -632,16 +649,16 @@ router.post('/import', async (req, res) => {
|
||||
maxPower: item.maxPower,
|
||||
status: item.status,
|
||||
roomId: roomNameToIdMap.get(item.roomName.trim()),
|
||||
currentPower: 0
|
||||
currentPower: 0,
|
||||
}));
|
||||
|
||||
|
||||
const result = await Rack.bulkCreate(dataWithRoomId, {
|
||||
transaction: t,
|
||||
ignoreDuplicates: true
|
||||
ignoreDuplicates: true,
|
||||
});
|
||||
createdCount = result.length;
|
||||
}
|
||||
|
||||
|
||||
// 提交事务
|
||||
await t.commit();
|
||||
|
||||
@@ -655,7 +672,7 @@ router.post('/import', async (req, res) => {
|
||||
duplicates: duplicateCount,
|
||||
total: jsonData.length,
|
||||
createdRacks,
|
||||
skippedRacks
|
||||
skippedRacks,
|
||||
});
|
||||
} finally {
|
||||
// 删除临时文件
|
||||
@@ -665,12 +682,12 @@ router.post('/import', async (req, res) => {
|
||||
}
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误',
|
||||
error: `导入过程中发生未知错误: ${error.message}`
|
||||
error: `导入过程中发生未知错误: ${error.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
+109
-67
@@ -32,7 +32,10 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
order: [['sort', 'ASC'], ['createdAt', 'DESC']]
|
||||
order: [
|
||||
['sort', 'ASC'],
|
||||
['createdAt', 'DESC'],
|
||||
],
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -41,14 +44,14 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
roles
|
||||
}
|
||||
roles,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取角色列表错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取角色列表失败'
|
||||
message: '获取角色列表失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -57,18 +60,18 @@ router.get('/all', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const roles = await Role.findAll({
|
||||
where: { status: 'active' },
|
||||
order: [['sort', 'ASC']]
|
||||
order: [['sort', 'ASC']],
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: roles
|
||||
data: roles,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取所有角色错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取角色列表失败'
|
||||
message: '获取角色列表失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -80,13 +83,13 @@ router.get('/:roleId', authMiddleware, async (req, res) => {
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '角色不存在'
|
||||
message: '角色不存在',
|
||||
});
|
||||
}
|
||||
|
||||
const permissions = await Permission.findAll({
|
||||
where: { status: 'active' },
|
||||
order: [['sort', 'ASC']]
|
||||
order: [['sort', 'ASC']],
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -94,14 +97,14 @@ router.get('/:roleId', authMiddleware, async (req, res) => {
|
||||
data: {
|
||||
role,
|
||||
permissions,
|
||||
rolePermissions: role.permissions || []
|
||||
}
|
||||
rolePermissions: role.permissions || [],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取角色详情错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取角色详情失败'
|
||||
message: '获取角色详情失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -113,7 +116,7 @@ router.post('/', authMiddleware, async (req, res) => {
|
||||
if (!roleName || !roleCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '角色名称和角色编码不能为空'
|
||||
message: '角色名称和角色编码不能为空',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,7 +124,7 @@ router.post('/', authMiddleware, async (req, res) => {
|
||||
if (existingRole) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '角色编码已存在'
|
||||
message: '角色编码已存在',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,31 +135,33 @@ router.post('/', authMiddleware, async (req, res) => {
|
||||
description,
|
||||
permissions: permissions || [],
|
||||
status: status || 'active',
|
||||
sort: sort || 0
|
||||
sort: sort || 0,
|
||||
});
|
||||
|
||||
const permissionNames = permissions && permissions.length > 0
|
||||
? permissions.join('、')
|
||||
: '无';
|
||||
const permissionNames = permissions && permissions.length > 0 ? permissions.join('、') : '无';
|
||||
|
||||
await logRoleOperation('create', `创建角色【${roleName}】(编码:${roleCode},权限:${permissionNames})`, {
|
||||
targetId: role.roleId,
|
||||
targetName: roleName,
|
||||
afterState: role.toJSON(),
|
||||
req,
|
||||
metadata: { roleCode, permissions, permissionNames }
|
||||
});
|
||||
await logRoleOperation(
|
||||
'create',
|
||||
`创建角色【${roleName}】(编码:${roleCode},权限:${permissionNames})`,
|
||||
{
|
||||
targetId: role.roleId,
|
||||
targetName: roleName,
|
||||
afterState: role.toJSON(),
|
||||
req,
|
||||
metadata: { roleCode, permissions, permissionNames },
|
||||
}
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '创建成功',
|
||||
data: role
|
||||
data: role,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建角色错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建角色失败'
|
||||
message: '创建角色失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -169,17 +174,27 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '角色不存在'
|
||||
message: '角色不存在',
|
||||
});
|
||||
}
|
||||
|
||||
const beforeState = role.toJSON();
|
||||
|
||||
if (roleName !== undefined) role.roleName = roleName;
|
||||
if (description !== undefined) role.description = description;
|
||||
if (permissions !== undefined) role.permissions = permissions;
|
||||
if (status !== undefined) role.status = status;
|
||||
if (sort !== undefined) role.sort = sort;
|
||||
if (roleName !== undefined) {
|
||||
role.roleName = roleName;
|
||||
}
|
||||
if (description !== undefined) {
|
||||
role.description = description;
|
||||
}
|
||||
if (permissions !== undefined) {
|
||||
role.permissions = permissions;
|
||||
}
|
||||
if (status !== undefined) {
|
||||
role.status = status;
|
||||
}
|
||||
if (sort !== undefined) {
|
||||
role.sort = sort;
|
||||
}
|
||||
|
||||
await role.save();
|
||||
|
||||
@@ -201,21 +216,33 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
|
||||
}
|
||||
if (status !== undefined && beforeState.status !== status) {
|
||||
const statusText = { active: '启用', inactive: '禁用' };
|
||||
changedFields.status = { from: beforeState.status, to: status, fromText: statusText[beforeState.status], toText: statusText[status] };
|
||||
changedFields.status = {
|
||||
from: beforeState.status,
|
||||
to: status,
|
||||
fromText: statusText[beforeState.status],
|
||||
toText: statusText[status],
|
||||
};
|
||||
}
|
||||
|
||||
const changeDetails = Object.entries(changedFields).map(([field, values]) => {
|
||||
const fieldNames = { roleName: '角色名称', description: '描述', permissions: '权限', status: '状态' };
|
||||
const displayName = fieldNames[field] || field;
|
||||
const changeDetails = Object.entries(changedFields)
|
||||
.map(([field, values]) => {
|
||||
const fieldNames = {
|
||||
roleName: '角色名称',
|
||||
description: '描述',
|
||||
permissions: '权限',
|
||||
status: '状态',
|
||||
};
|
||||
const displayName = fieldNames[field] || field;
|
||||
|
||||
if (field === 'permissions') {
|
||||
return `权限: ${(values.from || []).join('、') || '无'} → ${(values.to || []).join('、') || '无'}`;
|
||||
}
|
||||
if (field === 'status') {
|
||||
return `状态: ${values.fromText} → ${values.toText}`;
|
||||
}
|
||||
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
||||
}).join(';');
|
||||
if (field === 'permissions') {
|
||||
return `权限: ${(values.from || []).join('、') || '无'} → ${(values.to || []).join('、') || '无'}`;
|
||||
}
|
||||
if (field === 'status') {
|
||||
return `状态: ${values.fromText} → ${values.toText}`;
|
||||
}
|
||||
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
||||
})
|
||||
.join(';');
|
||||
|
||||
const updateDesc = changeDetails
|
||||
? `更新角色【${role.roleName}】:${changeDetails}`
|
||||
@@ -227,19 +254,23 @@ router.put('/:roleId', authMiddleware, async (req, res) => {
|
||||
beforeState,
|
||||
afterState,
|
||||
req,
|
||||
metadata: { changedFields, oldRoleName: beforeState.roleName, oldPermissions: beforeState.permissions }
|
||||
metadata: {
|
||||
changedFields,
|
||||
oldRoleName: beforeState.roleName,
|
||||
oldPermissions: beforeState.permissions,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '更新成功',
|
||||
data: role
|
||||
data: role,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新角色错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新角色失败'
|
||||
message: '更新角色失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -251,14 +282,14 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
|
||||
if (!role) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '角色不存在'
|
||||
message: '角色不存在',
|
||||
});
|
||||
}
|
||||
|
||||
if (role.roleCode === 'admin') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '不能删除管理员角色'
|
||||
message: '不能删除管理员角色',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -266,7 +297,7 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
|
||||
if (userCount > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '该角色下有用户,不能删除'
|
||||
message: '该角色下有用户,不能删除',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -276,23 +307,27 @@ router.delete('/:roleId', authMiddleware, async (req, res) => {
|
||||
|
||||
await role.destroy();
|
||||
|
||||
await logRoleOperation('delete', `删除角色【${roleName}】(编码:${roleCode},权限:${(role.permissions || []).join('、') || '无'})`, {
|
||||
targetId: req.params.roleId,
|
||||
targetName: roleName,
|
||||
beforeState,
|
||||
req,
|
||||
metadata: { roleCode, userCount, permissions: role.permissions }
|
||||
});
|
||||
await logRoleOperation(
|
||||
'delete',
|
||||
`删除角色【${roleName}】(编码:${roleCode},权限:${(role.permissions || []).join('、') || '无'})`,
|
||||
{
|
||||
targetId: req.params.roleId,
|
||||
targetName: roleName,
|
||||
beforeState,
|
||||
req,
|
||||
metadata: { roleCode, userCount, permissions: role.permissions },
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
message: '删除成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除角色错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除角色失败'
|
||||
message: '删除角色失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -307,16 +342,23 @@ router.post('/init-roles', async (req, res) => {
|
||||
description: '系统管理员,拥有所有权限',
|
||||
permissions: ['*'],
|
||||
status: 'active',
|
||||
sort: 1
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
roleId: 'role_operator',
|
||||
roleName: '运维人员',
|
||||
roleCode: 'operator',
|
||||
description: '负责日常运维操作',
|
||||
permissions: ['devices:read', 'devices:write', 'racks:read', 'rooms:read', 'consumables:read', 'consumables:write'],
|
||||
permissions: [
|
||||
'devices:read',
|
||||
'devices:write',
|
||||
'racks:read',
|
||||
'rooms:read',
|
||||
'consumables:read',
|
||||
'consumables:write',
|
||||
],
|
||||
status: 'active',
|
||||
sort: 2
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
roleId: 'role_viewer',
|
||||
@@ -325,8 +367,8 @@ router.post('/init-roles', async (req, res) => {
|
||||
description: '仅能查看数据',
|
||||
permissions: ['devices:read', 'racks:read', 'rooms:read', 'consumables:read'],
|
||||
status: 'active',
|
||||
sort: 3
|
||||
}
|
||||
sort: 3,
|
||||
},
|
||||
];
|
||||
|
||||
for (const roleData of defaultRoles) {
|
||||
@@ -335,13 +377,13 @@ router.post('/init-roles', async (req, res) => {
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '初始化角色成功'
|
||||
message: '初始化角色成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('初始化角色错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '初始化角色失败'
|
||||
message: '初始化角色失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,12 +15,12 @@ router.get('/', async (req, res) => {
|
||||
const { count, rows } = await Room.findAndCountAll({
|
||||
include: [{ model: Rack, attributes: ['rackId', 'name'] }],
|
||||
offset: offset,
|
||||
limit: pageSize
|
||||
limit: pageSize,
|
||||
});
|
||||
|
||||
res.json({
|
||||
rooms: rows,
|
||||
total: count
|
||||
total: count,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -31,7 +31,7 @@ router.get('/', async (req, res) => {
|
||||
router.get('/:roomId', async (req, res) => {
|
||||
try {
|
||||
const room = await Room.findByPk(req.params.roomId, {
|
||||
include: Rack
|
||||
include: Rack,
|
||||
});
|
||||
if (!room) {
|
||||
return res.status(404).json({ error: '机房不存在' });
|
||||
@@ -56,7 +56,7 @@ router.post('/', async (req, res) => {
|
||||
router.put('/:roomId', validateBody(updateRoomSchema), async (req, res) => {
|
||||
try {
|
||||
const [updated] = await Room.update(req.body, {
|
||||
where: { roomId: req.params.roomId }
|
||||
where: { roomId: req.params.roomId },
|
||||
});
|
||||
if (updated) {
|
||||
const updatedRoom = await Room.findByPk(req.params.roomId);
|
||||
@@ -77,9 +77,9 @@ router.delete('/:roomId', async (req, res) => {
|
||||
if (racks.length > 0) {
|
||||
return res.status(400).json({ error: '该机房下有机柜,无法删除' });
|
||||
}
|
||||
|
||||
|
||||
const deleted = await Room.destroy({
|
||||
where: { roomId: req.params.roomId }
|
||||
where: { roomId: req.params.roomId },
|
||||
});
|
||||
if (deleted) {
|
||||
res.status(204).json();
|
||||
@@ -91,4 +91,4 @@ router.delete('/:roomId', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
@@ -32,26 +32,37 @@ router.get('/', async (req, res) => {
|
||||
where: {
|
||||
createdAt: {
|
||||
[Op.gte]: dayStart,
|
||||
[Op.lt]: dayEnd
|
||||
}
|
||||
}
|
||||
})
|
||||
[Op.lt]: dayEnd,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const dayResults = await Promise.all(dayQueries.map(d => d.query));
|
||||
const deviceTrendData = dayQueries.map((d, index) => ({
|
||||
label: d.label,
|
||||
value: dayResults[index]
|
||||
value: dayResults[index],
|
||||
}));
|
||||
|
||||
const [
|
||||
totalDevices, faultDevices, totalRacks, rooms, totalUsers, activeTickets,
|
||||
newDevicesThisWeek, newDevicesLastWeek,
|
||||
faultDevicesThisWeek, faultDevicesLastWeek,
|
||||
newUsersThisWeek, newUsersLastWeek,
|
||||
newTicketsThisWeek, newTicketsLastWeek,
|
||||
runningDevices, maintenanceDevices, offlineDevices
|
||||
totalDevices,
|
||||
faultDevices,
|
||||
totalRacks,
|
||||
rooms,
|
||||
totalUsers,
|
||||
activeTickets,
|
||||
newDevicesThisWeek,
|
||||
newDevicesLastWeek,
|
||||
faultDevicesThisWeek,
|
||||
faultDevicesLastWeek,
|
||||
newUsersThisWeek,
|
||||
newUsersLastWeek,
|
||||
newTicketsThisWeek,
|
||||
newTicketsLastWeek,
|
||||
runningDevices,
|
||||
maintenanceDevices,
|
||||
offlineDevices,
|
||||
] = await Promise.all([
|
||||
Device.count(),
|
||||
Device.count({ where: { status: 'fault' } }),
|
||||
@@ -62,7 +73,9 @@ router.get('/', async (req, res) => {
|
||||
Device.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
||||
Device.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
||||
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: oneWeekAgo } } }),
|
||||
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
||||
Device.count({
|
||||
where: { status: 'fault', updatedAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } },
|
||||
}),
|
||||
User.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
||||
User.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
||||
Ticket.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
||||
@@ -74,7 +87,9 @@ router.get('/', async (req, res) => {
|
||||
|
||||
let deviceGrowth = 0;
|
||||
if (newDevicesLastWeek > 0) {
|
||||
deviceGrowth = parseFloat(((newDevicesThisWeek - newDevicesLastWeek) / newDevicesLastWeek * 100).toFixed(1));
|
||||
deviceGrowth = parseFloat(
|
||||
(((newDevicesThisWeek - newDevicesLastWeek) / newDevicesLastWeek) * 100).toFixed(1)
|
||||
);
|
||||
} else if (newDevicesThisWeek > 0) {
|
||||
deviceGrowth = 100;
|
||||
} else {
|
||||
@@ -83,7 +98,9 @@ router.get('/', async (req, res) => {
|
||||
|
||||
let faultTrend = 0;
|
||||
if (faultDevicesLastWeek > 0) {
|
||||
faultTrend = parseFloat(((faultDevicesThisWeek - faultDevicesLastWeek) / faultDevicesLastWeek * 100).toFixed(1));
|
||||
faultTrend = parseFloat(
|
||||
(((faultDevicesThisWeek - faultDevicesLastWeek) / faultDevicesLastWeek) * 100).toFixed(1)
|
||||
);
|
||||
} else if (faultDevicesThisWeek > 0) {
|
||||
faultTrend = 100;
|
||||
} else {
|
||||
@@ -92,7 +109,9 @@ router.get('/', async (req, res) => {
|
||||
|
||||
let userGrowth = 0;
|
||||
if (newUsersLastWeek > 0) {
|
||||
userGrowth = parseFloat(((newUsersThisWeek - newUsersLastWeek) / newUsersLastWeek * 100).toFixed(1));
|
||||
userGrowth = parseFloat(
|
||||
(((newUsersThisWeek - newUsersLastWeek) / newUsersLastWeek) * 100).toFixed(1)
|
||||
);
|
||||
} else if (newUsersThisWeek > 0) {
|
||||
userGrowth = 100;
|
||||
} else {
|
||||
@@ -101,16 +120,17 @@ router.get('/', async (req, res) => {
|
||||
|
||||
let ticketTrend = 0;
|
||||
if (newTicketsLastWeek > 0) {
|
||||
ticketTrend = parseFloat(((newTicketsThisWeek - newTicketsLastWeek) / newTicketsLastWeek * 100).toFixed(1));
|
||||
ticketTrend = parseFloat(
|
||||
(((newTicketsThisWeek - newTicketsLastWeek) / newTicketsLastWeek) * 100).toFixed(1)
|
||||
);
|
||||
} else if (newTicketsThisWeek > 0) {
|
||||
ticketTrend = 100;
|
||||
} else {
|
||||
ticketTrend = 0;
|
||||
}
|
||||
|
||||
const onlineRate = totalDevices > 0
|
||||
? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1)
|
||||
: 100;
|
||||
const onlineRate =
|
||||
totalDevices > 0 ? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1) : 100;
|
||||
|
||||
const totalRooms = rooms.length;
|
||||
|
||||
@@ -146,4 +166,4 @@ router.get('/', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
+276
-109
@@ -10,33 +10,194 @@ const { FRONTEND } = require('../config');
|
||||
const initDefaultSettings = async () => {
|
||||
const defaultSettings = [
|
||||
// 全局配置
|
||||
{ settingKey: 'site_name', settingValue: JSON.stringify('机柜管理系统'), settingType: 'string', category: 'general', description: '网站名称', isEditable: true },
|
||||
{ settingKey: 'site_logo', settingValue: JSON.stringify(''), settingType: 'string', category: 'general', description: '网站Logo URL', isEditable: true },
|
||||
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
|
||||
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
|
||||
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
|
||||
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
|
||||
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(60), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
|
||||
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
|
||||
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
|
||||
{
|
||||
settingKey: 'site_name',
|
||||
settingValue: JSON.stringify('机柜管理系统'),
|
||||
settingType: 'string',
|
||||
category: 'general',
|
||||
description: '网站名称',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'site_logo',
|
||||
settingValue: JSON.stringify(''),
|
||||
settingType: 'string',
|
||||
category: 'general',
|
||||
description: '网站Logo URL',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'timezone',
|
||||
settingValue: JSON.stringify('Asia/Shanghai'),
|
||||
settingType: 'string',
|
||||
category: 'general',
|
||||
description: '时区设置',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'date_format',
|
||||
settingValue: JSON.stringify('YYYY-MM-DD'),
|
||||
settingType: 'string',
|
||||
category: 'general',
|
||||
description: '日期格式',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'session_timeout',
|
||||
settingValue: JSON.stringify(30),
|
||||
settingType: 'number',
|
||||
category: 'general',
|
||||
description: '登录有效期(分钟)',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'idle_timeout',
|
||||
settingValue: JSON.stringify(30),
|
||||
settingType: 'number',
|
||||
category: 'general',
|
||||
description: '用户空闲超时时间(分钟)',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'idle_warning_time',
|
||||
settingValue: JSON.stringify(60),
|
||||
settingType: 'number',
|
||||
category: 'general',
|
||||
description: '空闲超时前警告时间(秒)',
|
||||
isEditable: false,
|
||||
},
|
||||
{
|
||||
settingKey: 'max_login_attempts',
|
||||
settingValue: JSON.stringify(5),
|
||||
settingType: 'number',
|
||||
category: 'general',
|
||||
description: '最大登录尝试次数',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'maintenance_mode',
|
||||
settingValue: JSON.stringify(false),
|
||||
settingType: 'boolean',
|
||||
category: 'general',
|
||||
description: '维护模式',
|
||||
isEditable: true,
|
||||
},
|
||||
|
||||
// 外观设置
|
||||
{ settingKey: 'primary_color', settingValue: JSON.stringify('#667eea'), settingType: 'string', category: 'appearance', description: '主题主色调', isEditable: true },
|
||||
{ settingKey: 'secondary_color', settingValue: JSON.stringify('#764ba2'), settingType: 'string', category: 'appearance', description: '主题辅助色调', isEditable: true },
|
||||
{ settingKey: 'compact_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '紧凑模式', isEditable: true },
|
||||
{ settingKey: 'sidebar_collapsed', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '侧边栏默认折叠', isEditable: true },
|
||||
{ settingKey: 'table_row_height', settingValue: JSON.stringify('default'), settingType: 'string', category: 'appearance', description: '表格行高: small/default/middle/large', isEditable: true },
|
||||
{ settingKey: 'animation_enabled', settingValue: JSON.stringify(true), settingType: 'boolean', category: 'appearance', description: '启用动画效果', isEditable: true },
|
||||
{
|
||||
settingKey: 'primary_color',
|
||||
settingValue: JSON.stringify('#667eea'),
|
||||
settingType: 'string',
|
||||
category: 'appearance',
|
||||
description: '主题主色调',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'secondary_color',
|
||||
settingValue: JSON.stringify('#764ba2'),
|
||||
settingType: 'string',
|
||||
category: 'appearance',
|
||||
description: '主题辅助色调',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'compact_mode',
|
||||
settingValue: JSON.stringify(false),
|
||||
settingType: 'boolean',
|
||||
category: 'appearance',
|
||||
description: '紧凑模式',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'sidebar_collapsed',
|
||||
settingValue: JSON.stringify(false),
|
||||
settingType: 'boolean',
|
||||
category: 'appearance',
|
||||
description: '侧边栏默认折叠',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'table_row_height',
|
||||
settingValue: JSON.stringify('default'),
|
||||
settingType: 'string',
|
||||
category: 'appearance',
|
||||
description: '表格行高: small/default/middle/large',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'animation_enabled',
|
||||
settingValue: JSON.stringify(true),
|
||||
settingType: 'boolean',
|
||||
category: 'appearance',
|
||||
description: '启用动画效果',
|
||||
isEditable: true,
|
||||
},
|
||||
|
||||
// 关于页面
|
||||
{ settingKey: 'app_version', settingValue: JSON.stringify('1.0.0'), settingType: 'string', category: 'about', description: '应用版本', isEditable: false },
|
||||
{ settingKey: 'company_name', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司/组织名称', isEditable: true },
|
||||
{ settingKey: 'contact_email', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系邮箱', isEditable: true },
|
||||
{ settingKey: 'contact_phone', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系电话', isEditable: true },
|
||||
{ settingKey: 'company_address', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司地址', isEditable: true },
|
||||
{ settingKey: 'system_description', settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'), settingType: 'string', category: 'about', description: '系统描述', isEditable: true },
|
||||
{ settingKey: 'privacy_policy', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '隐私政策URL', isEditable: true },
|
||||
{ settingKey: 'terms_of_service', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '服务条款URL', isEditable: true },
|
||||
{
|
||||
settingKey: 'app_version',
|
||||
settingValue: JSON.stringify('1.0.0'),
|
||||
settingType: 'string',
|
||||
category: 'about',
|
||||
description: '应用版本',
|
||||
isEditable: false,
|
||||
},
|
||||
{
|
||||
settingKey: 'company_name',
|
||||
settingValue: JSON.stringify(''),
|
||||
settingType: 'string',
|
||||
category: 'about',
|
||||
description: '公司/组织名称',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'contact_email',
|
||||
settingValue: JSON.stringify(''),
|
||||
settingType: 'string',
|
||||
category: 'about',
|
||||
description: '联系邮箱',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'contact_phone',
|
||||
settingValue: JSON.stringify(''),
|
||||
settingType: 'string',
|
||||
category: 'about',
|
||||
description: '联系电话',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'company_address',
|
||||
settingValue: JSON.stringify(''),
|
||||
settingType: 'string',
|
||||
category: 'about',
|
||||
description: '公司地址',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'system_description',
|
||||
settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'),
|
||||
settingType: 'string',
|
||||
category: 'about',
|
||||
description: '系统描述',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'privacy_policy',
|
||||
settingValue: JSON.stringify(''),
|
||||
settingType: 'string',
|
||||
category: 'about',
|
||||
description: '隐私政策URL',
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
settingKey: 'terms_of_service',
|
||||
settingValue: JSON.stringify(''),
|
||||
settingType: 'string',
|
||||
category: 'about',
|
||||
description: '服务条款URL',
|
||||
isEditable: true,
|
||||
},
|
||||
];
|
||||
|
||||
let createdCount = 0;
|
||||
@@ -60,7 +221,9 @@ const initDefaultSettings = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`系统设置初始化结果: 创建 ${createdCount} 个, 更新 ${updatedCount} 个, 失败 ${errorCount} 个`);
|
||||
console.log(
|
||||
`系统设置初始化结果: 创建 ${createdCount} 个, 更新 ${updatedCount} 个, 失败 ${errorCount} 个`
|
||||
);
|
||||
return { createdCount, updatedCount, errorCount };
|
||||
};
|
||||
|
||||
@@ -75,12 +238,15 @@ router.get('/', async (req, res) => {
|
||||
if (category) {
|
||||
where.category = category;
|
||||
}
|
||||
|
||||
|
||||
const settings = await SystemSetting.findAll({
|
||||
where,
|
||||
order: [['category', 'ASC'], ['settingKey', 'ASC']]
|
||||
order: [
|
||||
['category', 'ASC'],
|
||||
['settingKey', 'ASC'],
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
// 格式化返回数据
|
||||
const formattedSettings = {};
|
||||
settings.forEach(setting => {
|
||||
@@ -90,10 +256,10 @@ router.get('/', async (req, res) => {
|
||||
category: setting.category,
|
||||
description: setting.description,
|
||||
isEditable: setting.isEditable,
|
||||
updatedAt: setting.updatedAt
|
||||
updatedAt: setting.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
res.json(formattedSettings);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -116,7 +282,7 @@ router.get('/idle-timeout', async (req, res) => {
|
||||
timeout: timeout * 60 * 1000, // 转换为毫秒
|
||||
warningTime: fixedWarningTime * 1000, // 固定10秒(转换为毫秒)
|
||||
timeoutMinutes: timeout,
|
||||
warningTimeSeconds: fixedWarningTime
|
||||
warningTimeSeconds: fixedWarningTime,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -128,11 +294,11 @@ router.get('/:key', async (req, res) => {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
const setting = await SystemSetting.findByPk(key);
|
||||
|
||||
|
||||
if (!setting) {
|
||||
return res.status(404).json({ error: '设置不存在' });
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
key: setting.settingKey,
|
||||
value: JSON.parse(setting.settingValue),
|
||||
@@ -140,7 +306,7 @@ router.get('/:key', async (req, res) => {
|
||||
category: setting.category,
|
||||
description: setting.description,
|
||||
isEditable: setting.isEditable,
|
||||
updatedAt: setting.updatedAt
|
||||
updatedAt: setting.updatedAt,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -152,17 +318,17 @@ router.put('/:key', async (req, res) => {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
const { value } = req.body;
|
||||
|
||||
|
||||
const setting = await SystemSetting.findByPk(key);
|
||||
|
||||
|
||||
if (!setting) {
|
||||
return res.status(404).json({ error: '设置不存在' });
|
||||
}
|
||||
|
||||
|
||||
if (!setting.isEditable) {
|
||||
return res.status(403).json({ error: '该设置不可编辑' });
|
||||
}
|
||||
|
||||
|
||||
// 验证值类型
|
||||
let parsedValue = value;
|
||||
if (setting.settingType === 'number') {
|
||||
@@ -173,18 +339,18 @@ router.put('/:key', async (req, res) => {
|
||||
} else if (setting.settingType === 'boolean') {
|
||||
parsedValue = Boolean(value);
|
||||
}
|
||||
|
||||
|
||||
await setting.update({
|
||||
settingValue: JSON.stringify(parsedValue)
|
||||
settingValue: JSON.stringify(parsedValue),
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
message: '设置更新成功',
|
||||
setting: {
|
||||
key: setting.settingKey,
|
||||
value: parsedValue,
|
||||
updatedAt: setting.updatedAt
|
||||
}
|
||||
updatedAt: setting.updatedAt,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -195,28 +361,28 @@ router.put('/:key', async (req, res) => {
|
||||
router.put('/', async (req, res) => {
|
||||
try {
|
||||
const { settings } = req.body;
|
||||
|
||||
|
||||
if (!settings || typeof settings !== 'object') {
|
||||
return res.status(400).json({ error: '请提供有效的设置对象' });
|
||||
}
|
||||
|
||||
|
||||
const updatedSettings = [];
|
||||
const errors = [];
|
||||
|
||||
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
try {
|
||||
const setting = await SystemSetting.findByPk(key);
|
||||
|
||||
|
||||
if (!setting) {
|
||||
errors.push({ key, error: '设置不存在' });
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (!setting.isEditable) {
|
||||
errors.push({ key, error: '该设置不可编辑' });
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
let parsedValue = value;
|
||||
if (setting.settingType === 'number') {
|
||||
parsedValue = Number(value);
|
||||
@@ -227,21 +393,21 @@ router.put('/', async (req, res) => {
|
||||
} else if (setting.settingType === 'boolean') {
|
||||
parsedValue = Boolean(value);
|
||||
}
|
||||
|
||||
|
||||
await setting.update({
|
||||
settingValue: JSON.stringify(parsedValue)
|
||||
settingValue: JSON.stringify(parsedValue),
|
||||
});
|
||||
|
||||
|
||||
updatedSettings.push({ key, value: parsedValue });
|
||||
} catch (error) {
|
||||
errors.push({ key, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
message: `成功更新 ${updatedSettings.length} 个设置`,
|
||||
updatedSettings,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -253,11 +419,11 @@ router.post('/reset/:key', async (req, res) => {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
const setting = await SystemSetting.findByPk(key);
|
||||
|
||||
|
||||
if (!setting) {
|
||||
return res.status(404).json({ error: '设置不存在' });
|
||||
}
|
||||
|
||||
|
||||
const defaultValues = {
|
||||
site_name: '机柜管理系统',
|
||||
site_logo: '',
|
||||
@@ -281,22 +447,22 @@ router.post('/reset/:key', async (req, res) => {
|
||||
company_address: '',
|
||||
system_description: '机柜管理系统 - 专业的数据中心设备管理解决方案',
|
||||
privacy_policy: '',
|
||||
terms_of_service: ''
|
||||
terms_of_service: '',
|
||||
};
|
||||
|
||||
|
||||
const defaultValue = defaultValues[key];
|
||||
if (defaultValue === undefined) {
|
||||
return res.status(400).json({ error: '该设置没有默认值' });
|
||||
}
|
||||
|
||||
|
||||
await setting.update({
|
||||
settingValue: JSON.stringify(defaultValue)
|
||||
settingValue: JSON.stringify(defaultValue),
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
message: '设置已重置为默认值',
|
||||
key,
|
||||
value: defaultValue
|
||||
value: defaultValue,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -309,7 +475,7 @@ router.post('/backup', async (req, res) => {
|
||||
// 获取系统设置中的备份路径
|
||||
const backupPathSetting = await SystemSetting.findByPk('backup_path');
|
||||
const backupPath = backupPathSetting ? JSON.parse(backupPathSetting.settingValue) : './backups';
|
||||
|
||||
|
||||
// 解析备份目录路径
|
||||
let backupDir;
|
||||
if (backupPath.startsWith('/') || backupPath.match(/^[A-Za-z]:\//)) {
|
||||
@@ -319,22 +485,22 @@ router.post('/backup', async (req, res) => {
|
||||
// 相对路径,基于项目根目录
|
||||
backupDir = path.join(__dirname, '../', backupPath);
|
||||
}
|
||||
|
||||
|
||||
// 确保备份目录存在
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupFile = path.join(backupDir, `backup_${timestamp}.json`);
|
||||
|
||||
|
||||
// 获取所有数据库数据
|
||||
const Device = require('../models/Device');
|
||||
const Rack = require('../models/Rack');
|
||||
const Room = require('../models/Room');
|
||||
const Consumable = require('../models/Consumable');
|
||||
const User = require('../models/User');
|
||||
|
||||
|
||||
const backupData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
@@ -344,38 +510,38 @@ router.post('/backup', async (req, res) => {
|
||||
rooms: await Room.findAll({ raw: true }),
|
||||
consumables: await Consumable.findAll({ raw: true }),
|
||||
// 不包含敏感用户信息
|
||||
users: await User.findAll({
|
||||
users: await User.findAll({
|
||||
attributes: ['userId', 'username', 'role', 'createdAt', 'updatedAt'],
|
||||
raw: true
|
||||
})
|
||||
}
|
||||
raw: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// 写入备份文件
|
||||
fs.writeFileSync(backupFile, JSON.stringify(backupData, null, 2));
|
||||
|
||||
|
||||
// 更新最后备份时间
|
||||
const lastBackupSetting = await SystemSetting.findByPk('last_backup_time');
|
||||
if (lastBackupSetting) {
|
||||
await lastBackupSetting.update({
|
||||
settingValue: JSON.stringify(new Date().toISOString())
|
||||
settingValue: JSON.stringify(new Date().toISOString()),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 统计备份文件数量
|
||||
const backupFiles = fs.readdirSync(backupDir).filter(f => f.startsWith('backup_'));
|
||||
const countSetting = await SystemSetting.findByPk('backup_count');
|
||||
if (countSetting) {
|
||||
await countSetting.update({
|
||||
settingValue: JSON.stringify(backupFiles.length)
|
||||
settingValue: JSON.stringify(backupFiles.length),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
message: '备份成功',
|
||||
backupFile: `${backupPath}/backup_${timestamp}.json`,
|
||||
fileSize: fs.statSync(backupFile).size,
|
||||
backupCount: backupFiles.length
|
||||
backupCount: backupFiles.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('备份失败:', error);
|
||||
@@ -387,7 +553,7 @@ router.post('/backup', async (req, res) => {
|
||||
const getBackupDir = async () => {
|
||||
const backupPathSetting = await SystemSetting.findByPk('backup_path');
|
||||
const backupPath = backupPathSetting ? JSON.parse(backupPathSetting.settingValue) : './backups';
|
||||
|
||||
|
||||
let backupDir;
|
||||
if (backupPath.startsWith('/') || backupPath.match(/^[A-Za-z]:\//)) {
|
||||
// 绝对路径
|
||||
@@ -396,7 +562,7 @@ const getBackupDir = async () => {
|
||||
// 相对路径,基于项目根目录
|
||||
backupDir = path.join(__dirname, '../', backupPath);
|
||||
}
|
||||
|
||||
|
||||
return backupDir;
|
||||
};
|
||||
|
||||
@@ -404,12 +570,13 @@ const getBackupDir = async () => {
|
||||
router.get('/backup/list', async (req, res) => {
|
||||
try {
|
||||
const backupDir = await getBackupDir();
|
||||
|
||||
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
return res.json({ backups: [] });
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(backupDir)
|
||||
|
||||
const files = fs
|
||||
.readdirSync(backupDir)
|
||||
.filter(f => f.startsWith('backup_') && f.endsWith('.json'))
|
||||
.map(f => {
|
||||
const filePath = path.join(backupDir, f);
|
||||
@@ -419,11 +586,11 @@ router.get('/backup/list', async (req, res) => {
|
||||
path: `${path.basename(backupDir)}/${f}`,
|
||||
size: stats.size,
|
||||
createdAt: stats.birthtime,
|
||||
modifiedAt: stats.mtime
|
||||
modifiedAt: stats.mtime,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
|
||||
|
||||
res.json({ backups: files });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -434,50 +601,50 @@ router.get('/backup/list', async (req, res) => {
|
||||
router.post('/backup/restore', async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.body;
|
||||
|
||||
|
||||
if (!filename) {
|
||||
return res.status(400).json({ error: '请提供备份文件名' });
|
||||
}
|
||||
|
||||
|
||||
const backupDir = await getBackupDir();
|
||||
const backupFile = path.join(backupDir, filename);
|
||||
|
||||
|
||||
if (!fs.existsSync(backupFile)) {
|
||||
return res.status(404).json({ error: '备份文件不存在' });
|
||||
}
|
||||
|
||||
|
||||
const backupData = JSON.parse(fs.readFileSync(backupFile, 'utf8'));
|
||||
|
||||
|
||||
// 恢复数据
|
||||
const { Device, Rack, Room, Consumable, User } = require('../models');
|
||||
|
||||
|
||||
if (backupData.data.devices) {
|
||||
for (const device of backupData.data.devices) {
|
||||
await Device.upsert(device);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (backupData.data.racks) {
|
||||
for (const rack of backupData.data.racks) {
|
||||
await Rack.upsert(rack);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (backupData.data.rooms) {
|
||||
for (const room of backupData.data.rooms) {
|
||||
await Room.upsert(room);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (backupData.data.consumables) {
|
||||
for (const consumable of backupData.data.consumables) {
|
||||
await Consumable.upsert(consumable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
message: '恢复成功',
|
||||
restoredAt: new Date().toISOString()
|
||||
restoredAt: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('恢复备份失败:', error);
|
||||
@@ -491,13 +658,13 @@ router.delete('/backup/:filename', async (req, res) => {
|
||||
const { filename } = req.params;
|
||||
const backupDir = await getBackupDir();
|
||||
const backupFile = path.join(backupDir, filename);
|
||||
|
||||
|
||||
if (!fs.existsSync(backupFile)) {
|
||||
return res.status(404).json({ error: '备份文件不存在' });
|
||||
}
|
||||
|
||||
|
||||
fs.unlinkSync(backupFile);
|
||||
|
||||
|
||||
res.json({ message: '删除成功', filename });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -510,11 +677,11 @@ router.get('/backup/download/:filename', async (req, res) => {
|
||||
const { filename } = req.params;
|
||||
const backupDir = await getBackupDir();
|
||||
const backupFile = path.join(backupDir, filename);
|
||||
|
||||
|
||||
if (!fs.existsSync(backupFile)) {
|
||||
return res.status(404).json({ error: '备份文件不存在' });
|
||||
}
|
||||
|
||||
|
||||
res.download(backupFile, filename);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -528,14 +695,14 @@ router.get('/system/info', async (req, res) => {
|
||||
const Rack = require('../models/Rack');
|
||||
const Room = require('../models/Room');
|
||||
const User = require('../models/User');
|
||||
|
||||
|
||||
const [deviceCount, rackCount, roomCount, userCount] = await Promise.all([
|
||||
Device.count(),
|
||||
Rack.count(),
|
||||
Room.count(),
|
||||
User.count()
|
||||
User.count(),
|
||||
]);
|
||||
|
||||
|
||||
res.json({
|
||||
system: {
|
||||
name: '机柜管理系统',
|
||||
@@ -545,15 +712,15 @@ router.get('/system/info', async (req, res) => {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
memoryUsage: process.memoryUsage(),
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
},
|
||||
statistics: {
|
||||
devices: deviceCount,
|
||||
racks: rackCount,
|
||||
rooms: roomCount,
|
||||
users: userCount
|
||||
users: userCount,
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -590,7 +757,7 @@ router.post('/frontend/port/sync', async (req, res) => {
|
||||
message: '前端端口配置已同步',
|
||||
port,
|
||||
configPath: '.frontend-port',
|
||||
notice: '配置已更新,请重启前端服务以应用新端口'
|
||||
notice: '配置已更新,请重启前端服务以应用新端口',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -614,8 +781,8 @@ router.post('/frontend/restart', async (req, res) => {
|
||||
after: {
|
||||
pid: result.pid,
|
||||
port: result.port,
|
||||
url: `http://localhost:${result.port}`
|
||||
}
|
||||
url: `http://localhost:${result.port}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
|
||||
@@ -14,7 +14,10 @@ router.get('/', async (req, res) => {
|
||||
|
||||
const categories = await FaultCategory.findAll({
|
||||
where,
|
||||
order: [['priority', 'ASC'], ['name', 'ASC']]
|
||||
order: [
|
||||
['priority', 'ASC'],
|
||||
['name', 'ASC'],
|
||||
],
|
||||
});
|
||||
|
||||
res.json(categories);
|
||||
@@ -31,8 +34,12 @@ router.get('/stats', async (req, res) => {
|
||||
const where = {};
|
||||
if (startDate || endDate) {
|
||||
where.createdAt = {};
|
||||
if (startDate) where.createdAt[Op.gte] = new Date(startDate);
|
||||
if (endDate) where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
|
||||
if (startDate) {
|
||||
where.createdAt[Op.gte] = new Date(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
|
||||
}
|
||||
}
|
||||
|
||||
const stats = await Ticket.findAll({
|
||||
@@ -40,16 +47,32 @@ router.get('/stats', async (req, res) => {
|
||||
attributes: [
|
||||
'faultCategory',
|
||||
[require('sequelize').fn('COUNT', '*'), 'totalCount'],
|
||||
[require('sequelize').sum(require('sequelize').case({
|
||||
when: { status: 'completed' },
|
||||
then: 1
|
||||
}, 0)), 'completedCount'],
|
||||
[require('sequelize').sum(require('sequelize').case({
|
||||
when: { status: { [Op.ne]: 'completed' } },
|
||||
then: 1
|
||||
}, 0)), 'pendingCount']
|
||||
[
|
||||
require('sequelize').sum(
|
||||
require('sequelize').case(
|
||||
{
|
||||
when: { status: 'completed' },
|
||||
then: 1,
|
||||
},
|
||||
0
|
||||
)
|
||||
),
|
||||
'completedCount',
|
||||
],
|
||||
[
|
||||
require('sequelize').sum(
|
||||
require('sequelize').case(
|
||||
{
|
||||
when: { status: { [Op.ne]: 'completed' } },
|
||||
then: 1,
|
||||
},
|
||||
0
|
||||
)
|
||||
),
|
||||
'pendingCount',
|
||||
],
|
||||
],
|
||||
group: ['faultCategory']
|
||||
group: ['faultCategory'],
|
||||
});
|
||||
|
||||
res.json(stats);
|
||||
@@ -72,15 +95,8 @@ router.get('/:categoryId', async (req, res) => {
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
priority,
|
||||
defaultPriority,
|
||||
expectedDuration,
|
||||
solutions,
|
||||
isActive
|
||||
} = req.body;
|
||||
const { name, description, priority, defaultPriority, expectedDuration, solutions, isActive } =
|
||||
req.body;
|
||||
|
||||
const existing = await FaultCategory.findOne({ where: { name } });
|
||||
if (existing) {
|
||||
@@ -98,7 +114,7 @@ router.post('/', async (req, res) => {
|
||||
expectedDuration: expectedDuration ? parseInt(expectedDuration) : null,
|
||||
solutions: solutions || [],
|
||||
isSystem: false,
|
||||
isActive: isActive !== false
|
||||
isActive: isActive !== false,
|
||||
});
|
||||
|
||||
res.status(201).json(category);
|
||||
@@ -110,16 +126,66 @@ router.post('/', async (req, res) => {
|
||||
router.post('/init', async (req, res) => {
|
||||
try {
|
||||
const defaultCategories = [
|
||||
{ name: '系统故障', description: '操作系统、应用程序等系统软件的故障问题', priority: 1, defaultPriority: 'high' },
|
||||
{ name: '硬件故障', description: '物理设备、服务器、存储等硬件设备的故障问题', priority: 2, defaultPriority: 'high' },
|
||||
{ name: '网络故障', description: '网络连接、交换机、路由器等网络相关故障', priority: 3, defaultPriority: 'high' },
|
||||
{ name: '软件故障', description: '应用程序错误、软件兼容性等问题', priority: 4, defaultPriority: 'medium' },
|
||||
{ name: '安全事件', description: '安全漏洞、入侵检测、权限异常等安全问题', priority: 5, defaultPriority: 'urgent' },
|
||||
{ name: '性能问题', description: '系统响应慢、资源利用率高等性能问题', priority: 6, defaultPriority: 'medium' },
|
||||
{ name: '配置变更', description: '系统配置、软件配置等变更需求', priority: 7, defaultPriority: 'low' },
|
||||
{ name: '例行维护', description: '定期维护、巡检、更新等计划性工作', priority: 8, defaultPriority: 'low' },
|
||||
{ name: '数据问题', description: '数据错误、数据丢失、数据同步等数据相关问题', priority: 9, defaultPriority: 'high' },
|
||||
{ name: '其他问题', description: '无法归类的其他问题', priority: 99, defaultPriority: 'medium' }
|
||||
{
|
||||
name: '系统故障',
|
||||
description: '操作系统、应用程序等系统软件的故障问题',
|
||||
priority: 1,
|
||||
defaultPriority: 'high',
|
||||
},
|
||||
{
|
||||
name: '硬件故障',
|
||||
description: '物理设备、服务器、存储等硬件设备的故障问题',
|
||||
priority: 2,
|
||||
defaultPriority: 'high',
|
||||
},
|
||||
{
|
||||
name: '网络故障',
|
||||
description: '网络连接、交换机、路由器等网络相关故障',
|
||||
priority: 3,
|
||||
defaultPriority: 'high',
|
||||
},
|
||||
{
|
||||
name: '软件故障',
|
||||
description: '应用程序错误、软件兼容性等问题',
|
||||
priority: 4,
|
||||
defaultPriority: 'medium',
|
||||
},
|
||||
{
|
||||
name: '安全事件',
|
||||
description: '安全漏洞、入侵检测、权限异常等安全问题',
|
||||
priority: 5,
|
||||
defaultPriority: 'urgent',
|
||||
},
|
||||
{
|
||||
name: '性能问题',
|
||||
description: '系统响应慢、资源利用率高等性能问题',
|
||||
priority: 6,
|
||||
defaultPriority: 'medium',
|
||||
},
|
||||
{
|
||||
name: '配置变更',
|
||||
description: '系统配置、软件配置等变更需求',
|
||||
priority: 7,
|
||||
defaultPriority: 'low',
|
||||
},
|
||||
{
|
||||
name: '例行维护',
|
||||
description: '定期维护、巡检、更新等计划性工作',
|
||||
priority: 8,
|
||||
defaultPriority: 'low',
|
||||
},
|
||||
{
|
||||
name: '数据问题',
|
||||
description: '数据错误、数据丢失、数据同步等数据相关问题',
|
||||
priority: 9,
|
||||
defaultPriority: 'high',
|
||||
},
|
||||
{
|
||||
name: '其他问题',
|
||||
description: '无法归类的其他问题',
|
||||
priority: 99,
|
||||
defaultPriority: 'medium',
|
||||
},
|
||||
];
|
||||
|
||||
for (const cat of defaultCategories) {
|
||||
@@ -132,7 +198,7 @@ router.post('/init', async (req, res) => {
|
||||
expectedDuration: 120,
|
||||
solutions: [],
|
||||
isSystem: true,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const TicketField = require('../models/TicketField');
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const fields = await TicketField.findAll({
|
||||
order: [['order', 'ASC']]
|
||||
order: [['order', 'ASC']],
|
||||
});
|
||||
res.json(fields);
|
||||
} catch (error) {
|
||||
@@ -37,7 +37,7 @@ router.post('/', async (req, res) => {
|
||||
router.put('/:fieldId', async (req, res) => {
|
||||
try {
|
||||
const [updated] = await TicketField.update(req.body, {
|
||||
where: { fieldId: req.params.fieldId }
|
||||
where: { fieldId: req.params.fieldId },
|
||||
});
|
||||
if (updated) {
|
||||
const updatedField = await TicketField.findByPk(req.params.fieldId);
|
||||
@@ -53,7 +53,7 @@ router.put('/:fieldId', async (req, res) => {
|
||||
router.delete('/:fieldId', async (req, res) => {
|
||||
try {
|
||||
const deleted = await TicketField.destroy({
|
||||
where: { fieldId: req.params.fieldId }
|
||||
where: { fieldId: req.params.fieldId },
|
||||
});
|
||||
if (deleted) {
|
||||
res.status(204).json();
|
||||
@@ -68,24 +68,24 @@ router.delete('/:fieldId', async (req, res) => {
|
||||
router.post('/config', async (req, res) => {
|
||||
try {
|
||||
const fieldConfigs = req.body;
|
||||
|
||||
|
||||
if (!Array.isArray(fieldConfigs)) {
|
||||
return res.status(400).json({ error: '输入必须是数组' });
|
||||
}
|
||||
|
||||
|
||||
const updatedFields = [];
|
||||
for (const config of fieldConfigs) {
|
||||
const [updated] = await TicketField.update(
|
||||
{ visible: config.visible },
|
||||
{ where: { fieldName: config.fieldName } }
|
||||
);
|
||||
|
||||
|
||||
if (updated) {
|
||||
const updatedField = await TicketField.findOne({ where: { fieldName: config.fieldName } });
|
||||
updatedFields.push(updatedField);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
res.json(updatedFields);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
|
||||
+90
-62
@@ -17,41 +17,56 @@ router.get('/stats', async (req, res) => {
|
||||
const where = {};
|
||||
if (startDate || endDate) {
|
||||
where.createdAt = {};
|
||||
if (startDate) where.createdAt[Op.gte] = new Date(startDate);
|
||||
if (endDate) where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
|
||||
if (startDate) {
|
||||
where.createdAt[Op.gte] = new Date(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
|
||||
}
|
||||
}
|
||||
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
const [total, statusStats, priorityStats, categoryStats, monthlyStats, deviceStats, dailyCreatedStats, dailyCompletedStats] = await Promise.all([
|
||||
const [
|
||||
total,
|
||||
statusStats,
|
||||
priorityStats,
|
||||
categoryStats,
|
||||
monthlyStats,
|
||||
deviceStats,
|
||||
dailyCreatedStats,
|
||||
dailyCompletedStats,
|
||||
] = await Promise.all([
|
||||
Ticket.count({ where }),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: ['status', [Sequelize.fn('COUNT', '*'), 'count']],
|
||||
group: ['status']
|
||||
group: ['status'],
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: ['priority', [Sequelize.fn('COUNT', '*'), 'count']],
|
||||
group: ['priority']
|
||||
group: ['priority'],
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: ['faultCategory', [Sequelize.fn('COUNT', '*'), 'count']],
|
||||
group: ['faultCategory']
|
||||
group: ['faultCategory'],
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
[dbDialect === 'mysql'
|
||||
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m')
|
||||
: Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')),
|
||||
'month'],
|
||||
[Sequelize.fn('COUNT', '*'), 'count']
|
||||
[
|
||||
dbDialect === 'mysql'
|
||||
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m')
|
||||
: Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')),
|
||||
'month',
|
||||
],
|
||||
[Sequelize.fn('COUNT', '*'), 'count'],
|
||||
],
|
||||
group: ['month'],
|
||||
order: [['month', 'DESC']],
|
||||
limit: 12
|
||||
limit: 12,
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
@@ -59,39 +74,43 @@ router.get('/stats', async (req, res) => {
|
||||
'deviceId',
|
||||
'deviceName',
|
||||
[Sequelize.fn('COUNT', '*'), 'count'],
|
||||
[Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime']
|
||||
[Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime'],
|
||||
],
|
||||
group: ['deviceId', 'deviceName'],
|
||||
order: [[Sequelize.fn('COUNT', '*'), 'DESC']],
|
||||
limit: 10
|
||||
limit: 10,
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
[dbDialect === 'mysql'
|
||||
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m-%d')
|
||||
: Sequelize.fn('date', Sequelize.col('createdAt')),
|
||||
'date'],
|
||||
[Sequelize.fn('COUNT', '*'), 'created']
|
||||
[
|
||||
dbDialect === 'mysql'
|
||||
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m-%d')
|
||||
: Sequelize.fn('date', Sequelize.col('createdAt')),
|
||||
'date',
|
||||
],
|
||||
[Sequelize.fn('COUNT', '*'), 'created'],
|
||||
],
|
||||
group: ['date'],
|
||||
order: [['date', 'ASC']]
|
||||
order: [['date', 'ASC']],
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where: {
|
||||
...where,
|
||||
status: 'completed'
|
||||
status: 'completed',
|
||||
},
|
||||
attributes: [
|
||||
[dbDialect === 'mysql'
|
||||
? Sequelize.fn('DATE_FORMAT', Sequelize.col('updatedAt'), '%Y-%m-%d')
|
||||
: Sequelize.fn('date', Sequelize.col('updatedAt')),
|
||||
'date'],
|
||||
[Sequelize.fn('COUNT', '*'), 'completed']
|
||||
[
|
||||
dbDialect === 'mysql'
|
||||
? Sequelize.fn('DATE_FORMAT', Sequelize.col('updatedAt'), '%Y-%m-%d')
|
||||
: Sequelize.fn('date', Sequelize.col('updatedAt')),
|
||||
'date',
|
||||
],
|
||||
[Sequelize.fn('COUNT', '*'), 'completed'],
|
||||
],
|
||||
group: ['date'],
|
||||
order: [['date', 'ASC']]
|
||||
})
|
||||
order: [['date', 'ASC']],
|
||||
}),
|
||||
]);
|
||||
|
||||
const statusData = statusStats.map(s => s.dataValues);
|
||||
@@ -103,21 +122,21 @@ router.get('/stats', async (req, res) => {
|
||||
const byStatus = statusData.map(item => ({
|
||||
status: item.status,
|
||||
count: item.count,
|
||||
percentage: total > 0 ? (item.count / total * 100) : 0
|
||||
percentage: total > 0 ? (item.count / total) * 100 : 0,
|
||||
}));
|
||||
|
||||
const byPriority = priorityStats.map(p => ({
|
||||
priority: p.dataValues.priority,
|
||||
count: p.dataValues.count,
|
||||
completed: 0,
|
||||
avgTime: 0
|
||||
avgTime: 0,
|
||||
}));
|
||||
|
||||
const byCategory = categoryStats.map(c => ({
|
||||
category: c.dataValues.faultCategory,
|
||||
count: c.dataValues.count,
|
||||
completed: 0,
|
||||
avgTime: 0
|
||||
avgTime: 0,
|
||||
}));
|
||||
|
||||
const byDevice = deviceStats.map(d => ({
|
||||
@@ -125,7 +144,7 @@ router.get('/stats', async (req, res) => {
|
||||
deviceName: d.deviceName,
|
||||
count: d.dataValues.count,
|
||||
lastFaultTime: d.dataValues.lastFaultTime,
|
||||
deviceType: ''
|
||||
deviceType: '',
|
||||
}));
|
||||
|
||||
const createdMap = {};
|
||||
@@ -137,22 +156,24 @@ router.get('/stats', async (req, res) => {
|
||||
completedMap[d.dataValues.date] = d.dataValues.completed;
|
||||
});
|
||||
|
||||
const allDates = [...new Set([...Object.keys(createdMap), ...Object.keys(completedMap)])].sort();
|
||||
const allDates = [
|
||||
...new Set([...Object.keys(createdMap), ...Object.keys(completedMap)]),
|
||||
].sort();
|
||||
const trend = allDates.map(date => ({
|
||||
date,
|
||||
created: createdMap[date] || 0,
|
||||
completed: completedMap[date] || 0,
|
||||
closed: 0,
|
||||
inProgress: 0,
|
||||
pending: 0
|
||||
pending: 0,
|
||||
}));
|
||||
|
||||
const completedTickets = await Ticket.findAll({
|
||||
where: {
|
||||
...where,
|
||||
status: 'completed'
|
||||
status: 'completed',
|
||||
},
|
||||
attributes: ['createdAt', 'updatedAt']
|
||||
attributes: ['createdAt', 'updatedAt'],
|
||||
});
|
||||
|
||||
let avgProcessingTime = 0;
|
||||
@@ -162,7 +183,11 @@ router.get('/stats', async (req, res) => {
|
||||
const updated = new Date(ticket.updatedAt);
|
||||
return sum + (updated - created);
|
||||
}, 0);
|
||||
avgProcessingTime = (totalProcessingTime / completedTickets.length / (1000 * 60 * 60)).toFixed(1);
|
||||
avgProcessingTime = (
|
||||
totalProcessingTime /
|
||||
completedTickets.length /
|
||||
(1000 * 60 * 60)
|
||||
).toFixed(1);
|
||||
}
|
||||
|
||||
res.json({
|
||||
@@ -177,7 +202,10 @@ router.get('/stats', async (req, res) => {
|
||||
byCategory,
|
||||
byDevice,
|
||||
trend,
|
||||
monthlyStats: monthlyStats.map(m => ({ month: m.dataValues.month, count: m.dataValues.count }))
|
||||
monthlyStats: monthlyStats.map(m => ({
|
||||
month: m.dataValues.month,
|
||||
count: m.dataValues.count,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -203,7 +231,7 @@ router.get('/', async (req, res) => {
|
||||
startDate,
|
||||
endDate,
|
||||
page = 1,
|
||||
pageSize = 10
|
||||
pageSize = 10,
|
||||
} = req.query;
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
@@ -216,7 +244,7 @@ router.get('/', async (req, res) => {
|
||||
{ title: { [Op.like]: `%${keyword}%` } },
|
||||
{ deviceName: { [Op.like]: `%${keyword}%` } },
|
||||
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
||||
{ description: { [Op.like]: `%${keyword}%` } }
|
||||
{ description: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -260,18 +288,18 @@ router.get('/', async (req, res) => {
|
||||
where,
|
||||
include: [
|
||||
{ model: User, as: 'reporter', attributes: ['userId', 'username'] },
|
||||
{ model: Device, attributes: ['deviceId', 'name', 'type', 'model'] }
|
||||
{ model: Device, attributes: ['deviceId', 'name', 'type', 'model'] },
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
offset,
|
||||
limit: parseInt(pageSize)
|
||||
limit: parseInt(pageSize),
|
||||
});
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
tickets: rows,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -293,14 +321,14 @@ router.get('/:ticketId', async (req, res) => {
|
||||
include: [
|
||||
{
|
||||
model: Room,
|
||||
attributes: ['roomId', 'name']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
attributes: ['roomId', 'name'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ model: TicketOperationRecord, as: 'operationRecords', order: [['createdAt', 'DESC']] }
|
||||
]
|
||||
{ model: TicketOperationRecord, as: 'operationRecords', order: [['createdAt', 'DESC']] },
|
||||
],
|
||||
});
|
||||
|
||||
if (!ticket) {
|
||||
@@ -334,7 +362,7 @@ router.post('/', async (req, res) => {
|
||||
expectedCompletionDate,
|
||||
title,
|
||||
attachments,
|
||||
tags
|
||||
tags,
|
||||
} = req.body;
|
||||
|
||||
let device = null;
|
||||
@@ -347,7 +375,7 @@ router.post('/', async (req, res) => {
|
||||
if (deviceId) {
|
||||
// 从设备列表选择
|
||||
device = await Device.findByPk(deviceId, {
|
||||
include: [{ model: require('../models/Rack') }]
|
||||
include: [{ model: require('../models/Rack') }],
|
||||
});
|
||||
if (!device) {
|
||||
return res.status(404).json({ error: '设备不存在' });
|
||||
@@ -385,7 +413,7 @@ router.post('/', async (req, res) => {
|
||||
location: ticketLocation,
|
||||
attachments: attachments || [],
|
||||
tags: tags || [],
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
// 创建操作记录
|
||||
@@ -397,7 +425,7 @@ router.post('/', async (req, res) => {
|
||||
operatorId: ticket.reporterId,
|
||||
operatorName: ticket.reporterName,
|
||||
operatorRole: 'user',
|
||||
afterState: ticket.toJSON()
|
||||
afterState: ticket.toJSON(),
|
||||
});
|
||||
|
||||
res.status(201).json(ticket);
|
||||
@@ -428,7 +456,7 @@ router.put('/:ticketId', async (req, res) => {
|
||||
operatorName: operatorName || ticket.reporterName,
|
||||
operatorRole: operatorRole || 'user',
|
||||
beforeState,
|
||||
afterState: ticket.toJSON()
|
||||
afterState: ticket.toJSON(),
|
||||
});
|
||||
|
||||
res.json(ticket);
|
||||
@@ -468,7 +496,7 @@ router.put('/:ticketId/status', async (req, res) => {
|
||||
operatorName: operatorName || ticket.reporterName,
|
||||
operatorRole: operatorRole || 'user',
|
||||
beforeState,
|
||||
afterState: ticket.toJSON()
|
||||
afterState: ticket.toJSON(),
|
||||
});
|
||||
|
||||
res.json(ticket);
|
||||
@@ -490,7 +518,7 @@ router.put('/:ticketId/process', async (req, res) => {
|
||||
const beforeState = ticket.toJSON();
|
||||
const updateData = {
|
||||
status: 'in_progress',
|
||||
resolution: solution
|
||||
resolution: solution,
|
||||
};
|
||||
|
||||
if (result === 'resolved') {
|
||||
@@ -513,7 +541,7 @@ router.put('/:ticketId/process', async (req, res) => {
|
||||
operatorName: operatorName || ticket.reporterName,
|
||||
operatorRole: operatorRole || 'user',
|
||||
beforeState,
|
||||
afterState: ticket.toJSON()
|
||||
afterState: ticket.toJSON(),
|
||||
});
|
||||
|
||||
res.json(ticket);
|
||||
@@ -535,7 +563,7 @@ router.post('/:ticketId/operations', async (req, res) => {
|
||||
notes,
|
||||
operatorId,
|
||||
operatorName,
|
||||
operatorRole
|
||||
operatorRole,
|
||||
} = req.body;
|
||||
|
||||
const ticket = await Ticket.findByPk(req.params.ticketId);
|
||||
@@ -555,7 +583,7 @@ router.post('/:ticketId/operations', async (req, res) => {
|
||||
notes,
|
||||
operatorId,
|
||||
operatorName,
|
||||
operatorRole
|
||||
operatorRole,
|
||||
});
|
||||
|
||||
res.status(201).json(record);
|
||||
@@ -569,7 +597,7 @@ router.get('/:ticketId/operations', async (req, res) => {
|
||||
try {
|
||||
const records = await TicketOperationRecord.findAll({
|
||||
where: { ticketId: req.params.ticketId },
|
||||
order: [['createdAt', 'DESC']]
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
res.json(records);
|
||||
@@ -613,7 +641,7 @@ router.post('/:ticketId/evaluate', async (req, res) => {
|
||||
operatorId,
|
||||
operatorName,
|
||||
operatorRole: 'user',
|
||||
notes: `评价: ${evaluation}, 星级: ${evaluationRating}`
|
||||
notes: `评价: ${evaluation}, 星级: ${evaluationRating}`,
|
||||
});
|
||||
|
||||
res.json(ticket);
|
||||
|
||||
+148
-111
@@ -15,7 +15,7 @@ const generateId = () => {
|
||||
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||
};
|
||||
|
||||
const getWhereClause = (query) => {
|
||||
const getWhereClause = query => {
|
||||
const where = {};
|
||||
|
||||
if (query.username) {
|
||||
@@ -33,10 +33,10 @@ const getWhereClause = (query) => {
|
||||
return where;
|
||||
};
|
||||
|
||||
const getUserRoleIds = async (userId) => {
|
||||
const getUserRoleIds = async userId => {
|
||||
const userRoles = await UserRole.findAll({
|
||||
where: { UserId: userId },
|
||||
attributes: ['RoleId']
|
||||
attributes: ['RoleId'],
|
||||
});
|
||||
return userRoles.map(ur => ur.RoleId);
|
||||
};
|
||||
@@ -45,7 +45,13 @@ const { Op } = require('sequelize');
|
||||
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE, username, status, realName } = req.query;
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = PAGINATION.DEFAULT_PAGE_SIZE,
|
||||
username,
|
||||
status,
|
||||
realName,
|
||||
} = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = Math.min(parseInt(pageSize), PAGINATION.MAX_PAGE_SIZE);
|
||||
|
||||
@@ -56,20 +62,22 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
attributes: { exclude: ['password'] },
|
||||
limit,
|
||||
offset,
|
||||
order: [['createdAt', 'DESC']]
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
if (users.length > 0) {
|
||||
const userIds = users.map(u => u.userId);
|
||||
const allUserRoles = await UserRole.findAll({
|
||||
include: [{
|
||||
model: Role,
|
||||
where: { status: 'active' },
|
||||
attributes: ['roleId', 'roleName', 'roleCode']
|
||||
}],
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
where: { status: 'active' },
|
||||
attributes: ['roleId', 'roleName', 'roleCode'],
|
||||
},
|
||||
],
|
||||
where: {
|
||||
UserId: { [Op.in]: userIds }
|
||||
}
|
||||
UserId: { [Op.in]: userIds },
|
||||
},
|
||||
});
|
||||
|
||||
const userRolesMap = {};
|
||||
@@ -80,7 +88,7 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
userRolesMap[ur.UserId].push({
|
||||
roleId: ur.Role.roleId,
|
||||
roleName: ur.Role.roleName,
|
||||
roleCode: ur.Role.roleCode
|
||||
roleCode: ur.Role.roleCode,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,14 +107,14 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
users
|
||||
}
|
||||
users,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户列表错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户列表失败'
|
||||
message: '获取用户列表失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -116,18 +124,18 @@ router.get('/all', authMiddleware, async (req, res) => {
|
||||
const users = await User.findAll({
|
||||
where: { status: 'active' },
|
||||
attributes: ['userId', 'username', 'realName', 'email'],
|
||||
order: [['realName', 'ASC']]
|
||||
order: [['realName', 'ASC']],
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: users
|
||||
data: users,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取所有用户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户列表失败'
|
||||
message: '获取用户列表失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -135,39 +143,41 @@ router.get('/all', authMiddleware, async (req, res) => {
|
||||
router.get('/:userId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.params.userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
attributes: { exclude: ['password'] },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
const userRoles = await UserRole.findAll({
|
||||
include: [{
|
||||
model: Role,
|
||||
where: { status: 'active' }
|
||||
}],
|
||||
where: { UserId: user.userId }
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
where: { status: 'active' },
|
||||
},
|
||||
],
|
||||
where: { UserId: user.userId },
|
||||
});
|
||||
|
||||
user.dataValues.roles = userRoles.map(ur => ({
|
||||
roleId: ur.Role.roleId,
|
||||
roleName: ur.Role.roleName,
|
||||
roleCode: ur.Role.roleCode
|
||||
roleCode: ur.Role.roleCode,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
data: user,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户详情错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户详情失败'
|
||||
message: '获取用户详情失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -179,7 +189,7 @@ router.post('/', authMiddleware, async (req, res) => {
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名和密码不能为空'
|
||||
message: '用户名和密码不能为空',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,7 +197,7 @@ router.post('/', authMiddleware, async (req, res) => {
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名已存在'
|
||||
message: '用户名已存在',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -201,34 +211,41 @@ router.post('/', authMiddleware, async (req, res) => {
|
||||
phone,
|
||||
realName: realName || username,
|
||||
status: status || 'active',
|
||||
remark
|
||||
remark,
|
||||
});
|
||||
|
||||
if (roleIds && roleIds.length > 0) {
|
||||
for (const roleId of roleIds) {
|
||||
await UserRole.create({
|
||||
UserId: user.userId,
|
||||
RoleId: roleId
|
||||
RoleId: roleId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const roleNames = roleIds && roleIds.length > 0
|
||||
? (await Role.findAll({ where: { roleId: { [Op.in]: roleIds } } })).map(r => r.roleName).join('、')
|
||||
: '未分配角色';
|
||||
const roleNames =
|
||||
roleIds && roleIds.length > 0
|
||||
? (await Role.findAll({ where: { roleId: { [Op.in]: roleIds } } }))
|
||||
.map(r => r.roleName)
|
||||
.join('、')
|
||||
: '未分配角色';
|
||||
|
||||
await logUserOperation('create', `创建用户【${username}】(姓名:${realName || '未填写'},邮箱:${email || '未填写'},角色:${roleNames})`, {
|
||||
targetId: user.userId,
|
||||
targetName: username,
|
||||
afterState: {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName,
|
||||
status: user.status
|
||||
},
|
||||
req,
|
||||
metadata: { roleIds, roleNames }
|
||||
});
|
||||
await logUserOperation(
|
||||
'create',
|
||||
`创建用户【${username}】(姓名:${realName || '未填写'},邮箱:${email || '未填写'},角色:${roleNames})`,
|
||||
{
|
||||
targetId: user.userId,
|
||||
targetName: username,
|
||||
afterState: {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName,
|
||||
status: user.status,
|
||||
},
|
||||
req,
|
||||
metadata: { roleIds, roleNames },
|
||||
}
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
@@ -238,14 +255,14 @@ router.post('/', authMiddleware, async (req, res) => {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName,
|
||||
status: user.status
|
||||
}
|
||||
status: user.status,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建用户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建用户失败'
|
||||
message: '创建用户失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -258,7 +275,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -268,29 +285,39 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
phone: user.phone,
|
||||
realName: user.realName,
|
||||
status: user.status,
|
||||
remark: user.remark
|
||||
remark: user.remark,
|
||||
};
|
||||
|
||||
const oldRoleIds = roleIds !== undefined ? null : await getUserRoleIds(user.userId);
|
||||
|
||||
if (username !== undefined && username !== user.username) {
|
||||
const existingUser = await User.findOne({
|
||||
where: { username, userId: { [Op.ne]: user.userId } }
|
||||
where: { username, userId: { [Op.ne]: user.userId } },
|
||||
});
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名已存在'
|
||||
message: '用户名已存在',
|
||||
});
|
||||
}
|
||||
user.username = username;
|
||||
}
|
||||
|
||||
if (email !== undefined) user.email = email;
|
||||
if (phone !== undefined) user.phone = phone;
|
||||
if (realName !== undefined) user.realName = realName;
|
||||
if (status !== undefined) user.status = status;
|
||||
if (remark !== undefined) user.remark = remark;
|
||||
if (email !== undefined) {
|
||||
user.email = email;
|
||||
}
|
||||
if (phone !== undefined) {
|
||||
user.phone = phone;
|
||||
}
|
||||
if (realName !== undefined) {
|
||||
user.realName = realName;
|
||||
}
|
||||
if (status !== undefined) {
|
||||
user.status = status;
|
||||
}
|
||||
if (remark !== undefined) {
|
||||
user.remark = remark;
|
||||
}
|
||||
|
||||
if (newPassword && newPassword.length >= PASSWORD_MIN_LENGTH) {
|
||||
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
||||
@@ -311,7 +338,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
for (const roleId of roleIds) {
|
||||
await UserRole.create({
|
||||
UserId: user.userId,
|
||||
RoleId: roleId
|
||||
RoleId: roleId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,7 +348,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
}
|
||||
|
||||
const updatedUser = await User.findByPk(req.params.userId, {
|
||||
attributes: { exclude: ['password'] }
|
||||
attributes: { exclude: ['password'] },
|
||||
});
|
||||
|
||||
if (permissionChanged) {
|
||||
@@ -332,7 +359,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
beforeState: { ...beforeState, roleIds: oldRoleIds, roleNames: oldRoleNames },
|
||||
afterState: { ...beforeState, roleIds, roleNames: newRoleNames },
|
||||
req,
|
||||
metadata: { oldRoleIds, newRoleIds: roleIds, oldRoleNames, newRoleNames }
|
||||
metadata: { oldRoleIds, newRoleIds: roleIds, oldRoleNames, newRoleNames },
|
||||
});
|
||||
} else {
|
||||
const afterState = {
|
||||
@@ -341,7 +368,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
phone: updatedUser.phone,
|
||||
realName: updatedUser.realName,
|
||||
status: updatedUser.status,
|
||||
remark: updatedUser.remark
|
||||
remark: updatedUser.remark,
|
||||
};
|
||||
|
||||
const changedFields = {};
|
||||
@@ -351,14 +378,20 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
const changeDetails = Object.entries(changedFields).map(([field, values]) => {
|
||||
const fieldNames = {
|
||||
username: '用户名', email: '邮箱', phone: '电话', realName: '姓名',
|
||||
status: '状态', remark: '备注'
|
||||
};
|
||||
const displayName = fieldNames[field] || field;
|
||||
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
||||
}).join(';');
|
||||
const changeDetails = Object.entries(changedFields)
|
||||
.map(([field, values]) => {
|
||||
const fieldNames = {
|
||||
username: '用户名',
|
||||
email: '邮箱',
|
||||
phone: '电话',
|
||||
realName: '姓名',
|
||||
status: '状态',
|
||||
remark: '备注',
|
||||
};
|
||||
const displayName = fieldNames[field] || field;
|
||||
return `${displayName}: ${values.from ?? '空'} → ${values.to ?? '空'}`;
|
||||
})
|
||||
.join(';');
|
||||
|
||||
const updateDesc = changeDetails
|
||||
? `更新用户【${updatedUser.username}】:${changeDetails}`
|
||||
@@ -370,20 +403,20 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
||||
beforeState,
|
||||
afterState,
|
||||
req,
|
||||
metadata: { changedFields }
|
||||
metadata: { changedFields },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '更新成功',
|
||||
data: updatedUser
|
||||
data: updatedUser,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新用户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新用户失败'
|
||||
message: '更新用户失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -396,14 +429,14 @@ router.put('/:userId/password', authMiddleware, async (req, res) => {
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
if (!newPassword || newPassword.length < PASSWORD_MIN_LENGTH) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
||||
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -412,13 +445,13 @@ router.put('/:userId/password', authMiddleware, async (req, res) => {
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '密码重置成功'
|
||||
message: '密码重置成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('重置密码错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '重置密码失败'
|
||||
message: '重置密码失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -430,14 +463,14 @@ router.delete('/:userId', authMiddleware, async (req, res) => {
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
if (user.userId === req.user.userId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '不能删除当前登录用户'
|
||||
message: '不能删除当前登录用户',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -448,29 +481,33 @@ router.delete('/:userId', authMiddleware, async (req, res) => {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
realName: user.realName,
|
||||
status: user.status
|
||||
status: user.status,
|
||||
};
|
||||
|
||||
await UserRole.destroy({ where: { UserId: user.userId } });
|
||||
await user.destroy();
|
||||
|
||||
await logUserOperation('delete', `删除用户【${userName}】(姓名:${userRealName || '未填写'},邮箱:${userEmail || '未填写'})`, {
|
||||
targetId: req.params.userId,
|
||||
targetName: userName,
|
||||
beforeState,
|
||||
req,
|
||||
metadata: { deletedUsername: userName, realName: userRealName, email: userEmail }
|
||||
});
|
||||
await logUserOperation(
|
||||
'delete',
|
||||
`删除用户【${userName}】(姓名:${userRealName || '未填写'},邮箱:${userEmail || '未填写'})`,
|
||||
{
|
||||
targetId: req.params.userId,
|
||||
targetName: userName,
|
||||
beforeState,
|
||||
req,
|
||||
metadata: { deletedUsername: userName, realName: userRealName, email: userEmail },
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
message: '删除成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除用户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除用户失败'
|
||||
message: '删除用户失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -482,31 +519,31 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => {
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.files || !req.files.avatar) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要上传的头像文件'
|
||||
message: '请选择要上传的头像文件',
|
||||
});
|
||||
}
|
||||
|
||||
const avatarFile = req.files.avatar;
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
|
||||
|
||||
if (!allowedTypes.includes(avatarFile.mimetype)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '只支持 JPG、PNG、GIF 和 WebP 格式的图片'
|
||||
message: '只支持 JPG、PNG、GIF 和 WebP 格式的图片',
|
||||
});
|
||||
}
|
||||
|
||||
if (avatarFile.size > FILE_UPLOAD.MAX_AVATAR_SIZE) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `图片大小不能超过 ${FILE_UPLOAD.MAX_AVATAR_SIZE / 1024 / 1024}MB`
|
||||
message: `图片大小不能超过 ${FILE_UPLOAD.MAX_AVATAR_SIZE / 1024 / 1024}MB`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -535,13 +572,13 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '头像上传成功',
|
||||
data: { avatar: avatarUrl }
|
||||
data: { avatar: avatarUrl },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('上传头像错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '上传头像失败'
|
||||
message: '上传头像失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -553,7 +590,7 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -569,13 +606,13 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '头像删除成功'
|
||||
message: '头像删除成功',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除头像错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除头像失败'
|
||||
message: '删除头像失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -587,14 +624,14 @@ router.put('/:userId/approve', authMiddleware, async (req, res) => {
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
if (user.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '该用户不在待审核状态'
|
||||
message: '该用户不在待审核状态',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -607,14 +644,14 @@ router.put('/:userId/approve', authMiddleware, async (req, res) => {
|
||||
data: {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
status: user.status
|
||||
}
|
||||
status: user.status,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('审核用户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '审核用户失败'
|
||||
message: '审核用户失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -626,14 +663,14 @@ router.put('/:userId/reject', authMiddleware, async (req, res) => {
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
if (user.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '该用户不在待审核状态'
|
||||
message: '该用户不在待审核状态',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -646,14 +683,14 @@ router.put('/:userId/reject', authMiddleware, async (req, res) => {
|
||||
data: {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
status: user.status
|
||||
}
|
||||
status: user.status,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('拒绝用户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '操作失败'
|
||||
message: '操作失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,8 +8,8 @@ const { logDeviceOperation } = require('../utils/operationLogger');
|
||||
async function generateWarehouseId() {
|
||||
const warehouses = await Warehouse.findAll({
|
||||
where: {
|
||||
warehouseId: { [Op.like]: 'WH%' }
|
||||
}
|
||||
warehouseId: { [Op.like]: 'WH%' },
|
||||
},
|
||||
});
|
||||
|
||||
let maxNumber = 0;
|
||||
@@ -38,7 +38,7 @@ router.get('/', async (req, res) => {
|
||||
where[Op.or] = [
|
||||
{ warehouseId: { [Op.like]: `%${keyword}%` } },
|
||||
{ name: { [Op.like]: `%${keyword}%` } },
|
||||
{ location: { [Op.like]: `%${keyword}%` } }
|
||||
{ location: { [Op.like]: `%${keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -50,17 +50,17 @@ router.get('/', async (req, res) => {
|
||||
where,
|
||||
offset: parseInt(offset),
|
||||
limit: parseInt(pageSize),
|
||||
order: [['createdAt', 'DESC']]
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
const warehousesWithCount = await Promise.all(
|
||||
rows.map(async (warehouse) => {
|
||||
rows.map(async warehouse => {
|
||||
const deviceCount = await Device.count({
|
||||
where: { warehouseId: warehouse.warehouseId, isIdle: true }
|
||||
where: { warehouseId: warehouse.warehouseId, isIdle: true },
|
||||
});
|
||||
return {
|
||||
...warehouse.toJSON(),
|
||||
deviceCount
|
||||
deviceCount,
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -69,7 +69,7 @@ router.get('/', async (req, res) => {
|
||||
total: count,
|
||||
warehouses: warehousesWithCount,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取库房列表失败:', error);
|
||||
@@ -85,12 +85,12 @@ router.get('/:warehouseId', async (req, res) => {
|
||||
}
|
||||
|
||||
const deviceCount = await Device.count({
|
||||
where: { warehouseId: warehouse.warehouseId, isIdle: true }
|
||||
where: { warehouseId: warehouse.warehouseId, isIdle: true },
|
||||
});
|
||||
|
||||
res.json({
|
||||
...warehouse.toJSON(),
|
||||
deviceCount
|
||||
deviceCount,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -111,14 +111,14 @@ router.get('/:warehouseId/devices', async (req, res) => {
|
||||
where: { warehouseId: req.params.warehouseId, isIdle: true },
|
||||
offset: parseInt(offset),
|
||||
limit: parseInt(pageSize),
|
||||
order: [['idleDate', 'DESC']]
|
||||
order: [['idleDate', 'DESC']],
|
||||
});
|
||||
|
||||
res.json({
|
||||
total: count,
|
||||
devices: rows,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取库房设备失败:', error);
|
||||
@@ -142,7 +142,7 @@ router.post('/', async (req, res) => {
|
||||
location: location || '',
|
||||
capacity: capacity || 100,
|
||||
status: 'active',
|
||||
description: description || ''
|
||||
description: description || '',
|
||||
});
|
||||
|
||||
await logDeviceOperation('create', `创建库房【${name}】`, {
|
||||
@@ -150,7 +150,7 @@ router.post('/', async (req, res) => {
|
||||
targetName: name,
|
||||
afterState: warehouse.toJSON(),
|
||||
req,
|
||||
metadata: { type: 'warehouse_create' }
|
||||
metadata: { type: 'warehouse_create' },
|
||||
});
|
||||
|
||||
res.status(201).json(warehouse);
|
||||
@@ -169,11 +169,21 @@ router.put('/:warehouseId', async (req, res) => {
|
||||
const beforeState = warehouse.toJSON();
|
||||
const { name, location, capacity, status, description } = req.body;
|
||||
|
||||
if (name) warehouse.name = name;
|
||||
if (location !== undefined) warehouse.location = location;
|
||||
if (capacity !== undefined) warehouse.capacity = capacity;
|
||||
if (status) warehouse.status = status;
|
||||
if (description !== undefined) warehouse.description = description;
|
||||
if (name) {
|
||||
warehouse.name = name;
|
||||
}
|
||||
if (location !== undefined) {
|
||||
warehouse.location = location;
|
||||
}
|
||||
if (capacity !== undefined) {
|
||||
warehouse.capacity = capacity;
|
||||
}
|
||||
if (status) {
|
||||
warehouse.status = status;
|
||||
}
|
||||
if (description !== undefined) {
|
||||
warehouse.description = description;
|
||||
}
|
||||
|
||||
await warehouse.save();
|
||||
|
||||
@@ -183,7 +193,7 @@ router.put('/:warehouseId', async (req, res) => {
|
||||
beforeState,
|
||||
afterState: warehouse.toJSON(),
|
||||
req,
|
||||
metadata: { type: 'warehouse_update' }
|
||||
metadata: { type: 'warehouse_update' },
|
||||
});
|
||||
|
||||
res.json(warehouse);
|
||||
@@ -200,12 +210,12 @@ router.delete('/:warehouseId', async (req, res) => {
|
||||
}
|
||||
|
||||
const idleDeviceCount = await Device.count({
|
||||
where: { warehouseId: req.params.warehouseId, isIdle: true }
|
||||
where: { warehouseId: req.params.warehouseId, isIdle: true },
|
||||
});
|
||||
|
||||
if (idleDeviceCount > 0) {
|
||||
return res.status(400).json({
|
||||
error: `库房中还有 ${idleDeviceCount} 台空闲设备,请先处理后再删除`
|
||||
error: `库房中还有 ${idleDeviceCount} 台空闲设备,请先处理后再删除`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -216,7 +226,7 @@ router.delete('/:warehouseId', async (req, res) => {
|
||||
targetId: req.params.warehouseId,
|
||||
targetName: warehouseName,
|
||||
req,
|
||||
metadata: { type: 'warehouse_delete' }
|
||||
metadata: { type: 'warehouse_delete' },
|
||||
});
|
||||
|
||||
res.json({ message: '库房删除成功' });
|
||||
|
||||
@@ -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('迁移完成!');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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(停用)');
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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: '连接测试成功',
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user