大文件上传

上图就是大致的流程


一、标题图片

  1. 上传课程的标题图片

  1. Ajax发送请求到后端

  1. 后端接收到图片使用IO流去保存图片,返回图片的信息对象

  1. JS回调函数接收对象通过$("元素id").val(值),方式给页面form表达img标签src属性值,达到上传图片并回显

二、大文件上传(流媒体、音频、.zip文件等)

  1. 页面div点击事件,选中一个视频文件

  1. JS判断文件大小,判断文件类型是否合法

3.JS循环切片,计算总片数,计算每一片的起始位置,循环上传

4.发送Ajax转给后端切片

5.后端接收并创建临时目录存放

    /**
     * 分片上传,前端调用此方法
     * @param request
     * @param guid
     * @param chunk
     * @param file
     * @return
     */
    @PostMapping("/uploadSlice")
    @ResponseBody
    public ResponseResult<?> uploadSlice2(HttpServletRequest request, @RequestParam("guid") String guid,
                                          @RequestParam("chunk") Integer chunk,
                                          @RequestParam("file") MultipartFile file) {
        if (this.uploadSlice(request, guid, chunk, file)){
            return ResponseResultUtils.genResult("上传成功","");
        }else{
            return ResponseResultUtils.genErrorResult("上传失败");
        }
    }

    /**
     * 分片上传的具体方法
     * @param request
     * @param guid
     * @param chunk
     * @param file
     * @return
     */
    private boolean uploadSlice(HttpServletRequest request, String guid, Integer chunk, MultipartFile file) {
        try {
            boolean isMultipart = ServletFileUpload.isMultipartContent(request);
            logger.info("isMultipart = {}",isMultipart);
            if (isMultipart) {
                if (chunk == null){
                    chunk = 0;
                }
                // 临时目录用来存放所有分片文件
                String tempFileDir =  rootFilePath + bigPath + guid;
                File parentFileDir = new File(tempFileDir);
                if (!parentFileDir.exists()) {
                    parentFileDir.mkdirs();
                }
                logger.info("接到上传的分片文件,{},{},{}",guid,chunk,tempFileDir);
                // 分片处理时,前台会多次调用上传接口,每次都会上传文件的一部分到后台
                File tempPartFile = new File(parentFileDir, guid + "_" + chunk + ".part");
                FileUtils.copyInputStreamToFile(file.getInputStream(), tempPartFile);
            }
        } catch (Exception e) {
            return false;
        }
        return true;
    }

6.计数器变量值=总片数时,发送合并请求

7.后端合并

/**
     * 分片文件合并,前端调用此方法
     * @param guid
     * @param fileName
     * @return
     */
    @RequestMapping("/uploadMerge")
    @ResponseBody
    public ResponseResult<?> uploadMerge2(@RequestParam("guid") String guid, @RequestParam("fileName") String fileName) {
        // 得到 destTempFile 就是最终的文件
        SpaceImage image = this.uploadMerge(guid, fileName);
        if(null != imageSpaceImage){
            return ResponseResultUtils.genResult(imageSpaceImage,"合并成功");
        }else{
            return ResponseResultUtils.genErrorResult("合并文件失败");
        }
    }

    private ImageSpaceImage uploadMerge(String guid, String fileName){
        SpaceImage image = mergeFile(guid, fileName);
        //此处需要注意,OSS需要再次切片上传,但minIO是不用得,它默认5M超过就会自动切片
        String path = "";
        //移除文件
        poolTaskExecutor.execute(() -> {
            com.eyang.ecpp.utils.FileUtils.deleteFile(rootFilePath+bigPath);
        });
        return imageSpaceImage;
    }

    private ImageSpaceImage mergeFile(String guid, String fileName) {
        logger.info("接到上传的分片文件合并请求,{},{}",guid,fileName);
        try {
            String sName = fileName.substring(fileName.lastIndexOf("."));
            //时间格式化格式
            Date currentTime = new Date();
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");
            //获取当前时间并作为时间戳
            String timeStamp = simpleDateFormat.format(currentTime);
            //拼接新的文件名
            String newName = timeStamp + sName;
            simpleDateFormat = new SimpleDateFormat("yyyyMM");
            String tempPath = rootFilePath + bigPath+guid;
            String margePath = rootFilePath + bigPath+simpleDateFormat.format(currentTime);
            File parentFileDir = new File(tempPath);
            if (parentFileDir.isDirectory()) {
                File destTempFile = new File(margePath, newName);
                if (!destTempFile.exists()) {
                    //先得到文件的上级目录,并创建上级目录,在创建文件
                    destTempFile.getParentFile().mkdir();
                    destTempFile.createNewFile();
                }
                for (int i = 0; i < Objects.requireNonNull(parentFileDir.listFiles()).length; i++) {
                    File partFile = new File(parentFileDir, guid + "_" + i + ".part");
                    FileOutputStream destTempfos = new FileOutputStream(destTempFile, true);
                    //遍历"所有分片文件"到"最终文件"中
                    FileUtils.copyFile(partFile, destTempfos);
                    destTempfos.close();
                }
                // 删除临时目录中的分片文件
                FileUtils.deleteDirectory(parentFileDir);

                String[] resultArr = FileStorageUtils.saveBigFile(Files.readAllBytes(Paths.get(destTempFile.getPath())), com.eyang.ecpp.utils.FileUtils.getExtension(destTempFile.getName()));
                ImageSpaceImage imageSpaceImage = new ImageSpaceImage();

                //第一个是组名 第二个是改后的文件名
                imageSpaceImage.setImgUrl(margePath+"/"+newName);
                //imageSpaceImage.setName(destTempFile.getName());
                imageSpaceImage.setName(fileName);
                return imageSpaceImage;//destTempFile.getAbsolutePath();
            }
        } catch (Exception e) {
            logger.error("切片文件合并,失败原因e:{}", e.getMessage());
        }
        return null;

    }

8.合并成功后进行转码

转码需要下载一个ffmpeg,下载完解压打开bin目录,打开电脑的环境变量往path中添加上bin的目录即可。

还需要引入依赖

 <!--  视频编码 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>

9.转码发送请求

10.后端转码

/**
     * 视频编码
     *
     * @param absolutePath 绝对路径
     * @return {@link ResponseResult}<{@link ?}>
     */
    @RequiresPermissions("cms:article:edit")
    @RequestMapping(value = "video/coding")
    @ResponseBody
    public ResponseResult<?> videoCoding(@RequestParam String absolutePath) {
        
        TranscodeConfig transcodeConfig = new TranscodeConfig();
        //设置视频封面
        transcodeConfig.setPoster("1");
        transcodeConfig.setTsSeconds("60");
        ResponseResult<?> transResult = TranscodeFileUtils.transCodeFile(absolutePath, transcodeConfig);
        String retUrl = "";
        if (null != transResult) {
            Map<String, Object> data = (Map<String, Object>) transResult.getData();
            if (null != data) {
                Map<String, Object> videoInfo = (Map<String, Object>) data.get("data");
                if (null != videoInfo) {
                    retUrl = (String) videoInfo.get("m3u8");
                }
            }
        }
        return ResponseResultUtils.genResult(retUrl,"转码成功");
    }

视频转码配置实体类

package com.utils;

public class TranscodeConfig {

    private String poster; // 截取封面的时间

    private String tsSeconds; // ts分片大小,单位是秒

    private String cutStart; // 视频裁剪,开始时间

    private String cutEnd; // 视频裁剪,结束时间


    public String getPoster() {
        return poster;
    }

    public void setPoster(String poster) {
        this.poster = poster;
    }

    public String getTsSeconds() {
        return tsSeconds;
    }

    public void setTsSeconds(String tsSeconds) {
        this.tsSeconds = tsSeconds;
    }

    public String getCutStart() {
        return cutStart;
    }

    public void setCutStart(String cutStart) {
        this.cutStart = cutStart;
    }

    public String getCutEnd() {
        return cutEnd;
    }

    public void setCutEnd(String cutEnd) {
        this.cutEnd = cutEnd;
    }

    public TranscodeConfig() {
    }

    public TranscodeConfig(String poster, String tsSeconds, String cutStart, String cutEnd) {
        this.poster = poster;
        this.tsSeconds = tsSeconds;
        this.cutStart = cutStart;
        this.cutEnd = cutEnd;
    }
}

转码工具类

public class TranscodeFileUtils {


    /**
     * 视频根路径
     */
    private static String videoFolder= Global.getConfig("video.folder");

    private static final Logger LOGGER = LoggerFactory.getLogger(TranscodeFileUtils.class);

    public static ResponseResult<Map<String, Object>> transCodeFile(String filePath, TranscodeConfig transcodeConfig){
        try {

            // 按照日期生成子目录
            String today = DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDate.now());
            String s = Identities.uuid2();
            Path targetFolder = Paths.get(videoFolder, today, s);

            // 执行转码操作
            LOGGER.info("开始转码");

            FFmpegUtils.transcodeToM3u8(filePath, targetFolder.toString(), transcodeConfig);

            // 封装结果
            Map<String, Object> videoInfo = new HashMap<>();
            videoInfo.put("m3u8", String.join("\\", targetFolder.toString(), "index.m3u8"));
            videoInfo.put("poster", String.join("\\", targetFolder.toString(), "poster.jpg"));

            Map<String, Object> result = new HashMap<>();
            result.put("success", true);
            result.put("data", videoInfo);
            return ResponseResultUtils.genResult(result,"转码成功!");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    public static void main(String[] args){

        System.out.println(TranscodeFileUtils.transCodeFile("E:\\录屏\\shipin\\test.mp4", new TranscodeConfig("00:00:00.001","15","","")));

    }

}

转码工具类

public class FFmpegUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(FFmpegUtils.class);

    // 跨平台换行符
    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    /**
     * 生成随机16个字节的AESKEY
     * @return
     */
    private static byte[] genAesKey ()  {
        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(128);
            return keyGenerator.generateKey().getEncoded();
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    /**
     * 在指定的目录下生成key_info, key文件,返回key_info文件
     * @param folder
     * @throws IOException
     */
    private static Path genKeyInfo(String folder) throws IOException {
        // AES 密钥
        byte[] aesKey = genAesKey();
        // AES 向量
        String iv = Hex.encodeHexString(genAesKey());

        // key 文件写入
        Path keyFile = Paths.get(folder, "key");
        Files.write(keyFile, aesKey, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

        // key_info 文件写入
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("key").append(LINE_SEPARATOR);                    // m3u8加载key文件网络路径
        stringBuilder.append(keyFile.toString()).append(LINE_SEPARATOR);    // FFmeg加载key_info文件路径
        stringBuilder.append(iv);                                            // ASE 向量

        Path keyInfo = Paths.get(folder, "key_info");

        Files.write(keyInfo, stringBuilder.toString().getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

        return keyInfo;
    }

    /**
     * 指定的目录下生成 master index.m3u8 文件
     * @param file            master m3u8文件地址
     * @param indexPath            访问子index.m3u8的路径
     * @param bandWidth            流码率
     * @throws IOException
     */
    private static void genIndex(String file, String indexPath, String bandWidth) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("#EXTM3U").append(LINE_SEPARATOR);
        stringBuilder.append("#EXT-X-STREAM-INF:BANDWIDTH=" + bandWidth).append(LINE_SEPARATOR);  // 码率
        stringBuilder.append(indexPath);
        Files.write(Paths.get(file), stringBuilder.toString().getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
    }

    /**
     * 转码视频为m3u8
     * @param source                源视频
     * @param destFolder            目标文件夹
     * @param config                配置信息
     * @throws IOException
     * @throws InterruptedException
     */
    public static void transcodeToM3u8(String source, String destFolder, TranscodeConfig config) throws IOException, InterruptedException {

        // 判断源视频是否存在
        if (!Files.exists(Paths.get(source))) {
            throw new IllegalArgumentException("文件不存在:" + source);
        }

        // 创建工作目录
        Path workDir = Paths.get(destFolder, "ts");
        Files.createDirectories(workDir);

        // 在工作目录生成KeyInfo文件
        Path keyInfo = genKeyInfo(workDir.toString());

        // 构建命令
        List<String> commands = new ArrayList<>();
        commands.add("ffmpeg");
        commands.add("-i");
        commands.add(source);                    // 源文件
        commands.add("-c:v");
        commands.add("libx264");                // 视频编码为H264
        commands.add("-c:a");
        commands.add("copy");                    // 音频直接copy
        commands.add("-hls_key_info_file");
        commands.add(keyInfo.toString());        // 指定密钥文件路径
        commands.add("-hls_time");
        commands.add(config.getTsSeconds());    // ts切片大小
        commands.add("-hls_playlist_type");
        commands.add("vod");                    // 点播模式
        commands.add("-hls_segment_filename");
        commands.add("%06d.ts");                // ts切片文件名称

        if (StringUtils.hasText(config.getCutStart())) {
            commands.add("-ss");
            commands.add(config.getCutStart());    // 开始时间
        }
        if (StringUtils.hasText(config.getCutEnd())) {
            commands.add("-to");
            commands.add(config.getCutEnd());        // 结束时间
        }
        commands.add("index.m3u8");                                                        // 生成m3u8文件

        // 构建进程
        Process process = new ProcessBuilder()
                .command(commands)
                .directory(workDir.toFile())
                .start();

        // 读取进程标准输出
        new Thread(() -> {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    LOGGER.info(line);
                }
            } catch (IOException e) {
            }
        }).start();

        // 读取进程异常输出
        new Thread(() -> {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    LOGGER.info(line);
                }
            } catch (IOException e) {
            }
        }).start();


        // 阻塞直到任务结束
        if (process.waitFor() != 0) {
            throw new RuntimeException("视频切片异常");
        }

        // 切出封面
        if (!screenShots(source, String.join(File.separator, destFolder, "poster.jpg"), config.getPoster())) {
            throw new RuntimeException("封面截取异常");
        }

        // 获取视频信息
        MediaInfo mediaInfo = getMediaInfo(source);
        if (mediaInfo == null) {
            throw new RuntimeException("获取媒体信息异常");
        }

        // 生成index.m3u8文件
        genIndex(String.join(File.separator, destFolder, "index.m3u8"), "ts/index.m3u8", mediaInfo.getFormat().getBitRate());

        // 删除keyInfo文件
        Files.delete(keyInfo);
    }

    /**
     * 获取视频文件的媒体信息
     * @param source
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    public static MediaInfo getMediaInfo(String source) throws IOException, InterruptedException {
        List<String> commands = new ArrayList<>();
        commands.add("ffprobe");
        commands.add("-i");
        commands.add(source);
        commands.add("-show_format");
        commands.add("-show_streams");
        commands.add("-print_format");
        commands.add("json");

        Process process = new ProcessBuilder(commands)
                .start();

        MediaInfo mediaInfo = null;

        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            mediaInfo = new Gson().fromJson(bufferedReader, MediaInfo.class);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (process.waitFor() != 0) {
            return null;
        }

        return mediaInfo;
    }

    /**
     * 截取视频的指定时间帧,生成图片文件
     * @param source        源文件
     * @param file            图片文件
     * @param time            截图时间 HH:mm:ss.[SSS]
     * @throws IOException
     * @throws InterruptedException
     */
    public static boolean screenShots(String source, String file, String time) throws IOException, InterruptedException {

        List<String> commands = new ArrayList<>();
        commands.add("ffmpeg");
        commands.add("-i");
        commands.add(source);
        commands.add("-ss");
        commands.add(time);
        commands.add("-y");
        commands.add("-q:v");
        commands.add("1");
        commands.add("-frames:v");
        commands.add("1");
        commands.add("-f");
        commands.add("image2");
        commands.add(file);

        Process process = new ProcessBuilder(commands)
                .start();

        // 读取进程标准输出
        new Thread(() -> {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    LOGGER.info(line);
                }
            } catch (IOException e) {
            }
        }).start();

        // 读取进程异常输出
        new Thread(() -> {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    LOGGER.error(line);
                }
            } catch (IOException e) {
            }
        }).start();

        return process.waitFor() == 0;
    }

}

11.转码成功返回m3u8文件的路径,放到页面input隐藏标签中

编码成功后执行回调函数,m3u8文件赋给_data变量如下图

