- 后端: 新增 user_levels, exp_history, badge_definitions, user_badges, level_configs 表 - 后端: 新增 LevelService 和 BadgeService 服务 - 后端: 新增等级/奖章/签到/排行榜 API 端点 - 后端: 考试/练习/陪练完成时触发经验值和奖章检查 - 前端: 新增 LevelBadge, ExpProgress, BadgeCard, LevelUpDialog 组件 - 前端: 新增排行榜页面 - 前端: 成长路径页面集成真实等级数据 - 数据库: 包含迁移脚本和初始数据
This commit is contained in:
@@ -107,5 +107,8 @@ api_router.include_router(admin_portal_router, tags=["admin-portal"])
|
||||
# system_settings_router 系统设置路由(企业管理员配置)
|
||||
from .system_settings import router as system_settings_router
|
||||
api_router.include_router(system_settings_router, prefix="/settings", tags=["system-settings"])
|
||||
# level_router 等级与奖章路由
|
||||
from .endpoints.level import router as level_router
|
||||
api_router.include_router(level_router, prefix="/level", tags=["level"])
|
||||
|
||||
__all__ = ["api_router"]
|
||||
|
||||
277
backend/app/api/v1/endpoints/level.py
Normal file
277
backend/app/api/v1/endpoints/level.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
等级与奖章 API
|
||||
|
||||
提供等级查询、奖章查询、排行榜、签到等接口
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db, get_current_user
|
||||
from app.schemas.base import ResponseModel
|
||||
from app.services.level_service import LevelService
|
||||
from app.services.badge_service import BadgeService
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================
|
||||
# 等级相关接口
|
||||
# ============================================
|
||||
|
||||
@router.get("/me", response_model=ResponseModel)
|
||||
async def get_my_level(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取当前用户等级信息
|
||||
|
||||
返回用户的等级、经验值、称号、连续登录天数等信息
|
||||
"""
|
||||
level_service = LevelService(db)
|
||||
level_info = await level_service.get_user_level_info(current_user.id)
|
||||
|
||||
return ResponseModel(
|
||||
message="获取成功",
|
||||
data=level_info
|
||||
)
|
||||
|
||||
|
||||
@router.get("/user/{user_id}", response_model=ResponseModel)
|
||||
async def get_user_level(
|
||||
user_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取指定用户等级信息
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
"""
|
||||
level_service = LevelService(db)
|
||||
level_info = await level_service.get_user_level_info(user_id)
|
||||
|
||||
return ResponseModel(
|
||||
message="获取成功",
|
||||
data=level_info
|
||||
)
|
||||
|
||||
|
||||
@router.post("/checkin", response_model=ResponseModel)
|
||||
async def daily_checkin(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
每日签到
|
||||
|
||||
每天首次签到获得经验值,连续签到有额外奖励
|
||||
"""
|
||||
level_service = LevelService(db)
|
||||
badge_service = BadgeService(db)
|
||||
|
||||
# 执行签到
|
||||
checkin_result = await level_service.daily_checkin(current_user.id)
|
||||
|
||||
# 检查是否解锁新奖章
|
||||
new_badges = []
|
||||
if checkin_result["success"]:
|
||||
new_badges = await badge_service.check_and_award_badges(current_user.id)
|
||||
await db.commit()
|
||||
|
||||
return ResponseModel(
|
||||
message=checkin_result["message"],
|
||||
data={
|
||||
**checkin_result,
|
||||
"new_badges": new_badges
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/exp-history", response_model=ResponseModel)
|
||||
async def get_exp_history(
|
||||
limit: int = Query(default=50, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
exp_type: Optional[str] = Query(default=None, description="经验值类型筛选"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取经验值变化历史
|
||||
|
||||
Args:
|
||||
limit: 每页数量(默认50,最大100)
|
||||
offset: 偏移量
|
||||
exp_type: 类型筛选(exam/practice/training/task/login/badge/other)
|
||||
"""
|
||||
level_service = LevelService(db)
|
||||
history, total = await level_service.get_exp_history(
|
||||
user_id=current_user.id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
exp_type=exp_type
|
||||
)
|
||||
|
||||
return ResponseModel(
|
||||
message="获取成功",
|
||||
data={
|
||||
"items": history,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/leaderboard", response_model=ResponseModel)
|
||||
async def get_leaderboard(
|
||||
limit: int = Query(default=50, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取等级排行榜
|
||||
|
||||
Args:
|
||||
limit: 每页数量(默认50,最大100)
|
||||
offset: 偏移量
|
||||
"""
|
||||
level_service = LevelService(db)
|
||||
|
||||
# 获取排行榜
|
||||
leaderboard, total = await level_service.get_leaderboard(limit=limit, offset=offset)
|
||||
|
||||
# 获取当前用户排名
|
||||
my_rank = await level_service.get_user_rank(current_user.id)
|
||||
|
||||
# 获取当前用户等级信息
|
||||
my_level_info = await level_service.get_user_level_info(current_user.id)
|
||||
|
||||
return ResponseModel(
|
||||
message="获取成功",
|
||||
data={
|
||||
"items": leaderboard,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"my_rank": my_rank,
|
||||
"my_level_info": my_level_info
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================
|
||||
# 奖章相关接口
|
||||
# ============================================
|
||||
|
||||
@router.get("/badges/all", response_model=ResponseModel)
|
||||
async def get_all_badges(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取所有奖章定义
|
||||
|
||||
返回所有可获得的奖章列表
|
||||
"""
|
||||
badge_service = BadgeService(db)
|
||||
badges = await badge_service.get_all_badges()
|
||||
|
||||
return ResponseModel(
|
||||
message="获取成功",
|
||||
data=badges
|
||||
)
|
||||
|
||||
|
||||
@router.get("/badges/me", response_model=ResponseModel)
|
||||
async def get_my_badges(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取当前用户的奖章(含解锁状态)
|
||||
|
||||
返回所有奖章及用户是否已解锁
|
||||
"""
|
||||
badge_service = BadgeService(db)
|
||||
badges = await badge_service.get_user_badges_with_status(current_user.id)
|
||||
|
||||
# 统计已解锁数量
|
||||
unlocked_count = sum(1 for b in badges if b["unlocked"])
|
||||
|
||||
return ResponseModel(
|
||||
message="获取成功",
|
||||
data={
|
||||
"badges": badges,
|
||||
"total": len(badges),
|
||||
"unlocked_count": unlocked_count
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/badges/unnotified", response_model=ResponseModel)
|
||||
async def get_unnotified_badges(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取未通知的新奖章
|
||||
|
||||
用于前端显示新获得奖章的弹窗提示
|
||||
"""
|
||||
badge_service = BadgeService(db)
|
||||
badges = await badge_service.get_unnotified_badges(current_user.id)
|
||||
|
||||
return ResponseModel(
|
||||
message="获取成功",
|
||||
data=badges
|
||||
)
|
||||
|
||||
|
||||
@router.post("/badges/mark-notified", response_model=ResponseModel)
|
||||
async def mark_badges_notified(
|
||||
badge_ids: Optional[list[int]] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
标记奖章为已通知
|
||||
|
||||
Args:
|
||||
badge_ids: 要标记的奖章ID列表(为空则标记全部)
|
||||
"""
|
||||
badge_service = BadgeService(db)
|
||||
await badge_service.mark_badges_notified(current_user.id, badge_ids)
|
||||
await db.commit()
|
||||
|
||||
return ResponseModel(
|
||||
message="标记成功"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/check-badges", response_model=ResponseModel)
|
||||
async def check_and_award_badges(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
检查并授予符合条件的奖章
|
||||
|
||||
手动触发奖章检查,返回新获得的奖章
|
||||
"""
|
||||
badge_service = BadgeService(db)
|
||||
new_badges = await badge_service.check_and_award_badges(current_user.id)
|
||||
await db.commit()
|
||||
|
||||
return ResponseModel(
|
||||
message="检查完成",
|
||||
data={
|
||||
"new_badges": new_badges,
|
||||
"count": len(new_badges)
|
||||
}
|
||||
)
|
||||
@@ -134,8 +134,44 @@ async def submit_exam(
|
||||
user_agent=http_request.headers.get("user-agent")
|
||||
)
|
||||
)
|
||||
|
||||
return ResponseModel(code=200, data=SubmitExamResponse(**result), message="考试提交成功")
|
||||
|
||||
# 考试通过时触发经验值和奖章检查
|
||||
exp_result = None
|
||||
new_badges = []
|
||||
if result.get("is_passed"):
|
||||
try:
|
||||
from app.services.level_service import LevelService
|
||||
from app.services.badge_service import BadgeService
|
||||
|
||||
level_service = LevelService(db)
|
||||
badge_service = BadgeService(db)
|
||||
|
||||
# 添加考试经验值
|
||||
exp_result = await level_service.add_exam_exp(
|
||||
user_id=current_user.id,
|
||||
exam_id=request.exam_id,
|
||||
score=result.get("total_score", 0),
|
||||
is_passed=True
|
||||
)
|
||||
|
||||
# 检查是否解锁新奖章
|
||||
new_badges = await badge_service.check_and_award_badges(current_user.id)
|
||||
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"考试经验值/奖章处理失败: {str(e)}")
|
||||
|
||||
# 将经验值结果添加到返回数据
|
||||
response_data = SubmitExamResponse(**result)
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
data={
|
||||
**response_data.model_dump(),
|
||||
"exp_result": exp_result,
|
||||
"new_badges": new_badges
|
||||
},
|
||||
message="考试提交成功"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/mistakes", response_model=ResponseModel[GetMistakesResponse])
|
||||
|
||||
@@ -704,10 +704,37 @@ async def end_practice_session(
|
||||
|
||||
logger.info(f"结束陪练会话: session_id={session_id}, 时长={session.duration_seconds}秒, 轮次={session.turns}")
|
||||
|
||||
# 练习完成时触发经验值和奖章检查
|
||||
exp_result = None
|
||||
new_badges = []
|
||||
try:
|
||||
from app.services.level_service import LevelService
|
||||
from app.services.badge_service import BadgeService
|
||||
|
||||
level_service = LevelService(db)
|
||||
badge_service = BadgeService(db)
|
||||
|
||||
# 添加练习经验值
|
||||
exp_result = await level_service.add_practice_exp(
|
||||
user_id=current_user.id,
|
||||
session_id=session.id
|
||||
)
|
||||
|
||||
# 检查是否解锁新奖章
|
||||
new_badges = await badge_service.check_and_award_badges(current_user.id)
|
||||
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"练习经验值/奖章处理失败: {str(e)}")
|
||||
|
||||
return ResponseModel(
|
||||
code=200,
|
||||
message="会话已结束",
|
||||
data=session
|
||||
data={
|
||||
"session": session,
|
||||
"exp_result": exp_result,
|
||||
"new_badges": new_badges
|
||||
}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
|
||||
@@ -261,8 +261,40 @@ async def end_training(
|
||||
)
|
||||
|
||||
logger.info(f"用户 {current_user['id']} 结束陪练会话: {session_id}")
|
||||
|
||||
# 陪练完成时触发经验值和奖章检查
|
||||
exp_result = None
|
||||
new_badges = []
|
||||
try:
|
||||
from app.services.level_service import LevelService
|
||||
from app.services.badge_service import BadgeService
|
||||
|
||||
level_service = LevelService(db)
|
||||
badge_service = BadgeService(db)
|
||||
|
||||
# 获取陪练得分(如果有报告的话)
|
||||
score = response.get("total_score") if isinstance(response, dict) else None
|
||||
|
||||
# 添加陪练经验值
|
||||
exp_result = await level_service.add_training_exp(
|
||||
user_id=current_user["id"],
|
||||
session_id=session_id,
|
||||
score=score
|
||||
)
|
||||
|
||||
# 检查是否解锁新奖章
|
||||
new_badges = await badge_service.check_and_award_badges(current_user["id"])
|
||||
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"陪练经验值/奖章处理失败: {str(e)}")
|
||||
|
||||
# 将经验值结果添加到返回数据
|
||||
result_data = response if isinstance(response, dict) else {"session_id": session_id}
|
||||
result_data["exp_result"] = exp_result
|
||||
result_data["new_badges"] = new_badges
|
||||
|
||||
return ResponseModel(data=response, message="结束陪练成功")
|
||||
return ResponseModel(data=result_data, message="结束陪练成功")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user