feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View File

@@ -0,0 +1,364 @@
"""
课程相关的数据验证模型
"""
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="岗位成员数")