PHP项目如何实现关注功能?

wen java案例 2

PHP项目如何实现关注功能?从入门到实战的完整指南

目录导读

  1. 关注功能的核心逻辑与数据库设计
  2. 基于PHP+MySQL的实现步骤
  3. 前后端交互与状态管理
  4. 性能优化与防重复操作
  5. 常见问题与解决方案(问答环节)

关注功能的核心逻辑与数据库设计

在社交媒体、内容平台或电商系统中,“关注”是用户间建立单向社交关系的基础操作,其本质是记录用户A关注用户B这一行为,并支持后续的“取消关注”“查询是否关注”“获取粉丝/关注列表”等扩展功能。

PHP项目如何实现关注功能?

数据库表设计

推荐使用一张独立的follows表来存储关系,避免与用户主表耦合:

CREATE TABLE `follows` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `follower_id` int(11) NOT NULL COMMENT '关注者ID',
  `following_id` int(11) NOT NULL COMMENT '被关注者ID',
  `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_follow` (`follower_id`,`following_id`), -- 防止重复关注
  INDEX `idx_follower` (`follower_id`),
  INDEX `idx_following` (`following_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计要点

  • UNIQUE索引确保同一对关注关系只存在一条记录(即“关注”为幂等操作)。
  • 索引按follower_idfollowing_id分别建立,便于快速查询用户的关注/粉丝列表。
  • 不要将关注数直接存为user表的字段,避免频繁更新带来的锁竞争,应通过COUNT查询实时统计或使用Redis缓存。

基于PHP+MySQL的实现步骤

1 关注/取消关注接口

以下是一个典型的RESTful API实现,使用PDO预处理防止SQL注入:

<?php
class FollowController {
    public function toggleFollow($currentUserId, $targetUserId) {
        // 1. 参数校验
        if ($currentUserId == $targetUserId) {
            throw new Exception("不能关注自己");
        }
        // 2. 检查是否已关注
        $stmt = $pdo->prepare("SELECT id FROM follows WHERE follower_id = ? AND following_id = ?");
        $stmt->execute([$currentUserId, $targetUserId]);
        $existing = $stmt->fetch();
        if ($existing) {
            // 已关注 → 取消关注
            $deleteStmt = $pdo->prepare("DELETE FROM follows WHERE id = ?");
            $deleteStmt->execute([$existing['id']]);
            return ['action' => 'unfollowed', 'status' => false];
        } else {
            // 未关注 → 添加关注
            $insertStmt = $pdo->prepare("INSERT INTO follows (follower_id, following_id) VALUES (?, ?)");
            $insertStmt->execute([$currentUserId, $targetUserId]);
            return ['action' => 'followed', 'status' => true];
        }
    }
}

2 查询是否关注

在展示用户主页时,需要快速判断当前登录用户是否已关注对方:

public function isFollowing($currentUserId, $targetUserId) {
    $stmt = $pdo->prepare("SELECT 1 FROM follows WHERE follower_id = ? AND following_id = ? LIMIT 1");
    $stmt->execute([$currentUserId, $targetUserId]);
    return (bool)$stmt->fetchColumn();
}

3 获取关注/粉丝列表

使用JOIN查询,并做分页处理:

public function getFollowingList($userId, $page = 1, $perPage = 20) {
    $offset = ($page - 1) * $perPage;
    $stmt = $pdo->prepare("
        SELECT u.id, u.username, u.avatar 
        FROM follows f
        JOIN users u ON f.following_id = u.id
        WHERE f.follower_id = ?
        ORDER BY f.created_at DESC
        LIMIT ? OFFSET ?
    ");
    $stmt->execute([$userId, $perPage, $offset]);
    return $stmt->fetchAll();
}

性能提示:当粉丝/关注数量超过10万时,避免使用COUNT全表扫描,可维护一个Redis计数器,或使用MySQL的EXPLAIN优化索引。


前后端交互与状态管理

1 前端按钮状态切换

以下为Vue.js的示例逻辑,核心在于乐观更新(先修改UI状态,再发送请求):

<template>
  <button @click="toggleFollow" :class="{ 'is-following': isFollowing }">
    {{ isFollowing ? '已关注' : '关注' }}
  </button>
</template>
<script>
export default {
  data() { return { isFollowing: false } },
  methods: {
    async toggleFollow() {
      // 乐观更新:立即翻转状态
      this.isFollowing = !this.isFollowing;
      try {
        const res = await axios.post('/api/follow', { targetUserId: this.userId });
        // 服务器返回的最终状态覆盖本地
        this.isFollowing = res.data.status;
      } catch (error) {
        // 请求失败时回滚状态
        this.isFollowing = !this.isFollowing;
        console.error('关注操作失败', error);
      }
    }
  }
}
</script>

2 防止重复点击

在请求返回前,禁用按钮:

this.isProcessing = true;
this.isFollowing = !this.isFollowing;
// ... 请求完成后 this.isProcessing = false;

性能优化与防重复操作

1 数据库唯一索引防重复

如前文设计,UNIQUE (follower_id, following_id)从数据库层面杜绝了同一用户对同一目标重复关注的可能,即使前端没有做防抖处理。

2 Redis缓存计数器

对于“粉丝数”“关注数”这种高频读取字段,使用Redis缓存避免每次查询都扫描表:

// 关注后更新
$redis->incr("user:{$targetUserId}:follower_count");
$redis->incr("user:{$currentUserId}:following_count");
// 获取时优先读缓存
$count = $redis->get("user:{$userId}:follower_count");
if ($count === false) {
    $count = $pdo->query("SELECT COUNT(*) FROM follows WHERE following_id = $userId")->fetchColumn();
    $redis->setex("user:{$userId}:follower_count", 3600, $count);
}

3 异步队列处理

在百万级用户平台,直接将关注操作写入数据库可能影响主库性能,可采用消息队列(RabbitMQ/Redis List)异步处理:

// 生产者(API层)
$redis->lpush('follow_queue', json_encode(['follower' => $uid, 'following' => $tid]));
// 消费者(Worker进程)
while ($data = $redis->brpop('follow_queue', 5)) {
    $info = json_decode($data[1], true);
    $db->insert('follows', $info);
    // 更新缓存...
}

常见问题与解决方案(问答环节)

Q1:用户关注自己怎么办?

A:在业务层做校验,如 if ($currentUserId == $targetUserId) 直接返回错误提示,数据库层面无法通过索引约束阻止,必须业务处理。

Q2:如何确保关注按钮的实时状态?

A:推荐后端在用户访问主页时,一次性返回该用户关注的所有ID列表(如 [3,5,8]),前端用Set存储,切换时使用乐观更新。

Q3:关注后需要发送通知吗?

A:典型做法是异步推送到通知表,例如关注后创建一个 notifications 记录(type=follow, from_id=关注者, to_id=被关注者),通过WebSocket或定时任务推送。

Q4:如何实现“互关”(双向关注)的标识?

A:查询时做两次检查:

SELECT 
  EXISTS(SELECT 1 FROM follows WHERE follower_id=? AND following_id=?) AS i_follow_him,
  EXISTS(SELECT 1 FROM follows WHERE follower_id=? AND following_id=?) AS he_follows_me

当两个值都为1时,在前端显示“互相关注”状态。

Q5:关注列表分页如何优化?

A:使用基于游标的分页(cursor-based pagination),而非OFFSET。WHERE created_at < 上次最后一条的时间 LIMIT 20,避免OFFSET在大数据量下的性能下降。


实现关注功能的核心是通过关系表存储用户间的关联,辅以唯一索引防止重复乐观更新提升交互体验缓存和异步处理增强性能,无论是搭建个人项目还是百万级社交平台,上述方案均可作为基础架构延伸扩展,实际开发中,请根据用户量级和服务器资源选择合适的优化策略,并始终关注数据一致性与并发控制。

抱歉,评论功能暂时关闭!