Python Django 商城项目总结

以下转载于https://www.cnblogs.com/cerofang/p/9457875.html   仅供本人学习和研究

商城商业模式:
C2B模式(消费者到企业的商业模式),相类似网站包括:京东,淘宝,海尔商城,尚品宅配等。

商城需求分析
1,用户部分
2,商品部分
3,购物车部分
4,商品订单备份
5,用户支付部分
6,上线程序的配置

用户部分模块:

基本功能:用户注册,登录,密码的重置,第三方登录
用户注册
1,图片验证码
流程分析:
1,前端生成uuid随机字符串
2,后端生成图片验证码发送给前端,将图形验证码的存入到redis中
2,短信验证码
1,检查图片的验证码
2,检验是否是在60s内是否已经发送过
3,生成短信验证码
4,保存发送的短信验证码
5,发送短信(第三方平台发送:云通讯)
3,判断用户名是否存在
1,用户输入用户名之后ajax局部刷新页面
2,后台查询数据库用户是否存在
3,返回数据给前端
4,判断手机号码是否已经存在
同3

技术点:前后端的域名不相同,涉及到csrf跨站请求伪造的问题;

  • Csrf相关概念:
    1,域=协议+域名+端口,在两个域中,以上三者中任意一个条件不同,均涉及到跨域的问题;
    2,浏览器的策略
    1,对于简单的请求,浏览器发送请求,但是得到请求之后会检验响应头中是否有当前的域中,如果没有则会在浏览器中报错;
    2,对于复杂的请求,浏览器会先发送一个option请求,询问服务器是否支持跨域,如果响应头中的域名允许,才会发送相对应的请求来获取数据,并交给js进行处理。
    3,Python的django中的跨域处理的相关模块django-cors-headers

技术点:前端用户将图片验证码发送给后台之后,第三方平台发送短信的过程中会有网络的阻塞程序继续往下执行,进而影响用户体验效果;

  • 解决方案:采用celery进行短信验证码的异步发送;
    Celery概念:分布式异步任务队列调度框架:
    1,支持定时任务和异步任务两种方式
    2,组成:大概分为四个部分client客户端发送数据,broker中间件(redis数据库,消息队列),worker(任务的执行者),backend(执行worker任务的执行结果)
    3,可以开启多进程也可以是多线程
    4,应用场景:在某一个任务的执行过程中,会涉及到耗时的操作,但是这个耗时操作并不会影响后续的程序的执行,此时就可以用celery来异步执行这些任务;

用户登录

JWTtoken的相关了解

  • cookies的使用目的

    • http协议本生是一种无状态的协议,假如用户每次发送请求过来将用户名和密码在后端进行验证后才可以登录到某些界面才可以进行操作,当客户端再次请求服务器时,又要重新进行认证;
    • 解决方法:在客户端设置cookie并在本地设置session存储用户的敏感信息,从而来记录当前用户登录的状态,如果用户量再次请求服务器,将cookie带给服务器,服务器查询session获取用户信息,进行下一步操作。
    • 客户端比较多的情况下,seession中默认会存在内存,随着用户量的增加服务器的压力就会变大;
  • 传统的cookies显示出来的问题
    - 在现在的市场各种不同的浏览器,对同一个网站进行,用户的每种设备都需要维护相关的session在服务器端,会造成服务器资源的浪费,相关网站采取单点登录来记录用户的状态状态来解决以上传统cookies带来的问题

  • 单点登录的概念

    • 用户在不同的设备中进行登录,服务器端不用维护用户的相关信息,在每次登录的过程中都由客户端将自己的用户信息发送过来,服务端进行解析来获取用户的相关信息
  • token认证的机制

    • 用户携带用户名和密码来后端进行验证
    • 服务器端验证通过后对为当前用户生成token
    • 将token返回给前端,记录用户的信息
    • 用户再次请求服务器的时候,服务端解析token相关的信息,验证用户
    • 确定用户状态,进行 相关操作
  • 备注:

    • jwt的组成第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
    • secretkey是存储在服务器端的,如果secret key
  • 用户登录JWT认证的的流程源代码(待继续理解)

  • qq登录

  • qq登录流程oauth2的认证流程分析
    附件0.00KB

    • 用户向美多网站发送qq注册的请求
    • 美多网站向用户返回qq登录的页面
    • 用户在qq登录的界面向qq的服务器发送qq用户名和密码发起登录的请求
    • qq服务器认证成功之后将用户引导到回调的网址中,并返回给用户qq服务器的token值
    • 用户重定向到美多页面并携带了qq服务器发送的token
    • 后端接收到token后向qq服务器请求access token
    • qq服务器返回access token
    • 美多服务器通过access token来向qq服务器来获取用户的openid
    • 通过id来查询用户是否已经在美多商城注册
    • 用户已经注册直接返回用户的access token值
    • 用户没有账号,生成注册的access token,(载荷openid)重新注册信息发送给后端
    • 后端接收到数据之后创建对象并将qq用户的openid和账号进行绑定
  • 忘记密码的功能的实现

    • 用户发送过来请求,携带用户名和图片验证码
    • 后端验证图片验证码,通过账号查询用户,将用户的电话号码部分返回给前端,并生成发送短信的access token(载荷mobile)值
    • 前端填写手机号码验证码并携带access token到后端
    • 后端接收到手机号码校验(正确性,发送时间间隔),通过手机号码站到用户对象,生成密码修改的access token(载荷uer obj)值
    • 前端用户填写新的密码之后,携带access token到后端重置密码

