diff --git a/src/passport/pay/index.config.ts b/src/passport/pay/index.config.ts new file mode 100644 index 0000000..656a5fe --- /dev/null +++ b/src/passport/pay/index.config.ts @@ -0,0 +1,5 @@ +export default { + navigationBarTitleText: '确认支付', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff' +} diff --git a/src/passport/pay/index.tsx b/src/passport/pay/index.tsx new file mode 100644 index 0000000..c118654 --- /dev/null +++ b/src/passport/pay/index.tsx @@ -0,0 +1,280 @@ +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' + +/** + * 小程序支付页面 + * 接收参数 subscriptionNo,完成 JSAPI 支付 + */ +interface SubscriptionDetail { + id: number + subscriptionNo: string + productId: number + productName: string + productLogo?: string + productIcon?: string + status: 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 + const getSubscriptionNo = useCallback(() => { + const instance = Taro.getCurrentInstance() + const params = instance?.router?.params || {} + return params.subscriptionNo || '' + }, []) + + // 获取订阅详情 + const fetchDetail = useCallback(async () => { + const subscriptionNo = getSubscriptionNo() + if (!subscriptionNo) { + setErrorMsg('缺少订单参数') + setLoading(false) + return + } + + try { + const res: any = await request({ + url: `/app/subscription/detail-by-no/${subscriptionNo}`, + method: 'GET' + }) + + if (res?.code === 200 || res?.code === 0) { + const data = res.data || res + + if (data.status === 'active') { + setPaid(true) + setDetail(data) + } else { + setDetail(data) + } + } else { + setErrorMsg(res?.message || '获取订单信息失败') + } + } catch (err: any) { + setErrorMsg(err?.message || '网络异常,请重试') + } finally { + setLoading(false) + } + }, [getSubscriptionNo]) + + useEffect(() => { + fetchDetail() + }, [fetchDetail]) + + // 执行支付 + const handlePay = async () => { + if (!detail) return + + setPaying(true) + try { + // 1. 获取登录凭证(code)并换取 openid + const loginRes = await Taro.login() + if (!loginRes.code) { + throw new Error('获取登录凭证失败') + } + + // 2. 通过 code 获取 openid(使用现有的 loginByOpenId 接口) + const openid = Taro.getStorageSync('openid') || '' + + // 3. 请求后端创建 JSAPI 预支付订单 + const prepayRes: any = await request({ + url: `/app/subscription/mp-prepay/${detail.id}`, + method: 'POST', + data: { openid } + }) + + if (prepayRes?.code !== 200 && prepayRes?.code !== 0) { + throw new Error(prepayRes?.message || '创建支付订单失败') + } + + const payParams: JsapiPayParams = prepayRes.data || prepayRes + + if (!payParams.timeStamp || !payParams.package || !payParams.paySign) { + throw new Error('支付参数不完整') + } + + // 4. 调用微信支付 + await Taro.requestPayment({ + timeStamp: payParams.timeStamp, + nonceStr: payParams.nonceStr, + package: payParams.package, + signType: (payParams.signType || 'RSA') as any, + paySign: payParams.paySign + }) + + // 5. 支付成功后通知后端确认 + try { + await request({ + url: `/app/subscription/mp-confirm/${detail.subscriptionNo}`, + method: 'POST', + data: {} + }) + } catch (confirmErr) { + console.warn('支付确认通知失败,等待轮询:', confirmErr) + } + + // 6. 显示成功状态 + setPaid(true) + Taro.showToast({ title: '支付成功', icon: 'success' }) + + } catch (err: any) { + console.error('支付失败:', err) + + 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