Django2.0.2(Django-rest-framework) 以及前端 vue 开发的前后端分离的商城网站
线上演示地址: http://vueshop.mtianyan.cn/
github 源代码地址: https://github.com/mtianyan/VueDjangoFrameWorkShop
本小节: 支付宝支付源码解读
支付宝支付源码解读
init 的时候有一些参数
- def __init__(self, appid, app_notify_url, app_private_key_path,
- alipay_public_key_path, return_url, debug=False):
- # appid 是我们的 appid 或沙箱环境的 appid
- self.appid = appid
- #
- self.app_notify_url = app_notify_url
- # 我们私钥文件的路径
- self.app_private_key_path = app_private_key_path
- # 我们是要读文件然后生成 private_key 的
- self.app_private_key = None
- #
- self.return_url = return_url
- # 打开这个文件, 读这个文件, 读完之后
- # 调用 RSA 的 import_key(from Crypto.PublicKey import RSA)
- with open(self.app_private_key_path) as fp:
- self.app_private_key = RSA.importKey(fp.read())
- # 支付宝的公钥在链接生成的过程中实际没什么用
- # 但是在验证支付宝给我们返回的消息的时候, 有用
- self.alipay_public_key_path = alipay_public_key_path
- with open(self.alipay_public_key_path) as fp:
- self.alipay_public_key = RSA.import_key(fp.read())
- # debug 是 true 会调用沙箱的 gateway
- if debug is True:
- self.__gateway = "https://openapi.alipaydev.com/gateway.do"
- else:
- self.__gateway = "https://openapi.alipay.com/gateway.do"
direct_pay 方法
- # 传递一些参数进来交易标题, 我们的订单号, 总金额
- def direct_pay(self, subject, out_trade_no, total_amount, return_url=None, **kwargs):
- biz_content = {
- "subject": subject,
- "out_trade_no": out_trade_no,
- "total_amount": total_amount,
- "product_code": "FAST_INSTANT_TRADE_PAY",
- # "qr_pay_mode":4
- }
- biz_content.update(kwargs)
- data = self.build_body("alipay.trade.page.pay", biz_content, self.return_url)
- return self.sign_data(data)
biz_content 是一个很重要的参数与订单相关的四个必填字段
通过 python 传入可变参数如果后面还有其他的值, 只需要传进来就行了
当 biz_content 生成好了之后, 会调用 build_body 方法
- def build_body(self, method, biz_content, return_url=None):
- # data 中字段与公共请求的必填字段一致
- data = {
- "app_id": self.appid,
- "method": method,
- "charset": "utf-8",
- "sign_type": "RSA2",
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- "version": "1.0",
- "biz_content": biz_content
- }
- if return_url is not None:
- data["notify_url"] = self.app_notify_url
- data["return_url"] = self.return_url
- return data
参数有两种: 一种是公共请求参数, 一种是请求参数 (业务)
整个参数以公共请求参数为主, 这里面嵌套了 bizcontent
build_body 是生成公共请求参数 (包含 biz_content)
如果传递进来的参数有 return_url 那么我们就将 return_url 也放入 data 中
buildbody 是用来生成整个消息格式的
获取到消息格式的 data
对于这个消息格式进行签名
- def sign_data(self, data):
- data.pop("sign", None)
- # 排序后的字符串
- unsigned_items = self.ordered_data(data)
- unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in unsigned_items)
- sign = self.sign(unsigned_string.encode("utf-8"))
- # ordered_items = self.ordered_data(data)
- quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in unsigned_items)
- # 获得最终的订单信息字符串
- signed_string = quoted_string + "&sign=" + quote_plus(sign)
- return signed_string
签名非常的关键在签名之前, 将 sign 字段先 pop 掉 (如果有, 现在当然是没有的)
data 就是一个清除了不必要参数的 data
对于 data 进行排序参数传进来进行排序
- def ordered_data(self, data):
- complex_keys = []
- for key, value in data.items():
- if isinstance(value, dict):
- complex_keys.append(key)
- # 将字典类型的数据 dump 出来
- for key in complex_keys:
- data[key] = json.dumps(data[key], separators=(',', ':'))
对于 data 进行排序参数传进来进行排序 return sorted([(k, v) for k, v in data.items()])
对于 data 进行排序, 生成有一个 tuple 这是一个有顺序的 tuple
mark
unsigned_items 已经是排过序的一个数组了
app_id = 沙箱 appid&biz_content={"subject":"\u6d4b\u8bd5\u8ba2\u5355","out_trade_no":"20180312mtianyan001","total_amount":9999,"product_code":"FAST_INSTANT_TRADE_PAY"}&charset=utf-8&method=alipay.trade.page.pay¬ify_url=http://127.0.0.1:8000/alipay/return/&return_url=http://127.0.0.1:8000/alipay/return/&sign_type=RSA2×tamp=2018-03-12 18:59:09&version=1.0
unsigned_string 是拼接而成的字符串
这时我们就要拿着这个字符串进行签名了
- sign = self.sign(unsigned_string.encode("utf-8"))
- def sign(self, unsigned_string):
- # 开始计算签名
- key = self.app_private_key
- signer = PKCS1_v1_5.new(key)
- signature = signer.sign(SHA256.new(unsigned_string))
- # base64 编码, 转换为 unicode 表示并移除回车
- sign = encodebytes(signature).decode("utf8").replace("\n", "")
- return sign
首先要拿到我们的私钥, 然后使用 PKCS1_v1_5 进行签名
from Crypto.Signature import PKCS1_v1_5
生成一个签名的对象 signer, 调用该对象的 sign 函数使用 SHA256 算法生成一个签名
生成签名还没有完, 文档中说明了签名还有最后一步并进行 base64 编码
from base64 import decodebytes, encodebytes
我们这里用到的是 base64 的 encodebytes 编码编码之后我们要使用 decode 将其转换为 utf-8 字符串
签名字符串完成
得到 sign 的值之后
quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in unsigned_items)
quote_plus 会将 url 做一定的处理区别在于不会让参数签名带 http:// 等多余字符
也就是签名用的是原始字符串, 而我们的 url 请求时是往 quote 后的字符串中添加 sign 参数
最终的返回值被 url 接收拼接上我们的调用沙箱接口 url 形成支付页面链接
这个订单我们也是可以进行支付的
支付宝支付成功之后会跳回到我们自己的页面这是因为我们没有给他传递我们的 return_url
支付完成之后立即跳回去的页面
已支付的订单号再去生成的话会提示订单已支付
会变成我们传递过去的 url 并跟一系列的参数 - 这就是我们 return url 的作用
那么问题来了, notify url 是什么意思呢?
用户扫码但是没有支付, 然后将支付页面关闭了用户如果在手机里的账单里支付
不是来这个页面支付, 那么这个 return url 就没有用了
return url 是我们在支付成功之后支付宝会自动跳转的页面
notify url 就是用户一旦完成支付 (不在当前支付页面, 而是通过如手机账单等)
支付宝会给你发起一个异步的请求
异步的请求就是说比如我们把页面关了之后, 支付宝发了一个通知告诉你该账单已经被用户支付了需要去系统里更改订单的状态啊等一些后续的工作
那时候的 url 已经和支付宝产生一个异步的交互了是不可能给浏览器再返回页面的
所以我们也需要一个异步接收的接口 return 是一个同步接收的接口
支付宝通知接口验证
对于 return url 和 notify url 进行更加详细的介绍
上一节课我们已经可以生成支付宝支付的页面
这是一个支付宝给我们发回来的请求我们需要验证支付宝给我们返回的数据是否有效
因为这个数据有可能是被别人截获它会修改里面的数据, 让订单状态变为成功支付
验证数据是否对与生成的加密过程正好是一个反过程
- def verify(self, data, signature):
- if "sign_type" in data:
- sign_type = data.pop("sign_type")
- # 排序后的字符串
- unsigned_items = self.ordered_data(data)
- message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items)
- return self._verify(message, signature)
mark
把里面的 sign pop 出去, 然后对剩下的部分进行签名, 将签名之后的字符串与原来的进行比对就知道是否合法了
- return_url = 'http://127.0.0.1:8000/?total_amount=100.00×tamp=2017-08-15+23:53:34&sign=e9E9UE0AxR84NK8TP1CicX6aZL8VQj68ylugWGHnM79zA7BKTIuxxkf/vhdDYz4XOLzNf9pTJxTDt8tTAAx/fUAJln4WAeZbacf1Gp4IzodcqU/sIc4z93xlfIZ7OLBoWW0kpKQ8AdOxrWBMXZck/1cffy4Ya2dWOYM6Pcdpd94CLNRPlH6kFsMCJCbhqvyJTflxdpVQ9kpH+/hpqrqvm678vLwM+29LgqsLq0lojFWLe5ZGS1iFBdKiQI6wZiisBff+dAKT9Wcao3XeBUGigzUmVyEoVIcWJBH0Q8KTwz6IRC0S74FtfDWTafplUHlL/nf6j/Qd1y6Wcr2A5Kl6BQ==&trade_no=2017081521001004340200204115&sign_type=RSA2&auth_app_id=2016080600180695&charset=utf-8&seller_id=2088102170208070&method=alipay.trade.page.pay.return&app_id=2016080600180695&out_trade_no=20170202185&version=1.0'
- o = urlparse(return_url)
- query = parse_qs(o.query)
- processed_query = {}
- ali_sign = query.pop("sign")[0]
一定要 pop 一下, 将里面的 sign 删除掉
- def _verify(self, raw_content, signature):
- # 开始计算签名
- key = self.alipay_public_key
- signer = PKCS1_v1_5.new(key)
- digest = SHA256.new()
- digest.update(raw_content.encode("utf8"))
- if signer.verify(digest, decodebytes(signature.encode("utf8"))):
- return True
- return False
字符串, 与支付宝给我们返回的 sign 进行比对
有了验证之后我们就要去修改支付宝支付的订单的状态等的接口实现
写一个接口 Alipayviewset
django 集成支付宝 notify_url 和 return_url 接口 - 1
不管是同步还是异步的支付, 都会向我们发起 notify url 的 post 请求
return url 是一个非必填字段, 只会在支付宝页面告诉我们支付成功
支付结果异步通知
https://docs.open.alipay.com/270/105902/
对于 PC 网站支付的交易, 在用户支付完成之后, 支付宝会根据 API 中商户传入的 notify_url, 通过 POST 请求的形式将支付结果作为参数通知到商户系统
页面回跳参数
return_url 是一种同步返回的机制: 会发起向我们服务器的 get 请求
写一个 view, 既可以处理 post, 又可以处理 get 只需要配置一个 url, 就可以完成对于两种形式返回的适配
因为我们跟支付宝相关的这部分没有 model 所以我们可以使用最底层的 api view 来完成
- from rest_framework.views import APIView
- class AlipayView(APIView):
定义 get 和 post 函数, 分别用于处理 return url 和 notify url
- def get(self, request):
- """处理支付宝的 return_url 返回"""
- def post(self, request):
- """处理支付宝的 notify_url"""
前往 url 中配置一个接口地址
- from trade.views import AlipayView
- # 支付宝支付相关接口
- path('alipay/return/', AlipayView.as_view())
先处理异步请求
先验证支付宝 post 请求是否进来
将我们的 utils 中的 Alipay.py 中的 return url 和 notifyurl 都改为线上地址
如:
http://115.159.122.64:8000/alipay/return/
重要问题: 现在的修改都是本地的修改
我们的解释器是线上的地址, 如果在本地修改了什么都不做不 upload, 还是会用服务器上的旧版代码进行调试
注意要选定整个项目目录文件夹进行上传
修改一下订单号
这次我们只修改了 Alipay.py 我们可以只单独上传
用手机扫码一下, 它跳转到了 post 方法
我们观察参数如下:
mark
数据都存放在 request.POST 中, 里面有 sign 等字段, 我们拿着 sign 就可以去验签
保证这个是支付宝给我们发过来的
异步通知的参数:
签名:
业务参数: trade_no 是支付宝返回的交易凭证号
out_trade_no: 我们自己平台的订单号
Trade_status: 支付宝的交易状态
TRADE_FINISHED 交易完成 false(不触发通知)
TRADE_SUCCESS 支付成功 true(触发通知)
WAIT_BUYER_PAY 交易创建 false(不触发通知)
TRADE_CLOSED 交易关闭 false(不触发通知)
我们就可以把这些定义到我们的 model 中
- ORDER_STATUS = (
- ("TRADE_SUCCESS", "成功"),
- ("TRADE_CLOSED", "超时关闭"),
- ("WAIT_BUYER_PAY", "交易创建"),
- ("TRADE_FINISHED", "交易结束"),
- ("paying", "待支付"),
- )
配置支付宝相关 key 的路径在 setttings.py 中
- # 支付宝相关的 key 路径
- private_key_path = os.path.join(BASE_DIR, 'apps/trade/keys/private_2048.txt')
- ali_pub_key_path = os.path.join(BASE_DIR, 'apps/trade/keys/alipay_key_2048.txt')
编程 post 方法
- from utils.alipay import AliPay
- from VueDjangoFrameWorkShop.settings import ali_pub_key_path, private_key_path
- def post(self, request):
- """处理支付宝的 notify_url"""
- # 1. 先将 sign 剔除掉
- processed_dict = {}
- for key, value in request.POST.items():
- processed_dict[key] = value
- sign = processed_dict.pop("sign", None)
- # 2. 生成一个 Alipay 对象
- alipay = AliPay(
- appid="",
- app_notify_url="http://115.159.122.64:8000/alipay/return/",
- app_private_key_path=private_key_path,
- alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥, 验证支付宝回传消息使用, 不是你自己的公钥,
- debug=True, # 默认 False,
- return_url="http://115.159.122.64:8000/alipay/return/"
- )
- # 3. 进行验签, 确保这是支付宝给我们的
- verify_re = alipay.verify(processed_dict, sign)
mark
可以看到我们的 processed_dict
- from rest_framework.response import Response
- # 如果验签成功
- if verify_re is True:
- order_sn = processed_dict.get('out_trade_no', None)
- trade_no = processed_dict.get('trade_no', None)
- trade_status = processed_dict.get('trade_status', None)
- # 查询数据库中存在的订单
- existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
- for existed_order in existed_orders:
- # 订单商品项
- order_goods = existed_order.goods.all()
- # 商品销量增加订单中数值
- for order_good in order_goods:
- goods = order_good.goods
- goods.sold_num += order_good.goods_num
- goods.save()
- # 更新订单状态, 填充支付宝给的交易凭证号
- existed_order.pay_status = trade_status
- existed_order.trade_no = trade_no
- existed_order.pay_time = datetime.now()
- existed_order.save()
- # 将 success 返回给支付宝, 支付宝就不会一直不停的继续发消息了
- return Response("success")
如果验签失败, 就不返回信息了
其实 return 的逻辑和 notify 的差不多, 只不过是从 get 中请求到数据罢了
如果支付后希望可以调回到个人订单页面, 这时候 return url 的作用就出来了
- def get(self, request):
- """处理支付宝的 return_url 返回"""
- processed_dict = {}
- # 1. 获取 GET 中参数
- for key, value in request.GET.items():
- processed_dict[key] = value
- # 2. 取出 sign
- sign = processed_dict.pop("sign", None)
- # 3. 生成 ALipay 对象
- alipay = AliPay(
- appid="2016091200490210",
- app_notify_url="http://115.159.122.64:8000/alipay/return/",
- app_private_key_path=private_key_path,
- alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥, 验证支付宝回传消息使用, 不是你自己的公钥,
- debug=True, # 默认 False,
- return_url="http://115.159.122.64:8000/alipay/return/"
- )
- verify_re = alipay.verify(processed_dict, sign)
- # 这里可以不做操作因为不管发不发 return urlnotify url 都会修改订单状态
- if verify_re is True:
- order_sn = processed_dict.get('out_trade_no', None)
- trade_no = processed_dict.get('trade_no', None)
- trade_status = processed_dict.get('trade_status', None)
- existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
- for existed_order in existed_orders:
- existed_order.pay_status = trade_status
- existed_order.trade_no = trade_no
- existed_order.pay_time = datetime.now()
- existed_order.save()
支付宝和 vue 联调
流程: 退出之后重新登录, 将 localhost 改为线上地址
在创建订单的过程中, 会将购物车清空, 创建 order_goods, 创建 order 对象
现在我们要涉及支付了, 还要有支付的页面如何生成支付的 url 呢
我们可以重写 createModelMixin 的 Create 方法把支付宝支付的 url 也放进来
不重载 create, 而是 Serializer 的另一个功能
Serializer fields 里面的 SerializerMethodField
- from django.contrib.auth.models import User
- from django.utils.timezone import now
- from rest_framework import serializers
- class UserSerializer(serializers.ModelSerializer):
- days_since_joined = serializers.SerializerMethodField()
- class Meta:
- model = User
- def get_days_since_joined(self, obj):
- return (now() - obj.date_joined).days
可以让我们自己写函数, 在函数里面写逻辑这样就会变的灵活, 不再依赖于数据表里的某一个字段了
在我们的 OrderSerializer 中
在用户提交了订单的时候, 我们可以在 Serializer 中加一个字段 alipay_url 专门返回
支付宝的支付链接不能让用户自己提交, 是服务器端生成返回给用户的
字段前面加上 get 就是用来跟它做对应的函数
- alipay_url = serializers.SerializerMethodField(read_only=True)
- def get_alipay_url(self, obj):
- alipay = AliPay(
- appid="2016091200490210",
- app_notify_url="http://115.159.122.64:8000/alipay/return/",
- app_private_key_path=private_key_path,
- alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥, 验证支付宝回传消息使用, 不是你自己的公钥,
- debug=True, # 默认 False,
- return_url="http://115.159.122.64:8000/alipay/return/"
- )
上线之后记得改为 debug 为 false, 参数改完之后就可以调用之前的逻辑了
- url = alipay.direct_pay(
- subject=obj.order_sn,
- out_trade_no=obj.order_sn,
- total_amount=obj.order_mount,
- )
- re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)
- return re_url
因为订单里会有多个商品, 所以标题改为我们自己商户的订单号
return_url 不需要再传,
data = self.build_body("alipay.trade.page.pay", biz_content, self.return_url)
因为在 Alipay 中 build_body 的时候已经传进去过了
别忘了服务器上传代码
我们还要将这个逻辑拷贝到 OrderDetailSerializer 中
- alipay_url = serializers.SerializerMethodField(read_only=True)
- def get_alipay_url(self, obj):
- alipay = AliPay(
- appid="2016091200490210",
- app_notify_url="http://115.159.122.64:8000/alipay/return/",
- app_private_key_path=private_key_path,
- alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥, 验证支付宝回传消息使用, 不是你自己的公钥,
- debug=True, # 默认 False,
- return_url="http://115.159.122.64:8000/alipay/return/"
- )
- url = alipay.direct_pay(
- subject=obj.order_sn,
- out_trade_no=obj.order_sn,
- total_amount=obj.order_mount,
- )
- re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)
- return re_url
因为订单详情中未支付订单的立即使用支付宝支付也是需要跳转到支付宝的
可以将 appid,return_url 等都配置到 setting 中去可配置性会高一点
查看 vue 中是怎么做的
src/views/cart/cart.vue 中:
- createOrder(
- {
- post_script:this.post_script,
- address:this.address,
- signer_name:this.signer_name,
- singer_mobile:this.signer_mobile,
- order_mount:this.totalPrice
- }
- ).then((response)=> {
- alert('订单创建成功')
- window.location.href=response.data.alipay_url;
- }).
订单创建成功会跳转到接口返回数据中的 alipay_url
这里的留言是必填的, 如果想改为可选修改 model
mark
我们希望支付完成之后, 跳转回我们的订单列表页面
解决方案 1: 支付宝返回的不是一个页面而是这个图片, 让 vue 去显示图片自己做页面内部的跳转
它的 return_url 实际是服务器的一个 url 服务器时无法通知我们的 vue 做更新的
解决方案 2
简单暴力的做法让 django 来返回这个页面不让 node.js 来代理这个页面
就类似于一种 template 的机制了这样就不存在跨域的问题了
将 vue 纳入 django 项目中
这关乎于部署方式
vue 有两种开发模式, 一种是 dev 模式, 一种是 build 模式
npm run build
build 是用来生成 html 及静态文件的
mark
首先将 index.html 拷贝出来放到 template 目录里
mark
然后新建 static 目录
mark
设置我们的 setting 文件中的 MEDIA_URL
- STATIC_URL = '/static/'
- STATICFILES_DIRS = [
- os.path.join(BASE_DIR, "static"),
- ]
修改 index.html 中的引用
mark
- # 首页
- path('index/', TemplateView.as_view(template_name='index.html'))
静态文件出问题 404:
- STATIC_URL = '/static/'
- STATIC_ROOT = os.path.join(BASE_DIR, 'static')
- re_path('static/(?P<path>.*)', serve, {"document_root": STATIC_ROOT}),
此时我们可以通过 http:// 远端 ip 地址: 8000/index / 访问到我们的首页
mark
为首页路径添加 name
path('index/', TemplateView.as_view(template_name='index.html'),name='index'),
trade/views.py 中 return 的时候:
- from django.shortcuts import render, redirect
- response = redirect("index")
- # 希望跳转到 vue 项目的时候直接帮我们跳转到支付的页面
- response.set_cookie("nextPath","pay", max_age=3)
- return response
- else:
- response = redirect("index")
- return response
实际上在 vue 中写过一个逻辑
- src/router/index.js
- // 进行路由判断
- router.beforeEach((to, from, next) => {
- var nextPath = cookie.getCookie('nextPath')
- console.log(nextPath)
- if(nextPath=="pay"){
- next({
- path: '/app/home/member/order',
- });
- }
如果 cookie 有 nextpath 直接跳转到订单 max_age 尽量设置短一点, 让它取一次就可以过期
将 order.vue 中的 success 改为与我们 model 中一致的 TRADE_SUCCESS
mark
src/views/member/orderDetail.vue
做同样的处理
还有一种模式是支付宝给我们一张图片让 vue 嵌入图片
参数: qr_pay_model 设置为 4. 订单码, 生成一张图片
第十章完结
老师埋的几个坑
支付宝接口会先发起 post 请求, 再响应 get 请求而 returnurl 的返回参数中并不包含我们的支付状态, 所以会导致原本被 post 请求写入的支付状态被 get 请求获取不到的默认值 none 覆盖为空
与 vue 联调时, 会报错
Uncaught RangeError: Maximum call stack size exceeded
前端不熟, 所以我将老师的 setcookie, 前端取值, 如果有该值就跳转订单页面改为了后端实现
- response = redirect("/index/#/app/home/member/order")
- # response.set_cookie("nextPath","pay", max_age=3)
这都是我自己摸索 debug 出来的成果如果对你有所帮助, 还请打赏
来源: http://www.jianshu.com/p/ca803f152ad3