forked from yoone/API
Compare commits
22 Commits
05a2ca8cfb
...
bff03de8b0
| Author | SHA1 | Date |
|---|---|---|
|
|
bff03de8b0 | |
|
|
86aa5f5790 | |
|
|
52fa7d651e | |
|
|
0ea834218d | |
|
|
86fd31ac12 | |
|
|
75056db42c | |
|
|
d5384944a4 | |
|
|
cb876e8c0f | |
|
|
71b2c249be | |
|
|
b3b7ee4793 | |
|
|
b7101ac866 | |
|
|
72cd20fcd6 | |
|
|
8766cf4a4c | |
|
|
d39341d683 | |
|
|
7f04de4583 | |
|
|
bdac4860df | |
|
|
fff62d6864 | |
|
|
c75c0a614f | |
|
|
bfa03fc6a0 | |
|
|
16539b133f | |
|
|
9fc1bedb0c | |
|
|
0f79b7536a |
|
|
@ -523,6 +523,23 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@faker-js/faker": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fakerjs"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
|
||||||
|
"npm": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@hapi/bourne": {
|
"node_modules/@hapi/bourne": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -393,7 +393,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 || '',
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
UnifiedVariationPaginationDTO,
|
UnifiedVariationPaginationDTO,
|
||||||
CreateReviewDTO,
|
CreateReviewDTO,
|
||||||
UpdateReviewDTO,
|
UpdateReviewDTO,
|
||||||
|
FulfillmentDTO,
|
||||||
} 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 {
|
||||||
|
|
@ -28,10 +29,13 @@ import {
|
||||||
WooWebhook,
|
WooWebhook,
|
||||||
WooOrderSearchParams,
|
WooOrderSearchParams,
|
||||||
WooProductSearchParams,
|
WooProductSearchParams,
|
||||||
|
WpMediaGetListParams,
|
||||||
|
WooFulfillment,
|
||||||
} from '../dto/woocommerce.dto';
|
} from '../dto/woocommerce.dto';
|
||||||
import { Site } from '../entity/site.entity';
|
import { Site } from '../entity/site.entity';
|
||||||
import { WPService } from '../service/wp.service';
|
import { WPService } from '../service/wp.service';
|
||||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
|
import { toArray, toNumber } from '../utils/trans.util';
|
||||||
|
|
||||||
export class WooCommerceAdapter implements ISiteAdapter {
|
export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
// 构造函数接收站点配置与服务实例
|
// 构造函数接收站点配置与服务实例
|
||||||
|
|
@ -249,13 +253,25 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
date_modified: item.date_modified ?? item.modified,
|
date_modified: item.date_modified ?? item.modified,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
mapMediaSearchParams(params: UnifiedSearchParamsDTO): Partial<WpMediaGetListParams> {
|
||||||
|
const page = params.page
|
||||||
|
const per_page = Number( params.per_page ?? 20);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...params.where,
|
||||||
|
page,
|
||||||
|
per_page,
|
||||||
|
// orderby,
|
||||||
|
// order,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 媒体操作方法
|
// 媒体操作方法
|
||||||
async getMedia(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
|
async getMedia(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
|
||||||
// 获取媒体列表并映射为统一媒体DTO集合
|
// 获取媒体列表并映射为统一媒体DTO集合
|
||||||
const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged(
|
const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged(
|
||||||
this.site,
|
this.site,
|
||||||
params
|
this.mapMediaSearchParams(params)
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
items: items.map(this.mapPlatformToUnifiedMedia.bind(this)),
|
items: items.map(this.mapPlatformToUnifiedMedia.bind(this)),
|
||||||
|
|
@ -317,22 +333,11 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
// }
|
// }
|
||||||
const mapped: any = {
|
const mapped: any = {
|
||||||
...(params.search ? { search: params.search } : {}),
|
...(params.search ? { search: params.search } : {}),
|
||||||
// ...(orderBy ? { orderBy } : {}),
|
|
||||||
page,
|
page,
|
||||||
per_page,
|
per_page,
|
||||||
};
|
};
|
||||||
|
|
||||||
const toArray = (value: any): any[] => {
|
|
||||||
if (Array.isArray(value)) return value;
|
|
||||||
if (value === undefined || value === null) return [];
|
|
||||||
return String(value).split(',').map(v => v.trim()).filter(Boolean);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toNumber = (value: any): number | undefined => {
|
|
||||||
if (value === undefined || value === null || value === '') return undefined;
|
|
||||||
const n = Number(value);
|
|
||||||
return Number.isFinite(n) ? n : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 时间过滤参数
|
// 时间过滤参数
|
||||||
if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after);
|
if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after);
|
||||||
|
|
@ -343,8 +348,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
|
|
||||||
// 集合过滤参数
|
// 集合过滤参数
|
||||||
if (where.exclude) mapped.exclude = toArray(where.exclude);
|
if (where.exclude) mapped.exclude = toArray(where.exclude);
|
||||||
if (where.include) mapped.include = toArray(where.include);
|
if (where.ids || where.number || where.id || where.include) mapped.include = [...new Set([where.number,where.id,...toArray(where.ids),...toArray(where.include)])].filter(Boolean);
|
||||||
if (where.ids) mapped.include = toArray(where.ids);
|
|
||||||
if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset);
|
if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset);
|
||||||
if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId);
|
if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId);
|
||||||
if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude);
|
if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude);
|
||||||
|
|
@ -395,13 +399,11 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
// 包含账单地址与收货地址以及创建与更新时间
|
// 包含账单地址与收货地址以及创建与更新时间
|
||||||
|
|
||||||
// 映射物流追踪信息,将后端格式转换为前端期望的格式
|
// 映射物流追踪信息,将后端格式转换为前端期望的格式
|
||||||
const fulfillments = (item.fulfillments || []).map((track: any) => ({
|
const fulfillments = (item.fulfillments || []).map((track) => ({
|
||||||
tracking_number: track.tracking_number || '',
|
tracking_id: track.tracking_id,
|
||||||
shipping_provider: track.shipping_provider || '',
|
tracking_number: track.tracking_number,
|
||||||
shipping_method: track.shipping_method || '',
|
shipping_provider: track.tracking_provider,
|
||||||
status: track.status || '',
|
date_created: track.data_sipped,
|
||||||
date_created: track.date_created || '',
|
|
||||||
items: track.items || [],
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -528,54 +530,25 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
return await this.wpService.getFulfillments(this.site, String(orderId));
|
return await this.wpService.getFulfillments(this.site, String(orderId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrderFulfillment(orderId: string | number, data: {
|
async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise<any> {
|
||||||
tracking_number: string;
|
const shipmentData: Partial<WooFulfillment> = {
|
||||||
shipping_provider: string;
|
tracking_provider: data.shipping_provider,
|
||||||
shipping_method?: string;
|
|
||||||
status?: string;
|
|
||||||
date_created?: string;
|
|
||||||
items?: Array<{
|
|
||||||
order_item_id: number;
|
|
||||||
quantity: number;
|
|
||||||
}>;
|
|
||||||
}): Promise<any> {
|
|
||||||
const shipmentData: any = {
|
|
||||||
shipping_provider: data.shipping_provider,
|
|
||||||
tracking_number: data.tracking_number,
|
tracking_number: data.tracking_number,
|
||||||
};
|
data_sipped: data.date_created,
|
||||||
|
// items: data.items,
|
||||||
if (data.shipping_method) {
|
|
||||||
shipmentData.shipping_method = data.shipping_method;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.status) {
|
|
||||||
shipmentData.status = data.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.date_created) {
|
|
||||||
shipmentData.date_created = data.date_created;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.items) {
|
|
||||||
shipmentData.items = data.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.wpService.createFulfillment(this.site, String(orderId), shipmentData);
|
const response = await this.wpService.createFulfillment(this.site, String(orderId), shipmentData);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: {
|
async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: FulfillmentDTO): Promise<any> {
|
||||||
tracking_number?: string;
|
const shipmentData: Partial<WooFulfillment> = {
|
||||||
shipping_provider?: string;
|
tracking_provider: data.shipping_provider,
|
||||||
shipping_method?: string;
|
tracking_number: data.tracking_number,
|
||||||
status?: string;
|
data_sipped: data.date_created,
|
||||||
date_created?: string;
|
// items: data.items,
|
||||||
items?: Array<{
|
}
|
||||||
order_item_id: number;
|
return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, shipmentData);
|
||||||
quantity: number;
|
|
||||||
}>;
|
|
||||||
}): Promise<any> {
|
|
||||||
return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean> {
|
async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean> {
|
||||||
|
|
@ -633,7 +606,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
sku: data.sku,
|
sku: data.sku,
|
||||||
regular_price: data.regular_price,
|
regular_price: data.regular_price,
|
||||||
sale_price: data.sale_price,
|
sale_price: data.sale_price,
|
||||||
price: data.price,
|
price: data.price,
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,20 @@ export default {
|
||||||
// dataSource: {
|
// dataSource: {
|
||||||
// default: {
|
// default: {
|
||||||
// host: '13.212.62.127',
|
// host: '13.212.62.127',
|
||||||
|
// port: '3306',
|
||||||
// username: 'root',
|
// username: 'root',
|
||||||
// password: 'Yoone!@.2025',
|
// password: 'Yoone!@.2025',
|
||||||
|
// database: 'inventory_v2',
|
||||||
|
// synchronize: true,
|
||||||
|
// logging: true,
|
||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
typeorm: {
|
typeorm: {
|
||||||
dataSource: {
|
dataSource: {
|
||||||
default: {
|
default: {
|
||||||
host: '13.212.62.127',
|
host: 'localhost',
|
||||||
port: "3306",
|
port: "23306",
|
||||||
username: 'root',
|
username: 'root',
|
||||||
password: 'Yoone!@.2025',
|
password: 'Yoone!@.2025',
|
||||||
database: 'inventory_v2',
|
database: 'inventory_v2',
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,31 @@ export class ProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: '成功返回分组后的产品列表',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/Product',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@Get('/list/grouped')
|
||||||
|
async getProductListGrouped(
|
||||||
|
@Query() query: UnifiedSearchParamsDTO<ProductWhereFilter>
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.getProductListGrouped(query);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('获取分组产品列表失败', error);
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOkResponse({ type: ProductRes })
|
@ApiOkResponse({ type: ProductRes })
|
||||||
@Post('/')
|
@Post('/')
|
||||||
async createProduct(@Body() productData: CreateProductDTO) {
|
async createProduct(@Body() productData: CreateProductDTO) {
|
||||||
|
|
@ -750,4 +775,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,30 @@ export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
|
||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '分组字段,例如 "categoryId"',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
groupBy?: 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=支付时间
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,16 @@ export class ShipmentBookDTO {
|
||||||
@ApiProperty({ type: 'number', isArray: true })
|
@ApiProperty({ type: 'number', isArray: true })
|
||||||
@Rule(RuleType.array<number>().default([]))
|
@Rule(RuleType.array<number>().default([]))
|
||||||
orderIds?: number[];
|
orderIds?: number[];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
shipmentPlatform: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ShipmentFeeBookDTO {
|
export class ShipmentFeeBookDTO {
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
shipmentPlatform: string;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
stockPointId: number;
|
stockPointId: number;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|
@ -63,6 +70,8 @@ export class ShipmentFeeBookDTO {
|
||||||
weight: number;
|
weight: number;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
weightUom: string;
|
weightUom: string;
|
||||||
|
@ApiProperty()
|
||||||
|
address_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PaymentMethodDTO {
|
export class PaymentMethodDTO {
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1211,6 +1211,7 @@ export interface ShopyyOrder {
|
||||||
// 时间戳信息
|
// 时间戳信息
|
||||||
// ========================================
|
// ========================================
|
||||||
// 订单创建时间
|
// 订单创建时间
|
||||||
|
date_paid?: number | string;
|
||||||
created_at?: number | string;
|
created_at?: number | string;
|
||||||
// 订单添加时间
|
// 订单添加时间
|
||||||
date_added?: string;
|
date_added?: string;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
UnifiedPaginationDTO,
|
UnifiedPaginationDTO,
|
||||||
} from './api.dto';
|
} from './api.dto';
|
||||||
import { Dict } from '../entity/dict.entity';
|
import { Dict } from '../entity/dict.entity';
|
||||||
|
import { Product } from '../entity/product.entity';
|
||||||
// export class UnifiedOrderWhere{
|
// export class UnifiedOrderWhere{
|
||||||
// []
|
// []
|
||||||
// }
|
// }
|
||||||
|
|
@ -306,17 +307,7 @@ export class UnifiedProductDTO {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
erpProduct?: {
|
erpProduct?: Product
|
||||||
id: number;
|
|
||||||
sku: string;
|
|
||||||
name: string;
|
|
||||||
nameCn?: string;
|
|
||||||
category?: any;
|
|
||||||
attributes?: any[];
|
|
||||||
components?: any[];
|
|
||||||
price: number;
|
|
||||||
promotionPrice: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnifiedOrderRefundDTO {
|
export class UnifiedOrderRefundDTO {
|
||||||
|
|
@ -808,14 +799,16 @@ export class UpdateWebhookDTO {
|
||||||
|
|
||||||
|
|
||||||
export class FulfillmentItemDTO {
|
export class FulfillmentItemDTO {
|
||||||
@ApiProperty({ description: '订单项ID' })
|
@ApiProperty({ description: '订单项ID' ,required: false})
|
||||||
order_item_id: number;
|
order_item_id: number;
|
||||||
|
|
||||||
@ApiProperty({ description: '数量' })
|
@ApiProperty({ description: '数量' ,required:false})
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FulfillmentDTO {
|
export class FulfillmentDTO {
|
||||||
|
@ApiProperty({ description: '物流id', required: false })
|
||||||
|
tracking_id?: string;
|
||||||
@ApiProperty({ description: '物流单号', required: false })
|
@ApiProperty({ description: '物流单号', required: false })
|
||||||
tracking_number?: string;
|
tracking_number?: string;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -370,17 +370,24 @@ export interface WooOrder {
|
||||||
date_modified?: string;
|
date_modified?: string;
|
||||||
date_modified_gmt?: string;
|
date_modified_gmt?: string;
|
||||||
// 物流追踪信息
|
// 物流追踪信息
|
||||||
fulfillments?: Array<{
|
fulfillments?: WooFulfillment[];
|
||||||
tracking_number?: string;
|
}
|
||||||
shipping_provider?: string;
|
// 这个是一个插件的物流追踪信息
|
||||||
shipping_method?: string;
|
// 接口:
|
||||||
status?: string;
|
export interface WooFulfillment {
|
||||||
date_created?: string;
|
data_sipped: string;
|
||||||
items?: Array<{
|
tracking_id: string;
|
||||||
order_item_id?: number;
|
tracking_link: string;
|
||||||
quantity?: number;
|
tracking_number: string;
|
||||||
}>;
|
tracking_provider: string;
|
||||||
}>;
|
}
|
||||||
|
// https://docs.zorem.com/docs/ast-free/developers/adding-tracking-info-to-orders/
|
||||||
|
export interface WooFulfillmentCreateParams {
|
||||||
|
order_id: string;
|
||||||
|
tracking_provider: string;
|
||||||
|
tracking_number: string;
|
||||||
|
date_shipped?: string;
|
||||||
|
status_shipped?: string;
|
||||||
}
|
}
|
||||||
export interface WooOrderRefund {
|
export interface WooOrderRefund {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
@ -552,7 +559,8 @@ export interface WooOrderSearchParams {
|
||||||
order: string;
|
order: string;
|
||||||
orderby: string;
|
orderby: string;
|
||||||
parant: string[];
|
parant: string[];
|
||||||
status: (WooOrderStatusSearchParams)[];
|
parent_exclude: string[];
|
||||||
|
status: WooOrderStatusSearchParams[];
|
||||||
customer: number;
|
customer: number;
|
||||||
product: number;
|
product: number;
|
||||||
dp: number;
|
dp: number;
|
||||||
|
|
@ -616,6 +624,83 @@ export interface ListParams {
|
||||||
parant: string[];
|
parant: string[];
|
||||||
parent_exclude: string[];
|
parent_exclude: string[];
|
||||||
}
|
}
|
||||||
|
export interface WpMediaGetListParams {
|
||||||
|
// 请求范围,决定响应中包含的字段
|
||||||
|
// 默认: view
|
||||||
|
// 可选值: view, embed, edit
|
||||||
|
context?: 'view' | 'embed' | 'edit';
|
||||||
|
|
||||||
|
// 当前页码
|
||||||
|
// 默认: 1
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
// 每页最大返回数量
|
||||||
|
// 默认: 10
|
||||||
|
per_page?: number;
|
||||||
|
|
||||||
|
// 搜索字符串,限制结果匹配
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
// ISO8601格式日期,限制发布时间之后的结果
|
||||||
|
after?: string;
|
||||||
|
|
||||||
|
// ISO8601格式日期,限制修改时间之后的结果
|
||||||
|
modified_after?: string;
|
||||||
|
|
||||||
|
// 作者ID数组,限制结果集为特定作者
|
||||||
|
author?: number[];
|
||||||
|
|
||||||
|
// 作者ID数组,排除特定作者的结果
|
||||||
|
author_exclude?: number[];
|
||||||
|
|
||||||
|
// ISO8601格式日期,限制发布时间之前的结果
|
||||||
|
before?: string;
|
||||||
|
|
||||||
|
// ISO8601格式日期,限制修改时间之前的结果
|
||||||
|
modified_before?: string;
|
||||||
|
|
||||||
|
// ID数组,排除特定ID的结果
|
||||||
|
exclude?: number[];
|
||||||
|
|
||||||
|
// ID数组,限制结果集为特定ID
|
||||||
|
include?: number[];
|
||||||
|
|
||||||
|
// 结果集偏移量
|
||||||
|
offset?: number;
|
||||||
|
|
||||||
|
// 排序方向
|
||||||
|
// 默认: desc
|
||||||
|
// 可选值: asc, desc
|
||||||
|
order?: 'asc' | 'desc';
|
||||||
|
|
||||||
|
// 排序字段
|
||||||
|
// 默认: date
|
||||||
|
// 可选值: author, date, id, include, modified, parent, relevance, slug, include_slugs, title
|
||||||
|
orderby?: 'author' | 'date' | 'id' | 'include' | 'modified' | 'parent' | 'relevance' | 'slug' | 'include_slugs' | 'title';
|
||||||
|
|
||||||
|
// 父ID数组,限制结果集为特定父ID
|
||||||
|
parent?: number[];
|
||||||
|
|
||||||
|
// 父ID数组,排除特定父ID的结果
|
||||||
|
parent_exclude?: number[];
|
||||||
|
|
||||||
|
// 搜索的列名数组
|
||||||
|
search_columns?: string[];
|
||||||
|
|
||||||
|
// slug数组,限制结果集为特定slug
|
||||||
|
slug?: string[];
|
||||||
|
|
||||||
|
// 状态数组,限制结果集为特定状态
|
||||||
|
// 默认: inherit
|
||||||
|
status?: string[];
|
||||||
|
|
||||||
|
// 媒体类型,限制结果集为特定媒体类型
|
||||||
|
// 可选值: image, video, text, application, audio
|
||||||
|
media_type?: 'image' | 'video' | 'text' | 'application' | 'audio';
|
||||||
|
|
||||||
|
// MIME类型,限制结果集为特定MIME类型
|
||||||
|
mime_type?: string;
|
||||||
|
}
|
||||||
export enum WooContext {
|
export enum WooContext {
|
||||||
view,
|
view,
|
||||||
edit
|
edit
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,16 @@ 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()
|
||||||
productId: number;
|
productId: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@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,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,9 @@ export class Shipment {
|
||||||
tracking_provider?: string;
|
tracking_provider?: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@Column()
|
@Column({ nullable: true })
|
||||||
@Expose()
|
@Expose()
|
||||||
unique_id: string;
|
unique_id?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
@Expose()
|
@Expose()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { ILogger, Inject, Logger } from '@midwayjs/core';
|
||||||
|
import { IJob, Job } from '@midwayjs/cron';
|
||||||
|
import { LogisticsService } from '../service/logistics.service';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Shipment } from '../entity/shipment.entity';
|
||||||
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
|
|
||||||
|
|
||||||
|
@Job({
|
||||||
|
cronTime: '0 0 12 * * *', // 每天12点执行
|
||||||
|
start: true
|
||||||
|
})
|
||||||
|
export class SyncTmsJob implements IJob {
|
||||||
|
@Logger()
|
||||||
|
logger: ILogger;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
logisticsService: LogisticsService;
|
||||||
|
|
||||||
|
@InjectEntityModel(Shipment)
|
||||||
|
shipmentModel: Repository<Shipment>
|
||||||
|
|
||||||
|
async onTick() {
|
||||||
|
const shipments:Shipment[] = await this.shipmentModel.findBy({ tracking_provider: 'freightwaves',finished: false });
|
||||||
|
const results = await Promise.all(
|
||||||
|
shipments.map(async shipment => {
|
||||||
|
return await this.logisticsService.updateFreightwavesShipmentState(shipment);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
this.logger.info(`更新运单状态完毕 ${JSON.stringify(results)}`);
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
onComplete(result: any) {
|
||||||
|
this.logger.info(`更新运单状态完成 ${result}`);
|
||||||
|
}
|
||||||
|
onError(error: any) {
|
||||||
|
this.logger.error(`更新运单状态失败 ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,8 @@ export class CategoryService {
|
||||||
order: {
|
order: {
|
||||||
sort: 'DESC',
|
sort: 'DESC',
|
||||||
createdAt: 'DESC'
|
createdAt: 'DESC'
|
||||||
}
|
},
|
||||||
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ interface Declaration {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 费用试算请求接口
|
// 费用试算请求接口
|
||||||
interface RateTryRequest {
|
export interface RateTryRequest {
|
||||||
shipCompany: string;
|
shipCompany: string;
|
||||||
partnerOrderNumber: string;
|
partnerOrderNumber: string;
|
||||||
warehouseId?: string;
|
warehouseId?: string;
|
||||||
|
|
@ -118,8 +118,8 @@ interface RateTryResponseData {
|
||||||
|
|
||||||
// 创建订单响应数据接口
|
// 创建订单响应数据接口
|
||||||
interface CreateOrderResponseData {
|
interface CreateOrderResponseData {
|
||||||
partnerOrderNumber: string;
|
msg: string;
|
||||||
shipOrderId: string;
|
data: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询订单响应数据接口
|
// 查询订单响应数据接口
|
||||||
|
|
@ -140,10 +140,10 @@ interface QueryOrderResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改订单响应数据接口
|
// 修改订单响应数据接口
|
||||||
interface ModifyOrderResponseData extends CreateOrderResponseData {}
|
interface ModifyOrderResponseData extends CreateOrderResponseData { }
|
||||||
|
|
||||||
// 订单退款响应数据接口
|
// 订单退款响应数据接口
|
||||||
interface RefundOrderResponseData {}
|
interface RefundOrderResponseData { }
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class FreightwavesService {
|
export class FreightwavesService {
|
||||||
|
|
@ -152,8 +152,8 @@ export class FreightwavesService {
|
||||||
// 默认配置
|
// 默认配置
|
||||||
private config: FreightwavesConfig = {
|
private config: FreightwavesConfig = {
|
||||||
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
|
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
|
||||||
apiBaseUrl: 'https://tms.freightwaves.ca',
|
apiBaseUrl: 'http://tms.freightwaves.ca:8901/',
|
||||||
partner: '25072621035200000060',
|
partner: '25072621035200000060'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化配置
|
// 初始化配置
|
||||||
|
|
@ -180,19 +180,19 @@ export class FreightwavesService {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 记录请求前的详细信息
|
// 记录请求前的详细信息
|
||||||
console.log(`Sending request to: ${this.config.apiBaseUrl}${url}`,JSON.stringify({
|
console.log(`Sending request to: ${this.config.apiBaseUrl}${url}`, JSON.stringify({
|
||||||
headers,
|
headers,
|
||||||
data
|
data
|
||||||
}))
|
}))
|
||||||
console.log('Request data:', `${this.config.apiBaseUrl}${url}`, data,headers);
|
console.log('Request data:', `${this.config.apiBaseUrl}${url}`, data, headers);
|
||||||
// 发送请求 - 临时禁用SSL证书验证以解决UNABLE_TO_VERIFY_LEAF_SIGNATURE错误
|
// 发送请求 - 临时禁用SSL证书验证以解决UNABLE_TO_VERIFY_LEAF_SIGNATURE错误
|
||||||
const response = await axios.post<FreightwavesResponse<T>>(
|
const response = await axios.post<FreightwavesResponse<T>>(
|
||||||
`${this.config.apiBaseUrl}${url}`,
|
`${this.config.apiBaseUrl}${url}`,
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
headers,
|
headers,
|
||||||
httpsAgent: new (require('https').Agent)({
|
httpsAgent: new (require('https').Agent)({
|
||||||
rejectUnauthorized: false
|
rejectUnauthorized: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -267,8 +267,8 @@ export class FreightwavesService {
|
||||||
partner: this.config.partner,
|
partner: this.config.partner,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await this.sendRequest<CreateOrderResponseData>('shipService/order/rateTry', requestData);
|
const response = await this.sendRequest<CreateOrderResponseData>('shipService/order/createOrder', requestData);
|
||||||
return response.data;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -283,6 +283,9 @@ export class FreightwavesService {
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await this.sendRequest<QueryOrderResponseData>('/shipService/order/queryOrder', requestData);
|
const response = await this.sendRequest<QueryOrderResponseData>('/shipService/order/queryOrder', requestData);
|
||||||
|
if (response.code !== '00000200') {
|
||||||
|
throw new Error(response.msg);
|
||||||
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -331,9 +334,9 @@ export class FreightwavesService {
|
||||||
|
|
||||||
// 准备测试数据
|
// 准备测试数据
|
||||||
const testParams: Omit<CreateOrderRequest, 'partner'> = {
|
const testParams: Omit<CreateOrderRequest, 'partner'> = {
|
||||||
shipCompany: '',
|
shipCompany: 'UPSYYZ7000NEW',
|
||||||
partnerOrderNumber: `test-order-${Date.now()}`,
|
partnerOrderNumber: `test-order-${Date.now()}`,
|
||||||
warehouseId: '25072621035200000060',
|
warehouseId: '25072621030107400060',
|
||||||
shipper: {
|
shipper: {
|
||||||
name: 'John Doe',
|
name: 'John Doe',
|
||||||
phone: '123-456-7890',
|
phone: '123-456-7890',
|
||||||
|
|
@ -397,12 +400,12 @@ export class FreightwavesService {
|
||||||
// 调用创建订单方法
|
// 调用创建订单方法
|
||||||
this.log('开始测试创建订单...');
|
this.log('开始测试创建订单...');
|
||||||
this.log('测试参数:', testParams);
|
this.log('测试参数:', testParams);
|
||||||
|
|
||||||
// 注意:在实际环境中取消注释以下行来执行真实请求
|
// 注意:在实际环境中取消注释以下行来执行真实请求
|
||||||
const result = await this.createOrder(testParams);
|
const result = await this.createOrder(testParams);
|
||||||
this.log('创建订单成功:', result);
|
this.log('创建订单成功:', result);
|
||||||
|
|
||||||
|
|
||||||
// 返回模拟结果
|
// 返回模拟结果
|
||||||
return {
|
return {
|
||||||
partnerOrderNumber: testParams.partnerOrderNumber,
|
partnerOrderNumber: testParams.partnerOrderNumber,
|
||||||
|
|
@ -414,6 +417,95 @@ export class FreightwavesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试费用试算方法
|
||||||
|
* @returns 费用试算结果
|
||||||
|
*/
|
||||||
|
async testRateTry() {
|
||||||
|
try {
|
||||||
|
// 设置必要的配置
|
||||||
|
this.setConfig({
|
||||||
|
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
|
||||||
|
apiBaseUrl: 'http://tms.freightwaves.ca:8901',
|
||||||
|
partner: '25072621035200000060'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 准备测试数据 - 符合RateTryRequest接口要求
|
||||||
|
const testParams: Omit<RateTryRequest, 'partner'> = {
|
||||||
|
shipCompany: 'UPSYYZ7000NEW',
|
||||||
|
partnerOrderNumber: `test-rate-try-${Date.now()}`,
|
||||||
|
warehouseId: '25072621030107400060',
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
signService: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用费用试算方法
|
||||||
|
this.log('开始测试费用试算...');
|
||||||
|
this.log('测试参数:', testParams);
|
||||||
|
|
||||||
|
// 注意:在实际环境中取消注释以下行来执行真实请求
|
||||||
|
const result = await this.rateTry(testParams);
|
||||||
|
this.log('费用试算成功:', result);
|
||||||
|
|
||||||
|
this.log('测试完成:费用试算方法调用成功(模拟)');
|
||||||
|
this.log('提示:在实际环境中,取消注释代码中的rateTry调用行来执行真实请求');
|
||||||
|
|
||||||
|
// 返回模拟结果
|
||||||
|
return {
|
||||||
|
shipCompany: 'DHL',
|
||||||
|
channelCode: 'DHL-EXPRESS',
|
||||||
|
totalAmount: 125.50,
|
||||||
|
currency: 'CAD'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.log('测试费用试算失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试查询订单方法
|
* 测试查询订单方法
|
||||||
* @returns 查询订单结果
|
* @returns 查询订单结果
|
||||||
|
|
@ -423,7 +515,7 @@ export class FreightwavesService {
|
||||||
// 设置必要的配置
|
// 设置必要的配置
|
||||||
this.setConfig({
|
this.setConfig({
|
||||||
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
|
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
|
||||||
apiBaseUrl: 'http://freightwaves.ca:8901/shipService/order/rateTry',
|
apiBaseUrl: 'http://freightwaves.ca:8901',
|
||||||
partner: '25072621035200000060'
|
partner: '25072621035200000060'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -437,13 +529,13 @@ export class FreightwavesService {
|
||||||
// 调用查询订单方法
|
// 调用查询订单方法
|
||||||
this.log('开始测试查询订单...');
|
this.log('开始测试查询订单...');
|
||||||
this.log('测试参数:', testParams);
|
this.log('测试参数:', testParams);
|
||||||
|
|
||||||
// 注意:在实际环境中取消注释以下行来执行真实请求
|
// 注意:在实际环境中取消注释以下行来执行真实请求
|
||||||
const result = await this.queryOrder(testParams);
|
const result = await this.queryOrder(testParams);
|
||||||
this.log('查询订单成功:', result);
|
this.log('查询订单成功:', result);
|
||||||
|
|
||||||
this.log('测试完成:查询订单方法调用成功(模拟)');
|
this.log('测试完成:查询订单方法调用成功(模拟)');
|
||||||
|
|
||||||
// 返回模拟结果
|
// 返回模拟结果
|
||||||
return {
|
return {
|
||||||
thirdOrderId: 'thirdOrder-123456789',
|
thirdOrderId: 'thirdOrder-123456789',
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { CanadaPostService } from './canadaPost.service';
|
||||||
import { OrderItem } from '../entity/order_item.entity';
|
import { OrderItem } from '../entity/order_item.entity';
|
||||||
import { OrderSale } from '../entity/order_sale.entity';
|
import { OrderSale } from '../entity/order_sale.entity';
|
||||||
import { UniExpressService } from './uni_express.service';
|
import { UniExpressService } from './uni_express.service';
|
||||||
|
import { FreightwavesService, RateTryRequest } from './freightwaves.service';
|
||||||
import { StockPoint } from '../entity/stock_point.entity';
|
import { StockPoint } from '../entity/stock_point.entity';
|
||||||
import { OrderService } from './order.service';
|
import { OrderService } from './order.service';
|
||||||
import { convertKeysFromCamelToSnake } from '../utils/object-transform.util';
|
import { convertKeysFromCamelToSnake } from '../utils/object-transform.util';
|
||||||
|
|
@ -73,6 +74,9 @@ export class LogisticsService {
|
||||||
@Inject()
|
@Inject()
|
||||||
uniExpressService: UniExpressService;
|
uniExpressService: UniExpressService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
freightwavesService: FreightwavesService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
wpService: WPService;
|
wpService: WPService;
|
||||||
|
|
||||||
|
|
@ -126,8 +130,8 @@ export class LogisticsService {
|
||||||
const data = await this.uniExpressService.getOrderStatus(shipment.return_tracking_number);
|
const data = await this.uniExpressService.getOrderStatus(shipment.return_tracking_number);
|
||||||
console.log('updateShipmentState data:', data);
|
console.log('updateShipmentState data:', data);
|
||||||
// huo
|
// huo
|
||||||
if(data.status === 'FAIL'){
|
if (data.status === 'FAIL') {
|
||||||
throw new Error('获取运单状态失败,原因为'+ data.ret_msg)
|
throw new Error('获取运单状态失败,原因为' + data.ret_msg)
|
||||||
}
|
}
|
||||||
shipment.state = data.data[0].state;
|
shipment.state = data.data[0].state;
|
||||||
if (shipment.state in [203, 215, 216, 230]) { // todo,写常数
|
if (shipment.state in [203, 215, 216, 230]) { // todo,写常数
|
||||||
|
|
@ -141,6 +145,30 @@ export class LogisticsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//"expressFinish": 0, //是否快递创建完成(1:完成 0:未完成,需要轮询 2:失败)
|
||||||
|
async updateFreightwavesShipmentState(shipment: Shipment) {
|
||||||
|
try {
|
||||||
|
const data = await this.freightwavesService.queryOrder({ shipOrderId: shipment.order_id.toString() });
|
||||||
|
console.log('updateFreightwavesShipmentState data:', data);
|
||||||
|
// huo
|
||||||
|
if (data.expressFinish === 2) {
|
||||||
|
throw new Error('获取运单状态失败,原因为' + data.expressFailMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.expressFinish === 0) {
|
||||||
|
shipment.state = '203';
|
||||||
|
shipment.finished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shipmentModel.save(shipment);
|
||||||
|
return shipment.state;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
// throw new Error(`更新运单状态失败 ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async updateShipmentStateById(id: number) {
|
async updateShipmentStateById(id: number) {
|
||||||
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id });
|
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id });
|
||||||
return this.updateShipmentState(shipment);
|
return this.updateShipmentState(shipment);
|
||||||
|
|
@ -294,7 +322,18 @@ export class LogisticsService {
|
||||||
currency: 'CAD',
|
currency: 'CAD',
|
||||||
// item_description: data.sales, // todo: 货品信息
|
// item_description: data.sales, // todo: 货品信息
|
||||||
}
|
}
|
||||||
const resShipmentFee = await this.uniExpressService.getRates(reqBody);
|
let resShipmentFee: any;
|
||||||
|
if (data.shipmentPlatform === 'uniuni') {
|
||||||
|
resShipmentFee = await this.uniExpressService.getRates(reqBody);
|
||||||
|
} else if (data.shipmentPlatform === 'freightwaves') {
|
||||||
|
|
||||||
|
|
||||||
|
// resShipmentFee = await this.freightwavesService.rateTry(reqBody);
|
||||||
|
} else {
|
||||||
|
throw new Error('不支持的运单平台');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (resShipmentFee.status !== 'SUCCESS') {
|
if (resShipmentFee.status !== 'SUCCESS') {
|
||||||
throw new Error(resShipmentFee.ret_msg);
|
throw new Error(resShipmentFee.ret_msg);
|
||||||
}
|
}
|
||||||
|
|
@ -319,40 +358,49 @@ export class LogisticsService {
|
||||||
|
|
||||||
let resShipmentOrder;
|
let resShipmentOrder;
|
||||||
try {
|
try {
|
||||||
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
|
//const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
|
||||||
const reqBody = {
|
// const reqBody = {
|
||||||
sender: data.details.origin.contact_name,
|
// sender: data.details.origin.contact_name,
|
||||||
start_phone: data.details.origin.phone_number,
|
// start_phone: data.details.origin.phone_number,
|
||||||
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
|
// start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
|
||||||
pickup_address: data.details.origin.address.address_line_1,
|
// pickup_address: data.details.origin.address.address_line_1,
|
||||||
pickup_warehouse: stock_point.upStreamStockPointId,
|
// pickup_warehouse: stock_point.upStreamStockPointId,
|
||||||
shipper_country_code: data.details.origin.address.country,
|
// shipper_country_code: data.details.origin.address.country,
|
||||||
receiver: data.details.destination.contact_name,
|
// receiver: data.details.destination.contact_name,
|
||||||
city: data.details.destination.address.city,
|
// city: data.details.destination.address.city,
|
||||||
province: data.details.destination.address.region,
|
// province: data.details.destination.address.region,
|
||||||
country: data.details.destination.address.country,
|
// country: data.details.destination.address.country,
|
||||||
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
|
// postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
|
||||||
delivery_address: data.details.destination.address.address_line_1,
|
// delivery_address: data.details.destination.address.address_line_1,
|
||||||
receiver_phone: data.details.destination.phone_number.number,
|
// receiver_phone: data.details.destination.phone_number.number,
|
||||||
receiver_email: data.details.destination.email_addresses,
|
// receiver_email: data.details.destination.email_addresses,
|
||||||
// item_description: data.sales, // todo: 货品信息
|
// // item_description: data.sales, // todo: 货品信息
|
||||||
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
|
// length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
|
||||||
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
|
// width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
|
||||||
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
|
// height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
|
||||||
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
|
// dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
|
||||||
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
|
// weight: data.details.packaging_properties.packages[0].measurements.weight.value,
|
||||||
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
|
// weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
|
||||||
currency: 'CAD',
|
// currency: 'CAD',
|
||||||
custom_field: {
|
// custom_field: {
|
||||||
'order_id': order.externalOrderId
|
// 'order_id': order.externalOrderId
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 添加运单
|
resShipmentOrder = await this.mepShipment(data, order);
|
||||||
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
|
|
||||||
|
// if (data.shipmentPlatform === 'uniuni') {
|
||||||
|
// // 添加运单
|
||||||
|
// resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (data.shipmentPlatform === 'freightwaves') {
|
||||||
|
// // 添加运单
|
||||||
|
// resShipmentOrder = await this.freightcomService.createShipment(reqBody);
|
||||||
|
// }
|
||||||
|
|
||||||
// 记录物流信息,并将订单状态转到完成
|
// 记录物流信息,并将订单状态转到完成
|
||||||
if (resShipmentOrder.status === 'SUCCESS') {
|
if (resShipmentOrder.status === 'SUCCESS' || resShipmentOrder.code === '00000200') {
|
||||||
order.orderStatus = ErpOrderStatus.COMPLETED;
|
order.orderStatus = ErpOrderStatus.COMPLETED;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('运单生成失败');
|
throw new Error('运单生成失败');
|
||||||
|
|
@ -363,12 +411,24 @@ export class LogisticsService {
|
||||||
await dataSource.transaction(async manager => {
|
await dataSource.transaction(async manager => {
|
||||||
const orderRepo = manager.getRepository(Order);
|
const orderRepo = manager.getRepository(Order);
|
||||||
const shipmentRepo = manager.getRepository(Shipment);
|
const shipmentRepo = manager.getRepository(Shipment);
|
||||||
const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数
|
const tracking_provider = data.shipmentPlatform; // todo: id未确定,后写进常数
|
||||||
|
|
||||||
// 同步物流信息到woocommerce
|
// 同步物流信息到woocommerce
|
||||||
const site = await this.siteService.get(Number(order.siteId), true);
|
const site = await this.siteService.get(Number(order.siteId), true);
|
||||||
|
let co: any;
|
||||||
|
let unique_id: any;
|
||||||
|
let state: any;
|
||||||
|
if (data.shipmentPlatform === 'uniuni') {
|
||||||
|
co = resShipmentOrder.data.tno;
|
||||||
|
unique_id = resShipmentOrder.data.uni_order_sn;
|
||||||
|
state = resShipmentOrder.data.uni_status_code;
|
||||||
|
} else {
|
||||||
|
co = resShipmentOrder.data?.shipOrderId;
|
||||||
|
unique_id = resShipmentOrder.data?.shipOrderId;
|
||||||
|
state = ErpOrderStatus.COMPLETED;
|
||||||
|
}
|
||||||
const res = await this.wpService.createFulfillment(site, order.externalOrderId, {
|
const res = await this.wpService.createFulfillment(site, order.externalOrderId, {
|
||||||
tracking_number: resShipmentOrder.data.tno,
|
tracking_number: co,
|
||||||
tracking_provider: tracking_provider,
|
tracking_provider: tracking_provider,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -376,10 +436,10 @@ export class LogisticsService {
|
||||||
const shipment = await shipmentRepo.save({
|
const shipment = await shipmentRepo.save({
|
||||||
tracking_provider: tracking_provider,
|
tracking_provider: tracking_provider,
|
||||||
tracking_id: res.data.tracking_id,
|
tracking_id: res.data.tracking_id,
|
||||||
unique_id: resShipmentOrder.data.uni_order_sn,
|
unique_id: unique_id,
|
||||||
stockPointId: String(data.stockPointId), // todo
|
stockPointId: String(data.stockPointId), // todo
|
||||||
state: resShipmentOrder.data.uni_status_code,
|
state: state,
|
||||||
return_tracking_number: resShipmentOrder.data.tno,
|
return_tracking_number: co,
|
||||||
fee: data.details.shipmentFee,
|
fee: data.details.shipmentFee,
|
||||||
order: order
|
order: order
|
||||||
});
|
});
|
||||||
|
|
@ -388,12 +448,15 @@ export class LogisticsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步订单状态到woocommerce
|
// 同步订单状态到woocommerce
|
||||||
if (order.status !== OrderStatus.COMPLETED) {
|
if (order.source_type != "shopyy") {
|
||||||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
if (order.status !== OrderStatus.COMPLETED) {
|
||||||
status: OrderStatus.COMPLETED,
|
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||||||
});
|
status: OrderStatus.COMPLETED,
|
||||||
order.status = OrderStatus.COMPLETED;
|
});
|
||||||
|
order.status = OrderStatus.COMPLETED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
order.orderStatus = ErpOrderStatus.COMPLETED;
|
order.orderStatus = ErpOrderStatus.COMPLETED;
|
||||||
|
|
||||||
await orderRepo.save(order);
|
await orderRepo.save(order);
|
||||||
|
|
@ -642,4 +705,208 @@ export class LogisticsService {
|
||||||
|
|
||||||
return { items, total, current, pageSize };
|
return { items, total, current, pageSize };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async mepShipment(data: ShipmentBookDTO, order: Order) {
|
||||||
|
try {
|
||||||
|
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
|
||||||
|
let resShipmentOrder;
|
||||||
|
|
||||||
|
if (data.shipmentPlatform === 'uniuni') {
|
||||||
|
const reqBody = {
|
||||||
|
sender: data.details.origin.contact_name,
|
||||||
|
start_phone: data.details.origin.phone_number,
|
||||||
|
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
|
||||||
|
pickup_address: data.details.origin.address.address_line_1,
|
||||||
|
pickup_warehouse: stock_point.upStreamStockPointId,
|
||||||
|
shipper_country_code: data.details.origin.address.country,
|
||||||
|
receiver: data.details.destination.contact_name,
|
||||||
|
city: data.details.destination.address.city,
|
||||||
|
province: data.details.destination.address.region,
|
||||||
|
country: data.details.destination.address.country,
|
||||||
|
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
|
||||||
|
delivery_address: data.details.destination.address.address_line_1,
|
||||||
|
receiver_phone: data.details.destination.phone_number.number,
|
||||||
|
receiver_email: data.details.destination.email_addresses,
|
||||||
|
// item_description: data.sales, // todo: 货品信息
|
||||||
|
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
|
||||||
|
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
|
||||||
|
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
|
||||||
|
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
|
||||||
|
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
|
||||||
|
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
|
||||||
|
currency: 'CAD',
|
||||||
|
custom_field: {
|
||||||
|
'order_id': order.externalOrderId // todo: 需要获取订单的externalOrderId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加运单
|
||||||
|
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.shipmentPlatform === 'freightwaves') {
|
||||||
|
// 根据TMS系统对接说明文档格式化参数
|
||||||
|
const reqBody: any = {
|
||||||
|
shipCompany: 'UPSYYZ7000NEW',
|
||||||
|
partnerOrderNumber: order.siteId + '-1-' + order.externalOrderId,
|
||||||
|
warehouseId: '25072621030107400060',
|
||||||
|
shipper: {
|
||||||
|
name: data.details.origin.contact_name, // 姓名
|
||||||
|
phone: data.details.origin.phone_number.number, // 电话(提取number属性转换为字符串)
|
||||||
|
company: '', // 公司
|
||||||
|
countryCode: data.details.origin.address.country, // 国家Code
|
||||||
|
city: data.details.origin.address.city, // 城市
|
||||||
|
state: data.details.origin.address.region, // 州/省Code,两个字母缩写
|
||||||
|
address1: data.details.origin.address.address_line_1, // 详细地址
|
||||||
|
address2: '', // 详细地址2(Address类型中没有address_line_2属性)
|
||||||
|
postCode: data.details.origin.address.postal_code.replace(/\s/g, ''), // 邮编
|
||||||
|
countryName: data.details.origin.address.country, // 国家名称(Address类型中没有country_name属性,使用country代替)
|
||||||
|
cityName: data.details.origin.address.city, // 城市名称
|
||||||
|
stateName: data.details.origin.address.region, // 州/省名称
|
||||||
|
companyName: '' // 公司名称
|
||||||
|
},
|
||||||
|
reciver: {
|
||||||
|
name: data.details.destination.contact_name, // 姓名
|
||||||
|
phone: data.details.destination.phone_number.number, // 电话
|
||||||
|
company: '', // 公司
|
||||||
|
countryCode: data.details.destination.address.country, // 国家Code
|
||||||
|
city: data.details.destination.address.city, // 城市
|
||||||
|
state: data.details.destination.address.region, // 州/省Code,两个字母的缩写
|
||||||
|
address1: data.details.destination.address.address_line_1, // 详细地址
|
||||||
|
address2: '', // 详细地址2(Address类型中没有address_line_2属性)
|
||||||
|
postCode: data.details.destination.address.postal_code.replace(/\s/g, ''), // 邮编
|
||||||
|
countryName: data.details.destination.address.country, // 国家名称(Address类型中没有country_name属性,使用country代替)
|
||||||
|
cityName: data.details.destination.address.city, // 城市名称
|
||||||
|
stateName: data.details.destination.address.region, // 州/省名称
|
||||||
|
companyName: '' // 公司名称
|
||||||
|
},
|
||||||
|
packages: [
|
||||||
|
{
|
||||||
|
dimensions: {
|
||||||
|
length: data.details.packaging_properties.packages[0].measurements.cuboid.l, // 长
|
||||||
|
width: data.details.packaging_properties.packages[0].measurements.cuboid.w, // 宽
|
||||||
|
height: data.details.packaging_properties.packages[0].measurements.cuboid.h, // 高
|
||||||
|
lengthUnit: (data.details.packaging_properties.packages[0].measurements.cuboid.unit === 'cm' ? 'CM' : 'IN') as 'CM' | 'IN', // 长度单位(IN,CM)
|
||||||
|
weight: data.details.packaging_properties.packages[0].measurements.weight.value, // 重量
|
||||||
|
weightUnit: (data.details.packaging_properties.packages[0].measurements.weight.unit === 'kg' ? 'KG' : 'LB') as 'KG' | 'LB' // 重量单位(LB,KG)
|
||||||
|
},
|
||||||
|
currency: 'CAD', // 币种(默认CAD)
|
||||||
|
description: 'site:' + order.siteId + ' orderId:' + order.externalOrderId // 包裹描述(确保是字符串类型)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
signService: 0
|
||||||
|
// signService: 0, // 签名服务 0不使用, 1使用
|
||||||
|
// declaration: {
|
||||||
|
// "boxNo": "", //箱子编号
|
||||||
|
// "sku": "", //SKU
|
||||||
|
// "cnname": "", //中文名称
|
||||||
|
// "enname": "", //英文名称
|
||||||
|
// "declaredPrice": 1, //申报单价
|
||||||
|
// "declaredQty": 1, //申报数量
|
||||||
|
// "material": "", //材质
|
||||||
|
// "intendedUse": "", //用途
|
||||||
|
// "cweight": 1, //产品单重
|
||||||
|
// "hsCode": "", //海关编码
|
||||||
|
// "battery": "" //电池描述
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用freightwaves费用试算或创建订单API
|
||||||
|
// 注意:根据实际需要调用对应的方法
|
||||||
|
// resShipmentOrder = await this.freightwavesService.rateTry(reqBody); // 费用试算
|
||||||
|
resShipmentOrder = await this.freightwavesService.createOrder(reqBody); // 创建订单
|
||||||
|
}
|
||||||
|
|
||||||
|
return resShipmentOrder;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('物流订单处理失败:', error); // 使用console.log代替this.log
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将ShipmentFeeBookDTO转换为freightwaves的RateTryRequest格式
|
||||||
|
* @param data ShipmentFeeBookDTO数据
|
||||||
|
* @returns RateTryRequest格式的数据
|
||||||
|
*/
|
||||||
|
convertToFreightwavesRateTry(data: ShipmentFeeBookDTO): Omit<RateTryRequest, 'partner'> {
|
||||||
|
// 转换为RateTryRequest格式
|
||||||
|
return {
|
||||||
|
shipCompany: 'UPSYYZ7000NEW', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
partnerOrderNumber: `order-${Date.now()}`, // 必填,使用时间戳生成
|
||||||
|
warehouseId: '25072621030107400060', // 可选,使用stockPointId转换
|
||||||
|
shipper: {
|
||||||
|
name: data.sender, // 必填
|
||||||
|
phone: data.startPhone, // 必填
|
||||||
|
company: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
countryCode: data.shipperCountryCode, // 必填
|
||||||
|
city: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
state: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
address1: data.pickupAddress, // 必填
|
||||||
|
address2: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
postCode: data.startPostalCode, // 必填
|
||||||
|
countryName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
cityName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
stateName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
companyName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
},
|
||||||
|
reciver: {
|
||||||
|
name: data.receiver, // 必填
|
||||||
|
phone: data.receiverPhone, // 必填
|
||||||
|
company: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
countryCode: data.country, // 必填,使用country代替countryCode
|
||||||
|
city: data.city, // 必填
|
||||||
|
state: data.province, // 必填,使用province代替state
|
||||||
|
address1: data.deliveryAddress, // 必填
|
||||||
|
address2: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
postCode: data.postalCode, // 必填
|
||||||
|
countryName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
cityName: data.city, // 必填,使用city代替cityName
|
||||||
|
stateName: data.province, // 必填,使用province代替stateName
|
||||||
|
companyName: '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||||
|
},
|
||||||
|
packages: [
|
||||||
|
{
|
||||||
|
dimensions: {
|
||||||
|
length: data.length, // 必填
|
||||||
|
width: data.width, // 必填
|
||||||
|
height: data.height, // 必填
|
||||||
|
lengthUnit: (data.dimensionUom.toUpperCase() === 'CM' ? 'CM' : 'IN') as 'CM' | 'IN', // 必填,转换为有效的单位
|
||||||
|
weight: data.weight, // 必填
|
||||||
|
weightUnit: (data.weightUom.toUpperCase() === 'KG' ? 'KG' : 'LB') as 'KG' | 'LB', // 必填,转换为有效的单位
|
||||||
|
},
|
||||||
|
currency: 'CAD', // 必填,但ShipmentFeeBookDTO中缺少,使用默认值
|
||||||
|
description: 'Package', // 必填,但ShipmentFeeBookDTO中缺少,使用默认值
|
||||||
|
},
|
||||||
|
],
|
||||||
|
signService: 0, // 可选,默认不使用签名服务
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ShipmentFeeBookDTO缺少的freightwaves必填字段
|
||||||
|
* @returns 缺少的必填字段列表
|
||||||
|
*/
|
||||||
|
getMissingFreightwavesFields(): string[] {
|
||||||
|
return [
|
||||||
|
'shipCompany', // 渠道
|
||||||
|
'partnerOrderNumber', // 第三方客户订单编号
|
||||||
|
'shipper.company', // 发货人公司
|
||||||
|
'shipper.city', // 发货人城市
|
||||||
|
'shipper.state', // 发货人州/省Code
|
||||||
|
'shipper.address2', // 发货人详细地址2
|
||||||
|
'shipper.countryName', // 发货人国家名称
|
||||||
|
'shipper.cityName', // 发货人城市名称
|
||||||
|
'shipper.stateName', // 发货人州/省名称
|
||||||
|
'shipper.companyName', // 发货人公司名称
|
||||||
|
'reciver.company', // 收货人公司
|
||||||
|
'reciver.address2', // 收货人详细地址2
|
||||||
|
'reciver.countryName', // 收货人国家名称
|
||||||
|
'reciver.companyName', // 收货人公司名称
|
||||||
|
'packages[0].currency', // 包裹币种
|
||||||
|
'packages[0].description', // 包裹描述
|
||||||
|
'partner', // 商户ID
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ export class OrderService {
|
||||||
errors: []
|
errors: []
|
||||||
};
|
};
|
||||||
console.log('开始进入循环同步订单', result.length, '个订单')
|
console.log('开始进入循环同步订单', result.length, '个订单')
|
||||||
|
console.log('开始进入循环同步订单', result.length, '个订单')
|
||||||
// 遍历每个订单进行同步
|
// 遍历每个订单进行同步
|
||||||
for (const order of result) {
|
for (const order of result) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -329,16 +330,33 @@ export class OrderService {
|
||||||
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
|
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 这里其实不用过滤不可编辑的行为,而是应在 save 中做判断
|
// 检查数据库中是否已存在该订单
|
||||||
// if(!order.is_editable && !forceUpdate){
|
const existingOrder = await this.orderModel.findOne({
|
||||||
// this.logger.debug('订单不可编辑,跳过处理', siteId, order.id)
|
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||||
// return;
|
});
|
||||||
// }
|
// 自动更新订单状态(如果需要)
|
||||||
// 自动转换远程订单的状态(如果需要)
|
|
||||||
await this.autoUpdateOrderStatus(siteId, order);
|
await this.autoUpdateOrderStatus(siteId, order);
|
||||||
|
|
||||||
|
if(existingOrder){
|
||||||
|
// 矫正数据库中的订单数据
|
||||||
|
const updateData: any = { status: order.status };
|
||||||
|
if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
|
||||||
|
updateData.orderStatus = this.mapOrderStatus(order.status as any);
|
||||||
|
}
|
||||||
|
// 更新订单主数据
|
||||||
|
await this.orderModel.update({ externalOrderId: String(order.id), siteId: siteId }, updateData);
|
||||||
|
// 更新 fulfillments 数据
|
||||||
|
await this.saveOrderFulfillments({
|
||||||
|
siteId,
|
||||||
|
orderId: existingOrder.id,
|
||||||
|
externalOrderId:order.id,
|
||||||
|
fulfillments: fulfillments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const externalOrderId = String(order.id);
|
||||||
// 这里的 saveOrder 已经包括了创建订单和更新订单
|
// 这里的 saveOrder 已经包括了创建订单和更新订单
|
||||||
let orderRecord: Order = await this.saveOrder(siteId, orderData);
|
let orderRecord: Order = await this.saveOrder(siteId, orderData);
|
||||||
// 如果订单从未完成变为完成状态,则更新库存
|
// 如果订单从未完成变为完成状态,则更新库存
|
||||||
if (
|
if (
|
||||||
orderRecord &&
|
orderRecord &&
|
||||||
orderRecord.orderStatus !== ErpOrderStatus.COMPLETED &&
|
orderRecord.orderStatus !== ErpOrderStatus.COMPLETED &&
|
||||||
|
|
@ -347,7 +365,6 @@ export class OrderService {
|
||||||
await this.updateStock(orderRecord);
|
await this.updateStock(orderRecord);
|
||||||
// 不再直接返回,继续执行后续的更新操作
|
// 不再直接返回,继续执行后续的更新操作
|
||||||
}
|
}
|
||||||
const externalOrderId = String(order.id);
|
|
||||||
const orderId = orderRecord.id;
|
const orderId = orderRecord.id;
|
||||||
// 保存订单项
|
// 保存订单项
|
||||||
await this.saveOrderItems({
|
await this.saveOrderItems({
|
||||||
|
|
@ -719,17 +736,16 @@ export class OrderService {
|
||||||
where: { siteSkus: Like(`%${orderItem.sku}%`) },
|
where: { siteSkus: Like(`%${orderItem.sku}%`) },
|
||||||
relations: ['components','attributes','attributes.dict'],
|
relations: ['components','attributes','attributes.dict'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!product) return;
|
if (!product) return;
|
||||||
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
|
||||||
|
|
@ -737,18 +753,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)
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ export class ProductService {
|
||||||
const pageSize = query.per_page || 10;
|
const pageSize = query.per_page || 10;
|
||||||
|
|
||||||
// 处理搜索参数
|
// 处理搜索参数
|
||||||
const name = query.where?.name || query.search || '';
|
const name = query.where?.name || '';
|
||||||
|
|
||||||
// 处理品牌过滤
|
// 处理品牌过滤
|
||||||
const brandId = query.where?.brandId;
|
const brandId = query.where?.brandId;
|
||||||
|
|
@ -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.dict_id = 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') {
|
||||||
|
|
@ -504,6 +468,299 @@ export class ProductService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProductListGrouped(query: UnifiedSearchParamsDTO<ProductWhereFilter>): Promise<Record<string, Product[]>> {
|
||||||
|
// 创建查询构建器
|
||||||
|
const qb = this.productModel
|
||||||
|
.createQueryBuilder('product')
|
||||||
|
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||||
|
.leftJoinAndSelect('attribute.dict', 'dict')
|
||||||
|
.leftJoinAndSelect('product.category', 'category');
|
||||||
|
|
||||||
|
// 验证分组字段
|
||||||
|
const groupBy = query.groupBy;
|
||||||
|
if (!groupBy) {
|
||||||
|
throw new Error('分组字段不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理搜索参数
|
||||||
|
const name = query.where?.name || '';
|
||||||
|
|
||||||
|
// 处理品牌过滤
|
||||||
|
const brandId = query.where?.brandId;
|
||||||
|
const brandIds = query.where?.brandIds;
|
||||||
|
|
||||||
|
// 处理分类过滤
|
||||||
|
const categoryId = query.where?.categoryId;
|
||||||
|
const categoryIds = query.where?.categoryIds;
|
||||||
|
|
||||||
|
// 处理排序参数
|
||||||
|
const orderBy = query.orderBy;
|
||||||
|
|
||||||
|
// 模糊搜索 name,支持多个关键词
|
||||||
|
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||||
|
if (nameFilter.length > 0) {
|
||||||
|
const nameConditions = nameFilter
|
||||||
|
.map((word, index) => `product.name LIKE :name${index}`)
|
||||||
|
.join(' AND ');
|
||||||
|
const nameParams = nameFilter.reduce(
|
||||||
|
(params, word, index) => ({ ...params, [`name${index}`]: `%${word}%` }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
qb.where(`(${nameConditions})`, nameParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理产品ID过滤
|
||||||
|
if (query.where?.id) {
|
||||||
|
qb.andWhere('product.id = :id', { id: query.where.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理产品ID列表过滤
|
||||||
|
if (query.where?.ids && query.where.ids.length > 0) {
|
||||||
|
qb.andWhere('product.id IN (:...ids)', { ids: query.where.ids });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理SKU过滤
|
||||||
|
if (query.where?.sku) {
|
||||||
|
qb.andWhere('product.sku LIKE :sku', { sku: `%${query.where.sku}%` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理SKU列表过滤
|
||||||
|
if (query.where?.skus && query.where.skus.length > 0) {
|
||||||
|
qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理产品中文名称过滤
|
||||||
|
if (query.where?.nameCn) {
|
||||||
|
qb.andWhere('product.nameCn LIKE :nameCn', { nameCn: `%${query.where.nameCn}%` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理产品类型过滤
|
||||||
|
if (query.where?.type) {
|
||||||
|
qb.andWhere('product.type = :type', { type: query.where.type });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理价格范围过滤
|
||||||
|
if (query.where?.minPrice !== undefined) {
|
||||||
|
qb.andWhere('product.price >= :minPrice', { minPrice: query.where.minPrice });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.where?.maxPrice !== undefined) {
|
||||||
|
qb.andWhere('product.price <= :maxPrice', { maxPrice: query.where.maxPrice });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理促销价格范围过滤
|
||||||
|
if (query.where?.minPromotionPrice !== undefined) {
|
||||||
|
qb.andWhere('product.promotionPrice >= :minPromotionPrice', { minPromotionPrice: query.where.minPromotionPrice });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.where?.maxPromotionPrice !== undefined) {
|
||||||
|
qb.andWhere('product.promotionPrice <= :maxPromotionPrice', { maxPromotionPrice: query.where.maxPromotionPrice });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理创建时间范围过滤
|
||||||
|
if (query.where?.createdAtStart) {
|
||||||
|
qb.andWhere('product.createdAt >= :createdAtStart', { createdAtStart: new Date(query.where.createdAtStart) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.where?.createdAtEnd) {
|
||||||
|
qb.andWhere('product.createdAt <= :createdAtEnd', { createdAtEnd: new Date(query.where.createdAtEnd) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理更新时间范围过滤
|
||||||
|
if (query.where?.updatedAtStart) {
|
||||||
|
qb.andWhere('product.updatedAt >= :updatedAtStart', { updatedAtStart: new Date(query.where.updatedAtStart) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.where?.updatedAtEnd) {
|
||||||
|
qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理属性过滤
|
||||||
|
const attributeFilters = query.where?.attributes || {};
|
||||||
|
Object.entries(attributeFilters).forEach(([attributeName, value], index) => {
|
||||||
|
if (value === 'hasValue') {
|
||||||
|
// 如果值为'hasValue',则过滤出具有该属性的产品
|
||||||
|
qb.andWhere(qb => {
|
||||||
|
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.dict_id = 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) {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
.getQuery();
|
||||||
|
return 'product.id IN ' + subQuery;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理品牌ID列表过滤
|
||||||
|
if (brandIds && brandIds.length > 0) {
|
||||||
|
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 IN (:...brandIds)', {
|
||||||
|
brandIds,
|
||||||
|
})
|
||||||
|
.getQuery();
|
||||||
|
return 'product.id IN ' + subQuery;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类过滤(向后兼容)
|
||||||
|
if (categoryId) {
|
||||||
|
qb.andWhere('product.categoryId = :categoryId', { categoryId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理分类ID列表过滤
|
||||||
|
if (categoryIds && categoryIds.length > 0) {
|
||||||
|
qb.andWhere('product.categoryId IN (:...categoryIds)', { categoryIds });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理排序(支持新旧两种格式)
|
||||||
|
if (orderBy) {
|
||||||
|
if (typeof orderBy === 'string') {
|
||||||
|
// 如果orderBy是字符串,尝试解析JSON
|
||||||
|
try {
|
||||||
|
const orderByObj = JSON.parse(orderBy);
|
||||||
|
Object.keys(orderByObj).forEach(key => {
|
||||||
|
const order = orderByObj[key].toUpperCase();
|
||||||
|
const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name'];
|
||||||
|
if (allowedSortFields.includes(key)) {
|
||||||
|
qb.addOrderBy(`product.${key}`, order as 'ASC' | 'DESC');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// 解析失败,使用默认排序
|
||||||
|
qb.orderBy('product.createdAt', 'DESC');
|
||||||
|
}
|
||||||
|
} else if (typeof orderBy === 'object') {
|
||||||
|
// 如果orderBy是对象,直接使用
|
||||||
|
Object.keys(orderBy).forEach(key => {
|
||||||
|
const order = orderBy[key].toUpperCase();
|
||||||
|
const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name'];
|
||||||
|
if (allowedSortFields.includes(key)) {
|
||||||
|
qb.addOrderBy(`product.${key}`, order as 'ASC' | 'DESC');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qb.orderBy('product.createdAt', 'DESC');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按指定字段分组
|
||||||
|
const groupedResult: Record<string, Product[]> = {};
|
||||||
|
|
||||||
|
// 检查是否按属性的字典名称分组
|
||||||
|
const isAttributeGrouping = await this.dictModel.findOne({ where: { name: groupBy } });
|
||||||
|
|
||||||
|
if (isAttributeGrouping) {
|
||||||
|
// 使用原生SQL查询获取每个产品对应的分组属性值
|
||||||
|
const attributeGroupQuery = `
|
||||||
|
SELECT product.id as productId, dict_item.id as attributeId, dict_item.name as attributeName, dict_item.title as attributeTitle
|
||||||
|
FROM product
|
||||||
|
INNER JOIN product_attributes_dict_item ON product.id = product_attributes_dict_item.productId
|
||||||
|
INNER JOIN dict_item ON product_attributes_dict_item.dictItemId = dict_item.id
|
||||||
|
INNER JOIN dict ON dict_item.dict_id = dict.id
|
||||||
|
WHERE dict.name = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
const attributeGroupResults = await this.productModel.query(attributeGroupQuery, [groupBy]);
|
||||||
|
|
||||||
|
// 创建产品ID到分组值的映射
|
||||||
|
const productGroupMap: Record<number, string> = {};
|
||||||
|
attributeGroupResults.forEach((result: any) => {
|
||||||
|
productGroupMap[result.productId] = result.attributeName;
|
||||||
|
});
|
||||||
|
|
||||||
|
items.forEach(product => {
|
||||||
|
// 获取分组值
|
||||||
|
const groupValue = productGroupMap[product.id] || 'unknown';
|
||||||
|
// 转换为字符串作为键
|
||||||
|
const groupKey = String(groupValue);
|
||||||
|
|
||||||
|
// 初始化分组
|
||||||
|
if (!groupedResult[groupKey]) {
|
||||||
|
groupedResult[groupKey] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加产品到分组
|
||||||
|
groupedResult[groupKey].push(product);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 按产品自身字段分组
|
||||||
|
items.forEach(product => {
|
||||||
|
// 获取分组值
|
||||||
|
const groupValue = product[groupBy as keyof Product];
|
||||||
|
// 转换为字符串作为键
|
||||||
|
const groupKey = String(groupValue);
|
||||||
|
|
||||||
|
// 初始化分组
|
||||||
|
if (!groupedResult[groupKey]) {
|
||||||
|
groupedResult[groupKey] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加产品到分组
|
||||||
|
groupedResult[groupKey].push(product);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupedResult;
|
||||||
|
}
|
||||||
|
|
||||||
async getOrCreateAttribute(
|
async getOrCreateAttribute(
|
||||||
dictName: string,
|
dictName: string,
|
||||||
itemTitle: string,
|
itemTitle: string,
|
||||||
|
|
@ -534,9 +791,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 +803,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 +910,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 +967,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 +1677,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 +1699,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 +2036,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 +2044,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 +2397,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -481,7 +481,7 @@ export class ShopyyService {
|
||||||
shipping_method: data.shipping_method
|
shipping_method: data.shipping_method
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await this.request(site, `orders/${orderId}/shipments`, 'POST', fulfillmentData);
|
const response = await this.request(site, `orders/${orderId}/fulfillments`, 'POST', fulfillmentData);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { SiteService } from './site.service';
|
||||||
import { WPService } from './wp.service';
|
import { WPService } from './wp.service';
|
||||||
import { ProductService } from './product.service';
|
import { ProductService } from './product.service';
|
||||||
import { UnifiedProductDTO } from '../dto/site-api.dto';
|
import { UnifiedProductDTO } from '../dto/site-api.dto';
|
||||||
|
import { Product } from '../entity/product.entity';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class SiteApiService {
|
export class SiteApiService {
|
||||||
|
|
@ -52,7 +53,7 @@ export class SiteApiService {
|
||||||
* @param siteProduct 站点商品信息
|
* @param siteProduct 站点商品信息
|
||||||
* @returns 包含ERP产品信息的站点商品
|
* @returns 包含ERP产品信息的站点商品
|
||||||
*/
|
*/
|
||||||
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: any): Promise<any> {
|
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: UnifiedProductDTO): Promise<UnifiedProductDTO & { erpProduct?: Product }> {
|
||||||
if (!siteProduct || !siteProduct.sku) {
|
if (!siteProduct || !siteProduct.sku) {
|
||||||
return siteProduct;
|
return siteProduct;
|
||||||
}
|
}
|
||||||
|
|
@ -64,18 +65,7 @@ export class SiteApiService {
|
||||||
// 将ERP产品信息合并到站点商品中
|
// 将ERP产品信息合并到站点商品中
|
||||||
return {
|
return {
|
||||||
...siteProduct,
|
...siteProduct,
|
||||||
erpProduct: {
|
erpProduct,
|
||||||
id: erpProduct.id,
|
|
||||||
sku: erpProduct.sku,
|
|
||||||
name: erpProduct.name,
|
|
||||||
nameCn: erpProduct.nameCn,
|
|
||||||
category: erpProduct.category,
|
|
||||||
attributes: erpProduct.attributes,
|
|
||||||
components: erpProduct.components,
|
|
||||||
price: erpProduct.price,
|
|
||||||
promotionPrice: erpProduct.promotionPrice,
|
|
||||||
// 可以根据需要添加更多ERP产品字段
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果找不到对应的ERP产品,返回原始站点商品
|
// 如果找不到对应的ERP产品,返回原始站点商品
|
||||||
|
|
@ -90,7 +80,7 @@ export class SiteApiService {
|
||||||
* @param siteProducts 站点商品列表
|
* @param siteProducts 站点商品列表
|
||||||
* @returns 包含ERP产品信息的站点商品列表
|
* @returns 包含ERP产品信息的站点商品列表
|
||||||
*/
|
*/
|
||||||
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: any[]): Promise<any[]> {
|
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: UnifiedProductDTO[]): Promise<(UnifiedProductDTO & { erpProduct?: Product })[]> {
|
||||||
if (!siteProducts || !siteProducts.length) {
|
if (!siteProducts || !siteProducts.length) {
|
||||||
return siteProducts;
|
return siteProducts;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
/**
|
/**
|
||||||
*
|
* wp 接口参考:
|
||||||
* https://developer.wordpress.org/rest-api/reference/media/
|
* https://developer.wordpress.org/rest-api/reference/media/
|
||||||
|
* woocommerce:
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
import { Inject, Provide } from '@midwayjs/core';
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
import axios, { AxiosRequestConfig } from 'axios';
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
|
@ -10,7 +12,7 @@ import { IPlatformService } from '../interface/platform.interface';
|
||||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
import * as FormData from 'form-data';
|
import * as FormData from 'form-data';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { WooProduct, WooVariation } from '../dto/woocommerce.dto';
|
import { WooProduct, WooVariation, WpMediaGetListParams } from '../dto/woocommerce.dto';
|
||||||
const MAX_PAGE_SIZE = 100;
|
const MAX_PAGE_SIZE = 100;
|
||||||
@Provide()
|
@Provide()
|
||||||
export class WPService implements IPlatformService {
|
export class WPService implements IPlatformService {
|
||||||
|
|
@ -1044,20 +1046,7 @@ export class WPService implements IPlatformService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchMediaPaged(site: any, params: Record<string, any> = {}) {
|
public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> = {}) {
|
||||||
const page = Number(params.page ?? 1);
|
|
||||||
const per_page = Number( params.per_page ?? 20);
|
|
||||||
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
|
||||||
let orderby: string | undefined = params.orderby;
|
|
||||||
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
|
|
||||||
if (!orderby && params.order && typeof params.order === 'object') {
|
|
||||||
const entries = Object.entries(params.order as Record<string, any>);
|
|
||||||
if (entries.length > 0) {
|
|
||||||
const [field, dir] = entries[0];
|
|
||||||
orderby = field;
|
|
||||||
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const apiUrl = site.apiUrl;
|
const apiUrl = site.apiUrl;
|
||||||
const { consumerKey, consumerSecret } = site as any;
|
const { consumerKey, consumerSecret } = site as any;
|
||||||
const endpoint = 'wp/v2/media';
|
const endpoint = 'wp/v2/media';
|
||||||
|
|
@ -1066,17 +1055,21 @@ export class WPService implements IPlatformService {
|
||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
headers: { Authorization: `Basic ${auth}` },
|
headers: { Authorization: `Basic ${auth}` },
|
||||||
params: {
|
params: {
|
||||||
...where,
|
...params,
|
||||||
...(params.search ? { search: params.search } : {}),
|
page: params.page ?? 1,
|
||||||
...(orderby ? { orderby } : {}),
|
per_page: params.per_page ?? 20,
|
||||||
...(order ? { order } : {}),
|
|
||||||
page,
|
|
||||||
per_page
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// 检查是否有错误信息
|
||||||
|
if(response?.data?.message){
|
||||||
|
throw new Error(`获取${apiUrl}条媒体文件失败,原因为${response.data.message}`)
|
||||||
|
}
|
||||||
|
if(!Array.isArray(response.data)) {
|
||||||
|
throw new Error(`获取${apiUrl}条媒体文件失败,原因为返回数据不是数组`);
|
||||||
|
}
|
||||||
const total = Number(response.headers['x-wp-total'] || 0);
|
const total = Number(response.headers['x-wp-total'] || 0);
|
||||||
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
|
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
|
||||||
return { items: response.data, total, totalPages, page, per_page, page_size: per_page };
|
return { items: response.data, total, totalPages, page:params.page ?? 1, per_page: params.per_page ?? 20, page_size: params.per_page ?? 20 };
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 上传媒体文件
|
* 上传媒体文件
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const toArray = (value: any): any[] => {
|
||||||
|
if (Array.isArray(value)) return value;
|
||||||
|
if (value === undefined || value === null) return [];
|
||||||
|
return String(value).split(',').map(v => v.trim()).filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toNumber = (value: any): number | undefined => {
|
||||||
|
if (value === undefined || value === null || value === '') return undefined;
|
||||||
|
const n = Number(value);
|
||||||
|
return Number.isFinite(n) ? n : undefined;
|
||||||
|
};
|
||||||
|
|
@ -9,7 +9,7 @@ async function testFreightwavesService() {
|
||||||
|
|
||||||
// Call the test method
|
// Call the test method
|
||||||
console.log('Starting test for createOrder method...');
|
console.log('Starting test for createOrder method...');
|
||||||
const result = await service.testCreateOrder();
|
const result = await service.testQueryOrder();
|
||||||
|
|
||||||
console.log('Test completed successfully!');
|
console.log('Test completed successfully!');
|
||||||
console.log('Result:', result);
|
console.log('Result:', result);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue