forked from yoone/API
1
0
Fork 0

Compare commits

...

16 Commits

Author SHA1 Message Date
zhuotianyuan f7335d9717 fix(shopyy): 修复订单查询参数映射问题并添加时间范围支持
修正shopyy服务中获取所有订单的参数映射逻辑,添加支付时间范围支持
统一处理日期格式转换,确保参数正确传递
同时清理合并冲突标记和冗余代码
2026-01-13 19:25:13 +08:00
zhuotianyuan 6be91f14a4 Merge remote-tracking branch 'origin/main' into main 2026-01-13 14:58:13 +08:00
zhuotianyuan 6dac9ff2f1 feat(freightwaves): 添加TMS系统API集成和测试方法 2026-01-13 14:55:01 +08:00
tikkhun 68574dbc7a refactor(order): 重构订单相关实体和服务逻辑
重构 OrderSale 实体,移除品牌判断标志字段,改为直接存储品牌、口味等属性
修改订单服务和统计服务,使用新的属性字段进行查询和统计
优化产品查询时的关联关系加载
2026-01-12 15:13:37 +08:00
tikkhun eb5cb215a9 fix: 修正shopyy适配器中email字段的拼写错误 2026-01-10 15:50:06 +08:00
tikkhun ca0b5e63a7 refactor(shopyy): 统一账单地址字段名并添加空值检查
将 billing_address 字段重命名为 bill_address 以保持命名一致性
在订单映射方法中添加空值检查防止空对象错误
2026-01-10 15:48:39 +08:00
tikkhun 5d7e0090aa style: 修复代码格式问题,包括空格和空行 2026-01-10 15:17:08 +08:00
tikkhun ecdedcc041 fix: 修复订单服务中产品属性和组件处理的问题
处理产品属性为空的情况,避免空指针异常
为产品组件查询添加关联关系
在订单销售记录创建时增加对空产品的过滤
添加新的品牌判断逻辑
2026-01-10 15:16:29 +08:00
tikkhun b2ee61e47d refactor: 移除未使用的导入和注释掉的生命周期钩子 2026-01-10 15:16:29 +08:00
tikkhun 64c1d1afe5 refactor(订单服务): 移除冗余的订单可编辑性检查注释
注释说明检查应在 save 方法中进行
2026-01-10 15:14:12 +08:00
tikkhun 4eb45af452 feat(订单): 增强订单相关功能及数据模型
- 在订单实体中添加orderItems和orderSales字段
- 优化QueryOrderSalesDTO,增加排序字段和更多描述信息
- 重构saveOrderSale方法,使用产品属性自动设置品牌和强度
- 在订单查询中返回关联的orderItems和orderSales数据
- 添加getAttributesObject方法处理产品属性
2026-01-10 15:14:12 +08:00
tikkhun a8d12a695e fix(product.service): 支持英文分号和逗号分隔siteSkus字段
修改siteSkus字段的分隔符处理逻辑,使其同时支持英文分号和逗号作为分隔符,提高数据兼容性
2026-01-10 15:09:52 +08:00
zhuotianyuan a00a95c9a3 style: 修复 typeorm 配置缩进问题 2026-01-10 07:07:24 +00:00
zhuotianyuan 82c8640f0c fix(config): 将数据库配置更改为本地开发环境
更新数据库连接配置为本地开发环境,包括主机、端口和密码
移除自动同步数据库的配置项
2026-01-10 07:07:24 +00:00
zhuotianyuan cb00076bd3 feat(webhook): 添加对shoppy平台webhook的支持
- 在site.entity.ts中添加webhookUrl字段
- 在auth.middleware.ts中添加/shoppy路由到白名单
- 在webhook.controller.ts中实现shoppy平台webhook处理逻辑

fix(webhook): 更新webhook控制器中的密钥值

refactor(entity): 将可选字段明确标记为可选类型

feat(adapter): 公开映射方法以支持统一接口调用

将各适配器中的私有映射方法改为公开,并在接口中定义统一方法签名
修改webhook控制器以使用适配器映射方法处理订单数据

feat: 添加订单支付日期字段并支持国家筛选

- 在ShopyyOrder接口中添加date_paid字段
- 在OrderStatisticsParams中添加country数组字段用于国家筛选
- 修改统计服务以支持按国家筛选订单数据
- 更新数据库配置和同步设置
- 优化订单服务中的类型定义和查询条件

refactor(webhook): 移除未使用的shoppy webhook处理逻辑

fix(订单服务): 修复订单内容括号处理并添加同步日志

添加订单同步过程的调试日志
修复订单内容中括号内容的处理逻辑
修正控制器方法名拼写错误
2026-01-10 07:07:24 +00:00
tikkhun cdff083940 fix(logistics): 处理获取运单状态失败的情况并优化批量更新
当获取运单状态返回FAIL时抛出错误信息
将运单状态更新改为并行处理并添加日志记录
2026-01-09 11:12:51 +08:00
14 changed files with 902 additions and 344 deletions

View File

@ -22,7 +22,7 @@ import {
CreateVariationDTO, CreateVariationDTO,
UpdateReviewDTO, UpdateReviewDTO,
} from '../dto/site-api.dto'; } from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto';
import { import {
ShopyyAllProductQuery, ShopyyAllProductQuery,
ShopyyCustomer, ShopyyCustomer,
@ -37,6 +37,7 @@ import {
OrderStatus, OrderStatus,
} from '../enums/base.enum'; } from '../enums/base.enum';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import dayjs = require('dayjs');
export class ShopyyAdapter implements ISiteAdapter { export class ShopyyAdapter implements ISiteAdapter {
shopyyFinancialStatusMap= { shopyyFinancialStatusMap= {
'200': '待支付', '200': '待支付',
@ -227,8 +228,10 @@ export class ShopyyAdapter implements ISiteAdapter {
// ========== 订单映射方法 ========== // ========== 订单映射方法 ==========
mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO { mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO {
// console.log(item)
if(!item) throw new Error('订单数据不能为空')
// 提取账单和送货地址 如果不存在则为空对象 // 提取账单和送货地址 如果不存在则为空对象
const billing = (item as any).billing_address || {}; const billing = (item).bill_address || {};
const shipping = (item as any).shipping_address || {}; const shipping = (item as any).shipping_address || {};
// 构建账单地址对象 // 构建账单地址对象
@ -313,7 +316,7 @@ export class ShopyyAdapter implements ISiteAdapter {
product_id: p.product_id, product_id: p.product_id,
quantity: p.quantity, quantity: p.quantity,
total: String(p.price ?? ''), total: String(p.price ?? ''),
sku: p.sku || p.sku_code || '', sku: p.sku_code || '',
price: String(p.price ?? ''), price: String(p.price ?? ''),
}) })
); );
@ -440,7 +443,7 @@ export class ShopyyAdapter implements ISiteAdapter {
} }
// 更新账单地址 // 更新账单地址
params.billing_address = params.billing_address || {}; params.billing_address = params?.billing_address || {};
if (data.billing.first_name !== undefined) { if (data.billing.first_name !== undefined) {
params.billing_address.first_name = data.billing.first_name; params.billing_address.first_name = data.billing.first_name;
} }
@ -557,9 +560,21 @@ export class ShopyyAdapter implements ISiteAdapter {
per_page, per_page,
}; };
} }
mapGetAllOrdersParams(params: UnifiedSearchParamsDTO) :ShopyyGetAllOrdersParams{
const pay_at_min = dayjs(params.after || '').valueOf().toString();
const pay_at_max = dayjs(params.before || '').valueOf().toString();
return {
page: params.page || 1,
per_page: params.per_page || 20,
pay_at_min: pay_at_min,
pay_at_max: pay_at_max,
}
}
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> { async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
const data = await this.shopyyService.getAllOrders(this.site.id, params); const normalizedParams = this.mapGetAllOrdersParams(params);
const data = await this.shopyyService.getAllOrders(this.site.id, normalizedParams);
return data.map(this.mapPlatformToUnifiedOrder.bind(this)); return data.map(this.mapPlatformToUnifiedOrder.bind(this));
} }