技能点

  • djangorestframework中的实现JWT token的模块itsdangerous的使用

用户中心

  • 个人信息

  • 个人信息是用户的私有信息,必须是登录用户才可以访问,并且值可以访问自己的相关信息

    • 用户个人信息的展示流程
    • 前端在页面被加载完成之后向后端发送请求用户数据
    • 后端通过rest_framework.permissions.IsAuthenticated判断用户是否登录,并获取用户的user模型
    • 将用户的详细信息返回给前端,在前端进行展示
  • 用户个人中心的信息中有一项是用户的邮箱是否激活

  • 邮箱验证的流程

    • 用户填入邮箱点击保存后端接收到邮箱后异步发出邮件,链接中包含access token(载荷uer id& email)
    • 邮件中包含token值,在用户点击邮件中的链接之后向前端发送激活的请求
    • 后端验证access token合法性,DRF中的序列化器update的方法,在序列化器中create方法中将用户的email字段更改为激活状态
    • 将用户的对象返回给前端

技术点:

  • django中发送邮件的配置信息
  • 利用celery来实现异步的邮件发送

用户收货地址的设置

  • DRF中序列化器的嵌套
from rest_framework import serializers

from .models import Area


class AreaSerializer(serializers.ModelSerializer):
    """
    行政区划信息序列化器
    """
    class Meta:
        model = Area
        fields = ('id', 'name')


class SubAreaSerializer(serializers.ModelSerializer):
    """
    子行政区划信息序列化器
    """
    subs = AreaSerializer(many=True, read_only=True)

    class Meta:
        model = Area
        fields = ('id', 'name', 'subs')
  • DRF中的ReadOnlyModelViewSet中将请求方式与资源状态进行了绑定,在这里我们只需要从数据库中去数据所以直接就可以继承ModelSerializer这个类
  • view视图中的action=='list'(即前端url不带参数)说明前端要获取所有的省份
  • view视图中的action!='list'(即前端url带参数)说明前端要获取所有的省份底下的行政区划
  • 在这个返回的过程中如果前端页面返回的url中返回的带有参数则返回省份

技术点

  • DRF的扩展类中的选择以及序列化器的嵌套调用方法
  • 对DRF的扩展集的理解
  • Views django 中的原始的Views
  • APIView继承类django中的Views,同时提供了用户认证,权限认证,权限认证,节流认证,分页,序列化等方法
  • GenericAPIView 继承了APIView:在这个类中实现了两个类实行和三个类方法
  • ListModelMixin 实现了list方法与get_queryset(),paginate_queryset,get_serializer
  • ListAPIView 可用的子类GenericAPIView、ListModelMixin 是上面两种方法的子类
  • ViewSetMixin 实现了view = MyViewSet.as_view({'get': 'list', 'post': 'create'})

订单模块:

基本功能:提交订单,我的订单,订单评价

  • 提交订单

FastDFS分布式文件系统

  • FastDFS分布式文件系统,数据冗余,数据的备份,数据量的存储扩展
  • tracker server的作用是负载均衡和调度,可以实现集群,每个reacker节点的地位平等,收集storage的状态;
  • storage server的作用是存储,不同的组内保存的内容是不同的,相同的组内保存的内容是相同的,这样的设计数据会相对比较安全安全;
  • 无论是tracker还是storage都支持集群的方式扩展,数据的扩展比较方便
  • 文件上传的流程
    • storage server定时向tracker server的上传存储状态信息
    • 客户端上传链接请求
    • 请求会先到达tracker server,tracker server查询可以调用的storage;
    • 返回storage server 的ip和port
    • 上传文件到指定的storage server中
    • srorage server生成file id,并将文件存入到磁盘中
    • 返回file id给客户端
    • 存储文件信息

docker的理解

  • docker是一种虚拟化的技术,我们可以将docker视为一种容器,在容器的内部可以运行服务,
  • Docker本身是一种C/S架构的程序,Docker客户端需要向服务器发送请求,服务器完成所有的工作之后返回给客户端结果;
  • 优点
    • 加速本地开发和构建的流程,在本地可以自己轻松的构建,运行,分享所配置好的docker环境
    • 能够在不同的操作系统的环境中获取相同的docker容器中的环境,减少了在部署环节中的环境问题待来的麻烦
    • docker可以创建虚拟化的沙箱环境可以供测试使用
    • docker可以让开发者在最开始的开发过程中在测试的环境中运行,而并非一开始就在生产环境中开发,部署和测试

首页静态化的技术

  • 电商类型的网站首页的访问评率较大,每次获取首页过程中去对数据库进行查询显然不太合理,除了使用传统意义上的缓存实现之外,我们可以使用首页静态化的技术,即将动态生成的html页面生成保存成静态文件,在用户访问的过程中直接将静态文件发送给用户

  • 优点:可以缓解数据库压力,并且可以提高用户访问的网站的速度,提高用户体验

  • 带来的问题是:用户在页面中动态生成的部分数据如何处理

    • 在静态页面加载完成之后,通过js的代码将动态的请求发送到后天请求数据,但是大部分数据均是静态页面中的数据,
    • 静态生成的页面因为并没有实时更新,会出现部分商品的静态化页面中的数据和数据库中实时更新的数据有差异
  • 应用场景:经常容易访问但是,数据的变动并不是太大的一些页面可以考虑使用静态化技术

  • 难点GoodsCategory,GoodsChannel两个表格之间的关系设计以及获取商城商品分类的菜单

    • 首先是GoodsChannel,将所有的商品的频道取出按照组号和组内顺序排序
    • 排序后将数据以categories[group_id] = {'channels': [], 'sub_cats': []}的形式存入到有个有序的字典中
    • channels的值为存储的是频道的相关信息(例如手机,相机,数码)
    • sub_cats中存储的值是该频道下的GoodsCategory相关信息(例如手机通讯,手机配件...相关,根据GoodsChannel数据结构表中顶级类别来查询子类别)
    • 分别为顶级类别下的子类别对象添加一个sub_cats的列表,来存储此类别下的所有GoodsCategory的queryset对象
  • 难点 商品详情页面的数据结构

    • 三 获取当前商品的规格信息

#!/usr/bin/env python

"""
功能:手动生成所有SKU的静态detail html文件
使用方法:
    ./regenerate_detail_html.py
"""
import sys
sys.path.insert(0, '../')
sys.path.insert(0, '../meiduo_mall/apps')

import os
if not os.getenv('DJANGO_SETTINGS_MODULE'):
    os.environ['DJANGO_SETTINGS_MODULE'] = 'meiduo_mall.settings.dev'

import django
django.setup()

from django.template import loader
from django.conf import settings

from goods.utils import get_categories
from goods.models import SKU


def generate_static_sku_detail_html(sku_id):
    """
    生成静态商品详情页面
    :param sku_id: 商品sku id
    """
    # 商品分类菜单
    categories = get_categories()

    # 获取当前sku的信息
    sku = SKU.objects.get(id=sku_id)
    sku.images = sku.skuimage_set.all()

    # 面包屑导航信息中的频道
    goods = sku.goods
    goods.channel = goods.category1.goodschannel_set.all()[0]

    # 构建当前商品的规格键
    sku_specs = sku.skuspecification_set.order_by('spec_id')
    sku_key = []
    for spec in sku_specs:
        sku_key.append(spec.option.id)
    print("当前商品的规格键[1,4,7]",sku_key)
    # 获取当前商品的所有SKU
    skus = goods.sku_set.all()
    print("获取当前商品的所在的SPU下的所有SKU对象",skus)

    # 构建不同规格参数(选项)的sku字典
    # spec_sku_map = {
    #     (规格1参数id, 规格2参数id, 规格3参数id, ...): sku_id,
    #     (规格1参数id, 规格2参数id, 规格3参数id, ...): sku_id,
    #     ...
    # }

    spec_sku_map = {}
    for s in skus:
        # 获取sku的规格参数
        s_specs = s.skuspecification_set.order_by('spec_id')
        # 用于形成规格参数-sku字典的键
        key = []
        for spec in s_specs:
            key.append(spec.option.id)
        # 向规格参数-sku字典添加记录
        # print("构造出的不同规格的参数",key)
        spec_sku_map[tuple(key)] = s.id
    print("{(1, 4, 7): 1, (1, 3, 7): 2}构造出的不同规格的参数",spec_sku_map)

    # 获取当前商品的规格信息
    specs = goods.goodsspecification_set.order_by('id')
    print("当前商品所有的规格选项,屏幕,颜色,,,",specs)
    # print("sku_key",sku_key)
    # 若当前sku的规格信息不完整,则不再继续
    if len(sku_key) < len(specs):
        return
    for index, spec in enumerate(specs):
        # if index == 0:
        #     print("index", index)
        #     print("GoodsSpecification的规格信息对象", spec)
        # 复制当前sku的规格键
        key = sku_key[:]
        print("当前的规格选项",spec.name)
        # 该规格的选项
        options = spec.specificationoption_set.all()
        print("options规格信息的选项", options)
        for option in options:
            # 在规格参数sku字典中查找符合当前规格的sku
            print("001",key)
            print("固定不变的数据库中只有这两种商品spec_sku_map",spec_sku_map)
            print("option.id", option.id)
            key[index] = option.id
            print("002",key)
            option.sku_id = spec_sku_map.get(tuple(key))
            print(option.sku_id)


        spec.options = options

    # 渲染模板,生成静态html文件
    context = {
        'categories': categories,
        'goods': goods,
        'specs': specs,
        'sku': sku
    }

    template = loader.get_template('detail.html')
    html_text = template.render(context)
    file_path = os.path.join(settings.GENERATED_STATIC_HTML_FILES_DIR, 'goods/'+str(sku_id)+'.html')
    with open(file_path, 'w') as f:
        f.write(html_text)

