uni-app 点赞系统

核心知识点讲解

1. 点赞系统概述

点赞系统是现代应用中常见的互动功能,它可以:

  • 衡量内容的受欢迎程度
  • 增强用户参与感和互动性
  • 为内容推荐系统提供数据支持
  • 鼓励优质内容的创作

2. 点赞系统架构

一个完整的点赞系统通常包含以下组件:

2.1 前端组件

  • 点赞按钮:用户触发点赞操作的界面元素
  • 点赞状态管理:管理用户的点赞状态
  • 点赞计数:显示内容的点赞数量
  • 动画效果:提供视觉反馈的动画

2.2 后端服务

  • 点赞 API:处理点赞和取消点赞的请求
  • 点赞存储:数据库设计和存储策略
  • 缓存机制:提高点赞计数的读取性能
  • 防作弊:防止恶意刷点赞的措施

3. 点赞数据结构

合理的点赞数据结构设计对于系统的性能和可扩展性非常重要:

3.1 点赞记录结构

{
  id: String,           // 点赞ID
  userId: String,       // 用户ID
  targetId: String,     // 目标ID(如文章ID、评论ID)
  targetType: String,   // 目标类型(如 article、comment)
  createdAt: Timestamp, // 创建时间
  updatedAt: Timestamp  // 更新时间
}

3.2 目标对象结构

{
  id: String,           // 目标ID
  likes: Number,        // 点赞数
  likedBy: Array,       // 点赞用户ID列表(可选,用于快速判断)
  // 其他字段...
}

4. 点赞功能实现

4.1 前端实现要点

  • 状态管理:使用 Vuex 或组件级状态管理点赞状态
  • 乐观更新:先更新本地状态,再同步到服务器
  • 防重复点击:添加点击防抖,防止重复操作
  • 动画效果:添加点赞动画,提升用户体验

4.2 后端实现要点

  • 幂等性处理:确保重复请求不会导致错误
  • 事务处理:保证点赞操作的原子性
  • 缓存更新:及时更新点赞计数缓存
  • 数据统计:记录点赞行为数据,用于分析

5. 点赞状态管理

点赞状态管理是前端实现的核心部分,需要考虑以下几点:

  • 本地状态:使用 Vuex 或组件 data 存储点赞状态
  • 持久化:可选地将点赞状态持久化到本地存储
  • 状态同步:确保多端点赞状态的一致性
  • 批量处理:优化批量点赞状态的获取和更新

6. 点赞计数更新

点赞计数的更新需要考虑性能和一致性:

  • 实时更新:前端立即更新计数,提供即时反馈
  • 异步同步:后台异步同步到数据库
  • 缓存策略:使用 Redis 等缓存提高计数读取性能
  • 计数一致性:定期校准缓存和数据库中的计数

7. 防作弊措施

为了防止恶意刷点赞,需要实现以下措施:

  • 用户认证:确保只有登录用户可以点赞
  • 速率限制:限制单个用户的点赞频率
  • 设备指纹:识别和限制异常设备
  • 行为分析:检测和阻止异常点赞行为

实用案例分析

案例:实现完整的点赞系统

功能需求

我们需要实现一个包含以下功能的点赞系统:

  1. 内容点赞和取消点赞
  2. 点赞状态管理
  3. 点赞计数更新
  4. 点赞动画效果
  5. 防作弊措施

实现方案

1. 点赞 API 服务

首先,我们需要实现点赞相关的 API 服务:

// api/like.js

// 导入请求封装
import request from './request';

// 点赞 API
export const likeApi = {
  // 点赞
  like: (data) => {
    return request({
      url: '/api/likes',
      method: 'POST',
      data
    });
  },
  
  // 取消点赞
  unlike: (data) => {
    return request({
      url: '/api/likes/cancel',
      method: 'POST',
      data
    });
  },
  
  // 获取点赞状态
  getLikeStatus: (params) => {
    return request({
      url: '/api/likes/status',
      method: 'GET',
      params
    });
  },
  
  // 批量获取点赞状态
  getBatchLikeStatus: (data) => {
    return request({
      url: '/api/likes/batch-status',
      method: 'POST',
      data
    });
  }
};
2. 点赞组件

