思路:调用第三方支付 API 接口实现支付功能。本来想用支付宝来实现第三方网站的支付功能的,但是在实际操作中发现支付宝没有 Python 接口,网上虽然有他人二次封装的的 Python 接口,但是对我这个小白白来说上手还是有点难度,后来发现 PayPal 有现成的 Django 模块,想着以学习的目的来实现这一功能(其实还是自己辣鸡),就决定以 PayPal 的电子支付功能来练手。
首先,安装 PayPal 的 Django 模块:django-paypal,具体介绍可以参考 GitHub上说明:https://github.com/spookylukey/django-paypal
pip install django-paypal
然后在 settings.py 中的 INSTALLED_APPS 将 'paypal.standard.ipn' 加入。并在 settings.py 中添加下列语句。
# 此付款机制作为测试用
PAYPAL_TEST = True
# 设置收款的 PayPal 电子邮件账户
PAYPAL_REVEIVER_EMAIL = 'your email'
执行同步数据库操作。
./manage.py migrate
urls.py 中加入下列样式。分别为付款完成通知,处理账务,显示完成付款,取消付款操作。
url(r'^paypal/', include('paypal.standard.ipn.urls')), # 付款完成通知
url(r'^payment/(d+)/$', views.payment),
url(r'^done/$', views.payment_done),
url(r'^canceled/$', views.payment_canceled),
PayPal 付款操作,建立含有正确数据的付款按钮。
@login_required
def payment(request, order_id):
all_categories = models.Category.objects.all()
try:
order = models.Order.objects.get(id=order_id)
except:
messages.add_message(request, messages.WARNING, "订单编号错误,无法处理付款。")
return redirect('/myorders/')
all_order_items = models.OrderItem.objects.filter(order=order)
items = list()
total = 0
for order_item in all_order_items:
t = dict()
t['name'] = order_item.product.name
t['price'] = order_item.product.price
t['quantity'] = order_item.quantity
t['subtotal'] = order_item.product.price * order_item.quantity
total = total + order_item.product.price
items.append(t)
host = request.get_host()
paypal_dict = {
"business": settings.PAYPAL_REVEIVER_EMAIL,
"amount": total,
"item_name": "迷你小电商商品编号:{}".format(order_id),
"invoice": "invoice-{}".format(order_id),
"currency_code": 'CNY',
"notify_url": "http://{}{}".format(host, reverse('paypal-ipn')),
"return_url": "http://{}/done/".format(host),
"cancel_return": "http://{}/canceled/".format(host),
}
paypal_form = PayPalPaymentsForm(initial=paypal_dict)
template = get_template('payment.html')
html = template.render(context=locals(), request=request)
return HttpResponse(html)
由于用到了 django-paypal 提供的 PayPalPaymentForm 类。因此在 views.py 的前面也要导入这个类。另外,因为用到了 settings.py 中的常数,所以也要导入 settings,语句如下:
from django.conf import settings
from paypal.standard.forms import PayPalPaymentsForm
from django.core.urlresolvers import reverse
付款完成。
@csrf_exempt #csrf 验证
def payment_done(request):
template = get_template('payment_done.html')
html = template.render(context=locals(), request=request)
return HttpResponse(html)
取消付款。
@csrf_exempt
def payment_canceled(request):
template = get_template('payment_canceled.html')
html = template.render(context=locals(), request=request)
return HttpResponse(html)
PayPal 付款页面。
<!-- payment.html (mshop project) -->
{% extends "base.html" %}
{% block title %}选择您的付款方式{% endblock %}
{% block content %}
<div class='container'>
{% for message in messages %}
<div class='alert alert-{{message.tags}}'>{{ message }}</div>
{% endfor %}
<div class='row'>
<div class='col-md-12'>
<div class='panel panel-default'>
<div class='panel-heading' align=center>
<h3>欢迎光临迷你小电商</h3>
{% if user.socialaccount_set.all.0.extra_data.name %}
{{user.socialaccount_set.all.0.extra_data.name}}<br/>
<img src='{{user.socialaccount_set.all.0.get_avatar_url}}' width='100'>
{% else %}
Welcome: {{ user.username }}
{% endif %}
</div>
</div>
</div>
</div>
<div class='row'>
<div class='col-sm-12'>
<div class='panel panel-info'>
<div class='panel panel-heading'>
<h4>在线付款(订单编号:{{order.id}})</h4>
</div>
<div class='panel panel-body'>
{% for item in items %}
{% if forloop.first %}
<table border=1>
<tr>
<td width=300 align=center>产品名称</td>
<td width=100 align=center>单价</td>
<td width=100 align=center>数量</td>
<td width=100 align=center>小计</td>
</tr>
{% endif %}
<div class='listgroup'>
<div class='listgroup-item'>
<tr>
<td>{{ item.name }}</td>
<td align=right>{{ item.price }}</td>
<td align=center>{{ item.quantity }}</td>
<td align=right>{{ item.subtotal }}</td>
</tr>
</div>
</div>
{% if forloop.last %}
</table>
{% endif %}
{% empty %}
<em>此订单是空的</em>
{% endfor %}
{{ paypal_form.render }}
</div>
<div class='panel panel-footer'>
NT$:{{ total }}元
</div>
</div>
</div>
</div>
</div>
{% endblock %}
付款完成页面。
<!-- payment_done.html (mshop project) -->
{% extends "base.html" %}
{% block title %}Pay using PayPal{% endblock %}
{% block content %}
<div class='container'>
{% for message in messages %}
<div class='alert alert-{{message.tags}}'>{{ message }}</div>
{% endfor %}
<div class='row'>
<div class='col-md-12'>
<div class='panel panel-default'>
<div class='panel-heading' align=center>
<h3>欢迎光临迷你小电商</h3>
{% if user.socialaccount_set.all.0.extra_data.name %}
{{user.socialaccount_set.all.0.extra_data.name}}<br/>
<img src='{{user.socialaccount_set.all.0.get_avatar_url}}' width='100'>
{% else %}
Welcome: {{ user.username }}
{% endif %}
</div>
</div>
</div>
</div>
<div class='row'>
<div class='col-sm-12'>
<div class='panel panel-info'>
<div class='panel panel-heading'>
<h4>从PayPal付款成功</h4>
</div>
<div class='panel panel-body'>
感谢您的支持,我们会尽快处理您的订单。
</div>
<div class='panel panel-footer'>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
取消付款页面。
<!-- payment_canceled.html (mshop project) -->
{% extends "base.html" %}
{% block title %}PayPal 付款取消通知{% endblock %}
{% block content %}
<div class='container'>
{% for message in messages %}
<div class='alert alert-{{message.tags}}'>{{ message }}</div>
{% endfor %}
<div class='row'>
<div class='col-md-12'>
<div class='panel panel-default'>
<div class='panel-heading' align=center>
<h3>欢迎光临迷你小电商</h3>
{% if user.socialaccount_set.all.0.extra_data.name %}
{{user.socialaccount_set.all.0.extra_data.name}}<br/>
<img src='{{user.socialaccount_set.all.0.get_avatar_url}}' width='100'>
{% else %}
Welcome: {{ user.username }}
{% endif %}
</div>
</div>
</div>
</div>
<div class='row'>
<div class='col-sm-12'>
<div class='panel panel-info'>
<div class='panel panel-heading'>
<h4>您刚刚取消了PayPal的付款</h4>
</div>
<div class='panel panel-body'>
<p>请再次检查您的付款,或是返回<a href='/myorders/'>我的订单</a>选用其它付款方式。</p>
</div>
<div class='panel panel-footer'>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
PayPal 在处理完在线付款流程后会另外发送一个 HTTP 数据给我们的网站,我们应该编写一个处理这个信号的函数,更改我们数据库中的内容,为了确保我们设置的监听函数可以被系统加载且保持运行,在 views.py 的同级目录中建立一个名为 signal.py 文件。
from mysite import models
from paypal.standard.models import ST_PP_COMPLETED
from paypal.standard.ipn.signals import valid_ipn_received
def payment_notfication(sender, **kwargs):
ipn_obj = sender
if ipn_obj.payment_status == ST_PP_COMPLETED:
order_id = ipn_obj.invocie.split('-')[-1]
order = models.Order.objects.get(id = order_id)
order_id.paid = True
order.save()
valid_ipn_received.connect(payment_notfication)
在同一文件夹下再创建一个名为 apps.py 的文件,确保上述编写的函数在一开始的时候就能够加载。
from django.apps import AppConfig
class PaymentConfig(AppConfig):
name = 'mysite'
verbose_name = 'Mysite'
def ready(self):
import mysite.signal
在同一文件夹下的 __init__.py 中加入以下语句,确保我们在应用程序初始化加载的时候,可以把我们自定义的应用程序环境设置成能够加载自定义的工作。
default_app_config = 'mysite.apps.PaymentConfig'
通过上述设置,我们的网站已经可以正确地接受订单并使用 PayPal 付款了,我们可以在 PayPal 开发者网站(https://developer.paypal.com/)申请一个测试账号来进行付款测试。
点击进入 dashboard 界面,点击 sandbox 下的 account 选项,我们可以在此创建一个测试账号。
点击创建账号下的 profile 选项,进入详情页,设置此账号的密码,并将 Payment Review 的功能设置为 Off。
接下来我们便可以在我们的网站中使用这个测试账号付款了,点击前往付款,调用 payment 函数,加载含有正确数据的付款按钮,点击后便跳转到 paypal 的沙盒付款页面,我们在其中填入我们之前建立好的测试账号信息,登录后便可以付款了。
付款成功后便返回我们之前编写好的付款成功页面。
注意:中国大陆的 paypal 账号不能用来测试实际支付,需要大陆以外的 paypal 账户才可测试实际支付。(真是坑。。。)
不然付款的时候会出现下列界面。
到这里,我们的付款便已经成功了,但是 PayPal 无法将支付状态通知发送到我们的应用,这是由于我们的项目运行在外部无法访问的 127.0.0.1 上。我们使用 Ngrok 来实现因特网访问开发环境。
在 Ngrok 官网 https://ngrok.com/ 下载解压文件并关联账号后,运行下列命令。
./ngrok http 8000
这个命令将在 8000 端口为本地主机创建一个通道并为其设置一个网络可以访问的主机名称,得到以下输出:
我们可以通过访问 Forwarding 中的网址来连接我们构建在本地的网站。
然后付款后便能在自己本地网站的后台管理看到 paypal ipn 的信息,我这里显示的状态是 pending,按理来说应该是 completed ,可能 paypal 设置中需要更改,这样的话需要将 signal.py 中 ST_PP_COMPLETED 修改为 ST_PP_PENDING,这样 signal.py 便能正常处理 paypal 返回的信息,将订单状态更改为已完成。
至此,我们便完成了调用 paypal 实现第三方网站支付的功能。