feat: 新增等级与奖章系统
Some checks failed
continuous-integration/drone/push Build is failing

- 后端: 新增 user_levels, exp_history, badge_definitions, user_badges, level_configs 表
- 后端: 新增 LevelService 和 BadgeService 服务
- 后端: 新增等级/奖章/签到/排行榜 API 端点
- 后端: 考试/练习/陪练完成时触发经验值和奖章检查
- 前端: 新增 LevelBadge, ExpProgress, BadgeCard, LevelUpDialog 组件
- 前端: 新增排行榜页面
- 前端: 成长路径页面集成真实等级数据
- 数据库: 包含迁移脚本和初始数据
This commit is contained in:
yuliang_guo
2026-01-29 16:19:22 +08:00
parent 5dfe23831d
commit 0933b936f9
19 changed files with 3207 additions and 65 deletions

153
backend/app/models/level.py Normal file
View 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" # 时长