尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Spring Boot与React实现用户收藏功能:从数据库设计到前后端状态同步

Spring Boot与React实现用户收藏功能:从数据库设计到前后端状态同步 最近在开发一个音乐播放器项目时遇到了一个经典需求如何让用户与播放器产生更深度的情感互动比如用户点击一个“爱心”按钮歌曲就被收藏到“我喜欢”的歌单里。这个看似简单的功能背后涉及到前端状态管理、后端API设计、数据库操作以及用户体验的连贯性。本文将以一个名为《Can You Love Me ?》的歌曲收藏功能为例手把手带你实现一套完整的“喜欢/收藏”技术方案。无论你是前端新手想学习状态同步还是后端开发者需要设计RESTful API或是全栈工程师寻找一个完整的实战案例这篇文章都能为你提供从零到一的实现路径。我们将从前端React组件与状态管理写起连接到Spring Boot后端服务最后落地到MySQL数据库并涵盖错误处理与性能优化的最佳实践。1. 背景与核心概念什么是“喜欢”功能在音乐、视频、文章等各类内容型应用中“喜欢”或“收藏”、“点赞”是一个最基础、最高频的用户互动功能。它的核心价值在于用户行为记录为用户提供一个快捷标记喜爱内容的方式形成个人化的内容库。推荐系统基础用户的行为数据是构建个性化推荐引擎如“猜你喜欢”的黄金燃料。内容热度衡量“喜欢”数常常作为内容排序如热门榜单的重要权重指标。增强用户粘性简单的交互能有效提升用户的参与感和归属感。从技术视角看一个完整的“喜欢”功能需要解决以下几个关键问题状态同步前端按钮状态已喜欢/未喜欢需要与服务器状态实时同步避免出现点击后状态回滚的糟糕体验。接口幂等性由于网络延迟用户可能快速双击按钮后端接口必须保证无论调用多少次结果都一致即同一用户对同一内容只能有一个“喜欢”记录。数据关系在数据库中它通常表现为一个“用户-内容”的多对多关系。一个用户可以喜欢多首歌一首歌也可以被多个用户喜欢。本文将实现的案例《Can You Love Me ?》就是一首歌内容实体。用户点击爱心按钮即创建或删除一条用户与这首歌的关联记录。2. 环境准备与版本说明为了完整复现本项目请确保你的开发环境包含以下组件。版本号以当前主流稳定版为例如果你的环境不同请注意调整依赖配置。后端环境 (Spring Boot):JDK: 17 或 11 (LTS版本)构建工具: Maven 3.6 或 Gradle 7.x框架: Spring Boot 2.7.x 或 3.0.x (本文示例基于Spring Boot 2.7.18)数据库: MySQL 8.0IDE: IntelliJ IDEA, Eclipse 或 VS Code前端环境 (React):Node.js: 16.x 或 18.x LTS包管理器: npm 或 yarn框架: React 18.xHTTP库: AxiosUI库: Ant Design (可选用于快速构建UI)IDE: VS Code, WebStorm项目结构预览music-like-demo/ ├── backend/ # Spring Boot后端项目 │ ├── src/main/java/com/example/music/ │ │ ├── controller/ # 控制器层 (LikeController) │ │ ├── service/ # 服务层 (LikeService) │ │ ├── repository/ # 数据访问层 (LikeRepository) │ │ ├── entity/ # 实体类 (User, Song, UserLikeSong) │ │ └── MusicApplication.java # 启动类 │ ├── src/main/resources/ │ │ ├── application.yml # 应用配置 │ │ └── ... │ └── pom.xml # Maven依赖管理 └── frontend/ # React前端项目 ├── src/ │ ├── components/ # React组件 (LikeButton) │ ├── services/ # API服务模块 (likeApi.js) │ ├── App.js │ └── index.js ├── package.json └── ...3. 核心原理与数据模型设计在动手编码前理解数据模型和API设计是至关重要的。3.1 数据库表设计我们至少需要三张表user: 用户表。song: 歌曲表存储《Can You Love Me ?》等歌曲信息。user_like_song:关联表核心中的核心记录用户喜欢歌曲的关系。它需要具备幂等约束。以下是MySQL的建表SQL语句-- 用户表 (简化版) CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(64) NOT NULL COMMENT 用户名, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 歌曲表 CREATE TABLE song ( id bigint NOT NULL AUTO_INCREMENT COMMENT 歌曲ID, title varchar(255) NOT NULL COMMENT 歌曲标题, artist varchar(255) DEFAULT NULL COMMENT 艺术家, cover_url varchar(512) DEFAULT NULL COMMENT 封面图URL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT歌曲表; -- 用户-歌曲喜欢关联表 CREATE TABLE user_like_song ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, user_id bigint NOT NULL COMMENT 用户ID, song_id bigint NOT NULL COMMENT 歌曲ID, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), -- 唯一约束保证同一用户对同一首歌只能有一条喜欢记录 UNIQUE KEY uk_user_song (user_id,song_id), -- 外键约束确保数据完整性可根据实际情况决定是否添加 KEY idx_user_id (user_id), KEY idx_song_id (song_id), CONSTRAINT fk_like_user FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE, CONSTRAINT fk_like_song FOREIGN KEY (song_id) REFERENCES song (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户喜欢歌曲关系表;关键点解释UNIQUE KEY uk_user_song (user_id, song_id): 这是实现接口幂等性的数据库保障。即使前端重复请求数据库也会阻止重复数据的插入从根源上防止数据冗余。外键约束ON DELETE CASCADE意味着当用户或歌曲被删除时其关联的喜欢记录也会自动删除保持数据清洁。在生产中根据业务复杂度有时会采用逻辑删除而非物理删除外键设计需相应调整。3.2 后端API设计 (RESTful风格)我们为“喜欢”功能设计两个核心接口方法端点描述幂等性POST/api/likes喜欢一首歌。如果已经喜欢则返回成功幂等。是DELETE/api/likes?userId{userId}songId{songId}取消喜欢一首歌。如果本来就没喜欢也返回成功幂等。是GET/api/likes/status?userId{userId}songId{songId}检查当前用户是否喜欢了某首歌。返回{ “liked”: true/false }是GET/api/users/{userId}/liked-songs获取某个用户喜欢的所有歌曲列表。是为什么使用POST和DELETE而不是GET根据RESTful规范创建资源使用POST删除资源使用DELETE。“喜欢”这个动作本质是创建一条关联记录因此用POST是语义正确的。使用GET来“创建”喜欢是不符合规范的且可能被网络爬虫无意触发。请求体与参数POST /api/likes: 请求体为JSON如{“userId”: 1, “songId”: 1001}。DELETE /api/likes: 通过查询参数传递userId和songId。在实际项目中userId通常从认证信息如JWT Token中获取而非前端传递以保障安全。本文为演示清晰暂由参数传递。4. 后端实战Spring Boot 实现4.1 创建Spring Boot项目并配置依赖使用 Spring Initializr 或IDE创建项目选择以下依赖Spring Web: 用于构建RESTful API。Spring Data JPA: 简化数据库操作。MySQL Driver: 连接MySQL数据库。Lombok(可选): 简化实体类代码。pom.xml关键依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies4.2 配置数据库连接在src/main/resources/application.yml中配置spring: datasource: url: jdbc:mysql://localhost:3306/music_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: your_username password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 首次启动可设为update自动建表生产环境建议使用none或validate并通过SQL脚本管理。 show-sql: true # 开发时开启方便查看生成的SQL properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect server: port: 80804.3 创建实体类(Entity)对应数据库的三张表我们创建三个JPA实体类。// 文件路径src/main/java/com/example/music/entity/User.java package com.example.music.entity; import lombok.Data; import javax.persistence.*; import java.util.ArrayList; import java.util.List; Entity Table(name user) Data public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false, unique true) private String username; // 一个用户可以有多个喜欢关系 OneToMany(mappedBy user, cascade CascadeType.ALL, orphanRemoval true) private ListUserLikeSong likedSongs new ArrayList(); }// 文件路径src/main/java/com/example/music/entity/Song.java package com.example.music.entity; import lombok.Data; import javax.persistence.*; import java.util.ArrayList; import java.util.List; Entity Table(name song) Data public class Song { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false) private String title; private String artist; private String coverUrl; // 一首歌可以被多个用户喜欢 OneToMany(mappedBy song, cascade CascadeType.ALL, orphanRemoval true) private ListUserLikeSong likedByUsers new ArrayList(); }// 文件路径src/main/java/com/example/music/entity/UserLikeSong.java package com.example.music.entity; import lombok.Data; import org.hibernate.annotations.CreationTimestamp; import javax.persistence.*; import java.time.LocalDateTime; Entity Table(name user_like_song, uniqueConstraints {UniqueConstraint(columnNames {user_id, song_id})}) // JPA层面声明唯一约束 Data public class UserLikeSong { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; ManyToOne(fetch FetchType.LAZY) JoinColumn(name user_id, nullable false) private User user; ManyToOne(fetch FetchType.LAZY) JoinColumn(name song_id, nullable false) private Song song; CreationTimestamp Column(updatable false) private LocalDateTime createTime; }4.4 创建数据访问层(Repository)Spring Data JPA 让数据库操作变得极其简单。// 文件路径src/main/java/com/example/music/repository/UserLikeSongRepository.java package com.example.music.repository; import com.example.music.entity.UserLikeSong; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; Repository public interface UserLikeSongRepository extends JpaRepositoryUserLikeSong, Long { // 根据用户ID和歌曲ID查找记录 OptionalUserLikeSong findByUserIdAndSongId(Long userId, Long songId); // 检查是否存在记录 boolean existsByUserIdAndSongId(Long userId, Long songId); // 根据用户ID和歌曲ID删除记录 void deleteByUserIdAndSongId(Long userId, Long songId); }4.5 创建服务层(Service)服务层包含核心业务逻辑特别是处理幂等性。// 文件路径src/main/java/com/example/music/service/LikeService.java package com.example.music.service; import com.example.music.entity.UserLikeSong; import com.example.music.repository.UserLikeSongRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityNotFoundException; Service RequiredArgsConstructor Slf4j public class LikeService { private final UserLikeSongRepository likeRepository; // 假设还有UserRepository和SongRepository这里为简化直接使用likeRepository /** * 喜欢一首歌 (幂等操作) * param userId 用户ID * param songId 歌曲ID * return 操作结果信息 */ Transactional public String likeSong(Long userId, Long songId) { // 1. 检查是否已喜欢 boolean alreadyLiked likeRepository.existsByUserIdAndSongId(userId, songId); if (alreadyLiked) { log.info(用户 {} 已经喜欢过歌曲 {}直接返回成功。, userId, songId); return Already liked; } // 2. 创建新的喜欢记录 (这里需要先获取User和Song实体为简化假设它们存在) // 实际项目中这里应该从UserRepository和SongRepository中findById并处理不存在的情况。 UserLikeSong like new UserLikeSong(); // like.setUser(userRepository.findById(userId).orElseThrow(...)); // like.setSong(songRepository.findById(songId).orElseThrow(...)); // 为演示我们创建一个“虚拟”关联仅作逻辑演示。实际代码需完善。 log.info(创建喜欢记录用户 {} - 歌曲 {}, userId, songId); // likeRepository.save(like); // 实际保存 return Liked successfully; } /** * 取消喜欢一首歌 (幂等操作) * param userId 用户ID * param songId 歌曲ID * return 操作结果信息 */ Transactional public String unlikeSong(Long userId, Long songId) { // 直接根据复合条件删除如果记录不存在deleteBy... 方法不会报错符合幂等性。 likeRepository.deleteByUserIdAndSongId(userId, songId); log.info(取消喜欢记录用户 {} - 歌曲 {}, userId, songId); return Unliked successfully; } /** * 检查喜欢状态 * param userId 用户ID * param songId 歌曲ID * return true 表示已喜欢false 表示未喜欢 */ public boolean isLiked(Long userId, Long songId) { return likeRepository.existsByUserIdAndSongId(userId, songId); } }4.6 创建控制器层(Controller)控制器负责接收HTTP请求调用服务并返回JSON响应。// 文件路径src/main/java/com/example/music/controller/LikeController.java package com.example.music.controller; import com.example.music.service.LikeService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; RestController RequestMapping(/api/likes) RequiredArgsConstructor public class LikeController { private final LikeService likeService; PostMapping public ResponseEntityMapString, Object like(RequestBody LikeRequest request) { String result likeService.likeSong(request.getUserId(), request.getSongId()); MapString, Object response new HashMap(); response.put(success, true); response.put(message, result); response.put(data, Map.of(liked, true)); // 操作后状态为已喜欢 return ResponseEntity.ok(response); } DeleteMapping public ResponseEntityMapString, Object unlike(RequestParam Long userId, RequestParam Long songId) { String result likeService.unlikeSong(userId, songId); MapString, Object response new HashMap(); response.put(success, true); response.put(message, result); response.put(data, Map.of(liked, false)); // 操作后状态为未喜欢 return ResponseEntity.ok(response); } GetMapping(/status) public ResponseEntityMapString, Object getLikeStatus(RequestParam Long userId, RequestParam Long songId) { boolean isLiked likeService.isLiked(userId, songId); MapString, Object response new HashMap(); response.put(success, true); response.put(data, Map.of(liked, isLiked)); return ResponseEntity.ok(response); } // 内部请求类 Data static class LikeRequest { private Long userId; private Long songId; } }启动Spring Boot应用 (MusicApplication)后端API就准备好了。你可以使用Postman或curl测试接口。5. 前端实战React Axios 实现5.1 创建React项目并安装依赖npx create-react-app frontend cd frontend npm install axios antd ant-design/icons5.2 创建API服务模块将所有的HTTP请求封装到一个独立的模块中便于管理和维护。// 文件路径frontend/src/services/likeApi.js import axios from axios; // 配置axios实例设置基础URL const apiClient axios.create({ baseURL: http://localhost:8080/api, // 后端服务地址 timeout: 10000, }); /** * 喜欢一首歌 * param {number} userId - 用户ID * param {number} songId - 歌曲ID * returns {Promise} - 包含操作结果的Promise */ export const likeSong (userId, songId) { return apiClient.post(/likes, { userId, songId }); }; /** * 取消喜欢一首歌 * param {number} userId - 用户ID * param {number} songId - 歌曲ID * returns {Promise} - 包含操作结果的Promise */ export const unlikeSong (userId, songId) { return apiClient.delete(/likes?userId${userId}songId${songId}); }; /** * 获取喜欢状态 * param {number} userId - 用户ID * param {number} songId - 歌曲ID * returns {Promise} - 包含状态数据的Promise */ export const getLikeStatus (userId, songId) { return apiClient.get(/likes/status?userId${userId}songId${songId}); };5.3 创建可复用的LikeButton组件这个组件是核心它要管理本地状态并与服务器同步。// 文件路径frontend/src/components/LikeButton.jsx import React, { useState, useEffect } from react; import { HeartOutlined, HeartFilled } from ant-design/icons; import { Button, message } from antd; import { likeSong, unlikeSong, getLikeStatus } from ../services/likeApi; import ./LikeButton.css; // 可选样式 const LikeButton ({ userId, songId, songTitle 这首歌 }) { const [liked, setLiked] useState(false); const [loading, setLoading] useState(false); const [initialized, setInitialized] useState(false); // 组件加载时获取初始喜欢状态 useEffect(() { const fetchLikeStatus async () { if (!userId || !songId) return; try { const response await getLikeStatus(userId, songId); if (response.data.success) { setLiked(response.data.data.liked); } } catch (error) { console.error(获取喜欢状态失败:, error); message.error(获取状态失败请检查网络); } finally { setInitialized(true); } }; fetchLikeStatus(); }, [userId, songId]); const handleLikeToggle async () { if (!userId || !songId) { message.warning(请先登录); return; } if (loading) return; // 防止重复点击 setLoading(true); try { if (liked) { // 当前是喜欢状态执行取消喜欢 await unlikeSong(userId, songId); setLiked(false); message.success(已取消喜欢《${songTitle}》); } else { // 当前是未喜欢状态执行喜欢 await likeSong(userId, songId); setLiked(true); message.success(已喜欢《${songTitle}》); } } catch (error) { console.error(操作失败:, error); // 根据错误类型细化提示 if (error.response) { // 请求已发出服务器响应状态码非2xx message.error(操作失败: ${error.response.data.message || 服务器错误}); } else if (error.request) { // 请求已发出但无响应 message.error(网络错误请检查连接); } else { // 请求配置出错 message.error(请求配置错误); } // 操作失败状态回滚可选更优体验是等待服务器确认后再更新UI // setLiked(prev !prev); } finally { setLoading(false); } }; // 如果状态未初始化完成可以显示加载中或默认状态 if (!initialized userId songId) { return Button icon{HeartOutlined /} loading shapecircle /; } return ( Button typetext icon{liked ? HeartFilled style{{ color: #ff4d4f }} / : HeartOutlined /} onClick{handleLikeToggle} loading{loading} shapecircle title{liked ? 取消喜欢《${songTitle}》 : 喜欢《${songTitle}》} classNamelike-button / ); }; export default LikeButton;5.4 在主应用中使用组件假设我们有一个歌曲列表或播放器界面。// 文件路径frontend/src/App.js import React from react; import { Card, Avatar, Space } from antd; import { PlayCircleOutlined } from ant-design/icons; import LikeButton from ./components/LikeButton; import ./App.css; const { Meta } Card; // 模拟当前登录用户和歌曲数据 const currentUserId 1; // 实际应从全局状态如Redux、Context或登录信息获取 const demoSong { id: 1001, title: Can You Love Me ?, artist: 董宇尘, coverUrl: https://example.com/cover.jpg, // 替换为实际图片URL description: 一首关于爱与追寻的原创歌曲。, }; function App() { return ( div classNameApp h1音乐播放器 - 《{demoSong.title}》/h1 Space directionvertical sizelarge Card style{{ width: 300 }} cover{img altsong-cover src{demoSong.coverUrl} /} actions{[ PlayCircleOutlined keyplay /, // 使用LikeButton组件 LikeButton keylike userId{currentUserId} songId{demoSong.id} songTitle{demoSong.title} /, ]} Meta avatar{Avatar srchttps://example.com/artist-avatar.jpg /} title{demoSong.title} description{歌手: ${demoSong.artist}} / p style{{ marginTop: 10px }}{demoSong.description}/p /Card div p用户ID: {currentUserId}/p p尝试点击歌曲卡片右下角的爱心按钮体验喜欢/取消喜欢功能。/p p功能包括状态初始化、点击交互、网络请求、成功/错误提示。/p /div /Space /div ); } export default App;现在运行前端 (npm start) 和后端应用你就可以看到一个完整的、可交互的“喜欢”功能了。6. 常见问题与排查思路在实现和运行上述功能时你可能会遇到以下典型问题问题现象可能原因排查步骤与解决方案前端点击按钮无反应控制台报错404或Network Error1. 后端服务未启动。2. 前端请求的API地址 (baseURL) 错误。3. 后端控制器路径 (RequestMapping) 不匹配。4. 跨域问题 (CORS)。1. 检查后端Spring Boot应用是否在localhost:8080成功启动。2. 核对likeApi.js中的baseURL。3. 核对LikeController的路径确保前端请求/api/likes。4. 在后端添加CORS配置CrossOrigin(origins http://localhost:3000)(开发环境) 或全局配置。点击喜欢按钮状态瞬间变化又变回来竞态条件或状态管理错误。前端在收到服务器成功响应前就更新了UI但请求失败后没有回滚状态。1. 确保handleLikeToggle中setLiked只在try块中请求成功后调用。2. 在catch块中不要自动回滚状态而是保持原状并提示用户操作失败。这就是“乐观更新”与“悲观更新”的选择。本文采用悲观更新等服务器确认。重复点击喜欢数据库出现多条相同记录数据库唯一约束uk_user_song未生效或后端幂等逻辑有漏洞。1. 检查数据库表是否成功创建了唯一约束 (SHOW CREATE TABLE user_like_song;)。2. 检查LikeService.likeSong方法是否先执行了existsByUserIdAndSongId检查。3. 确保服务方法加了Transactional注解保证检查与插入的原子性。取消喜欢后刷新页面又显示已喜欢前端状态 (liked) 初始化依赖的API (getLikeStatus) 返回了旧数据或缓存问题。1. 检查useEffect依赖项[userId, songId]是否正确。2. 在后端unlikeSong方法中确认deleteByUserIdAndSongId执行成功。3. 在前端可以尝试在操作成功后重新调用一次getLikeStatus来同步最新状态。JPA操作报LazyInitializationException在Controller返回JSON时序列化了懒加载 (FetchType.LAZY) 的关联属性如user.likedSongs。1. 使用JsonIgnore在实体类的关联字段上忽略序列化。2. 使用DTO (Data Transfer Object) 代替实体直接返回。3. 在查询时使用EntityGraph或JOIN FETCH主动抓取所需数据。7. 最佳实践与工程建议将功能跑通只是第一步要让代码健壮、可维护、高性能还需要遵循以下实践安全性是第一位永远不要信任前端传回的userId。在实际项目中用户ID必须从后端的安全上下文中获取如Spring Security的SecurityContextHolder。本文为演示简化了此流程。对songId等参数进行有效性校验防止无效ID冲击数据库。所有API尤其是修改数据的POST/DELETE应考虑添加防重放攻击和CSRF保护如果使用Session管理。性能优化缓存喜欢状态对于“是否喜欢”这种读远多于写、且对实时性要求不苛刻的状态可以引入缓存如Redis。在likeSong/unlikeSong操作时同时更新缓存在getLikeStatus时优先查询缓存。批量查询在歌曲列表页需要显示每首歌的喜欢状态时应提供一个批量查询接口如POST /api/likes/batch-status接收一个歌曲ID列表返回对应的喜欢状态Map避免N1查询问题。数据库索引确保user_like_song表上的(user_id, song_id)唯一索引存在它不仅是约束也是查询的性能保障。代码结构与可维护性使用DTOController不应直接返回JPA实体。创建LikeDTO、SongDTO等只暴露前端需要的字段避免数据泄露和循环引用。统一异常处理使用ControllerAdvice或RestControllerAdvice创建全局异常处理器统一处理EntityNotFoundException、DataIntegrityViolationException唯一约束冲突等并返回结构化的错误信息给前端。完善的日志在Service层的关键操作点如创建喜欢、取消喜欢记录INFO日志在捕获异常时记录ERROR日志便于问题追踪。前端用户体验防抖与加载状态对按钮点击事件可以加入防抖防止用户疯狂点击。同时loading状态能有效提示用户操作正在进行。乐观更新对于对成功率有信心的操作如点赞可以采用乐观更新先更新本地UI状态再发送请求。如果请求失败再回滚状态并提示。这能带来更迅捷的体验。离线考虑在移动端或弱网环境下可以考虑将操作记录暂存到本地如IndexedDB待网络恢复后同步到服务器。扩展性思考消息队列解耦“喜欢”动作可以作为一个领域事件发布出去。其他系统如推荐系统、热度计算服务、消息通知服务可以订阅这个事件实现系统间的解耦和异步处理。数据统计在user_like_song表中create_time字段可用于分析用户喜好随时间的变化趋势为推荐算法提供更丰富的特征。从一个小小的“喜欢”按钮出发我们完成了一次完整的前后端全栈之旅。这个功能麻雀虽小五脏俱全涵盖了RESTful API设计、数据库关系建模、JPA操作、React状态管理、用户体验处理等多个核心知识点。希望这个围绕《Can You Love Me ?》的实战案例能帮助你不仅实现功能更理解其背后的设计原理和工程考量。
返回列表