feat(server-release): 出题批阅业务逻辑优化 — 移除20题限制、max_total支持、填空题格式校验、修正认证状态码
- exam_pkg/api.py: 移除总题数20道上限,改为50道警告;修正认证错误使用正确的UNAUTHORIZED/FORBIDDEN状态码;端点保持同步模式 - exam_pkg/generator.py: AI智能出题prompt支持max_total参数 - exam_pkg/grader.py: 填空题增加学生答案格式校验(列表类型+字符串元素)
This commit is contained in:
@@ -29,7 +29,7 @@ from auth.gateway import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 导入统一响应格式
|
# 导入统一响应格式
|
||||||
from core.status_codes import EXAM_SUCCESS, GRADE_SUCCESS, EXAM_ERROR, GRADE_ERROR, BAD_REQUEST, NO_CONTENT, LLM_ERROR
|
from core.status_codes import EXAM_SUCCESS, GRADE_SUCCESS, EXAM_ERROR, GRADE_ERROR, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NO_CONTENT, LLM_ERROR
|
||||||
from api.response_utils import success_response, error_response
|
from api.response_utils import success_response, error_response
|
||||||
|
|
||||||
# 合法题型
|
# 合法题型
|
||||||
@@ -152,13 +152,14 @@ def api_generate_questions():
|
|||||||
if diff_error:
|
if diff_error:
|
||||||
return error_response("INVALID_PARAMS", BAD_REQUEST, diff_error, http_status=400)
|
return error_response("INVALID_PARAMS", BAD_REQUEST, diff_error, http_status=400)
|
||||||
|
|
||||||
# 校验总题数上限
|
# 校验总题数(不设上限,但提醒用户大量出题可能影响性能)
|
||||||
MAX_TOTAL_QUESTIONS = 20
|
|
||||||
total_requested = sum(question_types.values())
|
total_requested = sum(question_types.values())
|
||||||
if total_requested > MAX_TOTAL_QUESTIONS:
|
if total_requested <= 0:
|
||||||
return error_response("INVALID_PARAMS", BAD_REQUEST,
|
return error_response("INVALID_PARAMS", BAD_REQUEST,
|
||||||
f"总题数不能超过 {MAX_TOTAL_QUESTIONS} 道,当前请求 {total_requested} 道",
|
"总题数必须大于0",
|
||||||
http_status=400)
|
http_status=400)
|
||||||
|
if total_requested > 50:
|
||||||
|
logger.warning(f"[出题] 请求生成 {total_requested} 道题,可能影响性能")
|
||||||
|
|
||||||
# 校验排除题干列表(可选)
|
# 校验排除题干列表(可选)
|
||||||
exclude_stems = data.get('exclude_stems')
|
exclude_stems = data.get('exclude_stems')
|
||||||
@@ -169,7 +170,7 @@ def api_generate_questions():
|
|||||||
# 获取当前用户
|
# 获取当前用户
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
if not user:
|
if not user:
|
||||||
return error_response("UNAUTHORIZED", BAD_REQUEST, "未认证", http_status=401)
|
return error_response("UNAUTHORIZED", UNAUTHORIZED, "未认证", http_status=401)
|
||||||
|
|
||||||
# 检查向量库访问权限
|
# 检查向量库访问权限
|
||||||
if not check_collection_permission(
|
if not check_collection_permission(
|
||||||
@@ -178,9 +179,9 @@ def api_generate_questions():
|
|||||||
collection_name=collection,
|
collection_name=collection,
|
||||||
operation="read"
|
operation="read"
|
||||||
):
|
):
|
||||||
return error_response("FORBIDDEN", BAD_REQUEST, "权限不足", http_status=403)
|
return error_response("FORBIDDEN", FORBIDDEN, "权限不足", http_status=403)
|
||||||
|
|
||||||
# 调用新版出题接口
|
# 调用新版出题接口(同步)
|
||||||
result = generate_questions_from_file(
|
result = generate_questions_from_file(
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
collection=collection,
|
collection=collection,
|
||||||
@@ -251,7 +252,7 @@ def api_generate_smart():
|
|||||||
# 获取当前用户
|
# 获取当前用户
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
if not user:
|
if not user:
|
||||||
return error_response("UNAUTHORIZED", BAD_REQUEST, "未认证", http_status=401)
|
return error_response("UNAUTHORIZED", UNAUTHORIZED, "未认证", http_status=401)
|
||||||
|
|
||||||
# 检查向量库访问权限
|
# 检查向量库访问权限
|
||||||
if not check_collection_permission(
|
if not check_collection_permission(
|
||||||
@@ -260,7 +261,7 @@ def api_generate_smart():
|
|||||||
collection_name=collection,
|
collection_name=collection,
|
||||||
operation="read"
|
operation="read"
|
||||||
):
|
):
|
||||||
return error_response("FORBIDDEN", BAD_REQUEST, "权限不足", http_status=403)
|
return error_response("FORBIDDEN", FORBIDDEN, "权限不足", http_status=403)
|
||||||
|
|
||||||
# 1. 调用 AI 分析文件,获取推荐的题型和数量
|
# 1. 调用 AI 分析文件,获取推荐的题型和数量
|
||||||
from exam_pkg.manager import analyze_file_for_exam
|
from exam_pkg.manager import analyze_file_for_exam
|
||||||
|
|||||||
@@ -1055,7 +1055,7 @@ def analyze_document_for_exam(chunks: List[Dict], max_total: int = None) -> Dict
|
|||||||
|
|
||||||
注意:
|
注意:
|
||||||
- 不适合的题型数量设为 0
|
- 不适合的题型数量设为 0
|
||||||
- 所有数量之和不要超过 {min(total_knowledge_points * 2, 20)}
|
- 所有数量之和不要超过 {min(total_knowledge_points * 2, max_total if max_total else 20)}
|
||||||
- 必须返回合法 JSON,不要有其他内容
|
- 必须返回合法 JSON,不要有其他内容
|
||||||
|
|
||||||
请直接输出 JSON:"""
|
请直接输出 JSON:"""
|
||||||
|
|||||||
@@ -219,6 +219,29 @@ def grade_fill_blank(answer: Dict) -> Dict:
|
|||||||
student_answers = answer.get('student_answer', [])
|
student_answers = answer.get('student_answer', [])
|
||||||
max_score = answer.get('max_score', 4.0)
|
max_score = answer.get('max_score', 4.0)
|
||||||
|
|
||||||
|
# 校验学生答案格式(必须是列表)
|
||||||
|
if not isinstance(student_answers, list):
|
||||||
|
logger.warning(f"填空题学生答案格式错误: 期望列表,实际为 {type(student_answers).__name__}: {student_answers}")
|
||||||
|
return {
|
||||||
|
"question_id": answer.get('question_id'),
|
||||||
|
"score": 0,
|
||||||
|
"max_score": max_score,
|
||||||
|
"grading_status": "failed",
|
||||||
|
"details": {"error": f"学生答案格式错误,期望列表,实际为 {type(student_answers).__name__}"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 校验学生答案列表中的每个元素必须是字符串
|
||||||
|
for i, ans in enumerate(student_answers):
|
||||||
|
if not isinstance(ans, str):
|
||||||
|
logger.warning(f"填空题学生答案第 {i+1} 项格式错误: 期望字符串,实际为 {type(ans).__name__}: {ans}")
|
||||||
|
return {
|
||||||
|
"question_id": answer.get('question_id'),
|
||||||
|
"score": 0,
|
||||||
|
"max_score": max_score,
|
||||||
|
"grading_status": "failed",
|
||||||
|
"details": {"error": f"填空题学生答案第 {i+1} 项格式错误,期望字符串,实际为 {type(ans).__name__}"}
|
||||||
|
}
|
||||||
|
|
||||||
# 归一化答案格式(修复 LLM 生成的扁平数组问题)
|
# 归一化答案格式(修复 LLM 生成的扁平数组问题)
|
||||||
blank_count = question_content.get('data', {}).get('blank_count', 0)
|
blank_count = question_content.get('data', {}).get('blank_count', 0)
|
||||||
correct_answers = _normalize_fill_blank_answer(correct_answers, blank_count)
|
correct_answers = _normalize_fill_blank_answer(correct_answers, blank_count)
|
||||||
|
|||||||
Reference in New Issue
Block a user