diff --git a/src/product/product.dto.ts b/src/product/product.dto.ts index c7d20d6..f662208 100644 --- a/src/product/product.dto.ts +++ b/src/product/product.dto.ts @@ -23,10 +23,6 @@ export class CreateSkuDto { @Min(0) stock: number; - @IsArray() - @ValidateNested({ each: true }) - @Type(() => CreateAttributeValueDto) - attributeValues: CreateAttributeValueDto[]; } export class CreateProductDto { @@ -37,8 +33,6 @@ export class CreateProductDto { @IsString() description: string; - @IsArray() - @ValidateNested({ each: true }) - @Type(() => CreateSkuDto) - skus: CreateSkuDto[]; + @IsString() + coverUrl: string; } \ No newline at end of file diff --git a/src/product/product.entity.ts b/src/product/product.entity.ts index cf2d405..691731d 100644 --- a/src/product/product.entity.ts +++ b/src/product/product.entity.ts @@ -89,6 +89,9 @@ export class Sku { @Column() stock: number; + @Column({ length: 50 }) + remark: string; + // SKU与属性值是多对多关系 @ManyToMany(() => AttributeValue) @JoinTable() diff --git a/src/product/product.service.ts b/src/product/product.service.ts index 58d65b9..4dc236b 100644 --- a/src/product/product.service.ts +++ b/src/product/product.service.ts @@ -1,9 +1,9 @@ // productService.ts -import { BadRequestException, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, EntityManager } from 'typeorm'; -import { Attribute, Product, AttributeValue, Sku } from './product.entity'; -import { CreateProductDto } from './product.dto'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common' +import { InjectRepository } from '@nestjs/typeorm' +import { Repository, EntityManager, In } from 'typeorm' +import { Attribute, Product, AttributeValue, Sku } from './product.entity' +import { CreateProductDto, CreateSkuDto } from './product.dto' @Injectable() export class ProductService { @@ -21,102 +21,205 @@ export class ProductService { private transformAttributeValues(data) { // 创建一个新的对象来存储转换后的数据 - const transformedData = { ...data }; - + const transformedData = { ...data } + // 针对每个SKU进行转换 - transformedData.skus = data.skus.map(sku => ({ + transformedData.skus = data.skus.map((sku) => ({ ...sku, // 保留其他属性不变 - attributeValues: sku.attributeValues.map(attrValue => ({ + attributeValues: sku.attributeValues.map((attrValue) => ({ attributeId: attrValue.attribute.id, attributeName: attrValue.attribute.name, attributeValueId: attrValue.id, attributeValue: attrValue.value, - })) - })); - + })), + })) + // 返回转换后的数据 - return transformedData; + return transformedData + } + + /** + * 创建一个新的产品实体。 + * + * @param {CreateProductDto} productData 产品的传输对象,包含所需的创建信息。 + * @returns {Promise} Promise类型的产品实体。 + */ + async createProductEntity(productData: CreateProductDto): Promise { + const product = this.productRepository.create({ + name: productData.name, + description: productData.description, + cover: productData.coverUrl, + }) + return this.productRepository.save(product) + } + + /** + * 为指定的产品创建一个SKU。 + * + * @param {number} productId 产品的唯一标识符。 + * @param {CreateSkuDto} skuData SKU的传输对象,包含价格和库存等信息。 + * @returns {Promise} Promise类型的SKU实体。 + * @throws {NotFoundException} 如果没有找到指定的产品,抛出异常。 + */ + async createSku(productId: number, skuData: CreateSkuDto): Promise { + const product = await this.productRepository.findOneBy({ id: productId }) + if (!product) { + throw new NotFoundException(`Product with ID ${productId} not found`) + } + const sku = this.skuRepository.create({ + product: product, + price: skuData.price, + stock: skuData.stock, + }) + return this.skuRepository.save(sku) } - async createProduct(productData: CreateProductDto): Promise { - // 使用事务处理创建产品和SKU - const product = await this.entityManager.transaction(async (manager) => { - // 创建新产品 - const newProduct = manager.create(Product, { - name: productData.name, - description: productData.description, - }); - await manager.save(newProduct); - - // 遍历SKU数据 - for (const skuData of productData.skus) { - // 创建SKU - const sku = manager.create(Sku, { - product: newProduct, - price: skuData.price, - stock: skuData.stock, - }); - await manager.save(sku); - - // 遍历属性值数据 - for (const attributeValueData of skuData.attributeValues) { - // 确认属性是否存在 - let attribute = await manager.findOneBy(Attribute, { - id: attributeValueData.attributeId, - }); - if (!attribute) { - if (!attributeValueData.attributeName) { - throw new BadRequestException('attributeName is required'); - } - const sameNameAttribute = await manager.findOneBy(Attribute, { - name: attributeValueData.attributeName, - product: newProduct, - }); - if (sameNameAttribute) { - // 如果同名属性存在,使用同名属性 - attribute = sameNameAttribute; - } else { - // 如果属性不存在,创建属性 - attribute = manager.create(Attribute, { - name: attributeValueData.attributeName, - }); - await manager.save(attribute); - } - } - - // 创建属性值 - const attributeValue = manager.create(AttributeValue, { - value: attributeValueData.value, - attribute: attribute, - }); - await manager.save(attributeValue); - - // 关联属性值到SKU - sku.attributeValues = [ - ...(sku.attributeValues || []), - attributeValue, - ]; - } - - // 保存SKU的属性值关联 - await manager.save(sku); - } - - // 返回新创建的产品 - return newProduct; - }); - - // 获取并返回完整的产品数据 - return this.productRepository.findOne({ - where: { id: product.id }, // 确保这里使用的是正确的属性来定位产品 - relations: [ - 'skus', - 'skus.attributeValues', - 'skus.attributeValues.attribute', - ], - }); + /** + * 为指定的产品创建一个属性。 + * + * @param {number} productId 产品的唯一标识符。 + * @param {string} attributeName 属性的名称。 + * @returns {Promise} Promise类型的属性实体。 + * @throws {NotFoundException} 如果没有找到指定的产品,抛出异常。 + */ + async createAttribute(productId: number, attributeName: string): Promise { + const product = await this.productRepository.findOneBy({ id: productId }) + if (!product) { + throw new NotFoundException(`Product with ID ${productId} not found`) + } + const attribute = this.attributeRepository.create({ + name: attributeName, + product: product, + }) + return this.attributeRepository.save(attribute) + } + + /** + * 创建一个属性值。 + * + * @param {number} attributeId 属性的唯一标识符。 + * @param {string} value 属性值的内容。 + * @returns {Promise} Promise类型的属性值实体。 + * @throws {NotFoundException} 如果没有找到指定的属性,抛出异常。 + */ + async createAttributeValue(attributeId: number, value: string): Promise { + const attribute = await this.attributeRepository.findOneBy({ id: attributeId }) + if (!attribute) { + throw new NotFoundException(`Attribute with ID ${attributeId} not found`) + } + const attributeValue = this.attributeValueRepository.create({ + value: value, + attribute: attribute, + }) + return this.attributeValueRepository.save(attributeValue) + } + + /** + * 将多个属性值添加到指定的SKU。 + * + * @param {number} skuId SKU的唯一标识符。 + * @param {number[]} attributeValueIds 要添加到SKU的属性值ID数组。 + * @returns {Promise} Promise类型的更新后的SKU实体。 + * @throws {NotFoundException} 如果没有找到SKU或任何属性值,抛出异常。 + */ + async addAttributeValuesToSku(skuId: number, attributeValueIds: number[]): Promise { + const sku = await this.skuRepository.findOne({ + where: { id: skuId }, + relations: ['attributeValues'], + }) + if (!sku) { + throw new NotFoundException(`未找到ID为 ${skuId} 的SKU`) + } + + const attributeValues = await this.attributeValueRepository.findBy({ + id: In(attributeValueIds), + }) + + // 检查是否所有 attributeValueIds 都被找到了 + if (attributeValues.length !== attributeValueIds.length) { + // 这里可以提供更多关于哪些ID未找到的细节 + throw new NotFoundException(`某些属性值未找到`) + } + + // 将新的属性值添加到现有的属性值数组中 + sku.attributeValues = [...(sku.attributeValues || []), ...attributeValues] + + // 保存更新后的SKU实体 + return this.skuRepository.save(sku) } + // async createProduct(productData: CreateProductDto): Promise { + // // 使用事务处理创建产品和SKU + // const product = await this.entityManager.transaction(async (manager) => { + // // 创建新产品 + // const newProduct = manager.create(Product, { + // name: productData.name, + // description: productData.description, + // }) + // await manager.save(newProduct) + + // // 遍历SKU数据 + // for (const skuData of productData.skus) { + // // 创建SKU + // const sku = manager.create(Sku, { + // product: newProduct, + // price: skuData.price, + // stock: skuData.stock, + // }) + // await manager.save(sku) + + // // 遍历属性值数据 + // for (const attributeValueData of skuData.attributeValues) { + // // 确认属性是否存在 + // let attribute = await manager.findOneBy(Attribute, { + // id: attributeValueData.attributeId, + // }) + // if (!attribute) { + // if (!attributeValueData.attributeName) { + // throw new BadRequestException('attributeName is required') + // } + // const sameNameAttribute = await manager.findOneBy(Attribute, { + // name: attributeValueData.attributeName, + // product: newProduct, + // }) + // if (sameNameAttribute) { + // // 如果同名属性存在,使用同名属性 + // attribute = sameNameAttribute + // } else { + // // 如果属性不存在,创建属性 + // attribute = manager.create(Attribute, { + // name: attributeValueData.attributeName, + // }) + // await manager.save(attribute) + // } + // } + + // // 创建属性值 + // const attributeValue = manager.create(AttributeValue, { + // value: attributeValueData.value, + // attribute: attribute, + // }) + // await manager.save(attributeValue) + + // // 关联属性值到SKU + // sku.attributeValues = [...(sku.attributeValues || []), attributeValue] + // } + + // // 保存SKU的属性值关联 + // await manager.save(sku) + // } + + // // 返回新创建的产品 + // return newProduct + // }) + + // // 获取并返回完整的产品数据 + // return this.productRepository.findOne({ + // where: { id: product.id }, // 确保这里使用的是正确的属性来定位产品 + // relations: ['skus', 'skus.attributeValues', 'skus.attributeValues.attribute'], + // }) + // } + async getProductsList( page: number = 1, // 默认为第一页 pageSize: number = 10, // 默认每页10条记录 @@ -124,24 +227,19 @@ export class ProductService { const [result, total] = await this.productRepository.findAndCount({ take: pageSize, skip: (page - 1) * pageSize, - }); + }) return { data: result, count: total, totalPages: Math.ceil(total / pageSize), - }; + } } async getProductDetailById(productId: number): Promise { const data = await this.productRepository.findOne({ where: { id: productId }, // 商品id - relations: [ - 'skus', - 'skus.attributeValues', - 'skus.attributeValues.attribute', - 'productImages' - ], - }); + relations: ['skus', 'skus.attributeValues', 'skus.attributeValues.attribute', 'productImages'], + }) return this.transformAttributeValues(data) } diff --git a/src/product/products.service.spec.ts b/src/product/products.service.spec.ts new file mode 100644 index 0000000..ba25363 --- /dev/null +++ b/src/product/products.service.spec.ts @@ -0,0 +1,225 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { ProductService } from './product.service' +import { Repository } from 'typeorm' +import { Attribute, AttributeValue, Product, Sku } from './product.entity' +import { getRepositoryToken } from '@nestjs/typeorm' +import { NotFoundException } from '@nestjs/common' + +describe('ProductService', () => { + let service: ProductService + let productRepository: Repository + let skuRepository: Repository + let attributeRepository: Repository + let attributeValueRepository: Repository + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ProductService, + { + provide: getRepositoryToken(Product), + useValue: { + findOneBy: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }, + }, + { + provide: getRepositoryToken(Sku), + useValue: { + create: jest.fn(), + save: jest.fn(), + }, + }, + { + provide: getRepositoryToken(Attribute), + useValue: { + findOneBy: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }, + }, + { + provide: getRepositoryToken(AttributeValue), + useValue: { + create: jest.fn(), + save: jest.fn(), + }, + }, + ], + }).compile() + + // 获取服务和仓库实例 + service = module.get(ProductService) + productRepository = module.get>(getRepositoryToken(Product)) + skuRepository = module.get>(getRepositoryToken(Sku)) + attributeRepository = module.get>(getRepositoryToken(Attribute)) + attributeValueRepository = module.get>(getRepositoryToken(AttributeValue)) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) + + describe('createProductEntity', () => { + it('should create a new product entity', async () => { + const productData = { + name: 'Test Product', + description: 'This is a test product', + coverUrl: 'http://example.com/cover.jpg', + } + const expectedProduct = new Product() + jest.spyOn(productRepository, 'create').mockReturnValue(expectedProduct) + jest.spyOn(productRepository, 'save').mockResolvedValue(expectedProduct) + + const result = await service.createProductEntity(productData) + + expect(productRepository.create).toHaveBeenCalledWith(productData) + expect(productRepository.save).toHaveBeenCalledWith(expectedProduct) + expect(result).toEqual(expectedProduct) + }) + }) + + describe('createSku', () => { + it('should create a SKU for the product', async () => { + const productId = 1 + const skuData = { price: 100, stock: 20 } + const product = new Product() + const sku = new Sku() + + jest.spyOn(productRepository, 'findOneBy').mockResolvedValue(product) + jest.spyOn(skuRepository, 'create').mockReturnValue(sku) + jest.spyOn(skuRepository, 'save').mockResolvedValue(sku) + + const result = await service.createSku(productId, skuData) + + expect(productRepository.findOneBy).toHaveBeenCalledWith({ id: productId }) + expect(skuRepository.create).toHaveBeenCalledWith({ product: product, ...skuData }) + expect(skuRepository.save).toHaveBeenCalledWith(sku) + expect(result).toEqual(sku) + }) + + it('should throw NotFoundException if product is not found', async () => { + const productId = 1 + const skuData = { price: 100, stock: 20 } + + jest.spyOn(productRepository, 'findOneBy').mockResolvedValue(null) + + await expect(service.createSku(productId, skuData)).rejects.toThrow(NotFoundException) + }) + }) + + describe('createAttribute', () => { + it('should create an attribute for the product', async () => { + const productId = 1 + const attributeName = 'Size' + const product = new Product() + const attribute = new Attribute() + + jest.spyOn(productRepository, 'findOneBy').mockResolvedValue(product) + jest.spyOn(attributeRepository, 'create').mockReturnValue(attribute) + jest.spyOn(attributeRepository, 'save').mockResolvedValue(attribute) + + const result = await service.createAttribute(productId, attributeName) + + expect(productRepository.findOneBy).toHaveBeenCalledWith({ id: productId }) + expect(attributeRepository.create).toHaveBeenCalledWith({ name: attributeName, product: product }) + expect(attributeRepository.save).toHaveBeenCalledWith(attribute) + expect(result).toEqual(attribute) + }) + + it('should throw NotFoundException if product is not found', async () => { + const productId = 1 + const attributeName = 'Size' + + jest.spyOn(productRepository, 'findOneBy').mockResolvedValue(null) + + await expect(service.createAttribute(productId, attributeName)).rejects.toThrow(NotFoundException) + }) + }) + + describe('createAttributeValue', () => { + it('should create an attribute value', async () => { + const attributeValue = new AttributeValue() + const attributeValueData = { value: 'Large' } + const attributeId = 1 + const attribute = new Attribute() + + jest.spyOn(attributeRepository, 'findOneBy').mockResolvedValue(attribute) + jest.spyOn(attributeValueRepository, 'create').mockReturnValue(attributeValue) + jest.spyOn(attributeValueRepository, 'save').mockResolvedValue(attributeValue) + + const result = await service.createAttributeValue(attributeId, attributeValueData.value) + + expect(attributeRepository.findOneBy).toHaveBeenCalledWith({ id: attributeId }) + expect(attributeValueRepository.create).toHaveBeenCalledWith({ + value: attributeValueData.value, + attribute: attribute, + }) + expect(attributeValueRepository.save).toHaveBeenCalledWith(attributeValue) + expect(result).toEqual(attributeValue) + }) + + it('should throw NotFoundException if attribute is not found', async () => { + const attributeId = 1 + const value = 'Large' + + jest.spyOn(attributeRepository, 'findOneBy').mockResolvedValue(undefined) + + await expect(service.createAttributeValue(attributeId, value)).rejects.toThrow(NotFoundException) + }) + }) + + describe('Product Creation Workflow', () => { + it('should create a product, SKU, attribute, attribute value, and link them together', async () => { + // Step 1: Create a new product + const productData = { + name: 'Test Product', + description: 'This is a test product', + coverUrl: 'http://example.com/cover.jpg', + } + const newProduct = new Product() + jest.spyOn(productRepository, 'create').mockReturnValue(newProduct) + jest.spyOn(productRepository, 'save').mockResolvedValue(newProduct) + const createdProduct = await service.createProductEntity(productData) + + // Step 2: Create a SKU for the product + const skuData = { price: 100, stock: 20 } + const newSku = new Sku() + jest.spyOn(skuRepository, 'create').mockReturnValue(newSku) + jest.spyOn(skuRepository, 'save').mockResolvedValue(newSku) + const createdSku = await service.createSku(createdProduct.id, skuData) + + // Step 3: Create an attribute for the product + const attributeName = 'Size' + const newAttribute = new Attribute() + jest.spyOn(attributeRepository, 'create').mockReturnValue(newAttribute) + jest.spyOn(attributeRepository, 'save').mockResolvedValue(newAttribute) + const createdAttribute = await service.createAttribute(createdProduct.id, attributeName) + + // Step 4: Create an attribute value for the attribute + const attributeValueData = 'Large' + const newAttributeValue = new AttributeValue() + jest.spyOn(attributeValueRepository, 'create').mockReturnValue(newAttributeValue) + jest.spyOn(attributeValueRepository, 'save').mockResolvedValue(newAttributeValue) + const createdAttributeValue = await service.createAttributeValue(createdAttribute.id, attributeValueData) + + // Step 5: Add the attribute value to the SKU + jest.spyOn(skuRepository, 'findOne').mockResolvedValue(createdSku) + jest.spyOn(attributeValueRepository, 'findBy').mockResolvedValue([createdAttributeValue]) + jest.spyOn(skuRepository, 'save').mockResolvedValue({ + ...createdSku, + attributeValues: [createdAttributeValue], + }) + + const updatedSku = await service.addAttributeValuesToSku(createdSku.id, [createdAttributeValue.id]) + + // Final assertions to verify the workflow + expect(createdProduct).toEqual(newProduct) + expect(createdSku).toEqual(newSku) + expect(createdAttribute).toEqual(newAttribute) + expect(createdAttributeValue).toEqual(newAttributeValue) + expect(updatedSku.attributeValues).toContainEqual(createdAttributeValue) + }) + }) +}) diff --git a/test/products/products.service.spec.ts b/test/products/products.service.spec.ts deleted file mode 100644 index 282eae7..0000000 --- a/test/products/products.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ProductsService } from '../../src/product/product.service'; - -describe('ProductsService', () => { - let service: ProductsService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ProductsService], - }).compile(); - - service = module.get(ProductsService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -});