本文目录导读:

我来详细介绍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缓存热门数据的收藏计数
- 数据一致性:考虑缓存和数据库的同步策略
- 安全性:校验用户身份,防止恶意操作
- 扩展性:支持多种收藏类型(文章、商品、视频等)
这个案例实现了完整的收藏功能,你可以根据实际业务需求进行调整和扩展。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播