Project-DailyFresh_review

本文介绍了天天生鲜电商平台的开发流程,包括用户、商品、购物车、订单和支付等核心模块的设计与实现,以及如何利用Django框架、Redis、MySQL、FastDFS等技术栈构建高性能系统。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

开发流程

这里写图片描述

项目介绍

  • 天天生鲜项目是一个表b2c的电商网站,售卖海鲜产品。用户可以完成从注册登录,浏览商品页面,查看商品详情页面,评论,将商品添加到购物车,完成购买,查看个人中心等一系列操作,是一个具备完整功能的电商网站。
  • 用户访问流程
    • 域名->Nginx服务器->UWSGI服务器->网页内容
  • 模块
    • 用户模块
      • 实现的页面:
        • register.html:注册页面
        • login.html:登陆页面
        • user_center_info.html:用户中心—个人信息页面
        • user_center_order.html:用户中心-订单详情页面
        • user_center_site.html :用户中心-收货地址页面
      • 实现的功能:
        • 注册
        • 检查用户名是否存在
        • 登录
        • 显示用户个人信息
        • 添加用户收件地址
        • 查询用户订单信息
        • 展示用户最近浏览商品信息
    • 商品模块
      • 实现的页面:
        • index.html:主页面
        • list.html:列表页面
        • details.html:商品详情页面
      • 实现的功能:
        • 展示首页商品
        • 获取某个上品的详情信息并展示
        • 获取某一大类上品信息并分页展示
        • 获取最近商品信息并展示
        • 将商品添加到购物车
    • 购物车模块
      • 实现的页面
        • cart.html:购物车页面
      • 实现的功能
        • 获取用户购物车中的商品信息并展示
        • 对购物车中的商品进行编辑
        • 提交用户想要购买的商品信息并转到提交订单页面
    • 订单模块
      • 实现的页面:
        • place_order.html:确认订单页面
      • 实现的功能
        • 展示用户选中的商品信息
        • 提交订单
    • 支付模块
      • 链接第三方接口
      • 实现的功能
        • 调用第三方接口,返回支付结果,修改订单状态
  • 部署
    • nginx+UWSGI
  • 后端服务
    • 数据服务
    • MySQL:主从同步,双机热备
    • Redis:session,缓存
    • FastDFS :分布式存储服务
    • 异步消息处理
    • celery :异步服务

数据库设计

  • 利用Djangon内嵌的ORM框架,利用面向对象的方式创建操作数据库,一个模型类映射一张数据表
  • 使用MySQL数据库进行存储

需要设计的数据库表

  • 用户模块
    • 用户表
      • 使用Django自带的用户认证系统管理
    • 用户地址表
  • 商品模块
    • 商品类别表
    • 商品SPU表
    • 商品SKU表
    • 商品图片表
    • 主页轮播商品展示表
    • 主页分类上品展示表
    • 主页促销活动展示表
  • 订单模块
    • 订单信息表
    • 订单商品表
  • 购物车模块数据存储在redis中

项目准备

创建项目

  • 1.创建应用并注册
    • 有四个应用:users,goods,cart,orders
  • 2.mysql配置
  • 3.定义模型并迁移
    • 注意users应用中的模型类User使用的是Django自带的用户认证系统维护的

准备静态文件

  • 创建static文件夹,文件夹下有css,js,images,文件夹,和所有的HTML页面
  • 可以将前端写好的静态文件都放在相应的文件夹了
  • 配置静态文件的加载路径

    STATIC_URL = '/static/'
    STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
    

get和post请求

  • 在注册页面,请求注册页面是一个get请求,填写好注册信息,点击提交按钮,是一个post请求

  • 处理注册页面的视图函数

    def register(request):
    """处理注册"""
    
    
    # 获取请求方法,判断是GET/POST请求
    
    if request.method == 'GET':
        # 处理GET请求,返回注册页面
        return render(request, 'register.html')
    else:
        # 处理POST请求,实现注册逻辑
        return HttpResponse('这里实现注册逻辑')
    

类视图

  • 将视图以类的形式定义
  • 需要继承自通用类试图基类 View

  • 示例:定义类视图处理注册逻辑

users/views.py:

class RegisterView(View):
    """类视图:处理注册"""

    def get(self, request):
        """处理GET请求,返回注册页面"""
        return render(request, 'register.html')

    def post(self, request):
        """处理POST请求,实现注册逻辑"""
        return HttpResponse('这里实现注册逻辑')
  • 匹配URL
urlpatterns = [
    # 类视图:注册
    url(r'^register$', views.RegisterView.as_view(), name='register'),
]
  • 类试图相对于视图函数,有更高的复用性,其他地方需要用到某个类视图中的某个逻辑,继承该类试图即可
  • 正常工作中,以实现逻辑为目标,选择性的使用类试图还是视图函数

模板加载静态文件

  • django中加载静态文件的格式和前端的不一样,django是使用static标签加载的
  • django擅长处理动态的业务逻辑,静态的业务逻辑交给nginx来处理

注册登录

  • 需要实现的逻辑
    • 用户注册逻辑
    • 用户激活逻辑
    • 用户登录逻辑

用户注册逻辑

  • 1.发送get请求,返回注册页面
  • 2.提交注册信息,post请求,注册成功后跳转到主页面
    • 1)获取POST对象中的参数
    • 2)判断各个参数是否满足条件,不满足立即返回
    • 3)将用户数据存储到数据库
    • 4)将用户是否激活设置为false
    • 5) 生成激活token(itsdangerous)
    • 6)异步发送激活邮件(celery)
    • 7)重定向到首页

