本文目录导读:

我来详细介绍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>
使用注意事项
- 防重复收藏:数据库添加唯一索引
- 性能优化:使用Redis缓存热门数据的收藏计数
- 数据一致性:考虑缓存和数据库的同步策略
- 安全性:校验用户身份,防止恶意操作
- 扩展性:支持多种收藏类型(文章、商品、视频等)
这个案例实现了完整的收藏功能,你可以根据实际业务需求进行调整和扩展。