Day1 本次项目实现的是一个在线教育平台
商业模式 B2C模式 (Business To Customer 会员模式)
商家到用户,这种模式是自己制作大量自有版权的视频,放在自有平台上,让用户按月付费或者按年付费。 这种模式简单,快速,只要专心录制大量视频即可快速发展,其曾因为 lynda 的天价融资而大热。
但在中国由于版权保护意识不强,教育内容易于复制,有海量的免费资源的竞争对手众多等原因,难以取得像样的现金流。
这种商业模式就是本次项目所使用的模式。 
谷粒学院 http://www.gulixueyuan.com/ 
B2B2C模式 平台链接第三方教育机构和用户,平台一般不直接提供课程内容,而是更多承担教育的互联网载体角色,为教学过程各个环节提供全方位支持和服务。
或者来说,举个例子,京东里面有京东自营店,还有其他个体卖家。
腾讯课堂 https://ke.qq.com/ 
谷粒学院功能简介 谷粒学院,是一个B2C模式的职业技能在线教育 系统,分为前台用户系统 和后台运营平台 。
Mybatis Plus 知识点总结
配置环境 配置properties文件(根据自己的需要改)
数据库环境看着配就行
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 =xth 
 
创建实体类 根据数据库中的字段一一对应着写就行,需要注意的是:MP自动会把下划线命名法转换为驼峰命名法 ,不需要再做额外的配置,也可以根据有关注解与数据库中的字段相照应,但是最好不要这样做
创建Mapper 
注意这里我没有加@Mapper注解,为了方便起见,最好还是用@MapperScan注解,二者其实并没有什么区别
下面是@MapperScan注解的位置,可以扫描整个包真的很省事
配置日志 在properties文件中加入这个就开启日志了
mybatis-plus.configuration.log-impl =org.apache.ibatis.logging.stdout.StdOutImpl 
 
主键新增策略 你可能会问,为什么MP知道哪个是主键字段?
情况一:
如果数据库中主键名称叫id,实体类中的属性名也叫id,mp默认认为字段名为id的是主键。
 
情况二:
如果数据库中主键名称叫uid,实体类中的属性名叫id,需要使用注解@TableId。
 
 
拉回正轨,咳咳,我们这个讨论的是MP的主键新增策略
一共有下面
六个策略,我简单的总结一下
 
 
 
 
AUTO 
自动增长(需要注意的是数据库中主键字段也需要设置为自动增长,要不然会报错) 
 
NONE 
不设置MP对于主键的自动生成策略 
 
INPUT 
通过用户自动设置 
 
ID_WORKER 
对于Long类型主键,MP默认的自动生成策略 
 
UUID 
UUID生成策略,之前一直用的 
 
ID_WORKER_STR 
对于String类型主键,用的这个 
 
自动填充 之前做过的两个项目都有自动填充功能,下面总结一下具体实现。
在实体类的属性上加入注解 
 
@TableField(fill = FieldFill.INSERT) private  Date createTime;@TableField(fill = FieldFill.INSERT_UPDATE) private  Date updateTime;
 
实现元对象处理器接口 
 
注意:要在类上加上@Component注解,加入springIOC容器中
代码很简单,在里面加入处理逻辑就好了
@Component public  class  MyMetaObjectHandler  implements  MetaObjectHandler  {    @Override      public  void  insertFill (MetaObject metaObject)  {                  this .setFieldValByName("createTime" , new  Date (), metaObject);         this .setFieldValByName("updateTime" , new  Date (), metaObject);     }     @Override      public  void  updateFill (MetaObject metaObject)  {         this .setFieldValByName("updateTime" , new  Date (), metaObject);     } } 
 
完成,之后再添加或更新就可以自动更新/添加字段了 
 
乐观锁 现在都不咋用悲观锁了,但还是介绍一下把
悲观锁:当你更新这条记录的时候,其他人都不能更新这条数据
乐观锁:当要更新一条记录的时候,记下version的值,之后修改过就+1,如果最后得到的值确实是version+1。则说明实现线程安全的数据更新。
我们主要再MP中实现乐观锁
乐观锁实现方式: 
取出记录时,获取当前version 
更新时,带上这个version 
执行更新时, set version = newVersion where version = oldVersion 
如果version不对,就更新失败 
 
具体步骤:
在数据库中添加version字段,并且设置插入时默认值为1 
 
在实体类中加入version属性和@Version注解 
 
@Version private  Integer version;
 
配置乐观锁插件 
 
@Configuration @MapperScan("com.atguigu.mpdemo.mapper") public  class  MybatisPlusConfig  {         @Bean      public  OptimisticLockerInterceptor optimisticLockerInterceptor ()  {         return  new  OptimisticLockerInterceptor ();     } } 
 
完成,可以测试(先查询再修改) 
 
分页插件 之前就用过了,简单总结一下
在MybatisPlusConfig类中配置分页插件 
 
@Bean public  PaginationInterceptor paginationInterceptor ()  {    return  new  PaginationInterceptor (); } 
 
编写查询分页代码 
 
@Test     void testDemo5(){         Page<User> page = new Page<User>(1,5);         userMapper.selectPage(page, null);         page.getRecords().forEach(System.out::println);         System.out.println(page.getCurrent());         System.out.println(page.getPages());         System.out.println(page.getSize());         System.out.println(page.getTotal());         System.out.println(page.hasNext());         System.out.println(page.hasPrevious());     } 
 
具体的属性可以去mp官网查,用到哪个找哪个
逻辑删除 比如一个员工离职,我们不想删除他的信息,我们可以给他标志位离职
在MP中有相应的插件可以帮我们完成逻辑删除
在数据库中添加一个deleted字段,默认值为0(修改值为1说明被删除了) 
 
在实体类中加入字段,并加上逻辑删除的@TableLogic注解 
 
在MybatisPlusConfig类中配置逻辑删除插件 
 
@Bean public  ISqlInjector sqlInjector ()  {    return  new  LogicSqlInjector (); } 
 
完成,可以测试了,执行删除代码,最终效果deleted值变成1,之后不管是查询全部还是其他查询,都会在后面加上一个条件where deleted = 0 
 
性能分析插件 这个插件用于在你执行sql的时候,会在控制台显示执行的时间,你可以设置超过多少s的sql不执行。
在MybatisPlusConfig类中配置性能分析插件 
 
@Bean @Profile({"dev","test"}) public  PerformanceInterceptor performanceInterceptor ()  {    PerformanceInterceptor  performanceInterceptor  =  new  PerformanceInterceptor ();     performanceInterceptor.setMaxTime(100 );     performanceInterceptor.setFormat(true );     return  performanceInterceptor; } 
 
在配置文件中加入现在是什么环境下的 
 
spring.profiles.active =test 
 
Day2 搭建项目结构 
模块说明 
guli-parent :在线教学根目录(父工程),管理四个子模块:
 canal-client :canal 数据库表同步模块(统计同步数据)
 common :公共模块父节点
 common-util:工具类模块,所有模块都可以依赖于它
 service-base:service服务的base包,包含service服务的公共配置类,所有service模块依赖于它
 spring-security:认证与授权模块,需要认证授权的service服务依赖于它
infrastructure :基础服务模块父节点
 api-gateway:api网关服务
 service :api 接口服务父节点
service-acl:用户权限管理api接口服务(用户管理、角色管理和权限管理等)
service-cms:cms api接口服务
service-edu:教学相关api接口服务
service-msm:短信api接口服务
service-order:订单相关api接口服务
service-oss:阿里云oss api接口服务
service-statistics:统计报表api接口服务
service-ucenter:会员api接口服务
service-vod:视频点播api接口服务
 
创建代码结构并配置依赖
(这里我配置依赖配置了好久,因为有的依赖下载不下来,但是我看在网上搜索了一下,在响应的依赖下加入相应的版本号就可以了)
10.2再寄,依赖冲突了,我再试试
最终还是导入了老师的仓库和源码,不得不说,配环境这个问题真的是很浪费时间
配置配置文件 根据自己的配置文件修改
server.port =8001 spring.application.name =service-edu spring.profiles.active =dev spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8 spring.datasource.username =root spring.datasource.password =xth mybatis-plus.configuration.log-impl =org.apache.ibatis.logging.stdout.StdOutImpl 
 
MP代码生成器生成大致框架 直接用就行,不过这部分代码放在测试中就可以了,只是一个生成的工具
public  class  CodeGenerator  {    @Test      public  void  run ()  {                  AutoGenerator  mpg  =  new  AutoGenerator ();                  GlobalConfig  gc  =  new  GlobalConfig ();         String  projectPath  =  System.getProperty("user.dir" );                  gc.setOutputDir(projectPath + "/src/main/java" );                  gc.setAuthor("昊昊" );         gc.setOpen(false );          gc.setFileOverride(false );          gc.setServiceName("%sService" );	         gc.setIdType(IdType.ID_WORKER);          gc.setDateType(DateType.ONLY_DATE);         gc.setSwagger2(true );         mpg.setGlobalConfig(gc);                  DataSourceConfig  dsc  =  new  DataSourceConfig ();         dsc.setUrl("jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8" );         dsc.setDriverName("com.mysql.cj.jdbc.Driver" );         dsc.setUsername("root" );         dsc.setPassword("xth" );         dsc.setDbType(DbType.MYSQL);         mpg.setDataSource(dsc);                                    PackageConfig  pc  =  new  PackageConfig ();         pc.setModuleName("eduservice" );          pc.setParent("com.atguigu" );         pc.setController("controller" );         pc.setEntity("entity" );         pc.setService("service" );         pc.setMapper("mapper" );         mpg.setPackageInfo(pc);                  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 );          strategy.setRestControllerStyle(true );          strategy.setControllerMappingHyphenStyle(true );          mpg.setStrategy(strategy);                  mpg.execute();     } } 
 
统一返回json时间的格式 默认情况下json时间格式带有时区,并且是世界标准时间,和我们的时间差了八个小时
在application.properties中设置
spring.jackson.date-format =yyyy-MM-dd HH:mm:ss spring.jackson.time-zone =GMT+8 
 
在service中使用swagger 创建以下工程
@Configuration @EnableSwagger2  public  class  SwaggerConfig  {    @Bean      public  Docket webApiConfig () {         return  new  Docket (DocumentationType.SWAGGER_2)                 .groupName("haohaoAPI" )                 .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 ("瑶瑶昊昊" , "http://atguigu.com" , "1499487526@qq.com" ))                 .build();     } } 
 
之后我们再在server中引入service_base模块
把pom文件中的bean交给spring管理的方法 比如说我们之前引入了service_base模块,但是如果不把config文件交给spring管理是加载不上的,所以可以这样
Springboot默认只会扫描启动类所在包极其子包下的带有@Component注解的类,但是如果超出这个范围就扫描不到了
下面配置的是既能扫描我们的包下的,又能扫描引入依赖那个包下面的 
统一返回结果 项目中我们会将响应封装成json返回,一般我们会将所有接口的数据格式统一, 使前端(iOS Android,Web)对数据的操作更一致、轻松。
一般情况下,统一返回数据格式没有固定的格式,只要能描述清楚返回的数据状态以及要返回的具体数据就可以。但是一般会包含状态码、返回消息、数据这几部分内容
例如,我们的系统要求返回的基本数据格式如下:
列表:
{     "success" :  true ,      "code" :  20000 ,      "message" :  "成功" ,      "data" :  {          "items" :  [              {                  "id" :  "1" ,                  "name" :  "刘德华" ,                  "intro" :  "毕业于师范大学数学系,热爱教育事业,执教数学思维6年有余"              }          ]          ....     }  } 
 
分页:
{     "success" :  true ,      "code" :  20000 ,      "message" :  "成功" ,      "data" :  {          "total" :  17 ,          "rows" :  [              {                  "id" :  "1" ,                  "name" :  "刘德华" ,                  "intro" :  "毕业于师范大学数学系,热爱教育事业,执教数学思维6年有余"              }          ]          ....     }  } 
 
没有返回数据:
{     "success" :  true ,      "code" :  20000 ,      "message" :  "成功" ,      "data" :  { }  } 
 
失败:
{     "success" :  false ,      "code" :  20001 ,      "message" :  "失败" ,      "data" :  { }  } 
 
所以根据规律,定义统一结果
{     "success" :  布尔,       "code" :  数字,       "message" :  字符串,       "data" :  HashMap  } 
 
编码实现 
编写响应码 
 
public  interface  ResultCode  {    public  static  Integer  SUCCESS  =  20000 ;      public  static  Integer  ERROR  =  20001 ;  } 
 
编写R返回结果类 
 
package  com.atguigu.commonutils;import  io.swagger.annotations.ApiModelProperty;import  lombok.Data;import  java.util.HashMap;import  java.util.Map;@Data public  class  R  {    @ApiModelProperty(value = "是否成功")      private  Boolean success;     @ApiModelProperty(value = "返回码")      private  Integer code;     @ApiModelProperty(value = "返回消息")      private  String message;     @ApiModelProperty(value = "返回数据")      private  Map<String, Object> data = new  HashMap <String, Object>();          private  R ()  {}          public  static  R ok ()  {         R  r  =  new  R ();         r.setSuccess(true );         r.setCode(ResultCode.SUCCESS);         r.setMessage("成功" );         return  r;     }          public  static  R error ()  {         R  r  =  new  R ();         r.setSuccess(false );         r.setCode(ResultCode.ERROR);         r.setMessage("失败" );         return  r;     }          public  R success (Boolean success) {         this .setSuccess(success);         return  this ;     }     public  R message (String message) {         this .setMessage(message);         return  this ;     }     public  R code (Integer code) {         this .setCode(code);         return  this ;     }     public  R data (String key, Object value) {         this .data.put(key, value);         return  this ;     }     public  R data (Map<String, Object> map) {         this .setData(map);         return  this ;     } } 
 
在用到返回结果的模块引入依赖 
 
跨域配置 浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域 。前后端分离开发中,需要考虑ajax跨域的问题。
这里我们可以从服务端解决这个问题
配置 
在Controller类上添加注解
 
