admin管理员组

文章数量:1565360

谷粒商城笔记

  • 1. Mybatis_plus复习
    • 1.1 数据库配置(字符编码设置)***
    • 1.2 mybatis_plus日志
    • 1.3 mybatis_plus主键策略
    • 1.4 分页查询
    • 1.5 逻辑删除**
    • 1.6 条件构造器
    • 1.7 Mp封装Server层***
    • 1.8 自动填充
    • 1.9 乐观锁(**)
    • 总结
  • 2.项目搭建(父工程)
    • 2.1 配置pom文件
    • 2.2 子项目(service)
  • 3.service-edu模块
    • 3.1 代码生成器
    • 3.2 讲师列表(mapper注入)
    • 3.3 json时间格式
    • 3.4 逻辑删除讲师
  • 4.整合swagger
    • 不同工程引入问题
    • swagger注解
  • 5.统一返回类型(swagger新注解)
  • 6.get post传参
  • 7.前端传递JSON数组
  • 8.讲师条件分页查询
    • 问题
  • 9.添加讲师
  • 10.修改讲师
  • 11.统一异常处理类
    • 全局异常处理
    • 特定异常处理
    • 自定义异常处理
  • 12.统一日志处理
    • 配置日志级别
    • 日志输出到文件
    • 将错误日志输出到文件
  • 13.ES6基本语法
    • let,var使用
    • 解构赋值
    • 模板字符串
    • 声明对象
    • 对象拓展运算符
    • 首次创建vue对象
    • 修饰符
    • es6模块化开发(export,export default)
    • Webpack
      • webpack打包js文件
      • 打包css文件
      • 总结
  • 14.前端模板文件分析
    • src目录
    • 前端入口文件
    • config目录
  • 15.登录(跨域)
    • vue前端功能实现过程
    • 登录改造本地
    • 解决跨域问题
  • 16.路由相关
    • 路由知识
    • 创建教师路由
  • 17.前端
    • 讲师列表(CRUD+element-ui)
      • 路由及页面初始化
      • 查询讲师
      • 删除讲师
      • 添加修改讲师
      • 路由传参
  • 18.对象存储阿里云oss
    • 开通对象存储OSS服务
    • 创建Bucket
    • 上传默认头像
    • java使用阿里云OSS
  • 19.搭建阿里云后台环境
  • 20.实现文件上传
    • 从配置文件读取常量
    • 上传文件
    • 问题
  • 21.nginx
  • 22.头像上传
  • 23.EasyExcel读写文件
    • EasyExcel写操作
    • EasyExcel读操作
  • 24.excel添加课程分类(后端)
    • 代码生成器
    • easyExcel读取
    • 总结
  • 25.添加课程分类前端
  • 26.课程分类列表
    • 前端
    • 后端
  • 27.课程添加
    • 后端
    • 课程信息
    • 课程章节大纲
    • 课程发布(***)
    • el-select
  • 28.课程列表
  • 29.视频点播
    • 视频上传实例
    • 视频上传项目创建
    • 启动类配置数据库问题
    • 文件上传后端
    • 文件上传nginx配置(*****)
    • 上传视频前端
    • 删除视频
  • spring cloud
    • nacos注册中心
    • feign

1. Mybatis_plus复习

mybatis_plus是好久之前学习的,现在已经有点遗忘了,还好老师讲了一些知识点。

(1)导入mybatis_plus的依赖。
springboot对应的mybatis_plus依赖,一定要选带starter启动器的。

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>最新版本</version>
</dependency>
<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
</dependency>

个人遇到的错误
打开mybatis_plus首页发现最新版本,直接复制粘贴,后来一直报错,最后才发现引入的时spring对应的mybatis_plus依赖,不是springboot的。说多了都是泪呀

1.1 数据库配置(字符编码设置)***

(2)配置文件配置数据库连接,一定要加characterEncoding=utf8,否则在查询时会失败。

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/guli?characterEncoding=utf8&serverTimezone=GMT%2B8
    username: root
    password: 747699

上面在引入mysql依赖的时候没有表明是mysql5还是mysql8,所以会使用自己电脑的版本。
com.mysql.cj.jdbc.Driver 这里的cj表示默认使用mysql8的驱动。
spring boot2版本使用的是mysql8的驱动。

(3)创建实体类User

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

(4)创建mapper接口类(核心)

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

@Mapper注解表示将我们的mapper注入到容器中。
每个自定义的mapper接口都要继承BaseMapper,< User >表示mapper接口操作的表,也可以理解为实体类。 BaseMapper是mybatis_plus为我们封装好了许多常用数据库操作函数。

(5)测试
测试时不小心将自己的测试类删除了,使用注解@SpringBootTest手动创建了测试类,但是在注入bean时,出现一个错误,提示没有找到userMapper。

原因:注解中没有加启动类,即@SpringBootTest(classes = 启动类.class)。因为学习springboot有讲过,启动类中的run函数会返回IOC容器,里面有我们注入的所有bean。只有和启动类同一包下的注解才可以扫描,然后注入到容器中。那么在使用bean时,应该也在同一包下,如果不在同一包下,应该加入上面的注解。但是,当初始化创建一个项目时,创建好的测试类只有@SpringBootTest也可以使用容器中的Bean。


总之使用了上面的注解可以正常使用bean组件,但是具体原因还不确定。


如果没有删除初始化的测试类,下面的代码不会报错

@SpringBootTest
class MybatisPlusApplicationTests {

    @Autowired
    UserMapper userMapper;
    @Test
    void contextLoads() {
        List<User> users = userMapper.selectList(null);
    }

}

其它相关操作mybatis_plus官方文档有相应介绍。

1.2 mybatis_plus日志

日志是方便我们在程序出现错误时,及时发现错误的原因。
在配置文件中设置开启

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

执行mybatis_plus插入操作时,控制台日志输出

1.3 mybatis_plus主键策略

通过@TableId注解实现,为什么说这是mybatis_plus主键策略呢,因为该注解的来源如下:
import com.baomidou.mybatisplus.annotation.TableId;
使用主键策略前提时,在数据库表中,将主键设置为自增。
主键策略如下所示:

NONE:该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)即默认策略。

AUTO:数据库ID自增。增加幅度为1。较为常用

INPUT:用户输入ID,该类型可以通过自己注册自动填充插件进行填充。

ASSIGN_ID:分配ID (主键类型为number或string),number类型较为常用,会默认为我们分配ID,也遵循自动增长原则,但是增长的幅度不确定。

ASSIGN_UUID:分配UUID (主键类型为 string) ,主键中一定包含字符,对于需要主键排序的情况不适用,所以不如ASSIGN_ID常用。
总结
一般在插入数据时,我们是不会设置主键值,所以此时主键策略起到了很大作用。一帮使用ASSIGN_ID或AUTO策略。
例如:在user类的主键加入逐渐策略

@TableId(type = IdType.ASSIGN_ID)
private Long id;

执行插入时,就可以不用设置主键值,系统会通过策略自动生成主键id。下面是一条插入示例

 void insert(){
        User user = new User();
        user.setAge(23);
        user.setEmail("mengfanxiaonb@gmail");
        user.setName("mfx");

        int insert = userMapper.insert(user);
        System.out.println(insert);
    }

执行两次插入操作:

可以发现两次的id都是随机的,但是有一个特点都是自增,但是增加的幅度不同。

1.4 分页查询

(1)配置分页插件配置文件,创建MpConfig配置类,Mp=Mybatis_plus。

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

(2)分页查询
selectPage函数执行分页查询操作,两个参数:
Page:使用Page类设置起始页和每页数据条数,(1,3)表示第一页,每页包含三条数据。
QueryWrapper:查询条件,具体见官方文档。

Page<User> userPage = userMapper.selectPage(new Page<>(1,3), null);
// 根据page查询到的数据
System.out.println(userPage.getRecords());
// 表中数据总条数
System.out.println(userPage.getTotal());
// 总页数
System.out.println(userPage.getPages());
// 每页大小
System.out.println(userPage.getSize());
// 是否有下一页
System.out.println(userPage.hasNext());
// 是否由前置页
System.out.println(userPage.hasPrevious());

结果如下

注意:如果不实现分页插件配置类,也可以查询,只是每次查询得到的结果都是所有数据。即能查询,不能分页查询。

1.5 逻辑删除**

最初接触的删除操作是直接将数据库中数据删除,也可以成为物理删除。但是实际中使用的时逻辑删除,原理为修改操作。
逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。
如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示。

使用方法
由于使用的版本时旧版本,所以逻辑删除的配置会比较繁琐。最新版本mp不需要注入bean操作。
(1)配置文件设置逻辑删除规则

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

(2)实体类添加逻辑删除字段,并添加逻辑删除注解。通过自动填充,创建对象时将delete字段自动填充为0。

@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer deleted;

自动填充配置类insertFill函数添加配置代码:

@Override
public void insertFill(MetaObject metaObject) {
 ......
this.setFieldValByName("deleted", 0, metaObject);
}

(3)数据库表中添加字段deleted
(4)配置类注册bean

  @Bean
    public ISqlInjector sqlInjector(){
        return new LogicSqlInjector();
    }

(5)测试

先插入数据

删除最后一条数据

逻辑删除成功,将最后一条数据的deleted字段设置为1.

再次执行查询所有数据操作

总结:逻辑删除并不会删除该条数据,只是在查询时无法查询而已,方便我们后续的数据恢复。

1.6 条件构造器

主要使用的有个类:QueryWrapperLambdaQueryWrapper

QueryWrapper较为简单,直接参考官方文档:QueryWrapper

LambdaQueryWrapperQueryWrapper类似,但是支持Lambda表达式,较为新颖。

示例:
查询用户名为mfx的数据
QueryWrapper写法

 void query(){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("name","mfx");
        List<User> users = userMapper.selectList(wrapper);
        System.out.println(users);

    }

