黑马点评全功能实现总结

1. 项目介绍

       黑马点评项目是一个前后端分离项目,类似于大众点评,实现了发布查看商家,达人探店,点赞,关注等功能,业务可以帮助商家引流,增加曝光度,也可以为用户提供查看提供附近消费场所,主要。用来配合学习Redis的知识。

 1.1 项目使用的技术栈

      SpringBoot+MySql+Lombok+MyBatis-Plus+Hutool+Redis

 1.2项目架构

      采用单体架构

黑马点评全功能实现总结_第1张图片

1.3项目地址

黑马点评: 基于springboot +mybatis+redis实现的多功能探店APP,涵盖发博客,评价,定位,关注,共同关注,秒杀,消息推送等多个功能

2.功能模块

2.1用户登录模块

手机号创建用户

private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        save(user);
        return user;

    }

登录验证

 @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        String code = loginForm.getCode();
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式错误!");
        }

        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
        if (cacheCode ==null || !cacheCode.equals(code)){
            return Result.fail("验证码错误");
        }
        User user = query().eq("phone", phone).one();
        if (user ==null){
            user=createUserWithPhone(phone);
        }
        String token = UUID.randomUUID().toString(true);
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        return Result.ok(token);
    }

发送验证码

 @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result sendCode(String phone, HttpSession session) {
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式错误!");
        }


        String code = RandomUtil.randomNumbers(6);

        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);


        log.debug("你的手机验证码为:{},时长为2分钟,请尽快使用",code);
        return Result.ok();
    }

登出(代码量太小直接在controll实现)

 @PostMapping("/logout")
    public Result logout(){
        UserHolder.removeUser();
        return Result.ok();
    }

拦截器

登录拦截器

package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;


public class LoginInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (UserHolder.getUser()==null){
            response.setStatus(401);
            return false;
        }
        return true;
    }

}

刷新token拦截器,长时间不操作用户token过期

package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;


public class RefreshTokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate=stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)){

            return true;
        }
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map userMap = stringRedisTemplate.opsForHash().entries(key);
        if (userMap.isEmpty()){
            return true;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);
        stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

在注册中心加入这两个拦截器并配置路径

package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/blog/hot",
                        "/voucher/**",
                        "/upload/**",
                        "/shop/**",
                        "/shop-type/**",
                        "/user/code",
                        "/user/login"
                ).order(1);
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

2.2查询商户模块

 进入主页,先从Redis中读出商户分类信息,若Redis中为空则向MySQL中读取,并写入Redis中。主页店铺分类信息为常用信息,应使用Redis避免频繁读取数据库。 该功能的实现分别应对Redis缓存容易出现的三种给出了三个不同的解决方案:

1)缓存穿透(用户对不存在的数据进行大量请求,在Redis中为未中便会请求MySQL数据库,造成数据库崩溃)

解决措施(缓存空对象,布隆过滤器)

这里采用设置默认值的方式应对穿透,当请求像MySQL中也未命中数据时,会返回一个默认值并写入Redis缓存。

2)缓存击穿(热点数据在Redis中的缓存失效,大量同时访问MySQL造成崩溃)

 解决措施(设置逻辑过期,互斥锁)

这里采用给热点数据在Redis中的缓存设置逻辑过期+互斥锁

3)缓存雪崩(Redis中大量缓存同时失效或Redis宕机,大量请求同时访问数据库,造成数据库崩溃)

解决措施(设置多级缓存,采用Redis集群服务,给缓存过期时间加上一个随机值,在业务中添加限流)

package com.hmdp.utils;


import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;


    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){

        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public   R queryWithPassThrougn(
            String keyPrefix, ID id, Class type, Function dbFallback,Long time, TimeUnit unit){
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)){

            return JSONUtil.toBean(json, type);
        }
        if (json!=null){
            return null;
        }
        R r = dbFallback.apply(id);
        if (r==null){
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        this.set(key,r,time,unit);

        return r;
    }

    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);

    public   R queryWithLogicalExpire(
            String keyPrefix, ID id, Class type, Function dbFallback,Long time, TimeUnit unit){
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(shopJson)){

            return null;
        }
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())){
            return r;
        }
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if (isLock){
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    R r1 = dbFallback.apply(id);
                    this.setWithLogicalExpire(key,r1,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unlock(lockKey);
                }
            });
        }


        return r;
    }

    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

}

2.3优惠券秒杀模块

 采用异步下单的方式,先运行Lua脚本,判断是否下过单,若未下过单,则扣减Redis库存,脚本运行成功,有购买资格,则生成一个全局Id作为订单id,生成订单信息,把订单保存到一个阻塞队列,阻塞队列收到订单后,获取分布式锁后再把订单信息和库存信息同步到MySQL,然后释放锁。该模块利用分布式锁实现一人一单功能,利用Lua确保库存不会变负数。

