当前位置:   article > 正文

黑马点评全功能实现总结

黑马点评

1. 项目介绍

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

 1.1 项目使用的技术栈

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

 1.2项目架构

      采用单体架构

1.3项目地址

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

2.功能模块

2.1用户登录模块

手机号创建用户

  1. private User createUserWithPhone(String phone) {
  2. User user = new User();
  3. user.setPhone(phone);
  4. user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
  5. save(user);
  6. return user;
  7. }

登录验证

  1. @Override
  2. public Result login(LoginFormDTO loginForm, HttpSession session) {
  3. String phone = loginForm.getPhone();
  4. String code = loginForm.getCode();
  5. if (RegexUtils.isPhoneInvalid(phone)){
  6. return Result.fail("手机号格式错误!");
  7. }
  8. String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
  9. if (cacheCode ==null || !cacheCode.equals(code)){
  10. return Result.fail("验证码错误");
  11. }
  12. User user = query().eq("phone", phone).one();
  13. if (user ==null){
  14. user=createUserWithPhone(phone);
  15. }
  16. String token = UUID.randomUUID().toString(true);
  17. UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
  18. Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
  19. CopyOptions.create().setIgnoreNullValue(true)
  20. .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
  21. String tokenKey = LOGIN_USER_KEY + token;
  22. stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
  23. stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
  24. return Result.ok(token);
  25. }

发送验证码

  1. @Resource
  2. private StringRedisTemplate stringRedisTemplate;
  3. @Override
  4. public Result sendCode(String phone, HttpSession session) {
  5. if (RegexUtils.isPhoneInvalid(phone)){
  6. return Result.fail("手机号格式错误!");
  7. }
  8. String code = RandomUtil.randomNumbers(6);
  9. stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
  10. log.debug("你的手机验证码为:{},时长为2分钟,请尽快使用",code);
  11. return Result.ok();
  12. }

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

  1. @PostMapping("/logout")
  2. public Result logout(){
  3. UserHolder.removeUser();
  4. return Result.ok();
  5. }

拦截器

登录拦截器

  1. package com.hmdp.utils;
  2. import cn.hutool.core.bean.BeanUtil;
  3. import cn.hutool.core.util.StrUtil;
  4. import com.hmdp.dto.UserDTO;
  5. import org.springframework.data.redis.core.StringRedisTemplate;
  6. import org.springframework.web.servlet.HandlerInterceptor;
  7. import javax.servlet.http.HttpServletRequest;
  8. import javax.servlet.http.HttpServletResponse;
  9. import javax.servlet.http.HttpSession;
  10. import java.util.Map;
  11. import java.util.concurrent.TimeUnit;
  12. public class LoginInterceptor implements HandlerInterceptor {
  13. @Override
  14. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  15. if (UserHolder.getUser()==null){
  16. response.setStatus(401);
  17. return false;
  18. }
  19. return true;
  20. }
  21. }

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

  1. package com.hmdp.utils;
  2. import cn.hutool.core.bean.BeanUtil;
  3. import cn.hutool.core.util.StrUtil;
  4. import com.hmdp.dto.UserDTO;
  5. import org.springframework.data.redis.core.StringRedisTemplate;
  6. import org.springframework.web.servlet.HandlerInterceptor;
  7. import javax.servlet.http.HttpServletRequest;
  8. import javax.servlet.http.HttpServletResponse;
  9. import javax.servlet.http.HttpSession;
  10. import java.util.Map;
  11. import java.util.concurrent.TimeUnit;
  12. public class RefreshTokenInterceptor implements HandlerInterceptor {
  13. private StringRedisTemplate stringRedisTemplate;
  14. public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
  15. this.stringRedisTemplate=stringRedisTemplate;
  16. }
  17. @Override
  18. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  19. String token = request.getHeader("authorization");
  20. if (StrUtil.isBlank(token)){
  21. return true;
  22. }
  23. String key = RedisConstants.LOGIN_USER_KEY + token;
  24. Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
  25. if (userMap.isEmpty()){
  26. return true;
  27. }
  28. UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
  29. UserHolder.saveUser(userDTO);
  30. stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
  31. return true;
  32. }
  33. @Override
  34. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  35. UserHolder.removeUser();
  36. }
  37. }

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

  1. package com.hmdp.config;
  2. import com.hmdp.utils.LoginInterceptor;
  3. import com.hmdp.utils.RefreshTokenInterceptor;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.data.redis.core.StringRedisTemplate;
  6. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  7. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  8. import javax.annotation.Resource;
  9. @Configuration
  10. public class MvcConfig implements WebMvcConfigurer {
  11. @Resource
  12. private StringRedisTemplate stringRedisTemplate;
  13. @Override
  14. public void addInterceptors(InterceptorRegistry registry) {
  15. registry.addInterceptor(new LoginInterceptor())
  16. .excludePathPatterns(
  17. "/blog/hot",
  18. "/voucher/**",
  19. "/upload/**",
  20. "/shop/**",
  21. "/shop-type/**",
  22. "/user/code",
  23. "/user/login"
  24. ).order(1);
  25. registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
  26. }
  27. }

