base commit

This commit is contained in:
@kirill.komarov
2026-04-29 19:14:34 +05:00
parent c1773e5c57
commit bfc9661d22
25 changed files with 1885 additions and 3 deletions
+246 -1
View File
@@ -60,7 +60,7 @@ export async function registerApiRoutes(fastify) {
return prisma.category.findMany({ orderBy: { sort: 'asc' } })
})
fastify.get('/api/products', async (request) => {
fastify.get('/api/products', async (request, reply) => {
const { categorySlug } = request.query
const qRaw = request.query?.q
const q = typeof qRaw === 'string' ? qRaw.trim() : ''
@@ -141,6 +141,71 @@ export async function registerApiRoutes(fastify) {
return mapProductForApi(product)
})
// ---- Отзывы к товарам ----
fastify.get('/api/products/:id/reviews', async (request, reply) => {
const { id } = request.params
const pageRaw = request.query?.page
const pageParsed = typeof pageRaw === 'string' ? Number(pageRaw) : Number(pageRaw)
const page = Number.isFinite(pageParsed) && pageParsed > 0 ? Math.floor(pageParsed) : 1
const pageSizeRaw = request.query?.pageSize
const pageSizeParsed = typeof pageSizeRaw === 'string' ? Number(pageSizeRaw) : Number(pageSizeRaw)
const pageSize = Number.isFinite(pageSizeParsed) && pageSizeParsed > 0 ? Math.floor(pageSizeParsed) : 10
if (pageSize > 50) return reply.code(400).send({ error: 'pageSize должен быть ≤ 50' })
const product = await prisma.product.findFirst({ where: { id, published: true } })
if (!product) return reply.code(404).send({ error: 'Товар не найден' })
const where = { productId: id, status: 'approved' }
const total = await prisma.review.count({ where })
const items = await prisma.review.findMany({
where,
include: { user: { select: { id: true, name: true, email: true } } },
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
})
return { items, total, page, pageSize }
})
fastify.post(
'/api/products/:id/reviews',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const userId = request.user.sub
const { id: productId } = request.params
const product = await prisma.product.findFirst({ where: { id: productId, published: true } })
if (!product) return reply.code(404).send({ error: 'Товар не найден' })
const rating = Number(request.body?.rating)
if (!Number.isFinite(rating) || rating < 1 || rating > 5) {
return reply.code(400).send({ error: 'rating должен быть от 1 до 5' })
}
const textRaw = request.body?.text
const text = textRaw === null || textRaw === undefined ? null : String(textRaw).trim()
if (text !== null && text.length > 1000) return reply.code(400).send({ error: 'Отзыв слишком длинный' })
try {
const created = await prisma.review.create({
data: {
productId,
userId,
rating: Math.floor(rating),
text: text && text.length ? text : null,
status: 'pending',
},
})
return reply.code(201).send({ item: created })
} catch {
return reply.code(409).send({ error: 'Вы уже оставляли отзыв на этот товар' })
}
},
)
// ---- Админ (тот же фронт, другой раздел) ----
fastify.get(
@@ -423,6 +488,186 @@ export async function registerApiRoutes(fastify) {
},
)
// ---- Админ: заказы ----
function canTransition(from, to) {
if (from === to) return true
const allowed = {
DRAFT: new Set(['PENDING_PAYMENT', 'CANCELLED']),
PENDING_PAYMENT: new Set(['PAID', 'CANCELLED']),
PAID: new Set(['IN_PROGRESS', 'CANCELLED']),
IN_PROGRESS: new Set(['SHIPPED', 'CANCELLED']),
SHIPPED: new Set(['DONE']),
DONE: new Set([]),
CANCELLED: new Set([]),
}
return Boolean(allowed[from]?.has(to))
}
fastify.get(
'/api/admin/orders',
{ preHandler: [fastify.verifyAdmin] },
async (request, reply) => {
const status = typeof request.query?.status === 'string' ? request.query.status.trim() : ''
const q = typeof request.query?.q === 'string' ? request.query.q.trim() : ''
const pageRaw = request.query?.page
const pageParsed = typeof pageRaw === 'string' ? Number(pageRaw) : Number(pageRaw)
const page = Number.isFinite(pageParsed) && pageParsed > 0 ? Math.floor(pageParsed) : 1
const pageSizeRaw = request.query?.pageSize
const pageSizeParsed = typeof pageSizeRaw === 'string' ? Number(pageSizeRaw) : Number(pageSizeRaw)
const pageSize = Number.isFinite(pageSizeParsed) && pageSizeParsed > 0 ? Math.floor(pageSizeParsed) : 20
if (pageSize > 100) return reply.code(400).send({ error: 'pageSize должен быть ≤ 100' })
const where = {}
if (status) where.status = status
if (q) {
where.OR = [
{ id: { contains: q } },
{ user: { email: { contains: q } } },
]
}
const total = await prisma.order.count({ where })
const items = await prisma.order.findMany({
where,
include: { user: { select: { id: true, email: true } }, items: true },
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
})
return {
items: items.map((o) => ({
id: o.id,
status: o.status,
totalCents: o.totalCents,
currency: o.currency,
createdAt: o.createdAt,
updatedAt: o.updatedAt,
user: o.user,
itemsCount: o.items.reduce((s, i) => s + i.qty, 0),
})),
total,
page,
pageSize,
}
},
)
fastify.get(
'/api/admin/orders/:id',
{ preHandler: [fastify.verifyAdmin] },
async (request, reply) => {
const { id } = request.params
const order = await prisma.order.findUnique({
where: { id },
include: {
user: { select: { id: true, email: true, name: true, phone: true } },
items: true,
messages: { orderBy: { createdAt: 'asc' } },
},
})
if (!order) return reply.code(404).send({ error: 'Заказ не найден' })
return { item: order }
},
)
fastify.patch(
'/api/admin/orders/:id/status',
{ preHandler: [fastify.verifyAdmin] },
async (request, reply) => {
const { id } = request.params
const next = String(request.body?.status || '').trim()
if (!next) return reply.code(400).send({ error: 'status обязателен' })
const existing = await prisma.order.findUnique({ where: { id } })
if (!existing) return reply.code(404).send({ error: 'Заказ не найден' })
if (!canTransition(existing.status, next)) {
return reply.code(409).send({ error: `Нельзя сменить статус ${existing.status}${next}` })
}
const updated = await prisma.order.update({ where: { id }, data: { status: next } })
return { item: updated }
},
)
fastify.post(
'/api/admin/orders/:id/messages',
{ preHandler: [fastify.verifyAdmin] },
async (request, reply) => {
const { id } = request.params
const text = String(request.body?.text || '').trim()
if (!text) return reply.code(400).send({ error: 'Сообщение пустое' })
if (text.length > 2000) return reply.code(400).send({ error: 'Сообщение слишком длинное' })
const order = await prisma.order.findUnique({ where: { id } })
if (!order) return reply.code(404).send({ error: 'Заказ не найден' })
const msg = await prisma.orderMessage.create({ data: { orderId: id, authorType: 'admin', text } })
return reply.code(201).send({ item: msg })
},
)
// ---- Админ: отзывы (модерация) ----
fastify.get(
'/api/admin/reviews',
{ preHandler: [fastify.verifyAdmin] },
async (request, reply) => {
const status = typeof request.query?.status === 'string' ? request.query.status.trim() : 'pending'
const pageRaw = request.query?.page
const pageParsed = typeof pageRaw === 'string' ? Number(pageRaw) : Number(pageRaw)
const page = Number.isFinite(pageParsed) && pageParsed > 0 ? Math.floor(pageParsed) : 1
const pageSizeRaw = request.query?.pageSize
const pageSizeParsed = typeof pageSizeRaw === 'string' ? Number(pageSizeRaw) : Number(pageSizeRaw)
const pageSize = Number.isFinite(pageSizeParsed) && pageSizeParsed > 0 ? Math.floor(pageSizeParsed) : 20
if (pageSize > 100) return reply.code(400).send({ error: 'pageSize должен быть ≤ 100' })
const where = status ? { status } : {}
const total = await prisma.review.count({ where })
const items = await prisma.review.findMany({
where,
include: {
user: { select: { id: true, email: true, name: true } },
product: { select: { id: true, title: true } },
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
})
return { items, total, page, pageSize }
},
)
fastify.patch(
'/api/admin/reviews/:id',
{ preHandler: [fastify.verifyAdmin] },
async (request, reply) => {
const { id } = request.params
const action = String(request.body?.action || '').trim()
if (action !== 'approve' && action !== 'reject') {
return reply.code(400).send({ error: 'action должен быть approve или reject' })
}
const existing = await prisma.review.findUnique({ where: { id } })
if (!existing) return reply.code(404).send({ error: 'Отзыв не найден' })
const updated = await prisma.review.update({
where: { id },
data: {
status: action === 'approve' ? 'approved' : 'rejected',
moderatedAt: new Date(),
},
})
return { item: updated }
},
)
// ---- Админ: пользователи ----
fastify.get(