From 5d972799f2fbbc033f0569f0bcffc0f312e4157f Mon Sep 17 00:00:00 2001 From: zhangbing <849185023@qq.com> Date: Sun, 29 Mar 2026 10:40:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E5=86=B2=E7=AA=81=E6=A3=80=E6=B5=8B=E5=92=8C?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E8=99=9A=E6=8B=9F=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为设备表添加复合索引优化位置冲突检测性能 在设备位置检查中添加悲观锁防止竞态条件 添加 react-window 和 react-virtualized-auto-sizer 支持虚拟滚动 --- backend/models/Device.js | 3 +++ backend/routes/devices.js | 4 +++ backend/routes/idleDevices.js | 2 ++ backend/scripts/migrate-all.js | 47 ++++++++++++++++++++++++++++++++++ frontend/package-lock.json | 22 ++++++++++++++++ frontend/package.json | 2 ++ 6 files changed, 80 insertions(+) diff --git a/backend/models/Device.js b/backend/models/Device.js index c3c7c08..6048a3f 100644 --- a/backend/models/Device.js +++ b/backend/models/Device.js @@ -103,6 +103,9 @@ const Device = sequelize.define( { fields: ['createdAt'] }, { fields: ['status', 'type'] }, { fields: ['name'] }, + // 优化位置冲突检测的复合索引 + { fields: ['rackId', 'position'] }, + { fields: ['rackId', 'position', 'isIdle'] }, ], } ); diff --git a/backend/routes/devices.js b/backend/routes/devices.js index 8ad5ee6..fe4ea64 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -63,6 +63,8 @@ async function checkPositionAvailable( if (transaction) { queryOptions.transaction = transaction; + // 使用悲观锁防止竞态条件 + queryOptions.lock = transaction.LOCK ? transaction.LOCK.UPDATE : 'UPDATE'; } const existingDevices = await Device.findAll(queryOptions); @@ -126,6 +128,8 @@ async function checkBatchPositions(rackId, devices, excludeDeviceIds = [], trans if (transaction) { queryOptions.transaction = transaction; + // 使用悲观锁防止竞态条件 + queryOptions.lock = transaction.LOCK ? transaction.LOCK.UPDATE : 'UPDATE'; } const existingDevices = await Device.findAll(queryOptions); diff --git a/backend/routes/idleDevices.js b/backend/routes/idleDevices.js index d5cc5c0..1178fe8 100644 --- a/backend/routes/idleDevices.js +++ b/backend/routes/idleDevices.js @@ -1033,6 +1033,8 @@ async function checkPositionAvailable( if (transaction) { queryOptions.transaction = transaction; + // 使用悲观锁防止竞态条件 + queryOptions.lock = transaction.LOCK ? transaction.LOCK.UPDATE : 'UPDATE'; } const existingDevices = await Device.findAll(queryOptions); diff --git a/backend/scripts/migrate-all.js b/backend/scripts/migrate-all.js index 8b0ca20..5817ce3 100644 --- a/backend/scripts/migrate-all.js +++ b/backend/scripts/migrate-all.js @@ -95,6 +95,11 @@ const migrations = [ description: '为 deviceFields 表添加 isSystem 字段,标记系统字段不可删除', migrate: migrateDeviceFieldsIsSystem, }, + { + name: '设备位置索引优化', + description: '为 devices 表添加复合索引,优化位置冲突检测和悲观锁性能', + migrate: migrateDevicePositionIndexes, + }, ]; async function runMigrations() { @@ -724,6 +729,48 @@ async function migrateIdleDeviceAndBusiness() { } } +async function migrateDevicePositionIndexes() { + const dialect = sequelize.getDialect(); + const tableName = 'devices'; + + if (!(await tableExists(tableName))) { + console.log(` ${tableName} 表不存在,跳过`); + return; + } + + console.log(` → 为 ${tableName} 表添加复合索引 rackId_position...`); + try { + if (dialect === 'sqlite') { + await sequelize.query(`CREATE INDEX IF NOT EXISTS devices_rackId_position ON ${tableName}(rackId, position)`); + } else { + await sequelize.query(`CREATE INDEX IF NOT EXISTS \`devices_rackId_position\` ON \`${tableName}\`(\`rackId\`, \`position\`)`); + } + console.log(' ✓ 索引创建成功'); + } catch (error) { + if (error.message.includes('already exists') || error.message.includes('Duplicate key name')) { + console.log(' → 索引已存在,跳过'); + } else { + throw error; + } + } + + console.log(` → 为 ${tableName} 表添加复合索引 rackId_position_isIdle...`); + try { + if (dialect === 'sqlite') { + await sequelize.query(`CREATE INDEX IF NOT EXISTS devices_rackId_position_isIdle ON ${tableName}(rackId, position, isIdle)`); + } else { + await sequelize.query(`CREATE INDEX IF NOT EXISTS \`devices_rackId_position_isIdle\` ON \`${tableName}\`(\`rackId\`, \`position\`, \`isIdle\`)`); + } + console.log(' ✓ 索引创建成功'); + } catch (error) { + if (error.message.includes('already exists') || error.message.includes('Duplicate key name')) { + console.log(' → 索引已存在,跳过'); + } else { + throw error; + } + } +} + // 执行迁移 runMigrations().catch(error => { console.error('迁移执行失败:', error); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f2976e..b6411b8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,8 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", "react-transition-group": "^4.4.5", + "react-virtualized-auto-sizer": "^2.0.3", + "react-window": "^2.2.7", "reactflow": "^11.11.4", "styled-components": "^6.3.9", "swr": "^2.4.0", @@ -7691,6 +7693,26 @@ } } }, + "node_modules/react-virtualized-auto-sizer": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-2.0.3.tgz", + "integrity": "sha512-nonmCSUIh5HtbzazGcQ1NhnMFps/ZBu/UKJyhCt0Fhi7ondLAUZNETtRCWM8RWYZDzVlMYOQGgBmIxUutIhqgw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-window": { + "version": "2.2.7", + "resolved": "https://registry.npmmirror.com/react-window/-/react-window-2.2.7.tgz", + "integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/reactflow": { "version": "11.11.4", "resolved": "https://registry.npmmirror.com/reactflow/-/reactflow-11.11.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index b8bb655..b715f8b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,8 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", "react-transition-group": "^4.4.5", + "react-virtualized-auto-sizer": "^2.0.3", + "react-window": "^2.2.7", "reactflow": "^11.11.4", "styled-components": "^6.3.9", "swr": "^2.4.0",