9 擴展你的商店
上一章中,你學習瞭如何在商店中集成支付網關。你完成了支付通知,學習瞭如何生成CSV和PDF文件。在這一章中,你會在商店中添加優惠券系統。你將學習如何處理國際化和本地化,並構建一個推薦引擎。
本章會覆蓋以下知識點:
- 創建優惠券系統實現折扣
- 在項目中添加國際化
- 使用Rosetta管理翻譯
- 使用django-parler翻譯模型
- 構建一個商品推薦系統
9.1 創建優惠券系統
很多在線商店會給顧客發放優惠券,在購買商品時可以兌換折扣。在線優惠券通常是一組發放給用戶的代碼,這個代碼在某個時間段內有效。這個代碼可以兌換一次或多次。
我們將爲我們的商品創建一個優惠券系統。顧客在某個時間段內輸入我們的優惠券纔有效。優惠券沒有使用次數限制,可以抵扣購物車的總金額。對於這個功能,我們需要創建一個模型,用於存儲優惠券碼,有效時間和折扣金額。
使用以下命令在myshop
項目中添加一個新應用:
python manage.py startapp coupons
- 1
- 1
編輯myshop
的settings.py
文件,把應用添加到INSTALLED_APPS
中:
INSTALLED_APPS = (
# ...
'coupons',
)
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
現在新應用已經在我們的Django項目中激活了。
9.1.1 構建優惠券模型
讓我們從創建Coupon
模型開始。編輯coupons
應用的models.py
文件,並添加以下代碼:
from django.db import models
from django.core.validators import MinValueValidator
from django.core.validators import MaxValueValidator
class Coupon(models.Model):
code = models.CharField(max_length=50, unique=True)
valid_from = models.DateTimeField()
valid_to = models.DateTimeField()
discount = models.IntegerField(
validators=[MinValueValidator(0), MaxValueValidator(100)])
active = models.BooleanField()
def __str__(self):
return self.code
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
這是存儲優惠券的模型。Coupon
模型包括以下字段:
code
:用戶必須輸入優惠券碼才能使用優惠券。valid_from
:優惠券開始生效的時間。valid_to
:優惠券過期的時間。discount
:折扣率(這是一個百分比,所以範圍是0到100)。我們使用驗證器限制這個字段的最小值和最大值。active
:表示優惠券是否有效的布爾值。
執行以下命令,生成coupon
應用的初始數據庫遷移:
python manage.py makemigrations
- 1
- 1
輸出會包括以下行:
Migrations for 'coupons':
coupons/migrations/0001_initial.py
- Create model Coupon
- 1
- 2
- 3
- 1
- 2
- 3
然後執行以下命令,讓數據庫遷移生效:
python manage.py migrate
- 1
- 1
你會看到包括這一行的輸出:
Applying coupons.0001_initial... OK
- 1
- 1
現在遷移已經應用到數據庫中了。讓我們把Coupon
模型添加到管理站點。編輯coupons
應用的admin.py
文件,並添加以下代碼:
from django.contrib import admin
from .models import Coupon
class CouponAdmin(admin.ModelAdmin):
list_display = ['code', 'valid_from', 'valid_to', 'discount', 'active']
list_filter = ['active', 'valid_from', 'valid_to']
search_fields = ['code']
admin.site.register(Coupon, CouponAdmin)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
現在Coupon
模型已經在管理站點註冊。執行Python manage.py
runserver
命令啓動開發服務器,然後在瀏覽器中打開http://127.0.0.1:8000/admin/coupons/coupon/add/
。你會看下圖中的表單:
填寫表單,創建一個當天可用的優惠券,並勾選Active
,然後點擊Save
按鈕。
9.1.2 在購物車中使用優惠券
我們可以存儲新的優惠券,並且可以查詢已存在的優惠券。現在我們需要讓顧客可以使用優惠券。考慮一下應該怎麼實現這個功能。使用優惠券的流程是這樣的:
- 用戶添加商品到購物車中。
- 用戶在購物車詳情頁面的表單中輸入優惠券碼。
- 當用戶輸入了優惠券碼,並提交了表單,我們用這個優惠券碼查找當前有效地一張優惠券。我們必須檢查這張優惠券碼匹配用戶輸入的優惠券碼,
active
屬性爲True
,以及當前時間在valid_from
和valid_to
之間。 - 如果找到了優惠券,我們把它保存在用戶會話中,顯示包括折扣的購物車,然後更新總金額。
- 當用戶下單時,我們把優惠券保存到指定的訂單中。
在coupons
應用目錄中創建forms.py
文件,並添加以下代碼:
from django import forms
class CouponApplyForm(forms.Form):
code = forms.CharField()
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
我們用這個表單讓用戶輸入優惠券碼。編輯coupons
應用的views.py
文件,並添加以下代碼:
from django.shortcuts import render, redirect
from django.utils import timezone
from django.views.decorators.http import require_POST
from .models import Coupon
from .forms import CouponApplyForm
@require_POST
def coupon_apply(request):
now = timezone.now()
form = CouponApplyForm(request.POST)
if form.is_valid():
code = form.cleaned_data['code']
try:
coupon = Coupon.objects.get(code__iexact=code,
valid_from__lte=now,
valid_to__gte=now,
active=True)
request.session['coupon_id'] = coupon.id
except Coupon.DoesNotExist:
request.session['coupon_id'] = None
return redirect('cart:cart_detail')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
coupon_apply
視圖驗證優惠券,並把它存儲在用戶會話中。我們用require_POST
裝飾器裝飾這個視圖,只允許POST請求。在視圖中,我們執行以下任務:
- 我們用提交的數據實例化
CouponApplyForm
表單,並檢查表單是否有效。 - 如果表單有效,我們從表單的
cleaned_data
字典中獲得用戶輸入的優惠券碼。我們用給定的優惠券碼查詢Coupon
對象。我們用iexact
字段執行大小寫不敏感的精確查詢。優惠券必須是有效的(active=True
),並且在當前時間是有效地。我們用Django的timezone.now()
函數獲得當前時區的時間,我們把它與valid_from
和valid_to
字段比較,對這兩個字段分別執行lte
(小於等於)和gte
(大於等於)字段查詢。 - 我們在用戶會話中存儲優惠券ID。
- 我們重定向用戶到
cart_detail
URL,顯示使用了優惠券的購物車。
我們需要一個coupon_apply
視圖的URL模式。在coupons
應用目錄中添加urls.py
文件,並添加以下代碼:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^apply/$', views.coupon_apply, name='apply'),
]
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
然後編輯myshop
項目的主urls.py
文件,添加coupons
的URL模式:
url(r'^coupons/', include('coupons.urls', namespace='coupons')),
- 1
- 1
記住,把這個模式放到shop.urls
模式之前。
現在編輯cart
應用的cart.py
文件,添加以下導入:
from coupons.models import Coupon
- 1
- 1
在Cart
類的__init__()
方法最後添加以下代碼,從當前會話中初始化優惠券:
# store current applied coupon
self.coupon_id = self.session.get('coupon_id')
- 1
- 2
- 1
- 2
這行代碼中,我們嘗試從當前會話中獲得coupon_id
會話鍵,並把它存儲到Cart
對象中。在Cart
對象中添加以下方法:
@property
def coupon(self):
if self.coupon_id:
return Coupon.objects.get(id=self.coupon_id)
return None
def get_discount(self):
if self.coupon:
return (self.coupon.discount / Decimal('100') * self.get_total_price())
return Decimal('0')
def get_total_price_after_discount(self):
return self.get_total_price() - self.get_discount()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
這些方法分別是:
coupon()
:我們定義這個方法爲property
。如果cart
中包括coupon_id
屬性,則返回給定id
的Coupon
對象。get_discount()
:如果cart
包括coupon
,則查詢它的折扣率,並返回從購物車總金額中扣除的金額。get_total_price_after_discount()
:減去get_discount()
方法返回的金額後,購物車的總金額。
現在Cart
類已經準備好處理當前會話中的優惠券,並且可以減去相應的折扣。
讓我們在購物車詳情視圖中引入優惠券系統。編輯cart
應用的views.py
,在文件頂部添加以下導入:
from coupons.forms import CouponApplyForm
- 1
- 1
接着編輯cart_detail
視圖,並添加新表單:
def cart_detail(request):
cart = Cart(request)
for item in cart:
item['update_quantity_form'] = CartAddProductForm(
initial={'quantity': item['quantity'], 'update': True})
coupon_apply_form = CouponApplyForm()
return render(request, 'cart/detail.html',
{'cart': cart, 'coupon_apply_form': coupon_apply_form})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
編輯cart
應用的cart/detail.html
目錄,找到以下代碼:
<tr class="total">
<td>Total</td>
<td colspan="4"></td>
<td class="num">${{ cart.get_total_price }}</td>
</tr>
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
替換爲下面的代碼:
{% if cart.coupon %}
<tr class="subtotal">
<td>Subtotal</td>
<td colspan="4"></td>
<td class="num">${{ cart.get_total_price }}</td>
</tr>
<tr>
<td>
"{{ cart.coupon.code }}" coupon
({{ cart.coupon.discount }}% off)
</td>
<td colspan="4"></td>
<td class="num neg">
- ${{ cart.get_discount|floatformat:"2" }}
</td>
</tr>
{% endif %}
<tr class="total">
<td>Total</td>
<td colspan="4"></td>
<td class="num">
${{ cart.get_total_price_after_discount|floatformat:"2" }}
</td>
</tr>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
這段代碼顯示一個可選的優惠券和它的折扣率。如果購物車包括一張優惠券,我們在第一行顯示購物車總金額爲Subtotal
。然後在第二行顯示購物車使用的當前優惠券。最後,我們調用cart
對象的cart.get_total_price_after_discount()
方法,顯示折扣之後的總金額。
在同一個文件的</table>
標籤之後添加以下代碼:
<p>Apply a coupon:</p>
<form action="{% url "coupons:apply" %}" method="post">
{{ coupon_apply_form }}
<input type="submit" value="Apply">
{% csrf_token %}
</form>
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
這會顯示輸入優惠券碼的表單,並在當前購物車中使用。
在瀏覽器中打開http://127.0.0.1:8000/
,添加一個商品到購物車中,然後使用表單中輸入的優惠券碼。你會看到購物車顯示優惠券折扣,如下圖所示:
讓我們把優惠券添加到購物流程的下一步。編輯orders
應用的orders/order/create.html
模板,找到以下代碼:
<ul>
{% for item in cart %}
<li>
{{ item.quantity }}x {{ item.product.name }}
<span>${{ item.total_price }}</span>
</li>
{% endfor %}
</ul>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
替換爲以下代碼:
<ul>
{% for item in cart %}
<li>
{{ item.quantity }}x {{ item.product.name }}
<span>${{ item.total_price }}</span>
</li>
{% endfor %}
{% if cart.coupon %}
<li>
"{{ cart.coupon.code }}" ({{ cart.coupon.discount }}% off)
<span>- ${{ cart.get_discount|floatformat:"2" }}</span>
</li>
{% endif %}
</ul>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
如果有優惠券的話,訂單彙總已經使用了優惠券。現在找到這行代碼:
<p>Total: ${{ cart.get_total_price }}</p>
- 1
- 1
替換爲下面這一行:
<p>Total: ${{ cart.get_total_price_after_discount|floatformat:"2" }}</p>
- 1
- 1
這樣,總價格是使用優惠券之後的價格。
在瀏覽器中打開中http://127.0.0.1:8000/orders/create/
。你會看到訂單彙總包括了使用的優惠券:
現在用戶可以在購物車中使用優惠券了。但是當用戶結賬時,我們還需要在創建的訂單中存儲優惠券信息。
9.1.3 在訂單中使用優惠券
我們將存儲每個訂單使用的優惠券。首先,我們需要修改Order
模型來存儲關聯的Coupon
對象(如果存在的話)。
編輯orders
應用的models.py
文件,並添加以下導入:
from decimal import Decimal
from django.core.validators import MinValueValidator
from django.core.validators import MaxValueValidator
from coupons.models import Coupon
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
然後在Order
模型中添加以下字段:
coupon = models.ForeignKey(Coupon, related_name='orders', null=True, blank=True)
discount = models.IntegerField(default=0,
validators=[MinValueValidator(0), MaxValueValidator(100)])
- 1
- 2
- 3
- 1
- 2
- 3
這些字段允許我們存儲一個可選的訂單使用的優惠券和優惠券的折扣。折扣存儲在關聯的Coupon
對象中,但我們在Order
模型中包括它,以便優惠券被修改或刪除後還能保存。
因爲修改了Order
模型,所以我們需要創建一個數據庫遷移。在命令行中執行以下命令:
python manage.py makemigrations
- 1
- 1
你會看到類似這樣的輸出:
Migrations for 'orders':
orders/migrations/0002_auto_20170515_0731.py
- Add field coupon to order
- Add field discount to order
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
執行以下命令同步數據庫遷移:
python manage.py migrate orders
- 1
- 1
你會看到新的數據庫遷移已經生效,現在Order
模型的字段修改已經同步到數據庫中。
回到models.py
文件,修改Order
模型的get_total_cost()
方法:
def get_total_cost(self):
total_cost = sum(item.get_cost() for item in self.items.all())
return total_cost - total_cost * self.discount / Decimal('100')
- 1
- 2
- 3
- 1
- 2
- 3
如果存在優惠券,Order
模型的get_total_cost()
方法會計算優惠券的折扣。
編輯orders
應用的views.py
文件,修改其中的order_create
視圖,當創建新訂單時,保存關聯的優惠券。找到這一行代碼:
order = form.save()
- 1
- 1
替換爲以下代碼:
order = form.save(commit=False)
if cart.coupon:
order.coupon = cart.coupon
order.discount = cart.coupon.discount
order.save()
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
在新代碼中,我們用OrderCreateForm
表單的save()
方法創建了一個Order
對象,並用commit=False
避免保存到數據庫中。如果購物車中包括優惠券,則存儲使用的關聯優惠券和折扣。然後我們把order
對象存儲到數據庫中。
執行python manage.py
runserver
命令啓動開發服務器,並使用./ngrok http 8000
命令啓動Ngrok。
在瀏覽器中打開Ngrok提供的URL,並使用你創建的優惠券完成一次購物。當你成功完成一次購物,你可以訪問http://127.0.0.1:8000/admin/orders/order/
,檢查訂單是否包括優惠券和折扣,如下圖所示:
你還可以修改管理訂單詳情模板和訂單PDF賬單,用跟購物車同樣的方式顯示使用的優惠券。
接下來,我們要爲項目添加國際化。