+
+ ⚙️
+ Xác nhận bảo trì
+Nội dung xác nhận...
+ +
+
+
+
+ diff --git a/backend/routes/apiRoutes.js b/backend/routes/apiRoutes.js index 42ccc7c..f1480fa 100644 --- a/backend/routes/apiRoutes.js +++ b/backend/routes/apiRoutes.js @@ -27,6 +27,65 @@ const tempDir = path.join(uploadDir, 'temp'); if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true }); +/** + * Hàm bổ trợ: Dọn dẹp dữ liệu mồ côi + * Được gọi tự động khi xóa user hoặc gọi thủ công từ Admin Dashboard + */ +const runOrphanedCleanup = async () => { + const validUserIds = await User.distinct('_id'); + + // 1. Xử lý Scenes mồ côi + const orphanedScenes = await Scene.find({ createdBy: { $nin: validUserIds } }); + const orphanedSceneIds = orphanedScenes.map(s => s._id); + + if (orphanedSceneIds.length > 0) { + // Xóa Hotspots liên quan + await Hotspot.deleteMany({ + $or: [ + { parent_scene_id: { $in: orphanedSceneIds } }, + { target_scene_id: { $in: orphanedSceneIds } } + ] + }); + // Xóa Scenes + await Scene.deleteMany({ _id: { $in: orphanedSceneIds } }); + } + + // 2. Xử lý Assets mồ côi (Không có owner hoặc không gắn vào Scene nào quá 2h) + const usedAssetIds = await Scene.distinct('assetId'); + const safeDate = new Date(Date.now() - 2 * 3600 * 1000); + + const orphanedAssets = await Asset.find({ + $or: [ + { uploadedBy: { $nin: validUserIds } }, + { + $and: [ + { _id: { $nin: usedAssetIds } }, + { createdAt: { $lt: safeDate } } + ] + } + ] + }); + + let deletedFilesCount = 0; + for (const asset of orphanedAssets) { + if (asset.filePath && fs.existsSync(asset.filePath)) { + try { + fs.unlinkSync(asset.filePath); + deletedFilesCount++; + } catch (e) { + console.error(`[Cleanup Error] File: ${asset.filePath}`, e.message); + } + } + await Asset.findByIdAndDelete(asset._id); + } + + return { + scenesDeleted: orphanedSceneIds.length, + assetsDeleted: orphanedAssets.length, + filesRemoved: deletedFilesCount + }; +}; + // Configure Multer for temp uploads const storage = multer.diskStorage({ destination: (req, file, cb) => { @@ -91,6 +150,68 @@ router.post('/admin/backup', protect, async (req, res) => { } }); +/** + * @route GET /api/admin/maintenance/stray-files + * @desc Kiểm tra các file trong thư mục uploads không có bản ghi DB trỏ tới + * @access Private (Admin) + */ +router.get('/admin/maintenance/stray-files', protect, async (req, res) => { + if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') { + return res.status(403).json({ message: 'Bạn không có quyền quản trị' }); + } + try { + // Đọc danh sách file thực tế (bỏ qua thư mục temp và file ẩn) + const filesOnDisk = fs.readdirSync(uploadDir).filter(file => { + const fullPath = path.join(uploadDir, file); + return fs.lstatSync(fullPath).isFile() && !file.startsWith('.'); + }); + + // Lấy danh sách file trong DB + const assets = await Asset.find().select('filePath').lean(); + const dbFileNames = new Set(assets.map(a => path.basename(a.filePath))); + + // Lọc ra các file mồ côi trên đĩa + const strayFiles = filesOnDisk.filter(file => !dbFileNames.has(file)); + + res.json({ + count: strayFiles.length, + files: strayFiles + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +/** + * @route POST /api/admin/maintenance/cleanup + * @desc Kích hoạt dọn dẹp dữ liệu mồ côi thủ công + * @access Private (Admin) + */ +router.post('/admin/maintenance/cleanup', protect, async (req, res) => { + if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') { + return res.status(403).json({ message: 'Forbidden' }); + } + try { + // Nếu có tham số deleteStray=true, xóa luôn các file không có trong DB + if (req.query.deleteStray === 'true') { + const assets = await Asset.find().select('filePath').lean(); + const dbFileNames = new Set(assets.map(a => path.basename(a.filePath))); + const filesOnDisk = fs.readdirSync(uploadDir).filter(f => fs.lstatSync(path.join(uploadDir, f)).isFile() && !f.startsWith('.')); + + filesOnDisk.forEach(file => { + if (!dbFileNames.has(file)) { + try { fs.unlinkSync(path.join(uploadDir, file)); } catch (e) {} + } + }); + } + + const report = await runOrphanedCleanup(); + res.json({ message: 'Quy trình dọn dẹp hoàn tất', report }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + /** * @route POST /api/admin/restore * @desc Khôi phục hệ thống từ file backup.zip @@ -1140,6 +1261,10 @@ router.delete('/admin/users/:id', protect, async (req, res) => { // Lưu ý: Trong thực tế bạn có thể muốn xóa cả các Scene của user này await User.findByIdAndDelete(req.params.id); + + // Tự động dọn dẹp dữ liệu liên quan để tránh rác hệ thống + await runOrphanedCleanup(); + res.json({ message: 'Đã xóa người dùng vĩnh viễn' }); } catch (error) { res.status(500).json({ message: error.message }); diff --git a/frontend/index.html b/frontend/index.html index ee547e7..50dd745 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -138,6 +138,15 @@