2.2查询商户模块

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

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

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

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

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

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

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

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

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

  1. package com.hmdp.utils;
  2. import cn.hutool.core.util.BooleanUtil;
  3. import cn.hutool.core.util.StrUtil;
  4. import cn.hutool.json.JSONObject;
  5. import cn.hutool.json.JSONUtil;
  6. import com.hmdp.entity.Shop;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.data.redis.core.StringRedisTemplate;
  9. import org.springframework.stereotype.Component;
  10. import java.time.LocalDateTime;
  11. import java.util.concurrent.ExecutorService;
  12. import java.util.concurrent.Executors;
  13. import java.util.concurrent.TimeUnit;
  14. import java.util.function.Function;
  15. import static com.hmdp.utils.RedisConstants.*;
  16. @Slf4j
  17. @Component
  18. public class CacheClient {
  19. private final StringRedisTemplate stringRedisTemplate;
  20. public CacheClient(StringRedisTemplate stringRedisTemplate) {
  21. this.stringRedisTemplate = stringRedisTemplate;
  22. }
  23. public void set(String key, Object value, Long time, TimeUnit unit){
  24. stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
  25. }
  26. public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
  27. RedisData redisData = new RedisData();
  28. redisData.setData(value);
  29. redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
  30. stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
  31. }
  32. public <R,ID> R queryWithPassThrougn(
  33. String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
  34. String key = keyPrefix + id;
  35. String json = stringRedisTemplate.opsForValue().get(key);
  36. if (StrUtil.isNotBlank(json)){
  37. return JSONUtil.toBean(json, type);
  38. }
  39. if (json!=null){
  40. return null;
  41. }
  42. R r = dbFallback.apply(id);
  43. if (r==null){
  44. stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
  45. return null;
  46. }
  47. this.set(key,r,time,unit);
  48. return r;
  49. }
  50. private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
  51. public <R,ID> R queryWithLogicalExpire(
  52. String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
  53. String key = CACHE_SHOP_KEY + id;
  54. String shopJson = stringRedisTemplate.opsForValue().get(key);
  55. if (StrUtil.isBlank(shopJson)){
  56. return null;
  57. }
  58. RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
  59. JSONObject data = (JSONObject) redisData.getData();
  60. R r = JSONUtil.toBean(data, type);
  61. LocalDateTime expireTime = redisData.getExpireTime();
  62. if (expireTime.isAfter(LocalDateTime.now())){
  63. return r;
  64. }
  65. String lockKey = LOCK_SHOP_KEY + id;
  66. boolean isLock = tryLock(lockKey);
  67. if (isLock){
  68. CACHE_REBUILD_EXECUTOR.submit(()->{
  69. try {
  70. R r1 = dbFallback.apply(id);
  71. this.setWithLogicalExpire(key,r1,time,unit);
  72. } catch (Exception e) {
  73. throw new RuntimeException(e);
  74. } finally {
  75. unlock(lockKey);
  76. }
  77. });
  78. }
  79. return r;
  80. }
  81. private boolean tryLock(String key){
  82. Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
  83. return BooleanUtil.isTrue(flag);
  84. }
  85. private void unlock(String key){
  86. stringRedisTemplate.delete(key);
  87. }
  88. }

