系统判断配送限制

This commit is contained in:
2026-07-26 09:59:38 +08:00
parent 19cb312465
commit da21cd3daa
12 changed files with 176 additions and 10 deletions
@@ -1,6 +1,12 @@
import { BadRequestException, Injectable, Logger, NotFoundException, Inject, forwardRef } from '@nestjs/common';
import type { CityWarehouse, FulfillmentProvider, Order } from '@prisma/client';
import { isXfxProviderCode } from '@dukang/shared-types';
import {
BOTTLES_PER_BOX,
XFX_AUTO_DISPATCH_MAX_BOXES,
calcOrderBoxCount,
shouldHoldAutoCourierDispatch,
} from '@dukang/domain';
import { PrismaService } from '../../common/prisma/prisma.module';
import { CourierService } from '../../integrations/courier/courier.service';
import { CourierPayMode } from '../../integrations/courier/courier.types';
@@ -16,6 +22,8 @@ export type ManualShipInput = {
export type HqLogisticsShipInput = ManualShipInput;
export const FULFILLMENT_HOLD_LARGE_ORDER = 'LARGE_ORDER_GE_10_BOXES';
@Injectable()
export class FulfillmentService {
private readonly logger = new Logger(FulfillmentService.name);
@@ -64,10 +72,48 @@ export class FulfillmentService {
await this.ensureDeliveryRecord(orderId, 'MANUAL');
return;
}
// 大单拦截:≥10 箱不自动推小飞侠,待总部确认后推单或自配送
if (shouldHoldAutoCourierDispatch(order.quantity)) {
const boxes = calcOrderBoxCount(order.quantity);
this.logger.warn(
`大单拦截自动推单:${order.orderNo} quantity=${order.quantity} bottles≈${boxes}箱(阈值 ${XFX_AUTO_DISPATCH_MAX_BOXES}箱/${BOTTLES_PER_BOX}瓶)`,
);
await this.prisma.order.update({
where: { id: orderId },
data: {
fulfillmentHold: true,
fulfillmentHoldReason: FULFILLMENT_HOLD_LARGE_ORDER,
},
});
await this.ensureDeliveryRecord(orderId, 'MANUAL', provider.id);
await this.prisma.logThirdParty.create({
data: {
provider: 'XFX',
scene: 'ORDER_DISPATCH_HOLD',
refType: 'ORDER',
refId: order.id,
status: 'PENDING',
errorMessage: `大单拦截:${order.quantity}瓶(约${boxes}箱),需总部确认后推小飞侠或自配送`.slice(
0,
512,
),
},
});
return;
}
await this.dispatchApiAuto(order, warehouse, provider);
}
}
async clearFulfillmentHold(orderId: bigint) {
await this.prisma.order.update({
where: { id: orderId },
data: { fulfillmentHold: false, fulfillmentHoldReason: null },
});
}
async dispatchApiAuto(order: Order, warehouse: CityWarehouse, provider: FulfillmentProvider) {
if (!isXfxProviderCode(provider.code)) {
this.logger.warn(`承运商 ${provider.code} 自动推单尚未实现,订单 ${order.orderNo} 保持待发货`);
@@ -129,6 +175,10 @@ export class FulfillmentService {
} else {
await tx.orderDelivery.create({ data: { orderId: order.id, ...data } });
}
await tx.order.update({
where: { id: order.id },
data: { fulfillmentHold: false, fulfillmentHoldReason: null },
});
await tx.logThirdParty.create({
data: {
provider: 'XFX',
@@ -260,6 +310,10 @@ export class FulfillmentService {
} else {
await tx.orderDelivery.create({ data: { orderId: order.id, ...data } });
}
await tx.order.update({
where: { id: order.id },
data: { fulfillmentHold: false, fulfillmentHoldReason: null },
});
});
await this.tradeService.applyStatusTransition(order.id, order.status, 'SHIPPING', input.operator);
@@ -34,6 +34,9 @@ export class AdminOrdersService {
if (query.userId) where.userId = BigInt(query.userId);
if (query.cityId) where.cityId = BigInt(query.cityId);
if (query.receiverPhone) where.receiverPhone = { contains: query.receiverPhone };
if (query.fulfillmentHold === true || query.fulfillmentHold === 'true') {
where.fulfillmentHold = true;
}
if (query.createdFrom || query.createdTo) {
where.createdAt = {};
if (query.createdFrom) where.createdAt.gte = new Date(query.createdFrom);
@@ -256,6 +259,10 @@ export class AdminOrdersService {
},
});
}
await tx.order.update({
where: { id },
data: { fulfillmentHold: false, fulfillmentHoldReason: null },
});
});
await this.tradeService.applyStatusTransition(id, order.status, 'SHIPPING', 'HQ_SHIP');
@@ -60,6 +60,10 @@ export class AdminOrdersQueryDto extends PaginationQueryDto {
@IsString()
receiverPhone?: string;
/** 仅看大单拦截待总部确认:true / 1 */
@IsOptional()
fulfillmentHold?: string | boolean;
@IsOptional()
@IsString()
createdFrom?: string;
@@ -1172,7 +1172,12 @@ export class SettlementService {
ok: true,
skipped: Boolean(bill.skipped),
message: bill.reason,
billId: bill.bill?.id?.toString?.() ?? bill.bill?.id,
billId:
bill.bill?.id != null
? typeof bill.bill.id === 'string'
? bill.bill.id
: String(bill.bill.id)
: undefined,
});
} catch (e) {
results.push({
@@ -1419,7 +1424,23 @@ export class SettlementService {
orderBy: { code: 'asc' },
});
const rows = [];
const rows: Array<{
providerId: string;
providerCode: string;
providerName: string;
settlementMethod: string;
prepaidBalance: number;
bankAccountName: string | null;
bankName: string | null;
bankAccountNo: string | null;
pricingRules: ReturnType<FulfillmentProviderService['parsePricingRules']>;
orderCount: number;
bottleCount: number;
logisticsAmount: number;
billId: string | null;
billStatus: string | null;
billAmount: number | null;
}> = [];
for (const p of providers) {
const pricing =
this.fulfillmentProviderService.parsePricingRules(p.pricingRulesJson) ??