1、此次项目,关于商品类别分类需要两个接口,一个是head部的全部分类,用于header首页的全部分类,一级、二级、三级分类等;另一个是侧边栏提供的分类
首页分类:
侧边栏分页:
1.1、对于商品类需要提供分页、过滤、搜索、排序等功能
1.2、对于商品分类,不需要提供上述功能
上一篇博文中我们已经实现了商品列表页API接口,现在我们接着实现商品分类的API接口
1)goods/views.py中创建分类CategoryViewset类:
serializers.py会创建CategorySerializer,用于序列化GoodsCategory商品分类。由于商品分类共有三级,我们通过在serializers.py中实现三级GoodsCategory序列化,再通过CategoryViewset中获取商品分类数据时传递进来的:category_type=1,实现三级分类顺序序列化classCategoryViewset(mixins.ListModelMixin,mixins.RetrieveModelMixin,viewsets.GenericViewSet):"""list:商品分类列表数据"""queryset=GoodsCategory.objects.filter(category_type=1)serializer_class=CategorySerializer
注:CategoryViewset需要继承mixins.RetrieveModelMixin,是为了商品分类接口实现后,我们可以通过url上加入ID就可以获取某个商品的所有信息,mixins.RetrieveModelMixin遵循restfulapi的规范(如:GET/zoos/ID:获取某个指定动物园的信息),已经帮我们将所有事做好了,不需要配置url等,只需要在商品分类api接口实现后,在url上加上/ID即可以直接获取某个商品的所有数据。
2)Serializers.py中添加三级分类的CategorySerializer:
classCategorySerializer3(serializers.ModelSerializer):classMeta:model=GoodsCategoryfields="__all__"classCategorySerializer2(serializers.ModelSerializer):sub_cat=CategorySerializer3(many=True)classMeta:model=GoodsCategoryfields="__all__"classCategorySerializer(serializers.ModelSerializer):"""商品一级类别序列化"""sub_cat=CategorySerializer2(many=True)#关联的是自身,拿到自己的下一类,不是单个,需加many=TrueclassMeta:model=GoodsCategoryfields="__all__"
当queryset=GoodsCategory.objects.filter(category_type=1),实例化CategorySerializer时,Serializers.py中CategorySerializer拿到category_type=1(一级分类)的所有数据进行序列化,
因里面又包含sub_cat=CategorySerializer2(many=True)二级GoodsCategory序列化,层层递进,就实现了三级分类按层次序列化。
3)配置到我们的Mxshop/url.py中:
#配置GoodsCategory的urlrouter.register(r'categories',CategoryViewset,base_name="categories")说明:
页面效果:
使用前后端分离的项目,不可避免的就会遇到跨域问题,一般解决方式有两种:
本节主要讲述服务器(后端)解决跨域问题
1)终端进入项目虚拟环境,安装django-cors-headers:
pipinstalldjango-cors-headers
2)Mxshop中的setting.py配置:
①、INSTALLED_APPS加上配置:注册INSTALLED_APPS=(...'corsheaders',...)
②、MIDDLEWARE中添加配置:添加中间件监听
MIDDLEWARE=[#OrMIDDLEWARE_CLASSESonDjango<1.10...'corsheaders.middleware.CorsMiddleware',#需要添加的'django.middleware.common.CommonMiddleware',#本来存在...]
注:新添加的CorsMiddleware要放在CommonMiddleware前面
③、setting中直接增加CORS_ORIGIN_ALLOW_ALL设置:为True则表示所有域名下都能访问(忽略跨域问题),默认为False
#IfTrue,thewhitelistwillnotbeusedandalloriginswillbeaccepted.DefaultstoFalse.CORS_ORIGIN_ALLOW_ALL=True④、设置CORS_ORIGIN_WHITELIST:允许访问的白名单,只有在白名单内的可以忽略跨域问题访问(第③步设为False):
#CORSCORS_ORIGIN_WHITELIST=('127.0.0.1:8080','localhost:8080','www.meiduo.site:8080','api.meiduo.site:8000')CORS_ALLOW_CREDENTIALS=True#允许携带cookie
也可以定义允许的匹配路径正则表达式:
⑤、设置允许访问的方法:
CORS_ALLOW_METHODS=('DELETE','GET','OPTIONS','PATCH','POST','PUT','VIEW',)
⑥、设置允许的header:
CORS_ALLOW_HEADERS=('XMLHttpRequest','X_FILENAME','accept-encoding','authorization','content-type','dnt','origin','user-agent','x-csrftoken','x-requested-with','Pragma',)注:以上都是在setting中配置。
跨域问题解决后,就可以来查看Django中数据,是否能跟前端vue接上了--前端使用vue语言,页面已经处理好,只要django数据接口设置好,跨域问题解决,前端就可以根据url获取django中提供的数据
首先,vue展示商品分类数据(上述已经做好后端--商品分类api接口):
1)在vue中的api.js文件中设置我们django的url及端口:
//获取商品类别信息exportconstgetCategory=params=>{if('id'inparams){returnaxios.get(`${loclahost}/categorys/`+params.id+'/');#获取8000端口下的单个商品分类数据}else{returnaxios.get(`${loclahost}/categorys/`,params);#获取8000端口下的所有商品分类数据}};
此时,访问url:127.0.0.1:8080,页面效果:
注意,上述操作前提需要pycharm中启动mxshop项目,终端启动vue前端项目
接下来是商品一级分类的过滤,如上图所示,当我们点击导航栏上的商品类别时,比如点击生鲜食品、酒水饮料,这些都是属于一级类目的,我们需要提供一级类目的接口给前端调用,这样前端页面可以显示出该一级商品下的所有二级商品分类及所有三级商品:
我们需要在goods→filter.py中,自定义过滤方法:
fromdjango.db.modelsimportQimportdjango_filtersfrom.modelsimportGoodsclassGoodsFilter(django_filters.rest_framework.FilterSet):'''自定义过滤器,实现区间过滤商品过滤的类'''#filters.NumberFilter有两个参数,name是要过滤的字段,lookup是执行的行为,‘小与等于本店价格’pricemin=django_filters.NumberFilter(field_name="shop_price",lookup_expr='gte')pricemax=django_filters.NumberFilter(field_name="shop_price",lookup_expr='lte')#方法:自定义过滤条件,过滤一级类目,field_name:字段名,method:指定自定义方法top_category=django_filters.NumberFilter(field_name="category",method='top_category_filter')deftop_category_filter(self,queryset,name,value):#value:一级类目,如果两个值相等,说明是过滤一级类目returnqueryset.filter(Q(category_id=value)|Q(category__parent_category_id=value)|Q(category__parent_category__parent_category_id=value))classMeta:model=Goodsfields=['pricemin','pricemax','top_category']
前后端分离的系统,不需要做crsf的验证。app和网站服务端本来就是跨站了,
drf为我们提供了三种不同的authen验证:
1)setting配置中INSTALLED_APPS添加:
INSTALLED_APPS=(...'rest_framework.authtoken'#新添加drf自带用户验证)
此时需要在在终端pythonmanage.pymakemigrations、migrate
原因在于authtoken会为我们创建一张关于token的表,所以需要做上述操作
注:凡是有表产生的都要注册到,INSTALLED_APPS中
2)url.py配置:
我们只需要将token令牌给到前端,在用户访问时将令牌以name:value(Authorization:Token9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b)的格式放在header返回到后端验证就可以了。后端再通过request.auth获取token值进行验证。
fromrest_framework.authtokenimportviewsurlpatterns=[#tokenpath('api-token-auth/',views.obtain_auth_token)#新添加token验证]
3)客户端身份验证
对于客户端进行身份验证,令牌密钥应包含在AuthorizationHTTPheader中。关键字应以字符串文字“Token”为前缀,用空格分隔两个字符串。例如:
Authorization:Token9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b注意:如果您想在header中使用不同的关键字(例如Bearer),只需子类化TokenAuthentication并设置keyword类变量。
如果成功通过身份验证,TokenAuthentication将提供以下凭据。
未经身份验证的响应被拒绝将导致HTTP401Unauthorized的响应和相应的WWW-Authenticateheader。
要想获取request.user和request.auth还要在settings中添加:
REST_FRAMEWORK={'DEFAULT_AUTHENTICATION_CLASSES':('rest_framework.authentication.BasicAuthentication','rest_framework.authentication.SessionAuthentication','rest_framework.authentication.TokenAuthentication')}drf使用场景:
用户注册时,在后端注册逻辑里添加下面代码,可实现token获取并保存到数据库中
fromrest_framework.authtoken.modelsimportTokentoken=Token.object.create(user=...)print(token.key)#token令牌drf的token缺点:
局部验证流程:
1)注释掉:
#setting中REST_FRAMEWORK中的TokenAuthenticition:
2)在需要做验证的页面加入验证:
JWT是一个开放标准(RFC7519),它定义了一种用于简洁,自包含的用于通信双方之间以JSON对象的形式安全传递信息的方法。JWT可以使用HMAC算法或者是RSA的公钥密钥对进行签名。它具备两个特点
可以通过URL,POST参数或者在HTTPheader发送,因为数据量小,传输速度快
负载中包含了所有用户所需要的信息,避免了多次查询数据库
JWT使用(全局验证):
1)虚拟环境下安装:
pipinstalldjangorestframework-jwt
2)setting.py中的REST_FRAMEWORK添加:
REST_FRAMEWORK={'DEFAULT_AUTHENTICATION_CLASSES':('rest_framework.authentication.BasicAuthentication','rest_framework.authentication.SessionAuthentication',#'rest_framework.authentication.TokenAuthentication''rest_framework_jwt.authentication.JSONWebTokenAuthentication',#新添加,JWT验证)}
3)urls.py中配置:
1)setting.py中配置:
AUTHENTICATION_BACKENDS=('users.views.CustomBackend',#用户认证的path路径(users/vlews下的CustomBackend))
2)users/views.py:
setting.py中配置:
importdatetime#有效期限JWT_AUTH={#JWT全局配置'JWT_EXPIRATION_DELTA':datetime.timedelta(days=7),#有效期7天,也可以设置seconds=20(20秒)等'JWT_AUTH_HEADER_PREFIX':'JWT',#JWT跟前端保持一致,比如“token”,这里默认JWT}*测试:
web界面:
输入账号密码,post提交
提交成功,后端会直接给我们返回token,可以直接拿到前端中使用:
云片网注册成功,可以设置多个子账号,每个子账号都有一个apikey(很重要),发送短信验证码需要用到
发送短信测试:
1)在云片网ip白名单设置中,将本地ip添加进去,原因是云片网默认ip地址不能发邮件,需手动添加到白名单才可以
2)在apps中新建包:utils包,
3)在utils包中新建py文件:yunpian.py
4)yunpian.py编写代码:
项目中使用:
用户通过手机号注册,有验证码验证的环节,来实现用户注册时,drf实现发送短信验证码接口
手机号验证:
1)setting中设置手机号正则表达式、云片网apikey:
#手机号码正则表达式REGEX_MOBILE="^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"#云片网APIKEYAPIKEY="xxxxx327d4be01608xxxxxxxxxx"
2)在usersapp下新建serializers.py,用于验证码合法验证及序列化:
#users/serializers.pyimportrefromdatetimeimportdatetime,timedeltafromMxShop.settingsimportREGEX_MOBILEfromusers.modelsimportVerifyCodefromrest_frameworkimportserializersfromdjango.contrib.authimportget_user_modelUser=get_user_model()classSmsSerializer(serializers.Serializer):mobile=serializers.CharField(max_length=11)#函数名必须:validate+验证字段名defvalidate_mobile(self,mobile):"""手机号码验证"""#是否已经注册ifUser.objects.filter(mobile=mobile).count():raiseserializers.ValidationError("用户已经存在")#是否合法ifnotre.match(REGEX_MOBILE,mobile):raiseserializers.ValidationError("手机号码非法")#验证码发送频率#60s内只能发送一次one_mintes_ago=datetime.now()-timedelta(hours=0,minutes=1,seconds=0)ifVerifyCode.objects.filter(add_time__gt=one_mintes_ago,mobile=mobile).count():raiseserializers.ValidationError("距离上一次发送未超过60s")returnmobile
3)usersapp中的views.py新增SmsCodeViewset,继承于CreateModelMixin、viewsets.GenericViewSet,并重新重写CreateModelMixin的create方法:
4)url.py配置:
fromusers.viewsimportSmsCodeViewset#配置codes的urlrouter.register(r'code',SmsCodeViewset,base_name="code")
5)前端vue中访问后端url配置:
//短信exportconstgetMessage=parmas=>{returnaxios.post(`${loclahost}/code/`,parmas)}#说明:前端页面客户注册账号,点击获取验证码时触发此行代码,vue向后端code请求数据,获取手机验证码
云片网单条短信发送的使用说明:
编写UserRegSerializer类,用于用户注册序列化
说明:
1)write_only:views中create()方法会将所有数据返回到api接口中显示,设置此字段可被忽略,不被序列化返回到api接口显示
2)validate+字段名:如,validate_code,是对单个字段进行验证
3)validate(self,attrs):作用于所有字段,每个字段验证成功后都会以"username":"admin"的形式添加到attrs中,attrs是个字典类型,
存有所有字段:attrs:{"username":"1256485412","password":'123456'}
4)用户从前端post过来的值都会放入initial_data里面
5)drf中提供的字段验证,如单个字段验证:UniqueValidator
username=serializers.CharField(label="用户名",help_text="用户名",required=True,allow_blank=False,validators=[UniqueValidator(queryset=User.objects.all(),message="用户已经存在")])查找数据库中关于该字段的记录,如果有则报错:(message="用户已经存在")
drf提供的其他验证:
注意:username字段,用户填的必须是手机号,而不是姓名或昵称,本次项目用户注册用的手机号注册
2、views用户注册:
classUserViewset(CreateModelMixin,viewsets.GenericViewSet):"""用户注册"""serializer_class=UserRegSerializerqueryset=User.objects.all()
3、urls配置:
#用户注册router.register(r'users',UserViewset,base_name="users")
4、前端vueapi接口调用:
//注册exportconstregister=parmas=>{returnaxios.post(`${loclahost}/users/`,parmas)}
前端客户注册账号:获取验证码→填写注册信息post提交→触发urlsuser路由→viewsUserViewset→UserRegSeralizer序列化,验证通过→viewsUserViewset的create方法
→调用create方法的save保存数据时→触发UserRegSeralizer重载的create方法,将用户输入数据保存→同时,将序列化数据都显示到IPA接口让vue前端调用
注册成功:
除了上述在users/serializers.py中重载父类create方法给password加密的这种方式外,还有另一种方式:使用Django自带signals
首先,简单介绍下django自带的signals信号量,本项目主要讲Model_Signals:
当操作数据库数据时,会触发Model_Signals,即会发送全局信号,我们捕捉到信号便能做我们自己的逻辑操作,如
post_save:当新增数据库数据或更新数据库数据时会触发此方法,我们可以捕捉到并处理我们的逻辑
post_delete:删除数据时触发
好了,现在可以开始我们另一种方式注册客户账号了,
1)首先,将users/serializers.py下UserRegSerializer中注释掉关于密码加密并保存部分,其余代码不变:
#验证成功后,需要保存密码,后端调用.save()时,触发此处#create方法,密码没进行验证,只加密处理#defcreate(self,validated_data):#重载父类create方法#user=super(UserRegSerializer,self).create(validated_data=validated_data)#user.set_password(validated_data["password"])#user.save()#returnuser
2)在users下新建signals.py:
fromdjango.db.models.signalsimportpost_savefromdjango.dispatchimportreceiverfromdjango.contrib.authimportget_user_modelUser=get_user_model()#post_save:接收哪种信号,sender:接收哪个model的信号@receiver(post_save,sender=User)defcreate_auth_token(sender,instance=None,created=False,**kwargs):#判断是否新建,新建才需要设置密码并保存,因为update的时候也会触发post_saveifcreated:password=instance.passwordinstance.set_password(password)instance.save()
3)重载配置:
users/apps.py
#users/apps.pyfromdjango.appsimportAppConfigclassUsersConfig(AppConfig):name='users'verbose_name="用户管理"defready(self):#新增方法importusers.signals#importusers/signals.pyAppConfig自定义的函数,会在django启动时被运行,现在添加用户的时候,密码就会自动加密存储了
但本项目前端使用的注册页面是这样的:
即注册成功便跳转到某页面,此时是已经给用户生成token并给到客户cookie中,所以需要重载create方法,给用户设置token,让前端可以在api接口拿到。
生成token的两个重要步骤:一是payload,二是encode:
payload=jwt_payload_handler(user)re_dict["token"]=jwt_encode_handler(payload)重载views/UserViewset中的create方法:
fromrest_framework_jwt.serializersimportjwt_payload_handler,jwt_encode_handler#此时views中的UserViewset类的代码:classUserViewset(CreateModelMixin,viewsets.GenericViewSet):'''用户'''serializer_class=UserRegSerializerqueryset=User.objects.all()defcreate(self,request,*args,**kwargs):#重载create方法serializer=self.get_serializer(data=request.data)serializer.is_valid(raise_exception=True)user=self.perform_create(serializer)re_dict=serializer.datapayload=jwt_payload_handler(user)re_dict["token"]=jwt_encode_handler(payload)#设置tokenre_dict["name"]=user.nameifuser.nameelseuser.username#设置用户名headers=self.get_success_headers(serializer.data)returnResponse(re_dict,status=status.HTTP_201_CREATED,headers=headers)defperform_create(self,serializer):returnserializer.save()*Django内置信号量:
Modelsignalspre_init#django的modal执行其构造方法前,自动触发post_init#django的modal执行其构造方法后,自动触发pre_save#django的modal对象保存前,自动触发post_save#django的modal对象保存后,自动触发pre_delete#django的modal对象删除前,自动触发post_delete#django的modal对象删除后,自动触发m2m_changed#django的modal中使用m2m字段操作第三张表(add,remove,clear)前后,自动触发class_prepared#程序启动时,检测已注册的app中modal类,对于每一个类,自动触发Managementsignalspre_migrate#执行migrate命令前,自动触发post_migrate#执行migrate命令后,自动触发Request/responsesignalsrequest_started#请求到来前,自动触发request_finished#请求结束后,自动触发got_request_exception#请求异常后,自动触发Testsignalssetting_changed#使用test测试修改配置文件时,自动触发template_rendered#使用test测试渲染模板时,自动触发DatabaseWrappersconnection_created#创建数据库连接时,自动触发