From 4bb0988034d300263c4af5221fd5360cc82e00ec Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 3 Dec 2025 09:51:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BA=A7=E5=93=81):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E6=9B=B4=E6=96=B0=E4=BA=A7=E5=93=81=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E5=BC=95=E5=85=A5eta=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=BC=95=E6=93=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加批量更新产品接口,支持简单字段批量更新和复杂字段逐个更新 引入eta模板引擎依赖并更新eslint配置忽略scripts目录 --- .eslintrc.json | 2 +- .gitignore | 2 ++ package-lock.json | 13 ++++++++ package.json | 1 + src/controller/product.controller.ts | 13 +++++++- src/dto/product.dto.ts | 46 ++++++++++++++++++++++++++++ src/service/product.service.ts | 44 ++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 8d20e22..95cba7d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": "./node_modules/mwts/", - "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"], + "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings", "scripts"], "env": { "jest": true } diff --git a/.gitignore b/.gitignore index 49015ee..a90d5c3 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ scripts/replace_punctuation.js scripts/test_db_count.d.ts scripts/test_db_count.js scripts/test_db_count.ts +ai/test.html +ai/wc-product-export-2-12-2025-1764686773307.csv diff --git a/package-lock.json b/package-lock.json index 379b738..4f93b95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "class-transformer": "^0.5.1", "csv-parse": "^6.1.0", "dayjs": "^1.11.13", + "eta": "^4.4.1", "i18n-iso-countries": "^7.14.0", "mysql2": "^3.15.3", "nodemailer": "^7.0.5", @@ -2278,6 +2279,18 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/eta": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eta/-/eta-4.4.1.tgz", + "integrity": "sha512-4o6fYxhRmFmO9SJcU9PxBLYPGapvJ/Qha0ZE+Y6UE9QIUd0Wk1qaLISQ6J1bM7nOcWHhs1YmY3mfrfwkJRBTWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/bgub/eta?sponsor=1" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", diff --git a/package.json b/package.json index 46d1f1d..1270b87 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "class-transformer": "^0.5.1", "csv-parse": "^6.1.0", "dayjs": "^1.11.13", + "eta": "^4.4.1", "i18n-iso-countries": "^7.14.0", "mysql2": "^3.15.3", "nodemailer": "^7.0.5", diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts index c905ee6..64f1156 100644 --- a/src/controller/product.controller.ts +++ b/src/controller/product.controller.ts @@ -12,7 +12,7 @@ import { import * as fs from 'fs'; import { ProductService } from '../service/product.service'; import { errorResponse, successResponse } from '../utils/response.util'; -import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO } from '../dto/product.dto'; +import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO, 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'; @@ -145,6 +145,17 @@ export class ProductController { } } + @ApiOkResponse({ type: BooleanRes }) + @Put('/batch-update') + async batchUpdateProduct(@Body() batchUpdateProductDTO: BatchUpdateProductDTO) { + try { + await this.productService.batchUpdateProduct(batchUpdateProductDTO); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || error); + } + } + @ApiOkResponse({ type: ProductRes }) @Put('updateNameCn/:id/:nameCn') async updatenameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) { diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts index 38eeccd..c026899 100644 --- a/src/dto/product.dto.ts +++ b/src/dto/product.dto.ts @@ -141,6 +141,52 @@ export class UpdateProductDTO { type?: string; } + +/** + * DTO 用于批量更新产品属性 + */ +export class BatchUpdateProductDTO { + @ApiProperty({ description: '产品ID列表', type: 'array', required: true }) + @Rule(RuleType.array().items(RuleType.number()).required().min(1)) + ids: number[]; + + @ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称', required: false }) + @Rule(RuleType.string().optional()) + name?: string; + + @ApiProperty({ description: '产品中文名称', required: false }) + @Rule(RuleType.string().allow('').optional()) + nameCn?: string; + + @ApiProperty({ example: '产品描述', description: '产品描述', required: false }) + @Rule(RuleType.string().optional()) + description?: string; + + @ApiProperty({ description: '产品 SKU', required: false }) + @Rule(RuleType.string().optional()) + sku?: string; + + @ApiProperty({ description: '分类ID (DictItem ID)', required: false }) + @Rule(RuleType.number().optional()) + categoryId?: number; + + @ApiProperty({ description: '价格', example: 99.99, required: false }) + @Rule(RuleType.number().optional()) + price?: number; + + @ApiProperty({ description: '促销价格', example: 99.99, required: false }) + @Rule(RuleType.number().optional()) + promotionPrice?: number; + + @ApiProperty({ description: '属性列表', type: 'array', required: false }) + @Rule(RuleType.array().optional()) + attributes?: AttributeInputDTO[]; + + @ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false }) + @Rule(RuleType.string().valid('single', 'bundle').optional()) + type?: string; +} + /** * DTO 用于创建分类属性绑定 */ diff --git a/src/service/product.service.ts b/src/service/product.service.ts index be17eb0..60a0a61 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -6,6 +6,7 @@ import { PaginationParams } from '../interface'; import { CreateProductDTO, UpdateProductDTO, + BatchUpdateProductDTO, } from '../dto/product.dto'; import { BrandPaginatedResponse, @@ -535,6 +536,49 @@ export class ProductService { return saved; } + async batchUpdateProduct( + batchUpdateProductDTO: BatchUpdateProductDTO + ): Promise { + const { ids, ...updateData } = batchUpdateProductDTO; + if (!ids || ids.length === 0) { + throw new Error('未选择任何产品'); + } + + // 检查 updateData 中是否有复杂字段 (attributes, categoryId, type, sku) + // 如果包含复杂字段,需要复用 updateProduct 的逻辑 + 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); + } + } 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); + } + } + + return true; + } + // 获取产品的库存组成列表(表关联版本) async getProductComponents(productId: number): Promise { // 条件判断:确保产品存在