feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
837
backend/app/services/course_service.py
Normal file
837
backend/app/services/course_service.py
Normal file
@@ -0,0 +1,837 @@
|
||||
"""
|
||||
课程服务层
|
||||
"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select, or_, and_, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.core.logger import get_logger
|
||||
from app.core.exceptions import NotFoundError, BadRequestError, ConflictError
|
||||
from app.models.course import (
|
||||
Course,
|
||||
CourseStatus,
|
||||
CourseMaterial,
|
||||
KnowledgePoint,
|
||||
GrowthPath,
|
||||
)
|
||||
from app.models.course_exam_settings import CourseExamSettings
|
||||
from app.models.position_member import PositionMember
|
||||
from app.models.position_course import PositionCourse
|
||||
from app.schemas.course import (
|
||||
CourseCreate,
|
||||
CourseUpdate,
|
||||
CourseList,
|
||||
CourseInDB,
|
||||
CourseMaterialCreate,
|
||||
KnowledgePointCreate,
|
||||
KnowledgePointUpdate,
|
||||
KnowledgePointInDB,
|
||||
GrowthPathCreate,
|
||||
)
|
||||
from app.schemas.base import PaginationParams, PaginatedResponse
|
||||
from app.services.base_service import BaseService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class CourseService(BaseService[Course]):
|
||||
"""
|
||||
课程服务类
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(Course)
|
||||
|
||||
async def get_course_list(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
page_params: PaginationParams,
|
||||
filters: CourseList,
|
||||
user_id: Optional[int] = None,
|
||||
) -> PaginatedResponse[CourseInDB]:
|
||||
"""
|
||||
获取课程列表(支持筛选)
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
page_params: 分页参数
|
||||
filters: 筛选条件
|
||||
user_id: 用户ID(用于记录访问日志)
|
||||
|
||||
Returns:
|
||||
分页的课程列表
|
||||
"""
|
||||
# 构建筛选条件
|
||||
filter_conditions = []
|
||||
|
||||
# 状态筛选(默认只显示已发布的课程)
|
||||
if filters.status is not None:
|
||||
filter_conditions.append(Course.status == filters.status)
|
||||
else:
|
||||
# 如果没有指定状态,默认只返回已发布的课程
|
||||
filter_conditions.append(Course.status == CourseStatus.PUBLISHED)
|
||||
|
||||
# 分类筛选
|
||||
if filters.category is not None:
|
||||
filter_conditions.append(Course.category == filters.category)
|
||||
|
||||
# 是否推荐筛选
|
||||
if filters.is_featured is not None:
|
||||
filter_conditions.append(Course.is_featured == filters.is_featured)
|
||||
|
||||
# 关键词搜索
|
||||
if filters.keyword:
|
||||
keyword = f"%{filters.keyword}%"
|
||||
filter_conditions.append(
|
||||
or_(Course.name.like(keyword), Course.description.like(keyword))
|
||||
)
|
||||
|
||||
# 记录查询日志
|
||||
logger.info(
|
||||
"查询课程列表",
|
||||
user_id=user_id,
|
||||
filters=filters.model_dump(exclude_none=True),
|
||||
page=page_params.page,
|
||||
size=page_params.page_size,
|
||||
)
|
||||
|
||||
# 执行分页查询
|
||||
query = select(Course).where(Course.is_deleted == False)
|
||||
|
||||
# 添加筛选条件
|
||||
if filter_conditions:
|
||||
query = query.where(and_(*filter_conditions))
|
||||
|
||||
# 添加排序:优先按sort_order升序,其次按创建时间降序(新课程优先)
|
||||
query = query.order_by(Course.sort_order.asc(), Course.created_at.desc())
|
||||
|
||||
# 获取总数
|
||||
count_query = (
|
||||
select(func.count()).select_from(Course).where(Course.is_deleted == False)
|
||||
)
|
||||
if filter_conditions:
|
||||
count_query = count_query.where(and_(*filter_conditions))
|
||||
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# 分页
|
||||
query = query.offset(page_params.offset).limit(page_params.limit)
|
||||
query = query.options(selectinload(Course.materials))
|
||||
|
||||
# 执行查询
|
||||
result = await db.execute(query)
|
||||
courses = result.scalars().all()
|
||||
|
||||
# 获取用户所属的岗位ID列表
|
||||
user_position_ids = []
|
||||
if user_id:
|
||||
position_result = await db.execute(
|
||||
select(PositionMember.position_id).where(
|
||||
PositionMember.user_id == user_id,
|
||||
PositionMember.is_deleted == False
|
||||
)
|
||||
)
|
||||
user_position_ids = [row[0] for row in position_result.fetchall()]
|
||||
|
||||
# 批量查询课程的岗位分配信息
|
||||
course_ids = [c.id for c in courses]
|
||||
course_type_map = {}
|
||||
if course_ids and user_position_ids:
|
||||
position_course_result = await db.execute(
|
||||
select(PositionCourse.course_id, PositionCourse.course_type).where(
|
||||
PositionCourse.course_id.in_(course_ids),
|
||||
PositionCourse.position_id.in_(user_position_ids),
|
||||
PositionCourse.is_deleted == False
|
||||
)
|
||||
)
|
||||
# 构建课程类型映射:如果有多个岗位,优先取required
|
||||
for course_id, course_type in position_course_result.fetchall():
|
||||
if course_id not in course_type_map:
|
||||
course_type_map[course_id] = course_type
|
||||
elif course_type == 'required':
|
||||
course_type_map[course_id] = 'required'
|
||||
|
||||
# 转换为 Pydantic 模型,并附加课程类型
|
||||
course_list = []
|
||||
for course in courses:
|
||||
course_data = CourseInDB.model_validate(course)
|
||||
# 设置课程类型:如果用户有岗位分配则使用分配类型,否则为None
|
||||
course_data.course_type = course_type_map.get(course.id)
|
||||
course_list.append(course_data)
|
||||
|
||||
# 计算总页数
|
||||
pages = (total + page_params.page_size - 1) // page_params.page_size
|
||||
|
||||
return PaginatedResponse(
|
||||
items=course_list,
|
||||
total=total,
|
||||
page=page_params.page,
|
||||
page_size=page_params.page_size,
|
||||
pages=pages,
|
||||
)
|
||||
|
||||
async def create_course(
|
||||
self, db: AsyncSession, *, course_in: CourseCreate, created_by: int
|
||||
) -> Course:
|
||||
"""
|
||||
创建课程
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
course_in: 课程创建数据
|
||||
created_by: 创建人ID
|
||||
|
||||
Returns:
|
||||
创建的课程
|
||||
"""
|
||||
# 检查名称是否重复
|
||||
existing = await db.execute(
|
||||
select(Course).where(
|
||||
and_(Course.name == course_in.name, Course.is_deleted == False)
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise ConflictError(f"课程名称 '{course_in.name}' 已存在")
|
||||
|
||||
# 创建课程
|
||||
course_data = course_in.model_dump()
|
||||
course = await self.create(db, obj_in=course_data, created_by=created_by)
|
||||
|
||||
# 自动创建默认考试设置
|
||||
default_exam_settings = CourseExamSettings(
|
||||
course_id=course.id,
|
||||
created_by=created_by,
|
||||
updated_by=created_by
|
||||
# 其他字段使用模型定义的默认值:
|
||||
# single_choice_count=4, multiple_choice_count=2, true_false_count=1,
|
||||
# fill_blank_count=2, essay_count=1, duration_minutes=10, 等
|
||||
)
|
||||
db.add(default_exam_settings)
|
||||
await db.commit()
|
||||
await db.refresh(course)
|
||||
|
||||
logger.info(
|
||||
"创建课程", course_id=course.id, course_name=course.name, created_by=created_by
|
||||
)
|
||||
logger.info(
|
||||
"自动创建默认考试设置", course_id=course.id, exam_settings_id=default_exam_settings.id
|
||||
)
|
||||
|
||||
return course
|
||||
|
||||
async def update_course(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
course_id: int,
|
||||
course_in: CourseUpdate,
|
||||
updated_by: int,
|
||||
) -> Course:
|
||||
"""
|
||||
更新课程
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
course_id: 课程ID
|
||||
course_in: 课程更新数据
|
||||
updated_by: 更新人ID
|
||||
|
||||
Returns:
|
||||
更新后的课程
|
||||
"""
|
||||
# 获取课程
|
||||
course = await self.get_by_id(db, course_id)
|
||||
if not course:
|
||||
raise NotFoundError(f"课程ID {course_id} 不存在")
|
||||
|
||||
# 检查名称是否重复(如果修改了名称)
|
||||
if course_in.name and course_in.name != course.name:
|
||||
existing = await db.execute(
|
||||
select(Course).where(
|
||||
and_(
|
||||
Course.name == course_in.name,
|
||||
Course.id != course_id,
|
||||
Course.is_deleted == False,
|
||||
)
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise ConflictError(f"课程名称 '{course_in.name}' 已存在")
|
||||
|
||||
# 记录状态变更
|
||||
old_status = course.status
|
||||
|
||||
# 更新课程
|
||||
update_data = course_in.model_dump(exclude_unset=True)
|
||||
|
||||
# 如果状态变为已发布,记录发布时间
|
||||
if (
|
||||
update_data.get("status") == CourseStatus.PUBLISHED
|
||||
and old_status != CourseStatus.PUBLISHED
|
||||
):
|
||||
update_data["published_at"] = datetime.now()
|
||||
update_data["publisher_id"] = updated_by
|
||||
|
||||
course = await self.update(
|
||||
db, db_obj=course, obj_in=update_data, updated_by=updated_by
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"更新课程",
|
||||
course_id=course.id,
|
||||
course_name=course.name,
|
||||
old_status=old_status,
|
||||
new_status=course.status,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
|
||||
return course
|
||||
|
||||
async def delete_course(
|
||||
self, db: AsyncSession, *, course_id: int, deleted_by: int
|
||||
) -> bool:
|
||||
"""
|
||||
删除课程(软删除 + 删除相关文件)
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
course_id: 课程ID
|
||||
deleted_by: 删除人ID
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from app.core.config import settings
|
||||
|
||||
course = await self.get_by_id(db, course_id)
|
||||
if not course:
|
||||
raise NotFoundError(f"课程ID {course_id} 不存在")
|
||||
|
||||
# 放开删除限制:任意状态均可软删除,由业务方自行控制
|
||||
|
||||
# 执行软删除(标记 is_deleted,记录删除时间),由审计日志记录操作者
|
||||
success = await self.soft_delete(db, id=course_id)
|
||||
|
||||
if success:
|
||||
# 删除课程文件夹及其所有内容
|
||||
course_folder = Path(settings.UPLOAD_PATH) / "courses" / str(course_id)
|
||||
if course_folder.exists() and course_folder.is_dir():
|
||||
try:
|
||||
shutil.rmtree(course_folder)
|
||||
logger.info(
|
||||
"删除课程文件夹成功",
|
||||
course_id=course_id,
|
||||
folder_path=str(course_folder),
|
||||
)
|
||||
except Exception as e:
|
||||
# 文件夹删除失败不影响业务流程,仅记录日志
|
||||
logger.error(
|
||||
"删除课程文件夹失败",
|
||||
course_id=course_id,
|
||||
folder_path=str(course_folder),
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
logger.warning(
|
||||
"删除课程",
|
||||
course_id=course_id,
|
||||
course_name=course.name,
|
||||
deleted_by=deleted_by,
|
||||
folder_deleted=course_folder.exists(),
|
||||
)
|
||||
|
||||
return success
|
||||
|
||||
async def add_course_material(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
course_id: int,
|
||||
material_in: CourseMaterialCreate,
|
||||
created_by: int,
|
||||
) -> CourseMaterial:
|
||||
"""
|
||||
添加课程资料
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
course_id: 课程ID
|
||||
material_in: 资料创建数据
|
||||
created_by: 创建人ID
|
||||
|
||||
Returns:
|
||||
创建的课程资料
|
||||
"""
|
||||
# 检查课程是否存在
|
||||
course = await self.get_by_id(db, course_id)
|
||||
if not course:
|
||||
raise NotFoundError(f"课程ID {course_id} 不存在")
|
||||
|
||||
# 创建资料
|
||||
material_data = material_in.model_dump()
|
||||
material_data.update({
|
||||
"course_id": course_id,
|
||||
"created_by": created_by,
|
||||
"updated_by": created_by
|
||||
})
|
||||
|
||||
material = CourseMaterial(**material_data)
|
||||
db.add(material)
|
||||
await db.commit()
|
||||
await db.refresh(material)
|
||||
|
||||
logger.info(
|
||||
"添加课程资料",
|
||||
course_id=course_id,
|
||||
material_id=material.id,
|
||||
material_name=material.name,
|
||||
file_type=material.file_type,
|
||||
file_size=material.file_size,
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
return material
|
||||
|
||||
async def get_course_materials(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
course_id: int,
|
||||
) -> List[CourseMaterial]:
|
||||
"""
|
||||
获取课程资料列表
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
course_id: 课程ID
|
||||
|
||||
Returns:
|
||||
课程资料列表
|
||||
"""
|
||||
# 确认课程存在
|
||||
course = await self.get_by_id(db, course_id)
|
||||
if not course:
|
||||
raise NotFoundError(f"课程ID {course_id} 不存在")
|
||||
|
||||
stmt = (
|
||||
select(CourseMaterial)
|
||||
.where(
|
||||
CourseMaterial.course_id == course_id,
|
||||
CourseMaterial.is_deleted == False,
|
||||
)
|
||||
.order_by(CourseMaterial.sort_order.asc(), CourseMaterial.id.asc())
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
materials = result.scalars().all()
|
||||
|
||||
logger.info(
|
||||
"查询课程资料列表", course_id=course_id, count=len(materials)
|
||||
)
|
||||
|
||||
return materials
|
||||
|
||||
async def delete_course_material(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
course_id: int,
|
||||
material_id: int,
|
||||
deleted_by: int,
|
||||
) -> bool:
|
||||
"""
|
||||
删除课程资料(软删除 + 删除物理文件)
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
course_id: 课程ID
|
||||
material_id: 资料ID
|
||||
deleted_by: 删除人ID
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from app.core.config import settings
|
||||
|
||||
# 先确认课程存在
|
||||
course = await self.get_by_id(db, course_id)
|
||||
if not course:
|
||||
raise NotFoundError(f"课程ID {course_id} 不存在")
|
||||
|
||||
# 查找资料并校验归属
|
||||
material_stmt = select(CourseMaterial).where(
|
||||
CourseMaterial.id == material_id,
|
||||
CourseMaterial.course_id == course_id,
|
||||
CourseMaterial.is_deleted == False,
|
||||
)
|
||||
result = await db.execute(material_stmt)
|
||||
material = result.scalar_one_or_none()
|
||||
if not material:
|
||||
raise NotFoundError(f"课程资料ID {material_id} 不存在或已删除")
|
||||
|
||||
# 获取文件路径信息用于删除物理文件
|
||||
file_url = material.file_url
|
||||
|
||||
# 软删除数据库记录
|
||||
material.is_deleted = True
|
||||
material.deleted_at = datetime.now()
|
||||
if hasattr(material, "deleted_by"):
|
||||
# 兼容存在该字段的表
|
||||
setattr(material, "deleted_by", deleted_by)
|
||||
|
||||
db.add(material)
|
||||
await db.commit()
|
||||
|
||||
# 删除物理文件
|
||||
if file_url and file_url.startswith("/static/uploads/"):
|
||||
try:
|
||||
# 从URL中提取相对路径
|
||||
relative_path = file_url.replace("/static/uploads/", "")
|
||||
file_path = Path(settings.UPLOAD_PATH) / relative_path
|
||||
|
||||
# 检查文件是否存在并删除
|
||||
if file_path.exists() and file_path.is_file():
|
||||
os.remove(file_path)
|
||||
logger.info(
|
||||
"删除物理文件成功",
|
||||
file_path=str(file_path),
|
||||
material_id=material_id,
|
||||
)
|
||||
except Exception as e:
|
||||
# 物理文件删除失败不影响业务流程,仅记录日志
|
||||
logger.error(
|
||||
"删除物理文件失败",
|
||||
file_url=file_url,
|
||||
material_id=material_id,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
logger.warning(
|
||||
"删除课程资料",
|
||||
course_id=course_id,
|
||||
material_id=material_id,
|
||||
deleted_by=deleted_by,
|
||||
file_deleted=file_url is not None,
|
||||
)
|
||||
return True
|
||||
|
||||
async def get_material_knowledge_points(
|
||||
self, db: AsyncSession, material_id: int
|
||||
) -> List[KnowledgePointInDB]:
|
||||
"""获取资料关联的知识点列表"""
|
||||
|
||||
# 获取资料信息
|
||||
result = await db.execute(
|
||||
select(CourseMaterial).where(
|
||||
CourseMaterial.id == material_id,
|
||||
CourseMaterial.is_deleted == False
|
||||
)
|
||||
)
|
||||
material = result.scalar_one_or_none()
|
||||
|
||||
if not material:
|
||||
raise NotFoundError(f"资料ID {material_id} 不存在")
|
||||
|
||||
# 直接查询关联到该资料的知识点
|
||||
query = select(KnowledgePoint).where(
|
||||
KnowledgePoint.material_id == material_id,
|
||||
KnowledgePoint.is_deleted == False
|
||||
).order_by(KnowledgePoint.created_at.desc())
|
||||
|
||||
result = await db.execute(query)
|
||||
knowledge_points = result.scalars().all()
|
||||
|
||||
from app.schemas.course import KnowledgePointInDB
|
||||
return [KnowledgePointInDB.model_validate(kp) for kp in knowledge_points]
|
||||
|
||||
async def add_material_knowledge_points(
|
||||
self, db: AsyncSession, material_id: int, knowledge_point_ids: List[int]
|
||||
) -> List[KnowledgePointInDB]:
|
||||
"""
|
||||
为资料添加知识点关联
|
||||
|
||||
注意:自2025-09-27起,知识点直接通过material_id关联到资料,
|
||||
material_knowledge_points中间表已废弃。此方法将更新知识点的material_id字段。
|
||||
"""
|
||||
# 验证资料是否存在
|
||||
result = await db.execute(
|
||||
select(CourseMaterial).where(
|
||||
CourseMaterial.id == material_id,
|
||||
CourseMaterial.is_deleted == False
|
||||
)
|
||||
)
|
||||
material = result.scalar_one_or_none()
|
||||
|
||||
if not material:
|
||||
raise NotFoundError(f"资料ID {material_id} 不存在")
|
||||
|
||||
# 验证知识点是否存在且属于同一课程
|
||||
result = await db.execute(
|
||||
select(KnowledgePoint).where(
|
||||
KnowledgePoint.id.in_(knowledge_point_ids),
|
||||
KnowledgePoint.course_id == material.course_id,
|
||||
KnowledgePoint.is_deleted == False
|
||||
)
|
||||
)
|
||||
valid_knowledge_points = result.scalars().all()
|
||||
|
||||
if len(valid_knowledge_points) != len(knowledge_point_ids):
|
||||
raise BadRequestError("部分知识点不存在或不属于同一课程")
|
||||
|
||||
# 更新知识点的material_id字段
|
||||
added_knowledge_points = []
|
||||
for kp in valid_knowledge_points:
|
||||
# 更新知识点的资料关联
|
||||
kp.material_id = material_id
|
||||
added_knowledge_points.append(kp)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 刷新对象以获取更新后的数据
|
||||
for kp in added_knowledge_points:
|
||||
await db.refresh(kp)
|
||||
|
||||
from app.schemas.course import KnowledgePointInDB
|
||||
return [KnowledgePointInDB.model_validate(kp) for kp in added_knowledge_points]
|
||||
|
||||
async def remove_material_knowledge_point(
|
||||
self, db: AsyncSession, material_id: int, knowledge_point_id: int
|
||||
) -> bool:
|
||||
"""
|
||||
移除资料的知识点关联(软删除知识点)
|
||||
|
||||
注意:自2025-09-27起,知识点直接通过material_id关联到资料,
|
||||
material_knowledge_points中间表已废弃。此方法将软删除知识点。
|
||||
"""
|
||||
# 查找知识点并验证归属
|
||||
result = await db.execute(
|
||||
select(KnowledgePoint).where(
|
||||
KnowledgePoint.id == knowledge_point_id,
|
||||
KnowledgePoint.material_id == material_id,
|
||||
KnowledgePoint.is_deleted == False
|
||||
)
|
||||
)
|
||||
knowledge_point = result.scalar_one_or_none()
|
||||
|
||||
if not knowledge_point:
|
||||
raise NotFoundError(f"知识点ID {knowledge_point_id} 不存在或不属于该资料")
|
||||
|
||||
# 软删除知识点
|
||||
knowledge_point.is_deleted = True
|
||||
knowledge_point.deleted_at = datetime.now()
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
"移除资料知识点关联",
|
||||
material_id=material_id,
|
||||
knowledge_point_id=knowledge_point_id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class KnowledgePointService(BaseService[KnowledgePoint]):
|
||||
"""
|
||||
知识点服务类
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(KnowledgePoint)
|
||||
|
||||
async def get_knowledge_points_by_course(
|
||||
self, db: AsyncSession, *, course_id: int, material_id: Optional[int] = None
|
||||
) -> List[KnowledgePoint]:
|
||||
"""
|
||||
获取课程的知识点列表
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
course_id: 课程ID
|
||||
material_id: 资料ID(可选,用于筛选特定资料的知识点)
|
||||
|
||||
Returns:
|
||||
知识点列表
|
||||
"""
|
||||
query = select(KnowledgePoint).where(
|
||||
and_(
|
||||
KnowledgePoint.course_id == course_id,
|
||||
KnowledgePoint.is_deleted == False,
|
||||
)
|
||||
)
|
||||
|
||||
if material_id is not None:
|
||||
query = query.where(KnowledgePoint.material_id == material_id)
|
||||
|
||||
query = query.order_by(KnowledgePoint.created_at.desc())
|
||||
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def create_knowledge_point(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
course_id: int,
|
||||
point_in: KnowledgePointCreate,
|
||||
created_by: int,
|
||||
) -> KnowledgePoint:
|
||||
"""
|
||||
创建知识点
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
course_id: 课程ID
|
||||
point_in: 知识点创建数据
|
||||
created_by: 创建人ID
|
||||
|
||||
Returns:
|
||||
创建的知识点
|
||||
"""
|
||||
# 检查课程是否存在
|
||||
course_service = CourseService()
|
||||
course = await course_service.get_by_id(db, course_id)
|
||||
if not course:
|
||||
raise NotFoundError(f"课程ID {course_id} 不存在")
|
||||
|
||||
# 创建知识点
|
||||
point_data = point_in.model_dump()
|
||||
point_data.update({"course_id": course_id})
|
||||
|
||||
knowledge_point = await self.create(
|
||||
db, obj_in=point_data, created_by=created_by
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"创建知识点",
|
||||
course_id=course_id,
|
||||
knowledge_point_id=knowledge_point.id,
|
||||
knowledge_point_name=knowledge_point.name,
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
return knowledge_point
|
||||
|
||||
async def update_knowledge_point(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
point_id: int,
|
||||
point_in: KnowledgePointUpdate,
|
||||
updated_by: int,
|
||||
) -> KnowledgePoint:
|
||||
"""
|
||||
更新知识点
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
point_id: 知识点ID
|
||||
point_in: 知识点更新数据
|
||||
updated_by: 更新人ID
|
||||
|
||||
Returns:
|
||||
更新后的知识点
|
||||
"""
|
||||
knowledge_point = await self.get_by_id(db, point_id)
|
||||
if not knowledge_point:
|
||||
raise NotFoundError(f"知识点ID {point_id} 不存在")
|
||||
|
||||
# 验证关联资料是否存在
|
||||
if hasattr(point_in, 'material_id') and point_in.material_id:
|
||||
result = await db.execute(
|
||||
select(CourseMaterial).where(
|
||||
CourseMaterial.id == point_in.material_id,
|
||||
CourseMaterial.is_deleted == False
|
||||
)
|
||||
)
|
||||
material = result.scalar_one_or_none()
|
||||
if not material:
|
||||
raise NotFoundError(f"资料ID {point_in.material_id} 不存在")
|
||||
|
||||
# 更新知识点
|
||||
update_data = point_in.model_dump(exclude_unset=True)
|
||||
knowledge_point = await self.update(
|
||||
db, db_obj=knowledge_point, obj_in=update_data, updated_by=updated_by
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"更新知识点",
|
||||
knowledge_point_id=knowledge_point.id,
|
||||
knowledge_point_name=knowledge_point.name,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
|
||||
return knowledge_point
|
||||
|
||||
|
||||
class GrowthPathService(BaseService[GrowthPath]):
|
||||
"""
|
||||
成长路径服务类
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(GrowthPath)
|
||||
|
||||
async def create_growth_path(
|
||||
self, db: AsyncSession, *, path_in: GrowthPathCreate, created_by: int
|
||||
) -> GrowthPath:
|
||||
"""
|
||||
创建成长路径
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
path_in: 成长路径创建数据
|
||||
created_by: 创建人ID
|
||||
|
||||
Returns:
|
||||
创建的成长路径
|
||||
"""
|
||||
# 检查名称是否重复
|
||||
existing = await db.execute(
|
||||
select(GrowthPath).where(
|
||||
and_(GrowthPath.name == path_in.name, GrowthPath.is_deleted == False)
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise ConflictError(f"成长路径名称 '{path_in.name}' 已存在")
|
||||
|
||||
# 验证课程是否存在
|
||||
if path_in.courses:
|
||||
course_ids = [c.course_id for c in path_in.courses]
|
||||
course_service = CourseService()
|
||||
for course_id in course_ids:
|
||||
course = await course_service.get_by_id(db, course_id)
|
||||
if not course:
|
||||
raise NotFoundError(f"课程ID {course_id} 不存在")
|
||||
|
||||
# 创建成长路径
|
||||
path_data = path_in.model_dump()
|
||||
# 转换课程列表为JSON格式
|
||||
if path_data.get("courses"):
|
||||
path_data["courses"] = [c.model_dump() for c in path_in.courses]
|
||||
|
||||
growth_path = await self.create(db, obj_in=path_data, created_by=created_by)
|
||||
|
||||
logger.info(
|
||||
"创建成长路径",
|
||||
growth_path_id=growth_path.id,
|
||||
growth_path_name=growth_path.name,
|
||||
course_count=len(path_in.courses) if path_in.courses else 0,
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
return growth_path
|
||||
|
||||
|
||||
# 创建服务实例
|
||||
course_service = CourseService()
|
||||
knowledge_point_service = KnowledgePointService()
|
||||
growth_path_service = GrowthPathService()
|
||||
Reference in New Issue
Block a user