小飞侠接口对接
This commit is contained in:
@@ -8,3 +8,13 @@ MOCK_SMS_CODE=123456
|
||||
MOCK_PAY=true
|
||||
MOCK_DELIVERY_AUTO=true
|
||||
AUTO_APPROVE_STORE=true
|
||||
|
||||
# 快递中间层(xiaofeixia | sf | jd,目前仅实现 xiaofeixia)
|
||||
COURIER_PROVIDER=xiaofeixia
|
||||
# 小飞侠同城快递
|
||||
XIAOFEIXIA_API_URL=https://beta.51xiaoju.cn/app/api/interface.do
|
||||
XIAOFEIXIA_APP_ID=
|
||||
XIAOFEIXIA_MCH_ID=
|
||||
XIAOFEIXIA_API_KEY=
|
||||
# 签名类型:MD5(默认)或 HMAC-SHA256
|
||||
XIAOFEIXIA_SIGN_TYPE=MD5
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CourierProviderCode } from './courier.constants';
|
||||
|
||||
export type XiaofeixiaSignType = 'MD5' | 'HMAC-SHA256';
|
||||
|
||||
export interface XiaofeixiaConfig {
|
||||
apiUrl: string;
|
||||
appId?: string;
|
||||
mchId: string;
|
||||
apiKey: string;
|
||||
signType: XiaofeixiaSignType;
|
||||
}
|
||||
|
||||
export interface CourierIntegrationConfig {
|
||||
provider: CourierProviderCode;
|
||||
xiaofeixia: XiaofeixiaConfig;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CourierConfigService {
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
load(): CourierIntegrationConfig {
|
||||
const providerRaw = (this.config.get<string>('COURIER_PROVIDER') ?? 'xiaofeixia').toLowerCase();
|
||||
const provider = this.resolveProvider(providerRaw);
|
||||
|
||||
return {
|
||||
provider,
|
||||
xiaofeixia: {
|
||||
apiUrl:
|
||||
this.config.get<string>('XIAOFEIXIA_API_URL') ??
|
||||
'https://beta.51xiaoju.cn/app/api/interface.do',
|
||||
appId: this.config.get<string>('XIAOFEIXIA_APP_ID') || undefined,
|
||||
mchId: this.config.get<string>('XIAOFEIXIA_MCH_ID') ?? '',
|
||||
apiKey: this.config.get<string>('XIAOFEIXIA_API_KEY') ?? '',
|
||||
signType: this.resolveSignType(this.config.get<string>('XIAOFEIXIA_SIGN_TYPE')),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private resolveProvider(raw: string): CourierProviderCode {
|
||||
switch (raw) {
|
||||
case 'xiaofeixia':
|
||||
return CourierProviderCode.XIAOFEIXIA;
|
||||
case 'sf':
|
||||
return CourierProviderCode.SF;
|
||||
case 'jd':
|
||||
return CourierProviderCode.JD;
|
||||
default:
|
||||
throw new Error(`Unsupported COURIER_PROVIDER: ${raw}`);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveSignType(raw?: string): XiaofeixiaSignType {
|
||||
if (raw?.toUpperCase() === 'HMAC-SHA256') {
|
||||
return 'HMAC-SHA256';
|
||||
}
|
||||
return 'MD5';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const COURIER_PROVIDER = 'COURIER_PROVIDER';
|
||||
|
||||
export enum CourierProviderCode {
|
||||
XIAOFEIXIA = 'XIAOFEIXIA',
|
||||
SF = 'SF',
|
||||
JD = 'JD',
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export class CourierApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly code: string,
|
||||
readonly providerCode?: string,
|
||||
readonly raw?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'CourierApiError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { COURIER_PROVIDER, CourierProviderCode } from './courier.constants';
|
||||
import { CourierConfigService } from './courier.config';
|
||||
import { CourierService } from './courier.service';
|
||||
import { XiaofeixiaClient } from './xiaofeixia/xiaofeixia.client';
|
||||
import { XiaofeixiaProvider } from './xiaofeixia/xiaofeixia.provider';
|
||||
import type { ICourierProvider } from './courier.types';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
CourierConfigService,
|
||||
XiaofeixiaClient,
|
||||
XiaofeixiaProvider,
|
||||
{
|
||||
provide: COURIER_PROVIDER,
|
||||
useFactory: (
|
||||
configService: CourierConfigService,
|
||||
xiaofeixia: XiaofeixiaProvider,
|
||||
): ICourierProvider => {
|
||||
const { provider } = configService.load();
|
||||
switch (provider) {
|
||||
case CourierProviderCode.XIAOFEIXIA:
|
||||
return xiaofeixia;
|
||||
default:
|
||||
throw new Error(`Courier provider not implemented: ${provider}`);
|
||||
}
|
||||
},
|
||||
inject: [CourierConfigService, XiaofeixiaProvider],
|
||||
},
|
||||
CourierService,
|
||||
],
|
||||
exports: [CourierService, COURIER_PROVIDER],
|
||||
})
|
||||
export class CourierModule {}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { COURIER_PROVIDER } from './courier.constants';
|
||||
import type {
|
||||
BatchShipmentQuery,
|
||||
CreateShipmentInput,
|
||||
CreateShipmentResult,
|
||||
DeliveryCoverageResult,
|
||||
FreightEstimateResult,
|
||||
ICourierProvider,
|
||||
ShipmentDetail,
|
||||
ShipmentQuery,
|
||||
TrackCallbackResponse,
|
||||
TrackNode,
|
||||
} from './courier.types';
|
||||
|
||||
/**
|
||||
* 快递统一门面:业务层只依赖本 Service,不感知具体快递商实现。
|
||||
*/
|
||||
@Injectable()
|
||||
export class CourierService {
|
||||
constructor(@Inject(COURIER_PROVIDER) private readonly provider: ICourierProvider) {}
|
||||
|
||||
get activeProvider() {
|
||||
return this.provider.code;
|
||||
}
|
||||
|
||||
createShipment(input: CreateShipmentInput): Promise<CreateShipmentResult> {
|
||||
return this.provider.createShipment(input);
|
||||
}
|
||||
|
||||
cancelShipment(query: ShipmentQuery): Promise<void> {
|
||||
return this.provider.cancelShipment(query);
|
||||
}
|
||||
|
||||
getShipment(query: ShipmentQuery): Promise<ShipmentDetail> {
|
||||
return this.provider.getShipment(query);
|
||||
}
|
||||
|
||||
batchGetShipments(query: BatchShipmentQuery): Promise<ShipmentDetail[]> {
|
||||
return this.provider.batchGetShipments(query);
|
||||
}
|
||||
|
||||
getTrack(query: ShipmentQuery): Promise<TrackNode[]> {
|
||||
return this.provider.getTrack(query);
|
||||
}
|
||||
|
||||
checkDeliveryCoverage(toAddress: string): Promise<DeliveryCoverageResult> {
|
||||
return this.provider.checkDeliveryCoverage(toAddress);
|
||||
}
|
||||
|
||||
estimateFreight(weight: number): Promise<FreightEstimateResult> {
|
||||
return this.provider.estimateFreight(weight);
|
||||
}
|
||||
|
||||
buildTrackCallbackResponse(success?: boolean): TrackCallbackResponse {
|
||||
return this.provider.buildTrackCallbackResponse(success);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { CourierProviderCode } from './courier.constants';
|
||||
|
||||
export interface CourierCoordinate {
|
||||
lng: number;
|
||||
lat: number;
|
||||
}
|
||||
|
||||
export interface CourierContact {
|
||||
name: string;
|
||||
mobile: string;
|
||||
address: string;
|
||||
addressDetail: string;
|
||||
coordinate?: CourierCoordinate;
|
||||
}
|
||||
|
||||
/** 付费对象:寄付 / 到付 */
|
||||
export enum CourierPayMode {
|
||||
SENDER = '1',
|
||||
RECEIVER = '2',
|
||||
}
|
||||
|
||||
export interface CreateShipmentInput {
|
||||
outNumber: string;
|
||||
customerId?: string;
|
||||
from: CourierContact;
|
||||
to: CourierContact;
|
||||
goodsName?: string;
|
||||
goodsNum?: number;
|
||||
weight?: number;
|
||||
insuredSumPrice?: number;
|
||||
collectionPrice?: number;
|
||||
payMode: CourierPayMode;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface CreateShipmentResult {
|
||||
providerShipmentId: number;
|
||||
trackingNumber: string;
|
||||
}
|
||||
|
||||
export interface ShipmentQuery {
|
||||
trackingNumber?: string;
|
||||
outNumber?: string;
|
||||
}
|
||||
|
||||
export interface BatchShipmentQuery {
|
||||
trackingNumbers?: string[];
|
||||
outNumbers?: string[];
|
||||
}
|
||||
|
||||
export interface TrackNode {
|
||||
trackInfo: string;
|
||||
createTime: string;
|
||||
statusName: string;
|
||||
}
|
||||
|
||||
export interface ShipmentDetail {
|
||||
trackingNumber: string;
|
||||
toCarrierName?: string;
|
||||
toSiteName?: string;
|
||||
toName: string;
|
||||
toMobile: string;
|
||||
toAddress: string;
|
||||
toAddressDetail?: string;
|
||||
fromCarrierName?: string;
|
||||
fromSiteName?: string;
|
||||
fromName: string;
|
||||
fromMobile: string;
|
||||
fromAddress: string;
|
||||
fromAddressDetail?: string;
|
||||
weight?: number;
|
||||
payModeName?: string;
|
||||
freightPrice?: string;
|
||||
insuredPrice?: number;
|
||||
collectionPrice?: number;
|
||||
sumPrice?: number;
|
||||
goodsName?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface DeliveryCoverageResult {
|
||||
arriveTime: string;
|
||||
siteName: string;
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
export interface FreightEstimateResult {
|
||||
freightPrice: number;
|
||||
}
|
||||
|
||||
/** 路由变化回调(各快递商 POST 到业务方) */
|
||||
export interface TrackCallbackPayload {
|
||||
outNumber: string;
|
||||
trackingNumber: string;
|
||||
status: string;
|
||||
statusName: string;
|
||||
trackInfo: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
export interface TrackCallbackResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
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>;
|
||||
buildTrackCallbackResponse(success?: boolean): TrackCallbackResponse;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './courier.constants';
|
||||
export * from './courier.types';
|
||||
export * from './courier.error';
|
||||
export * from './courier.service';
|
||||
export * from './courier.module';
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CourierApiError } from '../courier.error';
|
||||
import { CourierConfigService } from '../courier.config';
|
||||
import { buildXiaofeixiaSign } from './xiaofeixia.sign';
|
||||
import { XIAOFEIXIA_SUCCESS_CODE } from './xiaofeixia.constants';
|
||||
import type { XiaofeixiaApiResponse } from './xiaofeixia.types';
|
||||
|
||||
type RequestParams = Record<string, string | number | undefined>;
|
||||
|
||||
@Injectable()
|
||||
export class XiaofeixiaClient {
|
||||
constructor(private readonly courierConfig: CourierConfigService) {}
|
||||
|
||||
async request<T>(cmd: string, bizParams: RequestParams): Promise<T> {
|
||||
const cfg = this.courierConfig.load().xiaofeixia;
|
||||
|
||||
if (!cfg.mchId || !cfg.apiKey) {
|
||||
throw new CourierApiError(
|
||||
'小飞侠商户配置不完整,请设置 XIAOFEIXIA_MCH_ID 与 XIAOFEIXIA_API_KEY',
|
||||
'CONFIG_ERROR',
|
||||
'XIAOFEIXIA',
|
||||
);
|
||||
}
|
||||
|
||||
const baseParams: RequestParams = {
|
||||
mchId: cfg.mchId,
|
||||
cmd,
|
||||
signType: cfg.signType,
|
||||
...bizParams,
|
||||
};
|
||||
|
||||
if (cfg.appId) {
|
||||
baseParams.appId = cfg.appId;
|
||||
}
|
||||
|
||||
const sign = buildXiaofeixiaSign(baseParams, cfg.apiKey, cfg.signType);
|
||||
const body = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries({ ...baseParams, sign })) {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
body.append(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(cfg.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new CourierApiError(
|
||||
'小飞侠接口网络异常',
|
||||
'200000',
|
||||
'XIAOFEIXIA',
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new CourierApiError(
|
||||
`小飞侠 HTTP 请求失败: ${response.status}`,
|
||||
'200000',
|
||||
'XIAOFEIXIA',
|
||||
);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as XiaofeixiaApiResponse<T>;
|
||||
|
||||
if (payload.code !== XIAOFEIXIA_SUCCESS_CODE) {
|
||||
throw new CourierApiError(
|
||||
payload.message || '小飞侠接口业务失败',
|
||||
payload.code,
|
||||
'XIAOFEIXIA',
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
return payload.data as T;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/** 小飞侠接口 cmd 编号 */
|
||||
export const XIAOFEIXIA_CMD = {
|
||||
CREATE_ORDER: '100101',
|
||||
TRACK_ROUTE: '100102',
|
||||
CANCEL_ORDER: '100103',
|
||||
GET_ORDER: '100104',
|
||||
ESTIMATE_FREIGHT: '100105',
|
||||
BATCH_GET_ORDER: '100106',
|
||||
DELIVERY_COVERAGE: '100301',
|
||||
} as const;
|
||||
|
||||
export const XIAOFEIXIA_SUCCESS_CODE = '100000';
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CourierProviderCode } from '../courier.constants';
|
||||
import { CourierApiError } from '../courier.error';
|
||||
import type {
|
||||
BatchShipmentQuery,
|
||||
CreateShipmentInput,
|
||||
CreateShipmentResult,
|
||||
DeliveryCoverageResult,
|
||||
FreightEstimateResult,
|
||||
ICourierProvider,
|
||||
ShipmentDetail,
|
||||
ShipmentQuery,
|
||||
TrackCallbackResponse,
|
||||
TrackNode,
|
||||
} from '../courier.types';
|
||||
import { XiaofeixiaClient } from './xiaofeixia.client';
|
||||
import { XIAOFEIXIA_CMD } from './xiaofeixia.constants';
|
||||
import type {
|
||||
XiaofeixiaCreateOrderData,
|
||||
XiaofeixiaDeliveryCoverageData,
|
||||
XiaofeixiaFreightEstimateData,
|
||||
XiaofeixiaOrderDetail,
|
||||
XiaofeixiaTrackNode,
|
||||
} from './xiaofeixia.types';
|
||||
|
||||
@Injectable()
|
||||
export class XiaofeixiaProvider implements ICourierProvider {
|
||||
readonly code = CourierProviderCode.XIAOFEIXIA;
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
return {
|
||||
providerShipmentId: data.id,
|
||||
trackingNumber: data.number,
|
||||
};
|
||||
}
|
||||
|
||||
async cancelShipment(query: ShipmentQuery): Promise<void> {
|
||||
this.assertShipmentQuery(query);
|
||||
await this.client.request(XIAOFEIXIA_CMD.CANCEL_ORDER, {
|
||||
number: query.trackingNumber,
|
||||
outNumber: query.outNumber,
|
||||
});
|
||||
}
|
||||
|
||||
async getShipment(query: ShipmentQuery): Promise<ShipmentDetail> {
|
||||
this.assertShipmentQuery(query);
|
||||
const data = await this.client.request<XiaofeixiaOrderDetail>(XIAOFEIXIA_CMD.GET_ORDER, {
|
||||
number: query.trackingNumber,
|
||||
outNumber: query.outNumber,
|
||||
});
|
||||
return this.mapOrderDetail(data);
|
||||
}
|
||||
|
||||
async batchGetShipments(query: BatchShipmentQuery): Promise<ShipmentDetail[]> {
|
||||
const number = query.trackingNumbers?.join(',');
|
||||
const outNumber = query.outNumbers?.join(',');
|
||||
|
||||
if (!number && !outNumber) {
|
||||
throw new CourierApiError('运单号与商家单号至少传一个', '300000', this.code);
|
||||
}
|
||||
|
||||
const data = await this.client.request<XiaofeixiaOrderDetail[]>(XIAOFEIXIA_CMD.BATCH_GET_ORDER, {
|
||||
number,
|
||||
outNumber,
|
||||
});
|
||||
|
||||
return (data ?? []).map((item) => this.mapOrderDetail(item));
|
||||
}
|
||||
|
||||
async getTrack(query: ShipmentQuery): Promise<TrackNode[]> {
|
||||
this.assertShipmentQuery(query);
|
||||
const data = await this.client.request<XiaofeixiaTrackNode[]>(XIAOFEIXIA_CMD.TRACK_ROUTE, {
|
||||
number: query.trackingNumber,
|
||||
outNumber: query.outNumber,
|
||||
});
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
async checkDeliveryCoverage(toAddress: string): Promise<DeliveryCoverageResult> {
|
||||
const data = await this.client.request<XiaofeixiaDeliveryCoverageData>(
|
||||
XIAOFEIXIA_CMD.DELIVERY_COVERAGE,
|
||||
{ toAddress },
|
||||
);
|
||||
|
||||
return {
|
||||
arriveTime: data.arriveTime,
|
||||
siteName: data.name,
|
||||
siteId: data.id,
|
||||
};
|
||||
}
|
||||
|
||||
async estimateFreight(weight: number): Promise<FreightEstimateResult> {
|
||||
const data = await this.client.request<XiaofeixiaFreightEstimateData>(
|
||||
XIAOFEIXIA_CMD.ESTIMATE_FREIGHT,
|
||||
{ weight },
|
||||
);
|
||||
|
||||
return { freightPrice: data.freightPrice };
|
||||
}
|
||||
|
||||
buildTrackCallbackResponse(success = true): TrackCallbackResponse {
|
||||
return {
|
||||
code: success ? '100000' : '300000',
|
||||
message: success ? 'success' : 'fail',
|
||||
};
|
||||
}
|
||||
|
||||
private assertShipmentQuery(query: ShipmentQuery): void {
|
||||
if (!query.trackingNumber && !query.outNumber) {
|
||||
throw new CourierApiError('运单号与商家单号至少传一个', '300000', this.code);
|
||||
}
|
||||
}
|
||||
|
||||
private formatCoordinate(coordinate?: { lng: number; lat: number }): string | undefined {
|
||||
if (!coordinate) return undefined;
|
||||
return JSON.stringify({ lng: coordinate.lng, lat: coordinate.lat });
|
||||
}
|
||||
|
||||
private mapOrderDetail(data: XiaofeixiaOrderDetail): ShipmentDetail {
|
||||
return {
|
||||
trackingNumber: data.number,
|
||||
toCarrierName: data.toCarrierName,
|
||||
toSiteName: data.toSiteName,
|
||||
toName: data.toName,
|
||||
toMobile: data.toMobile,
|
||||
toAddress: data.toAddress,
|
||||
toAddressDetail: data.toAddressDetail,
|
||||
fromCarrierName: data.fromCarrierName,
|
||||
fromSiteName: data.fromSiteName,
|
||||
fromName: data.fromName,
|
||||
fromMobile: data.fromMobile,
|
||||
fromAddress: data.fromAddress,
|
||||
fromAddressDetail: data.fromAddressDetail,
|
||||
weight: data.weight,
|
||||
payModeName: data.payModeName,
|
||||
freightPrice: data.freightPrice,
|
||||
insuredPrice: data.insuredPrice,
|
||||
collectionPrice: data.collectionPrice,
|
||||
sumPrice: data.sumPrice,
|
||||
goodsName: data.goodsName,
|
||||
remark: data.remark,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createHash, createHmac } from 'crypto';
|
||||
import type { XiaofeixiaSignType } from '../courier.config';
|
||||
|
||||
type SignParams = Record<string, string | number | undefined | null>;
|
||||
|
||||
function isEmpty(value: unknown): boolean {
|
||||
return value === undefined || value === null || value === '';
|
||||
}
|
||||
|
||||
/** 按 ASCII 字典序拼接并生成签名 */
|
||||
export function buildXiaofeixiaSign(
|
||||
params: SignParams,
|
||||
apiKey: string,
|
||||
signType: XiaofeixiaSignType = 'MD5',
|
||||
): string {
|
||||
const sortedKeys = Object.keys(params)
|
||||
.filter((key) => key !== 'sign' && !isEmpty(params[key]))
|
||||
.sort();
|
||||
|
||||
const stringA = sortedKeys.map((key) => `${key}=${params[key]}`).join('&');
|
||||
const stringSignTemp = `${stringA}&key=${apiKey}`;
|
||||
|
||||
if (signType === 'HMAC-SHA256') {
|
||||
return createHmac('sha256', apiKey).update(stringSignTemp).digest('hex').toUpperCase();
|
||||
}
|
||||
|
||||
return createHash('md5').update(stringSignTemp).digest('hex').toUpperCase();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
export interface XiaofeixiaApiResponse<T> {
|
||||
cmd: string;
|
||||
code: string;
|
||||
message: string;
|
||||
bizCode?: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
export interface XiaofeixiaCreateOrderData {
|
||||
id: number;
|
||||
number: string;
|
||||
}
|
||||
|
||||
export interface XiaofeixiaTrackNode {
|
||||
trackInfo: string;
|
||||
createTime: string;
|
||||
statusName: string;
|
||||
}
|
||||
|
||||
export interface XiaofeixiaOrderDetail {
|
||||
number: string;
|
||||
toCarrierName?: string;
|
||||
toSiteName?: string;
|
||||
toName: string;
|
||||
toMobile: string;
|
||||
toAddress: string;
|
||||
toAddressDetail?: string;
|
||||
fromCarrierName?: string;
|
||||
fromSiteName?: string;
|
||||
fromName: string;
|
||||
fromMobile: string;
|
||||
fromAddress: string;
|
||||
fromAddressDetail?: string;
|
||||
weight?: number;
|
||||
payModeName?: string;
|
||||
freightPrice?: string;
|
||||
insuredPrice?: number;
|
||||
collectionPrice?: number;
|
||||
sumPrice?: number;
|
||||
goodsName?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface XiaofeixiaDeliveryCoverageData {
|
||||
arriveTime: string;
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface XiaofeixiaFreightEstimateData {
|
||||
freightPrice: number;
|
||||
}
|
||||
|
||||
export interface XiaofeixiaCancelOrderData {
|
||||
id: number;
|
||||
number: string;
|
||||
}
|
||||
@@ -3,11 +3,12 @@ import { BullModule } from '@nestjs/bullmq';
|
||||
import { SmsMockProvider } from './sms/sms.mock.provider';
|
||||
import { PayMockProvider } from './pay/pay.mock.provider';
|
||||
import { DeliveryMockProvider } from './delivery/delivery.mock.provider';
|
||||
import { CourierModule } from './courier/courier.module';
|
||||
import { SMS_PROVIDER, PAY_PROVIDER, DELIVERY_PROVIDER } from './integrations.constants';
|
||||
import { DELIVERY_QUEUE } from '../jobs/jobs.constants';
|
||||
|
||||
@Module({
|
||||
imports: [BullModule.registerQueue({ name: DELIVERY_QUEUE })],
|
||||
imports: [BullModule.registerQueue({ name: DELIVERY_QUEUE }), CourierModule],
|
||||
providers: [
|
||||
{ provide: SMS_PROVIDER, useClass: SmsMockProvider },
|
||||
{ provide: PAY_PROVIDER, useClass: PayMockProvider },
|
||||
@@ -16,6 +17,6 @@ import { DELIVERY_QUEUE } from '../jobs/jobs.constants';
|
||||
PayMockProvider,
|
||||
DeliveryMockProvider,
|
||||
],
|
||||
exports: [SMS_PROVIDER, PAY_PROVIDER, DELIVERY_PROVIDER],
|
||||
exports: [SMS_PROVIDER, PAY_PROVIDER, DELIVERY_PROVIDER, CourierModule],
|
||||
})
|
||||
export class IntegrationsModule {}
|
||||
|
||||
Reference in New Issue
Block a user