掌握Django(十二)

本文设计创建订单API。这是对API及安全设计的一次回顾。


设计订单API

设计订单API如下

其中POST操作需提供一个购物车ID,然后跟用户绑定,创建订单。其他一目了然。


获取订单

首先来实现获取订单API,因为创建订单稍有点复杂,我们先从简单的开始,然后一步一步实现。

先在 store–serializers.py 添加:

class OrderItemSerializer(serializers.ModelSerializer):

    product = SimpleProductSerializer()

    class Meta:

        model = OrderItem

        fields = [‘id’, ‘product’, ‘quantity’, ‘unit_price’]

class OrderSerializer(serializers.ModelSerializer):

    items = OrderItemSerializer(many=True)

    class Meta:

        model = Order

        fields = [‘id’, ‘placed_at’, ‘payment_status’, ‘customer’, ‘items’]

再添加 store–views.py 添加:

class OrderViewSet(ModelViewSet):

    queryset = Order.objects.all()

    serializer_class = OrderSerializer

上面代码先创建OrderItemSerialiazer和OrderSerializer序列类,其中的product关联到SimpleProductSerializer,再在views中展现。另外配置urls路由,以及需要修改OrderItem的模型对Order的relate_name为items

store–urls.py:

router.register(‘orders’, views.OrderViewSet)

store–models.py:

class OrderItem(models.Model):

    order = models.ForeignKey(

        Order, on_delete=models.PROTECT, related_name=’items’)

完成后,访问 …/store/orders/ 得到类似下面这样:

GET /store/orders/
HTTP 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

[
{
"id": 2,
"placed_at": "2023-02-28T10:23:17Z",
"payment_status": "P",
"customer": 3,
"items": [
{
"id": 1,
"product": {
"id": 1,
"title": "Bread Ww Cluster",
"unit_price": 4.0
},
"quantity": 10,
"unit_price": 10.0
},
{
"id": 2,
"product": {
"id": 2,
"title": "Island Oasis - Raspberry",
"unit_price": 84.64
},
"quantity": 20,
"unit_price": 20.0
}
]
}
]

应用权限

目前我们访问 /store/orders/ 是没有权限控制的,即便是匿名用户访问该API也能得到订单列表,我们得加上权限控制。

class OrderViewSet(ModelViewSet):

    …

    permission_classes = [IsAuthenticated]

对于普通顾客,目前也能看到所有订单,这是不希望的,他应该只能看到他的订单。而对于管理员用户,则可以看到所有订单,我们修改下视图。

class OrderViewSet(ModelViewSet):

    serializer_class = OrderSerializer

    permission_classes = [IsAuthenticated]

    def get_queryset(self):

        user = self.request.user

        if user.is_staff:

            return Order.objects.all()

        (customer_id, created) = Customer.objects.only(

            “id”).get_or_create(user_id=user.id)

        return Order.objects.filter(customer_id=customer_id)

我们重载了get_queryset方法,该方法判断是否管理员,不是管理员的话仅获取本人的订单,这里面用了查询集的only方法,我们只需要id,这也是一个好的实践。为了防止Customer未配置user的配置文件,这里使用 get_or_create方法,实际上这不是个好主意,一般要求查询操作与更改操作要分离,这个方法合并了两个操作。后续我们来改进,这里暂保持。

由于重载了get_queryset方法,我们需要在url路由中指定basename。

store–urls.py:

router.register(‘orders’, views.OrderViewSet, basename=’orders’)


创建订单

前面在设计API时,我们说到,创建订单,需提供一个购物车ID。显然,使用ModelSerializer派生的序列类是不能够完成任务的,我们需要手动编写。

store — serializers.py:

class CreateOrderSerializer(serializers.Serializer):

    cart_id = serializers.UUIDField()

    def save(self, **kwargs):

        (customer, created) = Customer.objects.get_or_create(

            user_id=self.context[‘user_id’])

        Order.objects.create(customer=customer)

store — views.py:

class OrderViewSet(ModelViewSet):

    …

    def get_serializer_context(self):

        return {“user_id”: self.request.user.id}

    def get_serializer_class(self):

        if self.request.method == ‘POST’:

            return CreateOrderSerializer

        return OrderSerializer

我们创建一个新序列类CreateOrderSerializer, 不继承ModelSerializer,只继承Serializer, 只有一个字段cart_id, 重载 save方法,查看Order类,我们只需提供customer即可,于是从user_id获取customer或创建customer, 其中 user_id是从视图中获取的上下文。视图OrderViewSet重载 get_serializer_class,判断当方法为POST时,使用CreateOrderSerializer。

注意,这里的get_or_create方法违反了查询修改分离原则,这里暂且保持。


创建订单清单

我们来修改CreateOrderSerializer,将购物车的清单写入订单。

