1 Star 3 Fork 1

贺向东 / guli-parent-back

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
Apache-2.0

1.环境的搭建

1.1后端环境的搭建

1.1.1采用springboot的版本是2.2.1.RELEASE,微服务的结构为

1645787584549

各个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.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

1.3nginx反向代理配置

1.4nacos下载和使用

下载地址: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.后台具体功能开发

2.1相关工具类的编写

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;
    }
}

2.2讲师管理功能

2.2.1准备工作

①在讲师管理模块中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>

2.1.2编写讲师分页查询

1645845355949

①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);
    }

2.2.3编写讲师条件查询

1645845373975

①根据讲师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);
    }

2.2.4自动填充类的设置

①在许多实体类中都有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;

2.2.5增删改查讲师功能

1645845408923

1645845450140

①代码生成器已经生成最简单的增删改查

②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>

2.2.6存在问题

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

1、我们可以在watch中监听路由的变化,当路由变化时,重新调用created中的内容

2、在init方法中我们判断路由的变化,如果是修改路由,则从api获取表单数据, 如果是新增路由,则重新初始化表单数据

watch: {//监听
    $route(to, from) {//路由发生变化,就会执行
        console.log('watch $route')
        this.init()
    }
}

2.3统一异常处理

2.3.1创建统一异常处理器

①在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());
    }
}

2.3.2创建自定义异常类

①在service-base创建异常处理类GuliException

@Data
@AllArgsConstructor//生成有参数的构造方法
@NoArgsConstructor//生成无参数的构造
public class GuliException extends RuntimeException {
    private Integer code;//状态码
    private String msg;//异常信息
}

②业务需要时抛出即可

2.4对象存储OSS

2.4.1阿里云申请

①开通“对象存储OSS”服务

②创建bucket,创建RAM子用户

2.4.2创建Maven项目

①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)

2.4.3实现文件上传

①从配置文件中读取OSS的相关密匙,使用@Value注解读取。 用spring的 InitializingBeanafterPropertiesSet 来初始化配置信息,这个方法将在所有的属性被初始化 后调用。 在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;
}

⑥相关前端代码在上方已经整合

2.5课程分类功能

2.5.1课程分类的添加

1645845569622

①课程分类的添加并不是手动添加的,而是使用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>

2.5.2课程分类的树形展示

1645845586594

①课程分类有两个分级,每个一级分类下会有好多二级分类,因此我们可以创建自定义实体类,一级分类实体内有二级分类列表。在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;
    }

④前端页面已经整合

2.6课程管理相关功能

2.6.1发布新课程

发布课程分为三步:填写课程基本信息、创建课程大纲、提交审核

添加课程基本信息

1645845608035

①创建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>

创建课程大纲

1645845665227

1645845675355

1645845685559

①课程大纲主要包括添加章节,在章节中添加小节,每个小节中有视频。定义添加章节和添加小节的接口

    //添加章节
    @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>

发布课程

1645846321194

①相当于是一个确认的界面,我们要从数据库中查出一些基本的信息,让管理员确认。内容包括了封面、讲师、课程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

2.6.2分页查询所有课程

1645858348061

①编写分页查询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>

2.6.3修改课程信息

①修改课程信息时,首先我们要根据课程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);
    }

2.6.4删除课程信息

①在课程列表中点击删除按钮并确认即可删除课程信息、课程描述、课程章节、课程小节信息。后端接口:

    //删除课程信息
    @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);
    }

2.6.5修改课程大纲

①和修改课程信息相似,要先获取课程大纲然后去修改提交。查询所有章节和小节的后端接口:

    //课程大纲的列表,更具课程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'
        })
    }

2.7阿里云视频点播

2.7.1service_vod环境搭建

①在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;
}

2.7.2视频上传功能

①编写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
}

2.7.3删除单个视频的功能

编写后端接口,直接在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'
        })
    }

2.7.4删除多个视频的功能

后端接口:

    //删除多个阿里云视频的方法
    //参数是多个视频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, "删除视频失败");
        }
    }

2.7.5根据视频id获取凭证

后端接口定义:

    //根据视频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, "获取凭证失败");
        }
    }

3.前台功能开发

3.1环境搭建

3.1.1前台前端环境的搭建

①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

3.1.2前台页面

