ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

零基础入门python41:下单 API 与订单查询

零基础入门python41:下单 API 与订单查询 零基础入门python41下单 API 与订单查询一、上一篇课后练习讲解价格范围应把字符串转换成 Decimal再追加price__gte/price__lte负数和 min 大于 max 返回 400不要让数据库默默返回空列表掩盖输入错误。上一篇课后练习完整答案上一篇练习的要求已落实到下面完整文件先运行项目测试再用 curl 对照状态码和数据库持久化结果答案要点商品列表按在售、分类、关键词和价格过滤排序字段走白名单DRF 分页限制 page_size非法区间返回 400。文件shop/views.py完整参考答案文件完整文件shop/views.pyfromdecimalimportDecimal,InvalidOperationfromrest_frameworkimportgenericsfrom.modelsimportProductfrom.serializersimportProductSerializerclassProductList(generics.ListAPIView):serializer_classProductSerializerdefget_queryset(self):qsProduct.objects.filter(is_activeTrue).select_related(category)ifself.request.query_params.get(q):qsqs.filter(name__icontainsself.request.query_params[q])returnqs.order_by(-created_at)完整参考答案文件本篇对应的交付源码完整文件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。预期测试通过若失败先检查迁移、配置和事务回滚。二、本篇完成什么把 CheckoutSerializer、create_order 服务和 OrderSerializer 串成一个 HTTP 接口客户端只提交商品 id 和数量价格从数据库读取。三、视图代码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)视图不实现库存扣减Service 不知道 HTTP 状态码职责分开后事务可以独立测试。四、验收库存 5 下单 2 件返回 201、总额正确、库存变 3未登录返回 403库存不足返回 400 且无订单。课后练习结算成功后清空 Session 购物车。项目增量下单 API 的事务用例下单接口不能先返回订单再扣库存。Serializer 验证商品数量Service 在 atomic 内锁库存、创建价格快照和计算总额。transaction.atomicdefplace_order(user,items):orderOrder.objects.create(useruser,totalDecimal(0))totalDecimal(0)foriteminitems:productProduct.objects.select_for_update().get(pkitem[product_id])ifproduct.stockitem[quantity]:raiseOutOfStock()product.stockF(stock)-item[quantity]product.save(update_fields[stock])totalsnapshot_item(order,product,item[quantity])order.totaltotal order.save(update_fields[total])returnorder任何一步失败都回滚订单和库存。重复提交要使用幂等键否则刷新页面可能创建两个订单。课后练习增加 idempotency_key 的唯一约束。五、上一篇练习讲解筛选参数先校验再进 QuerySet商品列表中的价格区间应在 Serializer/FilterSet 中转成 Decimal排序只允许白名单字段。下单接口复用商品查询但不能复用“只显示在售”的查询而跳过锁定和库存校验。六、下单 API 的输入和幂等classOrderCreateSerializer(serializers.Serializer):itemsserializers.ListField(childserializers.DictField(),allow_emptyFalse)idempotency_keyserializers.UUIDField()defvalidate_items(self,items):foriteminitems:ifint(item.get(quantity,0))0:raiseserializers.ValidationError(数量必须大于 0)returnitemsclassOrderCreateView(APIView):permission_classes[IsAuthenticated]defpost(self,request):serializerOrderCreateSerializer(datarequest.data)serializer.is_valid(raise_exceptionTrue)keyserializer.validated_data[idempotency_key]oldOrder.objects.filter(userrequest.user,idempotency_keykey).first()ifold:returnResponse(OrderSerializer(old).data,status200)try:orderplace_order(request.user,serializer.validated_data[items],key)exceptOutOfStock:returnResponse({error:out_of_stock},status409)returnResponse(OrderSerializer(order).data,status201)幂等键必须和用户一起唯一同一个客户端重试得到原订单不能全局唯一否则不同用户恰好使用同一个 UUID 会互相冲突。数据库唯一约束仍需保留防止两个并发请求同时通过first()。七、错误码和事务边界库存不足是业务冲突返回 409输入格式错误是 400未登录是 401/403不要把所有异常都返回 500。place_order只负责事务和领域规则View 负责把异常映射成 HTTP这样 CLI、异步任务也能复用同一服务。八、运行结果与排错curl-Method Post http://127.0.0.1:8000/api/orders/-ContentType application/json-Headers {AuthorizationToken ...}-Body{idempotency_key:11111111-1111-1111-1111-111111111111,items:[{product_id:1,quantity:2}]}首次返回 201使用相同 key 重试返回 200 且订单 id 不变库存不足返回 409数据库中没有半张订单。若重复产生订单检查唯一约束迁移和place_order是否在同一atomic中。九、本篇练习增加订单创建后清空 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)
返回列表