Files
item_bank/app/models/question.py
T
2026-08-01 16:50:55 +08:00

102 lines
3.6 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.
"""题目、标签、题目-标签关联。"""
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")