zksu
/
API
forked from yoone/API
1
0
Fork 0

feat: 添加产品图片字段并优化字典导入功能

添加产品图片URL字段到产品相关实体和DTO
重命名字典导入方法并优化导入逻辑
新增站点商品实体和ShopYY商品更新接口
优化Excel处理以支持UTF-8编码
This commit is contained in:
tikkhun 2026-01-14 19:16:30 +08:00
parent 56deb447b3
commit fbbb86ae37
10 changed files with 217 additions and 22 deletions

View File

@ -118,8 +118,7 @@ export class MainConfiguration {
}); });
try { try {
this.logger.info('正在检查数据库是否存在...'); this.logger.info(`正在检查数据库是否存在...`+ JSON.stringify(typeormConfig));
// 初始化临时数据源 // 初始化临时数据源
await tempDataSource.initialize(); await tempDataSource.initialize();

View File

@ -30,7 +30,7 @@ export class DictController {
// 从上传的文件列表中获取第一个文件 // 从上传的文件列表中获取第一个文件
const file = files[0]; const file = files[0];
// 调用服务层方法处理XLSX文件 // 调用服务层方法处理XLSX文件
const result = await this.dictService.importDictsFromXLSX(file.data); const result = await this.dictService.importDictsFromTable(file.data);
// 返回导入结果 // 返回导入结果
return result; return result;
} }

View File

@ -86,7 +86,10 @@ export class CreateProductDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
promotionPrice?: number; promotionPrice?: number;
// 产品图片URL
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
@Rule(RuleType.string().optional())
image?: string;
// 商品类型(默认 single; bundle 需手动设置组成) // 商品类型(默认 single; bundle 需手动设置组成)
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false }) @ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false })
@ -153,7 +156,10 @@ export class UpdateProductDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
promotionPrice?: 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 }) @ApiProperty({ description: '属性列表', type: 'array', required: false })
@ -228,6 +234,10 @@ export class BatchUpdateProductDTO {
@Rule(RuleType.number().optional()) @Rule(RuleType.number().optional())
promotionPrice?: number; 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 }) @ApiProperty({ description: '属性列表', type: 'array', required: false })
@Rule(RuleType.array().optional()) @Rule(RuleType.array().optional())
attributes?: AttributeInputDTO[]; attributes?: AttributeInputDTO[];

View File

@ -180,6 +180,94 @@ export interface ShopyyProduct {
updated_at?: string | number; 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 { export interface ShopyyVariant {
id: number; id: number;

View File

@ -62,6 +62,11 @@ export class OrderSale {
@Expose() @Expose()
isPackage: boolean; isPackage: boolean;
@ApiProperty({ description: '商品品类', type: 'string',nullable: true})
@Expose()
@Column({ nullable: true })
category?: string;
// TODO 这个其实还是直接保存 product 比较好
@ApiProperty({ description: '品牌', type: 'string',nullable: true}) @ApiProperty({ description: '品牌', type: 'string',nullable: true})
@Expose() @Expose()
@Column({ nullable: true }) @Column({ nullable: true })

View File

@ -55,6 +55,9 @@ export class Product {
@Column({ nullable: true }) @Column({ nullable: true })
description?: string; description?: string;
@ApiProperty({ example: '图片URL', description: '产品图片URL' })
@Column({ nullable: true })
image?: string;
// 商品价格 // 商品价格
@ApiProperty({ description: '价格', example: 99.99 }) @ApiProperty({ description: '价格', example: 99.99 })
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })

View File

@ -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;
}

View File

@ -50,7 +50,7 @@ export class DictService {
} }
// 从XLSX文件导入字典 // 从XLSX文件导入字典
async importDictsFromXLSX(bufferOrPath: Buffer | string) { async importDictsFromTable(bufferOrPath: Buffer | string) {
// 判断传入的是 Buffer 还是文件路径字符串 // 判断传入的是 Buffer 还是文件路径字符串
let buffer: Buffer; let buffer: Buffer;
if (typeof bufferOrPath === 'string') { if (typeof bufferOrPath === 'string') {
@ -216,10 +216,10 @@ export class DictService {
// 如果提供了 dictId,则只返回该字典下的项 // 如果提供了 dictId,则只返回该字典下的项
if (params.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'] });
} }
// 创建新字典项 // 创建新字典项

View File

@ -774,7 +774,7 @@ export class ProductService {
} }
} else { } else {
// 简单字段,直接批量更新以提高性能 // 简单字段,直接批量更新以提高性能
// UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice, siteSkus // UpdateProductDTO 里的简单字段: name, nameCn, description, shortDescription, price, promotionPrice, image, siteSkus
const simpleUpdate: any = {}; const simpleUpdate: any = {};
if (updateData.name !== undefined) simpleUpdate.name = updateData.name; 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.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;
if (updateData.image !== undefined) simpleUpdate.image = updateData.image;
if (updateData.siteSkus !== undefined) simpleUpdate.siteSkus = updateData.siteSkus; if (updateData.siteSkus !== undefined) simpleUpdate.siteSkus = updateData.siteSkus;
if (Object.keys(simpleUpdate).length > 0) { if (Object.keys(simpleUpdate).length > 0) {
@ -1663,7 +1664,9 @@ export class ProductService {
rows.push(rowData.join(',')); rows.push(rowData.join(','));
} }
return rows.join('\n'); // 添加UTF-8 BOM以确保中文在Excel中正确显示
return '\ufeff' + rows.join('\n');
} }
async getRecordsFromTable(file: any) { async getRecordsFromTable(file: any) {
// 解析文件(使用 xlsx 包自动识别文件类型并解析) // 解析文件(使用 xlsx 包自动识别文件类型并解析)
@ -1686,7 +1689,8 @@ export class ProductService {
let records: any[] = [] let records: any[] = []
// xlsx 包会自动根据文件内容识别文件类型(CSV 或 XLSX) // 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]]; const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// 将工作表转换为 JSON 数组 // 将工作表转换为 JSON 数组