if __name__ == '__main__':
    sku = SKU.objects.get(id=1)
    generate_static_sku_detail_html(sku.id)

  • 传入单个对象时执行结果
当前商品的规格键[1,4,7] [1, 4, 7]
获取当前商品的所在的SPU下的所有SKU对象 <QuerySet [<SKU: 1: Apple MacBook Pro 13.3英寸笔记本 银色>, <SKU: 2: Apple MacBook Pro 13.3英寸笔记本 深灰色>]>
{(1, 4, 7): 1, (1, 3, 7): 2}构造出的不同规格的参数 {(1, 4, 7): 1, (1, 3, 7): 2}
当前商品所有的规格选项,屏幕,颜色,,, <QuerySet [<GoodsSpecification: Apple MacBook Pro 笔记本: 屏幕尺寸>, <GoodsSpecification: Apple MacBook Pro 笔记本: 颜色>, <GoodsSpecification: Apple MacBook Pro 笔记本: 版本>]>
当前的规格选项 屏幕尺寸
options规格信息的选项 <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 屏幕尺寸 - 13.3英寸>, <SpecificationOption: Apple MacBook Pro 笔记本: 屏幕尺寸 - 15.4英寸>]>
001 [1, 4, 7]
固定不变的数据库中只有这两种商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2}
option.id 1
002 [1, 4, 7]
1
001 [1, 4, 7]
固定不变的数据库中只有这两种商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2}
option.id 2
002 [2, 4, 7]
None
当前的规格选项 颜色
options规格信息的选项 <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 颜色 - 深灰色>, <SpecificationOption: Apple MacBook Pro 笔记本: 颜色 - 银色>]>
001 [1, 4, 7]
固定不变的数据库中只有这两种商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2}
option.id 3
002 [1, 3, 7]
2
001 [1, 3, 7]
固定不变的数据库中只有这两种商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2}
option.id 4
002 [1, 4, 7]
1
当前的规格选项 版本
options规格信息的选项 <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/256G存储>, <SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/128G存储>, <SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/512G存储>]>
001 [1, 4, 7]
固定不变的数据库中只有这两种商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2}
option.id 5
002 [1, 4, 5]
None
001 [1, 4, 5]
固定不变的数据库中只有这两种商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2}
option.id 6
002 [1, 4, 6]
None
001 [1, 4, 6]
固定不变的数据库中只有这两种商品spec_sku_map {(1, 4, 7): 1, (1, 3, 7): 2}
option.id 7
002 [1, 4, 7]
1
  • 最后返回的specs的数据结构为
 specs = [
    {
        'name': '屏幕尺寸',
        'options': [
            {'value': '13.3寸', 'sku_id': xxx},
            {'value': '15.4寸', 'sku_id': xxx},
        ]
    },
    {
        'name': '颜色',
        'options': [
            {'value': '银色', 'sku_id': xxx},
            {'value': '黑色', 'sku_id': xxx}
        ]
    },
    ...
 ]
  • 通过sku id来取出此sku商品的SPU对应的所有存在的商品组合
  • 循环数据库中所有的商品选项,将商品的选项与sku id来做对应,返回上面的数据类型
  • 相同的SPU对应的不同的SKU,返回的specs是相同的,例如如果同属于手机这个SPU的Iphone6手机和Iphone7手机,返回的specs是相同的,若假设手机品牌只有屏幕大小不相同,则返回的数据类型如下
 specs = [
    {
        'name': '屏幕尺寸',
        'options': [
            {'value': '13.3寸', 'sku_id': Iphone7的sku_id},
            {'value': '15.4寸', 'sku_id': Iphone6的sku_id},
            {'value': '15.4寸', 'sku_id': Iphone7的sku_id2},
        ]
    },
    {
        'name': '颜色',
        'options': [
            {'value': '银色', 'sku_id': Iphone7的sku_id},
            {'value': '银色', 'sku_id': Iphone6的sku_id},
            {'value': '金色', 'sku_id': Iphone7的sku_id2},
        ]
    },
    ...
 ]
  • 前端通过传入的sku id来取值生成的详情页面,从同一份数据数据中拿的值,就会避免重复,例如Iphone6的只是取出所有的Iphone6的所有的信息生成静态页面,传入的sku id不同得到的页面效果不同,通过不同的id也可以找到不同的商品的详情页面

  • 细节完善在运营人员相关修改商品信息,要在后端实现,自动刷新详情的页面的数据,自动触发静态页面的生成,利用了django中的ModelAdmin,在数据发生变动之后自动进行更新相关数据

class SKUSpecificationAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        obj.save()
        from celery_tasks.html.tasks import generate_static_sku_detail_html
        generate_static_sku_detail_html.delay(obj.sku.id)

    def delete_model(self, request, obj):
        sku_id = obj.sku.id
        obj.delete()
        from celery_tasks.html.tasks import generate_static_sku_detail_html
        generate_static_sku_detail_html.delay(sku_id)

获取热销商品

  • 技术点
  • 详情页面中的热销商品每个页面加载完成之后都会来向后端请求数据,但是热销商品却不经常发生变化或者是在一段时间内根据相关字段统计生成返回给前端即可,所有使用缓存的方式存储热销商品是最合理的方式,避免了数据的链接,减少了服务器的压力,充分的利用了缓存的响应速度也比较快可以提高用户的体验

商品列表页面的展示

  • 商品数据动态的从后端获取,其他部分生成静态化页面
  • 技术点:
    • DRF框架中过滤,排序,分页,序列化器数据的返回
    • 适当使用到了DRF提供的扩展类ListAPIView来简化代码

商品搜索的功能实现

  • 技术点
    • Elasticsearch搜索引擎的原理:通过搜索引擎进行搜索数据查询的过程中,搜索引擎并不是直接去数据库中去进行数据库的查询,而是搜素引擎会对数据库中所有的数据进行一遍的预处理,单独的建立一份索引表,在进行数据库查询的过程中,会在索引表中进行查询,然后返回相应的数据。
    • Elasticsearch 不支持对中文进行分词建立索引,需要配合扩展elasticsearch-analysis-ik来实现中文分词处理
    • 使用haystack对接Elasticsearch的流程
      • 安装
      • 在配置文件中配置haystack使用的搜索引擎后端
      • SKU索引数据模型类
      • 在templates目录中创建text字段使用的模板文件
      • 手动生成初始索引
      • 创建序列化器
      • 创建视图
      • 定义路由

购物车模块

  • 对于未登录的用户,购物车
class CartMixin():
    def str2dict(self, redis_data):  # redis_data从redis中读取的数据
        """ 转化为python字典"""
        redis_dict = pickle.loads(base64.b64decode(redis_data))
        return redis_dict

    def dict2str(self, redis_dict):
        """ python中dict转为可以存入redis中的数据"""
        # 将合并后的字典再次存入到redis中
        redis_bytes = pickle.dumps(redis_dict)
        redis_str = base64.b64encode(redis_bytes)
        return redis_str

    def write_cart(self, request: Request, response: Response, cart_dict: dict):
        """ 写购物车数据"""
        cart_str = self.dict2str(cart_dict)
        if request.user.is_authenticated:
            key = "cart2_%s" % request.user.id
            get_redis_connection("cart").set(key, cart_str)
        else:
            response.set_cookie("cart", cart_str)

    def read_from_redis(self, user_id):
        """ 返回一个字典"""
        key = "cart2_%s" % user_id
        redis_data = get_redis_connection("cart").get(key)

        if redis_data is None:
            return {}
        return self.str2dict(redis_data)

    def read_from_cookie(self, request: Request):
        value = request.COOKIES.get("cart")
        if value is None:
            return {}
        return self.str2dict(value)

    def read(self, request: Request) -> dict:
        if request.user.is_authenticated:
            cart_dict = self.read_from_redis(request.user.id)
        else:
            cart_dict = self.read_from_cookie(request)
        return cart_dict


class CartView(CartMixin, APIView):
    # pagination_class = StandardResultsSerPagination

    def post(self, request):
        serializer = CartSerializer(data=request.data)  # 检查前端发送过来数据是否正确
        serializer.is_valid(raise_exception=True)  # 数据检验通过
        sku_id = serializer.validated_data['sku_id']
        count = serializer.validated_data['count']
        selected = serializer.validated_data["selected"]

        cart_dict = self.read(request)
        if sku_id in cart_dict:
            #
            new_count = cart_dict[sku_id][0] + count
            cart_dict[sku_id] = [new_count, selected]
        else:
            cart_dict[sku_id] = [count, selected]

        resp = Response(serializer.data, status=status.HTTP_201_CREATED)
        self.write_cart(request, resp, cart_dict)
        return resp

    def get(self, request):
        cart_dict = self.read(request)
        skus = SKU.objects.filter(id__in=cart_dict.keys())

        for sku in skus:
            sku.count = cart_dict[sku.id][0]
            sku.selected = cart_dict[sku.id][1]

        serializer = CartSKUSerializer(skus, many=True)

        return Response(serializer.data)

    def put(self, request):
        # 检查前端发送的数据是否正确
        serializer = CartSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        sku_id = serializer.validated_data.get('sku_id')
        count = serializer.validated_data.get('count')
        selected = serializer.validated_data.get('selected')

        cart_dict = self.read(request)

        cart_dict[sku_id] = [count, selected]
        resp = Response(serializer.data, status=status.HTTP_201_CREATED)
        self.write_cart(request, resp, cart_dict)
        return resp

    def delete(self, request):
        # 检查参数sku_id
        serializer = CartDeleteSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        sku_id = serializer.validated_data['sku_id']

        cart_dict = self.read(request)

        if sku_id in cart_dict:
            del cart_dict[sku_id]
        resp = Response(serializer.data, status=status.HTTP_204_NO_CONTENT)
        self.write_cart(request, resp, cart_dict)

        return resp

def merge_cart_cookie_to_redis(request, response, user):
    """ 合并购物车"""
    cookies_cart = request.COOKIES.get('cart')
    if cookies_cart is not None:
        cookies_dict = pickle.loads(base64.b64decode(cookies_cart))  # 取出cookies中的数据
        print("cookies_dict000", cookies_dict)
        redis_conn = get_redis_connection("cart")
        redis_cart = redis_conn.get("cart2_%s" % user.id)  #
        redis_dict = {}
        if redis_cart:
            redis_dict = pickle.loads(base64.b64decode(redis_cart))  # 取出的是redis中的数据
        print("redis_dict001", redis_dict)

        # 过滤购物车中没有被选中的商品
        new_cookies_dict = deepcopy(cookies_dict)
        for sku_id, value in cookies_dict.items():
            if not value[1]:
                new_cookies_dict.pop(sku_id)

        print("redis_dict002", new_cookies_dict)

        # 合并cookies中的值
        redis_dict.update(new_cookies_dict)

        print("redis_dict003", redis_dict)

        redis_str = base64.b64encode(pickle.dumps(redis_dict))
        key = "cart2_%s" % user.id
        get_redis_connection("cart").set(key, redis_str)

        # 往redis中写入数据
        # if cart:
        #     pl = redis_conn.pipeline()
        #     pl.hmset("cart_%s" % user.id, cart)
        #     pl.sadd("cart_selected_%s" % user.id, *redis_cart_selected)
        #     pl.execute()

        # 删除cookie中的数据
        response.delete_cookie("cart")
        return response
    return response
  • 数据类型的设计原则:
    • 尽量将cookies中的数据类型格式与redis数据库中数据类型设计成形同的
    • 对redis数据库的相关操作
    • {sku id :[count,True]}数据中sku id:商品的id,count:购物车中数据的商品的个数;True或者False代表是否被选中
    • 将对数据的操作封装成一个扩展集,在视图中继承扩展类

订单相关的操作

  • 订单数据库表的设计
    • 订单的字段分析
    • 首先将订单分为两个表格
    • 1,订单的基本信息表;
    • 2,订单的商品信息,两者之间的关系是一对多的关系
    • 订单的基本信息表中主要存储这笔订单的相关信息(订单的id,下单的用户,用户的默认地址,邮费的状态,订单的支付方式,订单的状态)
    • 订单商品中保存(订单的id,用户商品的id,商品的数量,商品的价格)
    • 在前端中展示还需要的字段有此次订单的总金额,以及该订单中商品的数量,这两个字段虽然经过表格之间的关联可以查询出来,在这里可以将这两个字段一起定义在订单的基本信息的表格中,避免在后续查询订单的过程中对数据库的操作;

具体字段的定义:

from django.db import models
from meiduo_mall.utils.models import BaseModel
from users.models import User, Address
from goods.models import SKU


# Create your models here.


class OrderInfo(BaseModel):
    """
    订单信息
    """
    PAY_METHODS_ENUM = {
        "CASH": 1,
        "ALIPAY": 2
    }

    PAY_METHOD_CHOICES = (
        (1, "货到付款"),
        (2, "支付宝"),
    )

    ORDER_STATUS_ENUM = {
        "UNPAID": 1,
        "UNSEND": 2,
        "UNRECEIVED": 3,
        "UNCOMMENT": 4,
        "FINISHED": 5
    }

    ORDER_STATUS_CHOICES = (
        (1, "待支付"),
        (2, "待发货"),
        (3, "待收货"),
        (4, "待评价"),
        (5, "已完成"),
        (6, "已取消"),
    )

    order_id = models.CharField(max_length=64, primary_key=True, verbose_name="订单号")
    user = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name="下单用户")
    address = models.ForeignKey(Address, on_delete=models.PROTECT, verbose_name="收获地址")
    total_count = models.IntegerField(default=1, verbose_name="商品总数")
    total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="商品总金额")
    freight = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="运费")
    pay_method = models.SmallIntegerField(choices=PAY_METHOD_CHOICES, default=1, verbose_name="支付方式")
    status = models.SmallIntegerField(choices=ORDER_STATUS_CHOICES, default=1, verbose_name="订单状态")

    class Meta:
        db_table = "tb_order_info"
        verbose_name = '订单基本信息'
        verbose_name_plural = verbose_name


class OrderGoods(BaseModel):
    """
    订单商品
    """
    SCORE_CHOICES = (
        (0, '0分'),
        (1, '20分'),
        (2, '40分'),
        (3, '60分'),
        (4, '80分'),
        (5, '100分'),
    )
    order = models.ForeignKey(OrderInfo, related_name='skus', on_delete=models.CASCADE, verbose_name="订单")
    sku = models.ForeignKey(SKU, on_delete=models.PROTECT, verbose_name="订单商品")
    count = models.IntegerField(default=1, verbose_name="数量")
    price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="单价")
    comment = models.TextField(default="", verbose_name="评价信息")
    score = models.SmallIntegerField(choices=SCORE_CHOICES, default=5, verbose_name='满意度评分')
    is_anonymous = models.BooleanField(default=False, verbose_name='是否匿名评价')
    is_commented = models.BooleanField(default=False, verbose_name='是否评价了')

    class Meta:
        db_table = "tb_order_goods"
        verbose_name = '订单商品'
        verbose_name_plural = verbose_name

获取购物车商品逻辑

