import React, { useState, useEffect, useCallback } from 'react' import { View, Text } from '@tarojs/components' import { Button, Cell, Price, Divider } from '@nutui/nutui-react-taro' import { Check, Close, Tips, Clock } from '@nutui/icons-react-taro' import Taro from '@tarojs/taro' import { request } from '@/utils/request' import { getOpenId, loginByOpenId } from '@/api/passport/wx-login' import { saveStorageByLoginUser } from '@/utils/server' import { TenantId } from '@/config/app' /** * 小程序支付页面 * 接收参数 subscriptionNo,完成 JSAPI 支付 * * 重要:判断订单是否已支付,必须用 payStatus(0=未支付 1=已支付), * 不能用 status(订阅生命周期:active/pending/expired/cancelled)。 * 续费场景下后端 renewPay 会保留原 status=active、仅设 payStatus=0, * 若用 status 判支付会直接误判为"已支付"。 */ interface SubscriptionDetail { id: number subscriptionNo: string productId: number productName: string productLogo?: string productIcon?: string status: string // 支付状态: 0-未支付 1-已支付 payStatus?: number payTime?: string transactionId?: string priceType: string payPrice: number subscriptionPeriod?: string } interface JsapiPayParams { timeStamp: string nonceStr: string package: string signType: string paySign: string outTradeNo: string } const PayPage: React.FC = () => { const [loading, setLoading] = useState(true) const [paying, setPaying] = useState(false) const [detail, setDetail] = useState(null) const [errorMsg, setErrorMsg] = useState('') const [paid, setPaid] = useState(false) // 从页面参数中获取 subscriptionNo // 小程序码 scene 会作为 query.scene 传入,需解码 const getSubscriptionNo = useCallback(() => { const instance = Taro.getCurrentInstance() const params = instance?.router?.params || {} console.log('[PayPage] ===== 参数调试开始 =====') console.log('[PayPage] 原始 params:', JSON.stringify(params)) console.log('[PayPage] params.scene:', params.scene) console.log('[PayPage] params.subscriptionNo:', params.subscriptionNo) // 优先读取 scene(小程序码参数),再读取 subscriptionNo(普通链接参数) const scene = params.scene ? decodeURIComponent(params.scene) : '' const subscriptionNo = scene || params.subscriptionNo || '' console.log('[PayPage] decodeURIComponent(scene):', scene) console.log('[PayPage] 最终 subscriptionNo:', subscriptionNo) console.log('[PayPage] ===== 参数调试结束 =====') return subscriptionNo }, []) // 获取订阅详情 const fetchDetail = useCallback(async () => { const subscriptionNo = getSubscriptionNo() if (!subscriptionNo) { console.error('[PayPage] subscriptionNo 为空,无法请求') setErrorMsg('缺少订单参数') setLoading(false) return } console.log('[PayPage] ===== 请求调试开始 =====') console.log('[PayPage] 请求 URL:', `/app/subscription/detail-by-no/${subscriptionNo}`) console.log('[PayPage] 当前 token:', Taro.getStorageSync('access_token') || '(无token)') try { // request 默认 returnRaw=false,拦截器已拆包,返回的就是 data 部分(订阅对象) const data: any = await request({ url: `/app/subscription/detail-by-no/${subscriptionNo}`, method: 'GET', returnRaw: true // 先用 returnRaw=true 拿到完整响应,方便调试 }) console.log('[PayPage] 接口完整返回(returnRaw=true):', JSON.stringify(data)) console.log('[PayPage] data.code:', data?.code) console.log('[PayPage] data.message:', data?.message) console.log('[PayPage] data.data:', data?.data) // 判断业务状态码 if (data?.code === 0 || data?.code === 200) { const subscription = data.data || data console.log('[PayPage] 订阅对象:', JSON.stringify(subscription)) console.log('[PayPage] 订阅状态 status:', subscription?.status) console.log('[PayPage] 支付状态 payStatus:', subscription?.payStatus) console.log('[PayPage] 支付时间 payTime:', subscription?.payTime) // 关键修复:判断是否已支付,必须用 payStatus 而非 status // 续费场景下后端 renewPay 会保留原 status=active、仅设 payStatus=0 const isPaid = subscription?.payStatus === 1 const isCancelled = subscription?.status === 'cancelled' const isExpired = subscription?.status === 'expired' if (isCancelled) { setErrorMsg('订单已取消,无法继续支付') } else if (isExpired) { setErrorMsg('订单已过期,请重新下单') } else if (isPaid) { setPaid(true) setDetail(subscription) } else { setDetail(subscription) } } else { console.error('[PayPage] 业务错误 code:', data?.code, 'message:', data?.message) setErrorMsg(data?.message || `业务错误(code=${data?.code})`) } } catch (err: any) { console.error('[PayPage] ===== 请求异常 =====') console.error('[PayPage] err.name:', err?.name) console.error('[PayPage] err.type:', err?.type) console.error('[PayPage] err.code:', err?.code) console.error('[PayPage] err.message:', err?.message) console.error('[PayPage] err.data:', err?.data) console.error('[PayPage] err 完整:', JSON.stringify(err)) setErrorMsg(err?.message || '网络异常,请重试') } finally { setLoading(false) console.log('[PayPage] ===== 请求调试结束 =====') } }, [getSubscriptionNo]) useEffect(() => { fetchDetail() }, [fetchDetail]) // 执行支付 const handlePay = async () => { if (!detail) return setPaying(true) try { // 1. 获取微信登录凭证(code) const loginRes = await Taro.login() if (!loginRes.code) { throw new Error('获取登录凭证失败') } console.log('[PayPage] Taro.login code:', loginRes.code) // 2. 用 code 换取 openid(关键步骤,之前遗漏了) const openIdRes = await getOpenId(loginRes.code) console.log('[PayPage] getOpenId 结果:', openIdRes) if (!openIdRes.success || !openIdRes.openid) { throw new Error(openIdRes.message || '获取 openid 失败,请重试') } const openid = openIdRes.openid // 缓存 openid,后续支付确认等接口可用 Taro.setStorageSync('openid', openid) console.log('[PayPage] 获取到 openid:', openid) // 3. 尝试自动登录(获取 token,mp-prepay 接口需要登录态) // 如果用户已在小程序注册过,loginByOpenId 会返回 token // 如果用户未注册,不影响 openid 已获取,但 mp-prepay 可能需要特殊处理 try { const loginResult = await loginByOpenId({ code: loginRes.code, tenantId: TenantId }) console.log('[PayPage] loginByOpenId 结果:', loginResult) if (loginResult.success && loginResult.data) { // 登录成功,保存 token saveStorageByLoginUser(loginResult.data.access_token || '', loginResult.data.user) console.log('[PayPage] 自动登录成功,已保存 token') } else { console.warn('[PayPage] 自动登录失败:', loginResult.message) // 未注册用户:openid 已经有了,但 mp-prepay 需要 userId // 提示用户先注册/登录 if (loginResult.message?.includes('未注册') || loginResult.message?.includes('不存在')) { Taro.showModal({ title: '需要先登录', content: '支付前需要先注册账号,是否前往登录?', confirmText: '去登录', cancelText: '取消' }).then(modalRes => { if (modalRes.confirm) { Taro.navigateTo({ url: '/passport/login' }) } }) return // 不继续支付流程 } } } catch (loginErr) { console.warn('[PayPage] 自动登录异常:', loginErr) // 登录异常但不阻断支付,openid 已拿到,尝试继续 } // 4. 请求后端创建 JSAPI 预支付订单 const prepayRes: any = await request({ url: `/app/subscription/mp-prepay/${detail.id}`, method: 'POST', data: { openid }, returnRaw: true }) console.log('[PayPage] mp-prepay 完整返回:', JSON.stringify(prepayRes)) if (prepayRes?.code !== 0 && prepayRes?.code !== 200) { throw new Error(prepayRes?.message || '创建支付订单失败') } const payParams: JsapiPayParams = prepayRes.data || prepayRes if (!payParams.timeStamp || !payParams.package || !payParams.paySign) { throw new Error('支付参数不完整') } // 5. 调用微信支付 await Taro.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: (payParams.signType || 'RSA') as any, paySign: payParams.paySign }) // 6. 支付成功后通知后端确认 try { await request({ url: `/app/subscription/mp-confirm/${detail.subscriptionNo}`, method: 'POST', data: {} }) } catch (confirmErr) { console.warn('支付确认通知失败,等待轮询:', confirmErr) } // 7. 显示成功状态 setPaid(true) Taro.showToast({ title: '支付成功', icon: 'success' }) // 8. 延迟自动跳转到已购产品页面 setTimeout(() => { Taro.navigateTo({ url: '/user/apps/index' }).catch(() => { // 如果 navigateTo 失败(比如在 tab 页),回退到用户页 Taro.switchTab({ url: '/pages/user/user' }) }) }, 1500) } catch (err: any) { console.error('[PayPage] 支付失败:', err) console.error('[PayPage] err.name:', err?.name, 'err.type:', err?.type) console.error('[PayPage] err.code:', err?.code, 'err.message:', err?.message) if (err?.errMsg?.includes('cancel')) { Taro.showToast({ title: '支付已取消', icon: 'none' }) } else { const msg = err?.message || err?.errMsg || '支付失败,请重试' Taro.showToast({ title: msg, icon: 'error', duration: 2000 }) } } finally { setPaying(false) } } // 返回 const handleBack = () => { Taro.navigateBack({ delta: 1 }).catch(() => { Taro.switchTab({ url: '/pages/user/user' }) }) } // 格式化价格描述 const getPriceDesc = () => { if (!detail) return '' if (detail.priceType === 'one_time') return '永久买断' if (detail.priceType === 'subscription') { return detail.subscriptionPeriod === 'year' ? '按年订阅' : '按月订阅' } return '' } // --- 加载中 --- if (loading) { return ( 加载订单信息... ) } // --- 错误 --- if (errorMsg) { return ( 获取订单失败 {errorMsg} ) } // --- 已支付 --- if (paid) { return ( 支付成功 {detail?.productName || '应用订阅'} {detail?.payPrice ? ( 已支付 ¥{Number(detail.payPrice).toFixed(2)} ) : null} ) } // --- 支付确认 --- return ( {/* 订单信息 */} 确认支付 支付金额 支付成功后即可在「已购产品」中使用该应用 {/* 支付按钮 */} {/* 底部安全距离占位 */} ) } export default PayPage