保存用户信息到数据库

  • 为了用户信息的安全,直接使用django提供的用户认证系统完成用户信息的保存
  • 调用creat_user(user_name, email, password)实现用户信息的加密保存
    • 如果重名,会返回integrityError错误
  • 保存完用户注册信息后,需要重置用户激活状态,因为django用户认证系统默认激活状态为True

  • 关键代码:

 # 保存数据到数据库
try:
    # 隐私信息需要加密,可以直接使用django提供的用户认证系统完成
    user = User.objects.create_user(user_name, email, password)
except db.IntegrityError:
    return render(request, 'register.html', {'errmsg': '用户已注册'})

# 手动的将用户认证系统默认的激活状态is_active设置成False,默认是True
user.is_active = False
# 保存数据到数据库
user.save()


用户激活逻辑

  • 激活需要发送邮件,是耗时操作,不应该让注册逻辑卡住
  • 要实现异步发送激活邮件
  • 也就是用户在点击注册按钮后,后台验证完信息,将信息保存到数据库,应该立马将页面跳转到主页面,不能阻塞住。
  • 实现异步发送激活邮件的方法就是–使用celery模块
  • 实现步骤
    • 1.生成激活token
    • 2.celery异步发送激活邮件
生成激活token
  • 使用itsdangerouse模块
  • 步骤
    • 1.调用serializer()方法,生成序列化器(传入混淆字符串和过期时间)
    • 2.序列化器.dumps(),得到user_id加密后的token(传入封装成字典的user_id)
  • 使用loads可以接粗token字符串,得到id明文
  • 代码

    User模型类中:
    from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
    from django.conf import settings
    
    class User(AbstractUser, BaseModel):
    """用户"""
    class Meta:
      db_table = "df_users"
    
    def generate_active_token(self):
      """生成激活令牌"""
      serializer = Serializer(settings.SECRET_KEY, 3600)
      token = serializer.dumps({"confirm": self.id})  # 返回bytes类型
      return token.decode()
    
    视图函数中调用:
    token = user.generate_active_token()
    
celery异步发送邮件
django发送邮件功能
  • django中内置了邮件发送的功能
  • 在django.core.mail模块中。发邮件的方法是:send_mail()
  • 发邮件流程:

    这里写图片描述

  • django内部发邮件设置步骤

    • 1.确定邮件服务器
    • 2.Django中配置邮件服务器参数
    • 3.调用send_mail()发送邮件
celery
  • celery是一个功能完备即插即用的任务队列
  • 是一种跨线程,跨机器工作的机制
  • 组成结构:
    • 客户端(client)
    • 任务队列(broker)
    • 任务处理者(worker)
  • 执行流程:
    • 客户端定义一些任务,如发邮件,将它们放入到中间人(broker)里,需要执行的时候,使用delay()方法,worker就会开始异步执行任务并且返回执行的结果。
  • 特点

    • 简单,易于使用和维护,文档丰富易懂
    • 高效,单个celery进程每分钟可以处理数百万个任务
    • 灵活,celery每个部分都可以自定义拓展
    • celery非常易于集成到一些web框架中
  • 使用步骤

    • 在项目根目录下创建celery_tasks文件夹,文件夹下创建tasks.py
    • 在tasks.py中创建celery应用对象,app
    • 定义需要异步处理的任务,用装饰器@app.task装饰
    • 在视图函数中,在需要异步处理某个耗时操作的时候,调用该任务函数的时候加上delay,完成异步任务处理

    • 代码

    tasks.py:
    from celery import Celery
    from django.core.mail import send_mail
    from django.conf import settings
    
    
    # 创建celery应用对象
    
    app = Celery('celery_tasks.tasks', broker='redis://192.168.243.191:6379/4')
    
    @app.task
    def send_active_email(to_email, user_name, token):
      """发送激活邮件"""
    
      subject = "天天生鲜用户激活"  # 标题
      body = ""  # 文本邮件体
      sender = settings.EMAIL_FROM  # 发件人
      receiver = [to_email]  # 接收人
      html_body = '<h1>尊敬的用户 %s, 感谢您注册天天生鲜!</h1>' \
                  '<br/><p>请点击此链接激活您的帐号<a href="http://127.0.0.1:8000/users/active/%s">' \
                  'http://127.0.0.1:8000/users/active/%s</a></p>' %(user_name, token, token)
      send_mail(subject, body, sender, receiver, html_message=html_body)
    
    
    注册视图函数:
    
    # 生成激活token
    
    token = user.generate_activate_token()
    
    # 发送激活邮件
    
    send_active_email.delay(email, user_name, token)
    
    # 响应给用户,注册后重定向到主页,也不一定到主页,根据公司需求而定
    
    return redirect(reverse('goods:index'))
worker
  • worker是用读取队列任务,执行任务的
  • 实现步骤

    • 1.重新拷贝一份代码到celery服务器上
    • 2.在celery_tasks/tasks.py文件顶部添加以下代码
    import os 
    os.environ["DJANGO_SETTINGS_MODULE"] = "dailyfresh.settings"
    
    # 放到Celery服务器上时添加的代码
    
    import django
    django.setup()
    
  • 3.终端创建worker:celery -A celery_tasks.tasks worker -l info
  • 4.开启redis-server
  • 5.测试发邮件

    • 完整代码:
