零基础入门python36:用 Django Session 实现购物车 📅 发布时间:2026/8/29 6:54:38 👁 浏览次数: 零基础入门python36用 Django Session 实现购物车一、上一篇课后练习讲解订单详情对象级权限应使用get_queryset().filter(userself.request.user)而不是先get(pkid)再在模板中判断否则错误对象可能已经泄露。上一篇课后练习完整答案上一篇练习的要求已落实到下面完整文件先运行项目测试再用 curl 对照状态码和数据库持久化结果答案要点认证视图用 authenticate/login/logout权限矩阵区分游客、普通用户、staff会话安全与 CSRF 不靠关闭中间件解决。文件shop/auth_views.py完整参考答案文件完整文件shop/auth_views.pyimportjsonfromdjango.contrib.authimportauthenticate,login,logoutfromdjango.httpimportJsonResponsedeflogin_view(request):datajson.loads(request.bodyor{})userauthenticate(request,usernamedata.get(username),passworddata.get(password))ifuserisNone:returnJsonResponse({error:invalid_credentials},status401)login(request,user);returnJsonResponse({id:user.id})deflogout_view(request):logout(request);returnJsonResponse({status:logged_out})完整参考答案文件本篇对应的交付源码完整文件django-shop/shop/views.pyfromrest_frameworkimportgenerics,permissions,status,viewsetsfromrest_framework.responseimportResponsefrom.modelsimportCategory,Order,Productfrom.serializersimportCategorySerializer,CheckoutSerializer,OrderSerializer,ProductSerializerfrom.servicesimportcreate_orderclassCategoryViewSet(viewsets.ReadOnlyModelViewSet):querysetCategory.objects.order_by(name)serializer_classCategorySerializerclassProductViewSet(viewsets.ReadOnlyModelViewSet):serializer_classProductSerializerdefget_queryset(self):queryProduct.objects.filter(is_activeTrue).select_related(category)ifcategory:self.request.query_params.get(category):queryquery.filter(category_idcategory)ifkeyword:self.request.query_params.get(q):queryquery.filter(name__icontainskeyword)returnquery.order_by(-created_at)classCheckoutView(generics.GenericAPIView):permission_classes(permissions.IsAuthenticated,)serializer_classCheckoutSerializerdefpost(self,request):serializerself.get_serializer(datarequest.data)serializer.is_valid(raise_exceptionTrue)ordercreate_order(request.user,serializer.validated_data[items])returnResponse(OrderSerializer(order).data,status201)classMyOrderViewSet(viewsets.ReadOnlyModelViewSet):permission_classes(permissions.IsAuthenticated,)serializer_classOrderSerializerdefget_queryset(self):returnOrder.objects.filter(userself.request.user).prefetch_related(items).order_by(-id)classCartView(generics.GenericAPIView):defget(self,request):returnResponse(request.session.get(cart,{}))defpost(self,request):productgenerics.get_object_or_404(Product,idrequest.data.get(product_id),is_activeTrue)quantityint(request.data.get(quantity,1))ifquantity1orquantityproduct.stock:returnResponse({message:数量必须大于0且不能超过库存},statusstatus.HTTP_400_BAD_REQUEST)cartrequest.session.setdefault(cart,{})cart[str(product.id)]quantity request.session.modifiedTruereturnResponse(cart,statusstatus.HTTP_201_CREATED)defdelete(self,request):cartrequest.session.get(cart,{})cart.pop(str(request.data.get(product_id)),None)request.session.modifiedTruereturnResponse(cart)验收命令python -m pytest -qDjango 项目使用 python manage.py test。预期测试通过若失败先检查迁移、配置和事务回滚。二、本篇完成什么购物车保存商品 id 和数量但不保存价格。价格和库存属于数据库事实下单时必须重新读取。三、Session购物车代码classCartView(generics.GenericAPIView):defpost(self,request):productgenerics.get_object_or_404(Product,idrequest.data.get(product_id),is_activeTrue)quantityint(request.data.get(quantity,1))ifquantity1orquantityproduct.stock:returnResponse({message:数量必须大于0且不能超过库存},status400)cartrequest.session.setdefault(cart,{})cart[str(product.id)]quantity request.session.modifiedTruereturnResponse(cart,status201)request.session.modified True告诉 Django 嵌套字典发生了变化。购物车只是临时意图不是订单用户关闭浏览器或 Session 过期后丢失是可接受的。四、验收加入库存 5 的商品 2 件返回 201加入 9 件返回 400删除后 Session 中没有商品 id。课后练习实现购物车数量增加/减少接口并拒绝减少到 0 以下。项目增量Session 购物车的边界购物车暂不写数据库使用 Session 保存 product_id 到 quantity 的映射价格和库存永远从数据库重新读取不能相信 Session 中的旧价格。cartrequest.session.get(cart,{})cart[str(product.id)]cart.get(str(product.id),0)quantity request.session[cart]cart request.session.modifiedTrue数量必须限制上限商品下架或库存不足时结算重新校验。课后练习实现删除购物车商品和过期商品清理下一篇创建订单快照。五、上一篇练习讲解Session 不是数据库Session 里只保存商品 id 和数量例如{3: 2}。它适合保存短期、可丢失的草稿不适合保存价格、库存和支付状态。商品价格变化后购物车页面应实时读取Product.objects.filter(id__inids, is_activeTrue)缺失的商品从 Session 清理。defclean_cart(request):cartrequest.session.get(cart,{})validProduct.objects.filter(id__incart.keys(),is_activeTrue)valid_ids{str(p.id)forpinvalid}new_cart{pid:qtyforpid,qtyincart.items()ifpidinvalid_idsandqty0}ifnew_cart!cart:request.session[cart]new_cart request.session.modifiedTruereturnnew_cart六、数量增减接口和边界defchange_cart(request,product_id):ifrequest.methodnotin{POST,PATCH}:returnJsonResponse({error:method_not_allowed},status405)productget_object_or_404(Product,idproduct_id,is_activeTrue)quantityint((json.loads(request.body)or{}).get(quantity,0))ifquantity0orquantity99:returnJsonResponse({error:quantity_invalid},status400)cartrequest.session.get(cart,{})ifquantity0:cart.pop(str(product.id),None)else:cart[str(product.id)]quantity request.session[cart]cart request.session.modifiedTruereturnJsonResponse({cart:cart})这里先验证商品仍在售再修改 Session结账时还要再次锁库存。quantity0代表删除客户端不需要额外设计一个删除请求。不能信任 Session 中的数量服务端要限制单品上限并在结账时重新检查总量。七、并发和 Session 覆盖两个请求同时修改同一 Session 可能发生“后写覆盖前写”。如果购物车需要跨设备同步应把购物车放到数据库并按用户加版本号课程项目先接受 Session 的短期语义并在文档中明确刷新页面可能丢失并发更新。八、运行验收、排错与本篇练习python manage.py test shop.tests.CartTests-v 2预期覆盖新增、增加、减少到 0、下架商品和数量超过 99。SessionInterrupted或 Session 不保存时检查SessionMiddleware购物车返回字符串 id 却查不到商品通常是把整数和字符串 key 混用。本篇练习实现删除接口和过期商品清理并在测试中先下架商品再访问购物车断言它被自动移除。下一篇开始创建订单和价格快照。十、购物车展示的查询优化购物车页面先把 Session 中的字符串 id 转成整数集合再一次性filter(id__inids)查询商品最后按 Session 中的顺序组装响应。不要在循环里逐个Product.objects.get否则购物车 50 项就产生 50 次 SQL。缺失或下架商品从结果中剔除并同步修改 Session用户看到的数量才与下一次结算一致。本篇结束完整模块文件下面是交付项目中真实存在的完整文件 django-shop/shop/views.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码复制单个函数会丢失上下文因此这里提供整份文件。fromrest_frameworkimportgenerics,permissions,status,viewsetsfromrest_framework.responseimportResponsefrom.modelsimportCategory,Order,Productfrom.serializersimportCategorySerializer,CheckoutSerializer,OrderSerializer,ProductSerializerfrom.servicesimportcreate_orderclassCategoryViewSet(viewsets.ReadOnlyModelViewSet):querysetCategory.objects.order_by(name)serializer_classCategorySerializerclassProductViewSet(viewsets.ReadOnlyModelViewSet):serializer_classProductSerializerdefget_queryset(self):queryProduct.objects.filter(is_activeTrue).select_related(category)ifcategory:self.request.query_params.get(category):queryquery.filter(category_idcategory)ifkeyword:self.request.query_params.get(q):queryquery.filter(name__icontainskeyword)returnquery.order_by(-created_at)classCheckoutView(generics.GenericAPIView):permission_classes(permissions.IsAuthenticated,)serializer_classCheckoutSerializerdefpost(self,request):serializerself.get_serializer(datarequest.data)serializer.is_valid(raise_exceptionTrue)ordercreate_order(request.user,serializer.validated_data[items])returnResponse(OrderSerializer(order).data,status201)classMyOrderViewSet(viewsets.ReadOnlyModelViewSet):permission_classes(permissions.IsAuthenticated,)serializer_classOrderSerializerdefget_queryset(self):returnOrder.objects.filter(userself.request.user).prefetch_related(items).order_by(-id)classCartView(generics.GenericAPIView):defget(self,request):returnResponse(request.session.get(cart,{}))defpost(self,request):productgenerics.get_object_or_404(Product,idrequest.data.get(product_id),is_activeTrue)quantityint(request.data.get(quantity,1))ifquantity1orquantityproduct.stock:returnResponse({message:数量必须大于0且不能超过库存},statusstatus.HTTP_400_BAD_REQUEST)cartrequest.session.setdefault(cart,{})cart[str(product.id)]quantity request.session.modifiedTruereturnResponse(cart,statusstatus.HTTP_201_CREATED)defdelete(self,request):cartrequest.session.get(cart,{})cart.pop(str(request.data.get(product_id)),None)request.session.modifiedTruereturnResponse(cart)