当前位置:首页 > 技术 > 正文内容

扩展电商平台功能:优惠券、国际化与推荐系统实现

访客 技术 2026年5月22日 3

在上一章节中,我们为电商平台集成了在线支付功能,并实现了PDF发票的自动生成与发送。本章将聚焦于三个重要功能的实现:优惠券折扣系统、网站国际化与本地化、以及基于购买数据的商品推荐引擎。

1. 优惠券系统

在线优惠券是电商平台常用的营销工具,通常由一串字符代码构成,具有有效期和使用次数限制。我们将实现一个基于百分比的折扣优惠券系统,该优惠券在有效期内可多次使用,用户输入优惠码后,购物车总价将自动计算折扣。

1.1 创建优惠券应用

首先在项目中创建新应用:

python manage.py startapp coupons

settings.py 中注册:

INSTALLED_APPS = [
    # ...
    'coupons.apps.CouponsConfig',
]

1.2 定义优惠券数据模型

编辑 coupons/models.py

from django.db import models
from django.core.validators import MinValueValidator, 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

模型说明:code 存储唯一优惠码,valid_fromvalid_to 定义有效期,discount 为0-100的整数折扣百分比,active 控制是否启用。

执行数据库迁移并在 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.3 实现购物车优惠逻辑

创建 coupons/forms.py 用于用户输入优惠码:

from django import forms

class CouponApplyForm(forms.Form):
    code = forms.CharField()

编写视图处理优惠码验证:

# coupons/views.py
from django.shortcuts import 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')

配置URL路由:

# coupons/urls.py
from django.urls import path
from . import views

app_name = 'coupons'
urlpatterns = [
    path('apply/', views.coupon_apply, name='apply'),
]

在主路由中包含:

urlpatterns += [
    path('coupons/', include('coupons.urls', namespace='coupons')),
]

1.4 扩展购物车类

修改 cart/cart.py,在 Cart 类中添加优惠相关方法:

from coupons.models import Coupon
from decimal import Decimal

class Cart:
    def __init__(self, request):
        # ... 原有代码 ...
        self.coupon_id = self.session.get('coupon_id')

    @property
    def coupon(self):
        if self.coupon_id:
            try:
                return Coupon.objects.get(id=self.coupon_id)
            except Coupon.DoesNotExist:
                return None
        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.5 更新订单模型和视图

Order 模型添加字段:

# orders/models.py
from decimal import Decimal
from django.core.validators import MinValueValidator, MaxValueValidator
from coupons.models import Coupon

class Order(models.Model):
    # ... 原有字段 ...
    coupon = models.ForeignKey(Coupon, related_name='orders', null=True, blank=True, on_delete=models.SET_NULL)
    discount = models.IntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)])

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

在订单创建视图中保存优惠信息:

# orders/views.py
def order_create(request):
    cart = Cart(request)
    if request.method == 'POST':
        form = OrderCreateForm(request.POST)
        if form.is_valid():
            order = form.save(commit=False)
            if cart.coupon:
                order.coupon = cart.coupon
                order.discount = cart.coupon.discount
            order.save()
            # ... 创建 OrderItem 并清空购物车 ...
            request.session['coupon_id'] = None  # 清除优惠券session
            # ... 后续处理 ...

2. 国际化与本地化

Django 提供完善的 i18n 框架,支持多语言翻译、本地化格式和时区处理。我们将为站点添加英语和西班牙语支持。

2.1 基础配置

settings.py 中配置:

from django.utils.translation import gettext_lazy as _

LANGUAGES = (
    ('en', _('English')),
    ('es', _('Spanish')),
)

LANGUAGE_CODE = 'en'

LOCALE_PATHS = (
    os.path.join(BASE_DIR, 'locale/'),
)

添加语言中间件,注意放置顺序:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ...
]

在项目根目录创建 locale/ 目录,包含 en/es/ 子目录。

2.2 翻译字符串

在模型和表单中使用 gettext_lazy 标记翻译:

# orders/models.py
from django.utils.translation import gettext_lazy as _

class Order(models.Model):
    first_name = models.CharField(_('first name'), max_length=50)
    # ... 其他字段

生成翻译文件:

django-admin makemessages --all

编辑 .po 文件添加翻译,然后编译:

django-admin compilemessages

2.3 使用 Rosetta 管理翻译

安装 django-rosetta:

pip install django-rosetta

配置路由:

urlpatterns += [
    path('rosetta/', include('rosetta.urls')),
]

通过 /rosetta/ 路径访问管理界面,可直接编辑翻译并自动编译。

2.4 翻译URL模式

使用 i18n_patterns 为URL添加语言前缀:

from django.conf.urls.i18n import i18n_patterns

