微信支付V3

微信支付V3

1、微信开放平台申请微信支付

https://open.weixin.qq.com/cgi-bin/index

2、微信开发平台文档中心JSAPI下单

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml

3、业务流程图

4、代码开发

  • 4.1 文件架构

    ├── WX
    │   ├── wxpay.py: 微信支付逻辑
    │   ├── wxpay_keys: 微信支付证书相关
    │   │   ├── apiclient_cert.p12
    │   │   ├── apiclient_cert.pem
    │   │   ├── apiclient_key.pem
    
  • 4.2 wxpay.py

    • 4.2.1 包安装

      pip install requests
      pip install cryptography 
      
    • 4.2.2 导包

      import json
      import random
      import string
      import os
      import base64
      import time
      import requests
      from base64 import b64encode
      from Cryptodome.PublicKey import RSA
      from Cryptodome.Signature import pkcs1_15
      from Cryptodome.Hash import SHA256
      from cryptography.hazmat.primitives.ciphers.aead import AESGCM
      
    • 4.2.3 微信支付相关数据

      # 微信支付数据
      WXPAY_DATA = {
          'APP_ID': 'wx*********',  # 你的应用ID
          'API_KEY': '*****************',  # 你的微信商户平台API KEY
          'MCH_ID': '***********',  # 你的直连商户号MCH_ID
          'PAY_KEY': '***********',  # 你的商户平台PAY_KEY
          'REQUEST_URL': "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi",  # 请求地址,固定写死
          'SERIAL_NO': '******************',  # 证书序列号  说明:证书序列号及key、cert、p12都要去申请并保证一致
      }
      
      # 证书路径  说明:证书序列号及key、cert、p12都要去申请并保证一致
      basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
      KEY_PATH = os.path.join(basedir, "wxpay_keys", "apiclient_key.pem")
      CERT_PATH = os.path.join(basedir, "wxpay_keys", "apiclient_cert.pem")
      P12_CERT_PATH = os.path.join(basedir,  "wxpay_keys", "apiclient_cert.p12")
      
    • 4.2.4 微信支付通用类

      class WXPayUtil:
          """
          微信支付工具类
          """
      
          @staticmethod
          def get_sign(sign_str: str) -> str:
              """
              加密
              :param sign_str:
              :return:
              """
              rsa_key = RSA.importKey(open(KEY_PATH).read())
              signer = pkcs1_15.new(rsa_key)
              digest = SHA256.new(sign_str.encode('utf8'))
              sign = b64encode(signer.sign(digest)).decode('utf-8')
              return sign
      
          @staticmethod
          def decrypt(nonce: str, ciphertext: str, associated_data: str):
              """
              解密
              :param nonce:
              :param ciphertext:
              :param associated_data:
              :return:
              """
              key = WXPAY_DATA['PAY_KEY']
              key_bytes = str.encode(key)
              nonce_bytes = str.encode(nonce)
              ad_bytes = str.encode(associated_data)
              data = base64.b64decode(ciphertext)
              aes = AESGCM(key_bytes)
              return aes.decrypt(nonce_bytes, data, ad_bytes)
      
          @staticmethod
          def create_out_trade_no(user_id: int) -> str:
              """
              创建唯一微信商户订单号
              :return:
              """
              local_time = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
              user_id = '{:0>7d}'.format(user_id)
              result = f'wx{local_time[2:]}{user_id}'
              return result
      
          @staticmethod
          def get_random_str() -> str:
              """
              获取随机字符串
              :return:
              """
              return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
      
          @staticmethod
          def get_headers(random_str: str, sign: str, time_stamps: str) -> dict:
              """
              请求头
              :param random_str:
              :param sign:
              :param time_stamps:
              :return:
              """
              headers = {
                  'Content-Type': 'application/json; charset=UTF-8',
                  'Accept': 'application/json',
                  'User-Agent': '*/*',
                  'Authorization': 'WECHATPAY2-SHA256-RSA2048 ' +
                                   f'mchid="{WXPAY_DATA["MCH_ID"]}",nonce_str="{random_str}",'
                                   f'signature="{sign}",timestamp="{time_stamps}",'
                                   f'serial_no="{WXPAY_DATA["SERIAL_NO"]}"'
              }
              return headers
      
          @staticmethod
          def get_data(out_trade_no, notify_url, total_fee, open_id):
              """
              支付数据
              :param out_trade_no: 自定义订单号
              :param notify_url: 回调地址
              :param total_fee: 金额
              :param open_id: open_id
              :return:
              """
              data = {
                  'appid': WXPAY_DATA['APP_ID'],  # appid
                  'mchid': WXPAY_DATA['MCH_ID'],  # 商户号
                  'description': '商品描述',  # 商品描述
                  'out_trade_no': out_trade_no,  # 系统里的唯一订单号
                  'notify_url': notify_url,  # 支付结果回调url
                  "amount": {
                      "total": total_fee * 100,  # 微信支付 1等于1分钱,所以要乘以100
                      "currency": "CNY"
                  },
                  "payer": {"openid": open_id},  # 用户的openid
              }
              data = json.dumps(data)  # 只能序列化一次
              return data
      
          @staticmethod
          def get_res(response, time_stamps, random_str) -> dict:
              """
              给前端返回获取的支付信息
              :param response:
              :param time_stamps:
              :param random_str:
              :return:
              """
              if 'prepay_id' not in response.json():
                  return dict()
              res = {
                  "message": dict(
                      package='prepay_id=' + response.json()['prepay_id'],
                      timeStamp=time_stamps,
                      nonceStr=random_str,
                      paySign=WXPayUtil.get_sign(
                          f"{WXPAY_DATA['APP_ID']}\n"
                          f"{time_stamps}\n{random_str}\n"
                          f"{'prepay_id=' + response.json()['prepay_id']}\n"),
                      signType='RSA',
                  ),
                  "meta": {
                      "msg": "",
                      "status": 200
                  }
              }
              return res
      
          @staticmethod
          def get_sign_str(time_stamps, random_str, data):
              return f"POST\n{'/v3/pay/transactions/jsapi'}\n{time_stamps}\n{random_str}\n{data}\n"
      
    • 4.2.5 与FastAPI框架结合使用示例

      """
      @Author: GuHaoHao
      @coding: utf-8
      """
      import json
      import random
      import string
      import os
      import base64
      import time
      import uvicorn
      import requests
      from base64 import b64encode
      
      from Cryptodome.PublicKey import RSA
      from Cryptodome.Signature import pkcs1_15
      from Cryptodome.Hash import SHA256
      from cryptography.hazmat.primitives.ciphers.aead import AESGCM
      
      from fastapi import FastAPI
      
      app = FastAPI()
      
      # 微信支付数据
      WXPAY_DATA = {
          'APP_ID': 'wx*********',  # 你的应用ID
          'API_KEY': '*****************',  # 你的微信商户平台API KEY
          'MCH_ID': '***********',  # 你的直连商户号MCH_ID
          'PAY_KEY': '***********',  # 你的商户平台PAY_KEY
          'REQUEST_URL': "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi",  # 请求地址,固定写死
          'SERIAL_NO': '******************',  # 证书序列号
      }
      
      # 证书路径  说明:证书序列号及key、cert、p12都要去申请
      basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
      KEY_PATH = os.path.join(basedir, "wxpay_keys", "apiclient_key.pem")
      CERT_PATH = os.path.join(basedir, "wxpay_keys", "apiclient_cert.pem")
      P12_CERT_PATH = os.path.join(basedir,  "wxpay_keys", "apiclient_cert.p12")
      
      
      class WXPayUtil:
          """
          微信支付工具类
          """
      
          @staticmethod
          def get_sign(sign_str: str) -> str:
              """
              加密
              :param sign_str:
              :return:
              """
              rsa_key = RSA.importKey(open(KEY_PATH).read())
              signer = pkcs1_15.new(rsa_key)
              digest = SHA256.new(sign_str.encode('utf8'))
              sign = b64encode(signer.sign(digest)).decode('utf-8')
              return sign
      
          @staticmethod
          def decrypt(nonce: str, ciphertext: str, associated_data: str):
              """
              解密
              :param nonce:
              :param ciphertext:
              :param associated_data:
              :return:
              """
              key = WXPAY_DATA['PAY_KEY']
              key_bytes = str.encode(key)
              nonce_bytes = str.encode(nonce)
              ad_bytes = str.encode(associated_data)
              data = base64.b64decode(ciphertext)
              aes = AESGCM(key_bytes)
              return aes.decrypt(nonce_bytes, data, ad_bytes)
      
          @staticmethod
          def create_out_trade_no(user_id: int) -> str:
              """
              创建唯一微信商户订单号
              :return:
              """
              local_time = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
              user_id = '{:0>7d}'.format(user_id)
              result = f'wx{local_time[2:]}{user_id}'
              return result
      
          @staticmethod
          def get_random_str() -> str:
              """
              获取随机字符串
              :return:
              """
              return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
      
          @staticmethod
          def get_headers(random_str: str, sign: str, time_stamps: str) -> dict:
              """
              请求头
              :param random_str:
              :param sign:
              :param time_stamps:
              :return:
              """
              headers = {
                  'Content-Type': 'application/json; charset=UTF-8',
                  'Accept': 'application/json',
                  'User-Agent': '*/*',
                  'Authorization': 'WECHATPAY2-SHA256-RSA2048 ' +
                                   f'mchid="{WXPAY_DATA["MCH_ID"]}",nonce_str="{random_str}",'
                                   f'signature="{sign}",timestamp="{time_stamps}",'
                                   f'serial_no="{WXPAY_DATA["SERIAL_NO"]}"'
              }
              return headers
      
          @staticmethod
          def get_data(out_trade_no, notify_url, total_fee, open_id):
              """
              支付数据
              :param out_trade_no: 自定义订单号
              :param notify_url: 回调地址
              :param total_fee: 金额
              :param open_id: open_id
              :return:
              """
              data = {
                  'appid': WXPAY_DATA['APP_ID'],  # appid
                  'mchid': WXPAY_DATA['MCH_ID'],  # 商户号
                  'description': '商品描述',  # 商品描述
                  'out_trade_no': out_trade_no,  # 系统里的唯一订单号
                  'notify_url': notify_url,  # 支付结果回调url
                  "amount": {
                      "total": total_fee * 100,  # 微信支付 1等于1分钱,所以要乘以100
                      "currency": "CNY"
                  },
                  "payer": {"openid": open_id},  # 用户的openid
              }
              data = json.dumps(data)  # 只能序列化一次
              return data
      
          @staticmethod
          def get_res(response, time_stamps, random_str) -> dict:
              """
              给前端返回获取的支付信息
              :param response:
              :param time_stamps:
              :param random_str:
              :return:
              """
              if 'prepay_id' not in response.json():
                  return dict()
              res = {
                  "message": dict(
                      package='prepay_id=' + response.json()['prepay_id'],
                      timeStamp=time_stamps,
                      nonceStr=random_str,
                      paySign=WXPayUtil.get_sign(
                          f"{WXPAY_DATA['APP_ID']}\n"
                          f"{time_stamps}\n{random_str}\n"
                          f"{'prepay_id=' + response.json()['prepay_id']}\n"),
                      signType='RSA',
                  ),
                  "meta": {
                      "msg": "",
                      "status": 200
                  }
              }
              return res
      
          @staticmethod
          def get_sign_str(time_stamps, random_str, data):
              return f"POST\n{'/v3/pay/transactions/jsapi'}\n{time_stamps}\n{random_str}\n{data}\n"
      
      
      @app.post("/wx_pay")
      async def wx_pay():
          """
          微信支付
          :return:
          """
          NOTIFY_URL = 'https://8.136.214.28:5000/wx_pay_result'  # 异步通知,通知成功则购买商品成功  todo 可修改为自己的回调地址
      
          # todo 以下为伪造数据,具体可通过前后端传参及数据库操作获取数据
          total_fee = 1  # 购买商品钱数
          user_id = 1   # 用户id
          open_id = '*************'   # 用户openId
      
          # 自己的唯一微信订单号
          out_trade_no = WXPayUtil.create_out_trade_no(user_id)
      
          data = WXPayUtil.get_data(out_trade_no=out_trade_no,
                                    notify_url=NOTIFY_URL,
                                    total_fee=total_fee,
                                    open_id=open_id)
          random_str = WXPayUtil.get_random_str()
          time_stamps = str(int(time.time()))
      
          sign_str = WXPayUtil.get_sign_str(
              time_stamps=time_stamps,
              random_str=random_str,
              data=data
          )
          sign = WXPayUtil.get_sign(sign_str)
      
          try:
              response = requests.post(WXPAY_DATA['REQUEST_URL'],
                                       data=data,
                                       headers=WXPayUtil.get_headers(
                                           random_str=random_str,
                                           sign=sign,
                                           time_stamps=time_stamps)
                                       )
      
              print(response.text)  # 返回{"prepay_id":"wx*************************"}
      
              res = WXPayUtil.get_res(
                  response=response,
                  time_stamps=time_stamps,
                  random_str=random_str
              )
              if not res:
                  return {'message': '操作接口失败'}
      
              # todo 充值逻辑,数据库操作,比如添加一条充值记录
              return {'message': '操作接口成功'}
      
          except requests.ConnectTimeout:
              return {'message': '操作接口失败'}
      
      
      @app.post("/wx_pay_result")
      async def wx_pay_result(**kwargs):
          wx_id = kwargs.get('id')
          create_time = kwargs.get('create_time')
          resource_type = kwargs.get('resource_type')
          event_type = kwargs.get('event_type')
          summary = kwargs.get('summary')
          resource = kwargs.get('resource')
      
          if not all([wx_id, create_time, resource_type, event_type, summary, resource]):
              return {"code": "FAIL", "message": "失败"}
      
          if isinstance(resource, str):
              resource = json.loads(resource)
          nonce = resource.get('nonce')
          ciphertext = resource.get('ciphertext')
          associated_data = resource.get('associated_data')
      
          try:
              data_dict = json.loads(WXPayUtil.decrypt(nonce, ciphertext, associated_data))
              print(data_dict)
      
              if not data_dict:
                  return {"code": "FAIL", "message": "失败"}
      
              # todo 根据订单号,把数据库的订单状态修改为支付成功,更改订单记录状态为”已到账“
              return {"code": "SUCCESS", "message": "成功"}
          except ValueError:
              return {"code": "FAIL", "message": "失败"}
      
      if __name__ == '__main__':
          uvicorn.run(
              "wx_pay_example",  # 不建议取中文名
              host='127.0.0.1',
              port=5000,
              log_level='info'
          )
      
posted @ 2021-07-16 15:57  小皮浩  阅读(804)  评论(0编辑  收藏  举报