SpringBoot实现文章点赞

点赞这种需求还算是很常见的,其大体流程也是很容易想明白的。因为类似于点赞这种操作,如果用户比较闲,就是一顿点...点一下我就操作一下数据库,取消一下我再操作一下数据库......所以具体实现思路是:

  • 用户点“点赞”按钮

  • redis存储这个“赞”

  • 用户取消“赞”

  • redis随之取消“赞”

  • 一定时间后,系统将这些“赞”做持久化

思路是这样的,具体实现也是比较容易的:

redis缓存相关

  
         org.springframework.boot
         spring-boot-starter-data-redis
  
  1. 在maven引入依赖后,对redis进行相关配置

    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import java.net.UnknownHostException;
    
    @Configuration
    public class RedisConfig {
    
        @Bean
        @ConditionalOnMissingBean(name = "redisTemplate")
        public RedisTemplate redisTemplate(
                RedisConnectionFactory redisConnectionFactory)
                throws UnknownHostException {
    
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
    
            RedisTemplate template = new RedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            template.setKeySerializer(jackson2JsonRedisSerializer);
            template.setValueSerializer(jackson2JsonRedisSerializer);
            template.setHashKeySerializer(jackson2JsonRedisSerializer);
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            template.afterPropertiesSet();
            return template;
        }
    
        @Bean
        @ConditionalOnMissingBean(StringRedisTemplate.class)
        public StringRedisTemplate stringRedisTemplate(
                RedisConnectionFactory redisConnectionFactory)
                throws UnknownHostException {
            StringRedisTemplate template = new StringRedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
    }
     

    配置文件也要写一下:

      spring.redis.host=127.0.0.1
      spring.redis.port: 6379
    
    1. 定时任务相关
      一样的,引入定时的依赖:

             
             
                 org.springframework.boot
                 spring-boot-starter-quartz
             
      

      并配置:

         import com.hanor.blog.quartz.LikeTask;
         import org.quartz.*;
         import org.springframework.context.annotation.Bean;
         import org.springframework.context.annotation.Configuration;
         
         @Configuration
         public class QuartzConfig {
         
             private static final String LIKE_TASK_IDENTITY = "LikeTaskQuartz";
         
             @Bean
             public JobDetail quartzDetail(){
                 return JobBuilder.newJob(LikeTask.class).withIdentity(LIKE_TASK_IDENTITY).storeDurably().build();
             }
         
             @Bean
             public Trigger quartzTrigger(){
                 SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                         .withIntervalInSeconds(60)  //设置时间周期单位秒,这样效果更明显
                         //.withIntervalInHours(2)  //两个小时执行一次
                         .repeatForever();
                 return TriggerBuilder.newTrigger().forJob(quartzDetail())
                         .withIdentity(LIKE_TASK_IDENTITY)
                         .withSchedule(scheduleBuilder)
                         .build();
             }
         }
      

      制定任务:

        ```java
        import com.hanor.blog.service.LikedService;
        import org.quartz.JobExecutionContext;
        import org.quartz.JobExecutionException;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.scheduling.quartz.QuartzJobBean;
        import org.springframework.stereotype.Component;
        
        import java.text.SimpleDateFormat;
        
        /**
         * 点赞的定时任务
         */
        public class LikeTask extends QuartzJobBean {
        
            @Autowired
            private LikedService likedService;
        
            private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        
            @Override
            protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        
                System.out.println("-----------quartz------------");
                //将 Redis 里的点赞信息同步到数据库里
                likedService.transLikedFromRedis2DB();
                likedService.transLikedCountFromRedis2DB();
            }
        }
      
    1. 数据库表结构的设计

      因为博客项目算是个小项目了,这里为了演示方便,点赞这个模块就先以简易为主。

    liked_user_id为被赞者,liked_post_id为发出者。

    import com.hanor.blog.entity.enums.LikedStatusEnum;
    
    /**
     * 用户点赞表
     */
    
    public class UserLike {
    
        //主键id
        private String likeId;
    
        //被点赞的用户的id
        private String likedUserId;
    
        //点赞的用户的id
        private String likedPostId;
    
        //点赞的状态.默认未点赞
        private Integer status = LikedStatusEnum.UNLIKE.getCode();
    
        public UserLike() {
        }
    
        public UserLike(String likedUserId, String likedPostId, Integer status) {
            this.likedUserId = likedUserId;
            this.likedPostId = likedPostId;
            this.status = status;
        }
        //getter setter
    }
    

    其中,用了枚举。

     /**
         * 用户点赞的状态
      */
    public enum LikedStatusEnum {
        /**
         * 点赞
         */
        LIKE(1, "点赞"),
        /**
         * 取消赞
         */
        UNLIKE(0, "取消点赞/未点赞");
    
        private Integer code;
    
        private String msg;
    
        LikedStatusEnum(Integer code, String msg) {
            this.code = code;
            this.msg = msg;
        }
        public Integer getCode(){
            return this.code;
        }
    
        public String getMsg(){
            return this.msg;
        }
    }
    

    最终的表,是这个样子的:


    SpringBoot实现文章点赞_第1张图片
    点赞表
    1. 具体实现业务逻辑

      这里有两点:第一是,先把用户的“赞”存在缓存层;第二,适当的时间,将缓存的数据拿出,进行持久化操作。

      考虑到redis存储的特点,选用hash的形式对“用户点赞操作”及“用户被点赞数量”两项进行存储。采用hash的具体原因:把点赞造成的不同影响,储存为不同分区,方便管理。

      @程序猿DD

      因为 Hash 里的数据都是存在一个键里,可以通过这个键很方便的把所有的点赞数据都取出。

      这个键里面的数据还可以存成键值对的形式,方便存入点赞人、被点赞人和点赞状态。

    第一,先把用户的“赞”存在缓存层。

    import com.hanor.blog.entity.DTO.LikedCountDTO;
    import com.hanor.blog.entity.enums.LikedStatusEnum;
    import com.hanor.blog.entity.pojo.UserLike;
    import com.hanor.blog.service.RedisService;
    import com.hanor.blog.util.RedisKeyUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.Cursor;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.ScanOptions;
    import org.springframework.stereotype.Service;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    
    @Service
    public class RedisServiceImpl implements RedisService {
    
        @Autowired
        RedisTemplate redisTemplate;
    
        @Override
        public void saveLiked2Redis(String likedUserId, String likedPostId) {
            String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
            redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.LIKE.getCode());
        }
    
        @Override
        public void unlikeFromRedis(String likedUserId, String likedPostId) {
            String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
            redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.UNLIKE.getCode());
        }
    
        @Override
        public void deleteLikedFromRedis(String likedUserId, String likedPostId) {
            String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
            redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
        }
    
        @Override
        public void incrementLikedCount(String likedUserId) {
            redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, 1);
        }
    
        @Override
        public void decrementLikedCount(String likedUserId) {
            redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, -1);
        }
    
        @Override
        public List getLikedDataFromRedis() {
            Cursor> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE);
            List list = new ArrayList<>();
            while (cursor.hasNext()){
                Map.Entry entry = cursor.next();
                String key = (String) entry.getKey();
                //分离出 likedUserId,likedPostId
                String[] split = key.split("::");
                String likedUserId = split[0];
                String likedPostId = split[1];
                Integer value = (Integer) entry.getValue();
    
                //组装成 UserLike 对象
                UserLike userLike = new UserLike(likedUserId, likedPostId, value);
                list.add(userLike);
    
                //存到 list 后从 Redis 中删除
                redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
            }
    
            return list;
        }
    
        @Override
        public List getLikedCountFromRedis() {
            Cursor> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE);
            List list = new ArrayList<>();
            while (cursor.hasNext()){
                Map.Entry map = cursor.next();
                //将点赞数量存储在 LikedCountDT
                String key = (String)map.getKey();
                LikedCountDTO dto = new LikedCountDTO(key, (Integer) map.getValue());
                list.add(dto);
                //从Redis中删除这条记录
                redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key);
            }
            return list;
        }
    }
    

    第二,持久化操作。

    import com.alibaba.fastjson.JSONObject;
    import com.hanor.blog.dao.BlogArticleMapper;
    import com.hanor.blog.dao.UserLikeMapper;
    import com.hanor.blog.entity.DTO.LikedCountDTO;
    import com.hanor.blog.entity.pojo.BlogArticle;
    import com.hanor.blog.entity.pojo.UserLike;
    import com.hanor.blog.service.LikedService;
    import com.hanor.blog.service.RedisService;
    import com.hanor.blog.util.IdUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    import org.springframework.stereotype.Service;
    
    import java.util.Date;
    import java.util.List;
    @Service
    public class LikedServiceImpl implements LikedService {
    
        @Autowired
        private RedisService redisService;
        @Autowired
        private UserLikeMapper userLikeMapper;
        @Autowired
        private BlogArticleMapper blogArticleMapper;
     
        @Override
        public int save(UserLike userLike) {
            return userLikeMapper.saveLike(userLike);
        }
    
        @Override
        public void saveAll(List list) {
            for (UserLike userLike : list) {
                userLikeMapper.saveLike(userLike);
            }
        }
    
        @Override
        public Page getLikedListByLikedUserId(String likedUserId, Pageable pageable) {
            return null;
        }
    
        @Override
        public Page getLikedListByLikedPostId(String likedPostId, Pageable pageable) {
            return null;
        }
    
        @Override
        public int getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId) {
            UserLike userLike = new UserLike();
            userLike.setLikedPostId(likedPostId);
            userLike.setLikedUserId(likedUserId);
            return userLikeMapper.searchLike(userLike);
        }
    
        @Override
        public void transLikedFromRedis2DB() {
            List userLikeList = redisService.getLikedDataFromRedis();
            for (UserLike like : userLikeList) {
                Integer userLikeExist = userLikeMapper.searchLike(like);
                if (userLikeExist > 0){
                    userLikeMapper.updateLike(like);
                }else {
                    like.setLikeId(IdUtil.nextId() + "");
                    userLikeMapper.saveLike(like);
                }
            }
        }
    
        @Override
        public void transLikedCountFromRedis2DB() {
            List likedCountDTOs = redisService.getLikedCountFromRedis();
    
            for (LikedCountDTO dto : likedCountDTOs) {
                JSONObject blogArticle = blogArticleMapper.getArticleById(dto.getUserId());
                if (null != blogArticle){
                    BlogArticle article = new BlogArticle();
                    article.setUpdateTime(new Date());
                    article.setArticleId(blogArticle.getString("articleId"));
                    article.setArticleLike(blogArticle.getInteger("articleLike") + dto.getLikedNum());
                    blogArticleMapper.updateArticle(article);
                }else {
                    return;
                }
            }
        }
    }
    
    1. 用到的工具类
      对点赞信息进行redis储存的id生成:
    public class RedisKeyUtils {
    
        //保存用户点赞数据的key
        public static final String MAP_KEY_USER_LIKED = "MAP_USER_LIKED";
        //保存用户被点赞数量的key
        public static final String MAP_KEY_USER_LIKED_COUNT = "MAP_USER_LIKED_COUNT";
    
        /**
         * 拼接被点赞的用户id和点赞的人的id作为key。格式 222222::333333
         * @param likedUserId 被点赞的人id
         * @param likedPostId 点赞的人的id
         * @return
         */
        public static String getLikedKey(String likedUserId, String likedPostId){
            StringBuilder builder = new StringBuilder();
            builder.append(likedUserId);
            builder.append("::");
            builder.append(likedPostId);
            return builder.toString();
        }
    }
    

    因为想做一个分布式项目,所以项目用到的id生成策略采用了雪花算法,代码过长,就不贴了。
    测试,给测试来个接口,用postman测吧。

    import com.hanor.blog.entity.pojo.UserLike;
    import com.hanor.blog.service.RedisService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/api/like")
    public class LikeController {
    
        @Autowired
        private RedisService redisService;
    
        @PostMapping
        public void doLike(@RequestBody UserLike userLike){
            redisService.saveLiked2Redis(userLike.getLikedUserId(),userLike.getLikedPostId());
            redisService.incrementLikedCount(userLike.getLikedPostId());
        }
    

    发送值为:

    {
            "likedUserId":"123",
            "likedPostId":"456"
    }
    

    此时缓存中可见:

    ![image](https://upload-images.jianshu.io/upload_images/19860184-50ad4050f1d8cc3f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 
    
    过了一会,缓存数据将被存进数据库中:缓存中没有数据,且值被写入数据库。
    
    ![image](https://upload-images.jianshu.io/upload_images/19860184-c0ad37bfc244af80.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 
    

    至此,点赞完成!!!撒花★,°:.☆( ̄▽ ̄)/$:.°★

    你可能感兴趣的:(SpringBoot实现文章点赞)