- 用户必须在登录的状态下才可以进入到购物车商品结算的页面
- 查询到当前订单的用户;
- 在redis数据库中将当前用户的购物车中所有商品查询出来
- 过滤筛选出用户选中的商品信息的id
- 查询出当前订单的运费的
- 将相关信息(例如运费和查询出来的商品的skus对象)传递给序列化器,序列化器将数据从数据库中序列化后返回给前端
  • 最终后端返回给前端的数据格式如图所示
{
    "freight":"10.00",
    "skus":[
        {
            "id":10,
            "name":"华为 HUAWEI P10 Plus 6GB+128GB 钻雕金 移动联通电信4G手机 双卡双待",
             "default_image_url":"http://image.meiduo.site:8888/group1/M00/00/02/CtM3BVrRchWAMc8rAARfIK95am88158618",
            "price":"3788.00",
            "count":1
        },
        {
            "id":16,
            "name":"华为 HUAWEI P10 Plus 6GB+128GB 曜石黑 移动联通电信4G手机 双卡双待",
            "default_image_url":"http://image.meiduo.site:8888/group1/M00/00/02/CtM3BVrRdPeAXNDMAAYJrpessGQ9777651",
            "price":"3788.00",
            "count":1
        }
    ]
}

保存订单

  • id的字段定义,默认情况下采用sql数据库中的自增的id作为主键,但是在考虑到id的可读性和扩展性将主键设置为具有特定格式的CharField字段,自己定义的id的格式的;
  • 保存订单的逻辑实现
    • 获取当前下单的用户
    • 获取用户的基本信息(用户的默认地址,用户选择的支付方式)
    • 创建事物,在以下的任何一个操作不成功的情况下就会返回到当前这个保存点
    • 组织订单的id
    • 创建一个订单基本信息的对象,进行保存
    • redis中取出所有的商品,过滤出用户选中的商品
    • 给订单的中设计的冗余的字段赋初值
    • 从数据库中查询商品信息,并判断用户购买的商品库存状态和销量的状态
    • 更新数据库中库存和销量的相关信息。在这个地方用乐观锁的方式来判断在事物保存的过程中是否有其他用户来操作过商品的,如果有则重新返回到事物保存点,没有则继续
    • 在这里来查询用户的商品信息表中将订单基本信息的表格中的相关冗余字段计算出来一起保存进入到数据库中
class SaveOrderSerializer(serializers.ModelSerializer):
    """ 用户支付订单的创建序列化器"""

    class Meta:
        model = OrderInfo
        fields = ("order_id", "address", "pay_method")
        read_only_fields = ("order_id",)
        extra_kwargs = {
            "address": {
                "write_only": True,
                "required": True
            },
            "pay_method": {
                "write_only": True,
                "required": True
            }
        }

    def create(self, validated_data):
        """ 保存订单序列化器"""
        # 获取当前下单用户
        user = self.context['request'].user
        address = validated_data.get("address")
        pay_method = validated_data.get("pay_method")
        # 组织订单信息20170903153611+user.id
        order_id = timezone.now().strftime("%Y%m%d%H%M%S") + ("%09d" % user.id)

        # 开启事务
        with transaction.atomic():
            # 创建保存点,记录当前数据状态
            save_id = transaction.savepoint()
            try:
                # 保存订单基本信息数据 OrderInfo
                order = OrderInfo.objects.create(**{
                    "order_id": order_id,
                    'user': user,
                    'address': address,
                    'total_count': 0,
                    "total_amount": Decimal(0),
                    "freight": Decimal(10),
                    "pay_method": pay_method,
                    #  如果用户选择现金支付,订单状态为待发货;反之为待支付
                    "status": OrderInfo.ORDER_STATUS_ENUM['UNSEND'] if validated_data['pay_method'] ==
                                                                       OrderInfo.PAY_METHODS_ENUM['CASH']
                    else OrderInfo.ORDER_STATUS_ENUM['UNPAID']
                })

                redis_conn = get_redis_connection("cart")
                # 从redis中获取购物车结算商品数据
                selected_sku_id_list = []
                redis_cart = redis_conn.get("cart2_%s" % user.id)  #
                redis_dict = pickle.loads(base64.b64decode(redis_cart))  # 取出的是redis中的数据
                # 过滤购物车中被选中的商品的id
                for sku_id, value in redis_dict.items():
                    if value[1]:
                        selected_sku_id_list.append(sku_id)

                # 冗余数据先设置默认值
                order.total_amount = Decimal(0)
                order.total_count = 0

                # 查询商品信息
                # skus = SKU.objects.filter(id__in=cart_dict.keys())  # 得到选中的商品objs
                # 遍历结算商品:
                for sku_id in selected_sku_id_list:
                    while True:
                        sku = SKU.objects.get(id=sku_id)
                        # 要购买的商品的数量
                        count = redis_dict[sku.id][0]

                        # 判断库存量和销售量
                        origin_stock = sku.stock
                        origin_sales = sku.sales
                        # 判断商品库存是否充足
                        if count > origin_stock:
                            transaction.savepoint_rollback(save_id)
                            raise serializers.ValidationError({"库存不足"})

                        # 更新库存和销量信息
                        new_stock = origin_stock - count
                        new_sales = origin_sales + count
                        # sku.stock = new_stock
                        # sku.sales = new_sales

                        # 返回受影响的行数
                        ret = SKU.objects.filter(id=sku.id, stock=origin_stock).update(stock=new_stock, sales=new_sales)
                        if ret == 0:
                            continue

                        # 计算order——info中的两个冗余字段结果并赋值
                        order.total_count += count
                        order.total_amount += (sku.price * count)

                        OrderGoods.objects.create(**{
                            "order": order,
                            "sku": sku,
                            "count": count,
                            "price": sku.price

                        })
                        break
                order.save()
            except serializers.ValidationError:
                raise
            except Exception:
                transaction.savepoint_rollback(save_id)
                raise

            # 提交事务
            transaction.savepoint_commit(save_id)
            # 清除购物车中已经结算的商品
            redis_conn.delete('cart2_%s' % user.id)
            return order
  • 数据库的事物
    • Django中对于数据库的操作,默认在每一次的数据库操作之后都会自动提交
    • 在Django中可以通过django.db.transaction模块提供的atomic来定义一个事务,atomic提供两种用法
  • 使用方法一:
