import {useEffect, useState} from "react"; import { Image, Button, Cell, CellGroup, Input, Space, ActionSheet, Popup, InputNumber, ConfigProvider } from '@nutui/nutui-react-taro' import {Location, ArrowRight} from '@nutui/icons-react-taro' import Taro, {useDidShow} from '@tarojs/taro' import {ShopGoods} from "@/api/shop/shopGoods/model"; import {getShopGoods} from "@/api/shop/shopGoods"; import {View, Text} from '@tarojs/components'; import {listShopUserAddress} from "@/api/shop/shopUserAddress"; import {ShopUserAddress} from "@/api/shop/shopUserAddress/model"; import './index.scss' import Gap from "@/components/Gap"; import {selectPayment} from "@/api/system/payment"; import {Payment} from "@/api/system/payment/model"; import {PaymentHandler, PaymentType, buildSingleGoodsOrder} from "@/utils/payment"; import OrderConfirmSkeleton from "@/components/OrderConfirmSkeleton"; import CouponList from "@/components/CouponList"; import {CouponCardProps} from "@/components/CouponCard"; import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon"; import {getGltTicketTemplateByGoodsId} from "@/api/glt/gltTicketTemplate"; import type {GltTicketTemplate} from "@/api/glt/gltTicketTemplate/model"; import { transformCouponData, calculateCouponDiscount, isCouponUsable, getCouponUnusableReason, sortCoupons, filterUsableCoupons, filterUnusableCoupons } from "@/utils/couponUtils"; import type {ShopStore} from "@/api/shop/shopStore/model"; import {getShopStore, listShopStore} from "@/api/shop/shopStore"; import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection"; const OrderConfirm = () => { const [goods, setGoods] = useState(null); const [address, setAddress] = useState() const [payments, setPayments] = useState([]) const [payment, setPayment] = useState() const [isVisible, setIsVisible] = useState(false) const [quantity, setQuantity] = useState(1) const [orderRemark, setOrderRemark] = useState('') const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [payLoading, setPayLoading] = useState(false) // 水票套票活动(若存在则按规则限制最小购买量等) const [ticketTemplate, setTicketTemplate] = useState(null) // InputNumber 主题配置 const customTheme = { nutuiInputnumberButtonWidth: '28px', nutuiInputnumberButtonHeight: '28px', nutuiInputnumberInputWidth: '40px', nutuiInputnumberInputHeight: '28px', nutuiInputnumberInputBorderRadius: '4px', nutuiInputnumberButtonBorderRadius: '4px', } // 优惠券相关状态 const [selectedCoupon, setSelectedCoupon] = useState(null) const [couponVisible, setCouponVisible] = useState(false) const [availableCoupons, setAvailableCoupons] = useState([]) const [couponLoading, setCouponLoading] = useState(false) // 门店选择:用于在下单页展示当前“已选门店”,并允许用户切换(写入 SelectedStore Storage) const [storePopupVisible, setStorePopupVisible] = useState(false) const [stores, setStores] = useState([]) const [storeLoading, setStoreLoading] = useState(false) const [selectedStore, setSelectedStore] = useState(getSelectedStoreFromStorage()) const router = Taro.getCurrentInstance().router; const goodsId = router?.params?.goodsId; const isTicketTemplateActive = !!ticketTemplate && ticketTemplate.enabled !== false && ticketTemplate.status !== 1 && ticketTemplate.deleted !== 1 // 套票活动最低购买量:优先取模板配置 const ticketMinBuyQty = (() => { const n = Number(ticketTemplate?.minBuyQty) return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1 })() const minBuyQty = isTicketTemplateActive ? ticketMinBuyQty : 1 const getGiftTicketQty = (buyQty: number) => { if (!isTicketTemplateActive) return 0 const multiplier = Number(ticketTemplate?.giftMultiplier || 0) const startSend = Number(ticketTemplate?.startSendQty || 0) if (multiplier > 0) return Math.max(0, buyQty) * multiplier return Math.max(0, startSend) } const loadStores = async () => { if (storeLoading) return try { setStoreLoading(true) const list = await listShopStore() setStores((list || []).filter(s => s?.isDelete !== 1)) } catch (e) { console.error('获取门店列表失败:', e) setStores([]) Taro.showToast({title: '获取门店列表失败', icon: 'none'}) } finally { setStoreLoading(false) } } // @ts-ignore const openStorePopup = async () => { setStorePopupVisible(true) if (!stores.length) { await loadStores() } } // 计算商品总价 const getGoodsTotal = () => { if (!goods) return 0 const price = parseFloat(goods.price || '0') // const total = price * quantity // 🔍 详细日志,用于排查数值精度问题 // console.log('💵 商品总价计算:', { // goodsPrice: goods.price, // goodsPriceType: typeof goods.price, // parsedPrice: price, // quantity: quantity, // total: total, // totalFixed2: total.toFixed(2), // totalString: total.toString() // }) return price * quantity } // 计算优惠券折扣 const getCouponDiscount = () => { if (!selectedCoupon || !goods) return 0 const total = getGoodsTotal() return calculateCouponDiscount(selectedCoupon, total) } // 计算实付金额 const getFinalPrice = () => { const total = getGoodsTotal() const discount = getCouponDiscount() return Math.max(0, total - discount) } const handleSelect = (item: any) => { setPayment(payments.find(payment => payment.name === item.name)) setIsVisible(false) } // 处理数量变化 const handleQuantityChange = (value: string | number) => { const fallback = isTicketTemplateActive ? minBuyQty : 1 const newQuantity = typeof value === 'string' ? parseInt(value, 10) || fallback : value const finalQuantity = Math.max(fallback, Math.min(newQuantity, goods?.stock || 999)) setQuantity(finalQuantity) // 数量变化时,重新排序优惠券并检查当前选中的优惠券是否还可用 if (availableCoupons.length > 0) { const newTotal = parseFloat(goods?.price || '0') * finalQuantity const sortedCoupons = sortCoupons(availableCoupons, newTotal) const usableCoupons = filterUsableCoupons(sortedCoupons, newTotal) setAvailableCoupons(sortedCoupons) // 检查当前选中的优惠券是否还可用 if (selectedCoupon && !isCouponUsable(selectedCoupon, newTotal)) { setSelectedCoupon(null) Taro.showToast({ title: '当前优惠券不满足使用条件,已自动取消', icon: 'none' }) // 🎯 自动推荐新的最优优惠券 if (usableCoupons.length > 0) { const bestCoupon = usableCoupons[0] const discount = calculateCouponDiscount(bestCoupon, newTotal) if (discount > 0) { setSelectedCoupon(bestCoupon) Taro.showToast({ title: `已为您重新推荐最优优惠券,可省¥${discount.toFixed(2)}`, icon: 'success', duration: 3000 }) } } } else if (!selectedCoupon && usableCoupons.length > 0) { // 🔔 如果没有选中优惠券但有可用的,推荐最优的 const bestCoupon = usableCoupons[0] const discount = calculateCouponDiscount(bestCoupon, newTotal) if (discount > 0) { setSelectedCoupon(bestCoupon) Taro.showToast({ title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`, icon: 'success', duration: 3000 }) } } else if (selectedCoupon && usableCoupons.length > 0) { // 🔍 检查是否有更好的优惠券 const bestCoupon = usableCoupons[0] const currentDiscount = calculateCouponDiscount(selectedCoupon, newTotal) const bestDiscount = calculateCouponDiscount(bestCoupon, newTotal) // 如果有更好的优惠券(优惠超过0.01元) if (bestDiscount > currentDiscount + 0.01 && bestCoupon.id !== selectedCoupon.id) { Taro.showModal({ title: '发现更优惠的优惠券', content: `有更好的优惠券可用,额外节省¥${(bestDiscount - currentDiscount).toFixed(2)},是否更换?`, success: (res) => { if (res.confirm) { setSelectedCoupon(bestCoupon) Taro.showToast({ title: '优惠券已更换', icon: 'success' }) } } }) } } } } // 处理优惠券选择 const handleCouponSelect = (coupon: CouponCardProps) => { const total = getGoodsTotal() // 🔍 详细日志记录,用于排查问题 console.log('🎫 手动选择优惠券详细信息:', { coupon: { id: coupon.id, title: coupon.title, type: coupon.type, amount: coupon.amount, minAmount: coupon.minAmount, status: coupon.status }, orderInfo: { goodsPrice: goods?.price, quantity: quantity, total: total, totalFixed: total.toFixed(2) }, validation: { isUsable: isCouponUsable(coupon, total), discount: calculateCouponDiscount(coupon, total), reason: getCouponUnusableReason(coupon, total) } }) // 检查是否可用 if (!isCouponUsable(coupon, total)) { const reason = getCouponUnusableReason(coupon, total) // 🚨 记录手动选择失败的详细信息 console.error('🚨 手动选择优惠券失败:', { reason, coupon, total, minAmount: coupon.minAmount, comparison: { totalVsMinAmount: `${total} < ${coupon.minAmount}`, result: total < (coupon.minAmount || 0) } }) Taro.showToast({ title: reason || '优惠券不可用', icon: 'none' }) return } setSelectedCoupon(coupon) setCouponVisible(false) Taro.showToast({ title: '优惠券选择成功', icon: 'success' }) } // 取消选择优惠券 const handleCouponCancel = () => { setSelectedCoupon(null) Taro.showToast({ title: '已取消使用优惠券', icon: 'success' }) } // 加载用户优惠券 const loadUserCoupons = async (totalOverride?: number) => { try { setCouponLoading(true) // 使用新的API获取可用优惠券 const res = await getMyAvailableCoupons() if (res && res.length > 0) { // 转换数据格式 const transformedCoupons = res.map(transformCouponData) // 按优惠金额排序 const total = totalOverride ?? getGoodsTotal() const sortedCoupons = sortCoupons(transformedCoupons, total) const usableCoupons = filterUsableCoupons(sortedCoupons, total) setAvailableCoupons(sortedCoupons) // 🎯 智能推荐:自动应用最优惠的可用优惠券 if (usableCoupons.length > 0 && !selectedCoupon) { const bestCoupon = usableCoupons[0] // 已经按优惠金额排序,第一个就是最优的 const discount = calculateCouponDiscount(bestCoupon, total) // 🔍 详细日志记录自动推荐的信息 console.log('🤖 自动推荐优惠券详细信息:', { coupon: { id: bestCoupon.id, title: bestCoupon.title, type: bestCoupon.type, amount: bestCoupon.amount, minAmount: bestCoupon.minAmount, status: bestCoupon.status }, orderInfo: { goodsPrice: goods?.price, quantity: quantity, total: total, totalFixed: total.toFixed(2) }, validation: { isUsable: isCouponUsable(bestCoupon, total), discount: discount, reason: getCouponUnusableReason(bestCoupon, total) } }) if (discount > 0) { setSelectedCoupon(bestCoupon) // 显示智能推荐提示 Taro.showToast({ title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`, icon: 'success', duration: 3000 }) } } // 🔔 优惠券提示:如果有可用优惠券,显示提示 if (usableCoupons.length > 0) { console.log(`发现${usableCoupons.length}张可用优惠券,已为您推荐最优惠券`) } console.log('加载优惠券成功:', { originalData: res, transformedData: transformedCoupons, sortedData: sortedCoupons, usableCoupons: usableCoupons, recommendedCoupon: usableCoupons[0] || null }) } else { setAvailableCoupons([]) console.log('暂无可用优惠券') } } catch (error) { console.error('加载优惠券失败:', error) setAvailableCoupons([]) Taro.showToast({ title: '加载优惠券失败', icon: 'none' }) } finally { setCouponLoading(false) } } /** * 统一支付入口 */ const onPay = async (goods: ShopGoods) => { try { setPayLoading(true) // 基础校验 if (!address) { Taro.showToast({ title: '请选择收货地址', icon: 'error' }) return; } if (!payment) { Taro.showToast({ title: '请选择支付方式', icon: 'error' }) return; } // 水票套票活动:最小购买量校验 if (isTicketTemplateActive && quantity < minBuyQty) { Taro.showToast({ title: `最低购买量:${minBuyQty}桶`, icon: 'none' }) return } // 库存校验 if (goods.stock !== undefined && quantity > goods.stock) { Taro.showToast({ title: '商品库存不足', icon: 'error' }) return; } // 优惠券校验 if (selectedCoupon) { const total = getGoodsTotal() if (!isCouponUsable(selectedCoupon, total)) { const reason = getCouponUnusableReason(selectedCoupon, total) Taro.showToast({ title: reason || '优惠券不可用', icon: 'error' }) return; } } else { // 🔔 支付前最后一次检查:提醒用户是否有可用优惠券 const total = getGoodsTotal() const usableCoupons = filterUsableCoupons(availableCoupons, total) if (usableCoupons.length > 0) { const bestCoupon = usableCoupons[0] const discount = calculateCouponDiscount(bestCoupon, total) if (discount > 0) { // 用模态框提醒用户 const confirmResult = await new Promise((resolve) => { Taro.showModal({ title: '发现可用优惠券', content: `您有优惠券可使用,可省¥${discount.toFixed(2)},是否使用?`, success: (res) => resolve(res.confirm), fail: () => resolve(false) }) }) if (confirmResult) { setSelectedCoupon(bestCoupon) // 🔄 使用优惠券后需要重新构建订单数据,这里直接递归调用支付函数 // 但要确保传递最新的优惠券信息 const currentPaymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT; const updatedOrderData = buildSingleGoodsOrder( goods.goodsId!, quantity, address.id, { comments: goods.name, deliveryType: 0, buyerRemarks: orderRemark, couponId: parseInt(String(bestCoupon.id), 10) } ); console.log('🎯 使用推荐优惠券的订单数据:', updatedOrderData); // 执行支付 await PaymentHandler.pay(updatedOrderData, currentPaymentType); return; // 提前返回,避免重复执行支付 } else { // 用户选择不使用优惠券,继续支付 } } } } // 构建订单数据 const orderData = buildSingleGoodsOrder( goods.goodsId!, quantity, address.id, { comments: '桂乐淘', deliveryType: 0, buyerRemarks: orderRemark, // 🔧 确保 couponId 是正确的数字类型,且不传递 undefined couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined } ); // 根据支付方式选择支付类型 const paymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT; // 🔍 支付前的详细信息记录 console.log('💰 开始支付 - 详细信息:', { orderData, paymentType, selectedCoupon: selectedCoupon ? { id: selectedCoupon.id, title: selectedCoupon.title, type: selectedCoupon.type, amount: selectedCoupon.amount, minAmount: selectedCoupon.minAmount, discount: getCouponDiscount() } : null, priceCalculation: { goodsPrice: goods?.price, quantity: quantity, goodsTotal: getGoodsTotal(), couponDiscount: getCouponDiscount(), finalPrice: getFinalPrice() }, couponValidation: selectedCoupon ? { isUsable: isCouponUsable(selectedCoupon, getGoodsTotal()), reason: getCouponUnusableReason(selectedCoupon, getGoodsTotal()) } : null }); // 执行支付 - 移除这里的成功提示,让PaymentHandler统一处理 await PaymentHandler.pay(orderData, paymentType); // ✅ 移除双重成功提示 - PaymentHandler会处理成功提示 // Taro.showToast({ // title: '支付成功', // icon: 'success' // }) } catch (error: any) { // return navTo('/user/order/order?statusFilter=0', true) // console.error('支付失败:', error) // 只处理PaymentHandler未处理的错误 // if (!error.handled) { // let errorMessage = '支付失败,请重试'; // // // 根据错误类型提供具体提示 // if (error.message?.includes('余额不足')) { // errorMessage = '账户余额不足,请充值后重试'; // } else if (error.message?.includes('优惠券')) { // errorMessage = '优惠券使用失败,请重新选择'; // } else if (error.message?.includes('库存')) { // errorMessage = '商品库存不足,请减少购买数量'; // } else if (error.message?.includes('地址')) { // errorMessage = '收货地址信息有误,请重新选择'; // } else if (error.message) { // errorMessage = error.message; // } // Taro.showToast({ // title: errorMessage, // icon: 'error' // }) // console.log('跳去未付款的订单列表页面') // } } finally { setPayLoading(false) } }; // 统一的数据加载函数 const loadAllData = async () => { try { setLoading(true) setError('') // 分别加载数据,避免类型推断问题 let goodsRes: ShopGoods | null = null if (goodsId) { goodsRes = await getShopGoods(Number(goodsId)) } const [addressRes, paymentRes] = await Promise.all([ listShopUserAddress({isDefault: true}), selectPayment({}) ]) // 设置商品信息 // 查询当前商品是否存在水票套票活动(失败/无数据时不影响正常下单) let tpl: GltTicketTemplate | null = null if (goodsId) { try { tpl = await getGltTicketTemplateByGoodsId(Number(goodsId)) } catch (e) { tpl = null } } const tplActive = !!tpl && tpl.enabled !== false && tpl.status !== 1 && tpl.deleted !== 1 const tplMinBuyQty = (() => { const n = Number(tpl?.minBuyQty) return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1 })() // 设置商品信息(若存在套票模板,则默认 canBuyNumber 使用模板最小购买量) if (goodsRes) { const patchedGoods: ShopGoods = { ...goodsRes } if (tplActive && ((patchedGoods.canBuyNumber ?? 0) === 0)) { patchedGoods.canBuyNumber = tplMinBuyQty } setGoods(patchedGoods) // 设置默认购买数量:优先使用 canBuyNumber,否则使用 1 const initQty = (patchedGoods.canBuyNumber ?? 0) > 0 ? (patchedGoods.canBuyNumber as number) : 1 setQuantity(initQty) } setTicketTemplate(tpl) // 设置默认收货地址 if (addressRes && addressRes.length > 0) { setAddress(addressRes[0]) } // 设置支付方式 if (paymentRes && paymentRes.length > 0) { setPayments(paymentRes.map((d) => ({ type: d.type, name: d.name }))) setPayment(paymentRes[0]) } // 加载优惠券:使用“初始数量”对应的总价做推荐,避免默认数量变化导致推荐不准 if (goodsRes) { const initQty = (() => { const n = Number(goodsRes?.canBuyNumber) if (Number.isFinite(n) && n > 0) return Math.floor(n) if (tplActive) return tplMinBuyQty return 1 })() const total = parseFloat(goodsRes.price || '0') * initQty await loadUserCoupons(total) } } catch (err) { console.error('加载数据失败:', err) setError('加载数据失败,请重试') } finally { setLoading(false) } } useDidShow(() => { // 返回/切换到该页面时,刷新一下当前已选门店 setSelectedStore(getSelectedStoreFromStorage()) loadAllData() }) useEffect(() => { loadAllData() }, [goodsId]); // 重新加载数据 const handleRetry = () => { loadAllData() } // 错误状态 if (error) { return ( {error} ) } // 加载状态 if (loading || !goods) { return } return (
{ address && ( Taro.navigateTo({url: '/user/address/index'})}> 送至 {address.province} {address.city} {address.region} {address.address} {address.name} {address.phone} ) } {!address && ( Taro.navigateTo({url: '/user/address/index'})}> 添加收货地址 )} {/**/} {/* */} {/* */} {/* 门店*/} {/* */} {/* )}*/} {/* extra={(*/} {/* */} {/* */} {/* {selectedStore?.name || '请选择门店'}*/} {/* */} {/* */} {/* */} {/* )}*/} {/* onClick={openStorePopup}*/} {/* />*/} {/**/} {goods.name} 80g/袋 ¥{goods.price} {goods.stock !== undefined && ( 库存 {goods.stock} 件 )} {isTicketTemplateActive && ( 最低购买量:{minBuyQty}桶 赠送水票:{getGiftTicketQty(quantity)}张 )} {payment?.name} )} onClick={() => setIsVisible(true)} /> ¥{getGoodsTotal().toFixed(2)}} /> {selectedCoupon ? `-¥${getCouponDiscount().toFixed(2)}` : '暂未使用'} {(() => { const usableCoupons = filterUsableCoupons(availableCoupons, getGoodsTotal()) if (usableCoupons.length > 0 && !selectedCoupon) { return ( {usableCoupons.length}张可用 ) } else if (usableCoupons.length > 0) { return ( 已选择 ) } else { return } })() } )} onClick={() => setCouponVisible(true)} /> 已优惠 ¥{getCouponDiscount().toFixed(2)} 实付 ¥{getFinalPrice().toFixed(2)} )}/> setOrderRemark(value)} maxLength={100} /> )}/> {ticketTemplate && ( 注意事项: 1.最低起送量≥20桶; 2.配送范围要在电子围栏内; 3.上楼费暂不收取,收费另行通知。
)}/> )} {/* 支付方式选择 */} setIsVisible(false)} /> {/* 优惠券选择弹窗 */} setCouponVisible(false)} style={{height: '60vh'}} > 选择优惠券 {couponLoading ? ( 加载优惠券中... ) : ( <> {selectedCoupon && ( 当前使用 {selectedCoupon.title} -¥{calculateCouponDiscount(selectedCoupon, getGoodsTotal()).toFixed(2)} )} {(() => { const total = getGoodsTotal() const usableCoupons = filterUsableCoupons(availableCoupons, total) const unusableCoupons = filterUnusableCoupons(availableCoupons, total) return ( <> {unusableCoupons.length > 0 && ( ({ ...coupon, status: 2 as const }))} layout="vertical" showEmpty={false} /> )} ) })()} )} {/* 门店选择弹窗 */} setStorePopupVisible(false)} > 选择门店 setStorePopupVisible(false)} > 关闭 {storeLoading ? ( 加载中... ) : ( {stores.map((s) => { const isActive = !!selectedStore?.id && selectedStore.id === s.id return ( {s.name || `门店${s.id}`}} description={s.address || ''} onClick={async () => { let storeToSave: ShopStore = s if (s?.id) { try { const full = await getShopStore(s.id) if (full) storeToSave = full } catch (_e) { // keep base item } } setSelectedStore(storeToSave) saveSelectedStoreToStorage(storeToSave) setStorePopupVisible(false) Taro.showToast({title: '门店已切换', icon: 'success'}) }} /> ) })} {!stores.length && ( 暂无门店数据} /> )} )}
实付金额: ¥{getFinalPrice().toFixed(2)} {selectedCoupon && ( 已优惠 ¥{getCouponDiscount().toFixed(2)} )}
); }; export default OrderConfirm;