f0b99ea9da
批量删除用户 模板上传图片数量限制改成30 小飞侠接口配置
184 lines
6.9 KiB
TypeScript
184 lines
6.9 KiB
TypeScript
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(),
|
|
};
|
|
}
|
|
}
|