专题:Vue+Django REST framework前后端分离生鲜电商
Vue+Django REST framework 打造前后端分离的生鲜电商项目(慕课网视频)。
Github地址:https://github.com/xyliurui/DjangoOnlineFreshSupermarket ;
Django版本:2.2、djangorestframework:3.9.2。
前端Vue模板可以直接联系我拿。
更多内容请点击 我的博客 查看,欢迎来访。
当访问商品详情时,将点击数+1
在 apps/goods/views.py 中的GoodsListViewSet
class GoodsListViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""
list:
显示商品列表,分页、过滤、搜索、排序
retrieve:
显示商品详情
"""
queryset = Goods.objects.all() # 使用get_queryset函数,依赖queryset的值
serializer_class = GoodsSerializer
pagination_class = GoodsPagination
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter,) # 将过滤器后端添加到单个视图或视图集
filterset_class = GoodsFilter
# authentication_classes = (TokenAuthentication, ) # 只在本视图中验证Token
search_fields = ('name', 'goods_desc', 'category__name') # 搜索字段
ordering_fields = ('click_num', 'sold_num', 'shop_price') # 排序
因为显示详情时继承了mixins.RetrieveModelMixin
按住Ctrl点击它,然后可以复制它的retrieve(self, request, *args, **kwargs)
方法
from rest_framework.response import Response
class GoodsListViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""
list:
显示商品列表,分页、过滤、搜索、排序
retrieve:
显示商品详情
"""
queryset = Goods.objects.all() # 使用get_queryset函数,依赖queryset的值
serializer_class = GoodsSerializer
pagination_class = GoodsPagination
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter,) # 将过滤器后端添加到单个视图或视图集
filterset_class = GoodsFilter
# authentication_classes = (TokenAuthentication, ) # 只在本视图中验证Token
search_fields = ('name', 'goods_desc', 'category__name') # 搜索字段
ordering_fields = ('click_num', 'sold_num', 'shop_price') # 排序
def retrieve(self, request, *args, **kwargs):
# 增加点击数
instance = self.get_object()
instance.click_num += 1
instance.save()
serializer = self.get_serializer(instance)
return Response(serializer.data)
[外链图片转存失败(img-Q0BgOiq1-1565763103092)(https://blog.starmeow.cn/media/blog/images/2019/08/BLOG_20190814_140437_18.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190814_140437_18.png”)]
进入商品详情时,增加点击数。
用户点击收藏+1,取消收藏-1
在 apps/user_operation/views.py 中UserFavViewSet
继承了mixins.CreateModelMixin
和mixins.DestroyModelMixin
按住Ctrl点击进去查看其中的方法,可以重写perform_create(self, serializer)
方法,将收藏数+1;重写perform_create(self, serializer)
方法,将收藏数-1。
class UserFavViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""
create:
用户收藏商品
destroy:
取消收藏商品
list:
显示收藏商品列表
retrieve:
根据商品id显示收藏详情
"""
queryset = UserFav.objects.all()
# serializer_class = UserFavSerializer
def get_serializer_class(self):
"""
不同的action使用不同的序列化
:return:
"""
if self.action == 'list':
return UserFavListSerializer # 显示用户收藏列表序列化
else:
return UserFavSerializer
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JWTAuthentication, SessionAuthentication) # 配置登录认证:支持JWT认证和DRF基本认证
lookup_field = 'goods_id'
def get_queryset(self):
# 过滤当前用户的收藏记录
return self.queryset.filter(user=self.request.user)
def perform_create(self, serializer):
# 添加收藏商品,商品收藏数+1
# 序列化保存,然后将它赋值给一个实例,也就是UserFav(models.Model)对象
instance = serializer.save()
# 获取其中的商品
goods = instance.goods
# 商品收藏数+1
goods.fav_num += 1
goods.save()
def perform_destroy(self, instance):
# 删除收藏商品,商品收藏数-1
goods = instance.goods
# 商品收藏数-1
goods.fav_num -= 1
if goods.fav_num < 0:
goods.fav_num = 0
goods.save()
instance.delete()
[外链图片转存失败(img-QCuGZi6w-1565763103093)(https://blog.starmeow.cn/media/blog/images/2019/08/BLOG_20190814_140427_72.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190814_140427_72.png”)]
进入调试模式,在这两个方法中添加断点,可以在API中添加收藏和删除收藏,当添加收藏时,收藏数+1,删除该收藏时,收藏数-1。
下面可以用Django的信号来实现,收藏注释掉上方的perform_create(self, serializer)
和perform_destroy(self, instance)
在 apps/user_operation/ 目录下创建 signals.py 文件,增加信号相关的代码
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from .models import UserFav
@receiver(post_save, sender=UserFav)
def userfav_post_save_handler(sender, instance=None, created=False, **kwargs):
# 首次创建时收藏数+1
if created:
goods = instance.goods
goods.fav_num += 1
goods.save()
@receiver(pre_delete, sender=UserFav)
def userfav_pre_delete_handler(sender, instance, **kwargs):
# 删除前发送信号
goods = instance.goods
# 商品收藏数-1
goods.fav_num -= 1
if goods.fav_num < 0:
goods.fav_num = 0
goods.save()
修改 apps/user_operation/init.py 增加以下代码
default_app_config = 'user_operation.apps.UserOperationConfig'
修改 apps/user_operation/apps.py 引入信号配置
from django.apps import AppConfig
class UserOperationConfig(AppConfig):
name = 'user_operation'
verbose_name = '操作'
def ready(self):
"""
在子类中重写此方法,以便在Django启动时运行代码。
:return:
"""
from .signals import userfav_post_save_handler, userfav_pre_delete_handler
接下来访问 http://127.0.0.1:8000/userfavs/ 进行收藏,取消收藏接口的测试,可以在信号相关代码上打上断点,看程序是否正常进入该位置。
收藏数已经从0变为1
进入收藏详情,删除收藏
刷新数据表,此时收藏数变为0
会引起商品库存数变化的请况有:
也就是与购物车相关功能有关
在 apps/trade/views.py 中有关于购物车相关的类ShoppingCartViewSet(viewsets.ModelViewSet)
,可以重写继承类的方法,来完成库存数修改,相较于信号来说更加灵活。
添加到购物车,重写方法
def perform_create(self, serializer):
serializer.save()
删除购物车,重写方法
def perform_destroy(self, instance):
instance.delete()
更新购物车,重写方法
def perform_update(self, serializer):
serializer.save()
先来打上断点,观察下serializer
的值,可以获取到更新前的库存量
[外链图片转存失败(img-j28uU6pd-1565763103094)(https://blog.starmeow.cn/media/blog/images/2019/08/BLOG_20190814_140348_77.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190814_140348_77.png”)]
修改ShoppingCartViewSet
,重写相关方法
class ShoppingCartViewSet(viewsets.ModelViewSet):
"""
购物车功能实现
list:
获取购物车列表
create:
添加商品到购物车
update:
更新购物车商品数量
delete:
从购物车中删除商品
"""
# 权限问题:购物车和用户权限关联,这儿和用户操作差不多
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly) # 用户必须登录才能访问
authentication_classes = (JWTAuthentication, SessionAuthentication) # 配置登录认证:支持JWT认证和DRF基本认证
# serializer_class = ShoppingCartSerializer # 使用get_serializer_class(),这个就不需要了
queryset = ShoppingCart.objects.all()
lookup_field = 'goods'
def get_serializer_class(self):
if self.action == 'list':
# 当获取购物车列表时,使用ModelSerializer,可以显示购物车商品详情
return ShoppingCartListSerializer
else:
return ShoppingCartSerializer
def get_queryset(self):
# 只能显示当前用户的购物车列表
return self.queryset.filter(user=self.request.user)
def perform_create(self, serializer):
# 添加到购物车,库存数减少
shop_cart = serializer.save()
goods = shop_cart.goods
# 商品的库存量goods_num,减去购物车中的数量
goods.goods_num -= shop_cart.nums
goods.save()
def perform_destroy(self, instance):
# 从购物车中删除,库存量减少
goods = instance.goods
# 商品的库存量goods_num,加上删除的数量
goods.goods_num += instance.nums
goods.save()
instance.delete()
def perform_update(self, serializer):
# 更新购物车中数量,先获取原来的数量,再进行更新
cart_goods = serializer.instance
old_cart_goods_num = cart_goods.nums # 获取购物车中该商品原来的数量
update_cart_goods = serializer.save()
diff_nums = update_cart_goods.nums - old_cart_goods_num # 现在的数量减去以前的数量
goods = cart_goods.goods
# 得到商品对象,更改库存量
goods.goods_num -= diff_nums
goods.save()
测试,首先将goods_num
初始值给10
选择该商品添加3个到购物车
[外链图片转存失败(img-Dw7jHI98-1565763103094)(https://blog.starmeow.cn/media/blog/images/2019/08/BLOG_20190814_140334_79.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190814_140334_79.png”)]
库存量减少到7
修改购物车中的数量
[外链图片转存失败(img-W0S7TgNU-1565763103095)(https://blog.starmeow.cn/media/blog/images/2019/08/BLOG_20190814_140321_92.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190814_140321_92.png”)]
库存量减少到5
从购物车删除该商品
库存量恢复到10
用户下单后,将购物车中的商品放到订单商品中。订单一旦取消,商品并未返回购物车,所以,需要将库存数量修改。
修改 apps/trade/views.py 中的OrderInfoViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet)
,重写删除订单的方法
def perform_destroy(self, instance):
instance.delete()
复制到OrderInfoViewSet
中
class OrderInfoViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet):
"""
订单管理
list:
获取个人订单
create:
新建订单
delete:
删除订单
detail:
订单详情
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly) # 用户必须登录才能访问
authentication_classes = (JWTAuthentication, SessionAuthentication) # 配置登录认证:支持JWT认证和DRF基本认证
queryset = OrderInfo.objects.all()
# serializer_class = OrderInfoSerializer # 添加序列化
def get_queryset(self):
return self.queryset.filter(user=self.request.user)
def get_serializer_class(self): # 动态序列化,当显示订单详情,用另一个Serializer
if self.action == 'retrieve':
return OrderInfoDetailSerializer
else:
return OrderInfoSerializer
def perform_create(self, serializer):
# 完成创建后保存到数据库,可以拿到保存的值
order = serializer.save()
shopping_carts = ShoppingCart.objects.filter(user=self.request.user)
# 将该用户购物车所有商品都取出来放在订单商品中
for shopping_cart in shopping_carts:
OrderGoods.objects.create(
order=order,
goods=shopping_cart.goods,
goods_nums=shopping_cart.nums
)
# 然后清空该用户购物车
shopping_carts.delete()
def perform_destroy(self, instance):
# 取消(删除)商品库存量增加
for order_goods in instance.order_goods.all():
goods = order_goods.goods
# 获取订单商品的数量,修改库存量
goods.goods_num += order_goods.goods_nums
goods.save()
instance.delete()
测试下单,取消订单
访问 http://127.0.0.1:8080/#/app/home/productDetail/105 直接在前端操作吧
添加1个到购物车
[外链图片转存失败(img-9TjVUZUa-1565763103096)(https://blog.starmeow.cn/media/blog/images/2019/08/BLOG_20190814_140247_48.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190814_140247_48.png”)]
商品库存量减少
但不进入支付,进入我的订单,点击取消
商品库存恢复10
一般销量变化发生在订单支付成功后。
修改 apps/trade/views.py 中的AliPayView(APIView)
class AliPayView(APIView):
def get(self, request):
# ......
def post(self, request):
"""
处理支付宝notify_url异步通知
:param request:
:return:
"""
processed_dict = {}
for key, value in request.POST.items():
processed_dict[key] = value
print('request.POST的值:', processed_dict)
sign = processed_dict.pop('sign', None) # 直接就是字符串了
server_ip = get_server_ip()
alipay = AliPay(
app_id=app_id, # 自己支付宝沙箱 APP ID
notify_url="http://{}:8000/alipay/return/".format(server_ip),
app_private_key_path=app_private_key_path, # 可以使用相对路径那个
alipay_public_key_path=alipay_public_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=alipay_debug, # 默认False,
return_url="http://{}:8000/alipay/return/".format(server_ip)
)
verify_result = alipay.verify(processed_dict, sign) # 验证签名,如果成功返回True
if verify_result:
order_sn = processed_dict.get('out_trade_no') # 原支付请求的商户订单号
trade_no = processed_dict.get('trade_no') # 支付宝交易凭证号
trade_status = processed_dict.get('trade_status') # 交易目前所处的状态
# 更新数据库订单状态
"""
OrderInfo.objects.filter(order_sn=order_sn).update(
trade_no=trade_no, # 更改交易号
pay_status=trade_status, # 更改支付状态
pay_time=timezone.now() # 更改支付时间
)
"""
orderinfos = OrderInfo.objects.filter(order_sn=order_sn)
for orderinfo in orderinfos:
orderinfo.trade_no = trade_no, # 更改交易号
orderinfo.pay_status = trade_status, # 更改支付状态
orderinfo.pay_time = timezone.now() # 更改支付时间
# 更改商品的销量
order_goods = orderinfo.order_goods.all()
for item in order_goods:
# 获取订单中商品和商品数量,然后将商品的销量进行增加
goods = item.goods
goods.sold_num += item.goods_nums
goods.save()
orderinfo.save()
# 给支付宝返回一个消息,证明已收到异步通知
# 当商户收到服务器异步通知并打印出 success 时,服务器异步通知参数 notify_id 才会失效。
# 也就是说在支付宝发送同一条异步通知时(包含商户并未成功打印出 success 导致支付宝重发数次通知),服务器异步通知参数 notify_id 是不变的。
return Response('success')
现在下单1个商品,并支付完成。确保Django运行到服务器,支付宝可以通过公网POST订单支付信息
刷新数据库,可以看到库存减1,销量加1
商品详情页数据也发生了变化
[外链图片转存失败(img-fiBDQYMh-1565763103097)(https://blog.starmeow.cn/media/blog/images/2019/08/BLOG_20190814_140202_55.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 “博客图集BLOG_20190814_140202_55.png”)]
采用redis的有序集合实现,每次搜索的关键字保存在redis,重复时将对应的分数+1
修改 apps/goods/views.py GoodsListViewSet
在商品过滤时提取集中的搜索关键字参数
class GoodsListViewSet(CacheResponseMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""
list:
显示商品列表,分页、过滤、搜索、排序
retrieve:
显示商品详情
"""
queryset = Goods.objects.all() # 使用get_queryset函数,依赖queryset的值
serializer_class = GoodsSerializer
pagination_class = GoodsPagination
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter,) # 将过滤器后端添加到单个视图或视图集
filterset_class = GoodsFilter
# authentication_classes = (TokenAuthentication, ) # 只在本视图中验证Token
search_fields = ('name', 'goods_desc', 'category__name') # 搜索字段
ordering_fields = ('click_num', 'sold_num', 'shop_price') # 排序
# throttle_classes = [UserRateThrottle, AnonRateThrottle] # DRF默认限速类,可以仿照写自己的限速类
throttle_scope = 'goods_list'
def retrieve(self, request, *args, **kwargs):
# 增加点击数
instance = self.get_object()
instance.click_num += 1
instance.save()
serializer = self.get_serializer(instance)
return Response(serializer.data)
def get_queryset(self):
keyword = self.request.query_params.get('search')
if keyword:
from utils.hotsearch import HotSearch
hot_search = HotSearch()
hot_search.save_keyword(keyword)
return self.queryset
在 utils/ 目录下创建 hotsearch.py ,用于关键字的保存和热搜排行
# 使用redis来记录热搜,并根据分数进行排序
import redis # 首先pip install redis安装好
class HotSearch(object):
def __init__(self):
pool = redis.ConnectionPool(host='localhost', port=6379, db=6, decode_responses=True)
self.r_conn = redis.Redis(connection_pool=pool) # 创建连接池,并进行连接
self.name = 'keyword:hot:search'
def save_keyword(self, keyword):
# 如果关键字已存在,分数+1
if self.r_conn.zscore(self.name, keyword):
self.r_conn.zincrby(self.name, amount=1, value=keyword)
else:
self.r_conn.zadd(self.name, {keyword: 1})
# print(self.r_conn.zrevrange(self.name, 0, 5, withscores=True))
def get_hotsearch(self):
hot_5 = self.r_conn.zrevrange(self.name, 0, 5)
# 得到一个关键字的列表
return hot_5
当然这个只能自己写api来序列化查询结果了,修改 apps/user_operation/views.py 添加HotSearchView
class HotSearchView(APIView):
def get(self, request):
from utils.hotsearch import HotSearch
from django.http import JsonResponse
from rest_framework.response import Response
from rest_framework import exceptions, status
import json
hot_search = HotSearch()
result = []
for keyword in hot_search.get_hotsearch():
tmp = dict()
tmp['keyword'] = keyword
result.append(tmp)
# return JsonResponse(result, safe=False)
return Response(result, status=status.HTTP_200_OK)
修改 DjangoOnlineFreshSupermarket/urls.py 添加路由
from user_operation.views import HotSearchView
urlpatterns = [
# ......
# 获取热搜
path('hotsearchs/', HotSearchView.as_view(), name='hotsearchs')
]
访问 http://127.0.0.1:8000/hotsearchs/ 可以查看热搜磅
在Vue前端页面中多搜索几个关键字
然后访问 http://127.0.0.1:8000/hotsearchs/ 可以查看结果
在 src/views/head/head.vue 获取热搜榜的函数是
getHotSearch() { //获取热搜
getHotSearch()
.then((response) => {
console.log('获取热搜榜:');
console.log(response.data);
this.hotSearch = response.data
})
.catch(function (error) {
console.log(error);
});
}
组件创建时,就请求 src/api/api.js 中的接口
//获取热门搜索关键词
export const getHotSearch = params => {
return axios.get(`${local_host}/hotsearchs/`)
};
然后遍历显示到页面中
<div class="head_search_hot">
<span>热搜榜:span>
<router-link v-for="item in hotSearch" :to="'/app/home/search/'+item.keyword" :key="item.keyword">
{{item.keyword}}
router-link>
div>