实现点赞按钮组件,支持动画效果:

<template>
  <view class="like-button" @click="toggleLike">
    <view 
      class="like-icon"
      :class="{ 'liked': isLiked }"
      :style="iconStyle"
    >
      <text class="like-emoji">{{ isLiked ? '❤️' : '🤍' }}</text>
    </view>
    <view class="like-count" :class="{ 'liked': isLiked }">
      {{ likeCount }}
    </view>
  </view>
</template>

<script>
export default {
  props: {
    // 目标ID
    targetId: {
      type: String,
      required: true
    },
    // 目标类型
    targetType: {
      type: String,
      default: 'article'
    },
    // 初始点赞数
    count: {
      type: Number,
      default: 0
    },
    // 初始点赞状态
    liked: {
      type: Boolean,
      default: false
    },
    // 是否显示计数
    showCount: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      isLiked: this.liked,
      likeCount: this.count,
      isAnimating: false,
      iconScale: 1
    };
  },
  computed: {
    iconStyle() {
      return {
        transform: `scale(${this.iconScale})`
      };
    }
  },
  watch: {
    liked: {
      handler(newVal) {
        this.isLiked = newVal;
      },
      immediate: true
    },
    count: {
      handler(newVal) {
        this.likeCount = newVal;
      },
      immediate: true
    }
  },
  methods: {
    // 切换点赞状态
    toggleLike() {
      if (this.isAnimating) return;
      
      // 触发点赞动画
      this.triggerLikeAnimation();
      
      // 切换本地状态
      this.isLiked = !this.isLiked;
      this.likeCount += this.isLiked ? 1 : -1;
      
      // 触发自定义事件
      this.$emit('like-change', {
        targetId: this.targetId,
        targetType: this.targetType,
        isLiked: this.isLiked,
        likeCount: this.likeCount
      });
    },
    
    // 触发点赞动画
    triggerLikeAnimation() {
      this.isAnimating = true;
      
      // 点赞动画
      this.iconScale = 1.2;
      setTimeout(() => {
        this.iconScale = 1;
        setTimeout(() => {
          this.isAnimating = false;
        }, 150);
      }, 150);
    }
  }
};
</script>

<style scoped>
.like-button {
  display: flex;
  align-items: center;
  cursor: pointer;
  user-select: none;
}

.like-icon {
  margin-right: 8rpx;
  transition: transform 0.3s ease;
}

.like-emoji {
  font-size: 32rpx;
  transition: all 0.3s ease;
}

.like-icon.liked .like-emoji {
  color: #FF3B30;
  filter: drop-shadow(0 0 4rpx rgba(255, 59, 48, 0.5));
}

.like-count {
  font-size: 24rpx;
  color: #666;
  transition: all 0.3s ease;
}

.like-count.liked {
  color: #FF3B30;
}

/* 禁用状态 */
.like-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
</style>
3. 点赞状态管理

使用 Vuex 管理全局点赞状态:

// store/modules/like.js

import { likeApi } from '@/api/like';

