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; // BƯỚC SỬA LỖI QUAN TRỌNG: Xóa toàn bộ "điều hướng" (Hotspots) trỏ ĐẾN scene này. // Đây chính là lệnh "xóa điều hướng" để cô lập scene con khỏi scene cha ngay lập tức. // Nó đảm bảo các scene cha không còn bất kỳ liên kết nào dẫn đến luồng xóa này. await Hotspot.deleteMany({ target_scene_id: rootSceneId }); // 1. Thuật toán BFS để tìm tất cả các scene con (Xóa theo chiều xuôi) let queue = [rootSceneId.toString()]; let scenesToDelete = [rootSceneId.toString()]; const visited = new Set(scenesToDelete); while (queue.length > 0) { const parentId = queue.shift(); // Chỉ tìm các hotspots xuất phát từ scene hiện tại trỏ đến các scene con. // QUAN TRỌNG: Phải loại bỏ các liên kết "Quay lại" (is_auto_return: true) // để tránh việc thuật toán đi ngược lên cảnh cha. // Populate target_scene_id để kiểm tra tourId 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; // [BẢO MẬT] Ngăn chặn xóa dây chuyền sang Tour khác. // Một cảnh chỉ được xóa nếu: // 1. Nó có cùng tourId với cảnh gốc đang bị xóa. // 2. tourId của cả hai phải tồn tại (không null) để tránh xóa nhầm dữ liệu cũ chưa migrate. if (tourId && targetTourId && targetTourId === tourId && !visited.has(targetIdStr)) { visited.add(targetIdStr); scenesToDelete.push(targetIdStr); queue.push(targetIdStr); } } } } // 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(() => {}); })); // 4. Dọn dẹp Database // Xử lý triệt để Dangling Hotspots: // - parent_scene_id in scenesToDelete: Xóa các điểm điều hướng nằm TRONG các scene bị xóa. // - target_scene_id in scenesToDelete: Xóa các điểm điều hướng từ CÁC SCENE KHÁC (cha hoặc hàng xóm) // đang trỏ đến các scene bị xóa. Điều này giúp ngăn chặn lỗi "Broken Link" trong toàn hệ thống. const hotspotCleanup = await Hotspot.deleteMany({ $or: [ { parent_scene_id: { $in: scenesToDelete } }, { target_scene_id: { $in: scenesToDelete } } ] }); const assetCleanup = await Asset.deleteMany({ _id: { $in: assetIds } }); const sceneCleanup = await Scene.deleteMany({ _id: { $in: scenesToDelete } }); // 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', { rootSceneId, deletedScenesCount: scenesToDelete.length, cleanedHotspotsCount: hotspotCleanup.deletedCount }, performer); 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} tourId - ID định danh của Tour (thường là ID của cảnh gốc) * @param {Object} privacyData - Dữ liệu quyền riêng tư mới */ const propagateScenePrivacy = async (tourId, privacyData) => { if (!tourId) return; const { privacy, shareToken, shareTokenExpires, sharedWith, sharedEmails } = privacyData; // 2. Chuẩn bị dữ liệu cập nhật đồng bộ cho toàn bộ chuỗi const updateFields = { privacy, tourId }; // Luôn đồng bộ tourId const unsets = {}; if (privacy === 'shared') { // Chỉ gán nếu có giá trị, nếu không thì xóa hẳn để tránh lỗi Duplicate Key Null if (shareToken) { updateFields.shareToken = shareToken; } else { unsets.shareToken = 1; } updateFields.shareTokenExpires = shareTokenExpires || undefined; updateFields.sharedWith = sharedWith || []; updateFields.sharedEmails = sharedEmails || []; } else { // [BẢO MẬT] Xóa hoàn toàn token cho mọi chế độ không phải 'shared' để tránh lỗi Duplicate Key Null unsets.shareToken = 1; unsets.shareTokenExpires = 1; // Nếu là private hoặc public, xóa luôn danh sách thành viên được chia sẻ if (privacy !== 'member') { updateFields.sharedWith = []; updateFields.sharedEmails = []; } } // Xây dựng Query cuối cùng const updateQuery = { $set: updateFields }; if (Object.keys(unsets).length > 0) updateQuery.$unset = unsets; // 3. Cập nhật hàng loạt dựa trên tourId (Nhanh và chính xác tuyệt đối) await Scene.updateMany( { tourId: tourId }, updateQuery ); await logActivity('PROPAGATE_PRIVACY_BY_TOUR', { tourId, privacy }, 'System'); }; module.exports = { deleteSceneCascade, propagateScenePrivacy };