PHP项目关注与粉丝关系

wen PHP项目 2

本文目录导读:

PHP项目关注与粉丝关系

  1. 数据库设计(核心)
  2. 核心功能逻辑实现
  3. 高并发与性能优化
  4. 常见问题与注意事项
  5. 完整的关注接口示例(Laravel风格)

在PHP项目中实现“关注与粉丝”关系(通常称为社交关系或用户关系),通常采用关系型数据库(如MySQL)来存储用户之间的关注信息,下面我将从数据结构设计、核心功能逻辑、性能优化及代码示例四个方面为你详细讲解。


数据库设计(核心)

最经典的设计是使用一张独立的关系表,而不是在用户表中存储JSON数组。

CREATE TABLE `user_follow` (
    `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `follower_id` INT UNSIGNED NOT NULL COMMENT '关注者ID',
    `followed_id` INT UNSIGNED NOT NULL COMMENT '被关注者ID',
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '关注时间',
    `status` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1:关注中, 0:取消关注(软删除)',
    UNIQUE KEY `uk_follower_followed` (`follower_id`, `followed_id`),
    INDEX `idx_follower_status` (`follower_id`, `status`),
    INDEX `idx_followed_status` (`followed_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关注关系表';

设计要点

  • 联合唯一索引 (follower_id, followed_id) 确保同一对用户仅有一条记录。
  • 软删除(status字段)比物理删除更方便恢复数据和做统计。
  • 复合索引 (follower_id, status) 用于快速查询某人关注了谁;(followed_id, status) 用于查询某人的粉丝。

用户表(users)补充字段(可选但推荐)

ALTER TABLE `users` ADD COLUMN `follower_count` INT UNSIGNED DEFAULT 0;
ALTER TABLE `users` ADD COLUMN `following_count` INT UNSIGNED DEFAULT 0;

这两个冗余字段可以避免每次查询都要COUNT,大大提升列表页和首页的性能。


核心功能逻辑实现

关注/取消关注

public function toggleFollow(int $currentUserId, int $targetUserId): array
{
    // 禁止关注自己
    if ($currentUserId === $targetUserId) {
        return ['code' => 400, 'msg' => '不能关注自己'];
    }
    // 检查目标用户是否存在
    $user = User::find($targetUserId);
    if (!$user) {
        return ['code' => 404, 'msg' => '用户不存在'];
    }
    // 使用事务保证数据一致性
    DB::beginTransaction();
    try {
        $record = UserFollow::where('follower_id', $currentUserId)
                            ->where('followed_id', $targetUserId)
                            ->first();
        if ($record) {
            // 已存在记录 -> 反转状态
            $newStatus = !$record->status;
            $record->status = $newStatus;
            $record->save();
        } else {
            // 首次关注
            UserFollow::create([
                'follower_id' => $currentUserId,
                'followed_id' => $targetUserId,
                'status'      => true
            ]);
            $newStatus = true;
        }
        // 更新计数器(可根据status增减)
        $this->updateCounters($currentUserId, $targetUserId, $newStatus);
        DB::commit();
        return ['code' => 200, 'msg' => $newStatus ? '关注成功' : '取消关注'];
    } catch (\Exception $e) {
        DB::rollBack();
        return ['code' => 500, 'msg' => '操作失败'];
    }
}
private function updateCounters(int $followerId, int $followedId, bool $isFollow)
{
    $diff = $isFollow ? 1 : -1;
    User::where('id', $followerId)->increment('following_count', $diff);
    User::where('id', $followedId)->increment('follower_count', $diff);
}

获取粉丝列表(关注我的人)

public function getFans(int $userId, int $page = 1, int $pageSize = 20): array
{
    return UserFollow::where('followed_id', $userId)
                     ->where('status', true)
                     ->orderBy('created_at', 'desc')
                     ->paginate($pageSize, ['*'], 'page', $page)
                     ->through(function ($follow) {
                         return [
                             'id'       => $follow->follower->id,
                             'name'     => $follow->follower->name,
                             'avatar'   => $follow->follower->avatar,
                             'followed_at' => $follow->created_at
                         ];
                     });
}

判断是否已关注(批量判断常用)

// 单个判断
public function isFollowing(int $currentUserId, int $targetUserId): bool
{
    return UserFollow::where('follower_id', $currentUserId)
                     ->where('followed_id', $targetUserId)
                     ->where('status', true)
                     ->exists();
}
// 批量判断(例如首页Feed流需要标记已关注状态)
public function batchCheckFollowStatus(int $currentUserId, array $targetUserIds): array
{
    $exists = UserFollow::where('follower_id', $currentUserId)
                        ->whereIn('followed_id', $targetUserIds)
                        ->where('status', true)
                        ->pluck('followed_id')
                        ->toArray();
    return array_fill_keys($targetUserIds, false) + array_fill_keys($exists, true);
}

高并发与性能优化

索引优化

  • 上面的表设计已包含两个核心索引,避免全表扫描
  • 如果查询量大,可以为 (follower_id, created_at) 建索引用于查询“我最近关注的人”。

计数器缓存(Redis)

  • follower_countfollowing_count 缓存在Redis,设置过期时间(如1小时)。
  • 更新时先更新数据库,再删除Redis缓存(Cache Aside模式),下次查询自动回填。
public function getUserFollowCount(int $userId): array
{
    $redis = Redis::connection();
    $key = "user:{$userId}:follow_count";
    $counts = $redis->hgetall($key);
    if (!$counts) {
        // 从数据库读取并存入缓存
        $user = User::find($userId, ['follower_count', 'following_count']);
        $counts = [
            'follower' => $user->follower_count,
            'following' => $user->following_count
        ];
        $redis->hmset($key, $counts);
        $redis->expire($key, 3600);
    }
    return $counts;
}

异步更新计数器

关注/取关操作非常频繁,可以使用消息队列(RabbitMQ/Redis Stream)异步更新MySQL中的计数,减少主流程响应时间。


常见问题与注意事项

问题 解决方案
幂等性(重复点击关注) 利用数据库 unique key,第二次插入报错,或使用INSERT ... ON DUPLICATE KEY UPDATE
用户注销/删除 删除用户时同时逻辑删除所有关注关系 (UPDATE ... SET status=0 WHERE follower_id=xxx OR followed_id=xxx)
分页性能 粉丝/关注列表避免用OFFSET大数值,改用游标分页(cursor based pagination),基于idcreated_at
双向关注判定 查询 (a->b) AND (b->a) 可定义为好友,用一条SQL + 自连接实现

游标分页示例

$lastId = request('last_id', 0);
$list = UserFollow::where('followed_id', $userId)
                  ->where('status', true)
                  ->where('id', '<', $lastId)  // 游标方式
                  ->orderBy('id', 'desc')
                  ->limit(20)
                  ->get();

完整的关注接口示例(Laravel风格)

// routes/api.php
Route::post('/user/{user}/follow', [FollowController::class, 'toggle']);
// FollowController.php
public function toggle(Request $request, User $user)
{
    $result = $this->followService->toggleFollow(
        $request->user()->id, 
        $user->id
    );
    if ($result['code'] !== 200) {
        return response()->json($result, $result['code']);
    }
    return response()->json([
        'followed'          => $result['msg'] === '关注成功',
        'following_count'   => $request->user()->fresh()->following_count,
        'follower_count'    => $user->fresh()->follower_count
    ]);
}

模块 推荐做法
存储结构 独立关系表 + 联合唯一索引 + 软删除status字段
计数器 用户表冗余字段 + Redis缓存加速
判断逻辑 批量查询使用 whereIn 一次返回
并发安全 MySQL事务 + 唯一索引保证不重复
分页 游标分页优于传统OFFSET

这个设计在百万级用户、千万级关注关系的场景下依然能保持良好性能,如果你的项目用户量级更大(如亿级),需要考虑分表(按用户ID取模)或改用图数据库(如Neo4j)。

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