package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 

* 服务实现类 *

* * @author 青云 * @since 2021-12-22 */ @Slf4j @Service public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService { @Resource private ISeckillVoucherService seckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Resource private StringRedisTemplate stringRedisTemplate; @Resource private RedissonClient redissonClient; private static final DefaultRedisScript SECKILL_SCRIPT; static { SECKILL_SCRIPT=new DefaultRedisScript<>(); SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua")); SECKILL_SCRIPT.setResultType(Long.class); } private BlockingQueue orderTasks=new ArrayBlockingQueue<>(1024*1024); private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor(); @PostConstruct private void init(){ SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle()); } private class VoucherOrderHandle implements Runnable{ @Override public void run() { while (true){ try { VoucherOrder voucherOrder = orderTasks.take(); handleVoucherOrder(voucherOrder); } catch (InterruptedException e) { log.error("处理订单异常",e); } } } } private IVoucherOrderService proxy; private void handleVoucherOrder(VoucherOrder voucherOrder) { Long userId = voucherOrder.getUserId(); RLock lock= redissonClient.getLock("lock:order:" + userId); boolean isLock = lock.tryLock(); if (!isLock){ log.error("不允许重复下单"); return ; } //获取代理对象 try { proxy.createVoucherOrder(voucherOrder); } finally { lock.unlock(); } } @Override public Result seckillVoucher(Long voucherId) { Long userId = UserHolder.getUser().getId(); //执行lua脚本 Long result = stringRedisTemplate.execute( SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString() ); //判断结果是否为0 int r = result.intValue(); if (r!=0){ //不为0,代表没有购买资格 return Result.fail(r==1?"库存不足":"不能重复下单"); } //为0,有购买资格,把下单信息保存到阻塞队列 VoucherOrder voucherOrder = new VoucherOrder(); long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setUserId(userId); voucherOrder.setVoucherId(voucherId); orderTasks.add(voucherOrder); proxy = (IVoucherOrderService) AopContext.currentProxy(); //返回订单id return Result.ok(orderId); } // @Override // public Result seckillVoucher(Long voucherId) { // //查询优惠券 // SeckillVoucher voucher = seckillVoucherService.getById(voucherId); // // //判断秒杀是否开始 // if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { // return Result.fail("秒杀尚未开始!"); // } // //判断秒杀是否结束 // if (voucher.getEndTime().isBefore(LocalDateTime.now())) { // return Result.fail("秒杀已结束!"); // } // //判断库存是否充足 // if (voucher.getStock()<1){ // return Result.fail("库存不足!"); // } // // Long userId = UserHolder.getUser().getId(); synchronized (UserHolder.getUser().getId().toString().intern()) { SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate); // // RLock lock= redissonClient.getLock("lock:order:" + userId); // // boolean isLock = lock.tryLock(); // if (!isLock){ // return Result.fail("不允许重复下单"); // } // //获取代理对象 // try { // IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); // return proxy.createVoucherOrder(voucherId); // } finally { // lock.unlock(); // } // // } @Transactional public void createVoucherOrder(VoucherOrder voucherOrder) { Long userId = voucherOrder.getId(); int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count(); if (count>0){ log.error("获取次数已上限"); return ; } //扣减库存 boolean success = seckillVoucherService.update() .setSql("stock=stock-1") .eq("voucher_id", voucherOrder.getVoucherId()) // .eq("stock",voucher.getStock()) .gt("stock",0) .update(); if (!success){ log.error("库存不足"); return ; } //创建订单 // VoucherOrder voucherOrder = new VoucherOrder(); // long orderId = redisIdWorker.nextId("order"); // voucherOrder.setId(orderId); // voucherOrder.setUserId(userId); // voucherOrder.setVoucherId(voucherOrder); save(voucherOrder); //返回订单id // return Result.ok(orderId); } }

lua脚本

-- 1.参数列表
-- 1.1 优惠券id
local voucherId = ARGV[1]
-- 1.2 用户id
local userId = ARGV[2]

-- 2.数据key
-- 2.1 库存key   key 是优惠的业务名称加优惠券id  value 是优惠券的库存数
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2 订单key   key 也是拼接的业务名称加优惠权id  而value是用户id, 这是一个set集合,凡购买该优惠券的用户都会将其id存入集合中
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务
-- 3.1 判断库存是否充足 get stockKey
if (tonumber(redis.call('get', stockKey)) <= 0)  then  --将get的value先转为数字类型才能判断比较
    -- 3.2 库存不足,返回1
    return 1
end
-- 3.3 判断用户是否下单 sismember orderKey userId命令,判断当前key集合中,是否存在该value;返回1存在,0不存在
if (redis.call('sismember', orderKey, userId) == 1) then
    --3.4 存在说明是重复下单,返回2
    return 2
end
-- 3.5 扣库存
redis.call('incrby', stockKey, -1)
-- 3.6 下单(保存用户)
redis.call('sadd', orderKey, userId)
return 0

2.4博客模块

点赞:用户浏览博客时,可以对博客进行点赞,点赞过的用户id,写入,Redis缓存中(zset:博客id,用户ID,时间)博客页并展示点赞次数和点赞列表头像,展示点赞列表时,注意点赞列表按时间排序,点赞时间早的排在前面,SQL语句应拼接order By  。

package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.dto.ScrollResult;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Blog;
import com.hmdp.entity.Follow;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IFollowService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static com.hmdp.utils.RedisConstants.BLOG_LIKED_KEY;
import static com.hmdp.utils.RedisConstants.FEED_KEY;

/**
 * 

* 服务实现类 *

* * @author 青云 * @since 2021-12-22 */ @Service public class BlogServiceImpl extends ServiceImpl implements IBlogService { @Resource private IUserService userService; @Resource private StringRedisTemplate stringRedisTemplate; @Resource private IFollowService followService; @Override public Result queryHotBlog(Integer current) { // 根据用户查询 Page page = query() .orderByDesc("liked") .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE)); // 获取当前页数据 List records = page.getRecords(); // 查询用户 records.forEach(blog -> { this.queryBlogUser(blog); this.isBlogLiked(blog); }); return Result.ok(records); } private void queryBlogUser(Blog blog) { Long userId = blog.getUserId(); User user = userService.getById(userId); blog.setName(user.getNickName()); blog.setIcon(user.getIcon()); } @Override public Result queryBlogById(Long id) { Blog blog = getById(id); if (blog ==null){ return Result.fail("博客不存在"); } queryBlogUser(blog); isBlogLiked(blog); return Result.ok(blog); } private void isBlogLiked(Blog blog) { UserDTO user = UserHolder.getUser(); if (user == null){ return; } Long userId = UserHolder.getUser().getId(); //判断当前用户是否点赞 String key = BLOG_LIKED_KEY + blog.getId(); Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString()); blog.setIsLike(score !=null); } @Override public Result likeBlog(Long id) { Long userId = UserHolder.getUser().getId(); //判断当前用户是否点赞 String key = BLOG_LIKED_KEY + id; Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString()); if (score==null){ boolean isSuccess = update().setSql("liked=liked+1").eq("id", id).update(); if (isSuccess){ stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis()); } }else { boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update(); if (isSuccess){ stringRedisTemplate.opsForZSet().remove(key,userId.toString()); } } return Result.ok(); } @Override public Result queryBlogLikes(Long id) { String key = BLOG_LIKED_KEY + id; //查询top点赞用户 Set top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4); if (top5==null||top5.isEmpty()){ return Result.ok(Collections.emptyList()); } List ids = top5.stream().map(Long::valueOf).collect(Collectors.toList()); String idStr = StrUtil.join(",", ids); List userDTOS = userService.query().in("id",ids) .last("ORDER BY FIELD(id,"+idStr+")").list() .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(userDTOS); } @Override public Result saveBlog(Blog blog) { // 获取登录用户 UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); // 保存探店博文 boolean isSuccess = save(blog); if (!isSuccess){ return Result.fail("新增笔记失败"); } //查询所有粉丝 List follows = followService.query().eq("follow_user_id", user.getId()).list(); for (Follow follow : follows) { Long userId = follow.getUserId(); String key = FEED_KEY + userId; stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis()); } // 返回id return Result.ok(blog.getId()); } @Override public Result queryBlogOfFollow(Long max, Integer offset) { Long userId = UserHolder.getUser().getId(); String key = FEED_KEY + userId; Set> typedTuples = stringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, max, offset, 2); if (typedTuples==null||typedTuples.isEmpty()){ return Result.ok(); } ArrayList ids = new ArrayList<>(typedTuples.size()); long minTime=0; int os=1; for (ZSetOperations.TypedTuple tuple : typedTuples) { String idStr = tuple.getValue(); ids.add(Long.valueOf(idStr)); long time = tuple.getScore().longValue(); if (time==minTime){ os++; }else { minTime = time; os=1; } } String idStr = StrUtil.join(",", ids); List blogs = query() .in("id",ids) .last("ORDER BY FIELD(id,"+idStr+")").list(); for (Blog blog : blogs) { queryBlogUser(blog); isBlogLiked(blog); } ScrollResult r = new ScrollResult(); r.setList(blogs); r.setOffset(os); r.setMinTime(minTime); return Result.ok(r); } }

2.5关注模块

package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Follow;
import com.hmdp.mapper.FollowMapper;
import com.hmdp.service.IFollowService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 

* 服务实现类 *

* * @author 青云 * @since 2021-12-22 */ @Service public class FollowServiceImpl extends ServiceImpl implements IFollowService { @Resource private StringRedisTemplate stringRedisTemplate; @Resource private IUserService userService; @Override public Result follow(Long followUserId, Boolean isFollow) { Long userId = UserHolder.getUser().getId(); String key = "follows" + userId; if (isFollow){ Follow follow=new Follow(); follow.setFollowUserId(followUserId); follow.setUserId(userId); boolean isSuccess = save(follow); if (isSuccess){ stringRedisTemplate.opsForSet().add(key,followUserId.toString()); } }else { boolean isSuccess = remove(new QueryWrapper() .eq("user_id", userId) .eq("follow_user_id", followUserId) ); if (isSuccess){ stringRedisTemplate.opsForSet().remove(key,followUserId.toString()); } } return Result.ok(); } @Override public Result isFollow(Long followUserId) { Long userId = UserHolder.getUser().getId(); Integer count = query().eq("user_id", userId) .eq("follow_user_id", followUserId) .count(); return Result.ok(count>0); } @Override public Result followCommons(Long id) { Long userId = UserHolder.getUser().getId(); String key = "follows" + userId; String key2 = "follows" + id; Set intersect = stringRedisTemplate.opsForSet().intersect(key, key2); if (intersect==null||intersect.isEmpty()){ return Result.ok(Collections.emptyList()); } List ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); List userDTOS = userService.listByIds(ids).stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(userDTOS); } }

2.6订阅模块

用户发布的内容推送给粉丝,实现策略有三种模式:拉取模式,推模式,推拉结合模式

该处实现了推模式,发布博客时,把博客推送给粉丝,会向粉丝的信箱(ZSet:粉丝id,博客id,时间)中存入博客id,用户查看订阅时,即根据信箱滚动分页查询最新的博客

 @Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        Long userId = UserHolder.getUser().getId();
        String key = FEED_KEY + userId;
        Set> typedTuples = stringRedisTemplate.opsForZSet()
                .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
        if (typedTuples==null||typedTuples.isEmpty()){
            return Result.ok();
        }
        ArrayList ids = new ArrayList<>(typedTuples.size());
        long minTime=0;
        int os=1;
        for (ZSetOperations.TypedTuple tuple : typedTuples) {
            String idStr = tuple.getValue();
            ids.add(Long.valueOf(idStr));
            long time = tuple.getScore().longValue();
            if (time==minTime){
                os++;
            }else {
                minTime = time;
                os=1;
            }

        }
        String idStr = StrUtil.join(",", ids);
        List blogs = query()
                .in("id",ids)
                .last("ORDER BY FIELD(id,"+idStr+")").list();
        for (Blog blog : blogs) {
            queryBlogUser(blog);

            isBlogLiked(blog);

        }

        ScrollResult r = new ScrollResult();
        r.setList(blogs);
        r.setOffset(os);
        r.setMinTime(minTime);
        return Result.ok(r);
    }

2.7签到模块

使用时间bitMap,打卡取1,为打卡取0,从第0位开始,n日的打卡数据在n-1位

把当月签到数据和1做与运算,得到最近一天是否打卡,为0则直接返回,为1则把签到数据右移一位和1做与运算,循环,直到与运算结果为0,循环次数为连续签到天数。

 @Override
    public Result sign() {
        Long userId = UserHolder.getUser().getId();
        LocalDateTime now = LocalDateTime.now();
        String keySuffix= now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + userId + keySuffix;
        int dayOfMonth = now.getDayOfMonth();
        stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
        return Result.ok();
    }

    @Override
    public Result signCount() {
        Long userId = UserHolder.getUser().getId();
        LocalDateTime now = LocalDateTime.now();
        String keySuffix= now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + userId + keySuffix;
        int dayOfMonth = now.getDayOfMonth();

        List result = stringRedisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0)
        );
        if (result==null||result.isEmpty()){
            return Result.ok(0);
        }
        Long num = result.get(0);
        if (num==null||num==0){
            return  Result.ok(0);
        }
        int count=0;
        while (true){
            if ((num&1)==0){
                break;
            }else {
                count++;
            }
            num >>>=1;
        }
        return Result.ok(count);
    }

3.心得体会

黑马点评是一个非常适合我们学习的项目,尽管功能大部分是Redis实现,但是可以学到很多新知识,例如秒杀的各种情况如何解决等,希望经过以后的学习能轻松的独立写出类似项目

你可能感兴趣的:(springboot,java,spring,boot,mybatis,redis)