2026-04-28 10:57:17 +08:00
|
|
|
/**
|
|
|
|
|
* 数据库外键约束修复迁移脚本
|
|
|
|
|
*
|
|
|
|
|
* 功能:
|
|
|
|
|
* 1. 清理现有孤儿数据
|
|
|
|
|
* 2. 添加外键约束和 onDelete 策略
|
|
|
|
|
* 3. 确保脚本幂等执行
|
|
|
|
|
*
|
|
|
|
|
* 使用方法:
|
|
|
|
|
* node backend/scripts/fix-foreign-keys.js
|
|
|
|
|
*/
|
|
|
|
|
|
2026-04-28 11:23:38 +08:00
|
|
|
require('dotenv').config();
|
|
|
|
|
const { sequelize, dbDialect } = require('../db');
|
|
|
|
|
|
2026-04-28 10:57:17 +08:00
|
|
|
/**
|
|
|
|
|
* 主迁移函数
|
|
|
|
|
*/
|
|
|
|
|
async function migrate() {
|
|
|
|
|
console.log('开始执行外键约束修复迁移...');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 第一步:清理孤儿数据
|
|
|
|
|
await cleanOrphanData();
|
|
|
|
|
|
|
|
|
|
// 第二步:添加外键约束
|
|
|
|
|
await addForeignKeyConstraints();
|
|
|
|
|
|
|
|
|
|
console.log('外键约束修复迁移完成!');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('迁移失败:', error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 清理孤儿数据
|
|
|
|
|
*/
|
|
|
|
|
async function cleanOrphanData() {
|
|
|
|
|
console.log('正在清理孤儿数据...');
|
|
|
|
|
|
|
|
|
|
const transaction = await sequelize.transaction();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 清理 Device 表中无效的 rackId
|
|
|
|
|
const [orphanDevicesWithRack] = await sequelize.query(
|
|
|
|
|
`SELECT COUNT(*) as count FROM devices d
|
|
|
|
|
LEFT JOIN racks r ON d.rackId = r.rackId
|
|
|
|
|
WHERE d.rackId IS NOT NULL AND r.rackId IS NULL`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log(`发现 ${orphanDevicesWithRack[0].count} 条无效 rackId 的设备记录`);
|
|
|
|
|
|
|
|
|
|
await sequelize.query(
|
|
|
|
|
`UPDATE devices SET rackId = NULL
|
|
|
|
|
WHERE rackId IS NOT NULL
|
|
|
|
|
AND rackId NOT IN (SELECT rackId FROM racks WHERE rackId IS NOT NULL)`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 清理 Device 表中无效的 warehouseId
|
|
|
|
|
const [orphanDevicesWithWarehouse] = await sequelize.query(
|
|
|
|
|
`SELECT COUNT(*) as count FROM devices d
|
|
|
|
|
LEFT JOIN warehouses w ON d.warehouseId = w.warehouseId
|
|
|
|
|
WHERE d.warehouseId IS NOT NULL AND w.warehouseId IS NULL`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log(`发现 ${orphanDevicesWithWarehouse[0].count} 条无效 warehouseId 的设备记录`);
|
|
|
|
|
|
|
|
|
|
await sequelize.query(
|
|
|
|
|
`UPDATE devices SET warehouseId = NULL
|
|
|
|
|
WHERE warehouseId IS NOT NULL
|
|
|
|
|
AND warehouseId NOT IN (SELECT warehouseId FROM warehouses WHERE warehouseId IS NOT NULL)`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 清理 Rack 表中无效的 roomId
|
|
|
|
|
const [orphanRacks] = await sequelize.query(
|
|
|
|
|
`SELECT COUNT(*) as count FROM racks r
|
|
|
|
|
LEFT JOIN rooms rm ON r.roomId = rm.roomId
|
|
|
|
|
WHERE r.roomId IS NOT NULL AND rm.roomId IS NULL`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log(`发现 ${orphanRacks[0].count} 条无效 roomId 的机柜记录,准备删除`);
|
|
|
|
|
|
|
|
|
|
await sequelize.query(
|
|
|
|
|
`DELETE FROM racks
|
|
|
|
|
WHERE roomId IS NOT NULL
|
|
|
|
|
AND roomId NOT IN (SELECT roomId FROM rooms WHERE roomId IS NOT NULL)`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 清理 Ticket 表中无效的 deviceId
|
|
|
|
|
const [orphanTicketsWithDevice] = await sequelize.query(
|
|
|
|
|
`SELECT COUNT(*) as count FROM tickets t
|
|
|
|
|
LEFT JOIN devices d ON t.deviceId = d.deviceId
|
|
|
|
|
WHERE t.deviceId IS NOT NULL AND d.deviceId IS NULL`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log(`发现 ${orphanTicketsWithDevice[0].count} 条无效 deviceId 的工单记录`);
|
|
|
|
|
|
|
|
|
|
await sequelize.query(
|
|
|
|
|
`UPDATE tickets SET deviceId = NULL
|
|
|
|
|
WHERE deviceId IS NOT NULL
|
|
|
|
|
AND deviceId NOT IN (SELECT deviceId FROM devices WHERE deviceId IS NOT NULL)`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 清理 Ticket 表中无效的 reporterId (NOT NULL 约束,必须删除)
|
|
|
|
|
const [orphanTicketsWithReporter] = await sequelize.query(
|
|
|
|
|
`SELECT COUNT(*) as count FROM tickets t
|
|
|
|
|
LEFT JOIN users u ON t.reporterId = u.userId
|
|
|
|
|
WHERE t.reporterId IS NOT NULL AND u.userId IS NULL`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log(`发现 ${orphanTicketsWithReporter[0].count} 条无效 reporterId 的工单记录,准备删除`);
|
|
|
|
|
|
|
|
|
|
await sequelize.query(
|
|
|
|
|
`DELETE FROM tickets
|
|
|
|
|
WHERE reporterId IS NOT NULL
|
|
|
|
|
AND reporterId NOT IN (SELECT userId FROM users WHERE userId IS NOT NULL)`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 清理 Ticket 表中无效的 assigneeId (允许 NULL,可以置空)
|
|
|
|
|
const [orphanTicketsWithAssignee] = await sequelize.query(
|
|
|
|
|
`SELECT COUNT(*) as count FROM tickets t
|
|
|
|
|
LEFT JOIN users u ON t.assigneeId = u.userId
|
|
|
|
|
WHERE t.assigneeId IS NOT NULL AND u.userId IS NULL`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log(`发现 ${orphanTicketsWithAssignee[0].count} 条无效 assigneeId 的工单记录`);
|
|
|
|
|
|
|
|
|
|
await sequelize.query(
|
|
|
|
|
`UPDATE tickets SET assigneeId = NULL
|
|
|
|
|
WHERE assigneeId IS NOT NULL
|
|
|
|
|
AND assigneeId NOT IN (SELECT userId FROM users WHERE userId IS NOT NULL)`,
|
|
|
|
|
{ transaction }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await transaction.commit();
|
|
|
|
|
console.log('孤儿数据清理完成!');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 添加外键约束
|
|
|
|
|
*/
|
|
|
|
|
async function addForeignKeyConstraints() {
|
|
|
|
|
console.log('正在添加外键约束...');
|
|
|
|
|
|
|
|
|
|
const queryInterface = sequelize.getQueryInterface();
|
|
|
|
|
const dialect = sequelize.options.dialect;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (dialect === 'mysql' || dialect === 'mariadb') {
|
|
|
|
|
await addMySQLConstraints(queryInterface);
|
|
|
|
|
} else if (dialect === 'sqlite') {
|
|
|
|
|
await addSQLiteConstraints(queryInterface);
|
|
|
|
|
} else {
|
|
|
|
|
console.warn(`数据库类型 ${dialect} 的外键约束需要手动配置`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('外键约束添加完成!');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('添加外键约束失败:', error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 为 MySQL 添加外键约束
|
|
|
|
|
*/
|
|
|
|
|
async function addMySQLConstraints(queryInterface) {
|
|
|
|
|
// 获取当前外键约束
|
|
|
|
|
const [foreignKeys] = await queryInterface.sequelize.query(
|
|
|
|
|
`SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME, DELETE_RULE
|
|
|
|
|
FROM information_schema.KEY_COLUMN_USAGE
|
|
|
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
|
|
|
AND (TABLE_NAME = 'devices' OR TABLE_NAME = 'racks' OR TABLE_NAME = 'tickets')
|
|
|
|
|
AND REFERENCED_TABLE_NAME IS NOT NULL`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log(`当前存在 ${foreignKeys.length} 个外键约束`);
|
|
|
|
|
|
|
|
|
|
// 删除现有的外键约束(如果存在)
|
|
|
|
|
for (const fk of foreignKeys) {
|
|
|
|
|
console.log(`删除现有外键: ${fk.CONSTRAINT_NAME}`);
|
|
|
|
|
await queryInterface.sequelize.query(
|
|
|
|
|
`ALTER TABLE ${fk.TABLE_NAME} DROP FOREIGN KEY ${fk.CONSTRAINT_NAME}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加新的外键约束
|
|
|
|
|
await queryInterface.sequelize.query(`
|
|
|
|
|
ALTER TABLE devices
|
|
|
|
|
ADD CONSTRAINT fk_device_rack
|
|
|
|
|
FOREIGN KEY (rackId) REFERENCES racks(rackId)
|
|
|
|
|
ON DELETE SET NULL
|
|
|
|
|
`);
|
|
|
|
|
console.log('添加外键: Device.rackId → Rack.rackId (ON DELETE SET NULL)');
|
|
|
|
|
|
|
|
|
|
await queryInterface.sequelize.query(`
|
|
|
|
|
ALTER TABLE devices
|
|
|
|
|
ADD CONSTRAINT fk_device_warehouse
|
|
|
|
|
FOREIGN KEY (warehouseId) REFERENCES warehouses(warehouseId)
|
|
|
|
|
ON DELETE SET NULL
|
|
|
|
|
`);
|
|
|
|
|
console.log('添加外键: Device.warehouseId → Warehouse.warehouseId (ON DELETE SET NULL)');
|
|
|
|
|
|
|
|
|
|
await queryInterface.sequelize.query(`
|
|
|
|
|
ALTER TABLE racks
|
|
|
|
|
ADD CONSTRAINT fk_rack_room
|
|
|
|
|
FOREIGN KEY (roomId) REFERENCES rooms(roomId)
|
|
|
|
|
ON DELETE CASCADE
|
|
|
|
|
`);
|
|
|
|
|
console.log('添加外键: Rack.roomId → Room.roomId (ON DELETE CASCADE)');
|
|
|
|
|
|
|
|
|
|
await queryInterface.sequelize.query(`
|
|
|
|
|
ALTER TABLE tickets
|
|
|
|
|
ADD CONSTRAINT fk_ticket_device
|
|
|
|
|
FOREIGN KEY (deviceId) REFERENCES devices(deviceId)
|
|
|
|
|
ON DELETE SET NULL
|
|
|
|
|
`);
|
|
|
|
|
console.log('添加外键: Ticket.deviceId → Device.deviceId (ON DELETE SET NULL)');
|
|
|
|
|
|
|
|
|
|
await queryInterface.sequelize.query(`
|
|
|
|
|
ALTER TABLE tickets
|
|
|
|
|
ADD CONSTRAINT fk_ticket_reporter
|
|
|
|
|
FOREIGN KEY (reporterId) REFERENCES users(userId)
|
|
|
|
|
ON DELETE SET NULL
|
|
|
|
|
`);
|
|
|
|
|
console.log('添加外键: Ticket.reporterId → User.userId (ON DELETE SET NULL)');
|
|
|
|
|
|
|
|
|
|
await queryInterface.sequelize.query(`
|
|
|
|
|
ALTER TABLE tickets
|
|
|
|
|
ADD CONSTRAINT fk_ticket_assignee
|
|
|
|
|
FOREIGN KEY (assigneeId) REFERENCES users(userId)
|
|
|
|
|
ON DELETE SET NULL
|
|
|
|
|
`);
|
|
|
|
|
console.log('添加外键: Ticket.assigneeId → User.userId (ON DELETE SET NULL)');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 为 SQLite 添加外键约束
|
|
|
|
|
* 注意:SQLite 不支持直接 ALTER TABLE 添加外键,需要重建表
|
|
|
|
|
*/
|
|
|
|
|
async function addSQLiteConstraints(queryInterface) {
|
|
|
|
|
console.log('SQLite 数据库:外键约束通过 Sequelize 同步自动创建');
|
|
|
|
|
console.log('注意:SQLite 默认不启用外键检查,需在连接配置中启用');
|
|
|
|
|
|
|
|
|
|
// SQLite 不需要手动添加外键,Sequelize 的 sync 会自动处理
|
|
|
|
|
// 但需要确保在连接时启用外键支持
|
|
|
|
|
await queryInterface.sequelize.query('PRAGMA foreign_keys = ON');
|
|
|
|
|
console.log('已启用 SQLite 外键检查');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 执行迁移
|
|
|
|
|
migrate()
|
|
|
|
|
.then(() => {
|
|
|
|
|
console.log('迁移成功完成');
|
|
|
|
|
process.exit(0);
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error('迁移失败:', error);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|