nex_math/backend/seed/__main__.py

250 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""初始化数据库:RBAC 账号、学习档案、题库与教材目录。
用法:在 backend/ 目录下执行 python -m seed [--force]
--force 会清空全部学习数据与题库后重新导入,保留用户与角色。
这里是**唯一**会补种数据的地方:后端每次启动只对齐表结构
(``database.ensure_schema``),不再补章节 / 知识点 / 教材等业务数据,
因此自己准备的 math.db 覆盖回来后不会被种子改动。
"""
from __future__ import annotations
import argparse
import json
from datetime import date, timedelta
from sqlalchemy import text
from database import (
DATABASE_URL,
SessionLocal,
ensure_schema,
migrate_legacy_courses,
)
from models import (
AttemptItem,
AttemptSession,
DailyCompletion,
ErrorEntry,
Question,
User,
UserKnowledge,
)
from seed.catalog import ensure_catalog
from seed.data import KNOWLEDGE, QUESTIONS
from seed.ebooks import ensure_ebooks
from seed.knowledge_graph import ensure_knowledge_graph
from seed.rbac import DEFAULT_USERS, ensure_rbac
from services import ebooks as ebook_store
from services.learning_profile import (
ensure_default_profiles,
remove_unused_admin_profile,
user_has_admin_role,
)
from services.llm_config import ensure_llm_channels
from services.knowledge_service import set_question_knowledge_names
def _insert_questions(db) -> None:
for item in QUESTIONS:
db.add(
Question(
stem=item["stem"],
options=json.dumps(item["options"], ensure_ascii=False),
correct_index=item["correct_index"],
knowledge_name=item["knowledge_name"],
explanation=item["explanation"],
)
)
def _demo_history(db, student: User) -> None:
questions = db.query(Question).order_by(Question.id.asc()).all()
configs = [
# (标题, 题目, 答错题号集合, 几天前)
("第一轮基础题", questions[:10], {2, 4, 7}, 3),
("第二轮综合题", questions[10:18], {1, 3, 5}, 1),
]
for title, questions, wrong_indexes, days_ago in configs:
if not questions:
continue
correct_count = len(questions) - len(wrong_indexes)
session = AttemptSession(
user_id=student.id,
mode="legacy",
title=title,
total=len(questions),
correct=correct_count,
happened_on=date.today() - timedelta(days=days_ago),
)
db.add(session)
db.flush()
for index, question in enumerate(questions, start=1):
is_correct = index not in wrong_indexes
selected = (
question.correct_index
if is_correct
else (question.correct_index + 1) % 4
)
teacher_text = question.explanation or "结合定义与图像性质重新判断。"
question.used_count = (question.used_count or 0) + 1
db.add(
AttemptItem(
attempt_id=session.id,
user_id=student.id,
question_index=index,
question_id=question.id,
stem=question.stem,
options=question.options,
correct_index=question.correct_index,
selected=selected,
correct=is_correct,
knowledge_name=question.knowledge_name,
knowledge_names=json.dumps(
[question.knowledge_name], ensure_ascii=False
),
explanation=teacher_text,
)
)
if not is_correct:
db.add(
ErrorEntry(
user_id=student.id,
attempt_id=session.id,
source_label=f"{title} · {index}",
knowledge_name=question.knowledge_name,
knowledge_names=json.dumps(
[question.knowledge_name], ensure_ascii=False
),
diagnosis="答题错误,需要复习",
question_id=question.id,
stem=question.stem,
options=question.options,
selected=selected,
correct_index=question.correct_index,
explanation=teacher_text,
)
)
# 保持每日任务演示数据(昨天已完成)
db.add(DailyCompletion(user_id=student.id, done_on=date.today() - timedelta(days=1)))
def _backfill_derived_fields(db) -> None:
"""给存量数据补派生字段(题目使用次数、知识点名快照)。
以前这些 UPDATE 挂在后端启动流程里,现在只属于初始化:
自己准备好的库覆盖回来后不会被重新计算 / 覆盖。
"""
db.execute(
text(
"UPDATE questions SET used_count = ("
"SELECT COUNT(*) FROM attempt_items "
"WHERE attempt_items.question_id = questions.id"
") WHERE id IN (SELECT DISTINCT question_id FROM attempt_items "
"WHERE question_id IS NOT NULL)"
)
)
for question in db.query(Question).all():
has_links = db.execute(
text(
"SELECT 1 FROM question_knowledge "
"WHERE question_id = :qid LIMIT 1"
),
{"qid": question.id},
).first()
if has_links is None:
set_question_knowledge_names(
db,
question,
[question.knowledge_name],
attach_to_chapter=True,
)
for row in db.query(ErrorEntry).all():
if not row.knowledge_names or row.knowledge_names == "[]":
row.knowledge_names = json.dumps(
[row.knowledge_name], ensure_ascii=False
)
for row in db.query(AttemptItem).all():
if not row.knowledge_names or row.knowledge_names == "[]":
row.knowledge_names = json.dumps(
[row.knowledge_name], ensure_ascii=False
)
db.commit()
def load_seed(force: bool = False) -> bool:
ensure_schema()
# 补列之后再搬数据:迁移要写 knowledge_resources.course_id
migrate_legacy_courses()
db = SessionLocal()
created = False
try:
ensure_rbac(db)
ensure_knowledge_graph(db)
users = {
username: db.query(User).filter(User.username == username).first()
for username, _, _, _ in DEFAULT_USERS
}
ensure_default_profiles(db)
ensure_llm_channels(db)
if force:
# --force 会清掉题库与学习数据:先留一份可回退的备份
from dbtool import backup as backup_database
saved = backup_database(label="pre-force")
print(f"[seed] --force 将清空题库与学习数据,已先备份:{saved}")
db.query(AttemptItem).delete()
db.query(DailyCompletion).delete()
db.query(ErrorEntry).delete()
db.query(AttemptSession).delete()
db.query(UserKnowledge).delete()
db.query(Question).delete()
ensure_default_profiles(db)
_insert_questions(db)
db.flush()
for question in db.query(Question).all():
set_question_knowledge_names(
db, question, [question.knowledge_name]
)
_demo_history(db, users["student"])
created = True
elif db.query(Question).count() == 0:
_insert_questions(db)
db.flush()
for question in db.query(Question).all():
set_question_knowledge_names(
db, question, [question.knowledge_name]
)
_demo_history(db, users["student"])
created = True
db.commit()
ensure_catalog(db, force_sync=force)
# 电子书与教材目录同源:种子跑完就把 res/ 的书和磁盘上的存量电子书挂好
ensure_ebooks(db)
ebook_store.backfill_pages(db)
_backfill_derived_fields(db)
for user in users.values():
if user and user_has_admin_role(db, user.id):
remove_unused_admin_profile(db, user.id)
return created
finally:
db.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="初始化数学学习系统数据库")
parser.add_argument("--force", action="store_true", help="清空学习数据后重新导入种子")
args = parser.parse_args()
created = load_seed(force=args.force)
print(f"数据库:{DATABASE_URL}")
if created:
total = len(QUESTIONS)
print(
"已完成初始化:默认账号 admin/admin123、student/student123,"
f"{total} 道题目。"
)
else:
print("已存在数据,跳过导入(重置可运行:python -m seed --force)。")