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

本文是关于黑马点评项目的总结,详细介绍了登录模块的短信验证和拦截器实现,查询商户模块中如何利用Redis优化,包括防止缓存穿透、击穿和雪崩的策略,以及优惠券秒杀、博客点赞、关注和签到等功能的实现。项目旨在应用和学习Redis知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

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 {
    /**
     * 是否是无效手机格式
     * @param phone 要校验的手机号
     * @return true:符合,false:不符合
     */
    public static boolean isPhoneInvalid(String phone){
        return mismatch(phone, RegexPatterns.PHONE_REGEX);
    }
    /**
     * 是否是无效邮箱格式
     * @param email 要校验的邮箱
     * @return true:符合,false:不符合
     */
    public static boolean isEmailInvalid(String email){
        return mismatch(email, RegexPatterns.EMAIL_REGEX);
    }

    /**
     * 是否是无效验证码格式
     * @param code 要校验的验证码
     * @return true:符合,false:不符合
     */
    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 {
    /***
     * @description: 登录拦截方法
     * @param: [javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object]
     * @return: boolean
     * @date: 2022/10/25 17:27
     */
    @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;
    }
    /***
     * @description: 登录拦截方法
     * @param: [javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object]
     * @return: boolean
     * @date: 2022/10/25 17:27
     */
    @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<Object, Object> 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();
    }
}

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

@Configuration
public 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/**",
                        "/shop-type/**",
                        "/voucher/**"
                ).order(1);
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);//刷新Token有效期    }
}

 2.2 查询商户模块

   2.2.1 主页面查询商户类型

            进入主页,先从Redis中读出商户分类信息,若Redis中为空则向MySQL中读取,并写入Redis中。主页店铺分类信息为常用信息,应使用Redis避免频繁读取数据库。

   2.2.2 商户详情页

             该功能的实现分别应对Redis缓存容易出现的三种给出了三个不同的解决方案:

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

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

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

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

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

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

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

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

                   这里采取给缓存过期时间加随机数的方式改进

             解决方法封装成一个工具类了

@Component
@Slf4j
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    /*** 
     * @description: 插入缓存 
     * @param: [java.lang.String, java.lang.Object, java.lang.Long] 
     * @return: void 
     * @date: 2022/10/29 21:06
     */ 
    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,TimeUnit.MINUTES);
    }
    
    /*** 
     * @description: 设置逻辑过期 
     * @param: [java.lang.String, java.lang.Object, java.lang.Long, java.util.concurrent.TimeUnit] 
     * @return: void 
     * @date: 2022/10/29 21:08
     */ 
    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));
    }
    /***
     * @description: 缓存穿透策略之设置默认值
     * @param: [java.lang.String, ID, java.lang.Class<R>]
     * @return: R
     * @date: 2022/10/29 23:18
     */
    public <R,ID> R queryWithPassThrough(
            String keyPrefix,ID id,Class<R> type,Function<ID,R> 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,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> 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;
    }

    /**
     * @description: 线程池
     * @param:
     * @return:
     * @date: 2022/10/30 14:27
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);

   /**
    * @description: 获取锁
    * @param:
    * @return:
    * @date: 2022/10/30 14:23
    */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
    /**
     * @description: 释放锁
     * @param: [java.lang.String]
     * @return: void
     * @date: 2022/10/30 14:24
     */
    private void unLock(String key){
        stringRedisTemplate.delete("key");
    }

}

   2.2.3 按距离查询商户

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

@Test
    void loadShopData(){
//        1.查询店铺信息
        List<Shop> shopList = shopService.list();
//        2.把店铺分组,按照typeId分组,typeId一致发到一个集合
        Map<Long, List<Shop>> map = shopList.stream().collect(Collectors.groupingBy(Shop::getTypeId));
//        3.分批完成写入Redis
        for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
//            获取类型id
            Long typeTd = entry.getKey();
            String key = SHOP_GEO_KEY + typeTd;
//            获取通类型的店铺集合
            List<Shop> shops = entry.getValue();
            List<RedisGeoCommands.GeoLocation<String>> 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<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.
                                GeoSearchCommandArgs.
                                newGeoSearchArgs().
                                includeDistance().limit(end)
                );

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