export default {
  namespaced: true,
  state: {
    // 点赞状态映射 { targetType: { targetId: isLiked } }
    likeStatus: {},
    // 点赞计数映射 { targetType: { targetId: count } }
    likeCounts: {},
    // 加载状态
    loading: false
  },
  mutations: {
    // 设置点赞状态
    SET_LIKE_STATUS(state, { targetType, targetId, isLiked }) {
      if (!state.likeStatus[targetType]) {
        state.likeStatus[targetType] = {};
      }
      state.likeStatus[targetType][targetId] = isLiked;
    },
    
    // 批量设置点赞状态
    SET_BATCH_LIKE_STATUS(state, { targetType, statusMap }) {
      if (!state.likeStatus[targetType]) {
        state.likeStatus[targetType] = {};
      }
      state.likeStatus[targetType] = {
        ...state.likeStatus[targetType],
        ...statusMap
      };
    },
    
    // 设置点赞计数
    SET_LIKE_COUNT(state, { targetType, targetId, count }) {
      if (!state.likeCounts[targetType]) {
        state.likeCounts[targetType] = {};
      }
      state.likeCounts[targetType][targetId] = count;
    },
    
    // 更新点赞计数
    UPDATE_LIKE_COUNT(state, { targetType, targetId, delta }) {
      if (!state.likeCounts[targetType]) {
        state.likeCounts[targetType] = {};
      }
      if (state.likeCounts[targetType][targetId] === undefined) {
        state.likeCounts[targetType][targetId] = 0;
      }
      state.likeCounts[targetType][targetId] += delta;
    },
    
    // 设置加载状态
    SET_LOADING(state, loading) {
      state.loading = loading;
    }
  },
  actions: {
    // 点赞
    async like({ commit, state }, { targetId, targetType = 'article' }) {
      try {
        commit('SET_LOADING', true);
        
        // 调用 API
        await likeApi.like({ targetId, targetType });
        
        // 更新状态
        commit('SET_LIKE_STATUS', { targetType, targetId, isLiked: true });
        commit('UPDATE_LIKE_COUNT', { targetType, targetId, delta: 1 });
        
        return true;
      } catch (error) {
        console.error('点赞失败:', error);
        // 恢复原状态
        commit('SET_LIKE_STATUS', { targetType, targetId, isLiked: state.likeStatus[targetType]?.[targetId] || false });
        commit('UPDATE_LIKE_COUNT', { targetType, targetId, delta: -1 });
        return false;
      } finally {
        commit('SET_LOADING', false);
      }
    },
    
    // 取消点赞
    async unlike({ commit, state }, { targetId, targetType = 'article' }) {
      try {
        commit('SET_LOADING', true);
        
        // 调用 API
        await likeApi.unlike({ targetId, targetType });
        
        // 更新状态
        commit('SET_LIKE_STATUS', { targetType, targetId, isLiked: false });
        commit('UPDATE_LIKE_COUNT', { targetType, targetId, delta: -1 });
        
        return true;
      } catch (error) {
        console.error('取消点赞失败:', error);
        // 恢复原状态
        commit('SET_LIKE_STATUS', { targetType, targetId, isLiked: state.likeStatus[targetType]?.[targetId] || true });
        commit('UPDATE_LIKE_COUNT', { targetType, targetId, delta: 1 });
        return false;
      } finally {
        commit('SET_LOADING', false);
      }
    },
    
    // 获取点赞状态
    async getLikeStatus({ commit }, { targetId, targetType = 'article' }) {
      try {
        const res = await likeApi.getLikeStatus({ targetId, targetType });
        commit('SET_LIKE_STATUS', { targetType, targetId, isLiked: res.data.isLiked });
        commit('SET_LIKE_COUNT', { targetType, targetId, count: res.data.count });
        return res.data;
      } catch (error) {
        console.error('获取点赞状态失败:', error);
        return { isLiked: false, count: 0 };
      }
    },
    
    // 批量获取点赞状态
    async getBatchLikeStatus({ commit }, { targetIds, targetType = 'article' }) {
      try {
        const res = await likeApi.getBatchLikeStatus({ targetIds, targetType });
        commit('SET_BATCH_LIKE_STATUS', { targetType, statusMap: res.data.statusMap });
        
        // 更新计数
        Object.keys(res.data.countMap).forEach(targetId => {
          commit('SET_LIKE_COUNT', { targetType, targetId, count: res.data.countMap[targetId] });
        });
        
        return res.data;
      } catch (error) {
        console.error('批量获取点赞状态失败:', error);
        return { statusMap: {}, countMap: {} };
      }
    }
  },
  getters: {
    // 获取点赞状态
    getLikeStatus: (state) => (targetId, targetType = 'article') => {
      return state.likeStatus[targetType]?.[targetId] || false;
    },
    
    // 获取点赞计数
    getLikeCount: (state) => (targetId, targetType = 'article') => {
      return state.likeCounts[targetType]?.[targetId] || 0;
    },
    
    // 获取批量点赞状态
    getBatchLikeStatus: (state) => (targetIds, targetType = 'article') => {
      const statusMap = {};
      targetIds.forEach(targetId => {
        statusMap[targetId] = state.likeStatus[targetType]?.[targetId] || false;
      });
      return statusMap;
    },
    
    // 获取批量点赞计数
    getBatchLikeCount: (state) => (targetIds, targetType = 'article') => {
      const countMap = {};
      targetIds.forEach(targetId => {
        countMap[targetId] = state.likeCounts[targetType]?.[targetId] || 0;
      });
      return countMap;
    }
  }
};
4. 点赞系统使用示例