class RegisterView(View):
    """类视图:处理注册"""

    def get(self, request):
        """处理GET请求,返回注册页面"""
        return render(request, 'register.html')

    def post(self, request):
        """处理POST请求,实现注册逻辑"""

        # 获取注册请求参数
        user_name = request.POST.get('user_name')
        password = request.POST.get('pwd')
        email = request.POST.get('email')
        allow = request.POST.get('allow')

        # 参数校验:缺少任意一个参数,就不要在继续执行
        if not all([user_name, password, email]):
            return redirect(reverse('users:register'))
        # 判断邮箱
        if not re.match(r"^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$", email):
            return render(request, 'register.html', {'errmsg':'邮箱格式不正确'})
        # 判断是否勾选协
        if allow != 'on':
            return render(request, 'register.html', {'errmsg': '没有勾选用户协议'})

        # 保存数据到数据库
        try:
            # 隐私信息需要加密,可以直接使用django提供的用户认证系统完成
            user = User.objects.create_user(user_name, email, password)
        except db.IntegrityError:
            return render(request, 'register.html', {'errmsg': '用户已注册'})

        # 手动的将用户认证系统默认的激活状态is_active设置成False,默认是True
        user.is_active = False
        # 保存数据到数据库
        user.save()

        # 生成激活token
        token = user.generate_active_token()

        # celery发送激活邮件:异步完成,发送邮件不会阻塞结果的返回
        send_active_email.delay(email, user_name, token)

        # 返回结果:比如重定向到首页
        return redirect(reverse('goods:index'))

激活逻辑

  • 当用户点击激活链接时,逻辑如下
    • 1.创建序列化器,解析出返回的token,判断是否过期,如果没有过期,获取user_id
    • 2.判断用户是否存在,不存在为黑客攻击,返回
    • 3.如果存在,修改is_active属性为True
  • 代码:
class ActiveView(View):
    """用户激活"""

    def get(self, request, token):
        # 创建序列化器
        serializer = Serializer(settings.SECRET_KEY, 3600)

        try:
            # 使用序列化器,获取token明文信息,需要判断签名是否过期
            result = serializer.loads(token)
        except SignatureExpired:
            # 提示激活链接已过期
            return HttpResponse('激活链接已过期')

        # 获取用户id
        user_id = result.get('confirm')

        try:
            # 查询需要激活的用户,需要判断查询的用户是否存在
            user = User.objects.get(id=user_id)
        except User.DoesNotExist:
            # 提示用户不存在
            return HttpResponse('用户不存在')

        # 设置激活用户的is_active为Ture
        user.is_active = True
        # 保存数据到数据库
        user.save()

        # 响应信息给客户端
        return redirect(reverse('users:login'))

用户登录逻辑

  • 流程
    • 1.点击登录,get请求,返回登陆页面
    • 2.用户输入的登录信息(用户名和密码),post请求将登录信息传入到后端
    • 3.参数验证
    • 4.调用django自带的用户认证系统authenticate,判断是否登陆成功
    • 5.判断用户是否激活
    • 6.使用django用户认证系统的的login方法,在session中保存用户的登录状态
    • 7.返回重定向页面
class LoginView(View):
    """登陆"""

    def get(self, request):
        """响应登陆页面"""
        return render(request, 'login.html')

    def post(self, request):
        """处理登陆逻辑"""

        # 获取用户名和密码
        user_name = request.POST.get('username')
        password = request.POST.get('pwd')

        # 参数校验
        if not all([user_name, password]):
            return redirect(reverse('users:login'))

        # django用户认证系统判断是否登陆成功
        user = authenticate(username=user_name, password=password)

        # 验证登陆失败
        if user is None:
            # 响应登录页面,提示用户名或密码错误
            return render(request, 'login.html', {'errmsg':'用户名或密码错误'})

        # 验证登陆成功,并判断是否是激活用户
        if user.is_active == False:
            # 如果不是激活用户
            return render(request, 'login.html', {'errmsg':'用户未激活'})

        # 使用django的用户认证系统,在session中保存用户的登陆状态
        login(request, user)

        # 登陆成功,重定向到主页
        return redirect(reverse('goods:index'))

  • 状态保持

    • 用户登陆成功后,需要将用户的登录状态记录下来,即伯村有服务器生成的session数据
    • 浏览器和服务器都要保存用户登录状态

      • 服务器存储session数据
      • 浏览器存储sessionid
    • 如何需要将服务器的session数据存储在redis中,需要配合django—redis模块

    • 步骤
      • 1.安装django-redis:pip install django-redis
      • 2.settings.py中配置django-redis

登录记住用户

  • 为什么要记住用户

    • 不记住用户:用户打开网页-》登录-》关闭网页-》打开网页-》登录–麻烦
    • 记住用户:用户打开网页-》登录-》关闭网页-》打开网页-》直接登录状态–方便
  • 技术分析

    • 服务器redis数据库中存储了用户的session信息
    • 浏览器cookie中,存储了用户的sessonid信息
    • 每次请求,浏览器都会带上完整的cookies信息给服务器
    • 则:只要更改session的过期时间即可,request.session.set_expiry(value)
      • value:整数,表示value秒后过期
      • value:0,表示浏览器关闭后过期
      • value:none:表示两个星期后过期
  • 代码:

     # 获取是否勾选'记住用户名'
    remembered = request.POST.get('remembered')
    
    
    # 判断是否是否勾选'记住用户名'
    
    if remembered != 'on':
        # 没有勾选,不需要记住cookie信息,浏览器关闭即过期
        request.session.set_expiry(0)
    else:
        # 已勾选,需要记住cookie信息,两周后过期
        request.session.set_expiry(None)
    

退出登录

class LogoutView(View):
    """退出登录"""

    def get(self, request):
        """处理退出登录逻辑"""

        # 由Django用户认证系统完成:需要清理cookie和session,request参数中有user对象
        logout(request)
        # 退出后跳转:由产品经理设计
        return redirect(reverse('goods:index'))
  • get请求
  • 使用django用户系统提供的logout()函数
  • 函数内部会进行清除session和cookie的操作

用户中心