2.3优惠券秒杀模块

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

  1. package com.hmdp.service.impl;
  2. import com.hmdp.dto.Result;
  3. import com.hmdp.entity.VoucherOrder;
  4. import com.hmdp.mapper.VoucherOrderMapper;
  5. import com.hmdp.service.ISeckillVoucherService;
  6. import com.hmdp.service.IVoucherOrderService;
  7. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  8. import com.hmdp.utils.RedisIdWorker;
  9. import com.hmdp.utils.UserHolder;
  10. import lombok.extern.slf4j.Slf4j;
  11. import org.redisson.api.RLock;
  12. import org.redisson.api.RedissonClient;
  13. import org.springframework.aop.framework.AopContext;
  14. import org.springframework.core.io.ClassPathResource;
  15. import org.springframework.data.redis.core.StringRedisTemplate;
  16. import org.springframework.data.redis.core.script.DefaultRedisScript;
  17. import org.springframework.stereotype.Service;
  18. import org.springframework.transaction.annotation.Transactional;
  19. import javax.annotation.PostConstruct;
  20. import javax.annotation.Resource;
  21. import java.util.Collections;
  22. import java.util.concurrent.ArrayBlockingQueue;
  23. import java.util.concurrent.BlockingQueue;
  24. import java.util.concurrent.ExecutorService;
  25. import java.util.concurrent.Executors;
  26. /**
  27. * <p>
  28. * 服务实现类
  29. * </p>
  30. *
  31. * @author 青云
  32. * @since 2021-12-22
  33. */
  34. @Slf4j
  35. @Service
  36. public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
  37. @Resource
  38. private ISeckillVoucherService seckillVoucherService;
  39. @Resource
  40. private RedisIdWorker redisIdWorker;
  41. @Resource
  42. private StringRedisTemplate stringRedisTemplate;
  43. @Resource
  44. private RedissonClient redissonClient;
  45. private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
  46. static {
  47. SECKILL_SCRIPT=new DefaultRedisScript<>();
  48. SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
  49. SECKILL_SCRIPT.setResultType(Long.class);
  50. }
  51. private BlockingQueue<VoucherOrder> orderTasks=new ArrayBlockingQueue<>(1024*1024);
  52. private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
  53. @PostConstruct
  54. private void init(){
  55. SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle());
  56. }
  57. private class VoucherOrderHandle implements Runnable{
  58. @Override
  59. public void run() {
  60. while (true){
  61. try {
  62. VoucherOrder voucherOrder = orderTasks.take();
  63. handleVoucherOrder(voucherOrder);
  64. } catch (InterruptedException e) {
  65. log.error("处理订单异常",e);
  66. }
  67. }
  68. }
  69. }
  70. private IVoucherOrderService proxy;
  71. private void handleVoucherOrder(VoucherOrder voucherOrder) {
  72. Long userId = voucherOrder.getUserId();
  73. RLock lock= redissonClient.getLock("lock:order:" + userId);
  74. boolean isLock = lock.tryLock();
  75. if (!isLock){
  76. log.error("不允许重复下单");
  77. return ;
  78. }
  79. //获取代理对象
  80. try {
  81. proxy.createVoucherOrder(voucherOrder);
  82. } finally {
  83. lock.unlock();
  84. }
  85. }
  86. @Override
  87. public Result seckillVoucher(Long voucherId) {
  88. Long userId = UserHolder.getUser().getId();
  89. //执行lua脚本
  90. Long result = stringRedisTemplate.execute(
  91. SECKILL_SCRIPT,
  92. Collections.emptyList(),
  93. voucherId.toString(), userId.toString()
  94. );
  95. //判断结果是否为0
  96. int r = result.intValue();
  97. if (r!=0){
  98. //不为0,代表没有购买资格
  99. return Result.fail(r==1?"库存不足":"不能重复下单");
  100. }
  101. //为0,有购买资格,把下单信息保存到阻塞队列
  102. VoucherOrder voucherOrder = new VoucherOrder();
  103. long orderId = redisIdWorker.nextId("order");
  104. voucherOrder.setId(orderId);
  105. voucherOrder.setUserId(userId);
  106. voucherOrder.setVoucherId(voucherId);
  107. orderTasks.add(voucherOrder);
  108. proxy = (IVoucherOrderService) AopContext.currentProxy();
  109. //返回订单id
  110. return Result.ok(orderId);
  111. }
  112. // @Override
  113. // public Result seckillVoucher(Long voucherId) {
  114. // //查询优惠券
  115. // SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
  116. //
  117. // //判断秒杀是否开始
  118. // if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
  119. // return Result.fail("秒杀尚未开始!");
  120. // }
  121. // //判断秒杀是否结束
  122. // if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
  123. // return Result.fail("秒杀已结束!");
  124. // }
  125. // //判断库存是否充足
  126. // if (voucher.getStock()<1){
  127. // return Result.fail("库存不足!");
  128. // }
  129. //
  130. // Long userId = UserHolder.getUser().getId();
  131. synchronized (UserHolder.getUser().getId().toString().intern()) {
  132. SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
  133. //
  134. // RLock lock= redissonClient.getLock("lock:order:" + userId);
  135. //
  136. // boolean isLock = lock.tryLock();
  137. // if (!isLock){
  138. // return Result.fail("不允许重复下单");
  139. // }
  140. // //获取代理对象
  141. // try {
  142. // IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
  143. // return proxy.createVoucherOrder(voucherId);
  144. // } finally {
  145. // lock.unlock();
  146. // }
  147. //
  148. // }
  149. @Transactional
  150. public void createVoucherOrder(VoucherOrder voucherOrder) {
  151. Long userId = voucherOrder.getId();
  152. int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
  153. if (count>0){
  154. log.error("获取次数已上限");
  155. return ;
  156. }
  157. //扣减库存
  158. boolean success = seckillVoucherService.update()
  159. .setSql("stock=stock-1")
  160. .eq("voucher_id", voucherOrder.getVoucherId())
  161. // .eq("stock",voucher.getStock())
  162. .gt("stock",0)
  163. .update();
  164. if (!success){
  165. log.error("库存不足");
  166. return ;
  167. }
  168. //创建订单
  169. // VoucherOrder voucherOrder = new VoucherOrder();
  170. // long orderId = redisIdWorker.nextId("order");
  171. // voucherOrder.setId(orderId);
  172. // voucherOrder.setUserId(userId);
  173. // voucherOrder.setVoucherId(voucherOrder);
  174. save(voucherOrder);
  175. //返回订单id
  176. // return Result.ok(orderId);
  177. }
  178. }

