本文设计创建订单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的高级用法。