用户是否登录的验证

  • 要想进入用户中心页面,那么这个用户就必须要登录,怎么验证登录呢?
  • 使用django内置的login_required方法

    • 该方法可以验证用户是否通过登录验证,如果已经登录,则返回True,否则返回False
    • 在django.contrib.auth.decorator中
  • 使用步骤

    • 1.主目录下的utils(工具集)中的views.py(有关于视图的工具类)中创建类loginRequireMixin类
    class LoginRequiredMixin(object):
    @classmethod
    def as_view(cls, **initkwargs):
        view = super().as_view(**initkwargs)
        return login_required(view)
    
    • 2.在需要验证的类视图里,继承loginRequriedMixin类即可
  • next参数

    • 用来标记,用户如果登陆或者访问目标页面失败,将要定向到的页面,如用户没有登陆就想访问用户信息页面,就会使用next参数重定向到该页面,具体重定向到哪个页面,由产品径流说了算

用户地址页

  • 页面分析:有get请求和post请求
    • get:根据根据获取到的用户ID从数据库中获取该用户的地址地址信息,放在上下文,传给模板,以一定的顺序(创建时间) 渲染排列出来
    • post:用户想要添加新的地址,使用表单提交,提交后,后端,验证接收数据,验证参数,将地址保存到数据库,重定向到地址页面,get请求

个人信息页面

  • 展示用户基本信息(get请求)和最近浏览记录
  • 最近浏览记录

    • 从redis中取出
    • 步骤
      • 创建redis链接对象,redis_connection = get_redis_connection('default')
      • 根据需求的用户id,获取用户浏览上品的sku_id列表,sku_ids = redis_connection.lrange('history_%s'%user_id,0,4)
      • 从数据库中查询出sku_ids对用的sku_list
      • 存储到上下文中返回
    • 代码:
    class UserInfoView(LoginRequiredMixin, View):
    """用户中心"""
    
    def get(self, request):
      """查询用户信息和地址信息"""
    
      # 从request中获取user对象,中间件从验证请求中的用户,所以request中带有user
      user = request.user
    
      try:
          # 查询用户地址:根据创建时间排序,取第1个地址
          address = user.address_set.latest('create_time')
      except Address.DoesNotExist:
          # 如果地址信息不存在
          address = None
    
      # 创建redis连接对象
      redis_connection = get_redis_connection('default')
      # 从Redis中获取用户浏览商品的sku_id,在redis中需要维护商品浏览顺序[8,2,5]
      sku_ids = redis_connection.lrange('history_%s'%user.id, 0, 4)
      # 从数据库中查询商品sku信息,范围在sku_ids中
      # skuList = GoodsSKU.objects.filter(id__in=sku_ids)
      # 问题:经过数据库查询后得到的skuList,就不再是redis中维护的顺序了,而是[2,5,8]
      # 需求:保证经过数据库查询后,依然是[8,2,5]
      skuList = []
      for sku_id in sku_ids:
          sku = GoodsSKU.objects.filter(id=sku_id)
          skuList.append(sku)
    
      # 构造上下文
      context = {
          'address':address,
          'skuList':skuList,
      }
    
      # 调出并渲染模板
      return render(request, 'user_center_info.html', context)
    
    

商品模块

FastDFS

  • 分布式文件存储系统,方便的进行网站图片等信息的存储
  • 解决了大容量存储和负载均衡的问题
  • 适合存储图片,视屏等
  • 组成

    • client:上传下载的请求者
    • tracker:负载均衡存储器,负责对存储器的调度
    • storage:存储信息的结构
  • 步骤

    • 上传
      • client询问tracker,有上传文件的需求
      • tracker返回一台可用的storage
      • client直接和storage进行通讯,完成文件的存储
    • 下载
      • client询问tracker,有下载文件的需求
      • tracker返回一台可用的storage
      • client直接和storage通讯完成文件的下载

主商品页面

  • 数据分析
    • 个人信息
    • 商品分类信息
    • 图片轮播图信息
    • 活动信息
    • 少年宫品SKU信息
    • 购物车信息
  • 当用户访问主页/跳转到主页时,get请求,会在数据库中找到以上信息,填充模板,返还给浏览器

