This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
/** 商品履约能力(与 HQ / 交易硬闸一致) */
|
||||||
|
|
||||||
|
export type FulfillmentFlags = {
|
||||||
|
allowOnlinePurchase?: boolean | null;
|
||||||
|
allowOnSitePickup?: boolean | null;
|
||||||
|
allowCrossCityDelivery?: boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 未返回时默认允许线上(存量商品) */
|
||||||
|
export function canBuyOnline(p: FulfillmentFlags): boolean {
|
||||||
|
return p.allowOnlinePurchase !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅显式开启才展示现场取货 */
|
||||||
|
export function canPickupOnSite(p: FulfillmentFlags): boolean {
|
||||||
|
return p.allowOnSitePickup === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canCrossCity(p: FulfillmentFlags): boolean {
|
||||||
|
return p.allowCrossCityDelivery !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeFulfillmentFlags<T extends FulfillmentFlags>(p: T): T {
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
allowOnlinePurchase: canBuyOnline(p),
|
||||||
|
allowOnSitePickup: canPickupOnSite(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;
|
||||||
|
}
|
||||||
@@ -17,6 +17,12 @@ import { isLoggedIn, request, toast } from '../../lib/api';
|
|||||||
import { ensurePayReady } from '../../lib/pay-ready';
|
import { ensurePayReady } from '../../lib/pay-ready';
|
||||||
import { capturePromoSceneAndTouchScan } from '../../lib/promo';
|
import { capturePromoSceneAndTouchScan } from '../../lib/promo';
|
||||||
import { getProductMainImage } from '../../lib/product-images';
|
import { getProductMainImage } from '../../lib/product-images';
|
||||||
|
import {
|
||||||
|
canBuyOnline,
|
||||||
|
canCrossCity,
|
||||||
|
canPickupOnSite,
|
||||||
|
normalizeFulfillmentFlags,
|
||||||
|
} from '../../lib/product-fulfillment';
|
||||||
import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location';
|
import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location';
|
||||||
import {
|
import {
|
||||||
DEFAULT_SHARE_DESC,
|
DEFAULT_SHARE_DESC,
|
||||||
@@ -50,9 +56,9 @@ const FULFILLMENT_FILTERS: Array<{ key: FulfillmentFilter; label: string }> = [
|
|||||||
|
|
||||||
function matchFulfillmentFilter(p: Product, filter: FulfillmentFilter): boolean {
|
function matchFulfillmentFilter(p: Product, filter: FulfillmentFilter): boolean {
|
||||||
if (filter === 'ALL') return true;
|
if (filter === 'ALL') return true;
|
||||||
if (filter === 'ONLINE') return p.allowOnlinePurchase !== false;
|
if (filter === 'ONLINE') return canBuyOnline(p);
|
||||||
if (filter === 'CROSS_CITY') return p.allowCrossCityDelivery !== false;
|
if (filter === 'CROSS_CITY') return canCrossCity(p);
|
||||||
return !!p.allowOnSitePickup;
|
return canPickupOnSite(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
type MiniHomeConfig = {
|
type MiniHomeConfig = {
|
||||||
@@ -117,7 +123,9 @@ export default function HomePage() {
|
|||||||
const loadProducts = useCallback(() => {
|
const loadProducts = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
return request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(cityCode)}`)
|
return request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(cityCode)}`)
|
||||||
.then((list) => setProducts(Array.isArray(list) ? list : []))
|
.then((list) =>
|
||||||
|
setProducts(Array.isArray(list) ? list.map((p) => normalizeFulfillmentFlags(p)) : []),
|
||||||
|
)
|
||||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
|
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [cityCode]);
|
}, [cityCode]);
|
||||||
@@ -138,7 +146,7 @@ export default function HomePage() {
|
|||||||
request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(nextCode)}`),
|
request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(nextCode)}`),
|
||||||
loadMiniHome(),
|
loadMiniHome(),
|
||||||
]);
|
]);
|
||||||
setProducts(Array.isArray(list) ? list : []);
|
setProducts(Array.isArray(list) ? list.map((p) => normalizeFulfillmentFlags(p)) : []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(e instanceof Error ? e.message : '加载失败');
|
toast(e instanceof Error ? e.message : '加载失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -268,7 +276,7 @@ export default function HomePage() {
|
|||||||
<CouponBadge amount={p.benefitDisplay ?? p.price} label="好客权益" />
|
<CouponBadge amount={p.benefitDisplay ?? p.price} label="好客权益" />
|
||||||
</View>
|
</View>
|
||||||
<View className="home-product-actions">
|
<View className="home-product-actions">
|
||||||
{p.allowOnSitePickup ? (
|
{canPickupOnSite(p) ? (
|
||||||
<Text
|
<Text
|
||||||
className="home-pickup-btn"
|
className="home-pickup-btn"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -279,7 +287,7 @@ export default function HomePage() {
|
|||||||
现场取货
|
现场取货
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
{p.allowOnlinePurchase !== false ? (
|
{canBuyOnline(p) ? (
|
||||||
<Text
|
<Text
|
||||||
className="home-buy-btn"
|
className="home-buy-btn"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -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
|
||||||
? '请选择地址'
|
? '请选择地址'
|
||||||
|
: !addressOk
|
||||||
|
? '请更换地址'
|
||||||
: !quantityOk
|
: !quantityOk
|
||||||
? `至少购买 ${minQty} 瓶`
|
? `至少购买 ${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>
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ import {
|
|||||||
getProductMainImage,
|
getProductMainImage,
|
||||||
type ProductImageSource,
|
type ProductImageSource,
|
||||||
} from '../../lib/product-images';
|
} from '../../lib/product-images';
|
||||||
|
import {
|
||||||
|
canBuyOnline,
|
||||||
|
canPickupOnSite,
|
||||||
|
normalizeFulfillmentFlags,
|
||||||
|
} from '../../lib/product-fulfillment';
|
||||||
import {
|
import {
|
||||||
DEFAULT_SHARE_DESC,
|
DEFAULT_SHARE_DESC,
|
||||||
DEFAULT_SHARE_TITLE,
|
DEFAULT_SHARE_TITLE,
|
||||||
@@ -49,7 +54,7 @@ export default function ProductDetailPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!productId) return;
|
if (!productId) return;
|
||||||
request<Product>(`/catalog/products/${productId}`)
|
request<Product>(`/catalog/products/${productId}`)
|
||||||
.then(setProduct)
|
.then((p) => setProduct(normalizeFulfillmentFlags(p)))
|
||||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
||||||
}, [productId]);
|
}, [productId]);
|
||||||
|
|
||||||
@@ -107,9 +112,6 @@ export default function ProductDetailPage() {
|
|||||||
Taro.navigateTo({ url: returnPath });
|
Taro.navigateTo({ url: returnPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowOnline = product?.allowOnlinePurchase !== false;
|
|
||||||
const allowOnSite = !!product?.allowOnSitePickup;
|
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
return (
|
return (
|
||||||
<PageShell variant="scroll" className="product-detail-page">
|
<PageShell variant="scroll" className="product-detail-page">
|
||||||
@@ -119,6 +121,8 @@ export default function ProductDetailPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowOnline = canBuyOnline(product);
|
||||||
|
const allowOnSite = canPickupOnSite(product);
|
||||||
const benefit = Number(product.benefitDisplay ?? product.benefitAmount ?? product.price);
|
const benefit = Number(product.benefitDisplay ?? product.benefitAmount ?? product.price);
|
||||||
const carouselImages = getProductCarouselImages(product);
|
const carouselImages = getProductCarouselImages(product);
|
||||||
const detailImages = getProductDetailImages(product);
|
const detailImages = getProductDetailImages(product);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ export class ClientErrorService {
|
|||||||
const stack = dto.stack?.trim().slice(0, 4000);
|
const stack = dto.stack?.trim().slice(0, 4000);
|
||||||
const pagePath = dto.pagePath?.trim().slice(0, 128);
|
const pagePath = dto.pagePath?.trim().slice(0, 128);
|
||||||
const fingerprint = `${dto.level}|${dto.category}|${message.slice(0, 120)}`;
|
const fingerprint = `${dto.level}|${dto.category}|${message.slice(0, 120)}`;
|
||||||
|
const anonymous = user?.actorId == null;
|
||||||
|
const skipWecom = shouldSkipWecomClientErrorAlert({
|
||||||
|
message,
|
||||||
|
pagePath,
|
||||||
|
clientApp,
|
||||||
|
anonymous,
|
||||||
|
category: dto.category,
|
||||||
|
});
|
||||||
|
|
||||||
const logLine = {
|
const logLine = {
|
||||||
level: dto.level,
|
level: dto.level,
|
||||||
@@ -34,6 +42,7 @@ export class ClientErrorService {
|
|||||||
actorId: user?.actorId != null ? String(user.actorId) : undefined,
|
actorId: user?.actorId != null ? String(user.actorId) : undefined,
|
||||||
stack: stack?.slice(0, 800),
|
stack: stack?.slice(0, 800),
|
||||||
extra: dto.extra,
|
extra: dto.extra,
|
||||||
|
skipWecom,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dto.level === 'fatal') {
|
if (dto.level === 'fatal') {
|
||||||
@@ -61,6 +70,7 @@ export class ClientErrorService {
|
|||||||
actorType: user?.actorType ?? null,
|
actorType: user?.actorType ?? null,
|
||||||
actorId: user?.actorId != null ? String(user.actorId) : null,
|
actorId: user?.actorId != null ? String(user.actorId) : null,
|
||||||
storeId: user?.storeId != null ? String(user.storeId) : null,
|
storeId: user?.storeId != null ? String(user.storeId) : null,
|
||||||
|
skipWecom,
|
||||||
...(dto.extra && typeof dto.extra === 'object' ? { clientExtra: dto.extra } : {}),
|
...(dto.extra && typeof dto.extra === 'object' ? { clientExtra: dto.extra } : {}),
|
||||||
} as Prisma.InputJsonValue,
|
} as Prisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
@@ -71,7 +81,7 @@ export class ClientErrorService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (WECOM_LEVELS.has(dto.level)) {
|
if (WECOM_LEVELS.has(dto.level) && !skipWecom) {
|
||||||
const alertLevel: AlertLevel = dto.level === 'fatal' ? 'P0' : 'P1';
|
const alertLevel: AlertLevel = dto.level === 'fatal' ? 'P0' : 'P1';
|
||||||
this.alert.notify({
|
this.alert.notify({
|
||||||
level: alertLevel,
|
level: alertLevel,
|
||||||
@@ -91,12 +101,56 @@ export class ClientErrorService {
|
|||||||
dedupeKey: `client_error|${fingerprint}`,
|
dedupeKey: `client_error|${fingerprint}`,
|
||||||
dedupeTtlSec: 600,
|
dedupeTtlSec: 600,
|
||||||
});
|
});
|
||||||
|
} else if (skipWecom && WECOM_LEVELS.has(dto.level)) {
|
||||||
|
this.logger.log(
|
||||||
|
`[client_error] skip WeCom alert (likely mini-program audit noise): ${message.slice(0, 160)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤企微推送:微信小程序审核机 / 自动化探测常见噪声。
|
||||||
|
* 仍落库与写服务端日志,仅跳过 webhook。
|
||||||
|
*
|
||||||
|
* 典型特征(与本次协议页报错一致):
|
||||||
|
* - 匿名 USER_MINI
|
||||||
|
* - navigateTo/redirectTo 等 page … is not found(常带 .html,Taro H5 路径形态)
|
||||||
|
*/
|
||||||
|
export function shouldSkipWecomClientErrorAlert(input: {
|
||||||
|
message: string;
|
||||||
|
pagePath?: string | null;
|
||||||
|
clientApp: string;
|
||||||
|
anonymous: boolean;
|
||||||
|
category?: string;
|
||||||
|
}): boolean {
|
||||||
|
const msg = input.message || '';
|
||||||
|
const app = input.clientApp || '';
|
||||||
|
const isMini = app === 'USER_MINI' || app === 'PARTNER_MINI' || app === 'HQ_MINI';
|
||||||
|
|
||||||
|
// 路由页不存在:审核机点协议/隐私链接触发最常见
|
||||||
|
const isNavPageMissing =
|
||||||
|
/(navigateTo|redirectTo|reLaunch|switchTab):fail/i.test(msg) &&
|
||||||
|
/is not found/i.test(msg);
|
||||||
|
|
||||||
|
// Taro 把路径拼成 *.html 的形态,几乎不可能是真·原生页路径
|
||||||
|
const isTaroHtmlPagePath =
|
||||||
|
/\.html(\b|"|')/i.test(msg) && /is not found|page /i.test(msg);
|
||||||
|
|
||||||
|
if (isMini && input.anonymous && (isNavPageMissing || isTaroHtmlPagePath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 即使带登录态,纯 *.html not found 也视为框架/探测噪声
|
||||||
|
if (isMini && isTaroHtmlPagePath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function isClientApp(v: string): v is ClientApp {
|
function isClientApp(v: string): v is ClientApp {
|
||||||
return [
|
return [
|
||||||
'USER_MINI',
|
'USER_MINI',
|
||||||
|
|||||||
@@ -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 = '现场取货';
|
||||||
|
|||||||
Reference in New Issue
Block a user