base commit

This commit is contained in:
@kirill.komarov
2026-04-30 22:34:55 +05:00
parent 123d86091d
commit 9139a24093
46 changed files with 2023 additions and 153 deletions
+79 -4
View File
@@ -1,5 +1,63 @@
import { prisma } from '../../lib/prisma.js'
const EMPTY_REVIEWS_SUMMARY = Object.freeze({
approvedReviewCount: 0,
avgRating: null,
latestApprovedText: null,
})
/** Сводка по одобренным отзывам для списка id товаров (для каталога и карточки товара). */
export async function approvedReviewSummariesForProducts(productIds) {
const map = new Map()
if (!productIds.length) return map
const uniqueIds = [...new Set(productIds)]
for (const id of uniqueIds) {
map.set(id, { ...EMPTY_REVIEWS_SUMMARY })
}
const grouped = await prisma.review.groupBy({
by: ['productId'],
where: { productId: { in: uniqueIds }, status: 'approved' },
_count: { _all: true },
_avg: { rating: true },
})
for (const g of grouped) {
const avg = g._avg.rating
const prev = map.get(g.productId)
if (!prev) continue
map.set(g.productId, {
...prev,
approvedReviewCount: g._count._all,
avgRating: avg != null ? Number(avg) : null,
})
}
const withReviews = [...map.entries()].filter(([, v]) => v.approvedReviewCount > 0).map(([k]) => k)
if (!withReviews.length) return map
const previewRows = await prisma.review.findMany({
where: { productId: { in: withReviews }, status: 'approved' },
orderBy: { createdAt: 'desc' },
select: { productId: true, text: true },
take: 450,
})
const hasPreviewFor = new Set()
for (const r of previewRows) {
if (hasPreviewFor.has(r.productId)) continue
const t = typeof r.text === 'string' ? r.text.trim() : ''
if (!t) continue
hasPreviewFor.add(r.productId)
const prev = map.get(r.productId)
if (!prev) continue
prev.latestApprovedText = t.length > 160 ? `${t.slice(0, 160)}` : t
if (hasPreviewFor.size === withReviews.length) break
}
return map
}
export async function registerPublicCatalogRoutes(fastify, { mapProductForApi } = {}) {
fastify.get('/api/categories', async () => {
return prisma.category.findMany({ orderBy: { sort: 'asc' } })
@@ -9,6 +67,8 @@ export async function registerPublicCatalogRoutes(fastify, { mapProductForApi }
const { categorySlug } = request.query
const qRaw = request.query?.q
const q = typeof qRaw === 'string' ? qRaw.trim() : ''
const availabilityRaw = request.query?.availability
const availability = typeof availabilityRaw === 'string' ? availabilityRaw.trim() : ''
const sortRaw = request.query?.sort
const sort = typeof sortRaw === 'string' ? sortRaw : ''
@@ -29,13 +89,21 @@ export async function registerPublicCatalogRoutes(fastify, { mapProductForApi }
const priceMaxParsed = typeof priceMaxRaw === 'string' ? Number(priceMaxRaw) : Number(priceMaxRaw)
const priceMax = Number.isFinite(priceMaxParsed) && priceMaxParsed >= 0 ? Math.floor(priceMaxParsed) : null
const where = { published: true, quantity: { gt: 0 } }
const where = { published: true }
if (typeof categorySlug === 'string' && categorySlug.length > 0) {
where.category = { slug: categorySlug }
}
if (q) {
where.OR = [{ title: { contains: q } }, { shortDescription: { contains: q } }]
}
if (availability === 'in_stock') {
where.inStock = true
where.quantity = { gt: 0 }
} else if (availability === 'made_to_order') {
where.inStock = false
} else if (availability && availability !== 'all') {
return reply.code(400).send({ error: 'availability должен быть all | in_stock | made_to_order' })
}
const applyPriceFilter = !(priceMin !== null && priceMax !== null && priceMin === 0 && priceMax === 0)
if (applyPriceFilter && (priceMin !== null || priceMax !== null)) {
@@ -64,20 +132,27 @@ export async function registerPublicCatalogRoutes(fastify, { mapProductForApi }
take: pageSize,
})
return { items: items.map(mapProductForApi), total, page, pageSize }
const summaries = await approvedReviewSummariesForProducts(items.map((it) => it.id))
return {
items: items.map((p) => mapProductForApi(p, summaries.get(p.id) ?? EMPTY_REVIEWS_SUMMARY)),
total,
page,
pageSize,
}
})
fastify.get('/api/products/:id', async (request, reply) => {
const { id } = request.params
const product = await prisma.product.findFirst({
where: { id, published: true, quantity: { gt: 0 } },
where: { id, published: true },
include: { category: true, images: { orderBy: { sort: 'asc' } } },
})
if (!product) {
reply.code(404).send({ error: 'Товар не найден' })
return
}
return mapProductForApi(product)
const summaries = await approvedReviewSummariesForProducts([product.id])
return mapProductForApi(product, summaries.get(product.id) ?? EMPTY_REVIEWS_SUMMARY)
})
}