zoukankan      html  css  js  c++  java
  • Flask-爱家租房项目ihome-10-支付模块(支付宝支付)

    支付宝支付

    当房东接单后, 房客就可以在我的订单页面点击去支付按钮, 完成支付, 这里只实现支付宝支付方式.

    image-20200911112720966

    打开支付宝开放平台, 可以查看支付宝的相关接口文档和教程, 这里就不一一说明了, 只写出关键的几个步骤:

    1. 这里只在支付宝的测试环境(沙箱环境)中做测试, 不在支付宝的正式环境, 沙箱环境提供了测试的沙箱应用和沙箱账号, 通过控制台>开发服务>研发服务进入沙箱页面

    2. 使用支付宝开放平台开发助手工具, 生成RSA2一对秘钥, 即应用私钥和应用公钥, 将应用公钥添加到沙箱应用的RSA2密钥

      image-20200911170230146

    3. 下载支付宝官方SDK, SDK网址为: https://opendocs.alipay.com/open/54/103419, 选择python, 来到pip下载页面: https://pypi.org/project/alipay-sdk-python/3.3.398/

      image-20200911170347389

    image-20200911170525564

    1. 找到支付宝相应的API, 在支付宝的SDK页面右上方选择API, 即可进入API页面, 这里我们需要的是手机网站支付接口alipay.trade.wap.pay

    支付宝支付的后端逻辑编写

    ihome/api_1_0下创建支付模块的视图文件pay.py, 并在api蓝图中导入该文件

    支付功能编写

    # ihome/api_1_0/pay.py
    from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
    from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
    from alipay.aop.api.domain.AlipayTradeWapPayModel import AlipayTradeWapPayModel
    from alipay.aop.api.request.AlipayTradeWapPayRequest import AlipayTradeWapPayRequest
    
    def alipay_client():
        """支付宝客户端初始化"""
        # 记录日志
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s %(levelname)s %(message)s',
            filemode='a', )
        logger = logging.getLogger('')
        # 实例化客户端
        alipay_client_config = AlipayClientConfig()
        alipay_client_config.server_url = constants.ALIPAY_SERVER_URL
        alipay_client_config.app_id = constants.ALIPAY_APP_ID
        alipay_client_config.app_private_key = constants.ALIPAY_PRIVATE_KEY
        alipay_client_config.alipay_public_key = constants.ALIPAY_PUBLIC_KEY
        client = DefaultAlipayClient(alipay_client_config, logger)
        return client
    
    def alipay_model(order):
        """支付宝模型"""
        # 构造请求参数对象
        model = AlipayTradeWapPayModel()
        # 调用方系统生成的订单编号
        model.out_trade_no = order.id
        # 支付金额
        model.total_amount = str(order.amount)
        # 支付标题
        model.subject = "爱家租房"
        # 与支付宝签约的产品码名称
        model.product_code = constants.PRODUCT_CODE
        # 订单过期关闭时长(分钟)
        model.timeout_express = constants.ALIPAY_EXPRESS
        return model
    
    def alipay_pay(client, model):
        """支付宝支付"""
        # 创建请求对象
        req = AlipayTradeWapPayRequest(biz_model=model)
        # 设置回调通知地址(GET), 用户浏览器直接访问该地址
        req.return_url = constants.ALIPAY_RETURN_URL
        # 设置回调通知地址(POST), 支付宝会请求该地址, 用户看不到
        # req.notify_url = constants.ALIPAY_NOTIFY_URL
        # 执行API调用,获取支付连接
        pay_url = client.page_execute(req, http_method='GET')
        return pay_url
    

    注:

    1. 根据支付宝SDK上的调用实例, 可以发现分为四步, 创建支付宝客户端/构造请求/执行API调用/解析API返回结果

    2. 对于不同的支付宝接口, 大体结构都不变, 只是换一下不同接口对应的Model和Request, 根据接口名首字母大写可找到Model和Request, 然后不同接口执行API后的返回结果可能也不一样, 有的返回的是url有的返回的是执行结果等.

    3. 这里分别构造了这几步对应的方法: alipay_client()获取客户端, alipay_model()获取模型, alipay_pay()调用API

    4. alipay_client()创建客户端, 具体的应用配置都放在了constants.py

      # 支付宝支付设置-沙箱环境
      # 支付宝网
      ALIPAY_SERVER_URL = 'https://openapi.alipaydev.com/gateway.do'
      # 应用ID
      ALIPAY_APP_ID = '2016102200739747'
      # 应用私钥
      ALIPAY_PRIVATE_KEY = 'MIIEpAIBAAK.................'
      # 支付宝公
      ALIPAY_PUBLIC_KEY = 'MIIBIjANBgkq.................'
      # 订单超时时间:如果买家超过这个时间不付款,会关闭交易(最小1m分钟)
      ALIPAY_EXPRESS = '10m'
      # 产品码
      PRODUCT_CODE = 'QUICK_WAP_WAY'
      # 回调通知地址
      ALIPAY_NOTIFY_URL = ""
      ALIPAY_RETURN_URL = "http://127.0.0.1:5000/payresult.html"
      

      产品码可以在支付宝的API接口文档中的请求参数中看到, 不同的API产品码会不一样

      image-20200911172725015

    5. 回调通知地址:

      image-20200911173013102

      • RETURN_URL是用户在支付完成后, 点击右上角"完成"按钮, 页面会跳转到RETURN_URL这个url是让用户访问的.

        一般我们会设置为一个支付的中间页面, 因为支付完成之后, 我们还需要对订单的状态或者一些信息进行修改, 所以设置了一个中间页面, 在这个中间页面进行信息的修改, 修改完成后自动跳转到"我的订单"页面, 如这里设置为ALIPAY_RETURN_URL = "http://127.0.0.1:5000/payresult.html"

        image-20200911173629418

      • NOTIFY_URL是指用户在支付完成后, 点击右上角"完成"按钮, 支付宝会使用POST方式请求这里设置的NOTIFY_URL, 将支付结果发送过来, 所以这个url是支付宝后台访问的, 跟前台用户没有关系. 当然既然是给支付宝访问的, 那么这个url必须是公网IP, 否则访问不到

      • 由于我们没有公网IP, 所以这里就只设置RETURN_URL, 除了设置这两个URL可以得知支付结果外, 还可以调用支付宝的查询接口alipay.trade.query(统一收单线下交易查询)具体使用可以查看另一个Django项目的支付宝例子: https://www.cnblogs.com/gcxblogs/p/12895891.html, 这里我们也不使用这个接口了.

    6. alipay_model(order)创建支付模型, 需要根据具体的API设置相应的属性, 支付功能的话一般订单编号, 金额, 标题, 产品码都是必须要填写的

    7. alipay_pay(client, model)传入前面创建的clientmodel, 调用执行API, 在这里选择是否设置return_url或者notify_url, 由于我们调用的是手机支付接口, 因此给我们返回的是一个url, 我们拿到这个url后需要让用户进行访问, 然后就会跳转到支付宝支付的页面, 让用户进行支付操作

    支付接口的编写

    前面编写的只是支付功能, 现在需要实现完整的后端支付接口, 还是编辑支付模块的视图文件pay.py, 添加后端接口, url为: /api/v1.0/orders/alipay, 请求方式为POST, 创建支付宝订单

    # ihome/api_1_0/pay.py
    @api.route('/orders/alipay', methods=['POST'])
    @login_required
    def create_alipay():
        # 接收数据
        data_dict = request.get_json()
        if not data_dict:
            return parameter_error()
        # 提取数据
        order_id = data_dict.get('order_id')
        if not order_id:
            return parameter_error()
        # 校验order_id
        try:
            order = Orders.query.get(order_id)
        except Exception as e:
            current_app.logger.error(e)
            return jsonify(errno=RET.DBERR, errmsg='获取订单异常')
        if not order:
            return jsonify(errno=RET.PARAMERR, errmsg='订单ID不存在')
        # 判断订单是否属于当前用户
        if order.user != g.user:
            return jsonify(errno=RET.PARAMERR, errmsg='该订单不属于当前用户')
        # 调用支付接口
        try:
            client = alipay_client()
            model = alipay_model(order)
            pay_url = alipay_pay(client, model)
        except Exception as e:
            current_app.logger.error(e)
            return jsonify(errno=RET.THIRDERR, errmsg='调用支付接口异常')
        return jsonify(errno=RET.OK, data={'url': pay_url})
    

    记录一下遇到过的问题:

    1. 如果支付的金额为0, 那么支付宝会直接返回报错: 系统繁忙

      image-20200911180409099

    2. 如果RETURN_URL设置错了, 那么在点击右上角的"完成"按钮后, 不会有反应

    3. 如果该订单已经支付了, 会报错已经支付成功

      image-20200911180828888

    支付宝支付前端逻辑编写

    前面在"我的订单"页面已经写好了, 就是点击"支付"按钮, 调用ajax请求访问后端接口

    //去支付按钮
    $('.order-pay').on("click", function () {
        var orderId = $(this).parents("li").attr("order-id");
        //发送ajax请求获取支付页面url
        $.ajax({
            url: '/api/v1.0/orders/alipay',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify({order_id: orderId}),
            headers: {'X-CSRFToken': getCookie('csrf_token')},
            dataType: 'json',
            success: function (resp) {
                if (resp.errno == '0'){
                    //成功, 新窗口打开支付宝链接
                    location.href = resp.data.url;
                }else {
                    alert(resp.errmsg);
                }
            }
        })
    });
    

    支付完成后的结果页面

    支付宝支付完成后, 点击右上角的"完成"按钮, 会跳转到设置的RETURN_URL, 前面说过了一般这个url为一个中间状态的页面, 用来更新系统的订单状态或者其他的信息, 更新完成后, 再跳转至"我的订单"页面. 如这里设置的中间页面为payresult.html:

    image-20200911173629418

    需要注意的是, 支付宝会在RETURN_URL后面添加一系列的参数, 如:

    http://127.0.0.1:5000/payresult.html?charset=utf-8&out_trade_no=97&method=alipay.trade.wap.pay.return&total_amount=300.00&sign=KSknR0q1OxyTFU5XNx02PG98E%2BXA27Cruyk1qlojlm1tnpaxGVJqOCZq4STFZfHcmUNR6dDN86yihkk3zYXjYREfitefWnQfV4RSWe%2Fozb5bAxDgx4XppVjVzMLqQlLGJFc7DAimuqkQJrrUbobut1zkJnLY6iXvqIZ%2FV9O%2BfRpuYVsyBPOsb1W9rAcS5AYIYHBQph03WaGzJ%2B7njsb8u6idq7r%2FOX2ElWhS4juWJls7KdQtP29IsUp7Mqzw8foK5Ksfd85O%2FTIQA5uv1relIPfex0Z8QOcX0XJ6doESwfXTkhyHQJ%2BUIt4gAya6fibbyYblTN%2BvYxNmZSNeZ34uYQ%3D%3D&trade_no=2020091122001489200501065763&auth_app_id=2016102200739747&version=1.0&app_id=2016102200739747&sign_type=RSA2&seller_id=2088102180664873&timestamp=2020-09-11+18%3A16%3A29
    

    我们这个页面设计的是用户访问到这个页面后, 就发送ajax请求, 后台更新订单的状态为WAIT_COMMENT(待评价)和更新订单的trade_no(支付宝端的订单编号)

    但是肯定不能浏览器一访问这个页面, 我们就直接把url中的trade_no提取出来然后直接更新订单, 因为有可能有人会伪造支付宝拼接的url, 因此我们需要对url中的这些参数进行校验, 来确定这些参数确实是支付宝返回给我们的. 这就是验签. 具体的验签步骤为:

    image-20200911182756059

    支付结果页面的前端逻辑编写

    前端功能主要有两个, 一个是进入页面后立刻发送ajax请求, 将url的参数原封不动的传给后端, 后端进行验签和更新订单操作. 第二个是更新完成之后, 启动3秒倒计时, 自动跳转到"我的订单"页面, 也可以用户直接点击跳转

    编辑对应的html文件payresult.html

    <div class="container">
        <div class="top-bar">
            <div class="nav-bar">
                <h3 class="page-title">支付结果</h3>
                <a class="nav-btn fl" href="#" onclick="hrefBack();"><span><i class="fa fa-angle-left fa-2x"></i></span></a>
            </div>
        </div>
        <div class="house-info">
            <h1>支付成功</h1>
            <a href="orders.html">跳转到我的订单(<span id="num">3</span>秒后自动跳转)</a>
        </div>
    </div>
    </div>
    

    编写对应的js文件payresult.js

    $(document).ready(function(){
        //获取url的数据, 原封不动直接传给后端进行校验
        var url_param = document.location.search.substr(1)
        //发送ajax请求, 修改订单状态
        $.ajax({
            url: 'api/v1.0/orders/alipay',
            type: 'PATCH',
            data: url_param,
            headers: {'X-CSRFToken': getCookie('csrf_token')},
            dataType: 'json',
            success: function (resp) {
                if (resp.errno == '0'){
                    //计时跳转到我的订单
                    function jump(count) {
                        window.setTimeout(function () {
                            count--;
                            if (count > 0) {
                                $('#num').text(count);
                                jump(count);
                            } else {
                                location.href = "orders.html";
                            }
                        }, 1000);
                    }
                    jump(3);
                }else {
                    alert(resp.errmsg);
                }
            }
        });
    })
    

    注:

    1. document.location.search能够获取到url中?后面的所有参数, 包括了?本身, 因此还需要使用substr(1)截取, 只拿到问号后面数据
    2. 获取到的url参数形如charset=utf-8&out_trade_no=97&method=alipay.trade.wap.pay.return, 这种格式和浏览器form表单提交时在请求体中的表单数据格式, 所以我们可以直接进行form表单格式的提交, 而不需要再转化为json格式提交.
    3. 使用jump()参数实现倒计时, 并实时显示倒计时的时间值, 倒计时结束或者用户手动点击了链接, 则会跳转到"我的订单"页面

    支付结果页面的后端逻辑编写

    编辑支付模块的视图文件pay.py, 添加修改订单的接口

    # ihome/api_1_0/pay.py
    @api.route('/orders/alipay', methods=['PATCH'])
    @login_required
    def change_order():
        """支付完成后修改状态"""
        # 接收数据
        data_dict = request.form.to_dict()
        if not data_dict:
            return parameter_error()
        # 校验签名
        if not check_signature(data_dict):
            return jsonify(errno=RET.PARAMERR, errmsg='数据验证失败')
        # 获取订单编号和支付宝编号
        order_id = data_dict.get('out_trade_no')
        trade_no = data_dict.get('trade_no')
        # 更新状态, 限制该订单状态为'待接单', 订单的提交人为当前登录用户
        try:
            order = Orders.query.get(order_id)
        except Exception as e:
            current_app.logger.error(e)
            return jsonify(errno=RET.DBERR, errmsg='获取订单异常')
        # 校验订单
        if not order:
            return jsonify(errno=RET.PARAMERR, errmsg='订单ID不存在')
        if order.status != 'WAIT_PAYMENT':
            return jsonify(errno=RET.PARAMERR, errmsg='订单状态不为"待支付"')
        if order.user != g.user:
            return jsonify(errno=RET.PARAMERR, errmsg='该订单不属于当前用户')
        # 更新订单
        order.status = 'WAIT_COMMENT'
        order.trade_no = trade_no
        try:
            db.session.add(order)
            db.session.commit()
        except Exception as e:
            current_app.logger.error(e)
            return jsonify(errno=RET.DBERR, errmsg='更新订单异常')
    
        return jsonify(errno=RET.OK)
    

    注:

    1. 由于前端使用的是表单格式的提交, 所以直接使用request.form.to_dict()获取到提交的参数并转化为python的字典类型, 需要注意的是request.form也能获取到参数, 并且也有.get()方法获取具体的参数值, 但是request.form返回的并不是python基础的字典类型, 而是其自定义的另一种类型werkzeug.datastructures.ImmutableMultiDict,只是也实现了get()方法而已.
    2. 订单的校验和修改状态和之前"接单"/"拒单"等逻辑类似
    3. 获取到前端的数据后需要先进行验签, 验证通过后才能进行订单的更新操作, 验签方法为check_signature(data_dict)

    验签

    官方的SDK提供了验签的方法verify_with_rs(), 我们需要做的就是提取出签名/排序/编码, 然后调用``verify_with_rs`方法即可

    from alipay.aop.api.util.SignatureUtils import verify_with_rs
    def check_signature(params):
        """验证签名"""
        # 取出签名
        sign = params.pop('sign')
        # 取出签名类型
        params.pop('sign_type')
        # 取出字典的value, 并对字典按key的字母升序排序, 得到新的列表
        params = sorted(params.items(), key=lambda x: x[0], reverse=False)
        # 将列表转换为二进制字符串
        message = '&'.join(f'{k}={v}' for k, v in params).encode()
        # 验证
        try:
            result = verify_with_rsa(constants.ALIPAY_PUBLIC_KEY.encode('utf-8').decode('utf-8'), message, sign)
            return result
        except Exception as e:
            current_app.logger.error(e)
            return False
    

    注:

    1. 该方法只需要剔除sign参数即可, 不需要剔除sign_type, 不然验证不会通过
    2. 如果verify_with_rsa验证通过, 会返回True, 不通过, 则会抛出异常, 所以这里抛出异常并不一定是程序出错了.
  • 相关阅读:
    python爬虫headers设置后无效解决方案
    idea建立web项目servlet映射的地址/jsp访问不到
    bootstrap栅格系统错位问题
    python2 python3共存解决方案
    Springboot+Thymeleaf框架的button错误
    星空雅梦
    星空雅梦
    星空雅梦
    星空雅梦
    星空雅梦
  • 原文地址:https://www.cnblogs.com/gcxblogs/p/13653494.html
Copyright © 2011-2022 走看看