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

This commit is contained in:
zhang1106
2026-03-05 10:07:52 +08:00
parent 03d69456c0
commit 0917b618be
+149 -173
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,155 +193,53 @@ 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();
if (dialect === 'sqlite') {
const fks = await sequelize.query( const fks = await sequelize.query(
`PRAGMA foreign_key_list(consumable_logs);`, `PRAGMA foreign_key_list(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT } { type: sequelize.QueryTypes.SELECT }
); );
if (!fks || fks.length === 0) { if (!fks || fks.length === 0) {
return; // 没有外键约束 return;
} }
// 重建表(去掉外键约束)
await sequelize.query(` await sequelize.query(`
CREATE TABLE consumable_logs_new ( CREATE TABLE consumable_logs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -316,7 +265,6 @@ async function removeConsumableLogFK() {
) )
`); `);
// 复制数据
await sequelize.query(` await sequelize.query(`
INSERT INTO consumable_logs_new INSERT INTO consumable_logs_new
SELECT id, consumableId, consumableName, operationType, quantity, SELECT id, consumableId, consumableName, operationType, quantity,
@@ -326,77 +274,105 @@ async function removeConsumableLogFK() {
FROM consumable_logs FROM consumable_logs
`); `);
// 删除旧表,重命名新表
await sequelize.query(`DROP TABLE consumable_logs`); await sequelize.query(`DROP TABLE consumable_logs`);
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO 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_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_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_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)`);
}
} }
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();
if (dialect === 'sqlite') {
await sequelize.query(`CREATE INDEX idx_archive_consumable_id ON consumable_log_archives(consumableId)`); 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_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_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 { } else {
await sequelize.query(`ALTER TABLE ${table} ADD COLUMN snList JSON DEFAULT '[]'`); console.log(` ${table} 表不存在,跳过`);
}
console.log(` ${table} 表添加 snList 字段成功`);
} else {
console.log(` ${table} 表 snList 字段已存在,跳过`);
} }
} }
} }