diff --git a/apps/mini-user/src/lib/product-fulfillment.ts b/apps/mini-user/src/lib/product-fulfillment.ts index 7ecde3c..41c45ae 100644 --- a/apps/mini-user/src/lib/product-fulfillment.ts +++ b/apps/mini-user/src/lib/product-fulfillment.ts @@ -28,3 +28,18 @@ export function normalizeFulfillmentFlags(p: T): T { allowCrossCityDelivery: canCrossCity(p), }; } + +/** 与交易侧一致:收货市 ≠ 开城市且 ≠ 郑州 → 跨城 */ +export function isCrossCityAddress( + addressCity: string | null | undefined, + openCityName: string | null | undefined, +): boolean { + const addr = (addressCity || '').trim(); + const open = (openCityName || '').trim(); + if (!addr) return false; + if (addr === '郑州市') return false; + if (open && addr === open) return false; + // 尚无开城信息时,非郑州地址先按可能跨城处理(由预览接口最终裁定) + if (!open) return addr !== '郑州市'; + return true; +} diff --git a/apps/mini-user/src/pages/order-confirm/index.tsx b/apps/mini-user/src/pages/order-confirm/index.tsx index f389c2c..2e665d0 100644 --- a/apps/mini-user/src/pages/order-confirm/index.tsx +++ b/apps/mini-user/src/pages/order-confirm/index.tsx @@ -9,7 +9,8 @@ import { tryGetClientGpsLocation } from '../../lib/client-location'; import { maskPhone } from '../../lib/phone'; import { ensurePayReady } from '../../lib/pay-ready'; import { fetchUserProfile } from '../../lib/pay-wechat'; -import { request } from '../../lib/api'; +import { request, toast } from '../../lib/api'; +import { canCrossCity, isCrossCityAddress } from '../../lib/product-fulfillment'; import { getProductMainImage } from '../../lib/product-images'; type Address = { @@ -31,6 +32,8 @@ type PreviewProduct = { price: number; mainImageUrl?: string | null; carouselUrls?: string[] | null; + allowCrossCityDelivery?: boolean; + allowOnlinePurchase?: boolean; }; type OrderPreview = { @@ -41,12 +44,18 @@ type OrderPreview = { freightPayType: 'COD' | null; payAmount: number; benefitAmount: number; - city?: { localMinQty: number; crossMinQty: number }; + city?: { name?: string; localMinQty: number; crossMinQty: number }; quantityOk?: boolean; quantityMessage?: string | null; + addressOk?: boolean; + addressMessage?: string | null; minQty?: number; + allowCrossCityDelivery?: boolean; + allowOnlinePurchase?: boolean; }; +const CROSS_CITY_BLOCK_MSG = '该商品不支持跨城配送,请更换为开城城市内的收货地址'; + function formatAddress(a: Address) { return `${a.province}${a.city}${a.district}${a.detail}`; } @@ -64,6 +73,7 @@ export default function OrderConfirmPage() { const [loading, setLoading] = useState(false); const [msg, setMsg] = useState(''); const phonePromptSkipped = useRef(false); + const toastedAddressBlockRef = useRef(''); useEffect(() => { request('/user/addresses') @@ -94,7 +104,24 @@ export default function OrderConfirmPage() { .then((data) => { if (!cancelled) { setPreview(data); - setMsg(data.quantityOk === false ? (data.quantityMessage || '') : ''); + const nextMsg = + data.addressOk === false + ? data.addressMessage || CROSS_CITY_BLOCK_MSG + : data.quantityOk === false + ? data.quantityMessage || '' + : ''; + setMsg(nextMsg); + if ( + data.addressOk === false && + addressId && + toastedAddressBlockRef.current !== addressId + ) { + toastedAddressBlockRef.current = addressId; + toast(data.addressMessage || CROSS_CITY_BLOCK_MSG); + } + if (data.addressOk !== false) { + toastedAddressBlockRef.current = ''; + } } }) .catch((e) => { @@ -117,12 +144,27 @@ export default function OrderConfirmPage() { [addresses, addressId], ); - const isCross = forceCross || preview?.deliveryType === 'CROSS_CITY'; + const allowCross = + preview?.allowCrossCityDelivery !== undefined + ? canCrossCity({ allowCrossCityDelivery: preview.allowCrossCityDelivery }) + : canCrossCity(preview?.product ?? {}); + const localCross = + !!selectedAddress && + isCrossCityAddress(selectedAddress.city, preview?.city?.name); + const isCross = + forceCross || preview?.deliveryType === 'CROSS_CITY' || localCross; + const crossBlocked = isCross && !allowCross; + const addressOk = preview ? preview.addressOk !== false && !crossBlocked : !crossBlocked; const minQty = preview?.minQty ?? (isCross ? (preview?.city?.crossMinQty ?? 6) : (preview?.city?.localMinQty ?? 2)); const quantityOk = preview ? preview.quantityOk !== false && quantity >= minQty : false; - const canSubmit = !!addressId && !!preview && quantityOk && !loading && !previewLoading; + const canSubmit = + !!addressId && !!preview && quantityOk && addressOk && !loading && !previewLoading; + + const addressHint = !addressOk + ? preview?.addressMessage || CROSS_CITY_BLOCK_MSG + : ''; function updateQuantity(next: number) { if (next < 1) return; @@ -162,6 +204,12 @@ export default function OrderConfirmPage() { setMsg('请选择收货地址'); return; } + if (!addressOk) { + const tip = addressHint || CROSS_CITY_BLOCK_MSG; + setMsg(tip); + toast(tip); + return; + } if (!quantityOk) { setMsg( isCross @@ -219,9 +267,12 @@ export default function OrderConfirmPage() { ? '提交中…' : !addressId ? '请选择地址' - : !quantityOk - ? `至少购买 ${minQty} 瓶` - : '提交订单'; + : !addressOk + ? '请更换地址' + : !quantityOk + ? `至少购买 ${minQty} 瓶` + : '提交订单'; + const displayMsg = msg || addressHint; return ( @@ -254,7 +305,13 @@ export default function OrderConfirmPage() { )} - {isCross ? ( + {!addressOk && addressId ? ( + + {addressHint || CROSS_CITY_BLOCK_MSG} + + ) : null} + + {isCross && addressOk ? ( 该地址超出同城配送范围,将由总部物流发货,运费到付 @@ -333,9 +390,9 @@ export default function OrderConfirmPage() { {!previewLoading && !preview && productId ? ( 无法加载商品信息 ) : null} - {msg ? ( - - {msg} + {displayMsg ? ( + + {displayMsg} ) : null} diff --git a/apps/mini-user/src/styles/order.css b/apps/mini-user/src/styles/order.css index 30ec234..f9c49e6 100644 --- a/apps/mini-user/src/styles/order.css +++ b/apps/mini-user/src/styles/order.css @@ -81,6 +81,19 @@ box-shadow: var(--shadow-card); } +.order-card--warn { + background: rgba(166, 29, 36, 0.06); + box-shadow: none; + border: 1px solid rgba(166, 29, 36, 0.15); +} + +.order-warn-text { + display: block; + font-size: 13px; + line-height: 1.5; + color: var(--color-heritage-red); +} + .order-card-title { display: block; font-family: var(--font-headline); diff --git a/server/dukang-api/src/modules/trade/trade.service.ts b/server/dukang-api/src/modules/trade/trade.service.ts index b10c9ae..0242f2d 100644 --- a/server/dukang-api/src/modules/trade/trade.service.ts +++ b/server/dukang-api/src/modules/trade/trade.service.ts @@ -86,18 +86,21 @@ export class TradeService { } } + let addressOk = true; + let addressMessage: string | null = null; if (!onSitePickup) { const allowOnline = product.allowOnlinePurchase !== false; const allowCross = product.allowCrossCityDelivery !== false; if (deliveryType === 'LOCAL' && !allowOnline) { - throw new BadRequestException('该商品不支持线上购买'); - } - if (deliveryType === 'CROSS_CITY') { + addressOk = false; + addressMessage = '该商品不支持线上购买'; + } else if (deliveryType === 'CROSS_CITY') { if (!allowOnline) { - throw new BadRequestException('该商品不支持线上购买'); - } - if (!allowCross) { - throw new BadRequestException('该商品不支持跨城配送'); + addressOk = false; + addressMessage = '该商品不支持线上购买'; + } else if (!allowCross) { + addressOk = false; + addressMessage = '该商品不支持跨城配送,请更换为开城城市内的收货地址'; } } } @@ -137,8 +140,13 @@ export class TradeService { /** 起购未满足时仍返回预览,供确认页改数量;下单接口仍会硬校验 */ quantityOk: check.ok, quantityMessage: check.ok ? null : (check.message ?? null), + /** 地址/履约未满足时仍返回预览,供确认页提示换地址;下单接口仍会硬校验 */ + addressOk, + addressMessage, minQty, onSitePickup, + allowCrossCityDelivery: product.allowCrossCityDelivery !== false, + allowOnlinePurchase: product.allowOnlinePurchase !== false, }; } @@ -157,6 +165,9 @@ export class TradeService { if (preview.quantityOk === false) { throw new BadRequestException(preview.quantityMessage || '购买数量不满足起购要求'); } + if (preview.addressOk === false) { + throw new BadRequestException(preview.addressMessage || '收货地址不可用'); + } const onSitePickup = !!body.onSitePickup || preview.deliveryType === 'ON_SITE_PICKUP'; let receiverName = '现场取货';