""" 课程相关的数据验证模型 """ from typing import Optional, List from datetime import datetime from enum import Enum from pydantic import BaseModel, Field, ConfigDict, field_validator from app.models.course import CourseStatus, CourseCategory class CourseBase(BaseModel): """ 课程基础模型 """ name: str = Field(..., min_length=1, max_length=200, description="课程名称") description: Optional[str] = Field(None, description="课程描述") category: CourseCategory = Field(default=CourseCategory.GENERAL, description="课程分类") cover_image: Optional[str] = Field(None, max_length=500, description="封面图片URL") duration_hours: Optional[float] = Field(None, ge=0, description="课程时长(小时)") difficulty_level: Optional[int] = Field(None, ge=1, le=5, description="难度等级(1-5)") tags: Optional[List[str]] = Field(default_factory=list, description="标签列表") sort_order: int = Field(default=0, description="排序顺序") is_featured: bool = Field(default=False, description="是否推荐") allow_download: bool = Field(default=False, description="是否允许下载资料") @field_validator("category", mode="before") @classmethod def normalize_category(cls, v): """允许使用枚举的名称或值(忽略大小写)。空字符串使用默认值。""" if isinstance(v, CourseCategory): return v if isinstance(v, str): s = v.strip() # 空字符串使用默认值 if not s: return CourseCategory.GENERAL # 优先按值匹配(technology 等) try: return CourseCategory(s.lower()) except Exception: pass # 再按名称匹配(TECHNOLOGY 等) try: return CourseCategory[s.upper()] except Exception: pass return v class CourseCreate(CourseBase): """ 创建课程模型 """ status: CourseStatus = Field(default=CourseStatus.DRAFT, description="课程状态") class CourseUpdate(BaseModel): """ 更新课程模型 """ name: Optional[str] = Field(None, min_length=1, max_length=200, description="课程名称") description: Optional[str] = Field(None, description="课程描述") category: Optional[CourseCategory] = Field(None, description="课程分类") status: Optional[CourseStatus] = Field(None, description="课程状态") cover_image: Optional[str] = Field(None, max_length=500, description="封面图片URL") duration_hours: Optional[float] = Field(None, ge=0, description="课程时长(小时)") difficulty_level: Optional[int] = Field(None, ge=1, le=5, description="难度等级(1-5)") tags: Optional[List[str]] = Field(None, description="标签列表") sort_order: Optional[int] = Field(None, description="排序顺序") is_featured: Optional[bool] = Field(None, description="是否推荐") allow_download: Optional[bool] = Field(None, description="是否允许下载资料") @field_validator("category", mode="before") @classmethod def normalize_category_update(cls, v): if v is None: return v if isinstance(v, CourseCategory): return v if isinstance(v, str): s = v.strip() if not s: # 空字符串视为None(不更新) return None try: return CourseCategory(s.lower()) except Exception: pass try: return CourseCategory[s.upper()] except Exception: pass return v class CourseInDB(CourseBase): """ 数据库中的课程模型 """ model_config = ConfigDict(from_attributes=True) id: int = Field(..., description="课程ID") status: CourseStatus = Field(..., description="课程状态") created_at: datetime = Field(..., description="创建时间") updated_at: datetime = Field(..., description="更新时间") published_at: Optional[datetime] = Field(None, description="发布时间") publisher_id: Optional[int] = Field(None, description="发布人ID") created_by: Optional[int] = Field(None, description="创建人ID") updated_by: Optional[int] = Field(None, description="更新人ID") # 用户岗位相关的课程类型(必修/选修),非数据库字段,由API动态计算 course_type: Optional[str] = Field(None, description="课程类型:required=必修, optional=选修") class CourseList(BaseModel): """ 课程列表查询参数 """ status: Optional[CourseStatus] = Field(None, description="课程状态") category: Optional[CourseCategory] = Field(None, description="课程分类") is_featured: Optional[bool] = Field(None, description="是否推荐") keyword: Optional[str] = Field(None, description="搜索关键词") # 课程资料相关模型 class CourseMaterialBase(BaseModel): """ 课程资料基础模型 """ name: str = Field(..., min_length=1, max_length=200, description="资料名称") description: Optional[str] = Field(None, description="资料描述") sort_order: int = Field(default=0, description="排序顺序") class CourseMaterialCreate(CourseMaterialBase): """ 创建课程资料模型 """ file_url: str = Field(..., max_length=500, description="文件URL") file_type: str = Field(..., max_length=50, description="文件类型") file_size: int = Field(..., gt=0, description="文件大小(字节)") @field_validator("file_type") def validate_file_type(cls, v): """验证文件类型 支持格式:TXT、Markdown、MDX、PDF、HTML、Excel、Word、CSV、VTT、Properties """ allowed_types = [ "txt", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "docx", "doc", "csv", "vtt", "properties" ] file_ext = v.lower() if file_ext not in allowed_types: raise ValueError(f"不支持的文件类型: {v}。允许的类型: TXT、Markdown、MDX、PDF、HTML、Excel、Word、CSV、VTT、Properties") return file_ext class CourseMaterialInDB(CourseMaterialBase): """ 数据库中的课程资料模型 """ model_config = ConfigDict(from_attributes=True) id: int = Field(..., description="资料ID") course_id: int = Field(..., description="课程ID") file_url: str = Field(..., description="文件URL") file_type: str = Field(..., description="文件类型") file_size: int = Field(..., description="文件大小(字节)") created_at: datetime = Field(..., description="创建时间") updated_at: datetime = Field(..., description="更新时间") # 知识点相关模型 class KnowledgePointBase(BaseModel): """ 知识点基础模型 """ name: str = Field(..., min_length=1, max_length=200, description="知识点名称") description: Optional[str] = Field(None, description="知识点描述") type: str = Field(default="理论知识", description="知识点类型") source: int = Field(default=0, description="来源:0=手动,1=AI分析") topic_relation: Optional[str] = Field(None, description="与主题的关系描述") class KnowledgePointCreate(KnowledgePointBase): """ 创建知识点模型 """ material_id: int = Field(..., description="关联资料ID(必填)") class KnowledgePointUpdate(BaseModel): """ 更新知识点模型 """ name: Optional[str] = Field(None, min_length=1, max_length=200, description="知识点名称") description: Optional[str] = Field(None, description="知识点描述") type: Optional[str] = Field(None, description="知识点类型") source: Optional[int] = Field(None, description="来源:0=手动,1=AI分析") topic_relation: Optional[str] = Field(None, description="与主题的关系描述") material_id: int = Field(..., description="关联资料ID(必填)") class KnowledgePointInDB(KnowledgePointBase): """ 数据库中的知识点模型 """ model_config = ConfigDict(from_attributes=True) id: int = Field(..., description="知识点ID") course_id: int = Field(..., description="课程ID") material_id: int = Field(..., description="关联资料ID") created_at: datetime = Field(..., description="创建时间") updated_at: datetime = Field(..., description="更新时间") class KnowledgePointTree(KnowledgePointInDB): """ 知识点树形结构 """ children: List["KnowledgePointTree"] = Field( default_factory=list, description="子知识点" ) # 成长路径相关模型 class GrowthPathCourse(BaseModel): """ 成长路径中的课程 """ course_id: int = Field(..., description="课程ID") order: int = Field(..., ge=0, description="排序") is_required: bool = Field(default=True, description="是否必修") class GrowthPathBase(BaseModel): """ 成长路径基础模型 """ name: str = Field(..., min_length=1, max_length=200, description="路径名称") description: Optional[str] = Field(None, description="路径描述") target_role: Optional[str] = Field(None, max_length=100, description="目标角色") courses: List[GrowthPathCourse] = Field(default_factory=list, description="课程列表") estimated_duration_days: Optional[int] = Field(None, ge=1, description="预计完成天数") is_active: bool = Field(default=True, description="是否启用") sort_order: int = Field(default=0, description="排序顺序") class GrowthPathCreate(GrowthPathBase): """ 创建成长路径模型 """ pass class GrowthPathInDB(GrowthPathBase): """ 数据库中的成长路径模型 """ model_config = ConfigDict(from_attributes=True) id: int = Field(..., description="路径ID") created_at: datetime = Field(..., description="创建时间") updated_at: datetime = Field(..., description="更新时间") # 课程考试设置相关Schema class CourseExamSettingsBase(BaseModel): """ 课程考试设置基础模型 """ single_choice_count: int = Field(default=4, ge=0, le=50, description="单选题数量") multiple_choice_count: int = Field(default=2, ge=0, le=30, description="多选题数量") true_false_count: int = Field(default=1, ge=0, le=20, description="判断题数量") fill_blank_count: int = Field(default=2, ge=0, le=10, description="填空题数量") essay_count: int = Field(default=1, ge=0, le=10, description="问答题数量") duration_minutes: int = Field(default=10, ge=10, le=180, description="考试时长(分钟)") difficulty_level: int = Field(default=3, ge=1, le=5, description="难度系数(1-5)") passing_score: int = Field(default=60, ge=0, le=100, description="及格分数") is_enabled: bool = Field(default=True, description="是否启用") show_answer_immediately: bool = Field(default=False, description="是否立即显示答案") allow_retake: bool = Field(default=True, description="是否允许重考") max_retake_times: Optional[int] = Field(None, ge=1, le=10, description="最大重考次数") class CourseExamSettingsCreate(CourseExamSettingsBase): """ 创建课程考试设置模型 """ pass class CourseExamSettingsUpdate(BaseModel): """ 更新课程考试设置模型 """ single_choice_count: Optional[int] = Field(None, ge=0, le=50, description="单选题数量") multiple_choice_count: Optional[int] = Field(None, ge=0, le=30, description="多选题数量") true_false_count: Optional[int] = Field(None, ge=0, le=20, description="判断题数量") fill_blank_count: Optional[int] = Field(None, ge=0, le=10, description="填空题数量") essay_count: Optional[int] = Field(None, ge=0, le=10, description="问答题数量") duration_minutes: Optional[int] = Field(None, ge=10, le=180, description="考试时长(分钟)") difficulty_level: Optional[int] = Field(None, ge=1, le=5, description="难度系数(1-5)") passing_score: Optional[int] = Field(None, ge=0, le=100, description="及格分数") is_enabled: Optional[bool] = Field(None, description="是否启用") show_answer_immediately: Optional[bool] = Field(None, description="是否立即显示答案") allow_retake: Optional[bool] = Field(None, description="是否允许重考") max_retake_times: Optional[int] = Field(None, ge=1, le=10, description="最大重考次数") class CourseExamSettingsInDB(CourseExamSettingsBase): """ 数据库中的课程考试设置模型 """ model_config = ConfigDict(from_attributes=True) id: int = Field(..., description="设置ID") course_id: int = Field(..., description="课程ID") created_at: datetime = Field(..., description="创建时间") updated_at: datetime = Field(..., description="更新时间") # 岗位分配相关Schema class CoursePositionAssignment(BaseModel): """ 课程岗位分配模型 """ position_id: int = Field(..., description="岗位ID") course_type: str = Field(default="required", pattern="^(required|optional)$", description="课程类型:required必修/optional选修") priority: int = Field(default=0, description="优先级/排序") class CoursePositionAssignmentInDB(CoursePositionAssignment): """ 数据库中的课程岗位分配模型 """ model_config = ConfigDict(from_attributes=True) id: int = Field(..., description="分配ID") course_id: int = Field(..., description="课程ID") position_name: Optional[str] = Field(None, description="岗位名称") position_description: Optional[str] = Field(None, description="岗位描述") member_count: Optional[int] = Field(None, description="岗位成员数")