
零基础入门python40商品、分类 API 与筛选一、上一篇课后练习讲解订单项小计应使用SerializerMethodField或模型 property 计算price * quantity不能让客户端提交小计。它是由事实字段推导出的只读结果。上一篇课后练习完整答案上一篇练习的要求已落实到下面完整文件先运行项目测试再用 curl 对照状态码和数据库持久化结果答案要点ProductSerializer 校验价格并声明只读 idModel/Serializer/Service 三层职责分开关联输出用 select_related/prefetch_related。文件shop/serializers.py完整参考答案文件完整文件shop/serializers.pyfromrest_frameworkimportserializersfrom.modelsimportProductclassProductSerializer(serializers.ModelSerializer):classMeta:modelProduct fields(id,name,price,stock,category)read_only_fields(id,)defvalidate_price(self,value):ifvalue0:raiseserializers.ValidationError(price_invalid)returnvalue完整参考答案文件本篇对应的交付源码完整文件django-shop/shop/serializers.pyfromrest_frameworkimportserializersfrom.modelsimportCategory,Order,OrderItem,ProductclassCategorySerializer(serializers.ModelSerializer):classMeta:modelCategory;fields(id,name,slug)classProductSerializer(serializers.ModelSerializer):category_nameserializers.CharField(sourcecategory.name,read_onlyTrue)classMeta:modelProduct;fields(id,category,category_name,name,slug,price,stock)classOrderItemSerializer(serializers.ModelSerializer):classMeta:modelOrderItem;fields(product,product_name,price,quantity)classOrderSerializer(serializers.ModelSerializer):itemsOrderItemSerializer(manyTrue,read_onlyTrue)classMeta:modelOrder;fields(id,status,total_amount,created_at,items)classCheckoutItemSerializer(serializers.Serializer):product_idserializers.IntegerField(min_value1)quantityserializers.IntegerField(min_value1,max_value99)classCheckoutSerializer(serializers.Serializer):itemsCheckoutItemSerializer(manyTrue,allow_emptyFalse)验收命令python -m pytest -qDjango 项目使用 python manage.py test。预期测试通过若失败先检查迁移、配置和事务回滚。二、本篇完成什么使用 ReadOnlyModelViewSet 和 Router 创建分类、商品列表/详情 API支持分类和关键词筛选。三、ViewSet查询classProductViewSet(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)公开 API 只返回上架商品Admin 可看到全部商品。select_related避免序列化category_name时产生 N1。四、验收请求/api/products/?category1qPython确认只返回匹配商品下架商品不可见。课后练习增加价格范围筛选并限制非法负数。项目增量商品分类 API 和筛选列表接口用 query_params 组合分类、价格区间和关键词过滤所有值由 Serializer 或 FilterSet 校验分页由 DRF 统一处理。querysetProduct.objects.filter(is_activeTrue).select_related(category)ifcategory:querysetqueryset.filter(category__slugcategory)ifkeyword:querysetqueryset.filter(name__icontainskeyword)验收无结果、超大 page_size 和不存在分类课后练习增加排序白名单下一篇实现下单 API。五、上一篇练习讲解排序必须白名单排序参数不能直接传给order_by(request.query_params[ordering])否则用户可以让接口按不存在字段报错甚至在原生 SQL 中造成注入。白名单做法ORDERING{price:price,-price:-price,newest:-created_at}orderingORDERING.get(request.query_params.get(ordering,newest),-created_at)querysetProduct.objects.filter(is_activeTrue).order_by(ordering)未知排序可以选择回退默认值也可以返回 400课程接口选择回退并在日志记录一次 warning避免移动端旧版本因为新增排序字段而中断。六、筛选、分页和查询计划classProductListView(generics.ListAPIView):serializer_classProductSerializer pagination_classPageNumberPaginationdefget_queryset(self):paramsself.request.query_params qsProduct.objects.filter(is_activeTrue).select_related(category)ifslug:params.get(category):qsqs.filter(category__slugslug)ifq:params.get(q):qsqs.filter(name__icontainsq)if(low:params.get(min_price))isnotNone:qsqs.filter(price__gteDecimal(low))if(high:params.get(max_price))isnotNone:qsqs.filter(price__lteDecimal(high))returnqs.order_by(ORDERING.get(params.get(ordering),-created_at))价格参数应由 FilterSet 或 Serializer 校验示例省略的Decimal(low)需要捕获InvalidOperation否则非法输入会 500。select_related让列表显示分类名时只有一次查询分页器限制page_size防止一次返回过多数据。七、验收输出与故障排查curlhttp://127.0.0.1:8000/api/products/?categorybooksqPythonordering-pricepage_size2响应应包含count/next/previous/results下架商品不应出现在results。若page_size100000仍返回全部检查分页器是否设置max_page_size若分类筛选无效确认 URL 使用 slug 而不是数字 id若 SQL 数量随结果条数增长检查是否漏掉select_related。八、本篇练习增加不存在分类返回空列表、非法价格返回 400、排序白名单和分页上限测试。下一篇会把购物车交给下单 service并处理库存不足与幂等键。九、筛选接口的产品约定明确空结果返回 200 还是 404列表通常返回 200 空数组分类不存在不应泄露数据库异常价格上下界都包含若min_price max_price返回 400。把这些约定写进 OpenAPI 描述和测试前端就能可靠地组合搜索条件而不是根据一次偶然响应猜规则。本篇结束完整模块文件下面是交付项目中真实存在的完整文件 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)