在内容列表中使用点赞组件:

<template>
  <view class="content-list">
    <view 
      v-for="item in contentList" 
      :key="item.id"
      class="content-item"
    >
      <!-- 内容标题 -->
      <text class="content-title">{{ item.title }}</text>
      
      <!-- 内容摘要 -->
      <text class="content-summary">{{ item.summary }}</text>
      
      <!-- 内容操作 -->
      <view class="content-actions">
        <!-- 点赞按钮 -->
        <like-button
          :target-id="item.id"
          :target-type="'article'"
          :count="likeCount(item.id)"
          :liked="isLiked(item.id)"
          @like-change="handleLikeChange"
        />
        
        <!-- 其他操作 -->
        <view class="content-action">
          <text class="action-icon">💬</text>
          <text class="action-text">{{ item.commentCount }}</text>
        </view>
        
        <view class="content-action">
          <text class="action-icon">🔗</text>
          <text class="action-text">分享</text>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
import LikeButton from '@/components/LikeButton.vue';
import { mapGetters, mapActions } from 'vuex';

export default {
  components: {
    LikeButton
  },
  data() {
    return {
      contentList: [
        {
          id: '1',
          title: 'uni-app 开发最佳实践',
          summary: '本文介绍 uni-app 开发中的一些最佳实践,帮助你提高开发效率和应用性能。',
          commentCount: 23
        },
        {
          id: '2',
          title: 'uni-app 性能优化技巧',
          summary: '本文分享一些 uni-app 性能优化的技巧,帮助你打造更流畅的应用体验。',
          commentCount: 45
        }
      ]
    };
  },
  computed: {
    ...mapGetters('like', [
      'getLikeStatus',
      'getLikeCount'
    ])
  },
  methods: {
    ...mapActions('like', [
      'like',
      'unlike'
    ]),
    
    // 获取点赞状态
    isLiked(targetId) {
      return this.getLikeStatus(targetId, 'article');
    },
    
    // 获取点赞计数
    likeCount(targetId) {
      return this.getLikeCount(targetId, 'article');
    },
    
    // 处理点赞状态变化
    async handleLikeChange({ targetId, targetType, isLiked }) {
      if (isLiked) {
        // 点赞
        await this.like({ targetId, targetType });
      } else {
        // 取消点赞
        await this.unlike({ targetId, targetType });
      }
    }
  },
  mounted() {
    // 批量获取点赞状态
    const targetIds = this.contentList.map(item => item.id);
    this.$store.dispatch('like/getBatchLikeStatus', {
      targetIds,
      targetType: 'article'
    });
  }
};
</script>

<style scoped>
.content-list {
  padding: 20rpx;
}

.content-item {
  background-color: white;
  border-radius: 10rpx;
  padding: 20rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}

.content-title {
  font-size: 28rpx;
  font-weight: bold;
  margin-bottom: 15rpx;
  display: block;
  color: #333;
}

.content-summary {
  font-size: 24rpx;
  line-height: 1.5;
  margin-bottom: 20rpx;
  display: block;
  color: #666;
}

.content-actions {
  display: flex;
  align-items: center;
  border-top: 1rpx solid #F0F0F0;
  padding-top: 15rpx;
}

.content-action {
  display: flex;
  align-items: center;
  margin-left: 40rpx;
  font-size: 22rpx;
  color: #666;
}

.action-icon {
  margin-right: 8rpx;
  font-size: 24rpx;
}

/* 响应式设计 */
@media (max-width: 750rpx) {
  .content-list {
    padding: 15rpx;
  }
  
  .content-item {
    padding: 15rpx;
    margin-bottom: 15rpx;
  }
  
  .content-title {
    font-size: 26rpx;
  }
  
  .content-summary {
    font-size: 22rpx;
  }
}
</style>
5. 防作弊措施

