Compare commits
No commits in common. "40a445830bf156c6269535dd11d72b592d9145bb" and "62f9ca947a3889669c7c3472a3aaa653618cd116" have entirely different histories.
40a445830b
...
62f9ca947a
|
|
@ -17,4 +17,3 @@ yarn.lock
|
||||||
container
|
container
|
||||||
scripts
|
scripts
|
||||||
ai
|
ai
|
||||||
tmp_uploads/
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { MidwayConfig } from '@midwayjs/core';
|
import { MidwayConfig } from '@midwayjs/core';
|
||||||
import { join } from 'path';
|
|
||||||
import { Product } from '../entity/product.entity';
|
import { Product } from '../entity/product.entity';
|
||||||
import { WpProduct } from '../entity/wp_product.entity';
|
import { WpProduct } from '../entity/wp_product.entity';
|
||||||
import { Variation } from '../entity/variation.entity';
|
import { Variation } from '../entity/variation.entity';
|
||||||
|
|
@ -37,7 +36,6 @@ import { DictItem } from '../entity/dict_item.entity';
|
||||||
import { Template } from '../entity/template.entity';
|
import { Template } from '../entity/template.entity';
|
||||||
import { Area } from '../entity/area.entity';
|
import { Area } from '../entity/area.entity';
|
||||||
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||||
import { ProductSiteSku } from '../entity/product_site_sku.entity';
|
|
||||||
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
||||||
import { Category } from '../entity/category.entity';
|
import { Category } from '../entity/category.entity';
|
||||||
import DictSeeder from '../db/seeds/dict.seeder';
|
import DictSeeder from '../db/seeds/dict.seeder';
|
||||||
|
|
@ -52,7 +50,6 @@ export default {
|
||||||
entities: [
|
entities: [
|
||||||
Product,
|
Product,
|
||||||
ProductStockComponent,
|
ProductStockComponent,
|
||||||
ProductSiteSku,
|
|
||||||
WpProduct,
|
WpProduct,
|
||||||
Variation,
|
Variation,
|
||||||
User,
|
User,
|
||||||
|
|
@ -149,7 +146,5 @@ export default {
|
||||||
mode: 'file',
|
mode: 'file',
|
||||||
fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
|
fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
|
||||||
whitelist: ['.csv'], // 支持的文件后缀
|
whitelist: ['.csv'], // 支持的文件后缀
|
||||||
tmpdir: join(__dirname, '../../tmp_uploads'),
|
|
||||||
cleanTimeout: 5 * 60 * 1000,
|
|
||||||
},
|
},
|
||||||
} as MidwayConfig;
|
} as MidwayConfig;
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import * as validate from '@midwayjs/validate';
|
||||||
import * as info from '@midwayjs/info';
|
import * as info from '@midwayjs/info';
|
||||||
import * as orm from '@midwayjs/typeorm';
|
import * as orm from '@midwayjs/typeorm';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { DefaultErrorFilter } from './filter/default.filter';
|
// import { DefaultErrorFilter } from './filter/default.filter';
|
||||||
import { NotFoundFilter } from './filter/notfound.filter';
|
// import { NotFoundFilter } from './filter/notfound.filter';
|
||||||
import { ReportMiddleware } from './middleware/report.middleware';
|
import { ReportMiddleware } from './middleware/report.middleware';
|
||||||
import * as swagger from '@midwayjs/swagger';
|
import * as swagger from '@midwayjs/swagger';
|
||||||
import * as crossDomain from '@midwayjs/cross-domain';
|
import * as crossDomain from '@midwayjs/cross-domain';
|
||||||
|
|
@ -55,7 +55,7 @@ export class MainConfiguration {
|
||||||
// add middleware
|
// add middleware
|
||||||
this.app.useMiddleware([ReportMiddleware, AuthMiddleware]);
|
this.app.useMiddleware([ReportMiddleware, AuthMiddleware]);
|
||||||
// add filter
|
// add filter
|
||||||
this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
|
// this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
|
||||||
|
|
||||||
this.decoratorService.registerParameterHandler(
|
this.decoratorService.registerParameterHandler(
|
||||||
USER_KEY,
|
USER_KEY,
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ import {
|
||||||
Query,
|
Query,
|
||||||
Controller,
|
Controller,
|
||||||
} from '@midwayjs/core';
|
} from '@midwayjs/core';
|
||||||
|
import * as fs from 'fs';
|
||||||
import { ProductService } from '../service/product.service';
|
import { ProductService } from '../service/product.service';
|
||||||
import { errorResponse, successResponse } from '../utils/response.util';
|
import { errorResponse, successResponse } from '../utils/response.util';
|
||||||
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, BatchUpdateProductDTO } from '../dto/product.dto';
|
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO, BatchUpdateProductDTO } from '../dto/product.dto';
|
||||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
|
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
|
||||||
import { ContentType, Files } from '@midwayjs/core';
|
import { ContentType, Files } from '@midwayjs/core';
|
||||||
|
|
@ -82,7 +83,7 @@ export class ProductController {
|
||||||
@Post('/')
|
@Post('/')
|
||||||
async createProduct(@Body() productData: CreateProductDTO) {
|
async createProduct(@Body() productData: CreateProductDTO) {
|
||||||
try {
|
try {
|
||||||
const data = await this.productService.createProduct(productData);
|
const data = this.productService.createProduct(productData);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -114,9 +115,19 @@ export class ProductController {
|
||||||
try {
|
try {
|
||||||
// 条件判断:确保存在文件
|
// 条件判断:确保存在文件
|
||||||
const file = files?.[0];
|
const file = files?.[0];
|
||||||
if (!file) return errorResponse('未接收到上传文件');
|
if (!file?.data) return errorResponse('未接收到上传文件');
|
||||||
|
|
||||||
const result = await this.productService.importProductsCSV(file);
|
// 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);
|
||||||
return successResponse(result);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -127,7 +138,7 @@ export class ProductController {
|
||||||
@Put('/:id')
|
@Put('/:id')
|
||||||
async updateProduct(@Param('id') id: number, @Body() productData: UpdateProductDTO) {
|
async updateProduct(@Param('id') id: number, @Body() productData: UpdateProductDTO) {
|
||||||
try {
|
try {
|
||||||
const data = await this.productService.updateProduct(id, productData);
|
const data = this.productService.updateProduct(id, productData);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -149,7 +160,7 @@ export class ProductController {
|
||||||
@Put('updateNameCn/:id/:nameCn')
|
@Put('updateNameCn/:id/:nameCn')
|
||||||
async updatenameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) {
|
async updatenameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) {
|
||||||
try {
|
try {
|
||||||
const data = await this.productService.updatenameCn(id, nameCn);
|
const data = this.productService.updatenameCn(id, nameCn);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -179,6 +190,17 @@ 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 的库存)
|
// 根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存)
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
|
|
@ -317,7 +339,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/brand')
|
@Post('/brand')
|
||||||
async compatCreateBrand(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
|
async compatCreateBrand(@Body() body: { title: string; name: string }) {
|
||||||
try {
|
try {
|
||||||
const has = await this.productService.hasAttribute('brand', body.name); // 唯一性校验
|
const has = await this.productService.hasAttribute('brand', body.name); // 唯一性校验
|
||||||
if (has) return errorResponse('品牌已存在');
|
if (has) return errorResponse('品牌已存在');
|
||||||
|
|
@ -330,7 +352,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/brand/:id')
|
@Put('/brand/:id')
|
||||||
async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
|
async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||||
try {
|
try {
|
||||||
if (body?.name) {
|
if (body?.name) {
|
||||||
const has = await this.productService.hasAttribute('brand', body.name, id); // 唯一性校验(排除自身)
|
const has = await this.productService.hasAttribute('brand', body.name, id); // 唯一性校验(排除自身)
|
||||||
|
|
@ -379,7 +401,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/flavors')
|
@Post('/flavors')
|
||||||
async compatCreateFlavors(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
|
async compatCreateFlavors(@Body() body: { title: string; name: string }) {
|
||||||
try {
|
try {
|
||||||
const has = await this.productService.hasAttribute('flavor', body.name);
|
const has = await this.productService.hasAttribute('flavor', body.name);
|
||||||
if (has) return errorResponse('口味已存在');
|
if (has) return errorResponse('口味已存在');
|
||||||
|
|
@ -392,7 +414,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/flavors/:id')
|
@Put('/flavors/:id')
|
||||||
async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
|
async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||||
try {
|
try {
|
||||||
if (body?.name) {
|
if (body?.name) {
|
||||||
const has = await this.productService.hasAttribute('flavor', body.name, id);
|
const has = await this.productService.hasAttribute('flavor', body.name, id);
|
||||||
|
|
@ -441,7 +463,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/strength')
|
@Post('/strength')
|
||||||
async compatCreateStrength(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
|
async compatCreateStrength(@Body() body: { title: string; name: string }) {
|
||||||
try {
|
try {
|
||||||
const has = await this.productService.hasAttribute('strength', body.name);
|
const has = await this.productService.hasAttribute('strength', body.name);
|
||||||
if (has) return errorResponse('规格已存在');
|
if (has) return errorResponse('规格已存在');
|
||||||
|
|
@ -454,7 +476,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/strength/:id')
|
@Put('/strength/:id')
|
||||||
async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
|
async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||||
try {
|
try {
|
||||||
if (body?.name) {
|
if (body?.name) {
|
||||||
const has = await this.productService.hasAttribute('strength', body.name, id);
|
const has = await this.productService.hasAttribute('strength', body.name, id);
|
||||||
|
|
@ -503,7 +525,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/size')
|
@Post('/size')
|
||||||
async compatCreateSize(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
|
async compatCreateSize(@Body() body: { title: string; name: string }) {
|
||||||
try {
|
try {
|
||||||
const has = await this.productService.hasAttribute('size', body.name);
|
const has = await this.productService.hasAttribute('size', body.name);
|
||||||
if (has) return errorResponse('尺寸已存在');
|
if (has) return errorResponse('尺寸已存在');
|
||||||
|
|
@ -516,7 +538,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/size/:id')
|
@Put('/size/:id')
|
||||||
async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
|
async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||||
try {
|
try {
|
||||||
if (body?.name) {
|
if (body?.name) {
|
||||||
const has = await this.productService.hasAttribute('size', body.name, id);
|
const has = await this.productService.hasAttribute('size', body.name, id);
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,6 @@ export class CreateDictItemDTO {
|
||||||
@Rule(RuleType.string().allow('').allow(null))
|
@Rule(RuleType.string().allow('').allow(null))
|
||||||
titleCN?: string; // 字典项中文标题 (可选)
|
titleCN?: string; // 字典项中文标题 (可选)
|
||||||
|
|
||||||
@Rule(RuleType.string().allow('').allow(null))
|
|
||||||
image?: string; // 图片 (可选)
|
|
||||||
|
|
||||||
@Rule(RuleType.string().allow('').allow(null))
|
|
||||||
shortName?: string; // 简称 (可选)
|
|
||||||
|
|
||||||
@Rule(RuleType.number().required())
|
@Rule(RuleType.number().required())
|
||||||
dictId: number; // 所属字典的ID
|
dictId: number; // 所属字典的ID
|
||||||
}
|
}
|
||||||
|
|
@ -53,10 +47,4 @@ export class UpdateDictItemDTO {
|
||||||
@Rule(RuleType.string().allow(null))
|
@Rule(RuleType.string().allow(null))
|
||||||
value?: string; // 字典项值 (可选)
|
value?: string; // 字典项值 (可选)
|
||||||
|
|
||||||
@Rule(RuleType.string().allow('').allow(null))
|
|
||||||
image?: string; // 图片 (可选)
|
|
||||||
|
|
||||||
@Rule(RuleType.string().allow('').allow(null))
|
|
||||||
shortName?: string; // 简称 (可选)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,6 @@ export class CreateProductDTO {
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
|
|
||||||
@Rule(RuleType.string().optional())
|
|
||||||
shortDescription?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '产品 SKU', required: false })
|
@ApiProperty({ description: '产品 SKU', required: false })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
sku?: string;
|
sku?: string;
|
||||||
|
|
@ -58,10 +54,6 @@ export class CreateProductDTO {
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number())
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
|
||||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
|
||||||
siteSkus?: string[];
|
|
||||||
|
|
||||||
// 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
// 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
||||||
@ApiProperty({ description: '属性列表', type: 'array' })
|
@ApiProperty({ description: '属性列表', type: 'array' })
|
||||||
@Rule(RuleType.array().required())
|
@Rule(RuleType.array().required())
|
||||||
|
|
@ -118,10 +110,6 @@ export class UpdateProductDTO {
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
|
|
||||||
@Rule(RuleType.string().optional())
|
|
||||||
shortDescription?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '产品 SKU', required: false })
|
@ApiProperty({ description: '产品 SKU', required: false })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
sku?: string;
|
sku?: string;
|
||||||
|
|
@ -130,10 +118,6 @@ export class UpdateProductDTO {
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number())
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
|
||||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
|
||||||
siteSkus?: string[];
|
|
||||||
|
|
||||||
// 商品价格
|
// 商品价格
|
||||||
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number())
|
||||||
|
|
@ -155,23 +139,6 @@ export class UpdateProductDTO {
|
||||||
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
|
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
|
||||||
@Rule(RuleType.string().valid('single', 'bundle'))
|
@Rule(RuleType.string().valid('single', 'bundle'))
|
||||||
type?: string;
|
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 }[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -195,10 +162,6 @@ export class BatchUpdateProductDTO {
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '产品简短描述', description: '产品简短描述', required: false })
|
|
||||||
@Rule(RuleType.string().optional())
|
|
||||||
shortDescription?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '产品 SKU', required: false })
|
@ApiProperty({ description: '产品 SKU', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
sku?: string;
|
sku?: string;
|
||||||
|
|
@ -207,10 +170,6 @@ export class BatchUpdateProductDTO {
|
||||||
@Rule(RuleType.number().optional())
|
@Rule(RuleType.number().optional())
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
|
||||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
|
||||||
siteSkus?: string[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||||
@Rule(RuleType.number().optional())
|
@Rule(RuleType.number().optional())
|
||||||
price?: number;
|
price?: number;
|
||||||
|
|
@ -274,3 +233,20 @@ export class QueryProductDTO {
|
||||||
sortOrder?: string;
|
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,12 +38,6 @@ export class DictItem {
|
||||||
@Column({ nullable: true, comment: '字典项值' })
|
@Column({ nullable: true, comment: '字典项值' })
|
||||||
value?: string;
|
value?: string;
|
||||||
|
|
||||||
@Column({ nullable: true, comment: '图片' })
|
|
||||||
image: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true, comment: '简称' })
|
|
||||||
shortName: string;
|
|
||||||
|
|
||||||
// 排序
|
// 排序
|
||||||
@Column({ default: 0, comment: '排序' })
|
@Column({ default: 0, comment: '排序' })
|
||||||
sort: number;
|
sort: number;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import {
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
import { DictItem } from './dict_item.entity';
|
import { DictItem } from './dict_item.entity';
|
||||||
import { ProductStockComponent } from './product_stock_component.entity';
|
import { ProductStockComponent } from './product_stock_component.entity';
|
||||||
import { ProductSiteSku } from './product_site_sku.entity';
|
|
||||||
import { Category } from './category.entity';
|
import { Category } from './category.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
|
@ -50,10 +49,6 @@ export class Product {
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
|
|
||||||
@Column({ nullable: true })
|
|
||||||
shortDescription?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'sku'})
|
@ApiProperty({ description: 'sku'})
|
||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
sku: string;
|
sku: string;
|
||||||
|
|
@ -87,10 +82,6 @@ export class Product {
|
||||||
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
|
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
|
||||||
components: ProductStockComponent[];
|
components: ProductStockComponent[];
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 SKU 列表', type: ProductSiteSku, isArray: true })
|
|
||||||
@OneToMany(() => ProductSiteSku, (siteSku) => siteSku.product, { cascade: true })
|
|
||||||
siteSkus: ProductSiteSku[];
|
|
||||||
|
|
||||||
// 来源
|
// 来源
|
||||||
@ApiProperty({ description: '来源', example: '1' })
|
@ApiProperty({ description: '来源', example: '1' })
|
||||||
@Column({ default: 0 })
|
@Column({ default: 0 })
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import {
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
Entity,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
|
||||||
import { Product } from './product.entity';
|
|
||||||
|
|
||||||
@Entity('product_site_sku')
|
|
||||||
export class ProductSiteSku {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 SKU' })
|
|
||||||
@Column({ length: 100, comment: '站点 SKU' })
|
|
||||||
code: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Product, product => product.siteSkus, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'productId' })
|
|
||||||
product: Product;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
productId: number;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
@ -61,7 +61,7 @@ export class DictService {
|
||||||
|
|
||||||
// 生成并返回字典项的XLSX模板
|
// 生成并返回字典项的XLSX模板
|
||||||
getDictItemXLSXTemplate() {
|
getDictItemXLSXTemplate() {
|
||||||
const headers = ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName'];
|
const headers = ['name', 'title', 'titleCN', 'value', 'sort'];
|
||||||
const ws = xlsx.utils.aoa_to_sheet([headers]);
|
const ws = xlsx.utils.aoa_to_sheet([headers]);
|
||||||
const wb = xlsx.utils.book_new();
|
const wb = xlsx.utils.book_new();
|
||||||
xlsx.utils.book_append_sheet(wb, ws, 'DictItems');
|
xlsx.utils.book_append_sheet(wb, ws, 'DictItems');
|
||||||
|
|
@ -78,7 +78,7 @@ export class DictService {
|
||||||
const wsname = wb.SheetNames[0];
|
const wsname = wb.SheetNames[0];
|
||||||
const ws = wb.Sheets[wsname];
|
const ws = wb.Sheets[wsname];
|
||||||
// 支持titleCN字段的导入
|
// 支持titleCN字段的导入
|
||||||
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName'] }).slice(1);
|
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title', 'titleCN', 'value', 'sort'] }).slice(1);
|
||||||
|
|
||||||
const items = data.map((row: any) => {
|
const items = data.map((row: any) => {
|
||||||
const item = new DictItem();
|
const item = new DictItem();
|
||||||
|
|
@ -86,8 +86,6 @@ export class DictService {
|
||||||
item.title = row.title;
|
item.title = row.title;
|
||||||
item.titleCN = row.titleCN; // 保存中文名称
|
item.titleCN = row.titleCN; // 保存中文名称
|
||||||
item.value = row.value;
|
item.value = row.value;
|
||||||
item.image = row.image;
|
|
||||||
item.shortName = row.shortName;
|
|
||||||
item.sort = row.sort || 0;
|
item.sort = row.sort || 0;
|
||||||
item.dict = dict;
|
item.dict = dict;
|
||||||
return item;
|
return item;
|
||||||
|
|
@ -170,8 +168,6 @@ export class DictService {
|
||||||
item.name = this.formatName(createDictItemDTO.name);
|
item.name = this.formatName(createDictItemDTO.name);
|
||||||
item.title = createDictItemDTO.title;
|
item.title = createDictItemDTO.title;
|
||||||
item.titleCN = createDictItemDTO.titleCN; // 保存中文名称
|
item.titleCN = createDictItemDTO.titleCN; // 保存中文名称
|
||||||
item.image = createDictItemDTO.image;
|
|
||||||
item.shortName = createDictItemDTO.shortName;
|
|
||||||
item.dict = dict;
|
item.dict = dict;
|
||||||
return this.dictItemModel.save(item);
|
return this.dictItemModel.save(item);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import { Inject, Provide } from '@midwayjs/core';
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
import * as fs from 'fs';
|
|
||||||
import { In, Like, Not, Repository } from 'typeorm';
|
import { In, Like, Not, Repository } from 'typeorm';
|
||||||
import { Product } from '../entity/product.entity';
|
import { Product } from '../entity/product.entity';
|
||||||
import { paginate } from '../utils/paginate.util';
|
import { paginate } from '../utils/paginate.util';
|
||||||
import { PaginationParams } from '../interface';
|
import { PaginationParams } from '../interface';
|
||||||
import { parse } from 'csv-parse';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CreateProductDTO,
|
CreateProductDTO,
|
||||||
UpdateProductDTO,
|
UpdateProductDTO,
|
||||||
|
|
@ -29,7 +26,6 @@ import { StockService } from './stock.service';
|
||||||
import { Stock } from '../entity/stock.entity';
|
import { Stock } from '../entity/stock.entity';
|
||||||
import { StockPoint } from '../entity/stock_point.entity';
|
import { StockPoint } from '../entity/stock_point.entity';
|
||||||
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||||
import { ProductSiteSku } from '../entity/product_site_sku.entity';
|
|
||||||
import { Category } from '../entity/category.entity';
|
import { Category } from '../entity/category.entity';
|
||||||
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
||||||
|
|
||||||
|
|
@ -68,9 +64,6 @@ export class ProductService {
|
||||||
@InjectEntityModel(ProductStockComponent)
|
@InjectEntityModel(ProductStockComponent)
|
||||||
productStockComponentModel: Repository<ProductStockComponent>;
|
productStockComponentModel: Repository<ProductStockComponent>;
|
||||||
|
|
||||||
@InjectEntityModel(ProductSiteSku)
|
|
||||||
productSiteSkuModel: Repository<ProductSiteSku>;
|
|
||||||
|
|
||||||
@InjectEntityModel(Category)
|
@InjectEntityModel(Category)
|
||||||
categoryModel: Repository<Category>;
|
categoryModel: Repository<Category>;
|
||||||
|
|
||||||
|
|
@ -246,8 +239,7 @@ export class ProductService {
|
||||||
.createQueryBuilder('product')
|
.createQueryBuilder('product')
|
||||||
.leftJoinAndSelect('product.attributes', 'attribute')
|
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||||
.leftJoinAndSelect('attribute.dict', 'dict')
|
.leftJoinAndSelect('attribute.dict', 'dict')
|
||||||
.leftJoinAndSelect('product.category', 'category')
|
.leftJoinAndSelect('product.category', 'category');
|
||||||
.leftJoinAndSelect('product.siteSkus', 'siteSku');
|
|
||||||
|
|
||||||
// 模糊搜索 name,支持多个关键词
|
// 模糊搜索 name,支持多个关键词
|
||||||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||||
|
|
@ -420,13 +412,13 @@ export class ProductService {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const isExist = await qb.getOne();
|
const isExist = await qb.getOne();
|
||||||
if (isExist) throw new Error('相同产品属性的产品已存在');
|
if (isExist) throw new Error('产品已存在');
|
||||||
|
|
||||||
// 创建新产品实例(绑定属性与基础字段)
|
// 创建新产品实例(绑定属性与基础字段)
|
||||||
const product = new Product();
|
const product = new Product();
|
||||||
|
|
||||||
// 使用 merge 填充基础字段,排除特殊处理字段
|
// 使用 merge 填充基础字段,排除特殊处理字段
|
||||||
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = createProductDTO;
|
const { attributes: _attrs, categoryId: _cid, sku: _sku, ...simpleFields } = createProductDTO;
|
||||||
this.productModel.merge(product, simpleFields);
|
this.productModel.merge(product, simpleFields);
|
||||||
|
|
||||||
product.attributes = resolvedAttributes;
|
product.attributes = resolvedAttributes;
|
||||||
|
|
@ -444,7 +436,7 @@ export class ProductService {
|
||||||
for (const a of resolvedAttributes) {
|
for (const a of resolvedAttributes) {
|
||||||
if (a?.dict?.name && a?.name) attributeMap[a.dict.name] = a.name;
|
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'] || '',
|
brand: attributeMap['brand'] || '',
|
||||||
flavor: attributeMap['flavor'] || '',
|
flavor: attributeMap['flavor'] || '',
|
||||||
strength: attributeMap['strength'] || '',
|
strength: attributeMap['strength'] || '',
|
||||||
|
|
@ -452,27 +444,7 @@ export class ProductService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedProduct = await this.productModel.save(product);
|
return await this.productModel.save(product);
|
||||||
|
|
||||||
// 保存站点 SKU 列表
|
|
||||||
if (createProductDTO.siteSkus && createProductDTO.siteSkus.length > 0) {
|
|
||||||
const siteSkus = createProductDTO.siteSkus.map(code => {
|
|
||||||
const s = new ProductSiteSku();
|
|
||||||
s.code = code;
|
|
||||||
s.product = savedProduct;
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
await this.productSiteSkuModel.save(siteSkus);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件信息
|
|
||||||
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', 'siteSkus'] });
|
|
||||||
}
|
|
||||||
|
|
||||||
return savedProduct;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProduct(
|
async updateProduct(
|
||||||
|
|
@ -486,7 +458,7 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 merge 更新基础字段,排除特殊处理字段
|
// 使用 merge 更新基础字段,排除特殊处理字段
|
||||||
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = updateProductDTO;
|
const { attributes: _attrs, categoryId: _cid, sku: _sku, ...simpleFields } = updateProductDTO;
|
||||||
this.productModel.merge(product, simpleFields);
|
this.productModel.merge(product, simpleFields);
|
||||||
|
|
||||||
// 处理分类更新
|
// 处理分类更新
|
||||||
|
|
@ -501,23 +473,6 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理站点 SKU 更新
|
|
||||||
if (updateProductDTO.siteSkus !== undefined) {
|
|
||||||
// 删除旧的 siteSkus
|
|
||||||
await this.productSiteSkuModel.delete({ productId: id });
|
|
||||||
|
|
||||||
// 如果有新的 siteSkus,则保存
|
|
||||||
if (updateProductDTO.siteSkus.length > 0) {
|
|
||||||
const siteSkus = updateProductDTO.siteSkus.map(code => {
|
|
||||||
const s = new ProductSiteSku();
|
|
||||||
s.code = code;
|
|
||||||
s.productId = id;
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
await this.productSiteSkuModel.save(siteSkus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 SKU 更新
|
// 处理 SKU 更新
|
||||||
if (updateProductDTO.sku !== undefined) {
|
if (updateProductDTO.sku !== undefined) {
|
||||||
// 校验 SKU 唯一性(如变更)
|
// 校验 SKU 唯一性(如变更)
|
||||||
|
|
@ -578,13 +533,6 @@ export class ProductService {
|
||||||
|
|
||||||
// 保存更新后的产品
|
// 保存更新后的产品
|
||||||
const saved = await this.productModel.save(product);
|
const saved = await this.productModel.save(product);
|
||||||
|
|
||||||
// 处理组件更新
|
|
||||||
if (updateProductDTO.components !== undefined) {
|
|
||||||
// 如果 components 为空数组,则删除所有组件? setProductComponents 会处理
|
|
||||||
await this.setProductComponents(saved.id, updateProductDTO.components);
|
|
||||||
}
|
|
||||||
|
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -620,7 +568,6 @@ export class ProductService {
|
||||||
if (updateData.name !== undefined) simpleUpdate.name = updateData.name;
|
if (updateData.name !== undefined) simpleUpdate.name = updateData.name;
|
||||||
if (updateData.nameCn !== undefined) simpleUpdate.nameCn = updateData.nameCn;
|
if (updateData.nameCn !== undefined) simpleUpdate.nameCn = updateData.nameCn;
|
||||||
if (updateData.description !== undefined) simpleUpdate.description = updateData.description;
|
if (updateData.description !== undefined) simpleUpdate.description = updateData.description;
|
||||||
if (updateData.shortDescription !== undefined) simpleUpdate.shortDescription = updateData.shortDescription;
|
|
||||||
if (updateData.price !== undefined) simpleUpdate.price = updateData.price;
|
if (updateData.price !== undefined) simpleUpdate.price = updateData.price;
|
||||||
if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice;
|
if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice;
|
||||||
|
|
||||||
|
|
@ -706,9 +653,7 @@ export class ProductService {
|
||||||
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
// 条件判断(单品 simple 不允许手动设置组成)
|
// 条件判断(单品 simple 不允许手动设置组成)
|
||||||
if (product.type === 'single') {
|
if (product.type === 'single') {
|
||||||
// 单品类型,直接清空关联的组成(如果有)
|
throw new Error('单品无需设置组成');
|
||||||
await this.productStockComponentModel.delete({ productId });
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const validItems = (items || [])
|
const validItems = (items || [])
|
||||||
|
|
@ -1168,7 +1113,7 @@ export class ProductService {
|
||||||
// 通用属性:创建字典项
|
// 通用属性:创建字典项
|
||||||
async createAttribute(
|
async createAttribute(
|
||||||
dictName: string,
|
dictName: string,
|
||||||
payload: { title: string; name: string; image?: string; shortName?: string }
|
payload: { title: string; name: string }
|
||||||
): Promise<DictItem> {
|
): Promise<DictItem> {
|
||||||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||||||
if (!dict) throw new Error(`字典 ${dictName} 不存在`);
|
if (!dict) throw new Error(`字典 ${dictName} 不存在`);
|
||||||
|
|
@ -1180,8 +1125,6 @@ export class ProductService {
|
||||||
const item = new DictItem();
|
const item = new DictItem();
|
||||||
item.title = payload.title;
|
item.title = payload.title;
|
||||||
item.name = payload.name;
|
item.name = payload.name;
|
||||||
item.image = payload.image;
|
|
||||||
item.shortName = payload.shortName;
|
|
||||||
item.dict = dict;
|
item.dict = dict;
|
||||||
return await this.dictItemModel.save(item);
|
return await this.dictItemModel.save(item);
|
||||||
}
|
}
|
||||||
|
|
@ -1189,14 +1132,12 @@ export class ProductService {
|
||||||
// 通用属性:更新字典项
|
// 通用属性:更新字典项
|
||||||
async updateAttribute(
|
async updateAttribute(
|
||||||
id: number,
|
id: number,
|
||||||
payload: { title?: string; name?: string; image?: string; shortName?: string }
|
payload: { title?: string; name?: string }
|
||||||
): Promise<DictItem> {
|
): Promise<DictItem> {
|
||||||
const item = await this.dictItemModel.findOne({ where: { id } });
|
const item = await this.dictItemModel.findOne({ where: { id } });
|
||||||
if (!item) throw new Error('字典项不存在');
|
if (!item) throw new Error('字典项不存在');
|
||||||
if (payload.title !== undefined) item.title = payload.title;
|
if (payload.title !== undefined) item.title = payload.title;
|
||||||
if (payload.name !== undefined) item.name = payload.name;
|
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);
|
return await this.dictItemModel.save(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1321,7 +1262,6 @@ export class ProductService {
|
||||||
price: num(rec.price),
|
price: num(rec.price),
|
||||||
promotionPrice: num(rec.promotionPrice),
|
promotionPrice: num(rec.promotionPrice),
|
||||||
type: val(rec.type),
|
type: val(rec.type),
|
||||||
siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined,
|
|
||||||
|
|
||||||
attributes: attributes.length > 0 ? attributes : undefined,
|
attributes: attributes.length > 0 ? attributes : undefined,
|
||||||
components: components.length > 0 ? components : undefined,
|
components: components.length > 0 ? components : undefined,
|
||||||
|
|
@ -1336,7 +1276,6 @@ export class ProductService {
|
||||||
dto.nameCn = data.nameCn;
|
dto.nameCn = data.nameCn;
|
||||||
dto.description = data.description;
|
dto.description = data.description;
|
||||||
dto.sku = data.sku;
|
dto.sku = data.sku;
|
||||||
if (data.siteSkus) dto.siteSkus = data.siteSkus;
|
|
||||||
|
|
||||||
// 数值类型转换
|
// 数值类型转换
|
||||||
if (data.price !== undefined) dto.price = Number(data.price);
|
if (data.price !== undefined) dto.price = Number(data.price);
|
||||||
|
|
@ -1349,7 +1288,7 @@ export class ProductService {
|
||||||
dto.attributes = Array.isArray(data.attributes) ? data.attributes : [];
|
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;
|
if (data.components) dto.components = data.components;
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
|
|
@ -1363,7 +1302,6 @@ export class ProductService {
|
||||||
if (data.nameCn !== undefined) dto.nameCn = data.nameCn;
|
if (data.nameCn !== undefined) dto.nameCn = data.nameCn;
|
||||||
if (data.description !== undefined) dto.description = data.description;
|
if (data.description !== undefined) dto.description = data.description;
|
||||||
if (data.sku !== undefined) dto.sku = data.sku;
|
if (data.sku !== undefined) dto.sku = data.sku;
|
||||||
if (data.siteSkus !== undefined) dto.siteSkus = data.siteSkus;
|
|
||||||
|
|
||||||
if (data.price !== undefined) dto.price = Number(data.price);
|
if (data.price !== undefined) dto.price = Number(data.price);
|
||||||
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
|
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
|
||||||
|
|
@ -1372,7 +1310,6 @@ export class ProductService {
|
||||||
|
|
||||||
if (data.type !== undefined) dto.type = data.type;
|
if (data.type !== undefined) dto.type = data.type;
|
||||||
if (data.attributes !== undefined) dto.attributes = data.attributes;
|
if (data.attributes !== undefined) dto.attributes = data.attributes;
|
||||||
if (data.components !== undefined) dto.components = data.components;
|
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
@ -1402,7 +1339,6 @@ export class ProductService {
|
||||||
// 基础数据
|
// 基础数据
|
||||||
const rowData = [
|
const rowData = [
|
||||||
esc(p.sku),
|
esc(p.sku),
|
||||||
esc(p.siteSkus ? p.siteSkus.map(s => s.code).join(',') : ''),
|
|
||||||
esc(p.name),
|
esc(p.name),
|
||||||
esc(p.nameCn),
|
esc(p.nameCn),
|
||||||
esc(p.price),
|
esc(p.price),
|
||||||
|
|
@ -1437,7 +1373,7 @@ export class ProductService {
|
||||||
async exportProductsCSV(): Promise<string> {
|
async exportProductsCSV(): Promise<string> {
|
||||||
// 查询所有产品及其属性(包含字典关系)和组成
|
// 查询所有产品及其属性(包含字典关系)和组成
|
||||||
const products = await this.productModel.find({
|
const products = await this.productModel.find({
|
||||||
relations: ['attributes', 'attributes.dict', 'components', 'siteSkus'],
|
relations: ['attributes', 'attributes.dict', 'components'],
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1466,7 +1402,6 @@ export class ProductService {
|
||||||
// 定义 CSV 表头(与导入字段一致)
|
// 定义 CSV 表头(与导入字段一致)
|
||||||
const baseHeaders = [
|
const baseHeaders = [
|
||||||
'sku',
|
'sku',
|
||||||
'siteSkus',
|
|
||||||
'name',
|
'name',
|
||||||
'nameCn',
|
'nameCn',
|
||||||
'price',
|
'price',
|
||||||
|
|
@ -1500,37 +1435,17 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 CSV 导入产品;存在则更新,不存在则创建
|
// 从 CSV 导入产品;存在则更新,不存在则创建
|
||||||
async importProductsCSV(file: any): Promise<{ created: number; updated: number; errors: string[] }> {
|
async importProductsCSV(buffer: Buffer): 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 按表头解析)
|
// 解析 CSV(使用 csv-parse/sync 按表头解析)
|
||||||
|
const { parse } = await import('csv-parse/sync');
|
||||||
let records: any[] = [];
|
let records: any[] = [];
|
||||||
try {
|
try {
|
||||||
records = await new Promise((resolve, reject) => {
|
records = parse(buffer, {
|
||||||
parse(buffer, {
|
|
||||||
columns: true,
|
columns: true,
|
||||||
skip_empty_lines: true,
|
skip_empty_lines: true,
|
||||||
trim: true,
|
trim: true,
|
||||||
bom: true,
|
bom: true,
|
||||||
}, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(data);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
})
|
|
||||||
console.log('Parsed records count:', records.length);
|
console.log('Parsed records count:', records.length);
|
||||||
if (records.length > 0) {
|
if (records.length > 0) {
|
||||||
console.log('First record keys:', Object.keys(records[0]));
|
console.log('First record keys:', Object.keys(records[0]));
|
||||||
|
|
@ -1551,7 +1466,10 @@ export class ProductService {
|
||||||
errors.push('缺少 SKU 的记录已跳过');
|
errors.push('缺少 SKU 的记录已跳过');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const { sku } = data;
|
const { sku, components } = data;
|
||||||
|
|
||||||
|
let currentProductId: number;
|
||||||
|
let currentProductType: string = data.type || 'single';
|
||||||
|
|
||||||
// 查找现有产品
|
// 查找现有产品
|
||||||
const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] });
|
const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] });
|
||||||
|
|
@ -1559,16 +1477,25 @@ export class ProductService {
|
||||||
if (!exist) {
|
if (!exist) {
|
||||||
// 创建新产品
|
// 创建新产品
|
||||||
const createDTO = this.prepareCreateProductDTO(data);
|
const createDTO = this.prepareCreateProductDTO(data);
|
||||||
await this.createProduct(createDTO);
|
const createdProduct = await this.createProduct(createDTO);
|
||||||
|
currentProductId = createdProduct.id;
|
||||||
|
currentProductType = createdProduct.type;
|
||||||
created += 1;
|
created += 1;
|
||||||
} else {
|
} else {
|
||||||
// 更新产品
|
// 更新产品
|
||||||
const updateDTO = this.prepareUpdateProductDTO(data);
|
const updateDTO = this.prepareUpdateProductDTO(data);
|
||||||
await this.updateProduct(exist.id, updateDTO);
|
await this.updateProduct(exist.id, updateDTO);
|
||||||
|
currentProductId = exist.id;
|
||||||
|
currentProductType = updateDTO.type || exist.type;
|
||||||
updated += 1;
|
updated += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 保存组件信息
|
||||||
|
if (currentProductType !== 'single' && components && components.length > 0) {
|
||||||
|
await this.setProductComponents(currentProductId, components);
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
errors.push(`产品${rec?.sku}导入失败:${e?.message || String(e)}`);
|
errors.push(e?.message || String(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue