Django电商平台

Django电商平台

数据库设计

  • 用户数据库模型设计
class UserProfile(AbstractUser):
    # 继承Django原先的用户基类
    '''
        Username and password are required. Other fields are optional.
    '''
    gender_choice = [
        ('male', '男'),
        ('female', '女'),
	]
    username = models.CharField('用户名', max_length=30, unique=True)
    mobile = models.CharField('电话', max_length=20, null=False, blank=False)
    email = models.EmailField('邮箱', null=True, blank=True)
    gender = models.CharField('性别', null=False, choices=gender_choice)

    def __str__(self):
        return self.username

    class Meta:
        db_table = 'user'
        verbose_name = '用户信息'
        verbose_name_plural = verbose_name
mobile = models.CharField('电话', max_length=20, null=False, blank=False)
1.null 是针对数据库而言的,False表示不能为空
2.blank 是针对表单而言,False 表示表单的该字段不能为空

gender = models.CharField('性别', null=False, choices=gender_choice)
	如果提供了选择,则模型验证将强制执行这些选择,默认表单窗口小部件将是带有这些选择的选择框,而不是标准文本字段。
	每个元组中的第一个元素是要在模型上设置的实际值,第二个元素是人类可读的名称。
  • 安装第三方插件
# xadmin
$ pip install https://codeload.github.com/sshwsfc/xadmin/zip/django2
# ckeditor
$ pip install django-ckeditor
  • 将第三方插件要注册到应用中去

  • 商品的数据库模型

class Category(models.Model):
    # 类别的多种级别
    types = [
        (1, '一级类别'),
        (2, '二级类别'),
        (3, '三级列别')
    ]
   # …………
    category_type = models.IntegerField('类别级别', choices=types)
    # 类别的多级分类
    # 一级类别:二级类别 1:n
        parent_category = models.ForeignKey('self', verbose_name='父级分类'
           , related_name='sub_category', on_delete=models.CASCADE,null=True, blank=True)
    # 是否添加导航
    is_tab = models.BooleanField('是否导航', default=False)
    add_time = models.DateTimeField('添加时间', default=datetime.now)
parent_category = models.ForeignKey('self', verbose_name='父级分类'
           , related_name='sub_category', on_delete=models.CASCADE,null=True, blank=True)
# 父级分类和子分类 为 1:n关系

on_delete参数:
    To create a recursive relationship – an object that has a many-to-one relationship with itself – use models.ForeignKey('self', on_delete=models.CASCADE).
    级联删除。 Django会在DELETE CASCADE上模拟SQL约束的行为,并删除包含ForeignKey的对象。
  • 后台站点显示
class GoodsAdmin(object):
    # …………
    # 在添加商品的时候可以添加商品图片
    class GoodsImagesInline(object):
        model = GoodsImage
        # 不显示的字段名称
        exclude = ["add_time"]
        # 控制初始表单数量,默认为3
        extra = 3
        style = 'tab'
	# 内部嵌套
    inlines = [GoodsImagesInline]

  • 进行数据填充
# 加载Django配置和Django APP的注册。
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shopAPI.settings")
django.setup()

# 然后导入数据,引入之前定义的数据库模型,将数据进行填充

注意要在加载Django配置之后在导入数据库模型

  • 配置多媒体文件存储路径
# settings.py

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")

  • 配置多媒体路由
# urls.py
urlpatterns = [
    # …………
    # 配置静态文件的路由
    path('media/', serve, {'document_root': MEDIA_ROOT})
]

视图函数

  • 视图函数的两种形式

    • FBV 视图函数接受request请求,函数体中直接是视图函数

    • CBV 该方法的视图继承了视图类views,然后类中可以定义各种http请求方式,

      不同的请求方式可以对应不同的视图函数,即将对图函数和HTTP请求方式绑定,

      编写路由的时候可以使用as_views()来使类生成视图函数

    FBV适合小型项目,综合、大型项目可以使用CBV

  • Django API

    • Django中可以通过原生的数据用json封装在返回response,但是图片、时间等字段不同正确转成json类型
    • Django中提供的序列化类,可以实现弥补上述缺点,但是图片的存储为相对路径,不能将静态资源的绝对路径自动补全,例如MEDIA_ROOT、MEDIA_URL无法自动添加
    • 使用DRF(Django Rest Framework)
  • 什么是序列化?

    序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程,例如json或者xml格式

  • 序列化的作用

    • 可以将查询集或者模型实例对象进行序列化为json或者xml格式,然后传输给用户
    • 可以对post或者patch/put请求进行数据处理,并进行验证。
  • 安装Django Rest Frameword的依赖包

$ pip install djangorestframework
# 自动生成API文档
$ pip install coreapi
# 文档的markdown展示
$ pip install markdown
# django-guardian是为Django提供额外的基于对象权限的身份验证后端
$ pip install django-guardian
# 过滤支持
$ pip install django-filter


# 最后在setting.py中添加第三方插件
  • 视图函数的编写流程
1.编写序列化类,规定需要进行序列化传输的数据
2.编写视图类,其中使用序列化
3.配置路由
# app/goods/serializers.py
from rest_framework import serializers
# 编写序列化类,其中的字段需要和数据库模型中的字段名对应
class GoodsSerializers(serializers.Serializer):
    name = serializers.CharField(required=True, max_length=20)
    shop_price = serializers.FloatField(default='0.0')
    goods_front_image = serializers.ImageField()
# app/goods/views.py
from app.goods.models import Goods
from app.goods.serializers import GoodsSerializers

class GoodsListViews(APIView):
    def get(self, request):
        goods = Goods.objects.all()
        # 对数据进行序列化
        goods_serializers = GoodsSerializers(goods, many=True)
        # 返回序列化后的数据
        return Response(goods_serializers.data)
    
# APIView是对Django的原有类View的进一步封装,对请求和相应做了进一步的处理,在一次封装了分发函数,增加了	一些功能:
		# Ensure that the incoming request is permitted
        self.perform_authentication(request)
        self.check_permissions(request)
        self.check_throttles(request)
#	但是继承APIView的类都禁用了csrf_token
这里GoodsSerializers需要指定参数many=True,否则会出现如下报错
many = True表示一对多关系
  • 当异常
AttributeError: Got AttributeError when attempting to get a value for field `name` on serializer `GoodsSerializers`.
The serializer field might be named incorrectly and not match any attribute or key on the `QuerySet` instance.
Original exception text was: 'QuerySet' object has no attribute 'name'.

  • 访问API文档时候的异常
'AutoSchema' object has no attribute 'get_link'

参考如下配置

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema'
}

  • 上述Serializer的弊端

需要自己手动指定需要序列化的字段名称,当字段名比较多的时候工作量会很大

因此下面提供ModelSerializers类:

​ 1.ModelSerializer能够自动序列化部分或者所有的字段

​ 2.能够对联合唯一约束进行验证

​ 3.能够自动完成创建、更新等操作

class GoodsSerializers(serializers.ModelSerializer):
    class Meta:
        model = Goods
        # fields = '__all__'
        exclude = ['goods_desc']
  • 展示的数据中存在外键

当你想看数据中外键的详细信息时,可以对外键的数据库模型进行序列化,然后嵌套进主序列化类中

class CategorySerializers(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = '__all__'

class GoodsSerializers(serializers.ModelSerializer):
    # 实现外键信息的嵌套
    category = CategorySerializers()

    class Meta:
        model = Goods
        # fields = '__all__'
        exclude = ['goods_desc']
  • APIView的进一步封装GenericAPIView

它在APIView的基础上,增加了筛选、分页等操作,并且可以和Mixin扩展类结合实现list、create、update等操作

class GoodsListViews(GenericAPIView, mixins.ListModelMixin):
    # ListAPIView(mixins.ListModelMixin,GenericAPIView):
    #       …………
    #     def get(self, request, *args, **kwargs):
    #         return self.list(request, *args, **kwargs)
    # 指定查询集
    queryset = Goods.objects.all()
    # 指定序列化类
    serializer_class = GoodsSerializers
	# 该方法继承了mixins.ListModelMixin的list方法
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)
  • GenericAPIView和 mixins扩展类结合的进一步封装

    • CreateAPIView(mixins.CreateModelMixin,GenericAPIView)
      # Concrete view for creating a model instance.
      # 创建模型实例
      
    • ListAPIView(mixins.ListModelMixin,GenericAPIView)
      # Concrete view for listing a queryset.
      # 列出查询集
      
    • RetrieveAPIView(mixins.RetrieveModelMixin,GenericAPIView)
      # Concrete view for retrieving a model instance.
      # 检索该实例的详细信息
      
    • DestroyAPIView(mixins.DestroyModelMixin,GenericAPIView)
      # Concrete view for deleting a model instance.
      # 删除该模型实例
      
    • UpdateAPIView(mixins.UpdateModelMixin,GenericAPIView)
      # Concrete view for updating a model instance.
      # 更新该模型实例的信息
      
  • GenericAPIView, mixins.ListModelMixin方法生成视图类的弊端

可以从上述代码看到我们需要手动绑定http请求方法和对应扩展类的方法(get和list)

  • 出现了GenericViewSet
# 继承了两个类
GenericViewSet(ViewSetMixin, generics.GenericAPIView)

# ViewSetMixin作用
# Overrides `.as_view()` so that it takes an `actions` keyword that performs the binding 	of HTTP methods to actions on the Resource.
# 绑定了请求方法和资源请求方式
# view = MyViewSet.as_view({'get': 'list', 'post': 'create'})
  • url绑定的两种方式

    • goodList = GoodsListViews.as_view({'get': 'list'})
      urlpatterns = [
          # …………
          path('goods/', goodList, name='goods'),
          # …………
      ]
      
    • # 实例化路由对象
      router = DefaultRouter()
      # 第一个参数为路由地址,第二个为视图类,第三个为别名
      router.register('goods', GoodsListViews, basename='goods')
      # 记得将该路由地址加入总的路由模式中去
      urlpatterns += router.urls
      
    • 官网上其实还有很多绑定的方式,例如使用Django的include函数,主要看自己

  • 官网上简单路由的对应

URL Style HTTP Method Action URL Name
{prefix}/ GET list {basename}-list
POST create
{prefix}/{url_path}/ GET, or as specified by methods argument @action(detail=False) decorated method {basename}-{url_name}
{prefix}/{lookup}/ GET retrieve {basename}-detail
PUT update
PATCH partial_update
DELETE destroy
{prefix}/{lookup}/{url_path}/ GET, or as specified by methods argument @action(detail=True) decorated method {basename}-{url_name}
  • 基于GenericAPIView的分页功能实现

    • # 使用全局的分页配置
      REST_FRAMEWORK = {
          # …………
          'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
          'PAGE_SIZE': 5
      }
      
    • # 自定义分页配置
      class LargeResultsSetPagination(PageNumberPagination):
          page_size = 1000
          page_size_query_param = 'page_size'
          max_page_size = 10000
          
      class BillingRecordsView(generics.ListAPIView):
          # …………
          pagination_class = LargeResultsSetPagination
      
  • 过滤功能

    django-filter文档

    class UserFilter(django_filters.FilterSet):
        max = django_filters.NumberFilter(field_name='shop_price', lookup_expr='gte')
        min = django_filters.NumberFilter(field_name='shop_price', lookup_expr='lte')
        class Meta:
            model = User
            fields = ['shop_price', 'last_login']
    
  • 搜索功能

# 注意导包 from rest_framework import filters
class GoodsListViews(GenericViewSet, ListModelMixin, RetrieveModelMixin):
    # …………
    # 设置过滤器后端
    filter_backends = [django_filters.rest_framework.DjangoFilterBackend, filters.SearchFilter]
    # …………
    search_fields = ['name', 'goods_brief']
  • 排序功能

  • 商品类别API的层级显示

# 首先显示一级分类,在显示一级分类下的子分类
# serializers.py
class CategorySerializers3(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = '__all__'
class CategorySerializers2(serializers.ModelSerializer):
    sub_category = CategorySerializers3(many=True)

    class Meta:
        model = Category
        fields = '__all__'
class CategorySerializers1(serializers.ModelSerializer):
    # 实现外键信息的嵌套
    # sub_category是当前Category的子分类
    sub_category = CategorySerializers2(many=True)
    # many = True表示子分类有多个
  • 商品分类的筛选
    def get_categoty_goods(self, queryset, name, value):
        return queryset.filter(Q(category_id=value) |
                               Q(category__parent_category_id=value) |                     		Q(category__parent_category__parent_category_id=value))
    # 根据分类进行筛选
    category = django_filters.NumberFilter(field_name='category', method='get_categoty_goods')

    # 当输入一级分类的时候,找出一级分类和2、3级分类
    # 当输入二级分类的时候,找出二级分类和三级分类
    # 三级分类直接筛选

DRF认证

  • 使用缓存的设置方案
# session适用于前后端都出于处于一台服务器上的场景
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ]
}
  • 使用Token的设置方案
# token使用于前后端分离处于不同服务器的场景
1 # To use the TokenAuthentication scheme you'll need to configure the authentication classes to include TokenAuthentication, and additionally include rest_framework.authtoken in your INSTALLED_APPS setting


2 INSTALLED_APPS = [
    ...
    'rest_framework.authtoken'
]
  • 什么是token?

    • Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。
    • Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
    • Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
  • token方案的设置

    1. INSTALLED_APPS = [
          ...
          'rest_framework.authtoken'
      ]
      
    2. REST_FRAMEWORK = {
          # …………
          'DEFAULT_AUTHENTICATION_CLASSES': [
              # 'rest_framework.authentication.BasicAuthentication',
              'rest_framework.authentication.TokenAuthentication',
          ]
      }
      
    3. from rest_framework.authtoken import views
      urlpatterns += [
          url(r'^api-token-auth/', views.obtain_auth_token)
      ]
      
  • 使用post访问,服务端返回token信息

$ curl -X POST -d 'username=XXXX&password=XXXXX' http://127.0.0.1:8000/api-token-auth/
  • DRF自带的Token的缺陷

    • token信息表格存在数据库中,不方便分布式管理
    • token信息无法设置失效时间
  • 使用djangorestframework-jwt

$ pip install djangorestframework-jwt
# 并在配置文件中注册
# 使用jwt认证
'DEFAULT_AUTHENTICATION_CLASSES': [
        # 'rest_framework.authentication.BasicAuthentication',
        # 'rest_framework.authentication.TokenAuthentication',
        "rest_framework_jwt.authentication.JSONWebTokenAuthentication",
    ]
# JWT的相关配置
JWT_AUTH = {
    # token的失效时间
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    # token前缀
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
urlpatterns = [
    # …………
    # 设置登录的路由
    url(r'^login/', obtain_jwt_token),
]
  • Django自带的用户认证功能只能验证用户名和密码登录

    重写后台认证的API,使得可以通过电话号码和密码登录

    # 在用户app定义视图类
    User = get_user_model()
    class CustomBackend(ModelBackend):
        # 该方法重写了ModelBackend中的认证方法
        def authenticate(self, request, username=None, password=None, **kwargs):
            try:
                user = User.objects.get(Q(username=username) |
                                        Q(mobile=username))
                if user.check_password(password) and self.user_can_authenticate(user):
                    return user
            except Exception as e:
                return None
    
    User = get_user_model()
    # 当你自定义用户模块的项目中
    # 你应该使用django.contrib.auth.get_user_model() 来引用用户模型,而不要直接引用		User。 这个方法将返回当前正在使用的用户模型 —— 指定的自定义用户模型或者User。
    # 当前使用的模型在配置文件中已经设置
    AUTH_USER_MODEL = 'user.UserProfile'
    
    # 编写自定义的后台验证,添加到配置文件中
    AUTHENTICATION_BACKENDS = {
        'app.user.views.CustomBackend'
    }
    

    用户短信验证

    • 使用云片网发送短信
    # sms.py
    import random
    import string
    from shopAPI.settings import APIKEY
    import requests
    
    class YunPian():
        def __init__(self, phone):
            # 单条发送短信
            self.url = 'https://sms.yunpian.com/v2/sms/single_send.json'
            self.apikey = APIKEY
            self.phone = phone
        @staticmethod
        def generate_code(count):
            return ''.join(random.sample(string.digits, count))
        
        def send_message(self, code):
            response = requests.post(self.url, data={
                'apikey': self.apikey,
                'text': '【XXtest】您的验证码是code'.replace('code', str(code)),
                'mobile': self.phone
            })
            return response
    
    if __name__ == '__main__':
        yunpian = YunPian(XXXX)
        code = YunPian.generate_code(count=4)
        response = yunpian.send_message(code)
        print(response.json())
    
    • 云片发送信息序列化
    # serializers.py
    User = get_user_model()
    class smsCodeSerializers(serializers.Serializer):
        # 注册时候发送验证码
        mobile = serializers.CharField(max_length=20)
    
        # 在序列化的时候进行数据验证(validate+验证字段名)
        def validate_mobile(self, mobile):
            # 如果该手机号已经被注册,则抛异常
            if User.objects.filter(mobile=mobile).count():
                raise serializers.ValidationError('该手机已被注册')
            # 判断该手机的形式是否合法
            if not re.match(REG_PATTERN, mobile):
                raise serializers.ValidationError('手机号码不合法')
            # 判断发送时间是否超过60s
            before_minute_time = datetime.now() - timedelta(minutes=1)
            # 拿出该手机号码的所有验证码信息,拿出最近的一条信息
            # codes = VerifyCode.objects.all().order_by('add_time')
            # recent_code_time = codes[0].add_time
            if VerifyCode.objects.filter(add_time__gt=before_minute_time, mobile=mobile):
                raise serializers.ValidationError('距离上一次发送短信未超过60s')
            return mobile
    
        class Meta:
            # 指定序列化的数据库模型
            model = VerifyCode
    
    • 编写云片发送信息视图
    # views.py
    class SmsView(GenericViewSet, CreateModelMixin):
        serializer_class = smsCodeSerializers
    
        # 需要重写create方法
        # 若不重写,在创建验证码的时候不会使用云片发送验证码,也不会将验证码存储到数据库中
        def create(self, request, *args, **kwargs):
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            # 从验证的信息中获取电话
            mobile = serializer.validated_data['mobile']
            yunpian = YunPian(mobile)
            code = YunPian.generate_code(count=6)
            response_status = yunpian.send_message(code).json()
            if response_status['code'] != 0:
                # 短信发送失败
                return Response(data={
                    'mobile': mobile,
                    'msg': response_status['msg']
                }, status=HTTP_400_BAD_REQUEST)
            else:
                # 如果发送成功,实例化验证码对象存入数据库中
                vcode = VerifyCode(mobile=mobile, code=code)
                vcode.save()
                return Response(serializer.data, status=HTTP_201_CREATED)
    
    
    • 编写路由
    # urls.py
    router.register('code', SmsView, basename='code')
    

用户注册API

  • 序列化类

class registerSerializers(serializers.Serializer):
    # 注册信息的序列化
    code = serializers.CharField(required=True, allow_blank=False,
                                 max_length=6, min_length=6,
                                 label='验证码',
                                 error_messages={
                                     'required': '请输入验证码',
                                     'allow_blank': '请输入验证码',
                                     'max_length': '验证码格式错误',
                                     'min_length': '验证码格式错误',
                                 })
    # UniqueValidator 用户名唯一验证
    username = serializers.CharField(label='用户名', required=True, 						allow_blank=False,validators=							[UniqueValidator(queryset=User.objects.all(),message='用户已存在')])
    mobile = serializers.CharField(label='电话', required=True, allow_blank=False,
                                   validators=[UniqueValidator(queryset=User.objects.all(),message='电话已注册')])
    # style={'input_type': 'password'} 表示密码不明文显示
    password = serializers.CharField(label='密码', required=True, 	allow_blank=False, min_length=6,error_messages={'min_length': '密码不能少于六位数'}, 	style={'input_type': 'password'})

    # 检验验证码是否过期
    def validate_code(self, code):
        codes = VerifyCode.objects.filter(mobile=self.initial_data['mobile']).order_by('-add_time')
        if codes:
            # 拿出最近的一个验证码
            recent_code = codes[0]
            ten_minutes_age = datetime.now() - timedelta(minutes=10)
            if ten_minutes_age > recent_code.add_time:
                raise serializers.ValidationError('验证码过期')
            if code != recent_code.code:
                raise serializers.ValidationError('验证码错误')
        else:
            raise serializers.ValidationError('请发送验证码')
        return code

    def validate(self, attrs):
        # 所有的字段信息存储在attrs字典中
        # 验证完成之后删除code属性,因为数据库表中没有code
        del attrs['code']
        return attrs

    class Meta:
        model = User
        field = ['username', 'mobile', 'code', 'password']
  • 视图类的编写

class RegisterView(GenericViewSet, CreateModelMixin):
    serializer_class = registerSerializers

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        username = serializer.validated_data['username']
        mobile = serializer.validated_data['mobile']
        password = serializer.validated_data['password']
        user = User(username=username, mobile=mobile, password=password)
        # 对密码进行加密
        user.set_password(password)
        user.save()
        return Response(data={
            'msg': '创建用户成功',
        }, status=status.HTTP_201_CREATED)
  • 路由的编写
# urls.py
router.register('register', RegisterView, basename='register')
  • 用户收藏API

    • 序列化
    class UserFavSerializers(serializers.ModelSerializer):
        # HiddenField当前用户,不显示
        # serializers.CurrentUserDefault()表示获取当前用户
        user = serializers.HiddenField(default=serializers.CurrentUserDefault())
    
        class Meta:
            model = UserFav
            fields = ['user', 'id', 'goods']
            # 对用户和收藏商品进行联合唯一验证
            validators = [
                UniqueTogetherValidator(
                    queryset=UserFav.objects.all(),
                    fields=('user', 'goods'),
                    message='该商品已经收藏'
                )
            ]
    
    class UserFavDetailSerializers(serializers.ModelSerializer):
        goods = GoodsSerializers()
    	# 外键覆盖,不只显示goods的id,还显示goods的详细信息
        class Meta:
            model = UserFav
            fields = '__all__'
    
    • 视图类的创建
    class UserFavView(GenericViewSet, CreateModelMixin,
                      ListModelMixin, RetrieveModelMixin, DestroyModelMixin):
        # queryset是返回所有的查询结果
        # queryset = UserFav.objects.all()
        # serializer_class = UserFavSerializers
        # 采用动态序列化,list时候展示收藏商品的详细信息
        def get_serializer_class(self):
            if self.action == 'list':
                return UserFavDetailSerializers
            else:
                return UserFavSerializers
        # 获取当前用户的收藏商品
        def get_queryset(self):
            # 获取当前用户
            user = self.request.user
            # 如果当前用户存在,拿出他的收藏商品,否则返回空
            if user:
                return UserFav.objects.filter(user_id=self.request.user.pk)
            else:
                return None
    

Gitee连接:

项目地址
https://gitee.com/hugeblackdog/shopAPI

你可能感兴趣的:(python,django)