102 lines
3.6 KiB
Python
102 lines
3.6 KiB
Python
"""题目、标签、题目-标签关联。"""
|
||
import enum
|
||
|
||
from sqlalchemy import (
|
||
Boolean,
|
||
ForeignKey,
|
||
Integer,
|
||
String,
|
||
Text,
|
||
UniqueConstraint,
|
||
)
|
||
from sqlalchemy import Enum as SAEnum
|
||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||
from sqlalchemy.types import JSON
|
||
|
||
from app.models.base import new_uuid
|
||
from app.db import Base, TimestampMixin
|
||
|
||
|
||
class QuestionType(str, enum.Enum):
|
||
# 拍照入库的默认值:先收进来,想练习时再补题型/答案
|
||
unclassified = "unclassified"
|
||
single_choice = "single_choice"
|
||
multiple_choice = "multiple_choice"
|
||
true_false = "true_false"
|
||
fill_blank = "fill_blank"
|
||
short_answer = "short_answer"
|
||
|
||
|
||
# 客观题:有标准答案时可本地精确判分;其余交给 AI 判
|
||
OBJECTIVE_TYPES = {
|
||
QuestionType.single_choice,
|
||
QuestionType.multiple_choice,
|
||
QuestionType.true_false,
|
||
}
|
||
|
||
|
||
class Tag(Base, TimestampMixin):
|
||
__tablename__ = "tags"
|
||
__table_args__ = (UniqueConstraint("user_id", "name", name="uq_tag_user_name"),)
|
||
|
||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||
user_id: Mapped[str] = mapped_column(
|
||
ForeignKey("users.id", ondelete="CASCADE"), index=True
|
||
)
|
||
name: Mapped[str] = mapped_column(String(128))
|
||
|
||
questions: Mapped[list["Question"]] = relationship(
|
||
secondary="question_tags", back_populates="tags"
|
||
)
|
||
|
||
|
||
class QuestionTag(Base):
|
||
__tablename__ = "question_tags"
|
||
|
||
question_id: Mapped[str] = mapped_column(
|
||
ForeignKey("questions.id", ondelete="CASCADE"), primary_key=True
|
||
)
|
||
tag_id: Mapped[str] = mapped_column(
|
||
ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True
|
||
)
|
||
|
||
|
||
class Question(Base, TimestampMixin):
|
||
"""一条笔记。
|
||
|
||
只有 type 和 stem_markdown 是必填,其中 type 可以是 unclassified,
|
||
所以"拍照识别完直接存"不需要用户补任何东西。
|
||
"""
|
||
|
||
__tablename__ = "questions"
|
||
|
||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||
user_id: Mapped[str] = mapped_column(
|
||
ForeignKey("users.id", ondelete="CASCADE"), index=True
|
||
)
|
||
type: Mapped[QuestionType] = mapped_column(
|
||
SAEnum(QuestionType), default=QuestionType.unclassified
|
||
)
|
||
stem_markdown: Mapped[str] = mapped_column(Text)
|
||
# 选项:[{"key": "A", "text_markdown": "..."}],选择/判断题用
|
||
options: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
||
# 标准答案:选择题 ["A","C"],填空/简答 ["文本"]
|
||
correct_answer: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
||
# AI 生成的讲解(每次 /ai/explain 会覆写,别往这里写用户内容)
|
||
explanation_markdown: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
# 用户自己写的笔记 / 错因反思,AI 永不触碰
|
||
my_note_markdown: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
# 手动标记"待复习";复习清单 = 这个标记 ∪ 最近一次作答判错
|
||
needs_review: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
||
difficulty: Mapped[int | None] = mapped_column(Integer, nullable=True) # 1-5
|
||
source_image_id: Mapped[str | None] = mapped_column(
|
||
ForeignKey("images.id", ondelete="SET NULL"), nullable=True, index=True
|
||
)
|
||
ocr_engine: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
|
||
tags: Mapped[list["Tag"]] = relationship(
|
||
secondary="question_tags", back_populates="questions"
|
||
)
|
||
# 便于详情页把原图和笔记一起展示
|
||
source_image: Mapped["Image | None"] = relationship("Image")
|