feat: 实现统一日志系统和维护模式功能
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 数据库外键约束修复迁移脚本
|
||||
*
|
||||
* 功能:
|
||||
* 1. 清理现有孤儿数据
|
||||
* 2. 添加外键约束和 onDelete 策略
|
||||
* 3. 确保脚本幂等执行
|
||||
*
|
||||
* 使用方法:
|
||||
* node backend/scripts/fix-foreign-keys.js
|
||||
*/
|
||||
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
/**
|
||||
* 主迁移函数
|
||||
*/
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 日志系统迁移脚本
|
||||
* 批量将 routes/ 目录中的 console 调用替换为 logger 调用
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const routesDir = path.join(__dirname, '../routes');
|
||||
const files = fs.readdirSync(routesDir).filter(f => f.endsWith('.js'));
|
||||
|
||||
let totalReplacements = 0;
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(routesDir, file);
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// 检查是否包含 console 调用
|
||||
const consoleMatches = content.match(/console\.(log|error|warn|debug)/g);
|
||||
if (!consoleMatches) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleName = file.replace('.js', '').replace(/^[a-z]/, m => m.toUpperCase()) + 'Route';
|
||||
|
||||
// 添加 logger 导入(如果还没有)
|
||||
if (!content.includes("require('../utils/logger')")) {
|
||||
// 在文件开头添加 logger 导入
|
||||
content = `const logger = require('../utils/logger').module('${moduleName}');\n${content}`;
|
||||
}
|
||||
|
||||
// 替换 console.error('xxx:', error) -> logger.error('xxx', { error: error.message, stack: error.stack })
|
||||
// 替换 console.error('xxx:', error.message) -> logger.error('xxx', { error: error.message })
|
||||
// 替换 console.error('xxx:', err) -> logger.error('xxx', { error: err.message, stack: err.stack })
|
||||
|
||||
// 先处理带有 error 对象的 console.error
|
||||
content = content.replace(
|
||||
/console\.error\(['"`](.+?)['"`],\s*(error|err)\);?/g,
|
||||
(match, msg, varName) => {
|
||||
totalReplacements++;
|
||||
// 清理消息末尾的冒号
|
||||
const cleanMsg = msg.replace(/[::]\s*$/, '').trim();
|
||||
return `logger.error('${cleanMsg}', { ${varName}: ${varName}.message, stack: ${varName}.stack });`;
|
||||
}
|
||||
);
|
||||
|
||||
// 处理 console.error('xxx:', error.message)
|
||||
content = content.replace(
|
||||
/console\.error\(['"`](.+?)['"`],\s*(error|err)\.message\);?/g,
|
||||
(match, msg) => {
|
||||
totalReplacements++;
|
||||
const cleanMsg = msg.replace(/[::]\s*$/, '').trim();
|
||||
return `logger.error('${cleanMsg}', { error: error.message });`;
|
||||
}
|
||||
);
|
||||
|
||||
// 处理 console.error('xxx:', someVar) -> logger.error('xxx', { error: someVar })
|
||||
content = content.replace(
|
||||
/console\.error\(['"`](.+?)['"`],\s*([^);]+)\);?/g,
|
||||
(match, msg, varName) => {
|
||||
totalReplacements++;
|
||||
const cleanMsg = msg.replace(/[::]\s*$/, '').trim();
|
||||
return `logger.error('${cleanMsg}', { error: ${varName.trim()} });`;
|
||||
}
|
||||
);
|
||||
|
||||
// 处理 console.error('xxx') -> logger.error('xxx')
|
||||
content = content.replace(
|
||||
/console\.error\(['"`](.+?)['"`]\);?/g,
|
||||
(match, msg) => {
|
||||
totalReplacements++;
|
||||
return `logger.error('${msg}');`;
|
||||
}
|
||||
);
|
||||
|
||||
// 处理 console.warn('xxx:', { ... }) -> logger.warn('xxx', { ... })
|
||||
content = content.replace(
|
||||
/console\.warn\(['"`](.+?)['"`],\s*\{/g,
|
||||
(match, msg) => {
|
||||
totalReplacements++;
|
||||
const cleanMsg = msg.replace(/[::]\s*$/, '').trim();
|
||||
return `logger.warn('${cleanMsg}', {`;
|
||||
}
|
||||
);
|
||||
|
||||
// 处理 console.warn('xxx') -> logger.warn('xxx')
|
||||
content = content.replace(
|
||||
/console\.warn\(['"`](.+?)['"`]\);?/g,
|
||||
(match, msg) => {
|
||||
totalReplacements++;
|
||||
return `logger.warn('${msg}');`;
|
||||
}
|
||||
);
|
||||
|
||||
// 处理 console.log('xxx:', var) -> logger.info('xxx', { var })
|
||||
content = content.replace(
|
||||
/console\.log\(['"`](.+?)['"`],\s*([^);]+)\);?/g,
|
||||
(match, msg, varName) => {
|
||||
totalReplacements++;
|
||||
const cleanMsg = msg.replace(/[::]\s*$/, '').trim();
|
||||
return `logger.info('${cleanMsg}', { data: ${varName.trim()} });`;
|
||||
}
|
||||
);
|
||||
|
||||
// 处理 console.log('xxx') -> logger.info('xxx')
|
||||
content = content.replace(
|
||||
/console\.log\(['"`](.+?)['"`]\);?/g,
|
||||
(match, msg) => {
|
||||
totalReplacements++;
|
||||
return `logger.info('${msg}');`;
|
||||
}
|
||||
);
|
||||
|
||||
// 处理 console.log(`xxx`) -> logger.info(`xxx`)
|
||||
content = content.replace(
|
||||
/console\.log\(`(.+?)`\);?/g,
|
||||
(match, msg) => {
|
||||
totalReplacements++;
|
||||
return `logger.info(\`${msg}\`);`;
|
||||
}
|
||||
);
|
||||
|
||||
// 处理 console.error(`xxx`) -> logger.error(`xxx`)
|
||||
content = content.replace(
|
||||
/console\.error\(`(.+?)`\);?/g,
|
||||
(match, msg) => {
|
||||
totalReplacements++;
|
||||
return `logger.error(\`${msg}\`);`;
|
||||
}
|
||||
);
|
||||
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(`✓ ${file}: 替换了 ${consoleMatches.length} 处 console 调用`);
|
||||
});
|
||||
|
||||
console.log(`\n总计替换了 ${totalReplacements} 处 console 调用`);
|
||||
Reference in New Issue
Block a user