①课程页面

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()">价格&nbsp;
                                    <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">&nbsp;</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)">&lt;</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)">&gt;</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}}&nbsp;&nbsp;&nbsp;</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>&nbsp;</p>
                            <aside>
                                <span class="c-fff f-fM">购买数</span>
                                <br>
                                <h6 class="c-fff f-fM mt10">{{courseWebVo.buyCount}}</h6>
                            </aside>
                        </li>
                        <li>
                            <p>&nbsp;</p>
                            <aside>
                                <span class="c-fff f-fM">课时数</span>
                                <br>
                                <h6 class="c-fff f-fM mt10">{{courseWebVo.lessonNum}}</h6>
                            </aside>
                        </li>
                        <li>
                            <p>&nbsp;</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">&nbsp;</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)">&lt;</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)">&gt;</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>

其他页面不再拷贝

4.权限管理功能

4.1分析数据库

4.1.1分许数据库关系

数据库关系如下

1645956194425

acl_user表中存放的是用户的登陆账号和密码等信息。

acl_role表中存放的是系统的角色,不同的角色有不同的权限。

acl_permission表存放的是系统的权限,包括每个路由的权限。

acl_user_role存放某个用户对应某些角色,一个用户可以有多个角色。

acl_role_permission存放某个角色拥有那些权限。

4.1.2分析数据库内容

①我们详细分析acl_permission表中的数据

1645956462389

path字段指的是路由中的路径,也就是网页网址中的路径。

permission_value相当于是权限的name字段,也就是他的名字。

component是组件,他会对应前端项目目录下的一个vue组件。

②我们再看前端项目的路由信息:

1645956633586

可以发现里面的内容path、component和数据库中的表都是一一对应的。这里要好好做检查,保证数据库中的内容和这里的一致,不要出现单词的不一致。

③其他表的内容比较简单,不再叙述。

4.2分析登录流程

4.2.1前端登录获取token

①登陆界面就不再截取了,我们在前端项目中找到登陆页面,在/views/login目录下的index.vue里。点击登录会调用handleLogin函数。

1645957101172

②首先会先进行输入内容的校验,校验成功后调用src/store/modules目录下的user.js的Login方法。

1645957300871

③然后调用login方法,记住我们走到了这里,不要忘记了,思路一会儿还要回来。

1645957362260

④现在就到了后端了,由于后端的service_acl模块导入了spring_security模块,所以这个login的请求一定会被springsecurity拦截,我们去找一下。在com/atguigu/security/config包下查看TokenWebSecurityConfig.java

1645957505733

⑤我们登录时会被TokenLoginFilter过滤器拦截,它会执行如下方法

1645957574140

⑥执行完毕后如果认证成功,就会执行如下方法,然会数据到前端。

1645957659258

⑦前端收到token,会给Cookie中添加一个token,并且给state设置token

1645957764824

4.2.2路由改变获取权限信息

①然后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文件中

1645958175537

③首先调用api总的getInfo方法去后端请求权限

1645958206567

④接着我们来看后端代码,同样的道理,这个请求一定会被springsecurity拦截。然后到controller调用接口

1645958443535

⑤这里的后端的业务逻辑比较简单,直接继续走。

1645958569941

4.2.3构建路由

①好,我们继续回到这里,返回数据之后,将相应的数据设置到state中。回到src目录下的permission.js,接着要执行这一步

1645958674564

②我们找到store目录下的permission.js,注意这两个permission.js文件不相同

1645958729822

③首先它效用了getMenu方法去获取菜单,我们继续去后端

1645958766165

④后端代码比较清晰,我们从这个方法开始看

1645958837780

首先判断是否是管理员,如果是的话直接查询所有的权限,否则条件查询。然后使用PermissionHelper构建路由,使用递归将路由分层构建。然后使用构建好的分层路由构造类似前端页面的路由信息。可以对比看一下,是一样的。

16459593282711645959354113

然后后端返回构建好的路由。

⑤我们回到src目录下的permission.js,得到路由之后开始执行这一段

1645959450283

⑥调用filterAsyncRouter对路由做进一步的处理,然后在src下的permission中对路由进行拼接。并跳转

1645959609810

4.3分析退出登录流程

①退出登录的按钮在src/views/components/Sidebar目录下的Navbar.vue文件中。点击登出会调用logout这个方法1645960310157

②到user.js中清空state的相关属性,然后调用logout接口

1645960383147

③logout接口在springsecurity方法中会被拦截

1645960436432

然后就是清除redis缓存

1645960499708

4.4一些问题

如果是复制老师的代码,在给角色添加权限的时候有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);
    }
Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

简介

谷粒学苑项目后端代码,已完结,有权限管理模块思路梳理以及bug修改的详细介绍,见readme.md 展开 收起
Java 等 2 种语言
Apache-2.0
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Java
1
https://gitee.com/XiangdongHe1/guli-parent.git
git@gitee.com:XiangdongHe1/guli-parent.git
XiangdongHe1
guli-parent
guli-parent-back
master

搜索帮助