第九章 扩展你的商店(上)

9 扩展你的商店

上一章中,你学习了如何在商店中集成支付网关。你完成了支付通知,学习了如何生成CSV和PDF文件。在这一章中,你会在商店中添加优惠券系统。你将学习如何处理国际化和本地化,并构建一个推荐引擎。

本章会覆盖以下知识点:

  • 创建优惠券系统实现折扣
  • 在项目中添加国际化
  • 使用Rosetta管理翻译
  • 使用django-parler翻译模型
  • 构建一个商品推荐系统

9.1 创建优惠券系统

很多在线商店会给顾客发放优惠券,在购买商品时可以兑换折扣。在线优惠券通常是一组发放给用户的代码,这个代码在某个时间段内有效。这个代码可以兑换一次或多次。

我们将为我们的商品创建一个优惠券系统。顾客在某个时间段内输入我们的优惠券才有效。优惠券没有使用次数限制,可以抵扣购物车的总金额。对于这个功能,我们需要创建一个模型,用于存储优惠券码,有效时间和折扣金额。

使用以下命令在myshop项目中添加一个新应用:

python manage.py startapp coupons

编辑myshopsettings.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 在购物车中使用优惠券

我们可以存储新的优惠券,并且可以查询已存在的优惠券。现在我们需要让顾客可以使用优惠券。考虑一下应该怎么实现这个功能。使用优惠券的流程是这样的:

  1. 用户添加商品到购物车中。
  2. 用户在购物车详情页面的表单中输入优惠券码。
  3. 当用户输入了优惠券码,并提交了表单,我们用这个优惠券码查找当前有效地一张优惠券。我们必须检查这张优惠券码匹配用户输入的优惠券码,active属性为True,以及当前时间在valid_fromvalid_to之间。
  4. 如果找到了优惠券,我们把它保存在用户会话中,显示包括折扣的购物车,然后更新总金额。
  5. 当用户下单时,我们把优惠券保存到指定的订单中。

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请求。在视图中,我们执行以下任务:

  1. 我们用提交的数据实例化CouponApplyForm表单,并检查表单是否有效。
  2. 如果表单有效,我们从表单的cleaned_data字典中获得用户输入的优惠券码。我们用给定的优惠券码查询Coupon对象。我们用iexact字段执行大小写不敏感的精确查询。优惠券必须是有效的(active=True),并且在当前时间是有效地。我们用Django的timezone.now()函数获得当前时区的时间,我们把它与valid_fromvalid_to字段比较,对这两个字段分别执行lte(小于等于)和gte(大于等于)字段查询。
  3. 我们在用户会话中存储优惠券ID。
  4. 我们重定向用户到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属性,则返回给定idCoupon对象。
  • 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目录,找到以下代码:

<tr class="total">
    <td>Total</td>
    <td colspan="4"></td>
    <td class="num">${{ cart.get_total_price }}</td>
</tr>

替换为下面的代码:

{% 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>

这段代码显示一个可选的优惠券和它的折扣率。如果购物车包括一张优惠券,我们在第一行显示购物车总金额为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>

这会显示输入优惠券码的表单,并在当前购物车中使用。

在浏览器中打开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>

替换为以下代码:

<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>

如果有优惠券的话,订单汇总已经使用了优惠券。现在找到这行代码:

<p>Total: ${{ cart.get_total_price }}</p>

替换为下面这一行:

<p>Total: ${{ cart.get_total_price_after_discount|floatformat:"2" }}</p>

这样,总价格是使用优惠券之后的价格。

在浏览器中打开中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账单,用跟购物车同样的方式显示使用的优惠券。

接下来,我们要为项目添加国际化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容