iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > JAVA >菜鸟项目练习:黑马点评项目总结
  • 809
分享到

菜鸟项目练习:黑马点评项目总结

javaredis 2023-09-05 06:09:34 809人浏览 薄情痞子
摘要

目录 1. 项目介绍 2.各个功能模块  2.1  登录模块   2.1.1 实现短信登录   2.1.2 编写拦截器  2.2 查询商户模块    2.2.1 主页面查询商户类型    2.2.3 按距离查询商户  2.3 优惠券秒杀模

目录

1. 项目介绍

2.各个功能模块

 2.1  登录模块

  2.1.1 实现短信登录

  2.1.2 编写拦截器

 2.2 查询商户模块

   2.2.1 主页面查询商户类型

   2.2.3 按距离查询商户

 2.3 优惠券秒杀模块

 2.4 博客模块 

  2.4.1 点赞

 2.5 订阅模块

 2.6 签到模块

  2.6.1 签到功能

  2.6.2 获取当月连续签到天数

3.项目学习收获


 

1. 项目介绍

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

 1.1 项目使用的技术栈

      SpringBoot+Mysql+Lombok+mybatis-Plus+Hutool+Redis

 1.2项目架构

      采用单体架构

后端部署在Tomcat上,前端部分部署在Nginx

2.各个功能模块

 2.1  登录模块

  2.1.1 实现短信登录

          编写一个工具类校验手机号格式,例如

public class RegexUtils {        public static boolean isPhoneInvalid(String phone){        return mismatch(phone, RegexPatterns.PHONE_REGEX);    }        public static boolean isEmailInvalid(String email){        return mismatch(email, RegexPatterns.EMAIL_REGEX);    }        public static boolean isCodeInvalid(String code){        return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);    }    // 校验是否不符合正则格式    private static boolean mismatch(String str, String regex){        if (StrUtil.isBlank(str)) {            return true;        }        return !str.matches(regex);    }}

       手机号码格式无误后生成验证码发送至手机,并将验证码内容写入到Redis。设置过期时间;

       系统根据输入的手机号验证码进行与Redis中写入的验证码比对一致,即可登录成功,从mysql中获取用户信息并生成Token,以Token为key将用户信息写入Redis中(hash),新用户则会注册信息并登录;

  2.1.2 编写拦截器

           登录拦截器,一些功能需要登录后才能使用

public class LoginInterceptor implements HandlerInterceptor {        @Override    public boolean preHandle(httpservletRequest request, HttpServletResponse response, Object handler) throws Exception {//        1.判断是否要做拦截        if(UserHolder.getUser()==null){            response.setStatus(401);            return false;        }//        2.有用户则放行        return true;    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        UserHolder.removeUser();    }}

  刷新Token拦截器,用户长时间没有操作会使Token过期,每次用户点击可以刷新Token过期时间

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 {//        1.获取token        String token = request.getHeader("authorization");//        2.判断token是否为空        if(StrUtil.isBlank(token)){            return true;        }//        4.基于token获取redis中的用户        Map userMap = stringRedisTemplate.opsForHash()                .entries(LOGIN_USER_KEY + token);//        3.判断用户是否为空        if(userMap.isEmpty()){            return true;        }//        5.将查询到的hash数据转为UserDTO对象        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(),false);//        6.存在则保存用户信息到ThreadLocal        UserHolder.saveUser(userDTO);//        7.刷新token有效期        stringRedisTemplate.expire(LOGIN_USER_KEY + token,LOGIN_USER_TTL, TimeUnit.SECONDS);//        8.放行        return true;    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        UserHolder.removeUser();    }}

  在注册中心添加这两个拦截器 

@Configurationpublic class mvcConfig implements WEBMvcConfigurer {    @Resource    private StringRedisTemplate stringRedisTemplate;    @Override    public void addInterceptors(InterceptorReGIStry registry) {        registry.addInterceptor(new LoginInterceptor())//登录拦截器                .excludePathPatterns(                        "/user/code",                        "/user/login",                        "/blog/hot",                        "/shop     public void set(String key, Object value, Long time, TimeUnit unit){        stringRedisTemplate.opsForValue().set(key, JSONUtil.tojsonStr(value),time,TimeUnit.MINUTES);    }             public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){//        1.设置逻辑过期        RedisData redisData = new RedisData();        redisData.setData(value);        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));//        2.写入Redis        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));    }        public  R queryWithPassThrough(            String keyPrefix,ID id,Class type,Function dbFallBack, Long time, TimeUnit unit    ){        String key = keyPrefix + id;//        1.从Redis中查询缓存        String json = stringRedisTemplate.opsForValue().get(key);//        2.判断缓存是否存在        if(StrUtil.isNotBlank(json)){//        3.存在,直接返回            return JSONUtil.toBean(json,type);        }//        4.不存在,判断是否是空字符串        if(json!=null){//        5.是空字符串            return null;        }//        6.不是空字符串,则向数据库中查找        R r = dbFallBack.apply(id);//        7.数据库中未找到,设置值为空字符串并插入缓存        if (r==null) {            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);            return null;        }//        8.找到数据源       this.set(key,r,time,unit);        return r;    }    public  R queryWithLogicalExpire(String keyPrefix,ID id,Class type,Function dbFallBack,Long time,TimeUnit unit){        String key = keyPrefix + id;//        1.从redis查询缓存        String json = stringRedisTemplate.opsForValue().get(key);        if(StrUtil.isBlank(json)){//        2.如果缓存未命中            return null;        }//        3.如果命中,把json字符反序列化为对象        RedisData redisData = JSONUtil.toBean(json, RedisData.class);        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);        LocalDateTime expireTime = redisData.getExpireTime();        System.out.println(expireTime);//        5.判断是否过期        if(expireTime.isAfter(LocalDateTime.now())){//            5.1.未过期,直接返回对象            return r;        }//        5.2已过期,缓存重建//        6.缓存重建//        6.1获取互斥锁        String lockKey = LOCK_SHOP_KEY + id;        boolean isLock = tryLock(lockKey);//        6.2判断是否获取锁成功        if(isLock){//            6.3成功            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 static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);       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.2.3 按距离查询商户

             第一步需要将商铺坐标按分类写入Redis(Geo),关键代码如下

@Test    void loadShopData(){//        1.查询店铺信息        List shopList = shopService.list();//        2.把店铺分组,按照typeId分组,typeId一致发到一个集合        Map> map = shopList.stream().collect(Collectors.groupingBy(Shop::getTypeId));//        3.分批完成写入Redis        for (Map.Entry> entry : map.entrySet()) {//            获取类型id            Long typeTd = entry.geTKEy();            String key = SHOP_GEO_KEY + typeTd;//            获取通类型的店铺集合            List shops = entry.getValue();            List> locations = new ArrayList<>(shops.size());//            写入Redis GEOADD key 经度 纬度 member            for (Shop shop : shops) {                locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),                        new Point(shop.getX(), shop.getY())                ));            }            stringRedisTemplate.opsForGeo().add(key,locations);        }    }

             请求参数中需要包含坐标,分页页码信息,类别ID,先向Redis中读取该类别的直到改页最后一个商铺商铺信息,并以距离排序,关键代码如下

GeoResults> results = stringRedisTemplate.opsForGeo()                .search(key,                        GeoReference.fromCoordinate(x, y),                        new Distance(5000),                        RedisGeoCommands.    GeoSearchCommandArgs.    newGeoSearchArgs().    includeDistance().limit(end)                );

          再将数据进行解析,并把该页第一个商铺前面的商铺信息都跳过得到想要商铺的id和对应distance的键值对集合

List>> list=results.getContent();        if(list.size()<=from){            return Result.ok();        }//        5.截取from-end的部分        List ids= new ArrayList<>(list.size());        HashMap distanceMap = new HashMap<>(list.size());//        截取掉from之前的部分,不重复查询        list.stream().skip(from).forEach(result->{//            获取店铺id            String shopId = result.getContent().getName();            ids.add(Long.valueOf(shopId));//            获取距离            Distance distance = result.getDistance();            distanceMap.put(shopId,distance);        });

          最后根据id查出商铺信息并将设置distance属性,返回商铺信息集合。

          补充:如果不按距离排序则直接按页码和页面尺寸查询店铺信息

Page page=query.eq("type_id",typeId)                        .page(new Page<>(current,SystemConstants.DEFEAUT_PAGE_SIZE));

 2.3 优惠券秒杀模块

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

@Slf4j@Servicepublic class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService {    @Resource    private ISeckillVoucherService iSeckillVoucherService;    @Resource    private RedissonClient redissonClient;    @Resource    private RedisIdWorker redisIdWorker;    @Resource    private StringRedisTemplate stringRedisTemplate;    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);//    创建单线程化线程池,用来运行实现Runnable的类    private static final ExecutorService SECKILL_ORDER_EXCUTOR= Executors.newSingleThreadExecutor();//    等依赖加载完再全部执行    @PostConstruct    private void init(){        SECKILL_ORDER_EXCUTOR.submit(new VoucherOrderHandler());    }    private class VoucherOrderHandler implements Runnable{        @Override        public void run() {            while (true) {                try {    //            获取队列中的订单信息                    VoucherOrder voucherOrder = orderTasks.take();    //            创建订单                    handleVoucherOrder(voucherOrder);                } catch (InterruptedException e) {                    log.error("订单处理异常",e);                }            }        }    }    private void handleVoucherOrder(VoucherOrder voucherOrder){//        1.获取用户        Long userId = voucherOrder.getUserId();//        2.创建锁对象        RLock lock = redissonClient.getLock("lock:order:" + userId);//        3.判断是否获取锁成功        boolean isLock = lock.tryLock();        if(!isLock){//            获取锁失败            log.error("不允许重复下单");            return;        }        try {            proxy.createVoucherOrder(voucherOrder);        } finally {            lock.unlock();        }    }        private IVoucherOrderService proxy;    @Override    public Result seckillVoucher(Long voucherId) {//        1.执行lua脚本//        获取userID        Long userId = UserHolder.getUser().getId();        Long result = stringRedisTemplate.execute(                SECKILL_SCRIPT,                Collections.emptyList(),                voucherId.toString(),                userId.toString()        );//        2.判断结果为0        int i = result.intValue();        if(i!=0)//        2.1不为0,代表没有购买资格        {            return Result.fail(i==1 ? "库存不足" : "不能重复下单");        }//        2.2为0,有购买资格,把下单信息保存到阻塞队列        long orderId = redisIdWorker.nextId("order");        VoucherOrder voucherOrder = new VoucherOrder();        voucherOrder.setId(orderId);        voucherOrder.setUserId(userId);        voucherOrder.setVoucherId(voucherId);//        TODO保存阻塞队列        orderTasks.add(voucherOrder);//        获取代理对象        proxy=(IVoucherOrderService) AopContext.currentProxy();//        3.返回订单id        return Result.ok(orderId);    }        @Transactional    public void createVoucherOrder(VoucherOrder voucherOrder) {        Long userId = voucherOrder.getUserId();//        查询订单        Integer count = query().eq("voucher_id", voucherOrder.getVoucherId()).eq("user_id", userId).count();        if(count>0){            log.error("用户已经购买过一次");            return ;        }//        5.扣减库存        boolean result = iSeckillVoucherService.update()                .setSql("stock=stock-1")                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0).update();//where stock >0        if(!result){//            扣减失败            log.error("库存不足!");            return;        }        save(voucherOrder);//        返回订单id        return;    }}

 2.4 博客模块 

  2.4.1 点赞

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

             点赞功能:

 public Result addLike(Long id) {//        1.获取当前用户        Long userId = UserHolder.getUser().getId();        Blog blog = query().eq("id", id).one();//        2.判断当前用户是否已经点赞        String key = BLOG_LIKED_KEY + id;        Double isLike = stringRedisTemplate.opsForZSet().score(key, userId.toString());//        3.如果未点赞,可以点赞        if(isLike==null)        {//            4.数据库该帖点赞+1            boolean isSuccess = update().setSql("liked=liked+1").eq("id", id).update();//            5.保存用户id到该贴子的Redis的Zset集合,并更新blog的isLike属性            if(BooleanUtil.isTrue(isSuccess)){                stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());                blog.setIsLike(true);            }            return Result.ok();        }//        6.如果已经点赞//        7.数据库该贴点赞-1;        boolean isSuccess = update().setSql("liked=liked-1").eq("id", id).update();//        8.把set集合中的用户id移除        if(BooleanUtil.isTrue(isSuccess)){            stringRedisTemplate.opsForZSet().remove(key,userId.toString());            blog.setIsLike(false);        }        return Result.ok();    }

              点赞列表:

 public Result queryLikesById(Long id) {//        1.获取key        String key = BLOG_LIKED_KEY + id;//        2.查询点赞时间前五的userId        Set userIds = stringRedisTemplate.opsForZSet().range(key, 0, 4);        if(userIds==null||userIds.isEmpty()){            return Result.ok();        }//        3.根据userId查询User        List list = userIds.stream().map(Long::valueOf).collect(Collectors.toList());        String idStr = StrUtil.join(",", list);//        4.返回User集        List UserDTOS = userService.query()                .in("id",list)                .last("ORDER BY FIELD(id,"+idStr+")")                .list()                .stream()                .map(user -> BeanUtil.copyProperties(user,UserDTO.class))                .collect(Collectors.toList());        return Result.ok(UserDTOS);    }

   2.4.2 关注作者

            与点赞功能相似,将关注用户写入Redis中(String:用户id,被关注与id)

 2.5 订阅模块

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

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

 public Result queryBlogByFollow(Long max, Integer offset) {//        1.获取当前用户id        Long userId = UserHolder.getUser().getId();        String key =  FEED_KEY+userId;//        2.查询信箱        Set> typedTuples = stringRedisTemplate.opsForZSet()                .reverseRangeByScoreWithScores(key,0,max,offset,3);        System.out.println(typedTuples);        if(typedTuples==null||typedTuples.isEmpty()){            return Result.ok();        }        List ids = new ArrayList<>(typedTuples.size());//        3.解析数据        long minTime=0;        Integer os=1;        for (ZSetOperations.TypedTuple tuple : typedTuples) {//            获取blogId            ids.add(Long.valueOf(tuple.getValue()));//            获取分数            long score = tuple.getScore().longValue();            if(minTime==score){                os++;            }else {                os=1;                minTime=score;            }        }//        4.根据id查询blog        String idStr = StrUtil.join(",", ids);        List blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();        for (Blog blog : blogs) {//            获取点赞信息            isLiked(blog);//            获取用户信息            User user = userService.getById(blog.getUserId());            blog.setName(user.getNickName());            blog.setIcon(user.getIcon());        }//        5.封装并返回        ScrollResult scrollResult = new ScrollResult();        scrollResult.setList(blogs);        scrollResult.setOffset(os);        scrollResult.setMinTime(minTime);        return Result.ok(scrollResult);    }

        

 2.6 签到模块

  2.6.1 签到功能

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

//        2.获取日期        LocalDateTime now = LocalDateTime.now();//        3.拼接key        String keySuffix = now.fORMat(DateTimeFormatter.ofPattern(":yyyyMM"));        String key = USER_SIGN_KEY + userId + keySuffix;//        4.获取今天是本月的第几天        int dayOfMonth = now.getDayOfMonth();//        5.写入Redis setBit key offset 1        stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);

  2.6.2 获取当月连续签到天数

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

//        2.获取用户在本月当前签到数据        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 sign = result.get(0);        if(sign==0||sign==null){            return Result.ok(0);        }//        3.取出和1做与运算        int count=0;        while (true){            if ((sign&1)==0) {    //        4.判断是否为0    //        4.1为0则返回                break;            }else {//        4.2为1则count++,并将sign右移                count++;            }            sign>>>=1;        }

3.项目学习收获

   项目实战可能碰到的场景,及问题,和解决方案

来源地址:https://blog.csdn.net/qq_47960640/article/details/127845888

--结束END--

本文标题: 菜鸟项目练习:黑马点评项目总结

本文链接: http://www.lsjlt.com/news/394811.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作