feat(shop): 优化VIP价格支持及线下付款兼容
- 结算页新增线下付款异常友好提示,避免支付配置中appid缺失错误 - 后端修复支付类型判断,防止线下付款仍试图创建微信支付订单 - 结算页默认支付方式调整为线下付款,货到付款选项注释 - 新增门店订单“修改金额”功能,支持弹窗输入新金额和修改原因 - 订单修改支持追加操作记录,接口复用updateShopOrder,无需新接口 - 重构多处页面和组件,使用响应式useVipStatus替代同步isVipMember缓存 - 新增OrderGoodsItem.price字段,下单时传VIP dealerPrice确保价格准确 - 购物车、商品详情、分类页、SkuSelector、商品卡片等支持动态VIP价格展示 - 修改购物车上下文calcPrice函数,响应VIP状态变更自动更新价格显示 - 提升前端VIP状态管理一致性,防止过期缓存导致VIP特权价格显示异常 - 验证构建通过,后端服务重启后生效线下付款修复和订单修改功能
This commit is contained in:
@@ -10,7 +10,7 @@ import Loading from '@/components/common/Loading'
|
||||
import { isGuest } from '@/utils/auth'
|
||||
import { getCompressedImageUrl } from '@/utils/image'
|
||||
import { requireLogin } from '@/utils/login-guard'
|
||||
import { isVipMember } from '@/utils/vip'
|
||||
import { useVipStatus } from '@/hooks/useVipStatus'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '购物车',
|
||||
@@ -19,6 +19,8 @@ definePageConfig({
|
||||
const CartPage: React.FC = () => {
|
||||
const { items, selectedCount, selectedPrice, updateQuantity, toggleSelect, selectAll, removeItem, refresh, loading, removeSelected, addItem } = useCartContext()
|
||||
const { isLoggedIn } = useUserContext()
|
||||
// VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染
|
||||
const { isVip } = useVipStatus()
|
||||
const [recommendGoods, setRecommendGoods] = useState<ShopGoods[]>([])
|
||||
const [recommendPage, setRecommendPage] = useState(1)
|
||||
const [refreshingRecommend, setRefreshingRecommend] = useState(false)
|
||||
@@ -189,7 +191,7 @@ const CartPage: React.FC = () => {
|
||||
)}
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-sm font-bold text-red-500">
|
||||
¥{isVipMember() && (item.dealerPrice || (item.product as any)?.dealerPrice) ? (item.dealerPrice || (item.product as any).dealerPrice) : (item.skuPrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || '0')}
|
||||
¥{isVip && (item.dealerPrice || (item.product as any)?.dealerPrice) ? (item.dealerPrice || (item.product as any).dealerPrice) : (item.skuPrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || '0')}
|
||||
</Text>
|
||||
<View className="flex items-center gap-3">
|
||||
<View className="w-6 h-6 rounded bg-gray-100 flex items-center justify-center" onClick={() => updateQuantity(item.goodsId, item.skuId, item.quantity - 1)}>
|
||||
@@ -238,7 +240,7 @@ const CartPage: React.FC = () => {
|
||||
</Text>
|
||||
<View className="flex items-center justify-between mt-1">
|
||||
<Text className="text-xs font-bold text-red-500">
|
||||
¥{isVipMember() && goods.dealerPrice ? goods.dealerPrice : (goods.salePrice || goods.price || '0')}
|
||||
¥{isVip && goods.dealerPrice ? goods.dealerPrice : (goods.salePrice || goods.price || '0')}
|
||||
</Text>
|
||||
<View
|
||||
className="w-5 h-5 rounded-full flex items-center justify-center"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useCartContext } from '@/contexts/CartContext'
|
||||
import type { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model'
|
||||
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
||||
import { isGuest } from '@/utils/auth'
|
||||
import { isVipMember } from '@/utils/vip'
|
||||
import { useVipStatus } from '@/hooks/useVipStatus'
|
||||
import { getCompressedImageUrl } from '@/utils/image'
|
||||
import { requireLogin } from '@/utils/login-guard'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
@@ -29,6 +29,8 @@ const SORT_TABS: { key: SortKey; label: string }[] = [
|
||||
|
||||
const CategoryPage: React.FC = () => {
|
||||
const { addItem } = useCartContext()
|
||||
// VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染
|
||||
const { isVip } = useVipStatus()
|
||||
const [categories, setCategories] = useState<ShopGoodsCategory[]>([])
|
||||
const [activeId, setActiveId] = useState<number | undefined>()
|
||||
|
||||
@@ -126,7 +128,7 @@ const CategoryPage: React.FC = () => {
|
||||
|
||||
/** 获取显示价格 */
|
||||
const getDisplayPrice = (product: ShopGoods) => {
|
||||
if (isVipMember() && product.dealerPrice) return product.dealerPrice
|
||||
if (isVip && product.dealerPrice) return product.dealerPrice
|
||||
return product.price || '0'
|
||||
}
|
||||
|
||||
@@ -242,7 +244,7 @@ const CategoryPage: React.FC = () => {
|
||||
<Text className='text-xs text-gray-400'>/{item.unitName}</Text>
|
||||
) : null}
|
||||
{/* 原价划掉 */}
|
||||
{isVipMember() && item.dealerPrice ? (
|
||||
{isVip && item.dealerPrice ? (
|
||||
<Text className='text-xs text-gray-400 line-through ml-1'>
|
||||
¥{item.price}
|
||||
</Text>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { listShopGoods } from '@/api/shop/shopGoods'
|
||||
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||||
import type { ShopUserCoupon } from '@/api/shop/shopUserCoupon/model'
|
||||
import type { OrderGoodsItem, OrderCreateRequest } from '@/api/shop/shopOrder/model'
|
||||
import { isVipMember } from '@/utils/vip'
|
||||
import { useVipStatus } from '@/hooks/useVipStatus'
|
||||
import { getCompressedImageUrl } from '@/utils/image'
|
||||
|
||||
// 满减门槛配置
|
||||
@@ -124,6 +124,9 @@ const CheckoutPage: React.FC = () => {
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [rushBuyProducts, setRushBuyProducts] = useState<ShopGoods[]>([])
|
||||
|
||||
// VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染
|
||||
const { isVip } = useVipStatus()
|
||||
|
||||
// 支付方式相关状态
|
||||
const [payType, setPayType] = useState<number>(9) // 9: 线下付款(默认),8: 货到付款(已注释)
|
||||
|
||||
@@ -153,14 +156,14 @@ const CheckoutPage: React.FC = () => {
|
||||
// 计算金额
|
||||
const goodsPrice = useMemo(() => {
|
||||
if (!items || items.length === 0) return 0
|
||||
const vip = isVipMember()
|
||||
const vip = isVip
|
||||
return items.reduce((sum, item) => {
|
||||
// VIP 会员优先使用 dealerPrice(后端关联字段 > product 嵌套对象)
|
||||
const dealerPrice = vip ? (item.dealerPrice || (item.product as any)?.dealerPrice) : null
|
||||
const unitPrice = dealerPrice || item.skuPrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || 0
|
||||
return sum + Number(unitPrice) * (item.quantity || item.num || 1)
|
||||
}, 0)
|
||||
}, [buyNowItems, selectedItems])
|
||||
}, [buyNowItems, selectedItems, isVip])
|
||||
|
||||
const couponDiscount = selectedCoupon?.reducePrice ? Number(selectedCoupon.reducePrice) : 0
|
||||
const totalPrice = Math.max(0, goodsPrice - couponDiscount).toFixed(2)
|
||||
@@ -262,12 +265,18 @@ const CheckoutPage: React.FC = () => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
// 构建商品列表
|
||||
const goodsItems: OrderGoodsItem[] = items.map(item => ({
|
||||
goodsId: item.goodsId,
|
||||
skuId: item.skuId && item.skuId > 0 ? item.skuId : undefined,
|
||||
quantity: item.quantity || item.num || 1,
|
||||
specInfo: item.skuSpec || item.sku?.sku || item.product?.specName,
|
||||
}))
|
||||
const goodsItems: OrderGoodsItem[] = items.map(item => {
|
||||
// VIP 会员优先使用 dealerPrice 作为下单单价
|
||||
const dealerPrice = isVip ? (item.dealerPrice || (item.product as any)?.dealerPrice) : null
|
||||
const unitPrice = dealerPrice || undefined
|
||||
return {
|
||||
goodsId: item.goodsId,
|
||||
skuId: item.skuId && item.skuId > 0 ? item.skuId : undefined,
|
||||
quantity: item.quantity || item.num || 1,
|
||||
specInfo: item.skuSpec || item.sku?.sku || item.product?.specName,
|
||||
price: unitPrice ? String(unitPrice) : undefined,
|
||||
}
|
||||
})
|
||||
|
||||
// 货到付款(8):后端会自动设置 payStatus=true,无需前端传递 payStatus/payTime
|
||||
// 线下付款(9):后端会设置 payStatus=false,等待商家确认收款后才设为 true
|
||||
@@ -295,7 +304,15 @@ const CheckoutPage: React.FC = () => {
|
||||
Taro.switchTab({ url: '/pages/order/list' })
|
||||
}, 1500)
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '提交失败', icon: 'none' })
|
||||
const message = err.message || '提交失败'
|
||||
// 线下付款(payType=9)下单时,后端如果仍尝试创建微信支付订单,
|
||||
// 会因支付配置缺失应用ID而报错。这里给出更明确的提示。
|
||||
if (payType === 9 && /应用ID|appid|appId|支付配置|微信支付订单|创建支付订单失败/.test(message)) {
|
||||
Taro.showToast({ title: '线下支付暂不可用:微信支付配置缺失', icon: 'none', duration: 3000 })
|
||||
console.error('[checkout] 线下付款下单失败,后端支付配置问题:', message)
|
||||
} else {
|
||||
Taro.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -367,7 +384,7 @@ const CheckoutPage: React.FC = () => {
|
||||
<View className='flex justify-between items-center mt-1'>
|
||||
<Text className='text-xs text-gray-500'>x{item.quantity || item.num || 1}</Text>
|
||||
<Text className='text-sm font-medium text-gray-800'>
|
||||
¥{isVipMember() && (item.dealerPrice || (item.product as any)?.dealerPrice) ? (item.dealerPrice || (item.product as any).dealerPrice) : (item.skuPrice || item.sku?.salePrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || '0')}
|
||||
¥{isVip && (item.dealerPrice || (item.product as any)?.dealerPrice) ? (item.dealerPrice || (item.product as any).dealerPrice) : (item.skuPrice || item.sku?.salePrice || item.sku?.price || item.salePrice || item.product?.salePrice || item.product?.price || '0')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useCartContext } from '@/contexts/CartContext'
|
||||
import type { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model'
|
||||
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
||||
import { isGuest } from '@/utils/auth'
|
||||
import { isVipMember } from '@/utils/vip'
|
||||
import { useVipStatus } from '@/hooks/useVipStatus'
|
||||
import { getCompressedImageUrl } from '@/utils/image'
|
||||
import { requireLogin } from '@/utils/login-guard'
|
||||
import { useShare } from '@/hooks/useShare'
|
||||
@@ -38,6 +38,8 @@ const SORT_TABS: { key: SortKey; label: string }[] = [
|
||||
|
||||
const ShopPage: React.FC = () => {
|
||||
const { addItem, totalCount } = useCartContext()
|
||||
// VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染
|
||||
const { isVip } = useVipStatus()
|
||||
const [categoryList, setCategoryList] = useState<any[]>(PRESET_CATEGORIES)
|
||||
const [activeCategory, setActiveCategory] = useState(0)
|
||||
|
||||
@@ -140,7 +142,7 @@ const ShopPage: React.FC = () => {
|
||||
|
||||
/** 获取显示价格 */
|
||||
const getDisplayPrice = (product: ShopGoods) => {
|
||||
if (isVipMember() && product.dealerPrice) return product.dealerPrice
|
||||
if (isVip && product.dealerPrice) return product.dealerPrice
|
||||
return product.price || '0'
|
||||
}
|
||||
|
||||
@@ -304,7 +306,7 @@ const ShopPage: React.FC = () => {
|
||||
<Price
|
||||
price={getDisplayPrice(item)}
|
||||
original={
|
||||
isVipMember() && item.dealerPrice
|
||||
isVip && item.dealerPrice
|
||||
? item.price
|
||||
: item.salePrice && item.salePrice !== item.price
|
||||
? item.salePrice
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useShare } from '@/hooks/useShare'
|
||||
import SharePoster, { SharePosterHandle } from '@/components/SharePoster'
|
||||
import { isGuest } from '@/utils/auth'
|
||||
import { requireLogin } from '@/utils/login-guard'
|
||||
import { isVipMember } from '@/utils/vip'
|
||||
import { useVipStatus } from '@/hooks/useVipStatus'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '商品详情',
|
||||
@@ -36,6 +36,8 @@ const ProductDetailPage: React.FC = () => {
|
||||
const [isFavorite, setIsFavorite] = useState(false)
|
||||
const { addItem, totalCount, refresh } = useCartContext()
|
||||
const { isLoggedIn } = useUserContext()
|
||||
// VIP 状态:异步校验并更新缓存,isVip 变化时触发重渲染
|
||||
const { isVip } = useVipStatus()
|
||||
// 底部操作栏高度约 90px(含按钮 + 安全区),动态计算 ScrollView 可用高度
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
|
||||
@@ -72,7 +74,7 @@ const ProductDetailPage: React.FC = () => {
|
||||
cover: product.image,
|
||||
title: product.name || product.goodsName || '优质商品推荐',
|
||||
price:
|
||||
product.dealerPrice && isVipMember() ? product.dealerPrice : product.price,
|
||||
product.dealerPrice && isVip ? product.dealerPrice : product.price,
|
||||
page: 'pages/shop/product-detail',
|
||||
})
|
||||
.then(setPosterPath)
|
||||
@@ -89,7 +91,7 @@ const ProductDetailPage: React.FC = () => {
|
||||
|
||||
// VIP 会员显示 dealerPrice,普通用户显示 price
|
||||
const getDisplayPrice = (): string => {
|
||||
if (isVipMember() && product?.dealerPrice) {
|
||||
if (isVip && product?.dealerPrice) {
|
||||
return product.dealerPrice
|
||||
}
|
||||
return product?.price || '0'
|
||||
@@ -172,7 +174,7 @@ const ProductDetailPage: React.FC = () => {
|
||||
}
|
||||
} else {
|
||||
// VIP 会员使用 dealerPrice 作为结算价
|
||||
const vipPrice = isVipMember() && product.dealerPrice ? product.dealerPrice : undefined
|
||||
const vipPrice = isVip && product.dealerPrice ? product.dealerPrice : undefined
|
||||
const buyNowData = [{
|
||||
goodsId: product.goodsId!,
|
||||
skuId: sku?.id || 0,
|
||||
@@ -292,7 +294,7 @@ const ProductDetailPage: React.FC = () => {
|
||||
<Price price={getDisplayPrice()} size='large' color='#ee0a24' loginMask />
|
||||
<Tag>到手价</Tag>
|
||||
{/* VIP 会员价 */}
|
||||
{!isGuest() && isVipMember() && Number(product.dealerPrice) > 0 ? (
|
||||
{!isGuest() && isVip && Number(product.dealerPrice) > 0 ? (
|
||||
<View className='ml-auto inline-flex items-center gap-1 bg-amber-50 rounded px-2 py-1'>
|
||||
<Text className='text-xs text-amber-600 font-medium'>VIP专享</Text>
|
||||
<Text className='text-sm text-amber-700 font-bold'>¥{product.dealerPrice}</Text>
|
||||
@@ -304,7 +306,7 @@ const ProductDetailPage: React.FC = () => {
|
||||
)}
|
||||
</View>
|
||||
{/* 会员价 */}
|
||||
{!isGuest() && !isVipMember() && Number(product.memberStorePrice) > 0 && product.memberStorePrice !== product.price && (
|
||||
{!isGuest() && !isVip && Number(product.memberStorePrice) > 0 && product.memberStorePrice !== product.price && (
|
||||
<View className='mt-2 inline-block bg-orange-50 rounded px-2 py-1'>
|
||||
<Text className='text-xs text-orange-500'>会员价: ¥{product.memberStorePrice}</Text>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user