Python实现微信扫码支付模式二(NativePay)
转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/7649207.html
核心代码github地址:https://github.com/ygj0930/Python-WeiXinNativePay
一:项目准备
官方资料阅读:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5
微信支付需要用到微信公众平台账号、微信商户账号。
注册完成后,我们需要在公众平台、商户平台找到以下信息:
# ========支付相关配置信息=========== _APP_ID = ""; # 公众账号appid _MCH_ID = ""; # 商户号 _API_KEY = ""; # 微信商户平台(pay.weixin.qq.com) -->账户设置 -->API安全 -->密钥设置,设置完成后把密钥复制到这里
然后,还需要配置以下信息:
_UFDODER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"; #url是微信下单api _NOTIFY_URL = ""; # 微信支付结果回调接口,需要改为你的服务器上处理结果回调的方法路径 _CREATE_IP = 你的服务器地址; # 发起支付请求的ip
二:编写统一下单工具类
统一下单API资料阅读:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1
1:全局常量配置所需信息
# ========支付相关配置信息=========== _APP_ID = ""; # 公众账号appid _MCH_ID = ""; # 商户号 _API_KEY = ""; # 微信商户平台(pay.weixin.qq.com) -->账户设置 -->API安全 -->密钥设置,设置完成后把密钥复制到这里 _UFDODER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"; #url是微信下单api _NOTIFY_URL = ""; # 微信支付结果回调接口,需要改为你的服务器上处理结果回调的方法路径 _CREATE_IP = 你的服务器地址; # 发起支付请求的ip
然后定义统一下单方法。
2:在统一下单方法中,构造所需参数,其中,必须的参数有十个:
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | 是 | String(32) | wxd678efh567hg6787 | 微信支付分配的公众账号ID(企业号corpid即为此appId) |
商户号 | mch_id | 是 | String(32) | 1230000109 | 微信支付分配的商户号 |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,长度要求在32位以内。推荐随机数生成算法 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 通过签名算法计算得出的签名值,详见签名生成算法 |
商品描述 | body | 是 | String(128) | 腾讯充值中心-QQ会员充值 |
商品简单描述,该字段请按照规范传递,具体请见参数规定 |
商户订单号 | out_trade_no | 是 | String(32) | 20150806125346 | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。详见商户订单号 |
标价金额 | total_fee | 是 | Int | 88 | 订单总金额,单位为分,详见支付金额 |
终端IP | spbill_create_ip | 是 | String(16) | 123.12.12.123 | APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。 |
通知地址 | notify_url | 是 | String(256) | http://www.weixin.qq.com/wxpay/pay.php | 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 |
交易类型 | trade_type | 是 | String(16) | JSAPI | 取值如下:JSAPI,NATIVE,APP等,说明详见参数规定 |
appid = self._APP_ID mch_id = self._MCH_ID key = self._API_KEY nonce_str = str(int(round(time.time() * 1000)))+str(random.randint(1,999))+string.join(random.sample(['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'], 5)).replace(" ","") #拼接出随机的字符串即可,我这里是用 时间+随机数字+5个随机字母 spbill_create_ip = self._CREATE_IP notify_url = self._NOTIFY_URL trade_type = "NATIVE" #扫码支付类型 params = {} params['appid'] = appid params['mch_id'] = mch_id params['nonce_str'] = nonce_str params['out_trade_no'] = 订单号参数.encode('utf-8') #客户端生成并传过来,参数必须用utf8编码,否则报错【订单号不是在服务器生成,而是在客户端生成的,否则客户端无法根据订单号轮询支付结果】 params['total_fee'] = 价格参数 #单位是分,必须是整数 params['spbill_create_ip'] = spbill_create_ip params['notify_url'] = notify_url params['body'] = 商品名参数.encode('utf-8') #中文必须用utf-8编码,否则xml格式错误 params['trade_type'] = trade_type
根据以上9个参数,生成签名:
#生成签名 ret = [] for k in sorted(params.keys()): if (k != 'sign') and (k != '') and (params[k] is not None): ret.append('%s=%s' % (k, params[k])) params_str = '&'.join(ret) params_str = '%(params_str)s&key=%(partner_key)s'%{'params_str': params_str, 'partner_key': key} #这里需要设置系统编码为utf-8,否则下面md5加密会报参数错误 reload(sys) sys.setdefaultencoding('utf8') params_str = hashlib.md5(params_str.encode('utf-8')).hexdigest() sign = params_str.upper() params['sign'] = sign
3:把上面10个参数拼接成XML格式字符串【微信统一下单API只接收和回传XML格式的数据】
#拼接参数的xml字符串 request_xml_str = '<xml>' for key, value in params.items(): if isinstance(value, basestring): request_xml_str = '%s<%s><![CDATA[%s]]></%s>' % (request_xml_str, key, value, key, ) else: request_xml_str = '%s<%s>%s</%s>' % (request_xml_str, key, value, key, ) request_xml_str = '%s</xml>' % request_xml_str
4:向微信统一下单API发出请求,传递参数过去,获得回传结果后提取数据
#向微信支付发出请求,接收回传数据 res = urllib2.Request(self._UFDODER_URL, data=request_xml_str) res_data = urllib2.urlopen(res) #打开响应流 res_read = res_data.read() #读取响应流中数据 doc = xmltodict.parse(res_read) #数据是xml格式的,转为dict return_code = doc['xml']['return_code'] #根据dict的层级,从顶层开始逐级访问提取所需内容 if return_code=="SUCCESS": result_code = doc['xml']['result_code'] if result_code=="SUCCESS": code_url = doc['xml']['code_url'] return code_url else: err_des = doc['xml']['err_code_des'] print "errdes==========="+err_des else: fail_des = doc['xml']['return_msg'] print "fail des============="+fail_des
返回结果
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | 是 | String(16) | SUCCESS |
SUCCESS/FAIL 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 |
返回信息 | return_msg | 否 | String(128) | 签名失败 |
返回信息,如非空,为错误原因 签名失败 参数格式校验错误 |
以下字段在return_code为SUCCESS的时候有返回
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | 是 | String(32) | wx8888888888888888 | 调用接口提交的公众账号ID |
商户号 | mch_id | 是 | String(32) | 1900000109 | 调用接口提交的商户号 |
设备号 | device_info | 否 | String(32) | 013467007045764 | 自定义参数,可以为请求支付的终端设备号等 |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 微信返回的随机字符串 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 微信返回的签名值,详见签名算法 |
业务结果 | result_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL |
错误代码 | err_code | 否 | String(32) | SYSTEMERROR | 详细参见下文错误列表 |
错误代码描述 | err_code_des | 否 | String(128) | 系统错误 | 错误信息描述 |
以下字段在return_code 和result_code都为SUCCESS的时候有返回
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
交易类型 | trade_type | 是 | String(16) | JSAPI | 交易类型,取值为:JSAPI,NATIVE,APP等,说明详见参数规定 |
预支付交易会话标识 | prepay_id | 是 | String(64) | wx201410272009395522657a690389285100 | 微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时 |
二维码链接 | code_url | 否 | String(64) | URl:weixin://wxpay/s/An4baqw | trade_type为NATIVE时有返回,用于生成二维码,展示给用户进行扫码支付 |
错误码
名称 | 描述 | 原因 | 解决方案 |
---|---|---|---|
NOAUTH | 商户无此接口权限 | 商户未开通此接口权限 | 请商户前往申请此接口权限 |
NOTENOUGH | 余额不足 | 用户帐号余额不足 | 用户帐号余额不足,请用户充值或更换支付卡后再支付 |
ORDERPAID | 商户订单已支付 | 商户订单已支付,无需重复操作 | 商户订单已支付,无需更多操作 |
ORDERCLOSED | 订单已关闭 | 当前订单已关闭,无法支付 | 当前订单已关闭,请重新下单 |
SYSTEMERROR | 系统错误 | 系统超时 | 系统异常,请用相同参数重新调用 |
APPID_NOT_EXIST | APPID不存在 | 参数中缺少APPID | 请检查APPID是否正确 |
MCHID_NOT_EXIST | MCHID不存在 | 参数中缺少MCHID | 请检查MCHID是否正确 |
APPID_MCHID_NOT_MATCH | appid和mch_id不匹配 | appid和mch_id不匹配 | 请确认appid和mch_id是否匹配 |
LACK_PARAMS | 缺少参数 | 缺少必要的请求参数 | 请检查参数是否齐全 |
OUT_TRADE_NO_USED | 商户订单号重复 | 同一笔交易不能多次提交 | 请核实商户订单号是否重复提交 |
SIGNERROR | 签名错误 | 参数签名结果不正确 | 请检查签名参数和方法是否都符合签名算法要求 |
XML_FORMAT_ERROR | XML格式错误 | XML格式错误 | 请检查XML参数格式是否正确 |
REQUIRE_POST_METHOD | 请使用post方法 | 未使用post传递参数 | 请检查请求参数是否通过post方法提交 |
POST_DATA_EMPTY | post数据为空 | post数据不能为空 | 请检查post数据是否为空 |
NOT_UTF8 | 编码格式错误 | 未使用指定编码格式 | 请使用UTF-8编码格式 |
三:编写Controller层方法,主要有三个
1:处理客户端的二维码请求的方法
def getWeChatQRCode(self, **kwargs): order_id = kwargs.get('order_id') #获取客户端生成的订单号 goodsName = kwargs.get('goodsName') #获取商品信息 goodsPrice = int(float(kwargs.get('goodsPrice')) * 100) #获取价格,单位是分,需要是整数 toolUtil = PayToolUtil() code_url=toolUtil.getPayUrl(order_id,goodsName,goodsPrice) #调用统一下单方法,获得支付订单的url链接 if code_url: res_info = code_url # 如果成功获得支付链接,则写入一条订单记录 #todo:自己的后台逻辑 else: res_info = "二维码失效" #获取url失败,则二维码信息为失效 #根据res_info生成二维码,使用qrcode模块 qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_H, box_size=10, border=1 ) qr.add_data(res_info) #二维码所含信息 img = qr.make_image() #生成二维码图片 byte_io = BytesIO() img.save(byte_io, 'PNG') #存入字节流 byte_io.seek(0) return http.send_file(byte_io, mimetype='image/png') #把字节流返回给客户端,解析得到二维码
2:处理微信支付平台支付结果回调
支付回调资料阅读:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7
支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。
商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。
def weChatQRCodeNotify(self, request, *args,**kwargs): order_result_xml = http.request.httprequest.stream.read() #从请求流提取数据 doc = xmltodict.parse(order_result_xml) #解析得到的xml字符串,转为dict out_trade_no = doc['xml']['out_trade_no'] #提取返回数据中的订单号 #todo:提取签名、支付金额等,验证签名是否正确、金额是否正确 #思路:在前面获取二维码时,生成了一条订单记录,订单应该保存下订单号、签名、金额等信息。在这里,根据回传的订单号查询数据库,得到对应的签名、金额进行验证即可 #最后,别忘了应答微信支付平台,防止重复发送数据 return ''' <xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg> </xml> '''
3:处理客户端轮询请求
我们在客户端发起二维码请求后,获得二维码图片,向微信支付平台下了一张订单,那么这张订单的支付状态,客户端怎么知道呢?用轮询。
客户端生成一个随机字符串,作为订单号,把订单号作为参数,发起二维码请求,同时,另开一个线程,用这个订单号不断轮询服务器,查询该订单号对应的订单记录的支付状态,获得返回值后检查返回值:如果支付成功,则客户端执行后续操作(如:售卖机出货、页面跳转之类逻辑)并停止轮询;如果失败,也执行相应的用户提醒,如(余额不足等)并停止轮询。
def weChatQRCodeHadPay(self, **kwargs): order_id = kwargs.get('order_id') #获取订单号 #todo:根据订单号查询对应的订单记录状态,返回支付结果