该商品不支持跨城配送,请更换为开城城市内的收货地址

This commit is contained in:
2026-08-02 21:41:50 +08:00
parent 8e25e23e81
commit 442f1ea842
4 changed files with 115 additions and 19 deletions
@@ -28,3 +28,18 @@ export function normalizeFulfillmentFlags<T extends FulfillmentFlags>(p: T): T {
allowCrossCityDelivery: canCrossCity(p), 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;
}
@@ -9,7 +9,8 @@ import { tryGetClientGpsLocation } from '../../lib/client-location';
import { maskPhone } from '../../lib/phone'; import { maskPhone } from '../../lib/phone';
import { ensurePayReady } from '../../lib/pay-ready'; import { ensurePayReady } from '../../lib/pay-ready';
import { fetchUserProfile } from '../../lib/pay-wechat'; 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'; import { getProductMainImage } from '../../lib/product-images';
type Address = { type Address = {
@@ -31,6 +32,8 @@ type PreviewProduct = {
price: number; price: number;
mainImageUrl?: string | null; mainImageUrl?: string | null;
carouselUrls?: string[] | null; carouselUrls?: string[] | null;
allowCrossCityDelivery?: boolean;
allowOnlinePurchase?: boolean;
}; };
type OrderPreview = { type OrderPreview = {
@@ -41,12 +44,18 @@ type OrderPreview = {
freightPayType: 'COD' | null; freightPayType: 'COD' | null;
payAmount: number; payAmount: number;
benefitAmount: number; benefitAmount: number;
city?: { localMinQty: number; crossMinQty: number }; city?: { name?: string; localMinQty: number; crossMinQty: number };
quantityOk?: boolean; quantityOk?: boolean;
quantityMessage?: string | null; quantityMessage?: string | null;
addressOk?: boolean;
addressMessage?: string | null;
minQty?: number; minQty?: number;
allowCrossCityDelivery?: boolean;
allowOnlinePurchase?: boolean;
}; };
const CROSS_CITY_BLOCK_MSG = '该商品不支持跨城配送,请更换为开城城市内的收货地址';
function formatAddress(a: Address) { function formatAddress(a: Address) {
return `${a.province}${a.city}${a.district}${a.detail}`; return `${a.province}${a.city}${a.district}${a.detail}`;
} }
@@ -64,6 +73,7 @@ export default function OrderConfirmPage() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState(''); const [msg, setMsg] = useState('');
const phonePromptSkipped = useRef(false); const phonePromptSkipped = useRef(false);
const toastedAddressBlockRef = useRef('');
useEffect(() => { useEffect(() => {
request<Address[]>('/user/addresses') request<Address[]>('/user/addresses')
@@ -94,7 +104,24 @@ export default function OrderConfirmPage() {
.then((data) => { .then((data) => {
if (!cancelled) { if (!cancelled) {
setPreview(data); 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) => { .catch((e) => {
@@ -117,12 +144,27 @@ export default function OrderConfirmPage() {
[addresses, addressId], [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 = const minQty =
preview?.minQty ?? preview?.minQty ??
(isCross ? (preview?.city?.crossMinQty ?? 6) : (preview?.city?.localMinQty ?? 2)); (isCross ? (preview?.city?.crossMinQty ?? 6) : (preview?.city?.localMinQty ?? 2));
const quantityOk = preview ? preview.quantityOk !== false && quantity >= minQty : false; 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) { function updateQuantity(next: number) {
if (next < 1) return; if (next < 1) return;
@@ -162,6 +204,12 @@ export default function OrderConfirmPage() {
setMsg('请选择收货地址'); setMsg('请选择收货地址');
return; return;
} }
if (!addressOk) {
const tip = addressHint || CROSS_CITY_BLOCK_MSG;
setMsg(tip);
toast(tip);
return;
}
if (!quantityOk) { if (!quantityOk) {
setMsg( setMsg(
isCross isCross
@@ -219,9 +267,12 @@ export default function OrderConfirmPage() {
? '提交中…' ? '提交中…'
: !addressId : !addressId
? '请选择地址' ? '请选择地址'
: !quantityOk : !addressOk
? `至少购买 ${minQty}` ? '请更换地址'
: '提交订单'; : !quantityOk
? `至少购买 ${minQty}`
: '提交订单';
const displayMsg = msg || addressHint;
return ( return (
<PageShell variant="sub" className="order-confirm-page" hasFixedFooter> <PageShell variant="sub" className="order-confirm-page" hasFixedFooter>
@@ -254,7 +305,13 @@ export default function OrderConfirmPage() {
)} )}
</View> </View>
{isCross ? ( {!addressOk && addressId ? (
<View className="order-card order-card--warn">
<Text className="order-warn-text">{addressHint || CROSS_CITY_BLOCK_MSG}</Text>
</View>
) : null}
{isCross && addressOk ? (
<View className="order-card"> <View className="order-card">
<Text className="u-muted"> <Text className="u-muted">
@@ -333,9 +390,9 @@ export default function OrderConfirmPage() {
{!previewLoading && !preview && productId ? ( {!previewLoading && !preview && productId ? (
<View className="u-empty"></View> <View className="u-empty"></View>
) : null} ) : null}
{msg ? ( {displayMsg ? (
<Text className="u-muted" style={{ display: 'block', marginTop: 8, textAlign: 'center' }}> <Text className="order-warn-text" style={{ display: 'block', marginTop: 8, textAlign: 'center' }}>
{msg} {displayMsg}
</Text> </Text>
) : null} ) : null}
</View> </View>
+13
View File
@@ -81,6 +81,19 @@
box-shadow: var(--shadow-card); 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 { .order-card-title {
display: block; display: block;
font-family: var(--font-headline); font-family: var(--font-headline);
@@ -86,18 +86,21 @@ export class TradeService {
} }
} }
let addressOk = true;
let addressMessage: string | null = null;
if (!onSitePickup) { if (!onSitePickup) {
const allowOnline = product.allowOnlinePurchase !== false; const allowOnline = product.allowOnlinePurchase !== false;
const allowCross = product.allowCrossCityDelivery !== false; const allowCross = product.allowCrossCityDelivery !== false;
if (deliveryType === 'LOCAL' && !allowOnline) { if (deliveryType === 'LOCAL' && !allowOnline) {
throw new BadRequestException('该商品不支持线上购买'); addressOk = false;
} addressMessage = '该商品不支持线上购买';
if (deliveryType === 'CROSS_CITY') { } else if (deliveryType === 'CROSS_CITY') {
if (!allowOnline) { if (!allowOnline) {
throw new BadRequestException('该商品不支持线上购买'); addressOk = false;
} addressMessage = '该商品不支持线上购买';
if (!allowCross) { } else if (!allowCross) {
throw new BadRequestException('该商品不支持跨城配送'); addressOk = false;
addressMessage = '该商品不支持跨城配送,请更换为开城城市内的收货地址';
} }
} }
} }
@@ -137,8 +140,13 @@ export class TradeService {
/** 起购未满足时仍返回预览,供确认页改数量;下单接口仍会硬校验 */ /** 起购未满足时仍返回预览,供确认页改数量;下单接口仍会硬校验 */
quantityOk: check.ok, quantityOk: check.ok,
quantityMessage: check.ok ? null : (check.message ?? null), quantityMessage: check.ok ? null : (check.message ?? null),
/** 地址/履约未满足时仍返回预览,供确认页提示换地址;下单接口仍会硬校验 */
addressOk,
addressMessage,
minQty, minQty,
onSitePickup, onSitePickup,
allowCrossCityDelivery: product.allowCrossCityDelivery !== false,
allowOnlinePurchase: product.allowOnlinePurchase !== false,
}; };
} }
@@ -157,6 +165,9 @@ export class TradeService {
if (preview.quantityOk === false) { if (preview.quantityOk === false) {
throw new BadRequestException(preview.quantityMessage || '购买数量不满足起购要求'); throw new BadRequestException(preview.quantityMessage || '购买数量不满足起购要求');
} }
if (preview.addressOk === false) {
throw new BadRequestException(preview.addressMessage || '收货地址不可用');
}
const onSitePickup = !!body.onSitePickup || preview.deliveryType === 'ON_SITE_PICKUP'; const onSitePickup = !!body.onSitePickup || preview.deliveryType === 'ON_SITE_PICKUP';
let receiverName = '现场取货'; let receiverName = '现场取货';