lua脚本

  1. -- 1.参数列表
  2. -- 1.1 优惠券id
  3. local voucherId = ARGV[1]
  4. -- 1.2 用户id
  5. local userId = ARGV[2]
  6. -- 2.数据key
  7. -- 2.1 库存key key 是优惠的业务名称加优惠券id value 是优惠券的库存数
  8. local stockKey = 'seckill:stock:' .. voucherId
  9. -- 2.2 订单key key 也是拼接的业务名称加优惠权id 而value是用户id, 这是一个set集合,凡购买该优惠券的用户都会将其id存入集合中
  10. local orderKey = 'seckill:order:' .. voucherId
  11. -- 3.脚本业务
  12. -- 3.1 判断库存是否充足 get stockKey
  13. if (tonumber(redis.call('get', stockKey)) <= 0) then --将get的value先转为数字类型才能判断比较
  14. -- 3.2 库存不足,返回1
  15. return 1
  16. end
  17. -- 3.3 判断用户是否下单 sismember orderKey userId命令,判断当前key集合中,是否存在该value;返回1存在,0不存在
  18. if (redis.call('sismember', orderKey, userId) == 1) then
  19. --3.4 存在说明是重复下单,返回2
  20. return 2
  21. end
  22. -- 3.5 扣库存
  23. redis.call('incrby', stockKey, -1)
  24. -- 3.6 下单(保存用户)
  25. redis.call('sadd', orderKey, userId)
  26. return 0

