feat(trade): 仓配可调配履约与三分支推单
CI / verify (pull_request) Has been cancelled

同城有仓按仓库绑定承运商自动推单或自管填单,无仓/跨城走总部快递;新增仓配注册表、FulfillmentService 及三端运单追踪。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-16 17:51:20 +08:00
parent f1dc23c54c
commit f6d97b4ee8
30 changed files with 1615 additions and 57 deletions
@@ -3,6 +3,7 @@ import {
Inject,
Injectable,
NotFoundException,
forwardRef,
} from '@nestjs/common';
import type { FreightPayType } from '@prisma/client';
import {
@@ -29,6 +30,7 @@ import { buildOrderClientLocationSnapshot } from '../../common/geo/client-locati
import { extractClientIp } from '../../common/geo/client-ip.util';
import { buildOrderStatusEvent, orderStatusLogWhere } from '../../common/event/event.helpers';
import { mapOrderCompat, mapStatusLogCompat } from '../../common/compat/v31-compat';
import { FulfillmentService } from '../fulfillment/fulfillment.service';
import type { Request } from 'express';
@Injectable()
@@ -45,6 +47,8 @@ export class TradeService {
private readonly partnerCityService: PartnerCityService,
private readonly authService: AuthService,
private readonly promoCodeService: PromoCodeService,
@Inject(forwardRef(() => FulfillmentService))
private readonly fulfillmentService: FulfillmentService,
) {}
async preview(userId: bigint, body: { productId: string; quantity: number; addressId?: string }) {
@@ -268,8 +272,7 @@ export class TradeService {
});
});
await this.benefitService.grantOnOrderPaid(order.id);
await this.deliveryProvider.scheduleAutoAdvance(order.id);
await this.afterOrderPaid(order.id);
this.analyticsService.trackOneSafe(userId, 'USER_H5', {
eventName: 'pay_success',
@@ -281,6 +284,15 @@ export class TradeService {
return this.getOrder(userId, orderId);
}
private async afterOrderPaid(orderId: bigint) {
await this.benefitService.grantOnOrderPaid(orderId);
await this.fulfillmentService.dispatchAfterPay(orderId);
const refreshed = await this.prisma.order.findUnique({ where: { id: orderId } });
if (refreshed?.status === 'PENDING_SHIP') {
await this.deliveryProvider.scheduleAutoAdvance(orderId);
}
}
/** 微信支付回调:幂等更新订单为已支付并发券 */
async handlePaySuccess(params: {
orderNo: string;
@@ -361,8 +373,7 @@ export class TradeService {
const refreshed = await this.prisma.order.findUnique({ where: { id: order.id } });
if (refreshed?.payStatus === 'PAID') {
await this.benefitService.grantOnOrderPaid(order.id);
await this.deliveryProvider.scheduleAutoAdvance(order.id);
await this.afterOrderPaid(order.id);
this.analyticsService.trackOneSafe(order.userId, 'USER_H5', {
eventName: 'pay_success',
refType: 'ORDER',
@@ -401,6 +412,7 @@ export class TradeService {
benefitCoupon: true,
imageResource: true,
product: true,
fulfillmentWarehouse: { select: { id: true, name: true } },
},
});
if (!order) throw new NotFoundException('订单不存在');
@@ -411,6 +423,15 @@ export class TradeService {
return serializeBigInt(mapOrderCompat({ ...order, statusLogs: mapStatusLogCompat(statusLogs) }));
}
async getOrderTrack(userId: bigint, orderId: bigint) {
const order = await this.prisma.order.findFirst({
where: { id: orderId, userId },
select: { id: true },
});
if (!order) throw new NotFoundException('订单不存在');
return this.fulfillmentService.getOrderTrack(orderId);
}
async updateAddress(userId: bigint, orderId: bigint, body: Record<string, unknown>) {
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
if (!order) throw new NotFoundException('订单不存在');
@@ -492,7 +513,7 @@ export class TradeService {
const [list, total] = await Promise.all([
this.prisma.order.findMany({
where,
include: { delivery: true, imageResource: true, user: { select: { phone: true, nickname: true } } },
include: { delivery: true, imageResource: true, user: { select: { phone: true, nickname: true } }, fulfillmentWarehouse: { select: { id: true, name: true } } },
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
@@ -507,7 +528,7 @@ export class TradeService {
const partnerOrderWhere = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
const order = await this.prisma.order.findFirst({
where: { id: orderId, ...partnerOrderWhere },
include: { delivery: true, user: true, imageResource: true },
include: { delivery: true, user: true, imageResource: true, fulfillmentWarehouse: { select: { id: true, name: true } } },
});
if (!order) throw new NotFoundException('订单不存在');
const statusLogs = await this.prisma.commonEvent.findMany({
@@ -517,6 +538,37 @@ export class TradeService {
return serializeBigInt(mapOrderCompat({ ...order, statusLogs: mapStatusLogCompat(statusLogs) }));
}
async partnerManualShip(
partnerAccountId: bigint,
orderId: bigint,
input: { logisticsCompany: string; trackingNo: string; manualQueryUrl?: string },
) {
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const warehouseIds = await this.partnerCityService.resolveManagedWarehouseIds(primary.id);
if (!warehouseIds.length) throw new BadRequestException('当前账号未绑定仓库');
await this.fulfillmentService.shipManualByWarehouse(orderId, warehouseIds, input);
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
partnerAccountId: primary.id,
eventName: 'partner_order_ship',
refType: 'ORDER',
refId: orderId,
extraJson: { mode: 'manual' },
});
return this.getPartnerOrder(partnerAccountId, orderId);
}
async getPartnerOrderTrack(partnerAccountId: bigint, orderId: bigint) {
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const partnerOrderWhere = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
const order = await this.prisma.order.findFirst({
where: { id: orderId, ...partnerOrderWhere },
select: { id: true },
});
if (!order) throw new NotFoundException('订单不存在');
return this.fulfillmentService.getOrderTrack(orderId);
}
async advanceDelivery(partnerAccountId: bigint, orderId: bigint, targetStatus: string) {
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const partnerOrderWhere = await this.partnerCityService.buildPartnerOrderWhere(primary.id);