Fix lỗi set privacy chéo

This commit is contained in:
2026-06-10 15:00:40 +07:00
parent 727bda9b48
commit ec7a9186b6
9 changed files with 190 additions and 76 deletions
+74 -63
View File
@@ -16,23 +16,15 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
const rootScene = await Scene.findById(rootSceneId);
if (!rootScene) return { deletedCount: 0 };
const tourId = rootScene.tourId ? rootScene.tourId.toString() : null;
const tourIdStr = tourId || rootSceneId.toString();
// 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)
// [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();
// 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 }
@@ -44,11 +36,15 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
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)) {
// [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);
@@ -57,6 +53,14 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
}
}
// 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);
@@ -67,27 +71,23 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
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 } }
]
});
// 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', {
rootSceneId,
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 ? performer.toString() : 'System');
return { deletedCount: scenesToDelete.length };
};
@@ -95,51 +95,62 @@ const deleteSceneCascade = async (rootSceneId, performer = 'System') => {
/**
* 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 {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 (tourId, privacyData) => {
if (!tourId) return;
const { privacy, shareToken, shareTokenExpires, sharedWith, sharedEmails } = privacyData;
const propagateScenePrivacy = async (rootSceneId, privacyData, performer = 'System') => {
const rootScene = await Scene.findById(rootSceneId);
if (!rootScene) return;
// 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 = {};
const tourId = rootScene.tourId || rootScene._id;
const tourIdStr = tourId.toString();
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 = [];
// 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);
}
}
}
}
// Xây dựng Query cuối cùng
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 hàng loạt dựa trên tourId (Nhanh và chính xác tuyệt đối)
await Scene.updateMany(
{ tourId: tourId },
updateQuery
);
// 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 }, 'System');
await logActivity('PROPAGATE_PRIVACY_BY_TOUR', { tourId, privacy }, performer ? performer.toString() : 'System');
};
module.exports = { deleteSceneCascade, propagateScenePrivacy };