Day 1

  1. 项目背景
  2. 项目采用的商业模式
  3. 项目实现的功能模块
  4. 项目使用的技术
  5. 学习技术 MyBatis-Plus

商业模式

B2C

两个角色:管理员普通用户

管理员:添加 修改 删除

普通用户:查询

本项目采用此模式。

B2B2C

举例电商平台

京东:商家:消费者

项目功能模块

b2c模式

系统后台:(管理员使用)

  1. 讲师管理模块

  2. 课程分类管理模块

  3. 课程管理模块

    视频

  4. 统计分析模块

  5. 订单管理

  6. banner管理

  7. 权限管理

系统前台:(普通用户使用)

  1. 首页数据显示

  2. 讲师列表和详情

  3. 课程列表和课程详情

    视频在线播放

  4. 注册和登录功能

  5. 微信扫描登录

  6. 微信扫描支付

项目技术点

采用前后端分离开发

后端技术

  • springboot
  • springcloud
  • mybatis-plus
  • spring security
  • redis
  • maven
  • easyExcel
  • jwt
  • OAuth2

前端技术

  • vue
  • element-ui
  • axios
  • nodejs

其他技术

  • 阿里云oss
  • 阿里云视频点播服务
  • 阿里云短信服务
  • 微信支付和登录
  • docker
  • jenkins
  • git

mp代码流程

mp入门:对mybatis增强,简化开发

  1. 创建数据库,创建表,添加数据

  2. 创建springboot工程

    image-20210422123452040

  3. 引入相关依赖

    springboot和mp依赖

    <?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>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.4.4</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.atguigu</groupId>
        <artifactId>mpdemo1010</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>mpdemo1010</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
    
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.0.5</version>
            </dependency>
            <!--mysql-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <!--lombok用来简化实体类-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
    
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>2.3.5.RELEASE</version>
                </plugin>
            </plugins>
        </build>
    
    </project>
  4. 安装lombok插件(IDEA自带)

  5. application.properties

    #mysql数据库连接,serverTimezone=GMT%2B8指北京时间东八区
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8
    spring.datasource.username=root
    spring.datasource.password=root
    
    #mybatis日志
    mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    
    #环境设置:dev、test、prod
    spring.profiles.active=dev
  6. 编写代码

mp实现添加操作

@Test
void addUser(){
    User user = new User();
    user.setName("DDDDWWWW112");
    user.setAge(123);
    user.setEmail("666@123.com");

    int insert = userMapper.insert(user);
    System.out.println("insert:" + insert);
}
  • 不需要设置id值(主键)
  • mp自动生成id值(19位)

主键生成策略

  • 自动增长 AUTO INCREMENT

    image-20210422160028398

  • UUID 每次生成唯一的随机值

    排序不方便

  • redis实现

  • mp自带策略_snowflake雪花算法

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

    image-20210422160348627

    AUTO:自动增长

    ID_WORKER / ID_WORKER_STR:mp自带策略,生成19位值,分别对应数字型 / 字符型

    INPUT:设置id值

    NONE:输入

    UUID:随机唯一值

mp实现修改操作

@Test
void updateUser(){
    User user = new User();

    user.setId(2L);
    user.setAge(1000);

    int i = userMapper.updateById(user);
    System.out.println("update:" + i);
}

mp实现自动填充

  1. 数据库表中添加两个字段

    image-20210422161155834

  2. Java类中添加实体类属性

不需要set值到对象,使用mp的方式实现添加

  1. 在实体类里对自动填充属性添加注解

    //注意数据表create_time,update_time与下面命名特点
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
  2. 创建类,实现接口 MetaObjectHandler,并实现接口里的方法

    package com.atguigu.mpdemo1010.handler;
    
    import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
    import org.apache.ibatis.reflection.MetaObject;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {
        @Override
        public void insertFill(MetaObject metaObject) {
            this.setFieldValByName("createTime",new Date(),metaObject);
            this.setFieldValByName("updateTime",new Date(),metaObject);
    
            this.setFieldValByName("version",1,metaObject);
    
            this.setFieldValByName("deleted",0,metaObject);
        }
    
        @Override
        public void updateFill(MetaObject metaObject) {
            this.setFieldValByName("updateTime",new Date(),metaObject);
        }
    }

乐观锁

  1. 数据库表中添加字段versioin,作为乐观锁版本号

  2. Java对应实体类添加版本号属性version,并添加注解@Version

    @Version //版本号注解
    @TableField(fill = FieldFill.INSERT)
    private Integer version;
  3. 在Config配置类中,配置乐观锁插件

    //乐观锁插件
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInterceptor();
    }
  4. 测试

    /**
     * 测试 乐观锁插件
     */
    @Test
    public void testOptimisticLocker() {
        //查询
        User user = userMapper.selectById(1373611971844370433L);
        //修改数据
        user.setName("Helen Yao");
        user.setEmail("helen@qq.com");
        //执行更新
        userMapper.updateById(user);
    }

mp实现简单查询

  1. 根据id查询

    User user = userMapper.selectById(1373611971844370433L);
  2. 多个id批量查询

    //多id批量查询
    @Test
    void testSelectDemo(){
        List<User> users = userMapper.selectBatchIds(Arrays.asList(1L,2L,3L));
        System.out.println(users);
    }

mp实现分页查询

  1. 在Config配置类中,配置分页插件

    //分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
  2. 编写分页代码

    //BaseMapper接口的selectPage方法
    IPage<T> selectPage(IPage<T> var1, @Param("ew") Wrapper<T> var2);
    //创建Page对象,传入参数
    //参数1:当前页,参数2:每页显示的记录数
    Page<User> page = new Page<>(1,3);
    //调用mp的分页查询方法selectPage,把所有数据封装到page对象中
    userMapper.selectPage(page,null);
    //通过page对象获取分页数据
    System.out.println(page.getCurrent());

mp实现逻辑删除

  1. 物理删除

    //BaseMapper接口的deleteById方法
    int deleteById(Serializable var1);
  2. 批量删除

    //BaseMapper接口的deleteBatchIds方法
    int deleteBatchIds(@Param("coll") Collection<? extends Serializable> var1);
  3. 逻辑删除

    • 数据库表中添加逻辑删除字段deleted

      image-20210422164854582

    • Java对应实体类中添加属性和注解

      @TableLogic //逻辑删除注解
      @TableField(fill = FieldFill.INSERT)
      private Integer deleted;
    • 配置逻辑删除插件

      //逻辑删除插件
      @Bean
      public ISqlInjector sqlInjector(){
          return new LogicSqlInjector();
      }
    • 执行代码,deleted值最终变为1

mp实现复杂条件查询

使用QueryWrapper构建条件

创建QueryWrapper对象,调用其方法实现各种条件查询

  • ge >=
  • gt >
  • le <=
  • lt <
  • eq =
  • ne !=
  • like 模糊查询
  • orderByDesc / orderByAsc 排序
  • last 语句最后拼接sql语句
  • between 范围
  • select 指定列

Day 2

  1. 前后端分离开发概念

  2. 讲师管理模块(后端)

    讲师CRUD操作

前后端分离开发

未命名文件

项目准备工作

  1. 创建数据库,创建讲师数据表 edu_teacher

    CREATE TABLE `edu_teacher` (
      `id` char(19) NOT NULL COMMENT '讲师ID',
      `name` varchar(20) NOT NULL COMMENT '讲师姓名',
      `intro` varchar(500) NOT NULL DEFAULT '' COMMENT '讲师简介',
      `career` varchar(500) DEFAULT NULL COMMENT '讲师资历,一句话说明讲师',
      `level` int(10) unsigned NOT NULL COMMENT '头衔 1高级讲师 2首席讲师',
      `avatar` varchar(255) DEFAULT NULL COMMENT '讲师头像',
      `sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序',
      `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
      `gmt_create` datetime NOT NULL COMMENT '创建时间',
      `gmt_modified` datetime NOT NULL COMMENT '更新时间',
      PRIMARY KEY (`id`),
      UNIQUE KEY `uk_name` (`name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='讲师';
  2. 创建项目结构

    项目结构

代码生成器

  1. 创建application.properties配置文件

  2. 编写controller service mapper代码内容

    mp提供代码生成器,生成相关代码

//CodeGenerator.java
//根据实际需要自行更改配置

package com.example.demo;

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

/**
 * @author
 * @since 2018/12/13
 */
public class CodeGenerator {

    @Test
    public void run() {

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

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

        mpg.setGlobalConfig(gc);

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

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

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

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

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

        mpg.setStrategy(strategy);


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

讲师列表接口

  1. 创建controller

    @Api(description="讲师管理")
    @RestController
    @RequestMapping("/eduservice/teacher")
    @CrossOrigin
    public class EduTeacherController {
        //访问地址:http://localhost:8001/eduservice/teacher/findAll
        //把service注入
        @Autowired
        private EduTeacherService teacherService;
    
        //1 查询讲师表中所有数据
        //rest风格
        @ApiOperation(value = "所有讲师列表")
        @GetMapping("findAll")
        public R findAllTeacher(){
            //调用service方法实现查询操作
            List<EduTeacher> list = teacherService.list(null);
            return R.ok().data("items",list);
        }
    }
  2. 创建启动类

    @SpringBootApplication
    @ComponentScan(basePackages = {"com.atguigu"})
    public class EduApplication {
        public static void main(String[] args) {
            SpringApplication.run(EduApplication.class,args);
        }
    }
  3. 创建配置类,配置mapper扫描等等

    @Configuration
    @MapperScan("com.atguigu.eduservice.mapper")
    public class EduConfig {
    	...
    }
  4. 测试,使用端口8001:

    http://localhost:8001/eduservice/teacher/findAll

讲师逻辑删除接口

  1. 在配置类EduConfig中,配置逻辑删除插件

    //逻辑删除插件
    @Bean
    public ISqlInjector sqlInjector() {
        return new LogicSqlInjector();
    }
  2. 在实体类EduTeacher中,对应逻辑删除属性上添加注解@TableLogic

    @ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除")
    @TableLogic
    private Boolean isDeleted;
  3. 编写controller内的方法

    //2 逻辑删除讲师
    //@PathVariable:得到浏览器路径中的值id(必要)
    @ApiOperation(value = "根据ID删除讲师")
    @DeleteMapping("{id}")
    public R removeTeacher(
            @ApiParam(name = "id", value = "讲师ID", required = true)
            @PathVariable String id){
        boolean flag = teacherService.removeById(id);
       
        if (flag){
            return R.ok();
        }else {
            return R.error();
        }
    }

    @DeleteMapping("{id}"):id值需要通过路径传递

    @PathVariable String id:获取路径中的id值

    例:localhost:8001/edu/delete/114514

  4. 如何测试?

    delete提交不同于get\post提交

    借助一些工具:

    swagger测试(重点)

    postman(了解)

swagger整合

优点:

  1. 生成在线接口文档
  2. 方便接口测试
  1. 创建公共模块,整合swagger,以便所有模块都能使用

    父工程guli_parent -> 子模块common -> 子模块service_base,创建配置类SwaggerConfig

    @Configuration //配置类
    @EnableSwagger2 //swagger注解
    public class SwaggerConfig {
    
        @Bean
        public Docket webApiConfig(){
            return new Docket(DocumentationType.SWAGGER_2)
                    .groupName("webApi")
                    .apiInfo(webApiInfo())
                    .select()
                    .paths(Predicates.not(PathSelectors.regex("/admin/.*")))
                    .paths(Predicates.not(PathSelectors.regex("/error.*")))
                    .build();
        }
    
        private ApiInfo webApiInfo(){
            return new ApiInfoBuilder()
                    .title("网站-课程中心API文档")
                    .description("本文档描述了课程中心微服务接口定义")
                    .version("1.0")
                    .contact(new Contact("Helen", "http://atguigu.com",
                            "55317332@qq.com"))
                    .build();
        }
    }
  2. 在service_edu中,引入service_base依赖

    <dependency>
        <groupId>com.atguigu</groupId>
        <artifactId>service_base</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
  3. 在service_edu启动类添加注解,设置包扫描规则

    @SpringBootApplication
    @ComponentScan(basePackages = {"com.atguigu"})
    public class EduApplication {
        public static void main(String[] args) {
            SpringApplication.run(EduApplication.class,args);
        }
    }
  4. 访问swagger

    固定页面:http://localhost:xxxx/swagger-ui.html

统一结果返回

json数据格式有2种:

对象 数组

这两种格式一般混合使用

{
    "success": boolean, //响应是否成功
    "code": num, //响应码
    "message": string, //返回消息
    "data": HashMap //返回数据,存放在键值对中
}
  1. 在common模块,创建子模块 common_utils

  2. 创建interface,定义数据返回状态码

    public interface ResultCode {
        public static Integer SUCCESS = 20000;//成功
        public static Integer ERROR = 20001;//失败
    }
  3. 定义数据返回格式

    //统一返回结果类
    @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>();
    }
  4. 使用统一结果

    • 在service中,引入 common_utils

      <dependency>
          <groupId>com.atguigu</groupId>
          <artifactId>common_utils</artifactId>
          <version>0.0.1-SNAPSHOT</version>
      </dependency>
    • 设置接口返回结果都是R

      public R findAllTeacher(){
          //调用service方法实现查询操作
          List<EduTeacher> list = teacherService.list(null);
          return R.ok().data("items",list);
      }

讲师分页查询和条件查询

讲师分页功能

  1. 在配置类EduConfig中,配置mp分页插件

    //分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
  2. 编写讲师分页查询接口方法

        //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();//每页数据List集合
    
    //        Map map = new HashMap<>();
    //        map.put("total",total);
    //        map.put("rows",records);
    //        return R.ok().data(map); //利用map存放返回数据,二选一均可
    
            return R.ok().data("total",total).data("rows",records);
        }

多条件组合查询带分页

实现效果:

未命名文件 (1)

  1. 把条件值传递到接口里面

    条件值封装到对象,对象传递到接口

  2. 根据条件值进行判断,拼接条件

    //4 条件查询带分页的方法
    @PostMapping("pageTeacherCondition/{current}/{limit}")
    public R pageTeacherCondition(@PathVariable long current,
                                  @PathVariable long limit,
                                  @RequestBody(required = false) TeacherQuery teacherQuery){
    	...
    }
    • RequestBody:

      需要使用post提交方式

      使用json传递数据,把json数据封装到对应对象里

      @RequestBody(required = false),即参数值可以为空

    • ResponseBody:

      返回json数据

讲师添加功能

  1. 自动填充配置

    @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);
        }
    }
    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreate;
       
    @ApiModelProperty(value = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;
  2. 编写controller

    //5 添加讲师接口方法
    @PostMapping("addTeacher")
    public R addTeacher(@RequestBody EduTeacher eduTeacher){
        boolean save = teacherService.save(eduTeacher);
        if (save){
            return R.ok();
        }else {
            return R.error();
        }
    }

讲师修改功能

  1. 根据讲师id进行查询

    //6 根据讲师id进行查询
    @GetMapping("getTeacher/{id}")
    public R getTeacher(@PathVariable String id){
        EduTeacher eduTeacher = teacherService.getById(id);
        return R.ok().data("teacher",eduTeacher);
    }
  2. 讲师修改

    //7 讲师修改
    @PostMapping("updateTeacher")
    public R updateTeacher(@RequestBody EduTeacher eduTeacher){
        boolean flag = teacherService.updateById(eduTeacher);
        if (flag){
            return R.ok();
        }else {
            return R.error();
        }
    }

Day 3

  1. 统一异常处理
    • 全局异常处理
    • 特定异常处理
    • 自定义异常处理
  2. 统一日志处理
    • Logback
  3. 前端知识
    • es6语法
    • vue指令

统一异常处理

  1. 全局异常处理

    @ControllerAdvice //全局异常处理注解
    @Slf4j
    public class GlobalExceptionHandler {
    
        @ResponseBody
        @ExceptionHandler(Exception.class)
        public R error(Exception e){
            e.printStackTrace();
            return R.error().message("执行了全局异常处理");
        }
    }
  2. 特定异常处理

    //特定异常
    @ResponseBody
    @ExceptionHandler(ArithmeticException.class)
    public R error(ArithmeticException e){
        e.printStackTrace();
        return R.error().message("执行了ArithmeticException异常处理");
    }
  3. 自定义异常处理

    • 创建自定义异常类,继承RuntimeException

      编写异常属性

      @Data
      @AllArgsConstructor //生成有参构造器
      @NoArgsConstructor //生成无参构造器
      public class GuliException extends RuntimeException{
          private Integer code;//状态码
          private String msg;//异常信息
      }
    • 在统一异常类添加规则

      //自定义异常
      @ResponseBody
      @ExceptionHandler(GuliException.class)
      public R error(GuliException e){
          log.error(e.getMessage());
          e.printStackTrace();
          return R.error().code(e.getCode()).message(e.getMsg());
      }
    • 执行自定义异常

      try{
          int a = 10 / 0;
      }catch(Exception e){
          throw new GuliException(20001,"执行了自定义异常");
      }

统一日志处理

  1. 日志级别

    ERROR WARN INFO DEBUG

    #设置日志级别
    logging.level.root=WARN
  2. 把日志输出到控制台和文件中,使用日志工具

    log4j

    logback日志工具

    • 删除application.properties中的日志配置

      #mybatis日志
      #mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
      
      #设置日志级别
      #logging.level.root=DEBUG
    • 在resources目录,创建logback-spring.xml

  3. 如果程序运行出现异常,则把异常信息输出到文件中

    log.error(e.getMessage());

es6介绍

es6,即ECMAScript 6.0

  1. es6与es5
    • es6代码简洁,es5代码复杂
    • es6浏览器兼容性差,es5浏览器兼容性好
  2. es6是一套标准规范,JavaScript遵守了这套规范

vue入门

vue是一个构建页面前端的框架

  1. 使用vscode快捷生成html页面

  2. 引入vue的js文件,类似于JQuery

    <script src="vue.min.js"></script>
  3. 在html页面创建div标签,并添加id属性

    <div id="app"></div>
  4. 编写vue代码,是一套固定的结构

    <script>
    	//创建一个vue对象
        new Vue({
            el: '#app', //绑定vue的作用范围
            data: { //定义页面中显示的模型数据
                message: 'Hello Vue!'
            }
        })
    </script>
  5. 使用插值表达式{{xxxx}},获取data里面定义的值

    <div id="app">
        {{message}}
    </div>

Day 4

  1. axios,在vue中发送ajax请求(重点)
  2. element-ui
  3. nodejs
  4. npm(重点)
  5. babel
  6. 模块化(重点)
  7. webpack
  8. 搭建项目前端环境(重点)

axios

  1. axios是独立的项目,不是vue里的一部分,只是axios经常与vue一起使用,实现ajax操作

  2. 使用axios应用场景

    image-20210428083606230

  3. axios使用

    • 创建html页面,引入js文件,包含两个js文件:vue和axios

      <script src="vue.min.js"></script>
      <script src="axios.min.js"></script>
    • 编写axios代码

      • 创建json文件,创建数据

        {
            "success":true,
            "code":20000,
            "message":成功,
            "data":{
                "items":[
                    {"name":"lucy","age":20},
                    {"name":"mary","age":30},
                    {"name":"jack","age":40}
                ]
            }
        }
      • 使用axios发送ajax请求,请求文件得到数据,在页面显示

        image-20210428084107339

nodejs

  1. nodejs是什么

    • 类比学过的 java,运行 java 需要 jdk 环境

      正在学的 nodejs,是 JavaScript 的运行环境,用于执行 JavaScript 代码

    • 不需要浏览器,直接使用 nodejs 运行 JavaScript 代码

    • 模拟服务器效果,比如tomcat

  2. 安装nodejs

  3. 使用nodejs执行 JavaScript 代码

    node xxx.js

  4. 在vscode中打开终端窗口(cmd窗口),执行js代码

    image-20210428084817993

npm

  1. npm是什么

    包管理工具

    • 在后端开发中使用过maven,maven构建项目,管理jar依赖,并联网下载依赖
    • npm类似于maven,用于前端,管理前端js依赖,并联网下载依赖
  2. 安装npm

    • 在安装nodejs时,npm也会一并安装
    • 使用npm -v,查看是否安装成功
  3. 演示npm具体操作

    • npm项目初始化操作

      使用命令npm init -y

      项目初始化之后,生成文件 package.json ,类似于后端pom.xml文件

    • npm下载指定js依赖,使用命令npm install xxxx(依赖名称)

      根据package.json下载依赖,使用命令npm install

babel

  1. babel是什么

    babel是转码器,把es6代码转换成es5代码

    因为写的代码是es6代码,es6代码的浏览器兼容性很差,改为使用es5浏览器兼容性好

  2. babel的使用

    • 安装babel工具

      使用命令npm install --global babel-cli

    • 创建js文件,编写es6代码

    • 创建babel配置文件.babelrc

      {
          "presets":["es2015"],
          "plugins":[]
      }
    • 安装es2015转码器

      npm install --save-dev babel-preset-es2015

    • 使用命令进行转码(以下es6 es5均是文件夹名称)

      根据文件转码:babel es6/xxx.js -o es5/xxx.js

      根据文件夹转码:babel es6 -d es5

模块化

  1. 是什么

    • 开发后端接口的时候,开发controller service mapper,controller注入service,service注入mapper
    • 在后端中,类与类之间的调用成为后端的模块化操作
    • 在前端中,js与js之间的调用成为前端的模块化操作
  2. es5实现模块化操作

    在01.js定义js方法

    //1 创建js方法
    const sum = function(a,b){
        return parseInt(a) + parseInt(b)
    }
    const subtract = function(a,b){
        return parseInt(a) - parseInt(b)
    }
    
    //2 设置哪些方法可以被其他js调用
    module.exports = {
        sum,
        subtract
    }

    在02.js调用01.js里的方法

    //调用01.js里面的方法
    //1 引入01.js文件
    const m = require("./01.js")
    //2 调用
    console.log(m,sum(1,2))
    consule.log(m,subtract(10,3))

    测试 node 02.js

  3. es6模块化代码写法一

    注意:如果使用es6写法实现模块化操作,在nodejs环境中不能直接运行,需要使用babel把es6代码转换为es5代码,才可以在nodejs运行

    01.js定义方法

    //定义方法,设置哪些方法可以被其他js调用
    export function getList(){
        console.log("getList...")
    }
    export function save(){
        console.log("save...")
    }

    02.js调用01.js的方法

    //调用01.js的方法,引入01.js文件,进行调用
    import {getList,save} from './01.js'
    //调用方法
    getList()
    save()
  4. es6模块化代码写法二

    01.js

    //定义方法,设置哪些方法可以被其他js调用
    export default {
        getList(){
            console.log('getList...')
        },
        update(){
            console.log('update...')
        }
    }

    02.js

    //调用01.js的方法,引入01.js文件,进行调用
    import m from './01.js'
    //调用方法
    m.getList()
    m.update()

webpack

  1. 是什么

    打包工具,可以把多个静态资源文件打包成一个文件

    image-20210428093816780

  2. webpack使用

    • 安装webpack工具

      npm install -g webpack webpack-cl

      查看是否安装成功

      webpack -v

    • 创建js文件用于打包操作

      创建三个js文件:

      在common.js和utils.js定义方法

      exports.info = function(str){
          document.write(str);
      }
      exports.add = function(a,b){
          return a + b;
      }

      在main.js引入common和utils

      const common = require('./common');
      const utils = require('./utils');
      
      common.info('Hello world!' + utils.add(100, 200));
    • 创建webpack配置文件,配置打包信息

      webpack.config.js

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

      webpack #有警告

      webpack --mode=development #没有警告

    • 最终测试

      创建html文件,引入打包之后的js文件,使用浏览器查看效果

      <script src="dist/bundle.js"></script>

打包css

  1. 创建css文件,写样式内容

    body {
        background-color: red;
    }
  2. 在main.js中,引入css文件

    //css文件引入
    require('./style.css')
  3. 安装css加载工具

    npm install --save-dev style-loader css-loader
  4. 修改webpack配置文件

    const path = require("path"); //Node.js内置模块
    module.exports = {
        //...
        output:{},
        module: {
            rules: [ 
                { 
                    test: /\.css$/, //打包规则应用到以css结尾的文件上
                    use: ['style-loader', 'css-loader']
                }
            ]
        }
    }

搭建前端页面环境

image-20210508083321495

选取一个模板(框架)进行环境搭建

vue-admin-template

  1. 找到模板源文件,并解压至工作区

  2. 通过vscode终端,下载依赖

    //通过配置文件下载依赖
    npm install
  3. 启动模板项目

    npm run dev

前端页面环境说明

  1. 前端框架入口:

    index.html

    main.js

  2. 前端页面环境使用的框架,主要基于两种技术:

    vue-admin-template = vue + element-ui

  3. build目录

    存放项目构建的脚本文件

  4. config目录

    index.js

    修改 useEslint: false true

    dev.env.js

    修改 BASE_API: '"http://localhost:8001"',即后端接口地址

  5. src目录

    image-20210508084239815

Day 5

讲师管理前端开发

  1. 讲师列表(分页条件查询)
  2. 讲师添加
  3. 讲师删除功能
  4. 讲师修改功能

先把后端管理系统登录改造到本地

后面登录功能添加权限框架 spring security

改造登录到本地接口

  1. 把登录请求地址改造到本地,修改配置文件

    config目录的dev.env.js

    BASE_API: '"http://localhost:9001"'
  2. 登录操作调用两个方法:login登录操作,info登录后获取用户信息

    创建接口两个方法实现登录

    • login 返回token值
    • info 返回roles name avatar
  3. 开发接口

    @RestController
    @RequestMapping("/eduservice/user")
    @CrossOrigin //解决跨域问题
    public class EduLoginController {
        //login
        @PostMapping("login")
        public R login(){
    
    
            return R.ok().data("token","admin");
        }
        //info
        @GetMapping("info")
        public R info(){
    
    
            return R.ok().data("roles","[admin]").data("name","admin").data("avatar","https://cdn.jsdelivr.net/gh/Rayucan/imageCloud/data/20210202224329.png");
        }
    }
  4. 修改api目录login.js,修改本地接口路径

    import request from '@/utils/request'
    
    export function login(username, password) {
      return request({
        url: '/eduservice/user/login',
        method: 'post',
        data: {
          username,
          password
        }
      })
    }
    
    export function getInfo(token) {
      return request({
        url: '/eduservice/user/info',
        method: 'get',
        params: { token }
      })
    }
  5. 最终测试出现问题Access-Control-Allow-Origin,即跨域问题

    • 跨域问题

      通过一个地址去访问另外一个地址,此过程中如果三个地方任何一个不同,便会出现

      • 访问协议 http https
      • ip地址 192.168.1.1 172.11.11.11
      • 端口号 9528 8001
    • 解决跨域问题

      • 在后端controller接口添加注解(常用)

        @CrossOrigin

      • 使用网关解决(后面用到)

前端框架开发过程介绍

  1. 添加路由

    src/router/index.js 进行配置

    {
      path: '/',
      component: Layout,
      redirect: '/dashboard',
      name: 'Dashboard',
      hidden: true,
      children: [{
        path: 'dashboard',
        //路由对应页面
        component: () => import('@/views/dashboard/index')
      }]
    },
  2. 实现效果:点击某个路由,显示路由对应页面

    在views目录创建vue页面

  3. api目录创建js文件,定义接口地址和参数

    import request from '@/utils/request'
    
    export function getList(params) {
      return request({
        url: '/table/list',
        method: 'get',
        params
      })
    }
  4. 在vue页面引入js文件,调用方法实现功能

    • 引入import user from '...'

    • 使用element-ui显示数据内容

      data:{
      
      },
      created() {
      
      },
      methods:{
      
      }

讲师列表前端开发

  1. 添加路由

    {
      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
        }
      ]
    },
  2. 创建对应页面

    image-20210508093449436

  3. 在api目录创建teacher.js定义访问的接口地址

    import request from '@/utils/request'
    export default {
        //1 讲师列表(条件查询分页)
        //current当前页 limit每页记录数 teacherQuery条件对象
        getTeacherListPage(current,limit,teacherQuery) {
            return request({
                //url: '/eduservice/teacher/pageTeacherCondition/'+current+"/"+limit,
                url: `/eduservice/teacher/pageTeacherCondition/${current}/${limit}`,
                method: 'post',
                //teacherQuery条件对象,后端使用RequestBody获取数据
                //data表示把对象转换json进行传递到接口里面
                data: teacherQuery
              })
        },
        //删除讲师
        deleteTeacherId(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
              })
        }
    }
  4. 在讲师列表页面list.vue,调用定义的接口方法,得到接口返回数据

    data(){
    	return{
    
    	}
    },
    created(){
    
    },
    methods:{
    
    }
  5. 把请求接口获取的数据,在页面进行显示

    查阅element-ui官方文档,copy表格代码

    <template>
      <el-table
        :data="tableData"
        style="width: 100%">
        <el-table-column
          prop="date"
          label="日期"
          width="180">
        </el-table-column>
        <el-table-column
          prop="name"
          label="姓名"
          width="180">
        </el-table-column>
        <el-table-column
          prop="address"
          label="地址">
        </el-table-column>
      </el-table>
    </template>
       
    <script>
      export default {
        data() {
          return {
            tableData: [{
              date: '2016-05-02',
              name: '王小虎',
              address: '上海市普陀区金沙江路 1518 弄'
            }, {
              date: '2016-05-04',
              name: '王小虎',
              address: '上海市普陀区金沙江路 1517 弄'
            }, {
              date: '2016-05-01',
              name: '王小虎',
              address: '上海市普陀区金沙江路 1519 弄'
            }, {
              date: '2016-05-03',
              name: '王小虎',
              address: '上海市普陀区金沙江路 1516 弄'
            }]
          }
        }
      }
    </script>
  6. 讲师列表添加分页实现

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

    分页方法修改,添加页码参数

       methods:{
           //讲师列表的方法
           getList(page=1) {
               this.page = page
    	}
    }
  7. 添加条件查询

    使用element-ui组件实现

    在列表上面添加条件输入表单,使用v-model数据绑定

    <el-form-item>
      <el-input v-model="teacherQuery.name" placeholder="讲师名"/>
    </el-form-item>
    <el-button type="primary" icon="el-icon-search" @click="getList()">查询</el-button>

    清空功能

    • 先清空表单输入的条件
    • 再查询所有数据
    resetData() {//清空的方法
        //表单输入项数据清空
        this.teacherQuery = {}
        //查询所有讲师数据
        this.getList()
    }

讲师删除功能前端实现

  1. 在每条记录后面添加删除按钮,按钮绑定事件,并在绑定事件的方法传递删除讲师的id值

    <el-button type="danger" size="mini" icon="el-icon-delete" @click="removeDataById(scope.row.id)">删除</el-button>
  2. 在api目录teacher.js定义删除接口的地址

    //删除讲师
    deleteTeacherId(id) {
        return request({
            url: `/eduservice/teacher/${id}`,
            method: 'delete'
          })
    },
  3. vue页面调用,实现删除

    //删除讲师的方法
    removeDataById(id) {
        this.$confirm('此操作将永久删除讲师记录, 是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
        }).then(() => {  //点击确定,执行then方法
            //调用删除的方法
            teacher.deleteTeacherId(id)
                .then(response =>{//删除成功
                //提示信息
                this.$message({
                    type: 'success',
                    message: '删除成功!'
                });
                //回到列表页面
                this.getList()
            })
        }) //点击取消,执行catch方法
    }

讲师添加功能前端实现

点击”添加讲师”按钮,进入表单页面,输入讲师信息

在表单页面,点击”保存”,提交接口,添加至数据库

  1. api定义接口地址

    //添加讲师
    addTeacher(teacher) {
        return request({
            url: `/eduservice/teacher/addTeacher`,
            method: 'post',
            data: teacher
          })
    }
  2. 在vue页面实现调用

    //添加讲师的方法
    saveTeacher() {
      teacherApi.addTeacher(this.teacher)
        .then(response => {//添加成功
          //提示信息
          this.$message({
              type: 'success',
              message: '添加成功!'
          });
          //回到列表页面 路由跳转
          this.$router.push({path:'/teacher/table'})
        })
    }

讲师修改功能

  1. 每条记录后面添加”修改”按钮

  2. 点击”修改”按钮,进入表单页面,进行数据回显

    根据讲师id查询所有讲师,实现数据回显

  3. 通过路由跳转,进入数据回显页面,在路由index页面添加路由

    {
      path: 'edit/:id',   
      name: 'EduTeacherEdit',
      component: () => import('@/views/edu/teacher/save'),
      meta: { title: '编辑讲师', noCache: true },
      hidden: true
    }
    <router-link :to="'/teacher/edit/'+scope.row.id">
      <el-button type="primary" size="mini" icon="el-icon-edit">修改</el-button>
    </router-link>
       
  4. 在表单页面实现数据回显

    • teacher.js中定义根据id查询接口

      //根据id查询讲师
      getTeacherInfo(id) {
          return request({
              url: `/eduservice/teacher/getTeacher/${id}`,
              method: 'get'
            })
      }
    • vue页面中调用接口,实现数据回显

      //根据讲师id查询的方法
      getInfo(id) {
        teacherApi.getTeacherInfo(id)
          .then(response => {
            this.teacher = response.data.teacher
          })
      }
    • 调用根据id查询方法

      由于”添加”和”修改”均使用save.vue页面,如何区别添加还是修改?

      只有修改的时候才进行数据回显。

      即:判断路径里是否有讲师id,如果有id为修改,否则为添加

      created() { //页面渲染之前执行
      	this.init()
      }
      methods:{
          init() {
            //判断路径有id值,做修改
            if(this.$route.params && this.$route.params.id) {
                //从路径获取id值
                const id = this.$route.params.id
                //调用根据id查询的方法
                this.getInfo(id)
            } else { //路径没有id值,做添加
              //清空表单
              this.teacher = {}
            }
          }
      }

最终修改实现

  1. 在api的teacher.js定义修改接口

    //修改讲师
    updateTeacherInfo(teacher) {
        return request({
            url: `/eduservice/teacher/updateTeacher`,
            method: 'post',
            data: teacher
          })
    }
  2. 在vue页面调用修改方法

    saveOrUpdate() {
      //判断修改还是添加
      //根据teacher是否有id
      if(!this.teacher.id) {
        //添加
        this.saveTeacher()
      } else {
        //修改
        this.updateTeacher()
      }
    }
    //修改讲师的方法
    updateTeacher() {
      teacherApi.updateTeacherInfo(this.teacher)
        .then(response => {
          //提示信息
          this.$message({
              type: 'success',
              message: '修改成功!'
          });
          //回到列表页面 路由跳转
          this.$router.push({path:'/teacher/table'})
        })

路由切换问题

第一次:点击”修改”,数据回显

第二次:再次点击”添加讲师”,进入表单页面,出现问题

问题:回显数据仍然存在,没有进行表单数据清空

原因:

多次路由跳转到同一页面,页面中的created()方法只执行一次,后续跳转不会执行

解决方式:

添加讲师时,清空表单数据

init() {
  //判断路径有id值,做修改
  if(this.$route.params && this.$route.params.id) {
      //从路径获取id值
      const id = this.$route.params.id
      //调用根据id查询的方法
      this.getInfo(id)
  } else { //路径没有id值,做添加
    //清空表单
    this.teacher = {}
  }
}

最终解决:

使用vue监听

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

Day 6

  1. 添加讲师实现头像上传功能
    • 阿里云oss存储服务
    • Nginx使用
  2. 添加课程分类功能
    • 使用EasyExcel,读取excel内容,添加数据
  3. 课程分类列表
    • 树形结构显示

阿里云oss

开发准备

  1. 阿里云官网,找到对象存储OSS服务,开通
  2. 进入阿里云oss管理控制台,创建阿里云oss许可证(AccessKey)

阿里云官方参考文档:https://help.aliyun.com/document_detail/32008.html

搭建项目环境

  1. 在service创建子模块service_oss

  2. service_oss中引入oss相关依赖

    <dependencies>
        <!-- 阿里云oss依赖 -->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
        </dependency>
        <!-- 日期工具栏依赖 -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>
    </dependencies>
  3. 创建配置文件application.properties

    #服务端口
    server.port=8002
    #服务名
    spring.application.name=service-oss
    #环境设置:dev、test、prod
    spring.profiles.active=dev
    
    #阿里云 OSS
    #不同的服务器,地址不同
    aliyun.oss.file.endpoint=
    aliyun.oss.file.keyid=
    aliyun.oss.file.keysecret=
    #bucket可以在控制台创建,也可以使用java代码创建
    aliyun.oss.file.bucketname=
  4. 创建启动类,报错

    image-20210510150320703

    原因:

    启动时,SpringBoot会去寻找数据库配置,然而现在模块并不需要操作数据库,也没有配置数据库

    解决方式:

    • 添加数据库配置

    • 在启动类添加注解属性,不加载数据库配置(使用这种方式)

      @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

上传文件接口实现

  1. 创建常量类,读取配置文件内容

    @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 END_POINT;
        public static String ACCESS_KEY_ID;
        public static String ACCESS_KEY_SECRET;
        public static String BUCKET_NAME;
    
        @Override
        public void afterPropertiesSet() throws Exception {
            END_POINT = endpoint;
            ACCESS_KEY_ID = keyId;
            ACCESS_KEY_SECRET = keySecret;
            BUCKET_NAME = bucketName;
        }
    }
  2. 创建controller service

    @RestController
    @RequestMapping("/eduoss/fileoss")
    @CrossOrigin
    public class OssController {
    
        @Autowired
        private OssService ossService;
    
        //上传头像方法
        @PostMapping
        public R uploadOssFile(MultipartFile file){
            //获取上传文件
            String url = ossService.uploadFileAvatar(file);
    
            return R.ok().data("url",url);
        }
    
    }
    @Service
    @CrossOrigin
    public class OssServiceImpl implements OssService {
    
        //上传头像到oss
        @Override
        public String uploadFileAvatar(MultipartFile file) {
            //通过工具类获取值
            String endpoint = ConstantPropertiesUtils.END_POINT ;
            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 = file.getInputStream();
    
                // 获取文件名称
                String fileName = file.getOriginalFilename();
    
                // 1 在文件名称中加入随机值(去掉-),防止重名
                String uuid = UUID.randomUUID().toString().replaceAll("-","");
                fileName = uuid + fileName;
    
                // 2 文件按照日期进行分类
                // 获取当前日期
                String datePath = new DateTime().toString("yyyy/MM/dd");
                // 拼接
                fileName = datePath + "/" + fileName;
    
                // 调用oss方法实现上传
                // 参数1 Bucket名称
                // 参数2 上传到oss文件路径和文件名称
                // 参数3 上传文件输入流
                ossClient.putObject(bucketName, fileName, inputStream);
    
                // 关闭OSSClient。
                ossClient.shutdown();
    
                // 返回上传文件路径
                String url = "https://" + bucketName + "." + endpoint + "/" + fileName;
    
                return url;
    
            }catch (Exception e){
                e.printStackTrace();
                return null;
            }
        }
    }

上传文件接口完善

  1. 问题:上传相同名称文件,造成文件覆盖

    解决方式:文件名称添加随机唯一值UUID

    // 1 在文件名称中加入随机值(去掉-),防止重名
    String uuid = UUID.randomUUID().toString().replaceAll("-","");
    fileName = uuid + fileName;
  2. 文件分类管理

    根据日期进行分类

    // 2 文件按照日期进行分类
    // 获取当前日期
    String datePath = new DateTime().toString("yyyy/MM/dd");
    // 拼接
    fileName = datePath + "/" + fileName;

Nginx

反向代理服务器

  1. 请求转发
  2. 负载均衡
  3. 动静分离

注意:如果使用cmd命令行启动nginx,关闭命令行窗口,nginx并不会停止

需要手动停止:nginx.exe -s stop

什么是请求转发?

image-20210512083001349

什么是负载均衡?

image-20210512083104460

配置nginx,实现请求转发

  1. 找到nginx配置文件nginx.conf

  2. nginx.conf中进行配置

    • 修改nginx默认端口,把80改为81

         server {
             listen       81;
             server_name  localhost;
      }
    • 配置nginx转发规则

      在http {}里,新建配置

         server {
             listen       9001;
             server_name  localhost;
           
             location ~ /eduservice/ {
                 proxy_pass   http://localhost:8001;
             }
           
             location ~ /eduoss/ {
                 proxy_pass   http://localhost:8002;
             }
      }
  3. 重启nginx,先停止后启动

  4. 修改前端请求地址,改为nginx地址

    dev.env.js

    module.exports = merge(prodEnv, {
      NODE_ENV: '"development"',
      BASE_API: '"http://localhost:9001"',
    })

上传头像前端整合

  1. 在添加讲师页面,创建上传组件,实现上传

    使用element-ui组件实现

    在视频提供的源码里找到组件,复制到src\components

    image-20210512084224767

  2. 添加讲师页面使用此复制上传组件

  3. 使用组件

    data()定义变量和初始值

    data() {
          //上传弹框组件是否显示
          imagecropperShow:false,
          imagecropperKey:0,//上传组件key值
          BASE_API:process.env.BASE_API, //获取dev.env.js里面地址
          saveBtnDisabled:false  // 保存按钮是否禁用
    }
  4. 引入组件和声明组件

    import ImageCropper from '@/components/ImageCropper'
    import PanThumb from '@/components/PanThumb'
    export default {
      components: { ImageCropper, PanThumb },
    }
  5. 修改上传接口地址

  6. 编写close方法和上传成功的方法

    <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:上传成功后的回调 
          <input type="file" name="file"/>
        -->
        <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>
    methods:{
      close() { //关闭上传弹框的方法
          this.imagecropperShow=false
          //上传组件初始化
          this.imagecropperKey = this.imagecropperKey+1
      },
      //上传成功方法
      cropSuccess(data) {
        this.imagecropperShow=false
        //上传之后接口返回图片地址
        this.teacher.avatar = data.url
        this.imagecropperKey = this.imagecropperKey+1
      },
    }

课程分类存储结构

假定有一级分类:前端开发、后端开发

二级分类:vue、js、java、c++

数据库表如何存储二级分类?

使用parent_id字段。

EasyExcel读写操作

写操作

  1. 引入easyexcel依赖

    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.1.1</version>
        </dependency>
    </dependencies>
  2. 创建实体类,与excel数据对应

    @Data
    public class DemoData {
        //设置excel表头名称
        @ExcelProperty(value = "学生编号",index = 0)
        private Integer sno;
        @ExcelProperty(value = "学生姓名",index = 1)
        private String sname;
    }
  3. 实现写操作

    @Test
    public void test() {
        //实现excel写操作
        String filename = "D:\\write.xlsx";
       
        //调用easyexcel实现写操作
        //参数1 文件路径 参数2 实体类class
        EasyExcel.write(filename,DemoData.class).sheet("学生列表").doWrite(getData());

读操作

  1. 创建excel对应实体类,标记对应列关系,同上

    @Data
    public class DemoData {
        //设置excel表头名称
        @ExcelProperty(value = "学生编号",index = 0)
        private Integer sno;
        @ExcelProperty(value = "学生姓名",index = 1)
        private String sname;
    }
  2. 创建监听,读取excel文件

    public class ExcelListener extends AnalysisEventListener<DemoData> {
    
        //逐行读取excel内容
        @Override
        public void invoke(DemoData demoData, AnalysisContext analysisContext) {
            System.out.println("****"+demoData);
        }
    
        //读取表头内容
        @Override
        public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
            System.out.println("表头:"+headMap);
        }
    
        //读取完成之后
        @Override
        public void doAfterAllAnalysed(AnalysisContext analysisContext) {
    
        }
    }
  3. 最终方法调用

    @Test
    public void test(){
        //实现excel读操作
        String filename = "D:\\write.xlsx";
        EasyExcel.read(filename,DemoData.class,new ExcelListener()).sheet().doRead();
    }

读操作分类

  1. 引入easyexcel依赖

  2. 使用代码生成器,生成课程分类代码

  3. 创建实体类,对应excel关系

    @Data
    public class SubjectData {
    
        @ExcelProperty(index = 0)
        private String oneSubjectName;
    
        @ExcelProperty(index = 1)
        private String twoSubjectName;
    
    }
  4. 实现监听器

    public class SubjectExcelListener extends AnalysisEventListener<SubjectData> {
    
        //SubjectExcelListener不会交给Spring进行管理,需要自己new,也不能注入其他对象
        public EduSubjectService subjectService;
    
        public SubjectExcelListener() {
        }
    
        public SubjectExcelListener(EduSubjectService subjectService) {
            this.subjectService = subjectService;
        }
    
        //逐行读取excel内容
        @Override
        public void invoke(SubjectData subjectData, AnalysisContext analysisContext) {
            if (subjectData == null){
                throw new GuliException(20001,"文件数据为空");
            }
    
            //添加一级分类
            //逐行读取时每次都有两个值,值1 一级分类,值2 二级分类
            //先判断一级分类是否重复
            EduSubject existOneSubject = this.existOneSubject(subjectService, subjectData.getOneSubjectName());
            if (existOneSubject == null){
                existOneSubject = new EduSubject();
    
                existOneSubject.setParentId("0");
                existOneSubject.setTitle(subjectData.getOneSubjectName());
    
                subjectService.save(existOneSubject);
            }
    
            //获取一级分类id
            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) {
    
        }
    }

Day 7

  1. 添加课程分类前端实现
  2. 课程分类列表显示功能(树形)
  3. 课程管理模块需求
  4. 添加课程基本信息功能

添加课程分类前端实现

  1. 添加课程分类路由

    router/index.js

    image-20210512095144003

  2. 创建课程分类页面,修改路由对应的页面路径

    views/edu/subject/list.vue

    views/edu/subject/save.vue

  3. 在添加课程分类页面,实现效果

    save.vue添加上传组件

    image-20210512095056681

课程分类列表-树形显示

image-20210512095240960

  1. 参考tree模块,整合前端

    TODO: 创建接口,把分类按照要求的格式返回数据

    data() {
      return {
        filterText: '',
        data2: [{
          id: 1,
          label: 'Level one 1',
          children: [{
            id: 4,
            label: 'Level two 1-1',
            children: [{
              id: 9,
              label: 'Level three 1-1-1'
            }, {
              id: 10,
              label: 'Level three 1-1-2'
            }]
          }]
        }, {
          id: 2,
          label: 'Level one 2',
          children: [{
            id: 5,
            label: 'Level two 2-1'
          }, {
            id: 6,
            label: 'Level two 2-2'
          }]
        }, {
          id: 3,
          label: 'Level one 3',
          children: [{
            id: 7,
            label: 'Level two 3-1'
          }, {
            id: 8,
            label: 'Level two 3-2'
          }]
        }],
        defaultProps: {
          children: 'children',
          label: 'label'
        }
      }
    }
  2. 针对返回数据创建对应实体类

    两个实体类:一级分类 二级分类

  3. 在两个实体类之间表示关系(一级分类包含多个二级分类)

    //一级分类 实体类
    @Data
    public class OneSubject {
        private String id;
        private String title;
    
        //一级分类包含多个二级分类
        private List<TwoSubject> children = new ArrayList<>();
    
    }
  4. 编写具体封装代码

    List<EduSubject> oneSubjectList ===> List<OneSubject> finalSubjectList

课程发布流程

image-20210513160814175

细节:

  1. 创建vo实体类,用于封装表单数据

  2. 把表单提交的数据添加至数据库

    向两张表添加:课程表和课程描述表

  3. 把”讲师”和”分类”用列表显示

    “分类”用二级联动效果实现(类似于 xx省 xx市 的关系效果)

课程相关表的关系

image-20210513161224510

添加课程基本信息接口

  1. 使用代码生成器生成课程相关代码
  2. 创建vo类,封装表单提交的数据
  3. 编写 controller service

注意:课程描述表对应id需要修改生成策略

public class EduCourseDescription implements Serializable {
    @ApiModelProperty(value = "课程ID")
    @TableId(value = "id", type = IdType.INPUT) //手动设置id
    private String id;
}

添加课程基本信息前端

  1. 添加课程管理路由

    该路由为隐藏路由,实现页面跳转

  2. 编写表单页面,实现接口

  3. 添加完成后返回课程id,便于之后添加大纲

    参照后端接口部分:

    //添加课程基本信息
    @PostMapping("addCourseInfo")
    public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo){
        //返回添加之后的课程id
        String id = courseService.saveCourseInfo(courseInfoVo);
       
        return R.ok().data("courseId",id);
    }

二级联动显示

//点击某个一级分类,触发change,显示对应二级分类
      subjectLevelOneChanged(value) {
          //value就是一级分类id值
          //遍历所有的分类,包含一级和二级
          for(var i=0;i<this.subjectOneList.length;i++) {
              //每个一级分类
              var oneSubject = this.subjectOneList[i]
              //判断:所有一级分类id 和 点击一级分类id是否一样
              if(value === oneSubject.id) {
                  //从一级分类获取里面所有的二级分类
                  this.subjectTwoList = oneSubject.children
                  //把二级分类id值清空
                  this.courseInfo.subjectId = ''
              }
          }
      },

Day 8

  1. 添加课程基本信息 完善

    整合文本编辑器

    修改课程基本信息功能 实现

  2. 课程大纲管理

    课程大纲列表显示

    章节添加 修改 删除

    小节添加 修改 删除

  3. 课程信息确认

    编写sql语句实现

    课程最终发布

整合文本编辑器

  1. 复制文本编辑器组件至项目路径

  2. build/webpack.dev.conf.js添加配置

    plugins: [
      new webpack.DefinePlugin({
        'process.env': require('../config/dev.env')
      }),
      new webpack.HotModuleReplacementPlugin(),
      // https://github.com/ampedandwired/html-webpack-plugin
      new HtmlWebpackPlugin({
        filename: 'index.html',
        template: 'index.html',
        inject: true,
        favicon: resolve('favicon.ico'),
        title: 'vue-admin-template',
        templateParameters: {
          BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory
       }
      })
    ]
  3. index.html引入脚本文件

    <script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script>
    <script src=<%= BASE_URL %>/tinymce4.7.5/langs/zh_CN.js></script>
  4. 使用文本编辑器组件

    import Tinymce from '@/components/Tinymce' //引入组件
    
    export default {
        //声明组件
        components: { Tinymce },
    	...
    }

    利用标签实现文本编辑器组件

    <!-- 课程简介-->
    <el-form-item label="课程简介">
        <tinymce :height="300" v-model="courseInfo.description"/>
    </el-form-item>

课程大纲列表功能

参考 Day 7 课程分类列表

  1. 创建两个实体类 章节 小节

    章节实体类中,使用list表示小节

  2. 编写封装代码

  3. 前端整合

课程信息修改功能

效果

  1. 点击 上一步,回显课程基本信息数据
  2. 在数据回显页面,修改数据,保存,数据库内容也进行修改

后端接口

  1. 根据课程id查询课程基本信息接口

    //根据课程id查询课程信息
    @GetMapping("getCourseInfo/{courseId}")
    public R getCourseInfo(@PathVariable String courseId){
        CourseInfoVo courseInfoVo = courseService.getCourseInfo(courseId);
       
        return R.ok().data("courseInfoVo",courseInfoVo);
    }
  2. 修改课程信息接口

    //修改课程信息
    @PostMapping("updateCourseInfo")
    public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo){
        courseService.updateCourseInfo(courseInfoVo);
       
        return R.ok();
    }

前端

  1. api中course.js定义两个方法

  2. 修改chapter页面,跳转路径

  3. info页面实现数据回显

    获取路由课程id,调用根据id查询接口,显示数据

章节添加 修改 删除

添加

点击 添加章节 按钮,弹出添加框,输入章节信息,点击 保存

删除

  1. 如果章节无小节,直接删除

  2. 如果章节有小节,如何删除?

    • 删除章节,小节全部删除

    • 若存在小节,不允许删除

      @Override
      public boolean deleteChapter(String chapterId) {
          //根据章节id查询小节,如果有数据,则不删除
          QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
          wrapper.eq("chapter_id",chapterId);
          int count = videoService.count(wrapper);
          //判断
          if (count > 0){
              throw new GuliException(20001,"不能删除");
          }else {
              //删除章节
              int result = baseMapper.deleteById(chapterId);
           
              return result > 0; //简写
          }
      }

课程信息确认

image-20210513171634905

这些数据需要查询多张表,一般编写sql语句来实现

即:多表连接查询

<!--sql语句:根据课程id查询课程确认信息-->
<select id="getPublishCourseInfo" resultType="com.atguigu.eduservice.entity.vo.CoursePublishVo">
    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=#{courseId}
</select>

报错

在mapper包创建xml文件编写sql语句,执行后出现错误

image-20210513172053701

原因:

maven默认加载机制问题。

maven加载时,只对src/main/java文件夹里的*.java文件进行编译,其他类型文件不加载

解决方式:

  1. xml文件复制到target目录

  2. xml文件放到resources目录

  3. 修改配置

    • pom.xml

      <!-- 项目打包时会将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

Day 9

  1. 课程最终发布 实现
    • 课程信息确认
    • 课程发布
  2. 课程列表
    • 课程列表显示
    • 课程删除
  3. 阿里云视频点播服务
  4. 添加小节实现视频上传

课程列表功能

image-20210513172840999

课程删除

课程包含课程描述、章节、小节、视频

删除课程,把上述部分全部删除

image-20210513173140371

外键一般不建议声明出来

@Override
public void removeCourse(String courseId) {
    //1 根据课程id删除小节
    eduVideoService.removeVideoByCourseId(courseId);
    //2 根据课程id删除章节
    chapterService.removeChapterByCourseId(courseId);
    //3 根据课程id删除描述
    courseDescriptionService.removeById(courseId);
    //4 根据课程id删除课程本身
    int result = baseMapper.deleteById(courseId);

    if (result == 0){
        throw new GuliException(20001,"删除失败");
    }
}

阿里云视频点播

  1. 阿里云开通视频点播服务,选择按流量计费
  2. 视频点播官方文档:https://help.aliyun.com/document_detail/51512.html
    • 服务端:后端接口
    • 客户端:浏览器、Android、ios
    • API:阿里云提供的固定地址,只需调用这个固定地址,并向地址传递参数,就可以实现某些功能
    • SDK:对API方式进行封装,更方便使用。例如EasyExcel调用阿里云提供的类或接口中方法实现某些功能
    • HttpClient 技术使得可以调用API地址http://vod.cn-shanghai.aliyuncs.com?Action=GetPlayInfo&VideoId=114514

SDK 获取视频地址和凭证

  1. 获取视频播放地址
  2. 获取视频播放凭证
  3. 上传到阿里云视频点播服务

阿里云对上传视频可以进行加密,此后使用地址不能播放视频,所以数据库不是存储地址,而是存储视频id

  1. 在 service 创建子模块 service_vod,引入相关依赖

    <dependencies>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-vod</artifactId>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-sdk-vod-upload</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>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>
    </dependencies>
  2. 初始化操作,创建DefaultAcsClient对象(test目录)

    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;
    }
  3. 根据视频id,获取视频播放地址

    public void getPlayUrl() throws ClientException {
        //1 根据视频id获取视频地址
        //创建初始化对象
        DefaultAcsClient client = InitObject.initVodClient(" "," ");
        //创建获取视频地址request response
        GetPlayInfoRequest request = new GetPlayInfoRequest();
        GetPlayInfoResponse response = new GetPlayInfoResponse();
        //向request对象设置视频id
        request.setVideoId("a60bc97edc9045618980a758a4f895c8");
        //调用初始化对象里面的方法,传递request,获取数据
        response = client.getAcsResponse(request);
        List<GetPlayInfoResponse.PlayInfo> playInfoList = response.getPlayInfoList();
        //播放地址
        for (GetPlayInfoResponse.PlayInfo playInfo : playInfoList) {
            System.out.print("PlayInfo.PlayURL = " + playInfo.getPlayURL() + "\n");
        }
        //Base信息
        System.out.print("VideoBase.Title = " + response.getVideoBase().getTitle() + "\n");
       
    }
  4. 根据视频id,获取视频播放凭证

    public void getPlayAuth() throws ClientException {
        //根据视频id获取视频播放凭证
        //创建初始化对象
        DefaultAcsClient client = InitObject.initVodClient(" "," ");
        //创建获取视频凭证request和response
        GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
        GetVideoPlayAuthResponse response = new GetVideoPlayAuthResponse();
        //向request对象设置视频id
        request.setVideoId("a60bc97edc9045618980a758a4f895c8");
        response = client.getAcsResponse(request);
        System.out.println("playauth:" + response.getPlayAuth());
    }

SDK 上传视频

注意:aliyun-java-vod-upload的jar依赖并未开源,需要到官网SDK下载jar包,并安装到本地maven仓库

通过官方jar包中自带的UploadVideoDemo.java文件,改造想要的功能

  1. 引入依赖

  2. 创建 application.properties 配置文件

  3. 创建启动类

    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @ComponentScan(basePackages = {"com.atguigu"})
    public class VodApplication {
        public static void main(String[] args) {
            SpringApplication.run(VodApplication.class,args);
        }
    }
  4. 创建controller service

    @RestController
    @RequestMapping("/eduvod/video")
    @CrossOrigin
    public class VodController {
    
        @Autowired
        private VodService vodService;
    
        //上传视频到阿里云
        @PostMapping("uploadAliVideo")
        public R uploadAliVideo(MultipartFile file){
            //返回上传视频的id
            String videoId = vodService.uploadVideoAli(file);
            return R.ok().data("videoId",videoId);
        }
    }
    @Service
    public class VodServiceImpl implements VodService {
        @Override
        public String uploadVideoAli(MultipartFile file) {
    
            try {
                //accessKeyId, accessKeySecret
    
                //fileName 上传文件原始名称
                String fileName = file.getOriginalFilename();
    
                //title 上传之后的名称
                String title = fileName.substring(0,fileName.lastIndexOf("."));
    
                //inputStream 上传文件输入流
                InputStream inputStream = file.getInputStream();
    
                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 videoId = null;
                if (response.isSuccess()) {
                    videoId = response.getVideoId();
                } else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因
                    videoId = response.getVideoId();
                }
    
                return videoId;
    
            }catch (Exception e){
                e.printStackTrace();
                return null;
            }
        }
    }
  5. 测试,出现tomcat上传大小问题

    • 解决方式:

      在application.properties中进行配置

      # 最大上传单个文件大小:默认1M
      spring.servlet.multipart.max-file-size=1024MB
      # 最大置总上传的数据大小 :默认10M
      spring.servlet.multipart.max-request-size=1024MB
  6. 前端同样出现问题

    • 在nginx.conf配置8003端口规则

      location ~ /eduvod/ {
                proxy_pass   http://localhost:8003;
            }
    • nginx支持的上传大小有限制

      POST http://localhost:9001/eduvod/video/uploadAliVideo 413(Request Entity Too Large)

      413:即请求体过大

      在nginx.conf中添加大小设置

      http {
          include       mime.types;
          default_type  application/octet-stream;
          client_max_body_size 1024m;
          ...
      }

Day 10

  1. 添加小节删除视频(包括阿里云视频)
  2. springcloud
    • 删除小节删除视频
    • 删除课程删除视频

添加小节删除视频

  1. 删除阿里云视频接口

    //根据视频id删除阿里云视频
    @DeleteMapping("removeAliVideo/{id}")
    public R removeAliVideo(@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,"删除视频失败");
        }
    }
  2. 前端页面调用接口

    • 在api定义接口路径

      //删除视频
      deleteAliyunvod(id) {
          return request({
              url: '/eduvod/video/removeAliVideo/'+id,
              method: 'delete'
            })
      }
    • chapter.vue页面调用

      handleVodRemove() {
          //调用接口的删除视频的方法
          video.deleteAliyunvod(this.video.videoSourceId)
              .then(response => {
                  //提示信息
                  this.$message({
                      type: 'success',
                      message: '删除视频成功!'
                  });
                  //把文件列表清空
                  this.fileList = []
                  //把video视频id和视频名称值清空
                  //上传视频id赋值
                  this.video.videoSourceId = ''
                  //上传视频名称赋值
                  this.video.videoOriginalName = ''
              })
      },

微服务与SpringCloud

微服务

image-20210517145847399

  1. 微服务是架构风格

  2. 把一个项目拆分成独立的多个服务

    多个服务独立运行,每个服务占用独立进程

SpringCloud

  1. SpringCloud并不是一种技术,而是多种技术总称,多种框架集合
  2. 使用SpringCloud这些框架,实现微服务操作
  3. 使用SpringCloud,需要依赖技术SpringBoot

SpringCloud相关基础服务组件

image-20210517150224097

Nacos

Nacos是注册中心,相当于房产中介

若要实现不同微服务模块之间的调用,需把这些模块注册到注册中心,此后实现相互调用

image-20210517150617343

Nacos流程

image-20210517150707275

Nacos注册过程

访问nacos:

http://localhost:8848/nacos

用户名:nacos

密码:nacos

  1. service的pom引入依赖

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <version>2.2.5.RELEASE</version>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
  2. 在注册服务的配置文件application.properties中,配置Nacos地址

    # nacos服务地址
    spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
  3. 在主启动类添加注解@EnableDiscoveryClient

  4. 访问Nacos控制台的服务列表,查看是否注册成功

OpenFeign

服务调用实现过程

前提:需要互相调用的服务在Nacos进行注册

  1. service模块引入依赖

    <!--服务调用-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. 调用端service-edu,主启动类添加注解@EnableFeignClients

  3. 调用端service-edu,创建interface,使用注解指定调用的服务名称,指定调用的方法路径(绝对路径)

    @Component
    @FeignClient(name = "service-vod")
    public interface VodClient {
    
        //定义调用的方法路径
        //@PathVariable注解一定要指定参数名称,否则出错
        @DeleteMapping("/eduvod/video/removeAlyVideo/{id}")
        public R removeAlyVideo(@PathVariable("id") String id);
        
    }
  4. 实现删除小节,同时删除阿里云视频

    //删除小节
    @DeleteMapping("{id}")
    public R deleteVideo(@PathVariable String id){
        //参数为小节id,需要根据小节id,得到视频id
        EduVideo eduVideo = videoService.getById(id);
        String videoSourceId = eduVideo.getVideoSourceId();
        //判断小节里是否有视频id
        if (!StringUtils.isEmpty(videoSourceId)){
            //根据视频id,"远程调用"实现视频删除
            vodClient.removeAlyVideo(videoSourceId);
        }
        //删除小节
        videoService.removeById(id);
        return R.ok();
    }

删除课程删除视频

一个课程有很多章节,一个章节有很多小节,每个小节有视频

删除课程时,有对应多个视频需要删除

  1. 在service-vod创建接口

    public interface VodService {
    	
        ...
    
        void removeMoreAlyVideo(List videoIdList);
    }
    @Override
    public void removeMoreAlyVideo(List videoIdList) {
        try {
            //初始化对象
            DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);
            //创建删除视频request对象
            DeleteVideoRequest request = new DeleteVideoRequest();
       
            //videoIdList中的id转换成 值1,值2,值3 的形式
            String videoIds = StringUtils.join(videoIdList.toArray(), ",");
       
            //向request设置视频id
            request.setVideoIds(videoIds);
       
            //调用初始化对象的方法实现删除
            client.getAcsResponse(request);
        }catch(Exception e) {
            e.printStackTrace();
            throw new GuliException(20001,"删除视频失败");
        }
    }
  2. 在service-edu,调用service-vod接口,实现删除多个视频的功能

    @Override
    public void removeVideoByCourseId(String courseId) {
        //根据课程id,查询所有视频id
        QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();
        wrapperVideo.eq("course_id",courseId);
        wrapperVideo.select("video_source_id");
        List<EduVideo> eduVideoList = baseMapper.selectList(wrapperVideo);
       
        //List<EduVideo>转换为List<String>
        List<String> videoIds = new ArrayList<>();
        for (int i = 0; i < eduVideoList.size(); i++) {
            EduVideo eduVideo = eduVideoList.get(i);
            String videoSourceId = eduVideo.getVideoSourceId();
       
            //判断是否为空,非空则加入
            if (!StringUtils.isEmpty(videoSourceId)){
                videoIds.add(videoSourceId);
            }
       
        }
       
        if (videoIds.size() > 0){
            //多个视频id,删除对应多个视频
            vodClient.deleteBatch(videoIds);
        }
       
       
        QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id",courseId);
        baseMapper.delete(wrapper);
    }
  3. 运行代码出现错误:java.util.List<?> Not declared?

    原因:List泛型没有定义

    解决:

    //删除多个阿里云视频
    @DeleteMapping("/eduvod/video/delete-batch")
    public R deleteBatch(@RequestParam("videoIdList") List<String> videoIdList);

SpringCloud调用接口过程

SpringCloud对于接口调用,大致会经过如下几个组件配合:

Feign -> Hystrix -> Ribbon -> HttpClient (Apache http components 或 Okhttp)

具体交互流程

image-20210602000857550

SpringCloud熔断器

image-20210602001002324

  1. 添加熔断器依赖

  2. 在调用端配置文件中,开启熔断器

    #开启熔断机制
    feign.hystrix.enabled=true
  3. 创建interface后,还需要创建其实现类,出错时实现错误内容输出

    @Component
    public class VodFileDegradeFeignClient implements VodClient {
       //出错之后会执行
        @Override
        public R removeAlyVideo(String id) {
            return R.error().message("删除视频出错了");
        }
    
        @Override
        public R deleteBatch(List<String> videoIdList) {
            return R.error().message("删除多个视频出错了");
        }
    }
  4. 在interface添加注解和属性

    @FeignClient(name = "service-vod",fallback = VodFileDegradeFeignClient.class) //调用的服务名称
    @Component
    public interface VodClient {
    	...
    }

Day 11

  1. 搭建项目前台环境

    NUXT框架

  2. 整合前台系统页面

  3. 首页显示banner数据

    轮播图或幻灯片

  4. 首页显示热门课程和名师

  5. 首页数据使用redis缓存

image-20210602002753199

搭建项目前台环境

image-20210602002833072

NUXT目录结构

image-20210602003436383

NUXT页面加载过程

layouts文件夹下的default.vue只布局头信息、尾信息

而中间信息则是从pages文件夹index.vue引入

image-20210602003806528

后端首页数据banner

  1. service创建子模块service_cms

  2. 创建application.properties配置文件

  3. 创建数据库crm_banner,根据表使用代码生成器

  4. 前台banner显示,后台banner管理

    image-20210602004207376

  5. 查询所有banner

    public List<CrmBanner> selectAllBanner() {
        //根据id降序排列,显示排列之后的前两条记录
        QueryWrapper<CrmBanner> wrapper = new QueryWrapper<>();
        wrapper.orderByDesc("id");
        //last可以拼接sql语句
        wrapper.last("limit 2");
       
        List<CrmBanner> list = baseMapper.selectList(wrapper);
        return list;
    }

首页热门课程和名师接口

//查询前8条热门课程,查询前4条名师
@GetMapping("index")
public R index(){
    //查询前8条热门课程
    QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
    wrapper.orderByDesc("id");
    wrapper.last("limit 8");
    List<EduCourse> eduList = courseService.list(wrapper);

    //查询前4条名师
    QueryWrapper<EduTeacher> wrapperTeacher = new QueryWrapper<>();
    wrapper.orderByDesc("id");
    wrapper.last("limit 4");
    List<EduTeacher> teacherList = teacherService.list(wrapperTeacher);

    return R.ok().data("eduList",eduList).data("teacherList",teacherList);
}

首页数据显示(前端)

前端环境准备

  1. 下载axios依赖

    npm install axios
  2. 封装axios

    import axios from 'axios'
    import cookie from 'js-cookie'
    import { MessageBox, Message } from 'element-ui'
    
    // 创建axios实例
    const service = axios.create({
      baseURL: `http://localhost:9001`, // api的base_url
      timeout: 20000 // 请求超时时间
    })
    
    export default service

首页banner数据显示

  1. 创建api文件夹,在api文件夹创建js文件,定义调用接口路径

    import request from '@/utils/request'
    export default {
      getListBanner() {
        return request({
          url: '/educms/bannerfront/getAllBanner',
          method: 'get'
        })
      }
    }
  2. 在页面index.vue,调用接口得到数据进行显示

     created() {
    //调用查询banner的方法
       this.getBannerList()
     }
     methods:{
         //查询banner数据
       getBannerList() {
         banner.getListBanner()
           .then(response => {
             this.bannerList = response.data.data.list
           })
       }
     }
       
  3. nginx进行访问规则配置

    location ~ /educms/ {
        proxy_pass   http://localhost:8004;
    }

Redis

  • 基于 key-value 进行存储
  • 支持多种数据结构:string字符串 list列表 hash哈希 set集合 zset有序集合
  • 支持持久化,通过内存进行存储,也可以存到硬盘里
  • 支持过期时间,支持事务
  • 一般情况下,把经常查询,不经常修改,且不重要的数据放到redis作为缓存
  1. 创建redis配置类,放到common模块下

    • 引入SpringBoot整合Redis的相关依赖

            <!-- 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>
    • 创建redis缓存配置类,配置插件

      @EnableCaching
      @Configuration
      public class RedisConfig extends CachingConfigurerSupport {
      	...
      }
  2. 在查询所有banner的方法上,添加缓存注解@Cacheable

    image-20210607150231153

    @Cacheable(key = "'selectIndexList'",value = "banner")
    @Override
    public List<CrmBanner> selectAllBanner() {
        ...
    }
  3. application.properties

    # redis
    spring.redis.host=192.168.161.129
    spring.redis.port=6379
    spring.redis.database= 0
    spring.redis.timeout=1800000
    spring.redis.lettuce.pool.max-active=20
    spring.redis.lettuce.pool.max-wait=-1
    #最大阻塞等待时间(负数表示没限制)
    spring.redis.lettuce.pool.max-idle=5
    spring.redis.lettuce.pool.min-idle=0
  4. 虚拟机Linux环境下,启动redis

