import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; import type { AdminProductDetailTemplatesQueryDto } from './dto/admin-query.dto'; import type { CreateProductDetailTemplateDto, UpdateProductDetailTemplateDto, } from './dto/admin-mutate.dto'; type TemplateRow = { id: bigint; code: string; name: string; description: string | null; aromaType: string | null; storyTitle: string | null; storyText: string | null; features: unknown; detailImageUrls?: unknown; suggestedDetailImageCount: number; sortOrder: number; status: string; createdAt: Date; updatedAt: Date; }; const MAX_TEMPLATE_DETAIL_IMAGES = 30; @Injectable() export class AdminProductDetailTemplatesService { constructor(private readonly prisma: PrismaService) {} async list(query: AdminProductDetailTemplatesQueryDto) { const page = query.page ?? 1; const pageSize = query.pageSize ?? 20; const where: Prisma.CommonProductDetailTemplateWhereInput = {}; if (query.name) where.name = { contains: query.name }; if (query.code) where.code = { contains: query.code }; if (query.status) { where.status = query.status as Prisma.EnumDetailTemplateStatusFilter['equals']; } if (query.aromaType) { where.aromaType = query.aromaType as Prisma.EnumAromaTypeFilter['equals']; } const [items, total] = await Promise.all([ this.prisma.commonProductDetailTemplate.findMany({ where, orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }], skip: (page - 1) * pageSize, take: pageSize, }), this.prisma.commonProductDetailTemplate.count({ where }), ]); return serializeBigInt({ items: items.map((row) => this.format(row)), total, page, pageSize, }); } async detail(id: bigint) { const row = await this.prisma.commonProductDetailTemplate.findUnique({ where: { id } }); if (!row) throw new NotFoundException('详情模板不存在'); return serializeBigInt(this.format(row)); } async create(dto: CreateProductDetailTemplateDto) { const exists = await this.prisma.commonProductDetailTemplate.findUnique({ where: { code: dto.code }, }); if (exists) throw new BadRequestException('模板编码已存在'); const detailImageUrls = this.normalizeDetailImageUrls(dto.detailImageUrls); const row = await this.prisma.commonProductDetailTemplate.create({ data: { code: dto.code, name: dto.name, description: dto.description, aromaType: dto.aromaType as Prisma.CommonProductDetailTemplateCreateInput['aromaType'], storyTitle: dto.storyTitle, storyText: dto.storyText, features: this.normalizeFeatures(dto.features) as Prisma.InputJsonValue, detailImageUrls: detailImageUrls as Prisma.InputJsonValue, suggestedDetailImageCount: detailImageUrls.length > 0 ? detailImageUrls.length : (dto.suggestedDetailImageCount ?? 1), sortOrder: dto.sortOrder ?? 0, status: (dto.status ?? 'ACTIVE') as Prisma.CommonProductDetailTemplateCreateInput['status'], } as Prisma.CommonProductDetailTemplateCreateInput, }); return serializeBigInt(this.format(row)); } async update(id: bigint, dto: UpdateProductDetailTemplateDto) { const existing = await this.prisma.commonProductDetailTemplate.findUnique({ where: { id } }); if (!existing) throw new NotFoundException('详情模板不存在'); if (dto.code && dto.code !== existing.code) { const dup = await this.prisma.commonProductDetailTemplate.findUnique({ where: { code: dto.code } }); if (dup) throw new BadRequestException('模板编码已存在'); } const data: Prisma.CommonProductDetailTemplateUpdateInput = {}; if (dto.code !== undefined) data.code = dto.code; if (dto.name !== undefined) data.name = dto.name; if (dto.description !== undefined) data.description = dto.description; if (dto.aromaType !== undefined) { data.aromaType = dto.aromaType as Prisma.CommonProductDetailTemplateUpdateInput['aromaType']; } if (dto.storyTitle !== undefined) data.storyTitle = dto.storyTitle; if (dto.storyText !== undefined) data.storyText = dto.storyText; if (dto.features !== undefined) { data.features = this.normalizeFeatures(dto.features) as Prisma.InputJsonValue; } if (dto.detailImageUrls !== undefined) { const detailImageUrls = this.normalizeDetailImageUrls(dto.detailImageUrls); (data as Prisma.CommonProductDetailTemplateUpdateInput & { detailImageUrls?: Prisma.InputJsonValue }).detailImageUrls = detailImageUrls as Prisma.InputJsonValue; data.suggestedDetailImageCount = detailImageUrls.length > 0 ? detailImageUrls.length : (dto.suggestedDetailImageCount ?? existing.suggestedDetailImageCount); } else if (dto.suggestedDetailImageCount !== undefined) { data.suggestedDetailImageCount = dto.suggestedDetailImageCount; } if (dto.sortOrder !== undefined) data.sortOrder = dto.sortOrder; if (dto.status !== undefined) { data.status = dto.status as Prisma.CommonProductDetailTemplateUpdateInput['status']; } const row = await this.prisma.commonProductDetailTemplate.update({ where: { id }, data }); return serializeBigInt(this.format(row)); } private normalizeDetailImageUrls(urls?: string[]) { if (!urls) return []; const cleaned = urls.map((u) => u?.trim()).filter(Boolean); if (cleaned.length > MAX_TEMPLATE_DETAIL_IMAGES) { throw new BadRequestException(`详情图最多 ${MAX_TEMPLATE_DETAIL_IMAGES} 张`); } return cleaned; } private normalizeFeatures(features?: Array<{ icon: string; title: string; desc: string }>) { if (!features) return []; return features .filter((f) => f.title?.trim() || f.desc?.trim()) .map((f) => ({ icon: f.icon?.trim() || 'star', title: f.title?.trim() ?? '', desc: f.desc?.trim() ?? '', })); } private format(row: TemplateRow) { const features = Array.isArray(row.features) ? (row.features as Array<{ icon: string; title: string; desc: string }>) : []; const detailImageUrls = Array.isArray(row.detailImageUrls) ? (row.detailImageUrls as string[]).filter(Boolean) : []; return { id: row.id.toString(), code: row.code, name: row.name, description: row.description, aromaType: row.aromaType, storyTitle: row.storyTitle, storyText: row.storyText, features, detailImageUrls, suggestedDetailImageCount: detailImageUrls.length || row.suggestedDetailImageCount, sortOrder: row.sortOrder, status: row.status, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }; } }