Xóa scene con mà không xóa scene cha

This commit is contained in:
2026-06-09 21:26:47 +07:00
parent d39d3b3d53
commit 67825b04cc
22 changed files with 1185 additions and 1486 deletions
+15
View File
@@ -0,0 +1,15 @@
/**
* Tính toán tọa độ Yaw ngược lại (180 độ) để tạo liên kết quay lại tự động.
* Pannellum sử dụng dải yaw từ -180 đến 180.
* @param {number|string} yaw - Tọa độ yaw hiện tại của điểm đi
* @returns {number} - Tọa độ yaw đối diện cho điểm về
*/
const calculateReverseYaw = (yaw) => {
const numYaw = Number(yaw);
if (isNaN(numYaw)) return 0;
// Logic: Cộng hoặc trừ 180 để đảo ngược hướng nhìn
return numYaw > 0 ? numYaw - 180 : numYaw + 180;
};
module.exports = { calculateReverseYaw };
+37
View File
@@ -0,0 +1,37 @@
const { calculateReverseYaw } = require('../utils/hotspotHelper');
describe('Hotspot Helper - calculateReverseYaw', () => {
test('nên trả về -90 khi yaw là 90 (hướng Đông -> hướng Tây)', () => {
expect(calculateReverseYaw(90)).toBe(-90);
});
test('nên trả về 90 khi yaw là -90 (hướng Tây -> hướng Đông)', () => {
expect(calculateReverseYaw(-90)).toBe(90);
});
test('nên trả về 180 khi yaw là 0 (hướng Bắc -> hướng Nam)', () => {
expect(calculateReverseYaw(0)).toBe(180);
});
test('nên trả về 0 khi yaw là 180 (hướng Nam -> hướng Bắc)', () => {
expect(calculateReverseYaw(180)).toBe(0);
});
test('nên trả về 0 khi yaw là -180', () => {
expect(calculateReverseYaw(-180)).toBe(0);
});
test('nên xử lý chính xác khi đầu vào là chuỗi số', () => {
expect(calculateReverseYaw("45")).toBe(-135);
});
test('nên trả về 0 nếu đầu vào không phải là số hợp lệ', () => {
expect(calculateReverseYaw("invalid")).toBe(0);
expect(calculateReverseYaw(undefined)).toBe(0);
});
test('nên giữ nguyên giá trị với các góc lẻ', () => {
expect(calculateReverseYaw(10.5)).toBe(-169.5);
expect(calculateReverseYaw(-10.5)).toBe(169.5);
});
});
+31
View File
@@ -0,0 +1,31 @@
const fs = require('fs');
const path = require('path');
// Tạo thư mục logs nếu chưa tồn tại
const logDir = path.join(__dirname, '../logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFilePath = path.join(logDir, 'activity.log');
/**
* Ghi log các hoạt động quan trọng vào hệ thống file
* @param {string} action - Tên hành động (vd: DELETE_SCENE, ORPHAN_CLEANUP)
* @param {object} details - Thông tin chi tiết (ID, số lượng...)
* @param {string} performer - Người thực hiện (Username hoặc 'System')
*/
const logActivity = async (action, details, performer = 'System') => {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] [${action.padEnd(20)}] | Performer: ${performer.padEnd(15)} | Details: ${JSON.stringify(details)}\n`;
try {
// Sử dụng appendFile bất đồng bộ để không chặn luồng xử lý chính
await fs.promises.appendFile(logFilePath, logEntry);
} catch (err) {
// Chỉ log ra console nếu việc ghi file thất bại để tránh làm sập app
console.error('[Logger Error]: Không thể ghi log vào file', err);
}
};
module.exports = { logActivity };
+81
View File
@@ -0,0 +1,81 @@
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 };
};
module.exports = { deleteSceneCascade };