179 lines
6.0 KiB
TypeScript
179 lines
6.0 KiB
TypeScript
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';
|
|
import type {
|
|
BatchShipmentQuery,
|
|
CreateShipmentInput,
|
|
ShipmentQuery,
|
|
} from '../../integrations/courier/courier.types';
|
|
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
|
import type {
|
|
XiaofeixiaBatchShipmentQueryDto,
|
|
XiaofeixiaCheckCoverageDto,
|
|
XiaofeixiaCreateShipmentDto,
|
|
XiaofeixiaEstimateFreightDto,
|
|
XiaofeixiaShipmentQueryDto,
|
|
} from './dto/admin-courier.dto';
|
|
|
|
function maskSecret(value: string, visible = 4) {
|
|
if (!value) return '';
|
|
if (value.length <= visible) return '*'.repeat(value.length);
|
|
return `${value.slice(0, visible)}${'*'.repeat(Math.min(8, value.length - visible))}`;
|
|
}
|
|
|
|
@Injectable()
|
|
export class AdminXiaofeixiaService {
|
|
constructor(
|
|
private readonly courier: CourierService,
|
|
private readonly courierConfig: CourierConfigService,
|
|
private readonly fulfillmentProviderService: FulfillmentProviderService,
|
|
) {}
|
|
|
|
async getConfig() {
|
|
const fromDb = await this.fulfillmentProviderService.resolveDefaultXiaofeixiaConfig();
|
|
const envCfg = this.courierConfig.load().xiaofeixia;
|
|
const xfx = fromDb ?? envCfg;
|
|
return {
|
|
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 && xfx.apiUrl),
|
|
hint: fromDb
|
|
? '凭证来自仓配管理中启用的小飞侠承运商'
|
|
: '未在仓配管理配置,回退到环境变量(请迁移至仓配管理)',
|
|
};
|
|
}
|
|
|
|
async estimateFreight(dto: XiaofeixiaEstimateFreightDto) {
|
|
const options = await this.callOptions();
|
|
return this.wrap(() => this.courier.estimateFreight(dto.weight, options));
|
|
}
|
|
|
|
async checkCoverage(dto: XiaofeixiaCheckCoverageDto) {
|
|
const options = await this.callOptions();
|
|
return this.wrap(() => this.courier.checkDeliveryCoverage(dto.toAddress, options));
|
|
}
|
|
|
|
async createShipment(dto: XiaofeixiaCreateShipmentDto, xfxOverride?: XiaofeixiaConfig) {
|
|
const input = this.mapCreateInput(dto);
|
|
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, options);
|
|
return { cancelled: true };
|
|
});
|
|
}
|
|
|
|
async getShipment(dto: XiaofeixiaShipmentQueryDto) {
|
|
const query = this.mapShipmentQuery(dto);
|
|
const options = await this.callOptions();
|
|
return this.wrap(() => this.courier.getShipment(query, options));
|
|
}
|
|
|
|
async batchGetShipments(dto: XiaofeixiaBatchShipmentQueryDto) {
|
|
const query: BatchShipmentQuery = {
|
|
trackingNumbers: dto.trackingNumbers?.filter(Boolean),
|
|
outNumbers: dto.outNumbers?.filter(Boolean),
|
|
};
|
|
const options = await this.callOptions();
|
|
return this.wrap(() => this.courier.batchGetShipments(query, options));
|
|
}
|
|
|
|
async getTrack(dto: XiaofeixiaShipmentQueryDto) {
|
|
const query = this.mapShipmentQuery(dto);
|
|
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 {
|
|
if (!dto.trackingNumber && !dto.outNumber) {
|
|
throw new BadRequestException('运单号与商家单号至少填一个');
|
|
}
|
|
return {
|
|
trackingNumber: dto.trackingNumber,
|
|
outNumber: dto.outNumber,
|
|
};
|
|
}
|
|
|
|
private mapCreateInput(dto: XiaofeixiaCreateShipmentDto): CreateShipmentInput {
|
|
const coord = (lng?: number, lat?: number) =>
|
|
lng != null && lat != null ? { lng, lat } : undefined;
|
|
|
|
return {
|
|
outNumber: dto.outNumber,
|
|
customerId: dto.customerId,
|
|
from: {
|
|
name: dto.fromName,
|
|
mobile: dto.fromMobile,
|
|
address: dto.fromAddress,
|
|
addressDetail: dto.fromAddressDetail,
|
|
coordinate: coord(dto.fromLng, dto.fromLat),
|
|
},
|
|
to: {
|
|
name: dto.toName,
|
|
mobile: dto.toMobile,
|
|
address: dto.toAddress,
|
|
addressDetail: dto.toAddressDetail,
|
|
coordinate: coord(dto.toLng, dto.toLat),
|
|
},
|
|
goodsName: dto.goodsName,
|
|
goodsNum: dto.goodsNum,
|
|
weight: dto.weight,
|
|
insuredSumPrice: dto.insuredSumPrice,
|
|
collectionPrice: dto.collectionPrice,
|
|
payMode: dto.payMode as CourierPayMode,
|
|
remark: dto.remark,
|
|
};
|
|
}
|
|
|
|
private async wrap<T>(fn: () => Promise<T>) {
|
|
const startedAt = Date.now();
|
|
try {
|
|
const data = await fn();
|
|
return {
|
|
ok: true,
|
|
elapsedMs: Date.now() - startedAt,
|
|
data,
|
|
};
|
|
} catch (err) {
|
|
if (err instanceof CourierApiError) {
|
|
return {
|
|
ok: false,
|
|
elapsedMs: Date.now() - startedAt,
|
|
error: err.message,
|
|
code: err.code,
|
|
provider: err.providerCode,
|
|
raw: err.raw,
|
|
};
|
|
}
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
return {
|
|
ok: false,
|
|
elapsedMs: Date.now() - startedAt,
|
|
error: message,
|
|
};
|
|
}
|
|
}
|
|
}
|