点赞这里我们分为三种情况,给文章点赞,给评论点赞,给评论的回复点赞;
public abstract class LikeTypeRole {
// 给文章点赞
public static final int ARTICLE_LIKE = 1;
// 给评论点赞
public static final int COMMENT_LIKE = 2;
// 给回复点赞
public static final int REPLY_LIKE = 3;
}
数据库表相应字段如下:
1.基本逻辑
1.1 Controller
提供点赞接口
@PutMapping("/like")
public ResponseEntity<Void> like(@RequestBody LikeDto likeDto){
if(this.likeService.like(likeDto)){
return ResponseEntity.noContent().build();
}else {
return ResponseEntity.badRequest().build();
}
}
LikeDto 是封装的点赞的请求对象
public class LikeDto {
/** 1.给文章点赞,2.评论点赞 3.给回复点赞*/
private Integer type;
/** 相应的 id */
private Long toId;
public Integer getType() {
if(type == null){
return LikeTypeRole.ARTICLE_LIKE;
}
return type;
}
public void setType(Integer type) {
this.type = type;
}
public Long getToId() {
return toId;
}
}
1.2 Service
点赞具体逻辑
public Boolean like(LikeDto likeDto) {
Integer type = likeDto.getType();
if(type == LikeTypeRole.ARTICLE_LIKE ){
// 更新文章点赞数
this.articleDao.updateLike(likeDto.getToId());
return true;
}else if(type == LikeTypeRole.COMMENT_LIKE ){
// 更新评论点赞数
this.commentDao.updateComLike(likeDto.getToId());
return true;
}else {
// 更新回复点赞数
this.replyDao.updateRepLike(likeDto.getToId());
return true;
}
return false;
}
1.3 Dao
这里就不把所有的 dao 层代码列出来了,只列出更新点赞数的方法及Sql(mybatis)
@Update("UPDATE article SET alike=alike+1 WHERE id=#{id}")
public void updateLike(@Param("id") Long id);
@Update("UPDATE `comment` SET com_like=com_like+1 WHERE id=#{id}")
public void updateComLike(@Param("id") Long id);
@Update("UPDATE reply SET rep_like=rep_like+1 WHERE rep_id=#{repId}")
public void updateRepLike(@Param("repId") Long repId);
上面虽然实现了最基本的点赞功能,但是根本无法防止一个用户对谋篇文章、回复、评论重复多次点赞。
2.实现防刷赞
其实防止刷赞的实现有很多方案,下面我们来演示其中一种思路。
-
在用户要点赞前判断用户是否登录,不登录就不能点赞
-
用户点赞后就将用户 id 保存到 redis 中,key 如下:
public abstract class LikeRedisRole { // 文章点赞的 redis key(like:article:articleId) public static final String PREFIX_KEY_ARTICLE = "like:article:"; // 评论点赞的 redis key(like:comment:commentId) public static final String PREFIX_KEY_COMMENT = "like:comment:"; // 评论点赞的 redis key(like:reply:replyId) public static final String PREFIX_KEY_REPLY = "like:reply:"; }
我们采用的结构是 Set,因为 Set 是一个不能添加重复的元素的集合。
-
当用户再要点赞时,判断 redis 中相应文章、评论、回复是否有该用户的点赞记录。
2.1 登录拦截
关于 JWT 鉴权的逻辑这里就不列出了,具体可以参考我的这篇文章
@Component
/**
* 校验当前用户是否已经登录
*/
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Resource
private JwtProperties jwtProperties; // 获取 cookie 信息
private static final ThreadLocal<Long> THREAD_LOCAL = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception{
// 获取 cookie 中的 token
String cookieValue = CookieUtils.getCookieValue(request, this.jwtProperties.getCookieName());
// 如果请求中没有 token
if(StringUtils.isBlank(cookieValue)){
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
// 校验 token 合法性,是否包含了 user 信息
UserInfo user = JwtUtils.getInfoFromToken(cookieValue,jwtProperties.getPublicKey());
if(user == null){
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
// 将 token 中保存的当前 user 的 id 放入 ThreadLocal
THREAD_LOCAL.set(user.getId());
return true;
}
public static Long getUserId(){
return THREAD_LOCAL.get();
}
/**
* 在请求处理完后删除 ThreadLocal 中保存的 id
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
THREAD_LOCAL.remove();
}
}
然后配置拦截路径
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private UvInterceptor uvInterceptor;
public void addInterceptors(InterceptorRegistry registry){
// 登录拦截
registry.addInterceptor(loginInterceptor).addPathPatterns("/user/detail")
.addPathPatterns("/like") // 点赞
.addPathPatterns("/upload/**")
.addPathPatterns("/article/post")
.addPathPatterns("/source/detail/*");
}
}
1.2 根据用户 ID 判断该用户能否点赞
这时也要修改 LikeService 了,添加一个 doLike 方法去判断该用户机是否已经点过赞,只有没点过赞的才能可以
@Override
@Transactional
public Boolean like(LikeDto likeDto) {
Integer type = likeDto.getType();
if(type == LikeTypeRole.ARTICLE_LIKE ){
// 判断能否点赞,redis key 为 like:article:articleId
if(doLike(LikeRedisRole.PREFIX_KEY_ARTICLE + likeDto.getToId())){
// 更新文章点赞数
this.articleDao.updateLike(likeDto.getToId());
return true;
}
}else if(type == LikeTypeRole.COMMENT_LIKE ){
// 判断能否点赞,redis key 为 like:comment:commentId
if(doLike(LikeRedisRole.PREFIX_KEY_COMMENT + likeDto.getToId())){
// 更新评论点赞数
this.commentDao.updateComLike(likeDto.getToId());
return true;
}
}else {
// 判断能否点赞,redis key 为 like:reply:replyId
if(doLike(LikeRedisRole.PREFIX_KEY_REPLY + likeDto.getToId())){
// 更新回复点赞数
this.replyDao.updateRepLike(likeDto.getToId());
return true;
}
}
return false;
}
/**
* 根据用户ID判断当前用户机能否点赞,
*/
private boolean doLike(String key){
// 如果 redis 中没有该文章、评论、回复的记录,可以点赞
if(!this.redisTemplate.hasKey(key)){
Long userId = LoginInterceptor.getUserId();
// 将当前用户ID放到 redis
this.redisTemplate.opsForSet().add(key,String.valueOf(userId));
return true;
// 如果 redis 中已经有该文章、评论、回复的点赞记录
}else {
// 判断当前用户是否点过赞
if(this.redisTemplate.opsForSet().isMember(key,String.valueOf(LoginInterceptor.getUserId()))){
return false; // 点过赞就返回 false
}else{
// 没点过赞就将该用户 ID 放到 redis,然后返回 true
this.redisTemplate.opsForSet().add(key,String.valueOf(LoginInterceptor.getUserId()));
return true;
}
}
}
之后我们就可以业务的实际体量,选择是将 redis 中的点赞情况定时落库,还是索性全部都持久化到 redis。