页面静态化

  • 主页,信息丰富,需要多次查询,才能获取到全部信息,这样就会反应很慢
  • 解决办法–页面静态化,通过Nginx访问主页

    • 将主页的html页面先存储起来,需要访问的时候,直接返回完整的HTMl页面
    • 实现思路
      • 后台站点在发布主页内容(增删改),Django使用异步任务的方式生成静态页面
      • 其中:
      • 使用celery服务器执行异步任务
      • 使用Nginx服务器提供静态页面的访问
      • 需要注册模型类到站点,并且创建模型类管理类,在模型类管理类中调用celery异步任务
    • 实现步骤

      • 1.定义异步任务
      @app.task
      def generate_static_index_html():
          """生成静态的html页面"""
      
          # 查询商品分类信息
          categorys = GoodsCategory.objects.all()
      
          # 查询图片轮播信息:按照index进行排序
          banners = IndexGoodsBanner.objects.all().order_by('index')
      
          # 查询活动信息
          promotion_banners = IndexPromotionBanner.objects.all().order_by('index')
      
          # 查询分类商品信息
          for category in categorys:
              title_banners = IndexCategoryGoodsBanner.objects.filter(category=category, display_type=0).order_by('index')
              category.title_banners = title_banners
      
              image_banners = IndexCategoryGoodsBanner.objects.filter(category=category, display_type=1).order_by('index')
              category.image_banners = image_banners
              print(category.image_banners[0].sku.default_image.url)
      
          # 查询购物车信息
          cart_num = 0
      
          # 构造上下文
          context = {
              'categorys': categorys,
              'banners': banners,
              'promotion_banners': promotion_banners,
              'cart_num': cart_num
          }
      
          # 加载模板
          template = loader.get_template('static_index.html')
          html_data = template.render(context)
      
          # 保存成html文件:放到静态文件中
          file_path = os.path.join(settings.STATICFILES_DIRS[0], 'index.html')
          with open(file_path, 'w') as file:
              file.write(html_data)
      
      • 2.配置Nginx访问静态页面
      • 3.模型管理类调用celery异步方法
        • 这是生成静态页面的发起点
        • 管理员在通过站点发布内容时,在这里会调用celery异步方法,生成HTML静态页面
           class BaseAdmin(admin.ModelAdmin):
          """商品活动信息的管理类,运营人员在后台发布内容时,异步生成静态页面"""
      
          def save_model(self, request, obj, form, change):
              """后台保存对象数据时使用"""
      
              # obj表示要保存的对象,调用save(),将对象保存到数据库中
              obj.save()
              # 调用celery异步生成静态文件方法
              generate_static_index_html.delay()
      
          def delete_model(self, request, obj):
              """后台保存对象数据时使用"""
              obj.delete()
              generate_static_index_html.delay()
      
      class IndexPromotionBannerAdmin(BaseAdmin):
          """商品活动站点管理,如果有自己的新的逻辑也是写在这里"""
          # list_display = []
          pass
      
      class GoodsCategoryAdmin(BaseAdmin):
          pass
      
      class GoodsAdmin(BaseAdmin):
          pass
      
      class GoodsSKUAdmin(BaseAdmin):
          pass
      
      class IndexCategoryGoodsBannerAdmin(BaseAdmin):
          pass
      
      
      # Register your models here.
      
      admin.site.register(GoodsCategory,GoodsCategoryAdmin)
      admin.site.register(Goods,GoodsAdmin)
      admin.site.register(GoodsSKU,GoodsSKUAdmin)
      admin.site.register(IndexPromotionBanner,IndexPromotionBannerAdmin)
      admin.site.register(IndexCategoryGoodsBanner,IndexCategoryGoodsBannerAdmin)
      

缓存

  • 用户未登录访问,主页,使用静态页面的方式,加快加载速度,用户登录后,访问主页,就要是动态访问,这种情况下,速度还是会很慢,怎么解决?–缓存
  • 缓存时对动态查询数据的一种存储,可以存储在系统内存中,也可以存储在缓存数据库中
  • 用户在访问页面时,会先尝试获取缓存数据,如果获取到,直接返回数据,获取不到,就查询数据库
  • 存储进去的是什么,获取到的就是什么
  • 关键代码:

    
    # 模块:
    
    from django.core.cache import cache
    
    # 设置缓存:
    
    cache.set('key', 内容, 有效期)
    
    # 读取缓存
    
    cache.get('key')
    
    # 删除缓存
    
    cache.delete('key')
    
  • 代码(缓存主页数据)

    class IndexView(View):
    """首页"""
    
    def get(self, request):
        """查询首页页面需要的数据,构造上下文,渲染首页页面"""
    
        # 查询用户个人信息(request.user)
    
        # 先从缓存中读取数据,如果有就获取缓存数据,反之,就执行查询
        context = cache.get('index_page_data')
    
        if context is None:
            print('没有缓存数据,查询了数据库')
            # 查询商品分类信息
            categorys = GoodsCategory.objects.all()
    
            # 查询图片轮播信息:按照index进行排序
            banners = IndexGoodsBanner.objects.all().order_by('index')
    
            # 查询活动信息
            promotion_banners = IndexPromotionBanner.objects.all().order_by('index')
    
            # 查询分类商品信息
            for category in categorys:
                title_banners = IndexCategoryGoodsBanner.objects.filter(category=category, display_type=0).order_by('index')
                category.title_banners = title_banners
    
                image_banners = IndexCategoryGoodsBanner.objects.filter(category=category, display_type=1).order_by('index')
                category.image_banners = image_banners
    
            # 构造上下文:先处理购物车以外的上下文,并缓存
            context = {
                'categorys':categorys,
                'banners':banners,
                'promotion_banners':promotion_banners,
            }
    
            # 设置缓存数据:名字,内容,有效期
            cache.set('index_page_data',context,3600)
    
        # 查询购物车信息:不能被缓存,因为会经常变化
        cart_num = 0
    
        # 补充购物车数据
        context.update(cart_num=cart_num)
    
        return render(request, 'index.html',context)
    
    
    • 注意:
    • 1.缓存需要设置有效期,不然数据永远无法得到更新,具体的有效期时间根据公司需求而定
    • 2.缓存在修改内容时需要删除,不然内容修改了,缓存的还是旧内容

        class BaseAdmin(admin.ModelAdmin):
      """商品活动信息的管理类,运营人员在后台发布内容时,异步生成静态页面"""
      
      def save_model(self, request, obj, form, change):
        """后台保存对象数据时使用"""
      
        # obj表示要保存的对象,调用save(),将对象保存到数据库中
        obj.save()
        # 调用celery异步生成静态文件方法,操作完表单后删除静态文件
        generate_static_index_html.delay()
        # 修改了数据库数据就需要删除缓存
        cache.delete('index_page_data')
      
      def delete_model(self, request, obj):
        """后台保存对象数据时使用"""
        obj.delete()
        generate_static_index_html.delay()
      cache.delete('index_page_data')
      
      

