fix(scripts): 在迁移脚本开头加载环境变量

This commit is contained in:
zhang1106
2026-03-05 10:07:52 +08:00
parent 03d69456c0
commit 0917b618be
+199 -223
View File
@@ -2,11 +2,11 @@
* IDC管理系统 - 数据库迁移汇总脚本 * IDC管理系统 - 数据库迁移汇总脚本
* 按顺序执行所有数据库迁移 * 按顺序执行所有数据库迁移
* 支持幂等执行(重复执行不会出错) * 支持幂等执行(重复执行不会出错)
* 支持 SQLite 和 MySQL
*/ */
const { sequelize, DB_TYPE } = require('../db'); const { sequelize, DB_TYPE } = require('../db');
// 迁移配置列表
const migrations = [ const migrations = [
{ {
name: 'v2.0 - 网卡和端口表', name: 'v2.0 - 网卡和端口表',
@@ -72,7 +72,6 @@ async function runMigrations() {
} catch (error) { } catch (error) {
results.push({ name: migration.name, status: '失败', error: error.message }); results.push({ name: migration.name, status: '失败', error: error.message });
console.error(` ✗ 失败: ${error.message}`); console.error(` ✗ 失败: ${error.message}`);
// 继续执行下一个迁移,不中断
} }
} }
@@ -99,6 +98,59 @@ async function runMigrations() {
process.exit(failCount > 0 ? 1 : 0); process.exit(failCount > 0 ? 1 : 0);
} }
// ==================== 工具函数 ====================
async function getTableColumns(tableName) {
const dialect = sequelize.getDialect();
if (dialect === 'sqlite') {
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 }
);
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=?",
{ replacements: [tableName], type: sequelize.QueryTypes.SELECT }
);
return tables.length > 0;
} else {
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}`;
await sequelize.query(sql);
console.log(` ${tableName} 表添加 ${columnName} 字段成功`);
} else {
console.log(` ${tableName}${columnName} 字段已存在,跳过`);
}
}
// ==================== 迁移函数 ==================== // ==================== 迁移函数 ====================
async function migrateV2() { async function migrateV2() {
@@ -106,8 +158,7 @@ async function migrateV2() {
const dialect = sequelize.getDialect(); const dialect = sequelize.getDialect();
// 1. 创建 network_cards 表 // 1. 创建 network_cards 表
const tables = await queryInterface.showAllTables(); if (!(await tableExists('network_cards'))) {
if (!tables.includes('network_cards')) {
await queryInterface.createTable('network_cards', { await queryInterface.createTable('network_cards', {
id: { id: {
type: sequelize.Sequelize.INTEGER, type: sequelize.Sequelize.INTEGER,
@@ -142,261 +193,186 @@ async function migrateV2() {
} }
// 2. 为 device_ports 添加 nic_id 字段 // 2. 为 device_ports 添加 nic_id 字段
if (dialect === 'sqlite') { if (await tableExists('device_ports')) {
const tableInfo = await sequelize.query( await addColumnIfNotExists('device_ports', 'nic_id', 'INTEGER');
"PRAGMA table_info(device_ports)",
{ type: sequelize.QueryTypes.SELECT }
);
if (!tableInfo.some(col => col.name === 'nic_id')) {
await sequelize.query("ALTER TABLE device_ports ADD COLUMN nic_id INTEGER");
}
} else {
try {
await queryInterface.addColumn('device_ports', 'nic_id', {
type: sequelize.Sequelize.INTEGER
});
} catch (err) {
if (!err.message.includes('Duplicate column')) throw err;
}
} }
} }
async function migratePendingStatus() { async function migratePendingStatus() {
const queryInterface = sequelize.getQueryInterface(); if (await tableExists('users')) {
const dialect = sequelize.getDialect(); await addColumnIfNotExists('users', 'status', "VARCHAR(255) DEFAULT 'active'");
if (dialect === 'sqlite') {
// 检查是否已有 status 字段
const tableInfo = await sequelize.query(
"PRAGMA table_info(users)",
{ type: sequelize.QueryTypes.SELECT }
);
if (!tableInfo.some(col => col.name === 'status')) {
// SQLite 需要重建表
await sequelize.query(`
CREATE TABLE users_new (
userId VARCHAR(255) PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(255),
role VARCHAR(255) DEFAULT 'user',
status VARCHAR(255) DEFAULT 'active',
createdAt DATETIME,
updatedAt DATETIME
)
`);
await sequelize.query(`
INSERT INTO users_new SELECT *, 'active' as status FROM users
`);
await sequelize.query(`DROP TABLE users`);
await sequelize.query(`ALTER TABLE users_new RENAME TO users`);
}
} else {
try {
await queryInterface.addColumn('users', 'status', {
type: sequelize.Sequelize.STRING,
defaultValue: 'active'
});
} catch (err) {
if (!err.message.includes('Duplicate column')) throw err;
}
} }
} }
async function migrateConsumableVersion() { async function migrateConsumableVersion() {
const tableInfo = await sequelize.query( if (await tableExists('consumables')) {
"PRAGMA table_info(consumables)", await addColumnIfNotExists('consumables', 'version', 'INTEGER DEFAULT 0');
{ type: sequelize.QueryTypes.SELECT }
);
if (!tableInfo.some(col => col.name === 'version')) {
await sequelize.query(
"ALTER TABLE consumables ADD COLUMN version INTEGER DEFAULT 0"
);
} }
} }
async function migrateConsumableLogs() { async function migrateConsumableLogs() {
const tableInfo = await sequelize.query( if (await tableExists('consumable_logs')) {
"PRAGMA table_info(consumable_logs)", await addColumnIfNotExists('consumable_logs', 'isEditable', 'BOOLEAN DEFAULT 1');
{ type: sequelize.QueryTypes.SELECT } await addColumnIfNotExists('consumable_logs', 'originalLogId', 'INTEGER');
); await addColumnIfNotExists('consumable_logs', 'modifiedBy', 'VARCHAR(255)');
await addColumnIfNotExists('consumable_logs', 'modifiedAt', 'DATETIME');
const columns = tableInfo.map(col => col.name); await addColumnIfNotExists('consumable_logs', 'modificationReason', 'TEXT');
if (!columns.includes('isEditable')) {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN isEditable BOOLEAN DEFAULT 1
`);
}
if (!columns.includes('originalLogId')) {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN originalLogId INTEGER
`);
}
if (!columns.includes('modifiedBy')) {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN modifiedBy VARCHAR(255)
`);
}
if (!columns.includes('modifiedAt')) {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN modifiedAt DATETIME
`);
}
if (!columns.includes('modificationReason')) {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN modificationReason TEXT
`);
} }
} }
async function migrateConsumableLogDecouple() { async function migrateConsumableLogDecouple() {
const tableInfo = await sequelize.query( if (await tableExists('consumable_logs')) {
"PRAGMA table_info(consumable_logs)", await addColumnIfNotExists('consumable_logs', 'isConsumableDeleted', 'BOOLEAN DEFAULT 0');
{ type: sequelize.QueryTypes.SELECT } await addColumnIfNotExists('consumable_logs', 'consumableSnapshot', 'TEXT');
);
const columns = tableInfo.map(col => col.name);
if (!columns.includes('isConsumableDeleted')) {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN isConsumableDeleted BOOLEAN DEFAULT 0
`);
}
if (!columns.includes('consumableSnapshot')) {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN consumableSnapshot TEXT
`);
} }
} }
async function removeConsumableLogFK() { async function removeConsumableLogFK() {
// SQLite 不支持直接删除外键,需要重建表 const dialect = sequelize.getDialect();
const fks = await sequelize.query(
`PRAGMA foreign_key_list(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT }
);
if (!fks || fks.length === 0) { if (dialect === 'sqlite') {
return; // 没有外键约束 const fks = await sequelize.query(
`PRAGMA foreign_key_list(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT }
);
if (!fks || fks.length === 0) {
return;
}
await sequelize.query(`
CREATE TABLE consumable_logs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
consumableId VARCHAR(255),
consumableName VARCHAR(255),
operationType VARCHAR(50),
quantity INTEGER,
previousStock INTEGER,
currentStock INTEGER,
operator VARCHAR(255),
reason TEXT,
notes TEXT,
isEditable BOOLEAN DEFAULT 1,
originalLogId INTEGER,
modifiedBy VARCHAR(255),
modifiedAt DATETIME,
modificationReason TEXT,
isConsumableDeleted BOOLEAN DEFAULT 0,
consumableSnapshot TEXT,
relatedId VARCHAR(255),
createdAt DATETIME,
updatedAt DATETIME
)
`);
await sequelize.query(`
INSERT INTO consumable_logs_new
SELECT id, consumableId, consumableName, operationType, quantity,
previousStock, currentStock, operator, reason, notes,
isEditable, originalLogId, modifiedBy, modifiedAt, modificationReason,
isConsumableDeleted, consumableSnapshot, relatedId, createdAt, updatedAt
FROM consumable_logs
`);
await sequelize.query(`DROP TABLE consumable_logs`);
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO consumable_logs`);
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 TABLE consumable_logs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
consumableId VARCHAR(255),
consumableName VARCHAR(255),
operationType VARCHAR(50),
quantity INTEGER,
previousStock INTEGER,
currentStock INTEGER,
operator VARCHAR(255),
reason TEXT,
notes TEXT,
isEditable BOOLEAN DEFAULT 1,
originalLogId INTEGER,
modifiedBy VARCHAR(255),
modifiedAt DATETIME,
modificationReason TEXT,
isConsumableDeleted BOOLEAN DEFAULT 0,
consumableSnapshot TEXT,
relatedId VARCHAR(255),
createdAt DATETIME,
updatedAt DATETIME
)
`);
// 复制数据
await sequelize.query(`
INSERT INTO consumable_logs_new
SELECT id, consumableId, consumableName, operationType, quantity,
previousStock, currentStock, operator, reason, notes,
isEditable, originalLogId, modifiedBy, modifiedAt, modificationReason,
isConsumableDeleted, consumableSnapshot, relatedId, createdAt, updatedAt
FROM consumable_logs
`);
// 删除旧表,重命名新表
await sequelize.query(`DROP TABLE consumable_logs`);
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO consumable_logs`);
// 创建索引
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)`);
} }
async function migrateConsumableLogArchive() { async function migrateConsumableLogArchive() {
const tables = await sequelize.getQueryInterface().showAllTables(); if (await tableExists('consumable_log_archives')) {
return;
if (tables.includes('consumable_log_archives')) {
return; // 表已存在
} }
await sequelize.query(` const queryInterface = sequelize.getQueryInterface();
CREATE TABLE consumable_log_archives ( await queryInterface.createTable('consumable_log_archives', {
id INTEGER PRIMARY KEY AUTOINCREMENT, id: {
archiveId VARCHAR(255) NOT NULL UNIQUE, type: sequelize.Sequelize.INTEGER,
consumableId VARCHAR(255) NOT NULL, primaryKey: true,
consumableName VARCHAR(255) NOT NULL, autoIncrement: true
consumableSnapshot TEXT, },
totalOperations INTEGER DEFAULT 0, archiveId: {
firstOperationAt DATETIME, type: sequelize.Sequelize.STRING,
lastOperationAt DATETIME, allowNull: false,
totalInQuantity INTEGER DEFAULT 0, unique: true
totalOutQuantity INTEGER DEFAULT 0, },
finalStock INTEGER DEFAULT 0, consumableId: {
deletedBy VARCHAR(255), type: sequelize.Sequelize.STRING,
deletedAt DATETIME, allowNull: false
deleteReason VARCHAR(255), },
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, consumableName: {
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP type: sequelize.Sequelize.STRING,
) allowNull: false
`); },
consumableSnapshot: {
type: sequelize.Sequelize.TEXT
},
totalOperations: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
},
firstOperationAt: {
type: sequelize.Sequelize.DATE
},
lastOperationAt: {
type: sequelize.Sequelize.DATE
},
totalInQuantity: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
},
totalOutQuantity: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
},
finalStock: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
},
deletedBy: {
type: sequelize.Sequelize.STRING
},
deletedAt: {
type: sequelize.Sequelize.DATE
},
deleteReason: {
type: sequelize.Sequelize.STRING
},
createdAt: {
type: sequelize.Sequelize.DATE,
allowNull: false,
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: sequelize.Sequelize.DATE,
allowNull: false,
defaultValue: sequelize.Sequelize.literal('CURRENT_TIMESTAMP')
}
});
// 创建索引 const dialect = sequelize.getDialect();
await sequelize.query(`CREATE INDEX idx_archive_consumable_id ON consumable_log_archives(consumableId)`); if (dialect === 'sqlite') {
await sequelize.query(`CREATE INDEX idx_archive_archive_id ON consumable_log_archives(archiveId)`); await sequelize.query(`CREATE INDEX idx_archive_consumable_id ON consumable_log_archives(consumableId)`);
await sequelize.query(`CREATE INDEX idx_archive_deleted_at ON consumable_log_archives(deletedAt)`); 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() { async function migrateSnList() {
const dialect = sequelize.getDialect(); const dialect = sequelize.getDialect();
const tables = ['consumables', 'consumable_records', 'consumable_logs']; const tables = ['consumables', 'consumable_records', 'consumable_logs'];
for (const table of tables) { for (const table of tables) {
const tableInfo = await sequelize.query( if (await tableExists(table)) {
dialect === 'sqlite' const columnDef = dialect === 'sqlite' ? "TEXT DEFAULT '[]'" : "JSON DEFAULT '[]'";
? `PRAGMA table_info(${table})` await addColumnIfNotExists(table, 'snList', columnDef);
: `SHOW COLUMNS FROM ${table}`,
{ type: sequelize.QueryTypes.SELECT }
);
const columns = dialect === 'sqlite'
? tableInfo.map(col => col.name)
: tableInfo.map(col => col.Field);
if (!columns.includes('snList')) {
if (dialect === 'sqlite') {
await sequelize.query(`ALTER TABLE ${table} ADD COLUMN snList TEXT DEFAULT '[]'`);
} else {
await sequelize.query(`ALTER TABLE ${table} ADD COLUMN snList JSON DEFAULT '[]'`);
}
console.log(` ${table} 表添加 snList 字段成功`);
} else { } else {
console.log(` ${table} snList 字段已存在,跳过`); console.log(` ${table}存在,跳过`);
} }
} }
} }