- 新增数据库表: growth_path_nodes, user_growth_path_progress, user_node_completions - 新增 Model: GrowthPathNode, UserGrowthPathProgress, UserNodeCompletion - 新增 Service: GrowthPathService(管理端CRUD、学员端进度追踪) - 新增 API: 学员端获取成长路径、管理端CRUD - 前端学员端从API动态加载成长路径数据 - 更新管理端API接口定义
This commit is contained in:
743
backend/app/services/growth_path_service.py
Normal file
743
backend/app/services/growth_path_service.py
Normal file
@@ -0,0 +1,743 @@
|
||||
"""
|
||||
成长路径服务
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import select, func, and_, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.course import GrowthPath, Course
|
||||
from app.models.growth_path import (
|
||||
GrowthPathNode,
|
||||
UserGrowthPathProgress,
|
||||
UserNodeCompletion,
|
||||
GrowthPathStatus,
|
||||
NodeStatus,
|
||||
)
|
||||
from app.models.user_course_progress import UserCourseProgress, ProgressStatus
|
||||
from app.models.position import Position
|
||||
from app.schemas.growth_path import (
|
||||
GrowthPathCreate,
|
||||
GrowthPathUpdate,
|
||||
GrowthPathNodeCreate,
|
||||
TraineeGrowthPathResponse,
|
||||
TraineeStageResponse,
|
||||
TraineeNodeResponse,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GrowthPathService:
|
||||
"""成长路径服务"""
|
||||
|
||||
# =====================================================
|
||||
# 管理端 - CRUD
|
||||
# =====================================================
|
||||
|
||||
async def create_growth_path(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
data: GrowthPathCreate,
|
||||
created_by: int
|
||||
) -> GrowthPath:
|
||||
"""创建成长路径"""
|
||||
# 检查名称是否重复
|
||||
existing = await db.execute(
|
||||
select(GrowthPath).where(
|
||||
and_(
|
||||
GrowthPath.name == data.name,
|
||||
GrowthPath.is_deleted == False
|
||||
)
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise ValueError(f"成长路径名称 '{data.name}' 已存在")
|
||||
|
||||
# 创建成长路径
|
||||
growth_path = GrowthPath(
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
target_role=data.target_role,
|
||||
position_id=data.position_id,
|
||||
stages=[s.model_dump() for s in data.stages] if data.stages else None,
|
||||
estimated_duration_days=data.estimated_duration_days,
|
||||
is_active=data.is_active,
|
||||
sort_order=data.sort_order,
|
||||
)
|
||||
db.add(growth_path)
|
||||
await db.flush()
|
||||
|
||||
# 创建节点
|
||||
if data.nodes:
|
||||
for node_data in data.nodes:
|
||||
node = GrowthPathNode(
|
||||
growth_path_id=growth_path.id,
|
||||
course_id=node_data.course_id,
|
||||
stage_name=node_data.stage_name,
|
||||
title=node_data.title,
|
||||
description=node_data.description,
|
||||
order_num=node_data.order_num,
|
||||
is_required=node_data.is_required,
|
||||
prerequisites=node_data.prerequisites,
|
||||
estimated_days=node_data.estimated_days,
|
||||
)
|
||||
db.add(node)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(growth_path)
|
||||
|
||||
logger.info(f"创建成长路径: {growth_path.name}, ID: {growth_path.id}")
|
||||
return growth_path
|
||||
|
||||
async def update_growth_path(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
path_id: int,
|
||||
data: GrowthPathUpdate
|
||||
) -> GrowthPath:
|
||||
"""更新成长路径"""
|
||||
growth_path = await db.get(GrowthPath, path_id)
|
||||
if not growth_path or growth_path.is_deleted:
|
||||
raise ValueError("成长路径不存在")
|
||||
|
||||
# 更新基本信息
|
||||
update_data = data.model_dump(exclude_unset=True, exclude={'nodes'})
|
||||
if 'stages' in update_data and update_data['stages']:
|
||||
update_data['stages'] = [s.model_dump() if hasattr(s, 'model_dump') else s for s in update_data['stages']]
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(growth_path, key, value)
|
||||
|
||||
# 如果提供了节点,整体替换
|
||||
if data.nodes is not None:
|
||||
# 删除旧节点
|
||||
await db.execute(
|
||||
delete(GrowthPathNode).where(GrowthPathNode.growth_path_id == path_id)
|
||||
)
|
||||
# 创建新节点
|
||||
for node_data in data.nodes:
|
||||
node = GrowthPathNode(
|
||||
growth_path_id=path_id,
|
||||
course_id=node_data.course_id,
|
||||
stage_name=node_data.stage_name,
|
||||
title=node_data.title,
|
||||
description=node_data.description,
|
||||
order_num=node_data.order_num,
|
||||
is_required=node_data.is_required,
|
||||
prerequisites=node_data.prerequisites,
|
||||
estimated_days=node_data.estimated_days,
|
||||
)
|
||||
db.add(node)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(growth_path)
|
||||
|
||||
logger.info(f"更新成长路径: {growth_path.name}, ID: {path_id}")
|
||||
return growth_path
|
||||
|
||||
async def delete_growth_path(self, db: AsyncSession, path_id: int) -> bool:
|
||||
"""删除成长路径(软删除)"""
|
||||
growth_path = await db.get(GrowthPath, path_id)
|
||||
if not growth_path or growth_path.is_deleted:
|
||||
raise ValueError("成长路径不存在")
|
||||
|
||||
growth_path.is_deleted = True
|
||||
growth_path.deleted_at = datetime.now()
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"删除成长路径: {growth_path.name}, ID: {path_id}")
|
||||
return True
|
||||
|
||||
async def get_growth_path(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
path_id: int
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""获取成长路径详情"""
|
||||
result = await db.execute(
|
||||
select(GrowthPath)
|
||||
.options(selectinload(GrowthPath.nodes))
|
||||
.where(
|
||||
and_(
|
||||
GrowthPath.id == path_id,
|
||||
GrowthPath.is_deleted == False
|
||||
)
|
||||
)
|
||||
)
|
||||
growth_path = result.scalar_one_or_none()
|
||||
if not growth_path:
|
||||
return None
|
||||
|
||||
# 获取岗位名称
|
||||
position_name = None
|
||||
if growth_path.position_id:
|
||||
position = await db.get(Position, growth_path.position_id)
|
||||
if position:
|
||||
position_name = position.name
|
||||
|
||||
# 获取课程名称
|
||||
nodes_data = []
|
||||
for node in sorted(growth_path.nodes, key=lambda x: x.order_num):
|
||||
if node.is_deleted:
|
||||
continue
|
||||
course = await db.get(Course, node.course_id)
|
||||
nodes_data.append({
|
||||
"id": node.id,
|
||||
"growth_path_id": node.growth_path_id,
|
||||
"course_id": node.course_id,
|
||||
"course_name": course.name if course else None,
|
||||
"stage_name": node.stage_name,
|
||||
"title": node.title,
|
||||
"description": node.description,
|
||||
"order_num": node.order_num,
|
||||
"is_required": node.is_required,
|
||||
"prerequisites": node.prerequisites,
|
||||
"estimated_days": node.estimated_days,
|
||||
"created_at": node.created_at,
|
||||
"updated_at": node.updated_at,
|
||||
})
|
||||
|
||||
return {
|
||||
"id": growth_path.id,
|
||||
"name": growth_path.name,
|
||||
"description": growth_path.description,
|
||||
"target_role": growth_path.target_role,
|
||||
"position_id": growth_path.position_id,
|
||||
"position_name": position_name,
|
||||
"stages": growth_path.stages,
|
||||
"estimated_duration_days": growth_path.estimated_duration_days,
|
||||
"is_active": growth_path.is_active,
|
||||
"sort_order": growth_path.sort_order,
|
||||
"nodes": nodes_data,
|
||||
"node_count": len(nodes_data),
|
||||
"created_at": growth_path.created_at,
|
||||
"updated_at": growth_path.updated_at,
|
||||
}
|
||||
|
||||
async def list_growth_paths(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
position_id: Optional[int] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Dict[str, Any]:
|
||||
"""获取成长路径列表"""
|
||||
query = select(GrowthPath).where(GrowthPath.is_deleted == False)
|
||||
|
||||
if position_id is not None:
|
||||
query = query.where(GrowthPath.position_id == position_id)
|
||||
if is_active is not None:
|
||||
query = query.where(GrowthPath.is_active == is_active)
|
||||
|
||||
# 计算总数
|
||||
count_result = await db.execute(
|
||||
select(func.count(GrowthPath.id)).where(GrowthPath.is_deleted == False)
|
||||
)
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
# 分页
|
||||
query = query.order_by(GrowthPath.sort_order, GrowthPath.id.desc())
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
result = await db.execute(query)
|
||||
paths = result.scalars().all()
|
||||
|
||||
items = []
|
||||
for path in paths:
|
||||
# 获取岗位名称
|
||||
position_name = None
|
||||
if path.position_id:
|
||||
position = await db.get(Position, path.position_id)
|
||||
if position:
|
||||
position_name = position.name
|
||||
|
||||
# 获取节点数量
|
||||
node_count_result = await db.execute(
|
||||
select(func.count(GrowthPathNode.id)).where(
|
||||
and_(
|
||||
GrowthPathNode.growth_path_id == path.id,
|
||||
GrowthPathNode.is_deleted == False
|
||||
)
|
||||
)
|
||||
)
|
||||
node_count = node_count_result.scalar() or 0
|
||||
|
||||
items.append({
|
||||
"id": path.id,
|
||||
"name": path.name,
|
||||
"description": path.description,
|
||||
"position_id": path.position_id,
|
||||
"position_name": position_name,
|
||||
"is_active": path.is_active,
|
||||
"node_count": node_count,
|
||||
"estimated_duration_days": path.estimated_duration_days,
|
||||
"created_at": path.created_at,
|
||||
})
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
# =====================================================
|
||||
# 学员端 - 获取成长路径
|
||||
# =====================================================
|
||||
|
||||
async def get_trainee_growth_path(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
position_id: Optional[int] = None
|
||||
) -> Optional[TraineeGrowthPathResponse]:
|
||||
"""
|
||||
获取学员的成长路径(含进度)
|
||||
如果指定了岗位ID,返回该岗位的成长路径
|
||||
否则根据用户岗位自动匹配
|
||||
"""
|
||||
# 查找成长路径
|
||||
query = select(GrowthPath).where(
|
||||
and_(
|
||||
GrowthPath.is_deleted == False,
|
||||
GrowthPath.is_active == True
|
||||
)
|
||||
)
|
||||
if position_id:
|
||||
query = query.where(GrowthPath.position_id == position_id)
|
||||
|
||||
query = query.order_by(GrowthPath.sort_order).limit(1)
|
||||
result = await db.execute(query)
|
||||
growth_path = result.scalar_one_or_none()
|
||||
|
||||
if not growth_path:
|
||||
return None
|
||||
|
||||
# 获取岗位名称
|
||||
position_name = None
|
||||
if growth_path.position_id:
|
||||
position = await db.get(Position, growth_path.position_id)
|
||||
if position:
|
||||
position_name = position.name
|
||||
|
||||
# 获取所有节点
|
||||
nodes_result = await db.execute(
|
||||
select(GrowthPathNode).where(
|
||||
and_(
|
||||
GrowthPathNode.growth_path_id == growth_path.id,
|
||||
GrowthPathNode.is_deleted == False
|
||||
)
|
||||
).order_by(GrowthPathNode.order_num)
|
||||
)
|
||||
nodes = nodes_result.scalars().all()
|
||||
|
||||
# 获取用户进度
|
||||
progress_result = await db.execute(
|
||||
select(UserGrowthPathProgress).where(
|
||||
and_(
|
||||
UserGrowthPathProgress.user_id == user_id,
|
||||
UserGrowthPathProgress.growth_path_id == growth_path.id
|
||||
)
|
||||
)
|
||||
)
|
||||
user_progress = progress_result.scalar_one_or_none()
|
||||
|
||||
# 获取用户节点完成情况
|
||||
completions_result = await db.execute(
|
||||
select(UserNodeCompletion).where(
|
||||
and_(
|
||||
UserNodeCompletion.user_id == user_id,
|
||||
UserNodeCompletion.growth_path_id == growth_path.id
|
||||
)
|
||||
)
|
||||
)
|
||||
completions = {c.node_id: c for c in completions_result.scalars().all()}
|
||||
|
||||
# 获取用户课程学习进度
|
||||
course_ids = [n.course_id for n in nodes]
|
||||
course_progress_result = await db.execute(
|
||||
select(UserCourseProgress).where(
|
||||
and_(
|
||||
UserCourseProgress.user_id == user_id,
|
||||
UserCourseProgress.course_id.in_(course_ids)
|
||||
)
|
||||
)
|
||||
)
|
||||
course_progress_map = {cp.course_id: cp for cp in course_progress_result.scalars().all()}
|
||||
|
||||
# 构建节点响应
|
||||
completed_node_ids = set(user_progress.completed_node_ids or []) if user_progress else set()
|
||||
nodes_by_stage: Dict[str, List[TraineeNodeResponse]] = {}
|
||||
|
||||
for node in nodes:
|
||||
# 获取课程信息
|
||||
course = await db.get(Course, node.course_id)
|
||||
|
||||
# 计算节点状态
|
||||
node_status = self._calculate_node_status(
|
||||
node=node,
|
||||
completed_node_ids=completed_node_ids,
|
||||
completions=completions,
|
||||
course_progress_map=course_progress_map
|
||||
)
|
||||
|
||||
# 获取课程进度
|
||||
course_prog = course_progress_map.get(node.course_id)
|
||||
progress = float(course_prog.progress) if course_prog else 0
|
||||
|
||||
node_response = TraineeNodeResponse(
|
||||
id=node.id,
|
||||
course_id=node.course_id,
|
||||
title=node.title,
|
||||
description=node.description,
|
||||
stage_name=node.stage_name,
|
||||
is_required=node.is_required,
|
||||
estimated_days=node.estimated_days,
|
||||
order_num=node.order_num,
|
||||
status=node_status,
|
||||
progress=progress,
|
||||
course_name=course.name if course else None,
|
||||
course_cover=course.cover_image if course else None,
|
||||
)
|
||||
|
||||
stage_name = node.stage_name or "默认阶段"
|
||||
if stage_name not in nodes_by_stage:
|
||||
nodes_by_stage[stage_name] = []
|
||||
nodes_by_stage[stage_name].append(node_response)
|
||||
|
||||
# 构建阶段响应
|
||||
stages = []
|
||||
stage_configs = growth_path.stages or []
|
||||
stage_order = {s.get('name', ''): s.get('order', i) for i, s in enumerate(stage_configs)}
|
||||
|
||||
for stage_name, stage_nodes in sorted(nodes_by_stage.items(), key=lambda x: stage_order.get(x[0], 999)):
|
||||
completed_in_stage = sum(1 for n in stage_nodes if n.status == NodeStatus.COMPLETED.value)
|
||||
stage_desc = next((s.get('description') for s in stage_configs if s.get('name') == stage_name), None)
|
||||
|
||||
stages.append(TraineeStageResponse(
|
||||
name=stage_name,
|
||||
description=stage_desc,
|
||||
completed=completed_in_stage,
|
||||
total=len(stage_nodes),
|
||||
nodes=stage_nodes,
|
||||
))
|
||||
|
||||
# 计算总体进度
|
||||
total_nodes = len(nodes)
|
||||
completed_count = len(completed_node_ids)
|
||||
total_progress = (completed_count / total_nodes * 100) if total_nodes > 0 else 0
|
||||
|
||||
return TraineeGrowthPathResponse(
|
||||
id=growth_path.id,
|
||||
name=growth_path.name,
|
||||
description=growth_path.description,
|
||||
position_id=growth_path.position_id,
|
||||
position_name=position_name,
|
||||
total_progress=round(total_progress, 1),
|
||||
completed_nodes=completed_count,
|
||||
total_nodes=total_nodes,
|
||||
status=user_progress.status if user_progress else GrowthPathStatus.NOT_STARTED.value,
|
||||
started_at=user_progress.started_at if user_progress else None,
|
||||
estimated_completion_days=growth_path.estimated_duration_days,
|
||||
stages=stages,
|
||||
)
|
||||
|
||||
def _calculate_node_status(
|
||||
self,
|
||||
node: GrowthPathNode,
|
||||
completed_node_ids: set,
|
||||
completions: Dict[int, UserNodeCompletion],
|
||||
course_progress_map: Dict[int, UserCourseProgress]
|
||||
) -> str:
|
||||
"""计算节点状态"""
|
||||
# 已完成
|
||||
if node.id in completed_node_ids:
|
||||
return NodeStatus.COMPLETED.value
|
||||
|
||||
# 检查前置节点
|
||||
prerequisites = node.prerequisites or []
|
||||
if prerequisites:
|
||||
for prereq_id in prerequisites:
|
||||
if prereq_id not in completed_node_ids:
|
||||
return NodeStatus.LOCKED.value
|
||||
|
||||
# 检查用户节点记录
|
||||
completion = completions.get(node.id)
|
||||
if completion:
|
||||
if completion.status == NodeStatus.IN_PROGRESS.value:
|
||||
return NodeStatus.IN_PROGRESS.value
|
||||
if completion.status == NodeStatus.COMPLETED.value:
|
||||
return NodeStatus.COMPLETED.value
|
||||
|
||||
# 检查课程进度
|
||||
course_progress = course_progress_map.get(node.course_id)
|
||||
if course_progress:
|
||||
if course_progress.status == ProgressStatus.COMPLETED.value:
|
||||
return NodeStatus.COMPLETED.value
|
||||
if course_progress.progress > 0:
|
||||
return NodeStatus.IN_PROGRESS.value
|
||||
|
||||
# 前置已完成,当前未开始
|
||||
return NodeStatus.UNLOCKED.value
|
||||
|
||||
# =====================================================
|
||||
# 学员端 - 开始/完成
|
||||
# =====================================================
|
||||
|
||||
async def start_growth_path(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
growth_path_id: int
|
||||
) -> UserGrowthPathProgress:
|
||||
"""开始学习成长路径"""
|
||||
# 检查成长路径是否存在
|
||||
growth_path = await db.get(GrowthPath, growth_path_id)
|
||||
if not growth_path or growth_path.is_deleted or not growth_path.is_active:
|
||||
raise ValueError("成长路径不存在或未启用")
|
||||
|
||||
# 检查是否已开始
|
||||
existing = await db.execute(
|
||||
select(UserGrowthPathProgress).where(
|
||||
and_(
|
||||
UserGrowthPathProgress.user_id == user_id,
|
||||
UserGrowthPathProgress.growth_path_id == growth_path_id
|
||||
)
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise ValueError("已开始学习此成长路径")
|
||||
|
||||
# 创建进度记录
|
||||
progress = UserGrowthPathProgress(
|
||||
user_id=user_id,
|
||||
growth_path_id=growth_path_id,
|
||||
status=GrowthPathStatus.IN_PROGRESS.value,
|
||||
started_at=datetime.now(),
|
||||
last_activity_at=datetime.now(),
|
||||
)
|
||||
db.add(progress)
|
||||
|
||||
# 获取第一个节点(无前置依赖的)
|
||||
first_node_result = await db.execute(
|
||||
select(GrowthPathNode).where(
|
||||
and_(
|
||||
GrowthPathNode.growth_path_id == growth_path_id,
|
||||
GrowthPathNode.is_deleted == False
|
||||
)
|
||||
).order_by(GrowthPathNode.order_num).limit(1)
|
||||
)
|
||||
first_node = first_node_result.scalar_one_or_none()
|
||||
|
||||
if first_node:
|
||||
progress.current_node_id = first_node.id
|
||||
# 解锁第一个节点
|
||||
completion = UserNodeCompletion(
|
||||
user_id=user_id,
|
||||
growth_path_id=growth_path_id,
|
||||
node_id=first_node.id,
|
||||
status=NodeStatus.UNLOCKED.value,
|
||||
unlocked_at=datetime.now(),
|
||||
)
|
||||
db.add(completion)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(progress)
|
||||
|
||||
logger.info(f"用户 {user_id} 开始学习成长路径 {growth_path_id}")
|
||||
return progress
|
||||
|
||||
async def complete_node(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
node_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""完成节点"""
|
||||
# 获取节点
|
||||
node = await db.get(GrowthPathNode, node_id)
|
||||
if not node or node.is_deleted:
|
||||
raise ValueError("节点不存在")
|
||||
|
||||
# 获取用户进度
|
||||
progress_result = await db.execute(
|
||||
select(UserGrowthPathProgress).where(
|
||||
and_(
|
||||
UserGrowthPathProgress.user_id == user_id,
|
||||
UserGrowthPathProgress.growth_path_id == node.growth_path_id
|
||||
)
|
||||
)
|
||||
)
|
||||
progress = progress_result.scalar_one_or_none()
|
||||
if not progress:
|
||||
raise ValueError("请先开始学习此成长路径")
|
||||
|
||||
# 检查前置节点是否完成
|
||||
completed_node_ids = set(progress.completed_node_ids or [])
|
||||
prerequisites = node.prerequisites or []
|
||||
for prereq_id in prerequisites:
|
||||
if prereq_id not in completed_node_ids:
|
||||
raise ValueError("前置节点未完成")
|
||||
|
||||
# 更新节点完成状态
|
||||
completion_result = await db.execute(
|
||||
select(UserNodeCompletion).where(
|
||||
and_(
|
||||
UserNodeCompletion.user_id == user_id,
|
||||
UserNodeCompletion.node_id == node_id
|
||||
)
|
||||
)
|
||||
)
|
||||
completion = completion_result.scalar_one_or_none()
|
||||
if not completion:
|
||||
completion = UserNodeCompletion(
|
||||
user_id=user_id,
|
||||
growth_path_id=node.growth_path_id,
|
||||
node_id=node_id,
|
||||
)
|
||||
db.add(completion)
|
||||
|
||||
completion.status = NodeStatus.COMPLETED.value
|
||||
completion.course_progress = Decimal("100.00")
|
||||
completion.completed_at = datetime.now()
|
||||
|
||||
# 更新用户进度
|
||||
completed_node_ids.add(node_id)
|
||||
progress.completed_node_ids = list(completed_node_ids)
|
||||
progress.last_activity_at = datetime.now()
|
||||
|
||||
# 计算总进度
|
||||
total_nodes_result = await db.execute(
|
||||
select(func.count(GrowthPathNode.id)).where(
|
||||
and_(
|
||||
GrowthPathNode.growth_path_id == node.growth_path_id,
|
||||
GrowthPathNode.is_deleted == False
|
||||
)
|
||||
)
|
||||
)
|
||||
total_nodes = total_nodes_result.scalar() or 0
|
||||
progress.total_progress = Decimal(str(len(completed_node_ids) / total_nodes * 100)) if total_nodes > 0 else Decimal("0")
|
||||
|
||||
# 检查是否全部完成
|
||||
if len(completed_node_ids) >= total_nodes:
|
||||
progress.status = GrowthPathStatus.COMPLETED.value
|
||||
progress.completed_at = datetime.now()
|
||||
|
||||
# 解锁下一个节点
|
||||
next_nodes = await self._get_unlockable_nodes(
|
||||
db, node.growth_path_id, completed_node_ids
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"用户 {user_id} 完成节点 {node_id}")
|
||||
return {
|
||||
"completed": True,
|
||||
"total_progress": float(progress.total_progress),
|
||||
"is_path_completed": progress.status == GrowthPathStatus.COMPLETED.value,
|
||||
"unlocked_nodes": [n.id for n in next_nodes],
|
||||
}
|
||||
|
||||
async def _get_unlockable_nodes(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
growth_path_id: int,
|
||||
completed_node_ids: set
|
||||
) -> List[GrowthPathNode]:
|
||||
"""获取可解锁的节点"""
|
||||
nodes_result = await db.execute(
|
||||
select(GrowthPathNode).where(
|
||||
and_(
|
||||
GrowthPathNode.growth_path_id == growth_path_id,
|
||||
GrowthPathNode.is_deleted == False,
|
||||
GrowthPathNode.id.notin_(completed_node_ids) if completed_node_ids else True
|
||||
)
|
||||
)
|
||||
)
|
||||
nodes = nodes_result.scalars().all()
|
||||
|
||||
unlockable = []
|
||||
for node in nodes:
|
||||
prerequisites = node.prerequisites or []
|
||||
if all(p in completed_node_ids for p in prerequisites):
|
||||
unlockable.append(node)
|
||||
|
||||
return unlockable
|
||||
|
||||
# =====================================================
|
||||
# 同步课程进度到节点
|
||||
# =====================================================
|
||||
|
||||
async def sync_course_progress(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
course_id: int,
|
||||
progress: float
|
||||
):
|
||||
"""
|
||||
同步课程学习进度到成长路径节点
|
||||
当用户完成课程学习时调用
|
||||
"""
|
||||
# 查找包含该课程的节点
|
||||
nodes_result = await db.execute(
|
||||
select(GrowthPathNode).where(
|
||||
and_(
|
||||
GrowthPathNode.course_id == course_id,
|
||||
GrowthPathNode.is_deleted == False
|
||||
)
|
||||
)
|
||||
)
|
||||
nodes = nodes_result.scalars().all()
|
||||
|
||||
for node in nodes:
|
||||
# 获取用户在该成长路径的进度
|
||||
progress_result = await db.execute(
|
||||
select(UserGrowthPathProgress).where(
|
||||
and_(
|
||||
UserGrowthPathProgress.user_id == user_id,
|
||||
UserGrowthPathProgress.growth_path_id == node.growth_path_id
|
||||
)
|
||||
)
|
||||
)
|
||||
user_progress = progress_result.scalar_one_or_none()
|
||||
if not user_progress:
|
||||
continue
|
||||
|
||||
# 更新节点完成记录
|
||||
completion_result = await db.execute(
|
||||
select(UserNodeCompletion).where(
|
||||
and_(
|
||||
UserNodeCompletion.user_id == user_id,
|
||||
UserNodeCompletion.node_id == node.id
|
||||
)
|
||||
)
|
||||
)
|
||||
completion = completion_result.scalar_one_or_none()
|
||||
if not completion:
|
||||
completion = UserNodeCompletion(
|
||||
user_id=user_id,
|
||||
growth_path_id=node.growth_path_id,
|
||||
node_id=node.id,
|
||||
status=NodeStatus.IN_PROGRESS.value,
|
||||
started_at=datetime.now(),
|
||||
)
|
||||
db.add(completion)
|
||||
|
||||
completion.course_progress = Decimal(str(progress))
|
||||
|
||||
# 如果进度达到100%,自动完成节点
|
||||
if progress >= 100:
|
||||
await self.complete_node(db, user_id, node.id)
|
||||
|
||||
await db.commit()
|
||||
|
||||
|
||||
# 全局实例
|
||||
growth_path_service = GrowthPathService()
|
||||
Reference in New Issue
Block a user