Compare commits
13 Commits
a4c1be32ef
...
b3b7ee4793
| Author | SHA1 | Date |
|---|---|---|
|
|
b3b7ee4793 | |
|
|
b7101ac866 | |
|
|
72cd20fcd6 | |
|
|
8766cf4a4c | |
|
|
d39341d683 | |
|
|
7f04de4583 | |
|
|
bdac4860df | |
|
|
fff62d6864 | |
|
|
c75c0a614f | |
|
|
bfa03fc6a0 | |
|
|
16539b133f | |
|
|
9fc1bedb0c | |
|
|
0f79b7536a |
|
|
@ -23,7 +23,7 @@ import {
|
||||||
UpdateReviewDTO,
|
UpdateReviewDTO,
|
||||||
OrderPaymentStatus,
|
OrderPaymentStatus,
|
||||||
} from '../dto/site-api.dto';
|
} from '../dto/site-api.dto';
|
||||||
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
|
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto';
|
||||||
import {
|
import {
|
||||||
ShopyyAllProductQuery,
|
ShopyyAllProductQuery,
|
||||||
ShopyyCustomer,
|
ShopyyCustomer,
|
||||||
|
|
@ -40,6 +40,7 @@ import {
|
||||||
OrderStatus,
|
OrderStatus,
|
||||||
} from '../enums/base.enum';
|
} from '../enums/base.enum';
|
||||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
|
import dayjs = require('dayjs');
|
||||||
export class ShopyyAdapter implements ISiteAdapter {
|
export class ShopyyAdapter implements ISiteAdapter {
|
||||||
shopyyFinancialStatusMap = {
|
shopyyFinancialStatusMap = {
|
||||||
'200': '待支付',
|
'200': '待支付',
|
||||||
|
|
@ -274,6 +275,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
state: shipping.province || item.shipping_zone || '',
|
state: shipping.province || item.shipping_zone || '',
|
||||||
postcode: shipping.zip || item.shipping_postcode || '',
|
postcode: shipping.zip || item.shipping_postcode || '',
|
||||||
method_title: item.payment_method || '',
|
method_title: item.payment_method || '',
|
||||||
|
phone: shipping.phone || (item as any).telephone || '',
|
||||||
country:
|
country:
|
||||||
shipping.country_name ||
|
shipping.country_name ||
|
||||||
shipping.country_code ||
|
shipping.country_code ||
|
||||||
|
|
@ -314,7 +316,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map(
|
const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map(
|
||||||
(product) => ({
|
(product) => ({
|
||||||
id: product.id,
|
id: product.id,
|
||||||
name: product.product_title || product.name,
|
name:product.sku_value?.[0]?.value || product.product_title || product.name,
|
||||||
product_id: product.product_id,
|
product_id: product.product_id,
|
||||||
quantity: product.quantity,
|
quantity: product.quantity,
|
||||||
total: String(product.price ?? ''),
|
total: String(product.price ?? ''),
|
||||||
|
|
@ -391,7 +393,6 @@ 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 || '',
|
||||||
|
|
@ -570,9 +571,21 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
per_page,
|
per_page,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
mapGetAllOrdersParams(params: UnifiedSearchParamsDTO) :ShopyyGetAllOrdersParams{
|
||||||
|
|
||||||
|
const pay_at_min = dayjs(params.after || '').unix().toString();
|
||||||
|
const pay_at_max = dayjs(params.before || '').unix().toString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
per_page: params.per_page || 100,
|
||||||
|
pay_at_min: pay_at_min,
|
||||||
|
pay_at_max: pay_at_max,
|
||||||
|
order_field: 'pay_at',
|
||||||
|
}
|
||||||
|
}
|
||||||
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
|
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
|
||||||
const data = await this.shopyyService.getAllOrders(this.site.id, params);
|
const normalizedParams = this.mapGetAllOrdersParams(params);
|
||||||
|
const data = await this.shopyyService.getAllOrders(this.site.id, normalizedParams);
|
||||||
return data.map(this.mapPlatformToUnifiedOrder.bind(this));
|
return data.map(this.mapPlatformToUnifiedOrder.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,12 @@ export default {
|
||||||
typeorm: {
|
typeorm: {
|
||||||
dataSource: {
|
dataSource: {
|
||||||
default: {
|
default: {
|
||||||
host: 'localhost',
|
host: '13.212.62.127',
|
||||||
port: "23306",
|
port: "3306",
|
||||||
username: 'root',
|
username: 'root',
|
||||||
password: '12345678',
|
password: 'Yoone!@.2025',
|
||||||
database: 'inventory_v2',
|
database: 'inventory_v2',
|
||||||
|
synchronize: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -750,4 +750,31 @@ export class ProductController {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取所有产品,支持按品牌过滤
|
||||||
|
@ApiOkResponse({ description: '获取所有产品', type: ProductListRes })
|
||||||
|
@Get('/all')
|
||||||
|
async getAllProducts(@Query('brand') brand?: string) {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.getAllProducts(brand);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取按属性分组的产品,默认按强度划分
|
||||||
|
@ApiOkResponse({ description: '获取按属性分组的产品' })
|
||||||
|
@Get('/grouped')
|
||||||
|
async getGroupedProducts(
|
||||||
|
@Query('brand') brand?: string,
|
||||||
|
@Query('attribute') attribute: string = 'strength'
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.getProductsGroupedByAttribute(brand, attribute);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,23 @@ export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
|
||||||
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shopyy获取所有订单参数DTO
|
||||||
|
*/
|
||||||
|
export class ShopyyGetAllOrdersParams {
|
||||||
|
@ApiProperty({ description: '每页数量', example: 100, required: false })
|
||||||
|
per_page?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '支付时间范围开始', example: '2023-01-01T00:00:00Z', required: false })
|
||||||
|
pay_at_min?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '支付时间范围结束', example: '2023-01-01T23:59:59Z', required: false })
|
||||||
|
pay_at_max?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '排序字段', example: 'id', required: false })
|
||||||
|
order_field?: string;//排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量操作错误项
|
* 批量操作错误项
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,10 @@ export class CreateProductDTO {
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number())
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类名称', required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
categoryName?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
||||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||||
siteSkus?: string[];
|
siteSkus?: string[];
|
||||||
|
|
@ -142,6 +146,10 @@ export class UpdateProductDTO {
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number())
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类名称', required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
categoryName?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
||||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||||
siteSkus?: string[];
|
siteSkus?: string[];
|
||||||
|
|
@ -311,6 +319,8 @@ export interface ProductWhereFilter {
|
||||||
updatedAtStart?: string;
|
updatedAtStart?: string;
|
||||||
// 更新时间范围结束
|
// 更新时间范围结束
|
||||||
updatedAtEnd?: string;
|
updatedAtEnd?: string;
|
||||||
|
// TODO 使用 attributes 过滤
|
||||||
|
attributes?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -873,10 +873,10 @@ export interface ShopyyOrder {
|
||||||
tax_price?: number;
|
tax_price?: number;
|
||||||
// SKU编码
|
// SKU编码
|
||||||
sku?: string;
|
sku?: string;
|
||||||
// SKU值
|
|
||||||
sku_value?: string;
|
|
||||||
// SKU代码
|
// SKU代码
|
||||||
sku_code?: string;
|
sku_code?: string;
|
||||||
|
sku_value?: string | any[];
|
||||||
// 条形码
|
// 条形码
|
||||||
barcode?: string;
|
barcode?: string;
|
||||||
// 商品来源
|
// 商品来源
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ export class OrderSale {
|
||||||
@Expose()
|
@Expose()
|
||||||
externalOrderItemId: string; // WooCommerce 订单item ID
|
externalOrderItemId: string; // WooCommerce 订单item ID
|
||||||
|
|
||||||
|
@ApiProperty({name: "父产品 ID"})
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@Expose()
|
||||||
|
parentProductId?: number; // 父产品 ID 用于统计套餐 如果是单品则不记录
|
||||||
|
|
||||||
@ApiProperty({name: "产品 ID"})
|
@ApiProperty({name: "产品 ID"})
|
||||||
@Column()
|
@Column()
|
||||||
@Expose()
|
@Expose()
|
||||||
|
|
@ -50,7 +55,7 @@ export class OrderSale {
|
||||||
@ApiProperty({ description: 'sku', type: 'string' })
|
@ApiProperty({ description: 'sku', type: 'string' })
|
||||||
@Expose()
|
@Expose()
|
||||||
@Column()
|
@Column()
|
||||||
sku: string;
|
sku: string;// 库存产品sku
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@Column()
|
@Column()
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,10 @@ export class Product {
|
||||||
@JoinColumn({ name: 'categoryId' })
|
@JoinColumn({ name: 'categoryId' })
|
||||||
category: Category;
|
category: Category;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类 ID', nullable: true, example: 1 })
|
||||||
|
@Column({ nullable: true })
|
||||||
|
categoryId?: number;
|
||||||
|
|
||||||
@ManyToMany(() => DictItem, dictItem => dictItem.products, {
|
@ManyToMany(() => DictItem, dictItem => dictItem.products, {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,486 @@
|
||||||
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import dayjs = require('dayjs');
|
||||||
|
import utc = require('dayjs/plugin/utc');
|
||||||
|
import timezone = require('dayjs/plugin/timezone');
|
||||||
|
|
||||||
|
// 扩展dayjs功能
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
// 全局参数配置接口
|
||||||
|
interface FreightwavesConfig {
|
||||||
|
appSecret: string;
|
||||||
|
apiBaseUrl: string;
|
||||||
|
partner: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 地址信息接口
|
||||||
|
interface Address {
|
||||||
|
name: string;
|
||||||
|
phone: string;
|
||||||
|
company: string;
|
||||||
|
countryCode: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
address1: string;
|
||||||
|
address2: string;
|
||||||
|
postCode: string;
|
||||||
|
zoneCode?: string;
|
||||||
|
countryName: string;
|
||||||
|
cityName: string;
|
||||||
|
stateName: string;
|
||||||
|
companyName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包裹尺寸接口
|
||||||
|
interface Dimensions {
|
||||||
|
length: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
lengthUnit: 'IN' | 'CM';
|
||||||
|
weight: number;
|
||||||
|
weightUnit: 'LB' | 'KG';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包裹信息接口
|
||||||
|
interface Package {
|
||||||
|
dimensions: Dimensions;
|
||||||
|
currency: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 申报信息接口
|
||||||
|
interface Declaration {
|
||||||
|
boxNo: string;
|
||||||
|
sku: string;
|
||||||
|
cnname: string;
|
||||||
|
enname: string;
|
||||||
|
declaredPrice: number;
|
||||||
|
declaredQty: number;
|
||||||
|
material: string;
|
||||||
|
intendedUse: string;
|
||||||
|
cweight: number;
|
||||||
|
hsCode: string;
|
||||||
|
battery: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 费用试算请求接口
|
||||||
|
interface RateTryRequest {
|
||||||
|
shipCompany: string;
|
||||||
|
partnerOrderNumber: string;
|
||||||
|
warehouseId?: string;
|
||||||
|
shipper: Address;
|
||||||
|
reciver: Address;
|
||||||
|
packages: Package[];
|
||||||
|
partner: string;
|
||||||
|
signService?: 0 | 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建订单请求接口
|
||||||
|
interface CreateOrderRequest extends RateTryRequest {
|
||||||
|
declaration: Declaration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询订单请求接口
|
||||||
|
interface QueryOrderRequest {
|
||||||
|
partnerOrderNumber?: string;
|
||||||
|
shipOrderId?: string;
|
||||||
|
partner: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改订单请求接口
|
||||||
|
interface ModifyOrderRequest extends CreateOrderRequest {
|
||||||
|
shipOrderId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单退款请求接口
|
||||||
|
interface RefundOrderRequest {
|
||||||
|
shipOrderId: string;
|
||||||
|
partner: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用响应接口
|
||||||
|
interface FreightwavesResponse<T> {
|
||||||
|
code: string;
|
||||||
|
msg: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 费用试算响应数据接口
|
||||||
|
interface RateTryResponseData {
|
||||||
|
shipCompany: string;
|
||||||
|
channelCode: string;
|
||||||
|
totalAmount: number;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建订单响应数据接口
|
||||||
|
interface CreateOrderResponseData {
|
||||||
|
partnerOrderNumber: string;
|
||||||
|
shipOrderId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询订单响应数据接口
|
||||||
|
interface QueryOrderResponseData {
|
||||||
|
thirdOrderId: string;
|
||||||
|
shipCompany: string;
|
||||||
|
expressFinish: 0 | 1 | 2;
|
||||||
|
expressFailMsg: string;
|
||||||
|
expressOrder: {
|
||||||
|
mainTrackingNumber: string;
|
||||||
|
labelPath: string[];
|
||||||
|
totalAmount: number;
|
||||||
|
currency: string;
|
||||||
|
balance: number;
|
||||||
|
};
|
||||||
|
partnerOrderNumber: string;
|
||||||
|
shipOrderId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改订单响应数据接口
|
||||||
|
interface ModifyOrderResponseData extends CreateOrderResponseData {}
|
||||||
|
|
||||||
|
// 订单退款响应数据接口
|
||||||
|
interface RefundOrderResponseData {}
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
export class FreightwavesService {
|
||||||
|
@Inject() logger;
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
private config: FreightwavesConfig = {
|
||||||
|
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
|
||||||
|
apiBaseUrl: 'https://tms.freightwaves.ca',
|
||||||
|
partner: '25072621035200000060',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化配置
|
||||||
|
setConfig(config: Partial<FreightwavesConfig>): void {
|
||||||
|
this.config = { ...this.config, ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
private generateSignature(body: any, date: string): string {
|
||||||
|
const bodyString = JSON.stringify(body);
|
||||||
|
const signatureStr = `${bodyString}${this.config.appSecret}${date}`;
|
||||||
|
return crypto.createHash('md5').update(signatureStr).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
private async sendRequest<T>(url: string, data: any): Promise<FreightwavesResponse<T>> {
|
||||||
|
try {
|
||||||
|
// 设置请求头 - 使用太平洋时间 (America/Los_Angeles)
|
||||||
|
const date = dayjs().tz('America/Los_Angeles').format('YYYY-mm-dd HH:mm:ss');
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'requestDate': date,
|
||||||
|
'signature': this.generateSignature(data, date),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 记录请求前的详细信息
|
||||||
|
this.log(`Sending request to: ${this.config.apiBaseUrl}${url}`, {
|
||||||
|
headers,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送请求 - 临时禁用SSL证书验证以解决UNABLE_TO_VERIFY_LEAF_SIGNATURE错误
|
||||||
|
const response = await axios.post<FreightwavesResponse<T>>(
|
||||||
|
`${this.config.apiBaseUrl}${url}`,
|
||||||
|
data,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
httpsAgent: new (require('https').Agent)({
|
||||||
|
rejectUnauthorized: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 记录响应信息
|
||||||
|
this.log(`Received response from: ${this.config.apiBaseUrl}${url}`, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
data: response.data
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理响应
|
||||||
|
if (response.data.code !== '00000200') {
|
||||||
|
this.log(`Freightwaves API error: ${response.data.msg}`, { url, data, response: response.data });
|
||||||
|
throw new Error(response.data.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
// 更详细的错误记录
|
||||||
|
if (error.response) {
|
||||||
|
// 请求已发送,服务器返回错误状态码
|
||||||
|
this.log(`Freightwaves API request failed with status: ${error.response.status}`, {
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
response: error.response.data,
|
||||||
|
status: error.response.status,
|
||||||
|
headers: error.response.headers
|
||||||
|
});
|
||||||
|
} else if (error.request) {
|
||||||
|
// 请求已发送,但没有收到响应
|
||||||
|
this.log(`Freightwaves API request no response received`, {
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
request: error.request
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 请求配置时发生错误
|
||||||
|
this.log(`Freightwaves API request configuration error: ${error.message}`, {
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 费用试算
|
||||||
|
* @param params 费用试算参数
|
||||||
|
* @returns 费用试算结果
|
||||||
|
*/
|
||||||
|
async rateTry(params: Omit<RateTryRequest, 'partner'>): Promise<RateTryResponseData> {
|
||||||
|
const requestData: RateTryRequest = {
|
||||||
|
...params,
|
||||||
|
partner: this.config.partner,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.sendRequest<RateTryResponseData>('/shipService/order/rateTry', requestData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订单
|
||||||
|
* @param params 创建订单参数
|
||||||
|
* @returns 创建订单结果
|
||||||
|
*/
|
||||||
|
async createOrder(params: Omit<CreateOrderRequest, 'partner'>): Promise<CreateOrderResponseData> {
|
||||||
|
const requestData: CreateOrderRequest = {
|
||||||
|
...params,
|
||||||
|
partner: this.config.partner,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.sendRequest<CreateOrderResponseData>('/shipService/order/createOrder?apipost_id=0422aa', requestData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询订单
|
||||||
|
* @param params 查询订单参数
|
||||||
|
* @returns 查询订单结果
|
||||||
|
*/
|
||||||
|
async queryOrder(params: Omit<QueryOrderRequest, 'partner'>): Promise<QueryOrderResponseData> {
|
||||||
|
const requestData: QueryOrderRequest = {
|
||||||
|
...params,
|
||||||
|
partner: this.config.partner,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.sendRequest<QueryOrderResponseData>('/shipService/order/queryOrder', requestData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改订单
|
||||||
|
* @param params 修改订单参数
|
||||||
|
* @returns 修改订单结果
|
||||||
|
*/
|
||||||
|
async modifyOrder(params: Omit<ModifyOrderRequest, 'partner'>): Promise<ModifyOrderResponseData> {
|
||||||
|
const requestData: ModifyOrderRequest = {
|
||||||
|
...params,
|
||||||
|
partner: this.config.partner,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.sendRequest<ModifyOrderResponseData>('/shipService/order/modifyOrder', requestData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单退款
|
||||||
|
* @param params 订单退款参数
|
||||||
|
* @returns 订单退款结果
|
||||||
|
*/
|
||||||
|
async refundOrder(params: Omit<RefundOrderRequest, 'partner'>): Promise<RefundOrderResponseData> {
|
||||||
|
const requestData: RefundOrderRequest = {
|
||||||
|
...params,
|
||||||
|
partner: this.config.partner,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.sendRequest<RefundOrderResponseData>('/shipService/order/refundOrder', requestData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试创建订单方法
|
||||||
|
* 用于演示如何使用createOrder方法
|
||||||
|
*/
|
||||||
|
async testCreateOrder() {
|
||||||
|
try {
|
||||||
|
// 设置必要的配置
|
||||||
|
this.setConfig({
|
||||||
|
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
|
||||||
|
apiBaseUrl: 'https://tms.freightwaves.ca',
|
||||||
|
partner: '25072621035200000060'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
const testParams: Omit<CreateOrderRequest, 'partner'> = {
|
||||||
|
shipCompany: 'DHL',
|
||||||
|
partnerOrderNumber: `test-order-${Date.now()}`,
|
||||||
|
warehouseId: '25072621035200000060',
|
||||||
|
shipper: {
|
||||||
|
name: 'John Doe',
|
||||||
|
phone: '123-456-7890',
|
||||||
|
company: 'Test Company',
|
||||||
|
countryCode: 'CA',
|
||||||
|
city: 'Toronto',
|
||||||
|
state: 'ON',
|
||||||
|
address1: '123 Main St',
|
||||||
|
address2: 'Suite 400',
|
||||||
|
postCode: 'M5V 2T6',
|
||||||
|
countryName: 'Canada',
|
||||||
|
cityName: 'Toronto',
|
||||||
|
stateName: 'Ontario',
|
||||||
|
companyName: 'Test Company Inc.'
|
||||||
|
},
|
||||||
|
reciver: {
|
||||||
|
name: 'Jane Smith',
|
||||||
|
phone: '987-654-3210',
|
||||||
|
company: 'Receiver Company',
|
||||||
|
countryCode: 'CA',
|
||||||
|
city: 'Vancouver',
|
||||||
|
state: 'BC',
|
||||||
|
address1: '456 Oak St',
|
||||||
|
address2: '',
|
||||||
|
postCode: 'V6J 2A9',
|
||||||
|
countryName: 'Canada',
|
||||||
|
cityName: 'Vancouver',
|
||||||
|
stateName: 'British Columbia',
|
||||||
|
companyName: 'Receiver Company Ltd.'
|
||||||
|
},
|
||||||
|
packages: [
|
||||||
|
{
|
||||||
|
dimensions: {
|
||||||
|
length: 10,
|
||||||
|
width: 8,
|
||||||
|
height: 6,
|
||||||
|
lengthUnit: 'IN',
|
||||||
|
weight: 5,
|
||||||
|
weightUnit: 'LB'
|
||||||
|
},
|
||||||
|
currency: 'CAD',
|
||||||
|
description: 'Test Package'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
declaration: {
|
||||||
|
boxNo: 'BOX-001',
|
||||||
|
sku: 'TEST-SKU-001',
|
||||||
|
cnname: '测试产品',
|
||||||
|
enname: 'Test Product',
|
||||||
|
declaredPrice: 100,
|
||||||
|
declaredQty: 1,
|
||||||
|
material: 'Plastic',
|
||||||
|
intendedUse: 'General use',
|
||||||
|
cweight: 5,
|
||||||
|
hsCode: '39269090',
|
||||||
|
battery: 'No'
|
||||||
|
},
|
||||||
|
signService: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用创建订单方法
|
||||||
|
this.log('开始测试创建订单...');
|
||||||
|
this.log('测试参数:', testParams);
|
||||||
|
|
||||||
|
// 注意:在实际环境中取消注释以下行来执行真实请求
|
||||||
|
const result = await this.createOrder(testParams);
|
||||||
|
this.log('创建订单成功:', result);
|
||||||
|
|
||||||
|
|
||||||
|
// 返回模拟结果
|
||||||
|
return {
|
||||||
|
partnerOrderNumber: testParams.partnerOrderNumber,
|
||||||
|
shipOrderId: `simulated-shipOrderId-${Date.now()}`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.log('测试创建订单失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试查询订单方法
|
||||||
|
* @returns 查询订单结果
|
||||||
|
*/
|
||||||
|
async testQueryOrder() {
|
||||||
|
try {
|
||||||
|
// 设置必要的配置
|
||||||
|
this.setConfig({
|
||||||
|
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
|
||||||
|
apiBaseUrl: 'https://tms.freightwaves.ca',
|
||||||
|
partner: '25072621035200000060'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 准备测试数据 - 可以通过partnerOrderNumber或shipOrderId查询
|
||||||
|
const testParams: Omit<QueryOrderRequest, 'partner'> = {
|
||||||
|
// 选择其中一个参数进行测试
|
||||||
|
partnerOrderNumber: 'test-order-123456789', // 示例订单号
|
||||||
|
// shipOrderId: 'simulated-shipOrderId-123456789' // 或者使用运单号
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用查询订单方法
|
||||||
|
this.log('开始测试查询订单...');
|
||||||
|
this.log('测试参数:', testParams);
|
||||||
|
|
||||||
|
// 注意:在实际环境中取消注释以下行来执行真实请求
|
||||||
|
const result = await this.queryOrder(testParams);
|
||||||
|
this.log('查询订单成功:', result);
|
||||||
|
|
||||||
|
this.log('测试完成:查询订单方法调用成功(模拟)');
|
||||||
|
|
||||||
|
// 返回模拟结果
|
||||||
|
return {
|
||||||
|
thirdOrderId: 'thirdOrder-123456789',
|
||||||
|
shipCompany: 'DHL',
|
||||||
|
expressFinish: 0,
|
||||||
|
expressFailMsg: '',
|
||||||
|
expressOrder: {
|
||||||
|
mainTrackingNumber: '1234567890',
|
||||||
|
labelPath: ['https://example.com/label.pdf'],
|
||||||
|
totalAmount: 100,
|
||||||
|
currency: 'CAD',
|
||||||
|
balance: 50
|
||||||
|
},
|
||||||
|
partnerOrderNumber: testParams.partnerOrderNumber,
|
||||||
|
shipOrderId: 'simulated-shipOrderId-123456789'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.log('测试查询订单失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助日志方法,处理logger可能未定义的情况
|
||||||
|
* @param message 日志消息
|
||||||
|
* @param data 附加数据
|
||||||
|
*/
|
||||||
|
private log(message: string, data?: any) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.info(message, data);
|
||||||
|
} else {
|
||||||
|
// 如果logger未定义,使用console输出
|
||||||
|
if (data) {
|
||||||
|
console.log(message, data);
|
||||||
|
} else {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -141,7 +141,7 @@ export class OrderService {
|
||||||
updated: 0,
|
updated: 0,
|
||||||
errors: []
|
errors: []
|
||||||
};
|
};
|
||||||
|
console.log('开始进入循环同步订单', result.length, '个订单')
|
||||||
// 遍历每个订单进行同步
|
// 遍历每个订单进行同步
|
||||||
for (const order of result) {
|
for (const order of result) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -165,6 +165,7 @@ export class OrderService {
|
||||||
} else {
|
} else {
|
||||||
syncResult.created++;
|
syncResult.created++;
|
||||||
}
|
}
|
||||||
|
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 记录错误但不中断整个同步过程
|
// 记录错误但不中断整个同步过程
|
||||||
syncResult.errors.push({
|
syncResult.errors.push({
|
||||||
|
|
@ -174,6 +175,8 @@ export class OrderService {
|
||||||
syncResult.processed++;
|
syncResult.processed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log('同步完成', syncResult.updated, 'created:', syncResult.created)
|
||||||
|
|
||||||
this.logger.debug('syncOrders result', syncResult)
|
this.logger.debug('syncOrders result', syncResult)
|
||||||
return syncResult;
|
return syncResult;
|
||||||
}
|
}
|
||||||
|
|
@ -350,14 +353,14 @@ export class OrderService {
|
||||||
await this.saveOrderItems({
|
await this.saveOrderItems({
|
||||||
siteId,
|
siteId,
|
||||||
orderId,
|
orderId,
|
||||||
externalOrderId,
|
externalOrderId: String(externalOrderId),
|
||||||
orderItems: line_items,
|
orderItems: line_items,
|
||||||
});
|
});
|
||||||
// 保存退款信息
|
// 保存退款信息
|
||||||
await this.saveOrderRefunds({
|
await this.saveOrderRefunds({
|
||||||
siteId,
|
siteId,
|
||||||
orderId,
|
orderId,
|
||||||
externalOrderId,
|
externalOrderId ,
|
||||||
refunds,
|
refunds,
|
||||||
});
|
});
|
||||||
// 保存费用信息
|
// 保存费用信息
|
||||||
|
|
@ -712,21 +715,18 @@ export class OrderService {
|
||||||
}
|
}
|
||||||
if (!orderItem.sku) return;
|
if (!orderItem.sku) return;
|
||||||
// 从数据库查询产品,关联查询组件
|
// 从数据库查询产品,关联查询组件
|
||||||
const product = await this.productModel.findOne({
|
const productDetail = await this.productService.getComponentDetailFromSiteSku({ sku: orderItem.sku, name: orderItem.name });
|
||||||
where: { siteSkus: Like(`%${orderItem.sku}%`) },
|
|
||||||
relations: ['components','attributes','attributes.dict'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!product) return;
|
if (!productDetail || !productDetail.quantity) return;
|
||||||
|
const {product, quantity} = productDetail
|
||||||
const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => {
|
const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => {
|
||||||
return {
|
return {
|
||||||
product: await this.productModel.findOne({
|
product: await this.productModel.findOne({
|
||||||
where: { sku: comp.sku },
|
where: { id: comp.productId },
|
||||||
relations: ['components', 'attributes','attributes.dict'],
|
|
||||||
}),
|
}),
|
||||||
quantity: comp.quantity * orderItem.quantity,
|
quantity: comp.quantity * orderItem.quantity,
|
||||||
}
|
}
|
||||||
})) : [{ product, quantity: orderItem.quantity }]
|
})) : [{ product, quantity }]
|
||||||
|
|
||||||
const orderSales: OrderSale[] = componentDetails.map(componentDetail => {
|
const orderSales: OrderSale[] = componentDetails.map(componentDetail => {
|
||||||
if (!componentDetail.product) return null
|
if (!componentDetail.product) return null
|
||||||
|
|
@ -734,18 +734,21 @@ export class OrderService {
|
||||||
const orderSale = plainToClass(OrderSale, {
|
const orderSale = plainToClass(OrderSale, {
|
||||||
orderId: orderItem.orderId,
|
orderId: orderItem.orderId,
|
||||||
siteId: orderItem.siteId,
|
siteId: orderItem.siteId,
|
||||||
externalOrderItemId: orderItem.externalOrderItemId,
|
externalOrderItemId: orderItem.externalOrderItemId,// 原始 itemId
|
||||||
|
parentProductId: product.id, // 父产品 ID 用于统计套餐 如果是单品则不记录
|
||||||
productId: componentDetail.product.id,
|
productId: componentDetail.product.id,
|
||||||
|
isPackage: product.type === 'bundle',// 这里是否是套餐取决于父产品
|
||||||
name: componentDetail.product.name,
|
name: componentDetail.product.name,
|
||||||
quantity: componentDetail.quantity * orderItem.quantity,
|
quantity: componentDetail.quantity * orderItem.quantity,
|
||||||
sku: componentDetail.product.sku,
|
sku: componentDetail.product.sku,
|
||||||
// 理论上直接存 product 的全部数据才是对的,因为这样我的数据才全面。
|
// 理论上直接存 product 的全部数据才是对的,因为这样我的数据才全面。
|
||||||
isPackage: componentDetail.product.type === 'bundle',
|
brand: attrsObj?.['brand']?.name,
|
||||||
isYoone: attrsObj?.['brand']?.name === 'yoone',
|
version: attrsObj?.['version']?.name,
|
||||||
isZyn: attrsObj?.['brand']?.name === 'zyn',
|
|
||||||
isZex: attrsObj?.['brand']?.name === 'zex',
|
|
||||||
isYooneNew: attrsObj?.['brand']?.name === 'yoone' && attrsObj?.['version']?.name === 'new',
|
|
||||||
strength: attrsObj?.['strength']?.name,
|
strength: attrsObj?.['strength']?.name,
|
||||||
|
flavor: attrsObj?.['flavor']?.name,
|
||||||
|
humidity: attrsObj?.['humidity']?.name,
|
||||||
|
size: attrsObj?.['size']?.name,
|
||||||
|
category: componentDetail.product.category.name,
|
||||||
});
|
});
|
||||||
return orderSale
|
return orderSale
|
||||||
}).filter(v => v !== null)
|
}).filter(v => v !== null)
|
||||||
|
|
@ -1229,13 +1232,13 @@ export class OrderService {
|
||||||
parameters.push(siteId);
|
parameters.push(siteId);
|
||||||
}
|
}
|
||||||
if (startDate) {
|
if (startDate) {
|
||||||
sqlQuery += ` AND o.date_created >= ?`;
|
sqlQuery += ` AND o.date_paid >= ?`;
|
||||||
totalQuery += ` AND o.date_created >= ?`;
|
totalQuery += ` AND o.date_paid >= ?`;
|
||||||
parameters.push(startDate);
|
parameters.push(startDate);
|
||||||
}
|
}
|
||||||
if (endDate) {
|
if (endDate) {
|
||||||
sqlQuery += ` AND o.date_created <= ?`;
|
sqlQuery += ` AND o.date_paid <= ?`;
|
||||||
totalQuery += ` AND o.date_created <= ?`;
|
totalQuery += ` AND o.date_paid <= ?`;
|
||||||
parameters.push(endDate);
|
parameters.push(endDate);
|
||||||
}
|
}
|
||||||
// 支付方式筛选(使用参数化,避免SQL注入)
|
// 支付方式筛选(使用参数化,避免SQL注入)
|
||||||
|
|
@ -1323,7 +1326,7 @@ export class OrderService {
|
||||||
// 添加分页到主查询
|
// 添加分页到主查询
|
||||||
sqlQuery += `
|
sqlQuery += `
|
||||||
GROUP BY o.id
|
GROUP BY o.id
|
||||||
ORDER BY o.date_created DESC
|
ORDER BY o.date_paid DESC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`;
|
`;
|
||||||
parameters.push(pageSize, (current - 1) * pageSize);
|
parameters.push(pageSize, (current - 1) * pageSize);
|
||||||
|
|
@ -2541,7 +2544,7 @@ export class OrderService {
|
||||||
'姓名地址': nameAddress,
|
'姓名地址': nameAddress,
|
||||||
'邮箱': order.customer_email || '',
|
'邮箱': order.customer_email || '',
|
||||||
'号码': phone,
|
'号码': phone,
|
||||||
'订单内容': orderContent,
|
'订单内容': this.removeLastParenthesesContent(orderContent),
|
||||||
'盒数': boxCount,
|
'盒数': boxCount,
|
||||||
'换盒数': exchangeBoxCount,
|
'换盒数': exchangeBoxCount,
|
||||||
'换货内容': exchangeContent,
|
'换货内容': exchangeContent,
|
||||||
|
|
@ -2641,4 +2644,80 @@ export class OrderService {
|
||||||
throw new Error(`导出CSV文件失败: ${error.message}`);
|
throw new Error(`导出CSV文件失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身)
|
||||||
|
* @param str 输入字符串
|
||||||
|
* @returns 删除后的字符串
|
||||||
|
*/
|
||||||
|
removeLastParenthesesContent(str: string): string {
|
||||||
|
if (!str || typeof str !== 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:删除指定位置的括号对及其内容
|
||||||
|
const removeParenthesesAt = (s: string, leftIndex: number): string => {
|
||||||
|
if (leftIndex === -1) return s;
|
||||||
|
|
||||||
|
let rightIndex = -1;
|
||||||
|
let parenCount = 0;
|
||||||
|
|
||||||
|
for (let i = leftIndex; i < s.length; i++) {
|
||||||
|
const char = s[i];
|
||||||
|
if (char === '(') {
|
||||||
|
parenCount++;
|
||||||
|
} else if (char === ')') {
|
||||||
|
parenCount--;
|
||||||
|
if (parenCount === 0) {
|
||||||
|
rightIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightIndex !== -1) {
|
||||||
|
return s.substring(0, leftIndex) + s.substring(rightIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. 处理每个分号前面的括号对
|
||||||
|
let result = str;
|
||||||
|
|
||||||
|
// 找出所有分号的位置
|
||||||
|
const semicolonIndices: number[] = [];
|
||||||
|
for (let i = 0; i < result.length; i++) {
|
||||||
|
if (result[i] === ';') {
|
||||||
|
semicolonIndices.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从后向前处理每个分号,避免位置变化影响后续处理
|
||||||
|
for (let i = semicolonIndices.length - 1; i >= 0; i--) {
|
||||||
|
const semicolonIndex = semicolonIndices[i];
|
||||||
|
|
||||||
|
// 从分号位置向前查找最近的左括号
|
||||||
|
let lastLeftParenIndex = -1;
|
||||||
|
for (let j = semicolonIndex - 1; j >= 0; j--) {
|
||||||
|
if (result[j] === '(') {
|
||||||
|
lastLeftParenIndex = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找到左括号,删除该括号对及其内容
|
||||||
|
if (lastLeftParenIndex !== -1) {
|
||||||
|
result = removeParenthesesAt(result, lastLeftParenIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 处理整个字符串的最后一个括号对
|
||||||
|
let lastLeftParenIndex = result.lastIndexOf('(');
|
||||||
|
if (lastLeftParenIndex !== -1) {
|
||||||
|
result = removeParenthesesAt(result, lastLeftParenIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -276,19 +276,9 @@ export class ProductService {
|
||||||
qb.andWhere('product.id IN (:...ids)', { ids: query.where.ids });
|
qb.andWhere('product.id IN (:...ids)', { ids: query.where.ids });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理where对象中的id过滤
|
|
||||||
if (query.where?.id) {
|
|
||||||
qb.andWhere('product.id = :whereId', { whereId: query.where.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理where对象中的ids过滤
|
|
||||||
if (query.where?.ids && query.where.ids.length > 0) {
|
|
||||||
qb.andWhere('product.id IN (:...whereIds)', { whereIds: query.where.ids });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理SKU过滤
|
// 处理SKU过滤
|
||||||
if (query.where?.sku) {
|
if (query.where?.sku) {
|
||||||
qb.andWhere('product.sku = :sku', { sku: query.where.sku });
|
qb.andWhere('product.sku LIKE :sku', { sku: `%${query.where.sku}%` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理SKU列表过滤
|
// 处理SKU列表过滤
|
||||||
|
|
@ -296,16 +286,6 @@ export class ProductService {
|
||||||
qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus });
|
qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理where对象中的sku过滤
|
|
||||||
if (query.where?.sku) {
|
|
||||||
qb.andWhere('product.sku = :whereSku', { whereSku: query.where.sku });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理where对象中的skus过滤
|
|
||||||
if (query.where?.skus && query.where.skus.length > 0) {
|
|
||||||
qb.andWhere('product.sku IN (:...whereSkus)', { whereSkus: query.where.skus });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理产品中文名称过滤
|
// 处理产品中文名称过滤
|
||||||
if (query.where?.nameCn) {
|
if (query.where?.nameCn) {
|
||||||
qb.andWhere('product.nameCn LIKE :nameCn', { nameCn: `%${query.where.nameCn}%` });
|
qb.andWhere('product.nameCn LIKE :nameCn', { nameCn: `%${query.where.nameCn}%` });
|
||||||
|
|
@ -316,11 +296,6 @@ export class ProductService {
|
||||||
qb.andWhere('product.type = :type', { type: query.where.type });
|
qb.andWhere('product.type = :type', { type: query.where.type });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理where对象中的type过滤
|
|
||||||
if (query.where?.type) {
|
|
||||||
qb.andWhere('product.type = :whereType', { whereType: query.where.type });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理价格范围过滤
|
// 处理价格范围过滤
|
||||||
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 });
|
||||||
|
|
@ -330,15 +305,6 @@ export class ProductService {
|
||||||
qb.andWhere('product.price <= :maxPrice', { maxPrice: query.where.maxPrice });
|
qb.andWhere('product.price <= :maxPrice', { maxPrice: query.where.maxPrice });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理where对象中的价格范围过滤
|
|
||||||
if (query.where?.minPrice !== undefined) {
|
|
||||||
qb.andWhere('product.price >= :whereMinPrice', { whereMinPrice: query.where.minPrice });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.where?.maxPrice !== undefined) {
|
|
||||||
qb.andWhere('product.price <= :whereMaxPrice', { whereMaxPrice: query.where.maxPrice });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理促销价格范围过滤
|
// 处理促销价格范围过滤
|
||||||
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 });
|
||||||
|
|
@ -348,15 +314,6 @@ export class ProductService {
|
||||||
qb.andWhere('product.promotionPrice <= :maxPromotionPrice', { maxPromotionPrice: query.where.maxPromotionPrice });
|
qb.andWhere('product.promotionPrice <= :maxPromotionPrice', { maxPromotionPrice: query.where.maxPromotionPrice });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理where对象中的促销价格范围过滤
|
|
||||||
if (query.where?.minPromotionPrice !== undefined) {
|
|
||||||
qb.andWhere('product.promotionPrice >= :whereMinPromotionPrice', { whereMinPromotionPrice: query.where.minPromotionPrice });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.where?.maxPromotionPrice !== undefined) {
|
|
||||||
qb.andWhere('product.promotionPrice <= :whereMaxPromotionPrice', { whereMaxPromotionPrice: query.where.maxPromotionPrice });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理创建时间范围过滤
|
// 处理创建时间范围过滤
|
||||||
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) });
|
||||||
|
|
@ -366,15 +323,6 @@ export class ProductService {
|
||||||
qb.andWhere('product.createdAt <= :createdAtEnd', { createdAtEnd: new Date(query.where.createdAtEnd) });
|
qb.andWhere('product.createdAt <= :createdAtEnd', { createdAtEnd: new Date(query.where.createdAtEnd) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理where对象中的创建时间范围过滤
|
|
||||||
if (query.where?.createdAtStart) {
|
|
||||||
qb.andWhere('product.createdAt >= :whereCreatedAtStart', { whereCreatedAtStart: new Date(query.where.createdAtStart) });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.where?.createdAtEnd) {
|
|
||||||
qb.andWhere('product.createdAt <= :whereCreatedAtEnd', { whereCreatedAtEnd: new Date(query.where.createdAtEnd) });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理更新时间范围过滤
|
// 处理更新时间范围过滤
|
||||||
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) });
|
||||||
|
|
@ -384,14 +332,40 @@ export class ProductService {
|
||||||
qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) });
|
qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理where对象中的更新时间范围过滤
|
// 处理属性过滤
|
||||||
if (query.where?.updatedAtStart) {
|
const attributeFilters = query.where?.attributes || {};
|
||||||
qb.andWhere('product.updatedAt >= :whereUpdatedAtStart', { whereUpdatedAtStart: new Date(query.where.updatedAtStart) });
|
Object.entries(attributeFilters).forEach(([attributeName, value], index) => {
|
||||||
}
|
if (value === 'hasValue') {
|
||||||
|
// 如果值为'hasValue',则过滤出具有该属性的产品
|
||||||
if (query.where?.updatedAtEnd) {
|
qb.andWhere(qb => {
|
||||||
qb.andWhere('product.updatedAt <= :whereUpdatedAtEnd', { whereUpdatedAtEnd: new Date(query.where.updatedAtEnd) });
|
const subQuery = qb
|
||||||
|
.subQuery()
|
||||||
|
.select('product_attributes_dict_item.productId')
|
||||||
|
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||||
|
.innerJoin('dict_item', 'dict_item', 'product_attributes_dict_item.dictItemId = dict_item.id')
|
||||||
|
.innerJoin('dict', 'dict', 'dict_item.dictId = dict.id')
|
||||||
|
.where('dict.name = :attributeName', {
|
||||||
|
attributeName,
|
||||||
|
})
|
||||||
|
.getQuery();
|
||||||
|
return 'product.id IN ' + subQuery;
|
||||||
|
});
|
||||||
|
} else if (typeof value === 'number' || !isNaN(Number(value))) {
|
||||||
|
// 如果值是数字,则过滤出该属性等于该值的产品
|
||||||
|
const attributeValueId = Number(value);
|
||||||
|
qb.andWhere(qb => {
|
||||||
|
const subQuery = qb
|
||||||
|
.subQuery()
|
||||||
|
.select('product_attributes_dict_item.productId')
|
||||||
|
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||||
|
.where('product_attributes_dict_item.dictItemId = :attributeValueId', {
|
||||||
|
attributeValueId,
|
||||||
|
})
|
||||||
|
.getQuery();
|
||||||
|
return 'product.id IN ' + subQuery;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 品牌过滤(向后兼容)
|
// 品牌过滤(向后兼容)
|
||||||
if (brandId) {
|
if (brandId) {
|
||||||
|
|
@ -433,16 +407,6 @@ export class ProductService {
|
||||||
qb.andWhere('product.categoryId IN (:...categoryIds)', { categoryIds });
|
qb.andWhere('product.categoryId IN (:...categoryIds)', { categoryIds });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理where对象中的分类ID过滤
|
|
||||||
if (query.where?.categoryId) {
|
|
||||||
qb.andWhere('product.categoryId = :whereCategoryId', { whereCategoryId: query.where.categoryId });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理where对象中的分类ID列表过滤
|
|
||||||
if (query.where?.categoryIds && query.where.categoryIds.length > 0) {
|
|
||||||
qb.andWhere('product.categoryId IN (:...whereCategoryIds)', { whereCategoryIds: query.where.categoryIds });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理排序(支持新旧两种格式)
|
// 处理排序(支持新旧两种格式)
|
||||||
if (orderBy) {
|
if (orderBy) {
|
||||||
if (typeof orderBy === 'string') {
|
if (typeof orderBy === 'string') {
|
||||||
|
|
@ -534,9 +498,8 @@ export class ProductService {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
||||||
const { attributes, sku, categoryId, type } = createProductDTO;
|
const { attributes, sku, categoryId, categoryName, type } = createProductDTO;
|
||||||
|
|
||||||
// 条件判断(校验属性输入)
|
// 条件判断(校验属性输入)
|
||||||
// 当产品类型为 'bundle' 时,attributes 可以为空
|
// 当产品类型为 'bundle' 时,attributes 可以为空
|
||||||
|
|
@ -547,47 +510,38 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const safeAttributes = attributes || [];
|
|
||||||
|
|
||||||
// 解析属性输入(按 id 或 dictName 创建/关联字典项)
|
// 解析属性输入(按 id 或 dictName 创建/关联字典项)
|
||||||
const resolvedAttributes: DictItem[] = [];
|
|
||||||
let categoryItem: Category | null = null;
|
let categoryItem: Category | null = null;
|
||||||
|
|
||||||
// 如果提供了 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']
|
||||||
});
|
});
|
||||||
if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`);
|
|
||||||
}
|
}
|
||||||
|
if (!categoryItem && categoryName) {
|
||||||
|
categoryItem = await this.categoryModel.findOne({
|
||||||
|
where: { name: categoryName },
|
||||||
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!categoryItem && categoryName) {
|
||||||
|
const category = new Category();
|
||||||
|
category.name = categoryName || '';
|
||||||
|
category.title = categoryName || '';
|
||||||
|
const savedCategory = await this.categoryModel.save(category);
|
||||||
|
categoryItem = await this.categoryModel.findOne({
|
||||||
|
where: { id: savedCategory.id },
|
||||||
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
|
});
|
||||||
|
if (!categoryItem) throw new Error(`分类名称 ${categoryName} 不存在`);
|
||||||
|
}
|
||||||
|
// 创造一定要有商品分类
|
||||||
|
if (!categoryItem) throw new Error('必须提供分类 ID 或分类名称');
|
||||||
|
|
||||||
|
const resolvedAttributes: DictItem[] = [];
|
||||||
|
const safeAttributes = attributes || [];
|
||||||
for (const attr of safeAttributes) {
|
for (const attr of safeAttributes) {
|
||||||
// 如果属性是分类,特殊处理
|
|
||||||
if (attr.dictName === 'category') {
|
|
||||||
if (attr.id) {
|
|
||||||
categoryItem = await this.categoryModel.findOne({
|
|
||||||
where: { id: attr.id },
|
|
||||||
relations: ['attributes', 'attributes.attributeDict']
|
|
||||||
});
|
|
||||||
} else if (attr.name) {
|
|
||||||
categoryItem = await this.categoryModel.findOne({
|
|
||||||
where: { name: attr.name },
|
|
||||||
relations: ['attributes', 'attributes.attributeDict']
|
|
||||||
});
|
|
||||||
} else if (attr.title) {
|
|
||||||
// 尝试用 title 匹配 name 或 title
|
|
||||||
categoryItem = await this.categoryModel.findOne({
|
|
||||||
where: [
|
|
||||||
{ name: attr.title },
|
|
||||||
{ title: attr.title }
|
|
||||||
],
|
|
||||||
relations: ['attributes', 'attributes.attributeDict']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let item: DictItem | null = null;
|
let item: DictItem | null = null;
|
||||||
if (attr.id) {
|
if (attr.id) {
|
||||||
// 如果传入了 id,直接查找字典项并使用,不强制要求 dictName
|
// 如果传入了 id,直接查找字典项并使用,不强制要求 dictName
|
||||||
|
|
@ -663,27 +617,46 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 merge 更新基础字段,排除特殊处理字段
|
// 使用 merge 更新基础字段,排除特殊处理字段
|
||||||
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = updateProductDTO;
|
const { attributes, categoryId, categoryName, sku, components, ...simpleFields } = updateProductDTO;
|
||||||
this.productModel.merge(product, simpleFields);
|
this.productModel.merge(product, simpleFields);
|
||||||
|
// 解析属性输入(按 id 或 dictName 创建/关联字典项)
|
||||||
// 处理分类更新
|
let categoryItem: Category | null = null;
|
||||||
if (updateProductDTO.categoryId !== undefined) {
|
// 如果提供了 categoryId,设置分类
|
||||||
if (updateProductDTO.categoryId) {
|
if (categoryId) {
|
||||||
const categoryItem = await this.categoryModel.findOne({ where: { id: updateProductDTO.categoryId } });
|
categoryItem = await this.categoryModel.findOne({
|
||||||
if (!categoryItem) throw new Error(`分类 ID ${updateProductDTO.categoryId} 不存在`);
|
where: { id: categoryId },
|
||||||
product.category = categoryItem;
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
} else {
|
});
|
||||||
// 如果传了 0 或 null,可以清除分类(根据需求)
|
|
||||||
// product.category = null;
|
|
||||||
}
|
}
|
||||||
|
if (!categoryItem && categoryName) {
|
||||||
|
categoryItem = await this.categoryModel.findOne({
|
||||||
|
where: { name: categoryName },
|
||||||
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
function nameToTitle(name: string) {
|
||||||
|
return name.replace('-', ' ');
|
||||||
|
}
|
||||||
|
if (!categoryItem && categoryName) {
|
||||||
|
const category = new Category();
|
||||||
|
category.name = categoryName || '';
|
||||||
|
category.title = nameToTitle(categoryName || '');
|
||||||
|
const savedCategory = await this.categoryModel.save(category);
|
||||||
|
categoryItem = await this.categoryModel.findOne({
|
||||||
|
where: { id: savedCategory.id },
|
||||||
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
|
});
|
||||||
|
if (!categoryItem) throw new Error(`分类名称 ${categoryName} 不存在`);
|
||||||
|
}
|
||||||
|
// 创造一定要有商品分类
|
||||||
|
if (!categoryItem) throw new Error('必须提供分类 ID 或分类名称');
|
||||||
|
product.categoryId = categoryItem.id;
|
||||||
// 处理 SKU 更新
|
// 处理 SKU 更新
|
||||||
if (updateProductDTO.sku !== undefined) {
|
if (updateProductDTO.sku !== undefined) {
|
||||||
// 校验 SKU 唯一性(如变更)
|
// 校验 SKU 唯一性(如变更)
|
||||||
const newSku = updateProductDTO.sku;
|
const newSku = updateProductDTO.sku;
|
||||||
if (newSku && newSku !== product.sku) {
|
if (newSku && newSku !== product.sku) {
|
||||||
const exist = await this.productModel.findOne({ where: { sku: newSku } });
|
const exist = await this.productModel.findOne({ where: { id: Not(id), sku: newSku } });
|
||||||
if (exist) {
|
if (exist) {
|
||||||
throw new Error('SKU 已存在,请更换后重试');
|
throw new Error('SKU 已存在,请更换后重试');
|
||||||
}
|
}
|
||||||
|
|
@ -701,14 +674,6 @@ export class ProductService {
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const attr of updateProductDTO.attributes) {
|
for (const attr of updateProductDTO.attributes) {
|
||||||
// 如果属性是分类,特殊处理
|
|
||||||
if (attr.dictName === 'category') {
|
|
||||||
if (attr.id) {
|
|
||||||
const categoryItem = await this.categoryModel.findOneBy({ id: attr.id });
|
|
||||||
if (categoryItem) product.category = categoryItem;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let item: DictItem | null = null;
|
let item: DictItem | null = null;
|
||||||
if (attr.id) {
|
if (attr.id) {
|
||||||
|
|
@ -1419,7 +1384,8 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将单条 CSV 记录转换为数据对象
|
// 将单条 CSV 记录转换为数据对象
|
||||||
transformCsvRecordToData(rec: any): CreateProductDTO & { sku: string } | null {
|
mapTableRecordToProduct(rec: any): CreateProductDTO | UpdateProductDTO | null {
|
||||||
|
const keys = Object.keys(rec);
|
||||||
// 必须包含 sku
|
// 必须包含 sku
|
||||||
const sku: string = (rec.sku || '').trim();
|
const sku: string = (rec.sku || '').trim();
|
||||||
if (!sku) {
|
if (!sku) {
|
||||||
|
|
@ -1440,43 +1406,105 @@ 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[] = [];
|
||||||
|
|
||||||
// 处理动态属性字段 (attribute_*)
|
// 处理动态属性字段 (attribute_*)
|
||||||
for (const key of Object.keys(rec)) {
|
for (const key of keys) {
|
||||||
if (key.startsWith('attribute_')) {
|
if (key.startsWith('attribute_')) {
|
||||||
const dictName = key.replace('attribute_', '');
|
const dictName = key.replace('attribute_', '');
|
||||||
if (dictName) {
|
if (dictName) {
|
||||||
const list = parseList(rec[key]);
|
const list = parseList(rec[key]) || [];
|
||||||
for (const item of list) attributes.push({ dictName, title: item });
|
for (const item of list) attributes.push({ dictName, title: item });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 目前的 components 由 component_{index}_sku和component_{index}_quantity组成
|
||||||
|
const component_sku_keys = keys.filter(key => key.startsWith('component_') && key.endsWith('_sku'));
|
||||||
|
const components = [];
|
||||||
|
for (const key of component_sku_keys) {
|
||||||
|
const index = key.replace('component_', '').replace('_sku', '');
|
||||||
|
if (index) {
|
||||||
|
const sku = val(rec[`component_${index}_sku`]);
|
||||||
|
const quantity = num(rec[`component_${index}_quantity`]);
|
||||||
|
if (sku && quantity) {
|
||||||
|
components.push({ sku, quantity });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// 处理分类字段
|
// 处理分类字段
|
||||||
const category = val(rec.category);
|
const categoryName = val(rec.category);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sku,
|
sku,
|
||||||
name: val(rec.name),
|
name: val(rec.name),
|
||||||
nameCn: val(rec.nameCn),
|
nameCn: val(rec.nameCn),
|
||||||
|
image: val(rec.image),
|
||||||
description: val(rec.description),
|
description: val(rec.description),
|
||||||
|
shortDescription: val(rec.shortDescription),
|
||||||
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 ? parseList(rec.siteSkus) : undefined,
|
||||||
? String(rec.siteSkus)
|
categoryName, // 添加分类字段
|
||||||
.split(/[;,]/) // 支持英文分号或英文逗号分隔
|
components,
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
: undefined,
|
|
||||||
category, // 添加分类字段
|
|
||||||
|
|
||||||
attributes: attributes.length > 0 ? attributes : undefined,
|
attributes: attributes.length > 0 ? attributes : undefined,
|
||||||
} as any;
|
}
|
||||||
|
}
|
||||||
|
isMixedSku(sku: string){
|
||||||
|
const splitSKu = sku.split('-')
|
||||||
|
const last = splitSKu[splitSKu.length - 1]
|
||||||
|
const second = splitSKu[splitSKu.length - 2]
|
||||||
|
// 这里判断 second 是否是数字
|
||||||
|
return sku.includes('-MX-') || sku.includes('-Mixed-') || /^\d+$/.test(second) && /^\d+$/.test(last)
|
||||||
|
}
|
||||||
|
async getComponentDetailFromSiteSku(siteProduct: { sku: string, name: string }) {
|
||||||
|
if (!siteProduct.sku) {
|
||||||
|
throw new Error('siteSku 不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
let product = await this.productModel.findOne({
|
||||||
|
where: { siteSkus: Like(`%${siteProduct.sku}%`) },
|
||||||
|
relations: ['components', 'attributes', 'attributes.dict'],
|
||||||
|
});
|
||||||
|
let quantity = 1;
|
||||||
|
// 这里处理一下特殊情况,就是无法直接通过 siteProduct.sku去获取, 但有一定规则转换成有的产品,就是 bundle 的部分
|
||||||
|
// 考察各个站点的 bundle 规则, 会发现
|
||||||
|
// wordpress:
|
||||||
|
// togovape YOONE Wintergreen 9MG (Moisture) - 10 cans TV-YOONE-NP-S-WG-9MG-0010
|
||||||
|
// togovape mixed 是这样的 TV-YOONE-NP-G-12MG-MX-0003 TV-ZEX-NP-Mixed-12MG-0001
|
||||||
|
//
|
||||||
|
// shopyy: shopyy 已经
|
||||||
|
// 只有 bundle 做这个处理
|
||||||
|
if (!product && !this.isMixedSku(siteProduct.sku)) {
|
||||||
|
const skuSplitArr = siteProduct.sku.split('-')
|
||||||
|
const quantityStr = skuSplitArr[skuSplitArr.length - 1]
|
||||||
|
const isBundleSku = quantityStr.startsWith('0')
|
||||||
|
if(!isBundleSku){
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
quantity = Number(quantityStr)
|
||||||
|
if(!isBundleSku){
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
// 更正为正确的站点 sku
|
||||||
|
const childSku = skuSplitArr.slice(0, skuSplitArr.length - 1).join('-')
|
||||||
|
// 重新获取匹配的商品
|
||||||
|
product = await this.productModel.findOne({
|
||||||
|
where: { siteSkus: Like(`%${childSku}%`) },
|
||||||
|
relations: ['components', 'attributes', 'attributes.dict'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new Error(`产品 ${siteProduct.sku} 不存在`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
product,
|
||||||
|
quantity,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备创建产品的 DTO, 处理类型转换和默认值
|
// 准备创建产品的 DTO, 处理类型转换和默认值
|
||||||
|
|
@ -1715,7 +1743,7 @@ export class ProductService {
|
||||||
// 逐条处理记录
|
// 逐条处理记录
|
||||||
for (const rec of records) {
|
for (const rec of records) {
|
||||||
try {
|
try {
|
||||||
const data = this.transformCsvRecordToData(rec);
|
const data = this.mapTableRecordToProduct(rec);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过' });
|
errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过' });
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1723,17 +1751,17 @@ export class ProductService {
|
||||||
const { sku } = data;
|
const { sku } = data;
|
||||||
|
|
||||||
// 查找现有产品
|
// 查找现有产品
|
||||||
const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] });
|
const exist = await this.productModel.findOne({ where: { sku } });
|
||||||
|
|
||||||
if (!exist) {
|
if (!exist) {
|
||||||
// 创建新产品
|
// 创建新产品
|
||||||
const createDTO = this.prepareCreateProductDTO(data);
|
// const createDTO = this.prepareCreateProductDTO(data);
|
||||||
await this.createProduct(createDTO);
|
await this.createProduct(data as CreateProductDTO)
|
||||||
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, data);
|
||||||
updated += 1;
|
updated += 1;
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
@ -2076,4 +2104,111 @@ export class ProductService {
|
||||||
|
|
||||||
return unifiedProduct;
|
return unifiedProduct;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有产品,支持按品牌过滤
|
||||||
|
* @param brand 品牌名称
|
||||||
|
* @returns 所有符合条件的产品
|
||||||
|
*/
|
||||||
|
async getAllProducts(brand?: string): Promise<{ items: Product[], total: number }> {
|
||||||
|
const qb = this.productModel
|
||||||
|
.createQueryBuilder('product')
|
||||||
|
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||||
|
.leftJoinAndSelect('attribute.dict', 'dict')
|
||||||
|
.leftJoinAndSelect('product.category', 'category');
|
||||||
|
|
||||||
|
// 按品牌过滤
|
||||||
|
if (brand) {
|
||||||
|
// 先获取品牌对应的字典项
|
||||||
|
const brandDict = await this.dictModel.findOne({ where: { name: 'brand' } });
|
||||||
|
if (brandDict) {
|
||||||
|
// 查找品牌名称对应的字典项(支持标题和名称匹配)
|
||||||
|
const brandItem = await this.dictItemModel.findOne({
|
||||||
|
where: [
|
||||||
|
{
|
||||||
|
title: brand,
|
||||||
|
dict: { id: brandDict.id }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: brand,
|
||||||
|
dict: { id: brandDict.id }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (brandItem) {
|
||||||
|
qb.andWhere(qb => {
|
||||||
|
const subQuery = qb
|
||||||
|
.subQuery()
|
||||||
|
.select('product_attributes_dict_item.productId')
|
||||||
|
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||||
|
.where('product_attributes_dict_item.dictItemId = :brandId', {
|
||||||
|
brandId: brandItem.id,
|
||||||
|
})
|
||||||
|
.getQuery();
|
||||||
|
return 'product.id IN ' + subQuery;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据类型填充组成信息
|
||||||
|
const items = await qb.getMany();
|
||||||
|
for (const product of items) {
|
||||||
|
if (product.type === 'single') {
|
||||||
|
// 单品不持久化组成,这里仅返回一个基于 SKU 的虚拟组成
|
||||||
|
const component = new ProductStockComponent();
|
||||||
|
component.productId = product.id;
|
||||||
|
component.sku = product.sku;
|
||||||
|
component.quantity = 1;
|
||||||
|
product.components = [component];
|
||||||
|
} else {
|
||||||
|
// 混装商品返回持久化的 SKU 组成
|
||||||
|
product.components = await this.productStockComponentModel.find({
|
||||||
|
where: { productId: product.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保属性按强度正确划分,只保留强度相关的属性
|
||||||
|
// 这里根据需求,如果需要可以进一步过滤或重组属性
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
total: items.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取产品按属性值分组,支持按强度划分
|
||||||
|
* @param brand 品牌名称
|
||||||
|
* @returns 按属性值分组的产品
|
||||||
|
*/
|
||||||
|
async getProductsGroupedByAttribute(brand?: string, attributeName: string = 'strength'): Promise<{ [key: string]: Product[] }> {
|
||||||
|
// 首先获取所有产品
|
||||||
|
const { items } = await this.getAllProducts(brand);
|
||||||
|
|
||||||
|
// 按指定属性分组
|
||||||
|
const groupedProducts: { [key: string]: Product[] } = {};
|
||||||
|
|
||||||
|
items.forEach(product => {
|
||||||
|
// 获取产品的指定属性值
|
||||||
|
const attribute = product.attributes.find(attr => attr.dict.name === attributeName);
|
||||||
|
if (attribute) {
|
||||||
|
const attributeValue = attribute.title || attribute.name;
|
||||||
|
if (!groupedProducts[attributeValue]) {
|
||||||
|
groupedProducts[attributeValue] = [];
|
||||||
|
}
|
||||||
|
groupedProducts[attributeValue].push(product);
|
||||||
|
} else {
|
||||||
|
// 如果没有该属性,放入未分组
|
||||||
|
if (!groupedProducts['未分组']) {
|
||||||
|
groupedProducts['未分组'] = [];
|
||||||
|
}
|
||||||
|
groupedProducts['未分组'].push(product);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupedProducts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { Site } from '../entity/site.entity';
|
||||||
import { UnifiedReviewDTO } from '../dto/site-api.dto';
|
import { UnifiedReviewDTO } from '../dto/site-api.dto';
|
||||||
import { ShopyyGetOneOrderResult, 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,ShopyyGetAllOrdersParams } from '../dto/api.dto';
|
||||||
/**
|
/**
|
||||||
* ShopYY平台服务实现
|
* ShopYY平台服务实现
|
||||||
*/
|
*/
|
||||||
|
|
@ -288,7 +288,7 @@ export class ShopyyService {
|
||||||
* @param pageSize 每页数量
|
* @param pageSize 每页数量
|
||||||
* @returns 分页订单列表
|
* @returns 分页订单列表
|
||||||
*/
|
*/
|
||||||
async getOrders(site: any | number, page: number = 1, pageSize: number = 100, params: UnifiedSearchParamsDTO = {}): Promise<any> {
|
async getOrders(site: any | number, page: number = 1, pageSize: number = 3000, params: ShopyyGetAllOrdersParams = {}): Promise<any> {
|
||||||
// 如果传入的是站点ID,则获取站点配置
|
// 如果传入的是站点ID,则获取站点配置
|
||||||
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
|
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
|
||||||
|
|
||||||
|
|
@ -308,12 +308,11 @@ export class ShopyyService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllOrders(site: any | number, params: Record<string, any> = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise<any> {
|
async getAllOrders(site: any | number, params: ShopyyGetAllOrdersParams = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise<any> {
|
||||||
const firstPage = await this.getOrders(site, 1, 100);
|
const firstPage = await this.getOrders(site, 1, 100, params);
|
||||||
|
|
||||||
const { items: firstPageItems, totalPages} = firstPage;
|
const { items: firstPageItems, totalPages} = firstPage;
|
||||||
|
|
||||||
// const { page = 1, per_page = 100 } = params;
|
|
||||||
// 如果只有一页数据,直接返回
|
// 如果只有一页数据,直接返回
|
||||||
if (totalPages <= 1) {
|
if (totalPages <= 1) {
|
||||||
return firstPageItems;
|
return firstPageItems;
|
||||||
|
|
@ -334,7 +333,7 @@ export class ShopyyService {
|
||||||
// 创建当前批次的并发请求
|
// 创建当前批次的并发请求
|
||||||
for (let i = 0; i < batchSize; i++) {
|
for (let i = 0; i < batchSize; i++) {
|
||||||
const page = currentPage + i;
|
const page = currentPage + i;
|
||||||
const pagePromise = this.getOrders(site, page, 100)
|
const pagePromise = this.getOrders(site, page, 100, params)
|
||||||
.then(pageResult => pageResult.items)
|
.then(pageResult => pageResult.items)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`获取第 ${page} 页数据失败:`, error);
|
console.error(`获取第 ${page} 页数据失败:`, error);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Test script for FreightwavesService createOrder method
|
||||||
|
|
||||||
|
const { FreightwavesService } = require('./dist/service/freightwaves.service');
|
||||||
|
|
||||||
|
async function testFreightwavesService() {
|
||||||
|
try {
|
||||||
|
// Create an instance of the FreightwavesService
|
||||||
|
const service = new FreightwavesService();
|
||||||
|
|
||||||
|
// Call the test method
|
||||||
|
console.log('Starting test for createOrder method...');
|
||||||
|
const result = await service.testQueryOrder();
|
||||||
|
|
||||||
|
console.log('Test completed successfully!');
|
||||||
|
console.log('Result:', result);
|
||||||
|
console.log('\nTo run the actual createOrder request:');
|
||||||
|
console.log('1. Uncomment the createOrder call in the testCreateOrder method');
|
||||||
|
console.log('2. Update the test-secret, test-partner-id with real credentials');
|
||||||
|
console.log('3. Run this script again');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
testFreightwavesService();
|
||||||
Loading…
Reference in New Issue