diff --git a/backend/scripts/fix_exam_scores.py b/backend/scripts/fix_exam_scores.py
new file mode 100644
index 0000000..4b85cf3
--- /dev/null
+++ b/backend/scripts/fix_exam_scores.py
@@ -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)
diff --git a/frontend/src/views/exam/practice.vue b/frontend/src/views/exam/practice.vue
index 1bd5d6b..1174855 100644
--- a/frontend/src/views/exam/practice.vue
+++ b/frontend/src/views/exam/practice.vue
@@ -35,7 +35,7 @@