微信支付浅尝
一. 微信支付方式概览
1. 刷卡支付
刷卡支付是用户展示微信钱包内的“刷卡条码/二维码”给商户系统扫描后直接完成支付的模式。主要应用线下面对面收银的场景
2. 公众号支付
公众号支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。应用场景有:
◆ 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
◆ 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付
◆ 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付
3. 扫码支付
扫码支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。
4. APP支付
APP支付又称移动端支付,是商户通过在移动端应用APP中集成开放SDK调起微信支付模块完成支付的模式。
二. 刷卡支付场景及流程 (被扫)
1. 刷卡支付场景
2. 刷卡支付流程
A:免密支付
B:验密支付
三. 公众号支付场景及流程(H5支付,JSSDK)
1. 公众号支付场景
2. 公众号支付流程
四. 扫码支付流程(主动扫)
1. 扫码支付场景
2. 扫码支付流程
A. 模式一(依赖回调)
商户后台系统根据微信支付规则链接生成二维码,链接中带固定参数productid(可定义为产品标识),
用户扫码后,微信支付系统将productid和用户唯一标识(openid)回调商户后台系统(需要设置支付回调URL),
商户后台系统根据productid生成支付交易,最后微信支付系统发起用户支付流程
适合应用场景:线下。因为一张二维码可重复应用于多人,不会过期
B. 模式二(不依赖回调)
商户后台系统调用微信支付【统一下单API】生成预付交易,
将接口返回的链接生成二维码,用户扫码后输入密码完成支付交易。注意:该模式的预付单有效期为2小时,过期后无法支付。
适合应用场景:线上。(线下不建议使用,因为每张二维码只能扫一次,而且有时间限制)
3. 商户后台开发
A:开发前提
微信支付的开发需要有一个公众号或服务号,并且开通微信支付功能。
要是想作为微信支付服务商,则需提交服务商申请。
服务商可为所拓展的特约商户完成支付申请、技术接入、活动营销等全生态服务。
申请完成后便会得到重要的账户参数和接口API参数
B:协议规则
生成二维码规则
二维码中的内容为链接,形式为:
weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX
其中XXXXX为商户需要填写的内容,商户将该链接生成二维码,如需要打印发布二维码,需要采用此格式。商户可调用第三方库生成二维码图片 二维码生成与解析工具
C:安全规范 详细说明
签名算法
生成随机数算法
商户证书
商户回调API安全
D:Develop实践
scanPay.py
import requests
import json
import tornado.web
from core.weixin_sdk.utils import Util
from core.weixin_sdk.utils import HttpUtil
from core.logger_helper import logger
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
""" 微信扫码支付 """
class WeiXinScanPayHandler(tornado.web.RequestHandler):
def post(self):
print("web2shopRequest=",self.request.body)
params = {}
params['service'] = 'pay.weixin.native'
# params['version'] = '2.0'
# params['charset'] = 'UTF-8'
# params['sign_type'] = 'MD5'
params['mch_id'] = '7551000001'
params['out_trade_no'] = self.get_argument('out_trade_no')
# params['device_info'] = '127.0.0.1'
params['body'] = self.get_argument('body')
params['attach'] = self.get_argument('attach')
params['total_fee'] = self.get_argument('total_fee')
params['mch_create_ip'] = self.get_argument('mch_create_ip')
params['notify_url'] = 'https://weixin.g-pay.cn/scanPaied' #通知地址
params['time_start'] = self.get_argument('time_start')
params['time_expire'] = self.get_argument('time_expire')
# params['op_user_id'] = '7551000001' #操作员账号,默认为商户号
# params['goods_tag'] = '商品标记,用于优惠券或者满减使用'
# params['product_id'] = '12345678' #商品ID
params['nonce_str'] = Util.generate_nonce()
sign = Util.get_sign(params,"9d101c97133837e13dde2d32a5054abb");
params['sign'] = sign
data = Util.dict_to_xml(params)
print('shop2weifutong=',data)
BASE_URL = 'https://pay.swiftpass.cn/pay/gateway'
r = requests.post(BASE_URL,data.encode('utf-8'))
if r.status_code == 200:
print('weifutongResponse==',r.text)
dic = Util.xml_to_dict(r.text)
if('0' == dic['status'] and '0' == dic['result_code']):
self.render("template.html",url=dic['code_img_url'],mch_id=dic['mch_id'])
else:
self.render('orderQueryResult.html',title='请求生成二维码失败',dic=dic)
"""支付宝扫码支付 """
class AliScanPayHandler(tornado.web.RequestHandler):
def post(self):
print("data=",self.request.body)
params = {}
params['service'] = 'pay.alipay.native'
# params['version'] = '2.0'
# params['charset'] = 'UTF-8'
# params['sign_type'] = 'MD5'
params['mch_id'] = '7551000001'
params['out_trade_no'] = self.get_argument('out_trade_no')
# params['device_info'] = '127.0.0.1'
params['body'] = self.get_argument('body')
params['attach'] = self.get_argument('attach')
params['total_fee'] = self.get_argument('total_fee')
params['mch_create_ip'] = self.get_argument('mch_create_ip')
params['notify_url'] = 'https://weixin.g-pay.cn/scanPaied' #支付后的异步通知地址
# params['time_start'] = self.get_argument('time_start')
# params['time_expire'] = self.get_argument('time_expire')
# params['op_user_id'] = '101520000465' #操作员账号,默认为商户号
# params['goods_tag'] = '商品标记,用于优惠券或者满减使用'
params['product_id'] = '12345678' #商品ID
params['nonce_str'] = Util.generate_nonce()
sign = Util.get_sign(params,"9d101c97133837e13dde2d32a5054abb");
params['sign'] = sign
data = Util.dict_to_xml(params)
print(data)
BASE_URL = 'https://pay.swiftpass.cn/pay/gateway'
r = requests.post(BASE_URL,data.encode('utf-8'))
if r.status_code == 200:
print('res==',r.text)
dic = Util.xml_to_dict(r.text)
if('0' == dic['status'] and '0' == dic['result_code']):
self.render("template.html",url=dic['code_img_url'],mch_id=dic['mch_id'])
else:
self.render('orderQueryResult.html',title='请求生成二维码失败',dic=dic)
""" 支付后接收异步通知,并做相应处理"""
class PaiedHandler(tornado.web.RequestHandler):
def post(self):
print("return=",self.request.body)
logger.debug('paidHandler:{}'.format(self.request.body))
self.write("success")
""" 订单查询 """
class OrderQueryHandler(tornado.web.RequestHandler):
def post(self):
print("data=",self.request.body)
params = {}
params['service'] = 'unified.trade.query' #订单查询接口 必填
# params['mch_id'] = '101520000465' #商户号 必填
params['mch_id'] = '7551000001'
params['nonce_str'] = Util.generate_nonce() #随机串 必填
out_trade_no = self.get_argument('out_trade_no')
if(out_trade_no):
params['out_trade_no'] = out_trade_no #商户订单号
transaction_id = self.get_argument('transaction_id')
if(transaction_id):
params['transaction_id'] = transaction_id #威富通订单号(与商户订单号必填1个)
# params['version'] = '2.0' #版本号 默认值2.0 选填
# params['charset'] = 'UTF-8' #字符集,默认值UTF-8 选填
# params['sign_type'] = 'MD5' #签名方式,默认MD5, 选填
# params['sign_agentno'] = '' #授权渠道编号 如果不为空,则用授权渠道的秘钥进行签名
# sign = Util.get_sign(params,"58bb7db599afc86ea7f7b262c32ff42f");
sign = Util.get_sign(params,'9d101c97133837e13dde2d32a5054abb')
params['sign'] = sign
data = Util.dict_to_xml(params)
print(data)
BASE_URL = 'https://pay.swiftpass.cn/pay/gateway'
r = requests.post(BASE_URL,data)
if r.status_code == 200:
res = r.text
print('res==',res)
dic = Util.xml_to_dict(res)
if('0' != dic['status']):
self.write(dic['message'])
else:
self.render('orderQueryResult.html',title='订单查询结果',dic=dic)
""" 退款 """
class TradeRefundHandler(tornado.web.RequestHandler):
def post(self):
print("data=",self.request.body)
params = {}
params['service'] = 'unified.trade.refund' #订单查询接口 必填
params['mch_id'] = '7551000001' #商户号 必填
params['op_user_id'] = '7551000001' #操作员
params['nonce_str'] = Util.generate_nonce() #随机串 必填
out_trade_no = self.get_argument('out_trade_no')
if(out_trade_no):
params['out_trade_no'] = out_trade_no #商户订单号
transaction_id = self.get_argument('out_transaction_id')
if(transaction_id):
params['transaction_id'] = transaction_id #微信订单号(与商户订单号必填1个)
params['out_refund_no'] = self.get_argument('out_refund_no') #商户退款单号 必填
params['total_fee'] = self.get_argument('total_fee')
params['refund_fee'] = self.get_argument('refund_fee')
sign = Util.get_sign(params,"9d101c97133837e13dde2d32a5054abb");
params['sign'] = sign
data = Util.dict_to_xml(params)
print(data)
BASE_URL = 'https://pay.swiftpass.cn/pay/gateway'
r = requests.post(BASE_URL,data)
if r.status_code == 200:
res = r.text
print('res==',res)
dic = Util.xml_to_dict(res)
if('0' != dic['status']):
self.write(dic['message'])
else:
self.render('orderQueryResult.html',title='退款结果页面',dic=dic)
""" 退款查询 """
class RefundQueryHandler(tornado.web.RequestHandler):
def post(self):
print("data=",self.request.body)
params = {}
params['service'] = 'unified.trade.refundquery' #订单查询接口 必填
params['mch_id'] = '7551000001' #商户号 必填
params['nonce_str'] = Util.generate_nonce() #随机串 必填
out_trade_no = self.get_argument('out_trade_no')
if(out_trade_no):
params['out_trade_no'] = out_trade_no #商户订单号
transaction_id = self.get_argument('out_transaction_id')
if(transaction_id):
params['transaction_id'] = transaction_id #微信订单号
out_refund_no = self.get_argument('out_refund_no')
if(out_refund_no):
params['out_refund_no'] = out_refund_no #商户退款单号
refund_id = self.get_argument('refund_id')
if(refund_id):
params['refund_id'] = refund_id #微信退款单号
# params['version'] = '2.0' #版本号 默认值2.0 选填
# params['charset'] = 'UTF-8' #字符集,默认值UTF-8 选填
# params['sign_type'] = 'MD5' #签名方式,默认MD5, 选填
# params['sign_agentno'] = '' #授权渠道编号 如果不为空,则用授权渠道的秘钥进行签名
sign = Util.get_sign(params,"9d101c97133837e13dde2d32a5054abb");
params['sign'] = sign
data = Util.dict_to_xml(params)
print(data)
BASE_URL = 'https://pay.swiftpass.cn/pay/gateway'
r = requests.post(BASE_URL,data)
if r.status_code == 200:
res = r.text
print('res==',res)
dic = Util.xml_to_dict(res)
if('0' != dic['status']):
self.write(dic['message'])
else:
self.render('orderQueryResult.html',title='退款查询结果',dic=dic)
""" 订单关闭 """
class OrderCloseHandler(tornado.web.RequestHandler):
def post(self):
print("data=",self.request.body)
params = {}
params['service'] = 'unified.trade.close' #订单查询接口 必填
params['mch_id'] = '7551000001' #商户号 必填
params['nonce_str'] = Util.generate_nonce() #随机串 必填
params['out_trade_no'] = self.get_argument('out_trade_no') #商户订单号
# params['version'] = '2.0' #版本号 默认值2.0 选填
# params['charset'] = 'UTF-8' #字符集,默认值UTF-8 选填
# params['sign_type'] = 'MD5' #签名方式,默认MD5, 选填
sign = Util.get_sign(params,"9d101c97133837e13dde2d32a5054abb");
params['sign'] = sign
data = Util.dict_to_xml(params)
print(data)
BASE_URL = 'https://pay.swiftpass.cn/pay/gateway'
r = requests.post(BASE_URL,data)
if r.status_code == 200:
res = r.text
print('res==',res)
dic = Util.xml_to_dict(res)
if('0' != dic['status']):
self.write(dic['message'])
else:
self.render('orderQueryResult.html',title='订单关闭结果',dic=dic)
class TEST(object):
"""docstring for TEST"""
def qcodeRequest(self):
params = {}
params['service'] = 'pay.weixin.native'
params['mch_id'] = '7551000001'
params['notify_url'] = 'http://www.qq.com'
params['out_trade_no'] = "123abc457"
params['body'] = u'测试商品'.encode('utf-8')
params['total_fee'] = 1
params['mch_create_ip'] = '127.0.0.1'
params['nonce_str'] = Util.generate_nonce()
sign = Util.get_sign(params,"9d101c97133837e13dde2d32a5054abb");
params['sign'] = sign
data = Util.dict_to_xml(params)
BASE_URL = 'https://pay.swiftpass.cn/pay/gateway'
print(data)
r = requests.post(BASE_URL,data)
if r.status_code == 200:
res = r.text
print("{}".format(res))
if __name__ == '__main__':
test = TEST()
test.qcodeRequest()
utils.py
# -*- coding: utf-8 -*-
import time
import string
import random
import json
import hashlib
import urllib
import requests
from xml.etree import ElementTree
class HttpUtil:
def __init__(self):
pass
@staticmethod
def get(url, params=None):
response = requests.get(url, params=params)
return json.loads(response.content)
@staticmethod
def post(url, params, ctype='json', **kwargs):
"""post请求,传入dict,返回dict,内部处理json或xml"""
if ctype == 'json':
data = json.dumps(params, ensure_ascii=False)
data = data.encode('utf8')
response = requests.post(url, data, **kwargs)
return json.loads(response.content)
elif ctype == 'xml':
data = Util.encode_data(params)
data = Util.dict_to_xml(data)
response = requests.post(url, data, **kwargs)
return Util.xml_to_dict(response.content)
else:
data = params
response = requests.post(url, data, **kwargs)
return response.json()
@staticmethod
def url_update_query(url, **kwargs):
url_parts = list(urllib.parse(url))
query = dict(urlparse.parse(url_parts[4]))
query.update(**kwargs)
url_parts[4] = urllib.urlencode(query)
final_url = urllib.unparse(url_parts)
return final_url
class Util:
@staticmethod
def xml_to_dict(xml_data):
"""xml -> dict"""
xml_data = Util.encode_data(xml_data)
data = {}
for child in ElementTree.fromstring(xml_data):
data[child.tag] = child.text
return data
@staticmethod
def dict_to_xml(dict_data):
xml_str = '<xml>'
for key, value in dict_data.items():
xml_str += '<%s><![CDATA[%s]]></%s>' % (key, value, key)
xml_str += '</xml>'
return xml_str
@staticmethod
def timestamp():
return int(time.time())
@staticmethod
def generate_nonce(length=6):
"""生成随机字符串"""
return ''.join([random.choice(string.digits + string.ascii_letters) for i in range(length)])
@staticmethod
def get_local_ip():
"""获取本机ip地址"""
import socket
return socket.gethostbyname(socket.gethostname())
@staticmethod
def get_sign(dic,key):
"""获取sign"""
string1 = ""
lis = sorted(dic)
for k in lis:
string1 += ('{0}={1}&'.format(k,dic[k]))
string1 += 'key={}'.format(key)
return hashlib.md5(string1.encode('utf-8')).hexdigest().upper()
@staticmethod
def camel_to_underline(camel_format):
"""驼峰命名格式转下划线命名格式"""
underline_format=''
if isinstance(camel_format, str):
for _s_ in camel_format:
underline_format += _s_ if _s_.islower() else '_'+_s_.lower()
return underline_format.strip('_')
@staticmethod
def underline_to_camel(underline_format):
"""
下划线命名格式驼峰命名格式
"""
camel_format = ''
if isinstance(underline_format, str):
for _s_ in underline_format.split('_'):
camel_format += _s_.capitalize()
return camel_format
@staticmethod
def cap_lower(origin_str):
"""首字母小写"""
if origin_str:
return origin_str[0].lower() + origin_str[1:]
return origin_str
@staticmethod
def md5(origin_str):
return hashlib.md5(origin_str).hexdigest()
@staticmethod
def sha1(origin_str):
return hashlib.sha1(origin_str).hexdigest()
@staticmethod
def encode_data(data):
"""对dict, list, unicode-str对象编码为utf-8格式"""
if not data:
return data
if isinstance(data, str):
result = data.encode('utf-8')
elif isinstance(data, dict):
result = {}
for k,v in data.items():
k = Util.encode_data(k)
v = Util.encode_data(v)
result.update({k:v})
return result
elif isinstance(data, list):
result = []
for item in data:
result.append(Util.encode_data(item))
return result
else:
result = data
return result
class ObjectDict(dict):
"""
Makes a dictionary behave like an object, with attribute-style access.
"""
def __getattr__(self, name):
try:
return self[name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
self[name] = value
class WxError(Exception):
pass
if __name__ == '__main__':
pass