Исправлена проблема пагинации (добавлена сортировка по id) Добавлена функция получения данных по статьям для sitemap для SEO

This commit is contained in:
anibilag 2025-04-14 22:05:01 +03:00
parent 3a495766ce
commit d3ef4f6f45
6 changed files with 118 additions and 20 deletions

View File

@ -7,7 +7,7 @@ import { AuthRequest } from '../../../middleware/auth';
import { checkPermission } from '../../../utils/permissions';
import { logger } from '../../../config/logger';
import { Article } from "../../../types";
import { uploadBufferToS3 } from "../../../services/s3Service";
import { bucketName, uploadBufferToS3 } from "../../../services/s3Service";
const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp';
@ -286,12 +286,12 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
if (!req.user) {
res.status(401).json({ error: 'Пользователь не вошел в систему' });
return
return;
}
if (!Array.isArray(articles) || articles.length === 0) {
res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' });
return
return;
}
let importedCount = 0;
@ -302,6 +302,7 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
// Шаг 1: Создание статьи
const newArticle = await prisma.article.create({
data: {
importId: article.importId,
title: article.title,
excerpt: article.excerpt,
content: article.content,
@ -336,23 +337,89 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
(contentType.includes('image/') ? `.${contentType.split('/')[1]}` : '');
// Генерация имени файла
const filename = `cover-${Date.now()}${fileExtension}`;
const fileName = `cover-${Date.now()}${fileExtension}`;
const webpFileName = path.basename(fileName, path.extname(fileName)) + '.webp';
// Загрузка буфера в S3
const uploadedUrl = await uploadBufferToS3(
imageBuffer,
folder,
filename,
webpFileName,
contentType,
'original',
'medium',
80
);
// Добавить обложку к статье
await prisma.article.update({
where: { id: newArticle.id },
data: {
coverImage: uploadedUrl
},
});
} catch (imageError) {
console.error(`Ошибка загрузки coverImage для статьи ${article.id}:`, imageError);
}
}
// Шаг 3: Обработка массива images
let updatedContent = newArticle.content;
if (article.images && Array.isArray(article.images) && article.images.length > 0) {
const folder = `articles/${newArticle.id}/images`;
const uploadedImageUrls: string[] = [];
// Загружаем каждое изображение в S3
for (let i = 0; i < article.images.length; i++) {
const imageUrl = article.images[i];
try {
const imageResponse = await axios.get(imageUrl, {responseType: 'arraybuffer'});
const imageBuffer = Buffer.from(imageResponse.data, 'binary');
const contentType = imageResponse.headers['content-type'];
const fileExtension = path.extname(new URL(imageUrl).pathname) ||
(contentType.includes('image/') ? `.${contentType.split('/')[1]}` : '');
const fileName = `image-${Date.now()}-${i}${fileExtension}`;
const webpFileName = path.basename(fileName, path.extname(fileName)) + '.webp';
const uploadedUrl = await uploadBufferToS3(
imageBuffer,
folder,
webpFileName,
contentType,
'medium',
80
);
uploadedImageUrls.push(uploadedUrl);
} catch (imageError) {
console.error(`Ошибка загрузки изображения ${imageUrl} для статьи ${newArticle.id}:`, imageError);
uploadedImageUrls.push(''); // Добавляем пустую строку, чтобы сохранить порядок
}
}
// Заменяем плейсхолдеры {{image1}}, {{image2}} и т.д. на uploadedUrl
updatedContent = newArticle.content;
const hasPlaceholders = uploadedImageUrls.some((_, index) => updatedContent.includes(`{{image${index + 1}}}`));
if (!hasPlaceholders) {
console.warn(`В content статьи ${newArticle.id} отсутствуют плейсхолдеры для изображений`);
}
uploadedImageUrls.forEach((uploadedUrl, index) => {
if (uploadedUrl) {
const placeholder = `{{image${index + 1}}}`;
const imgTag = `<img src="${uploadedUrl}" alt="" scale="0.9" style="transform: scale(0.9)">`;
const regex = new RegExp(placeholder, 'g');
updatedContent = updatedContent.replace(regex, imgTag);
}
});
// Обновляем поле content в базе данных
await prisma.article.update({
where: {id: newArticle.id},
data: {
content: updatedContent,
},
});
}
importedCount++;
} catch (articleError) {
console.error(`Ошибка импорта статьи ${article.id}:`, articleError);
@ -369,4 +436,3 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
await prisma.$disconnect();
}
}