from django.db import transaction

@transaction.atomic
def viewfunc(request):
    # 这些代码会在一个事务中执行
    ...
  • 使用方法二
from django.db import transaction

# 创建保存点
save_id = transaction.savepoint()  

# 回滚到保存点
transaction.savepoint_rollback(save_id)

# 提交从保存点到当前状态的所有数据库事务操作
transaction.savepoint_commit(save_id)

支付模块:

 

 

 

import os

from alipay import AliPay
from django.conf import settings
from django.shortcuts import render

# Create your views here.
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated

from orders.models import OrderInfo
from payment.models import Payment


class PaymentView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, order_id):
        user = request.user
        # 校验订单order_id
        try:
            order = OrderInfo.objects.get(order_id=order_id, user=user, status=OrderInfo.ORDER_STATUS_ENUM["UNPAID"])
        except OrderInfo.DoesNotExist:
            return Response({"message": "订单信息有误"}, status=status.HTTP_400_BAD_REQUEST)

        # 根据订单的数据,向支付宝发起请求,获取支付链接参数
        alipay_client = AliPay(
            appid=settings.ALIPAY_APPID,
            app_notify_url=None,  # 默认回调url
            app_private_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/app_private_key.pem"),
            alipay_public_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                                "keys/alipay_public_key.pem"),  # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
            sign_type="RSA2",  # RSA 或者 RSA2
            debug=settings.ALIPAY_DEBUG  # 默认False
        )

        order_string = alipay_client.api_alipay_trade_page_pay(
            out_trade_no=order_id,
            total_amount=str(order.total_amount),
            subject="美多商城%s" % order_id,
            return_url="http://www.meiduo.site:8080/pay_success.html",
        )

        alipay_url = settings.ALIPAY_GATEWAY_URL + '?' + order_string
        # 需要跳转到https://openapi.alipay.com/gateway.do? + order_string
        # 拼接链接返回前端
        return Response({'alipay_url': alipay_url}, status=status.HTTP_201_CREATED)


class PaymentStatusView(APIView):
    """
    修改支付结果状态
    """

    def put(self, request):
        # 取出请求的参数
        query_dict = request.query_params
        # 将django中的QueryDict 转换python的字典
        alipay_data_dict = query_dict.dict()

        sign = alipay_data_dict.pop('sign')

        # 校验请求参数是否是支付宝的
        alipay_client = AliPay(
            appid=settings.ALIPAY_APPID,
            app_notify_url=None,  # 默认回调url
            app_private_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/app_private_key.pem"),
            alipay_public_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                                "keys/alipay_public_key.pem"),  # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
            sign_type="RSA2",  # RSA 或者 RSA2
            debug=settings.ALIPAY_DEBUG  # 默认False
        )

        success = alipay_client.verify(alipay_data_dict, sign)

        if success:
            order_id = alipay_data_dict.get('out_trade_no')
            trade_id = alipay_data_dict.get('trade_no')  # 支付宝交易流水号

            # 保存支付数据
            # 修改订单数据
            Payment.objects.create(
                order_id=order_id,
                trade_id=trade_id
            )
            OrderInfo.objects.filter(order_id=order_id, status=OrderInfo.ORDER_STATUS_ENUM['UNPAID']).update(
                status=OrderInfo.ORDER_STATUS_ENUM['UNSEND'])
            return Response({'trade_id': trade_id})
        else:
            # 参数据不是支付宝的,是非法请求
            return Response({'message': '非法请求'}, status=status.HTTP_403_FORBIDDEN)
  • 数据加密的过程

    • 在双方进行通信的过程中,若A要给B发送消息
    • 互相交换公钥密码
    • 在数据包的发送过程中,A先用自己的私钥加密(保证数据的安全的,至少不会明文显示)。
    • 再用B交给A的公钥进行加密(只有B有自己的私钥才可以打开最外层的包裹信息)
      enter description here
 
 
posted @ 2018-10-11 22:24  阴天气球  阅读(6275)  评论(1编辑  收藏  举报