From 810599c718e1d6d78fe797a5cb5b15b600f76a5d Mon Sep 17 00:00:00 2001 From: anibilag Date: Tue, 3 Jun 2025 22:31:07 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D0=BD=D0=B0=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D0=B2=D1=8F=D0=B7=D0=BA=D0=B8=20=D0=B0?= =?UTF-8?q?=D0=B2=D1=82=D0=BE=D1=80=D0=BE=D0=B2=20=D0=BA=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=D1=8F=D0=BC,=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D1=8B=20=D1=82=D1=80=D0=B5=D1=85=20=D1=82=D0=B8=D0=BF?= =?UTF-8?q?=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 5 + .../migration.sql | 17 ++ .../migration.sql | 8 + .../migration.sql | 9 + prisma/schema.prisma | 44 +++-- src/routes/articles/controllers/crud.ts | 167 ++++++++++++------ src/routes/articles/controllers/list.ts | 17 +- src/routes/articles/controllers/search.ts | 33 +++- src/routes/authors/controllers/authors.ts | 34 +++- src/routes/other/controllers/other.ts | 3 +- src/services/authorService.ts | 61 +++++-- src/types/index.ts | 2 + 12 files changed, 298 insertions(+), 102 deletions(-) create mode 100644 prisma/migrations/20250521200533_authors_roles/migration.sql create mode 100644 prisma/migrations/20250524201156_authors_articles/migration.sql create mode 100644 prisma/migrations/20250524203921_author_id_remove_articles/migration.sql create mode 100644 prisma/migrations/20250525210300_author_articles_add_key/migration.sql diff --git a/prisma/migrations/20250521200533_authors_roles/migration.sql b/prisma/migrations/20250521200533_authors_roles/migration.sql new file mode 100644 index 0000000..ee168f9 --- /dev/null +++ b/prisma/migrations/20250521200533_authors_roles/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "AuthorRole" AS ENUM ('WRITER', 'PHOTOGRAPHER', 'EDITOR', 'TRANSLATOR'); + +-- AlterTable +ALTER TABLE "Author" ADD COLUMN "roles" "AuthorRole"[] DEFAULT ARRAY[]::"AuthorRole"[]; diff --git a/prisma/migrations/20250524201156_authors_articles/migration.sql b/prisma/migrations/20250524201156_authors_articles/migration.sql new file mode 100644 index 0000000..77e58dc --- /dev/null +++ b/prisma/migrations/20250524201156_authors_articles/migration.sql @@ -0,0 +1,17 @@ +-- DropForeignKey +ALTER TABLE "Article" DROP CONSTRAINT "Article_authorId_fkey"; + +-- CreateTable +CREATE TABLE "ArticleAuthor" ( + "articleId" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + "role" "AuthorRole" NOT NULL, + + CONSTRAINT "ArticleAuthor_pkey" PRIMARY KEY ("articleId","authorId") +); + +-- AddForeignKey +ALTER TABLE "ArticleAuthor" ADD CONSTRAINT "ArticleAuthor_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArticleAuthor" ADD CONSTRAINT "ArticleAuthor_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "Author"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250524203921_author_id_remove_articles/migration.sql b/prisma/migrations/20250524203921_author_id_remove_articles/migration.sql new file mode 100644 index 0000000..c3c9115 --- /dev/null +++ b/prisma/migrations/20250524203921_author_id_remove_articles/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `authorId` on the `Article` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Article" DROP COLUMN "authorId"; diff --git a/prisma/migrations/20250525210300_author_articles_add_key/migration.sql b/prisma/migrations/20250525210300_author_articles_add_key/migration.sql new file mode 100644 index 0000000..69d625b --- /dev/null +++ b/prisma/migrations/20250525210300_author_articles_add_key/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - The primary key for the `ArticleAuthor` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- AlterTable +ALTER TABLE "ArticleAuthor" DROP CONSTRAINT "ArticleAuthor_pkey", +ADD CONSTRAINT "ArticleAuthor_pkey" PRIMARY KEY ("articleId", "authorId", "role"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bb0e369..6e52aca 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,27 +30,27 @@ enum AuthorRole { } model Author { - id String @id @default(uuid()) + id String @id @default(uuid()) displayName String bio String? avatarUrl String - order Int @default(0) + order Int @default(0) okUrl String? vkUrl String? websiteUrl String? email String? - userId String? @unique - user User? @relation(fields: [userId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - articles Article[] - isActive Boolean @default(true) - roles AuthorRole[] @default([]) + userId String? @unique + user User? @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + isActive Boolean @default(true) + roles AuthorRole[] @default([]) + articles ArticleAuthor[] } model Article { - id String @id @default(uuid()) - importId Int @default(0) + id String @id @default(uuid()) + importId Int @default(0) title String excerpt String content String @@ -58,13 +58,23 @@ model Article { cityId Int coverImage String readTime Int - likes Int @default(0) - dislikes Int @default(0) - publishedAt DateTime @default(now()) - author Author @relation(fields: [authorId], references: [id]) - authorId String + likes Int @default(0) + dislikes Int @default(0) + publishedAt DateTime @default(now()) gallery GalleryImage[] - isActive Boolean @default(false) + isActive Boolean @default(false) + authors ArticleAuthor[] +} + +model ArticleAuthor { + articleId String + authorId String + role AuthorRole + + article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) + author Author @relation(fields: [authorId], references: [id], onDelete: Cascade) + + @@id([articleId, authorId, role]) } model GalleryImage { diff --git a/src/routes/articles/controllers/crud.ts b/src/routes/articles/controllers/crud.ts index a98e224..0181b11 100644 --- a/src/routes/articles/controllers/crud.ts +++ b/src/routes/articles/controllers/crud.ts @@ -7,7 +7,9 @@ import { AuthRequest } from '../../../middleware/auth'; import { checkPermission } from '../../../utils/permissions'; import { logger } from '../../../config/logger'; import { Article } from "../../../types"; -import { bucketName, uploadBufferToS3 } from "../../../services/s3Service"; +import { uploadBufferToS3 } from "../../../services/s3Service"; +import { AuthorRole } from "@prisma/client"; + const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp'; @@ -16,13 +18,18 @@ export async function getArticle(req: Request, res: Response) : Promise { const article = await prisma.article.findUnique({ where: { id: req.params.id }, include: { - author: { - select: { - id: true, - displayName: true, - avatarUrl: true, - email: true - } + authors: { + include: { + author: { + select: { + id: true, + displayName: true, + avatarUrl: true, + email: true, + order: true, + }, + }, + }, }, gallery: { orderBy: {order: "asc"}, // Сортировка изображений по порядку @@ -95,6 +102,7 @@ export async function createArticle(req: AuthRequest, res: Response) : Promise { try { - const { title, excerpt, content, categoryId, cityId, coverImage, readTime, author } = req.body; + const { title, excerpt, content, categoryId, cityId, coverImage, readTime, authors } = req.body; if (!req.user) { res.status(401).json({ error: 'Пользователь не вошел в систему' }); return } - const article = await prisma.article.findUnique({ + const existingArticle = await prisma.article.findUnique({ where: { id: req.params.id }, - select: { authorId: true } // Берём только автора + select: { id: true }, }); - if (!article) { + if (!existingArticle) { res.status(404).json({ error: 'Статья не найдена' }); - return + return; } if (!checkPermission(req.user, categoryId, 'edit')) { @@ -159,22 +179,34 @@ export async function updateArticle(req: AuthRequest, res: Response) : Promise ({ + author: { connect: { id: a.author?.id } }, + role: a.role as AuthorRole + })), + }; } const updatedArticle = await prisma.article.update({ where: { id: req.params.id }, data: updateData, include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - } + authors: { + include: { + author: { + select: { + id: true, + displayName: true, + email: true, + }, + }, + }, + }, + }, }); res.json(updatedArticle); @@ -208,13 +240,17 @@ export async function activeArticle(req: AuthRequest, res: Response) : Promise { try { - const authors = await authorService.getAuthors(); + const { role, page } = req.query; + const authors = await authorService.getAuthors(Number(page), role as string); res.json(authors); } catch { res.status(500).json({ error: 'Server error' }); @@ -22,7 +24,14 @@ export async function createAuthor(req: AuthRequest, res: Response): Promise + Object.values(AuthorRole).includes(role as AuthorRole) + ) + : []; const lastAuthor = await prisma.author.findFirst({ orderBy: { @@ -38,7 +47,11 @@ export async function createAuthor(req: AuthRequest, res: Response): Promise + Object.values(AuthorRole).includes(role as AuthorRole) + ) + : []; // Подготовка данных для изменения const updateData: any = { @@ -79,6 +103,7 @@ export async function updateAuthor(req: AuthRequest, res: Response): Promise { try { - const authors = await authorService.getAuthors(); + const { role, page } = req.query; + const authors = await authorService.getAuthors(Number(page), role as string); res.json(authors); } catch { res.status(500).json({ error: 'Серверная ошибка' }); diff --git a/src/services/authorService.ts b/src/services/authorService.ts index b3e7bc7..1c20f72 100644 --- a/src/services/authorService.ts +++ b/src/services/authorService.ts @@ -1,10 +1,29 @@ import { PrismaClient } from '@prisma/client'; +import { AuthorRole } from '@prisma/client'; import { Author } from '../types/auth'; const prisma = new PrismaClient(); export const authorService = { - getAuthors: async (): Promise => { + getAuthors: async (page: number, role?: string): Promise => { + + if (role && !Object.values(AuthorRole).includes(role as AuthorRole)) { + throw new Error(`Недопустимая роль: ${role}`); + } + + const pageSize = 10; + + const sortRoles = role + ? { + roles: { + has: role as AuthorRole, // Prisma оператор для enum[] + }, + } + : {}; + + const skip = page ? (page - 1) * pageSize : undefined; + const take = page ? pageSize : undefined; + try { // 1. Получаем авторов const authors = await prisma.author.findMany({ @@ -20,8 +39,15 @@ export const authorService = { email: true, userId: true, isActive: true, + roles: true, _count: { - select: {articles: true}, // Подсчёт количества статей автора + select: { + articles: { + where: { + role: role as AuthorRole, // 👈 учитываются только WRITER-авторы + }, + }, + }, }, user: { select: { @@ -29,24 +55,28 @@ export const authorService = { }, } }, - where: {order: {not: 0}}, + where: { + order: {not: 0}, + ...sortRoles, // 👈 добавляем условие по ролям + }, orderBy: { order: 'asc', }, + skip, + take, }); + const sql = ` + SELECT "authorId", SUM("Article"."likes") AS "totalLikes" + FROM "ArticleAuthor" + JOIN "Article" ON "ArticleAuthor"."articleId" = "Article"."id" + WHERE "ArticleAuthor"."role" = $1::"AuthorRole" + GROUP BY "authorId" + `; + // 2. Считаем сумму лайков по авторам - const likesPerAuthor = await prisma.article.groupBy({ - by: ['authorId'], - _sum: { - likes: true, - }, - }); - - // 3. Строим map для быстрого доступа - const likesMap = new Map( - likesPerAuthor.map((item) => [item.authorId, item._sum.likes ?? 0]) - ); + const rawLikes: { authorId: string; totalLikes: number | null }[] = await prisma.$queryRawUnsafe(sql, role); + const likesMap = new Map(rawLikes.map(item => [item.authorId, item.totalLikes ?? 0])); return authors.map(author => ({ id: author.id, @@ -62,7 +92,8 @@ export const authorService = { userId: author.userId, userDisplayName: author.user?.displayName || null, isActive: author.isActive, - totalLikes: likesMap.get(author.id) ?? 0, + roles: author.roles, + totalLikes: Number(likesMap.get(author.id) ?? 0), })); } catch (error) { console.error('Ошибка получения авторов:', error); diff --git a/src/types/index.ts b/src/types/index.ts index 930b756..f87c323 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -10,6 +10,8 @@ export interface Article { cityId: number; author: Author; authorName: string; + coAuthorName: string; + photographerName: string; coverImage: string; images?: string[]; imageSubs: string[];