@@ -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