From d7cccad8952b64f34ca3ee5b4343721e2ad07ca4 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Thu, 4 Dec 2025 10:05:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E9=A1=B9=E5=9B=BE=E7=89=87=E5=92=8C=E7=AE=80=E7=A7=B0=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BA=A7=E5=93=81=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 扩展字典项实体和DTO,新增image和shortName字段 重构产品导入逻辑,支持直接处理上传文件 启用默认错误过滤器并配置上传临时目录 合并产品组件功能到主DTO中,简化API设计 优化CSV导入错误处理和异步解析 --- .gitignore | 1 + src/config/config.default.ts | 3 + src/configuration.ts | 6 +- src/controller/product.controller.ts | 50 ++----- src/dto/dict.dto.ts | 12 ++ src/dto/product.dto.ts | 34 ++--- src/entity/dict_item.entity.ts | 6 + src/service/dict.service.ts | 8 +- src/service/product.service.ts | 210 +++++++++++++++------------ 9 files changed, 183 insertions(+), 147 deletions(-) diff --git a/.gitignore b/.gitignore index b24fb71..9f2715f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ yarn.lock container scripts ai +tmp_uploads/ diff --git a/src/config/config.default.ts b/src/config/config.default.ts index b00d992..8a1ffc2 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -1,4 +1,5 @@ import { MidwayConfig } from '@midwayjs/core'; +import { join } from 'path'; import { Product } from '../entity/product.entity'; import { WpProduct } from '../entity/wp_product.entity'; import { Variation } from '../entity/variation.entity'; @@ -146,5 +147,7 @@ export default { mode: 'file', fileSize: '10mb', // 最大支持的文件大小,默认为 10mb whitelist: ['.csv'], // 支持的文件后缀 + tmpdir: join(__dirname, '../../tmp_uploads'), + cleanTimeout: 5 * 60 * 1000, }, } as MidwayConfig; diff --git a/src/configuration.ts b/src/configuration.ts index 09019e4..ec6a64e 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -9,8 +9,8 @@ import * as validate from '@midwayjs/validate'; import * as info from '@midwayjs/info'; import * as orm from '@midwayjs/typeorm'; import { join } from 'path'; -// import { DefaultErrorFilter } from './filter/default.filter'; -// import { NotFoundFilter } from './filter/notfound.filter'; +import { DefaultErrorFilter } from './filter/default.filter'; +import { NotFoundFilter } from './filter/notfound.filter'; import { ReportMiddleware } from './middleware/report.middleware'; import * as swagger from '@midwayjs/swagger'; import * as crossDomain from '@midwayjs/cross-domain'; @@ -55,7 +55,7 @@ export class MainConfiguration { // add middleware this.app.useMiddleware([ReportMiddleware, AuthMiddleware]); // add filter - // this.app.useFilter([NotFoundFilter, DefaultErrorFilter]); + this.app.useFilter([NotFoundFilter, DefaultErrorFilter]); this.decoratorService.registerParameterHandler( USER_KEY, diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts index 64f1156..44c6066 100644 --- a/src/controller/product.controller.ts +++ b/src/controller/product.controller.ts @@ -9,10 +9,9 @@ import { Query, Controller, } from '@midwayjs/core'; -import * as fs from 'fs'; import { ProductService } from '../service/product.service'; import { errorResponse, successResponse } from '../utils/response.util'; -import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO, BatchUpdateProductDTO } from '../dto/product.dto'; +import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, BatchUpdateProductDTO } from '../dto/product.dto'; import { ApiOkResponse } from '@midwayjs/swagger'; import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto'; import { ContentType, Files } from '@midwayjs/core'; @@ -83,7 +82,7 @@ export class ProductController { @Post('/') async createProduct(@Body() productData: CreateProductDTO) { try { - const data = this.productService.createProduct(productData); + const data = await this.productService.createProduct(productData); return successResponse(data); } catch (error) { return errorResponse(error?.message || error); @@ -115,19 +114,9 @@ export class ProductController { try { // 条件判断:确保存在文件 const file = files?.[0]; - if (!file?.data) return errorResponse('未接收到上传文件'); + if (!file) return errorResponse('未接收到上传文件'); - // midway/upload file 模式下,data 是临时文件路径 - let buffer = file.data; - if (typeof file.data === 'string') { - try { - buffer = fs.readFileSync(file.data); - } catch (err) { - return errorResponse('读取上传文件失败'); - } - } - - const result = await this.productService.importProductsCSV(buffer); + const result = await this.productService.importProductsCSV(file); return successResponse(result); } catch (error) { return errorResponse(error?.message || error); @@ -138,7 +127,7 @@ export class ProductController { @Put('/:id') async updateProduct(@Param('id') id: number, @Body() productData: UpdateProductDTO) { try { - const data = this.productService.updateProduct(id, productData); + const data = await this.productService.updateProduct(id, productData); return successResponse(data); } catch (error) { return errorResponse(error?.message || error); @@ -160,7 +149,7 @@ export class ProductController { @Put('updateNameCn/:id/:nameCn') async updatenameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) { try { - const data = this.productService.updatenameCn(id, nameCn); + const data = await this.productService.updatenameCn(id, nameCn); return successResponse(data); } catch (error) { return errorResponse(error?.message || error); @@ -190,17 +179,6 @@ export class ProductController { } } - // 设置产品的库存组成(覆盖式) - @ApiOkResponse() - @Post('/:id/components') - async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) { - try { - const data = await this.productService.setProductComponents(id, body?.components || []); - return successResponse(data); - } catch (error) { - return errorResponse(error?.message || error); - } - } // 根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存) @ApiOkResponse() @@ -339,7 +317,7 @@ export class ProductController { @ApiOkResponse() @Post('/brand') - async compatCreateBrand(@Body() body: { title: string; name: string }) { + async compatCreateBrand(@Body() body: { title: string; name: string; image?: string; shortName?: string }) { try { const has = await this.productService.hasAttribute('brand', body.name); // 唯一性校验 if (has) return errorResponse('品牌已存在'); @@ -352,7 +330,7 @@ export class ProductController { @ApiOkResponse() @Put('/brand/:id') - async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string }) { + async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) { try { if (body?.name) { const has = await this.productService.hasAttribute('brand', body.name, id); // 唯一性校验(排除自身) @@ -401,7 +379,7 @@ export class ProductController { @ApiOkResponse() @Post('/flavors') - async compatCreateFlavors(@Body() body: { title: string; name: string }) { + async compatCreateFlavors(@Body() body: { title: string; name: string; image?: string; shortName?: string }) { try { const has = await this.productService.hasAttribute('flavor', body.name); if (has) return errorResponse('口味已存在'); @@ -414,7 +392,7 @@ export class ProductController { @ApiOkResponse() @Put('/flavors/:id') - async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string }) { + async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) { try { if (body?.name) { const has = await this.productService.hasAttribute('flavor', body.name, id); @@ -463,7 +441,7 @@ export class ProductController { @ApiOkResponse() @Post('/strength') - async compatCreateStrength(@Body() body: { title: string; name: string }) { + async compatCreateStrength(@Body() body: { title: string; name: string; image?: string; shortName?: string }) { try { const has = await this.productService.hasAttribute('strength', body.name); if (has) return errorResponse('规格已存在'); @@ -476,7 +454,7 @@ export class ProductController { @ApiOkResponse() @Put('/strength/:id') - async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string }) { + async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) { try { if (body?.name) { const has = await this.productService.hasAttribute('strength', body.name, id); @@ -525,7 +503,7 @@ export class ProductController { @ApiOkResponse() @Post('/size') - async compatCreateSize(@Body() body: { title: string; name: string }) { + async compatCreateSize(@Body() body: { title: string; name: string; image?: string; shortName?: string }) { try { const has = await this.productService.hasAttribute('size', body.name); if (has) return errorResponse('尺寸已存在'); @@ -538,7 +516,7 @@ export class ProductController { @ApiOkResponse() @Put('/size/:id') - async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string }) { + async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) { try { if (body?.name) { const has = await this.productService.hasAttribute('size', body.name, id); diff --git a/src/dto/dict.dto.ts b/src/dto/dict.dto.ts index a6c8f7e..6fae6aa 100644 --- a/src/dto/dict.dto.ts +++ b/src/dto/dict.dto.ts @@ -29,6 +29,12 @@ export class CreateDictItemDTO { @Rule(RuleType.string().allow('').allow(null)) titleCN?: string; // 字典项中文标题 (可选) + @Rule(RuleType.string().allow('').allow(null)) + image?: string; // 图片 (可选) + + @Rule(RuleType.string().allow('').allow(null)) + shortName?: string; // 简称 (可选) + @Rule(RuleType.number().required()) dictId: number; // 所属字典的ID } @@ -47,4 +53,10 @@ export class UpdateDictItemDTO { @Rule(RuleType.string().allow(null)) value?: string; // 字典项值 (可选) + @Rule(RuleType.string().allow('').allow(null)) + image?: string; // 图片 (可选) + + @Rule(RuleType.string().allow('').allow(null)) + shortName?: string; // 简称 (可选) + } diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts index c026899..d7514c9 100644 --- a/src/dto/product.dto.ts +++ b/src/dto/product.dto.ts @@ -139,6 +139,23 @@ export class UpdateProductDTO { @ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false }) @Rule(RuleType.string().valid('single', 'bundle')) type?: string; + + // 仅当 type 为 'bundle' 时,才需要提供 components + @ApiProperty({ description: '产品组成', type: 'array', required: false }) + @Rule( + RuleType.array() + .items( + RuleType.object({ + sku: RuleType.string().required(), + quantity: RuleType.number().required(), + }) + ) + .when('type', { + is: 'bundle', + then: RuleType.array().optional(), + }) + ) + components?: { sku: string; quantity: number }[]; } @@ -233,20 +250,3 @@ export class QueryProductDTO { sortOrder?: string; } -/** - * DTO 用于设置产品组成 - */ -export class SetProductComponentsDTO { - @ApiProperty({ description: '产品组成', type: 'array', required: true }) - @Rule( - RuleType.array() - .items( - RuleType.object({ - sku: RuleType.string().required(), - quantity: RuleType.number().required(), - }) - ) - .required() - ) - components: { sku: string; quantity: number }[]; -} diff --git a/src/entity/dict_item.entity.ts b/src/entity/dict_item.entity.ts index 7546bac..ffa6896 100644 --- a/src/entity/dict_item.entity.ts +++ b/src/entity/dict_item.entity.ts @@ -38,6 +38,12 @@ export class DictItem { @Column({ nullable: true, comment: '字典项值' }) value?: string; + @Column({ nullable: true, comment: '图片' }) + image: string; + + @Column({ nullable: true, comment: '简称' }) + shortName: string; + // 排序 @Column({ default: 0, comment: '排序' }) sort: number; diff --git a/src/service/dict.service.ts b/src/service/dict.service.ts index 6bce09a..12c62ed 100644 --- a/src/service/dict.service.ts +++ b/src/service/dict.service.ts @@ -61,7 +61,7 @@ export class DictService { // 生成并返回字典项的XLSX模板 getDictItemXLSXTemplate() { - const headers = ['name', 'title', 'titleCN', 'value', 'sort']; + const headers = ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName']; const ws = xlsx.utils.aoa_to_sheet([headers]); const wb = xlsx.utils.book_new(); xlsx.utils.book_append_sheet(wb, ws, 'DictItems'); @@ -78,7 +78,7 @@ export class DictService { const wsname = wb.SheetNames[0]; const ws = wb.Sheets[wsname]; // 支持titleCN字段的导入 - const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title', 'titleCN', 'value', 'sort'] }).slice(1); + const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName'] }).slice(1); const items = data.map((row: any) => { const item = new DictItem(); @@ -86,6 +86,8 @@ export class DictService { item.title = row.title; item.titleCN = row.titleCN; // 保存中文名称 item.value = row.value; + item.image = row.image; + item.shortName = row.shortName; item.sort = row.sort || 0; item.dict = dict; return item; @@ -168,6 +170,8 @@ export class DictService { item.name = this.formatName(createDictItemDTO.name); item.title = createDictItemDTO.title; item.titleCN = createDictItemDTO.titleCN; // 保存中文名称 + item.image = createDictItemDTO.image; + item.shortName = createDictItemDTO.shortName; item.dict = dict; return this.dictItemModel.save(item); } diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 60a0a61..36741d3 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -1,8 +1,11 @@ import { Inject, Provide } from '@midwayjs/core'; +import * as fs from 'fs'; import { In, Like, Not, Repository } from 'typeorm'; import { Product } from '../entity/product.entity'; import { paginate } from '../utils/paginate.util'; import { PaginationParams } from '../interface'; +import { parse } from 'csv-parse'; + import { CreateProductDTO, UpdateProductDTO, @@ -87,7 +90,7 @@ export class ProductService { where: { id: categoryId }, relations: ['attributes', 'attributes.attributeDict', 'attributes.attributeDict.items'], }); - + if (!category) { return []; } @@ -133,7 +136,7 @@ export class ProductService { if (!category) { throw new Error('分类不存在'); } - + const dict = await this.dictModel.findOne({ where: { id: payload.dictId } }); if (!dict) { throw new Error('字典不存在'); @@ -351,20 +354,20 @@ export class ProductService { if (!attributes && categoryId) { // 继续执行,下面会处理 categoryId } else { - throw new Error('属性列表不能为空'); + 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} 不存在`); + categoryItem = await this.categoryModel.findOne({ where: { id: categoryId } }); + if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`); } for (const attr of safeAttributes) { @@ -412,18 +415,18 @@ export class ProductService { ); }); const isExist = await qb.getOne(); - if (isExist) throw new Error('产品已存在'); + if (isExist) throw new Error('相同产品属性的产品已存在'); // 创建新产品实例(绑定属性与基础字段) const product = new Product(); - + // 使用 merge 填充基础字段,排除特殊处理字段 - const { attributes: _attrs, categoryId: _cid, sku: _sku, ...simpleFields } = createProductDTO; + const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = createProductDTO; this.productModel.merge(product, simpleFields); product.attributes = resolvedAttributes; if (categoryItem) { - product.category = categoryItem; + product.category = categoryItem; } // 确保默认类型 if (!product.type) product.type = 'single'; @@ -436,7 +439,7 @@ export class ProductService { 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', { + product.sku = await this.templateService.render('product.sku', { brand: attributeMap['brand'] || '', flavor: attributeMap['flavor'] || '', strength: attributeMap['strength'] || '', @@ -444,7 +447,16 @@ export class ProductService { }); } - return await this.productModel.save(product); + const savedProduct = await this.productModel.save(product); + + // 保存组件信息 + if (createProductDTO.components && createProductDTO.components.length > 0) { + await this.setProductComponents(savedProduct.id, createProductDTO.components); + // 重新加载带组件的产品 + return await this.productModel.findOne({ where: { id: savedProduct.id }, relations: ['attributes', 'attributes.dict', 'category', 'components'] }); + } + + return savedProduct; } async updateProduct( @@ -458,19 +470,19 @@ export class ProductService { } // 使用 merge 更新基础字段,排除特殊处理字段 - const { attributes: _attrs, categoryId: _cid, sku: _sku, ...simpleFields } = updateProductDTO; + const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = updateProductDTO; this.productModel.merge(product, simpleFields); // 处理分类更新 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.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; + } } // 处理 SKU 更新 @@ -504,7 +516,7 @@ export class ProductService { } continue; } - + let item: DictItem | null = null; if (attr.id) { // 当提供 id 时直接查询字典项,不强制要求 dictName @@ -533,6 +545,13 @@ export class ProductService { // 保存更新后的产品 const saved = await this.productModel.save(product); + + // 处理组件更新 + if (updateProductDTO.components !== undefined) { + // 如果 components 为空数组,则删除所有组件? setProductComponents 会处理 + await this.setProductComponents(saved.id, updateProductDTO.components); + } + return saved; } @@ -546,34 +565,34 @@ export class ProductService { // 检查 updateData 中是否有复杂字段 (attributes, categoryId, type, sku) // 如果包含复杂字段,需要复用 updateProduct 的逻辑 - const hasComplexFields = - updateData.attributes !== undefined || - updateData.categoryId !== undefined || - updateData.type !== undefined || - updateData.sku !== undefined; - + const hasComplexFields = + updateData.attributes !== undefined || + updateData.categoryId !== undefined || + updateData.type !== undefined || + updateData.sku !== undefined; + if (hasComplexFields) { - // 循环调用 updateProduct - for (const id of ids) { - const updateDTO = new UpdateProductDTO(); - // 复制属性 - Object.assign(updateDTO, updateData); - await this.updateProduct(id, updateDTO); - } + // 循环调用 updateProduct + for (const id of ids) { + const updateDTO = new UpdateProductDTO(); + // 复制属性 + Object.assign(updateDTO, updateData); + await this.updateProduct(id, updateDTO); + } } else { - // 简单字段,直接批量更新以提高性能 - // UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice - - const simpleUpdate: any = {}; - if (updateData.name !== undefined) simpleUpdate.name = updateData.name; - if (updateData.nameCn !== undefined) simpleUpdate.nameCn = updateData.nameCn; - if (updateData.description !== undefined) simpleUpdate.description = updateData.description; - if (updateData.price !== undefined) simpleUpdate.price = updateData.price; - if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice; - - if (Object.keys(simpleUpdate).length > 0) { - await this.productModel.update({ id: In(ids) }, simpleUpdate); - } + // 简单字段,直接批量更新以提高性能 + // UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice + + const simpleUpdate: any = {}; + if (updateData.name !== undefined) simpleUpdate.name = updateData.name; + if (updateData.nameCn !== undefined) simpleUpdate.nameCn = updateData.nameCn; + if (updateData.description !== undefined) simpleUpdate.description = updateData.description; + if (updateData.price !== undefined) simpleUpdate.price = updateData.price; + if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice; + + if (Object.keys(simpleUpdate).length > 0) { + await this.productModel.update({ id: In(ids) }, simpleUpdate); + } } return true; @@ -706,7 +725,7 @@ export class ProductService { } // 重复定义的 getProductList 已合并到前面的实现(移除重复) - + async updatenameCn(id: number, nameCn: string): Promise { // 确认产品是否存在 const product = await this.productModel.findOneBy({ id }); @@ -1113,7 +1132,7 @@ export class ProductService { // 通用属性:创建字典项 async createAttribute( dictName: string, - payload: { title: string; name: string } + payload: { title: string; name: string; image?: string; shortName?: string } ): Promise { const dict = await this.dictModel.findOne({ where: { name: dictName } }); if (!dict) throw new Error(`字典 ${dictName} 不存在`); @@ -1125,6 +1144,8 @@ export class ProductService { const item = new DictItem(); item.title = payload.title; item.name = payload.name; + item.image = payload.image; + item.shortName = payload.shortName; item.dict = dict; return await this.dictItemModel.save(item); } @@ -1132,12 +1153,14 @@ export class ProductService { // 通用属性:更新字典项 async updateAttribute( id: number, - payload: { title?: string; name?: string } + payload: { title?: string; name?: string; image?: string; shortName?: string } ): Promise { 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; + if (payload.image !== undefined) item.image = payload.image; + if (payload.shortName !== undefined) item.shortName = payload.shortName; return await this.dictItemModel.save(item); } @@ -1219,7 +1242,7 @@ export class ProductService { // 解析属性字段(分号分隔多值) const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []); - + // 将属性解析为 DTO 输入 const attributes: any[] = []; @@ -1276,7 +1299,7 @@ export class ProductService { dto.nameCn = data.nameCn; dto.description = data.description; dto.sku = data.sku; - + // 数值类型转换 if (data.price !== undefined) dto.price = Number(data.price); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); @@ -1286,38 +1309,39 @@ export class ProductService { // 默认值和特殊处理 dto.attributes = Array.isArray(data.attributes) ? data.attributes : []; - + // 如果有组件信息,透传 - dto.type = data.type || data.components?.length? 'bundle':'single' + dto.type = data.type || data.components?.length ? 'bundle' : 'single' if (data.components) dto.components = data.components; - + return dto; } // 准备更新产品的 DTO, 处理类型转换 prepareUpdateProductDTO(data: any): UpdateProductDTO { const dto = new UpdateProductDTO(); - + if (data.name !== undefined) dto.name = data.name; if (data.nameCn !== undefined) dto.nameCn = data.nameCn; if (data.description !== undefined) dto.description = data.description; if (data.sku !== undefined) dto.sku = data.sku; - + if (data.price !== undefined) dto.price = Number(data.price); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId); - + if (data.type !== undefined) dto.type = data.type; if (data.attributes !== undefined) dto.attributes = data.attributes; - + if (data.components !== undefined) dto.components = data.components; + return dto; } // 将单个产品转换为 CSV 行数组 transformProductToCsvRow( - p: Product, - sortedDictNames: string[], + p: Product, + sortedDictNames: string[], maxComponentCount: number ): string[] { // CSV 字段转义,处理逗号与双引号 @@ -1365,7 +1389,7 @@ export class ProductService { rowData.push(''); } } - + return rowData; } @@ -1425,7 +1449,7 @@ export class ProductService { const rows: string[] = []; rows.push(allHeaders.join(',')); - + for (const p of products) { const rowData = this.transformProductToCsvRow(p, sortedDictNames, maxComponentCount); rows.push(rowData.join(',')); @@ -1435,20 +1459,40 @@ export class ProductService { } // 从 CSV 导入产品;存在则更新,不存在则创建 - async importProductsCSV(buffer: Buffer): Promise<{ created: number; updated: number; errors: string[] }> { + async importProductsCSV(file: any): Promise<{ created: number; updated: number; errors: string[] }> { + let buffer: Buffer; + if (Buffer.isBuffer(file)) { + buffer = file; + } else if (file?.data) { + if (typeof file.data === 'string') { + buffer = fs.readFileSync(file.data); + } else { + buffer = file.data; + } + } else { + throw new Error('无效的文件输入'); + } + // 解析 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, - bom: true, - }); + records = await new Promise((resolve, reject) => { + parse(buffer, { + columns: true, + skip_empty_lines: true, + trim: true, + bom: true, + }, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }) console.log('Parsed records count:', records.length); if (records.length > 0) { - console.log('First record keys:', Object.keys(records[0])); + console.log('First record keys:', Object.keys(records[0])); } } catch (e: any) { return { created: 0, updated: 0, errors: [`CSV 解析失败:${e?.message || e}`] }; @@ -1466,10 +1510,7 @@ export class ProductService { errors.push('缺少 SKU 的记录已跳过'); continue; } - const { sku, components } = data; - - let currentProductId: number; - let currentProductType: string = data.type || 'single'; + const { sku } = data; // 查找现有产品 const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] }); @@ -1477,25 +1518,16 @@ export class ProductService { if (!exist) { // 创建新产品 const createDTO = this.prepareCreateProductDTO(data); - const createdProduct = await this.createProduct(createDTO); - currentProductId = createdProduct.id; - currentProductType = createdProduct.type; + await this.createProduct(createDTO); created += 1; } else { // 更新产品 const updateDTO = this.prepareUpdateProductDTO(data); await this.updateProduct(exist.id, updateDTO); - currentProductId = exist.id; - currentProductType = updateDTO.type || exist.type; updated += 1; } - - // 4. 保存组件信息 - if (currentProductType !== 'single' && components && components.length > 0) { - await this.setProductComponents(currentProductId, components); - } } catch (e: any) { - errors.push(e?.message || String(e)); + errors.push(`产品${rec?.sku}导入失败:${e?.message || String(e)}`); } }