第一次提交

This commit is contained in:
2026-08-01 16:50:55 +08:00
commit c8f9e39039
68 changed files with 6016 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
"""题目、标签、题目-标签关联。"""
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")