拦截伪区县写入与下单;推单失败挂 fulfillmentHold,HQ 可见超区/推单失败。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
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 {
|
||||
FULFILLMENT_HOLD_COURIER_DISPATCH_FAILED,
|
||||
FULFILLMENT_HOLD_COURIER_OUT_OF_SERVICE,
|
||||
isXfxProviderCode,
|
||||
} from '@dukang/shared-types';
|
||||
import {
|
||||
BOTTLES_PER_BOX,
|
||||
XFX_AUTO_DISPATCH_MAX_BOXES,
|
||||
@@ -29,6 +33,7 @@ export type ManualShipInput = {
|
||||
export type HqLogisticsShipInput = ManualShipInput;
|
||||
|
||||
export const FULFILLMENT_HOLD_LARGE_ORDER = 'LARGE_ORDER_GE_10_BOXES';
|
||||
export { FULFILLMENT_HOLD_COURIER_OUT_OF_SERVICE, FULFILLMENT_HOLD_COURIER_DISPATCH_FAILED };
|
||||
|
||||
@Injectable()
|
||||
export class FulfillmentService {
|
||||
@@ -138,6 +143,7 @@ export class FulfillmentService {
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await this.logDispatchFailure(order, provider, message);
|
||||
await this.markCourierDispatchHold(order.id, message);
|
||||
await this.ensureDeliveryRecord(order.id, 'MANUAL', provider.id);
|
||||
return;
|
||||
}
|
||||
@@ -213,6 +219,7 @@ export class FulfillmentService {
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await this.logDispatchFailure(order, provider, message);
|
||||
await this.markCourierDispatchHold(order.id, message);
|
||||
await this.ensureDeliveryRecord(order.id, 'MANUAL', provider.id);
|
||||
}
|
||||
}
|
||||
@@ -559,6 +566,22 @@ export class FulfillmentService {
|
||||
});
|
||||
}
|
||||
|
||||
/** 自动推单失败:挂履约拦截,HQ 可见,避免静默 MANUAL 像「没推单」 */
|
||||
private async markCourierDispatchHold(orderId: bigint, error: string) {
|
||||
const outOfService = /超出服务区/.test(error);
|
||||
const reason = outOfService
|
||||
? FULFILLMENT_HOLD_COURIER_OUT_OF_SERVICE
|
||||
: FULFILLMENT_HOLD_COURIER_DISPATCH_FAILED;
|
||||
this.logger.warn(`承运商推单失败挂起:orderId=${orderId} reason=${reason} err=${error}`);
|
||||
await this.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
fulfillmentHold: true,
|
||||
fulfillmentHoldReason: reason,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async buildQueryUrlFromTemplate(warehouseId: bigint | null, trackingNo: string) {
|
||||
if (!warehouseId) return undefined;
|
||||
const wh = await this.prisma.cityWarehouse.findUnique({ where: { id: warehouseId } });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { validateShippingAddress } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
@@ -27,7 +28,25 @@ export class UserAddressService {
|
||||
});
|
||||
}
|
||||
|
||||
private assertShippingFields(body: Record<string, unknown>, existing?: {
|
||||
province: string;
|
||||
city: string;
|
||||
district: string;
|
||||
detail: string;
|
||||
}) {
|
||||
const check = validateShippingAddress({
|
||||
province: body.province != null ? String(body.province) : existing?.province,
|
||||
city: body.city != null ? String(body.city) : existing?.city,
|
||||
district: body.district != null ? String(body.district) : existing?.district,
|
||||
detail: body.detail != null ? String(body.detail) : existing?.detail,
|
||||
});
|
||||
if (!check.ok) {
|
||||
throw new BadRequestException(check.message || '收货地址不完整');
|
||||
}
|
||||
}
|
||||
|
||||
async create(userId: bigint, body: Record<string, unknown>) {
|
||||
this.assertShippingFields(body);
|
||||
const isDefault = body.isDefault ? 1 : 0;
|
||||
const address = await this.prisma.$transaction(async (tx) => {
|
||||
if (isDefault) {
|
||||
@@ -52,6 +71,7 @@ export class UserAddressService {
|
||||
async update(userId: bigint, id: bigint, body: Record<string, unknown>) {
|
||||
const existing = await this.prisma.userAddress.findFirst({ where: { id, userId } });
|
||||
if (!existing) throw new NotFoundException('地址不存在');
|
||||
this.assertShippingFields(body, existing);
|
||||
const address = await this.prisma.$transaction(async (tx) => {
|
||||
if (body.isDefault) {
|
||||
await tx.userAddress.updateMany({ where: { userId }, data: { isDefault: 0 } });
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
orderTabToStatuses,
|
||||
toMinSaleQuantity,
|
||||
validateMinPurchase,
|
||||
validateShippingAddress,
|
||||
} from '@dukang/domain';
|
||||
import { loadAppConfig, ClientApp, WECHAT_AUTH_REQUIRED } from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
@@ -155,6 +156,19 @@ export class TradeService {
|
||||
addressMessage = '该规格不支持跨城配送,请更换为开城城市内的收货地址';
|
||||
}
|
||||
}
|
||||
|
||||
if (addressOk && body.addressId) {
|
||||
const address = await this.prisma.userAddress.findFirst({
|
||||
where: { id: BigInt(body.addressId), userId },
|
||||
});
|
||||
if (address) {
|
||||
const shipping = validateShippingAddress(address);
|
||||
if (!shipping.ok) {
|
||||
addressOk = false;
|
||||
addressMessage = shipping.message || '请完善收货地址(需具体区县)';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bottlesPerUnit = sale.bottlesPerUnit > 0 ? sale.bottlesPerUnit : 1;
|
||||
@@ -276,6 +290,10 @@ export class TradeService {
|
||||
where: { id: BigInt(body.addressId), userId },
|
||||
});
|
||||
if (!address) throw new BadRequestException('请选择收货地址');
|
||||
const shipping = validateShippingAddress(address);
|
||||
if (!shipping.ok) {
|
||||
throw new BadRequestException(shipping.message || '请完善收货地址(需具体区县)');
|
||||
}
|
||||
receiverName = address.receiverName;
|
||||
receiverPhone = address.phone;
|
||||
receiverAddress = `${address.province}${address.city}${address.district}${address.detail}`;
|
||||
@@ -1866,6 +1884,15 @@ export class TradeService {
|
||||
if (!body.addressDetail?.trim()) {
|
||||
throw new BadRequestException('请填写详细地址');
|
||||
}
|
||||
const shipping = validateShippingAddress({
|
||||
province: receiverProvince,
|
||||
city: receiverCity,
|
||||
district: receiverDistrict,
|
||||
detail: body.addressDetail,
|
||||
});
|
||||
if (!shipping.ok) {
|
||||
throw new BadRequestException(shipping.message || '请完善收货地址(需具体区县)');
|
||||
}
|
||||
receiverAddress = `${receiverProvince}${receiverCity}${receiverDistrict}${body.addressDetail.trim()}`;
|
||||
}
|
||||
|
||||
@@ -2281,6 +2308,15 @@ export class TradeService {
|
||||
if (!body.addressDetail?.trim()) {
|
||||
throw new BadRequestException('请填写详细地址');
|
||||
}
|
||||
const shipping = validateShippingAddress({
|
||||
province: receiverProvince,
|
||||
city: receiverCity,
|
||||
district: receiverDistrict,
|
||||
detail: body.addressDetail,
|
||||
});
|
||||
if (!shipping.ok) {
|
||||
throw new BadRequestException(shipping.message || '请完善收货地址(需具体区县)');
|
||||
}
|
||||
receiverAddress = `${receiverProvince}${receiverCity}${receiverDistrict}${body.addressDetail.trim()}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user