谷粒商城笔记
- 1. Mybatis_plus复习
- 2.项目搭建(父工程)
- 3.service-edu模块
- 4.整合swagger
- 5.统一返回类型(swagger新注解)
- 6.get post传参
- 7.前端传递JSON数组
- 8.讲师条件分页查询
- 9.添加讲师
- 10.修改讲师
- 11.统一异常处理类
- 12.统一日志处理
- 13.ES6基本语法
- 14.前端模板文件分析
- 15.登录(跨域)
- 16.路由相关
- 17.前端
- 18.对象存储阿里云oss
- 19.搭建阿里云后台环境
- 20.实现文件上传
- 21.nginx
- 22.头像上传
- 23.EasyExcel读写文件
- 24.excel添加课程分类(后端)
- 25.添加课程分类前端
- 26.课程分类列表
- 27.课程添加
- 28.课程列表
- 29.视频点播
- spring cloud
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.com");
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 条件构造器
主要使用的有个类:QueryWrapper
,LambdaQueryWrapper
。
QueryWrapper
较为简单,直接参考官方文档:QueryWrapper
LambdaQueryWrapper
和QueryWrapper
类似,但是支持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帮我们实现这一步骤,也就是自动填充。
自动填充官网
自动填充分三步:
- 数据库创建两个字段create_time,update_time;
- 实体类添加两个字段createTime,updateTime,并添加注解
@TableField
; - 创建配置类,并注入到容器;
实体类添加字段
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实现乐观锁:
- 表中添加创建version字段;
- 实体类添加对应字段,使用@Version注解,并使用@TableField自动填充默认值为1;
@Version
@TableField(fill = FieldFill.INSERT)
private Integer version;
- 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接口没有注入到容器中。
解决方法:
- 在每个mapper接口类上加入
@Mapper
注解; - 创建配置类,通过
@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-edu
是service
的子模块,所以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.com", "atguigu.com"))
.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.修改讲师
修改操作分为两步:
- 根据id查询;
- 修改数据。
@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.com/' @click.prevent='click1()'>可能跳转到百度</a>
click1(){
alert('没想到吧!')
}
这样,就不会跳转到百度了,阻止了浏览器的默认行为。
es6模块化开发(export,export default)
正常情况下,一个js中方法是私有的,是不可以在其他文件中使用的。为了解决该问题,es6
可以使用export
或export 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:{},
})
export
和export 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前端功能实现过程
- 创建路由和vue页面
- 定义api
- 前端发出请求获取数据
- 数据展示
登录改造本地
(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.com/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
,这样可以保证每个一级路由的子路由都可以在Layout
的view
中显示。
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)
路由及页面初始化
-
创建讲师路由
{ 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' } } ] }
-
创建vue页面
创建目录edu/teacher,并创建list.vue、save.vue页面,用于展示讲师列表和添加讲师。
-
创建讲师相关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 }) } }
查询讲师
-
构建
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 内部的状态管理)的数据。
详细解释 -
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) }) }
-
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() }
删除讲师
-
删除讲师
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
参数为id
,id
是通过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
-
个人方法(有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函数获取讲师信息 - 如果没有,则表示为添加讲师 - 将获取的讲师信息和页面表单绑定,修改讲师信息,提交完成修改。
点击修改后,由于我们是直接跳转到了添加讲师组件,所以面包屑和左侧导航栏显示为添加讲师。但是我们期望的结果是面包屑显示编辑讲师,并且左侧导航栏还是在讲师列表,我们只是希望重用一下添加讲师的组件。
期望结果
为了解决这问题我们还是使用官方的方法,通过虚拟路由来实现。 -
官方方法
(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 = {} } }
-
-
总结
对于添加功能 :
- 讲师添加页面中的
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>
- 讲师添加页面中的
路由传参
三种:
-
跳转路径传参,定义路由path时需要加
:参数
,例如path: 'edit/:id',
其中id就是传递的参数。使用$route.params
接收。 -
使用query传参,参数也会显示在请求路径中,使用
$route.query
接收。 -
使用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
- 创建accesskeys
2.使用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.com/%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
调用listener
,listener
调用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
调用listener
,listener
调用service
,所以不能使用同一个service
对象。所以只能将service
作为构造函数的参数传递给监听器,然后在监听器内完成对数据库的操作。
完成过程过程:
controler
接口通过MultipartFile
接收excel
文件- 调用
subjectService
的增加课程分类方法,并将excel
文件和ioc
容器中的subjectService
作为参数。 subjectServiceImpl
,获取文件的输入流,并通过EasyExcel.read
读取excel文件。- E
asyExcel.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>
课程章节大纲
实现效果如下
-
获取当前课程的章节大纲
章节大纲分为两级:
- 章节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; }
-
根据课程id获取章节(后端)
太简单省略 -
前端接受数据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>
-
添加修改章节
添加修改章节使用的是同一个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.org//DTD Mapper 3.0//EN" "http://mybatis.org/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.com/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();
}