布谷课堂2

day10 springCloud微服务调用

布谷课堂(谷粒学院)一

springCloud

springCloud介绍

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Nacos注册中心介绍

引入:想在edu模块删除小节的方法中调用vod模块中删除视频的方法。不能把vod中的方法复制到edu中,也不能在edu中引入vod的依赖从而调用这样两个模块就有关联依赖了就不叫微服务了。这就要用到Nacos注册中心介绍来实现两个独立运行模块之间的调用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Nacos安装和服务注册

一、Nacos安装
在这里插入图片描述
在这里插入图片描述
访问:http://localhost:8848/nacos/
http://192.168.2.2:8848/nacos/index.html
在这里插入图片描述
二、服务注册
网上下载不了,引入依赖直接把包复制到文件中
service的pom中

   <!--服务注册-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--要是没有这个注解@EnableDiscoveryClient加下面的依赖->
          <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-commons</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

三、在要注册的服务的配置文件中application中配置
service_edu和service_vod的配置文件中

# nacos服务地址 千万不要写错
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

四、在service_edu和service_vod的启动类添加注解

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient

在这里插入图片描述

删除小节视频(服务调用)

Feign

在这里插入图片描述
使用的前提互相调用服务在Nacos进行注册

删除小节视频

第一步、先引入依赖

 <!--服务调用-->
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-openfeign</artifactId>
         </dependency>

第二步、在调用端service_edu服务启动类添加注解

@EnableFeignClients    //feign调用服务

第三步、在调用端service_edu创建interface。使用注解指定调用服务名称,定义调用的方法路径
client/VodClient .java

@Component //交给spring管理
@FeignClient("service-vod") //是在配置文件中配置的服务名字不是包名
public interface VodClient {
    //定义调用的方法路径
    //根据视频id删除阿里云视频
    @DeleteMapping("/eduvod/video/removeAlyVideo/{id}") //路径写全
    public R removeAlyVideo(@PathVariable("id") String id);//这个里面使用@PathVariable一定要指定参数名称
}

注意:使用@PathVariable注解一定要指定参数名称(“id”)
第四步、实现代码删除小节删除视频
EduVideoController.java

   //注入vodClient
    @Autowired
    private VodClient vodClient;
    
//删除小节,删除对应的阿里云中的视频
    @DeleteMapping("{id}")
    public R deleteVideo(@PathVariable String id) {//这个是小节id
        //根据小节id获取视频id,调用方法实现视频删除
        EduVideo eduVideo = videoService.getById(id);
        String videoSourceId = eduVideo.getVideoSourceId();//得到视频id
        //判断小节里面是否有视频id
        if (!StringUtils.isEmpty(videoSourceId) ) {
       //根据视频id,远程调用实现视频删除
            vodClient.removeAlyVideo(videoSourceId);//需要视频id
        }
        videoService.removeById(id);
        return R.ok();
    }

测试删除视频成功
问题启动service_oss模块的时候因为这个模块没有注册所以下面会报错。引入nacos注册中心的依赖后项目启动后就会去找注册中心。
在这里插入图片描述

所以也给service_oss模块注册在配置文件中加入nacos服务地址

# nacos服务地址 千万不要写错
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

在启动类中加入注解

@EnableDiscoveryClient

删除课程删除视频

一、service_vod模块里
VodController.java

 //删除多个阿里云视频的方法
    //参数是多个视频id
    @DeleteMapping("delete-batch")
    public R deleteBatch(@RequestParam("videoIdList") List<String> videoIdList) {
        vodService.removeMoreAlyVideo(videoIdList);
        return R.ok();
    }

VodService接口

  /**
     * 删除多个视频的方法
     * @param videoIdList
     */
    void removeMoreAlyVideo(List videoIdList);

VodServiceImpl

import org.apache.commons.lang3.StringUtils;

  @Override
    public void removeMoreAlyVideo(List videoIdList) {
        try {
            //初始化对象
            DefaultAcsClient client = InitObject.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);
            //创建一个删除视频request对象
            DeleteVideoRequest request = new DeleteVideoRequest();
            //videoIdList值转换为1,2,3
            String videoIds = StringUtils.join(videoIdList.toArray(), ",");
            //向request设置视频id
            request.setVideoIds(videoIds);
            //调用初始化对象的方法实现删除
            client.getAcsResponse(request);
        } catch (Exception e) {
            e.printStackTrace();
            throw new BuguException(20001, "删除视频失败");
        }
    }

二、要在service_edu模块中远程调用删除多个视频的方法
在VodClient接口中定义方法

  //删除多个阿里云视频的方法
    //参数是多个视频id
    @DeleteMapping("/eduvod/vidoe/delete-batch")
    public R deleteBatch(@RequestParam("videoIdList") List<list> videoIdList);

在这里插入图片描述

在EduVideoServiceImpl中

 @Autowired
    private VodClient vodClient;

    // 删除小节,删除对应的视频文件
    @Override
    public void removeVideoByCourseId(String courseId) {
        //1 根据课程id查询出所有的视频id
        QueryWrapper<EduVideo> wrapperVideo= new QueryWrapper<>();
        wrapperVideo.eq("course_id", courseId);//查出指定课程id的所有EduVideo对象
        wrapperVideo.select("video_source_id)"); //只得到EduVideo对象的video_source_id属性
        List<EduVideo> eduVideoList = baseMapper.selectList(wrapperVideo);//最终得到的EduVideo对象集合中只有video_source_id
        //List<EduVideo>变成List<String>
        ArrayList<String> videoIds = new ArrayList<>();
        for (int i = 0; i < eduVideoList.size(); i++) {
            EduVideo eduVideo = eduVideoList.get(i);
        //   if (eduVideo != null) { //因为网速的原因,视频还没有上传成功,id和名字也没有到数据库中就删除,eduVideo查出来可能为null报空指针异常
                String videoSourceId = eduVideo.getVideoSourceId();
                if (!StringUtils.isEmpty(videoSourceId)) {
                    //放到videoIds集合里面
                    videoIds.add(videoSourceId);
                }
      //      }
        }
        
       //根据多个视频id删除多个视频
        if (videoIds.size() > 0) {
            vodClient.deleteBatch(videoIds);
        }
        
        //根据删除指定课程id的小节
        QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id", courseId);
        baseMapper.delete(wrapper);
    }

在这里插入图片描述

注意:报空指针异常就加一个判断eduVideo不为空
在这里插入图片描述

day11 首页数据显示和添加Redis缓存

在这里插入图片描述

搭建项目前台环境(NUXT)

NUXT介绍
在这里插入图片描述
在这里插入图片描述

搭建环境过程

在这里插入图片描述

在这里插入图片描述

NUXT目录结构

在这里插入图片描述

文件名文件内容
nutx编译之后的文件
assets一般放项目使用使用静态资源比如css,js,img
components放项目使用相关组件
layouts里面的default.vue定义网页布局方式
pagesindex.vue是项目页面
nuxt.config.js项目核心配置文件

在这里插入图片描述

整合项目首页面

1、安装幻灯片插件

npm install vue-awesome-swiper

2、在plugins目录下创建nuxt-swiper-plugin.js
里面的内容

import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'

Vue.use(VueAwesomeSwiper)


3、nuxt.config.js中配置插件,将plugins和css节点复制到module.exports节点下

 // 幻灯片
  plugins: [
    { src: '~/plugins/nuxt-swiper-plugin.js', ssr: false }
  ],

  css: [
    'swiper/dist/css/swiper.css'
  ]

4、复制项目使用的静态资源,assets目录
在这里插入图片描述
5、从课件中复制代码到layouts目录下default.vue
在这里插入图片描述
6、定义首页面
从课件中复制代码到pages/index.vue
修改了原始文件中的资源路径~/assets/
7、复制幻灯片代码到index.vue

整合课程和名师页面

在这里插入图片描述
course.vue静态路由。_id.vue是动态路由代码都复制

首页数据显示-banner接口

一、在service模块里创建新的maven模块service_cms
问题:新建的模块里面resources图标不对,配置文件是加载不出来的
在这里插入图片描述
解决:把这个文件设置为配置文件的图标
在这里插入图片描述
2、在配置文件中application.properties配置

# 服务端口
server.port=8004
# 服务名
spring.application.name=service-edu  

# 环境设置:dev开发环境、test、prod生产环境
spring.profiles.active=dev

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/bugu?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

# 设置日志级别
#logging.level.root=info


#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

#配置mapper.xml文件的路径  为了自己可以写sql语句
mybatis-plus.mapper-locations=classpath:com/yhn/educms/mapper/xml/*.xml


3、创建一个数据库表管理幻灯片,根据表使用代码生成器
在这里插入图片描述

# Host: localhost  (Version 5.7.19)
# Date: 2019-11-18 15:49:41
# Generator: MySQL-Front 6.1  (Build 1.26)


#
# Structure for table "crm_banner"
#

CREATE TABLE `crm_banner` (
  `id` char(19) NOT NULL DEFAULT '' COMMENT 'ID',
  `title` varchar(20) DEFAULT '' COMMENT '标题',
  `image_url` varchar(500) NOT NULL DEFAULT '' COMMENT '图片地址',
  `link_url` varchar(500) DEFAULT '' COMMENT '链接地址',
  `sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序',
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_name` (`title`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='首页banner表';

#
# Data for table "crm_banner"
#

INSERT INTO `crm_banner` VALUES ('1194556896025845762','test1','https://online-teach-file.oss-cn-beijing.aliyuncs.com/cms/2019/11/14/297acd3b-b592-4cfb-a446-a28310369675.jpg','/course',1,0,'2019-11-13 18:05:32','2019-11-18 10:28:22'),('1194607458461216769','test2','https://online-teach-file.oss-cn-beijing.aliyuncs.com/cms/2019/11/13/8f80790d-d736-4842-a6a4-4dcb0d684d4e.jpg','/teacher',2,0,'2019-11-13 21:26:27','2019-11-14 09:12:15');

5、创建启动类
com.yhn.eudcms

@SpringBootApplication
@ComponentScan({"com.yhn"}) //指定扫描位置
@MapperScan("com.yhn.educms.mapper") //可以扫描到mapper,或者可以建个配置类加这个注解

public class CmsApplication {
    public static void main(String[] args) {
        SpringApplication.run(CmsApplication.class, args);

    }
}


6、使用代码生成器
里面的路径,包名,表名都要改
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
7、创建接口 后台对banner管理接口crud操作
BannerAdminController.java

/**
 * <p>
 * 首页banner表 前端控制器
 * 后台管理员用
 * </p>
 *
 * @author yhn
 * @since 2021-03-11
 */
@RestController
@RequestMapping("/educms/banneradmin")
@CrossOrigin
public class BannerAdminController {

    @Autowired
    private CrmBannerService bannerService;
    //分页查询banner
    @GetMapping("pageBanner/{page}/{limit}")
    public R pageBanner(@PathVariable long page,@PathVariable long limit) {
        Page<CrmBanner> pageBanner = new Page<>(page, limit);
        bannerService.page(pageBanner, null);
        return R.ok().data("items", pageBanner.getRecords()).data("total", pageBanner.getTotal());
    }
    //2 添加banner
    @PostMapping("addBanner")
    public R addBanner(@RequestBody CrmBanner crmBanner) {
        bannerService.save(crmBanner);
        return R.ok();
    }

    @ApiOperation(value = "获取Banner")
    @GetMapping("get/{id}")
    public R get(@PathVariable String id) {
        CrmBanner banner = bannerService.getById(id);
        return R.ok().data("item", banner);
    }

    @ApiOperation(value = "修改Banner")
    @PutMapping("update")
    public R updateById(@RequestBody CrmBanner banner) {
        bannerService.updateById(banner);
        return R.ok();
    }

    @ApiOperation(value = "删除Banner")
    @DeleteMapping("remove/{id}")
    public R remove(@PathVariable String id) {
        bannerService.removeById(id);
        return R.ok();
    }
}


自己完善后台前端页面
8、创建接口前台对banner显示接口
BannerFrontController.java

/**前台显示banner
 * @author YHN
 * @create 2021-03-11 17:06
 */
@RestController
@RequestMapping("/educms/bannerfront")
@CrossOrigin
public class BannerFrontController {
    @Autowired
    private CrmBannerService bannerService;

    //查询所有banner
    @GetMapping("getAllBanner")
    public R getAllBanner() {
        //为了后面加redis所有在service中新写一个方法
        List<CrmBanner> list = bannerService.selectAllBanner();
        return R.ok().data("list",list);
    }
}

CrmBannerService接口

public interface CrmBannerService extends IService<CrmBanner> {

    List<CrmBanner> selectAllBanner();

}

CrmBannerServiceImpl

@Service
public class CrmBannerServiceImpl extends ServiceImpl<CrmBannerMapper, CrmBanner> implements CrmBannerService {
    //查询所有banner
    @Override
    public List<CrmBanner> selectAllBanner() {
        //根据id进行降序排列,显示排列之后的前两个
        QueryWrapper<CrmBanner> wrapper = new QueryWrapper<>();
        wrapper.orderByDesc("id");
        //last方法。拼接sql语句
        wrapper.last("limit 2");

        List<CrmBanner> list = baseMapper.selectList(wrapper);
        return list;
    }
}

首页数据显示-banner显示(前端)

一、因为这个里面默认没有axios,所以要下载

npm install axios

在这里插入图片描述

二、封装axios
之前的前端代码是帮我们封装好的
新建utils文件夹在里面创建request.js

在这里插入图片描述

import axios from 'axios'

// 创建axios实例,配置了nginx,写nginx的地址
const service = axios.create({
  baseURL: 'http://localhost:9001', // api的base_url
  timeout: 20000 // 请求超时时间
    })

export default service

三、首页的banner数据显示
1、创建api文件夹,在api文件夹创建js文件、
banner.js

import request from '@/utils/request'

export default {
    //查询前两条banner数据
  getListBanner() {
    return request({
      url: '/educms/bannerfront/getAllBanner',
      method: 'get'
    })
  }
}

2、在页面中调用接口得到数据进行显示
在index.vue中进行调用
先引入banner.js

import banner from "@/api/banner"

3、在data中定义数据

在这里插入图片描述

4、编写方法,并且在初始化时候调用

  created() {
    //调用查询banner的方法
     this.getBannerList()
  },
  methods:{
   
    //查询banner数据
    getBannerList() {
      banner.getListBanner()
        .then(response => {
          this.bannerList = response.data.data.list
        })
    }
  }

之前的前端框架帮我们做了封装,现在没有封装就要写两个data,response.data.data.得到数据
在这里插入图片描述
5、在幻灯片中遍历显示

   <!-- 幻灯片 开始 -->
  <div v-swiper:mySwiper="swiperOption">
      <div class="swiper-wrapper">

          <div v-for="banner in bannerList" :key="banner.id" class="swiper-slide" style="background: #040B1B;">
              <a target="_blank" :href="banner.linkUrl">
                  <img :src="banner.imageUrl" :alt="banner.title">
              </a>
          </div>
      </div>
      <div class="swiper-pagination swiper-pagination-white"></div>
      <div class="swiper-button-prev swiper-button-white" slot="button-prev"></div>
      <div class="swiper-button-next swiper-button-white" slot="button-next"></div>
  </div>
  <!-- 幻灯片 结束 -->

热门课程和名师接口

一、接口部分
在edu_service的controller包下新建一个front包
里面写一个controller
IndexFrontController

@RestController
@RequestMapping("/eduservice/indexfront")
public class IndexFrontController {
    @Autowired
    private EduCourseService courseService;

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

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

    }
}

二、前端部分
1、引入api
index.js

import request from '@/utils/request'

export default {
    //查询热门课程和名师
  getIndexData() {
    return request({
      url: '/eduservice/indexfront/index',
      method: 'get'
    })
  }
}

2、引入index.js,在data中定义数据

import index from '@/api/index'

