Files
012-kaopeilian/backend/app/models/growth_path.py
yuliang_guo 973ce53bf3
All checks were successful
continuous-integration/drone/push Build is passing
feat: 完善成长路径画布设计器
后端:
- 添加 position_x, position_y 字段保存节点位置

前端:
- 支持从节点右侧圆点拖拽出箭头连接到其他课程
- 自动根据节点Y坐标识别所属阶段
- 保存并恢复节点位置,不再重置
- 阶段区域高亮显示
- 循环依赖检测

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 14:55:01 +08:00

219 lines
6.4 KiB
Python

"""
成长路径相关数据库模型
"""
from enum import Enum
from typing import List, Optional, TYPE_CHECKING
from datetime import datetime
from decimal import Decimal
if TYPE_CHECKING:
from app.models.course import Course
from app.models.user import User
from sqlalchemy import (
String,
Text,
Integer,
Boolean,
ForeignKey,
Enum as SQLEnum,
JSON,
DateTime,
DECIMAL,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import BaseModel, SoftDeleteMixin
class GrowthPathStatus(str, Enum):
"""成长路径学习状态"""
NOT_STARTED = "not_started" # 未开始
IN_PROGRESS = "in_progress" # 进行中
COMPLETED = "completed" # 已完成
class NodeStatus(str, Enum):
"""节点状态"""
LOCKED = "locked" # 锁定(前置未完成)
UNLOCKED = "unlocked" # 已解锁(可以开始)
IN_PROGRESS = "in_progress" # 学习中
COMPLETED = "completed" # 已完成
class GrowthPathNode(BaseModel, SoftDeleteMixin):
"""
成长路径节点表
每个节点对应一门课程
"""
__tablename__ = "growth_path_nodes"
# 关联
growth_path_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("growth_paths.id", ondelete="CASCADE"),
nullable=False,
comment="成长路径ID"
)
course_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("courses.id", ondelete="CASCADE"),
nullable=False,
comment="课程ID"
)
# 节点信息
stage_name: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True, comment="所属阶段名称"
)
title: Mapped[str] = mapped_column(
String(200), nullable=False, comment="节点标题"
)
description: Mapped[Optional[str]] = mapped_column(
Text, nullable=True, comment="节点描述"
)
# 配置
order_num: Mapped[int] = mapped_column(
Integer, default=0, nullable=False, comment="排序顺序"
)
is_required: Mapped[bool] = mapped_column(
Boolean, default=True, nullable=False, comment="是否必修"
)
prerequisites: Mapped[Optional[List[int]]] = mapped_column(
JSON, nullable=True, comment="前置节点IDs"
)
estimated_days: Mapped[int] = mapped_column(
Integer, default=7, nullable=False, comment="预计学习天数"
)
# 画布位置(用于可视化编辑器)
position_x: Mapped[Optional[int]] = mapped_column(
Integer, nullable=True, default=0, comment="画布X坐标"
)
position_y: Mapped[Optional[int]] = mapped_column(
Integer, nullable=True, default=0, comment="画布Y坐标"
)
# 关联关系
growth_path: Mapped["GrowthPath"] = relationship( # noqa: F821
"GrowthPath", back_populates="nodes"
)
course: Mapped["Course"] = relationship("Course")
user_completions: Mapped[List["UserNodeCompletion"]] = relationship(
"UserNodeCompletion", back_populates="node"
)
class UserGrowthPathProgress(BaseModel):
"""
用户成长路径进度表
记录用户在某条成长路径上的整体进度
"""
__tablename__ = "user_growth_path_progress"
# 关联
user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
comment="用户ID"
)
growth_path_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("growth_paths.id", ondelete="CASCADE"),
nullable=False,
comment="成长路径ID"
)
# 进度信息
current_node_id: Mapped[Optional[int]] = mapped_column(
Integer, nullable=True, comment="当前学习节点ID"
)
completed_node_ids: Mapped[Optional[List[int]]] = mapped_column(
JSON, nullable=True, comment="已完成节点IDs"
)
total_progress: Mapped[Decimal] = mapped_column(
DECIMAL(5, 2), default=0.00, nullable=False, comment="总进度百分比"
)
# 状态
status: Mapped[str] = mapped_column(
String(20),
default=GrowthPathStatus.NOT_STARTED.value,
nullable=False,
comment="状态"
)
# 时间记录
started_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True), nullable=True, comment="开始时间"
)
completed_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True), nullable=True, comment="完成时间"
)
last_activity_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True), nullable=True, comment="最后活动时间"
)
# 关联关系
user: Mapped["User"] = relationship("User")
growth_path: Mapped["GrowthPath"] = relationship("GrowthPath") # noqa: F821
class UserNodeCompletion(BaseModel):
"""
用户节点完成记录表
详细记录用户在每个节点上的学习状态
"""
__tablename__ = "user_node_completions"
# 关联
user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
comment="用户ID"
)
growth_path_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("growth_paths.id", ondelete="CASCADE"),
nullable=False,
comment="成长路径ID"
)
node_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("growth_path_nodes.id", ondelete="CASCADE"),
nullable=False,
comment="节点ID"
)
# 进度信息
course_progress: Mapped[Decimal] = mapped_column(
DECIMAL(5, 2), default=0.00, nullable=False, comment="课程学习进度"
)
status: Mapped[str] = mapped_column(
String(20),
default=NodeStatus.LOCKED.value,
nullable=False,
comment="状态"
)
# 时间记录
unlocked_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True), nullable=True, comment="解锁时间"
)
started_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True), nullable=True, comment="开始学习时间"
)
completed_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True), nullable=True, comment="完成时间"
)
# 关联关系
user: Mapped["User"] = relationship("User")
node: Mapped["GrowthPathNode"] = relationship(
"GrowthPathNode", back_populates="user_completions"
)
growth_path: Mapped["GrowthPath"] = relationship("GrowthPath") # noqa: F821