本文目录导读:

这是一个非常典型的社交功能需求,实现用户关注功能,核心是在数据库中记录 “谁关注了谁” 的关系。
我将从数据库设计、核心SQL操作、性能优化和代码示例四个方面来详细说明如何在PHP项目中实现。
数据库设计
最常见的做法是创建一个单独的关系表(多对多关系表),记录每一条关注关系。
CREATE TABLE `follows` (
`id` BIGINT UNSIGNED AUTO_INCREMENT,
`follower_id` BIGINT UNSIGNED NOT NULL COMMENT '关注者用户ID',
`following_id` BIGINT UNSIGNED NOT NULL COMMENT '被关注者用户ID',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '关注时间',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_follow` (`follower_id`, `following_id`), -- 防止重复关注
INDEX `idx_follower_id` (`follower_id`), -- 用于查询“我关注的人”
INDEX `idx_following_id` (`following_id`) -- 用于查询“我的粉丝”
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关注关系表';
关键点说明:
- 唯一索引:
UNIQUE KEY unique_follow确保了同一个人不会重复关注同一个人。 - 索引:为
follower_id和following_id分别建立索引,这是查询性能的关键。
核心PHP代码实现
以下代码使用 PDO(推荐方式)并假设你有一个$user_id表示当前登录用户。
1 关注与取消关注(切换逻辑)
<?php
/**
* 切换关注状态(关注/取消关注)
* @param PDO $pdo
* @param int $followerId 当前用户ID
* @param int $followingId 目标用户ID
* @return bool|string 成功返回true,失败返回错误信息
*/
function toggleFollow(PDO $pdo, int $followerId, int $followingId): mixed {
// 1. 基础校验:不能关注自己
if ($followerId === $followingId) {
return '不能关注自己';
}
// 2. 检查关系是否存在
$checkSql = "SELECT id FROM follows
WHERE follower_id = :follower_id AND following_id = :following_id";
$stmt = $pdo->prepare($checkSql);
$stmt->execute([
':follower_id' => $followerId,
':following_id' => $followingId
]);
$follow = $stmt->fetch();
if ($follow) {
// 3. 已关注 -> 取消关注(DELETE)
$deleteSql = "DELETE FROM follows WHERE id = :id";
$stmt = $pdo->prepare($deleteSql);
$stmt->execute([':id' => $follow['id']]);
return false; // 或者返回 '已取消关注'
} else {
// 4. 未关注 -> 添加关注(INSERT)
// 注意:由于有唯一索引,直接用INSERT,如果重复会抛异常,但我们已提前检查
$insertSql = "INSERT INTO follows (follower_id, following_id)
VALUES (:follower_id, :following_id)";
$stmt = $pdo->prepare($insertSql);
$stmt->execute([
':follower_id' => $followerId,
':following_id' => $followingId
]);
return true; // 或者返回 '关注成功'
}
}
2 检查是否已关注
<?php
function isFollowing(PDO $pdo, int $currentUserId, int $targetUserId): bool {
if ($currentUserId === $targetUserId) return false; // 自己不关注自己
$sql = "SELECT 1 FROM follows
WHERE follower_id = :follower_id AND following_id = :following_id
LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':follower_id' => $currentUserId,
':following_id' => $targetUserId
]);
return (bool) $stmt->fetchColumn();
}
3 获取关注列表与粉丝列表
<?php
// 获取我关注的人 (Following)
function getFollowingUsers(PDO $pdo, int $userId): array {
$sql = "SELECT u.id, u.username, u.avatar, f.created_at as followed_at
FROM follows f
JOIN users u ON f.following_id = u.id
WHERE f.follower_id = :user_id
ORDER BY f.created_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute([':user_id' => $userId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 获取我的粉丝 (Followers)
function getFollowerUsers(PDO $pdo, int $userId): array {
$sql = "SELECT u.id, u.username, u.avatar, f.created_at as followed_at
FROM follows f
JOIN users u ON f.follower_id = u.id
WHERE f.following_id = :user_id
ORDER BY f.created_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute([':user_id' => $userId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
前端交互(AJAX 示例)
为了提高用户体验,通常使用 Ajax 实现无刷新关注。
<!-- 关注按钮 -->
<button class="follow-btn" data-target-id="<?= $targetUserId ?>">
<?= isFollowing($pdo, $currentUserId, $targetUserId) ? '已关注' : '+ 关注' ?>
</button>
<script>
document.querySelector('.follow-btn').addEventListener('click', function() {
const btn = this;
const targetId = btn.dataset.targetId;
fetch('/api/follow', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_user_id: targetId })
})
.then(res => res.json())
.then(data => {
if (data.status === 'followed') {
btn.textContent = '已关注';
btn.classList.add('following');
} else if (data.status === 'unfollowed') {
btn.textContent = '+ 关注';
btn.classList.remove('following');
} else {
alert(data.message);
}
});
});
</script>
对应的PHP后端接口 /api/follow.php:
<?php
// 假设已启动session并验证用户登录
$sessionUserId = $_SESSION['user_id'];
$input = json_decode(file_get_contents('php://input'), true);
$targetUserId = (int)($input['target_user_id'] ?? 0);
if ($targetUserId <= 0) {
echo json_encode(['status' => 'error', 'message' => '无效的用户']);
exit;
}
try {
$result = toggleFollow($pdo, $sessionUserId, $targetUserId);
if ($result === true) {
echo json_encode(['status' => 'followed']);
} elseif ($result === false) {
echo json_encode(['status' => 'unfollowed']);
} else {
echo json_encode(['status' => 'error', 'message' => $result]);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => '服务器错误']);
}
性能优化与注意事项
1 缓存关注数
每次显示用户主页时都去COUNT粉丝数会影响性能,建议在users表中增加两个冗余字段:
ALTER TABLE users ADD COLUMN followers_count INT NOT NULL DEFAULT 0 COMMENT '粉丝数', ADD COLUMN following_count INT NOT NULL DEFAULT 0 COMMENT '关注数';
在关注/取消关注时,同时更新这两个字段:
// 在 toggleFollow 函数中,执行 INSERT/DELETE 后,同步更新计数
if ($follow) {
// 取消关注
$pdo->exec("UPDATE users SET followers_count = followers_count - 1 WHERE id = $followingId");
$pdo->exec("UPDATE users SET following_count = following_count - 1 WHERE id = $followerId");
} else {
// 加关注
$pdo->exec("UPDATE users SET followers_count = followers_count + 1 WHERE id = $followingId");
$pdo->exec("UPDATE users SET following_count = following_count + 1 WHERE id = $followerId");
}
2 使用 Redis 或 Memcached 缓存
对于高并发场景,可以将“某人是否关注了另一个人”的查询结果缓存起来:
// 伪代码 - 使用 Redis
$cacheKey = "follow:{$followerId}_{$followingId}";
$isFollowed = $redis->get($cacheKey);
if ($isFollowed === null) {
// 查数据库
$isFollowed = isFollowing($pdo, $followerId, $followingId);
$redis->setex($cacheKey, 3600, $isFollowed); // 缓存1小时
}
3 防止循环关注
你可以在业务层增加判断:如果B关注了A,A就不能再关注B(类似Twitter的设计),或者允许双向关注(类似Instagram),根据你的产品逻辑选择。
完整功能流程图
用户点击关注按钮
↓
Ajax 请求到 /api/follow
↓
PHP接收请求,验证用户登录
↓
检查数据库中是否已存在关注关系
↓
┌───────┴───────┐
存在(取消关注) 不存在(添加关注)
↓ ↓
删除记录 插入记录
↓ ↓
更新粉丝/关注计数 更新粉丝/关注计数
↓ ↓
返回JSON结果 返回JSON结果
↓
前端更新按钮状态和数字
核心要点:
- 数据库:使用
follows表 + 唯一索引 + 双字段索引。 - SQL:
INSERT和DELETE切换,配合唯一索引防止重复。 - 性能:冗余计数字段 + 缓存(可选)。
- 交互:Ajax 实现无刷新关注。
如果你使用的是 Laravel 框架,可以考虑使用 Eloquent 的 attach/detach 方法,或者使用 laravel-follow 这类扩展包,可以省去很多底层代码,如果需要原生 PHP 的完整示例,上面的代码已经足够落地使用。