购物车数据

  • 对于用户的购物车浏览记录,因为一直在变化,所以不能存储在缓存中,那么存储在哪里?
  • redis中,因为redis是一种存储在内存中的数据库,快捷方便
  • 存储方式

    • 每个用户用一条购物车数据维护
    • 使用哈希类型
  • 步骤

    • 1.先判断用户是否登录,登陆了,才能获取到购物车数据
    • 2.创建django-redis中的redis_conn对象
    • 3.获取用户id
    • 4.从redis中获取购物车数据,返回字典
    • 5.遍历购物车字典的值,累加购物车内的商品的值(用于页面显示)
    
    # 如果用户登录,就获取购物车数据
    
        if request.user.is_authenticated():
            # 创建redis_conn对象
            redis_conn = get_redis_connection('default')
            # 获取用户id
            user_id = request.user.id
            # 从redis中获取购物车数据,返回字典
            cart_dict = redis_conn.hgetall('cart_%s'%user_id)
            # 遍历购物车字典的值,累加购物车的值
            for value in cart_dict.values():
                cart_num += int(value)
    
    

商品详情页

  • 参数:需要商品的sku_id
  • 查询的内容
    • 查询上品的SKU信息
    • 查询所有商品的分类信息
    • 查询商品的订单评论信息
    • 查询最新商品推荐
    • 查询其他规格商品
    • 如果已经登录,查询购物车信息
  • 注意
    • 检查是否有缓存,有缓存,使用缓存
    • 在商品详情页需要实现存储浏览记录的逻辑(存储在redis中)
class DetailView(View):
    """商品详细信息页面"""

    def get(self, request, sku_id):
        # 尝试获取缓存数据
        context = cache.get("detail_%s" % sku_id)

        # 如果缓存不存在
        if context is None:
            try:
                # 获取商品信息
                sku = GoodsSKU.objects.get(id=sku_id)
            except GoodsSKU.DoesNotExist:
                # from django.http import Http404
                # raise Http404("商品不存在!")
                return redirect(reverse("goods:index"))

            # 获取类别
            categorys = GoodsCategory.objects.all()

            # 从订单中获取评论信息
            sku_orders = sku.ordergoods_set.all().order_by('-create_time')[:30]
            if sku_orders:
                for sku_order in sku_orders:
                    sku_order.ctime = sku_order.create_time.strftime('%Y-%m-%d %H:%M:%S')
                    sku_order.username = sku_order.order.user.username
            else:
                sku_orders = []

            # 获取最新推荐
            new_skus = GoodsSKU.objects.filter(category=sku.category).order_by("-create_time")[:2]

            # 获取其他规格的商品
            other_skus = sku.goods.goodssku_set.exclude(id=sku_id)

            context = {
                "categorys": categorys,
                "sku": sku,
                "orders": sku_orders,
                "new_skus": new_skus,
                "other_skus": other_skus
            }

            # 设置缓存
            cache.set("detail_%s"%sku_id, context, 3600)

        # 购物车数量
        cart_num = 0
        # 如果是登录的用户
        if request.user.is_authenticated():
            # 获取用户id
            user_id = request.user.id
            # 从redis中获取购物车信息
            redis_conn = get_redis_connection("default")
            # 如果redis中不存在,会返回None
            cart_dict = redis_conn.hgetall("cart_%s"%user_id)
            for val in cart_dict.values():
                cart_num += int(val)

            # 浏览记录: lpush history_userid sku_1, sku_2
            # 移除已经存在的本商品浏览记录
            redis_conn.lrem("history_%s"%user_id, 0, sku_id)
            # 添加新的浏览记录
            redis_conn.lpush("history_%s"%user_id, sku_id)
            # 只保存最多5条记录
            redis_conn.ltrim("history_%s"%user_id, 0, 4)

        context.update({"cart_num": cart_num})

        return render(request, 'detail.html', context)

商品列表页

  • 分析

    • 需要知道展示是哪一类商品
    • 需要展示的是第几页
    • 需要知道排序的规则(默认?价格?人气?)
    • 请求方法是get,只需要获取数据
    • 传递参数(排序规则)到视图中,需要进行参数校验
  • 需要查询的数据

    • 购物车数据
    • 上品分类信息
    • 新推荐信息,在GoodsSKU表中,查询特定类别信息,按照时间倒序
    • 商品列表信息
    • 上品分页信息
  • 参数传递方式

    • 展示某商品第几页的数据,然后再排序
    • /list/category_id/page_num/?sort='默认,价格,人气'
  • 提示

    • 1.获取请求参数信息:商品id,第几页数据 ,排序规则
    • 2.校验参数
      • 1)判断类别是否存在,查询数据库验证,如果不存在,异常:GoodCategory.DoesNotExist
      • 2)分页的异常,在创建分页对象时校验
        • 因为只有创建了分页数据,才能知道页数page是否正确
        • 如果页数错误,异常为:EmptyPage
class ListView(View):
    """商品列表"""

    def get(self, request, category_id, page_num):

        # 获取sort参数:如果用户不传,就是默认的排序规则
        sort = request.GET.get('sort', 'default')

        # 校验参数
        # 判断category_id是否正确,通过异常来判断
        try:
            category = GoodsCategory.objects.get(id=category_id)
        except GoodsCategory.DoesNotExist:
            return redirect(reverse('goods:index'))

        # 购物车
        cart_num = 0
        # 如果是登录的用户
        if request.user.is_authenticated():
            # 获取用户id
            user_id = request.user.id
            # 从redis中获取购物车信息
            redis_conn = get_redis_connection("default")
            # 如果redis中不存在,会返回None
            cart_dict = redis_conn.hgetall("cart_%s" % user_id)
            for val in cart_dict.values():
                cart_num += int(val)

        # 查询商品所有类别
        categorys = GoodsCategory.objects.all()

        # 查询该类别商品新品推荐
        new_skus = GoodsSKU.objects.filter(category=category).order_by('-create_time')[:2]

        # 查询该类别所有商品SKU信息:按照排序规则来查询
        if sort == 'price':
            # 按照价格由低到高
            skus = GoodsSKU.objects.filter(category=category).order_by('price')
        elif sort == 'hot':
            # 按照销量由高到低
            skus = GoodsSKU.objects.filter(category=category).order_by('-sales')
        else:
            skus = GoodsSKU.objects.filter(category=category)
            # 无论用户是否传入或者传入其他的排序规则,我在这里都重置成'default'
            sort = 'default'

        # 分页:需要知道从第几页展示
        page_num = int(page_num)

        # 创建分页器:每页两条记录
        paginator = Paginator(skus,2)

        # 校验page_num:只有知道分页对对象,才能知道page_num是否正确
        try:
            page_skus = paginator.page(page_num)
        except EmptyPage:
            # 如果page_num不正确,默认给用户第一页数据
            page_skus = paginator.page(1)

        # 获取页数列表
        page_list = paginator.page_range

        # 构造上下文
        context = {
            'sort':sort,
            'category':category,
            'cart_num':cart_num,
            'categorys':categorys,
            'new_skus':new_skus,
            'page_skus':page_skus,
            'page_list':page_list
        }

        # 渲染模板
        return render(request, 'list.html', context)

