chore(scripts): 归档旧迁移脚本并添加迁移汇总脚本

将多个单独的数据库迁移脚本归档到archive目录
添加migrate-all.js汇总脚本统一执行所有迁移
This commit is contained in:
zhang1106
2026-02-24 13:47:00 +08:00
parent f094778c8d
commit ef03abc92f
8 changed files with 373 additions and 0 deletions
@@ -0,0 +1,119 @@
const { sequelize } = require('../db');
async function migrate() {
try {
console.log('开始迁移:添加 pending 状态到用户表...');
const queryInterface = sequelize.getQueryInterface();
const dialect = sequelize.getDialect();
if (dialect === 'sqlite') {
// SQLite 不支持 ALTER COLUMN,需要重建表
console.log('检测到 SQLite 数据库,使用重建表方式迁移...');
// 1. 创建新表
await queryInterface.createTable('users_new', {
userId: {
type: sequelize.Sequelize.STRING,
primaryKey: true,
allowNull: false
},
username: {
type: sequelize.Sequelize.STRING,
allowNull: false,
unique: true
},
password: {
type: sequelize.Sequelize.STRING,
allowNull: false
},
email: {
type: sequelize.Sequelize.STRING,
allowNull: true
},
phone: {
type: sequelize.Sequelize.STRING,
allowNull: true
},
realName: {
type: sequelize.Sequelize.STRING,
allowNull: true
},
avatar: {
type: sequelize.Sequelize.STRING,
allowNull: true
},
status: {
type: sequelize.Sequelize.ENUM('active', 'inactive', 'locked', 'pending'),
defaultValue: 'active'
},
lastLoginTime: {
type: sequelize.Sequelize.DATE,
allowNull: true
},
lastLoginIp: {
type: sequelize.Sequelize.STRING,
allowNull: true
},
loginCount: {
type: sequelize.Sequelize.INTEGER,
defaultValue: 0
},
remark: {
type: sequelize.Sequelize.TEXT,
allowNull: true
},
createdAt: {
type: sequelize.Sequelize.DATE,
allowNull: false
},
updatedAt: {
type: sequelize.Sequelize.DATE,
allowNull: false
}
});
// 2. 复制数据
await sequelize.query(`
INSERT INTO users_new (
userId, username, password, email, phone, realName, avatar,
status, lastLoginTime, lastLoginIp, loginCount, remark, createdAt, updatedAt
)
SELECT
userId, username, password, email, phone, realName, avatar,
status, lastLoginTime, lastLoginIp, loginCount, remark, createdAt, updatedAt
FROM users
`);
// 3. 删除旧表
await queryInterface.dropTable('users');
// 4. 重命名新表
await queryInterface.renameTable('users_new', 'users');
// 5. 重新创建索引
await queryInterface.addIndex('users', ['status']);
await queryInterface.addIndex('users', ['username']);
await queryInterface.addIndex('users', ['email']);
console.log('SQLite 迁移完成');
} else if (dialect === 'mysql') {
// MySQL 可以直接修改 ENUM
console.log('检测到 MySQL 数据库,直接修改 ENUM...');
await sequelize.query(`
ALTER TABLE users
MODIFY COLUMN status ENUM('active', 'inactive', 'locked', 'pending') DEFAULT 'active'
`);
console.log('MySQL 迁移完成');
} else {
console.log(`不支持的数据库类型: ${dialect},请手动迁移`);
}
console.log('迁移成功完成!');
} catch (error) {
console.error('迁移失败:', error);
process.exit(1);
}
}
migrate();
@@ -0,0 +1,126 @@
/**
* 耗材操作日志归档表迁移脚本
* 创建归档表用于存储被删除耗材的历史操作记录
*/
const { sequelize, DB_TYPE } = require('../db');
async function migrate() {
try {
console.log('开始创建耗材操作日志归档表...');
console.log(`数据库类型: ${DB_TYPE}`);
if (DB_TYPE === 'mysql') {
await migrateMySQL();
} else {
await migrateSQLite();
}
console.log('\n归档表创建完成!');
process.exit(0);
} catch (error) {
console.error('迁移失败:', error);
process.exit(1);
}
}
async function migrateMySQL() {
console.log('\n执行 MySQL 迁移...');
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS consumable_log_archives (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
archiveId VARCHAR(255) NOT NULL UNIQUE COMMENT '归档记录唯一标识',
consumableId VARCHAR(255) NOT NULL COMMENT '被删除的耗材ID',
consumableName VARCHAR(255) NOT NULL COMMENT '耗材名称',
consumableSnapshot JSON DEFAULT NULL COMMENT '耗材快照信息',
totalOperations INTEGER DEFAULT 0 COMMENT '操作记录总数',
firstOperationAt DATETIME COMMENT '首次操作时间',
lastOperationAt DATETIME COMMENT '最后操作时间',
totalInQuantity INTEGER DEFAULT 0 COMMENT '总入库数量',
totalOutQuantity INTEGER DEFAULT 0 COMMENT '总出库数量',
finalStock INTEGER DEFAULT 0 COMMENT '删除时库存',
deletedBy VARCHAR(255) COMMENT '删除人',
deletedAt DATETIME COMMENT '删除时间',
deleteReason VARCHAR(255) COMMENT '删除原因',
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_consumable_id (consumableId),
INDEX idx_archive_id (archiveId),
INDEX idx_deleted_at (deletedAt),
INDEX idx_consumable_deleted (consumableId, deletedAt)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='耗材操作日志归档表'
`);
console.log('✓ 创建归档表成功');
} catch (err) {
if (err.message.includes('already exists')) {
console.log('✓ 归档表已存在');
} else {
throw err;
}
}
}
async function migrateSQLite() {
console.log('\n执行 SQLite 迁移...');
try {
// 检查表是否已存在
const tables = await sequelize.query(
`SELECT name FROM sqlite_master WHERE type='table' AND name='consumable_log_archives'`,
{ type: sequelize.QueryTypes.SELECT }
);
if (tables && tables.length > 0) {
console.log('✓ 归档表已存在');
return;
}
await sequelize.query(`
CREATE TABLE consumable_log_archives (
id INTEGER PRIMARY KEY AUTOINCREMENT,
archiveId VARCHAR(255) NOT NULL UNIQUE,
consumableId VARCHAR(255) NOT NULL,
consumableName VARCHAR(255) NOT NULL,
consumableSnapshot TEXT,
totalOperations INTEGER DEFAULT 0,
firstOperationAt DATETIME,
lastOperationAt DATETIME,
totalInQuantity INTEGER DEFAULT 0,
totalOutQuantity INTEGER DEFAULT 0,
finalStock INTEGER DEFAULT 0,
deletedBy VARCHAR(255),
deletedAt DATETIME,
deleteReason VARCHAR(255),
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('✓ 创建归档表成功');
// 创建索引
const indexes = [
{ 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' }
];
for (const idx of indexes) {
try {
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);
}
}
} catch (err) {
console.error('创建归档表失败:', err);
throw err;
}
}
migrate();
@@ -0,0 +1,167 @@
/**
* 耗材操作日志解耦迁移脚本
* 添加 isConsumableDeleted 和 consumableSnapshot 字段
* 实现耗材删除后日志仍可保留
*/
const { sequelize, DB_TYPE } = require('../db');
const Consumable = require('../models/Consumable');
const ConsumableLog = require('../models/ConsumableLog');
async function migrate() {
try {
console.log('开始迁移耗材操作日志解耦...');
console.log(`数据库类型: ${DB_TYPE}`);
const isMySQL = DB_TYPE === 'mysql';
if (isMySQL) {
await migrateMySQL();
} else {
await migrateSQLite();
}
await updateExistingLogs();
console.log('\n迁移完成!耗材删除后日志将被保留。');
process.exit(0);
} catch (error) {
console.error('迁移失败:', error);
process.exit(1);
}
}
async function migrateMySQL() {
console.log('\n执行 MySQL 迁移...');
try {
await sequelize.query(`
ALTER TABLE consumable_logs
ADD COLUMN isConsumableDeleted TINYINT(1) DEFAULT 0 COMMENT '关联耗材是否已被删除'
`);
console.log('✓ 添加 isConsumableDeleted 字段成功');
} catch (err) {
if (err.message.includes('Duplicate column')) {
console.log('✓ isConsumableDeleted 字段已存在');
} else {
throw err;
}
}
try {
await sequelize.query(`
ALTER TABLE consumable_logs
ADD COLUMN consumableSnapshot JSON DEFAULT NULL COMMENT '耗材快照信息'
`);
console.log('✓ 添加 consumableSnapshot 字段成功');
} catch (err) {
if (err.message.includes('Duplicate column')) {
console.log('✓ consumableSnapshot 字段已存在');
} else {
throw err;
}
}
try {
await sequelize.query(`
CREATE INDEX idx_consumable_logs_is_deleted ON consumable_logs(isConsumableDeleted)
`);
console.log('✓ 创建 isConsumableDeleted 索引成功');
} catch (err) {
if (err.message.includes('Duplicate key name')) {
console.log('✓ isConsumableDeleted 索引已存在');
} else {
console.log('! isConsumableDeleted 索引创建失败:', err.message);
}
}
}
async function migrateSQLite() {
console.log('\n执行 SQLite 迁移...');
try {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN isConsumableDeleted BOOLEAN DEFAULT 0
`);
console.log('✓ 添加 isConsumableDeleted 字段成功');
} catch (err) {
if (err.message.includes('duplicate column name')) {
console.log('✓ isConsumableDeleted 字段已存在');
} else {
throw err;
}
}
try {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN consumableSnapshot TEXT
`);
console.log('✓ 添加 consumableSnapshot 字段成功');
} catch (err) {
if (err.message.includes('duplicate column name')) {
console.log('✓ consumableSnapshot 字段已存在');
} else {
throw err;
}
}
try {
await sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_consumable_logs_is_deleted ON consumable_logs(isConsumableDeleted)
`);
console.log('✓ 创建 isConsumableDeleted 索引成功');
} catch (err) {
console.log('! isConsumableDeleted 索引创建失败:', err.message);
}
}
async function updateExistingLogs() {
console.log('\n更新现有日志数据...');
try {
const consumables = await Consumable.findAll({
attributes: ['consumableId', 'category', 'unit', 'unitPrice', 'supplier', 'location', 'minStock', 'maxStock', 'status']
});
const consumableMap = new Map();
consumables.forEach(c => {
consumableMap.set(c.consumableId, {
category: c.category,
unit: c.unit,
unitPrice: c.unitPrice,
supplier: c.supplier,
location: c.location,
minStock: c.minStock,
maxStock: c.maxStock,
status: c.status
});
});
const logs = await ConsumableLog.findAll({
where: {
consumableSnapshot: null
}
});
let updatedCount = 0;
for (const log of logs) {
const snapshot = consumableMap.get(log.consumableId);
if (snapshot) {
await log.update({ consumableSnapshot: snapshot });
updatedCount++;
}
}
console.log(`✓ 更新了 ${updatedCount} 条日志的快照信息`);
const deletedLogsCount = await ConsumableLog.count({
where: { operationType: 'delete' }
});
console.log(`✓ 当前有 ${deletedLogsCount} 条删除类型日志`);
} catch (error) {
console.log('! 更新现有日志数据时出错:', error.message);
}
}
migrate();
@@ -0,0 +1,118 @@
/**
* 耗材操作日志表结构迁移脚本
* 添加修改记录相关字段
*/
const { sequelize } = require('../db');
async function migrate() {
try {
console.log('开始迁移耗材操作日志表...');
// 检查并添加 isEditable 字段
try {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN isEditable BOOLEAN DEFAULT 1
`);
console.log('✓ 添加 isEditable 字段成功');
} catch (err) {
if (err.message.includes('duplicate column name')) {
console.log('✓ isEditable 字段已存在');
} else {
throw err;
}
}
// 检查并添加 originalLogId 字段
try {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN originalLogId INTEGER
`);
console.log('✓ 添加 originalLogId 字段成功');
} catch (err) {
if (err.message.includes('duplicate column name')) {
console.log('✓ originalLogId 字段已存在');
} else {
throw err;
}
}
// 检查并添加 modifiedBy 字段
try {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN modifiedBy VARCHAR(255)
`);
console.log('✓ 添加 modifiedBy 字段成功');
} catch (err) {
if (err.message.includes('duplicate column name')) {
console.log('✓ modifiedBy 字段已存在');
} else {
throw err;
}
}
// 检查并添加 modifiedAt 字段
try {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN modifiedAt DATETIME
`);
console.log('✓ 添加 modifiedAt 字段成功');
} catch (err) {
if (err.message.includes('duplicate column name')) {
console.log('✓ modifiedAt 字段已存在');
} else {
throw err;
}
}
// 检查并添加 modificationReason 字段
try {
await sequelize.query(`
ALTER TABLE consumable_logs ADD COLUMN modificationReason VARCHAR(255)
`);
console.log('✓ 添加 modificationReason 字段成功');
} catch (err) {
if (err.message.includes('duplicate column name')) {
console.log('✓ modificationReason 字段已存在');
} else {
throw err;
}
}
// 创建索引
try {
await sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_consumable_logs_original_log_id ON consumable_logs(originalLogId)
`);
console.log('✓ 创建 originalLogId 索引成功');
} catch (err) {
console.log('! originalLogId 索引创建失败:', err.message);
}
try {
await sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_consumable_logs_is_editable ON consumable_logs(isEditable)
`);
console.log('✓ 创建 isEditable 索引成功');
} catch (err) {
console.log('! isEditable 索引创建失败:', err.message);
}
// 更新现有记录:将出入库、调整等系统生成的记录标记为不可编辑
const [result] = await sequelize.query(`
UPDATE consumable_logs
SET isEditable = 0
WHERE operationType IN ('in', 'out', 'adjust')
AND (isEditable IS NULL OR isEditable = 1)
`);
console.log(`✓ 更新 ${result.changes || 0} 条系统生成记录为不可编辑状态`);
console.log('\n迁移完成!');
process.exit(0);
} catch (error) {
console.error('迁移失败:', error);
process.exit(1);
}
}
migrate();
@@ -0,0 +1,101 @@
/**
* 耗材管理乐观锁迁移脚本
* 为 consumables 表添加 version 字段
*/
require('dotenv').config();
const { sequelize, dbDialect } = require('../db');
// 从环境变量重新确定数据库类型
const actualDbType = process.env.DB_TYPE || 'sqlite';
async function migrate() {
try {
console.log('开始执行耗材乐观锁迁移...');
console.log('数据库类型:', actualDbType);
if (actualDbType === 'sqlite') {
// SQLite: 检查字段是否存在
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"
);
console.log('version 字段添加成功');
} else {
console.log('version 字段已存在,跳过');
}
// 检查 updatedAt 索引
const indexes = await sequelize.query(
"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)"
);
console.log('updatedAt 索引添加成功');
} else {
console.log('updatedAt 索引已存在,跳过');
}
} else if (actualDbType === 'mysql') {
// MySQL: 检查并添加字段
try {
console.log('添加 version 字段...');
await sequelize.query(
"ALTER TABLE consumables ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'"
);
console.log('version 字段添加成功');
} catch (err) {
if (err.message.includes('Duplicate column')) {
console.log('version 字段已存在,跳过');
} else {
throw err;
}
}
// 添加索引
try {
console.log('添加 updatedAt 索引...');
await sequelize.query(
"CREATE INDEX idx_consumables_updatedAt ON consumables(updatedAt)"
);
console.log('updatedAt 索引添加成功');
} catch (err) {
if (err.message.includes('Duplicate key')) {
console.log('updatedAt 索引已存在,跳过');
} else {
throw err;
}
}
}
// 初始化现有数据的 version 值
console.log('初始化现有数据的 version 值...');
await sequelize.query(
"UPDATE consumables SET version = 0 WHERE version IS NULL"
);
console.log('version 值初始化完成');
console.log('迁移完成!');
process.exit(0);
} catch (error) {
console.error('迁移失败:', error);
process.exit(1);
}
}
migrate();
+170
View File
@@ -0,0 +1,170 @@
const { sequelize } = require('../db');
const NetworkCard = require('../models/NetworkCard');
const DevicePort = require('../models/DevicePort');
const Device = require('../models/Device');
async function migrate() {
console.log('========================================');
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') {
await migrateMySQL();
} else {
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");
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);
console.error('');
console.error('错误详情:', error.stack);
process.exit(1);
} finally {
await sequelize.close();
}
}
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;
}
}
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,
\`device_id\` VARCHAR(255) NOT NULL,
\`name\` VARCHAR(255) NOT NULL,
\`description\` TEXT,
\`slot_number\` INT,
\`port_count\` INT DEFAULT 0,
\`model\` VARCHAR(255),
\`manufacturer\` VARCHAR(255),
\`status\` ENUM('normal', 'warning', 'fault', 'offline') DEFAULT 'normal',
\`created_at\` DATETIME DEFAULT CURRENT_TIMESTAMP,
\`updated_at\` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX \`idx_device_id\` (\`device_id\`),
INDEX \`idx_slot_number\` (\`slot_number\`),
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 }
);
if (columns.length === 0) {
console.log(' → 添加 nic_id 字段...');
await sequelize.query(
'ALTER TABLE `device_ports` ADD COLUMN `nic_id` VARCHAR(255) NULL AFTER `device_id`'
);
} else {
console.log(' → nic_id 字段已存在,跳过');
}
console.log(' → 创建 nic_id 索引...');
try {
await sequelize.query('CREATE INDEX `idx_port_nic_id` ON `device_ports`(`nic_id`)');
} catch (error) {
if (error.message.includes('Duplicate key name')) {
console.log(' → 索引已存在,跳过');
} else {
throw error;
}
}
console.log(' → 同步模型以确保关系正确...');
await NetworkCard.sync({ force: false });
await DevicePort.sync({ force: false });
console.log(' → 添加外键约束...');
try {
await sequelize.query(
'ALTER TABLE `device_ports` ADD CONSTRAINT `fk_port_nic` FOREIGN KEY (`nic_id`) REFERENCES `network_cards`(`nic_id`)'
);
} catch (error) {
if (error.message.includes('Duplicate key name') || error.message.includes('already exists')) {
console.log(' → 外键约束已存在,跳过');
} else {
throw error;
}
}
}
migrate();
@@ -0,0 +1,155 @@
/**
* 移除 consumable_logs 表的外键约束
* 解决删除耗材时日志被级联删除的问题
*/
const { sequelize, DB_TYPE } = require('../db');
async function removeForeignKey() {
try {
console.log('开始移除 consumable_logs 表的外键约束...');
console.log(`数据库类型: ${DB_TYPE}`);
if (DB_TYPE === 'mysql') {
await removeMySQLForeignKey();
} else {
await removeSQLiteForeignKey();
}
console.log('\n外键约束移除完成!');
process.exit(0);
} catch (error) {
console.error('移除外键约束失败:', error);
process.exit(1);
}
}
async function removeMySQLForeignKey() {
console.log('\n执行 MySQL 外键移除...');
// 获取外键名称
const [fks] = await sequelize.query(`
SELECT CONSTRAINT_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_NAME = 'consumable_logs'
AND TABLE_SCHEMA = DATABASE()
AND REFERENCED_TABLE_NAME IS NOT NULL
`);
console.log('发现外键约束:', fks);
for (const fk of fks) {
try {
await sequelize.query(`
ALTER TABLE consumable_logs DROP FOREIGN KEY ${fk.CONSTRAINT_NAME}
`);
console.log(`✓ 移除外键约束: ${fk.CONSTRAINT_NAME}`);
} catch (err) {
console.log(`! 移除外键约束失败: ${fk.CONSTRAINT_NAME}`, err.message);
}
}
}
async function removeSQLiteForeignKey() {
console.log('\n执行 SQLite 外键移除...');
console.log('SQLite 不支持直接删除外键,需要重建表');
// 检查外键是否存在
const fks = await sequelize.query(
`PRAGMA foreign_key_list(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT }
);
if (!fks || fks.length === 0) {
console.log('✓ 没有外键约束需要移除');
return;
}
console.log('发现外键约束:', fks);
// SQLite 不支持 ALTER TABLE DROP FOREIGN KEY,需要重建表
await sequelize.transaction(async (transaction) => {
// 获取表结构
const columns = await sequelize.query(
`PRAGMA table_info(consumable_logs);`,
{ type: sequelize.QueryTypes.SELECT, transaction }
);
console.log('\n当前表列:', columns.map(c => c.name));
// 创建新表(不包含外键约束)
await sequelize.query(`
CREATE TABLE consumable_logs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
consumableId VARCHAR(255) NOT NULL,
consumableName VARCHAR(255) NOT NULL,
operationType TEXT NOT NULL,
quantity INTEGER DEFAULT 0,
previousStock INTEGER NOT NULL,
currentStock INTEGER NOT NULL,
operator VARCHAR(255),
reason VARCHAR(255),
notes TEXT,
relatedId VARCHAR(255),
isEditable TINYINT(1) DEFAULT 1,
originalLogId INTEGER,
modifiedBy VARCHAR(255),
modifiedAt DATETIME,
modificationReason VARCHAR(255),
createdAt DATETIME NOT NULL,
updatedAt DATETIME NOT NULL,
isConsumableDeleted BOOLEAN DEFAULT 0,
consumableSnapshot TEXT
)
`, { transaction });
console.log('✓ 创建新表成功');
// 复制数据
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 }
);
console.log(`✓ 复制了 ${countResult[0].count} 条数据`);
// 删除旧表
await sequelize.query(`DROP TABLE consumable_logs`, { transaction });
console.log('✓ 删除旧表成功');
// 重命名新表
await sequelize.query(`ALTER TABLE consumable_logs_new RENAME TO consumable_logs`, { transaction });
console.log('✓ 重命名新表成功');
// 创建索引
const indexes = [
{ name: 'consumable_logs_consumable_id', fields: ['consumableId'] },
{ name: 'consumable_logs_operation_type', fields: ['operationType'] },
{ name: 'consumable_logs_created_at', fields: ['createdAt'] },
{ 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'] }
];
for (const idx of indexes) {
try {
await sequelize.query(
`CREATE INDEX IF NOT EXISTS ${idx.name} ON consumable_logs(${idx.fields.join(', ')})`,
{ transaction }
);
console.log(`✓ 创建索引: ${idx.name}`);
} catch (err) {
console.log(`! 创建索引失败: ${idx.name}`, err.message);
}
}
});
console.log('\n✓ 表重建完成,外键约束已移除');
}
removeForeignKey();