Day 12

  1. 登录实现流程
  2. 注册接口
    • 整合jwt
    • 整合阿里云短信服务(已替换为邮件发送)
  3. 登录接口
  4. 注册和登录前端实现

单点登录三种方式

单一服务器模式

使用session对象实现

  • 登录成功之后,把用户数据放到session里面
  • 从session获取数据,判断是否登录
session.setAttribute("user",user);

session.getAttribute("user");

集群部署模式

image-20210607151417616

单点登录三种常见方式

  1. session 广播机制实现

    即session复制

  2. 使用 cookie+redis 实现

    • 在项目中任何一个模块进行登录,登陆之后,把数据放到两个地方
      • redis:key生成随机唯一值(ip或用户id等),value存放用户数据
      • cookie:把redis生成的key放到cookie里
    • 访问项目其他模块,携带cookie进行发送请求
      • 通过cookie取值,到redis进行查询,根据key查询数据,若存在数据则判断登录
  3. 使用 token 实现

    什么是token?

    按照一定规则生成的字符串。可以包含用户信息

    • 在项目某个模块登录,登录之后,按照特定规则生成字符串,用户信息包含到字符串中,返回字符串
      • 字符串可以通过cookie返回
      • 也可以通过地址栏返回
    • 访问其他模块,每次访问在地址栏携带生成的字符串,访问模块中获取该字符串,根据字符串获取用户信息