List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list=results.getContent();
        if(list.size()<=from){
            return Result.ok();
        }
//        5.截取from-end的部分
        List<Long> ids= new ArrayList<>(list.size());
        HashMap<String, Distance> 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<Shop> page=query.eq("type_id",typeId)
                        .page(new Page<>(current,SystemConstants.DEFEAUT_PAGE_SIZE));

 2.3 优惠券秒杀模块

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

@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService iSeckillVoucherService;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT=new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }
//    创建一个队列
    private BlockingQueue<VoucherOrder> 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();
        }

    }

    /**
     * @description: 购买优惠券
     * @param: [java.lang.Long]
     * @return: com.hmdp.dto.Result
     * @date: 2022/11/2 21:14
     */
    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);
    }

    /**
     * @description: 创建订单
     * @param: [java.lang.Long]
     * @date: 2022/11/3 20:56
     * @param voucherOrder
     */
    @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<String> userIds = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        if(userIds==null||userIds.isEmpty()){
            return Result.ok();
        }
//        3.根据userId查询User
        List<Long> list = userIds.stream().map(Long::valueOf).collect(Collectors.toList());
        String idStr = StrUtil.join(",", list);
//        4.返回User集
        List<UserDTO> 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<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
                .reverseRangeByScoreWithScores(key,0,max,offset,3);

        System.out.println(typedTuples);
        if(typedTuples==null||typedTuples.isEmpty()){
            return Result.ok();
        }
        List<Long> ids = new ArrayList<>(typedTuples.size());
