API/src/service/product.service.ts

835 lines
26 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 {
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`;
}
}