diff --git a/__pycache__/alipay_config.cpython-310.pyc b/__pycache__/alipay_config.cpython-310.pyc new file mode 100644 index 00000000..a1e0e673 Binary files /dev/null and b/__pycache__/alipay_config.cpython-310.pyc differ diff --git a/__pycache__/alipay_pay.cpython-310.pyc b/__pycache__/alipay_pay.cpython-310.pyc new file mode 100644 index 00000000..d76f9af8 Binary files /dev/null and b/__pycache__/alipay_pay.cpython-310.pyc differ diff --git a/alipay/应用公钥.txt b/alipay/应用公钥.txt new file mode 100644 index 00000000..3751e8ac --- /dev/null +++ b/alipay/应用公钥.txt @@ -0,0 +1 @@ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkBfIjOiu8vfmOSq1BXcjDsAqQ+xtwGO0aCn0VrhVzc0T70nDchaW/TJ4nW8qlRMBlgfTi00jDGFiW4ND9JHc4aES8yiDSNeaBW4gLQhC1isvpOndyu4YgDc+2lMfghv9+6D8uFl9VS8Vk6o7D+5SiE7F8aBn49rrLyvsmdN5i6eLxIuY9NM58/o0xVG5f3ktGqfFKzhtclPbt8ej39EgziCiNFbIk2FnZp9dB56vtmCKht4t3STDpM0RfC8YlQ2WpGu9okLJYSy1rfynhh0hlOy/9y4cYl50wthoQVxH/Hm9abiTMo2u6xWESreavtdDZ8ByKVltnUrRvzDQ4tVkYwIDAQAB \ No newline at end of file diff --git a/alipay/应用私钥.txt b/alipay/应用私钥.txt new file mode 100644 index 00000000..bb3d0518 --- /dev/null +++ b/alipay/应用私钥.txt @@ -0,0 +1 @@ +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCQF8iM6K7y9+Y5KrUFdyMOwCpD7G3AY7RoKfRWuFXNzRPvScNyFpb9MnidbyqVEwGWB9OLTSMMYWJbg0P0kdzhoRLzKINI15oFbiAtCELWKy+k6d3K7hiANz7aUx+CG/37oPy4WX1VLxWTqjsP7lKITsXxoGfj2usvK+yZ03mLp4vEi5j00znz+jTFUbl/eS0ap8UrOG1yU9u3x6Pf0SDOIKI0VsiTYWdmn10Hnq+2YIqG3i3dJMOkzRF8LxiVDZaka72iQslhLLWt/KeGHSGU7L/3LhxiXnTC2GhBXEf8eb1puJMyja7rFYRKt5q+10NnwHIpWW2dStG/MNDi1WRjAgMBAAECggEAHQ8+2fQfPE70djj/su94+YOVwocPB0rUWmGDrm2UmGGwkISezwZxQvUH0DBYNSJVIo3HgwN2ewu0y2HotY0pL7PNX46fE3Sv0kKIaKyO1iR1glvL6B4mgM0jduJmq1W73iB0dzVNCn3paxNcv/S/XlAMqZNBAHnpDmVcXRWCIMDG1yRN3l5NbfgPoQZI4MfAdthjIcIQmEVjNWy69swsdNndj8yrIu9E9RlWly/xqB/BJ6O53i0n+jyviy2grgHxo9VgWKv89qZiczioLT7aAJITpcMofUkGImZy+DHlf+6GU762UkwbLykYN2RRkw5TPvWt4ZUXeON4flr3ig02yQKBgQDMh82rc3TklOZSCw3lwYE58eeYxe9PXpZzcOyrSZZhCs0y528dmwYbm0mPlpVti1MGKnn2eGVKeGQ8a5CbJCi2T+VwIILWM9U2+h82nJW5KD6CYaE86KK/PlzhTGwmpecv8hafkpn51zvyjI3UkKbv/Ep+Mfy89PLumvh5Ze+EfwKBgQC0Wn1o0JCjgCt3K39tahavBuCPDvk7oLA6JLp2W9437YFeuh9L/gYdAtJU79WpmOfgr228cOlExdGGpHqLPSDFpN3ssx1pkUJ6RlTN8YInc+dbAkC6gsm5XR0Jvj4YqghyWHKhxXErnFGDof2EQq7ghHK9pokpBFPowwlzkpOeHQKBgFbqVwJG/COvCvlObUd3pbzECdEoO/wUjAbetBROHzN57Z12MAf6uuu8X9Q+/50fmdaC8nVE0HaHFsF+TGNBSHPBHBU8G51/RVopjF4eyJl4eqfZaTWC/rYagEnVuhfqZIZBcE+7cudzCayXAiaUmfxd0CI0h9yckyfGf1THdrNtAoGASMtNWwTznEqbQJpZ8HuldDe+Y3+TsTGGb7FrYWJrKv+9+9H719xL82G0K3wyLSX+UX39ONYKESwXCdVRcOnXVG7a9DLHaFitEFVa3VThR7NMajtajO1FJoAivFABGEto5V41xn2+0+9gJ1U20i9oDk7nUQzqx5drlsNCCVfcJTECgYEAlEYIQ3AiVqx0RqZjfbg+nyZQmoUfHPASY2Hu9pJWvLwXsQWSPh1gf03galXzZ46wrCaeLygGaoHXW7W8WwsYBR1tG7voQeSe8mmlWgiscmvmvqSowJ10BnsdWwpOTZ8O6rKy8HdyI8gFJyfJgfz+6KekcdGEnQbmwCvB8j+Y7IQ= \ No newline at end of file diff --git a/alipay/支付宝公钥.txt b/alipay/支付宝公钥.txt new file mode 100644 index 00000000..79415837 --- /dev/null +++ b/alipay/支付宝公钥.txt @@ -0,0 +1 @@ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjyULL5tuD2cjfNvgn9fvVfn+WbLhoP3wk3bcYb9AabsZc1GCBlnG579Socc3U9dG5IR7C3KHD4kYHnH4tbK2pEWmmfjbVKXWguLqL5K3Dggnl5KVOlVVjrcsmLPqTj7JdeO0AQolmgdr/o6TlhQdsqINQAK5F5wWwIwwcSoJsWZ6zlPPX/Q/eMIN4zGgK2taMhx656zSxsYE5OKRYkTJhVrMktxQdwbUzoFSID++dTpjxF4w5k5qeVW/1WZaaswMFWh2IcJ5oyc+VjTRqZvtQt4gB0Fa0EblfmSJaozhoWHwzwF+1qtv7gp/TcMYj/Ah2+tY0UPxucEcTqY/i7PPfwIDAQAB \ No newline at end of file diff --git a/alipay_config.py b/alipay_config.py new file mode 100644 index 00000000..ff4a1f8e --- /dev/null +++ b/alipay_config.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +支付宝支付配置文件 +电脑网站支付 (alipay.trade.page.pay) +""" +import os + +# 获取当前文件所在目录 +_BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +# 支付宝支付配置 +ALIPAY_CONFIG = { + # 应用ID - 从支付宝开放平台获取 + 'app_id': '2021005183650009', + + # 支付宝网关 - 正式环境 + 'gateway_url': 'https://openapi.alipay.com/gateway.do', + # 沙箱环境网关(测试用) + # 'gateway_url': 'https://openapi-sandbox.dl.alipaydev.com/gateway.do', + + # 签名方式 - 必须使用RSA2 + 'sign_type': 'RSA2', + + # 编码格式 + 'charset': 'utf-8', + + # 返回格式 + 'format': 'json', + + # 密钥文件路径 + 'app_private_key_path': os.path.join(_BASE_DIR, 'alipay', '应用私钥.txt'), + 'alipay_public_key_path': os.path.join(_BASE_DIR, 'alipay', '支付宝公钥.txt'), + + # 回调地址 - 替换为你的实际域名 + 'notify_url': 'https://valuefrontier.cn/api/payment/alipay/callback', # 异步通知地址(后端) + 'return_url': 'https://valuefrontier.cn/pricing?payment_return=alipay', # 同步跳转地址(前端页面) + + # 产品码 - 电脑网站支付固定为此值 + 'product_code': 'FAST_INSTANT_TRADE_PAY', +} + + +def load_key_from_file(file_path): + """从文件读取密钥内容""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + return f.read().strip() + except FileNotFoundError: + print(f"❌ 密钥文件不存在: {file_path}") + return None + except Exception as e: + print(f"❌ 读取密钥文件失败: {e}") + return None + + +def get_app_private_key(): + """获取应用私钥""" + return load_key_from_file(ALIPAY_CONFIG['app_private_key_path']) + + +def get_alipay_public_key(): + """获取支付宝公钥""" + return load_key_from_file(ALIPAY_CONFIG['alipay_public_key_path']) + + +def validate_config(): + """验证配置是否完整""" + issues = [] + + # 检查 app_id + if not ALIPAY_CONFIG.get('app_id') or ALIPAY_CONFIG['app_id'].startswith('your_'): + issues.append("❌ app_id 未配置,请替换为真实的支付宝应用ID") + + # 检查密钥文件 + if not os.path.exists(ALIPAY_CONFIG['app_private_key_path']): + issues.append(f"❌ 应用私钥文件不存在: {ALIPAY_CONFIG['app_private_key_path']}") + else: + key = get_app_private_key() + if not key or len(key) < 100: + issues.append("❌ 应用私钥内容异常,请检查文件格式") + + if not os.path.exists(ALIPAY_CONFIG['alipay_public_key_path']): + issues.append(f"❌ 支付宝公钥文件不存在: {ALIPAY_CONFIG['alipay_public_key_path']}") + else: + key = get_alipay_public_key() + if not key or len(key) < 100: + issues.append("❌ 支付宝公钥内容异常,请检查文件格式") + + return len(issues) == 0, issues + + +if __name__ == "__main__": + print("支付宝支付配置验证") + print("=" * 50) + + is_valid, issues = validate_config() + + if is_valid: + print("✅ 配置验证通过!") + print(f" App ID: {ALIPAY_CONFIG['app_id']}") + print(f" 网关: {ALIPAY_CONFIG['gateway_url']}") + print(f" 回调地址: {ALIPAY_CONFIG['notify_url']}") + print(f" 同步跳转: {ALIPAY_CONFIG['return_url']}") + else: + print("⚠️ 配置存在问题:") + for issue in issues: + print(f" {issue}") + + print("\n📋 配置步骤:") + print("1. 登录支付宝开放平台 (open.alipay.com)") + print("2. 创建应用并获取 App ID") + print("3. 在开发设置中配置RSA2密钥") + print("4. 将应用私钥、支付宝公钥放到 ./alipay/ 文件夹") + print("5. 更新本文件中的配置信息") + + print("=" * 50) diff --git a/alipay_pay.py b/alipay_pay.py new file mode 100644 index 00000000..9affb942 --- /dev/null +++ b/alipay_pay.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +支付宝支付集成模块 +电脑网站支付 (alipay.trade.page.pay) +""" + +import json +import time +import base64 +import hashlib +from datetime import datetime +from urllib.parse import quote_plus, urlencode +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.backends import default_backend + + +class AlipayPay: + """支付宝电脑网站支付类""" + + def __init__(self, app_id, app_private_key, alipay_public_key, notify_url, return_url, + gateway_url='https://openapi.alipay.com/gateway.do', + sign_type='RSA2', charset='utf-8'): + """ + 初始化支付宝支付配置 + + Args: + app_id: 支付宝应用ID + app_private_key: 应用私钥(Base64编码的字符串) + alipay_public_key: 支付宝公钥(Base64编码的字符串) + notify_url: 异步通知地址 + return_url: 同步跳转地址 + gateway_url: 支付宝网关URL + sign_type: 签名类型,默认RSA2 + charset: 编码,默认utf-8 + """ + self.app_id = app_id + self.notify_url = notify_url + self.return_url = return_url + self.gateway_url = gateway_url + self.sign_type = sign_type + self.charset = charset + + # 加载密钥 + self.app_private_key = self._load_private_key(app_private_key) + self.alipay_public_key = self._load_public_key(alipay_public_key) + + print(f"✅ 支付宝支付初始化成功") + print(f" App ID: {app_id}") + print(f" 网关: {gateway_url}") + + def _load_private_key(self, key_str): + """加载RSA私钥(支持 PKCS#1 和 PKCS#8 格式)""" + try: + # 如果密钥不包含头尾,尝试添加PEM格式头尾 + if '-----BEGIN' not in key_str: + # 支付宝通常使用 PKCS#8 格式(MIIEv 开头) + # 先尝试 PKCS#8 格式 + key_str_pkcs8 = f"-----BEGIN PRIVATE KEY-----\n{key_str}\n-----END PRIVATE KEY-----" + try: + private_key = serialization.load_pem_private_key( + key_str_pkcs8.encode('utf-8'), + password=None, + backend=default_backend() + ) + return private_key + except Exception: + # 如果 PKCS#8 失败,尝试 PKCS#1 格式 + key_str = f"-----BEGIN RSA PRIVATE KEY-----\n{key_str}\n-----END RSA PRIVATE KEY-----" + + private_key = serialization.load_pem_private_key( + key_str.encode('utf-8'), + password=None, + backend=default_backend() + ) + return private_key + except Exception as e: + print(f"[AlipayPay] Load private key failed: {e}") + raise + + def _load_public_key(self, key_str): + """加载RSA公钥""" + try: + # 如果密钥不包含头尾,添加PEM格式头尾 + if '-----BEGIN' not in key_str: + key_str = f"-----BEGIN PUBLIC KEY-----\n{key_str}\n-----END PUBLIC KEY-----" + + public_key = serialization.load_pem_public_key( + key_str.encode('utf-8'), + backend=default_backend() + ) + return public_key + except Exception as e: + print(f"❌ 加载公钥失败: {e}") + raise + + def _sign(self, unsigned_string): + """ + RSA2签名 + + Args: + unsigned_string: 待签名字符串 + + Returns: + Base64编码的签名 + """ + try: + signature = self.app_private_key.sign( + unsigned_string.encode('utf-8'), + padding.PKCS1v15(), + hashes.SHA256() + ) + return base64.b64encode(signature).decode('utf-8') + except Exception as e: + print(f"❌ 签名失败: {e}") + raise + + def _verify(self, message, signature): + """ + 验证支付宝签名 + + Args: + message: 原始消息 + signature: Base64编码的签名 + + Returns: + bool: 验证是否通过 + """ + try: + self.alipay_public_key.verify( + base64.b64decode(signature), + message.encode('utf-8'), + padding.PKCS1v15(), + hashes.SHA256() + ) + return True + except Exception as e: + print(f"❌ 签名验证失败: {e}") + return False + + def _get_sign_content(self, params): + """ + 获取待签名字符串 + + Args: + params: 参数字典 + + Returns: + 排序后的参数字符串 + """ + # 过滤空值和sign参数,按key排序 + filtered_params = {k: v for k, v in params.items() + if v is not None and v != '' and k != 'sign'} + sorted_params = sorted(filtered_params.items()) + + # 拼接字符串 + sign_content = '&'.join([f'{k}={v}' for k, v in sorted_params]) + return sign_content + + def create_page_pay_url(self, out_trade_no, total_amount, subject, body=None, timeout_express='30m'): + """ + 创建电脑网站支付URL + + Args: + out_trade_no: 商户订单号 + total_amount: 订单总金额(元,精确到小数点后两位) + subject: 订单标题 + body: 订单描述(可选) + timeout_express: 订单超时时间,默认30分钟 + + Returns: + dict: 包含支付URL的响应 + """ + try: + # 业务参数 + biz_content = { + 'out_trade_no': out_trade_no, + 'total_amount': str(total_amount), + 'subject': subject, + 'product_code': 'FAST_INSTANT_TRADE_PAY', # 电脑网站支付固定值 + 'timeout_express': timeout_express, + } + if body: + biz_content['body'] = body + + # 公共请求参数 + params = { + 'app_id': self.app_id, + 'method': 'alipay.trade.page.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)}" + + print(f"✅ 支付宝订单创建成功: {out_trade_no}") + print(f" 金额: {total_amount}元") + print(f" 标题: {subject}") + + return { + 'success': True, + 'pay_url': pay_url, + 'order_no': out_trade_no + } + + except Exception as e: + print(f"❌ 创建支付宝订单失败: {e}") + return { + 'success': False, + 'error': str(e) + } + + def query_order(self, out_trade_no=None, trade_no=None): + """ + 查询订单状态 + + Args: + out_trade_no: 商户订单号 + trade_no: 支付宝交易号 + + Returns: + dict: 订单状态信息 + """ + import requests + + try: + if not out_trade_no and not trade_no: + return {'success': False, 'error': '订单号和交易号不能同时为空'} + + # 业务参数 + biz_content = {} + if out_trade_no: + biz_content['out_trade_no'] = out_trade_no + if trade_no: + biz_content['trade_no'] = trade_no + + # 公共请求参数 + params = { + 'app_id': self.app_id, + 'method': 'alipay.trade.query', + 'format': 'json', + 'charset': self.charset, + 'sign_type': self.sign_type, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'version': '1.0', + 'biz_content': json.dumps(biz_content, ensure_ascii=False), + } + + # 生成签名 + sign_content = self._get_sign_content(params) + params['sign'] = self._sign(sign_content) + + # 发送请求 + response = requests.get(self.gateway_url, params=params, timeout=30) + result = response.json() + + print(f"📡 支付宝查询响应: {result}") + + # 解析响应 + query_response = result.get('alipay_trade_query_response', {}) + if query_response.get('code') == '10000': + trade_status = query_response.get('trade_status') + return { + 'success': True, + 'trade_status': trade_status, + 'trade_no': query_response.get('trade_no'), + 'out_trade_no': query_response.get('out_trade_no'), + 'total_amount': query_response.get('total_amount'), + 'buyer_logon_id': query_response.get('buyer_logon_id'), + 'send_pay_date': query_response.get('send_pay_date'), + # 状态映射 + 'is_paid': trade_status == 'TRADE_SUCCESS' or trade_status == 'TRADE_FINISHED', + 'is_closed': trade_status == 'TRADE_CLOSED', + 'is_waiting': trade_status == 'WAIT_BUYER_PAY', + } + else: + error_msg = query_response.get('sub_msg') or query_response.get('msg', '查询失败') + return { + 'success': False, + 'error': error_msg, + 'code': query_response.get('code'), + 'sub_code': query_response.get('sub_code') + } + + except requests.RequestException as e: + print(f"❌ 支付宝API请求失败: {e}") + return {'success': False, 'error': f'网络请求失败: {e}'} + except Exception as e: + print(f"❌ 查询订单异常: {e}") + return {'success': False, 'error': str(e)} + + def verify_callback(self, params): + """ + 验证支付宝异步回调 + + Args: + params: 回调参数字典 + + Returns: + dict: 验证结果 + """ + try: + # 获取签名 + sign = params.pop('sign', None) + sign_type = params.pop('sign_type', 'RSA2') + + if not sign: + return {'success': False, 'error': '缺少签名参数'} + + # 构建待验签字符串 + sign_content = self._get_sign_content(params) + + # 验证签名 + if self._verify(sign_content, sign): + print(f"✅ 支付宝回调签名验证通过") + return { + 'success': True, + 'data': params + } + else: + print(f"❌ 支付宝回调签名验证失败") + return { + 'success': False, + 'error': '签名验证失败' + } + + except Exception as e: + print(f"❌ 验证回调异常: {e}") + return {'success': False, 'error': str(e)} + + def verify_return(self, params): + """ + 验证支付宝同步返回(用户支付后跳转回来) + + Args: + params: 同步返回参数字典 + + Returns: + dict: 验证结果 + """ + # 同步返回的验签逻辑与异步回调相同 + return self.verify_callback(params) + + +# 工厂函数 +def create_alipay_instance(): + """创建支付宝支付实例""" + try: + from alipay_config import ALIPAY_CONFIG, get_app_private_key, get_alipay_public_key, validate_config + + # 验证配置 + is_valid, issues = validate_config() + if not is_valid: + raise ValueError(f"支付宝配置错误: {'; '.join(issues)}") + + # 获取密钥 + app_private_key = get_app_private_key() + alipay_public_key = get_alipay_public_key() + + if not app_private_key or not alipay_public_key: + raise ValueError("密钥加载失败") + + return AlipayPay( + app_id=ALIPAY_CONFIG['app_id'], + app_private_key=app_private_key, + alipay_public_key=alipay_public_key, + notify_url=ALIPAY_CONFIG['notify_url'], + return_url=ALIPAY_CONFIG['return_url'], + gateway_url=ALIPAY_CONFIG['gateway_url'], + sign_type=ALIPAY_CONFIG['sign_type'], + charset=ALIPAY_CONFIG['charset'] + ) + + except ImportError as e: + raise ValueError(f"无法导入支付宝配置: {e}") + except Exception as e: + raise ValueError(f"创建支付宝实例失败: {e}") + + +def check_alipay_ready(): + """检查支付宝支付是否就绪""" + try: + instance = create_alipay_instance() + return True, "支付宝支付配置正确" + except Exception as e: + return False, str(e) + + +if __name__ == '__main__': + """测试代码""" + import sys + + print("=" * 60) + print("支付宝支付测试") + print("=" * 60) + + try: + # 检查配置 + is_ready, message = check_alipay_ready() + print(f"\n配置状态: {'✅ 就绪' if is_ready else '❌ 未就绪'}") + print(f"详情: {message}") + + if is_ready: + # 创建实例 + alipay = create_alipay_instance() + + # 测试创建订单 + test_order_no = f"TEST{int(time.time())}" + result = alipay.create_page_pay_url( + out_trade_no=test_order_no, + total_amount='0.01', + subject='测试商品', + body='这是一个测试订单' + ) + + print(f"\n创建订单结果:") + print(json.dumps(result, indent=2, ensure_ascii=False)) + + if result['success']: + print(f"\n🔗 支付链接(复制到浏览器打开):") + print(result['pay_url'][:200] + '...') + + except Exception as e: + print(f"\n❌ 测试失败: {e}") + import traceback + traceback.print_exc() + + print("\n" + "=" * 60) diff --git a/alipay_pay_worker.py b/alipay_pay_worker.py new file mode 100644 index 00000000..c4dacdcb --- /dev/null +++ b/alipay_pay_worker.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +支付宝支付工作脚本 +用于在 subprocess 中执行,绕过 eventlet DNS 问题 + +用法: + python alipay_pay_worker.py check # 检查配置 + python alipay_pay_worker.py create [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',