View File

@ -45,7 +45,10 @@ export async function listArticles(req: Request, res: Response) {
},
skip,
take: perPage,
orderBy: { publishedAt: 'desc' },
orderBy: [
{ publishedAt: 'desc' },
{ id: 'desc' }, // вторичный критерий сортировки
]
}),
prisma.article.count({ where }),
]);

View File

@ -0,0 +1,26 @@
import { Request, Response } from 'express';
import { prisma } from '../../../lib/prisma';
export async function sitemapArticles(req: Request, res: Response) {
try {
const articles = await prisma.article.findMany({
select: {
id: true,
publishedAt: true,
},
where: {
isActive: true,
},
take: 10000, // запасной лимит
});
// Формируем ответ
res.json({
articles,
});
} catch (error) {
console.error('Ошибка получения статей для карты сайта:', error);
res.status(500).json({ error: 'Серверная ошибка' });
}
}

View File

@ -1,6 +1,7 @@
import express from 'express';
import { auth } from '../../middleware/auth';
import { searchArticles } from './controllers/search';
import { sitemapArticles } from './controllers/sitemap';
import { listArticles } from './controllers/list';
import {
getArticle,
@ -16,6 +17,7 @@ const router = express.Router();
// Поиск и список routes
router.get('/search', searchArticles);
router.get('/sitemap', sitemapArticles);
router.get('/', listArticles);
// Импорт route

View File

@ -1,4 +1,4 @@
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { v4 as uuidv4 } from 'uuid';
import multer from 'multer';
import sharp from 'sharp';
@ -103,12 +103,12 @@ export const uploadToS3 = async (
/**
* Функция для загрузки буфера в S3
* @param buffer - Буфер для загрузки
* @param folder - Путь к папке в S3
* @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя)
* @param mimetype - MIME тип контента буфера
* @param buffer - Буфер для загрузки
* @param folder - Путь к папке в S3
* @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя)
* @param mimetype - MIME тип контента буфера
* @param resolutionId - Опциональный идентификатор разрешения изображения
* @param quality - Отцтональное значение качества при оптимизации
* @param quality - Опциональное значение качества при оптимизации
* @returns URL загрущенного файла
*/
export const uploadBufferToS3 = async (
@ -149,18 +149,18 @@ export const uploadBufferToS3 = async (
})
);
// Возвращаем URL файла
const fileUrl = `https://${bucketName}.s3.regru.cloud/${finalFilename}`;
logger.info(`Buffer successfully uploaded to S3: ${key}`);
// Возвращаем URL файла с заменой расширения
const fileUrl = `https://${bucketName}.s3.regru.cloud/${key}`;
logger.info(`Буффер успешно загружен в S3: ${key}`);
return fileUrl;
} catch (error) {
logger.error('Error uploading buffer to S3');
logger.error('Ошибка заргузки буффера в S3');
console.error(error);
throw new Error('Error uploading buffer to S3');
throw new Error('Ошибка заргузки буффера в S3');
}
};
// Функция получения пасширения файла из MIME типа
// Функция получения расширения файла из MIME типа
function getExtensionFromMimeType(mimetype: string): string {
const mimeToExt: Record<string, string> = {
'image/jpeg': '.jpg',

View File

@ -8,6 +8,7 @@ export interface Article {
cityId: number;
author: Author;
coverImage: string;
images?: string[];
gallery?: GalleryImage[];
publishedAt: string;
readTime: number;