2.4博客模块

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

  1. package com.hmdp.service.impl;
  2. import cn.hutool.core.bean.BeanUtil;
  3. import cn.hutool.core.util.BooleanUtil;
  4. import cn.hutool.core.util.StrUtil;
  5. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  6. import com.hmdp.dto.Result;
  7. import com.hmdp.dto.ScrollResult;
  8. import com.hmdp.dto.UserDTO;
  9. import com.hmdp.entity.Blog;
  10. import com.hmdp.entity.Follow;
  11. import com.hmdp.entity.User;
  12. import com.hmdp.mapper.BlogMapper;
  13. import com.hmdp.service.IBlogService;
  14. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  15. import com.hmdp.service.IFollowService;
  16. import com.hmdp.service.IUserService;
  17. import com.hmdp.utils.SystemConstants;
  18. import com.hmdp.utils.UserHolder;
  19. import org.springframework.data.redis.core.StringRedisTemplate;
  20. import org.springframework.data.redis.core.ZSetOperations;
  21. import org.springframework.stereotype.Service;
  22. import javax.annotation.Resource;
  23. import java.util.ArrayList;
  24. import java.util.Collections;
  25. import java.util.List;
  26. import java.util.Set;
  27. import java.util.stream.Collectors;
  28. import static com.hmdp.utils.RedisConstants.BLOG_LIKED_KEY;
  29. import static com.hmdp.utils.RedisConstants.FEED_KEY;
  30. /**
  31. * <p>
  32. * 服务实现类
  33. * </p>
  34. *
  35. * @author 青云
  36. * @since 2021-12-22
  37. */
  38. @Service
  39. public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
  40. @Resource
  41. private IUserService userService;
  42. @Resource
  43. private StringRedisTemplate stringRedisTemplate;
  44. @Resource
  45. private IFollowService followService;
  46. @Override
  47. public Result queryHotBlog(Integer current) {
  48. // 根据用户查询
  49. Page<Blog> page = query()
  50. .orderByDesc("liked")
  51. .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
  52. // 获取当前页数据
  53. List<Blog> records = page.getRecords();
  54. // 查询用户
  55. records.forEach(blog -> {
  56. this.queryBlogUser(blog);
  57. this.isBlogLiked(blog);
  58. });
  59. return Result.ok(records);
  60. }
  61. private void queryBlogUser(Blog blog) {
  62. Long userId = blog.getUserId();
  63. User user = userService.getById(userId);
  64. blog.setName(user.getNickName());
  65. blog.setIcon(user.getIcon());
  66. }
  67. @Override
  68. public Result queryBlogById(Long id) {
  69. Blog blog = getById(id);
  70. if (blog ==null){
  71. return Result.fail("博客不存在");
  72. }
  73. queryBlogUser(blog);
  74. isBlogLiked(blog);
  75. return Result.ok(blog);
  76. }
  77. private void isBlogLiked(Blog blog) {
  78. UserDTO user = UserHolder.getUser();
  79. if (user == null){
  80. return;
  81. }
  82. Long userId = UserHolder.getUser().getId();
  83. //判断当前用户是否点赞
  84. String key = BLOG_LIKED_KEY + blog.getId();
  85. Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
  86. blog.setIsLike(score !=null);
  87. }
  88. @Override
  89. public Result likeBlog(Long id) {
  90. Long userId = UserHolder.getUser().getId();
  91. //判断当前用户是否点赞
  92. String key = BLOG_LIKED_KEY + id;
  93. Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
  94. if (score==null){
  95. boolean isSuccess = update().setSql("liked=liked+1").eq("id", id).update();
  96. if (isSuccess){
  97. stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());
  98. }
  99. }else {
  100. boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
  101. if (isSuccess){
  102. stringRedisTemplate.opsForZSet().remove(key,userId.toString());
  103. }
  104. }
  105. return Result.ok();
  106. }
  107. @Override
  108. public Result queryBlogLikes(Long id) {
  109. String key = BLOG_LIKED_KEY + id;
  110. //查询top点赞用户
  111. Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
  112. if (top5==null||top5.isEmpty()){
  113. return Result.ok(Collections.emptyList());
  114. }
  115. List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
  116. String idStr = StrUtil.join(",", ids);
  117. List<UserDTO> userDTOS = userService.query().in("id",ids)
  118. .last("ORDER BY FIELD(id,"+idStr+")").list()
  119. .stream()
  120. .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
  121. .collect(Collectors.toList());
  122. return Result.ok(userDTOS);
  123. }
  124. @Override
  125. public Result saveBlog(Blog blog) {
  126. // 获取登录用户
  127. UserDTO user = UserHolder.getUser();
  128. blog.setUserId(user.getId());
  129. // 保存探店博文
  130. boolean isSuccess = save(blog);
  131. if (!isSuccess){
  132. return Result.fail("新增笔记失败");
  133. }
  134. //查询所有粉丝
  135. List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
  136. for (Follow follow : follows) {
  137. Long userId = follow.getUserId();
  138. String key = FEED_KEY + userId;
  139. stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
  140. }
  141. // 返回id
  142. return Result.ok(blog.getId());
  143. }
  144. @Override
  145. public Result queryBlogOfFollow(Long max, Integer offset) {
  146. Long userId = UserHolder.getUser().getId();
  147. String key = FEED_KEY + userId;
  148. Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
  149. .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
  150. if (typedTuples==null||typedTuples.isEmpty()){
  151. return Result.ok();
  152. }
  153. ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
  154. long minTime=0;
  155. int os=1;
  156. for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
  157. String idStr = tuple.getValue();
  158. ids.add(Long.valueOf(idStr));
  159. long time = tuple.getScore().longValue();
  160. if (time==minTime){
  161. os++;
  162. }else {
  163. minTime = time;
  164. os=1;
  165. }
  166. }
  167. String idStr = StrUtil.join(",", ids);
  168. List<Blog> blogs = query()
  169. .in("id",ids)
  170. .last("ORDER BY FIELD(id,"+idStr+")").list();
  171. for (Blog blog : blogs) {
  172. queryBlogUser(blog);
  173. isBlogLiked(blog);
  174. }
  175. ScrollResult r = new ScrollResult();
  176. r.setList(blogs);
  177. r.setOffset(os);
  178. r.setMinTime(minTime);
  179. return Result.ok(r);
  180. }
  181. }

