😎 知识点概览

为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。

本章节为【学成在线】项目的 day14 的内容

  • 视频上传成功后通过 RabbitMQ 进行消息发送,再通过 视频处理服务 对视频进行格式转换,以及 m3u8 视频文件的生成。
  • 实现媒资信息的浏览
  • Vue 跨组件间的通讯实战,实现课程计划与已上传的媒资文件的关联

目录

知识点结合实战应用会更有意义,所以这里不再对单个知识点进行拆分成单个笔记,内容会比较多,可以根据目录进行按需查阅。

文章目录

  • 😎 知识点概览
  • 目录
  • 一、视频处理
    • 0x01 需求分析
    • 0x02 视频处理开发
      • 视频处理工程创建
      • 视频处理技术方案
      • 视频处理实现
        • 1、确定消息格式
        • 2、处理流程
        • 3、数据模型
        • 4、视频处理生成 MP4
        • 5、视频处理生成 m3u8
    • 0x03 发送视频处理消息
      • 配置RabbitMQ
      • 配置Service
    • 0x04 视频处理测试
    • 0x05 视频处理并发设置
    • 0x06 完整代码
      • RabbitMQConfig
      • MediaProcessTask
      • MediaUploadServiceImpl
  • 二、我的媒资
    • 0x01 需求分析
    • 0x02 API
    • 0x03 服务端开发
      • Dao
      • Service
      • Controller
      • 接口测试
    • 0x04 前端开发
      • API方法
      • 页面
  • 三、媒资与课程计划关联
    • 0x01 需求分析
    • 0x02 选择视频
      • Vue 父子组件通信
      • 父组件:修改课程计划
      • 子组件:我的媒资查询
      • 页面效果
      • 全部代码
        • course_plan.vue
        • media_list.vue
    • 0x03 保存视频信息
      • 需求分析
      • 数据模型
      • API接口
      • 服务端开发
        • 1、Controller
        • 2、Dao
        • 3、Service
      • 前端开发
        • 1、API方法
        • 2、API调用
        • 3、测试
    • 0x04 查询视频信息
      • 需求分析
      • Dao
      • 页面查询视频
  • 😁 认识作者

一、视频处理

0x01 需求分析

原始视频通常需要经过编码处理,生成 m3u8ts 文件方可基于 HLS 协议播放视频。通常用户上传原始视频,系统自动处理成标准格式,系统对用户上传的视频自动编码、转换,最终生成m3u8 文件和 ts 文件,处理流程如下:

1、用户上传视频成功

2、系统对上传成功的视频自动开始编码处理

3、用户查看视频处理结果,没有处理成功的视频用户可在管理界面再次触发处理

4、视频处理完成将视频地址及处理结果保存到数据库

视频处理流程如下:

视频处理进程的任务是接收视频处理消息进行视频处理,业务流程如下:

1、监听 MQ,接收视频处理消息。

2、进行视频处理。

3、向数据库写入视频处理结果。

视频处理进程属于媒资管理系统的一部分,考虑提高系统的扩展性,将视频处理单独定义视频处理工程。

0x02 视频处理开发

视频处理工程创建

1、导入“资料” 下的视频处理工程:xc-service-manage-media-processor

2、RabbitMQ 配置

使用 rabbitMQrouting 交换机模式,视频处理程序监听视频处理队列,如下图:

RabbitMQ配置如下:

@Configuration
public class RabbitMQConfig {public static final String EX_MEDIA_PROCESSTASK = "ex_media_processor";//视频处理队列@Value("${xc‐service‐manage‐media.mq.queue‐media‐video‐processor}")public String queue_media_video_processtask;//视频处理路由@Value("${xc‐service‐manage‐media.mq.routingkey‐media‐video}")public String routingkey_media_video;/**
* 交换机配置
* @return the exchange
*/@Bean(EX_MEDIA_PROCESSTASK)public Exchange EX_MEDIA_VIDEOTASK() {return ExchangeBuilder.directExchange(EX_MEDIA_PROCESSTASK).durable(true).build();} //声明队列@Bean("queue_media_video_processtask")public Queue QUEUE_PROCESSTASK() {Queue queue = new Queue(queue_media_video_processtask,true,false,true);return queue;} /**
* 绑定队列到交换机 .
* @param queue the queue
* @param exchange the exchange
* @return the binding
*/@Beanpublic Binding binding_queue_media_processtask(@Qualifier("queue_media_video_processtask")Queue queue, @Qualifier(EX_MEDIA_PROCESSTASK) Exchange exchange) {return BindingBuilder.bind(queue).to(exchange).with(routingkey_media_video).noargs();}
}

application.yml 中配置队列名称及 routingkey

xc‐service‐manage‐media:mq:queue‐media‐video‐processor: queue_media_video_processorroutingkey‐media‐video: routingkey_media_video

视频处理技术方案

如何通过程序进行视频处理?

ffmpeg 是一个可行的视频处理程序,可以通过 Java 调用 ffmpeg.exe 完成视频处理。

java 中可以使用 Runtime 类和 Process Builder 类两种方式来执行外部程序,工作中至少掌握一种。

本项目使用 Process Builder 的方式来调用 ffmpeg 完成视频处理。

关于 Process Builder 的测试如下:

//测试ping命令
@Test
public void testProcessBuilder() throws IOException {//创建ProcessBuilder对象ProcessBuilder processBuilder = new ProcessBuilder();//设置执行的第三方程序(命令)List<String> cmds = new ArrayList<>();cmds.add("ping");cmds.add("127.0.0.1");processBuilder.command(cmds);//合并标准输入流和错误输出processBuilder.redirectErrorStream(true);Process start = processBuilder.start();//获取输入流InputStream inputStream = start.getInputStream();//将输入流转换为字符输入流InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "gbk");//获取流的数据int len = -1;//数据缓冲区char[] cache = new char[1024];StringBuffer stringBuffer = new StringBuffer();while ((len = inputStreamReader.read(cache)) != -1) {//获取缓冲区内的数据String outStr = new String(cache, 0, len);System.out.println(outStr);stringBuffer.append(outStr);}inputStream.close();
}//测试使用工具类将avi转成mp4
@Test
public void testProcessMp4() throws IOException {ProcessBuilder processBuilder = new ProcessBuilder();//定义命令内容List<String> command = new ArrayList<>();command.add("D:/soft/ffmpeg-20200315-c467328-win64-static/bin/ffmpeg.exe");command.add("-i");command.add("E:/temp/1.avi");command.add("-y"); //覆盖输出文件command.add("-c:v");command.add("libx264");command.add("-s");command.add("1280x720");command.add("-pix_fmt");command.add("yuv420p");command.add("-b:a");command.add("63k");command.add("-b:v");command.add("753k");command.add("-r");command.add("18");command.add("E:/temp/1.mp4");processBuilder.command(command);//将标准输入流和错误输入流合并,通过标准输入流读取信息processBuilder.redirectErrorStream(true);Process start = processBuilder.start();InputStream inputStream = start.getInputStream();InputStreamReader streamReader = new InputStreamReader(inputStream, "gbk");//获取输入流数据int len = -1;//数据缓冲区char[] cache = new char[1024];StringBuffer stringBuffer = new StringBuffer();while ((len=streamReader.read(cache)) != -1){//从缓冲区获取数据String out = new String(cache, 0, len);System.out.println(out);stringBuffer.append(out);}inputStream.close();
}

上边的代码已经封装成工具类,参见:

上边的工具类中:

Mp4VideoUtil.java 完成 avimp4

HlsVideoUtil.java 完成 mp4hls

分别测试每个工具类的使用方法。

public static void main(String[] args) throws IOException {String ffmpeg_path = "D:/soft/ffmpeg-20200315-c467328-win64-static/bin/ffmpeg.exe";//ffmpeg的安装位置String video_path = "E:\\temp\\1.avi";String mp4_name = "2.mp4";String mp4_path = "E:\\temp\\";Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path);String s = videoUtil.generateMp4();System.out.println(s);
}

视频处理实现

1、确定消息格式

MQ 消息统一采用 json 格式,视频处理生产方会向 MQ 发送如下消息,视频处理消费方接收此消息后进行视频处
理:

{"mediaId":XXX}

2、处理流程

1)接收视频处理消息

2)判断媒体文件是否需要处理(本视频处理程序目前只接收avi 视频的处理)当前只有 avi 文件需要处理,其它文件需要更新处理状态为 “无需处理”。

3)处理前初始化处理状态为 “未处理

4)处理失败需要在数据库记录处理日志,及处理状态为 “处理失败

5)处理成功记录处理状态为 “处理成功

3、数据模型

MediaFile 类中添加 mediaFileProcess_m3u8 属性记录 ts 文件列表,代码如下 :

//处理状态
private String processStatus;
//hls处理
private MediaFileProcess_m3u8 mediaFileProcess_m3u8;

MediaFileProcess_m3u8 如下

@Data
@ToString
public class MediaFileProcess_m3u8 extends MediaFileProcess {
//ts列表
private List<String> tslist;
}

4、视频处理生成 MP4

1)创建 dao

视频处理结果需要保存到媒资数据库,创建 dao 如下:

public interface MediaFileRepository extends MongoRepository<MediaFile,String> {}

2)在 application.yml 中配置 ffmpeg 的位置及视频目录的根目录

xc‐service‐manage‐media:video‐location: F:/develop/video/ffmpeg‐path: D:/Program Files/ffmpeg‐20180227‐fa0c9d6‐win64‐static/bin/ffmpeg.exe

3)处理任务类

mq 包下创建 MediaProcessTask 类,此类负责监听视频处理队列,并进行视频处理。

整个视频处理内容较多,这里分两部分实现:生成 Mp4 和生成 m3u8,下边代码实现了生成 mp4

@Component
public class MediaProcessTask {//日志对象private static final Logger LOGGER = LoggerFactory.getLogger(MediaProcessTask.class);//ffmpeg绝对路径@Value("${xc-service-manage-media.ffmpeg-path}")String ffmpeg_path;//上传文件根目录@Value("${xc-service-manage-media.video-location}")String serverPath;@AutowiredMediaFileRepository mediaFileRepository;@RabbitListener(queues = "${xc-service-manage-media.mq.queue-media-video-processor}")public void receiveMediaProcessTask(String msg){//将接收到的消息转换为json数据Map msgMap = JSON.parseObject(msg);LOGGER.info("receive media process task msg :{} ",msgMap);//解析消息//媒资文件idString mediaId = (String) msgMap.get("mediaId");//获取媒资文件信息Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);if(!byId.isPresent()){return;}MediaFile mediaFile = byId.get();//媒资文件类型String fileType = mediaFile.getFileType();//目前只处理avi文件if(fileType == null || !fileType.equals("avi")){mediaFile.setProcessStatus("303004"); // 处理状态为无需处理mediaFileRepository.save(mediaFile);}else{mediaFile.setProcessStatus("303001"); //处理状态为未处理}//生成MP4String videoPath = serverPath + mediaFile.getFilePath() + mediaFile.getFileName();String mp4Name = mediaFile.getFileId() + ".mp4";String mp4FloderPath = serverPath  + mediaFile.getFilePath();Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);String result = mp4VideoUtil.generateMp4();if(result == null || !result.equals("success")){//操作失败写入处理日志mediaFile.setProcessStatus("303003");//处理状态为处理失败MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();mediaFileProcess_m3u8.setErrormsg(result);mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);mediaFileRepository.save(mediaFile);return;}//生成m3u8...}
}

说明:

1、原始视频转成 mp4 如何判断转换成功?

根据视频时长来判断,取原视频和转换成功视频的时长(时分秒),如果相等则相同。

5、视频处理生成 m3u8

下边是完整的视频处理任务类代码,包括了生成 m3u8 及生成 mp4 的代码

package com.xuecheng.manage_media_process.mq;import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.MediaFileProcess_m3u8;
import com.xuecheng.framework.utils.HlsVideoUtil;
import com.xuecheng.framework.utils.Mp4VideoUtil;
import com.xuecheng.manage_media_process.dao.MediaFileRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.Map;
import java.util.Optional;@Component
public class MediaProcessTask {//日志对象private static final Logger LOGGER = LoggerFactory.getLogger(MediaProcessTask.class);//ffmpeg绝对路径@Value("${xc-service-manage-media.ffmpeg-path}")String ffmpeg_path;//上传文件根目录@Value("${xc-service-manage-media.video-location}")String serverPath;@AutowiredMediaFileRepository mediaFileRepository;@RabbitListener(queues = "${xc-service-manage-media.mq.queue-media-video-processor}")public void receiveMediaProcessTask(String msg){//将接收到的消息转换为json数据Map msgMap = JSON.parseObject(msg);LOGGER.info("receive media process task msg :{} ",msgMap);//解析消息//媒资文件idString mediaId = (String) msgMap.get("mediaId");//获取媒资文件信息Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);if(!byId.isPresent()){return;}MediaFile mediaFile = byId.get();//媒资文件类型String fileType = mediaFile.getFileType();//目前只处理avi文件if(fileType == null || !fileType.equals("avi")){mediaFile.setProcessStatus("303004"); // 处理状态为无需处理mediaFileRepository.save(mediaFile);}else{mediaFile.setProcessStatus("303001"); //处理状态为未处理}//生成MP4String videoPath = serverPath + mediaFile.getFilePath() + mediaFile.getFileName();String mp4Name = mediaFile.getFileId() + ".mp4";String mp4FloderPath = serverPath  + mediaFile.getFilePath();Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);String result = mp4VideoUtil.generateMp4();if(result == null || !result.equals("success")){//操作失败写入处理日志mediaFile.setProcessStatus("303003");//处理状态为处理失败MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();mediaFileProcess_m3u8.setErrormsg(result);mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);mediaFileRepository.save(mediaFile);return;}//生成m3u8列表//生成m3u8String mp4VideoPath = serverPath + mediaFile.getFilePath()+ mp4Name;//此地址为mp4的地址String m3u8Name = mediaFile.getFileId()+".m3u8";String m3u8FolderPath = serverPath + mediaFile.getFilePath()+"hls/";//调用工具类进行生成m3u8HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, mp4VideoPath, m3u8Name, m3u8FolderPath);String m3u8Result = hlsVideoUtil.generateM3u8();if(m3u8Result==null || !m3u8Result.equals("success")){//操作失败写入处理日志mediaFile.setProcessStatus("303003");//处理状态为处理失败MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();mediaFileProcess_m3u8.setErrormsg(m3u8Result);mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);mediaFileRepository.save(mediaFile);return ;}//获取m3u8列表List<String> ts_list = hlsVideoUtil.get_ts_list();//更新处理状态为成功mediaFile.setProcessStatus("303002");//处理状态为处理成功MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();mediaFileProcess_m3u8.setTslist(ts_list);mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);//m3u8文件urlmediaFile.setFileUrl(mediaFile.getFilePath()+"hls/"+m3u8Name);mediaFileRepository.save(mediaFile);}
}

0x03 发送视频处理消息

当视频上传成功后向 MQ 发送视频 处理消息。

修改媒资管理服务的文件上传代码,当文件上传成功向 MQ 发送视频处理消息。

配置RabbitMQ

1、将media-processor 工程下的 RabbitmqConfig 配置类拷贝到 media 工程下。

2、在 media 工程下配置 mq 队列等信息

修改 application.yml

xc-service-manage-media:mq:queue-media-video-processor: queue_media_video_processorroutingkey-media-video: routingkey_media_video

配置Service

在文件合并方法中添加向 mq 发送视频处理消息的代码:

//视频处理路由
@Value("${xc-service-manage-media.mq.routingkey-media-video}")
public String routingkey_media_video;@Autowired
RabbitTemplate rabbitTemplate;//向MQ发送视频处理消息
private ResponseResult sendProcessVideoMsg(String mediaId){Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);if(!optional.isPresent()){return new ResponseResult(CommonCode.FAIL);}MediaFile mediaFile = optional.get();//发送视频处理消息Map<String,String> msgMap = new HashMap<>();msgMap.put("mediaId",mediaId);//发送的消息String msg = JSON.toJSONString(msgMap);try {this.rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK,routingkey_media_video,msg);LOGGER.info("send media process task msg:{}",msg);}catch (Exception e){e.printStackTrace();LOGGER.info("send media process task error,msg is:{},error:{}",msg,e.getMessage());return new ResponseResult(CommonCode.FAIL);}return new ResponseResult(CommonCode.SUCCESS);
}

mergechunks 方法最后调用 sendProcessVideo 方法。

......
//状态为上传成功
mediaFile.setFileStatus("301002");
mediaFileRepository.save(mediaFile);
String mediaId = mediaFile.getFileId();
//向MQ发送视频处理消息
sendProcessVideoMsg(mediaId);
......

0x04 视频处理测试

测试流程:

1、上传avi文件

2、观察日志是否发送消息

3、观察视频处理进程是否接收到消息进行处理

4、观察 mp4 文件是否生成

5、观察 m3u8ts 文件是否生成

0x05 视频处理并发设置

代码中使用 @RabbitListener 注解指定消费方法,默认情况是单线程监听队列,可以观察当队列有多个任务时消费端每次只消费一个消息,单线程处理消息容易引起消息处理缓慢,消息堆积,不能最大利用硬件资源。

可以配置 mq 的容器工厂参数,增加并发处理数量即可实现多线程处理监听队列,实现多线程处理消息。

1、在 RabbitmqConfig.java 中添加容器工厂配置:

/*** 多线程处理消息* @param configurer* @param connectionFactory* @return*/
@Bean("customContainerFactory")
public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactoryconnectionFactory) {SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();factory.setConcurrentConsumers(DEFAULT_CONCURRENT);factory.setMaxConcurrentConsumers(DEFAULT_CONCURRENT);configurer.configure(factory,connectionFactory);return factory;
}

2、在 @RabbitListener 注解中指定容器工厂

//视频处理方法
@RabbitListener(queues = {"${xc‐service‐manage‐media.mq.queue‐media‐video‐processor}"},
containerFactory="customContainerFactory")

再次测试当队列有多个任务时消费端的并发处理能力。

0x06 完整代码

RabbitMQConfig

package com.xuecheng.manage_media_process.config;import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RabbitMQConfig {public static final String EX_MEDIA_PROCESSTASK = "ex_media_processor";//视频处理队列@Value("${xc-service-manage-media.mq.queue-media-video-processor}")public  String queue_media_video_processtask;//视频处理路由@Value("${xc-service-manage-media.mq.routingkey-media-video}")public  String routingkey_media_video;//消费者并发数量public static final int DEFAULT_CONCURRENT = 10;/*** 交换机配置* @return the exchange*/@Bean(EX_MEDIA_PROCESSTASK)public Exchange EX_MEDIA_VIDEOTASK() {return ExchangeBuilder.directExchange(EX_MEDIA_PROCESSTASK).durable(true).build();}//声明队列@Bean("queue_media_video_processtask")public Queue QUEUE_PROCESSTASK() {Queue queue = new Queue(queue_media_video_processtask,true,false,true);return queue;}/*** 绑定队列到交换机 .* @param queue    the queue* @param exchange the exchange* @return the binding*/@Beanpublic Binding binding_queue_media_processtask(@Qualifier("queue_media_video_processtask") Queue queue, @Qualifier(EX_MEDIA_PROCESSTASK) Exchange exchange) {return BindingBuilder.bind(queue).to(exchange).with(routingkey_media_video).noargs();}/*** 多线程处理消息* @param configurer* @param connectionFactory* @return*/@Bean("customContainerFactory")public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactoryconnectionFactory) {SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();factory.setConcurrentConsumers(DEFAULT_CONCURRENT);factory.setMaxConcurrentConsumers(DEFAULT_CONCURRENT);configurer.configure(factory,connectionFactory);return factory;}
}

MediaProcessTask

package com.xuecheng.manage_media_process.mq;import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.MediaFileProcess_m3u8;
import com.xuecheng.framework.utils.HlsVideoUtil;
import com.xuecheng.framework.utils.Mp4VideoUtil;
import com.xuecheng.manage_media_process.dao.MediaFileRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.Map;
import java.util.Optional;@Component
public class MediaProcessTask {//日志对象private static final Logger LOGGER = LoggerFactory.getLogger(MediaProcessTask.class);//ffmpeg绝对路径@Value("${xc-service-manage-media.ffmpeg-path}")String ffmpeg_path;//上传文件根目录@Value("${xc-service-manage-media.video-location}")String serverPath;@AutowiredMediaFileRepository mediaFileRepository;@RabbitListener(queues = "${xc-service-manage-media.mq.queue-media-video-processor}" , containerFactory="customContainerFactory")public void receiveMediaProcessTask(String msg){//将接收到的消息转换为json数据Map msgMap = JSON.parseObject(msg);LOGGER.info("receive media process task msg :{} ",msgMap);//解析消息//媒资文件idString mediaId = (String) msgMap.get("mediaId");//获取媒资文件信息Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);if(!byId.isPresent()){return;}MediaFile mediaFile = byId.get();//媒资文件类型String fileType = mediaFile.getFileType();//目前只处理avi文件if(fileType == null || !fileType.equals("avi")){mediaFile.setProcessStatus("303004"); // 处理状态为无需处理mediaFileRepository.save(mediaFile);}else{mediaFile.setProcessStatus("303001"); //处理状态为未处理}//生成MP4String videoPath = serverPath + mediaFile.getFilePath() + mediaFile.getFileName();String mp4Name = mediaFile.getFileId() + ".mp4";String mp4FloderPath = serverPath  + mediaFile.getFilePath();Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);String result = mp4VideoUtil.generateMp4();if(result == null || !result.equals("success")){//操作失败写入处理日志mediaFile.setProcessStatus("303003");//处理状态为处理失败MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();mediaFileProcess_m3u8.setErrormsg(result);mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);mediaFileRepository.save(mediaFile);return;}//生成m3u8列表//生成m3u8String mp4VideoPath = serverPath + mediaFile.getFilePath()+ mp4Name;//此地址为mp4的地址String m3u8Name = mediaFile.getFileId()+".m3u8";String m3u8FolderPath = serverPath + mediaFile.getFilePath()+"hls/";//调用工具类进行生成m3u8HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, mp4VideoPath, m3u8Name, m3u8FolderPath);String m3u8Result = hlsVideoUtil.generateM3u8();if(m3u8Result==null || !m3u8Result.equals("success")){//操作失败写入处理日志mediaFile.setProcessStatus("303003");//处理状态为处理失败MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();mediaFileProcess_m3u8.setErrormsg(m3u8Result);mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);mediaFileRepository.save(mediaFile);return ;}//获取m3u8列表List<String> ts_list = hlsVideoUtil.get_ts_list();//更新处理状态为成功mediaFile.setProcessStatus("303002");//处理状态为处理成功MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();mediaFileProcess_m3u8.setTslist(ts_list);mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);//m3u8文件urlmediaFile.setFileUrl(mediaFile.getFilePath()+"hls/"+m3u8Name);mediaFileRepository.save(mediaFile);}
}

MediaUploadServiceImpl

package com.xuecheng.manage_media.service.impl;import com.alibaba.fastjson.JSON;
import com.netflix.discovery.converters.Auto;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.domain.media.response.MediaCode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.config.RabbitMQConfig;
import com.xuecheng.manage_media.controller.MediaUploadController;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import com.xuecheng.manage_media.service.MediaUploadService;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;import javax.jws.Oneway;
import java.io.*;
import java.util.*;@Service
class MediaUploadServiceImpl implements MediaUploadService {private final static Logger LOGGER = LoggerFactory.getLogger(MediaUploadController.class);@AutowiredMediaFileRepository mediaFileRepository;//上传文件根目录@Value("${xc-service-manage-media.upload-location}")String uploadPath;/*** 检查文件信息是否已经存在本地以及mongodb内,其中一者不存在则重新注册* @param fileMd5 文件md5值* @param fileExt 文件扩展名* @return 文件路径*/@Overridepublic ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {//1.检查文件在磁盘上是否存在//2.检查文件信息在mongodb上是否存在//获取文件所属目录以及文件路径String fileFloderPath = this.getFileFloderPath(fileMd5);String filePath = this.getFileFullPath(fileMd5, fileExt);File file = new File(filePath);boolean exists = file.exists();//查询mongodb上的文件信息Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5);if(exists && optional.isPresent()){ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);}//其中一者不存在则重新注册文件信息File fileFloder = new File(fileFloderPath);if(!fileFloder.exists()){//创建文件目录fileFloder.mkdirs();}return new ResponseResult(CommonCode.SUCCESS);}/*** 检查文件块是否存在* @param fileMd5 文件md5* @param chunk 块编号* @param chunkSize 块大小* @return CheckChunkResult*/@Overridepublic CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {//获取文件块路径String chunkFloder = this.getChunkFloderPath(fileMd5);File chunkFile = new File(chunkFloder + chunk);if(chunkFile.exists()){return new CheckChunkResult(CommonCode.SUCCESS, true);}return new CheckChunkResult(CommonCode.SUCCESS, false);}/*** 上传分块文件* @param file 上传的文件* @param chunk 分块号* @param fileMd5 文件MD5* @return*/@Overridepublic ResponseResult uploadChunk(MultipartFile file, Integer chunk, String fileMd5) {//获取分块文件所属目录String chunkFloder = this.getChunkFloderPath(fileMd5);InputStream inputStream = null;FileOutputStream fileOutputStream = null;try {inputStream = file.getInputStream();fileOutputStream = new FileOutputStream(chunkFloder + chunk);IOUtils.copy(inputStream,fileOutputStream);} catch (IOException e) {//文件保存失败e.printStackTrace();LOGGER.error("upload chunk file fail:{}",e.getMessage());ExceptionCast.cast(MediaCode.CHUNK_FILE_UPLOAD_FAIL);}return new ResponseResult(CommonCode.SUCCESS);}/*** 合并文件块信息* @param fileMd5 文件MD5* @param fileName 文件名称* @param fileSize 文件大小* @param mimetype 文件类型* @param fileExt 文件拓展名* @return ResponseResult*/@Overridepublic ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {//获取文件块路径String chunkFloderPath = getChunkFloderPath(fileMd5);//合并文件路径String fileFullPath = this.getFileFullPath(fileMd5, fileExt);File mergeFile = new File(fileFullPath);//创建合并文件,如果存在则先删除再创建if(mergeFile.exists()){mergeFile.delete();}boolean newFile = false;try {newFile = mergeFile.createNewFile();} catch (IOException e) {e.printStackTrace();LOGGER.error("mergechunks..create mergeFile fail:{}",e.getMessage());}if(!newFile){//文件创建失败ExceptionCast.cast(MediaCode.MERGE_FILE_CREATEFAIL);}//获取块文件列表,此列表是已经排序好的List<File> chunkFiles = this.getChunkFiles(chunkFloderPath);//合并文件mergeFile = this.mergeFile(mergeFile, chunkFiles);if(mergeFile == null){ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);}//校验文件boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);if(!checkResult){ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);}//将文件信息保存到数据库MediaFile mediaFile = new MediaFile();mediaFile.setFileId(fileMd5);mediaFile.setFileName(fileMd5+"."+fileExt);mediaFile.setFileOriginalName(fileName);//文件路径保存相对路径String filePath = this.getFilePath(fileMd5,fileExt);mediaFile.setFilePath(this.getFilePath(fileMd5,fileExt));mediaFile.setFileUrl(filePath + fileName + "." + fileExt);mediaFile.setFileSize(fileSize);mediaFile.setUploadTime(new Date());mediaFile.setMimeType(mimetype);mediaFile.setFileType(fileExt);//状态为上传成功mediaFile.setFileStatus("301002");MediaFile save = mediaFileRepository.save(mediaFile);//向MQ发送视频处理消息this.sendProcessVideoMsg(fileMd5);return new ResponseResult(CommonCode.SUCCESS);}//视频处理路由@Value("${xc-service-manage-media.mq.routingkey-media-video}")public String routingkey_media_video;@AutowiredRabbitTemplate rabbitTemplate;//向MQ发送视频处理消息private ResponseResult sendProcessVideoMsg(String mediaId){Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);if(!optional.isPresent()){return new ResponseResult(CommonCode.FAIL);}MediaFile mediaFile = optional.get();//发送视频处理消息Map<String,String> msgMap = new HashMap<>();msgMap.put("mediaId",mediaId);//发送的消息String msg = JSON.toJSONString(msgMap);try {this.rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK,routingkey_media_video,msg);LOGGER.info("send media process task msg:{}",msg);}catch (Exception e){e.printStackTrace();LOGGER.info("send media process task error,msg is:{},error:{}",msg,e.getMessage());return new ResponseResult(CommonCode.FAIL);}return new ResponseResult(CommonCode.SUCCESS);}//校验文件MD5private boolean checkFileMd5(File mergeFile, String fileMd5) {if(mergeFile == null || StringUtils.isEmpty(fileMd5)){return false;}//进行md5校验try {FileInputStream fileInputStream = new FileInputStream(mergeFile);//得到文件的MD5String md5Hex = DigestUtils.md5Hex(fileInputStream);//比较两个MD5值if(md5Hex.equalsIgnoreCase(fileMd5)){return true;}} catch (FileNotFoundException e) {e.printStackTrace();LOGGER.error("未找到该文件 {}",e.getMessage());} catch (IOException e) {e.printStackTrace();}return false;}//合并文件private File mergeFile(File mergeFile, List<File> chunkFiles) {try {//创建写文件对象RandomAccessFile raf_write = new RandomAccessFile(mergeFile,"rw");//遍历分块文件开始合并//读取文件缓冲区byte[] b = new byte[1024];for(File chunkFile:chunkFiles){RandomAccessFile raf_read = new RandomAccessFile(chunkFile,"r");int len = -1;//读取分块文件while((len = raf_read.read(b))!= -1){//向合并文件中写数据raf_write.write(b,0,len);} raf_read.close();} raf_write.close();} catch (Exception e) {e.printStackTrace();LOGGER.error("merge file error:{}",e.getMessage());return null;} return mergeFile;}//获取块文件列表private List<File> getChunkFiles(String chunkFloderPath) {//块文件目录File chunkFolder = new File(chunkFloderPath);//分块文件列表File[] fileArray = chunkFolder.listFiles();//将分块列表转为集合,便于排序ArrayList<File> fileList = new ArrayList<>(Arrays.asList(fileArray));//从小到大排序,按名称升序Collections.sort(fileList, new Comparator<File>() {@Overridepublic int compare(File o1, File o2) {//比较两个文件的名称if (Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())) {return -1;}return 1;}});return fileList;}//获取文件块路径private String getChunkFloderPath(String fileMd5) {//获取分块文件所属目录String fileFloderPath = this.getFileFloderPath(fileMd5);String chunkFloder = fileFloderPath + "chunk/";File fileChunkFloder = new File(chunkFloder);//如果分块所属目录不存在则创建if(!fileChunkFloder.exists()){fileChunkFloder.mkdirs();}return chunkFloder;}/*** 根据文件md5得到文件的所属目录* 规则:* 一级目录:md5的第一个字符* 二级目录:md5的第二个字符* 三级目录:md5*/private String getFileFloderPath(String fileMd5){String floderPath = uploadPath + "/" + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" +fileMd5  + "/";return floderPath;}/*** 获取全文件路径* 文件名:md5+文件扩展名*/private String getFileFullPath(String fileMd5, String fileExt){String floderPath = this.getFileFloderPath(fileMd5);String filePath = floderPath + fileMd5 + "." + fileExt;return filePath;}/*** 获取文件路径* 文件名:md5+文件扩展名*/private String getFilePath(String fileMd5, String fileExt){String filePath = "/" + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/";return filePath;}
}

二、我的媒资

0x01 需求分析

通过我的媒资可以查询本教育机构拥有的媒资文件,进行文件处理、删除文件、修改文件信息等操作,具体需求如
下:

1、分页查询我的媒资文件

2、删除媒资文件

3、处理媒资文件

4、修改媒资文件信息

0x02 API

本节讲解我的媒资文件分页查询、处理媒资文件,其它功能请学员自行实现

@Api(value = "媒体文件管理",description = "媒体文件管理接口",tags = {"媒体文件管理接口"})
public interface MediaFileControllerApi {@ApiOperation("查询文件列表")public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);
}

0x03 服务端开发

Dao

public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
}

Service

定义 findList 方法实现媒资文件查询列表

package com.xuecheng.manage_media.service;import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.QueryResponseResult;public interface MediaFileService {/*** 查询媒体问价内信息* @param page 页码* @param size 每页数量* @param queryMediaFileRequest 查询条件* @return QueryResponseResult*/public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);
}

实现

package com.xuecheng.manage_media.service.impl;import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.QueryResult;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import com.xuecheng.manage_media.service.MediaFileService;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;@Service
public class MediaFileServiceImpl implements MediaFileService {private static Logger logger = LoggerFactory.getLogger(MediaFileService.class);@AutowiredMediaFileRepository mediaFileRepository;/*** 分页查询文件信息* @param page 页码* @param size 每页数量* @param queryMediaFileRequest 查询条件* @return QueryResponseResult*/@Overridepublic QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest) {MediaFile mediaFile = new MediaFile();//查询条件if(queryMediaFileRequest == null){queryMediaFileRequest = new QueryMediaFileRequest();}//查询条件匹配器ExampleMatcher exampleMatcher = ExampleMatcher.matching().withMatcher("tag", ExampleMatcher.GenericPropertyMatchers.contains()) //模糊匹配.withMatcher("fileOriginalName", ExampleMatcher.GenericPropertyMatchers.contains()) //模糊匹配文件原始名称.withMatcher("processStatus", ExampleMatcher.GenericPropertyMatchers.exact());//精确匹配//设置查询条件对象if(StringUtils.isNotEmpty(queryMediaFileRequest.getTag())){//设置标签mediaFile.setTag(queryMediaFileRequest.getTag());}if(StringUtils.isNotEmpty(queryMediaFileRequest.getFileOriginalName())){//设置文件原始名称mediaFile.setFileOriginalName(queryMediaFileRequest.getFileOriginalName());}if(StringUtils.isNotEmpty(queryMediaFileRequest.getProcessStatus())){//设置处理状态mediaFile.setProcessStatus(queryMediaFileRequest.getProcessStatus());}//定义Example实例Example<MediaFile> example = Example.of(mediaFile, exampleMatcher);//校验page和size参数的合法性,并设置默认值if(page <=0){page = 0;}else{page = page -1;}if(size <=0){size = 10;}//分页对象PageRequest pageRequest = new PageRequest(page, size);//分页查询Page<MediaFile> all = mediaFileRepository.findAll(example, pageRequest);//设置响应对象属性QueryResult<MediaFile> mediaFileQueryResult = new QueryResult<MediaFile>();mediaFileQueryResult.setList(all.getContent());mediaFileQueryResult.setTotal(all.getTotalElements());return new QueryResponseResult(CommonCode.SUCCESS,mediaFileQueryResult);}
}

Controller

@RestController
@RequestMapping("/media/file")
public class MediaFileController implements MediaFileControllerApi {@AutowiredMediaFileService mediaFileService;@GetMapping("/list/{page}/{size}")@Overridepublic QueryResponseResult findList(@PathVariable("page") int page,@PathVariable("size") int size, QueryMediaFileRequest queryMediaFileRequest) {//媒资文件信息查询return mediaFileService.findList(page,size,queryMediaFileRequest);}
}

接口测试

使用 swagger 进行接口测试

0x04 前端开发

API方法

media 模块定义api方法如下:

import http from './../../../base/api/public'
import querystring from 'querystring'
let sysConfig = require('@/../config/sysConfig')
let apiUrl = sysConfig.xcApiUrlPre;/*页面列表*/
export const media_list = (page,size,params) => {//params为json格式//使用querystring将json对象转成key/value串let querys = querystring.stringify(params)return http.requestQuickGet(apiUrl+'/media/file/list/'+page+'/'+size+'/?'+querys)
}/*发送处理消息*/
export const media_process = (id) => {return http.requestPost(apiUrl+'/media/file/process/'+id)
}

页面

media 模块创建 media_list.vue,可参考 cms系统的 page_list.vue 来编写此页面。

1、视图

<template><div><!--查询表单--><el-form :model="params">标签:<el-input v-model="params.tag" style="width:160px"></el-input>原始名称:<el-input v-model="params.fileOriginalName" style="width:160px"></el-input>处理状态:<el-select v-model="params.processStatus" placeholder="请选择处理状态"><el-optionv-for="item in processStatusList":key="item.id":label="item.name":value="item.id"></el-option></el-select><br/><el-button type="primary" v-on:click="query" size="small">查询</el-button><router-link class="mui-tab-item" :to="{path:'/upload'}"><el-button  type="primary" size="small" v-if="ischoose != true">上传文件</el-button></router-link></el-form><!--列表--><el-table :data="list" highlight-current-row v-loading="listLoading" style="width: 100%;"><el-table-column type="index" width="30"></el-table-column><el-table-column prop="fileOriginalName" label="原始文件名称" width="220"></el-table-column><el-table-column prop="fileName" label="文件名称" width="220"></el-table-column><el-table-column prop="fileUrl" label="访问url" width="260"></el-table-column><el-table-column prop="tag" label="标签" width="100"></el-table-column><el-table-column prop="fileSize" label="文件大小" width="120"></el-table-column><el-table-column prop="processStatus" label="处理状态" width="100" :formatter="formatProcessStatus"></el-table-column><el-table-column prop="uploadTime" label="创建时间" width="110" :formatter="formatCreatetime"></el-table-column><el-table-column label="开始处理" width="" v-if="ischoose != true"><template slot-scope="scope"><el-buttonsize="small" type="primary" plain @click="process(scope.row.fileId)">开始处理</el-button></template></el-table-column><el-table-column label="选择" width="80" v-if="ischoose == true"><template slot-scope="scope"><el-buttonsize="small" type="primary" plain @click="choose(scope.row)">选择</el-button></template></el-table-column></el-table><!--分页--><el-col :span="24" class="toolbar"><el-pagination background layout="prev, pager, next" @current-change="changePage" :page-size="this.params.size":total="total" :current-page="this.params.page"style="float:right;"></el-pagination></el-col></div>
</template>

2、数据对象、方法、钩子函数

<script>import * as mediaApi from '../api/media'import utilApi from '@/common/utils';export default{props: ['ischoose'],// 页面数据data(){return {params:{page:1,//页码size:10,//每页显示个数tag:'',//标签fileName:'',//文件名称processStatus:''//处理状态},listLoading:false,list:[],total:0,processStatusList:[]}},//方法methods:{formatCreatetime(row, column){var createTime = new Date(row.uploadTime);if (createTime) {return utilApi.formatDate(createTime, 'yyyy-MM-dd hh:mm:ss');}},formatProcessStatus(row,column){var processStatus = row.processStatus;if (processStatus) {if(processStatus == '303001'){return "处理中";}else if(processStatus == '303002'){return "处理成功";}else if(processStatus == '303003'){return "处理失败";}else if(processStatus == '303004'){return "无需处理";}}},choose(mediaFile){if(mediaFile.processStatus !='303002' && mediaFile.processStatus !='303004'){this.$message.error('该文件未处理,不允许选择');return ;}if(!mediaFile.fileUrl){this.$message.error('该文件的访问url为空,不允许选择');return ;}//调用父组件的choosemedia方法this.$emit('choosemedia',mediaFile.fileId,mediaFile.fileOriginalName,mediaFile.fileUrl);},changePage(page){this.params.page = page;this.query()},process (id) {
//        console.log(id)mediaApi.media_process(id).then((res)=>{console.log(res)if(res.success){this.$message.success('开始处理,请稍后查看处理结果');}else{this.$message.error('操作失败,请刷新页面重试');}})},query(){mediaApi.media_list(this.params.page,this.params.size,this.params).then((res)=>{console.log(res)this.total = res.queryResult.totalthis.list = res.queryResult.list})}},//页面初始化完成前钩子created(){//默认第一页this.params.page = Number.parseInt(this.$route.query.page||1);},//页面初始化加载前的钩子mounted() {//默认查询页面this.query()//初始化处理状态this.processStatusList = [{id:'',name:'全部'},{id:'303001',name:'处理中'},{id:'303002',name:'处理成功'},{id:'303003',name:'处理失败'},{id:'303004',name:'无需处理'}]}}
</script>

三、媒资与课程计划关联

0x01 需求分析

到目前为止,媒资管理已完成文件上传、视频处理、我的媒资功能等基本功能。其它模块已可以使用媒资管理功
能,本节要讲解课程计划在编辑时如何选择媒资文件。

操作的业务流程如下:

1、进入课程计划修改页面

2、选择视频

打开媒资文件查询窗口,找到该课程章节的视频,选择此视频。

点击 “选择媒资文件” 打开媒资文件列表

3、 选择成功后,将在课程管理数据库保存课程计划对应在的课程视频地址。

在课程管理数据库创建表 teachplan_media 存储课程计划与媒资关联信息,表结构如下:

0x02 选择视频

Vue 父子组件通信

上一章已实现了我的媒资页面,所以媒资查询窗口页面不需要再开发,将 “我的媒资页面” 作为一个组件在修改课程
计划页面中引用,如下图:

修改课程计划页面为父组件,我的媒资查询页面为子组件。

问题1:

我的媒资页面在选择媒资文件时不允许显示,比如 视频处理 按钮,该如何控制?

这时就需要父组件(修改课程计划页面)向子组件(我的媒资页面)传入一个变量,使用此变量来控制当前是否进入选择媒资文件业务,从而控制哪些元素不显示,如下图:

问题2:

在我的媒资页面选择了媒资文件,如何将选择的媒资文件信息传到父组件?

这时就需要子组件调用父组件的方法来解决此问题,如下图:

父组件:修改课程计划

本节实现功能:在课程计划页面打开我的媒资页面。

1、引入子组件

import mediaList from '@/module/media/page/media_list.vue';
export default {components:{mediaList},data() {....

2、使用子组件

在父组件的视图中使用子组件,同时传入变量 ischoose,并指定父组件的方法名为choosemedia

这里使用 el-dialog 实现弹出窗口。

<el‐dialog title="选择媒资文件" :visible.sync="mediaFormVisible"><media‐list v‐bind:ischoose="true" @choosemedia="choosemedia"></media‐list>
</el‐dialog>

3、choosemedia 方法

在父组件中定义 choosemedia 方法,接收子组件调用,参数包括:媒资文件 id、媒资文件的原始名称、媒资文件 url

choosemedia(mediaId,fileOriginalName,mediaUrl){
}

4、打开子组件窗口

1)打开子组件窗口按钮定义

<el‐button style="font‐size: 12px;" type="text" on‐click={ () => this.choosevideo(data.id) }>选择视频</el‐button>

效果如下:

2)打开子组件窗口方法

定义 querymedia 方法:

methods: {//打开查询媒资文件窗口,传入课程计划idchoosevideo(teachplanId){this.activeTeachplanId = teachplanId;this.mediaFormVisible = true;},...
}

子组件:我的媒资查询

1、定义 ischoose 变量,接收父组件传入的 ischoose

export default{props: ['ischoose'],data(){

2、父组件传的 ischoose 变量为 true 时表示当前是选择媒资文件业务,需要控制页面元素是否显示

1)ischoose=true,选择按钮显示

<el‐table‐column label="选择" width="80" v‐if="ischoose == true"><template slot‐scope="scope"><el‐button size="small" type="primary" plain @click="choose(scope.row)">选择</el‐button></template>
</el‐table‐column>

2)ischoose=false,视频处理按钮显示

<el‐table‐column label="开始处理" width="100" v‐if="ischoose != true"><template slot‐scope="scope"><el‐buttonsize="small" type="primary" plain @click="process(scope.row.fileId)">开始处理</el‐button></template>
</el‐table‐column>

3)选择媒资文件方法

用户点击“选择”按钮将向父组件传递媒资文件信息

choose(mediaFile){if(mediaFile.processStatus !='303002' && mediaFile.processStatus !='303004'){this.$message.error('该文件未处理,不允许选择');return ;}if(!mediaFile.fileUrl){this.$message.error('该文件的访问url为空,不允许选择');return ;} //调用父组件的choosemedia方法this.$emit('choosemedia',mediaFile.fileId,mediaFile.fileOriginalName);
}

页面效果

全部代码

course_plan.vue

<template><div><el-button type="primary" @click="teachplayFormVisible = true">添加课程计划</el-button><el-tree:data="teachplanList":props="defaultProps"node-key="id"default-expand-all:expand-on-click-node="false":render-content="renderContent"></el-tree><el-dialog title="添加课程计划" :visible.sync="teachplayFormVisible" ><el-form ref="teachplanForm"  :model="teachplanActive" label-width="140px" style="width:600px;" :rules="teachplanRules" ><el-form-item label="上级结点" ><el-select v-model="teachplanActive.parentid" placeholder="不填表示根结点"><el-optionv-for="item in teachplanList":key="item.id":label="item.pname":value="item.id"></el-option></el-select></el-form-item><el-form-item label="章节/课时名称" prop="pname"><el-input v-model="teachplanActive.pname" auto-complete="off"></el-input></el-form-item><el-form-item label="课程类型" ><el-radio-group v-model="teachplanActive.ptype"><el-radio class="radio" label='1'>视频</el-radio><el-radio class="radio" label='2'>文档</el-radio></el-radio-group></el-form-item><el-form-item label="学习时长(分钟)  请输入数字" ><el-input type="number" v-model="teachplanActive.timelength" auto-complete="off" ></el-input></el-form-item><el-form-item label="排序字段" ><el-input v-model="teachplanActive.orderby" auto-complete="off" ></el-input></el-form-item><el-form-item label="章节/课时介绍" prop="description"><el-input type="textarea" v-model="teachplanActive.description" ></el-input></el-form-item><el-form-item label="状态" prop="status"><el-radio-group v-model="teachplanActive.status" ><el-radio class="radio" label="0" >未发布</el-radio><el-radio class="radio" label='1'>已发布</el-radio></el-radio-group></el-form-item><el-form-item  ><el-button type="primary" v-on:click="addTeachplan">提交</el-button><el-button type="primary" v-on:click="resetForm">重置</el-button></el-form-item></el-form></el-dialog><el-dialog title="选择媒资文件" :visible.sync="mediaFormVisible"><media-list v-bind:ischoose="true" @choosemedia="choosemedia"></media-list></el-dialog></div>
</template>
<script>let id = 1000;import * as courseApi from '../../api/course';import utilApi from '../../../../common/utils';import * as systemApi from '../../../../base/api/system';import mediaList from '@/module/media/page/media_list.vue';export default {components:{mediaList},data() {return {mediaFormVisible:false,teachplayFormVisible:false,//控制添加窗口是否显示teachplanList : [{id: 1,pname: '一级 1',children: [{id: 4,pname: '二级 1-1',children: [{id: 9,pname: '三级 1-1-1'}, {id: 10,pname: '三级 1-1-2'}]}]}],defaultProps:{children: 'children',label: 'pname'},teachplanRules: {pname: [{required: true, message: '请输入课程计划名称', trigger: 'blur'}],status: [{required: true, message: '请选择状态', trigger: 'blur'}]},teachplanActive:{},teachplanId:''}},methods: {//选择视频,打开窗口choosevideo(data){//得到当前的课程计划this.teachplanId = data.id
//        alert(this.teachplanId)this.mediaFormVisible = true;//打开窗口},//保存选择的视频choosemedia(mediaId,fileOriginalName,mediaUrl){//保存视频到课程计划表中let teachplanMedia ={}teachplanMedia.mediaId =mediaId;teachplanMedia.mediaFileOriginalName =fileOriginalName;teachplanMedia.mediaUrl =mediaUrl;teachplanMedia.courseId =this.courseid;//课程计划teachplanMedia.teachplanId=this.teachplanIdcourseApi.savemedia(teachplanMedia).then(res=>{if(res.success){this.$message.success("选择视频成功")//查询课程计划this.findTeachplan()}else{this.$message.error(res.message)}})},//提交课程计划addTeachplan(){//校验表单this.$refs.teachplanForm.validate((valid) => {if (valid) {//调用api方法//将课程id设置到teachplanActivethis.teachplanActive.courseid = this.courseidcourseApi.addTeachplan(this.teachplanActive).then(res=>{if(res.success){this.$message.success("添加成功")//刷新树this.findTeachplan()}else{this.$message.error(res.message)}})}})},//重置表单resetForm(){this.teachplanActive = {}},append(data) {const newChild = { id: id++, label: 'testtest', children: [] };if (!data.children) {this.$set(data, 'children', []);}data.children.push(newChild);},edit(data){//alert(data.id);},remove(node, data) {const parent = node.parent;const children = parent.data.children || parent.data;const index = children.findIndex(d => d.id === data.id);children.splice(index, 1);},renderContent(h, { node, data, store }) {return (<span style="flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px;"><span><span>{node.label}</span></span><span><el-button style="font-size: 12px;" type="text" on-click={ () => this.choosevideo(data) }>{data.mediaFileOriginalName}&nbsp;&nbsp;&nbsp;&nbsp; 选择视频</el-button><el-button style="font-size: 12px;" type="text" on-click={ () => this.edit(data) }>修改</el-button><el-button style="font-size: 12px;" type="text" on-click={ () => this.remove(node, data) }>删除</el-button></span></span>);},findTeachplan(){this.teachplanList = []//查询课程计划courseApi.findTeachplanList(this.courseid).then(res=>{if(res && res.children){this.teachplanList = res.children;}else {this.$message.error("课程计划查询失败")console.log(res)}})}},mounted(){//课程idthis.courseid = this.$route.params.courseid;//查询课程计划this.findTeachplan()}}
</script>
<style></style>

media_list.vue

<template><div><!--查询表单--><el-form :model="params">标签:<el-input v-model="params.tag" style="width:160px"></el-input>原始名称:<el-input v-model="params.fileOriginalName" style="width:160px"></el-input>处理状态:<el-select v-model="params.processStatus" placeholder="请选择处理状态"><el-optionv-for="item in processStatusList":key="item.id":label="item.name":value="item.id"></el-option></el-select><br/><el-button type="primary" v-on:click="query" size="small">查询</el-button><router-link class="mui-tab-item" :to="{path:'/upload'}"><el-button  type="primary" size="small" v-if="ischoose != true">上传文件</el-button></router-link></el-form><!--列表--><el-table :data="list" highlight-current-row v-loading="listLoading" style="width: 100%;"><el-table-column type="index" width="30"></el-table-column><el-table-column prop="fileOriginalName" label="原始文件名称" width="220"></el-table-column><el-table-column prop="fileName" label="文件名称" width="220"></el-table-column><el-table-column prop="fileUrl" label="访问url" width="260"></el-table-column><el-table-column prop="tag" label="标签" width="100"></el-table-column><el-table-column prop="fileSize" label="文件大小" width="120"></el-table-column><el-table-column prop="processStatus" label="处理状态" width="100" :formatter="formatProcessStatus"></el-table-column><el-table-column prop="uploadTime" label="创建时间" width="110" :formatter="formatCreatetime"></el-table-column><el-table-column label="开始处理" width="" v-if="ischoose != true"><template slot-scope="scope"><el-buttonsize="small" type="primary" plain @click="process(scope.row.fileId)">开始处理</el-button></template></el-table-column><el-table-column label="选择" width="80" v-if="ischoose == true"><template slot-scope="scope"><el-buttonsize="small" type="primary" plain @click="choose(scope.row)">选择</el-button></template></el-table-column></el-table><!--分页--><el-col :span="24" class="toolbar"><el-pagination background layout="prev, pager, next" @current-change="changePage" :page-size="this.params.size":total="total" :current-page="this.params.page"style="float:right;"></el-pagination></el-col></div>
</template>
<script>import * as mediaApi from '../api/media'import utilApi from '@/common/utils';export default{props: ['ischoose'],// 页面数据data(){return {params:{page:1,//页码size:10,//每页显示个数tag:'',//标签fileName:'',//文件名称processStatus:''//处理状态},listLoading:false,list:[],total:0,processStatusList:[]}},//方法methods:{formatCreatetime(row, column){var createTime = new Date(row.uploadTime);if (createTime) {return utilApi.formatDate(createTime, 'yyyy-MM-dd hh:mm:ss');}},formatProcessStatus(row,column){var processStatus = row.processStatus;if (processStatus) {if(processStatus == '303001'){return "处理中";}else if(processStatus == '303002'){return "处理成功";}else if(processStatus == '303003'){return "处理失败";}else if(processStatus == '303004'){return "无需处理";}}},choose(mediaFile){if(mediaFile.processStatus !='303002' && mediaFile.processStatus !='303004'){this.$message.error('该文件未处理,不允许选择');return ;}if(!mediaFile.fileUrl){this.$message.error('该文件的访问url为空,不允许选择');return ;}//调用父组件的choosemedia方法this.$emit('choosemedia',mediaFile.fileId,mediaFile.fileOriginalName,mediaFile.fileUrl);},changePage(page){this.params.page = page;this.query()},process (id) {
//        console.log(id)mediaApi.media_process(id).then((res)=>{console.log(res)if(res.success){this.$message.success('开始处理,请稍后查看处理结果');}else{this.$message.error('操作失败,请刷新页面重试');}})},query(){mediaApi.media_list(this.params.page,this.params.size,this.params).then((res)=>{console.log(res)this.total = res.queryResult.totalthis.list = res.queryResult.list})}},//页面初始化完成前钩子created(){//默认第一页this.params.page = Number.parseInt(this.$route.query.page||1);},//页面初始化加载前的钩子mounted() {//默认查询页面this.query()//初始化处理状态this.processStatusList = [{id:'',name:'全部'},{id:'303001',name:'处理中'},{id:'303002',name:'处理成功'},{id:'303003',name:'处理失败'},{id:'303004',name:'无需处理'}]}}
</script>
<style></style>

0x03 保存视频信息

需求分析

用户进入课程计划页面,选择视频,将课程计划与视频信息保存在课程管理数据库中。

用户操作流程:

1、进入课程计划,点击”选择视频“,打开我的媒资查询页面

2、为课程计划选择对应的视频,选择“选择”

3、前端请求课程管理服务保存课程计划与视频信息

数据模型

在课程管理数据库创建表 teachplan_media 存储课程计划与媒资关联信息,如下:

创建 teachplanMedia 模型类:

@Data
@ToString
@Entity
@Table(name="teachplan_media")
@GenericGenerator(name = "jpa‐assigned", strategy = "assigned")
public class TeachplanMedia implements Serializable {private static final long serialVersionUID =916357110051689485L;@Id@GeneratedValue(generator = "jpa‐assigned")@Column(name="teachplan_id")private String teachplanId;@Column(name="media_id")private String mediaId;@Column(name="media_fileoriginalname")private String mediaFileOriginalName;@Column(name="media_url")private String mediaUrl;@Column(name="courseid")private String courseId;
}

API接口

此接口作为前端请求课程管理服务保存课程计划与视频信息的接口:

TeachplanControllerApi 增加接口:

@ApiOperation("保存媒资信息")
public ResponseResult saveTeachplanMedia(TeachplanMedia teachplanMedia);

服务端开发

1、Controller

TeachplanController 下添加该方法

@Override
@PostMapping("/savemedia")
public ResponseResult saveTeachplanMedia(@RequestBody TeachplanMedia teachplanMedia) {return teachplanService.saveTeachplanMedia(teachplanMedia);
}

2、Dao

创建 TeachplanMediaRepository 用于对 TeachplanMedia 的操作。

public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> {}

3、Service

//保存媒资信息
public ResponseResult saveTeachplanMedia(TeachplanMedia teachplanMedia) {if(teachplanMedia == null){ExceptionCast.cast(CommonCode.INVALIDPARAM);} //课程计划String teachplanId = teachplanMedia.getTeachplanId();//查询课程计划Optional<Teachplan> optional = teachplanRepository.findById(teachplanId);if(!optional.isPresent()){ExceptionCast.cast(CourseCode.COURSE_MEDIA_TEACHPLAN_ISNULL);} Teachplan teachplan = optional.get();//只允许为叶子结点课程计划选择视频String grade = teachplan.getGrade();if(StringUtils.isEmpty(grade) || !grade.equals("3")){ExceptionCast.cast(CourseCode.COURSE_MEDIA_TEACHPLAN_GRADEERROR);} TeachplanMedia one = null;Optional<TeachplanMedia> teachplanMediaOptional =teachplanMediaRepository.findById(teachplanId);if(!teachplanMediaOptional.isPresent()){one = new TeachplanMedia();}else{one = teachplanMediaOptional.get();} //保存媒资信息与课程计划信息one.setTeachplanId(teachplanId);one.setCourseId(teachplanMedia.getCourseId());one.setMediaFileOriginalName(teachplanMedia.getMediaFileOriginalName());one.setMediaId(teachplanMedia.getMediaId());one.setMediaUrl(teachplanMedia.getMediaUrl());teachplanMediaRepository.save(one);return new ResponseResult(CommonCode.SUCCESS);
}

前端开发

1、API方法

/*保存媒资信息*/
export const savemedia = teachplanMedia => {return http.requestPost(apiUrl+'/course/savemedia',teachplanMedia);
}

2、API调用

在课程视频方法中调用 api

choosemedia(mediaId,fileOriginalName,mediaUrl){this.mediaFormVisible = false;//保存课程计划与视频对应关系let teachplanMedia = {};teachplanMedia.teachplanId = this.activeTeachplanId;teachplanMedia.mediaId = mediaId;teachplanMedia.mediaFileOriginalName = fileOriginalName;teachplanMedia.mediaUrl = mediaUrl;teachplanMedia.courseId = this.courseid;//保存媒资信息到课程数据库courseApi.savemedia(teachplanMedia).then(res=>{if(res.success){this.$message.success("选择视频成功")}else{this.$message.error(res.message)}})
},

3、测试

1、向叶子结点课程计划保存媒资信息

操作结果:保存成功

2、向非叶子结点课程计划保存媒资信息

操作结果:保存失败

0x04 查询视频信息

需求分析

课程计划的视频信息保存后在页面无法查看,本节解决课程计划页面显示相关联的媒资信息。

解决方案:

在获取课程计划树结点信息时将关联的媒资信息一并查询,并在前端显示,下图说明了课程计划显示的区域。

Dao

修改课程计划查询的 Dao:

1、修改模型

在课程计划结果信息中添加媒资信息

package com.xuecheng.framework.domain.course.ext;import com.xuecheng.framework.domain.course.Teachplan;
import lombok.Data;
import lombok.ToString;import java.util.List;@Data
@ToString
public class TeachplanNode extends Teachplan {List<TeachplanNode> children;//媒资信息private String media_id;private String media_fileoriginalname;
}

2、修改sql 语句,添加关联查询媒资信息

添加 mediaIdmediaFileOriginalName

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xuecheng.manage_course.dao.TeachplanMapper"><resultMap id="teachplanMap" type="com.xuecheng.framework.domain.course.ext.TeachplanNode"><!--一级节点--><id property="id" column="one_id"/><result property="pname" column="one_pname"/><collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode"><!--二级节点--><id property="id" column="two_id"/><result property="pname" column="two_pname"/><collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode"><!--三级节点--><id property="id" column="three_id"/><result property="pname" column="three_pname"/><result property="media_id" column="media_id"/><result property="media_fileoriginalname" column="media_fileoriginalname"/></collection></collection></resultMap><!--三级菜单查询--><select id="selectList" resultMap="teachplanMap" parameterType="java.lang.String">SELECTa.id one_id,a.pname one_pname,a.courseid one_course,b.id two_id,b.pname two_pname,c.id three_id,c.pname three_pname,media.media_id media_id,media.media_fileoriginalname media_fileoriginalnameFROMteachplan aLEFT JOIN teachplan bON b.parentid = a.idLEFT JOIN teachplan cON c.parentid = b.idLEFT JOIN teachplan_media mediaON c.id = media.teachplan_idWHEREa.parentid = '0'<!--判断参数不为空时才进行参数的匹配--><if test="_parameter!=null and _parameter!=''">and a.courseid = #{courseId}</if>ORDER BY a.orderby,b.orderby,c.orderby</select>
</mapper>

这里的核心代码是使用 LEFT JOIN 关联 teachplan_media 表中的数据,再获取该课程计划下的 mediaIdmediaFileOriginalName 代码如下

LEFT JOIN teachplan_media media ON c.id = media.teachplan_id
WHERE
<!--三级节点-->
<id property="id" column="three_id"/>
<result property="pname" column="three_pname"/>
<result property="media_id" column="media_id"/>
<result property="media_fileoriginalname" 

使用swagger进行接口测试

从结果中成功的查询到了课程计划所关联的媒资信息。

页面查询视频

课程计划结点信息已包括媒资信息,可在页面获取信息后显示。

通过 data.media_fileoriginalname 获取媒资视频的原始名称

<el‐button style="font‐size: 12px;" type="text" on‐click={ () => this.querymedia(data.id) }>
{data.media_fileoriginalname}&nbsp;&nbsp;&nbsp;&nbsp;选择视频</el‐button>

效果如下:

选择视频后立即刷新课程计划树,在提交成功后,添加查询课程计划代码:this.findTeachplan(),完整代码如下:

choosemedia(mediaId,fileOriginalName,mediaUrl){this.mediaFormVisible = false;//保存课程计划与视频对应关系let teachplanMedia = {};teachplanMedia.teachplanId = this.activeTeachplanId;teachplanMedia.mediaId = mediaId;teachplanMedia.mediaFileOriginalName = fileOriginalName;teachplanMedia.mediaUrl = mediaUrl;teachplanMedia.courseId = this.courseid;//保存媒资信息到课程数据库courseApi.savemedia(teachplanMedia).then(res=>{if(res.success){this.$message.success("选择视频成功")//查询课程计划this.findTeachplan()}else{this.$message.error(res.message)}})
},

😁 认识作者

作者:👦 LCyee ,全干型代码🐕

自建博客:https://www.codeyee.com

记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。

CSDN 博客:https://blog.csdn.net/codeyee

记录和分享一些开发过程中遇到的问题以及解决的思路。

欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. Vue router的Tab切换

    在Vue router使用成功的前提下 1、引入需要切换的组件2、将路径写到特定的页面中,使用children实现路由的嵌套3、在嵌套的页面中写入Tab标签点击切换4、保存运行项目...

    2024/4/12 10:29:11
  2. 06 CephFS文件存储

    参考连接:<https://docs.ceph.com/docs/master/install/ceph-deploy/quick-cephfs/>;1 CephFS介绍1.1 CephFS概念​ Ceph File System (CephFS) 是与 POSIX 标准兼容的文件系统, 能够提供对 Ceph 存储集群上的文件访问.Jewel 版本 (10.2.0) 是第一个包含稳定 CephFS 的 …...

    2024/4/22 9:16:08
  3. 【笔记】前端学习第一天——常见概念

    跟随百度学院学前端。 常见前端概念:HTML、CSS、JAvaScript、SQL、WebServices、PHP等。一个普通网站访问的过程先了解一个网站访问的大概过程:用户操作浏览器访问,浏览器向服务器发出一个 HTTP 请求;服务器接收到 HTTP 请求,Web Server 进行相应的初步处理,使用服务器脚…...

    2024/5/5 18:31:03
  4. 微信小程序学习(1)-老陈

    学习微信小程序 文章目录学习微信小程序注册和初始化小程序配置tabbar导航栏模板插样与WXML循环渲染条件渲染模板微信小程序脚本WXSmodulerequiresrcdemo微信小程序常用组件事件绑定并阻止事件冒泡互斥事件绑定事件的捕获阶段API请求数据小程序项目-商城初始化引入vant使用组件…...

    2024/5/2 1:54:12
  5. 心有盏灯,向阳而行

    疫情原因,不用工作,刚好有空余的时间,自己在家自学了一个多月吧。这一个月里面,掌握了很多以前没有掌握的知识。但是随着自己掌握的越多,越感觉自己知识的匮乏。以前懂的都是一些皮毛而已,甚至说连皮毛也算不上。第一篇博客给予自己鼓励。只要开始,虽晚不迟。起于3月2日…...

    2024/4/18 10:28:24
  6. ES2015 实现可迭代接口与迭代器模式

    for…of 循环 在 ECMAScript 中遍历数据有很多中方法,首先是最基本的 for 循环,它比较适用于变量普通的数组,然后就是 for...in 循环,它比较适合于遍历键值对,再有就是一些对象的遍历方法,比如数组的 forEach() 方法。这些各种各样的遍历数据的方式都会有一定的局限性,所…...

    2024/4/23 1:03:58
  7. Windows10搭建本地FTP服务器

    1.用搜索功能打开控制面板2.打开程序3.打开启用或关闭Windows功能4.找到“Internet Information Service ”,并选中“FTP服务”、“FTP扩展性”和“IIS管理控制台”前的复选框,点击“确定”5.搜索IIS,然后点击打开“Internet Information Service(IIS)管理器”6.右击“网站…...

    2024/4/16 18:15:24
  8. 丽江——大理 旅游

    6.11 南京禄口机场——丽江三义机场 665 当晚住在花水湾民宿(三晚229),位于丽江古城内,靠近四方街 6.12 茶马古道+拉市海【160团】 上午至一个马场骑马,下午拉市海湿地公园 晚上住花水湾 晚饭滇西王子26(105) 酒吧 一米阳光旗舰店(165) 6.13 玉龙雪山【470团】 甘海子…...

    2024/4/26 3:45:49
  9. 解决Jenkins的html样式不生效问题的终极方案

    本文从四个步骤来分享我们在自行搭建jenkins过程中遇到的报表样式不全(即html报告展示不正确)的问题:1.问题现象2.问题原因3.问题原因补充4.解决方法(可以直接跳到第四步解决问题)一、问题现象1.当你是使用Jenkins的Html Publisher插件来发布报表的时候,可能发现报表的CS…...

    2024/5/7 16:53:46
  10. 删除单表中多个字段联合为重复数据的数据,只保留id最大的数据

    --方法一:DELETE FROM`user` WHERE ( NAME, age ) IN ( SELECT name,age FROM (SELECT NAME, age FROM `user` GROUP BY NAME, age HAVING count( * ) > 1) t1 ) AND id NOT IN ( SELECT id FROM (SELECT MAX( id ) AS id FROM USER GROUP BY NAME, age HAVING count( *…...

    2024/4/23 18:36:06
  11. Anki 浏览器必备插件之 Customize Sidebar

    Anki 浏览器必备插件之 Customize Sidebar 前言 当 Anki 中的牌组、模板、标签一多,浏览器中往往会显得杂乱无章,如图:我的屏幕已经很长了,然而还是一眼看不到尽头。这种情况显然会影响到使用,那么该如何是好呢? Customize Sidebar 今天介绍一款整理,可以将浏览器中的牌…...

    2024/5/1 20:02:18
  12. 剑指 Offer 17. 打印从1到最大的n位数java题解

    2.不考虑大数的解法 因为leetcode上,要返回一个int数组,所以这样也能过。但书上的本意,这题考察的是大数。不考虑大数就没啥意义了。 class Solution {public int[] printNumbers(int n) {int[] numbers=new int[(int)Math.pow(10,n)-1];int last=(int)Math.pow(10,n)-1;for…...

    2024/4/27 5:06:17
  13. 并查ji

    大意看收藏的博客吧,就是每个人都会有一个帮派。。。。A 题意给你n个人属于两个帮派 不知道每个人属于哪个,然后d xy 就是xy 不在一个帮派 A XY问xy是否在一个帮派思路只有两个帮派是关键,d x,y 就把x 和 y+n 绑定在一个帮派 x+n 和y 绑定在一个帮派 如果再来个d cy 那么 …...

    2024/5/7 20:26:18
  14. Linux探索-python版本切换及安装

    这里写自定义目录标题python版本切换1下载源代码2编译3可以切换版本 python版本切换 ubuntu16.04 自带 python2.7.12 和 python3.5.2 ,由于在ros-kinetc版本中需要python2.7 作为默认的解释其,所以我们ubuntu下默认的版本应该为python2.7, 1下载源代码 Python 官方下载地址:…...

    2024/4/23 12:34:45
  15. 机器学习算法基础day6

    文章目录分类算法-逻辑回归逻辑回归sigmoid函数逻辑回归公式逻辑回归的损失函数、优化sklearn逻辑回归APILogisticRegression总结k-means步骤k-means APIk-means对Instacart Market用户聚类Kmeans性能评估指标Kmeans性能评估指标APIKmeans总结 分类算法-逻辑回归 逻辑回归sigmo…...

    2024/4/24 1:08:08
  16. kubernetes使用ceph-csi 报rbd: map failed问题的处理

    最近在新搭建了一个新版的kubernetes集群,在按照ceph官方文档,创建测试pod时,报rbd: map failed: (6) No such device or address的错误。decribe pod 详细错误信息:按照提示,在pod所在节点上执行命令dmesg|tail,发现一推unsupported features的报错[421074.405749] rbd: i…...

    2024/4/28 11:42:30
  17. 295. 数据流的中位数

    题目链接:数据流的中位数 题目描述: 中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。 例如, [2,3,4] 的中位数是 3 [2,3] 的中位数是 (2 + 3) / 2 = 2.5 设计一个支持以下两种操作的数据结构: void addNum(int num) - 从数据流中添加一个整…...

    2024/4/24 11:47:56
  18. 反馈电路反馈类型的快速判断

    模电反馈电路反馈类型快速判断步骤1.判断是串联/并联2.确定并列出净输入量表达式3. 根据上述净输入量的表达式判断正反馈or负反馈4. 列出净输入量的表达式 反馈类型快速判断步骤 自己总结的步骤,欢迎指正 1.判断是串联/并联 输入与反馈接于同一端,则为并联反馈;输入与反馈接…...

    2024/4/25 6:28:05
  19. Vue router前端路由配置以及实现tab切换

    vue router安装:npm install vue-router或cnpm install vue-router或yarn add vue-router。 安装完成之后会在package.json中找到版本号。"dependencies": {"core-js": "^3.6.5","vue": "^2.6.11","vue-router":…...

    2024/4/25 23:41:19
  20. 使用Turtle库绘制图像----哆啦A梦

    使用Turtle库绘制图像----哆啦A梦 前言 Turtle库是Python中的一个绘图的函数库。 老师布置的作业,让我们设计图像 呜呜呜~好难过,毕竟太死了这个工具。。 方法都很简单,基本都是一样的步骤,但细节处理起来很浪费时间。 下面是我算是花了点时间的,我个人觉得还不错(不,完美…...

    2024/4/22 6:09:13

最新文章

  1. Android 巧用putBinder方法传递大文件

    使用Intent传递数据大家都知道&#xff0c;但是如果你使用Intent传递大于1Mb的数据时&#xff0c;就一定会报如下的错误&#xff1a; Caused by: android.os.TransactionTooLargeException: data parcel size 1049112 bytes 就是说你的传输数据太大了&#xff0c;当前的大小达…...

    2024/5/8 3:01:40
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/5/7 10:36:02
  3. 华为OD机试 - 跳马(Java JS Python C C++)

    须知 哈喽,本题库完全免费,收费是为了防止被爬,大家订阅专栏后可以私信联系退款。感谢支持 文章目录 须知题目描述输入描述输出描述解题思路:题目描述 马是象棋(包括中国象棋和国际象棋)中的棋子,走法是每步直一格再斜一格,即先横着或者直者走一格,然后再斜着走一个…...

    2024/5/3 4:50:16
  4. [Spring Cloud] gateway全局异常捕捉统一返回值

    文章目录 处理转发失败的情况全局参数同一返回格式操作消息对象AjaxResult返回值状态描述对象AjaxStatus返回值枚举接口层StatusCode 全局异常处理器自定义通用异常定一个自定义异常覆盖默认的异常处理自定义异常处理工具 在上一篇章时我们有了一个简单的gateway网关 [Spring C…...

    2024/5/8 1:47:24
  5. Nginx配置文件修改结合内网穿透实现公网访问多个本地web站点

    文章目录 1. 下载windows版Nginx2. 配置Nginx3. 测试局域网访问4. cpolar内网穿透5. 测试公网访问6. 配置固定二级子域名7. 测试访问公网固定二级子域名 1. 下载windows版Nginx 进入官方网站(http://nginx.org/en/download.html)下载windows版的nginx 下载好后解压进入nginx目…...

    2024/5/5 0:23:44
  6. 416. 分割等和子集问题(动态规划)

    题目 题解 class Solution:def canPartition(self, nums: List[int]) -> bool:# badcaseif not nums:return True# 不能被2整除if sum(nums) % 2 ! 0:return False# 状态定义&#xff1a;dp[i][j]表示当背包容量为j&#xff0c;用前i个物品是否正好可以将背包填满&#xff…...

    2024/5/7 19:05:20
  7. 【Java】ExcelWriter自适应宽度工具类(支持中文)

    工具类 import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;/*** Excel工具类** author xiaoming* date 2023/11/17 10:40*/ public class ExcelUti…...

    2024/5/7 22:31:36
  8. Spring cloud负载均衡@LoadBalanced LoadBalancerClient

    LoadBalance vs Ribbon 由于Spring cloud2020之后移除了Ribbon&#xff0c;直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件&#xff0c;我们讨论Spring负载均衡以Spring Cloud2020之后版本为主&#xff0c;学习Spring Cloud LoadBalance&#xff0c;暂不讨论Ribbon…...

    2024/5/8 1:37:40
  9. TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

    一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

    2024/5/7 14:19:30
  10. VB.net WebBrowser网页元素抓取分析方法

    在用WebBrowser编程实现网页操作自动化时&#xff0c;常要分析网页Html&#xff0c;例如网页在加载数据时&#xff0c;常会显示“系统处理中&#xff0c;请稍候..”&#xff0c;我们需要在数据加载完成后才能继续下一步操作&#xff0c;如何抓取这个信息的网页html元素变化&…...

    2024/5/8 1:37:39
  11. 【Objective-C】Objective-C汇总

    方法定义 参考&#xff1a;https://www.yiibai.com/objective_c/objective_c_functions.html Objective-C编程语言中方法定义的一般形式如下 - (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgu…...

    2024/5/7 16:57:02
  12. 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】

    &#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格…...

    2024/5/7 14:58:59
  13. 【ES6.0】- 扩展运算符(...)

    【ES6.0】- 扩展运算符... 文章目录 【ES6.0】- 扩展运算符...一、概述二、拷贝数组对象三、合并操作四、参数传递五、数组去重六、字符串转字符数组七、NodeList转数组八、解构变量九、打印日志十、总结 一、概述 **扩展运算符(...)**允许一个表达式在期望多个参数&#xff0…...

    2024/5/7 1:54:46
  14. 摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

    文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕&#xff0c;各大品牌纷纷晒出优异的成绩单&#xff0c;摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称&#xff0c;在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁&#xff0c;多个平台数据都表现出极度异常…...

    2024/5/7 21:15:55
  15. Go语言常用命令详解(二)

    文章目录 前言常用命令go bug示例参数说明 go doc示例参数说明 go env示例 go fix示例 go fmt示例 go generate示例 总结写在最后 前言 接着上一篇继续介绍Go语言的常用命令 常用命令 以下是一些常用的Go命令&#xff0c;这些命令可以帮助您在Go开发中进行编译、测试、运行和…...

    2024/5/8 1:37:35
  16. 用欧拉路径判断图同构推出reverse合法性:1116T4

    http://cplusoj.com/d/senior/p/SS231116D 假设我们要把 a a a 变成 b b b&#xff0c;我们在 a i a_i ai​ 和 a i 1 a_{i1} ai1​ 之间连边&#xff0c; b b b 同理&#xff0c;则 a a a 能变成 b b b 的充要条件是两图 A , B A,B A,B 同构。 必要性显然&#xff0…...

    2024/5/7 16:05:05
  17. 【NGINX--1】基础知识

    1、在 Debian/Ubuntu 上安装 NGINX 在 Debian 或 Ubuntu 机器上安装 NGINX 开源版。 更新已配置源的软件包信息&#xff0c;并安装一些有助于配置官方 NGINX 软件包仓库的软件包&#xff1a; apt-get update apt install -y curl gnupg2 ca-certificates lsb-release debian-…...

    2024/5/7 16:04:58
  18. Hive默认分割符、存储格式与数据压缩

    目录 1、Hive默认分割符2、Hive存储格式3、Hive数据压缩 1、Hive默认分割符 Hive创建表时指定的行受限&#xff08;ROW FORMAT&#xff09;配置标准HQL为&#xff1a; ... ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0001 COLLECTION ITEMS TERMINATED BY , MAP KEYS TERMI…...

    2024/5/8 1:37:32
  19. 【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法

    文章目录 摘要1 引言2 问题描述3 拟议框架4 所提出方法的细节A.数据预处理B.变量相关分析C.MAG模型D.异常分数 5 实验A.数据集和性能指标B.实验设置与平台C.结果和比较 6 结论 摘要 异常检测是保证航天器稳定性的关键。在航天器运行过程中&#xff0c;传感器和控制器产生大量周…...

    2024/5/7 16:05:05
  20. --max-old-space-size=8192报错

    vue项目运行时&#xff0c;如果经常运行慢&#xff0c;崩溃停止服务&#xff0c;报如下错误 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 因为在 Node 中&#xff0c;通过JavaScript使用内存时只能使用部分内存&#xff08;64位系统&…...

    2024/5/8 1:37:31
  21. 基于深度学习的恶意软件检测

    恶意软件是指恶意软件犯罪者用来感染个人计算机或整个组织的网络的软件。 它利用目标系统漏洞&#xff0c;例如可以被劫持的合法软件&#xff08;例如浏览器或 Web 应用程序插件&#xff09;中的错误。 恶意软件渗透可能会造成灾难性的后果&#xff0c;包括数据被盗、勒索或网…...

    2024/5/8 1:37:31
  22. JS原型对象prototype

    让我简单的为大家介绍一下原型对象prototype吧&#xff01; 使用原型实现方法共享 1.构造函数通过原型分配的函数是所有对象所 共享的。 2.JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象…...

    2024/5/7 11:08:22
  23. C++中只能有一个实例的单例类

    C中只能有一个实例的单例类 前面讨论的 President 类很不错&#xff0c;但存在一个缺陷&#xff1a;无法禁止通过实例化多个对象来创建多名总统&#xff1a; President One, Two, Three; 由于复制构造函数是私有的&#xff0c;其中每个对象都是不可复制的&#xff0c;但您的目…...

    2024/5/7 7:26:29
  24. python django 小程序图书借阅源码

    开发工具&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html 小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索图书&#xff0c;轮播图&#xff0…...

    2024/5/8 1:37:29
  25. 电子学会C/C++编程等级考试2022年03月(一级)真题解析

    C/C++等级考试(1~8级)全部真题・点这里 第1题:双精度浮点数的输入输出 输入一个双精度浮点数,保留8位小数,输出这个浮点数。 时间限制:1000 内存限制:65536输入 只有一行,一个双精度浮点数。输出 一行,保留8位小数的浮点数。样例输入 3.1415926535798932样例输出 3.1…...

    2024/5/7 17:09:45
  26. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  27. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  28. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  29. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  30. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  31. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  32. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  33. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  34. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  35. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  36. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  37. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  38. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  39. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  40. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  41. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  42. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  43. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  44. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  45. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57