153 lines
6.8 KiB
JavaScript
153 lines
6.8 KiB
JavaScript
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 }; |