使用$(#页面元素id).val(值),下图是把m3u8赋值给页面的input

12.最后页面form提交,保存视频转码后m3u8的路径。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

若依微服务(ruoyi-cloud)保姆版容器编排运行

一、简介 项目gitee地址&#xff1a;https://gitee.com/y_project/RuoYi-Cloud 由于该项目运行有很多坑&#xff0c;大家可以在git克隆拷贝到本地后&#xff0c;执行下面的命令使master版本回退到本篇博客的版本&#xff1a; git reset --hard 05ca78e82fb4e074760156359d09a…

扒一扒抖音是如何做线程优化的

背景 最近在对一些大厂App进行研究学习&#xff0c;在对某音App进行研究时&#xff0c;发现其在线程方面做了一些优化工作&#xff0c;并且其解决的问题也是之前我在做线上卡顿优化时遇到的&#xff0c;因此对其具体实现方案做了深入分析。本文是对其相关源码的研究加上个人理…

人员玩手机离岗识别检测系统 yolov5

人员玩手机离岗识别检测系统根通过pythonyolov5网络模型识别算法技术&#xff0c;人员玩手机离岗识别检测算法可以对画面中人员睡岗离岗、玩手机打电话、脱岗睡岗情况进行全天候不间断进行识别检测报警提醒。Python是一种由Guido van Rossum开发的通用编程语言&#xff0c;它很…

【2023.3.18 美团校招】

文章目录1. 小美剪彩带2. 最多修改两个字符&#xff0c;生成字典序最小的回文串1. 小美剪彩带 题意&#xff1a;找出区间内不超过k种数字子数组的最大长度 使用双指针的方式&#xff0c;用哈希表来统计每个数出现次数。在双指针移动的过程中&#xff0c;动态的维护区间内不同数…

难以置信,已经有人用 ChatGPT 做 Excel 报表了?

要问2023年初科技领域什么最火&#xff0c;那自然是 ChatGPT。 ChatGPT 由人工智能研究实验室 OpenAI 于2022年11月30日推出。上线短短5天&#xff0c;用户数量已突破100万&#xff0c;在今年2月份&#xff0c;用户数量已经突破1亿。 ChatGPT 是一个超级智能聊天机器人&#…

【java】笔试强训Day4【计算糖果、进制转换】

目录 ⛳一、单选题 1.下列与队列结构有关联的是&#xff08; &#xff09; 2.类所实现接口的修饰符不能为&#xff08; &#xff09; 3.下列关于栈叙述正确的是&#xff08; &#xff09; 4.下面关于abstract关键字描述错误的是&#xff08; …

9.0 自定义SystemUI下拉状态栏和通知栏视图(十五)之悬浮通知布局

1.前言在进行9.0的系统rom产品定制化开发中&#xff0c;在9.0中针对systemui下拉状态栏和通知栏的定制UI的工作开发中&#xff0c;原生系统的下拉状态栏和通知栏的视图UI在产品开发中会不太满足功能&#xff0c;所以根据产品需要来自定义SystemUI的下拉状态栏和通知栏功能&…

【jenkins部署】一文弄懂自动打包部署(前后台)

这里写目录标题序言软件安装jdkmaven配置maven阿里镜像以及本地库位置git安装安装jenkins插件安装环境配置创建项目配置gitee生成gitee WebHookmaven打包验证是否打包成功连接远程服务器并重启服务远程服务器生成私钥配置ssh项目配置ssh脚本vue项目打包nodejs安装下载配置环境变…

【C++进阶】位图和布隆过滤器

文章目录位图位图概念位图使用场景位图的结构构造setresettest完整代码布隆过滤器布隆过滤器概念布隆过滤器结构构造setresettest完整版代码位图 位图概念 所谓位图&#xff0c;就是用每一位来存放某种状态&#xff0c;适用于海量数据&#xff0c;数据无重复的场景。通常是用…

智能火焰与烟雾检测系统(Python+YOLOv5深度学习模型+清新界面)

摘要&#xff1a;智能火焰与烟雾检测系统用于智能日常火灾检测报警&#xff0c;利用摄像头画面实时识别火焰与烟雾&#xff0c;另外支持图片、视频火焰检测并进行结果可视化。本文详细介绍基于智能火焰与烟雾检测系统&#xff0c;在介绍算法原理的同时&#xff0c;给出Python的…

【数据结构与算法】设计循环队列

文章目录&#x1f451;前言如何设计循环队列设计循环队列整体的代码&#x1f4ef;写在最后&#x1f451;前言 &#x1f6a9;前面我们 用队列实现了一个栈 &#xff0c;用栈实现了一个队列 &#xff0c;相信大家随随便便轻松拿捏&#xff0c;而本章将带大家上点难度&#xff0c;…

【Linux】SIGCHLD信号

文章目录SIGCHLD信号SIGCHLD信号 回忆: 为了避免出现僵尸进程,父进程需要使用wait或waitpid函数等待子进程结束,父进程可以阻塞等待子进程结束,也可以非阻塞地查询的是否有子进程结束等待清理,即轮询的方式 如果采用阻塞等待:父进程阻塞就不能处理自己的工作了如果采用非阻塞等…

Python日志logging实战教程

一、什么是日志 在《网络安全之认识日志采集分析审计系统》中我们认识了日志。日志数据的核心就是日志消息或日志&#xff0c;日志消息是计算机系统、设备、软件等在某种刺激下反应生成的东西。 日志数据&#xff08;log data&#xff09;就是一条日志消息的内在含义&#xf…

第十四届蓝桥杯第三期模拟赛 【python】

第十四届蓝桥杯第三期模拟赛 【python】 文章目录第十四届蓝桥杯第三期模拟赛 【python】✨最小的十六进制&#xff08;python的16进制&#xff09;❓️问题描述答案提交&#x1f9e0;思路&#x1f5a5;︎参考答案✨Excel的列&#xff08;进制转化&#xff09;❓️问题描述答案…

串口通信(STM32演示实现)

目录 一、串行通信的概念 二、寄存器 2.1控制寄存器USART_CR1 2.2控制寄存器USART_CR2​编辑 2.3串口寄存器USART_BRR 2.4 USART_ISR 2.5USART_TDR 2.6USART_RDR​编辑 三、实现串口数据的收发 一、串行通信的概念 u通信&#xff0c;最少要有两个对象&#xff0c;一个收…

强化学习、监督学习、无监督学习是什么

1 强化学习 1.1 定义 强化学习是机器学习学习方式的一种&#xff0c;是让计算机实现从一开始完全随机的进行操作&#xff0c;通过不断试错的方式去总结出每一步的最佳行为决策&#xff0c;基于环境给予的反馈&#xff0c;去调整自己的行为决策&#xff0c;从而对未来的行为给…

什么是推挽输出,开漏输出?

这篇文章是看B站“工科男孙老师”这个视频的笔记推挽 开漏 高阻 这都是谁想出来的词&#xff1f;&#xff1f; 我觉得讲的很好&#xff0c;做一下笔记 1.什么是IO输出三态 一共有&#xff1a;高电平, 低电平&#xff0c;浮空/高阻态 三种IO态 2.推挽输出 推挽输出能够表示高、…

短链接是怎么设计的?带你入门

文章目录前言一、短链1、原理1.1 短链生成原理1.2 短链跳转原理&#xff1a;2、设计&#xff1a;2.1 短链需求2.2 考虑的问题&#xff1f;二、实践案例1、设计表&#xff1a;2、生成短链&#xff1a;前言 说到 URL 你肯定不陌生&#xff0c;浏览器输入一段 URL&#xff0c;立马…

QMessageBox手动添加按钮并绑定按钮的信号

视频展示效果&#xff08;结合代码看效果更佳哦&#xff0c;代码在最下面&#xff09;&#xff1a; QMessageBox手动添加有重试效果的按钮效果图&#xff1a; 点击详细文本之后展开如下图&#xff1a; 图标可选&#xff1a; QMessageBox::Critical错误图标QMessageBox::NoIco…

第二十一天 数据库开发-MySQL

目录 数据库开发-MySQL 前言 1. MySQL概述 1.1 安装 1.2 数据模型 1.3 SQL介绍 1.4 项目开发流程 2. 数据库设计-DDL 2.1 数据库操作 2.2 图形化工具 2.3 表操作 3. 数据库操作-DML 3.1 增加(insert) 3.2 修改(update) 3.3 删除(delete) 数据库开发-MySQL 前言 …