条件查询分页 这个业务是  
根据讲师名称name,讲师头衔level、讲师入驻时间gmt_create(时间段)查询 
之后再分页
开发规范中,通常把需要传属性多的请求封装成一个实体类,叫vo ,在这个业务中,我们封装成TeacherQuery
 \
那么这个类怎么写呢,答案是根据业务写
业务大概是这样
对着,然后改成代码
@Data public  class  TeacherQuery  {    @ApiModelProperty(value = "教师名称,模糊查询" ,example = "张三")      private  String name;     @ApiModelProperty(value = "头衔 1高级讲师 2首席讲师" ,example = "1")      private  Integer level;     @ApiModelProperty(value = "查询开始时间", example = "2019-01-01 10:10:10")      private  String begin;     @ApiModelProperty(value = "查询结束时间", example = "2019-12-01 10:10:10")      private  String end; } 
 
需要注意的是
这个红框里面的东西,是和上面的实体类中的swagger注解的example属性相对应的
@RequestBody
需要使用post提交方式,用其他方式是取不到的
只能发送POST请求,GET方式无请求体
用法是:使用json传递数据,把json数据封装到对应对象里面
完善代码 
    @PostMapping("pageTeacherCondition/{current}/{limit}")      public  R pageTeacherCondition (@PathVariable  long  current,@PathVariable  long  limit,                                    @RequestBody(required = false)   TeacherQuery teacherQuery)  {                 Page<EduTeacher> pageTeacher = new  Page <>(current,limit);                  LambdaQueryWrapper<EduTeacher> wrapper = new  LambdaQueryWrapper <EduTeacher>();                           String  name  =  teacherQuery.getName();         Integer  level  =  teacherQuery.getLevel();         String  begin  =  teacherQuery.getBegin();         String  end  =  teacherQuery.getEnd();         wrapper.like(name!=null ,EduTeacher::getName,name);         wrapper.eq(level!=null ,EduTeacher::getLevel,level);         wrapper.ge(begin!=null ,EduTeacher::getGmtCreate,begin);         wrapper.le(end!=null ,EduTeacher::getGmtCreate,end);                  teacherService.page(pageTeacher,wrapper);         long  total  =  pageTeacher.getTotal();         List<EduTeacher> records = pageTeacher.getRecords();          return  R.ok().data("total" ,total).data("rows" ,records);     } 
 
统一异常处理 @ControllerAdvice public  class  GlobalExceptionHandler  {         @ExceptionHandler(Exception.class)      @ResponseBody       public  R error (Exception e)  {         e.printStackTrace();         return  R.error().message("执行了全局异常处理.." );     }          @ExceptionHandler(ArithmeticException.class)      @ResponseBody      public  R arithmeticExceptionError (ArithmeticException e) {         e.printStackTrace();         return  R.error().message("执行了特定异常处理" );     } } 
 
再在需要用到全局异常处理的地方引入依赖就可以了
基本原理是,如果有特定的异常处理类,就走特定,没有特定就走全局异常
统一日志处理 配置日志级别 日志记录器(Logger)的行为是分等级的。如下表所示:
分为:OFF、FATAL、ERROR 、WARN 、INFO 、DEBUG 、ALL
默认情况下,spring boot从控制台打印出来的日志级别只有INFO及以上级别,可以配置日志级别
 
把日志输出到文件中 
删除配置文件中的关于日志的配置删除 
 
resources 中创建 logback-spring.xml 
 
<?xml version="1.0"  encoding="UTF-8" ?> <configuration  scan ="true"  scanPeriod ="10 seconds" >                         <contextName > logback</contextName >           <property  name ="log.path"  value ="D:/All项目/谷粒学院/log"  />                                         <property  name ="CONSOLE_LOG_PATTERN"                 value ="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level)  |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)" />         <appender  name ="CONSOLE"  class ="ch.qos.logback.core.ConsoleAppender" >                            <filter  class ="ch.qos.logback.classic.filter.ThresholdFilter" >  <level > INFO</level >          </filter >  <encoder >  <Pattern > ${CONSOLE_LOG_PATTERN}</Pattern >                   <charset > UTF-8</charset >      </encoder >      </appender >                <appender  name ="INFO_FILE"                 class ="ch.qos.logback.core.rolling.RollingFileAppender" >                  <file > ${log.path}/log_info.log</file >                   <encoder >  <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level             %logger{50} - %msg%n</pattern >  <charset > UTF-8</charset >          </encoder >                   <rollingPolicy                   class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" >                          <fileNamePattern > ${log.path}/info/log-info-%d{yyyy-MM�dd}.%i.log</fileNamePattern >  <timeBasedFileNamingAndTriggeringPolicy                   class ="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP" >  <maxFileSize > 100MB</maxFileSize >         </timeBasedFileNamingAndTriggeringPolicy >                           <maxHistory > 15</maxHistory >          </rollingPolicy >                   <filter  class ="ch.qos.logback.classic.filter.LevelFilter" >              <level > INFO</level >              <onMatch > ACCEPT</onMatch >              <onMismatch > DENY</onMismatch >          </filter >      </appender >           <appender  name ="WARN_FILE"                 class ="ch.qos.logback.core.rolling.RollingFileAppender" >                  <file > ${log.path}/log_warn.log</file >                   <encoder >  <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level             %logger{50} - %msg%n</pattern >  <charset > UTF-8</charset >           </encoder >                   <rollingPolicy                   class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" >  <fileNamePattern > ${log.path}/warn/log-warn-%d{yyyy-MM�dd}.%i.log</fileNamePattern >  <timeBasedFileNamingAndTriggeringPolicy                  class ="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP" >  <maxFileSize > 100MB</maxFileSize >         </timeBasedFileNamingAndTriggeringPolicy >                           <maxHistory > 15</maxHistory >          </rollingPolicy >                   <filter  class ="ch.qos.logback.classic.filter.LevelFilter" >  <level > warn</level >  <onMatch > ACCEPT</onMatch >  <onMismatch > DENY</onMismatch >          </filter >      </appender >           <appender  name ="ERROR_FILE"                 class ="ch.qos.logback.core.rolling.RollingFileAppender" >                  <file > ${log.path}/log_error.log</file >                   <encoder >  <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level             %logger{50} - %msg%n</pattern >  <charset > UTF-8</charset >           </encoder >                   <rollingPolicy                   class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" >  <fileNamePattern > ${log.path}/error/log-error-%d{yyyy-MM�dd}.%i.log</fileNamePattern >  <timeBasedFileNamingAndTriggeringPolicy                  class ="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP" >  <maxFileSize > 100MB</maxFileSize >         </timeBasedFileNamingAndTriggeringPolicy >                           <maxHistory > 15</maxHistory >          </rollingPolicy >                   <filter  class ="ch.qos.logback.classic.filter.LevelFilter" >  <level > ERROR</level >  <onMatch > ACCEPT</onMatch >  <onMismatch > DENY</onMismatch >          </filter >      </appender >                     <springProfile  name ="dev" >                   <logger  name ="com.guli"  level ="INFO"  />                   <root  level ="INFO" >  <appender-ref  ref ="CONSOLE"  />              <appender-ref  ref ="INFO_FILE"  />              <appender-ref  ref ="WARN_FILE"  />              <appender-ref  ref ="ERROR_FILE"  />          </root >      </springProfile >           <springProfile  name ="pro" >  <root  level ="INFO" >  <appender-ref  ref ="CONSOLE"  />          <appender-ref  ref ="DEBUG_FILE"  />          <appender-ref  ref ="INFO_FILE"  />          <appender-ref  ref ="ERROR_FILE"  />          <appender-ref  ref ="WARN_FILE"  />      </root >      </springProfile >  </configuration > 
 
里面需要注意的是,输出路径的格式,嘿嘿因为我踩过这个坑了
把错误的日志单独输出 加入这两个东西,就可以输出了
 
之后触发异常后就会输出错误信息到这里面啦(里面输出的错误信息可以修改哦)
 
Day3 在VSCode里面创建工作区 
先创建一个空文件夹 
用VSCode打开这个空文件夹 
点击 
保存文件就可以了 
运行 
 
ES6 ES6是一套标准,JavaScript是ES6的具体实现
基本语法 let声明变量 	{         var  a = 0          let  b = 1       }     console .log (a)      console .log (b)  
 
var  m = 1 var  m = 2 let  n = 3 let  n = 4 console .log (m) console .log (n) 
 
const声明常量(只读变量) const  PI  = "3.1415926" PI  = 3  const  MY_AGE  
 
解构赋值 解构赋值是对赋值运算符的扩展。
他是一种针对数组或者对象进行模式匹配,然后对其中的变量进行赋值。
在代码书写上简洁且易读,语义更加清晰明了;也方便了复杂对象中数据字段获取。
let  a = 1 , b = 2 , c = 3 console .log (a, b, c)let  [x, y, z] = [1 , 2 , 3 ]console .log (x, y, z)let  user = {name : 'Helen' , age : 18 }let  name1 = user.name let  age1 = user.age console .log (name1, age1)let  { name, age } = userconsole .log (name, age)
 
