feat: 实现统一日志系统和维护模式功能

This commit is contained in:
zhang1106
2026-04-28 10:57:17 +08:00
parent 763e425f78
commit bf2cd221b9
43 changed files with 1966 additions and 430 deletions
+275
View File
@@ -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);
});
+136
View File
@@ -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 调用`);