去掉 username

This commit is contained in:
2025-09-24 22:14:20 +08:00
parent 2d2bf2c3c5
commit 7e2f820738

View File

@@ -20,16 +20,14 @@ from account.decorators import login_required
from ai.models import AIAnalysis from ai.models import AIAnalysis
# 常量定义 CACHE_TIMEOUT = 300
CACHE_TIMEOUT = 300 # 5分钟缓存
DIFFICULTY_MAP = {"Low": "简单", "Mid": "中等", "High": "困难"} DIFFICULTY_MAP = {"Low": "简单", "Mid": "中等", "High": "困难"}
DEFAULT_CLASS_SIZE = 45 DEFAULT_CLASS_SIZE = 45
GRADE_THRESHOLDS = [(20, "S"), (50, "A"), (85, "B")]
def get_cache_key(prefix, *args): def get_cache_key(prefix, *args):
"""生成缓存键""" return hashlib.md5(f"{prefix}:{'_'.join(map(str, args))}".encode()).hexdigest()
key_string = f"{prefix}:{'_'.join(map(str, args))}"
return hashlib.md5(key_string.encode()).hexdigest()
def get_difficulty(difficulty): def get_difficulty(difficulty):
@@ -37,68 +35,51 @@ def get_difficulty(difficulty):
def get_grade(rank, submission_count): def get_grade(rank, submission_count):
"""
根据排名和提交人数计算等级
只有三分之一的人完成,直接给到 S
"""
if not rank or rank <= 0 or submission_count <= 0: if not rank or rank <= 0 or submission_count <= 0:
return "C" return "C"
if submission_count < DEFAULT_CLASS_SIZE // 3: if submission_count < DEFAULT_CLASS_SIZE // 3:
return "S" return "S"
top_percent = round(rank / submission_count * 100) top_percent = round(rank / submission_count * 100)
if top_percent < 20: for threshold, grade in GRADE_THRESHOLDS:
return "S" if top_percent < threshold:
elif top_percent < 50: return grade
return "A"
elif top_percent < 85:
return "B"
else:
return "C" return "C"
def get_class_user_ids(user): def get_class_user_ids(user):
"""获取班级用户ID列表""" """Get user IDs in the same class with caching."""
if not user.class_name: if not user.class_name:
return [] return []
cache_key = get_cache_key("class_users", user.class_name) cache_key = get_cache_key("class_users", user.class_name)
user_ids = cache.get(cache_key) user_ids = cache.get(cache_key)
if user_ids is None: if user_ids is None:
user_ids = list( user_ids = list(
User.objects.filter(class_name=user.class_name).values_list("id", flat=True) User.objects.filter(class_name=user.class_name).values_list("id", flat=True)
) )
cache.set(cache_key, user_ids, CACHE_TIMEOUT) cache.set(cache_key, user_ids, CACHE_TIMEOUT)
return user_ids return user_ids
def get_user_first_ac_submissions( def get_user_first_ac_submissions(
user_id, start, end, class_user_ids=None, use_class_scope=False user_id, start, end, class_user_ids=None, use_class_scope=False
): ):
"""获取用户首次AC提交记录""" """Get user's first AC submissions with ranking data."""
base_qs = Submission.objects.filter( base_qs = Submission.objects.filter(
result=JudgeStatus.ACCEPTED, result=JudgeStatus.ACCEPTED, create_time__gte=start, create_time__lte=end
create_time__gte=start,
create_time__lte=end,
) )
if use_class_scope and class_user_ids: if use_class_scope and class_user_ids:
base_qs = base_qs.filter(user_id__in=class_user_ids) base_qs = base_qs.filter(user_id__in=class_user_ids)
# 获取用户首次AC
user_first_ac = list( user_first_ac = list(
base_qs.filter(user_id=user_id) base_qs.filter(user_id=user_id)
.values("problem_id") .values("problem_id")
.annotate(first_ac_time=Min("create_time")) .annotate(first_ac_time=Min("create_time"))
) )
if not user_first_ac: if not user_first_ac:
return [], {}, [] return [], {}, []
# 获取相关题目的所有首次AC记录用于排名
problem_ids = [item["problem_id"] for item in user_first_ac] problem_ids = [item["problem_id"] for item in user_first_ac]
ranked_first_ac = list( ranked_first_ac = list(
base_qs.filter(problem_id__in=problem_ids) base_qs.filter(problem_id__in=problem_ids)
@@ -106,13 +87,12 @@ def get_user_first_ac_submissions(
.annotate(first_ac_time=Min("create_time")) .annotate(first_ac_time=Min("create_time"))
) )
# 按题目分组并排序 # Group by problem and sort by AC time
by_problem = defaultdict(list) by_problem = defaultdict(list)
for item in ranked_first_ac: for item in ranked_first_ac:
by_problem[item["problem_id"]].append(item) by_problem[item["problem_id"]].append(item)
for submissions in by_problem.values():
for _, arr in by_problem.items(): submissions.sort(key=lambda x: (x["first_ac_time"], x["user_id"]))
arr.sort(key=lambda x: (x["first_ac_time"], x["user_id"]))
return user_first_ac, by_problem, problem_ids return user_first_ac, by_problem, problem_ids
@@ -122,17 +102,9 @@ class AIDetailDataAPI(APIView):
def get(self, request): def get(self, request):
start = request.GET.get("start") start = request.GET.get("start")
end = request.GET.get("end") end = request.GET.get("end")
username = request.GET.get("username")
if username:
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return self.error("User does not exist")
else:
user = request.user user = request.user
# 检查缓存
cache_key = get_cache_key( cache_key = get_cache_key(
"ai_detail", user.id, user.class_name or "", start, end "ai_detail", user.id, user.class_name or "", start, end
) )
@@ -140,16 +112,12 @@ class AIDetailDataAPI(APIView):
if cached_result: if cached_result:
return self.success(cached_result) return self.success(cached_result)
# 获取班级用户ID
class_user_ids = get_class_user_ids(user) class_user_ids = get_class_user_ids(user)
use_class_scope = bool(user.class_name) and len(class_user_ids) > 1 use_class_scope = bool(user.class_name) and len(class_user_ids) > 1
# 获取用户首次AC记录
user_first_ac, by_problem, problem_ids = get_user_first_ac_submissions( user_first_ac, by_problem, problem_ids = get_user_first_ac_submissions(
user.id, start, end, class_user_ids, use_class_scope user.id, start, end, class_user_ids, use_class_scope
) )
if not user_first_ac:
result = { result = {
"user": user.username, "user": user.username,
"class_name": user.class_name, "class_name": user.class_name,
@@ -161,68 +129,49 @@ class AIDetailDataAPI(APIView):
"difficulty": {}, "difficulty": {},
"contest_count": 0, "contest_count": 0,
} }
cache.set(cache_key, result, CACHE_TIMEOUT)
return self.success(result)
# 优化的题目查询 - 一次性获取所有需要的数据 if user_first_ac:
problems = self._get_problems_with_data(problem_ids) problems = {
p.id: p
# 构建解题记录 for p in Problem.objects.filter(id__in=problem_ids)
.select_related("contest")
.prefetch_related("tags")
}
solved, contest_ids = self._build_solved_records( solved, contest_ids = self._build_solved_records(
user_first_ac, by_problem, problems, user.id user_first_ac, by_problem, problems, user.id
) )
result.update(
# 计算统计数据 {
avg_grade = self._calculate_average_grade(solved)
tags = self._calculate_top_tags(problems.values())
difficulty = self._calculate_difficulty_distribution(problems.values())
result = {
"user": user.username,
"class_name": user.class_name,
"start": start,
"end": end,
"solved": solved, "solved": solved,
"grade": avg_grade, "grade": self._calculate_average_grade(solved),
"tags": tags, "tags": self._calculate_top_tags(problems.values()),
"difficulty": difficulty, "difficulty": self._calculate_difficulty_distribution(
problems.values()
),
"contest_count": len(set(contest_ids)), "contest_count": len(set(contest_ids)),
} }
)
# 缓存结果
cache.set(cache_key, result, CACHE_TIMEOUT) cache.set(cache_key, result, CACHE_TIMEOUT)
return self.success(result) return self.success(result)
def _get_problems_with_data(self, problem_ids):
"""优化的题目数据获取"""
problem_qs = (
Problem.objects.filter(id__in=problem_ids)
.select_related("contest")
.prefetch_related("tags")
)
return {p.id: p for p in problem_qs}
def _build_solved_records(self, user_first_ac, by_problem, problems, user_id): def _build_solved_records(self, user_first_ac, by_problem, problems, user_id):
"""构建解题记录""" solved, contest_ids = [], []
solved = []
contest_ids = []
for item in user_first_ac: for item in user_first_ac:
pid = item["problem_id"] pid = item["problem_id"]
ranking_list = by_problem.get(pid, [])
# 查找用户排名
rank = None
for idx, rec in enumerate(ranking_list):
if rec["user_id"] == user_id:
rank = idx + 1
break
problem = problems.get(pid) problem = problems.get(pid)
if not problem: if not problem:
continue continue
grade = get_grade(rank, len(ranking_list)) ranking_list = by_problem.get(pid, [])
rank = next(
(
idx + 1
for idx, rec in enumerate(ranking_list)
if rec["user_id"] == user_id
),
None,
)
if problem.contest_id: if problem.contest_id:
contest_ids.append(problem.contest_id) contest_ids.append(problem.contest_id)
@@ -238,45 +187,38 @@ class AIDetailDataAPI(APIView):
"ac_time": timezone.localtime(item["first_ac_time"]).isoformat(), "ac_time": timezone.localtime(item["first_ac_time"]).isoformat(),
"rank": rank, "rank": rank,
"ac_count": len(ranking_list), "ac_count": len(ranking_list),
"grade": grade, "grade": get_grade(rank, len(ranking_list)),
} }
) )
# 按AC时间排序 return sorted(solved, key=lambda x: x["ac_time"]), contest_ids
solved.sort(key=lambda x: x["ac_time"])
return solved, contest_ids
def _calculate_average_grade(self, solved): def _calculate_average_grade(self, solved):
"""计算平均等级(出现次数最多的等级)"""
if not solved: if not solved:
return "" return ""
grade_count = defaultdict(int) grade_count = defaultdict(int)
for s in solved: for s in solved:
grade_count[s["grade"]] += 1 grade_count[s["grade"]] += 1
return max(grade_count, key=grade_count.get) return max(grade_count, key=grade_count.get)
def _calculate_top_tags(self, problems): def _calculate_top_tags(self, problems):
"""计算标签TOP5"""
tags_counter = defaultdict(int) tags_counter = defaultdict(int)
for problem in problems: for problem in problems:
for tag in problem.tags.all(): for tag in problem.tags.all():
if tag.name: if tag.name:
tags_counter[tag.name] += 1 tags_counter[tag.name] += 1
return dict(sorted(tags_counter.items(), key=lambda x: x[1], reverse=True)[:5])
top_tags = sorted(tags_counter.items(), key=lambda x: x[1], reverse=True)[:5]
return {name: count for name, count in top_tags}
def _calculate_difficulty_distribution(self, problems): def _calculate_difficulty_distribution(self, problems):
"""计算难度分布"""
diff_counter = {"Low": 0, "Mid": 0, "High": 0} diff_counter = {"Low": 0, "Mid": 0, "High": 0}
for problem in problems: for problem in problems:
key = problem.difficulty if problem.difficulty in diff_counter else "Mid" diff_counter[
diff_counter[key] += 1 problem.difficulty if problem.difficulty in diff_counter else "Mid"
] += 1
diff_sorted = sorted(diff_counter.items(), key=lambda x: x[1], reverse=True) return {
return {get_difficulty(k): v for k, v in diff_sorted} get_difficulty(k): v
for k, v in sorted(diff_counter.items(), key=lambda x: x[1], reverse=True)
}
class AIWeeklyDataAPI(APIView): class AIWeeklyDataAPI(APIView):
@@ -284,17 +226,9 @@ class AIWeeklyDataAPI(APIView):
def get(self, request): def get(self, request):
end_iso = request.GET.get("end") end_iso = request.GET.get("end")
duration = request.GET.get("duration") duration = request.GET.get("duration")
username = request.GET.get("username")
if username:
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return self.error("User does not exist")
else:
user = request.user user = request.user
# 检查缓存
cache_key = get_cache_key( cache_key = get_cache_key(
"ai_weekly", user.id, user.class_name or "", end_iso, duration "ai_weekly", user.id, user.class_name or "", end_iso, duration
) )
@@ -302,11 +236,8 @@ class AIWeeklyDataAPI(APIView):
if cached_result: if cached_result:
return self.success(cached_result) return self.success(cached_result)
# 获取班级用户ID
class_user_ids = get_class_user_ids(user) class_user_ids = get_class_user_ids(user)
use_class_scope = bool(user.class_name) and len(class_user_ids) > 1 use_class_scope = bool(user.class_name) and len(class_user_ids) > 1
# 解析时间参数
time_config = self._parse_duration(duration) time_config = self._parse_duration(duration)
start = datetime.fromisoformat(end_iso) - time_config["total_delta"] start = datetime.fromisoformat(end_iso) - time_config["total_delta"]
@@ -315,30 +246,21 @@ class AIWeeklyDataAPI(APIView):
start = start + time_config["delta"] start = start + time_config["delta"]
period_end = start + time_config["delta"] period_end = start + time_config["delta"]
submission_count = Submission.objects.filter(
user_id=user.id, create_time__gte=start, create_time__lte=period_end
).count()
period_data = { period_data = {
"unit": time_config["show_unit"], "unit": time_config["show_unit"],
"index": time_config["show_count"] - 1 - i, "index": time_config["show_count"] - 1 - i,
"start": start.isoformat(), "start": start.isoformat(),
"end": period_end.isoformat(), "end": period_end.isoformat(),
"problem_count": 0, "problem_count": 0,
"submission_count": 0, "submission_count": submission_count,
"grade": "", "grade": "",
} }
# 获取提交数量 if submission_count > 0:
submission_count = Submission.objects.filter(
user_id=user.id,
create_time__gte=start,
create_time__lte=period_end,
).count()
period_data["submission_count"] = submission_count
if submission_count == 0:
weekly_data.append(period_data)
continue
# 获取AC记录和等级
user_first_ac, by_problem, problem_ids = get_user_first_ac_submissions( user_first_ac, by_problem, problem_ids = get_user_first_ac_submissions(
user.id, user.id,
start.isoformat(), start.isoformat(),
@@ -346,7 +268,6 @@ class AIWeeklyDataAPI(APIView):
class_user_ids, class_user_ids,
use_class_scope, use_class_scope,
) )
if user_first_ac: if user_first_ac:
period_data["problem_count"] = len(problem_ids) period_data["problem_count"] = len(problem_ids)
period_data["grade"] = self._calculate_period_grade( period_data["grade"] = self._calculate_period_grade(
@@ -355,7 +276,6 @@ class AIWeeklyDataAPI(APIView):
weekly_data.append(period_data) weekly_data.append(period_data)
# 缓存结果
cache.set(cache_key, weekly_data, CACHE_TIMEOUT) cache.set(cache_key, weekly_data, CACHE_TIMEOUT)
return self.success(weekly_data) return self.success(weekly_data)
@@ -363,70 +283,56 @@ class AIWeeklyDataAPI(APIView):
unit, count = duration.split(":") unit, count = duration.split(":")
count = int(count) count = int(count)
# 默认配置 configs = {
show_count = 4 ("months", 2): {
show_unit = "weeks" "show_count": 8,
total_delta = timedelta(weeks=show_count + 1) "show_unit": "weeks",
delta = timedelta(weeks=1) "total_delta": timedelta(weeks=9),
"delta": timedelta(weeks=1),
if unit == "months" and count == 2: },
# 过去八周 ("months", 6): {
show_count = 8 "show_count": 6,
total_delta = timedelta(weeks=9) "show_unit": "months",
elif unit == "months" and count == 6: "total_delta": relativedelta(months=7),
# 过去六个月 "delta": relativedelta(months=1),
show_count = 6 },
show_unit = "months" ("years", 1): {
total_delta = relativedelta(months=7) "show_count": 12,
delta = relativedelta(months=1) "show_unit": "months",
elif unit == "years": "total_delta": relativedelta(months=13),
# 过去一年 "delta": relativedelta(months=1),
show_count = 12 },
show_unit = "months"
total_delta = relativedelta(months=13)
delta = relativedelta(months=1)
return {
"show_count": show_count,
"show_unit": show_unit,
"total_delta": total_delta,
"delta": delta,
} }
return configs.get(
(unit, count),
{
"show_count": 4,
"show_unit": "weeks",
"total_delta": timedelta(weeks=5),
"delta": timedelta(weeks=1),
},
)
def _calculate_period_grade(self, user_first_ac, by_problem, user_id): def _calculate_period_grade(self, user_first_ac, by_problem, user_id):
"""计算周期内的等级"""
grade_count = defaultdict(int) grade_count = defaultdict(int)
for item in user_first_ac: for item in user_first_ac:
pid = item["problem_id"] ranking_list = by_problem.get(item["problem_id"], [])
ranking_list = by_problem.get(pid, []) rank = next(
(
# 查找用户排名 idx + 1
rank = None for idx, rec in enumerate(ranking_list)
for idx, rec in enumerate(ranking_list): if rec["user_id"] == user_id
if rec["user_id"] == user_id: ),
rank = idx + 1 None,
break )
grade_count[get_grade(rank, len(ranking_list))] += 1
grade = get_grade(rank, len(ranking_list))
grade_count[grade] += 1
return max(grade_count, key=grade_count.get) if grade_count else "" return max(grade_count, key=grade_count.get) if grade_count else ""
class AIAnalysisAPI(APIView): class AIAnalysisAPI(APIView):
@login_required @login_required
def post(self, request): def post(self, request):
user = request.user
# 如果超管帮别人查询,则需要获取用户信息
username = request.data.get("username")
if username:
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return self.error("User does not exist")
details = request.data.get("details") details = request.data.get("details")
weekly = request.data.get("weekly") weekly = request.data.get("weekly")
@@ -437,8 +343,8 @@ class AIAnalysisAPI(APIView):
client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com") client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com")
system_prompt = "你是一个风趣的编程老师,学生使用判题狗平台进行编程练习。请根据学生提供的详细数据和每周数据,给出用户的学习建议。请使用 markdown 格式输出,不要在代码块中输出。最后不要忘记写一句祝福语。" system_prompt = "你是一个风趣的编程老师,学生使用判题狗平台进行编程练习。请根据学生提供的详细数据和每周数据,给出用户的学习建议,最后写一句鼓励学生的话。请使用 markdown 格式输出,不要在代码块中输出。"
user_prompt = f"这段时间内的详细数据: {details} \n每周或每月的数据: {weekly}" user_prompt = f"这段时间内的详细数据: {details}\n每周或每月的数据: {weekly}"
analysis_chunks = [] analysis_chunks = []
saved_instance = None saved_instance = None
@@ -447,18 +353,15 @@ class AIAnalysisAPI(APIView):
def save_analysis(): def save_analysis():
nonlocal saved_instance nonlocal saved_instance
if analysis_chunks and not saved_instance: if analysis_chunks and not saved_instance:
try:
saved_instance = AIAnalysis.objects.create( saved_instance = AIAnalysis.objects.create(
user=user, user=request.user,
provider="deepseek", provider="deepseek",
model="deepseek-chat", model="deepseek-chat",
data={"details": details, "weekly": weekly}, data={"details": details, "weekly": weekly},
system_prompt=system_prompt, system_prompt=system_prompt,
user_prompt=user_prompt, user_prompt="这段时间内的详细数据,每周或每月的数据。",
analysis="".join(analysis_chunks).strip(), analysis="".join(analysis_chunks).strip(),
) )
except Exception:
pass
def stream_generator(): def stream_generator():
nonlocal completed nonlocal completed