diff --git a/apps/mini-user/src/pages/order-confirm/index.tsx b/apps/mini-user/src/pages/order-confirm/index.tsx
index 5213ddb..eb63ffc 100644
--- a/apps/mini-user/src/pages/order-confirm/index.tsx
+++ b/apps/mini-user/src/pages/order-confirm/index.tsx
@@ -42,6 +42,9 @@ type OrderPreview = {
payAmount: number;
benefitAmount: number;
city?: { localMinQty: number; crossMinQty: number };
+ quantityOk?: boolean;
+ quantityMessage?: string | null;
+ minQty?: number;
};
function formatAddress(a: Address) {
@@ -53,7 +56,7 @@ export default function OrderConfirmPage() {
const checkoutCtx = readCheckoutContext(router.params);
const productId = checkoutCtx.productId ?? '';
const forceCross = checkoutCtx.cross === true;
- const [quantity, setQuantity] = useState(Math.max(2, Number(checkoutCtx.qty || 2)));
+ const [quantity, setQuantity] = useState(Math.max(1, Number(checkoutCtx.qty || 2)));
const [addresses, setAddresses] = useState
([]);
const [addressId, setAddressId] = useState(checkoutCtx.addressId || '');
const [preview, setPreview] = useState(null);
@@ -91,7 +94,7 @@ export default function OrderConfirmPage() {
.then((data) => {
if (!cancelled) {
setPreview(data);
- setMsg('');
+ setMsg(data.quantityOk === false ? (data.quantityMessage || '') : '');
}
})
.catch((e) => {
@@ -115,18 +118,14 @@ export default function OrderConfirmPage() {
);
const isCross = forceCross || preview?.deliveryType === 'CROSS_CITY';
- const minQty = isCross ? (preview?.city?.crossMinQty ?? 6) : (preview?.city?.localMinQty ?? 2);
+ 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;
function updateQuantity(next: number) {
- if (next < minQty) {
- setMsg(
- !isCross
- ? `同城配送至少购买 ${minQty} 瓶`
- : `跨城配送至少购买 ${minQty} 瓶(1箱)`,
- );
- return;
- }
- setMsg('');
+ if (next < 1) return;
setQuantity(next);
}
@@ -158,8 +157,18 @@ export default function OrderConfirmPage() {
}
async function submit() {
- if (!addressId) {
- setMsg('请选择收货地址');
+ if (!canSubmit) {
+ if (!addressId) {
+ setMsg('请选择收货地址');
+ return;
+ }
+ if (!quantityOk) {
+ setMsg(
+ isCross
+ ? `跨城配送至少购买 ${minQty} 瓶(1箱)`
+ : `同城配送至少购买 ${minQty} 瓶`,
+ );
+ }
return;
}
@@ -206,6 +215,13 @@ export default function OrderConfirmPage() {
}
const productImage = preview?.product ? getProductMainImage(preview.product) : '';
+ const submitLabel = loading
+ ? '提交中…'
+ : !addressId
+ ? '请选择地址'
+ : !quantityOk
+ ? `至少购买 ${minQty} 瓶`
+ : '提交订单';
return (
@@ -241,7 +257,8 @@ export default function OrderConfirmPage() {
{isCross ? (
- 该地址超出同城配送范围,将由总部物流发货,运费到付。
+ 该地址超出同城配送范围,将由总部物流发货,运费到付
+ {quantity < minQty ? `;跨城至少购买 ${minQty} 瓶(1箱)` : ''}。
) : null}
@@ -269,7 +286,7 @@ export default function OrderConfirmPage() {
购买数量
updateQuantity(quantity - 1)}
>
−
@@ -283,6 +300,13 @@ export default function OrderConfirmPage() {
+ {!quantityOk ? (
+
+ {isCross
+ ? `跨城配送至少购买 ${minQty} 瓶(1箱),请调整数量`
+ : `同城配送至少购买 ${minQty} 瓶,请调整数量`}
+
+ ) : null}
@@ -320,10 +344,13 @@ export default function OrderConfirmPage() {
!loading && void submit()}
+ className={`order-confirm-submit${canSubmit ? '' : ' order-confirm-submit--disabled'}`}
+ onClick={() => {
+ if (!canSubmit) return;
+ void submit();
+ }}
>
- {loading ? '提交中…' : !addressId ? '请选择地址' : '提交订单'}
+ {submitLabel}
diff --git a/apps/mini-user/src/styles/order.css b/apps/mini-user/src/styles/order.css
index 5e0eed8..4f64095 100644
--- a/apps/mini-user/src/styles/order.css
+++ b/apps/mini-user/src/styles/order.css
@@ -93,6 +93,11 @@
color: var(--color-on-surface);
}
+.order-qty-btn--disabled {
+ opacity: 0.4;
+ pointer-events: none;
+}
+
.order-qty-value {
margin: 0 14px;
font-size: 16px;
@@ -101,6 +106,14 @@
text-align: center;
}
+.order-qty-hint {
+ display: block;
+ margin-top: 10px;
+ font-size: 13px;
+ color: var(--color-heritage-red);
+ line-height: 1.4;
+}
+
.order-row {
display: flex;
justify-content: space-between;
@@ -180,6 +193,13 @@
justify-content: center;
}
+.order-confirm-submit--disabled {
+ background: #c9c5c0;
+ color: #fff;
+ opacity: 0.85;
+ pointer-events: none;
+}
+
.order-tabs {
display: flex;
padding: 0 var(--space-page);
diff --git a/server/dukang-api/src/modules/trade/trade.service.ts b/server/dukang-api/src/modules/trade/trade.service.ts
index ffcce37..59dbb1d 100644
--- a/server/dukang-api/src/modules/trade/trade.service.ts
+++ b/server/dukang-api/src/modules/trade/trade.service.ts
@@ -75,7 +75,6 @@ export class TradeService {
city.localMinQty,
city.crossMinQty,
);
- if (!check.ok) throw new BadRequestException(check.message);
const unitPrice = Number(product.price);
const productAmount = unitPrice * body.quantity;
@@ -96,6 +95,10 @@ export class TradeService {
payAmount: productAmount,
benefitAmount: benefitPerUnit * body.quantity,
city: serializeBigInt(city),
+ /** 起购未满足时仍返回预览,供确认页改数量;下单接口仍会硬校验 */
+ quantityOk: check.ok,
+ quantityMessage: check.ok ? null : (check.message ?? null),
+ minQty: deliveryType === 'LOCAL' ? city.localMinQty : city.crossMinQty,
};
}
@@ -110,6 +113,9 @@ export class TradeService {
req: Request,
) {
const preview = await this.preview(userId, body);
+ if (preview.quantityOk === false) {
+ throw new BadRequestException(preview.quantityMessage || '购买数量不满足起购要求');
+ }
const address = await this.prisma.userAddress.findFirst({
where: { id: BigInt(body.addressId), userId },
});