JWT

token是按照一定规则生成的字符串,且包含用户信息

一般采用通用规则 JWT

JWT生成的字符串包含三部分:

  1. JWT头信息
  2. 有效载荷,包含主体信息(用户信息)
  3. 哈希签名,防伪标志

如何使用

  1. 引入 JWT 依赖

    <!-- JWT-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
    </dependency>
  2. 创建 JWT 工具类

    public class JwtUtils {
    
        public static final long EXPIRE = 1000 * 60 * 60 * 24;
        public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
    
        public static String getJwtToken(String id, String nickname){
    
            String JwtToken = Jwts.builder()
                    .setHeaderParam("typ", "JWT")
                    .setHeaderParam("alg", "HS256")
                    .setSubject("guli-user")
                    .setIssuedAt(new Date())
                    .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                    .claim("id", id)
                    .claim("nickname", nickname)
                    .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                    .compact();
    
            return JwtToken;
        }
    
        /**
         * 判断token是否存在与有效
         * @param jwtToken
         * @return
         */
        public static boolean checkToken(String jwtToken) {
            if(StringUtils.isEmpty(jwtToken)) return false;
            try {
                Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }
    
        /**
         * 判断token是否存在与有效
         * @param request
         * @return
         */
        public static boolean checkToken(HttpServletRequest request) {
            try {
                String jwtToken = request.getHeader("token");
                if(StringUtils.isEmpty(jwtToken)) return false;
                Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }
    
        /**
         * 根据token获取会员id
         * @param request
         * @return
         */
        public static String getMemberIdByJwtToken(HttpServletRequest request) {
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return "";
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            Claims claims = claimsJws.getBody();
            return (String)claims.get("id");
        }
    }

阿里云短信服务

邮件发送服务

由于阿里云审核方面的问题,已替换为邮件发送,具体参考另一篇文章

SpringCloud实现邮件发送

Redis验证码有效时间问题

//这里的phone只起到账号作用
@GetMapping("/send/{phone}")
public R sendMail(@PathVariable String phone){
    //1 从redis获取验证码,如果获取到直接返回
    String code = redisTemplate.opsForValue().get(phone);
    if (!StringUtils.isEmpty(code)){
        return R.ok();
    }

    //2 获取不到,生成随机验证码,发送邮件
    code = RandomUtil.getFourBitRandom();
    Boolean isSend = msmService.sendMail(code,phone);

    //发送成功,账号对应的邮件验证码存入redis,并设置有效时间为5分钟
    if (isSend){
        redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
        return R.ok();
    }else {
        return R.error().message("发送邮件失败");
    }
}

登录接口

  1. 在service下面创建子模块service_ucenter
  2. 创建用户表,使用代码生成器生成controller entity mapper service

controller

//登录
@PostMapping("login")
public R loginUser(@RequestBody UcenterMember member){
    //member封装手机号和密码
    //调用service
    //返回token,使用jwt生成
    String token = memberService.login(member);
    return R.ok().data("token",token);
}

service

@Override
public String login(UcenterMember member) {
    //获取登录手机号和密码
    String mobile = member.getMobile();
    String password = member.getPassword();

    if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
        throw new GuliException(20001,"登录失败");
    }

    //判断手机号是否存在
    QueryWrapper<UcenterMember > wrapper = new QueryWrapper<>();
    wrapper.eq("mobile",mobile);
    UcenterMember ucenterMember = baseMapper.selectOne(wrapper);
    if (ucenterMember == null){
        throw new GuliException(20001,"手机号不存在");
    }

    //判断密码是否正确
    //注意:数据库密码是经过加密的,md5加密
    //要先把输入密码加密后进行比较
    if (!MD5.encrypt(password).equals(ucenterMember.getPassword())){
        throw new GuliException(20001,"密码错误");
    }

    //判断账号是否禁用
    if (ucenterMember.getIsDisabled()){
        throw new GuliException(20001,"账号被禁用");
    }

    //登录成功后生成token字符串
    String jwtToken = JwtUtils.getJwtToken(ucenterMember.getId(), ucenterMember.getNickname());

    return jwtToken;
}

注册接口

  1. 创建实体类,封装注册数据

    @Data
    @ApiModel(value="注册对象", description="注册对象")
    public class RegisterVo {
        @ApiModelProperty(value = "昵称")
        private String nickname;
    
        @ApiModelProperty(value = "手机号")
        private String mobile;
    
        @ApiModelProperty(value = "密码")
        private String password;
    
        @ApiModelProperty(value = "验证码")
        private String code;
    }
  2. controller

    //注册
    @PostMapping("register")
    public R registerUser(@RequestBody RegisterVo registerVo){
        memberService.register(registerVo);
        return R.ok();
    }
  3. service

    @Override
    public void register(RegisterVo registerVo) {
        //获取注册数据
        String code = registerVo.getCode();
        String mobile = registerVo.getMobile();
        String nickname = registerVo.getNickname();
        String password = registerVo.getPassword();
       
        //非空判断
        if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)
        || StringUtils.isEmpty(nickname) || StringUtils.isEmpty(code)){
            throw new GuliException(20001,"注册失败");
        }
       
        //判断验证码
        String redisCode = redisTemplate.opsForValue().get(mobile);
        if (!code.equals(redisCode)){
            throw new GuliException(20001,"注册失败");
        }
       
        //手机号不能重复
        QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile",mobile);
        Integer count = baseMapper.selectCount(wrapper);
        if (count > 0){
            throw new GuliException(20001,"注册失败");
        }
       
        //数据添加至数据库
        UcenterMember ucenterMember = new UcenterMember();
        ucenterMember.setMobile(mobile);
        ucenterMember.setNickname(nickname);
        ucenterMember.setPassword(MD5.encrypt(password)); //密码需要MD5加密
        ucenterMember.setIsDisabled(false);
        ucenterMember.setAvatar("http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132");
       
       
        baseMapper.insert(ucenterMember);
    }
  4. 接口:根据token字符串获取用户信息

    //根据token获取用户信息
    @GetMapping("getMemberInfo")
    public R getMemberInfo(HttpServletRequest request){
        //调用jwt工具类,根据request对象获取头信息,返回用户id
        String memberId = JwtUtils.getMemberIdByJwtToken(request);
        //查询数据库,根据id获取用户信息
        UcenterMember member = memberService.getById(memberId);
        return R.ok().data("userInfo",member);
    }

注册与登录页面前端整合

  1. 安装插件

    npm install element-ui
    npm install vue-qriously
  2. 在nuxt中使用插件element-ui

    plugins/nuxt-swiper-plugin.js

    import Vue from 'vue'
    import VueAwesomeSwiper from 'vue-awesome-swiper'
    import VueQriously from 'vue-qriously'
    import ElementUI from 'element-ui' //element-ui的全部组件
    import 'element-ui/lib/theme-chalk/index.css'//element-ui的css
    Vue.use(ElementUI) //使用elementUI
    Vue.use(VueQriously)
    Vue.use(VueAwesomeSwiper)
  3. 整合注册页面

    • layouts文件夹创建登录注册布局页面sign.vue

    • 修改登录和注册地址

      layouts/default.vue

      <li v-if="!loginInfo.id" id="no-login">
          <a href="/login" title="登录">
              <em class="icon18 login-icon">&nbsp;</em>
              <span class="vam ml5">登录</span>
          </a>
          |
          <a href="/register" title="注册">
              <span class="vam ml5">注册</span>
          </a>
      </li>
    • 创建注册页面register.vue

  4. 整合注册功能

    • api创建register.js,定义注册接口的方法

    • 实现倒计时效果,使用js定时器方法实现

    • nginx配置路径匹配规则

      location ~ /edumsm/ {
          proxy_pass   http://localhost:8005;
      }
           
      location ~ /educenter/ {
          proxy_pass   http://localhost:8006;
      }

Day 13

  1. 登录前端整合
  2. 微信扫描登录
    • OAuth2
    • 微信登录

登录前端整合

  1. 在api创建login.js,定义接口地址

  2. 安装插件

    npm install js-cookie
  3. 登录及登录之后,首页显示数据实现过程分析

    image-20210627214041507

OAuth2

OAuth2是针对特定问题的一种解决方案

  1. 开放系统间的授权
  2. 分布式访问

开放系统间的授权

image-20210627214302363

分布式访问(单点登录)

image-20210627214334851

微信扫描登录