export default {
  data () {
    return {
     eduList:[],
      teacherList:[]

3、页面中调用方法
pages\index.vue

created() {
    //调用查询banner的方法
     this.getBannerList()
    //调用查询热门课程和名师的方法
     this.getHotCourseTeacher()
  },
  methods:{
    //查询热门课程和名师
    getHotCourseTeacher() {
      index.getIndexData()
        .then(response => {
          this.eduList = response.data.data.eduList
          this.teacherList = response.data.data.teacherList
        })
    },

4、组件中循环获得显示在页面中

<div id="aCoursesList">
      <!-- 网校课程 开始 -->
      <div>
        <section class="container">
          <header class="comm-title">
            <h2 class="tac">
              <span class="c-333">热门课程</span>
            </h2>
          </header>
          <div>
            <article class="comm-course-list">
              <ul class="of" id="bna">
                <li v-for="course in eduList" :key="course.id">
                  <div class="cc-l-wrap">
                    <section class="course-img">
                      <img
                        :src="course.cover"
                        class="img-responsive"
                        :alt="course.title"
                      >
                      <div class="cc-mask">
                        <a href="#" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
                      </div>
                    </section>
                    <h3 class="hLh30 txtOf mt10">
                      <a href="#" :title="course.title" class="course-title fsize18 c-333">{{course.title}}</a>
                    </h3>
                    <section class="mt10 hLh20 of">
                      <span class="fr jgTag bg-green" v-if="Number(course.price) === 0">
                        <i class="c-fff fsize12 f-fA">免费</i>
                      </span>
                      <span class="fl jgAttr c-ccc f-fA">
                        <i class="c-999 f-fA">9634人学习</i>
                        |
                        <i class="c-999 f-fA">9634评论</i>
                      </span>
                    </section>
                  </div>
                </li>
               
              </ul>
              <div class="clear"></div>
            </article>
            <section class="tac pt20">
              <a href="#" title="全部课程" class="comm-btn c-btn-2">全部课程</a>
            </section>
          </div>
        </section>
      </div>
      <!-- /网校课程 结束 -->
      <!-- 网校名师 开始 -->
      <div>
        <section class="container">
          <header class="comm-title">
            <h2 class="tac">
              <span class="c-333">名师大咖</span>
            </h2>
          </header>
          <div>
            <article class="i-teacher-list">
              <ul class="of">
                <li v-for="teacher in teacherList" :key="teacher.id">
                  <section class="i-teach-wrap">
                    <div class="i-teach-pic">
                      <a href="/teacher/1" :title="teacher.name">
                        <img :alt="teacher.name" :src="teacher.avatar">
                      </a>
                    </div>
                    <div class="mt10 hLh30 txtOf tac">
                      <a href="/teacher/1" :title="teacher.name" class="fsize18 c-666">{{teacher.name}}</a>
                    </div>
                    <div class="hLh30 txtOf tac">
                      <span class="fsize14 c-999">{{teacher.career}}</span>
                    </div>
                    <div class="mt15 i-q-txt">
                      <p
                        class="c-999 f-fA"
                      >{{teacher.intro}}</p>
                    </div>
                  </section>
                </li>
                
              </ul>
              <div class="clear"></div>
            </article>
            <section class="tac pt20">
              <a href="#" title="全部讲师" class="comm-btn c-btn-2">全部讲师</a>
            </section>
          </div>
        </section>
      </div>

redis介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

给查询banner添加redis缓存

第一步、创建redis配置类,写到common里面
1、引入springboot整合redis相关依赖
在common的pom文件中

  <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- spring2.X集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.0</version>
        </dependency>

2、创建redis缓存配置类
在common子模块service_base模块下


@EnableCaching //开启缓存
@Configuration  //配置类
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

3、在查询所有banner的方法上面添加缓存注解@Cacheable
在这里插入图片描述
CrmBannerServiceImpl实现类
在这里插入图片描述
4、启动redis服务
在这里插入图片描述
找到redis配置文件
在这里插入图片描述
使用命令 指定配置文件启动redis

cd /usr/local/bin
redis-server /usr/local/bin/kconfig/redis.conf

表示启动成功
在这里插入图片描述
查看里面的值
在这里插入图片描述

在这里插入图片描述
5、修改service_cms的配置文件

#redis中的配置
spring.redis.host=192.168.2.102
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000

spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0

启动程序测试
redis中有了缓存

127.0.0.1:6379> keys *
1) "banner::selectIndexList"
127.0.0.1:6379> 

查看缓存中的内容
在这里插入图片描述

给查课程和名师加上redis缓存

service_base中的配置类之前已经写过了
1、先在配置文件中加入redis的配置

#redis中的配置
spring.redis.host=192.168.2.102
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000

spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0

2、把原来写在controller中的代码移到service实现类中,因为@Cacheable要加在返回list或者其他值的的方法上而controller中返回的是json数据
IndexFrontController

@RestController
@CrossOrigin
@RequestMapping("/eduservice/indexfront")
public class IndexFrontController {
    @Autowired
    private EduCourseService courseService;

    @Autowired
    private EduTeacherService teacherService;
    //查询前8条热门课程 查询前四条名师
    @GetMapping("index")
    public R index() {
        //查询前8条热门课程
        List<EduCourse> eduList = courseService.selectHotCourse();
        //查询前四条名师
        List<EduTeacher> teacherList = teacherService.selectHotTeacher();
        return R.ok().data("eduList", eduList).data("teacherList", teacherList);
    }
}

EduCourseService接口

   /**
     * 查询8个热门课程
     * @return
     */
    List<EduCourse> selectHotCourse();

EduCourseService实现类中添加redis的注解

 @Cacheable(key = "'selectHotCourse'",value = "course")
    @Override
    public List<EduCourse> selectHotCourse() {
        QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
        wrapper.orderByDesc("id");
        wrapper.last("limit 8");
        List<EduCourse> eduCourses = baseMapper.selectList(wrapper);
        return eduCourses;
    }

EduTeacherService接口

  /**
     * 查询热门教师
     * @return
     */
    List<EduTeacher> selectHotTeacher();

EduTeacherServiceImpl

@Cacheable(key = "'selectHotTeacher'",value = "teacher")
@Override
public List<EduTeacher> selectHotTeacher() {
    QueryWrapper<EduTeacher> wrapperTeacher = new QueryWrapper<>();
    wrapperTeacher.orderByDesc("id");
    wrapperTeacher.last("limit 4");
    List<EduTeacher> teacherList = baseMapper.selectList(wrapperTeacher);
    return teacherList;
}

测试redis添加成功
在这里插入图片描述

day12

登录业务介绍-单点登录

单一服务器模式登录介绍
在这里插入图片描述

单点登录业务介绍
在这里插入图片描述
单点登录的三种常见方式:
在这里插入图片描述

jwt介绍

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
5、JWT问题和趋势

•JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。

•生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库

•存储在客户端,不占用服务端的内存资源

•JWT默认不加密,但可以加密。生成原始令牌后,可以再次对其进行加密。

•当JWT未加密时,一些私密数据无法通过JWT传输。
•JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。

•JWT本身包含认证信息,token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。

•为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。

整合jwt-放到公共包

一、先引入依赖
在common_utils里面的pom

<dependencies>
        <!-- JWT-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
    </dependencies>

二、在common_utils内创建JWT工具类

public class JwtUtils {

    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    public static String getJwtToken(String id, String nickname){

        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("bugu-user")//分类
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("id", id)//设置token的主体部分存储用户信息
                .claim("nickname", nickname)
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token获取会员id
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
}

阿里云短信服务介绍

在这里插入图片描述

在这里插入图片描述

申请模板
在这里插入图片描述

整合阿里云短信服务,注册时候发送手机验证码

1、在service创建子模块service_msm
在这里插入图片描述

2、创建包结构创建controller和service
创建配置文件和启动类
在这里插入图片描述
配置类MsmApplication

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //默认不加载数据库配置
@ComponentScan(basePackages = {"com.yhn"})
public class MsmApplication {
    public static void main(String[] args) {
        SpringApplication.run(MsmApplication.class, args);
    }
}

3、在service_msm包的pom文件中引入依赖

 <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
        </dependency>
    </dependencies>

4、在service_msm模块下新建包utils
里面创建一个生成随机数的工具类


/**
 * 获取随机数
 * 
 * @author qianyi
 *
 */
public class RandomUtil {

	private static final Random random = new Random();

	private static final DecimalFormat fourdf = new DecimalFormat("0000");

	private static final DecimalFormat sixdf = new DecimalFormat("000000");

	public static String getFourBitRandom() {
		return fourdf.format(random.nextInt(10000));
	}

	public static String getSixBitRandom() {
		return sixdf.format(random.nextInt(1000000));
	}

	/**
	 * 给定数组,抽取n个数据
	 * @param list
	 * @param n
	 * @return
	 */
	public static ArrayList getRandom(List list, int n) {

		Random random = new Random();

		HashMap<Object, Object> hashMap = new HashMap<Object, Object>();

		// 生成随机数字并存入HashMap
		for (int i = 0; i < list.size(); i++) {

			int number = random.nextInt(100) + 1;

			hashMap.put(number, i);
		}

		// 从HashMap导入数组
		Object[] robjs = hashMap.values().toArray();

		ArrayList r = new ArrayList();

		// 遍历数组并打印数据
		for (int i = 0; i < n; i++) {
			r.add(list.get((int) robjs[i]));
			System.out.print(list.get((int) robjs[i]) + "\t");
		}
		System.out.print("\n");
		return r;
	}
}

5、编写controller

@RestController
@RequestMapping("/edumsm/msm")
@CrossOrigin
public class MsmController {

    @Autowired
    private MsmService msmService;

    //发送短信的方法
    @GetMapping("send/{phone}")
    public R sendMsm(@PathVariable String phone) {
       //生成随机的值,传递阿里云进行发送
        String code = RandomUtil.getFourBitRandom();
        Map<String, Object> param = new HashMap<>();
        param.put("code", code);
        //调用service发送短信的方法
        boolean isSend = msmService.send(param, phone);
        if (isSend) {
            return R.ok();
        } else {
            return R.error().message("短信发送失败");
        }

    }

}


6、编写service
MsmService接口

public interface MsmService {
    /**
     * 发送短信的方法
     * @param param
     * @param phone
     * @return
     */
    boolean send(Map<String, Object> param, String phone);
}

MsmServiceImpl实现类

@Service
public class MsmServiceImpl implements MsmService {

    @Override
    public boolean send(Map<String, Object> param, String phone) {
        //如果手机号为空返回false
        if(StringUtils.isEmpty(phone)) return false;

        DefaultProfile profile =
                DefaultProfile.getProfile("default", "LTAI4GJ8qrUvsDHAWoeL5QLR", "50UPe6R0lUhFTZolVNn4G5DctF2v0U");
        IAcsClient client = new DefaultAcsClient(profile);

        //设置相关参数
        CommonRequest request = new CommonRequest();
        //request.setProtocol(ProtocolType.HTTPS);
        request.setMethod(MethodType.POST);
        request.setDomain("dysmsapi.aliyuncs.com");
        request.setVersion("2017-05-25");
        request.setAction("SendSms");

        //设置发送相关的参数
        request.putQueryParameter("PhoneNumbers",phone); //手机号
        request.putQueryParameter("SignName","布谷在线教育网站"); //申请阿里云 签名名称
        request.putQueryParameter("TemplateCode","SMS_180051135"); //申请阿里云 模板code
        request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param)); //验证码数据,转换json数据传递

        try {
            //最终发送
            CommonResponse response = client.getCommonResponse(request);
            boolean success = response.getHttpResponse().isSuccess();
            return success;
        }catch(Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

7、通过redis解决验证码有效时间
修改controller

@RestController
@RequestMapping("/edumsm/msm")
@CrossOrigin
public class MsmController {

    @Autowired
    private MsmService msmService;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    //发送短信的方法
    @GetMapping("send/{phone}")

    public R sendMsm(@PathVariable String phone) {
        //1 先从redis获取验证码,如果获取到直接返回
        String code = redisTemplate.opsForValue().get(phone);
        if (StringUtils.isEmpty(code)) {
            return R.ok();
        }

        //生成随机的值,传递阿里云进行发送
        code = RandomUtil.getFourBitRandom();
        Map<String, Object> param = new HashMap<>();
        param.put("code", code);
        //调用service发送短信的方法
        boolean isSend = msmService.send(param, phone);
        if (isSend) {
            //如果发送成功把发送成功验证码放到redis里面
            //设置有效时间
            redisTemplate.opsForValue().set(phone, code,5, TimeUnit.MINUTES);
            return R.ok();
        } else {
            return R.error().message("短信发送失败");
        }

    }

}


整合腾讯云短信服务

先申请一个个人的公众号,然后在腾讯云的短信里创建公众号类型的签名

在这里插入图片描述

在这里插入图片描述

一、创建两个工具类
1、生成四位随机字符串的工具类

/**生成四位随机字符串的工具类
 * @author YHN
 * @create 2021-03-14 17:27
 */

public class keyUtil {
    public static String keyUtils() {
        String str="0123456789";
        StringBuilder st=new StringBuilder(4);
        for(int i=0;i<4;i++){
            char ch=str.charAt(new Random().nextInt(str.length()));
            st.append(ch);
        }
        String findkey=st.toString().toLowerCase();
        return findkey;
    }
}

2、发生短信的工具类

package com.yhn.msmservice.utils;

import com.github.qcloudsms.SmsSingleSender;
import com.github.qcloudsms.SmsSingleSenderResult;
import org.json.JSONException;
import org.json.JSONObject;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.xml.ws.http.HTTPException;
import java.io.IOException;

/**发生短信的工具类
 * @author YHN
 * @create 2021-03-14 16:57
 */
public class SmsUtil {
    
    public static String sendSMS(HttpServletRequest request, String phoneNumber,String code) {
        String reStr = ""; //定义返回值
        // 短信应用SDK AppID  1400开头
        int appid = 1400494### ;
        // 短信应用SDK AppKey 在短信应用里面找
        String appkey = "ea628068be870c7c83982731d7d48###";
        // 短信模板ID,需要在短信应用中申请
        int templateId = 892###;
        // 签名,使用的是签名内容,而不是签名ID
        String smsSign = "布谷课堂吧";
        //随机生成四位验证码的工具类
//        String code = keyUtil.keyUtils();
        try {
            //参数,一定要对应短信模板中的参数顺序和个数,
            String[] params = {code};
            //创建ssender对象
            SmsSingleSender ssender = new SmsSingleSender(appid, appkey);
            //发送
            SmsSingleSenderResult result = ssender.sendWithParam("86", phoneNumber,templateId, params, smsSign, "", "");
            if(result.result!=0){
                reStr = "error";
            }
            // 签名参数未提供或者为空时,会使用默认签名发送短信
            HttpSession session = request.getSession();
            //JSONObject存入数据
            JSONObject json = new JSONObject();
            json.put("Code", code);//存入验证码
            json.put("createTime", System.currentTimeMillis());//存入发送短信验证码的时间
            // 将验证码和短信发送时间码存入SESSION
            request.getSession().setAttribute("MsCode", json);
            reStr = "success";
        } catch (HTTPException e) {
            // HTTP响应码错误
            e.printStackTrace();
        } catch (JSONException e) {
            // json解析错误
            e.printStackTrace();
        } catch (IOException e) {
            // 网络IO错误
            e.printStackTrace();
        }catch (Exception e) {
            // 网络IO错误
            e.printStackTrace();
        }
        return reStr;
    }

}

二、编写controller

package com.yhn.msmservice.controller;

import com.yhn.commonutils.R;
import com.yhn.msmservice.utils.SmsUtil;
import com.yhn.msmservice.utils.keyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
 * @author YHN
 * @create 2021-03-14 20:32
 */
@RestController
@RequestMapping("/edumsm/msm")
@CrossOrigin
public class MsmController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    //点击发送验证码
    @GetMapping("sendMs/{phoneNumber}")
    public R sendMs (@PathVariable String phoneNumber,HttpServletRequest request){
        //1 先从redis获取验证码,如果获取到直接返回
        String code = redisTemplate.opsForValue().get(phoneNumber);
        if (!StringUtils.isEmpty(code)) {
            return R.ok();
        }
        code = keyUtil.keyUtils();
        if(phoneNumber!=null&&!phoneNumber.equals("")){
            String s = SmsUtil.sendSMS(request,phoneNumber,code);
            if (s == "success") {
                //如果发送成功把发送成功验证码放到redis里面
                //设置有效时间
                redisTemplate.opsForValue().set(phoneNumber, code,5, TimeUnit.MINUTES);
                return R.ok();
            } else {
                return R.error().message("短信发送失败");
            }
        }else{
            return R.error().message("短信发送失败");
        }
    }

}

如果没有redis把下面两处删除就行
在这里插入图片描述
在这里插入图片描述

三、配置文件
application.properties

# 服务端口
server.port=8005
# 服务名
spring.application.name=service-msm

# 环境设置:dev开发环境、test、prod生产环境
spring.profiles.active=dev

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/bugu?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

# 设置日志级别
#logging.level.root=info

#redis中的配置
spring.redis.host=192.168.2.102
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000

spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0



#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

#mybatis日志
#mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

#配置mapper.xml文件的路径
#mybatis-plus.mapper-locations=classpath:com/yhn/eduservice/mapper/xml/*.xml

# nacos服务地址 千万不要写错
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

# 开启熔断机制
feign.hystrix.enabled=true
# 设置hystrix超时时间 默认1000s
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=6000

登录功能-接口

一、创建service_ucenter
二、创建数据库表
在这里插入图片描述
三、在service_ucenter使用代码生成器
修改绝对路径
在这里插入图片描述
修改包名
在这里插入图片描述
修改要连的数据库表名
在这里插入图片描述
四、把配置文件复制过来
在这里插入图片描述
在这里插入图片描述
五、编写启动类

@ComponentScan({"com.yhn"})
@SpringBootApplication
@MapperScan("com.yhn.educenter.mapper")//因为需要用到mapper配置文件
public class UcenterApplication {
    public static void main(String[] args) {
        SpringApplication.run(UcenterApplication.class, args);
    }
}

六、在工具类中加入MD5加密加密工具类给密码进行加密
common_utils包下


/**
 * MD5加密方式给密码进行加密
 */
public final class MD5 {

    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }

    public static void main(String[] args) {
        System.out.println(MD5.encrypt("111111"));
    }

}

七、编写controller
UcenterMemberController.java

@RestController
@RequestMapping("/educenter/member")
@CrossOrigin
public class UcenterMemberController {
    @Autowired
    private UcenterMemberService memberService;

    //登录
    @PostMapping("login")
    public R loginUser(@RequestBody UcenterMember member) {
        //member对象里面封装的是从前端返回的手机号和密码
        //调用service方法实现登录
        //返回token值使用jwt生成
        String token = memberService.login(member);
        return R.ok().data("token", token);
    }

    //注册
    

}


八、编写service
UcenterMemberService 接口

public interface UcenterMemberService extends IService<UcenterMember> {

    /**
     * 登录的方法
     * @param member
     * @return
     */
    String login(UcenterMember member);
}

UcenterMemberServiceImpl 实现类

@Service
public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService {

    @Override
    public String login(UcenterMember member) {

        //获取登录手机号和密码
        String mobile = member.getMobile();
        String password = member.getPassword();

        //手机号和密码非空的判断
        if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
            throw new BuguException(20001, "登录失败");
        }
        //判断手机号是否正确
        QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile", mobile);
        UcenterMember mobileMember = baseMapper.selectOne(wrapper);
        //判断查询对象是否为空
        if (mobileMember == null) { //没有这个手机号
            throw new BuguException(20001, "登录失败");
         }

         //判断密码
        //因为存储到数据库密码肯定要加密
        //把输入的密码进行加密,再和数据库密码进行比较
        //加密方式 MD5
        if (!MD5.encrypt(password).equals(mobileMember.getPassword())) {
            throw new BuguException(20001, "登录失败");
        }
         //用户是否禁用
        if (mobileMember.getIsDisabled()) {
            throw new BuguException(20001, "登录失败");
        }

        //登录成功
        //生成token字符串,使用jwt工具类
        String jwtToken = JwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname());
        return jwtToken;


    }
  }

在这里插入图片描述

注册功能-接口

一、在entity包下创建vo包,包内创建一个实体类
原有的实体类中没有验证码

@Data
public class RegisterVo {
    @ApiModelProperty(value = "昵称")
    private String nickname;
    @ApiModelProperty(value = "手机号")
    private String mobile;
    @ApiModelProperty(value = "密码")
    private String password;
    @ApiModelProperty(value = "验证码")
    private String code;
}

二、给UcenterMember实体类中的时间加上默认填充
在这里插入图片描述
三、编写controller
UcenterMemberController

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

四、编写service
UcenterMemberService接口

   /**
     * 注册的方法
     * @param registerVo
     */
    void register(RegisterVo registerVo);

UcenterMemberServiceImpl实现类

   @Autowired
    private RedisTemplate<String,String> redisTemplate;//没有泛型不能得到string类型的返回值
    
    @Override
    public void register(RegisterVo registerVo) {
        //获取注册的数据
        String code = registerVo.getCode();//验证码
        String mobile = registerVo.getMobile();//手机号
        String nickname = registerVo.getNickname();//昵称
        String password = registerVo.getPassword();//密码

        //非空判断
        //非空判断
        if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)
                || StringUtils.isEmpty(code) || StringUtils.isEmpty(nickname)) {
            throw new BuguException(20001,"注册失败");
        }
        //判断验证码
        //获取redis验证码
        String redisCode = redisTemplate.opsForValue().get(mobile);
        if(!code.equals(redisCode)) {
            throw new BuguException(20001,"注册失败");
        }

        //判断手机号是否重复,表里面存在相同手机号不进行添加
        QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile",mobile);
        Integer count = baseMapper.selectCount(wrapper);
        if(count > 0) {
            throw new BuguException(20001,"注册失败");
        }

        //数据添加数据库中
        UcenterMember member = new UcenterMember();
        member.setMobile(mobile);
        member.setNickname(nickname);
        member.setPassword(MD5.encrypt(password));//密码需要加密的
        member.setIsDisabled(false);//用户不禁用
        member.setAvatar("http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132");//头像
        baseMapper.insert(member);
    }

根据token字符串获取用户信息接口

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

    }

整合前端登录和注册页面

登录和注册静态页面的基本搭建

一、在nuxt环境安装插件

npm install element-ui

npm install vue-qriously # 为了后面下载支付二维码使用

二、在nuxt环境中使用element-ui,修改nuxt-swiper-plugin.js
引入插件

import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr' //轮播图的插件
import VueQriously from 'vue-qriously' //下载微信支付二维码的插件
import ElementUI from 'element-ui' //element-ui的全部组件
import 'element-ui/lib/theme-chalk/index.css'//element-ui的css
Vue.use(ElementUI) //使用elementUI
Vue.use(VueQriously)
Vue.use(VueAwesomeSwiper)

三、整合注册页面
1、在layouts文件夹中创建布局页面
登录注册的时候就会使用这个布局
sign.vue

<template>
  <div class="sign">
    <!--标题-->
    <div class="logo">
      <img src="~/assets/img/logo.png" alt="logo">
    </div>
    <!--表单-->
    <nuxt/>
  </div>
</template>

2、修改登录和注册超链接地址
在layouts里面的default.vue修改
在这里插入图片描述
修改后点击注册就会到pages的里面找一个register.vue的页面
3、创建一个注册页面
注册页面

<template>
  <div class="main">
    <div class="title">
      <a href="/login">登录</a>
      <span>·</span>
      <a class="active" href="/register">注册</a>
    </div>

    <div class="sign-up-container">
      <el-form ref="userForm" :model="params">

        <el-form-item class="input-prepend restyle" prop="nickname" :rules="[{ required: true, message: '请输入你的昵称', trigger: 'blur' }]">
          <div>
            <el-input type="text" placeholder="你的昵称" v-model="params.nickname"/>
            <i class="iconfont icon-user"/>
          </div>
        </el-form-item>

        <el-form-item class="input-prepend restyle no-radius" prop="mobile" :rules="[{ required: true, message: '请输入手机号码', trigger: 'blur' },{validator: checkPhone, trigger: 'blur'}]">
          <div>
            <el-input type="text" placeholder="手机号" v-model="params.mobile"/>
            <i class="iconfont icon-phone"/>
          </div>
        </el-form-item>

        <el-form-item class="input-prepend restyle no-radius" prop="code" :rules="[{ required: true, message: '请输入验证码', trigger: 'blur' }]">
          <div style="width: 100%;display: block;float: left;position: relative">
            <el-input type="text" placeholder="验证码" v-model="params.code"/>
            <i class="iconfont icon-phone"/>
          </div>
          <div class="btn" style="position:absolute;right: 0;top: 6px;width: 40%;">
            <a href="javascript:" type="button" @click="getCodeFun()" :value="codeTest" style="border: none;background-color: none">{{codeTest}}</a>
          </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="params.password"/>
            <i class="iconfont icon-password"/>
          </div>
        </el-form-item>

        <div class="btn">
          <input type="button" class="sign-up-button" value="注册" @click="submitRegister()">
        </div>
        <p class="sign-up-msg">
          点击 “注册” 即表示您同意并愿意遵守简书
          <br>
          <a target="_blank" href="http://www.jianshu.com/p/c44d171298ce">用户协议</a><a target="_blank" href="http://www.jianshu.com/p/2ov8x3">隐私政策</a></p>
      </el-form>
      <!-- 更多注册方式 -->
      <div class="more-sign">
        <h6>社交帐号直接注册</h6>
        <ul>
          <li><a id="weixin" class="weixin" target="_blank" href="http://huaan.free.idcfengye.com/api/ucenter/wx/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 registerApi from '@/api/register'

  export default {
    layout: 'sign',
    data() {
      return {
        params: { //封装注册输入数据
          mobile: '',
          code: '',  //验证码
          nickname: '',
          password: ''
        },
        sending: true,      //是否发送验证码
        second: 60,        //倒计时间
        codeTest: '获取验证码'
      }
    },
    methods: {
    }
  }
</script>

