day6_redis学习

news2024/12/26 21:13:50

文章目录

  • 关注和取关
  • 查看其他用户界面及共同关注
  • 关注推送

关注和取关

因为关注用户的时候可能涉及到共同关注的对象,所以需要利用到交集,而在Redis中可以使用交集的,是Set以及ZSet数据结构,但是显然这里并不需要排序,所以Set已经满足了我们的需求。所以对于每一个用户来说,都需要维护一个Set,用来保存这个用户关注的人的id。
如果进行的关注,那么这时候需要将两者添加到tb_follow数据库表中,同时将被关注的用户id添加到当前的用户对应的Set中,否则如果进行的是取关操作,那么就需要删除数据库表中对应的记录,同时需要将被关注的用户id从当前用户对应的Set中删除。
那么我们将如何判断是进行的关注操作还是取关操作呢?如下所示:
在这里插入图片描述
所以我们需要给这个关注接口传递参数,从而判断执行的是关注操作还是取关操作,对应的代码:

/**
 * 当前用户关注/取关followerUser
 * @param followerUserId
 * @param isFollowed
 * @return
 */
@Override
public Result follow(Long followerUserId, Boolean isFollowed) {
    //1、获取当前用户的id
    Long userId = UserHolder.getUser().getId();
    //2、判断isFollowed的值,从而判断是关注还是取关
    String user_key = RedisConstants.FOLLOW_USER_KEY + userId;
    if(isFollowed){
        //2.1 关注followerUser,那么将这一条数据插入到数据库tb_follower中
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(followerUserId);
        boolean isSuccess = save(follow);
        if(isSuccess){
            //将followerUserId添加到当前的用户的set中,来统计当前用户关注的人
            stringRedisTemplate.opsForSet().add(user_key, followerUserId.toString());
        }
    }else{
        //2.2 取关followerUser,那么将这一条数据删除
        boolean isSuccess = remove(new LambdaQueryWrapper<Follow>().eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followerUserId));
        if(isSuccess){
            //将followerUserId从当前用户关注的set中删除
            stringRedisTemplate.opsForSet().remove(user_key, followerUserId.toString());
        }
    }
    return Result.ok();
}

判断当前blog的作者是否被当前的用户关注,那么只需要判断被关注的用户id是否存在于当前用户对应的Set中即可,所以对应的接口代码为:

//判断当前的用户是否已经关注了followUserId这个用户
@Override
public Result followOrNot(Long followUserId) {
    UserDTO user = UserHolder.getUser();
    if(user == null){
        //1、用户没有登录,那么默认是没有关注
        return Result.ok(false);
    }
    Long userId = user.getId();
    //获取当前用户关注的set的key
    String user_key = RedisConstants.FOLLOW_USER_KEY + userId;
    Boolean isMember = stringRedisTemplate.opsForSet().isMember(user_key, followUserId.toString());
    return Result.ok(BooleanUtil.isTrue(isMember));
}

查看其他用户界面及共同关注

如果我们点击查看某一篇blog,突然对这个博主写的内容感兴趣,那么就会点击这个作者,此时我们就会来到其他用户的界面,那么对应的步骤如下所示:
在这里插入图片描述
所以根据这个步骤,需要在UserController中添加API接口,用于获取其他用户的信息,所以对应的代码为:
根据userId来获取用户信息:

//根据userId来获取这个id对应的用户的基本信息
@GetMapping("/{userId}")
public Result queryById(@PathVariable("userId")Long userId){
    return userService.queryUserDTOById(userId);
}

根据userId来获取userInfo信息,例如粉丝数量,用户的简介,点赞数等,对应的代码为:

@GetMapping("/info/{id}")
public Result info(@PathVariable("id") Long userId){
    // 查询详情
    UserInfo info = userInfoService.getById(userId);
    if (info == null) {
        // 没有详情,应该是第一次查看详情
        return Result.ok();
    }
    info.setCreateTime(null);
    info.setUpdateTime(null);
    // 返回
    return Result.ok(info);
}

根据userId,来获取这个用户写的所有blog,所以blogController需要添加新的API接口,对应的代码为:

/**
 * 当进入其他用户的主页的时候,需要获取其他用户的博客
 * @param userId
 * @param current
 * @return
 */
@GetMapping("/of/user")
public Result queryByUser(@RequestParam("id") Long userId, @RequestParam(value = "current", defaultValue = "1") Long current){
    return blogService.queryByUser(userId, current);
}

而BlogService接口提供的API接口为queryByUser,对应的实现类重写的方法代码为:

/**
 * 获取userId中的第current页的笔记
 * @param userId
 * @param current
 * @return
 */
@Override
public Result queryByUser(Long userId, Long current) {
    Page<Blog> page = query().eq("user_id", userId) //获取当前用户的博客
            //根据点赞数降序排序
            .orderByDesc("liked")
            //获取第current页的记录,并且每一页有MAX_PAGE_SIZE条
            .page(new Page<Blog>(current, SystemConstants.MAX_PAGE_SIZE));
    //将page页的博客通过getRecords方法获取出来
    List<Blog> records = page.getRecords();
    records.forEach(blog -> {
        this.isLikeByCurrentUser(blog);
        this.queryBlogUser(blog);
    });
    return Result.ok(records);
}

而如果来到我的界面的时候,同样获取对应的信息,只是这时候相对于前者来到其他用户界面的时候,少了queryLoginUser方法,而是直接调用queryUser,对应的代码如下所示:
在这里插入图片描述
所以我们需要在BlogController中添加API接口,用于获取当前登录用户的blog,对应的代码为:

//获取当前登录用户的第current页的blog
@GetMapping("/of/me")
public Result queryMyBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
    return blogService.queryMyBlog(current);
}

对应的BlogService对应的API接口以及子类重写的方法:

//BlogService接口中的方法
Result queryMyBlog(Integer current);

//BlogServiceImpl重写的方法
@Override
public Result queryMyBlog(Integer current) {
    // 获取登录用户
    UserDTO userDTO = UserHolder.getUser();
    // 根据用户查询
    Page<Blog> page = query()
            .eq("user_id", userDTO.getId()).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
    // 获取当前页数据
    List<Blog> records = page.getRecords();
    records.forEach(blog -> {
        this.isLikeByCurrentUser(blog);
        this.queryBlogUser(blog);
    });
    return Result.ok(records);
}

当我们来到其他的用户界面的时候,我们需要获取它和当前用户共同关注的人的时候,那么这时候我们将分别获取两者的Set(保存的是关注的用户Id),然后获取两者的交集就是共同关注的用户Id,然后将遍历这些用户Id,获取这些用户的基本信息在返回给前端,对应的代码为:
对应FollowController提供的API接口代码为:

/**
 * 获取当前的用户和otherUserId的共同关注的人
 * @param otherUserId
 * @return
 */
@GetMapping("/common/{otherUserId}")
public Result queryCommonFollow(@PathVariable("otherUserId")Long otherUserId){
    return followService.queryCommonFollow(otherUserId);
}

对应的FollowService接口以及子类重写的代码为:

/**
 * 获取当前用户和otherUserId的共同关注对象
 * @param otherUserId
 * @return
 */
@Override
public Result queryCommonFollow(Long otherUserId) {
    Long userId = UserHolder.getUser().getId();
    String current_user_key = RedisConstants.FOLLOW_USER_KEY + userId;
    String other_user_key = RedisConstants.FOLLOW_USER_KEY + otherUserId;
    //获取当前用户和otherUserId共同关注的用户id
    List<Long> userIds = stringRedisTemplate.opsForSet().intersect(current_user_key, other_user_key)
                                               .stream()
                                               .map(Long::valueOf)
                                               .collect(Collectors.toList());
    if(userIds == null || userIds.isEmpty()){
        return Result.ok(Collections.emptyList());
    }
    //获取共同关注的UserDTO
    List<UserDTO> userDTO = userService.listByIds(userIds)
            .stream()
            .map(user -> {
                return BeanUtil.copyProperties(user, UserDTO.class, "false");
            })
            .collect(Collectors.toList());
    return Result.ok(userDTO);
}