2.5关注模块

  1. package com.hmdp.service.impl;
  2. import cn.hutool.core.bean.BeanUtil;
  3. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  4. import com.hmdp.dto.Result;
  5. import com.hmdp.dto.UserDTO;
  6. import com.hmdp.entity.Follow;
  7. import com.hmdp.mapper.FollowMapper;
  8. import com.hmdp.service.IFollowService;
  9. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  10. import com.hmdp.service.IUserService;
  11. import com.hmdp.utils.UserHolder;
  12. import org.springframework.data.redis.core.StringRedisTemplate;
  13. import org.springframework.stereotype.Service;
  14. import javax.annotation.Resource;
  15. import java.util.Collections;
  16. import java.util.List;
  17. import java.util.Set;
  18. import java.util.stream.Collectors;
  19. /**
  20. * <p>
  21. * 服务实现类
  22. * </p>
  23. *
  24. * @author 青云
  25. * @since 2021-12-22
  26. */
  27. @Service
  28. public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
  29. @Resource
  30. private StringRedisTemplate stringRedisTemplate;
  31. @Resource
  32. private IUserService userService;
  33. @Override
  34. public Result follow(Long followUserId, Boolean isFollow) {
  35. Long userId = UserHolder.getUser().getId();
  36. String key = "follows" + userId;
  37. if (isFollow){
  38. Follow follow=new Follow();
  39. follow.setFollowUserId(followUserId);
  40. follow.setUserId(userId);
  41. boolean isSuccess = save(follow);
  42. if (isSuccess){
  43. stringRedisTemplate.opsForSet().add(key,followUserId.toString());
  44. }
  45. }else {
  46. boolean isSuccess = remove(new QueryWrapper<Follow>()
  47. .eq("user_id", userId)
  48. .eq("follow_user_id", followUserId)
  49. );
  50. if (isSuccess){
  51. stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
  52. }
  53. }
  54. return Result.ok();
  55. }
  56. @Override
  57. public Result isFollow(Long followUserId) {
  58. Long userId = UserHolder.getUser().getId();
  59. Integer count = query().eq("user_id", userId)
  60. .eq("follow_user_id", followUserId)
  61. .count();
  62. return Result.ok(count>0);
  63. }
  64. @Override
  65. public Result followCommons(Long id) {
  66. Long userId = UserHolder.getUser().getId();
  67. String key = "follows" + userId;
  68. String key2 = "follows" + id;
  69. Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
  70. if (intersect==null||intersect.isEmpty()){
  71. return Result.ok(Collections.emptyList());
  72. }
  73. List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
  74. List<UserDTO> userDTOS = userService.listByIds(ids).stream()
  75. .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
  76. .collect(Collectors.toList());
  77. return Result.ok(userDTOS);
  78. }
  79. }

