nex_math/backend/models/__init__.py

400 lines
16 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.

"""ORM 模型:知识点、题目、练习记录、错题与每日完成状态。"""
from __future__ import annotations
from datetime import date, datetime
from sqlalchemy import (
Boolean,
Date,
Float,
ForeignKey,
Integer,
String,
Text,
UniqueConstraint,
DateTime,
)
from sqlalchemy.orm import Mapped, mapped_column
from database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(Text)
display_name: Mapped[str] = mapped_column(String(64), default="")
active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class Role(Base):
__tablename__ = "roles"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
code: Mapped[str] = mapped_column(String(32), unique=True)
name: Mapped[str] = mapped_column(String(64))
description: Mapped[str] = mapped_column(Text, default="")
class Permission(Base):
__tablename__ = "permissions"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
code: Mapped[str] = mapped_column(String(64), unique=True)
name: Mapped[str] = mapped_column(String(64))
group: Mapped[str] = mapped_column(String(32), default="system")
class UserRole(Base):
__tablename__ = "user_roles"
__table_args__ = (UniqueConstraint("user_id", "role_id"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
role_id: Mapped[int] = mapped_column(
ForeignKey("roles.id", ondelete="CASCADE"), index=True
)
class RolePermission(Base):
__tablename__ = "role_permissions"
__table_args__ = (UniqueConstraint("role_id", "permission_id"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
role_id: Mapped[int] = mapped_column(
ForeignKey("roles.id", ondelete="CASCADE"), index=True
)
permission_id: Mapped[int] = mapped_column(
ForeignKey("permissions.id", ondelete="CASCADE"), index=True
)
class Knowledge(Base):
"""全局知识点(同一知识点可被多个章节复用,用关系表关联)。"""
__tablename__ = "knowledge"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(64), unique=True)
domain: Mapped[str] = mapped_column(String(16), default="初等") # 初等 / 高等
category: Mapped[str] = mapped_column(String(64), default="")
description: Mapped[str] = mapped_column(Text, default="")
class KnowledgeRelation(Base):
"""知识点间关系,支持未来知识图谱(前置/包含/相关等)。"""
__tablename__ = "knowledge_relations"
__table_args__ = (
UniqueConstraint("source_id", "target_id", "relation_type"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
source_id: Mapped[int] = mapped_column(
ForeignKey("knowledge.id", ondelete="CASCADE"), index=True
)
target_id: Mapped[int] = mapped_column(
ForeignKey("knowledge.id", ondelete="CASCADE"), index=True
)
relation_type: Mapped[str] = mapped_column(
String(16), default="前置"
) # 前置 / 包含 / 相关
class KnowledgeResource(Base):
"""知识点对外资源链接(视频 / 在线课程 / 电子教材)。
教材与章节不在此登记:章节通过 ChapterKnowledge 反向引用知识点,
知识点详情据此反查“哪本书的哪一章讲了它”。
"""
__tablename__ = "knowledge_resources"
__table_args__ = (UniqueConstraint("knowledge_id", "kind", "url"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
knowledge_id: Mapped[int] = mapped_column(
ForeignKey("knowledge.id", ondelete="CASCADE"), index=True
)
kind: Mapped[str] = mapped_column(String(16), default="video") # video | book
title: Mapped[str] = mapped_column(String(128), default="")
url: Mapped[str] = mapped_column(String(256), default="")
textbook_id: Mapped[int | None] = mapped_column(
ForeignKey("textbooks.id", ondelete="CASCADE"),
nullable=True,
)
course_id: Mapped[int | None] = mapped_column(
ForeignKey("courses.id", ondelete="CASCADE"),
nullable=True,
)
position: Mapped[int] = mapped_column(Integer, default=0)
class ChapterKnowledge(Base):
__tablename__ = "chapter_knowledge"
__table_args__ = (UniqueConstraint("chapter_id", "knowledge_id"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
chapter_id: Mapped[int] = mapped_column(
ForeignKey("chapters.id", ondelete="CASCADE"), index=True
)
knowledge_id: Mapped[int] = mapped_column(
ForeignKey("knowledge.id", ondelete="CASCADE"), index=True
)
class QuestionKnowledge(Base):
__tablename__ = "question_knowledge"
__table_args__ = (UniqueConstraint("question_id", "knowledge_id"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
question_id: Mapped[int] = mapped_column(
ForeignKey("questions.id", ondelete="CASCADE"), index=True
)
knowledge_id: Mapped[int] = mapped_column(
ForeignKey("knowledge.id", ondelete="CASCADE"), index=True
)
class UserKnowledge(Base):
"""用户对某全局知识点的掌握度(知识点经章节/题目引用,不靠名称匹配)。"""
__tablename__ = "user_knowledge"
__table_args__ = (UniqueConstraint("user_id", "knowledge_id"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
knowledge_id: Mapped[int] = mapped_column(
ForeignKey("knowledge.id", ondelete="CASCADE"), index=True
)
mastery: Mapped[int] = mapped_column(Integer, default=60)
position: Mapped[int] = mapped_column(Integer, default=0)
class Textbook(Base):
__tablename__ = "textbooks"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(128), unique=True)
author: Mapped[str] = mapped_column(String(128), default="")
publisher: Mapped[str] = mapped_column(String(128), default="")
isbn: Mapped[str] = mapped_column(String(32), default="")
edition: Mapped[str] = mapped_column(String(64), default="")
grade: Mapped[str] = mapped_column(String(32), default="")
link: Mapped[str] = mapped_column(String(256), default="")
description: Mapped[str] = mapped_column(Text, default="")
position: Mapped[int] = mapped_column(Integer, default=0)
# 电子书:文件放在 data/ebooks/{textbook_id}/ 下,表里只存文件名与元信息
ebook_file: Mapped[str] = mapped_column(String(256), default="")
ebook_name: Mapped[str] = mapped_column(String(256), default="") # 上传时的原始文件名
ebook_format: Mapped[str] = mapped_column(String(8), default="") # pdf | epub
ebook_size: Mapped[int] = mapped_column(Integer, default=0)
# 篇幅:PDF 页数 / EPUB 章节数,阅读器首次打开后回填真实值
ebook_pages: Mapped[int] = mapped_column(Integer, default=0)
ebook_uploaded_at: Mapped[datetime | None] = mapped_column(
DateTime, nullable=True
)
class Course(Base):
"""在线课程 / 视频课:只有外链,不带章节与电子书,因此与教材分表存放。"""
__tablename__ = "courses"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(128), unique=True)
provider: Mapped[str] = mapped_column(String(128), default="") # 开课方
url: Mapped[str] = mapped_column(String(256), default="")
grade: Mapped[str] = mapped_column(String(32), default="")
description: Mapped[str] = mapped_column(Text, default="")
position: Mapped[int] = mapped_column(Integer, default=0)
class Chapter(Base):
__tablename__ = "chapters"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
textbook_id: Mapped[int] = mapped_column(
ForeignKey("textbooks.id", ondelete="CASCADE"), index=True
)
name: Mapped[str] = mapped_column(String(128))
position: Mapped[int] = mapped_column(Integer, default=0)
summary: Mapped[str] = mapped_column(Text, default="")
# 章节在电子书里的起始定位:PDF 是页码,EPUB 是 spine 节序号;0 表示未设置
ebook_page: Mapped[int] = mapped_column(Integer, default=0)
class Question(Base):
__tablename__ = "questions"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# 遗留字段:早期“专项练习 / 自适应题库”分类,已不再作为业务概念使用,
# 仅为兼容旧数据库保留列。
source: Mapped[str] = mapped_column(String(16), default="bank")
stem: Mapped[str] = mapped_column(Text)
options: Mapped[str] = mapped_column(Text) # JSON 字符串
correct_index: Mapped[int] = mapped_column(Integer)
knowledge_name: Mapped[str] = mapped_column(String(64), index=True)
explanation: Mapped[str] = mapped_column(Text, default="")
chapter_id: Mapped[int | None] = mapped_column(
ForeignKey("chapters.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
difficulty: Mapped[int] = mapped_column(Integer, default=1)
is_generated: Mapped[bool] = mapped_column(Boolean, default=False)
used_count: Mapped[int] = mapped_column(Integer, default=0)
class AttemptSession(Base):
__tablename__ = "attempt_sessions"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
mode: Mapped[str] = mapped_column(String(16))
title: Mapped[str] = mapped_column(String(64))
total: Mapped[int] = mapped_column(Integer)
correct: Mapped[int] = mapped_column(Integer)
chapter_id: Mapped[int | None] = mapped_column(
ForeignKey("chapters.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
happened_on: Mapped[date] = mapped_column(Date, default=date.today)
class AttemptItem(Base):
"""练习中的逐题作答明细(题目做快照保存,删除题库不影响历史记录)。"""
__tablename__ = "attempt_items"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
attempt_id: Mapped[int] = mapped_column(
ForeignKey("attempt_sessions.id", ondelete="CASCADE"), index=True
)
user_id: Mapped[int] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
question_index: Mapped[int] = mapped_column(Integer, default=1)
question_id: Mapped[int | None] = mapped_column(
ForeignKey("questions.id", ondelete="SET NULL"),
nullable=True,
)
stem: Mapped[str] = mapped_column(Text)
options: Mapped[str] = mapped_column(Text) # JSON 字符串
correct_index: Mapped[int] = mapped_column(Integer)
selected: Mapped[int | None] = mapped_column(Integer, nullable=True)
correct: Mapped[bool] = mapped_column(Boolean)
knowledge_name: Mapped[str] = mapped_column(String(64), index=True)
knowledge_names: Mapped[str] = mapped_column(Text, default="[]")
explanation: Mapped[str] = mapped_column(Text, default="")
class ErrorEntry(Base):
__tablename__ = "error_entries"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
source_label: Mapped[str] = mapped_column(String(64))
knowledge_name: Mapped[str] = mapped_column(String(64), index=True)
knowledge_names: Mapped[str] = mapped_column(Text, default="[]")
diagnosis: Mapped[str] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(16), default="待复习")
created_on: Mapped[date] = mapped_column(Date, default=date.today)
attempt_id: Mapped[int | None] = mapped_column(
ForeignKey("attempt_sessions.id", ondelete="SET NULL"),
nullable=True,
)
question_id: Mapped[int | None] = mapped_column(
ForeignKey("questions.id", ondelete="SET NULL"),
nullable=True,
)
stem: Mapped[str] = mapped_column(Text, default="")
options: Mapped[str] = mapped_column(Text, default="") # JSON 字符串
selected: Mapped[int | None] = mapped_column(Integer, nullable=True)
correct_index: Mapped[int | None] = mapped_column(Integer, nullable=True)
explanation: Mapped[str] = mapped_column(Text, default="")
class DailyCompletion(Base):
__tablename__ = "daily_completions"
__table_args__ = (UniqueConstraint("user_id", "done_on"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
chapter_id: Mapped[int | None] = mapped_column(
ForeignKey("chapters.id", ondelete="SET NULL"), nullable=True, index=True
)
rating: Mapped[str] = mapped_column(String(16), default="")
done_on: Mapped[date] = mapped_column(Date)
class LlmSetting(Base):
__tablename__ = "llm_settings"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(128), default="")
provider: Mapped[str] = mapped_column(String(32), default="openai")
api_key: Mapped[str] = mapped_column(Text, default="")
base_url: Mapped[str] = mapped_column(String(256), default="")
model: Mapped[str] = mapped_column(String(64), default="")
temperature: Mapped[float] = mapped_column(Float, default=0.3)
max_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class LlmTask(Base):
"""异步大模型任务:题目生成 / 章节生成,支持进度查询。"""
__tablename__ = "llm_tasks"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
kind: Mapped[str] = mapped_column(String(32)) # generate_questions / generate_chapters
status: Mapped[str] = mapped_column(String(16), default="pending")
progress: Mapped[int] = mapped_column(Integer, default=0)
message: Mapped[str] = mapped_column(Text, default="等待执行")
params: Mapped[str] = mapped_column(Text, default="{}")
result: Mapped[str] = mapped_column(Text, default="")
created_by: Mapped[int] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
finished_at: Mapped[datetime | None] = mapped_column(
DateTime, nullable=True
)
class EbookProgress(Base):
"""阅读进度:PDF 记页码,EPUB 记 CFI,每人每本书一条。"""
__tablename__ = "ebook_progress"
__table_args__ = (UniqueConstraint("user_id", "textbook_id"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
textbook_id: Mapped[int] = mapped_column(
ForeignKey("textbooks.id", ondelete="CASCADE"), index=True
)
locator: Mapped[str] = mapped_column(String(512), default="") # EPUB CFI
page: Mapped[int] = mapped_column(Integer, default=0)
total: Mapped[int] = mapped_column(Integer, default=0)
position: Mapped[float] = mapped_column(Float, default=0.0) # 0~1
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)