- 后端: 新增 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
|
||||
|
||||
@@ -17,6 +17,16 @@ from app.models.practice import PracticeScene, PracticeSession, PracticeDialogue
|
||||
from app.models.system_log import SystemLog
|
||||
from app.models.task import Task, TaskCourse, TaskAssignment
|
||||
from app.models.notification import Notification
|
||||
from app.models.level import (
|
||||
UserLevel,
|
||||
ExpHistory,
|
||||
BadgeDefinition,
|
||||
UserBadge,
|
||||
LevelConfig,
|
||||
ExpType,
|
||||
BadgeCategory,
|
||||
ConditionType,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Base",
|
||||
@@ -46,4 +56,12 @@ __all__ = [
|
||||
"TaskCourse",
|
||||
"TaskAssignment",
|
||||
"Notification",
|
||||
"UserLevel",
|
||||
"ExpHistory",
|
||||
"BadgeDefinition",
|
||||
"UserBadge",
|
||||
"LevelConfig",
|
||||
"ExpType",
|
||||
"BadgeCategory",
|
||||
"ConditionType",
|
||||
]
|
||||
|
||||
153
backend/app/models/level.py
Normal file
153
backend/app/models/level.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
等级与奖章系统模型
|
||||
|
||||
包含:
|
||||
- UserLevel: 用户等级信息
|
||||
- ExpHistory: 经验值变化历史
|
||||
- BadgeDefinition: 奖章定义
|
||||
- UserBadge: 用户已获得的奖章
|
||||
- LevelConfig: 等级配置
|
||||
"""
|
||||
|
||||
from datetime import datetime, date
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Date, Boolean, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class UserLevel(BaseModel):
|
||||
"""用户等级表"""
|
||||
__tablename__ = "user_levels"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
|
||||
level = Column(Integer, nullable=False, default=1, comment="当前等级")
|
||||
exp = Column(Integer, nullable=False, default=0, comment="当前经验值")
|
||||
total_exp = Column(Integer, nullable=False, default=0, comment="累计获得经验值")
|
||||
login_streak = Column(Integer, nullable=False, default=0, comment="连续登录天数")
|
||||
max_login_streak = Column(Integer, nullable=False, default=0, comment="历史最长连续登录天数")
|
||||
last_login_date = Column(Date, nullable=True, comment="最后登录日期")
|
||||
last_checkin_at = Column(DateTime, nullable=True, comment="最后签到时间")
|
||||
|
||||
# 关联
|
||||
user = relationship("User", backref="user_level", uselist=False)
|
||||
|
||||
# 不继承 is_deleted 等软删除字段
|
||||
is_deleted = None
|
||||
deleted_at = None
|
||||
|
||||
|
||||
class ExpHistory(BaseModel):
|
||||
"""经验值历史表"""
|
||||
__tablename__ = "exp_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
exp_change = Column(Integer, nullable=False, comment="经验值变化")
|
||||
exp_type = Column(String(50), nullable=False, index=True, comment="类型:exam/practice/training/task/login/badge/other")
|
||||
source_id = Column(Integer, nullable=True, comment="来源记录ID")
|
||||
description = Column(String(255), nullable=False, comment="描述")
|
||||
level_before = Column(Integer, nullable=True, comment="变化前等级")
|
||||
level_after = Column(Integer, nullable=True, comment="变化后等级")
|
||||
|
||||
# 关联
|
||||
user = relationship("User", backref="exp_histories")
|
||||
|
||||
# 不继承软删除字段
|
||||
is_deleted = None
|
||||
deleted_at = None
|
||||
|
||||
|
||||
class BadgeDefinition(BaseModel):
|
||||
"""奖章定义表"""
|
||||
__tablename__ = "badge_definitions"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
code = Column(String(50), nullable=False, unique=True, comment="奖章编码")
|
||||
name = Column(String(100), nullable=False, comment="奖章名称")
|
||||
description = Column(String(255), nullable=False, comment="奖章描述")
|
||||
icon = Column(String(100), nullable=False, default="Medal", comment="图标名称")
|
||||
category = Column(String(50), nullable=False, index=True, comment="分类")
|
||||
condition_type = Column(String(50), nullable=False, comment="条件类型")
|
||||
condition_field = Column(String(100), nullable=True, comment="条件字段")
|
||||
condition_value = Column(Integer, nullable=False, default=1, comment="条件数值")
|
||||
exp_reward = Column(Integer, nullable=False, default=0, comment="奖励经验值")
|
||||
sort_order = Column(Integer, nullable=False, default=0, comment="排序")
|
||||
is_active = Column(Boolean, nullable=False, default=True, comment="是否启用")
|
||||
|
||||
# 关联
|
||||
user_badges = relationship("UserBadge", back_populates="badge")
|
||||
|
||||
# 不继承软删除字段
|
||||
is_deleted = None
|
||||
deleted_at = None
|
||||
|
||||
|
||||
class UserBadge(BaseModel):
|
||||
"""用户奖章表"""
|
||||
__tablename__ = "user_badges"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
badge_id = Column(Integer, ForeignKey("badge_definitions.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
unlocked_at = Column(DateTime, nullable=False, default=datetime.now, comment="解锁时间")
|
||||
is_notified = Column(Boolean, nullable=False, default=False, comment="是否已通知")
|
||||
notified_at = Column(DateTime, nullable=True, comment="通知时间")
|
||||
|
||||
# 关联
|
||||
user = relationship("User", backref="badges")
|
||||
badge = relationship("BadgeDefinition", back_populates="user_badges")
|
||||
|
||||
# 不继承软删除字段
|
||||
is_deleted = None
|
||||
deleted_at = None
|
||||
|
||||
|
||||
class LevelConfig(BaseModel):
|
||||
"""等级配置表"""
|
||||
__tablename__ = "level_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
level = Column(Integer, nullable=False, unique=True, comment="等级")
|
||||
exp_required = Column(Integer, nullable=False, comment="升到此级所需经验值")
|
||||
total_exp_required = Column(Integer, nullable=False, comment="累计所需经验值")
|
||||
title = Column(String(50), nullable=False, comment="等级称号")
|
||||
color = Column(String(20), nullable=True, comment="等级颜色")
|
||||
|
||||
# 不继承软删除字段
|
||||
is_deleted = None
|
||||
deleted_at = None
|
||||
|
||||
|
||||
# 经验值类型枚举
|
||||
class ExpType:
|
||||
"""经验值类型"""
|
||||
EXAM = "exam" # 考试
|
||||
PRACTICE = "practice" # 练习
|
||||
TRAINING = "training" # 陪练
|
||||
TASK = "task" # 任务
|
||||
LOGIN = "login" # 登录/签到
|
||||
BADGE = "badge" # 奖章奖励
|
||||
OTHER = "other" # 其他
|
||||
|
||||
|
||||
# 奖章分类枚举
|
||||
class BadgeCategory:
|
||||
"""奖章分类"""
|
||||
LEARNING = "learning" # 学习进度
|
||||
EXAM = "exam" # 考试成绩
|
||||
PRACTICE = "practice" # 练习时长
|
||||
STREAK = "streak" # 连续打卡
|
||||
SPECIAL = "special" # 特殊成就
|
||||
|
||||
|
||||
# 条件类型枚举
|
||||
class ConditionType:
|
||||
"""解锁条件类型"""
|
||||
COUNT = "count" # 次数
|
||||
SCORE = "score" # 分数
|
||||
STREAK = "streak" # 连续天数
|
||||
LEVEL = "level" # 等级
|
||||
DURATION = "duration" # 时长
|
||||
467
backend/app/services/badge_service.py
Normal file
467
backend/app/services/badge_service.py
Normal file
@@ -0,0 +1,467 @@
|
||||
"""
|
||||
奖章服务
|
||||
|
||||
提供奖章管理功能:
|
||||
- 获取奖章定义
|
||||
- 检查奖章解锁条件
|
||||
- 授予奖章
|
||||
- 获取用户奖章
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
from sqlalchemy import select, func, and_, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.logger import get_logger
|
||||
from app.models.level import (
|
||||
BadgeDefinition, UserBadge, UserLevel, ExpHistory, ExpType,
|
||||
BadgeCategory, ConditionType
|
||||
)
|
||||
from app.models.exam import Exam
|
||||
from app.models.practice import PracticeSession, PracticeReport
|
||||
from app.models.training import TrainingSession, TrainingReport
|
||||
from app.models.task import TaskAssignment
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BadgeService:
|
||||
"""奖章服务"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
self._badge_definitions: Optional[List[BadgeDefinition]] = None
|
||||
|
||||
async def _get_badge_definitions(self) -> List[BadgeDefinition]:
|
||||
"""获取所有奖章定义(带缓存)"""
|
||||
if self._badge_definitions is None:
|
||||
result = await self.db.execute(
|
||||
select(BadgeDefinition)
|
||||
.where(BadgeDefinition.is_active == True)
|
||||
.order_by(BadgeDefinition.sort_order)
|
||||
)
|
||||
self._badge_definitions = list(result.scalars().all())
|
||||
return self._badge_definitions
|
||||
|
||||
async def get_all_badges(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有奖章定义
|
||||
|
||||
Returns:
|
||||
奖章定义列表
|
||||
"""
|
||||
badges = await self._get_badge_definitions()
|
||||
return [
|
||||
{
|
||||
"id": b.id,
|
||||
"code": b.code,
|
||||
"name": b.name,
|
||||
"description": b.description,
|
||||
"icon": b.icon,
|
||||
"category": b.category,
|
||||
"condition_type": b.condition_type,
|
||||
"condition_value": b.condition_value,
|
||||
"exp_reward": b.exp_reward,
|
||||
}
|
||||
for b in badges
|
||||
]
|
||||
|
||||
async def get_user_badges(self, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取用户已解锁的奖章
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
用户奖章列表
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(UserBadge, BadgeDefinition)
|
||||
.join(BadgeDefinition, UserBadge.badge_id == BadgeDefinition.id)
|
||||
.where(UserBadge.user_id == user_id)
|
||||
.order_by(UserBadge.unlocked_at.desc())
|
||||
)
|
||||
rows = result.all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": user_badge.id,
|
||||
"badge_id": badge.id,
|
||||
"code": badge.code,
|
||||
"name": badge.name,
|
||||
"description": badge.description,
|
||||
"icon": badge.icon,
|
||||
"category": badge.category,
|
||||
"unlocked_at": user_badge.unlocked_at.isoformat() if user_badge.unlocked_at else None,
|
||||
"is_notified": user_badge.is_notified,
|
||||
}
|
||||
for user_badge, badge in rows
|
||||
]
|
||||
|
||||
async def get_user_badges_with_status(self, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有奖章及用户解锁状态
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
所有奖章列表(含解锁状态)
|
||||
"""
|
||||
# 获取所有奖章定义
|
||||
all_badges = await self._get_badge_definitions()
|
||||
|
||||
# 获取用户已解锁的奖章
|
||||
result = await self.db.execute(
|
||||
select(UserBadge).where(UserBadge.user_id == user_id)
|
||||
)
|
||||
user_badges = {ub.badge_id: ub for ub in result.scalars().all()}
|
||||
|
||||
badges_with_status = []
|
||||
for badge in all_badges:
|
||||
user_badge = user_badges.get(badge.id)
|
||||
badges_with_status.append({
|
||||
"id": badge.id,
|
||||
"code": badge.code,
|
||||
"name": badge.name,
|
||||
"description": badge.description,
|
||||
"icon": badge.icon,
|
||||
"category": badge.category,
|
||||
"condition_type": badge.condition_type,
|
||||
"condition_value": badge.condition_value,
|
||||
"exp_reward": badge.exp_reward,
|
||||
"unlocked": user_badge is not None,
|
||||
"unlocked_at": user_badge.unlocked_at.isoformat() if user_badge else None,
|
||||
})
|
||||
|
||||
return badges_with_status
|
||||
|
||||
async def _get_user_stats(self, user_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
获取用户统计数据(用于检查奖章条件)
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
统计数据字典
|
||||
"""
|
||||
stats = {
|
||||
"login_count": 0,
|
||||
"login_streak": 0,
|
||||
"course_completed": 0,
|
||||
"exam_passed": 0,
|
||||
"exam_perfect_count": 0,
|
||||
"exam_excellent": 0,
|
||||
"practice_count": 0,
|
||||
"practice_hours": 0,
|
||||
"training_count": 0,
|
||||
"first_practice_90": 0,
|
||||
"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
|
||||
)
|
||||
)
|
||||
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))
|
||||
)
|
||||
.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)
|
||||
)
|
||||
.where(
|
||||
PracticeSession.user_id == user_id,
|
||||
PracticeSession.status == "completed"
|
||||
)
|
||||
)
|
||||
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["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
|
||||
)
|
||||
)
|
||||
stats["first_practice_90"] = 1 if (result.scalar() or 0) > 0 else 0
|
||||
|
||||
return stats
|
||||
|
||||
async def check_and_award_badges(self, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
检查并授予用户符合条件的奖章
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
新获得的奖章列表
|
||||
"""
|
||||
# 获取用户统计数据
|
||||
stats = await self._get_user_stats(user_id)
|
||||
|
||||
# 获取所有奖章定义
|
||||
all_badges = await self._get_badge_definitions()
|
||||
|
||||
# 获取用户已有的奖章
|
||||
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 all_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
|
||||
|
||||
def _check_badge_condition(self, badge: BadgeDefinition, stats: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
检查奖章解锁条件
|
||||
|
||||
Args:
|
||||
badge: 奖章定义
|
||||
stats: 用户统计数据
|
||||
|
||||
Returns:
|
||||
是否满足条件
|
||||
"""
|
||||
condition_field = badge.condition_field
|
||||
condition_value = badge.condition_value
|
||||
condition_type = badge.condition_type
|
||||
|
||||
if not condition_field:
|
||||
return False
|
||||
|
||||
current_value = stats.get(condition_field, 0)
|
||||
|
||||
if condition_type == ConditionType.COUNT:
|
||||
return current_value >= condition_value
|
||||
elif condition_type == ConditionType.SCORE:
|
||||
return current_value >= condition_value
|
||||
elif condition_type == ConditionType.STREAK:
|
||||
return current_value >= condition_value
|
||||
elif condition_type == ConditionType.LEVEL:
|
||||
return current_value >= condition_value
|
||||
elif condition_type == ConditionType.DURATION:
|
||||
return current_value >= condition_value
|
||||
|
||||
return False
|
||||
|
||||
async def award_badge(self, user_id: int, badge_code: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
直接授予用户奖章(用于特殊奖章)
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
badge_code: 奖章编码
|
||||
|
||||
Returns:
|
||||
奖章信息(如果成功)
|
||||
"""
|
||||
# 获取奖章定义
|
||||
result = await self.db.execute(
|
||||
select(BadgeDefinition).where(BadgeDefinition.code == badge_code)
|
||||
)
|
||||
badge = result.scalar_one_or_none()
|
||||
|
||||
if not badge:
|
||||
logger.warning(f"奖章不存在: {badge_code}")
|
||||
return None
|
||||
|
||||
# 检查是否已拥有
|
||||
result = await self.db.execute(
|
||||
select(UserBadge).where(
|
||||
UserBadge.user_id == user_id,
|
||||
UserBadge.badge_id == badge.id
|
||||
)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
logger.info(f"用户 {user_id} 已拥有奖章: {badge_code}")
|
||||
return None
|
||||
|
||||
# 授予奖章
|
||||
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}」"
|
||||
)
|
||||
|
||||
await self.db.flush()
|
||||
|
||||
logger.info(f"用户 {user_id} 获得奖章: {badge.name}")
|
||||
|
||||
return {
|
||||
"badge_id": badge.id,
|
||||
"code": badge.code,
|
||||
"name": badge.name,
|
||||
"description": badge.description,
|
||||
"icon": badge.icon,
|
||||
"exp_reward": badge.exp_reward,
|
||||
}
|
||||
|
||||
async def get_unnotified_badges(self, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取用户未通知的新奖章
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
未通知的奖章列表
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(UserBadge, BadgeDefinition)
|
||||
.join(BadgeDefinition, UserBadge.badge_id == BadgeDefinition.id)
|
||||
.where(
|
||||
UserBadge.user_id == user_id,
|
||||
UserBadge.is_notified == False
|
||||
)
|
||||
.order_by(UserBadge.unlocked_at.desc())
|
||||
)
|
||||
rows = result.all()
|
||||
|
||||
return [
|
||||
{
|
||||
"user_badge_id": user_badge.id,
|
||||
"badge_id": badge.id,
|
||||
"code": badge.code,
|
||||
"name": badge.name,
|
||||
"description": badge.description,
|
||||
"icon": badge.icon,
|
||||
"exp_reward": badge.exp_reward,
|
||||
"unlocked_at": user_badge.unlocked_at.isoformat() if user_badge.unlocked_at else None,
|
||||
}
|
||||
for user_badge, badge in rows
|
||||
]
|
||||
|
||||
async def mark_badges_notified(self, user_id: int, badge_ids: List[int] = None):
|
||||
"""
|
||||
标记奖章为已通知
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
badge_ids: 要标记的奖章ID列表(为空则标记全部)
|
||||
"""
|
||||
from sqlalchemy import update
|
||||
|
||||
query = update(UserBadge).where(
|
||||
UserBadge.user_id == user_id,
|
||||
UserBadge.is_notified == False
|
||||
)
|
||||
|
||||
if badge_ids:
|
||||
query = query.where(UserBadge.badge_id.in_(badge_ids))
|
||||
|
||||
query = query.values(
|
||||
is_notified=True,
|
||||
notified_at=datetime.now()
|
||||
)
|
||||
|
||||
await self.db.execute(query)
|
||||
await self.db.flush()
|
||||
588
backend/app/services/level_service.py
Normal file
588
backend/app/services/level_service.py
Normal file
@@ -0,0 +1,588 @@
|
||||
"""
|
||||
等级服务
|
||||
|
||||
提供用户等级管理功能:
|
||||
- 经验值获取与计算
|
||||
- 等级升级判断
|
||||
- 每日签到
|
||||
- 排行榜查询
|
||||
"""
|
||||
|
||||
from datetime import datetime, date, timedelta
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
from sqlalchemy import select, func, and_, desc
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.logger import get_logger
|
||||
from app.models.level import (
|
||||
UserLevel, ExpHistory, LevelConfig, ExpType
|
||||
)
|
||||
from app.models.user import User
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# 经验值配置
|
||||
EXP_CONFIG = {
|
||||
# 每日签到
|
||||
"login_base": 10, # 基础签到经验
|
||||
"login_streak_7": 20, # 连续7天额外奖励
|
||||
"login_streak_30": 50, # 连续30天额外奖励
|
||||
|
||||
# 考试
|
||||
"exam_pass": 50, # 通过考试
|
||||
"exam_excellent": 30, # 90分以上额外
|
||||
"exam_perfect": 50, # 满分额外
|
||||
|
||||
# 练习
|
||||
"practice_complete": 20, # 完成练习
|
||||
"practice_good": 10, # 80分以上额外
|
||||
|
||||
# 陪练
|
||||
"training_complete": 30, # 完成陪练
|
||||
"training_good": 15, # 80分以上额外
|
||||
|
||||
# 任务
|
||||
"task_complete": 40, # 完成任务
|
||||
}
|
||||
|
||||
|
||||
class LevelService:
|
||||
"""等级服务"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
self._level_configs: Optional[List[LevelConfig]] = None
|
||||
|
||||
async def _get_level_configs(self) -> List[LevelConfig]:
|
||||
"""获取等级配置(带缓存)"""
|
||||
if self._level_configs is None:
|
||||
result = await self.db.execute(
|
||||
select(LevelConfig).order_by(LevelConfig.level)
|
||||
)
|
||||
self._level_configs = list(result.scalars().all())
|
||||
return self._level_configs
|
||||
|
||||
async def get_or_create_user_level(self, user_id: int) -> UserLevel:
|
||||
"""
|
||||
获取或创建用户等级记录
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
UserLevel 对象
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(UserLevel).where(UserLevel.user_id == user_id)
|
||||
)
|
||||
user_level = result.scalar_one_or_none()
|
||||
|
||||
if not user_level:
|
||||
user_level = UserLevel(
|
||||
user_id=user_id,
|
||||
level=1,
|
||||
exp=0,
|
||||
total_exp=0,
|
||||
login_streak=0,
|
||||
max_login_streak=0
|
||||
)
|
||||
self.db.add(user_level)
|
||||
await self.db.flush()
|
||||
logger.info(f"为用户 {user_id} 创建等级记录")
|
||||
|
||||
return user_level
|
||||
|
||||
async def get_user_level_info(self, user_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
获取用户等级详细信息
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
包含等级、经验值、称号等信息的字典
|
||||
"""
|
||||
user_level = await self.get_or_create_user_level(user_id)
|
||||
level_configs = await self._get_level_configs()
|
||||
|
||||
# 获取当前等级配置
|
||||
current_config = next(
|
||||
(c for c in level_configs if c.level == user_level.level),
|
||||
level_configs[0] if level_configs else None
|
||||
)
|
||||
|
||||
# 获取下一等级配置
|
||||
next_config = next(
|
||||
(c for c in level_configs if c.level == user_level.level + 1),
|
||||
None
|
||||
)
|
||||
|
||||
# 计算升级所需经验
|
||||
exp_to_next_level = 0
|
||||
next_level_total_exp = 0
|
||||
if next_config:
|
||||
next_level_total_exp = next_config.total_exp_required
|
||||
exp_to_next_level = next_level_total_exp - user_level.total_exp
|
||||
if exp_to_next_level < 0:
|
||||
exp_to_next_level = 0
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"level": user_level.level,
|
||||
"exp": user_level.exp,
|
||||
"total_exp": user_level.total_exp,
|
||||
"title": current_config.title if current_config else "初学者",
|
||||
"color": current_config.color if current_config else "#909399",
|
||||
"login_streak": user_level.login_streak,
|
||||
"max_login_streak": user_level.max_login_streak,
|
||||
"last_checkin_at": user_level.last_checkin_at.isoformat() if user_level.last_checkin_at else None,
|
||||
"next_level_exp": next_level_total_exp,
|
||||
"exp_to_next_level": exp_to_next_level,
|
||||
"is_max_level": next_config is None,
|
||||
}
|
||||
|
||||
async def add_exp(
|
||||
self,
|
||||
user_id: int,
|
||||
exp_amount: int,
|
||||
exp_type: str,
|
||||
description: str,
|
||||
source_id: Optional[int] = None
|
||||
) -> Tuple[UserLevel, bool, Optional[int]]:
|
||||
"""
|
||||
增加用户经验值
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
exp_amount: 经验值数量
|
||||
exp_type: 经验值类型
|
||||
description: 描述
|
||||
source_id: 来源ID(可选)
|
||||
|
||||
Returns:
|
||||
(用户等级对象, 是否升级, 新等级)
|
||||
"""
|
||||
if exp_amount <= 0:
|
||||
logger.warning(f"尝试增加非正数经验值: {exp_amount}")
|
||||
return await self.get_or_create_user_level(user_id), False, None
|
||||
|
||||
user_level = await self.get_or_create_user_level(user_id)
|
||||
level_before = user_level.level
|
||||
|
||||
# 增加经验值
|
||||
user_level.exp += exp_amount
|
||||
user_level.total_exp += exp_amount
|
||||
|
||||
# 检查是否升级
|
||||
level_configs = await self._get_level_configs()
|
||||
leveled_up = False
|
||||
new_level = None
|
||||
|
||||
for config in level_configs:
|
||||
if config.level > user_level.level and user_level.total_exp >= config.total_exp_required:
|
||||
user_level.level = config.level
|
||||
leveled_up = True
|
||||
new_level = config.level
|
||||
|
||||
# 记录经验值历史
|
||||
exp_history = ExpHistory(
|
||||
user_id=user_id,
|
||||
exp_change=exp_amount,
|
||||
exp_type=exp_type,
|
||||
source_id=source_id,
|
||||
description=description,
|
||||
level_before=level_before,
|
||||
level_after=user_level.level
|
||||
)
|
||||
self.db.add(exp_history)
|
||||
|
||||
await self.db.flush()
|
||||
|
||||
if leveled_up:
|
||||
logger.info(f"用户 {user_id} 升级: {level_before} -> {new_level}")
|
||||
|
||||
logger.info(f"用户 {user_id} 获得 {exp_amount} 经验值: {description}")
|
||||
|
||||
return user_level, leveled_up, new_level
|
||||
|
||||
async def daily_checkin(self, user_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
每日签到
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
签到结果
|
||||
"""
|
||||
user_level = await self.get_or_create_user_level(user_id)
|
||||
today = date.today()
|
||||
|
||||
# 检查今天是否已签到
|
||||
if user_level.last_login_date == today:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "今天已经签到过了",
|
||||
"exp_gained": 0,
|
||||
"login_streak": user_level.login_streak,
|
||||
"already_checked_in": True
|
||||
}
|
||||
|
||||
# 计算连续登录
|
||||
yesterday = today - timedelta(days=1)
|
||||
if user_level.last_login_date == yesterday:
|
||||
# 连续登录
|
||||
user_level.login_streak += 1
|
||||
else:
|
||||
# 中断了,重新计算
|
||||
user_level.login_streak = 1
|
||||
|
||||
# 更新最长连续登录记录
|
||||
if user_level.login_streak > user_level.max_login_streak:
|
||||
user_level.max_login_streak = user_level.login_streak
|
||||
|
||||
user_level.last_login_date = today
|
||||
user_level.last_checkin_at = datetime.now()
|
||||
|
||||
# 计算签到经验
|
||||
exp_gained = EXP_CONFIG["login_base"]
|
||||
bonus_exp = 0
|
||||
bonus_reason = []
|
||||
|
||||
# 连续登录奖励
|
||||
if user_level.login_streak >= 30 and user_level.login_streak % 30 == 0:
|
||||
bonus_exp += EXP_CONFIG["login_streak_30"]
|
||||
bonus_reason.append(f"连续{user_level.login_streak}天")
|
||||
elif user_level.login_streak >= 7 and user_level.login_streak % 7 == 0:
|
||||
bonus_exp += EXP_CONFIG["login_streak_7"]
|
||||
bonus_reason.append(f"连续{user_level.login_streak}天")
|
||||
|
||||
total_exp = exp_gained + bonus_exp
|
||||
|
||||
# 添加经验值
|
||||
description = f"每日签到"
|
||||
if bonus_reason:
|
||||
description += f"({', '.join(bonus_reason)}奖励)"
|
||||
|
||||
_, leveled_up, new_level = await self.add_exp(
|
||||
user_id=user_id,
|
||||
exp_amount=total_exp,
|
||||
exp_type=ExpType.LOGIN,
|
||||
description=description
|
||||
)
|
||||
|
||||
await self.db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "签到成功",
|
||||
"exp_gained": total_exp,
|
||||
"base_exp": exp_gained,
|
||||
"bonus_exp": bonus_exp,
|
||||
"login_streak": user_level.login_streak,
|
||||
"leveled_up": leveled_up,
|
||||
"new_level": new_level,
|
||||
"already_checked_in": False
|
||||
}
|
||||
|
||||
async def get_exp_history(
|
||||
self,
|
||||
user_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
exp_type: Optional[str] = None
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
"""
|
||||
获取经验值历史
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
limit: 限制数量
|
||||
offset: 偏移量
|
||||
exp_type: 类型筛选
|
||||
|
||||
Returns:
|
||||
(历史记录列表, 总数)
|
||||
"""
|
||||
# 构建查询
|
||||
query = select(ExpHistory).where(ExpHistory.user_id == user_id)
|
||||
|
||||
if exp_type:
|
||||
query = query.where(ExpHistory.exp_type == exp_type)
|
||||
|
||||
# 获取总数
|
||||
count_query = select(func.count(ExpHistory.id)).where(ExpHistory.user_id == user_id)
|
||||
if exp_type:
|
||||
count_query = count_query.where(ExpHistory.exp_type == exp_type)
|
||||
total_result = await self.db.execute(count_query)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# 获取记录
|
||||
query = query.order_by(desc(ExpHistory.created_at)).limit(limit).offset(offset)
|
||||
result = await self.db.execute(query)
|
||||
records = result.scalars().all()
|
||||
|
||||
history = [
|
||||
{
|
||||
"id": r.id,
|
||||
"exp_change": r.exp_change,
|
||||
"exp_type": r.exp_type,
|
||||
"description": r.description,
|
||||
"level_before": r.level_before,
|
||||
"level_after": r.level_after,
|
||||
"created_at": r.created_at.isoformat() if r.created_at else None
|
||||
}
|
||||
for r in records
|
||||
]
|
||||
|
||||
return history, total
|
||||
|
||||
async def get_leaderboard(
|
||||
self,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
"""
|
||||
获取等级排行榜
|
||||
|
||||
Args:
|
||||
limit: 限制数量
|
||||
offset: 偏移量
|
||||
|
||||
Returns:
|
||||
(排行榜列表, 总数)
|
||||
"""
|
||||
# 获取总数
|
||||
count_query = select(func.count(UserLevel.id))
|
||||
total_result = await self.db.execute(count_query)
|
||||
total = total_result.scalar() or 0
|
||||
|
||||
# 获取排行榜(按等级和总经验值排序)
|
||||
query = (
|
||||
select(UserLevel, User)
|
||||
.join(User, UserLevel.user_id == User.id)
|
||||
.where(User.is_deleted == False)
|
||||
.order_by(desc(UserLevel.level), desc(UserLevel.total_exp))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
rows = result.all()
|
||||
|
||||
# 获取等级配置
|
||||
level_configs = await self._get_level_configs()
|
||||
config_map = {c.level: c for c in level_configs}
|
||||
|
||||
leaderboard = []
|
||||
for i, (user_level, user) in enumerate(rows):
|
||||
config = config_map.get(user_level.level)
|
||||
leaderboard.append({
|
||||
"rank": offset + i + 1,
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"level": user_level.level,
|
||||
"title": config.title if config else "初学者",
|
||||
"color": config.color if config else "#909399",
|
||||
"total_exp": user_level.total_exp,
|
||||
"login_streak": user_level.login_streak,
|
||||
})
|
||||
|
||||
return leaderboard, total
|
||||
|
||||
async def get_user_rank(self, user_id: int) -> Optional[int]:
|
||||
"""
|
||||
获取用户排名
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
排名(从1开始)
|
||||
"""
|
||||
user_level = await self.get_or_create_user_level(user_id)
|
||||
|
||||
# 计算排在该用户前面的人数
|
||||
query = select(func.count(UserLevel.id)).where(
|
||||
and_(
|
||||
UserLevel.user_id != user_id,
|
||||
(
|
||||
(UserLevel.level > user_level.level) |
|
||||
(
|
||||
(UserLevel.level == user_level.level) &
|
||||
(UserLevel.total_exp > user_level.total_exp)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
count = result.scalar() or 0
|
||||
|
||||
return count + 1
|
||||
|
||||
async def add_exam_exp(
|
||||
self,
|
||||
user_id: int,
|
||||
exam_id: int,
|
||||
score: float,
|
||||
is_passed: bool
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
考试通过获得经验值
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
exam_id: 考试ID
|
||||
score: 得分
|
||||
is_passed: 是否通过
|
||||
|
||||
Returns:
|
||||
经验值变化信息
|
||||
"""
|
||||
if not is_passed:
|
||||
return None
|
||||
|
||||
exp_gained = EXP_CONFIG["exam_pass"]
|
||||
bonus = []
|
||||
|
||||
if score >= 100:
|
||||
exp_gained += EXP_CONFIG["exam_perfect"]
|
||||
bonus.append("满分")
|
||||
elif score >= 90:
|
||||
exp_gained += EXP_CONFIG["exam_excellent"]
|
||||
bonus.append("优秀")
|
||||
|
||||
description = f"通过考试"
|
||||
if bonus:
|
||||
description += f"({', '.join(bonus)}奖励)"
|
||||
|
||||
_, leveled_up, new_level = await self.add_exp(
|
||||
user_id=user_id,
|
||||
exp_amount=exp_gained,
|
||||
exp_type=ExpType.EXAM,
|
||||
description=description,
|
||||
source_id=exam_id
|
||||
)
|
||||
|
||||
return {
|
||||
"exp_gained": exp_gained,
|
||||
"leveled_up": leveled_up,
|
||||
"new_level": new_level
|
||||
}
|
||||
|
||||
async def add_practice_exp(
|
||||
self,
|
||||
user_id: int,
|
||||
session_id: int,
|
||||
score: Optional[float] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
完成练习获得经验值
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
session_id: 练习会话ID
|
||||
score: 得分(可选)
|
||||
|
||||
Returns:
|
||||
经验值变化信息
|
||||
"""
|
||||
exp_gained = EXP_CONFIG["practice_complete"]
|
||||
bonus = []
|
||||
|
||||
if score is not None and score >= 80:
|
||||
exp_gained += EXP_CONFIG["practice_good"]
|
||||
bonus.append("高分")
|
||||
|
||||
description = f"完成练习"
|
||||
if bonus:
|
||||
description += f"({', '.join(bonus)}奖励)"
|
||||
|
||||
_, leveled_up, new_level = await self.add_exp(
|
||||
user_id=user_id,
|
||||
exp_amount=exp_gained,
|
||||
exp_type=ExpType.PRACTICE,
|
||||
description=description,
|
||||
source_id=session_id
|
||||
)
|
||||
|
||||
return {
|
||||
"exp_gained": exp_gained,
|
||||
"leveled_up": leveled_up,
|
||||
"new_level": new_level
|
||||
}
|
||||
|
||||
async def add_training_exp(
|
||||
self,
|
||||
user_id: int,
|
||||
session_id: int,
|
||||
score: Optional[float] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
完成陪练获得经验值
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
session_id: 陪练会话ID
|
||||
score: 得分(可选)
|
||||
|
||||
Returns:
|
||||
经验值变化信息
|
||||
"""
|
||||
exp_gained = EXP_CONFIG["training_complete"]
|
||||
bonus = []
|
||||
|
||||
if score is not None and score >= 80:
|
||||
exp_gained += EXP_CONFIG["training_good"]
|
||||
bonus.append("高分")
|
||||
|
||||
description = f"完成陪练"
|
||||
if bonus:
|
||||
description += f"({', '.join(bonus)}奖励)"
|
||||
|
||||
_, leveled_up, new_level = await self.add_exp(
|
||||
user_id=user_id,
|
||||
exp_amount=exp_gained,
|
||||
exp_type=ExpType.TRAINING,
|
||||
description=description,
|
||||
source_id=session_id
|
||||
)
|
||||
|
||||
return {
|
||||
"exp_gained": exp_gained,
|
||||
"leveled_up": leveled_up,
|
||||
"new_level": new_level
|
||||
}
|
||||
|
||||
async def add_task_exp(
|
||||
self,
|
||||
user_id: int,
|
||||
task_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
完成任务获得经验值
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
经验值变化信息
|
||||
"""
|
||||
exp_gained = EXP_CONFIG["task_complete"]
|
||||
|
||||
_, leveled_up, new_level = await self.add_exp(
|
||||
user_id=user_id,
|
||||
exp_amount=exp_gained,
|
||||
exp_type=ExpType.TASK,
|
||||
description="完成任务",
|
||||
source_id=task_id
|
||||
)
|
||||
|
||||
return {
|
||||
"exp_gained": exp_gained,
|
||||
"leveled_up": leveled_up,
|
||||
"new_level": new_level
|
||||
}
|
||||
Reference in New Issue
Block a user