97-微服务项目的编写(上篇)
微服务项目的编写
回顾微服务:
这里需要通过89章博客的学习,才可操作下去,若你知道了Spring Cloud的基础组件(或者可以说框架),那么也可以往下操作
概述:
微服务是一种架构模式或者架构风格,它提倡将单一的应用程序划分成一组小的服务,每个服务运行在其独立的进程中
服务之间相互协调,配置,共同为用户提供最终的价值
通俗点:
建王朝,很有多州郡,每个州郡都是皇上的亲戚,也有贡献突出的将军,镇南侯,平西王等等封疆大吏
他们每个人在自己的管辖区域,就是最高权力的象征
自己的州郡都独立运营(单一架构)
但是风土人情不同,统治的策略也要满足不同的需求(南方有文化底蕴,孔夫子教化即可。西北民风彪悍,肯定要用雷霆手段)
这样大大小小的不同州郡,统一起来,就是一个至高权力的朝廷(总架构)
总结:
将传统的一站式应用,拆分成一个一个独立的服务,基本彻底的解除耦合,每个服务提供单个业务功能的服务
一个服务就做一件事,成立一个独立的进程,能够自行启动和销毁,甚至拥有独立的数据库
优点:
1:每个服务内聚,足够小,开发简单,效率高,一个服务做一件事
2:微服务是松耦合的,无论是开发还是部署阶段都是独立
3:微服务能使用不同的语言开发
4:微服务只是业务逻辑代码,不会和HTML,CSS或其他页面组件混合
5:每个服务都有自己的储存能力,可以有自己的数据库,当然,也可以有统一的数据
缺点:
1:开发人员要处理分布式系统的复杂性
2:随着服务的增加,运维的难度越大
3:系统部署依赖
4:通信成本加大
5:数据一致性难搞
6:系统集成测试麻烦
7:性能监控不易
等等缺点,当然一般还有其他缺点,就不多说了
但既然可用更好的操作,那么一般也会有对应的难处,而这个难处就是缺点,所以缺点是不可避免的
就如修仙一样,如果需要非常高的境界,那么就需要很多的时间努力来提升,那么这个缺点就是使用了很多的时间
所以有时候缺点并不是缺点,只是必须要经历的难处
微服务与微服务架构:
微服务:
强调的是一个服务的大小,关注的是一个点,能够解决某个问题而存在的应用,类似于项目中的某个工程(module)
单独的牙科医院,眼科医院
手机,电脑,沙发,床垫,运动服,每一个都是微服务
专注个体,每个个体完成一个具体的任务或功能
微服务架构:
一种架构模式,它提倡单一应用程序划分一组小的服务,服务之间相互协调,相互配合,为用户提供最终的价值
服务之间采用轻量级通信机制,(HTTP协议的RESTfull)
每个服务都围绕具体的业务进行构建,并且能够独立部署到生产环境中
尽量避免统一的,集中式的服务管理机制
单独的门诊就不要了,我们所有的门诊整合,形成了一个综合性医院
小米生态链,牙刷,电饭锅,手机,路由器,全是小米的
SpringCloud和SpringBoot区别:
SpringBoot专注于快速方便的开发单个个体服务
SpringCloud关注全局微服务的协调和整理,它将SpringBoot开发的一个个单体微服务整合起来
SpringBoot可以独立使用开发,但是SpringCloud基本离不开SpringBoot,属于依赖关系
SpringBoot属于一个科室,SpringCloud是综合医院
SpringCloud对比Dubbo:
相关的作用 | Dubbo | SpringCloud |
---|---|---|
服务注册中心 | Zookeeper | String Cloud Netflix Eureka |
服务调用方式 | RPC | REST API |
服务监控 | Dubbo-monitor | Spring Boot Admin |
断路器 | 不完善 | Spring Cloud Netflix Hystrix |
服务网关 | 无 | Spring Cloud Netflix Zuul |
分布式配置 | 无 | Spring Cloud Config |
服务跟踪 | 无 | Spring Cloud Sleuth |
消息总线 | 无 | Spring Cloud Bus |
数据流 | 无 | Spring Cloud Stream |
批量任务 | 无 | Spring Cloud Task |
品牌机(SpringCloud,固定品牌)和组装机(Dubbo,组装的,那么性能通常高,因为通常组装好的内存或者cpu等等,只是可能兼容不好)的区别
微服务架构项目:
edu-lagou:父工程
edu-api:通用的公共子模块
edu-eureka-boot:服务(注册)中心:7001
edu-ad-boot:广告微服务:8001
edu-user-boot:用户微服务:8002
edu-authority-boot:认证微服务:80
之所以是80端口,而不是8003端口,主要是为了操作微信相关操作,因为对应的回调只会操作80端口,所以这里需要80端口
那么8003这里也就不操作了,这里我们就认为已经操作了吧
edu-course-boot:课程微服务:8004
edu-comment-boot:留言微服务:8005
edu-pay-boot:支付微服务:8006
edu-order-boot:订单微服务:8007
edu-config-boot:配置中心:8008
edu-gateway-boot:网关微服务:9001
搭建项目:
父工程edu-lagou:
最终成果,但凡看到这四个字,最好结合对应图片后面的代码,因为这是对应图片后面的代码积累的成果
后面就不提示了,这里自己注意:
在创建项目时,如果你加上了多余的目录或者说有没用的目录,那么会自动的创建,这是idea的作用
父工程为聚合项目,打包方式为pom
src目录也没有意义,可以删除
依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.lagou</groupId><artifactId>edu-lagou</artifactId><version>1.0-SNAPSHOT</version><!--后面的子项目,基本是Spring Boot项目(Boot项目的创建和maven的创建是不同的,其中maven需要modules标签才可算是一个项目,通常是因为项目文件的原因,可能是.idea里面的文件的原因,或者idea本身的原因,当然,有时候可能还会认为是一个项目,当然,这里只需要了解即可,因为基本并没有什么作用,而Boot项目基本是独立的)
所以不会当成他(父工程)的子项目
只是路径是子的而已(所以我们打包父项目,不会都打包其子路径项目,通常会自动出现,可能是boot项目会自动的保存其主要的class文件吧,这里我不确定,可能是操作了联系,因为后面的子项目是手动操作不了编译和打包的,即不能独立出来,即不能单独出来,相当于他操作了idea当前的整个所有项目,而不是单独操作,而启动可以,所以启动是利用了联系的),这里就认为子项目是路径的子了(注意即可)
所以这里并不会出现maven管理的子项目的modules标签--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.2</version><relativePath/> <!-- lookup parent from repository --></parent><properties><java.version>1.8</java.version><spring-cloud.version>2020.0.0</spring-cloud.version></properties><packaging>pom</packaging><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency>
<!--如果你需要日志操作,可以加上这个:<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId></dependency>然后修改这个<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency>最后,在项目的资源文件夹下,加上log4j.properties文件,内容如下:log4j.rootLogger=info,A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=[%t] [%c]-[%p] %m%n
log4j.logger.com.lagou.mapper=TRACE这样,我们在访问后端时,基本就可以看具体日志了
通常可以看sql语句的日志(结构日志,如具体的sql是什么,对应参数值是什么等等)但是该操作,通常不会显示启动项目时的日志信息,如果需要启动项目时的日志信息,可以改回来即可,即spring-boot-starter-logging主要的作用是操作项目启动的信息,虽然也会覆盖上面加上的slf4j-log4j12的部分操作
--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement></project>
创建子项目(子工程),服务中心edu-eureka-boot(7001):
最终成果:
对应的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.lagou</groupId><artifactId>edu-lagou</artifactId><version>1.0-SNAPSHOT</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.lagou</groupId><artifactId>edu-eureka-boot</artifactId><version>0.0.1-SNAPSHOT</version><name>edu-eureka-boot</name><!-- <name>:在maven里面显示的名称,如果删除,一般默认为<artifactId>标签里面的值--><description>edu-eureka-boot</description><!--<description>:描述,就是字面的意思,即描述的信息,一般可以删除,当然name标签也可以删除--><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
修改配置文件后缀为yml,并加上如下:
server:# 配置服务端口port: 7001
eureka:instance:hostname: localhostclient:service-url:# 配置eureka服务器地址,使用${}来使用当前配置,与对应的配置先后没有关系#因为他并不是读取一个操作一次,而是都进行读取,然后操作,所以将上面的server.port放在后面也行defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/#是否需要将自己注册到注册中心(注册中心集群需要设置为true,因为是本身所以不需要注册,虽然也可以注册)register-with-eureka: false#是否需要搜索服务信息 因为自己是注册中心所以为falsefetch-registry: false
启动类:
package com.lagou;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication
@EnableEurekaServer //开启Eureka服务
public class EduEurekaBootApplication {public static void main(String[] args) {SpringApplication.run(EduEurekaBootApplication.class, args);}}
启动,然后启动好后,访问localhost:7001,若出现如下,则代表操作成功:
最后将数据库执行,地址如下:
链接:https://pan.baidu.com/s/1-RIsUvp0OSk0b3cb11Cq1A
提取码:alsk
执行后,需要改一改,将edu_ad的promotion_space表的id为1和id为3的两个name互换
因为promotion_ad主要操作1,且后面以首页顶部轮播为开始的
创建子项目,广告微服务edu-ad-boot(8001):
最终成果(下面的bootstrap.yml是在以后会操作的,你只需要改变原来的后缀即可,不需要改成这个名称):
对应的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.lagou</groupId><artifactId>edu-lagou</artifactId><version>1.0-SNAPSHOT</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.lagou</groupId><artifactId>edu-ad-boot</artifactId><version>0.0.1-SNAPSHOT</version><name>edu-ad-boot</name><description>edu-ad-boot</description><properties><java.version>11</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- eureka客户端 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!-- mybatis plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version></dependency><!-- mysql --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--pojo持久化使用--><dependency><groupId>javax.persistence</groupId><artifactId>javax.persistence-api</artifactId><version>2.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
在对应的启动类所在的包下(以后默认在启动类所在的包下创建,所以后面就不写了,基本只会给出开头提示一下)
创建controller.AdController类(先不进行操作):
package com.lagou.controller;/****/
public class AdController {}
然后再创建entity.PromotionAd类(先不进行操作):
package com.lagou.entity;/****/
public class PromotionAd {}
再创建mapper.AdDao接口(先不进行操作):
package com.lagou.mapper;/****/
public interface AdDao {}
再创建service.AdService接口及其实现类:
package com.lagou.service;/****/
public interface AdService {}
package com.lagou.service.impl; //在接口所在的包下面,创建的impl包下import com.lagou.service.AdService;/****/
public class AdServiceImpl implements AdService {}
将配置文件修改成yml后缀,并加上如下:
server:port: 8001
spring:application:name: edu-ad-bootdatasource:driver-class-name: com.mysql.cj.jdbc.Driver#记得要是自己的数据库地址,用户名,密码,否则基本执行不了对应代码(即报错,除非你的与我相同)#启动不会报错,只是执行不了对应代码而已,因为那时是使用到了#对于mysql来说,启动只是准备信息而已,并没有验证,大多数都没有验证,除了个别需要的属性,比如下面的url属性#需要写上,否则启动会报错,而测试时,对应的注解@SpringBootTest相当于先启动,然后执行方法#只是他们一起操作了,但是启动是肯定先操作的#比如我这里就是如下:url: jdbc:mysql://192.168.164.128:3306/edu_ad?useUnicode=true&characterEncoding=utf8&serverTimezone=UTCusername: rootpassword: QiDian@666
eureka:client:service-url:defaultZone: http://localhost:7001/eureka/register-with-eureka: truefetch-registry: trueinstance:prefer-ip-address: trueinstance-id: ${spring.cloud.client.ip-address}:${server.port}
启动类:
package com.lagou;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication
@EnableEurekaClient // 注册到中心的客户端
@MapperScan("com.lagou.mapper") //自己的mapper包所在的位置
public class EduAdBootApplication {public static void main(String[] args) {SpringApplication.run(EduAdBootApplication.class, args);}}
PromotionAd实体类:
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import javax.persistence.Id;
import java.io.Serializable;
import java.util.Date;/****/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PromotionAd implements Serializable {private static final long serialVersionUID = -29054335318173039L;//@Idprivate Integer id;/*** 广告名*/private String name;private Integer spaceId;private String keyword;private String htmlContent;private String text;private String link;private Date startTime;private Date endTime;private Date createTime;private Date updateTime;private Integer status ;private Integer priority;private String img;
}
接口AdDao:
package com.lagou.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.PromotionAd;/****/
public interface AdDao extends BaseMapper<PromotionAd> {}
接口AdService及其实现类:
package com.lagou.service;import com.lagou.entity.PromotionAd;import java.util.List;/****/
public interface AdService {List<PromotionAd> getAdsBySpaceId(Integer sid);
}
package com.lagou.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lagou.entity.PromotionAd;
import com.lagou.mapper.AdDao;
import com.lagou.service.AdService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;/****/
@Service
public class AdServiceImpl implements AdService {@Autowiredprivate AdDao adDao;@Overridepublic List<PromotionAd> getAdsBySpaceId(Integer sid){QueryWrapper<PromotionAd> qw = new QueryWrapper<>();qw.eq("space_id", sid); // where space_id = #{sid}return adDao.selectList(qw);}
}
AdController类:
package com.lagou.controller;import com.lagou.entity.PromotionAd;
import com.lagou.service.AdService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.List;/****/
@RestController
@RequestMapping("ad")
@CrossOrigin
//操作解决跨域,那么就不用我们自己配置跨域了,比如添加依赖,添加配置(他们两个通常是一起的)等等
//他是Spring Web依赖自带的
//即加上这个,那么前端才可以访问,否则前端会报跨域的错误
public class AdController {@Autowiredprivate AdService adService;@GetMapping("getAdsBySpaceId/{spaceid}")public List<PromotionAd> getAdsBySpaceId(@PathVariable("spaceid") Integer sid){List<PromotionAd> list = adService.getAdsBySpaceId(sid);return list;}
}
接下来我们可用启动启动类,但是现在有个问题,我们都是找到对应的启动类来启动的,如果这样的类非常多
那么一个一个的找非常的麻烦,有什么方法可用统一管理呢,我们点击如下:
使得下面出现Services这个选项,如果有,那么可以不点击,再点击如下:
找到这个(如果你点击过了,即上图中的显示,那么他不会出现):
至此可以有:
现在就不用我们自己去寻找启动类了,上面的Not Started代表没有启动过(一般新加的项目启动类通常会突然出现在这里,其中如果关闭当前窗口或者说项目,然后重新打开,所有项目基本都会重新在这个地方)
下面的Running代表启动的或者正在启动,Finished代表启动过(或者说正在连接)
所以Not Started基本上只有第一次的启动之前(或者关闭连接)才会出现,当然还有其他状态,比如Failed,代表启动过程中,启动失败(如果是编译过程中报错,那么状态不变)
关闭连接是(右键点击如下即可变成Not Started):
访问这个localhost:8001/ad/getAdsBySpaceId/1地址,若出现数据,代表操作成功
接下来下载下面的前端项目:
链接:https://pan.baidu.com/s/1cSGlkfHUlzOXfJzYUNDa2A
提取码:alsk
上面是已经大致操作好的,你通过后面的说明补充修改即可,一般都会存在的,可以自己先查看,来决定是否添加或者修改
接下来在下面的地址,找到对应的代码(后面基本只会给出代码,而不会给出具体图片的操作,因为既然你能看到这个博客,那么对于后面的代码应该有一定的了解,通常可以自己来启动前端项目,然后来点击测试,所以具体的测试,自己进行)
使用Element-UI的轮播组件展示广告:https://element.faas.ele.me/#/zh-CN/component/installation
找到的代码(可能随着时间的推移会有所改变)如下:
<template><el-carousel indicator-position="outside"><el-carousel-item v-for="item in 4" :key="item"><h3>{{ item }}</h3></el-carousel-item></el-carousel>
</template><style>.el-carousel__item h3 {color: #475669;font-size: 18px;opacity: 0.75;line-height: 300px;margin: 0;}.el-carousel__item:nth-child(2n) {background-color: #99a9bf;}.el-carousel__item:nth-child(2n+1) {background-color: #d3dce6;}
</style>
我们找到前端项目的Index.vue组件,添加如下:
<!--头部登录导航条--><Header></Header><div style=" width:1200px; margin:0px auto; margin-top:20px;"><el-carousel indicator-position="outside"><el-carousel-item v-for="item in 4" :key="item"><h3>{{ item }}</h3></el-carousel-item></el-carousel>
</div>
对应的style标签的样式记得加上,然后直接的启动项目,看看效果,其他的不用管,若效果正确,那么添加成功
再在如下添加部分代码(其余的代码可以不用管,下面只给出部分):
created() {// 加载顶部轮播广告this.getAdList();
methods: {// 加载顶部轮播广告getAdList(){return this.axios.get("http://localhost:8001/ad/getAdsBySpaceId/1").then((result) => {console.log(result);this.adList = result.data;}).catch( (error)=>{this.$message.error("获取轮播广告失败!");} );},
data() {return {adList:null, //广告集合};
然后修改如下:
<!--首页顶部轮播广告--><div style=" width:1200px; margin:0px auto; margin-top:20px;"><el-carousel indicator-position="outside"><el-carousel-item v-for="(item, index) in adList" :key="index"><a :href="item.link"><img :src="item.img" style="width:100%;height:100%;object-fit:cover"/> <!--类似于 background-size: cover;--></a></el-carousel-item></el-carousel></div>
启动前端项目看页面效果,如果有效果,那么操作完成
这里给出一个图片转换的地址,http://www.mf2.cn/img2base64/,自己可以进行操作
创建子项目,网关微服务edu-gateway-boot(9001):
最终成果:
网关相当于你想吃到饺子馅,得先吃饺子皮
每个提供服务的url如果都暴露出来的话,不安全,我们应该采用一种转发的形式来提供统一的url,将具体的服务url隐藏
现在在父工程创建该子项目:
依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.lagou</groupId><artifactId>edu-lagou</artifactId><version>1.0-SNAPSHOT</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.lagou</groupId><artifactId>edu-gateway-boot</artifactId><version>0.0.1-SNAPSHOT</version><name>edu-gateway-boot</name><description>edu-gateway-boot</description><properties><java.version>11</java.version></properties><dependencies><!-- eureka 客户端 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!--GateWay 网关--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!--引入webflux--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
启动类:
package com.lagou;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication
@EnableEurekaClient
public class EduGatewayBootApplication {public static void main(String[] args) {SpringApplication.run(EduGatewayBootApplication.class, args);}}
将配置后缀修改成yml,并添加如下:
server:port: 9001
eureka:client:service-url:defaultZone: http://localhost:7001/eurekaregister-with-eureka: truefetch-registry: trueinstance:prefer-ip-address: trueinstance-id: ${spring.cloud.client.ip-address}:${server.port}
spring:application:name: edu-gateway-bootcloud:gateway:routes:- id: edu-routes-ad # 路由名uri: lb://edu-ad-boot # 去注册中心查找的微服务名predicates: # 当断言成功后,交给某一个微服务处理时使用的是转发- Path=/ad/**filters:- StripPrefix=1 # 去掉uri中的第一部分
启动后,访问http://localhost:9001/ad/ad/getAdsBySpaceId/1,若访问成功,则操作成功
将这个地址放在如下(修改如下):
// 加载顶部轮播广告getAdList(){return this.axios.get("http://localhost:9001/ad/ad/getAdsBySpaceId/1").then((result) => {console.log(result);this.adList = result.data;}).catch( (error)=>{this.$message.error("获取轮播广告失败!");} );},
继续看页面效果,如果有效果,那么操作完成
SSO单点登录:
概述:
传统的单体架构项目,我们将登陆信息保存在session中,需要获取getSession就可以了
但是要知道session是属于服务器的,也就是一个tomcat对应一个session(只是有多个sessionid)
当我们做分布式集群架构的项目时,tomcat多个,session就对应多个,那在tomcat1中保存的用户信息,在tomcat3中就获取不到了
所以,session做是否登录的验证并不全面(76章博客也说明过类似的登录,且基本操作完毕,但始终也只是针对sessionid而已,通常是通过sessionid来得到是否登录的消息,而该消息一般放在一个库中,比如mysql或者redis里面等等,使得,如果他操作了其他的服务器,如负载均衡造成的其他服务器,那么也会认为是登录)
而且,我们发现一个有意思的事情
要知道,两个网站的网址分别是:https://www.taobao.com 和 https://www.tmall.com,域名完全不一样
是如何做到一次登录,两个系统都登录呢
原因是"阿里巴巴"专门为这些应用搭建了一个"登录身份认证中心"
比如一个sessionid可以对应一个身份,该身份可以实现多个网站登录,当然,你也可以认为是上面的可以登录的消息
无论谁登录,都要得到这个中心统一颁发的"令牌",方可成功
搭建"颁发令牌的中心"的解决方案,我们就叫做"单点登录系统"
单点登录全称 Single Sign On(以下简称SSO)
是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分
流程详述:
1:接收用户名和密码
2:验证用户名和密码
3:生成token(使用JWT),将用户信息写到token中
4:把token写到redis中,并设置过期时间
5:把token返回给客户端,并写到cookie中
6:客户端每次打开浏览器都会从cookie中获取token,然后去sso校验token(是否存在或是否正确)
那么可以用sessionid代替token吗,实际上也可以,但是,如果结合其他的框架
可能sessionid会被其他框架改变(比如Spring Security框架,可能会使得改变)
所以最好是自己的token(cookie)
那么为什么需要其他库来存放token呢,实际上访问不同服务器对应的sessionid不同
之所以不同是因为对应的域名(简单来说就是服务器不同了,端口不会影响cookie,那么sessionid也不会影响,当然通常都会设置不同,具体可以看百度)发生了改变
而服务器不同,除了端口不同外,若域名不同自然对应的当前cookie不会出现在对方服务器的请求中
这时就需要第三个库来存放,使得也可以访问多个服务器地址,当然,这里因为只修改了端口
所以cookie是会出现在对方里面的,即可以使用cookie,那么redis的作用是什么呢
主要是为了针对域名,当然,就算是端口也可以操作
但这里并没有什么作用(也没有取得token的信息的操作,因为只是一个服务器,在多个服务器的情况下,我们通常在操作检查token时,进行取出检查,也就是校验,即只返回redis的key,然后取出来进行检查对比或者说校验对比,这样也防止不会将token给前端了,也加强了安全,也防止会被解密第二部分和第一部分,虽然第一部分解密也没有什么问题,因为通常是不变的,反正也基本不知道算法解密的原理,除非很牛)
因为他并没有操作类似于76章博客对应的相同浏览器的信息使得取得同一个token的操作
如果可以的话,你可以进行操作测试,看上面的解释操作,或者可以百度其他方式,这里就给出雏形了,但基本都是这样的
就意思一下了
具体操作可以看76章博客,虽然76章博客,没有给出操作代码,也并没有操作不同域名
但他还是操作了不同域名的操作方式(删除共享后的操作就是了,当然也操作了端口,即不同服务器,因为依赖,即他操作了真正的不同地址,而不是因为只有端口不同,使得cookie相同)
详情参考下图:
JWT:
Json Web Token,基于json格式信息的一种token令牌
JWT token 包含三部分,第一部分header、第二部分payload、第三部分签证
第一部分:利用base64算法处理json信息,作为token第一部分
{typ: jwt,alg: HS256
}
第二部分:利用base64算法处理json信息,作为token第二部分,可存放用户的各项信息
{exp:xxx,uid:xxx,name:xxx
}
一般情况下,base64容易被解密,比如加密的图片,以及这里等等,所以第三部分则不是使用base64的加密,看如下解释:
第三部分:将"第一部分,第二部分"进行操作后
然后调用第一部分的header中指定的alg指定的HS256算法对第三部分进行加密处理(需要一个secret秘钥,不对外公开,实际上其他人得到该加密信息是基本不可能解密的,因为该值不公开,即该值不知道,他参与加密了,除非你也知道该密钥或者他的工作原理,即解密原理,考虑到正常情况下,基本解密不了,所以解密忽略,而又由于这是内部的,所以其他人基本不可能知道,所以就算你得到的token,那么基本解密不了了)
他的加密内容用来作为第三部分结果
三个部分(不是第三部分,而是三个部分)的总体如下,例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXNzd29yZCI6IjEiLCJuaWNrbmFtZSI6IuW5sumlreS6uiIsInBvcnRyYWl0IjoiaHR0cDovLzE5Mi4xNjguMjA0LjE0MS9ncm91cDEvTTAwLzAwLzAwL3dLak1qV0FhWVJxQVJ0TWxBQVdJQ2ZRbkh1azk2MS5qcGciLCJleHAiOjE2MTMyMDI0MDksInVzZXJpZCI6MTAwMDMwMDI0fQ.s38sQnGe9Eybr8hfcFuJyDIg-tHpQo7vgRDAStthuRc
所以我们使用的类型是jwt,算法是HS256,具体加密的内容是第二部分,第三部分(密钥)
从结果看,有两个点(" . "):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.(最后面有一个"."),代表第一部分加密结果(第一部分也会给自己加密)
eyJwYXNzd29yZCI6IjEiLCJuaWNrbmFtZSI6IuW5sumlreS6uiIsInBvcnRyYWl0IjoiaHR0cDovLzE5Mi4xNjguMjA0LjE0MS9ncm91cDEvTTAwLzAwLzAwL3dLak1qV0FhWVJxQVJ0TWxBQVdJQ2ZRbkh1azk2MS5qcGciLCJleHAiOjE2MTMyMDI0MDksInVzZXJpZCI6MTAwMDMwMDI0fQ.(最后面有一个"."),代表第二部分加密结果
s38sQnGe9Eybr8hfcFuJyDIg-tHpQo7vgRDAStthuRc(那么剩下的就是第三部分,这里也就是密钥),这一部分通常不会对外公开
所以你也很难猜到,通常来说,该值存在程序里面,无论加密还是解密,程序需要添加他来操作
解密后的值是相同的(加密前的),那么就会通过
但是用户虽然会得到对应的第一部分和第二部分和第三部分,但是第三部分的解密方式却是根据程序来的(除非知道秘钥,或者工作原理,当然,如果知道工作原理,自然可以解密,这里以只知道秘钥为主),秘钥通过算法变成密钥的
注意:第三部分和第二部分,通常多次的生成是不同的(因为不只是根据数据,还有算法,其中第二部分只有部分不同,大体相同,而第三部分基本都不同),但是解密还是可以相同,而其他部分(也就是第一部分了)基本多次的生成是相同的(因为只是根据数据,基本没用算法),这里注意即可,所以即这里的密钥有点特殊,总不能得到的值是相同的吧(虽然是因为算法的原因导致不同,但这是为了更难破解,因为如果只有一个,那么破解后,所有的都会破解了,而多个就难了,因为多个可能是不同的秘钥形成的,因为没有很多的精力,除非总体也只有一个秘钥,不是密钥,秘钥形成密钥的,那么破解后,所有的都会破解了)
所以简单来说,密钥是验证的主体,而正是因为不同,所以可以是多个密钥都可以验证成一个值,即都可以登录上
当然,通常不同的用户自然肯定是不会的(不同秘钥,比如循环,只要有一个验证成功,即可,虽然需要更多的执行空间)
除非是相同的秘钥,虽然相对不安全些(因为一个秘钥知道全部,而不是部分),但实际上还是非常难破解的(需要更加高级的密码学知识)
核心代码:
创建子项目,认证微服务edu-authority-boot(80):
最终成果:
对应的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.lagou</groupId><artifactId>edu-lagou</artifactId><version>1.0-SNAPSHOT</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.lagou</groupId><artifactId>edu-authority-boot</artifactId><version>0.0.1-SNAPSHOT</version><name>edu-authority-boot</name><description>edu-authority-boot</description><properties><java.version>11</java.version></properties><dependencies><!-- web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- eureka客户端 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!-- mybatis plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version></dependency><!-- mysql --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--pojo持久化使用--><dependency><groupId>javax.persistence</groupId><artifactId>javax.persistence-api</artifactId><version>2.2</version></dependency><!--自动getset--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version></dependency><!--jwt--><dependency><!--存在有Algorithm这个类操作--><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.8.0</version></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
在启动类所在的包下,创建entity.User类:
package com.lagou.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;/****/
@Data //get和set都全部生成了
@Table(name="user")
public class User implements Serializable {private static final long serialVersionUID = -89788707895046947L;/*** 用户id*///@Id@TableId(type = IdType.AUTO)private Integer id;/*** 用户昵称*/private String name;/*** 用户头像地址*/private String portrait;/*** 注册手机*/private String phone;/*** 用户密码(可以为空,支持只用验证码注册、登录)*/private String password;/*** 注册ip*/private String regIp;/*** 是否有效用户*/private Object accountNonExpired;/*** 账号是否未过期*/private Object credentialsNonExpired;/*** 是否未锁定*/private Object accountNonLocked;/*** 用户状态:ENABLE能登录,DISABLE不能登录*/private String status;/*** 是否删除*/private Object isDel;/*** 注册时间*/private Date createTime;/*** 记录更新时间*/private Date updateTime;}
在entity包下创建UserDTO类:
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;/****/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
// 数据传输对象(DTO) Data Transfer Object
public class UserDTO<User> implements Serializable {private static final long serialVersionUID = 1L;private int state; // 操作状态private String message; // 状态描述private User content; // 响应内容private String token; //令牌
}
在entity包下创建EduConstant类(常量类):
package com.lagou.entity;/****/
/**常量内容说明*/
public class EduConstant {// 状态码public static Integer ERROR_NOT_FOUND_PHONE_CODE = 1;public static Integer ERROR_PASSWORD_CODE = 2;public static Integer LOGIN_SUCCESS_CODE = 3;public static Integer TOKEN_SUCCESS_CODE = 4;public static Integer TOKEN_TIMEOUT_CDOE = 5;public static Integer TOKEN_NULL_CODE = 6;public static Integer TOKEN_ERROR_CDOE = 7;// 状态描述public static String ERROR_NOT_FOUND_PHONE = "该手机尚未注册";public static String ERROR_PASSWORD = "登录失败,帐号密码不匹配";public static String LOGIN_SUCCESS = "登录成功";public static String TOKEN_SUCCESS = "令牌校验通过";public static String TOKEN_TIMEOUT = "令牌过期";public static String TOKEN_ERROR1 = "令牌格式错误!或为空令牌";public static String TOKEN_ERROR2 = "校验失败,token令牌就是错误的";//上面是对应的,比如ERROR_NOT_FOUND_PHONE_CODE = 1对应ERROR_NOT_FOUND_PHONE = "该手机尚未注册"//以此类推}
封装好的jwt工具类JwtUtil:
在启动类所在的包下,创建tools.JwtUtil类(这个类了解即可):
package com.lagou.tools;import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.lagou.entity.User;import java.util.Date;
import java.util.HashMap;
import java.util.Map;/****/
public class JwtUtil {private static final long EXPIRE_TIME = 15 * 60 * 1000;private static final String TOKEN_SECRET = "laosunshigedashuaige666"; //secret秘钥:自定义/*** 生成签名,15分钟过期* @param **username*** @param **password*** @return*/public static String createToken(User user) {try {// 设置过期时间Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);// 私钥和加密算法Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);// 设置头部信息Map<String, Object> header = new HashMap<>(2);header.put("typ", "JWT");header.put("alg", "HS256");// 返回token字符串,基本上很难通过后端的JWT来得到对于的数据//如果可以,那么自己可以测试,具体可以百度//我们一般通过前端来得到return JWT.create()// 第一部分.withHeader(header) // 第二部分.withClaim("nickname", user.getName()) .withClaim("userid", user.getId()).withClaim("password", user.getPassword()).withClaim("portrait", user.getPortrait()).withExpiresAt(date) //设置过期时间// 第三部分.sign(algorithm); } catch (Exception e) {e.printStackTrace();return null;}}/*** 检验token是否正确* @param **token*** @return*/public static int isVerify(String token) {try {Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); //使用HMAC256加密算法,生成签名JWTVerifier verifier = JWT.require(algorithm).build();verifier.verify(token); // 解析tokenreturn 0;// 校验通过} catch (TokenExpiredException e) {e.printStackTrace();System.out.println("令牌过期");return 1; // 令牌过期} catch (JWTDecodeException e) {e.printStackTrace();System.out.println("令牌格式错误!或为空令牌!");return 2;// 校验失败,token令牌就是错误的} catch (JWTVerificationException e) {e.printStackTrace();System.out.println("校验失败,token令牌就是错误的");return 3;// 校验失败,token令牌就是错误的}}/***从token解析出 用户编号 信息* @param token* @return*/public static int parseTokenUserid(String token) {DecodedJWT jwt = JWT.decode(token);return jwt.getClaim("userid").asInt();}/***从token解析出 昵称 信息* @param token* @return*/public static String parseTokenNickname(String token) {DecodedJWT jwt = JWT.decode(token);return jwt.getClaim("nickname").asString();}/***从token解析出 头像 信息* @param token* @return*/public static String parseTokenPortrait(String token) {DecodedJWT jwt = JWT.decode(token);return jwt.getClaim("portrait").asString();}/***从token解析出 密码 信息* @param token* @return*/public static String parseTokenPassword(String token) {DecodedJWT jwt = JWT.decode(token);return jwt.getClaim("password").asString();}}
创建mapper.UserMapper接口:
package com.lagou.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.User;/****/
public interface UserMapper extends BaseMapper<User> {}
再创建service.UserService接口及其实现类:
package com.lagou.service;import com.lagou.entity.UserDTO;/****/
public interface UserService {UserDTO login(String phone, String password);UserDTO checkToken(String token);
}
package com.lagou.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lagou.entity.EduConstant;
import com.lagou.entity.User;
import com.lagou.entity.UserDTO;
import com.lagou.mapper.UserMapper;
import com.lagou.service.UserService;
import com.lagou.tools.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;/****/
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RedisTemplate<Object,Object> redisTemplate;@Overridepublic UserDTO login(String phone, String password) {UserDTO dto = new UserDTO();// 创建条件构造QueryWrapper<User> queryWrapper = new QueryWrapper();// 编写具体的查询条件queryWrapper.eq("phone", phone);Integer i1 = userMapper.selectCount(queryWrapper);if(i1 == 0){ // 手机号不存在dto.setState(EduConstant.ERROR_NOT_FOUND_PHONE_CODE);dto.setMessage(EduConstant.ERROR_NOT_FOUND_PHONE);dto.setContent(null);return dto;}else{queryWrapper.eq("password", password);// 调用mp的查询方法selectOneUser user = userMapper.selectOne(queryWrapper);if(user == null){ // 帐号密码不匹配dto.setState(EduConstant.ERROR_PASSWORD_CODE);dto.setMessage(EduConstant.ERROR_PASSWORD);dto.setContent(null);return dto;}else{// 登录成功dto.setState(EduConstant.LOGIN_SUCCESS_CODE);dto.setMessage(EduConstant.LOGIN_SUCCESS);//dto.setContent(user);String token = JwtUtil.createToken(user);dto.setToken(token);
//用来解决乱码问题的
// redisTemplate.setKeySerializer(new StringRedisSerializer());
// //设置序列化Value的实例化对象
// redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());// 将token和用户信息存在redis中redisTemplate.opsForValue().set(token, token,15, TimeUnit.SECONDS);System.out.println("token = " + token);return dto;}}}public UserDTO checkToken(String token){int i = JwtUtil.isVerify(token);UserDTO dto = new UserDTO();if(i == 0){dto.setState(EduConstant.TOKEN_SUCCESS_CODE);dto.setMessage(EduConstant.TOKEN_SUCCESS);// 校验通过,重新设置过期时间redisTemplate.opsForValue().set(token, token,15, TimeUnit.SECONDS);}else if(i == 1){dto.setState(EduConstant.TOKEN_TIMEOUT_CDOE);dto.setMessage(EduConstant.TOKEN_TIMEOUT);}else if(i == 2){dto.setState(EduConstant.TOKEN_NULL_CODE);dto.setMessage(EduConstant.TOKEN_ERROR1);}else{dto.setState(EduConstant.TOKEN_ERROR_CDOE);dto.setMessage(EduConstant.TOKEN_ERROR2);}return dto;}
}
再创建controller.AuthorityContoller类:
package com.lagou.controller;import com.lagou.entity.UserDTO;
import com.lagou.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/****/
@RestController
@RequestMapping("user")
@CrossOrigin
public class AuthorityContoller {@Autowiredprivate UserService userService;@GetMapping("login")public UserDTO login(String phone, String password){UserDTO dto = userService.login(phone, password);return dto;}@GetMapping("checkToken")public UserDTO checkToken(String token){System.out.println("待校验的token = " + token);UserDTO dto = userService.checkToken(token);return dto;}
}
启动类:
package com.lagou;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication
@EnableEurekaClient // 注册到中心的客户端
@MapperScan("com.lagou.mapper")
public class EduAuthorityBootApplication {public static void main(String[] args) {SpringApplication.run(EduAuthorityBootApplication.class, args);}}
将配置文件后缀修改成yml,并添加如下:
server:# 服务端口号port: 80
spring:application:# 服务名称 - 服务之间使用名称进行通讯name: edu-authority-bootdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.164.128:3306/edu_user?useUnicode=true&characterEncoding=utf8&serverTimezone=UTCusername: rootpassword: QiDian@666redis:host: 192.168.164.128port: 6379
eureka:client:service-url:# 填写注册中心服务器地址defaultZone: http://localhost:7001/eureka# 是否需要将自己注册到注册中心register-with-eureka: true# 是否需要搜索服务信息fetch-registry: trueinstance:# 使用ip地址注册到注册中心prefer-ip-address: true# 注册中心列表中显示的状态参数instance-id: ${spring.cloud.client.ip-address}:${server.port}
启动启动类,访问localhost:8003/user/login?phone=110&password=123,若登录成功,且redis里面多出来了数据,那么操作成功
如果不看redis显示,那么可以不操作解决中文问题,具体在88章博客有说明
前端联调:
我们进入Header.vue组件:
修改或者查看如下
methods: {// 前去登录login(){ return this.axios.get("http://localhost:80/user/login" , {params:{phone:this.phone,password:this.password}}).then( (result)=>{console.log( result );if(result.data.state == 3){// 1.关闭登录框this.dialogFormVisible = false ; // 2.更新登录状态this.isLogin = true;// 3.将返回的token保存在cookie中,先注释,或者添加了后面的方法,那么可以不注释//this.setCookie("user",result.data.token,600);// 4.解析token中的数据(昵称和头像),基本只会解析第二部分的数据得到const code = jwtDecode(result.data.token);this.user = code;console.log( this.user );// 刷新页面,这里先进行注释//this.$router.go(0);}else if(result.data.state == 1){this.$message.error("手机号(账号)不存在");}else if(result.data.state == 2){this.$message.error("密码错误!");}} ).catch( (error)=>{this.$message.error("登录失败!");});},
在这之前,我们首先需要解析token的工具,在客户端中输入如下:
npm install jwt-decode --save
#一般来说,他的安装,基本只是拿取对应的js而已
那么就可以引入这个了:
import jwtDecode from 'jwt-decode'
<script>
import jwtDecode from 'jwt-decode'
import { setInterval, clearInterval } from 'timers';export default {
接下来我们登录,输入110,以及123密码(账号登录),如果有头像,说明操作完成
接下来在methods里添加如下(如果存在那么就不需要):
//设置cookiesetCookie(key,value,expires){var exp = new Date();exp.setTime(exp.getTime() + expires*1000);document.cookie = key + "=" + escape (value) + ";expires=" + exp.toGMTString();//document.cookie = "a" + "=" + escape (value) + ";expires=" + exp.toGMTString();//这只是添加,而不是赋值,这是特殊的//expires代表是过期时间,在检查中可以发现对应的过期时间的属性也就是这个属性},//从cookie中获取tokengetCookie(key){var name = key + "=";if(document.cookie.indexOf(';') > 0){var ca = document.cookie.split(';');for(var i=0; i<ca.length; i++) {var c = ca[i].trim();if (c.indexOf(name)==0) { return c.substring(name.length,c.length); }}}else{var ca = document.cookieif (ca.indexOf(name)==0) { return ca.substring(name.length,ca.length); }}// return "";},
对应的方法的注释可以去掉了:
this.setCookie("user",result.data.token,10); //修改成10好测试
然后查看或者添加如下:
created(){//根据实际情况下来测试// 从url中获取token参数// 从url中获取token参数// let token = this.getValueByUrlParams('token');// if(token == null || token == ""){// 从cookie中获取user的tokenlet token = this.getCookie("user");// }console.log(11)console.log(token)
然后查看控制台,若有数据,代表我们的确是设置了cookie,且可以被得到
等待一会,刷新,发现没有了,因为过期时间只有10秒
然后修改如下:
if(token != null || token != ""){// 将token发送到sso进行校验this.axios.get("http://localhost:80/user/checkToken",{params:{token:token}}).then( (result)=>{if(result.data.state == 4){this.isLogin = true;this.setCookie("user",token,600);this.user = jwtDecode(token);}}).catch( (error)=>{});}
注意:现在我们将程序里面的过期时间(redis,设置为600秒,虽然我们现在并没有使用),前端的cookie也设置为600秒
接下来我们登录后,刷新,会发现,还是在登录的,因为校验完成,使得会是登录的状态
那么这里可以去掉注释了:
// 刷新页面this.$router.go(0); //在history(历史)记录中前进或者后退(负数)val步,当val为0时刷新当前页面
//如果是小数一般取整(忽略小数,所以-1.1就是-1,回到上一个历史记录)
//注意:是我们访问的记录,而不是存在的记录,所以去删除记录基本是没有用的
实际上如果是多个域名来操作,通常需要给出登录的url地址
因为一般来说对应的登录页面地址通常也对应一个首页,否则不给出地址的话,我怎么知道你要跳到哪个首页呢
接下来我们来删除cookie也就是登出(添加方法,自然是methods里面的):
//删除cookiedelCookie(name){// 删除cookie只需要将值清空即可或者整个删除(过期时间)// 我们手动的设置-1是让指定名为name的cookie过期实现自动清除,相当于是0了//而不是在java中设置的一样是会话(java里面是int类型的赋值,这里可以是小数)//当然,他们的负数都是,即-2,-3都是一样的,与-1一样的作用//否则如果是很大的过期时间//那么只是将对应的cookie值删除(因为''),而这条cookie记录并没有删除,当然,只删除值也可以的//只是为了不占用空间,还是过期删除该记录算了,当然他还是会赋值的,即赋值操作为''//节省时间(虽然可能并不会)this.setCookie(name,'',-1);},
通常我们也需要删除redis的token,再AuthorityContoller类里添加如下:
@Autowiredprivate RedisTemplate<Object,Object> redisTemplate;@GetMapping("logout")public void logout(String token) {//对应的opsForValue().get若是没有得到数据,通常认为是返回null//且通常返回值类型需要Object(因为也通常没有指定泛型的类,返回值类型是泛型的字母)//而opsForValue().set基本没有返回值,即返回值类型是voidredisTemplate.delete(token); //若删除了返回true,否则返回false//这里就不得到返回值了(返回值类型是Boolean)//通常是类似于自带的客户端命令一样,他的是,没删除返回0,删除返回1//我们大多数认为自带的客户端与我们的客户端是不同的,虽然也的确如此(相当于多开了几个命令行窗口)//但是实际上也可以认为是我们的客户端来操作自带的客户端来执行命令的,虽然我们也称是不同的客户端//通常情况下,删除null(单独的,不是字符串)会报错//其他的代表什么就删除什么,比如空字符串"",就删除空的""的key,具体可以看里面的源码就知道了//当然也可以自己测试(需要注意序列化,可以看88章博客来了解)}
然后修改如下:
//登出logout(){ //删除cookiethis.delCookie("user"); // 更新登录状态this.isLogin = false; alert('谢谢使用,再见!');// 重定向到未登录的首页,也就是刷新当前界面,因为没有了cookie,那么自然就是未登录的(即为false)//所以如果加上了这个,那么上面的this.isLogin = false; 可以不写,否则需要写this.$router.go(0);// 去redis删除tokenthis.axios.get("http://localhost:80/user/logout",{params:{token:this.token}}).then( (result)=>{}).catch( (error)=>{//this.$message.error("登录失败!");});},
退出登录时,查看redis,若发现的确删除了,那么说明操作成功
微信扫码登录改造(具体为什么要这样实现,可以到87章博客里查看):
在edu-authority-boot认证微服务(80)下添加如下依赖:
<dependency><!--可以操作json格式的转换,在56章博客里有使用,如果这里使用了,那么最好加上,否则你也可以删除--><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version>
</dependency>
在tools包下创建HttpClientUtil类:
package com.lagou.tools;import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;import java.net.URI;
import java.util.Map;/****/
public class HttpClientUtil {public static String doGet(String url) {String s = doGet(url,null);return s;}/*** get请求 支持request请求方式,不支持restfull方式(了解即可)* @param url 请求地址* @param param 参数* @return 响应字符串*/public static String doGet(String url, Map<String,String> param){//创建httpclient对象CloseableHttpClient aDefault = HttpClients.createDefault();String s = null;CloseableHttpResponse response = null;try {//创建urlURIBuilder uriBuilder = new URIBuilder(url);if(param!=null){//在url后面拼接请求参数//map本身并不能操作迭代器,需要先变成set集合来操作,这里是操作key当setfor(String key:param.keySet()){//当前的set,假设对应key的value值,实现拼接uriBuilder.addParameter(key,param.get(key));}}//对应的请求总地址,上面只是操作url的值,如加上http://等等URI uri = uriBuilder.build();//创建http get请求,并将总地址给他进行请求,使得是get方式HttpGet httpGet = new HttpGet(uri);//执行请求,并得到响应的结果response = aDefault.execute(httpGet);//获取响应结果中的状态码int statusCode = response.getStatusLine().getStatusCode();System.out.println(response); System.out.println("响应的状态:" +response.getStatusLine()); System.out.println("响应的状态:" +statusCode); //200表示响应成功if(statusCode==200){//响应的内容字符串System.out.println(response.getEntity()); System.out.println("---");s = EntityUtils.toString(response.getEntity(),"UTF-8");System.out.println(s);System.out.println("---");System.out.println(1);}}catch (Exception e){e.printStackTrace();}finally {try {if (response != null) {response.close(); //关闭对应的对象,总得也让其他人也能执行请求吧}aDefault.close(); //关闭对应的对象}catch (Exception e){e.printStackTrace();}}return s;}
}
再entity包下创建Token类和UserId类:
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/****/
@Data
//这里不能与其对应的手写的构造冲突,否则报错,自己测试就知道了,即他不是覆盖(手写为主,或者优先),而是添加
@AllArgsConstructor
@NoArgsConstructor
public class Token {private String access_token;//接口调用凭证private String expires_in; //access_token接口调用凭证超时时间,单位(秒)private String refresh_token;//用户刷新access_tokenprivate String openid; //授权用户唯一标识private String scope; //用户授权的作用域,使用逗号(,)分隔private String unionid; //当且仅当该网站应用已获得该用户的userinfo授权时,才会出现该字段}
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/****/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserId {private String openid;//普通用户的标识,对当前开发者帐号唯一private String nickname;//普通用户昵称private String sex;//普通用户性别,1为男性,2为女性private String province;//普通用户个人资料填写的省份private String city;//普通用户个人资料填写的城市private String country;//国家,如中国为CNprivate String headimgurl;//用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空private String privilege;//用户特权信息,json数组,如微信沃卡用户为(chinaunicom)private String unionid;//用户统一标识,针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的}
回到UserService接口,添加如下方法:
Integer register(String phone, String password,String nickname,String headimg);
对应实现类添加的方法:
@Overridepublic Integer register(String phone, String password, String nickname, String headimg) {User user = new User();user.setPhone(phone);user.setPassword(password);user.setName(nickname);user.setPortrait(headimg);Date date = new Date();//可以不添加下面的两个设置,因为数据库的对应字段//添加了默认值为CURRENT_TIMESTAMP//可以代表添加一条记录时,若我们没有操作添加该值,那么该字段值自动设置为系统当前时间//当然也有sysdate()函数可以在添加时操作,也是当前日期和时间//他的结果与CURRENT_TIMESTAMP差不多一样//通常情况下格式化(SimpleDateFormat)的H代表24小时,而h代表12小时(所以可能他的一个数,通常可能代表两个,比如他的10点,在生活中可能是10点,也可能是22点,即需要看生活中的时间,所以并不是很友好,所以通常使用H来操作//比如19点,那么H代表19,而h代表7//当然了,格式化的对象(构造方法的字符串)里面基本可以随便写,只是对应的字母会替换而已//user.setCreateTime(date);//user.setUpdateTime(date);return userMapper.insert(user);}
然后再controller包下创建WxLoginController类:
package com.lagou.controller;import com.alibaba.fastjson.JSON;
import com.lagou.entity.EduConstant;
import com.lagou.entity.Token;
import com.lagou.entity.UserDTO;
import com.lagou.entity.UserId;
import com.lagou.service.UserService;import com.lagou.tools.HttpClientUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Reference;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/****/
@RestController
@RequestMapping("user")
@CrossOrigin
public class WxLoginController {@Autowired //远程调用private UserService userService;@GetMapping("wxlogin")public UserDTO wxlogin(HttpServletRequest request, HttpServletResponse response) throws IOException {//微信官方给我们的临时凭证String code = request.getParameter("code");System.out.println("【临时凭证】code=" + code);//通过code去微信官方申请一个正式的令牌(token)//发出一个get请求,httpclient-封装好的操作//微信官方文档上获得token令牌的请求String getTokenByCode_url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=wxd99431bbff8305a0&secret=60f78681d063590a469f1b297feff3c4&code=" + code + "&grant_type=authorization_code";String tokenString = HttpClientUtil.doGet(getTokenByCode_url);System.out.println(tokenString);//将json格式的该token字符串转换成实体对象,方便存和取Token token = JSON.parseObject(tokenString, Token.class);//通过token,去微信官方获取用户的信息String getUserByToken = "https://api.weixin.qq.com/sns/userinfo?access_token=" + token.getAccess_token() + "&openid=" + token.getOpenid();System.out.println("----------------------");String UserString = HttpClientUtil.doGet(getUserByToken);System.out.println("UserString = " + UserString);//将json格式的用户字符串转换成实体对象,方便存和取//记得User类的对应的包是对应的UserId user = JSON.parseObject(UserString, UserId.class);System.out.println("微信的用户昵称 = " + user.getNickname());System.out.println("微信的用户头像 = " + user.getHeadimgurl());System.out.println("微信的unionid参数值:" + user.getUnionid());//检测手机号是否注册UserDTO dto = userService.login(user.getUnionid(),user.getUnionid());if (dto.getState() == EduConstant.ERROR_NOT_FOUND_PHONE_CODE) {//未注册,注册并登录userService.register(user.getUnionid(), user.getUnionid(), user.getNickname(), user.getHeadimgurl());dto = userService.login(user.getUnionid(), user.getUnionid());}//直接给出参数来得到信息,因为我们只需要token就可以了,来保证我们微信登录的自动登录response.sendRedirect("http://localhost:8080/#/?token="+dto.getToken());return null;}}
然后在前端修改如下:
created(){//从url中获取token参数let token = this.getValueByUrlParams('token');//这就代表,若url有token,那么就是微信登录的//那么也就不需要cookie的token了(没有手动登录,那么也就没有cookie来设置
//而如果是我们手动输入登录,自然,url是没有token的,且也设置了cookie,所以就执行这里面的if(token == null || token == ""){ //实际上只需要判断""即可(方法里面返回的就是""这个)//但是null是为了以防万一// 从cookie中获取user的tokentoken = this.getCookie("user");}this.token = token; //这一步是为了让登出方法来操作的,使得删除redis对应的token//因为登出使用到了token:this.token,即需要有this.token的值//当然,校验时,最终也会设置cookie的
// 获取url中参数getValueByUrlParams(paramKey) {// http://localhost:8080/#/?token=1&id=2var url = location.href;var paraString = url.substring(url.indexOf("?") + 1, url.length).split("&");var paraObj = {}var i, jfor (i = 0; j = paraString[i]; i++) {paraObj[j.substring(0, j.indexOf("=")).toLowerCase()] = j.substring(j.indexOf("=") + 1, j.length);}var returnValue = paraObj[paramKey.toLowerCase()];if (typeof(returnValue) == "undefined") {return "";} else {return returnValue;}},
当然,我们还需要修改一个这个地方:
//登出logout(){ //删除cookiethis.delCookie("user"); // 更新登录状态this.isLogin = false; alert('谢谢使用,再见!');// 重定向到未登录的首页,也就是刷新当前界面,因为没有了cookie,那么自然就是未登录的(即为false)//所以如果加上了这个,那么上面的this.isLogin = false; 可以不写,否则需要写//this.$router.go(0);//如果是相同的地址(不包括参数的)//一般不会刷新,所以如果需要刷新,那么再次的执行this.$router.go(0);即可window.location.href = "http://localhost:8080/#/" //相同的地址(不看参数,只看地址)基本不会刷新,这里就不会刷新this.$router.go(0);//这样,我们刷新时,而不会一直因为多出的url参数,导致登出时,还会登录
至此,我们用微信扫描二维码后(登录那里有个微信图标,点击即可),会发现,数据变化了,变成你微信的头像和名称
至此,我们可以登出或者刷新来测试,会发现,并没有什么问题,即微信登录操作完成
短信验证码登录:
通过第三方短信平台向用户手机发出验证码,用户得到验证码进行登录
第三方平台,我们使用阿里云短信平台,这里使用我的手册"阿里云短信平台.docx"
该手册下载地址:
链接:https://pan.baidu.com/s/1sjaC_kkzhGl37H1-fxPYlA
提取码:alsk
这里大致说明一下流程:
首先我们访问https://free.aliyun.com/这个地址:
往下滑找到如下(即短信免费试用套餐包):
点击这个0元试用,一般会要你登录,你可以直接手动号登录即可
因为如果是第一次登录,他会自动注册并登录的,相当于直接登录了
然后再次的回到这里,点击0元试用,点击他弹出来的框框中的"前往个人认证"
到如下:
这里我们就不点击企业认证了,在公司里,一般是点击他的,我们直接点击个人实名认证即可
注意:不是点击相关文档,也不是点击人的图形,直接点击空白即可
我们点击第一个,使用支付宝认证(因为即时开通的,不需要等待,也要注意,点击空白处)
点击继续认证(记得打上勾勾),出现登录窗口
接下来就是使用支付宝登录了,具体登录方式看你怎么操作
然后授权即可,后面的就有点隐私了,但是很简单,一路点击即可,登录窗口会自动的关闭的
直到出现个人实名认证完成,那么就完成了
现在我们回到https://free.aliyun.com/这个地址,仍然点击前面的0元试用,会出现如下:
看到没,0元购买,还在等啥呢,直接点击购买,当然,但凡只要你加一个数量,那么就会要你的小钱钱了
即我们只能试用一个数量的套餐包(别想白嫖太多了,(●ˇ∀ˇ●)),很明显4.8元一个,但是如果是2个或者以上,那么就没有优惠喽
即2个是9.6元
点击立即购买后,到如下:
打上勾勾,点击去支付,出现如下:
点击支付,然后出现如下:
你可以点击上面的管理控制台,或者点击这个地址:https://dysms.console.aliyun.com/,到如下:
我们可以到套餐包余量那里可以看到有100条,就是我们之前免费试用的资源包规格:
接下来我们点击国内消息:
再然后点击添加签名,添加如下
注意:下面只是给出一个需要编写的地方,实际上需要一个具体的网站域名(已备案的网站,具体备案流程可以百度)
现在好像并不能搭建具体的个人测试了(需要具体网站),所以从这里开始,后面只需要了解即可
如果你以后有对应的网站了,那么就可以看对应的文档了,或者看一下后面的操作:
点击提交,出现如下:
然后点击添加模板,具体的内容,就不多说了(一般需要审核通过的签名)
接下来,我们认为对应的模板已经编写完成(文档里面的模板)
且都审核通过了(虽然上面没有,因为我并没有对应的备案网站),那么通常需要点击如下:
点击AccessKey管理,然后会出现如下:
点击开始使用子账户,当然,你可以多次的点击AccessKey管理来测试他们的区别
我们点击创建用户,内容如下:
点击确定,你可以使用自己的方式来验证,比如手机号验证
一般他会知道你的手机号的,因为支付宝登录,通常也能得到你的手机号的绑定的
如果没有绑定,一般会提示你去绑定或者跳转绑定)
最后得到如下:
后面还有两个是很重要的东西,这里我就不给出了,可以参照如下内容(不是我的):
/*
用户登录名称:xxx 这个就不给出了(因为后面代码并不需要他)
AccessKey ID:xxx
AccessKeySecret:xxx
通常情况下,最好记住他们,尤其是AccessKeySecret,一般刷新页面,可能就找不到了上面的后两个xxx属性值,由于博客可能不能审核通过,所以需要在如下地址里进行提取:
链接:https://pan.baidu.com/s/1yFjve2AWACR3mj6HVmJMoA
提取码:alsk*/
然后点击如下(又一个测试的):
打上勾勾,点击添加权限,然后到如下:
输入sms找到对应的权限,点击第一个名称,那么右边就会出现点击的名称,代表已选择
点击确定,然后点击完成,然后回到国内消息这里:
通过模板的CODE和签名名称可以得到一组信息(结合上面),这里给出测试用的信息:
/*
这四个数据在后面需要用到短信签名名称:大佬孙
短信模板CODE:SMS_177536068
下面的两个值,自己在前面的地址里自行提取
accessKeyId:xxx
AccessKeySecret:xxx
*/
但是我们也可以发现,他定义了签名和模板,他们两个用来影响短信的
而用户的对应的两个值是用来使得给发送的,因为是我们的账号来来发送
因为他申请发送的权限的,当然如果是主账号,自然有权限,而不用设置权限,主账号可以直接创建该两个值即可
既然发送者,和消息都指定好了,那么接收者是谁呢,这就需要参数了,通常来说我们只需要指定电话号码即可
在后面的测试中可以进行操作
注意:阿里为了防止恶意高频发送验证码,加入了流量限制,一般当返回下面的结果就是这个原因(一般是如下,也有可能会发生改变):
{"RequestId":"D0558EEE-8331-47F4-99B5-1B4A4148373A","Message":"触发天级流控Permits:10","Code":"isv.BUSINESS_LIMIT_CONTROL"
}
注意:实际上短信可能是有限制的,比如
短信验证码 :使用同一个签名,对同一个手机号码发送短信验证码,支持1条/分钟,5条/小时 ,累计10条/天
短信通知: 使用同一个签名和同一个短信模板ID,对同一个手机号码发送短信通知,支持50条/日
所以我们个人开发测试的时候,对一个手机发送验证码太多次,就发不过去了,换个手机号发送就可以了
或者可以进入短信的管理后台调整流量频次
通过上面的初步介绍,我们来编写代码,实际上你可以通过如下来测试或者查看API
发送验证码:
这里给出具体API(一般是原版的SDK,SDK:软件开发工具包)
首先我们先在项目里引入依赖,自然仍然是edu-authority-boot认证微服务(80)里添加如下依赖:
<dependency><!--有DefaultProfile类可以操作--><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>4.5.3</version>
</dependency>
再在yml文件加上如下:
ali:sms:signName: 大佬孙templateCode: SMS_177536068assessKeyId: LTAI4FwKDkeZ6StZvRxg5RDfassessKeySecret: 09IMDRUia2uIC7HMXpSmM5CiXuUgvf
然后在AuthorityContoller类里添加如下:
package com.lagou.controller;import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.lagou.entity.UserDTO;
import com.lagou.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/****/
@RestController
@RequestMapping("user")
@CrossOrigin
public class AuthorityContoller {@Autowiredprivate UserService userService;@GetMapping("login")public UserDTO login(String phone, String password){UserDTO dto = userService.login(phone, password);return dto;}@GetMapping("checkToken")public UserDTO checkToken(String token){System.out.println("待校验的token = " + token);UserDTO dto = userService.checkToken(token);return dto;}@Autowiredprivate RedisTemplate<Object,Object> redisTemplate;@GetMapping("logout")public void logout(String token) {System.out.println(1);System.out.println(token);redisTemplate.delete(token);}//下面就是之前说的重要的四个数据//签名名称@Value("${ali.sms.signName}")private String signName;//模板的code@Value("${ali.sms.templateCode}")private String templateCode;//下面是对应的用户创建的两个重要数据@Value("${ali.sms.assessKeyId}")private String assessKeyId;@Value("${ali.sms.assessKeySecret}")private String assessKeySecret;@GetMapping("sendSms")public boolean sendSms(String phoneNumber) {//指定发送者DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", assessKeyId, assessKeySecret);//创建发送对象,拥有其信息(对象保存了他的发送者信息,以及操作该信息的方法)IAcsClient client = new DefaultAcsClient(profile);//创建消息设置对象CommonRequest request = new CommonRequest();//通常来说aliyun-java-sdk-core的老版本的依赖没有Sys的添加//你可以删除Sys可以发现是过时的方法,比如4.1.0版本,就没有Sys的添加//这里我们使用的是4.5.3,则有Sys的添加的方法,自然一般新的方法效率是高点的,虽然可能占用的空间大//但是自然是新的,那么自然考虑的很全面,可能效率提高,占用内存也降低了,全方位的提升//比如使用更好的算法,代码少了(内存占用降低),执行速度变快了,所以并不是速度变快//内存的占用也就一点会变大request.setSysMethod(MethodType.POST);request.setSysDomain("dysmsapi.aliyuncs.com");request.setSysVersion("2017-05-25");request.setSysAction("SendSms");request.putQueryParameter("RegionId", "cn-hangzhou");request.putQueryParameter("PhoneNumbers", phoneNumber); //手机号,指定接收者,如果是空的,则会报错(后面的CommonResponse response = client.getCommonResponse(request);)//下面指定消息内容request.putQueryParameter("SignName", signName); //签名名称request.putQueryParameter("TemplateCode", templateCode); //模板code//操作验证码String vcode = "";for(int i = 0; i<6; i++){vcode = vcode + (int)(Math.random()*9+1); //生成1到9之间的整数(包括1和9)}/*定义参数的值,通常来说操作验证码的操作基本必须要指定code参数(对于这里来说,实际上是必须要有参数,当然不是code也行,即只需要模板是有该参数的设置存在即可)在创建模板(签名模板,而不是信息模板,这里需要注意,即签名名称那里,而不是模板名称那里)时也是这样,验证码模式的好像必须要指定一个属性(不是code也行)但验证码模式只能是一个变量(但在程序中可以多指定,只是只会操作对应的属性)且长度(即字数限制)以及数字类型有限(基本不能是汉字,以及基本只能是4-6个数,包括4和6,通常外面操作6个验证码,因为更加的安全,比如不易被试出来,这些是针对验证码模式来说的,如果是其他默认则不同,比如短信,具体看申请时的介绍,比如可以操作1到35个字符,包括汉字,包括1和35等等)其他模式(如短信模式)可以基本可以自定义(在后面的短信通知的操作中会出现一个对应的问题,操作中文了,若没有短信的操作,通常会认为不符合匹配验证码的操作,如果有短信,如果匹配了参数,自然会使用,否则默认使用验证码的模板,看打印信息就知道了,与下面的不写或者写错是不同的错误,但都是他们提示错误信息)具体的验证码模式和短信模式(自定义的)的细节如下:如果参数是单个或者多个,首先看值,先看看是否有匹配的模板(优先短信),然后看验证码如果不符合对应的短信或者验证码的格式(是否是汉字以及是否超过字数限制),通常会报错,比如验证码的会出现params must be [a-zA-Z0-9] for verification sms的提示错误然后再看属性但无论是短信还是验证码,只要参数里有对应的模板属性,那么就可以使用(优先短信)即无论是否有多余的参数而当有匹配多个模板时,通常根据创建时间来决定,先创建的优先使用如果都没有匹配,那么会出现请 "检查模板内容与模板参数是否匹配" ,也就是下面的不写或者写错的提示错误*///需要指定参数,否则一般不会创建模板成功,会有提示错误信息,当然可以指定多个模板,只需要与对应模板的参数匹配了,那么使用的就是对应的模板,没有匹配的,则自然会报错//不写或者写错都是一样的错误,一般是检查参数不匹配的提示(这里是的,因为模板的原因)//提示错误信息:也就是打印信息里面显示错误的信息,而不是程序,通常来说都是程序,这里特别说明了request.putQueryParameter("TemplateParam", "{\"code\":\"" + vcode + "\"}");try {//让发送对象操作消息发送//因为依赖会使得我们的用户(我们创建的用户,不需要审核的,即他基本正确,而签名名称和模板code通常需要弄出来,即审核通过,可能有赠送的模板,但可能操作不了,当然可能认为是所有签名都操作)来发送//所以我们也通常需要导入依赖(通常有地址,从而导致用户发送)//所以自然不是我们自己的程序来发送的CommonResponse response = client.getCommonResponse(request);//返回结果//通常对应的平台将调用结果返回时,也顺便去操作了短信的发送,可以说他们是一起操作的//正确的自然对应正确的发送,错误的自然不会发送//当然,可能因为错误,导致没有发送短信System.out.println(response.getData());//一般这里可能会出现对应的问题,比如json不合法(或者不匹配),或者业务停机等等//一般来说,如果是业务停机,代表我们并没有操作失败,只是对应已经停机而已//大概是没钱了,或者没有开启短信使用(通常也就是没钱或者没有条数)//如果对应的四个参数有错误了,那么也会出现对应的错误提示//比如签名没有审核通过(一般提示找不到对应签名),当然可能也有找不到模板的错误//那么自然会提示错误的,当然只是错误信息而已//签名的两个是错误信息//而后面两个则是程序报错(一般不同的错误)//即CommonResponse response = client.getCommonResponse(request);这里程序报错//很明显程序错误优先,因为在前面//通常没有给权限的,会提示没有权限//上面的错误并不需要了解,因为你总不能将所有的错误信息背下吧//这是不可能的,虽然他有限,但是看到错误信息就能知道为什么了//否则通常没有什么错误的出现,那么后面自然返回true(出现了Message是ok的值),否则都会返回falseString jsonStr = response.getData();JSONObject jsonObject = JSONObject.parseObject(jsonStr);if("OK".equals(jsonObject.get("Message"))){return true;}System.out.println(1);} catch (ServerException e) {e.printStackTrace();} catch (ClientException e) {e.printStackTrace();}return false;}}
重启项目,访问localhost:80/user/sendSms?phoneNumber=1234567890,后面的1234567890是你电话号码(这里是测试用)
由于上面是测试用的,所以如果出现的打印信息是业务停机,那么代表代表没有问题
当然,如果你有对应的好的四个信息,可以复制粘贴上,且访问地址里面添加你的电话号码,那么你的手机上就会收到验证码了
至此,验证码的发送操作完毕
比如说如下的截图(新的程序,这里了解结构):
其中"拉勾教育通知"就是签名的名称,而"】"通常是自动加上的(在添加签名时,自然就知道了,他有提示的),用来分开
后面紧跟(基本没有加上空格)的就是模板内容了
一般来说签名都是公司的名称的,注意即可
验证码登录流程:
通常来说,对应的验证码会存放到一个库里面,比如mysql和redis
这样用户在复制粘贴短信里面的验证码时,可以进行比对保存
但是这里有个隐患,在发送验证码之前,保存的验证码也需要设置过期时间
在redis里面的确可以设置,但在mysql里面可能会一直存在,那么针对于mysql,也就可以使用上次的验证码
所以使用mysql是有隐患的,使用redis好点,但我们也知道,无论你使用哪个库,都需要保存,然后查询
那么有没有可以不用保存并查询,也可以操作的呢,看如下流程:
我们操作如下流程,当然,大多数的情况下,具体流程看对应项目的需求,这里就按照如下:
后端业务:
1:通过短信平台向手机发验证码,验证通过后将手机号和验证码返回给前端进行判断
2:前端比对通过,将手机号进行登录
user表中如果不存在该手机号
则自动注册(插入手机号,并且密码也设置为手机号,头像默认和昵称都默认为:手机新用户)然后登录
当然,密码也可以不用设置,因为有验证码了,这里之所以设置,是为了操作普通登录
由于null在mysql中需要特殊的操作而添加的密码
eq就使得直接的=null,这是不会认为的,通常代表不会返回任何结果,即返回false
所以如果是or,那么若有一个true还是会得到结果的
user表中如果存在该手机号,则登录成功(这里不需要比对密码,因为操作的是验证码)
3:生成token,并返回
也就是说我们直接给前端进行判断了,即更快速的使用掉验证码
后面的登录流程就是微信登录(自动注册)和普通登录(比对登录)的类似的结合了
我们到项目的UserService接口下添加如下方法:
UserDTO loginPhoneSms(String phoneNumber);
对应的实现类如下:
@Overridepublic UserDTO loginPhoneSms(String phoneNumber) {QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("phone",phoneNumber);User user = userMapper.selectOne(queryWrapper);if(user == null){ //手机号不存在// 先注册//后面两个你可以自己随便写,现在并不需要具体的值register(phoneNumber,phoneNumber,"手机新用户","xxx"); return loginPhoneSms(phoneNumber); //再执行自己一次,即使得进行了登录//加上return代表不会操作第一次的后面的代码(不操作第一次的结果,对应的token基本是null)//且以第二次的结果返回过去}System.out.println("user = " + user);// 创建tokenString token = JwtUtil.createToken(user); //传递null的user,会使得调用不了对应的方法,即空指针异常,前面加上了return防止了这种情况//里面进行了异常处理,会导致返回null// 封装DTOUserDTO dto = new UserDTO();dto.setState(EduConstant.LOGIN_SUCCESS_CODE);dto.setMessage(EduConstant.LOGIN_SUCCESS);dto.setToken(token);return dto;//这里就不在redis里保存token了}
然后在对应的AuthorityContoller类里添加如下:
@GetMapping("loginPhoneSms")public UserDTO loginPhoneSms(String phoneNumber) {return userService.loginPhoneSms(phoneNumber);}
重启项目,访问localhost:80/user/loginPhoneSms?phoneNumber=666666
查看数据库和访问的返回结果,若有数据并且返回登录成功,那么代表操作成功
我们回到前端界面(Header.vue组件),找到如下:
<el-form><el-form-item><el-input v-model="phoneNumber" placeholder="请输入手机号"></el-input></el-form-item><el-form-item ><el-input v-model="smsCode" placeholder="请输入验证码"></el-input> <div class="get-verify-code" @click="sendSms">{{ smsCodeTimeSecond>0 ? (smsCodeTimeSecond + 's 后重试') : '获取验证码' }}<!--当然了,就与js一样,js可以影响元素的底层改变,自然的,vue也是通过js改变的
一般不会出现{{}}的符号出现,因为在一开始渲染时就改变了,查看源代码是整个的返回数据,而不是浏览器操作的数据
所以可以看到
--></div></el-form-item></el-form>
查看如下:
//data里面的:smsCodeTimeSecond:0, // 验证码倒计时的秒数phoneNumber:null, // 发送验证码的手机号,当你没有输入时,自然就是null//而输入后,那么就是有值了,如果都删除,那么到时候就变成了""(空字符串)//而不是null,这是输入框的问题//注意:需要与原来的表单的属性分开//因为绑定的,所以如果一样,那么这里填写后//另外的一个输入的地方也填写了,自己测试就知道了,所以这里再来一个smsCode:null, // 输入的验证码resultPhoneNumber:null, // 返回的手机号//那么为什么有了输入的phoneNumber,还要有一个这个呢,难道我们不能使用phoneNumber吗//答:是的,我们的确是不能使用phoneNumber,因为可能在验证码到达之前就被你修改了//但是这里再后面验证时,还是操作了phoneNumber,因为设置了比较,我们也通常这样//即为了页面的完整性(总不能发现手机号不是你的还是提交了吧)//所以我们也通常比较,那么在比较的情况下//自然使用phoneNumber或者resultPhoneNumber都可以resultSmsCode:null,// 返回的验证码//methods里面的:// 给手机发验证码sendSms(){return this.axios.get("http://localhost:80/user/sendSms" , {params:{phoneNumber:this.phoneNumber}}).then( (result)=>{console.log( result );this.resultPhoneNumber = result.data.phoneNumber;this.resultSmsCode = result.data.smsCode;// 验证码发送成功if(result.data.Code == "OK"){this.setTimerCode();}} ).catch( (error)=>{this.$message.error("发送验证码失败!");});},// 验证码倒计时setTimerCode(){this.smsCodeTimeSecond = 60;let codeTimer = setInterval(() => {this.smsCodeTimeSecond-- ;if(this.smsCodeTimeSecond <= 0){ //防止多次执行时,导致小于0//因为如果是等于0的话,小于0那么会一直执行)// 停止计时clearInterval(codeTimer);}}, 1000);},// 手机验证码登录loginPhone(){//这个判断就说明了,界面的手机号不能乱动,即保证了界面的完整性if(this.phoneNumber == this.resultPhoneNumber && this.smsCode == this.resultSmsCode ){return this.axios.get("http://localhost:80/user/loginPhoneSms" , {params:{//这里因为判断,那么可以是this.phoneNumber了,否则一般要是this.resultPhoneNumber//因为如果没有判断时,是this.phoneNumber的话//那么去登录的就是你输入的值(那么导致可能不是自己的手机号了,比如手滑就改变了)//所以加上判断也是为了保证完整性的,即也是为了防止手滑,或者说登录的手机号不一致的原因//所以整体来说,也是因为判断,导致完整//现在的验证,通常是不能修改手机号的,也进一步防止修改了phoneNumber:this.phoneNumber}}).then( (result)=>{console.log( result );if(result.data.state == 3){// 1.关闭登录框this.dialogFormVisible = false ; // 2.更新登录状态this.isLogin = true;// 3.将返回的token保存在cookie中this.setCookie("user",result.data.token,600);// 4.解析token中的数据(昵称和头像)const code = jwtDecode(result.data.token);this.user = code;console.log( this.user );this.smsCodeTimeSecond = 0; //结束循环(减少),因为没有必要了(因为已经登录了)}} ).catch( (error)=>{this.$message.error("发送验证码失败!");}); }else{this.$message.error("输入的手机号与返回的手机号不一致或者验证码没填写或者验证码不一致(发送验证码时,不用乱改手机号哦)!");}}
在这之前,我们修改一下AuthorityContoller类的sendSms方法,部分代码如下:
//记得返回类型修改成Object
//部分:JSONObject jsonObject = JSONObject.parseObject(jsonStr);if("OK".equals(jsonObject.get("Message"))){//返回手机号和验证码jsonObject.put("phoneNumber",phoneNumber);jsonObject.put("smsCode",vcode);return jsonObject;}System.out.println(1);} catch (ServerException e) {e.printStackTrace();} catch (ClientException e) {e.printStackTrace();}return null; //一般情况下,返回null(不是字符串的null)或者""在前端基本都会认为是null和""
//即空的数据
//在某些中间的操作,可能null在给前端时,会变成""或者""给前端时,会变成null
//或者说前端读取时会认为是""或者null
//而前端给后端时,如果没有参数,那么则是null,有参数但没有给值
//那么就是""
//大多数的情况下,无论是什么的数据交换,基本上是null对应null,或者""对应""
//而不会出现null和""互相比较的原因,若有,大多数是因为中间的操作//即前端在操作后端的null时,通常默认为null,有时会认为是"",当然可能是因为后端在返回时,会导致null变成""
//这里了解即可,因为只是数据的读取方式而已,也就是说,要么是前端自己数据的变化,要么是后端变化后,给前端
//当然通常是后端变化后给前端,所以null变成""
//大多数是后端的原因
//一般是依赖的原因,比如mvc的依赖中我们进行手动变化(自然也包括自定义方法或者自带的方法)
//当然,可能也是依赖自身的原因,或者前端自身的其他原因等等
//比如前端的方法(比如框架或者自定义的,或者自带的)或者我们的手动变化等等
//当然,可能也会有使得是null,则删除属性的操作,比如这里的token变化为属性就会,实际上是因为后端进行了删除//在前端,通常"",null都代表不会出现数据,即对于数据的显示,基本都认为是""
//而undefined就是undefined,这是特殊的//总结:在没有其他的因素或者特殊情况下,数据库的null对应后端的null对应前端的null(通常也包括undefined)
//后端的""对应前端""对应数据库的空数据(不是null,相当于都删除)
但是因为我们可能是测试的,也就是说,若要认为成功,可以这样修改:
jsonObject.put("Message","OK"); //默认发送成功if("OK".equals(jsonObject.get("Message"))){//返回手机号和验证码jsonObject.put("phoneNumber",phoneNumber);jsonObject.put("smsCode",vcode);return jsonObject;}
使得默认已经发送短信了,当然,如果你有对应的正确的四个信息,那么可以不用默认已经发送短信,即不用修改
前端进行修改:
// 给手机发验证码sendSms(){//防止前面的多次的执行(点击,那么他就可以设置为等于0了),使得有发送访问,且重置了秒数if(this.smsCodeTimeSecond <= 0){ //这里也最好设置<=0//防止特殊情况导致的多次执行(比如修改了值),虽然基本不能修改js的值return this.axios.get("http://localhost:80/user/sendSms" , {params:{phoneNumber:this.phoneNumber}}).then( (result)=>{console.log(99)console.log( result );this.resultPhoneNumber = result.data.phoneNumber;this.resultSmsCode = result.data.smsCode;// 验证码发送成功,一般情况下,我们需要验证Code为OK,但是如果是测试的,那么记得将Code修改成Message//if(result.data.Code == "OK"){// this.setTimerCode();// }if(result.data.Message == "OK"){this.setTimerCode();}} ).catch( (error)=>{this.$message.error("发送验证码失败!");});}},
我们可以发现,验证码的样式不好,将修改如下:
.get-verify-code{position: absolute;top:10px;right: 10px;z-index: 2;width: 80px;font-size:16px;line-height:20px;color: #00B38A;text-align: right;cursor: pointer;}
至此我们进行测试(测试的可以看打印信息,有短信的可以看手机验证码或者看打印信息),若登录成功,那么代表操作成功
测试的:对应的停机或者没有自己的短信操作的四个值,或者说使用我给出的四个值
短信的:自己操作自己给的四个值
edu-user-boot用户微服务(8002):
我们先看前端的Header.vue组件的如下:
<ul style=""><li @click="goToSetting">账号设置</li><li @click="logout">退出</li></ul>
查看对应的方法:
methods: {goToSetting() {this.$router.push("/Setting"); // 跳转个人设置页面},
查看路由index.js文件,找到如下:
import Setting from '../components/Setting.vue'
{path: '/Setting',name: 'Setting',component: Setting,meta: {title: '个人设置'}},
即到达了Setting.vue组件
我们现在登录后,点击账号设置即可
现在我们创建子项目edu-user-boot用户微服务(8002):
最终成果:
对应的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.lagou</groupId><artifactId>edu-lagou</artifactId><version>1.0-SNAPSHOT</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.lagou</groupId><artifactId>edu-user-boot</artifactId><version>0.0.1-SNAPSHOT</version><name>edu-user-boot</name><description>edu-user-boot</description><properties><java.version>11</java.version></properties><dependencies><!-- web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- eureka客户端 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!-- mybatis plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version></dependency><!-- mysql --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--pojo持久化使用--><dependency><groupId>javax.persistence</groupId><artifactId>javax.persistence-api</artifactId><version>2.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version></dependency><dependency><!--
java连接fastDFS的客户端工具
有读取文件的操作,如ClientGlobal.initByProperties("config/fastdfs-client.properties");
基本是必须的
--><groupId>net.oschina.zcx7878</groupId><artifactId>fastdfs-client-java</artifactId><version>1.27.0.0</version></dependency><dependency><!--
图片上传到FastDFS可以用到的IO工具
有对应的文件操作,比如
使用IOUtils完成文件的复制,这个类直接封装了对输入流和输出流的读取写入操作
注意:输出流会对路径(目录)进行判断,即必须是存在的目录,否则不会执行读取写入操作,即报错
但文件不会,会自动创建文件
IOUtils.copy(InputStream input, OutputStream output)
--><groupId>org.apache.commons</groupId><artifactId>commons-io</artifactId><version>1.3.2</version></dependency><!--上传文件,文件包括很多,比如图片,视频等等,他们都是文件
所以FastDFS的文件操作和68章博客的文件操作都可以操作视频,其实就相当于我们使用java程序,操作IO流一样而已
但是,68章博客那里通常操作视频时,需要设置大小的上限(需要大点,否则可能会报错,因为默认通常是1MB)
FastDFS一般不需要设置大小的上限,因为足够大了,通常默认是64MB--><dependency><!-- 图片保存到web服务器可以用到的IO工具比如创建磁盘文件工厂对象,临时的操作,必要的DiskFileItemFactory factory = new DiskFileItemFactory();创建文件上传核心类,有操作文件的方法ServletFileUpload upload = new ServletFileUpload(factory);DiskFileItemFactory类和ServletFileUpload类需要这个依赖
--><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.3.1</version></dependency><dependency><!--需要用到他的User类--><groupId>com.lagou</groupId><artifactId>edu-authority-boot</artifactId><version>0.0.1-SNAPSHOT</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
首先在资源文件夹下创建config文件,然后在该文件里面创建fastdfs-client.properties文件,内容如下:
##fastdfs-client.properties
fastdfs.connect_timeout_in_seconds = 5
fastdfs.network_timeout_in_seconds = 30
fastdfs.charset = UTF-8
fastdfs.http_anti_steal_token = false
fastdfs.http_secret_key = FastDFS1234567890
fastdfs.http_tracker_http_port = 80
fastdfs.tracker_servers = 192.168.164.128:22122
在启动类所在的包下创建实体类entity.FileSystem:
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;/****/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class FileSystem {private String fileId;private String filePath;private String fileName;
}
创建mapper.UserMapper类:
package com.lagou.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.User;/****/
public interface UserMapper extends BaseMapper<User> {}
创建service.UserService接口及其实现类:
package com.lagou.service;/****/
public interface UserService {public void updateUser(Integer userid,String newName,String imgfileId);//接口默认将方法的权限设置为public//无论是否写都是这样,当然私有private和protected除外,私有需要方法体,而protected基本不能操作void updatePassword(Integer userid,String newPwd);
}
对应的实现类:
package com.lagou.service.impl;import com.lagou.entity.User;
import com.lagou.mapper.UserMapper;
import com.lagou.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;/****/
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic void updateUser(Integer userid, String newName, String imgfileId) {User user = new User();user.setId(userid);user.setName(newName);user.setPortrait(imgfileId);userMapper.updateById(user);}@Overridepublic void updatePassword(Integer userid, String newPwd) {User user = new User();user.setId(userid);user.setPassword(newPwd);userMapper.updateById(user);}
}
创建controller.UserController类:
package com.lagou.controller;import com.lagou.entity.FileSystem;
import com.lagou.service.UserService;
import org.csource.common.IniFileReader;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Properties;/****/
@RestController
@RequestMapping("userSetting")
@CrossOrigin //跨域
public class UserController {@Autowiredprivate UserService userService;static String fastdfsip = null;static{Properties props = new Properties();InputStream in = IniFileReader.loadFromOsFileSystemOrClasspathAsStream("config/fastdfs-client.properties");if (in != null) {try {props.load(in);} catch (IOException e) {e.printStackTrace();}}fastdfsip = props.getProperty("fastdfs.tracker_servers").split(":")[0];}@PostMapping("upload")@ResponseBodypublic FileSystem upload(@RequestParam("file") MultipartFile file) throws IOException {System.out.println("接收到:" +file);FileSystem fs = new FileSystem();//获得文件的原始名称String oldFileName = file.getOriginalFilename();//获得后缀名String hou = oldFileName.substring(oldFileName.lastIndexOf(".")+1);try {//加载配置文件ClientGlobal.initByProperties("config/fastdfs-client.properties");System.out.println("ip:" + fastdfsip );//创建tracker客户端TrackerClient tc = new TrackerClient();//根据tracker客户端创建连接TrackerServer ts = tc.getConnection();StorageServer ss = null;//定义storage客户端StorageClient1 client = new StorageClient1(ts, ss);//文件元信息NameValuePair[] list = new NameValuePair[1];list[0] = new NameValuePair("fileName", oldFileName);//上传,返回fileIdString fileId = client.upload_file1(file.getBytes(), hou, list);System.out.println(fileId);ts.close();//封装数据对象,将路径保存到数据库(本次不写)fs.setFileId(fileId);fs.setFilePath(fileId);fs.setFileName(oldFileName);} catch (Exception e) {e.printStackTrace();}return fs;}//修改昵称@GetMapping("updateUser")public void updateUser(Integer userid,String newName,String fileId) throws UnsupportedEncodingException {System.out.println("newName = " + newName);fileId = "http://"+fastdfsip+"/"+fileId;System.out.println("imgfileId = " + fileId);userService.updateUser(userid,newName, fileId);}//修改密码@GetMapping("updatePassword")public void updatePassword(Integer userid,String newPwd){System.out.println("userid = " + userid);System.out.println("newPwd = " + newPwd);userService.updatePassword(userid,newPwd);}
}
启动类:
package com.lagou;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication
@EnableEurekaClient // 注册到中心的客户端
@MapperScan("com.lagou.mapper") // 扫描mapper包
public class EduUserBootApplication {public static void main(String[] args) {SpringApplication.run(EduUserBootApplication.class, args);}}
将配置文件的后缀修改成yml,文件内容如下:
server:port: 8002
spring:application:name: edu-user-bootdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.164.128:3306/edu_user?useUnicode=true&characterEncoding=utf8&serverTimezone=UTCusername: rootpassword: QiDian@666redis:host: 192.168.164.128port: 6379
eureka:client:service-url:defaultZone: http://localhost:7001/eurekaregister-with-eureka: truefetch-registry: trueinstance:prefer-ip-address: trueinstance-id: ${spring.cloud.client.ip-address}:${server.port}
至此这里的后端项目搭建完毕
接下来我们到前端的Setting.vue组件(不是副本,副本是旧的,所以我们不操作他,而Setting.vue是我操作好的):
查看(查看或者说检查)如下:
data() {return {dialogImageUrl: '',//预览urldialogVisibleImg:false,httpRequestImg:false,//展示单个图片dialogFormVisible: false, // 修改密码的模态框不显示dialogVisible:false,// 隐藏的图片文件框user: null,// 当前登录的用户对象信息fileId:"",// 上传图片后,返回的fastdfs的图片地址oldPwd:null,// 旧密码newPwd1:null,// 第一次新密码newPwd2:null,// 第二次新密码token:null,isLogin: false, // 登录状态,true:已登录,false:未登录};},
<el-uploadclass="avatar-uploader"action="1"list-type="picture-card":on-preview="handlePictureCardPreview" :on-remove="handleRemove":http-request="myUpload":class="{'demo-httpRequestImg':httpRequestImg}"><i class="el-icon-plus"></i></el-upload>
找到myUpload方法:
// 上传头像myUpload(content) {// 必须将表单中的数据进行封装才能发送,否则传不过去let form = new FormData();form.append("file",content.file);this.axios.post("http://localhost:8002/userSetting/upload" , form).then( (result)=>{this.fileId = result.data.fileId;console.log("头像URL:" + this.fileId);// 正常情况下会出现第二个图片框,true为只显示一个this.httpRequestImg = true;} ).catch( (error)=>{this.$message.error("上传头像失败!");});},
那么接下来,我们需要启动fastdfs了,如果你没有操作过如果保存文件到fastdfs,可以到82章博客里去查看
在这之前,我们首先说明一下打包的一些问题,在之前,我们都是操作maven项目的打包,通常情况下
打包时子项目会得到父项目的依赖,但是在Boot项目中,并不需要必须规定与maven中的modules标签对应
所以可以直接的得到父依赖,需要注意这个<relativePath/ >
他代表从仓库(通常是本地仓库或者远程仓库,基本没有只从远程仓库获取,如果是只从远程仓库获取的话,那么安装好可能也是没有用的,但基本是不会的)获取,所以通常需要安装
虽然代表从仓库获取,但是如果不是打包,若当前项目存在(没有安装的),他还是会使用的,但是打包就不会了,这里要注意
到那时自然需要删除他或者有安装好的(安装好通常也没有作用,因为是远程仓库,而不是本地)
打包的过程中,他的依赖通常是以仓库获取的(无论是否是父依赖还是当前依赖,都是从仓库获取,所以打包之前,通常需要安装)
但是使用时,还是会先到当前项目中来使用(即前面说明的联系),即项目之间互相可以使用,idea或者maven的作用
当然,打包时,最好都进行安装,否则打包通常会报错(找不到对应的jar包的报错,即前面说明的单独出来)
至此介绍完毕,现在我们直接启动该项目,会发现报错,看如下解释:
最后注意:如果是相同的文件,那么以当前的文件为主,所以我们需要加上如下:
ali:sms:signName: 大佬孙templateCode: SMS_177536068#下面的两个值,自己在前面的地址里自行提取assessKeyId: xxxassessKeySecret: xxx
这样就防止了没有对应依赖的需要的值
现在再次的启动,那么就不会报错了,但是在这里也要注意,因为我们引入了对应的依赖,那么启动类是有多个的
所以测试类需要指定启动类,否则测试类的方法运行会报错,因为多个情况下,就需要指定,就一个启动类的话,就不需要了
那么现在我们去点击上传头像(在页面你会知道在哪里的),查看fastdfs服务器是否有对应的文件,若有,则代表操作成功
现在记得修改方法如下:
//从cookie中获取tokengetCookie(key){var name = key + "=";if(document.cookie.indexOf(';') > 0){var ca = document.cookie.split(';');for(var i=0; i<ca.length; i++) {var c = ca[i].trim();if (c.indexOf(name)==0) { return c.substring(name.length,c.length); }}}else{var ca = document.cookie//console.log(ca)if (ca.indexOf(name)==0) { console.log(9)console.log(ca.substring(name.length,ca.length))return ca.substring(name.length,ca.length); }}// return "";},
否则,基本上是得不到token的
查看如下:
//更新个人信息updateInfo(){return this.axios.get("http://localhost:8002/userSetting/updateUser" , {params:{userid:this.user.userid,newName: document.getElementById("newNickName").value,fileId:this.fileId}}).then( (result)=>{// 友好提示this.$message.success("上传头像成功!");// 退出this.logout();// 回到首页this.$router.push("/");} ).catch( (error)=>{this.$message.error("上传头像失败!");});},
点击更新信息,然后再次的登录,若头像和名称都改变了,那么就操作成功
接下来找到如下:
//修改密码updatePwd(){if(this.oldPwd == this.user.password){if(this.newPwd1 == this.newPwd2){return this.axios.get("http://localhost:8002/userSetting/updatePassword" , {params:{userid:this.user.userid,newPwd:this.newPwd2}}).then( (result)=>{// 友好提示this.$message.success("密码修改成功,请重新登录");// 隐藏修改密码的对话框this.dialogFormVisible = false;// 退出this.logout();// 回到首页this.$router.push("/");} ).catch( (error)=>{this.$message.error("修改密码失败!");});}else{this.$message.error("两次密码不一致");return;}}else{this.$message.error("原密码输入错误");return;}},
对应的前端如下:
<div class="dialog-footer"><el-button class="confirm-button" type="primary" @click="updatePwd">确 定</el-button><el-button class="cancel-button" @click="dialogFormVisible = false">取 消</el-button></div><!--@click="dialogFormVisible = false",代表直接操作里面的表达式(dialogFormVisible是data里面的内容)
相当于一个方法的返回值就是dialogFormVisible = false
而在前端,返回值dialogFormVisible = false的值就是返回值dialogFormVisible的结果,也就是false
-->
现在我们继续测试修改密码,若修改后,登录成功,那么操作完毕,但是这里有个问题
如果我们打开框框,然后直接取消,再次的打开,会发现,对应的值还存在,所以我们需要改动一下
对应的前端:
<div class="dialog-footer"><el-button class="confirm-button" type="primary" @click="updatePwd">确 定</el-button><el-button class="cancel-button" @click="dialogFormVisiblee">取 消</el-button></div><!--注意:记得修改如下:--><el-dialog title="修改密码" :visible.sync="dialogFormVisible" :before-close="dialogFormVisiblee"><!--用来解决点击x的问题,即点击"x"也要执行该方法
:before-close这个数据就是点击"x"会跳转的方法的属性,他不能得到一个返回值(false和true的返回值)
因为每次的打开框框,他都是执行,如果是false,那么打不开,如果是true,那么关不掉
即他不只是独属于"x"的,基本也属于打开和关闭框框的所有相关操作
通常情况下,:visible.sync="dialogFormVisible"代表操作框框,默认使得点击他时
对应的dialogFormVisible值变成true,只后,才会操作其他的属性,比如:before-close
:visible.sync不能操作表达式,即只能是一个属性,因为对于某些情况vue是不准用表达式的
比如这里和双向绑定的v-model,随便变量可以赋值表达式,但是vue在识别时
通常不识别(比如识别到=,>等等,那么就会报错)
-->
对应的方法如下:
dialogFormVisiblee(){this.dialogFormVisible=false;this.oldPwd=null// 旧密码this.newPwd1=null// 第一次新密码this.newPwd2=null// 第二次新密码},//注意:方法名不要和变量名相同,否则执行时,会报错,因为属性是后操作的//即覆盖了方法,所以会报错(没有找到该一个的方法)
至此,我们进行测试,在输入后,点击取消或者点击"x"后,再次的进入,会发现,数据没有了
当然,可能有其他的弹出框我并没有解决,你可以自己进行解决,比如登录的框框,这里只是给出一个解决方案
最好的方案是在打开之前,就进行删除,虽然会保留数据
特别是数据量大的情况下,我们通常需要使用上面的方式,来解决数据保留,但是这是最方便的,比如添加方法:
dialogFormVisibles(){this.oldPwd=null// 旧密码this.newPwd1=null// 第一次新密码this.newPwd2=""// 第二次新密码this.dialogFormVisible=true; //放在后面,主要用来防止在一瞬间会出现},
对应的前端:
<div class="title-right" @click="dialogFormVisibles">修改密码</div>
那么又可以修改回来了:
<el-button class="cancel-button" @click="dialogFormVisible = false">取 消</el-button><el-dialog title="修改密码" :visible.sync="dialogFormVisible">
那么为什么不都操作呢,因为你删除一次,还要删除干啥呢
根据这个思路,我们可以回到Header.vue组件,修改如下:
goToLogin() {this.phone= "", // 双向绑定表单 手机号this.password= "", // 双向绑定表单 密码this.phoneNumber=null, // 发送验证码的手机号this.smsCode=null, // 输入的验证码this.dialogFormVisible = true; // 显示登录框},
这样,我们就解决了框框数据存在问题
注意:最好不要手动设置密码为null或者undefined,因为在后端会变成null,那么就相当于UPDATE user WHERE id=?(我们的id)
这个在sql中是会报错的,那么他返回报错信息,那么前端自然也会得到报错信息
从而执行this.$message.error(“修改密码失败!”);这个代码
至此,我们修改密码操作成功
现在我们再次创建子项目课程微服务edu-course-boot(8004):
最终成果:
mybatis-plus暂不支持或者不好支持比较复杂的多表关联查询,因为他自带的方法,是有限的,如果遇到复杂的多表查询
依旧使用mybatis+xml配置文件即可,用法和之前一样
修改yml配置文件,告诉程序去哪里找mapper.xml
对应的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.lagou</groupId><artifactId>edu-lagou</artifactId><version>1.0-SNAPSHOT</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.lagou</groupId><artifactId>edu-course-boot</artifactId><version>0.0.1-SNAPSHOT</version><name>edu-course-boot</name><description>edu-course-boot</description><properties><java.version>11</java.version></properties><dependencies><!-- web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- eureka客户端 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!-- mybatis plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version></dependency><!-- mysql --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--pojo持久化使用--><dependency><groupId>javax.persistence</groupId><artifactId>javax.persistence-api</artifactId><version>2.2</version></dependency><!--自动getset--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
将配置文件后缀修改成yml,内容如下:
server:port: 8004
spring:application:name: edu-course-bootdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.164.128:3306/edu_course?useUnicode=true&characterEncoding=utf8&serverTimezone=UTCusername: rootpassword: QiDian@666redis:host: 192.168.164.128port: 6379
eureka:client:service-url:defaultZone: http://localhost:7001/eurekaregister-with-eureka: truefetch-registry: trueinstance:prefer-ip-address: trueinstance-id: ${spring.cloud.client.ip-address}:${server.port}
mybatis-plus:mapper-locations: classpath:mybatis/mapper/*.xml #resources下创建mybatis/mapper
在启动类所在的包下创建entity包,然后创建如下实体类:
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;/*** 活动课程表(ActivityCourse)实体类** @author makejava* @since 2022-07-14 18:11:05*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString public class ActivityCourse implements Serializable {private static final long serialVersionUID = -89461375082427542L;/*** 主键ID*/private Integer id;/*** 课程ID*/private Integer courseId;/*** 活动开始时间*/private Date beginTime;/*** 活动结束时间*/private Date endTime;/*** 活动价格*/private Long amount;/*** 库存值*/private Integer stock;/*** 状态 0未上架 10已上架*/private Integer status;/*** 逻辑删除 0未删除 1删除*/private Integer isDel;/*** 备注*/private String remark;/*** 创建时间*/private Date createTime;/*** 创建人*/private String createUser;/*** 更新时间*/private Date updateTime;/*** 更新人*/private String updateUser;}
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;
import java.util.List;/*** 课程(Course)实体类** @author makejava* @since 2022-07-13 14:49:05*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Course implements Serializable {private Teacher teacher; //一门课程对应一个讲师private List<CourseSection> courseSectionList; //一门课程对应多个章节private ActivityCourse activityCourse;private static final long serialVersionUID = 464248821202087847L;/*** id*/private Object id;/*** 课程名*/private String courseName;/*** 课程一句话简介*/private String brief;/*** 原价*/private Object price;/*** 原价标签*/private String priceTag;/*** 优惠价*/private Object discounts;/*** 优惠标签*/private String discountsTag;/*** 描述markdown*/private String courseDescriptionMarkDown;/*** 课程描述*/private String courseDescription;/*** 课程分享图片url*/private String courseImgUrl;/*** 是否新品*/private Integer isNew;/*** 广告语*/private String isNewDes;/*** 最后操作者*/private Integer lastOperatorId;/*** 自动上架时间*/private Date autoOnlineTime;/*** 记录创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;/*** 总时长(分钟)*/private Integer totalDuration;/*** 课程列表展示图片*/private String courseListImg;/*** 课程状态,0-草稿,1-上架*/private Integer status;/*** 课程排序,用于后台保存草稿时用到*/private Integer sortNum;/*** 课程预览第一个字段*/private String previewFirstField;/*** 课程预览第二个字段*/private String previewSecondField;/*** 销量*/private Integer sales;}
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;/*** 课程节内容(CourseLesson)实体类** @author makejava* @since 2022-07-13 14:50:05*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseLesson implements Serializable {private CourseMedia courseMedia; //一小节课对应一个视频private static final long serialVersionUID = -35857311228165600L;/*** id*/private Object id;/*** 课程id*/private Integer courseId;/*** 章节id*/private Integer sectionId;/*** 课时主题*/private String theme;/*** 课时时长(分钟)*/private Integer duration;/*** 是否免费*/private Integer isFree;/*** 记录创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;/*** 排序字段*/private Integer orderNum;/*** 课时状态,0-隐藏,1-未发布,2-已发布*/private Integer status;}
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;/*** 课节视频表(CourseMedia)实体类** @author makejava* @since 2022-07-13 15:30:55*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseMedia implements Serializable {private static final long serialVersionUID = 673974921818890080L;/*** 课程媒体主键ID*/private Integer id;/*** 课程Id*/private Integer courseId;/*** 章ID*/private Integer sectionId;/*** 课时ID*/private Integer lessonId;/*** 封面图URL*/private String coverImageUrl;/*** 时长(06:02)*/private String duration;/*** 媒体资源文件对应的EDK*/private String fileEdk;/*** 文件大小MB*/private Long fileSize;/*** 文件名称*/private String fileName;/*** 媒体资源文件对应的DK*/private String fileDk;/*** 创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除,0未删除,1删除*/private Integer isDel;/*** 时长,秒数(主要用于音频在H5控件中使用)*/private Integer durationNum;/*** 媒体资源文件ID*/private String fileId;}
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;
import java.util.List;/*** 课程章节表(CourseSection)实体类** @author makejava* @since 2022-07-13 14:49:57*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseSection implements Serializable {private List<CourseLesson> courseLessonList; //一个章节对应多个小节private static final long serialVersionUID = 698702451600763670L;/*** id*/private Object id;/*** 课程id*/private Integer courseId;/*** 章节名*/private String sectionName;/*** 章节描述*/private String description;/*** 记录创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;/*** 排序字段*/private Integer orderNum;/*** 状态,0:隐藏;1:待更新;2:已发布*/private Integer status;}
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;/*** 讲师表(Teacher)实体类** @author makejava* @since 2022-07-13 14:49:40*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Teacher implements Serializable {private static final long serialVersionUID = 738571768582945875L;/*** id*/private Object id;/*** 课程ID*/private Integer courseId;/*** 讲师姓名*/private String teacherName;/*** 职务*/private String position;/*** 讲师介绍*/private String description;/*** 记录创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;}
创建mapper.CourseMapper接口:
package com.lagou.mapper;import com.lagou.entity.Course;
import org.apache.ibatis.annotations.Param;import java.util.List;/****/
public interface CourseMapper {/*** 查询全部课程信息* @return*/List<Course> getAllCourse();/*** 查询已登录用户购买的全部课程信息* @return*/List<Course> getCourseByUserId(@Param("userId") String userId);/*** 查询某门课程的详细信息* @param courseid 课程编号* @return*/Course getCourseById(@Param("courseid") Integer courseid);
}
创建service.CourseService接口及其实现类:
package com.lagou.service;import com.lagou.entity.Course;import java.util.List;/****/
public interface CourseService {/*** 查询全部课程信息* @return*/List<Course> getAllCourse();/*** 查询已登录用户购买的全部课程信息* @return*/List<Course> getCourseByUserId(String userId);/*** 查询某门课程的详细信息* @param courseid 课程编号* @return*/Course getCourseById(Integer courseid);
}
package com.lagou.service.impl;import com.lagou.entity.Course;
import com.lagou.mapper.CourseMapper;
import com.lagou.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;/****/
@Service
public class CourseServiceImpl implements CourseService {@Autowiredprivate CourseMapper courseMapper; @Overridepublic List<Course> getAllCourse() {System.out.println("===查询mysql===");return courseMapper.getAllCourse();}@Overridepublic List<Course> getCourseByUserId(String userId) {return courseMapper.getCourseByUserId(userId);}@Overridepublic Course getCourseById(Integer courseid) {return courseMapper.getCourseById(courseid);}}
创建controller.CourseController类:
package com.lagou.controller;import com.lagou.entity.Course;
import com.lagou.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.List;/****/
@RestController
@RequestMapping("course")
@CrossOrigin //跨域
public class CourseController {@Autowiredprivate CourseService courseService;@GetMapping("getAllCourse")public List<Course> getAllCourse() {List<Course> list = courseService.getAllCourse();return list;}@GetMapping("getCourseByUserId/{userid}")public List<Course> getCourseByUserId( @PathVariable("userid") String userid ) {List<Course> list = courseService.getCourseByUserId(userid);return list;}@GetMapping("getCourseById/{courseid}")public Course getCourseById(@PathVariable("courseid")Integer courseid) {Course course = courseService.getCourseById(courseid);return course;}
}
启动类:
package com.lagou;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication
@EnableEurekaClient // 注册到中心的客户端
@MapperScan("com.lagou.mapper") // 扫描mapper包
public class EduCourseBootApplication {public static void main(String[] args) {SpringApplication.run(EduCourseBootApplication.class, args);}}
在资源文件夹下,创建mybatis包,然后在该包下创建mapper包,最后在该mapper包下创建CourseDao.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.lagou.mapper.CourseMapper"><resultMap type="com.lagou.entity.Course" id="CourseMap"><result property="id" column="c_id" /><result property="courseName" column="course_name" /><result property="brief" column="brief" /><result property="price" column="price" /><result property="priceTag" column="price_tag" /><result property="discounts" column="discounts" /><result property="discountsTag" column="discounts_tag" /><result property="courseDescriptionMarkDown" column="course_description_mark_down" /><result property="courseDescription" column="course_description" /><result property="courseImgUrl" column="course_img_url" /><result property="isNew" column="is_new" /><result property="isNewDes" column="is_new_des" /><result property="lastOperatorId" column="last_operator_id" /><result property="autoOnlineTime" column="auto_online_time" /><result property="createTime" column="c_create_time" /><result property="updateTime" column="c_update_time" /><result property="isDel" column="c_is_del" /><result property="totalDuration" column="total_duration" /><result property="courseListImg" column="course_list_img" /><result property="status" column="c_status" /><result property="sortNum" column="sort_num" /><result property="previewFirstField" column="preview_first_field" /><result property="previewSecondField" column="preview_second_field" /><result property="sales" column="sales" /><!--注意:可以多次对应--><!--对应的老师一对一--><association property="teacher" javaType="com.lagou.entity.Teacher"><result property="id" column="t_id" /><result property="courseId" column="t_course_id" /><result property="teacherName" column="teacher_name" /><result property="position" column="position" /><result property="description" column="t_description" /><result property="createTime" column="t_create_time" /><result property="updateTime" column="t_update_time" /><result property="isDel" column="t_is_del" /></association><!--活动课程,顺序记得是association然后collection--><association property="activityCourse" javaType="com.lagou.entity.ActivityCourse"><result property="id" column="ac_id"/><result property="courseId" column="ac_course_id"/><result property="beginTime" column="begin_time"/><result property="endTime" column="end_time"/><result property="amount" column="amount"/><result property="stock" column="stock"/><result property="status" column="ac_status"/><result property="isDel" column="ac_is_del"/><result property="remark" column="remark"/><result property="createTime" column="ac_create_time"/><result property="createUser" column="create_user"/><result property="updateTime" column="ac_update_time"/><result property="updateUser" column="update_user"/></association><!--对应的章节,一对多--><collection property="courseSectionList" ofType="com.lagou.entity.CourseSection"><result property="id" column="cs_id"/><result property="courseId" column="cs_course_id"/><result property="sectionName" column="section_name"/><result property="description" column="cs_description"/><result property="createTime" column="cs_create_time"/><result property="updateTime" column="cs_update_time"/><!--表里面的这个字段,可能是is_de,若是,则修改成is_del--><result property="isDel" column="cs_is_del" /><result property="orderNum" column="cs_order_num" /><result property="status" column="cs_status" /><!--章节对应的多个小节--><collection property="courseLessonList" ofType="com.lagou.entity.CourseLesson"><result property="id" column="cl_id"/><result property="courseId" column="cl_course_id"/><result property="sectionId" column="cl_section_id"/><result property="theme" column="theme"/><result property="duration" column="cl_duration"/><result property="isFree" column="is_free"/><result property="createTime" column="cl_create_time"/><result property="updateTime" column="cl_update_time"/><result property="isDel" column="cl_is_del"/><result property="orderNum" column="cl_order_num"/><result property="status" column="cl_status"/><!--对应的视频--><association property="courseMedia" javaType="com.lagou.entity.CourseMedia"><result property="id" column="cm_id" /><result property="courseId" column="cm_course_id" /><result property="sectionId" column="cm_section_id" /><result property="lessonId" column="cm_lesson_id" /><result property="coverImageUrl" column="cover_image_url" /><result property="duration" column="cm_duration" /><result property="fileEdk" column="file_edk" /><result property="fileSize" column="file_size" /><result property="fileName" column="file_name" /><result property="fileDk" column="file_dk" /><result property="createTime" column="cm_create_time" /><result property="updateTime" column="cm_update_time" /><result property="isDel" column="cm_is_del" /><result property="durationNum" column="duration_num" /><result property="fileId" column="file_id" /></association></collection></collection></resultMap><select id="getAllCourse" resultMap="CourseMap"><include refid="courseInfo"/>ORDER BY amount DESC,c_id ,ac_create_time DESC</select><sql id="courseInfo">SELECTc.`id` c_id,`course_name`,`brief`,`price`,`price_tag`,`discounts`,`discounts_tag`,`course_description_mark_down`,`course_description`,`course_img_url`,`is_new`,`is_new_des`,`last_operator_id`,`auto_online_time`,c.`create_time` c_create_time,c.`update_time` c_update_time,c.`is_del` c_is_del,`total_duration`,`course_list_img`,c.`status` c_status,`sort_num`,`preview_first_field`,`preview_second_field`,`sales`,t.`id` t_id,t.`course_id` t_course_id,`teacher_name`,`position`,t.`description` t_description,t.`create_time` t_create_time,t.`update_time` t_update_time,t.`is_del` t_is_del,cs.`id` cs_id,cs.`course_id` cs_course_id,`section_name`,cs.`description` cs_description,cs.`create_time` cs_create_time,cs.`update_time` cs_update_time,cs.`is_del` cs_is_del,cs.`order_num` cs_order_num,cs.`status` cs_status,cl.`id` cl_id,cl.`course_id` cl_course_id,cl.`section_id` cl_section_id,`theme`,cl.`duration` cl_duration,`is_free`,cl.`create_time` cl_create_time,cl.`update_time` cl_update_time,cl.`is_del` cl_is_del,cl.`order_num` cl_order_num,cl.`status` cl_status,cm.`id` cm_id,cm.`course_id` cm_course_id,cm.`section_id` cm_section_id,cm.`lesson_id` cm_lesson_id,`cover_image_url`,cm.`duration` cm_duration,`file_edk`,`file_size`,`file_name`,`file_dk`,cm.`create_time` cm_create_time,cm.`update_time` cm_update_time,cm.`is_del` cm_is_del,`duration_num`,`file_id`,ac.id ac_id,ac.course_id ac_course_id,`begin_time`,`end_time`,`amount`,`stock`,ac.`status` ac_status,ac.`is_del` ac_is_del,`remark`,ac.`create_time`ac_create_time,`create_user`,ac.`update_time` ac_update_time,`update_user`FROMactivity_course ac RIGHT JOIN course c ON c.`id` = ac.`course_id`INNER JOIN teacher t ON c.id = t.`course_id`INNER JOIN course_section cs ON c.id = cs.`course_id`INNER JOIN course_lesson cl ON cs.`id` = cl.`section_id`LEFT JOIN course_media cm ON cm.`lesson_id` = cl.`id`</sql><select id="getCourseByUserId" resultMap="CourseMap"><include refid="courseInfo"/>WHERE c.id IN ( SELECT course_id FROM user_course_order WHERE STATUS = 20 AND is_del = 0 AND user_id = #{userid})ORDER BY amount DESC,c_id ,ac_create_time DESC</select><select id="getCourseById" resultMap="CourseMap"><include refid="courseInfo"/>where c.id = #{courseId}</select></mapper>
现在我们进行启动,访问localhost:8004/course/getAllCourse,若出现了数据,代表操作完成
当然,如果在后端明明有对应的数据,但是他还是报红,这并不需要注意,因为可能是idea的问题,在启动时,不会出现影响
现在我们回到前端Index.vue组件:
找到或查看如下:
// 去课程微服务 获取 全部课程getCourseList(){ return this.axios.get("http://localhost:8004/course/getAllCourse").then((result) => {console.log(result);this.courseList = result.data;}).catch( (error)=>{this.$message.error("获取课程信息失败!");} );},
然后查看如下:
created() {this.getCourseList(); //保证钩子函数里面有执行该方法
接下来查看前端页面,若数据出现,代表操作完成
但现在有个问题,我们发现,每次的刷新,他都会去mysql里面去查询,那么在大量的用户下,这对mysql是很大的负担的
所以我们需要缓存,也就是说,我们需要一个固定的数据,而不用我们去查了,那么我们可以使用redis
即高并发下redis帮你扛
看后面的操作:
引入redis依赖,加入redis,前面的依赖中已经存在了,所以这里就不给出了
修改yml 配置redis服务器ip,前面的yml也进行了操作,所以这里也不给出了
那么配置操作完毕,这就有个问题,redis操作应该放在controller?还是service?
由于我们的具体操作在service里面,所以就放在service,当然,在controller里面操作也行,具体看你如何操作
这里就放在service里面
修改或添加对应实现类的部分代码:
@Autowiredprivate RedisTemplate<Object,Object> redisTemplate;@Overridepublic List<Course> getAllCourse() {//将redis内存中的序列化的集合名称用String重新命名(增加可读性)RedisSerializer rs = new StringRedisSerializer();redisTemplate.setKeySerializer(rs); //这个可以不加,只是单纯的给redis的客户端好的查看//即可读性提高System.out.println("查询redis");List<Course> list = (List<Course>)redisTemplate.opsForValue().get("allCourses");if(list == null){//去数据库System.out.println("====MySql数据库====");list = courseMapper.getAllCourse();// 把从数据库查询的集合放在redis内存中(key,value,过期时间,分钟为单位)redisTemplate.opsForValue().set("allCourses", list,10, TimeUnit.MINUTES);}return list;}
重启服务,然后继续访问localhost:8004/course/getAllCourse
查看redis,若有数据,则代表操作成功,然后多次的访问,查看打印信息
若只有"查询redis"则代表从redis里获取数据成功,我们也可以很直观的发现,第二次的查询一般要快一点
虽然并不是明显,这是因为数据量还不是非常大
即操作mysql时,比单纯的去redis获取数据慢,这是因为mysql经历了查询,而不是直接的得到数据
当然,这里可能会出现的问题是,如果修改了mysql表数据,那么可能查询的值与表不一致
因为从redis里获取了,所以,我们通常需要给redis的数据设置过期时间,具体的大小,就要看实际情况了,这里我就设置了10分钟
通常在redis中,具体大小上限如下:
String类型:对应的value最大可以存512MB
List类型:对应的value的元素个数最大可以存2^32-1,即4294967295个
Set类型(Zset有序集合类型):对应的value的元素个数最大可以存2^32-1,即4294967295个
Hash类型:对应的value的键值对个数最大可以存2^32-1,即4294967295个
高并发下缓存穿透问题:
因为,我们假设高并发下,1000个人同时进入方法执行,1000个人从缓存中找集合,没有找到,那么进入下一步的if
就会发生1000个人同时从数据库查询,这样的话,执行了1000次查询数据库,效率低下,redis没用到
这样的原因,就是redis缓存查第一次之后,后续的查询没有拦住,这就是"缓存穿透"
简单来说就是判断后面的并没有执行完或者还在执行,那么这个判断虽然有值,但是与他们并没有影响
我们可以进行测试,模拟20个线程高并发
我们在CourseController类里加上如下代码:
@GetMapping("getAllCoursee") //不是getAllCourse,用来区分public List<Course> getAllCoursee() {// 模拟多线程:创建一个容量20个的线程池ExecutorService es = Executors.newFixedThreadPool(20);// 模拟20个线程同时查询for (int i = 1; i <= 20; i++) {es.submit(new Runnable() {@Overridepublic void run() {courseService.getAllCourse();}});}return courseService.getAllCourse();}
我们重启项目,访问localhost:8004/course/getAllCoursee后,查看后台,可以发现,打印了多个"mysql"的信息,即:
====MySql数据库====
所以我们可以发现,他的确发生了缓存穿透,且出现了21个对应的两个redis和mysql的打印信息
当我们再次的执行,可以发现,只有21个redis了
那么如何解决呢:
最简单粗暴的解决方案:同步方法锁
@Overridepublic synchronized List<Course> getAllCourse() {
因为我们都是访问该一个服务器,所以直接的加锁就可以,而不是访问多个(在80章博客说明过,那么就需要分布式锁了)
所以这里直接的加锁就可以了
重启项目,再次的测试,会发现,只会出现一个对应的mysql的打印信息了,即:
====MySql数据库====
但是我们也知道,直接的加锁,必然导致效率低下,因为后面的线程需要等待,虽然这里并没有明显体现(因为用户还是太少了)
所以说,若在大量的用户下,这个方式是不可取的,因为效率非常低,可能导致某些用户需要等待许久
效率稍微高一些的方案:同步代码块(双层检测锁 DCL:double check lock)
我们现在修改getAllCourse方法:
@Overridepublic List<Course> getAllCourse() {//将redis内存中的序列化的集合名称用String重新命名(增加可读性)RedisSerializer rs = new StringRedisSerializer();redisTemplate.setKeySerializer(rs);System.out.println("查询redis");List<Course> list = (List<Course>)redisTemplate.opsForValue().get("allCourses");if(list == null){//为什么不在"if(list == null){"这里或者前面的地方加上锁呢//因为上面的获取list必然是多个null,所以若在这里加上锁//并没有什么作用,虽然也行,但是在锁的代码比这里多,而这里是基本最小的锁的代码//我们最好保证,锁的粒度要尽量的小,虽然锁的开销一样//但是粒度越大,维护起来也越麻烦,特别是大的项目中//即双层检测锁实际上就是一个粒度非常小的锁操作//且解决了对应的问题(比如再次的获取,为了后面的线程做服务的,因为第一个设置了)//排队,让第一个人进,走一遍流程(后面的人就会走缓存了)synchronized (this){list = (List<Course>)redisTemplate.opsForValue().get("allCourses");if(list == null){ //在公司里面,最好设置null == list,防止赋值,虽然这里通常会使得报错//但是在其他的语言中,可能不会,比如js,所以最好养成习惯//去数据库System.out.println("====MySql数据库====");list = courseMapper.getAllCourse();// 把从数据库查询的集合放在redis内存中redisTemplate.opsForValue().set("allCourses", list,10, TimeUnit.MINUTES);}}}return list;//实际上通过实际情况我们也可以看到,若在外面加上锁,那么任何线程都会操作锁,而里面不会,而之所以里面也加上了对应的判断,是使得后续操作缓存,所以这个方法是基本上很好的一种方法//而正是因为对应的内部和外面的判断基本是互通的,所以才会称为双层检测锁}
这样,在小的锁的代码下,我们也解决了缓存穿透
我们重启项目,继续测试,若只有一个对应的mysql信息打印,代表操作成功
现在我们来讲讲如果保证redis的数据是最新的,在前面我也提到过,通常使用过期时间来解决
但是过期时间自然只是一个兜底的操作,所以我们需要解决这种问题
我们通常会这样操作:
如果课程中内容发生变化,通常我们在修改课程内容的时候(写操作基本都是如此)
会先将redis中的相关集合删除,然后将最新的数据保存到数据库
而查询数据时,因为redis中的数据已经删除了,所以会第一时间去数据库查询,保证数据是最新的
这样就避免了redis的数据不是最新的,所以从这里可以看出,在多个服务互相操作时
服务之间通常需要根据对应的服务的业务来进行相应的改变,这样的改变,能够更加的使得数据完整或者解决某些问题
那么对于这样,你可能会假设,如果非常的细度会怎么样呢
细度:在redis删除(或者其他的写操作,如修改和添加)过程中,可以得到数据吗
通常来说,一般不能,因为redis在接收到删除时(当然也包括任何改变数据的操作,比如修改,添加等等)
基本都会在对应的键(键里面也可也包括键,比如键值对类型)上加上类似于锁的概念,所以通常并不能
那么删除后,我们可以进入,然后可以发现没有数据了,即锁是在访问数据之前加的,或者说准备访问数据之前加的
当然了,上面最好是不频繁的操作更新,因为如果频繁的更新,那么由于每次的操作都会连接redis
所以到那个时候,最好是操作mysql,而不要存redis了
因为那个时候,还操作redis的话,性能自然比单独的mysql要慢了,因为需要连接操作redis,所以具体问题,需要具体分析
这是对于更新来说的,当然了,单独的查询来说,我们通常是操作redis缓存的
所以我们也通常操作读写分离(自然这里的读取也操作了细度,但是其他数据没有,所以可能是旧数据
这里不是在键,而是具体的一条数据),具体就不多说了
使用mybatisplus换血大改造:
我们在配置文件里面复制对应的查询代码,使用EXPLAIN(explain:中文意思:解释)来看看对应的性能,发现,是不好的
但我们并不进行优化,因为这里数据量还是很少的,优化并不明显,如果数据量够大
那么优化后,那么执行的效率通常也会更快,比如原来需要30秒执行完
优化后,可能只需要1秒就执行完了,当然这只是举例而已
这里主要操作后面,如下:
mp并不支持多表查询,所以我们通过oop的思想进行数据组装
90%的查询优化都是采用:“空间换时间”(比如创建索引自然需要空间来保存索引,就如字典需要一定的页数保存对应的目录)
9.9%的优化通常需要算法来完成,剩余的看机器或者其他特殊原因了
我们使用基本的查询来分开前面的sql语句,只需要多执行几次即可,然后将结果进行组装
那么有个问题,关联查询比分开后的多个单表查询总体来说是慢了还是快了,答:并不绝对
因为关联需要考虑笛卡尔积(在这里操作条件),如果没有条件,那么自然,多个单表查询快
如果有条件,通常多表查询可能要快些(前提是有索引),但是总体来说,多个单表要快,虽然单表也可以设置条件以及索引
但是通过条件的话,对应的条数基本是一样的,所以由于mysql的计算或者程序的计算基本类似
所以在条件方面,他们可以说是相同的,但是没有条件,一般是单表查询快
所以总体来说,没有条件时,单表是相加的形式,而多表是相乘的形式,自然,单表通常快点,有条件时,基本都是相乘的模式
但是有时候单表可能有格外的操作,使得解决后面的多余(在没有后续操作的情况下)
这时候,真的总体是相同的,但且由于基本都有条件
即就在代码多,访问多(即条数多)和解决多余(可以说是细度的维护,或者在没有后续操作的情况下)以及mysql资源利用(计算,因为代码之所以多,就是为了解决这个)之间徘徊了,当然单表可能有其他好处,具体可以百度(比如表的锁竞争等等)
可以浅看一下这个博客https://blog.csdn.net/wangxuelei036/article/details/107647034
为什么说单表也能是相乘:因为执行多次,后面的代码会体现的
但是我们还是不会在mysql层面来进行关联了,这里我们在内存中进行关联,也就是在程序中进行
因为在内存中操作,实际上也就是操作计算形式,而在数据库中,他的关联可能需要更多或者更少的时间(计算的算法原因)
我们就认为算是相同的时间,在相同的时间下
对于数据库来说,他的资源通常是多个服务的总和(多个服务访问他),即资源少点,所以也通常不会交给他来计算
而是交给各自的服务内存来计算,就不麻烦数据库的计算资源了
所以一般的,大多数的业务都是操作多个表查询(不是多表),然后将结果进行组装
现在我们进行改造,首先是yml文件,我们去除如下:
mybatis-plus:mapper-locations: classpath:mybatis/mapper/*.xml
然后删除资源文件里面的CourseDao.xml文件以及他的包mybatis/mapper
修改实体类:
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;
import java.util.List;/*** 课程(Course)实体类** @author makejava* @since 2022-07-13 14:49:05*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Course implements Serializable {private static final long serialVersionUID = 464248821202087847L;/*** id*/private Object id;/*** 课程名*/private String courseName;/*** 课程一句话简介*/private String brief;/*** 原价*/private Object price;/*** 原价标签*/private String priceTag;/*** 优惠价*/private Object discounts;/*** 优惠标签*/private String discountsTag;/*** 描述markdown*/private String courseDescriptionMarkDown;/*** 课程描述*/private String courseDescription;/*** 课程分享图片url*/private String courseImgUrl;/*** 是否新品*/private Integer isNew;/*** 广告语*/private String isNewDes;/*** 最后操作者*/private Integer lastOperatorId;/*** 自动上架时间*/private Date autoOnlineTime;/*** 记录创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;/*** 总时长(分钟)*/private Integer totalDuration;/*** 课程列表展示图片*/private String courseListImg;/*** 课程状态,0-草稿,1-上架*/private Integer status;/*** 课程排序,用于后台保存草稿时用到*/private Integer sortNum;/*** 课程预览第一个字段*/private String previewFirstField;/*** 课程预览第二个字段*/private String previewSecondField;/*** 销量*/private Integer sales;}
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;/*** 课程节内容(CourseLesson)实体类** @author makejava* @since 2022-07-13 14:50:05*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseLesson implements Serializable {private static final long serialVersionUID = -35857311228165600L;/*** id*/private Object id;/*** 课程id*/private Integer courseId;/*** 章节id*/private Integer sectionId;/*** 课时主题*/private String theme;/*** 课时时长(分钟)*/private Integer duration;/*** 是否免费*/private Integer isFree;/*** 记录创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;/*** 排序字段*/private Integer orderNum;/*** 课时状态,0-隐藏,1-未发布,2-已发布*/private Integer status;}
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;
import java.util.List;/*** 课程章节表(CourseSection)实体类** @author makejava* @since 2022-07-13 14:49:57*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseSection implements Serializable {private static final long serialVersionUID = 698702451600763670L;/*** id*/private Object id;/*** 课程id*/private Integer courseId;/*** 章节名*/private String sectionName;/*** 章节描述*/private String description;/*** 记录创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;/*** 排序字段*/private Integer orderNum;/*** 状态,0:隐藏;1:待更新;2:已发布*/private Integer status;}
即对应的多或者单的属性删除了,之所以需要修改,是因为在使用对应的字段时
是没有的(报错,前提是使用到了,因为null基本会忽略),且在mp里操作了其他类型的数据
比如这里的Teacher,那么他会优先在没有对应字段前报错(即是很长的相同报错,而不会是没有找到)
即只能是基本的类型(自然也包括String)
现在我们修改CourseMapper接口:
package com.lagou.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.Course;/****/
public interface CourseMapper extends BaseMapper<Course> {}
对应的CourseService接口及其实现类:
package com.lagou.service;import com.lagou.entity.Course;import java.util.List;/****/
public interface CourseService {/*** 查询全部课程信息* @return*/List<Course> getAllCourse();/*** 查询已登录用户购买的全部课程信息* @return*/List<Course> getCourseByUserId(String userId); /*** 查询某门课程的详细信息* @param courseid 课程编号* @return*/Course getCourseById(Integer courseid);//上面的方法先定义好,可能后面会使用到
}
package com.lagou.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lagou.entity.*;
import com.lagou.mapper.CourseMapper;
import com.lagou.service.CourseService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;/****/
@Service
public class CourseServiceImpl implements CourseService {@Autowiredprivate CourseMapper courseMapper;//如果爆红,不需要理会,这是idea的问题,执行时是不会出错误的
//前面我并没有说明,这里就说明一下,若要解决该问题,有多种方式//比如只需要在接口上加上代表容器类的注解即可,比如@Component@Autowiredprivate RedisTemplate<Object,Object> redisTemplate;@Overridepublic List<Course> getAllCourse(){List<Course> initCourse = getInitCourse();return initCourse;}@Overridepublic List<Course> getCourseByUserId(String userId) {return null;}@Overridepublic Course getCourseById(Integer courseid) {return null;}// 初始化基本的全部课程private List<Course> getInitCourse(){QueryWrapper q = new QueryWrapper();q.eq("status", 1);// 已上架q.eq("is_del", Boolean.FALSE);// 未删除,在mysql中,false代表0,true代表1,所以这里相当于是0//且这里规定0代表未删除,所以这里就是未删除q.orderByDesc("sort_num");// 排序,记住,在相同时//自然根据表的排列进行(无论是否是升序还是降序,到那时,基本都是根据表的排列)//这里就算8,7,9,12,如果是升序,那么是7,9,12,8,即7,9,12都是根据表排列进行的//而不会升序是相反的或者降序是相反的return courseMapper.selectList(q);}}
对应的controller类:
package com.lagou.controller;import com.lagou.entity.Course;
import com.lagou.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/****/
@RestController
@RequestMapping("course")
@CrossOrigin //跨域
public class CourseController {@Autowiredprivate CourseService courseService;@GetMapping("getAllCourse")public List<Course> getAllCourse() {List<Course> list = courseService.getAllCourse();return list;}}
我们重启,继续访问localhost:8004/course/getAllCourse,若有数据,代表初步操作成功
现在,我们在mapper包下创建TeacherMapper接口:
package com.lagou.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.Course;
import com.lagou.entity.Teacher;/****/
public interface TeacherMapper extends BaseMapper<Teacher> {}
在entity包下创建CourseDTO类:
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.util.Date;
import java.util.List;/****/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString //这个可以不加,因为@Data,在对应的方法中,我们手动的是覆盖@Data的
//可能toString方法有提示算法覆盖,但并没有什么用
//即无论是点击替换还是不替换都是我们手动的为主
public class CourseDTO {//与普通的Course类是相同的,只是这里有了对应的其他字段,因为我们并不将他用来操作mp,所以可以写上private Teacher teacher; // 一门课程对应一个讲师//该字段放前面,因为返回给前端的json是根据类的属性先后来进行的//因为这里的@ResponseBody注解是以属性名称的内容为主private static final long serialVersionUID = 464248821202087847L;/*** id*/private Object id;/*** 课程名*/private String courseName;/*** 课程一句话简介*/private String brief;/*** 原价*/private Object price;/*** 原价标签*/private String priceTag;/*** 优惠价*/private Object discounts;/*** 优惠标签*/private String discountsTag;/*** 描述markdown*/private String courseDescriptionMarkDown;/*** 课程描述*/private String courseDescription;/*** 课程分享图片url*/private String courseImgUrl;/*** 是否新品*/private Integer isNew;/*** 广告语*/private String isNewDes;/*** 最后操作者*/private Integer lastOperatorId;/*** 自动上架时间*/private Date autoOnlineTime;/*** 记录创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;/*** 总时长(分钟)*/private Integer totalDuration;/*** 课程列表展示图片*/private String courseListImg;/*** 课程状态,0-草稿,1-上架*/private Integer status;/*** 课程排序,用于后台保存草稿时用到*/private Integer sortNum;/*** 课程预览第一个字段*/private String previewFirstField;/*** 课程预览第二个字段*/private String previewSecondField;/*** 销量*/private Integer sales;
}
修改CourseService接口的部分方法:
List<CourseDTO> getAllCourse();
在CourseServiceImpl类里(也就是CourseService接口的实现类)加上或者修改如下:
@Autowiredprivate TeacherMapper teacherMapper;@Overridepublic List<CourseDTO> getAllCourse(){List<Course> initCourse = getInitCourse();List<CourseDTO> courseDTOS = new ArrayList<>();for(Course course : initCourse){CourseDTO dto = new CourseDTO();// course将属性全部赋给给courseDTO对象BeanUtils.copyProperties(course, dto);//首先执行course对应的所有get方法(首字母大小写忽略)//然后将对应的值给操作对应dto的所有set方法(首字母大小写忽略)//只要对应,自然可以赋值,否则没有赋值//上面就得到了数据,只是CourseDTO可以多几个属性字段courseDTOS.add(dto);setTeacher(dto); //因为地址的原因,所以改变dto,实际上courseDTOS集合的dto的值也进行改变了}return courseDTOS;}// 基本的老师查询private void setTeacher(CourseDTO courseDTO){QueryWrapper q = new QueryWrapper();q.eq("course_id", courseDTO.getId());// 一个课程,一个老师q.eq("is_del", Boolean.FALSE);// 未删除Teacher teacher = teacherMapper.selectOne(q);courseDTO.setTeacher(teacher);}
修改CourseController类的部分方法:
package com.lagou.controller;import com.lagou.entity.Course;
import com.lagou.entity.CourseDTO;
import com.lagou.service.CourseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/****/
@RestController
@RequestMapping("course")
@CrossOrigin //跨域
public class CourseController {@Autowiredprivate CourseService courseService;@GetMapping("getAllCourse")public List<CourseDTO> getAllCourse() {List<CourseDTO> list = courseService.getAllCourse();return list;}//也就是说,我们也通常创建一个相同的类,只是该类不参与mp,所以我们用来的存放数据(因为可以加属性)//原来的类来操作数据库//他不能加数据,因为属性字段是需要与数据库一致的,数据库没有,会报错,且类的类型,也会报错//前面说过了,在没有字段的报错之前(即是很长的相同报错,而不会是没有找)}
现在我们重启启动,访问localhost:8004/course/getAllCourse,若出现了teacher的数据,代表操作成功
所以上面基础查询我们使用entity(对应类的)的mapper
再将数据拷贝到entityDTO(对应类的DTO)中进行组装
即entity对应类删除了依赖性的属性,entityDTO对应类的DTO添加依赖依赖性的属性
所以上面的确是将我们的查询结果进行组装了,而不是在mysql自己进行查询关联出来
现在我们再次在entity包下创建LessonDTO:
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.util.Date;/****/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class LessonDTO {private static final long serialVersionUID = -35857311228165600L;/*** id*/private Object id;/*** 课程id*/private Integer courseId;/*** 章节id*/private Integer sectionId;/*** 课时主题*/private String theme;/*** 课时时长(分钟)*/private Integer duration;/*** 是否免费*/private Integer isFree;/*** 记录创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;/*** 排序字段*/private Integer orderNum;/*** 课时状态,0-隐藏,1-未发布,2-已发布*/private Integer status;}
现在,我们在CourseDTO类里添加如下属性:
private List<CourseLesson> lessonsDTO2; //一门课对应多个课时,但我们只需要前两个,因为业务需求的原因
//这里虽然并没有操作LessonDTO这个类,但是在以后可能需要,比如操作视频时,所以上面的LessonDTO类先保留//注意:在前端操作时,是使用了lessonsDTO2名称,所以这个名称不要改变
//否则页面不会出现对应的课时,自己测试或者看代码(源码)就知道了
在mapper包下创建lessonMapper接口:
package com.lagou.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.CourseLesson;/****/
public interface LessonMapper extends BaseMapper<CourseLesson> {}
然后在CourseServiceImpl类里修改或则添加如下:
@Autowiredprivate LessonMapper lessonMapper;@Overridepublic List<CourseDTO> getAllCourse(){List<Course> initCourse = getInitCourse();List<CourseDTO> courseDTOS = new ArrayList<>();for(Course course : initCourse){CourseDTO dto = new CourseDTO();// course将属性全部赋给给courseDTO对象BeanUtils.copyProperties(course, dto);//首先执行course对应的所有get方法(首字母大小写忽略)//然后将对应的值给操作对应dto的所有set方法(首字母大小写忽略)//只要对应,自然可以赋值,否则没有赋值//上面就得到了数据,只是CourseDTO可以多几个属性字段courseDTOS.add(dto);setTeacher(dto); //因为地址的原因,所以改变dto,实际上courseDTOS集合的dto的值也进行改变了setTop2Lesson(dto); //也保存了对应的前两条数据}return courseDTOS;}// 前两节课private void setTop2Lesson( CourseDTO courseDTO ){QueryWrapper q = new QueryWrapper();q.eq("course_id", courseDTO.getId());// 一个课程,一个老师q.eq("is_del", Boolean.FALSE);// 未删除q.orderByAsc("section_id","order_num"); //排序q.last("limit 0," + 2); // 只要前2条数据// last方法:代表往最后的语句添加里面的内容,注意:不分先后,也就是说//就算该行代码先执行,他也是在最后,若有多个,则以后面的为主,即后面的覆盖了//从这里可以得出,平常,我们操作关联时,那是从所有的数据进行取出//但是,我们分开后,可以更加细度的操作了//所以说,有时候,我们可以更加的扩展,虽然需要的条数变多了//且需要多访问一下(因为后续可能需要全部的数据)//但是更好维护了,而多表就不好维护,所以单表的操作方式更加的多//其实,在大量的数据下,前端操作计算,可能会比这里慢点,因为这里固定两条//而计算可能需要更多(因为访问可以有索引来提高执行速度)//即只要计算超过了连接mysql的时间,且包括执行时间,那么就算慢了,所以单表还是好的//上面的只是可能,实际上,访问的执行时间通常是比较慢的(因为数据量大,自然也需要更多的执行时间),即访问多,条数多//所以这里也只是为了更好的维护(如改变需求)而已(实际上是主要解决集中访问,如果多次的刷新对应的那个部分,整体部分,那么多表是非常不友好的,所以不只是好维护,也是防止这样的可能,虽然多表在其他页面可以使得不访问,即全部化了,这也是多表在单表之上的优点,这里也就是单表的缺点,而维护性,自然就是前端关联出现的维护和后端的维护了,可以认为是后端好维护些,因为数据基本是对应的,而不用在前端将数据依次的传递了)//但这里我们也知道,虽然单表在某些方面比较好,但是代码也比多表更多,虽然多表不好具体维护,而单表可以更加细度维护(直接取部分数据,有具体来源,而不用在前端找数据了),但多表的整体维护比单表好的(对于数据关联来说的),只是多表的数据在前端的耦合度太高了(一个数据到处使用)//即无论是访问多(添加条数)还是代码多(相同条数),都是为了更好的维护(虽然两个都有优点),以及防止集中访问//简单来说,单表将访问平均,虽然在前端耦合度比较低,但是有时需要多访问//多表访问集中,虽然其他页面不需要访问,但是在前端耦合度比较高List<CourseLesson> list = lessonMapper.selectList(q);courseDTO.setLessonsDTO2(list);}//总结:
//单表:访问平均,虽然多访问,多条数(在没有后续操作的情况下,可能就是少条数了,即变成了优点,而不是缺点了),但在前端耦合度比较低(即对项目耦合度比较低),且节省mysql服务器资源,在对应数据方面可以更好的解决需求问题,即更加的细度维护(sql数据来说的),但是在整体上来说,不好维护(sql数据来说的),需要大量工作进行关联,只是关联后,比较稳定
//多表:访问集中,但是其他页面可以不访问,可是在前端耦合度比较高(即对项目耦合度比较高),不节省mysql服务器资源,在对应数据方面,不好维护,因为数据是到处使用的,即如果有需求问题,可能需要改变sql语句,对整体来说不友好,即不好细度维护(sql数据来说的),但是整体来说,改变sql语句可以改变大多数关联,即好维护(sql数据来说的),只是关联后,可能不稳定//但是在后期,我们需要的是解决耦合度问题,所以单表使用比较多,因为在整个项目上来说,单表比较好维护,而在sql上各有千秋,但是稳定性还是单表好
至此,我们重启,再次的访问localhost:8004/course/getAllCourse,看看有没有对应的课时,若有,则操作成功
我们也发现,对应的DTO注意存放具体需要的内容,所以整体来说,单表,虽然使用更多的代码,但是也解决了数据的多余
就如上面说的,在大量的数据下(多余),前端操作计算大于连接mysql的时间,且包括执行时间,多表就不划算了
但实际上就算没有大于,我们也会为了更加的好细度的维护,也会使用单表,虽然单表的代码多(自然也是有缺点的)
虽然在少量数据下,访问多,但是可以更好维护,但在在大量数据下,访问多,但是执行速度快,且也更好的维护
但我们发现,这里多次使用DTO,这是因为mp的原因导致的,即mp虽然简化了代码,但也多出了简化的限制
现在我们在entity包下创建SectionDTO类:
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.util.Date;/****/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class SectionDTO {private List<LessonDTO> courseLessons; //一章对应多个小节//因为操作了LessonDTO虽然前面我们没有操作循环,但这里操作了所以需要是LessonDTOprivate static final long serialVersionUID = 698702451600763670L;/*** id*/private Object id;/*** 课程id*/private Integer courseId;/*** 章节名*/private String sectionName;/*** 章节描述*/private String description;/*** 记录创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;/*** 排序字段*/private Integer orderNum;/*** 状态,0:隐藏;1:待更新;2:已发布*/private Integer status;}
在CourseDTO类下,添加如下属性:
private List<SectionDTO> courseSections;
//一门课程对应多个章节,因为章节里面一般有课时,且现在会用到,所以是DTO
在mapper包下创建SectionMapper接口:
package com.lagou.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.CourseSection;
import com.lagou.entity.Teacher;/****/
public interface SectionMapper extends BaseMapper<CourseSection> {}
修改对应的CourseService接口的部分方法:
CourseDTO getCourseById(Integer courseid);
然后在UserServiceImpl类里添加如下:
@Autowiredprivate SectionMapper sectionMapper; //如果爆红,不需要理会,这是idea的问题,执行时是不会出错误的
//前面如果没有说明,这里就说明一下,若要解决该问题,只需要在接口上加上代表容器类的注解即可,比如@Component@Overridepublic CourseDTO getCourseById(Integer courseid) {// 根据课程id获取课程的基本信息Course course = courseMapper.selectOne(new QueryWrapper<Course>().eq("id", courseid));CourseDTO courseDTO = new CourseDTO();BeanUtils.copyProperties(course, courseDTO);// 关联老师setTeacher(courseDTO);// 关联章节List<SectionDTO> sectionDTOS = getCourseSection(courseDTO);courseDTO.setCourseSections(sectionDTOS);return courseDTO;}//关联章节private List<SectionDTO> getCourseSection(CourseDTO courseDTO){QueryWrapper q = new QueryWrapper();q.eq("course_id", courseDTO.getId());// 一个课程,N章q.eq("is_del", Boolean.FALSE);// 未删除q.eq("status", 2);// 已发布q.orderByAsc("order_num"); //排序// 基本的章节集合List<CourseSection> list = sectionMapper.selectList(q);// 关联的章节集合List<SectionDTO> sectionDTOS = new ArrayList<>();for(CourseSection section : list){SectionDTO sectionDTO = new SectionDTO();BeanUtils.copyProperties(section, sectionDTO);q.clear(); // 清除条件q.eq("section_id", sectionDTO.getId());// 已发布q.eq("is_del", Boolean.FALSE);// 未删除q.orderByDesc("order_num"); //排序// 某章节的全部小节(基本信息)List<CourseLesson> lessons = lessonMapper.selectList(q);// 某章节的全部小节(关联信息)List<LessonDTO> lessonDTOS = new ArrayList<>();for(CourseLesson lesson : lessons){LessonDTO lessonDTO = new LessonDTO();BeanUtils.copyProperties(lesson, lessonDTO);lessonDTOS.add(lessonDTO);}// 章节关联所有小节sectionDTO.setCourseLessons(lessonDTOS);// 某个章节放入到章节集合sectionDTOS.add(sectionDTO);}return sectionDTOS;}
现在我们在CourseController类下,添加如下:
@GetMapping("getCourseById/{courseid}")public CourseDTO getCourseById(@PathVariable("courseid")Integer courseid) {CourseDTO courseDTO = courseService.getCourseById(courseid);return courseDTO;}
现在我们启动项目,访问localhost:8004/course/getCourseById/7,若出现了对应的数据,代表操作成功
我们也可也发现,上面有升序和降序,实际上这里并不需要理会,因为有些排序字段是值大的在前
比如上面的课程表和课时表,而小的在前就是章节表
当然,这里是根据业务来的,所以并不是绝对的,且也可也通过前端进行再次的排序操作,所以这里并不是非常重要
实际上通常也由字段的不同,或者表里面数据的不同来进行的,通常以主键为主
像这里的order_num以主键和自身为主来决定是否升序,还是降序
比如课时降序(1,0,主键导致1在前,0在后,那么就是降序)
而章节升序(1,2,主键导致1在前,2在后,那么就是升序)等等
而sort_num,就以自身为主(大的在前),所以通常都是降序
现在我们在LessonDTO类里加上如下属性:
private CourseMedia courseMedia; //一小节对应一个视频
//他不需要DTO,因为他并没有其他关联,也就是说,他的字段可以之间操作基础查询
//而不会出现没有字段的原因,所以不需要DTO了
再在mapper包下创建MediaMapper接口:
package com.lagou.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.Course;
import com.lagou.entity.CourseMedia;/****/
public interface MediaMapper extends BaseMapper<CourseMedia> {}
在CourseServiceImpl类里加上如下:
@Autowiredprivate MediaMapper mediaMapper;// 设置每节课的视频private void setMedia(LessonDTO lessonDTO){QueryWrapper q = new QueryWrapper();q.eq("lesson_id", lessonDTO.getId());// 一节课,一个视频q.eq("is_del", Boolean.FALSE);// 未删除CourseMedia media = mediaMapper.selectOne(q);lessonDTO.setCourseMedia(media);}//部分添加for(CourseLesson lesson : lessons){LessonDTO lessonDTO = new LessonDTO();BeanUtils.copyProperties(lesson, lessonDTO);setMedia(lessonDTO); //这里加上了一行代码lessonDTOS.add(lessonDTO);}//注意:之前的前两节课不需要他,因为他那里基本用不到,所以就不修改了
现在,我们再次的重启项目,访问localhost:8004/course/getCourseById/7,若出现了对应的数据,代表操作成功
接下来我们来补充redis的代码,在CourseServiceImpl类里修改如下:
@Overridepublic List<CourseDTO> getAllCourse() {//将redis内存中的序列化的集合名称用String重新命名(增加可读性)RedisSerializer rs = new StringRedisSerializer();redisTemplate.setKeySerializer(rs);// 1、先去redis中查询System.out.println("***查询redis***");// 课程dto集合List<CourseDTO> courseDTOS = (List<CourseDTO>) redisTemplate.opsForValue().get("allCourses");// 2、redis中没有,才会去mysql查询if (null == courseDTOS) {synchronized (this) {courseDTOS = (List<CourseDTO>) redisTemplate.opsForValue().get("allCourses");if (null == courseDTOS) {System.out.println("===查询mysql===");List<Course> initCourse = getInitCourse();courseDTOS = new ArrayList<>();for (Course course : initCourse) {CourseDTO dto = new CourseDTO();// course将属性全部赋给给courseDTO对象BeanUtils.copyProperties(course, dto);courseDTOS.add(dto);setTeacher(dto); setTop2Lesson(dto);}redisTemplate.opsForValue().set("allCourses", courseDTOS, 10, TimeUnit.MINUTES);}}}return courseDTOS;}//简单来说就是获取数据的方式的原因,所以与前面的是类似的
在这之前,我们首先需要给CourseDTO类实现一个接口,如下:
public class CourseDTO implements Serializable {//若没有写serialVersionUID,默认会生成//因为redis操作类时(没有操作value的序列化设置),那么需要该类序列化//否则对应的set方法和get方法(如果没有对应的key,那么会跳过,即不会报错,否则会报错)执行不了,即报错//80章博客也有说明过
我们再次的重启项目,访问localhost:8004/course/getAllCourse,查看redis,若有,那么操作成功
然后看打印信息,会发现,多次的访问后,只有一个mysql的打印信息出现,即的确操作完成
我们到Course.vue组件里面,找到如下:
created(){this.course = this.$route.params.course; // 从路由中获得参数对象赋值给本组件的参数if(this.course == undefined){ //防止刷新当前页面导致的错误,这里就直接回到首页了this.$router.push("/"); //操作这个方法时,通常不能是到本身的路径,前端会报错误的,自己测试就知道了//因为我们认为他是跳转的意思,但是他不能跳转本身
}// 获取课程详情this.getCourseById();
对应的方法:
// 去课程微服务 获取 某门课的详情getCourseById(){ return this.axios.get("http://localhost:8004/course/getCourseById/"+this.course.id).then((result) => {this.course = result.data;}).catch( (error)=>{this.$message.error("获取课程详情失败!");} );},
我们点击一个课程进去,若有数据了,代表上面的操作都完成
现在我们来创建子项目留言微服务edu-comment-boot(8005):
最终成果:
对应的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.lagou</groupId><artifactId>edu-lagou</artifactId><version>1.0-SNAPSHOT</version></parent><groupId>com.lagou</groupId><artifactId>edu-comment-boot</artifactId><version>0.0.1-SNAPSHOT</version><name>edu-comment-boot</name><description>edu-comment-boot</description><properties><java.version>11</java.version></properties><dependencies><!-- web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- eureka客户端 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!-- mybatis plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version></dependency><!-- mysql --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--pojo持久化使用--><dependency><groupId>javax.persistence</groupId><artifactId>javax.persistence-api</artifactId><version>2.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
将配置文件修改成yml后缀,且内容如下:
server:port: 8005
spring:application:name: edu-comment-bootdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.164.128:3306/edu_comment?useUnicode=true&characterEncoding=utf8&serverTimezone=UTCusername: rootpassword: QiDian@666
eureka:client:service-url:defaultZone: http://localhost:7001/eureka/register-with-eureka: truefetch-registry: trueinstance:prefer-ip-address: trueinstance-id: ${spring.cloud.client.ip-address}:${server.port}
对应的启动类:
package com.lagou;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication
@EnableEurekaClient // 注册到中心的客户端
@MapperScan("com.lagou.mapper") // 扫描mapper包
public class EduCommentBootApplication {public static void main(String[] args) {SpringApplication.run(EduCommentBootApplication.class, args);}}
在启动类当前的包下创建entity包,并在该包下创建如下实体类:
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;
import java.util.List;/*** 课程留言表(CourseComment)实体类*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseComment implements Serializable {private static final long serialVersionUID = -11641570368573216L;//一条留言对应多个点赞private List<CourseCommentFavoriteRecord> favoriteRecords;/*** 主键*/private Object id;/*** 课程id*/private Integer courseId;/*** 章节id*/private Integer sectionId;/*** 课时id*/private Integer lessonId;/*** 用户id*/private Integer userId;/*** 运营设置用户昵称*/private String userName;/*** 父级评论id*/private Integer parentId;/*** 是否置顶:0不置顶,1置顶*/private Integer isTop;/*** 评论*/private String comment;/*** 点赞数*/private Integer likeCount;/*** 是否回复留言:0普通留言,1回复留言*/private Integer isReply;/*** 留言类型:0用户留言,1讲师留言,2运营马甲 3讲师回复 4小编回复 5官方客服回复*/private Integer type;/*** 留言状态:0待审核,1审核通过,2审核不通过,3已删除*/private Integer status;/*** 创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/private Integer isDel;/*** 最后操作者id*/private Integer lastOperator;/*** 是否发送了通知,1表示未发出,0表示已发出*/private Integer isNotify;/*** 标记归属*/private Integer markBelong;/*** 回复状态 0 未回复 1 已回复*/private Integer replied;}
package com.lagou.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;/*** 课程留言点赞表(CourseCommentFavoriteRecord)实体类*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseCommentFavoriteRecord implements Serializable {private static final long serialVersionUID = 159062001487532233L;/*** 用户评论点赞j记录ID*/private Integer id;/*** 用户ID*/private Integer userId;/*** 用户评论ID*/private Integer commentId;/*** 是否删除,0:未删除(已赞),1:已删除(取消赞状态)*/private Integer isDel;/*** 创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;}
创建mapper.CourseCommentDao接口:
package com.lagou.dao;import com.lagou.entity.CourseComment;
import org.apache.ibatis.annotations.Param;import java.util.List;/****/
public interface CourseCommentDao {//注意:后面的方法,只是保留,使用到了可以操作,没有使用到,保留也行/*** 保存留言* @param courseComment 留言内容对象* @return 受影响的行数*/Integer saveComment(CourseComment courseComment);/*** 某个课程的全部留言(分页)* @param courseId 课程编号* @param offset 起始位置,或者说数据偏移* @param pagesize 每页的条数* @return 留言集合*/List<CourseComment> getCommentsByCourseId(@Param("courseId") Integer courseId,@Param("offset") Integer offset,@Param("pagesize") Integer pagesize);/*** 查看某个用户的某条留言是否点过赞* @param cid 对应用户的对应的评论id* @param uid 用户编号* @return 0:没点过赞,1:点过赞(一般只会返回1或者0,因为用户对同一个留言一般只能点赞一次,或者取消)* 即基本只有点赞和没有点赞这两个状态*/Integer existsFavorite(@Param("cid") Integer cid,@Param("uid") Integer uid);/*** 没有点过赞的,则保存点赞信息* @param comment_id 对应用户的对应的评论id* @param user_id 用户编号* @return 0:保存失败,1:保存成功,也可以说是受影响的行数*/Integer saveCommentFavorite(@Param("comment_id") Integer comment_id,@Param("user_id") Integer user_id);/**** @param is_del 状态改变,0:点赞,1:未点赞* @param comment_id 对应用户的对应的评论id* @param user_id 用户编号* @return 受影响的行数*/Integer updateFavoriteStatus(@Param("is_del") Integer is_del,@Param("comment_id") Integer comment_id,@Param("user_id") Integer user_id);/**** @param comment_id 对应用户的对应的评论id* @param user_id 用户编号* @return 点赞状态,0:点赞,1:未点赞*/Integer FavoriteStatus(@Param("comment_id") Integer comment_id,@Param("user_id") Integer user_id);/*** 更新点赞的数量,当然这里我进行统一处理了,所以还有其他作用* @param like_count 点赞则加1,取消赞则减一* @param comment_id 对应用户的对应的评论id* @return 受影响的行数*/Integer updateLikeCount(@Param("like_count") Integer like_count,@Param("comment_id") Integer comment_id);}
创建service.CommentService接口及其实现类:
package com.lagou.service;import com.lagou.entity.CourseComment;
import org.apache.ibatis.annotations.Param;import java.util.List;/****/
public interface CommentService {/*** 保存留言** @param courseComment 留言内容对象* @return 受影响的行数*/Integer saveComment(CourseComment courseComment);/*** 某个课程的全部留言(分页)** @param courseId 课程编号* @param offset 起始位置,或者说数据偏移* @param pagesize 每页的条数* @return 留言集合*/List<CourseComment> getCommentsByCourseId(@Param("courseId") Integer courseId, @Param("offset") Integer offset, @Param("pagesize") Integer pagesize);/*** 没有点过赞的,则保存点赞信息,当然这里我进行统一处理了,所以还有其他作用** @param comment_id 对应用户的对应的评论id* @param user_id 用户编号* @return 对应用户的对应的评论id*/Integer saveFavorite(Integer comment_id, Integer user_id);
}
package com.lagou.service.impl;import com.lagou.entity.CourseComment;
import com.lagou.mapper.CourseCommentDao;
import com.lagou.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;/****/
@Service
public class CommentServiceImpl implements CommentService {@Autowiredprivate CourseCommentDao courseCommentDao;@Overridepublic Integer saveComment(CourseComment courseComment) {Integer integer = courseCommentDao.saveComment(courseComment);return integer;}@Overridepublic List<CourseComment> getCommentsByCourseId(Integer courseId, Integer offset, Integer pagesize) {List<CourseComment> commentsByCourseId = courseCommentDao.getCommentsByCourseId(courseId, offset, pagesize);return commentsByCourseId;}/*先查看当前用户对这条留言是否点过赞如果点过,修改对应的字段is_del状态即可,0表示取消赞,1表示已点赞没点过,保存对应点赞的信息,一般都是进行点赞,也就是is_del为1(因为第一次)每次的点赞和取消赞,对应的评论总赞数要进行添加和减少*/@Override@Transactionalpublic Integer saveFavorite(Integer comment_id, Integer user_id) {Integer i = courseCommentDao.existsFavorite(comment_id, user_id);int i1 = 0;int i2 = 0;if(i == 0){ //没点过赞i1 = courseCommentDao.saveCommentFavorite(comment_id, user_id);i2 = courseCommentDao.updateLikeCount(1,comment_id);}else{Integer is_del = courseCommentDao.FavoriteStatus(comment_id,user_id);is_del = (is_del==0?1:0);//进行取反,即原来是点过赞的,变成没有点赞,没有点赞的,变成点过赞//修改赞的状态i1 = courseCommentDao.updateFavoriteStatus(is_del,comment_id,user_id);if(is_del == 1){i2 = courseCommentDao.updateLikeCount(-1,comment_id);}if(is_del == 0){i2 = courseCommentDao.updateLikeCount(1,comment_id);}}if(i1 == 0 || i2 == 0){ //只要有一个是0,那么就有一个是失败的throw new RuntimeException("点赞失败");}return comment_id;}}
创建controller.CommentController类:
package com.lagou.controller;import com.lagou.entity.CourseComment;
import com.lagou.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.io.UnsupportedEncodingException;
import java.util.List;/****/
@RestController
@RequestMapping("course")
@CrossOrigin
public class CommentController {@Autowiredprivate CommentService commentService;@GetMapping("comment/saveCourseComment")public Object saveCourseComment(Integer courseid,Integer userid,String username,String comment) throws UnsupportedEncodingException {System.out.println(username);System.out.println(comment);System.out.println(new String(username.getBytes("ISO-8859-1")));System.out.println(new String(comment.getBytes("ISO-8859-1")));username = new String(username.getBytes("ISO-8859-1"),"UTF-8");comment = new String(comment.getBytes("ISO-8859-1"),"UTF-8");//解决get情况中,传递过来的乱码问题,因为get默认是ISO-8859-1的操作//我们也使用ISO-8859-1进行变成对应的中文//然后设置操作UTF-8,虽然并不需要设置(一般是默认的,所以可以不添加)CourseComment courseComment = new CourseComment();courseComment.setCourseId(courseid); //课程编号//这里写为0,因为我们的留言只操作对应的课程,章节和小节并没有操作//所以这样的操作,我们称为预留字段//即在后面的版本中(如更新版本)进行执行,现在默认写为0,虽然写其他的也可courseComment.setSectionId(0); //章节编号,预留字段courseComment.setLessonId(0); //小节编号,预留字段courseComment.setUserId(userid); //用户编号courseComment.setUserName(username); //用户昵称courseComment.setParentId(0); //没有父id,预留字段courseComment.setComment(comment); //留言内容courseComment.setType(0); //0:用户留言,预留字段courseComment.setLastOperator(courseid); //最后操作的用户编号Integer integer = commentService.saveComment(courseComment);return integer;}@GetMapping("comment/getCourseCommentList/{courseId}/{pageIndex}/{pageSize}")public List<CourseComment> getCommentsByCourseId(@PathVariable("courseId") Integer courseId, @PathVariable("pageIndex")Integer pageIndex, @PathVariable("pageSize")Integer pageSize) {int pagesize = pageSize; //每页条数int pageindex = pageIndex; //页码List<CourseComment> commentsByCourseId = commentService.getCommentsByCourseId(courseId, (pageindex-1)*20, pagesize);return commentsByCourseId;}@GetMapping("comment/Favorite/{commentid}/{userid}")public Integer Favorite(@PathVariable("commentid") Integer commentid,@PathVariable("userid")Integer userid) {Integer integer = commentService.saveFavorite(commentid, userid);return integer;}
}
如果没有什么报错,那么初步完成
现在我们改变CourseCommentDao接口的部分地方:
public interface CourseCommentDao extends BaseMapper<CourseComment> {//中间删除了如下//Integer saveComment(CourseComment courseComment);
再修改CommentServiceImpl类的部分方法:
@Overridepublic Integer saveComment(CourseComment courseComment) {Integer integer = courseCommentDao.insert(courseComment);return integer;}
修改CommentController类的部分地方:
@RequestMapping("comment")@GetMapping("saveCourseComment")public Object saveCourseComment(Integer courseid,Integer userid,String username,String comment) throws UnsupportedEncodingException {@GetMapping("getCourseCommentList/{courseId}/{pageIndex}/{pageSize}")public List<CourseComment> getCommentsByCourseId(@PathVariable("courseId") Integer courseId, @PathVariable("pageIndex")Integer pageIndex, @PathVariable("pageSize")Integer pageSize) {@GetMapping("Favorite/{commentid}/{userid}")public Integer Favorite(@PathVariable("commentid") Integer commentid,@PathVariable("userid")Integer userid) {
然后启动该项目,访问localhost:8005/comment/saveCourseComment?courseid=1&userid=2&username=aa&comment=hello
执行后,查看数据库,若有数据,代表操作成功,因为表的原因,可能主键的起始自增不是默认的设置的1,通常有设置的起始值
但是这里需要注意,程序的添加可能不会操作自增(手动基本都会)
这是因为mp(Mybatis-Plus)的原因,所以这里需要注意,但也基本不会相同
且对应的创建时间和更新时间自带了对应的时间(前面也说明过了,即CURRENT_TIMESTAMP)
但是我们在操作中文时,即访问localhost:8005/comment/saveCourseComment?courseid=1&userid=2&username=嘿嘿&comment=hello
会发现出现了乱码,为什么明明操作了编码还是会乱码呢,我们看打印信息,会发现,实际上他并没有是乱码的进入
也就是说,他本来是UTF-8的类型,但是因为我们将使用ISO-8859-1解码后,那么数据就是乱码
我们再将乱码进行编码,自然是得不到对应的值的,那么为什么这里传递后是没有乱码的呢
主要是服务器的版本原因,具体可以看看86章博客的内容,有具体说明,说过一般tomcat8及其以后就不需要了
而Spring Boot本来就能是中文,相当于tomcat8及其以后,所以这里就是中文
所以我们修改对应的CommentController类里面的方法:
username = new String(username.getBytes("ISO-8859-1"),"UTF-8");comment = new String(comment.getBytes("ISO-8859-1"),"UTF-8");//我们将这两个删除即可
删除后,现在我们继续访问localhost:8005/comment/saveCourseComment?courseid=1&userid=2&username=嘿嘿&comment=hello
会发现没有乱码了,至此操作成功
现在为了有初始数据,我们执行如下(记得到对应的表里面去):
INSERT INTO course_comment VALUES(452,7,0,0,100030017,'天高云淡',0,0,'中国万岁!',1,0,0,0,SYSDATE(),SYSDATE(),0,100030017,1,0,0);
INSERT INTO course_comment VALUES(453,7,8,10,100030011,'Angier',0,0,'强烈推荐!',1,0,0,0,SYSDATE(),SYSDATE(),0,100030011,1,0,0);
INSERT INTO course_comment VALUES(454,7,8,10,100030011,'Angier',0,0,'very good?!',2,0,0,0,SYSDATE(),SYSDATE(),0,100030011,1,0,0);
INSERT INTO course_comment VALUES(455,7,8,10,100030011,'Angier',0,0,'old tie...!',1,0,0,0,SYSDATE(),SYSDATE(),0,100030011,1,0,0);
访问SELECT * FROM course_comment WHERE course_id = 7,出现了四个数据,代表操作成功
也可也SELECT * FROM edu_comment.course_comment WHERE course_id = 7,指定数据库
那么就可以不用必须在对应的数据库里面了,其他数据库里也可也执行,否则会报错(即没有该表的错误)
我们修改CourseCommentDao接口的部分方法,修改如下:
@Select({" SELECT\n" +" cc.*,\n" +" ccfr.id ccfr_id,ccfr.user_id ccfr_user_id,comment_id,ccfr.is_del ccfr_is_del,ccfr.create_time ccfr_create_time,ccfr.update_time ccfr_update_time\n" +" FROM course_comment cc LEFT JOIN (select * from course_comment_favorite_record where is_del =0) ccfr ON cc.id = ccfr.`comment_id`\n" +" WHERE cc.is_del = 0\n" +" AND course_id = #{courseId}\n" +" ORDER BY is_top DESC,like_count DESC,cc.create_time DESC\n" +" LIMIT #{offset},#{pagesize}"})List<CourseComment> getCommentsByCourseId(@Param("courseId") Integer courseId,@Param("offset") Integer offset,@Param("pagesize") Integer pagesize);
重启项目,访问localhost:8005/comment/getCourseCommentList/7/1/2,若有数据,代表操作成功
再次的修改CourseCommentDao接口的部分方法:
@Select({"SELECT\n" +" id,`course_id`,`section_id`,`lesson_id`,user_id,`user_name`,`parent_id`,`is_top`,`comment`,`like_count`,`is_reply`,`type`,`status`,create_time ,update_time ,is_del,`last_operator`,`is_notify`,`mark_belong`,`replied` \n" +" FROM course_comment \n" +" WHERE is_del = 0\n" +" AND course_id = #{courseId}\n" +" ORDER BY is_top DESC , like_count DESC , create_time DESC\n" +" LIMIT #{offset}, #{pageSize}"})List<CourseComment> getCommentsByCourseId(@Param("courseId") Integer courseId,@Param("offset") Integer offset,@Param("pageSize") Integer pagesize);
重启项目,进行访问localhost:8005/comment/getCourseCommentList/7/1/2,若有数据,则代表操作成功
为什么要这样修改呢,因为我们并不操作关联查询,即只操作单表(这里博客是这样的操作,前面我已经将优缺点说明了)
现在我们再次在mapper包下创建CourseCommentFavoriteRecordDao接口:
package com.lagou.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.CourseCommentFavoriteRecord;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Service;import java.util.List;@Service
public interface CourseCommentFavoriteRecordDao extends BaseMapper<CourseCommentFavoriteRecord> {@Select({"SELECT * FROM course_comment_favorite_record WHERE comment_id = #{commnet_id} and is_del = 0"})List<CourseCommentFavoriteRecord> getFavorites(Integer commnet_id);
}
为了进行关联,我们再次的进行修改CourseCommentDao接口的部分方法:
@Select({"SELECT\n" +" id,`course_id`,`section_id`,`lesson_id`,user_id,`user_name`,`parent_id`,`is_top`,`comment`,`like_count`,`is_reply`,`type`,`status`,create_time ,update_time ,is_del,`last_operator`,`is_notify`,`mark_belong`,`replied` \n" +" FROM course_comment \n" +" WHERE is_del = 0\n" +" AND course_id = #{courseId}\n" +" ORDER BY is_top DESC , like_count DESC , create_time DESC\n" +" LIMIT #{offset}, #{pageSize}"})@Results({@Result(column = "id",property = "id"),@Result(column = "id" , property = "favoriteRecords", many = @Many(select = "com.lagou.mapper.CourseCommentFavoriteRecordDao.getFavorites"))})List<CourseComment> getCommentsByCourseId(@Param("courseId") Integer courseId,@Param("offset") Integer offset,@Param("pageSize") Integer pagesize);//注意:上面的@Result(column = "id",property = "id"),必须要加,否则id的值是null
//无论是注解的形式,还是xml的形式,通常来说,如果不加任何操作,默认是自动转换过去的,但是如果使用了
//那么就不会操作自动转换了,由于上面不加这个的话,因为使用的id(用来关联),所以他不会设置值
//除非你手动的添加,因为虽然不会操作自动转换,但是手动的添加的会操作,且手动的存在,会使得不会操作自动
//所以这里需要加上@Result(column = "id",property = "id"),
然后重启项目,访问localhost:8005/comment/getCourseCommentList/7/1/2,若有数据
代表操作成功(注意看看对应的属性是否有数据,有则代表操作成功,只要不是null即可)
现在我们回到前端,找到Course.vue组件,然后找到如下:
// 获取本课程的全部留言getComment(){return this.axios.get("http://localhost:8005/comment/getCourseCommentList/"+this.course.id+"/1/20").then((result) => {this.commentList = result.data;console.log("获取留言:");console.log(this.commentList);}).catch( (error)=>{this.$message.error("获取留言信息失败!");} );},
现在我们点击前端中的id为7的课程,注意:要id为7,这样才可以查询到留言的,否则就是空的
即我们就看不到数据,因为只有四条数据,且都是id为7的课程
首先我们先找到如下:
created(){// 计算多少节课要讲// let x = 0;// for(let i = 0; i< this.course.courseSections.length; i++){// alert(2)// let section = this.course.courseSections[i]; //每一章// for( let j = 0; j<section.courseLessons.length ; j++){// x++;// }// }// this.totalLessons = x;
}//全部注释掉,然后放在这里// 去课程微服务 获取 某门课的详情getCourseById(){ return this.axios.get("http://localhost:8004/course/getCourseById/"+this.course.id).then((result) => {this.course = result.data;console.log(909)console.log(this.course);// 计算多少节课要讲let x = 0;for(let i = 0; i< this.course.courseSections.length; i++){let section = this.course.courseSections[i]; //每一章for( let j = 0; j<section.courseLessons.length ; j++){x++;}}this.totalLessons = x;}).catch( (error)=>{this.$message.error("获取课程详情失败!");} );},//因为前面的会比这里先执行的,所以得到的就是null,即需要放在getCourseById方法里面
现在我们点击id为7的课程,往下滑,可以发现,出现留言了
现在我们修改CourseComment的部分属性:
@TableId(type = IdType.AUTO)private Integer id; //注意,如果自增的值很大,超过了Integer的值,可以使用Long类型
//但也要注意:如果非常大,可能操作查询时,会报错(查到了对应的超大数,由于不会默认变成对应的类型,通常都认为是int类型,所以可能到那个时候会报错,这里注意即可,因为这是mybatis的设置的原因)
以及CourseCommentFavoriteRecord的部分属性:
@TableId(type = IdType.AUTO)private Integer id;
这样来保证自增,当然类型也可也不是Integer,因为不会加上该字段,只是为了更好的区分,或者后续的维护而已
重启项目,访问localhost:8005/comment/saveCourseComment?courseid=1&userid=2&username=嘿嘿&comment=hello
看看结果是不是之前说的程序的没有操作自增,会发现,操作了自增,即这里操作完毕
现在找到前端这个部分:
// 发表留言saveComment(){return this.axios.get("http://localhost:8005/comment/saveCourseComment",{params:{courseid:this.course.id,userid:this.userid,username:this.user.nickname,comment:this.comment,}}).then((result) => {// console.log(result);// 重新获取本门课的全部留言信息this.getComment();}).catch( (error)=>{this.$message.error("发表留言失败!");} ); },
记得修改如下(基本上需要用到user的都需要修改,即操作token,前端以及操作好的):
//从cookie中获取tokengetCookie(key){var name = key + "=";if(document.cookie.indexOf(';') > 0){var ca = document.cookie.split(';');for(var i=0; i<ca.length; i++) {var c = ca[i].trim();if (c.indexOf(name)==0) { return c.substring(name.length,c.length); }}}else{var ca = document.cookie//console.log(ca)if (ca.indexOf(name)==0) { console.log(9)console.log(ca.substring(name.length,ca.length))return ca.substring(name.length,ca.length); }}// return "";},
来保证得到对应的user,由于Index.vue和Header.vue是一起的,所以他们其中一个可以注释掉对应的操作token的代码
虽然我并没有操作,通常是Index.vue先操作,因为他是主,然后才会操作其他组件的内容
我之所以没有注释,是因为我需要对应的user信息,所以再次的得到
然后我们操作发表留言,可以看到,对应出现了新的留言了
但是并没有清除原来留言的内容,所以我们需要修改如下:
// 发表留言saveComment(){console.log(777)console.log(this.user)return this.axios.get("http://localhost:8005/comment/saveCourseComment",{params:{courseid:this.course.id,userid:this.userid,username:this.user.nickname,comment:this.comment,}}).then((result) => {// console.log(result);// 重新获取本门课的全部留言信息this.comment= null; //加上了这个,进行初始化this.getComment();}).catch( (error)=>{this.$message.error("发表留言失败!");} ); },
至此,我们就操作完毕
现在我们进行大改变
首先是CourseCommentDao接口:
package com.lagou.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.CourseComment;
import org.apache.ibatis.annotations.*;import java.util.List;/****/
public interface CourseCommentDao extends BaseMapper<CourseComment> {/*** 某个课程的全部留言(分页)* @param courseId 课程编号* @param offset 起始位置,或者说数据偏移* @param pagesize 每页的条数* @return 留言集合*/@Select({"SELECT\n" +" id,`course_id`,`section_id`,`lesson_id`,user_id,`user_name`,`parent_id`,`is_top`,`comment`,`like_count`,`is_reply`,`type`,`status`,create_time ,update_time ,is_del,`last_operator`,`is_notify`,`mark_belong`,`replied` \n" +" FROM course_comment \n" +" WHERE is_del = 0\n" +" AND course_id = #{courseId}\n" +" ORDER BY is_top DESC , like_count DESC , create_time DESC\n" +" LIMIT #{offset}, #{pageSize}"})@Results({@Result(column = "id",property = "id"),@Result(column = "id" , property = "favoriteRecords", many = @Many(select = "com.lagou.mapper.CourseCommentFavoriteRecordDao.getFavorites"))})List<CourseComment> getCommentsByCourseId(@Param("courseId") Integer courseId,@Param("offset") Integer offset,@Param("pageSize") Integer pagesize);/*** 更新点赞的数量* @param like_count 点赞则加1,取消赞则减一* @param comment_id 对应用户的对应的评论id* @return 受影响的行数*/Integer updateLikeCount(@Param("like_count") Integer like_count,@Param("comment_id") Integer comment_id);}
对应的CommentService接口及其实现类:
package com.lagou.service;import com.lagou.entity.CourseComment;
import org.apache.ibatis.annotations.Param;import java.util.List;/****/
public interface CommentService {/*** 保存留言* @param comment 留言内容对象* @return 受影响的行数*/Integer saveComment(CourseComment comment);/*** 某个课程的全部留言(分页)* @param courseid 课程编号* @param offset 数据偏移* @param pageSize 每页条目数* @return 留言集合*/List<CourseComment> getCommentsByCourseId(@Param("courseid")Integer courseid, @Param("offset")Integer offset, @Param("pageSize")Integer pageSize);/*** 点赞* @param comment_id 留言编号* @param userid 用户编号* @return 0:保存失败,1:保存成功*/Integer saveFavorite(Integer comment_id,Integer userid);/*** 取消赞* @param comment_id 留言编号* @param userid 用户编号* @return 0:保存失败,1:保存成功*/Integer cancelFavorite(Integer comment_id,Integer userid);
}
package com.lagou.service.impl;import com.lagou.entity.CourseComment;
import com.lagou.mapper.CourseCommentDao;
import com.lagou.mapper.CourseCommentFavoriteRecordDao;
import com.lagou.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;/****/
@Service
public class CommentServiceImpl implements CommentService {@Autowiredprivate CourseCommentDao courseCommentDao;@Autowiredprivate CourseCommentFavoriteRecordDao courseCommentFavoriteRecordDao;@Overridepublic Integer saveComment(CourseComment comment) {return courseCommentDao.insert(comment);}@Overridepublic List<CourseComment> getCommentsByCourseId(Integer courseid, Integer offset, Integer pageSize) {return courseCommentDao.getCommentsByCourseId(courseid, offset, pageSize);}@Overridepublic Integer saveFavorite(Integer comment_id, Integer userid) {return null;}@Overridepublic Integer cancelFavorite(Integer comment_id, Integer userid) {return null;}}
对应的CommentController类:
package com.lagou.controller;import com.lagou.entity.CourseComment;
import com.lagou.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.io.UnsupportedEncodingException;
import java.util.List;/****/
@RestController
@RequestMapping("comment")
@CrossOrigin
public class CommentController {@Autowiredprivate CommentService commentService;@GetMapping("saveCourseComment")public Object saveCourseComment(Integer courseid,Integer userid,String username,String comment) throws UnsupportedEncodingException {System.out.println(username);System.out.println(comment);System.out.println(new String(username.getBytes("ISO-8859-1"),"UTF-8"));System.out.println(new String(comment.getBytes("ISO-8859-1")));// username = new String(username.getBytes("ISO-8859-1"),"UTF-8");// comment = new String(comment.getBytes("ISO-8859-1"),"UTF-8");//解决get情况中,传递过来的乱码问题,因为get默认是ISO-8859-1的操作,我们也使用ISO-8859-1进行变成对应的中文//然后设置操作UTF-8,虽然并不需要设置CourseComment courseComment = new CourseComment();courseComment.setCourseId(courseid); //课程编号//这里写为0,因为我们的留言只操作对应的课程,章节和小节并没有操作//所以这样的操作,我们称为预留字段//即在后面的版本中(如更新版本)进行执行,现在默认写为0,虽然写其他的也可courseComment.setSectionId(0); //章节编号,预留字段courseComment.setLessonId(0); //小节编号,预留字段courseComment.setUserId(userid); //用户编号courseComment.setUserName(username); //用户昵称courseComment.setParentId(0); //没有父id,预留字段courseComment.setComment(comment); //留言内容courseComment.setType(0); //0:用户留言,预留字段courseComment.setLastOperator(courseid); //最后操作的用户编号Integer integer = commentService.saveComment(courseComment);return integer;}@GetMapping("getCourseCommentList/{courseId}/{pageIndex}/{pageSize}")public List<CourseComment> getCommentsByCourseId(@PathVariable("courseId") Integer courseId, @PathVariable("pageIndex")Integer pageIndex, @PathVariable("pageSize")Integer pageSize) {int pagesize = pageSize; //每页条数int pageindex = pageIndex; //页码List<CourseComment> commentsByCourseId = commentService.getCommentsByCourseId(courseId, (pageindex-1)*20, pagesize);return commentsByCourseId;}@GetMapping("saveFavorite/{commentid}/{userid}")public Integer saveFavorite(@PathVariable("commentid") Integer commentid, @PathVariable("userid") Integer userid){Integer integer = commentService.saveFavorite(commentid, userid);return integer;}@GetMapping("cancelFavorite/{commentid}/{userid}")public Integer cancelFavorite(@PathVariable("commentid") Integer commentid,@PathVariable("userid") Integer userid){Integer integer = commentService.cancelFavorite(commentid, userid);return integer;}
}
好了,进行了一系列改变,实际上原来的查询留言和添加留言并没有进行改变
你可以重启项目访问如下:
localhost:8005/comment/getCourseCommentList/7/1/20
localhost:8005/comment/saveCourseComment?courseid=1&userid=2&username=嘿嘿&comment=hello
若都有数据或者添加了数据,那么改造完成,也可也在前端页面进行测试
好了,现在我们修改CommentServiceImpl实现类的saveFavorite方法,内容如下:
@Overridepublic Integer saveFavorite(Integer comment_id, Integer userid) {QueryWrapper<CourseCommentFavoriteRecord> qw = new QueryWrapper<>();qw.eq("comment_id", comment_id);qw.eq("user_id", userid);Integer i = courseCommentFavoriteRecordDao.selectCount(qw);int i1 = 0;int i2 = 0;CourseCommentFavoriteRecord favorite = new CourseCommentFavoriteRecord();favorite.setIsDel(0);if (i == 0) { //没点过赞// 添加赞的信息favorite.setCommentId(comment_id);favorite.setUserId(userid);favorite.setCreateTime(new Date());favorite.setUpdateTime(new Date());i1 = courseCommentFavoriteRecordDao.insert(favorite);} else {i1 = courseCommentFavoriteRecordDao.update(favorite,qw);}i2 = courseCommentDao.updateLikeCount(1, comment_id);if (i1 == 0 || i2 == 0) {throw new RuntimeException("点赞失败!");}return comment_id;}
现在,我们编写CourseCommentDao接口的updateLikeCount方法,我们直接加注解即可,内容如下:
@Update({"update course_comment set like_count = like_count + #{like_count} where id = #{comment_id}"})Integer updateLikeCount(@Param("like_count") Integer like_count,@Param("comment_id") Integer comment_id);
然后,我们再次的编写CommentServiceImpl实现类的cancelFavorite方法,内容如下:
@Overridepublic Integer cancelFavorite(Integer comment_id, Integer userid) {QueryWrapper<CourseCommentFavoriteRecord> qw = new QueryWrapper<>();qw.eq("comment_id", comment_id);qw.eq("user_id", userid);CourseCommentFavoriteRecord favorite = new CourseCommentFavoriteRecord();favorite.setIsDel(1); // 1 表示该赞被取消Integer i1 = courseCommentFavoriteRecordDao.update(favorite, qw);Integer i2 = courseCommentDao.updateLikeCount(-1, comment_id);if (i1 == 0 || i2 == 0) {throw new RuntimeException("取消赞失败!");}return i2;}
至此,我们编写完成,现在我们回到Course.vue组件,找到如下:
// 点赞zan(comment){ return this.axios.get("http://localhost:8005/comment/saveFavorite/"+comment.id+"/"+this.userid).then((result) => {// console.log(result);// 重新获取本门课的全部留言信息this.getComment();}).catch( (error)=>{this.$message.error("点赞失败!");} );},// 取消赞cancelzan(comment){return this.axios.get("http://localhost:8005/comment/cancelFavorite/"+comment.id+"/"+this.userid).then((result) => {// console.log(result);// 重新获取本门课的全部留言信息this.getComment();}).catch( (error)=>{this.$message.error("取消赞失败!");} );},
重启项目,在前端进行测试,或者访问如下:
http://localhost:8005/comment/saveFavorite/452/100030011
然后查看数据库的是否增加了值(或者多出了一条数据,或者是否改变字段is_del的值为0)
http://localhost:8005/comment/cancelFavorite/452/100030011,然后查看数据库是否减少了值(或者是否改变字段为1)
若操作成功,那么就没有问题,至此点赞操作完成,当然了,因为前端操作了判断,所以基本不会执行相同的,而这里可以
使得更新相同的,但点赞数量增加了
但是,我们可以发现一个问题,上面虽然手动操作了异常,但是他还是始终是多个操作的,也就是说,如果前面的执行成功
后面的执行失败,那么虽然报错了,但是还是进行了操作,比如修改如下:
@Overridepublic Integer saveFavorite(Integer comment_id, Integer userid) {QueryWrapper<CourseCommentFavoriteRecord> qw = new QueryWrapper<>();qw.eq("comment_id", comment_id);qw.eq("user_id", userid);Integer i = courseCommentFavoriteRecordDao.selectCount(qw);int i1 = 0;int i2 = 0;CourseCommentFavoriteRecord favorite = new CourseCommentFavoriteRecord();favorite.setIsDel(0);if (i == 0) { //没点过赞// 添加赞的信息favorite.setCommentId(comment_id);favorite.setUserId(userid);favorite.setCreateTime(new Date());favorite.setUpdateTime(new Date());i1 = courseCommentFavoriteRecordDao.insert(favorite);} else {i1 = courseCommentFavoriteRecordDao.update(favorite,qw);}//我们让前面执行成功,这里执行失败,当然,下面的==0,是防止出现数据没有的//加上并没有什么坏处//虽然基本不会出现(因为保存基本不会返回0,基本只有直接的失败,自然后面的不会执行)//执行失败的操作int ii = 1/0;i2 = courseCommentDao.updateLikeCount(1, comment_id);if (i1 == 0 || i2 == 0) {throw new RuntimeException("点赞失败!");}return comment_id;}
重启项目,然后将对应数据库的点赞信息都删除,然后访问http://localhost:8005/comment/saveFavorite/452/100030011
查看数据库变化,会发现,我们添加了一条数据,且对应字段is_del的值为0
但是对应的条数并没有改变,也就是说出现了数据的错误
所以我们需要事务操作,当然,事务的操作有很多
比如我们使用@Transactional(在mp依赖中,有对应的spring-tx依赖,所以可以使用,mp依赖也就是mybatis-plus的对应依赖,这里就是mybatis-plus-boot-starter依赖)
内容如下:
@Override@Transactional()public Integer saveFavorite(Integer comment_id, Integer userid) {QueryWrapper<CourseCommentFavoriteRecord> qw = new QueryWrapper<>();qw.eq("comment_id", comment_id);qw.eq("user_id", userid);Integer i = courseCommentFavoriteRecordDao.selectCount(qw);int i1 = 0;int i2 = 0;CourseCommentFavoriteRecord favorite = new CourseCommentFavoriteRecord();favorite.setIsDel(0);if (i == 0) { //没点过赞// 添加赞的信息favorite.setCommentId(comment_id);favorite.setUserId(userid);favorite.setCreateTime(new Date());favorite.setUpdateTime(new Date());i1 = courseCommentFavoriteRecordDao.insert(favorite);} else {i1 = courseCommentFavoriteRecordDao.update(favorite,qw);}int ii = 1/0; i2 = courseCommentDao.updateLikeCount(1, comment_id);if (i1 == 0 || i2 == 0) {throw new RuntimeException("点赞失败!");}return comment_id;}
现在我们再次的访问,会发现,对应的数据没有添加了,即操作了回滚,为什么他操作了呢,实际上我们是需要扫描的
但是Spring Boot会操作Spring之类的扫描,自然不只是普通的扫描包,还有其他扫描方式,比如事务的扫描
所以这里只需要加上@Transactional()即可,括号可以删除,即@Transactional,具体的该注解解释,可以看66章博客
至此,事务操作完毕,将int ii = 1/0; 注释掉吧
实际上你可以手动改变数据库数据,那么可以造成-1的出现,但是这里没有必要的,因为在前端基本是不会出现这样的情况的
这是执行顺序的原因,所以这里我们只要不乱改数据,那么数据就基本没有问题
但是我们要注意,该事务只是针对一个数据库,假设,里面的操作是分开在多个数据库的
也就是说,操作分布式的访问,他这里访问一次,然后其他数据库也访问一次,那么这时就需要分布式的事务了
因为不同数据库之间的事务基本是不会共享的,在后面会说明该问题
这里的操作基本都是在一个数据库里面,所以这里只需要加上@Transactional即可
至此,操作完毕,后面还有后续,但是由于博客字数有限制,就将后续放在98章博客(下一章博客)里了,去该博客继续查看吧
97-微服务项目的编写(上篇)相关推荐
- 98-微服务项目的编写(下篇)
微服务项目的编写 这里是续写97章博客(上一章博客)的,所以若没有看的话,最好看完再来: 接着续写:再创建子项目支付微服务edu-pay-boot(8006): 最终成果: 对应的依赖: <?x ...
- IDEA集成Docker插件实现一键自动打包部署微服务项目
一. 前言 大家在自己玩微服务项目的时候,动辄十几个服务,每次修改逐一部署繁琐不说也会浪费越来越多时间,所以本篇整理通过一次性配置实现一键部署微服务,实现真正所谓的一劳永逸. 二. 配置服务器 1. ...
- 微服务项目的整合与测试
实验目的 掌握微服务项目的整合使用 掌握Swagger-UI的简单使用 练习内容 1.微服务项目整合 1.1.项目预览 1.1.1.在 https://github.com/shi469391tou/ ...
- Docker Compose配置springboot微服务项目
[Docker那些事]系列文章 docker 安装 与 卸载 centos Dockerfile 文件结构.docker镜像构建过程详细介绍 Dockerfile文件中CMD指令与ENTRYPOINT ...
- 微服务项目部署在docker容器运行
昨天的一篇微服务项目中涉及到docker部署,今天写一篇关于微服务项目部署在docker容器中运行,使用github上另外一个比较经典的微服务项目piggyMetric,项目的github地址:htt ...
- k8s部署微服务项目
之前用docker-compose部署微服务项目,但是只能单节点的(那你用微服务架构干啥?),所以想搞一下k8s集群,网上找了下资料没有视频专门讲这一块,自己找了很多资料,搞了蛮长时间的,所以记录一下 ...
- SpringCloud入门总结 + 使用SpringCloud搭建微服务项目
SpringCloud 1.认识微服务 2.认识spring Cloud 3.Spring Cloud Eureka 服务发现框架 3.1认识Eureka 3.2 实战--开发并部署Eureka Se ...
- 微服务项目--商城管理系统的整合与测试
一.微服务项目结构预览 1.商城微服务项目源码:https://github.com/shi469391tou/microservice-mallmanagement.git 项目源码 通过一个名为m ...
- 微服务项目后台技术栈
微服务项目后台相关技术整理 主要技术 ORM框架-Mybatis Plus Mybatis Plus核心功能 MyBatis Plus与SpringBoot集成 MyBatis Plus集成Sprin ...
最新文章
- OpenGL multiple lights多光源的实例
- 2019\Province_C_C++_B\试题C-数列求值
- SQL Server中表锁定的原理及解锁演示
- [pytorch、学习] - 5.7 使用重复元素的网络(VGG)
- 判断大小简单算法_算法浅谈——人人皆知却很多人写不对的二分法
- python列表框_Python列表框
- 吐槽大会4_《吐槽大会4》不愧都是国家队,真吐槽!孙杨霸气喊话霍顿
- MySQL 慌了!这个分库分表方法论,要火了?
- mysql序列 mybatis_MySQL实现序列(Sequence)效果以及在Mybatis中如何使用这种策略
- Redis bind用法
- 浙大python读者验证码_Python实现简单生成验证码功能【基于random模块】
- Linux运维跳槽40道面试精华题
- Apache Flink 在斗鱼的应用与实践
- java表述环形链表_数据结构环形链表(java实现)
- java游戏下载怎么玩_jar的手机游戏怎么玩?java手机游戏的玩法
- 软件测试的艺术读书笔记
- 办公室购买计算机会计分录,购买办公用品属于什么会计科目
- windows php进程数,win10的进程数应该多少?
- 中国最受欢迎50大技术博客评选结果详见
- excel导出图片---HSSFWorkbook--SXSSFWorkbook
热门文章
- python 异常检测算法_吴恩达机器学习中文版笔记:异常检测(Anomaly Detection)
- Docker网络管理
- Hark语音识别学习(二)--HARK数据类型
- 软件开发过程中常见漏洞的解析
- laravel Specified key was too long 解决方案
- 项目管理心得--第一篇
- imageio不存在java,Java自带的ImageIO留下的坑
- Pandas + Pyecharts | ADX游戏广告投放渠道综合分析
- 47、打印二叉树的右视图 和 左视图
- 瑞盟MS2358 96KHz、24bit音频ADC芯片--DFN12 封装