仓库管理配置 小飞侠配置
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-07-16 21:40:25 +08:00
parent f6d97b4ee8
commit bde85a7b82
15 changed files with 580 additions and 197 deletions
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { COURIER_PROVIDER } from './courier.constants';
import type {
BatchShipmentQuery,
CourierCallOptions,
CreateShipmentInput,
CreateShipmentResult,
DeliveryCoverageResult,
@@ -24,32 +25,32 @@ export class CourierService {
return this.provider.code;
}
createShipment(input: CreateShipmentInput): Promise<CreateShipmentResult> {
return this.provider.createShipment(input);
createShipment(input: CreateShipmentInput, options?: CourierCallOptions): Promise<CreateShipmentResult> {
return this.provider.createShipment(input, options);
}
cancelShipment(query: ShipmentQuery): Promise<void> {
return this.provider.cancelShipment(query);
cancelShipment(query: ShipmentQuery, options?: CourierCallOptions): Promise<void> {
return this.provider.cancelShipment(query, options);
}
getShipment(query: ShipmentQuery): Promise<ShipmentDetail> {
return this.provider.getShipment(query);
getShipment(query: ShipmentQuery, options?: CourierCallOptions): Promise<ShipmentDetail> {
return this.provider.getShipment(query, options);
}
batchGetShipments(query: BatchShipmentQuery): Promise<ShipmentDetail[]> {
return this.provider.batchGetShipments(query);
batchGetShipments(query: BatchShipmentQuery, options?: CourierCallOptions): Promise<ShipmentDetail[]> {
return this.provider.batchGetShipments(query, options);
}
getTrack(query: ShipmentQuery): Promise<TrackNode[]> {
return this.provider.getTrack(query);
getTrack(query: ShipmentQuery, options?: CourierCallOptions): Promise<TrackNode[]> {
return this.provider.getTrack(query, options);
}
checkDeliveryCoverage(toAddress: string): Promise<DeliveryCoverageResult> {
return this.provider.checkDeliveryCoverage(toAddress);
checkDeliveryCoverage(toAddress: string, options?: CourierCallOptions): Promise<DeliveryCoverageResult> {
return this.provider.checkDeliveryCoverage(toAddress, options);
}
estimateFreight(weight: number): Promise<FreightEstimateResult> {
return this.provider.estimateFreight(weight);
estimateFreight(weight: number, options?: CourierCallOptions): Promise<FreightEstimateResult> {
return this.provider.estimateFreight(weight, options);
}
buildTrackCallbackResponse(success?: boolean): TrackCallbackResponse {
@@ -1,4 +1,10 @@
import { CourierProviderCode } from './courier.constants';
import type { XiaofeixiaConfig } from './courier.config';
/** 调用时覆盖默认 env 配置(仓配管理里维护的凭证) */
export interface CourierCallOptions {
xiaofeixia?: XiaofeixiaConfig;
}
export interface CourierCoordinate {
lng: number;
@@ -106,12 +112,12 @@ export interface TrackCallbackResponse {
export interface ICourierProvider {
readonly code: CourierProviderCode;
createShipment(input: CreateShipmentInput): Promise<CreateShipmentResult>;
cancelShipment(query: ShipmentQuery): Promise<void>;
getShipment(query: ShipmentQuery): Promise<ShipmentDetail>;
batchGetShipments(query: BatchShipmentQuery): Promise<ShipmentDetail[]>;
getTrack(query: ShipmentQuery): Promise<TrackNode[]>;
checkDeliveryCoverage(toAddress: string): Promise<DeliveryCoverageResult>;
estimateFreight(weight: number): Promise<FreightEstimateResult>;
createShipment(input: CreateShipmentInput, options?: CourierCallOptions): Promise<CreateShipmentResult>;
cancelShipment(query: ShipmentQuery, options?: CourierCallOptions): Promise<void>;
getShipment(query: ShipmentQuery, options?: CourierCallOptions): Promise<ShipmentDetail>;
batchGetShipments(query: BatchShipmentQuery, options?: CourierCallOptions): Promise<ShipmentDetail[]>;
getTrack(query: ShipmentQuery, options?: CourierCallOptions): Promise<TrackNode[]>;
checkDeliveryCoverage(toAddress: string, options?: CourierCallOptions): Promise<DeliveryCoverageResult>;
estimateFreight(weight: number, options?: CourierCallOptions): Promise<FreightEstimateResult>;
buildTrackCallbackResponse(success?: boolean): TrackCallbackResponse;
}
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { CourierApiError } from '../courier.error';
import { CourierConfigService } from '../courier.config';
import { CourierConfigService, type XiaofeixiaConfig } from '../courier.config';
import { PrismaService } from '../../../common/prisma/prisma.module';
import {
logCourierCall,
@@ -21,12 +21,12 @@ export class XiaofeixiaClient {
private readonly prisma: PrismaService,
) {}
async request<T>(cmd: string, bizParams: RequestParams): Promise<T> {
const cfg = this.courierConfig.load().xiaofeixia;
async request<T>(cmd: string, bizParams: RequestParams, override?: XiaofeixiaConfig): Promise<T> {
const cfg = override ?? this.courierConfig.load().xiaofeixia;
const scene = sceneForXfxCmd(cmd);
const externalNo = this.pickExternalNo(bizParams);
if (!cfg.mchId || !cfg.apiKey) {
if (!cfg.mchId || !cfg.apiKey || !cfg.apiUrl) {
await logCourierCall(this.prisma, {
scene,
requestUrl: cfg.apiUrl || '(未配置)',
@@ -36,7 +36,7 @@ export class XiaofeixiaClient {
externalNo,
});
throw new CourierApiError(
'小飞侠商户配置不完整,请设置 XIAOFEIXIA_MCH_ID 与 XIAOFEIXIA_API_KEY',
'小飞侠商户配置不完整,请在仓配管理中填写 API 地址、商户号与 API Key',
'CONFIG_ERROR',
'XIAOFEIXIA',
);
@@ -3,6 +3,7 @@ import { CourierProviderCode } from '../courier.constants';
import { CourierApiError } from '../courier.error';
import type {
BatchShipmentQuery,
CourierCallOptions,
CreateShipmentInput,
CreateShipmentResult,
DeliveryCoverageResult,
@@ -29,28 +30,32 @@ export class XiaofeixiaProvider implements ICourierProvider {
constructor(private readonly client: XiaofeixiaClient) {}
async createShipment(input: CreateShipmentInput): Promise<CreateShipmentResult> {
const data = await this.client.request<XiaofeixiaCreateOrderData>(XIAOFEIXIA_CMD.CREATE_ORDER, {
customerId: input.customerId,
outNumber: input.outNumber,
fromAddress: input.from.address,
fromAddressDetail: input.from.addressDetail,
fromCoordinate: this.formatCoordinate(input.from.coordinate),
fromMobile: input.from.mobile,
fromName: input.from.name,
toAddress: input.to.address,
toAddressDetail: input.to.addressDetail,
toCoordinate: this.formatCoordinate(input.to.coordinate),
toMobile: input.to.mobile,
toName: input.to.name,
goodsName: input.goodsName,
goodsNum: input.goodsNum,
weight: input.weight,
insuredSumPrice: input.insuredSumPrice,
collectionPrice: input.collectionPrice,
payMode: input.payMode,
remark: input.remark,
});
async createShipment(input: CreateShipmentInput, options?: CourierCallOptions): Promise<CreateShipmentResult> {
const data = await this.client.request<XiaofeixiaCreateOrderData>(
XIAOFEIXIA_CMD.CREATE_ORDER,
{
customerId: input.customerId,
outNumber: input.outNumber,
fromAddress: input.from.address,
fromAddressDetail: input.from.addressDetail,
fromCoordinate: this.formatCoordinate(input.from.coordinate),
fromMobile: input.from.mobile,
fromName: input.from.name,
toAddress: input.to.address,
toAddressDetail: input.to.addressDetail,
toCoordinate: this.formatCoordinate(input.to.coordinate),
toMobile: input.to.mobile,
toName: input.to.name,
goodsName: input.goodsName,
goodsNum: input.goodsNum,
weight: input.weight,
insuredSumPrice: input.insuredSumPrice,
collectionPrice: input.collectionPrice,
payMode: input.payMode,
remark: input.remark,
},
options?.xiaofeixia,
);
return {
providerShipmentId: data.id,
@@ -58,24 +63,32 @@ export class XiaofeixiaProvider implements ICourierProvider {
};
}
async cancelShipment(query: ShipmentQuery): Promise<void> {
async cancelShipment(query: ShipmentQuery, options?: CourierCallOptions): Promise<void> {
this.assertShipmentQuery(query);
await this.client.request(XIAOFEIXIA_CMD.CANCEL_ORDER, {
number: query.trackingNumber,
outNumber: query.outNumber,
});
await this.client.request(
XIAOFEIXIA_CMD.CANCEL_ORDER,
{
number: query.trackingNumber,
outNumber: query.outNumber,
},
options?.xiaofeixia,
);
}
async getShipment(query: ShipmentQuery): Promise<ShipmentDetail> {
async getShipment(query: ShipmentQuery, options?: CourierCallOptions): Promise<ShipmentDetail> {
this.assertShipmentQuery(query);
const data = await this.client.request<XiaofeixiaOrderDetail>(XIAOFEIXIA_CMD.GET_ORDER, {
number: query.trackingNumber,
outNumber: query.outNumber,
});
const data = await this.client.request<XiaofeixiaOrderDetail>(
XIAOFEIXIA_CMD.GET_ORDER,
{
number: query.trackingNumber,
outNumber: query.outNumber,
},
options?.xiaofeixia,
);
return this.mapOrderDetail(data);
}
async batchGetShipments(query: BatchShipmentQuery): Promise<ShipmentDetail[]> {
async batchGetShipments(query: BatchShipmentQuery, options?: CourierCallOptions): Promise<ShipmentDetail[]> {
const number = query.trackingNumbers?.join(',');
const outNumber = query.outNumbers?.join(',');
@@ -83,27 +96,36 @@ export class XiaofeixiaProvider implements ICourierProvider {
throw new CourierApiError('运单号与商家单号至少传一个', '300000', this.code);
}
const data = await this.client.request<XiaofeixiaOrderDetail[]>(XIAOFEIXIA_CMD.BATCH_GET_ORDER, {
number,
outNumber,
});
const data = await this.client.request<XiaofeixiaOrderDetail[]>(
XIAOFEIXIA_CMD.BATCH_GET_ORDER,
{
number,
outNumber,
},
options?.xiaofeixia,
);
return (data ?? []).map((item) => this.mapOrderDetail(item));
}
async getTrack(query: ShipmentQuery): Promise<TrackNode[]> {
async getTrack(query: ShipmentQuery, options?: CourierCallOptions): Promise<TrackNode[]> {
this.assertShipmentQuery(query);
const data = await this.client.request<XiaofeixiaTrackNode[]>(XIAOFEIXIA_CMD.TRACK_ROUTE, {
number: query.trackingNumber,
outNumber: query.outNumber,
});
const data = await this.client.request<XiaofeixiaTrackNode[]>(
XIAOFEIXIA_CMD.TRACK_ROUTE,
{
number: query.trackingNumber,
outNumber: query.outNumber,
},
options?.xiaofeixia,
);
return data ?? [];
}
async checkDeliveryCoverage(toAddress: string): Promise<DeliveryCoverageResult> {
async checkDeliveryCoverage(toAddress: string, options?: CourierCallOptions): Promise<DeliveryCoverageResult> {
const data = await this.client.request<XiaofeixiaDeliveryCoverageData>(
XIAOFEIXIA_CMD.DELIVERY_COVERAGE,
{ toAddress },
options?.xiaofeixia,
);
return {
@@ -113,10 +135,11 @@ export class XiaofeixiaProvider implements ICourierProvider {
};
}
async estimateFreight(weight: number): Promise<FreightEstimateResult> {
async estimateFreight(weight: number, options?: CourierCallOptions): Promise<FreightEstimateResult> {
const data = await this.client.request<XiaofeixiaFreightEstimateData>(
XIAOFEIXIA_CMD.ESTIMATE_FREIGHT,
{ weight },
options?.xiaofeixia,
);
return { freightPrice: data.freightPrice };