View File

@ -52,6 +52,23 @@ export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
orderBy?: Record<string, 'asc' | 'desc'> | string; orderBy?: Record<string, 'asc' | 'desc'> | string;
} }
/**
* Shopyy获取所有订单参数DTO
*/
export class ShopyyGetAllOrdersParams {
@ApiProperty({ description: '页码', example: 1, required: false })
page?: number;
@ApiProperty({ description: '每页数量', example: 20, 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;
}
/** /**
* *
*/ */

View File

@ -98,13 +98,9 @@ export class QueryOrderDTO {
} }
export class QueryOrderSalesDTO { export class QueryOrderSalesDTO {
@ApiProperty() @ApiProperty({ description: '是否为原产品还是库存产品' })
@Rule(RuleType.bool().default(false)) @Rule(RuleType.bool().default(false))
isSource: boolean; isSource: boolean;
@ApiProperty()
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ example: '1', description: '页码' }) @ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number()) @Rule(RuleType.number())
@ -114,19 +110,31 @@ export class QueryOrderSalesDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
pageSize: number; pageSize: number;
@ApiProperty() @ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }',type: 'any', required: false })
@Rule(RuleType.object().allow(null))
orderBy?: Record<string, 'asc' | 'desc'>;
// filter
@ApiProperty({ description: '是否排除套餐' })
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ description: '站点ID' })
@Rule(RuleType.number()) @Rule(RuleType.number())
siteId: number; siteId: number;
@ApiProperty() @ApiProperty({ description: '名称' })
@Rule(RuleType.string()) @Rule(RuleType.string())
name: string; name: string;
@ApiProperty() @ApiProperty({ description: 'SKU' })
@Rule(RuleType.string())
sku: string;
@ApiProperty({ description: '开始日期' })
@Rule(RuleType.date()) @Rule(RuleType.date())
startDate: Date; startDate: Date;
@ApiProperty() @ApiProperty({ description: '结束日期' })
@Rule(RuleType.date()) @Rule(RuleType.date())
endDate: Date; endDate: Date;
} }

View File