商品搜索

  • 搜素引擎和框架

    • whoosh
      • 纯python编写的全文搜索引擎
      • 性能比不上sphinx,xapian,Elasticsearc,但是无二进制包,程序不会莫名奇妙的崩溃,对于小型站点,whoosh已经够用
    • haystack
      • 全文检索框架,支持whoosh,solr,Xapian,Elasticsearc四种全文检索引擎
      • 作用:搭建了用户和搜索引擎之间的桥梁
    • jieba
      • 免费的中文分词包,用来对输入的中文关键字进行分词处理
  • 步骤

    • 配置全文检索
      • 1.pip安装 django-haystack,whoosh,jieba
      • 2.在settings.py中的INSTALLED_APPS配置项中,添加‘haystack‘;配置haystack和whoosh的链接
    • 定义商品索引类
    • 指定要建立索引的字段
    • 生成索引文件
    • 搜索表单处理
    • 配置搜索地址正则

购物车

  • 使用redis数据库存储购物车数据
  • 购物车里需要完成增删改查的操作
  • 查询的结果,需要由服务器响应界面给客户端展示出来
  • 增删改的操作,是客户端发数据给服务器,连着之间的交互是局部刷新的效果,需要使用ajax
  • 添加购物车的请求方法:POST
  • 服务器和客户端的数据传输格式:json
  • 服务器接收到的数据

    • 用户id:user_id
    • 商品id:sku_id
    • 商品数量:count
  • 当用户已登录时,将购物车数据存储到服务器的redis中

  • 当用户未登录时,将购物车数据存储到浏览器的cookie中
    • 使用json字符串将购物车数据保存到浏览器的cookie中
    • 每个人的浏览器cookie存储的都是个人的购物车数据,所以key不用唯一标示
  • 当用户进行登录是,将cookie中的购物车数据合并到redis中

  • 由于主页,详情页,列表页,中,都涉及到购物车的数据展示,所以将购物车逻辑封装到BaseCartView中

登录时,购物车合并cookie和redis

  • 在登陆页面跳转前,将cookie和redis的购物车数据合并到redis中
    • 步骤:
    • 获取cookie中的购物车数据
    • 获取redis中的购物车数据
    • 合并购物车上商品数量信息
      • 如果cookie中存在,redis中也有,则进行数量累加
      • 如果cookie中存在的,redis中没有,则生成新的购物车数据
    • 将cookie中的购物车数据合并到redis中
    • 清除浏览器购物车cookie

订单

  • 页面入口
    • 点击详情页立即购买进入订单确认页面
    • 点击购物车去结算 进入订单确认页面
    • 点击订单确认页面提交订单进入全部订单页面
    • 点击全部订单去付款进入支付宝

提交订单之事务支持

  • Django中的数据库,默认是自动提交的
  • 当OrderInfo和OrderGoods保存数据时,如果出现异常,需要执行回滚,不要自动提交
  • 保存数据时,只有当没有任何错误时,才能完成数据的保存
  • 要么一起成功,要么一起失败

  • 实现步骤

    • 定义TransactionAtomicMixin类
    • 在类试图中继承 transactionAtomcMixin类
    • 在创建数据库祈前创建事务保存点:save_point = transaction.savepoint()
    • 出现异常的地方都回滚到事务保存点
    • 当数据库操作结束,还没有异常时,才能提交事务:transaction.savepoint_commit(savepoint)

提交订单之并发和锁

  • ?:多线程和多进程访问共享资源时,容易出现资源抢夺的问题

    • 解决办法:加锁(悲观锁/乐观锁)
    • 悲观锁
      • 当要操作某条记录时,立即将该条记录锁起来,谁也无法操作
      • select * from table where id=17 for update;
    • 乐观锁

      • 在查询数据的时候不加锁,在更新时进行判断
      • 判断更新时的库存和之前查出来的库存是否一致
      • update table set set stock=2 where id=17 and stock=7;
    • 代码:没有使用锁

    
    # 减少sku库存
    
    sku.stock -= sku_count
    
    # 增加sku销量
    
    sku_sales += sku_count
    sku.save()
    
    
    • 代码:使用乐观锁
    
    # 查询当前库存量
    
    origin_stock = sku.stock
    
    # 减少库存,增加销量
    
    new_stock = origin_stock - sku_count
    new_sales = sku_sales + sku_count
    
    # 更新库存和销量
    
    result = GoodsSKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales)
    
    

支付宝

提交订单到支付宝

  • 点击确认支付按钮,跳转到支付宝支付页面
  • 步骤
    • 用户向服务器传递订单id,当做请求参数传入
    • 服务器接收订单id,并校验订单id
    • 获取要支付的订单的信息,order,并校验
    • 创建用于对接支付宝的支付的对象,alipay
    • 创建支付接口对应的请求信息,order_string
    • 生成访问支付宝的支付地址
