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

style(backend): 格式化模型文件代码
style(frontend): 调整组件代码格式
chore: 删除旧 ESLint 配置并添加新配置
refactor(backend): 重构模型定义语法
style: 统一箭头函数和对象属性简写
This commit is contained in:
zhang1106
2026-03-27 19:12:16 +08:00
parent 6a8d4144ff
commit 63f0cb570e
166 changed files with 15483 additions and 11170 deletions
+26 -18
View File
@@ -3,34 +3,42 @@ const { sequelize } = require('../db');
async function addIsSystemColumn() {
try {
console.log('开始添加 isSystem 列到 deviceFields 表...');
// 检查列是否已存在
const tableInfo = await sequelize.query(
"PRAGMA table_info(deviceFields)",
{ type: sequelize.QueryTypes.SELECT }
);
const tableInfo = await sequelize.query('PRAGMA table_info(deviceFields)', {
type: sequelize.QueryTypes.SELECT,
});
const hasIsSystemColumn = tableInfo.some(col => col.name === 'isSystem');
if (hasIsSystemColumn) {
console.log('isSystem 列已存在,跳过添加');
} else {
// 添加 isSystem 列
await sequelize.query(
"ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0",
{ type: sequelize.QueryTypes.RAW }
);
await sequelize.query('ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0', {
type: sequelize.QueryTypes.RAW,
});
console.log('isSystem 列添加成功');
}
// 更新现有数据:将核心字段标记为系统字段
const systemFields = [
'deviceId', 'name', 'type', 'model', 'serialNumber',
'rackId', 'position', 'height', 'powerConsumption',
'status', 'purchaseDate', 'warrantyExpiry',
'ipAddress', 'description'
'deviceId',
'name',
'type',
'model',
'serialNumber',
'rackId',
'position',
'height',
'powerConsumption',
'status',
'purchaseDate',
'warrantyExpiry',
'ipAddress',
'description',
];
for (const fieldName of systemFields) {
await sequelize.query(
`UPDATE deviceFields SET isSystem = 1 WHERE fieldName = '${fieldName}'`,
@@ -38,7 +46,7 @@ async function addIsSystemColumn() {
);
}
console.log('系统字段标记完成');
console.log('数据库迁移完成');
process.exit(0);
} catch (error) {
@@ -16,61 +16,61 @@ async function migrate() {
userId: {
type: sequelize.Sequelize.STRING,
primaryKey: true,
allowNull: false
allowNull: false,
},
username: {
type: sequelize.Sequelize.STRING,
allowNull: false,
unique: true
unique: true,
},
password: {
type: sequelize.Sequelize.STRING,
allowNull: false
allowNull: false,
},
email: {
type: sequelize.Sequelize.STRING,
allowNull: true
allowNull: true,
},
phone: {
type: sequelize.Sequelize.STRING,
allowNull: true
allowNull: true,
},
realName: {
type: sequelize.Sequelize.STRING,
allowNull: true
allowNull: true,
},
avatar: {
type: sequelize.Sequelize.STRING,
allowNull: true
allowNull: true,
},
status: {
type: sequelize.Sequelize.ENUM('active', 'inactive', 'locked', 'pending'),
defaultValue: 'active'
defaultValue: 'active',
},
lastLoginTime: {
type: sequelize.Sequelize.DATE,
allowNull: true
allowNull: true,
},
lastLoginIp: {
type: sequelize.Sequelize.STRING,
allowNull: true
allowNull: true,
},
loginCount: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
defaultValue: 0,
},
remark: {
type: sequelize.Sequelize.TEXT,
allowNull: true
allowNull: true,
},
createdAt: {
type: sequelize.Sequelize.DATE,
allowNull: false
allowNull: false,
},
updatedAt: {
type: sequelize.Sequelize.DATE,
allowNull: false
}
allowNull: false,
},
});
// 2. 复制数据
@@ -104,14 +104,12 @@ async function migrateSQLite() {
{ name: 'idx_archive_consumable_id', fields: 'consumableId' },
{ name: 'idx_archive_archive_id', fields: 'archiveId' },
{ name: 'idx_archive_deleted_at', fields: 'deletedAt' },
{ name: 'idx_archive_consumable_deleted', fields: 'consumableId, deletedAt' }
{ name: 'idx_archive_consumable_deleted', fields: 'consumableId, deletedAt' },
];
for (const idx of indexes) {
try {
await sequelize.query(
`CREATE INDEX ${idx.name} ON consumable_log_archives(${idx.fields})`
);
await sequelize.query(`CREATE INDEX ${idx.name} ON consumable_log_archives(${idx.fields})`);
console.log(`✓ 创建索引: ${idx.name}`);
} catch (err) {
console.log(`! 创建索引失败: ${idx.name}`, err.message);
@@ -120,7 +120,17 @@ async function updateExistingLogs() {
try {
const consumables = await Consumable.findAll({
attributes: ['consumableId', 'category', 'unit', 'unitPrice', 'supplier', 'location', 'minStock', 'maxStock', 'status']
attributes: [
'consumableId',
'category',
'unit',
'unitPrice',
'supplier',
'location',
'minStock',
'maxStock',
'status',
],
});
const consumableMap = new Map();
@@ -133,14 +143,14 @@ async function updateExistingLogs() {
location: c.location,
minStock: c.minStock,
maxStock: c.maxStock,
status: c.status
status: c.status,
});
});
const logs = await ConsumableLog.findAll({
where: {
consumableSnapshot: null
}
consumableSnapshot: null,
},
});
let updatedCount = 0;
@@ -155,10 +165,9 @@ async function updateExistingLogs() {
console.log(`✓ 更新了 ${updatedCount} 条日志的快照信息`);
const deletedLogsCount = await ConsumableLog.count({
where: { operationType: 'delete' }
where: { operationType: 'delete' },
});
console.log(`✓ 当前有 ${deletedLogsCount} 条删除类型日志`);
} catch (error) {
console.log('! 更新现有日志数据时出错:', error.message);
}
@@ -16,18 +16,15 @@ async function migrate() {
if (actualDbType === 'sqlite') {
// SQLite: 检查字段是否存在
const tableInfo = await sequelize.query(
"PRAGMA table_info(consumables)",
{ type: sequelize.QueryTypes.SELECT }
);
const tableInfo = await sequelize.query('PRAGMA table_info(consumables)', {
type: sequelize.QueryTypes.SELECT,
});
const hasVersion = tableInfo.some(col => col.name === 'version');
if (!hasVersion) {
console.log('添加 version 字段...');
await sequelize.query(
"ALTER TABLE consumables ADD COLUMN version INTEGER DEFAULT 0"
);
await sequelize.query('ALTER TABLE consumables ADD COLUMN version INTEGER DEFAULT 0');
console.log('version 字段添加成功');
} else {
console.log('version 字段已存在,跳过');
@@ -38,19 +35,16 @@ async function migrate() {
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='consumables'",
{ type: sequelize.QueryTypes.SELECT }
);
const hasUpdatedAtIndex = indexes.some(idx => idx.name === 'consumables_updatedAt');
if (!hasUpdatedAtIndex) {
console.log('添加 updatedAt 索引...');
await sequelize.query(
"CREATE INDEX consumables_updatedAt ON consumables(updatedAt)"
);
await sequelize.query('CREATE INDEX consumables_updatedAt ON consumables(updatedAt)');
console.log('updatedAt 索引添加成功');
} else {
console.log('updatedAt 索引已存在,跳过');
}
} else if (actualDbType === 'mysql') {
// MySQL: 检查并添加字段
try {
@@ -70,9 +64,7 @@ async function migrate() {
// 添加索引
try {
console.log('添加 updatedAt 索引...');
await sequelize.query(
"CREATE INDEX idx_consumables_updatedAt ON consumables(updatedAt)"
);
await sequelize.query('CREATE INDEX idx_consumables_updatedAt ON consumables(updatedAt)');
console.log('updatedAt 索引添加成功');
} catch (err) {
if (err.message.includes('Duplicate key')) {
@@ -85,9 +77,7 @@ async function migrate() {
// 初始化现有数据的 version 值
console.log('初始化现有数据的 version 值...');
await sequelize.query(
"UPDATE consumables SET version = 0 WHERE version IS NULL"
);
await sequelize.query('UPDATE consumables SET version = 0 WHERE version IS NULL');
console.log('version 值初始化完成');
console.log('迁移完成!');
+30 -31
View File
@@ -8,19 +8,19 @@ async function migrate() {
console.log(' IDC管理系统 - 数据库迁移脚本 v2.0 ');
console.log('========================================');
console.log('');
try {
console.log('🔍 检测数据库类型...');
const dbType = sequelize.getDialect();
console.log(` 数据库类型: ${dbType}`);
console.log('');
console.log('📋 开始迁移...');
console.log(' 1. 创建 network_cards 表');
console.log(' 2. 为 device_ports 添加 nic_id 字段');
console.log(' 3. 创建相关索引');
console.log('');
if (dbType === 'sqlite') {
await migrateSQLite();
} else if (dbType === 'mysql') {
@@ -29,25 +29,26 @@ async function migrate() {
console.log(`⚠️ 不支持的数据库类型: ${dbType}`);
process.exit(1);
}
console.log('');
console.log('✅ 迁移完成!');
console.log('');
console.log('📊 验证迁移结果...');
const [tables] = await sequelize.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
const [tables] = await sequelize.query(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
);
console.log(` 数据库表: ${tables.map(t => t.name).join(', ')}`);
const portCount = await DevicePort.count();
const cardCount = await NetworkCard.count();
console.log(` device_ports: ${portCount} 条记录`);
console.log(` network_cards: ${cardCount} 条记录`);
console.log('');
console.log('========================================');
console.log(' 迁移成功完成!🎉');
console.log('========================================');
} catch (error) {
console.error('');
console.error('❌ 迁移失败:', error.message);
@@ -62,33 +63,32 @@ async function migrate() {
async function migrateSQLite() {
console.log('');
console.log('🔄 执行 SQLite 迁移...');
await sequelize.query('PRAGMA foreign_keys = OFF');
try {
console.log(' → 删除旧的 device_ports 表...');
await sequelize.query('DROP TABLE IF EXISTS `device_ports`');
console.log(' → 删除旧的 network_cards 表...');
await sequelize.query('DROP TABLE IF EXISTS `network_cards`');
console.log(' → 同步 DevicePort 模型...');
await DevicePort.sync({ force: false });
console.log(' → 同步 NetworkCard 模型...');
await NetworkCard.sync({ force: false });
console.log(' → 同步 Device 模型(确保外键关系)...');
await Device.sync({ force: false });
console.log(' → 重新同步 DevicePort 模型(含外键)...');
await DevicePort.sync({ force: true });
console.log(' → 重新同步 NetworkCard 模型...');
await NetworkCard.sync({ force: false });
await sequelize.query('PRAGMA foreign_keys = ON');
} catch (error) {
await sequelize.query('PRAGMA foreign_keys = ON');
throw error;
@@ -98,10 +98,10 @@ async function migrateSQLite() {
async function migrateMySQL() {
console.log('');
console.log('🔄 执行 MySQL 迁移...');
const tableName = 'network_cards';
console.log(` → 创建表: ${tableName}`);
const createTableSQL = `
CREATE TABLE IF NOT EXISTS \`${tableName}\` (
\`nic_id\` VARCHAR(255) NOT NULL PRIMARY KEY,
@@ -120,15 +120,14 @@ async function migrateMySQL() {
UNIQUE INDEX \`idx_device_name\` (\`device_id\`, \`name\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`;
await sequelize.query(createTableSQL);
console.log(' → 检查 nic_id 字段是否存在...');
const [columns] = await sequelize.query(
"SHOW COLUMNS FROM `device_ports` LIKE 'nic_id'",
{ type: sequelize.QueryTypes.SELECT }
);
const [columns] = await sequelize.query("SHOW COLUMNS FROM `device_ports` LIKE 'nic_id'", {
type: sequelize.QueryTypes.SELECT,
});
if (columns.length === 0) {
console.log(' → 添加 nic_id 字段...');
await sequelize.query(
@@ -137,7 +136,7 @@ async function migrateMySQL() {
} else {
console.log(' → nic_id 字段已存在,跳过');
}
console.log(' → 创建 nic_id 索引...');
try {
await sequelize.query('CREATE INDEX `idx_port_nic_id` ON `device_ports`(`nic_id`)');
@@ -148,11 +147,11 @@ async function migrateMySQL() {
throw error;
}
}
console.log(' → 同步模型以确保关系正确...');
await NetworkCard.sync({ force: false });
await DevicePort.sync({ force: false });
console.log(' → 添加外键约束...');
try {
await sequelize.query(
@@ -55,10 +55,9 @@ async function removeSQLiteForeignKey() {
console.log('SQLite 不支持直接删除外键,需要重建表');
// 检查外键是否存在
const fks = await sequelize.query(
`PRAGMA foreign_key_list(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT }
);
const fks = await sequelize.query(`PRAGMA foreign_key_list(consumable_logs);`, {
type: sequelize.QueryTypes.SELECT,
});
if (!fks || fks.length === 0) {
console.log('✓ 没有外键约束需要移除');
@@ -68,17 +67,21 @@ async function removeSQLiteForeignKey() {
console.log('发现外键约束:', fks);
// SQLite 不支持 ALTER TABLE DROP FOREIGN KEY,需要重建表
await sequelize.transaction(async (transaction) => {
await sequelize.transaction(async transaction => {
// 获取表结构
const columns = await sequelize.query(
`PRAGMA table_info(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT, transaction }
const columns = await sequelize.query(`PRAGMA table_info(consumable_logs);`, {
type: sequelize.QueryTypes.SELECT,
transaction,
});
console.log(
'\n当前表列:',
columns.map(c => c.name)
);
console.log('\n当前表列:', columns.map(c => c.name));
// 创建新表(不包含外键约束)
await sequelize.query(`
await sequelize.query(
`
CREATE TABLE consumable_logs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
consumableId VARCHAR(255) NOT NULL,
@@ -101,20 +104,25 @@ async function removeSQLiteForeignKey() {
isConsumableDeleted BOOLEAN DEFAULT 0,
consumableSnapshot TEXT
)
`, { transaction });
`,
{ transaction }
);
console.log('✓ 创建新表成功');
// 复制数据
await sequelize.query(`
await sequelize.query(
`
INSERT INTO consumable_logs_new
SELECT * FROM consumable_logs
`, { transaction });
const countResult = await sequelize.query(
`SELECT COUNT(*) as count FROM consumable_logs_new`,
{ type: sequelize.QueryTypes.SELECT, transaction }
`,
{ transaction }
);
const countResult = await sequelize.query(`SELECT COUNT(*) as count FROM consumable_logs_new`, {
type: sequelize.QueryTypes.SELECT,
transaction,
});
console.log(`✓ 复制了 ${countResult[0].count} 条数据`);
// 删除旧表
@@ -122,7 +130,9 @@ async function removeSQLiteForeignKey() {
console.log('✓ 删除旧表成功');
// 重命名新表
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO consumable_logs`, { transaction });
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO consumable_logs`, {
transaction,
});
console.log('✓ 重命名新表成功');
// 创建索引
@@ -133,7 +143,7 @@ async function removeSQLiteForeignKey() {
{ name: 'consumable_logs_consumable_id_created_at', fields: ['consumableId', 'createdAt'] },
{ name: 'consumable_logs_original_log_id', fields: ['originalLogId'] },
{ name: 'consumable_logs_is_editable', fields: ['isEditable'] },
{ name: 'idx_consumable_logs_is_deleted', fields: ['isConsumableDeleted'] }
{ name: 'idx_consumable_logs_is_deleted', fields: ['isConsumableDeleted'] },
];
for (const idx of indexes) {
+4 -4
View File
@@ -3,10 +3,10 @@
/**
* 命令行备份脚本
* 用于独立执行数据备份,支持环境迁移
*
*
* 使用方法:
* node scripts/backup.js [options]
*
*
* 选项:
* --output, -o 指定备份文件输出路径
* --description 备份描述
@@ -67,7 +67,7 @@ async function runBackup() {
try {
process.chdir(path.join(__dirname, '..'));
const { createBackup, getBackupPath } = require('../utils/backup');
const { sequelize } = require('../db');
@@ -76,7 +76,7 @@ async function runBackup() {
console.log('数据库连接成功\n');
const backupPath = options.output ? path.dirname(options.output) : getBackupPath();
console.log('备份配置:');
console.log(` 输出路径: ${backupPath}`);
console.log(` 包含文件: ${options.includeFiles ? '是' : '否'}`);
+8 -7
View File
@@ -8,22 +8,23 @@ async function ensureSchema() {
if (dbType === 'sqlite') {
// Check if nicId column exists in device_ports
const [columns] = await sequelize.query("PRAGMA table_info(device_ports)");
const [columns] = await sequelize.query('PRAGMA table_info(device_ports)');
const hasNicId = columns.some(col => col.name === 'nicId');
if (!hasNicId) {
console.log('Adding nicId column to device_ports...');
await sequelize.query('ALTER TABLE device_ports ADD COLUMN nicId VARCHAR(255) NULL REFERENCES network_cards(nicId)');
await sequelize.query(
'ALTER TABLE device_ports ADD COLUMN nicId VARCHAR(255) NULL REFERENCES network_cards(nicId)'
);
console.log('Added nicId column.');
} else {
console.log('nicId column already exists in device_ports.');
}
} else if (dbType === 'mysql') {
const [columns] = await sequelize.query(
"SHOW COLUMNS FROM `device_ports` LIKE 'nicId'",
{ type: QueryTypes.SELECT }
);
const [columns] = await sequelize.query("SHOW COLUMNS FROM `device_ports` LIKE 'nicId'", {
type: QueryTypes.SELECT,
});
if (columns.length === 0) {
console.log('Adding nicId column to device_ports...');
await sequelize.query(
@@ -4,98 +4,91 @@ const path = require('path');
const templateData = [
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC2-SERVER-01',
'所属机房名称': 'IDC2',
机柜名称: 'IDC2-SERVER-01',
所属机房名称: 'IDC2',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC2-SERVER-02',
'所属机房名称': 'IDC2',
机柜名称: 'IDC2-SERVER-02',
所属机房名称: 'IDC2',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC2-SERVER-03',
'所属机房名称': 'IDC2',
机柜名称: 'IDC2-SERVER-03',
所属机房名称: 'IDC2',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC4-NETWORK-01',
'所属机房名称': 'IDC4',
机柜名称: 'IDC4-NETWORK-01',
所属机房名称: 'IDC4',
'高度(U)': 48,
'最大功率(W)': 8000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC4-NETWORK-02',
'所属机房名称': 'IDC4',
机柜名称: 'IDC4-NETWORK-02',
所属机房名称: 'IDC4',
'高度(U)': 48,
'最大功率(W)': 8000,
'状态': 'maintenance'
状态: 'maintenance',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC5-STORAGE-01',
'所属机房名称': 'IDC5',
机柜名称: 'IDC5-STORAGE-01',
所属机房名称: 'IDC5',
'高度(U)': 42,
'最大功率(W)': 10000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC5-STORAGE-02',
'所属机房名称': 'IDC5',
机柜名称: 'IDC5-STORAGE-02',
所属机房名称: 'IDC5',
'高度(U)': 42,
'最大功率(W)': 10000,
'状态': 'inactive'
状态: 'inactive',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': 'IDC7-SERVER-01',
'所属机房名称': 'IDC7',
机柜名称: 'IDC7-SERVER-01',
所属机房名称: 'IDC7',
'高度(U)': 36,
'最大功率(W)': 6000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': '古荡-机柜-01',
'所属机房名称': '古荡机房1-1',
机柜名称: '古荡-机柜-01',
所属机房名称: '古荡机房1-1',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
状态: 'active',
},
{
'机柜ID(留空自动生成)': '',
'机柜名称': '古荡-机柜-02',
'所属机房名称': '古荡机房1-1',
机柜名称: '古荡-机柜-02',
所属机房名称: '古荡机房1-1',
'高度(U)': 42,
'最大功率(W)': 5000,
'状态': 'active'
}
状态: 'active',
},
];
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(templateData);
ws['!cols'] = [
{ wch: 20 },
{ wch: 20 },
{ wch: 15 },
{ wch: 10 },
{ wch: 15 },
{ wch: 15 }
];
ws['!cols'] = [{ wch: 20 }, { wch: 20 }, { wch: 15 }, { wch: 10 }, { wch: 15 }, { wch: 15 }];
XLSX.utils.book_append_sheet(wb, ws, '机柜导入模板');
@@ -107,7 +100,9 @@ console.log(`共 ${templateData.length} 条测试数据`);
console.log('\n字段说明:');
console.log('- 机柜ID(留空自动生成): 留空则系统自动生成唯一ID');
console.log('- 机柜名称: 必填,机柜的唯一标识名称');
console.log('- 所属机房名称: 必填,系统现有机房: IDC2, IDC4, IDC5, IDC7, 古荡机房1-1, 三墩机房1-1, 地市机房, IDCT');
console.log(
'- 所属机房名称: 必填,系统现有机房: IDC2, IDC4, IDC5, IDC7, 古荡机房1-1, 三墩机房1-1, 地市机房, IDCT'
);
console.log('- 高度(U): 必填,机柜的标准高度(1-50U');
console.log('- 最大功率(W): 必填,机柜的最大承载功率');
console.log('- 状态: active(在用)/maintenance(维护中)/inactive(停用)');
console.log('- 状态: active(在用)/maintenance(维护中)/inactive(停用)');
+190 -29
View File
@@ -1,7 +1,7 @@
/**
* 数据库初始化脚本
* 用于生产环境首次部署时创建表结构
*
*
* 注意:此脚本为独立运行脚本,与 server.js 的初始化逻辑重复
* 如果 server.js 已能正常完成初始化,则无需单独运行此脚本
* 如需单独运行:node backend/scripts/init-database.js
@@ -26,33 +26,194 @@ const InventoryRecord = require('../models/InventoryRecord');
// 默认系统设置(包含中文描述)
const defaultSettings = [
// 全局配置
{ settingKey: 'site_name', settingValue: JSON.stringify('机柜管理系统'), settingType: 'string', category: 'general', description: '网站名称', isEditable: true },
{ settingKey: 'site_logo', settingValue: JSON.stringify(''), settingType: 'string', category: 'general', description: '网站Logo URL', isEditable: true },
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '登录有效期(分钟)', isEditable: true },
{ settingKey: 'idle_timeout', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '用户空闲超时时间(分钟)', isEditable: true },
{ settingKey: 'idle_warning_time', settingValue: JSON.stringify(10), settingType: 'number', category: 'general', description: '空闲超时前警告时间(秒)', isEditable: false },
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
{
settingKey: 'site_name',
settingValue: JSON.stringify('机柜管理系统'),
settingType: 'string',
category: 'general',
description: '网站名称',
isEditable: true,
},
{
settingKey: 'site_logo',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'general',
description: '网站Logo URL',
isEditable: true,
},
{
settingKey: 'timezone',
settingValue: JSON.stringify('Asia/Shanghai'),
settingType: 'string',
category: 'general',
description: '时区设置',
isEditable: true,
},
{
settingKey: 'date_format',
settingValue: JSON.stringify('YYYY-MM-DD'),
settingType: 'string',
category: 'general',
description: '日期格式',
isEditable: true,
},
{
settingKey: 'session_timeout',
settingValue: JSON.stringify(30),
settingType: 'number',
category: 'general',
description: '登录有效期(分钟)',
isEditable: true,
},
{
settingKey: 'idle_timeout',
settingValue: JSON.stringify(10),
settingType: 'number',
category: 'general',
description: '用户空闲超时时间(分钟)',
isEditable: true,
},
{
settingKey: 'idle_warning_time',
settingValue: JSON.stringify(10),
settingType: 'number',
category: 'general',
description: '空闲超时前警告时间(秒)',
isEditable: false,
},
{
settingKey: 'max_login_attempts',
settingValue: JSON.stringify(5),
settingType: 'number',
category: 'general',
description: '最大登录尝试次数',
isEditable: true,
},
{
settingKey: 'maintenance_mode',
settingValue: JSON.stringify(false),
settingType: 'boolean',
category: 'general',
description: '维护模式',
isEditable: true,
},
// 外观设置
{ settingKey: 'primary_color', settingValue: JSON.stringify('#667eea'), settingType: 'string', category: 'appearance', description: '主题主色调', isEditable: true },
{ settingKey: 'secondary_color', settingValue: JSON.stringify('#764ba2'), settingType: 'string', category: 'appearance', description: '主题辅助色调', isEditable: true },
{ settingKey: 'compact_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '紧凑模式', isEditable: true },
{ settingKey: 'sidebar_collapsed', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '侧边栏默认折叠', isEditable: true },
{ settingKey: 'table_row_height', settingValue: JSON.stringify('default'), settingType: 'string', category: 'appearance', description: '表格行高: small/default/middle/large', isEditable: true },
{ settingKey: 'animation_enabled', settingValue: JSON.stringify(true), settingType: 'boolean', category: 'appearance', description: '启用动画效果', isEditable: true },
{
settingKey: 'primary_color',
settingValue: JSON.stringify('#667eea'),
settingType: 'string',
category: 'appearance',
description: '主题主色调',
isEditable: true,
},
{
settingKey: 'secondary_color',
settingValue: JSON.stringify('#764ba2'),
settingType: 'string',
category: 'appearance',
description: '主题辅助色调',
isEditable: true,
},
{
settingKey: 'compact_mode',
settingValue: JSON.stringify(false),
settingType: 'boolean',
category: 'appearance',
description: '紧凑模式',
isEditable: true,
},
{
settingKey: 'sidebar_collapsed',
settingValue: JSON.stringify(false),
settingType: 'boolean',
category: 'appearance',
description: '侧边栏默认折叠',
isEditable: true,
},
{
settingKey: 'table_row_height',
settingValue: JSON.stringify('default'),
settingType: 'string',
category: 'appearance',
description: '表格行高: small/default/middle/large',
isEditable: true,
},
{
settingKey: 'animation_enabled',
settingValue: JSON.stringify(true),
settingType: 'boolean',
category: 'appearance',
description: '启用动画效果',
isEditable: true,
},
// 关于页面
{ settingKey: 'app_version', settingValue: JSON.stringify('1.0.0'), settingType: 'string', category: 'about', description: '应用版本', isEditable: false },
{ settingKey: 'company_name', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司/组织名称', isEditable: true },
{ settingKey: 'contact_email', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系邮箱', isEditable: true },
{ settingKey: 'contact_phone', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系电话', isEditable: true },
{ settingKey: 'company_address', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司地址', isEditable: true },
{ settingKey: 'system_description', settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'), settingType: 'string', category: 'about', description: '系统描述', isEditable: true },
{ settingKey: 'privacy_policy', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '隐私政策URL', isEditable: true },
{ settingKey: 'terms_of_service', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '服务条款URL', isEditable: true },
{
settingKey: 'app_version',
settingValue: JSON.stringify('1.0.0'),
settingType: 'string',
category: 'about',
description: '应用版本',
isEditable: false,
},
{
settingKey: 'company_name',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '公司/组织名称',
isEditable: true,
},
{
settingKey: 'contact_email',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '联系邮箱',
isEditable: true,
},
{
settingKey: 'contact_phone',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '联系电话',
isEditable: true,
},
{
settingKey: 'company_address',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '公司地址',
isEditable: true,
},
{
settingKey: 'system_description',
settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'),
settingType: 'string',
category: 'about',
description: '系统描述',
isEditable: true,
},
{
settingKey: 'privacy_policy',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '隐私政策URL',
isEditable: true,
},
{
settingKey: 'terms_of_service',
settingValue: JSON.stringify(''),
settingType: 'string',
category: 'about',
description: '服务条款URL',
isEditable: true,
},
];
async function initDatabase() {
@@ -65,29 +226,29 @@ async function initDatabase() {
// 同步所有模型(按依赖顺序)
console.log('开始同步模型...');
// 基础模型
await User.sync({ alter: true });
await Room.sync({ alter: true });
await Rack.sync({ alter: true });
await Device.sync({ alter: true });
await FaultCategory.sync({ alter: true });
// 耗材相关模型
await ConsumableCategory.sync({ alter: true });
await Consumable.sync({ alter: true });
await ConsumableLog.sync({ alter: true });
await ConsumableRecord.sync({ alter: true });
await ConsumableLogArchive.sync({ alter: true });
// 盘点相关模型
await InventoryPlan.sync({ alter: true });
await InventoryTask.sync({ alter: true });
await InventoryRecord.sync({ alter: true });
// 系统设置
await SystemSetting.sync({ alter: true });
console.log('数据库表结构同步完成');
// 初始化或更新系统设置
+134 -113
View File
@@ -28,73 +28,73 @@ const migrations = [
{
name: 'v2.0 - 网卡和端口表',
description: '创建 network_cards 表,为 device_ports 添加 nic_id 字段',
migrate: migrateV2
migrate: migrateV2,
},
{
name: '用户表 pending 状态',
description: '为用户表添加 pending 状态支持',
migrate: migratePendingStatus
migrate: migratePendingStatus,
},
{
name: '耗材乐观锁',
description: '为 consumables 表添加 version 字段',
migrate: migrateConsumableVersion
migrate: migrateConsumableVersion,
},
{
name: '耗材操作日志表结构',
description: '添加 isEditable、originalLogId 等修改记录字段',
migrate: migrateConsumableLogs
migrate: migrateConsumableLogs,
},
{
name: '耗材日志解耦',
description: '添加 isConsumableDeleted 和 consumableSnapshot 字段',
migrate: migrateConsumableLogDecouple
migrate: migrateConsumableLogDecouple,
},
{
name: '移除日志外键约束',
description: '移除 consumable_logs 表的外键约束,防止级联删除',
migrate: removeConsumableLogFK
migrate: removeConsumableLogFK,
},
{
name: '耗材日志归档表',
description: '创建 consumable_log_archives 归档表',
migrate: migrateConsumableLogArchive
migrate: migrateConsumableLogArchive,
},
{
name: '耗材SN序列号字段',
description: '为 consumables、consumable_records、consumable_logs 添加 snList 字段',
migrate: migrateSnList
migrate: migrateSnList,
},
{
name: '设备型号字段可空',
description: '将 devices 表 model 字段改为可空,支持非必填',
migrate: migrateDeviceModelField
migrate: migrateDeviceModelField,
},
{
name: '设备字段配置同步',
description: '同步前后端字段必填配置',
migrate: migrateDeviceFieldsConfig
migrate: migrateDeviceFieldsConfig,
},
{
name: '设备表字段可空',
description: '将设备表所有字段改为可空,由应用层验证控制',
migrate: migrateDeviceFieldsNullable
migrate: migrateDeviceFieldsNullable,
},
{
name: '暂存设备自定义字段',
description: '为 pending_devices 表添加 customFields 字段,支持自定义字段存储',
migrate: migratePendingDeviceCustomFields
migrate: migratePendingDeviceCustomFields,
},
{
name: '空闲设备与业务关联',
description: '创建 businesses、warehouses、device_business 表,为 devices 添加空闲设备字段',
migrate: migrateIdleDeviceAndBusiness
migrate: migrateIdleDeviceAndBusiness,
},
{
name: '设备字段系统标记',
description: '为 deviceFields 表添加 isSystem 字段,标记系统字段不可删除',
migrate: migrateDeviceFieldsIsSystem
}
migrate: migrateDeviceFieldsIsSystem,
},
];
async function runMigrations() {
@@ -149,25 +149,23 @@ async function runMigrations() {
async function getTableColumns(tableName) {
const dialect = sequelize.getDialect();
if (dialect === 'sqlite') {
const tableInfo = await sequelize.query(
`PRAGMA table_info(${tableName})`,
{ type: sequelize.QueryTypes.SELECT }
);
const tableInfo = await sequelize.query(`PRAGMA table_info(${tableName})`, {
type: sequelize.QueryTypes.SELECT,
});
return tableInfo.map(col => col.name);
} else {
const tableInfo = await sequelize.query(
`SHOW COLUMNS FROM ${tableName}`,
{ type: sequelize.QueryTypes.SELECT }
);
const tableInfo = await sequelize.query(`SHOW COLUMNS FROM ${tableName}`, {
type: sequelize.QueryTypes.SELECT,
});
return tableInfo.map(col => col.Field);
}
}
async function tableExists(tableName) {
const dialect = sequelize.getDialect();
if (dialect === 'sqlite') {
const tables = await sequelize.query(
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
@@ -175,22 +173,23 @@ async function tableExists(tableName) {
);
return tables.length > 0;
} else {
const tables = await sequelize.query(
"SHOW TABLES LIKE ?",
{ replacements: [tableName], type: sequelize.QueryTypes.SELECT }
);
const tables = await sequelize.query('SHOW TABLES LIKE ?', {
replacements: [tableName],
type: sequelize.QueryTypes.SELECT,
});
return tables.length > 0;
}
}
async function addColumnIfNotExists(tableName, columnName, columnDef) {
const columns = await getTableColumns(tableName);
if (!columns.includes(columnName)) {
const dialect = sequelize.getDialect();
const sql = dialect === 'sqlite'
? `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`
: `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`;
const sql =
dialect === 'sqlite'
? `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`
: `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`;
await sequelize.query(sql);
console.log(` ${tableName} 表添加 ${columnName} 字段成功`);
} else {
@@ -210,32 +209,32 @@ async function migrateV2() {
id: {
type: sequelize.Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
autoIncrement: true,
},
nicId: {
type: sequelize.Sequelize.STRING,
allowNull: false,
unique: true
unique: true,
},
name: {
type: sequelize.Sequelize.STRING,
allowNull: false
allowNull: false,
},
macAddress: {
type: sequelize.Sequelize.STRING
type: sequelize.Sequelize.STRING,
},
ipAddress: {
type: sequelize.Sequelize.STRING
type: sequelize.Sequelize.STRING,
},
deviceId: {
type: sequelize.Sequelize.STRING
type: sequelize.Sequelize.STRING,
},
status: {
type: sequelize.Sequelize.STRING,
defaultValue: 'active'
defaultValue: 'active',
},
createdAt: sequelize.Sequelize.DATE,
updatedAt: sequelize.Sequelize.DATE
updatedAt: sequelize.Sequelize.DATE,
});
}
@@ -276,12 +275,11 @@ async function migrateConsumableLogDecouple() {
async function removeConsumableLogFK() {
const dialect = sequelize.getDialect();
if (dialect === 'sqlite') {
const fks = await sequelize.query(
`PRAGMA foreign_key_list(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT }
);
const fks = await sequelize.query(`PRAGMA foreign_key_list(consumable_logs);`, {
type: sequelize.QueryTypes.SELECT,
});
if (!fks || fks.length === 0) {
return;
@@ -327,7 +325,9 @@ async function removeConsumableLogFK() {
await sequelize.query(`CREATE INDEX idx_logs_consumable_id ON consumable_logs(consumableId)`);
await sequelize.query(`CREATE INDEX idx_logs_operation_type ON consumable_logs(operationType)`);
await sequelize.query(`CREATE INDEX idx_logs_created_at ON consumable_logs(createdAt)`);
await sequelize.query(`CREATE INDEX idx_logs_is_consumable_deleted ON consumable_logs(isConsumableDeleted)`);
await sequelize.query(
`CREATE INDEX idx_logs_is_consumable_deleted ON consumable_logs(isConsumableDeleted)`
);
}
}
@@ -341,80 +341,86 @@ async function migrateConsumableLogArchive() {
id: {
type: sequelize.Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
autoIncrement: true,
},
archiveId: {
type: sequelize.Sequelize.STRING,
allowNull: false,
unique: true
unique: true,
},
consumableId: {
type: sequelize.Sequelize.STRING,
allowNull: false
allowNull: false,
},
consumableName: {
type: sequelize.Sequelize.STRING,
allowNull: false
allowNull: false,
},
consumableSnapshot: {
type: sequelize.Sequelize.TEXT
type: sequelize.Sequelize.TEXT,
},
totalOperations: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
defaultValue: 0,
},
firstOperationAt: {
type: sequelize.Sequelize.DATE
type: sequelize.Sequelize.DATE,
},
lastOperationAt: {
type: sequelize.Sequelize.DATE
type: sequelize.Sequelize.DATE,
},
totalInQuantity: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
defaultValue: 0,
},
totalOutQuantity: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
defaultValue: 0,
},
finalStock: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
defaultValue: 0,
},
deletedBy: {
type: sequelize.Sequelize.STRING
type: sequelize.Sequelize.STRING,
},
deletedAt: {
type: sequelize.Sequelize.DATE
type: sequelize.Sequelize.DATE,
},
deleteReason: {
type: sequelize.Sequelize.STRING
type: sequelize.Sequelize.STRING,
},
createdAt: {
type: sequelize.Sequelize.DATE,
allowNull: false,
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP'),
},
updatedAt: {
type: sequelize.Sequelize.DATE,
allowNull: false,
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
}
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
if (dbDialect === 'sqlite') {
await sequelize.query(`CREATE INDEX idx_archive_consumable_id ON consumable_log_archives(consumableId)`);
await sequelize.query(`CREATE INDEX idx_archive_archive_id ON consumable_log_archives(archiveId)`);
await sequelize.query(`CREATE INDEX idx_archive_deleted_at ON consumable_log_archives(deletedAt)`);
await sequelize.query(
`CREATE INDEX idx_archive_consumable_id ON consumable_log_archives(consumableId)`
);
await sequelize.query(
`CREATE INDEX idx_archive_archive_id ON consumable_log_archives(archiveId)`
);
await sequelize.query(
`CREATE INDEX idx_archive_deleted_at ON consumable_log_archives(deletedAt)`
);
}
}
async function migrateSnList() {
const tables = ['consumables', 'consumable_records', 'consumable_logs'];
for (const table of tables) {
if (await tableExists(table)) {
const columnDef = dbDialect === 'sqlite' ? "TEXT DEFAULT '[]'" : "JSON";
const columnDef = dbDialect === 'sqlite' ? "TEXT DEFAULT '[]'" : 'JSON';
await addColumnIfNotExists(table, 'snList', columnDef);
} else {
console.log(` ${table} 表不存在,跳过`);
@@ -429,11 +435,9 @@ async function migrateDeviceModelField() {
}
const dialect = sequelize.getDialect();
if (dialect === 'mysql') {
await sequelize.query(
'ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL'
);
await sequelize.query('ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL');
console.log(' devices 表 model 字段已改为可空');
} else if (dialect === 'sqlite') {
const columns = await getTableColumns('devices');
@@ -441,7 +445,7 @@ async function migrateDeviceModelField() {
console.log(' model_old 字段已存在,跳过迁移');
return;
}
await sequelize.query('ALTER TABLE devices RENAME COLUMN model TO model_old');
await sequelize.query('ALTER TABLE devices ADD COLUMN model VARCHAR(255)');
await sequelize.query('UPDATE devices SET model = model_old');
@@ -452,14 +456,14 @@ async function migrateDeviceModelField() {
async function migrateDeviceFieldsConfig() {
const DeviceField = require('../models/DeviceField');
const updates = [
{ fieldName: 'model', required: false },
{ fieldName: 'powerConsumption', required: true },
{ fieldName: 'purchaseDate', required: false },
{ fieldName: 'warrantyExpiry', required: false },
];
for (const update of updates) {
const field = await DeviceField.findOne({ where: { fieldName: update.fieldName } });
if (field && field.required !== update.required) {
@@ -475,20 +479,20 @@ async function migrateDeviceFieldsConfig() {
async function migrateDeviceFieldsNullable() {
const dialect = sequelize.getDialect();
if (dialect === 'mysql') {
const alterCommands = [
"ALTER TABLE devices MODIFY COLUMN name VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN type VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN serialNumber VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN rackId VARCHAR(255) NULL",
"ALTER TABLE devices MODIFY COLUMN position INTEGER NULL",
"ALTER TABLE devices MODIFY COLUMN height INTEGER NULL",
"ALTER TABLE devices MODIFY COLUMN powerConsumption FLOAT NULL",
"ALTER TABLE devices MODIFY COLUMN customFields JSON NULL"
'ALTER TABLE devices MODIFY COLUMN name VARCHAR(255) NULL',
'ALTER TABLE devices MODIFY COLUMN type VARCHAR(255) NULL',
'ALTER TABLE devices MODIFY COLUMN model VARCHAR(255) NULL',
'ALTER TABLE devices MODIFY COLUMN serialNumber VARCHAR(255) NULL',
'ALTER TABLE devices MODIFY COLUMN rackId VARCHAR(255) NULL',
'ALTER TABLE devices MODIFY COLUMN position INTEGER NULL',
'ALTER TABLE devices MODIFY COLUMN height INTEGER NULL',
'ALTER TABLE devices MODIFY COLUMN powerConsumption FLOAT NULL',
'ALTER TABLE devices MODIFY COLUMN customFields JSON NULL',
];
for (const sql of alterCommands) {
try {
await sequelize.query(sql);
@@ -499,21 +503,20 @@ async function migrateDeviceFieldsNullable() {
}
}
console.log(' devices 表字段已改为可空');
} else if (dialect === 'sqlite') {
const columns = await getTableColumns('devices');
const hasNullableFlag = columns.includes('_nullable_migration_done');
if (hasNullableFlag) {
console.log(' 已完成可空迁移,跳过');
return;
}
await sequelize.query('PRAGMA foreign_keys = OFF');
try {
await sequelize.query('DROP TABLE IF EXISTS devices_new');
await sequelize.query(`
CREATE TABLE devices_new (
deviceId VARCHAR(255) PRIMARY KEY NOT NULL UNIQUE,
@@ -536,7 +539,7 @@ async function migrateDeviceFieldsNullable() {
_nullable_migration_done INTEGER DEFAULT 1
)
`);
await sequelize.query(`
INSERT INTO devices_new (
deviceId, name, type, model, serialNumber, rackId, position, height,
@@ -549,15 +552,15 @@ async function migrateDeviceFieldsNullable() {
description, customFields, createdAt, updatedAt
FROM devices
`);
await sequelize.query('DROP TABLE devices');
await sequelize.query('ALTER TABLE devices_new RENAME TO devices');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_status ON devices(status)');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_type ON devices(type)');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_rackId ON devices(rackId)');
await sequelize.query('CREATE INDEX IF NOT EXISTS idx_devices_name ON devices(name)');
console.log(' devices 表字段已改为可空');
} finally {
await sequelize.query('PRAGMA foreign_keys = ON');
@@ -571,7 +574,7 @@ async function migratePendingDeviceCustomFields() {
return;
}
const columnDef = dbDialect === 'sqlite' ? "JSON DEFAULT '{}'" : "JSON";
const columnDef = dbDialect === 'sqlite' ? "JSON DEFAULT '{}'" : 'JSON';
await addColumnIfNotExists('pending_devices', 'customFields', columnDef);
}
@@ -605,10 +608,9 @@ async function migrateDeviceFieldsIsSystem() {
} else {
const columns = await getTableColumns('deviceFields');
if (!columns.includes('isSystem')) {
await sequelize.query(
"ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0",
{ type: sequelize.QueryTypes.RAW }
);
await sequelize.query('ALTER TABLE deviceFields ADD COLUMN isSystem BOOLEAN DEFAULT 0', {
type: sequelize.QueryTypes.RAW,
});
console.log(' deviceFields 表添加 isSystem 字段成功');
} else {
console.log(' deviceFields 表 isSystem 字段已存在,跳过');
@@ -616,16 +618,25 @@ async function migrateDeviceFieldsIsSystem() {
}
const systemFields = [
'deviceId', 'name', 'type', 'model', 'serialNumber',
'rackId', 'position', 'height', 'powerConsumption',
'status', 'purchaseDate', 'warrantyExpiry'
'deviceId',
'name',
'type',
'model',
'serialNumber',
'rackId',
'position',
'height',
'powerConsumption',
'status',
'purchaseDate',
'warrantyExpiry',
];
for (const fieldName of systemFields) {
await sequelize.query(
`UPDATE deviceFields SET isSystem = 1 WHERE fieldName = ?`,
{ replacements: [fieldName], type: sequelize.QueryTypes.RAW }
);
await sequelize.query(`UPDATE deviceFields SET isSystem = 1 WHERE fieldName = ?`, {
replacements: [fieldName],
type: sequelize.QueryTypes.RAW,
});
console.log(` 标记系统字段: ${fieldName}`);
}
@@ -643,14 +654,19 @@ async function migrateIdleDeviceAndBusiness() {
try {
if (!(await tableExists('businesses'))) {
await queryInterface.createTable('businesses', {
businessId: { type: sequelize.Sequelize.STRING, primaryKey: true, allowNull: false, unique: true },
businessId: {
type: sequelize.Sequelize.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: { type: sequelize.Sequelize.STRING, allowNull: false },
description: { type: sequelize.Sequelize.TEXT },
status: { type: sequelize.Sequelize.ENUM('active', 'offline'), defaultValue: 'active' },
offlineDate: { type: sequelize.Sequelize.DATE },
offlineReason: { type: sequelize.Sequelize.STRING },
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false }
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false },
});
console.log(' businesses 表创建成功');
} else {
@@ -659,14 +675,19 @@ async function migrateIdleDeviceAndBusiness() {
if (!(await tableExists('warehouses'))) {
await queryInterface.createTable('warehouses', {
warehouseId: { type: sequelize.Sequelize.STRING, primaryKey: true, allowNull: false, unique: true },
warehouseId: {
type: sequelize.Sequelize.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
name: { type: sequelize.Sequelize.STRING, allowNull: false },
location: { type: sequelize.Sequelize.STRING },
capacity: { type: sequelize.Sequelize.INTEGER, defaultValue: 100 },
status: { type: sequelize.Sequelize.ENUM('active', 'inactive'), defaultValue: 'active' },
description: { type: sequelize.Sequelize.TEXT },
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false }
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false },
});
console.log(' warehouses 表创建成功');
} else {
@@ -680,7 +701,7 @@ async function migrateIdleDeviceAndBusiness() {
businessId: { type: sequelize.Sequelize.STRING, allowNull: false },
isPrimary: { type: sequelize.Sequelize.BOOLEAN, defaultValue: false },
createdAt: { type: sequelize.Sequelize.DATE, allowNull: false },
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false }
updatedAt: { type: sequelize.Sequelize.DATE, allowNull: false },
});
console.log(' device_business 表创建成功');
} else {
+11 -11
View File
@@ -3,10 +3,10 @@
/**
* 命令行恢复脚本
* 用于独立执行数据恢复,支持跨环境迁移
*
*
* 使用方法:
* node scripts/restore.js <backup-file> [options]
*
*
* 选项:
* --skip-users 跳过用户数据恢复
* --skip-files 跳过文件恢复
@@ -76,7 +76,7 @@ async function runRestore() {
console.log('========================================\n');
const backupFile = path.resolve(options.file);
if (!fs.existsSync(backupFile)) {
console.error(`错误: 备份文件不存在: ${backupFile}`);
process.exit(1);
@@ -84,7 +84,7 @@ async function runRestore() {
try {
process.chdir(path.join(__dirname, '..'));
const { restoreBackup, validateBackupFile } = require('../utils/backup');
const { sequelize } = require('../db');
@@ -94,7 +94,7 @@ async function runRestore() {
console.log('验证备份文件...');
const validation = await validateBackupFile(backupFile);
if (!validation.valid) {
console.error(`错误: 备份文件验证失败: ${validation.error}`);
process.exit(1);
@@ -124,17 +124,17 @@ async function runRestore() {
}
console.log('开始恢复数据...\n');
const result = await restoreBackup(backupFile, {
overwriteExisting: options.overwrite,
skipTables,
skipFiles: options.skipFiles,
onProgress: (tableName, status, count) => {
const statusMap = {
'restored': '✓ 已恢复',
'skipped': '○ 已跳过',
'empty': '- 无数据',
'error': '✗ 错误',
restored: '✓ 已恢复',
skipped: '○ 已跳过',
empty: '- 无数据',
error: '✗ 错误',
};
const statusText = statusMap[status] || status;
const countText = count ? ` (${count} 条)` : '';
@@ -162,7 +162,7 @@ async function runRestore() {
}
console.log('\n提示: 请重启后端服务以确保所有数据生效');
await sequelize.close();
process.exit(0);
} catch (error) {
+23 -17
View File
@@ -3,26 +3,32 @@ const { sequelize } = require('../db');
async function updateSystemFields() {
try {
console.log('开始调整系统字段标记...');
// 核心系统字段(数据库必填字段,不可删除)
const coreSystemFields = [
'deviceId', 'name', 'type', 'model', 'serialNumber',
'rackId', 'position', 'height', 'powerConsumption',
'status', 'purchaseDate', 'warrantyExpiry'
'deviceId',
'name',
'type',
'model',
'serialNumber',
'rackId',
'position',
'height',
'powerConsumption',
'status',
'purchaseDate',
'warrantyExpiry',
];
// 可选字段(非系统字段,可删除)
const optionalFields = [
'ipAddress', 'description', 'owner', 'department', 'assetId', 'brand'
];
const optionalFields = ['ipAddress', 'description', 'owner', 'department', 'assetId', 'brand'];
// 先将所有字段设为非系统字段
await sequelize.query(
`UPDATE deviceFields SET isSystem = 0`,
{ type: sequelize.QueryTypes.RAW }
);
await sequelize.query(`UPDATE deviceFields SET isSystem = 0`, {
type: sequelize.QueryTypes.RAW,
});
console.log('已重置所有字段为非系统字段');
// 标记核心系统字段
for (const fieldName of coreSystemFields) {
await sequelize.query(
@@ -31,7 +37,7 @@ async function updateSystemFields() {
);
console.log(`标记为核心系统字段: ${fieldName}`);
}
// 确保可选字段为非系统字段
for (const fieldName of optionalFields) {
await sequelize.query(
@@ -40,11 +46,11 @@ async function updateSystemFields() {
);
console.log(`标记为可选字段: ${fieldName}`);
}
console.log('\n系统字段调整完成!');
console.log('核心系统字段(不可删除):', coreSystemFields.join(', '));
console.log('可选字段(可删除):', optionalFields.join(', '));
process.exit(0);
} catch (error) {
console.error('更新失败:', error);
+17 -7
View File
@@ -3,15 +3,25 @@ const { sequelize } = require('../db');
async function updateSystemFields() {
try {
console.log('开始更新系统字段标记...');
// 系统字段列表
const systemFields = [
'deviceId', 'name', 'type', 'model', 'serialNumber',
'rackId', 'position', 'height', 'powerConsumption',
'status', 'purchaseDate', 'warrantyExpiry',
'ipAddress', 'description'
'deviceId',
'name',
'type',
'model',
'serialNumber',
'rackId',
'position',
'height',
'powerConsumption',
'status',
'purchaseDate',
'warrantyExpiry',
'ipAddress',
'description',
];
// 更新系统字段标记
for (const fieldName of systemFields) {
const [result] = await sequelize.query(
@@ -20,7 +30,7 @@ async function updateSystemFields() {
);
console.log(`标记系统字段: ${fieldName}`);
}
console.log('系统字段标记更新完成');
process.exit(0);
} catch (error) {