@ -200,7 +200,7 @@ export interface ShopyyOrder {
customer_email?: string; customer_email?: string;
email?: string; email?: string;
// 地址字段 // 地址字段
billing_address?: { bill_address?: {
first_name?: string; first_name?: string;
last_name?: string; last_name?: string;
name?: string; name?: string;

View File

@ -272,6 +272,14 @@ export class Order {
@Expose() @Expose()
updatedAt: Date; updatedAt: Date;
@ApiProperty({ type: 'json', nullable: true, description: '订单项列表' })
@Expose()
orderItems?: any[];
@ApiProperty({ type: 'json', nullable: true, description: '销售项列表' })
@Expose()
orderSales?: any[];
// 在插入或更新前处理用户代理字符串 // 在插入或更新前处理用户代理字符串
@BeforeInsert() @BeforeInsert()
@BeforeUpdate() @BeforeUpdate()

View File

@ -1,8 +1,8 @@
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer'; import { Exclude, Expose } from 'class-transformer';
import { import {
BeforeInsert, // BeforeInsert,
BeforeUpdate, // BeforeUpdate,
Column, Column,
CreateDateColumn, CreateDateColumn,
Entity, Entity,
@ -22,22 +22,22 @@ export class OrderSale {
@Expose() @Expose()
id?: number; id?: number;
@ApiProperty() @ApiProperty({ name:'原始订单ID' })
@Column() @Column()
@Expose() @Expose()
orderId: number; // 订单 ID orderId: number; // 订单 ID
@ApiProperty() @ApiProperty({ name:'站点' })
@Column({ nullable: true }) @Column()
@Expose() @Expose()
siteId: number; // 来源站点唯一标识 siteId: number; // 来源站点唯一标识
@ApiProperty() @ApiProperty({name: "原始订单 itemId"})
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
externalOrderItemId: string; // WooCommerce 订单item ID externalOrderItemId: string; // WooCommerce 订单item ID
@ApiProperty() @ApiProperty({name: "产品 ID"})
@Column() @Column()
@Expose() @Expose()
productId: number; productId: number;
@ -62,25 +62,35 @@ export class OrderSale {
@Expose() @Expose()
isPackage: boolean; isPackage: boolean;
@ApiProperty() @ApiProperty({ description: '品牌', type: 'string',nullable: true})
@Column({ default: false })
@Expose() @Expose()
isYoone: boolean; @Column({ nullable: true })
brand?: string;
@ApiProperty() @ApiProperty({ description: '口味', type: 'string', nullable: true })
@Column({ default: false })
@Expose() @Expose()
isZex: boolean; @Column({ nullable: true })
flavor?: string;
@ApiProperty({ nullable: true }) @ApiProperty({ description: '湿度', type: 'string', nullable: true })
@Column({ type: 'int', nullable: true })
@Expose() @Expose()
size: number | null; @Column({ nullable: true })
humidity?: string;
@ApiProperty() @ApiProperty({ description: '尺寸', type: 'string', nullable: true })
@Column({ default: false })
@Expose() @Expose()
isYooneNew: boolean; @Column({ nullable: true })
size?: string;
@ApiProperty({name: '强度', nullable: true })
@Column({ nullable: true })
@Expose()
strength: string | null;
@ApiProperty({ description: '版本', type: 'string', nullable: true })
@Expose()
@Column({ nullable: true })
version?: string;
@ApiProperty({ @ApiProperty({
example: '2022-12-12 11:11:11', example: '2022-12-12 11:11:11',
@ -97,25 +107,4 @@ export class OrderSale {
@UpdateDateColumn() @UpdateDateColumn()
@Expose() @Expose()
updatedAt?: Date; updatedAt?: Date;
// === 自动计算逻辑 ===
@BeforeInsert()
@BeforeUpdate()
setFlags() {
if (!this.name) return;
const lower = this.name.toLowerCase();
this.isYoone = lower.includes('yoone');
this.isZex = lower.includes('zex');
this.isYooneNew = this.isYoone && lower.includes('new');
let size: number | null = null;
const sizes = [3, 6, 9, 12, 15, 18];
for (const s of sizes) {
if (lower.includes(s.toString())) {
size = s;
break;
}
}
this.size = size;
}
} }

View File

@ -75,17 +75,20 @@ export class SyncUniuniShipmentJob implements IJob{
'255': 'Gateway_To_Gateway_Transit' '255': 'Gateway_To_Gateway_Transit'
}; };
async onTick() { async onTick() {
try {
const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false }); const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false });
shipments.forEach(shipment => { const results = await Promise.all(
this.logisticsService.updateShipmentState(shipment); shipments.map(async shipment => {
}); return await this.logisticsService.updateShipmentState(shipment);
} catch (error) { })
this.logger.error(`更新运单状态失败 ${error.message}`); )
} this.logger.info(`更新运单状态完毕 ${JSON.stringify(results)}`);
return results
} }
onComplete(result: any) { onComplete(result: any) {
this.logger.info(`更新运单状态完成 ${result}`);
}
onError(error: any) {
this.logger.error(`更新运单状态失败 ${error.message}`);
} }
} }

View File

@ -0,0 +1,486 @@
import { Inject, Provide } from '@midwayjs/core';
import axios from 'axios';
import * as crypto from 'crypto';
import dayjs = require('dayjs');
import utc = require('dayjs/plugin/utc');
import timezone = require('dayjs/plugin/timezone');
// 扩展dayjs功能
dayjs.extend(utc);
dayjs.extend(timezone);
// 全局参数配置接口
interface FreightwavesConfig {
appSecret: string;
apiBaseUrl: string;
partner: string;
}
// 地址信息接口
interface Address {
name: string;
phone: string;
company: string;
countryCode: string;
city: string;
state: string;
address1: string;
address2: string;
postCode: string;
zoneCode?: string;
countryName: string;
cityName: string;
stateName: string;
companyName: string;
}
// 包裹尺寸接口
interface Dimensions {
length: number;
width: number;
height: number;
lengthUnit: 'IN' | 'CM';
weight: number;
weightUnit: 'LB' | 'KG';
}
// 包裹信息接口
interface Package {
dimensions: Dimensions;
currency: string;
description: string;
}
// 申报信息接口
interface Declaration {
boxNo: string;
sku: string;
cnname: string;
enname: string;
declaredPrice: number;
declaredQty: number;
material: string;
intendedUse: string;
cweight: number;
hsCode: string;
battery: string;
}
// 费用试算请求接口
interface RateTryRequest {
shipCompany: string;
partnerOrderNumber: string;
warehouseId?: string;
shipper: Address;
reciver: Address;
packages: Package[];
partner: string;
signService?: 0 | 1;
}
// 创建订单请求接口
interface CreateOrderRequest extends RateTryRequest {
declaration: Declaration;
}
// 查询订单请求接口
interface QueryOrderRequest {
partnerOrderNumber?: string;
shipOrderId?: string;
partner: string;
}
// 修改订单请求接口
interface ModifyOrderRequest extends CreateOrderRequest {
shipOrderId: string;
}
// 订单退款请求接口
interface RefundOrderRequest {
shipOrderId: string;
partner: string;
}
// 通用响应接口
interface FreightwavesResponse<T> {
code: string;
msg: string;
data: T;
}
// 费用试算响应数据接口
interface RateTryResponseData {
shipCompany: string;
channelCode: string;
totalAmount: number;
currency: string;
}
// 创建订单响应数据接口
interface CreateOrderResponseData {
partnerOrderNumber: string;
shipOrderId: string;
}
// 查询订单响应数据接口
interface QueryOrderResponseData {
thirdOrderId: string;
shipCompany: string;
expressFinish: 0 | 1 | 2;
expressFailMsg: string;
expressOrder: {
mainTrackingNumber: string;
labelPath: string[];
totalAmount: number;
currency: string;
balance: number;
};
partnerOrderNumber: string;
shipOrderId: string;
}
// 修改订单响应数据接口
interface ModifyOrderResponseData extends CreateOrderResponseData {}
// 订单退款响应数据接口
interface RefundOrderResponseData {}
@Provide()
export class FreightwavesService {
@Inject() logger;
// 默认配置
private config: FreightwavesConfig = {
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
apiBaseUrl: 'https://tms.freightwaves.ca',
partner: '25072621035200000060',
};
// 初始化配置
setConfig(config: Partial<FreightwavesConfig>): void {
this.config = { ...this.config, ...config };
}
// 生成签名
private generateSignature(body: any, date: string): string {
const bodyString = JSON.stringify(body);
const signatureStr = `${bodyString}${this.config.appSecret}${date}`;
return crypto.createHash('md5').update(signatureStr).digest('hex');
}
// 发送请求
private async sendRequest<T>(url: string, data: any): Promise<FreightwavesResponse<T>> {
try {
// 设置请求头 - 使用太平洋时间 (America/Los_Angeles)
const date = dayjs().tz('America/Los_Angeles').format('YYYY-mm-dd HH:mm:ss');
const headers = {
'Content-Type': 'application/json',
'requestDate': date,
'signature': this.generateSignature(data, date),
};
// 记录请求前的详细信息
this.log(`Sending request to: ${this.config.apiBaseUrl}${url}`, {
headers,
data
});
// 发送请求 - 临时禁用SSL证书验证以解决UNABLE_TO_VERIFY_LEAF_SIGNATURE错误
const response = await axios.post<FreightwavesResponse<T>>(
`${this.config.apiBaseUrl}${url}`,
data,
{
headers,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
}
);
// 记录响应信息
this.log(`Received response from: ${this.config.apiBaseUrl}${url}`, {
status: response.status,
statusText: response.statusText,
data: response.data
});
// 处理响应
if (response.data.code !== '00000200') {
this.log(`Freightwaves API error: ${response.data.msg}`, { url, data, response: response.data });
throw new Error(response.data.msg);
}
return response.data;
} catch (error) {
// 更详细的错误记录
if (error.response) {
// 请求已发送,服务器返回错误状态码
this.log(`Freightwaves API request failed with status: ${error.response.status}`, {
url,
data,
response: error.response.data,
status: error.response.status,
headers: error.response.headers
});
} else if (error.request) {
// 请求已发送,但没有收到响应
this.log(`Freightwaves API request no response received`, {
url,
data,
request: error.request
});
} else {
// 请求配置时发生错误
this.log(`Freightwaves API request configuration error: ${error.message}`, {
url,
data,
error: error.message
});
}
throw error;
}
}
/**
*
* @param params
* @returns
*/
async rateTry(params: Omit<RateTryRequest, 'partner'>): Promise<RateTryResponseData> {
const requestData: RateTryRequest = {
...params,
partner: this.config.partner,
};
const response = await this.sendRequest<RateTryResponseData>('/shipService/order/rateTry', requestData);
return response.data;
}
/**
*
* @param params
* @returns
*/
async createOrder(params: Omit<CreateOrderRequest, 'partner'>): Promise<CreateOrderResponseData> {
const requestData: CreateOrderRequest = {
...params,
partner: this.config.partner,
};
const response = await this.sendRequest<CreateOrderResponseData>('/shipService/order/createOrder?apipost_id=0422aa', requestData);
return response.data;
}
/**
*
* @param params
* @returns
*/
async queryOrder(params: Omit<QueryOrderRequest, 'partner'>): Promise<QueryOrderResponseData> {
const requestData: QueryOrderRequest = {
...params,
partner: this.config.partner,
};
const response = await this.sendRequest<QueryOrderResponseData>('/shipService/order/queryOrder', requestData);
return response.data;
}
/**
*
* @param params
* @returns
*/
async modifyOrder(params: Omit<ModifyOrderRequest, 'partner'>): Promise<ModifyOrderResponseData> {
const requestData: ModifyOrderRequest = {
...params,
partner: this.config.partner,
};
const response = await this.sendRequest<ModifyOrderResponseData>('/shipService/order/modifyOrder', requestData);
return response.data;
}
/**
* 退
* @param params 退
* @returns 退
*/
async refundOrder(params: Omit<RefundOrderRequest, 'partner'>): Promise<RefundOrderResponseData> {
const requestData: RefundOrderRequest = {
...params,
partner: this.config.partner,
};
const response = await this.sendRequest<RefundOrderResponseData>('/shipService/order/refundOrder', requestData);
return response.data;
}
/**
*
* 使createOrder方法
*/
async testCreateOrder() {
try {
// 设置必要的配置
this.setConfig({
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
apiBaseUrl: 'https://tms.freightwaves.ca',
partner: '25072621035200000060'
});
// 准备测试数据
const testParams: Omit<CreateOrderRequest, 'partner'> = {
shipCompany: 'DHL',
partnerOrderNumber: `test-order-${Date.now()}`,
warehouseId: '25072621035200000060',
shipper: {
name: 'John Doe',
phone: '123-456-7890',
company: 'Test Company',
countryCode: 'CA',
city: 'Toronto',
state: 'ON',
address1: '123 Main St',
address2: 'Suite 400',
postCode: 'M5V 2T6',
countryName: 'Canada',
cityName: 'Toronto',
stateName: 'Ontario',
companyName: 'Test Company Inc.'
},
reciver: {
name: 'Jane Smith',
phone: '987-654-3210',
company: 'Receiver Company',
countryCode: 'CA',
city: 'Vancouver',
state: 'BC',
address1: '456 Oak St',
address2: '',
postCode: 'V6J 2A9',
countryName: 'Canada',
cityName: 'Vancouver',
stateName: 'British Columbia',
companyName: 'Receiver Company Ltd.'
},
packages: [
{
dimensions: {
length: 10,
width: 8,
height: 6,
lengthUnit: 'IN',
weight: 5,
weightUnit: 'LB'
},
currency: 'CAD',
description: 'Test Package'
}
],
declaration: {
boxNo: 'BOX-001',
sku: 'TEST-SKU-001',
cnname: '测试产品',
enname: 'Test Product',
declaredPrice: 100,
declaredQty: 1,
material: 'Plastic',
intendedUse: 'General use',
cweight: 5,
hsCode: '39269090',
battery: 'No'
},
signService: 0
};
// 调用创建订单方法
this.log('开始测试创建订单...');
this.log('测试参数:', testParams);
// 注意:在实际环境中取消注释以下行来执行真实请求
const result = await this.createOrder(testParams);
this.log('创建订单成功:', result);
// 返回模拟结果
return {
partnerOrderNumber: testParams.partnerOrderNumber,
shipOrderId: `simulated-shipOrderId-${Date.now()}`
};
} catch (error) {
this.log('测试创建订单失败:', error);
throw error;
}
}
/**
*
* @returns
*/
async testQueryOrder() {
try {
// 设置必要的配置
this.setConfig({
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
apiBaseUrl: 'https://console-mock.apipost.cn/mock/0',
partner: '25072621035200000060'
});
// 准备测试数据 - 可以通过partnerOrderNumber或shipOrderId查询
const testParams: Omit<QueryOrderRequest, 'partner'> = {
// 选择其中一个参数进行测试
partnerOrderNumber: 'test-order-123456789', // 示例订单号
// shipOrderId: 'simulated-shipOrderId-123456789' // 或者使用运单号
};
// 调用查询订单方法
this.log('开始测试查询订单...');
this.log('测试参数:', testParams);
// 注意:在实际环境中取消注释以下行来执行真实请求
const result = await this.queryOrder(testParams);
this.log('查询订单成功:', result);
this.log('测试完成:查询订单方法调用成功(模拟)');
// 返回模拟结果
return {
thirdOrderId: 'thirdOrder-123456789',
shipCompany: 'DHL',
expressFinish: 0,
expressFailMsg: '',
expressOrder: {
mainTrackingNumber: '1234567890',
labelPath: ['https://example.com/label.pdf'],
totalAmount: 100,
currency: 'CAD',
balance: 50
},
partnerOrderNumber: testParams.partnerOrderNumber,
shipOrderId: 'simulated-shipOrderId-123456789'
};
} catch (error) {
this.log('测试查询订单失败:', error);
throw error;
}
}
/**
* logger可能未定义的情况
* @param message
* @param data
*/
private log(message: string, data?: any) {
if (this.logger) {
this.logger.info(message, data);
} else {
// 如果logger未定义使用console输出
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
}
}

View File

@ -125,6 +125,10 @@ export class LogisticsService {
try { try {
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
if(data.status === 'FAIL'){
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,写常数
shipment.finished = true; shipment.finished = true;

View File

@ -39,6 +39,7 @@ import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import { UnifiedOrderDTO } from '../dto/site-api.dto'; import { UnifiedOrderDTO } from '../dto/site-api.dto';
import { CustomerService } from './customer.service'; import { CustomerService } from './customer.service';
import { ProductService } from './product.service';
@Provide() @Provide()
export class OrderService { export class OrderService {
@ -110,7 +111,9 @@ export class OrderService {
@Logger() @Logger()
logger; // 注入 Logger 实例 logger; // 注入 Logger 实例
@Inject()
productService: ProductService;
/** /**
* *
* : * :
@ -146,8 +149,8 @@ export class OrderService {
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId }, where: { externalOrderId: String(order.id), siteId: siteId },
}); });
if(!existingOrder){ if (!existingOrder) {
console.log("数据库中不存在",order.id, '订单状态:', order.status ) console.log("数据库中不存在", order.id, '订单状态:', order.status)
} }
// 同步单个订单 // 同步单个订单
await this.syncSingleOrder(siteId, order); await this.syncSingleOrder(siteId, order);
@ -211,8 +214,8 @@ export class OrderService {
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId }, where: { externalOrderId: String(order.id), siteId: siteId },
}); });
if(!existingOrder){ if (!existingOrder) {
console.log("数据库不存在", siteId , "订单:",order.id, '订单状态:' + order.status ) console.log("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status)
} }
// 同步单个订单 // 同步单个订单
await this.syncSingleOrder(siteId, order, true); await this.syncSingleOrder(siteId, order, true);
@ -271,7 +274,7 @@ export class OrderService {
try { try {
const site = await this.siteService.get(siteId); const site = await this.siteService.get(siteId);
// 仅处理 WooCommerce 站点 // 仅处理 WooCommerce 站点
if(site.type !== 'woocommerce'){ if (site.type !== 'woocommerce') {
return return
} }
// 将订单状态同步到 WooCommerce,然后切换至下一状态 // 将订单状态同步到 WooCommerce,然后切换至下一状态
@ -281,6 +284,11 @@ export class OrderService {
console.error('更新订单状态失败,原因为:', error) console.error('更新订单状态失败,原因为:', error)
} }
} }
async getOrderByExternalOrderId(siteId: number, externalOrderId: string) {
return await this.orderModel.findOne({
where: { externalOrderId: String(externalOrderId), siteId },
});
}
/** /**
* *
* : * :
@ -318,47 +326,28 @@ export class OrderService {
// console.log('同步进单个订单', order) // console.log('同步进单个订单', order)
// 如果订单状态为 AUTO_DRAFT,则跳过处理 // 如果订单状态为 AUTO_DRAFT,则跳过处理
if (order.status === OrderStatus.AUTO_DRAFT) { if (order.status === OrderStatus.AUTO_DRAFT) {
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
return; return;
} }
// 检查数据库中是否已存在该订单 // 这里其实不用过滤不可编辑的行为,而是应在 save 中做判断
const existingOrder = await this.orderModel.findOne({ // if(!order.is_editable && !forceUpdate){
where: { externalOrderId: String(order.id), siteId: siteId }, // this.logger.debug('订单不可编辑,跳过处理', siteId, order.id)
}); // return;
// 自动更新订单状态(如果需要) // }
// 自动转换远程订单的状态(如果需要)
await this.autoUpdateOrderStatus(siteId, order); await this.autoUpdateOrderStatus(siteId, order);
// 这里的 saveOrder 已经包括了创建订单和更新订单
if(existingOrder){ let orderRecord: Order = await this.saveOrder(siteId, orderData);
// 矫正数据库中的订单数据 // 如果订单从未完成变为完成状态,则更新库存
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);
// 如果订单从未完成变为完成状态,则更新库存
if ( if (
existingOrder && orderRecord &&
existingOrder.orderStatus !== ErpOrderStatus.COMPLETED && orderRecord.orderStatus !== ErpOrderStatus.COMPLETED &&
orderData.status === OrderStatus.COMPLETED orderData.status === OrderStatus.COMPLETED
) { ) {
this.updateStock(existingOrder); await this.updateStock(orderRecord);
// 不再直接返回,继续执行后续的更新操作 // 不再直接返回,继续执行后续的更新操作
} }
// 如果订单不可编辑且不强制更新,则跳过处理 const externalOrderId = String(order.id);
if (existingOrder && !existingOrder.is_editable && !forceUpdate) {
return;
}
// 保存订单主数据
const orderRecord = await this.saveOrder(siteId, orderData);
const orderId = orderRecord.id; const orderId = orderRecord.id;
// 保存订单项 // 保存订单项
await this.saveOrderItems({ await this.saveOrderItems({
@ -462,13 +451,14 @@ export class OrderService {
* @param order * @param order
* @returns * @returns
*/ */
async saveOrder(siteId: number, order: Partial<UnifiedOrderDTO>): Promise<Order> { // 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等
async saveOrder(siteId: number, order: Omit<UnifiedOrderDTO, 'line_items' | 'refunds'>): Promise<Order> {
// 将外部订单ID转换为字符串 // 将外部订单ID转换为字符串
const externalOrderId = String(order.id) const externalOrderId = String(order.id)
delete order.id delete order.id
// 创建订单实体对象 // 创建订单实体对象
const entity = plainToClass(Order, {...order, externalOrderId, siteId}); const entity = plainToClass(Order, { ...order, externalOrderId, siteId });
// 检查数据库中是否已存在该订单 // 检查数据库中是否已存在该订单
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId, siteId: siteId }, where: { externalOrderId, siteId: siteId },
@ -711,6 +701,8 @@ export class OrderService {
* *
* @param orderItem * @param orderItem
*/ */
// TODO 这里存的是库存商品实际
// 所以叫做 orderInventoryItems 可能更合适
async saveOrderSale(orderItem: OrderItem) { async saveOrderSale(orderItem: OrderItem) {
const currentOrderSale = await this.orderSaleModel.find({ const currentOrderSale = await this.orderSaleModel.find({
where: { where: {
@ -725,50 +717,53 @@ export class OrderService {
// 从数据库查询产品,关联查询组件 // 从数据库查询产品,关联查询组件
const product = await this.productModel.findOne({ const product = await this.productModel.findOne({
where: { siteSkus: Like(`%${orderItem.sku}%`) }, where: { siteSkus: Like(`%${orderItem.sku}%`) },
relations: ['components'], 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 orderSales: OrderSale[] = []; return {
product: await this.productModel.findOne({
if (product.components && product.components.length > 0) {
for (const comp of product.components) {
const baseProduct = await this.productModel.findOne({
where: { sku: comp.sku }, where: { sku: comp.sku },
}); relations: ['components', 'attributes','attributes.dict'],
if (baseProduct) { }),
const orderSaleItem: OrderSale = plainToClass(OrderSale, { quantity: comp.quantity * orderItem.quantity,
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
productId: baseProduct.id,
name: baseProduct.name,
quantity: comp.quantity * orderItem.quantity,
sku: comp.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
});
orderSales.push(orderSaleItem);
}
} }
} else { })) : [{ product, quantity: orderItem.quantity }]
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
const orderSales: OrderSale[] = componentDetails.map(componentDetail => {
if (!componentDetail.product) return null
const attrsObj = this.productService.getAttributesObject(product.attributes)
const orderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId, orderId: orderItem.orderId,
siteId: orderItem.siteId, siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId, externalOrderItemId: orderItem.externalOrderItemId,
productId: product.id, productId: componentDetail.product.id,
name: product.name, name: componentDetail.product.name,
quantity: orderItem.quantity, quantity: componentDetail.quantity * orderItem.quantity,
sku: product.sku, sku: componentDetail.product.sku,
isPackage: orderItem.name.toLowerCase().includes('package'), // 理论上直接存 product 的全部数据才是对的,因为这样我的数据才全面。
isPackage: componentDetail.product.type === 'bundle',
isYoone: attrsObj?.['brand']?.name === 'yoone',
isZyn: attrsObj?.['brand']?.name === 'zyn',
isZex: attrsObj?.['brand']?.name === 'zex',
isYooneNew: attrsObj?.['brand']?.name === 'yoone' && attrsObj?.['version']?.name === 'new',
strength: attrsObj?.['strength']?.name,
}); });
orderSales.push(orderSaleItem); return orderSale
} }).filter(v => v !== null)
console.log("orderSales",orderSales)
if (orderSales.length > 0) { if (orderSales.length > 0) {
await this.orderSaleModel.save(orderSales); await this.orderSaleModel.save(orderSales);
} }
} }
// // extract stren
// extractNumberFromString(str: string): number {
// if (!str) return 0;
// const num = parseInt(str, 10);
// return isNaN(num) ? 0 : num;
// }
/** /**
* 退 * 退
@ -1429,7 +1424,7 @@ export class OrderService {
* @param params * @param params
* @returns * @returns
*/ */
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) { async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage, orderBy }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'); const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
@ -1582,14 +1577,14 @@ export class OrderService {
`; `;
let yooneSql = ` let yooneSql = `
SELECT SELECT
SUM(CASE WHEN os.isYoone = 1 AND os.size = 3 THEN os.quantity ELSE 0 END) AS yoone3Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '3mg' THEN os.quantity ELSE 0 END) AS yoone3Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 6 THEN os.quantity ELSE 0 END) AS yoone6Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '6mg' THEN os.quantity ELSE 0 END) AS yoone6Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 9 THEN os.quantity ELSE 0 END) AS yoone9Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '9mg' THEN os.quantity ELSE 0 END) AS yoone9Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12Quantity,
SUM(CASE WHEN os.isYooneNew = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12QuantityNew, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 15 THEN os.quantity ELSE 0 END) AS yoone15Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '15mg' THEN os.quantity ELSE 0 END) AS yoone15Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 18 THEN os.quantity ELSE 0 END) AS yoone18Quantity, SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '18mg' THEN os.quantity ELSE 0 END) AS yoone18Quantity,
SUM(CASE WHEN os.isZex = 1 THEN os.quantity ELSE 0 END) AS zexQuantity SUM(CASE WHEN os.brand = 'zex' THEN os.quantity ELSE 0 END) AS zexQuantity
FROM order_sale os FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ? WHERE o.date_paid BETWEEN ? AND ?
@ -1645,11 +1640,12 @@ export class OrderService {
* @returns * @returns
*/ */
async getOrderItems({ async getOrderItems({
current,
pageSize,
siteId, siteId,
startDate, startDate,
endDate, endDate,
current, sku,
pageSize,
name, name,
}: QueryOrderSalesDTO) { }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
@ -1907,8 +1903,8 @@ export class OrderService {
const key = it?.externalSubscriptionId const key = it?.externalSubscriptionId
? `sub:${it.externalSubscriptionId}` ? `sub:${it.externalSubscriptionId}`
: it?.externalOrderId : it?.externalOrderId
? `ord:${it.externalOrderId}` ? `ord:${it.externalOrderId}`
: `id:${it?.id}`; : `id:${it?.id}`;
if (!seen.has(key)) { if (!seen.has(key)) {
seen.add(key); seen.add(key);
relatedList.push(it); relatedList.push(it);
@ -2202,14 +2198,14 @@ export class OrderService {
for (const sale of sales) { for (const sale of sales) {
const product = await productRepo.findOne({ where: { sku: sale.sku } }); const product = await productRepo.findOne({ where: { sku: sale.sku } });
const saleItem = { const saleItem = {
orderId: order.id, orderId: order.id,
siteId: order.siteId, siteId: order.siteId,
externalOrderItemId: '-1', externalOrderItemId: '-1',
productId: product.id, productId: product.id,
name: product.name, name: product.name,
sku: sale.sku, sku: sale.sku,
quantity: sale.quantity, quantity: sale.quantity,
}; };
await orderSaleRepo.save(saleItem); await orderSaleRepo.save(saleItem);
} }
}); });
@ -2342,83 +2338,83 @@ export class OrderService {
//换货功能更新OrderSale和Orderitem数据 //换货功能更新OrderSale和Orderitem数据
async updateExchangeOrder(orderId: number, data: any) { async updateExchangeOrder(orderId: number, data: any) {
throw new Error('暂未实现') throw new Error('暂未实现')
// try { // try {
// const dataSource = this.dataSourceManager.getDataSource('default'); // const dataSource = this.dataSourceManager.getDataSource('default');
// let transactionError = undefined; // let transactionError = undefined;
// await dataSource.transaction(async manager => { // await dataSource.transaction(async manager => {
// const orderRepo = manager.getRepository(Order); // const orderRepo = manager.getRepository(Order);
// const orderSaleRepo = manager.getRepository(OrderSale); // const orderSaleRepo = manager.getRepository(OrderSale);
// const orderItemRepo = manager.getRepository(OrderItem); // const orderItemRepo = manager.getRepository(OrderItem);
// const productRepo = manager.getRepository(ProductV2); // const productRepo = manager.getRepository(ProductV2);
// const order = await orderRepo.findOneBy({ id: orderId }); // const order = await orderRepo.findOneBy({ id: orderId });
// let product: ProductV2; // let product: ProductV2;
// await orderSaleRepo.delete({ orderId }); // await orderSaleRepo.delete({ orderId });
// await orderItemRepo.delete({ orderId }); // await orderItemRepo.delete({ orderId });
// for (const sale of data['sales']) { // for (const sale of data['sales']) {
// product = await productRepo.findOneBy({ sku: sale['sku'] }); // product = await productRepo.findOneBy({ sku: sale['sku'] });
// await orderSaleRepo.save({ // await orderSaleRepo.save({
// orderId, // orderId,
// siteId: order.siteId, // siteId: order.siteId,
// productId: product.id, // productId: product.id,
// name: product.name, // name: product.name,
// sku: sale['sku'], // sku: sale['sku'],
// quantity: sale['quantity'], // quantity: sale['quantity'],
// }); // });
// }; // };
// for (const item of data['items']) { // for (const item of data['items']) {
// product = await productRepo.findOneBy({ sku: item['sku'] }); // product = await productRepo.findOneBy({ sku: item['sku'] });
// await orderItemRepo.save({ // await orderItemRepo.save({
// orderId, // orderId,
// siteId: order.siteId, // siteId: order.siteId,
// productId: product.id, // productId: product.id,
// name: product.name, // name: product.name,
// externalOrderId: order.externalOrderId, // externalOrderId: order.externalOrderId,
// externalProductId: product.externalProductId, // externalProductId: product.externalProductId,
// sku: item['sku'], // sku: item['sku'],
// quantity: item['quantity'], // quantity: item['quantity'],
// }); // });
// }; // };
// //将是否换货状态改为true // //将是否换货状态改为true
// await orderRepo.update( // await orderRepo.update(
// order.id // order.id
// , { // , {
// is_exchange: true // is_exchange: true
// }); // });
// //查询这个用户换过多少次货 // //查询这个用户换过多少次货
// const counts = await orderRepo.countBy({ // const counts = await orderRepo.countBy({
// is_editable: true, // is_editable: true,
// customer_email: order.customer_email, // customer_email: order.customer_email,
// }); // });
// //批量更新当前用户换货次数 // //批量更新当前用户换货次数
// await orderRepo.update({ // await orderRepo.update({
// customer_email: order.customer_email // customer_email: order.customer_email
// }, { // }, {
// exchange_frequency: counts // exchange_frequency: counts
// }); // });
// }).catch(error => { // }).catch(error => {
// transactionError = error; // transactionError = error;
// }); // });
// if (transactionError !== undefined) { // if (transactionError !== undefined) {
// throw new Error(`更新物流信息错误:${transactionError.message}`); // throw new Error(`更新物流信息错误:${transactionError.message}`);
// } // }
// return true; // return true;
// } catch (error) { // } catch (error) {
// throw new Error(`更新发货产品失败:${error.message}`); // throw new Error(`更新发货产品失败:${error.message}`);
// } // }
} }
/** /**
@ -2464,17 +2460,17 @@ export class OrderService {
} }
try { try {
// 过滤掉NaN和非数字值只保留有效的数字ID // 过滤掉NaN和非数字值只保留有效的数字ID
const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0); const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0);
const dataSource = this.dataSourceManager.getDataSource('default'); const dataSource = this.dataSourceManager.getDataSource('default');
// 优化事务使用 // 优化事务使用
return await dataSource.transaction(async manager => { return await dataSource.transaction(async manager => {
// 准备查询条件 // 准备查询条件
const whereCondition: any = {}; const whereCondition: any = {};
if(validIds.length > 0){ if (validIds.length > 0) {
whereCondition.id = In(validIds); whereCondition.id = In(validIds);
} }
@ -2490,7 +2486,7 @@ export class OrderService {
// 获取所有订单ID // 获取所有订单ID
const orderIds = orders.map(order => order.id); const orderIds = orders.map(order => order.id);
// 获取所有订单项 // 获取所有订单项
const orderItems = await manager.getRepository(OrderItem).find({ const orderItems = await manager.getRepository(OrderItem).find({
where: { where: {
@ -2511,13 +2507,13 @@ export class OrderService {
const exportDataList: ExportData[] = orders.map(order => { const exportDataList: ExportData[] = orders.map(order => {
// 获取订单的订单项 // 获取订单的订单项
const items = orderItemsByOrderId[order.id] || []; const items = orderItemsByOrderId[order.id] || [];
// 计算总盒数 // 计算总盒数
const boxCount = items.reduce((total, item) => total + item.quantity, 0); const boxCount = items.reduce((total, item) => total + item.quantity, 0);
// 构建订单内容 // 构建订单内容
const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; '); const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; ');
// 构建姓名地址 // 构建姓名地址
const shipping = order.shipping; const shipping = order.shipping;
const billing = order.billing; const billing = order.billing;
@ -2531,10 +2527,10 @@ export class OrderService {
const postcode = shipping?.postcode || billing?.postcode || ''; const postcode = shipping?.postcode || billing?.postcode || '';
const country = shipping?.country || billing?.country || ''; const country = shipping?.country || billing?.country || '';
const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`; const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`;
// 获取电话号码 // 获取电话号码
const phone = shipping?.phone || billing?.phone || ''; const phone = shipping?.phone || billing?.phone || '';
// 获取快递号 // 获取快递号
const trackingNumber = order.shipment?.tracking_id || ''; const trackingNumber = order.shipment?.tracking_id || '';
@ -2570,84 +2566,84 @@ export class OrderService {
* CSV格式 * CSV格式
* @param {any[]} data * @param {any[]} data
* @param {Object} options * @param {Object} options
* @param {string} [options.type='string'] :'string' | 'buffer' * @param {string} [options.type='string'] :'string' | 'buffer'
* @param {string} [options.fileName] (使) * @param {string} [options.fileName] (使)
* @param {boolean} [options.writeFile=false] * @param {boolean} [options.writeFile=false]
* @returns {string|Buffer} type返回字符串或Buffer * @returns {string|Buffer} type返回字符串或Buffer
*/ */
async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> { async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
try { try {
// 检查数据是否为空 // 检查数据是否为空
if (!data || data.length === 0) { if (!data || data.length === 0) {
throw new Error('导出数据不能为空'); throw new Error('导出数据不能为空');
}
const { type = 'string', fileName, writeFile = false } = options;
// 生成表头
const headers = Object.keys(data[0]);
let csvContent = headers.join(',') + '\n';
// 处理数据行
data.forEach(item => {
const row = headers.map(key => {
const value = item[key as keyof any];
// 处理特殊字符
if (typeof value === 'string') {
// 转义双引号,将"替换为""
const escapedValue = value.replace(/"/g, '""');
// 如果包含逗号或换行符,需要用双引号包裹
if (escapedValue.includes(',') || escapedValue.includes('\n')) {
return `"${escapedValue}"`;
}
return escapedValue;
}
// 处理日期类型
if (value instanceof Date) {
return value.toISOString();
}
// 处理undefined和null
if (value === undefined || value === null) {
return '';
}
return String(value);
}).join(',');
csvContent += row + '\n';
});
// 如果需要写入文件
if (writeFile && fileName) {
// 获取当前用户目录
const userHomeDir = os.homedir();
// 构建目标路径(下载目录)
const downloadsDir = path.join(userHomeDir, 'Downloads');
// 确保下载目录存在
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
} }
const filePath = path.join(downloadsDir, fileName); const { type = 'string', fileName, writeFile = false } = options;
// 写入文件 // 生成表头
fs.writeFileSync(filePath, csvContent, 'utf8'); const headers = Object.keys(data[0]);
let csvContent = headers.join(',') + '\n';
console.log(`数据已成功导出至 ${filePath}`);
return filePath; // 处理数据行
data.forEach(item => {
const row = headers.map(key => {
const value = item[key as keyof any];
// 处理特殊字符
if (typeof value === 'string') {
// 转义双引号,将"替换为""
const escapedValue = value.replace(/"/g, '""');
// 如果包含逗号或换行符,需要用双引号包裹
if (escapedValue.includes(',') || escapedValue.includes('\n')) {
return `"${escapedValue}"`;
}
return escapedValue;
}
// 处理日期类型
if (value instanceof Date) {
return value.toISOString();
}
// 处理undefined和null
if (value === undefined || value === null) {
return '';
}
return String(value);
}).join(',');
csvContent += row + '\n';
});
// 如果需要写入文件
if (writeFile && fileName) {
// 获取当前用户目录
const userHomeDir = os.homedir();
// 构建目标路径(下载目录)
const downloadsDir = path.join(userHomeDir, 'Downloads');
// 确保下载目录存在
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
}
const filePath = path.join(downloadsDir, fileName);
// 写入文件
fs.writeFileSync(filePath, csvContent, 'utf8');
console.log(`数据已成功导出至 ${filePath}`);
return filePath;
}
// 根据类型返回不同结果
if (type === 'buffer') {
return Buffer.from(csvContent, 'utf8');
}
return csvContent;
} catch (error) {
console.error('导出CSV时出错:', error);
throw new Error(`导出CSV文件失败: ${error.message}`);
} }
// 根据类型返回不同结果
if (type === 'buffer') {
return Buffer.from(csvContent, 'utf8');
}
return csvContent;
} catch (error) {
console.error('导出CSV时出错:', error);
throw new Error(`导出CSV文件失败: ${error.message}`);
} }
}
/** /**
* *
@ -2724,9 +2720,4 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?:
return result; return result;
} }
} }

View File

@ -1461,12 +1461,17 @@ export class ProductService {
return { return {
sku, sku,
name: val(rec.name), name: val(rec.name),
nameCn: val(rec.nameCn), nameCn: val(rec.nameCn),
description: val(rec.description), description: val(rec.description),
price: num(rec.price), price: num(rec.price),
promotionPrice: num(rec.promotionPrice), promotionPrice: num(rec.promotionPrice),
type: val(rec.type), type: val(rec.type),
siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined, siteSkus: rec.siteSkus
? String(rec.siteSkus)
.split(/[;,]/) // 支持英文分号或英文逗号分隔
.map(s => s.trim())
.filter(Boolean)
: undefined,
category, // 添加分类字段 category, // 添加分类字段
attributes: attributes.length > 0 ? attributes : undefined, attributes: attributes.length > 0 ? attributes : undefined,
@ -1531,7 +1536,14 @@ export class ProductService {
return dto; return dto;
} }
getAttributesObject(attributes:DictItem[]){
if(!attributes) return {}
const obj:any = {}
attributes.forEach(attr=>{
obj[attr.dict.name] = attr
})
return obj
}
// 将单个产品转换为 CSV 行数组 // 将单个产品转换为 CSV 行数组
transformProductToCsvRow( transformProductToCsvRow(
p: Product, p: Product,

View File

@ -313,7 +313,6 @@ export class ShopyyService {
const { items: firstPageItems, totalPages} = firstPage; const { items: firstPageItems, totalPages} = firstPage;
// const { page = 1, per_page = 100 } = params;
// 如果只有一页数据,直接返回 // 如果只有一页数据,直接返回
if (totalPages <= 1) { if (totalPages <= 1) {
return firstPageItems; return firstPageItems;
@ -334,7 +333,7 @@ export class ShopyyService {
// 创建当前批次的并发请求 // 创建当前批次的并发请求
for (let i = 0; i < batchSize; i++) { for (let i = 0; i < batchSize; i++) {
const page = currentPage + i; const page = currentPage + i;
const pagePromise = this.getOrders(site, page, 100) const pagePromise = this.getOrders(site, page, 100, params)
.then(pageResult => pageResult.items) .then(pageResult => pageResult.items)
.catch(error => { .catch(error => {
console.error(`获取第 ${page} 页数据失败:`, error); console.error(`获取第 ${page} 页数据失败:`, error);

View File

@ -73,16 +73,16 @@ export class StatisticsService {
order_sales_summary AS ( order_sales_summary AS (
SELECT SELECT
orderId, orderId,
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity, SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity, SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity, SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
FROM order_sale FROM order_sale
GROUP BY orderId GROUP BY orderId
), ),
@ -269,16 +269,16 @@ export class StatisticsService {
order_sales_summary AS ( order_sales_summary AS (
SELECT SELECT
orderId, orderId,
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity, SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity, SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity, SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
FROM order_sale FROM order_sale
GROUP BY orderId GROUP BY orderId
), ),
@ -466,16 +466,16 @@ export class StatisticsService {
order_sales_summary AS ( order_sales_summary AS (
SELECT SELECT
orderId, orderId,
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity, SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity, SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity, SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity, SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
FROM order_sale FROM order_sale
GROUP BY orderId GROUP BY orderId
), ),

26
test-freightwaves.js Normal file
View File

@ -0,0 +1,26 @@
// Test script for FreightwavesService createOrder method
const { FreightwavesService } = require('./dist/service/test-freightwaves.service');
async function testFreightwavesService() {
try {
// Create an instance of the FreightwavesService
const service = new FreightwavesService();
// Call the test method
console.log('Starting test for createOrder method...');
const result = await service.testQueryOrder();
console.log('Test completed successfully!');
console.log('Result:', result);
console.log('\nTo run the actual createOrder request:');
console.log('1. Uncomment the createOrder call in the testCreateOrder method');
console.log('2. Update the test-secret, test-partner-id with real credentials');
console.log('3. Run this script again');
} catch (error) {
console.error('Test failed:', error);
}
}
// Run the test
testFreightwavesService();