feat(字典): 重构字典模块并实现产品属性关联
重构字典模块,支持字典项与产品的多对多关联 添加字典项导入导出功能,支持XLSX模板下载 优化产品管理,使用字典项作为产品属性 新增字典项排序和值字段 修改数据源配置,添加字典种子数据
This commit is contained in:
parent
889f00bde8
commit
0809840507
|
|
@ -25,6 +25,7 @@
|
|||
"axios": "^1.13.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"csv-parse": "^6.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"mysql2": "^3.11.5",
|
||||
"nodemailer": "^7.0.5",
|
||||
|
|
@ -1917,6 +1918,12 @@
|
|||
"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": {
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"axios": "^1.13.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"csv-parse": "^6.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"mysql2": "^3.11.5",
|
||||
"nodemailer": "^7.0.5",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { Subscription } from '../entity/subscription.entity';
|
|||
import { Site } from '../entity/site.entity';
|
||||
import { Dict } from '../entity/dict.entity';
|
||||
import { DictItem } from '../entity/dict_item.entity';
|
||||
import DictSeeder from '../db/seeds/dict.seeder';
|
||||
|
||||
export default {
|
||||
// use for cookie sign key, should change to your own and keep security
|
||||
|
|
@ -77,6 +78,7 @@ export default {
|
|||
],
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
seeders: [DictSeeder],
|
||||
},
|
||||
dataSource: {
|
||||
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 { CreateDictDTO, UpdateDictDTO, CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
||||
import { Validate } from '@midwayjs/validate';
|
||||
import { Context } from '@midwayjs/koa';
|
||||
|
||||
@Controller('/api')
|
||||
/**
|
||||
* 字典管理
|
||||
* @decorator Controller
|
||||
*/
|
||||
@Controller('/dict')
|
||||
export class DictController {
|
||||
@Inject()
|
||||
dictService: DictService;
|
||||
|
||||
// 获取字典列表
|
||||
@Get('/dicts')
|
||||
async getDicts(@Query('title') title?: string) {
|
||||
return this.dictService.getDicts(title);
|
||||
@Inject()
|
||||
ctx: Context;
|
||||
|
||||
/**
|
||||
* 批量导入字典
|
||||
* @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()
|
||||
async createDict(@Body() createDictDTO: CreateDictDTO) {
|
||||
// 调用服务层方法
|
||||
return this.dictService.createDict(createDictDTO);
|
||||
}
|
||||
|
||||
// 更新字典
|
||||
@Put('/dicts/:id')
|
||||
/**
|
||||
* 更新字典
|
||||
* @param id 字典ID
|
||||
* @param updateDictDTO 待更新的字典数据
|
||||
*/
|
||||
@Put('/:id')
|
||||
@Validate()
|
||||
async updateDict(@Param('id') id: number, @Body() updateDictDTO: UpdateDictDTO) {
|
||||
// 调用服务层方法
|
||||
return this.dictService.updateDict(id, updateDictDTO);
|
||||
}
|
||||
|
||||
// 删除字典
|
||||
@Del('/dicts/:id')
|
||||
/**
|
||||
* 删除字典
|
||||
* @param id 字典ID
|
||||
*/
|
||||
@Del('/:id')
|
||||
async deleteDict(@Param('id') id: number) {
|
||||
// 调用服务层方法
|
||||
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) {
|
||||
// 调用服务层方法
|
||||
return this.dictService.getDictItems(dictId);
|
||||
}
|
||||
|
||||
// 创建新字典项
|
||||
@Post('/dict-items')
|
||||
/**
|
||||
* 创建新字典项
|
||||
* @param createDictItemDTO 字典项数据
|
||||
*/
|
||||
@Post('/item')
|
||||
@Validate()
|
||||
async createDictItem(@Body() createDictItemDTO: CreateDictItemDTO) {
|
||||
// 调用服务层方法
|
||||
return this.dictService.createDictItem(createDictItemDTO);
|
||||
}
|
||||
|
||||
// 更新字典项
|
||||
@Put('/dict-items/:id')
|
||||
/**
|
||||
* 更新字典项
|
||||
* @param id 字典项ID
|
||||
* @param updateDictItemDTO 待更新的字典项数据
|
||||
*/
|
||||
@Put('/item/:id')
|
||||
@Validate()
|
||||
async updateDictItem(@Param('id') id: number, @Body() updateDictItemDTO: UpdateDictItemDTO) {
|
||||
// 调用服务层方法
|
||||
return this.dictService.updateDictItem(id, updateDictItemDTO);
|
||||
}
|
||||
|
||||
// 删除字典项
|
||||
@Del('/dict-items/:id')
|
||||
/**
|
||||
* 删除字典项
|
||||
* @param id 字典项ID
|
||||
*/
|
||||
@Del('/item/:id')
|
||||
async deleteDictItem(@Param('id') id: number) {
|
||||
// 调用服务层方法
|
||||
return this.dictService.deleteDictItem(id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,7 +185,8 @@ export class ProductController {
|
|||
@Post('/brand')
|
||||
async createBrand(@Body() brandData: CreateBrandDTO) {
|
||||
try {
|
||||
const hasBrand = await this.productService.hasBrand(
|
||||
const hasBrand = await this.productService.hasAttribute(
|
||||
'brand',
|
||||
brandData.name
|
||||
);
|
||||
if (hasBrand) {
|
||||
|
|
@ -207,8 +208,10 @@ export class ProductController {
|
|||
@Body() brandData: UpdateBrandDTO
|
||||
) {
|
||||
try {
|
||||
const hasBrand = await this.productService.hasBrand(
|
||||
brandData.name
|
||||
const hasBrand = await this.productService.hasAttribute(
|
||||
'brand',
|
||||
brandData.name,
|
||||
id
|
||||
);
|
||||
if (hasBrand) {
|
||||
return errorResponse('品牌已存在');
|
||||
|
|
@ -226,7 +229,7 @@ export class ProductController {
|
|||
@Del('/brand/:id')
|
||||
async deleteBrand(@Param('id') id: number) {
|
||||
try {
|
||||
const hasProducts = await this.productService.hasProductsInBrand(id);
|
||||
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||
if (hasProducts) throw new Error('该品牌下有商品,无法删除');
|
||||
const data = await this.productService.deleteBrand(id);
|
||||
return successResponse(data);
|
||||
|
|
@ -279,7 +282,7 @@ export class ProductController {
|
|||
@Post('/flavors')
|
||||
async createFlavors(@Body() flavorsData: CreateFlavorsDTO) {
|
||||
try {
|
||||
const hasFlavors = await this.productService.hasFlavors(flavorsData.name);
|
||||
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name);
|
||||
if (hasFlavors) {
|
||||
return errorResponse('口味已存在');
|
||||
}
|
||||
|
|
@ -297,7 +300,7 @@ export class ProductController {
|
|||
@Body() flavorsData: UpdateFlavorsDTO
|
||||
) {
|
||||
try {
|
||||
const hasFlavors = await this.productService.hasFlavors(flavorsData.name);
|
||||
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name, id);
|
||||
if (hasFlavors) {
|
||||
return errorResponse('口味已存在');
|
||||
}
|
||||
|
|
@ -314,7 +317,7 @@ export class ProductController {
|
|||
@Del('/flavors/:id')
|
||||
async deleteFlavors(@Param('id') id: number) {
|
||||
try {
|
||||
const hasProducts = await this.productService.hasProductsInFlavors(id);
|
||||
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||
if (hasProducts) throw new Error('该口味下有商品,无法删除');
|
||||
const data = await this.productService.deleteFlavors(id);
|
||||
return successResponse(data);
|
||||
|
|
@ -353,7 +356,8 @@ export class ProductController {
|
|||
@Post('/strength')
|
||||
async createStrength(@Body() strengthData: CreateStrengthDTO) {
|
||||
try {
|
||||
const hasStrength = await this.productService.hasStrength(
|
||||
const hasStrength = await this.productService.hasAttribute(
|
||||
'strength',
|
||||
strengthData.name
|
||||
);
|
||||
if (hasStrength) {
|
||||
|
|
@ -373,8 +377,10 @@ export class ProductController {
|
|||
@Body() strengthData: UpdateStrengthDTO
|
||||
) {
|
||||
try {
|
||||
const hasStrength = await this.productService.hasStrength(
|
||||
strengthData.name
|
||||
const hasStrength = await this.productService.hasAttribute(
|
||||
'strength',
|
||||
strengthData.name,
|
||||
id
|
||||
);
|
||||
if (hasStrength) {
|
||||
return errorResponse('规格已存在');
|
||||
|
|
@ -392,7 +398,7 @@ export class ProductController {
|
|||
@Del('/strength/:id')
|
||||
async deleteStrength(@Param('id') id: number) {
|
||||
try {
|
||||
const hasProducts = await this.productService.hasProductsInStrength(id);
|
||||
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||
if (hasProducts) throw new Error('该规格下有商品,无法删除');
|
||||
const data = await this.productService.deleteStrength(id);
|
||||
return successResponse(data);
|
||||
|
|
|
|||
|
|
@ -84,4 +84,5 @@ const options: DataSourceOptions & SeederOptions = {
|
|||
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' },
|
||||
];
|
||||
|
||||
// 在插入新数据前,清空旧数据
|
||||
await dictItemRepository.query('DELETE FROM `dict_item`');
|
||||
await dictRepository.query('DELETE FROM `dict`');
|
||||
// 重置自增 ID
|
||||
await dictItemRepository.query('ALTER TABLE `dict_item` AUTO_INCREMENT = 1');
|
||||
await dictRepository.query('ALTER TABLE `dict` AUTO_INCREMENT = 1');
|
||||
// 在插入新数据前,不清空旧数据,改为如果不存在则创建
|
||||
// await dictItemRepository.query('DELETE FROM `dict_item`');
|
||||
// await dictRepository.query('DELETE FROM `dict`');
|
||||
// // 重置自增 ID
|
||||
// await dictItemRepository.query('ALTER TABLE `dict_item` 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 flavorDict = await dictRepository.save({ title: '口味', name: 'flavor' });
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ export class Dict {
|
|||
@OneToMany(() => DictItem, item => item.dict)
|
||||
items: DictItem[];
|
||||
|
||||
// 是否可删除
|
||||
@Column({ default: true, comment: '是否可删除' })
|
||||
deletable: boolean;
|
||||
// 创建时间
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
* @date 2025-11-27
|
||||
*/
|
||||
import { Dict } from './dict.entity';
|
||||
import { Product } from './product.entity';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
|
|
@ -28,11 +30,23 @@ export class DictItem {
|
|||
@Column({ unique: true, comment: '字典唯一标识名称' })
|
||||
name: string;
|
||||
|
||||
// 字典项值
|
||||
@Column({ nullable: true, comment: '字典项值' })
|
||||
value?: string;
|
||||
|
||||
// 排序
|
||||
@Column({ default: 0, comment: '排序' })
|
||||
sort: number;
|
||||
|
||||
// 属于哪个字典
|
||||
@ManyToOne(() => Dict, dict => dict.items)
|
||||
@JoinColumn({ name: 'dict_id' })
|
||||
dict: Dict;
|
||||
|
||||
// 关联的产品
|
||||
@ManyToMany(() => Product, product => product.attributes)
|
||||
products: Product[];
|
||||
|
||||
// 创建时间
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import {
|
|||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Entity,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { DictItem } from './dict_item.entity';
|
||||
|
||||
@Entity()
|
||||
export class Product {
|
||||
|
|
@ -31,30 +34,19 @@ export class Product {
|
|||
@Column({ default: '' })
|
||||
nameCn: string;
|
||||
|
||||
@ApiProperty({ example: '产品描述', description: '产品描述', type: 'string' })
|
||||
@ApiProperty({ example: '产品描述', description: '产品描述' })
|
||||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: '1', description: '品牌 ID', type: 'number' })
|
||||
@Column()
|
||||
brandId: number;
|
||||
@ApiProperty({ description: 'sku'})
|
||||
@Column({ unique: true })
|
||||
sku: string;
|
||||
|
||||
@ApiProperty({ description: '口味ID' })
|
||||
@Column()
|
||||
flavorsId: number;
|
||||
|
||||
@ApiProperty({ description: '尼古丁强度ID' })
|
||||
@Column()
|
||||
strengthId: number;
|
||||
|
||||
@ApiProperty({ description: '湿度' })
|
||||
@Column()
|
||||
humidity: string;
|
||||
|
||||
|
||||
@ApiProperty({ description: 'sku', type: 'string' })
|
||||
@Column({ nullable: true })
|
||||
sku?: string;
|
||||
@ManyToMany(() => DictItem, {
|
||||
cascade: true,
|
||||
})
|
||||
@JoinTable()
|
||||
attributes: DictItem[];
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
|
|
|
|||
|
|
@ -5,23 +5,100 @@ import { Dict } from '../entity/dict.entity';
|
|||
import { DictItem } from '../entity/dict_item.entity';
|
||||
import { CreateDictDTO, UpdateDictDTO } from '../dto/dict.dto';
|
||||
import { CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
||||
import * as xlsx from 'xlsx';
|
||||
|
||||
@Provide()
|
||||
export class DictService {
|
||||
|
||||
@InjectEntityModel(Dict)
|
||||
dictModel: Repository<Dict>;
|
||||
|
||||
@InjectEntityModel(DictItem)
|
||||
dictItemModel: Repository<DictItem>;
|
||||
|
||||
// 获取字典列表,支持按标题搜索
|
||||
async getDicts(title?: string) {
|
||||
// 如果提供了标题,则使用模糊查询
|
||||
if (title) {
|
||||
return this.dictModel.find({ where: { title: Like(`%${title}%`) } });
|
||||
// 生成并返回字典的XLSX模板
|
||||
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 importDictsFromXLSX(buffer: Buffer) {
|
||||
// 读取缓冲区中的工作簿
|
||||
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||||
// 获取第一个工作表的名称
|
||||
const wsname = wb.SheetNames[0];
|
||||
// 获取第一个工作表
|
||||
const ws = wb.Sheets[wsname];
|
||||
// 将工作表转换为JSON对象数组
|
||||
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title'] }).slice(1);
|
||||
// 创建要保存的字典实体数组
|
||||
const dicts = data.map((row: any) => {
|
||||
const dict = new Dict();
|
||||
dict.name = row.name;
|
||||
dict.title = row.title;
|
||||
return dict;
|
||||
});
|
||||
// 保存字典实体数组到数据库
|
||||
await this.dictModel.save(dicts);
|
||||
// 返回成功导入的记录数
|
||||
return { success: true, count: dicts.length };
|
||||
}
|
||||
|
||||
// 生成并返回字典项的XLSX模板
|
||||
getDictItemXLSXTemplate() {
|
||||
const headers = ['name', 'title', 'value', 'sort'];
|
||||
const ws = xlsx.utils.aoa_to_sheet([headers]);
|
||||
const wb = xlsx.utils.book_new();
|
||||
xlsx.utils.book_append_sheet(wb, ws, 'DictItems');
|
||||
return xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||||
}
|
||||
|
||||
// 从XLSX文件导入字典项
|
||||
async importDictItemsFromXLSX(buffer: Buffer, dictId: number) {
|
||||
const dict = await this.dictModel.findOneBy({ id: dictId });
|
||||
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) => {
|
||||
const item = new DictItem();
|
||||
item.name = row.name;
|
||||
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 });
|
||||
}
|
||||
|
||||
// 创建新字典
|
||||
|
|
|
|||
|
|
@ -113,64 +113,37 @@ export class ProductService {
|
|||
name?: string,
|
||||
brandId?: number
|
||||
): 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
|
||||
.createQueryBuilder('product')
|
||||
.leftJoin(
|
||||
DictItem,
|
||||
'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',
|
||||
]);
|
||||
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||
.leftJoinAndSelect('attribute.dict', 'dict');
|
||||
|
||||
// 模糊搜索 name,支持多个关键词
|
||||
nameFilter.forEach((word, index) => {
|
||||
qb.andWhere(`product.name LIKE :name${index}`, {
|
||||
[`name${index}`]: `%${word}%`,
|
||||
});
|
||||
});
|
||||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||
if (nameFilter.length > 0) {
|
||||
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) {
|
||||
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
|
||||
);
|
||||
|
||||
// 执行查询
|
||||
const items = await qb.getRawMany();
|
||||
const total = await qb.getCount();
|
||||
const [items, total] = await qb.getManyAndCount();
|
||||
|
||||
// 格式化返回的数据
|
||||
const formattedItems = items.map(product => {
|
||||
const getAttributeTitle = (dictName: string) =>
|
||||
product.attributes.find(a => a.dict.name === dictName)?.title || null;
|
||||
|
||||
return {
|
||||
items,
|
||||
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 {
|
||||
items: formattedItems,
|
||||
total,
|
||||
...pagination,
|
||||
};
|
||||
|
|
@ -237,29 +228,31 @@ export class ProductService {
|
|||
strength.title,
|
||||
strength.name
|
||||
);
|
||||
const humidityItem = await this.getOrCreateDictItem('humidity', humidity);
|
||||
|
||||
// 检查产品是否已存在
|
||||
const isExit = await this.productModel.findOne({
|
||||
where: {
|
||||
brandId: brandItem.id,
|
||||
flavorsId: flavorItem.id,
|
||||
strengthId: strengthItem.id,
|
||||
humidity,
|
||||
},
|
||||
// 检查具有完全相同属性组合的产品是否已存在
|
||||
const attributesToMatch = [brandItem, flavorItem, strengthItem, humidityItem];
|
||||
const qb = this.productModel.createQueryBuilder('product');
|
||||
attributesToMatch.forEach((attr, index) => {
|
||||
qb.innerJoin(
|
||||
'product.attributes',
|
||||
`attr${index}`,
|
||||
`attr${index}.id = :attrId${index}`,
|
||||
{ [`attrId${index}`]: attr.id }
|
||||
);
|
||||
});
|
||||
const isExit = await qb.getOne();
|
||||
|
||||
if (isExit) throw new Error('产品已存在');
|
||||
|
||||
// 创建新产品实例
|
||||
const product = new Product();
|
||||
product.name = name;
|
||||
product.description = description;
|
||||
product.brandId = brandItem.id;
|
||||
product.flavorsId = flavorItem.id;
|
||||
product.strengthId = strengthItem.id;
|
||||
product.humidity = humidity;
|
||||
product.attributes = attributesToMatch;
|
||||
|
||||
// 生成 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);
|
||||
|
|
@ -328,34 +321,32 @@ export class ProductService {
|
|||
return result.affected > 0; // `affected` 表示删除的行数
|
||||
}
|
||||
|
||||
async hasProductsInBrand(brandId: number): Promise<boolean> {
|
||||
// 检查是否有产品属于该品牌
|
||||
const count = await this.productModel.count({
|
||||
where: { brandId },
|
||||
|
||||
async hasAttribute(
|
||||
dictName: string,
|
||||
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;
|
||||
}
|
||||
|
||||
async hasBrand(title: string, id?: number): Promise<boolean> {
|
||||
// 查找 'brand' 字典
|
||||
const brandDict = await this.dictModel.findOne({
|
||||
where: { name: 'brand' },
|
||||
});
|
||||
|
||||
// 如果字典不存在,则品牌不存在
|
||||
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,
|
||||
});
|
||||
|
||||
async hasProductsInAttribute(attributeId: number): Promise<boolean> {
|
||||
const count = await this.productModel
|
||||
.createQueryBuilder('product')
|
||||
.innerJoin('product.attributes', 'attribute')
|
||||
.where('attribute.id = :attributeId', { attributeId })
|
||||
.getCount();
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
|
|
@ -451,27 +442,9 @@ export class ProductService {
|
|||
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(
|
||||
pagination: PaginationParams,
|
||||
title?: string
|
||||
|
|
@ -535,12 +508,7 @@ export class ProductService {
|
|||
const result = await this.dictItemModel.delete(id);
|
||||
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> {
|
||||
const strengthDict = await this.dictModel.findOne({
|
||||
|
|
|
|||
Loading…
Reference in New Issue