9 扩展你的商店
上一章中,你学习了如何在商店中集成支付网关。你完成了支付通知,学习了如何生成CSV和PDF文件。在这一章中,你会在商店中添加优惠券系统。你将学习如何处理国际化和本地化,并构建一个推荐引擎。
本章会覆盖以下知识点:
- 创建优惠券系统实现折扣
- 在项目中添加国际化
- 使用Rosetta管理翻译
- 使用django-parler翻译模型
- 构建一个商品推荐系统
9.1 创建优惠券系统
很多在线商店会给顾客发放优惠券,在购买商品时可以兑换折扣。在线优惠券通常是一组发放给用户的代码,这个代码在某个时间段内有效。这个代码可以兑换一次或多次。
我们将为我们的商品创建一个优惠券系统。顾客在某个时间段内输入我们的优惠券才有效。优惠券没有使用次数限制,可以抵扣购物车的总金额。对于这个功能,我们需要创建一个模型,用于存储优惠券码,有效时间和折扣金额。
使用以下命令在myshop
项目中添加一个新应用:
python manage.py startapp coupons
编辑myshop
的settings.py
文件,把应用添加到INSTALLED_APPS
中:
INSTALLED_APPS = (
# ...
'coupons',
)
现在新应用已经在我们的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
这是存储优惠券的模型。Coupon
模型包括以下字段:
-
code
:用户必须输入优惠券码才能使用优惠券。 -
valid_from
:优惠券开始生效的时间。 -
valid_to
:优惠券过期的时间。 -
discount
:折扣率(这是一个百分比,所以范围是0到100)。我们使用验证器限制这个字段的最小值和最大值。 -
active
:表示优惠券是否有效的布尔值。
执行以下命令,生成coupon
应用的初始数据库迁移:
python manage.py makemigrations
输出会包括以下行:
Migrations for 'coupons':
coupons/migrations/0001_initial.py
- Create model Coupon
然后执行以下命令,让数据库迁移生效:
python manage.py migrate
你会看到包括这一行的输出:
Applying coupons.0001_initial... OK
现在迁移已经应用到数据库中了。让我们把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)
现在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()
我们用这个表单让用户输入优惠券码。编辑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')
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'),
]
然后编辑myshop
项目的主urls.py
文件,添加coupons
的URL模式:
url(r'^coupons/', include('coupons.urls', namespace='coupons')),
记住,把这个模式放到shop.urls
模式之前。
现在编辑cart
应用的cart.py
文件,添加以下导入:
from coupons.models import Coupon
在Cart
类的__init__()
方法最后添加以下代码,从当前会话中初始化优惠券:
# store current applied coupon
self.coupon_id = self.session.get('coupon_id')
这行代码中,我们尝试从当前会话中获得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()
这些方法分别是:
-
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
接着编辑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})
编辑cart
应用的cart/detail.html
目录,找到以下代码:
Total
${{ cart.get_total_price }}
替换为下面的代码:
{% if cart.coupon %}
Subtotal
${{ cart.get_total_price }}
"{{ cart.coupon.code }}" coupon
({{ cart.coupon.discount }}% off)
- ${{ cart.get_discount|floatformat:"2" }}
{% endif %}
Total
${{ cart.get_total_price_after_discount|floatformat:"2" }}
这段代码显示一个可选的优惠券和它的折扣率。如果购物车包括一张优惠券,我们在第一行显示购物车总金额为Subtotal
。然后在第二行显示购物车使用的当前优惠券。最后,我们调用cart
对象的cart.get_total_price_after_discount()
方法,显示折扣之后的总金额。
在同一个文件的标签之后添加以下代码:
Apply a coupon:
这会显示输入优惠券码的表单,并在当前购物车中使用。
在浏览器中打开http://127.0.0.1:8000/
,添加一个商品到购物车中,然后使用表单中输入的优惠券码。你会看到购物车显示优惠券折扣,如下图所示:
让我们把优惠券添加到购物流程的下一步。编辑orders
应用的orders/order/create.html
模板,找到以下代码:
{% for item in cart %}
-
{{ item.quantity }}x {{ item.product.name }}
${{ item.total_price }}
{% endfor %}
替换为以下代码:
{% for item in cart %}
-
{{ item.quantity }}x {{ item.product.name }}
${{ item.total_price }}
{% endfor %}
{% if cart.coupon %}
-
"{{ cart.coupon.code }}" ({{ cart.coupon.discount }}% off)
- ${{ cart.get_discount|floatformat:"2" }}
{% endif %}
如果有优惠券的话,订单汇总已经使用了优惠券。现在找到这行代码:
Total: ${{ cart.get_total_price }}
替换为下面这一行:
Total: ${{ cart.get_total_price_after_discount|floatformat:"2" }}
这样,总价格是使用优惠券之后的价格。
在浏览器中打开中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
然后在Order
模型中添加以下字段:
coupon = models.ForeignKey(Coupon, related_name='orders', null=True, blank=True)
discount = models.IntegerField(default=0,
validators=[MinValueValidator(0), MaxValueValidator(100)])
这些字段允许我们存储一个可选的订单使用的优惠券和优惠券的折扣。折扣存储在关联的Coupon
对象中,但我们在Order
模型中包括它,以便优惠券被修改或删除后还能保存。
因为修改了Order
模型,所以我们需要创建一个数据库迁移。在命令行中执行以下命令:
python manage.py makemigrations
你会看到类似这样的输出:
Migrations for 'orders':
orders/migrations/0002_auto_20170515_0731.py
- Add field coupon to order
- Add field discount to order
执行以下命令同步数据库迁移:
python manage.py migrate orders
你会看到新的数据库迁移已经生效,现在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')
如果存在优惠券,Order
模型的get_total_cost()
方法会计算优惠券的折扣。
编辑orders
应用的views.py
文件,修改其中的order_create
视图,当创建新订单时,保存关联的优惠券。找到这一行代码:
order = form.save()
替换为以下代码:
order = form.save(commit=False)
if cart.coupon:
order.coupon = cart.coupon
order.discount = cart.coupon.discount
order.save()
在新代码中,我们用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账单,用跟购物车同样的方式显示使用的优惠券。
接下来,我们要为项目添加国际化。