7 建構線上商店
在上一章中,你建立了關注系統和使用者活動流。你還學習了Django信号是如何工作的,并在項目中內建了Redis,用于計算圖檔的浏覽次數。在這一章中,你會學習如何建構一個基本的線上商店。你會建立商品目錄(catalog),并用Django會話(session)實作購物車。你還會學習如果建立自定義上下文管理器,以及用Celery啟動異步任務。
在這一章中,你會學習:
- 建立商品目錄
- 使用Django會話建立購物車
- 管理客戶訂單
- 用Celery給客戶發送異步通知
7.1 建立線上商店項目
我們将建立一個新的Django項目來建構線上商店。使用者可以通過商品目錄浏覽,并把商品添加到購物車中。最後,客戶結賬并下單。本章将會覆寫線上商店以下幾個功能:
- 建立商品目錄模型,把它們添加到管理站點,并建立一個基礎視圖,用于顯示目錄
- 使用Django會話建構購物車系統,允許使用者浏覽網站時保留標明的商品
- 建立用于下單的表單和功能
- 使用者下單後,發送一封異步确認郵件給使用者
首先,我們為新項目建立虛機環境,并用以下指令激活虛拟環境:
mkdiv env
virtualenv env/myshop
source env/myshop/bin/activate
使用以下指令在虛拟環境中安裝Django:
pip install Django
打開終端,執行以下指令,建立
myshop
項目,以及
shop
應用:
django-admin startproject myshop
cd myshop
django-admin startapp shop
編輯項目的
settings.py
檔案,在
INSTALLED_APPS
設定中添加
shop
應用:
INSTALLED_APPS = (
# ...
'shop',
)
現在項目中的應用已經激活。讓我們為商品目錄定義模型。
7.1.1 建立商品目錄模型
商店的目錄由屬于不同類别的商品組成。每個商品有名字,可選的描述,可選的圖檔,價格,以及有效的庫存。編輯你剛建立的
shop
應用的
models.py
檔案,添加以下代碼:
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=, db_index=True)
slug = models.SlugField(max_length=, db_index=True, unique=True)
class Meta:
ordering = ('name', )
verbose_name = 'category'
verbose_name_plural = 'categories'
def __str__(self):
return self.name
class Product(models.Model):
category = models.ForeignKey(Category, related_name='products')
name = models.CharField(max_length=, db_index=True)
slug = models.SlugField(max_length=, db_index=True)
image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=, decimal_places=)
stock = models.PositiveIntegerField()
available = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ('name', )
index_together = (('id', 'slug'), )
def __str__(self):
return self.name
這是我們的
Category
和
Product
模型。
Category
模型由
name
字段和唯一的
slug
字段組成。
Product
模型包括以下字段:
-
:這是指向category
模型的Catetory
。這是一個多對一的關系:一個商品屬于一個目錄,而一個目錄包括多個商品。ForeignKey
-
:這是商品的名稱。name
-
:這是商品的别名,用于建構友好的URL。slug
-
:這是一張可選的商品圖檔。image
-
:這是商品的可選描述。description
-
:這是price
。這個字段用Python的DecimalField
類型存儲固定精度的十進制數。使用decimal.Decimal
屬性設定最大的位數(包括小數位),使用max_digits
屬性設定小數位。decimal_places
-
:這個stock
存儲商品的庫存。PositiveIntegerField
-
:這個布爾值表示商品是否有效。這允許我們在目錄中啟用或禁用商品。available
-
:對象建立時存儲該字段。created
-
:對象最後更新時存儲該字段。updated
對于
price
字段,我們使用
DecimalField
代替
FloatField
,來避免四舍五入的問題。
總是使用存儲貨币值。在Python内部,
DecimalField
使用
FloatField
類型,而
float
使用
DecimalField
類型。使用
Decimal
類型可以避免
Decimal
的四舍五入問題。
float
在
Product
模型的
Meta
類中,我們用
index_together
元選項為
id
和
slug
字段指定共同索引。這是因為我們計劃通過
id
和
slug
來查詢商品。兩個字段共同索引可以提升用這兩個字段查詢的性能。
因為我們要在模型中處理圖檔,打開終端,用以下指令安裝
Pillow
:
pip install Pillow
現在,執行以下指令,建立項目的初始資料庫遷移:
python manage.py makemigrations
你會看到以下輸出:
Migrations for 'shop':
shop/migrations/0001_initial.py
- Create model Category
- Create model Product
- Alter index_together for product ( constraint(s))
執行以下指令同步資料:
python manage.py migrate
你會看到包括這一行的輸出:
Applying shop_initial... OK
現在資料庫與模型已經同步了。
7.1.2 在管理站點注冊目錄模型
讓我們把模型添加到管理站點,進而可以友善的管理目錄和商品。編輯
shop
應用的
admin.py
檔案,添加以下代碼:
from django.contrib import admin
from .models import Category, Product
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
prepopulated_fields = {'slug': ('name', )}
admin.site.register(Category, CategoryAdmin)
class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'price', 'stock', 'available', 'created', 'updated')
list_filter = ('available', 'created', 'updated')
list_editable = ('price', 'stock', 'available')
prepopulated_fields = {'slug': ('name', )}
admin.site.register(Product, ProductAdmin)
記住,我們使用
prepopulated_fields
屬性指定用其它字段的值自動填充的字段。正如你前面看到的,這樣可以很容易的生成别名。我們在
ProductAdmin
類中使用
list_editable
屬性設定可以在管理站點的清單顯示頁面編輯的字段。這樣可以一次編輯多行。
list_editable
屬性中的所有字段都必須列在
list_display
屬性中,因為隻有顯示的字段才可以編輯。
現在使用以下指令為網站建立超級使用者:
python manage.py createsuperuser
執行
python manage.py runserver
指令啟動開伺服器。在浏覽器中打開
http://127.0.0.1:8000/admin/shop/product/add/
,然後用剛建立的使用者登入。使用管理界面添加一個新的目錄和商品。管理頁面的商品修改清單頁面看起來是這樣的:
7.1.3 建構目錄視圖
為了顯示商品目錄,我們需要建立一個視圖列出所有商品,或者通過制定的目錄過濾商品。編輯
shop
應用的
views.py
檔案,添加以下代碼:
from django.shortcuts import render, get_object_or_404
from .models import Category, Product
def product_list(request, category_slug=None):
category = None
categories = Category.objects.all()
products = Product.objects.filter(available=True)
if category_slug:
category = get_object_or_404(Category, slug=category_slug)
products = products.filter(category=category)
return render(request,
'shop/product/list.html',
{'category': category,
'categories': categories,
'products': products})
我們用
available=True
過濾
QuerySet
,隻檢索有效地商品。我們用可選的
category_slug
參數,過濾指定目錄的商品。
我們還需要一個查詢和顯示單個商品的視圖。添加以下代碼到
views.py
檔案中:
def product_detail(request, id, slug):
product = get_object_or_404(Product, id=id, slug=slug, available=True)
return render(request,
'shop/product/detail.html',
{'product': product})
product_detail
視圖接收
id
和
slug
參數來查詢
Product
執行個體。我們可以隻使用ID獲得該執行個體,因為ID是唯一性的屬性。但是我們會在URL中包括别名,為商品建構搜尋引擎友好的URL。
建立商品清單和詳情視圖後,我們需要為它們定義URL模式。在
shop
應用目錄中建立
urls.py
檔案,添加以下代碼:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.product_list, name='product_list'),
url(r'^(?P<category_slug>[-\w]+)/$', views.product_list, name='product_list_by_category'),
url(r'^(?P<id>\d+)/(?P<slug>[-\w]+)/$', views.product_detail, name='product_detail'),
]
這些是商品目錄的URL模式。我們為
product_list
視圖定義了兩個不同的URL模式:
product_list
模式不帶任何參數調用
product_list
視圖;
product_list_by_category
模式給視圖提供
category_slug
參數,用于過濾指定目錄的商品。我們添加了
product_detail
模式,傳遞
id
和
slug
參數給視圖,用于檢索特定商品。
編輯
myshop
項目的
urls.py
檔案,如下所示:
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^', include('shop.urls', namespace='shop')),
]
我們在項目的主URL模式中引入了
shop
應用的URL,并指定命名空間為
shop
。
現在編輯
shop
應用的
models.py
檔案,導入
reverse()
函數,并為
Category
和
Product
模型添加
get_absolute_url()
方法,如下所示:
from django.core.urlresolvers import reverse
# ...
class Category(models.Model):
# ...
def get_absolute_url(self):
return reverse('shop:product_list_by_category', args=[self.slug])
class Product(models.Model):
# ...
def get_absolute_url(self):
return reverse('shop:product_detail', args=[self.id, self.slug])
你已經知道,
get_absolute_url()
是檢索指定對象URL的約定成俗的方法。我們在這裡使用之前在
urls.py
檔案中定義的URL模式。
7.1.4 建立目錄模闆
現在我們需要為商品清單和詳情視圖建立模闆。在
shop
應用目錄中建立以下目錄和檔案結構:
templates/
shop/
base.html
product/
list.html
detail.html
我們需要定義一個基礎模闆,并在商品清單和詳情模闆中繼承它。編輯
shop/base.html
模闆,添加以下代碼:
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{% block title %}My shop{% endblock %}</title>
<link href="{% static "css/base.css" %}" rel="stylesheet">
</head>
<body>
<div id="header">
<a href="/" class="logo">My shop</a>
</div>
<div id="subheader">
<div class="cart">
Your cart is empty.
</div>
</div>
<div id="content">
{% block content %}
{% endblock %}
</div>
</body>
</html>
這是商店的基礎模闆。為了引入模闆使用的CSS樣式表和圖檔,你需要拷貝本章執行個體中的靜态檔案,它們位于
shop
應用的
static/
目錄。把它們拷貝到你的項目中的相同位置。
編輯
shop/product/list.html
模闆,添加以下代碼:
{% extends "shop/base.html" %}
{% load static %}
{% block title %}
{% if category %}{{ category.name }}{% else %}Products{% endif %}
{% endblock title %}
{% block content %}
<div id="sidebar">
<h3>Categories</h3>
<ul>
<li {% if not category %}class="selected"{% endif %}>
<a href="{% url "shop:product_list" %}">All</a>
</li>
{% for c in categories %}
<li {% if category.slug == c.slug %}class="selected"{% endif %}>
<a href="{{ c.get_absolute_url }}">{{ c.name }}</a>
</li>
{% endfor %}
</ul>
</div>
<div id="main" class="product-list">
<h1>{% if catetory %}{{ category.name }}{% else %}Products{% endif %}</h1>
{% for product in products %}
<div class="item">
<a href="{{ product.get_absolute_url }}">
![]({% if product.image %}{{ product.image.url }}{% else %}{% static )
</a>
<a href="{{ product.get_absolute_url }}" target="_blank" rel="external nofollow" >{{ product.name }}</a><br/>
${{ product.price }}
</div>
{% endfor %}
</div>
{% endblock content %}
這是商品清單目錄。它繼承自
shop/base.html
目錄,用
categories
上下文變量在側邊欄顯示所有目錄,用
products
顯示目前頁商品。用同一個模闆列出所有有效商品和通過目錄過濾的所有商品。因為
Product
模型的
image
字段可以為空,是以如果商品沒有圖檔時,我們需要提供一張預設圖檔。圖檔位于靜态檔案目錄,相對路徑為
img/no_image.png
。
因為我們用
ImageField
存儲商品圖檔,是以需要開發伺服器管理上傳的圖檔檔案。編輯
myshop
的
settings.py
檔案,添加以下設定:
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_URL
是管理使用者上傳的多媒體檔案的基礎URL。
MEDIA_ROOT
是這些檔案的本地路徑,我們在前面添加
BASE_DIR
變量,動态生成該路徑。
要讓Django管理通過開發伺服器上傳的多媒體檔案,需要編輯
myshop
項目的
urls.py
檔案,如下所示:
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
記住,我們隻在開發階段這麼做。在生産環境,你不應該用Django管理靜态檔案。
使用管理站點添加一些商品,然後在浏覽器中打開
http://127.0.0.1:8000/
。你會看到商品清單頁面,如下圖所示:
如果你用管理站點建立了一個商品,但是沒有上傳圖檔,則會顯示預設圖檔:
讓我們編輯商品詳情模闆。編輯
shop/product/detail.html
模闆,添加以下代碼:
{% extends "shop/base.html" %}
{% load static %}
{% block titie %}
{% if category %}{{ category.title }}{% else %}Products{% endif %}
{% endblock titie %}
{% block content %}
<div class="product-detail">
![]({% if product.image %}{{ product.image.url }}{% else %} {% static )
<h1>{{ product.name }}</h1>
<h2><a href="{{ product.category.get_absolute_url }}">{{ product.category }}</a></h2>
<p class="price">${{ product.price }}</p>
{{ product.description|linebreaks }}
</div>
{% endblock content %}
我們在關聯的目錄對象上調用
get_absolute_url()
方法,來顯示屬于同一個目錄的有效商品。現在在浏覽器中打開
http://127.0.0.1/8000/
,點選某個商品檢視詳情頁面,如下圖所示:
我們現在已經建立了一個基本的商品目錄。
7.2 建構購物車
建立商品目錄之後,下一步是建立購物車,讓使用者選擇他們希望購買的商品。當使用者浏覽網站時,購物車允許使用者選擇并暫時存儲他們想要的商品,直到最後下單。購物車存儲在會話中,是以在使用者通路期間可以儲存購物車裡的商品。
我們将使用Django的會話架構儲存購物車。購物車會一直儲存在會話中,直到完成購物或者使用者結賬離開。我們還需要為購物車和它的商品建立額外的Django模型。
7.2.1 使用Django會話
Django提供了一個會話架構,支援匿名和使用者會話。會話架構允許你為每個通路者存儲任意資料。會話資料儲存在服務端,cookies包括會話ID,除非你使用基于cookie的會話引擎。會話中間件負責發送和接收cookies。預設的會話引擎在資料庫中存儲會話資料,但是接下來你會看到,可以選擇不同的會話引擎。要使用會話,你必須確定項目的
MIDDLEWARE_CLASSES
設定中包括
django.contrib.sessions.middleware.SessionMiddleware
。這個中間件負責管理會話,當你用
startproject
指令建立新項目時,會預設添加這個中間件。
會話中間件讓目前會話在
request
對象中生效。你可以使用
request.session
通路目前會話,與使用Python字典類似的存儲和檢索會話資料。會話字典預設接收所有可以序列化為JSON的Python對象。你可以這樣在會話中設定變量:
查詢一個會話的鍵:
request.session.get('foo')
删除存儲在會話中的鍵:
del request.session['foo']
正如你所看到的,我們把
request.session
當做标準的Python字典。
當使用者登入到網站時,他們的匿名會話丢失,并未認證使用者建立新的會話。如果你在匿名會話中存儲了資料,并想在使用者登入後保留,你需要舊的會話資料拷貝到新的會話中。
7.2.2 會話設定
你可以使用幾種設定為項目配置會話。其中最重要的是
SESSION_ENGINE
。該設定允許你設定會話存儲的位置。預設情況下,Django使用
django.contrib.sessions
應用的
Session
模型,把會話存儲在資料庫中。
Django為存儲會話資料提供了以下選項:
-
:會話資料存儲在資料庫中。這是預設的會話引擎。Database sessions
-
:會話資料存儲在檔案系統中。File-based sessions
-
:會話資料存儲在緩存背景。你可以使用Cached sessions
設定指定婚車背景。在緩存系統中存儲會話資料的性能最好。CACHES
-
:會話資料存儲在連續寫入的緩存(write-through cache)和資料庫中。隻有在緩存中沒有資料時才讀取資料庫。Cached database sessions
-
:會話資料存儲于發送到浏覽器的cookies。Cookie-based sessions
使用 cache-based
會話引擎有更好的性能。Django支援Memcached,以及其它支援Redis的第三方緩存背景和緩存系統。
你可以隻是用其它設定自定義會話。以下是一些重要的會話相關設定:
-
:這是會話cookies的持續時間(機關是秒)。預設值是1209600(兩周)。SESSION_COOKIE_AGE
-
:會話cookies使用的域。設定為SESSION_COOKIE_DOMAIN
可以啟用跨域cookies。.mydomain.com
-
:當浏覽器關閉後,表示會話是否過期的一個布爾值。SESSION_EXPIRE_AT_BROWSER_CLOSE
-
:如果這個布爾值為SESSION_SAVE_EVERY_REQUEST
,則會在每次請求時把會話儲存到資料庫中。會話的過期時間也會每次更新。True
你可以在這裡檢視所有會話設定。
7.2.3 會話過期
你可以使用
SESSTION_EXPIRE_AT_BROWSER_CLOSE
設定選擇
browser-length
會話或者持久會話。預設值為
False
,強制把會話的有效期設定為
SESSION_COOKIE_AGE
的值。如果設定
SESSTION_EXPIRE_AT_BROWSER_CLOSE
為
True
,當使用者關閉浏覽器後,會話會過期,而
SESSION_COOKIE_AGE
不會起任何作用。
你可以使用
request.session
的
set_expiry()
方法覆寫目前會話的有效期。
7.2.4 在會話中存儲購物車
我們需要建立一個簡單的可以序列号為JSON的結構體,在會話中存儲購物車商品。購物車的每一件商品必須包括以下資料:
-
執行個體的Product
id
- 選擇該商品的數量
- 該商品的單價
因為商品價格可能變化,是以當商品添加到購物車時,我們把商品的價格和商品本身同僚存入購物車。這樣的話,即使之後商品的價格發生變化,使用者看到的還是添加到購物車時的價格。
現在你需要建立購物車,并與會話關聯起來。購物車必須這樣工作:
- 需要購物車時,我們檢查是否設定了自定義會話鍵。如果會話中沒有設定購物車,則建立一個新的購物車,并儲存在購物車會話鍵中。
- 對于連續的請求,我們執行相同的檢查,并從購物車會話鍵中取出購物車的商品。我們從會話中檢索購物車商品,并從資料庫中檢索它們關聯的
對象。Product
編輯項目
settings.py
檔案,添加以下設定:
我們在使用者會話用這個鍵存儲購物車。因為每個訪客的Django會話是獨立的,是以我們可以為所有會話使用同一個購物車會話鍵。
讓我們建立一個管理購物車的應用。打開終端,執行以下指令建立一個新應用:
python manage.py startapp cart
然後編輯項目的
settings.py
檔案,把
cart
添加到
INSTALLED_APPS
:
INSTALLED_APPS = (
# ...
'cart',
)
在
cart
應用目錄中建立
cart.py
檔案,并添加以下代碼:
from decimal import Decimal
from django.conf import settings
from shop.models import Product
class Cart:
def __init__(self, request):
self.session = request.session
cart = self.session.get(settings.CART_SESSION_ID)
if not cart:
# save an empty cart in the session
cart = self.session[settings.CART_SESSION_ID] = {}
self.cart = cart
這個
Cart
類用于管理購物車。我們要求用
request
對象初始化購物車。我們用
self.session = request.session
存儲目前會話,以便在
Cart
類的其它方法中可以通路。首先,我們用
self.session.get(settings.CART_SESSION_ID)
嘗試從目前會話中獲得購物車。如果目前會話中沒有購物車,通過在會話中設定一個空字典來設定一個空的購物車。我們希望購物車字典用商品ID做為鍵,一個帶數量和價格的字典作為值。這樣可以保證一個商品不會在購物車中添加多次;同時還可以簡化通路購物車的資料。
讓我們建立一個方法,用于向購物車中添加商品,或者更新商品數量。在
Cart
類中添加
add()
和
save()
方法:
def add(self, product, quantity=, update_quantity=False):
product_id = str(product.id)
if product_id not in self.cart:
self.cart[product_id] = {
'quantity': ,
'price': str(product.price)
}
if update_quantity:
self.cart[product_id]['quantity'] = quantity
else:
self.cart[product_id]['quantity'] += quantity
self.save()
def save(self):
# update the session cart
self.session[settings.CART_SESSION_ID] = self.cart
# mark the sessions as "modified" to make sure it is saved
self.session.modified = True
add()
方法接收以下參數:
-
:在購物車中添加或更新的product
執行個體。Product
-
:可選的商品數量。預設為1.quantity
-
:一個布爾值,表示使用給定的數量更新數量(update_quantity
),或者把新數量加到已有的數量上(True
)。False
我們用商品
id
作為購物車内容字典的鍵。因為Django使用JSON序列号會話資料,而JSON隻允許字元串類型的鍵名,是以我們把商品
id
轉換為字元串。商品
id
是鍵,儲存的值是帶商品
quantity
和
price
的字典。為了序列号,我們把商品價格轉換為字元串。最後,我們調用
save()
方法在會話中儲存購物車。
save()
方法在會話中儲存購物車的所有修改,并使用
session.modified = True
标記會話已修改。這告訴Django,會話已經修改,需要儲存。
我們還需要一個方法從購物車中移除商品。在
Cart
類中添加以下方法:
def remove(self, product):
product_id = str(product.id)
if product_id in self.cart:
del self.cart[product_id]
self.save()
remove()
方法從購物車字典中移除指定商品,并調用
save()
方法更新會話中的購物車。
我們将需要疊代購物車中的商品,并通路關聯的
Product
執行個體。因為需要在類中定義
__iter__()
方法。在
Cart
類中添加以下方法:
def __iter__(self):
product_ids = self.cart.keys()
# get the product objects and add them to the cart
products = Product.objects.filter(id__in=product_ids)
for product in products:
self.cart[str(product.id)]['product'] = product
for item in self.cart.values():
item['price'] = Decimal(item['price'])
item['total_price'] = item['price'] * item['quantity']
yield item
在
__iter__()
方法中,我們檢索購物車中的
Product
執行個體,并把它們包括在購物車商品中。最後,我們疊代購物車商品,把
price
轉換回
Decimal
類型,并為每一項添加
total_price
屬性。現在我們可以在購物車中友善的疊代商品。
我們還需要傳回購物車中商品總數量。當在一個對象上調用
len()
函數時,Python會調用
__len__()
方法傳回對象的長度。我們定義一個
__len__()
方法,傳回購物車中所有商品的總數量。在
Cart
類中添加
__len__()
方法:
def __len__(self):
return sum(item['quantity'] for item in self.cart.values())
我們傳回購物車中所有商品數量的總和。
添加以下方法,計算購物車中所有商品的總價:
def get_total_price(self):
return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())
最後,添加一個清空購物車會話的方法:
def clear(self):
del self.session[settings.CART_SESSION_ID]
self.session.modified = True
我們的
Cart
類已經可以管理購物車了。
7.2.5 建立購物車視圖
現在我們已經建立了
Cart
類來管理購物車,我們需要建立添加,更新和移除購物車商品的視圖。我們需要建立以下視圖:
- 一個添加或更新購物車商品的視圖,可以處理目前和新的數量
- 一個從購物車中移除商品的視圖
- 一個顯示購物車商品和總數的視圖
7.2.5.1 添加商品到購物車
要添加商品到購物車中,我們需要一個使用者可以選擇數量的表單。在
cart
應用目錄中建立
forms.py
檔案,并添加以下代碼:
from django import forms
PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(, )]
class CartAddProductForm(forms.Form):
quantity = forms.TypedChoiceField(choices=PRODUCT_QUANTITY_CHOICES, coerce=int)
update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)
我們用這個表單向購物車中添加商品。
CartAddProductForm
類包括以下兩個字段:
-
:允許使用者選擇1-20之間的數量。我們使用帶quantity
的coerce=int
字段把輸入的值轉換為整數。TypedChoiceField
-
:允許你指定把數量累加到購物車中已存在的商品數量上(update
),還是用給定的數量更新已存在商品數量(False
)。我們為該字段使用True
元件,因為我們不想讓使用者看見它。HiddenInput
讓我們建立向購物車添加商品的視圖。編輯
cart
應用的
views.py
檔案,并添加以下代碼:
from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_POST
from shop.models import Product
from .cart import Cart
from .forms import CartAddProductForm
@require_POST
def cart_add(request, product_id):
cart = Cart(request)
product = get_object_or_404(Product, id=product_id)
form = CartAddProductForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
cart.add(product=product, quantity=cd['quantity'], update_quantity=cd['update'])
return redirect('cart:cart_detail')
這個視圖用于向購物車中添加商品或者更新已有商品的數量。因為這個視圖會修改資料,是以我們隻允許POST請求。視圖接收商品ID作為參數。我們用給定的商品ID檢索
Product
執行個體,并驗證
CartAddProductForm
。如果表單有效,則添加或更新購物車中的商品。該視圖重定向到
cart_detail
URL,它會顯示購物車中的内容。之後我們會建立
cart_detail
視圖。
我們還需要一個從購物車中移除商品的視圖。在
cart
應用的
views.py
檔案中添加以下代碼:
def cart_remove(request, product_id):
cart = Cart(request)
product = get_object_or_404(Product, id=product.id)
cart.remove(product)
return redirect('cart:cart_detail')
cart_remove
視圖接收商品ID作為參數。我們用給定的商品ID檢索
Product
執行個體,并從購物車中移除該商品。接着我們重定向到
cart_detail
URL。
最後,我們需要一個顯示購物車和其中的商品的視圖。在
views.py
檔案中添加以下代碼:
def cart_detail(request):
cart = Cart(request)
return render(request, 'cart/detail.html', {'cart': cart})
cart_detail
視圖獲得目前購物車,并顯示它。
我們已經建立了以下視圖:向購物車中添加商品,更新數量,從購物車中移除商品,已經顯示購物車。讓我們為這些視圖添加URL。在
cart
應用目錄中建立
urls.py
檔案,并添加以下URL模式:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.cart_detail, name='cart_detail'),
url(r'^add/(?P<product_id>\d+)/$', views.cart_add, name='cart_add'),
url(r'^remove/(?P<product_id>\d+)/$', views.cart_remove, name='cart_remove'),
]
最後,編輯
myshop
項目的主
urls.py
檔案,引入
cart
的URL模式:
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^cart/', include('cart.urls', namespace='cart')),
url(r'^', include('shop.urls', namespace='shop')),
]
確定在
shop.urls
模式之前引入這個URL模式,因為它比前者更有限定性。
7.2.5.2 建構顯示購物車的模闆
cart_add
和
cart_remove
視圖不需要渲染任何模闆,但是我們需要為
cart_detail
視圖建立顯示購物車和總數量的模闆。
在
cart
應用目錄中建立以下檔案結構:
templates/
cart/
detail.html
編輯
cart/detail.html
目錄,并添加以下代碼:
{% extends "shop/base.html" %}
{% load static %}
{% block title %}
Your shopping cart
{% endblock title %}
{% block content %}
<h1>Your shopping cart</h1>
<table class="cart">
<thead>
<tr>
<th>Image</th>
<th>Product</th>
<th>Quantity</th>
<th>Remove</th>
<th>Unit price</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{% for item in cart %}
{% with product=item.product %}
<tr>
<td>
<a href="{{ prouct.get_absolute_url }}">
![]({% if product.image %}{{ product.image.url}}{% else %}{% static )
</a>
</td>
<td>{{ product.name }}</td>
<td>{{ item.quantity }}</td>
<td><a href="{% url "cart:cart_remove" product.id %}">Remove</a></td>
<td class="num">${{ item.price }}</td>
<td class="num">${{ item.total_price }}</td>
</tr>
{% endwith %}
{% endfor %}
<tr class="total">
<td>Total</td>
<td colspan="4"></td>
<td class="num">${{ cart.get_total_price }}</td>
</tr>
</tbody>
</table>
<p class="text-right">
<a href="{% url "shop:product_list" %}" class="button light">Continue shopping</a>
<a href="#" class="button">Checkout</a>
</p>
{% endblock content %}
這個模闆用于顯示購物車的内容。它包括一個目前購物車中商品的表格。使用者通過送出表單到
cart_add
視圖,來修改選中商品的數量。我們為每個商品提供了
Remove
連結,使用者可以從購物車移除商品。
7.2.5.3 添加商品到購物車
現在我們需要在商品詳情頁面添加
Add to cart
按鈕。編輯
shop
應用的
views.py
檔案,修改
product_detail
視圖,如下所示:
from cart.forms import CartAddProductForm
def product_detail(request, id, slug):
product = get_object_or_404(Product, id=id, slug=slug, available=True)
cart_product_form = CartAddProductForm()
return render(request,
'shop/product/detail.html',
{'product': product,
'cart_product_form': cart_product_form})
編輯
shop
應用的
shop/product/detail.html
模闆,在商品價格之後添加表單,如下所示:
<p class="price">${{ product.price }}</p>
<form action="{% url "cart:cart_add" product.id %}" method="post">
{{ cart_product_form }}
{% csrf_token %}
<input type="submit" value="Add to cart">
</form>
使用
python manage.py runserver
指令啟動開發伺服器。在浏覽器中打開
127.0.0.1/8000/
,然後導航到商品詳情頁面。它現在包括一個選擇數量的表單,如下圖所示:
選擇數量,然後點選
Add to cart
按鈕。表單通過POST送出到
cart_add
視圖。該視圖把商品添加到會話中的購物車,包括目前價格和選擇的數量。然後重定義到購物車詳情頁面,如下圖所示:
7.2.5.4 在購物車中更新商品數量
當使用者檢視購物車時,他們可能希望在下單前修改商品數量。我們接下來實作在購物車詳情頁面修改數量。
編輯
cart
應用的
views.py
檔案,如下修改
cart_detail
視圖:
def cart_detail(request):
cart = Cart(request)
for item in cart:
item['update_quantity_form'] = CartAddProductForm(
initial={'quantity': item['quantity'], 'update': True})
return render(request, 'cart/detail.html', {'cart': cart})
我們為購物車中的每個商品建立了一個
CartAddProductForm
執行個體,允許使用者修改商品數量。我們用目前商品數量初始化表單,并設定
update
字段為
True
。是以,當我們把表單送出到
cart_add
視圖時,會用新數量了代替目前數量。
現在編輯
cart
應用的
cart/detail.html
模闆,找到這一行代碼:
把這行代碼替換為:
<td>
<form action="{% url "cart:cart_add" product.id %}" method="post">
{{ item.update_quantity_form.quantity }}
{{ item.update_quantity_form.update }}
<input type="submit" value="Update">
{% csrf_token %}
</form>
</td>
在浏覽器中打開
http://127.0.0.1:8000/cart/
。你會看到購物車中每個商品都有一個修改數量的表單,如下圖所示:
修改商品數量,然後點選
Update
按鈕,測試一下新功能。
7.2.6 為目前購物車建立上下文處理器
你可能已經注意到了,我們的網站頭部還是顯示
Your cart is emtpy
。當我們開始向購物車中添加商品,我們将看到它替換成購物車中商品的總數量和總價錢。因為這是需要在所有頁面顯示,是以我們将建立一個上下文處理器(context processor),将目前購物車包含在請求上下文中,而不管已經處理的視圖。
7.2.6.1 上下文處理器
上下文處理器是一個Python函數,它将
request
對象作為參數,并傳回一個添加到請求上下文中的字典。當你需要讓某些東西在所有模闆都可用時,它會派上用場。
預設情況下,當你使用
startproject
指令建立新項目時,項目中會包括以下模闆上下文處理器,它們位于
TEMPLATES
設定的
context_processors
選項中:
-
:在上下文中設定django.template.context_processors.debug
布爾值和debug
變量,表示請求中執行的SQL查詢清單sql_queries
-
:在上下文中設定django.template.context_processors.request
變量request
-
:在請求中設定django.contrib.auth.context_processors.auth
變量user
-
:在上下文中設定django.contrib.messages.context_processors.messages
變量,其中包括所有已經用消息架構發送的消息。message
Django還啟用了
django.template.context_processors.csrf
來避免跨站點請求僞造攻擊。這個上下文處理器不在設定中,但它總是啟用的,并且為了安全不能關閉。
你可以在這裡檢視所有内置的上下文處理器清單。
7.2.6.2 在請求上下文中設定購物車
讓我們建立一個上下文處理器,把目前購物車添加到模闆的請求上下文中。我們可以在所有模闆中通路購物車。
在
cart
應用目錄中建立
context_processors.py
檔案。上下文處理器可以位于代碼的任何地方,但是在這裡建立他們将保持代碼組織良好。在檔案中添加以下代碼:
from .cart import Cart
def cart(request):
return {'cart': Cart(request)}
正如你所看到的,上下文處理器是一個函數,它将
request
對象作為參數,并傳回一個對象的字典,這些對象可用于所有使用
RequestContext
渲染的模闆。在我們的上下文處理器中,我們用
request
對象執行個體化購物車,模闆可以通過
cart
變量名通路它。
編輯項目的
settings.py
檔案,在
TEMPLATES
設定的
context_processors
選項中添加
cart.context_processors.cart
,如下所示:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'cart.context_processors.cart',
],
},
},
]
每次使用
RequestContext
渲染模闆時,會執行你的上下文處理器。
cart
變量會設定在模闆的上下文中。
上下文處理器會在所有使用 RequestContext
的請求中執行。如果你想通路資料庫的話,可能希望建立一個自定義模闆标簽來代替上下文處理器。
現在編輯
shop
應用的
shop/base.html
模闆,找到以下代碼:
<div class="cart">
Your cart is empty.
</div>
用下面的代碼替換上面的代碼:
<div class="cart">
{% with total_items=cart|length %}
{% if cart|length > 0 %}
Your cart:
<a href="{% url "cart:cart_detail" %}">
{{ total_items }} item{{ total_items|pluralize }},
${{ cart.get_total_price }}
</a>
{% else %}
Your cart is empty.
{% endif %}
{% endwith %}
</div>
使用
python manage.py runserver
重新開機開發伺服器。在浏覽器中打開
http://127.0.0.1:8000/
,并添加一些商品到購物車中。在網站頭部,你會看到目前購物車總數量和總價錢,如下所示:
7.3 注冊使用者訂單
當購物車結賬後,你需要在資料庫中儲存訂單。訂單包括使用者資訊和他們購買的商品。
使用以下指令建立一個新應用,來管理使用者訂單:
python manage.py startapp orders
編輯項目的
settings.py
檔案,在
INSTALLED_APPS
設定中添加
orders
:
INSTALLED_APPS = [
# ...
'orders',
]
你已經激活了新應用。
7.3.1 建立訂單模型
你需要建立一個模型存儲訂單詳情,以及一個模型存儲購買的商品,包括價格和數量。編輯
orders
應用的
models.py
檔案,添加以下代碼:
from django.db import models
from shop.models import Product
class Order(models.Model):
first_name = models.CharField(max_length=)
last_name = models.CharField(max_length=)
email = models.EmailField()
address = models.CharField(max_length=)
postal_code = models.CharField(max_length=)
city = models.CharField(max_length=)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
paid = models.BooleanField(default=False)
class Meta:
ordering = ('-created', )
def __str__(self):
return 'Order {}'.format(self.id)
def get_total_cost(self):
return sum(item.get_cost() for item in self.items.all())
class OrderItem(models.Model):
order = models.ForeignKey(Order, related_name='items')
product = models.ForeignKey(Product, related_name='order_items')
price = models.DecimalField(max_digits=, decimal_places=)
quantity = models.PositiveIntegerField(default=)
def __str__(self):
return '{}'.format(self.id)
def get_cost(self):
return self.price * self.quantity
Order
模型包括幾個使用者資訊字段和一個預設值為
False
的
paid
布爾字段。之後,我們将用這個字段區分已支付和未支付的訂單。我們還定義了
get_total_cost()
方法,獲得這個訂單中購買商品的總價錢。
OrderItem
模型允許我們存儲商品,數量和每個商品的支付價格。我們用
get_cost()
傳回商品價錢。
運作以下指令,為
orders
應用建立初始資料庫遷移:
python manage.py makemigrations
你會看到以下輸出:
Migrations for 'orders':
orders/migrations/_initial.py
- Create model Order
- Create model OrderItem
運作以下指令讓新的遷移生效:
python manage.py migrate
現在你的訂單模型已經同步到資料庫中。
7.3.2 在管理站點引入訂單模型
讓我們在管理站點添加訂單模型。編輯
orders
應用的
admin.py
檔案,添加以下代碼:
from django.contrib import admin
from .models import Order, OrderItem
class OrderItemInline(admin.TabularInline):
model = OrderItem
raw_id_fields = ['product']
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'first_name', 'last_name', 'email',
'address', 'postal_code', 'city', 'paid', 'created', 'updated']
list_filter = ['paid', 'created', 'updated']
inlines = [OrderItemInline]
admin.site.register(Order, OrderAdmin)
我們為
OrderItem
模型使用
ModeInline
,把它作為内聯模型引入
OrderAdmin
類。内聯可以包含一個模型,與父模型在同一個編輯頁面顯示。
使用
python manage.py runserver
指令啟動開發伺服器,然後在浏覽器中打開
http:127.0.0.1/8000/admin/order/add/
。你會看到以下界面:
7.3.3 建立使用者訂單
當使用者最終下單時,我們需要使用剛建立的訂單模型來儲存購物車中的商品。建立一個新訂單的工作流程是這樣的:
- 向使用者顯示一個填寫資料的訂單表單。
- 用使用者輸入的資料建立一個新的
執行個體,然後為購物車中的每件商品建立關聯的Order
執行個體。OrderItem
- 清空購物車中所有内容,然後重定向到成功頁面。
首先,我們需要一個輸入訂單詳情的表單。在
orders
應用目錄中建立
forms.py
檔案,并添加以下代碼:
from django import forms
from .models import Order
class OrderCreateForm(forms.ModelForm):
class Meta:
model = Order
fields = ['first_name', 'last_name', 'email',
'address', 'postal_code', 'city']
這是我們用于建立新
Order
對象的表單。現在我們需要一個視圖處理表單和建立新表單。編輯
orders
應用的
views.py
檔案,并添加以下代碼:
from django.shortcuts import render
from .models import OrderItem
from .forms import OrderCreateForm
from cart.cart import Cart
def order_create(request):
cart = Cart(request)
if request.method == 'POST':
form = OrderCreateForm(request.POST)
if form.is_valid():
order = form.save()
for item in cart:
OrderItem.objects.create(order=order, product=item['product'],
price=item['price'], quantity=item['quantity'])
# clear the cart
cart.clear()
return render(request, 'orders/order/created.html', {'order': order})
else:
form = OrderCreateForm()
return render(request, 'orders/order/create.html', {'cart': cart, 'form': form})
在
order_create
視圖中,我們用
cart = Cart(request)
從會話中獲得目前購物車。根據請求的方法,我們執行以下任務:
-
請求:執行個體化GET
表單,并渲染OrderCreateForm
模闆。orders/order/create.html
-
請求:驗證送出的資料。如果資料有效,則使用POST
建立一個新的order = form.save()
執行個體。然後我們會将它儲存到資料庫中,并存儲在Order
變量中。建立order
之後,我們會疊代購物車中的商品,并為每個商品建立order
。最後,我們會清空購物車的内容。OrderItem
現在,在
orders
應用目錄中建立
urls.py
檔案,并添加以下代碼:
from django.conf.urls import url
from .import views
urlpatterns = [
url(r'^create/$', views.order_create, name='order_create'),
]
這是
order_create
視圖的URL模式。編輯
myshop
項目的
urls.py
檔案,并引入以下模式。記住,把它放在
shop.urls
模式之前:
url(r'^orders/', include('orders.urls', namespace='orders')),
編輯
cart
應用的
cart/detail.html
模闆,找到這行代碼:
把這樣代碼替換為以下代碼:
現在使用者可以從購物車詳情頁面導航到訂單表單。我們還需要為下單定義模闆。在
orders
應用目錄中建立以下檔案結構:
templates/
orders/
order/
create.html
created.html
編輯
orders/order/create.html
模闆,并添加以下代碼:
{% extends "shop/base.html" %}
{% block title %}
Checkout
{% endblock title %}
{% block content %}
<h1>Checkout</h1>
<div class="order-info">
<h3>Your order</h3>
<ul>
{% for item in cart %}
<li>
{{ item.quantity }}x {{ item.product.name }}
<span>${{ item.total_price }}</span>
</li>
{% endfor %}
</ul>
<p>Total: ${{ cart.get_total_price }}</p>
</div>
<form action="." method="post" class="order-form">
{{ form.as_p }}
<p><input type="submit" value="Place order"></p>
{% csrf_token %}
</form>
{% endblock content %}
這個模闆顯示購物車中的商品,包括總數量和下單的表單。
編輯
orders/order/created.html
模闆,并添加以下代碼:
{% extends "shop/base.html" %}
{% block title %}
Thank you
{% endblock title %}
{% block content %}
<h1>Thank you</h1>
<p>Your order has been successfully completed.
Your order number is <stong>{{ order.id }}</stong>
</p>
{% endblock content %}
成功建立訂單後,我們渲染這個模闆。啟動開發伺服器,并在浏覽器中打開
http://127.0.0.1:8000/
。在購物車中添加一些商品,然後跳轉到結賬界面。如下圖所示:
用有效的資料填寫表單,然後點選
Place order
按鈕。訂單會被建立,你将看到成功頁面,如下圖所示:
7.4 使用Celery啟動異步任務
你在視圖中執行的所有操作都會影響響應時間。在很多場景中,你可能希望盡快給使用者傳回響應,并讓伺服器執行一些異步處理。對于費時處理,或者失敗後可能需要重試政策的處理尤其重要。例如,一個視訊分享平台允許使用者上傳視訊,但轉碼上傳的視訊需要很長的時間。網站可能給使用者傳回一個響應,告訴使用者馬上開始轉碼,然後開始異步轉碼。另一個例子是給使用者發送郵件。如果網站在視圖中發送郵件通知,SMTP連接配接可能失敗,或者減慢響應時間。啟動異步任務避免阻塞操作是必不可少的。
Celery是一個可以處理大量消息的分布式任務隊列。它既可以實時處理,也支援任務排程。使用Celery不僅可以很容易的建立異步任務,還可以盡快執行任務,但也可以在一個指定時間執行任務。
你可以在這裡檢視Celery文檔。
7.4.1 安裝Celery
讓我們安裝Celery,并在項目中內建它。使用以下
pip
指令安裝Celery:
pip install celery
Celery必須有一個消息代理(message broker)處理外部請求。代理負責發送消息給Celery的
worker
,
worker
收到消息後處理任務。讓我們安裝一個消息代理。
7.4.2 安裝RabbitMQ
Celery有幾個消息代理可供選擇,包括鍵值對存儲(比如Redis),或者一個實際的消息系統(比如RabbitMQ)。我們将用RabbitMQ配置Celery,因為它是Celery的推薦消息worker。
如果你使用的是Linux,可以在終端執行以下指令安裝RabbitMQ:
apt-get install rabbitmq
如果你需要在Max OS X或者Windows上安裝RabbitMQ,你可以在這裡找到獨立的版本。
安裝後,在終端執行以下指令啟動RabbitMQ:
你會看到以這一行結尾的輸出:
7.4.3 在項目中添加Celery
你需要為Celery執行個體提供一個配置。在
myshop
中建立
celery.py
檔案,該檔案會包括項目的Celery配置,并添加以下代碼:
import os
from celery import Celery
from django.conf import settings
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myshop.settings')
app = Celery('myshop')
app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
在這段代碼中,我們為Celery指令行程式設定
DJANGO_SETINGS_MODULE
變量。然後用
app = Celery('myshop.)
建立了一個應用執行個體。我們用
config_from_object()
方法從項目設定中加載所有自定義設定。最後我們告訴Celery,為
INSTALLED_APPS
設定中列出的應用自動查找異步任務。Celery會在每個應用目錄中查找
tasks.py
檔案,并加載其中定義的異步任務。
你需要在項目的
__init__.py
檔案中導入
celery
子產品,確定Django啟動時會加載Celery。編輯
myshop/__init__.py
檔案,并添加以下代碼:
from .celery import app as celery_app
現在你可以開始為應用編寫異步任務了。
CELERY_ALWAYS_EAGER
設定允許你以同步方式在本地執行任務,而不是将其發送到隊列。這對于運作單元測試,或者在不運作Celery的情況下,運作本地環境中的項目時非常有用。
7.4.4 在應用中添加異步任務
當使用者下單後,我們将建立一個異步任務,給使用者發送一封郵件通知。
一般的做法是在應用目錄的
tasks
子產品中包括應用的異步任務。在
orders
應用目錄中建立
tasks.py
檔案。Celery會在這裡查找異步任務。在其中添加以下代碼:
from celery import task
from django.core.mail import send_mail
from .models import Order
@task
def order_created(order_id):
order = Order.objects.get(id=order_id)
subject = 'Order nr. {}'.format(order.id)
message = 'Dear {},\n\nYou have successfully placed an order.\
Your order id is {}.'.format(order.first_name, order.id)
mail_sent = send_mail(subject, message, '[email protected]', [order.email])
return mail_sent
我們使用
task
裝飾器定義了
order_created
任務。正如你所看到的,Celery任務就是一個用
task
裝飾的Python函數。我們的
task
函數接收
order_id
作為參數。推薦隻傳遞ID給任務函數,并在任務執行時查詢對象。我們用Django提供的
send_mail()
函數,當使用者下單後發送郵件通知。如果你不想配置郵件選項,你可以在項目的
settings.py
檔案中添加以下設定,讓Django在控制台輸出郵件:
異步任務不僅可以用于費時的操作,還可以用于可能失敗的操作,這些操作不會執行很長時間,但它們可能會連接配接失敗,或者需要重試政策。
現在我們需要在
order_create
視圖中添加任務。打開
orders
應用的
views.py
檔案,并導入任務:
from .tasks import order_created
然後在清空購物車之後調用
order_created
異步任務:
# clear the cart
cart.clear()
# launch asynchronous task
order_created.delay(order.id)
我們調用任務的
delay()
方法異步執行任務。任務會被添加到隊列中,
worker
會盡快執行。
打開另一個終端,并使用以下指令啟動Celery的
worker
:
celery -A myshop worker -l info
譯者注:必須在 myshop
項目目錄下執行上面的指令。
現在Celery的
worker
已經運作,準備好處理任務了。確定Django開發伺服器也在運作。在浏覽器中打開
http://127.0.0.1/8000/
,添加一些商品到購物車中,然後完成訂單。在終端,你已經啟動了Celery
worker
,你會看到類似這樣的輸出:
[-- ::,: INFO/MainProcess] Received task: orders.tasks.order_created[d6f667b-cc7--fc-fae54]
[-- ::,: INFO/PoolWorker-] Task orders.tasks.order_created[d6f667b-cc7--fc-fae54] succeeded in s:
任務已經執行,你會收到一封訂單的郵件通知。
7.4.5 監控Celery
你可能希望監控已經執行的異步任務。Flower是一個基于網頁的監控Celery工具。你可以使用
pip install flower
安裝Flower。
安裝後,你可以在項目目錄下執行以下指令啟動Flower:
celery -A myshop flower
在浏覽器中打開
http://127.0.0.1:5555/dashboard
。你會看到活動的Celery
worker
和異步任務統計:
你可以在這裡檢視Flower的文檔。
7.5 總結
在這章中,你建立了一個基礎的線上商店應用。你建立了商品目錄,并用會話建構了購物車。你實作了自定義上下文處理器,讓模闆可以通路購物車,并建立了下單的表單。你還學習了如何使用Celery啟動異步任務。
在下一章中,你會學習在商店中內建支付網關(payment gateway),在管理站點添加使用者操作,導出CVS格式資料,以及動态生成PDF檔案。