零基础入门python36:用 Django Session 实现购物车

零基础入门python36:用 Django Session 实现购物车

一、上一篇课后练习讲解

订单详情对象级权限应使用 get_queryset().filter(user=self.request.user),而不是先 get(pk=id) 再在模板中判断,否则错误对象可能已经泄露。

上一篇课后练习完整答案

上一篇练习的要求已落实到下面完整文件;先运行项目测试,再用 curl 对照状态码和数据库持久化结果

答案要点:认证视图用 authenticate/login/logout,权限矩阵区分游客、普通用户、staff;会话安全与 CSRF 不靠关闭中间件解决。

文件:shop/auth_views.py

完整参考答案文件

完整文件:shop/auth_views.py

python 复制代码
import json
from django.contrib.auth import authenticate, login, logout
from django.http import JsonResponse
def login_view(request):
    data = json.loads(request.body or "{}")
    user = authenticate(request, username=data.get("username"), password=data.get("password"))
    if user is None: return JsonResponse({"error": "invalid_credentials"}, status=401)
    login(request, user); return JsonResponse({"id": user.id})
def logout_view(request):
    logout(request); return JsonResponse({"status": "logged_out"})

完整参考答案文件

本篇对应的交付源码完整文件:django-shop/shop/views.py

python 复制代码
from rest_framework import generics, permissions, status, viewsets
from rest_framework.response import Response
from .models import Category, Order, Product
from .serializers import CategorySerializer, CheckoutSerializer, OrderSerializer, ProductSerializer
from .services import create_order

class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Category.objects.order_by('name')
    serializer_class = CategorySerializer

class ProductViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = ProductSerializer
    def get_queryset(self):
        query = Product.objects.filter(is_active=True).select_related('category')
        if category := self.request.query_params.get('category'): query = query.filter(category_id=category)
        if keyword := self.request.query_params.get('q'): query = query.filter(name__icontains=keyword)
        return query.order_by('-created_at')