2.6订阅模块

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

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

  1. @Override
  2. public Result queryBlogOfFollow(Long max, Integer offset) {
  3. Long userId = UserHolder.getUser().getId();
  4. String key = FEED_KEY + userId;
  5. Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
  6. .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
  7. if (typedTuples==null||typedTuples.isEmpty()){
  8. return Result.ok();
  9. }
  10. ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
  11. long minTime=0;
  12. int os=1;
  13. for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
  14. String idStr = tuple.getValue();
  15. ids.add(Long.valueOf(idStr));
  16. long time = tuple.getScore().longValue();
  17. if (time==minTime){
  18. os++;
  19. }else {
  20. minTime = time;
  21. os=1;
  22. }
  23. }
  24. String idStr = StrUtil.join(",", ids);
  25. List<Blog> blogs = query()
  26. .in("id",ids)
  27. .last("ORDER BY FIELD(id,"+idStr+")").list();
  28. for (Blog blog : blogs) {
  29. queryBlogUser(blog);
  30. isBlogLiked(blog);
  31. }
  32. ScrollResult r = new ScrollResult();
  33. r.setList(blogs);
  34. r.setOffset(os);
  35. r.setMinTime(minTime);
  36. return Result.ok(r);
  37. }

2.7签到模块

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

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

  1. @Override
  2. public Result sign() {
  3. Long userId = UserHolder.getUser().getId();
  4. LocalDateTime now = LocalDateTime.now();
  5. String keySuffix= now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
  6. String key = USER_SIGN_KEY + userId + keySuffix;
  7. int dayOfMonth = now.getDayOfMonth();
  8. stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
  9. return Result.ok();
  10. }
  11. @Override
  12. public Result signCount() {
  13. Long userId = UserHolder.getUser().getId();
  14. LocalDateTime now = LocalDateTime.now();
  15. String keySuffix= now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
  16. String key = USER_SIGN_KEY + userId + keySuffix;
  17. int dayOfMonth = now.getDayOfMonth();
  18. List<Long> result = stringRedisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
  19. .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
  20. .valueAt(0)
  21. );
  22. if (result==null||result.isEmpty()){
  23. return Result.ok(0);
  24. }
  25. Long num = result.get(0);
  26. if (num==null||num==0){
  27. return Result.ok(0);
  28. }
  29. int count=0;
  30. while (true){
  31. if ((num&1)==0){
  32. break;
  33. }else {
  34. count++;
  35. }
  36. num >>>=1;
  37. }
  38. return Result.ok(count);
  39. }

3.心得体会

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/693087
推荐阅读
相关标签
  

闽ICP备14008679号