API/src/service/product.service.ts

1221 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {
CreateProductDTO,
UpdateProductDTO,
} 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';
import { Stock } from '../entity/stock.entity';
import { StockPoint } from '../entity/stock_point.entity';
import { ProductStockComponent } from '../entity/product_stock_component.entity';
import { Category } from '../entity/category.entity';
@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>;
@InjectEntityModel(Stock)
stockModel: Repository<Stock>;
@InjectEntityModel(StockPoint)
stockPointModel: Repository<StockPoint>;
@InjectEntityModel(ProductStockComponent)
productStockComponentModel: Repository<ProductStockComponent>;
@InjectEntityModel(Category)
categoryModel: Repository<Category>;
// 获取所有 WordPress 商品
async getWpProducts() {
return this.wpProductModel.find();
}
// 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')
.leftJoinAndSelect('product.category', 'category');
// 保证 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,
},
relations: ['category', 'attributes', 'attributes.dict']
});
}
async getProductList(
pagination: PaginationParams,
name?: string,
brandId?: number
): Promise<ProductPaginatedResponse> {
const qb = this.productModel
.createQueryBuilder('product')
.leftJoinAndSelect('product.attributes', 'attribute')
.leftJoinAndSelect('attribute.dict', 'dict')
.leftJoinAndSelect('product.category', 'category');
// 模糊搜索 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();
// 中文注释:根据类型填充组成信息
for (const product of items) {
if (product.type === 'single') {
// 中文注释:单品不持久化组成,这里仅返回一个基于 SKU 的虚拟组成
const component = new ProductStockComponent();
component.productId = product.id;
component.sku = product.sku;
component.quantity = 1;
product.components = [component];
} else {
// 中文注释:混装商品返回持久化的 SKU 组成
product.components = await this.productStockComponentModel.find({
where: { productId: product.id },
});
}
}
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}' 不存在`);
}
const nameForLookup = itemName || itemTitle;
// 查找字典项
let item = await this.dictItemModel.findOne({
where: { name: nameForLookup, dict: { id: dict.id } },
});
// 如果字典项不存在,则创建
if (!item) {
item = new DictItem();
item.title = itemTitle;
item.name = nameForLookup;
item.dict = dict;
await this.dictItemModel.save(item);
}
return item;
}
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
const { name, description, attributes, sku, price, categoryId } = createProductDTO;
// 条件判断(中文注释:校验属性输入)
if (!Array.isArray(attributes) || attributes.length === 0) {
// 如果提供了 categoryId 但没有 attributes初始化为空数组
if (!attributes && categoryId) {
// 继续执行,下面会处理 categoryId
} else {
throw new Error('属性列表不能为空');
}
}
const safeAttributes = attributes || [];
// 解析属性输入(中文注释:按 id 或 dictName 创建/关联字典项)
const resolvedAttributes: DictItem[] = [];
let categoryItem: Category | null = null;
// 如果提供了 categoryId设置分类
if (categoryId) {
categoryItem = await this.categoryModel.findOne({ where: { id: categoryId } });
if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`);
}
for (const attr of safeAttributes) {
// 中文注释:如果属性是分类,特殊处理
if (attr.dictName === 'category') {
if (attr.id) {
categoryItem = await this.categoryModel.findOneBy({ id: attr.id });
} else if (attr.name) {
categoryItem = await this.categoryModel.findOneBy({ name: attr.name });
}
continue;
}
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;
if (categoryItem) {
product.category = categoryItem;
}
// 条件判断(中文注释:设置商品类型,默认 simple
product.type = (createProductDTO.type as any) || 'single';
// 生成或设置 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', 'category'] });
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.categoryId !== undefined) {
if (updateProductDTO.categoryId) {
const categoryItem = await this.categoryModel.findOne({ where: { id: updateProductDTO.categoryId } });
if (!categoryItem) throw new Error(`分类 ID ${updateProductDTO.categoryId} 不存在`);
product.category = categoryItem;
} else {
// 如果传了 0 或 null可以清除分类根据需求
// product.category = null;
}
}
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) {
// 中文注释:如果属性是分类,特殊处理
if (attr.dictName === 'category') {
if (attr.id) {
const categoryItem = await this.categoryModel.findOneBy({ id: attr.id });
if (categoryItem) product.category = categoryItem;
}
continue;
}
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;
}
// 条件判断(中文注释:更新商品类型,如传入)
if (updateProductDTO.type !== undefined) {
product.type = updateProductDTO.type as any;
}
// 保存更新后的产品
const saved = await this.productModel.save(product);
return saved;
}
// 中文注释:获取产品的库存组成列表(表关联版本)
async getProductComponents(productId: number): Promise<any[]> {
// 条件判断:确保产品存在
const product = await this.productModel.findOne({ where: { id: productId } });
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
let components: ProductStockComponent[] = [];
// 条件判断(中文注释:单品 simple 不持久化组成,按 SKU 动态返回单条组成)
if (product.type === 'single') {
const comp = new ProductStockComponent();
comp.productId = productId;
comp.sku = product.sku;
comp.quantity = 1;
components = [comp];
} else {
// 混装 bundle返回已保存的 SKU 组成
components = await this.productStockComponentModel.find({ where: { productId } });
}
// 中文注释:获取所有组件的 SKU 列表
const skus = components.map(c => c.sku);
if (skus.length === 0) {
return components;
}
// 中文注释:查询这些 SKU 的库存信息
const stocks = await this.stockModel.find({
where: { sku: In(skus) },
});
// 中文注释:获取所有相关的库存点 ID
const stockPointIds = [...new Set(stocks.map(s => s.stockPointId))];
const stockPoints = await this.stockPointModel.find({ where: { id: In(stockPointIds) } });
const stockPointMap = stockPoints.reduce((map, sp) => {
map[sp.id] = sp;
return map;
}, {});
// 中文注释:将库存信息按 SKU 分组
const stockMap = stocks.reduce((map, stock) => {
if (!map[stock.sku]) {
map[stock.sku] = [];
}
const stockPoint = stockPointMap[stock.stockPointId];
if (stockPoint) {
map[stock.sku].push({
name: stockPoint.name,
quantity: stock.quantity,
});
}
return map;
}, {});
// 中文注释:将库存信息附加到组件上
const componentsWithStock = components.map(comp => {
return {
...comp,
stock: stockMap[comp.sku] || [],
};
});
return componentsWithStock;
}
// 中文注释:设置产品的库存组成(覆盖式,表关联版本)
async setProductComponents(
productId: number,
items: { sku: string; quantity: number }[]
): Promise<ProductStockComponent[]> {
// 条件判断:确保产品存在
const product = await this.productModel.findOne({ where: { id: productId } });
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
// 条件判断(中文注释:单品 simple 不允许手动设置组成)
if (product.type === 'single') {
throw new Error('单品无需设置组成');
}
const validItems = (items || [])
.filter(i => i && i.sku && i.quantity && i.quantity > 0)
.map(i => ({ sku: String(i.sku), quantity: Number(i.quantity) }));
// 删除旧的组成
await this.productStockComponentModel.delete({ productId });
// 插入新的组成
const created: ProductStockComponent[] = [];
for (const i of validItems) {
// 中文注释:校验 SKU 格式,允许不存在库存但必须非空
if (!i.sku || i.sku.trim().length === 0) {
throw new Error('SKU 不能为空');
}
const comp = new ProductStockComponent();
comp.productId = productId;
comp.sku = i.sku;
comp.quantity = i.quantity;
created.push(await this.productStockComponentModel.save(comp));
}
return created;
}
// 中文注释:根据 SKU 自动绑定产品的库存组成(匹配所有相同 SKU 的库存,默认数量 1
async autoBindComponentsBySku(productId: number): Promise<ProductStockComponent[]> {
// 条件判断:确保产品存在
const product = await this.productModel.findOne({ where: { id: productId } });
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
// 中文注释:按 SKU 自动绑定
// 条件判断simple 类型不持久化组成,直接返回单条基于 SKU 的组成
if (product.type === 'single') {
const comp = new ProductStockComponent();
comp.productId = productId;
comp.sku = product.sku;
comp.quantity = 1; // 默认数量 1
return [comp];
}
// bundle 类型:若不存在则持久化一条基于 SKU 的组成
const exist = await this.productStockComponentModel.findOne({ where: { productId, sku: product.sku } });
if (!exist) {
const comp = new ProductStockComponent();
comp.productId = productId;
comp.sku = product.sku;
comp.quantity = 1;
await this.productStockComponentModel.save(comp);
}
return await this.getProductComponents(productId);
}
// 重复定义的 getProductList 已合并到前面的实现(中文注释:移除重复)
async updatenameCn(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 sku = product.sku;
// 查询 wp_product 表中是否存在与该 SKU 关联的产品
const wpProduct = await this.wpProductModel
.createQueryBuilder('wp_product')
.where('JSON_CONTAINS(wp_product.constitution, :sku)', {
sku: JSON.stringify({ sku: sku }),
})
.getOne();
if (wpProduct) {
throw new Error('无法删除请先删除关联的WP产品');
}
const variation = await this.variationModel
.createQueryBuilder('variation')
.where('JSON_CONTAINS(variation.constitution, :sku)', {
sku: JSON.stringify({ sku: sku }),
})
.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: any): 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: any) {
// 确认品牌是否存在
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: any): 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: any) {
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: any): 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: any) {
// 先查询(中文注释:确保尺寸项存在)
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: any): 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: any) {
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`;
}
// 中文注释:导出所有产品为 CSV 文本
async exportProductsCSV(): Promise<string> {
// 查询所有产品及其属性(中文注释:包含字典关系)
const products = await this.productModel.find({
relations: ['attributes', 'attributes.dict'],
order: { id: 'ASC' },
});
// 定义 CSV 表头(中文注释:与导入字段一致)
const headers = [
'sku',
'name',
'nameCn',
'price',
'promotionPrice',
'type',
'stock',
'brand',
'flavor',
'strength',
'size',
'description',
];
// 中文注释CSV 字段转义,处理逗号与双引号
const esc = (v: any) => {
const s = v === undefined || v === null ? '' : String(v);
const needsQuote = /[",\n]/.test(s);
const escaped = s.replace(/"/g, '""');
return needsQuote ? `"${escaped}"` : escaped;
};
// 中文注释:将属性列表转为字典名到显示值的映射
const pickAttr = (prod: Product, key: string) => {
const list = (prod.attributes || []).filter(a => a?.dict?.name === key);
if (list.length === 0) return '';
// 多个值使用分号分隔
return list.map(a => a.title || a.name).join(';');
};
const rows: string[] = [];
rows.push(headers.join(','));
for (const p of products) {
// 中文注释:逐行输出产品数据
const row = [
esc(p.sku),
esc(p.name),
esc(p.nameCn),
esc(p.price),
esc(p.promotionPrice),
esc(p.type),
esc(p.stock),
esc(pickAttr(p, 'brand')),
esc(pickAttr(p, 'flavor')),
esc(pickAttr(p, 'strength')),
esc(pickAttr(p, 'size')),
esc(p.description),
].join(',');
rows.push(row);
}
return rows.join('\n');
}
// 中文注释:从 CSV 导入产品;存在则更新,不存在则创建
async importProductsCSV(buffer: Buffer): Promise<{ created: number; updated: number; errors: string[] }> {
// 解析 CSV中文注释使用 csv-parse/sync 按表头解析)
const { parse } = await import('csv-parse/sync');
let records: any[] = [];
try {
records = parse(buffer, {
columns: true,
skip_empty_lines: true,
trim: true,
});
} catch (e: any) {
return { created: 0, updated: 0, errors: [`CSV 解析失败:${e?.message || e}`] };
}
let created = 0;
let updated = 0;
const errors: string[] = [];
// 中文注释:逐条处理记录
for (const rec of records) {
try {
// 条件判断:必须包含 sku
const sku: string = (rec.sku || '').trim();
if (!sku) {
// 缺少 SKU 直接跳过
errors.push('缺少 SKU 的记录已跳过');
continue;
}
// 查找现有产品
const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] });
// 中文注释:准备基础字段
const base = {
name: rec.name || '',
nameCn: rec.nameCn || '',
description: rec.description || '',
price: rec.price ? Number(rec.price) : undefined,
promotionPrice: rec.promotionPrice ? Number(rec.promotionPrice) : undefined,
type: rec.type || '',
stock: rec.stock ? Number(rec.stock) : undefined,
sku,
} as any;
// 中文注释:解析属性字段(分号分隔多值)
const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []);
const brands = parseList(rec.brand);
const flavors = parseList(rec.flavor);
const strengths = parseList(rec.strength);
const sizes = parseList(rec.size);
// 中文注释:将属性解析为 DTO 输入
const attrDTOs: { dictName: string; title: string }[] = [];
for (const b of brands) attrDTOs.push({ dictName: 'brand', title: b });
for (const f of flavors) attrDTOs.push({ dictName: 'flavor', title: f });
for (const s of strengths) attrDTOs.push({ dictName: 'strength', title: s });
for (const z of sizes) attrDTOs.push({ dictName: 'size', title: z });
if (!exist) {
// 中文注释:创建新产品
const dto = {
name: base.name,
description: base.description,
price: base.price,
sku: base.sku,
attributes: attrDTOs,
} as any;
const createdProduct = await this.createProduct(dto);
// 条件判断:更新可选字段
const patch: any = {};
if (base.nameCn) patch.nameCn = base.nameCn;
if (base.promotionPrice !== undefined) patch.promotionPrice = base.promotionPrice;
if (base.type) patch.type = base.type;
if (base.stock !== undefined) patch.stock = base.stock;
if (Object.keys(patch).length > 0) await this.productModel.update(createdProduct.id, patch);
created += 1;
} else {
// 中文注释:更新产品
const updateDTO: any = {
name: base.name || exist.name,
description: base.description || exist.description,
price: base.price !== undefined ? base.price : exist.price,
sku: base.sku,
attributes: attrDTOs,
};
// 条件判断:附加可选字段
if (base.nameCn) updateDTO.nameCn = base.nameCn;
if (base.promotionPrice !== undefined) updateDTO.promotionPrice = base.promotionPrice;
if (base.type) updateDTO.type = base.type;
if (base.stock !== undefined) updateDTO.stock = base.stock;
await this.updateProduct(exist.id, updateDTO);
updated += 1;
}
} catch (e: any) {
errors.push(e?.message || String(e));
}
}
return { created, updated, errors };
}
}