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, 不通过, 则会抛出异常, 所以这里抛出异常并不一定是程序出错了.
posted @ 2020-09-11 19:09  Alex-GCX  阅读(267)  评论(0编辑  收藏  举报