diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 9a3894d..6b373c9 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -244,7 +244,7 @@ export class ShopyyAdapter implements ISiteAdapter { fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(), company: billing.company || '', email: item.customer_email || item.email || '', - phone: billing.phone || (item as any).telephone || '', + phone: billing.phone || item.telephone || '', address_1: billing.address1 || item.payment_address || '', address_2: billing.address2 || '', city: billing.city || item.payment_city || '', @@ -275,7 +275,7 @@ export class ShopyyAdapter implements ISiteAdapter { state: shipping.province || item.shipping_zone || '', postcode: shipping.zip || item.shipping_postcode || '', method_title: item.payment_method || '', - phone: shipping.phone || (item as any).telephone || '', + phone: shipping.phone || item.telephone || '', country: shipping.country_name || shipping.country_code || @@ -393,6 +393,7 @@ export class ShopyyAdapter implements ISiteAdapter { tracking_number: f.tracking_number || '', shipping_provider: f.tracking_company || '', shipping_method: f.tracking_company || '', + date_created: typeof f.created_at === 'number' ? new Date(f.created_at * 1000).toISOString() : f.created_at || '', diff --git a/src/dto/logistics.dto.ts b/src/dto/logistics.dto.ts index 206884c..b49b72c 100644 --- a/src/dto/logistics.dto.ts +++ b/src/dto/logistics.dto.ts @@ -19,15 +19,22 @@ export class ShipmentBookDTO { @ApiProperty({ type: 'number', isArray: true }) @Rule(RuleType.array().default([])) orderIds?: number[]; + + @ApiProperty() + @Rule(RuleType.string()) + shipmentPlatform: string; } export class ShipmentFeeBookDTO { + + @ApiProperty() + shipmentPlatform: string; @ApiProperty() stockPointId: number; @ApiProperty() sender: string; @ApiProperty() - startPhone: string; + startPhone: string|any; @ApiProperty() startPostalCode: string; @ApiProperty() @@ -63,6 +70,8 @@ export class ShipmentFeeBookDTO { weight: number; @ApiProperty() weightUom: string; + @ApiProperty() + address_id: number; } export class PaymentMethodDTO { diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index bd708ab..c25cc2f 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -967,8 +967,6 @@ export interface ShopyyOrder { }>; // 支付时间 pay_at?: number | null; - // 支付时间(备用) - date_paid?: number | string; // 系统支付ID sys_payment_id?: number; @@ -1211,6 +1209,7 @@ export interface ShopyyOrder { // 时间戳信息 // ======================================== // 订单创建时间 + date_paid?: number | string; created_at?: number | string; // 订单添加时间 date_added?: string; diff --git a/src/entity/shipment.entity.ts b/src/entity/shipment.entity.ts index 947db07..26237d8 100644 --- a/src/entity/shipment.entity.ts +++ b/src/entity/shipment.entity.ts @@ -54,9 +54,9 @@ export class Shipment { tracking_provider?: string; @ApiProperty() - @Column() + @Column({ nullable: true }) @Expose() - unique_id: string; + unique_id?: string; @Column({ nullable: true }) @Expose() diff --git a/src/entity/shipping_address.entity.ts b/src/entity/shipping_address.entity.ts index 7af0422..e6fea86 100644 --- a/src/entity/shipping_address.entity.ts +++ b/src/entity/shipping_address.entity.ts @@ -47,6 +47,11 @@ export class ShippingAddress { @Expose() phone_number_country: string; + @ApiProperty() + @Column() + @Expose() + email: string; + @ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', diff --git a/src/job/sync_tms.job.ts b/src/job/sync_tms.job.ts new file mode 100644 index 0000000..ece6af6 --- /dev/null +++ b/src/job/sync_tms.job.ts @@ -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 + + 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}`); + } +} \ No newline at end of file diff --git a/src/service/freightwaves.service.ts b/src/service/freightwaves.service.ts index 10900ef..f2bbe0a 100644 --- a/src/service/freightwaves.service.ts +++ b/src/service/freightwaves.service.ts @@ -67,7 +67,7 @@ interface Declaration { } // 费用试算请求接口 -interface RateTryRequest { +export interface RateTryRequest { shipCompany: string; partnerOrderNumber: string; warehouseId?: string; @@ -118,8 +118,8 @@ interface RateTryResponseData { // 创建订单响应数据接口 interface CreateOrderResponseData { - partnerOrderNumber: string; - shipOrderId: string; + msg: string; + data: any; } // 查询订单响应数据接口 @@ -140,10 +140,10 @@ interface QueryOrderResponseData { } // 修改订单响应数据接口 -interface ModifyOrderResponseData extends CreateOrderResponseData {} +interface ModifyOrderResponseData extends CreateOrderResponseData { } // 订单退款响应数据接口 -interface RefundOrderResponseData {} +interface RefundOrderResponseData { } @Provide() export class FreightwavesService { @@ -152,8 +152,8 @@ export class FreightwavesService { // 默认配置 private config: FreightwavesConfig = { appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt', - apiBaseUrl: 'https://tms.freightwaves.ca', - partner: '25072621035200000060', + apiBaseUrl: 'http://tms.freightwaves.ca:8901', + partner: '25072621035200000060' }; // 初始化配置 @@ -172,27 +172,21 @@ export class FreightwavesService { 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 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 + httpsAgent: new (require('https').Agent)({ + rejectUnauthorized: false }) } ); @@ -267,8 +261,8 @@ export class FreightwavesService { partner: this.config.partner, }; - const response = await this.sendRequest('/shipService/order/createOrder?apipost_id=0422aa', requestData); - return response.data; + const response = await this.sendRequest('/shipService/order/createOrder', requestData); + return response; } /** @@ -283,6 +277,9 @@ export class FreightwavesService { }; const response = await this.sendRequest('/shipService/order/queryOrder', requestData); + if (response.code !== '00000200') { + throw new Error(response.msg); + } return response.data; } @@ -311,161 +308,10 @@ export class FreightwavesService { ...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 日志消息 diff --git a/src/service/logistics.service.ts b/src/service/logistics.service.ts index b37c781..4e472be 100644 --- a/src/service/logistics.service.ts +++ b/src/service/logistics.service.ts @@ -27,10 +27,12 @@ import { CanadaPostService } from './canadaPost.service'; import { OrderItem } from '../entity/order_item.entity'; import { OrderSale } from '../entity/order_sale.entity'; import { UniExpressService } from './uni_express.service'; +import { FreightwavesService, RateTryRequest } from './freightwaves.service'; import { StockPoint } from '../entity/stock_point.entity'; import { OrderService } from './order.service'; import { convertKeysFromCamelToSnake } from '../utils/object-transform.util'; import { SiteService } from './site.service'; +import { ShopyyService } from './shopyy.service'; @Provide() export class LogisticsService { @@ -73,9 +75,15 @@ export class LogisticsService { @Inject() uniExpressService: UniExpressService; + @Inject() + freightwavesService: FreightwavesService; + @Inject() wpService: WPService; + @Inject() + shopyyService: ShopyyService; + @Inject() orderService: OrderService; @@ -126,8 +134,8 @@ export class LogisticsService { const data = await this.uniExpressService.getOrderStatus(shipment.return_tracking_number); console.log('updateShipmentState data:', data); // huo - if(data.status === 'FAIL'){ - throw new Error('获取运单状态失败,原因为'+ data.ret_msg) + if (data.status === 'FAIL') { + throw new Error('获取运单状态失败,原因为' + data.ret_msg) } shipment.state = data.data[0].state; if (shipment.state in [203, 215, 216, 230]) { // todo,写常数 @@ -141,6 +149,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) { const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id }); return this.updateShipmentState(shipment); @@ -247,8 +279,7 @@ export class LogisticsService { shipmentRepo.remove(shipment); - const res = await this.uniExpressService.deleteShipment(shipment.return_tracking_number); - console.log('res', res.data); // todo + await this.uniExpressService.deleteShipment(shipment.return_tracking_number); await orderRepo.save(order); @@ -278,7 +309,6 @@ export class LogisticsService { console.log('同步到woocommerce失败', error); return true; } - return true; } catch { throw new Error('删除运单失败'); @@ -294,7 +324,16 @@ export class LogisticsService { currency: 'CAD', // 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') { + const fre_reqBody = await this.convertToFreightwavesRateTry(data); + resShipmentFee = await this.freightwavesService.rateTry(fre_reqBody); + } else { + throw new Error('不支持的运单平台'); + } + if (resShipmentFee.status !== 'SUCCESS') { throw new Error(resShipmentFee.ret_msg); } @@ -319,40 +358,10 @@ export class LogisticsService { let resShipmentOrder; try { - const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId }); - 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 - } - } + resShipmentOrder = await this.mepShipment(data, order); - // 添加运单 - resShipmentOrder = await this.uniExpressService.createShipment(reqBody); - - // 记录物流信息,并将订单状态转到完成 - if (resShipmentOrder.status === 'SUCCESS') { + // 记录物流信息,并将订单状态转到完成,uniuni状态为SUCCESS,tms.freightwaves状态为00000200 + if (resShipmentOrder.status === 'SUCCESS' || resShipmentOrder.code === '00000200') { order.orderStatus = ErpOrderStatus.COMPLETED; } else { throw new Error('运单生成失败'); @@ -363,49 +372,89 @@ export class LogisticsService { await dataSource.transaction(async manager => { const orderRepo = manager.getRepository(Order); const shipmentRepo = manager.getRepository(Shipment); - const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数 + const tracking_provider = data.shipmentPlatform; // todo: id未确定,后写进常数 // 同步物流信息到woocommerce const site = await this.siteService.get(Number(order.siteId), true); - const res = await this.wpService.createFulfillment(site, order.externalOrderId, { - tracking_number: resShipmentOrder.data.tno, - tracking_provider: tracking_provider, - }); - - if (order.orderStatus === ErpOrderStatus.COMPLETED) { - const shipment = await shipmentRepo.save({ - tracking_provider: tracking_provider, - tracking_id: res.data.tracking_id, - unique_id: resShipmentOrder.data.uni_order_sn, - stockPointId: String(data.stockPointId), // todo - state: resShipmentOrder.data.uni_status_code, - return_tracking_number: resShipmentOrder.data.tno, - fee: data.details.shipmentFee, - order: order - }); - order.shipmentId = shipment.id; - shipmentId = shipment.id; + 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; } // 同步订单状态到woocommerce - if (order.status !== OrderStatus.COMPLETED) { - await this.wpService.updateOrder(site, order.externalOrderId, { - status: OrderStatus.COMPLETED, + if (order.source_type != "shopyy") { + const res = await this.wpService.createFulfillment(site, order.externalOrderId, { + tracking_number: co, + tracking_provider: tracking_provider, }); - order.status = OrderStatus.COMPLETED; + + if (order.orderStatus === ErpOrderStatus.COMPLETED) { + const shipment = await shipmentRepo.save({ + tracking_provider: tracking_provider, + tracking_id: res.data.tracking_id, + unique_id: unique_id, + stockPointId: String(data.stockPointId), // todo + state: state, + return_tracking_number: co, + fee: data.details.shipmentFee, + order: order + }); + order.shipmentId = shipment.id; + shipmentId = shipment.id; + } + if (order.status !== OrderStatus.COMPLETED) { + await this.wpService.updateOrder(site, order.externalOrderId, { + status: OrderStatus.COMPLETED, + }); + order.status = OrderStatus.COMPLETED; + } + } + if (order.source_type === "shopyy") { + const res = await this.shopyyService.createFulfillment(site, order.externalOrderId, { + tracking_number: co, + tracking_company: resShipmentOrder.shipCompany, + carrier_code: resShipmentOrder.shipperOrderId, + }); + if (order.orderStatus === ErpOrderStatus.COMPLETED) { + const shipment = await shipmentRepo.save({ + tracking_provider: tracking_provider, + tracking_id: res.data.tracking_id, + unique_id: unique_id, + stockPointId: String(data.stockPointId), // todo + state: state, + return_tracking_number: co, + fee: data.details.shipmentFee, + order: order + }); + order.shipmentId = shipment.id; + shipmentId = shipment.id; + } + if (order.status !== OrderStatus.COMPLETED) { + // shopyy未提供更新订单接口,暂不更新订单状态 + // await this.shopyyService.updateOrder(site, order.externalOrderId, { + // status: OrderStatus.COMPLETED, + // }); + order.status = OrderStatus.COMPLETED; + } } order.orderStatus = ErpOrderStatus.COMPLETED; - await orderRepo.save(order); }).catch(error => { transactionError = error + throw new Error(`请求错误:${error}`); }); if (transactionError !== undefined) { - console.log('err', transactionError); throw transactionError; } - // 更新产品发货信息 this.orderService.updateOrderSales(order.id, sales); @@ -642,4 +691,190 @@ export class LogisticsService { 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 + '-' + 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 + // 非跨境订单不需要declaration + // declaration: { + // "boxNo": "", //箱子编号 + // "sku": "", //SKU + // "cnname": "", //中文名称 + // "enname": "", //英文名称 + // "declaredPrice": 1, //申报单价 + // "declaredQty": 1, //申报数量 + // "material": "", //材质 + // "intendedUse": "", //用途 + // "cweight": 1, //产品单重 + // "hsCode": "", //海关编码 + // "battery": "" //电池描述 + // } + }; + + resShipmentOrder = await this.freightwavesService.createOrder(reqBody); // 创建订单 + //tms只返回了物流订单号,需要查询一次来获取完整的物流信息 + const queryRes = await this.freightwavesService.queryOrder({ shipOrderId: resShipmentOrder.shipOrderId }); // 查询订单 + resShipmentOrder.push(queryRes); + } + + return resShipmentOrder; + } catch (error) { + console.log('物流订单处理失败:', error); // 使用console.log代替this.log + throw error; + } + } + + /** + * 将ShipmentFeeBookDTO转换为freightwaves的RateTryRequest格式 + * @param data ShipmentFeeBookDTO数据 + * @returns RateTryRequest格式的数据 + */ + async convertToFreightwavesRateTry(data: ShipmentFeeBookDTO): Promise> { + + const shipments = await this.shippingAddressModel.findOne({ + where: { + id: data.address_id, + }, + }) + + const address = shipments?.address; + // 转换为RateTryRequest格式 + const r = { + shipCompany: 'UPSYYZ7000NEW', // 必填,但ShipmentFeeBookDTO中缺少 + partnerOrderNumber: `order-${Date.now()}`, // 必填,使用时间戳生成 + warehouseId: '25072621030107400060', // 可选,使用stockPointId转换 + shipper: { + name: data.sender, // 必填 + phone: data.startPhone.phone, // 必填 + company: address.country, // 必填,但ShipmentFeeBookDTO中缺少 + countryCode: data.shipperCountryCode, // 必填 + city: address.city || '', // 必填,但ShipmentFeeBookDTO中缺少 + state: address.region || '', // 必填,但ShipmentFeeBookDTO中缺少 + address1: address.address_line_1, // 必填 + address2: address.address_line_1 || '', // 必填,但ShipmentFeeBookDTO中缺少 + postCode: data.startPostalCode, // 必填 + countryName: address.country || '', // 必填,但ShipmentFeeBookDTO中缺少 + cityName: address.city || '', // 必填,但ShipmentFeeBookDTO中缺少 + stateName: address.region || '', // 必填,但ShipmentFeeBookDTO中缺少 + companyName: address.country || '', // 必填,但ShipmentFeeBookDTO中缺少 + }, + reciver: { + name: data.receiver, // 必填 + phone: data.receiverPhone, // 必填 + company: address.country,// 必填,但ShipmentFeeBookDTO中缺少 + countryCode: data.country, // 必填,使用country代替countryCode + city: data.city, // 必填 + state: data.province, // 必填,使用province代替state + address1: data.deliveryAddress, // 必填 + address2: data.deliveryAddress, // 必填,但ShipmentFeeBookDTO中缺少 + postCode: data.postalCode, // 必填 + countryName: address.country, // 必填,但ShipmentFeeBookDTO中缺少 + cityName: data.city || '', // 必填,使用city代替cityName + stateName: data.province || '', // 必填,使用province代替stateName + companyName: address.country || '', // 必填,但ShipmentFeeBookDTO中缺少 + }, + packages: [ + { + dimensions: { + length: data.length, // 必填 + width: data.width, // 必填 + height: data.height, // 必填 + lengthUnit: (data.dimensionUom === 'IN' ? 'IN' : 'CM') as 'IN' | 'CM', // 必填,转换为有效的单位 + weight: data.weight, // 必填 + weightUnit: (data.weightUom === 'LBS' ? 'LB' : 'KG') as 'LB' | 'KG', // 必填,转换为有效的单位 + }, + currency: 'CAD', // 必填,但ShipmentFeeBookDTO中缺少,使用默认值 + description: 'Package', // 必填,但ShipmentFeeBookDTO中缺少,使用默认值 + }, + ], + signService: 0, // 可选,默认不使用签名服务 + }; + return r as any; + } } diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 0cc42ed..321221c 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -141,7 +141,7 @@ export class OrderService { updated: 0, errors: [] }; - console.log('开始进入循环同步订单', result.length, '个订单') + this.logger.info('开始进入循环同步订单', result.length, '个订单') // 遍历每个订单进行同步 for (const order of result) { try { @@ -150,7 +150,7 @@ export class OrderService { where: { externalOrderId: String(order.id), siteId: siteId }, }); if (!existingOrder) { - console.log("数据库中不存在", order.id, '订单状态:', order.status) + this.logger.debug("数据库中不存在", order.id, '订单状态:', order.status) } // 同步单个订单 await this.syncSingleOrder(siteId, order); @@ -165,7 +165,7 @@ export class OrderService { } else { syncResult.created++; } - // console.log('updated', syncResult.updated, 'created:', syncResult.created) + // console.log('updated', syncResult.updated, 'created:', syncResult.created) } catch (error) { // 记录错误但不中断整个同步过程 syncResult.errors.push({ @@ -175,9 +175,7 @@ export class OrderService { syncResult.processed++; } } - console.log('同步完成', syncResult.updated, 'created:', syncResult.created) - - this.logger.debug('syncOrders result', syncResult) + this.logger.info('同步完成', syncResult.updated, 'created:', syncResult.created) return syncResult; } @@ -215,7 +213,7 @@ export class OrderService { where: { externalOrderId: String(order.id), siteId: siteId }, }); if (!existingOrder) { - console.log("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status) + this.logger.debug("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status) } // 同步单个订单 await this.syncSingleOrder(siteId, order, true); @@ -329,13 +327,30 @@ export class OrderService { this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id) return; } - // 这里其实不用过滤不可编辑的行为,而是应在 save 中做判断 - // if(!order.is_editable && !forceUpdate){ - // this.logger.debug('订单不可编辑,跳过处理', siteId, order.id) - // return; - // } - // 自动转换远程订单的状态(如果需要) + // 检查数据库中是否已存在该订单 + const existingOrder = await this.orderModel.findOne({ + where: { externalOrderId: String(order.id), siteId: siteId }, + }); + // 自动更新订单状态(如果需要) 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 已经包括了创建订单和更新订单 let orderRecord: Order = await this.saveOrder(siteId, orderData); // 如果订单从未完成变为完成状态,则更新库存 @@ -347,7 +362,6 @@ export class OrderService { await this.updateStock(orderRecord); // 不再直接返回,继续执行后续的更新操作 } - const externalOrderId = String(order.id); const orderId = orderRecord.id; // 保存订单项 await this.saveOrderItems({ @@ -360,7 +374,7 @@ export class OrderService { await this.saveOrderRefunds({ siteId, orderId, - externalOrderId , + externalOrderId, refunds, }); // 保存费用信息 @@ -714,11 +728,12 @@ export class OrderService { await this.orderSaleModel.delete(currentOrderSale.map(v => v.id)); } if (!orderItem.sku) return; + // 从数据库查询产品,关联查询组件 const productDetail = await this.productService.getComponentDetailFromSiteSku({ sku: orderItem.sku, name: orderItem.name }); - + if (!productDetail || !productDetail.quantity) return; - const {product, quantity} = productDetail + const { product, quantity } = productDetail const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => { return { product: await this.productModel.findOne({ @@ -752,7 +767,6 @@ export class OrderService { }); return orderSale }).filter(v => v !== null) - console.log("orderSales",orderSales) if (orderSales.length > 0) { await this.orderSaleModel.save(orderSales); } @@ -1544,7 +1558,6 @@ export class OrderService { GROUP BY os.productId `; - console.log('------3.5-----', pcSql, pcParams, exceptPackage); const pcResults = await this.orderSaleModel.query(pcSql, pcParams); const pcMap = new Map(); @@ -2512,7 +2525,7 @@ export class OrderService { 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} x ${item.quantity}`).join('; '); // 构建姓名地址 const shipping = order.shipping; @@ -2544,7 +2557,7 @@ export class OrderService { '姓名地址': nameAddress, '邮箱': order.customer_email || '', '号码': phone, - '订单内容': this.removeLastParenthesesContent(orderContent), + '订单内容': orderContent, '盒数': boxCount, '换盒数': exchangeBoxCount, '换货内容': exchangeContent, @@ -2623,13 +2636,9 @@ export class OrderService { 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; } @@ -2640,7 +2649,6 @@ export class OrderService { return csvContent; } catch (error) { - console.error('导出CSV时出错:', error); throw new Error(`导出CSV文件失败: ${error.message}`); } } @@ -2658,10 +2666,10 @@ export class OrderService { // 辅助函数:删除指定位置的括号对及其内容 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 === '(') { @@ -2674,17 +2682,17 @@ export class OrderService { } } } - + 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++) { @@ -2692,11 +2700,11 @@ export class OrderService { 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--) { @@ -2705,7 +2713,7 @@ export class OrderService { break; } } - + // 如果找到左括号,删除该括号对及其内容 if (lastLeftParenIndex !== -1) { result = removeParenthesesAt(result, lastLeftParenIndex); diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index 28029bb..616705f 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -10,14 +10,14 @@ import { Site } from '../entity/site.entity'; import { UnifiedReviewDTO } from '../dto/site-api.dto'; import { ShopyyGetOneOrderResult, ShopyyReview } from '../dto/shopyy.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -import { UnifiedSearchParamsDTO,ShopyyGetAllOrdersParams } from '../dto/api.dto'; +import { UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto'; /** * ShopYY平台服务实现 */ @Provide() export class ShopyyService { @Inject() - logger:ILogger; + logger: ILogger; /** * 获取ShopYY评论列表 * @param site 站点配置 @@ -184,9 +184,9 @@ export class ShopyyService { */ public async fetchResourcePaged(site: any, endpoint: string, params: Record = {}) { const response = await this.request(site, endpoint, 'GET', null, params); - return this.mapPageResponse(response,params); + return this.mapPageResponse(response, params); } - mapPageResponse(response:any,query: Record){ + mapPageResponse(response: any, query: Record) { if (response?.code !== 0) { throw new Error(response?.msg) } @@ -272,7 +272,7 @@ export class ShopyyService { const response = await this.request(site, `products/${productId}/variations/${variationId}`, 'GET'); return response.data; } - mapOrderSearchParams(params: UnifiedSearchParamsDTO){ + mapOrderSearchParams(params: UnifiedSearchParamsDTO) { const { after, before, ...restParams } = params; return { ...restParams, @@ -310,9 +310,9 @@ export class ShopyyService { async getAllOrders(site: any | number, params: ShopyyGetAllOrdersParams = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise { const firstPage = await this.getOrders(site, 1, 100, params); - - const { items: firstPageItems, totalPages} = firstPage; - + + const { items: firstPageItems, totalPages } = firstPage; + // 如果只有一页数据,直接返回 if (totalPages <= 1) { return firstPageItems; @@ -320,7 +320,7 @@ export class ShopyyService { // 限制最大页数,避免过多的并发请求 const actualMaxPages = Math.min(totalPages, maxPages); - + // 收集所有页面数据,从第二页开始 const allItems = [...firstPageItems]; let currentPage = 2; @@ -329,7 +329,7 @@ export class ShopyyService { while (currentPage <= actualMaxPages) { const batchPromises: Promise[] = []; const batchSize = Math.min(concurrencyLimit, actualMaxPages - currentPage + 1); - + // 创建当前批次的并发请求 for (let i = 0; i < batchSize; i++) { const page = currentPage + i; @@ -339,25 +339,25 @@ export class ShopyyService { console.error(`获取第 ${page} 页数据失败:`, error); return []; // 如果某页获取失败,返回空数组,不影响整体结果 }); - + batchPromises.push(pagePromise); } // 等待当前批次完成 const batchResults = await Promise.all(batchPromises); - + // 合并当前批次的数据 for (const pageItems of batchResults) { allItems.push(...pageItems); } - + // 移动到下一批次 currentPage += batchSize; } return allItems; } - + /** * 获取ShopYY订单详情 @@ -475,13 +475,16 @@ export class ShopyyService { async createFulfillment(site: Site, orderId: string, data: any): Promise { // ShopYY API: POST /orders/{id}/shipments const fulfillmentData = { - tracking_number: data.tracking_number, - carrier_code: data.carrier_code, - carrier_name: data.carrier_name, - shipping_method: data.shipping_method + data: [{ + order_number: orderId, + tracking_company: data.tracking_company, + tracking_number: data.tracking_number, + carrier_code: data.carrier_code, + note: "note", + mode: "" + }] }; - - const response = await this.request(site, `orders/${orderId}/shipments`, 'POST', fulfillmentData); + const response = await this.request(site, `orders/fulfillments`, 'POST', fulfillmentData); return response.data; } @@ -494,7 +497,7 @@ export class ShopyyService { */ async deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise { try { - // ShopYY API: DELETE /orders/{order_id}/shipments/{fulfillment_id} + // ShopYY API: DELETE /orders/fulfillments/{fulfillment_id} await this.request(site, `orders/${orderId}/fulfillments/${fulfillmentId}`, 'DELETE'); return true; } catch (error) { @@ -542,7 +545,7 @@ export class ShopyyService { try { // ShopYY API: PUT /orders/{order_id}/shipments/{tracking_id} const fulfillmentData: any = {}; - + // 只传递有值的字段 if (data.tracking_number !== undefined) { fulfillmentData.tracking_number = data.tracking_number; @@ -645,10 +648,10 @@ export class ShopyyService { // ShopYY API: POST /products/batch const response = await this.request(site, 'products/batch', 'POST', data); const result = response.data; - + // 转换 ShopYY 批量操作结果为统一格式 - const errors: Array<{identifier: string, error: string}> = []; - + const errors: Array<{ identifier: string, error: string }> = []; + // 假设 ShopYY 返回格式与 WooCommerce 类似: { create: [...], update: [...], delete: [...] } // 错误信息可能在每个项目的 error 字段中 const checkForErrors = (items: any[]) => { @@ -661,12 +664,12 @@ export class ShopyyService { } }); }; - + // 检查每个操作类型的结果中的错误 if (result.create) checkForErrors(result.create); if (result.update) checkForErrors(result.update); if (result.delete) checkForErrors(result.delete); - + return { total: (data.create?.length || 0) + (data.update?.length || 0) + (data.delete?.length || 0), processed: (result.create?.length || 0) + (result.update?.length || 0) + (result.delete?.length || 0),