Исправлена проблема пагинации (добавлена сортировка по 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 { checkPermission } from '../../../utils/permissions';
import { logger } from '../../../config/logger'; import { logger } from '../../../config/logger';
import { Article } from "../../../types"; import { Article } from "../../../types";
import { uploadBufferToS3 } from "../../../services/s3Service"; import { bucketName, uploadBufferToS3 } from "../../../services/s3Service";
const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp'; const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp';
@ -286,12 +286,12 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
if (!req.user) { if (!req.user) {
res.status(401).json({ error: 'Пользователь не вошел в систему' }); res.status(401).json({ error: 'Пользователь не вошел в систему' });
return return;
} }
if (!Array.isArray(articles) || articles.length === 0) { if (!Array.isArray(articles) || articles.length === 0) {
res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' }); res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' });
return return;
} }
let importedCount = 0; let importedCount = 0;
@ -302,6 +302,7 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
// Шаг 1: Создание статьи // Шаг 1: Создание статьи
const newArticle = await prisma.article.create({ const newArticle = await prisma.article.create({
data: { data: {
importId: article.importId,
title: article.title, title: article.title,
excerpt: article.excerpt, excerpt: article.excerpt,
content: article.content, content: article.content,
@ -336,23 +337,89 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
(contentType.includes('image/') ? `.${contentType.split('/')[1]}` : ''); (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 // Загрузка буфера в S3
const uploadedUrl = await uploadBufferToS3( const uploadedUrl = await uploadBufferToS3(
imageBuffer, imageBuffer,
folder, folder,
filename, webpFileName,
contentType, contentType,
'original', 'medium',
80 80
); );
// Добавить обложку к статье
await prisma.article.update({
where: { id: newArticle.id },
data: {
coverImage: uploadedUrl
},
});
} catch (imageError) { } catch (imageError) {
console.error(`Ошибка загрузки coverImage для статьи ${article.id}:`, 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++; importedCount++;
} catch (articleError) { } catch (articleError) {
console.error(`Ошибка импорта статьи ${article.id}:`, articleError); console.error(`Ошибка импорта статьи ${article.id}:`, articleError);
@ -369,4 +436,3 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
await prisma.$disconnect(); await prisma.$disconnect();
} }
} }

View File

@ -45,7 +45,10 @@ export async function listArticles(req: Request, res: Response) {
}, },
skip, skip,
take: perPage, take: perPage,
orderBy: { publishedAt: 'desc' }, orderBy: [
{ publishedAt: 'desc' },
{ id: 'desc' }, // вторичный критерий сортировки
]
}), }),
prisma.article.count({ where }), 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 express from 'express';
import { auth } from '../../middleware/auth'; import { auth } from '../../middleware/auth';
import { searchArticles } from './controllers/search'; import { searchArticles } from './controllers/search';
import { sitemapArticles } from './controllers/sitemap';
import { listArticles } from './controllers/list'; import { listArticles } from './controllers/list';
import { import {
getArticle, getArticle,
@ -16,6 +17,7 @@ const router = express.Router();
// Поиск и список routes // Поиск и список routes
router.get('/search', searchArticles); router.get('/search', searchArticles);
router.get('/sitemap', sitemapArticles);
router.get('/', listArticles); router.get('/', listArticles);
// Импорт route // Импорт 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 { v4 as uuidv4 } from 'uuid';
import multer from 'multer'; import multer from 'multer';
import sharp from 'sharp'; import sharp from 'sharp';
@ -103,12 +103,12 @@ export const uploadToS3 = async (
/** /**
* Функция для загрузки буфера в S3 * Функция для загрузки буфера в S3
* @param buffer - Буфер для загрузки * @param buffer - Буфер для загрузки
* @param folder - Путь к папке в S3 * @param folder - Путь к папке в S3
* @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя) * @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя)
* @param mimetype - MIME тип контента буфера * @param mimetype - MIME тип контента буфера
* @param resolutionId - Опциональный идентификатор разрешения изображения * @param resolutionId - Опциональный идентификатор разрешения изображения
* @param quality - Отцтональное значение качества при оптимизации * @param quality - Опциональное значение качества при оптимизации
* @returns URL загрущенного файла * @returns URL загрущенного файла
*/ */
export const uploadBufferToS3 = async ( export const uploadBufferToS3 = async (
@ -149,18 +149,18 @@ export const uploadBufferToS3 = async (
}) })
); );
// Возвращаем URL файла // Возвращаем URL файла с заменой расширения
const fileUrl = `https://${bucketName}.s3.regru.cloud/${finalFilename}`; const fileUrl = `https://${bucketName}.s3.regru.cloud/${key}`;
logger.info(`Buffer successfully uploaded to S3: ${key}`); logger.info(`Буффер успешно загружен в S3: ${key}`);
return fileUrl; return fileUrl;
} catch (error) { } catch (error) {
logger.error('Error uploading buffer to S3'); logger.error('Ошибка заргузки буффера в S3');
console.error(error); console.error(error);
throw new Error('Error uploading buffer to S3'); throw new Error('Ошибка заргузки буффера в S3');
} }
}; };
// Функция получения пасширения файла из MIME типа // Функция получения расширения файла из MIME типа
function getExtensionFromMimeType(mimetype: string): string { function getExtensionFromMimeType(mimetype: string): string {
const mimeToExt: Record<string, string> = { const mimeToExt: Record<string, string> = {
'image/jpeg': '.jpg', 'image/jpeg': '.jpg',

View File

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