fix: ảnh phải giữ nguyên hướng chụp

This commit is contained in:
2026-06-20 07:28:28 +07:00
parent def0e0d0f9
commit 4373ed684a
26 changed files with 665 additions and 18 deletions
+239 -1
View File
@@ -1052,6 +1052,8 @@ let TourController = class TourController {
fs.mkdirSync(memberOriginalDir, { recursive: true });
if (!fs.existsSync(tourDisplayPath))
fs.mkdirSync(tourDisplayPath, { recursive: true });
const bodyLat = req.body.latitude ? parseFloat(req.body.latitude) : undefined;
const bodyLng = req.body.longitude ? parseFloat(req.body.longitude) : undefined;
return Promise.all(files.map(async (file) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const originalExtension = path.extname(file.originalname).toLowerCase() || '.jpg';
@@ -1060,6 +1062,28 @@ let TourController = class TourController {
const originalFilePath = path.join(memberOriginalDir, originalFilename);
const displayFilePath = path.join(tourDisplayPath, displayFilename);
await fs.promises.writeFile(originalFilePath, file.buffer);
let lat;
let lng;
try {
const gps = await exifr_1.default.gps(file.buffer);
if (gps && typeof gps.latitude === 'number' && typeof gps.longitude === 'number') {
lat = gps.latitude;
lng = gps.longitude;
}
}
catch (e) {
console.warn('[EXIF GPS] Không thể giải nén GPS từ EXIF ảnh:', e.message);
}
if (lat === undefined || lng === undefined) {
if (bodyLat !== undefined && !isNaN(bodyLat) && bodyLng !== undefined && !isNaN(bodyLng)) {
lat = bodyLat;
lng = bodyLng;
}
}
if (lat === undefined || lng === undefined) {
lat = 10.7769;
lng = 106.7009;
}
let processBuffer = file.buffer;
const isHeic = file.originalname.toLowerCase().endsWith('.heic') || file.originalname.toLowerCase().endsWith('.heif') || file.mimetype === 'image/heic' || file.mimetype === 'image/heif';
if (isHeic) {
@@ -1076,6 +1100,7 @@ let TourController = class TourController {
}
}
await (0, sharp_1.default)(processBuffer)
.rotate()
.resize(2560, 2560, {
fit: 'inside',
withoutEnlargement: true
@@ -1089,6 +1114,10 @@ let TourController = class TourController {
imageUrl: `/uploads/tours/${displayFilename}`,
originalUrl: `/uploads/members/${uploaderId}/originals/${originalFilename}`,
privacy: 'TOUR_ONLY',
metadata: {
lat: lat,
lng: lng
}
}
});
}));
@@ -1624,6 +1653,7 @@ let PhotoController = class PhotoController {
}
}
await (0, sharp_1.default)(processBuffer)
.rotate()
.resize(2560, 2560, { fit: 'inside', withoutEnlargement: true })
.jpeg({ quality: 85 })
.toFile(displayFilePath);
@@ -2041,6 +2071,214 @@ AdminOtpController = __decorate([
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
EmailService, Object])
], AdminOtpController);
let AdminController = class AdminController {
constructor(prisma) {
this.prisma = prisma;
}
async getTrashPhotos() {
const dbPhotos = await this.prisma.photo.findMany({
select: {
id: true,
imageUrl: true,
originalUrl: true,
privacy: true,
tourId: true,
}
});
const dbImageUrls = new Set(dbPhotos.map(p => p.imageUrl).filter(Boolean));
const dbOriginalUrls = new Set(dbPhotos.map(p => p.originalUrl).filter(Boolean));
const allDiskFiles = [];
const getFilesRecursively = (dir) => {
if (!fs.existsSync(dir))
return;
const list = fs.readdirSync(dir);
for (const file of list) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
getFilesRecursively(fullPath);
}
else {
allDiskFiles.push(fullPath);
}
}
};
getFilesRecursively(UPLOAD_ROOT);
const trashPhotos = [];
for (const filePath of allDiskFiles) {
const relativePath = '/uploads' + filePath.substring(UPLOAD_ROOT.length).replace(/\\/g, '/');
if (path.basename(filePath).startsWith('.'))
continue;
const isUsed = dbImageUrls.has(relativePath) || dbOriginalUrls.has(relativePath);
if (!isUsed) {
let size = 0;
try {
size = fs.statSync(filePath).size;
}
catch (e) { }
trashPhotos.push({
filePath: filePath,
url: relativePath,
size: size,
reason: 'Tệp tin không tồn tại trong cơ sở dữ liệu (ảnh mồ côi)',
type: 'file_only'
});
}
}
const orphanedDbPhotos = dbPhotos.filter(p => !p.tourId && p.privacy !== 'PUBLIC');
for (const dbPhoto of orphanedDbPhotos) {
let size = 0;
if (dbPhoto.imageUrl) {
const fullPath = path.join(process.cwd(), dbPhoto.imageUrl.replace(/^\//, ''));
try {
if (fs.existsSync(fullPath)) {
size += fs.statSync(fullPath).size;
}
}
catch (e) { }
}
if (dbPhoto.originalUrl) {
const fullPath = path.join(process.cwd(), dbPhoto.originalUrl.replace(/^\//, ''));
try {
if (fs.existsSync(fullPath)) {
size += fs.statSync(fullPath).size;
}
}
catch (e) { }
}
trashPhotos.push({
id: dbPhoto.id,
url: dbPhoto.imageUrl,
size: size,
reason: 'Bản ghi ảnh riêng tư không gắn với chuyến đi nào',
type: 'db_orphaned'
});
}
return trashPhotos;
}
async cleanTrashPhotos() {
const trashList = await this.getTrashPhotos();
let deletedCount = 0;
let freedSpace = 0;
for (const item of trashList) {
if (item.type === 'file_only') {
if (item.filePath && fs.existsSync(item.filePath)) {
try {
fs.unlinkSync(item.filePath);
deletedCount++;
freedSpace += item.size;
}
catch (e) { }
}
}
else if (item.type === 'db_orphaned') {
const photo = await this.prisma.photo.findUnique({ where: { id: item.id } });
if (photo) {
if (photo.imageUrl) {
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
if (fs.existsSync(displayFilePath)) {
try {
fs.unlinkSync(displayFilePath);
}
catch (e) { }
}
}
if (photo.originalUrl) {
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
if (fs.existsSync(originalFilePath)) {
try {
fs.unlinkSync(originalFilePath);
}
catch (e) { }
}
}
try {
await this.prisma.photo.delete({ where: { id: item.id } });
deletedCount++;
freedSpace += item.size;
}
catch (e) { }
}
}
}
return {
success: true,
message: `Đã dọn dẹp thành công. Đã xóa ${deletedCount} mục, giải phóng ${(freedSpace / (1024 * 1024)).toFixed(2)} MB.`,
deletedCount,
freedSpace
};
}
async deleteSingleTrash(body) {
const { type, filePath, id } = body;
if (type === 'file_only') {
if (!filePath)
throw new common_1.BadRequestException('Đường dẫn tệp tin không hợp lệ');
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(UPLOAD_ROOT)) {
throw new common_1.ForbiddenException('Không được phép xóa tệp tin ngoài thư mục uploads');
}
if (fs.existsSync(resolvedPath)) {
fs.unlinkSync(resolvedPath);
return { success: true, message: 'Đã xóa tệp tin thành công' };
}
else {
throw new common_1.NotFoundException('Tệp tin không tồn tại');
}
}
else if (type === 'db_orphaned') {
if (!id)
throw new common_1.BadRequestException('ID bản ghi không hợp lệ');
const photo = await this.prisma.photo.findUnique({ where: { id } });
if (!photo)
throw new common_1.NotFoundException('Bản ghi không tồn tại trong cơ sở dữ liệu');
if (photo.imageUrl) {
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
if (fs.existsSync(displayFilePath)) {
try {
fs.unlinkSync(displayFilePath);
}
catch (e) { }
}
}
if (photo.originalUrl) {
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
if (fs.existsSync(originalFilePath)) {
try {
fs.unlinkSync(originalFilePath);
}
catch (e) { }
}
}
await this.prisma.photo.delete({ where: { id } });
return { success: true, message: 'Đã xóa bản ghi và các tệp liên quan thành công' };
}
throw new common_1.BadRequestException('Yêu cầu không hợp lệ');
}
};
__decorate([
(0, common_1.Get)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], AdminController.prototype, "getTrashPhotos", null);
__decorate([
(0, common_1.Delete)('clean'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], AdminController.prototype, "cleanTrashPhotos", null);
__decorate([
(0, common_1.Delete)('delete-single'),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], AdminController.prototype, "deleteSingleTrash", null);
AdminController = __decorate([
(0, common_1.Controller)('admin/trash-photos'),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], AdminController);
let PublicPhotoController = class PublicPhotoController {
constructor(prisma, commentGateway) {
this.prisma = prisma;
@@ -2155,7 +2393,7 @@ AppModule = __decorate([
signOptions: { expiresIn: '1d' },
}),
],
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController, PublicPhotoController, AdminOtpController],
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController, PublicPhotoController, AdminOtpController, AdminController],
providers: [prisma_service_1.PrismaService, jwt_strategy_1.JwtStrategy, TourRoleGuard, jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard, CommentGateway, core_2.Reflector, EmailService, config_1.ConfigService],
exports: [prisma_service_1.PrismaService]
})
+1 -1
View File
File diff suppressed because one or more lines are too long