Files
012-kaopeilian/backend/scripts/fix_exam_scores.py
yuliang_guo 0b7c07eb7f
All checks were successful
continuous-integration/drone/push Build is passing
feat: 添加请求验证错误详细日志
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-01-31 10:03:54 +08:00

202 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)