feat(产品): 重构产品属性管理为通用字典项实现
重构产品相关的品牌、口味、规格和尺寸属性管理,改为基于通用字典项的实现方式: 1. 新增 AttributeInputDTO 用于统一处理属性输入 2. 实现通用的字典项增删改查接口 3. 保留旧接口作为兼容层 4. 优化产品创建和更新逻辑以支持新属性结构
This commit is contained in:
parent
bc575840b2
commit
64b8468df8
|
|
@ -11,35 +11,9 @@ import {
|
|||
} from '@midwayjs/core';
|
||||
import { ProductService } from '../service/product.service';
|
||||
import { errorResponse, successResponse } from '../utils/response.util';
|
||||
import {
|
||||
BatchSetSkuDTO,
|
||||
CreateBrandDTO,
|
||||
CreateFlavorsDTO,
|
||||
CreateProductDTO,
|
||||
CreateStrengthDTO,
|
||||
CreateSizeDTO,
|
||||
QueryBrandDTO,
|
||||
QueryFlavorsDTO,
|
||||
QueryProductDTO,
|
||||
QueryStrengthDTO,
|
||||
QuerySizeDTO,
|
||||
UpdateBrandDTO,
|
||||
UpdateFlavorsDTO,
|
||||
UpdateProductDTO,
|
||||
UpdateStrengthDTO,
|
||||
UpdateSizeDTO,
|
||||
} from '../dto/product.dto';
|
||||
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO } from '../dto/product.dto';
|
||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||
import {
|
||||
BooleanRes,
|
||||
ProductBrandListRes,
|
||||
ProductBrandRes,
|
||||
ProductSizeListRes,
|
||||
ProductSizeRes,
|
||||
ProductListRes,
|
||||
ProductRes,
|
||||
ProductsRes,
|
||||
} from '../dto/reponse.dto';
|
||||
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
|
||||
|
||||
@Controller('/product')
|
||||
export class ProductController {
|
||||
|
|
@ -98,9 +72,7 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: ProductRes,
|
||||
})
|
||||
@ApiOkResponse({ type: ProductRes })
|
||||
@Post('/')
|
||||
async createProduct(@Body() productData: CreateProductDTO) {
|
||||
try {
|
||||
|
|
@ -111,14 +83,9 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: ProductRes,
|
||||
})
|
||||
@ApiOkResponse({ type: ProductRes })
|
||||
@Put('/:id')
|
||||
async updateProduct(
|
||||
@Param('id') id: number,
|
||||
@Body() productData: UpdateProductDTO
|
||||
) {
|
||||
async updateProduct(@Param('id') id: number, @Body() productData: UpdateProductDTO) {
|
||||
try {
|
||||
const data = this.productService.updateProduct(id, productData);
|
||||
return successResponse(data);
|
||||
|
|
@ -127,14 +94,9 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: ProductRes,
|
||||
})
|
||||
@ApiOkResponse({ type: ProductRes })
|
||||
@Put('updateNameCn/:id/:nameCn')
|
||||
async updateProductNameCn(
|
||||
@Param('id') id: number,
|
||||
@Param('nameCn') nameCn: string
|
||||
) {
|
||||
async updateProductNameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) {
|
||||
try {
|
||||
const data = this.productService.updateProductNameCn(id, nameCn);
|
||||
return successResponse(data);
|
||||
|
|
@ -143,9 +105,7 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Del('/:id')
|
||||
async deleteProduct(@Param('id') id: number) {
|
||||
try {
|
||||
|
|
@ -156,14 +116,19 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: ProductBrandListRes,
|
||||
})
|
||||
@Get('/brands')
|
||||
async getBrands(@Query() query: QueryBrandDTO) {
|
||||
const { current = 1, pageSize = 10, name } = query;
|
||||
|
||||
// 通用属性接口:分页列表
|
||||
@ApiOkResponse()
|
||||
@Get('/attribute')
|
||||
async getAttributeList(
|
||||
@Query('dictName') dictName: string,
|
||||
@Query('current') current = 1,
|
||||
@Query('pageSize') pageSize = 10,
|
||||
@Query('name') name?: string
|
||||
) {
|
||||
try {
|
||||
let data = await this.productService.getBrandList(
|
||||
const data = await this.productService.getAttributeList(
|
||||
dictName,
|
||||
{ current, pageSize },
|
||||
name
|
||||
);
|
||||
|
|
@ -173,95 +138,142 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 通用属性接口:全部列表
|
||||
@ApiOkResponse()
|
||||
@Get('/brandAll')
|
||||
async getBrandAll() {
|
||||
@Get('/attributeAll')
|
||||
async getAttributeAll(@Query('dictName') dictName: string) {
|
||||
try {
|
||||
let data = await this.productService.getBrandAll();
|
||||
const data = await this.productService.getAttributeAll(dictName);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: ProductBrandRes,
|
||||
})
|
||||
@Post('/brand')
|
||||
async createBrand(@Body() brandData: CreateBrandDTO) {
|
||||
try {
|
||||
const hasBrand = await this.productService.hasAttribute(
|
||||
'brand',
|
||||
brandData.name
|
||||
);
|
||||
if (hasBrand) {
|
||||
return errorResponse('品牌已存在');
|
||||
}
|
||||
let data = await this.productService.createBrand(brandData);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: ProductBrandRes,
|
||||
})
|
||||
@Put('/brand/:id')
|
||||
async updateBrand(
|
||||
@Param('id') id: number,
|
||||
@Body() brandData: UpdateBrandDTO
|
||||
// 通用属性接口:创建
|
||||
@ApiOkResponse()
|
||||
@Post('/attribute')
|
||||
async createAttribute(
|
||||
@Query('dictName') dictName: string,
|
||||
@Body() body: { title: string; name: string }
|
||||
) {
|
||||
try {
|
||||
const hasBrand = await this.productService.hasAttribute(
|
||||
'brand',
|
||||
brandData.name,
|
||||
id
|
||||
const hasItem = await this.productService.hasAttribute(
|
||||
dictName,
|
||||
body.name
|
||||
);
|
||||
if (hasBrand) {
|
||||
return errorResponse('品牌已存在');
|
||||
if (hasItem) return errorResponse('字典项已存在');
|
||||
const data = await this.productService.createAttribute(dictName, body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 通用属性接口:更新
|
||||
@ApiOkResponse()
|
||||
@Put('/attribute/:id')
|
||||
async updateAttribute(
|
||||
@Param('id') id: number,
|
||||
@Query('dictName') dictName: string,
|
||||
@Body() body: { title?: string; name?: string }
|
||||
) {
|
||||
try {
|
||||
if (body?.name) {
|
||||
const hasItem = await this.productService.hasAttribute(
|
||||
dictName,
|
||||
body.name,
|
||||
id
|
||||
);
|
||||
if (hasItem) return errorResponse('字典项已存在');
|
||||
}
|
||||
const data = this.productService.updateBrand(id, brandData);
|
||||
const data = await this.productService.updateAttribute(id, body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Del('/brand/:id')
|
||||
async deleteBrand(@Param('id') id: number) {
|
||||
// 通用属性接口:删除
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Del('/attribute/:id')
|
||||
async deleteAttribute(@Param('id') id: number) {
|
||||
try {
|
||||
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||
if (hasProducts) throw new Error('该品牌下有商品,无法删除');
|
||||
const data = await this.productService.deleteBrand(id);
|
||||
return successResponse(data);
|
||||
await this.productService.deleteAttribute(id);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/batchSetSku')
|
||||
@ApiOkResponse({
|
||||
description: '批量设置 sku 的响应结果',
|
||||
type: BooleanRes,
|
||||
})
|
||||
async batchSetSku(@Body() body: BatchSetSkuDTO) {
|
||||
// 兼容旧接口:品牌
|
||||
@ApiOkResponse()
|
||||
@Get('/brandAll')
|
||||
async compatBrandAll() {
|
||||
try {
|
||||
const result = await this.productService.batchSetSku(body.skus);
|
||||
return successResponse(result, '批量设置 sku 成功');
|
||||
const data = await this.productService.getAttributeAll('brand'); // 中文注释:返回所有品牌字典项
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message, 400);
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/flavorsAll')
|
||||
async getFlavorsAll() {
|
||||
@Get('/brands')
|
||||
async compatBrands(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||
try {
|
||||
let data = await this.productService.getFlavorsAll();
|
||||
const data = await this.productService.getAttributeList('brand', { current, pageSize }, name); // 中文注释:分页品牌列表
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Post('/brand')
|
||||
async compatCreateBrand(@Body() body: { title: string; name: string }) {
|
||||
try {
|
||||
const has = await this.productService.hasAttribute('brand', body.name); // 中文注释:唯一性校验
|
||||
if (has) return errorResponse('品牌已存在');
|
||||
const data = await this.productService.createAttribute('brand', body); // 中文注释:创建品牌字典项
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Put('/brand/:id')
|
||||
async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||
try {
|
||||
if (body?.name) {
|
||||
const has = await this.productService.hasAttribute('brand', body.name, id); // 中文注释:唯一性校验(排除自身)
|
||||
if (has) return errorResponse('品牌已存在');
|
||||
}
|
||||
const data = await this.productService.updateAttribute(id, body); // 中文注释:更新品牌字典项
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Del('/brand/:id')
|
||||
async compatDeleteBrand(@Param('id') id: number) {
|
||||
try {
|
||||
await this.productService.deleteAttribute(id); // 中文注释:删除品牌字典项
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧接口:口味
|
||||
@ApiOkResponse()
|
||||
@Get('/flavorsAll')
|
||||
async compatFlavorsAll() {
|
||||
try {
|
||||
const data = await this.productService.getAttributeAll('flavor');
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -270,13 +282,9 @@ export class ProductController {
|
|||
|
||||
@ApiOkResponse()
|
||||
@Get('/flavors')
|
||||
async getFlavors(@Query() query: QueryFlavorsDTO) {
|
||||
const { current = 1, pageSize = 10, name } = query;
|
||||
async compatFlavors(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||
try {
|
||||
let data = await this.productService.getFlavorsList(
|
||||
{ current, pageSize },
|
||||
name
|
||||
);
|
||||
const data = await this.productService.getAttributeList('flavor', { current, pageSize }, name);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -285,13 +293,11 @@ export class ProductController {
|
|||
|
||||
@ApiOkResponse()
|
||||
@Post('/flavors')
|
||||
async createFlavors(@Body() flavorsData: CreateFlavorsDTO) {
|
||||
async compatCreateFlavors(@Body() body: { title: string; name: string }) {
|
||||
try {
|
||||
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name);
|
||||
if (hasFlavors) {
|
||||
return errorResponse('口味已存在');
|
||||
}
|
||||
let data = await this.productService.createFlavors(flavorsData);
|
||||
const has = await this.productService.hasAttribute('flavor', body.name);
|
||||
if (has) return errorResponse('口味已存在');
|
||||
const data = await this.productService.createAttribute('flavor', body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -300,42 +306,36 @@ export class ProductController {
|
|||
|
||||
@ApiOkResponse()
|
||||
@Put('/flavors/:id')
|
||||
async updateFlavors(
|
||||
@Param('id') id: number,
|
||||
@Body() flavorsData: UpdateFlavorsDTO
|
||||
) {
|
||||
async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||
try {
|
||||
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name, id);
|
||||
if (hasFlavors) {
|
||||
return errorResponse('口味已存在');
|
||||
if (body?.name) {
|
||||
const has = await this.productService.hasAttribute('flavor', body.name, id);
|
||||
if (has) return errorResponse('口味已存在');
|
||||
}
|
||||
const data = this.productService.updateFlavors(id, flavorsData);
|
||||
const data = await this.productService.updateAttribute(id, body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Del('/flavors/:id')
|
||||
async deleteFlavors(@Param('id') id: number) {
|
||||
async compatDeleteFlavors(@Param('id') id: number) {
|
||||
try {
|
||||
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||
if (hasProducts) throw new Error('该口味下有商品,无法删除');
|
||||
const data = await this.productService.deleteFlavors(id);
|
||||
return successResponse(data);
|
||||
await this.productService.deleteAttribute(id);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧接口:规格
|
||||
@ApiOkResponse()
|
||||
@Get('/strengthAll')
|
||||
async getStrengthAll() {
|
||||
async compatStrengthAll() {
|
||||
try {
|
||||
let data = await this.productService.getStrengthAll();
|
||||
const data = await this.productService.getAttributeAll('strength');
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -344,13 +344,9 @@ export class ProductController {
|
|||
|
||||
@ApiOkResponse()
|
||||
@Get('/strength')
|
||||
async getStrength(@Query() query: QueryStrengthDTO) {
|
||||
const { current = 1, pageSize = 10, name } = query;
|
||||
async compatStrength(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||
try {
|
||||
let data = await this.productService.getStrengthList(
|
||||
{ current, pageSize },
|
||||
name
|
||||
);
|
||||
const data = await this.productService.getAttributeList('strength', { current, pageSize }, name);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -359,16 +355,11 @@ export class ProductController {
|
|||
|
||||
@ApiOkResponse()
|
||||
@Post('/strength')
|
||||
async createStrength(@Body() strengthData: CreateStrengthDTO) {
|
||||
async compatCreateStrength(@Body() body: { title: string; name: string }) {
|
||||
try {
|
||||
const hasStrength = await this.productService.hasAttribute(
|
||||
'strength',
|
||||
strengthData.name
|
||||
);
|
||||
if (hasStrength) {
|
||||
return errorResponse('规格已存在');
|
||||
}
|
||||
let data = await this.productService.createStrength(strengthData);
|
||||
const has = await this.productService.hasAttribute('strength', body.name);
|
||||
if (has) return errorResponse('规格已存在');
|
||||
const data = await this.productService.createAttribute('strength', body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -377,109 +368,75 @@ export class ProductController {
|
|||
|
||||
@ApiOkResponse()
|
||||
@Put('/strength/:id')
|
||||
async updateStrength(
|
||||
@Param('id') id: number,
|
||||
@Body() strengthData: UpdateStrengthDTO
|
||||
) {
|
||||
async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||
try {
|
||||
const hasStrength = await this.productService.hasAttribute(
|
||||
'strength',
|
||||
strengthData.name,
|
||||
id
|
||||
);
|
||||
if (hasStrength) {
|
||||
return errorResponse('规格已存在');
|
||||
if (body?.name) {
|
||||
const has = await this.productService.hasAttribute('strength', body.name, id);
|
||||
if (has) return errorResponse('规格已存在');
|
||||
}
|
||||
const data = this.productService.updateStrength(id, strengthData);
|
||||
const data = await this.productService.updateAttribute(id, body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Del('/strength/:id')
|
||||
async deleteStrength(@Param('id') id: number) {
|
||||
async compatDeleteStrength(@Param('id') id: number) {
|
||||
try {
|
||||
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||
if (hasProducts) throw new Error('该规格下有商品,无法删除');
|
||||
const data = await this.productService.deleteStrength(id);
|
||||
return successResponse(data);
|
||||
await this.productService.deleteAttribute(id);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// size 路由与增删改查
|
||||
// 兼容旧接口:尺寸
|
||||
@ApiOkResponse()
|
||||
@Get('/sizeAll')
|
||||
async getSizeAll() {
|
||||
async compatSizeAll() {
|
||||
try {
|
||||
// 中文注释:获取所有尺寸项
|
||||
const data = await this.productService.getSizeAll();
|
||||
const data = await this.productService.getAttributeAll('size');
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: ProductSizeListRes })
|
||||
@ApiOkResponse()
|
||||
@Get('/size')
|
||||
async getSize(@Query() query: QuerySizeDTO) {
|
||||
// 中文注释:解析分页与关键字
|
||||
const { current = 1, pageSize = 10, name } = query;
|
||||
async compatSize(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||
try {
|
||||
// 中文注释:分页查询尺寸列表
|
||||
const data = await this.productService.getSizeList(
|
||||
{ current, pageSize },
|
||||
name
|
||||
);
|
||||
const data = await this.productService.getAttributeList('size', { current, pageSize }, name);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: ProductSizeRes })
|
||||
@ApiOkResponse()
|
||||
@Post('/size')
|
||||
async createSize(@Body() sizeData: CreateSizeDTO) {
|
||||
async compatCreateSize(@Body() body: { title: string; name: string }) {
|
||||
try {
|
||||
// 条件判断(中文注释:唯一性校验,禁止重复)
|
||||
const hasSize = await this.productService.hasAttribute(
|
||||
'size',
|
||||
sizeData.name
|
||||
);
|
||||
if (hasSize) {
|
||||
return errorResponse('尺寸已存在');
|
||||
}
|
||||
// 调用服务创建(中文注释:新增尺寸项)
|
||||
const data = await this.productService.createSize(sizeData);
|
||||
const has = await this.productService.hasAttribute('size', body.name);
|
||||
if (has) return errorResponse('尺寸已存在');
|
||||
const data = await this.productService.createAttribute('size', body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: ProductSizeRes })
|
||||
@ApiOkResponse()
|
||||
@Put('/size/:id')
|
||||
async updateSize(
|
||||
@Param('id') id: number,
|
||||
@Body() sizeData: UpdateSizeDTO
|
||||
) {
|
||||
async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||
try {
|
||||
// 条件判断(中文注释:唯一性校验,排除自身)
|
||||
const hasSize = await this.productService.hasAttribute(
|
||||
'size',
|
||||
sizeData.name,
|
||||
id
|
||||
);
|
||||
if (hasSize) {
|
||||
return errorResponse('尺寸已存在');
|
||||
if (body?.name) {
|
||||
const has = await this.productService.hasAttribute('size', body.name, id);
|
||||
if (has) return errorResponse('尺寸已存在');
|
||||
}
|
||||
// 调用服务更新(中文注释:提交变更)
|
||||
const data = await this.productService.updateSize(id, sizeData);
|
||||
const data = await this.productService.updateAttribute(id, body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -488,14 +445,10 @@ export class ProductController {
|
|||
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Del('/size/:id')
|
||||
async deleteSize(@Param('id') id: number) {
|
||||
async compatDeleteSize(@Param('id') id: number) {
|
||||
try {
|
||||
// 条件判断(中文注释:若有商品关联则不可删除)
|
||||
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||
if (hasProducts) throw new Error('该尺寸下有商品,无法删除');
|
||||
// 调用服务删除(中文注释:返回是否成功)
|
||||
const data = await this.productService.deleteSize(id);
|
||||
return successResponse(data);
|
||||
await this.productService.deleteAttribute(id);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ export class UpdateDictItemDTO {
|
|||
title?: string; // 字典项标题 (可选)
|
||||
|
||||
@Rule(RuleType.string().allow(null))
|
||||
value?: string; // 字典项值 (可选)
|
||||
titleCN?: string; // 字典项中文标题 (可选)
|
||||
|
||||
@Rule(RuleType.string().allow(null))
|
||||
titleCN?: string; // 字典项中文标题 (可选)
|
||||
value?: string; // 字典项值 (可选)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,35 +21,52 @@ export class CreateProductDTO {
|
|||
@Rule(RuleType.string())
|
||||
sku?: string;
|
||||
|
||||
@ApiProperty({ description: '品牌 ID', type: 'number' })
|
||||
@Rule(RuleType.number().required())
|
||||
brandId: number;
|
||||
|
||||
@ApiProperty({ description: '规格 ID', type: 'number' })
|
||||
@Rule(RuleType.number().required())
|
||||
strengthId: number;
|
||||
|
||||
@ApiProperty({ description: '口味 ID', type: 'number' })
|
||||
@Rule(RuleType.number().required())
|
||||
flavorsId: number;
|
||||
|
||||
@ApiProperty()
|
||||
@Rule(RuleType.string())
|
||||
humidity: string;
|
||||
// 通用属性输入(中文注释:通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
||||
@ApiProperty({ description: '属性列表', type: 'array' })
|
||||
@Rule(RuleType.array().required())
|
||||
attributes: AttributeInputDTO[];
|
||||
|
||||
// 商品价格
|
||||
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||
@Rule(RuleType.number())
|
||||
price?: number;
|
||||
|
||||
// 促销价格
|
||||
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
|
||||
@Rule(RuleType.number())
|
||||
promotionPrice?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于更新产品
|
||||
*/
|
||||
export class UpdateProductDTO extends CreateProductDTO {
|
||||
export class UpdateProductDTO {
|
||||
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ example: '产品描述', description: '产品描述' })
|
||||
@Rule(RuleType.string())
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ description: '产品 SKU', required: false })
|
||||
@Rule(RuleType.string())
|
||||
sku?: string;
|
||||
|
||||
// 商品价格
|
||||
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||
@Rule(RuleType.number())
|
||||
price?: number;
|
||||
|
||||
// 促销价格
|
||||
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
|
||||
@Rule(RuleType.number())
|
||||
promotionPrice?: number;
|
||||
|
||||
// 属性更新(中文注释:可选,支持增量替换指定字典的属性项)
|
||||
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||
@Rule(RuleType.array())
|
||||
attributes?: AttributeInputDTO[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,6 +90,25 @@ export class QueryProductDTO {
|
|||
brandId: number;
|
||||
}
|
||||
|
||||
// 属性输入项(中文注释:用于在创建/更新产品时传递字典项信息)
|
||||
export class AttributeInputDTO {
|
||||
@ApiProperty({ description: '字典名称', example: 'brand' })
|
||||
@Rule(RuleType.string().required())
|
||||
dictName: string;
|
||||
|
||||
@ApiProperty({ description: '字典项 ID', required: false })
|
||||
@Rule(RuleType.number())
|
||||
id?: number;
|
||||
|
||||
@ApiProperty({ description: '字典项显示名称', required: false })
|
||||
@Rule(RuleType.string())
|
||||
title?: string;
|
||||
|
||||
@ApiProperty({ description: '字典项唯一标识', required: false })
|
||||
@Rule(RuleType.string())
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于创建品牌
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -164,57 +164,14 @@ export class ProductService {
|
|||
|
||||
const [items, total] = await qb.getManyAndCount();
|
||||
|
||||
// 获取所有 SKU 的库存信息
|
||||
const skus = items.map(item => item.sku).filter(Boolean);
|
||||
const stocks = await this.stockService.getStocksBySkus(skus);
|
||||
|
||||
// 将库存信息映射到 SKU
|
||||
const stockMap = stocks.reduce((map, stock) => {
|
||||
map[stock.productSku] = stock.totalQuantity;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// 格式化返回的数据(中文注释:将品牌/口味/规格/尺寸均以 DictItem 对象形式返回,并保留 attributes 列表)
|
||||
const formattedItems = items.map(product => {
|
||||
// 函数(中文注释:按字典名称获取对应的属性对象)
|
||||
const getAttributeByDict = (dictName: string) =>
|
||||
product.attributes.find(a => a.dict?.name === dictName) || null;
|
||||
// 条件判断(中文注释:从属性中取出各类维度对象)
|
||||
const brand = getAttributeByDict('brand');
|
||||
const flavors = getAttributeByDict('flavor');
|
||||
const strength = getAttributeByDict('strength');
|
||||
const size = getAttributeByDict('size');
|
||||
|
||||
return {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
nameCn: product.nameCn,
|
||||
description: product.description,
|
||||
sku: product.sku,
|
||||
stock: stockMap[product.sku] || 0, // 中文注释:库存使用聚合库存值
|
||||
price: product.price,
|
||||
promotionPrice: product.promotionPrice,
|
||||
source: product.source, // 中文注释:返回产品来源字段
|
||||
createdAt: product.createdAt,
|
||||
updatedAt: product.updatedAt,
|
||||
// 单列属性(中文注释:直接返回 DictItem 对象,方便前端展示与使用)
|
||||
brand,
|
||||
flavors,
|
||||
strength,
|
||||
size,
|
||||
// 全量属性列表(中文注释:保留原 attributes,包含字典关系)
|
||||
attributes: product.attributes,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
items: formattedItems,
|
||||
items,
|
||||
total,
|
||||
...pagination,
|
||||
};
|
||||
}
|
||||
|
||||
async getOrCreateDictItem(
|
||||
async getOrCreateAttribute(
|
||||
dictName: string,
|
||||
itemTitle: string,
|
||||
itemName?: string
|
||||
|
|
@ -243,25 +200,32 @@ export class ProductService {
|
|||
}
|
||||
|
||||
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
||||
const { name, description, brandId, flavorsId, strengthId, humidity, sku, price } =
|
||||
createProductDTO;
|
||||
const { name, description, attributes, sku, price } = createProductDTO;
|
||||
|
||||
// 获取或创建品牌、口味、规格
|
||||
const brandItem = await this.dictItemModel.findOne({ where: { id: brandId } });
|
||||
if (!brandItem) throw new Error('品牌不存在');
|
||||
// 条件判断(中文注释:校验属性输入)
|
||||
if (!Array.isArray(attributes) || attributes.length === 0) {
|
||||
throw new Error('属性列表不能为空');
|
||||
}
|
||||
|
||||
const flavorItem = await this.dictItemModel.findOne({ where: { id: flavorsId } });
|
||||
if (!flavorItem) throw new Error('口味不存在');
|
||||
// 解析属性输入(中文注释:按 id 或 dictName 创建/关联字典项)
|
||||
const resolvedAttributes: DictItem[] = [];
|
||||
for (const attr of attributes) {
|
||||
if (!attr?.dictName) throw new Error('属性项缺少字典名称');
|
||||
let item: DictItem | null = null;
|
||||
if (attr.id) {
|
||||
item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] });
|
||||
if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`);
|
||||
} else {
|
||||
const titleOrName = attr.title || attr.name;
|
||||
if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name');
|
||||
item = await this.getOrCreateAttribute(attr.dictName, titleOrName, attr.name);
|
||||
}
|
||||
resolvedAttributes.push(item);
|
||||
}
|
||||
|
||||
const strengthItem = await this.dictItemModel.findOne({ where: { id: strengthId } });
|
||||
if (!strengthItem) throw new Error('规格不存在');
|
||||
|
||||
const humidityItem = await this.getOrCreateDictItem('humidity', humidity);
|
||||
|
||||
// 检查具有完全相同属性组合的产品是否已存在
|
||||
const attributesToMatch = [brandItem, flavorItem, strengthItem, humidityItem];
|
||||
// 检查完全相同属性组合是否已存在(中文注释:避免重复)
|
||||
const qb = this.productModel.createQueryBuilder('product');
|
||||
attributesToMatch.forEach((attr, index) => {
|
||||
resolvedAttributes.forEach((attr, index) => {
|
||||
qb.innerJoin(
|
||||
'product.attributes',
|
||||
`attr${index}`,
|
||||
|
|
@ -269,30 +233,32 @@ export class ProductService {
|
|||
{ [`attrId${index}`]: attr.id }
|
||||
);
|
||||
});
|
||||
const isExit = await qb.getOne();
|
||||
const isExist = await qb.getOne();
|
||||
if (isExist) throw new Error('产品已存在');
|
||||
|
||||
if (isExit) throw new Error('产品已存在');
|
||||
|
||||
// 创建新产品实例
|
||||
// 创建新产品实例(中文注释:绑定属性与基础字段)
|
||||
const product = new Product();
|
||||
product.name = name;
|
||||
product.description = description;
|
||||
product.attributes = attributesToMatch;
|
||||
product.attributes = resolvedAttributes;
|
||||
|
||||
// 如果用户提供了 sku,则直接使用;否则,通过模板引擎生成
|
||||
// 生成或设置 SKU(中文注释:基于属性字典项的 name 生成)
|
||||
if (sku) {
|
||||
product.sku = sku;
|
||||
} else {
|
||||
// 生成 SKU
|
||||
const attributeMap: Record<string, string> = {};
|
||||
for (const a of resolvedAttributes) {
|
||||
if (a?.dict?.name && a?.name) attributeMap[a.dict.name] = a.name;
|
||||
}
|
||||
product.sku = await this.templateService.render('product_sku', {
|
||||
brand: brandItem.name,
|
||||
flavor: flavorItem.name,
|
||||
strength: strengthItem.name,
|
||||
humidity: humidityItem.name,
|
||||
brand: attributeMap['brand'] || '',
|
||||
flavor: attributeMap['flavor'] || '',
|
||||
strength: attributeMap['strength'] || '',
|
||||
humidity: attributeMap['humidity'] || '',
|
||||
});
|
||||
}
|
||||
|
||||
// 价格与促销价
|
||||
// 价格与促销价(中文注释:可选字段)
|
||||
if (price !== undefined) {
|
||||
product.price = Number(price);
|
||||
}
|
||||
|
|
@ -301,7 +267,6 @@ export class ProductService {
|
|||
product.promotionPrice = Number(promotionPrice);
|
||||
}
|
||||
|
||||
// 保存产品
|
||||
return await this.productModel.save(product);
|
||||
}
|
||||
|
||||
|
|
@ -340,40 +305,31 @@ export class ProductService {
|
|||
}
|
||||
}
|
||||
|
||||
// 处理属性更新(品牌/口味/强度/干湿)
|
||||
const nextAttributes: DictItem[] = [...(product.attributes || [])];
|
||||
// 处理属性更新(中文注释:若传入 attributes 则按字典名称替换对应项)
|
||||
if (Array.isArray(updateProductDTO.attributes) && updateProductDTO.attributes.length > 0) {
|
||||
const nextAttributes: DictItem[] = [...(product.attributes || [])];
|
||||
|
||||
// 根据 dict.name 查找或替换已有属性
|
||||
const replaceAttr = (dictName: string, item: DictItem) => {
|
||||
const idx = nextAttributes.findIndex(a => a.dict?.name === dictName);
|
||||
if (idx >= 0) nextAttributes[idx] = item; else nextAttributes.push(item);
|
||||
};
|
||||
const replaceAttr = (dictName: string, item: DictItem) => {
|
||||
const idx = nextAttributes.findIndex(a => a.dict?.name === dictName);
|
||||
if (idx >= 0) nextAttributes[idx] = item; else nextAttributes.push(item);
|
||||
};
|
||||
|
||||
// 品牌
|
||||
if (updateProductDTO.brandId !== undefined) {
|
||||
const brandItem = await this.dictItemModel.findOne({ where: { id: updateProductDTO.brandId }, relations: ['dict'] });
|
||||
if (!brandItem) throw new Error('品牌不存在');
|
||||
replaceAttr('brand', brandItem);
|
||||
}
|
||||
// 口味
|
||||
if (updateProductDTO.flavorsId !== undefined) {
|
||||
const flavorItem = await this.dictItemModel.findOne({ where: { id: updateProductDTO.flavorsId }, relations: ['dict'] });
|
||||
if (!flavorItem) throw new Error('口味不存在');
|
||||
replaceAttr('flavor', flavorItem);
|
||||
}
|
||||
// 强度
|
||||
if (updateProductDTO.strengthId !== undefined) {
|
||||
const strengthItem = await this.dictItemModel.findOne({ where: { id: updateProductDTO.strengthId }, relations: ['dict'] });
|
||||
if (!strengthItem) throw new Error('规格不存在');
|
||||
replaceAttr('strength', strengthItem);
|
||||
}
|
||||
// 干湿(按 title 获取或创建)
|
||||
if (updateProductDTO.humidity !== undefined) {
|
||||
const humidityItem = await this.getOrCreateDictItem('humidity', updateProductDTO.humidity);
|
||||
replaceAttr('humidity', humidityItem);
|
||||
}
|
||||
for (const attr of updateProductDTO.attributes) {
|
||||
if (!attr?.dictName) throw new Error('属性项缺少字典名称');
|
||||
let item: DictItem | null = null;
|
||||
if (attr.id) {
|
||||
item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] });
|
||||
if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`);
|
||||
} else {
|
||||
const titleOrName = attr.title || attr.name;
|
||||
if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name');
|
||||
item = await this.getOrCreateAttribute(attr.dictName, titleOrName, attr.name);
|
||||
}
|
||||
replaceAttr(attr.dictName, item);
|
||||
}
|
||||
|
||||
product.attributes = nextAttributes;
|
||||
product.attributes = nextAttributes;
|
||||
}
|
||||
|
||||
// 保存更新后的产品
|
||||
const saved = await this.productModel.save(product);
|
||||
|
|
@ -752,6 +708,75 @@ export class ProductService {
|
|||
return await this.dictItemModel.save(strength);
|
||||
}
|
||||
|
||||
// 通用属性:分页获取指定字典的字典项
|
||||
async getAttributeList(
|
||||
dictName: string,
|
||||
pagination: PaginationParams,
|
||||
name?: string
|
||||
): Promise<BrandPaginatedResponse> {
|
||||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||||
if (!dict) return { items: [], total: 0, ...pagination } as any;
|
||||
const where: any = { dict: { id: dict.id } };
|
||||
if (name) where.title = Like(`%${name}%`);
|
||||
const [items, total] = await this.dictItemModel.findAndCount({
|
||||
where,
|
||||
skip: (pagination.current - 1) * pagination.pageSize,
|
||||
take: pagination.pageSize,
|
||||
order: { sort: 'ASC', id: 'DESC' },
|
||||
relations: ['dict'],
|
||||
});
|
||||
return { items, total, ...pagination } as any;
|
||||
}
|
||||
|
||||
// 通用属性:获取指定字典的全部字典项
|
||||
async getAttributeAll(dictName: string): Promise<DictItem[]> {
|
||||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||||
if (!dict) return [];
|
||||
return this.dictItemModel.find({
|
||||
where: { dict: { id: dict.id } },
|
||||
order: { sort: 'ASC', id: 'DESC' },
|
||||
relations: ['dict'],
|
||||
});
|
||||
}
|
||||
|
||||
// 通用属性:创建字典项
|
||||
async createAttribute(
|
||||
dictName: string,
|
||||
payload: { title: string; name: string }
|
||||
): Promise<DictItem> {
|
||||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||||
if (!dict) throw new Error(`字典 ${dictName} 不存在`);
|
||||
const exists = await this.dictItemModel.findOne({
|
||||
where: { name: payload.name, dict: { id: dict.id } },
|
||||
relations: ['dict'],
|
||||
});
|
||||
if (exists) throw new Error('字典项已存在');
|
||||
const item = new DictItem();
|
||||
item.title = payload.title;
|
||||
item.name = payload.name;
|
||||
item.dict = dict;
|
||||
return await this.dictItemModel.save(item);
|
||||
}
|
||||
|
||||
// 通用属性:更新字典项
|
||||
async updateAttribute(
|
||||
id: number,
|
||||
payload: { title?: string; name?: string }
|
||||
): Promise<DictItem> {
|
||||
const item = await this.dictItemModel.findOne({ where: { id } });
|
||||
if (!item) throw new Error('字典项不存在');
|
||||
if (payload.title !== undefined) item.title = payload.title;
|
||||
if (payload.name !== undefined) item.name = payload.name;
|
||||
return await this.dictItemModel.save(item);
|
||||
}
|
||||
|
||||
// 通用属性:删除字典项(若存在产品关联则禁止删除)
|
||||
async deleteAttribute(id: number): Promise<void> {
|
||||
const hasProducts = await this.hasProductsInAttribute(id);
|
||||
if (hasProducts) throw new Error('当前字典项存在关联产品,无法删除');
|
||||
await this.dictItemModel.delete({ id });
|
||||
}
|
||||
|
||||
async updateStrength(id: number, updateStrength: UpdateStrengthDTO) {
|
||||
const strength = await this.dictItemModel.findOneBy({ id });
|
||||
if (!strength) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue