forked from yoone/API
1
0
Fork 0

Merge pull request 'main' (#49) from zhuotianyuan/API:main into main

Reviewed-on: yoone/API#49
This commit is contained in:
zhuotianyuan 2026-01-14 11:45:31 +00:00
commit 408ec8f424
7 changed files with 633 additions and 2664 deletions

View File

@ -23,7 +23,7 @@ import {
UpdateReviewDTO,
OrderPaymentStatus,
} from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto';
import {
ShopyyAllProductQuery,
ShopyyCustomer,
@ -40,6 +40,7 @@ import {
OrderStatus,
} from '../enums/base.enum';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import dayjs = require('dayjs');
export class ShopyyAdapter implements ISiteAdapter {
shopyyFinancialStatusMap = {
'200': '待支付',
@ -570,9 +571,21 @@ export class ShopyyAdapter implements ISiteAdapter {
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[]> {
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));
}

View File

@ -52,6 +52,23 @@ export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
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

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

File diff suppressed because it is too large Load Diff

View File

@ -141,7 +141,7 @@ export class OrderService {
updated: 0,
errors: []
};
console.log('开始进入循环同步订单', result.length, '个订单')
// 遍历每个订单进行同步
for (const order of result) {
try {
@ -165,6 +165,7 @@ export class OrderService {
} else {
syncResult.created++;
}
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
} catch (error) {
// 记录错误但不中断整个同步过程
syncResult.errors.push({
@ -174,6 +175,8 @@ export class OrderService {
syncResult.processed++;
}
}
console.log('同步完成', syncResult.updated, 'created:', syncResult.created)
this.logger.debug('syncOrders result', syncResult)
return syncResult;
}
@ -350,14 +353,14 @@ export class OrderService {
await this.saveOrderItems({
siteId,
orderId,
externalOrderId,
externalOrderId: String(externalOrderId),
orderItems: line_items,
});
// 保存退款信息
await this.saveOrderRefunds({
siteId,
orderId,
externalOrderId,
externalOrderId ,
refunds,
});
// 保存费用信息
@ -1229,13 +1232,13 @@ export class OrderService {
parameters.push(siteId);
}
if (startDate) {
sqlQuery += ` AND o.date_created >= ?`;
totalQuery += ` AND o.date_created >= ?`;
sqlQuery += ` AND o.date_paid >= ?`;
totalQuery += ` AND o.date_paid >= ?`;
parameters.push(startDate);
}
if (endDate) {
sqlQuery += ` AND o.date_created <= ?`;
totalQuery += ` AND o.date_created <= ?`;
sqlQuery += ` AND o.date_paid <= ?`;
totalQuery += ` AND o.date_paid <= ?`;
parameters.push(endDate);
}
// 支付方式筛选(使用参数化,避免SQL注入)
@ -1323,7 +1326,7 @@ export class OrderService {
// 添加分页到主查询
sqlQuery += `
GROUP BY o.id
ORDER BY o.date_created DESC
ORDER BY o.date_paid DESC
LIMIT ? OFFSET ?
`;
parameters.push(pageSize, (current - 1) * pageSize);
@ -2541,7 +2544,7 @@ export class OrderService {
'姓名地址': nameAddress,
'邮箱': order.customer_email || '',
'号码': phone,
'订单内容': orderContent,
'订单内容': this.removeLastParenthesesContent(orderContent),
'盒数': boxCount,
'换盒数': exchangeBoxCount,
'换货内容': exchangeContent,
@ -2641,4 +2644,80 @@ export class OrderService {
throw new Error(`导出CSV文件失败: ${error.message}`);
}
}
/**
*
* @param str
* @returns
*/
removeLastParenthesesContent(str: string): string {
if (!str || typeof str !== 'string') {
return str;
}
// 辅助函数:删除指定位置的括号对及其内容
const removeParenthesesAt = (s: string, leftIndex: number): string => {
if (leftIndex === -1) return s;
let rightIndex = -1;
let parenCount = 0;
for (let i = leftIndex; i < s.length; i++) {
const char = s[i];
if (char === '(') {
parenCount++;
} else if (char === ')') {
parenCount--;
if (parenCount === 0) {
rightIndex = i;
break;
}
}
}
if (rightIndex !== -1) {
return s.substring(0, leftIndex) + s.substring(rightIndex + 1);
}
return s;
};
// 1. 处理每个分号前面的括号对
let result = str;
// 找出所有分号的位置
const semicolonIndices: number[] = [];
for (let i = 0; i < result.length; i++) {
if (result[i] === ';') {
semicolonIndices.push(i);
}
}
// 从后向前处理每个分号,避免位置变化影响后续处理
for (let i = semicolonIndices.length - 1; i >= 0; i--) {
const semicolonIndex = semicolonIndices[i];
// 从分号位置向前查找最近的左括号
let lastLeftParenIndex = -1;
for (let j = semicolonIndex - 1; j >= 0; j--) {
if (result[j] === '(') {
lastLeftParenIndex = j;
break;
}
}
// 如果找到左括号,删除该括号对及其内容
if (lastLeftParenIndex !== -1) {
result = removeParenthesesAt(result, lastLeftParenIndex);
}
}
// 2. 处理整个字符串的最后一个括号对
let lastLeftParenIndex = result.lastIndexOf('(');
if (lastLeftParenIndex !== -1) {
result = removeParenthesesAt(result, lastLeftParenIndex);
}
return result;
}
}

View File

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

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();