Java收藏功能案例如何实操

wen java案例 29

本文目录导读:

Java收藏功能案例如何实操

  1. 系统设计
  2. 后端实现
  3. 前端实现(Vue3 + Element Plus)
  4. Redis缓存优化
  5. MyBatis Mapper
  6. maven依赖
  7. 使用注意事项

我来详细介绍Java中实现收藏功能的完整实操案例,包含前端和后端的核心代码。

系统设计

数据表设计

-- 收藏表
CREATE TABLE favorites (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    item_id BIGINT NOT NULL,
    item_type VARCHAR(50) NOT NULL,  -- 收藏类型:article/product/...VARCHAR(255),
    cover_url VARCHAR(500),
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_user_item (user_id, item_id, item_type),
    INDEX idx_user_id (user_id),
    INDEX idx_item_id (item_id)
);
-- 用户收藏计数表(可选,用于性能优化)
CREATE TABLE favorite_counts (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    item_id BIGINT NOT NULL,
    item_type VARCHAR(50) NOT NULL,
    count INT DEFAULT 0,
    UNIQUE KEY uk_item (item_id, item_type)
);

实体类

// 收藏实体
@Data
@TableName("favorites")
public class Favorite {
    private Long id;
    private Long userId;
    private Long itemId;
    private String itemType;
    private String title;
    private String coverUrl;
    private LocalDateTime createTime;
}
// 收藏计数字段(用于响应)
@Data
public class FavoriteVO {
    private Long itemId;
    private String itemType;
    private boolean isFavorited;  // 当前用户是否已收藏
    private Integer favoriteCount; // 收藏总数
}

后端实现

Service层

@Service
@Transactional
public class FavoriteService {
    @Autowired
    private FavoriteMapper favoriteMapper;
    @Autowired
    private FavoriteCountMapper countMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    // 收藏/取消收藏(切换操作)
    public FavoriteVO toggleFavorite(Long userId, Long itemId, String itemType) {
        // 1. 检查是否已收藏
        Favorite existing = favoriteMapper.findByUserAndItem(
            userId, itemId, itemType);
        if (existing != null) {
            // 已收藏,执行取消收藏
            favoriteMapper.deleteById(existing.getId());
            updateCount(itemId, itemType, -1);
            return buildFavoriteVO(itemId, itemType, false);
        } else {
            // 未收藏,执行收藏
            Favorite favorite = new Favorite();
            favorite.setUserId(userId);
            favorite.setItemId(itemId);
            favorite.setItemType(itemType);
            // 获取标题和封面图(可根据实际业务调整)
            favorite.setTitle(getItemTitle(itemId, itemType));
            favorite.setCoverUrl(getItemCoverUrl(itemId, itemType));
            favoriteMapper.insert(favorite);
            updateCount(itemId, itemType, 1);
            return buildFavoriteVO(itemId, itemType, true);
        }
    }
    // 更新收藏计数
    private void updateCount(Long itemId, String itemType, int delta) {
        // 更新数据库计数
        countMapper.updateCount(itemId, itemType, delta);
        // 更新Redis缓存
        String redisKey = "favorite:count:" + itemType + ":" + itemId;
        redisTemplate.opsForValue().increment(redisKey, delta);
    }
    // 检查是否已收藏
    public boolean isFavorited(Long userId, Long itemId, String itemType) {
        return favoriteMapper.findByUserAndItem(userId, itemId, itemType) != null;
    }
    // 获取收藏总数
    public Integer getFavoriteCount(Long itemId, String itemType) {
        String redisKey = "favorite:count:" + itemType + ":" + itemId;
        Object count = redisTemplate.opsForValue().get(redisKey);
        if (count != null) {
            return (Integer) count;
        }
        // 从数据库获取并缓存
        Integer dbCount = countMapper.getCount(itemId, itemType);
        if (dbCount == null) {
            dbCount = 0;
        }
        redisTemplate.opsForValue().set(redisKey, dbCount, 1, TimeUnit.HOURS);
        return dbCount;
    }
    // 构建返回VO
    private FavoriteVO buildFavoriteVO(Long itemId, String itemType, boolean isFavorited) {
        FavoriteVO vo = new FavoriteVO();
        vo.setItemId(itemId);
        vo.setItemType(itemType);
        vo.setFavorited(isFavorited);
        vo.setFavoriteCount(getFavoriteCount(itemId, itemType));
        return vo;
    }
    // 获取用户收藏列表
    public Page<Favorite> getUserFavorites(Long userId, int page, int size, String itemType) {
        QueryWrapper<Favorite> wrapper = new QueryWrapper<>();
        wrapper.eq("user_id", userId);
        if (StringUtils.isNotBlank(itemType)) {
            wrapper.eq("item_type", itemType);
        }
        wrapper.orderByDesc("create_time");
        Page<Favorite> pageInfo = new Page<>(page, size);
        return favoriteMapper.selectPage(pageInfo, wrapper);
    }
}