在这里插入图片描述
四、登录页面
在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'
  export default {
    layout: 'sign',

    data () {
      return {
        user:{
          mobile:'',
          password:''
        },
        loginInfo:{}
      }
    },

    methods: {

    }
  }
</script>
<style>
   .el-form-item__error{
    z-index: 9999999;
  }
</style>

在这里插入图片描述

注册前端整合

一、在api文件夹中创建注册的js文件,定义接口
register.js

import request from '@/utils/request'

export default {
    //根据手机号发验证码
  sendCode(phone) {
    return request({
      url: `/edumsm/msm/send/${phone}`,
      method: 'get'
    })
  },
  //注册的方法
  registerMember(formItem){
    return request({
        url: `/educenter/member/register`,
        method: 'post',
        data:formItem
      })
  }
}

二、在页面中调用
register.vue
1、先引入register.js

  import registerApi from '@/api/register'

2、在data中定义数据

  export default {
    layout: 'sign',
    data() {
      return {
        params: { //封装注册输入数据
          mobile: '',
          code: '',  //验证码
          nickname: '',
          password: ''
        },
        sending: true,      //是否发送验证码
        second: 60,        //倒计时间
        codeTest: '获取验证码'
      }
    },

3、编写方法

  methods: {
     
       //注册提交的方法
       submitRegister() {
         registerApi.registerMember(this.params)
          .then(response => {
            //提示注册成功
              this.$message({
                type: 'success',
                message: "注册成功"
              })
            //跳转登录页面
            this.$router.push({path:'/login'})
              
          })
       },
       timeDown() {
        let result = setInterval(() => {
          --this.second;
          this.codeTest = this.second
          if (this.second < 1) {
            clearInterval(result);
            this.sending = true;
            //this.disabled = false;
            this.second = 60;
            this.codeTest = "获取验证码"
          }
        }, 1000);

      },
       //通过输入手机号发送验证码
       getCodeFun() {
         registerApi.sendCode(this.params.mobile)
          .then(response => {
              //发送之后不能再点发送
              this.sending = false
              //调用倒计时的方法
              this.timeDown()
          })
       },
     //自己定义的规则
      checkPhone (rule, value, callback) {
        //debugger
        if (!(/^1[34578]\d{9}$/.test(value))) {
          return callback(new Error('手机号码格式不正确'))
        }
        return callback()
      }
    }
  }
</script>

注意要修改ngnix中的端口检测

day13

登录前端整合

一、在api中新建login.js定义方法

import request from '@/utils/request'

export default {
    //登录的方法
  submitLoginUser(userInfo) {
    return request({
      url: `/educenter/member/login`,
      method: 'post',
      data:userInfo
    })
  },
  //根据token获取用户信息
  getLoginUserInfo(){
    return request({
        url: `/educenter/member/getMemberInfo`,
        method: 'get'
        
      })
  }
}

二、在登录页面进行方法的调用
1、下载插件

npm install js-cookie

2、引入api和添加方法

  import cookie from 'js-cookie'
 import loginApi from '@/api/login'
  //登录的方法
      submitLogin(){
         loginApi.submitLoginUser(this.user)
         .then(response=>{
           //获取到token字符串
           response.data.data.token
         })
          
      },

三、登录和登录成功之后页面显示数据实现过程分析
1、调用接口登录返回token字符串
2、把返回 的token字符串放到cookie里面

3、创建拦截器
判断cookie中是否有token字符串
如果有,把token字符串放到header(请求头)
在这里插入图片描述
写到utils的request.js中

import cookie from 'js-cookie'
//http request拦截器
service.interceptors.request.use(
  config=>{
    //debugger
    //判断cookie里面是否有名称是bugu_token数据
    if(cookie .get('bugu_token')){
      config.headers['token']=cookie.get('bugu_token');
    }
    return config
  },
  err=>{
    return Promise.reject(err);
  })

4、根据token值调用接口,根据token获取用户信息,为了首页面显示
把调用接口返回的用户信息也放到cookie里面

 //登录的方法
      submitLogin(){
         loginApi.submitLoginUser(this.user)
         .then(response=>{
           //第一、二步、获取到token字符串放到cookie里面
           //          cookie名称   参数值                  domain是作用范围
           cookie.set('bugu_token',response.data.data.token,{domain:'localhost'})
           
           //第四步 调用接口 根据token获取用户信息,为了首页面显示
           loginApi.getLoginUserInfo()
            .then(response=>{
              //获取返回的用户信息放到cookie中
               cookie.set('bugu_ucenter',response.data.data.userInfo,{domain:'localhost'})
               //跳转页面 /表示首页面 
               window.location.href='/';
            })
         })       
      },

5、在首页面显示用户信息
从第四步cookie获取用户信息
在default.vue中编写显示用户信息
定义数据,引入cookie,编写方法
后端都叫字符串前端才叫json

import cookie from 'js-cookie'
export default {
  data(){
    return{
       token:'',
        loginInfo: {
          id: '',
          age: '',
          avatar: '',
          mobile: '',
          nickname: '',
          sex: ''
        }
     }
  },
  created(){
  this.showInfo()
  },
  methods:{
    //创建方法,从cookie获取用户信息
    showInfo(){
      //从cookie获取用户信息
      var userStr =cookie.get("bugu_ucenter")
     //把字符串转换为json对象(js对象)
    // "{'AFE':20}"   "{AFE:20}"  AFE带引号是取不到的
     if(userStr){
       this.loginInfo = JSON.parse(userStr)
     }
      
}

  }

};
</script>

在页面中判断是否loginInfo中有id

 <!-- / nav -->
          <ul class="h-r-login">
              <li v-if="!loginInfo.id" id="no-login">
                  <a href="/login" title="登录">
                      <em class="icon18 login-icon">&nbsp;</em>
                      <span class="vam ml5">登录</span>
                  </a>
                  |
                  <a href="/register" title="注册">
                      <span class="vam ml5">注册</span>
                  </a>
              </li>
              <li v-if="loginInfo.id" id="is-login-one" class="mr10">
                  <a id="headerMsgCountId" href="#" title="消息">
                      <em class="icon18 news-icon">&nbsp;</em>
                  </a>
                  <q class="red-point" style="display: none">&nbsp;</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;登录后显示第23 li -->
          </ul>

在这里插入图片描述

退出的操作

在这里插入图片描述
退出的方法

 //退出
  logout(){
      //清空cookie值,把值的位置改为空
      //token字符串
      cookie.set('bugu_token','',{domain: 'localhost'})
      //      用户信息
      cookie.set('bugu_ucenter','',{domain: 'localhost'})
      //回到首页面
      window.location.href = "/";

  }

微信登录

OAuth2

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

微信扫描登录准备工作

1、注册一个开发者资质
https://open.weixin.qq.com/
在这里插入图片描述
在这里插入图片描述
需要的内容

wx:
  open:
    # 微信开放平台 appid
    appid: wxed9954c01bb89b47
    # 微信开放平台 appsecret
    appsecret: a7482517235173ddb4083788de60b90e

    # 微信开放平台 重定向url(guli.shop需要在微信开放平台配置)
    redirecturl: http://guli.shop/api/ucenter/wx/callback

生成二维码

1、在service-ucenter模块的配置文件

# 微信开放平台 appid
wx.open.app_id=wxed9954c01bb89b47
# 微信开放平台 appsecret
wx.open.app_secret=a7482517235173ddb4083788de60b90e
# 微信开放平台 重定向url(guli.shop需要在微信开放平台配置)
wx.open.redirect_url: http://guli.shop/api/ucenter/wx/callback

2、在service-ucenter模块的utils包下创建一个工具类
用于读取配置文件内容

package com.yhn.educenter.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**读取配置文件内容的工具类
 * @author YHN
 * @create 2021-03-14 23:46
 */

@Component
public class ConstantWxUtils 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;
    }
}

3、生成微信二维码


/**生成微信二维码
 * @author YHN
 * @create 2021-03-14 23:53
 */
@CrossOrigin
@Controller //只是请求地址,不需要返回数据  @RestController的作用是创建对象交给spring管理和通过response返回数据,
@RequestMapping("/api/ucenter/wx")
public class WxApiContorller {
    //1 生成微信二维码
    @GetMapping("login")
    public String getWXCode() {
        //固定地址,后面拼接参数 有局限性
//        String url = "https://open.weixin.qq.com/" +
//                "connect/qrconnect?appid="+ ConstantWxUtils.WX_OPEN_APP_ID+"&response_type=code";

        // 微信开放平台授权baseUrl  %s相当于?代表占位符
        String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
                "?appid=%s" +
                "&redirect_uri=%s" +
                "&response_type=code" +
                "&scope=snsapi_login" +
                "&state=%s" +
                "#wechat_redirect";

        //对redirect_url进行URLEncoder编码
        String redirectUrl = ConstantWxUtils.WX_OPEN_REDIRECT_URL;
        try {
            redirectUrl = URLEncoder.encode(redirectUrl, "utf-8");
        }catch(Exception e) {
        }

        //设置%s里面值
        String url = String.format(
                baseUrl,
                ConstantWxUtils.WX_OPEN_APP_ID,
                redirectUrl,
                "bugu"
        );

        //重定向到请求微信地址里面
        return "redirect:"+url;
    }
}

写@Controller启动服务后直接访问http://localhost:8006/api/ucenter/wx/login就能出来二维码。写@RestController访问http://localhost:8006/api/ucenter/wx/login返回的是二维码的地址

获取扫描人的信息分析

在这里插入图片描述

在这里插入图片描述

把service_ucenter配置文件里面的端口号改为8150
在这里插入图片描述
在这里插入图片描述

  // 2 获取扫描人信息,添加数据
    @GetMapping("callback")
    public String callback(String code,String state){
        System.out.println("code"+code);
        System.out.println("state"+state);
        return "redirect:http://localhost:3000";
    }

先测试一下是不是成功,访问 http://localhost:8150/api/ucenter/wx/login生成二维码
然后手机扫描,测试成功返回了信息,并且跳转到了3000

获取信息的步骤

在这里插入图片描述
1、添加在service_ucenter中添加依赖


    <dependencies>

        <!--httpclient-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <!--commons-io-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>
        <!--gson-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
    </dependencies>

2、在utils中添加一个工具类


/**
 *  依赖的jar包有:commons-lang-2.6.jar、httpclient-4.3.2.jar、httpcore-4.3.1.jar、commons-io-2.4.jar
 * @author zhaoyb
 *
 */
public class HttpClientUtils {

	public static final int connTimeout=10000;
	public static final int readTimeout=10000;
	public static final String charset="UTF-8";
	private static HttpClient client = null;

	static {
		PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
		cm.setMaxTotal(128);
		cm.setDefaultMaxPerRoute(128);
		client = HttpClients.custom().setConnectionManager(cm).build();
	}

	public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{
		return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
	}

	public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{
		return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
	}

	public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
			SocketTimeoutException, Exception {
		return postForm(url, params, null, connTimeout, readTimeout);
	}

	public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
			SocketTimeoutException, Exception {
		return postForm(url, params, null, connTimeout, readTimeout);
	}

	public static String get(String url) throws Exception {
		return get(url, charset, null, null);
	}

	public static String get(String url, String charset) throws Exception {
		return get(url, charset, connTimeout, readTimeout);
	}

	/**
	 * 发送一个 Post 请求, 使用指定的字符集编码.
	 *
	 * @param url
	 * @param body RequestBody
	 * @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
	 * @param charset 编码
	 * @param connTimeout 建立链接超时时间,毫秒.
	 * @param readTimeout 响应超时时间,毫秒.
	 * @return ResponseBody, 使用指定的字符集编码.
	 * @throws ConnectTimeoutException 建立链接超时异常
	 * @throws SocketTimeoutException  响应超时
	 * @throws Exception
	 */
	public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
			throws ConnectTimeoutException, SocketTimeoutException, Exception {
		HttpClient client = null;
		HttpPost post = new HttpPost(url);
		String result = "";
		try {
			if (StringUtils.isNotBlank(body)) {
				HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
				post.setEntity(entity);
			}
			// 设置参数
			Builder customReqConf = RequestConfig.custom();
			if (connTimeout != null) {
				customReqConf.setConnectTimeout(connTimeout);
			}
			if (readTimeout != null) {
				customReqConf.setSocketTimeout(readTimeout);
			}
			post.setConfig(customReqConf.build());

			HttpResponse res;
			if (url.startsWith("https")) {
				// 执行 Https 请求.
				client = createSSLInsecureClient();
				res = client.execute(post);
			} else {
				// 执行 Http 请求.
				client = HttpClientUtils.client;
				res = client.execute(post);
			}
			result = IOUtils.toString(res.getEntity().getContent(), charset);
		} finally {
			post.releaseConnection();
			if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {
				((CloseableHttpClient) client).close();
			}
		}
		return result;
	}


	/**
	 * 提交form表单
	 *
	 * @param url
	 * @param params
	 * @param connTimeout
	 * @param readTimeout
	 * @return
	 * @throws ConnectTimeoutException
	 * @throws SocketTimeoutException
	 * @throws Exception
	 */
	public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
			SocketTimeoutException, Exception {

		HttpClient client = null;
		HttpPost post = new HttpPost(url);
		try {
			if (params != null && !params.isEmpty()) {
				List<NameValuePair> formParams = new ArrayList<NameValuePair>();
				Set<Entry<String, String>> entrySet = params.entrySet();
				for (Entry<String, String> entry : entrySet) {
					formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
				}
				UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
				post.setEntity(entity);
			}

			if (headers != null && !headers.isEmpty()) {
				for (Entry<String, String> entry : headers.entrySet()) {
					post.addHeader(entry.getKey(), entry.getValue());
				}
			}
			// 设置参数
			Builder customReqConf = RequestConfig.custom();
			if (connTimeout != null) {
				customReqConf.setConnectTimeout(connTimeout);
			}
			if (readTimeout != null) {
				customReqConf.setSocketTimeout(readTimeout);
			}
			post.setConfig(customReqConf.build());
			HttpResponse res = null;
			if (url.startsWith("https")) {
				// 执行 Https 请求.
				client = createSSLInsecureClient();
				res = client.execute(post);
			} else {
				// 执行 Http 请求.
				client = HttpClientUtils.client;
				res = client.execute(post);
			}
			return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
		} finally {
			post.releaseConnection();
			if (url.startsWith("https") && client != null
					&& client instanceof CloseableHttpClient) {
				((CloseableHttpClient) client).close();
			}
		}
	}