LambdaQueryWrapper`写法

void lambdaQuery(){
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getName,"mfx");
        List<User> users = userMapper.selectList(wrapper);
        System.out.println(users);

    }

而这唯一的区别就是在条件查询函数中第一个参数的书写形式不同。个人觉得LambdaQueryWrapper好用一些,不易出错。

注意:Mp提供的这些函数只适用于单表操作,当涉及到多表的复杂业务时需要xml文件mybatis实现。

1.7 Mp封装Server层***

Mp不仅对mapper实现了封装,也对server层进行了封装。
正常后端代码的步骤是:controller 层调用server层,server层调用mapper层。但是如果只是简单的单表增删改查,可以只是用server层实现,因为Mp对server层进行了封装。

(1)创建server接口,要继承IService,原理和mapper继承baseMapper相同

public interface UserServer extends IService<User> {
}

(2)创建serverImp,@Service注入容器,要继承 ServiceImpl<Usermapper, User>,并且实现server接口

@Service
public class UserServerImp extends ServiceImpl<UserMapper, User> implements UserServer {
}


ServiceImpl继承了BaseMapper,并且实现了IService,所以本质上还是mapper对表的操作,只是Mp对server层的封装。我们可以直接使用server代替之前mapper对表的操作。

测试:

  @Autowired
    UserServer userServer;
    @Test
    void userServers(){
        List<User> list = userServer.list();
        System.out.println(list);
    }

上面list是Mp为我们封装好的查询所有数据的方法,相当于mapper的selectList方法。

总结
对于接口的实现,有两种方法。
(1)只是简单单表操作:在controller注入server,直接执行对数据库的操作。
(2)涉及到复杂操作,mybatis通过xml实现对表的操作,在server中注入mapper,在contorller中注入server,最后对数据库进行操作。
(3)Mp的意义就是要简化我们的操作和代码书写,但是只针对于简单的单表操作,正常开发中还是需要(2)中的步骤来实现一个接口。

1.8 自动填充

每一张表都会设置两个字段,创建时间和更新时间。如果我们插入数据时,我们需要每次set两个时间,然后insert。但是,这项工作是重复的,所以mp帮我们实现这一步骤,也就是自动填充。
自动填充官网


自动填充分三步:

  1. 数据库创建两个字段create_time,update_time;
  2. 实体类添加两个字段createTime,updateTime,并添加注解@TableField
  3. 创建配置类,并注入到容器;

实体类添加字段

public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
    @TableLogic
    private Integer deleted;

    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
}

配置类

package com.example.mybatis_plus.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @Author: mfx
 * @Description:
 * @Date: Created in 15:08 2022/8/11
 */
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.setFieldValByName("createTime", new Date(),metaObject);
        this.setFieldValByName("updateTime", new Date(),metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.setFieldValByName("updateTime", new Date(),metaObject);
    }
}

效果展示

1.9 乐观锁(**)

(1)乐观锁
首先来看乐观锁,顾名思义,乐观锁就是持比较乐观态度的锁。就是在操作数据时非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但是在更新的时候会判断在此期间别的线程有没有更新过这个数据。
(2)悲观锁(一般不用)
反之,悲观锁就是持悲观态度的锁。就在操作数据时比较悲观,每次去拿数据的时候认为别的线程也会同时修改数据,所以每次在拿数据的时候都会上锁,这样别的线程想拿到这个数据就会阻塞直到它拿到锁。


根据乐观锁的定义,我们可以知道要使用version版本号实现乐观锁。
mp实现乐观锁:

  1. 表中添加创建version字段;
  2. 实体类添加对应字段,使用@Version注解,并使用@TableField自动填充默认值为1;
@Version
@TableField(fill = FieldFill.INSERT)
private Integer version;
  1. mpConfig配置插件(注入Bean)
// Spring Boot 方式
@Configuration
public class MybatisPlusConfig {
    /**
     * 旧版
     */
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInterceptor();
    }

    /**
     * 新版
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

测试:
先添加一条数据


修改数据后version变为了2,每修改一次就会+1。

先查询获取版本号,修改后和数据库中得版本号比对,一致就修改,必须先查后改

void update(){
        User user = userMapper.selectById(1542771054261653511L);
        user.setName("mfx111");
        
        int i = userMapper.updateById(user);
        System.out.println(i);
    }

使用sql语句不查询直接修改
update user set name = ‘version_test’ where id = 1542771054261653511
发现name修改成功,但是version没有+1,即乐观锁没有起作用。所以使用乐观锁,必须先查后改。

总结

Mp特性:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer2005、SQLServer 等多种数据库
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 XML 热加载:Mapper 对应的 XML 支持热加载,对于简单的 CRUD 操作,甚至可以无 XML 启动
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 支持关键词自动转义:支持数据库关键词(order、key…)自动转义,还可自定义关键词
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
  • 内置 Sql 注入剥离器:支持 Sql 注入剥离,有效预防 Sql 注入攻击

2.项目搭建(父工程)

glkt表示谷粒课堂。利用maven搭建项目,大致模块划分如下:

创建父工程guli_parent,spring boot使用2.2.1版本。

总结:
(1)guli-parent为父工程,里面定义依赖版本和依赖管理,不涉及使用依赖,不需要src文件夹;
(2)service为父工程下的一个子模块,由于其下面还有子模块,所以service是父模块,也不需要src文件夹,service使用父工程管理的部分依赖,service的子模块可以直接使用service中的依赖;
(3)service-edu为service中的子模块,需要src,里面写一些接口。由于service已经引入了依赖,所以service-edu中不需要再引入依赖。

2.1 配置pom文件

<artifactId>guli-parent</artifactId>后面加上下面代码

<packaging>pom</packaging>

<packaging>pom</packaging>含义:

项目的打包类型pom、jar、war
父工程都要设置为pom类型,pom项目不存放java代码,只适用于聚合子项目和传递项目依赖。

建立父工程,父工程负责管理整个项目的依赖。将父工程初始删除,替换为如下部分。
内容包括:定义版本号,根据定义的版本号导入依赖。
因为在父工程中定义了依赖的版本号,所以后续子模块只需要引入依赖,不需要加版本号。

	<properties>
        <java.version>1.8</java.version>
        <guli.version>0.0.1-SNAPSHOT</guli.version>
        <mybatis-plus.version>3.0.5</mybatis-plus.version>
        <velocity.version>2.0</velocity.version>
        <swagger.version>2.7.0</swagger.version>
        <aliyun.oss.version>2.8.3</aliyun.oss.version>
        <jodatime.version>2.10.1</jodatime.version>
        <poi.version>3.17</poi.version>
        <commons-fileupload.version>1.3.1</commons-fileupload.version>
        <commons-io.version>2.6</commons-io.version>
        <httpclient.version>4.5.1</httpclient.version>
        <jwt.version>0.7.0</jwt.version>
        <aliyun-java-sdk-core.version>4.3.3</aliyun-java-sdk-core.version>
        <aliyun-sdk-oss.version>3.1.0</aliyun-sdk-oss.version>
        <aliyun-java-sdk-vod.version>2.15.2</aliyun-java-sdk-vod.version>
        <aliyun-java-vod-upload.version>1.4.11</aliyun-java-vod-upload.version>
        <aliyun-sdk-vod-upload.version>1.4.11</aliyun-sdk-vod-upload.version>
        <fastjson.version>1.2.28</fastjson.version>
        <gson.version>2.8.2</gson.version>
        <json.version>20170516</json.version>
        <commons-dbutils.version>1.7</commons-dbutils.version>
        <canal.client.version>1.1.0</canal.client.version>
        <docker.image.prefix>zx</docker.image.prefix>
        <cloud-alibaba.version>0.2.2.RELEASE</cloud-alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!--Spring Cloud-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--mybatis-plus 持久层-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>

            <!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
            <dependency>
                <groupId>org.apache.velocity</groupId>
                <artifactId>velocity-engine-core</artifactId>
                <version>${velocity.version}</version>
            </dependency>

            <!--swagger-->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${swagger.version}</version>
            </dependency>
            <!--swagger ui-->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${swagger.version}</version>
            </dependency>

            <!--aliyunOSS-->
            <dependency>
                <groupId>com.aliyun.oss</groupId>
                <artifactId>aliyun-sdk-oss</artifactId>
                <version>${aliyun.oss.version}</version>
            </dependency>

            <!--日期时间工具-->
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>${jodatime.version}</version>
            </dependency>

            <!--xls-->
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi</artifactId>
                <version>${poi.version}</version>
            </dependency>
            <!--xlsx-->
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi-ooxml</artifactId>
                <version>${poi.version}</version>
            </dependency>

            <!--文件上传-->
            <dependency>
                <groupId>commons-fileupload</groupId>
                <artifactId>commons-fileupload</artifactId>
                <version>${commons-fileupload.version}</version>
            </dependency>

            <!--commons-io-->
            <dependency>
                <groupId>commons-io</groupId>
                <artifactId>commons-io</artifactId>
                <version>${commons-io.version}</version>
            </dependency>

            <!--httpclient-->
             <dependency>
                <groupId>org.apache.httpcomponents</groupId>
                <artifactId>httpclient</artifactId>
                <version>${httpclient.version}</version>
            </dependency>

            <dependency>
                <groupId>com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
                <version>${gson.version}</version>
            </dependency>

            <!-- JWT -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>${jwt.version}</version>
            </dependency>

            <!--aliyun-->
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-core</artifactId>
                <version>${aliyun-java-sdk-core.version}</version>
            </dependency>
            <dependency>
                <groupId>com.aliyun.oss</groupId>
                <artifactId>aliyun-sdk-oss</artifactId>
                <version>${aliyun-sdk-oss.version}</version>
            </dependency>
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-vod</artifactId>
                <version>${aliyun-java-sdk-vod.version}</version>
            </dependency>
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-vod-upload</artifactId>
                <version>${aliyun-java-vod-upload.version}</version>
            </dependency>
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-sdk-vod-upload</artifactId>
                <version>${aliyun-sdk-vod-upload.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
            </dependency>
            <dependency>
                <groupId>org.json</groupId>
                <artifactId>json</artifactId>
                <version>${json.version}</version>
            </dependency>

            <dependency>
                <groupId>commons-dbutils</groupId>
                <artifactId>commons-dbutils</artifactId>
                <version>${commons-dbutils.version}</version>
            </dependency>

            <dependency>
                <groupId>com.alibaba.otter</groupId>
                <artifactId>canal.client</artifactId>
                <version>${canal.client.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

2.2 子项目(service)

在父工程下创建service模块,前面父工程中只是定义了依赖管理即dependencyManagement,依赖的使用在service使用。service下面也有许多子模块,所以也要变为pom项目。即 节点后面添加 pom类型。

使用依赖,并将前四个依赖注释掉,因为现在还不是用,如果不注释会报错。

	<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <!--hystrix依赖,主要是用 @HystrixCommand -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

         <!--服务注册-->
         <dependency>
            <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
         </dependency>
         <!--服务调用-->
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
         </dependency>

        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

         <!--mybatis-plus-->
         <dependency>
             <groupId>com.baomidou</groupId>
             <artifactId>mybatis-plus-boot-starter</artifactId>
         </dependency>

         <!--mysql-->
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             </dependency>

         <!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
         <dependency>
             <groupId>org.apache.velocity</groupId>
             <artifactId>velocity-engine-core</artifactId>
            </dependency>

         <!--swagger-->
         <dependency>
             <groupId>io.springfox</groupId>
             <artifactId>springfox-swagger2</artifactId>
             </dependency>
         <dependency>
             <groupId>io.springfox</groupId>
             <artifactId>springfox-swagger-ui</artifactId>
         </dependency>

         <!--lombok用来简化实体类:需要安装lombok插件-->
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
         </dependency>

         <!--xls-->
         <dependency>
             <groupId>org.apache.poi</groupId>
             <artifactId>poi</artifactId>
         </dependency>

         <dependency>
             <groupId>org.apache.poi</groupId>
             <artifactId>poi-ooxml</artifactId>
         </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
        </dependency>

        <!--httpclient-->
        <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
             </dependency>
         <!--commons-io-->
         <dependency>
             <groupId>commons-io</groupId>
             <artifactId>commons-io</artifactId>
             </dependency>
         <!--gson-->
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>
         </dependency>

         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.12</version>
         </dependency>
    </dependencies>

3.service-edu模块

在service中创建service-edu模块
配置文件

server:
  port: 8001

# 服务名
spring:
  application:
    name:  service-edu

  # 环境设置:dev、test、prod
  profiles:
    active: dev

  # mysql数据库连接
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
    username: root
    password: 747699
    
#mybatis日志
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3.1 代码生成器

可以利用代码生成器为数据库中的某一张自动生成对应的entity、Controller、service、mapper类。
可以将以下代码在test中运行。
核心部分:

  • setOutputDir:生成Controller、service、mapper层代码的存放位置;
  • setUrl:数据库连接;
  • setDriverName:数据库驱动;
  • setUsername:用户名;
  • setPassword:密码;
  • PackageConfig:包配置;
  • setInclude:表名。
    1、输出路径
    2、逐渐策略(根据数据库表字段设置)
    3、数据库连接字段

代码生成器依赖:

<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
</dependency>

代码生成器代码:

package com.atguigu.demo;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.Test;


public class CodeGenerator {

    @Test
    public void run() {

        // 1、创建代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 2、全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("mfx");
        gc.setOpen(false); //生成后是否打开资源管理器
        gc.setFileOverride(false); //重新生成时文件是否覆盖
        gc.setServiceName("%sService");	//去掉Service接口的首字母I
        gc.setIdType(IdType.ID_WORKER_STR); //主键策略
        gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
        gc.setSwagger2(true);//开启Swagger2模式

        mpg.setGlobalConfig(gc);

        // 3、数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("747699");
        dsc.setDbType(DbType.MYSQL);
        mpg.setDataSource(dsc);

        // 4、包配置
        PackageConfig pc = new PackageConfig();
        pc.setModuleName("edu"); //模块名
        pc.setParent("com.atguigu");
        pc.setController("controller");
        pc.setEntity("entity");
        pc.setService("service");
        pc.setMapper("mapper");
        mpg.setPackageInfo(pc);

        // 5、策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setInclude("edu_teacher");
        strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
        strategy.setTablePrefix(pc.getModuleName() + "_"); //生成实体时去掉表前缀

        strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
        strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作

        strategy.setRestControllerStyle(true); //restful api风格控制器
        strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符

        mpg.setStrategy(strategy);


        // 6、执行
        mpg.execute();
    }
}

实现效果:

3.2 讲师列表(mapper注入)

编写controller接口

@RestController
@RequestMapping("/edu/teacher")
public class TeacherController {

    @Autowired
    private TeacherService teacherService;

    @GetMapping("findAll")
    public List<Teacher> findAll(){
        return teacherService.list(null);
    }
}

由于我们是创建的maven项目,所以还没有启动类,所以要创建一个启动类

@SpringBootApplication

public class EduApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduApplication.class);
    }
}

如果此时执行启动类,会出现bean工厂中找不到teacherService
原因:teacherService需要使用teacherMapper,代码生成器生成的mapper接口没有注入到容器中。
解决方法:

  1. 在每个mapper接口类上加入@Mapper注解;
  2. 创建配置类,通过@MapperScan注解扫描注入所有mapper
@Configuration
@MapperScan("com.atguigu.edu.mapper")
public class EduConfig {
}

3.3 json时间格式

时间格式问题:返回的数据中对于修改时间字段信息显示如下:"gmtModified":"2019-11-12T05:36:36.000+0000",这是默认的标准时间格式,但是,这与我们正常的时间格式不同,只需要在配置文件中配置一下json时间格式即可。

spring:
	 jackson:
	    date-format: yyyy-MM-dd HH:mm:ss
	    time-zone: GMT+8

修改后的时间显示"gmtModified":"2019-11-12 13:36:36"

3.4 逻辑删除讲师

前面第一章节mp中已经讲过了逻辑删除的步骤。所以这里就不贴代码了。
步骤:配置文件配置规则、@TableLogic、controller接口

4.整合swagger

由于浏览器只支持get post请求测试,所以如果有其它请求时,无法通过浏览器测试,如果使用postman测试,还需要下载软件。所以可以通过swagger进行接口测试。

swagger配置流程

  • 在父工程下建立一个common模块;
  • common模块下建立一个services-base模块;
  • services-base模块中定义一个swagger配置类;
  • 由于我们要在教师所在模块service-edu使用,所以我们需要先将services-base引入到service模块,由于service-eduservice的子模块,所以service-edu可以使用该swagger配置类。补充:service是我们书写具体业务的模块;
  • 为了使得swagger配置类生效,我们还需要在启动类上加一个组件扫描,使得从其他模块导入的配置类可以成功配置

具体实现
(1)创建common模块,并导入swagger相关依赖,由于common也是父模块,所以加入 <packing>pom</packing>
common pom文件

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>provided </scope>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
             <groupId>com.baomidou</groupId>
             <artifactId>mybatis-plus-boot-starter</artifactId>
             <scope>provided </scope>
        </dependency>

        <!--lombok用来简化实体类:需要安装lombok插件-->
        <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
             <scope>provided </scope>
        </dependency>

         <!--swagger-->
        <dependency>
             <groupId>io.springfox</groupId>
             <artifactId>springfox-swagger2</artifactId>
             <scope>provided </scope>
        </dependency>
         <dependency>
             <groupId>io.springfox</groupId>
             <artifactId>springfox-swagger-ui</artifactId>
             <scope>provided </scope>
         </dependency>

         <!-- redis -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>

         <!-- spring2.X集成redis所需common-pool2
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.0</version>
         </dependency>-->
    </dependencies>

(2)创建子模块service-base,并创建swagger配置类所在位置
Swagger2Config

package com.atguigu.ggkt.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * @Author: mfx
 * @Description:
 * @Date: Created in 20:08 2022/7/5
 */

@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket webApiConfig(){
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("ggkt")
                .apiInfo(webApiInfo())
                .select()
                //只显示api路径下的页面
                //.paths(Predicates.and(PathSelectors.regex("/api/.*")))
                .build();
    }

    private ApiInfo webApiInfo(){
        return new ApiInfoBuilder()
                .title("网站-API文档")
                .description("本文档描述了网站微服务接口定义")
                .version("1.0")
                .contact(new Contact("atguigu", "http://atguigu", "atguigu"))
                .build();
    }
}

(3)在service中引入该模块

<!-- 引入common内的service-base模块,service-base中引入了common-utils模块-->
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>service-base</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

(4)在ServiceVod模块启动类上加入注解@ComponentScan("com.atguigu")
(5)访问swagger文档,swagger访问方式为http://localhost:端口号/swagger-ui.html

不同工程引入问题

上面edu模块引入了commons中swagger,由于跨工程,需要导入工程依赖,还需要在启动类上添加注解@ComponentScan("com.atguigu")

swagger注解

swagger还提供了多个注解,@ApiOperation、@ApiParam,分被用于描述方法和方法参数
例如

    @ApiOperation("查询所有讲师")
    @GetMapping ("/findAll")
    public List<Teacher> findAllTeacher(){
        return teacherService.list();
    }

    @DeleteMapping("remove/{id}")
    public boolean removeById(@ApiParam(name = "id", value="ID", required = true) @PathVariable Long id){
        boolean isSuccess = teacherService.removeById(id);
        return isSuccess;
    }

5.统一返回类型(swagger新注解)

@ApiModel,@ApiModelProperty两个注解分别加在类名和类属性名上。

统一返回类型Result:

package com.atguigu.utils;

import com.sun.org.apache.xpath.internal.operations.Bool;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author: mfx
 * @Description:
 * @Date: Created in 19:32 2022/8/16
 */
@Data
public class Result {
    @ApiModelProperty(value = "是否成功")
    private Boolean success;

    @ApiModelProperty(value = "状态码")
    private Integer code;

    @ApiModelProperty(value = "消息")
    private String message;
    
    @ApiModelProperty(value = "返回数据")
    private Map<String, Object> data = new HashMap<>();


    // 构造函数私有化,保证只能使用静态方法
    private Result(){}

    public static Result ok(){
        Result res = new Result();
        res.setCode(20000);
        res.setSuccess(true);
        res.setMessage("成功");

        return res;
    }

    public static Result error(){
        Result result = new Result();
        result.setCode(20001);
        result.setSuccess(false);
        result.setMessage("失败");

        return result;
    }

    public Result success(Boolean success){
        this.success = success;
        return this;
    }

    public Result message(String msg){
        this.message = msg;
        return this;
    }

    public Result code(Integer code){
        this.code = code;
        return this;
    }

    public Result data(String str, Object obj){
        this.data.put(str, obj);
        return this;
    }

    public Result data(HashMap<String, Object> map){
        this.setData(map);
        return this;
    }
}


上面代码中有一个知识点,类方法通过return this,可以实现链式编程。
例如:Result.ok(..).message(...).code(..)

在网页访问swagger时出现错误
swagger报错java.lang.NumberFormatException: For input string: ““

解决方法:
我使用的是io.springfox:springfox-swagger2:2.9.2的版本,而该版本依赖了swagger-models的1.5.20版本(会导致报此错),深挖原因是1.5.20版本中的example只判断了是否为null,没有判断是否为空串;1.5.21版本对两者都进行了判断。

<!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.5.21</version>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
            <version>1.5.21</version>
        </dependency>

6.get post传参

注意:只要是路径传参,后端必须@PathVariable接收,data传参必须@RequestBody接收。不然后端接收到的值为空。使用@RequestBody时应设置参数可以为空,即@RequestBody(require=false)
get post是常用的两种传参方式,参数存放的位置有两个:params,data。
如果使用data传参,请一定使用post方式,因为get可能不能接受@RequestBody的参数
params传参即将参数放在请求路径中,get,post都可以使用,使用方法相同。

对应的接受方式:
(1)基础类型接收,名字对应即可;

// method
const params = {
    id: '123456789',
    name: '张三'
}
test(params)

// api
export function test (params) {
  return axios({
    url: url,
    method: 'GET',
    params: params
  })
}

// 后台
@PostMapping("/test")
public Result test(Long id, String name) {
    return Res.ok();
}

(2)使用Map接收,需要添加 RequestParam 注解;

// method
const params = {
    id: '123456789',
    name: '张三'
}
test(params)

// api
export function test (params) {
  return axios({
    url: url,
    method: 'POST',
    params: params
  })
}

// 后台
@PostMapping("/test")
public Result test(@RequestParam Map<String, Object> map) {
    return Res.ok();
}

(3)使用实体类接收。

// 实体类
@Data
public class TestEntity {
    Long id;
    String name;
}

// method
const params = {
    id: '123456789',
    name: '张三'
}
test(params)

// api
export function test (params) {
  return axios({
    url: url,
    method: 'POST', 
    params: params
  })
}

// 后台
@PostMapping("/test")
public Result test(TestEntity testEntity) {
    return Res.ok();
}

(4)接收列表元素,需要@RequestParam


// method
const list= [a,b,c,d]
test(params)

// api
export function test (list) {
  return axios({
    url: url,
    method: 'GET', 
    params: list
  })
}

// 后台
@PostMapping("/test")
public Result test(@RequestParam("list") List<泛型> list) {
    return Res.ok();
}

data传参是将参数放在请求体里面,正常情况只有post可以使用。
对应的接收方式:
使用实体类接收

// 实体类
@Data
public class TestEntity {
    Long id;
    String name;
}

// method
const params = {
    id: '123456789',
    name: '张三'
}
test(params)

// api
export function test (params) {
  return axios({
    url: url,
    method: 'POST', 
    data: params
  })
}

@PostMapping("/test")
public Result test(@RequestBody TestEntity testEntity) {
    return Res.ok();
}

7.前端传递JSON数组

如果前端传送的json数组,后端应该使用List对象接受,因为是对象,所以要用@RequestBody注解

以批量删除讲师为例

 // 前端传递的参数是json数组[1,2,3...]
    @ApiOperation("批量删除讲师")
    @DeleteMapping("removeBatch")
    public Result removeBatch(@RequestBody List<Long> idList){
        teacherService.removeByIds(idList);
        return Result.ok(null);
    }

8.讲师条件分页查询

(1)创建条件类

@ApiModel(value = "teacher条件查询类")
@Data
public class TeacherQuery {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "教师名称,模糊查询")
    private String name;
    @ApiModelProperty(value = "头衔 1高级讲师 2首席讲师")
    private Integer level;
    @ApiModelProperty(value = "查询开始时间", example = "2019-01-01 10:10:10")
    private String begin;//注意,这里使用的是String类型,前端传过来的数据无需进行类型转换
    @ApiModelProperty(value = "查询结束时间", example = "2019-12-01 10:10:10")
    private String end;
}

(2)contorller

	@ApiOperation("条件分页查询讲师")
    @PostMapping("pageListByQuery/{current}/{limit}")
    public Result pageListByQuery(@PathVariable Long current, @PathVariable Long limit,
                                  @RequestBody(required = false) TeacherQuery teacherQuery)
    {
        Page<Teacher> page = new Page<>(current, limit);
        return teacherService.pageQuery(page,teacherQuery);
    }

(3)service

Result pageQuery(Page<Teacher> page, TeacherQuery teacherQuery);

(4)serviceImpl
注入teacherMapper时,编译器可能会提示没有这个bean的爆红,但是实际是有的,因为我们在配置类中通过MapperScan注入了所有的mapper,应该是编译器问题。

@Autowired
    TeacherMapper teacherMapper;

    @Override
    public Result pageQuery(Page<Teacher> page, TeacherQuery teacherQuery) {
        QueryWrapper<Teacher> wrapper = new QueryWrapper<>();
        wrapper.orderByAsc("sort");
        if(teacherQuery != null){
            String name = teacherQuery.getName();
            Integer level = teacherQuery.getLevel();
            String begin = teacherQuery.getBegin();
            String end = teacherQuery.getEnd();
            if(!StringUtils.isEmpty(name))
                wrapper.like("name",name);
            if(!StringUtils.isEmpty(level))
                wrapper.eq("level",level);
            if(!StringUtils.isEmpty(begin))
                wrapper.ge("gmt_create", begin);
            if(!StringUtils.isEmpty(end))
                wrapper.le("gmt_create", end);
        }
        System.out.println(teacherMapper);
        System.out.println(page);
        System.out.println(wrapper);
        IPage<Teacher> teacherIPage = teacherMapper.selectPage(page, wrapper);
        long total = teacherIPage.getTotal();
        List<Teacher> records = teacherIPage.getRecords();
        HashMap<String, Object> map = new HashMap<>();
        map.put("total", total);
        map.put("list", records);
        return Result.ok().data(map);
    }

(5)swagger测试

结果:

问题

在测试时,最初配置文件中的数据库连接没有设置数据库的字符编码,导致mp执行模糊查询时查不到结果,但是数据库使用相同sql则可以成功。
解决方法:
添加字符编码characterEncoding=utf8即可:
jdbc:mysql://localhost:3306/guli?characterEncoding=utf8&serverTimezone=GMT%2B8

9.添加讲师

添加讲师比较简单,唯一需要注意的是,要设置自动填充
自动填充在第一节有讲到,只需要@TableFiled注解和相关实体类。由于所有模块都会用到自动填充,所以在common模块下的service-base中创建自动填充的相关实体类,将其放在handler包中。

10.修改讲师

修改操作分为两步:

  1. 根据id查询;
  2. 修改数据。
@ApiOperation("根据id查询")
    @GetMapping("{id}")
    public Result getTeacher(@PathVariable String id){
        Teacher teacher = teacherService.getById(id);
        return Result.ok().data("item", teacher);
    }

    @ApiOperation("修改讲师信息")
    @PostMapping
    public Result updateById(@RequestBody Teacher teacher){
        boolean b = teacherService.updateById(teacher);
        return Result.ok();
    }

11.统一异常处理类

common工程下的service-base中创建except包,然后创建异常处理类。
由于异常类需要common-utils中的Result类,所以需要引入common-utils模块,又由于service模块之前即引入了common-utils,又引入了service-base模块,所以此时service可以修改为只需要引入service-base模块,因为service-base中包含commo-utils模块。原理是依赖传递
所有的异常处理函数都是放在统一异常处理类中,大都数成为全局异常处理类,为了防止和下面的全局异常处理冲突,暂且命名为统一异常处理类。

异常处理的顺序是,先看有没有特定的异常,如果没有才会全局异常处理。因为全局异常处理可以处理所有的异常,可以认为保底的异常处理手段。

@RestControllerAdvice//用于controller层异常捕获,并且返回类型为json类型
public class GlobalException {

}

后面所讲的全局异常、特定异常、自定义异常只是不同的异常处理函数,他们的区别只是异常类参数不同。

全局异常处理

  • @RestControllerAdvice,@ControllerAdvice用于标识为异常处理类,Rest返回类型为json类型。
  • @ExceptionHandler加在处理异常方法上,是该方法可以执行。
@RestControllerAdvice//用于controller层异常捕获,并且返回类型为json类型
public class GlobalException {

    @ExceptionHandler(Exception.class)
    public Result handler(Exception e){
        return Result.fail(null).message("全局异常处理");
    }
}

当异常处理函数和@ExceptionHandler的参数为Exception时,代表全局异常处理,因为Exception是所有异常的父类。所以后面特定异常处理只需要修改参数即可。

特定异常处理

特定异常处理只需要修改处理异常函数和处理异常注解的参数即可

// 特定异常处理示例:ArithmeticException
    @ExceptionHandler(ArithmeticException.class)
    public Result handler(ArithmeticException e){
        return Result.fail(null).message("执行ArithmeticException异常处理");
    }

自定义异常处理

自定义异常需要手动抛出。

(1)创建自定义异常类。
先看一下官方的异常类如何定义的:

由上图可知,创建自定义异常类,需要继承RuntimeException类。创建自己的异常类 GgktException

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

(2)在自定义类中创建属性,我们已经创建了两个属性code、msg
(3)在统一异常处理类中添加自定义异常处理方法。

 // 自定义异常处理 GgktException
    @ExceptionHandler(GgktException.class)
    public Result handler(GgktException e){
        e.printStackTrace();
        return Result.fail(null).code(e.getCode()).message(e.getMsg());
    }

(4)手动抛出自定义异常。
我们假设在查询所有讲师接口中尝试捕获某个异常,然后手动抛出。

  public Result findAllTeacher(){
        try{
            int i  = 10/ 0;
        }catch(Exception e){
            throw new GgktException(201, "执行自定义异常处理");
        }
        return Result.ok(teacherService.list()).message("查询数据成功");
    }

throw new GgktException(201, "执行自定义异常处理");就是手动抛出异常,此句执行后,就会被我们自定义的异常处理函数捕获,然后返回异常信息。

12.统一日志处理

配置日志级别

日志记录器(Logger)的行为是分等级的。如下表所示:
分为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL
默认情况下,spring boot从控制台打印出来的日志级别只有INFO及以上级别,可以配置日志级别
配置文件设置

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

日志输出到文件

默认的日志只输出在控制台,无法输出到文件,logback可以实现日志输出到文件。
(1)删除配置文件中有关日志的代码,包括logging和mp的logging。否则会出错。
(2)resources 中创建 logback-spring.xml(文件名字固定),因为我们目前使用的是service-edu项目,所以在该项目下的resources中创建。(这个配置文件使用时直接拿来用,只需要按需修改相关字段即可

<?xml version="1.0" encoding="UTF-8"?>
    <configuration scan="true" scanPeriod="10 seconds">
    <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设
置为WARN,则低于WARN的信息都不会输出 -->
    <!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值
为true -->
    <!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认
单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
    <!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查
看logback运行状态。默认值为false。 -->
    <contextName>logback</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入
    到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->

    <!--    日志输出路径-->
    <property name="log.path" value="D:/Develop/IDEA/work/guli_log/edu" />
    <!-- 彩色日志 -->
    <!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
    <!-- magenta:洋红 -->
    <!-- boldMagenta:粗红-->
    <!-- cyan:青色 -->
    <!-- white:白色 -->
    <!-- magenta:洋红 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level)
|%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>
    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或
        等于此级别的日志信息-->
        <!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日
        志,也不会被输出 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>INFO</level>
        </filter> <encoder> <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
        <!-- 设置字符集 -->
        <charset>UTF-8</charset>
    </encoder>
    </appender>
    <!--输出到文件-->
    <!-- 时间滚动输出 level为 INFO 日志 -->
    <appender name="INFO_FILE"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_info.log</file>
        <!--日志文件输出格式-->
        <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level
            %logger{50} - %msg%n</pattern> <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy
                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->

            <fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM�dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- 时间滚动输出 level为 WARN 日志 -->
    <appender name="WARN_FILE"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_warn.log</file>
        <!--日志文件输出格式-->
        <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level
            %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy
                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM�dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize>
        </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>warn</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch>
        </filter>

    </appender>
    <!-- 时间滚动输出 level为 ERROR 日志 -->
    <appender name="ERROR_FILE"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_error.log</file>
        <!--日志文件输出格式-->
        <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level
            %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy
                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM�dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize>
        </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--
    <logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指
    定<appender>。
    <logger>仅有一个name属性,
    一个可选的level和一个可选的addtivity属性。
    name:用来指定受此logger约束的某一个包或者具体的某一个类。
    level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL
    和 OFF,
    如果未设置此属性,那么当前logger将会继承上级的级别。
    -->
    <!--
    使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想
    要查看sql语句的话,有以下两种操作:
    第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过
    这样日志那边会出现很多其他消息
    第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打
    印,其他还是正常DEBUG级别:
    -->
    <!--开发环境:打印控制台-->
    <springProfile name="dev">
        <!--可以输出项目中的debug日志,包括mybatis的sql日志-->
        <logger name="com.guli" level="INFO" />
        <!--
        root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR,
        ALL 和 OFF,默认是DEBUG
        可以包含零个或多个appender元素。
        -->
        <root level="INFO"> <appender-ref ref="CONSOLE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
        </root>
    </springProfile>
    <!--生产环境:输出到文件-->
    <springProfile name="pro"> <root level="INFO"> <appender-ref ref="CONSOLE" />
        <appender-ref ref="DEBUG_FILE" />
        <appender-ref ref="INFO_FILE" />
        <appender-ref ref="ERROR_FILE" />
        <appender-ref ref="WARN_FILE" />
    </root>
    </springProfile>
</configuration>

控制台日志输出

日志文件输出路径即对应输出文件。
<property name="log.path" value="D:/Develop/IDEA/work/guli_log/edu" />

将错误日志输出到文件

java中错误一般都会引发异常,当我们出现异常时,后台一般只会抛出打印异常,并不会出现错误日志。如下图所示,没有错误日志。

如果想要将错误日志打印并输出到文件,下面两步操作。
(1)异常处理类上加@Slf4j注解
(2)log.error(e.message)输出错误日志,由于我们设置了将日志存储文件,所以日志只要输出就会存储到文件中。

相关代码:

package com.atguigu.except;

import com.atguigu.utils.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @Author: mfx
 * @Description:
 * @Date: Created in 14:22 2022/8/19
 */
@Slf4j
@RestControllerAdvice
public class GlobalException {
    // 全局异常处理
    @ExceptionHandler(Exception.class)
    public Result handler(Exception e){
        log.error(e.getMessage());
        return Result.error().message("执行了全局异常处理");
    }

    // 特定异常处理
    @ExceptionHandler(ArithmeticException.class)
    public Result handler(ArithmeticException e){
        log.error(e.getMessage());
        return Result.error().message("执行了特定异常ArithmeticException处理");
    }

    //自定义异常处理
    @ExceptionHandler(MyException.class)
    public Result handler(MyException e){
        e.printStackTrace();
        log.error(e.getMsg());
        return Result.error().code(e.getCode()).message(e.getMsg());
    }
}

测试:
自定义异常不仅抛出,而且还打印了错误日志,并且成功输入到了文件。


错误日志文件

13.ES6基本语法

let,var使用

let是ES6声明变量方式,var是ES5声明变量方式。

var 声明的变量没有局部作用域,let 声明的变量 有局部作用域

{
var a = 0
let b = 1
}
console.log(a)  // 0
console.log(b)  // ReferenceError: b is not defined

var 可以声明多次,let 只能声明一次

var m = 1
var m = 2
let n = 3
let n = 4
console.log(m)  // 2
console.log(n)  // Identifier 'n' has already been declared

解构赋值

解构赋值是对赋值运算符的扩展。他是一种针对数组或者对象进行模式匹配,然后对其中的变量进行赋值。在代码书写上简洁且易读,语义更加清晰明了;也方便了复杂对象中数据字段获取。

(1)数组解构赋值

 	// 传统
    let a = 1, b =2, c=3
    console.log(a,b,c)
    // ES6
    let [x, y, z] = [2, 4, 6]
    console.log(x,y,z)

(2)对象解构赋值

//2、对象解构
let user = {name: 'Helen', age: 18}
// 传统
let name1 = user.name
let age1 = user.age
console.log(name1, age1)
// ES6
let { name, age } =  user//注意:结构的变量必须是user中的属性
console.log(name, age)

解构赋值思想:将传统的多个语句赋值修改为一行代码的语句赋值。

模板字符串

模板字符串相当于加强版的字符串,用反引号 `,除了作为普通字符串,还可以在字符串中加入变量和表达式。

字符串插入变量和表达式。变量名写在 ${} 中,${} 中可以放入 JavaScript 表达式。

let name = "Mike"
let age = 27
let info = `My Name is ${name},I am ${age+1} years old next year.`
console.log(info)
// My Name is Mike,I am 28 years old next year.

字符串中调用函数

function f(){
    return "have fun!"
}
let string2 = `Game start,${f()}`
console.log(string2);  // Game start,have fun!

声明对象

在js中创建对象时,写key时可以不加双引号

 	let name = 'mfx'
    let age = 22
    // 传统写法
    let person = {name:name,age:age}
    console.log(person)
    // ES6
    let persons = {name,age}
    console.log(persons)

对象拓展运算符

拓展运算符...用于取出参数对象所有可遍历属性然后拷贝到当前对象。

 	// 1、对象复制
    let person = {name:'mfx',age:22}
    let person1 = {...person}
    console.log(person1)
    // 2、对象合并
    let persons = {grade:90,color:'yellow'}
    let person2 = {...person,...persons}
    console.log(person2)

输出结果

首次创建vue对象

正常开发中都是直接创建vue文件,然后使用vue框架进行编程。但是要理解vue的原理,还是要学会如何创建vue对象。因为创建vue文件也是创建的一个vue对象。
(1)创建vueDemo.html文件,并使用快捷键!+Tab插入html模板
(2)引入vue.js,我将vue.js放在了同级目录下

<body>
	<script src="vue.js"></script>
</body>

(3)在script中创建vue对象,new Vue({...})

<script>
    new Vue({
      el: '#app',
      data: {
        message: 'hello vue'
      }
    })
</script>

(4)创建div标签,并将id复制为Vue对象中el的值即app。名通过{{ }}获取vue对象的变量。全部代码如下:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
  >
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
<div id="app">
  {{message}}
</div>
  <script src = "vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      data: {
        message: 'hello vue'
      }
    })
  </script>
</body>
</html>

修饰符

vue修饰符详解
.prevent为例,.prevent表示 阻止标签默认行为
这里举一个使用例子方便理解:使用@click.prevent阻止超链接点击跳转事件。
写一个不自动跳转的超链接,代码如下:

<a href='https://www.baidu/' @click.prevent='click1()'>可能跳转到百度</a>
click1(){
	alert('没想到吧!')
}

这样,就不会跳转到百度了,阻止了浏览器的默认行为。

es6模块化开发(export,export default)

正常情况下,一个js中方法是私有的,是不可以在其他文件中使用的。为了解决该问题,es6可以使用exportexport default暴露文件中的某个方法,或对象,以供其它文件使用。

例如:
下面文件创建了vuex,我们想在其他文件中使用vuex,我们可以通过export default暴露该实例,在其他文件中直接import该文件路径即可。
注意:export default{要暴露的内容}

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

//对外暴露store的一个实例
export default new Vuex.Store({
    state:{},
    mutations:{},
    actions:{},
    
})

exportexport default使用

export使用时,每个对外暴露的实例对应一个export。如下所示,对外暴露一个方法,一个变量,所以需要两个export

test.js文件

export function list(){
	....
}
export let a = 10;

export的引用
方式一

import {a, list} from '...js'

方式二

// 这里的test要和js文件名对应
import * as test from 'test.js'
test.a     // 调用变量
test.list  // 调用函数

注意:export 不能直接写成这样子

export{
    "":""    // 这样会报错
    ....
}

export default使用
test.js文件

export default{
	a: 10,
	list: () => {...}
}

export default引用

import test from 'test.js'

注意export default不能类似这样的写 一样也是会报错的

export default let a=10   

总结:export和export default的定义方式不同,都不能使用对方的定义方法,引入方法也不同。export default更简洁一些,建议使用。一个js文件是可以有多个 export,但是一个js文件中只能有一个export default

Webpack

Webpack 是一个前端资源加载/打包工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。
从图中我们可以看出,Webpack 可以将多种静态资源 js、css、less 转换成一个静态文件,减少了页面的请求。

webpack 安装
npm install -g webpack webpack-cli

webpack打包js文件

(1)创建webpack文件夹,并创建多个js文件,用于打包测试

common.js

exports.info = function(str) {
  document.write(str)// 浏览器输出
}

utils.js

exports.add = function(a, b) {
  return a + b
}

main.js

import common from './common'
import utils from './utils'

common.info('hello common' + utils.add(1, 2))

(2)创建webpack.config.js文件

const path = require("path"); //Node.js内置模块
module.exports = {
	entry: './src/main.js', //配置入口文件
	output: {
		path: path.resolve(__dirname, './dist'), //输出路径,__dirname:当前文件所在路径
		filename: 'bundle.js' //输出文件
	}
}

(3)命令打包操作
webpack #有黄色警告
webpack --mode=development #没有警告
测试:进入webpackDeno文件终端目录执行webpack命令,成功打包,根据webpack.config.js文件设置,dist是打包好的目录,bundle.js是输出文件

(4)测试
创建一个html文件,引入打包好的js文件,会执行main.js中的操作
test.html
<script src="./bundle.js"></script>
成功输出main.js中内容:hello common3

打包css文件

(1)创建css文件style.css

body{
  background: red;
}

(2)main.js中引入css文件

require('./style.css')

(3)安装style-loader和css-loader

npm install --save-dev style-loader css-loader

(4)修改打包配置文件webpack.config.js

const path = require('path') // Node.js内置模块
module.exports = {
  entry: './src/main.js', // 配置入口文件
  output: {
    path: path.resolve(__dirname, './dist'), // 输出路径,__dirname:当前文件所在路径
    filename: 'bundle.js' // 输出文件
  },
  module: {
    rules: [
      {
        test: /\.css$/,	// 	打包规则应用到css结尾的文件上
        use: ['style-loader', 'css-loader']
      }
    ]
  }
}

(5)测试,运行tets.html文件
输出还是为hello common3,由于我们设置了css背景色为红色,所以这里的背景色为红色。

Module build failed: TypeError: this.getOptions is not a function at Object.loader错误:
安装完style-loader,css-loader后再打包时出现上面错误,原因是style-loader,css-loader版本过高。通过下面代码降低版本。

npm install css-loader@2.0.2 --save-dev
npm install style-loader@0.23.1  --save-dev

总结

上面是使用webpack打包,实际上我们是可以通过使用vue-cli脚手架的npm run build打包的。

14.前端模板文件分析

我们使用的前端模板是老版本,和视频中的一样,可能目录结构和新版本有所不同。

src目录

前端入口文件

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>vue-admin-template</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

main.js

import Vue from 'vue'

import 'normalize.css/normalize.css' // A modern alternative to CSS resets

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/en' // lang i18n

import '@/styles/index.scss' // global css

import App from './App'
import router from './router'
import store from './store'

import '@/icons' // icon
import '@/permission' // permission control

Vue.use(ElementUI, { locale })

Vue.config.productionTip = false

new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})

config目录


index.js是一些配置,包括端口号的设置等。dev,prod是两个运行环境,以dev为例

后面会将base_api更换为后端的地址。

15.登录(跨域)

vue前端功能实现过程

  1. 创建路由和vue页面
  2. 定义api
  3. 前端发出请求获取数据
  4. 数据展示

登录改造本地

(1)dev.env.js文件

将上面的base_api改为后端地址:http://localhost:8001,配置文件一旦重启,必须重启项目。

(2)vuex中的user.js中actions涉及两个登陆相关函数,Login,GetInfo。
根据代码中的信息,可以确定后断两个接口:登录和获取用户信息,登录接口返回token,用户信息接口返回角色、名称、头像。

actions: {
    // 登录
    Login({ commit }, userInfo) {
      const username = userInfo.username.trim()
      return new Promise((resolve, reject) => {
        login(username, userInfo.password).then(response => {
          const data = response.data
          setToken(data.token)
          commit('SET_TOKEN', data.token)
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

    // 获取用户信息
    GetInfo({ commit, state }) {
      return new Promise((resolve, reject) => {
        getInfo(state.token).then(response => {
          const data = response.data
          if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
            commit('SET_ROLES', data.roles)
          } else {
            reject('getInfo: roles must be a non-null array !')
          }
          commit('SET_NAME', data.name)
          commit('SET_AVATAR', data.avatar)
          resolve(response)
        }).catch(error => {
          reject(error)
        })
      })
    }
    }

(3)根据(2)中分析创建后端接口
此时创建的后端接口还不涉及数据库,只是返回给前端某个固定的自定义值,用户测试登陆改造是否成功

@RestController
@RequestMapping("/eduservice/user")
public class LoginController {

    @PostMapping("login")
    public Result login(){
        return Result.ok().data("token","admin");
    }

    @GetMapping("info")
    public Result info(){
        return Result.ok().data("roles","[admin]").data("name","admin")
        .data("avatar","https://guli-file-190513.oss-cn-beijing.aliyuncs/avatar/default.jpg");
    }
}

(4)将前端api中login、getInfo的url修改为后端定义的接口路径
'/eduservice/user/login','/eduservice/user/info'
将user.js中的登录请求与后端接口对应/admin/vod/user/login

export function login(data) {
  return request({
    url: '/admin/vod/user/login',
    method: 'post',
    data
  })
}

(5)跨域解决

跨域原因:如果出现协议、ip地址、端口号任意一个不一样就会出现跨域问题。
解决
在contorller类上加入@CrossOrigin解决跨域问题。

(6)前端登陆测试

登陆成功


获取用户信息成功


成功进入后台首页

解决跨域问题

解决跨域方法:前端、后端、gateway。
目前先使用后端解决跨域。
后端解决跨域方法也有多种:第一种是在所有controller类上加注解@CrossOrigin
所以目前先使用该方法。

16.路由相关

路由知识

vue模板项目路由示例

import Layout from '@/layout'
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },

  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: { title: 'Dashboard', icon: 'dashboard' }
    }]
  },

  {
    path: '/example',
    component: Layout,
    redirect: '/example/table',
    name: 'Example',
    meta: { title: 'Example', icon: 'el-icon-s-help' },
    children: [
      {
        path: 'table',
        name: 'Table',
        component: () => import('@/views/table/index'),
        meta: { title: 'Table', icon: 'table' }
      },
      {
        path: 'tree',
        name: 'Tree',
        component: () => import('@/views/tree/index'),
        meta: { title: 'Tree', icon: 'tree' }
      }
    ]
  }
 }

根据上面代码可以发现每一个一级路由的component都是Layout,这样可以保证每个一级路由的子路由都可以在Layoutview中显示。

component: Layout解释:
component:每一个路由都会对应一个路由组件即vue页面,由component赋值,这里的Layout是整个后台系统的布局,并且在route的index.js文件中已经引入了import Layout from '@/layout'
layout组件结构

layout 的index.js
可以看到就是后台的整体框架,分为左右结构

<template>
  <div :class="classObj" class="app-wrapper">
    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
    <sidebar class="sidebar-container" />
    <div class="main-container">
      <div :class="{'fixed-header':fixedHeader}">
        <navbar />
      </div>
      <app-main />
    </div>
  </div>
</template>

如下图所:

创建教师路由

所以,我们需要添加教师管理的路由,可以直接仿照example路由。在example路由下面加入

{
    path: '/vod',
    component: Layout,
    redirect: '/vod/teacher/list',
    name: 'vod',
    meta: { title: '讲师管理', icon: 'el-icon-s-help' },
    children: [
      {
        path: 'teacher/list',
        name: 'TeacherList',
        component: () => import('@/views/table/index'),
        meta: { title: '讲师列表', icon: 'table' }
      },
      {
        path: 'teacher/create',
        name: 'TeacherCreate',
        component: () => import('@/views/tree/index'),
        meta: { title: '添加讲师', icon: 'tree' }
      }
    ]
  },

效果展示

17.前端

讲师列表(CRUD+element-ui)

路由及页面初始化

  1. 创建讲师路由

    {
        path: '/teacher',
        component: Layout,
        redirect: '/teacher/list',
        name: 'Teacher',
        meta: { title: '讲师管理', icon: 'example' },
        children: [
          {
            path: 'list',
            name: 'List',
            component: () => import('@/views/edu/teacher/list'),
            meta: { title: '讲师列表', icon: 'table' }
          },
          {
            path: 'save',
            name: 'Save',
            component: () => import('@/views/edu/teacher/save'),
            meta: { title: '添加讲师', icon: 'tree' }
          }
        ]
      }
    
  2. 创建vue页面

    创建目录edu/teacher,并创建list.vue、save.vue页面,用于展示讲师列表和添加讲师。

  3. 创建讲师相关api文件teacher.js

    注意:js中创建函数有三种

    a(){}
    a: function(){}
    a: ()=>{}
    

    teacher.js

    import request from '../../utils/request'
    const api_name = '/edu/teacher'
    
    export default {
      // 条件分页查询讲师列表
      getTeacherListPage(current, limit, teacherQuery) {
        return request({
          url: `${api_name}/pageListByQuery/${current}/${limit}`,
          method: 'post',
          data: teacherQuery
        })
      }
    }
    

查询讲师

  1. 构建list.vue讲师列表页面
    (1)创建getList函数,调用前面封装好的条件获取讲师列表的api接口。

    	// 条件分页查询讲师列表
        getList() {
          teacher.getTeacherListPage(this.page, this.limit, this.teacherQuery)
            .then(res => {
              console.log(res.data)
            })
            .catch(error => {
              alert(error)
            })
        }
    

    (2)element-ui表格显示讲师数据

    <el-table
      :data="list"
      stripe
      style="width: 100%">
      <el-table-column
        label="序号"
        width="120"
        align="center">
        <template slot-scope="scope">
          {{ (page - 1) * limit + scope.$index + 1 }}
        </template>
      </el-table-column>
      <el-table-column
        prop="name"
        label="姓名"
        width="120"/>
      <el-table-column label="头衔">
        <template slot-scope="scope">
          {{ scope.row.level === 1 ? '高级讲师' : '首席讲师' }}
        </template>
      </el-table-column>
      <el-table-column prop="intro" label="资历" width="460"/>
      <el-table-column prop="gmtCreate" label="添加时间" width="160"/>
      <el-table-column prop="sort" label="排序" width="60" />
      <el-table-column label="操作" width="200" align="center">
        <template slot-scope="scope">
          <router-link :to="'/teacher/edit/'+scope.row.id">
            <el-button type="primary" size="mini" icon="el-icon-edit">修改</el-button>
          </router-link>
          <el-button type="danger" size="mini" icon="el-icon-delete" @click="removeDataById(scope.row.id)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    

    temple模版(****)

    template(模版)在这里属于一个固定用法: <template slot-scope="scope">。通过 Scoped slot 可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据。
    详细解释

  2. element-ui分页(***)

    <el-pagination
          :current-page="page"
          :page-size="limit"
          :total="total"
          style="padding: 30px 0; text-align: center;"
          layout="total, prev, pager, next, jumper"
          @current-change="getList"
        />
    

    唯一需要注意的是@current-change="getList",该属性需要绑定分页查询函数,就是我们前面创建的getList函数。该属性绑定的函数会默认传入page参数,用于分页请求。所以要将getList函数添加一个page参数。

    原理:当我们前端点击分页器中的某个页数时,分页器会将page传给current-change,继而传给绑定的分页查询函数getList

    getList函数:由于getList函数被分页中的@current-change="getList"绑定,所以定义时要传入一个参数page,默认值为1。

    getList(page = 1) {
          this.page = page
          teacher.getTeacherListPage(this.page, this.limit, this.teacherQuery)
            .then(res => {
              this.list = res.data.list
              this.total = res.data.total
              console.log(this.list)
            })
            .catch(error => {
              alert(error)
            })
        }
    
  3. form表单条件查询讲师列表

    <el-form :inline="true" class="demo-form-inline">
          <el-form-item> <el-input v-model="teacherQuery.name" placeholder="讲师名"/></el-form-item>
          <el-form-item>
            <el-select v-model="teacherQuery.level" clearable placeholder="讲师头衔">
              <el-option :value="1" label="高级讲师"/>
              <el-option :value="2" label="首席讲师"/>
            </el-select>
          </el-form-item>
          <el-form-item label="添加时间">
            <el-date-picker
              v-model="teacherQuery.begin"
              type="datetime"
              placeholder="选择开始时间"
              value-format="yyyy-MM-dd HH:mm:ss"
              default-time="00:00:00"/>
          </el-form-item>
          <el-form-item>
            <el-date-picker
              v-model="teacherQuery.end"
              type="datetime"
              placeholder="选择截止时间"
              value-format="yyyy-MM-dd HH:mm:ss"
              default-time="00:00:00"/>
          </el-form-item>
          <el-button type="primary" icon="el-icon-search" @click="getList()">查 询</el-button>
          <el-button type="default" @click="resetData()">清空</el-button>
        </el-form>
    

    form表单绑定的属性为teacherQuery,里面有两个功能,分别为查询清空,查询还是getList函数,getList会传入teacherQuery参数。
    清空函数,只需要将teacherQuery设置为空,为了让页面还有数据显示,需要再次调用一次getList函数,显示首页数据。

    // 清空form查询表单
        resetData() {
          this.teacherQuery = {}
          this.getList()
        }
    

删除讲师

  1. 删除讲师
    table列表的最后一列包括修改和删除两个按钮

    <el-table-column label="操作" width="200" align="center">
    	   <template slot-scope="scope">
    	      <router-link :to="'/teacher/edit/'+scope.row.id">
    	        <el-button type="primary" size="mini" icon="el-icon-edit">
    	        修改</el-button>
    	      </router-link>
    	      <el-button type="danger" size="mini" icon="el-icon-delete"
    	      @click="removeDataById(scope.row.id)">删除</el-button>
    	   </template>
    	</el-table-column>
    

    我们只需要定义删除函数removeDataById即可。删除成功后还需要再次请求一次数据。
    removeDataById参数为idid是通过template模板获取的

    (1)删除讲师api

     // 根据id删除讲师
      removeById(id) {
        return request({
          url: `${api_name}/delete/${id}`,
          method: 'delete'
        })
      }
    

    (2)removeDataById函数
    删除时应该右系统弹框用于确认是否删除,直接在elementui官网复制代码即可。点击确定执行then方法,点击取消执行catch方法。

    // 根据id删除讲师
    removeDataById(id) {
      this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
        // 点击确定执行then方法,点击取消执行catch方法
      }).then(() => {
        teacher.removeById(id)
          .then(() => {
            // 成功之后先刷新页面
            this.getList()
            this.$message({
              type: 'success',
              message: '删除成功!'
            })
          })
          .catch(error => {
            console.log(error)
          })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        })
      })
    }
    

添加修改讲师

(1)添加和修改写在了同一个函数里面
有讲师id代表修改,否则为添加讲师

saveData() {
      // 有讲师id代表修改,否则为添加讲师
      if (this.teacher.id) {
        teacher.updateTeacher(this.teacher)
          .then(res => {
            this.$message({
              type: 'success',
              message: '修改成功!'
            })
            // 回到列表页面 路由跳转
            this.$router.push({ path: '/teacher/list' })
          })
          .catch(error => {
            this.$message({
              type: 'error',
              message: error
            })
          })
      } else {
        teacher.addTeacher(this.teacher)
          .then(res => {
            this.$message({
              type: 'success',
              message: '添加成功!'
            })
            // 回到列表页面 路由跳转
            this.$router.push({ path: '/teacher/list' })
          })
          .catch(error => {
            this.$message({
              type: 'error',
              message: error
            })
          })
      }
    }

根据上面的代码可知,核心是如何获取讲师id

  1. 个人方法(有bug)

    由于修改按钮是在讲师列表(list.vue)界面,所以增加一个点击函数,
    @click="$router.push({name:'Save', params: {id: scope.row.id}})
    代码如下

    <el-table-column label="操作" width="200" align="center">
            <template slot-scope="scope">
              <el-button type="primary" size="mini" icon="el-icon-edit" @click="$router.push({name:'Save', params: {id: scope.row.id}})">修改</el-button>
              <el-button type="danger" size="mini" icon="el-icon-delete" @click="removeDataById(scope.row.id)">删除</el-button>
            </template>
          </el-table-column>
    

    具体思路如下:

    - 点击修改时,跳转到添加讲师页面,并通过路由params将讲师id传过去;
    - 在添加讲师页面的`created`中判断路由是否有讲师id参数
    - 如果有,则表示为修改讲师。则通过getTeacherById函数获取讲师信息
    - 如果没有,则表示为添加讲师
    - 将获取的讲师信息和页面表单绑定,修改讲师信息,提交完成修改。
    

    点击修改后,由于我们是直接跳转到了添加讲师组件,所以面包屑和左侧导航栏显示为添加讲师。但是我们期望的结果是面包屑显示编辑讲师,并且左侧导航栏还是在讲师列表,我们只是希望重用一下添加讲师的组件。

    期望结果

    为了解决这问题我们还是使用官方的方法,通过虚拟路由来实现。

  2. 官方方法
    (1)创建虚拟路由edit
    虚拟路由即存在但是不会在导航栏显示的路由,hidden: true可以实现设置。

    该路由还是指向添加讲师组件save.vue,只是路径和路由名称及导航栏标签改变。并且通过hidden: true将该路由设置为虚拟路由,即不会在导航栏显示。这样完美解决了面包屑和导航栏显示问题。点击修改按钮路由跳转时,通过路径传递讲师id,然后在添加讲师组件获取讲师信息。path: 'edit/:id',,这里的:代表占位符,即:id表示路径要传递的参数,通过$route.params.id接收。

    添加虚拟路由后,完整的teacher路由

    {
        path: '/teacher',
        component: Layout,
        redirect: '/teacher/list',
        name: 'Teacher',
        meta: { title: '讲师管理', icon: 'example' },
        children: [
          {
            path: 'list',
            name: 'List',
            component: () => import('@/views/edu/teacher/list'),
            meta: { title: '讲师列表', icon: 'table' }
          },
          {
            path: 'edit/:id',
            name: 'Edit',
            component: () => import('@/views/edu/teacher/save'),
            meta: { title: '编辑讲师', noCache: true },
            hidden: true
          },
          {
            path: 'save',
            name: 'Save',
            component: () => import('@/views/edu/teacher/save'),
            meta: { title: '添加讲师', icon: 'tree' }
          }
        ]
      },
    

    讲师列表组件修改按钮代码

    <el-table-column label="操作" width="200" align="center"
    	<template slot-scope="scope"> 
    		<router-link :to="'/teacher/edit/'+scope.row.id"> 
    			<el-button type="primary" size="mini" icon="el-icon-edit">修改</elbutton>
    		</router-link>
    	 	<el-button type="danger" size="mini" icon="el-icon-delete"@click="removeDataById(scope.row.id)">删除</el-button>
    	</template>
    </el-table-column>
    

    存在两个问题

    • 如果在路由跳转路径上传递参数id,该id会暴露在浏览器,容易引发安全问题。

    • vue-router导航切换 时,如果两个路由都渲染同个组件,组件会重(chong)用,组件的生命周期钩子(created)不会再被调用, 使得组件的一些数据无法根据 path的改变得到更新。

      我们在讲师添加组件就是通过created方法中通过路由信息获取的讲师id。
      由于编辑和添加讲师两个路由都是指向同一个组件save.vue,所以当我们通过点击修改按钮跳转到编辑界面时,实际上跳转到了save.vue组件,此时页面表单显示用户信息,用于我们的修改。如果此时我们不想修改,而是想添加用户信息,我们会发现当我们点击添加讲师导航栏时,表单内还是有修改时的用户信息,即使我们在created中判断当路由参数没有id时清空表单也会出现这种情况。原因就是上面的第二个问题。

    问题解决
    对于问题一:路由跳转时通过params传参,就不会在url中显示id
    params传参,必须通过name实现路由跳转,不能通过path。
    @click="$router.push({name:'Edit', params:{id:scope.row.id}})

    修改后的讲师列表页面修改按钮代码

    <el-table-column label="操作" width="200" align="center">
    	       <template slot-scope="scope">
    	          <el-button type="primary" size="mini" icon="el-icon-edit" @click="$router.push({name:'Edit', params:{id:scope.row.id}})">修改</el-button>
    	          <el-button type="danger" size="mini" icon="el-icon-delete" @click="removeDataById(scope.row.id)">删除</el-button>
    	        </template>
    	      </el-table-column>
    

    对于问题二:虽然两个路由指向的是同一个组件,但是他们的路由信息是不同的所以,我们可以通过watch监听路由信息来重新调用created中的内容,或者清空表单。
    当我们监听到路由信息改变时我们就清空讲师信息。

    watch: {
        $route(to, from) {
          console.log('watch $route')
          this.teacher = {}
        }
      }
    
  3. 总结

    对于添加功能 :

    • 讲师添加页面中的created判断路由中是否有讲师id
    • 没有讲师id,填写讲师信息,点击提交完成讲师添加。

    对于修改功能:

    • 在讲师列表页面点击修改
    • 通过路由params传递讲师id
    • 讲师添加页面中的created判断路由中是否有讲师id
    • 有讲师id,调用获取讲师信息函数,获取讲师信息
    • 页面表单绑定获取的讲师信息,并显示
    • 修改表单,点击提交完成讲师修改
    • 若不想修改而是想添加,点击左侧添加讲师导航栏
    • 此时页面watch监测到路由信息变化,将表单绑定的讲师信息清空,执行添加操作。

    完整添加讲师页面代码save.vue

    <template>
      <div>
        <el-form label-width="120px">
          <el-form-item label="讲师名称" >
            <el-input v-model="teacher.name"/>
          </el-form-item>
          <el-form-item label="讲师排序">
            <el-input-number v-model="teacher.sort" :min="0" controls-position="right"/>
          </el-form-item>
          <el-form-item label="讲师头衔">
            <el-select v-model="teacher.level" clearable placeholder="请选择">
              <!-- 数据类型一定要和取出的json中的一致,否则没法回填因此,这里value使用动态绑定的值,保证其数据类型是number-->
              <el-option :value="1" label="高级讲师"/>
              <el-option :value="2" label="首席讲师"/>
            </el-select>
          </el-form-item>
          <el-form-item label="讲师资历">
            <el-input v-model="teacher.career"/>
          </el-form-item>
          <el-form-item label="讲师简介">
            <el-input v-model="teacher.intro" :rows="10" type="textarea"/>
          </el-form-item>
          <!-- 讲师头像:TODO -->
          <el-form-item>
            <el-button
              :disabled="saveBtnDisabled"
              type="primary"
              @click="saveOrUpdate">保存</el-button>
          </el-form-item>
        </el-form>
      </div>
    </template>
    
    <script>
    import teacher from '../../../api/teacher/teacher'
    export default {
      name: 'Save',
      data() {
        return {
          teacher: {
            name: '',
            sort: 0,
            level: 1,
            career: '',
            intro: '',
            avatar: '',
            saveBtnDisabled: false // 保存按钮是否禁用,
          }
        }
      },
      watch: {
        $route(to, from) {
          console.log('watch $route')
          this.teacher = {}
        }
      },
      created() {
        // 路由路径传参使用params接收
        const id = this.$route.params.id
        if (id !== undefined) {
          console.log(this.$route.params.id)
          this.getTeacherById(id)
        }
      },
      methods: {
        // 添加修改讲师
        saveOrUpdate() {
          this.saveBtnDisabled = true
          this.saveData()
        },
        // 添加or修改函数
        saveData() {
          // 有讲师id代表修改,否则为添加讲师
          if (this.teacher.id) {
            teacher.updateTeacher(this.teacher)
              .then(res => {
                this.$message({
                  type: 'success',
                  message: '修改成功!'
                })
                // 回到列表页面 路由跳转
                this.$router.push({ path: '/teacher/list' })
              })
              .catch(error => {
                this.$message({
                  type: 'error',
                  message: error
                })
              })
          } else {
            teacher.addTeacher(this.teacher)
              .then(res => {
                this.$message({
                  type: 'success',
                  message: '添加成功!'
                })
                // 回到列表页面 路由跳转
                this.$router.push({ path: '/teacher/list' })
              })
              .catch(error => {
                this.$message({
                  type: 'error',
                  message: error
                })
              })
          }
        },
        // 根据id获取用户信息
        getTeacherById(id) {
          teacher.getTeacherById(id)
            .then(res => {
              this.teacher = { ...res.data.item }
              console.log(this.teacher)
            })
            .catch(error => {
              this.$message({
                type: 'error',
                message: error
              })
            })
        }
      }
    }
    </script>
    
    <style scoped>
    
    </style>
    
    

路由传参

三种:

  1. 跳转路径传参,定义路由path时需要加:参数,例如path: 'edit/:id',其中id就是传递的参数。使用$route.params接收。

  2. 使用query传参,参数也会显示在请求路径中,使用$route.query接收。

  3. 使用params传参,参数不会显示在请求路径中,使用$route.params接收。但是必须通过路由的name实现路由跳转,例如$router.push({name:'Edit', params:{id:scope.row.id}})

    路由传参详解

18.对象存储阿里云oss

我们数据库中avatar字段存储的是图片链接,为了将用户上传的图片转化为链接,我们可以将图片存储在阿里云,然后阿里云会自动图片生成访问链接。
为了解决海量数据存储与弹性扩容,项目中我们采用云存储的解决方案- 阿里云OSS。

开通对象存储OSS服务

(1)申请阿里云账号
(2)实名认证
(3)开通“对象存储OSS”服务
(4)进入管理控制台

创建Bucket

选择:低频访问、公共读、不开通

上传默认头像

可以在后台手动上传图片

点击详情可查看图片超链接

java使用阿里云OSS

  1. 创建accesskeys

    2.使用java SDK

java SDK文档

19.搭建阿里云后台环境

(1)在service下创建maven工程service-oss
导入依赖

<dependencies>
        <!-- 阿里云oss依赖 -->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
        </dependency>
        <!-- 日期工具栏依赖 -->
         <dependency>
             <groupId>joda-time</groupId>
             <artifactId>joda-time</artifactId>
         </dependency>
<!--如果使用的是Java 9及以上的版本,则需要添加jaxb相关依赖。添加jaxb相关依赖示例代码如下:-->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!-- no more than 2.3.3-->
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.3.3</version>
        </dependency>
    </dependencies>

(2)配置配置文件application。properties
和aliyun有关的配置存放在了配置文件中,如果使用可以通过常量类读取。

其中aliyun.oss.file.keyid、keysecret是自己access的密钥

(3)创建启动类

package com.atguigu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

/**
 * @Author: mfx
 * @Description:
 * @Date: Created in 11:03 2022/8/31
 */
@SpringBootApplication
@ComponentScan("com.atguigu")
public class OssApplication {
    public static void main(String[] args) {
        SpringApplication.run(OssApplication.class);
    }
}

启动项目

spring boot 会默认加载org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration这个类,而DataSourceAutoConfiguration类使用了@Configuration注解向spring注入了dataSource bean,又因为项目(oss模块)中并没有关于dataSource相关的配置信息,所以当spring创建dataSource bean时因缺少相关的信息就会报错。
解决办法:
方法1、在@SpringBootApplication注解上加上exclude,解除自动加载DataSourceAutoConfiguration
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

20.实现文件上传

从配置文件读取常量

上一节我们将阿里云的一些参数存放在了配置文件中,为了方便使用,我们可以创建一个工具类读取配置文件,之后使用相关阿里云参数时,直接调用该类就可以。
使用@Value读取application.properties里的配置内容用spring的 InitializingBean 的 afterPropertiesSet 来初始化配置信息,这个方法将在所有的属性被初始化后调用。
对类中每个属性创建静态变量,之后可以不用创建对象直接获取属性。

package com.atguigu.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @Author: mfx
 * @Description:
 * @Date: Created in 11:24 2022/8/31
 */
@Component
public class ConstantPropertiesUtil implements InitializingBean {

    @Value("${aliyun.oss.file.endpoint}")
    private String endpoint;

    @Value("${aliyun.oss.file.keyid}")
    private String keyId;

    @Value("${aliyun.oss.file.keysecret}")
    private String keySecret;

    @Value("${aliyun.oss.file.bucketname}")
    private String bucketName;

    // 创建静态变量,可以不用创建对象直接获取属性
    public static String END_POINT;
    public static String ACCESS_KEY_ID;
    public static String ACCESS_KEY_SECRET;
    public static String BUCKET_NAME;

    @Override
    public void afterPropertiesSet() throws Exception {
        END_POINT = endpoint;
        ACCESS_KEY_ID = keyId;
        ACCESS_KEY_SECRET = keySecret;
        BUCKET_NAME = bucketName;
    }
}

上传文件

controller类
MultipartFile用于接收上传的文件。uploadFileAvatar实现文件上传功能。

@CrossOrigin
@RestController
@RequestMapping("/eduoss/fileoss")
public class OssController {

    @Autowired
    OssService ossService;

    @ApiOperation("文件上传")
    @PostMapping("upload")
    public Result upload(MultipartFile file){
        // MultipartFile用于接收上传的文件
        // 返回上传到oss图片的访问链接
        String url = ossService.uploadFileAvatar(file);
        return Result.ok().data("url",url);
    }
}

service

public interface OssService {
    // 上传图片到阿里云,返回图片访问链接
    String uploadFileAvatar(MultipartFile file);
}

serviceImpl
上传文件的具体操作阿里云的官方文档有给出示例代码

分析一下:

  • endpoint 、accessKeyId 、accessKeySecret 、bucketName 获取,我们可以通过前面定义好的工具类获取;
  • 创建OSSClient实例;
  • 获取我们上传文件的输入流;
  • 创建PutObject请求;
  • 关闭OSSClient实例
  • 返回上传图片的url

上传图片的url是有固定格式的,我们可以分析一下手动上传的url
https://guli-komorebi.oss-cn-hangzhou.aliyuncs/%E7%B2%BE%E7%81%B5%E5%A5%B3%E7%8E%8B%E6%A1%8C%E9%9D%A2.jpg
可以划分为"https://“+bucketName+”.“+endpoint+”/"+filename,所以代码中url通过字符串拼接获取,最终返回url。

package com.atguigu.service.impl;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.atguigu.service.OssService;
import com.atguigu.utils.ConstantPropertiesUtil;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * @Author: mfx
 * @Description:
 * @Date: Created in 11:32 2022/8/31
 */
@Service
public class OssServiceImp implements OssService {
    // 上传图片到阿里云,返回图片访问链接
    @Override
    public String uploadFileAvatar(MultipartFile file) {
        // 工具类获取阿里云相关参数
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = ConstantPropertiesUtil.END_POINT;
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = ConstantPropertiesUtil.ACCESS_KEY_ID;
        String accessKeySecret = ConstantPropertiesUtil.ACCESS_KEY_SECRET;
        // 填写Bucket名称,例如examplebucket。
        String bucketName = ConstantPropertiesUtil.BUCKET_NAME;

        try {
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

            // 根据传入的文件参数获取文件输入流
            InputStream inputStream = file.getInputStream();

            // 获取文件名称
            String filename = file.getOriginalFilename();

            // 创建PutObject请求。
            // 第一个参数bucket名称,第二个参数:上传到oss文件路径和文件名称,第三个参数上传文件流
            ossClient.putObject(bucketName, filename, inputStream);

            // 关闭
            ossClient.shutdown();

            // 把文件上传后的路径返回
            String url = "https://"+bucketName+"."+endpoint+"/"+filename;
            return url;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

swagger测试

问题

当多次长传相同名称,最后一次上传的文件会覆盖掉前面上传的文件,解决方法如下。
下面的DateTime是我们前面引入的工具包依赖,可以快速获取时间格式

<dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.3.3</version>
</dependency>


在serviceImpl中添加如下代码

  // 获取文件名称
 String filename = file.getOriginalFilename();
 // 防止相同文件名图片覆盖:uuid+日期文件夹
 String uuid = UUID.randomUUID().toString().replace("-", "");
 String dataPath = new DateTime().toString("yyyy/MM/dd");
 filename = dataPath+ "/"+uuid+filename;

21.nginx

(1)官网下载安装windows版本nginx
进入nginx目录启动

(2)配置nginx配置文件
将监听端口改为81,原本是80,为了防止访问localhost报错,改为81

设置路由转发,此后前端只需要请求9001端口即可,nginx根据请求路径的参数,判断请求后端的哪个端口。

(3)修改前端请求端口为9001

配置文件修改,记得重启项目
(4)测试
启动后端两个项目,系统功能正常

22.头像上传

(1)在课件中复制两个头像上传的组件

(2)在添加讲师组件中引入并声明两个组件

(3)使用两个组件

      <!-- 讲师头像:TODO -->
      <el-form-item label="讲师头像">
        <!-- 头衔缩略图 -->
        <pan-thumb :image="teacher.avatar"/>
        <!-- 文件上传按钮 -->
        <el-button
          type="primary"
          icon="el-icon-upload"
          @click="imagecropperShow=true">
          更换头像
        </el-button>
        <!--
          v-show:是否显示上传组件
          :key:类似于id,如果一个页面多个图片上传控件,可以做区分
          :url:后台上传的url地址
          @close:关闭上传组件
          @crop-upload-success:上传成功后的回调 -->
        <image-cropper
          v-show="imagecropperShow"
          :width="300"
          :height="300"
          :key="imagecropperKey"
          :url="BASE_API+'/eduoss/fileoss/upload'"
          field="file"
          @close="close"
          @crop-upload-success="cropSuccess"/>
      </el-form-item>

对应data中的属性和methods中函数

data() {
	return{
		  BASE_API: process.env.BASE_API, // 接口API地址
	      imagecropperShow: false, // 是否显示上传组件
	      saveBtnDisabled: false, // 保存按钮是否禁用,
	      imagecropperKey: 0 // 上传组件id
	 }
 }    
 methods:{
 // 关闭上传弹窗
    close() {
      this.imagecropperShow = false
    },
    // 上传图片成功
    // data接收后端接口返回的数据
    cropSuccess(data) {
      this.teacher.avatar = data.url
    }}

23.EasyExcel读写文件

EasyExcel写操作

(1) 引入依赖

<dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>2.2.0-beta2</version>
</dependency>

(2)定义实体类,类属性对应excel的表头
使用@ExcelProperty注解将类属性于表头绑定。

@Data
public class DemoStudent {
    @ExcelProperty("学生id")
    private Integer sno;
    @ExcelProperty("学生姓名")
    private String name;
}

(3)定义文件地址,写入excel

public class test {
    public static void main(String[] args) {
        String filename = "D:/write.xlsx";
        EasyExcel.write(filename, DemoStudent.class)
                .sheet("学生信息")
                .doWrite(getList());

    }

    public static List<DemoStudent> getList(){
        List<DemoStudent> list = new ArrayList<>();
        for(int i = 0; i < 10; ++i){
            DemoStudent demoStudent = new DemoStudent();
            demoStudent.setSno(i);
            demoStudent.setName("luck"+i);
            list.add(demoStudent);
        }
        return list;
    }
}

(4)结果

EasyExcel读操作

以刚刚写入excle文件为例,实现读操作
(1)创建与excel文件对应的类,并使用注解@ExcelProperty的index属性标识第几列

@Data
public class DemoStudent {
    @ExcelProperty(value = "学生id", index = 0)
    private Integer sno;
    @ExcelProperty(value = "学生姓名", index = 1)
    private String name;
}

(2)创建监听器
监听器继承AnalysisEventListener<T data>,T表示与excel对应的实体类

public class EasyExcelListener extends AnalysisEventListener<DemoStudent> {
    // 一行一行读取excel
    @Override
    public void invoke(DemoStudent data, AnalysisContext context) {
        System.out.println("***"+data);
    }
    // 读取表头
    @Override
    public void invokeHead(Map<Integer, CellData> headMap, AnalysisContext context) {
        System.out.println("表头信息"+headMap);
    }
    // 读完后的操作
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {

    }
}

(3)读操作测试
一定不能忘记写.sheet()和.doRead()

 public static void main(String[] args) {
        // EasyExcel写操作
        //String filename = "D:/write.xlsx";
        //EasyExcel.write(filename, DemoStudent.class)
        //        .sheet("学生信息")
        //        .doWrite(getList());

        // EasyExcel读操作
        String filename = "D:/write.xlsx";
        EasyExcel.read(filename, DemoStudent.class, new EasyExcelListener())
        .sheet()
        .doRead();

    }

24.excel添加课程分类(后端)

代码生成器

使用之前的代码生成器,生成课程表相关的controller、entity、service、serviceImpl等类。只需要将代码生成器中表明更换为课程表。

strategy.setInclude("edu_subject");

easyExcel读取

(1)创建和excel对应的实体类

@Data
public class EasyExcelSubject {
    // 一级分类名
    @ExcelProperty(index = 0)
    private String oneSubject;
    @ExcelProperty(index = 1)
    private String twoSubject;
}

(2) controller接口

这里也是使用MultipartFile 接收excel文件

@Api(description = "课程分类管理")
@CrossOrigin
@RestController
@RequestMapping("/eduservice/subject")
public class SubjectController {
    @Autowired
    private SubjectService subjectService;

    @ApiOperation("Excel批量导入课程")
    @PostMapping("addSubject")
    public Result addSubject(MultipartFile file){
        //1 获取上传的excel文件 MultipartFile
        subjectService.addSubject(file, subjectService);
        return Result.ok();
    }
}

(3)serviceImpl
addSubject方法不仅要传递excel文件,还要将subjectService传过去。
这里传service的意义在于自动注入会形成循环依赖。listener调用service,service调用listener,所以不能使用同一个service对象。

@Service
public class SubjectServiceImpl extends ServiceImpl<SubjectMapper, Subject> implements SubjectService {

    // excel添加课程分类
    @Override
    public void addSubject(MultipartFile file, SubjectService subjectService) {
        try {
            //1 获取文件输入流
            InputStream inputStream = file.getInputStream();
            // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
            EasyExcel.read(inputStream, EasyExcelSubject.class, new SubjectListener(subjectService))
                    .sheet()
                    .doRead();
        }catch (IOException e){
            e.printStackTrace();
            // 以自定义异常的方式抛出
            throw new MyException(20002,"添加课程分类失败");
        }
    }
}

(4)编写excel监听器
由于监听器不能注入到ioc容器中,并且监听器中不能注入servvice,因为自动注入会形成循环依赖。service调用listenerlistener调用service,所以不能使用同一个service对象。所以只能将service作为构造函数的参数传递给监听器,然后在监听器内完成对数据库的操作。

package com.atguigu.edu.listen;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.atguigu.edu.entity.EasyExcelSubject;
import com.atguigu.edu.entity.Subject;
import com.atguigu.edu.service.SubjectService;
import com.atguigu.except.MyException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

/**
 * @Author: mfx
 * @Description: 读取课程分类excel的监听器
 * @Date: Created in 15:24 2022/9/1
 */
public class SubjectListener extends AnalysisEventListener<EasyExcelSubject> {

    public SubjectService subjectService;
    public SubjectListener(){}
    public SubjectListener(SubjectService subjectService){
        this.subjectService = subjectService;
    }
    //一行一行去读取excle内容
    @Override
    public void invoke(EasyExcelSubject data, AnalysisContext context) {
        if(data == null){
            throw new MyException(20001, "添加失败,数据为空");
        }
        // 添加一级分类
        Subject existOneSubject = existOneSubject(subjectService, data.getOneSubjectName());
        if(existOneSubject == null){
            // 数据库中没有想同的一级分类,添加
            existOneSubject = new Subject();
            existOneSubject.setTitle(data.getOneSubjectName());
            existOneSubject.setParentId("0");
            // 存入数据库中
            subjectService.save(existOneSubject);
        }
        // 添加二级分类
        String pid = existOneSubject.getId();
        Subject existTwoSubject = existTwoSubject(subjectService, data.getTwoSubjectName(), pid);
        if(existTwoSubject == null){
            // 数据库中没有想同的一级分类,添加
            existTwoSubject = new Subject();
            existTwoSubject.setTitle(data.getTwoSubjectName());
            existTwoSubject.setParentId(pid);
            // 存入数据库中
            subjectService.save(existTwoSubject);
        }
    }

    // 判断数据库中是否已经含有一级分类
    private Subject existOneSubject(SubjectService subjectService, String name){
        QueryWrapper<Subject> wrapper = new QueryWrapper<>();
        wrapper.eq("title",name);
        // 一级分类的parent_id为0
        wrapper.eq("parent_id","0");
        Subject subject = subjectService.getOne(wrapper);
        return subject;
    }

    // 判断数据库中是否已经含有二级分类
    private Subject existTwoSubject(SubjectService subjectService, String name, String pid){
        QueryWrapper<Subject> wrapper = new QueryWrapper<>();
        wrapper.eq("title",name);
        // 一级分类的parent_id为0
        wrapper.eq("parent_id",pid);
        Subject subject = subjectService.getOne(wrapper);
        return subject;
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {

    }
}

总结

由于监听器不能注入到ioc容器中,并且监听器中不能注入servvice,因为自动注入会形成循环依赖。service调用listenerlistener调用service,所以不能使用同一个service对象。所以只能将service作为构造函数的参数传递给监听器,然后在监听器内完成对数据库的操作。

完成过程过程

  • controler接口通过MultipartFile接收excel文件
  • 调用subjectService的增加课程分类方法,并将excel文件和ioc容器中的subjectService作为参数。
  • subjectServiceImpl,获取文件的输入流,并通过EasyExcel.read读取excel文件。
  • EasyExcel.read(inputStream, EasyExcelSubject.class, new SubjectListener(subjectService))有三个参数
  • 定义EasyExcel.read中的监听器
  • 监听器通过构造函数获取subjectService,用于操作数据库
  • 监听器通过函数invoke一行一行去读取excle内容
  • invoke函数逻辑
    (1)如果excel为空,抛出自定义异常,数据为空
    (2)subjectService判断一级分类是否存在与数据库
    (3)不存在将其存入
    (4)获取一级分类的id,将作为一级分类的parent_id
    (5)subjectService判断二级分类是否存在与数据库
    (6)不存在将其存入
  • 结束

25.添加课程分类前端

(1)创建路由,和讲师管理类似
(2)使用element-ui的上传组件
subject/save.vue如下所示

<template>
  <div class="app-container">
    <el-form label-width="120px">
      <el-form-item label="信息描述">
        <el-tag type="info">excel模版说明</el-tag>
        <el-tag>
          <i class="el-icon-download"/>
          <a :href="'/static/example.xlsx'">点击下载模版</a>
        </el-tag>
      </el-form-item>
      <!--
    uto-upload: 是否自动上传
    on-error:上传失败的回调函数
    on-success:上传成功的回调函数
    limit:一次上传的文件个数
    action:上传问见后端接口
    accept:接收文件类型
-->
      <el-form-item label="选择Excel">
        <el-upload
          ref="upload"
          :auto-upload="false"
          :on-success="fileUploadSuccess"
          :on-error="fileUploadError"
          :disabled="importBtnDisabled"
          :limit="1"
          :action="BASE_API+'/eduservice/subject/addSubject'"
          name="file"
          accept=".xls,.xlsx">
          <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
          <!-- loading:	是否加载中状态-->
          <el-button
            :loading="loading"
            style="margin-left: 10px;"
            size="small"
            type="success"
            @click="submitUpload">{{ fileUploadBtnText }}</el-button>
        </el-upload>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  name: 'Save',
  data() {
    return {
      BASE_API: process.env.BASE_API, // api接口地址
      fileUploadBtnText: '上传到服务器', // 按钮文字
      importBtnDisabled: false,
      loading: false
    }
  },
  methods: {
    // 上传按钮
    submitUpload() {
      this.fileUploadBtnText = '正在上传'
      this.importBtnDisabled = true
      this.loading = true
      // 执行表单提交及文件上传
      this.$refs.upload.submit()
    },
    // 文件上传成功回调函数
    fileUploadSuccess() {
      this.fileUploadBtnText = '导入成功'
      this.loading = false
      this.$message({
        type: 'success',
        message: '文件上传成功'
      })
    },
    // 文件上传失败回调函数
    fileUploadError() {
      this.fileUploadBtnText = '导入失败'
      this.loading = false
      this.$message({
        type: 'error',
        message: '文件上传失败'
      })
    }
  }
}
</script>

<style scoped>

</style>

前端效果

26.课程分类列表

课程分类列表格式如下,分为两级,前端直接使用element-ui的插件

前端

<template>
  <div class="app-container">
    <el-input v-model="filterText" placeholder="Filter keyword" style="margin-bottom:30px;" />

    <el-tree
      ref="tree2"
      :data="data2"
      :props="defaultProps"
      :filter-node-method="filterNode"
      class="filter-tree"
      default-expand-all
    />

  </div>
</template>

<script>
import subject from '../../../api/subject/subject'
export default {

  data() {
    return {
      filterText: '',
      data2: [],
      defaultProps: {
        children: 'children',
        label: 'name'
      }
    }
  },
  watch: {
    filterText(val) {
      this.$refs.tree2.filter(val)
    }
  },
  created() {
    this.getAllSubject()
  },

  methods: {
    getAllSubject() {
      subject.getAllSubject()
        .then(result => {
          this.data2 = result.data.subjectList
          console.log(this.data2)
        })
        .catch(error => {
          this.$message({
            type: 'error',
            message: error
          })
        })
    },
    filterNode(value, data) {
      if (!value) return true
      return data.name.indexOf(value) !== -1
    }
  }
}
</script>


后端

(1)创建vo类,用于存储二级课程分类信息

@Data
@ApiModel("二级分类前端显示实体类")
public class SubjectVo {
    private String id;
    private String name;
    private List<SubjectVo> children = new ArrayList<>();
}

(2)service获取二级分类列表

public List<SubjectVo> getAllSubject() {
        // 获取所有一节分类课程
        QueryWrapper<Subject> queryWrapperOne = new QueryWrapper<>();
        queryWrapperOne.eq("parent_id",'0');
        List<Subject> oneSubjects = baseMapper.selectList(queryWrapperOne);

        // 获取所有二级分类课程
        QueryWrapper<Subject> queryWrapperTwo = new QueryWrapper<>();
        // ne表示不等于
        queryWrapperTwo.ne("parent_id",'0');
        List<Subject> twoSubjects = baseMapper.selectList(queryWrapperTwo);

        // 封装
        List<SubjectVo> subjectVoList = new ArrayList<>();
        for(Subject subject : oneSubjects){
            // 一级分类
            SubjectVo subjectVo1 = new SubjectVo();
            subjectVo1.setId(subject.getId());
            subjectVo1.setName(subject.getTitle());

            List<SubjectVo> children = new ArrayList<>();

            for(Subject subjectChildren : twoSubjects){
                // 一级分类的二级分类
                if(subjectChildren.getParentId().equals(subjectVo1.getId())){
                    SubjectVo subjectVo2 = new SubjectVo();
                    subjectVo2.setId(subjectChildren.getId());
                    subjectVo2.setName(subjectChildren.getTitle());
                    subjectVo2.setChildren(null);
                    children.add(subjectVo2);
                }
            }

            subjectVo1.setChildren(children);
            subjectVoList.add(subjectVo1);

        }
        return subjectVoList;
    }

前端效果

27.课程添加

课程添加包括三步:

  • 填写课程基本信息
  • 填写章节大纲信息
  • 课程确认和发布

后端

(1)代码生成器生成course、description、video、chapter的相关代码
strategy.setInclude("edu_course","edu_course_description","edu_chapter","edu_video");

(2)创建课程vo类,用于接受前端表单提交信息。
price字段需要精确到0.01,使用double或float可能会出错,所以采用 BigDecimal类型

@Data
public class CourseVo {
        @ApiModelProperty(value = "课程ID")
     private String id;
        
        @ApiModelProperty(value = "课程讲师ID")
     private String teacherId;
    
        @ApiModelProperty(value = "课程专业ID")
     private String subjectId;
    
        @ApiModelProperty(value = "课程标题")
     private String title;
    
        @ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")
     private BigDecimal price;
        @ApiModelProperty(value = "总课时")
     private Integer lessonNum;
    
        @ApiModelProperty(value = "课程封面图片路径")
     private String cover;
    
        @ApiModelProperty(value = "课程简介")
     private String description;
}

(3)controller、service部分

  • 将前端传递数据使用courseVo接收,向课程表和描述表添加数据。
  • 由于serviceImpl继承了ServiceImpl,ServiceImpl内部包含一个basemapper,所以在serviceImpl可以通过this.basemapper直接调用mapper,不需要再次注入mapper
  • BeanUtils.copyProperties(A,B)可以将A中的属性值复制到B对象中。
  • 在添加课程函数上使用事务注解@Transactional.
    • 在入口类使用注解@EnableTransactionManagement开启事务
    • 在访问数据库的service方法上添加注解@Transactional即可
  • 对课程、课程描述类的创建修改时间添加自动填充注解。
  • 要将所有在service-edu的接口第一访问路径设置为eduservice,即@RequestMapping(“/eduservice/相关模块”),要与nginx对应。
    controller 添加课程相关信
@Api("课程信息管理")
@RestController
@RequestMapping("/eduservice/course")
@CrossOrigin
public class CourseController {

    @Autowired
    CourseService courseService;

    @PostMapping("/addCourseInfo")
    @ApiOperation("添加课程相关信息")
    public Result addCourseInfo(@RequestBody CourseVo courseVo){
        String id = courseService.addCourseInfo(courseVo);
        return Result.ok().data("courseId", id);
    }
}

service添加课程相关信息

@Autowired
    CourseDescriptionService courseDescriptionService;

    // 添加课程相关信息
    @Override
    @Transactional
    // 使用事务注解,一旦出错,则全部失败
    public String addCourseInfo(CourseVo courseVo) {
        // 像课程表中添加信息
        Course course = new Course();
        BeanUtils.copyProperties(courseVo,course);
        int insert = this.baseMapper.insert(course);
        if(insert == 0)
            throw new MyException(20001,"添加课程失败");
        // 向课程描述表添加信息
        String courseId = course.getId();
        CourseDescription courseDescription = new CourseDescription();
        courseDescription.setId(courseId);
        courseDescription.setDescription(courseVo.getDescription());
        boolean save = courseDescriptionService.save(courseDescription);
        if(!save)
            throw new MyException(20001,"添加课程描述失败");
        return courseId;
    }

课程信息

页面布局



课程添加逻辑


  • 课程管理要设置四个页面:列表、基本信息(课程添加)、章节大纲、发布
  • 导航栏我们只希望显示:列表、基本信息(课程添加),所以可以通过hidden:true将章节大纲、发布两个组件隐藏。
  • 章节大纲、发布的前提是已经填写了课程基本信息,所以路径上要传递课程id,即章节大纲、发布页面对应的路径chapter/:id,publish/:id
  • 在章节大纲页面可以通过点击上一页回到基本信息页面,所以基本信息页面path也需要id,info/:id

由于课程添加和课程修改是同一个页面,所以还是要用到隐藏路由,这里path: 'info/:id'表示隐藏路由。涉及到隐藏路由还会涉及到同教师添加同样的问题,从修改页面点击导航栏添加按钮,发现数据没有清空。方法:watch

watch: {
    // 课程信息修改路由跳转到课程添加路由,要清空表单
    $route(to, from) {
      console.log('watch $route')
      this.courseInfo = {}
      this.courseInfo.cover = '/static/dog.jpg'
    }
  }
{
    path: '/course',
    component: Layout,
    redirect: '/course/list',
    name: 'Course',
    meta: { title: '课程管理', icon: 'example' },
    children: [
      {
        path: 'list',
        name: 'List',
        component: () => import('@/views/edu/course/list'),
        meta: { title: '课程列表', icon: 'table' }
      },
      {
        path: 'info',
        name: 'Info',
        component: () => import('@/views/edu/course/info'),
        meta: { title: '课程添加', icon: 'table' }
      },
      {
        path: 'info/:id',
        name: 'Edit',
        component: () => import('@/views/edu/course/info'),
        meta: { title: '编辑课程信息基本信息', noCache: true },
        hidden: true

      },
      {
        path: 'chapter/:id',
        name: 'Chapter',
        component: () => import('@/views/edu/course/chapter'),
        meta: { title: '课程章节', icon: 'table' },
        hidden: true
      },
      {
        path: 'publish/:id',
        name: 'Publish',
        component: () => import('@/views/edu/course/publish'),
        meta: { title: '课程发布', icon: 'table' },
        hidden: true
      }

    ]
  },

页面效果

课程信息页面完整代码

<template>
  <div class="app-container"> <h2 style="text-align: center;">发布新课程</h2>
    <el-steps :active="1" process-status="wait" align-center style="margin-bottom: 40px;">
      <el-step title="填写课程基本信息"/>
      <el-step title="创建课程大纲"/>
      <el-step title="最终发布"/>
    </el-steps>

    <el-form label-width="120px">
      <el-form-item label="课程标题">
        <el-input v-model="courseInfo.title" placeholder=" 示例:机器学习项目课:从基础到搭建项目视"/>
      </el-form-item>
      <!-- 所属分类 TODO -->
      <el-form-item label="课程分类">
        <el-select v-model="courseInfo.subjectParentId" placeholder="一级分类" @change="subjectLevelOneChanged">
          <el-option
            v-for="subject in subjectOneList"
            :key="subject.id"
            :label="subject.name"
            :value="subject.id"/>
        </el-select>
        <el-select v-model="courseInfo.subjectId" placeholder="二级分类">
          <el-option
            v-for="subject in subjectTwoList"
            :key="subject.id"
            :label="subject.name"
            :value="subject.id"/>
        </el-select>
      </el-form-item>

      <!-- 课程讲师 TODO -->
      <el-form-item label="课程讲师">
        <el-select v-model="courseInfo.teacherId" placeholder="请选择">
          <el-option
            v-for="teacher in teacherList"
            :key="teacher.id"
            :label="teacher.name"
            :value="teacher.id"/>
        </el-select>
      </el-form-item>
      <el-form-item label="总课时">
        <el-input-number :min="0" v-model="courseInfo.lessonNum" controls-position="right" placeholder="请填写课时"/>
      </el-form-item>

      <!-- 课程简介 TODO -->
      <!-- 课程简介-->
      <el-form-item label="课程简介">
        <tinymce :height="300" v-model="courseInfo.description"/>
      </el-form-item>
      <!-- 课程封面 TODO -->
      <el-form-item label="课程封面">
        <el-upload
          :show-file-list="false"
          :on-success="handleAvatarSuccess"
          :before-upload="beforeAvatarUpload"
          :action="BASE_API+'/eduoss/fileoss/upload'"
          class="avatar-uploader">
          <img :src="courseInfo.cover">
        </el-upload>
      </el-form-item>
      <el-form-item label="课程价格">
        <el-input-number :min="0" v-model="courseInfo.price" controls-position="right" placeholder="请填写课程价格"/>
      </el-form-item>

      <el-form-item>
        <el-button :disabled="saveBtnDisabled" type="primary" @click="saveOrUpdate">保存并下一步</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
import course from '../../../api/edu/course'
import teacher from '../../../api/edu/teacher'
import subject from '../../../api/edu/subject'
import Tinymce from '../../../components/Tinymce'
export default {
  name: 'Info',
  components: { Tinymce },
  data() {
    return {
      courseId: '',
      courseInfo: {
        title: '',
        teacherId: '',
        lessonNum: 0, // 总课时数
        description: '',
        cover: '/static/dog.jpg', // 课程封面url
        price: 0,
        subjectParentId: '', // 一级课程id
        subjectId: '' // 二级课程id
      }, // 课程基本信息
      saveBtnDisabled: false, // 保存按钮是否禁用
      teacherList: [], // 讲师列表
      subjectOneList: [], // 一级分类
      subjectTwoList: [], // 二级分类
      BASE_API: process.env.BASE_API
    }
  },
  watch: {
    // 课程信息修改路由跳转到课程添加路由,要清空表单
    $route(to, from) {
      console.log('watch $route')
      this.courseInfo = {}
      this.courseInfo.cover = '/static/dog.jpg'
    }
  },
  created() {
    // 获取所有讲师列表,用于显示讲师select
    this.getAllTeacher()
    // 获取所有课程分类
    this.getAllSubject()
    // 获取路由课程id
    if (this.$route.params && this.$route.params.id) {
      this.courseId = this.$route.params.id
      // 根据课程id回显课程信息

      this.getCourseInfoById()
    }
  },
  methods: {
    // 添加或修改课程信息
    saveOrUpdate() {
      if (this.courseId) {
        course.updateCourseInfo(this.courseInfo)
          .then(res => {
            this.$message({
              message: '修改课程成功',
              type: 'success'
            })
            // 跳转到第二步大纲页面
            this.$router.push({ path: `/course/chapter/${this.courseId}` })
          })
          .catch(res => {
            this.$message({
              message: '修改课程失败' + res.data,
              type: 'error'
            })
          })
      } else {
        course.addCourseInfo(this.courseInfo)
          .then(res => {
            this.$message({
              type: 'success',
              message: '添加课程信息成功'
            })
            // 跳转到第二步
            const courseId = res.data.courseId
            this.$router.push({ path: `/course/chapter/${courseId}` })
          })
      }
    },
    // 获取所有讲师
    getAllTeacher() {
      teacher.findAll()
        .then(res => {
          this.teacherList = res.data.teacherList
        })
    },
    // 获取所有课程分类
    getAllSubject() {
      subject.getAllSubject()
        .then(res => {
          this.subjectOneList = res.data.subjectList
        })
    },
    // 选中一级分类标签后触发的事件
    subjectLevelOneChanged(value) {
      // 获取一节分类对应的二级分类
      for (const subject of this.subjectOneList) {
        if (subject.id === value) {
          this.subjectTwoList = subject.children
        }
      }
      // 清空上一次的二级分类
      this.courseInfo.subjectId = ''
    },
    // 图片上传成功
    handleAvatarSuccess(res, file) {
      this.courseInfo.cover = res.data.url
    },
    // 图片上传之前方法
    beforeAvatarUpload(file) {
      const isJPG = file.type === 'image/jpeg'
      const isLt2M = file.size / 1024 / 1024 < 2
      if (!isJPG) {
        this.$message.error('上传头像图片只能是 JPG 格式!')
      }
      if (!isLt2M) {
        this.$message.error('上传头像图片大小不能超过 2MB!')
      }
      return isJPG && isLt2M
    },
    // 获取课程信息
    getCourseInfoById() {
      course.getCourseInfoById(this.courseId).then(res => {
        // 查询所有二级课程分类
        for (const subject of this.subjectOneList) {
          if (subject.id === res.data.courseInfo.subjectParentId) {
            this.subjectTwoList = subject.children
          }
        }
        console.log('课程信息编辑')
        this.courseInfo = res.data.courseInfo
      })
    }
  }
}
</script>

<style scoped>
.tinymce-container {
  line-height: 29px; }
</style>



课程章节大纲

实现效果如下

  1. 获取当前课程的章节大纲

    章节大纲分为两级:

    • 章节capter
    • 小节video

    为此创建两个vo类 ,chapterVo,videoVo,由下面的代码可知返回给前端的数据是chapterVo类

    @Data
    @ApiModel(value = "章节信息中包含多个video小节")
    public class ChapterVo {
        private String id;
        private String title;
        private List<VideoVo> children = new ArrayList<>();
    }
    
    @Data
    @ApiModel(value = "章节小节")
    public class VideoVo {
        private String id;
        private String title;
        private Boolean free;
    }
    
  2. 根据课程id获取章节(后端)
    太简单省略

  3. 前端接受数据capterVo使用列表展示

    <!-- 章节 -->
        <ul class="chanpterList">
          <li v-for="chapter in chapterNestedList" :key="chapter.id">
            <p>  {{ chapter.title }}
              <span class="acts">
                <el-button type="text">添加课时</el-button>
                <el-button style="" type="text">编辑</el-button>
                <el-button type="text">删除</el-button>
              </span>
            </p>
    
            <!-- 视频 -->
            <ul class="chanpterList videoList">
              <li v-for="video in chapter.children" :key="video.id">
                <p>{{ video.title }}
                  <span class="acts">
                    <el-button type="text">编辑</el-button>
                    <el-button type="text">删除</el-button>
                  </span>
                </p>
              </li>
            </ul>
          </li>
        </ul>
    
  4. 添加修改章节
    添加修改章节使用的是同一个dialog

    <!-- 添加和修改章节表单 -->
        <el-dialog :visible.sync="dialogChapterFormVisible" title="添加修改章节">
          <el-form :model="chapter" label-width="120px">
            <el-form-item label="章节标题">
              <el-input v-model="chapter.title"/>
            </el-form-item>
            <el-form-item label="章节排序">
              <el-input-number v-model="chapter.sort" :min="0" controls-position="right"/>
            </el-form-item>
          </el-form>
          <div slot="footer" class="dialog-footer">
            <el-button @click="dialogChapterFormVisible = false">取 消</el-button>
            <el-button type="primary" @click="saveOrUpdate">确 定</el-button>
          </div>
        </el-dialog>
    

    注意:
    - 根据是否有章节id判断是添加还是修改
    - 添加和修改结束后要关闭弹窗,并将chapter修改为默认值,并刷新页面获取全部章节信息
    - 小节video的增删改查和章节同理

章节页面完整代码

<template>
  <div class="app-container">
    <el-button type="text" style="font-size: 15px" @click="dialogChapterFormVisible = true">添加章节</el-button>
    <br>
    <h2 style="text-align: center;">发布新课程</h2>
    <el-steps :active="2" process-status="wait" align-center style="margin-bottom: 40px;">
      <el-step title="填写课程基本信息"/>
      <el-step title="创建课程大纲"/>
      <el-step title="提交审核"/>
    </el-steps>
    <!-- 章节 -->
    <ul class="chanpterList">
      <li v-for="chapter in chapterNestedList" :key="chapter.id">
        <p>  {{ chapter.title }}
          <span class="acts">
            <el-button type="text" @click="dialogVideoFormVisible = true; chapterId =chapter.id">添加课时</el-button>
            <el-button style="" type="text" @click="openEditChapter(chapter.id)">编辑</el-button>
            <el-button type="text" @click="removeChapter(chapter.id)">删除</el-button>
          </span>
        </p>

        <!-- 视频 -->
        <ul class="chanpterList videoList">
          <li v-for="video in chapter.children" :key="video.id">
            <p>{{ video.title }}
              <span class="acts">
                <el-button type="text" @click="openEditVideo(video.id)">编辑</el-button>
                <el-button type="text" @click="deleteVideo(video.id)">删除</el-button>
              </span>
            </p>
          </li>
        </ul>
      </li>
    </ul>
    <div>
      <el-button @click="previous">上一步</el-button>
      <el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
    </div>

    <!-- 添加和修改章节表单 -->
    <el-dialog :visible.sync="dialogChapterFormVisible" title="添加修改章节">
      <el-form :model="chapter" label-width="120px">
        <el-form-item label="章节标题">
          <el-input v-model="chapter.title"/>
        </el-form-item>
        <el-form-item label="章节排序">
          <el-input-number v-model="chapter.sort" :min="0" controls-position="right"/>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogChapterFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="saveOrUpdate">确 定</el-button>
      </div>
    </el-dialog>

    <!-- 添加和修改小节课时表单 -->
    <el-dialog :visible.sync="dialogVideoFormVisible" title="添加课时">
      <el-form :model="video" label-width="120px">
        <el-form-item label="课时标题">
          <el-input v-model="video.title"/>
        </el-form-item>
        <el-form-item label="课时排序">
          <el-input-number v-model="video.sort" :min="0" controls-position="right"/>
        </el-form-item>
        <el-form-item label="是否免费">
          <el-radio-group v-model="video.free">
            <el-radio :label="true">免费</el-radio>
            <el-radio :label="false">默认</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="上传视频">
          <!-- TODO -->
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogVideoFormVisible = false">取 消</el-button>
        <el-button
          :disabled="saveVideoBtnDisabled"
          type="primary"
          @click="saveOrUpdateVideo">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import chapter from '../../../api/edu/chapter'
import video from '../../../api/edu/video'
export default {
  name: 'Chapter',
  data() {
    return {
      courseId: '',
      chapterNestedList: [], // 章节小节信息
      saveBtnDisabled: false, // 保存按钮是否禁用
      dialogChapterFormVisible: false, // dialog弹窗
      chapter: { // 章节对象
        title: '',
        sort: 0
      },
      chapterId: '', // 课时所在的章节id
      video: {// 课时对象
        title: '',
        sort: 0,
        free: 0,
        videoSourceId: ''
      },
      saveVideoBtnDisabled: false, // 课时按钮是否禁用
      dialogVideoFormVisible: false // 是否显示小节课时表单
    }
  },
  created() {
    console.log(this.$route.params)
    if (this.$route.params && this.$route.params.id) {
      this.courseId = this.$route.params.id
      this.chapter.courseId = this.courseId
      this.getAllChapterVideo()
    }
  },
  methods: {
    // 获取当前课程所有章节信息
    getAllChapterVideo() {
      chapter.getAllChapterVideo(this.courseId)
        .then(res => {
          this.chapterNestedList = res.data.chapterVoList
        })
    },
    // 删除章节
    removeChapter(chapterId) {
      chapter.deleteChapter(chapterId).then(res => {
        this.getAllChapterVideo()
        this.$message({
          message: '删除成功',
          type: 'success'
        })
      })
    },
    // 通过修改按钮打开dialog
    openEditChapter(chapterId) {
      chapter.getChapterById(chapterId).then(res => {
        this.chapter = res.data.chapter
        this.dialogChapterFormVisible = true
      })
    },
    // 添加章节
    addChapter() {
      chapter.addChapter(this.chapter).then(res => {
        this.dialogChapterFormVisible = false
        this.chapter.title = ''
        this.chapter.sort = 0
        this.getAllChapterVideo()
        this.$message({
          message: '添加成功',
          type: 'success'
        })
      })
    },
    // 修改章节
    updateChapter() {
      chapter.updateChapter(this.chapter).then(res => {
        this.dialogChapterFormVisible = false
        this.chapter.title = ''
        this.chapter.sort = 0
        this.getAllChapterVideo()
        this.$message({
          message: '修改成功',
          type: 'success'
        })
      })
    },
    // dialog确认函数绑定
    saveOrUpdate() {
      // 添加
      if (!this.chapter.id) {
        this.addChapter()
      } else {
        this.updateChapter()
      }
    },
    // ===============================
    // 添加小节到章节
    addVideo() {
      this.video.courseId = this.courseId
      this.video.chapterId = this.chapterId
      video.addVideo(this.video).then(res => {
        // 关闭弹窗
        this.dialogVideoFormVisible = false
        this.getAllChapterVideo()
        this.$message({
          type: 'success',
          message: '添加小节成功'
        })
      })
      // 清空弹窗
      this.video = {// 课时对象
        title: '',
        sort: 0,
        free: 0,
        videoSourceId: ''
      }
    },
    // 删除小节
    deleteVideo(videoId) {
      this.$confirm('此操作将永久删除该小节, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
        // 点击确定执行then方法,点击取消执行catch方法
      }).then(() => {
        video.deleteVideo(videoId).then(res => {
          this.$message({
            message: '删除小节成功',
            type: 'success'
          })
          this.getAllChapterVideo()
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        })
      })
    },
    // 通过修改按钮打开dialog并查询小节信息
    openEditVideo(videoId) {
      video.getVideoById(videoId).then(res => {
        this.video = res.data.video
        this.dialogVideoFormVisible = true
      })
    },
    // 修改小节
    updateVideo() {
      video.updateVideo(this.video).then(res => {
        this.dialogVideoFormVisible = false
        this.video = {// 课时对象
          title: '',
          sort: 0,
          free: 0,
          videoSourceId: ''
        }
        this.$message({
          type: 'success',
          message: '修改小节成功'
        })
      })
    },
    saveOrUpdateVideo() {
      if (!this.video.id) {
        this.addVideo()
      } else {
        this.updateVideo()
      }
    },
    // 上一步按钮
    previous() {
      console.log('previous')
      this.$router.push({ path: `/course/info/${this.courseId}` })
    },
    // 下一步按钮
    next() {
      console.log('next')
      this.$router.push({ path: '/course/publish/${this.courseId}' })
    }
  }
}
</script>

<style scoped>

</style>


课程发布(***)

(1)课程发布确认

该界面显示前面我们填写的信息,所以要先获取这些信息。使用多表联查获取信息
使用xml文件查询

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis//DTD Mapper 3.0//EN" "http://mybatis/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.edu.mapper.CourseMapper">
<!--    根据课程id查询课程确认信息-->
    <select id="getCoursePublishVoById" resultType="com.atguigu.edu.entity.vo.CoursePublicVo">
        select
            ec.id,
            ec.title,
            ec.cover,
            ec.lesson_num as lessonNum,
            CONVERT(ec.price, DECIMAL(8,2)) AS price,
            es1.title as subjectLevelOne,
            es2.title as subjectLevelTwo,
            et.name as teacherName

        from
            edu_course ec
            left join edu_course_description ecd on ec.id = ecd.id
            left join edu_teacher et on ec.teacher_id = et.id
            left join edu_subject es1 on ec.subject_parent_id = es1.id
            left join edu_subject es2 on ec.subject_id = es2.id
        where ec.id = #{id}
    </select>
</mapper>

对应的vo类

@Data
@ApiModel(value = "课程发布信息")
public class CoursePublicVo {
    private String id;
    private String title;
    private String cover;
    private Integer lessonNum;
    private String subjectLevelOne;
    private String subjectLevelTwo;
    private String teacherName;
    private String price;//只用于显示
}

接口

@ApiOperation("查询课程发布信息")
    @GetMapping("getCoursePublishVoById/{courseId}")
    public Result getCoursePublishVoById(@PathVariable String courseId){
        CoursePublicVo coursePublicVo = courseService.getCoursePublishVoById(courseId);
        return Result.ok().data("coursePublicVo", coursePublicVo);
    }

测试,报错

dao层编译后只有class文件,没有mapper.xml,因为maven工程在默认情况下src/main/java目录下的所有资源文件是不发布到target目录下的

解决方法

方法三
在service的pom中添加,切记pom文件改变后,一定要刷新maven.

<!--    加载xml文件-->
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

在配置文件中添加

#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/edu/mapper/xml/*.xml

(2)课程最终发布
course表中status字段有两个值 Draft:未发布,normal:已发布。
所以课程发布只需要将该字段改为normal

后端接口

@ApiOperation("课程最终发布")
    @GetMapping("publishCourse/{courseId}")
    public Result publishCourse(@PathVariable String courseId){
        Course course = new Course();
        course.setId(courseId);
        course.setStatus("Normal");
        boolean b = courseService.updateById(course);
        if(b)
            return Result.ok().message("课程发布成功");
        return Result.error().message("课程发布失败");
    }

前端点击发布按钮后跳转到课程列表页面

publish() {
      course.publishCourse(this.courseId).then(res => {
        this.$message({
          type: 'success',
          message: res.message
        })
        this.$router.push({ path: '/course/list' })
      })
    }

课程发布页面

el-select

<el-select v-model="courseInfo.subjectParentId" placeholder="一级分类" @change="subjectLevelOneChanged">
          <el-option
            v-for="subject in subjectOneList"
            :key="subject.id"
            :label="subject.name"
            :value="subject.id"/>
</el-select>
  • change事件前要加@或v-on绑定事件
  • change事件是定义在select标签中,而不是option标签中
  • change事件的参数是回调参数,自动绑定选中的下拉框选项,不需要定义参数。若需要传递除下拉框选项以外的其他参数,则可以在声明时定义

接受时方法参数列表中需要定义参数接收

subjectLevelOneChanged(value) {
      console.log(value)
      alert(value)
    }

28.课程列表

课程列表的前端展示和教师列表完全一样,只需要修改一下变量。

唯一不同点就是删除课程会涉及多张表的操作。
分四步:

  • 删除小节(删除视频后续实现)
  • 删除章节
  • 删除描述
  • 删除课程
@Transactional
    @Override
    // 删除课程
    public boolean deleteById(String courseId) {
        // 根据课程id删除小节
        videoService.removeByCourseId(courseId);
        // 根据课程id删除章节
        chapterService.removeChapterById(courseId);
        // 根据课程id删除描述
        courseDescriptionService.removeById(courseId);
        // 根据课程id删除课程
        int i = baseMapper.deleteById(courseId);
        return i>0;
    }

29.视频点播

视频上传遇到的问题:视频上传VODUploadDemo-java-1.4.14jar包问题

(1)官方方法

官方链接

<dependency>
        <groupId>com.aliyun.vod</groupId>
        <artifactId>upload</artifactId>
        <version>1.4.14</version>
        <scope>system</scope>
        <systemPath>${project.basedir}/src/main/resources/aliyun-java-vod-upload-1.4.14.jar</systemPath>
    </dependency>

尝试了官方方法,没有成功。还是使用老师的方法

(2)老师方法

网上下载1.4.11的jar包,然后在下载好的文件夹内的lib目录下进入cmd。

mvn install:install-file -DgroupId=com.aliyun -DartifactId=aliyun-sdk-vod-upload -Dversion=1.4.11 -Dpackaging=jar -Dfile=aliyun-java-vod-upload-1.4.11.jar
执行该命令,该命令是安装jar包到仓库。不要复制,手打在cmd中,否则会出错。

对应的阿里云视频点播完整依赖

<dependency> 
		<groupId>com.aliyun</groupId> 
	 	<artifactId>aliyun-java-sdk-core</artifactId> 		
	 	<version>4.3.3</version>
</dependency> 
 <dependency> 
 	<groupId>com.aliyun.oss</groupId> 
 	<artifactId>aliyun-sdk-oss</artifactId> 
  	<version>3.1.0</version>
</dependency>
<dependency>
 	<groupId>com.aliyun</groupId>
 	<artifactId>aliyun-java-sdk-vod</artifactId>
 	<version>2.15.2</version>
</dependency>
<dependency>
	 <groupId>com.alibaba</groupId>
 	<artifactId>fastjson</artifactId>
 	<version>1.2.28</version>
</dependency>
<dependency>
 	<groupId>org.json</groupId>
	 <artifactId>json</artifactId>
 	<version>20170516</version>
</dependency>
<dependency>
 	<groupId>com.google.code.gson</groupId>
	 <artifactId>gson</artifactId>
 	<version>2.8.2</version>
</dependency>
<dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-sdk-vod-upload</artifactId>
            <version>1.4.11</version>
   </dependency>

视频上传实例

处理好前面的依赖问题,就可以进行视频上传测试了,官方文档给出了多种方法。
官方方法

以上传本地视频为例

下面代码我们只需要设置自己的accessKeyId ,accessKeySecret,上传后的视频名称,本地视频文件的位置

package com.atguigu.vodtest;

import com.aliyun.vod.upload.impl.UploadVideoImpl;
import com.aliyun.vod.upload.req.UploadVideoRequest;
import com.aliyun.vod.upload.resp.UploadVideoResponse;

/**
 * @Author: mfx
 * @Description:
 * @Date: Created in 12:06 2022/9/16
 */
// 本地文件上传
public class UploadVideo {
    //  官方链接  https://help.aliyun/document_detail/53406.html
    public static void main(String[] args) {
        String accessKeyId = "";
        String accessKeySecret = "";
        String title = "uploadVideoTest"; // 上传后的视频名称
        String fileName = "D:/data/videoTest.mp4"; // 本地视频文件的位置

        // 上传视频的方法
        UploadVideoRequest request = new UploadVideoRequest(accessKeyId, accessKeySecret, title, fileName);
        request.setPartSize(2 * 1024 * 1024L);
        request.setApiRegionId("cn-shanghai");
        request.setEcsRegionId("cn-shanghai");
        UploadVideoImpl uploader = new UploadVideoImpl();
        UploadVideoResponse response = uploader.uploadVideo(request);
        System.out.print("RequestId=" + response.getRequestId() + "\n");  //请求视频点播服务的请求ID
        if (response.isSuccess()) {
            System.out.print("VideoId=" + response.getVideoId() + "\n");
        } else {
            /* 如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 */
            System.out.print("VideoId=" + response.getVideoId() + "\n");
            System.out.print("ErrorCode=" + response.getCode() + "\n");
            System.out.print("ErrorMessage=" + response.getMessage() + "\n");
        }
    }
}

视频上传项目创建

1、创建一个新的工程,service-vod


2、创建配置文件,设置accessKey

server.port= 8003
spring.application.name=service-vod

spring.profiles.active=dev
# 阿里云accessKey
aliyun.vod.file.keyid=
aliyun.vod.file.keysecret=

3、创建启动类

由于要用到其他工程的工具类,例如swagger、统一返回类型,我们还需要加上@ComponentScan(basePackages = {"com.atguigu"})

// 默认不加载数据的东西
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ComponentScan(basePackages = {"com.atguigu"})
public class VodApplication {
    public static void main(String[] args) {
        SpringApplication.run(VodApplication.class);
    }
}

启动类配置数据库问题

如果我们的工程不需要数据库,需要在启动类配置@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

文件上传后端

由于阿里云的accesskkey放在了配置文件中,所以我们使用一个类去读取配置文件中的accesskey,并且注入到容器中。该类要实现InitializingBean接口。

@Component
public class ConstantPropertiesUtil implements InitializingBean {
    // @Value("${}")获取配置文件中的值
    @Value("${aliyun.vod.file.keyid}")
    public String accessKeyId;
    @Value("${aliyun.vod.file.keysecret}")
    public String accessKeySecret;

    public static String ACCESS_KEY_ID;
    public static String ACCESS_KEY_SECRET;

    // bean注入后的回调函数
    @Override
    public void afterPropertiesSet() throws Exception {
        ACCESS_KEY_ID = this.accessKeyId;
        ACCESS_KEY_SECRET = this.accessKeySecret;
    }
}

controller接口
MultipartFile 接收视频文件,返回视频上传后的ID

// 上传视频到阿里云
    @PostMapping("uploadAlyVideo")
    public Result uploadVideo(MultipartFile file) throws IOException {
        String VideoId = vodService.uploadVideo(file);
        return Result.ok();
    }

MultipartFile 对上传的文件有的大小限制,需要配置文件设置一下上限,否则会出现文件过大异常

spring.servlet.multipart.max-file-size=50MB
spring.servlet.multipart.max-request-size=50MB

service实现类

public String uploadVideo(MultipartFile file) {

        try{
            String accessKeyId = ConstantPropertiesUtil.ACCESS_KEY_ID;
            String accessKeySecret = ConstantPropertiesUtil.ACCESS_KEY_SECRET;
            String fileName = file.getOriginalFilename();//上传文件的原始名称
            String title = fileName.substring(0,fileName.lastIndexOf("."));// 上传后的文件显示名称

            InputStream inputStream = file.getInputStream();
            UploadStreamRequest request = new UploadStreamRequest(accessKeyId, accessKeySecret, title, fileName, inputStream);

            UploadVideoImpl uploader = new UploadVideoImpl();
            UploadStreamResponse response = uploader.uploadStream(request);
            System.out.print("RequestId=" + response.getRequestId() + "\n");  //请求视频点播服务的请求ID
            String videoId = "";
            if (response.isSuccess()) {
                videoId = response.getVideoId();
            } else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因
                videoId = response.getVideoId();
            }
            return videoId;
        }catch (IOException e) {
            e.printStackTrace();
            return null;
        }

    }

文件上传nginx配置(*****)

nginx除了要配置跨域还需要配置上传文件的大小,否则上传时会有 413 (Request Entity Too Large) 异常。

location ~ /eduvod/{
			proxy_pass http://localhost:8003;
		}

上传视频前端

<el-form-item label="上传视频">
          <el-upload
            :on-success="handleVodUploadSuccess"
            :on-remove="handleVodRemove"
            :before-remove="beforeVodRemove"
            :on-exceed="handleUploadExceed"
            :file-list="fileList"
            :action="BASE_API+'/eduvod/video/uploadAlyVideo'"
            :limit="1"
            class="upload-demo">
            <el-button size="small" type="primary">上传视频</el-button>
            <el-tooltip placement="right-end">
              <div slot="content">最大支持1G,<br>
                支持3GP、ASF、AVI、DAT、DV、FLV、F4V、<br>
                GIF、M2T、M4V、MJ2、MJPEG、MKV、MOV、MP4、<br>
                MPE、MPG、MPEG、MTS、OGG、QT、RM、RMVB、<br>
                SWF、TS、VOB、WMV、WEBM 等视频格式上传</div>
              <i class="el-icon-question"/>
            </el-tooltip>
          </el-upload>
        </el-form-item>

上传视频成功函数handleVodUploadSuccess
将上传视频的id和名字赋值给video对象,然后点击确认按钮调用我们之前写的上传小节函数实现小节上传(视频只是小节的一个属性)。

// 上传视频成功
    handleVodUploadSuccess(response, file, fileList) {
      this.video.videoSourceId = response.data.item
      this.video.videoOriginalName = file.name
    }

上传成功后数据库

删除视频

删除视频阿里云官网文档也提供相应代码
根据视频阿里云id删除

public Boolean removeAlyVideoById(String id) {
        try {
            // 初始化对象
            DefaultAcsClient client = initVodClient(ConstantPropertiesUtil.ACCESS_KEY_ID, ConstantPropertiesUtil.ACCESS_KEY_SECRET);
            // 创建删除视频的request对象
            DeleteVideoRequest request = new DeleteVideoRequest();
            //支持传入多个视频ID,多个用逗号分隔
            request.setVideoIds(id);
            // 删除
            client.getAcsResponse(request);
            return true;

        }catch (Exception e){
            e.printStackTrace();
            throw new MyException(20001,"删除视频失败");
        }

    }

代码中的initVodClient类代码也是官网提供的,需要自己创建。

// 视频点播初始化对象
public class initVodClient {
    public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException {
        String regionId = "cn-shanghai";  // 点播服务接入地域
        DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
        DefaultAcsClient client = new DefaultAcsClient(profile);
        return client;
    }
}

spring cloud

nacos注册中心

删除视频接口定义在了service-vod中,但是小节的相关服务写在了service-edu中,并且删除小节时要删除对应视频,所以要在service-edu中调用service-vod中的删除视频接口。
为了实现该功能引入了nacos注册中心,将需要相互调用的服务在注册中心中注册。


(1)引入依赖

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

(2)在要注册的服务的配置文件中进行配置nacos地址
nacos地址默认是8848端口。

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

(3)在要注册的项目的启动类上加入注解@EnableDiscoveryClient // nacos注册

注册成功结果

feign

前面使用macos注册了服务,如果想要成功调用服务,需要feign实现。
(1)添加feign依赖

 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
 </dependency>

(2)服务调用端edu启动类加注解

@EnableFeignClients

(3)服务调用端创建包和接口

@Component
@FeignClient("service-vod")
public interface VodClient {

    // 根据视频ID删除阿里云视频
    @DeleteMapping("/eduvod/video/removeAlyVideo/{id}")
    public Result removeAlyVideo(@PathVariable("id") String id);
}

(4)注入服务调用接口并调用微服务

	@Autowired
    VodClient vodClient;
 // 删除小节时,同时删除里面的视频
    @ApiOperation("删除小节")
    @DeleteMapping("deleteVideo/{videoId}")
    public Result deleteVideo(@PathVariable String videoId){
        Video video = videoService.getById(videoId);
        String videoSourceId = video.getVideoSourceId();
        if(!StringUtils.isEmpty(videoSourceId))
            vodClient.removeAlyVideo(videoSourceId);
        videoService.removeById(videoId);
        return Result.ok();
    }

本文标签: 谷粒笔记商城