Controller层

@RestController
@RequestMapping("/api/favorite")
public class FavoriteController {
    @Autowired
    private FavoriteService favoriteService;
    // 切换收藏状态
    @PostMapping("/toggle")
    public Result<FavoriteVO> toggleFavorite(
            @RequestParam Long itemId,
            @RequestParam String itemType) {
        Long userId = getCurrentUserId(); // 从token获取用户ID
        FavoriteVO result = favoriteService.toggleFavorite(userId, itemId, itemType);
        return Result.success(result);
    }
    // 检查是否已收藏
    @GetMapping("/status")
    public Result<Boolean> checkFavoriteStatus(
            @RequestParam Long itemId,
            @RequestParam String itemType) {
        Long userId = getCurrentUserId();
        boolean isFavorited = favoriteService.isFavorited(userId, itemId, itemType);
        return Result.success(isFavorited);
    }
    // 获取收藏总数
    @GetMapping("/count")
    public Result<Integer> getFavoriteCount(
            @RequestParam Long itemId,
            @RequestParam String itemType) {
        Integer count = favoriteService.getFavoriteCount(itemId, itemType);
        return Result.success(count);
    }
    // 获取用户收藏列表
    @GetMapping("/list")
    public Result<Page<Favorite>> getUserFavorites(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(required = false) String itemType) {
        Long userId = getCurrentUserId();
        Page<Favorite> favorites = favoriteService.getUserFavorites(userId, page, size, itemType);
        return Result.success(favorites);
    }
}

前端实现(Vue3 + Element Plus)

收藏按钮组件

<template>
  <div class="favorite-wrapper">
    <el-button 
      :type="isFavorited ? 'danger' : 'default'"
      :icon="isFavorited ? 'StarFilled' : 'Star'"
      @click="toggleFavorite"
      :loading="loading"
      size="small"
    >
      {{ isFavorited ? '已收藏' : '收藏' }}
      <span v-if="favoriteCount > 0" class="count-badge">
        {{ favoriteCount }}
      </span>
    </el-button>
  </div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const props = defineProps({
  itemId: {
    type: Number,
    required: true
  },
  itemType: {
    type: String,
    required: true
  }
})
const isFavorited = ref(false)
const favoriteCount = ref(0)
const loading = ref(false)
// 获取收藏状态和数量
const fetchFavoriteStatus = async () => {
  try {
    const [statusRes, countRes] = await Promise.all([
      axios.get('/api/favorite/status', {
        params: { itemId: props.itemId, itemType: props.itemType }
      }),
      axios.get('/api/favorite/count', {
        params: { itemId: props.itemId, itemType: props.itemType }
      })
    ])
    isFavorited.value = statusRes.data.data
    favoriteCount.value = countRes.data.data
  } catch (error) {
    console.error('获取收藏状态失败:', error)
  }
}
// 切换收藏
const toggleFavorite = async () => {
  if (loading.value) return
  loading.value = true
  try {
    const res = await axios.post('/api/favorite/toggle', null, {
      params: {
        itemId: props.itemId,
        itemType: props.itemType
      }
    })
    const result = res.data.data
    isFavorited.value = result.favorited
    favoriteCount.value = result.favoriteCount
    ElMessage.success(result.favorited ? '收藏成功' : '已取消收藏')
  } catch (error) {
    ElMessage.error('操作失败,请重试')
  } finally {
    loading.value = false
  }
}
onMounted(() => {
  fetchFavoriteStatus()
})
</script>
<style scoped>
.favorite-wrapper {
  display: inline-flex;
  align-items: center;
}
.count-badge {
  margin-left: 4px;
  background: rgba(255, 255, 255, 0.2);
  padding: 1px 6px;
  border-radius: 10px;
  font-size: 12px;
}
</style>

收藏列表页面

