装饰器
ViewSet 装饰器¶
ViewSet 装饰器用于扩展和定制 ViewSet 的行为,其中最重要的是 @action 装饰器。
@action 装饰器¶
@action 装饰器用于为 ViewSet 添加自定义操作,超越标准的 CRUD 操作。
基本用法¶
from srf.views import BaseViewSet
from srf.views.decorators import action
from sanic.response import json
class ProductViewSet(BaseViewSet):
@action(methods=["get"], detail=False, url_path="featured")
async def list_featured(self, request):
"""获取推荐产品(集合级操作)"""
products = await Product.filter(is_featured=True)
schema = self.get_schema(request, is_safe=True)
data = [schema.model_validate(p).model_dump() for p in products]
return json({"results": data})
@action(methods=["post"], detail=True, url_path="publish")
async def publish(self, request, pk):
"""发布产品(详情级操作)"""
product = await self.get_object(request, pk)
product.is_published = True
product.published_at = datetime.now()
await product.save()
return json({"message": "产品已发布"})
装饰器参数¶
action(
*,
detail: bool = False, # True 为详情级;False 为集合级
methods: Sequence[str] = ("GET",), # 默认 HTTP 方法
url_path: str | None = None, # 默认:"/<方法名>"
url_name: str | None = None, # 默认:方法名
**kwargs,
)
参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
methods |
Sequence[str] | ("GET",) |
HTTP 方法列表,如 ["get"], ["post"], ["get", "post"](注册时大小写不敏感) |
detail |
bool | False |
True 为详情级操作(需要 pk),False 为集合级操作 |
url_path |
str | None | /<方法名> |
自定义 URL 路径段 |
url_name |
str | None | 方法名 | 路由名称,用于 URL 反向解析 |
集合级操作 vs 详情级操作¶
集合级操作 (detail=False)¶
不需要资源 ID,作用于整个集合。
特点:
- 不需要 pk 参数
- URL 格式:/api/products/action-name
- 适用于批量操作、统计、搜索等
示例:
@action(methods=["get"], detail=False, url_path="statistics")
async def get_statistics(self, request):
"""获取产品统计信息"""
from tortoise.functions import Count, Avg, Sum
stats = await Product.all().annotate(
total=Count("id"),
avg_price=Avg("price"),
total_value=Sum("price")
).values("total", "avg_price", "total_value")
return json(stats[0] if stats else {})
@action(methods=["post"], detail=False, url_path="bulk-update")
async def bulk_update(self, request):
"""批量更新产品"""
ids = request.json.get("ids", [])
updates = request.json.get("updates", {})
if not ids:
return json({"error": "请提供产品 ID 列表"}, status=400)
await Product.filter(id__in=ids).update(**updates)
return json({"message": f"成功更新 {len(ids)} 个产品"})
@action(methods=["get"], detail=False, url_path="search")
async def advanced_search(self, request):
"""高级搜索"""
keyword = request.args.get("q", "")
category = request.args.get("category")
queryset = Product.all()
if keyword:
queryset = queryset.filter(name__icontains=keyword)
if category:
queryset = queryset.filter(category_id=category)
products = await queryset
schema = self.get_schema(request, is_safe=True)
data = [schema.model_validate(p).model_dump() for p in products]
return json({"results": data})
生成的路由:
- GET /api/products/statistics
- POST /api/products/bulk-update
- GET /api/products/search
详情级操作 (detail=True)¶
需要资源 ID,作用于单个资源。
特点:
- 需要 pk 参数
- URL 格式:/api/products/<pk>/action-name
- 适用于状态变更、关联操作等
示例:
@action(methods=["post"], detail=True, url_path="activate")
async def activate(self, request, pk):
"""激活产品"""
product = await self.get_object(request, pk)
if product.is_active:
return json({"error": "产品已激活"}, status=400)
product.is_active = True
await product.save()
return json({"message": "产品已激活"})
@action(methods=["post"], detail=True, url_path="duplicate")
async def duplicate(self, request, pk):
"""复制产品"""
product = await self.get_object(request, pk)
# 复制产品
new_product = await Product.create(
name=f"{product.name} (副本)",
price=product.price,
description=product.description,
category_id=product.category_id
)
schema = self.get_schema(request, is_safe=True)
data = schema.model_validate(new_product).model_dump()
return json(data, status=201)
@action(methods=["get"], detail=True, url_path="related")
async def get_related(self, request, pk):
"""获取相关产品"""
product = await self.get_object(request, pk)
# 同类别的其他产品
related = await Product.filter(
category_id=product.category_id
).exclude(id=product.id).limit(5)
schema = self.get_schema(request, is_safe=True)
data = [schema.model_validate(p).model_dump() for p in related]
return json({"results": data})
生成的路由:
- POST /api/products/<pk>/activate
- POST /api/products/<pk>/duplicate
- GET /api/products/<pk>/related
多 HTTP 方法¶
一个 action 可以支持多个 HTTP 方法:
@action(methods=["get", "post"], detail=True, url_path="comments")
async def handle_comments(self, request, pk):
"""处理评论(GET 获取,POST 添加)"""
product = await self.get_object(request, pk)
if request.method == "GET":
# 获取评论
comments = await product.comments.all()
return json({"results": [c.to_dict() for c in comments]})
elif request.method == "POST":
# 添加评论
content = request.json.get("content")
comment = await Comment.create(
product=product,
user=request.ctx.user,
content=content
)
return json(comment.to_dict(), status=201)
自定义 URL 路径¶
使用 url_path 参数自定义 URL:
@action(methods=["post"], detail=True, url_path="change-status")
async def change_status(self, request, pk):
"""更改状态"""
product = await self.get_object(request, pk)
new_status = request.json.get("status")
product.status = new_status
await product.save()
return json({"message": "状态已更新"})
URL:POST /api/products/<pk>/change-status
省略 url_path 时默认为 /<方法名>(下划线不会转换):
URL:POST /api/products/<pk>/change_status
自定义路由名称¶
使用 url_name 参数自定义路由名称,用于 URL 反向解析:
@action(detail=False, methods=["get"], url_path="featured", url_name="featured_list")
async def featured(self, request):
"""推荐列表"""
pass
# 反向解析;默认 prefix="api" 时 Blueprint 名称为 api
url = request.app.url_for("api.featured_list")
权限控制¶
可以在 action 中进行权限检查:
from srf.permission.permission import IsRoleAdminUser
from sanic.exceptions import Forbidden
@action(methods=["post"], detail=True, url_path="approve")
async def approve(self, request, pk):
"""审核产品(仅管理员)"""
# 检查管理员权限
if not IsRoleAdminUser.has_permission(request, self):
raise Forbidden("需要管理员权限")
product = await self.get_object(request, pk)
product.is_approved = True
await product.save()
return json({"message": "审核通过"})
完整示例¶
from srf.views import BaseViewSet
from srf.views.decorators import action
from srf.permission.permission import IsAuthenticated, IsRoleAdminUser
from sanic.response import json
from sanic.exceptions import Forbidden
from sanic.constants import SAFE_HTTP_METHODS
from datetime import datetime
from models import Product, Comment
from schemas import ProductSchemaReader, ProductSchemaWriter
class ProductViewSet(BaseViewSet):
"""产品 ViewSet - 装饰器示例"""
permission_classes = (IsAuthenticated,)
@property
def queryset(self):
return Product.all()
def get_schema(self, request, is_safe=False):
if request.method.upper() in SAFE_HTTP_METHODS or is_safe:
return ProductSchemaReader
return ProductSchemaWriter
# 集合级操作
@action(methods=["get"], detail=False, url_path="featured")
async def list_featured(self, request):
"""获取推荐产品"""
products = await Product.filter(is_featured=True, is_active=True)
schema = self.get_schema(request, is_safe=True)
data = [schema.model_validate(p).model_dump() for p in products]
return json({"results": data})
@action(methods=["get"], detail=False, url_path="statistics")
async def get_statistics(self, request):
"""获取统计信息"""
from tortoise.functions import Count, Avg
stats = await Product.all().annotate(
total=Count("id"),
avg_price=Avg("price")
).values("total", "avg_price")
return json(stats[0] if stats else {})
@action(methods=["post"], detail=False, url_path="bulk-delete")
async def bulk_delete(self, request):
"""批量删除(仅管理员)"""
# 检查管理员权限
user = self.get_current_user(request)
if not user.role or user.role.name != 'admin':
raise Forbidden("需要管理员权限")
ids = request.json.get("ids", [])
count = await Product.filter(id__in=ids).delete()
return json({"message": f"成功删除 {count} 个产品"})
# 详情级操作
@action(methods=["post"], detail=True, url_path="publish")
async def publish(self, request, pk):
"""发布产品"""
product = await self.get_object(request, pk)
if product.is_published:
return json({"error": "产品已发布"}, status=400)
product.is_published = True
product.published_at = datetime.now()
await product.save()
return json({"message": "产品已发布"})
@action(methods=["post"], detail=True, url_path="duplicate")
async def duplicate(self, request, pk):
"""复制产品"""
product = await self.get_object(request, pk)
new_product = await Product.create(
name=f"{product.name} (副本)",
price=product.price,
description=product.description,
category_id=product.category_id
)
schema = self.get_schema(request, is_safe=True)
data = schema.model_validate(new_product).model_dump()
return json(data, status=201)
@action(methods=["get", "post"], detail=True, url_path="comments")
async def handle_comments(self, request, pk):
"""处理评论"""
product = await self.get_object(request, pk)
if request.method == "GET":
# 获取评论
comments = await product.comments.all()
return json({"results": [c.to_dict() for c in comments]})
elif request.method == "POST":
# 添加评论
content = request.json.get("content")
if not content:
return json({"error": "评论内容不能为空"}, status=400)
comment = await Comment.create(
product=product,
user=request.ctx.user,
content=content
)
return json(comment.to_dict(), status=201)
最佳实践¶
- 语义化命名:方法名应清晰表达操作意图
- 适当分组:相关操作使用一致的 URL 路径前缀
- 权限检查:敏感操作添加权限验证
- 错误处理:提供友好的错误信息
- 文档字符串:为每个 action 添加清晰的文档
- HTTP 方法:遵循 RESTful 约定(GET 查询,POST 创建/操作)
- 幂等性:GET 操作应该是幂等的
常见模式¶
状态变更¶
@action(methods=["post"], detail=True, url_path="archive")
async def archive(self, request, pk):
"""归档"""
obj = await self.get_object(request, pk)
obj.status = "archived"
await obj.save()
return json({"message": "已归档"})
关联资源¶
@action(methods=["get"], detail=True, url_path="reviews")
async def get_reviews(self, request, pk):
"""获取评论"""
obj = await self.get_object(request, pk)
reviews = await obj.reviews.all()
return json({"results": [r.to_dict() for r in reviews]})
批量操作¶
@action(methods=["post"], detail=False, url_path="bulk-update")
async def bulk_update(self, request):
"""批量更新"""
ids = request.json.get("ids", [])
updates = request.json.get("updates", {})
await Model.filter(id__in=ids).update(**updates)
return json({"message": "更新成功"})