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') => { // 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. const childHotspots = await Hotspot.find({ parent_scene_id: parentId, is_auto_return: { $ne: true } }); for (const hs of childHotspots) { if (hs.target_scene_id) { const targetIdStr = hs.target_scene_id.toString(); if (!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ư từ Scene cha xuống TOÀN BỘ các Scene con trong Tour (Đệ quy/BFS). * Đả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} parentSceneId - ID của Scene cha vừa được cập nhật * @param {Object} privacyData - Dữ liệu quyền riêng tư từ Scene cha (privacy, tokens, v.v.) */ const propagateScenePrivacy = async (parentSceneId, privacyData) => { const { privacy, shareToken, shareTokenExpires, sharedWith, sharedEmails } = privacyData; // 1. Tìm tất cả các scene con ở mọi cấp độ bằng thuật toán BFS let queue = [parentSceneId.toString()]; let allChildIds = []; const visited = new Set(queue); while (queue.length > 0) { const currentId = queue.shift(); // Tìm các hotspots xuất phát từ scene hiện tại (bỏ qua link quay lại để tránh vòng lặp) const hotspots = await Hotspot.find({ parent_scene_id: currentId, is_auto_return: { $ne: true } }).select('target_scene_id'); for (const hs of hotspots) { if (hs.target_scene_id) { const targetIdStr = hs.target_scene_id.toString(); if (!visited.has(targetIdStr)) { visited.add(targetIdStr); allChildIds.push(targetIdStr); queue.push(targetIdStr); } } } } if (allChildIds.length === 0) return; // 2. Chuẩn bị dữ liệu cập nhật đồng bộ cho toàn bộ chuỗi const updateFields = { privacy }; const updateQuery = { $set: updateFields }; 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 { if (!updateQuery.$unset) updateQuery.$unset = {}; updateQuery.$unset.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 if (!updateQuery.$unset) updateQuery.$unset = {}; updateQuery.$unset.shareToken = 1; updateQuery.$unset.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 = []; } } // 3. Cập nhật hàng loạt cho tất cả các scene con được tìm thấy await Scene.updateMany( { _id: { $in: allChildIds } }, updateQuery ); await logActivity('PROPAGATE_PRIVACY_DEEP', { parentSceneId, childCount: allChildIds.length, privacy }, 'System'); }; module.exports = { deleteSceneCascade, propagateScenePrivacy };