test_alipay_sandbox.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. """
  2. 支付宝沙箱环境测试脚本
  3. 测试 alipay.trade.page.pay 接口
  4. 沙箱应用: 2088721092817692/9021000159623614
  5. """
  6. import os
  7. import sys
  8. import base64
  9. import json
  10. import uuid
  11. from datetime import datetime
  12. from decimal import Decimal
  13. from urllib.parse import urlencode, quote_plus
  14. # 添加项目根目录到路径
  15. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  16. from cryptography.hazmat.primitives import hashes, serialization
  17. from cryptography.hazmat.primitives.asymmetric import padding
  18. from cryptography.hazmat.backends import default_backend
  19. # ==================== 沙箱配置 ====================
  20. # 沙箱网关
  21. SANDBOX_GATEWAY = "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
  22. # 沙箱应用 APP_ID(你提供的)
  23. SANDBOX_APP_ID = "9021000122696995"
  24. # 沙箱商户 PID
  25. SANDBOX_SELLER_ID = "2088721004566557"
  26. # 沙箱私钥(需要从支付宝开放平台沙箱获取)
  27. # 请将你的沙箱私钥粘贴到这里(单行格式,不含头尾)
  28. SANDBOX_PRIVATE_KEY = """
  29. MIIEogIBAAKCAQEAtbYqPvCXlvLf0wHzMojixChBJwgIHd69lD2LN8N6RF+h/Zpmj1wwiuuoggJf7fHJ15xmaais716RnNOt8eJnISU9w5r9JI3SRE3AB7BmNyYDqav3ot+eQ8z7j5dOPHaC/+5x46nsnGLnBbVtlL4cqQzIrIuwEMyXJvtQSlR2v1Zkg5ZVh0jOnsLkDWE+vNCYc5RyO6v0XwrXEACb/CvtW0BTKqYHBdeSkwTUNioh2iFPr74IW8prpm4ib2sA2qRD7AvF9ihJLQkU73CavruucLJswK5CFanxQy9jhZI2OsveZT41BPAr1CeMpaU4SWvNtLIJh3o2l46A+RIJiJ77uwIDAQABAoIBAHfQlsiLgZjxqm4K0h4XLlcjJ9qhrCBt91kgv4RUa3FcEYcT4N5Ublmbi6+1+yT9EhcONGUk6GqpU6Ax33cnztKHKNOqhZItxWBuV9l6Edv5P98H2jBV/Jg+N/11SgwKJNS2l/gC2lY/zI7yE0BJnsJFAKjtLnnVPTFh8o4gZXBBWtdr7U1Gl/ENvesI7xEhSGlyXlJIMHeIe03jOgu0Z/Sy7TQePul2xD7iyqVpVpXWLc1LGIts+IW7CouuELv+QNe+EXJCBODIb9TCYN2EUMx8+poo7hYjqJPpoA4VS1zI0Z9b7HFNlZkuAbID5kxn/5sO36VINvgzGFGlyE8ckRECgYEA9v1KUUyWEVFMt/UOiuIy96A2f5OLzXcvQbHJrRiXyC9loW3RdXeCLK732jRKfGxTrq1VSmT/Y9hc+JAC7XYsmw+/YRVFLx0BIVouFIsOy7l6wCpblWja8e7BpoEE5/mJSDpqq+yMBp7PV/7O2yib85xabqkZ4yyQQKJPGXpfYsMCgYEAvFc5niYwfvJ3+S2mnhCJA9HXANFIlG7SZuolR5RpZIYCMnH+cLPRSpUkENTzcFRs9HpQfVQbbWZckH4KJWC3rsvrLwUoa13iYH9MPfamUFlbTq7K3LCW9mwkTYh2ejGeiepEhyOTaKNsY9TaQpAAGqBemmZUjBLxNxM4P3Ofg6kCgYAw+1hyuRKFyq7BSKsipetfqnlEYbl2/Oz1RVHurxLi02t/US0Z86JwRB7JOlePR1htKtjgURlI7s65SK4b87Sy57OXiEVZK5Jez+iVkGJVyqnqVDwnbE0Hb8cdwzZ63sT3+wXOpLG8WmBaXiEd23baICbbDVQh9mOBPe8b6WXjSwKBgBlZkCrGLx7XGejTCSsbRKXb95lnkeJQrEcn2s6wniLmmqMZjsqymUf1nP4a+40x/9xEHlNQH8Tw0yKrpEf1paOEVOpmQlN5NoAQgj2Q1j/YVx7XDfz4U8llMtmwtWmrBhDAFGswEaYy4OLrAlmVMj5jOXiEr3qPDiKAlsCLmnvJAoGAFYIAbeDpfqslnBWY2a9N5stOKS31E7XU5jM+DqoVEITjihGzpXaYvtZIq3w87lSxU9qeqzV8LMbCcB4sGckxZUBo07KisfF8xEpnMKBzlX48IZhTaUXbT3I31XB//edUKjNVSU37+3zh2LToQyMWg86wtcYVnOhrPXbzw7HvVQ8=
  30. """
  31. def format_pem_key(key_content: str, key_type: str) -> str:
  32. """格式化 PEM 密钥"""
  33. key_content = key_content.strip()
  34. # 移除可能的头尾
  35. for prefix in ["-----BEGIN RSA PRIVATE KEY-----", "-----BEGIN PRIVATE KEY-----"]:
  36. key_content = key_content.replace(prefix, "")
  37. for suffix in ["-----END RSA PRIVATE KEY-----", "-----END PRIVATE KEY-----"]:
  38. key_content = key_content.replace(suffix, "")
  39. key_content = key_content.replace("\n", "").replace("\r", "").replace(" ", "")
  40. # 每64字符换行
  41. lines = [key_content[i:i+64] for i in range(0, len(key_content), 64)]
  42. formatted = "\n".join(lines)
  43. return f"-----BEGIN {key_type}-----\n{formatted}\n-----END {key_type}-----"
  44. def load_private_key(key_content: str):
  45. """加载私钥"""
  46. # 尝试 PKCS8 格式
  47. pem_content = format_pem_key(key_content, "PRIVATE KEY")
  48. try:
  49. return serialization.load_pem_private_key(
  50. pem_content.encode(),
  51. password=None,
  52. backend=default_backend()
  53. )
  54. except Exception:
  55. pass
  56. # 尝试 RSA PRIVATE KEY 格式
  57. pem_content = format_pem_key(key_content, "RSA PRIVATE KEY")
  58. try:
  59. return serialization.load_pem_private_key(
  60. pem_content.encode(),
  61. password=None,
  62. backend=default_backend()
  63. )
  64. except Exception as e:
  65. print(f"加载私钥失败: {e}")
  66. return None
  67. def build_sign_content(params: dict, exclude: list = None) -> str:
  68. """构建待签名字符串"""
  69. exclude = exclude or []
  70. filtered = {k: v for k, v in params.items() if v and k not in exclude}
  71. sorted_keys = sorted(filtered.keys())
  72. pairs = [f"{k}={filtered[k]}" for k in sorted_keys]
  73. return "&".join(pairs)
  74. def sign_rsa2(params: dict, private_key) -> str:
  75. """RSA2 签名"""
  76. sign_content = build_sign_content(params)
  77. signature = private_key.sign(
  78. sign_content.encode("utf-8"),
  79. padding.PKCS1v15(),
  80. hashes.SHA256()
  81. )
  82. return base64.b64encode(signature).decode("utf-8")
  83. def generate_payment_url(
  84. out_trade_no: str,
  85. total_amount: str,
  86. subject: str,
  87. private_key
  88. ) -> str:
  89. """
  90. 生成支付宝支付页面 URL
  91. 调用接口: alipay.trade.page.pay
  92. """
  93. # 业务参数
  94. biz_content = {
  95. "out_trade_no": out_trade_no,
  96. "total_amount": total_amount,
  97. "subject": subject,
  98. "product_code": "FAST_INSTANT_TRADE_PAY"
  99. }
  100. # 公共参数
  101. params = {
  102. "app_id": SANDBOX_APP_ID,
  103. "method": "alipay.trade.page.pay",
  104. "format": "JSON",
  105. "charset": "utf-8",
  106. "sign_type": "RSA2",
  107. "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  108. "version": "1.0",
  109. "biz_content": json.dumps(biz_content, ensure_ascii=False, separators=(",", ":"))
  110. }
  111. # 签名
  112. params["sign"] = sign_rsa2(params, private_key)
  113. # 构建 URL
  114. query_string = urlencode(params, quote_via=quote_plus)
  115. return f"{SANDBOX_GATEWAY}?{query_string}"
  116. def main():
  117. print("=" * 60)
  118. print("支付宝沙箱环境测试 - alipay.trade.page.pay")
  119. print("=" * 60)
  120. # 检查私钥配置
  121. if "请在这里粘贴" in SANDBOX_PRIVATE_KEY:
  122. print("\n⚠️ 请先配置沙箱私钥!")
  123. print("\n获取方式:")
  124. print("1. 登录支付宝开放平台: https://open.alipay.com/")
  125. print("2. 进入沙箱环境")
  126. print("3. 获取沙箱应用的私钥")
  127. print("4. 将私钥粘贴到本脚本的 SANDBOX_PRIVATE_KEY 变量中")
  128. print("\n或者,你可以使用现有的 .env 配置(如果已配置沙箱密钥)")
  129. # 尝试使用 .env 中的配置
  130. from dotenv import load_dotenv
  131. load_dotenv()
  132. env_private_key = os.getenv("ALIPAY_PRIVATE_KEY", "")
  133. env_app_id = os.getenv("ALIPAY_APP_ID", "")
  134. if env_private_key and env_app_id:
  135. print(f"\n检测到 .env 配置,APP_ID: {env_app_id}")
  136. use_env = input("是否使用 .env 中的配置进行测试?(y/n): ").strip().lower()
  137. if use_env == 'y':
  138. private_key = load_private_key(env_private_key)
  139. if private_key:
  140. run_test(private_key, env_app_id)
  141. return
  142. return
  143. # 加载私钥
  144. private_key = load_private_key(SANDBOX_PRIVATE_KEY)
  145. if not private_key:
  146. print("❌ 私钥加载失败,请检查私钥格式")
  147. return
  148. run_test(private_key, SANDBOX_APP_ID)
  149. def run_test(private_key, app_id: str):
  150. """执行测试"""
  151. global SANDBOX_APP_ID
  152. SANDBOX_APP_ID = app_id
  153. # 生成唯一订单号
  154. timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
  155. unique_id = uuid.uuid4().hex[:8].upper()
  156. out_trade_no = f"TEST{timestamp}{unique_id}"
  157. print(f"\n📋 测试参数:")
  158. print(f" APP_ID: {SANDBOX_APP_ID}")
  159. print(f" 订单号: {out_trade_no}")
  160. print(f" 金额: 0.01 元")
  161. print(f" 商品: 测试商品")
  162. # 生成支付链接
  163. try:
  164. payment_url = generate_payment_url(
  165. out_trade_no=out_trade_no,
  166. total_amount="0.01",
  167. subject="测试商品",
  168. private_key=private_key
  169. )
  170. print(f"\n✅ 支付链接生成成功!")
  171. print(f"\n🔗 支付链接:")
  172. print("-" * 60)
  173. print(payment_url)
  174. print("-" * 60)
  175. print(f"\n📝 入参示例:")
  176. biz_content = {
  177. "out_trade_no": out_trade_no,
  178. "total_amount": 0.01,
  179. "subject": "测试商品",
  180. "product_code": "FAST_INSTANT_TRADE_PAY"
  181. }
  182. print(json.dumps({"biz_content": biz_content}, indent=2, ensure_ascii=False))
  183. print(f"\n💡 下一步:")
  184. print(" 1. 复制上面的支付链接")
  185. print(" 2. 在浏览器中打开")
  186. print(" 3. 使用沙箱买家账号登录支付")
  187. print(" 4. 沙箱买家账号可在支付宝开放平台沙箱环境中查看")
  188. except Exception as e:
  189. print(f"\n❌ 生成支付链接失败: {e}")
  190. import traceback
  191. traceback.print_exc()
  192. if __name__ == "__main__":
  193. main()