准备工作:

  1. 注册开发者资质
  2. 申请网站应用名称
  3. 需要域名地址
  1. service-ucenter模块配置文件,微信id、秘钥和域名地址

  2. 创建类读取配置文件内容

    @GetMapping("login")
    public String getWxCode(){
        String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
                "?appid=%s" +
                "&redirect_uri=%s" +
                "&response_type=code" +
                "&scope=snsapi_login" +
                "&state=%s" +
                "#wechat_redirect";
       
        //对redirect_url进行编码
        String redirect_url = ConstantWxUtils.WX_OPEN_REDIRECT_URL;
        try {
            redirect_url = URLEncoder.encode(redirect_url, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
       
        String url = String.format(
                baseUrl,
                ConstantWxUtils.WX_OPEN_APP_ID,
                redirect_url,
                "atguigu"
        );
        
        //重定向到请求微信地址
        return "redirect:" + url;
    }
  3. 生成微信扫描二维码

    • 直接请求微信提供的固定地址,向地址后面拼接参数

扫描之后获取扫描人信息

技术点:

  1. httpclient

  2. json转换工具

    fast json, gson, jackson

  1. 扫描之后,执行本地的callback方法,获取两个值,跳转时进行传递

    • state:原样传递
    • code:类似于手机验证码,唯一随机值(临时票据)
  2. 通过code值,请求微信提供的固定地址,获取另外两个值

    • access_token:访问凭证
    • openid:每个微信唯一标识
  3. 通过access_token和openid,再去请求微信提供的固定地址,获取扫描人信息(昵称、头像等)

  4. 在首页显示微信信息,如昵称、头像

    • 把扫描后的信息放到cookie,跳转到首页显示

    • 但是存在一个问题:cookie无法实现跨域访问

    • 解决方式:使用jwt,生成token字符串,通过路径传递到首页

      //使用jwt,根据member对象,生成token字符串
      String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
      return "redirect:http://localhost:3000?token=" + jwtToken;

首页显示数据

  1. 首页路径中有token字符串,获取token
  2. token值放入cookie
    • 前端拦截器,判断cookie是否有token,若有,获取token,放到header里
  3. 调用后端接口,根据token值获取用户信息,用户信息存入cookie

Day 14

  1. 名师列表功能
  2. 名师详情功能
  3. 课程列表功能
  4. 课程详情功能
  5. 整合阿里云视频播放器
  6. 课程评论功能

名师列表功能

  1. 分页查询名师接口

    @Override
    public Map<String, Object> getTeacherFrontList(Page<EduTeacher> pageParam) {
        QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();
        wrapper.orderByDesc("id");
        //把分页数据封装到pageTeacher对象
        baseMapper.selectPage(pageParam,wrapper);
       
        List<EduTeacher> records = pageParam.getRecords();
        long current = pageParam.getCurrent();
        long pages = pageParam.getPages();
        long size = pageParam.getSize();
        long total = pageParam.getTotal();
       
        boolean hasNext = pageParam.hasNext();
        boolean hasPrevious = pageParam.hasPrevious();
       
        //分页数据放到map集合
        Map<String,Object> map = new HashMap<>();
        map.put("items", records);
        map.put("current", current);
        map.put("pages", pages);
        map.put("size", size);
        map.put("total", total);
        map.put("hasNext", hasNext);
        map.put("hasPrevious", hasPrevious);
       
        return map;
    }
  2. 整合前端页面

    • 在api创建js文件,定义接口地址
    • 在页面引入js文件,调用方法进行显示

讲师详情功能

  1. 修改前端讲师列表页面超链接,改成讲师id

  2. 后端讲师详情接口

    • 根据讲师id查询讲师基本信息

    • 根据讲师id查询讲师所讲课程

      @GetMapping("getTeacherFrontInfo/{teacherId}")
      public R getTeacherFrontInfo(@PathVariable String teacherId){
          //1 根据讲师id,查询讲师基本信息
          EduTeacher eduTeacher = teacherService.getById(teacherId);
           
          //2 根据讲师id,查询所讲课程
          QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
          wrapper.eq("teacher_id",teacherId);
          List<EduCourse> courseList = courseService.list(wrapper);
           
          return R.ok().data("teacher",eduTeacher).data("courseList",courseList);
      }

课程列表功能

条件查询带分页功能

  1. 创建 vo 对象,封装条件数据

    @Data
    public class CourseFrontVo{
        ...
    }
  2. 编写 controller service

    @PostMapping("getFrontCourseList/{page}/{limit}")
    public R getFrontCourseList(@PathVariable long page, @PathVariable long limit,
                                @RequestBody(required = false) CourseFrontVo courseFrontVo) {
        Page<EduCourse> pageCourse = new Page<>(page,limit);
        Map<String,Object> map = courseService.getCourseFrontList(pageCourse,courseFrontVo);
        //返回分页所有数据
        return R.ok().data(map);
    }
  3. 课程列表前端整合

课程详情功能

  1. 编写 sql 语句,根据课程 id 查询课程信息

    • 课程基本信息
    • 课程分类
    • 课程描述
    • 所属讲师
  2. 根据课程 id 查询章节和小节

    @GetMapping("getFrontCourseInfo/{courseId}")
    public R getFrontCourseInfo(@PathVariable String courseId) {
        //根据课程id,编写sql语句查询课程信息
        CourseWebVo courseWebVo = courseService.getBaseCourseInfo(courseId);
       
        //根据课程id查询章节和小节
        List<ChapterVo> chapterVideoList = chapterService.getChapterVideoByCourseId(courseId);
       
        return R.ok().data("courseWebVo",courseWebVo).data("chapterVideoList",chapterVideoList);
    }

整合阿里云播放器

  1. 创建接口,根据视频 id 获取视频播放凭证

    //根据视频id获取视频凭证
    @GetMapping("getPlayAuth/{id}")
    public R getPlayAuth(@PathVariable String id) {
        try {
            //创建初始化对象
            DefaultAcsClient client =
                    InitVodCilent.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);
            //创建获取凭证request和response对象
            GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
            //向request设置视频id
            request.setVideoId(id);
            //调用方法得到凭证
            GetVideoPlayAuthResponse response = client.getAcsResponse(request);
            String playAuth = response.getPlayAuth();
            return R.ok().data("playAuth",playAuth);
        }catch(Exception e) {
            throw new GuliException(20001,"获取凭证失败");
        }
    }
  2. 修改前端,实现点击某个小节,打开新的页面进行视频播放

Day15

  1. 课程评论功能
  2. 课程支付功能

课程评论功能

实现以下效果:

image-20211030232302114

具体实现过程

  1. 创建课程评论表 edu_comment
  2. 创建接口和两个方法
    • 分页查询课程评论方法
    • 添加评论方法

image-20211030233235446

注意:评论前必须先登录

  1. 从 header 获取 token(从 request 获取)
  2. 根据 token 获取用户 id(使用 jwt)
  3. 根据用户 id 查询用户表,获取数据

课程支付功能

需求分析

image-20211101172058132

image-20211101172010346

支付相关接口汇总

  1. 生成订单接口
  2. 根据订单 id 查询订单信息接口
  3. 微信支付二维码生成接口
  4. 订单支付状态查询接口

生成订单接口

生成订单需要:课程信息、用户信息

远程调用-Nacos

image-20211101173219899

service_order

//1 生成订单的方法
@PostMapping("createOrder/{courseId}")
public R saveOrder(@PathVariable String courseId, HttpServletRequest request) {
    //创建订单,返回订单号
    String orderNo =
            orderService.createOrders(courseId,JwtUtils.getMemberIdByJwtToken(request));
    return R.ok().data("orderId",orderNo);
}

service_edu

//根据课程id查询课程信息
@PostMapping("getCourseInfoOrder/{id}")
public CourseWebVoOrder getCourseInfoOrder(@PathVariable String id) {
    CourseWebVo courseInfo = courseService.getBaseCourseInfo(id);
    CourseWebVoOrder courseWebVoOrder = new CourseWebVoOrder();
    BeanUtils.copyProperties(courseInfo,courseWebVoOrder);
    return courseWebVoOrder;
}

service_ucenter

//根据用户id获取用户信息
@PostMapping("getUserInfoOrder/{id}")
public UcenterMemberOrder getUserInfoOrder(@PathVariable String id) {
    UcenterMember member = memberService.getById(id);
    //把member对象里面值复制给UcenterMemberOrder对象
    UcenterMemberOrder ucenterMemberOrder = new UcenterMemberOrder();
    BeanUtils.copyProperties(member,ucenterMemberOrder);
    return ucenterMemberOrder;
}

根据订单 id 查询订单信息

//2 根据订单id查询订单信息
@GetMapping("getOrderInfo/{orderId}")
public R getOrderInfo(@PathVariable String orderId) {
    QueryWrapper<Order> wrapper = new QueryWrapper<>();
    wrapper.eq("order_no",orderId);
    Order order = orderService.getOne(wrapper);
    return R.ok().data("item",order);
}

微信支付二维码生成接口

准备:微信支付 id,商户号,商户 key

引入微信支付 maven 依赖

<dependencies>
    <dependency>
        <groupId>com.github.wxpay</groupId>
        <artifactId>wxpay-sdk</artifactId>
        <version>0.0.3</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
    </dependency>
</dependencies>

编写接口

//生成微信支付二维码接口
//参数是订单号
@GetMapping("createNative/{orderNo}")
public R createNative(@PathVariable String orderNo) {
    //返回信息,包含二维码地址,还有其他需要的信息
    Map map = payLogService.createNatvie(orderNo);
    System.out.println("****返回二维码map集合:"+map);
    return R.ok().data(map);
}

Day 16

  1. 支付后执行过程总结
  2. 课程详情页面观看和购买功能完善
  3. 系统后台统计分析模块
    • 统计分析模块需求
    • 生成统计数据
    • 使用图表显示统计数据

支付后流程

在课程详情页面,有立即购买/立即观看按钮

  • 如果课程是免费的,按钮为立即观看
  • 如果课程是付费的,且已支付,按钮为立即观看
  • 如果课程是付费的,但未支付,按钮为立即购买

如何判断课程是否已支付

根据课程 id 和用户 id,查询订单表中的订单状态

若状态值为 1,表示已支付,否则未支付

//根据课程id和用户id查询订单表中订单状态
@GetMapping("isBuyCourse/{courseId}/{memberId}")
public boolean isBuyCourse(@PathVariable String courseId,@PathVariable String memberId) {
    QueryWrapper<Order> wrapper = new QueryWrapper<>();
    wrapper.eq("course_id",courseId);
    wrapper.eq("member_id",memberId);
    wrapper.eq("status",1);//支付状态 1代表已经支付
    int count = orderService.count(wrapper);
    if(count>0) { //已经支付
        return true;
    } else {
        return false;
    }
}

在课程详情页面显示立即购买/立即观看,需要修改课程详情查询接口

添加返回值,返回当前显示详情的课程是否已被购买

//2 课程详情的方法
@GetMapping("getFrontCourseInfo/{courseId}")
public R getFrontCourseInfo(@PathVariable String courseId, HttpServletRequest request) {
    //根据课程id,编写sql语句查询课程信息
    CourseWebVo courseWebVo = courseService.getBaseCourseInfo(courseId);
    //根据课程id查询章节和小节
    List<ChapterVo> chapterVideoList = chapterService.getChapterVideoByCourseId(courseId);
    //根据课程id和用户id查询当前课程是否已经支付过了
    boolean buyCourse = ordersClient.isBuyCourse(courseId, JwtUtils.getMemberIdByJwtToken(request));
    return R.ok().data("courseWebVo",courseWebVo).data("chapterVideoList",chapterVideoList).data("isBuy",buyCourse);
}

统计分析模块

  1. 统计项目中每天有多少注册人数
  2. 将其用图表显示出来

准备工作

创建表,用于存储统计数据

统计某一天注册人数

查询用户表ucenter_member,得到需要的数据

SELECT COUNT(*) FROM ucenter_member uc WHERE DATE(uc.gmt_create)='2020-03-09'

最终实现过程

生成统计数据

图表显示数据

把统计分析表中存储的数据,使用图表显示

定时任务

定时任务,就是特定时候自动执行程序

类似闹钟

  1. 在启动类添加注解

    @EnableScheduling
    public class StaApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(StaApplication.class, args);
        }
    }
  2. 创建定时任务类,在类中利用 cron 表达式设置执行规则

    @Component
    public class ScheduledTask {
    
        @Autowired
        private StatisticsDailyService staService;
    
        // 0/5 * * * * ?表示每隔5秒执行一次这个方法
        @Scheduled(cron = "0/5 * * * * ?")
        public void task1() {
            System.out.println("**************task1执行了..");
        }
    
        //在每天凌晨1点,把前一天数据进行数据查询添加
        @Scheduled(cron = "0 0 1 * * ?")
        public void task2() {
            staService.registerCount(DateUtil.formatDate(DateUtil.addDays(new Date(), -1)));
        }
    }

Day 17

  1. canal 数据同步工具
  2. SpringCloud 组件—— Gateway 网关
  3. 权限管理模块
    • 权限管理需求
    • 权限管理接口

canal 数据同步工具

canal 是阿里巴巴的一个开源项目,基于 Java 开发,支持 MySQL 数据库

应用场景

把远程库内容同步到本地库

canal 同步过程

  • linux 系统

    • 安装 MySQL 数据库

      创建数据库和数据表

    • 安装 canal 数据同步工具

  • 本地 windows 系统

    • 创建数据库和数据表
修改 linux 系统 MySQL 数据库配置

检查 binlog 功能是否开启

show variables like 'log_bin'

若 Value 为 OFF,表示未开启

若 Value 为 ON,表示已开启

修改 MySQL 配置文件 my.cnf

vi /etc/my.cnf

追加内容

log-bin=mysql-bin
binlog_format=ROW
server_id=1

重启 MySQL 数据库

重启后再去查看 binlog,若变为 ON,则已经开启

安装 canal 工具
  1. 把 canal 压缩文件上传到 linux 系统

  2. 解压缩

  3. 修改 canal 配置文件

    vi conf/example/instance.properties
    # 需要改成自己的数据库信息
    canal.instance.master.address=
    canal.instance.dbUsername=
    canal.instance.dbPassword=
    canal.instance.filter.regex=
  4. 启动 canal

canal 代码编写

  1. 创建模块 canal_clientedu
  2. 在该模块中引入依赖
  3. 创建 application.properties 配置文件
  4. 创建 canal 客户端类 CanalClient,执行启动类 CanalApplication

Gateway 网关

什么是网关

在客户端和服务端中间隔一面墙,其作用:请求转发、负载均衡、权限控制等

image-20211103163338962

Gateway

image-20211103163816506

