- 使用 distributeScores 智能整数分配题目分数 - 格式化分数显示,避免显示长小数 - 总分100分,根据题目数量智能分配整数分数 例如:11道题 = [10,10,10,10,10,10,10,10,10,5,5] 而不是 [9.09...,9.09...]
This commit is contained in:
201
backend/scripts/fix_exam_scores.py
Normal file
201
backend/scripts/fix_exam_scores.py
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
修复历史考试的小数分数问题
|
||||
|
||||
将历史考试中的小数分数(如 0.9090909090909091)转换为整数分数
|
||||
使用智能整数分配算法,确保所有题目分数之和等于总分
|
||||
|
||||
使用方法:
|
||||
# 在后端容器中执行
|
||||
cd /app
|
||||
python scripts/fix_exam_scores.py --dry-run # 预览模式,不实际修改
|
||||
python scripts/fix_exam_scores.py # 实际执行修复
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from decimal import Decimal
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
def distribute_integer_scores(total_score: float, question_count: int) -> list:
|
||||
"""
|
||||
整数分配分数
|
||||
|
||||
将总分分配为整数,前面的题目分数可能比后面的多1分
|
||||
|
||||
示例:
|
||||
100分 / 6题 = [17, 17, 17, 17, 16, 16]
|
||||
100分 / 11题 = [10, 10, 10, 10, 10, 10, 10, 10, 10, 5, 5]
|
||||
"""
|
||||
total = int(total_score)
|
||||
count = question_count
|
||||
|
||||
# 基础分数(向下取整)
|
||||
base_score = total // count
|
||||
# 需要额外加1分的题目数量
|
||||
extra_count = total % count
|
||||
|
||||
# 生成分数列表
|
||||
scores = []
|
||||
for i in range(count):
|
||||
if i < extra_count:
|
||||
scores.append(base_score + 1)
|
||||
else:
|
||||
scores.append(base_score)
|
||||
|
||||
return scores
|
||||
|
||||
|
||||
def is_decimal_score(score) -> bool:
|
||||
"""检查分数是否是小数(非整数)"""
|
||||
if score is None:
|
||||
return False
|
||||
try:
|
||||
score_float = float(score)
|
||||
return score_float != int(score_float)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
def fix_exam_scores(dry_run: bool = True, db_url: str = None):
|
||||
"""
|
||||
修复考试分数
|
||||
|
||||
Args:
|
||||
dry_run: 如果为 True,只预览不实际修改
|
||||
db_url: 数据库连接字符串,如果为 None 则从环境变量读取
|
||||
"""
|
||||
import os
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# 获取数据库连接
|
||||
if db_url is None:
|
||||
db_url = os.environ.get('DATABASE_URL')
|
||||
if not db_url:
|
||||
# 尝试从配置文件读取
|
||||
try:
|
||||
from app.core.config import settings
|
||||
db_url = settings.DATABASE_URL
|
||||
except:
|
||||
print("错误:无法获取数据库连接字符串")
|
||||
print("请设置 DATABASE_URL 环境变量或确保在正确的目录下运行")
|
||||
sys.exit(1)
|
||||
|
||||
# 将 mysql+aiomysql:// 转换为 mysql+pymysql://
|
||||
if 'aiomysql' in db_url:
|
||||
db_url = db_url.replace('aiomysql', 'pymysql')
|
||||
|
||||
print(f"连接数据库...")
|
||||
engine = create_engine(db_url, echo=False)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
|
||||
try:
|
||||
# 查询所有考试记录
|
||||
result = session.execute(text("""
|
||||
SELECT id, user_id, course_id, exam_name, question_count, total_score, questions
|
||||
FROM exams
|
||||
WHERE questions IS NOT NULL
|
||||
ORDER BY id DESC
|
||||
"""))
|
||||
|
||||
exams = result.fetchall()
|
||||
print(f"找到 {len(exams)} 条考试记录")
|
||||
|
||||
fixed_count = 0
|
||||
skipped_count = 0
|
||||
error_count = 0
|
||||
|
||||
for exam in exams:
|
||||
exam_id, user_id, course_id, exam_name, question_count, total_score, questions_json = exam
|
||||
|
||||
try:
|
||||
# 解析 questions JSON
|
||||
if isinstance(questions_json, str):
|
||||
questions = json.loads(questions_json)
|
||||
else:
|
||||
questions = questions_json
|
||||
|
||||
if not questions or not isinstance(questions, list):
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# 检查是否有小数分数
|
||||
has_decimal = False
|
||||
for q in questions:
|
||||
if 'score' in q and is_decimal_score(q['score']):
|
||||
has_decimal = True
|
||||
break
|
||||
|
||||
if not has_decimal:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# 计算新的整数分数
|
||||
actual_count = len(questions)
|
||||
actual_total = total_score or 100
|
||||
new_scores = distribute_integer_scores(actual_total, actual_count)
|
||||
|
||||
# 更新每道题的分数
|
||||
old_scores = [q.get('score', 0) for q in questions]
|
||||
for i, q in enumerate(questions):
|
||||
q['score'] = new_scores[i]
|
||||
|
||||
# 验证总分
|
||||
new_total = sum(new_scores)
|
||||
if abs(new_total - actual_total) > 0.01:
|
||||
print(f" 警告:exam_id={exam_id} 分数总和不匹配: {new_total} != {actual_total}")
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
print(f"[预览] exam_id={exam_id}, 课程={course_id}, 题数={actual_count}, 总分={actual_total}")
|
||||
print(f" 旧分数: {old_scores[:5]}..." if len(old_scores) > 5 else f" 旧分数: {old_scores}")
|
||||
print(f" 新分数: {new_scores[:5]}..." if len(new_scores) > 5 else f" 新分数: {new_scores}")
|
||||
else:
|
||||
# 实际更新数据库
|
||||
new_json = json.dumps(questions, ensure_ascii=False)
|
||||
session.execute(text("""
|
||||
UPDATE exams SET questions = :questions WHERE id = :exam_id
|
||||
"""), {"questions": new_json, "exam_id": exam_id})
|
||||
print(f"[已修复] exam_id={exam_id}, 题数={actual_count}, 分数: {new_scores}")
|
||||
|
||||
fixed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f" 错误:处理 exam_id={exam_id} 时出错: {e}")
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
if not dry_run:
|
||||
session.commit()
|
||||
print("\n已提交数据库更改")
|
||||
|
||||
print(f"\n=== 统计 ===")
|
||||
print(f"需要修复: {fixed_count}")
|
||||
print(f"已跳过(无小数): {skipped_count}")
|
||||
print(f"错误: {error_count}")
|
||||
|
||||
if dry_run:
|
||||
print("\n这是预览模式,未实际修改数据库。")
|
||||
print("如需实际执行,请去掉 --dry-run 参数重新运行。")
|
||||
|
||||
except Exception as e:
|
||||
print(f"执行失败: {e}")
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="修复历史考试的小数分数问题")
|
||||
parser.add_argument("--dry-run", action="store_true", help="预览模式,不实际修改数据库")
|
||||
parser.add_argument("--db-url", type=str, help="数据库连接字符串")
|
||||
args = parser.parse_args()
|
||||
|
||||
fix_exam_scores(dry_run=args.dry_run, db_url=args.db_url)
|
||||
Reference in New Issue
Block a user