835 lines
26 KiB
TypeScript
835 lines
26 KiB
TypeScript
import { Inject, Provide } from '@midwayjs/core';
|
||
import { In, Like, Not, Repository } from 'typeorm';
|
||
import { Product } from '../entity/product.entity';
|
||
import { paginate } from '../utils/paginate.util';
|
||
import { PaginationParams } from '../interface';
|
||
import {
|
||
CreateBrandDTO,
|
||
CreateFlavorsDTO,
|
||
CreateProductDTO,
|
||
CreateStrengthDTO,
|
||
CreateSizeDTO,
|
||
UpdateBrandDTO,
|
||
UpdateFlavorsDTO,
|
||
UpdateProductDTO,
|
||
UpdateStrengthDTO,
|
||
UpdateSizeDTO,
|
||
} from '../dto/product.dto';
|
||
import {
|
||
BrandPaginatedResponse,
|
||
FlavorsPaginatedResponse,
|
||
ProductPaginatedResponse,
|
||
StrengthPaginatedResponse,
|
||
SizePaginatedResponse,
|
||
} from '../dto/reponse.dto';
|
||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||
import { WpProduct } from '../entity/wp_product.entity';
|
||
import { Variation } from '../entity/variation.entity';
|
||
import { Dict } from '../entity/dict.entity';
|
||
import { DictItem } from '../entity/dict_item.entity';
|
||
import { Context } from '@midwayjs/koa';
|
||
import { TemplateService } from './template.service';
|
||
import { StockService } from './stock.service';
|
||
|
||
@Provide()
|
||
export class ProductService {
|
||
@Inject()
|
||
ctx: Context;
|
||
|
||
@Inject()
|
||
templateService: TemplateService;
|
||
|
||
@Inject()
|
||
stockService: StockService;
|
||
|
||
@InjectEntityModel(Product)
|
||
productModel: Repository<Product>;
|
||
|
||
@InjectEntityModel(Dict)
|
||
dictModel: Repository<Dict>;
|
||
|
||
@InjectEntityModel(DictItem)
|
||
dictItemModel: Repository<DictItem>;
|
||
|
||
@InjectEntityModel(WpProduct)
|
||
wpProductModel: Repository<WpProduct>;
|
||
|
||
@InjectEntityModel(Variation)
|
||
variationModel: Repository<Variation>;
|
||
|
||
// async findProductsByName(name: string): Promise<Product[]> {
|
||
// const where: any = {};
|
||
// const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||
// if (nameFilter.length > 0) {
|
||
// const nameConditions = nameFilter.map(word => Like(`%${word}%`));
|
||
// where.name = And(...nameConditions);
|
||
// }
|
||
// if(name){
|
||
// where.nameCn = Like(`%${name}%`)
|
||
// }
|
||
// where.sku = Not(IsNull());
|
||
// // 查询 SKU 不为空且 name 包含关键字的产品,最多返回 50 条
|
||
// return this.productModel.find({
|
||
// where,
|
||
// take: 50,
|
||
// });
|
||
// }
|
||
|
||
async findProductsByName(name: string): Promise<Product[]> {
|
||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||
const query = this.productModel.createQueryBuilder('product');
|
||
|
||
// 保证 sku 不为空
|
||
query.where('product.sku IS NOT NULL');
|
||
|
||
if (nameFilter.length > 0 || name) {
|
||
const params: Record<string, string> = {};
|
||
const conditions: string[] = [];
|
||
|
||
// 英文名关键词全部匹配(AND)
|
||
if (nameFilter.length > 0) {
|
||
const nameConds = nameFilter.map((word, index) => {
|
||
const key = `name${index}`;
|
||
params[key] = `%${word}%`;
|
||
return `product.name LIKE :${key}`;
|
||
});
|
||
conditions.push(`(${nameConds.join(' AND ')})`);
|
||
}
|
||
|
||
// 中文名模糊匹配
|
||
if (name) {
|
||
params['nameCn'] = `%${name}%`;
|
||
conditions.push(`product.nameCn LIKE :nameCn`);
|
||
}
|
||
|
||
// 英文名关键词匹配 OR 中文名匹配
|
||
query.andWhere(`(${conditions.join(' OR ')})`, params);
|
||
}
|
||
|
||
query.take(50);
|
||
|
||
return await query.getMany();
|
||
}
|
||
|
||
async findProductBySku(sku: string): Promise<Product> {
|
||
return this.productModel.findOne({
|
||
where: {
|
||
sku,
|
||
},
|
||
});
|
||
}
|
||
|
||
async getProductList(
|
||
pagination: PaginationParams,
|
||
name?: string,
|
||
brandId?: number
|
||
): Promise<ProductPaginatedResponse> {
|
||
const qb = this.productModel
|
||
.createQueryBuilder('product')
|
||
.leftJoinAndSelect('product.attributes', 'attribute')
|
||
.leftJoinAndSelect('attribute.dict', 'dict');
|
||
|
||
// 模糊搜索 name,支持多个关键词
|
||
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(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;
|
||
});
|
||
}
|
||
|
||
// 分页
|
||
qb.skip((pagination.current - 1) * pagination.pageSize).take(
|
||
pagination.pageSize
|
||
);
|
||
|
||
const [items, total] = await qb.getManyAndCount();
|
||
|
||
return {
|
||
items,
|
||
total,
|
||
...pagination,
|
||
};
|
||
}
|
||
|
||
async getOrCreateAttribute(
|
||
dictName: string,
|
||
itemTitle: string,
|
||
itemName?: string
|
||
): Promise<DictItem> {
|
||
// 查找字典
|
||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||
if (!dict) {
|
||
throw new Error(`字典 '${dictName}' 不存在`);
|
||
}
|
||
|
||
// 查找字典项
|
||
let item = await this.dictItemModel.findOne({
|
||
where: { title: itemTitle, dict: { id: dict.id } },
|
||
});
|
||
|
||
// 如果字典项不存在,则创建
|
||
if (!item) {
|
||
item = new DictItem();
|
||
item.title = itemTitle;
|
||
item.name = itemName || itemTitle; // 如果没有提供 name,则使用 title
|
||
item.dict = dict;
|
||
await this.dictItemModel.save(item);
|
||
}
|
||
|
||
return item;
|
||
}
|
||
|
||
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
||
const { name, description, attributes, sku, price } = createProductDTO;
|
||
|
||
// 条件判断(中文注释:校验属性输入)
|
||
if (!Array.isArray(attributes) || attributes.length === 0) {
|
||
throw new Error('属性列表不能为空');
|
||
}
|
||
|
||
// 解析属性输入(中文注释:按 id 或 dictName 创建/关联字典项)
|
||
const resolvedAttributes: DictItem[] = [];
|
||
for (const attr of attributes) {
|
||
let item: DictItem | null = null;
|
||
if (attr.id) {
|
||
// 中文注释:如果传入了 id,直接查找字典项并使用,不强制要求 dictName
|
||
item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] });
|
||
if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`);
|
||
} else {
|
||
// 中文注释:当未提供 id 时,需要 dictName 与 title/name 信息创建或获取字典项
|
||
if (!attr?.dictName) throw new Error('属性项缺少字典名称');
|
||
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 qb = this.productModel.createQueryBuilder('product');
|
||
resolvedAttributes.forEach((attr, index) => {
|
||
qb.innerJoin(
|
||
'product.attributes',
|
||
`attr${index}`,
|
||
`attr${index}.id = :attrId${index}`,
|
||
{ [`attrId${index}`]: attr.id }
|
||
);
|
||
});
|
||
const isExist = await qb.getOne();
|
||
if (isExist) throw new Error('产品已存在');
|
||
|
||
// 创建新产品实例(中文注释:绑定属性与基础字段)
|
||
const product = new Product();
|
||
product.name = name;
|
||
product.description = description;
|
||
product.attributes = resolvedAttributes;
|
||
|
||
// 生成或设置 SKU(中文注释:基于属性字典项的 name 生成)
|
||
if (sku) {
|
||
product.sku = sku;
|
||
} else {
|
||
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: attributeMap['brand'] || '',
|
||
flavor: attributeMap['flavor'] || '',
|
||
strength: attributeMap['strength'] || '',
|
||
humidity: attributeMap['humidity'] || '',
|
||
});
|
||
}
|
||
|
||
// 价格与促销价(中文注释:可选字段)
|
||
if (price !== undefined) {
|
||
product.price = Number(price);
|
||
}
|
||
const promotionPrice = (createProductDTO as any)?.promotionPrice;
|
||
if (promotionPrice !== undefined) {
|
||
product.promotionPrice = Number(promotionPrice);
|
||
}
|
||
|
||
return await this.productModel.save(product);
|
||
}
|
||
|
||
async updateProduct(
|
||
id: number,
|
||
updateProductDTO: UpdateProductDTO
|
||
): Promise<Product> {
|
||
// 检查产品是否存在(包含属性关系)
|
||
const product = await this.productModel.findOne({ where: { id }, relations: ['attributes', 'attributes.dict'] });
|
||
if (!product) {
|
||
throw new Error(`产品 ID ${id} 不存在`);
|
||
}
|
||
|
||
// 处理基础字段更新(若传入则更新)
|
||
if (updateProductDTO.name !== undefined) {
|
||
product.name = updateProductDTO.name;
|
||
}
|
||
if (updateProductDTO.description !== undefined) {
|
||
product.description = updateProductDTO.description;
|
||
}
|
||
if (updateProductDTO.price !== undefined) {
|
||
product.price = Number(updateProductDTO.price);
|
||
}
|
||
if ((updateProductDTO as any).promotionPrice !== undefined) {
|
||
product.promotionPrice = Number((updateProductDTO as any).promotionPrice);
|
||
}
|
||
if (updateProductDTO.sku !== undefined) {
|
||
// 校验 SKU 唯一性(如变更)
|
||
const newSku = updateProductDTO.sku;
|
||
if (newSku && newSku !== product.sku) {
|
||
const exist = await this.productModel.findOne({ where: { sku: newSku } });
|
||
if (exist) {
|
||
throw new Error('SKU 已存在,请更换后重试');
|
||
}
|
||
product.sku = newSku;
|
||
}
|
||
}
|
||
|
||
// 处理属性更新(中文注释:若传入 attributes 则按字典名称替换对应项)
|
||
if (Array.isArray(updateProductDTO.attributes) && updateProductDTO.attributes.length > 0) {
|
||
const nextAttributes: DictItem[] = [...(product.attributes || [])];
|
||
|
||
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);
|
||
};
|
||
|
||
for (const attr of updateProductDTO.attributes) {
|
||
let item: DictItem | null = null;
|
||
if (attr.id) {
|
||
// 中文注释:当提供 id 时直接查询字典项,不强制要求 dictName
|
||
item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] });
|
||
if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`);
|
||
} else {
|
||
// 中文注释:未提供 id 则需要 dictName 与 title/name 信息
|
||
if (!attr?.dictName) throw new Error('属性项缺少字典名称');
|
||
const titleOrName = attr.title || attr.name;
|
||
if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name');
|
||
item = await this.getOrCreateAttribute(attr.dictName, titleOrName, attr.name);
|
||
}
|
||
// 中文注释:以传入的 dictName 或查询到的 item.dict.name 作为替换键
|
||
const dictKey = attr.dictName || item?.dict?.name;
|
||
if (!dictKey) throw new Error('无法确定字典名称用于替换属性');
|
||
replaceAttr(dictKey, item);
|
||
}
|
||
|
||
product.attributes = nextAttributes;
|
||
}
|
||
|
||
// 保存更新后的产品
|
||
const saved = await this.productModel.save(product);
|
||
return saved;
|
||
}
|
||
|
||
async updateProductNameCn(id: number, nameCn: string): Promise<Product> {
|
||
// 确认产品是否存在
|
||
const product = await this.productModel.findOneBy({ id });
|
||
if (!product) {
|
||
throw new Error(`产品 ID ${id} 不存在`);
|
||
}
|
||
// 更新产品
|
||
await this.productModel.update(id, { nameCn });
|
||
// 返回更新后的产品
|
||
return await this.productModel.findOneBy({ id });
|
||
}
|
||
|
||
async deleteProduct(id: number): Promise<boolean> {
|
||
// 检查产品是否存在
|
||
const product = await this.productModel.findOneBy({ id });
|
||
if (!product) {
|
||
throw new Error(`产品 ID ${id} 不存在`);
|
||
}
|
||
const productSku = product.sku;
|
||
|
||
// 查询 wp_product 表中是否存在与该 SKU 关联的产品
|
||
const wpProduct = await this.wpProductModel
|
||
.createQueryBuilder('wp_product')
|
||
.where('JSON_CONTAINS(wp_product.constitution, :sku)', {
|
||
sku: JSON.stringify({ sku: productSku }),
|
||
})
|
||
.getOne();
|
||
if (wpProduct) {
|
||
throw new Error('无法删除,请先删除关联的WP产品');
|
||
}
|
||
|
||
const variation = await this.variationModel
|
||
.createQueryBuilder('variation')
|
||
.where('JSON_CONTAINS(variation.constitution, :sku)', {
|
||
sku: JSON.stringify({ sku: productSku }),
|
||
})
|
||
.getOne();
|
||
|
||
if (variation) {
|
||
console.log(variation);
|
||
throw new Error('无法删除,请先删除关联的WP变体');
|
||
}
|
||
|
||
// 删除产品
|
||
const result = await this.productModel.delete(id);
|
||
return result.affected > 0; // `affected` 表示删除的行数
|
||
}
|
||
|
||
|
||
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 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;
|
||
}
|
||
|
||
async getBrandList(
|
||
pagination: PaginationParams,
|
||
title?: string
|
||
): Promise<BrandPaginatedResponse> {
|
||
// 查找 'brand' 字典
|
||
const brandDict = await this.dictModel.findOne({
|
||
where: { name: 'brand' },
|
||
});
|
||
|
||
// 如果字典不存在,则返回空
|
||
if (!brandDict) {
|
||
return {
|
||
items: [],
|
||
total: 0,
|
||
...pagination,
|
||
};
|
||
}
|
||
|
||
// 设置查询条件
|
||
const where: any = { dict: { id: brandDict.id } };
|
||
if (title) {
|
||
where.title = Like(`%${title}%`);
|
||
}
|
||
|
||
// 分页查询
|
||
return await paginate(this.dictItemModel, { pagination, where });
|
||
}
|
||
|
||
async getBrandAll(): Promise<BrandPaginatedResponse> {
|
||
// 查找 'brand' 字典
|
||
const brandDict = await this.dictModel.findOne({
|
||
where: { name: 'brand' },
|
||
});
|
||
|
||
// 如果字典不存在,则返回空数组
|
||
if (!brandDict) {
|
||
return [];
|
||
}
|
||
|
||
// 返回所有品牌
|
||
return this.dictItemModel.find({ where: { dict: { id: brandDict.id } } });
|
||
}
|
||
|
||
async createBrand(createBrandDTO: CreateBrandDTO): Promise<DictItem> {
|
||
const { title, name } = createBrandDTO;
|
||
|
||
// 查找 'brand' 字典
|
||
const brandDict = await this.dictModel.findOne({
|
||
where: { name: 'brand' },
|
||
});
|
||
|
||
// 如果字典不存在,则抛出错误
|
||
if (!brandDict) {
|
||
throw new Error('品牌字典不存在');
|
||
}
|
||
|
||
// 创建新的品牌实例
|
||
const brand = new DictItem();
|
||
brand.title = title;
|
||
brand.name = name;
|
||
brand.dict = brandDict;
|
||
|
||
// 保存到数据库
|
||
return await this.dictItemModel.save(brand);
|
||
}
|
||
|
||
async updateBrand(id: number, updateBrand: UpdateBrandDTO) {
|
||
// 确认品牌是否存在
|
||
const brand = await this.dictItemModel.findOneBy({ id });
|
||
if (!brand) {
|
||
throw new Error(`品牌 ID ${id} 不存在`);
|
||
}
|
||
|
||
// 更新品牌
|
||
await this.dictItemModel.update(id, updateBrand);
|
||
|
||
// 返回更新后的品牌
|
||
return await this.dictItemModel.findOneBy({ id });
|
||
}
|
||
|
||
async deleteBrand(id: number): Promise<boolean> {
|
||
// 检查品牌是否存在
|
||
const brand = await this.dictItemModel.findOneBy({ id });
|
||
if (!brand) {
|
||
throw new Error(`品牌 ID ${id} 不存在`);
|
||
}
|
||
|
||
// 删除品牌
|
||
const result = await this.dictItemModel.delete(id);
|
||
return result.affected > 0; // `affected` 表示删除的行数
|
||
}
|
||
|
||
|
||
|
||
|
||
async getFlavorsList(
|
||
pagination: PaginationParams,
|
||
title?: string
|
||
): Promise<FlavorsPaginatedResponse> {
|
||
const flavorsDict = await this.dictModel.findOne({
|
||
where: { name: 'flavor' },
|
||
});
|
||
if (!flavorsDict) {
|
||
return {
|
||
items: [],
|
||
total: 0,
|
||
...pagination,
|
||
};
|
||
}
|
||
const where: any = { dict: { id: flavorsDict.id } };
|
||
if (title) {
|
||
where.title = Like(`%${title}%`);
|
||
}
|
||
return await paginate(this.dictItemModel, { pagination, where });
|
||
}
|
||
|
||
async getFlavorsAll(): Promise<FlavorsPaginatedResponse> {
|
||
const flavorsDict = await this.dictModel.findOne({
|
||
where: { name: 'flavor' },
|
||
});
|
||
if (!flavorsDict) {
|
||
return [];
|
||
}
|
||
return this.dictItemModel.find({ where: { dict: { id: flavorsDict.id } } });
|
||
}
|
||
|
||
async createFlavors(createFlavorsDTO: CreateFlavorsDTO): Promise<DictItem> {
|
||
const { title, name } = createFlavorsDTO;
|
||
const flavorsDict = await this.dictModel.findOne({
|
||
where: { name: 'flavor' },
|
||
});
|
||
if (!flavorsDict) {
|
||
throw new Error('口味字典不存在');
|
||
}
|
||
const flavors = new DictItem();
|
||
flavors.title = title;
|
||
flavors.name = name;
|
||
flavors.dict = flavorsDict;
|
||
return await this.dictItemModel.save(flavors);
|
||
}
|
||
|
||
async updateFlavors(id: number, updateFlavors: UpdateFlavorsDTO) {
|
||
const flavors = await this.dictItemModel.findOneBy({ id });
|
||
if (!flavors) {
|
||
throw new Error(`口味 ID ${id} 不存在`);
|
||
}
|
||
await this.dictItemModel.update(id, updateFlavors);
|
||
return await this.dictItemModel.findOneBy({ id });
|
||
}
|
||
|
||
async deleteFlavors(id: number): Promise<boolean> {
|
||
const flavors = await this.dictItemModel.findOneBy({ id });
|
||
if (!flavors) {
|
||
throw new Error(`口味 ID ${id} 不存在`);
|
||
}
|
||
const result = await this.dictItemModel.delete(id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
// size 尺寸相关方法
|
||
async getSizeList(
|
||
pagination: PaginationParams,
|
||
title?: string
|
||
): Promise<SizePaginatedResponse> {
|
||
// 查找 'size' 字典(中文注释:用于尺寸)
|
||
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
|
||
// 条件判断(中文注释:如果字典不存在则返回空分页)
|
||
if (!sizeDict) {
|
||
return {
|
||
items: [],
|
||
total: 0,
|
||
...pagination,
|
||
} as any;
|
||
}
|
||
// 构建 where 条件(中文注释:按标题模糊搜索)
|
||
const where: any = { dict: { id: sizeDict.id } };
|
||
if (title) {
|
||
where.title = Like(`%${title}%`);
|
||
}
|
||
// 分页查询(中文注释:复用通用分页工具)
|
||
return await paginate(this.dictItemModel, { pagination, where });
|
||
}
|
||
|
||
async getSizeAll(): Promise<SizePaginatedResponse> {
|
||
// 查找 'size' 字典(中文注释:获取所有尺寸项)
|
||
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
|
||
// 条件判断(中文注释:如果字典不存在返回空数组)
|
||
if (!sizeDict) {
|
||
return [] as any;
|
||
}
|
||
return this.dictItemModel.find({ where: { dict: { id: sizeDict.id } } }) as any;
|
||
}
|
||
|
||
async createSize(createSizeDTO: CreateSizeDTO): Promise<DictItem> {
|
||
const { title, name } = createSizeDTO;
|
||
// 获取 size 字典(中文注释:用于挂载尺寸项)
|
||
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
|
||
// 条件判断(中文注释:尺寸字典不存在则抛错)
|
||
if (!sizeDict) {
|
||
throw new Error('尺寸字典不存在');
|
||
}
|
||
// 创建字典项(中文注释:保存尺寸名称与唯一标识)
|
||
const size = new DictItem();
|
||
size.title = title;
|
||
size.name = name;
|
||
size.dict = sizeDict;
|
||
return await this.dictItemModel.save(size);
|
||
}
|
||
|
||
async updateSize(id: number, updateSize: UpdateSizeDTO) {
|
||
// 先查询(中文注释:确保尺寸项存在)
|
||
const size = await this.dictItemModel.findOneBy({ id });
|
||
// 条件判断(中文注释:不存在则报错)
|
||
if (!size) {
|
||
throw new Error(`尺寸 ID ${id} 不存在`);
|
||
}
|
||
// 更新(中文注释:写入变更字段)
|
||
await this.dictItemModel.update(id, updateSize);
|
||
// 返回最新(中文注释:再次查询返回)
|
||
return await this.dictItemModel.findOneBy({ id });
|
||
}
|
||
|
||
async deleteSize(id: number): Promise<boolean> {
|
||
// 先查询(中文注释:确保尺寸项存在)
|
||
const size = await this.dictItemModel.findOneBy({ id });
|
||
// 条件判断(中文注释:不存在则报错)
|
||
if (!size) {
|
||
throw new Error(`尺寸 ID ${id} 不存在`);
|
||
}
|
||
// 删除(中文注释:执行删除并返回受影响行数是否>0)
|
||
const result = await this.dictItemModel.delete(id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
|
||
async hasStrength(title: string, id?: string): Promise<boolean> {
|
||
const strengthDict = await this.dictModel.findOne({
|
||
where: { name: 'strength' },
|
||
});
|
||
if (!strengthDict) {
|
||
return false;
|
||
}
|
||
const where: any = { title, dict: { id: strengthDict.id } };
|
||
if (id) where.id = Not(id);
|
||
const count = await this.dictItemModel.count({
|
||
where,
|
||
});
|
||
return count > 0;
|
||
}
|
||
async getStrengthList(
|
||
pagination: PaginationParams,
|
||
title?: string
|
||
): Promise<StrengthPaginatedResponse> {
|
||
const strengthDict = await this.dictModel.findOne({
|
||
where: { name: 'strength' },
|
||
});
|
||
if (!strengthDict) {
|
||
return {
|
||
items: [],
|
||
total: 0,
|
||
...pagination,
|
||
};
|
||
}
|
||
const where: any = { dict: { id: strengthDict.id } };
|
||
if (title) {
|
||
where.title = Like(`%${title}%`);
|
||
}
|
||
return await paginate(this.dictItemModel, { pagination, where });
|
||
}
|
||
|
||
async getStrengthAll(): Promise<StrengthPaginatedResponse> {
|
||
const strengthDict = await this.dictModel.findOne({
|
||
where: { name: 'strength' },
|
||
});
|
||
if (!strengthDict) {
|
||
return [];
|
||
}
|
||
return this.dictItemModel.find({ where: { dict: { id: strengthDict.id } } });
|
||
}
|
||
|
||
async createStrength(createStrengthDTO: CreateStrengthDTO): Promise<DictItem> {
|
||
const { title, name } = createStrengthDTO;
|
||
const strengthDict = await this.dictModel.findOne({
|
||
where: { name: 'strength' },
|
||
});
|
||
if (!strengthDict) {
|
||
throw new Error('规格字典不存在');
|
||
}
|
||
const strength = new DictItem();
|
||
strength.title = title;
|
||
strength.name = name;
|
||
strength.dict = strengthDict;
|
||
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) {
|
||
throw new Error(`规格 ID ${id} 不存在`);
|
||
}
|
||
await this.dictItemModel.update(id, updateStrength);
|
||
return await this.dictItemModel.findOneBy({ id });
|
||
}
|
||
|
||
async deleteStrength(id: number): Promise<boolean> {
|
||
const strength = await this.dictItemModel.findOneBy({ id });
|
||
if (!strength) {
|
||
throw new Error(`规格 ID ${id} 不存在`);
|
||
}
|
||
const result = await this.dictItemModel.delete(id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
async batchSetSku(skus: { productId: number; sku: string }[]) {
|
||
// 提取所有 sku
|
||
const skuList = skus.map(item => item.sku);
|
||
|
||
// 检查是否存在重复 sku
|
||
const existingProducts = await this.productModel.find({
|
||
where: { sku: In(skuList) },
|
||
});
|
||
|
||
if (existingProducts.length > 0) {
|
||
const existingSkus = existingProducts.map(product => product.sku);
|
||
throw new Error(`以下 SKU 已存在: ${existingSkus.join(', ')}`);
|
||
}
|
||
|
||
// 遍历检查产品 ID 是否存在,并更新 sku
|
||
for (const { productId, sku } of skus) {
|
||
const product = await this.productModel.findOne({
|
||
where: { id: productId },
|
||
});
|
||
if (!product) {
|
||
throw new Error(`产品 ID '${productId}' 不存在`);
|
||
}
|
||
|
||
product.sku = sku;
|
||
await this.productModel.save(product);
|
||
}
|
||
|
||
return `成功更新 ${skus.length} 个 sku`;
|
||
}
|
||
}
|