feat: 添加字典项图片和简称字段并优化产品导入功能
扩展字典项实体和DTO,新增image和shortName字段 重构产品导入逻辑,支持直接处理上传文件 启用默认错误过滤器并配置上传临时目录 合并产品组件功能到主DTO中,简化API设计 优化CSV导入错误处理和异步解析
This commit is contained in:
parent
62f9ca947a
commit
d7cccad895
|
|
@ -17,3 +17,4 @@ yarn.lock
|
|||
container
|
||||
scripts
|
||||
ai
|
||||
tmp_uploads/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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; // 简称 (可选)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Product> {
|
||||
// 确认产品是否存在
|
||||
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<DictItem> {
|
||||
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<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;
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue