项目复习(2)

news2025/5/19 18:34:59

第四天

高并发优化

前端每隔15秒就发起一次请求,将播放记录写入数据库。

但问题是,提交播放记录的业务太复杂了,其中涉及到大量的数据库操作:在并发较高的情况下,会给数据库带来非常大的压力

使用Redis合并写请求

一方面我们要缓存写数据,减少写数据库频率;另一方面我们要缓存播放记录,减少查询数据库。因此,缓存中至少要包含3个字段:

  • 记录id:id,用于根据id更新数据库
  • 播放进度:moment,用于缓存播放进度
  • 播放状态(是否学完):finished,用于判断是否是第一次学完

可以使用redis种Hash结构

DelayQueue

Redisson

MQ

时间轮

原理

JDK自带延迟队列,基于阻塞队列实现。

基于Redis数据结构模拟JDK的DelayQueue实现

利用MQ的特性。例如RabbitMQ的死信队列

时间轮算法

优点

  • 不依赖第三方服务
  • 分布式系统下可用
  • 不占用JVM内存
  • 分布式系统下可以
  • 不占用JVM内存
  • 不依赖第三方服务
  • 性能优异

缺点

  • 占用JVM内存
  • 只能单机使用
  • 依赖第三方服务
  • 依赖第三方服务
  • 只能单机使用

优化代码

改的时绿框的2

把原来视频处理的第一步查询数据库的旧数据改为先查询缓存,缓存没有在查询数据库并返回缓存(这里封装成一个方法了方便业务逻辑)

/**
 * 查询旧的学习记录
 *
 * @param userId
 * @param dto
 */
private LearningRecord queryOldRecord(Long userId, LearningRecordFormDTO dto) {
    //1。查询缓存
    LearningRecord learningRecord = recordDelayTaskHandler.readRecordCache(dto.getLessonId(), dto.getSectionId());
    //2.查到数据直接方法
    if (learningRecord != null) {
        return learningRecord;
    }
    //3.没有查询到就数据库
    LearningRecord dbrecord = this.lambdaQuery()
            .eq(LearningRecord::getUserId, userId)
            .eq(LearningRecord::getLessonId, dto.getLessonId())
            .eq(LearningRecord::getSectionId, dto.getSectionId())
            .one();
    //4.把数据库查询到的数据放入缓存
    if(dbrecord == null){
        return null;
    }
    recordDelayTaskHandler.writeRecordCache(dbrecord);
    return dbrecord;
}

这里时绿框的1和3

//3.不是第一次学习--更新学习记录--moment//finished和finishTime取决与是否第一次学完
//3.1判断是否第一次学完
Boolean finished = record.getFinished();//是否学完
Integer moment = record.getMoment();//视频的当前观看时长,单位秒
Integer duration = dto.getDuration();//视频总时长
boolean isFinished = !finished && moment * 2 > duration;//是否第一次学完
if(!isFinished){
    //不是第一次学完
    //学习记录到缓存
    LearningRecord learningRecord = new LearningRecord();
    learningRecord.setLessonId(dto.getLessonId());
    learningRecord.setSectionId(dto.getSectionId());
    learningRecord.setMoment(dto.getMoment());
    learningRecord.setFinished(finished);
    learningRecord.setId(record.getId());
    //提交延迟任务
    recordDelayTaskHandler.addLearningRecordTask(learningRecord);
    return finished;
}
//第一次学完,更新数据库
boolean update = this.lambdaUpdate()
            .set(LearningRecord::getMoment, dto.getMoment())
            .set(isFinished, LearningRecord::getFinished, isFinished)
            .set(isFinished, LearningRecord::getFinishTime, dto.getCommitTime())
            .eq(LearningRecord::getUserId, userId)
            .eq(LearningRecord::getLessonId, dto.getLessonId())
            .eq(LearningRecord::getSectionId, dto.getSectionId())
            .update();
if (!update) {
    throw new DbException("更新学习记录失败");
}
//还需要清理缓存
recordDelayTaskHandler.cleanRecordCache(dto.getLessonId(), dto.getSectionId());
return true;

第五天

1.新增和修改互动问题

简单的crud---无

2.用户端分页查询问题列表

⭐Mybatis-plus中排除某个字段

1.直接把需要的字段用.select(InteractionQuestion::getId.......)

Page<InteractionQuestion> page = this.lambdaQuery()
//不需要返回问题描述
//.select(InteractionQuestion::getId, InteractionQuestion::getTitle, InteractionQuestion::getCourseId)
//.select(InteractionQuestion.class, info -> !info.getProperty().equals("description"))
.select(InteractionQuestion.class, new Predicate<TableFieldInfo>() {
    @Override
    public boolean test(TableFieldInfo tableFieldInfo) {
        //指定不用查询的字段
        // tableFieldInfo.getProperty();获取InteractionQuestion中的属性名称
        return !tableFieldInfo.getProperty().equals("description");
    }
})
.eq(courseId != null, InteractionQuestion::getCourseId, courseId)
.eq(sectionId != null, InteractionQuestion::getSectionId, sectionId)
.eq(query.getOnlyMine(), InteractionQuestion::getUserId, userId)
.eq(InteractionQuestion::getHidden, false)
.page(query.toMpPageDefaultSortByCreateTimeDesc());

⭐Steam流--过滤


Set<Long> userIds = new HashSet<>();
Set<Long> answerIds = new HashSet<>();
for (InteractionQuestion record : records) {
    //获取没匿名的提问者id集合
    if(!record.getAnonymity()){
        userIds.add(record.getUserId());
    }
    //最新回答的id集合
    if(record.getLatestAnswerId()!=null) {
        answerIds.add(record.getLatestAnswerId());
    }
}

Set<Long> answerIds = records.stream()
        .filter(c -> c.getLatestAnswerId() != null)
        .map(InteractionQuestion::getLatestAnswerId)
        .collect(Collectors.toSet());

Set<Long> userIds = records.stream()
        .filter(interactionQuestion -> !interactionQuestion.getAnonymity())
        .map(InteractionQuestion::getUserId)
        .collect(Collectors.toSet());

完整代码

/**
     * 用户端分页查询问题列表
     *
     * @param query
     * @return
     */
    @Override
    public PageDTO<QuestionVO> pageQuestion(QuestionPageQuery query) {
        //如果用户是匿名提问,则不应返回提问者信息
        //如果是被管理端隐藏的问题,不应返回
        Long userId = UserContext.getUser();
        //1.校验参数
        Long courseId = query.getCourseId();
        Long sectionId = query.getSectionId();
        if (courseId == null && sectionId == null) {
            throw new BadRequestException("参数错误");
        }




        //2.分页查询--interaction_question 条件:
        // course_id
        // onMine为ture才加
        // userId
        // 小节id不为空才加
        // Hidden为false(不被隐藏)
        // 分页id
        // 按照提问时间排序
        Page<InteractionQuestion> page = this.lambdaQuery()
                //不需要返回问题描述
                //.select(InteractionQuestion::getId, InteractionQuestion::getTitle, InteractionQuestion::getCourseId)
                //.select(InteractionQuestion.class, info -> !info.getProperty().equals("description"))
                .select(InteractionQuestion.class, new Predicate<TableFieldInfo>() {
                    @Override
                    public boolean test(TableFieldInfo tableFieldInfo) {
                        //指定不用查询的字段
                        // tableFieldInfo.getProperty();获取InteractionQuestion中的属性名称
                        return !tableFieldInfo.getProperty().equals("description");
                    }
                })
                .eq(courseId != null, InteractionQuestion::getCourseId, courseId)
                .eq(sectionId != null, InteractionQuestion::getSectionId, sectionId)
                .eq(query.getOnlyMine(), InteractionQuestion::getUserId, userId)
                .eq(InteractionQuestion::getHidden, false)
                .page(query.toMpPageDefaultSortByCreateTimeDesc());
        List<InteractionQuestion> records = page.getRecords();
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page);
        }
        //查询最新回答者id和最新问题的回答id
        Set<Long> userIds = new HashSet<>();
        Set<Long> answerIds = new HashSet<>();
        for (InteractionQuestion record : records) {
            //获取没匿名的提问者id集合
            if (!record.getAnonymity()) {
                userIds.add(record.getUserId());
            }
            //最新回答的id集合
            if (record.getLatestAnswerId() != null) {
                answerIds.add(record.getLatestAnswerId());
            }
        }

//        Set<Long> answerIds = records.stream()
//                .filter(c -> c.getLatestAnswerId() != null)
//                .map(InteractionQuestion::getLatestAnswerId)
//                .collect(Collectors.toSet());
//
//        Set<Long> userIds = records.stream()
//                .filter(interactionQuestion -> !interactionQuestion.getAnonymity())
//                .map(InteractionQuestion::getUserId)
//                .collect(Collectors.toSet());





        //3.查询最近一次的回答---interaction_reply 条件:latest.answer_id
        Map<Long, InteractionReply> replyMap = new HashMap<>();
        if (!CollUtils.isEmpty(answerIds)) {
            List<InteractionReply> latestAnswers = replyService.lambdaQuery()
                    .in(InteractionReply::getId, answerIds)
                    .list();
            //转成map
            replyMap = latestAnswers.stream()
                    .collect(Collectors.toMap(InteractionReply::getId, r -> r));
            //查到的是user_id--要把user_id转成user_name(直接放入userIds)
            //latestAnswers.forEach(r -> userIds.add(r.getUserId()));
            for (InteractionReply latestAnswer : latestAnswers) {
                if (!latestAnswer.getAnonymity()) {
                    userIds.add(latestAnswer.getTargetUserId());
                }
            }

        }

        //4.查询最近回答的用户--远程调用用户服务
        Map<Long, UserDTO> userDTOMap = new HashMap<>();
        if (CollUtils.isEmpty(userIds)) {
            List<UserDTO> userDTOS = userClient.queryUserByIds(userIds);
            //list转map
            userDTOMap = userDTOS.stream()
                    .collect(Collectors.toMap(UserDTO::getId, u -> u));
        }

        //5.封装DTO返回
        List<QuestionVO> voList = new ArrayList<>();
        for (InteractionQuestion record : records) {
            QuestionVO vo = BeanUtils.copyBean(record, QuestionVO.class);
            if (!vo.getAnonymity()) {
                UserDTO userDTO = userDTOMap.get(record.getUserId());
                //最新回答者信息
                vo.setUserName(userDTO.getName());//昵称
                vo.setUserIcon(userDTO.getIcon());//头像
            }
            //最新回答信息
            InteractionReply reply = replyMap.get(record.getLatestAnswerId());
            if (reply != null) {
                vo.setLatestReplyContent(reply.getContent());//最新回答内容
                if (!reply.getAnonymity()) {//最新回答者不是匿名的
                    vo.setLatestReplyUser(userDTOMap.get(reply.getUserId()).getName());//最新回答者昵称
                }
            }
            voList.add(vo);
        }

        return PageDTO.of(page, voList);
    }

 3.用户端根据id查询问题详情

⭐️4.管理端分页查询问题列表

1.如果前端传了课程名称,先es中得到课程id(注意这里是es中的课程id)

//1.如果前端传了课程名称,先es中得到es中课程id
boolean haveCourseIdList = false;
List<Long> coursesIdList = new ArrayList<>();
if (StringUtils.isNotBlank(query.getCourseName())) {
    coursesIdList = searchClient.queryCoursesIdByName(query.getCourseName());
    haveCourseIdList = CollUtils.isEmpty(coursesIdList);
    if(haveCourseIdList){
        return PageDTO.empty(0L, 0L);
    }

2.分页查询---没有章节名称,小节名称,课程名称,分类名称,提问者名称

//2.分页查询 条件:前端条件+分页条件
Page<InteractionQuestion> page = this.lambdaQuery()
.in(haveCourseIdList, InteractionQuestion::getCourseId, coursesIdList)
.between(query.getBeginTime() != null && query.getEndTime() != null, InteractionQuestion::getCreateTime, query.getBeginTime(), query.getEndTime())
.eq(query.getStatus() != null, InteractionQuestion::getStatus, query.getStatus())
.page(query.toMpPageDefaultSortByCreateTimeDesc());
List<InteractionQuestion> records = page.getRecords();
if (CollUtils.isEmpty(records)) {
    return PageDTO.empty(page);
}

3.先把章节id和小节id,课程id,提问者id都封装成集合

//提问学员id集合
Set<Long> userIdList = records.stream()
        .map(InteractionQuestion::getUserId).collect(Collectors.toSet());
//章节和小节id集合
Set<Long> chapterAndSectionIdList = new HashSet<>();
        chapterAndSectionIdList = records.stream()
                .map(InteractionQuestion::getChapterId)
                .collect(Collectors.toSet());
        Set<Long> sectionIds = records.stream()
                .map(InteractionQuestion::getSectionId)
                .collect(Collectors.toSet());
chapterAndSectionIdList.addAll(sectionIds);
//表里面课程id集合
Set<Long> courseIdList = records.stream()
        .map(InteractionQuestion::getCourseId).collect(Collectors.toSet());

4.根据课程id远程调用课程服务查询课程信息-------课程名称

//4.根据课程id远程调用课程服务查询课程信息
List<CourseSimpleInfoDTO> cInfoList = courseClient.getSimpleInfoList(coursesIdList);
if(CollUtils.isEmpty(cInfoList)){
   throw new BadRequestException("课程不存在");
}
//转成map方便查询
Map<Long, CourseSimpleInfoDTO> cInfoMap = cInfoList.stream()
.		collect(Collectors.toMap(CourseSimpleInfoDTO::getId, c -> c));

5.根据提问学员id远程调用查询用户表--------提问者名称

//5.根据提问学员id远程调用查询用户表
List<UserDTO> userDTOS = userClient.queryUserByIds(userIdList);
if(CollUtils.isEmpty(userDTOS)){
    throw new BadRequestException("用户不存在");
}
//转成map方便查询
Map<Long, UserDTO> userDTOMap = userDTOS.stream()
        .collect(Collectors.toMap(UserDTO::getId, u -> u));

6.根据章节id和小节id远程调用查询章节信息-----

//6.根据章节id和小节id远程调用查询分类信息
List<CataSimpleInfoDTO> infoDTOS = categoryClient.batchQueryCatalogue(chapterAndSectionIdList);
if(CollUtils.isEmpty(infoDTOS)){
   throw new BadRequestException("章节或小节信息不存在");
}
//转成map方便查询
Map<Long, CataSimpleInfoDTO> infoMap = infoDTOS.stream()
        .collect(Collectors.toMap(CataSimpleInfoDTO::getId, c -> c));

7.封装方法

List<QuestionAdminVO> voList = new ArrayList<>();
for (InteractionQuestion record : records) {
    QuestionAdminVO vo = BeanUtils.copyBean(record, QuestionAdminVO.class);
    UserDTO userDTO = uInfoMap.get(record.getUserId());
    if(userDTO != null){
        vo.setUserName(userDTO.getName());//提问者昵称
    }
    CourseSimpleInfoDTO cInfoDTO = cInfoMap.get(record.getCourseId());
    if(cInfoDTO != null) {
        vo.setCourseName(cInfoDTO.getName());//课程名称
        //Caffeine
        List<Long> categoryIds = cInfoDTO.getCategoryIds();
        String categoryNames = categoryCache.getCategoryNames(categoryIds);
        vo.setCategoryName(categoryNames);//三级分类名称,中间使用/隔开
    }
    CataSimpleInfoDTO CASInfoDTO = CASinfoMap.get(record.getChapterId());
    if(CASInfoDTO != null) {
        vo.setChapterName(CASInfoDTO.getName());//章名称

    }
    CataSimpleInfoDTO CASInfoDTO2 = CASinfoMap.get(record.getSectionId());
    if(CASInfoDTO2 != null){
        vo.setSectionName(CASInfoDTO2.getName());
    }


    //    private String userName;
    //    private String courseName;
    //    private String chapterName;
    //    private String sectionName;
    //    private String categoryName;
}
return PageDTO.of(page, voList);
}

5.新增评论/回答

可以先把问题表的属性赋值了,最后一块修改数据库,不要多次查询修改

/**
 * 新增评论/回答
 *
 * @param dto
 */
@Override
public void saveReply(ReplyDTO dto) {
//1.获取当前用户id
Long userId = UserContext.getUser();
//2.dto转成实体类
InteractionReply reply = BeanUtils.copyBean(dto, InteractionReply.class);
//3.新增评论/回答到回复表
boolean save = this.save(reply);
//4.如果是回答,更新问题表
InteractionQuestion question = new InteractionQuestion();
if (dto.getAnswerId() != null) {
    //4.1.不是是回答是累加评论数
    InteractionReply answerInfo = this.getById(dto.getAnswerId());
    answerInfo.setReplyTimes(answerInfo.getReplyTimes() + 1);
    this.updateById(answerInfo);
} else {
    //4.2.是回答,累加回答次数
    question = questionMapper.selectById(dto.getQuestionId());
    question.setId(dto.getQuestionId());
    question.setAnswerTimes(question.getAnswerTimes() + 1);
}
//5.是否是学生提交
if (dto.getIsStudent()) {
    //6.是学生提交把问题表status字段设置为已查看
    question.setStatus(QuestionStatus.CHECKED.getValue());
}

//等属性全部修改完,在更新数据库
questionMapper.updateById(question);

}

6.分页查询回答/评论

和上面的分页一模一样

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

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

相关文章

UE 材质基础 第一天

课程&#xff1a;虚幻引擎【UE5】材质宝典【初学者材质基础入门系列】-北冥没有鱼啊_-稍后再看-哔哩哔哩视频 随便记录一些 黑色是0到负无穷&#xff0c;白色是1到无穷 各向异性 有点类似于高光&#xff0c;可以配合切线来使用&#xff0c;R G B 相当于 X Y Z轴&#xff0c;切…

学习FineBI

FineBI 第一章 FineBI 介绍 1.1. FineBI 概述 FineBI 是帆软软件有限公司推出的一款商业智能 &#xff08;Business Intelligence&#xff09; 产品 。 FineBI 是新一代大数据分析的 BI 工具 &#xff0c; 旨在帮助企业的业务人员充分了解和利用他们的数据 。FineBI 凭借强…

深入剖析某App视频详情逆向:聚焦sig3参数攻克

深入剖析某手App视频详情逆向&#xff1a;聚焦sig3参数攻克 一、引言 在当今互联网信息爆炸的时代&#xff0c;短视频平台如某手&#xff0c;已成为人们获取信息、娱乐消遣的重要渠道。对于技术爱好者和研究人员而言&#xff0c;深入探索其内部机制&#xff0c;特别是视频详情…

【Linux】Linux安装并配置MongoDB

目录 1.添加仓库 2.安装 MongoDB 包 3.启动 MongoDB 服务 4. 验证安装 5.配置 5.1.进入无认证模式 5.2.1创建用户 5.2.2.开启认证 5.2.3重启 5.2.4.登录 6.端口变更 7.卸载 7.1.停止 MongoDB 服务 7.2.禁用 MongoDB 开机自启动 7.3.卸载 MongoDB 包 7.4.删除数…

新电脑软件配置二:安装python,git, pycharm

安装python 地址 https://www.python.org/downloads/ 不是很懂为什么这么多版本 安装windows64位的 这里我是凭自己感觉装的了 然后cmd输入命令没有生效&#xff0c;先重启下&#xff1f; 重启之后再次验证 环境是成功的 之前是输入的python -version 命令输入错误 安装pyc…

数据仓库:企业数据管理的核心引擎

一、数据仓库的由来 数据仓库&#xff08;Data Warehouse, DW&#xff09;概念的诞生源于企业对数据价值的深度挖掘需求。在1980年代&#xff0c;随着OLTP&#xff08;联机事务处理&#xff09;系统在企业中的普及&#xff0c;传统关系型数据库在处理海量数据分析时显露出明显瓶…

MCU开发学习记录17* - RTC学习与实践(HAL库) - 日历、闹钟、RTC备份寄存器 -STM32CubeMX

名词解释&#xff1a; RTC&#xff1a;Real-Time Clock​ 统一文章结构&#xff08;数字后加*&#xff09;&#xff1a; 第一部分&#xff1a; 阐述外设工作原理&#xff1b;第二部分&#xff1a;芯片参考手册对应外设的学习&#xff1b;第三部分&#xff1a;使用STM32CubeMX进…

C++中的四种强制转换

static_cast 原型&#xff1a;static_cast<type-id>(expression) type-id表示目标类型&#xff0c;expression表示要转换的表达式 static_cast用于非多态类型的转换&#xff08;静态转换&#xff09;&#xff0c;编译器隐式执行的任何类型转换都可用static_c…

YOLOv2目标检测算法:速度与精度的平衡之道

一、YOLOv2的核心改进&#xff1a;从V1到V2的蜕变 YOLOv2作为YOLO系列的第二代算法&#xff0c;在继承V1端到端、单阶段检测的基础上&#xff0c;针对V1存在的小目标检测弱、定位精度低等问题进行了全方位升级&#xff0c;成为目标检测领域的重要里程碑。 &#xff08;一&am…

利用腾讯云MCP提升跨平台协作效率的实践与探索

一、场景痛点 在当今这个数字化快速发展的时代&#xff0c;跨平台协作成为了许多企业和团队面临的一个重大挑战。随着企业业务的不断拓展&#xff0c;团队成员往往需要利用多种工具和平台进行沟通、协作和管理。这些平台包括但不限于电子邮件、即时通讯工具、项目管理软件、文…

【Vue篇】数据秘语:从watch源码看响应式宇宙的蝴蝶效应

目录 引言 一、watch侦听器&#xff08;监视器&#xff09; 1.作用&#xff1a; 2.语法&#xff1a; 3.侦听器代码准备 4. 配置项 5.总结 二、翻译案例-代码实现 1.需求 2.代码实现 三、综合案例——购物车案例 1. 需求 2. 代码 引言 &#x1f4ac; 欢迎讨论&#…

OGGMA 21c 微服务 (MySQL) 安装避坑指南

前言 这两天在写 100 天实战课程 的 OGG 微服务课程&#xff1a; 在 Oracle Linux 8.10 上安装 OGGMA 21c MySQL 遇到了一点问题&#xff0c;分享给大家一起避坑&#xff01; 环境信息 环境信息&#xff1a; 主机版本主机名实例名MySQL 版本IP 地址数据库字符集Goldengate …

Linux面试题集合(4)

现有压缩文件:a.tar.gz存在于etc目录&#xff0c;如何解压到data目录 tar -zxvf /etc/a.tar.gz -C /data 给admin.txt创建一个软链接 ln -s admin.txt adminl 查找etc目录下以vilinux开头的文件 find /etc -name vilinux* 查找admin目录下以test开头的文件 find admin -name te…

Android Studio 安装与配置完全指南

文章目录 第一部分&#xff1a;Android Studio 简介与安装准备1.1 Android Studio 概述1.2 系统要求Windows 系统&#xff1a;macOS 系统&#xff1a;Linux 系统&#xff1a; 1.3 下载 Android Studio 第二部分&#xff1a;安装 Android Studio2.1 Windows 系统安装步骤2.2 mac…

基于 Zookeeper 部署 Kafka 集群

文章目录 1、前期准备2、安装 JDK 83、搭建 Zookeeper 集群3.1、下载3.2、调整配置3.3、标记节点3.4、启动集群 4、搭建 Kafka 集群4.1、下载4.2、调整配置4.3、启动集群 1、前期准备 本次集群搭建使用&#xff1a;3 Zookeeper 3 Kafka&#xff0c;所以我在阿里云租了3台ECS用…

IDE/IoT/搭建物联网(LiteOS)集成开发环境,基于 LiteOS Studio + GCC + JLink

文章目录 概述LiteOS Studio不推荐&#xff1f;安装和使用手册呢?HCIP实验的源码呢&#xff1f; 软件和依赖安装软件下载软件安装插件安装依赖工具-方案2依赖工具-方案1 工程配置打开或新建工程板卡配置组件配置编译器配置-gcc工具链编译器配置-Makefile脚本其他配置编译完成 …

算法加训之最短路 上(dijkstra算法)

目录 P4779 【模板】单源最短路径&#xff08;标准版&#xff09;&#xff08;洛谷&#xff09; 思路 743. 网络延迟时间&#xff08;力扣&#xff09; 思路 1514.概率最大路径&#xff08;力扣&#xff09; 思路 1631.最小体力消耗路径 思路 1976. 到达目的地的方案数 …

QT+Opencv 卡尺工具找直线

QTOpencv 卡尺工具找直线 自己将别的项目中&#xff0c;单独整理出来的。实现了一个找直线的工具类。 功能如下&#xff1a;1.添加图片 2.添加卡尺工具 3.鼠标可任意拖动图片和卡尺工具 4.可调整卡尺参数和直线拟合参数 5.程序中包含了接口函数&#xff0c;其他cpp文件传入相…

GraphPad Prism简介、安装与工作界面

《2025GraphPad Prism操作教程书籍 GraphPad Prism图表可视化与统计数据分析视频教学版GraphPad Prism科技绘图与数据分析学术图表 GraphPadPrism图表》【摘要 书评 试读】- 京东图书 GraphPad Prism统计数据分析_夏天又到了的博客-CSDN博客 1.1 GraphPad Prism简介 GraphP…

esp32课设记录(一)按键的短按、长按与双击

课程用的esp32的板子上只有一个按键&#xff0c;引脚几乎都被我用光了&#xff0c;很难再外置按键。怎么控制屏幕的gui呢&#xff1f;这就得充分利用按键了&#xff0c;比如说短按、长按与双击&#xff0c;实现不同的功能。 咱们先从短按入手讲起。 通过查看原理图&#xff0c;…