PHP项目怎样实现内容收藏统计?

wen PHP项目 45

本文目录导读:

PHP项目怎样实现内容收藏统计?

  1. 核心功能拆解
  2. 方案一:基础实现(基于关系型数据库 + 计数冗余)
  3. 方案二:高性能实现(推荐,Redis + MySQL异步落盘)
  4. 前端交互示例(AJAX + 动画)
  5. 总结与选择建议

在PHP项目中实现内容收藏统计,通常有两种主要思路:实时查询数据库计数使用缓存(如Redis),对于大多数中小型项目,合理利用数据库索引和缓存即可高效实现。

以下是完整的实现方案,包含数据库设计、核心代码示例和性能优化建议。

核心功能拆解

  1. 用户收藏/取消收藏(写操作)
  2. 查询某个内容的收藏数(读操作)
  3. 判断当前用户是否已收藏(读操作)

基础实现(基于关系型数据库 + 计数冗余)

这种方式简单可靠,适合并发不高的项目(如日均PV < 10万)。

数据库表设计

需要三张表:用户表、内容表(如文章)、收藏关系表。

核心收藏表 (favorites)

CREATE TABLE `favorites` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` int(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '用户ID',
  `target_id` int(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '被收藏内容ID(如文章ID)',
  `target_type` varchar(30) NOT NULL DEFAULT 'article' COMMENT '内容类型(支持多种内容,如文章、视频)',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间',
  PRIMARY KEY (`id`),
  -- 关键索引:用于快速查询某个用户对某个内容的收藏状态,以及统计数量
  UNIQUE KEY `uk_user_target` (`user_id`, `target_id`, `target_type`),
  KEY `idx_target_count` (`target_id`, `target_type`) -- 用于统计该内容的收藏总数
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

表(冗余计数字段,如 articles)**

ALTER TABLE `articles` ADD COLUMN `fav_count` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '收藏数';

PHP代码实现

<?php
class FavoriteService {
    private $db; // PDO 实例
    public function __construct($db) {
        $this->db = $db;
    }
    /**
     * 切换收藏状态(如果已收藏则取消,否则收藏)
     * @param int $userId
     * @param int $targetId
     * @param string $targetType
     * @return array ['status' => 'collected'|'uncollected', 'fav_count' => int]
     */
    public function toggle($userId, $targetId, $targetType = 'article') {
        // 先检查是否已收藏(利用唯一索引)
        $stmt = $this->db->prepare("SELECT id FROM favorites WHERE user_id = ? AND target_id = ? AND target_type = ? LIMIT 1");
        $stmt->execute([$userId, $targetId, $targetType]);
        $existing = $stmt->fetchColumn();
        if ($existing) {
            // 已收藏:取消收藏
            $this->db->beginTransaction();
            try {
                $stmt = $this->db->prepare("DELETE FROM favorites WHERE id = ?");
                $stmt->execute([$existing]);
                // 更新内容表收藏计数(减1)
                $stmt = $this->db->prepare("UPDATE articles SET fav_count = GREATEST(fav_count - 1, 0) WHERE id = ?");
                $stmt->execute([$targetId]);
                $this->db->commit();
            } catch (Exception $e) {
                $this->db->rollBack();
                throw $e;
            }
            $status = 'uncollected';
        } else {
            // 未收藏:添加收藏
            $this->db->beginTransaction();
            try {
                $stmt = $this->db->prepare("INSERT INTO favorites (user_id, target_id, target_type) VALUES (?, ?, ?)");
                $stmt->execute([$userId, $targetId, $targetType]);
                // 更新内容表收藏计数(加1)
                $stmt = $this->db->prepare("UPDATE articles SET fav_count = fav_count + 1 WHERE id = ?");
                $stmt->execute([$targetId]);
                $this->db->commit();
            } catch (Exception $e) {
                $this->db->rollBack();
                throw $e;
            }
            $status = 'collected';
        }
        // 返回最新的收藏数
        $stmt = $this->db->prepare("SELECT fav_count FROM articles WHERE id = ?");
        $stmt->execute([$targetId]);
        $favCount = (int) $stmt->fetchColumn();
        return ['status' => $status, 'fav_count' => $favCount];
    }
    /**
     * 获取内容的收藏总数
     */
    public function getCount($targetId, $targetType = 'article') {
        $stmt = $this->db->prepare("SELECT fav_count FROM articles WHERE id = ?");
        $stmt->execute([$targetId]);
        return (int) $stmt->fetchColumn();
    }
    /**
     * 检查用户是否收藏了某个内容
     */
    public function isFavored($userId, $targetId, $targetType = 'article') {
        $stmt = $this->db->prepare("SELECT 1 FROM favorites WHERE user_id = ? AND target_id = ? AND target_type = ? LIMIT 1");
        $stmt->execute([$userId, $targetId, $targetType]);
        return (bool) $stmt->fetchColumn();
    }
}

优缺点

  • 优点:数据强一致,实现简单,不需要额外组件。
  • 缺点:每次收藏/取消都写两次数据库(插入/删除 + 更新计数),高并发下,UPDATE fav_count = fav_count - 1 可能产生行锁等待。

高性能实现(推荐,Redis + MySQL异步落盘)

适用于高并发场景(如短视频、新闻客户端),核心思想:收藏操作直接写入Redis,然后通过定时任务或消息队列异步同步到MySQL做持久化

Redis数据结构选择

  • 一个用户收藏了哪些内容:使用 Set,key 为 user:1:favs(value 为 target_id)。
  • 被哪些用户收藏:使用 Set,key 为 article:100:fav_users,的收藏总数**:使用计数器(string),key 为 article:100:fav_count

核心流程

用户收藏/取消(PHP -> Redis)

<?php
class FavoriteFastService {
    private $redis;
    public function __construct($redis) {
        $this->redis = $redis;
    }
    public function toggle($userId, $targetId) {
        // 判断是否已收藏
        $key = "user:{$userId}:favs";
        $isFav = $this->redis->sIsMember($key, $targetId);
        if ($isFav) {
            // 取消收藏
            $this->redis->multi() // 开启事务
                ->sRem($key, $targetId)
                ->sRem("article:{$targetId}:fav_users", $userId)
                ->decr("article:{$targetId}:fav_count")
                ->exec();
            return ['status' => 'uncollected', 'fav_count' => $this->redis->get("article:{$targetId}:fav_count")];
        } else {
            // 收藏
            $this->redis->multi()
                ->sAdd($key, $targetId)
                ->sAdd("article:{$targetId}:fav_users", $userId)
                ->incr("article:{$targetId}:fav_count")
                ->exec();
            return ['status' => 'collected', 'fav_count' => $this->redis->get("article:{$targetId}:fav_count")];
        }
    }
    public function getCount($targetId) {
        return (int) $this->redis->get("article:{$targetId}:fav_count");
    }
    public function isFavored($userId, $targetId) {
        return $this->redis->sIsMember("user:{$userId}:favs", $targetId);
    }
}

异步同步到MySQL(保证数据不丢)

方案A:定时任务(Crontab)

  • 每分钟执行一个脚本:遍历Redis中所有收藏相关Key,将数据批量写入MySQL的favorites表,并更新articles.fav_count
  • 缺点:如果进程崩溃,可能丢失1分钟数据。

方案B:消息队列(推荐)

  • 用户收藏后,除了写Redis,还推送一条消息到消息队列(如RabbitMQ、Kafka或Redis Streams)。
  • 一个消费者进程(Worker)从队列消费消息,执行:INSERT INTO favorites ... ON DUPLICATE KEY UPDATEUPDATE articles SET fav_count = ?
  • 可以在每次消费时直接更新文章的fav_count,也可以在消费者中累积一段时间后再批量更新

示例:使用 Redis Streams 做消息队列

// 收藏成功后,推送消息
$redis->xAdd('fav_stream', '*', [
    'user_id' => $userId,
    'target_id' => $targetId,
    'action' => $isFav ? 'remove' : 'add'
]);
// 消费者进程(可以写成一个常驻脚本)
while (true) {
    $messages = $redis->xReadGroup('fav_group', 'worker_1', ['fav_stream' => '>'], 1, 5000);
    if ($messages) {
        foreach ($messages as $stream => $items) {
            foreach ($items as $id => $data) {
                // 写MySQL表
                $db->execute('...');
                // 确认消息处理完毕(ACK)
                $redis->xAck('fav_stream', 'fav_group', [$id]);
            }
        }
    }
}

优缺点

  • 优点:性能极高(全部基于内存操作),支持水平扩展。
  • 缺点:系统复杂度增加,需要引入Redis和数据同步组件,在Redis宕机且未同步到MySQL时会丢失部分数据(可通过持久化RDB/AOF缓解)。

前端交互示例(AJAX + 动画)

无论后端用哪种方案,前端逻辑类似:

// 假设 .fav-btn 是收藏按钮
$('.fav-btn').on('click', function() {
    const $btn = $(this);
    const targetId = $btn.data('id'); // 文章ID
    $.post('/api/favorite/toggle', { target_id: targetId }, function(res) {
        if (res.code === 0) {
            // 更新UI
            $btn.toggleClass('active', res.data.status === 'collected');
            $btn.find('.count').text(res.data.fav_count);
            // 触发放心形动画等
        } else {
            alert('操作失败,请重试');
        }
    });
});

总结与选择建议

场景 推荐方案 原因
小型站点、用户量<1万 方案一(MySQL直接操作) 实现简单,无需额外组件,数据一致
中型站点、高并发读 方案一 + 本地缓存(如Redis做纯只读缓存) 写操作直接落库,但读时走Redis
大型站点、社交类应用 方案二(Redis计数 + 异步落盘) 支持高并发读写,用户体验好

额外建议:类型不止一种,target_type 字段非常有用。

  • 在用户列表页或文章列表页展示收藏数时,务必使用冗余字段 fav_count,而不是 COUNT(*),否则会严重拖慢性能。
  • 对于“该用户是否收藏过”的判断,如果用户经常刷新列表,可以考虑前端缓存用户收藏的ID列表(如localStorage或发送一次完整ID列表的接口)。

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