const fs = require('fs'); const Scene = require('../models/Scene'); const Asset = require('../models/Asset'); const Hotspot = require('../models/Hotspot'); const { logActivity } = require('./logger'); /** * Xóa dây chuyền một Scene và tất cả các Scene con liên quan (BFS). * Tuân thủ logic: Xóa cha thì xóa con, xóa con không xóa cha. * @param {string} rootSceneId - ID của Scene gốc cần xóa * @param {string} performer - Tên người thực hiện thao tác * @returns {Promise<{deletedCount: number}>} Số lượng scene đã xóa */ const deleteSceneCascade = async (rootSceneId, performer = 'System') => { // 0. Xác định tourId của scene gốc để thiết lập biên giới xóa const rootScene = await Scene.findById(rootSceneId); if (!rootScene) return { deletedCount: 0 }; const tourId = rootScene.tourId ? rootScene.tourId.toString() : null; const tourIdStr = tourId || rootSceneId.toString(); // [Discovery BFS] Tìm kiếm các cảnh dựa trên liên kết thực tế để xử lý dữ liệu lỗi (Broken Root) let queue = [rootSceneId.toString()]; let scenesToDelete = [rootSceneId.toString()]; const visited = new Set(scenesToDelete); while (queue.length > 0) { const parentId = queue.shift(); const childHotspots = await Hotspot.find({ parent_scene_id: parentId, is_auto_return: { $ne: true } }).populate('target_scene_id', 'tourId'); for (const hs of childHotspots) { if (hs.target_scene_id && typeof hs.target_scene_id === 'object') { const targetScene = hs.target_scene_id; const targetIdStr = targetScene._id.toString(); const targetTourId = targetScene.tourId ? targetScene.tourId.toString() : null; // [Biên giới Tour] Chỉ xóa nếu: // 1. Cùng tourId // 2. Hoặc là Broken Root (tourId tự trỏ về chính nó nhưng lại được liên kết ở đây) // 3. Hoặc là Orphan (không có tourId) const isSameTour = targetTourId === tourIdStr; const isBrokenRoot = targetTourId === targetIdStr; const isOrphan = !targetTourId; if (!visited.has(targetIdStr) && (isSameTour || isBrokenRoot || isOrphan)) { visited.add(targetIdStr); scenesToDelete.push(targetIdStr); queue.push(targetIdStr); } } } } // 1. Dọn dẹp Hotspots (Cả link đi và link trỏ ĐẾN các scene sắp xóa) const hotspotCleanup = await Hotspot.deleteMany({ $or: [ { parent_scene_id: { $in: scenesToDelete } }, { target_scene_id: { $in: scenesToDelete } } ] }); // 2. Thu thập tất cả Asset ID liên quan const scenes = await Scene.find({ _id: { $in: scenesToDelete } }); const assetIds = scenes.map(s => s.assetId).filter(id => id); const assets = await Asset.find({ _id: { $in: assetIds } }); // 3. Xóa tệp tin vật lý trên đĩa (Bất đồng bộ) await Promise.all(assets.map(async asset => { if (asset.filePath) await fs.promises.unlink(asset.filePath).catch(() => {}); })); // 3. Xóa bản ghi trong Database const assetCleanup = await Asset.deleteMany({ _id: { $in: assetIds } }); const sceneCleanup = await Scene.deleteMany({ _id: { $in: scenesToDelete } }); // Chuẩn bị nội dung thông báo cho log const tourName = rootScene.name || rootScene.title || 'Chưa đặt tên'; const childCount = scenesToDelete.length > 0 ? scenesToDelete.length - 1 : 0; // Xác định xem đây là xóa cả Tour hay xóa lẻ const isRootAction = tourIdStr === rootSceneId.toString(); // Ghi log hoạt động xóa chi tiết để dễ dàng truy vết và kiểm tra tính toàn vẹn await logActivity('CASCADE_DELETE_SCENE', { message: isRootAction ? `Xóa trọn bộ Tour [${tourName}] và ${childCount} cảnh con` : `Xóa cảnh lẻ [${tourName}] khỏi Tour`, deletedScenesCount: scenesToDelete.length, cleanedHotspotsCount: hotspotCleanup.deletedCount }, performer ? performer.toString() : 'System'); return { deletedCount: scenesToDelete.length }; }; /** * Lan truyền thiết lập quyền riêng tư cho toàn bộ Tour dựa trên tourId. * Đảm bảo tính nhất quán của toàn bộ Tour khi thay đổi quyền truy cập. * @param {string} rootSceneId - ID của cảnh gốc thực hiện thay đổi * @param {Object} privacyData - Dữ liệu quyền riêng tư mới * @param {string} performer - ID người thực hiện (mặc định là System) */ const propagateScenePrivacy = async (rootSceneId, privacyData, performer = 'System') => { const rootScene = await Scene.findById(rootSceneId); if (!rootScene) return; const tourId = rootScene.tourId || rootScene._id; const tourIdStr = tourId.toString(); // 1. Tìm tất cả cảnh con cần cập nhật bằng BFS để đảm bảo tính "tự chữa lành" cho tourId let queue = [rootSceneId.toString()]; let scenesToUpdate = [rootSceneId.toString()]; const visited = new Set(scenesToUpdate); while (queue.length > 0) { const parentId = queue.shift(); const childHotspots = await Hotspot.find({ parent_scene_id: parentId, is_auto_return: { $ne: true } }).populate('target_scene_id', 'tourId'); for (const hs of childHotspots) { if (hs.target_scene_id && typeof hs.target_scene_id === 'object') { const targetScene = hs.target_scene_id; const targetIdStr = targetScene._id.toString(); const targetTourId = targetScene.tourId ? targetScene.tourId.toString() : null; // Chấp nhận cập nhật nếu là cùng tour hoặc là broken root (tự kế thừa lại tourId đúng) const isBrokenRoot = targetTourId === targetIdStr; const isSameTour = targetTourId === tourIdStr; if (!visited.has(targetIdStr) && (isSameTour || isBrokenRoot || !targetTourId)) { visited.add(targetIdStr); scenesToUpdate.push(targetIdStr); queue.push(targetIdStr); } } } } const { privacy, shareToken, shareTokenExpires, sharedWith, sharedEmails } = privacyData; // 2. Chuẩn bị dữ liệu cập nhật (Luôn đồng bộ tourId để sửa lỗi dữ liệu) const updateFields = { privacy, tourId }; const unsets = {}; if (privacy === 'shared') { if (shareToken) updateFields.shareToken = shareToken; else unsets.shareToken = 1; updateFields.shareTokenExpires = shareTokenExpires || undefined; updateFields.sharedWith = []; updateFields.sharedEmails = []; } else { unsets.shareToken = 1; unsets.shareTokenExpires = 1; if (privacy !== 'member') { updateFields.sharedWith = []; updateFields.sharedEmails = []; } } const updateQuery = { $set: updateFields }; if (Object.keys(unsets).length > 0) updateQuery.$unset = unsets; // 3. Cập nhật dựa trên danh sách ID đã tìm được qua BFS await Scene.updateMany({ _id: { $in: scenesToUpdate } }, updateQuery); await logActivity('PROPAGATE_PRIVACY_BY_TOUR', { tourId, privacy }, performer ? performer.toString() : 'System'); }; module.exports = { deleteSceneCascade, propagateScenePrivacy };