商品模板功能上传
This commit is contained in:
@@ -90,6 +90,11 @@ enum ProductStatus {
|
||||
OFF_SALE
|
||||
}
|
||||
|
||||
enum DetailTemplateStatus {
|
||||
ACTIVE
|
||||
DISABLED
|
||||
}
|
||||
|
||||
enum PromoCodeStatus {
|
||||
ACTIVE
|
||||
DISABLED
|
||||
@@ -333,6 +338,26 @@ model CommonProductItem {
|
||||
@@map("common_product_item")
|
||||
}
|
||||
|
||||
model CommonProductDetailTemplate {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
code String @unique @db.VarChar(32)
|
||||
name String @db.VarChar(64)
|
||||
description String? @db.VarChar(256)
|
||||
aromaType AromaType? @map("aroma_type")
|
||||
storyTitle String? @map("story_title") @db.VarChar(128)
|
||||
storyText String? @map("story_text") @db.Text
|
||||
features Json?
|
||||
detailImageUrls Json? @map("detail_image_urls")
|
||||
suggestedDetailImageCount Int @default(1) @map("suggested_detail_image_count")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
status DetailTemplateStatus @default(ACTIVE)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
@@index([status, sortOrder])
|
||||
@@map("common_product_detail_template")
|
||||
}
|
||||
|
||||
model CommonStoreCategory {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
code String @unique @db.VarChar(32)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PrismaClient, ResourceBizType, ResourceMediaType, ResourceOwnerType } from '@prisma/client';
|
||||
import { DEFAULT_PRODUCT_DETAIL_TEMPLATES } from './seeds/product-detail-templates.default';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@@ -46,6 +47,7 @@ async function main() {
|
||||
await prisma.commonCity.deleteMany();
|
||||
await prisma.partner.deleteMany();
|
||||
await prisma.commonProductItem.deleteMany();
|
||||
await prisma.commonProductDetailTemplate.deleteMany();
|
||||
await prisma.commonStoreCategory.deleteMany();
|
||||
await prisma.commonPromoCode.deleteMany();
|
||||
await prisma.commonResource.deleteMany();
|
||||
@@ -89,6 +91,24 @@ async function main() {
|
||||
prisma.commonStoreCategory.create({ data: { code: 'LOCAL', name: '地方菜', sort: 2 } }),
|
||||
]);
|
||||
|
||||
for (const tpl of DEFAULT_PRODUCT_DETAIL_TEMPLATES) {
|
||||
await prisma.commonProductDetailTemplate.create({
|
||||
data: {
|
||||
code: tpl.code,
|
||||
name: tpl.name,
|
||||
description: tpl.description,
|
||||
aromaType: tpl.aromaType as 'QINGXIANG' | 'JIANGXIANG' | 'NONGXIANG' | null,
|
||||
storyTitle: tpl.storyTitle,
|
||||
storyText: tpl.storyText,
|
||||
features: tpl.features,
|
||||
detailImageUrls: [],
|
||||
suggestedDetailImageCount: tpl.suggestedDetailImageCount,
|
||||
sortOrder: tpl.sortOrder,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const productDefs = [
|
||||
{ skuCode: 'QX-001', name: '杜康·白水古酿 500ml', subtitle: '清香型 52度 礼盒装', price: 599, sortOrder: 1, img: 'https://picsum.photos/seed/dukang1/400/400' },
|
||||
{ skuCode: 'QX-002', name: '杜康·年份陈酿(十年)', subtitle: '清香型 42度 纯粮酿造', price: 880, sortOrder: 2, img: 'https://picsum.photos/seed/dukang2/400/400' },
|
||||
@@ -227,6 +247,7 @@ async function main() {
|
||||
|
||||
console.log('Seed complete:', {
|
||||
city: city.name,
|
||||
detailTemplates: DEFAULT_PRODUCT_DETAIL_TEMPLATES.length,
|
||||
products: products.length,
|
||||
stores: storeDefs.length,
|
||||
testPhones: {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/** 商品详情模板默认种子(与 admin-web 历史静态模板一致) */
|
||||
export const DEFAULT_PRODUCT_DETAIL_TEMPLATES = [
|
||||
{
|
||||
code: 'dukang-classic',
|
||||
name: '杜康经典',
|
||||
description: '品牌故事 + 双卖点,适合主力 SKU',
|
||||
aromaType: null as string | null,
|
||||
storyTitle: '千年杜康 · 唯有此处',
|
||||
storyText:
|
||||
'选自白水杜康核心产区,取山泉之灵气,集五谷之精华。古法酿造工艺,历经九九八十一道工序,方得这一口醇厚绵甜。',
|
||||
features: [
|
||||
{ icon: 'water_drop', title: '泉水酿造', desc: '甘冽清甜 灵动自然' },
|
||||
{ icon: 'grain', title: '精选五谷', desc: '传统比例 匠心发酵' },
|
||||
],
|
||||
suggestedDetailImageCount: 2,
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
code: 'qingxiang',
|
||||
name: '清香型',
|
||||
description: '突出清香甘冽、入口绵柔',
|
||||
aromaType: 'QINGXIANG',
|
||||
storyTitle: '清香传世 · 入口甘冽',
|
||||
storyText:
|
||||
'以优质高粱、大麦、豌豆为原料,地缸固态发酵,酒体清澈透明,清香纯正,甘冽爽口,余味悠长。',
|
||||
features: [
|
||||
{ icon: 'air', title: '清香纯正', desc: '窖香优雅 入口绵甜' },
|
||||
{ icon: 'local_drink', title: '纯粮固态', desc: '地缸发酵 传统工艺' },
|
||||
],
|
||||
suggestedDetailImageCount: 2,
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
code: 'jiangxiang',
|
||||
name: '酱香型',
|
||||
description: '突出酱香突出、回味悠长',
|
||||
aromaType: 'JIANGXIANG',
|
||||
storyTitle: '酱香典范 · 岁月醇香',
|
||||
storyText:
|
||||
'遵循端午制曲、重阳下沙,九次蒸煮八次发酵,长期窖藏陈化,酱香突出,幽雅细腻,空杯留香持久。',
|
||||
features: [
|
||||
{ icon: 'schedule', title: '陈年窖藏', desc: '时光淬炼 醇厚丰满' },
|
||||
{ icon: 'spa', title: '酱香工艺', desc: '九蒸八酵 匠心酿造' },
|
||||
],
|
||||
suggestedDetailImageCount: 3,
|
||||
sortOrder: 3,
|
||||
},
|
||||
{
|
||||
code: 'nongxiang',
|
||||
name: '浓香型',
|
||||
description: '突出窖香浓郁、绵甜爽净',
|
||||
aromaType: 'NONGXIANG',
|
||||
storyTitle: '浓香典范 · 窖香天成',
|
||||
storyText:
|
||||
'泥窖固态发酵,续糟配料,蒸馏摘酒,分级贮存。酒体无色透明,窖香浓郁,绵甜爽净,尾净余香长。',
|
||||
features: [
|
||||
{ icon: 'foundation', title: '百年窖池', desc: '微生物群落 窖香天成' },
|
||||
{ icon: 'water_drop', title: '绵甜爽净', desc: '入口醇和 回味悠长' },
|
||||
],
|
||||
suggestedDetailImageCount: 2,
|
||||
sortOrder: 4,
|
||||
},
|
||||
{
|
||||
code: 'benefit-focus',
|
||||
name: '好客权益',
|
||||
description: '强调购酒享权益、到店核销',
|
||||
aromaType: null,
|
||||
storyTitle: '买杜康美酒 · 享全城好客礼遇',
|
||||
storyText:
|
||||
'购酒即享本城专属好客权益,为您安排一场地道饭局。权益金可直接用于本地签约门店消费,到店享用,醇香美酒配佳肴。',
|
||||
features: [
|
||||
{ icon: 'confirmation_number', title: '购酒发券', desc: '支付成功即享权益额度' },
|
||||
{ icon: 'store', title: '全城好店', desc: '本地签约门店随心核销' },
|
||||
],
|
||||
suggestedDetailImageCount: 1,
|
||||
sortOrder: 5,
|
||||
},
|
||||
{
|
||||
code: 'minimal',
|
||||
name: '简约留白',
|
||||
description: '仅故事正文,无卖点模块',
|
||||
aromaType: null,
|
||||
storyTitle: '商品详情',
|
||||
storyText: '请在上方上传详情长图,或在此补充商品说明文字。',
|
||||
features: [] as Array<{ icon: string; title: string; desc: string }>,
|
||||
suggestedDetailImageCount: 1,
|
||||
sortOrder: 6,
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { DEFAULT_PRODUCT_DETAIL_TEMPLATES } from './seeds/product-detail-templates.default';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
for (const tpl of DEFAULT_PRODUCT_DETAIL_TEMPLATES) {
|
||||
await prisma.commonProductDetailTemplate.upsert({
|
||||
where: { code: tpl.code },
|
||||
create: {
|
||||
code: tpl.code,
|
||||
name: tpl.name,
|
||||
description: tpl.description,
|
||||
aromaType: tpl.aromaType as 'QINGXIANG' | 'JIANGXIANG' | 'NONGXIANG' | null,
|
||||
storyTitle: tpl.storyTitle,
|
||||
storyText: tpl.storyText,
|
||||
features: tpl.features,
|
||||
detailImageUrls: [],
|
||||
suggestedDetailImageCount: tpl.suggestedDetailImageCount,
|
||||
sortOrder: tpl.sortOrder,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
update: {
|
||||
name: tpl.name,
|
||||
description: tpl.description,
|
||||
aromaType: tpl.aromaType as 'QINGXIANG' | 'JIANGXIANG' | 'NONGXIANG' | null,
|
||||
storyTitle: tpl.storyTitle,
|
||||
storyText: tpl.storyText,
|
||||
features: tpl.features,
|
||||
detailImageUrls: [],
|
||||
suggestedDetailImageCount: tpl.suggestedDetailImageCount,
|
||||
sortOrder: tpl.sortOrder,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(`Upserted ${DEFAULT_PRODUCT_DETAIL_TEMPLATES.length} product detail templates`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -83,6 +83,6 @@ import type { ISmsProvider } from './sms/sms.interface';
|
||||
TencentLbsProvider,
|
||||
{ provide: MAP_PROVIDER, useExisting: TencentLbsProvider },
|
||||
],
|
||||
exports: [SMS_PROVIDER, SmsCodeStore, PAY_PROVIDER, DELIVERY_PROVIDER, WECHAT_PROVIDER, OSS_PROVIDER, MAP_PROVIDER, CourierModule],
|
||||
exports: [SMS_PROVIDER, SmsCodeStore, PAY_PROVIDER, DELIVERY_PROVIDER, WECHAT_PROVIDER, OSS_PROVIDER, MAP_PROVIDER, TencentLbsProvider, CourierModule],
|
||||
})
|
||||
export class IntegrationsModule {}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { AdminProductDetailTemplatesService } from './admin-product-detail-templates.service';
|
||||
import { AdminProductDetailTemplatesQueryDto } from './dto/admin-query.dto';
|
||||
import {
|
||||
CreateProductDetailTemplateDto,
|
||||
UpdateProductDetailTemplateDto,
|
||||
} from './dto/admin-mutate.dto';
|
||||
|
||||
@Controller('admin/product-detail-templates')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminProductDetailTemplatesController {
|
||||
constructor(private readonly service: AdminProductDetailTemplatesService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: AdminProductDetailTemplatesQueryDto) {
|
||||
return this.service.list(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateProductDetailTemplateDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateProductDetailTemplateDto) {
|
||||
return this.service.update(BigInt(id), dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
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 = 20;
|
||||
|
||||
@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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -454,3 +454,108 @@ export class UpdateProductDto {
|
||||
@IsObject()
|
||||
detailContent?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
class ProductDetailFeatureInputDto {
|
||||
@IsString()
|
||||
icon: string;
|
||||
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export class CreateProductDetailTemplateDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['QINGXIANG', 'JIANGXIANG', 'NONGXIANG'])
|
||||
aromaType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
storyTitle?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
storyText?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
features?: ProductDetailFeatureInputDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
detailImageUrls?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
suggestedDetailImageCount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
sortOrder?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class UpdateProductDetailTemplateDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['QINGXIANG', 'JIANGXIANG', 'NONGXIANG', null])
|
||||
aromaType?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
storyTitle?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
storyText?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
features?: ProductDetailFeatureInputDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
detailImageUrls?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
suggestedDetailImageCount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
sortOrder?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
}
|
||||
|
||||
@@ -227,6 +227,24 @@ export class AdminProductsQueryDto extends PaginationQueryDto {
|
||||
aromaType?: string;
|
||||
}
|
||||
|
||||
export class AdminProductDetailTemplatesQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
aromaType?: string;
|
||||
}
|
||||
|
||||
export class AdminUserLogsQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -31,6 +31,8 @@ import { CommonModule } from '../common/common.module';
|
||||
import { IntegrationsModule } from '../../integrations/integrations.module';
|
||||
import { AdminXiaofeixiaController } from './admin-xiaofeixia.controller';
|
||||
import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
|
||||
import { AdminProductDetailTemplatesController } from './admin-product-detail-templates.controller';
|
||||
import { AdminProductDetailTemplatesService } from './admin-product-detail-templates.service';
|
||||
|
||||
@Module({
|
||||
imports: [IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule],
|
||||
@@ -53,6 +55,7 @@ import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
|
||||
AdminUserLogsController,
|
||||
AdminTicketsController,
|
||||
AdminXiaofeixiaController,
|
||||
AdminProductDetailTemplatesController,
|
||||
],
|
||||
providers: [
|
||||
AdminDashboardService,
|
||||
@@ -69,6 +72,7 @@ import { AdminXiaofeixiaService } from './admin-xiaofeixia.service';
|
||||
AdminUserLogsService,
|
||||
AdminTicketsService,
|
||||
AdminXiaofeixiaService,
|
||||
AdminProductDetailTemplatesService,
|
||||
SuperAdminGuard,
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user