- 新增数据库表: 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:
@@ -125,5 +125,8 @@ api_router.include_router(speech_router, prefix="/speech", tags=["speech"])
|
||||
# recommendation_router 智能推荐路由
|
||||
from .endpoints.recommendation import router as recommendation_router
|
||||
api_router.include_router(recommendation_router, prefix="/recommendations", tags=["recommendations"])
|
||||
# growth_path_router 成长路径路由
|
||||
from .endpoints.growth_path import router as growth_path_router
|
||||
api_router.include_router(growth_path_router, tags=["growth-path"])
|
||||
|
||||
__all__ = ["api_router"]
|
||||
|
||||
234
backend/app/api/v1/endpoints/growth_path.py
Normal file
234
backend/app/api/v1/endpoints/growth_path.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
成长路径 API 端点
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.services.growth_path_service import growth_path_service
|
||||
from app.schemas.growth_path import (
|
||||
GrowthPathCreate,
|
||||
GrowthPathUpdate,
|
||||
GrowthPathResponse,
|
||||
GrowthPathListResponse,
|
||||
TraineeGrowthPathResponse,
|
||||
UserGrowthPathProgressResponse,
|
||||
StartGrowthPathRequest,
|
||||
CompleteNodeRequest,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# =====================================================
|
||||
# 学员端 API
|
||||
# =====================================================
|
||||
|
||||
@router.get("/trainee/growth-path", response_model=Optional[TraineeGrowthPathResponse])
|
||||
async def get_trainee_growth_path(
|
||||
position_id: Optional[int] = Query(None, description="岗位ID,不传则自动匹配"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
获取学员的成长路径(含进度)
|
||||
|
||||
返回数据包含:
|
||||
- 成长路径基本信息
|
||||
- 各阶段及节点信息
|
||||
- 每个节点的学习状态(locked/unlocked/in_progress/completed)
|
||||
- 每个节点的课程学习进度
|
||||
"""
|
||||
try:
|
||||
result = await growth_path_service.get_trainee_growth_path(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
position_id=position_id
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"获取成长路径失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/trainee/growth-path/start")
|
||||
async def start_growth_path(
|
||||
request: StartGrowthPathRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
开始学习成长路径
|
||||
"""
|
||||
try:
|
||||
progress = await growth_path_service.start_growth_path(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
growth_path_id=request.growth_path_id
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "已开始学习成长路径",
|
||||
"progress_id": progress.id,
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"开始成长路径失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/trainee/growth-path/node/complete")
|
||||
async def complete_growth_path_node(
|
||||
request: CompleteNodeRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
完成成长路径节点
|
||||
"""
|
||||
try:
|
||||
result = await growth_path_service.complete_node(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
node_id=request.node_id
|
||||
)
|
||||
return result
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"完成节点失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# =====================================================
|
||||
# 管理端 API
|
||||
# =====================================================
|
||||
|
||||
@router.get("/manager/growth-paths")
|
||||
async def list_growth_paths(
|
||||
position_id: Optional[int] = Query(None, description="岗位ID筛选"),
|
||||
is_active: Optional[bool] = Query(None, description="是否启用"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
获取成长路径列表(管理端)
|
||||
"""
|
||||
try:
|
||||
result = await growth_path_service.list_growth_paths(
|
||||
db=db,
|
||||
position_id=position_id,
|
||||
is_active=is_active,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"获取成长路径列表失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/manager/growth-paths")
|
||||
async def create_growth_path(
|
||||
data: GrowthPathCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
创建成长路径(管理端)
|
||||
"""
|
||||
try:
|
||||
growth_path = await growth_path_service.create_growth_path(
|
||||
db=db,
|
||||
data=data,
|
||||
created_by=current_user.id
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "创建成功",
|
||||
"id": growth_path.id,
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"创建成长路径失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/manager/growth-paths/{path_id}")
|
||||
async def get_growth_path(
|
||||
path_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
获取成长路径详情(管理端)
|
||||
"""
|
||||
try:
|
||||
result = await growth_path_service.get_growth_path(db=db, path_id=path_id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="成长路径不存在")
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取成长路径详情失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/manager/growth-paths/{path_id}")
|
||||
async def update_growth_path(
|
||||
path_id: int,
|
||||
data: GrowthPathUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
更新成长路径(管理端)
|
||||
"""
|
||||
try:
|
||||
await growth_path_service.update_growth_path(
|
||||
db=db,
|
||||
path_id=path_id,
|
||||
data=data
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "更新成功",
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"更新成长路径失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/manager/growth-paths/{path_id}")
|
||||
async def delete_growth_path(
|
||||
path_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
删除成长路径(管理端)
|
||||
"""
|
||||
try:
|
||||
await growth_path_service.delete_growth_path(db=db, path_id=path_id)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "删除成功",
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"删除成长路径失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
118
backend/app/migrations/add_growth_path_tables.sql
Normal file
118
backend/app/migrations/add_growth_path_tables.sql
Normal file
@@ -0,0 +1,118 @@
|
||||
-- 成长路径功能数据库迁移脚本
|
||||
-- 创建时间: 2026-01-30
|
||||
|
||||
-- =====================================================
|
||||
-- 1. 修改 growth_paths 表,添加岗位关联
|
||||
-- =====================================================
|
||||
ALTER TABLE growth_paths
|
||||
ADD COLUMN IF NOT EXISTS position_id INT NULL COMMENT '关联岗位ID' AFTER target_role,
|
||||
ADD COLUMN IF NOT EXISTS stages JSON NULL COMMENT '阶段配置[{name, description, order}]' AFTER courses,
|
||||
ADD INDEX idx_position_id (position_id);
|
||||
|
||||
-- =====================================================
|
||||
-- 2. 创建成长路径节点表
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS growth_path_nodes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
growth_path_id INT NOT NULL COMMENT '成长路径ID',
|
||||
course_id INT NOT NULL COMMENT '课程ID',
|
||||
stage_name VARCHAR(100) NULL COMMENT '所属阶段名称',
|
||||
title VARCHAR(200) NOT NULL COMMENT '节点标题',
|
||||
description TEXT NULL COMMENT '节点描述',
|
||||
order_num INT DEFAULT 0 NOT NULL COMMENT '排序顺序',
|
||||
is_required BOOLEAN DEFAULT TRUE NOT NULL COMMENT '是否必修',
|
||||
prerequisites JSON NULL COMMENT '前置节点IDs [1, 2, 3]',
|
||||
estimated_days INT DEFAULT 7 COMMENT '预计学习天数',
|
||||
|
||||
-- 软删除
|
||||
is_deleted BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
deleted_at DATETIME NULL,
|
||||
|
||||
-- 时间戳
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
-- 索引
|
||||
INDEX idx_growth_path_id (growth_path_id),
|
||||
INDEX idx_course_id (course_id),
|
||||
INDEX idx_stage_name (stage_name),
|
||||
INDEX idx_order_num (order_num),
|
||||
INDEX idx_is_deleted (is_deleted),
|
||||
|
||||
-- 外键
|
||||
CONSTRAINT fk_gpn_growth_path FOREIGN KEY (growth_path_id)
|
||||
REFERENCES growth_paths(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_gpn_course FOREIGN KEY (course_id)
|
||||
REFERENCES courses(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='成长路径节点表';
|
||||
|
||||
-- =====================================================
|
||||
-- 3. 创建用户成长路径进度表
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS user_growth_path_progress (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL COMMENT '用户ID',
|
||||
growth_path_id INT NOT NULL COMMENT '成长路径ID',
|
||||
current_node_id INT NULL COMMENT '当前学习节点ID',
|
||||
completed_node_ids JSON NULL COMMENT '已完成节点IDs [1, 2, 3]',
|
||||
total_progress DECIMAL(5,2) DEFAULT 0.00 COMMENT '总进度百分比',
|
||||
status VARCHAR(20) DEFAULT 'not_started' NOT NULL COMMENT '状态: not_started/in_progress/completed',
|
||||
|
||||
-- 时间记录
|
||||
started_at DATETIME NULL COMMENT '开始时间',
|
||||
completed_at DATETIME NULL COMMENT '完成时间',
|
||||
last_activity_at DATETIME NULL COMMENT '最后活动时间',
|
||||
|
||||
-- 时间戳
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
-- 索引
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_growth_path_id (growth_path_id),
|
||||
INDEX idx_status (status),
|
||||
UNIQUE KEY uk_user_growth_path (user_id, growth_path_id),
|
||||
|
||||
-- 外键
|
||||
CONSTRAINT fk_ugpp_user FOREIGN KEY (user_id)
|
||||
REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_ugpp_growth_path FOREIGN KEY (growth_path_id)
|
||||
REFERENCES growth_paths(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='用户成长路径进度表';
|
||||
|
||||
-- =====================================================
|
||||
-- 4. 创建用户节点完成记录表(详细记录每个节点的完成情况)
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS user_node_completions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL COMMENT '用户ID',
|
||||
growth_path_id INT NOT NULL COMMENT '成长路径ID',
|
||||
node_id INT NOT NULL COMMENT '节点ID',
|
||||
course_progress DECIMAL(5,2) DEFAULT 0.00 COMMENT '课程学习进度',
|
||||
status VARCHAR(20) DEFAULT 'locked' NOT NULL COMMENT '状态: locked/unlocked/in_progress/completed',
|
||||
|
||||
-- 时间记录
|
||||
unlocked_at DATETIME NULL COMMENT '解锁时间',
|
||||
started_at DATETIME NULL COMMENT '开始学习时间',
|
||||
completed_at DATETIME NULL COMMENT '完成时间',
|
||||
|
||||
-- 时间戳
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
-- 索引
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_growth_path_id (growth_path_id),
|
||||
INDEX idx_node_id (node_id),
|
||||
INDEX idx_status (status),
|
||||
UNIQUE KEY uk_user_node (user_id, node_id),
|
||||
|
||||
-- 外键
|
||||
CONSTRAINT fk_unc_user FOREIGN KEY (user_id)
|
||||
REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_unc_node FOREIGN KEY (node_id)
|
||||
REFERENCES growth_path_nodes(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='用户节点完成记录表';
|
||||
@@ -37,6 +37,13 @@ from app.models.user_course_progress import (
|
||||
UserMaterialProgress,
|
||||
ProgressStatus,
|
||||
)
|
||||
from app.models.growth_path import (
|
||||
GrowthPathNode,
|
||||
UserGrowthPathProgress,
|
||||
UserNodeCompletion,
|
||||
GrowthPathStatus,
|
||||
NodeStatus,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Base",
|
||||
@@ -80,4 +87,9 @@ __all__ = [
|
||||
"UserCourseProgress",
|
||||
"UserMaterialProgress",
|
||||
"ProgressStatus",
|
||||
"GrowthPathNode",
|
||||
"UserGrowthPathProgress",
|
||||
"UserNodeCompletion",
|
||||
"GrowthPathStatus",
|
||||
"NodeStatus",
|
||||
]
|
||||
|
||||
@@ -224,9 +224,19 @@ class GrowthPath(BaseModel, SoftDeleteMixin):
|
||||
String(100), nullable=True, comment="目标角色"
|
||||
)
|
||||
|
||||
# 路径配置
|
||||
# 岗位关联
|
||||
position_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True, comment="关联岗位ID"
|
||||
)
|
||||
|
||||
# 路径配置(保留用于兼容,新版使用 nodes 关联表)
|
||||
courses: Mapped[Optional[List[dict]]] = mapped_column(
|
||||
JSON, nullable=True, comment="课程列表[{course_id, order, is_required}]"
|
||||
JSON, nullable=True, comment="课程列表[{course_id, order, is_required}](已废弃,使用nodes)"
|
||||
)
|
||||
|
||||
# 阶段配置
|
||||
stages: Mapped[Optional[List[dict]]] = mapped_column(
|
||||
JSON, nullable=True, comment="阶段配置[{name, description, order}]"
|
||||
)
|
||||
|
||||
# 预计时长
|
||||
@@ -242,6 +252,11 @@ class GrowthPath(BaseModel, SoftDeleteMixin):
|
||||
Integer, default=0, nullable=False, comment="排序顺序"
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
nodes: Mapped[List["GrowthPathNode"]] = relationship(
|
||||
"GrowthPathNode", back_populates="growth_path", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class MaterialKnowledgePoint(BaseModel, SoftDeleteMixin):
|
||||
"""
|
||||
|
||||
206
backend/app/models/growth_path.py
Normal file
206
backend/app/models/growth_path.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
成长路径相关数据库模型
|
||||
"""
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
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="预计学习天数"
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
growth_path: Mapped["GrowthPath"] = relationship(
|
||||
"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")
|
||||
|
||||
|
||||
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")
|
||||
224
backend/app/schemas/growth_path.py
Normal file
224
backend/app/schemas/growth_path.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
成长路径相关 Schema
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# =====================================================
|
||||
# 基础数据结构
|
||||
# =====================================================
|
||||
|
||||
class StageConfig(BaseModel):
|
||||
"""阶段配置"""
|
||||
name: str = Field(..., description="阶段名称")
|
||||
description: Optional[str] = Field(None, description="阶段描述")
|
||||
order: int = Field(0, description="排序")
|
||||
|
||||
|
||||
class NodeBase(BaseModel):
|
||||
"""节点基础信息"""
|
||||
course_id: int = Field(..., description="课程ID")
|
||||
stage_name: Optional[str] = Field(None, description="所属阶段名称")
|
||||
title: str = Field(..., description="节点标题")
|
||||
description: Optional[str] = Field(None, description="节点描述")
|
||||
order_num: int = Field(0, description="排序顺序")
|
||||
is_required: bool = Field(True, description="是否必修")
|
||||
prerequisites: Optional[List[int]] = Field(None, description="前置节点IDs")
|
||||
estimated_days: int = Field(7, description="预计学习天数")
|
||||
|
||||
|
||||
# =====================================================
|
||||
# 管理端 - 创建/更新
|
||||
# =====================================================
|
||||
|
||||
class GrowthPathNodeCreate(NodeBase):
|
||||
"""创建节点"""
|
||||
pass
|
||||
|
||||
|
||||
class GrowthPathNodeUpdate(BaseModel):
|
||||
"""更新节点"""
|
||||
course_id: Optional[int] = None
|
||||
stage_name: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
order_num: Optional[int] = None
|
||||
is_required: Optional[bool] = None
|
||||
prerequisites: Optional[List[int]] = None
|
||||
estimated_days: Optional[int] = None
|
||||
|
||||
|
||||
class GrowthPathCreate(BaseModel):
|
||||
"""创建成长路径"""
|
||||
name: str = Field(..., description="路径名称")
|
||||
description: Optional[str] = Field(None, description="路径描述")
|
||||
target_role: Optional[str] = Field(None, description="目标角色")
|
||||
position_id: Optional[int] = Field(None, description="关联岗位ID")
|
||||
stages: Optional[List[StageConfig]] = Field(None, description="阶段配置")
|
||||
estimated_duration_days: Optional[int] = Field(None, description="预计完成天数")
|
||||
is_active: bool = Field(True, description="是否启用")
|
||||
sort_order: int = Field(0, description="排序")
|
||||
nodes: Optional[List[GrowthPathNodeCreate]] = Field(None, description="节点列表")
|
||||
|
||||
|
||||
class GrowthPathUpdate(BaseModel):
|
||||
"""更新成长路径"""
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
target_role: Optional[str] = None
|
||||
position_id: Optional[int] = None
|
||||
stages: Optional[List[StageConfig]] = None
|
||||
estimated_duration_days: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
sort_order: Optional[int] = None
|
||||
nodes: Optional[List[GrowthPathNodeCreate]] = None # 整体替换节点
|
||||
|
||||
|
||||
# =====================================================
|
||||
# 管理端 - 响应
|
||||
# =====================================================
|
||||
|
||||
class GrowthPathNodeResponse(NodeBase):
|
||||
"""节点响应"""
|
||||
id: int
|
||||
growth_path_id: int
|
||||
course_name: Optional[str] = None # 课程名称(关联查询)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class GrowthPathResponse(BaseModel):
|
||||
"""成长路径响应(管理端)"""
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
target_role: Optional[str] = None
|
||||
position_id: Optional[int] = None
|
||||
position_name: Optional[str] = None # 岗位名称(关联查询)
|
||||
stages: Optional[List[StageConfig]] = None
|
||||
estimated_duration_days: Optional[int] = None
|
||||
is_active: bool
|
||||
sort_order: int
|
||||
nodes: List[GrowthPathNodeResponse] = []
|
||||
node_count: int = 0 # 节点数量
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class GrowthPathListResponse(BaseModel):
|
||||
"""成长路径列表响应"""
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
position_id: Optional[int] = None
|
||||
position_name: Optional[str] = None
|
||||
is_active: bool
|
||||
node_count: int = 0
|
||||
estimated_duration_days: Optional[int] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# =====================================================
|
||||
# 学员端 - 响应
|
||||
# =====================================================
|
||||
|
||||
class TraineeNodeResponse(BaseModel):
|
||||
"""学员端节点响应(含进度状态)"""
|
||||
id: int
|
||||
course_id: int
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
stage_name: Optional[str] = None
|
||||
is_required: bool
|
||||
estimated_days: int
|
||||
order_num: int
|
||||
|
||||
# 学员特有
|
||||
status: str = Field(..., description="状态: locked/unlocked/in_progress/completed")
|
||||
progress: float = Field(0, description="课程学习进度 0-100")
|
||||
|
||||
# 课程信息
|
||||
course_name: Optional[str] = None
|
||||
course_cover: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TraineeStageResponse(BaseModel):
|
||||
"""学员端阶段响应"""
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
completed: int = Field(0, description="已完成节点数")
|
||||
total: int = Field(0, description="总节点数")
|
||||
nodes: List[TraineeNodeResponse] = []
|
||||
|
||||
|
||||
class TraineeGrowthPathResponse(BaseModel):
|
||||
"""学员端成长路径响应"""
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
position_id: Optional[int] = None
|
||||
position_name: Optional[str] = None
|
||||
|
||||
# 进度信息
|
||||
total_progress: float = Field(0, description="总进度百分比")
|
||||
completed_nodes: int = Field(0, description="已完成节点数")
|
||||
total_nodes: int = Field(0, description="总节点数")
|
||||
status: str = Field("not_started", description="状态: not_started/in_progress/completed")
|
||||
|
||||
# 时间信息
|
||||
started_at: Optional[datetime] = None
|
||||
estimated_completion_days: Optional[int] = None
|
||||
|
||||
# 阶段和节点
|
||||
stages: List[TraineeStageResponse] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# =====================================================
|
||||
# 用户进度
|
||||
# =====================================================
|
||||
|
||||
class UserGrowthPathProgressResponse(BaseModel):
|
||||
"""用户成长路径进度响应"""
|
||||
id: int
|
||||
user_id: int
|
||||
growth_path_id: int
|
||||
growth_path_name: str
|
||||
current_node_id: Optional[int] = None
|
||||
current_node_title: Optional[str] = None
|
||||
completed_node_ids: List[int] = []
|
||||
total_progress: float
|
||||
status: str
|
||||
started_at: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
last_activity_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class StartGrowthPathRequest(BaseModel):
|
||||
"""开始学习成长路径请求"""
|
||||
growth_path_id: int = Field(..., description="成长路径ID")
|
||||
|
||||
|
||||
class CompleteNodeRequest(BaseModel):
|
||||
"""完成节点请求"""
|
||||
node_id: int = Field(..., description="节点ID")
|
||||
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()
|
||||
@@ -102,28 +102,97 @@ export interface CourseDetailForManager extends CourseInfo {
|
||||
}>
|
||||
}
|
||||
|
||||
// 成长路径配置
|
||||
export interface GrowthPathConfig {
|
||||
// 成长路径节点
|
||||
export interface GrowthPathNode {
|
||||
id: number
|
||||
positionId: number
|
||||
positionName: string
|
||||
growth_path_id: number
|
||||
course_id: number
|
||||
course_name?: string
|
||||
stage_name?: string
|
||||
title: string
|
||||
description?: string
|
||||
order_num: number
|
||||
is_required: boolean
|
||||
prerequisites?: number[]
|
||||
estimated_days: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 阶段配置
|
||||
export interface StageConfig {
|
||||
name: string
|
||||
description?: string
|
||||
courses: Array<{
|
||||
courseId: number
|
||||
courseName: string
|
||||
isRequired: boolean
|
||||
prerequisites: number[]
|
||||
order: number
|
||||
estimatedDays: number
|
||||
}>
|
||||
totalCourses: number
|
||||
requiredCourses: number
|
||||
optionalCourses: number
|
||||
estimatedDays: number
|
||||
status: 'active' | 'inactive'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
order: number
|
||||
}
|
||||
|
||||
// 成长路径配置(新版)
|
||||
export interface GrowthPathConfig {
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
target_role?: string
|
||||
position_id?: number
|
||||
position_name?: string
|
||||
stages?: StageConfig[]
|
||||
estimated_duration_days?: number
|
||||
is_active: boolean
|
||||
sort_order: number
|
||||
nodes: GrowthPathNode[]
|
||||
node_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 成长路径列表项
|
||||
export interface GrowthPathListItem {
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
position_id?: number
|
||||
position_name?: string
|
||||
is_active: boolean
|
||||
node_count: number
|
||||
estimated_duration_days?: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 创建节点请求
|
||||
export interface CreateGrowthPathNode {
|
||||
course_id: number
|
||||
stage_name?: string
|
||||
title: string
|
||||
description?: string
|
||||
order_num: number
|
||||
is_required: boolean
|
||||
prerequisites?: number[]
|
||||
estimated_days: number
|
||||
}
|
||||
|
||||
// 创建成长路径请求
|
||||
export interface CreateGrowthPathRequest {
|
||||
name: string
|
||||
description?: string
|
||||
target_role?: string
|
||||
position_id?: number
|
||||
stages?: StageConfig[]
|
||||
estimated_duration_days?: number
|
||||
is_active?: boolean
|
||||
sort_order?: number
|
||||
nodes?: CreateGrowthPathNode[]
|
||||
}
|
||||
|
||||
// 更新成长路径请求
|
||||
export interface UpdateGrowthPathRequest {
|
||||
name?: string
|
||||
description?: string
|
||||
target_role?: string
|
||||
position_id?: number
|
||||
stages?: StageConfig[]
|
||||
estimated_duration_days?: number
|
||||
is_active?: boolean
|
||||
sort_order?: number
|
||||
nodes?: CreateGrowthPathNode[]
|
||||
}
|
||||
|
||||
// AI陪练场景(管理视图)
|
||||
@@ -364,49 +433,44 @@ export const analyzeKnowledgePoints = (courseId: number) => {
|
||||
*/
|
||||
export const getGrowthPathConfigs = (params: {
|
||||
page?: number
|
||||
size?: number
|
||||
positionId?: number
|
||||
status?: string
|
||||
page_size?: number
|
||||
position_id?: number
|
||||
is_active?: boolean
|
||||
} = {}) => {
|
||||
return request.get<{
|
||||
items: GrowthPathConfig[]
|
||||
items: GrowthPathListItem[]
|
||||
total: number
|
||||
page: number
|
||||
size: number
|
||||
page_size: number
|
||||
}>('/api/v1/manager/growth-paths', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取成长路径详情
|
||||
*/
|
||||
export const getGrowthPathDetail = (pathId: number) => {
|
||||
return request.get<GrowthPathConfig>(`/api/v1/manager/growth-paths/${pathId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成长路径
|
||||
*/
|
||||
export const createGrowthPath = (data: {
|
||||
positionId: number
|
||||
name: string
|
||||
description?: string
|
||||
courses: Array<{
|
||||
courseId: number
|
||||
isRequired: boolean
|
||||
prerequisites?: number[]
|
||||
estimatedDays?: number
|
||||
}>
|
||||
}) => {
|
||||
return request.post<GrowthPathConfig>('/api/v1/manager/growth-paths', data)
|
||||
export const createGrowthPath = (data: CreateGrowthPathRequest) => {
|
||||
return request.post<{ success: boolean; message: string; id: number }>('/api/v1/manager/growth-paths', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新成长路径
|
||||
*/
|
||||
export const updateGrowthPath = (pathId: number, data: {
|
||||
name?: string
|
||||
description?: string
|
||||
courses?: Array<{
|
||||
courseId: number
|
||||
isRequired: boolean
|
||||
prerequisites?: number[]
|
||||
estimatedDays?: number
|
||||
}>
|
||||
}) => {
|
||||
return request.put<GrowthPathConfig>(`/api/v1/manager/growth-paths/${pathId}`, data)
|
||||
export const updateGrowthPath = (pathId: number, data: UpdateGrowthPathRequest) => {
|
||||
return request.put<{ success: boolean; message: string }>(`/api/v1/manager/growth-paths/${pathId}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除成长路径
|
||||
*/
|
||||
export const deleteGrowthPath = (pathId: number) => {
|
||||
return request.delete<{ success: boolean; message: string }>(`/api/v1/manager/growth-paths/${pathId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,48 @@ export interface CourseMaterial {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 成长路径节点
|
||||
// 成长路径节点(学员端)
|
||||
export interface TraineeGrowthPathNode {
|
||||
id: number
|
||||
course_id: number
|
||||
title: string
|
||||
description?: string
|
||||
stage_name?: string
|
||||
is_required: boolean
|
||||
estimated_days: number
|
||||
order_num: number
|
||||
status: 'locked' | 'unlocked' | 'in_progress' | 'completed'
|
||||
progress: number
|
||||
course_name?: string
|
||||
course_cover?: string
|
||||
}
|
||||
|
||||
// 成长路径阶段
|
||||
export interface TraineeGrowthPathStage {
|
||||
name: string
|
||||
description?: string
|
||||
completed: number
|
||||
total: number
|
||||
nodes: TraineeGrowthPathNode[]
|
||||
}
|
||||
|
||||
// 成长路径(学员端响应)
|
||||
export interface TraineeGrowthPath {
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
position_id?: number
|
||||
position_name?: string
|
||||
total_progress: number
|
||||
completed_nodes: number
|
||||
total_nodes: number
|
||||
status: 'not_started' | 'in_progress' | 'completed'
|
||||
started_at?: string
|
||||
estimated_completion_days?: number
|
||||
stages: TraineeGrowthPathStage[]
|
||||
}
|
||||
|
||||
// 兼容旧接口
|
||||
export interface GrowthPathNode {
|
||||
id: number
|
||||
courseId: number
|
||||
@@ -54,7 +95,7 @@ export interface GrowthPathNode {
|
||||
order: number
|
||||
}
|
||||
|
||||
// 成长路径
|
||||
// 兼容旧接口
|
||||
export interface GrowthPath {
|
||||
id: number
|
||||
name: string
|
||||
@@ -226,10 +267,35 @@ export const markMaterialCompleted = (materialId: number) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取成长路径
|
||||
* 获取成长路径(学员端)
|
||||
*/
|
||||
export const getGrowthPath = () => {
|
||||
return request.get<GrowthPath>('/api/v1/trainee/growth-path')
|
||||
export const getGrowthPath = (positionId?: number) => {
|
||||
return request.get<TraineeGrowthPath>('/api/v1/trainee/growth-path', {
|
||||
params: positionId ? { position_id: positionId } : {}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始学习成长路径
|
||||
*/
|
||||
export const startGrowthPath = (growthPathId: number) => {
|
||||
return request.post<{ success: boolean; message: string; progress_id: number }>('/api/v1/trainee/growth-path/start', {
|
||||
growth_path_id: growthPathId
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成成长路径节点
|
||||
*/
|
||||
export const completeGrowthPathNode = (nodeId: number) => {
|
||||
return request.post<{
|
||||
completed: boolean
|
||||
total_progress: number
|
||||
is_path_completed: boolean
|
||||
unlocked_nodes: number[]
|
||||
}>('/api/v1/trainee/growth-path/node/complete', {
|
||||
node_id: nodeId
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -363,7 +363,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
@@ -374,7 +374,8 @@ import {
|
||||
Cpu // 机器人图标
|
||||
} from '@element-plus/icons-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { analyzeYanjiBadge, getCourseDetail } from '@/api/trainee'
|
||||
import { analyzeYanjiBadge, getCourseDetail, getGrowthPath, startGrowthPath } from '@/api/trainee'
|
||||
import type { TraineeGrowthPath, TraineeGrowthPathStage } from '@/api/trainee'
|
||||
import { getCurrentUserProfile } from '@/api/user'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -451,100 +452,73 @@ const expPercentage = computed(() => {
|
||||
return Math.round((userInfo.value.exp / userInfo.value.nextLevelExp) * 100)
|
||||
})
|
||||
|
||||
// 成长路径数据
|
||||
const growthPath = ref([
|
||||
{
|
||||
name: '基础阶段',
|
||||
completed: 3,
|
||||
total: 3,
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
title: '机构文化与服务理念',
|
||||
description: '了解机构的服务理念和专业文化',
|
||||
status: 'completed',
|
||||
progress: 100
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '美容产品基础知识',
|
||||
description: '学习美容产品的基本知识和使用方法',
|
||||
status: 'completed',
|
||||
progress: 100
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '客户接待与服务标准',
|
||||
description: '掌握专业的客户接待和服务流程',
|
||||
status: 'completed',
|
||||
progress: 100
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '进阶阶段',
|
||||
completed: 2,
|
||||
total: 4,
|
||||
nodes: [
|
||||
{
|
||||
id: 4,
|
||||
title: '皮肤分析技术',
|
||||
description: '学习专业的皮肤分析方法和技术',
|
||||
status: 'completed',
|
||||
progress: 100
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '美容仪器操作',
|
||||
description: '掌握各种美容仪器的操作方法',
|
||||
status: 'completed',
|
||||
progress: 100
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '客户沟通进阶',
|
||||
description: '学习高级的客户沟通和咨询技巧',
|
||||
status: 'current',
|
||||
progress: 65
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: '美容方案设计',
|
||||
description: '学习为客户设计个性化的美容方案',
|
||||
status: 'locked',
|
||||
progress: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '高级阶段',
|
||||
completed: 0,
|
||||
total: 3,
|
||||
nodes: [
|
||||
{
|
||||
id: 8,
|
||||
title: '轻医美项目咨询',
|
||||
description: '掌握轻医美项目的专业咨询技能',
|
||||
status: 'locked',
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: '团队协作与管理',
|
||||
description: '学习团队协作和基础管理技能',
|
||||
status: 'locked',
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: '高级美容技术',
|
||||
description: '掌握高级美容技术和创新方法',
|
||||
status: 'locked',
|
||||
progress: 0
|
||||
}
|
||||
]
|
||||
// 成长路径数据(从API加载)
|
||||
const growthPathData = ref<TraineeGrowthPath | null>(null)
|
||||
const growthPathLoading = ref(false)
|
||||
|
||||
// 将API数据转换为前端展示格式
|
||||
const growthPath = computed(() => {
|
||||
if (!growthPathData.value || !growthPathData.value.stages) {
|
||||
// 返回默认空数据或占位数据
|
||||
return [{
|
||||
name: '暂无成长路径',
|
||||
completed: 0,
|
||||
total: 0,
|
||||
nodes: []
|
||||
}]
|
||||
}
|
||||
])
|
||||
|
||||
return growthPathData.value.stages.map(stage => ({
|
||||
name: stage.name,
|
||||
completed: stage.completed,
|
||||
total: stage.total,
|
||||
nodes: stage.nodes.map(node => ({
|
||||
id: node.id,
|
||||
courseId: node.course_id,
|
||||
title: node.title,
|
||||
description: node.description || '',
|
||||
status: node.status, // locked/unlocked/in_progress/completed
|
||||
progress: node.progress
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
/**
|
||||
* 加载成长路径数据
|
||||
*/
|
||||
const loadGrowthPath = async () => {
|
||||
growthPathLoading.value = true
|
||||
try {
|
||||
const response = await getGrowthPath()
|
||||
if (response.code === 200 && response.data) {
|
||||
growthPathData.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载成长路径失败:', error)
|
||||
// 失败时不显示错误,保持空状态
|
||||
} finally {
|
||||
growthPathLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始学习成长路径
|
||||
*/
|
||||
const handleStartGrowthPath = async () => {
|
||||
if (!growthPathData.value) return
|
||||
|
||||
try {
|
||||
const response = await startGrowthPath(growthPathData.value.id)
|
||||
if (response.code === 200) {
|
||||
ElMessage.success('已开始学习成长路径')
|
||||
// 重新加载数据
|
||||
await loadGrowthPath()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('开始学习失败:', error)
|
||||
ElMessage.error(error.response?.data?.detail || '开始学习失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化雷达图
|
||||
@@ -754,12 +728,12 @@ const handleNodeClick = (node: any) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (node.status === 'completed') {
|
||||
ElMessage.info(`${node.title} 已完成`)
|
||||
return
|
||||
// 跳转到课程详情页
|
||||
if (node.courseId) {
|
||||
router.push(`/trainee/course-detail?id=${node.courseId}`)
|
||||
} else {
|
||||
ElMessage.info(`${node.title}`)
|
||||
}
|
||||
|
||||
ElMessage.success(`开始学习:${node.title}`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -921,6 +895,9 @@ onMounted(async () => {
|
||||
// 获取用户信息
|
||||
await fetchUserInfo()
|
||||
|
||||
// 加载成长路径数据
|
||||
await loadGrowthPath()
|
||||
|
||||
nextTick(() => {
|
||||
initRadarChart()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user