class CheckoutView(generics.GenericAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = CheckoutSerializer
    def post(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        order = create_order(request.user, serializer.validated_data['items'])
        return Response(OrderSerializer(order).data, status=201)

class MyOrderViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = OrderSerializer
    def get_queryset(self):
        return Order.objects.filter(user=self.request.user).prefetch_related('items').order_by('-id')

class CartView(generics.GenericAPIView):
    def get(self, request):
        return Response(request.session.get('cart', {}))

    def post(self, request):
        product = generics.get_object_or_404(Product, id=request.data.get('product_id'), is_active=True)
        quantity = int(request.data.get('quantity', 1))
        if quantity < 1 or quantity > product.stock:
            return Response({'message': '数量必须大于0且不能超过库存'}, status=status.HTTP_400_BAD_REQUEST)
        cart = request.session.setdefault('cart', {})
        cart[str(product.id)] = quantity
        request.session.modified = True
        return Response(cart, status=status.HTTP_201_CREATED)

    def delete(self, request):
        cart = request.session.get('cart', {})
        cart.pop(str(request.data.get('product_id')), None)
        request.session.modified = True
        return Response(cart)

验收命令:python -m pytest -q(Django 项目使用 python manage.py test)。预期测试通过;若失败先检查迁移、配置和事务回滚。

二、本篇完成什么

购物车保存商品 id 和数量,但不保存价格。价格和库存属于数据库事实,下单时必须重新读取。

三、Session购物车代码

python 复制代码
class CartView(generics.GenericAPIView):
    def post(self, request):
        product = generics.get_object_or_404(Product, id=request.data.get('product_id'), is_active=True)
        quantity = int(request.data.get('quantity', 1))
        if quantity < 1 or quantity > product.stock:
            return Response({'message': '数量必须大于0且不能超过库存'}, status=400)
        cart = request.session.setdefault('cart', {})
        cart[str(product.id)] = quantity
        request.session.modified = True
        return Response(cart, status=201)

request.session.modified = True 告诉 Django 嵌套字典发生了变化。购物车只是临时意图,不是订单;用户关闭浏览器或 Session 过期后丢失是可接受的。

四、验收

加入库存 5 的商品 2 件返回 201;加入 9 件返回 400;删除后 Session 中没有商品 id。课后练习:实现购物车数量增加/减少接口,并拒绝减少到 0 以下。

项目增量:Session 购物车的边界

购物车暂不写数据库,使用 Session 保存 product_id 到 quantity 的映射;价格和库存永远从数据库重新读取,不能相信 Session 中的旧价格。

python 复制代码
cart = request.session.get('cart', {})
cart[str(product.id)] = cart.get(str(product.id), 0) + quantity
request.session['cart'] = cart
request.session.modified = True

数量必须限制上限,商品下架或库存不足时结算重新校验。课后练习实现删除购物车商品和过期商品清理,下一篇创建订单快照。

五、上一篇练习讲解:Session 不是数据库

Session 里只保存商品 id 和数量,例如 {"3": 2}。它适合保存短期、可丢失的草稿,不适合保存价格、库存和支付状态。商品价格变化后,购物车页面应实时读取 Product.objects.filter(id__in=ids, is_active=True),缺失的商品从 Session 清理。

python 复制代码
def clean_cart(request):
    cart = request.session.get("cart", {})
    valid = Product.objects.filter(id__in=cart.keys(), is_active=True)
    valid_ids = {str(p.id) for p in valid}
    new_cart = {pid: qty for pid, qty in cart.items() if pid in valid_ids and qty > 0}
    if new_cart != cart:
        request.session["cart"] = new_cart
        request.session.modified = True
    return new_cart

六、数量增减接口和边界

python 复制代码
def change_cart(request, product_id):
    if request.method not in {"POST", "PATCH"}:
        return JsonResponse({"error": "method_not_allowed"}, status=405)
    product = get_object_or_404(Product, id=product_id, is_active=True)
    quantity = int((json.loads(request.body) or {}).get("quantity", 0))
    if quantity < 0 or quantity > 99:
        return JsonResponse({"error": "quantity_invalid"}, status=400)
    cart = request.session.get("cart", {})
    if quantity == 0:
        cart.pop(str(product.id), None)
    else:
        cart[str(product.id)] = quantity
    request.session["cart"] = cart
    request.session.modified = True
    return JsonResponse({"cart": cart})

这里先验证商品仍在售,再修改 Session;结账时还要再次锁库存。quantity=0 代表删除,客户端不需要额外设计一个删除请求。不能信任 Session 中的数量,服务端要限制单品上限并在结账时重新检查总量。

七、并发和 Session 覆盖

两个请求同时修改同一 Session 可能发生"后写覆盖前写"。如果购物车需要跨设备同步,应把购物车放到数据库并按用户加版本号;课程项目先接受 Session 的短期语义,并在文档中明确刷新页面可能丢失并发更新。

八、运行验收、排错与本篇练习

powershell 复制代码
python manage.py test shop.tests.CartTests -v 2

预期覆盖新增、增加、减少到 0、下架商品和数量超过 99。SessionInterrupted 或 Session 不保存时,检查 SessionMiddleware;购物车返回字符串 id 却查不到商品,通常是把整数和字符串 key 混用。

本篇练习:实现删除接口和过期商品清理,并在测试中先下架商品再访问购物车,断言它被自动移除。下一篇开始创建订单和价格快照。

十、购物车展示的查询优化

购物车页面先把 Session 中的字符串 id 转成整数集合,再一次性 filter(id__in=ids) 查询商品,最后按 Session 中的顺序组装响应。不要在循环里逐个 Product.objects.get,否则购物车 50 项就产生 50 次 SQL。缺失或下架商品从结果中剔除并同步修改 Session,用户看到的数量才与下一次结算一致。

本篇结束:完整模块文件

下面是交付项目中真实存在的完整文件 django-shop/shop/views.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码;复制单个函数会丢失上下文,因此这里提供整份文件。

python 复制代码
from rest_framework import generics, permissions, status, viewsets
from rest_framework.response import Response
from .models import Category, Order, Product
from .serializers import CategorySerializer, CheckoutSerializer, OrderSerializer, ProductSerializer
from .services import create_order

class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Category.objects.order_by('name')
    serializer_class = CategorySerializer

class ProductViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = ProductSerializer
    def get_queryset(self):
        query = Product.objects.filter(is_active=True).select_related('category')
        if category := self.request.query_params.get('category'): query = query.filter(category_id=category)
        if keyword := self.request.query_params.get('q'): query = query.filter(name__icontains=keyword)
        return query.order_by('-created_at')

class CheckoutView(generics.GenericAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = CheckoutSerializer
    def post(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        order = create_order(request.user, serializer.validated_data['items'])
        return Response(OrderSerializer(order).data, status=201)

class MyOrderViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = OrderSerializer
    def get_queryset(self):
        return Order.objects.filter(user=self.request.user).prefetch_related('items').order_by('-id')

class CartView(generics.GenericAPIView):
    def get(self, request):
        return Response(request.session.get('cart', {}))

    def post(self, request):
        product = generics.get_object_or_404(Product, id=request.data.get('product_id'), is_active=True)
        quantity = int(request.data.get('quantity', 1))
        if quantity < 1 or quantity > product.stock:
            return Response({'message': '数量必须大于0且不能超过库存'}, status=status.HTTP_400_BAD_REQUEST)
        cart = request.session.setdefault('cart', {})
        cart[str(product.id)] = quantity
        request.session.modified = True
        return Response(cart, status=status.HTTP_201_CREATED)

    def delete(self, request):
        cart = request.session.get('cart', {})
        cart.pop(str(request.data.get('product_id')), None)
        request.session.modified = True
        return Response(cart)
相关推荐
l12586533 分钟前
# LangGraph Tool Calling Agent 深度实战:从零构建 ReAct 循环与工具调用链
人工智能·python·自然语言处理·langchain·agent
reasonsummer41 分钟前
【办公类-119-03】20260901三个园区“国旗下讲话” 按班级组合docx模板(AI+excel+python、deepseek和豆包、微信自动私发)
python
慢云智慧空间1 小时前
从设备联网到空间理解,智能建筑的系统架构正在经历哪些关键变化?
python·系统架构
磁场转动100万匹3 小时前
PyTorch 手写数字识别实战:从数据加载到模型训练(零基础详解版)
人工智能·pytorch·python
青 春 记 忆9 小时前
零基础入门python30:Flask个人账本从空目录运行与阶段验收
python·flask·后端开发
l1258659 小时前
# LangGraph Memory机制深度解析:短期记忆与长期记忆的工程实践
前端·人工智能·python·langchain·bootstrap
金銀銅鐵11 小时前
斐波那契数列的个位数出现的周期是多少?
python·数学
whcyhhh12 小时前
头歌实践教学平台:数据科学与大数据技术导论(十二)
大数据·开发语言·python·数据清洗
qq_3164110312 小时前
专业的腹腔镜医疗器械物联网APP开发服务商
python