fix(api): 替换所有接口请求的域名为 guilixu-api.websoft.top

- 将获取 STS Token 的接口地址由 paopao-api 改为 guilixu-api
- 更新图片上传接口地址为新的 guilixu-api 域名
- 修改用户推广页面中邀请码链接和二维码接口的域名
- 更改注册页微信登录接口请求的域名为 guilixu-api
This commit is contained in:
2026-06-16 17:15:59 +08:00
commit f3886664f7
617 changed files with 77059 additions and 0 deletions

694
src/pages/shop/checkout.tsx Normal file
View File

@@ -0,0 +1,694 @@
import React, { useState, useEffect, useMemo } from 'react'
import { View, Text, Image, ScrollView, Input } from '@tarojs/components'
import Taro, { useRouter, useDidShow } from '@tarojs/taro'
import { useCartContext } from '@/contexts/CartContext'
import { useAddress } from '@/hooks/useAddress'
import { useCoupon } from '@/hooks/useCoupon'
import { useScrollHeight } from '@/hooks/useScrollHeight'
import { getMyAvailableCoupons } from '@/api/shop/shopUserCoupon'
import AddressCard from '@/components/business/AddressCard'
import CouponCard from '@/components/business/CouponCard'
import { createOrder, repairOrder, type WxPayResult } from '@/api/shop/shopOrder'
import type { ShopOrder } from '@/api/shop/shopOrder/model'
import { payByBalance } from '@/api/system/payment'
import { getUserBalance } from '@/api/system/user'
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'
// 满减门槛配置
const THRESHOLDS = [
{ target: 99, discount: 10 },
{ target: 199, discount: 20 },
{ target: 299, discount: 30 },
{ target: 499, discount: 50 },
]
// 解析可能为JSON字符串的规格值
const parseSpecValue = (value: string | undefined): string => {
if (!value) return ''
const trimmed = value.trim()
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
try {
return JSON.parse(trimmed)
} catch {
return trimmed.slice(1, -1)
}
}
return trimmed
}
// 格式化规格信息,展示规格名称和值
const formatSpecInfo = (item: any): { specList: Array<{ name: string; value: string }>; specText: string } => {
const specList: Array<{ name: string; value: string }> = []
// 防御性检查:确保 goodsSpecs 是数组
const goodsSpecs = item.product?.goodsSpecs
if (Array.isArray(goodsSpecs) && goodsSpecs.length > 0) {
const specGroups = new Map<number, { name: string; values: string[] }>()
goodsSpecs.forEach((spec: any) => {
const specId = spec?.specId || 0
if (!specGroups.has(specId)) {
specGroups.set(specId, {
name: spec?.specName || `规格${specGroups.size + 1}`,
values: []
})
}
const group = specGroups.get(specId)
if (group) {
group.values.push(parseSpecValue(spec?.specValue))
}
})
// 2. 如果选择了 SKU根据 sku.sku 匹配规格值
const skuStr = item.sku?.sku
if (typeof skuStr === 'string' && skuStr) {
const selectedValues = skuStr.split('|').map((v: string) => parseSpecValue(v.trim())).sort()
specGroups.forEach((group) => {
// 防御性检查:确保 values 是数组
if (!Array.isArray(group.values)) return
// 查找匹配的规格值
const matchedValue = group.values.find(v => selectedValues.includes(v))
if (matchedValue) {
specList.push({ name: group.name, value: matchedValue })
}
})
}
// 如果没有 SKU 但有 skuSpec 字符串
if (specList.length === 0 && item.skuSpec) {
const values = String(item.skuSpec).split(',').map(v => v.trim()).filter(Boolean)
let idx = 0
specGroups.forEach((group) => {
if (values[idx]) {
specList.push({ name: group.name, value: values[idx] })
}
idx++
})
}
}
// 3. 如果没有完整的规格定义,但有 skuSpec 字符串
if (specList.length === 0 && item.skuSpec) {
const parts = String(item.skuSpec).split(/[,]/).map(v => v.trim()).filter(Boolean)
if (parts.length > 0) {
specList.push({ name: '规格', value: parts.join(', ') })
}
}
return {
specList,
specText: specList.map(s => `${s.name}: ${s.value}`).join(' | ') || item.skuSpec || ''
}
}
definePageConfig({
navigationBarTitleText: '确认订单',
})
const CheckoutPage: React.FC = () => {
const router = useRouter()
const fromBuyNow = router.params.from === 'buyNow'
const { selectedItems, refresh: refreshCart, removeSelected } = useCartContext()
const { defaultAddress, loadAddresses } = useAddress()
const maxPopupHeight = useScrollHeight(0)
// 底部提交栏高度约 60px动态计算 ScrollView 可用高度
const scrollHeight = useScrollHeight(44)
const [coupons, setCoupons] = useState<ShopUserCoupon[]>([])
const [selectedCoupon, setSelectedCoupon] = useState<ShopUserCoupon | null>(null)
const [couponVisible, setCouponVisible] = useState(false)
const [remarks, setRemarks] = useState('')
const [submitting, setSubmitting] = useState(false)
const [rushBuyProducts, setRushBuyProducts] = useState<ShopGoods[]>([])
// 支付方式相关状态
const [payType, setPayType] = useState<number>(1) // 1: 微信支付, 0: 余额支付
const [userBalance, setUserBalance] = useState<string>('0.00')
const [loadingBalance, setLoadingBalance] = useState(false)
// 从本地存储获取立即购买的数据
const buyNowItems = useMemo(() => {
if (fromBuyNow) {
const data = Taro.getStorageSync('buy_now')
if (data) {
try {
const parsed = JSON.parse(data)
return Array.isArray(parsed) ? parsed : []
} catch { /* ignore */ }
}
}
return null
}, [fromBuyNow])
// 使用的商品列表
const items = buyNowItems || selectedItems
// 计算金额
const goodsPrice = useMemo(() => {
if (!items || items.length === 0) return 0
return items.reduce((sum, item) => {
return sum + Number(item.skuPrice || item.sku?.price || item.product?.salePrice || item.product?.price || 0) * (item.quantity || item.num || 1)
}, 0)
}, [buyNowItems, selectedItems])
const couponDiscount = selectedCoupon?.reducePrice ? Number(selectedCoupon.reducePrice) : 0
const totalPrice = Math.max(0, goodsPrice - couponDiscount).toFixed(2)
// 计算凑单信息
const rushBuyInfo = useMemo(() => {
if (goodsPrice === 0) return null
// 找出最近的满减门槛
for (const threshold of THRESHOLDS) {
if (goodsPrice < threshold.target) {
const needAmount = threshold.target - goodsPrice
// 只在需要凑的金额在 5-80 元之间时显示推荐
if (needAmount <= 80) {
return {
target: threshold.target,
discount: threshold.discount,
needAmount: needAmount.toFixed(2)
}
}
}
}
return null
}, [goodsPrice])
// 加载优惠券和凑单商品
useEffect(() => {
loadCoupons()
loadRushBuyProducts()
loadUserBalance()
}, [])
// 每次页面显示时刷新地址(从地址列表选择后返回)
useDidShow(() => {
loadAddresses()
})
const loadCoupons = async () => {
try {
const data = await getMyAvailableCoupons()
setCoupons(data || [])
} catch (e) {
console.error('加载优惠券失败:', e)
// 不设置任何内容,让页面继续显示
}
}
// 加载凑单推荐商品
const loadRushBuyProducts = async () => {
try {
// 获取热销/推荐商品
const data = await listShopGoods({ page: 1, limit: 10, isShow: 1 })
// 防御性检查:确保返回的是数组
const safeData = Array.isArray(data) ? data : []
if (safeData.length > 0) {
// 过滤掉已在购物车的商品
const currentItems = buyNowItems || selectedItems
const cartGoodsIds = Array.isArray(currentItems) ? currentItems.map(item => item.goodsId) : []
const filtered = safeData.filter(p => !cartGoodsIds.includes(p.goodsId))
setRushBuyProducts(filtered.slice(0, 4))
}
} catch (e) {
console.error('加载凑单商品失败:', e)
// 不设置任何内容,让页面继续显示
}
}
// 加载用户余额
const loadUserBalance = async () => {
setLoadingBalance(true)
try {
const data = await getUserBalance()
setUserBalance(data?.balance || '0.00')
} catch (e) {
console.error('加载余额失败:', e)
setUserBalance('0.00')
} finally {
setLoadingBalance(false)
}
}
// 选择地址
const handleSelectAddress = () => {
Taro.navigateTo({
url: '/pages/user/address-list?from=checkout',
events: {
refresh: () => loadAddresses()
}
})
}
// 选择优惠券
const handleSelectCoupon = () => {
setCouponVisible(true)
}
const handleCouponConfirm = (coupon: ShopUserCoupon | null) => {
setSelectedCoupon(coupon)
setCouponVisible(false)
}
// 提交订单
const handleSubmit = async () => {
if (!defaultAddress) {
Taro.showToast({ title: '请选择收货地址', icon: 'none' })
return
}
if (items.length === 0) {
Taro.showToast({ title: '请选择商品', icon: 'none' })
return
}
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,
}))
// 检查余额是否足够(如果选择余额支付)
if (payType === 0 && parseFloat(userBalance) < parseFloat(totalPrice)) {
Taro.showModal({
title: '余额不足',
content: `当前余额 ¥${parseFloat(userBalance).toFixed(2)},需要 ¥${totalPrice},是否前往充值?`,
confirmText: '去充值',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
Taro.navigateTo({ url: '/pages/user/recharge' })
}
}
})
setSubmitting(false)
return
}
// 创建订单
const orderParams: OrderCreateRequest = {
goodsItems,
addressId: defaultAddress.id,
payType,
couponId: selectedCoupon?.id ? Number(selectedCoupon.id) : undefined,
comments: remarks,
deliveryType: 0, // 快递配送
}
const res = await createOrder(orderParams)
// 根据支付方式处理
if (payType === 0) {
// 余额支付 - res 包含 { orderId, orderNo, payType, payPrice }
const balanceRes = res as { orderId?: number; orderNo?: string }
await handleBalancePay(Number(balanceRes.orderId))
} else {
// 微信支付 - res 是 WxPayResult
await handleWxPay(res as WxPayResult)
}
// 清理数据
if (fromBuyNow) {
Taro.removeStorageSync('buy_now')
} else {
await removeSelected()
}
Taro.showToast({ title: '支付成功', icon: 'success' })
setTimeout(() => {
Taro.redirectTo({ url: '/pages/order/list' })
}, 1500)
} catch (err: any) {
Taro.showToast({ title: err.message || '提交失败', icon: 'none' })
} finally {
setSubmitting(false)
}
}
// 微信支付
const handleWxPay = async (payData: WxPayResult & { paid?: string }) => {
// 后端返回已支付(回调丢失自动修复场景)
if (payData.paid === 'true') {
return // 直接视为支付成功
}
return new Promise<void>((resolve, reject) => {
Taro.requestPayment({
timeStamp: payData.timeStamp,
nonceStr: payData.nonceStr,
package: payData.package,
signType: payData.signType,
paySign: payData.paySign,
success: () => resolve(),
fail: (err) => reject(new Error('支付取消'))
})
})
}
// 余额支付
const handleBalancePay = async (orderId: number | string) => {
try {
await payByBalance({ orderId: Number(orderId) })
// 余额支付成功后,修复订单支付状态
try {
await repairOrder({ orderId: Number(orderId), payStatus: true } as Partial<ShopOrder>)
} catch {
// 修复失败不影响支付流程
}
// 刷新余额
loadUserBalance()
} catch (err: any) {
throw new Error(err.message || '余额支付失败')
}
}
// 安全的数字格式化
const formatPrice = (price: number | string): string => {
const num = typeof price === 'string' ? parseFloat(price) : price
if (isNaN(num)) return '0.00'
return num.toFixed(2)
}
return (
<View className='bg-gray-50 flex flex-col' style={{ height: '100vh' }}>
<ScrollView scrollY style={{ height: scrollHeight }}>
<View className='p-3'>
{/* 地址 */}
{defaultAddress ? (
<AddressCard
address={defaultAddress}
onClick={handleSelectAddress}
/>
) : (
<View className='bg-white rounded-lg p-3' onClick={handleSelectAddress}>
<View className='flex items-center justify-between'>
<View className='flex items-center gap-2'>
<Text className='text-gray-400 text-sm'>📍</Text>
<View>
<Text className='text-sm font-medium text-gray-800 block'></Text>
</View>
</View>
<Text className='text-gray-400 text-sm'></Text>
</View>
</View>
)}
{/* 商品列表 */}
<View className='bg-white rounded-lg mt-3 p-3'>
<Text className='text-sm font-medium text-gray-800 mb-3 block'>
({(items || []).length})
</Text>
{items && items.length > 0 ? (
items.map((item, idx) => {
const { specList } = formatSpecInfo(item)
return (
<View key={`${item.goodsId}-${item.skuId}-${idx}`} className='flex gap-3 py-3 border-b border-gray-50'>
<Image
className='w-16 h-16 rounded-md bg-gray-100'
src={item.goodsImage || item.product?.image || item.sku?.image || ''}
mode='aspectFill'
/>
<View className='flex-1'>
<Text className='text-sm text-gray-700 block line-clamp-2'>
{item.goodsName || item.product?.name || item.product?.goodsName}
</Text>
{/* 多规格属性展示 */}
{specList.length > 0 ? (
<View className='mt-1'>
{specList.map((spec, specIdx) => (
<View key={specIdx} className='flex items-center text-xs'>
<Text className='text-gray-400'>{spec.name}:</Text>
<Text className='text-gray-600 ml-1'>{spec.value}</Text>
</View>
))}
</View>
) : item.skuSpec ? (
<Text className='text-xs text-gray-400 mt-1'>{item.skuSpec}</Text>
) : null}
<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'>
¥{item.skuPrice || item.sku?.salePrice || item.sku?.price || item.product?.salePrice || item.product?.price || '0'}
</Text>
</View>
</View>
</View>
)
})
) : (
<View className='py-10 text-center'>
<Text className='text-gray-400 text-sm'></Text>
<View
className='mt-3 px-6 py-2 rounded-full text-white text-sm inline-block'
style={{ backgroundColor: '#0e932e' }}
onClick={() => Taro.switchTab({ url: '/pages/shop/index' })}
>
</View>
</View>
)}
</View>
{/* 优惠券 */}
<View className='bg-white rounded-lg mt-3 p-3' onClick={handleSelectCoupon}>
<View className='flex justify-between items-center'>
<Text className='text-sm text-gray-700'></Text>
<View className='flex items-center gap-2'>
{selectedCoupon ? (
<Text className='text-sm text-red-500'>-{selectedCoupon.reducePrice}</Text>
) : coupons.length > 0 ? (
<Text className='text-sm text-gray-400'>{coupons.length}</Text>
) : (
<Text className='text-sm text-gray-400'></Text>
)}
<Text className='text-gray-400'></Text>
</View>
</View>
</View>
{/* 备注 */}
<View className='bg-white rounded-lg mt-3 p-3'>
<View className='flex justify-between items-center'>
<Text className='text-sm text-gray-700'></Text>
<Text className='text-xs text-gray-400'></Text>
</View>
<View className='mt-2'>
<Input
className='bg-gray-50 rounded-lg px-3 py-2 text-sm'
placeholder='点击添加备注...'
value={remarks}
onInput={(e: any) => setRemarks(e.detail.value)}
maxlength={200}
/>
</View>
</View>
{/* 金额明细 */}
<View className='bg-white rounded-lg mt-3 p-3'>
<View className='flex justify-between mb-2'>
<Text className='text-sm text-gray-500'></Text>
<Text className='text-sm text-gray-700'>¥{formatPrice(goodsPrice)}</Text>
</View>
<View className='flex justify-between mb-2'>
<Text className='text-sm text-gray-500'></Text>
<Text className='text-sm text-green-600'></Text>
</View>
{couponDiscount > 0 && (
<View className='flex justify-between mb-2'>
<Text className='text-sm text-gray-500'></Text>
<Text className='text-sm text-red-500'>-¥{formatPrice(couponDiscount)}</Text>
</View>
)}
<View className='flex justify-between pt-2 border-t border-gray-100'>
<Text className='text-sm font-medium'></Text>
<Text className='text-lg font-bold text-red-500'>¥{totalPrice}</Text>
</View>
</View>
{/* 支付方式选择 */}
<View className='bg-white rounded-lg mt-3 p-3'>
<Text className='text-sm font-medium text-gray-800 mb-3 block'></Text>
{/* 微信支付 */}
<View
className='flex items-center py-3 px-2 rounded-lg mb-2'
style={{ backgroundColor: payType === 1 ? '#f0fdf4' : '#f9fafb' }}
onClick={() => setPayType(1)}
>
<View className='w-8 h-8 rounded-full bg-green-50 flex items-center justify-center mr-3'>
<Text className='text-sm'></Text>
</View>
<View className='flex-1'>
<Text className='text-sm text-gray-800 block'></Text>
<Text className='text-xs text-gray-400 block'>使</Text>
</View>
<View
className='w-5 h-5 rounded-full border-2 flex items-center justify-center'
style={{ borderColor: payType === 1 ? '#22c55e' : '#d1d5db' }}
>
{payType === 1 && (
<View className='w-2 h-2 rounded-full' style={{ backgroundColor: '#22c55e' }} />
)}
</View>
</View>
{/* 余额支付 */}
<View
className='flex items-center py-3 px-2 rounded-lg'
style={{ backgroundColor: payType === 0 ? '#f0fdf4' : '#f9fafb' }}
onClick={() => setPayType(0)}
>
<View className='w-8 h-8 rounded-full bg-amber-50 flex items-center justify-center mr-3'>
<Text className='text-sm'></Text>
</View>
<View className='flex-1'>
<Text className='text-sm text-gray-800 block'></Text>
<Text className='text-xs text-gray-400 block'>
{loadingBalance ? '加载中...' : `可用: ¥${parseFloat(userBalance).toFixed(2)}`}
</Text>
</View>
<View
className='w-5 h-5 rounded-full border-2 flex items-center justify-center'
style={{ borderColor: payType === 0 ? '#22c55e' : '#d1d5db' }}
>
{payType === 0 && (
<View className='w-2 h-2 rounded-full' style={{ backgroundColor: '#22c55e' }} />
)}
</View>
</View>
{/* 余额不足提示 */}
{payType === 0 && parseFloat(userBalance) < parseFloat(totalPrice) && (
<View className='mt-2 px-2'>
<Text className='text-xs text-red-500'>
¥{(parseFloat(totalPrice) - parseFloat(userBalance)).toFixed(2)}
</Text>
</View>
)}
</View>
{/* 凑单推荐 */}
{rushBuyInfo && rushBuyProducts.length > 0 && (
<View className='rounded-lg mt-3 p-3 border border-red-100' style={{ background: 'linear-gradient(to right, #fef2f2, #fff7ed)', display: 'none' }}>
<View className='flex items-center justify-between mb-3'>
<View className='flex items-center gap-2'>
<Text className='text-red-500 text-lg'>🎯</Text>
<Text className='text-sm font-medium text-red-600'>
¥{rushBuyInfo.needAmount} ¥{rushBuyInfo.discount}
</Text>
</View>
<Text className='text-xs text-gray-400'></Text>
</View>
<ScrollView scrollX className='flex-row'>
<View className='flex flex-row gap-2'>
{rushBuyProducts.map(product => (
<View
key={product.goodsId}
className='w-24 bg-white rounded-lg p-2'
onClick={() => {
Taro.navigateTo({
url: `/pages/shop/product-detail?id=${product.goodsId}&from=rushbuy`
})
}}
>
<Image
className='w-full h-20 rounded bg-gray-100'
src={product.image || ''}
mode='aspectFill'
/>
<Text className='text-xs text-gray-700 block line-clamp-1 mt-1'>
{product.name}
</Text>
<View className='flex items-center justify-between mt-1'>
<Text className='text-xs font-medium text-red-500'>
¥{product.salePrice || product.price}
</Text>
<Text className='text-xs text-gray-400'>+</Text>
</View>
</View>
))}
</View>
</ScrollView>
</View>
)}
{/* 底部留白(固定栏高度 + 安全区) */}
<View style={{ height: '80px' }} />
</View>
</ScrollView>
{/* 底部提交栏 - 固定吸底 */}
<View
className='bg-white border-t border-gray-100 px-3'
style={{
position: 'fixed',
left: 0,
right: 0,
bottom: 0,
paddingBottom: 'env(safe-area-inset-bottom)',
zIndex: 100,
}}
>
<View className='flex items-center justify-between py-3'>
<View className='flex items-center gap-2'>
<Text className='text-sm text-gray-500'>:</Text>
<Text className='text-lg font-bold text-red-500'>¥{totalPrice}</Text>
</View>
<View
className='px-8 py-2 rounded-full text-white text-sm font-medium'
style={{ backgroundColor: submitting ? '#ccc' : '#0e932e' }}
onClick={submitting ? undefined : handleSubmit}
>
<Text>{submitting ? '提交中...' : '提交订单'}</Text>
</View>
</View>
</View>
{/* 优惠券选择弹窗 */}
{couponVisible && (
<View className='absolute flex items-end' style={{ top: 0, right: 0, bottom: 0, left: 0, backgroundColor: 'rgba(0,0,0,0.5)', zIndex: 110 }}>
<View className='w-full bg-white rounded-t-xl overflow-hidden' style={{ maxHeight: maxPopupHeight }}>
<View className='p-4 border-b border-gray-100 flex justify-between items-center'>
<Text className='text-base font-medium'></Text>
<Text className='text-gray-400' onClick={() => setCouponVisible(false)}></Text>
</View>
<ScrollView scrollY className='flex-1'>
<View className='p-3'>
{/* 不使用优惠券 */}
<View
className='bg-white rounded-lg p-3 mb-3 border border-gray-200'
onClick={() => handleCouponConfirm(null)}
>
<Text className='text-sm text-gray-600'>使</Text>
</View>
{/* 可用优惠券列表 */}
{coupons.map(coupon => (
<CouponCard
key={coupon.id}
coupon={coupon}
selected={selectedCoupon?.id === coupon.id}
onClick={() => handleCouponConfirm(coupon)}
/>
))}
{coupons.length === 0 && (
<View className='py-10 text-center'>
<Text className='text-gray-400 text-sm'></Text>
</View>
)}
</View>
</ScrollView>
</View>
</View>
)}
</View>
)
}
export default CheckoutPage