From 5e8c2400a3e83fb6dd4d467cd15d333813a61101 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Sat, 13 Dec 2025 10:31:46 +0800 Subject: [PATCH] update pay ui --- alipay_pay.py | 72 ++++++++++ alipay_pay_worker.py | 45 ++++-- app.py | 34 ++++- .../Subscription/SubscriptionContentNew.tsx | 129 ++++++++++++++++-- 4 files changed, 253 insertions(+), 27 deletions(-) diff --git a/alipay_pay.py b/alipay_pay.py index 20dd3fc0..3e9ad2cf 100644 --- a/alipay_pay.py +++ b/alipay_pay.py @@ -228,6 +228,78 @@ class AlipayPay: 'error': str(e) } + def create_wap_pay_url(self, out_trade_no, total_amount, subject, body=None, timeout_express='30m', quit_url=None): + """ + 创建手机网站支付URL (H5支付,可调起手机支付宝APP) + + Args: + out_trade_no: 商户订单号 + total_amount: 订单总金额(元,精确到小数点后两位) + subject: 订单标题 + body: 订单描述(可选) + timeout_express: 订单超时时间,默认30分钟 + quit_url: 用户付款中途退出返回商户网站的地址(可选) + + Returns: + dict: 包含支付URL的响应 + """ + try: + # 金额格式化为两位小数(支付宝要求) + formatted_amount = f"{float(total_amount):.2f}" + + # 业务参数 + biz_content = { + 'out_trade_no': out_trade_no, + 'total_amount': formatted_amount, + 'subject': subject, + 'product_code': 'QUICK_WAP_WAY', # 手机网站支付固定值 + 'timeout_express': timeout_express, + } + if body: + biz_content['body'] = body + if quit_url: + biz_content['quit_url'] = quit_url + + # 公共请求参数 + params = { + 'app_id': self.app_id, + 'method': 'alipay.trade.wap.pay', # 手机网站支付接口 + 'format': 'json', + 'charset': self.charset, + 'sign_type': self.sign_type, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'version': '1.0', + 'notify_url': self.notify_url, + 'return_url': self.return_url, + 'biz_content': json.dumps(biz_content, ensure_ascii=False), + } + + # 生成签名 + sign_content = self._get_sign_content(params) + params['sign'] = self._sign(sign_content) + + # 构建完整的支付URL + pay_url = f"{self.gateway_url}?{urlencode(params)}" + + # 日志输出到 stderr,避免影响 subprocess JSON 输出 + import sys + print(f"[AlipayPay] WAP Order created: {out_trade_no}, amount: {formatted_amount}", file=sys.stderr) + + return { + 'success': True, + 'pay_url': pay_url, + 'order_no': out_trade_no, + 'pay_type': 'wap' # 标识为手机网站支付 + } + + except Exception as e: + import sys + print(f"[AlipayPay] Create WAP order failed: {e}", file=sys.stderr) + return { + 'success': False, + 'error': str(e) + } + def query_order(self, out_trade_no=None, trade_no=None): """ 查询订单状态 diff --git a/alipay_pay_worker.py b/alipay_pay_worker.py index c4dacdcb..2a67dfc9 100644 --- a/alipay_pay_worker.py +++ b/alipay_pay_worker.py @@ -6,7 +6,8 @@ 用法: python alipay_pay_worker.py check # 检查配置 - python alipay_pay_worker.py create [body] # 创建订单 + python alipay_pay_worker.py create [body] [pay_type] # 创建订单 + pay_type: page=电脑网站支付(默认), wap=手机网站支付 python alipay_pay_worker.py query # 查询订单 """ @@ -31,19 +32,39 @@ def check_config(): } -def create_order(order_no, amount, subject, body=None): - """创建支付宝订单""" +def create_order(order_no, amount, subject, body=None, pay_type='page'): + """创建支付宝订单 + + Args: + order_no: 订单号 + amount: 金额 + subject: 标题 + body: 描述 + pay_type: 支付类型 'page'=电脑网站支付, 'wap'=手机网站支付 + """ try: from alipay_pay import create_alipay_instance alipay = create_alipay_instance() - result = alipay.create_page_pay_url( - out_trade_no=order_no, - total_amount=str(amount), - subject=subject, - body=body, - timeout_express='30m' - ) + if pay_type == 'wap': + # 手机网站支付 + result = alipay.create_wap_pay_url( + out_trade_no=order_no, + total_amount=str(amount), + subject=subject, + body=body, + timeout_express='30m', + quit_url='https://valuefrontier.cn/pricing' # 用户取消支付时返回的页面 + ) + else: + # 电脑网站支付(默认) + result = alipay.create_page_pay_url( + out_trade_no=order_no, + total_amount=str(amount), + subject=subject, + body=body, + timeout_express='30m' + ) return result except Exception as e: @@ -108,7 +129,9 @@ def main(): amount = sys.argv[3] subject = sys.argv[4] body = sys.argv[5] if len(sys.argv) > 5 else None - result = create_order(order_no, amount, subject, body) + # pay_type: page=电脑网站支付, wap=手机网站支付 + pay_type = sys.argv[6] if len(sys.argv) > 6 else 'page' + result = create_order(order_no, amount, subject, body, pay_type) elif command == 'query': if len(sys.argv) < 3: diff --git a/app.py b/app.py index 56807a69..534df06e 100755 --- a/app.py +++ b/app.py @@ -2687,7 +2687,8 @@ def create_alipay_order(): { "plan_name": "pro", "billing_cycle": "yearly", - "promo_code": "WELCOME2025" // 可选 + "promo_code": "WELCOME2025", // 可选 + "is_mobile": true // 可选,是否为手机端(自动使用 WAP 支付) } """ try: @@ -2698,6 +2699,8 @@ def create_alipay_order(): plan_name = data.get('plan_name') billing_cycle = data.get('billing_cycle') promo_code = (data.get('promo_code') or '').strip() or None + # 前端传入的设备类型,用于决定使用 page 支付还是 wap 支付 + is_mobile = data.get('is_mobile', False) if not plan_name or not billing_cycle: return jsonify({'success': False, 'error': '参数不完整'}), 400 @@ -2782,8 +2785,12 @@ def create_alipay_order(): # 金额格式化为两位小数(支付宝要求) amount_str = f"{float(amount):.2f}" + # 根据设备类型选择支付方式:wap=手机网站支付,page=电脑网站支付 + pay_type = 'wap' if is_mobile else 'page' + print(f"[支付宝] 设备类型: {'手机' if is_mobile else '电脑'}, 支付方式: {pay_type}") + create_result = subprocess.run( - [sys.executable, script_path, 'create', order.order_no, amount_str, subject, body], + [sys.executable, script_path, 'create', order.order_no, amount_str, subject, body, pay_type], capture_output=True, text=True, timeout=60 ) @@ -3104,6 +3111,29 @@ def check_alipay_order_status(order_id): return jsonify({'success': False, 'error': '查询失败'}), 500 +@app.route('/api/payment/alipay/order-by-no//status', methods=['GET']) +def check_alipay_order_status_by_no(order_no): + """通过订单号查询支付宝订单支付状态(用于手机端支付返回)""" + try: + if 'user_id' not in session: + return jsonify({'success': False, 'error': '未登录'}), 401 + + # 通过订单号查找订单 + order = PaymentOrder.query.filter_by( + order_no=order_no, + user_id=session['user_id'] + ).first() + + if not order: + return jsonify({'success': False, 'error': '订单不存在'}), 404 + + # 复用现有的状态检查逻辑 + return check_alipay_order_status(str(order.id)) + + except Exception as e: + return jsonify({'success': False, 'error': '查询失败'}), 500 + + @app.route('/api/auth/session', methods=['GET']) def get_session_info(): """获取当前登录用户信息""" diff --git a/src/components/Subscription/SubscriptionContentNew.tsx b/src/components/Subscription/SubscriptionContentNew.tsx index 888ca818..8c02d3d5 100644 --- a/src/components/Subscription/SubscriptionContentNew.tsx +++ b/src/components/Subscription/SubscriptionContentNew.tsx @@ -185,6 +185,79 @@ export default function SubscriptionContentNew() { fetchSubscriptionPlans(); }, []); + // 检查是否从支付宝支付返回(手机端支付完成后会跳转回来) + useEffect(() => { + const checkAlipayReturn = async () => { + // 检查 URL 参数是否包含支付宝返回标记 + const urlParams = new URLSearchParams(window.location.search); + const paymentReturn = urlParams.get('payment_return'); + // 支付宝返回的参数是 out_trade_no,后端重定向时会转成 order_no + const orderNo = urlParams.get('order_no') || urlParams.get('out_trade_no'); + + if (paymentReturn === 'alipay' && orderNo) { + // 从支付宝返回,检查支付状态 + toast({ + title: '正在确认支付结果...', + status: 'info', + duration: 2000, + isClosable: true, + }); + + try { + // 优先使用 sessionStorage 中的 orderId,否则使用 order_no 查询 + const orderId = sessionStorage.getItem('alipay_order_id'); + const statusUrl = orderId + ? `/api/payment/alipay/order/${orderId}/status` + : `/api/payment/alipay/order-by-no/${orderNo}/status`; + + const response = await fetch(statusUrl, { + credentials: 'include', + }); + + if (response.ok) { + const data = await response.json(); + if (data.success && (data.data?.status === 'paid' || data.payment_success)) { + toast({ + title: '支付成功!', + description: '您的订阅已激活', + status: 'success', + duration: 5000, + isClosable: true, + }); + + // 清理 sessionStorage + sessionStorage.removeItem('alipay_order_id'); + sessionStorage.removeItem('alipay_order_no'); + + // 清除 URL 参数并刷新页面 + window.history.replaceState({}, document.title, window.location.pathname); + setTimeout(() => window.location.reload(), 2000); + } else { + toast({ + title: '支付状态待确认', + description: '如已完成支付,请稍候或刷新页面', + status: 'warning', + duration: 5000, + isClosable: true, + }); + // 清除 URL 参数 + window.history.replaceState({}, document.title, window.location.pathname); + } + } else { + // 清除 URL 参数 + window.history.replaceState({}, document.title, window.location.pathname); + } + } catch (error) { + logger.error('SubscriptionContentNew', 'checkAlipayReturn', error); + // 清除 URL 参数 + window.history.replaceState({}, document.title, window.location.pathname); + } + } + }; + + checkAlipayReturn(); + }, [toast]); + const fetchSubscriptionPlans = async () => { try { logger.debug('SubscriptionContentNew', '正在获取订阅套餐'); @@ -409,6 +482,9 @@ export default function SubscriptionContentNew() { ? '/api/payment/alipay/create-order' : '/api/payment/create-order'; + // 检测是否为移动端设备 + const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + const response = await fetch(apiUrl, { method: 'POST', headers: { @@ -419,6 +495,7 @@ export default function SubscriptionContentNew() { plan_name: selectedPlan.name, billing_cycle: selectedCycle, promo_code: promoCodeApplied ? promoCode : null, + is_mobile: isMobileDevice, // 传递设备类型,用于支付宝选择 page/wap 支付 }), }); @@ -428,22 +505,46 @@ export default function SubscriptionContentNew() { if (paymentMethod === 'alipay') { // 支付宝:跳转到支付页面 if (data.data.pay_url) { - setPaymentOrder(data.data); - setPaymentCountdown(30 * 60); - startAutoPaymentCheck(data.data.id, 'alipay'); + // 检测是否为移动端设备 + const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); - toast({ - title: '订单创建成功', - description: '正在跳转到支付宝支付页面...', - status: 'success', - duration: 3000, - isClosable: true, - }); + if (isMobileDevice) { + // 手机端:直接在当前页面跳转(可调起支付宝APP) + toast({ + title: '订单创建成功', + description: '正在跳转到支付宝...', + status: 'success', + duration: 2000, + isClosable: true, + }); - // 延迟跳转,让用户看到提示 - setTimeout(() => { - window.open(data.data.pay_url, '_blank'); - }, 500); + // 保存订单信息到 sessionStorage,支付完成后返回时可以用来检查状态 + sessionStorage.setItem('alipay_order_id', data.data.id); + sessionStorage.setItem('alipay_order_no', data.data.order_no); + + // 延迟跳转,让用户看到提示 + setTimeout(() => { + window.location.href = data.data.pay_url; + }, 500); + } else { + // PC端:新窗口打开 + setPaymentOrder(data.data); + setPaymentCountdown(30 * 60); + startAutoPaymentCheck(data.data.id, 'alipay'); + + toast({ + title: '订单创建成功', + description: '正在跳转到支付宝支付页面...', + status: 'success', + duration: 3000, + isClosable: true, + }); + + // 延迟跳转,让用户看到提示 + setTimeout(() => { + window.open(data.data.pay_url, '_blank'); + }, 500); + } } else { throw new Error('支付链接获取失败'); }