feat: 添加少许特性 #50
|
|
@ -118,8 +118,7 @@ export class MainConfiguration {
|
|||
});
|
||||
|
||||
try {
|
||||
this.logger.info('正在检查数据库是否存在...');
|
||||
|
||||
this.logger.info(`正在检查数据库是否存在...`+ JSON.stringify(typeormConfig));
|
||||
// 初始化临时数据源
|
||||
await tempDataSource.initialize();
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export class DictController {
|
|||
// 从上传的文件列表中获取第一个文件
|
||||
const file = files[0];
|
||||
// 调用服务层方法处理XLSX文件
|
||||
const result = await this.dictService.importDictsFromXLSX(file.data);
|
||||
const result = await this.dictService.importDictsFromTable(file.data);
|
||||
// 返回导入结果
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,10 @@ export class CreateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
promotionPrice?: number;
|
||||
|
||||
|
||||
// 产品图片URL
|
||||
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
image?: string;
|
||||
|
||||
// 商品类型(默认 single; bundle 需手动设置组成)
|
||||
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false })
|
||||
|
|
@ -153,7 +156,10 @@ export class UpdateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
promotionPrice?: number;
|
||||
|
||||
|
||||
// 产品图片URL
|
||||
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
image?: string;
|
||||
|
||||
// 属性更新(可选, 支持增量替换指定字典的属性项)
|
||||
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||
|
|
@ -228,6 +234,10 @@ export class BatchUpdateProductDTO {
|
|||
@Rule(RuleType.number().optional())
|
||||
promotionPrice?: number;
|
||||
|
||||
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
image?: string;
|
||||
|
||||
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||
@Rule(RuleType.array().optional())
|
||||
attributes?: AttributeInputDTO[];
|
||||
|
|
|
|||
|
|
@ -180,6 +180,94 @@ export interface ShopyyProduct {
|
|||
updated_at?: string | number;
|
||||
}
|
||||
|
||||
// 商品变体接口(用于更新)
|
||||
export interface ShopyyProductVariant {
|
||||
id?: number; // 变体ID
|
||||
option1_title?: string; // 选项1名称
|
||||
option2_title?: string; // 选项2名称
|
||||
option3_title?: string; // 选项3名称
|
||||
option1_value_title?: string; // 选项1值
|
||||
option2_value_title?: string; // 选项2值
|
||||
option3_value_title?: string; // 选项3值
|
||||
image_id?: number; // 图片ID
|
||||
src?: string; // 图片URL
|
||||
title?: string; // 变体标题
|
||||
barcode?: string; // 条形码
|
||||
sku?: string; // 库存单位
|
||||
inventory_quantity?: number; // 库存数量
|
||||
price?: string; // 价格
|
||||
compare_at_price?: string; // 比较价格
|
||||
weight?: string; // 重量
|
||||
}
|
||||
|
||||
// 商品图片接口(用于更新)
|
||||
export interface ShopyyProductImage {
|
||||
image_id?: number; // 图片ID
|
||||
src?: string; // 图片URL
|
||||
alt?: string; // 图片替代文本
|
||||
}
|
||||
|
||||
// 选项值接口(用于更新)
|
||||
export interface ShopyyProductOptionValue {
|
||||
id?: number; // 选项值ID
|
||||
option_id?: number; // 所属选项ID
|
||||
option_value?: string; // 选项值
|
||||
}
|
||||
|
||||
// 商品选项接口(用于更新)
|
||||
export interface ShopyyProductOption {
|
||||
id?: number; // 选项ID
|
||||
option_name?: string; // 选项名称
|
||||
values?: ShopyyProductOptionValue[]; // 选项值列表
|
||||
}
|
||||
|
||||
// 商品分类接口(用于更新)
|
||||
export interface ShopyyProductCollection {
|
||||
collection_id?: number; // 分类ID
|
||||
}
|
||||
|
||||
// Shopyy商品更新参数接口
|
||||
export interface ShopyyProductUpdateParams {
|
||||
product_type?: string; // 商品类型
|
||||
handle?: string; // 商品别名
|
||||
spu?: string; // 商品编码
|
||||
title?: string; // 商品标题
|
||||
subtitle?: string; // 商品副标题
|
||||
vendor?: string; // 供应商
|
||||
meta_title?: string; // 元标题
|
||||
meta_descript?: string; // 元描述
|
||||
meta_keywords?: string[]; // 元关键词列表
|
||||
inner_title?: string; // 内部名称
|
||||
inventory_tracking?: number; // 库存跟踪(1:开启, 0:关闭)
|
||||
spec_mode?: number; // 规格模式
|
||||
mini_detail?: string; // 简要描述
|
||||
free_shipping?: number; // 免运费(1:是, 0:否)
|
||||
inventory_policy?: number; // 库存策略
|
||||
taxable?: number; // 是否 taxable(1:是, 0:否)
|
||||
virtual_sale_count?: number; // 虚拟销量
|
||||
body_html?: string; // 商品详情HTML
|
||||
status?: number; // 商品状态(1:上架, 0:下架)
|
||||
variants?: ShopyyProductVariant[]; // 商品变体列表
|
||||
images?: ShopyyProductImage[]; // 商品图片列表
|
||||
options?: ShopyyProductOption[]; // 商品选项列表
|
||||
tags?: string[]; // 商品标签列表
|
||||
collections?: ShopyyProductCollection[]; // 商品分类列表
|
||||
}
|
||||
export interface ShopyyProductUpdateManyParams {
|
||||
|
||||
products: ({
|
||||
id: number;
|
||||
product
|
||||
})[]
|
||||
}
|
||||
|
||||
export interface ShopyyProductStockUpdateParams {
|
||||
data: ({
|
||||
sku: string;
|
||||
sku_code: string;
|
||||
inventory_quantity: number;
|
||||
})[]
|
||||
}
|
||||
// 变体类型
|
||||
export interface ShopyyVariant {
|
||||
id: number;
|
||||
|
|
@ -525,8 +613,8 @@ export interface ShopyyGetOneOrderResult {
|
|||
refunds: any[];
|
||||
}
|
||||
|
||||
export interface ShopyyOrderCreateParams {}
|
||||
export interface ShopyyOrderUpdateParams {}
|
||||
export interface ShopyyOrderCreateParams { }
|
||||
export interface ShopyyOrderUpdateParams { }
|
||||
// 订单类型
|
||||
export interface ShopyyOrder {
|
||||
// ========================================
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ export class OrderSale {
|
|||
@Expose()
|
||||
isPackage: boolean;
|
||||
|
||||
@ApiProperty({ description: '商品品类', type: 'string',nullable: true})
|
||||
@Expose()
|
||||
@Column({ nullable: true })
|
||||
category?: string;
|
||||
// TODO 这个其实还是直接保存 product 比较好
|
||||
@ApiProperty({ description: '品牌', type: 'string',nullable: true})
|
||||
@Expose()
|
||||
@Column({ nullable: true })
|
||||
|
|
@ -85,7 +90,7 @@ export class OrderSale {
|
|||
@ApiProperty({name: '强度', nullable: true })
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
strength: string | null;
|
||||
strength: string | null;
|
||||
|
||||
@ApiProperty({ description: '版本', type: 'string', nullable: true })
|
||||
@Expose()
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ export class Product {
|
|||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: '图片URL', description: '产品图片URL' })
|
||||
@Column({ nullable: true })
|
||||
image?: string;
|
||||
// 商品价格
|
||||
@ApiProperty({ description: '价格', example: 99.99 })
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export class ProductStockComponent {
|
|||
@ApiProperty({ description: '组件所关联的 SKU', type: 'string' })
|
||||
@Column({ type: 'varchar', length: 64 })
|
||||
sku: string;
|
||||
|
||||
|
||||
@ApiProperty({ type: Number, description: '组成数量' })
|
||||
@Column({ type: 'int', default: 1 })
|
||||
quantity: number;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Site } from './site.entity';
|
||||
import { Product } from './product.entity';
|
||||
|
||||
@Entity('site_product')
|
||||
export class SiteProduct {
|
||||
@ApiProperty({
|
||||
example: '12345',
|
||||
description: '站点商品ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
})
|
||||
@Column({ primary: true })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '站点ID' })
|
||||
@Column()
|
||||
siteId: number;
|
||||
|
||||
@ApiProperty({ description: '商品ID' })
|
||||
@Column({ nullable: true })
|
||||
productId: number;
|
||||
|
||||
@ApiProperty({ description: 'sku'})
|
||||
@Column()
|
||||
sku: string;
|
||||
|
||||
@ApiProperty({ description: '类型' })
|
||||
@Column({ length: 16, default: 'single' })
|
||||
type: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '产品名称',
|
||||
type: 'string',
|
||||
required: true,
|
||||
})
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: '产品图片' })
|
||||
@Column({ default: '' })
|
||||
image: string;
|
||||
|
||||
@ApiProperty({ description: '父商品ID', example: '12345' })
|
||||
@Column({ nullable: true })
|
||||
parentId: string;
|
||||
|
||||
// 站点关联
|
||||
@ManyToOne(() => Site, site => site.id)
|
||||
@JoinColumn({ name: 'siteId' })
|
||||
site: Site;
|
||||
|
||||
// 商品关联
|
||||
@ManyToOne(() => Product, product => product.id)
|
||||
@JoinColumn({ name: 'productId' })
|
||||
product: Product;
|
||||
|
||||
// 父商品关联
|
||||
@ManyToOne(() => SiteProduct, siteProduct => siteProduct.id)
|
||||
@JoinColumn({ name: 'parentId' })
|
||||
parent: SiteProduct;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
description: '创建时间',
|
||||
required: true,
|
||||
})
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
description: '更新时间',
|
||||
required: true,
|
||||
})
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ export class DictService {
|
|||
}
|
||||
|
||||
// 从XLSX文件导入字典
|
||||
async importDictsFromXLSX(bufferOrPath: Buffer | string) {
|
||||
async importDictsFromTable(bufferOrPath: Buffer | string) {
|
||||
// 判断传入的是 Buffer 还是文件路径字符串
|
||||
let buffer: Buffer;
|
||||
if (typeof bufferOrPath === 'string') {
|
||||
|
|
@ -60,7 +60,7 @@ export class DictService {
|
|||
// 如果是 Buffer,直接使用
|
||||
buffer = bufferOrPath;
|
||||
}
|
||||
|
||||
|
||||
// 读取缓冲区中的工作簿
|
||||
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||||
// 获取第一个工作表的名称
|
||||
|
|
@ -93,7 +93,7 @@ export class DictService {
|
|||
|
||||
// 从XLSX文件导入字典项
|
||||
async importDictItemsFromXLSX(bufferOrPath: Buffer | string, dictId: number): Promise<BatchOperationResultDTO> {
|
||||
if(!dictId){
|
||||
if (!dictId) {
|
||||
throw new Error("引入失败, 请输入字典 ID")
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ export class DictService {
|
|||
if (!dict) {
|
||||
throw new Error('指定的字典不存在');
|
||||
}
|
||||
|
||||
|
||||
// 判断传入的是 Buffer 还是文件路径字符串
|
||||
let buffer: Buffer;
|
||||
if (typeof bufferOrPath === 'string') {
|
||||
|
|
@ -111,7 +111,7 @@ export class DictService {
|
|||
// 如果是 Buffer,直接使用
|
||||
buffer = bufferOrPath;
|
||||
}
|
||||
|
||||
|
||||
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||||
const wsname = wb.SheetNames[0];
|
||||
const ws = wb.Sheets[wsname];
|
||||
|
|
@ -122,7 +122,7 @@ export class DictService {
|
|||
const createdItems = [];
|
||||
const updatedItems = [];
|
||||
const errors = [];
|
||||
|
||||
|
||||
for (const row of data) {
|
||||
try {
|
||||
const result = await this.upsertDictItem(dictId, {
|
||||
|
|
@ -150,7 +150,7 @@ export class DictService {
|
|||
|
||||
const processed = createdItems.length + updatedItems.length;
|
||||
|
||||
return {
|
||||
return {
|
||||
total: data.length,
|
||||
processed: processed,
|
||||
updated: updatedItems.length,
|
||||
|
|
@ -216,10 +216,10 @@ export class DictService {
|
|||
|
||||
// 如果提供了 dictId,则只返回该字典下的项
|
||||
if (params.dictId) {
|
||||
return this.dictItemModel.find({ where });
|
||||
return this.dictItemModel.find({ where, relations: ['dict'] });
|
||||
}
|
||||
// 否则,返回所有字典项
|
||||
return this.dictItemModel.find();
|
||||
return this.dictItemModel.find({ relations: ['dict'] });
|
||||
}
|
||||
|
||||
// 创建新字典项
|
||||
|
|
@ -251,7 +251,7 @@ export class DictService {
|
|||
}) {
|
||||
// 格式化 name
|
||||
const formattedName = this.formatName(itemData.name);
|
||||
|
||||
|
||||
// 查找是否已存在该字典项(根据 name 和 dictId)
|
||||
const existingItem = await this.dictItemModel.findOne({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -774,7 +774,7 @@ export class ProductService {
|
|||
}
|
||||
} else {
|
||||
// 简单字段,直接批量更新以提高性能
|
||||
// UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice, siteSkus
|
||||
// UpdateProductDTO 里的简单字段: name, nameCn, description, shortDescription, price, promotionPrice, image, siteSkus
|
||||
|
||||
const simpleUpdate: any = {};
|
||||
if (updateData.name !== undefined) simpleUpdate.name = updateData.name;
|
||||
|
|
@ -783,6 +783,7 @@ export class ProductService {
|
|||
if (updateData.shortDescription !== undefined) simpleUpdate.shortDescription = updateData.shortDescription;
|
||||
if (updateData.price !== undefined) simpleUpdate.price = updateData.price;
|
||||
if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice;
|
||||
if (updateData.image !== undefined) simpleUpdate.image = updateData.image;
|
||||
if (updateData.siteSkus !== undefined) simpleUpdate.siteSkus = updateData.siteSkus;
|
||||
|
||||
if (Object.keys(simpleUpdate).length > 0) {
|
||||
|
|
@ -1663,7 +1664,9 @@ export class ProductService {
|
|||
rows.push(rowData.join(','));
|
||||
}
|
||||
|
||||
return rows.join('\n');
|
||||
// 添加UTF-8 BOM以确保中文在Excel中正确显示
|
||||
return '\ufeff' + rows.join('\n');
|
||||
|
||||
}
|
||||
async getRecordsFromTable(file: any) {
|
||||
// 解析文件(使用 xlsx 包自动识别文件类型并解析)
|
||||
|
|
@ -1686,7 +1689,8 @@ export class ProductService {
|
|||
|
||||
let records: any[] = []
|
||||
// xlsx 包会自动根据文件内容识别文件类型(CSV 或 XLSX)
|
||||
const workbook = xlsx.read(buffer, { type: 'buffer' });
|
||||
// 添加codepage: 65001以确保正确处理UTF-8编码的中文
|
||||
const workbook = xlsx.read(buffer, { type: 'buffer', codepage: 65001 });
|
||||
// 获取第一个工作表
|
||||
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
// 将工作表转换为 JSON 数组
|
||||
|
|
|
|||
Loading…
Reference in New Issue