feat: KPL v1.5.0 功能迭代
Some checks failed
continuous-integration/drone/push Build is failing

1. 奖章条件优化
- 修复统计查询 SQL 语法
- 添加按类别检查奖章方法

2. 移动端适配
- 登录页、课程中心、课程详情
- 考试页面、成长路径、排行榜

3. 证书系统
- 数据库模型和迁移脚本
- 证书颁发/列表/下载/验证 API
- 前端证书列表页面

4. 数据大屏
- 企业级/团队级数据 API
- ECharts 可视化大屏页面
This commit is contained in:
yuliang_guo
2026-01-29 16:51:17 +08:00
parent 813ba2c295
commit 6f0f2e6363
21 changed files with 4907 additions and 80 deletions

View File

@@ -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])