You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django REST Framework如何通过限流实现用户每周答题次数限制

作答次数限制实现方案

不要直接套用框架默认的全局throttling规则,你的需求是单用户对单道当周题目独立计算2次作答额度,新旧题额度不共享,发布周几可动态调整,默认限流的固定周期、全局/用户维度计数完全不匹配这个场景,采用「数据库存作答记录+业务层校验」的方案最稳妥,可维护性也最高。


1. 补全核心数据模型

你现有的QuizDetail模型只存了题目本身的信息,缺少用户作答记录的存储,这是计数的核心依据,先新增QuizSubmission模型存储每一次作答行为:

from django.db import models
from django.conf import settings

# 原有Days枚举保留,修正QuizDetail的两处拼写错误
Days = (
   ("Mon", "Monday"),
   ("Tue", "Tuesday"),
   ("Wed", "Wednesday"),
   ("Thu", "Thursday"),
   ("Fri", "Friday"),
   ("Sat", "Saturday"),
   ("Sun", "Sunday")
)

class QuizDetail(models.Model):
    name = models.CharField(max_length=255, blank=False, null=False) # 原max_lenght拼写修正
    start_date = models.DateTimeField()
    end_date = models.DateTimeField()
    published_week_day = models.CharField(max_length=255, choices=Days) # 原publisehd_week_day拼写修正

# 新增作答记录模型
class QuizSubmission(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, 
        on_delete=models.CASCADE, 
        related_name="quiz_submissions"
    )
    quiz = models.ForeignKey(
        QuizDetail, 
        on_delete=models.CASCADE, 
        related_name="submissions"
    )
    submit_time = models.DateTimeField(auto_now_add=True)
    score = models.IntegerField(null=True, blank=True) # 可扩展存储得分、答案等业务字段

    class Meta:
        # 加索引提升后续计数查询的速度
        indexes = [
            models.Index(fields=["user", "quiz", "submit_time"]),
        ]

注意:每道周更题创建时,直接把start_date设为当周题目上线的具体时间(比如本月周二上线就设为对应周二的10点),end_date设为下一道题上线前的截止时间即可,后续统计直接复用这两个字段,不需要动态根据周几计算时间窗口,哪怕后续调整发布周几,只要新建题目时填对这两个时间,逻辑完全不需要修改。


2. 实现作答权限校验逻辑

核心规则是统计当前用户在当前题目的有效期内的提交次数,因为计数是和具体题目ID绑定的,天然满足「旧题额度耗尽不影响新题作答」的要求:周一答上周的旧题满2次,周二新题上线后统计的是新题ID下的提交数,从0开始计数,完全独立。
把校验逻辑封装成可复用的工具函数:

from django.utils import timezone
from .models import QuizSubmission

MAX_ATTEMPTS_PER_QUIZ = 2

def validate_quiz_attempt(user, quiz):
    """
    返回格式:(是否允许作答, 剩余次数, 提示信息)
    """
    now = timezone.now()
    # 先校验题目是否在作答有效期
    if now < quiz.start_date or now > quiz.end_date:
        return (False, 0, "当前题目不在作答有效期内")
    
    # 统计用户对这道题已经提交的有效次数
    used_attempts = QuizSubmission.objects.filter(
        user=user,
        quiz=quiz,
        submit_time__gte=quiz.start_date,
        submit_time__lte=quiz.end_date
    ).count()

    remaining = MAX_ATTEMPTS_PER_QUIZ - used_attempts
    if remaining <= 0:
        return (False, 0, "你当前这道题的2次作答机会已用完")
    return (True, remaining, "校验通过")

3. 接口层接入校验

如果用Django REST Framework开发接口,不需要自定义复杂的Throttle类,直接在提交作答的接口前置校验即可:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import QuizDetail
from .serializers import QuizSubmissionSerializer
from .utils import validate_quiz_attempt

class QuizSubmitAPI(APIView):
    def post(self, request, quiz_id):
        # 登录校验
        if not request.user.is_authenticated:
            return Response({"msg": "请先登录后再作答"}, status=status.HTTP_401_UNAUTHORIZED)
        
        # 校验题目是否存在
        try:
            target_quiz = QuizDetail.objects.get(id=quiz_id)
        except QuizDetail.DoesNotExist:
            return Response({"msg": "题目不存在"}, status=status.HTTP_404_NOT_FOUND)
        
        # 校验作答次数
        allow_attempt, remaining, msg = validate_quiz_attempt(request.user, target_quiz)
        if not allow_attempt:
            return Response({"msg": msg, "remaining": remaining}, status=status.HTTP_403_FORBIDDEN)
        
        # 正常处理提交逻辑
        serializer = QuizSubmissionSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(user=request.user, quiz=target_quiz)
            return Response({
                "msg": "作答提交成功",
                "remaining": remaining - 1
            }, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

可选性能优化

如果接口访问量较高,每次请求都count数据库会带来不必要的压力,可以搭配Redis做计数缓存:

  • 缓存key规则:quiz_attempt_cnt:{user_id}:{quiz_id}
  • 缓存过期时间:和对应题目的end_date对齐,到期自动失效
  • 校验逻辑优先查缓存,缓存不存在时再查数据库回写,提交成功后同步更新缓存计数即可,注意不要完全依赖缓存做唯一判断,数据库的提交记录始终是计数的可信数据源,避免缓存故障导致限制失效。

为什么不推荐用默认Throttling实现

框架自带的Throttle方案(包括DRF默认的UserRateThrottle、AnonRateThrottle)存在几个核心问题,完全不匹配你的需求:

  • 计数维度不支持绑定具体题目ID,只能按用户/IP做全局计数,会出现旧题作答次数占用新题额度的问题
  • 限流周期只支持固定的自然时间单位(分钟/小时/天/自然周),没法对齐你动态调整的题目上线周期,也无法支持新旧题额度独立的规则
  • 限流计数默认存在缓存中,缓存清空/过期会导致限制失效,对于作答次数这种强业务规则,可靠性不足

内容的提问来源于stack exchange,提问作者Arthur Dayne

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 01:15:29