Chỉnh sửa và tối ưu form đăng nhập và đăng ký

This commit is contained in:
2026-06-09 12:22:29 +07:00
parent 2fba77d50c
commit 9f9c38e6e7
9 changed files with 357 additions and 32 deletions
+1 -1
View File
@@ -641,7 +641,7 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
const processedFilePath = path.join(uploadDir, processedFileName);
await resizeTo8K(req.file.path, processedFilePath);
await injectGPSCoordinates(processedFilePath, scene.lat, scene.lng);
await injectGPSCoordinates(processedFilePath, scene.gps.lat, scene.gps.lng);
const asset = new Asset({
filePath: processedFilePath,
+4 -4
View File
@@ -16,12 +16,12 @@ const imageQueue = new Queue('image-processing', {
backoff: { type: 'exponential', delay: 5000 },
// Tự động dọn dẹp Job để tối ưu bộ nhớ Redis
removeOnComplete: {
age: 3600, // Xóa các job hoàn thành sau 1 giờ (3600 giây)
count: 100 // Hoặc giữ tối đa 100 job hoàn thành gần nhất
age: 1800, // Xóa các job hoàn thành sau 30 phút (1800 giây) để giải phóng RAM nhanh hơn
count: 50 // Chỉ giữ 50 job gần nhất (đủ để xem log gần đây)
},
removeOnFail: {
age: 24 * 3600, // Giữ lại job lỗi trong 24 giờ để admin kiểm tra
count: 500 // Giữ tối đa 500 job lỗi
age: 12 * 3600, // Giữ lại job lỗi trong 12 giờ để admin kiểm tra
count: 100 // Giữ tối đa 100 job lỗi
}
}
});
+17 -1
View File
@@ -1,7 +1,7 @@
const { Worker } = require('bullmq');
const fs = require('fs');
const path = require('path');
const { connection } = require('./imageQueue');
const { imageQueue, connection } = require('./imageQueue');
const { resizeTo8K } = require('../utils/imageHelper');
const { injectGPSCoordinates } = require('../utils/exifHelper');
const Asset = require('../models/Asset');
@@ -51,4 +51,20 @@ imageWorker.on('failed', (job, err) => {
console.error(`Job ${job.id} thất bại sau nhiều lần thử: ${err.message}`);
});
// Khi khởi động Worker, thực hiện dọn dẹp các job "stalled" hoặc dữ liệu rác
// để đảm bảo Redis luôn sạch sẽ khi hệ thống khởi động lại.
imageWorker.on('ready', async () => {
console.log('[Worker] Sẵn sàng xử lý hàng đợi.');
try {
// Xóa các job hoàn thành quá 1 giờ và job thất bại quá 24 giờ
// (Bổ trợ thêm cho cơ chế tự động dọn dẹp của Queue)
await imageQueue.clean(3600000, 1000, 'completed');
await imageQueue.clean(86400000, 1000, 'failed');
console.log('[Worker] Đã dọn dẹp các job cũ không cần thiết trong Redis.');
} catch (err) {
console.error('[Worker Cleanup Error]:', err.message);
}
});
module.exports = imageWorker;
+97
View File
@@ -0,0 +1,97 @@
const mongoose = require('mongoose');
const fs = require('fs');
const path = require('path');
const connectDB = require('../config/db');
const Asset = require('../models/Asset');
const Scene = require('../models/Scene');
/**
* Script kiểm tra tính toàn vẹn dữ liệu
* So sánh sự đồng bộ giữa Database (MongoDB) và Filesystem (thư mục uploads)
*/
const checkIntegrity = async () => {
try {
console.log('=== KHỞI CHẠY KIỂM TRA TÍNH TOÀN VẸN HỆ THỐNG ===');
await connectDB();
const assets = await Asset.find().lean();
const scenes = await Scene.find().lean();
const uploadDir = path.resolve(__dirname, '../uploads');
const tempDir = path.resolve(uploadDir, 'temp');
const dbFilePaths = new Set();
const brokenAssets = [];
const assetIdsInDB = new Set(assets.map(a => a._id.toString()));
console.log(`\n1. Kiểm tra Database -> Filesystem (${assets.length} Assets):`);
for (const asset of assets) {
if (asset.filePath) {
const absolutePath = path.resolve(asset.filePath);
dbFilePaths.add(absolutePath);
if (!fs.existsSync(absolutePath)) {
brokenAssets.push({
id: asset._id,
path: asset.filePath,
user: asset.uploadedBy
});
}
}
}
if (brokenAssets.length > 0) {
console.error(` [!] CẢNH BÁO: Tìm thấy ${brokenAssets.length} bản ghi mất file vật lý:`);
brokenAssets.forEach(ba => console.error(` - Asset ID: ${ba.id} | Path: ${ba.path}`));
} else {
console.log(' [✓] Tất cả bản ghi Asset đều có file vật lý tương ứng.');
}
console.log(`\n2. Kiểm tra Filesystem -> Database:`);
let strayFilesCount = 0;
if (fs.existsSync(uploadDir)) {
const files = fs.readdirSync(uploadDir);
files.forEach(file => {
const fullPath = path.join(uploadDir, file);
// Bỏ qua thư mục temp và các thư mục con khác
if (fs.lstatSync(fullPath).isFile() && !file.startsWith('.')) {
if (!dbFilePaths.has(path.resolve(fullPath))) {
console.warn(` [?] File không có bản ghi DB: ${file}`);
strayFilesCount++;
}
}
});
}
console.log(` [i] Tìm thấy ${strayFilesCount} file mồ côi (không được quản lý bởi Asset).`);
console.log(`\n3. Kiểm tra Liên kết Scene -> Asset (${scenes.length} Scenes):`);
let brokenScenesCount = 0;
for (const scene of scenes) {
const assetId = scene.assetId?.toString();
if (assetId && !assetIdsInDB.has(assetId)) {
console.error(` [!] Scene "${scene.name || scene.title}" (ID: ${scene._id}) trỏ tới Asset ID không tồn tại: ${assetId}`);
brokenScenesCount++;
}
}
if (brokenScenesCount === 0) {
console.log(' [✓] Tất cả các Scene đều liên kết với Asset hợp lệ.');
} else {
console.error(` [!] Tìm thấy ${brokenScenesCount} Scene bị hỏng liên kết.`);
}
console.log('\n=== TỔNG KẾT ===');
console.log(`- Bản ghi Asset lỗi: ${brokenAssets.length}`);
console.log(`- File mồ côi: ${strayFilesCount}`);
console.log(`- Scene hỏng liên kết: ${brokenScenesCount}`);
console.log('================================================');
mongoose.connection.close();
process.exit(0);
} catch (err) {
console.error('Lỗi nghiêm trọng khi kiểm tra:', err.message);
if (mongoose.connection) mongoose.connection.close();
process.exit(1);
}
};
checkIntegrity();
+91
View File
@@ -0,0 +1,91 @@
const mongoose = require('mongoose');
const fs = require('fs');
const path = require('path');
const connectDB = require('../config/db');
const User = require('../models/User');
const Scene = require('../models/Scene');
const Asset = require('../models/Asset');
const Hotspot = require('../models/Hotspot');
/**
* Script dọn dẹp dữ liệu rác (orphaned data)
* Xóa các Scene, Asset và File vật lý của những người dùng đã bị xóa khỏi hệ thống.
* Đồng thời dọn dẹp các Asset "mồ côi" không còn được gắn vào bất kỳ Scene nào.
*/
const cleanup = async () => {
try {
console.log('--- Bắt đầu quy trình dọn dẹp dữ liệu rác ---');
await connectDB();
// 1. Lấy danh sách tất cả ID người dùng hiện đang tồn tại
const validUserIds = await User.distinct('_id');
console.log(`- Tìm thấy ${validUserIds.length} người dùng hợp lệ.`);
// 2. Tìm các Scene mồ côi (Người tạo không còn tồn tại)
const orphanedScenes = await Scene.find({ createdBy: { $nin: validUserIds } });
const orphanedSceneIds = orphanedScenes.map(s => s._id);
console.log(`- Tìm thấy ${orphanedSceneIds.length} cảnh (Scene) mồ côi.`);
// 3. Xóa các Hotspot liên quan đến các Scene mồ côi
// Xóa cả hotspot xuất phát từ và trỏ đến các scene bị xóa
const hotspotResult = await Hotspot.deleteMany({
$or: [
{ parent_scene_id: { $in: orphanedSceneIds } },
{ target_scene_id: { $in: orphanedSceneIds } }
]
});
console.log(`- Đã xóa ${hotspotResult.deletedCount} liên kết Hotspot liên quan.`);
// 4. Xóa các Scene mồ côi trong DB
const sceneResult = await Scene.deleteMany({ _id: { $in: orphanedSceneIds } });
console.log(`- Đã xóa ${sceneResult.deletedCount} bản ghi Scene trong Database.`);
// 5. Tìm các Asset mồ côi (Người upload không tồn tại HOẶC không có Scene nào liên kết)
// Chúng ta lấy danh sách assetId đang được sử dụng bởi các Scene còn lại
const usedAssetIds = await Scene.distinct('assetId');
// Để an toàn, chỉ xóa các Asset không liên kết nếu chúng đã tồn tại hơn 2 giờ
// (Tránh xóa nhầm ảnh đang trong hàng đợi xử lý của Worker)
const safeDate = new Date(Date.now() - 2 * 3600 * 1000);
const orphanedAssets = await Asset.find({
$or: [
{ uploadedBy: { $nin: validUserIds } }, // User đã bị xóa
{
$and: [
{ _id: { $nin: usedAssetIds } }, // Không có scene nào trỏ tới
{ createdAt: { $lt: safeDate } } // Đã quá 2 giờ
]
}
]
});
console.log(`- Tìm thấy ${orphanedAssets.length} ảnh (Asset) mồ côi hoặc không liên kết.`);
// 6. Xóa tệp tin vật lý và bản ghi Asset
let filesDeleted = 0;
for (const asset of orphanedAssets) {
if (asset.filePath && fs.existsSync(asset.filePath)) {
try {
fs.unlinkSync(asset.filePath);
filesDeleted++;
} catch (e) {
console.error(` [Lỗi] Không thể xóa file: ${asset.filePath}`);
}
}
await Asset.findByIdAndDelete(asset._id);
}
console.log(`- Đã dọn dẹp ${filesDeleted} tệp tin vật lý.`);
console.log(`- Đã xóa ${orphanedAssets.length} bản ghi Asset trong Database.`);
console.log('--- Hoàn tất dọn dẹp! Hệ thống đã sạch sẽ. ---');
mongoose.connection.close();
process.exit(0);
} catch (err) {
console.error('Lỗi nghiêm trọng khi dọn dẹp:', err.message);
if (mongoose.connection) mongoose.connection.close();
process.exit(1);
}
};
cleanup();
+36
View File
@@ -0,0 +1,36 @@
const mongoose = require('mongoose');
const connectDB = require('../config/db');
const User = require('../models/User');
const promote = async () => {
const username = process.argv[2]; // Lấy username từ câu lệnh: node promoteAdmin.js <username>
if (!username) {
console.error('Lỗi: Vui lòng cung cấp username. Ví dụ: node promoteAdmin.js locpham');
process.exit(1);
}
try {
await connectDB();
const user = await User.findOneAndUpdate(
{ username: username },
{ $set: { role: 'admin' } },
{ new: true }
);
if (!user) {
console.error(`Không tìm thấy người dùng có tên: ${username}`);
} else {
console.log(`--- THÀNH CÔNG ---`);
console.log(`Người dùng ${user.username} đã được nâng cấp lên quyền: ${user.role}`);
}
mongoose.connection.close();
} catch (err) {
console.error('Lỗi:', err.message);
process.exit(1);
}
};
promote();
+86
View File
@@ -0,0 +1,86 @@
const mongoose = require('mongoose');
const fs = require('fs');
const path = require('path');
const connectDB = require('../config/db');
const User = require('../models/User');
const Asset = require('../models/Asset');
/**
* Hàm định dạng dung lượng file
*/
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* Script thống kê dung lượng lưu trữ theo từng người dùng
*/
const getStorageStats = async () => {
try {
console.log('=== ĐANG TRUY XUẤT THỐNG KÊ DUNG LƯỢNG LƯU TRỮ ===');
await connectDB();
// 1. Lấy tất cả user để map tên
const users = await User.find().select('username email').lean();
const userMap = {};
const stats = {};
users.forEach(u => {
userMap[u._id.toString()] = u.username;
stats[u._id.toString()] = {
username: u.username,
email: u.email,
fileCount: 0,
totalBytes: 0
};
});
// Thêm mục cho các asset không có chủ sở hữu (nếu có)
stats['unknown'] = { username: 'Không xác định', email: 'N/A', fileCount: 0, totalBytes: 0 };
// 2. Lấy tất cả Asset
const assets = await Asset.find().lean();
console.log(`- Đang kiểm tra ${assets.length} tệp tin trong hệ thống...`);
for (const asset of assets) {
const userId = asset.uploadedBy ? asset.uploadedBy.toString() : 'unknown';
if (asset.filePath && fs.existsSync(asset.filePath)) {
const fileStat = fs.statSync(asset.filePath);
if (stats[userId]) {
stats[userId].totalBytes += fileStat.size;
stats[userId].fileCount += 1;
} else {
stats['unknown'].totalBytes += fileStat.size;
stats['unknown'].fileCount += 1;
}
}
}
// 3. Hiển thị kết quả
console.log('\nKết quả thống kê:');
console.log(''.padEnd(60, '-'));
console.log(`${'Username'.padEnd(20)} | ${'Số file'.padEnd(10)} | ${'Dung lượng'}`);
console.log(''.padEnd(60, '-'));
Object.values(stats).forEach(userStat => {
if (userStat.fileCount > 0) {
console.log(`${userStat.username.padEnd(20)} | ${userStat.fileCount.toString().padEnd(10)} | ${formatBytes(userStat.totalBytes)}`);
}
});
console.log(''.padEnd(60, '-'));
mongoose.connection.close();
} catch (err) {
console.error('Lỗi khi thống kê:', err.message);
if (mongoose.connection) mongoose.connection.close();
process.exit(1);
}
};
getStorageStats();