"""
水平检测服务
使用AI实时生成测试题目,评估用户水平
"""
import json
import uuid
from typing import List, Optional, Dict
from datetime import datetime
from ..models.learning import (
AssessmentQuestion, AssessmentResult, UserLevel, LearningPath
)
from .llm_service import get_llm
from .learning_content import get_learning_path
from .data_store import data_store
# 测试配置
TOTAL_QUESTIONS = 10
# 各路径的测试分类定义
PATH_CATEGORIES = {
LearningPath.FRONTEND: {
"html_css": "HTML/CSS基础",
"javascript": "JavaScript核心",
"vue": "Vue.js框架",
"browser_apis": "浏览器API与DOM",
},
LearningPath.BACKEND: {
"python": "Python基础",
"api_design": "REST API设计",
"database": "数据库操作",
"system_design": "系统设计",
},
LearningPath.FULLSTACK: {
"html_css": "HTML/CSS",
"javascript": "JavaScript",
"python": "Python",
"vue": "Vue.js",
"api_design": "API设计",
},
}
def get_categories_for_path(path_type: str) -> Dict[str, str]:
"""获取指定路径的测试分类"""
try:
path = LearningPath(path_type)
return PATH_CATEGORIES.get(path, PATH_CATEGORIES[LearningPath.FRONTEND])
except ValueError:
return PATH_CATEGORIES[LearningPath.FRONTEND]
def get_modules_for_path(path_type: str) -> List[Dict]:
"""获取指定路径的模块信息"""
try:
path = LearningPath(path_type)
path_data = get_learning_path(path)
return [
{
"id": m.id,
"title": m.title,
"description": m.description,
"lessons": [l.title for l in m.lessons],
}
for m in path_data.modules
]
except ValueError:
return []
# ===== AI题目生成 =====
def generate_assessment_questions(path_type: str) -> List[AssessmentQuestion]:
"""使用AI生成测试题目"""
categories = get_categories_for_path(path_type)
modules = get_modules_for_path(path_type)
categories_text = ", ".join([f"{k}({v})" for k, v in categories.items()])
modules_text = json.dumps([m["title"] for m in modules], ensure_ascii=False)
prompt = """请为"{path_type}"学习路径生成{total}道编程水平测试题。
测试分类:{categories}
涉及模块:{modules}
要求:
1. 混合4种题型:choice(选择题/知识题)、code_output(预测输出)、code_fill(代码填空)、bug_fix(找Bug)
2. 每道题包含:id(q1到q10)、category(分类key)、difficulty(难度1-5)、question_type(题型)、content(题目描述,用中文)、code_snippet(代码片段,选择题填null)、options(A/B/C/D选项,用中文)、correct_answer(正确答案字母)、explanation(解析,用中文)
3. 代码题必须包含5-15行的代码片段
4. 难度分布:简单30%、中等40%、困难30%
5. 每个分类至少2道题
6. 所有题目内容、选项、解析必须用中文输出
只输出JSON数组:
[
{{"id":"q1","category":"html_css","difficulty":2,"question_type":"choice","content":"关于HTML语义化标签的说法,正确的是?","code_snippet":null,"options":["A.
是语义化标签","B.
表示页面头部区域","C. 是块级元素","D. 只能用于博客文章"],"correct_answer":"B","explanation":"是HTML5语义化标签,表示页面或区块的头部区域。div是无语义容器,span是行内元素,article可用于任何独立内容。"}},
{{"id":"q2","category":"javascript","difficulty":3,"question_type":"code_output","content":"以下代码的输出是什么?","code_snippet":"const arr = [1, 2, 3];\\nconst result = arr.map(x => x * 2).filter(x => x > 3);\\nconsole.log(result);","options":["A. [2, 4, 6]","B. [4, 6]","C. [2, 4]","D. [6]"],"correct_answer":"B","explanation":"map将每个元素乘2得到[2,4,6],filter筛选大于3的元素得到[4,6]。"}}
]""".format(
path_type=path_type,
total=TOTAL_QUESTIONS,
categories=categories_text,
modules=modules_text,
)
try:
llm = get_llm()
messages = [{"role": "user", "content": prompt}]
response = llm.invoke(messages)
content = response if isinstance(response, str) else str(response)
# 提取JSON
start_idx = content.find("[")
end_idx = content.rfind("]") + 1
if start_idx == -1 or end_idx == 0:
print("[ERROR] AI返回格式错误,无法解析题目")
return _get_fallback_questions(path_type)
questions_data = json.loads(content[start_idx:end_idx])
questions = []
for q in questions_data:
questions.append(AssessmentQuestion(
id=q["id"],
category=q["category"],
difficulty=q.get("difficulty", 3),
content=q["content"],
question_type=q.get("question_type", "choice"),
code_snippet=q.get("code_snippet"),
options=q["options"],
correct_answer=q["correct_answer"],
explanation=q["explanation"],
))
print(f"[OK] AI生成了 {len(questions)} 道题目")
return questions[:TOTAL_QUESTIONS]
except Exception as e:
print(f"[ERROR] AI生成题目失败: {e}")
return _get_fallback_questions(path_type)
def _get_fallback_questions(path_type: str) -> List[AssessmentQuestion]:
"""备用题目(当AI生成失败时)"""
categories = get_categories_for_path(path_type)
cat_keys = list(categories.keys())
fallback = []
for i in range(TOTAL_QUESTIONS):
cat = cat_keys[i % len(cat_keys)]
fallback.append(AssessmentQuestion(
id=f"q{i+1}",
category=cat,
difficulty=2,
content=f"这是一道关于{categories[cat]}的测试题(备用题目)",
question_type="choice",
code_snippet=None,
options=["A. 选项1", "B. 选项2", "C. 选项3", "D. 选项4"],
correct_answer="A",
explanation="备用题目解析",
))
return fallback
# ===== 评估会话管理 =====
class AssessmentSession:
"""评估会话"""
def __init__(self, session_id: str, path_type: str, questions: List[AssessmentQuestion], user_id: str = "default"):
self.session_id = session_id
self.path_type = path_type
self.user_id = user_id
self.questions = questions
self.answers: Dict[str, str] = {} # question_id -> answer
self.current_index = 0
self.created_at = datetime.now()
@property
def is_completed(self) -> bool:
return self.current_index >= len(self.questions)
@property
def total_questions(self) -> int:
return len(self.questions)
def get_current_question(self) -> Optional[AssessmentQuestion]:
if self.current_index < len(self.questions):
return self.questions[self.current_index]
return None
def submit_answer(self, question_id: str, answer: str) -> Dict:
"""提交答案"""
question = self.questions[self.current_index]
if question.id != question_id:
return {"error": "题目ID不匹配"}
self.answers[question_id] = answer
is_correct = answer.upper() == question.correct_answer.upper()
self.current_index += 1
next_question = self.get_current_question()
return {
"is_correct": is_correct,
"correct_answer": question.correct_answer,
"explanation": question.explanation,
"next_question": next_question.model_dump() if next_question else None,
"current_index": self.current_index,
"total_questions": self.total_questions,
"is_completed": self.is_completed,
}
# 会话存储
_sessions: Dict[str, AssessmentSession] = {}
def create_session(path_type: str, user_id: str = "default") -> AssessmentSession:
"""创建新的评估会话"""
session_id = str(uuid.uuid4())[:8]
questions = generate_assessment_questions(path_type)
session = AssessmentSession(session_id, path_type, questions, user_id=user_id)
_sessions[session_id] = session
print(f"[INFO] 创建评估会话 {session_id}(用户: {user_id}),{len(questions)}道题")
return session
def get_session(session_id: str) -> Optional[AssessmentSession]:
"""获取评估会话"""
return _sessions.get(session_id)
def delete_session(session_id: str):
"""删除评估会话"""
if session_id in _sessions:
del _sessions[session_id]
# ===== 评分与结果 =====
def calculate_result(session: AssessmentSession, user_id: str = "default") -> AssessmentResult:
"""计算评估结果"""
questions = session.questions
answers = session.answers
categories = get_categories_for_path(session.path_type)
# 统计
total = len(questions)
correct = 0
category_correct = {cat: 0 for cat in categories}
category_total = {cat: 0 for cat in categories}
for q in questions:
user_answer = answers.get(q.id, "")
is_correct = user_answer.upper() == q.correct_answer.upper()
if is_correct:
correct += 1
if q.category in category_correct:
category_correct[q.category] += 1
if q.category in category_total:
category_total[q.category] += 1
# 总分
score = (correct / total * 100) if total > 0 else 0
# 各分类得分
category_scores = {}
for cat in categories:
if category_total.get(cat, 0) > 0:
category_scores[cat] = round(
category_correct[cat] / category_total[cat] * 100, 1
)
else:
category_scores[cat] = 0.0
# 确定水平
if score >= 80:
level = UserLevel.ADVANCED
elif score >= 50:
level = UserLevel.INTERMEDIATE
else:
level = UserLevel.BEGINNER
# 推荐开始模块
recommended_module = _recommend_module(session.path_type, level, category_scores)
return AssessmentResult(
user_id=user_id,
path_type=session.path_type,
total_questions=total,
correct_count=correct,
score=round(score, 1),
level=level,
category_scores=category_scores,
recommended_start_module=recommended_module,
completed_at=datetime.now(),
is_current=True,
)
def _recommend_module(
path_type: str, level: UserLevel, category_scores: Dict[str, float]
) -> str:
"""根据水平推荐开始模块"""
try:
path = LearningPath(path_type)
path_data = get_learning_path(path)
modules = sorted(path_data.modules, key=lambda m: m.order)
if level == UserLevel.BEGINNER:
return modules[0].id
# 中级:检查各分类得分,跳过掌握较好的模块
for module in modules:
# 检查模块相关的分类得分
module_cats = _get_module_categories(module.id)
avg_score = sum(
category_scores.get(cat, 0) for cat in module_cats
) / max(len(module_cats), 1)
if avg_score < 70:
return module.id
return modules[0].id
except Exception:
return ""
def _get_module_categories(module_id: str) -> List[str]:
"""根据模块ID获取相关分类"""
mapping = {
"fe-html-css": ["html_css"],
"fe-javascript": ["javascript"],
"fe-vue": ["vue"],
"fe-project": ["html_css", "javascript", "vue"],
"be-python": ["python"],
"be-api": ["api_design"],
"be-system": ["system_design", "database"],
"be-project": ["python", "api_design", "system_design"],
"fs-web-basics": ["html_css", "javascript"],
"fs-frontend": ["vue"],
"fs-backend": ["python", "api_design"],
"fs-fullstack": ["html_css", "javascript", "python", "vue", "api_design"],
}
return mapping.get(module_id, [])
# ===== 顶层API =====
def start_assessment(path_type: str, user_id: str = "default") -> Dict:
"""开始评估"""
session = create_session(path_type, user_id=user_id)
question = session.get_current_question()
return {
"session_id": session.session_id,
"question": question.model_dump() if question else None,
"current_index": session.current_index,
"total_questions": session.total_questions,
}
def submit_answer(session_id: str, question_id: str, answer: str, user_id: str = "default") -> Dict:
"""提交答案"""
session = get_session(session_id)
if not session:
return {"error": "会话不存在或已过期"}
result = session.submit_answer(question_id, answer)
# 如果完成,计算结果
if result.get("is_completed"):
assessment_result = calculate_result(session, user_id=user_id)
data_store.save_assessment(assessment_result)
result["assessment_result"] = assessment_result
# 发放XP奖励
from .gamification_service import get_gamification_service
svc = get_gamification_service()
svc.award_xp(user_id, 20, f"完成水平检测: {session.path_type}")
if assessment_result.score >= 100:
svc.award_xp(user_id, 30, "满分通关奖励")
svc.check_perfect_score(user_id)
svc.check_speed_demon(user_id)
return result
def complete_assessment(session_id: str, user_id: str = "default") -> Optional[AssessmentResult]:
"""强制完成评估(跳过剩余题目)"""
session = get_session(session_id)
if not session:
return None
result = calculate_result(session, user_id=user_id)
data_store.save_assessment(result)
delete_session(session_id)
# 发放XP奖励
from .gamification_service import get_gamification_service
svc = get_gamification_service()
svc.award_xp(user_id, 20, f"完成水平检测: {session.path_type}")
if result.score >= 100:
svc.award_xp(user_id, 30, "满分通关奖励")
svc.check_perfect_score(user_id)
# 检查速度徽章也在这里
svc.check_speed_demon(user_id)
return result