feat(字典): 重构字典模块并实现产品属性关联
重构字典模块,支持字典项与产品的多对多关联 添加字典项导入导出功能,支持XLSX模板下载 优化产品管理,使用字典项作为产品属性 新增字典项排序和值字段 修改数据源配置,添加字典种子数据
This commit is contained in:
parent
889f00bde8
commit
0809840507
|
|
@ -25,6 +25,7 @@
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
|
"csv-parse": "^6.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"mysql2": "^3.11.5",
|
"mysql2": "^3.11.5",
|
||||||
"nodemailer": "^7.0.5",
|
"nodemailer": "^7.0.5",
|
||||||
|
|
@ -1917,6 +1918,12 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/csv-parse": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.18",
|
"version": "1.11.18",
|
||||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz",
|
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
|
"csv-parse": "^6.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"mysql2": "^3.11.5",
|
"mysql2": "^3.11.5",
|
||||||
"nodemailer": "^7.0.5",
|
"nodemailer": "^7.0.5",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import { Subscription } from '../entity/subscription.entity';
|
||||||
import { Site } from '../entity/site.entity';
|
import { Site } from '../entity/site.entity';
|
||||||
import { Dict } from '../entity/dict.entity';
|
import { Dict } from '../entity/dict.entity';
|
||||||
import { DictItem } from '../entity/dict_item.entity';
|
import { DictItem } from '../entity/dict_item.entity';
|
||||||
|
import DictSeeder from '../db/seeds/dict.seeder';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// use for cookie sign key, should change to your own and keep security
|
// use for cookie sign key, should change to your own and keep security
|
||||||
|
|
@ -77,6 +78,7 @@ export default {
|
||||||
],
|
],
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false,
|
logging: false,
|
||||||
|
seeders: [DictSeeder],
|
||||||
},
|
},
|
||||||
dataSource: {
|
dataSource: {
|
||||||
default: {
|
default: {
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,171 @@
|
||||||
import { Inject, Controller, Get, Post, Put, Del, Query, Body, Param } from '@midwayjs/core';
|
|
||||||
|
import { Inject, Controller, Get, Post, Put, Del, Query, Body, Param, Files, ContentType } from '@midwayjs/core';
|
||||||
import { DictService } from '../service/dict.service';
|
import { DictService } from '../service/dict.service';
|
||||||
import { CreateDictDTO, UpdateDictDTO, CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
import { CreateDictDTO, UpdateDictDTO, CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
||||||
import { Validate } from '@midwayjs/validate';
|
import { Validate } from '@midwayjs/validate';
|
||||||
|
import { Context } from '@midwayjs/koa';
|
||||||
|
|
||||||
@Controller('/api')
|
/**
|
||||||
|
* 字典管理
|
||||||
|
* @decorator Controller
|
||||||
|
*/
|
||||||
|
@Controller('/dict')
|
||||||
export class DictController {
|
export class DictController {
|
||||||
@Inject()
|
@Inject()
|
||||||
dictService: DictService;
|
dictService: DictService;
|
||||||
|
|
||||||
// 获取字典列表
|
@Inject()
|
||||||
@Get('/dicts')
|
ctx: Context;
|
||||||
async getDicts(@Query('title') title?: string) {
|
|
||||||
return this.dictService.getDicts(title);
|
/**
|
||||||
|
* 批量导入字典
|
||||||
|
* @param files 上传的文件
|
||||||
|
*/
|
||||||
|
@Post('/import')
|
||||||
|
@Validate()
|
||||||
|
async importDicts(@Files() files: any) {
|
||||||
|
// 从上传的文件列表中获取第一个文件
|
||||||
|
const file = files[0];
|
||||||
|
// 调用服务层方法处理XLSX文件
|
||||||
|
const result = await this.dictService.importDictsFromXLSX(file.data);
|
||||||
|
// 返回导入结果
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新字典
|
/**
|
||||||
@Post('/dicts')
|
* 下载字典XLSX模板
|
||||||
|
*/
|
||||||
|
@Get('/template')
|
||||||
|
@ContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
async downloadDictTemplate() {
|
||||||
|
// 设置下载文件的名称
|
||||||
|
this.ctx.set('Content-Disposition', 'attachment; filename=dict-template.xlsx');
|
||||||
|
// 返回XLSX模板内容
|
||||||
|
return this.dictService.getDictXLSXTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个字典及其所有字典项
|
||||||
|
* @param id 字典ID
|
||||||
|
*/
|
||||||
|
@Get('/:id')
|
||||||
|
async getDict(@Param('id') id: number) {
|
||||||
|
// 调用服务层方法,并关联查询字典项
|
||||||
|
return this.dictService.getDict({ id }, ['items']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典列表
|
||||||
|
* @param title 字典标题 (模糊查询)
|
||||||
|
* @param name 字典名称 (模糊查询)
|
||||||
|
*/
|
||||||
|
@Get('/list')
|
||||||
|
async getDicts(@Query('title') title?: string, @Query('name') name?: string) {
|
||||||
|
// 调用服务层方法
|
||||||
|
return this.dictService.getDicts({ title, name });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新字典
|
||||||
|
* @param createDictDTO 字典数据
|
||||||
|
*/
|
||||||
|
@Post('/')
|
||||||
@Validate()
|
@Validate()
|
||||||
async createDict(@Body() createDictDTO: CreateDictDTO) {
|
async createDict(@Body() createDictDTO: CreateDictDTO) {
|
||||||
|
// 调用服务层方法
|
||||||
return this.dictService.createDict(createDictDTO);
|
return this.dictService.createDict(createDictDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新字典
|
/**
|
||||||
@Put('/dicts/:id')
|
* 更新字典
|
||||||
|
* @param id 字典ID
|
||||||
|
* @param updateDictDTO 待更新的字典数据
|
||||||
|
*/
|
||||||
|
@Put('/:id')
|
||||||
@Validate()
|
@Validate()
|
||||||
async updateDict(@Param('id') id: number, @Body() updateDictDTO: UpdateDictDTO) {
|
async updateDict(@Param('id') id: number, @Body() updateDictDTO: UpdateDictDTO) {
|
||||||
|
// 调用服务层方法
|
||||||
return this.dictService.updateDict(id, updateDictDTO);
|
return this.dictService.updateDict(id, updateDictDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除字典
|
/**
|
||||||
@Del('/dicts/:id')
|
* 删除字典
|
||||||
|
* @param id 字典ID
|
||||||
|
*/
|
||||||
|
@Del('/:id')
|
||||||
async deleteDict(@Param('id') id: number) {
|
async deleteDict(@Param('id') id: number) {
|
||||||
|
// 调用服务层方法
|
||||||
return this.dictService.deleteDict(id);
|
return this.dictService.deleteDict(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取字典项列表
|
/**
|
||||||
@Get('/dict-items')
|
* 批量导入字典项
|
||||||
|
* @param files 上传的文件
|
||||||
|
* @param body 请求体,包含字典ID
|
||||||
|
*/
|
||||||
|
@Post('/item/import')
|
||||||
|
@Validate()
|
||||||
|
async importDictItems(@Files() files: any, @Body() body: { dictId: number }) {
|
||||||
|
// 获取第一个文件
|
||||||
|
const file = files[0];
|
||||||
|
// 调用服务层方法
|
||||||
|
const result = await this.dictService.importDictItemsFromXLSX(file.data, body.dictId);
|
||||||
|
// 返回结果
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载字典项XLSX模板
|
||||||
|
*/
|
||||||
|
@Get('/item/template')
|
||||||
|
@ContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
async downloadDictItemTemplate() {
|
||||||
|
// 设置下载文件名
|
||||||
|
this.ctx.set('Content-Disposition', 'attachment; filename=dict-item-template.xlsx');
|
||||||
|
// 返回模板内容
|
||||||
|
return this.dictService.getDictItemXLSXTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典项列表
|
||||||
|
* @param dictId 字典ID (可选)
|
||||||
|
*/
|
||||||
|
@Get('/items')
|
||||||
async getDictItems(@Query('dictId') dictId?: number) {
|
async getDictItems(@Query('dictId') dictId?: number) {
|
||||||
|
// 调用服务层方法
|
||||||
return this.dictService.getDictItems(dictId);
|
return this.dictService.getDictItems(dictId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新字典项
|
/**
|
||||||
@Post('/dict-items')
|
* 创建新字典项
|
||||||
|
* @param createDictItemDTO 字典项数据
|
||||||
|
*/
|
||||||
|
@Post('/item')
|
||||||
@Validate()
|
@Validate()
|
||||||
async createDictItem(@Body() createDictItemDTO: CreateDictItemDTO) {
|
async createDictItem(@Body() createDictItemDTO: CreateDictItemDTO) {
|
||||||
|
// 调用服务层方法
|
||||||
return this.dictService.createDictItem(createDictItemDTO);
|
return this.dictService.createDictItem(createDictItemDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新字典项
|
/**
|
||||||
@Put('/dict-items/:id')
|
* 更新字典项
|
||||||
|
* @param id 字典项ID
|
||||||
|
* @param updateDictItemDTO 待更新的字典项数据
|
||||||
|
*/
|
||||||
|
@Put('/item/:id')
|
||||||
@Validate()
|
@Validate()
|
||||||
async updateDictItem(@Param('id') id: number, @Body() updateDictItemDTO: UpdateDictItemDTO) {
|
async updateDictItem(@Param('id') id: number, @Body() updateDictItemDTO: UpdateDictItemDTO) {
|
||||||
|
// 调用服务层方法
|
||||||
return this.dictService.updateDictItem(id, updateDictItemDTO);
|
return this.dictService.updateDictItem(id, updateDictItemDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除字典项
|
/**
|
||||||
@Del('/dict-items/:id')
|
* 删除字典项
|
||||||
|
* @param id 字典项ID
|
||||||
|
*/
|
||||||
|
@Del('/item/:id')
|
||||||
async deleteDictItem(@Param('id') id: number) {
|
async deleteDictItem(@Param('id') id: number) {
|
||||||
|
// 调用服务层方法
|
||||||
return this.dictService.deleteDictItem(id);
|
return this.dictService.deleteDictItem(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,8 @@ export class ProductController {
|
||||||
@Post('/brand')
|
@Post('/brand')
|
||||||
async createBrand(@Body() brandData: CreateBrandDTO) {
|
async createBrand(@Body() brandData: CreateBrandDTO) {
|
||||||
try {
|
try {
|
||||||
const hasBrand = await this.productService.hasBrand(
|
const hasBrand = await this.productService.hasAttribute(
|
||||||
|
'brand',
|
||||||
brandData.name
|
brandData.name
|
||||||
);
|
);
|
||||||
if (hasBrand) {
|
if (hasBrand) {
|
||||||
|
|
@ -207,8 +208,10 @@ export class ProductController {
|
||||||
@Body() brandData: UpdateBrandDTO
|
@Body() brandData: UpdateBrandDTO
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const hasBrand = await this.productService.hasBrand(
|
const hasBrand = await this.productService.hasAttribute(
|
||||||
brandData.name
|
'brand',
|
||||||
|
brandData.name,
|
||||||
|
id
|
||||||
);
|
);
|
||||||
if (hasBrand) {
|
if (hasBrand) {
|
||||||
return errorResponse('品牌已存在');
|
return errorResponse('品牌已存在');
|
||||||
|
|
@ -226,7 +229,7 @@ export class ProductController {
|
||||||
@Del('/brand/:id')
|
@Del('/brand/:id')
|
||||||
async deleteBrand(@Param('id') id: number) {
|
async deleteBrand(@Param('id') id: number) {
|
||||||
try {
|
try {
|
||||||
const hasProducts = await this.productService.hasProductsInBrand(id);
|
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||||
if (hasProducts) throw new Error('该品牌下有商品,无法删除');
|
if (hasProducts) throw new Error('该品牌下有商品,无法删除');
|
||||||
const data = await this.productService.deleteBrand(id);
|
const data = await this.productService.deleteBrand(id);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
|
|
@ -279,7 +282,7 @@ export class ProductController {
|
||||||
@Post('/flavors')
|
@Post('/flavors')
|
||||||
async createFlavors(@Body() flavorsData: CreateFlavorsDTO) {
|
async createFlavors(@Body() flavorsData: CreateFlavorsDTO) {
|
||||||
try {
|
try {
|
||||||
const hasFlavors = await this.productService.hasFlavors(flavorsData.name);
|
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name);
|
||||||
if (hasFlavors) {
|
if (hasFlavors) {
|
||||||
return errorResponse('口味已存在');
|
return errorResponse('口味已存在');
|
||||||
}
|
}
|
||||||
|
|
@ -297,7 +300,7 @@ export class ProductController {
|
||||||
@Body() flavorsData: UpdateFlavorsDTO
|
@Body() flavorsData: UpdateFlavorsDTO
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const hasFlavors = await this.productService.hasFlavors(flavorsData.name);
|
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name, id);
|
||||||
if (hasFlavors) {
|
if (hasFlavors) {
|
||||||
return errorResponse('口味已存在');
|
return errorResponse('口味已存在');
|
||||||
}
|
}
|
||||||
|
|
@ -314,7 +317,7 @@ export class ProductController {
|
||||||
@Del('/flavors/:id')
|
@Del('/flavors/:id')
|
||||||
async deleteFlavors(@Param('id') id: number) {
|
async deleteFlavors(@Param('id') id: number) {
|
||||||
try {
|
try {
|
||||||
const hasProducts = await this.productService.hasProductsInFlavors(id);
|
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||||
if (hasProducts) throw new Error('该口味下有商品,无法删除');
|
if (hasProducts) throw new Error('该口味下有商品,无法删除');
|
||||||
const data = await this.productService.deleteFlavors(id);
|
const data = await this.productService.deleteFlavors(id);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
|
|
@ -353,7 +356,8 @@ export class ProductController {
|
||||||
@Post('/strength')
|
@Post('/strength')
|
||||||
async createStrength(@Body() strengthData: CreateStrengthDTO) {
|
async createStrength(@Body() strengthData: CreateStrengthDTO) {
|
||||||
try {
|
try {
|
||||||
const hasStrength = await this.productService.hasStrength(
|
const hasStrength = await this.productService.hasAttribute(
|
||||||
|
'strength',
|
||||||
strengthData.name
|
strengthData.name
|
||||||
);
|
);
|
||||||
if (hasStrength) {
|
if (hasStrength) {
|
||||||
|
|
@ -373,8 +377,10 @@ export class ProductController {
|
||||||
@Body() strengthData: UpdateStrengthDTO
|
@Body() strengthData: UpdateStrengthDTO
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const hasStrength = await this.productService.hasStrength(
|
const hasStrength = await this.productService.hasAttribute(
|
||||||
strengthData.name
|
'strength',
|
||||||
|
strengthData.name,
|
||||||
|
id
|
||||||
);
|
);
|
||||||
if (hasStrength) {
|
if (hasStrength) {
|
||||||
return errorResponse('规格已存在');
|
return errorResponse('规格已存在');
|
||||||
|
|
@ -392,7 +398,7 @@ export class ProductController {
|
||||||
@Del('/strength/:id')
|
@Del('/strength/:id')
|
||||||
async deleteStrength(@Param('id') id: number) {
|
async deleteStrength(@Param('id') id: number) {
|
||||||
try {
|
try {
|
||||||
const hasProducts = await this.productService.hasProductsInStrength(id);
|
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||||
if (hasProducts) throw new Error('该规格下有商品,无法删除');
|
if (hasProducts) throw new Error('该规格下有商品,无法删除');
|
||||||
const data = await this.productService.deleteStrength(id);
|
const data = await this.productService.deleteStrength(id);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
|
|
|
||||||
|
|
@ -84,4 +84,5 @@ const options: DataSourceOptions & SeederOptions = {
|
||||||
seeds: ['src/db/seeds/**/*.ts'],
|
seeds: ['src/db/seeds/**/*.ts'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default new DataSource(options);
|
export const AppDataSource = new DataSource(options);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class ProductDictItemManyToMany1764238434984 implements MigrationInterface {
|
||||||
|
name = 'ProductDictItemManyToMany1764238434984'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE \`product_attributes_dict_item\` (\`productId\` int NOT NULL, \`dictItemId\` int NOT NULL, INDEX \`IDX_592cdbdaebfec346c202ffb82c\` (\`productId\`), INDEX \`IDX_406c1da5b6de45fecb7967c3ec\` (\`dictItemId\`), PRIMARY KEY (\`productId\`, \`dictItemId\`)) ENGINE=InnoDB`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`brandId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`flavorsId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`strengthId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`humidity\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` ADD \`sku\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` ADD UNIQUE INDEX \`IDX_34f6ca1cd897cc926bdcca1ca3\` (\`sku\`)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` ADD CONSTRAINT \`FK_592cdbdaebfec346c202ffb82ca\` FOREIGN KEY (\`productId\`) REFERENCES \`product\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` ADD CONSTRAINT \`FK_406c1da5b6de45fecb7967c3ec0\` FOREIGN KEY (\`dictItemId\`) REFERENCES \`dict_item\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` DROP FOREIGN KEY \`FK_406c1da5b6de45fecb7967c3ec0\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` DROP FOREIGN KEY \`FK_592cdbdaebfec346c202ffb82ca\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` DROP INDEX \`IDX_34f6ca1cd897cc926bdcca1ca3\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`sku\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` ADD \`humidity\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` ADD \`strengthId\` int NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` ADD \`flavorsId\` int NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`product\` ADD \`brandId\` int NOT NULL`);
|
||||||
|
await queryRunner.query(`DROP INDEX \`IDX_406c1da5b6de45fecb7967c3ec\` ON \`product_attributes_dict_item\``);
|
||||||
|
await queryRunner.query(`DROP INDEX \`IDX_592cdbdaebfec346c202ffb82c\` ON \`product_attributes_dict_item\``);
|
||||||
|
await queryRunner.query(`DROP TABLE \`product_attributes_dict_item\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -69,12 +69,52 @@ export default class DictSeeder implements Seeder {
|
||||||
{ id: 10, title: '30MG', name: '30MG' },
|
{ id: 10, title: '30MG', name: '30MG' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 在插入新数据前,清空旧数据
|
// 在插入新数据前,不清空旧数据,改为如果不存在则创建
|
||||||
await dictItemRepository.query('DELETE FROM `dict_item`');
|
// await dictItemRepository.query('DELETE FROM `dict_item`');
|
||||||
await dictRepository.query('DELETE FROM `dict`');
|
// await dictRepository.query('DELETE FROM `dict`');
|
||||||
// 重置自增 ID
|
// // 重置自增 ID
|
||||||
await dictItemRepository.query('ALTER TABLE `dict_item` AUTO_INCREMENT = 1');
|
// await dictItemRepository.query('ALTER TABLE `dict_item` AUTO_INCREMENT = 1');
|
||||||
await dictRepository.query('ALTER TABLE `dict` AUTO_INCREMENT = 1');
|
// await dictRepository.query('ALTER TABLE `dict` AUTO_INCREMENT = 1');
|
||||||
|
|
||||||
|
// 初始化语言字典
|
||||||
|
const locales = [
|
||||||
|
{ name: 'zh-CN', title: '简体中文' },
|
||||||
|
{ name: 'en-US', title: 'English' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const locale of locales) {
|
||||||
|
let dict = await dictRepository.findOne({ where: { name: locale.name } });
|
||||||
|
if (!dict) {
|
||||||
|
dict = await dictRepository.save(locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加示例翻译条目
|
||||||
|
const zhDict = await dictRepository.findOne({ where: { name: 'zh-CN' } });
|
||||||
|
const enDict = await dictRepository.findOne({ where: { name: 'en-US' } });
|
||||||
|
|
||||||
|
const translations = [
|
||||||
|
{ name: 'common.save', zh: '保存', en: 'Save' },
|
||||||
|
{ name: 'common.cancel', zh: '取消', en: 'Cancel' },
|
||||||
|
{ name: 'common.success', zh: '操作成功', en: 'Success' },
|
||||||
|
{ name: 'common.failure', zh: '操作失败', en: 'Failure' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const t of translations) {
|
||||||
|
// 添加中文翻译
|
||||||
|
let item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: zhDict.id } } });
|
||||||
|
if (!item) {
|
||||||
|
await dictItemRepository.save({ name: t.name, title: t.zh, dict: zhDict });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加英文翻译
|
||||||
|
item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: enDict.id } } });
|
||||||
|
if (!item) {
|
||||||
|
await dictItemRepository.save({ name: t.name, title: t.en, dict: enDict });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const brandDict = await dictRepository.save({ title: '品牌', name: 'brand' });
|
const brandDict = await dictRepository.save({ title: '品牌', name: 'brand' });
|
||||||
const flavorDict = await dictRepository.save({ title: '口味', name: 'flavor' });
|
const flavorDict = await dictRepository.save({ title: '口味', name: 'flavor' });
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ export class Dict {
|
||||||
@OneToMany(() => DictItem, item => item.dict)
|
@OneToMany(() => DictItem, item => item.dict)
|
||||||
items: DictItem[];
|
items: DictItem[];
|
||||||
|
|
||||||
|
// 是否可删除
|
||||||
|
@Column({ default: true, comment: '是否可删除' })
|
||||||
|
deletable: boolean;
|
||||||
// 创建时间
|
// 创建时间
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@
|
||||||
* @date 2025-11-27
|
* @date 2025-11-27
|
||||||
*/
|
*/
|
||||||
import { Dict } from './dict.entity';
|
import { Dict } from './dict.entity';
|
||||||
|
import { Product } from './product.entity';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
|
|
@ -28,11 +30,23 @@ export class DictItem {
|
||||||
@Column({ unique: true, comment: '字典唯一标识名称' })
|
@Column({ unique: true, comment: '字典唯一标识名称' })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
// 字典项值
|
||||||
|
@Column({ nullable: true, comment: '字典项值' })
|
||||||
|
value?: string;
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
@Column({ default: 0, comment: '排序' })
|
||||||
|
sort: number;
|
||||||
|
|
||||||
// 属于哪个字典
|
// 属于哪个字典
|
||||||
@ManyToOne(() => Dict, dict => dict.items)
|
@ManyToOne(() => Dict, dict => dict.items)
|
||||||
@JoinColumn({ name: 'dict_id' })
|
@JoinColumn({ name: 'dict_id' })
|
||||||
dict: Dict;
|
dict: Dict;
|
||||||
|
|
||||||
|
// 关联的产品
|
||||||
|
@ManyToMany(() => Product, product => product.attributes)
|
||||||
|
products: Product[];
|
||||||
|
|
||||||
// 创建时间
|
// 创建时间
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ import {
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
ManyToMany,
|
||||||
|
JoinTable,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
|
import { DictItem } from './dict_item.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Product {
|
export class Product {
|
||||||
|
|
@ -31,30 +34,19 @@ export class Product {
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
nameCn: string;
|
nameCn: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '产品描述', description: '产品描述', type: 'string' })
|
@ApiProperty({ example: '产品描述', description: '产品描述' })
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '1', description: '品牌 ID', type: 'number' })
|
@ApiProperty({ description: 'sku'})
|
||||||
@Column()
|
@Column({ unique: true })
|
||||||
brandId: number;
|
sku: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '口味ID' })
|
@ManyToMany(() => DictItem, {
|
||||||
@Column()
|
cascade: true,
|
||||||
flavorsId: number;
|
})
|
||||||
|
@JoinTable()
|
||||||
@ApiProperty({ description: '尼古丁强度ID' })
|
attributes: DictItem[];
|
||||||
@Column()
|
|
||||||
strengthId: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '湿度' })
|
|
||||||
@Column()
|
|
||||||
humidity: string;
|
|
||||||
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'sku', type: 'string' })
|
|
||||||
@Column({ nullable: true })
|
|
||||||
sku?: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: '2022-12-12 11:11:11',
|
example: '2022-12-12 11:11:11',
|
||||||
|
|
|
||||||
|
|
@ -5,80 +5,157 @@ import { Dict } from '../entity/dict.entity';
|
||||||
import { DictItem } from '../entity/dict_item.entity';
|
import { DictItem } from '../entity/dict_item.entity';
|
||||||
import { CreateDictDTO, UpdateDictDTO } from '../dto/dict.dto';
|
import { CreateDictDTO, UpdateDictDTO } from '../dto/dict.dto';
|
||||||
import { CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
import { CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
||||||
|
import * as xlsx from 'xlsx';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class DictService {
|
export class DictService {
|
||||||
@InjectEntityModel(Dict)
|
|
||||||
dictModel: Repository<Dict>;
|
|
||||||
|
|
||||||
@InjectEntityModel(DictItem)
|
@InjectEntityModel(Dict)
|
||||||
dictItemModel: Repository<DictItem>;
|
dictModel: Repository<Dict>;
|
||||||
|
|
||||||
// 获取字典列表,支持按标题搜索
|
@InjectEntityModel(DictItem)
|
||||||
async getDicts(title?: string) {
|
dictItemModel: Repository<DictItem>;
|
||||||
// 如果提供了标题,则使用模糊查询
|
|
||||||
if (title) {
|
// 生成并返回字典的XLSX模板
|
||||||
return this.dictModel.find({ where: { title: Like(`%${title}%`) } });
|
getDictXLSXTemplate() {
|
||||||
|
// 定义表头
|
||||||
|
const headers = ['name', 'title'];
|
||||||
|
// 创建一个新的工作表
|
||||||
|
const ws = xlsx.utils.aoa_to_sheet([headers]);
|
||||||
|
// 创建一个新的工作簿
|
||||||
|
const wb = xlsx.utils.book_new();
|
||||||
|
// 将工作表添加到工作簿
|
||||||
|
xlsx.utils.book_append_sheet(wb, ws, 'Dicts');
|
||||||
|
// 将工作簿写入缓冲区
|
||||||
|
return xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||||||
}
|
}
|
||||||
// 否则,返回所有字典
|
|
||||||
return this.dictModel.find();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新字典
|
// 从XLSX文件导入字典
|
||||||
async createDict(createDictDTO: CreateDictDTO) {
|
async importDictsFromXLSX(buffer: Buffer) {
|
||||||
const dict = new Dict();
|
// 读取缓冲区中的工作簿
|
||||||
dict.name = createDictDTO.name;
|
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||||||
dict.title = createDictDTO.title;
|
// 获取第一个工作表的名称
|
||||||
return this.dictModel.save(dict);
|
const wsname = wb.SheetNames[0];
|
||||||
}
|
// 获取第一个工作表
|
||||||
|
const ws = wb.Sheets[wsname];
|
||||||
// 更新字典
|
// 将工作表转换为JSON对象数组
|
||||||
async updateDict(id: number, updateDictDTO: UpdateDictDTO) {
|
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title'] }).slice(1);
|
||||||
await this.dictModel.update(id, updateDictDTO);
|
// 创建要保存的字典实体数组
|
||||||
return this.dictModel.findOneBy({ id });
|
const dicts = data.map((row: any) => {
|
||||||
}
|
const dict = new Dict();
|
||||||
|
dict.name = row.name;
|
||||||
// 删除字典及其所有字典项
|
dict.title = row.title;
|
||||||
async deleteDict(id: number) {
|
return dict;
|
||||||
// 首先删除该字典下的所有字典项
|
});
|
||||||
await this.dictItemModel.delete({ dict: { id } });
|
// 保存字典实体数组到数据库
|
||||||
// 然后删除字典本身
|
await this.dictModel.save(dicts);
|
||||||
const result = await this.dictModel.delete(id);
|
// 返回成功导入的记录数
|
||||||
return result.affected > 0;
|
return { success: true, count: dicts.length };
|
||||||
}
|
|
||||||
|
|
||||||
// 获取字典项列表,支持按 dictId 过滤
|
|
||||||
async getDictItems(dictId?: number) {
|
|
||||||
// 如果提供了 dictId,则只返回该字典下的项
|
|
||||||
if (dictId) {
|
|
||||||
return this.dictItemModel.find({ where: { dict: { id: dictId } } });
|
|
||||||
}
|
}
|
||||||
// 否则,返回所有字典项
|
|
||||||
return this.dictItemModel.find();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新字典项
|
// 生成并返回字典项的XLSX模板
|
||||||
async createDictItem(createDictItemDTO: CreateDictItemDTO) {
|
getDictItemXLSXTemplate() {
|
||||||
const dict = await this.dictModel.findOneBy({ id: createDictItemDTO.dictId });
|
const headers = ['name', 'title', 'value', 'sort'];
|
||||||
if (!dict) {
|
const ws = xlsx.utils.aoa_to_sheet([headers]);
|
||||||
throw new Error('指定的字典不存在');
|
const wb = xlsx.utils.book_new();
|
||||||
|
xlsx.utils.book_append_sheet(wb, ws, 'DictItems');
|
||||||
|
return xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||||||
}
|
}
|
||||||
const item = new DictItem();
|
|
||||||
item.name = createDictItemDTO.name;
|
|
||||||
item.title = createDictItemDTO.title;
|
|
||||||
item.dict = dict;
|
|
||||||
return this.dictItemModel.save(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新字典项
|
// 从XLSX文件导入字典项
|
||||||
async updateDictItem(id: number, updateDictItemDTO: UpdateDictItemDTO) {
|
async importDictItemsFromXLSX(buffer: Buffer, dictId: number) {
|
||||||
await this.dictItemModel.update(id, updateDictItemDTO);
|
const dict = await this.dictModel.findOneBy({ id: dictId });
|
||||||
return this.dictItemModel.findOneBy({ id });
|
if (!dict) {
|
||||||
}
|
throw new Error('指定的字典不存在');
|
||||||
|
}
|
||||||
|
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||||||
|
const wsname = wb.SheetNames[0];
|
||||||
|
const ws = wb.Sheets[wsname];
|
||||||
|
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title', 'value', 'sort'] }).slice(1);
|
||||||
|
|
||||||
// 删除字典项
|
const items = data.map((row: any) => {
|
||||||
async deleteDictItem(id: number) {
|
const item = new DictItem();
|
||||||
const result = await this.dictItemModel.delete(id);
|
item.name = row.name;
|
||||||
return result.affected > 0;
|
item.title = row.title;
|
||||||
}
|
item.value = row.value;
|
||||||
|
item.sort = row.sort || 0;
|
||||||
|
item.dict = dict;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.dictItemModel.save(items);
|
||||||
|
return { success: true, count: items.length };
|
||||||
|
}
|
||||||
|
getDict(where: { name?: string; id?: number; }, relations: string[]) {
|
||||||
|
if (!where.name && !where.id) {
|
||||||
|
throw new Error('必须提供 name 或 id');
|
||||||
|
}
|
||||||
|
return this.dictModel.findOne({ where, relations });
|
||||||
|
}
|
||||||
|
// 获取字典列表,支持按标题搜索
|
||||||
|
async getDicts(options: { title?: string; name?: string; }) {
|
||||||
|
const where = {
|
||||||
|
title: options.title ? Like(`%${options.title}%`) : undefined,
|
||||||
|
name: options.name ? Like(`%${options.name}%`) : undefined,
|
||||||
|
}
|
||||||
|
return this.dictModel.find({ where });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新字典
|
||||||
|
async createDict(createDictDTO: CreateDictDTO) {
|
||||||
|
const dict = new Dict();
|
||||||
|
dict.name = createDictDTO.name;
|
||||||
|
dict.title = createDictDTO.title;
|
||||||
|
return this.dictModel.save(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新字典
|
||||||
|
async updateDict(id: number, updateDictDTO: UpdateDictDTO) {
|
||||||
|
await this.dictModel.update(id, updateDictDTO);
|
||||||
|
return this.dictModel.findOneBy({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除字典及其所有字典项
|
||||||
|
async deleteDict(id: number) {
|
||||||
|
// 首先删除该字典下的所有字典项
|
||||||
|
await this.dictItemModel.delete({ dict: { id } });
|
||||||
|
// 然后删除字典本身
|
||||||
|
const result = await this.dictModel.delete(id);
|
||||||
|
return result.affected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字典项列表,支持按 dictId 过滤
|
||||||
|
async getDictItems(dictId?: number) {
|
||||||
|
// 如果提供了 dictId,则只返回该字典下的项
|
||||||
|
if (dictId) {
|
||||||
|
return this.dictItemModel.find({ where: { dict: { id: dictId } } });
|
||||||
|
}
|
||||||
|
// 否则,返回所有字典项
|
||||||
|
return this.dictItemModel.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新字典项
|
||||||
|
async createDictItem(createDictItemDTO: CreateDictItemDTO) {
|
||||||
|
const dict = await this.dictModel.findOneBy({ id: createDictItemDTO.dictId });
|
||||||
|
if (!dict) {
|
||||||
|
throw new Error('指定的字典不存在');
|
||||||
|
}
|
||||||
|
const item = new DictItem();
|
||||||
|
item.name = createDictItemDTO.name;
|
||||||
|
item.title = createDictItemDTO.title;
|
||||||
|
item.dict = dict;
|
||||||
|
return this.dictItemModel.save(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新字典项
|
||||||
|
async updateDictItem(id: number, updateDictItemDTO: UpdateDictItemDTO) {
|
||||||
|
await this.dictItemModel.update(id, updateDictItemDTO);
|
||||||
|
return this.dictItemModel.findOneBy({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除字典项
|
||||||
|
async deleteDictItem(id: number) {
|
||||||
|
const result = await this.dictItemModel.delete(id);
|
||||||
|
return result.affected > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,64 +113,37 @@ export class ProductService {
|
||||||
name?: string,
|
name?: string,
|
||||||
brandId?: number
|
brandId?: number
|
||||||
): Promise<ProductPaginatedResponse> {
|
): Promise<ProductPaginatedResponse> {
|
||||||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
|
||||||
|
|
||||||
// 查询品牌、口味、规格字典
|
|
||||||
const brandDict = await this.dictModel.findOne({
|
|
||||||
where: { name: 'brand' },
|
|
||||||
});
|
|
||||||
const flavorDict = await this.dictModel.findOne({
|
|
||||||
where: { name: 'flavor' },
|
|
||||||
});
|
|
||||||
const strengthDict = await this.dictModel.findOne({
|
|
||||||
where: { name: 'strength' },
|
|
||||||
});
|
|
||||||
|
|
||||||
// 构建查询
|
|
||||||
const qb = this.productModel
|
const qb = this.productModel
|
||||||
.createQueryBuilder('product')
|
.createQueryBuilder('product')
|
||||||
.leftJoin(
|
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||||
DictItem,
|
.leftJoinAndSelect('attribute.dict', 'dict');
|
||||||
'brand',
|
|
||||||
'brand.id = product.brandId AND brand.dict = :brandDictId',
|
|
||||||
{ brandDictId: brandDict?.id }
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
DictItem,
|
|
||||||
'flavor',
|
|
||||||
'flavor.id = product.flavorsId AND flavor.dict = :flavorDictId',
|
|
||||||
{ flavorDictId: flavorDict?.id }
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
DictItem,
|
|
||||||
'strength',
|
|
||||||
'strength.id = product.strengthId AND strength.dict = :strengthDictId',
|
|
||||||
{ strengthDictId: strengthDict?.id }
|
|
||||||
)
|
|
||||||
.select([
|
|
||||||
'product.id as id',
|
|
||||||
'product.name as name',
|
|
||||||
'product.nameCn as nameCn',
|
|
||||||
'product.description as description',
|
|
||||||
'product.humidity as humidity',
|
|
||||||
'product.sku as sku',
|
|
||||||
'product.createdAt as createdAt',
|
|
||||||
'product.updatedAt as updatedAt',
|
|
||||||
'brand.title AS brandName',
|
|
||||||
'flavor.title AS flavorsName',
|
|
||||||
'strength.title AS strengthName',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 模糊搜索 name,支持多个关键词
|
// 模糊搜索 name,支持多个关键词
|
||||||
nameFilter.forEach((word, index) => {
|
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||||
qb.andWhere(`product.name LIKE :name${index}`, {
|
if (nameFilter.length > 0) {
|
||||||
[`name${index}`]: `%${word}%`,
|
const nameConditions = nameFilter
|
||||||
});
|
.map((word, index) => `product.name LIKE :name${index}`)
|
||||||
});
|
.join(' AND ');
|
||||||
|
const nameParams = nameFilter.reduce(
|
||||||
|
(params, word, index) => ({ ...params, [`name${index}`]: `%${word}%` }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
qb.where(`(${nameConditions})`, nameParams);
|
||||||
|
}
|
||||||
|
|
||||||
// 品牌过滤
|
// 品牌过滤
|
||||||
if (brandId) {
|
if (brandId) {
|
||||||
qb.andWhere('product.brandId = :brandId', { brandId });
|
qb.andWhere(qb => {
|
||||||
|
const subQuery = qb
|
||||||
|
.subQuery()
|
||||||
|
.select('product_attributes_dict_item.productId')
|
||||||
|
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||||
|
.where('product_attributes_dict_item.dictItemId = :brandId', {
|
||||||
|
brandId,
|
||||||
|
})
|
||||||
|
.getQuery();
|
||||||
|
return 'product.id IN ' + subQuery;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
|
|
@ -178,12 +151,30 @@ export class ProductService {
|
||||||
pagination.pageSize
|
pagination.pageSize
|
||||||
);
|
);
|
||||||
|
|
||||||
// 执行查询
|
const [items, total] = await qb.getManyAndCount();
|
||||||
const items = await qb.getRawMany();
|
|
||||||
const total = await qb.getCount();
|
// 格式化返回的数据
|
||||||
|
const formattedItems = items.map(product => {
|
||||||
|
const getAttributeTitle = (dictName: string) =>
|
||||||
|
product.attributes.find(a => a.dict.name === dictName)?.title || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: product.id,
|
||||||
|
name: product.name,
|
||||||
|
nameCn: product.nameCn,
|
||||||
|
description: product.description,
|
||||||
|
humidity: getAttributeTitle('humidity'),
|
||||||
|
sku: product.sku,
|
||||||
|
createdAt: product.createdAt,
|
||||||
|
updatedAt: product.updatedAt,
|
||||||
|
brandName: getAttributeTitle('brand'),
|
||||||
|
flavorsName: getAttributeTitle('flavor'),
|
||||||
|
strengthName: getAttributeTitle('strength'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items: formattedItems,
|
||||||
total,
|
total,
|
||||||
...pagination,
|
...pagination,
|
||||||
};
|
};
|
||||||
|
|
@ -237,29 +228,31 @@ export class ProductService {
|
||||||
strength.title,
|
strength.title,
|
||||||
strength.name
|
strength.name
|
||||||
);
|
);
|
||||||
|
const humidityItem = await this.getOrCreateDictItem('humidity', humidity);
|
||||||
|
|
||||||
// 检查产品是否已存在
|
// 检查具有完全相同属性组合的产品是否已存在
|
||||||
const isExit = await this.productModel.findOne({
|
const attributesToMatch = [brandItem, flavorItem, strengthItem, humidityItem];
|
||||||
where: {
|
const qb = this.productModel.createQueryBuilder('product');
|
||||||
brandId: brandItem.id,
|
attributesToMatch.forEach((attr, index) => {
|
||||||
flavorsId: flavorItem.id,
|
qb.innerJoin(
|
||||||
strengthId: strengthItem.id,
|
'product.attributes',
|
||||||
humidity,
|
`attr${index}`,
|
||||||
},
|
`attr${index}.id = :attrId${index}`,
|
||||||
|
{ [`attrId${index}`]: attr.id }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
const isExit = await qb.getOne();
|
||||||
|
|
||||||
if (isExit) throw new Error('产品已存在');
|
if (isExit) throw new Error('产品已存在');
|
||||||
|
|
||||||
// 创建新产品实例
|
// 创建新产品实例
|
||||||
const product = new Product();
|
const product = new Product();
|
||||||
product.name = name;
|
product.name = name;
|
||||||
product.description = description;
|
product.description = description;
|
||||||
product.brandId = brandItem.id;
|
product.attributes = attributesToMatch;
|
||||||
product.flavorsId = flavorItem.id;
|
|
||||||
product.strengthId = strengthItem.id;
|
|
||||||
product.humidity = humidity;
|
|
||||||
|
|
||||||
// 生成 SKU
|
// 生成 SKU
|
||||||
product.sku = `${brandItem.name}-${flavorItem.name}-${strengthItem.name}-${humidity}`;
|
product.sku = `${brandItem.name}-${flavorItem.name}-${strengthItem.name}-${humidityItem.name}`;
|
||||||
|
|
||||||
// 保存产品
|
// 保存产品
|
||||||
return await this.productModel.save(product);
|
return await this.productModel.save(product);
|
||||||
|
|
@ -328,34 +321,32 @@ export class ProductService {
|
||||||
return result.affected > 0; // `affected` 表示删除的行数
|
return result.affected > 0; // `affected` 表示删除的行数
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasProductsInBrand(brandId: number): Promise<boolean> {
|
|
||||||
// 检查是否有产品属于该品牌
|
async hasAttribute(
|
||||||
const count = await this.productModel.count({
|
dictName: string,
|
||||||
where: { brandId },
|
title: string,
|
||||||
|
id?: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
const dict = await this.dictModel.findOne({
|
||||||
|
where: { name: dictName },
|
||||||
|
});
|
||||||
|
if (!dict) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const where: any = { title, dict: { id: dict.id } };
|
||||||
|
if (id) where.id = Not(id);
|
||||||
|
const count = await this.dictItemModel.count({
|
||||||
|
where,
|
||||||
});
|
});
|
||||||
return count > 0;
|
return count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasBrand(title: string, id?: number): Promise<boolean> {
|
async hasProductsInAttribute(attributeId: number): Promise<boolean> {
|
||||||
// 查找 'brand' 字典
|
const count = await this.productModel
|
||||||
const brandDict = await this.dictModel.findOne({
|
.createQueryBuilder('product')
|
||||||
where: { name: 'brand' },
|
.innerJoin('product.attributes', 'attribute')
|
||||||
});
|
.where('attribute.id = :attributeId', { attributeId })
|
||||||
|
.getCount();
|
||||||
// 如果字典不存在,则品牌不存在
|
|
||||||
if (!brandDict) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置查询条件
|
|
||||||
const where: any = { title, dict: { id: brandDict.id } };
|
|
||||||
if (id) where.id = Not(id);
|
|
||||||
|
|
||||||
// 统计数量
|
|
||||||
const count = await this.dictItemModel.count({
|
|
||||||
where,
|
|
||||||
});
|
|
||||||
|
|
||||||
return count > 0;
|
return count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -451,27 +442,9 @@ export class ProductService {
|
||||||
return result.affected > 0; // `affected` 表示删除的行数
|
return result.affected > 0; // `affected` 表示删除的行数
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasProductsInFlavors(flavorsId: number): Promise<boolean> {
|
|
||||||
const count = await this.productModel.count({
|
|
||||||
where: { flavorsId },
|
|
||||||
});
|
|
||||||
return count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasFlavors(title: string, id?: string): Promise<boolean> {
|
|
||||||
const flavorsDict = await this.dictModel.findOne({
|
|
||||||
where: { name: 'flavor' },
|
|
||||||
});
|
|
||||||
if (!flavorsDict) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const where: any = { title, dict_id: flavorsDict.id };
|
|
||||||
if (id) where.id = Not(id);
|
|
||||||
const count = await this.dictItemModel.count({
|
|
||||||
where,
|
|
||||||
});
|
|
||||||
return count > 0;
|
|
||||||
}
|
|
||||||
async getFlavorsList(
|
async getFlavorsList(
|
||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
title?: string
|
title?: string
|
||||||
|
|
@ -535,12 +508,7 @@ export class ProductService {
|
||||||
const result = await this.dictItemModel.delete(id);
|
const result = await this.dictItemModel.delete(id);
|
||||||
return result.affected > 0;
|
return result.affected > 0;
|
||||||
}
|
}
|
||||||
async hasProductsInStrength(strengthId: number): Promise<boolean> {
|
|
||||||
const count = await this.productModel.count({
|
|
||||||
where: { strengthId },
|
|
||||||
});
|
|
||||||
return count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasStrength(title: string, id?: string): Promise<boolean> {
|
async hasStrength(title: string, id?: string): Promise<boolean> {
|
||||||
const strengthDict = await this.dictModel.findOne({
|
const strengthDict = await this.dictModel.findOne({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue