2021-11-10 谷粒学院技术总结-后台

目录

 一、主键生成策略

1、自动增长

2、UUID

3、Redis

4、snowflake算法(雪花算法)

二、项目分页

1、创建配置类

三、统一异常、日志处理

1、统一异常处理

2、统一日志处理

1、配置日志级别

2、Logback日志

3、将错误日志输出到文件中

四、Vue

1、Vue.js 是什么

2、Vue生命周期

3、Vue的路由

 1、编写html

2、编写js

4、axios

五、阿里云OSS讲师头像上传

六、使用EasyExcel实现读文件操作

1、简介

2、EasyExcel特点

3、EasyExcel写

六、课程分类列表模块树形控件

1、展示课程分类列表

2、对课程分类列表进行搜索

 七、课程信息管理

1、填写课程基本信息

1、课程分类

2、创建课程大纲

1、编辑删除操作

2、阿里云视频点播服务上传小节视频

3、发布课程 

4、在课程列表删除课程信息

 八、统计分析模块

1、生成数据

2、Echarts图表

1、前端


 

项目描述:

在线教育系统,分为前台网站系统和后台运营平台。
前台用户系统包括:首页、课程、名师、问答、文章。
后台管理系统包括:讲师管理、课程分类管理、课程管理、统计分析、Banner管理、订单管理、权限管理等功能。
后端的主要技术架构是:SpringBoot + SpringCloud + MyBatis-Plus 
前端的架构是: Vue.js +element-ui
其他涉及到的中间件包括Redis、阿里云OSS、阿里云视频点播、
业务中使用了ECharts做图表展示,使用EasyExcel完成分类批量添加、注册分布式单点登录使用了JWT+token

 一、主键生成策略

2021-11-10 谷粒学院技术总结-后台_第1张图片

1、自动增长

2021-11-10 谷粒学院技术总结-后台_第2张图片

优缺点如下:

最常见的方式。利用数据库,全数据库唯一。

优点:

1)简单,代码方便,性能可以接受。

2)数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

1)不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。

2)在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。

3)如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦。

4)分表分库的时候会有麻烦。

5)并非一定连续,类似MySQL,当生成新ID的事务回滚,那么后续的事务也不会再用这个ID了。这个在性能和连续性的折中。如果为了保证连续,必须要在事务结束后才能生成ID,那性能就会出现问题。

2、UUID

常见的方式。可以利用数据库也可以利用程序生成,一般来说全球唯一。UUID是由32个的16进制数字组成,所以每个UUID的长度是128位(16^32 = 2^128)。UUID作为一种广泛使用标准,有多个实现版本,影响它的因素包括时间、网卡MAC地址、自定义Namesapce等等。

优点:

1)简单,代码方便。

2)生成ID性能非常好,基本不会有性能问题。

3)全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。

缺点:

1)没有排序,无法保证趋势递增。

2)UUID往往是使用字符串存储,查询的效率比较低。

3)存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。

4)传输数据量大

5)不可读。

3、Redis

当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。

可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:

A:1,6,11,16,21

B:2,7,12,17,22

C:3,8,13,18,23

D:4,9,14,19,24

E:5,10,15,20,25

这个,随便负载到哪个机确定好,未来很难做修改。但是3-5台服务器基本能够满足器上,都可以获得不同的ID。但是步长和初始值一定需要事先需要了。使用Redis集群也可以方式单点故障的问题。

另外,比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。

优点:

1)不依赖于数据库,灵活方便,且性能优于数据库。

2)数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

1)如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。

2)需要编码和配置的工作量比较大。

4、snowflake算法(雪花算法)

MP自带策略,在id上添加@TableId(type = ID_WORKER_STR/ID_WORKER)分别是string和long型主键,19位,采用雪花算法

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。

snowflake算法可以根据自身项目的需要进行一定的修改。比如估算未来的数据中心个数,每个数据中心的机器数以及统一毫秒可以能的并发数来调整在算法中所需要的bit数。

优点:

1)不依赖于数据库,灵活方便,且性能优于数据库。

2)ID按照时间在单机上是递增的。

缺点:

1)在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,在算法上要解决时间回拨的问题。

二、项目分页

MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能

1、创建配置类

/**
 * 分页插件
 */
@Bean
public PaginationInterceptor paginationInterceptor() {
    return new PaginationInterceptor();
}

配置分页插件,测试,最终通过page对象获取分页信息

@Test
public void testSelectPage() {
    //传入当前页及每页记录数
    Page page = new Page<>(1,5);
    userMapper.selectPage(page, null);

    page.getRecords().forEach(System.out::println);
    System.out.println(page.getCurrent());
    System.out.println(page.getPages());
    System.out.println(page.getSize());
    System.out.println(page.getTotal());
    System.out.println(page.hasNext());
    System.out.println(page.hasPrevious());
}
teacherService.page(pageTeacher,wrapper);在service层调用分页方法

控制台sql语句打印:

SELECT id,name,age,email,create_time,update_time FROM user LIMIT 0,5 

三、统一异常、日志处理

1、统一异常处理

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    //指定出现什么异常执行该方法
    @ExceptionHandler(Exception.class)
    @ResponseBody//为了返回数据
    public R error(Exception e){
        e.printStackTrace();
        return R.error().message("执行了全局异常处理");
    }

    //指定出现什么异常执行该方法
    @ExceptionHandler(ArithmeticException .class)
    @ResponseBody//为了返回数据
    public R error(ArithmeticException  e){
        e.printStackTrace();
        return R.error().message("执行了ArithmeticException 异常处理");
    }

    //指定出现什么异常执行该方法
    @ExceptionHandler(GuliException.class)
    @ResponseBody//为了返回数据
    public R error(GuliException e){
        e.printStackTrace();
        log.error(e.getMessage());
        return R.error().code(e.getCode()).message(e.getMsg());
    }


}

也可以自定义异常

2021-11-10 谷粒学院技术总结-后台_第3张图片

 以上三种方式可以实现自定义异常

@AllArgsConstructor
@Data
@NoArgsConstructor
public class GuliException extends RuntimeException{
    private Integer code;
    private String msg;
}

2、统一日志处理

1、配置日志级别

日志记录器(Logger)的行为是分等级的。如下表所示:

分为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL

默认情况下,spring boot从控制台打印出来的日志级别只有INFO及以上级别,在application.properties中可以配置日志级别,输出到控制台

# 设置日志级别
logging.level.root=WARN

2、Logback日志

spring boot内部使用Logback作为日志实现的框架。

Logback和log4j非常相似,如果你对log4j很熟悉,那对logback很快就会得心应手。

resources 中创建 logback-spring.xml 



    
    
    
    

    logback
    
    

    
    
    
    
    
    
    
    


    
    
        
        
        
            INFO
        
        
            ${CONSOLE_LOG_PATTERN}
            
            UTF-8
        
    


    

    
    
        
        ${log.path}/log_info.log
        
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            UTF-8
        
        
        
            
            ${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log
            
                100MB
            
            
            15
        
        
        
            INFO
            ACCEPT
            DENY
        
    

    
    
        
        ${log.path}/log_warn.log
        
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            UTF-8 
        
        
        
            ${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log
            
                100MB
            
            
            15
        
        
        
            warn
            ACCEPT
            DENY
        
    


    
    
        
        ${log.path}/log_error.log
        
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            UTF-8 
        
        
        
            ${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log
            
                100MB
            
            
            15
        
        
        
            ERROR
            ACCEPT
            DENY
        
    

    
    
    
    
        
        

        
        
            
            
            
            
        
    


    
    

        
            
            
            
            
            
        
    

3、将错误日志输出到文件中

2021-11-10 谷粒学院技术总结-后台_第4张图片

四、Vue

1、Vue.js 是什么

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。

Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

2、Vue生命周期

我们常用的有created()和mounted()

页面渲染之前和页面渲染之后 

3、Vue的路由

Vue.js 路由允许我们通过不同的 URL 访问不同的内容。

通过 Vue.js 可以实现多视图的单页Web应用(single page web application,SPA)。

Vue.js 路由需要载入 vue-router 库


 1、编写html

Hello App!

首页 会员管理 讲师管理

2、编写js

4、axios

axios是独立于vue的一个项目,基于promise用于浏览器和node.js的http客户端

  • 在浏览器中可以帮助我们完成 ajax请求的发送
  • 在node.js中可以向远程接口发送请求

五、阿里云OSS讲师头像上传


@Service
public class OssServiceImpl implements OssService {

    /**
     * 文件上传的方法
     * @param file 要上传的文件
     * @return 返回oss中文件路径
     */
    @Override
    public String uploadFileAvatar(MultipartFile file) {
        //通过工具类获取阿里云存储相关常量
        String endPoint = ConstantPropertiesUtil.END_POINT;
        String accessKeyId = ConstantPropertiesUtil.ACCESS_KEY_ID;
        String accessKeySecret = ConstantPropertiesUtil.ACCESS_KEY_SECRET;
        String bucketName = ConstantPropertiesUtil.BUCKET_NAME;

        //上传成功后存储在阿里云中的地址
        String uploadUrl = null;

        //在文件名中添加uuid以防止大量图片重名问题。
        String uuid = UUID.randomUUID().toString().replace("-", "");

        try {
            //判断oss实例是否存在:如果不存在则创建,如果存在则获取
            OSSClient ossClient = new OSSClient(endPoint, accessKeyId, accessKeySecret);
            if (!ossClient.doesBucketExist(bucketName)) {
                //创建bucket
                ossClient.createBucket(bucketName);
                //设置oss实例的访问权限:公共读
                ossClient.setBucketAcl(bucketName, CannedAccessControlList.PublicRead);
            }

            //获取上传文件流
            InputStream inputStream = file.getInputStream();

            //优化1:文件名加上uuid
            String fileUrl = uuid + file.getOriginalFilename();

            //优化2:把文件按照日期分类存储
            //2021/11/11/avatar.jpb
            //获取当前日期,借助joda-time工具类来简单实现转换日期为指定格式
            String datePath = new DateTime().toString("yyyy/MM/dd");

            //拼接
            fileUrl = datePath + "/" + fileUrl;

            //文件上传至阿里云
            /*
             * 第一个参数 Bucket名称
             * 第二个参数 上传到oss的路径和文件名
             * 第三个参数 上传文件输入流
             */
            ossClient.putObject(bucketName, fileUrl, inputStream);

            // 关闭OSSClient。
            ossClient.shutdown();

            //获取url地址
            uploadUrl = "http://" + bucketName + "." + endPoint + "/" + fileUrl;

        } catch (IOException e) {
            throw new GuliException();
        }
        return uploadUrl;
    }
}

六、使用EasyExcel实现读文件操作

1、简介

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便

2、EasyExcel特点

  • Java领域解析、生成Excel比较有名的框架有Apache poi、jxl等。但他们都存在一个严重的问题就是非常的耗内存。如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc。
  • EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
  • EasyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)

3、EasyExcel写

先引入依赖,由于底层是对poi进行重写,因此使用时还需要引入poi的依赖

    
    
        com.alibaba
        easyexcel
        2.1.1
    

前端代码


        
          选取文件
          上传到服务器
        
      

脚本

      //点击上传到接口中
      submitUpload() {
        this.importBtnDisabled = true
        this.loading = true
        //js:document.getElementById("upload").submit()
        this.$refs.upload.submit()
      },
      //上传成功
      fileUploadSuccess() {
          //提示信息
          this.loading = false
          this.$message({
              type: 'success',
              message: '添加课程分类成功'
          })
          //跳转到课程分类的列表中
          this.$router.push({path: '/subject/list'})
      },

后端接口代码

    /**
     * 添加课程方法
     * @param file 要添加的excel文件
     */
    @PostMapping("addSubject")
    private R addSubject(MultipartFile file){
        subjectService.saveSubject(file,subjectService);
        return R.ok();
    }

接口中传入一个subjectService操作数据库,因为saveSubject方法中需要用到监听器,而监听器并没有交给Spring管理,因此不能注入subjectService,需要我们传入

接口实现类

    //添加课程
    @Override
    public void saveSubject(MultipartFile file,EduSubjectService subjectService) {
        try {
            InputStream is = file.getInputStream();
            EasyExcel.read(is, SubjectData.class,new SubjectExcelListener(subjectService)).sheet().doRead();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

监听器SubjectExcelListener,继承阿里巴巴easyexcel的AnalysisEventListener类,重写里面方法

public class SubjectExcelListener extends AnalysisEventListener {

    //因为SubjectExcelListener不能交给spring管理,需要自己new,不能注入对象
    //不能实现数据库操作,利用有参构造器传入,EduSubjectService,以便操作
    public EduSubjectService subjectService;

    public SubjectExcelListener(){}

    //利用构造器接收接口传来的service类操作数据库。
    public SubjectExcelListener(EduSubjectService subjectService){
        this.subjectService = subjectService;
    }

    @Override
    public void invoke(SubjectData subjectData, AnalysisContext analysisContext) {
        if (subjectData == null) {
            throw new GuliException(20001,"表中数据为空");
        }
        //一行一行读取数据,每次读取两个值,第一个值一级分类,第二个二级分类
        //判断一级分类是否为空,为空则不存在,添加
        EduSubject existOneSubject = this.existOneSubject(subjectService, subjectData.getOneSubjectName());
        if (existOneSubject == null){
            existOneSubject = new EduSubject();
            existOneSubject.setParentId("0");
            existOneSubject.setTitle(subjectData.getOneSubjectName());
            subjectService.save(existOneSubject);
        }

        //判断二级分类是否为空,为空则不存在,添加
//        String pid = existOneSubject.getId();

        EduSubject existTwoSubject = this.existTwoSubject(subjectService,subjectData.getTwoSubjectName(),existOneSubject.getId());
        if (existTwoSubject == null){
            existTwoSubject = new EduSubject();
            existTwoSubject.setParentId(existOneSubject.getId());
            existTwoSubject.setTitle(subjectData.getTwoSubjectName());
            subjectService.save(existTwoSubject);
        }
    }

    //判断一级分类是否存在
    private EduSubject existOneSubject(EduSubjectService subjectService,String name){
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.eq("title",name);
        wrapper.eq("parent_id",0);
        return subjectService.getOne(wrapper);
    }

    //判断二级分类是否存在
    private EduSubject existTwoSubject(EduSubjectService subjectService,String name,String pid){
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.eq("title",name);
        wrapper.eq("parent_id",pid);
        return subjectService.getOne(wrapper);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }
}

此处问题如下

比如当我们第一次上传一级分类Vue

2021-11-10 谷粒学院技术总结-后台_第5张图片

改为小写vue上传,并不会改变,这一点需要明确。

 因为,在判断二级分类是否存在时,方法中传入了一级分类的id,自然,得到的方法返回对象为空,因为同父id下的二级分类title并不与传递过来的相同,所以反回了空值。

六、课程分类列表模块树形控件

1、展示课程分类列表

接口如下:

    /**
     * 课程分类列表(树形)
     * @return 查询到的一级二级分类对象
     */
    @GetMapping("getAllSubject")
    public R getAllSubject(){
        List list = subjectService.getAllOneTwoSubject();
        return R.ok().data("list",list);
    }

实现方法如下:

    //课程分类
    @Override
    public List getAllOneTwoSubject() {
        QueryWrapper wrapperOne = new QueryWrapper<>();
        QueryWrapper wrapperTwo = new QueryWrapper<>();
        //1 查出所有的一级分类
        wrapperOne.eq("parent_id", '0');
        List oneSubjectList = this.list(wrapperOne);
        //2 查出所有的二级分类
        wrapperTwo.ne("parent_id", '0');
        List twoSubjectList = this.list(wrapperTwo);

        //创建一个集合封装数据
        List finalSubjectList = new ArrayList<>();

        //3 封装一级分类
        //遍历得到的一级分类集合,然后抽取OneSubject对象封装
        for (EduSubject eduSubject : oneSubjectList) {
            OneSubject oneSubject = new OneSubject();
//            oneSubject.setId(eduSubject.getId());
//            oneSubject.setTitle(eduSubject.getTitle());

            //上面注释的方法太过复杂,利用spring中的BeanUtils工具类进行优化
            //方法作用为将eduSubject中属性自动复制到oneSubject
            BeanUtils.copyProperties(eduSubject, oneSubject);
            finalSubjectList.add(oneSubject);

        }
        //4 封装二级分类
        for (OneSubject oneSubject : finalSubjectList) {
            List twoList = new ArrayList<>();
            for (EduSubject subject : twoSubjectList) {
                if (subject.getParentId().equals(oneSubject.getId())) {
                    TwoSubject twoSubject = new TwoSubject();
                    BeanUtils.copyProperties(subject, twoSubject);
                    twoList.add(twoSubject);
                }
            }
            oneSubject.setChildren(twoList);
        }

        return finalSubjectList;
    }

思路:

  1. 用eq和nq方法,查找父id为0及不为0的两个集合,得到了所有的一级分类集合和二级分类集合
  2. 我们自定义的一级分类和二级分类vo用于封装要显示的信息:id和title,一级分类vo中有二级分类集合
  3. 建立一个以一级分类为泛型的集合finalList用于最终封装
  4. 对一级分类集合进行遍历,将属性拷贝进一级分类vo,然后添加到finalList
  5. 对finalList进行遍历,即对每一个一级分类进行遍历,内部对二级分类进行遍历,把每一个二级分类的父id等于一级分类id的对象拷贝进二级分类vo,拷贝完添加进这个一级分类的chirldren集合。
  6. 整个循环结束后,对应的一级分类和二级分类封装完成,返回这个finalList。

2、对课程分类列表进行搜索

该搜索由前端实现,对搜索文本双向绑定

对该文本框的值的变化进行监听

 watch: {
    filterText(val) {
      this.$refs.tree2.filter(val);
    },
  },

当值变化时,会传入tree2的过滤器方法

方法如下:

filterNode(value, data) {
      if (!value) return true;
      let lowerVal = value.toLowerCase();
      return data.title.toLowerCase().indexOf(lowerVal) !== -1;
    }

如果文本框为空,全部显示

data代表树形控件的各个节点,否则就将文本值转为小写,标题转为小写,显示各个节点标题包含文本值的节点

 七、课程信息管理

2021-11-10 谷粒学院技术总结-后台_第6张图片

 一共有三个步骤,填写课程基本信息-》创建课程大纲-》最终发布

1、填写课程基本信息

有两个关键点

2021-11-10 谷粒学院技术总结-后台_第7张图片

实现思路:

在created()中先获取所有的课程分类和讲师信息。

1、课程分类

使用v-for输出一级分类,绑定一个value,即分类的id

        
        
          
        

 绑定change事件,参数传入绑定的value,遍历一级分类,如果传入的课程分类id等于这个一级分类id,那么将这个一级分类的子集合,也就是二级分类集合赋给初始化的二级分类集合。

    //点击选择一级分类后触发change事件获取二级分类
    change(value) {
        //one:遍历所有一级分类得到
        this.subjectOneList.forEach((one) => {
          //遍历所有一级分类的子元素
          console.log(one.children);
          if (value === one.id) {
            this.subjectTwoList = one.children;
          }
        });
        this.courseInfo.subjectId = "";
     }

二级分类集合显示

        
        
          
        

2、创建课程大纲

这一步的路由跳转时由课程基本信息页面携带课程id跳转,created()方法会根据课程id查询所有的章节和小节信息显示,添加时查询到的信息为空,修改时传递查询到的信息回显。

1、编辑删除操作

每个章节或者小节都对应绑定相应的id,编辑删除方法根据这个id进行操作

2、阿里云视频点播服务上传小节视频

2021-11-10 谷粒学院技术总结-后台_第8张图片

前端代码

      
          
            上传视频
            
              
最大支持1G,
支持3GP、ASF、AVI、DAT、DV、FLV、F4V、
GIF、M2T、M4V、MJ2、MJPEG、MKV、MOV、MP4、
MPE、MPG、MPEG、MTS、OGG、QT、RM、RMVB、
SWF、TS、VOB、WMV、WEBM 等视频格式上传

 选中要上传的视频后发送如下请求,执行了action中的请求,上传到阿里云

 上传流程

    /**
     * 上传视频到阿里云
     * 流式上传接口
     */
    @Override
    public String uploadVideoAly(MultipartFile file) {
        String accessKeyId = VodUtils.ACCESS_KEY_ID;
        String accessKeySecret = VodUtils.ACCESS_KEY_SECRET;
        //上传文件原始名
        String fileName = file.getOriginalFilename();

        //上传后文件名
        //先得到.的位置,然后用subString截取
        String title = "";
        if (fileName != null){
            //利用去掉文件后缀名做上传的文件名称
            title = fileName.substring(0,fileName.lastIndexOf('.'));
        }

        InputStream inputStream = null;
        try {
            inputStream = file.getInputStream();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
        //创建一个上传流请求,传入密钥和文件标题,文件名称,输入流
        UploadStreamRequest request = new UploadStreamRequest(accessKeyId, accessKeySecret, title, fileName, inputStream);
        /* 点播服务接入点 */
        request.setApiRegionId("cn-shanghai");
        //创建一个上传实例
        UploadVideoImpl uploader = new UploadVideoImpl();
        //执行上传请求,返回值是一个上传流响应
        UploadStreamResponse response = uploader.uploadStream(request);
        System.out.print("RequestId=" + response.getRequestId() + "\n");  //请求视频点播服务的请求ID
        String videoId = "";
        if (response.isSuccess()) {
            //通过响应可以拿到阿里云文件的存储id返回
            videoId = response.getVideoId();
        } else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因
            videoId = response.getVideoId();
        }
        return  videoId;
    }

视频上传成功后执行成功回调,绑定文件名和视频id

//成功回调
    //file参数是这个文件对象
    handleVodUploadSuccess(response, file, fileList) {
      this.video.videoSourceId = response.data.videoId;
      this.video.videoOriginalName = file.name
    },

3、发布课程 

2021-11-10 谷粒学院技术总结-后台_第9张图片

这一系列三步操作核心都是路由跳转时携带课程id,操作这个id对数据库进行修改

这个回显通过课程id,得到我们自定义vo的对象进行回显

@Data
public class CoursePublishVo {
    private String id;
    private String title;
    private String cover;
    private Integer lessonNum;
    private String subjectLevelOne;
    private String subjectLevelTwo;
    private String teacherName;
    private String price;
}

问题:当用户未发布课程即退出,那么会发生什么

答:我们三个步骤每次进入下一个步骤,实际都保存了对应的信息,第一次保存了基本信息,第二次保存了章节小节信息,如果退出,课程列表仍然可以查到,只不过有一个关键状态:课程发布状态字段为未发布,而发布页即是修改这个状态的操作 

4、在课程列表删除课程信息

2021-11-10 谷粒学院技术总结-后台_第10张图片

 这一操作的思路是

    /**
     * 根据id删除课程以及下面的信息
     */
    @Override
    public void removeCourse(String courseId) {
        //1 根据课程id删除课程里面的小节
        videoService.removeVideoByCourseId(courseId);
        //2 根据课程id删除课程里面的章节
        chapterService.removeChapterByCourseId(courseId);
        //3 根据课程id删除课程里面的描述
        courseDescriptionService.removeById(courseId);
        //4 根据课程id删除课程本身(逻辑删除)
        this.removeById(courseId);
    }

 删除小节

    /*
    * 根据课程id删除小节
    */
    @Override
    public void removeVideoByCourseId(String courseId) {
//        删除小节时一起删除视频
        //在video表中仅查出video_source_id字段即可
        QueryWrapper videoQueryWrapper = new QueryWrapper<>();
        videoQueryWrapper.eq("course_id",courseId);
        videoQueryWrapper.select("video_source_id");
        //根据课程id,查出对应的视频id的集合
        List eduVideos = this.list(videoQueryWrapper);
        List videoIdLists = new ArrayList<>();
        for (EduVideo eduVideo : eduVideos) {
            if (eduVideo != null){
                videoIdLists.add(eduVideo.getVideoSourceId());
            }
        }
        //如果视频id集合不为空,那么远程调用视频模块的根据视频id批量删除视频方法
        if (videoIdLists.size() > 0){
            vodClient.delVodBatch(videoIdLists);
        }
        //删除小节信息
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.eq("course_id",courseId);
        this.remove(wrapper);
    }

批量删除方法如下 

    /**
     * 根据视频id批量删除多个阿里云视频
     */
    @Override
    public void removeVideos(List videoIdList) {
        //此方法为自己封装,内含有地区,id及key
        DefaultAcsClient client = InitVodClient.initVodClient();

        DeleteVideoRequest request = new DeleteVideoRequest();

        String videoIds = StringUtils.join(videoIdList.toArray(), ",");
        //支持传入多个视频ID,多个用逗号分隔
        request.setVideoIds(videoIds);
        try {
            client.getAcsResponse(request);
        } catch (Exception e) {
            System.out.print("ErrorMessage = " + e.getLocalizedMessage());
            throw new GuliException(20001,"删除视频失败");
        }
    }

 这个方法是远程调用

 八、统计分析模块

1、生成数据

2021-11-10 谷粒学院技术总结-后台_第11张图片

选择一个日期,传入接口,接口统计注册、登录、新增课程以及视频播放数。

    @Override
    public void registerCount(String date) {
//        先删除相同日期的统计记录
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.eq("date_calculated",date);
        this.remove(wrapper);

        //远程调用获取注册人数
        R r = ucenterClient.countRegister(date);
        Integer countReg = (Integer) r.getData().get("countReg");

        //将数据添加到数据库
        StatisticsDaily sta = new StatisticsDaily();
        sta.setRegisterNum(countReg);//注册人数
        sta.setDateCalculated(date);//统计日期

        //TODO 这三个数据统计暂未实现,用随机数代替
        sta.setCourseNum(RandomUtils.nextInt(100, 200));//新增课程数
        sta.setLoginNum(RandomUtils.nextInt(100, 200));//登录统计
        sta.setVideoViewNum(RandomUtils.nextInt(100, 200));//视频播放
        this.save(sta);

    }
  1.  先删除要统计日期的统计数,因为我们要确保拿到最新的数据
  2. 统计完成后将这些数据保存到数据库中,进行更新
  3. 注意:如果直接保存,那么相当于插入,这样我们如果之前统计的有数据的话就会出现多个相同日期的行,这里远程调用了用户模块,使用count对数据库进行统计

2、Echarts图表

简介

ECharts是百度的一个项目,后来百度把Echart捐给apache,用于图表展示,提供了常规的折线图柱状图散点图饼图K线图,用于统计的盒形图,用于地理数据可视化的地图热力图线图,用于关系数据可视化的关系图treemap旭日图,多维数据可视化的平行坐标,还有用于 BI 的漏斗图仪表盘,并且支持图与图之间的混搭。
官方网站:https://echarts.baidu.com/

1、前端

该图标工具采用折线图类型,需要执行图的x轴和y轴,x为日期,y为人数

        // x轴是类目轴(离散数据),必须通过data设置类目数据
        xAxis: {
          type: "category",
          data: this.xData,
        },
        // y轴是数据轴(连续数据)
        yAxis: {
          type: "value",
        },
        // 系列列表。每个系列通过 type 决定自己的图表类型
        series: [
          {
            // 系列中的数据内容数组
            data: this.yData,
            // 折线图
            type: "line",
          },

2、接口

    @GetMapping("showData/{type}/{begin}/{end}")
    public R showData(@PathVariable String type,
                      @PathVariable String begin,
                      @PathVariable String end){
        Map map = staService.showData(type,begin,end);

        return R.ok().data(map);
    }

实现类

    /**
     * 根据筛选条件展示统计图表信息
     */
    @Override
    public Map showData(String type, String begin, String end) {
        //根据条件查询数据
        QueryWrapper wrapper = new QueryWrapper<>();
        //拼接日期的筛选条件
        wrapper.between("date_calculated",begin,end);
        //参数是可变形参,查出统计日期和统计类型两列
        wrapper.select("date_calculated",type);
        List staList = this.list(wrapper);

        //因为返回有两部分数据日期和时间对应的数量
        //前端要求json数组结构,对应后端是list集合
        //创建两个list集合,一个日期list,一个数量list
        List dateList = new ArrayList<>();
        List countList = new ArrayList<>();

        for (StatisticsDaily sta : staList) {
            dateList.add(sta.getDateCalculated());
            //判断查询的是哪个类型的数剧
            switch(type){
                case "login_num": countList.add(sta.getLoginNum());
                    break;
                case "register_num": countList.add(sta.getRegisterNum());
                    break;
                case "video_view_num": countList.add(sta.getVideoViewNum());
                    break;
                case "course_num": countList.add(sta.getCourseNum());
                    break;
                default:
                    break;
            }
        }

        Map map  = new HashMap<>();
        map.put("dateList",dateList);
        map.put("countList",countList);

        return map;
    }

以map形式返回,封装日期和人数进行显示
 

你可能感兴趣的:(SpringCloud,SpringBoot2,微服务,java,spring,cloud)