@@ -189,6 +189,12 @@ async function main() {
|
||||
callback: true,
|
||||
cancel: true,
|
||||
}),
|
||||
configJson: JSON.stringify({
|
||||
apiUrl: 'https://beta.51xiaoju.cn/app/api/interface.do',
|
||||
mchId: '',
|
||||
apiKey: '',
|
||||
signType: 'MD5',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ export const SYSTEM_CONFIG_GROUPS: SystemConfigGroupMeta[] = [
|
||||
{ key: 'sms', label: '短信' },
|
||||
{ key: 'wechat', label: '微信' },
|
||||
{ key: 'oss', label: '对象存储 OSS' },
|
||||
{ key: 'courier', label: '同城配送' },
|
||||
{ key: 'app', label: '应用链接' },
|
||||
{ key: 'deploy', label: '发布部署' },
|
||||
];
|
||||
@@ -16,7 +15,6 @@ const G = {
|
||||
sms: 'sms',
|
||||
wechat: 'wechat',
|
||||
oss: 'oss',
|
||||
courier: 'courier',
|
||||
app: 'app',
|
||||
deploy: 'deploy',
|
||||
} as const;
|
||||
@@ -79,19 +77,6 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
|
||||
{ key: 'OSS_UPLOAD_EXPIRE_SECONDS', label: '直传凭证有效期(秒)', group: G.oss, type: 'number', requiresRestart: false },
|
||||
{ key: 'OSS_MAX_UPLOAD_BYTES', label: '单文件上限(字节)', group: G.oss, type: 'number', requiresRestart: false },
|
||||
|
||||
{ key: 'COURIER_PROVIDER', label: '配送服务商', group: G.courier, type: 'string', requiresRestart: true, placeholder: 'xiaofeixia' },
|
||||
{ key: 'XIAOFEIXIA_API_URL', label: '小飞侠 API 地址', group: G.courier, type: 'string', requiresRestart: false },
|
||||
{ key: 'XIAOFEIXIA_MCH_ID', label: '小飞侠商户号', group: G.courier, type: 'string', requiresRestart: false },
|
||||
{ key: 'XIAOFEIXIA_API_KEY', label: '小飞侠 API Key', group: G.courier, type: 'password', secret: true, requiresRestart: false },
|
||||
{ key: 'XIAOFEIXIA_SIGN_TYPE', label: '小飞侠签名类型', group: G.courier, type: 'string', requiresRestart: false, placeholder: 'MD5' },
|
||||
{ key: 'XIAOFEIXIA_APP_ID', label: '小飞侠 AppID', group: G.courier, type: 'string', requiresRestart: false },
|
||||
{ key: 'SHIP_FROM_NAME', label: '默认寄件人', group: G.courier, type: 'string', requiresRestart: false },
|
||||
{ key: 'SHIP_FROM_MOBILE', label: '默认寄件手机', group: G.courier, type: 'string', requiresRestart: false },
|
||||
{ key: 'SHIP_FROM_ADDRESS', label: '默认寄件地址', group: G.courier, type: 'string', requiresRestart: false },
|
||||
{ key: 'SHIP_FROM_ADDRESS_DETAIL', label: '默认寄件门牌', group: G.courier, type: 'string', requiresRestart: false },
|
||||
{ key: 'SHIP_FROM_LNG', label: '默认寄件经度', group: G.courier, type: 'string', requiresRestart: false },
|
||||
{ key: 'SHIP_FROM_LAT', label: '默认寄件纬度', group: G.courier, type: 'string', requiresRestart: false },
|
||||
|
||||
{ key: 'USER_H5_URL', label: 'C 端 H5 落地页', group: G.app, type: 'string', requiresRestart: false },
|
||||
{ key: 'TENCENT_LBS_KEY', label: '腾讯位置服务 Key', group: G.app, type: 'password', secret: true, requiresRestart: false },
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { FulfillmentProviderStatus, FulfillmentProviderType } from '@prisma/client';
|
||||
import {
|
||||
isXfxProviderCode,
|
||||
type XiaofeixiaProviderConfig,
|
||||
type XiaofeixiaProviderConfigPublic,
|
||||
} from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { XiaofeixiaConfig, XiaofeixiaSignType } from '../../integrations/courier/courier.config';
|
||||
|
||||
export type CreateFulfillmentProviderInput = {
|
||||
code: string;
|
||||
@@ -10,6 +16,7 @@ export type CreateFulfillmentProviderInput = {
|
||||
status?: FulfillmentProviderStatus;
|
||||
configJson?: string;
|
||||
capabilitiesJson?: string;
|
||||
xiaofeixiaConfig?: Partial<XiaofeixiaProviderConfig>;
|
||||
};
|
||||
|
||||
export type UpdateFulfillmentProviderInput = Partial<CreateFulfillmentProviderInput>;
|
||||
@@ -21,6 +28,8 @@ type Capabilities = {
|
||||
cancel?: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_XFX_API_URL = 'https://beta.51xiaoju.cn/app/api/interface.do';
|
||||
|
||||
@Injectable()
|
||||
export class FulfillmentProviderService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
@@ -46,6 +55,44 @@ export class FulfillmentProviderService {
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
/** 供推单使用:解析完整小飞侠凭证(含 apiKey) */
|
||||
async resolveXiaofeixiaConfig(providerId: bigint): Promise<XiaofeixiaConfig> {
|
||||
const row = await this.prisma.fulfillmentProvider.findUnique({ where: { id: providerId } });
|
||||
if (!row) throw new NotFoundException('仓配承运商不存在');
|
||||
if (!isXfxProviderCode(row.code)) {
|
||||
throw new BadRequestException('该承运商不是小飞侠');
|
||||
}
|
||||
const cfg = this.parseXiaofeixiaConfig(row.configJson);
|
||||
if (!cfg?.mchId || !cfg?.apiKey || !cfg?.apiUrl) {
|
||||
throw new BadRequestException('小飞侠仓配配置不完整,请在仓配管理中填写 API 地址、商户号与 API Key');
|
||||
}
|
||||
return {
|
||||
apiUrl: cfg.apiUrl,
|
||||
mchId: cfg.mchId,
|
||||
apiKey: cfg.apiKey,
|
||||
signType: this.resolveSignType(cfg.signType),
|
||||
appId: cfg.appId || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 取第一个启用的小飞侠承运商配置(联调/兼容) */
|
||||
async resolveDefaultXiaofeixiaConfig(): Promise<XiaofeixiaConfig | null> {
|
||||
const row = await this.prisma.fulfillmentProvider.findFirst({
|
||||
where: {
|
||||
status: 'ACTIVE',
|
||||
type: 'API',
|
||||
code: { in: ['XFX', 'XIAOFEIXIA'] },
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
if (!row?.configJson) return null;
|
||||
try {
|
||||
return await this.resolveXiaofeixiaConfig(row.id);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async create(input: CreateFulfillmentProviderInput) {
|
||||
const code = input.code.trim().toUpperCase();
|
||||
if (!/^[A-Z0-9_]+$/.test(code)) {
|
||||
@@ -54,28 +101,54 @@ export class FulfillmentProviderService {
|
||||
const existing = await this.prisma.fulfillmentProvider.findUnique({ where: { code } });
|
||||
if (existing) throw new BadRequestException('承运商编码已存在');
|
||||
|
||||
const configJson = this.resolveConfigJsonForWrite(code, null, input);
|
||||
if (isXfxProviderCode(code) && input.type === 'API') {
|
||||
this.assertXiaofeixiaConfigComplete(configJson, true);
|
||||
}
|
||||
|
||||
const row = await this.prisma.fulfillmentProvider.create({
|
||||
data: {
|
||||
code,
|
||||
name: input.name.trim(),
|
||||
type: input.type,
|
||||
status: input.status ?? 'ACTIVE',
|
||||
configJson: input.configJson?.trim() || null,
|
||||
capabilitiesJson: input.capabilitiesJson?.trim() || null,
|
||||
configJson,
|
||||
capabilitiesJson:
|
||||
input.capabilitiesJson?.trim() ||
|
||||
(isXfxProviderCode(code)
|
||||
? JSON.stringify({
|
||||
createShipment: true,
|
||||
getTrack: true,
|
||||
callback: true,
|
||||
cancel: true,
|
||||
})
|
||||
: null),
|
||||
},
|
||||
});
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async update(id: bigint, input: UpdateFulfillmentProviderInput) {
|
||||
await this.getById(id);
|
||||
const current = await this.prisma.fulfillmentProvider.findUnique({ where: { id } });
|
||||
if (!current) throw new NotFoundException('仓配承运商不存在');
|
||||
|
||||
const code = current.code;
|
||||
const configJson =
|
||||
input.xiaofeixiaConfig !== undefined || input.configJson !== undefined
|
||||
? this.resolveConfigJsonForWrite(code, current.configJson, input)
|
||||
: undefined;
|
||||
|
||||
if (configJson !== undefined && isXfxProviderCode(code) && (input.type ?? current.type) === 'API') {
|
||||
this.assertXiaofeixiaConfigComplete(configJson, false);
|
||||
}
|
||||
|
||||
const row = await this.prisma.fulfillmentProvider.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(input.name !== undefined ? { name: input.name.trim() } : {}),
|
||||
...(input.type !== undefined ? { type: input.type } : {}),
|
||||
...(input.status !== undefined ? { status: input.status } : {}),
|
||||
...(input.configJson !== undefined ? { configJson: input.configJson?.trim() || null } : {}),
|
||||
...(configJson !== undefined ? { configJson } : {}),
|
||||
...(input.capabilitiesJson !== undefined
|
||||
? { capabilitiesJson: input.capabilitiesJson?.trim() || null }
|
||||
: {}),
|
||||
@@ -93,6 +166,74 @@ export class FulfillmentProviderService {
|
||||
}
|
||||
}
|
||||
|
||||
parseXiaofeixiaConfig(raw: string | null): XiaofeixiaProviderConfig | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<XiaofeixiaProviderConfig>;
|
||||
if (!parsed || typeof parsed !== 'object') return null;
|
||||
return {
|
||||
apiUrl: String(parsed.apiUrl ?? '').trim(),
|
||||
mchId: String(parsed.mchId ?? '').trim(),
|
||||
apiKey: String(parsed.apiKey ?? '').trim(),
|
||||
signType: parsed.signType === 'HMAC-SHA256' ? 'HMAC-SHA256' : 'MD5',
|
||||
appId: parsed.appId ? String(parsed.appId).trim() : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private resolveConfigJsonForWrite(
|
||||
code: string,
|
||||
existingRaw: string | null,
|
||||
input: CreateFulfillmentProviderInput | UpdateFulfillmentProviderInput,
|
||||
): string | null {
|
||||
if (isXfxProviderCode(code) && input.xiaofeixiaConfig) {
|
||||
const existing = this.parseXiaofeixiaConfig(existingRaw);
|
||||
const next: XiaofeixiaProviderConfig = {
|
||||
apiUrl: (input.xiaofeixiaConfig.apiUrl ?? existing?.apiUrl ?? DEFAULT_XFX_API_URL).trim(),
|
||||
mchId: (input.xiaofeixiaConfig.mchId ?? existing?.mchId ?? '').trim(),
|
||||
apiKey: (input.xiaofeixiaConfig.apiKey || existing?.apiKey || '').trim(),
|
||||
signType:
|
||||
input.xiaofeixiaConfig.signType === 'HMAC-SHA256'
|
||||
? 'HMAC-SHA256'
|
||||
: input.xiaofeixiaConfig.signType === 'MD5'
|
||||
? 'MD5'
|
||||
: existing?.signType ?? 'MD5',
|
||||
appId: (input.xiaofeixiaConfig.appId ?? existing?.appId)?.trim() || undefined,
|
||||
};
|
||||
return JSON.stringify(next);
|
||||
}
|
||||
if (input.configJson !== undefined) {
|
||||
return input.configJson?.trim() || null;
|
||||
}
|
||||
return existingRaw;
|
||||
}
|
||||
|
||||
private assertXiaofeixiaConfigComplete(configJson: string | null, requireApiKey: boolean) {
|
||||
const cfg = this.parseXiaofeixiaConfig(configJson);
|
||||
if (!cfg?.apiUrl) throw new BadRequestException('请填写小飞侠 API 地址');
|
||||
if (!cfg.mchId) throw new BadRequestException('请填写小飞侠商户号');
|
||||
if (requireApiKey && !cfg.apiKey) throw new BadRequestException('请填写小飞侠 API Key');
|
||||
if (!requireApiKey && !cfg.apiKey) throw new BadRequestException('小飞侠 API Key 缺失,请重新填写');
|
||||
}
|
||||
|
||||
private toPublicXiaofeixiaConfig(raw: string | null): XiaofeixiaProviderConfigPublic | null {
|
||||
const cfg = this.parseXiaofeixiaConfig(raw);
|
||||
if (!cfg) return null;
|
||||
return {
|
||||
apiUrl: cfg.apiUrl || DEFAULT_XFX_API_URL,
|
||||
mchId: cfg.mchId,
|
||||
signType: cfg.signType === 'HMAC-SHA256' ? 'HMAC-SHA256' : 'MD5',
|
||||
appId: cfg.appId,
|
||||
hasApiKey: Boolean(cfg.apiKey),
|
||||
};
|
||||
}
|
||||
|
||||
private resolveSignType(raw?: string): XiaofeixiaSignType {
|
||||
return raw?.toUpperCase() === 'HMAC-SHA256' ? 'HMAC-SHA256' : 'MD5';
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: bigint;
|
||||
code: string;
|
||||
@@ -112,6 +253,9 @@ export class FulfillmentProviderService {
|
||||
status: row.status,
|
||||
capabilities: this.parseCapabilities(row.capabilitiesJson),
|
||||
hasConfig: Boolean(row.configJson),
|
||||
xiaofeixiaConfig: isXfxProviderCode(row.code)
|
||||
? this.toPublicXiaofeixiaConfig(row.configJson)
|
||||
: null,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException, Inject, forwardRef } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { CityWarehouse, FulfillmentProvider, Order } from '@prisma/client';
|
||||
import { isXfxProviderCode } from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { CourierService } from '../../integrations/courier/courier.service';
|
||||
import { CourierPayMode } from '../../integrations/courier/courier.types';
|
||||
import type { XiaofeixiaConfig } from '../../integrations/courier/courier.config';
|
||||
import { TradeService } from '../trade/trade.service';
|
||||
import { FulfillmentProviderService } from './fulfillment-provider.service';
|
||||
|
||||
export type ManualShipInput = {
|
||||
logisticsCompany: string;
|
||||
@@ -14,16 +16,14 @@ export type ManualShipInput = {
|
||||
|
||||
export type HqLogisticsShipInput = ManualShipInput;
|
||||
|
||||
const XFX_CODES = new Set(['XFX', 'XIAOFEIXIA']);
|
||||
|
||||
@Injectable()
|
||||
export class FulfillmentService {
|
||||
private readonly logger = new Logger(FulfillmentService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly courier: CourierService,
|
||||
private readonly fulfillmentProviderService: FulfillmentProviderService,
|
||||
@Inject(forwardRef(() => TradeService))
|
||||
private readonly tradeService: TradeService,
|
||||
) {}
|
||||
@@ -69,38 +69,50 @@ export class FulfillmentService {
|
||||
}
|
||||
|
||||
async dispatchApiAuto(order: Order, warehouse: CityWarehouse, provider: FulfillmentProvider) {
|
||||
if (!XFX_CODES.has(provider.code)) {
|
||||
if (!isXfxProviderCode(provider.code)) {
|
||||
this.logger.warn(`承运商 ${provider.code} 自动推单尚未实现,订单 ${order.orderNo} 保持待发货`);
|
||||
await this.ensureDeliveryRecord(order.id, 'MANUAL', provider.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const defaults = this.getShipDefaults();
|
||||
const fromLng = warehouse.lng != null ? Number(warehouse.lng) : defaults.fromLng;
|
||||
const fromLat = warehouse.lat != null ? Number(warehouse.lat) : defaults.fromLat;
|
||||
let xfxConfig: XiaofeixiaConfig;
|
||||
try {
|
||||
xfxConfig = await this.fulfillmentProviderService.resolveXiaofeixiaConfig(provider.id);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await this.logDispatchFailure(order, provider, message);
|
||||
await this.ensureDeliveryRecord(order.id, 'MANUAL', provider.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const fromLng = warehouse.lng != null ? Number(warehouse.lng) : 113.665;
|
||||
const fromLat = warehouse.lat != null ? Number(warehouse.lat) : 34.757;
|
||||
|
||||
try {
|
||||
const result = await this.courier.createShipment({
|
||||
outNumber: order.orderNo,
|
||||
from: {
|
||||
name: warehouse.contactName,
|
||||
mobile: warehouse.contactPhone,
|
||||
address: warehouse.address,
|
||||
addressDetail: warehouse.name,
|
||||
coordinate: { lng: fromLng, lat: fromLat },
|
||||
const result = await this.courier.createShipment(
|
||||
{
|
||||
outNumber: order.orderNo,
|
||||
from: {
|
||||
name: warehouse.contactName,
|
||||
mobile: warehouse.contactPhone,
|
||||
address: warehouse.address,
|
||||
addressDetail: warehouse.name,
|
||||
coordinate: { lng: fromLng, lat: fromLat },
|
||||
},
|
||||
to: {
|
||||
name: order.receiverName,
|
||||
mobile: order.receiverPhone,
|
||||
address: `${order.receiverProvince}${order.receiverCity}${order.receiverDistrict}`,
|
||||
addressDetail: order.receiverAddress,
|
||||
},
|
||||
goodsName: order.productName,
|
||||
goodsNum: order.quantity,
|
||||
weight: 2,
|
||||
payMode: CourierPayMode.SENDER,
|
||||
remark: `仓配自动发货 ${order.orderNo}`,
|
||||
},
|
||||
to: {
|
||||
name: order.receiverName,
|
||||
mobile: order.receiverPhone,
|
||||
address: `${order.receiverProvince}${order.receiverCity}${order.receiverDistrict}`,
|
||||
addressDetail: order.receiverAddress,
|
||||
},
|
||||
goodsName: order.productName,
|
||||
goodsNum: order.quantity,
|
||||
weight: defaults.weight,
|
||||
payMode: defaults.payMode,
|
||||
remark: `仓配自动发货 ${order.orderNo}`,
|
||||
});
|
||||
{ xiaofeixia: xfxConfig },
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
@@ -196,10 +208,20 @@ export class FulfillmentService {
|
||||
|
||||
if (order.delivery.provider === 'XFX' && (order.delivery.trackingNo || order.orderNo)) {
|
||||
try {
|
||||
const nodes = await this.courier.getTrack({
|
||||
trackingNumber: order.delivery.trackingNo ?? undefined,
|
||||
outNumber: order.orderNo,
|
||||
});
|
||||
const options = order.delivery.fulfillmentProviderId
|
||||
? {
|
||||
xiaofeixia: await this.fulfillmentProviderService.resolveXiaofeixiaConfig(
|
||||
order.delivery.fulfillmentProviderId,
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
const nodes = await this.courier.getTrack(
|
||||
{
|
||||
trackingNumber: order.delivery.trackingNo ?? undefined,
|
||||
outNumber: order.orderNo,
|
||||
},
|
||||
options,
|
||||
);
|
||||
return {
|
||||
nodes,
|
||||
manualQueryUrl: order.delivery.manualQueryUrl,
|
||||
@@ -208,7 +230,7 @@ export class FulfillmentService {
|
||||
logisticsCompany: order.delivery.logisticsCompany,
|
||||
};
|
||||
} catch {
|
||||
// fall through to manual fields
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,13 +316,4 @@ export class FulfillmentService {
|
||||
if (!tpl) return undefined;
|
||||
return tpl.replace(/\{trackingNo\}/g, encodeURIComponent(trackingNo));
|
||||
}
|
||||
|
||||
private getShipDefaults() {
|
||||
return {
|
||||
fromLng: Number(this.config.get<string>('SHIP_FROM_LNG') || 113.665),
|
||||
fromLat: Number(this.config.get<string>('SHIP_FROM_LAT') || 34.757),
|
||||
weight: 2,
|
||||
payMode: CourierPayMode.SENDER,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,15 @@ export class AdminFulfillmentProvidersController {
|
||||
status: dto.status as FulfillmentProviderStatus | undefined,
|
||||
configJson: dto.configJson,
|
||||
capabilitiesJson: dto.capabilitiesJson,
|
||||
xiaofeixiaConfig: dto.xiaofeixiaConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.getById(BigInt(id));
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WAREHOUSE_UPDATE,
|
||||
@@ -56,6 +62,7 @@ export class AdminFulfillmentProvidersController {
|
||||
status: dto.status as FulfillmentProviderStatus | undefined,
|
||||
configJson: dto.configJson,
|
||||
capabilitiesJson: dto.capabilitiesJson,
|
||||
xiaofeixiaConfig: dto.xiaofeixiaConfig,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
@@ -11,6 +10,7 @@ import type { AdminOrdersQueryDto } from './dto/admin-query.dto';
|
||||
import type { AdminShipOrderDto, HqLogisticsShipDto } from './dto/admin-mutate.dto';
|
||||
import type { XiaofeixiaCreateShipmentDto } from './dto/admin-courier.dto';
|
||||
import { FulfillmentService } from '../fulfillment/fulfillment.service';
|
||||
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
||||
|
||||
@Injectable()
|
||||
export class AdminOrdersService {
|
||||
@@ -19,7 +19,7 @@ export class AdminOrdersService {
|
||||
private readonly tradeService: TradeService,
|
||||
private readonly xiaofeixiaService: AdminXiaofeixiaService,
|
||||
private readonly fulfillmentService: FulfillmentService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly fulfillmentProviderService: FulfillmentProviderService,
|
||||
) {}
|
||||
|
||||
async list(query: AdminOrdersQueryDto) {
|
||||
@@ -96,21 +96,6 @@ export class AdminOrdersService {
|
||||
return this.detail(id);
|
||||
}
|
||||
|
||||
getShipDefaults() {
|
||||
return {
|
||||
provider: 'XFX',
|
||||
providerLabel: '小飞侠',
|
||||
fromName: this.config.get<string>('SHIP_FROM_NAME') || '杜康仓库',
|
||||
fromMobile: this.config.get<string>('SHIP_FROM_MOBILE') || '13800000000',
|
||||
fromAddress: this.config.get<string>('SHIP_FROM_ADDRESS') || '河南省郑州市金水区',
|
||||
fromAddressDetail: this.config.get<string>('SHIP_FROM_ADDRESS_DETAIL') || '杜康酒业仓',
|
||||
fromLng: Number(this.config.get<string>('SHIP_FROM_LNG') || 113.665),
|
||||
fromLat: Number(this.config.get<string>('SHIP_FROM_LAT') || 34.757),
|
||||
weight: 2,
|
||||
payMode: '1',
|
||||
};
|
||||
}
|
||||
|
||||
async shipOrder(id: bigint, dto: AdminShipOrderDto) {
|
||||
if (dto.provider !== 'XFX') {
|
||||
throw new BadRequestException('暂仅支持小飞侠配送');
|
||||
@@ -118,7 +103,10 @@ export class AdminOrdersService {
|
||||
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id },
|
||||
include: { delivery: true },
|
||||
include: {
|
||||
delivery: true,
|
||||
fulfillmentWarehouse: true,
|
||||
},
|
||||
});
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
|
||||
@@ -129,7 +117,23 @@ export class AdminOrdersService {
|
||||
throw new BadRequestException('该订单已有运单号,请勿重复发货');
|
||||
}
|
||||
|
||||
const defaults = this.getShipDefaults();
|
||||
const warehouse = order.fulfillmentWarehouse;
|
||||
const providerId =
|
||||
order.delivery?.fulfillmentProviderId ??
|
||||
warehouse?.fulfillmentProviderId ??
|
||||
null;
|
||||
|
||||
let xfxConfig;
|
||||
if (providerId) {
|
||||
xfxConfig = await this.fulfillmentProviderService.resolveXiaofeixiaConfig(providerId);
|
||||
} else {
|
||||
xfxConfig = await this.fulfillmentProviderService.resolveDefaultXiaofeixiaConfig();
|
||||
if (!xfxConfig) {
|
||||
throw new BadRequestException('请先在仓配管理中注册并配置小飞侠承运商');
|
||||
}
|
||||
}
|
||||
|
||||
const defaults = this.getShipDefaults(warehouse);
|
||||
const shipmentDto: XiaofeixiaCreateShipmentDto = {
|
||||
outNumber: order.orderNo,
|
||||
fromName: dto.fromName || defaults.fromName,
|
||||
@@ -149,13 +153,14 @@ export class AdminOrdersService {
|
||||
remark: dto.remark || `HQ发货 ${order.orderNo}`,
|
||||
};
|
||||
|
||||
const result = await this.xiaofeixiaService.createShipment(shipmentDto);
|
||||
const result = await this.xiaofeixiaService.createShipment(shipmentDto, xfxConfig);
|
||||
if (!result.ok || !result.data) {
|
||||
throw new BadRequestException(result.error || '小飞侠创建运单失败');
|
||||
}
|
||||
|
||||
const { providerShipmentId, trackingNumber } = result.data;
|
||||
const now = new Date();
|
||||
const resolvedProviderId = providerId;
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
if (order.delivery) {
|
||||
@@ -163,6 +168,7 @@ export class AdminOrdersService {
|
||||
where: { orderId: id },
|
||||
data: {
|
||||
provider: 'XFX',
|
||||
fulfillmentProviderId: resolvedProviderId,
|
||||
trackingNo: trackingNumber,
|
||||
providerOrderNo: String(providerShipmentId),
|
||||
shippingAt: now,
|
||||
@@ -173,6 +179,7 @@ export class AdminOrdersService {
|
||||
data: {
|
||||
orderId: id,
|
||||
provider: 'XFX',
|
||||
fulfillmentProviderId: resolvedProviderId,
|
||||
trackingNo: trackingNumber,
|
||||
providerOrderNo: String(providerShipmentId),
|
||||
shippingAt: now,
|
||||
@@ -185,6 +192,40 @@ export class AdminOrdersService {
|
||||
return this.detail(id);
|
||||
}
|
||||
|
||||
getShipDefaults(warehouse?: {
|
||||
contactName: string;
|
||||
contactPhone: string;
|
||||
address: string;
|
||||
name: string;
|
||||
lng: { toNumber?: () => number } | number | null;
|
||||
lat: { toNumber?: () => number } | number | null;
|
||||
} | null) {
|
||||
const lng =
|
||||
warehouse?.lng != null
|
||||
? typeof warehouse.lng === 'object' && warehouse.lng && 'toNumber' in warehouse.lng
|
||||
? Number(warehouse.lng)
|
||||
: Number(warehouse.lng)
|
||||
: 113.665;
|
||||
const lat =
|
||||
warehouse?.lat != null
|
||||
? typeof warehouse.lat === 'object' && warehouse.lat && 'toNumber' in warehouse.lat
|
||||
? Number(warehouse.lat)
|
||||
: Number(warehouse.lat)
|
||||
: 34.757;
|
||||
return {
|
||||
provider: 'XFX',
|
||||
providerLabel: '小飞侠',
|
||||
fromName: warehouse?.contactName || '杜康仓库',
|
||||
fromMobile: warehouse?.contactPhone || '13800000000',
|
||||
fromAddress: warehouse?.address || '河南省郑州市金水区',
|
||||
fromAddressDetail: warehouse?.name || '杜康酒业仓',
|
||||
fromLng: lng,
|
||||
fromLat: lat,
|
||||
weight: 2,
|
||||
payMode: '1',
|
||||
};
|
||||
}
|
||||
|
||||
/** 总部传统快递填单(同城无仓 / 跨城) */
|
||||
async shipLogistics(id: bigint, dto: HqLogisticsShipDto) {
|
||||
await this.fulfillmentService.shipHqLogistics(id, dto);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { CourierConfigService } from '../../integrations/courier/courier.config';
|
||||
import type { XiaofeixiaConfig } from '../../integrations/courier/courier.config';
|
||||
import { CourierApiError } from '../../integrations/courier/courier.error';
|
||||
import { CourierService } from '../../integrations/courier/courier.service';
|
||||
import { CourierPayMode } from '../../integrations/courier/courier.types';
|
||||
@@ -8,6 +9,7 @@ import type {
|
||||
CreateShipmentInput,
|
||||
ShipmentQuery,
|
||||
} from '../../integrations/courier/courier.types';
|
||||
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
||||
import type {
|
||||
XiaofeixiaBatchShipmentQueryDto,
|
||||
XiaofeixiaCheckCoverageDto,
|
||||
@@ -27,48 +29,61 @@ export class AdminXiaofeixiaService {
|
||||
constructor(
|
||||
private readonly courier: CourierService,
|
||||
private readonly courierConfig: CourierConfigService,
|
||||
private readonly fulfillmentProviderService: FulfillmentProviderService,
|
||||
) {}
|
||||
|
||||
getConfig() {
|
||||
const cfg = this.courierConfig.load();
|
||||
const xfx = cfg.xiaofeixia;
|
||||
async getConfig() {
|
||||
const fromDb = await this.fulfillmentProviderService.resolveDefaultXiaofeixiaConfig();
|
||||
const envCfg = this.courierConfig.load().xiaofeixia;
|
||||
const xfx = fromDb ?? envCfg;
|
||||
return {
|
||||
provider: cfg.provider,
|
||||
provider: this.courierConfig.load().provider,
|
||||
activeProvider: this.courier.activeProvider,
|
||||
source: fromDb ? 'fulfillment_provider' : 'env',
|
||||
apiUrl: xfx.apiUrl,
|
||||
appId: xfx.appId ?? null,
|
||||
mchId: xfx.mchId || null,
|
||||
mchIdMasked: xfx.mchId ? maskSecret(xfx.mchId) : null,
|
||||
hasApiKey: Boolean(xfx.apiKey),
|
||||
signType: xfx.signType,
|
||||
ready: Boolean(xfx.mchId && xfx.apiKey),
|
||||
ready: Boolean(xfx.mchId && xfx.apiKey && xfx.apiUrl),
|
||||
hint: fromDb
|
||||
? '凭证来自仓配管理中启用的小飞侠承运商'
|
||||
: '未在仓配管理配置,回退到环境变量(请迁移至仓配管理)',
|
||||
};
|
||||
}
|
||||
|
||||
async estimateFreight(dto: XiaofeixiaEstimateFreightDto) {
|
||||
return this.wrap(() => this.courier.estimateFreight(dto.weight));
|
||||
const options = await this.callOptions();
|
||||
return this.wrap(() => this.courier.estimateFreight(dto.weight, options));
|
||||
}
|
||||
|
||||
async checkCoverage(dto: XiaofeixiaCheckCoverageDto) {
|
||||
return this.wrap(() => this.courier.checkDeliveryCoverage(dto.toAddress));
|
||||
const options = await this.callOptions();
|
||||
return this.wrap(() => this.courier.checkDeliveryCoverage(dto.toAddress, options));
|
||||
}
|
||||
|
||||
async createShipment(dto: XiaofeixiaCreateShipmentDto) {
|
||||
async createShipment(dto: XiaofeixiaCreateShipmentDto, xfxOverride?: XiaofeixiaConfig) {
|
||||
const input = this.mapCreateInput(dto);
|
||||
return this.wrap(() => this.courier.createShipment(input));
|
||||
const options = xfxOverride
|
||||
? { xiaofeixia: xfxOverride }
|
||||
: await this.callOptions();
|
||||
return this.wrap(() => this.courier.createShipment(input, options));
|
||||
}
|
||||
|
||||
async cancelShipment(dto: XiaofeixiaShipmentQueryDto) {
|
||||
const query = this.mapShipmentQuery(dto);
|
||||
const options = await this.callOptions();
|
||||
return this.wrap(async () => {
|
||||
await this.courier.cancelShipment(query);
|
||||
await this.courier.cancelShipment(query, options);
|
||||
return { cancelled: true };
|
||||
});
|
||||
}
|
||||
|
||||
async getShipment(dto: XiaofeixiaShipmentQueryDto) {
|
||||
const query = this.mapShipmentQuery(dto);
|
||||
return this.wrap(() => this.courier.getShipment(query));
|
||||
const options = await this.callOptions();
|
||||
return this.wrap(() => this.courier.getShipment(query, options));
|
||||
}
|
||||
|
||||
async batchGetShipments(dto: XiaofeixiaBatchShipmentQueryDto) {
|
||||
@@ -76,12 +91,19 @@ export class AdminXiaofeixiaService {
|
||||
trackingNumbers: dto.trackingNumbers?.filter(Boolean),
|
||||
outNumbers: dto.outNumbers?.filter(Boolean),
|
||||
};
|
||||
return this.wrap(() => this.courier.batchGetShipments(query));
|
||||
const options = await this.callOptions();
|
||||
return this.wrap(() => this.courier.batchGetShipments(query, options));
|
||||
}
|
||||
|
||||
async getTrack(dto: XiaofeixiaShipmentQueryDto) {
|
||||
const query = this.mapShipmentQuery(dto);
|
||||
return this.wrap(() => this.courier.getTrack(query));
|
||||
const options = await this.callOptions();
|
||||
return this.wrap(() => this.courier.getTrack(query, options));
|
||||
}
|
||||
|
||||
private async callOptions() {
|
||||
const fromDb = await this.fulfillmentProviderService.resolveDefaultXiaofeixiaConfig();
|
||||
return fromDb ? { xiaofeixia: fromDb } : undefined;
|
||||
}
|
||||
|
||||
private mapShipmentQuery(dto: XiaofeixiaShipmentQueryDto): ShipmentQuery {
|
||||
|
||||
@@ -583,6 +583,16 @@ export class CreateFulfillmentProviderDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
capabilitiesJson?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
xiaofeixiaConfig?: {
|
||||
apiUrl?: string;
|
||||
mchId?: string;
|
||||
apiKey?: string;
|
||||
signType?: 'MD5' | 'HMAC-SHA256';
|
||||
appId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class UpdateFulfillmentProviderDto {
|
||||
@@ -605,6 +615,16 @@ export class UpdateFulfillmentProviderDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
capabilitiesJson?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
xiaofeixiaConfig?: {
|
||||
apiUrl?: string;
|
||||
mchId?: string;
|
||||
apiKey?: string;
|
||||
signType?: 'MD5' | 'HMAC-SHA256';
|
||||
appId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ManualShipOrderDto {
|
||||
|
||||
Reference in New Issue
Block a user