//        3.解析数据
        long minTime=0;
        Integer os=1;
        for (ZSetOperations.TypedTuple<String> 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<Blog> 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<Long> 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.项目学习收获

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

嗯,用户想要找从Python入门到全栈开发的免费学习网站。我先看看参考内容里的信息有没有合适的资源。 第一个参考内容提到腾讯课堂的星越教育课程,但看起来是付费的,可能不符合用户的免费需求。第二个和第五个参考内容提到了优快云博客里的资源,里面有很多免费视频,比如B站的千锋教育和黑马程序员的教程链接,这些应该都是免费的。第三个和第六个参考内容推荐了官方文档、w3school、菜鸟教程,这些网站都是经典的学习资源,完全免费。还有ProgramCreek和LearnPython这样的英文网站,可能适合有一定英语基础的学习者。 另外,第五个参考内容里提到百度网盘的资料,但链接可能需要提取密码,用户可能需要注册或关注才能获取,不过也是免费资源。Udacity的免费课程和Google的Python课程也是不错的选项,尤其是Google的课程质量应该很高。 需要确认的是这些资源是否都还在线,比如B站的视频链接是否有效。用户可能需要自行访问确认,但大部分应该是可用的。总结的时候应该分门别类,比如视频教程、文档网站、实战项目等,让用户方便选择。要确保涵盖从基础到全栈的不同阶段,包括Web开发、爬虫、数据库等内容。需要注意用户要的是全栈,所以后端和前端的内容都要包含。比如Django框架、数据库持久化部分,这些在参考内容里有提到,可以整合进去。 还要注意时效性,虽然参考内容发布时间在2024到2025年之间,但大部分资源比如官方文档和B站教程通常长期有效。可能建议用户优先选择中文资源,如果英语好可以看英文网站。同时提醒用户实践的重要性,结合项目来学习。最后整理成一个结构清晰的回答,列出每个资源类型和对应的链接,方便用户使用。 以下是针对Python从入门到全栈开发的免费学习资源推荐,结合权威性与时效性,覆盖基础语法、Web开发、数据库、爬虫等全栈方向:   一、系统化视频教程(免费) 1. B站千锋教育Python教程      ○ 特点:700集零基础课程,涵盖基础语法、Web开发、爬虫、数据分析、AI基础。      ○ 链接:B站700集教程 2. B站黑马程序员Python教程      ○ 特点:600集系统课程,基于Linux环境,包含基础语法、面向对象、游戏开发。      ○ 链接:B站600集教程 3. Udacity Python入门课(英文)           ○ 特点:免费入门课程,结合搜索引擎与网络爬虫项目,适合动手实践。      ○ 链接:Intro to Computer Science   二、官方文档与中文社区 1. Python官方中文文档      ○ 特点:最权威的语法参考,覆盖基础语法、标准库及进阶指南。      ○ 链接:Python中文文档 2. 菜鸟教程 & W3School           ○ 特点:新手友好,提供Python语法、实例及操作步骤详解。      ○ 链接:          ● 菜鸟教程Python          ● W3School Python   三、实战项目与案例 1. ProgramCreek Python代码实例      ○ 特点(英文):海量模块实例(如 datetime 、 requests ),直接搜索函数名称即可查看用法。      ○ 链接:Python Code Examples 2. Github开源项目           ○ 推荐:搜索关键词“Python全栈项目”,如Django博客系统、电商平台。      ○ 学习方式:阅读代码 → 复现功能 → 参与Issue讨论。   四、专项技能提升 1. Web开发(Django/Flask)      ○ 资源推荐:          ● B站Django教程:全栈开发教程          ● Flask官方文档:Flask中文指南 2. 数据库与数据持久化      ○ 免费课:B站搜索“MySQL+Python”或“SQLAlchemy教程”,如SQLAlchemy ORM教程。 3. 爬虫开发(Scrapy/Requests)           ○ B站爬虫框架Scrapy实战:Scrapy框架教程   五、综合学习平台 1. LearnPython(英文)      ○ 特点:交互式学习,涵盖初级到高级语法,附带在线代码编辑器。      ○ 链接:LearnPython 2. Google Python Class           ○ 特点:Google官方课程,包含文本与视频教程,适合快速上手。      ○ 链接:Google Python课程   六、国内免费资源包 1. 优快云博主共享资料      ○ 内容:Python零基础入门、爬虫开发、Web全栈资料包(需百度网盘提取)。      ○ 获取方式:访问此链接 按指引领取。   学习建议 1. 路线:先掌握基础语法(变量、循环、函数)→ 学习数据库与Web框架 → 实战项目 → 深入爬虫/数据分析。 2. 实践:每学完一个知识点,用代码实现小功能(如爬取天气、搭建个人博客)。 3. 社区:加入Python技术群或Stack Overflow,遇到问题及时提问。 希望这些资源能助你从入门到全栈!
最新发布
06-17
<think>好的,我现在需要帮一个想快速学习Python的新手制定学习计划。用户的情况是不会英文,但有一定的网络知识和逻辑思维。首先,我得考虑如何克服语言障碍,因为Python的很多资源和错误信息都是英文的。然后,要利用他的优势,比如网络知识和逻辑思维,来设计学习路径。 首先,基础部分应该从安装Python开始,但用户有网络知识,可能已经熟悉安装软件,这部分可以简单带过。接下来是基础语法,变量、数据类型、流程控制这些。不过用户不会英文,所以需要推荐中文资源,比如菜鸟教程、廖雪峰的Python教程,或者视频课程,比如慕课网或B站的中文内容。同时要提醒他,虽然代码中会有英文关键词,但可以通过记忆和练习来掌握,不需要深入理解英文。 然后是实践部分,用户需要多做练习,从简单的小项目开始,比如计算器、猜数字游戏,这样能巩固基础。接下来是进阶内容,比如函数、文件操作、模块,这些是更复杂项目的基础。网络知识方面,用户可能对爬虫感兴趣,可以教他使用requests和BeautifulSoup库,但要注意法律问题。数据处理和自动化办公也是实用方向,可以用pandas和openpyxl,这些库有中文文档吗?可能需要找中文教程或者翻译工具辅助。 项目实战很重要,要让他实际做项目,比如爬取天气数据、自动化处理Excel报表,或者用Flask/Django做简单网站。遇到问题时,要教他如何利用搜索引擎,比如用百度加上中文关键词,或者到知乎、优快云找答案。同时,推荐他加入中文社区,如知乎、豆瓣小组或QQ群,获得帮助。 持续学习方面,推荐关注中文公众号、知乎专栏,阅读开源项目的中文注释,参与中文项目。最后,注意事项要提醒他不要急于求成,多写代码,善用工具如翻译插件,记录问题。可能还需要推荐一些具体的工具,比如VSCode的中文界面,如何设置,以及翻译工具如谷歌翻译或有道词典的屏幕取词功能。 检查是否有遗漏的部分,比如开发环境设置是否需要详细指导?用户有网络知识,可能自己会解决,但可能需要推荐中文教程。另外,错误信息通常是英文的,如何处理?可能需要教他如何识别常见错误信息的关键词,或者使用翻译工具快速翻译错误提示。 总结下来,结构应该是分阶段:基础语法->实践->进阶->项目->问题解决->社区和资源。每个阶段推荐中文资源,强调实践和项目,利用现有网络知识,比如爬虫和自动化。同时,提供克服英文障碍的方法,比如翻译工具和记忆关键词。以下</think>是为您量身定制的Python快速学习计划(针对中文母语者,无需英文基础): --- ### **📅 阶段一:搭建基础(1-2周)** 1️⃣ **环境搭建** - 使用[【清华镜像源】](https://mirrors.tuna.tsinghua.edu.cn/)快速安装Python - 推荐IDE:Thonny(中文界面)或VSCode(安装中文语言包) 2️⃣ **核心语法学习** - **重点中文资源**: - 文字教程:[菜鸟教程Python3](https://www.runoob.com/python3/python3-tutorial.html) - 视频课程:B站【黑马程序员Python教程】(零基础入门) - **必学内容**: - 变量与基础数据类型(无需记英文术语,用中文理解概念) - 条件判断(if...elif...else) - 循环结构(while/for配合range) - 中文命名变量技巧(Python3支持中文变量名) --- ### **💻 阶段二:实战演练(2-3周)** 3️⃣ **小项目实战** - 推荐项目: - 汇率计算器(练习数值运算) - 文本版「猜数字」游戏(综合运用判断与循环) - 自动整理桌面文件(利用os模块) 4️⃣ **中文代码示例**: ```python # 计算圆面积(使用中文变量名) 半径 = float(input("请输入半径:")) 圆周率 = 3.1415 面积 = 圆周率 * 半径 ** 2 print(f"面积为:{面积:.2f}") ``` --- ### **🚀 阶段三:专项突破(3-4周)** 5️⃣ **选择发展方向** - **网络方向**(利用现有知识): - 学习requests库抓取网页 - 用BeautifulSoup解析中文网页 - 案例:自动抓取天气预报数据 - **自动化方向**: - 用openpyxl操作Excel - 用python-docx处理Word文档 - 案例:批量重命名文件/自动填写表格 6️⃣ **中文友好工具包**: - 网络请求:`requests`(中文文档齐全) - 数据处理:`pandas`(搜索「pandas中文教程」) - 图形界面:`PySimpleGUI`(简单易用) --- ### **🔧 问题解决策略** - **错误提示处理**: 1. 使用截图翻译工具(微信/QQ自带OCR翻译) 2. 记忆常见错误关键词: - `IndexError` → 列表索引越界 - `KeyError` → 字典键不存在 - `IndentationError` → 缩进错误 - **高效搜索技巧**: - 百度搜索:「Python 如何【中文描述问题】」 - 优先查看优快云、知乎、简书的中文解决方案 --- ### **📚 持续学习路径** - **中文进阶资源**: - 书籍:《Python编程:从入门到实践》(图灵出品,中文版) - 开源项目:GitHub搜索「python-zh」(中文注释项目) - **实践建议**: - 每天坚持30分钟编码(可用LeetCode中文版简单题练手) - 参与中文编程社区(如「Python中文社区」微信公众号) --- ### **⚠️ 重要提示** 1. 遇到问题先尝试拆解问题(利用已有逻辑思维) 2. 所有专业术语可通过「中文术语+Python」搜索理解 3. 推荐安装「划词翻译」浏览器插件辅助阅读英文文档片段 通过这个路径,您可以在2-3个月内达到能独立开发实用脚本的水平。关键是多动手实践,遇到问题善用中文搜索和翻译工具,您的网络知识和逻辑能力会成为学习加速器!
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值