微信支付浅尝

微信支付浅尝

一. 微信支付方式概览

支付方式

1. 刷卡支付

刷卡支付是用户展示微信钱包内的“刷卡条码/二维码”给商户系统扫描后直接完成支付的模式。主要应用线下面对面收银的场景

2. 公众号支付

公众号支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。应用场景有:
◆ 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
◆ 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付
◆ 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付

3. 扫码支付

扫码支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。

4. APP支付

APP支付又称移动端支付,是商户通过在移动端应用APP中集成开放SDK调起微信支付模块完成支付的模式。

二. 刷卡支付场景及流程 (被扫)

1. 刷卡支付场景

2. 刷卡支付流程

A:免密支付


刷卡支付免密

B:验密支付


刷卡支付验密.png

三. 公众号支付场景及流程(H5支付,JSSDK)

1. 公众号支付场景

2. 公众号支付流程

公众号支付

四. 扫码支付流程(主动扫)

1. 扫码支付场景

2. 扫码支付流程

A. 模式一(依赖回调)
商户后台系统根据微信支付规则链接生成二维码,链接中带固定参数productid(可定义为产品标识),
用户扫码后,微信支付系统将productid和用户唯一标识(openid)回调商户后台系统(需要设置支付回调URL),
商户后台系统根据productid生成支付交易,最后微信支付系统发起用户支付流程
适合应用场景:线下。因为一张二维码可重复应用于多人,不会过期

扫码支付模式一

B. 模式二(不依赖回调)
商户后台系统调用微信支付【统一下单API】生成预付交易,
将接口返回的链接生成二维码,用户扫码后输入密码完成支付交易。注意:该模式的预付单有效期为2小时,过期后无法支付。
适合应用场景:线上。(线下不建议使用,因为每张二维码只能扫一次,而且有时间限制)

扫码支付模式二

3. 商户后台开发

A:开发前提
微信支付服务商.png

微信支付的开发需要有一个公众号或服务号,并且开通微信支付功能。
要是想作为微信支付服务商,则需提交服务商申请。
服务商可为所拓展的特约商户完成支付申请、技术接入、活动营销等全生态服务。

申请完成后便会得到重要的账户参数和接口API参数


重要参数.png
B:协议规则

协议规则.png

生成二维码规则
二维码中的内容为链接,形式为:
weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX
其中XXXXX为商户需要填写的内容,商户将该链接生成二维码,如需要打印发布二维码,需要采用此格式。商户可调用第三方库生成二维码图片 二维码生成与解析工具

C:安全规范 详细说明

签名算法
生成随机数算法
商户证书
商户回调API安全

D:Develop实践

xx通接入实践

微信支付API列表

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

五. APP支付(SDK)

1. APP支付场景

2. APP支付流程

APP支付.png

APP支付尾部
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容