支付宝支付
当房东接单后, 房客就可以在我的订单页面点击去支付按钮, 完成支付, 这里只实现支付宝支付方式.
打开支付宝开放平台, 可以查看支付宝的相关接口文档和教程, 这里就不一一说明了, 只写出关键的几个步骤:
-
这里只在支付宝的测试环境(沙箱环境)中做测试, 不在支付宝的正式环境, 沙箱环境提供了测试的沙箱应用和沙箱账号, 通过
控制台>开发服务>研发服务
进入沙箱页面 -
使用支付宝开放平台开发助手工具, 生成
RSA2
一对秘钥, 即应用私钥和应用公钥, 将应用公钥添加到沙箱应用的RSA2密钥
处 -
下载支付宝官方SDK, SDK网址为: https://opendocs.alipay.com/open/54/103419, 选择python, 来到pip下载页面: https://pypi.org/project/alipay-sdk-python/3.3.398/
- 找到支付宝相应的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
注:
-
根据支付宝SDK上的调用实例, 可以发现分为四步, 创建支付宝客户端/构造请求/执行API调用/解析API返回结果
-
对于不同的支付宝接口, 大体结构都不变, 只是换一下不同接口对应的Model和Request, 根据接口名首字母大写可找到Model和Request, 然后不同接口执行API后的返回结果可能也不一样, 有的返回的是url有的返回的是执行结果等.
-
这里分别构造了这几步对应的方法:
alipay_client()
获取客户端,alipay_model()
获取模型,alipay_pay()
调用API -
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产品码会不一样
-
回调通知地址:
-
RETURN_URL
是用户在支付完成后, 点击右上角"完成"按钮, 页面会跳转到RETURN_URL
这个url是让用户访问的.一般我们会设置为一个支付的中间页面, 因为支付完成之后, 我们还需要对订单的状态或者一些信息进行修改, 所以设置了一个中间页面, 在这个中间页面进行信息的修改, 修改完成后自动跳转到"我的订单"页面, 如这里设置为
ALIPAY_RETURN_URL = "http://127.0.0.1:5000/payresult.html"
-
NOTIFY_URL
是指用户在支付完成后, 点击右上角"完成"按钮, 支付宝会使用POST
方式请求这里设置的NOTIFY_URL
, 将支付结果发送过来, 所以这个url是支付宝后台访问的, 跟前台用户没有关系. 当然既然是给支付宝访问的, 那么这个url必须是公网IP, 否则访问不到 -
由于我们没有公网IP, 所以这里就只设置
RETURN_URL
, 除了设置这两个URL可以得知支付结果外, 还可以调用支付宝的查询接口alipay.trade.query(统一收单线下交易查询)
具体使用可以查看另一个Django项目的支付宝例子: https://www.cnblogs.com/gcxblogs/p/12895891.html, 这里我们也不使用这个接口了.
-
-
alipay_model(order)
创建支付模型, 需要根据具体的API设置相应的属性, 支付功能的话一般订单编号, 金额, 标题, 产品码都是必须要填写的 -
alipay_pay(client, model)
传入前面创建的client
和model
, 调用执行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})
记录一下遇到过的问题:
-
如果支付的金额为0, 那么支付宝会直接返回报错: 系统繁忙
-
如果
RETURN_URL
设置错了, 那么在点击右上角的"完成"按钮后, 不会有反应 -
如果该订单已经支付了, 会报错已经支付成功
支付宝支付前端逻辑编写
前面在"我的订单"页面已经写好了, 就是点击"支付"按钮, 调用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
:
需要注意的是, 支付宝会在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×tamp=2020-09-11+18%3A16%3A29
我们这个页面设计的是用户访问到这个页面后, 就发送ajax请求, 后台更新订单的状态为WAIT_COMMENT
(待评价)和更新订单的trade_no
(支付宝端的订单编号)
但是肯定不能浏览器一访问这个页面, 我们就直接把url中的trade_no
提取出来然后直接更新订单, 因为有可能有人会伪造支付宝拼接的url, 因此我们需要对url中的这些参数进行校验, 来确定这些参数确实是支付宝返回给我们的. 这就是验签. 具体的验签步骤为:
支付结果页面的前端逻辑编写
前端功能主要有两个, 一个是进入页面后立刻发送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);
}
}
});
})
注:
document.location.search
能够获取到url中?
后面的所有参数, 包括了?
本身, 因此还需要使用substr(1)
截取, 只拿到问号后面数据- 获取到的url参数形如
charset=utf-8&out_trade_no=97&method=alipay.trade.wap.pay.return
, 这种格式和浏览器form表单提交时在请求体中的表单数据格式, 所以我们可以直接进行form表单格式的提交, 而不需要再转化为json格式提交. - 使用
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)
注:
- 由于前端使用的是表单格式的提交, 所以直接使用
request.form.to_dict()
获取到提交的参数并转化为python的字典类型, 需要注意的是request.form
也能获取到参数, 并且也有.get()
方法获取具体的参数值, 但是request.form
返回的并不是python基础的字典类型, 而是其自定义的另一种类型werkzeug.datastructures.ImmutableMultiDict
,只是也实现了get()
方法而已. - 订单的校验和修改状态和之前"接单"/"拒单"等逻辑类似
- 获取到前端的数据后需要先进行验签, 验证通过后才能进行订单的更新操作, 验签方法为
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
注:
- 该方法只需要剔除
sign
参数即可, 不需要剔除sign_type
, 不然验证不会通过 - 如果
verify_with_rsa
验证通过, 会返回True, 不通过, 则会抛出异常, 所以这里抛出异常并不一定是程序出错了.