1. 奖章条件优化 - 修复统计查询 SQL 语法 - 添加按类别检查奖章方法 2. 移动端适配 - 登录页、课程中心、课程详情 - 考试页面、成长路径、排行榜 3. 证书系统 - 数据库模型和迁移脚本 - 证书颁发/列表/下载/验证 API - 前端证书列表页面 4. 数据大屏 - 企业级/团队级数据 API - ECharts 可视化大屏页面
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
from sqlalchemy import select, func, and_, or_
|
||||
from sqlalchemy import select, func, and_, or_, case
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.logger import get_logger
|
||||
@@ -162,80 +162,102 @@ class BadgeService:
|
||||
"user_level": 1,
|
||||
}
|
||||
|
||||
# 获取用户等级信息
|
||||
result = await self.db.execute(
|
||||
select(UserLevel).where(UserLevel.user_id == user_id)
|
||||
)
|
||||
user_level = result.scalar_one_or_none()
|
||||
if user_level:
|
||||
stats["login_streak"] = user_level.login_streak
|
||||
stats["user_level"] = user_level.level
|
||||
|
||||
# 获取登录次数(从经验值历史)
|
||||
result = await self.db.execute(
|
||||
select(func.count(ExpHistory.id))
|
||||
.where(
|
||||
ExpHistory.user_id == user_id,
|
||||
ExpHistory.exp_type == ExpType.LOGIN
|
||||
try:
|
||||
# 获取用户等级信息
|
||||
result = await self.db.execute(
|
||||
select(UserLevel).where(UserLevel.user_id == user_id)
|
||||
)
|
||||
)
|
||||
stats["login_count"] = result.scalar() or 0
|
||||
|
||||
# 获取考试统计
|
||||
result = await self.db.execute(
|
||||
select(
|
||||
func.count(Exam.id),
|
||||
func.sum(func.if_(Exam.score >= 100, 1, 0)),
|
||||
func.sum(func.if_(Exam.score >= 90, 1, 0))
|
||||
user_level = result.scalar_one_or_none()
|
||||
if user_level:
|
||||
stats["login_streak"] = user_level.login_streak or 0
|
||||
stats["user_level"] = user_level.level or 1
|
||||
|
||||
# 获取登录/签到次数(从经验值历史)
|
||||
result = await self.db.execute(
|
||||
select(func.count(ExpHistory.id))
|
||||
.where(
|
||||
ExpHistory.user_id == user_id,
|
||||
ExpHistory.exp_type == ExpType.LOGIN
|
||||
)
|
||||
)
|
||||
.where(
|
||||
Exam.user_id == user_id,
|
||||
Exam.is_passed == True,
|
||||
Exam.status == "submitted"
|
||||
stats["login_count"] = result.scalar() or 0
|
||||
|
||||
# 获取考试统计 - 使用 case 语句
|
||||
# 通过考试数量
|
||||
result = await self.db.execute(
|
||||
select(func.count(Exam.id))
|
||||
.where(
|
||||
Exam.user_id == user_id,
|
||||
Exam.is_passed == True,
|
||||
Exam.status == "submitted"
|
||||
)
|
||||
)
|
||||
)
|
||||
row = result.first()
|
||||
if row:
|
||||
stats["exam_passed"] = row[0] or 0
|
||||
stats["exam_perfect_count"] = int(row[1] or 0)
|
||||
stats["exam_excellent"] = int(row[2] or 0)
|
||||
|
||||
# 获取练习统计
|
||||
result = await self.db.execute(
|
||||
select(
|
||||
func.count(PracticeSession.id),
|
||||
func.sum(PracticeSession.duration_seconds)
|
||||
stats["exam_passed"] = result.scalar() or 0
|
||||
|
||||
# 满分考试数量(score >= 总分,通常是 100)
|
||||
result = await self.db.execute(
|
||||
select(func.count(Exam.id))
|
||||
.where(
|
||||
Exam.user_id == user_id,
|
||||
Exam.status == "submitted",
|
||||
Exam.score >= Exam.total_score
|
||||
)
|
||||
)
|
||||
.where(
|
||||
PracticeSession.user_id == user_id,
|
||||
PracticeSession.status == "completed"
|
||||
stats["exam_perfect_count"] = result.scalar() or 0
|
||||
|
||||
# 优秀考试数量(90分以上)
|
||||
result = await self.db.execute(
|
||||
select(func.count(Exam.id))
|
||||
.where(
|
||||
Exam.user_id == user_id,
|
||||
Exam.status == "submitted",
|
||||
Exam.score >= 90
|
||||
)
|
||||
)
|
||||
)
|
||||
row = result.first()
|
||||
if row:
|
||||
stats["practice_count"] = row[0] or 0
|
||||
total_seconds = row[1] or 0
|
||||
stats["practice_hours"] = total_seconds / 3600
|
||||
|
||||
# 获取陪练统计
|
||||
result = await self.db.execute(
|
||||
select(func.count(TrainingSession.id))
|
||||
.where(
|
||||
TrainingSession.user_id == user_id,
|
||||
TrainingSession.status == "COMPLETED"
|
||||
stats["exam_excellent"] = result.scalar() or 0
|
||||
|
||||
# 获取练习统计(PracticeSession - AI 陪练)
|
||||
result = await self.db.execute(
|
||||
select(
|
||||
func.count(PracticeSession.id),
|
||||
func.coalesce(func.sum(PracticeSession.duration_seconds), 0)
|
||||
)
|
||||
.where(
|
||||
PracticeSession.user_id == user_id,
|
||||
PracticeSession.status == "completed"
|
||||
)
|
||||
)
|
||||
)
|
||||
stats["training_count"] = result.scalar() or 0
|
||||
|
||||
# 检查首次高分陪练
|
||||
result = await self.db.execute(
|
||||
select(func.count(TrainingReport.id))
|
||||
.where(
|
||||
TrainingReport.user_id == user_id,
|
||||
TrainingReport.overall_score >= 90
|
||||
row = result.first()
|
||||
if row:
|
||||
stats["practice_count"] = row[0] or 0
|
||||
total_seconds = row[1] or 0
|
||||
stats["practice_hours"] = float(total_seconds) / 3600.0
|
||||
|
||||
# 获取培训/陪练统计(TrainingSession)
|
||||
result = await self.db.execute(
|
||||
select(func.count(TrainingSession.id))
|
||||
.where(
|
||||
TrainingSession.user_id == user_id,
|
||||
TrainingSession.status == "COMPLETED"
|
||||
)
|
||||
)
|
||||
)
|
||||
stats["first_practice_90"] = 1 if (result.scalar() or 0) > 0 else 0
|
||||
stats["training_count"] = result.scalar() or 0
|
||||
|
||||
# 检查是否有高分陪练(90分以上)
|
||||
result = await self.db.execute(
|
||||
select(func.count(TrainingReport.id))
|
||||
.where(
|
||||
TrainingReport.user_id == user_id,
|
||||
TrainingReport.overall_score >= 90
|
||||
)
|
||||
)
|
||||
high_score_count = result.scalar() or 0
|
||||
stats["first_practice_90"] = 1 if high_score_count > 0 else 0
|
||||
|
||||
logger.debug(f"用户 {user_id} 奖章统计数据: {stats}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户统计数据失败: {e}")
|
||||
|
||||
return stats
|
||||
|
||||
@@ -465,3 +487,100 @@ class BadgeService:
|
||||
|
||||
await self.db.execute(query)
|
||||
await self.db.flush()
|
||||
|
||||
async def check_badges_by_category(
|
||||
self,
|
||||
user_id: int,
|
||||
categories: List[str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
按类别检查并授予奖章(优化触发时机)
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
categories: 要检查的奖章类别列表
|
||||
|
||||
Returns:
|
||||
新获得的奖章列表
|
||||
"""
|
||||
# 获取用户统计数据
|
||||
stats = await self._get_user_stats(user_id)
|
||||
|
||||
# 获取指定类别的奖章定义
|
||||
result = await self.db.execute(
|
||||
select(BadgeDefinition)
|
||||
.where(
|
||||
BadgeDefinition.is_active == True,
|
||||
BadgeDefinition.category.in_(categories)
|
||||
)
|
||||
.order_by(BadgeDefinition.sort_order)
|
||||
)
|
||||
category_badges = list(result.scalars().all())
|
||||
|
||||
# 获取用户已有的奖章
|
||||
result = await self.db.execute(
|
||||
select(UserBadge.badge_id).where(UserBadge.user_id == user_id)
|
||||
)
|
||||
owned_badge_ids = {row[0] for row in result.all()}
|
||||
|
||||
# 检查每个奖章的解锁条件
|
||||
newly_awarded = []
|
||||
for badge in category_badges:
|
||||
if badge.id in owned_badge_ids:
|
||||
continue
|
||||
|
||||
# 检查条件
|
||||
condition_met = self._check_badge_condition(badge, stats)
|
||||
|
||||
if condition_met:
|
||||
# 授予奖章
|
||||
user_badge = UserBadge(
|
||||
user_id=user_id,
|
||||
badge_id=badge.id,
|
||||
unlocked_at=datetime.now(),
|
||||
is_notified=False
|
||||
)
|
||||
self.db.add(user_badge)
|
||||
|
||||
# 如果有经验奖励,添加经验值
|
||||
if badge.exp_reward > 0:
|
||||
from app.services.level_service import LevelService
|
||||
level_service = LevelService(self.db)
|
||||
await level_service.add_exp(
|
||||
user_id=user_id,
|
||||
exp_amount=badge.exp_reward,
|
||||
exp_type=ExpType.BADGE,
|
||||
description=f"解锁奖章「{badge.name}」"
|
||||
)
|
||||
|
||||
newly_awarded.append({
|
||||
"badge_id": badge.id,
|
||||
"code": badge.code,
|
||||
"name": badge.name,
|
||||
"description": badge.description,
|
||||
"icon": badge.icon,
|
||||
"exp_reward": badge.exp_reward,
|
||||
})
|
||||
|
||||
logger.info(f"用户 {user_id} 解锁奖章: {badge.name}")
|
||||
|
||||
if newly_awarded:
|
||||
await self.db.flush()
|
||||
|
||||
return newly_awarded
|
||||
|
||||
async def check_exam_badges(self, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""考试后检查考试类奖章"""
|
||||
return await self.check_badges_by_category(user_id, [BadgeCategory.EXAM])
|
||||
|
||||
async def check_practice_badges(self, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""练习后检查练习类奖章"""
|
||||
return await self.check_badges_by_category(user_id, [BadgeCategory.PRACTICE])
|
||||
|
||||
async def check_streak_badges(self, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""签到后检查连续打卡类奖章"""
|
||||
return await self.check_badges_by_category(user_id, [BadgeCategory.STREAK, BadgeCategory.LEARNING])
|
||||
|
||||
async def check_level_badges(self, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""等级变化后检查等级类奖章"""
|
||||
return await self.check_badges_by_category(user_id, [BadgeCategory.SPECIAL])
|
||||
|
||||
Reference in New Issue
Block a user