class CreateOrderSerializer(serializers.Serializer):

    cart_id = serializers.UUIDField()

    def save(self, **kwargs):

        with transaction.atomic():

            cart_id = self.validated_data[‘cart_id’]

            (customer, created) = Customer.objects.get_or_create(

                user_id=self.context[‘user_id’])

            order = Order.objects.create(customer=customer)

            cart_items = CartItem.objects.select_related(“product”).filter(

                cart_id=cart_id)

            order_items = [

                OrderItem(

                    order=order,

                    product=item.product,

                    unit_price=item.product.unit_price,

                    quantity=item.quantity

                ) for item in cart_items

            ]

            OrderItem.objects.bulk_create(order_items)

            Cart.objects.filter(pk=cart_id).delete()

通过列表推导将购物车的商品清单导入到订单中,订单清单OrderItem使用批量创建方法 bulk_create, 订单创建后删除购物车。

由于这些操作涉及多次操作数据库,且不能只执行某些代码而中断执行其他代码,所以引入了事务处理 with transaction.atomic()。

完成上述代码后,我们可以用API测试下,先用/store/carts创建cart, 然后使用 /store/carts/<UUID>/items 添加一些购物车商品清单。然后再访问 /store/orders/ ,POST 购物车的UUID,查看是否已用购物车的物品清单生成了新订单。


返回创建的订单

你可能注意到了,如果我们POST一个购物车UUID创建订单的话,返回的响应也是购物车ID,类似这样:

POST /store/orders/
HTTP 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
"cart_id": "30ec077c-f89d-40f9-92f1-01b60ed80520"
}

我们期望返回的是订单信息,所以我们需要修改。但首先呢,我们需要了解ModelViewSet是如何实现的。ModelViewSet的父类之一CreateModelMixin 的serializer只看字段,创建成功后也返回该字段,由于CreateOrderSerializer只有cart_id字段,所以也只返回了这个。我们重载。

store — views.py:

class OrderViewSet(ModelViewSet):

    …

    def create(self, request, *args, **kwargs):

        serializer = CreateOrderSerializer(

            data=request.data,

            context={“user_id”: self.request.user.id}

        )

        serializer.is_valid(raise_exception=True)

        order = serializer.save()

        serializer = OrderSerializer(order)

        return Response(serializer.data)

重载的create方法,先使用CreateOrderSerializer创建order,上下文 user_id也是创建时提供了,这样 tget_serializer_context 重载方法也可以删除了。由于要从CreateOrderSerializer 的 save方法返回 order,我们还要添加一行返回语句:

class CreateOrderSerializer(serializers.Serializer):

    …

    def save(self, **kwargs):

        with transaction.atomic():

            …

            return order

再说回OrderViewSet的create方法。前面用CreateOrderSerializer创建了一个order,然后再用回序列类 OrderSerializer,将创建的order序列化并Response回给用户。


数据验证

目前我们创建订单时没进行验证,一般有两种错误。一是购物车的UUID是不存在的,我们自然不能通过该购物车创建订单;二是购物车是空的,这样建立空订单也是无意义的。所以我们要进行验证。在CreateOrderSerializer添加验证方法validate_cart_id,以前我们说过,validata_<字段名> 的方法表示验证该字段。

class CreateOrderSerializer(serializers.Serializer):

    …

    def validate_cart_id(self, cart_id):

        if not Cart.objects.filter(pk=cart_id).exists():

            raise serializers.ValidationError(‘No cart with given cart id.’)

        if CartItem.objects.filter(cart_id=cart_id).count() == 0:

            raise serializers.ValidationError(‘The cart is empty.’)

        return cart_id

键入代码后,我们测试用不存在的购物车或空购物车都会提示错误,示例 如下。

POST /store/orders/
HTTP 400 Bad Request
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
"cart_id": [
"The cart is empty."
]
}

重设权限

我们访问 /store/orders/<id>,可以看到可以删除、更新等操作。明显我们只允许管理员才有这些功能,普通认证用户是不应该有这些权限的。

class OrderViewSet(ModelViewSet):

    http_method_names = [‘get’, ‘patch’, ‘delete’, ‘head’, ‘options’]

    def get_permissions(self):

        if self.request.method in [‘PATCH’, ‘DELETE’]:

            return [IsAdminUser()]

        return [IsAuthenticated()]

    …

重载get_permissions方法,PATCH和DELETE需要管理员权限,其他允许认证用户操作,我们不希望有PUT操作,所以这里不列。注意返回的是对象而不是类,所以是有括号的,如IsAdminUser()。再提一下,http_method_names里用小写,面request.method用大写。


更新订单

更新订单只需更改订单的状态,这里有2个选择。一是设置原序列化类,除订单状态外,其他都是只读;二是建一个更新序列化类。我们选择第二种。