urlpatterns = i18n_patterns(
    path(_('admin/'), admin.site.urls),
    path(_('cart/'), include('cart.urls', namespace='cart')),
    path(_('orders/'), include('orders.urls', namespace='orders')),
    path(_('payment/'), include('payment.urls', namespace='payment')),
    path(_('coupons/'), include('coupons.urls', namespace='coupons')),
    path('', include('shop.urls', namespace='shop')),
)

2.5 添加语言选择器

在模板中添加切换语言的功能:

{# base.html #}
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}

<div class="languages">
    {% trans "Language" %}:
    <ul>
        {% for language in languages %}
            <li>
                <a href="/{{ language.code }}/"
                   {% if language.code == LANGUAGE_CODE %}class="selected"{% endif %}>
                    {{ language.name_local }}
                </a>
            </li>
        {% endfor %}
    </ul>
</div>

2.6 模型字段翻译

安装 django-parler:

pip install django-parler

配置:

PARLER_LANGUAGES = {
    None: (
        {'code': 'en'},
        {'code': 'es'},
    ),
    'default': {
        'fallback': 'en',
        'hide_untranslated': False,
    }
}

修改模型使用 TranslatableModel

from parler.models import TranslatableModel, TranslatedFields

class Product(TranslatableModel):
    translations = TranslatedFields(
        name=models.CharField(max_length=200),
        slug=models.SlugField(max_length=200),
        description=models.TextField(blank=True)
    )
    # ... 其他字段

更新管理后台:

from parler.admin import TranslatableAdmin

@admin.register(Product)
class ProductAdmin(TranslatableAdmin):
    list_display = ['name', 'price', 'available']
    # ...

3. 商品推荐系统

基于 Redis 实现协同过滤推荐,记录经常被一起购买的商品组合。

3.1 安装和配置 Redis

确保 Redis 服务运行,安装 Python 客户端:

pip install redis

配置连接参数:

REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 1

3.2 实现推荐引擎

创建 shop/recommender.py

import redis
from django.conf import settings
from .models import Product

r = redis.StrictRedis(
    host=settings.REDIS_HOST,
    port=settings.REDIS_PORT,
    db=settings.REDIS_DB
)

class Recommender:
    def get_product_key(self, product_id):
        return f'product:{product_id}:purchased_with'

    def products_bought(self, products):
        product_ids = [p.id for p in products]
        for product_id in product_ids:
            for with_id in product_ids:
                if product_id != with_id:
                    r.zincrby(
                        self.get_product_key(product_id),
                        1,
                        with_id
                    )

    def suggest_products_for(self, products, max_results=6):
        product_ids = [p.id for p in products]

        if len(product_ids) == 1:
            suggestions = r.zrange(
                self.get_product_key(product_ids[0]),
                0, -1, desc=True
            )[:max_results]
        else:
            flat_ids = ''.join(str(id) for id in product_ids)
            tmp_key = f'tmp_{flat_ids}'
            keys = [self.get_product_key(id) for id in product_ids]
            r.zunionstore(tmp_key, keys)
            r.zrem(tmp_key, *product_ids)
            suggestions = r.zrange(tmp_key, 0, -1, desc=True)[:max_results]
            r.delete(tmp_key)

        suggested_ids = [int(id) for id in suggestions]
        suggested_products = list(
            Product.objects.filter(id__in=suggested_ids)
        )
        suggested_products.sort(
            key=lambda x: suggested_ids.index(x.id)
        )
        return suggested_products

    def clear_purchases(self):
        for pid in Product.objects.values_list('id', flat=True):
            r.delete(self.get_product_key(pid))

3.3 在视图中集成推荐

在商品详情视图添加推荐:

# shop/views.py
from .recommender import Recommender

def product_detail(request, id, slug):
    # ... 获取商品对象 ...
    r = Recommender()
    recommended = r.suggest_products_for([product], 4)
    return render(request, 'shop/product/detail.html', {
        'product': product,
        'recommended_products': recommended,
        # ...
    })

在购物车视图添加推荐:

# cart/views.py
from shop.recommender import Recommender

def cart_detail(request):
    cart = Cart(request)
    # ... 其他代码 ...
    r = Recommender()
    cart_products = [item['product'] for item in cart]
    recommended = r.suggest_products_for(cart_products, 4)
    return render(request, 'cart/detail.html', {
        'cart': cart,
        'recommended_products': recommended,
        # ...
    })

3.4 更新购买数据

在支付成功时记录商品关联:

# payment/views.py
from shop.recommender import Recommender

def payment_process(request):
    # ... 支付处理 ...
    if payment_success:
        order.paid = True
        order.save()

        r = Recommender()
        items = [item.product for item in order.items.all()]
        r.products_bought(items)

总结

本章实现了三个核心功能:基于 session 的优惠券系统,多语言国际化支持,以及基于 Redis 的商品推荐引擎。这些功能显著提升了电商平台的实用性和用户体验。下一章将构建在线教育平台,重点使用 Django 的类视图和内容管理系统。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。