<template>
  <div class="favorites-page">
    <h2>我的收藏</h2>
    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
      <el-tab-pane label="全部" name="all"></el-tab-pane>
      <el-tab-pane label="文章" name="article"></el-tab-pane>
      <el-tab-pane label="商品" name="product"></el-tab-pane>
    </el-tabs>
    <!-- 收藏列表 -->
    <div v-loading="loading" class="favorites-list">
      <el-empty v-if="favorites.length === 0 && !loading" description="暂无收藏" />
      <el-card 
        v-for="item in favorites" 
        :key="item.id" 
        class="favorite-item"
      >
        <div class="item-content">
          <el-image 
            v-if="item.coverUrl" 
            :src="item.coverUrl" 
            class="item-cover"
          />
          <div class="item-info">
            <h3>{{ item.title }}</h3>
            <p class="item-time">
              收藏于:{{ formatDate(item.createTime) }}
            </p>
          </div>
          <el-button 
            type="danger" 
            size="small" 
            @click="removeFavorite(item.id)"
            :loading="deletingId === item.id"
          >
            取消收藏
          </el-button>
        </div>
      </el-card>
    </div>
    <!-- 分页 -->
    <el-pagination
      v-if="total > 0"
      v-model:current-page="currentPage"
      :page-size="pageSize"
      :total="total"
      layout="prev, pager, next"
      @current-change="fetchFavorites"
    />
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
import dayjs from 'dayjs'
const favorites = ref([])
const loading = ref(false)
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const activeTab = ref('all')
const itemTypeMap = {
  'all': null,
  'article': 'article',
  'product': 'product'
}
const fetchFavorites = async () => {
  loading.value = true
  try {
    const res = await axios.get('/api/favorite/list', {
      params: {
        page: currentPage.value,
        size: pageSize.value,
        itemType: itemTypeMap[activeTab.value]
      }
    })
    favorites.value = res.data.data.records
    total.value = res.data.data.total
  } catch (error) {
    console.error('获取收藏列表失败:', error)
    ElMessage.error('获取收藏列表失败')
  } finally {
    loading.value = false
  }
}
const removeFavorite = async (favoriteId) => {
  try {
    await ElMessageBox.confirm('确定要取消收藏吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })
    // 使用toggle接口实现
    const item = favorites.value.find(f => f.id === favoriteId)
    await axios.post('/api/favorite/toggle', null, {
      params: {
        itemId: item.itemId,
        itemType: item.itemType
      }
    })
    // 刷新列表
    await fetchFavorites()
    ElMessage.success('已取消收藏')
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('操作失败')
    }
  }
}
const handleTabChange = () => {
  currentPage.value = 1
  fetchFavorites()
}
const formatDate = (date) => {
  return dayjs(date).format('YYYY-MM-DD HH:mm')
}
onMounted(() => {
  fetchFavorites()
})
</script>

Redis缓存优化

@Component
public class FavoriteCacheManager {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    // 批量获取收藏状态
    public Map<Long, Boolean> getFavoriteStatusBatch(Long userId, List<Long> itemIds, String itemType) {
        Map<Long, Boolean> result = new HashMap<>();
        List<String> keys = itemIds.stream()
            .map(id -> "favorite:user:" + userId + ":" + itemType + ":" + id)
            .collect(Collectors.toList());
        List<Object> cachedValues = redisTemplate.opsForValue().multiGet(keys);
        for (int i = 0; i < itemIds.size(); i++) {
            Object value = cachedValues.get(i);
            result.put(itemIds.get(i), value != null && (Boolean) value);
        }
        return result;
    }
    // 批量获取收藏计数
    public Map<Long, Integer> getFavoriteCountBatch(List<Long> itemIds, String itemType) {
        Map<Long, Integer> result = new HashMap<>();
        List<String> keys = itemIds.stream()
            .map(id -> "favorite:count:" + itemType + ":" + id)
            .collect(Collectors.toList());
        List<Object> cachedValues = redisTemplate.opsForValue().multiGet(keys);
        for (int i = 0; i < itemIds.size(); i++) {
            Object value = cachedValues.get(i);
            result.put(itemIds.get(i), value != null ? (Integer) value : 0);
        }
        return result;
    }
}

MyBatis Mapper

@Mapper
public interface FavoriteMapper extends BaseMapper<Favorite> {
    @Select("SELECT * FROM favorites WHERE user_id = #{userId} AND item_id = #{itemId} AND item_type = #{itemType} LIMIT 1")
    Favorite findByUserAndItem(@Param("userId") Long userId, 
                               @Param("itemId") Long itemId, 
                               @Param("itemType") String itemType);
    @Select("SELECT COUNT(*) FROM favorites WHERE item_id = #{itemId} AND item_type = #{itemType}")
    Integer countByItem(@Param("itemId") Long itemId, @Param("itemType") String itemType);
    @Select("SELECT EXISTS(SELECT 1 FROM favorites WHERE user_id = #{userId} AND item_id = #{itemId} AND item_type = #{itemType})")
    boolean existsByUserAndItem(@Param("userId") Long userId, 
                                @Param("itemId") Long itemId, 
                                @Param("itemType") String itemType);
}

maven依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

使用注意事项

  1. 防重复收藏:数据库添加唯一索引
  2. 性能优化:使用Redis缓存热门数据的收藏计数
  3. 数据一致性:考虑缓存和数据库的同步策略
  4. 安全性:校验用户身份,防止恶意操作
  5. 扩展性:支持多种收藏类型(文章、商品、视频等)

这个案例实现了完整的收藏功能,你可以根据实际业务需求进行调整和扩展。

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