1.1.1采用springboot的版本是2.2.1.RELEASE
,微服务的结构为
各个maven项目的功能:
1.canal_clientedu模块用于数据同步
2.common_utils是工具类模块
3.service_base也是工具类模块,但是里面也有一些配置类,例如redis和swagger的配置
4.spring_security是springsecurity相关的配置和工具
5.api_gateway模块是网关模块
6.service_acl是权限管理模块
7.service_cms模块用于管理页面中banner的显示
8.service_edu模块负责课程、章节、讲师、视频的增删改查等功能
9.service_msm模块用于阿里云短信服务
10.service_order模块用于订单生成与支付功能
11.service_oss模块负责阿里云oos服务,用于上传课程外观图片,讲师照片
12.service_statistics模块用于统计功能
13.servce_ucenter模块用户用户的登录
14.service_vod模块负责阿里云vod服务,用于上传课程每个章节的视频
guli-parent项目的pom文件为设置了全部微服务项目的依赖的版本信息以及相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modules>
<module>service</module>
<module>common</module>
<module>canal_clientedu</module>
<module>infrastructure</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>guli-parent</artifactId>
<packaging>pom</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>guli-parent</name>
<description>Demo project for Spring Boot</description>
<repositories>
<repository>
<id>sonatype-nexus-staging</id>
<name>Sonatype Nexus Staging</name>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<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.5.1</aliyun-java-sdk-core.version>
<aliyun-sdk-oss.version>3.10.2</aliyun-sdk-oss.version>
<aliyun-java-sdk-vod.version>2.15.11</aliyun-java-sdk-vod.version>
<aliyun-java-vod-upload.version>1.4.14</aliyun-java-vod-upload.version>
<aliyun-sdk-vod-upload.version>1.4.14</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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
service模块中的pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>guli-parent</artifactId>
<groupId>com.atguigu</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>service</artifactId>
<packaging>pom</packaging>
<modules>
<module>service_oss</module>
<module>service_vod</module>
<module>service_edu</module>
<module>service_cms</module>
<module>service_msm</module>
<module>service_ucenter</module>
<module>service_order</module>
<module>service_statistics</module>
<module>service_acl</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>service_base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<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>
</project>
common模块的相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>guli-parent</artifactId>
<groupId>com.atguigu</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common</artifactId>
<packaging>pom</packaging>
<modules>
<module>service_base</module>
<module>common_utils</module>
<module>spring_security</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<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>
</project>
canal_clientedu相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>guli-parent</artifactId>
<groupId>com.atguigu</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>canal_clientedu</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
</dependency>
</dependencies>
</project>
1.2.1安装node.js。前端使用ES6编写程序,需要使用Babel将ES6转为ES5代码,安装babel-cli工具
npm install --global babel-cli
babel --version
1.2.2安装webpack前端资源加载打包工具
npm install -g webpack webpack-cli
1.2.3使用vue-admin-template,是基于element-ui的一套后台管理系统,下载地址https://github.com/PanJiaChen/vue-admin-template ,下载压缩包解压
#进入目录
cd vue-admin-template-master
#安装依赖
npm install
#启动
npm run dev
1.2.4修改项目信息
package.json
{
"name": "guli-admin",
"description": "谷粒学院后台管理系统",
"author": "HeHeHe <545466093@qq.com>",
}
config/index.js
#修改端口信息
port:9528
1.2.4项目的目录结构
.
├── build // 构建脚本
├── config // 全局配置
├── node_modules // 项目依赖模块
├── src //项目源代码
├── static // 静态资源
└── package.jspon // 项目信息和依赖配置
src
├── api // 各种接口
├── assets // 图片等资源
├── components // 各种公共组件,非公共组件在各自view下维护
├── icons //svg icon
├── router // 路由表
├── store // 存储
├── styles // 各种样式
├── utils // 公共工具,非公共工具,在各自view下维护
├── views // 各种layout
├── App.vue //***项目顶层组件***
├── main.js //***项目入口文件***
└── permission.js //认证入口
1.2.5运行项目
npm run dev
下载地址:https://github.com/alibaba/nacos/releases
下载版本:nacos-server-1.1.4.tar.gz或nacos-server-1.1.4.zip,解压任意目录即可
windows直接双击startup.cmd即可,linux启动使用命令
sh startup.sh -m standalone
2.1.1在common_utils模块中创建接口定义返回码ResultCode.java
package com.atguigu.commonutils;
public interface ResultCode {
public static Integer SUCCESS = 20000;
public static Integer ERROR = 20001;
}
2.1.2在common_utils模块中创建结果类R.java
@Data
public class R {
@ApiModelProperty(value = "是否成功")
private Boolean success;
@ApiModelProperty(value = "返回码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private Map<String, Object> data = new HashMap<String, Object>();
//把构造方法私有化
private R(){}
public static R ok(){
R r = new R();
r.setSuccess(true);
r.setCode(ResultCode.SUCCESS);
r.setMessage("成功");
return r;
}
public static R error(){
R r = new R();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage("失败");
return r;
}
//链式编程 R.ok().message().code();
public R success(Boolean success){
this.setSuccess(success);
return this;
}
public R message(String message){
this.setMessage(message);
return this;
}
public R code(Integer code){
this.setCode(code);
return this;
}
public R data(String key, Object value){
this.data.put(key, value);
return this;
}
public R data(Map<String, Object> map){
this.setData(map);
return this;
}
}
①在讲师管理模块中resources目录下创建application.properties
# 服务端口
server.port=8001
# 服务名
spring.application.name=service-edu
# 环境设置:dev、test、prod
spring.profiles.active=dev
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456
#返回json的全局时间样式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
#mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#logging.level.root=INFO
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/eduservice/mapper/xml/*.xml
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=1.116.28.110:8848
#开启熔断机制
feign.hystrix.enabled=true
# 设置hystrix超时时间,默认1000ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=6000
②在test/java目录下创建包com.atguigu.eduservice创建代码生成器CodeGenerator.java并运行
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("D:\\IdeaWorkSpace\\guli-parent\\service\\service_edu" + "/src/main/java");
gc.setAuthor("testjava");
gc.setOpen(false); //生成后是否打开资源管理器
gc.setFileOverride(false); //重新生成时文件是否覆盖
//UserServie
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("123456");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("eduservice"); //模块名
//包 com.atguigu.eduservice
pc.setParent("com.atguigu");
//包 com.atguigu.eduservice.controller
pc.setController("controller");
pc.setEntity("entity");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);
// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("edu_comment");
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();
}
}
③创建MyBatisPlusConfig中的分页插件,新建cofig包,创建EduConfig.java
@Configuration
public class EduConfig {
/**
*逻辑删除插件
*/
@Bean
public ISqlInjector sqlInjector() {
return new LogicSqlInjector();
}
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
④配置日志输出,resources下创建logback-spring.xml
<?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:/guli_1010/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>
⑤修改前端vue-admin-template项目src/router/index.js文件,添加路由
// 讲师管理
{
path: '/teacher',
component: Layout,
redirect: '/teacher/table',
name: '讲师管理',
meta: { title: '讲师管理', icon: 'example' },
children: [
{
path: 'table',
name: '讲师列表',
component: () => import('@/views/edu/teacher/list'),
meta: { title: '讲师列表', icon: 'table' }
},
{
path: 'save',
name: '添加讲师',
component: () => import('@/views/edu/teacher/save'),
meta: { title: '添加讲师', icon: 'tree' }
},
{
path: 'edit/:id',
name: 'EduTeacherEdit',
component: () => import('@/views/edu/teacher/save'),
meta: { title: '编辑讲师', noCache: true },
hidden: true
}
]
}
⑥在src/views目录下创建from.vue和list.vue
⑦在src/api/edu目录下创建teacher.js,用于发送ajax请求
import request from '@/utils/request'
export default {
//1.讲师列表
getTeacherListPage(current, limit, teacherQuery) {
return request({
url: `/eduservice/teacher/pageTeacherCondition/${current}/${limit}`,
method: 'post',
//teachQuery条件对象,后端使用RequestBody获取数据
//data将对象转为json传递到接口里面
data: teacherQuery
})
},
//2.删除讲师
deteteTeacherId(id) {
return request({
url: `/eduservice/teacher/${id}`,
method: 'delete'
})
},
//添加讲师
addTeacher(teacher) {
return request({
url: '/eduservice/teacher/addTeacher',
method: 'post',
data: teacher
})
},
//根据id去查询讲师
getTeacherInfo(id) {
return request({
url: `/eduservice/teacher/getTeacher/${id}`,
method: 'get'
})
},
//修改讲师
updateTeacherInfo(teacher) {
return request({
url: '/eduservice/teacher/updateTeacher',
method: 'post',
data: teacher
})
}
}
⑧在src/views/edu/teacher目录下创建list.vue
<template>
<div>
<!--查询表单-->
<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>
<!-- 表格 -->
<el-table v-loading="listLoading" :data="list" element-loading-text="数据加载中" border fit highlight-current-row>
<el-table-column label="序号" width="70" align="center">
<template slot-scope="scope">
{{ (page - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="80" />
<el-table-column label="头衔" width="80">
<template slot-scope="scope">
{{ scope.row.level===1?'高级讲师':'首席讲师' }}
</template>
</el-table-column>
<el-table-column prop="intro" label="资历" />
<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>
<!-- 分页 -->
<el-pagination
:current-page="page"
:page-size="limit"
:total="total"
style="padding: 30px 0; text-align: center;"
background
layout="total, prev, pager, next, jumper"
@current-change="getList"
/>
</div>
</template>
<script>
//引入调用Teacher.js文件
import teacher from '@/api/edu/teacher'
export default{
data() {//定义变量和初始值
return {
list:null,
page:1,//第一页
limit:3,//记录数10
total:0,
teacherQuery:{}
}
},
created() {//页面渲染之前执行,一般调用menthods定义的方法
this.getList()
},
methods:{//创建具体的方法,调用teacher.js定义的方法
getList(page=1) {
this.page = page;
teacher.getTeacherListPage(this.page, this.limit, this.teacherQuery)
.then(response => {
//response是接口返回的数据
this.list = response.data.rows;
this.total = response.data.total;
console.log(this.list);
console.log(this.total);
})
.catch(error => {
console.log(error);
})
},
resetData() {
//把表单输入项数据清空
this.teacherQuery = {};
//查所有讲师数据
this.getList();
},
removeDataById(id) {
this.$confirm('此操作将永久删除讲师记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
teacher.deteteTeacherId(id)
.then(reponse => {//删除成功
//提示信息
this.$message({
type: 'success',
message: '删除成功!'
});
//再次回到列表页面
this.getList();
})
})
}
}
}
</script>
①EduTeacherController中添加分页方法
//3.分页查询讲师的方法
//current当前页 limit每页多少
@GetMapping("/pageTeacher/{current}/{limit}")
public R pageListTeacher(@PathVariable long current,
@PathVariable long limit) {
//创建page对象
Page<EduTeacher> pageTeacher = new Page<>(current, limit);
//调用方法实现分页
//调用方法时候,底层封装,把分页所有数据封装大pageTeacher对象里面
teacherService.page(pageTeacher, null);
long total = pageTeacher.getTotal();//总记录数
List<EduTeacher> records = pageTeacher.getRecords();
return R.ok().data("total", total).data("rows", records);
}
①根据讲师name,头衔等级level以及入驻时间gmt_create查询
②这三个变量都是从前端传过来的,因此我们要创建一个实体类去接受,新建vo类TeacherQuery.java
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;
}
③Controller层,我们使用条件查询加分页,注意判断查询条件不是空的。这里我们直接在Controller中去查询了,正确的做法应该是放到service中查询数据库。
//4.条件查询加分页
@PostMapping("/pageTeacherCondition/{current}/{limit}")
public R pageTeacherCondition(@PathVariable long current,
@PathVariable long limit,
@RequestBody(required = false) TeacherQuery teacherQuery) {
//创建page对象
Page<EduTeacher> pageTeacher = new Page<>(current, limit);
//构建条件
QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();
//动态sql,判断条件值是否为空 否的话拼接上
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);
}
//排序
wrapper.orderByDesc("gmt_create");
//分页查询
teacherService.page(pageTeacher, wrapper);
long total = pageTeacher.getTotal();//总记录数
List<EduTeacher> records = pageTeacher.getRecords();//数据list集合
return R.ok().data("total", total).data("rows", records);
}
①在许多实体类中都有gmt_create和gmt_update字段的,我们如果每次都对他们修改过于麻烦,而且容易出现疏漏,因此我们对他们进行自动填充。
②service-base模块中创建自动填充类MyMetaObjectHandler
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
//属性名称不是字段名称
this.setFieldValByName("gmtCreate", new Date(), metaObject);
this.setFieldValByName("gmtModified", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("gmtModified", new Date(), metaObject);
}
}
③在实体类中添加自动填充注解即可
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
①代码生成器已经生成最简单的增删改查
②Controller代码
//5.添加讲师
@PostMapping("/addTeacher")
public R addTeacher(@RequestBody EduTeacher eduTeacher) {
boolean save = teacherService.save(eduTeacher).va;
if(save) {
return R.ok();
} else {
return R.error();
}
}
//6.根据Id进行查询功能
@GetMapping("getTeacher/{id}")
public R getTeacher(@PathVariable String id) {
EduTeacher eduTeacher = teacherService.getById(id);
return R.ok().data("teacher", eduTeacher);
}
//7.讲师修改功能
@PostMapping("updateTeacher")
public R updateTeacher(@RequestBody EduTeacher eduTeacher) {
boolean res = teacherService.updateById(eduTeacher);
if(res) {
return R.ok();
} else {
return R.error();
}
}
//逻辑删除讲师的方法
@ApiOperation(value="逻辑删除讲师")
@DeleteMapping("/{id}")
public R removeTeacher(
@ApiParam(name="id", value="讲师ID", readOnly = true)
@PathVariable String id) {
boolean res = teacherService.removeById(id);
if(res) {
return R.ok();
} else {
return R.error();
}
}
③添加讲师前端页面,src/views/edu/teacher目录下创建save.vue
<template>
<div class="app-container">
<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" controls-position="right"
min="0"/>
</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 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'"
field="file"
@close="close"
@crop-upload-success="cropSuccess"/>
</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 teacherApi from '@/api/edu/teacher'
import ImageCropper from '@/components/ImageCropper'
import PanThumb from '@/components/PanThumb'
export default {
components:{ImageCropper, PanThumb},
data() {
return {
teacher:{
name:'',
sort:0,
level:1,
carrer:'',
intro:'',
avatar:''
},
//上传弹框的组件是否显示
imagecropperShow:false,
imagecropperKey:0,
BASE_API:process.env.BASE_API,//获取dev.env.js的端口号
saveBtnDisabled:false
}
},
created() {//页面渲染之前执行
console.log('created')
this.init()
},
watch: {//监听
$route(to, from) {//路由发生变化,就会执行
console.log('watch $route')
this.init()
}
},
methods:{
close() {
this.imagecropperShow=false;
this.imagecropperKey = this.imagecropperShow ^ 1;
},
//上传成功
cropSuccess(data) {
this.imagecropperShow=false;
//上传之后的接口返回图片的地址,赋值给avatar
this.teacher.avatar = data.url;
this.imagecropperKey = this.imagecropperShow ^ 1;
},
init() {
//判断路径是否有id值
if (this.$route.params && this.$route.params.id) {
//从路径中获取id值
const id = this.$route.params.id
this.getInfo(id)
} else {
this.teacher = {}
}
},
//根据讲师id查询的方法
getInfo(id) {
teacherApi.getTeacherInfo(id)
.then(response => {
this.teacher = response.data.teacher
})
},
//修改讲师的方法
updateTeacher() {
teacherApi.updateTeacherInfo(this.teacher)
.then(response => {
//提示信息
this.$message({
type: 'success',
message: '修改成功!'
});
//回到列表页面
this.$router.push({path:'/teacher/table'})
})
},
//添加讲师的方法
saveOrUpdate() {
//判断是修改还是添加
//根据Teacher里面有没有id
if(!this.teacher.id) {
//添加
this.saveTeacher();
} else {
this.updateTeacher();
}
},
saveTeacher() {
teacherApi.addTeacher(this.teacher)
.then(response => {
//提示信息
this.$message({
type: 'success',
message: '添加成功!'
});
//回到列表页面
this.$router.push({path:'/teacher/table'})
})
}
}
}
</script>
vue-router导航切换 时,如果两个路由都渲染同个组件,组件会重(chong)用, 组件的生命周期钩子(created)不会再被调用, 使得组件的一些数据无法根据 path的改变得到更新 因此:
1、我们可以在watch中监听路由的变化,当路由变化时,重新调用created中的内容
2、在init方法中我们判断路由的变化,如果是修改路由,则从api获取表单数据, 如果是新增路由,则重新初始化表单数据
watch: {//监听
$route(to, from) {//路由发生变化,就会执行
console.log('watch $route')
this.init()
}
}
①在service-base创建异常处理类GlobalExceptionHandler
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
//指定捕获的异常的种类
@ExceptionHandler(Exception.class)
@ResponseBody
public R error(Exception e) {
e.printStackTrace();
return R.error().message("执行了全局异常处理");
}
//特定异常
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public R error(ArithmeticException e) {
e.printStackTrace();
return R.error().message("执行了运算异常处理");
}
//自定义异常处理
@ExceptionHandler(GuliException.class)
@ResponseBody
public R error(GuliException e) {
log.error(e.getMessage());
e.printStackTrace();
return R.error().code(e.getCode()).message(e.getMsg());
}
}
①在service-base创建异常处理类GuliException
@Data
@AllArgsConstructor//生成有参数的构造方法
@NoArgsConstructor//生成无参数的构造
public class GuliException extends RuntimeException {
private Integer code;//状态码
private String msg;//异常信息
}
②业务需要时抛出即可
①开通“对象存储OSS”服务
②创建bucket,创建RAM子用户
①service_oss模块中引入阿里云oss依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId></dependency>
<!-- 日期工具栏依赖 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
②找到编码时要用到的常量值,,放入application.properties
#阿里云 OSS
#不同的服务器,地址不同
aliyun.oss.file.endpoint=oss-cn-beijing.aliyuncs.com
aliyun.oss.file.keyid=LTAI5t8DVgWoCtKqyaUUXxvL
aliyun.oss.file.keysecret=7ykSrbtZIPc0VlYRptwgvrnGE39z6w
#bucket可以在控制台创建,也可以使用java代码创建
aliyun.oss.file.bucketname=edu-545466093
③在启动类上加上注解,排除数据源
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
①从配置文件中读取OSS的相关密匙,使用@Value注解读取。 用spring的 InitializingBean
的 afterPropertiesSet
来初始化配置信息,这个方法将在所有的属性被初始化 后调用。 在service_oss模块com/atguigu.oss.utils包下创建ConstantPropertiesUtils.java文件
@Component
public class ConstantPropertiesUtils 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 ACCESS_KEY_ID;
public static String ACCESS_KEY_SECRET;
public static String BUCKET_NAME;
public static String END_POINT;
@Override
public void afterPropertiesSet() throws Exception {
END_POINT = endpoint;
ACCESS_KEY_ID = keyId;
ACCESS_KEY_SECRET = keySecret;
BUCKET_NAME = bucketName;
}
}
②在service包下创建接口OssService.java
public interface OssService {
String uploadFileAvatar(MultipartFile file);
}
③在service/impl包下创建OssService接口的实现类OssServiceImpl.java。这里我们使用简单文件流上传
@Service
public class OssServiceImpl implements OssService {
//上传头像到OSS
@Override
public String uploadFileAvatar(MultipartFile file) {
// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
String endpoint = ConstantPropertiesUtils.END_POINT;
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = ConstantPropertiesUtils.ACCESS_KEY_ID;
String accessKeySecret = ConstantPropertiesUtils.ACCESS_KEY_SECRET;
String bucketName = ConstantPropertiesUtils.BUCKET_NAME;
try {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
InputStream inputStream = null;
inputStream = file.getInputStream();
//获取文件名称
String fileName = file.getOriginalFilename();
//在文件名称里面添加上随机的唯一的一个值
String uuid = UUID.randomUUID().toString().replaceAll("-","");
fileName = uuid + fileName ;
//把文件按照日期分类,获取当前日期
String dataPath = new DateTime().toString("yyyy/MM/dd");
//拼接
fileName = dataPath + "/" + fileName;
// 依次填写Bucket名称(例如examplebucket)和Object完整路径(例如exampledir/exampleobject.txt)。Object完整路径中不能包含Bucket名称。
ossClient.putObject(bucketName, fileName, inputStream);
//https://edu-545466093.oss-cn-beijing.aliyuncs.com/1.png
// 关闭OSSClient。
ossClient.shutdown();
//上传的aliyun的路径手动拼接
String url = "https://" + bucketName + "." + endpoint + "/" + fileName;
return url;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
④在controller包下从创建OssController.java文件
@RestController
@RequestMapping("/eduoss/fileoss")
public class OssController {
@Autowired
private OssService ossService;
//上传讲师头像
@PostMapping
public R uploadOssFile(MultipartFile file) {
//获取上传文件,MultipartFile
//返回上传的oss路径
String url = ossService.uploadFileAvatar(file);
return R.ok().data("url", url);
}
}
⑤配置nginx反向代理
location ~ /eduoss/ {
proxy_pass http://localhost:8001;
}
⑥相关前端代码在上方已经整合
①课程分类的添加并不是手动添加的,而是使用easyexcel读取excel内容自动添加。service_edu模块中导入easyexcel依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
②创建分类对应的实体类SubjectData.java
@Data
public class SubjectData {
@ExcelProperty(index = 0)
private String oneSubjectName;
@ExcelProperty(index = 1)
private String twoSubjectName;
}
③在EduSubjectController.java创建文件读取接口
//添加课程分类
//获取上传过来的文件,把文件内容读取出来
@PostMapping("addSubject")
public R addSubject(MultipartFile file) {
subjectService.saveSubject(file, subjectService);
return R.ok();
}
④service层创建saveSubject方法的实现
@Override
public void saveSubject(MultipartFile file,EduSubjectService subjectService) {
try {
//文件按输入流
InputStream in = file.getInputStream();
EasyExcel.read(in, SubjectData.class, new SubjectExcelLinstener(this)).sheet().doRead();
} catch(Exception e) {
e.printStackTrace();
}
}
④创建SubjectExcelLinstener.java,Excel监听器
public class SubjectExcelLinstener extends AnalysisEventListener<SubjectData> {
public EduSubjectService subjectService;
public SubjectExcelLinstener() {
}
public SubjectExcelLinstener(EduSubjectService subjectService) {
this.subjectService = subjectService;
}
//读取Excel内容
@Override
public void invoke(SubjectData subjectData, AnalysisContext analysisContext) {
if(subjectData == null) {
throw new GuliException(20001, "文件数据为空");
}
//一行一行读取的,每次读取两个值
//一级分类
EduSubject existOneSubject = this.existOneSubject(subjectService, subjectData.getOneSubjectName());
if(existOneSubject == null) {
existOneSubject = new EduSubject();
existOneSubject.setParentId("0");
existOneSubject.setTitle(subjectData.getOneSubjectName());
subjectService.save(existOneSubject);
}
//二级分类
String pid = existOneSubject.getId();
EduSubject existTwoSubject = this.existTwoSubject(subjectService, subjectData.getTwoSubjectName(), pid);
if(existTwoSubject == null) {
existTwoSubject = new EduSubject();
existTwoSubject.setParentId(pid);
existTwoSubject.setTitle(subjectData.getTwoSubjectName());
subjectService.save(existTwoSubject);
}
}
//判断一级分类不能重复
private EduSubject existOneSubject(EduSubjectService subjectService, String name) {
QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
wrapper.eq("title", name);
wrapper.eq("parent_id", "0");
EduSubject oneSubject = subjectService.getOne(wrapper);
return oneSubject;
}
//判断二级分类不重复
private EduSubject existTwoSubject(EduSubjectService subjectService, String name, String pid) {
QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();
wrapper.eq("title", name);
wrapper.eq("parent_id", pid);
EduSubject twoSubject = subjectService.getOne(wrapper);
return twoSubject;
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
⑤前端router.js中添加路由
{
path: '/subject',
component: Layout,
redirect: '/subject/list',
name: '课程分类管理',
meta: { title: '课程分类管理', icon: 'example' },
children: [
{
path: 'list',
name: '课程分类列表',
component: () => import('@/views/edu/subject/list'),
meta: { title: '课程分类列表', icon: 'table' }
},
{
path: 'save',
name: '添加课程分类',
component: () => import('@/views/edu/subject/save'),
meta: { title: '添加课程分类', icon: 'tree' }
}
]
}
⑥创建api接口,在src/api目录下创建subject.js
import request from '@/utils/request'
export default {
//1.课程分类列表
getSubjectList(current, limit, teacherQuery) {
return request({
//url: '/table/list/' + current + "/" + limit,
//url: '/eduservice/teacher/pageTeacherCondition/' + current + '/' + limit,
url: `/eduservice/subject/getAllSubject`,
method: 'get',
//teachQuery条件对象,后端使用RequestBody获取数据
//data将对象转为json传递到接口里面
data: teacherQuery
})
}
}
⑦在src/views/edu/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/01.xlsx'">点击下载模版</a>
</el-tag>
</el-form-item>
<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="application/vnd.ms-excel">
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button
:loading="loading"
style="margin-left: 10px;"
size="small"
type="success"
@click="submitUpload">
上传到服务器
</el-button>
</el-upload>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
BASE_API: process.env.BASE_API, // 接口API地址
importBtnDisabled: false, // 按钮是否禁用,
loading: false
}
},
created() {
},
methods: {
//点击按钮上传文件到接口里面
submitUpload() {
this.importBtnDisabled = true
this.loading = true
this.$refs.upload.submit()
},
//上传成功
fileUploadSuccess() {
this.loading = false
this.$message({
type: 'success',
message: "添加课程分类成功"
})
//跳转到列表
this.$router.push({path:'/subject/list'})
},
//上传失败
fileUploadError() {
this.loading = false
this.$message({
type: 'success',
message: "添加课程分类失败"
})
}
}
}
</script>
⑧在src/views/edu/subject目录下创建list.vue
<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/edu/subject"
export default {
data() {
return {
filterText: '',
data2: [],
defaultProps: {
children: 'children',
label: 'title'
}
}
},
created() {
this.getAllSubjectList();
},
watch: {
filterText(val) {
this.$refs.tree2.filter(val)
}
},
methods: {
getAllSubjectList() {
subject.getSubjectList()
.then(response => {
this.data2 = response.data.list;
})
},
filterNode(value, data) {
if (!value) return true
return data.title.toLowerCase().indexOf(value.toLowerCase()) !== -1
}
}
}
</script>
①课程分类有两个分级,每个一级分类下会有好多二级分类,因此我们可以创建自定义实体类,一级分类实体内有二级分类列表。在entity/subject包中创建二级分类实体类TwoSubject.java
@Data
public class TwoSubject {
private String id;
private String title;
}
创建一级分类实体类OneSubject.java
@Data
public class OneSubject {
private String id;
private String title;
//一个一级分类有多个二级分类
private List<TwoSubject> children = new ArrayList<>();
}
②编写controller接口
//课程分类的列表(树形)
@GetMapping("getAllSubject")
public R getAllSubject() {
//list集合的泛型是一级分类
List<OneSubject> list = subjectService.getAllOneTwoSubject();
return R.ok().data("list", list);
}
③编写service层的实现,将二级分类封装近一级分类实体类里面
@Override
public List<OneSubject> getAllOneTwoSubject() {
//1查询所有的一级分类
QueryWrapper<EduSubject> wrapperOne = new QueryWrapper<>();
wrapperOne.eq("parent_id", 0);
List<EduSubject> oneSubjectList = baseMapper.selectList(wrapperOne);
//2查询所有的二级分类
QueryWrapper<EduSubject> wrapperTwo = new QueryWrapper<>();
wrapperTwo.ne("parent_id", 0);
List<EduSubject> twoSubjectList = baseMapper.selectList(wrapperTwo);
//创建List集合,用于存储最终的数据
List<OneSubject> res = new ArrayList<>();
//将一级分类放入最终结果
Map<String, Integer> map = new HashMap<>();
int index = 0;
for(EduSubject one : oneSubjectList) {
OneSubject temp = new OneSubject();
BeanUtils.copyProperties(one, temp);
res.add(temp);
map.put(temp.getId(), index);
index += 1;
}
//System.out.println(twoSubjectList);
//将二级分类放入最终结果
for(EduSubject two : twoSubjectList) {
TwoSubject temp = new TwoSubject();
BeanUtils.copyProperties(two, temp);
res.get(map.get(two.getParentId())).getChildren().add(temp);
}
return res;
}
④前端页面已经整合
发布课程分为三步:填写课程基本信息、创建课程大纲、提交审核
①创建form表单对象CourseInfoVo.java
@Data
public class CourseInfoVo {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "课程ID")
private String id;
@ApiModelProperty(value = "课程讲师ID")
private String teacherId;
@ApiModelProperty(value = "课程专业ID")
private String subjectId;
@ApiModelProperty(value = "一级分类ID")
private String subjectParentId;
@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;
}
②自动生成的EduCourseDescription实体类主键是自动生成的,但是实际上需要绑定课程的id,所以我们取消它的逐渐自动生成策略。
@ApiModelProperty(value = "课程ID")
//需要手动输入,不会自动生成
@TableId(value = "id", type = IdType.INPUT)
private String id;
③定义controller接口新增课程
//添加课程基本信息的方法
@PostMapping("addCourseInfo")
public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {
String id = courseService.saveCourseInfo(courseInfoVo);
return R.ok().data("courseId", id);
}
④定义service层新增课程的具体实现
@Override
public String saveCourseInfo(CourseInfoVo courseInfoVo) {
//1.向课程表里面添加课程信息
//CourseInfoVo对象转换为eduCourse对象
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo, eduCourse);
int insert = baseMapper.insert(eduCourse);
if(insert <= 0) {
throw new GuliException(20001,"添加课程信息失败");
}
String cid = eduCourse.getId();
//2.向课程简介表添加课程简介
EduCourseDescription courseDescription = new EduCourseDescription();
courseDescription.setId(cid);
courseDescription.setDescription(courseInfoVo.getDescription());
courseDescriptionService.save(courseDescription);
return cid;
}
⑤定义controller层查询所有讲师,因为在填写课程信息时需要选中该课程的讲师
@ApiOperation(value="所有讲师列表")
@GetMapping("/findAll")
public R findAllTeacher() {
//调用service方法实现查询所有的操作
List<EduTeacher> list = teacherService.list(null);
return R.ok().data("items", list);
}
⑥前端添加课程管理相关路由
{
path: '/course',
component: Layout,
redirect: '/course/list',
name: '课程管理',
meta: { title: '课程管理', icon: 'example' },
children: [
{
path: 'list',
name: '课程列表',
component: () => import('@/views/edu/course/list'),
meta: { title: '课程列表', icon: 'table' }
},
{
path: 'info',
name: '添加课程',
component: () => import('@/views/edu/course/info'),
meta: { title: '添加课程', icon: 'tree' }
},
{
path: 'info/:id',
name: 'EduCourseInfoEdit',
component: () => import('@/views/edu/course/info'),
meta: { title: '编辑课程基本信息', noCache: true },
hidden: true
},
{
path: 'chapter/:id',
name: 'EduCourseChapterEdit',
component: () => import('@/views/edu/course/chapter'),
meta: { title: '编辑课程大纲', noCache: true },
hidden: true
},
{
path: 'publish/:id',
name: 'EduCoursePublishEdit',
component: () => import('@/views/edu/course/publish'),
meta: { title: '发布课程', noCache: true },
hidden: true
}
]
}
⑦前端添加课程基本信息页面,创建info.vue
<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.title" :value="subject.id"/>
</el-select>
<!-- 二级分类 -->
<el-select v-model="courseInfo.subjectId" placeholder="二级分类">
<el-option v-for="subject in subjectTwoList" :key="subject.id" :label="subject.title" :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-select v-model="courseInfo.subjectParentId" placeholder="请选择">
<el-option v-for="subject in subjectNestedList" :key="subject.id"
:label="subject.title" :value="subject.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"
:auto-upload="true"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
:action="BASE_API+'/eduoss/fileoss'"
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="免费课程请设置为0元"/>
</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 subject from '@/api/edu/subject'
import Tinymce from '@/components/Tinymce'//引入组件
export default{
//声明组件
components:{Tinymce},
data() {
return {
saveBtnDisabled:false,
courseInfo:{
title: '',
subjectId: '',//二级分类ID
subjectParentId:'',//一级分类ID
teacherId: '',
lessonNum: 0,
description: '',
cover: '/static/love.jpg',
price: 0
},
BASE_API: process.env.BASE_API,
teacherList:[],//讲师数据
subjectOneList:[],//一级分类
subjectTwoList:[],//二级分类
courseId:''
}
},
created() {
//初始化所有讲师
this.getListTeacher()
//初始化一级分类
this.getOneSubject()
//获取路由的id
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
this.getInfo();
} else {
//初始化所有的讲师
this.getListTeacher()
//初始化一级分类
this.getOneSubject()
}
},
methods:{
//根据课程ID查询信息
getInfo() {
course.getCourseInfoId(this.courseId)
.then(response => {
//在CourseInfo中有基本信息,包括一级分类二级分类
this.courseInfo = response.data.courseInfoVo
//1查询出所有的分类
subject.getSubjectList()
.then(response => {
//获取所有的一级分类
this.subjectOneList = response.data.list
//3.把所有的一级分类的数组进行遍历,比较当前courseInfo里面的一级分类id和所有的一级分类id
for(var i = 0; i < this.subjectOneList.length; ++i) {
//获取每个一级分类
var oneSubject = this.subjectOneList[i]
//比较一下id
if(this.courseInfo.subjectParentId == oneSubject.id) {
//获取一级分类里面所有的二级分类
this.subjectTwoList = oneSubject.children
}
}
})
})
},
//上传封面成功调用的方法
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
},
//点击一级分类,会触发change事件,显示对应的二级分类
subjectLevelOneChanged(value) {
//value就是一级分类id值
for(var i = 0; i < this.subjectOneList.length; ++i) {
var oneSubject = this.subjectOneList[i];
if(oneSubject.id === value) {
this.subjectTwoList = oneSubject.children
this.courseInfo.subjectId =""
break;
}
}
},
//查询所有的一级分类
getOneSubject() {
subject.getSubjectList()
.then(response => {
this.subjectOneList = response.data.list;
})
},
//查询所有的讲师
getListTeacher() {
course.getListTeacher()
.then(response => {
this.teacherList = response.data.items
})
},
//添加课程
addCourse() {
course.addCourseInfo(this.courseInfo)
.then(response => {
this.$message({
type:'success',
message:'添加课程信息成功'
})
//跳转到下一个
this.$router.push({path:'/course/chapter/' + response.data.courseId})
})
},
//修改课程
updateCourse() {
course.updateCourseInfoId(this.courseInfo)
.then(response => {
this.$message({
type:'success',
message:'修改课程信息成功'
})
//跳转到下一个
this.$router.push({path:'/course/chapter/' + this.courseId})
})
},
saveOrUpdate() {
//判断添加还是修改
if (this.$route.params && this.$route.params.id) {
this.updateCourse()
} else {
this.addCourse()
}
}
}
}
</script>
<style scoped>
.tinymce-container {
line-height: 29px;
}
</style>
①课程大纲主要包括添加章节,在章节中添加小节,每个小节中有视频。定义添加章节和添加小节的接口
//添加章节
@PostMapping("addChapter")
public R addChapter(@RequestBody EduChapter eduChapter) {
chapterService.save(eduChapter);
return R.ok();
}
//添加小节
@PostMapping("addVideo")
public R addVideo(@RequestBody EduVideo eduVideo) {
videoService.save(eduVideo);
return R.ok();
}
②如果在添加的过程中添加了错误的章节或者小节,我们需要将他删除。删除章节的时候,我们需要判断它下面是否有小节,有的话就不能删除。删除章节的接口:
//删除的方法
@DeleteMapping("{chapterId}")
public R deleteChapter(@PathVariable String chapterId) {
boolean flag = chapterService.deleteChapter(chapterId);
if(flag) {
return R.ok();
} else {
return R.error();
}
}
删除章节的service层具体实现:
//删除章节方法
@Override
public boolean deleteChapter(String chapterId) {
//看看有没有小节 去查询小节表,如果查询数据,不进行删除
QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
wrapper.eq("chapter_id", chapterId);
if(videoService.count(wrapper) == 0) {
int result = baseMapper.deleteById(chapterId);
return result > 0;
} else {//查询出数据
throw new GuliException(20001, "不能删除");
}
}
删除小节的时候我们要将上传到阿里云的视频一并删除,这里就需要调用远程服务了。创建包com.atguigu.eduservice.client,在包下创建远程调用接口VodClient.java,注意相关的api函数名称要相同:
@FeignClient(name = "service-vod", fallback = VodFileDegradeFeignClient.class)
@Component
public interface VodClient {
//定义调用发放的路径
//根据视频ID删除视频
@DeleteMapping("/eduvod/video/remove/{id}")
public R removeAliyunVideo(@PathVariable("id") String id);
//删除多个阿里云视频的方法
//参数是多个视频ID
@DeleteMapping("/eduvod/video/deleteBatch")
public R deleteBatch(@RequestParam("videoIdList") List<String> voidIdList);
}
然后再controller中调用远程接口删除阿里云中的视频:
//删除小节并且删除对应的阿里云的视频
@DeleteMapping("{id}")
public R deleteVideo(@PathVariable String id) {
//根据小节id获取视频id
EduVideo eduVideo = videoService.getById(id);
String videoSourceId = eduVideo.getVideoSourceId();
//判断小节里面是否有视频id
if(!StringUtils.isEmpty(videoSourceId)) {
//根据视频id,远程调用实现视频删除
R r = vodClient.removeAliyunVideo(videoSourceId);
if(r.getCode() == 20001) {
throw new GuliException(20001, "删除视频失败,熔断器....");
}
}
videoService.removeById(id);
return R.ok();
}
前端页面:
<template>
<div class="app-container">
<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>
<el-button type="text" @click="openChapterDialog()">添加章节</el-button>
<!-- 章节 -->
<ul class="chanpterList">
<li v-for="chapter in chapterVideoList" :key="chapter.id">
<p>
{{chapter.title}}
<span class="acts">
<el-button type="text" @click="openVideo(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="removeVideo(video.id)">删除</el-button>
</span>
</p>
</li>
</ul>
</li>
</ul>
<el-form>
<el-form-item>
<el-button @click="previous">上一步</el-button>
<el-button :disabled="saveBtnDisabled" type="primary" @click="next">下一步</el-button>
</el-form-item>
</el-form>
<!-- 添加和修改章节表单 -->
<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="上传视频">
<el-upload :on-success="handleVodUploadSuccess" :on-remove="handleVodRemove" :before-remove="beforeVodRemove"
:on-exceed="handleUploadExceed" :file-list="fileList" :action="BASE_API+'/eduvod/video/upload'" :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>
</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{
data() {
return {
saveBtnDisabled: false,
chapterVideoList:[],
courseId:'',
chapter:{
title:'',
sort:0,
},
video:{
title:'',
sort:0,
free:0,
videoSourceId:''
},
dialogChapterFormVisible:false,
dialogVideoFormVisible:false,
//上传视频的值
fileList: [],//上传文件列表
BASE_API: process.env.BASE_API // 接口API地址
}
},
created() {
//获取路由的id
if (this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
//根据课程id查询章节和小节
this.getChapterVideo(this.courseId)
}
},
methods:{
//点击x执行的方法
beforeVodRemove(file, fileList){
return this.$confirm(`确定移除 ${ file.name }?`);
},
//点击确定会执行的方法
handleVodRemove(){
//调用接口的删除视频的方法
video.deleteAliyunvod(this.video.videoSourceId)
.then(response => {
this.$message({
type:'success',
message:'删除视频成功'
})
//文件列表清空
this.fileList = []
//清空视频id和名称
this.video.videoSourceId = ""
this.video.videoOriginalName = ""
})
},
//上传视频成功的方法
handleVodUploadSuccess(response, file, fileList) {
this.video.videoSourceId = response.data.videoId
this.video.videoOriginalName = file.name
},
//上传之前的方法
handleUploadExceed() {
this.$message.warning('想要重新上传视频,请先删除已上传的视频')
},
//=================小节==================
//打开弹窗
openEditVideo(videoId) {
//弹框
this.dialogVideoFormVisible = true
console.log(videoId + "=====================")
//调用接口
video.getVideo(videoId)
.then(response => {
this.video = response.data.video
})
},
//修改小节
updateVideo() {
this.dialogVideoFormVisible = false
video.updateVideo(this.video)
.then(response => {
this.$message({
type:'success',
message:'修改小节信息成功'
})
//刷新页面
this.getChapterVideo()
})
},
//删除小节
removeVideo(id) {
this.$confirm('此操作将永久删除小节记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
video.deleteVideo(id)
.then(response => {//删除成功
//提示信息
this.$message({
type: 'success',
message: '删除成功!'
});
//刷新页面
this.getChapterVideo()
})
})
},
//添加小节弹框的方法
openVideo(chapterId) {
//弹框
this.dialogVideoFormVisible = true;
this.video.title = '';
this.video.sort = 0;
this.video.free = true;
this.video.id='';
//设置章节id
this.video.chapterId = chapterId;
//设置课程ID
this.video.courseId = this.courseId;
//清空视频列表
this.fileList = []
},
//添加小节
addVideo() {
video.addVideo(this.video)
.then(response => {
//关闭弹框
this.dialogVideoFormVisible = false
//提示信息
this.$message({
type:'success',
message:'添加小节信息成功'
})
//刷新页面
this.getChapterVideo()
})
},
saveOrUpdateVideo(){
if(this.video.id) {
this.updateVideo()
} else {
this.addVideo()
}
},
//=================章节==================
//删除章节
removeChapter(chapterId) {
this.$confirm('此操作将永久删除章节记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
chapter.deleteChapter(chapterId)
.then(response => {//删除成功
//提示信息
this.$message({
type: 'success',
message: '删除成功!'
});
//刷新页面
this.getChapterVideo()
})
})
},
//修改章节弹框数据回显
openEditChapter(chapterId) {
//弹框
this.dialogChapterFormVisible = true
//调用接口
chapter.getChapter(chapterId)
.then(response => {
this.chapter = response.data.chapter
})
},
//弹出页面
openChapterDialog() {
//弹框
this.dialogChapterFormVisible=true
this.chapter.title=''
this.chapter.title=''
this.chapter.sort=0
},
addChapter() {
this.chapter.courseId = this.courseId
chapter.addChapter(this.chapter)
.then(response => {
//关闭弹框
this.dialogChapterFormVisible = false
//提示信息
this.$message({
type:'success',
message:'添加章节信息成功'
})
//刷新页面
this.getChapterVideo()
})
},
//修改章节的方法
updateChapter() {
chapter.updateChapter(this.chapter)
.then(response => {
//关闭弹框
this.dialogChapterFormVisible = false
//提示信息
this.$message({
type:'success',
message:'修改章节信息成功'
})
//刷新页面
this.getChapterVideo()
})
},
//添加章节
saveOrUpdate() {
if(!this.chapter.id) {
this.addChapter()
} else {
this.updateChapter()
}
},
//根据课程Id查询章节和小节
getChapterVideo() {
chapter.getAllChapterVideo(this.courseId)
.then(response => {
this.chapterVideoList = response.data.allChapterVideo
})
},
previous() {
console.log('previous')
this.$router.push({ path: '/course/info/' + this.courseId})
},
next() {
console.log('publish')
this.$router.push({ path: '/course/publish/' + this.courseId })
}
}
}
</script>
<style scoped>
.chanpterList{
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.chanpterList li{
position: relative;
}
.chanpterList p{
font-size: 20px;
margin: 10px 0;
padding: 10px;
height: 70px;
line-height: 50px;
width: 100%;
border: 1px solid #DDD;
}
.chanpterList .acts {
float: right;
font-size: 14px;
}
.videoList{
padding-left: 50px;
}
.videoList p{
font-size: 14px;
margin: 10px 0;
padding: 10px;
height: 50px;
line-height: 30px;
width: 100%;
border: 1px dotted #DDD;
}
</style>
①相当于是一个确认的界面,我们要从数据库中查出一些基本的信息,让管理员确认。内容包括了封面、讲师、课程id、课时、分类、价格。包含了课程表、讲师表、分类表。这里我们使用自定义sql语句去查询
SELECT ec.id,ec.title,ec.price,ec.lesson_num as lessonNum,ec.cover,
et.`name` as teacherName,
es1.title as subjectLevelOne,
es2.title as subjectLevelTwo
FROM edu_course ec LEFT OUTER JOIN edu_course_description ecd ON ec.id=ecd.id
LEFT OUTER JOIN edu_teacher et ON ec.teacher_id=et.id
LEFT OUTER JOIN edu_subject es1 ON ec.subject_parent_id=es1.id
LEFT OUTER JOIN edu_subject es2 ON ec.subject_id=es2.id
WHERE ec.id=#{value}
后端接口:
//根据课程id查询课程确认信息
@GetMapping("getPublishCourseInfo/{id}")
public R getPublishCourseInfo(@PathVariable String id ) {
CoursePublishVo coursePublishVo = courseService.getPublishCourseInfo(id);
return R.ok().data("coursePublishVo", coursePublishVo);
}
EduCourseMapper中添加方法,然后再xml中间中写sql语句:
public CoursePublishVo getPublishCourseInfo(String courseId);
②提交课程,这个步骤比较简单,我们先前已经保存了课程的信息,这里只需要将课程的状态改为'Normal'即可。后端接口:
//课程最终发布
//修改课程状态
@PostMapping("publishCourse/{id}")
public R publishCourse(@PathVariable String id) {
EduCourse eduCourse = new EduCourse();
eduCourse.setId(id);
eduCourse.setStatus("Normal");
courseService.updateById(eduCourse);
return R.ok();
}
②前端页面
<template>
<div class="app-container">
<h2 style="text-align: center;">发布新课程</h2>
<el-steps :active="3" process-status="wait" align-center style="margin-bottom: 40px;">
<el-step title="填写课程基本信息"/>
<el-step title="创建课程大纲"/>
<el-step title="提交审核"/>
</el-steps>
<div class="ccInfo">
<img :src="coursePublish.cover">
<div class="main">
<h2>{{ coursePublish.title }}</h2>
<p class="gray"><span>共{{ coursePublish.lessonNum }}课时</span></p>
<p><span>所属分类:{{ coursePublish.subjectLevelOne }} — {{
coursePublish.subjectLevelTwo }}</span></p>
<p>课程讲师:{{ coursePublish.teacherName }}</p>
<h3 class="red">¥{{ coursePublish.price }}</h3>
</div>
</div>
<div>
<el-button @click="previous">返回修改</el-button>
<el-button :disabled="saveBtnDisabled" type="primary"
@click="publish">发布课程</el-button>
</div>
</div>
</template>
<script>
import course from '@/api/edu/course'
export default{
data() {
return {
saveBtnDisabled: false,
courseId:'',
coursePublish:{}
}
},
created() {
//获取路由的id值
if(this.$route.params && this.$route.params.id) {
this.courseId = this.$route.params.id
}
//调用接口方法根据课程id查询
this.getCoursePublishId()
console.log(coursePublish)
},
methods:{
//根据课程id查询课程信息
getCoursePublishId() {
course.getPublishCourseInfo(this.courseId)
.then(response => {
this.coursePublish = response.data.coursePublishVo
})
},
previous() {
console.log('previous')
this.$router.push({ path: '/course/chapter/' + this.courseId })
},
publish() {
course.publishCourse(this.courseId)
.then(response => {
//提示信息
this.$message({
type:'success',
message:'发布课程信息成功'
})
//跳转列表页面
this.$router.push({ path: '/course/list' })
})
}
}
}
</script>
<style scoped>
.ccInfo {
background: #f5f5f5;
padding: 20px;
overflow: hidden;
border: 1px dashed #DDD;
margin-bottom: 40px;
position: relative;
}
.ccInfo img {
background: #d6d6d6;
width: 500px;
height: 278px;
display: block;
float: left;
border: none;
}
.ccInfo .main {
margin-left: 520px;
}
.ccInfo .main h2 {
font-size: 28px;
margin-bottom: 30px;
line-height: 1;
font-weight: normal;
}
.ccInfo .main p {
margin-bottom: 10px;
word-wrap: break-word;
line-height: 24px;
max-height: 48px;
overflow: hidden;
}
.ccInfo .main p {
margin-bottom: 10px;
word-wrap: break-word;
line-height: 24px;
max-height: 48px;
overflow: hidden;
}
.ccInfo .main h3 {
left: 540px;
bottom: 20px;
line-height: 1;
font-size: 28px;
color: #d32f24;
font-weight: normal;
position: absolute;
}
</style>
可能会报如下错误:
org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver
|Resolved exception caused by handler execution: org.apache.ibatis.binding.BindingException: Invalid
bound statement (not found): com.guli.edu.mapper.CourseMapper.getCoursePublishVoById
因为dao层编译后只有class文件,没有mapper.xml文件,maven工程在默认情况下不会将src/main/java目录下的所有资源文件发布到target目录下的。
在pom.xml中添加build的设置:
<!-- 项目打包时会将java目录中的*.xml文件也进行打包 -->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
或者application.properties文件中添加配置
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/eduservice/mapper/xml/*.xml
①编写分页查询controller接口api,需要查询的课程信息都在一个表中,因此不需要多表查询。这里我们可以使用@PostMapping,因为发送来的请求中请求体有内容,要用前端post方式发送。
//完善成添加查询带分页
@PostMapping("getAllCourse/{current}/{limit}")
public R getCourseList(@PathVariable long current,
@PathVariable long limit,
@RequestBody(required = false) CourseQuery courseQuery) {
//创建page对象
Page<EduCourse> pageCourse = new Page<>(current, limit);
//构建条件
QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
//动态sql,判断条件值是否为空 否的话拼接上
String title = courseQuery.getTitle();
String status = courseQuery.getStatus();
Integer low = courseQuery.getLow();
Integer high = courseQuery.getHigh();
if(!StringUtils.isEmpty(title)) {
wrapper.like("title", title);
}
if(!StringUtils.isEmpty(status)) {
wrapper.eq("status", status);
}
if(!StringUtils.isEmpty(low)) {
wrapper.ge("price", low);
}
if(!StringUtils.isEmpty(high)) {
wrapper.le("price", high);
}
//排序
wrapper.orderByDesc("gmt_create");
//分页查询
courseService.page(pageCourse, wrapper);
long total = pageCourse.getTotal();//总记录数
List<EduCourse> records = pageCourse.getRecords();//数据list集合
return R.ok().data("total", total).data("rows", records);
}
②前端course.js中新增请求接口:
//查询所有的课程
getListCourse(current, limit, courseQuery) {
return request({
url: `/eduservice/course/getAllCourse/${current}/${limit}`,
method: 'post',
data: courseQuery
})
}
③前端页面list.vue
<template>
<div>
<!--查询表单-->
<el-form :inline="true" class="demo-form-inline">
<el-form-item>
<el-input v-model="courseQuery.title" placeholder="课程名"/>
</el-form-item>
<el-form-item>
<el-select v-model="courseQuery.status" clearable placeholder="课程状态">
<el-option :value='"Normal"' label="已发布"/>
<el-option :value='"Draft"' label="未发布"/>
</el-select>
</el-form-item>
<el-form-item>
<el-input v-model="courseQuery.low" placeholder="最低价格"/>
</el-form-item>
<el-form-item>
<el-input v-model="courseQuery.high" placeholder="最高价格"/>
</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>
<!-- 表格 -->
<el-table :data="list" element-loading-text="数据加载中" border fit highlight-current-row>
<el-table-column label="序号" width="70" align="center">
<template slot-scope="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="title" label="课程名称" width="300" />
<el-table-column label="课程状态" width="100">
<template slot-scope="scope">
{{ scope.row.status==="Normal" ? '已发布':'未发布' }}
</template>
</el-table-column>
<el-table-column prop="lessonNum" label="课时数" width="100px"/>
<el-table-column prop="gmtCreate" label="添加时间" width="200px"/>
<el-table-column prop="viewCount" label="浏览数量" width="100px"/>
<el-table-column label="操作" align="center" width="400px">
<template slot-scope="scope">
<router-link :to="'/course/info/'+scope.row.id">
<el-button type="primary" size="mini" icon="el-icon-edit">编辑课程基本信息</el-button>
</router-link>
<router-link :to="'/course/chapter/'+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>
<!-- 分页 -->
<el-pagination
:current-page="page"
:page-size="limit"
:total="total"
style="padding: 30px 0; text-align: center;"
background
layout="total, prev, pager, next, jumper"
@current-change="getList"
/>
</div>
</template>
<script>
//引入调用Course.js文件
import course from '@/api/edu/course'
export default{
data() {//定义变量和初始值
return {
list:[],
page:1,//第一页
limit:3,//记录数10
total:0,
courseQuery:{}
}
},
created() {//页面渲染之前执行,一般调用menthods定义的方法
this.getList()
},
methods:{//创建具体的方法,调用course.js定义的方法
getList(page=1) {
this.page = page;
course.getListCourse(this.page, this.limit, this.courseQuery)
.then(response => {
this.list = response.data.rows
this.total = response.data.total;
})
.catch(error => {
console.log(error);
})
},
resetData() {
//把表单输入项数据清空
this.courseQuery = {};
//查所有课程数据
this.getList();
},
removeDataById(id) {
this.$confirm('此操作将永久删除课程记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
course.deleteCourse(id)
.then(response => {//删除成功
//提示信息
this.$message({
type: 'success',
message: '删除成功!'
});
//再次回到列表页面
this.getList();
})
})
}
}
}
</script>
①修改课程信息时,首先我们要根据课程id查询课程信息,然后将当前的课程信息以及课程描述展示出来,展示使用的页面和添加课程基本信息页面相同。然后修改并保存。编写根据课程id查询课程信息的后端接口:
//根据课程id查询基本信息
@GetMapping("getCourseInfo/{courseId}")
public R getCourseInfo(@PathVariable String courseId) {
CourseInfoVo courseInfoVo = courseService.getCourseInfo(courseId);
return R.ok().data("courseInfoVo", courseInfoVo);
}
service层实现:
@Override
public CourseInfoVo getCourseInfo(String courseId) {
//1.先查询课程表
EduCourse eduCourse = baseMapper.selectById(courseId);
CourseInfoVo courseInfoVo = new CourseInfoVo();
BeanUtils.copyProperties(eduCourse, courseInfoVo);
//2.再查询简介表
EduCourseDescription courseDescription = courseDescriptionService.getById(courseId);
courseInfoVo.setDescription(courseDescription.getDescription());
return courseInfoVo;
}
②更新课程信息时,我们要先更新课程表,然后更新课程描述表,后端接口:
//修改课程信息
@PostMapping("updateCourseInfo")
public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {
courseService.updateCourseInfo(courseInfoVo);
return R.ok();
}
service层的实现:
@Override
public void updateCourseInfo(CourseInfoVo courseInfoVo) {
//1.修改课程表
EduCourse eduCourse = new EduCourse();
BeanUtils.copyProperties(courseInfoVo, eduCourse);
int update = baseMapper.updateById(eduCourse);
if(update == 0) {
throw new GuliException(20001, "修改课程信息失败");
}
//2.修改课程描述
EduCourseDescription description = new EduCourseDescription();
BeanUtils.copyProperties(courseInfoVo, description);
courseDescriptionService.updateById(description);
}
①在课程列表中点击删除按钮并确认即可删除课程信息、课程描述、课程章节、课程小节信息。后端接口:
//删除课程信息
@DeleteMapping("deleteCourse/{id}")
public R deleteCourse(@PathVariable String id) {
courseService.removeCourse(id);
return R.ok();
}
②service层:
@Override
public void removeCourse(String id) {
//删除小节
eduVideoService.removeVideo(id);
//删除章节
chapterService.removeChapter(id);
//删除描述
courseDescriptionService.removeById(id);
//删除课程
baseMapper.deleteById(id);
}
①和修改课程信息相似,要先获取课程大纲然后去修改提交。查询所有章节和小节的后端接口:
//课程大纲的列表,更具课程Id查询
@GetMapping("getChapterVideo/{courseId}")
public R getChapterVideo(@PathVariable String courseId) {
List<ChapterVo> list = chapterService.getChapterVideoByCourseId(courseId);
return R.ok().data("allChapterVideo", list);
}
service层实现:
//课程大纲列表,根据课程id进行查询
@Override
public List<ChapterVo> getChapterVideoByCourseId(String courseId) {
//1.根据课程id查询课程里面的所有章节
QueryWrapper<EduChapter> wrapperChapter = new QueryWrapper<>();
wrapperChapter.eq("course_id", courseId);
List<EduChapter> eduChapterList = baseMapper.selectList(wrapperChapter);
//2.根据课程id查询所有的小节
List<EduVideo> eduVideoList = videoService.selectList(courseId);
// for(EduVideo eduVideo : eduVideoList) {
// System.out.println(eduVideo);
// }
//创建list集合,最终数据
List<ChapterVo> res = new ArrayList<>();
Map<String, Integer> map = new HashMap<>();
//3.遍历查询章节list集合进行封装
int i = 0;
for(EduChapter eduChapter : eduChapterList) {
//System.out.println(eduChapter);
ChapterVo temp = new ChapterVo();
BeanUtils.copyProperties(eduChapter, temp);
res.add(temp);
map.put(eduChapter.getId(), i);
i += 1;
}
//System.out.println("======================");
//System.out.println(map);
//4.遍历查询小节list集合,进行封装
for(EduVideo eduVideo : eduVideoList) {
VideoVo temp = new VideoVo();
BeanUtils.copyProperties(eduVideo, temp);
//System.out.println(temp);
res.get(map.get(eduVideo.getChapterId())).getChildren().add(temp);
}
return res;
}
②前端在chapter.js中添加ajax请求,然后去修改和删除即可
//1.根据课程Id获取章节和小节
getAllChapterVideo(courseId) {
return request({
url: `/eduservice/chapter/getChapterVideo/${courseId}`,
method: 'get'
})
}
①在pom文件中添加依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>service</artifactId>
<groupId>com.atguigu</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>service_vod</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-vod</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-sdk-vod-upload</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
</dependencies>
</project>
安装非开源jar包
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
②初始化,创建测试用例
创建播放工具类InitObject
public class InitObject {
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;
}
}
获取视频播放地址:
@Test
public static void getPlayUrl() {
//1.根据视频的id,获取播放地址
//创建初始化对象
try {
DefaultAcsClient client = InitObject.initVodClient("LTAI5t8DVgWoCtKqyaUUXxvL", "7ykSrbtZIPc0VlYRptwgvrnGE39z6w");
//创建获取视频地址request和response
GetPlayInfoRequest request = new GetPlayInfoRequest();
GetPlayInfoResponse response = new GetPlayInfoResponse();
//向request对象里面设置视频ID值
request.setVideoId("290ebf723d234da09ec22d7296b6548d");
//调用初始化对象里面的方法,传递request,获取数据
response = client.getAcsResponse(request);
List<GetPlayInfoResponse.PlayInfo> playInfoList = response.getPlayInfoList();
System.out.println(playInfoList);
//播放地址
for (GetPlayInfoResponse.PlayInfo playInfo : playInfoList) {
System.out.print("PlayInfo.PlayURL = " + playInfo.getPlayURL() + "\n");
}
//Base信息
System.out.print("VideoBase.Title = " + response.getVideoBase().getTitle() + "\n");
} catch (Exception e) {
e.printStackTrace();
}
}
获取视频播放凭证:
@Test
public static void getPlayAuth(){
//根据视频ID获取视频播放凭证
//创建初始化对象
try {
DefaultAcsClient client = InitObject.initVodClient("LTAI5t8DVgWoCtKqyaUUXxvL", "7ykSrbtZIPc0VlYRptwgvrnGE39z6w");
//获取视频凭证的request和response对象
GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
GetVideoPlayAuthResponse response = new GetVideoPlayAuthResponse();
//向request设置视频id
request.setVideoId("c8c22c0a92564338957269178b25bbfb");
//调用初始化对象的方法得到凭证
response = client.getAcsResponse(request);
System.out.println("PlayAuth = " + response.getPlayAuth());
System.out.println("PlayAuth = " + response.getPlayAuth());
//VideoMeta信息
System.out.print("VideoMeta.Title = " + response.getVideoMeta().getTitle() + "\n");
} catch (ClientException e) {
e.printStackTrace();
}
}
③配置application.properties文件
# 服务端口
server.port=8003
# 服务名
spring.application.name=service-vod
# 环境设置:dev、test、prod
spring.profiles.active=dev
#阿里云 vod
#不同的服务器,地址不同
aliyun.vod.file.keyid=LTAI5t8DVgWoCtKqyaUUXxvL
aliyun.vod.file.keysecret=7ykSrbtZIPc0VlYRptwgvrnGE39z6w
# 最大上传单个文件大小:默认1M
spring.servlet.multipart.max-file-size=1024MB
# 最大置总上传的数据大小 :默认10M
spring.servlet.multipart.max-request-size=1024MB
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=101.132.146.181:8848
④创建常量类ConstantVodUtils.java
@Component
public class ConstantVodUtils implements InitializingBean {
@Value("${aliyun.vod.file.keyid}")
private String keyId;
@Value("${aliyun.vod.file.keysecret}")
private String keyPassword;
public static String ACCESS_KEY_ID;
public static String ACCESS_KEY_SECRET;
@Override
public void afterPropertiesSet() throws Exception {
ACCESS_KEY_ID = keyId;
ACCESS_KEY_SECRET = keyPassword;
}
}
⑤创建工具类InitVodClient.java,其实和测试时用到的工具类一样的
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;
}
}
⑥配置nginx反向代理:
location ~ /vod/ {
proxy_pass http://localhost:8003;
}
①编写Controller接口,定义后端上传接口:
//上传视频的方法
@PostMapping("upload")
public R uploadAliyunVideo(MultipartFile file) {
//返回上传视频的ID值
String videoId = vodService.uploadVideoAly(file);
return R.ok().data("videoId", videoId);
}
编写service层的具体实现:
@Override
public String uploadVideoAly(MultipartFile file) {
String fileName = file.getOriginalFilename();
String title = fileName.substring(0, fileName.lastIndexOf('.'));
InputStream inputStream = null;
try {
inputStream = file.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
UploadStreamRequest request = new UploadStreamRequest(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET, title, fileName, inputStream);
UploadVideoImpl uploader = new UploadVideoImpl();
UploadStreamResponse response = uploader.uploadStream(request);
String voidId = response.getVideoId(); //请求视频点播服务的请求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");
}
return voidId;
}
前端整合上传组件:
<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/upload'" :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>
</el-tooltip>
</el-upload>
</el-form-item>
成功之后回调的方法:
//上传视频成功的方法
handleVodUploadSuccess(response, file, fileList) {
this.video.videoSourceId = response.data.videoId
this.video.videoOriginalName = file.name
}
编写后端接口,直接在controller层删除
//根据视频ID删除视频
@DeleteMapping("remove/{id}")
public R removeAliyunVideo(@PathVariable String id) {
try {
//初始化对象
DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);
//创建一个删除视频的request对象
DeleteVideoRequest request = new DeleteVideoRequest();
//向request设置视频id
request.setVideoIds(id);
client.getAcsResponse(request);
return R.ok();
} catch (Exception e) {
e.printStackTrace();
throw new GuliException(20001, "删除视频失败");
}
}
前端接口定义:
//删除阿里云的视频
deleteAliyunvod(id) {
return request({
url: `/eduvod/video/remove/${id}`,
method: 'delete'
})
}
后端接口:
//删除多个阿里云视频的方法
//参数是多个视频ID
@DeleteMapping("deleteBatch")
public R deleteBatch(@RequestParam("videoIdList") List<String> voidIdList) {
vodService.removeMoreAlyVideo(voidIdList);
return R.ok();
}
service层实现:
@Override
public void removeMoreAlyVideo(List voidIdList) {
try {
//初始化对象
DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);
//创建一个删除视频的request对象
DeleteVideoRequest request = new DeleteVideoRequest();
String ids = StringUtils.join(voidIdList.toArray(), ",");
//向request设置视频id
request.setVideoIds(ids);
client.getAcsResponse(request);
} catch (Exception e) {
e.printStackTrace();
throw new GuliException(20001, "删除视频失败");
}
}
后端接口定义:
//根据视频id获取凭证
@GetMapping("getPlayAuth/{id}")
public R getPlayAuth(@PathVariable String id) {
//根据视频ID获取视频播放凭证
//创建初始化对象
try {
DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);
//获取视频凭证的request和response对象
GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
GetVideoPlayAuthResponse response = new GetVideoPlayAuthResponse();
//向request设置视频id
request.setVideoId(id);
//调用初始化对象的方法得到凭证
response = client.getAcsResponse(request);
return R.ok().data("playAuth", response.getPlayAuth());
} catch (ClientException e) {
throw new GuliException(20001, "获取凭证失败");
}
}
①nuxt: Nuxt.js 是一个基于 Vue.js 的轻量级应用框架,可用来创建服务端渲染 (SSR) 应用,也可充当静态站点引擎 生成静态站点应用,具有优雅的代码结构分层和热加载等特性。
②nuxt环境初始化:下载压缩包https://github.com/nuxt-community/starter-template/archive/master.zip
③解压,将template中的内容复制到guli
④修改package.json
"name": "name",
"version": "1.0.0",
"description": "谷粒学苑前台网站",
"author": "Hehehe<545466093@qq.com>"
⑤修改nuxt.config.js
修改title:{{name}}
、content:{{escape description}}
这里设置后的会显示在页面标题栏和meta数据中
head: {
title: '谷粒学院 - Java视频|HTML5视频|前端视频|Python视频|大数据视频-自学拿1万+月薪的IT在线视频课程,谷粉力挺,老学员为你推荐',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content:
'width=device-width, initial-scale=1' },
{ hid: 'keywords', name: 'keywords',content: '谷粒学院,IT在线视频教程,Java视频,HTML5视频,前端视频,Python视频,大数据视频' },
{ hid: 'description', name: 'description',content: '谷粒学院是国内领先的IT在线视频学习平台、职业教育平台。截止目前,谷粒学院线上、线下学习人次数以万计!会同上百个知名开发团队联合制定的Java、HTML5前端、大数据、Python等视频课程,被广大学习者及IT工程师誉为:业界最适合自学、代码量最大、案例最多、实战性最强、技术最前沿的IT系列视频课程!' }
],
link: [
{ rel: 'icon', type: 'image/x-icon',href: '/favicon.ico' }
]
}
⑥安装swiper插件,用于banner轮播
npm install vue-awesome-swiper
配置插件,在plugins文件夹新建文件nuxt-swipper-plugin.js
import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'
Vue.use(VueAwesomeSwiper)
在nuxt.config.js文件中配置插件
plugins: [
{ src: '~/plugins/nuxt-swiper-plugin.js', ssr: false }
],
css: [
'swiper/dist/css/swiper.css'
]
⑥进入项目目录安装依赖并启动
npm install
npm run dev
⑦路由设置,导航栏使用固定路由,使用router-link构建路由。例如课程的路由是"/course"。在layouts目录下的default中构建如下信息
<router-link to="/course" tag="li" active-class="current">
<a>课程</a>
</router-link>
在pages/course目录下创建文件index.vue,点击导航"/course"就能跳转到index.vue。
<template>
<div>
课程列表
</div>
</template>
点击不同课程需要使用动态路由, NUXT的动态路由是以下划线开头的vue文件,参数名为下划线后边的文件名 。在pages/course目录下创建_id.vue
<template>
<div>
课程详情
</div>
</template>
⑧封装axio
我们可以参考guli-admin将axios操作封装起来,下载axios使用命令
npm install axios
创建utils文件夹,utils下创建request.js
//创建axios实例
const service = axios.create({
baseURL: "http://localhost:8011", //api中的base_url
timeout: 20000
})
export default service
①课程页面
index.vue
<template>
<div id="aCoursesList" class="bg-fa of">
<!-- /课程列表 开始 -->
<section class="container">
<header class="comm-title">
<h2 class="fl tac">
<span class="c-333">全部课程</span>
</h2>
</header>
<section class="c-sort-box">
<section class="c-s-dl">
<dl>
<dt>
<span class="c-999 fsize14">课程类别</span>
</dt>
<!-- 一级分类 -->
<dd class="c-s-dl-li">
<ul class="clearfix">
<li>
<a title="全部" href="#">全部</a>
</li>
<li v-for="(item, index) in subjectNestedList" :key="index" :class="{active:oneIndex == index}">
<a :title="item.title" href="#" @click="searchOne(item.id, index)">{{item.title}}</a>
</li>
</ul>
</dd>
</dl>
<dl>
<dt>
<span class="c-999 fsize14"></span>
</dt>
<!-- 二级分类 -->
<dd class="c-s-dl-li">
<ul class="clearfix">
<li v-for="(item,index) in subSubjectList" :key="index" :class="{active:twoIndex == index}">
<a :title="item.title" href="#" @click="searchTwo(item.id, index)">{{item.title}}</a>
</li>
</ul>
</dd>
</dl>
<div class="clear"></div>
</section>
<div class="js-wrap">
<section class="fr">
<span class="c-ccc">
<i class="c-master f-fM">1</i>/
<i class="c-666 f-fM">1</i>
</span>
</section>
<!-- 排序方式 -->
<section class="fl">
<ol class="js-tap clearfix">
<li :class="{'current bg-orange':buyCountSort!=''}">
<a title="销量" href="javascript:void(0);" @click="searchBuyCount()">销量
<span :class="{hide:buyCountSort==''}">↓</span>
</a>
</li>
<li :class="{'current bg-orange':gmtCreateSort!=''}">
<a title="最新" href="javascript:void(0);" @click="searchGmtCreate()">最新
<span :class="{hide:gmtCreateSort==''}">↓</span>
</a>
</li>
<li :class="{'current bg-orange':priceSort!=''}">
<a title="价格" href="javascript:void(0);" @click="searchPrice()">价格
<span :class="{hide:priceSort==''}">↓</span>
</a>
</li>
</ol>
</section>
</div>
<div class="mt40">
<!-- /无数据提示 开始-->
<section class="no-data-wrap" v-if="data.total == 0">
<em class="icon30 no-data-ico"> </em>
<span class="c-666 fsize14 ml10 vam">没有相关数据,小编正在努力整理中...</span>
</section>
<!-- /无数据提示 结束-->
<article class="comm-course-list" v-if="data.total != 0">
<ul class="of" id="bna">
<li v-for="item in data.records" :key="item.id">
<div class="cc-l-wrap">
<section class="course-img">
<img :src="item.cover"
class="img-responsive" :alt="item.title">
<div class="cc-mask">
<a :href="'/course/' + item.id" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a :href="'/course/' + item.id" :title="item.title" class="course-title fsize18 c-333">{{item.title}}</a>
</h3>
<section class="mt10 hLh20 of">
<span class="fr jgTag bg-green">
<i class="c-fff fsize12 f-fA">{{Number(item.price) === 0 ? "免费" : "¥" + item.price}}</i>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA">9634人学习</i>
<i class="c-999 f-fA">9634评论</i>
</span>
</section>
</div>
</li>
</ul>
<div class="clear"></div>
</article>
</div>
<!-- 公共分页 开始 -->
<div>
<div class="paging">
<!-- undisable这个class是否存在,取决于数据属性hasPrevious -->
<!-- @click用来阻止a标签的跳转行为,变为执行click的方法 -->
<a :class="{undisable: !data.hasPrevious}" href="#" title="首页" @click.prevent="gotoPage(1)">首页</a>
<a :class="{undisable: !data.hasPrevious}" href="#" title="前一页" @click.prevent="gotoPage(data.current-1)"><</a>
<a v-for="page in data.pages" :key="page" :class="{current: data.current == page, undisable: data.current == page}"
:title="'第'+page+'页'" href="#" @click.prevent="gotoPage(page)">{{ page }}</a>
<a :class="{undisable: !data.hasNext}" href="#" title="后一页" @click.prevent="gotoPage(data.current+1)">></a>
<a :class="{undisable: !data.hasNext}" href="#" title="末页" @click.prevent="gotoPage(data.pages)">尾页</a>
<div class="clear"/>
</div>
</div>
<!-- 公共分页 结束 -->
</section>
</section>
<!-- /课程列表 结束 -->
</div>
</template>
<script>
import courseApi from '@/api/course'
export default {
data() {
return {
page:1,//当前页
data:{},//课程的列表
subjectNestedList: [], // 一级分类列表
subSubjectList: [], // 二级分类列表
searchObj: {}, // 查询表单对象
oneIndex:-1,
twoIndex:-1,
buyCountSort:"",
gmtCreateSort:"",
priceSort:""
}
},
created() {
this.initCourseFirst();
this.initSubject();
},
methods:{
//1.查询第一页的数据
initCourseFirst() {
courseApi.getCourseList(1, 8, this.searchObj)
.then(response => {
this.data = response.data.data;
})
},
//2.查询所有的分类,用于显示
initSubject() {
courseApi.getAllSubject()
.then(response => {
this.subjectNestedList = response.data.data.list;
})
},
//3.分页切换方法
gotoPage(page) {
courseApi.getCourseList(page, 8, this.searchObj)
.then(response => {
this.data = response.data.data;
})
},
//4.点击一级分类,查询出对应的二级分类
searchOne(subjectParentId, index) {
//初始化
this.oneIndex = index
this.twoIndex = -1
this.searchObj.subjectId = ''
this.searchObj.subjectParentId = ''
//把一级分类点击的id值赋值给searchObj
this.searchObj.subjectParentId = subjectParentId
this.gotoPage(1)
//拿着点击的一级分类的id和所有的一级分类id进行比较
//如果id相同,就从一级分类里面获取二级分类
for(let i = 0; i < this.subjectNestedList.length; ++i) {
var oneSubject = this.subjectNestedList[i];
if(subjectParentId === oneSubject.id) {
//获取二级分类
this.subSubjectList = oneSubject.children;
}
}
},
//5.点击二级分类,查询出课程
searchTwo(subjectId, index) {
//样式生效
this.twoIndex = index
//查询的条件
this.searchObj.subjectId = subjectId
this.gotoPage(1)
},
//6.按照数量排序
searchBuyCount() {
//初始化标识变量
this.buyCountSort = '1'
this.priceSort = ''
this.gmtCreateSort = ''
//赋值给查询变量
this.searchObj.buyCountSort = this.buyCountSort
this.searchObj.gmtCreateSort = this.gmtCreateSort
this.searchObj.priceSort = this.priceSort
this.gotoPage(1)
},
//7.根据发布时间排序
searchGmtCreate() {
//初始化标识变量
this.buyCountSort = ''
this.priceSort = ''
this.gmtCreateSort = '1'
//赋值给查询变量
this.searchObj.buyCountSort = this.buyCountSort
this.searchObj.gmtCreateSort = this.gmtCreateSort
this.searchObj.priceSort = this.priceSort
this.gotoPage(1)
},
//8.根据价格排序
searchPrice() {
//初始化标识变量
this.buyCountSort = ''
this.priceSort = '1'
this.gmtCreateSort = ''
//赋值给查询变量
this.searchObj.buyCountSort = this.buyCountSort
this.searchObj.gmtCreateSort = this.gmtCreateSort
this.searchObj.priceSort = this.priceSort
this.gotoPage(1)
}
}
};
</script>
<style scoped>
.active {
background: #65d681;
}
.hide {
display: none;
}
.show {
display: block;
}
</style>
_id.vue
<template>
<div id="aCoursesList" class="bg-fa of">
<!-- /课程详情 开始 -->
<section class="container">
<section class="path-wrap txtOf hLh30">
<a href="#" title class="c-999 fsize14">首页</a>
<a href="#" title class="c-999 fsize14">{{courseWebVo.subjectLevelOne}}</a>
<span class="c-333 fsize14">{{courseWebVo.subjectLevelTwo}}</span>
</section>
<div>
<article class="c-v-pic-wrap" style="height: 357px;">
<section class="p-h-video-box" id="videoPlay">
<img height="375px" :src="courseWebVo.cover" :alt="courseWebVo.title" class="dis c-v-pic">
</section>
</article>
<aside class="c-attr-wrap">
<section class="ml20 mr15">
<h2 class="hLh30 txtOf mt15">
<span class="c-fff fsize24">{{courseWebVo.title}}</span>
</h2>
<section class="c-attr-jg">
<span class="c-fff">价格:</span>
<b class="c-yellow" style="font-size:24px;">¥{{courseWebVo.price}}</b>
</section>
<section class="c-attr-mt c-attr-undis">
<span class="c-fff fsize14">主讲: {{courseWebVo.teacherName}} </span>
</section>
<section class="c-attr-mt of">
<span class="ml10 vam">
<em class="icon18 scIcon"></em>
<a class="c-fff vam" title="收藏" href="#" >收藏</a>
</span>
</section>
<section class="c-attr-mt" v-if="Number(courseWebVo.price) === 0 || isBuy">
<a href="#" title="立即观看" class="comm-btn c-btn-3">立即观看</a>
</section>
<section class="c-attr-mt" v-else>
<a @click="createOrder()" href="#" title="立即购买" class="comm-btn c-btn-3">立即购买</a>
</section>
</section>
</aside>
<aside class="thr-attr-box">
<ol class="thr-attr-ol clearfix">
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">购买数</span>
<br>
<h6 class="c-fff f-fM mt10">{{courseWebVo.buyCount}}</h6>
</aside>
</li>
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">课时数</span>
<br>
<h6 class="c-fff f-fM mt10">{{courseWebVo.lessonNum}}</h6>
</aside>
</li>
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">浏览数</span>
<br>
<h6 class="c-fff f-fM mt10">{{courseWebVo.viewCount}}</h6>
</aside>
</li>
</ol>
</aside>
<div class="clear"></div>
</div>
<!-- /课程封面介绍 -->
<div class="mt20 c-infor-box">
<article class="fl col-7">
<section class="mr30">
<div class="i-box">
<div>
<section id="c-i-tabTitle" class="c-infor-tabTitle c-tab-title">
<a name="c-i" class="current" title="课程详情">课程详情</a>
</section>
</div>
<article class="ml10 mr10 pt20">
<div>
<h6 class="c-i-content c-infor-title">
<span>课程介绍</span>
</h6>
<div class="course-txt-body-wrap">
<section class="course-txt-body">
<!-- description里面有<p>,使用v-html渲染页面内容标签 -->
<span v-html="courseWebVo.description"></span>
</section>
</div>
</div>
<!-- /课程介绍 -->
<div class="mt50">
<h6 class="c-g-content c-infor-title">
<span>课程大纲</span>
</h6>
<section class="mt20">
<div class="lh-menu-wrap">
<menu id="lh-menu" class="lh-menu mt10 mr10">
<ul>
<!-- 文件目录 -->
<li class="lh-menu-stair" v-for="chapter in chapterVideoList" :key="chapter.id">
<a href="javascript: void(0)" :title="chapter.title" class="current-1">
<em class="lh-menu-i-1 icon18 mr10"></em>{{chapter.title}}
</a>
<ol class="lh-menu-ol" style="display: block;">
<li class="lh-menu-second ml30" v-for="video in chapter.children" :key="video.id">
<a :href="'/player/' + video.videoSourceId" title target="_blank">
<span class="fr">
<i class="free-icon vam mr10">免费试听</i>
</span>
<em class="lh-menu-i-2 icon16 mr5"> </em>{{video.title}}
</a>
</li>
</ol>
</li>
</ul>
</menu>
</div>
</section>
</div>
</article>
</div>
</section>
</article>
<aside class="fl col-3">
<div class="i-box">
<div>
<section class="c-infor-tabTitle c-tab-title">
<a title href="javascript:void(0)">主讲讲师</a>
</section>
<section class="stud-act-list">
<ul style="height: auto;">
<li>
<div class="u-face">
<a href="#">
<img :src="courseWebVo.avatar" width="50" height="50" alt>
</a>
</div>
<section class="hLh30 txtOf">
<a class="c-333 fsize16 fl" href="#">{{courseWebVo.teacherName}}</a>
</section>
<section class="hLh20 txtOf">
<span class="c-999">{{courseWebVo.intro}}</span>
</section>
</li>
</ul>
</section>
<!-- 课程评论 -->
<section style="margin-top:30px">
<h6 class="c-c-content c-infor-title" id="i-art-comment">
<span class="commentTitle">课程评论</span>
</h6>
<section class="question-list lh-bj-list pr">
<ul class="pr10">
<li v-for="(comment,index) in comments.records" v-bind:key="index">
<aside class="noter-pic">
<img width="50" height="50" class="picImg" :src="comment.avatar">
</aside>
<div class="of">
<span class="fl">
<font class="fsize12 c-blue">
{{comment.nickname}}</font>
<font class="fsize12 c-999 ml5">评论:</font></span>
</div>
<div class="noter-txt mt5">
<p>{{comment.content}}</p>
</div>
<div class="of mt5">
<span class="fr"><font class="fsize12 c-999 ml5">{{comment.gmtCreate}}</font></span>
</div>
</li>
</ul>
</section>
<!-- 公共分页 开始 -->
<div class="paging">
<!-- undisable这个class是否存在,取决于数据属性hasPrevious -->
<a :class="{undisable: !comments.hasPrevious}" href="#" title="首页" @click.prevent="gotoPage(1)">首</a>
<a :class="{undisable: !comments.hasPrevious}" href="#" title="前一页" @click.prevent="gotoPage(comments.current-1)"><</a>
<a v-for="page in comments.pages" :key="page" :class="{current: comments.current == page, undisable: comments.current == page}"
:title="'第'+page+'页'" href="#" @click.prevent="gotoPage(page)">{{page}}</a>
<a :class="{undisable: !comments.hasNext}" href="#" title="后一页" @click.prevent="gotoPage(comments.current+1)">></a>
<a :class="{undisable: !comments.hasNext}" href="#" title="末页" @click.prevent="gotoPage(comments.pages)">末</a>
<div class="clear"/>
</div>
<!-- 公共分页 结束 -->
<!-- 写评论 -->
<section class="lh-bj-list pr mt20 replyhtml">
<ul>
<li class="unBr">
<aside class="noter-pic">
<img width="50" height="50" class="picImg" src="~/assets/img/avatar-boy.gif">
</aside>
<div class="of">
<section class="n-reply-wrap">
<fieldset>
<textarea name="" v-model="commentInfo.content" placeholder="输入您要评论的文字" id="commentContent"></textarea>
</fieldset>
<p class="of mt5 tar pl10 pr10">
<span class="fl "><tt class="c-red commentContentmeg" style="display: none;"></tt></span>
<input type="button" @click="addComment()" value="回复" class="lh-reply-btn">
</p>
</section>
</div>
</li>
</ul>
</section>
</section>
</div>
</div>
</aside>
<div class="clear"></div>
</div>
</section>
<!-- /课程详情 结束 -->
</div>
</template>
<script>
import courseApi from "@/api/course"
import cookies from "js-cookie"
import ordersApi from "@/api/orders"
export default {
data() {
return {
//课程的详细信息
courseWebVo : {},
//课程的章节和小节
chapterVideoList : {},
//存储分页查询的评论信息
comments : {},
//存储添加评论的信息
commentInfo : {
courseId : '',
teacherId : '',
memberId : '',
nickname : '',
avatar : '',
content : ''
},
//当前登录用户的基本信息
loginInfo:{
id:'',
age:'',
avatar:'',
mobile:'',
nickname:'',
sex:''
}
}
},
created() {
//获取当前的课程id
this.courseId = this.$route.params.id
this.isBuy = false
//获取课程的详细信息
this.getCourseInfo()
//获取所有的评论
this.getAllcomment(this.courseId, 1, 5)
//获取用户信息
this.getUserInfo()
},
methods: {
//获取课程的详细信息
getCourseInfo() {
courseApi.getCourseInfo(this.courseId)
.then(response => {
this.courseWebVo = response.data.data.courseWebVo,
this.chapterVideoList = response.data.data.chapterVideoList
this.isBuy = response.data.data.isBuy
})
},
//获取所有的评论
getAllcomment(courseId, page, limit) {
courseApi.getCourseComment(courseId, page, limit)
.then(response => {
this.comments = response.data.data
})
},
//搜索指定页面
gotoPage(page) {
courseApi.getCourseComment(this.courseId, page, 5)
.then(response => {
this.comments = response.data.data
})
},
getUserInfo() {
//获取当前用户的信息,发表评论时使用
var userStr = cookies.get('guli_ucenter')
if(userStr)
this.loginInfo = JSON.parse(userStr)
},
//添加评论
addComment() {
this.commentInfo.courseId = this.courseId
this.commentInfo.teacherId = this.courseWebVo.teacherId
this.commentInfo.memberId = this.loginInfo.id
this.commentInfo.nickname = this.loginInfo.nickname
this.commentInfo.avatar = this.loginInfo.avatar
courseApi.addComment(this.commentInfo)
.then(response => {
this.$message({
type:'success',
message:'发表评论成功'
});
this.commentInfo.content = ''
})
},
//生成订单
createOrder() {
ordersApi.createOrders(this.courseId)
.then(response => {
//生成订单之后跳转到订单的显示页面
/**
* 跳转
* 1.this.$route.push({})
* 2.window.location.href = ""
*/
this.$router.push({path:'/orders/' + response.data.data.orderNo})
})
}
}
};
</script>
其他页面不再拷贝
数据库关系如下
acl_user表中存放的是用户的登陆账号和密码等信息。
acl_role表中存放的是系统的角色,不同的角色有不同的权限。
acl_permission表存放的是系统的权限,包括每个路由的权限。
acl_user_role存放某个用户对应某些角色,一个用户可以有多个角色。
acl_role_permission存放某个角色拥有那些权限。
①我们详细分析acl_permission表中的数据
path字段指的是路由中的路径,也就是网页网址中的路径。
permission_value相当于是权限的name字段,也就是他的名字。
component是组件,他会对应前端项目目录下的一个vue组件。
②我们再看前端项目的路由信息:
可以发现里面的内容path、component和数据库中的表都是一一对应的。这里要好好做检查,保证数据库中的内容和这里的一致,不要出现单词的不一致。
③其他表的内容比较简单,不再叙述。
①登陆界面就不再截取了,我们在前端项目中找到登陆页面,在/views/login目录下的index.vue里。点击登录会调用handleLogin
函数。
②首先会先进行输入内容的校验,校验成功后调用src/store/modules目录下的user.js的Login方法。
③然后调用login方法,记住我们走到了这里,不要忘记了,思路一会儿还要回来。
④现在就到了后端了,由于后端的service_acl模块导入了spring_security模块,所以这个login的请求一定会被springsecurity拦截,我们去找一下。在com/atguigu/security/config包下查看TokenWebSecurityConfig.java
⑤我们登录时会被TokenLoginFilter过滤器拦截,它会执行如下方法
⑥执行完毕后如果认证成功,就会执行如下方法,然会数据到前端。
⑦前端收到token,会给Cookie中添加一个token,并且给state设置token
①然后Promise给handleLogin
,handleLogin会修改路由,前往首页。但是并不会立即修改,因为在src目录下的permission.js中有如下内容需要在路由改变前执行
//路由改变之前执行
router.beforeEach(async(to, from, next) => {
// start progress bar
//显示顶部加载进度条
NProgress.start()
//从Cookie中获取token
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
//查看当权用户是否已经获取了权限
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
//当前用户还没有获取权限,就去获取
const { roles } = await store.dispatch('GetInfo')
//初始化路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// dynamically add accessible routes
router.addRoutes(accessRoutes)
next({...to, replace: true })
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('FedLogOut')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
②按正常的登录逻辑,我们会走到获取权限信息这一步
const { roles } = await store.dispatch('GetInfo')
我们去store目录下找GetInfo函数,它在user.js文件中
③首先调用api总的getInfo方法去后端请求权限
④接着我们来看后端代码,同样的道理,这个请求一定会被springsecurity拦截。然后到controller调用接口
⑤这里的后端的业务逻辑比较简单,直接继续走。
①好,我们继续回到这里,返回数据之后,将相应的数据设置到state中。回到src目录下的permission.js,接着要执行这一步
②我们找到store目录下的permission.js,注意这两个permission.js文件不相同
③首先它效用了getMenu方法去获取菜单,我们继续去后端
④后端代码比较清晰,我们从这个方法开始看
首先判断是否是管理员,如果是的话直接查询所有的权限,否则条件查询。然后使用PermissionHelper构建路由,使用递归将路由分层构建。然后使用构建好的分层路由构造类似前端页面的路由信息。可以对比看一下,是一样的。
然后后端返回构建好的路由。
⑤我们回到src目录下的permission.js,得到路由之后开始执行这一段
⑥调用filterAsyncRouter
对路由做进一步的处理,然后在src下的permission中对路由进行拼接。并跳转
①退出登录的按钮在src/views/components/Sidebar目录下的Navbar.vue文件中。点击登出会调用logout这个方法
②到user.js中清空state的相关属性,然后调用logout接口
③logout接口在springsecurity方法中会被拦截
然后就是清除redis缓存
如果是复制老师的代码,在给角色添加权限的时候有bug,我们可以发现在构建权限的时候这里需要一个Pid是"0"的权限,否则得到的权限列表会是空的。
查看数据库发现pid为0的权限是代表的全部权限,因此在给用户添加权限的时候一定要加上他,否则得到的权限列表为空
修改PermissionServiceImpl.java文件中的saveRolePermissionRealtionShipGuli方法
@Override
public void saveRolePermissionRealtionShipGuli(String roleId, String[] permissionIds) {
//先全部删除再保存
QueryWrapper<RolePermission> wrapper = new QueryWrapper<>();
wrapper.eq("role_id", roleId);
rolePermissionService.remove(wrapper);
//roleId角色id
//permissionId菜单id 数组形式
//1 创建list集合,用于封装添加数据
List<RolePermission> rolePermissionList = new ArrayList<>();
//遍历所有菜单数组
for(String perId : permissionIds) {
//RolePermission对象
RolePermission rolePermission = new RolePermission();
rolePermission.setRoleId(roleId);
rolePermission.setPermissionId(perId);
//封装到list集合
rolePermissionList.add(rolePermission);
}
//添加到角色菜单关系表
RolePermission rootPermission = new RolePermission();
rootPermission.setPermissionId("1");
rootPermission.setRoleId(roleId);
rolePermissionList.add(rootPermission);
rolePermissionService.saveBatch(rolePermissionList);
}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。