模板字符串 模板字符串相当于加强版的字符串,用反引号 `,除了作为普通字符串,还可以用来定义多行字符串,还可以在字符串中加入变量和表达式。
let  string1 = `Hey, can you stop angry now?` console .log (string1)
 
let  name = "Mike" let  age = 27 let  info = `My Name is ${name} ,I am ${age+1 }  years old next year.` console .log (info)
 
function  f ( ){ return  "have fun!"  } let  string2 = `Game start,${f()} ` console .log (string2); 
 
声明对象简写 const  age = 12 const  name = "Amy" const  person1 = {age : age, name : name}console .log (person1)const  person2 = {age, name}console .log (person2) 
 
定义方法简写 const  person1 = {    sayHi :function ( ){         console .log ("Hi" )     } } person1.sayHi (); const  person2 = {    sayHi ( ){         console .log ("Hi" )     } } person2.sayHi ()  
 
对象扩展运算符 拓展运算符(…)用于取出参数对象所有可遍历属性然后拷贝到当前对象
let  person1 = {name : "Amy" , age : 15 }let  person2 = { ...person1 }console .log (person2) 
 
let  age = {age : 15 }let  name = {name : "Amy" }let  person2 = {...age, ...name}console .log (person2) 
 
箭头函数 箭头函数提供了一种更加简洁的函数书写方式。基本语法是:
参数 => 函数体
var  f1 = function (a ){ 	return  a  } console .log (f1 (1 ))var  f2  = a => aconsole .log (f2 (1 ))
 
var  f3  = (a,b ) => { let  result = a+b  return  result } console .log (f3 (6 ,2 )) var  f4  = (a,b ) => a+b
 
箭头函数多用于匿名函数的定义
Vue Vue | 是小白菜哦 (xiaobaicai350.github.io) 
Element-UI element-ui 是饿了么前端出品的基于 Vue.js的 后台组件库,方便程序员进行页面快速布局和构建
官网: https://element-cn.eleme.io/#/zh-CN 
下次直接用这里面的组件开发程序
组件 | Element 
Node.js (1)之前学过java,运行java需要安装jdk环境
学习的这个nodejs,是JavaScript的运行环境 ,用于执行JavaScript代码环境不需要浏览器,直接使用nodejs运行JavaScript代码
(2)模拟服务器效果,比如tomcat
模拟服务器效果 直接拿来用就行,不需要自己写,知道有这个就行
创建demo02.js
const  http = require ('http' );http.createServer (function  (request, response ) {        response.writeHead (200 , {'Content-Type' : 'text/plain' });    response.end ('Hello Server' ); }).listen (8888 ); console .log ('Server running at http://127.0.0.1:8888/' );
 
访问http://127.0.0.1:8888/ 
在vscode使用nodejs 在vscode工具中打开cmd窗口,进行js代码执行
之后在终端中写代码
 
安装NPM NPM全称Node Package Manager,是Node.js包管理工具,是全球最大的模块生态系统,里面所有的模块都是开源免费的;也是Node.js的包管理工具,相当于**前端的Maven **
安装Node.js的时候自动就安装好了npm,下面是我的npm的地址
npm项目初始化操作 
使用命令npm init
之后一路回车,使用默认的就行。最后点击yes,会生成一个名叫package.json的文件,这个文件类似于maven的pom.xml文件
npm下载js依赖 命令npm install 依赖名称 
package-lock.json文件是锁定依赖的版本,只能用这个版本的依赖
根据json文件下载依赖 
只需要下面这个
在这个项目中(这里面就是npmdemo)的命令行中,再输入命令npm install,之后就会下载成功
babel转码器 babel可以把es6 的代码转换成es5 的代码
因为写的代码es6代码,但是es6代码浏览器兼容性很差,如果使用es5代码浏览器兼容性很好
编写es6代码,把es6代码转换es5运行
安装babel 初始化项目
 npm init -y
安装babel
npm install --global babel-cli
正常安装后
可以查看babel的版本
babel --version
如果没有正常安装
可以强制覆盖 安装
npm install --force --global babel-cli
查看版本可能会爆红
查不了版本号,以管理员 身份重启vscode,终端输入set-ExecutionPolicy RemoteSigned就OK了
创建js文件,编写es6代码 
在es6文件夹下创建js文件
 
配置.babelrc文件(这里面是es6转es5的配置文件)
{     "presets" : ["es2015" ],     "plugins" : [] } 
 
 
安装转换es5的插件npm install --save-dev babel-preset-es2015
 
有两种转换方式,一种是根据文件名单个转换,另一种是根据文件夹把文件夹里面的es6文件全部转换为es5
(1)babel es6/01.js -o dist/001.js 
(2)babel es6 -d dist
 
 
模块化操作 es5模块化操作 目录结构
01.js中的代码
const  sum=function (a,b ){    return  parseInt (a)+parseInt (b) } const  sub=function (a,b ){    return  parseInt (a)-parseInt (b) } module .exports ={    sum,     sub } 
 
02.js中的代码
const  m=require ('./01.js' )console .log (m.sum (1 ,2 ))console .log (m.sub (1 ,2 ))
 
输出结果
es6模块化操作 注意:如果使用es6写法实现模块化操作,在nodejs环境中不能直接运行的,需要使用babel把es6代码转换es5代码,才可以在nodejs进行运行
基本结构
es5的代码是根据es6生成的
第一种写法 es6的01.js的代码
export  function  sum (a,b ){   return  a+b } export  function  sub (a,b ){   return  a-b } 
 
02.js代码
import  {sum,sub} from  "./01.js" console .log (sum (1 ,2 ))console .log (sub (1 ,2 ))
 
之后转成es5的代码才能运行
第二种写法 01.js的代码
export  default  {    sum (a, b ) {         return  a + b     },     sub (a, b ) {         return  a - b     } } 
 
02.js的代码
import  m from  "./01.js" console .log (m.sum (1 ,2 ))console .log (m.sub (1 ,2 ))
 
第二种写法是对第一种写法的进一步简化
我们后面用的最多的当然是用的最简化的写法啦
 
Webpack Webpack 是一个前端资源加载/打包工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按
照指定的规则生成对应的静态资源。
==Webpack 可以将多种静态资源 js、css、less 转换成一个静态文件 ,减少了页面的请求==
安装Webpack npm install -g webpack webpack-cli
查看版本
webpack -v
使用Webpack打包js 一个小demo
基本结构 
记得要先npm init -y 哦
分别创建这个目录结构和文件
src下面的代码 
common.js
exports.info = function (str) {     document.write(str); } 
 
 
utils.js
exports.add = function (a, b) {     return a + b; } 
 
 
main.js
const common = require('./common'); const utils = require('./utils'); common.info('Hello world!' + utils.add(100, 200)); 
 
 
创建webpack的配置类 
文件名为webpack.config.js(里面可以修改参数)
const  path = require ("path" ); module .exports  = {    entry : './src/main.js' ,      output : {         path : path.resolve (__dirname, './输出' ),          filename : 'haohao的webpack输出.js'       } } 
 
在根目录下使用webpack指令
得到结果 
测试 
之后启动index.html就可以看到效果了
使用Webpack打包css Webpack 本身只能处理 JavaScript 模块,如果要处理其他类型的文件,就需要使用 loader 进行转换。
Loader 可以理解为是模块和资源的转换器。
首先我们需要安装相关Loader插件,css-loader 是将 css 装载到 javascript;style-loader 是让 javascript认识css
npm install --save-dev style-loader css-loader
修改webpack.config.js 
const  path = require ("path" ); module .exports  = {    entry : './src/main.js' ,      output : {         path : path.resolve (__dirname, './输出' ),          filename : 'haohao的webpack输出.js'       },               module : {         rules : [             {                 test : /\.css$/ ,                  use : ['style-loader' , 'css-loader' ]             }         ]     } } 
 
在src文件夹创建style.css 
 
修改main.js 
在第一行引入style.css
 
重新打包 
之后再查看index.html
使用模板启动项目 npm run dev
项目的创建和基本配置 创建项目 将vue-admin-template-master重命名为guli-admin
修改项目信息 package.json
{     "name" :  "guli-admin" ,      ......     "description" :  "谷粒学院后台管理系统" ,      "author" :  "Helen <55317332@qq.com>" ,      ...... } 
 
项目的目录结构 
├── build // 构建脚本
├── config // 全局配置
├── node_modules // 项目依赖模块
├── src //项目源代码
├── static // 静态资源
└── package.jspon // 项目信息和依赖配置
 
src的目录结构
src
├── api // 各种接口
├── assets // 图片等资源
├── components // 各种公共组件,非公共组件在各自view下维护
├── icons //svg icon
├── router // 路由表
├── store // 存储
├── styles // 各种样式
├── utils // 公共工具,非公共工具,在各自view下维护
├── views // 各种layout
├── App.vue //项目顶层组件 
├── main.js //项目入口文件 
└── permission.js //认证入口
 
运行项目 npm run dev
我太开心了 因为下modules包下了一下午都下不下来,出现了各种各样的错误,期间成功了一次,但是之后再启动又失败了,现在找到了问题所在
这两个版本得一致,并且下载的时候不要开代理!!!如果你正在开,先关掉,再执行下面两条指令
重置代理
 
npm config set proxy null
清除缓存
 
npm cache clean --force
之后再下载,就ok啦
npm install
当我看见这个,内心无比激动
看到这个我更激动了
对接上了后端,我要哭出来了
项目中需要注意的点 项目中不认识./
只认识@/
@/表示的含义是从src目录
多次路由跳转到同一页面时,created 方法只会被执行一次
项目中报403错误,错误原因可能是
1.跨域问题
2.路径写错了
 
前端页面 讲师列表前端实现 
<template >      <div  class ="app-container" >  <el-form  label-width ="120px" >  <el-form-item  label ="讲师名称" >  <el-input  v-model ="teacher.name" />      </el-form-item >  <el-form-item  label ="讲师排序" >  <el-input-number  v-model ="teacher.sort"  controls-position ="right"                                                                    min ="0" />     </el-form-item >  <el-form-item  label ="讲师头衔" >  <el-select  v-model ="teacher.level"  clearable  placeholder ="请选择" >           <el-option  :value ="1"  label ="高级讲师" />      <el-option  :value ="2"  label ="首席讲师" />      </el-select >      </el-form-item >       <el-form-item  label ="讲师资历" >  <el-input  v-model ="teacher.career" />      </el-form-item >  <el-form-item  label ="讲师简介" >  <el-input  v-model ="teacher.intro"  :rows ="10"  type ="textarea" />      </el-form-item >           <el-form-item >  <el-button  :disabled ="saveBtnDisabled"  type ="primary"                                 @click ="saveOrUpdate" > 保存</el-button >         </el-form     </el-form-item >  >         </div >  </template > 
 
import numpy as np from matplotlib.pyplot import*x=np. linspace(0,2*np.pi,200) y1=np.sin(x);y2=np.cos(pow(x,2))rc( 'font' ,size=16); plot(x,y1,'r ',label= ' $sin(x)$ ' ,linewidth=2)plot(x,y2,'b--',label='$cos(x^2)$ ') xlabel( '$x$ ' );ylabel( '$y$ ' ,rotation=0) savefig( 'figure2_38.png ' ,dpi=500);legend().show() 
 
EasyExcel 使用EasyExcel进行写操作 
引入依赖 
 
<dependencies >          <dependency >           <groupId > com.alibaba</groupId >           <artifactId > easyexcel</artifactId >           <version > 2.1.1</version >      </dependency >  </dependencies > 
 
还需要这个依赖
创建实体类,找到表头 
 
开始写入(getData方法是得到一个list表)
 
 
使用EasyExcel进行读操作 
创建实体类 
 
public  class  ReadData  {         @ExcelProperty(index = 0)      private  int  sid;                    @ExcelProperty(index = 1)      private  String sname; } 
 
创建读取操作的监听器
public  class  ExcelListener  extends  AnalysisEventListener <ReadData> {         List<ReadData> list = new  ArrayList <ReadData>();          @Override      public  void  invoke (ReadData user, AnalysisContext analysisContext)  {         System.out.println(user);         list.add(user);     }          @Override      public  void  invokeHeadMap (Map<Integer, String> headMap, AnalysisContext context)  {         System.out.println("表头信息:" +headMap);     }          @Override      public  void  doAfterAllAnalysed (AnalysisContext analysisContext)  {     } } 
 
 
调用实现最终的读取
 
 
public  static  void  main (String[] args)  throws  Exception {         String  fileName  =  "F:\\01.xlsx" ;               EasyExcel.read(fileName, ReadData.class, new  ExcelListener ()).sheet().doRead(); 
 
Day9 课程最终发布实现 昨天出现了一点小问题
问题的原因是因为Maven的默认加载机制:只会把src-main-java文件夹中的java类型文件进行加载,其他类型文件不会加载。(昨天我们在mapper的xml文件中写了sql语句,没有进行加载)
解决方式 
把 这个文件夹复制到target目录下的mapper文件夹下
 
通过配置文件进行配置,让maven默认加载xml文件
在guli_edu的pom中配置如下节点
  <build >        <resources >            <resource >                <directory > src/main/java</directory >                <includes >                    <include > **/*.xml</include >                </includes >                <filtering > false</filtering >            </resource >        </resources >    </build >  
 
 
在Spring Boot配置文件中添加配置
mybatis-plus.mapper-locations =classpath:com/guli/edu/mapper/xml/*.xml 
 
 
 
 
 
课程信息确认 这是那个三步走的最后一个阶段,查询出来最终的信息,并作显示,现在写前端的页面
写前端api调用发请求的部分
 
导入这个api的js文件
 
之后再data里面加入courseId并且在create方法里面给他赋值
 
定义方法并且在created方法中调用
 
数据回显,利用组件进行解析结果(elementUI)
 
 
课程发布 课程的最终发布
这个业务主要是修改的这个字段
将Draft(默认)改成Normal
编写后端代码
 
编写前端代码
编写api的js文件
之后调用这个方法并进行跳转
 
 
课程列表 课程列表显示 课程删除 编写后端代码。
因为要把课程里面的视频,小节,章节,描述,课程本身都删除,所以我们把删除的方法都封装到removeCourse方法中(但是我觉得可以用更好的方法,用外键的级联删除和级联更新就可以实现 具体可以看Mysql实现级联操作(级联更新、级联删除)_元宝321的博客-CSDN博客_mysql设置级联删除 )
之后就是在后端中,根据课程id进行删除视频..小结….之类的
阿里云视频点播服务 
服务端:后端接口
客户端:浏览器、安卓、ios
 
API:阿里云提供固定的地址,只需要调用这个固定的地址,向地址传递参数,就可以实现这个功能
SDK:SDK对API进行封装,更方便使用,比如之前使用的EasyExcel,可以调用阿里云提供的类和接口里面的方法进行实现相应的功能
添加小节实现视频上传 Day12 登录实现流程 SSO模式 single sign on
单点登录 
在任何一个模块登录之后其他模块都可以登录上去,不需要二次登录,这就是单点登录
常见的三种单点登录 第一种:
session广播机制实现 
基于session复制,但是现在已经不怎么使用这种方式,这种方式主要是用单台主机实现,现在都是分布式集群了
第二种:
使用cookie+redis实现 
在项目中任何一个模块进行登录,登录之后,把数据放到两个地方
redis:在key中,生成唯一随机值,在value中存储用户数据 
cookie:把redis里面生成的key值放到cookie里面 
 
当用户访问项目中的其他模块,发送请求会携带着cookie进行发送,当发送请求到服务器,会把cookie值到redis中进行查询,如果找到了对应的cookie值==redis中的key值,说明就是该用户
第三种:
使用token实现 
token是按照一定规则生成的字符串 (字符串中包含用户信息)
token还有一种叫法叫令牌,全称叫自包含令牌
具体步骤:
在项目中某个模块进行登录,登录之后,会按照规则生成字符串,把信息都存储到字符串中并编码加密,之后服务端把字符串返回 
 
通过cookie把字符串返回
通过url中地址栏中的数据返回
 
客户端每次访问项目中的其他模块,每次在地址栏中带着字符串,如果服务端获取到字符串(解析过后),就算登录成功了 
 
JWT 之前提到过token是按照一定规则生成的字符串,但是这个规则是怎么样的,是不确定的(可以自己写,也可以用别人规定的规则,JWT显然是后者)
JWT就是官方给我们的一种规则 
JWT由三部分组成:
第一部分:JWT头信息
第二部分:有效载荷,包含了主体信息(用户信息)
第三部分:签名哈希(也就是防伪标志,知道这个token不是伪造的)
整合JWT 加依赖
<dependencies >          <dependency >          <groupId > io.jsonwebtoken</groupId >          <artifactId > jjwt</artifactId >      </dependency >  </dependencies > 
 
创建工具类
package  com.atguigu.commonutils;import  io.jsonwebtoken.Claims;import  io.jsonwebtoken.Jws;import  io.jsonwebtoken.Jwts;import  io.jsonwebtoken.SignatureAlgorithm;import  org.springframework.http.server.reactive.ServerHttpRequest;import  org.springframework.util.StringUtils;import  javax.servlet.http.HttpServletRequest;import  java.util.Date;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;     }          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 ;     }          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 ;     }          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" );     } } 
 
整合阿里云短信微服务 发送验证码并设置超时时间
@GetMapping("send/{phone}")     public  R sendMsm (@PathVariable  String phone) {                  String  code  =  (String) redisTemplate.opsForValue().get(phone);         if (!StringUtils.isEmpty(code)){             return  R.ok();         }                  code =RandomUtils.getFourBitRandom();         HashMap<String, Object> param = new  HashMap <>();         param.put("code" ,code);                  boolean  isSend=msmService.send(param,phone);         if (isSend){             redisTemplate.opsForValue().set(phone,code,5 , TimeUnit.MINUTES);             return  R.ok();         }else  {             return  R.error();         }     } 
 
登录接口和注册接口 编写实体类 LoginVo 
@Data @ApiModel(value="登录对象", description="登录对象") public  class  LoginVo  {    @ApiModelProperty(value = "手机号")      private  String mobile;     @ApiModelProperty(value = "密码")      private  String password; } 
 
RegisterVo 
@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; } 
 
创建controller编写登录和注册方法 MemberApiController.java
@RestController @RequestMapping("/ucenterservice/apimember") @CrossOrigin public  class  MemberApiController  {    @Autowired      private  MemberService memberService;               @ApiOperation(value = "登录")      @PostMapping("login")      public  R login (@RequestBody  LoginVo loginVo)  {                  String  token  =  memberService.login(loginVo);         return  R.ok().data("token" , token);     }               @ApiOperation(value = "注册")      @PostMapping("register")      public  R register (@RequestBody  RegisterVo registerVo) {                  memberService.register(registerVo);         return  R.ok();     } } 
 
创建service接口和实现类 @Service public  class  MemberServiceImpl  extends  ServiceImpl <MemberMapper, Member> implements  MemberService  {    @Autowired      private  RedisTemplate<String, String> redisTemplate;          @Override      public  String login (LoginVo loginVo)  {         String  mobile  =  loginVo.getMobile();         String  password  =  loginVo.getPassword();                  if (StringUtils.isEmpty(mobile) ||            StringUtils.isEmpty(password) ||            StringUtils.isEmpty(mobile)) {             throw  new  GuliException (20001 ,"error" );         }                  Member  member  =  baseMapper.selectOne(new  QueryWrapper <Member>().eq("mobile" , mobile));         if (null  == member) {             throw  new  GuliException (20001 ,"error" );         }                  if (!MD5.encrypt(password).equals(member.getPassword())) {             throw  new  GuliException (20001 ,"error" );         }                  if (member.getIsDisabled()) {             throw  new  GuliException (20001 ,"error" );         }                  String  token  =  JwtUtils.getJwtToken(member.getId(), member.getNickname());         return  token;     }          @Override      public  void  register (RegisterVo registerVo)  {                  String  nickname  =  registerVo.getNickname();         String  mobile  =  registerVo.getMobile();         String  password  =  registerVo.getPassword();         String  code  =  registerVo.getCode();                  if (StringUtils.isEmpty(mobile) ||            StringUtils.isEmpty(mobile) ||            StringUtils.isEmpty(password) ||            StringUtils.isEmpty(code)) {             throw  new  GuliException (20001 ,"error" );         }                           String  mobleCode  =  redisTemplate.opsForValue().get(mobile);         if (!code.equals(mobleCode)) {             throw  new  GuliException (20001 ,"error" );         }                           Integer  count  =  baseMapper.selectCount(new  QueryWrapper <Member>().eq("mobile" , mobile));         if (count.intValue() > 0 ) {             throw  new  GuliException (20001 ,"error" );         }                  Member  member  =  new  Member ();         member.setNickname(nickname);         member.setMobile(registerVo.getMobile());         member.setPassword(MD5.encrypt(password));         member.setIsDisabled(false );         member.setAvatar("{头像}" );                           this .save(member);        } } 
 
创建接口根据token获取用户信息 在MemberApiController中创建方法
@ApiOperation(value = "根据token获取登录信息") @GetMapping("auth/getLoginInfo") public  R getLoginInfo (HttpServletRequest request) {    try  {         String  memberId  =  JwtUtils.getMemberIdByJwtToken(request);                  Member  member  =  memberService.getById(memberId);         return  R.ok().data("userInfo" ,member);     }catch  (Exception e){         e.printStackTrace();         throw  new  GuliException (20001 ,"error" );     } } 
 
Day13 登录前端实现 页面中实现倒计时的方法,可以用js中的定时器方法实现
这段代码的意思是每隔3000毫秒弹出一下haohao
所以我们可以实现那个发验证码的方法的倒计时效果
每隔1000毫秒执行一次这个方法
登录和登录成功之后首页显示数据的实现过程 
调用登录接口返回token字符串 
把第一步返回的token字符串放到cookie中 
创建前端拦截器:判断cookie里面是否有token字符串,如果有,把token字符串放到请求头 中(因为我们是通过请求头获取token并获取用户id的,当然也可以用cookie,但是我们习惯用请求头) 
根据token值,调用接口,根据token获取用户信息,为了首页面右上角的显示,再把调用接口返回的用户信息放到cookie中 
从cookie中获取用户信息,在首页面显示 
 
在api文件夹中创建登录的js文件,定义接口 
login.js
import  request from  '@/utils/request' export  default  {         submitLogin (userInfo ) {         return  request ({             url : `/ucenterservice/apimember/login` ,             method : 'post' ,             data : userInfo         })     },          getLoginInfo ( ) {         return  request ({             url : `/ucenterservice/apimember/auth/getLoginInfo` ,             method : 'get'          })     } } 
 
在pages文件夹中创建登录页面,调用方法 
login.vue
<template> <div class="main">     <div class="title">         <a class="active" href="/login">登录</a>         <span>·</span>         <a href="/register">注册</a>     </div>     <div class="sign-up-container">         <el-form ref="userForm" :model="user">             <el-form-item class="input-prepend restyle" prop="mobile" :rules="[{required: true, message: '请输入手机号码', trigger: 'blur' },{validator:checkPhone, trigger:'blur'}]">                 <div >                     <el-input type="text" placeholder="手机号" v-model="user.mobile"/>                     <i class="iconfont icon-phone" />     </div>     </el-form-item>             <el-form-item class="input-prepend" prop="password" :rules="[{ required:                                                                         true, message: '请输入密码', trigger: 'blur' }]">                 <div>                     <el-input type="password" placeholder="密码" v-model="user.password"/>                     <i class="iconfont icon-password"/>     </div>     </el-form-item>             <div class="btn">                 <input type="button" class="sign-in-button" value="登录"                        @click="submitLogin()">     </div>     </el-form>         <!-- 更多登录方式 -->         <div class="more-sign">             <h6>社交帐号登录</h6>             <ul>                 <li><a id="weixin" class="weixin" target="_blank"                        href="http://qy.free.idcfengye.com/api/ucenter/weixinLogin/login"><i                                                                                             class="iconfont icon-weixin"/></a></li>                 <li><a id="qq" class="qq" target="_blank" href="#"><i class="iconfont                     icon-qq"/></a></li>     </ul>     </div>     </div>     </div> </template> <script>     import '~/assets/css/sign.css'     import '~/assets/css/iconfont.css'     import cookie from 'js-cookie'     import loginApi from '@/api/login'     export default {         layout: 'sign',         data () {             return {                 user:{                     mobile:'',                     password:''                 },                 loginInfo:{}             }         },         methods: {             submitLogin(){//点击登录触发的方法                 loginApi.submitLogin(this.user).then(response => {                     if(response.data.success){                         //第二步:把token存在cookie中、也可以放在localStorage中                         cookie.set('guli_token', response.data.data.token, { domain: 'localhost' })                         //第四步:登录成功根据token获取用户信息                         loginApi.getLoginInfo().then(response => {                             this.loginInfo = response.data.data.item                             //将用户信息记录cookie                             cookie.set('guli_ucenter', this.loginInfo, { domain: 'localhost'})                             //跳转页面                             window.location.href = "/";                         })                     }                 })             },             checkPhone (rule, value, callback) {                 //debugger                 if (!(/^1[34578]\d{9}$/.test(value))) {                     return callback(new Error('手机号码格式不正确'))                 }                 return callback()             }         }     } </script> 
 
第三步:在request.js添加拦截器,用于传递token信息
import  axios from  'axios' import  { MessageBox , Message  } from  'element-ui' import  cookie from  'js-cookie' const  service = axios.create ({              baseURL : 'http://localhost:9001' ,     timeout : 15000   }) service.interceptors .request .use (     config  =>  {                  if  (cookie.get ('guli_token' )) {             config.headers ['token' ] = cookie.get ('guli_token' );         }         return  config     },     err  =>  {         return  Promise .reject (err);     }) service.interceptors .response .use (     response  =>  {                 if  (response.data .code  == 28004 ) {             console .log ("response.data.resultCode是28004" )                                       window .location .href ="/login"              return          }else {             if  (response.data .code  !== 20000 ) {                                  if (response.data .code  != 25000 ) {                     Message ({                         message : response.data .message  || 'error' ,                         type : 'error' ,                         duration : 5  * 1000                      })                 }             } else  {                 return  response;             }         }     },     error  =>  {         return  Promise .reject (error.response )      }); export  default  service
 
修改layouts中的default.vue页面 显示登录之后的用户信息
<script>     import cookie from 'js-cookie'     import userApi from '@/api/login'     export default {         data() {             return {                 token: '',                 loginInfo: {                     id: '',                     age: '',                     avatar: '',                     mobile: '',                     nickname: '',                     sex: ''                 }             }         },         created() {             this.showInfo()         },         methods: {             showInfo() {//渲染页面前调用的方法                 var jsonStr = cookie.get("guli_ucenter");                 if (jsonStr) {                     this.loginInfo = JSON.parse(jsonStr)                 }             },             logout() {//退出登录                 //给cookie值清空就可以了                 cookie.set('guli_ucenter', "", {domain: 'localhost'})                 cookie.set('guli_token', "", {domain: 'localhost'})                 //跳转页面                 window.location.href = "/"             }         }     } </script> 
 
default.vue页面显示登录之后的用户信息(直接复制粘贴就可以)
<ul class="h-r-login">     <li v-if="!loginInfo.id" id="no-login">         <a href="/login" title="登录">             <em class="icon18 login-icon"> </em>             <span class="vam ml5">登录</span>         </a>         |         <a href="/register" title="注册">             <span class="vam ml5">注册</span>         </a>     </li>                    <li v-if="loginInfo.id" id="is-login-one" class="mr10">         <a id="headerMsgCountId" href="#" title="消息">             <em class="icon18 news-icon"> </em>         </a>         <q class="red-point" style="display: none"> </q>     </li>     <li v-if="loginInfo.id" id="is-login-two" class="h-r-user">         <a href="/ucenter" title>             <img                  :src="loginInfo.avatar"                  width="30"                  height="30"                  class="vam picImg"                  alt                  >             <span id="userName" class="vam disIb">{{ loginInfo.nickname }}</span>         </a>         <a href="javascript:void(0);" title="退出" @click="logout()" class="ml5">退出</a>     </li>     <!-- /未登录显示第1 li;登录后显示第2,3 li --> </ul> 
 
微信扫码登录 OAuth2 
OAuth2是针对特定问题的一种解决方案 
OAuth2主要可以解决两个问题:
开放系统间的授权(存储照片打印照片问题) 
分布式访问问题(单点登录) 
 
 
关于分布式访问(单点登录)问题,之前我们就做过解释(在单点登录的时候)
微信登录 熟悉微信登录流程
后端 application.properties添加相关配置信息
wx.open.app_id =你的appid wx.open.app_secret =你的appsecret wx.open.redirect_url =http://你的服务器名称/api/ucenter/wx/callback 
 
创建util包,创建ConstantPropertiesUtil.java常量类,这个类主要是得到配置文件中的值
@Component public  class  ConstantPropertiesUtil  implements  InitializingBean  {    @Value("${wx.open.app_id}")      private  String appId;     @Value("${wx.open.app_secret}")      private  String appSecret;     @Value("${wx.open.redirect_url}")      private  String redirectUrl;     public  static  String WX_OPEN_APP_ID;     public  static  String WX_OPEN_APP_SECRET;     public  static  String WX_OPEN_REDIRECT_URL;     @Override      public  void  afterPropertiesSet ()  throws  Exception {         WX_OPEN_APP_ID = appId;         WX_OPEN_APP_SECRET = appSecret;         WX_OPEN_REDIRECT_URL = redirectUrl;     } } 
 
创建controller 
guli-microservice-ucenter微服务中创建api包 api包中创建WxApiController(访问这个controller会出现一个二维码哦)
@CrossOrigin @Controller @RequestMapping("/api/ucenter/wx") public  class  WxApiController  {    @GetMapping("login")      public  String genQrConnect (HttpSession session)  {                  String  baseUrl  =  "https://open.weixin.qq.com/connect/qrconnect"  +             "?appid=%s"  +             "&redirect_uri=%s"  +             "&response_type=code"  +             "&scope=snsapi_login"  +             "&state=%s"  +             "#wechat_redirect" ;                  String  redirectUrl  =  ConstantPropertiesUtil.WX_OPEN_REDIRECT_URL;          try  {             redirectUrl = URLEncoder.encode(redirectUrl, "UTF-8" );          } catch  (UnsupportedEncodingException e) {             throw  new  GuliException (20001 , e.getMessage());         }                           String  state  =  "imhelen" ;             System.out.println("state = "  + state);                                                      String  qrcodeUrl  =  String.format(             baseUrl,             ConstantPropertiesUtil.WX_OPEN_APP_ID,             redirectUrl,             state);         return  "redirect:"  + qrcodeUrl;     } } 
 
用户点击“确认登录”后,微信服务器会向谷粒学院的业务服务器发起回调,因此接下来我们需要开发回调controller
跳转路径在配置配置文件的时候已经配置好了 
在WxApiController中添加方法
这个是下面的得到的result ,注释中有标注
openId是指该微信用户的唯一标识,可以用它来查询该用户是否登录过这个业务
需要注意的是:下面的代码中最后生成的token和access_token是不一样的,access_token
 
@GetMapping("callback") public  String callback (String code, String state, HttpSession session)  {         System.out.println("code = "  + code);     System.out.println("state = "  + state);                    String  baseAccessTokenUrl  =          "https://api.weixin.qq.com/sns/oauth2/access_token"  +         "?appid=%s"  +         "&secret=%s"  +         "&code=%s"  +         "&grant_type=authorization_code" ;     String  accessTokenUrl  =  String.format(baseAccessTokenUrl,                                           ConstantPropertiesUtil.WX_OPEN_APP_ID,                                           ConstantPropertiesUtil.WX_OPEN_APP_SECRET,                                           code);     String  result  =  null ;     try  {         result = HttpClientUtils.get(accessTokenUrl);         System.out.println("accessToken============="  + result);     } catch  (Exception e) {         throw  new  GuliException (20001 , "获取access_token失败" );     }          Gson  gson  =  new  Gson ();     HashMap  map  =  gson.fromJson(result, HashMap.class);     String  accessToken  =  (String)map.get("access_token" );     String  openid  =  (String)map.get("openid" );          Member  member  =  memberService.getByOpenid(openid);     if (member == null ){         System.out.println("新用户注册" );                  String  baseUserInfoUrl  =  "https://api.weixin.qq.com/sns/userinfo"  +             "?access_token=%s"  +             "&openid=%s" ;         String  userInfoUrl  =  String.format(baseUserInfoUrl, accessToken, openid);         String  resultUserInfo  =  null ;         try  {             resultUserInfo = HttpClientUtils.get(userInfoUrl);             System.out.println("resultUserInfo=========="  + resultUserInfo);         } catch  (Exception e) {             throw  new  GuliException (20001 , "获取用户信息失败" );         }                  HashMap<String, Object> mapUserInfo = gson.fromJson(resultUserInfo,HashMap.class);         String  nickname  =  (String)mapUserInfo.get("nickname" );         String  headimgurl  =  (String)mapUserInfo.get("headimgurl" );                  member = new  Member ();         member.setNickname(nickname);         member.setOpenid(openid);         member.setAvatar(headimgurl);                  memberService.save(member);     }                              String  token  =  JwtUtils.geneJsonWebToken(member.getId(),member.getNickName());               return  "redirect:http://localhost:3000?token="  + token; } 
 
实现service
@Override public  Member getByOpenid (String openid)  {    QueryWrapper<Member> queryWrapper = new  QueryWrapper <>();     queryWrapper.eq("openid" , openid);     Member  member  =  baseMapper.selectOne(queryWrapper);     return  member; } 
 
前端 
在页面路径中获取token字符串
 
把获取的token字符串放到cookie里
有前端拦截器,判断cookie里面是否有token,如果有,把cookie中的token取出来,放到header里面
 
 
调用后端接口,根据token值获取用户信息,把获取出来的用户信息放到cookie中
 
 
export  default  {    data ( ) {         return  {             token : '' ,             loginInfo : {                 id : '' ,                 age : '' ,                 avatar : '' ,                 mobile : '' ,                 nickname : '' ,                 sex : ''              }         }     },     created ( ) {                  this .token  = this .$route .query .token          if  (this .token ) {             this .wxLogin ()         }         this .showInfo ()     },     methods : {         showInfo ( ) {             var  jsonStr = cookie.get ("guli_ucenter" );             if  (jsonStr) {                 this .loginInfo  = JSON .parse (jsonStr)             }         },         logout ( ) {             cookie.set ('guli_ucenter' , "" , {domain : 'localhost' })             cookie.set ('guli_token' , "" , {domain : 'localhost' })                          window .location .href  = "/"          },         wxLogin ( ) {             if  (this .token  == '' ) return                           cookie.set ('guli_token' , this .token , {domain : 'localhost' })             cookie.set ('guli_ucenter' , '' , {domain : 'localhost' })                          userApi.getLoginInfo ().then (response  =>  {                 this .loginInfo  = response.data .data .item                                   cookie.set ('guli_ucenter' , this .loginInfo , {domain : 'localhost' })             })         }     } } 
 
Day14 名师列表功能 名师详情功能 用到了动态路由的知识点
动态路由的页面由_开头
课程列表功能 课程详情功能 需要编写sql语句,根据课程id查询课程信息 
课程的基本信息 
课程分类 
课程描述 
所属讲师 
 
定义vo对象 在项目中很多时候需要把model转换成dto用于网站信息的展示,按前端的需要传递对象的数据,保 证model对外是隐私的,例如密码之类的属性能很好地避免暴露在外,同时也会减小数据传输的体积。
CourseWebVo
@ApiModel(value="课程信息", description="网站课程详情页需要的相关字段") @Data public  class  CourseWebVo  implements  Serializable  {    private  static  final  long  serialVersionUID  =  1L ;     private  String id;     @ApiModelProperty(value = "课程标题")      private  String title;     @ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")      private  BigDecimal price;     @ApiModelProperty(value = "总课时")      private  Integer lessonNum;     @ApiModelProperty(value = "课程封面图片路径")      private  String cover;     @ApiModelProperty(value = "销售数量")      private  Long buyCount;     @ApiModelProperty(value = "浏览数量")      private  Long viewCount;     @ApiModelProperty(value = "课程简介")      private  String description;     @ApiModelProperty(value = "讲师ID")      private  String teacherId;     @ApiModelProperty(value = "讲师姓名")      private  String teacherName;     @ApiModelProperty(value = "讲师资历,一句话说明讲师")      private  String intro;     @ApiModelProperty(value = "讲师头像")      private  String avatar;     @ApiModelProperty(value = "课程类别ID")      private  String subjectLevelOneId;     @ApiModelProperty(value = "类别名称")      private  String subjectLevelOne;     @ApiModelProperty(value = "课程类别ID")      private  String subjectLevelTwoId;     @ApiModelProperty(value = "类别名称")      private  String subjectLevelTwo; } 
 
定义Mapper方法 CourseMapper.Java
CourseWebVo selectInfoWebById(String courseId); 
 
CourseMapper.xml
< < select  id= "selectInfoWebById" resultType= "com.guli.edu.vo.CourseWebVo">     SELECT      c.id,     c.title,     c.cover,     CONVERT (c.price, DECIMAL (8 ,2 )) AS  price,     c.lesson_num AS  lessonNum,     c.cover,     c.buy_count AS  buyCount,     c.view_count AS  viewCount,     cd.description,     t.id AS  teacherId,     t.name AS  teacherName,     t.intro,     t.avatar,     s1.id AS  subjectLevelOneId,     s1.title AS  subjectLevelOne,     s2.id AS  subjectLevelTwoId,     s2.title AS  subjectLevelTwo                    FROM      edu_course c     LEFT  JOIN  edu_course_description cd ON  c.id =  cd.id     LEFT  JOIN  edu_teacher t ON  c.teacher_id =  t.id     LEFT  JOIN  edu_subject s1 ON  c.subject_parent_id =  s1.id     LEFT  JOIN  edu_subject s2 ON  c.subject_id =  s2.id               WHERE      c.id =  #{id} < / select > 
 
业务层获取数据并更新浏览量 @Override public  CourseWebVo selectInfoWebById (String id)  {         this .updatePageViewCount(id);          return  baseMapper.selectInfoWebById(id); } @Override public  void  updatePageViewCount (String id)  {         Course  course  =  baseMapper.selectById(id);     course.setViewCount(course.getViewCount() + 1 );     baseMapper.updateById(course); } 
 
接口层 CourseController
@Autowired private  ChapterService chapterService;@ApiOperation(value = "根据ID查询课程") @GetMapping(value = "{courseId}") public  R getById (     @ApiParam(name = "courseId", value = "课程ID", required = true)      @PathVariable  String courseId) {         CourseWebVo  courseWebVo  =  courseService.selectInfoWebById(courseId);          List<ChapterVo> chapterVoList = chapterService.nestedList(courseId);     return  R.ok().data("course" , courseWebVo).data("chapterVoList" , chapterVoList); } 
 
整合阿里云视频播放器实现视频播放 
引入阿里云提供给我们的链接(放在前端的head标签中)
<link  rel ="stylesheet"  href ="https://g.alicdn.com/de/prismplayer/2.8.1/skins/default/aliplayer-min.css"  />    <script  charset ="utf-8"  type ="text/javascript"          src ="https://g.alicdn.com/de/prismplayer/2.8.1/aliplayer-min.js" > </script > 
 
 
在body中初始化播放器
<body >     <div  class ="prism-player"  id ="J_prismPlayer" > </div >      <script >          var  player = new  Aliplayer ({             id : 'J_prismPlayer' ,             width : '100%' ,             autoplay : false ,             cover : 'http://liveroom-img.oss-cn-qingdao.aliyuncs.com/logo.png' ,                                       source : '你的视频播放地址' ,                          encryptType :'1' ,             vid : '视频id' ,             playauth : '视频授权码' ,         }, function  (player ) {             console .log ('播放器创建好了。' )         });      </script > </body > 
 
 这两种方式取哪种都可以,但是推荐用第二种,比较安全
 
 
后端获取播放凭证 在vod微服务中创建controller,再创建获取凭证的方法
@CrossOrigin  @RestController @RequestMapping("/vod/video") public  class  VideoController  {    @GetMapping("get-play-auth/{videoId}")      public  R getVideoPlayAuth (@PathVariable("videoId")  String videoId)  throws          Exception {                  String  accessKeyId  =  ConstantPropertiesUtil.ACCESS_KEY_ID;         String  accessKeySecret  =  ConstantPropertiesUtil.ACCESS_KEY_SECRET;                  DefaultAcsClient  client  =  AliyunVodSDKUtils.initVodClient(accessKeyId, accessKeySecret);                  GetVideoPlayAuthRequest  request  =  new  GetVideoPlayAuthRequest ();         request.setVideoId(videoId);                  GetVideoPlayAuthResponse  response  =  client.getAcsResponse(request);                  String  playAuth  =  response.getPlayAuth();                  return  R.ok().message("获取凭证成功" ).data("playAuth" , playAuth);     } } 
 
前端播放器整合 
在线配置:https://player.alicdn.com/aliplayer/setting/setting.html  
功能展示:https://player.alicdn.com/aliplayer/presentation/index.html 
 
利用动态路由创建新播放器页面
修改超链接 修改原来信息页面中的超链接属性
<a   :href ="'/player/'+video.videoSourceId"  	:title ="video.title"  	target ="_blank" > 
 
编写api 创建api/vod.js(发送请求到后端获取播放凭证)
import  request from  '@/utils/request' const  api_name = '/vod/video' export  default  {    getPlayAuth (vid ) {         return  request ({             url : `eduvod/video/getplayAuth/${vid} ` ,             method : 'get'          })     } } 
 
创建动态路由跳转的页面(播放页面) 创建 pages/player/_vid.vue
<template> <div>     <!-- 阿里云视频播放器样式 -->     <link rel="stylesheet"           href="https://g.alicdn.com/de/prismplayer/2.8.1/skins/default/aliplayer-min.css" >     <!-- 阿里云视频播放器脚本 -->     <script charset="utf-8" type="text/javascript"             src="https://g.alicdn.com/de/prismplayer/2.8.1/aliplayer-min.js" />  <!-- 定义播放器dom -->  	<div id="J_prismPlayer" class="prism-player" /> </div> </template> <script>     import vod from '@/api/vod'     export default {         layout: 'video',//应用video布局         asyncData({ params, error }) {//异步请求取得路径中的vid值             return vod.getPlayAuth(params.vid).then(response => {                 // console.log(response.data.data)                 return {                     vid: params.vid,//得到路径中的值,并保存                     playAuth: response.data.data.playAuth//得到登录凭证                 }             })         },         /**         * 页面渲染完成时:此时js脚本已加载,Aliplayer已定义,可以使用         * 如果在created生命周期函数中使用,Aliplayer is not defined错误         */         mounted() {             new Aliplayer({                 id: 'J_prismPlayer',//这个id写的是播放器dom的id                 vid: this.vid, // 视频id                 playauth: this.playAuth, // 播放凭证                 encryptType: '1', // 如果播放加密视频,则需设置encryptType=1,非加密视频无需设                 置此项                 width: '100%',                 height: '500px'             }, function(player) {                 console.log('播放器创建成功')             })         }     } </script> 
 
Day15 支付功能 课程支付需求描述 
课程分为免费课程和付费课程,如果是免费课程可以直接观看,如果是付费课程,需要下单支付后才可以观看
 
如果是免费课程,在用户选择课程后进入课程详情页后,直接显示“立即观看”,点击立即观看,可以切换到播放列表进行视频播放
 
如果是付费课程,在用户选择课程后进入到课程详情页面后,会显示“立即购买”
(1)点击”立即购买”会生成课程的订单,跳转到订单页面。
(2)点击”去支付”,跳转到支付页面(就是微信扫码)
(3)扫码支付后,会跳转到课程详情页面,同时显示“立即观看”
 
 
准备工作 创建两张表
开发创建订单接口 编写订单controller 
@RestController @RequestMapping("/orderservice/order") @CrossOrigin public  class  TOrderController  {    @Autowired      private  TOrderService orderService;          @PostMapping("createOrder/{courseId}")      public  R save (@PathVariable  String courseId, HttpServletRequest request)  {         String  orderId  =  orderService.saveOrder(courseId,JwtUtils.getMemberIdByJwtToken(request));         return  R.ok().data("orderId" , orderId);     } } 
 
在service_edu创建接口 
实现根据课程id获取课程信息,返回课程信息对象
@GetMapping("getDto/{courseId}") public  com.atguigu.commonutils.vo.CourseInfoForm getCourseInfoDto (@PathVariable  String courseId)  {    CourseInfoForm  courseInfoForm  =  courseService.getCourseInfo(courseId);     com.atguigu.commonutils.vo.CourseInfoForm  courseInfo  =  new  com .atguigu.commonutils.vo.CourseInfoForm();     BeanUtils.copyProperties(courseInfoForm,courseInfo);     return  courseInfo; } 
 
在service_ucenter创建接口 
实现用户id获取用户信息,返回用户信息对象
@PostMapping("getInfoUc/{id}") public  com.atguigu.commonutils.vo.UcenterMember getInfo (@PathVariable  String id)  {         UcenterMember  ucenterMember  =  memberService.getById(id);     com.atguigu.commonutils.vo.UcenterMember  memeber  =  new  com .atguigu.commonutils.vo.UcenterMember();     BeanUtils.copyProperties(ucenterMember,memeber);     return  memeber; } 
 
编写订单service 
在service_order模块创建接口,实现远程调用
EduClient
@Component @FeignClient("service-edu") public  interface  EduClient  {         @GetMapping("/eduservice/course/getDto/{courseId}")      public  com.atguigu.commonutils.vo.CourseInfoForm         getCourseInfoDto (@PathVariable("courseId")  String courseId) ; } 
 
UcenterClient
@Component @FeignClient("service-ucenter") public  interface  UcenterClient  {         @PostMapping("/ucenterservice/member/getInfoUc/{id}")      public  com.atguigu.commonutils.vo.UcenterMember getInfo (@PathVariable("id")  String id) ; } 
 
在service_order模块编写创建订单service
@Service public  class  TOrderServiceImpl  extends  ServiceImpl <TOrderMapper, TOrder>    implements  TOrderService  {     @Autowired      private  EduClient eduClient;     @Autowired      private  UcenterClient ucenterClient;          @Override      public  String saveOrder (String courseId, String memberId)  {                  CourseInfoForm  courseDto  =  eduClient.getCourseInfoDto(courseId);                  UcenterMember  ucenterMember  =  ucenterClient.getInfo(memberId);                  TOrder  order  =  new  TOrder ();         order.setOrderNo(OrderNoUtil.getOrderNo());         order.setCourseId(courseId);         order.setCourseTitle(courseDto.getTitle());         order.setCourseCover(courseDto.getCover());         order.setTeacherName("test" );         order.setTotalFee(courseDto.getPrice());         order.setMemberId(memberId);         order.setMobile(ucenterMember.getMobile());         order.setNickname(ucenterMember.getNickname());         order.setStatus(0 );         order.setPayType(1 );         baseMapper.insert(order);         return  order.getOrderNo();     } } 
 
开发获取订单接口 在订单controller创建根据id获取订单信息接口
@GetMapping("getOrder/{orderId}") public  R get (@PathVariable  String orderId)  {    QueryWrapper<TOrder> wrapper = new  QueryWrapper <>();     wrapper.eq("order_no" ,orderId);     TOrder  order  =  orderService.getOne(wrapper);     return  R.ok().data("item" , order); } 
 
生成微信支付二维码接口 引入依赖
<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 > 
 
编写controller
@RestController @RequestMapping("/orderservice/log") @CrossOrigin public  class  PayLogController  {    @Autowired      private  PayLogService payService;          @GetMapping("/createNative/{orderNo}")      public  R createNative (@PathVariable  String orderNo)  {         Map  map  =  payService.createNative(orderNo);         return  R.ok().data(map);     } } 
 
编写service
@Service public  class  PayLogServiceImpl  extends  ServiceImpl <PayLogMapper, PayLog>    implements  PayLogService  {     @Autowired      private  TOrderService orderService;     @Override      public  Map createNative (String orderNo)  {         try  {                          QueryWrapper<TOrder> wrapper = new  QueryWrapper <>();             wrapper.eq("order_no" ,orderNo);             TOrder  order  =  orderService.getOne(wrapper);                          Map  m  =  new  HashMap ();                          m.put("appid" , "wx74862e0dfcf69954" );             m.put("mch_id" , "1558950191" );             m.put("nonce_str" , WXPayUtil.generateNonceStr());             m.put("body" , order.getCourseTitle());             m.put("out_trade_no" , orderNo);             m.put("total_fee" , order.getTotalFee().multiply(new  BigDecimal ("100" )).longValue()+"" );             m.put("spbill_create_ip" , "127.0.0.1" );             m.put("notify_url" ,"http://guli.shop/api/order/weixinPay/weixinNotify\n" );             m.put("trade_type" , "NATIVE" );                          HttpClient  client  =  new  HttpClient ("https://api.mch.weixin.qq.com/pay/unifiedorder" );                          client.setXmlParam(WXPayUtil.generateSignedXml(m, "T6m9iK73b0kn9g5v426MKfHQH7X8rKwb" ));             client.setHttps(true );                          client.post();                          String  xml  =  client.getContent();                          Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);                          Map  map  =  new  HashMap <>();             map.put("out_trade_no" , orderNo);             map.put("course_id" , order.getCourseId());             map.put("total_fee" , order.getTotalFee());             map.put("result_code" , resultMap.get("result_code" ));             map.put("code_url" , resultMap.get("code_url" ));                                       return  map;         } catch  (Exception e) {             e.printStackTrace();             return  new  HashMap <>();         }     } } 
 
查询用户的支付状态接口 这个接口主要是为了查询用户是否已支付,调用的主要是微信的api
编写controller
@GetMapping("/queryPayStatus/{orderNo}") public  R queryPayStatus (@PathVariable  String orderNo)  {         Map<String, String> map = payService.queryPayStatus(orderNo);     if  (map == null ) {         return  R.error().message("支付出错" );     }     if  (map.get("trade_state" ).equals("SUCCESS" )) {                  payService.updateOrderStatus(map);         return  R.ok().message("支付成功" );     }     return  R.ok().code(25000 ).message("支付中" ); } 
 
编写service,查询支付状态与更新订单状态
@Override public  Map queryPayStatus (String orderNo)  {    try  {                  Map  m  =  new  HashMap <>();         m.put("appid" , "wx74862e0dfcf69954" );         m.put("mch_id" , "1558950191" );         m.put("out_trade_no" , orderNo);         m.put("nonce_str" , WXPayUtil.generateNonceStr());                  HttpClient  client  =  new              HttpClient ("https://api.mch.weixin.qq.com/pay/orderquery" );         client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb" ));         client.setHttps(true );         client.post();                  String  xml  =  client.getContent();                  Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);                  return  resultMap;     } catch  (Exception e) {         e.printStackTrace();     }     return  null ; } 
 
@Override public  void  updateOrderStatus (Map<String, String> map)  {         String  orderNo  =  map.get("out_trade_no" );          QueryWrapper<TOrder> wrapper = new  QueryWrapper <>();     wrapper.eq("order_no" ,orderNo);     TOrder  order  =  orderService.getOne(wrapper);     if (order.getStatus().intValue() == 1 ) return ;     order.setStatus(1 );     orderService.updateById(order);          PayLog payLog=new  PayLog ();     payLog.setOrderNo(order.getOrderNo());     payLog.setPayTime(new  Date ());     payLog.setPayType(1 );     payLog.setTotalFee(order.getTotalFee());     payLog.setTradeState(map.get("trade_state" ));     payLog.setTransactionId(map.get("transaction_id" ));     payLog.setAttr(JSONObject.toJSONString(map));     baseMapper.insert(payLog); } 
 
课程支付前端 
创建order.js
import  request from  '@/utils/request' export  default  {         createOrder (cid ) {         return  request ({             url : '/orderservice/order/createOrder/' +cid,             method : 'post'          })     },          getById (cid ) {         return  request ({             url : '/orderservice/order/getOrder/' +cid,             method : 'get'          })     },          createNative (cid ) {         return  request ({             url : '/orderservice/log/createNative/' +cid,             method : 'get'          })     },          queryPayStatus (cid ) {         return  request ({             url : '/orderservice/log/queryPayStatus/' +cid,             method : 'get'          })     } } 
 
在课程详情页面中添加创建订单方法 
在“立即购买”位置添加事件
methods :{         createOrder ( ){         order.createOrder (this .courseId ).then (response  =>  {             if (response.data .success ){                                  this .$router .push ({ path : '/order/' + response.data .data .orderId  })             }         })     }, } 
 
创建订单页面,显示订单信息 
在pages下面创建order文件夹,创建_oid.vue页面
在_oid.vue页面调用方法,获取订单信息
在页面中给值赋值,并且调用方法
<script> import  orderApi from  '@/api/order' export  default  {         asyncData ({params, error} ) {         return  orderApi.getById (params.oid ).then (response  =>  {             return  {                 order : response.data .data .item              }         })     },     methods : {                  toPay ( ) {             this .$router .push ({path : '/pay/'  + this .order .orderNo })         }     } } </script> 
 
创建支付页面,生成二维码完成支付
<script>     import  orderApi from  '@/api/course'  export  default  {         asyncData ({params, error} ) {         return  orderApi.createNative (params.pid ).then (response  =>  {             return  {                 payObj : response.data .data              }         })     },     data ( ) {         return  {             timer : null ,              initQCode : '' ,             timer1 :''          }     },     mounted ( ) {                           this .timer1  = setInterval (() =>  {             this .queryPayStatus (this .payObj .out_trade_no )         }, 3000 );     },     methods : {                  queryPayStatus (out_trade_no ) {             orderApi.queryPayStatus (out_trade_no).then (response  =>  {                 if  (response.data .success ) {                                          clearInterval (this .timer1 )                     this .$message({                         type : 'success' ,                         message : '支付成功!'                      })                                          this .$router .push ({path : '/course/'  + this .payObj .course_id })                 }             })         }     } } </script> 
 
需要注意的是,这里用了两次动态路由
 ,都是根据id进行跳转的,所以都可以使用动态路由
Day16 统计分析模块 #首先介绍一个函数 #Date 函数,Date 函数用来获取时间格式里面的日期部分 #比如查询2020 -03 -09 有多少创建人数人 select  count (* ) from  member  where  date (member.gmt_create)= ‘2020 -03 -09 ’;
 
统计在线教育项目中,没一天有多少注册人数
把统计出来的注册人数,使用图表显示出来
后端实现服务 在service_ucenter模块创建接口,统计某一天的注册人数
controller
@GetMapping(value = "countregister/{day}") public  R registerCount ( @PathVariable  String day) {    Integer  count  =  memberService.countRegisterByDay(day);     return  R.ok().data("countRegister" , count); } 
 
service
@Override public  Integer countRegisterByDay (String day)  {    return  baseMapper.selectRegisterCount(day); } 
 
mapper
<select  id ="selectRegisterCount"  resultType ="java.lang.Integer" >     SELECT COUNT(1)     FROM ucenter_member     WHERE DATE(gmt_create) = #{value} </select > 
 
在service_statistics模块调用微服务(这个调用的是上面查某一天注册人数的接口)
controller
@PostMapping("{day}") public  R createStatisticsByDate (@PathVariable  String day)  {    dailyService.createStatisticsByDay(day);     return  R.ok(); } 
 
service
@Service public  class  StatisticsDailyServiceImpl  extends  ServiceImpl <StatisticsDailyMapper,StatisticsDaily> implements  StatisticsDailyService  {    @Autowired      private  UcenterClient ucenterClient;     @Override      public  void  createStatisticsByDay (String day)  {                  QueryWrapper<StatisticsDaily> dayQueryWrapper = new  QueryWrapper <>();         dayQueryWrapper.eq("date_calculated" , day);         baseMapper.delete(dayQueryWrapper);                  Integer  registerNum  =  (Integer)ucenterClient.registerCount(day).getData().get("countRegister" );         Integer  loginNum  =  RandomUtils.nextInt(100 , 200 );         Integer  videoViewNum  =  RandomUtils.nextInt(100 , 200 );         Integer  courseNum  =  RandomUtils.nextInt(100 , 200 );                  StatisticsDaily  daily  =  new  StatisticsDaily ();         daily.setRegisterNum(registerNum);         daily.setLoginNum(loginNum);         daily.setVideoViewNum(videoViewNum);         daily.setCourseNum(courseNum);         daily.setDateCalculated(day);         baseMapper.insert(daily);     } } 
 
定时任务 生成cron表达式
在线Cron表达式生成器 (qqe2.com) 
定时任务的意思是在固定的时候自动执行程序
创建定时任务类,使用cron表达式 
 
@Component public  class  ScheduledTask  {    @Autowired      private  StatisticsDailyService dailyService;          @Scheduled(cron = "0 0 1 * * ?")      public  void  task2 ()  {                  String  day  =  DateUtil.formatDate(DateUtil.addDays(new  Date (), -1 ));         dailyService.createStatisticsByDay(day);     } } 
 
在启动类上加上注解 
 
前端页面实现 创建api
创建src/api/sta.js
import  request from  '@/utils/request' const  api_name = '/admin/statistics/daily' export  default  {    createStatistics (day ) {         return  request ({             url : `${api_name} /${day} ` ,             method : 'post'          })     } } 
 
增加路由
src/router/index.js
{     path : '/statistics/daily' ,         component : Layout ,             redirect : '/statistics/daily/create' ,                 name : 'Statistics' ,                     meta : { title : '统计分析' , icon : 'chart'  },                         children : [                             {                                 path : 'create' ,                                 name : 'StatisticsDailyCreate' ,                                 component : () =>  import ('@/views/statistics/daily/create' ),                                 meta : { title : '生成统计'  }                             }                         ] }, 
 
创建组件
src/views/statistics/daily/create.vue 模板部分
这个是element-ui的组件,直接粘贴即用
<template >     <div  class ="app-container" >                   <el-form  :inline ="true"  class ="demo-form-inline" >              <el-form-item  label ="日期" >                  <el-date-picker                                   v-model ="day"                                  type ="date"                                  placeholder ="选择要统计的日期"                                  value-format ="yyyy-MM-dd"  />             </el-form-item >              <el-button                          :disabled ="btnDisabled"                         type ="primary"                         @click ="create()" > 生成</el-button >         </el-form >      </div >  </template > 
 
script部分
<script>     import  daily from  '@/api/sta'  export  default  {    data ( ) {         return  {             day : '' ,             btnDisabled : false          }     },     methods : {         create ( ) {             this .btnDisabled  = true              daily.createStatistics (this .day ).then (response  =>  {                 this .btnDisabled  = false                  this .$message({                     type : 'success' ,                     message : '生成成功'                  })             })         }     } } </script> 
 
前端集成ECharts 安装ECharts
npm install --save echarts@4.1.0
增加路由
src/router/index.js 在统计分析路由中增加子路由
{     path : 'chart' ,         name : 'StatisticsDayChart' ,             component : () =>  import ('@/views/statistics/daily/chart' ),                 meta : { title : '统计图表'  } }  
 
创建组件
src/views/statistics/daily/chart.vue
模板
<template >     <div  class ="app-container" >                   <el-form  :inline ="true"  class ="demo-form-inline" >              <el-form-item >                  <el-select  v-model ="searchObj.type"  clearable  placeholder ="请选择" >                      <el-option  label ="学员登录数统计"  value ="login_num" />                      <el-option  label ="学员注册数统计"  value ="register_num" />                      <el-option  label ="课程播放数统计"  value ="video_view_num" />                      <el-option  label ="每日课程数统计"  value ="course_num" />                  </el-select >              </el-form-item >              <el-form-item >                  <el-date-picker                                   v-model ="searchObj.begin"                                  type ="date"                                  placeholder ="选择开始日期"                                  value-format ="yyyy-MM-dd"  />             </el-form-item >              <el-form-item >                  <el-date-picker                                   v-model ="searchObj.end"                                  type ="date"                                  placeholder ="选择截止日期"                                  value-format ="yyyy-MM-dd"  />             </el-form-item >              <el-button                          :disabled ="btnDisabled"                         type ="primary"                         icon ="el-icon-search"                         @click ="showChart()" > 查询</el-button >         </el-form >          <div  class ="chart-container" >              <div  id ="chart"  class ="chart"  style ="height:500px;width:100%"  />          </div >      </div >  </template > 
 
js:暂时显示临时数据
<script>     import  echarts from  'echarts'  export  default  {    data ( ) {         return  {             searchObj : {                 type : '' ,                 begin : '' ,                 end : ''              },             btnDisabled : false ,             chart : null ,             title : '' ,             xData : [],             yData : []         }     },     methods : {         showChart ( ) {             this .initChartData ()             this .setChart ()         },                  initChartData ( ) {         },                  setChart ( ) {                          this .chart  = echarts.init (document .getElementById ('chart' ))                                       var  option = {                                  xAxis : {                     type : 'category' ,                     data : ['Mon' , 'Tue' , 'Wed' , 'Thu' , 'Fri' , 'Sat' , 'Sun' ]                 },                                  yAxis : {                     type : 'value'                  },                                  series : [{                                          data : [820 , 932 , 901 , 934 , 1290 , 1330 , 1320 ],                                          type : 'line'                  }]             }             this .chart .setOption (option)         }     } } </script> 
 
完成后端业务 controller
@GetMapping("show-chart/{begin}/{end}/{type}") public  R showChart (@PathVariable  String begin,@PathVariable  String end,@PathVariable  String type) {    Map<String, Object> map = dailyService.getChartData(begin, end, type);     return  R.ok().data(map); } 
 
service
@Override public  Map<String, Object> getChartData (String begin, String end, String type)  {    QueryWrapper<Daily> dayQueryWrapper = new  QueryWrapper <>();     dayQueryWrapper.select(type, "date_calculated" );     dayQueryWrapper.between("date_calculated" , begin, end);     List<Daily> dayList = baseMapper.selectList(dayQueryWrapper);     Map<String, Object> map = new  HashMap <>();     List<Integer> dataList = new  ArrayList <Integer>();     List<String> dateList = new  ArrayList <String>();     map.put("dataList" , dataList);     map.put("dateList" , dateList);     for  (int  i  =  0 ; i < dayList.size(); i++) {         Daily  daily  =  dayList.get(i);         dateList.add(daily.getDateCalculated());         switch  (type) {             case  "register_num" :                 dataList.add(daily.getRegisterNum());                 break ;             case  "login_num" :                 dataList.add(daily.getLoginNum());                 break ;             case  "video_view_num" :                 dataList.add(daily.getVideoViewNum());                 break ;             case  "course_num" :                 dataList.add(daily.getCourseNum());                 break ;             default :                 break ;         }     }     return  map; } 
 
前后端整合
创建api
src/api/statistics/daily.js中添加方法
showChart (searchObj ) {    return  request ({         url : `${api_name} /show-chart/${searchObj.begin} /${searchObj.end} /${searchObj.type} ` ,         method : 'get'      }) } 
 
chart.vue中引入api模块
import daily from '@/api/statistics/daily' 
 
修改initChartData方法
showChart ( ) {    this .initChartData ()      },          initChartData ( ) {         daily.showChart (this .searchObj ).then (response  =>  {                          this .yData  = response.data .dataList                           this .xData  = response.data .dateList                           switch  (this .searchObj .type ) {                 case  'register_num' :                     this .title  = '学员注册数统计'                      break                  case  'login_num' :                     this .title  = '学员登录数统计'                      break                  case  'video_view_num' :                     this .title  = '课程播放数统计'                      break                  case  'course_num' :                     this .title  = '每日课程数统计'                      break              }             this .setChart ()         })     }, 
 
修改options中的数据
xAxis : {    type : 'category' ,         data : this .xData  },          yAxis : {         type : 'value'      },                  series : [{                          data : this .yData ,                          type : 'line'          }], 
 
Day17 Canal Canal介绍 在前面的统计分析功能中,我们采取了服务调用 获取统计数据,这样耦合度高,效率相对较低,目前我们采取另一种实现方式,通过实时同步数据库表 的方式实现,
例如我们要统计每天注册与登录人数,我们只需把会员表同步到统计库中,实现本地统计就可以了。
这样效率更高,耦合度更低,Canal就是一个很好的数据库同步工具。
canal是阿里巴巴旗下 的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL。
Canal环境搭建 canal的原理是基于mysql binlog技术,所以这里一定需要开启mysql的binlog写入功能
开启mysql服务: service mysql start
(1)检查binlog功能是否有开启
show variables like 'log_bin';
(2)如果显示状态为OFF表示该功能未开启,开启binlog功能
1 ,修改 mysql 的配置文件 my.cnfvi / etc/ my.cnf 追加内容: log- bin= mysql- bin #binlog文件名 binlog_format= ROW  #选择row 模式 server_id= 1  #mysql实例id,不能和canal的slaveId重复 2 ,重启 mysql:service mysql restart 3 ,登录 mysql 客户端,查看 log_bin 变量mysql>  show  variables like  'log_bin' ; + |  Variable_name |  Value  | + |  log_bin |  ON | + 1  row  in  set  (0.00  sec)———————————————— 如果显示状态为ON 表示该功能已开启 
 
(3)在mysql里面添加以下的相关用户和权限
CREATE  USER  'canal' @'%'  IDENTIFIED BY  'canal' ;GRANT  SHOW  VIEW , SELECT , REPLICATION SLAVE, REPLICATION CLIENT ON  * .*  TO 'canal' @'%' ;FLUSH PRIVILEGES; 
 
下载安装Canal服务 下载地址:https://github.com/alibaba/canal/releases  (1)下载之后,放到目录中,解压文件
cd /usr/local/canal canal.deployer-1.1.4.tar.gz tar zxvf canal.deployer-1.1.4.tar.gz 
 
(2)修改配置文件 vi conf/example/instance.properties
canal.instance.master.address =192.168.44.132:3306 canal.instance.dbUsername =canal canal.instance.dbPassword =canal canal.instance.filter.regex =guli_ucenter.ucenter_member 
 
(3)进入bin目录下启动 
sh bin/startup.sh
使用Cannal 引入依赖
<dependencies >     <dependency >          <groupId > org.springframework.boot</groupId >          <artifactId > spring-boot-starter-web</artifactId >      </dependency >           <dependency >          <groupId > mysql</groupId >          <artifactId > mysql-connector-java</artifactId >      </dependency >      <dependency >          <groupId > commons-dbutils</groupId >          <artifactId > commons-dbutils</artifactId >      </dependency >      <dependency >          <groupId > org.springframework.boot</groupId >          <artifactId > spring-boot-starter-jdbc</artifactId >      </dependency >      <dependency >          <groupId > com.alibaba.otter</groupId >          <artifactId > canal.client</artifactId >      </dependency >  </dependencies > 
 
、创建application.properties配置文件
server.port =10000 spring.application.name =canal-client spring.profiles.active =dev spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8 spring.datasource.username =root spring.datasource.password =root 
 
编写canal客户端类
@Component public  class  CanalClient  {         private  Queue<String> SQL_QUEUE = new  ConcurrentLinkedQueue <>();     @Resource      private  DataSource dataSource;          public  void  run ()  {         CanalConnector  connector  =  CanalConnectors.newSingleConnector(new                                                                        InetSocketAddress ("192.168.44.132" ,                                                                                         11111 ), "example" , "" , "" );         int  batchSize  =  1000 ;         try  {             connector.connect();             connector.subscribe(".*\\..*" );             connector.rollback();             try  {                 while  (true ) {                                          Message  message  =  connector.getWithoutAck(batchSize);                     long  batchId  =  message.getId();                     int  size  =  message.getEntries().size();                     if  (batchId == -1  || size == 0 ) {                         Thread.sleep(1000 );                     } else  {                         dataHandle(message.getEntries());                     }                     connector.ack(batchId);                                          if  (SQL_QUEUE.size() >= 1 ) {                         executeQueueSql();                     }                 }             } catch  (InterruptedException e) {                 e.printStackTrace();             } catch  (InvalidProtocolBufferException e) {                 e.printStackTrace();             }         } finally  {             connector.disconnect();         }     }          public  void  executeQueueSql ()  {         int  size  =  SQL_QUEUE.size();         for  (int  i  =  0 ; i < size; i++) {             String  sql  =  SQL_QUEUE.poll();             System.out.println("[sql]----> "  + sql);             this .execute(sql.toString());         }     }          private  void  dataHandle (List<Entry> entrys)  throws          InvalidProtocolBufferException {         for  (Entry entry : entrys) {             if  (EntryType.ROWDATA == entry.getEntryType()) {                 RowChange  rowChange  =  RowChange.parseFrom(entry.getStoreValue());                 EventType  eventType  =  rowChange.getEventType();                 if  (eventType == EventType.DELETE) {                     saveDeleteSql(entry);                 } else  if  (eventType == EventType.UPDATE) {                     saveUpdateSql(entry);                 } else  if  (eventType == EventType.INSERT) {                     saveInsertSql(entry);                 }             }         }     }          private  void  saveUpdateSql (Entry entry)  {         try  {             RowChange  rowChange  =  RowChange.parseFrom(entry.getStoreValue());             List<RowData> rowDatasList = rowChange.getRowDatasList();             for  (RowData rowData : rowDatasList) {                 List<Column> newColumnList = rowData.getAfterColumnsList();                 StringBuffer  sql  =  new  StringBuffer ("update "  +                                                     entry.getHeader().getTableName() + " set " );                 for  (int  i  =  0 ; i < newColumnList.size(); i++) {                     sql.append(" "  + newColumnList.get(i).getName()                                + " = '"  + newColumnList.get(i).getValue() + "'" );                     if  (i != newColumnList.size() - 1 ) {                         sql.append("," );                     }                 }                 sql.append(" where " );                 List<Column> oldColumnList = rowData.getBeforeColumnsList();                 for  (Column column : oldColumnList) {                     if  (column.getIsKey()) {                                                  sql.append(column.getName() + "="  + column.getValue());                         break ;                     }                 }                 SQL_QUEUE.add(sql.toString());             }         } catch  (InvalidProtocolBufferException e) {             e.printStackTrace();         }     }          private  void  saveDeleteSql (Entry entry)  {         try  {             RowChange  rowChange  =  RowChange.parseFrom(entry.getStoreValue());             List<RowData> rowDatasList = rowChange.getRowDatasList();             for  (RowData rowData : rowDatasList) {                 List<Column> columnList = rowData.getBeforeColumnsList();                 StringBuffer  sql  =  new  StringBuffer ("delete from "  +                                                     entry.getHeader().getTableName() + " where " );                 for  (Column column : columnList) {                     if  (column.getIsKey()) {                                                  sql.append(column.getName() + "="  + column.getValue());                         break ;                     }                 }                 SQL_QUEUE.add(sql.toString());             }         } catch  (InvalidProtocolBufferException e) {             e.printStackTrace();         }     }          private  void  saveInsertSql (Entry entry)  {         try  {             RowChange  rowChange  =  RowChange.parseFrom(entry.getStoreValue());             List<RowData> rowDatasList = rowChange.getRowDatasList();             for  (RowData rowData : rowDatasList) {                 List<Column> columnList = rowData.getAfterColumnsList();                 StringBuffer  sql  =  new  StringBuffer ("insert into "  +                                                     entry.getHeader().getTableName() + " (" );                 for  (int  i  =  0 ; i < columnList.size(); i++) {                     sql.append(columnList.get(i).getName());                     if  (i != columnList.size() - 1 ) {                         sql.append("," );                     }                 }                 sql.append(") VALUES (" );                 for  (int  i  =  0 ; i < columnList.size(); i++) {                     sql.append("'"  + columnList.get(i).getValue() + "'" );                     if  (i != columnList.size() - 1 ) {                         sql.append("," );                     }                 }                 sql.append(")" );                 SQL_QUEUE.add(sql.toString());             }         } catch  (InvalidProtocolBufferException e) {             e.printStackTrace();         }     }          public  void  execute (String sql)  {         Connection  con  =  null ;         try  {             if (null  == sql) return ;             con = dataSource.getConnection();             QueryRunner  qr  =  new  QueryRunner ();             int  row  =  qr.execute(con, sql);             System.out.println("update: " + row);         } catch  (SQLException e) {             e.printStackTrace();         } finally  {             DbUtils.closeQuietly(con);         }     } } 
 
创建启动类
@SpringBootApplication public  class  CanalApplication  implements  CommandLineRunner  {    @Resource      private  CanalClient canalClient;     public  static  void  main (String[] args)  {         SpringApplication.run(CanalApplication.class, args);     }     @Override      public  void  run (String... strings)  throws  Exception {                  canalClient.run();     } } 
 
权限管理 需要五张表,五张表的关系如下
根据第三范可知,如果是两张表互为多对多的关系,需要多新建一张表来存储多对多的这种关系
菜单管理
(1)菜单列表
(2)菜单添加、修改
(3)菜单删除功能
角色管理(管理员..等等)
(1)角色的CRUD
(2)为角色分配菜单
用户管理(haohao…)
(1)用户的CRUD
   (2)为用户分配角色
Day18 Spring Security Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication) 和用户授权(Authorization) 两个部分。
用户认证:
进入用户登录的时候,输入用户名和密码,查询数据库,验证用户名和密码是否正确,如果正确的话,认证就成功了
用户授权:
登录了系统,登录用户可能为不同的角色。比如可能是管理员,管理员所可以操作的功能要比普通用户多
 
Spring Security其实就是用filter ,多请求的路径进行过滤。
 (1)如果是基于Session,那么Spring-security会对cookie里的sessionid进行解析,找到服务器存储 的session信息,然后判断当前用户是否符合请求的要求。 
(2)如果是token,则是解析出token,然后将当前请求加入到Spring-security管理的权限信息中去
认证与授权实现思路 如果系统的模块众多,每个模块都需要就行授权与认证,所以我们选择基于token的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为key,权限列表为value的形式存入redis缓存中,根据用户名相关信息生成token返回,浏览器将token记录到cookie中, 每次调用api接口都默认将token携带到header请求头中,Spring-security解析header头获取token信息,解 析token获取当前用户名,根据用户名就可以从redis中获取权限列表,这样Spring-security就能够判断当前 请求是否有权限访问
 
1、项目描述 (1) 在线教育系统,分为前台网站系统和后台运营平台,B2C模式。 前台用户系统包括课程、讲师、问答、文章几大大部分,使用了微服务技术架构,前后端分离开发。 后端的主要技术架构是:SpringBoot + SpringCloud + MyBatis-Plus + HttpClient + MySQL + Maven+EasyExcel+ nginx 前端的架构是:Node.js + Vue.js +element-ui+NUXT+ECharts 其他涉及到的中间件包括Redis、阿里云OSS、阿里云视频点播 业务中使用了ECharts做图表展示,使用EasyExcel完成分类批量添加、注册分布式单点登录使用了JWT (2) 项目前后端分离开发,后端采用SpringCloud微服务架构,持久层用的是MyBatis-Plus,微服务分库设 计,使用Swagger生成接口文档 接入了阿里云视频点播、阿里云OSS。 系统分为前台用户系统和后台管理系统两部分。 前台用户系统包括:首页、课程、名师、问答、文章。 后台管理系统包括:讲师管理、课程分类管理、课程管理、统计分析、Banner管理、订单管理、权限管 理等功能。 在线教育计费案例: 小A是一名杭州的创业者,带领团队研发了一个在线教育平台。他希望把视频托管在阿里云上,存量视频大约1000个, 占用存储空间近1T,每月预计新增视频100个,并新增存储约100G,课程视频的时长集中在20-40分钟,并且按照不同课 程进行分类管理。为了保障各端的观看效果,计划为用户提供“标清480P”和“高清720P”两种清晰度。目前已有用 户400人左右,每日平均视频观看次数1000次,在移动端和PC端观看次数比例大致为3:1。 2、这是一个项目还是一个产品 这是一个产品 1.0版本是单体应用:SSM 2.0版本加入了SpringCloud,将一些关键业务和访问量比较大的部分分离了出去 目前独立出来的服务有教学服务、视频点播服务、用户服务、统计分析服务、网关服务 3、测试要求 首页和视频详情页qps单机qps要求 2000+ 经常用每秒查询率来衡量域名系统服务器的机器的性能,其即为QPS QPS = 并发量 / 平均响应时间 4、企业中的项目(产品)开发流程 一个中大型项目的开发流程 1、需求调研(产品经理) 2、需求评审(产品/设计/前端/后端/测试/运营) 3、立项(项目经理、品管) 4、UI设计 5、开发 架构、数据库设计、API文档、MOCK数据、开发、单元测试 前端 后端 6、前端后端联调 7、项目提测:黑盒白盒、压力测试(qps) loadrunner 8、bug修改 9、回归测试 10、运维和部署上线 11、灰度发布 12、全量发布 13、维护和运营 5、系统中都有那些角色?数据库是怎么设计的? 前台:会员(学员) 后台:系统管理员、运营人员 后台分库,每个微服务一个独立的数据库,使用了分布式id生成器 6、视频点播是怎么实现的(流媒体你们是怎么实现的) 我们直接接入了阿里云的云视频点播。云平台上的功能包括视频上传、转码、加密、智能审核、监控统 计等。 还包括视频播放功能,阿里云还提供了一个视频播放器。 7、前后端联调经常遇到的问题:  1、请求方式post、get  2、json、x-wwww-form-urlencoded混乱的错误  3、后台必要的参数,前端省略了  4、数据类型不匹配  5、空指针异常  6、分布式系统中分布式id生成器生成的id 长度过大(19个字符长度的整数),js无法解析(js智能解 析16个长度:2的53次幂)  id策略改成 ID_WORKER_STR 8、前后端分离项目中的跨域问题是如何解决的 后端服务器配置:我们的项目中是通过Spring注解解决跨域的 @CrossOrigin 也可以使用nginx反向代理、httpClient、网关 9、说说你做了哪个部分、遇到了什么问题、怎么解决的 问题1: 分布式id生成器在前端无法处理,总是在后三位进行四舍五入。 分布式id生成器生成的id是19个字符的长度,前端javascript脚本对整数的处理能力只有2的53次方,也就 是最多只能处理16个字符 解决的方案是把id在程序中设置成了字符串的性质 问题2: 项目迁移到Spring-Cloud的时候,经过网关时,前端传递的cookie后端一只获取不了,看 了cloud中zuul的源码,发现向下游传递数据的时候,zull默认过滤了敏感信息,将cookie过滤掉了 解决的方案是在配置文件中将请求头的过滤清除掉,使cookie可以向下游传递 问题3……. 10、分布式系统的id生成策略https://www.cnblogs.com/haoxinyue/p/5208136.html  11、项目组有多少人,人员如何组成? 12、分布式系统的CAP原理 CAP定理: 指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分 区容错性),三者不可同时获得。 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(所有节点在同一时间的 数据完全一致,越多节点,数据同步越耗时) 可用性(A):负载过大后,集群整体是否还能响应客户端的读写请求。(服务一直可用,而且是正常响 应时间) 分区容错性(P):分区容错性,就是高可用性,一个节点崩了,并不影响其它的节点(100个节点,挂 了几个,不影响服务,越多机器越好) CA 满足的情况下,P不能满足的原因: 数据同步(C)需要时间,也要正常的时间内响应(A),那么机器数量就要少,所以P就不满足
CP 满足的情况下,A不能满足的原因: 数据同步(C)需要时间, 机器数量也多(P),但是同步数据需要时间,所以不能再正常时间内响应,所以A就 不满足 AP 满足的情况下,C不能满足的原因: 机器数量也多(P),正常的时间内响应(A),那么数据就不能及时同步到其他节点,所以C不满足 注册中心选择的原则: Zookeeper:CP设计,保证了一致性,集群搭建的时候,某个节点失效,则会进行选举行的leader,或 者半数以上节点不可用,则无法提供服务,因此可用性没法满足 Eureka:AP原则,无主从节点,一个节点挂了,自动切换其他节点可以使用,去中心化 结论: 分布式系统中P,肯定要满足,所以我们只能在一致性和可用性之间进行权衡 如果要求一致性,则选择zookeeper,如金融行业 如果要求可用性,则Eureka,如教育、电商系统 没有最好的选择,最好的选择是根据业务场景来进行架构设计 13、前端渲染和后端渲染有什么区别 前端渲染是返回json给前端,通过javascript将数据绑定到页面上 后端渲染是在服务器端将页面生成直接发送给服务器,有利于SEO的优化 14、能画一下系统架构图吗
总结在线教育项目功能点 一、准备 
1、把后端接口启动起来
2、启动前端项目(前台系统和后台系统)
二、项目后台管理系统功能 
1、登录功能 (SpringSecurity框架)
2、权限管理模块 
(1)菜单管理:列表、添加、修改、删除
(2)角色管理
* 列表、添加、修改、删除、批量删除
* 为角色分配菜单
(3)用户管理
* 列表、添加、修改、删除、批量删除
* 为用户分配角色
(4)权限管理表和关系 
*** 使用五张表**
  
3、讲师管理模块 
(1)条件查询分页列表、添加、修改、删除
4、课程分类模块 
(1)添加课程分类
* 读取Excel里面课程分类数据,添加到数据库中
(2)课程分类列表
* 使用树形结构显示课程分类列表
5、课程管理模块 
(1)课程列表功能
(2)添加课程 
* 课程发布流程:第一步填写课程基本信息,第二步添加课程大纲(章节和小节),第三步课程信息确认,最终课程发布
* 课程如何判断是否已经被发布了? 使用status字段
* 课程添加过程中,中途把课程停止添加,重新去添加新的课程,如何找到之前没有发布完成课程,继续进行发布?  到课程列表中根据课程状态查询未发布的课程,点击课程右边超链接把课程继续发布完成
(3)添加小节上传课程视频 
  
6、统计分析模块 
(1)生成统计数据 
(2)统计数据图表显示 
三、项目前台用户系统功能 
1、首页数据显示
(1)显示幻灯片功能
(2)显示热门课程
(3)显示名师
2、注册功能 
(1)获取手机验证码
3、登录功能 
(1)普通登录和退出 
*** SSO(单点登录)**
  
l JWT 
l 使用JWT生成token字符串
l JWT有三部分组成
l 登录实现流程 
登录调用登录接口返回token字符串,把返回token字符串放到cookie里面,创建前l 端拦截器进行判断,如果cookie里面包含token字符串,把token字符串放到header里面。调用接口根据token获取用户信息,把用户信息放到cookie里面,进行显示
(2)微信扫描登录 
l OAuth2 
l 是针对特定问题解决方案
l 主要有两个问题:开放系统间授权,分布式访问
l 如何获取扫描人信息过程? 
l 扫描之后微信接口返回code(临时票据),拿着code值请求微信固定地址,得到两个值:access_token(访问凭证)和openid(微信唯一标识),你拿着这两个值再去请求微信固定的地址,得到微信扫描人信息(比如昵称,头像等等)
4、名师列表功能
5、名师详情功能
6、课程列表功能
(1)条件查询分页列表功能
7、课程详情页 
(1)课程信息显示(包含课程基本信息,分类,讲师,课程大纲)
(2)判断课程是否需要购买
8、课程视频在线播放 
9、课程支付功能(微信支付) 
(1)生成课程订单
(2)生成微信支付二维码
(3)微信最终支付
*** 微信支付实现流程:**
* 如果课程是收费课程,点击立即购买,生成课程订单
* 点击订单页面去支付,生成微信支付二维码
* 使用微信扫描支付二维码实现支付
* 支付之后,每隔3秒查询支付状态(是否支付成功),如果没有支付成功等待,如果支付成功之后,更新订单状态(已经支付状态),向支付记录表添加支付成功记录
总结在线教育项目技术点(前端) 1、在线教育项目采用前后端分离开发 
2、项目使用前端技术 
(1)vue 
* 基本语法
* 常见指令 : v-bind  v-model  v-if  v-for  v-html
* 绑定事件: v-on-click  @click
* 生命周期:created() 页面渲染之前   mounted()页面渲染之后
*** ES6规范**
(2)Element-ui 
(3)nodejs 
* 是JavaScript运行环境,不需要浏览器直接运行js代码,模拟服务器效果
(4)NPM 
* 包管理工具,类似于Maven
* npm命令: npm init    npm install  依赖名称
(5)Babel 
* 转码器,可以把ES6代码转换成ES5代码
(6)前端模块化 
* 通过一个页面或者一个js文件,调用另外一个js文件里面的方法
* 问题:ES6的模块化无法在Node.js中执行,需要用Babel编辑成ES5后再执行
(6)后台系统使用vue-admin-template 
* 基于vue+Element-ui
(7)前台系统使用Nuxt 
* 基于vue
* 服务器渲染技术
(8)Echarts 
* 图表工具
总结在线教育项目技术点(后端技术一) 1、项目采用微服务架构 
2、SpringBoot 
(1)SpringBoot本质是就是Spring,只是快速构建Spring工程脚手架
(2)细节:
* 启动类包扫描机制
* 设置扫描规则 @ComponentScan(*“包路径”*  )
* 配置类
(3)SpringBoot配置文件
* 配置文件类型:properties和yml
* 配置文件加载顺序:bootstrap   application   application-dev
3、SpringCloud 
(1)是很多框架总称,使用这些框架实现微服务架构,基于SpringBoot实现
(2)组成框架有哪些?
(3)项目中,使用阿里巴巴Nacos,替代SpringCloud一些组件
(4)Nacos
* 使用Nacos作为注册中心
* 使用Nacos作为配置中心
(5)Feign
* 服务调用,一个微服务调用另外一个微服务,实现远程调用
(6)熔断器
(7)Gateway网关
* SpringCloud之前zuul网关,目前Gateway网关
(8)版本 
  
4、MyBatisPlus 
(1)MyBatisPlus就是对MyBatis做增强
(2)自动填充
(3)乐观锁
(4)逻辑删除
(5)代码生成器
5、EasyExcel 
(1)阿里巴巴提供操作excel工具,代码简洁,效率很高
(2)EasyExcel对poi进行封装,采用SAX方式解析
(3)项目应用在添加课程分类,读取excel数据
总结在线教育项目技术点(后端技术二)
1、Spring Security
(1)在项目整合框架实现权限管理功能
(2)SpringSecurity框架组成:认证和授权
(3)SpringSecurity登录认证过程
  
(4)SpringSecurity代码执行过程
2、Redis
(1)首页数据通过Redis进行缓存
(2)Redis数据类型
(3)使用Redis作为缓存,不太重要或者不经常改变数据适合放到Redis作为缓存 
3、Nginx
(1)反向代理服务器
(2)请求转发,负载均衡,动静分离
4、OAuth2+JWT
(1)OAuth2针对特定问题解决方案
(2)JWT包含三部分
5、HttpClient
(1)发送请求返回响应的工具,不需要浏览器完成请求和响应的过程
(2)应用场景:微信登录获取扫描人信息,微信支付查询支付状态
6、Cookie
(1)Cookie特点:
* 客户端技术
* 每次发送请求带着cookie值进行发送
* cookie有默认会话级别,关闭浏览器cookie默认不存在了,
* 但是可以设置cookie有效时长  setMaxAge
7、微信登录
8、微信支付
9、阿里云OSS
(1)文件存储服务器
(2)添加讲师时候上传讲师头像
10、阿里云视频点播
(1)视频上传、删除、播放
(2)整合阿里云视频播放器进行视频播放
* 使用视频播放凭证
11、阿里云短信服务
(1)注册时候,发送手机验证码
12、Git
(1)代码提交到远程Git仓库
13、Docker+Jenkins
(1)手动打包运行
(2)idea打包
(3)jenkins自动化部署过程
总结在线教育项目问题 1、前端问题-路由切换问题
(1)多次路由跳转到同一个vue页面,页面中created方法只会执行一次
(2)解决方案:使用vue监听
2、前端问题-ES6模块化运行问题
(1)Nodejs不能直接运行ES6模块化代码,需要使用Babel把ES6模块化代码转换ES5代码 执行
3、mp生成19位id值
(1)mp生成id值是19位,JavaScript处理数字类型值时候,只会处理到16位
4、跨域问题
(1)访问协议,ip地址,端口号,这三个如果有任何一个不一样,产生跨域
(2)跨域解决:
* 在Controller添加注解
* 通过网关解决
5、413问题
(1)上传视频时候,因为Nginx有上传文件大小限制,如果超过Nginx大小,出现413
(2)413错误:请求体过大
(3)在Nginx配置客户端大小
(4)响应状态码:413  403  302
6、Maven加载问题
(1)maven加载项目时候,默认不会加载src-java文件夹里面xml类型文件的
(2)解决方案:
* 直接复制xml文件到target目录
* 通过配置实现