feat(fulfillment): 同城运费与路由修复,配送单展示商品用户地址
同城 MANUAL/ZZXFX 按小飞侠价规计费,路由查询回退仓配凭证;HQ 配送单补商品、用户和收货地址。门店核销回跳与小程序核销码一并带上。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -181,7 +181,12 @@ export class AdminDashboardService {
|
||||
: Promise.resolve(0),
|
||||
can('deliveries')
|
||||
? this.prisma.orderDelivery.count({
|
||||
where: cityFilter ? { order: { cityId: cityFilter } } : undefined,
|
||||
where: {
|
||||
order: {
|
||||
deliveryType: { not: 'ON_SITE_PICKUP' },
|
||||
...(cityFilter ? { cityId: cityFilter } : {}),
|
||||
},
|
||||
},
|
||||
})
|
||||
: Promise.resolve(0),
|
||||
can('finance')
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { loadStorePrimaryBank } from '../../common/store/store-bank.util';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
||||
import { calcDeliveryFreightAmount } from '../fulfillment/delivery-freight.util';
|
||||
import type { AdminDeliveriesQueryDto, AdminRedeemRecordsQueryDto } from './dto/admin-query.dto';
|
||||
import type { UpdateDeliveryDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@@ -334,18 +336,45 @@ export class AdminRedeemService {
|
||||
}
|
||||
}
|
||||
|
||||
const deliveryOrderSelect = {
|
||||
id: true,
|
||||
orderNo: true,
|
||||
status: true,
|
||||
deliveryType: true,
|
||||
productName: true,
|
||||
productSpec: true,
|
||||
barcode69: true,
|
||||
quantity: true,
|
||||
saleUnit: true,
|
||||
bottlesPerUnit: true,
|
||||
payAmount: true,
|
||||
receiverName: true,
|
||||
receiverPhone: true,
|
||||
receiverAddress: true,
|
||||
receiverProvince: true,
|
||||
receiverCity: true,
|
||||
receiverDistrict: true,
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||
imageResource: { select: { url: true } },
|
||||
} satisfies Prisma.OrderSelect;
|
||||
|
||||
@Injectable()
|
||||
export class AdminDeliveriesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly fulfillmentProviderService: FulfillmentProviderService,
|
||||
) {}
|
||||
|
||||
async list(query: AdminDeliveriesQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.OrderDeliveryWhereInput = {};
|
||||
const where: Prisma.OrderDeliveryWhereInput = {
|
||||
order: { deliveryType: { not: 'ON_SITE_PICKUP' } },
|
||||
};
|
||||
if (query.provider) where.provider = query.provider as DeliveryProvider;
|
||||
if (query.trackingNo) where.trackingNo = { contains: query.trackingNo };
|
||||
if (query.orderNo) {
|
||||
where.order = { orderNo: { contains: query.orderNo } };
|
||||
where.order = { deliveryType: { not: 'ON_SITE_PICKUP' }, orderNo: { contains: query.orderNo } };
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
@@ -355,39 +384,53 @@ export class AdminDeliveriesService {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
order: {
|
||||
select: {
|
||||
id: true,
|
||||
orderNo: true,
|
||||
status: true,
|
||||
receiverName: true,
|
||||
receiverPhone: true,
|
||||
deliveryType: true,
|
||||
productName: true,
|
||||
quantity: true,
|
||||
},
|
||||
},
|
||||
order: { select: deliveryOrderSelect },
|
||||
fulfillmentProvider: { select: { code: true, pricingRulesJson: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.orderDelivery.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
return serializeBigInt({
|
||||
items: items.map((row) => this.withLogisticsFee(row)),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detail(id: bigint) {
|
||||
const delivery = await this.prisma.orderDelivery.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
user: { select: { id: true, userNo: true, phone: true } },
|
||||
imageResource: { select: { url: true } },
|
||||
},
|
||||
},
|
||||
order: { select: deliveryOrderSelect },
|
||||
fulfillmentProvider: { select: { code: true, pricingRulesJson: true } },
|
||||
},
|
||||
});
|
||||
if (!delivery) throw new NotFoundException('配送单不存在');
|
||||
return serializeBigInt(delivery);
|
||||
return serializeBigInt(this.withLogisticsFee(delivery));
|
||||
}
|
||||
|
||||
private withLogisticsFee<
|
||||
T extends {
|
||||
provider: string;
|
||||
order: { quantity: number; bottlesPerUnit: number; deliveryType: string };
|
||||
fulfillmentProvider?: { code: string; pricingRulesJson: string | null } | null;
|
||||
},
|
||||
>(row: T) {
|
||||
const { fulfillmentProvider, ...rest } = row;
|
||||
return {
|
||||
...rest,
|
||||
logisticsFee: calcDeliveryFreightAmount({
|
||||
quantity: row.order.quantity,
|
||||
bottlesPerUnit: row.order.bottlesPerUnit,
|
||||
deliveryType: row.order.deliveryType,
|
||||
provider: row.provider,
|
||||
providerCode: fulfillmentProvider?.code,
|
||||
pricing: this.fulfillmentProviderService.parsePricingRules(
|
||||
fulfillmentProvider?.pricingRulesJson ?? null,
|
||||
),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateDeliveryDto) {
|
||||
|
||||
Reference in New Issue
Block a user