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 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') => { const scene = await Scene.findById(rootSceneId); if (!scene) return { deletedCount: 0 }; const sceneId = rootSceneId.toString(); const scenesToDelete = [sceneId]; // 1. Thu thập Asset ID const assetIds = [scene.assetId].filter(id => id); const assets = await Asset.find({ _id: { $in: assetIds } }); // 2. 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. Dọn dẹp Hotspots (Link đi và Link đến cảnh này) const hotspotCleanup = await Hotspot.deleteMany({ $or: [ { parent_scene_id: sceneId }, { target_scene_id: sceneId } ] }); // 4. Cập nhật Tour cha (Gỡ bỏ reference và cập nhật rootSceneId) if (scene.tourId) { const Tour = require('../models/Tour'); // Tránh dependency vòng const tour = await Tour.findById(scene.tourId); if (tour) { tour.scenes = tour.scenes.filter(id => id.toString() !== sceneId); // Nếu cảnh bị xóa là cảnh khởi đầu, gán lại cảnh đầu tiên còn lại hoặc null if (tour.rootSceneId && tour.rootSceneId.toString() === sceneId) { tour.rootSceneId = tour.scenes.length > 0 ? tour.scenes[0] : null; } await tour.save(); // Cập nhật lại vị trí trung tâm của Tour sau khi một cảnh bị xóa khỏi danh sách const tourController = require('../middlewares/TourController'); if (tourController && tourController.updateTourCenter) { await tourController.updateTourCenter(tour._id); } } } // 5. Xóa bản ghi trong Database await Asset.deleteMany({ _id: { $in: assetIds } }); await Scene.deleteOne({ _id: sceneId }); const sceneName = scene.name || scene.title || 'Chưa đặt tên'; await logActivity('DELETE_SCENE', { message: `Xóa cảnh [${sceneName}] và các tài nguyên liên quan`, sceneId: sceneId, cleanedHotspotsCount: hotspotCleanup.deletedCount }, performer ? performer.toString() : 'System'); return { deletedCount: 1 }; }; /** * 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 của Tour 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 (tourId, privacyData, performer = 'System') => { const { privacy, shareToken, shareTokenExpires, sharedWith, sharedEmails } = privacyData; // 2. Chuẩn bị dữ liệu cập nhật (Chỉ cập nhật Privacy, giữ nguyên tourId) const updateFields = { privacy }; 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; // [BẢO MẬT TUYỆT ĐỐI] Chỉ cập nhật cho các cảnh mang đúng tourId này (Con đẻ). // Các cảnh liên kết chéo mang tourId khác nên sẽ được bảo vệ an toàn. const result = await Scene.updateMany({ tourId: tourId }, updateQuery); await logActivity('PROPAGATE_PRIVACY_BY_TOUR', { tourId, privacy, affectedCount: result.modifiedCount }, performer ? performer.toString() : 'System'); }; module.exports = { deleteSceneCascade, propagateScenePrivacy };