diff --git a/contest/contest/tasks.py b/contest/contest/tasks.py index c753582..b5de7a2 100644 --- a/contest/contest/tasks.py +++ b/contest/contest/tasks.py @@ -1,6 +1,9 @@ -import logging +"""celery 任务""" + +from __future__ import annotations + +from typing import TYPE_CHECKING -import redis from celery import shared_task from django.core.cache import cache from django.shortcuts import get_object_or_404 @@ -10,71 +13,65 @@ # 导致运行的时候如果直接import contest.quiz.models会找不到 # 所以就直接默认已经在contest路径里开始索引,因此lint会报错 # 但是实际上是可以运行的,忽略Lint报错即可 -from quiz.models import ( - Choice, - DraftAnswer, - DraftResponse, -) +from quiz.models import Choice, DraftAnswer, DraftResponse -# Get an instance of a logger -logger = logging.getLogger("django") +if TYPE_CHECKING: + from typing import Generator @shared_task def auto_save_redis_to_database() -> None: - # 获取 Redis 连接 - r = redis.Redis(host="127.0.0.1", port=6379, db=1) - # 使用 scan_iter 获取所有键 - keys = r.scan_iter("*_ddl") - if keys is None: - return - for key in keys: - ddl_key = key.decode("utf-8")[3:] - ddl = cache.get(ddl_key) - now = timezone.now() - if ddl is not None: - if ddl < now: - try: - draft_response = DraftResponse.objects.get(id=int(ddl_key[:-4])) - cache_key = f"{ddl_key[:-4]}_json" - # # 从 Redis 获取现有的答案缓存 - cached_answers = cache.get(cache_key, {}) - - if cached_answers is not None: # 防止未提交的是白卷 - for question_id, choice_id in cached_answers.items(): - # Filter out tokens - if not question_id.startswith("question-"): - continue - - if not isinstance(choice_id, str) or not choice_id.startswith( - "choice-" - ): - return - - answer: DraftAnswer = get_object_or_404( - draft_response.answer_set, - question_id=int(question_id.removeprefix("question-")), - ) - - answer.choice = get_object_or_404( - Choice.objects, - pk=int(choice_id.removeprefix("choice-")), - question=answer.question, - ) - - answer.save() + """提交 Redis 缓存中过期的答卷草稿""" + scanner: Generator[str, None, None] = cache.iter_keys("*_ddl") # type: ignore[attr-defined] + # `iter_keys`由 django-redis 提供,django 本身没有 + for ddl_key in scanner: + pk = int(ddl_key.removesuffix("_ddl")) + ddl = cache.get(f"{pk}_ddl") + if ddl is not None and ddl < timezone.now(): + try: + draft = DraftResponse.objects.get(pk=pk) + cached_answers = cache.get(f"{pk}_json", {}) + + # 同步 Redis 缓存到数据库 + # 若未作答,可能 Redis 中无记录,但数据库中仍有 + if cached_answers is not None: + for question_id, choice_id in cached_answers.items(): + # Filter out tokens + if not question_id.startswith("question-"): + continue + + if not isinstance(choice_id, str) or not choice_id.startswith( + "choice-" + ): + return + + answer: DraftAnswer = get_object_or_404( + draft.answer_set, + question_id=int(question_id.removeprefix("question-")), + ) + + answer.choice = get_object_or_404( + Choice.objects, + pk=int(choice_id.removeprefix("choice-")), + question=answer.question, + ) + + answer.save() # 1. Convert from draft - response, answers = draft_response.finalize(submit_at=timezone.now()) + response, answers = draft.finalize(submit_at=draft.deadline) # 2. Save response.save() response.answer_set.bulk_create(answers) - draft_response.delete() + draft.delete() - except DraftResponse.DoesNotExist as e: - print("here is tasks.py 74 line") - print(e) + except DraftResponse.DoesNotExist as e: + # TODO: 需要认真报错 + print("here is tasks.py auto_save_redis_to_database") + print(e) - r.delete(key) - r.delete(":1:" + ddl_key[:-4] + "_json") + # 即使`DraftResponse.DoesNotExist`,也应尝试删除 Redis 中的记录 + # 因为可能是 Django 正常处理过了 + cache.delete(f"{pk}_ddl") + cache.delete(f"{pk}_json") diff --git a/contest/quiz/views.py b/contest/quiz/views.py index abc367d..1538649 100644 --- a/contest/quiz/views.py +++ b/contest/quiz/views.py @@ -34,7 +34,7 @@ from .util import AuthenticatedHttpRequest -def continue_or_finalize(draft_response: DraftResponse) -> bool: +def continue_or_finalize(draft: DraftResponse) -> bool: """自动提交 若草稿已超期,则定稿,否则什么也不做。 @@ -47,9 +47,9 @@ def continue_or_finalize(draft_response: DraftResponse) -> bool: https://docs.djangoproject.com/en/4.2/ref/models/instances/#django.db.models.Model.delete https://docs.djangoproject.com/en/4.2/ref/models/instances/#refreshing-objects-from-database """ - if draft_response.outdated(): + if draft.outdated(): # 从 Redis 获取现有的答案缓存 - cached_answers = cache.get(f"{draft_response.id}_json", {}) + cached_answers = cache.get(f"{draft.id}_json", {}) if cached_answers: for question_id, choice_id in cached_answers.items(): @@ -61,7 +61,7 @@ def continue_or_finalize(draft_response: DraftResponse) -> bool: return False answer: DraftAnswer = get_object_or_404( - draft_response.answer_set, + draft.answer_set, question_id=int(question_id.removeprefix("question-")), ) @@ -73,18 +73,18 @@ def continue_or_finalize(draft_response: DraftResponse) -> bool: answer.save() - cache.delete(f"{draft_response.id}_json") - cache.delete(f"{draft_response.id}_ddl") + cache.delete(f"{draft.id}_json") + cache.delete(f"{draft.id}_ddl") # 提交之前的草稿 # 1. Convert from draft - response, answers = draft_response.finalize(submit_at=timezone.now()) + response, answers = draft.finalize(submit_at=draft.deadline) # 2. Save response.save() response.answer_set.bulk_create(answers) - draft_response.delete() + draft.delete() return True