400 lines
16 KiB
Python
400 lines
16 KiB
Python
"""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
|
||
)
|