本文目录导读:

我来详细介绍如何在PHP项目中实现一个完整的用户勋章系统。
数据库设计
首先设计勋章系统的数据库表结构:
-- 勋章定义表
CREATE TABLE badges (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL COMMENT '勋章名称',
description VARCHAR(255) COMMENT '勋章描述',
icon VARCHAR(100) COMMENT '勋章图标路径/类名',
category VARCHAR(50) COMMENT '分类(如: 活动, 成就, 等级)',
criteria JSON COMMENT '获取条件(存储为JSON)',
rarity ENUM('common', 'rare', 'epic', 'legendary') DEFAULT 'common',
is_active TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 用户勋章关联表
CREATE TABLE user_badges (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
badge_id INT NOT NULL,
earned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
progress JSON COMMENT '进度记录(如: 已完成50%)',
is_displayed TINYINT(1) DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (badge_id) REFERENCES badges(id),
UNIQUE KEY unique_user_badge (user_id, badge_id)
);
-- 用户活动日志表(用于检测条件)
CREATE TABLE user_activity_log (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
activity_type VARCHAR(50) COMMENT '活动类型',
activity_data JSON COMMENT '活动详情',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_activity (user_id, activity_type)
);
勋章系统核心类
<?php
namespace App\Services;
class BadgeSystem
{
private $db;
private $badgeRules = [];
public function __construct($db)
{
$this->db = $db;
$this->loadBadgeRules();
}
/**
* 加载所有勋章规则
*/
private function loadBadgeRules()
{
$this->badgeRules = [
'first_login' => [
'class' => 'FirstLoginBadge',
'handler' => function($userId) {
return $this->checkFirstLogin($userId);
}
],
'login_streak' => [
'class' => 'LoginStreakBadge',
'handler' => function($userId, $days) {
return $this->checkLoginStreak($userId, $days);
}
],
'post_count' => [
'class' => 'PostCountBadge',
'handler' => function($userId, $count) {
return $this->checkPostCount($userId, $count);
}
],
'comment_count' => [
'class' => 'CommentCountBadge',
'handler' => function($userId, $count) {
return $this->checkCommentCount($userId, $count);
}
],
'level_reached' => [
'class' => 'LevelReachedBadge',
'handler' => function($userId, $level) {
return $this->checkLevelReached($userId, $level);
}
],
'points_earned' => [
'class' => 'PointsEarnedBadge',
'handler' => function($userId, $points) {
return $this->checkPointsEarned($userId, $points);
}
]
];
}
/**
* 检测并授予勋章
*/
public function checkAndAward($userId, $eventType, $eventData = [])
{
$awardedBadges = [];
// 获取适用的勋章
$badges = $this->getApplicableBadges($eventType);
foreach ($badges as $badge) {
// 检查用户是否已有该勋章
if ($this->hasBadge($userId, $badge['id'])) {
continue;
}
// 检测条件是否满足
$criteria = json_decode($badge['criteria'], true);
$condition = $criteria['condition'] ?? '';
$value = $criteria['value'] ?? 0;
if ($this->evaluateCondition($userId, $condition, $value, $eventData)) {
$this->awardBadge($userId, $badge['id']);
$awardedBadges[] = $badge;
}
}
return $awardedBadges;
}
/**
* 获取适用的勋章
*/
private function getApplicableBadges($eventType)
{
$query = "SELECT * FROM badges WHERE is_active = 1";
// 根据事件类型过滤(这里简化处理)
$result = $this->db->query($query);
return $result->fetch_all(MYSQLI_ASSOC);
}
/**
* 评估条件
*/
private function evaluateCondition($userId, $condition, $value, $eventData)
{
switch ($condition) {
case 'login_count':
return $this->checkLoginCount($userId) >= $value;
case 'post_count':
return $this->checkPostCount($userId) >= $value;
case 'comment_count':
return $this->checkCommentCount($userId) >= $value;
case 'login_streak':
return $this->checkLoginStreak($userId) >= $value;
case 'points':
return $this->getUserPoints($userId) >= $value;
case 'level':
return $this->getUserLevel($userId) >= $value;
case 'register_days':
return $this->getRegisterDays($userId) >= $value;
case 'custom_event':
return $this->checkCustomEvent($userId, $eventData);
default:
return false;
}
}
/**
* 授予勋章
*/
public function awardBadge($userId, $badgeId)
{
$query = "INSERT INTO user_badges (user_id, badge_id, earned_at, is_displayed)
VALUES (?, ?, NOW(), 1)";
$stmt = $this->db->prepare($query);
$stmt->bind_param("ii", $userId, $badgeId);
if ($stmt->execute()) {
// 通知用户
$this->notifyUser($userId, $badgeId);
return true;
}
return false;
}
/**
* 检查用户是否已有勋章
*/
public function hasBadge($userId, $badgeId)
{
$query = "SELECT id FROM user_badges
WHERE user_id = ? AND badge_id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("ii", $userId, $badgeId);
$stmt->execute();
return $stmt->get_result()->num_rows > 0;
}
/**
* 获取用户所有勋章
*/
public function getUserBadges($userId, $includeProgress = false)
{
$query = "SELECT b.*, ub.earned_at, ub.is_displayed, ub.progress
FROM badges b
JOIN user_badges ub ON b.id = ub.badge_id
WHERE ub.user_id = ?
ORDER BY ub.earned_at DESC";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
$badges = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
if ($includeProgress) {
foreach ($badges as &$badge) {
$badge['progress_data'] = json_decode($badge['progress'], true);
}
}
return $badges;
}
/**
* 获取用户勋章统计
*/
public function getUserBadgeStats($userId)
{
$stats = [
'total' => 0,
'by_rarity' => [],
'recent' => []
];
// 总数
$query = "SELECT COUNT(*) as total FROM user_badges WHERE user_id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
$stats['total'] = $stmt->get_result()->fetch_assoc()['total'];
// 按稀有度统计
$query = "SELECT b.rarity, COUNT(*) as count
FROM user_badges ub
JOIN badges b ON ub.badge_id = b.id
WHERE ub.user_id = ?
GROUP BY b.rarity";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
while ($row = $stmt->get_result()->fetch_assoc()) {
$stats['by_rarity'][$row['rarity']] = $row['count'];
}
// 最近获得的5个勋章
$query = "SELECT b.*, ub.earned_at
FROM user_badges ub
JOIN badges b ON ub.badge_id = b.id
WHERE ub.user_id = ?
ORDER BY ub.earned_at DESC
LIMIT 5";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
$stats['recent'] = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
return $stats;
}
/**
* 获取所有可获得的勋章(包括未获得的)
*/
public function getAllBadgesWithProgress($userId)
{
$query = "SELECT b.*, ub.earned_at, ub.progress,
CASE WHEN ub.id IS NOT NULL THEN 1 ELSE 0 END as is_earned
FROM badges b
LEFT JOIN user_badges ub ON b.id = ub.badge_id AND ub.user_id = ?
WHERE b.is_active = 1
ORDER BY b.category, b.name";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
/**
* 更新勋章进度
*/
public function updateBadgeProgress($userId, $badgeId, $progress, $total)
{
$progressData = json_encode([
'current' => $progress,
'total' => $total,
'percentage' => ($total > 0) ? round(($progress / $total) * 100, 2) : 0
]);
$query = "UPDATE user_badges SET progress = ?
WHERE user_id = ? AND badge_id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("sii", $progressData, $userId, $badgeId);
return $stmt->execute();
}
/**
* 设置勋章显示状态
*/
public function toggleBadgeDisplay($userId, $badgeId)
{
$query = "UPDATE user_badges
SET is_displayed = NOT is_displayed
WHERE user_id = ? AND badge_id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("ii", $userId, $badgeId);
return $stmt->execute();
}
/**
* 通知用户获得勋章
*/
private function notifyUser($userId, $badgeId)
{
// 获取勋章信息
$query = "SELECT name, description, icon FROM badges WHERE id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $badgeId);
$stmt->execute();
$badge = $stmt->get_result()->fetch_assoc();
// 创建通知
$notificationService = new NotificationService($this->db);
$notificationService->createNotification(
$userId,
'badge_earned',
"恭喜你获得了「{$badge['name']}」勋章!",
['badge_id' => $badgeId, 'badge_icon' => $badge['icon']]
);
}
// 辅助检查方法
private function checkLoginCount($userId)
{
$query = "SELECT COUNT(*) as count FROM user_activity_log
WHERE user_id = ? AND activity_type = 'login'";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
return $stmt->get_result()->fetch_assoc()['count'];
}
private function checkLoginStreak($userId)
{
// 计算连续登录天数
$query = "SELECT DISTINCT DATE(created_at) as login_date
FROM user_activity_log
WHERE user_id = ? AND activity_type = 'login'
ORDER BY login_date DESC";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();
$streak = 0;
$expectedDate = new DateTime();
while ($row = $result->fetch_assoc()) {
$loginDate = new DateTime($row['login_date']);
if ($loginDate->format('Y-m-d') === $expectedDate->format('Y-m-d')) {
$streak++;
$expectedDate->modify('-1 day');
} else {
break;
}
}
return $streak;
}
private function checkPostCount($userId)
{
// 假设有一个posts表
$query = "SELECT COUNT(*) as count FROM posts WHERE user_id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
return $stmt->get_result()->fetch_assoc()['count'];
}
private function getUserPoints($userId)
{
$query = "SELECT SUM(points) as total FROM user_points WHERE user_id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
return $stmt->get_result()->fetch_assoc()['total'] ?? 0;
}
private function getUserLevel($userId)
{
$query = "SELECT level FROM users WHERE id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
return $stmt->get_result()->fetch_assoc()['level'] ?? 1;
}
private function getRegisterDays($userId)
{
$query = "SELECT DATEDIFF(NOW(), created_at) as days FROM users WHERE id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $userId);
$stmt->execute();
return $stmt->get_result()->fetch_assoc()['days'] ?? 0;
}
}
勋章事件监听器
<?php
namespace App\Services;
class BadgeEventDispatcher
{
private $badgeSystem;
private $events = [];
public function __construct($badgeSystem)
{
$this->badgeSystem = $badgeSystem;
$this->registerEvents();
}
private function registerEvents()
{
$this->events = [
'user.login' => 'onUserLogin',
'user.register' => 'onUserRegister',
'post.created' => 'onPostCreated',
'comment.created' => 'onCommentCreated',
'user.level_up' => 'onLevelUp',
'user.points_earned' => 'onPointsEarned',
'user.like_received' => 'onLikeReceived',
'achievement.completed' => 'onAchievementCompleted'
];
}
public function dispatch($event, $userId, $data = [])
{
if (isset($this->events[$event])) {
$handler = $this->events[$event];
$this->$handler($userId, $data);
}
}
private function onUserLogin($userId, $data)
{
// 记录登录活动
$this->logActivity($userId, 'login', $data);
// 检查登录相关勋章
$this->badgeSystem->checkAndAward($userId, 'login', $data);
}
private function onUserRegister($userId, $data)
{
// 注册时检查首次登录勋章
$this->badgeSystem->checkAndAward($userId, 'register', $data);
}
private function onPostCreated($userId, $data)
{
// 记录发帖活动
$this->logActivity($userId, 'post', $data);
// 检查发帖相关勋章
$this->badgeSystem->checkAndAward($userId, 'post', $data);
}
private function onLevelUp($userId, $data)
{
$this->badgeSystem->checkAndAward($userId, 'level', $data);
}
private function logActivity($userId, $type, $data)
{
$query = "INSERT INTO user_activity_log (user_id, activity_type, activity_data)
VALUES (?, ?, ?)";
$stmt = $this->badgeSystem->db->prepare($query);
$activityData = json_encode($data);
$stmt->bind_param("iss", $userId, $type, $activityData);
$stmt->execute();
}
}
勋章管理员类
<?php
namespace App\Admin;
class BadgeManager
{
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* 创建新勋章
*/
public function createBadge($data)
{
$query = "INSERT INTO badges (name, description, icon, category, criteria, rarity)
VALUES (?, ?, ?, ?, ?, ?)";
$stmt = $this->db->prepare($query);
$criteria = json_encode($data['criteria']);
$stmt->bind_param("ssssss",
$data['name'],
$data['description'],
$data['icon'],
$data['category'],
$criteria,
$data['rarity']
);
return $stmt->execute();
}
/**
* 更新勋章
*/
public function updateBadge($badgeId, $data)
{
$fields = [];
$params = [];
$types = '';
foreach (['name', 'description', 'icon', 'category', 'rarity'] as $field) {
if (isset($data[$field])) {
$fields[] = "$field = ?";
$params[] = $data[$field];
$types .= 's';
}
}
if (isset($data['criteria'])) {
$fields[] = "criteria = ?";
$params[] = json_encode($data['criteria']);
$types .= 's';
}
if (!empty($fields)) {
$params[] = $badgeId;
$types .= 'i';
$query = "UPDATE badges SET " . implode(', ', $fields) . " WHERE id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param($types, ...$params);
return $stmt->execute();
}
return false;
}
/**
* 批量授予勋章
*/
public function massAward($badgeId, $userIds)
{
$success = 0;
$query = "INSERT IGNORE INTO user_badges (user_id, badge_id, earned_at) VALUES (?, ?, NOW())";
$stmt = $this->db->prepare($query);
foreach ($userIds as $userId) {
$stmt->bind_param("ii", $userId, $badgeId);
if ($stmt->execute()) {
$success++;
}
}
return $success;
}
/**
* 获取勋章统计数据
*/
public function getBadgeStats()
{
$stats = [
'total_badges' => 0,
'total_awarded' => 0,
'most_earned' => [],
'least_earned' => []
];
// 总勋章数
$query = "SELECT COUNT(*) as total FROM badges WHERE is_active = 1";
$stats['total_badges'] = $this->db->query($query)->fetch_assoc()['total'];
// 总授予次数
$query = "SELECT COUNT(*) as total FROM user_badges";
$stats['total_awarded'] = $this->db->query($query)->fetch_assoc()['total'];
// 最多获得的勋章
$query = "SELECT b.name, COUNT(ub.id) as count
FROM badges b
JOIN user_badges ub ON b.id = ub.badge_id
GROUP BY b.id
ORDER BY count DESC
LIMIT 5";
$stats['most_earned'] = $this->db->query($query)->fetch_all(MYSQLI_ASSOC);
// 最少获得的勋章
$query = "SELECT b.name, COUNT(ub.id) as count
FROM badges b
LEFT JOIN user_badges ub ON b.id = ub.badge_id
GROUP BY b.id
ORDER BY count ASC
LIMIT 5";
$stats['least_earned'] = $this->db->query($query)->fetch_all(MYSQLI_ASSOC);
return $stats;
}
}
使用示例
// 初始化
$badgeSystem = new BadgeSystem($db);
$badgeDispatcher = new BadgeEventDispatcher($badgeSystem);
// 在用户登录时触发
$badgeDispatcher->dispatch('user.login', $userId, ['ip' => $ip]);
// 在用户发帖时触发
$badgeDispatcher->dispatch('post.created', $userId, ['post_id' => $postId]);
// 获取用户勋章墙
$badges = $badgeSystem->getUserBadges($userId, true);
// 获取所有可获得的勋章及进度
$allBadges = $badgeSystem->getAllBadgesWithProgress($userId);
// 管理后台创建新勋章
$badgeManager = new BadgeManager($db);
$badgeManager->createBadge([
'name' => '内容大师',
'description' => '发布100篇优质文章',
'icon' => 'fa-trophy',
'category' => 'content',
'criteria' => [
'condition' => 'post_count',
'value' => 100
],
'rarity' => 'epic'
]);
前端展示示例
<!-- 用户勋章墙 -->
<div class="badge-wall">
<?php foreach ($badges as $badge): ?>
<div class="badge-item <?= $badge['is_displayed'] ? 'displayed' : 'hidden' ?>">
<div class="badge-icon">
<i class="fas <?= $badge['icon'] ?>"></i>
<span class="badge-rarity <?= $badge['rarity'] ?>">
<?= ucfirst($badge['rarity']) ?>
</span>
</div>
<div class="badge-info">
<h4><?= $badge['name'] ?></h4>
<p><?= $badge['description'] ?></p>
</div>
<div class="badge-date">
<?= date('Y-m-d', strtotime($badge['earned_at'])) ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- 勋章进度条 -->
<div class="badge-progress">
<?php foreach ($allBadges as $badge): ?>
<div class="progress-item <?= $badge['is_earned'] ? 'earned' : 'locked' ?>">
<div class="badge-icon">
<i class="fas <?= $badge['icon'] ?>"></i>
</div>
<div class="progress-details">
<span class="badge-name"><?= $badge['name'] ?></span>
<?php if (!$badge['is_earned'] && $badge['progress']): ?>
<div class="progress-bar">
<?php $progress = json_decode($badge['progress'], true); ?>
<div class="progress-fill" style="width: <?= $progress['percentage'] ?>%"></div>
</div>
<span class="progress-text">
<?= $progress['current'] ?>/<?= $progress['total'] ?>
</span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
高级功能扩展
1 勋章成就系统
class AchievementBadgeSystem extends BadgeSystem
{
public function checkAchievementChain($userId, $badgeId)
{
// 检查是否需要完成前置勋章
$query = "SELECT prerequisites FROM badges WHERE id = ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $badgeId);
$stmt->execute();
$badge = $stmt->get_result()->fetch_assoc();
if ($badge && !empty($badge['prerequisites'])) {
$prerequisites = json_decode($badge['prerequisites'], true);
foreach ($prerequisites as $prereqId) {
if (!$this->hasBadge($userId, $prereqId)) {
return false; // 前置未完成
}
}
}
return parent::checkAndAward($userId, 'achievement', ['badge_id' => $badgeId]);
}
}
2 勋章排行榜
class BadgeLeaderboard
{
public function getLeaderboard($limit = 50, $timeframe = 'all')
{
$timeCondition = '';
switch ($timeframe) {
case 'week':
$timeCondition = "AND ub.earned_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)";
break;
case 'month':
$timeCondition = "AND ub.earned_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)";
break;
}
$query = "SELECT u.id, u.username, COUNT(ub.id) as badge_count,
GROUP_CONCAT(b.name SEPARATOR ', ') as badges
FROM users u
JOIN user_badges ub ON u.id = ub.user_id
JOIN badges b ON ub.badge_id = b.id
WHERE 1=1 $timeCondition
GROUP BY u.id
ORDER BY badge_count DESC
LIMIT ?";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $limit);
$stmt->execute();
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
}
}
这个勋章系统实现了:
- 完整的CRUD操作 - 创建、读取、更新、删除勋章
- 条件检测机制 - 自动检测用户行为并授予勋章
- 进度追踪 - 记录用户完成进度
- 事件驱动 - 通过事件监听自动触发
- 管理后台 - 批量授予、统计分析
- 前端展示 - 勋章墙、进度条、排行榜
- 扩展性 - 支持自定义条件和连锁成就
根据项目需要,还可以添加:
- 勋章动画效果
- 社交分享功能
- 勋章交易系统
- 特殊勋章(限时、节日等)