为了防止恶意刷点赞,我们可以实现以下措施:

// utils/anti-cheat.js

// 点赞限制配置
const likeLimits = {
  maxPerMinute: 10,   // 每分钟最大点赞数
  maxPerHour: 50,     // 每小时最大点赞数
  maxPerDay: 200      // 每天最大点赞数
};

// 用户点赞记录
export const userLikeRecord = {
  // 记录用户点赞
  recordLike: (userId) => {
    const key = `like_record_${userId}`;
    let record = uni.getStorageSync(key) || [];
    
    // 添加新记录
    record.push({
      timestamp: Date.now()
    });
    
    // 清理过期记录(超过24小时)
    const now = Date.now();
    record = record.filter(item => now - item.timestamp < 24 * 60 * 60 * 1000);
    
    // 保存记录
    uni.setStorageSync(key, record);
    
    return record;
  },
  
  // 检查用户点赞是否超过限制
  checkLikeLimit: (userId) => {
    const key = `like_record_${userId}`;
    const record = uni.getStorageSync(key) || [];
    
    const now = Date.now();
    
    // 计算不同时间窗口内的点赞数
    const minuteLikes = record.filter(item => now - item.timestamp < 60 * 1000).length;
    const hourLikes = record.filter(item => now - item.timestamp < 60 * 60 * 1000).length;
    const dayLikes = record.filter(item => now - item.timestamp < 24 * 60 * 60 * 1000).length;
    
    // 检查限制
    if (minuteLikes > likeLimits.maxPerMinute) {
      return { allowed: false, message: '点赞过于频繁,请稍后再试' };
    }
    
    if (hourLikes > likeLimits.maxPerHour) {
      return { allowed: false, message: '每小时点赞数已达上限' };
    }
    
    if (dayLikes > likeLimits.maxPerDay) {
      return { allowed: false, message: '每天点赞数已达上限' };
    }
    
    return { allowed: true };
  }
};

// 验证点赞操作
export function validateLikeOperation(userId) {
  // 检查用户登录状态
  const userInfo = uni.getStorageSync('userInfo');
  if (!userInfo) {
    return { valid: false, message: '请先登录' };
  }
  
  // 检查点赞限制
  const limitCheck = userLikeRecord.checkLikeLimit(userId);
  if (!limitCheck.allowed) {
    return { valid: false, message: limitCheck.message };
  }
  
  return { valid: true };
}

代码优化建议

1. 点赞缓存优化

使用本地缓存提高点赞状态的读取性能:

// utils/like-cache.js

// 点赞缓存
export const likeCache = {
  // 缓存键前缀
  prefix: 'like_',
  
  // 缓存点赞状态
  setLikeStatus: (targetType, targetId, isLiked) => {
    const key = `${likeCache.prefix}${targetType}_${targetId}`;
    uni.setStorageSync(key, isLiked);
  },
  
  // 获取点赞状态
  getLikeStatus: (targetType, targetId) => {
    const key = `${likeCache.prefix}${targetType}_${targetId}`;
    return uni.getStorageSync(key) || false;
  },
  
  // 批量获取点赞状态
  getBatchLikeStatus: (targetType, targetIds) => {
    const statusMap = {};
    targetIds.forEach(targetId => {
      statusMap[targetId] = likeCache.getLikeStatus(targetType, targetId);
    });
    return statusMap;
  },
  
  // 清除点赞状态缓存
  clearLikeStatus: (targetType, targetId) => {
    const key = `${likeCache.prefix}${targetType}_${targetId}`;
    uni.removeStorageSync(key);
  },
  
  // 清除所有点赞状态缓存
  clearAllLikeStatus: () => {
    // 获取所有缓存键
    const keys = uni.getStorageInfoSync().keys;
    // 过滤出点赞缓存键
    const likeKeys = keys.filter(key => key.startsWith(likeCache.prefix));
    // 清除缓存
    likeKeys.forEach(key => {
      uni.removeStorageSync(key);
    });
  }
};

2. 点赞动画优化

使用 CSS 动画提高点赞动画的性能:

