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(fn: () => Promise) { 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, }; } } }