之所以返回共同的用户ID之后,还需要查询UserDTO,因为我们总不能仅仅返回共同关注的UserId吧,至少得需要知道共同关注得用户Nickname吧,因此返回的是UserDTO。

关注推送

如果我们关注了某一个用户的时候,那么当被关注的人如果发布了新的blog,那么就需要将新的blog推送给它的粉丝,此时就需要利用到了Feed流来实现推送。
而Feed流产品有2中模式:

  • Timeline: 不会对内容进行筛选,而是简单按照发布时间升序排序,通常用于好友或者关注
  • 智能排序: 利用智能算法屏蔽违规,不感兴趣的内容

而当前的项目中则采用Timeline模式来实现Feed流推送,实现关注推送,其中Timeline同样存在3中方案,如下所示:

  • 拉模式: 也称为读扩散,当被关注的用户一旦发布了blog,那么会将blog保存到发布者这边,只有粉丝点击关注的人,才会拉取关注者的blog。显然,每次都需要粉丝来去关注者的blog,耗时
  • 推模式:也成为写扩散,当被关注的用户一旦发布了blog,就会主动推送到粉丝的收件箱,那么这时候当粉丝需要查看的时候,就可以直接从自己的收件箱中获取,从而节省了时间。但是这时候就会出现一个问题,如果被关注者有很多的粉丝,那么这时候就会导致需要将一篇blog复制成多篇,才可以推送给粉丝,从而导致内存占用的问题
  • 推拉结合:也叫读写混合,兼具了拉和推模式的优点。

在这次的项目中采用的是推模式来实现Feed流推送,此时就需要保证每个用户都有一个收件箱,用来保存关注者推送的blogId,当粉丝查看的时候,那么需要保证这些blog是根据发布时间排序的,那么这时候就需要保证有序。
此时Redis中可以利用List以及ZSet来保证有序,因为List可以实现队列这种数据结构,而ZSet则是拥有score属性,只要将时间戳的值作为score,那么就可以实现按照时间戳排序了。

但是如果采用的是Feed流推送的时候,采用的分页并不是传统给的分页模式,按照下标来获取的,如下所示:
在这里插入图片描述
所以这时候可以知道,如果是按照传统的方式来进行分页(也即根据起始下标来进行分页),那么就会导致查询到的记录有重复的情况(即不同的页有重复的blog)

所以这时候并不会采用List这种数据结构作为用户的收件箱(List只能根据起始下标来实现分页),但是ZSet是否可以呢?答案是肯定的,因为ZSet除了可以根据下标来获取元素之后,还可以获取score范围的元素,即通过命令zrangebyscore来获得,因此这里就可以利用score(时间戳)来实现滚动分页,避免每页存在重复blog的情况

但是这里需要根据时间戳降序排序的,因此应该利用的命令是zrevrangebyscore.此外由于采用的是zrevrangebyscore,那么我们就需要考虑到score的范围应该设置多少才可以获取到对应页的blog。
如果是第1次来查询的时候,那么score的最大值应该是当前的时间戳,最小值为0,下一次再来查询的时候,那么score的最大值就是上一次查询记录的最小值,最小值依旧是0, 所以我们除了获取元素值之外,还需要的值对应的score属性的值,因此命令再次改为使用zrevrangebyscorewithscore.

上面仅仅实现了获取某一个score范围的元素,并没有实现分页,这时候我们同样可以通过limit offset count来实现分页,其中的offset表示起始下标, count表示获取条目。此时我们就需要考虑offset的值应该如何设置。

如果是第1次查询,那么offset应该是0,表示从第1条记录开始查询,否则,如果不是第1次查询,那么offset应该是上一次查询记录中的最小score出现次数
在这里插入图片描述
所以利用ZSet来实现滚动分页查询的命令就是zrevrangebyscorewithscore key max min limit offset count,其中max的初始值为当前的时间戳(用于第1次查询),下次再查询的时候,值为上一次查询中的最小时间戳, min固定为0; offset表示起始下标,那么第1次查询时offset值为0,下一次再来查询的时候offset的值为上一次查询的最小时间戳出现的次数, count表示每页的blog数量。因此我们只需要关注max以及offset即可。

