feat(fulfillment): 同城运费与路由修复,配送单展示商品用户地址

同城 MANUAL/ZZXFX 按小飞侠价规计费,路由查询回退仓配凭证;HQ 配送单补商品、用户和收货地址。门店核销回跳与小程序核销码一并带上。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 21:12:36 +08:00
parent 52a7d3789d
commit cc0c0a6ef8
44 changed files with 1029 additions and 218 deletions
@@ -12,6 +12,8 @@ import type { AdminShipOrderDto, HqLogisticsShipDto } from './dto/admin-mutate.d
import type { XiaofeixiaCreateShipmentDto } from './dto/admin-courier.dto';
import { FulfillmentService } from '../fulfillment/fulfillment.service';
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
import { buildXfxGoodsPayload } from '../fulfillment/xfx-goods.util';
import { calcDeliveryFreightAmount } from '../fulfillment/delivery-freight.util';
import { AdminRedeemService } from './admin-redeem.service';
import {
buildExportFilename,
@@ -93,7 +95,16 @@ export class AdminOrdersService {
take: pageSize,
include: {
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
delivery: { select: { provider: true, trackingNo: true, providerOrderNo: true } },
delivery: {
select: {
provider: true,
trackingNo: true,
providerOrderNo: true,
logisticsCompany: true,
manualQueryUrl: true,
fulfillmentProvider: { select: { code: true, pricingRulesJson: true } },
},
},
city: { select: { id: true, name: true, code: true } },
fulfillmentWarehouse: { select: { id: true, name: true } },
benefitCoupon: {
@@ -104,7 +115,12 @@ export class AdminOrdersService {
this.prisma.order.count({ where }),
]);
return serializeBigInt({ items, total, page, pageSize });
return serializeBigInt({
items: items.map((row) => this.withDeliveryLogisticsFee(row)),
total,
page,
pageSize,
});
}
async previewExport(dto: AdminOrdersExportDto) {
@@ -240,7 +256,11 @@ export class AdminOrdersService {
phoneVerifiedAt: true,
},
},
delivery: true,
delivery: {
include: {
fulfillmentProvider: { select: { code: true, pricingRulesJson: true } },
},
},
benefitCoupon: {
select: {
id: true,
@@ -279,7 +299,8 @@ export class AdminOrdersService {
? await this.adminRedeemService.buildCouponRedeemTrace(coupon)
: { redeemSummary: null, redeemRecords: [] };
const { benefitCoupon: _coupon, ...orderRest } = order;
const withFee = this.withDeliveryLogisticsFee(order);
const { benefitCoupon: _coupon, ...orderRest } = withFee;
return serializeBigInt(
mapOrderCompat({
@@ -320,6 +341,7 @@ export class AdminOrdersService {
include: {
delivery: true,
fulfillmentWarehouse: true,
product: { select: { spec: true } },
},
});
if (!order) throw new NotFoundException('订单不存在');
@@ -346,7 +368,7 @@ export class AdminOrdersService {
});
order = await this.prisma.order.findUniqueOrThrow({
where: { id },
include: { delivery: true, fulfillmentWarehouse: true },
include: { delivery: true, fulfillmentWarehouse: true, product: { select: { spec: true } } },
});
}
}
@@ -371,6 +393,13 @@ export class AdminOrdersService {
}
const defaults = this.getShipDefaults(warehouse);
const { goodsName, goodsNum } = buildXfxGoodsPayload({
productName: order.productName,
productSpec: order.productSpec,
physicalSpec: order.product?.spec,
quantity: order.quantity,
bottlesPerUnit: order.bottlesPerUnit,
});
const shipmentDto: XiaofeixiaCreateShipmentDto = {
outNumber: order.orderNo,
fromName: dto.fromName || defaults.fromName,
@@ -383,8 +412,8 @@ export class AdminOrdersService {
toMobile: order.receiverPhone,
toAddress: `${order.receiverProvince}${order.receiverCity}${order.receiverDistrict}`,
toAddressDetail: order.receiverAddress,
goodsName: order.productName,
goodsNum: order.quantity,
goodsName,
goodsNum,
weight: dto.weight ?? defaults.weight,
payMode: dto.payMode || defaults.payMode,
remark: dto.remark || `HQ发货 ${order.orderNo}`,
@@ -433,6 +462,34 @@ export class AdminOrdersService {
return this.detail(id);
}
private withDeliveryLogisticsFee<
T extends {
quantity: number;
bottlesPerUnit: number;
deliveryType: string;
delivery?: {
provider: string;
fulfillmentProvider?: { code: string; pricingRulesJson: string | null } | null;
} | null;
},
>(order: T): T {
if (!order.delivery) return order;
const fp = order.delivery.fulfillmentProvider;
const logisticsFee = calcDeliveryFreightAmount({
quantity: order.quantity,
bottlesPerUnit: order.bottlesPerUnit,
deliveryType: order.deliveryType,
provider: order.delivery.provider,
providerCode: fp?.code,
pricing: this.fulfillmentProviderService.parsePricingRules(fp?.pricingRulesJson ?? null),
});
const { fulfillmentProvider: _fp, ...deliveryRest } = order.delivery;
return {
...order,
delivery: { ...deliveryRest, logisticsFee },
};
}
getShipDefaults(warehouse?: {
contactName: string;
contactPhone: string;