更新序列化类 store — serializers.py:

class UpdateOrderSerializer(serializers.ModelSerializer):

    class Meta:

        model = Order

        fields = [‘payment_status’]

再到store — views.py 中判别请求方法是否是PATCH再选择更新序列化类。

class OrderViewSet(ModelViewSet):

    …

    def get_serializer_class(self):

        if self.request.method == ‘POST’:

            return CreateOrderSerializer

        elif self.request.method == ‘PATCH’:

            return UpdateOrderSerializer

        return OrderSerializer


信号

前面我们在OrderViewSet的 get_queryset中使用了get_or_create方法,这个使用违反了单一责任的原则,因为get_queryset本来的职责只是获取数据,但该方法还有创建的功能。出现这个问题是因为我们要防止在通过user_id获取Customer时,他们还未进行关联。有一个解决方法,就是在注册用户时就与customer进行关联,但这也不妥,user只关注与用户注册相关的事情,没必要知道与customer有关的事情,难道随着应用扩展,user还要了解跟其他如student、employee的关联吗?显然我们得找更好的方法。

这就要用到Django中的signal。

Django的每个模型都有相应信号,比如pre_save表示准备保存时,post_save表示保存时,我们可以根据这些某些时间点发出的信号作相关工作。

对于我们这个项目,store应用可以监听 core应用的 post_save信号,然后在信号发出时,创建一条customer记录。这样就实现了user和customer的分离。

下面来实现,我们创建 store — signals.py 文件:

from django.conf import settings

from django.db.models.signals import post_save

from django.dispatch import receiver

from store.models import Customer

@receiver(post_save, sender=settings.AUTH_USER_MODEL)

def create_customer_for_new_user(sender, **kwargs):

    if kwargs[‘created’]:

        Customer.objects.create(user=kwargs[‘instance’])

我们新建一个函数 create_customer_for_new_user ,这个函数名也已经描述了功能,并用装饰符 @receiver 表示接收User的 post_save信号,由于User在 core应用中,自然不能直接导入它,否则会产生依赖,我们使用了settings文件的配置AUTH_USER_MODEL解耦。

创建了代码,还需要有一个地方执行它,我们可以在store应用的apps.py配置中重载 ready方法。store — apps.py:

class StoreConfig(AppConfig):

    …

    def ready(self) -> None:

        import store.signals

ready方法是在应用准备好时执行,我们导入了store.signals.py文件,就会在应用初始化后执行该文件的代码。

然后我们可以搜索之前的代码中有 get_or_create方法,修改成get方法,返回也就是一个变量customer,而不是元组了,比如这样的:

customer = Customer.objects.get(

                user_id=self.context[‘user_id’])

然后进行测试,进行管理后台,创建一个新用户比如user3,查看数据库也相应添加了一条customer记录并与user3进行了关联。


创建自定义信号

可以自定义信号。比如我们在store应用中,创建一条订单我们就希望发送一个order_created信号,其他应用可以监听这个信号,从而实现相关操作。

先整理一下文件结构。我们在 store 应用下新建一个文件夹 signals, 然后将signals.py文件移到该文件夹里并改名为 handlers.py,再在signals文件夹里新建 __init__.py文件:

from django.dispatch import Signal

order_created = Signal()

这里创建一个信号,名为order_created。

然后,我们要在store — CreateOrderSerializer — save 中发送这个信号。

from store.signals import order_created

class CreateOrderSerializer(serializers.Serializer):

    …

    def save(self, **kwargs):

        with transaction.atomic():

            …

            order_created.send_robust(self.__class__, order=order)

            return order

先导入 order_created, 然后调用发送方法,有两个方法send 和 send_robust,。两者不同之处是,使用方法send,某个接收者出问题将抛出异常其他接收者将接收不到信号。这里我们使用了send_rebust。发送方法第一个参数是类,我们通过魔术属性__class__获取,另一个可选参数用于携带数据,我们发送了创建的订单对象。

下一步,我们在core应用中接收这个信号。

类似地,我们创建 core — signals 文件夹,再在该文件夹下新建文件 handlers.py:

from django.dispatch import receiver

from store.signals import order_created

@receiver(order_created)

def on_order_created(sender, **kwargs):

    print(kwargs[‘order’])

在捕获信号的操作中,我们只是简地输出订单信息。然后,也类似地,在core — apps.py 中重载 ready 方法,执行前面 handlers.py中的代码。

class CoreConfig(AppConfig):

    …

    def ready(self) -> None:

        import core.signals.handlers

完成之后,我们测试创建一个订单,确认控制台会输出订单信息。


小结

本文设计了订单API。至此,已基本结束django restframework的内容。下一篇开始,介绍一些django的高级用法。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注