所以利用Feed流来实现关注推送,以及查看关注者的blog代码为:

发布blog的时候,需要将blogId推送到粉丝的收件箱中:

//发布blog,并将blog推送给粉丝
@Override
public Result saveBlog(Blog blog) {
    // 1、获取登录用户
    UserDTO user = UserHolder.getUser();
    Long userId = user.getId();
    blog.setUserId(userId);
    // 2、保存探店博文
    Boolean isSuccess = save(blog);
    if(BooleanUtil.isFalse(isSuccess)){
        return Result.fail("发布blog失败");
    }
    // 3、将blog推送给粉丝
    // 3. 1 查询数据库,从而获取当前用户的所有粉丝
    List<Follow> follows = followService.query().eq("follow_user_id", userId).list();
    // 3. 2 获取所有的follow中的userId,就是粉丝的id,然后分别推送到粉丝id中的收件箱中
    //并且是根据发布的时间降序排序的
    for(Follow follow : follows){
        stringRedisTemplate.opsForZSet().add(RedisConstants.FEED_KEY + follow.getUserId(), blog.getId().toString(), System.currentTimeMillis());
    }
    // 4、返回id
    return Result.ok(blog.getId());
}

当用户点击关注的时候,那么就会看到关注的人发布的blog,如下所示:
在这里插入图片描述
这时候需要实现滚动分页,在获取到分页的内容之后,需要将对应blog数据,最小时间戳以及最小时间戳出现的次数封装到ScrollResult这个实体中,然后再将其返回,之所以需要封装到这个实体,因为下次查询的时候需要利用到上次的记录,此时如果我们将数据封装到这个实体中,就可以从这个实体中获取上次的记录了,对应的代码为:

/**
 * 实现滚动分页查询,从而获取关注者发布的blog
 * @param offset
 * @param lastId
 * @return
 */
@Override
public Result queryOfFollow(Long offset, Long lastId) {
    //1、获取当前的登陆用户
    Long userId = UserHolder.getUser().getId();
    //2、根据Feed流,进行滚动查询,其中是根据上一次查询到的记录后面的offset开始
    //lastId就是上一次查询记录的时间戳
    String key = RedisConstants.FEED_KEY + userId;
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,  0,lastId, offset, 5L);
    if(typedTuples == null || typedTuples.isEmpty()){
        //2.1 数据为空
        return Result.ok();
    }
    //3、解析数据,获取关注着的blogId,以及发布时间,然后统计记录中的最小时间戳以及出现的次数
    //为下一次请求的时候,发送offset以及lastId
    List<Long> blogIds = new ArrayList<>();
    long minTime = 0L, time;
    int count = 0;
    for(ZSetOperations.TypedTuple<String> typedTuple : typedTuples){
        //获取blogId
        long blogId = Long.parseLong(typedTuple.getValue());
        blogIds.add(blogId);
        //获取时间戳,由于zset已经实现了降序,所以可以直接遍历,那么最后一个必然就是这一次查询
        //中的最小时间戳
        time = typedTuple.getScore().longValue();
        if(time != minTime){
            minTime = time;
            count = 1;
        }else{
            ++count;
        }
    }
    //获取查询到的blog,但是需要根据Order BY FIELD (id, xxx, xxx)方式排序查询
    //因为如果直接是listByIds,那么数据库根据in子句进行查询,此时查询到的数据不一定
    //是和上面分页记录中的blogIds一致
    String idStr = StrUtil.join(",", blogIds);
    System.out.println("idStr = " + idStr);
    List<Blog> blogs = query().in("id", blogIds)
            .last("ORDER BY Field(id, " + idStr + " )").list();
    //4、对每一个blog,都需要获取blog的作者,避免点击blog的时候,发现作者发生报错
    //同时需要判断blog是否被当前的用户点赞了
    blogs.forEach(blog -> {
            isLikeByCurrentUser(blog);
            queryBlogUser(blog);
    });
    //5、封装blogs,count以及minTime
    ScrollResult scrollResult = new ScrollResult();
    scrollResult.setList(blogs);
    scrollResult.setMinTime(minTime);
    scrollResult.setOffset(count);
    return Result.ok(scrollResult);
}

ScrollResult实体代码为:

@Data
public class ScrollResult {
    private List<?> list;
    private Long minTime;
    private Integer offset;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/65590.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java学习之多态二

目录 一、运用多态解决宠物喂食问题 原理分析 运行测试 运行结果 分析 增加宠物和食物种类 Pig类 Rice类 测试 运行结果 一、运用多态解决宠物喂食问题 改变Master类的feed方法的参数列表 package com.hspedu.poly_;public class Master {private String name;public…

基于机器学习之模型树短期负荷预测(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f4dd;目前更新&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f;电力系统相关知识&#xff0c;期刊论文&…

Python 中的 Raincloud 图绘制

Python 中的 Raincloud 图 提示&#xff1a;一种强大的数据可视化方法&#xff0c;由小提琴图、散点图和箱线图的组合组成 提示&#xff1a;目录 Python 中的 Raincloud 图绘制Python 中的 Raincloud 图前言一、什么是 Raincloud 图&#xff1f;二、使用步骤1.加载数据集2.读入…

S7协议抓包分析(附pcap数据包)

一、S7协议概述 1、S7协议简介 S7comm&#xff08;S7 通信&#xff09;是西门子专有协议&#xff0c;可在西门子 S7-300/400 系列的可编程逻辑控制器 (PLC) 之间运行。它用于 PLC 编程、PLC 之间的数据交换、从 SCADA&#xff08;监控和数据采集&#xff09;系统访问 PLC 数据…

刷爆力扣之最长连续递增序列

刷爆力扣之最长连续递增序列 HELLO&#xff0c;各位看官大大好&#xff0c;我是阿呆 &#x1f648;&#x1f648;&#x1f648; 今天阿呆继续记录下力扣刷题过程&#xff0c;收录在专栏算法中 &#x1f61c;&#x1f61c;&#x1f61c; 该专栏按照不同类别标签进行刷题&#…

代码随想录算法训练营第五十五天|392. 判断子序列、115. 不同的子序列

LeetCode 392. 判断子序列 链接&#xff1a;392. 判断子序列 双指针&#xff1a; 思路&#xff1a; 本题较容易&#xff0c;如果不用动态规划而是用双指针的办法思路会更加简单。首先两个指针fast&#xff0c;slow分别代表t&#xff0c;s的下标&#xff0c;快指针用于遍历长…

来浅谈一下:GraalVM下载、安装、特点、概括

文章目录前言一、GraaIVM是什么&#xff1f;二、GraaIVM优点三、安装GraaIVM1.GraaIVM Community版本简略2.下载3.解压4.配置变量4.1、JAVA_HOME改成graalvm的位置4.2、编辑path5、查看总结前言 GraaIVM High-performance runtime with new compiler optimizations to accele…

unity计算着色器

序 计算着色器&#xff0c;是什么&#xff1f;好像是并行计算的一个东西。 并行计算&#xff0c;挖矿&#xff1f;显卡&#xff1f; 那看来得先了解显卡&#xff0c;再了解计算着色器了。 认识显卡 显卡&#xff0c;小白&#xff0c;不懂。 显卡的印象&#xff0c;只是停…

批量修改文件名,图文教学,2分钟简单学会

​文件名称是文件的重要组成部分&#xff0c;在我们日常生活中&#xff0c;对文件进行命名&#xff0c;是经常使用到的一种功能。可是有时候需要重命名的文件实在是太多了咋办呢&#xff1f;有没有什么方法可以批量修改文件名&#xff1f; 本文将以图文教学的方式&#xff0c;…

为什么不建议在MySQL中使用 utf8?

MySQL 字符编码集中有两套 UTF-8 编码实现&#xff1a;utf8 和 utf8mb4。 如果使用 utf8 的话&#xff0c;存储 emoji 符号和一些比较复杂的汉字、繁体字就会出错。 为什么会这样呢&#xff1f;这篇文章可以从源头给你解答。 何为字符集&#xff1f; 字符是各种文字和符号的…

LPA-star算法(Lifelong Planning)及相关思考

一、LPA-star算法&#xff08;Lifelong Planning&#xff09;简介 LPA * ( Lifelong Planning 终身规划 A * )是一种基于A * 的增量启发式搜索算法&#xff0c;被用来处理动态环境下从给定起始点到给定目标点的最短路径问题&#xff0c;即起始点和目标点是固定的。 &#xff08…

图数据库知识点1:图数据库与关系型数据库区别

文章目录 前言一、图数据库区别于其他数据库的核心是什么&#xff1f;二、图数据库能解决哪些问题&#xff1f; 1.图的优势2.目前的图的实现方式及优劣3.图的技术趋势及优势小结总结前言 《图数据库知识点》系列有20讲&#xff0c;每一讲中会重点分享一个图数据库知识点&#…

什么是JVM?JVM的机制与JVM自动内存管理机制,如何进行优化

1. 什么是JVM&#xff1f; JVM是Java Virtual Machine&#xff08;Java虚拟机&#xff09;的缩写&#xff0c;JVM是一种用于计算设备的规范&#xff0c;它是一个虚构出来的计算机&#xff0c;是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码…

彻底搞懂MySql的B+Tree

1.什么是索引 官方定义&#xff1a;一种能为mysql提高查询效率的数据结构&#xff0c;索引是为了加速对表中数据行的检索而创建的一种分散存储的数据结构。好比如&#xff0c;一本书&#xff0c;你想找到自己想看的章节内容&#xff0c;直接查询目录就行。这里的目录就类似索引…

华为路由器升级系统文件

欢迎关注微信公众号【厦门微思网络】。http://www.xmws.cn 组网图形 组网需求 RouterA的管理网口与用户侧主机HostA相连。要求通过BootROM菜单下载系统文件至RouterA完成系统升级。 操作步骤 1.在PC端启动FTP Server服务。 2.用串口线连接并通过Console口登录设备。 3.重启设…

Java内存模型与线程(3)

文章目录4. Java与线程4.1 线程的实现4.2 Java线程调度4.3 状态转换4. Java与线程 并发不一定要依赖多线程&#xff08;如PHP中很常见的多进程并发&#xff09;&#xff0c;但是在Jva里面谈论并发&#xff0c;大多数都与线程脱不开关系。既然我们这本书探讨的话题是Java虚拟机…

一个系列涨粉47w,小红书内容创意卷出新高度

前有双11&#xff0c;后有世界杯&#xff0c;11月注定是热闹的。图源新红_流量分析_趋势查询在此情况下&#xff0c; 小红书内又涌现出哪些黑马博主&#xff1f;有多少品牌打造出了爆品&#xff1f;什么样的种草玩法才能成功出圈&#xff1f;我们将全面分析11月榜单&#xff0c…

java面向对象最全入门笔记

Java面向对象 什么是面向对象编程&#xff1f; 面向&#xff1a;找、拿。 对象&#xff1a;东西。 面向对象编程&#xff1a;找或者拿东西过来编程。 设计对象并使用 设计类&#xff0c;创建对象并使用 类是什么&#xff1f; 类&#xff08;设计图&#xff09;&#xff1…

Vue Cli安装和node-sass、less-loader、sass-loader安装

一、Vue Cli安装 CLI全程是Command-Line Interface&#xff0c;命令行界面&#xff0c;俗称脚手架&#xff0c;可以帮我们快速的创建vue项 Vue Cli的使用必须依赖node环境和webpack 管理员方式打开cmd进行安装&#xff0c;安装命令&#xff1a; npm i -g vue/cli 查看版本…

鸢尾花数据种类预测、分析与处理、scikit-learn数据集使用、seaborn作图及数据集的划分

一、鸢尾花种类预测 Iris数据集是常用的分类实验数据集&#xff0c;由Fisher, 1936收集整理&#xff0c;Iris也称鸢尾花卉数据集&#xff0c;是一类多重变量分析的数据集 鸢尾花数据集包含了 4个属性&#xff08;特征值&#xff09; Sepal.Length&#xff08;花萼长度&#…