From d7193c3a6327da7ba657232b785ce72432ce63e5 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 12 Dec 2025 13:30:55 +0800 Subject: [PATCH] update pay ui --- __pycache__/alipay_config.cpython-310.pyc | Bin 0 -> 3078 bytes __pycache__/alipay_pay.cpython-310.pyc | Bin 0 -> 10931 bytes alipay/应用公钥.txt | 1 + alipay/应用私钥.txt | 1 + alipay/支付宝公钥.txt | 1 + alipay_config.py | 117 ++++++ alipay_pay.py | 440 +++++++++++++++++++++ alipay_pay_worker.py | 140 +++++++ app.py | 455 +++++++++++++++++++++- src/components/Auth/WechatRegister.js | 2 +- 10 files changed, 1152 insertions(+), 5 deletions(-) create mode 100644 __pycache__/alipay_config.cpython-310.pyc create mode 100644 __pycache__/alipay_pay.cpython-310.pyc create mode 100644 alipay/应用公钥.txt create mode 100644 alipay/应用私钥.txt create mode 100644 alipay/支付宝公钥.txt create mode 100644 alipay_config.py create mode 100644 alipay_pay.py create mode 100644 alipay_pay_worker.py diff --git a/__pycache__/alipay_config.cpython-310.pyc b/__pycache__/alipay_config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1e0e6730a1415b0605ca062b1fb8a5e9df0b1c5 GIT binary patch literal 3078 zcmb7G>vPoB71x#ap>88I6ynf2#K9))CZQ!1Ym|42Vy!*QR}LHSbwgP%gNet|d+b25h!=Cepf8al=21y+Z& zZ~+yN9Kiw(;V_QiD3eFE2o`ay;9L2lctOAwxDr?4>e;v~;u@Sd$H_5Vi|e3`<88Pe z+6ufKH$YpdRpF=ldGe7~jdzSA#6jE0+RAb6I4{@839W`WAl(^gpVmaYtC5@GuJVE8 zJ0N*xi)1%Qq%D#?AbECEVl)BbciH?zC-<9}TnpppwAyvl-~JB9djtIEgZ71>Z3^0C z(0(sy*}5^`&C!zlzVma~)p&7ger0Leo4e|F)+R2lKAbCFesg8%)A;JtKT78=tv6pdj5*NUr`p;efMHV(9qI09`!ejKed^f{V2QWBE%$a7|zMC z-PoZs87*h)rjZonSTC6QhlYAxqcb=_x}w z#%$vZMI3=M#1;Mg2-i!Kc9lc-XYJ;N()?JEs;B3@cmCqdT`0bLmk6Mg!$YP+6z3rH zDc}7>Bo$|G`V*ck!{mF#nMcKoAFeDuSe?D*&3&+X<=p0RFphL}GJZIF0+@2U|wd+1aF~96BPqLGDo(pDgu7hRuulaIb`ewub$rH_p z>13QVLhA~^C^v4|DzU9W9kMM=+M0n~-n85h%fAurjBeQ$qqZw#HG|PC$-5EVvK-BF z1xVFpbY)Dffq##VX&(0joe_}IGsN!@;wT|Bpa_fY1i#rMFVH24iZY<;hN7@^wy_m{ z$3H*+@Q2$D{@;&7zxwg)gj5c+HhQJ>=*=g~W5X4aBz5F+QdeiY#F*sJs}~620^m*D z0bvNnPOUHWWS-qlf1iRG$yE);KHFWtLSdUy*|F{+{=`uIxz$>O9Gxc3wD z+Iw?rA5OOq^OD5=>G6ME`|}?I3jkTW`Ek3`0&`na((3z5-otk{_QYErElz#r-M`~a zE=Wx@Z0Kg@l`@BJyMICHmnd7(Ia zYn|A)A@+QKD~}4rvH4`38&wo+W)y|c;3LmM-nYzK)&9XYXyQ|O6X3=VaDk)Hg6_h@y7#jugGez z(g3p3P=$CgR4?#h-Y-J6P>rAuCE|&@#VRoj?@kekD9(c|s$*|G5}}`9HPz9#k`I;s E3$1D?wg3PC literal 0 HcmV?d00001 diff --git a/__pycache__/alipay_pay.cpython-310.pyc b/__pycache__/alipay_pay.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d76f9af835130d314a35a5da76786ed74806281c GIT binary patch literal 10931 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 a+{=M!nW!9y<0m*8zzV|maV(_#`~LzwT [body] # 创建订单 + python alipay_pay_worker.py query # 查询订单 +""" + +import sys +import json + + +def check_config(): + """检查支付宝配置""" + try: + from alipay_pay import check_alipay_ready + is_ready, message = check_alipay_ready() + return { + 'success': is_ready, + 'message': message if is_ready else None, + 'error': None if is_ready else message + } + except Exception as e: + return { + 'success': False, + 'error': str(e) + } + + +def create_order(order_no, amount, subject, body=None): + """创建支付宝订单""" + 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' + ) + + return result + except Exception as e: + return { + 'success': False, + 'error': str(e) + } + + +def query_order(order_no): + """查询支付宝订单""" + try: + from alipay_pay import create_alipay_instance + alipay = create_alipay_instance() + + result = alipay.query_order(out_trade_no=order_no) + + # 转换状态为统一格式 + if result.get('success'): + trade_status = result.get('trade_status') + # 映射支付宝状态到统一状态 + if trade_status in ['TRADE_SUCCESS', 'TRADE_FINISHED']: + result['trade_state'] = 'SUCCESS' + elif trade_status == 'WAIT_BUYER_PAY': + result['trade_state'] = 'NOTPAY' + elif trade_status == 'TRADE_CLOSED': + result['trade_state'] = 'CLOSED' + else: + result['trade_state'] = trade_status + + return result + except Exception as e: + return { + 'success': False, + 'error': str(e) + } + + +def main(): + """主函数""" + if len(sys.argv) < 2: + print(json.dumps({ + 'success': False, + 'error': '缺少命令参数。用法: check | create | query ' + })) + sys.exit(1) + + command = sys.argv[1].lower() + + try: + if command == 'check': + result = check_config() + + elif command == 'create': + if len(sys.argv) < 5: + result = { + 'success': False, + 'error': '创建订单需要参数: order_no, amount, subject' + } + else: + order_no = sys.argv[2] + 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) + + elif command == 'query': + if len(sys.argv) < 3: + result = { + 'success': False, + 'error': '查询订单需要参数: order_no' + } + else: + order_no = sys.argv[2] + result = query_order(order_no) + + else: + result = { + 'success': False, + 'error': f'未知命令: {command}' + } + + print(json.dumps(result, ensure_ascii=False)) + + except Exception as e: + print(json.dumps({ + 'success': False, + 'error': str(e) + }, ensure_ascii=False)) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/app.py b/app.py index b2f93fde..9c245948 100755 --- a/app.py +++ b/app.py @@ -1063,9 +1063,12 @@ class PaymentOrder(db.Model): original_amount = db.Column(db.Numeric(10, 2), nullable=True) # 原价 discount_amount = db.Column(db.Numeric(10, 2), nullable=True, default=0) # 折扣金额 promo_code_id = db.Column(db.Integer, db.ForeignKey('promo_codes.id'), nullable=True) # 优惠码ID - wechat_order_id = db.Column(db.String(64), nullable=True) + payment_method = db.Column(db.String(20), default='wechat') # 支付方式: wechat/alipay + wechat_order_id = db.Column(db.String(64), nullable=True) # 微信交易号 + alipay_trade_no = db.Column(db.String(64), nullable=True) # 支付宝交易号 prepay_id = db.Column(db.String(64), nullable=True) - qr_code_url = db.Column(db.String(200), nullable=True) + qr_code_url = db.Column(db.String(200), nullable=True) # 微信支付二维码URL + pay_url = db.Column(db.String(2000), nullable=True) # 支付宝支付链接(较长) status = db.Column(db.String(20), default='pending') created_at = db.Column(db.DateTime, default=beijing_now) paid_at = db.Column(db.DateTime, nullable=True) @@ -1097,10 +1100,25 @@ class PaymentOrder(db.Model): except Exception as e: return False - def mark_as_paid(self, wechat_order_id, transaction_id=None): + def mark_as_paid(self, transaction_id, payment_method=None): + """ + 标记订单为已支付 + + Args: + transaction_id: 交易号(微信或支付宝) + payment_method: 支付方式(可选,如果已设置则不覆盖) + """ self.status = 'paid' self.paid_at = beijing_now() - self.wechat_order_id = wechat_order_id + + # 根据支付方式存储交易号 + if payment_method: + self.payment_method = payment_method + + if self.payment_method == 'alipay': + self.alipay_trade_no = transaction_id + else: + self.wechat_order_id = transaction_id def to_dict(self): return { @@ -1113,7 +1131,9 @@ class PaymentOrder(db.Model): 'original_amount': float(self.original_amount) if self.original_amount else None, 'discount_amount': float(self.discount_amount) if self.discount_amount else 0, 'promo_code': self.promo_code.code if self.promo_code else None, + 'payment_method': self.payment_method or 'wechat', 'qr_code_url': self.qr_code_url, + 'pay_url': self.pay_url, 'status': self.status, 'is_expired': self.is_expired(), 'created_at': self.created_at.isoformat() if self.created_at else None, @@ -2645,6 +2665,433 @@ def _parse_xml_callback(xml_data): return None +# ======================================== +# 支付宝支付相关API +# ======================================== + +@app.route('/api/payment/alipay/create-order', methods=['POST']) +def create_alipay_order(): + """ + 创建支付宝支付订单 + + Request Body: + { + "plan_name": "pro", + "billing_cycle": "yearly", + "promo_code": "WELCOME2025" // 可选 + } + """ + try: + if 'user_id' not in session: + return jsonify({'success': False, 'error': '未登录'}), 401 + + data = request.get_json() + plan_name = data.get('plan_name') + billing_cycle = data.get('billing_cycle') + promo_code = (data.get('promo_code') or '').strip() or None + + if not plan_name or not billing_cycle: + return jsonify({'success': False, 'error': '参数不完整'}), 400 + + # 使用简化价格计算 + price_result = calculate_subscription_price_simple(session['user_id'], plan_name, billing_cycle, promo_code) + + if 'error' in price_result: + return jsonify({'success': False, 'error': price_result['error']}), 400 + + amount = price_result['final_amount'] + subscription_type = price_result.get('subscription_type', 'new') + + # 检查是否为免费升级 + if amount <= 0 and price_result.get('is_upgrade'): + return jsonify({ + 'success': False, + 'error': '当前剩余价值可直接免费升级,请使用免费升级功能', + 'should_free_upgrade': True, + 'price_info': price_result + }), 400 + + # 创建订单 + try: + original_amount = price_result.get('original_amount', amount) + discount_amount = price_result.get('discount_amount', 0) + + order = PaymentOrder( + user_id=session['user_id'], + plan_name=plan_name, + billing_cycle=billing_cycle, + amount=amount, + original_amount=original_amount, + discount_amount=discount_amount + ) + + # 设置支付方式为支付宝 + order.payment_method = 'alipay' + order.remark = f"{subscription_type}订阅" if subscription_type == 'renew' else "新购订阅" + + # 关联优惠码 + if promo_code and price_result.get('promo_code'): + promo_obj = PromoCode.query.filter_by(code=promo_code.upper()).first() + if promo_obj: + order.promo_code_id = promo_obj.id + print(f"📦 订单关联优惠码: {promo_obj.code} (ID: {promo_obj.id})") + + db.session.add(order) + db.session.commit() + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'error': f'订单创建失败: {str(e)}'}), 500 + + # 调用支付宝支付API(使用 subprocess 绕过 eventlet DNS 问题) + try: + import subprocess + + script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'alipay_pay_worker.py') + + # 先检查配置 + check_result = subprocess.run( + [sys.executable, script_path, 'check'], + capture_output=True, text=True, timeout=10 + ) + + if check_result.returncode != 0: + check_data = json.loads(check_result.stdout) if check_result.stdout else {} + error_msg = check_data.get('error', check_data.get('message', '支付宝配置错误')) + order.remark = f"支付宝配置错误 - {error_msg}" + db.session.commit() + return jsonify({ + 'success': False, + 'error': f'支付宝支付暂不可用: {error_msg}' + }), 500 + + # 创建支付宝订单 + plan_display_name = f"{plan_name.upper()}版本-{billing_cycle}" + subject = f"VFr-{plan_display_name}" + body = f"价值前沿订阅服务-{plan_display_name}" + + create_result = subprocess.run( + [sys.executable, script_path, 'create', order.order_no, str(float(amount)), subject, body], + capture_output=True, text=True, timeout=60 + ) + + print(f"[支付宝] 创建订单返回: {create_result.stdout}") + if create_result.stderr: + print(f"[支付宝] 错误输出: {create_result.stderr}") + + alipay_result = json.loads(create_result.stdout) if create_result.stdout else {'success': False, 'error': '无返回'} + + if alipay_result.get('success'): + # 获取支付宝返回的支付链接 + pay_url = alipay_result['pay_url'] + order.pay_url = pay_url + order.remark = f"支付宝支付 - 订单已创建" + db.session.commit() + + return jsonify({ + 'success': True, + 'data': order.to_dict(), + 'message': '订单创建成功' + }) + else: + order.remark = f"支付宝支付失败: {alipay_result.get('error')}" + db.session.commit() + return jsonify({ + 'success': False, + 'error': f"支付宝订单创建失败: {alipay_result.get('error')}" + }), 500 + + except subprocess.TimeoutExpired: + order.remark = "支付宝支付超时" + db.session.commit() + return jsonify({'success': False, 'error': '支付宝支付超时'}), 500 + except json.JSONDecodeError as e: + order.remark = f"支付宝返回解析失败: {str(e)}" + db.session.commit() + return jsonify({'success': False, 'error': '支付宝返回数据异常'}), 500 + except Exception as e: + import traceback + print(f"[支付宝] Exception: {e}") + traceback.print_exc() + order.remark = f"支付异常: {str(e)}" + db.session.commit() + return jsonify({'success': False, 'error': '支付异常'}), 500 + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'error': '创建订单失败'}), 500 + + +@app.route('/api/payment/alipay/callback', methods=['POST']) +def alipay_payment_callback(): + """支付宝异步回调处理""" + try: + # 获取POST参数 + callback_params = request.form.to_dict() + print(f"📥 收到支付宝支付回调: {callback_params}") + + # 验证回调数据 + try: + from alipay_pay import create_alipay_instance + alipay = create_alipay_instance() + verify_result = alipay.verify_callback(callback_params.copy()) + + if not verify_result['success']: + print(f"❌ 支付宝回调签名验证失败: {verify_result['error']}") + return 'fail' + + callback_data = verify_result['data'] + + except Exception as e: + print(f"❌ 支付宝回调处理异常: {e}") + return 'fail' + + # 获取关键字段 + trade_status = callback_data.get('trade_status') + out_trade_no = callback_data.get('out_trade_no') # 商户订单号 + trade_no = callback_data.get('trade_no') # 支付宝交易号 + total_amount = callback_data.get('total_amount') + + print(f"📦 支付宝回调数据解析:") + print(f" 交易状态: {trade_status}") + print(f" 订单号: {out_trade_no}") + print(f" 交易号: {trade_no}") + print(f" 金额: {total_amount}") + + if not out_trade_no: + print("❌ 缺少订单号") + return 'fail' + + # 查找订单 + order = PaymentOrder.query.filter_by(order_no=out_trade_no).first() + if not order: + print(f"❌ 订单不存在: {out_trade_no}") + return 'fail' + + # 只处理交易成功的回调 + if trade_status in ['TRADE_SUCCESS', 'TRADE_FINISHED']: + print(f"🎉 支付宝支付成功: 订单 {out_trade_no}") + + # 检查订单是否已经处理过 + if order.status == 'paid': + print(f"ℹ️ 订单已处理过: {out_trade_no}") + return 'success' + + # 更新订单状态 + old_status = order.status + order.mark_as_paid(trade_no, 'alipay') + print(f"📝 订单状态已更新: {old_status} -> paid") + + # 激活用户订阅 + subscription = activate_user_subscription(order.user_id, order.plan_name, order.billing_cycle) + + if subscription: + print(f"✅ 用户订阅已激活: 用户{order.user_id}, 套餐{order.plan_name}") + else: + print(f"⚠️ 订阅激活失败,但订单已标记为已支付") + + # 记录优惠码使用情况 + if order.promo_code_id: + try: + existing_usage = PromoCodeUsage.query.filter_by(order_id=order.id).first() + + if not existing_usage: + usage = PromoCodeUsage( + promo_code_id=order.promo_code_id, + user_id=order.user_id, + order_id=order.id, + original_amount=order.original_amount or order.amount, + discount_amount=order.discount_amount or 0, + final_amount=order.amount + ) + db.session.add(usage) + + promo = PromoCode.query.get(order.promo_code_id) + if promo: + promo.current_uses = (promo.current_uses or 0) + 1 + print(f"🎫 优惠码使用记录已创建: {promo.code}, 当前使用次数: {promo.current_uses}") + else: + print(f"ℹ️ 优惠码使用记录已存在,跳过") + except Exception as e: + print(f"⚠️ 记录优惠码使用失败: {e}") + + db.session.commit() + + elif trade_status == 'TRADE_CLOSED': + # 交易关闭 + if order.status not in ['paid', 'cancelled']: + order.status = 'cancelled' + db.session.commit() + print(f"📝 订单已关闭: {out_trade_no}") + + # 返回成功响应给支付宝 + return 'success' + + except Exception as e: + db.session.rollback() + print(f"❌ 支付宝回调处理失败: {e}") + import traceback + app.logger.error(f"支付宝回调处理错误: {e}", exc_info=True) + return 'fail' + + +@app.route('/api/payment/alipay/return', methods=['GET']) +def alipay_payment_return(): + """支付宝同步返回处理(用户支付后跳转回来)""" + try: + # 获取GET参数 + return_params = request.args.to_dict() + print(f"📥 支付宝同步返回: {return_params}") + + out_trade_no = return_params.get('out_trade_no') + + if out_trade_no: + # 重定向到前端支付结果页面 + return redirect(f'/pricing?payment_return=alipay&order_no={out_trade_no}') + else: + return redirect('/pricing?payment_return=alipay&error=missing_order') + + except Exception as e: + print(f"❌ 支付宝同步返回处理失败: {e}") + return redirect('/pricing?payment_return=alipay&error=exception') + + +@app.route('/api/payment/alipay/order//status', methods=['GET']) +def check_alipay_order_status(order_id): + """查询支付宝订单支付状态""" + try: + if 'user_id' not in session: + return jsonify({'success': False, 'error': '未登录'}), 401 + + # 查找订单 + order = PaymentOrder.query.filter_by( + id=order_id, + user_id=session['user_id'] + ).first() + + if not order: + return jsonify({'success': False, 'error': '订单不存在'}), 404 + + # 如果订单已经是已支付状态,直接返回 + if order.status == 'paid': + return jsonify({ + 'success': True, + 'data': order.to_dict(), + 'message': '订单已支付', + 'payment_success': True + }) + + # 如果订单过期,标记为过期 + if order.is_expired(): + order.status = 'expired' + db.session.commit() + return jsonify({ + 'success': True, + 'data': order.to_dict(), + 'message': '订单已过期' + }) + + # 调用支付宝API查询真实状态 + try: + import subprocess + + script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'alipay_pay_worker.py') + + query_proc = subprocess.run( + [sys.executable, script_path, 'query', order.order_no], + capture_output=True, text=True, timeout=30 + ) + + query_result = json.loads(query_proc.stdout) if query_proc.stdout else {'success': False, 'error': '无返回'} + + if query_result.get('success'): + trade_state = query_result.get('trade_state') + trade_no = query_result.get('trade_no') + + if trade_state == 'SUCCESS': + # 支付成功,更新订单状态 + order.mark_as_paid(trade_no, 'alipay') + + # 激活用户订阅 + activate_user_subscription(order.user_id, order.plan_name, order.billing_cycle) + + # 记录优惠码使用情况 + if order.promo_code_id: + try: + existing_usage = PromoCodeUsage.query.filter_by(order_id=order.id).first() + if not existing_usage: + usage = PromoCodeUsage( + promo_code_id=order.promo_code_id, + user_id=order.user_id, + order_id=order.id, + original_amount=order.original_amount or order.amount, + discount_amount=order.discount_amount or 0, + final_amount=order.amount + ) + db.session.add(usage) + promo = PromoCode.query.get(order.promo_code_id) + if promo: + promo.current_uses = (promo.current_uses or 0) + 1 + print(f"🎫 优惠码使用记录已创建: {promo.code}") + except Exception as e: + print(f"⚠️ 记录优惠码使用失败: {e}") + + db.session.commit() + + return jsonify({ + 'success': True, + 'data': order.to_dict(), + 'message': '支付成功!订阅已激活', + 'payment_success': True + }) + elif trade_state in ['NOTPAY', 'WAIT_BUYER_PAY']: + # 未支付或等待支付 + return jsonify({ + 'success': True, + 'data': order.to_dict(), + 'message': '等待支付...', + 'payment_success': False + }) + elif trade_state in ['CLOSED', 'TRADE_CLOSED']: + # 交易关闭 + order.status = 'cancelled' + db.session.commit() + return jsonify({ + 'success': True, + 'data': order.to_dict(), + 'message': '交易已关闭', + 'payment_success': False + }) + else: + # 其他状态 + return jsonify({ + 'success': True, + 'data': order.to_dict(), + 'message': f'当前状态: {trade_state}', + 'payment_success': False + }) + else: + # 支付宝查询失败,返回当前状态 + return jsonify({ + 'success': True, + 'data': order.to_dict(), + 'message': f"查询失败: {query_result.get('error')}", + 'payment_success': False + }) + + except Exception as e: + # 查询失败,返回当前订单状态 + return jsonify({ + 'success': True, + 'data': order.to_dict(), + 'message': '无法查询支付状态,请稍后重试', + 'payment_success': False + }) + + 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/Auth/WechatRegister.js b/src/components/Auth/WechatRegister.js index 1f077acb..81090afa 100644 --- a/src/components/Auth/WechatRegister.js +++ b/src/components/Auth/WechatRegister.js @@ -496,7 +496,7 @@ export default function WechatRegister() { width="300" height="350" scrolling="no" - sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation" + sandbox="allow-scripts allow-same-origin allow-forms allow-popups" allow="clipboard-write" style={{ border: 'none',