<template>
  <view class="like-button" @click="toggleLike">
    <view 
      class="like-icon"
      :class="{ 'liked': isLiked, 'animating': isAnimating }"
    >
      <text class="like-emoji">{{ isLiked ? '❤️' : '🤍' }}</text>
    </view>
    <view class="like-count" :class="{ 'liked': isLiked }">
      {{ likeCount }}
    </view>
  </view>
</template>

<script>
export default {
  // 组件逻辑...
  methods: {
    // 切换点赞状态
    toggleLike() {
      if (this.isAnimating) return;
      
      // 触发点赞动画
      this.isAnimating = true;
      setTimeout(() => {
        this.isAnimating = false;
      }, 300);
      
      // 其他逻辑...
    }
  }
};
</script>

<style scoped>
.like-button {
  display: flex;
  align-items: center;
  cursor: pointer;
  user-select: none;
}

.like-icon {
  margin-right: 8rpx;
  transition: all 0.3s ease;
}

.like-emoji {
  font-size: 32rpx;
  transition: all 0.3s ease;
}

.like-icon.liked .like-emoji {
  color: #FF3B30;
  filter: drop-shadow(0 0 4rpx rgba(255, 59, 48, 0.5));
}

.like-icon.animating {
  animation: likeAnimation 0.3s ease;
}

.like-count {
  font-size: 24rpx;
  color: #666;
  transition: all 0.3s ease;
}

.like-count.liked {
  color: #FF3B30;
}

/* 点赞动画 */
@keyframes likeAnimation {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
  }
}

/* 禁用状态 */
.like-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
</style>

3. 批量点赞处理

优化批量点赞状态的获取和更新:

// store/modules/like.js

// 批量获取点赞状态
export const getBatchLikeStatus = async (targetIds, targetType = 'article') => {
  try {
    // 先从缓存获取
    const cachedStatus = likeCache.getBatchLikeStatus(targetType, targetIds);
    
    // 检查缓存是否完整
    const cachedIds = Object.keys(cachedStatus);
    const uncachedIds = targetIds.filter(id => !cachedIds.includes(id));
    
    if (uncachedIds.length === 0) {
      // 所有状态都在缓存中
      return cachedStatus;
    }
    
    // 从服务器获取未缓存的状态
    const res = await likeApi.getBatchLikeStatus({ targetIds: uncachedIds, targetType });
    
    // 更新缓存
    Object.keys(res.data.statusMap).forEach(targetId => {
      likeCache.setLikeStatus(targetType, targetId, res.data.statusMap[targetId]);
    });
    
    // 合并缓存和服务器数据
    return {
      ...cachedStatus,
      ...res.data.statusMap
    };
  } catch (error) {
    console.error('批量获取点赞状态失败:', error);
    // 失败时返回缓存数据
    return likeCache.getBatchLikeStatus(targetType, targetIds);
  }
};

4. 点赞防抖

添加点赞操作的防抖,防止重复点击:

// utils/debounce.js

// 防抖函数
export function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// 应用到点赞操作
const debouncedLike = debounce(async (targetId, targetType) => {
  // 点赞逻辑
}, 1000);

总结

本教程详细介绍了 uni-app 点赞系统的实现方法,包括:

  1. 点赞系统架构:前端组件和后端服务的设计
  2. 点赞数据结构:合理的数据结构设计
  3. 点赞功能实现:点赞和取消点赞的功能
  4. 点赞状态管理:使用 Vuex 管理点赞状态
  5. 点赞计数更新:实时更新点赞计数
  6. 防作弊措施:防止恶意刷点赞

通过本教程的学习,您应该能够:

  • 理解点赞系统的核心概念和实现原理
  • 掌握点赞系统的前端和后端实现方法
  • 开发功能完整的点赞系统
  • 优化点赞系统的性能和安全性

点赞系统是增强用户互动的重要工具,一个好的点赞系统可以显著提升应用的用户体验和社区氛围。在实现点赞系统时,需要平衡功能丰富性、性能和安全性,为用户提供一个公平、流畅的互动体验。

« 上一篇 uni-app 评论系统 下一篇 » uni-app 收藏系统