feat: 添加少许特性 #50

Merged
zhuotianyuan merged 2 commits from zksu/API:main into main 2026-01-14 11:45:15 +00:00
14 changed files with 1320 additions and 303 deletions

View File

@ -21,13 +21,16 @@ import {
CreateReviewDTO, CreateReviewDTO,
CreateVariationDTO, CreateVariationDTO,
UpdateReviewDTO, UpdateReviewDTO,
OrderPaymentStatus,
} from '../dto/site-api.dto'; } from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
import { import {
ShopyyAllProductQuery, ShopyyAllProductQuery,
ShopyyCustomer, ShopyyCustomer,
ShopyyOrder, ShopyyOrder,
ShopyyOrderCreateParams,
ShopyyOrderQuery, ShopyyOrderQuery,
ShopyyOrderUpdateParams,
ShopyyProduct, ShopyyProduct,
ShopyyProductQuery, ShopyyProductQuery,
ShopyyVariant, ShopyyVariant,
@ -38,15 +41,15 @@ import {
} from '../enums/base.enum'; } from '../enums/base.enum';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
export class ShopyyAdapter implements ISiteAdapter { export class ShopyyAdapter implements ISiteAdapter {
shopyyFinancialStatusMap= { shopyyFinancialStatusMap = {
'200': '待支付', '200': '待支付',
'210': "支付中", '210': "支付中",
'220':"部分支付", '220': "部分支付",
'230':"已支付", '230': "已支付",
'240':"支付失败", '240': "支付失败",
'250':"部分退款", '250': "部分退款",
'260':"已退款", '260': "已退款",
'290':"已取消", '290': "已取消",
} }
constructor(private site: any, private shopyyService: ShopyyService) { constructor(private site: any, private shopyyService: ShopyyService) {
this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this);
@ -123,8 +126,8 @@ export class ShopyyAdapter implements ISiteAdapter {
return data return data
} }
async getCustomer(where: {id?: string | number,email?: string,phone?: string}): Promise<UnifiedCustomerDTO> { async getCustomer(where: { id?: string | number, email?: string, phone?: string }): Promise<UnifiedCustomerDTO> {
if(!where.id && !where.email && !where.phone){ if (!where.id && !where.email && !where.phone) {
throw new Error('必须传入 id 或 email 或 phone') throw new Error('必须传入 id 或 email 或 phone')
} }
const customer = await this.shopyyService.getCustomer(this.site, where.id); const customer = await this.shopyyService.getCustomer(this.site, where.id);
@ -153,12 +156,12 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedCustomer(createdCustomer); return this.mapPlatformToUnifiedCustomer(createdCustomer);
} }
async updateCustomer(where: {id: string | number}, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> { async updateCustomer(where: { id: string | number }, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
const updatedCustomer = await this.shopyyService.updateCustomer(this.site, where.id, data); const updatedCustomer = await this.shopyyService.updateCustomer(this.site, where.id, data);
return this.mapPlatformToUnifiedCustomer(updatedCustomer); return this.mapPlatformToUnifiedCustomer(updatedCustomer);
} }
async deleteCustomer(where: {id: string | number}): Promise<boolean> { async deleteCustomer(where: { id: string | number }): Promise<boolean> {
return await this.shopyyService.deleteCustomer(this.site, where.id); return await this.shopyyService.deleteCustomer(this.site, where.id);
} }
@ -212,12 +215,12 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedMedia(createdMedia); return this.mapPlatformToUnifiedMedia(createdMedia);
} }
async updateMedia(where: {id: string | number}, data: any): Promise<UnifiedMediaDTO> { async updateMedia(where: { id: string | number }, data: any): Promise<UnifiedMediaDTO> {
const updatedMedia = await this.shopyyService.updateMedia(this.site, where.id, data); const updatedMedia = await this.shopyyService.updateMedia(this.site, where.id, data);
return this.mapPlatformToUnifiedMedia(updatedMedia); return this.mapPlatformToUnifiedMedia(updatedMedia);
} }
async deleteMedia(where: {id: string | number}): Promise<boolean> { async deleteMedia(where: { id: string | number }): Promise<boolean> {
return await this.shopyyService.deleteMedia(this.site, where.id); return await this.shopyyService.deleteMedia(this.site, where.id);
} }
@ -228,10 +231,10 @@ export class ShopyyAdapter implements ISiteAdapter {
// ========== 订单映射方法 ========== // ========== 订单映射方法 ==========
mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO { mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO {
// console.log(item) // console.log(item)
if(!item) throw new Error('订单数据不能为空') if (!item) throw new Error('订单数据不能为空')
// 提取账单和送货地址 如果不存在则为空对象 // 提取账单和送货地址 如果不存在则为空对象
const billing = (item).bill_address || {}; const billing = item.billing_address || {};
const shipping = (item as any).shipping_address || {}; const shipping = item.shipping_address || {};
// 构建账单地址对象 // 构建账单地址对象
const billingObj: UnifiedAddressDTO = { const billingObj: UnifiedAddressDTO = {
@ -309,14 +312,14 @@ export class ShopyyAdapter implements ISiteAdapter {
}; };
const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map( const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map(
(p: any) => ({ (product) => ({
id: p.id, id: product.id,
name: p.product_title || p.name, name: product.product_title || product.name,
product_id: p.product_id, product_id: product.product_id,
quantity: p.quantity, quantity: product.quantity,
total: String(p.price ?? ''), total: String(product.price ?? ''),
sku: p.sku_code || '', sku: product.sku || product.sku_code || '',
price: String(p.price ?? ''), price: String(product.price ?? ''),
}) })
); );
// 货币符号 // 货币符号
@ -334,12 +337,12 @@ export class ShopyyAdapter implements ISiteAdapter {
'SGD': 'S$' 'SGD': 'S$'
// 可以根据需要添加更多货币代码和符号 // 可以根据需要添加更多货币代码和符号
}; };
// 映射订单状态,如果不存在则默认 pending // 映射订单状态,如果不存在则默认 pending
const status = this.shopyyOrderStatusMap[item.status ?? item.order_status] || OrderStatus.PENDING; const status = this.shopyyOrderStatusMap[item.status ?? item.order_status] || OrderStatus.PENDING;
const finalcial_status = this.shopyyFinancialStatusMap[item.financial_status] const finalcial_status = this.shopyyFinancialStatusMap[item.financial_status]
// 发货状态 // 发货状态
const fulfillment_status = this.shopyyFulfillmentStatusMap[item.fulfillment_status]; const fulfillment_status = this.fulfillmentStatusMap[item.fulfillment_status];
return { return {
id: item.id || item.order_id, id: item.id || item.order_id,
number: item.order_number || item.order_sn, number: item.order_number || item.order_sn,
@ -388,7 +391,7 @@ export class ShopyyAdapter implements ISiteAdapter {
tracking_number: f.tracking_number || '', tracking_number: f.tracking_number || '',
shipping_provider: f.tracking_company || '', shipping_provider: f.tracking_company || '',
shipping_method: f.tracking_company || '', shipping_method: f.tracking_company || '',
date_created: typeof f.created_at === 'number' date_created: typeof f.created_at === 'number'
? new Date(f.created_at * 1000).toISOString() ? new Date(f.created_at * 1000).toISOString()
: f.created_at || '', : f.created_at || '',
@ -402,11 +405,11 @@ export class ShopyyAdapter implements ISiteAdapter {
return data return data
} }
mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): any { mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): ShopyyOrderCreateParams {
return data return data
} }
mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): any { mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): ShopyyOrderUpdateParams {
// 构建 ShopYY 订单更新参数(仅包含传入的字段) // 构建 ShopYY 订单更新参数(仅包含传入的字段)
const params: any = {}; const params: any = {};
@ -536,9 +539,17 @@ export class ShopyyAdapter implements ISiteAdapter {
return params; return params;
} }
async getOrder(where: {id: string | number}): Promise<UnifiedOrderDTO> { async getOrder(where: { id: string | number }): Promise<UnifiedOrderDTO> {
const data = await this.shopyyService.getOrder(this.site.id, String(where.id)); const data = await this.getOrders({
return this.mapPlatformToUnifiedOrder(data); where: {
id: where.id,
},
page: 1,
per_page: 1,
})
return data.items[0] || null
// const data = await this.shopyyService.getOrder(this.site.id, String(where.id));
// return this.mapPlatformToUnifiedOrder(data);
} }
async getOrders( async getOrders(
@ -565,7 +576,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return data.map(this.mapPlatformToUnifiedOrder.bind(this)); return data.map(this.mapPlatformToUnifiedOrder.bind(this));
} }
async countOrders(where: Record<string,any>): Promise<number> { async countOrders(where: Record<string, any>): Promise<number> {
// 使用最小分页只获取总数 // 使用最小分页只获取总数
const searchParams = { const searchParams = {
where, where,
@ -583,13 +594,13 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedOrder(createdOrder); return this.mapPlatformToUnifiedOrder(createdOrder);
} }
async updateOrder(where: {id: string | number}, data: Partial<UnifiedOrderDTO>): Promise<boolean> { async updateOrder(where: { id: string | number }, data: Partial<UnifiedOrderDTO>): Promise<boolean> {
// 使用映射方法转换参数 // 使用映射方法转换参数
const requestParams = this.mapUpdateOrderParams(data); const requestParams = this.mapUpdateOrderParams(data);
return await this.shopyyService.updateOrder(this.site, String(where.id), requestParams); return await this.shopyyService.updateOrder(this.site, String(where.id), requestParams);
} }
async deleteOrder(where: {id: string | number}): Promise<boolean> { async deleteOrder(where: { id: string | number }): Promise<boolean> {
return await this.shopyyService.deleteOrder(this.site, where.id); return await this.shopyyService.deleteOrder(this.site, where.id);
} }
@ -662,7 +673,7 @@ export class ShopyyAdapter implements ISiteAdapter {
mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial<ShopyyOrderQuery> { mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial<ShopyyOrderQuery> {
// 首先使用通用参数转换 // 首先使用通用参数转换
const baseParams = this.mapSearchParams(params); const baseParams = this.mapSearchParams(params);
// 订单状态映射 // 订单状态映射
const statusMap = { const statusMap = {
'pending': '100', // 100 未完成 'pending': '100', // 100 未完成
@ -670,7 +681,7 @@ export class ShopyyAdapter implements ISiteAdapter {
'completed': "180", // 180 已完成(确认收货) 'completed': "180", // 180 已完成(确认收货)
'cancelled': '190', // 190 取消 'cancelled': '190', // 190 取消
}; };
// 如果有状态参数,进行特殊映射 // 如果有状态参数,进行特殊映射
if (baseParams.status) { if (baseParams.status) {
const unifiedStatus = baseParams.status const unifiedStatus = baseParams.status
@ -678,13 +689,13 @@ export class ShopyyAdapter implements ISiteAdapter {
baseParams.status = statusMap[unifiedStatus]; baseParams.status = statusMap[unifiedStatus];
} }
} }
// 处理ID参数 // 处理ID参数
if (baseParams.id) { if (baseParams.id) {
baseParams.ids = baseParams.id; baseParams.ids = baseParams.id;
delete baseParams.id; delete baseParams.id;
} }
return baseParams; return baseParams;
} }
@ -699,7 +710,7 @@ export class ShopyyAdapter implements ISiteAdapter {
name: item.name || item.title, name: item.name || item.title,
type: String(item.product_type ?? ''), type: String(item.product_type ?? ''),
status: mapProductStatus(item.status), status: mapProductStatus(item.status),
sku: item.variant?.sku || '', sku: item.variant?.sku || item.variant?.sku_code || '',
regular_price: String(item.variant?.price ?? ''), regular_price: String(item.variant?.price ?? ''),
sale_price: String(item.special_price ?? ''), sale_price: String(item.special_price ?? ''),
price: String(item.price ?? ''), price: String(item.price ?? ''),
@ -728,7 +739,7 @@ export class ShopyyAdapter implements ISiteAdapter {
name: c.title || '', name: c.title || '',
})), })),
variations: item.variants?.map(this.mapPlatformToUnifiedVariation.bind(this)) || [], variations: item.variants?.map(this.mapPlatformToUnifiedVariation.bind(this)) || [],
permalink: `${this.site.websiteUrl}/products/${item.handle}`, permalink: `${this.site.websiteUrl}/products/${item.handle}`,
date_created: date_created:
typeof item.created_at === 'number' typeof item.created_at === 'number'
? new Date(item.created_at * 1000).toISOString() ? new Date(item.created_at * 1000).toISOString()
@ -864,8 +875,8 @@ export class ShopyyAdapter implements ISiteAdapter {
return params; return params;
} }
async getProduct(where: {id?: string | number, sku?: string}): Promise<UnifiedProductDTO> { async getProduct(where: { id?: string | number, sku?: string }): Promise<UnifiedProductDTO> {
if(!where.id && !where.sku){ if (!where.id && !where.sku) {
throw new Error('必须传入 id 或 sku') throw new Error('必须传入 id 或 sku')
} }
if (where.id) { if (where.id) {
@ -901,11 +912,11 @@ export class ShopyyAdapter implements ISiteAdapter {
per_page, per_page,
}; };
} }
mapAllProductParams(params: UnifiedSearchParamsDTO): Partial<ShopyyAllProductQuery>{ mapAllProductParams(params: UnifiedSearchParamsDTO): Partial<ShopyyAllProductQuery> {
const mapped = { const mapped = {
...params.where, ...params.where,
} as any } as any
if(params.per_page){mapped.limit = params.per_page} if (params.per_page) { mapped.limit = params.per_page }
return mapped return mapped
} }
@ -919,7 +930,7 @@ export class ShopyyAdapter implements ISiteAdapter {
null, null,
requestParams requestParams
); );
if(response.code !==0){ if (response.code !== 0) {
throw new Error(response.msg || '获取产品列表失败') throw new Error(response.msg || '获取产品列表失败')
} }
const { data = [] } = response; const { data = [] } = response;
@ -934,7 +945,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedProduct(res); return this.mapPlatformToUnifiedProduct(res);
} }
async updateProduct(where: {id?: string | number, sku?: string}, data: Partial<UnifiedProductDTO>): Promise<boolean> { async updateProduct(where: { id?: string | number, sku?: string }, data: Partial<UnifiedProductDTO>): Promise<boolean> {
let productId: string; let productId: string;
if (where.id) { if (where.id) {
productId = String(where.id); productId = String(where.id);
@ -951,7 +962,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return true; return true;
} }
async deleteProduct(where: {id?: string | number, sku?: string}): Promise<boolean> { async deleteProduct(where: { id?: string | number, sku?: string }): Promise<boolean> {
let productId: string | number; let productId: string | number;
if (where.id) { if (where.id) {
productId = where.id; productId = where.id;
@ -966,11 +977,11 @@ export class ShopyyAdapter implements ISiteAdapter {
await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] }); await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] });
return true; return true;
} }
// 通过sku获取产品详情的私有方法 // 通过sku获取产品详情的私有方法
private async getProductBySku(sku: string): Promise<UnifiedProductDTO> { private async getProductBySku(sku: string): Promise<UnifiedProductDTO> {
// 使用Shopyy API的搜索功能通过sku查询产品 // 使用Shopyy API的搜索功能通过sku查询产品
const response = await this.getAllProducts({ where: {sku} }); const response = await this.getAllProducts({ where: { sku } });
console.log('getProductBySku', response) console.log('getProductBySku', response)
const product = response?.[0] const product = response?.[0]
if (!product) { if (!product) {
@ -1034,12 +1045,12 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedReview(createdReview); return this.mapPlatformToUnifiedReview(createdReview);
} }
async updateReview(where: {id: string | number}, data: any): Promise<UnifiedReviewDTO> { async updateReview(where: { id: string | number }, data: any): Promise<UnifiedReviewDTO> {
const updatedReview = await this.shopyyService.updateReview(this.site, where.id, data); const updatedReview = await this.shopyyService.updateReview(this.site, where.id, data);
return this.mapPlatformToUnifiedReview(updatedReview); return this.mapPlatformToUnifiedReview(updatedReview);
} }
async deleteReview(where: {id: string | number}): Promise<boolean> { async deleteReview(where: { id: string | number }): Promise<boolean> {
return await this.shopyyService.deleteReview(this.site, where.id); return await this.shopyyService.deleteReview(this.site, where.id);
} }
@ -1101,10 +1112,11 @@ export class ShopyyAdapter implements ISiteAdapter {
// ========== 产品变体映射方法 ========== // ========== 产品变体映射方法 ==========
mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO {
// 映射变体 // 映射变体
console.log('ivarianttem', variant)
return { return {
id: variant.id, id: variant.id,
name: variant.sku || '', name: variant.title || '',
sku: variant.sku || '', sku: variant.sku || variant.sku_code || '',
regular_price: String(variant.price ?? ''), regular_price: String(variant.price ?? ''),
sale_price: String(variant.special_price ?? ''), sale_price: String(variant.special_price ?? ''),
price: String(variant.price ?? ''), price: String(variant.price ?? ''),
@ -1195,7 +1207,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return data return data
} }
async getWebhook(where: {id: string | number}): Promise<UnifiedWebhookDTO> { async getWebhook(where: { id: string | number }): Promise<UnifiedWebhookDTO> {
const webhook = await this.shopyyService.getWebhook(this.site, where.id); const webhook = await this.shopyyService.getWebhook(this.site, where.id);
return this.mapPlatformToUnifiedWebhook(webhook); return this.mapPlatformToUnifiedWebhook(webhook);
} }
@ -1221,12 +1233,12 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedWebhook(createdWebhook); return this.mapPlatformToUnifiedWebhook(createdWebhook);
} }
async updateWebhook(where: {id: string | number}, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO> { async updateWebhook(where: { id: string | number }, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO> {
const updatedWebhook = await this.shopyyService.updateWebhook(this.site, where.id, data); const updatedWebhook = await this.shopyyService.updateWebhook(this.site, where.id, data);
return this.mapPlatformToUnifiedWebhook(updatedWebhook); return this.mapPlatformToUnifiedWebhook(updatedWebhook);
} }
async deleteWebhook(where: {id: string | number}): Promise<boolean> { async deleteWebhook(where: { id: string | number }): Promise<boolean> {
return await this.shopyyService.deleteWebhook(this.site, where.id); return await this.shopyyService.deleteWebhook(this.site, where.id);
} }
@ -1269,14 +1281,14 @@ export class ShopyyAdapter implements ISiteAdapter {
// 处理分页参数 // 处理分页参数
const page = Number(params.page || 1); const page = Number(params.page || 1);
const limit = Number(params.per_page ?? 20); const limit = Number(params.per_page ?? 20);
// 处理 where 条件 // 处理 where 条件
const query: any = { const query: any = {
...(params.where || {}), ...(params.where || {}),
page, page,
limit, limit,
} }
if(params.orderBy){ if (params.orderBy) {
const [field, dir] = Object.entries(params.orderBy)[0]; const [field, dir] = Object.entries(params.orderBy)[0];
query.order_by = dir === 'desc' ? 'desc' : 'asc'; query.order_by = dir === 'desc' ? 'desc' : 'asc';
query.order_field = field query.order_field = field
@ -1286,24 +1298,24 @@ export class ShopyyAdapter implements ISiteAdapter {
// 映射产品状态: publish -> 1, draft -> 0 // 映射产品状态: publish -> 1, draft -> 0
mapStatus = (status: string) => { mapStatus = (status: string) => {
return status === 'publish' ? 1 : 0; return status === 'publish' ? 1 : 0;
}; };
// 映射库存状态: instock -> 1, outofstock -> 0 // 映射库存状态: instock -> 1, outofstock -> 0
mapStockStatus = (stockStatus: string) => { mapStockStatus = (stockStatus: string) => {
return stockStatus === 'instock' ? 1 : 0; return stockStatus === 'instock' ? 1 : 0;
}; };
shopyyOrderStatusMap = {//订单状态 100 未完成110 待处理180 已完成(确认收货); 190 取消; shopyyOrderStatusMap = {//订单状态 100 未完成110 待处理180 已完成(确认收货); 190 取消;
[100]: OrderStatus.PENDING, // 100 未完成 转为 pending [100]: OrderStatus.PENDING, // 100 未完成 转为 pending
[110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing [110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing
// 已发货 // 已发货
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
[190]: OrderStatus.CANCEL // 190 取消 转为 cancelled [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled
} }
// 物流状态 300 未发货310 部分发货320 已发货330(确认收货)
shopyyFulfillmentStatusMap = { fulfillmentStatusMap = {
// 未发货 // 未发货
'300': OrderFulfillmentStatus.PENDING, '300': OrderFulfillmentStatus.PENDING,
// 部分发货 // 部分发货
@ -1314,4 +1326,23 @@ export class ShopyyAdapter implements ISiteAdapter {
'330': OrderFulfillmentStatus.CANCELLED, '330': OrderFulfillmentStatus.CANCELLED,
// 确认发货 // 确认发货
} }
// 支付状态 200 待支付210 支付中220 部分支付230 已支付240 支付失败250 部分退款260 已退款 290 已取消;
financialStatusMap = {
// 待支付
'200': OrderPaymentStatus.PENDING,
// 支付中
'210': OrderPaymentStatus.PAYING,
// 部分支付
'220': OrderPaymentStatus.PARTIALLY_PAID,
// 已支付
'230': OrderPaymentStatus.PAID,
// 支付失败
'240': OrderPaymentStatus.FAILED,
// 部分退款
'250': OrderPaymentStatus.PARTIALLY_REFUNDED,
// 已退款
'260': OrderPaymentStatus.REFUNDED,
// 已取消
'290': OrderPaymentStatus.CANCELLED,
}
} }

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

@ -117,7 +117,7 @@ export class ProductController {
const file = files?.[0]; const file = files?.[0];
if (!file) return errorResponse('未接收到上传文件'); if (!file) return errorResponse('未接收到上传文件');
const result = await this.productService.importProductsCSV(file); const result = await this.productService.importProductsFromTable(file);
return successResponse(result); return successResponse(result);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);

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[];

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,24 @@ export enum OrderFulfillmentStatus {
// 确认发货 // 确认发货
CONFIRMED, CONFIRMED,
} }
export enum OrderPaymentStatus {
// 待支付
PENDING,
// 支付中
PAYING,
// 部分支付
PARTIALLY_PAID,
// 已支付
PAID,
// 支付失败
FAILED,
// 部分退款
PARTIALLY_REFUNDED,
// 已退款
REFUNDED,
// 已取消
CANCELLED,
}
// //
export class UnifiedProductWhere { export class UnifiedProductWhere {
sku?: string; sku?: string;

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 })
@ -85,7 +90,7 @@ export class OrderSale {
@ApiProperty({name: '强度', nullable: true }) @ApiProperty({name: '强度', nullable: true })
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
strength: string | null; strength: string | null;
@ApiProperty({ description: '版本', type: 'string', nullable: true }) @ApiProperty({ description: '版本', type: 'string', nullable: true })
@Expose() @Expose()

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

@ -15,7 +15,7 @@ export class ProductStockComponent {
@ApiProperty({ description: '组件所关联的 SKU', type: 'string' }) @ApiProperty({ description: '组件所关联的 SKU', type: 'string' })
@Column({ type: 'varchar', length: 64 }) @Column({ type: 'varchar', length: 64 })
sku: string; sku: string;
@ApiProperty({ type: Number, description: '组成数量' }) @ApiProperty({ type: Number, description: '组成数量' })
@Column({ type: 'int', default: 1 }) @Column({ type: 'int', default: 1 })
quantity: number; quantity: number;

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') {
@ -60,7 +60,7 @@ export class DictService {
// 如果是 Buffer直接使用 // 如果是 Buffer直接使用
buffer = bufferOrPath; buffer = bufferOrPath;
} }
// 读取缓冲区中的工作簿 // 读取缓冲区中的工作簿
const wb = xlsx.read(buffer, { type: 'buffer' }); const wb = xlsx.read(buffer, { type: 'buffer' });
// 获取第一个工作表的名称 // 获取第一个工作表的名称
@ -93,7 +93,7 @@ export class DictService {
// 从XLSX文件导入字典项 // 从XLSX文件导入字典项
async importDictItemsFromXLSX(bufferOrPath: Buffer | string, dictId: number): Promise<BatchOperationResultDTO> { async importDictItemsFromXLSX(bufferOrPath: Buffer | string, dictId: number): Promise<BatchOperationResultDTO> {
if(!dictId){ if (!dictId) {
throw new Error("引入失败, 请输入字典 ID") throw new Error("引入失败, 请输入字典 ID")
} }
@ -101,7 +101,7 @@ export class DictService {
if (!dict) { if (!dict) {
throw new Error('指定的字典不存在'); throw new Error('指定的字典不存在');
} }
// 判断传入的是 Buffer 还是文件路径字符串 // 判断传入的是 Buffer 还是文件路径字符串
let buffer: Buffer; let buffer: Buffer;
if (typeof bufferOrPath === 'string') { if (typeof bufferOrPath === 'string') {
@ -111,7 +111,7 @@ export class DictService {
// 如果是 Buffer直接使用 // 如果是 Buffer直接使用
buffer = bufferOrPath; buffer = bufferOrPath;
} }
const wb = xlsx.read(buffer, { type: 'buffer' }); const wb = xlsx.read(buffer, { type: 'buffer' });
const wsname = wb.SheetNames[0]; const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname]; const ws = wb.Sheets[wsname];
@ -122,7 +122,7 @@ export class DictService {
const createdItems = []; const createdItems = [];
const updatedItems = []; const updatedItems = [];
const errors = []; const errors = [];
for (const row of data) { for (const row of data) {
try { try {
const result = await this.upsertDictItem(dictId, { const result = await this.upsertDictItem(dictId, {
@ -150,7 +150,7 @@ export class DictService {
const processed = createdItems.length + updatedItems.length; const processed = createdItems.length + updatedItems.length;
return { return {
total: data.length, total: data.length,
processed: processed, processed: processed,
updated: updatedItems.length, updated: updatedItems.length,
@ -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'] });
} }
// 创建新字典项 // 创建新字典项
@ -251,7 +251,7 @@ export class DictService {
}) { }) {
// 格式化 name // 格式化 name
const formattedName = this.formatName(itemData.name); const formattedName = this.formatName(itemData.name);
// 查找是否已存在该字典项(根据 name 和 dictId) // 查找是否已存在该字典项(根据 name 和 dictId)
const existingItem = await this.dictItemModel.findOne({ const existingItem = await this.dictItemModel.findOne({
where: { where: {

View File

@ -1,6 +1,6 @@
import { Inject, Provide } from '@midwayjs/core'; import { Inject, Provide } from '@midwayjs/core';
import { parse } from 'csv-parse';
import * as fs from 'fs'; import * as fs from 'fs';
import * as xlsx from 'xlsx';
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 { PaginationParams } from '../interface'; import { PaginationParams } from '../interface';
@ -238,18 +238,18 @@ export class ProductService {
// 处理分页参数(支持新旧两种格式) // 处理分页参数(支持新旧两种格式)
const page = query.page || 1; const page = query.page || 1;
const pageSize = query.per_page || 10; const pageSize = query.per_page || 10;
// 处理搜索参数 // 处理搜索参数
const name = query.where?.name || query.search || ''; const name = query.where?.name || query.search || '';
// 处理品牌过滤 // 处理品牌过滤
const brandId = query.where?.brandId; const brandId = query.where?.brandId;
const brandIds = query.where?.brandIds; const brandIds = query.where?.brandIds;
// 处理分类过滤 // 处理分类过滤
const categoryId = query.where?.categoryId; const categoryId = query.where?.categoryId;
const categoryIds = query.where?.categoryIds; const categoryIds = query.where?.categoryIds;
// 处理排序参数 // 处理排序参数
const orderBy = query.orderBy; const orderBy = query.orderBy;
@ -270,17 +270,17 @@ export class ProductService {
if (query.where?.id) { if (query.where?.id) {
qb.andWhere('product.id = :id', { id: query.where.id }); qb.andWhere('product.id = :id', { id: query.where.id });
} }
// 处理产品ID列表过滤 // 处理产品ID列表过滤
if (query.where?.ids && query.where.ids.length > 0) { if (query.where?.ids && query.where.ids.length > 0) {
qb.andWhere('product.id IN (:...ids)', { ids: query.where.ids }); qb.andWhere('product.id IN (:...ids)', { ids: query.where.ids });
} }
// 处理where对象中的id过滤 // 处理where对象中的id过滤
if (query.where?.id) { if (query.where?.id) {
qb.andWhere('product.id = :whereId', { whereId: query.where.id }); qb.andWhere('product.id = :whereId', { whereId: query.where.id });
} }
// 处理where对象中的ids过滤 // 处理where对象中的ids过滤
if (query.where?.ids && query.where.ids.length > 0) { if (query.where?.ids && query.where.ids.length > 0) {
qb.andWhere('product.id IN (:...whereIds)', { whereIds: query.where.ids }); qb.andWhere('product.id IN (:...whereIds)', { whereIds: query.where.ids });
@ -290,17 +290,17 @@ export class ProductService {
if (query.where?.sku) { if (query.where?.sku) {
qb.andWhere('product.sku = :sku', { sku: query.where.sku }); qb.andWhere('product.sku = :sku', { sku: query.where.sku });
} }
// 处理SKU列表过滤 // 处理SKU列表过滤
if (query.where?.skus && query.where.skus.length > 0) { if (query.where?.skus && query.where.skus.length > 0) {
qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus }); qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus });
} }
// 处理where对象中的sku过滤 // 处理where对象中的sku过滤
if (query.where?.sku) { if (query.where?.sku) {
qb.andWhere('product.sku = :whereSku', { whereSku: query.where.sku }); qb.andWhere('product.sku = :whereSku', { whereSku: query.where.sku });
} }
// 处理where对象中的skus过滤 // 处理where对象中的skus过滤
if (query.where?.skus && query.where.skus.length > 0) { if (query.where?.skus && query.where.skus.length > 0) {
qb.andWhere('product.sku IN (:...whereSkus)', { whereSkus: query.where.skus }); qb.andWhere('product.sku IN (:...whereSkus)', { whereSkus: query.where.skus });
@ -315,7 +315,7 @@ export class ProductService {
if (query.where?.type) { if (query.where?.type) {
qb.andWhere('product.type = :type', { type: query.where.type }); qb.andWhere('product.type = :type', { type: query.where.type });
} }
// 处理where对象中的type过滤 // 处理where对象中的type过滤
if (query.where?.type) { if (query.where?.type) {
qb.andWhere('product.type = :whereType', { whereType: query.where.type }); qb.andWhere('product.type = :whereType', { whereType: query.where.type });
@ -325,16 +325,16 @@ export class ProductService {
if (query.where?.minPrice !== undefined) { if (query.where?.minPrice !== undefined) {
qb.andWhere('product.price >= :minPrice', { minPrice: query.where.minPrice }); qb.andWhere('product.price >= :minPrice', { minPrice: query.where.minPrice });
} }
if (query.where?.maxPrice !== undefined) { if (query.where?.maxPrice !== undefined) {
qb.andWhere('product.price <= :maxPrice', { maxPrice: query.where.maxPrice }); qb.andWhere('product.price <= :maxPrice', { maxPrice: query.where.maxPrice });
} }
// 处理where对象中的价格范围过滤 // 处理where对象中的价格范围过滤
if (query.where?.minPrice !== undefined) { if (query.where?.minPrice !== undefined) {
qb.andWhere('product.price >= :whereMinPrice', { whereMinPrice: query.where.minPrice }); qb.andWhere('product.price >= :whereMinPrice', { whereMinPrice: query.where.minPrice });
} }
if (query.where?.maxPrice !== undefined) { if (query.where?.maxPrice !== undefined) {
qb.andWhere('product.price <= :whereMaxPrice', { whereMaxPrice: query.where.maxPrice }); qb.andWhere('product.price <= :whereMaxPrice', { whereMaxPrice: query.where.maxPrice });
} }
@ -343,16 +343,16 @@ export class ProductService {
if (query.where?.minPromotionPrice !== undefined) { if (query.where?.minPromotionPrice !== undefined) {
qb.andWhere('product.promotionPrice >= :minPromotionPrice', { minPromotionPrice: query.where.minPromotionPrice }); qb.andWhere('product.promotionPrice >= :minPromotionPrice', { minPromotionPrice: query.where.minPromotionPrice });
} }
if (query.where?.maxPromotionPrice !== undefined) { if (query.where?.maxPromotionPrice !== undefined) {
qb.andWhere('product.promotionPrice <= :maxPromotionPrice', { maxPromotionPrice: query.where.maxPromotionPrice }); qb.andWhere('product.promotionPrice <= :maxPromotionPrice', { maxPromotionPrice: query.where.maxPromotionPrice });
} }
// 处理where对象中的促销价格范围过滤 // 处理where对象中的促销价格范围过滤
if (query.where?.minPromotionPrice !== undefined) { if (query.where?.minPromotionPrice !== undefined) {
qb.andWhere('product.promotionPrice >= :whereMinPromotionPrice', { whereMinPromotionPrice: query.where.minPromotionPrice }); qb.andWhere('product.promotionPrice >= :whereMinPromotionPrice', { whereMinPromotionPrice: query.where.minPromotionPrice });
} }
if (query.where?.maxPromotionPrice !== undefined) { if (query.where?.maxPromotionPrice !== undefined) {
qb.andWhere('product.promotionPrice <= :whereMaxPromotionPrice', { whereMaxPromotionPrice: query.where.maxPromotionPrice }); qb.andWhere('product.promotionPrice <= :whereMaxPromotionPrice', { whereMaxPromotionPrice: query.where.maxPromotionPrice });
} }
@ -361,16 +361,16 @@ export class ProductService {
if (query.where?.createdAtStart) { if (query.where?.createdAtStart) {
qb.andWhere('product.createdAt >= :createdAtStart', { createdAtStart: new Date(query.where.createdAtStart) }); qb.andWhere('product.createdAt >= :createdAtStart', { createdAtStart: new Date(query.where.createdAtStart) });
} }
if (query.where?.createdAtEnd) { if (query.where?.createdAtEnd) {
qb.andWhere('product.createdAt <= :createdAtEnd', { createdAtEnd: new Date(query.where.createdAtEnd) }); qb.andWhere('product.createdAt <= :createdAtEnd', { createdAtEnd: new Date(query.where.createdAtEnd) });
} }
// 处理where对象中的创建时间范围过滤 // 处理where对象中的创建时间范围过滤
if (query.where?.createdAtStart) { if (query.where?.createdAtStart) {
qb.andWhere('product.createdAt >= :whereCreatedAtStart', { whereCreatedAtStart: new Date(query.where.createdAtStart) }); qb.andWhere('product.createdAt >= :whereCreatedAtStart', { whereCreatedAtStart: new Date(query.where.createdAtStart) });
} }
if (query.where?.createdAtEnd) { if (query.where?.createdAtEnd) {
qb.andWhere('product.createdAt <= :whereCreatedAtEnd', { whereCreatedAtEnd: new Date(query.where.createdAtEnd) }); qb.andWhere('product.createdAt <= :whereCreatedAtEnd', { whereCreatedAtEnd: new Date(query.where.createdAtEnd) });
} }
@ -379,16 +379,16 @@ export class ProductService {
if (query.where?.updatedAtStart) { if (query.where?.updatedAtStart) {
qb.andWhere('product.updatedAt >= :updatedAtStart', { updatedAtStart: new Date(query.where.updatedAtStart) }); qb.andWhere('product.updatedAt >= :updatedAtStart', { updatedAtStart: new Date(query.where.updatedAtStart) });
} }
if (query.where?.updatedAtEnd) { if (query.where?.updatedAtEnd) {
qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) }); qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) });
} }
// 处理where对象中的更新时间范围过滤 // 处理where对象中的更新时间范围过滤
if (query.where?.updatedAtStart) { if (query.where?.updatedAtStart) {
qb.andWhere('product.updatedAt >= :whereUpdatedAtStart', { whereUpdatedAtStart: new Date(query.where.updatedAtStart) }); qb.andWhere('product.updatedAt >= :whereUpdatedAtStart', { whereUpdatedAtStart: new Date(query.where.updatedAtStart) });
} }
if (query.where?.updatedAtEnd) { if (query.where?.updatedAtEnd) {
qb.andWhere('product.updatedAt <= :whereUpdatedAtEnd', { whereUpdatedAtEnd: new Date(query.where.updatedAtEnd) }); qb.andWhere('product.updatedAt <= :whereUpdatedAtEnd', { whereUpdatedAtEnd: new Date(query.where.updatedAtEnd) });
} }
@ -407,7 +407,7 @@ export class ProductService {
return 'product.id IN ' + subQuery; return 'product.id IN ' + subQuery;
}); });
} }
// 处理品牌ID列表过滤 // 处理品牌ID列表过滤
if (brandIds && brandIds.length > 0) { if (brandIds && brandIds.length > 0) {
qb.andWhere(qb => { qb.andWhere(qb => {
@ -427,17 +427,17 @@ export class ProductService {
if (categoryId) { if (categoryId) {
qb.andWhere('product.categoryId = :categoryId', { categoryId }); qb.andWhere('product.categoryId = :categoryId', { categoryId });
} }
// 处理分类ID列表过滤 // 处理分类ID列表过滤
if (categoryIds && categoryIds.length > 0) { if (categoryIds && categoryIds.length > 0) {
qb.andWhere('product.categoryId IN (:...categoryIds)', { categoryIds }); qb.andWhere('product.categoryId IN (:...categoryIds)', { categoryIds });
} }
// 处理where对象中的分类ID过滤 // 处理where对象中的分类ID过滤
if (query.where?.categoryId) { if (query.where?.categoryId) {
qb.andWhere('product.categoryId = :whereCategoryId', { whereCategoryId: query.where.categoryId }); qb.andWhere('product.categoryId = :whereCategoryId', { whereCategoryId: query.where.categoryId });
} }
// 处理where对象中的分类ID列表过滤 // 处理where对象中的分类ID列表过滤
if (query.where?.categoryIds && query.where.categoryIds.length > 0) { if (query.where?.categoryIds && query.where.categoryIds.length > 0) {
qb.andWhere('product.categoryId IN (:...whereCategoryIds)', { whereCategoryIds: query.where.categoryIds }); qb.andWhere('product.categoryId IN (:...whereCategoryIds)', { whereCategoryIds: query.where.categoryIds });
@ -555,7 +555,7 @@ export class ProductService {
// 如果提供了 categoryId,设置分类 // 如果提供了 categoryId,设置分类
if (categoryId) { if (categoryId) {
categoryItem = await this.categoryModel.findOne({ categoryItem = await this.categoryModel.findOne({
where: { id: categoryId }, where: { id: categoryId },
relations: ['attributes', 'attributes.attributeDict'] relations: ['attributes', 'attributes.attributeDict']
}); });
@ -566,12 +566,12 @@ export class ProductService {
// 如果属性是分类,特殊处理 // 如果属性是分类,特殊处理
if (attr.dictName === 'category') { if (attr.dictName === 'category') {
if (attr.id) { if (attr.id) {
categoryItem = await this.categoryModel.findOne({ categoryItem = await this.categoryModel.findOne({
where: { id: attr.id }, where: { id: attr.id },
relations: ['attributes', 'attributes.attributeDict'] relations: ['attributes', 'attributes.attributeDict']
}); });
} else if (attr.name) { } else if (attr.name) {
categoryItem = await this.categoryModel.findOne({ categoryItem = await this.categoryModel.findOne({
where: { name: attr.name }, where: { name: attr.name },
relations: ['attributes', 'attributes.attributeDict'] relations: ['attributes', 'attributes.attributeDict']
}); });
@ -637,7 +637,7 @@ export class ProductService {
if (sku) { if (sku) {
product.sku = sku; product.sku = sku;
} else { } else {
product.sku = await this.templateService.render('product.sku', {product}); product.sku = await this.templateService.render('product.sku', { product });
} }
const savedProduct = await this.productModel.save(product); const savedProduct = await this.productModel.save(product);
@ -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) {
@ -1440,7 +1441,7 @@ export class ProductService {
// 解析属性字段(分号分隔多值) // 解析属性字段(分号分隔多值)
const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []); const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []);
// 将属性解析为 DTO 输入 // 将属性解析为 DTO 输入
const attributes: any[] = []; const attributes: any[] = [];
@ -1461,16 +1462,16 @@ export class ProductService {
return { return {
sku, sku,
name: val(rec.name), name: val(rec.name),
nameCn: val(rec.nameCn), nameCn: val(rec.nameCn),
description: val(rec.description), description: val(rec.description),
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 siteSkus: rec.siteSkus
? String(rec.siteSkus) ? String(rec.siteSkus)
.split(/[;,]/) // 支持英文分号或英文逗号分隔 .split(/[;,]/) // 支持英文分号或英文逗号分隔
.map(s => s.trim()) .map(s => s.trim())
.filter(Boolean) .filter(Boolean)
: undefined, : undefined,
category, // 添加分类字段 category, // 添加分类字段
@ -1536,10 +1537,10 @@ export class ProductService {
return dto; return dto;
} }
getAttributesObject(attributes:DictItem[]){ getAttributesObject(attributes: DictItem[]) {
if(!attributes) return {} if (!attributes) return {}
const obj:any = {} const obj: any = {}
attributes.forEach(attr=>{ attributes.forEach(attr => {
obj[attr.dict.name] = attr obj[attr.dict.name] = attr
}) })
return obj return obj
@ -1663,59 +1664,60 @@ 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) {
// 从 CSV 导入产品;存在则更新,不存在则创建 // 解析文件(使用 xlsx 包自动识别文件类型并解析)
async importProductsCSV(file: any): Promise<BatchOperationResult> {
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 按表头解析)
let records: any[] = [];
try { try {
records = await new Promise((resolve, reject) => { let buffer: Buffer;
parse(buffer, {
columns: true, // 处理文件输入,获取 buffer
skip_empty_lines: true, if (Buffer.isBuffer(file)) {
trim: true, buffer = file;
bom: true, }
}, (err, data) => { else if (file?.data) {
if (err) { if (typeof file.data === 'string') {
reject(err); buffer = fs.readFileSync(file.data);
} else { } else {
resolve(data); buffer = file.data;
} }
}); } else {
}) throw new Error('无效的文件输入');
}
let records: any[] = []
// xlsx 包会自动根据文件内容识别文件类型(CSV 或 XLSX)
// 添加codepage: 65001以确保正确处理UTF-8编码的中文
const workbook = xlsx.read(buffer, { type: 'buffer', codepage: 65001 });
// 获取第一个工作表
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// 将工作表转换为 JSON 数组
records = xlsx.utils.sheet_to_json(worksheet);
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]));
} }
return records;
} catch (e: any) { } catch (e: any) {
throw new Error(`CSV 解析失败:${e?.message || e}`) throw new Error(`文件解析失败:${e?.message || e}`);
} }
}
// 从 CSV 导入产品;存在则更新,不存在则创建
async importProductsFromTable(file: any): Promise<BatchOperationResult> {
let created = 0; let created = 0;
let updated = 0; let updated = 0;
const errors: BatchErrorItem[] = []; const errors: BatchErrorItem[] = [];
const records = await this.getRecordsFromTable(file);
// 逐条处理记录 // 逐条处理记录
for (const rec of records) { for (const rec of records) {
try { try {
const data = this.transformCsvRecordToData(rec); const data = this.transformCsvRecordToData(rec);
if (!data) { if (!data) {
errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过'}); errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过' });
continue; continue;
} }
const { sku } = data; const { sku } = data;
@ -1735,7 +1737,7 @@ export class ProductService {
updated += 1; updated += 1;
} }
} catch (e: any) { } catch (e: any) {
errors.push({ identifier: '' + rec.sku, error: `产品${rec?.sku}导入失败:${e?.message || String(e)}`}); errors.push({ identifier: '' + rec.sku, error: `产品${rec?.sku}导入失败:${e?.message || String(e)}` });
} }
} }
@ -1749,7 +1751,7 @@ export class ProductService {
.createQueryBuilder('stock') .createQueryBuilder('stock')
.select('DISTINCT(stock.sku)', 'sku') .select('DISTINCT(stock.sku)', 'sku')
.getRawMany(); .getRawMany();
const skus = stockSkus.map(s => s.sku).filter(Boolean); const skus = stockSkus.map(s => s.sku).filter(Boolean);
let added = 0; let added = 0;
const errors: string[] = []; const errors: string[] = [];
@ -1783,7 +1785,7 @@ export class ProductService {
where: { id }, where: { id },
relations: ['category', 'attributes', 'attributes.dict', 'components'] relations: ['category', 'attributes', 'attributes.dict', 'components']
}); });
if (!product) { if (!product) {
throw new Error(`产品 ID ${id} 不存在`); throw new Error(`产品 ID ${id} 不存在`);
} }
@ -1867,7 +1869,7 @@ export class ProductService {
await this.bindSiteSkus(localProduct.id, [unifiedProduct.sku]); await this.bindSiteSkus(localProduct.id, [unifiedProduct.sku]);
return result; return result;
} catch (error) { } catch (error) {
throw new Error(`同步产品到站点失败: ${error?.response?.data?.message??error.message}`); throw new Error(`同步产品到站点失败: ${error?.response?.data?.message ?? error.message}`);
} }
} }
@ -1893,7 +1895,7 @@ export class ProductService {
siteId, siteId,
siteSku: item.siteSku siteSku: item.siteSku
}); });
results.synced++; results.synced++;
results.processed++; results.processed++;
} catch (error) { } catch (error) {
@ -1915,18 +1917,18 @@ export class ProductService {
* @returns * @returns
*/ */
async syncProductFromSite(siteId: number, siteProductId: string | number, sku: string): Promise<any> { async syncProductFromSite(siteId: number, siteProductId: string | number, sku: string): Promise<any> {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const siteProduct = await adapter.getProduct({ id: siteProductId }); const siteProduct = await adapter.getProduct({ id: siteProductId });
// 从站点获取产品信息 // 从站点获取产品信息
if (!siteProduct) { if (!siteProduct) {
throw new Error(`站点产品 ID ${siteProductId} 不存在`); throw new Error(`站点产品 ID ${siteProductId} 不存在`);
} }
// 将站点产品转换为本地产品格式 // 将站点产品转换为本地产品格式
const productData = await this.mapUnifiedToLocalProduct(siteProduct); const productData = await this.mapUnifiedToLocalProduct(siteProduct);
return await this.upsertProduct({sku}, productData); return await this.upsertProduct({ sku }, productData);
} }
async upsertProduct(where: Partial<Pick<Product,'id'| 'sku'>>, productData: any) { async upsertProduct(where: Partial<Pick<Product, 'id' | 'sku'>>, productData: any) {
const existingProduct = await this.productModel.findOne({ where: where}); const existingProduct = await this.productModel.findOne({ where: where });
if (existingProduct) { if (existingProduct) {
// 更新现有产品 // 更新现有产品
const updateData: UpdateProductDTO = productData; const updateData: UpdateProductDTO = productData;
@ -1944,7 +1946,7 @@ export class ProductService {
* @param siteProductIds ID数组 * @param siteProductIds ID数组
* @returns * @returns
*/ */
async batchSyncFromSite(siteId: number, data: Array<{siteProductId:string, sku: string}>): Promise<{ synced: number, errors: string[] }> { async batchSyncFromSite(siteId: number, data: Array<{ siteProductId: string, sku: string }>): Promise<{ synced: number, errors: string[] }> {
const results = { const results = {
synced: 0, synced: 0,
errors: [] errors: []
@ -2030,13 +2032,13 @@ export class ProductService {
* @param localProduct * @param localProduct
* @returns * @returns
*/ */
private async mapLocalToUnifiedProduct(localProduct: Product,siteSku?: string): Promise<Partial<UnifiedProductDTO>> { private async mapLocalToUnifiedProduct(localProduct: Product, siteSku?: string): Promise<Partial<UnifiedProductDTO>> {
const tags = localProduct.attributes?.map(a => ({name: a.name})) || []; const tags = localProduct.attributes?.map(a => ({ name: a.name })) || [];
// 将本地产品数据转换为UnifiedProductDTO格式 // 将本地产品数据转换为UnifiedProductDTO格式
const unifiedProduct: any = { const unifiedProduct: any = {
id: localProduct.id ? String(localProduct.id) : undefined, // 如果产品已存在使用现有ID id: localProduct.id ? String(localProduct.id) : undefined, // 如果产品已存在使用现有ID
name: localProduct.name, name: localProduct.name,
type: localProduct.type === 'single'? 'simple' : 'bundle', // 默认类型,可以根据实际需要调整 type: localProduct.type === 'single' ? 'simple' : 'bundle', // 默认类型,可以根据实际需要调整
status: 'publish', // 默认状态,可以根据实际需要调整 status: 'publish', // 默认状态,可以根据实际需要调整
sku: siteSku || await this.templateService.render('site.product.sku', { product: localProduct, sku: localProduct.sku }), sku: siteSku || await this.templateService.render('site.product.sku', { product: localProduct, sku: localProduct.sku }),
regular_price: String(localProduct.price || 0), regular_price: String(localProduct.price || 0),

View File

@ -8,7 +8,7 @@ import * as FormData from 'form-data';
import { SiteService } from './site.service'; import { SiteService } from './site.service';
import { Site } from '../entity/site.entity'; import { Site } from '../entity/site.entity';
import { UnifiedReviewDTO } from '../dto/site-api.dto'; import { UnifiedReviewDTO } from '../dto/site-api.dto';
import { ShopyyReview } from '../dto/shopyy.dto'; import { ShopyyGetOneOrderResult, ShopyyReview } from '../dto/shopyy.dto';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import { UnifiedSearchParamsDTO } from '../dto/api.dto'; import { UnifiedSearchParamsDTO } from '../dto/api.dto';
/** /**
@ -366,7 +366,7 @@ export class ShopyyService {
* @param orderId ID * @param orderId ID
* @returns * @returns
*/ */
async getOrder(siteId: string, orderId: string): Promise<any> { async getOrder(siteId: string, orderId: string): Promise<ShopyyGetOneOrderResult> {
const site = await this.siteService.get(Number(siteId)); const site = await this.siteService.get(Number(siteId));
// ShopYY API: GET /orders/{id} // ShopYY API: GET /orders/{id}