Gateway 具体使用

  1. 创建微服务模块 api_gateway

  2. 在模块中引入相关依赖

  3. 创建启动类和配置文件,在配置文件中配置网关规则

    @SpringBootApplication
    @EnableDiscoveryClient
    public class ApiGatewayApplication {
        public static void main(String[] args) {
            SpringApplication.run(ApiGatewayApplication.class, args);
        }
    }
    # 服务端口
    server.port=8222
    # 服务名
    spring.application.name=service-gateway
    # nacos服务地址
    spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
    
    #使用服务发现路由
    spring.cloud.gateway.discovery.locator.enabled=true
    
    #设置路由id
    spring.cloud.gateway.routes[0].id=service-acl
    #设置路由的uri   lb://nacos注册服务名称
    spring.cloud.gateway.routes[0].uri=lb://service-acl
    #设置路由断言,代理servicerId为auth-service的/auth/路径
    spring.cloud.gateway.routes[0].predicates= Path=/*/acl/**
    
    #配置service-edu服务
    spring.cloud.gateway.routes[1].id=service-edu
    spring.cloud.gateway.routes[1].uri=lb://service-edu
    spring.cloud.gateway.routes[1].predicates= Path=/eduservice/**
    
    #配置service-edu服务
    spring.cloud.gateway.routes[2].id=service-msm
    spring.cloud.gateway.routes[2].uri=lb://service-msm
    spring.cloud.gateway.routes[2].predicates= Path=/edumsm/**

Gateway 负载均衡

image-20211103164930033

权限管理模块

权限管理需求

image-20211103165131861

后端接口整合

  1. 创建子模块,在 service 里创建 service_acl

  2. 该模块引入依赖

    <dependencies>
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>spring_security</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
    </dependencies>
  3. 引入service_acl 模块的核心代码,包含 application.properties

  4. common 模块中引入 spring_security 代码,包含 pom.xml

递归查询菜单

image-20211104150550276

//========================递归查询所有菜单================================================
//获取全部菜单
@Override
public List<Permission> queryAllMenuGuli() {
    //1 查询菜单表所有数据
    QueryWrapper<Permission> wrapper = new QueryWrapper<>();
    wrapper.orderByDesc("id");
    List<Permission> permissionList = baseMapper.selectList(wrapper);
    //2 把查询所有菜单list集合按照要求进行封装
    List<Permission> resultList = bulidPermission(permissionList);
    return resultList;
}

//把返回所有菜单list集合进行封装的方法
public static List<Permission> bulidPermission(List<Permission> permissionList) {

    //创建list集合,用于数据最终封装
    List<Permission> finalNode = new ArrayList<>();
    //把所有菜单list集合遍历,得到顶层菜单 pid=0菜单,设置level是1
    for(Permission permissionNode : permissionList) {
        //得到顶层菜单 pid=0菜单
        if("0".equals(permissionNode.getPid())) {
            //设置顶层菜单的level是1
            permissionNode.setLevel(1);
            //根据顶层菜单,向里面进行查询子菜单,封装到finalNode里面
            finalNode.add(selectChildren(permissionNode,permissionList));
        }
    }
    return finalNode;
}

private static Permission selectChildren(Permission permissionNode, List<Permission> permissionList) {
    //1 因为向一层菜单里面放二层菜单,二层里面还要放三层,把对象初始化
    permissionNode.setChildren(new ArrayList<Permission>());

    //2 遍历所有菜单list集合,进行判断比较,比较id和pid值是否相同
    for(Permission it : permissionList) {
        //判断 id和pid值是否相同
        if(permissionNode.getId().equals(it.getPid())) {
            //把父菜单的level值+1
            int level = permissionNode.getLevel()+1;
            it.setLevel(level);
            //如果children为空,进行初始化操作
            if(permissionNode.getChildren() == null) {
                permissionNode.setChildren(new ArrayList<Permission>());
            }
            //把查询出来的子菜单放到父菜单里面
            permissionNode.getChildren().add(selectChildren(it,permissionList));
        }
    }
    return permissionNode;
}

递归删除菜单

//============递归删除菜单==================================
@Override
public void removeChildByIdGuli(String id) {
    //1 创建list集合,用于封装所有删除菜单id值
    List<String> idList = new ArrayList<>();
    //2 向idList集合设置删除菜单id
    this.selectPermissionChildById(id,idList);
    //把当前id封装到list里面
    idList.add(id);
    baseMapper.deleteBatchIds(idList);
}

//2 根据当前菜单id,查询菜单里面子菜单id,封装到list集合
private void selectPermissionChildById(String id, List<String> idList) {
    //查询菜单里面子菜单id
    QueryWrapper<Permission>  wrapper = new QueryWrapper<>();
    wrapper.eq("pid",id);
    wrapper.select("id");
    List<Permission> childIdList = baseMapper.selectList(wrapper);
    //把childIdList里面菜单id值获取出来,封装idList里面,做递归查询
    childIdList.stream().forEach(item -> {
        //封装idList里面
        idList.add(item.getId());
        //递归查询
        this.selectPermissionChildById(item.getId(),idList);
    });
}

角色分配菜单

//=========================给角色分配菜单=======================
@Override
public void saveRolePermissionRealtionShipGuli(String roleId, String[] permissionIds) {
    //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);
    }
    //添加到角色菜单关系表
    rolePermissionService.saveBatch(rolePermissionList);
}

Day 18

  1. 整合 Spring Security 权限框架
  2. Nacos 配置中心
  3. 提交项目到远程 Git 仓库( Gitee / GitHub )

Spring Security

框架介绍

Spring Security 主要包含两部分:用户认证用户授权

  • 用户认证

    用户进行登录时,输入用户名及密码,后端查询数据库,验证用户名和密码是否正确,若正确则认证成功

  • 用户授权

    用户登录系统后,该登录用户可能是不同的角色,若为管理员,则具有所有操作功能,若为普通用户,则只拥有部分操作功能

Spring Security 本质上就是过滤器(Filter),对请求进行过滤

授权过程

image-20211104151234053

代码执行过程

image-20211104151339393

Nacos 配置中心

配置中心的作用

image-20211104151432726

Spring Boot 配置文件

配置文件的加载顺序

  1. bootstrap.properties

  2. application.properties

    • spring.profiles.active=dev
      
  3. application-dev.properties

Day 19

持续化部署工具 jenkins

  • 手动打包运行
  • 自动打包运行

Day 20 总结

  1. 总结项目功能
  2. 总结项目技术点
  3. 总结项目问题

项目描述

对项目总体介绍

在线教育项目采用 B2C 商业模块,使用微服务架构,项目采用前后端分离开发

项目功能模块

在线教育项目分为前台系统和后台系统

  • 前台系统
    • 首页数据显示
    • 课程列表和详情
    • 课程支付
    • 课程视频播放
    • 微信登录
    • 微信支付
  • 后台系统
    • 权限管理
    • 课程管理
    • 统计分析
    • 课程分类管理

项目涉及技术

项目采用前后端分离开发

  • 前端技术
    • vue
    • element-ui
    • nuxt
    • babel
  • 后端技术
    • Spring Boot
    • Spring Cloud
    • EasyExcel
  • 第三方技术
    • 阿里云 OSS
    • 视频点播
    • 短信服务

总结项目功能

准备

  1. 启动后端接口
  2. 启动前端项目(前台系统和后台系统)

项目后台管理系统功能

登录功能

Spring Security 框架

权限管理模块
  1. 菜单管理

    • 列表总览、添加、修改、删除
  2. 角色管理

    • 列表总览、添加、修改、删除(批量删除)

    • 为角色分配菜单

  3. 用户管理

    • 列表总览、添加、修改、删除(批量删除)

    • 为用户分配角色

  4. 权限管理表和关系

    • 使用五张表

      acl_permission

      acl_role

      acl_role_permission

      acl_user

      acl_user_role

讲师管理模块

条件查询分页列表、添加、修改、删除

课程分类模块
  1. 添加课程分类
    • 读取 Excel 课程分类数据,并添加到数据库中
  2. 课程分类列表
    • 使用树形结构显示课程分类列表
课程管理模块
  1. 课程列表功能

  2. 添加课程

    • 课程发布流程:

      第一步填写课程基本信息

      第二步添加课程大纲(章节和小节)

      第三步确认课程信息,最终课程发布

    • 如何判断课程是否已被发布

      使用 status 字段

    • 添加课程时,中途停止添加,重新添加另一课程,如何找到之前未发布的课程,继续发布?

      根据课程状态,到课程列表中查询未发布的课程,点击课程右边超链接,继续发布课程

  3. 添加小节,上传课程视频

统计分析模块
  1. 生成统计数据
  2. 统计数据图表显示

项目前台用户系统功能

首页数据显示
  1. 显示幻灯片
  2. 显示热门课程
  3. 显示名师
注册功能

获取手机验证码

登录功能
  1. 输入用户名和密码登录(一般登录)

    • SSO (单点登录)

      单点登录常见方式:

      • Session 广播机制
      • 使用 Cookie + Redis
      • 使用 token
    • JWT

      使用 JWT 生成 token 字符串

      JWT 由三部分组成

    • 登录实现流程

      调用登录接口,返回 token,添加到 Cookie

      创建前端拦截器进行判断,若 Cookie 中包含 token,把 token 添加到 Header,调用接口,根据 token 获取用户信息,添加到 Cookie,显示用户信息

  2. 微信扫描登录

    • OAuth 2

      用于解决两个问题:开放系统间授权和分布式访问

    • 如何获取扫描人信息?

      扫描后调用微信接口,返回 code (临时票据),请求微信 URL,得到 access_token (访问凭证)和 openid (微信唯一标识),再次请求微信 URL,得到扫描人信息(昵称、头像等)

名师列表功能
名师详情功能
课程列表功能

条件分页查询

课程详情页
  1. 课程信息显示(包括课程概要、分类、讲师、课程大纲)
  2. 判断课程是否需要购买
课程视频在线播放
课程支付功能(微信支付)
  1. 生成课程订单
  2. 生成微信支付二维码
  3. 微信支付

微信支付实现流程

若课程是收费课程,点击立即购买,生成订单,点击订单进行支付,生成微信二维码,扫描二维码实现支付

支付后,每隔 3 秒查询支付状态(是否支付成功),若未支付则继续等待,若支付成功,则更新订单状态,向支付记录表添加支付成功记录

总结项目技术点(前端)

vue

  • 基本语法

  • 常见指令:v-bind v-model v-if v-for v-html

  • 绑定事件:v-on-click @click

  • 生命周期:created() 页面渲染前 mounted() 页面渲染后

  • ES6 规范

Element-ui

nodejs

nodejs 是 JavaScript 运行环境,无需浏览器就可直接运行 js 代码,模拟服务器效果

NPM

  • 包管理工具,类似 Maven
  • npm命令:npm init npm install xxxx

Babel

转码器,可以把 ES6 代码转换成 ES5 代码

前端模块化

通过一个页面或一个 js 文件,调用另一个 js 文件里的方法

出现的问题:ES6 的模块化无法在 Node.js 中执行,需要用 Babel 编辑成 ES5 后再执行

后台系统

使用 vue-admin-template,基于 vue + Element-ui

前台系统

使用 Nuxt,基于 vue

服务器渲染技术

Echarts

图表工具

总结项目技术点(后端)

项目采用微服务架构

SpringBoot

  1. SpringBoot 本质就是 Spring,只是快速构建 Spring 工程的脚手架
  2. 细节
    • 启动类包扫描机制
    • 设置扫描规则 @ComponentScan("包路径")
    • 配置类
  3. SpringBoot 配置文件
    • 配置文件类型:properties 和 yml
    • 配置文件加载顺序:bootstrap application applicaiton-dev

SpringCloud

  1. 是很多框架的总称,使用这些框架可以实现微服务架构

  2. 框架组成

    • 服务发现 Netflix Eureka Nacos
    • 服务调用 Netflix Feign
    • 熔断器 Netflix Hystrix
    • 服务网关 Spring Cloud Gateway
    • 分布式配置 Spring Cloud Config Nacos
    • 消息总线 Spring Cloud Bus Nacos
  3. 项目中使用 Alibaba Nacos 替代 SpringCloud 一些组件

    • Nacos 作为注册中心
    • Nacos 作为配置中心
  4. Feign

    服务调用,一个微服务调用另一个微服务,即远程调用

  5. 熔断器

  6. Gateway 网关

    SpringCloud 之前使用 zuul 网关,目前使用 Gateway 网关

MyBatisPlus

  1. MyBatisPlus 就是 MyBatis 增强版
  2. 自动填充
  3. 乐观锁
  4. 逻辑删除
  5. 代码生成器

EasyExcel

  1. 阿里巴巴提供的操作 Excel 工具,代码简洁且效率高
  2. 对 POI 进行封装,采用 SAX 方式解析
  3. 项目中应用场景:添加课程分类,读取 Excel 数据

SpringSecurity

  1. 在项目整合框架实现权限管理功能
  2. SpringSecurity 框架组成:认证和授权
  3. SpringSecurity 认证过程
  4. SpringSecurity 代码执行过程

Redis

  1. 首页数据通过 Redis 进行缓存
  2. Redis 数据类型
  3. Redis 缓存适用于不太重要或不经常更改的数据

Nginx

  1. 反向代理服务器
  2. 请求转发,负载均衡,动静分离

OAuth2 + JWT

  1. OAuth2 是一套特定问题的解决方案
  2. JWT 三个组成部分

HttpClient

  1. 发送请求返回响应的工具,无需浏览器完成请求和响应的过程
  2. 应用场景:微信登录获取扫描人信息,微信支付查询支付状态
  1. 客户端技术
  2. 每次发送请求携带 Cookie
  3. Cookie 有默认会话级别,一般关闭浏览器 Cookie 则消失
  4. Cookie 可以设置有效时长 setMaxAge()

参考另一篇文章 Cookie 和 Session

微信支付

阿里云 OSS

  1. 文件存储服务器
  2. 应用场景:添加讲师时上传讲师头像

阿里云视频点播

  1. 视频上传、播放、删除
  2. 整合阿里云视频播放器

阿里云短信服务

注册时发送手机验证码

Git

代码提交到远程 GIt 仓库

Docker + Jenkins

  1. 手动打包运行
  2. IDEA 打包
  3. Jenkins 自动化部署

总结项目问题

前端:路由切换问题

多次路由跳转到同一 vue 界面,页面 created() 方法只会执行一次

解决:使用 vue 监听

前端:ES6 模块化运行问题

Nodejs 不能直接运行 ES6 模块化代码,需要使用 Babel 把 ES6 代码转换为 ES5 代码执行

MyBatisPlus 生成19位 id 值问题

MyBatisPlus 生成 id 值为 19 位,JavaScript 处理数字类型时,只会处理到 16 位

跨域问题

访问协议、ip地址、端口号,三者有任一不一样,就会产生跨域问题

解决:

  1. 在 Controller 添加注解 @CrossOrigin
  2. 通过网关解决

413 问题

上传视频时,由于 Nginx 有上传文件大小限制,若超过其大小,则会出现 413 问题

413:请求体过大

解决:

在 Nginx 配置文件中设置客户端大小

Maven 加载问题

Maven 加载项目时,默认不会加载 src/java 中的 .xml 类型文件

解决:

  1. 直接复制 xml 文件到 target 目录(不推荐)
  2. 通过配置实现