diff --git a/backend/routes/devicePorts.js b/backend/routes/devicePorts.js index fc0d56a..2e388ac 100644 --- a/backend/routes/devicePorts.js +++ b/backend/routes/devicePorts.js @@ -304,7 +304,20 @@ router.post('/batch', async (req, res) => { router.put('/:portId', async (req, res) => { try { - const [updated] = await DevicePort.update(req.body, { + // 白名单过滤:只允许更新安全字段 + const ALLOWED_FIELDS = ['portName', 'portType', 'portSpeed', 'status', 'vlanId', 'description', 'connectedDevice', 'macAddress']; + const updateData = {}; + ALLOWED_FIELDS.forEach(field => { + if (req.body[field] !== undefined) { + updateData[field] = req.body[field]; + } + }); + + if (Object.keys(updateData).length === 0) { + return res.status(400).json({ error: '没有可更新的字段' }); + } + + const [updated] = await DevicePort.update(updateData, { where: { portId: req.params.portId }, }); diff --git a/backend/routes/devices.js b/backend/routes/devices.js index 2f1abdd..b2e3525 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -520,7 +520,9 @@ router.post('/import-preview', async (req, res) => { router.get('/', validateQuery(queryDeviceSchema), async (req, res) => { try { - const { keyword, status, type, rackId, roomId, page = 1, pageSize = 10, isIdle } = req.query; + const { keyword, status, type, rackId, roomId, isIdle } = req.query; + const page = Math.max(1, parseInt(req.query.page, 10) || 1); + const pageSize = Math.min(100, Math.max(1, parseInt(req.query.pageSize, 10) || 10)); const offset = (page - 1) * pageSize; // 构建查询条件 @@ -1078,8 +1080,12 @@ router.post('/import', async (req, res) => { fs.mkdirSync(path.join(__dirname, '../temp')); } - // 保存上传的文件 - const filePath = path.join(__dirname, '../temp', csvFile.name); + // 保存上传的文件(使用 path.basename 防止路径遍历) + const safeFileName = path.basename(csvFile.name); + if (!safeFileName || safeFileName.startsWith('.') || safeFileName !== csvFile.name) { + return res.status(400).json({ error: '文件名不合法' }); + } + const filePath = path.join(__dirname, '../temp', safeFileName); await csvFile.mv(filePath); // 读取并解析CSV文件(GBK编码) diff --git a/backend/routes/inventory.js b/backend/routes/inventory.js index 3ba9021..5bb9258 100644 --- a/backend/routes/inventory.js +++ b/backend/routes/inventory.js @@ -150,9 +150,9 @@ router.put('/plans/:planId', async (req, res) => { type: type || plan.type, description: description !== undefined ? description : plan.description, scheduledDate: scheduledDate ? new Date(scheduledDate) : plan.scheduledDate, - targetRooms: targetRooms || plan.targetRooms, - targetRacks: targetRacks || plan.targetRacks, - status: status || plan.status, + targetRooms: Array.isArray(targetRooms) ? targetRooms : plan.targetRooms, + targetRacks: Array.isArray(targetRacks) ? targetRacks : plan.targetRacks, + status: status !== undefined ? status : plan.status, }); res.json(plan); diff --git a/backend/routes/operationLogs.js b/backend/routes/operationLogs.js index e480d91..af139b7 100644 --- a/backend/routes/operationLogs.js +++ b/backend/routes/operationLogs.js @@ -183,11 +183,11 @@ router.get('/statistics', authMiddleware, async (req, res) => { OperationLog.findAll({ where, attributes: [ - [sequelize.fn('DATE', sequelize.col('createdAt')), 'date'], + [sequelize.literal("strftime('%Y-%m-%d', \"createdAt\")"), 'date'], [sequelize.fn('COUNT', '*'), 'count'], ], - group: [sequelize.fn('DATE', sequelize.col('createdAt'))], - order: [[sequelize.fn('DATE', sequelize.col('createdAt')), 'DESC']], + group: [sequelize.literal("strftime('%Y-%m-%d', \"createdAt\")")], + order: [[sequelize.literal("strftime('%Y-%m-%d', \"createdAt\")"), 'DESC']], limit: 30, }), ]); diff --git a/backend/routes/statistics.js b/backend/routes/statistics.js index 67e8d11..c27990d 100644 --- a/backend/routes/statistics.js +++ b/backend/routes/statistics.js @@ -130,7 +130,7 @@ router.get('/', async (req, res) => { } const onlineRate = - totalDevices > 0 ? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1) : 100; + totalDevices > 0 ? ((runningDevices / totalDevices) * 100).toFixed(1) : 100; const totalRooms = rooms.length; diff --git a/backend/routes/systemSettings.js b/backend/routes/systemSettings.js index 6c67987..35c90a1 100644 --- a/backend/routes/systemSettings.js +++ b/backend/routes/systemSettings.js @@ -350,7 +350,7 @@ router.put('/:key', async (req, res) => { return res.status(400).json({ error: '值必须是有效的数字' }); } } else if (setting.settingType === 'boolean') { - parsedValue = Boolean(value); + parsedValue = value === true || value === 'true' || value === '1' || value === 1; } await setting.update({ diff --git a/backend/routes/tickets.js b/backend/routes/tickets.js index def463a..a27abd7 100644 --- a/backend/routes/tickets.js +++ b/backend/routes/tickets.js @@ -390,6 +390,14 @@ router.get('/export', async (req, res) => { order: [['createdAt', 'DESC']], }); + // 收集所有工单的自定义字段 key,确保导出列完整 + const allCustomKeys = new Set(); + tickets.forEach(ticket => { + if (ticket.metadata && typeof ticket.metadata === 'object') { + Object.keys(ticket.metadata).forEach(key => allCustomKeys.add(key)); + } + }); + const exportData = tickets.map(ticket => { const item = {}; TICKET_EXPORT_FIELDS.forEach(({ fieldName, displayName }) => { @@ -418,12 +426,11 @@ router.get('/export', async (req, res) => { item[displayName] = value !== null && value !== undefined ? String(value) : ''; }); - if (ticket.metadata && typeof ticket.metadata === 'object') { - Object.entries(ticket.metadata).forEach(([key, val]) => { - const customDisplayName = key; - item[customDisplayName] = val !== null && val !== undefined ? String(val) : ''; - }); - } + // 填充所有自定义字段(缺失的填空字符串,保证每行列数一致) + allCustomKeys.forEach(key => { + const val = ticket.metadata && ticket.metadata[key]; + item[key] = val !== null && val !== undefined ? String(val) : ''; + }); return item; }); @@ -857,6 +864,10 @@ router.post('/:ticketId/evaluate', async (req, res) => { return res.status(404).json({ error: '工单不存在' }); } + if (ticket.status !== 'completed') { + return res.status(400).json({ error: '只有已完成的工单才能评价' }); + } + await ticket.update({ evaluation, evaluationRating }); await TicketOperationRecord.create({ diff --git a/backend/routes/users.js b/backend/routes/users.js index e8c61c1..531ef30 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -433,6 +433,14 @@ router.put('/:userId/password', authMiddleware, async (req, res) => { }); } + // 权限检查:只能重置自己的密码,或管理员重置他人密码 + if (req.user.userId !== user.userId && req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + message: '无权重置其他用户的密码', + }); + } + if (!newPassword || newPassword.length < PASSWORD_MIN_LENGTH) { return res.status(400).json({ success: false, diff --git a/backend/server.js b/backend/server.js index 3132f96..4bfefef 100644 --- a/backend/server.js +++ b/backend/server.js @@ -93,10 +93,18 @@ async function syncBusinessModels() { const DeviceBusiness = require('./models/DeviceBusiness'); const Warehouse = require('./models/Warehouse'); - await Business.sync({ alter: true }); - await DeviceBusiness.sync({ alter: true }); - await Warehouse.sync({ alter: true }); - console.log('业务/设备关联/库房模型同步完成'); + // 仅在开发环境使用 sync({ alter: true }),生产环境应使用 migrations + if (process.env.NODE_ENV !== 'production') { + await Business.sync({ alter: true }); + await DeviceBusiness.sync({ alter: true }); + await Warehouse.sync({ alter: true }); + console.log('业务/设备关联/库房模型同步完成(alter mode)'); + } else { + await Business.sync(); + await DeviceBusiness.sync(); + await Warehouse.sync(); + console.log('业务/设备关联/库房模型同步完成(safe mode)'); + } } async function initDefaultSystemSettings() { diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 6cd1d42..ba2335d 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -75,7 +75,12 @@ api.interceptors.response.use( if (!currentPath.startsWith('/login')) { secureStorage.remove(TOKEN_KEY); secureStorage.remove('user'); - window.location.href = '/login'; + // 使用 React Router 导航而非强制刷新,保留 SPA 状态 + if (window.__navigate) { + window.__navigate('/login'); + } else { + window.location.href = '/login'; + } } } diff --git a/frontend/src/components/3d/Scene.jsx b/frontend/src/components/3d/Scene.jsx index 4b4d5a8..a0a22dd 100644 --- a/frontend/src/components/3d/Scene.jsx +++ b/frontend/src/components/3d/Scene.jsx @@ -62,11 +62,16 @@ const Controls = ({ rack, onControlsReady }) => { } }, [controlsRef.current, camera, initialCameraPosition, fixedTarget, onControlsReady]); + const prevTargetRef = React.useRef(null); + useFrame(() => { if (controlsRef.current) { - // 强制保持 target 在机柜中轴线 - controlsRef.current.target.copy(fixedTarget); - controlsRef.current.update(); + // 仅在 target 变化时更新,避免每帧执行矩阵计算 + if (prevTargetRef.current !== fixedTarget) { + controlsRef.current.target.copy(fixedTarget); + controlsRef.current.update(); + prevTargetRef.current = fixedTarget; + } } }); diff --git a/frontend/vite.config.mjs b/frontend/vite.config.mjs index b1a2744..f15390a 100644 --- a/frontend/vite.config.mjs +++ b/frontend/vite.config.mjs @@ -3,8 +3,6 @@ import react from '@vitejs/plugin-react'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import Components from 'unplugin-vue-components/vite'; -import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -37,14 +35,6 @@ const port = getFrontendPort(); export default defineConfig({ plugins: [ react(), - Components({ - resolvers: [ - AntDesignVueResolver({ - importStyle: false, - }), - ], - dts: false, - }), ], assetsInclude: ['**/*.hdr', '**/*.woff2'], server: {