diff --git a/src/user/ticket/use.tsx b/src/user/ticket/use.tsx index 2a67781..952e9a8 100644 --- a/src/user/ticket/use.tsx +++ b/src/user/ticket/use.tsx @@ -1,58 +1,44 @@ -import {useEffect, useState} from "react"; +import { useEffect, useMemo, useState } from 'react' +import Taro, { useDidShow } from '@tarojs/taro' +import { View, Text } from '@tarojs/components' import { - Image, Button, Cell, CellGroup, + ConfigProvider, Input, - Space, - ActionSheet, - Popup, InputNumber, - ConfigProvider + Popup, + Space } from '@nutui/nutui-react-taro' -import {Location, ArrowRight, Shop} 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 { ArrowRight, Location, Shop, Ticket } from '@nutui/icons-react-taro' +import type { ShopGoods } from '@/api/shop/shopGoods/model' +import { getShopGoods } from '@/api/shop/shopGoods' +import { listShopUserAddress } from '@/api/shop/shopUserAddress' +import type { ShopUserAddress } from '@/api/shop/shopUserAddress/model' import './use.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 { - transformCouponData, - calculateCouponDiscount, - isCouponUsable, - getCouponUnusableReason, - sortCoupons, - filterUsableCoupons, - filterUnusableCoupons -} from "@/utils/couponUtils"; -import navTo from "@/utils/common"; import type {ShopStore} from "@/api/shop/shopStore/model"; import {getShopStore, listShopStore} from "@/api/shop/shopStore"; import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection"; +import type { GltUserTicket } from '@/api/glt/gltUserTicket/model' +import { listGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket' +import { addGltUserTicketLog } from '@/api/glt/gltUserTicketLog' +import { createOrder, listShopOrder } from '@/api/shop/shopOrder' +import type { OrderCreateRequest } from '@/api/shop/shopOrder/model' +// payType=12 in this project is "free order" (no payment). Used for water-ticket orders. +const PAY_TYPE_FREE = 12 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 [submitLoading, setSubmitLoading] = useState(false) // InputNumber 主题配置 const customTheme = { @@ -64,20 +50,87 @@ const OrderConfirm = () => { 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 [tickets, setTickets] = useState([]) + const [ticketPopupVisible, setTicketPopupVisible] = useState(false) + const [ticketLoading, setTicketLoading] = useState(false) + const router = Taro.getCurrentInstance().router; const goodsId = router?.params?.goodsId; + const numericGoodsId = useMemo(() => { + const n = goodsId ? Number(goodsId) : undefined + return typeof n === 'number' && Number.isFinite(n) ? n : undefined + }, [goodsId]) + + const userId = useMemo(() => { + const raw = Taro.getStorageSync('UserId') + const id = Number(raw) + return Number.isFinite(id) && id > 0 ? id : undefined + }, []) + + const usableTickets = useMemo(() => { + const list = (tickets || []) + .filter(t => t?.deleted !== 1) + .filter(t => t?.status !== 1) + .filter(t => Number.isFinite(Number(t?.id)) && Number(t.id) > 0) + .filter(t => (t.availableQty ?? 0) > 0) + .filter(t => (numericGoodsId ? t.goodsId === numericGoodsId : true)) + // FIFO: use older tickets first (reduce disputes). + return list.sort((a, b) => { + const ta = new Date(a.createTime || 0).getTime() || 0 + const tb = new Date(b.createTime || 0).getTime() || 0 + if (ta !== tb) return ta - tb + return (a.id || 0) - (b.id || 0) + }) + }, [tickets, numericGoodsId]) + + const availableTicketTotal = useMemo(() => { + return usableTickets.reduce((sum, t) => sum + Number(t.availableQty || 0), 0) + }, [usableTickets]) + + const maxQuantity = useMemo(() => { + const stockMax = goods?.stock ?? 999 + return Math.max(0, Math.min(stockMax, availableTicketTotal)) + }, [availableTicketTotal, goods?.stock]) + + const displayQty = useMemo(() => { + if (maxQuantity <= 0) return 0 + return Math.max(1, Math.min(quantity, maxQuantity)) + }, [quantity, maxQuantity]) + + type ConsumePlanItem = { + ticket: GltUserTicket + qty: number + availableAfter: number + usedAfter: number + } + + const buildConsumePlan = (needQty: number): ConsumePlanItem[] => { + let remaining = Math.max(0, needQty) + const plan: ConsumePlanItem[] = [] + for (const t of usableTickets) { + if (!remaining) break + const available = Number(t.availableQty || 0) + const used = Number(t.usedQty || 0) + if (available <= 0) continue + const take = Math.min(available, remaining) + remaining -= take + plan.push({ + ticket: t, + qty: take, + availableAfter: available - take, + usedAfter: used + take + }) + } + if (remaining > 0) return [] + return plan + } const loadStores = async () => { if (storeLoading) return @@ -101,463 +154,188 @@ const OrderConfirm = () => { } } - // 计算商品总价 - 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 newQuantity = typeof value === 'string' ? parseInt(value) || 1 : value - const finalQuantity = Math.max(1, Math.min(newQuantity, goods?.stock || 999)) - setQuantity(finalQuantity) + const parsed = typeof value === 'string' ? parseInt(value) : value + const newQuantity = Number.isFinite(parsed) ? Number(parsed) : 0 + const upper = maxQuantity + if (upper <= 0) { + setQuantity(0) + return + } + setQuantity(Math.max(1, Math.min(newQuantity || 1, upper))) + } - // 数量变化时,重新排序优惠券并检查当前选中的优惠券是否还可用 - 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 loadUserTickets = async () => { + if (ticketLoading) return + if (!userId) { + setTickets([]) + return + } + try { + setTicketLoading(true) + const list = await listGltUserTicket({ userId, status: 0 }) + setTickets(list || []) + } catch (e) { + console.error('获取水票失败:', e) + setTickets([]) + Taro.showToast({ title: '获取水票失败', icon: 'none' }) + } finally { + setTicketLoading(false) } } - // 处理优惠券选择 - const handleCouponSelect = (coupon: CouponCardProps) => { - const total = getGoodsTotal() + const findOrderIdByOrderNo = async (orderNo: string): Promise => { + try { + const list = await listShopOrder({ orderNo, userId } as any) + const first = (list || []).find(o => o?.orderNo === orderNo) + return first?.orderId + } catch (_e) { + return undefined + } + } - // 🔍 详细日志记录,用于排查问题 - 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) - } - }) + const consumeTicketsForOrder = async ( + needQty: number, + orderNo: string, + orderId?: number + ) => { + const plan = buildConsumePlan(needQty) + if (!plan.length) throw new Error('水票可用次数不足') - // 检查是否可用 - if (!isCouponUsable(coupon, total)) { - const reason = getCouponUnusableReason(coupon, total) + // NOTE: This is a client-side best-effort implementation. + // For strict consistency (order + ticket deduction + log in one transaction), + // please implement a backend API to do these steps atomically. + for (const item of plan) { + const t = item.ticket + const availableBefore = Number(t.availableQty || 0) - // 🚨 记录手动选择失败的详细信息 - console.error('🚨 手动选择优惠券失败:', { - reason, - coupon, - total, - minAmount: coupon.minAmount, - comparison: { - totalVsMinAmount: `${total} < ${coupon.minAmount}`, - result: total < (coupon.minAmount || 0) - } + await updateGltUserTicket({ + ...t, + availableQty: item.availableAfter, + usedQty: item.usedAfter }) - Taro.showToast({ - title: reason || '优惠券不可用', - icon: 'none' + // Write-off log (核销记录) + await addGltUserTicketLog({ + userTicketId: t.id, + changeType: 2, // 约定:2=消费/核销(若后端有枚举,请按后端约定调整) + changeAvailable: -item.qty, + changeUsed: item.qty, + availableAfter: item.availableAfter, + usedAfter: item.usedAfter, + orderId, + orderNo, + userId: userId || t.userId, + comments: `水票下单核销:${item.qty} 张(${availableBefore}→${item.availableAfter})` }) + } + } + + const onSubmit = async () => { + if (submitLoading) return + if (!goods?.goodsId) return + + // 基础校验 + if (!userId) { + Taro.showToast({ title: '请先登录', icon: 'none' }) + return + } + if (!selectedStore?.id) { + Taro.showToast({ title: '请选择门店', icon: 'none' }) + return + } + if (!address?.id) { + Taro.showToast({ title: '请选择收货地址', icon: 'none' }) + return + } + if (availableTicketTotal <= 0) { + Taro.showToast({ title: '暂无可用水票', icon: 'none' }) return } - setSelectedCoupon(coupon) - setCouponVisible(false) - Taro.showToast({ - title: '优惠券选择成功', - icon: 'success' - }) - } + const finalQty = displayQty + if (finalQty <= 0) { + Taro.showToast({ title: '请选择送水数量', icon: 'none' }) + return + } + if (finalQty > availableTicketTotal) { + Taro.showToast({ title: '水票可用次数不足', icon: 'none' }) + return + } + if (goods.stock !== undefined && finalQty > goods.stock) { + Taro.showToast({ title: '商品库存不足', icon: 'none' }) + return + } - // 取消选择优惠券 - const handleCouponCancel = () => { - setSelectedCoupon(null) - Taro.showToast({ - title: '已取消使用优惠券', - icon: 'success' + const confirmRes = await Taro.showModal({ + title: '确认下单', + content: `将使用 ${finalQty} 张水票下单,送水 ${finalQty} 桶,是否确认?` }) - } + if (!confirmRes.confirm) return - // 加载用户优惠券 - const loadUserCoupons = async () => { try { - setCouponLoading(true) + setSubmitLoading(true) + Taro.showLoading({ title: '提交中...' }) - // 使用新的API获取可用优惠券 - const res = await getMyAvailableCoupons() - - if (res && res.length > 0) { - // 转换数据格式 - const transformedCoupons = res.map(transformCouponData) - - // 按优惠金额排序 - const total = 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('暂无可用优惠券') + const orderData: OrderCreateRequest = { + goodsItems: [{ goodsId: goods.goodsId, quantity: finalQty }], + addressId: address.id, + storeId: selectedStore.id, + storeName: selectedStore.name, + payType: PAY_TYPE_FREE, + deliveryType: 0, + comments: orderRemark || '水票下单' } - } catch (error) { - console.error('加载优惠券失败:', error) - setAvailableCoupons([]) - Taro.showToast({ - title: '加载优惠券失败', - icon: 'none' - }) + + const res = await createOrder(orderData) + const orderNo = res?.orderNo + if (!orderNo) throw new Error('下单失败,请稍后重试') + + const orderId = await findOrderIdByOrderNo(orderNo) + try { + await consumeTicketsForOrder(finalQty, orderNo, orderId) + } catch (consumeErr: any) { + console.error('订单已创建,但水票核销失败:', { orderNo, consumeErr }) + await Taro.showModal({ + title: '下单已成功', + content: `订单已创建(${orderNo}),但水票扣除/核销记录写入失败,请联系管理员处理。`, + showCancel: false + }) + // 避免用户重复下单:直接跳转到订单列表查看处理结果 + Taro.redirectTo({ url: '/user/order/order' }) + return + } + + await loadUserTickets() + + Taro.showToast({ title: '下单成功', icon: 'success' }) + setTimeout(() => { + // 跳转到“我的送水订单”(当前项目使用“我的订单”页承载) + Taro.redirectTo({ url: '/user/order/order' }) + }, 800) + } catch (e: any) { + console.error('水票下单失败:', e) + Taro.showToast({ title: e?.message || '下单失败', icon: 'none' }) } finally { - setCouponLoading(false) + Taro.hideLoading() + setSubmitLoading(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 (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)) + if (numericGoodsId) { + goodsRes = await getShopGoods(numericGoodsId) } - const [addressRes, paymentRes] = await Promise.all([ - listShopUserAddress({isDefault: true}), - selectPayment({}) + const [addressRes] = await Promise.all([ + listShopUserAddress({ isDefault: true }) ]) // 设置商品信息 @@ -569,20 +347,13 @@ const OrderConfirm = () => { if (addressRes && addressRes.length > 0) { setAddress(addressRes[0]) } + await loadUserTickets() - // 设置支付方式 - if (paymentRes && paymentRes.length > 0) { - setPayments(paymentRes.map((d) => ({ - type: d.type, - name: d.name - }))) - setPayment(paymentRes[0]) - } - - // 加载优惠券(在商品信息加载完成后) - if (goodsRes) { - await loadUserCoupons() - } + // Clamp quantity after loading tickets/stock. + setQuantity(prev => { + const upper = maxQuantity > 0 ? maxQuantity : 1 + return Math.max(1, Math.min(prev, upper)) + }) } catch (err) { console.error('加载数据失败:', err) setError('加载数据失败,请重试') @@ -601,6 +372,15 @@ const OrderConfirm = () => { loadAllData() }, [goodsId]); + // When tickets/stock change, clamp quantity into [0..maxQuantity]. + useEffect(() => { + setQuantity(prev => { + if (maxQuantity <= 0) return 0 + if (!prev || prev < 1) return 1 + return Math.min(prev, maxQuantity) + }) + }, [maxQuantity]) + // 重新加载数据 const handleRetry = () => { loadAllData() @@ -681,10 +461,10 @@ const OrderConfirm = () => { extra={( @@ -692,31 +472,26 @@ const OrderConfirm = () => { /> - {/**/} - {/* */} - {/* {payment?.name}*/} - {/* */} - {/* */} - {/* )}*/} - {/* onClick={() => setIsVisible(true)}*/} - {/* />*/} - {/**/} - 送 {quantity} 桶} + title={( + + + 可用水票 + + )} + extra={( + + {availableTicketTotal} 张 + + + )} + onClick={() => setTicketPopupVisible(true)} + /> + {displayQty} 张} /> - {/**/} - - 需使用 - 1张 - - )}/> @@ -731,6 +506,46 @@ const OrderConfirm = () => { )}/> + {/* 水票明细弹窗 */} + setTicketPopupVisible(false)} + > + + + 水票明细 + setTicketPopupVisible(false)} + > + 关闭 + + + + {ticketLoading ? ( + + 加载中... + + ) : ( + + {usableTickets.map((t) => ( + {t.templateName || '水票'}} + description={t.orderNo ? `来源订单:${t.orderNo}` : ''} + extra={可用 {t.availableQty ?? 0}} + /> + ))} + {!usableTickets.length && ( + 暂无可用水票} /> + )} + + )} + + + {/* 门店选择弹窗 */} {
使用水票: - {getFinalPrice().toFixed(2)} + + {displayQty} + +