import { Provide } from '@midwayjs/core'; import { Config } from '@midwayjs/decorator'; import axios from 'axios'; import { parseStringPromise, Builder } from 'xml2js'; import { ShipmentType } from '../enums/base.enum'; @Provide() export class CanadaPostService { @Config('canadaPost.url') url; @Config('canadaPost.username') username; @Config('canadaPost.password') password; @Config('canadaPost.customerNumber') customerNumber; @Config('canadaPost.contractId') contractId; private getAuth() { return { username: this.username, password: this.password, }; } private getHeaders(accept = 'application/vnd.cpc.shipment-v8+xml') { return { Accept: accept, 'Content-Type': accept, 'Accept-language': 'en-CA', }; } private async parseXML(xml: string) { return await parseStringPromise(xml, { explicitArray: false }); } private buildXML(data: any, rootName?: string, namespace?: string): string { const builder = new Builder({ headless: false, xmldec: { version: '1.0', encoding: 'UTF-8' }, }); // 如果指定了根节点和命名空间 if (rootName && namespace) { const xmlObj = { [rootName]: { $: { xmlns: namespace }, ...data, }, }; return builder.buildObject(xmlObj); } // 默认直接构建(用于 createShipment 这类已有完整结构) return builder.buildObject(data); } private normalizeCanadaPostRates(rawData: any) { const services = rawData['price-quotes']?.['price-quote']; if (!services) return []; const list = Array.isArray(services) ? services : [services]; return list.map(s => ({ carrier_name: 'Canada Post', service_name: s['service-name'], service_id: s['service-code'], total: { value: 100 * parseFloat(s['price-details']['due']), currency: 'CAD', }, transit_time_days: s['service-standard']?.['expected-transit-time'], type: ShipmentType.CANADAPOST, })); } /** 获取运费估价 */ async getRates(rateRequest: any) { const xmlBody = this.buildXML( rateRequest, 'mailing-scenario', 'http://www.canadapost.ca/ws/ship/rate-v4' ); const url = `${this.url}/rs/ship/price`; const res = await axios.post(url, xmlBody, { auth: this.getAuth(), headers: this.getHeaders('application/vnd.cpc.ship.rate-v4+xml'), }); return this.normalizeCanadaPostRates(await this.parseXML(res.data)); } private normalizeCanadaPostShipment(rawData: any) { const shipment = rawData['shipment-info']; return { id: shipment['shipment-id'], tracking_provider: 'Canada Post', unique_id: shipment['shipment-id'], state: 'waiting-for-transit', primary_tracking_number: shipment['tracking-pin'], tracking_url: `https://www.canadapost-postescanada.ca/track-reperage/en#/details/${shipment['tracking-pin']}`, labels: [ { url: shipment['links']['link']?.find( link => link['$']['rel'] === 'label' )?.['$']['href'], }, ], }; } /** 创建运单 */ async createShipment(shipmentRequest: any) { const xmlBody = this.buildXML( shipmentRequest, 'shipment', 'http://www.canadapost.ca/ws/shipment-v8' ); const url = `${this.url}/rs/${this.customerNumber}/${this.customerNumber}/shipment`; const res = await axios.post(url, xmlBody, { auth: this.getAuth(), headers: this.getHeaders('application/vnd.cpc.shipment-v8+xml'), }); return this.normalizeCanadaPostShipment(await this.parseXML(res.data)); } /** 查询运单 */ async getShipment(pin: string) { const url = `${this.url}/vis/track/pin/${pin}/summary`; const res = await axios.get(url, { auth: this.getAuth(), headers: this.getHeaders('application/vnd.cpc.track-v2+xml'), }); const shipment = await this.parseXML(res.data); const eventType = shipment['tracking-summary']['pin-summary']['event-type']?.toUpperCase(); return { shipment: { state: eventType === 'INDUCTION' ? 'waiting-for-transit' : eventType === 'DELIVERED' ? 'delivered' : 'in-transit', }, }; } /** 取消运单 */ async cancelShipment(shipmentId: number) { const url = `${this.url}/rs/${this.customerNumber}/${this.customerNumber}/shipment/${shipmentId}`; const res = await axios.delete(url, { auth: this.getAuth(), headers: this.getHeaders('application/vnd.cpc.shipment-v8+xml'), }); console.log(res); return res.status === 200 || res.status === 204; } }