	/**
	 * 发送一个 GET 请求
	 *
	 * @param url
	 * @param charset
	 * @param connTimeout  建立链接超时时间,毫秒.
	 * @param readTimeout  响应超时时间,毫秒.
	 * @return
	 * @throws ConnectTimeoutException   建立链接超时
	 * @throws SocketTimeoutException   响应超时
	 * @throws Exception
	 */
	public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
			throws ConnectTimeoutException,SocketTimeoutException, Exception {

		HttpClient client = null;
		HttpGet get = new HttpGet(url);
		String result = "";
		try {
			// 设置参数
			Builder customReqConf = RequestConfig.custom();
			if (connTimeout != null) {
				customReqConf.setConnectTimeout(connTimeout);
			}
			if (readTimeout != null) {
				customReqConf.setSocketTimeout(readTimeout);
			}
			get.setConfig(customReqConf.build());

			HttpResponse res = null;

			if (url.startsWith("https")) {
				// 执行 Https 请求.
				client = createSSLInsecureClient();
				res = client.execute(get);
			} else {
				// 执行 Http 请求.
				client = HttpClientUtils.client;
				res = client.execute(get);
			}

			result = IOUtils.toString(res.getEntity().getContent(), charset);
		} finally {
			get.releaseConnection();
			if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
				((CloseableHttpClient) client).close();
			}
		}
		return result;
	}


	/**
	 * 从 response 里获取 charset
	 *
	 * @param ressponse
	 * @return
	 */
	@SuppressWarnings("unused")
	private static String getCharsetFromResponse(HttpResponse ressponse) {
		// Content-Type:text/html; charset=GBK
		if (ressponse.getEntity() != null  && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
			String contentType = ressponse.getEntity().getContentType().getValue();
			if (contentType.contains("charset=")) {
				return contentType.substring(contentType.indexOf("charset=") + 8);
			}
		}
		return null;
	}



	/**
	 * 创建 SSL连接
	 * @return
	 * @throws GeneralSecurityException
	 */
	private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
		try {
			SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
				public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {
					return true;
				}
			}).build();

			SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {

				@Override
				public boolean verify(String arg0, SSLSession arg1) {
					return true;
				}

				@Override
				public void verify(String host, SSLSocket ssl)
						throws IOException {
				}

				@Override
				public void verify(String host, X509Certificate cert)
						throws SSLException {
				}

				@Override
				public void verify(String host, String[] cns,
								   String[] subjectAlts) throws SSLException {
				}

			});

			return HttpClients.custom().setSSLSocketFactory(sslsf).build();

		} catch (GeneralSecurityException e) {
			throw e;
		}
	}

	public static void main(String[] args) {
		try {
			String str= post("https://localhost:443/ssl/test.shtml","name=12&page=34","application/x-www-form-urlencoded", "UTF-8", 10000, 10000);
			//String str= get("https://localhost:443/ssl/test.shtml?name=12&page=34","GBK");
            /*Map<String,String> map = new HashMap<String,String>();
            map.put("name", "111");
            map.put("page", "222");
            String str= postForm("https://localhost:443/ssl/test.shtml",map,null, 10000, 10000);*/
			System.out.println(str);
		} catch (ConnectTimeoutException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (SocketTimeoutException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

编写controller

@CrossOrigin
@Controller //只是请求地址,不需要返回数据  @RestController的作用是创建对象交给spring管理和通过response返回数据,
@RequestMapping("/api/ucenter/wx")
public class WxApiContorller {

    @Autowired
    private UcenterMemberService memberService;
    
    // 2 获取扫描人信息,添加数据
    @GetMapping("callback")
    public String callback(String code,String state){
        try {
            //1 获取code值,临时票据,类似于验证码
            //2 拿着code请求 微信固定的地址,得到两个值 accsess_token 和 openid
            String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
                    "?appid=%s" +
                    "&secret=%s" +
                    "&code=%s" +
                    "&grant_type=authorization_code";
            //拼接三个参数 :id  秘钥 和 code值
            String accessTokenUrl = String.format(
                    baseAccessTokenUrl,
                    ConstantWxUtils.WX_OPEN_APP_ID,
                    ConstantWxUtils.WX_OPEN_APP_SECRET,
                    code
            );
            //请求这个拼接好的地址,得到返回两个值 accsess_token 和 openid
            //使用httpclient发送请求,得到返回结果json字符串
            String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);

            //从accessTokenInfo字符串获取出来两个值 accsess_token 和 openid
            //把accessTokenInfo字符串转换map集合,根据map里面key获取对应值
            //使用json转换工具 Gson
            Gson gson = new Gson();
            HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
            String access_token = (String)mapAccessToken.get("access_token");
            String openid = (String)mapAccessToken.get("openid");

            //把扫描人信息添加数据库里面
            //判断数据表里面是否存在相同微信信息,根据openid判断
            UcenterMember member = memberService.getOpenIdMember(openid);
            if(member == null) {//memeber是空,表没有相同微信数据,进行添加

                //3 拿着得到accsess_token 和 openid,再去请求微信提供固定的地址,获取到扫描人信息
                //访问微信的资源服务器,获取用户信息
                String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
                        "?access_token=%s" +
                        "&openid=%s";
                //拼接两个参数
                String userInfoUrl = String.format(
                        baseUserInfoUrl,
                        access_token,
                        openid
                );
                //发送请求
                String userInfo = HttpClientUtils.get(userInfoUrl);
                //获取返回userinfo字符串扫描人信息
                HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);
                String nickname = (String)userInfoMap.get("nickname");//昵称
                String headimgurl = (String)userInfoMap.get("headimgurl");//头像

                member = new UcenterMember();
                member.setOpenid(openid);
                member.setNickname(nickname);
                member.setAvatar(headimgurl);
                memberService.save(member);//添加到数据库
            }

            //使用jwt根据member对象生成token字符串
            String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
            //最后:返回首页面,通过路径传递token字符串
            return "redirect:http://localhost:3000?token="+jwtToken;
        }catch(Exception e) {
            throw new BuguException(20001,"登录失败");
        }
    }
 }

service层
UcenterMemberService接口

 /**
     * 根据openid判断
     * @param openid
     * @return
     */
    UcenterMember getOpenIdMember(String openid);

UcenterMemberServiceImpl实现类

//根据openid判断
    @Override
    public UcenterMember getOpenIdMember(String openid) {
        QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
        wrapper.eq("openid",openid);
        UcenterMember member = baseMapper.selectOne(wrapper);
        return member;
    }

现在的cookie只能用在localhost跨域无法显示
功能完善-扫描之后信息跨域显示
在这里插入图片描述
微信名字带有特殊符合的扫描登录不 了

扫描二维码之后带着token值跳转到首页
在这里插入图片描述

首页显示信息

之前的登录是把信息放到cookie中,这样是为了 在前端拦截器中把值放到head中能取值,我们也需要这样做
1、首页路径中有token字符串
获取首页路径里面的token字符串、
注意:

两种路径参数传递取值的方法
http://localhost:3000/edit/1234
this.$route.params.id

http://localhost:3000/?token=deyfdsfjlksjglksfg
this.$route.$route.query.token

两种参数传递取值的方式不一样
mport cookie from 'js-cookie'
import loginApi from '@/api/login'
export default {
  data(){
    return{
       token:'',
        loginInfo: {
          id: '',
          age: '',
          avatar: '',
          mobile: '',
          nickname: '',
          sex: ''
        }
     }
  },
created(){
    //获取路径里面token值
   this.toke= this.$route.query.token
   if(this.token){//判断路径中是否有token值
      this.wxLogin()
   }
   this.showInfo()
  },
  methods:{
    //微信登录显示的方法
    wxLogin(){
      //把token值放到cookie中
      cookie.set('bugu_token',this.token,{domain:'localhost'})
      cookie.set('bugu_ucenter','',{domain: 'localhost'})
      //调用接口,根据token值获取用户信息
      loginApi.getLoginUserInfo()
        .then(response =>{
          this.loginInfo=  response.data.data.userInfo
         cookie.set('bugu_ucenter',this.loginInfo,{domain: 'localhost'})
        })
    },

2、把获取token值放到cookie中
有前端拦截器,判断cookie是否有token,如果有把cookie里面的token值取出来放到head中能取值
在default页面中获取信息显示到页面

created(){
    //获取路径里面token值
   this.token= this.$route.query.token
   
   if(this.token){//判断路径中是否有token值
      this.wxLogin() //获取微信登录用户的信息
   }
   this.showInfo()//获取账号密码登录用户的信息
  },
  methods:{
    //微信登录显示的方法
    wxLogin(){
      //把token值放到cookie中
      
      cookie.set('bugu_token',this.token,{domain:'localhost'})
      cookie.set('bugu_ucenter','',{domain: 'localhost'})
      //调用接口,根据token值获取用户信息
      loginApi.getLoginUserInfo()
        .then(response =>{
          this.loginInfo=  response.data.data.userInfo
          console.log(this.loginInfo)
         cookie.set('bugu_ucenter',this.loginInfo,{domain: 'localhost'})
        console.log(cookie.get('bugu_ucenter')+'----xxxxxxxxxx---')
       })
    },

3、点击微信图片跳转到获取验证码界面
在这里插入图片描述

 methods: {
      submitWX(){//点击微信图片跳到获取验证码界面
       window.location.href='http://localhost:8150/api/ucenter/wx/login';
      },

day14

讲师分页查询接口-不用elementui

1、controller
TeacherFrontController

@RestController
@RequestMapping("/eduservice/teacherfront")
public class TeacherFrontController {
    @Autowired
    private EduTeacherService teacherService;

    //分页查询讲师的方法
    @PostMapping("getTeacherFrontList/{page}/{limit}")
    public R getTeacherFrontList(@PathVariable long page,@PathVariable long limit) {
        Page<EduTeacher> pageTeacher = new Page<>(page, limit);
        Map<String,Object> map=teacherService.getTeacherFrontList(pageTeacher);
     //和之前的分页有区别,做的底层一点不用element ui 需要返回分页所有数据
        return R.ok().data(map);

    }

}

2、service
EduTeacherService接口

 /**
     * 分页查询讲师的方法
     * @param pageTeacher
     * @return
     */
    Map<String, Object> getTeacherFrontList(Page<EduTeacher> pageTeacher);

EduTeacherServiceImpl实现类

 @Override
    public Map<String, Object> getTeacherFrontList(Page<EduTeacher> pageTeacher) {
        QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();
        wrapper.orderByDesc("id");
        //把分页的数据封装的pageTeacher对象
        baseMapper.selectPage(pageTeacher, wrapper);

        List<EduTeacher> records = pageTeacher.getRecords();
        long pages = pageTeacher.getPages();
        long current = pageTeacher.getCurrent();
        long size = pageTeacher.getSize();
        long total = pageTeacher.getTotal();
        boolean hasNext = pageTeacher.hasNext();//当前是否有下一页
        boolean hasPrevious = pageTeacher.hasPrevious();//当前是否有上一页

        //把分页数据获取出来,放到map集合
        Map<String, Object> map = new HashMap<>();
        map.put("items", records);
        map.put("current", current);
        map.put("pages", pages);
        map.put("size", size);
        map.put("total", total);
        map.put("hasNext", hasNext);
        map.put("hasPrevious", hasPrevious);

        //map返回
        return map;
    }

3、swagger测试成功

讲师分页查询前端

1、在api中定义方法
teacher.js

import request from '@/utils/request'

export default {
    //分页讲师查询的方法
  getTeacherList(page,limit) {
    return request({
      url: `/eduservice/teacherfront/getTeacherFrontList/${page}/${limit}`,
      method: 'post'
    })
  }

}

2、在页面中引入js文件,调用方法显示

import teacherApi from '@/api/teacher'
export default {
  // data(){ 这么写可以选现在换一种写法
  //   return{
  //     data:[]
  //   }
  // },

   //异步调用
   //params:相当于之前 this.$route.params.id 等价于 params.id
  asyncData({params,error}){
    return teacherApi.getTeacherList(1,8)
      .then(response=>{
        //response.data.data得到是返回过来的map
        //this.data=response.data.data
        return {data:response.data.data}
      })
  }
};

3、没有数据显示下面的界面
在这里插入图片描述
在这里插入图片描述
4、有数据把数据遍历显示
pages/teacher.index.vue

  <!-- 有数据就显示 -->
          <article v-if="data.total>0" class="i-teacher-list">
            <ul class="of">
              <li v-for="teacher in data.items" :key="teacher.id">
                <section class="i-teach-wrap">
                  <div class="i-teach-pic">
                    <a :href="'/teacher/'+teacher.id" :title="teacher.name" target="_blank">
                      <img :src="teacher.avatar" :alt="teacher.name">
                    </a>
                  </div>
                  <div class="mt10 hLh30 txtOf tac">
                    <a :href="'/teacher/'+teacher.id" :title="teacher.name" target="_blank" class="fsize18 c-666">{{teacher.name}}</a>
                  </div>
                  <div class="hLh30 txtOf tac">
                    <span class="fsize14 c-999">{{teacher.intro}}</span>
                  </div>
                  <div class="mt15 i-q-txt">
                    <p class="c-999 f-fA">{{teacher.career}}</p>
                  </div>
                </section>
              </li>
     
            </ul>
            <div class="clear"></div>
          </article>

添加分页

切换分页的方法

  methods:{
    //分页切换的方法
    //参数是页码数
    gotoPage(page) {
      teacherApi.getTeacherList(page,8)
        .then(response => {
          //teacherList在异步调用里面的return就相当于被定义可以直接用
          this.teacherList = response.data.data
        })
    }
  }

在前端中显示的分页的代码

<!-- 公共分页 开始 -->
        <div>
         <div class="paging">
          <!-- undisable这个class是否存在,取决于数据属性hasPrevious 
           @click.prevent表示a标签不会跳转而是去执行gotoPage方法-->
          <a
            :class="{undisable: !teacherList.hasPrevious}"
            href="#"
            title="首页"
            @click.prevent="gotoPage(1)">首页</a>

          <a
            :class="{undisable: !teacherList.hasPrevious}"
            href="#"
            title="前一页"
            @click.prevent="gotoPage(teacherList.current-1)">&lt;</a>

          <a
            v-for="page in teacherList.pages"
            :key="page"
            :class="{current: teacherList.current == page, undisable: teacherList.current == page}"
            :title="'第'+page+'页'"
            href="#"
            @click.prevent="gotoPage(page)">{{ page }}</a> 

          <a
            :class="{undisable: !teacherList.hasNext}"
            href="#"
            title="后一页"
            @click.prevent="gotoPage(teacherList.current+1)">&gt;</a>

          <a
            :class="{undisable: !teacherList.hasNext}"
            href="#"
            title="末页"
            @click.prevent="gotoPage(teacherList.pages)">末页</a>

          <div class="clear"/>
        </div>
      </div>
        <!-- 公共分页 结束 -->

在这里插入图片描述

讲师详情页面

1、修改讲师列表页面超链接,改为讲师id
在这里插入图片描述
href前面加:的作用,如果不加冒号访问的是/teacher/teacher.id,没有把英文id取出来,作为字符串显示。加了冒号之后访问的是/teacher/1209714998.。。。。
会把id传到url中
在这里插入图片描述

2、编写讲师详情接口
(1)根据讲师id查询讲师基本信息、
(2)根据讲师id查询讲师所有课程

front/TeacherFrontController

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

3、在api中定义方法
teacher.js

  //讲师详情的方法
  getTeacherInfo(id) {
    return request({
      url: `/eduservice/teacherfront/getTeacherFrontInfo/${id}`,
      method: 'get'
    })
  }

4、在_id.vue

<script>
import teacherApi from '@/api/teacher'
export default {
  //异步调用 只会执行一次
   //params:相当于之前 this.$route.params.id 等价于 params.id 获取路由中的id值
   //这个id要和文件_id对应
  asyncData({params,error}){
    return teacherApi.getTeacherInfo(params.id)
      .then(response=>{
        //this.data=response.data.data
        return {
          teacher:response.data.data.teacher,
          courseList:response.data.data.courseList
          }
      })
  },
};
</script>

5、 在页面中显示


<template>
  <div id="aCoursesList" class="bg-fa of">
    <!-- 讲师介绍 开始 -->
    <section class="container">
      <header class="comm-title">
        <h2 class="fl tac">
          <span class="c-333">讲师介绍</span>
        </h2>
      </header>
      <div class="t-infor-wrap">
        <!-- 讲师基本信息 -->
        <section class="fl t-infor-box c-desc-content">
          <div class="mt20 ml20">
            <section class="t-infor-pic">
              <img :src="teacher.avatar">
            </section>
            <h3 class="hLh30">
              <span class="fsize24 c-333">{{teacher.name}}&nbsp;
                {{ teacher.level===1?'高级讲师':'首席讲师' }}
              </span>
            </h3>
            <section class="mt10">
              <span class="t-tag-bg">{{teacher.intro}}</span>
            </section>
            <section class="t-infor-txt">
              <p
                class="mt20">{{teacher.career}}</p>
            </section>
            <div class="clear"></div>
          </div>
        </section>
        <div class="clear"></div>
      </div>
      <section class="mt30">
        <div>
          <header class="comm-title all-teacher-title c-course-content">
            <h2 class="fl tac">
              <span class="c-333">主讲课程</span>
            </h2>
            <section class="c-tab-title">
              <a href="javascript: void(0)">&nbsp;</a>
            </section>
          </header>
          <!-- /无数据提示 开始-->
          <section class="no-data-wrap" v-if="courseList.length==0">
            <em class="icon30 no-data-ico">&nbsp;</em>
            <span class="c-666 fsize14 ml10 vam">没有相关数据,小编正在努力整理中...</span>
          </section>
          <!-- /无数据提示 结束-->
          <article class="comm-course-list">
            <ul class="of">
              <li v-for="course in courseList" :key="course.id">
                <div class="cc-l-wrap">
                  <section class="course-img">
                    <img :src="course.cover" class="img-responsive" >
                    <div class="cc-mask">
                      <a href="#" title="开始学习" target="_blank" class="comm-btn c-btn-1">开始学习</a>
                    </div>
                  </section>
                  <h3 class="hLh30 txtOf mt10">
                    <a href="#" :title="course.title" target="_blank" class="course-title fsize18 c-333">{{course.title}}</a>
                  </h3>
                </div>
              </li>
             
            </ul>
            <div class="clear"></div>
          </article>
        </div>
      </section>
    </section>
    <!-- /讲师介绍 结束 -->
  </div>
</template>

测试
在这里插入图片描述

课程列表接口

课程条件查询带分页功能

在这里插入图片描述

1、创建vo对象封装条件数据
在entity包中建frontvo包在包内创建类

CourseFrontVo.java


@Data
public class CourseFrontVo {

    @ApiModelProperty(value = "课程名称")
    private String title;

    @ApiModelProperty(value = "讲师id")
    private String teacherId;

    @ApiModelProperty(value = "一级类别id")
    private String subjectParentId;

    @ApiModelProperty(value = "二级类别id")
    private String subjectId;

    @ApiModelProperty(value = "销量排序")
    private String buyCountSort;

    @ApiModelProperty(value = "最新时间排序")
    private String gmtCreateSort;

    @ApiModelProperty(value = "价格排序")
    private String priceSort;
}

2、编写controller
CourseFrontController

@RestController
@RequestMapping("/eduservice/coursefront")
@CrossOrigin
public class CourseFrontController {

    @Autowired
    private EduCourseService courseService;

    @Autowired
    private EduChapterService chapterService;

    //1 条件查询带分页查询课程
    @PostMapping("getFrontCourseList/{page}/{limit}")
    public R getFrontCourseList(@PathVariable long page, @PathVariable long limit,
                                @RequestBody(required = false) CourseFrontVo courseFrontVo) {
        Page<EduCourse> pageParam = new Page<>(page,limit);
        Map<String,Object> map = courseService.getCourseFrontList(pageParam,courseFrontVo);
        //返回分页所有数据
        return R.ok().data(map);
    }
}


3、编写service
EduCourseService接口

   /**
     * 条件查询带分页查询课程
     * @param pageParam
     * @param courseFrontVo
     * @return
     */
    Map<String, Object> getCourseFrontList(Page<EduCourse> pageParam, CourseFrontVo courseFrontVo);

EduCourseServiceImpl实现类

public Map<String, Object> getCourseFrontList(Page<EduCourse> pageParam, CourseFrontVo courseFrontVo) {
        //2 根据讲师id查询所讲课程
        QueryWrapper<EduCourse> wrapper = new QueryWrapper<>();
        //判断条件值是否为空,不为空拼接
        if(!StringUtils.isEmpty(courseFrontVo.getSubjectParentId())) { //一级分类
            wrapper.eq("subject_parent_id",courseFrontVo.getSubjectParentId());
        }
        if(!StringUtils.isEmpty(courseFrontVo.getSubjectId())) { //二级分类
            wrapper.eq("subject_id",courseFrontVo.getSubjectId());
        }
        if(!StringUtils.isEmpty(courseFrontVo.getBuyCountSort())) { //关注度
            wrapper.orderByDesc("buy_count");
        }
        if (!StringUtils.isEmpty(courseFrontVo.getGmtCreateSort())) { //最新
            wrapper.orderByDesc("gmt_create");
        }

        if (!StringUtils.isEmpty(courseFrontVo.getPriceSort())) {//价格
            wrapper.orderByDesc("price");
        }

        baseMapper.selectPage(pageParam,wrapper);

        List<EduCourse> records = pageParam.getRecords();
        long current = pageParam.getCurrent();
        long pages = pageParam.getPages();
        long size = pageParam.getSize();
        long total = pageParam.getTotal();
        boolean hasNext = pageParam.hasNext();//下一页
        boolean hasPrevious = pageParam.hasPrevious();//上一页

        //把分页数据获取出来,放到map集合
        Map<String, Object> map = new HashMap<>();
        map.put("items", records);
        map.put("current", current);
        map.put("pages", pages);
        map.put("size", size);
        map.put("total", total);
        map.put("hasNext", hasNext);
        map.put("hasPrevious", hasPrevious);

        //map返回
        return map;
    }

课程列表前端

1、在api中定义方法
course.js
先查询显示一级分类,查询课程列表并且分页显示

import request from '@/utils/request'

export default {
  //条件分页课程查询的方法
  getCourseList(page,limit,searchObj) {
    return request({
      url: `/eduservice/coursefront/getFrontCourseList/${page}/${limit}`,
      method: 'post',
      data: searchObj
    })
  },
  //查询所有分类的方法
  getAllSubject() {
    return request({
      url: '/eduservice/subject/getAllSubject', //之前写过的接口为了在后台显示树形课程列表
      method: 'get'
    })
  }

}

2、在页面中进行调用
course/index.vue

<template>
  <div id="aCoursesList" class="bg-fa of">
    <!-- /课程列表 开始 -->
    <section class="container">
      <header class="comm-title">
        <h2 class="fl tac">
          <span class="c-333">全部课程</span>
        </h2>
      </header>
      <section class="c-sort-box">
        <section class="c-s-dl">
          <dl>
            <dt>
              <span class="c-999 fsize14">课程类别</span>
            </dt>
            <dd class="c-s-dl-li">
              <ul class="clearfix">
                <li>
                  <a title="全部" href="http://localhost:3000/course">全部</a>
                </li>
                <li v-for="(item,index) in subjectNestedList" :key="index" :class="{active:oneIndex==index}">
                  <a :title="item.title" href="#" @click="searchOne(item.id,index)">{{item.title}}</a>
                </li>
               
              </ul>
            </dd>
          </dl>
          <dl>
            <dt>
              <span class="c-999 fsize14"></span>
            </dt>
            <dd class="c-s-dl-li">
              <ul class="clearfix">
                <li v-for="(item,index) in subSubjectList" :key="index" :class="{active:twoIndex==index}">
                  <a :title="item.title" href="#" @click="searchTwo(item.id,index)">{{item.title}}</a>
                </li>
               
              </ul>
            </dd>
          </dl>
          <div class="clear"></div>
        </section>
        <div class="js-wrap">
          <section class="fr">
            <span class="c-ccc">
              <i class="c-master f-fM">1</i>/
              <i class="c-666 f-fM">1</i>
            </span>
          </section>
          <section class="fl">
            <ol class="js-tap clearfix">
             <li :class="{'current bg-orange':buyCountSort!=''}">
                <a title="销量" href="javascript:void(0);" @click="searchBuyCount()">销量
                <span :class="{hide:buyCountSort==''}"></span>
                </a>
              </li>
              <li :class="{'current bg-orange':gmtCreateSort!=''}">
                <a title="最新" href="javascript:void(0);" @click="searchGmtCreate()">最新
                <span :class="{hide:gmtCreateSort==''}"></span>
                </a>
              </li>
              <li :class="{'current bg-orange':priceSort!=''}">
                <a title="价格" href="javascript:void(0);" @click="searchPrice()">价格&nbsp;
                  <span :class="{hide:priceSort==''}"></span>
                </a>
              </li>
            </ol>
          </section>
        </div>
        <div class="mt40">
          <!-- /无数据提示 开始-->
          <section class="no-data-wrap" v-if="data.total==0">
            <em class="icon30 no-data-ico">&nbsp;</em>
            <span class="c-666 fsize14 ml10 vam">没有相关数据,小编正在努力整理中...</span>
          </section>
          <!-- /无数据提示 结束-->
          <article  v-if="data.total>0" class="comm-course-list">
            <ul class="of" id="bna">
              <li v-for="item in data.items" :key="item.id">
                <div class="cc-l-wrap">
                  <section class="course-img">
                    <img :src="item.cover" class="img-responsive" :alt="item.title">
                    <div class="cc-mask">
                      <a :href="'/course/'+item.id" title="开始学习" class="comm-btn c-btn-1">开始学习</a>
                    </div>
                  </section>
                  <h3 class="hLh30 txtOf mt10">
                    <a :href="'/course/'+item.id" :title="item.title" class="course-title fsize18 c-333">{{item.title}}</a>
                  </h3>
                  <section class="mt10 hLh20 of">
                    <span v-if="Number(item.price) === 0" class="fr jgTag bg-green">
                      <i class="c-fff fsize12 f-fA">免费</i>
                    </span>
                    <span class="fl jgAttr c-ccc f-fA">
                      <i class="c-999 f-fA">9634人学习</i>
                      |
                      <i class="c-999 f-fA">9634评论</i>
                    </span>
                  </section>
                </div>
              </li>
              
            </ul>
            <div class="clear"></div>
          </article>
        </div>
        <!-- 公共分页 开始 -->
        <div>
      <div class="paging">
        <!-- undisable这个class是否存在,取决于数据属性hasPrevious -->
        <a
          :class="{undisable: !data.hasPrevious}"
          href="#"
          title="首页"
          @click.prevent="gotoPage(1)"></a>
        <a
          :class="{undisable: !data.hasPrevious}"
          href="#"
          title="前一页"
          @click.prevent="gotoPage(data.current-1)">&lt;</a>
        <a
          v-for="page in data.pages"
          :key="page"
          :class="{current: data.current == page, undisable: data.current == page}"
          :title="'第'+page+'页'"
          href="#"
          @click.prevent="gotoPage(page)">{{ page }}</a>
        <a
          :class="{undisable: !data.hasNext}"
          href="#"
          title="后一页"
          @click.prevent="gotoPage(data.current+1)">&gt;</a>
        <a
          :class="{undisable: !data.hasNext}"
          href="#"
          title="末页"
          @click.prevent="gotoPage(data.pages)"></a>
        <div class="clear"/>
      </div>
    </div>
      </section>
    </section>
    <!-- /课程列表 结束 -->
  </div>
</template>
<script>
import courseApi from '@/api/course'

export default {
  data() {
    return {
      page:1, //当前页
      data:{},  //课程列表
      subjectNestedList: [], // 一级分类列表
      subSubjectList: [], // 二级分类列表

      searchObj: {}, // 查询表单对象

      oneIndex:-1,
      twoIndex:-1,
      buyCountSort:"",
      gmtCreateSort:"",
      priceSort:""
    }
  },
  created() {
    //课程第一次查询
    this.initCourseFirst()
    //一级分类显示
    this.initSubject()
  },
  methods:{
    //1 查询第一页数据
    initCourseFirst() {
      courseApi.getCourseList(1,8,this.searchObj).then(response => {
        this.data = response.data.data
      })
    },

    //2 查询所有一级分类
    initSubject() {
      courseApi.getAllSubject()
        .then(response => {
          this.subjectNestedList = response.data.data.list
        })
    },

    //3 分页切换的方法
    gotoPage(page) {
      courseApi.getCourseList(page,8,this.searchObj).then(response => {
        this.data = response.data.data
      })
    }
  }
};
</script>
<style scoped>
  .active {
    background: #bdbdbd;
  }
  .hide {
    display: none;
  }
  .show {
    display: block;
  }
</style>

在这里插入图片描述

点击一级分类的时候,显示二级分类

在这里插入图片描述

  //4 点击某个一级分类,查询对应二级分类
    searchOne(subjectParentId,index) {
      //把传递index值赋值给oneIndex,为了active样式生效
      this.oneIndex = index
//查询一级分类的时候不需要二级分类把二级分类清空
      this.twoIndex = -1
      this.searchObj.subjectId = ""
      this.subSubjectList = []

      //把一级分类点击id值,赋值给searchObj
      this.searchObj.subjectParentId = subjectParentId
      //点击某个一级分类进行条件查询
      this.gotoPage(1)

      //拿着点击一级分类id 和 所有一级分类id进行比较,
      //如果id相同,从一级分类里面获取对应的二级分类
      for(let i=0;i<this.subjectNestedList.length;i++) {
        //获取每个一级分类
        var oneSubject = this.subjectNestedList[i]
        //比较id是否相同
        if(subjectParentId == oneSubject.id) {
          //从一级分类里面获取对应的二级分类
          this.subSubjectList = oneSubject.children
        }
      }
    },

点击分类的时候给分类加上样式背景色
在这里插入图片描述
在这里插入图片描述

<style scoped>
  .active {
    background: #fcb7b7;
  }
  .hide {
    display: none;
  }
  .show {
    display: block;
  }
</style>

点击二级分类查询

点击二级分类的时候也添加样式
同时绑定一个事件
在这里插入图片描述

  //5 点击某个二级分类实现查询
    searchTwo(subjectId,index) {
      //把index赋值,为了样式生效
      this.twoIndex = index
      //把二级分类点击id值,赋值给searchObj
      this.searchObj.subjectId = subjectId
      //点击某个二级分类进行条件查询
      this.gotoPage(1)
    },

排序

在这里插入图片描述

判断有没有点这个值点了就排序不点不排序
在这里插入图片描述


    //6 根据销量排序
    searchBuyCount() {
      //设置对应变量值,为了样式生效
      this.buyCountSort = "1"
      this.gmtCreateSort = ""
      this.priceSort = ""

      //把值赋值到searchObj
      this.searchObj.buyCountSort = this.buyCountSort
      this.searchObj.gmtCreateSort = this.gmtCreateSort;
      this.searchObj.priceSort = this.priceSort;

      //调用方法查询
      this.gotoPage(1)
    },

    //7 最新排序
    searchGmtCreate() {
      //设置对应变量值,为了样式生效
      this.buyCountSort = ""
      this.gmtCreateSort = "1"
      this.priceSort = ""

      //把值赋值到searchObj
      this.searchObj.buyCountSort = this.buyCountSort
      this.searchObj.gmtCreateSort = this.gmtCreateSort;
      this.searchObj.priceSort = this.priceSort;

      //调用方法查询
      this.gotoPage(1)
    },

    //8 价格排序
    searchPrice() {
      //设置对应变量值,为了样式生效
      this.buyCountSort = ""
      this.gmtCreateSort = ""
      this.priceSort = "1"

      //把值赋值到searchObj
      this.searchObj.buyCountSort = this.buyCountSort
      this.searchObj.gmtCreateSort = this.gmtCreateSort;
      this.searchObj.priceSort = this.priceSort;

      //调用方法查询
      this.gotoPage(1)
    },

课程详情

改超链接
点开始学习要跳到对应id值的课程详情页面中
在这里插入图片描述
在这里插入图片描述

课程详情的接口

一、编写sql语句
1、根据课程id查询课程信息
查询课程基本信息,课程分类,课程描述,所属讲师

SELECT 
  ec.id,
  ec.`title`,
  ec.`price`,
  ec.`lesson_num` AS lessonNum,
  ec.cover,
  ec.buy_count AS buyCount,
  ec.view_count AS viewCount,
  ecd.id AS teacherId,
  ecd.description,
  et.id AS teacherId,
  et.name AS teacherName,
  et.intro,
  et.avatar,
  es1.`id` AS subjectLevelOneId,
  es1.`title` AS subjectLevelOne,
  es2.`id` AS subjectLevelTwoId,
  es2.`title` AS subjectLevelTwo 
FROM
  edu_course ec 
  LEFT OUTER JOIN `edu_course_description` ecd 
    ON ec.id = ecd.`id` 
  LEFT OUTER JOIN `edu_teacher` et 
    ON ec.`teacher_id` = et.`id` 
  LEFT OUTER JOIN `edu_subject` es1 
    ON ec.`subject_parent_id` = es1.`id` 
  LEFT OUTER JOIN `edu_subject` es2 
    ON ec.`subject_id` = es2.`id` 
WHERE ec.id = 1192252213659774977

2、根据课程id查询章节小节–之前写过这个方法直接调用

二、创建一个实体类
entity/frontvo/CourseWebVo .java

@Data
public class CourseWebVo {

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

三、编写controller
CourseFrontController.java

 //2 课程详情的方法
    @GetMapping("getFrontCourseInfo/{courseId}")
    public R getFrontCourseInfo(@PathVariable String courseId) {
        //根据课程id,编写sql语句查询课程信息
        CourseWebVo courseWebVo = courseService.getBaseCourseInfo(courseId);

        //根据课程id查询章节和小节
        List<ChapterVo> chapterVideoList = chapterService.getChapterVideoByCourseId(courseId);

        return R.ok().data("courseWebVo",courseWebVo).data("chapterVideoList",chapterVideoList);
    }

四、编写service
EduCourseService接口

 /**
     * 查询课程详情的方法
     * @param courseId
     * @return
     */
    CourseWebVo getBaseCourseInfo(String courseId);

EduCourseServiceImpl

 @Override
    public CourseWebVo getBaseCourseInfo(String courseId) {
        return baseMapper.getBaseCoursefo(courseId);
    }

五、在mapper中创建方法
EduCourseMapper

 CourseWebVo getBaseCoursefo(String courseId);

六、编写sql语句
EduCourseMapper.xml

 <!--sql语句 根据课程id查询课基本信息             返回类型-->
    <select id="getBaseCourseInfo" resultType="com.yhn.eduservice.entity.frontvo.CourseWebVo">
        SELECT ec.id,ec.`title`,ec.`price`,ec.`lesson_num` AS lessonNum,ec.cover,
        ec.buy_count AS buyCount,
        ec.view_count AS viewCount,
        ecd.id AS teacherId,
        ecd.description,
        et.id AS teacherId,et.name AS teacherName,
        et.intro,et.avatar,
        es1.`id` AS subjectLevelOneId,
       es1.`title` AS subjectLevelOne,
       es2.`id` AS subjectLevelTwoId,
       es2.`title` AS subjectLevelTwo
        FROM edu_course ec
        LEFT OUTER JOIN `edu_course_description` ecd ON ec.id=ecd.`id`
        LEFT OUTER JOIN  `edu_teacher` et ON ec.`teacher_id`=et.`id`
        LEFT OUTER JOIN `edu_subject`  es1 ON ec.`subject_parent_id`=es1.`id`
        LEFT OUTER JOIN `edu_subject`  es2 ON ec.`subject_id`=es2.`id`
        WHERE ec.id=#{courseId}
    </select>

之前解决过mapper文件加载不进去的问题
在这里插入图片描述
在这里插入图片描述

整合阿里云视频播放器播放

一、创建接口,根据视频id获取视频播放凭证
service_vod模块下
VodController.java

  //根据视频id获取视频播放凭证
    @GetMapping("getPlayAuth/{id}")
    public R getPlayAuth(@PathVariable String id) {
        try {
            //创建初始化对象
            DefaultAcsClient client = InitObject.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);
            //创建获取凭证request和response对象
            GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
            //向request设置视频id
            request.setVideoId(id);
            //调用方法得到凭证
            GetVideoPlayAuthResponse response = client.getAcsResponse(request);
            String playAuth = response.getPlayAuth();
            return R.ok().data("playAuth", playAuth);

        } catch (Exception e) {
            throw new BuguException(20001, "获取凭证失败");
        }
    }

在setvice_edu的VideoVo中添加属性
在这里插入图片描述

二、前端
1、在course/_id中修改播放小节视频跳转的地址(通过动态路由)
在这里插入图片描述

在这里插入图片描述

2、在api中定义方法
vod.js

import request from '@/utils/request'
export default {
  getPlayAuth(vid) {
    return request({
      url: `/eduvod/video/getPlayAuth/${vid}`,
      method: 'get'
    })
  }

}

3、在页面中调用
在建文件/page/player/_vid.vue
在这里插入图片描述

4、在teacher/_id.vue中也给老师所讲课程加上跳转地址
在这里插入图片描述

day15

课程评论功能

后端接口

在这里插入图片描述
一、创建数据库表edu_comment
在这里插入图片描述
二、创建接口,创建两个方法
在这里插入图片描述

1、分页查询课程评论的方法
2、添加评论

UcenterMemberController

 //根据用户id查询用户信息
    @PostMapping("/getMemberInfoById/{memberId}")
    public UcenterMember getMemberInfoById(@PathVariable String memberId){
        UcenterMember member = memberService.getById(memberId);
        UcenterMember memberVo = new UcenterMember();
        BeanUtils.copyProperties(member,memberVo);

        return memberVo;
    }

2、application.properties

# nacos服务地址 千万不要写错
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

3、在service_ucenter模块的启动类上添加注解

@EnableDiscoveryClient
public class UcenterApplication {

4、在公共模块中创建封装返回给评论服务的对象

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="UcenterMember对象", description="会员表")
public class UcenterMemberVo implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "会员id")
    @TableId(value = "id", type = IdType.ID_WORKER_STR)
    private String id;

    @ApiModelProperty(value = "微信openid")
    private String openid;

    @ApiModelProperty(value = "手机号")
    private String mobile;

    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty(value = "昵称")
    private String nickname;

    @ApiModelProperty(value = "性别 1 女,2 男")
    private Integer sex;

    @ApiModelProperty(value = "年龄")
    private Integer age;

    @ApiModelProperty(value = "用户头像")
    private String avatar;

    @ApiModelProperty(value = "用户签名")
    private String sign;

    @ApiModelProperty(value = "是否禁用 1(true)已禁用,  0(false)未禁用")
    private Boolean isDisabled;

    @ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除")
    private Boolean isDeleted;

    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreate;

    @ApiModelProperty(value = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;


5、、在service_edu模块中创建 Feign远程调用类
UcenterClient接口

@Component
@FeignClient(name = "service-ucenter",fallback = UcenterClientImpl.class)
public interface UcenterClient {

    @PostMapping("/educenter/member/getMemberInfoById/{memberId}")
    public UcenterMemberVo getMemberInfoById(@PathVariable String memberId);

}

UcenterClientImpl实现类

@Component
public class UcenterClientImpl implements UcenterClient {
    @Override
    public UcenterMemberVo getMemberInfoById(String memberId) {
        //接口调用远程方法执行失败会执行这个备用方法
        return null;

    }
}

5、创建课程评论controller
EduCommentController

@RestController
@RequestMapping("/eduservice/comment")
@CrossOrigin
public class EduCommentController {

    @Autowired
    private EduCommentService eduCommentService;

    @Autowired
    private UcenterClient ucenterClient;
    //根据课程id_分页查询课程评论的方法
    @GetMapping("/getCommentPage/{page}/{limit}/{courseId}")
    public R getCommentPage(@PathVariable Long page, @PathVariable Long limit, @PathVariable String courseId){
        Page<EduComment> commentPage = new Page<>(page, limit);

        QueryWrapper<EduComment> wrapper = new QueryWrapper<>();
//        courseId = "1372520068382015489";
        //判断课程id是否为空
        if (!StringUtils.isEmpty(courseId)){
            wrapper.eq("course_id",courseId);
        }

        //按最新排序
        wrapper.orderByDesc("gmt_create");

        //数据会被封装到commentPage中
        eduCommentService.page(commentPage,wrapper);

        List<EduComment> commentList = commentPage.getRecords();
        long current = commentPage.getCurrent();//当前页
        long size = commentPage.getSize();//一页记录数
        long total = commentPage.getTotal();//总记录数
        long pages = commentPage.getPages();//总页数
        boolean hasPrevious = commentPage.hasPrevious();//是否有上页
        boolean hasNext = commentPage.hasNext();//是否有下页

        HashMap<String, Object> map = new HashMap<>();
        map.put("current",current);
        map.put("size",size);
        map.put("total",total);
        map.put("pages",pages);
        map.put("hasPrevious",hasPrevious);
        map.put("hasNext",hasNext);
        map.put("list",commentList);

        return R.ok().data(map);
    }

    //添加评论
    @PostMapping("/auth/addComment")
    public R addComment(HttpServletRequest request, @RequestBody EduComment eduComment){
        String memberId = JwtUtils.getMemberIdByJwtToken(request);
        //判断用户是否登录
        if (StringUtils.isEmpty(memberId)){
            throw new BuguException(20001,"请先登录");
        }
        eduComment.setMemberId(memberId);

        //远程调用ucenter根据用户id获取用户信息
        UcenterMemberVo memberVo = ucenterClient.getMemberInfoById(memberId);

        eduComment.setAvatar(memberVo.getAvatar());
        eduComment.setNickname(memberVo.getNickname());

        //保存评论
        eduCommentService.save(eduComment);

        return R.ok();
    }


前端实现

1、在api中定义方法
comment.js

import request from '@/utils/request'
export default {
    //根据课程分页查询评论
    getPageList(page, limit, courseId) {
        return request({
            url: `/eduservice/comment/getCommentPage/${page}/${limit}/${courseId}`,
            method: 'get'
        })
    },
    //添加评论
    addComment(comment) {
        return request({
            url: `/eduservice/comment/auth/addComment`,
            method: 'post',
            data: comment
        })
    }
}

2、在页面中调用-引入方法,定义数据,编写方法
把之前的异步方法删除重新写


import courseApi from '@/api/course'
import comment from "@/api/comment";
export default {
   created() {
    this.courseId = this.$route.params.id;
    // 获取课程详细信息
    this.getCourseInfo()
     this.initComment()
  },
  methods: {
    //获取课程详细信息
    getCourseInfo() {
      courseApi.getCourseInfo(this.courseId).then((resp) => {
        this.chapterVideoList = resp.data.data.chapterVideoList;
        this.courseWebVo = resp.data.data.courseWebVo;
       this.courseId = resp.data.data.courseWebVo.id;
       console.log(this.courseId)
         this.videoSourceId=resp.data.data.videoSourceId

      });
    },
    //初始化评论
    initComment() {
      comment
        .getPageList(this.page, this.limit, this.courseId)
        .then((response) => {
          this.data = response.data.data;
          console.log(response.data.data+"--------");
        });
    },
    //添加评论
    addComment() {
      this.comment.courseId = this.courseId;
      this.comment.teacherId = this.courseWebVo.teacherId;
      comment.addComment(this.comment).then((response) => {
        if (response.data.success) {
          this.$message({
            message: "评论成功",
            type: "success",
          });
          this.comment.content = "";
          this.initComment();
        }
      });
    },
    //分页查询
    gotoPage(page) {
      comment.getPageList(page, this.limit, this.courseId).then((response) => {
        this.data = response.data.data;
      });
    },
  },
  data() {
    return {
      chapterVideoList: [],
      courseWebVo: {
      },
      courseId: "1372520068382015489",
      data: {},
      page: 1,
      limit: 8,
      total: 10,
      videoSourceId:0,
      comment: {
        content: "",
        courseId: "",
        teacherId: "",
      },
      isbuyCourse: false,
    };
  }
  // ,
  //  asyncData({ params, error }) {
  //    return courseApi.getCourseInfo(params.id)
  //       .then(response => {
  //         return {
  //           courseWebVo: response.data.data.courseWebVo,
  //           chapterVideoList: response.data.data.chapterVideoList,
  //           videoSourceId: response.data.data.videoSourceId

  //         }
  //       })
  //  }
};
</script>

3、页面中显示

<div class="mt50 commentHtml">
        <div>
            <h6 class="c-c-content c-infor-title" id="i-art-comment">
                <span class="commentTitle">课程评论</span>
            </h6>
            <section class="lh-bj-list pr mt20 replyhtml">
                <ul>
                    <li class="unBr">
                        <aside class="noter-pic">
                            <img
                                width="50"
                                height="50"
                                class="picImg"
                                src="~/assets/img/avatar-boy.gif"
                                />
                        </aside>
                        <div class="of">
                            <section class="n-reply-wrap">
                                <fieldset>
                                    <textarea
                                              name=""
                                              v-model="comment.content"
                                              placeholder="输入您要评论的文字"
                                              id="commentContent"
                                              ></textarea>
                                </fieldset>
                                <p class="of mt5 tar pl10 pr10">
                                    <span class="fl"
                                          ><tt
                                              class="c-red commentContentmeg"
                                              style="display: none"
                                              ></tt
                                        ></span>
                                    <input
                                          type="button"
                                          @click="addComment()"
                                          value="回复"
                                          class="lh-reply-btn"
                                          />
                                </p>
                            </section>
                        </div>
                    </li>
                </ul>
            </section>
            <section class="">
                <section class="question-list lh-bj-list pr">
                    <ul class="pr10">
                        <li v-for="comment in data.list" :key="comment.id">
                            <aside class="noter-pic">
                                <img
                                    width="50"
                                    height="50"
                                    class="picImg"
                                    :src="comment.avatar"
                                    />
                            </aside>
                            <div class="of">
                                <span class="fl">
                                    <font class="fsize12 c-blue">{{ comment.nickname }}</font>
                                    <font class="fsize12 c-999 ml5">评论:</font></span
                                    >
                            </div>
                            <div class="noter-txt mt5">
                                <p>{{ comment.content }}</p>
                            </div>
                            <div class="of mt5">
                                <span class="fr"
                                      ><font class="fsize12 c-999ml5">{{
                                    comment.gmtCreate
                                    }}</font></span
                                    >
                            </div>
                        </li>
                    </ul>
                </section>
            </section>
            <!-- 公共分页 开始 -->
            <div class="paging">
                <!-- undisable这个class是否存在,取决于数据属性hasPrevious -->
                <a
                  :class="{ undisable: !data.hasPrevious }"
                  href="#"
                  title="首页"
                  @click.prevent="gotoPage(1)"
                  ></a
                    >
                <a
                  :class="{ undisable: !data.hasPrevious }"
                  href="#"
                  title="前一页"
                  @click.prevent="gotoPage(data.current - 1)"
                  >&lt;</a
                    >
                <a
                  v-for="page in data.pages"
                  :key="page"
                  :class="{
                          current: data.current == page,
                          undisable: data.current == page,
                          }"
                  :title="'第' + page + '页'"
                  href="#"
                  @click.prevent="gotoPage(page)"
                  >{{ page }}</a
                    >
                <a
                  :class="{ undisable: !data.hasNext }"
                  href="#"
                  title="后一页"
                  @click.prevent="gotoPage(data.current + 1)"
                  >&gt;</a
                    >
                <a
                  :class="{ undisable: !data.hasNext }"
                  href="#"
                  title="末页"
                  @click.prevent="gotoPage(data.pages)"
                  ></a
                    >
                <div class="clear" />
            </div>
            <!-- 公共分页 结束 -->
        </div>
    </div>

在这里插入图片描述

课程支付功能

  • 过程分析

在这里插入图片描述

一、创建数据表
在这里插入图片描述

二、新建一个支付模块service_order并且用代码生成器生成代码
注意:因为现在的项目是新建的,绝对路径发生了变化,所以要修改路径,不然就把代码生成到其他包中,现在项目的包内看不到生成文件,只是显示代码已经生成
原来
在这里插入图片描述
修改之后
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

创建生成订单接口

在这里插入图片描述
调用端和被调用端需要用同一个类UcenterMemberVo,CourseWebVoOrder
把这个两个类放到公共模块下
一、创建启动类
在这里插入图片描述
把用户信息放到了cookie中,拦截器又把信息放到了header中
通过token可以获取用户信息

后端通过request获取header中的信息

@SpringBootApplication
@EnableDiscoveryClient  //nacos注册
@EnableFeignClients    //feign调用服务
@MapperScan("com.yhn.eduorder.mapper")  //需要用到mapper
@ComponentScan(basePackages = {"com.yhn"}) //改变扫描规则为了让扫描到同样在com.yhn包下的SwaggerConfig
public class OrdersApplication {
    public static void main(String[] args) {

        SpringApplication.run(OrdersApplication.class, args);
    }

}

二、配置类
在这里插入图片描述
在这里插入图片描述
三、编写controller
service_order模块下
OrderController

@RestController
@RequestMapping("/eduorder/order")
@CrossOrigin
public class OrderController {

    @Autowired
    private OrderService orderService;
    
    //1 生成订单的方法
    @PostMapping("createOrder/{courseId}")
    public R save(@PathVariable String courseId, HttpServletRequest request) {
        //wtUtils.getMemberIdByJwtToken(request)可以获取到header中的用户id
        //创建订单,返回订单号
        String orderId = orderService.createOrders(courseId, JwtUtils.getMemberIdByJwtToken(request));
        return R.ok().data("orderId",orderId);
    }
    
}

四、编写service
OrderService 接口

public interface OrderService extends IService<Order> {

    /**
     * 生成订单的方法
     * @param courseId
     * @param memberId
     * @return
     */
    String createOrders(String courseId, String memberId);
}

OrderServiceImpl实现类

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    @Autowired
    private EduClient eduClient;
    @Autowired
    private UcenterClient ucenterClient;

    @Override
    public String createOrders(String courseId, String memberId) {
        //通过远程调用 根据用户id获取用户信息
        UcenterMemberVo userInfoOrder = ucenterClient.getMemberInfoById(memberId);
        //通过远程调用 根据课程id获取课程信息
        CourseWebVoOrder courseInfoOrder = eduClient.getCourseInfoOrder(courseId);

        //创建order对象,向order对象里面设置值
        Order order = new Order();
        order.setOrderNo(OrderNoUtil.getOrderNo());//订单号
        order.setCourseId(courseId); //课程id
        order.setCourseTitle(courseInfoOrder.getTitle());
        order.setCourseCover(courseInfoOrder.getCover());
        order.setTeacherName(courseInfoOrder.getTeacherName());
        order.setTotalFee(courseInfoOrder.getPrice());
        order.setMemberId(memberId);
        order.setMobile(userInfoOrder.getMobile());
        order.setNickname(userInfoOrder.getNickname());
        order.setStatus(0);  //订单状态(0:未支付 1:已支付)
        order.setPayType(1);  //支付类型 ,微信1
        baseMapper.insert(order);
        //返回订单号
        return order.getOrderNo();
    }
}

五、实现类中需要远程调用的两个接口中的方法
1、service_ucenter模块里的之前添加评论时候定义的方法,
UcenterMemberController

 //根据用户id查询用户信息
    @PostMapping("/getMemberInfoById/{memberId}")
    public UcenterMemberVo getMemberInfoById(@PathVariable String memberId){
        //不能直接返回UcenterMemberVo需要复制
        UcenterMember member = memberService.getById(memberId);
        UcenterMemberVo memberVo = new UcenterMemberVo();
        //课程和用户返回的都需要是同一个类UcenterMemberVo,这个类是公共的
        //返回的是调用端和被调用端同样的对象,UcenterMember,UcenterMemberVo是两个类
        BeanUtils.copyProperties(member,memberVo);
        return memberVo;
    }

2、service_edu模块下定义方法
front/CourseFrontController

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

CourseWebVoOrder 之前放到公共模块里面了
3、创建两个interface指定调用服务名称和调用接口地址
service_order模块下新建包client
EduClient接口

@Component
@FeignClient("service-edu")
public interface EduClient {
    //根据课程id查询课程信息
    @PostMapping("/eduservice/coursefront/getCourseInfoOrder/{id}")
                                                      //必须把"id"加上
    public CourseWebVoOrder getCourseInfoOrder(@PathVariable("id") String id) ;
}

UcenterClient接口

@Component
@FeignClient("service-ucenter")
public interface UcenterClient {
    //根据用户id获取用户信息
    @PostMapping("/educenter/member/getMemberInfoById/{memberId}")
    public UcenterMemberVo getMemberInfoById(@PathVariable("memberId") String memberId);
}

查询订单接口

OrderController

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

整合生成订单的页面

一、复制新的assets中文件进去
二、在api中定义方法
点立即购买会生成课程的订单,跳转到订单页面
order.js

import request from '@/utils/request'

export default {
    //生成订单
  createOrders(courseId) {
    return request({
      url: '/eduorder/order/createOrder/'+courseId,
      method: 'post'
    })
  },
  //根据订单id查询订单信息
  getOrdersInfo(id) {
    return request({
      url: '/eduorder/order/getOrderInfo/'+id,
      method: 'get'
    })
  }
}

三、在页面中调用
在课程详情页面立即购买按钮,绑定事件,调用方法
/course/_id.vue
在这里插入图片描述
注意不用加这个标签不然会新跳到一个页面
在这里插入图片描述

 //生成订单
    createOrders(){
      ordersApi.createOrders(this.courseId)
      .then(response=>{
        //获取返回订单号
        //生成订单之后,跳转到显示页面
        this.$router.push({path:'/orders/'+response.data.data.orderId})
      })
    }

四、创建订单显示页面,显示生成的订单的详细信息
在pages包下创建orders/_oid.vue
在这里插入图片描述
动态路由跳转到_oid.vue

_oid.vue

<template>
  <div class="Page Confirm">
    <div class="Title">
      <h1 class="fl f18">订单确认</h1>
      <img src="~/assets/img/cart_setp2.png" class="fr">
      <div class="clear"></div>
    </div>
    <form name="flowForm" id="flowForm" method="post" action="">
      <table class="GoodList">
        <tbody>
        <tr>
          <th class="name">商品</th>
          <th class="price">原价</th>
          <th class="priceNew">价格</th>
        </tr>
        </tbody>
        <tbody>
        <!-- <tr>
          <td colspan="3" class="Title red f18 fb"><p>限时折扣</p></td>
        </tr> -->
        <tr>
          <td colspan="3" class="teacher">讲师:{{order.teacherName}}</td>
        </tr>
        <tr class="good">
          <td class="name First">
            <a target="_blank" :href="'https://localhost:3000/course/'+order.courseId">
              <img :src="order.courseCover"></a>
            <div class="goodInfo">
              <input type="hidden" class="ids ids_14502" value="14502">
              <a target="_blank" :href="'https://localhost:3000/course/'+ order.courseId">{{order.courseTitle}}</a>
            </div>
          </td>
          <td class="price">
            <p><strong>{{order.totalFee}}</strong></p>
            <!-- <span class="discName red">限时8</span> -->
          </td>
          <td class="red priceNew Last"><strong>{{order.totalFee}}</strong></td>
        </tr>
        <tr>
          <td class="Billing tr" colspan="3">
            <div class="tr">
              <p><strong class="red">1</strong> 件商品,合计<span
                class="red f20"><strong>{{order.totalFee}}</strong></span></p>
            </div>
          </td>
        </tr>
        </tbody>
      </table>
      <div class="Finish">
        <div class="fr" id="AgreeDiv">
          
          <label for="Agree"><p class="on"><input type="checkbox" checked="checked">我已阅读并同意<a href="javascript:" target="_blank">《谷粒学院购买协议》</a></p></label>
        </div>
        <div class="clear"></div>
        <div class="Main fl">
          <div class="fl">
            <a :href="'/course/'+order.courseId">返回课程详情页</a>
          </div>
          <div class="fr">
            <p><strong class="red">1</strong> 件商品,合计<span class="red f20"><strong
              id="AllPrice">{{order.totalFee}}</strong></span></p>
          </div>
        </div>
        <input name="score" value="0" type="hidden" id="usedScore">
        <button class="fr redb" type="button" id="submitPay" @click="toPay()">去支付</button>
        <div class="clear"></div>
      </div>
    </form>
  </div>
</template>
<script>
import ordersApi from '@/api/orders'
export default {
    asyncData({ params, error }) {
        return ordersApi.getOrdersInfo(params.oid) //页面叫oid就写oid
            .then(response => {
                return {
                    order: response.data.data.item
                }
            })
    }
    
}
</script>

五、在default.vue页面中引入其他的css样式
在这里插入图片描述
在这里插入图片描述

生成微信支付的二维码

一、准备工作

weixin:
  pay:
    #关联的公众号appid
    appid: wx74862e0dfcf69954
    #商户号
    partner: 1558950191
    #商户key
    partnerkey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
    #回调地址
    notifyurl: http://guli.shop/api/order/weixinPay/weixinNotify

二、微信支付二维码接口
1、在service_order pom中引入依赖

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

2、编写controller
PayLogController.java

@RestController
@RequestMapping("/eduorder/paylog")
@CrossOrigin
public class PayLogController {
    @Autowired
    private PayLogService payLogService;

    //生成微信支付二维码接口
    @GetMapping("createNative")
    public R createNative(@PathVariable String orderNo) {
        //返回信息包含二维码地址还有其他信息
       Map map=payLogService.createNative(orderNo);
        return R.ok().data(map);
    }

}

3、编写service
PayLogService 接口

public interface PayLogService extends IService<PayLog> {

    /**
     * 生成微信支付二维码接口
     * @param orderNo
     * @return
     */
    Map createNative(String orderNo);
}

PayLogServiceImpl实现类

@Service
public class PayLogServiceImpl extends ServiceImpl<PayLogMapper, PayLog> implements PayLogService {

    @Autowired
    private OrderService orderService;
    //生成微信支付二维码接口
    @Override
    public Map createNative(String orderNo) {
        try {
            //1 根据订单号查询订单信息
            QueryWrapper<Order> wrapper = new QueryWrapper<>();
            wrapper.eq("order_no",orderNo);
            Order order = orderService.getOne(wrapper);

            //2 使用map设置生成二维码需要参数
            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");//本地IP号
            m.put("notify_url", "http://guli.shop/api/order/weixinPay/weixinNotify\n");
            m.put("trade_type", "NATIVE");

            //3 发送httpclient请求,传递参数xml格式,微信支付提供的固定的地址
            HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
            //设置xml格式的参数
            client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
            client.setHttps(true);
            //执行post请求发送
            client.post();

            //4 得到发送请求返回结果
            //返回内容,是使用xml格式返回
            String xml = client.getContent();

            //把xml格式转换map集合,把map集合返回
            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) {
            throw new BuguException(20001,"生成二维码失败");
        }

    } 
}

查询订单支付状态接口

1、编写controller
PayLogController

  //查询订单支付状态
    //参数:订单号,根据订单号查询 支付状态
    @GetMapping("queryPayStatus/{orderNo}")
    public R queryPayStatus(@PathVariable String orderNo) {
        Map<String,String> map = payLogService.queryPayStatus(orderNo);
        System.out.println("*****查询订单状态map集合:"+map);
        if(map == null) {
            return R.error().message("支付出错了");//失败的状态码是20001
        }
        //如果返回map里面不为空,通过map获取订单状态
        if(map.get("trade_state").equals("SUCCESS")) {//支付成功
            //添加记录到支付表,更新订单表订单状态
            payLogService.updateOrdersStatus(map);
            return R.ok().message("支付成功");//成功的状态码是20000,success是true
        }
        return R.ok().code(25000).message("支付中");

    }

2、编写service
PayLogService接口

  /**
     *根据订单号查询订单支付状态
     */
    Map<String, String> queryPayStatus(String orderNo);

    /**
     *  向支付表添加记录,更新订单状态
     * @param map
     */
    void updateOrdersStatus(Map<String, String> map);

PayLogServiceImpl实现类

 //查询订单支付状态
    @Override
    public Map<String, String> queryPayStatus(String orderNo) {
        try {
            //1、封装参数
            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());

            //2 发送httpclient
            HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
            client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));
            client.setHttps(true);
            client.post();

            //3 得到请求返回内容
            String xml = client.getContent();
            Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
            //6、转成Map再返回
            return resultMap;
        }catch(Exception e) {
            return null;
        }
    }

    //添加支付记录和更新订单状态
    @Override
    public void updateOrdersStatus(Map<String, String> map) {
        //从map获取订单号
        String orderNo = map.get("out_trade_no");
        //根据订单号查询订单信息
        QueryWrapper<Order> wrapper = new QueryWrapper<>();
        wrapper.eq("order_no",orderNo);
        Order order = orderService.getOne(wrapper);

        //更新订单表订单状态
        if(order.getStatus().intValue() == 1) { return; }
        order.setStatus(1);//1代表已经支付
        orderService.updateById(order);

        //向支付表添加支付记录
        PayLog payLog = new PayLog();
        payLog.setOrderNo(orderNo);  //订单号
        payLog.setPayTime(new Date()); //订单完成时间
        payLog.setPayType(1);//支付类型 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);
    }

生成微信支付二维码前端

一、在api中定义方法
orders.js

  //生成二维码的方法
  createNatvie(orderNo) {
    return request({
      url: '/eduorder/paylog/createNative/'+orderNo,
      method: 'get'
    })
  },

  //查询订单状态的方法
  //生成二维码的方法
  queryPayStatus(orderNo) {
    return request({
      url: '/eduorder/paylog/queryPayStatus/'+orderNo,
      method: 'get'
    })
  }

二、_oid.vue页面中新建支付跳转的方法
order/_oid.vue页面编写方法

  ,
    methods:{
        //去支付
        toPay() {
            this.$router.push({path:'/pay/'+this.order.orderNo})
        }
    }

通过动态路由跳转
三、在pages包下新建/pay/_pid.vue页面
在这里插入图片描述
要安装这个组件下载二维码的组件
在nuxt-swiper-plugin.js页面中引入组件
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个是因为没有登录

<template>
  <div class="cart py-container">
    <!--主内容-->
    <div class="checkout py-container  pay">
      <div class="checkout-tit">
        <h4 class="fl tit-txt"><span class="success-icon"></span><span class="success-info">订单提交成功,请您及时付款!订单号:{{payObj.out_trade_no}}</span>
        </h4>
        <span class="fr"><em class="sui-lead">应付金额:</em><em class="orange money">{{payObj.total_fee}}</em></span>
        <div class="clearfix"></div>
      </div>
      <div class="checkout-steps">
        <div class="fl weixin">微信支付</div>
        <div class="fl sao">
          <p class="red">请使用微信扫一扫。</p>
          <div class="fl code">
            <!-- <img id="qrious" src="~/assets/img/erweima.png" alt=""> -->
            <!-- <qriously value="weixin://wxpay/bizpayurl?pr=R7tnDpZ" :size="338"/> -->
            <qriously :value="payObj.code_url" :size="338"/>
            <div class="saosao">
              <p>请使用微信扫一扫</p>
              <p>扫描二维码支付</p>
            </div>

          </div>

        </div>
        <div class="clearfix"></div>
        <!-- <p><a href="pay.html" target="_blank">> 其他支付方式</a></p> -->
        
      </div>
    </div>
  </div>
</template>
<script>
import ordersApi from '@/api/orders'
export default {
     asyncData({ params, error }) {
         return ordersApi.createNatvie(params.pid)
            .then(response => {
                return {
                      payObj: response.data.data
                    }
            })
     },
     data() {
         return {
             timer1:''
         }
     },
     //每隔三秒调用一次查询订单状态的方法
     mounted() {//页面渲染之后执行
        this.timer1 = setInterval(() => {
            this.queryOrderStatus(this.payObj.out_trade_no)
        },3000);
     },
     methods:{
         queryOrderStatus(orderNo) {
             ordersApi.queryPayStatus(orderNo)
                .then(response => {
                     if (response.data.success) { //success是R返回的状态码是20000
                        //支付成功,清除定时器
                        clearInterval(this.timer1)
                        //提示
                        this.$message({
                            type: 'success',
                            message: '支付成功!'
                        })
                        //跳转回到课程详情页面
                        this.$router.push({path: '/course/' + this.payObj.course_id})
                     }
                })
         }
     }
}
</script>

四、在给后端给支付中加上25000的状态码
五、在utils/request.js页面中新建响应拦截器

// http response 拦截器
service.interceptors.response.use(
  response => {
    //debugger
    if (response.data.code == 28004) {
        console.log("response.data.resultCode是28004")
        // 返回 错误代码-1 清除ticket信息并跳转到登录页面
        //debugger
        window.location.href="/login"
        return
    }else{
      if (response.data.code !== 20000) {//支付中返回的也是R.ok(),success也是true,不加拦截器就返回response,定时器就会判断支付成功直接返回
        //25000:订单支付中,不做任何提示
        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)   // 返回接口返回的错误信息
});

最后测试成功
课程支付流程总结

在这里插入图片描述

day14

课程详情页显示效果完善

在这里插入图片描述
一、前端页面
course/_id.vue
在这里插入图片描述
二、后端新加一个接口
service_order模块
OrderController

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

三、修改接口,添加当前课程是否被购买的信息
在这里插入图片描述
需要在edu模块调用order模块中的方法
在service_edu模块中
1、client定义远程调用的方法
OrdersClient接口

@Component
@FeignClient(name="service-order",fallback = OrdersClientImpl.class)
public interface OrdersClient {
    //根据课程id和用户id查询订单表中状态
    @GetMapping("/eduorder/order/isByCourse/{courseId}/{memberId}")
    public boolean isByCourse(@PathVariable("courseId") String courseId, @PathVariable("memberId") String memberId);

}

OrdersClientImpl实现类

@Component
public class OrdersClientImpl implements OrdersClient{
    @Override
    public boolean isByCourse(String courseId, String memberId) {
        return false;
    }
}

2、在接口中调用方法
CourseFrontController接口的getFrontCourseInfo方法中调用远程方法,返回isBuy

  //2 课程详情的方法-为了课程详情页面显示
    @GetMapping("getFrontCourseInfo/{courseId}")
    public R getFrontCourseInfo(@PathVariable String courseId, HttpServletRequest request) {
        //根据课程id,编写sql语句查询课程信息
        CourseWebVo courseWebVo = courseService.getBaseCourseInfo(courseId);

        //根据课程id查询章节和小节  之前写过直接调用
        List<ChapterVo> chapterVideoList = chapterService.getChapterByCourseId(courseId);

        String memberId = JwtUtils.getMemberIdByJwtToken(request);
        boolean buyCourse = false;
        //判断用户是否登录
        if (!StringUtils.isEmpty(memberId)){
            buyCourse = ordersClient.isByCourse(courseId, memberId);//远程方法调用
        }


        //得到videoSourceId
        ChapterVo chapterVo = chapterVideoList.get(0);
        List<VideoVo> children = chapterVo.getChildren();
        VideoVo videoVo = children.get(0);
        String videoSourceId = videoVo.getVideoSourceId();
        return R.ok().data("courseWebVo", courseWebVo).data("chapterVideoList", chapterVideoList).data("videoSource Id", videoSourceId).data("isBuy", buyCourse);
    }

四、前端
进入页面前就要判断是否购买课程,如果购买了课程就显示立即观看
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

统计分析模块

一、准备工作
1、创建表存储统计数据

CREATE TABLE `statistics_daily` (
  `id` CHAR(19) NOT NULL COMMENT '主键',
  `date_calculated` VARCHAR(20) NOT NULL COMMENT '统计日期',
  `register_num` INT(11) NOT NULL DEFAULT '0' COMMENT '注册人数',
  `login_num` INT(11) NOT NULL DEFAULT '0' COMMENT '登录人数',
  `video_view_num` INT(11) NOT NULL DEFAULT '0' COMMENT '每日播放视频数',
  `course_num` INT(11) NOT NULL DEFAULT '0' COMMENT '每日新增课程数',
  `gmt_create` DATETIME NOT NULL COMMENT '创建时间',
  `gmt_modified` DATETIME NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `statistics_day` (`date_calculated`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='网站统计日数据';

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

后端接口实现

一、创建一个新的模块service_statistics,并进行初始化配置
1、创建配置文件
在这里插入图片描述
在这里插入图片描述

2、使用代码生成器生成代码
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
3、创建启动类

@SpringBootApplication
@ComponentScan(basePackages = {"com.yhn"})
@EnableDiscoveryClient //nacos注册
@EnableFeignClients   //服务发现远程调用
@MapperScan("com.yhn.staservice.service")
public class StaApplication {
    public static void main(String[] args) {
        SpringApplication.run(StaApplication.class,args);
    }
}

二、在service_ucenter模块新加接口
1、编写controller
UcenterMemberController

 //查询某一天注册人数
    @GetMapping("countRegister/{day}")
    public R countRegister(@PathVariable String day) {
        Integer count = memberService.countRegisterDay(day);
        return R.ok().data("countRegister", count);
    }

2、编写service
UcenterMemberService接口

   /**
     * 查询某一天注册人数
     * @param day
     * @return
     */
    Integer countRegisterDay(String day);

UcenterMemberServiceImpl实现类


    @Override
    public Integer countRegisterDay(String day) {
        return baseMapper.countRegisterDay(day);
    }

3、编写mapper
UcenterMemberMapper

public interface UcenterMemberMapper extends BaseMapper<UcenterMember> {

    Integer countRegisterDay(String day);
}

UcenterMemberMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yhn.educenter.mapper.UcenterMemberMapper">

<!--    查询某一天的注册人数-->
    <select id="countRegisterDay" resultType="java.lang.Integer">
        select count(*) from ucenter_member uc
         where date(uc.gmt_create)=#{day}
    </select>
    
</mapper>

问题:要是需要mapper有两个参数在xml文件中怎么取
参数前面加注解
在这里插入图片描述
在这里插入图片描述
4、在service的pom文件中加入扫描能扫描到mapper文件(之前加过)

<build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

5、然后在service_ucenter模块中的配置文件中加入配置

#配置mapper.xml文件的路径每个包下是不一样的
mybatis-plus.mapper-locations=classpath:com/yhn/educenter/mapper/xml/*.xml

三、在service_statistics模块中远程调用service_ucenter模块中的方法
1、先定义远程要调用的方法
client/UcenterClient.java

@Component
@FeignClient("service-ucenter")
public interface UcenterClient {
    //查询某一天注册人数
    @GetMapping("/educenter/member/countRegister/{day}")
    public R countRegister(@PathVariable("day") String day);
}

2、编写controller
StatisticsDailyController


@RestController
@RequestMapping("/staservice/sta")
@CrossOrigin
public class StatisticsDailyController {
    @Autowired
    private StatisticsDailyService staService;

    //统计某一天的注册人数.生成统计数据
    @PostMapping("registerCount/{day}")
    public R registerCount(@PathVariable String day) {
        staService.registerCount(day);
        return R.ok();
    }
}


3、编写service
StatisticsDailyService 接口

public interface StatisticsDailyService extends IService<StatisticsDaily> {

    /**
     * 统计某一天的注册人数.生成统计数据
     * @param day
     */
    void registerCount(String day);
}

StatisticsDailyServiceImpl实现类

@Service
public class StatisticsDailyServiceImpl extends ServiceImpl<StatisticsDailyMapper, StatisticsDaily> implements StatisticsDailyService {

    @Autowired
    private UcenterClient ucenterClient;

    @Override
    public void registerCount(String day) {
    
        //添加记录之前删除表相同日期的数据-完善
        QueryWrapper<StatisticsDaily> wrapper = new QueryWrapper<>();
        wrapper.eq("date_calculated",day);
        baseMapper.delete(wrapper);
   
        R registerR = ucenterClient.countRegister(day);
        Integer countRegister = (Integer) registerR.getData().get("countRegister");

        //把获取数据添加到数据库,添加分析表里面
        StatisticsDaily sta = new StatisticsDaily();
        sta.setRegisterNum(countRegister);//注册人数
        sta.setDateCalculated(day);//统计日期

        sta.setVideoViewNum( RandomUtils.nextInt(100, 200));
        sta.setLoginNum( RandomUtils.nextInt(100, 200));
        sta.setCourseNum( RandomUtils.nextInt(100, 200));
        baseMapper.insert(sta);
    }
}

改善让每个日期在表中只有一条数据,一条不能时间端人数不一样不能每次新插入新的一条
可以不用修改,而是先删除这个日期原来的数据再添加
在这里插入图片描述

生成统计数据前端整合

在后台管理系统
一、配置nginx路径规则
二、添加路由
router/index.js

 {
    path: '/sta',
    component: Layout,
    redirect: '/sta/create',
    name: '统计分析',
    meta: { title: '统计分析', icon: 'example' },
    children: [
      {
        path: 'create',
        name: '生成数据',
        component: () => import('@/views/sta/create'),
        meta: { title: '生成数据', icon: 'table' }
      },
      {
        path: 'show',
        name: '图表显示',
        component: () => import('@/views/sta/show'),
        meta: { title: '图表显示', icon: 'tree' }
      }
    ]
  },

三、在api中定义方法

import request from '@/utils/request'
export default {
    //1 生成统计数据
    createStaData(day) {
        return request({
            url: '/staservice/sta/registerCount/'+day,
            method: 'post'
          })
    }
}

四、页面中调用在views包下新建包sta里面新建两个vue文件
在这里插入图片描述
create.vue

<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>
import sta from '@/api/sta'
export default {
    data() {
        return {
            day:'',
            btnDisabled: false
        }
    },
    created() {

    },
    methods:{
        create() {
            sta.createStaData(this.day)
                .then(response => {
                    //提示信息
                    this.$message({
                        type: 'success',
                        message: '生成数据成功!'
                    })
                    //跳转到图表显示页面
                    this.$router.push({path:'/sta/show'})
                })
        }
    }
}
</script>

项目中整合定时项目

在这里插入图片描述

一、启动类加一个注解
在这里插入图片描述
二、创建一个自动执行方法的类
schedule/ScheduleTask.java

@Component
public class ScheduleTask {
    @Autowired
    private StatisticsDailyService staService;

//    @Scheduled(cron = "0/5 * * * * ?") //每隔五秒执行一次
//    public void tas1() {
//        System.out.println("执行了");
//    }

    //在每天凌晨1点,把前一天数据进行数据查询添加
    @Scheduled(cron = "0 0 1 * * ?")
    public void task2() {
        staService.registerCount(DateUtil.formatDate(DateUtil.addDays(new Date(), -1)));
    }
}

三、utils/DateUtil.java工具类


public class DateUtil {

    private static final String dateFormat = "yyyy-MM-dd";

    /**
     * 格式化日期
     *
     * @param date
     * @return
     */
    public static String formatDate(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
        return sdf.format(date);

    }

    /**
     * 在日期date上增加amount天 。
     *
     * @param date   处理的日期,非null
     * @param amount 要加的天数,可能为负数
     */
    public static Date addDays(Date date, int amount) {
        Calendar now =Calendar.getInstance();
        now.setTime(date);
        now.set(Calendar.DATE,now.get(Calendar.DATE)+amount);
        return now.getTime();
    }

    public static void main(String[] args) {
        System.out.println(DateUtil.formatDate(new Date()));
        System.out.println(DateUtil.formatDate(DateUtil.addDays(new Date(), -1)));
    }
}

Echarts简介–用于图表显示

在这里插入图片描述

图表显示

图表显示后端

一、编写controller
StatisticsDailyController

//图表显示,返回两部分数据,日期json数组,数量json数组
    @GetMapping("showData/{type}/{begin}/{end}")
    public R showData(@PathVariable String type,@PathVariable String begin,
                      @PathVariable String end) {
        Map<String,Object> map = staService.getShowData(type,begin,end);
        return R.ok().data(map);
    }

二、编写service
StatisticsDailyService.java接口

 /**
     * 图表显示
     * @param type
     * @param begin
     * @param end
     * @return
     */
    Map<String, Object> getShowData(String type, String begin, String end);

StatisticsDailyServiceImpl实现类

  //图表显示,返回两部分数据,日期json数组,数量json数组
    @Override
    public Map<String, Object> getShowData(String type, String begin, String end) {
        //根据条件查询对应数据
        QueryWrapper<StatisticsDaily> wrapper = new QueryWrapper<>();
        wrapper.between("date_calculated",begin,end);
        wrapper.select("date_calculated",type);
        List<StatisticsDaily> staList = baseMapper.selectList(wrapper);

        //因为返回有两部分数据:日期 和 日期对应数量
        //前端要求数组json结构,对应后端java代码是list集合
        //创建两个list集合,一个日期list,一个数量list
        List<String> date_calculatedList = new ArrayList<>();
        List<Integer> numDataList = new ArrayList<>();

        //遍历查询所有数据list集合,进行封装
        for (int i = 0; i < staList.size(); i++) {
            StatisticsDaily daily = staList.get(i);
            //封装日期list集合
            date_calculatedList.add(daily.getDateCalculated());
            //封装对应数量
            switch (type) {
                case "login_num":
                    numDataList.add(daily.getLoginNum());
                    break;
                case "register_num":
                    numDataList.add(daily.getRegisterNum());
                    break;
                case "video_view_num":
                    numDataList.add(daily.getVideoViewNum());
                    break;
                case "course_num":
                    numDataList.add(daily.getCourseNum());
                    break;
                default:
                    break;
            }
        }
        //把封装之后两个list集合放到map集合,进行返回
        Map<String, Object> map = new HashMap<>();
        map.put("date_calculatedList",date_calculatedList);
        map.put("numDataList",numDataList);
        return map;
    }

在这里插入图片描述
后端对象到前端对象或者map会变成json对象格式
list集合会变成json数组

统计分析-图表显示(页面整合)

一、在后台管理系统安装echarts

npm install --save echarts@4.1.0  

下载之后会到node_modules包下
二、整合页面-图表显示
在这里插入图片描述
1、在api中定义方法
sta.js

//2 获取统计数据
    getDataSta(searchObj) {
        return request({
            url: `/staservice/sta/showData/${searchObj.type}/${searchObj.begin}/${searchObj.end}`,
            method: 'get'
          })
    }

2、页面中显示,编写方法

<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>
<script>
import echarts from 'echarts'
import staApi from '@/api/sta'

export default {
    data() {
        return {
            searchObj:{},
            btnDisabled:false,
            xData:[],
            yData:[]
        }
    },
    methods:{
        showChart() {
            staApi.getDataSta(this.searchObj)
                .then(response => {
                    console.log('*****************'+response)
                    //后台系统中就不是两个data了
                    this.yData = response.data.numDataList
                    this.xData = response.data.date_calculatedList

                    //调用下面生成图表的方法,改变值 ,在当前方法内才能取到值
                    this.setChart()
                })
        },
        setChart() {
            // 基于准备好的dom,初始化echarts实例
            this.chart = echarts.init(document.getElementById('chart'))
            // console.log(this.chart)

            // 指定图表的配置项和数据
            var option = {
                title: {
                    text: '数据统计'
                },
                //x坐标轴触发提示,光标移动会有显示
                tooltip: {
                    trigger: 'axis'
                },
                //区域缩放
                dataZoom: [{
                    show: true,
                    height: 30,
                    xAxisIndex: [
                        0
                    ],
                    bottom: 30,
                    start: 10,
                    end: 80,
                    handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
                    handleSize: '110%',
                    handleStyle: {
                        color: '#d3dee5'

                    },
                    textStyle: {
                        color: '#fff'
                    },
                    borderColor: '#90979c'
                    },
                    {
                    type: 'inside',
                    show: true,
                    height: 15,
                    start: 1,
                    end: 35
                 }],
                // x轴是类目轴(离散数据),必须通过data设置类目数据
                xAxis: {
                    type: 'category',
                    data: this.xData
                },
                // y轴是数据轴(连续数据)
                yAxis: {
                    type: 'value'
                },
                // 系列列表。每个系列通过 type 决定自己的图表类型
                series: [{
                    // 系列中的数据内容数组
                    data: this.yData,
                    // 折线图
                    type: 'line'
                }]
            }

            this.chart.setOption(option)
        }
    }
}
</script>

day17

canal数据同步

在这里插入图片描述
在这里插入图片描述

canal安装

一、安装数据库
在这里插入图片描述

在Linux中的数据库里创建和windows中系统的一个数据库bugu和系统的一个数据表
在这里插入图片描述
在这里插入图片描述
在Windows中的数据库也创建同样的表

 CREATE TABLE members( id INT PRIMARY KEY, username VARCHAR(100), age INT );

二、安装canal数据同步工具
1、修改Linux系统MySQL数据库中配置
先启动MySQL

mysql -uroot -p

2、检查binlog功能是否开启

show variables like 'log_bin';

在这里插入图片描述
在这里插入图片描述
三、在Windows的数据库中建给用户,其他人都能访问

CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';

GRANT SHOW VIEW,SELECT,REPLICATION SLAVE,REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;

在这里插入图片描述
后面全输入y
四、安装canal工具
在这里插入图片描述

在这里插入图片描述

 vim  /opt/canal/canal.deployer-1.1.4/conf/example/instance.properties 

在这里插入图片描述
五、启动canal数据同步工具
在canl

cd /opt/canal/canal.deployer-1.1.4/bin/
./startup.sh

cancal数据同步客户端代码编写

一、在bugu_parent模块下新建一个模块canal_clientedu
在这里插入图片描述

二、在canal_clientedu中引入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>commons-dbutils</groupId>
            <artifactId>commons-dbutils</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
        </dependency>
    </dependencies>

三、编写配置文件
application.properties

# 服务端口
server.port=10000
# 服务名
spring.application.name=canal_clientedu

# 环境设置:dev开发环境、test、prod生产环境
spring.profiles.active=dev

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/bugu?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

四、创建canal客户端同步类,在启动类里面执行
在这里插入图片描述
CanalClient是固定的直接复制就行
改成虚拟机的IP地址
在这里插入图片描述

CanalApplication启动类
实现一个接口CommandLineRunner

@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 {
        //项目启动,执行canal客户端监听
        canalClient.run();
    }
}

网关

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

spring cloud-Gateway网关使用

一、新建网关模块
在这里插入图片描述
二、在api_gateway模块的pom文件中引入依赖

 <dependencies>
        <dependency>
            <groupId>com.yhn</groupId>
            <artifactId>common_utils</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!--gson-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>

        <!--服务调用-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

三、创建启动类
ApiGatewayApplication

@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

四、编写配置类
application.properties

# 服务端口
server.port=8222
# 服务名
spring.application.name=service-gateway

# 环境设置:dev开发环境、test、prod生产环境
spring.profiles.active=dev

#nacos服务地址 千万不要写错
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true


#设置路由id
spring.cloud.gateway.routes[0].id=service-acl
#设置路由的uri   lb://nacos注册服务名称
spring.cloud.gateway.routes[0].uri=lb://service-acl
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[0].predicates= Path=/*/acl/**

#配置service-edu服务
spring.cloud.gateway.routes[1].id=service-edu
spring.cloud.gateway.routes[1].uri=lb://service-edu
spring.cloud.gateway.routes[1].predicates= Path=/eduservice/**

#配置service-msm服务
spring.cloud.gateway.routes[2].id=service-msm
spring.cloud.gateway.routes[2].uri=lb://service-msm
spring.cloud.gateway.routes[2].predicates= Path=/edumsm/**

#配置service-statistic服务
spring.cloud.gateway.routes[3].id=service-statistics
spring.cloud.gateway.routes[3].uri=lb://service-statistics
spring.cloud.gateway.routes[3].predicates= Path=/staservice/**

#配置service-vod
spring.cloud.gateway.routes[4].id=service-vod
spring.cloud.gateway.routes[4].uri=lb://service-vod
spring.cloud.gateway.routes[4].predicates= Path=/eduvod/**

#配置service-order
spring.cloud.gateway.routes[5].id=service-order
spring.cloud.gateway.routes[5].uri=lb://service-order
spring.cloud.gateway.routes[5].predicates= Path=/eduorder/**

#配置service-cms
spring.cloud.gateway.routes[6].id=service-cms
spring.cloud.gateway.routes[6].uri=lb://service-cms
spring.cloud.gateway.routes[6].predicates= Path=/educms/**

#配置service-ucenter
spring.cloud.gateway.routes[7].id=service-ucenter
spring.cloud.gateway.routes[7].uri=lb://service-ucenter
spring.cloud.gateway.routes[7].predicates= Path=/educenter/**

#配置service-ucenter
spring.cloud.gateway.routes[8].id=service-ucenter
spring.cloud.gateway.routes[8].uri=lb://service-ucenter
spring.cloud.gateway.routes[8].predicates= Path=/*/ucenter/**


现在服务edu就不用通过edu端口号而是通过api_gateway的网关8222
api_gateway和其他需要使用网关的都需要在nacos中注册,在配置文件中配置nacos的地址在启动类加@EnableDiscoveryClient //nacos注册的注解
测试成功
在这里插入图片描述
网关帮我们实现负载均衡
在这里插入图片描述

Gateway网关跨域

以前用nginx的时候用@CrossOrigin解决跨域
现在用这三个包解决网关跨域,所以把之前的@CrossOrigin注解都删除,两个跨域不能都加
在这里插入图片描述
测试修改前端的端口号原来的nginx的9001改为8222
在这里插入图片描述
在这里插入图片描述
这个是因为跨域跨了两次

权限管理

在这里插入图片描述
在这里插入图片描述

表结构介绍

先创建五张表
在这里插入图片描述
acl_user_role用户和角色关系表
acl_role_permission角色和菜单关系表
acl_permission权限
在这里插入图片描述
在这里插入图片描述

权限管理-后端接口整合

一、在service模块下创建子模块service_acl
在这里插入图片描述
二、引入依赖

 <dependencies>
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>spring_security</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
    </dependencies>

3、复制service_acl模块核心代码,包含application配置文件
在这里插入图片描述
复制过去需要修改的地方
application.properties
在这里插入图片描述
mapper.xml文件中把地址改为yhn
在这里插入图片描述
实体类中时间加上注解

3.1获取所有菜单
在这里插入图片描述

(1)controller
PermissionController

@RestController
@RequestMapping("/admin/acl/permission")
//@CrossOrigin
public class PermissionController {

    @Autowired
    private PermissionService permissionService;

    //获取全部菜单
    @ApiOperation(value = "查询所有菜单")
    @GetMapping
    public R indexAllPermission() {
        List<Permission> list =  permissionService.queryAllMenuGuli();
        return R.ok().data("children",list);
    }
}    

(2)service
PermissionService接口

//获取全部菜单
    List<Permission> queryAllMenuGuli();

PermissionServiceImpl实现类

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

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

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

3.2 递归删除菜单
(1)controller
PermissionController

  @ApiOperation(value = "递归删除菜单")
    @DeleteMapping("remove/{id}")
    public R remove(@PathVariable String id) {
        permissionService.removeChildByIdGuli(id);
        return R.ok();
    }

(2)service
PermissionService


    //递归删除菜单
    void removeChildByIdGuli(String id);

PermissionServiceImpl实现类

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

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


3.3给角色分配菜单
(1)controller


    @ApiOperation(value = "给角色分配权限")
    @PostMapping("/doAssign")
    public R doAssign(String roleId,String[] permissionId) {
        permissionService.saveRolePermissionRealtionShipGuli(roleId,permissionId);
        return R.ok();
    }

PermissionServiceImpl实现类

  //给角色分配权限
    void saveRolePermissionRealtionShipGuli(String roleId, String[] permissionId);

    @Autowired
    private RolePermissionService rolePermissionService;
//=========================给角色分配菜单=======================
    @Override
    public void saveRolePermissionRealtionShipGuli(String roleId, String[] permissionIds) {
        //roleId角色id
        //permissionId菜单id 数组形式
        //1 创建list集合,用于封装添加数据
        List<RolePermission> rolePermissionList = new ArrayList<>();
        //遍历所有菜单数组
        for(String perId : permissionIds) {
            //RolePermission对象
            RolePermission rolePermission = new RolePermission();
            rolePermission.setRoleId(roleId);
            rolePermission.setPermissionId(perId);
            //封装到list集合
            rolePermissionList.add(rolePermission);
        }
        //添加到角色菜单关系表
        rolePermissionService.saveBatch(rolePermissionList);
    }

四、创建spring_security模块放到common模块
复制day18的代码day17的用不 了
在这里插入图片描述
spring_security模块需要的公共类放到了service_utils中
ResponseUtil

package com.yhn.commonutils;

import com.yhn.commonutils.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ResponseUtil {

    public static void out(HttpServletResponse response, R r) {
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
            mapper.writeValue(response.getWriter(), r);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

测试的时候吧swagger中这行代码注释
在这里插入图片描述
这行代码表示路径中有admin就不显示
测试结果
在这里插入图片描述

http://localhost:8009/swagger-ui.html
查询所有菜单
在这里插入图片描述

{
  "success": true,
  "code": 20000,
  "message": "成功",
  "data": {
    "children": [
      {
        "id": "1",
        "pid": "0",
        "name": "全部数据",
        "type": 0,
        "permissionValue": null,
        "path": null,
        "component": null,
        "icon": null,
        "status": null,
        "level": 1,
        "children": [
          {
            "id": "1195354076890370050",
            "pid": "1",
            "name": "订单管理",
            "type": 1,
            "permissionValue": null,
            "path": "/order",
            "component": "Layout",
            "icon": null,
            "status": null,
            "level": 2,
            "children": [
              {
                "id": "1195354153482555393",
                "pid": "1195354076890370050",
                "name": "订单列表",
                "type": 1,
                "permissionValue": null,
                "path": "list",
                "component": "/order/list",
                "icon": null,
                "status": null,
                "level": 3,
                "children": [
                  {
                    "id": "1195354315093282817",
                    "pid": "1195354153482555393",
                    "name": "查看",
                    "type": 2,
                    "permissionValue": "order.list",
                    "path": "",
                    "component": "",
                    "icon": null,
                    "status": null,
                    "level": 4,
                    "children": [],
                    "isDeleted": false,
                    "gmtCreate": "2019-11-15 22:54:12",
                    "gmtModified": "2019-11-15 22:54:12",
                    "select": false
                  }
                ],
                "isDeleted": false,
                "gmtCreate": "2019-11-15 22:53:33",
                "gmtModified": "2019-11-15 22:53:58",
                "select": false
              }
            ],
            "isDeleted": false,
            "gmtCreate": "2019-11-15 22:53:15",
            "gmtModified": "2019-11-15 22:53:15",
            "select": false
          },
          {
            "id": "1195352547621965825",
            "pid": "1",
            "name": "CMS管理",
            "type": 1,
            "permissionValue": null,
            "path": "/cms",
            "component": "Layout",
            "icon": null,
            "status": null,
            "level": 2,
            "children": [
              {
                "id": "1195353513549205505",
                "pid": "1195352547621965825",
                "name": "Bander列表",
                "type": 1,
                "permissionValue": null,
                "path": "banner/list",
                "component": "/cms/banner/list",
                "icon": null,
                "status": null,
                "level": 3,
                "children": [
                  {
                    "id": "1195353672110673921",
                    "pid": "1195353513549205505",
                    "name": "删除",
                    "type": 2,
                    "permissionValue": "banner.remove",
                    "path": "",
                    "component": "",
                    "icon": null,
                    "status": null,
                    "level": 4,
                    "children": [],
                    "isDeleted": false,
                    "gmtCreate": "2019-11-15 22:51:39",
                    "gmtModified": "2019-11-15 22:51:39",
                    "select": false
                  },
                  {
                    "id": "1195353051395624961",
                    "pid": "1195353513549205505",
                    "name": "修改",
                    "type": 2,
                    "permissionValue": "banner.update",
                    "path": "banner/update/:id",
                    "component": "/cms/banner/form",
                    "icon": null,
                    "status": null,
                    "level": 4,
                    "children": [],
                    "isDeleted": false,
                    "gmtCreate": "2019-11-15 22:49:11",
                    "gmtModified": "2019-11-18 10:52:05",
                    "select": false
                  },
                  {
                    "id": "1195352909401657346",
                    "pid": "1195353513549205505",
                    "name": "添加",
                    "type": 2,
                    "permissionValue": "banner.add",
                    "path": "banner/add",
                    "component": "/cms/banner/form",
                    "icon": null,
                    "status": null,
                    "level": 4,
                    "children": [],
                    "isDeleted": false,
                    "gmtCreate": "2019-11-15 22:48:37",
                    "gmtModified": "2019-11-18 10:52:10",
                    "select": false
                  },
                  {
                    "id": "1195352856645701633",
                    "pid": "1195353513549205505",
                    "name": "查看",
                    "type": 2,
                    "permissionValue": "banner.list",
                    "path": "",
                    "component": null,
                    "icon": null,
                    "status": null,
                    "level": 4,
                    "children": [],
                    "isDeleted": false,
                    "gmtCreate": "2019-11-15 22:48:24",
                    "gmtModified": "2019-11-15 22:48:24",
                    "select": false
                  }

day18

Spirng Security权限框架

框架介绍

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

权限管理-整合SpringSecurity-后端

一、在common模块下创建新的模块spring_security模块
在这里插入图片描述
二、引入依赖

<dependencies>

        <!-- Spring Security依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
        <dependency>
            <groupId>com.yhn</groupId>
            <artifactId>common_utils</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

三、复制代码进去
在这里插入图片描述
需要两个工具类

在这里插入图片描述
四、在service_acl中引入spring_security的依赖
在这里插入图片描述
在这里插入图片描述

整合SpringSecurity-前端

一、到源码找到element-ui然后替换后台项目中node_modules中的element-ui
二、替换其他代码
1、api中新加acl里面的文件

2、login.js

import request from '@/utils/request'

// 登录
export function login(username, password) {
 // debugger
  return request({
    url: '/admin/acl/login',
    method: 'post',
    data: {
      username,
      password
    }
  })
}

// 获取用户信息
export function getInfo(token) {
  return request({
    url: '/admin/acl/index/info',
    method: 'get',
    params: { token }
  })
}

// 登出
export function logout() {
  //debugger
  return request({
    url: '/admin/acl/index/logout',
    method: 'post'
  })
}

// 获取菜单权限数据
export function getMenu() {
  return request({
    url: '/admin/acl/index/menu',
    method: 'get'
  })
}

在这里插入图片描述
3、在项目中安装依赖

npm install --save vuex-persistedstate

4、需要修改几个地方
(1)修改router中的index.js中改成自己项目的地址
(2)修改数据库
acl_permission
在这里插入图片描述

和vue的router中的路径要对应
(3)改为我们网关的地址
在这里插入图片描述
登录的过程
1、先到spring_security的TokenLoginFilter中拦截得到用户名和密码

2、UserDetailsServiceImpl查询数据库中是否有对应的用户
在这里插入图片描述
3、返回的登录成功的TokenLoginFilter里面的方法,放到redis中在这里插入图片描述

4、从header中token得到用户信息,看用户有没有权限,有权限可以做操作
在这里插入图片描述
老师的过程整理
在这里插入图片描述

Nacos配置中心

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
一个模块部署在不同的服务器中,使用的同样的配置文件,当要修改配置文件里面的数据库切换数据库的时候,修改就不方便,使用配置中心一个修改其他所有都可以读取

读取配置文件-第一种方式

在这里插入图片描述
在这里插入图片描述
一、在nacos中创建配置文件-以service_statistics为例
要是注掉dev就不用写dev了
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在application.properties中有spring.profiles.active=dev这个代码会继续加载第三个配置文件
二、创建bootstrap.properties

#配置中心地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

#该配置影响统一配置中心中的dataId
spring.application.name=service-statistics
# 环境设置:dev开发环境、test、prod生产环境
#spring.profiles.active=dev

三、service_statistics的pom文件中引入依赖

  <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>

四、把application.properties中的代码都注掉留着
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
启动之后发现端口号为8999

读取配置文件-第二种方式

一、bootstrap.properties的dev打开

#配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

#该配置影响统一配置中心中的dataId
spring.application.name=service-statistics
# 环境设置:dev开发环境、test、prod生产环境
spring.profiles.active=dev

二、application.properties只开这行代码
在这里插入图片描述
三、在nacos的配置中心中新建一个配置
在这里插入图片描述
开了dev之后就会找配置中心中名字有dev的配置文件
在这里插入图片描述

nacos使用注意事项

no server available
Error watching Nacos Service change

在这里插入图片描述
解决方法

版本一致 最后我的问题是在application.yml中配置错误
这个两个配置不要搞混,都要配上
spring.cloud.nacos.config.server-addr=127.0.0.1:8848 //在 bootstrap.properties 中配置 Nacos server 的地址和应用名
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 //在 application.properties 中配置 Nacos server 的地址

名称(命名)空间切换

在这里插入图片描述
一、创建三个名称空间
在这里插入图片描述
二、通过克隆把public中的配置文件克隆到dev命名空间中,并把端口号改为9999
在这里插入图片描述
在这里插入图片描述
三、在bootstrap.properties中加入dev命名空间的id


#dev命名空间的id
spring.cloud.nacos.config.namespace=8909aa41-90a8-4bef-a43d-fb26e7fd106f

测试成功
在这里插入图片描述

加载多个配置文件

在dev环境
1、创建多个配置文件
service-statistics-dev.properties中去掉端口号


# 服务名
spring.application.name=service-statistics

# 环境设置:dev开发环境、test、prod生产环境
spring.profiles.active=dev
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/bugu?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

#配置mapper.xml文件的路径
mybatis-plus.mapper-locations=classpath:com/yhn/staservice/mapper/xml/*.xml

port.properties

## 服务端口
server.port=8333

在这里插入图片描述

2、修改项目配置文件,加载多个Nacos多个配置文件

#配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

#该配置影响统一配置中心中的dataId
spring.application.name=service-statistics
# 环境设置:dev开发环境、test、prod生产环境
spring.profiles.active=dev

#dev命名空间的id
spring.cloud.nacos.config.namespace=8909aa41-90a8-4bef-a43d-fb26e7fd106f

spring.cloud.nacos.config.ext-config[0].data-id=port.properties
#开启动态刷新配置,否则配置文件修改,工程无法感知
spring.cloud.nacos.config.ext-config[0].refresh=true

在这里插入图片描述

Git版本控制工具

远程仓库Git-码云介绍

在这里插入图片描述

创建一个仓库
在这里插入图片描述

提交代码

在这里插入图片描述

在这里插入图片描述
2、创建一个本地的git仓库,可以选择当前项目作为本地仓库的地址
在这里插入图片描述
在这里插入图片描述
选完之后有的图表变为红色
在这里插入图片描述
3、右键项目选择git里面的add.添加代码到本地库
在这里插入图片描述
在这里插入图片描述
4、设置远程库的地址

在这里插入图片描述
在这里插入图片描述

5、把本地Git仓库内容添加到码云远程GIt仓库
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
表示代码中有一些警告,忽略继续点中间 的按钮
然后点push上传就行
在这里插入图片描述
或者这样提交
在这里插入图片描述
Push是上传Pull是下载,如果在gitee中修改代码之后要先Pull

如何删除保存的gitee用户名和密码

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如何让本地库和远程库断开连接
在这里插入图片描述
在这里插入图片描述

版本之间的切换

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从 GitHub 上克隆项目到本地

  1. 点击 Idea 中的 CVS 选项
    在这里插入图片描述
    在这里插入图片描述
  2. 提示是否为克隆的项目创建一个新工程
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  3. 克隆成功

day19

项目部署

手动打包运行

1、把工程项目打包,运行,因为spring使用maven打包
前体条件安装了maven并配置了环境变量
打开文件所在目录打开cmd窗口使用命令打包
在这里插入图片描述
2、打包完成后在文件中多了一个target目录目录里面有一个jar文件
这就是把当前项目打包成为了一个jar包
在这里插入图片描述
3、cmd进入jar包目录
使用命令启动项目

java -jar demojenkins.jar

项目部署——Jenkins自动部署

一、在Linux系统安装相关软件
1、安装Java环境-jdk环境
2、安装maven环境
上传maven到/usr/local然后解压

cd /usr/local
tar -zvxf apache-maven-3.6.3-bin.tar.gz

配置环境变量
vim /etc/profile 在最后加入下面代码

export MAVEN_HOME=/usr/local/apache-maven-3.6.3
export PATH=$MAVEN_HOME/bin:$PATH 

使用命令查看maven是否安装成功
在这里插入图片描述
3、安装Git

yum install git

4、安装docker
安装些必要的工具

yum install -y yum-utils device-mapper-persistent-data lvm2

添加软件源信息-设置镜像仓库

yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

更新并安装Docker-CE

yum makecache fast

yum -y install docker-ce

开启Docker服务

service docker start

这里到另一台服务器B也安装Docker,然后开始安装下面软件

day20 在线教育项目总结

在线教育项目功能点

一、项目启动准备
nacos和reids启动
1、把后端接口启动
2、启动前端项目(前台和后台系统)
二、项目后台管理系统功能
1、登录功能(springsecurity框架)
2、权限管理功能
(1)菜单管理:列表,添加、修改、删除
(2)角色管理
* 列表、添加、修改、删除、批量删除
*为角色分配菜单
(3)用户管理
列表、添加、修改、删除、批量删除
为用户分配角色
(4)权限管理表之间的关系
使用五张表
3、讲师管理模块
(1)条件查询分页列表、添加、修改、删除
4、课程分类模块
(1)添加课程分类
读取excel里面的课程分类添加到数据库中
(2)课程分类列表
使用树形结构显示课程分类列表
5、课程管理模块
(1)课程列表功能
(2)添加课程
课程发布流程:第一步填写课程基本信息,第二步、添加课程大纲(章节和小节),第三步课程发布信息确认,最终课程发布

课程如何判定是否已经发布了?
点了最终发布之后会把课程表的status字段课程状态改为Normal
在这里插入图片描述
课程发布过程中,中途把课程停止添加,重新去添加新的课程,如何找到之前没有发布完成的课程,继续进行发布?

在课程列表中根据课程状态查询出没有发布的课程,点击课程右边的超链接编辑课程大纲把课程继续发布完成。
在这里插入图片描述
(3)添加小节上传课程视频
6、统计分析模块
(1)生成统计数据–部分数据生成用的随机数还没有实现
(2)统计数据图表显示
三、项目功能点-前台系统
1、首页数据显示
(1)轮播图显示
什么幻灯片在这显示。我们根据id排序每次显示前两条最新的数据
(2)显示热门课程
(3)显示名师
2、注册功能
(1)获取手机验证码

3、登录功能
(1)普通登录
SSO(单点登录)
在这里插入图片描述
使用JWT生成token字符串
JWT有三部分组成:
在这里插入图片描述

在这里插入图片描述

(2)扫描登录
OAuth2
是针对特定问题解决方案
主要针对两个问题:开放系统之间授权,分布式访问

如何获取扫描人信息过程?在这里插入图片描述
4、名师列表功能
5、名师详情功能
6、课程列表功能
(1)条件查询分页列表功能
7、课程详情页
(1)课程信息显示(包含课程基本信息,分类,讲师,课程大纲)
(2)判断课程是否需要购买
8、课程视频在线播放
9、课程支付功能(微信支付)
(1)生成课程订单
(2)生成微信支付二维码
(3)微信最终支付
微信支付实现流程:
如果课程是收费课程,点击立即购买,生成课程订单
点击订单页面去支付,生成微信支付二维码
使用微信扫描支付二维码实现支付
支付之后每隔3秒查询支付状态(是否支付成功—),如果没有支付成功等候,如果支付成功之后,更新订单状态(已经支付状态),向支付记录表添加成功记录
10、课程支付功能

总结项目技术点

在线教育项目采用前后端分离开发

项目使用前端技术

1、vue
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在线教育项目后端技术点总结

一、后端技术点(1)
1、项目采用微服务架构
2、springboot
(1)springboot本质就是spring,只是快速构建spring工程的脚手架
(2)细节
启动类包扫描机制
设置扫描规则
在这里插入图片描述
配置类
(3)springboot配置文件
在这里插入图片描述
3、springCloud
(1)是很多框架的总称,使用这些框架实现微服务架构,教育spring boot实现
(2)组成框架有哪些
在这里插入图片描述
(3)在我们项目中使用阿里巴巴Nacos,替代springcloud一些组件
(4)Nacos
使用nacos作为注册中心实现不同模块的调用
使用Nacos作为配置中心
(5)Fegin服务调用
一个微服务调用另外一个微服务、
(6)熔断器
(7)Gateway网关
(8)版本
在这里插入图片描述
4、MybatisPlus
(1)是对Mybatis的增强
(2)字段填充
(3)乐观锁
(4)逻辑删除
(5)代码生成器
5、EasyExcel
(1)阿里巴巴提供操作excel工具,代码简洁效率高
(2)EasyExcel对poi进行封装,采用SAX方式解析
(3)项目应用在添加课程分类,读取excel数据

二、后端技术点(2)
1、SpringSecurity
(1)在项目整合框架实现权限管理功能
(2)SpringSecurity框架组成:认证和授权
(3)SpringSecurity登录认证过程
在这里插入图片描述

(4)SpringSecurity代码执行过程
2、Redis
(1)项目首页数据提供Redis进行缓存
(2)Redis数据类型
(3)使用Redis作为缓存,不太重要或者不经常改变数据适合放到Redis作为缓存
3、Nginx
(1)反向代理服务器
(2)请求转发,负载均衡,动静分离
4、OAuth2+JWT
(1)针对特定问题解决方案
(2)JWT包含3部分
5、HttpClient
(1)发送请求返回响应的工具,不需要浏览器完成请求和响应的过程
(2)应用场景,微信登录获取扫描人信息,微信支付查询支付状态
6、Cookie
(1)cookie特点
客户端技术
每次发送请求带着cookie值进行发送
cookie有默认有效时长,关闭浏览器cookie默认不存在了,
可以设置cookie有效时长
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目录
通过配置实现

项目描述

在这里插入图片描述
2、这是一个项目还是一个产品
是一个产品;项目是从0开始搭建的

3、测试要求
首页和视频详情页qps单机qps要求 2000+

经常用每秒查询率来衡量域名系统服务器的机器的性能,其即为QPS

QPS = 并发量 / 平均响应时间

4、企业中的项目(产品)开发流程

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不能满足的原因:

数据同步©需要时间,也要正常的时间内响应(A),那么机器数量就要少,所以P就不满足

CP 满足的情况下,A不能满足的原因:

数据同步©需要时间, 机器数量也多§,但是同步数据需要时间,所以不能再正常时间内响应,所以A就不满足AP 满足的情况下,C不能满足的原因:

机器数量也多§,正常的时间内响应(A),那么数据就不能及时同步到其他节点,所以C不满足注册中心选择的原则:

Zookeeper:CP设计,保证了一致性,集群搭建的时候,某个节点失效,则会进行选举行的leader,或者半数以上节点不可用,则无法提供服务,因此可用性没法满足Eureka:AP原则,无主从节点,一个节点挂了,自动切换其他节点可以使用,去中心化

结论:

分布式系统中P,肯定要满足,所以我们只能在一致性和可用性之间进行权衡

如果要求一致性,则选择zookeeper,如金融行业

如果要求可用性,则Eureka,如教育、电商系统

没有最好的选择,最好的选择是根据业务场景来进行架构设计

13、前端渲染和后端渲染有什么区别
前端渲染是返回 json 给前端,通过 javascript 将数据绑定到页面上

后端渲染是在服务器端将页面生成直接发送给服务器,有利于 SEO 的优化

14、能画一下系统架构图吗

在这里插入图片描述

day21尚医通

尚医通是一个网上预约挂号系统,包含后台管理系统和前台用户系统,采用前后端分离开发模式。

项目技术应用广泛,涵盖微服务、全栈、集群、分布式、高并发;技术应用场景合理,并非多技术的盲目堆叠;业务场景贴近实际,按照市场需求开发。

项目后端技术栈,采用主流的SpringBoot+SpringCloud微服务架构,全面使用了目前流行的NoSQL技术,使用Redis缓存数据,使用MongoDB实现高并发读写,整合消息中间件RabbitMQ提高订单的并发量,同时还整合了定时任务,实现就医提醒功能,综合应用了阿里云OSS,短信服务以及微信登录、微信支付,同时增加了微信退款功能。

在这里插入图片描述

项目前端技术栈,采用主流前端框架Vue,使用Nuxt和vue-admin-template模板搭建页面环境,采用Element-ui进行页面布局,Npm进行依赖管理,axios进行异步调用,使用ECharts进行图表显示,实现全栈开发。

全面使用了目前流行的NoSQL技术,使用Redis缓存数据,使用MongoDB实现高并发读写,整合消息中间件RabbitMQ提高订单的并发量,同时还整合了定时任务,实现就医提醒功能,综合应用了阿里云OSS,短信服务以及微信登录、微信支付,同时增加了微信退款功能。
在这里插入图片描述

1项目总结

1.1项目功能总结(后台管理系统)

1、医院设置管理
(1)医院设置列表、添加、锁定、删除
(2)医院列表、详情、排班、下线

2、数据管理
(1)数据字典树形显示、导入、导出

3、用户管理
(1)用户列表、查看、锁定
(2)认证用户审批

4、订单管理
(1)订单列表、详情

5、统计管理
(1)预约统计

1.2项目功能总结(前台用户系统)

1、首页数据显示
(1)医院列表

2、医院详情显示
(1)医院科室显示

3、用户登录功能
(1)手机号登录(短信验证码发送)
(2)微信扫描登录

4、用户实名认证

5、就诊人管理
(1)列表、添加、详情、删除

6、预约挂号功能
(1)排班和挂号详情信息
(2)确认挂号信息
(3)生成预约挂号订单
(4)挂号订单支付(微信)
(5)取消预约订单

7、就医提醒功能

1.3项目技术点总结(后端技术)

1、SpringBoot

2、SpringCloud
(1)Nacos注册中心
(2)Feign
(3)GateWay

3、Redis
(1)使用Redis作为缓存
(2)验证码有效时间、支付二维码有效时间

4、MongoDB
(1)使用MongoDB存储 医院相关数据

5、EasyExcel
(1)操作excel表格,进行读和写操作

6、MyBatisPlus

7、RabbitMQ
(1)订单相关操作,发送mq消息

8、Docker
(1)下载镜像 docker pull
(2)创建容器 docker run

9、阿里云OSS

10、阿里云短信服务

11、微信登录/支付

12、定时任务

1.4项目技术点总结(前端技术)

1、vue
(1)指令

2、Element-ui

3、nuxt

4、npm

5、ECharts

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值