Product flag, ON_SITE_PICKUP delivery, mini confirm/receive pages; pay skips warehouse auto-advance. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Button, Descriptions, Divider, Drawer, Form, Input, InputNumber, Modal, Popconfirm, Select, Space,
|
||||
Table, Tabs, Tag, Typography, message,
|
||||
Switch, Table, Tabs, Tag, Typography, message,
|
||||
} from 'antd';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
@@ -31,6 +31,7 @@ type Row = {
|
||||
benefitAmount: number;
|
||||
status: string;
|
||||
sortOrder: number;
|
||||
allowOnSitePickup?: boolean;
|
||||
mainImageUrl?: string | null;
|
||||
carouselUrls?: string[];
|
||||
detailImageUrls?: string[];
|
||||
@@ -49,6 +50,7 @@ type ProductFormValues = {
|
||||
benefitAmount?: number;
|
||||
status?: string;
|
||||
sortOrder?: number;
|
||||
allowOnSitePickup?: boolean;
|
||||
coverUrl?: string;
|
||||
carouselUrls?: string[];
|
||||
detailImageUrls?: string[];
|
||||
@@ -100,6 +102,7 @@ function buildProductPayload(v: ProductFormValues) {
|
||||
benefitAmount: v.benefitAmount,
|
||||
status: v.status,
|
||||
sortOrder: v.sortOrder,
|
||||
allowOnSitePickup: !!v.allowOnSitePickup,
|
||||
coverUrl: v.coverUrl,
|
||||
carouselUrls,
|
||||
detailImageUrls,
|
||||
@@ -217,6 +220,9 @@ function BaseInfoFields({ mode }: { mode: 'create' | 'edit' }) {
|
||||
<Form.Item name="sortOrder" label="排序">
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="allowOnSitePickup" label="允许现场取货" valuePropName="checked">
|
||||
<Switch checkedChildren="开" unCheckedChildren="关" />
|
||||
</Form.Item>
|
||||
<Form.Item name="coverUrl" label="封面">
|
||||
<OssUpload bizType="COVER" mediaType="IMAGE" />
|
||||
</Form.Item>
|
||||
@@ -266,6 +272,12 @@ export default function ProductsPage() {
|
||||
{ title: '售价', dataIndex: 'price', width: 80, render: (v) => `¥${v}` },
|
||||
{ title: '权益额', dataIndex: 'benefitAmount', width: 80, render: (v) => `¥${v}` },
|
||||
{ title: '状态', dataIndex: 'status', width: 80, render: (s) => <Tag>{PRODUCT_STATUS_LABELS[s] || s}</Tag> },
|
||||
{
|
||||
title: '现场取货',
|
||||
dataIndex: 'allowOnSitePickup',
|
||||
width: 90,
|
||||
render: (v: boolean) => (v ? <Tag color="green">允许</Tag> : <Tag>否</Tag>),
|
||||
},
|
||||
{ title: '排序', dataIndex: 'sortOrder', width: 60 },
|
||||
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||
{
|
||||
@@ -357,7 +369,7 @@ export default function ProductsPage() {
|
||||
void reload();
|
||||
}} width={720}>
|
||||
<Form form={createForm} layout="vertical" initialValues={{
|
||||
aromaType: 'QINGXIANG', status: 'DRAFT', sortOrder: 0,
|
||||
aromaType: 'QINGXIANG', status: 'DRAFT', sortOrder: 0, allowOnSitePickup: false,
|
||||
carouselUrls: [''], detailImageUrls: [''],
|
||||
features: [{ icon: 'water_drop', title: '', desc: '' }],
|
||||
}}>
|
||||
|
||||
@@ -7,9 +7,11 @@ export default defineAppConfig({
|
||||
'pages/product-detail/index',
|
||||
'pages/store-detail/index',
|
||||
'pages/order-confirm/index',
|
||||
'pages/order-confirm-pickup/index',
|
||||
'pages/pay/index',
|
||||
'pages/orders/index',
|
||||
'pages/order-detail/index',
|
||||
'pages/pickup-receive/index',
|
||||
'pages/addresses/index',
|
||||
'pages/address-edit/index',
|
||||
'pages/customer-service/index',
|
||||
|
||||
@@ -6,7 +6,9 @@ import TabMainHeader from '../../components/TabMainHeader';
|
||||
import CouponBadge from '../../components/CouponBadge';
|
||||
import ProductCarousel from '../../components/ProductCarousel';
|
||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||
import { request, toast } from '../../lib/api';
|
||||
import { goLogin } from '../../lib/auth-nav';
|
||||
import { isLoggedIn, request, toast } from '../../lib/api';
|
||||
import { ensurePayReady } from '../../lib/pay-ready';
|
||||
import { capturePromoSceneAndTouchScan } from '../../lib/promo';
|
||||
import { getProductImages } from '../../lib/product-images';
|
||||
import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location';
|
||||
@@ -19,6 +21,7 @@ type Product = {
|
||||
mainImageUrl?: string | null;
|
||||
carouselUrls?: string[] | null;
|
||||
aromaType: string;
|
||||
allowOnSitePickup?: boolean;
|
||||
};
|
||||
|
||||
const AROMA_TABS = [
|
||||
@@ -95,6 +98,17 @@ export default function HomePage() {
|
||||
Taro.navigateTo({ url: `/pages/product-detail/index?id=${id}` });
|
||||
}
|
||||
|
||||
async function goOnSitePickup(productId: string) {
|
||||
const returnPath = `/pages/order-confirm-pickup/index?productId=${productId}&qty=1`;
|
||||
if (!isLoggedIn()) {
|
||||
goLogin(returnPath);
|
||||
return;
|
||||
}
|
||||
const ready = await ensurePayReady(returnPath);
|
||||
if (!ready) return;
|
||||
Taro.navigateTo({ url: returnPath });
|
||||
}
|
||||
|
||||
const filtered = products.filter((p) => p.aromaType === tab);
|
||||
|
||||
return (
|
||||
@@ -140,6 +154,16 @@ export default function HomePage() {
|
||||
</View>
|
||||
</View>
|
||||
<View className="home-product-actions">
|
||||
{p.allowOnSitePickup ? (
|
||||
<Text
|
||||
className="home-pickup-btn"
|
||||
onClick={() => {
|
||||
void goOnSitePickup(p.id);
|
||||
}}
|
||||
>
|
||||
现场取货
|
||||
</Text>
|
||||
) : null}
|
||||
<Text className="home-buy-btn" onClick={() => openProductDetail(p.id)}>
|
||||
立即购买
|
||||
</Text>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '现场取货确认',
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { View, Text, Image } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import SubPageHeader from '../../components/SubPageHeader';
|
||||
import { goLogin } from '../../lib/auth-nav';
|
||||
import { buildPayUrl } from '../../lib/checkout-nav';
|
||||
import { ensurePayReady } from '../../lib/pay-ready';
|
||||
import { fetchUserProfile } from '../../lib/pay-wechat';
|
||||
import { request } from '../../lib/api';
|
||||
import { getProductMainImage } from '../../lib/product-images';
|
||||
|
||||
type PreviewProduct = {
|
||||
id: string;
|
||||
name: string;
|
||||
spec?: string;
|
||||
subtitle?: string;
|
||||
price: number;
|
||||
mainImageUrl?: string | null;
|
||||
carouselUrls?: string[] | null;
|
||||
};
|
||||
|
||||
type OrderPreview = {
|
||||
product: PreviewProduct;
|
||||
quantity: number;
|
||||
deliveryType: string;
|
||||
productAmount: number;
|
||||
payAmount: number;
|
||||
benefitAmount: number;
|
||||
quantityOk?: boolean;
|
||||
quantityMessage?: string | null;
|
||||
minQty?: number;
|
||||
};
|
||||
|
||||
export default function OrderConfirmPickupPage() {
|
||||
const router = useRouter();
|
||||
const productId = router.params.productId ?? '';
|
||||
const [quantity, setQuantity] = useState(Math.max(1, Number(router.params.qty || 1)));
|
||||
const [preview, setPreview] = useState<OrderPreview | null>(null);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
const phonePromptSkipped = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!productId) return;
|
||||
let cancelled = false;
|
||||
setPreviewLoading(true);
|
||||
request<OrderPreview>('/trade/orders/preview', {
|
||||
method: 'POST',
|
||||
data: { productId, quantity, onSitePickup: true },
|
||||
})
|
||||
.then((data) => {
|
||||
if (!cancelled) {
|
||||
setPreview(data);
|
||||
setMsg(data.quantityOk === false ? data.quantityMessage || '' : '');
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!cancelled) {
|
||||
setPreview(null);
|
||||
setMsg(e instanceof Error ? e.message : '加载失败');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setPreviewLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [productId, quantity]);
|
||||
|
||||
const minQty = preview?.minQty ?? 1;
|
||||
const quantityOk = preview ? preview.quantityOk !== false && quantity >= minQty : false;
|
||||
const canSubmit = !!preview && quantityOk && !loading && !previewLoading;
|
||||
|
||||
function updateQuantity(next: number) {
|
||||
if (next < 1) return;
|
||||
setQuantity(next);
|
||||
}
|
||||
|
||||
async function doSubmit() {
|
||||
const order = await request<{ id: string }>('/trade/orders', {
|
||||
method: 'POST',
|
||||
data: { productId, quantity, onSitePickup: true },
|
||||
});
|
||||
Taro.redirectTo({
|
||||
url: buildPayUrl({
|
||||
orderId: order.id,
|
||||
productId,
|
||||
qty: String(quantity),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!canSubmit) {
|
||||
if (!quantityOk) setMsg(`现场取货至少购买 ${minQty} 瓶`);
|
||||
return;
|
||||
}
|
||||
|
||||
const returnPath = `/pages/order-confirm-pickup/index?productId=${productId}&qty=${quantity}`;
|
||||
|
||||
if (!phonePromptSkipped.current) {
|
||||
try {
|
||||
const profile = await fetchUserProfile();
|
||||
const phoneBound =
|
||||
!!profile.phoneVerified ||
|
||||
(!!profile.phone && /^1[3-9]\d{9}$/.test(String(profile.phone)));
|
||||
if (!phoneBound) {
|
||||
const { confirm, cancel } = await Taro.showModal({
|
||||
title: '建议绑定手机号',
|
||||
content: '绑定后便于订单通知与售后联系;也可跳过,不绑定也能继续下单。',
|
||||
confirmText: '去绑定',
|
||||
cancelText: '暂不绑定',
|
||||
});
|
||||
if (confirm) {
|
||||
goLogin(returnPath, { needPhone: '1' });
|
||||
return;
|
||||
}
|
||||
if (cancel) {
|
||||
phonePromptSkipped.current = true;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* 拉取档案失败不阻塞下单 */
|
||||
}
|
||||
}
|
||||
|
||||
const ready = await ensurePayReady(returnPath);
|
||||
if (!ready) return;
|
||||
|
||||
setLoading(true);
|
||||
setMsg('');
|
||||
try {
|
||||
await doSubmit();
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '下单失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const productImage = preview?.product ? getProductMainImage(preview.product) : '';
|
||||
const submitLabel = loading
|
||||
? '提交中…'
|
||||
: !quantityOk
|
||||
? `至少购买 ${minQty} 瓶`
|
||||
: '提交订单';
|
||||
|
||||
return (
|
||||
<PageShell variant="sub" className="order-confirm-page" hasFixedFooter>
|
||||
<SubPageHeader title="现场取货确认" />
|
||||
<View className="sub-page-body">
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">取货方式</Text>
|
||||
<Text className="u-muted">现场取货 · 无需填写收货地址 · 免运费</Text>
|
||||
</View>
|
||||
|
||||
{preview ? (
|
||||
<>
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">商品信息</Text>
|
||||
<View className="order-product-row">
|
||||
<View className="order-product-thumb">
|
||||
{productImage ? (
|
||||
<Image
|
||||
className="order-product-thumb-img"
|
||||
src={productImage}
|
||||
mode="aspectFill"
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text className="order-product-name">{preview.product.name}</Text>
|
||||
{preview.product.spec ? (
|
||||
<Text className="u-muted">{preview.product.spec}</Text>
|
||||
) : null}
|
||||
<Text className="order-product-price">
|
||||
¥{Number(preview.product.price).toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="order-qty-row">
|
||||
<Text>购买数量</Text>
|
||||
<View className="order-qty-controls">
|
||||
<View
|
||||
className={`order-qty-btn${quantity <= 1 ? ' order-qty-btn--disabled' : ''}`}
|
||||
onClick={() => updateQuantity(quantity - 1)}
|
||||
>
|
||||
<Text>−</Text>
|
||||
</View>
|
||||
<Text className="order-qty-value">{quantity}</Text>
|
||||
<View className="order-qty-btn" onClick={() => updateQuantity(quantity + 1)}>
|
||||
<Text>+</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">费用明细</Text>
|
||||
<View className="order-row">
|
||||
<Text className="order-row-label">商品金额</Text>
|
||||
<Text className="order-row-value">¥{Number(preview.productAmount).toFixed(2)}</Text>
|
||||
</View>
|
||||
<View className="order-row">
|
||||
<Text className="order-row-label">好客权益</Text>
|
||||
<Text className="order-row-value--price">¥{Number(preview.benefitAmount).toFixed(2)}</Text>
|
||||
</View>
|
||||
<View className="order-row">
|
||||
<Text className="order-row-label">运费</Text>
|
||||
<Text className="order-row-value">免运费</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
) : previewLoading ? (
|
||||
<View className="u-empty">加载订单信息…</View>
|
||||
) : null}
|
||||
|
||||
{msg ? (
|
||||
<Text className="u-muted" style={{ display: 'block', marginTop: 8 }}>
|
||||
{msg}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<View className="order-confirm-bar">
|
||||
<View className="order-confirm-total">
|
||||
<Text className="order-confirm-total-label">应付合计</Text>
|
||||
<Text className="order-confirm-total-value">
|
||||
¥{preview ? Number(preview.payAmount).toFixed(2) : '—'}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`order-confirm-submit${canSubmit ? '' : ' order-confirm-submit--disabled'}`}
|
||||
onClick={() => {
|
||||
if (!canSubmit) return;
|
||||
void submit();
|
||||
}}
|
||||
>
|
||||
<Text>{submitLabel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -56,16 +56,6 @@ const STATUS_LABELS: Record<string, string> = {
|
||||
REFUNDED: '已退款',
|
||||
};
|
||||
|
||||
/** 已付款未完成:可选现场取货并确认收货 */
|
||||
const ON_SITE_PICKUP_STATUSES = new Set([
|
||||
'PENDING_SHIP',
|
||||
'OUT_WAREHOUSE',
|
||||
'SHIPPING',
|
||||
'SHIPPED',
|
||||
'PENDING_RECEIVE',
|
||||
'DELIVERED',
|
||||
]);
|
||||
|
||||
function fullReceiverAddress(order: OrderDetail) {
|
||||
const detail = (order.receiverAddress || '').trim();
|
||||
const region = [order.receiverProvince, order.receiverCity, order.receiverDistrict]
|
||||
@@ -80,7 +70,6 @@ export default function OrderDetailPage() {
|
||||
const router = useRouter();
|
||||
const orderId = router.params.id ?? '';
|
||||
const [order, setOrder] = useState<OrderDetail | null>(null);
|
||||
const [onSitePickup, setOnSitePickup] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -92,13 +81,8 @@ export default function OrderDetailPage() {
|
||||
|
||||
const isReship = !!order?.originOrderId;
|
||||
const canPay = !!order && order.status === 'PENDING_PAY' && !isReship;
|
||||
const canOnSitePickup =
|
||||
!!order && !isReship && ON_SITE_PICKUP_STATUSES.has(order.status || '');
|
||||
const canConfirmReceive =
|
||||
!!order &&
|
||||
!isReship &&
|
||||
(['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '') ||
|
||||
(onSitePickup && canOnSitePickup));
|
||||
!!order && !isReship && ['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '');
|
||||
|
||||
const item = order?.items?.[0];
|
||||
const productName = item?.productName || order?.productName || '杜康商品';
|
||||
@@ -137,13 +121,9 @@ export default function OrderDetailPage() {
|
||||
async function confirmReceive() {
|
||||
if (!order || !canConfirmReceive || confirming) return;
|
||||
|
||||
const useOnSite =
|
||||
onSitePickup || !['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '');
|
||||
const { confirm } = await Taro.showModal({
|
||||
title: useOnSite ? '确认现场取货?' : '确认收货?',
|
||||
content: useOnSite
|
||||
? '请确认您已在现场拿到商品。确认后订单将完成,好客权益即时可用,无法再安排配送。若尚未取到酒,请勿确认。'
|
||||
: '请确认已收到商品。确认后订单将完成,好客权益可正常使用。',
|
||||
title: '确认收货?',
|
||||
content: '请确认已收到商品。确认后订单将完成,好客权益可正常使用。',
|
||||
confirmText: '确认收货',
|
||||
cancelText: '再想想',
|
||||
});
|
||||
@@ -153,11 +133,10 @@ export default function OrderDetailPage() {
|
||||
try {
|
||||
const updated = await request<OrderDetail>(`/trade/orders/${order.id}/confirm-receive`, {
|
||||
method: 'POST',
|
||||
data: { onSitePickup: useOnSite },
|
||||
data: {},
|
||||
});
|
||||
setOrder(updated);
|
||||
setOnSitePickup(false);
|
||||
toast(useOnSite ? '现场取货已确认,订单完成' : '已确认收货');
|
||||
toast('已确认收货');
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : '确认收货失败');
|
||||
} finally {
|
||||
@@ -225,27 +204,6 @@ export default function OrderDetailPage() {
|
||||
<Text className="u-muted">地址信息待完善</Text>
|
||||
)}
|
||||
</View>
|
||||
{canOnSitePickup ? (
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">取货方式</Text>
|
||||
<View
|
||||
className="order-pickup-option"
|
||||
onClick={() => setOnSitePickup((v) => !v)}
|
||||
>
|
||||
<View
|
||||
className={`order-pickup-check${onSitePickup ? ' order-pickup-check--on' : ''}`}
|
||||
>
|
||||
{onSitePickup ? <Text className="order-pickup-check-mark">✓</Text> : null}
|
||||
</View>
|
||||
<View className="order-pickup-copy">
|
||||
<Text className="order-pickup-title">现场取货</Text>
|
||||
<Text className="order-pickup-desc">
|
||||
已在活动现场或门店拿到商品时勾选,确认后订单直接完成
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">订单信息</Text>
|
||||
<View className="order-row">
|
||||
@@ -303,7 +261,7 @@ export default function OrderDetailPage() {
|
||||
className={`order-confirm-submit${confirming ? ' order-confirm-submit--disabled' : ''}`}
|
||||
onClick={confirming ? undefined : () => void confirmReceive()}
|
||||
>
|
||||
{confirming ? '提交中…' : onSitePickup ? '确认现场取货' : '确认收货'}
|
||||
{confirming ? '提交中…' : '确认收货'}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function PayPage() {
|
||||
const [msg, setMsg] = useState('');
|
||||
const [orderNo, setOrderNo] = useState('');
|
||||
const [payAmount, setPayAmount] = useState('—');
|
||||
const [deliveryType, setDeliveryType] = useState('');
|
||||
|
||||
const returnPath = orderId
|
||||
? `/pages/pay/index?orderId=${orderId}`
|
||||
@@ -62,11 +63,15 @@ export default function PayPage() {
|
||||
setPayAmount('—');
|
||||
return;
|
||||
}
|
||||
request<{ orderNo?: string; payAmount?: number | string; totalAmount?: number | string }>(
|
||||
`/trade/orders/${orderId}`,
|
||||
)
|
||||
request<{
|
||||
orderNo?: string;
|
||||
payAmount?: number | string;
|
||||
totalAmount?: number | string;
|
||||
deliveryType?: string;
|
||||
}>(`/trade/orders/${orderId}`)
|
||||
.then((order) => {
|
||||
setOrderNo(order.orderNo || '');
|
||||
setDeliveryType(order.deliveryType || '');
|
||||
const amount = Number(order.payAmount ?? order.totalAmount ?? 0);
|
||||
if (Number.isFinite(amount) && amount > 0) {
|
||||
setPayAmount(amount.toFixed(2));
|
||||
@@ -74,6 +79,7 @@ export default function PayPage() {
|
||||
})
|
||||
.catch((e) => {
|
||||
setOrderNo('');
|
||||
setDeliveryType('');
|
||||
toast(e instanceof Error ? e.message : '加载订单失败');
|
||||
});
|
||||
}, [orderId]);
|
||||
@@ -137,7 +143,11 @@ export default function PayPage() {
|
||||
} else {
|
||||
toast('支付成功', 'success');
|
||||
}
|
||||
if (deliveryType === 'ON_SITE_PICKUP') {
|
||||
Taro.redirectTo({ url: `/pages/pickup-receive/index?id=${orderId}` });
|
||||
} else {
|
||||
Taro.redirectTo({ url: '/pages/orders/index?tab=paid' });
|
||||
}
|
||||
} catch (e) {
|
||||
if (isWechatAuthRequiredError(e)) {
|
||||
setNeedsWechatAuth(true);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '确认收货',
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { View, Text, Image } from '@tarojs/components';
|
||||
import Taro, { useDidShow, useRouter } from '@tarojs/taro';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import SubPageHeader from '../../components/SubPageHeader';
|
||||
import { request, toast } from '../../lib/api';
|
||||
import { getProductMainImage } from '../../lib/product-images';
|
||||
|
||||
type OrderDetail = {
|
||||
id: string;
|
||||
orderNo?: string;
|
||||
status?: string;
|
||||
payAmount?: number | string;
|
||||
productName?: string;
|
||||
productSpec?: string;
|
||||
quantity?: number;
|
||||
product?: {
|
||||
name?: string;
|
||||
spec?: string;
|
||||
mainImageUrl?: string | null;
|
||||
carouselUrls?: string[] | null;
|
||||
};
|
||||
imageUrl?: string | null;
|
||||
mainImageUrl?: string | null;
|
||||
};
|
||||
|
||||
export default function PickupReceivePage() {
|
||||
const router = useRouter();
|
||||
const orderId = router.params.id ?? router.params.orderId ?? '';
|
||||
const [order, setOrder] = useState<OrderDetail | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const load = useCallback(() => {
|
||||
if (!orderId) return;
|
||||
setLoading(true);
|
||||
request<OrderDetail>(`/trade/orders/${orderId}`)
|
||||
.then((data) => setOrder(data))
|
||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}, [orderId]);
|
||||
|
||||
useDidShow(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
async function confirmReceive() {
|
||||
if (!orderId || submitting) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await request(`/trade/orders/${orderId}/confirm-receive`, {
|
||||
method: 'POST',
|
||||
data: {},
|
||||
});
|
||||
toast('确认收货成功', 'success');
|
||||
setTimeout(() => {
|
||||
Taro.redirectTo({ url: '/pages/orders/index?tab=done' });
|
||||
}, 500);
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : '确认失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const name = order?.productName || order?.product?.name || '商品';
|
||||
const spec = order?.productSpec || order?.product?.spec;
|
||||
const image =
|
||||
order?.mainImageUrl ||
|
||||
order?.imageUrl ||
|
||||
(order?.product ? getProductMainImage(order.product) : '') ||
|
||||
'';
|
||||
const amount = Number(order?.payAmount ?? 0);
|
||||
const canConfirm = order?.status === 'PENDING_RECEIVE' || order?.status === 'DELIVERED';
|
||||
|
||||
return (
|
||||
<PageShell variant="sub" className="order-confirm-page" hasFixedFooter>
|
||||
<SubPageHeader title="确认收货" />
|
||||
<View className="sub-page-body">
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">现场取货</Text>
|
||||
<Text className="u-muted">请确认已在现场拿到商品后再点击确认收货</Text>
|
||||
</View>
|
||||
|
||||
{loading && !order ? (
|
||||
<View className="order-card">
|
||||
<Text className="u-muted">加载中…</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{order ? (
|
||||
<>
|
||||
<View className="order-card">
|
||||
<View className="order-row">
|
||||
<Text className="order-row-label">订单号</Text>
|
||||
<Text className="order-row-value">{order.orderNo || '—'}</Text>
|
||||
</View>
|
||||
<View className="order-product-row" style={{ marginTop: 12 }}>
|
||||
<View className="order-product-thumb">
|
||||
{image ? (
|
||||
<Image className="order-product-thumb-img" src={image} mode="aspectFill" />
|
||||
) : null}
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text className="order-product-name">{name}</Text>
|
||||
{spec ? <Text className="u-muted">{spec}</Text> : null}
|
||||
<Text className="u-muted">×{order.quantity ?? 1}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="order-card">
|
||||
<View className="order-row">
|
||||
<Text className="order-row-label">实付金额</Text>
|
||||
<Text className="order-row-value order-pay-amount">
|
||||
¥{Number.isFinite(amount) ? amount.toFixed(2) : '—'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
<View className="pay-bar">
|
||||
<View
|
||||
className="order-confirm-submit"
|
||||
style={{ flex: 1, opacity: canConfirm && !submitting ? 1 : 0.6 }}
|
||||
onClick={() => {
|
||||
if (!canConfirm || submitting) return;
|
||||
void confirmReceive();
|
||||
}}
|
||||
>
|
||||
<Text>{submitting ? '提交中…' : canConfirm ? '确认收货' : '订单状态不可确认'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -191,6 +191,20 @@
|
||||
padding: 0 var(--space-gutter) var(--space-gutter);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.home-pickup-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-full);
|
||||
background: #2e7d32;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.home-buy-btn {
|
||||
|
||||
@@ -73,58 +73,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.order-pickup-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-pickup-check {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
border: 1.5px solid var(--color-outline, #c8c4be);
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.order-pickup-check--on {
|
||||
border-color: var(--color-primary, #8b1a1a);
|
||||
background: var(--color-primary, #8b1a1a);
|
||||
}
|
||||
|
||||
.order-pickup-check-mark {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.order-pickup-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-pickup-title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-on-surface);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.order-pickup-desc {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-on-surface-variant, #78716c);
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: var(--color-card);
|
||||
border-radius: var(--radius-lg);
|
||||
|
||||
@@ -32,6 +32,11 @@ describe('validateMinPurchase', () => {
|
||||
expect(validateMinPurchase('CROSS_CITY', 5, 2, 6).ok).toBe(false);
|
||||
expect(validateMinPurchase('CROSS_CITY', 6, 2, 6).ok).toBe(true);
|
||||
});
|
||||
|
||||
it('on-site pickup requires at least 1 bottle', () => {
|
||||
expect(validateMinPurchase('ON_SITE_PICKUP', 0, 2, 6).ok).toBe(false);
|
||||
expect(validateMinPurchase('ON_SITE_PICKUP', 1, 2, 6).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateRedeemAmount', () => {
|
||||
|
||||
@@ -8,11 +8,17 @@ export function calcBenefitAmount(product: ProductPricing): number {
|
||||
}
|
||||
|
||||
export function validateMinPurchase(
|
||||
deliveryType: 'LOCAL' | 'CROSS_CITY',
|
||||
deliveryType: 'LOCAL' | 'CROSS_CITY' | 'ON_SITE_PICKUP',
|
||||
quantity: number,
|
||||
localMinQty: number,
|
||||
crossMinQty: number,
|
||||
): { ok: boolean; message?: string } {
|
||||
if (deliveryType === 'ON_SITE_PICKUP') {
|
||||
if (quantity < 1) {
|
||||
return { ok: false, message: '现场取货至少购买 1 瓶' };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
const min = deliveryType === 'LOCAL' ? localMinQty : crossMinQty;
|
||||
if (quantity < min) {
|
||||
return {
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface ProductDto {
|
||||
/** 详情长图(bizType=DETAIL 或 detailContent JSON) */
|
||||
detailImageUrls?: string[];
|
||||
detailContent?: ProductDetailContentDto | null;
|
||||
/** 是否允许现场取货下单 */
|
||||
allowOnSitePickup?: boolean;
|
||||
}
|
||||
|
||||
export interface ProductDetailFeatureDto {
|
||||
|
||||
@@ -91,6 +91,7 @@ export enum OrderTab {
|
||||
export enum DeliveryType {
|
||||
LOCAL = 'LOCAL',
|
||||
CROSS_CITY = 'CROSS_CITY',
|
||||
ON_SITE_PICKUP = 'ON_SITE_PICKUP',
|
||||
}
|
||||
|
||||
export enum AromaType {
|
||||
|
||||
@@ -262,6 +262,7 @@ enum PayStatus {
|
||||
enum DeliveryType {
|
||||
LOCAL
|
||||
CROSS_CITY
|
||||
ON_SITE_PICKUP
|
||||
}
|
||||
|
||||
enum FreightPayType {
|
||||
@@ -451,6 +452,7 @@ model CommonProductItem {
|
||||
benefitAmount Decimal? @map("benefit_amount") @db.Decimal(10, 2)
|
||||
status ProductStatus @default(DRAFT)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
allowOnSitePickup Boolean @default(false) @map("allow_on_site_pickup")
|
||||
coverResourceId BigInt? @map("cover_resource_id") @db.UnsignedBigInt
|
||||
detailContent Json? @map("detail_content")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
|
||||
@@ -89,6 +89,7 @@ export class AdminProductsService {
|
||||
benefitAmount: dto.benefitAmount ?? dto.price,
|
||||
status: (dto.status ?? 'DRAFT') as 'DRAFT' | 'ON_SALE' | 'OFF_SALE',
|
||||
sortOrder: dto.sortOrder ?? 0,
|
||||
allowOnSitePickup: dto.allowOnSitePickup ?? false,
|
||||
...(dto.detailContent !== undefined
|
||||
? { detailContent: dto.detailContent as Prisma.InputJsonValue }
|
||||
: {}),
|
||||
@@ -118,6 +119,7 @@ export class AdminProductsService {
|
||||
...(dto.benefitAmount !== undefined ? { benefitAmount: dto.benefitAmount } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'DRAFT' | 'ON_SALE' | 'OFF_SALE' } : {}),
|
||||
...(dto.sortOrder !== undefined ? { sortOrder: dto.sortOrder } : {}),
|
||||
...(dto.allowOnSitePickup !== undefined ? { allowOnSitePickup: dto.allowOnSitePickup } : {}),
|
||||
...(dto.detailContent !== undefined
|
||||
? { detailContent: dto.detailContent as Prisma.InputJsonValue }
|
||||
: {}),
|
||||
|
||||
@@ -947,6 +947,10 @@ export class CreateProductDto {
|
||||
@IsNumber()
|
||||
sortOrder?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allowOnSitePickup?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coverUrl?: string;
|
||||
@@ -995,6 +999,10 @@ export class UpdateProductDto {
|
||||
@IsNumber()
|
||||
sortOrder?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allowOnSitePickup?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coverUrl?: string;
|
||||
|
||||
@@ -51,7 +51,10 @@ export class TradeService {
|
||||
private readonly fulfillmentService: FulfillmentService,
|
||||
) {}
|
||||
|
||||
async preview(userId: bigint, body: { productId: string; quantity: number; addressId?: string }) {
|
||||
async preview(
|
||||
userId: bigint,
|
||||
body: { productId: string; quantity: number; addressId?: string; onSitePickup?: boolean },
|
||||
) {
|
||||
const product = await this.catalogService.getProduct(BigInt(body.productId));
|
||||
if (!product || product.status !== 'ON_SALE') {
|
||||
throw new BadRequestException('商品不可购买');
|
||||
@@ -59,8 +62,15 @@ export class TradeService {
|
||||
const city = await this.prisma.commonCity.findFirst({ where: { status: 'ACTIVE' } });
|
||||
if (!city) throw new BadRequestException('暂无开城城市');
|
||||
|
||||
let deliveryType: 'LOCAL' | 'CROSS_CITY' = 'LOCAL';
|
||||
if (body.addressId) {
|
||||
const onSitePickup = !!body.onSitePickup;
|
||||
if (onSitePickup && !product.allowOnSitePickup) {
|
||||
throw new BadRequestException('该商品不支持现场取货');
|
||||
}
|
||||
|
||||
let deliveryType: 'LOCAL' | 'CROSS_CITY' | 'ON_SITE_PICKUP' = onSitePickup
|
||||
? 'ON_SITE_PICKUP'
|
||||
: 'LOCAL';
|
||||
if (!onSitePickup && body.addressId) {
|
||||
const address = await this.prisma.userAddress.findFirst({
|
||||
where: { id: BigInt(body.addressId), userId },
|
||||
});
|
||||
@@ -84,13 +94,19 @@ export class TradeService {
|
||||
});
|
||||
|
||||
const freightPayType: FreightPayType | null = deliveryType === 'CROSS_CITY' ? 'COD' : null;
|
||||
const minQty =
|
||||
deliveryType === 'ON_SITE_PICKUP'
|
||||
? 1
|
||||
: deliveryType === 'LOCAL'
|
||||
? city.localMinQty
|
||||
: city.crossMinQty;
|
||||
|
||||
return {
|
||||
product,
|
||||
quantity: body.quantity,
|
||||
deliveryType,
|
||||
productAmount,
|
||||
freightAmount: deliveryType === 'CROSS_CITY' ? 0 : 0,
|
||||
freightAmount: 0,
|
||||
freightPayType,
|
||||
payAmount: productAmount,
|
||||
benefitAmount: benefitPerUnit * body.quantity,
|
||||
@@ -98,7 +114,8 @@ export class TradeService {
|
||||
/** 起购未满足时仍返回预览,供确认页改数量;下单接口仍会硬校验 */
|
||||
quantityOk: check.ok,
|
||||
quantityMessage: check.ok ? null : (check.message ?? null),
|
||||
minQty: deliveryType === 'LOCAL' ? city.localMinQty : city.crossMinQty,
|
||||
minQty,
|
||||
onSitePickup,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,7 +124,8 @@ export class TradeService {
|
||||
body: {
|
||||
productId: string;
|
||||
quantity: number;
|
||||
addressId: string;
|
||||
addressId?: string;
|
||||
onSitePickup?: boolean;
|
||||
clientLocation?: unknown;
|
||||
},
|
||||
req: Request,
|
||||
@@ -116,10 +134,35 @@ export class TradeService {
|
||||
if (preview.quantityOk === false) {
|
||||
throw new BadRequestException(preview.quantityMessage || '购买数量不满足起购要求');
|
||||
}
|
||||
|
||||
const onSitePickup = !!body.onSitePickup || preview.deliveryType === 'ON_SITE_PICKUP';
|
||||
let receiverName = '现场取货';
|
||||
let receiverPhone = '00000000000';
|
||||
let receiverAddress = '现场取货';
|
||||
let receiverProvince = '';
|
||||
let receiverCity = '';
|
||||
let receiverDistrict = '';
|
||||
|
||||
if (onSitePickup) {
|
||||
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
||||
receiverPhone = user?.phone || '00000000000';
|
||||
receiverName = (user?.nickname?.trim() || '现场取货').slice(0, 32);
|
||||
receiverProvince = '现场';
|
||||
receiverCity = '现场';
|
||||
receiverDistrict = '取货';
|
||||
} else {
|
||||
if (!body.addressId) throw new BadRequestException('请选择收货地址');
|
||||
const address = await this.prisma.userAddress.findFirst({
|
||||
where: { id: BigInt(body.addressId), userId },
|
||||
});
|
||||
if (!address) throw new BadRequestException('请选择收货地址');
|
||||
receiverName = address.receiverName;
|
||||
receiverPhone = address.phone;
|
||||
receiverAddress = `${address.province}${address.city}${address.district}${address.detail}`;
|
||||
receiverProvince = address.province;
|
||||
receiverCity = address.city;
|
||||
receiverDistrict = address.district;
|
||||
}
|
||||
|
||||
const product = await this.prisma.commonProductItem.findUniqueOrThrow({
|
||||
where: { id: BigInt(body.productId) },
|
||||
@@ -149,7 +192,7 @@ export class TradeService {
|
||||
cityId: city.id,
|
||||
status: 'PENDING_PAY',
|
||||
payStatus: 'UNPAID',
|
||||
deliveryType: preview.deliveryType as 'LOCAL' | 'CROSS_CITY',
|
||||
deliveryType: preview.deliveryType as 'LOCAL' | 'CROSS_CITY' | 'ON_SITE_PICKUP',
|
||||
productId: product.id,
|
||||
barcode69: product.barcode69,
|
||||
productName: product.name,
|
||||
@@ -159,12 +202,12 @@ export class TradeService {
|
||||
listUnitPrice: product.price,
|
||||
listAmount: preview.productAmount,
|
||||
productAmount: preview.productAmount,
|
||||
receiverName: address.receiverName,
|
||||
receiverPhone: address.phone,
|
||||
receiverAddress: `${address.province}${address.city}${address.district}${address.detail}`,
|
||||
receiverProvince: address.province,
|
||||
receiverCity: address.city,
|
||||
receiverDistrict: address.district,
|
||||
receiverName,
|
||||
receiverPhone,
|
||||
receiverAddress,
|
||||
receiverProvince,
|
||||
receiverCity,
|
||||
receiverDistrict,
|
||||
clientIp: location.clientIp,
|
||||
ipProvince: location.ipProvince,
|
||||
ipCity: location.ipCity,
|
||||
@@ -203,6 +246,7 @@ export class TradeService {
|
||||
orderId: order.id.toString(),
|
||||
productId: body.productId,
|
||||
quantity: body.quantity,
|
||||
onSitePickup,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -241,12 +285,14 @@ export class TradeService {
|
||||
order.cityId,
|
||||
order.receiverDistrict,
|
||||
);
|
||||
const toStatus =
|
||||
order.deliveryType === 'ON_SITE_PICKUP' ? 'PENDING_RECEIVE' : 'PENDING_SHIP';
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.order.update({
|
||||
where: { id: order.id },
|
||||
data: {
|
||||
status: 'PENDING_SHIP',
|
||||
status: toStatus,
|
||||
payStatus: 'PAID',
|
||||
paidAt: now,
|
||||
payExternalNo: externalNo,
|
||||
@@ -269,7 +315,7 @@ export class TradeService {
|
||||
data: buildOrderStatusEvent({
|
||||
orderId: order.id,
|
||||
fromStatus: 'PENDING_PAY',
|
||||
toStatus: 'PENDING_SHIP',
|
||||
toStatus,
|
||||
operator: 'MOCK_PAY',
|
||||
}),
|
||||
});
|
||||
@@ -292,6 +338,20 @@ export class TradeService {
|
||||
|
||||
private async afterOrderPaid(orderId: bigint) {
|
||||
await this.benefitService.grantOnOrderPaid(orderId);
|
||||
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
|
||||
if (!order) return;
|
||||
|
||||
const delivery = await this.prisma.orderDelivery.findUnique({ where: { orderId } });
|
||||
if (!delivery) {
|
||||
await this.prisma.orderDelivery.create({
|
||||
data: { orderId, provider: 'MANUAL' },
|
||||
});
|
||||
}
|
||||
|
||||
if (order.deliveryType === 'ON_SITE_PICKUP') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fulfillmentService.dispatchAfterPay(orderId);
|
||||
const refreshed = await this.prisma.order.findUnique({ where: { id: orderId } });
|
||||
if (refreshed?.status === 'PENDING_SHIP') {
|
||||
@@ -335,6 +395,8 @@ export class TradeService {
|
||||
order.cityId,
|
||||
order.receiverDistrict,
|
||||
);
|
||||
const toStatus =
|
||||
order.deliveryType === 'ON_SITE_PICKUP' ? 'PENDING_RECEIVE' : 'PENDING_SHIP';
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const current = await tx.order.findUnique({ where: { id: order.id } });
|
||||
if (!current || current.payStatus === 'PAID') return;
|
||||
@@ -342,7 +404,7 @@ export class TradeService {
|
||||
await tx.order.update({
|
||||
where: { id: order.id },
|
||||
data: {
|
||||
status: 'PENDING_SHIP',
|
||||
status: toStatus,
|
||||
payStatus: 'PAID',
|
||||
paidAt: now,
|
||||
payExternalNo: params.transactionId,
|
||||
@@ -365,7 +427,7 @@ export class TradeService {
|
||||
data: buildOrderStatusEvent({
|
||||
orderId: order.id,
|
||||
fromStatus: 'PENDING_PAY',
|
||||
toStatus: 'PENDING_SHIP',
|
||||
toStatus,
|
||||
operator: 'WECHAT_PAY',
|
||||
}),
|
||||
});
|
||||
@@ -470,25 +532,12 @@ export class TradeService {
|
||||
async confirmReceive(
|
||||
userId: bigint,
|
||||
orderId: bigint,
|
||||
opts?: { onSitePickup?: boolean },
|
||||
_opts?: { onSitePickup?: boolean },
|
||||
) {
|
||||
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
|
||||
const onSitePickup = !!opts?.onSitePickup;
|
||||
const onSiteEligible = [
|
||||
'PENDING_SHIP',
|
||||
'OUT_WAREHOUSE',
|
||||
'SHIPPING',
|
||||
'SHIPPED',
|
||||
'PENDING_RECEIVE',
|
||||
'DELIVERED',
|
||||
];
|
||||
if (onSitePickup) {
|
||||
if (!onSiteEligible.includes(order.status)) {
|
||||
throw new BadRequestException('当前状态不可现场取货');
|
||||
}
|
||||
} else if (!['PENDING_RECEIVE', 'DELIVERED'].includes(order.status)) {
|
||||
if (!['PENDING_RECEIVE', 'DELIVERED'].includes(order.status)) {
|
||||
throw new BadRequestException('当前状态不可确认收货');
|
||||
}
|
||||
|
||||
@@ -496,8 +545,8 @@ export class TradeService {
|
||||
order.id,
|
||||
order.status,
|
||||
'COMPLETED',
|
||||
onSitePickup ? 'USER_ON_SITE' : 'USER',
|
||||
onSitePickup ? '用户现场取货确认收货' : undefined,
|
||||
order.deliveryType === 'ON_SITE_PICKUP' ? 'USER_ON_SITE' : 'USER',
|
||||
order.deliveryType === 'ON_SITE_PICKUP' ? '用户现场取货确认收货' : undefined,
|
||||
);
|
||||
return this.getOrder(userId, orderId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user