在上一章中,你创建了一个包括商品目录和订单系统的在线商店。你还学习了如何用Celery启动异步任务。在这一章中,你会学习如何在网站中集成支付网关。你还会扩展管理站点,用于管理订单和导出不同格式的订单。 我们会在本章覆盖以下知识点:
8.1 集成支付网关支付网关允许你在线处理支付。你可以使用支付网关管理用户订单,以及通过可靠的,安全的第三方代理处理支付。这意味着你不用考虑在自己的系统中存储信用卡。 有很多支付网关可供选择。我们将集成PayPal,它是最流行的支付网关之一。 PayPal提供了几种方法在网站中集成它的网关。标准集成包括一个 8.1.1 创建PayPal账户你需要一个PayPal商家账户,才能在网站中集成支付网关。如果你还没有PayPal账户,在这里注册。确保你选择了商家账户。 在注册表单填写详细信息完成注册。PayPal会给你发送一封邮件确认账户。 8.1.2 安装django-paypal
在终端使用以下命令安装django-paypal: pip install django-paypal 编辑项目的 INSTALLED_APPS = [ # ... 'paypal.standard.ipn',] 这个应用是django-paypal提供的,通过 在 # django-paypal settingsPAYPAL_RECEIVER_EMAIL = 'mypaypalemail@myshop.com'PAYPAL_TEST = True 这些设置分别是:
打开终端执行以下命令,同步django-paypal的模型到数据库中: python manage.py migrate 你会看到类似这样结尾的输出: Running migrations: Applying ipn.0001_initial... OK Applying ipn.0002_paypalipn_mp_id... OK Applying ipn.0003_auto_20141117_1647... OK Applying ipn.0004_auto_20150612_1826... OK Applying ipn.0005_auto_20151217_0948... OK Applying ipn.0006_auto_20160108_1112... OK Applying ipn.0007_auto_20160219_1135... OK 现在django-paypal的模型已经同步到数据库中。你还需要添加django-paypal的URL模式到项目中。编辑 url(r'^paypal/', include('paypal.standard.ipn.urls')), 让我们把支付网关添加到结账过程中。 8.1.3 添加支付网关结账流程是这样的:
使用以下命令在项目中创建一个新应用: python manage.py startapp payment 我们将使用这个应用管理结账流程和用户支付。 编辑项目的 INSTALLED_APPS = [ # ... 'paypal.standard.ipn', 'payment',] 现在 from django.shortcuts import render, redirectfrom django.core.urlresolvers import reverse 找到 # launch asynchronous taskorder_created.delay(order.id)return render(request, 'orders/order/created.html', {'order': order}) 替换为下面的代码: # launch asynchronous taskorder_created.delay(order.id)request.session['order_id'] = order.idreturn redirect(reverse('payment:process')) 创建订单成功之后,我们用 编辑 from decimal import Decimalfrom django.conf import settingsfrom django.core.urlresolvers import reversefrom django.shortcuts import render, get_object_or_404from paypal.standard.forms import PayPalPaymentsFormfrom orders.models import Orderdef payment_process(request): order_id = request.session.get('order_id') order = get_object_or_404(Order, id=order_id) host = request.get_host() paypal_dict = { 'business': settings.PAYPAL_RECEIVER_EMAIL, 'amount': '%.2f' % order.get_total_cost().quantize(Decimal('.01')), 'item_name': 'Order {}'.format(order.id), 'invoice': str(order.id), 'currency_code': 'USD', 'notify_url': 'http://{}{}'.format(host, reverse('paypal-ipn')), 'return_url': 'http://{}{}'.format(host, reverse('payment:done')), 'cancel_return': 'http://{}{}'.format(host, reverse('payment:canceled')), } form = PayPalPaymentsForm(initial=paypal_dict) return render(request, 'payment/process.html', {'order': order, 'form': form}) 在
让我们创建一个简单的视图,当支付完成,或者因为某些原因取消支付,让PayPal重定向用户。在同一个 from django.views.decorators.csrf import csrf_exempt@csrf_exemptdef payment_done(request): return render(request, 'payment/done.html')@csrf_exemptdef payment_canceled(request): return render(request, 'payment/canceled.html') 因为PayPal可以通过POST重定向用户到这些视图的任何一个,所以我们用 from django.conf.urls import urlfrom . import viewsurlpatterns = [ url(r'^process/$', views.payment_process, name='process'), url(r'^done/$', views.payment_done, name='done'), url(r'^canceled/$', views.payment_canceled, name='canceled'),] 这些是支付流程的URL。我们包括了以下URL模式:
编辑 url(r'^payment/', include('payment.urls', namespace='payment')), 记住把它放在 在 templates/ payment/ process.html done.html canceled.html 编辑 {% extends 'shop/base.html' %}{% block title %}Pay using PayPal{% endblock title %}{% block content %} 这个模板用于渲染 编辑 {% extends 'shop/base.html' %}{% block content %} 用户支付成功后,会重定向到这个模板页面。 编辑 {% extends 'shop/base.html' %}{% block content %} 处理支付遇到问题,或者用户取消支付时,会重定向到这个模板页面。 让我们尝试完整的支付流程。 8.1.4 使用PayPal的Sandbox在浏览器中打开 ![]() 最初,你会看到一个商家账户和一个PayPal自动生成的个人测试账户。你可以点击 点击列表中 ![]() 在 当你的网站使用sandbox环境时,测试账户可以用来处理支付。导航到 在终端执行 ![]()
你可以看一眼HTML源码,查看生成的表单字段。 点击 ![]() 输入顾客测试账号的邮箱地址和密码,然后点击登录按钮。你会被重定向到以下页面: ![]()
现在点击 ![]() 点击 ![]() 支付成功!但是因为我们在本地运行项目,127.0.0.1不是一个公网IP,所以PayPal不能给我们的应用发送支付状态通知。我们接下来学习如何让我们的网站可以从Internet访问,从而接收IPN通知。 8.1.5 获得支付通知IPN是大部分支付网关都会提供的方法,用于实时跟踪购买。当网关处理完一个支付后,会立即给你的服务器发送一个通知。该通知包括所有支付细节,包括状态和用于确认通知来源的支付签名。这个通知作为独立的HTTP请求发送到你的服务器。出现问题的时候,PayPal会多次尝试发送通知。
我们将创建一个自定义接收函数,并把它连接到 在 from django.shortcuts import get_object_or_404from paypal.standard.models import ST_PP_COMPLETEDfrom paypal.standard.ipn.signals import valid_ipn_receivedfrom orders.models import Orderdef payment_notification(sender, **kwargs): ipn_obj = sender if ipn_obj.payment_status == ST_PP_COMPLETED: # payment was successful order = get_object_or_404(Order, id=ipn_obj.invoice) # mark the order as paid order.paid = True order.save()valid_ipn_received.connect(payment_notification) 我们把
当 8.1.6 配置我们的应用你已经在第六章学习了应用配置。我们将为 在 from django.apps import AppConfigclass PaymentConfig(AppConfig): name = 'payment' verbose_name = 'Payment' def ready(self): # improt signal handlers import payment.signals 在这段代码中,我们为 编辑 default_app_config = 'payment.apps.PaymentConfig' 这会让Django自动加载你的自定义应用配置类。你可以在这里阅读更多关于应用配置的信息。 8.1.7 测试支付通知因为我们在本地环境开发,所以我们需要让PayPal可以访问我们的网站。有几个应用程序可以让开发环境通过Internet访问。我们将使用Ngrok,是最流行的之一。 从这里下载你的操作系统版本的Ngrok,并使用以下命令运行: ./ngrok http 8000 这个命令告诉Ngrok在8000端口为你的本地主机创建一个链路,并为它分配一个Internet可访问的主机名。你可以看到类似这样的输入: Session Status onlineAccount lakerszhy (Plan: Free)Update update available (version 2.2.4, Ctrl-U to update)Version 2.1.18Region United States (us)Web Interface http://127.0.0.1:4040Forwarding http://c0f17d7c. -> localhost:8000Forwarding https://c0f17d7c. -> localhost:8000Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00 Ngrok告诉我们,我们网站使用的Django开发服务器在本机的8000端口运行,现在可以通过
完成支付处理后,在浏览器中打开
你也可以在这里使用PayPal的模拟器发送IPN。模拟器允许你指定通知的字段和类型。 除了 8.2 导出订单到CSV文件有时你可能希望把模型中的信息导出到文件中,然后把它导入到其它系统中。其中使用最广泛的格式是 8.2.1 在管理站点你添加自定义操作Django提供了大量自定义管理站点的选项。我们将修改对象列表视图,在其中包括一个自定义的管理操作。 一个管理操作是这样工作的:用户在管理站点的对象列表页面用复选框选择对象,然后选择一个在所有选中选项上执行的操作,最后执行操作。下图显示了操作位于管理站点的哪个位置: ![]()
你可以编写一个常规函数来创建自定义操作,该函数需要接收以下参数:
当在管理站点触发操作时,会执行这个函数。 我们将创建一个自定义管理操作,来下载一组订单的CSV文件。编辑 import csvimport datetimefrom django.http import HttpResponsedef export_to_csv(modeladmin, request, queryset): opts = modeladmin.model._meta response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment;filename={}.csv'.format(opts.verbose_name) writer = csv.writer(response) fields = [field for field in opts.get_fields() if not field.many_to_many and not field.one_to_many] # Write a first row with header information writer.writerow([field.verbose_name for field in fields]) # Write data rows for obj in queryset: data_row = [] for field in fields: value = getattr(obj, field.name) if isinstance(value, datetime.datetime): value = value.strftime('%d/%m/%Y') data_row.append(value) writer.writerow(data_row) return responseexport_to_csv.short_description = 'Export to CSV' 在这段代码中执行了以下任务:
我们创建了一个通用的管理操作,可以添加到所有 最后,如下添加 calss OrderAdmin(admin.ModelAdmin): # ... actions = [export_to_csv] 在浏览器中打开 ![]() 选中几条订单,然后在选择框中选择 ID,first name,last name,email,address,postal code,city,created,updated,paid1,allen,iverson,lakerszhy@gmail.com,北京市朝阳区,100012,北京市,11/05/2017,11/05/2017,False2,allen,kobe,lakerszhy@gmail.com,北京市朝阳区,100012,北京市,11/05/2017,11/05/2017,False 正如你所看到的,创建管理操作非常简单。 8.3 用自定义视图扩展管理站点有时,你可能希望通过配置 让我们创建一个自定义视图,显示订单的相关信息。编辑 from django.contrib.admin.views.decorators import staff_member_requiredfrom django.shortcuts import get_object_or_404from .models import Order@staff_member_requireddef admin_order_detail(request, order_id): order = get_object_or_404(Order, id=order_id) return render(request, 'admin/orders/order/detail.html', {'order': order})
现在编辑 url(r'^admin/order/(?P 在 admin/ orders/ order/ detail.html 编辑 {% extends 'admin/base_site.html' %}{% load static %}{% block extrastyle %} {% endblock extrastyle %}{% block title %} Order {{ order.id }} {{ block.super }}{% endblock title %}{% block breadcrumbs %} {% endblock breadcrumbs %}{% block content %} 这个模板用于在管理站点显示订单详情。模板扩展自Django管理站点的 为了使用静态文件,我们可以从本章的示例代码中获得它们。拷贝 我们使用父模板中定义的块引入自己的内容。我们显示订单信息和购买的商品。 当你想要扩展一个管理模板时,你需要了解它的结构,并确定它存在哪些块。你可以在这里查看所有管理模板。 如果需要,你也可以覆盖一个管理模板。把要覆盖的模板拷贝到 最后,让我们为管理站点的列表显示页中每个 from django.core.urlresolvers import reversedef order_detail(obj): return 'View'.format(reverse('orders:admin_order_detail', args=[obj.id]))order_detail.allow_tags = True 这个函数接收一个
然后编辑 class OrderAdmin(admin.ModelAdmin): list_display = [... order_detail] 在浏览器中打开 ![]() 点击任何一个订单的 ![]() 8.4 动态生成PDF单据我们现在已经有了完成的结账和支付系统,可以为每个订单生成PDF单据了。有几个Python库可以生成PDF文件。一个流行的生成PDF文件的Python库是Reportlab。你可以在这里查看如果使用Reportlab输出PDF文件。 大部分情况下,你必须在PDF文件中添加自定义样式和格式。你会发现,让Python远离表现层,渲染一个HTML模板,然后把它转换为PDF文件更加方便。我们将采用这种方法,在Django中用模块生成PDF文件。我们会使用WeasyPrint,它是一个Python库,可以从HTML模板生成PDF文件。 8.4.1 安装WeasyPrint首先,为你的操作系统安装WeasyPrint的依赖,请访问这里。 然后用以下命令安装WeasyPrint: pip install WeasyPrint 8.4.2 创建PDF模板我们需要一个HTML文档作为WeasyPrint的输入。我们将创建一个HTML模板,用Django渲染它,然后把它传递给WeasyPrint生成PDF文件。 在 这是PDF单据的模板。在这个模板中,我们显示所有订单详情和一个包括商品的HTML的 8.4.3 渲染PDF文件我们将创建一个视图,在管理站点中生成已存在订单的PDF单据。编辑 from django.conf import settingsfrom django.http import HttpResponsefrom django.template.loader import render_to_stringimport weasyprint@staff_member_requireddef admin_order_pdf(request, order_id): order = get_object_or_404(Order, id=order_id) html = render_to_string('orders/order/pdf.html', {'order': order}) response = HttpResponse(content_type='application/pdf') response['Content-Disposition'] = 'filename='order_{}.pdf''.format(order.id) weasyprint.HTML(string=html).write_pdf(response, stylesheets=[weasyprint.CSS(settings.STATIC_ROOT + 'css/pdf.css')]) return response 这个视图用于生成订单的PDF单据。我们用 因为我们需要使用 STATIC_ROOT = os.path.join(BASE_DIR, 'static/') 接着执行 You have requested to collect static files at the destinationlocation as specified in your settings: /Users/lakerszhy/Documents/GitHub/Django-By-Example/code/Chapter 8/myshop/staticThis will overwrite existing files!Are you sure you want to do this? 输入
编辑 url(r'admin/order/(?P 现在,我们可以编辑管理列表显示页面,为 def order_pdf(obj): return 'PDF'.format(reverse('orders:admin_order_pdf', args=[obj.id]))order_pdf.allow_tags = Trueorder_pdf.short_description = 'PDF bill' 把 class OrderAdmin(admin.ModelAdmin): list_display = [..., order_detail, order_pdf] 如果你为可调用对象指定了 在浏览器中打开 ![]() 点击任意一条订单的PDF链接。你会看到生成的PDF文件,下图是未支付的订单: ![]() 已支付订单如下图所示: ![]() 8.4.4 通过邮件发送PDF文件当收到支付时,让我们给顾客发送一封包括PDF单据的邮件。编辑 from django.template.loader import render_to_stringfrom django.core.mail import EmailMessagefrom django.conf import settingsimport weasyprintfrom io import BytesIO 然后在 # create invoice e-mailsubject = 'My Shop - Invoice no. {}'.format(order.id)message = 'Please, find attached the invoice for your recent purchase.'email = EmailMessage(subject, message, 'admin@myshop.com', [order.email])# generate PDFhtml = render_to_string('orders/order/pdf.html', {'order': order})out = BytesIO()stylesheets = [weasyprint.CSS(settings.STATIC_ROOT + 'css/pdf.css')]weasyprint.HTML(string=html).write_pdf(out, stylesheets=stylesheets)# attach PDF fileemail.attach('order_{}.pdf'.format(order.id), out.getvalue(), 'application/pdf')# send e-mailemail.send() 在这个信号中,我们用Django提供的 记得在项目 现在打开Ngrok提供的应用URL,完成一笔新的支付,就能在邮件中收到PDF单据了。 8.5 总结在这一章中,你在项目中集成了支付网关。你自定义了Django管理站点,并学习了如果动态生成CSV和PDF文件。 下一章会深入了解Django项目的国际化和本地化。你还会创建一个优惠券系统和商品推荐引擎。 |
|