class PayView(LoginRequiredJSONMixin, View):
    """订单支付"""

    def post(self, request):
        # 订单id

        # 校验订单

        # 获取订单信息

        # 创建用于支付宝支付的对象

        # 电脑网站支付,需要跳转到https://openapi.alipay.com/gateway.do? + order_string

        # 生成url:让用户进入支付宝页面的支付网址

        # 响应结果
准备url
# 支付请求
url(r'^pay$', views.PayView.as_view(), name='pay')
准备视图
class PayView(LoginRequiredJSONMixin, View):
    """订单支付"""

    def post(self, request):
        # 订单id
        order_id = request.POST.get('order_id')

        # 校验订单
        if not order_id:
            return JsonResponse({'code': 2, 'message': '订单id错误'})

        # 获取订单信息
        try:
            order = OrderInfo.objects.get(order_id=order_id, user=request.user,
                                          status=OrderInfo.ORDER_STATUS_ENUM["UNPAID"],
                                          pay_method=OrderInfo.PAY_METHODS_ENUM["ALIPAY"])

        except OrderInfo.DoesNotExist:
            return JsonResponse({'code': 3, 'message': '订单错误'})

        # 创建用于支付宝支付的对象
        alipay = AliPay(
            appid=settings.ALIPAY_APPID,
            app_notify_url=None,  # 默认回调url
            app_private_key_path=os.path.join(settings.BASE_DIR,'apps/orders/app_private_key.pem'),
            alipay_public_key_path=os.path.join(settings.BASE_DIR,'apps/orders/alipay_public_key.pem'),  # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
            sign_type = "RSA2",  # RSA 或者 RSA2
            debug = True  # 默认False 配合沙箱模式使用
        )

        # 电脑网站支付,需要跳转到https://openapi.alipay.com/gateway.do? + order_string
        order_string = alipay.api_alipay_trade_page_pay(
            out_trade_no=order_id,
            total_amount=str(order.total_amount), # 将浮点数转成字符串
            subject='头条生鲜',
            return_url=None,
            notify_url=None  # 可选, 不填则使用默认notify url
        )

        # 生成url:让用户进入支付宝页面的支付网址
        url = settings.ALIPAY_URL + '?' + order_string
        return JsonResponse({'code': 0, 'message': '支付成功', 'url':url})

查询支付状态

  • 使用支付对象的api_alipay_trade_query(order_id),查询订单完成状态
  • 根据返回的状态进行操作,如果正在进行中,继续循环请求,如果支付成功,更改数据库中订单状态信息,返回给前端,订单支付成功,如果订单支付失败,直接返回信息给前端,说明订单支付失败
   while True:
            # 查询支付结果:返回字典
            response = alipay.api_alipay_trade_query(order_id)

            # 判断支付结果
            code = response.get('code') # 支付宝接口调用结果的标志
            trade_status = response.get('trade_status') # 用户支付状态

            if code == '10000' and trade_status == 'TRADE_SUCCESS':
                # 表示用户支付成功
                # 设置订单的支付状态为待评论
                order.status = OrderInfo.ORDER_STATUS_ENUM['UNCOMMENT']
                # 设置支付宝对应的订单编号
                order.trade_id = response.get('trade_no')
                order.save()

                 # 返回json,告诉前端结果
                return JsonResponse({'code': 0, 'message': '支付成功'})

            elif code == '40004' or (code == '10000' and trade_status == 'WAIT_BUYER_PAY'):
                # 表示支付宝的接口暂时调用失败,网络延迟,订单还未生成;or 等待订单的支付
                # 继续查询
                continue
            else:
                # 支付失败,返回支付失败的通知
                return JsonResponse({'code': 4, 'message': '支付失败'})

部署

  • 当项目开发完成后,需要将项目放到服务器上
  • 让服务器使用固定的ip,再通过绑定域名,就可以供其他人浏览了
  • 对于python web开发,可以使用wsgi,apache服务器
  • 部署需要安装的软件:uWSGI,Nginx
  • 访问流程
    • 浏览器发送请求给Nginx服务器,如果是静态文件,则nginx会直接返回
    • 如果不是静态页面,nginx会把请求信息转给uWSGI服务器,uWSGI将请求转给django框架,url路由到相应的视图函数,处理完逻辑,返回响应结果经由uWSGI和nginx在到浏览器

django对接MySQL主从

  • 对mysql进行读写分离设置,提高数据库的访问速度
  • 步骤

    • 1.增加主从数据库
    DATABASES = {
      'default': {
          'ENGINE': 'django.db.backends.mysql',
          'NAME': 'dailyfresh',
          'HOST': '192.168.243.193', # MySQL数据库地址(主)
          'PORT': '3306',
          'USER': 'root',
          'PASSWORD': 'mysql',
      },
      'slave': {
              'ENGINE': 'django.db.backends.mysql',
              'NAME': 'dailyfresh',
              'HOST': '192.168.243.189', # MySQL数据库地址(从)
              'PORT': '3306',
              'USER': 'root',
              'PASSWORD': 'mysql',
          }
    }
    
    • 2.编辑路由分发的类,在根目录下的utils目录下常见db_router.py
    class MasterSlaveDBRouter(object):
      """读写分离路由"""
    
      def db_for_read(self, model, **hints):
          """读"""
          return 'slave'
    
      def db_for_write(self, model, **hints):
          """读"""
          return 'default'
    
      def allow_relation(self, obj1, obj2, **hints):
          """是否允许关联查询"""
          return True
    
    • 3.读写分离引导:settings.py
     # 配置读写分离
    DATABASE_ROUTERS = ['utils.db_router.MasterSlaveDBRouter']
    
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值