From bfa03fc6a0cf6b369f1835c87b9c017c59ecd43e Mon Sep 17 00:00:00 2001 From: zhuotianyuan Date: Tue, 13 Jan 2026 14:55:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(freightwaves):=20=E6=B7=BB=E5=8A=A0TMS?= =?UTF-8?q?=E7=B3=BB=E7=BB=9FAPI=E9=9B=86=E6=88=90=E5=92=8C=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/freightwaves.service.ts | 486 ++++++++++++++++++++++++++++ test-freightwaves.js | 26 ++ 2 files changed, 512 insertions(+) create mode 100644 src/service/freightwaves.service.ts create mode 100644 test-freightwaves.js diff --git a/src/service/freightwaves.service.ts b/src/service/freightwaves.service.ts new file mode 100644 index 0000000..90c0d4a --- /dev/null +++ b/src/service/freightwaves.service.ts @@ -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 { + 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): 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(url: string, data: any): Promise> { + 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>( + `${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): Promise { + const requestData: RateTryRequest = { + ...params, + partner: this.config.partner, + }; + + const response = await this.sendRequest('/shipService/order/rateTry', requestData); + return response.data; + } + + /** + * 创建订单 + * @param params 创建订单参数 + * @returns 创建订单结果 + */ + async createOrder(params: Omit): Promise { + const requestData: CreateOrderRequest = { + ...params, + partner: this.config.partner, + }; + + const response = await this.sendRequest('/shipService/order/createOrder', requestData); + return response.data; + } + + /** + * 查询订单 + * @param params 查询订单参数 + * @returns 查询订单结果 + */ + async queryOrder(params: Omit): Promise { + const requestData: QueryOrderRequest = { + ...params, + partner: this.config.partner, + }; + + const response = await this.sendRequest('/shipService/order/queryOrder', requestData); + return response.data; + } + + /** + * 修改订单 + * @param params 修改订单参数 + * @returns 修改订单结果 + */ + async modifyOrder(params: Omit): Promise { + const requestData: ModifyOrderRequest = { + ...params, + partner: this.config.partner, + }; + + const response = await this.sendRequest('/shipService/order/modifyOrder', requestData); + return response.data; + } + + /** + * 订单退款 + * @param params 订单退款参数 + * @returns 订单退款结果 + */ + async refundOrder(params: Omit): Promise { + const requestData: RefundOrderRequest = { + ...params, + partner: this.config.partner, + }; + + const response = await this.sendRequest('/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 = { + shipCompany: 'DHL', + partnerOrderNumber: `test-order-${Date.now()}`, + warehouseId: '25072621035200000060', + shipper: { + name: 'John Doe', + phone: '123-456-7890', + company: 'Test Company', + countryCode: 'CA', + city: 'Toronto', + state: 'ON', + address1: '123 Main St', + address2: 'Suite 400', + postCode: 'M5V 2T6', + countryName: 'Canada', + cityName: 'Toronto', + stateName: 'Ontario', + companyName: 'Test Company Inc.' + }, + reciver: { + name: 'Jane Smith', + phone: '987-654-3210', + company: 'Receiver Company', + countryCode: 'CA', + city: 'Vancouver', + state: 'BC', + address1: '456 Oak St', + address2: '', + postCode: 'V6J 2A9', + countryName: 'Canada', + cityName: 'Vancouver', + stateName: 'British Columbia', + companyName: 'Receiver Company Ltd.' + }, + packages: [ + { + dimensions: { + length: 10, + width: 8, + height: 6, + lengthUnit: 'IN', + weight: 5, + weightUnit: 'LB' + }, + currency: 'CAD', + description: 'Test Package' + } + ], + declaration: { + boxNo: 'BOX-001', + sku: 'TEST-SKU-001', + cnname: '测试产品', + enname: 'Test Product', + declaredPrice: 100, + declaredQty: 1, + material: 'Plastic', + intendedUse: 'General use', + cweight: 5, + hsCode: '39269090', + battery: 'No' + }, + signService: 0 + }; + + // 调用创建订单方法 + this.log('开始测试创建订单...'); + this.log('测试参数:', testParams); + + // 注意:在实际环境中取消注释以下行来执行真实请求 + const result = await this.createOrder(testParams); + this.log('创建订单成功:', result); + + + // 返回模拟结果 + return { + partnerOrderNumber: testParams.partnerOrderNumber, + shipOrderId: `simulated-shipOrderId-${Date.now()}` + }; + } catch (error) { + this.log('测试创建订单失败:', error); + throw error; + } + } + + /** + * 测试查询订单方法 + * @returns 查询订单结果 + */ + async testQueryOrder() { + try { + // 设置必要的配置 + this.setConfig({ + appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt', + apiBaseUrl: 'https://tms.freightwaves.ca', + partner: '25072621035200000060' + }); + + // 准备测试数据 - 可以通过partnerOrderNumber或shipOrderId查询 + const testParams: Omit = { + // 选择其中一个参数进行测试 + 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); + } + } + } +} \ No newline at end of file diff --git a/test-freightwaves.js b/test-freightwaves.js new file mode 100644 index 0000000..e2a537a --- /dev/null +++ b/test-freightwaves.js @@ -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(); \ No newline at end of file