feat(fulfillment): 同城运费与路由修复,配送单展示商品用户地址
同城 MANUAL/ZZXFX 按小飞侠价规计费,路由查询回退仓配凭证;HQ 配送单补商品、用户和收货地址。门店核销回跳与小程序核销码一并带上。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
XFX_AUTO_DISPATCH_MAX_BOXES,
|
||||
calcOrderBoxCount,
|
||||
shouldHoldAutoCourierDispatch,
|
||||
toBottleQuantity,
|
||||
} from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { CourierService } from '../../integrations/courier/courier.service';
|
||||
@@ -15,6 +16,9 @@ import { OSS_PROVIDER } from '../../integrations/integrations.constants';
|
||||
import type { IOssProvider } from '../../integrations/oss/oss.interface';
|
||||
import { TradeService } from '../trade/trade.service';
|
||||
import { FulfillmentProviderService } from './fulfillment-provider.service';
|
||||
import { buildXfxGoodsPayload } from './xfx-goods.util';
|
||||
|
||||
type OrderForXfxDispatch = Order & { product?: { spec: string } | null };
|
||||
|
||||
export type ManualShipInput = {
|
||||
logisticsCompany: string;
|
||||
@@ -42,7 +46,7 @@ export class FulfillmentService {
|
||||
async dispatchAfterPay(orderId: bigint) {
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
include: { delivery: true },
|
||||
include: { delivery: true, product: { select: { spec: true } } },
|
||||
});
|
||||
if (!order || order.payStatus !== 'PAID') return;
|
||||
|
||||
@@ -77,7 +81,10 @@ export class FulfillmentService {
|
||||
}
|
||||
|
||||
// 大单拦截:≥10 箱不自动推小飞侠,待总部确认后推单或自配送(按瓶当量)
|
||||
const bottleQty = order.quantity * (order.bottlesPerUnit > 0 ? order.bottlesPerUnit : 1);
|
||||
const bottleQty = toBottleQuantity(
|
||||
order.quantity,
|
||||
order.bottlesPerUnit > 0 ? order.bottlesPerUnit : 1,
|
||||
);
|
||||
if (shouldHoldAutoCourierDispatch(bottleQty)) {
|
||||
const boxes = calcOrderBoxCount(bottleQty);
|
||||
this.logger.warn(
|
||||
@@ -118,7 +125,7 @@ export class FulfillmentService {
|
||||
});
|
||||
}
|
||||
|
||||
async dispatchApiAuto(order: Order, warehouse: CityWarehouse, provider: FulfillmentProvider) {
|
||||
async dispatchApiAuto(order: OrderForXfxDispatch, warehouse: CityWarehouse, provider: FulfillmentProvider) {
|
||||
if (!isXfxProviderCode(provider.code)) {
|
||||
this.logger.warn(`承运商 ${provider.code} 自动推单尚未实现,订单 ${order.orderNo} 保持待发货`);
|
||||
await this.ensureDeliveryRecord(order.id, 'MANUAL', provider.id);
|
||||
@@ -137,6 +144,13 @@ export class FulfillmentService {
|
||||
|
||||
const fromLng = warehouse.lng != null ? Number(warehouse.lng) : 113.665;
|
||||
const fromLat = warehouse.lat != null ? Number(warehouse.lat) : 34.757;
|
||||
const { goodsName, goodsNum } = buildXfxGoodsPayload({
|
||||
productName: order.productName,
|
||||
productSpec: order.productSpec,
|
||||
physicalSpec: order.product?.spec,
|
||||
quantity: order.quantity,
|
||||
bottlesPerUnit: order.bottlesPerUnit,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await this.courier.createShipment(
|
||||
@@ -155,8 +169,8 @@ export class FulfillmentService {
|
||||
address: `${order.receiverProvince}${order.receiverCity}${order.receiverDistrict}`,
|
||||
addressDetail: order.receiverAddress,
|
||||
},
|
||||
goodsName: order.productName,
|
||||
goodsNum: order.quantity * (order.bottlesPerUnit > 0 ? order.bottlesPerUnit : 1),
|
||||
goodsName,
|
||||
goodsNum,
|
||||
weight: 2,
|
||||
payMode: CourierPayMode.SENDER,
|
||||
remark: `仓配自动发货 ${order.orderNo}`,
|
||||
@@ -252,8 +266,12 @@ export class FulfillmentService {
|
||||
where: { id: orderId },
|
||||
include: {
|
||||
delivery: {
|
||||
include: { signPhotoResource: true },
|
||||
include: {
|
||||
signPhotoResource: true,
|
||||
fulfillmentProvider: { select: { id: true, code: true } },
|
||||
},
|
||||
},
|
||||
fulfillmentWarehouse: { select: { fulfillmentProviderId: true } },
|
||||
},
|
||||
});
|
||||
const base = {
|
||||
@@ -264,35 +282,44 @@ export class FulfillmentService {
|
||||
provider: order?.delivery?.provider,
|
||||
trackingNo: order?.delivery?.trackingNo ?? null,
|
||||
logisticsCompany: order?.delivery?.logisticsCompany ?? null,
|
||||
queryError: null as string | null,
|
||||
};
|
||||
if (!order?.delivery) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const providerCode = order.delivery.fulfillmentProvider?.code || String(order.delivery.provider || '');
|
||||
const isXfx =
|
||||
order.delivery.provider === 'XFX' || isXfxProviderCode(String(order.delivery.provider || ''));
|
||||
const canQueryCourier = isXfx && (order.delivery.trackingNo || order.orderNo);
|
||||
order.delivery.provider === 'XFX' ||
|
||||
isXfxProviderCode(providerCode) ||
|
||||
order.deliveryType === 'LOCAL';
|
||||
const canQueryCourier = isXfx && !!(order.delivery.trackingNo || order.orderNo);
|
||||
|
||||
if (canQueryCourier) {
|
||||
try {
|
||||
const options = order.delivery.fulfillmentProviderId
|
||||
? {
|
||||
xiaofeixia: await this.fulfillmentProviderService.resolveXiaofeixiaConfig(
|
||||
order.delivery.fulfillmentProviderId,
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
const xiaofeixia = await this.resolveTrackXiaofeixiaConfig({
|
||||
delivery: order.delivery,
|
||||
fulfillmentWarehouse: order.fulfillmentWarehouse,
|
||||
});
|
||||
const options = xiaofeixia ? { xiaofeixia } : undefined;
|
||||
const shipmentQuery = {
|
||||
trackingNumber: order.delivery.trackingNo ?? undefined,
|
||||
outNumber: order.orderNo,
|
||||
};
|
||||
|
||||
const [nodes, signPhotoDataUris] = await Promise.all([
|
||||
this.courier.getTrack(shipmentQuery, options).catch(() => [] as TrackNode[]),
|
||||
const [trackResult, signPhotoDataUris] = await Promise.all([
|
||||
this.courier
|
||||
.getTrack(shipmentQuery, options)
|
||||
.then((nodes) => ({ nodes: Array.isArray(nodes) ? nodes : [], error: null as string | null }))
|
||||
.catch((err: unknown) => ({
|
||||
nodes: [] as TrackNode[],
|
||||
error: err instanceof Error ? err.message : '查询路由失败',
|
||||
})),
|
||||
this.courier.getSignPhotos(shipmentQuery, options).catch(() => [] as string[]),
|
||||
]);
|
||||
|
||||
base.nodes = this.sortTrackNodesOldestFirst(nodes);
|
||||
base.nodes = this.sortTrackNodesOldestFirst(trackResult.nodes);
|
||||
base.queryError = trackResult.error;
|
||||
base.signPhotoUrls = await this.resolveSignPhotoUrls(order, signPhotoDataUris);
|
||||
|
||||
if (this.shouldFetchEstimatedArrival(order.status, base.nodes)) {
|
||||
@@ -309,8 +336,8 @@ export class FulfillmentService {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
} catch (err) {
|
||||
base.queryError = err instanceof Error ? err.message : '查询路由失败';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,6 +354,28 @@ export class FulfillmentService {
|
||||
};
|
||||
}
|
||||
|
||||
private async resolveTrackXiaofeixiaConfig(order: {
|
||||
delivery: { fulfillmentProviderId: bigint | null } | null;
|
||||
fulfillmentWarehouse?: { fulfillmentProviderId: bigint | null } | null;
|
||||
}): Promise<XiaofeixiaConfig | null> {
|
||||
const ids = [
|
||||
order.delivery?.fulfillmentProviderId,
|
||||
order.fulfillmentWarehouse?.fulfillmentProviderId,
|
||||
].filter((id): id is bigint => id != null);
|
||||
const seen = new Set<string>();
|
||||
for (const id of ids) {
|
||||
const key = String(id);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
try {
|
||||
return await this.fulfillmentProviderService.resolveXiaofeixiaConfig(id);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return this.fulfillmentProviderService.resolveDefaultXiaofeixiaConfig();
|
||||
}
|
||||
|
||||
private sortTrackNodesOldestFirst(nodes: TrackNode[]): TrackNode[] {
|
||||
return [...nodes].sort((a, b) => {
|
||||
const ta = new Date(a.createTime).getTime();
|
||||
|
||||
Reference in New Issue
Block a user