微服务项目的编写
回顾微服务:
这里需要通过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>
< parent>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-parent</ artifactId>
< version> 2.4.2</ version>
< relativePath/>
</ 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.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/>
</ parent>
< groupId> com.lagou</ groupId>
< artifactId> edu-eureka-boot</ artifactId>
< version> 0.0.1-SNAPSHOT</ version>
< name> edu-eureka-boot</ name>
< description> edu-eureka-boot</ description>
< 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 : localhost
client :
service-url :
defaultZone : http: //${ eureka.instance.hostname} : ${ server.port} /eureka/
register-with-eureka : false
fetch-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
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/>
</ 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>
< dependency>
< groupId> org.springframework.cloud</ groupId>
< artifactId> spring-cloud-starter-netflix-eureka-client</ artifactId>
</ dependency>
< dependency>
< groupId> com.baomidou</ groupId>
< artifactId> mybatis-plus-boot-starter</ artifactId>
< version> 3.3.2</ version>
</ dependency>
< dependency>
< groupId> mysql</ groupId>
< artifactId> mysql-connector-java</ artifactId>
< scope> runtime</ scope>
</ dependency>
< 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 ;
import com. lagou. service. AdService ;
public class AdServiceImpl implements AdService {
}
将配置文件修改成yml后缀,并加上如下:
server :
port : 8001
spring :
application :
name : edu- ad- boot
datasource :
driver-class-name : com.mysql.cj.jdbc.Driver
url : jdbc: mysql: //192.168.164.128: 3306/edu_ad? useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username : root
password : QiDian@666
eureka :
client :
service-url :
defaultZone : http: //localhost: 7001/eureka/
register-with-eureka : true
fetch-registry : true
instance :
prefer-ip-address : true
instance-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" )
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 ;
private 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 {
@Autowired
private AdDao adDao;
@Override
public List < PromotionAd > getAdsBySpaceId ( Integer sid) {
QueryWrapper < PromotionAd > qw = new QueryWrapper < > ( ) ;
qw. eq ( "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
public class AdController {
@Autowired
private 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" />
</ 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/>
</ 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>
< dependency>
< groupId> org.springframework.cloud</ groupId>
< artifactId> spring-cloud-starter-netflix-eureka-client</ artifactId>
</ dependency>
< dependency>
< groupId> org.springframework.cloud</ groupId>
< artifactId> spring-cloud-starter-gateway</ artifactId>
</ dependency>
< 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/eureka
register-with-eureka : true
fetch-registry : true
instance :
prefer-ip-address : true
instance-id : ${ spring.cloud.client.ip- address} : ${ server.port}
spring :
application :
name : edu- gateway- boot
cloud :
gateway :
routes :
- id : edu- routes- ad
uri : lb: //edu- ad- boot
predicates :
- Path=/ad/**
filters :
- StripPrefix=1
启动后,访问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(那么剩下的就是第三部分,这里也就是密钥),这一部分通常不会对外公开
所以你也很难猜到,通常来说,该值存在程序里面,无论加密还是解密,程序需要添加他来操作
解密后的值是相同的(加密前的),那么就会通过
但是用户虽然会得到对应的第一部分和第二部分和第三部分,但是第三部分的解密方式却是根据程序来的(除非知道秘钥,或者工作原理,当然,如果知道工作原理,自然可以解密,这里以只知道秘钥为主),秘钥通过算法变成密钥的
注意:第三部分和第二部分,通常多次的生成是不同的(因为不只是根据数据,还有算法,其中第二部分只有部分不同,大体相同,而第三部分基本都不同),但是解密还是可以相同,而其他部分(也就是第一部分了)基本多次的生成是相同的(因为只是根据数据,基本没用算法),这里注意即可,所以即这里的密钥有点特殊,总不能得到的值是相同的吧(虽然是因为算法的原因导致不同,但这是为了更难破解,因为如果只有一个,那么破解后,所有的都会破解了,而多个就难了,因为多个可能是不同的秘钥形成的,因为没有很多的精力,除非总体也只有一个秘钥,不是密钥,秘钥形成密钥的,那么破解后,所有的都会破解了)
所以简单来说,密钥是验证的主体,而正是因为不同,所以可以是多个密钥都可以验证成一个值,即都可以登录上
当然,通常不同的用户自然肯定是不会的(不同秘钥,比如循环,只要有一个验证成功,即可,虽然需要更多的执行空间)
除非是相同的秘钥,虽然相对不安全些(因为一个秘钥知道全部,而不是部分),但实际上还是非常难破解的(需要更加高级的密码学知识)
要注意:JWT只是操作验证的,即他主要用来token,其中第二部分是容易破解的(因为base64又不是什么厉害的加密操作,自然容易破解,要不然对应的也是他的加密的图片又怎么显示呢),或者我们一般需要再次的加密,所以尽量不要利用JWT保存重要数据,当然这里只是说明一下,后面还是使用的,在实际开发中最好还是不要这样,除非你再次的加密,将加密的数据放入第二部分,那么就算第二部分破解,那么对方也是难以获取的
核心代码:
创建子项目,认证微服务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/>
</ 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>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-web</ artifactId>
</ dependency>
< dependency>
< groupId> org.springframework.cloud</ groupId>
< artifactId> spring-cloud-starter-netflix-eureka-client</ artifactId>
</ dependency>
< dependency>
< groupId> com.baomidou</ groupId>
< artifactId> mybatis-plus-boot-starter</ artifactId>
< version> 3.3.2</ version>
</ dependency>
< dependency>
< groupId> mysql</ groupId>
< artifactId> mysql-connector-java</ artifactId>
< scope> runtime</ scope>
</ dependency>
< 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>
< groupId> com.auth0</ groupId>
< artifactId> java-jwt</ artifactId>
< version> 3.8.0</ version>
</ dependency>
< 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
@Table ( name= "user" )
public class User implements Serializable {
private static final long serialVersionUID = - 89788707895046947L ;
@TableId ( type = IdType . AUTO )
private Integer id;
private String name;
private String portrait;
private String phone;
private String password;
private String regIp;
private Object accountNonExpired;
private Object credentialsNonExpired;
private Object accountNonLocked;
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
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令牌就是错误的" ;
}
封装好的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" ;
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" ) ;
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 ;
}
}
public static int isVerify ( String token) {
try {
Algorithm algorithm = Algorithm . HMAC256 ( TOKEN_SECRET ) ;
JWTVerifier verifier = JWT . require ( algorithm) . build ( ) ;
verifier. verify ( token) ;
return 0 ;
} catch ( TokenExpiredException e) {
e. printStackTrace ( ) ;
System . out. println ( "令牌过期" ) ;
return 1 ;
} catch ( JWTDecodeException e) {
e. printStackTrace ( ) ;
System . out. println ( "令牌格式错误!或为空令牌!" ) ;
return 2 ;
} catch ( JWTVerificationException e) {
e. printStackTrace ( ) ;
System . out. println ( "校验失败,token令牌就是错误的" ) ;
return 3 ;
}
}
public static int parseTokenUserid ( String token) {
DecodedJWT jwt = JWT . decode ( token) ;
return jwt. getClaim ( "userid" ) . asInt ( ) ;
}
public static String parseTokenNickname ( String token) {
DecodedJWT jwt = JWT . decode ( token) ;
return jwt. getClaim ( "nickname" ) . asString ( ) ;
}
public static String parseTokenPortrait ( String token) {
DecodedJWT jwt = JWT . decode ( token) ;
return jwt. getClaim ( "portrait" ) . asString ( ) ;
}
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 {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate < Object , Object > redisTemplate;
@Override
public 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) ;
User 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 ) ;
String token = JwtUtil . createToken ( user) ;
dto. setToken ( token) ;
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 {
@Autowired
private 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- boot
datasource :
driver-class-name : com.mysql.cj.jdbc.Driver
url : jdbc: mysql: //192.168.164.128: 3306/edu_user? useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username : root
password : QiDian@666
redis :
host : 192.168.164.128
port : 6379
eureka :
client :
service-url :
defaultZone : http: //localhost: 7001/eureka
register-with-eureka : true
fetch-registry : true
instance :
prefer-ip-address : true
instance-id : ${ spring.cloud.client.ip- address} : ${ server.port}
启动启动类,访问localhost:8003/user/login?phone=110&password=123(这里记得是80),若登录成功,且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 ) {
this . dialogFormVisible = false ;
this . isLogin = true ;
const code = jwtDecode ( result. data. token) ;
this . user = code;
console. log ( this . user ) ;
} 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
那么就可以引入这个了:
import jwtDecode from 'jwt-decode'
< script>
import jwtDecode from 'jwt-decode'
import { setInterval, clearInterval } from 'timers' ;
export default {
接下来我们登录,输入110,以及123密码(账号登录),如果有头像,说明操作完成
接下来在methods里添加如下(如果存在那么就不需要):
setCookie ( key, value, expires ) {
var exp = new Date ( ) ;
exp. setTime ( exp. getTime ( ) + expires* 1000 ) ;
document. cookie = key + "=" + escape ( value) + ";expires=" + exp. toGMTString ( ) ;
} ,
getCookie ( 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
if ( ca. indexOf ( name) == 0 ) {
return ca. substring ( name. length, ca. length) ;
}
}
} ,
对应的方法的注释可以去掉了:
this . setCookie ( "user" , result. data. token, 10 ) ;
然后查看或者添加如下:
created ( ) {
let token = this . getCookie ( "user" ) ;
console. log ( 11 )
console. log ( token)
然后查看控制台,若有数据,代表我们的确是设置了cookie,且可以被得到
等待一会,刷新,发现没有了,因为过期时间只有10秒
然后修改如下:
if ( token != null || token != "" ) {
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 ) ;
实际上如果是多个域名来操作,通常需要给出登录的url地址
因为一般来说对应的登录页面地址通常也对应一个首页,否则不给出地址的话,我怎么知道你要跳到哪个首页呢
接下来我们来删除cookie也就是登出(添加方法,自然是methods里面的):
delCookie ( name ) {
this . setCookie ( name, '' , - 1 ) ;
} ,
通常我们也需要删除redis的token,再AuthorityContoller类里添加如下:
@Autowired
private RedisTemplate < Object , Object > redisTemplate;
@GetMapping ( "logout" )
public void logout ( String token) {
redisTemplate. delete ( token) ;
}
然后修改如下:
logout ( ) {
this . delCookie ( "user" ) ;
this . isLogin = false ;
alert ( '谢谢使用,再见!' ) ;
this . $router. go ( 0 ) ;
this . axios
. get ( "http://localhost:80/user/logout" , {
params : {
token : this . token
}
} )
. then ( ( result ) => {
} )
. catch ( ( error ) => {
} ) ;
} ,
退出登录时,查看redis,若发现的确删除了,那么说明操作成功
微信扫码登录改造(具体为什么要这样实现,可以到87章博客里查看):
在edu-authority-boot认证微服务(80)下添加如下依赖:
< dependency>
< 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;
}
public static String doGet ( String url, Map < String , String > param) {
CloseableHttpClient aDefault = HttpClients . createDefault ( ) ;
String s = null ;
CloseableHttpResponse response = null ;
try {
URIBuilder uriBuilder = new URIBuilder ( url) ;
if ( param!= null ) {
for ( String key: param. keySet ( ) ) {
uriBuilder. addParameter ( key, param. get ( key) ) ;
}
}
URI uri = uriBuilder. build ( ) ;
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) ;
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;
private String refresh_token;
private String openid;
private String scope;
private String unionid;
}
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;
private String province;
private String city;
private String country;
private String headimgurl;
private String privilege;
private String unionid;
}
回到UserService接口,添加如下方法:
Integer register ( String phone, String password, String nickname, String headimg) ;
对应实现类添加的方法:
@Override
public 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 ( ) ;
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) ;
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) ;
Token token = JSON . parseObject ( tokenString, Token . class ) ;
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 ) ;
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 ( ) ) ;
}
response. sendRedirect ( "http://localhost:8080/#/?token=" + dto. getToken ( ) ) ;
return null ;
}
}
然后在前端修改如下:
created ( ) {
let token = this . getValueByUrlParams ( 'token' ) ;
if ( token == null || token == "" ) {
token = this . getCookie ( "user" ) ;
}
this . token = token;
getValueByUrlParams ( paramKey ) {
var url = location. href;
var paraString = url. substring ( url. indexOf ( "?" ) + 1 , url. length) . split ( "&" ) ;
var paraObj = { }
var i, j
for ( 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 ( ) {
this . delCookie ( "user" ) ;
this . isLogin = false ;
alert ( '谢谢使用,再见!' ) ;
window. location. href = "http://localhost:8080/#/"
this . $router. go ( 0 ) ;
至此,我们用微信扫描二维码后(登录那里有个微信图标,点击即可),会发现,数据变化了,变成你微信的头像和名称
至此,我们可以登出或者刷新来测试,会发现,并没有什么问题,即微信登录操作完成
短信验证码登录:
通过第三方短信平台向用户手机发出验证码,用户得到验证码进行登录
第三方平台,我们使用阿里云短信平台,这里使用我的手册"阿里云短信平台.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/,到如下:
所有"未归"(谐音,因为对应的不能写出来,否则发不了博客)的图片地址(从上到下):
链接:https://pan.baidu.com/s/11znFFQ6o2allFEa06Qtswg
提取码:alsk
我们可以到套餐包余量那里可以看到有100条,就是我们之前免费试用的资源包规格:
接下来我们点击国内消息:
再然后点击添加签名,添加如下
注意:下面只是给出一个需要编写的地方,实际上需要一个具体的网站域名(已备案的网站,具体备案流程可以百度)
现在好像并不能搭建具体的个人测试了(需要具体网站),所以从这里开始,后面只需要了解即可
如果你以后有对应的网站了,那么就可以看对应的文档了,或者看一下后面的操作:
点击提交,出现如下:
然后点击添加模板,具体的内容,就不多说了(一般需要审核通过的签名)
接下来,我们认为对应的模板已经编写完成(文档里面的模板)
且都审核通过了(虽然上面没有,因为我并没有对应的备案网站),那么通常需要点击如下:
点击AccessKey管理,然后会出现如下:
点击开始使用子账户,当然,你可以多次的点击AccessKey管理来测试他们的区别
我们点击创建用户,内容如下:
点击确定,你可以使用自己的方式来验证,比如手机号验证
一般他会知道你的手机号的,因为支付宝登录,通常也能得到你的手机号的绑定的
如果没有绑定,一般会提示你去绑定或者跳转绑定)
最后得到如下:
后面还有两个是很重要的东西,这里我就不给出了,可以参照如下内容(不是我的):
然后点击如下(又一个测试的):
打上勾勾,点击添加权限,然后到如下:
输入sms找到对应的权限,点击第一个名称,那么右边就会出现点击的名称,代表已选择
点击确定,然后点击完成,然后回到国内消息这里:
通过模板的CODE和签名名称可以得到一组信息(结合上面),这里给出测试用的信息:
但是我们也可以发现,他定义了签名和模板,他们两个用来影响短信的
而用户的对应的两个值是用来使得给发送的,因为是我们的账号来来发送
因为他申请发送的权限的,当然如果是主账号,自然有权限,而不用设置权限,主账号可以直接创建该两个值即可
既然发送者,和消息都指定好了,那么接收者是谁呢,这就需要参数了,通常来说我们只需要指定电话号码即可
在后面的测试中可以进行操作
注意:阿里为了防止恶意高频发送验证码,加入了流量限制,一般当返回下面的结果就是这个原因(一般是如下,也有可能会发生改变):
{
"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>
< groupId> com.aliyun</ groupId>
< artifactId> aliyun-java-sdk-core</ artifactId>
< version> 4.5.3</ version>
</ dependency>
再在yml文件加上如下:
ali :
sms :
signName : 大佬孙
templateCode : SMS_177536068
assessKeyId : LTAI4FwKDkeZ6StZvRxg5RDf
assessKeySecret : 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 {
@Autowired
private 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;
}
@Autowired
private 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;
@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 ( ) ;
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) ;
request. putQueryParameter ( "SignName" , signName) ;
request. putQueryParameter ( "TemplateCode" , templateCode) ;
String vcode = "" ;
for ( int i = 0 ; i< 6 ; i++ ) {
vcode = vcode + ( int ) ( Math . random ( ) * 9 + 1 ) ;
}
request. putQueryParameter ( "TemplateParam" , "{\"code\":\"" + vcode + "\"}" ) ;
try {
CommonResponse response = client. getCommonResponse ( request) ;
System . out. println ( response. getData ( ) ) ;
String 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) ;
对应的实现类如下:
@Override
public 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) ;
}
System . out. println ( "user = " + user) ;
String token = JwtUtil . createToken ( user) ;
UserDTO dto = new UserDTO ( ) ;
dto. setState ( EduConstant . LOGIN_SUCCESS_CODE ) ;
dto. setMessage ( EduConstant . LOGIN_SUCCESS ) ;
dto. setToken ( token) ;
return dto;
}
然后在对应的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 后重试') : '获取验证码' }}
</ div>
</ el-form-item>
</ el-form>
查看如下:
smsCodeTimeSecond : 0 ,
phoneNumber : null ,
smsCode : null ,
resultPhoneNumber : null ,
resultSmsCode : null ,
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 ) {
clearInterval ( codeTimer) ;
}
} , 1000 ) ;
} ,
loginPhone ( ) {
if ( this . phoneNumber == this . resultPhoneNumber && this . smsCode == this . resultSmsCode ) {
return this . axios
. get ( "http://localhost:80/user/loginPhoneSms" , {
params : {
phoneNumber : this . phoneNumber
}
} )
. then ( ( result ) => {
console. log ( result ) ;
if ( result. data. state == 3 ) {
this . dialogFormVisible = false ;
this . isLogin = true ;
this . setCookie ( "user" , result. data. token, 600 ) ;
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方法,部分代码如下:
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 ;
但是因为我们可能是测试的,也就是说,若要认为成功,可以这样修改:
jsonObject. put ( "Message" , "OK" ) ;
if ( "OK" . equals ( jsonObject. get ( "Message" ) ) ) {
jsonObject. put ( "phoneNumber" , phoneNumber) ;
jsonObject. put ( "smsCode" , vcode) ;
return jsonObject;
}
使得默认已经发送短信了,当然,如果你有对应的正确的四个信息,那么可以不用默认已经发送短信,即不用修改
前端进行修改:
sendSms ( ) {
if ( this . smsCodeTimeSecond <= 0 ) {
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;
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/>
</ 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>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-web</ artifactId>
</ dependency>
< dependency>
< groupId> org.springframework.cloud</ groupId>
< artifactId> spring-cloud-starter-netflix-eureka-client</ artifactId>
</ dependency>
< dependency>
< groupId> com.baomidou</ groupId>
< artifactId> mybatis-plus-boot-starter</ artifactId>
< version> 3.3.2</ version>
</ dependency>
< dependency>
< groupId> mysql</ groupId>
< artifactId> mysql-connector-java</ artifactId>
< scope> runtime</ scope>
</ dependency>
< 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>
< groupId> net.oschina.zcx7878</ groupId>
< artifactId> fastdfs-client-java</ artifactId>
< version> 1.27.0.0</ version>
</ dependency>
< dependency>
< groupId> org.apache.commons</ groupId>
< artifactId> commons-io</ artifactId>
< version> 1.3.2</ version>
</ dependency>
< dependency>
< groupId> commons-fileupload</ groupId>
< artifactId> commons-fileupload</ artifactId>
< version> 1.3.1</ version>
</ dependency>
< dependency>
< 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) ;
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 {
@Autowired
private UserMapper userMapper;
@Override
public void updateUser ( Integer userid, String newName, String imgfileId) {
User user = new User ( ) ;
user. setId ( userid) ;
user. setName ( newName) ;
user. setPortrait ( imgfileId) ;
userMapper. updateById ( user) ;
}
@Override
public 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 {
@Autowired
private 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" )
@ResponseBody
public 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 ) ;
TrackerClient tc = new TrackerClient ( ) ;
TrackerServer ts = tc. getConnection ( ) ;
StorageServer ss = null ;
StorageClient1 client = new StorageClient1 ( ts, ss) ;
NameValuePair [ ] list = new NameValuePair [ 1 ] ;
list[ 0 ] = new NameValuePair ( "fileName" , oldFileName) ;
String 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" )
public class EduUserBootApplication {
public static void main ( String [ ] args) {
SpringApplication . run ( EduUserBootApplication . class , args) ;
}
}
将配置文件的后缀修改成yml,文件内容如下:
server :
port : 8002
spring :
application :
name : edu- user- boot
datasource :
driver-class-name : com.mysql.cj.jdbc.Driver
url : jdbc: mysql: //192.168.164.128: 3306/edu_user? useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username : root
password : QiDian@666
redis :
host : 192.168.164.128
port : 6379
eureka :
client :
service-url :
defaultZone : http: //localhost: 7001/eureka
register-with-eureka : true
fetch-registry : true
instance :
prefer-ip-address : true
instance-id : ${ spring.cloud.client.ip- address} : ${ server.port}
至此这里的后端项目搭建完毕
接下来我们到前端的Setting.vue组件(不是副本,副本是旧的,所以我们不操作他,而Setting.vue是我操作好的):
查看(查看或者说检查)如下:
data ( ) {
return {
dialogImageUrl : '' ,
dialogVisibleImg : false ,
httpRequestImg : false ,
dialogFormVisible : false ,
dialogVisible : false ,
user : null ,
fileId : "" ,
oldPwd : null ,
newPwd1 : null ,
newPwd2 : null ,
token : null ,
isLogin : false ,
} ;
} ,
< el-upload
class = " 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) ;
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 : xxx
assessKeySecret : xxx
这样就防止了没有对应依赖的需要的值
现在再次的启动,那么就不会报错了,但是在这里也要注意,因为我们引入了对应的依赖,那么启动类是有多个的
所以测试类需要指定启动类,否则测试类的方法运行会报错,因为多个情况下,就需要指定,就一个启动类的话,就不需要了
那么现在我们去点击上传头像(在页面你会知道在哪里的),查看fastdfs服务器是否有对应的文件,若有,则代表操作成功
现在记得修改方法如下:
getCookie ( 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
if ( ca. indexOf ( name) == 0 ) {
console. log ( 9 )
console. log ( ca. substring ( name. length, ca. length) )
return ca. substring ( name. length, ca. length) ;
}
}
} ,
否则,基本上是得不到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>
现在我们继续测试修改密码,若修改后,登录成功,那么操作完毕,但是这里有个问题
如果我们打开框框,然后直接取消,再次的打开,会发现,对应的值还存在,所以我们需要改动一下
对应的前端:
< 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" >
对应的方法如下:
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/>
</ 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>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-web</ artifactId>
</ dependency>
< dependency>
< groupId> org.springframework.cloud</ groupId>
< artifactId> spring-cloud-starter-netflix-eureka-client</ artifactId>
</ dependency>
< dependency>
< groupId> com.baomidou</ groupId>
< artifactId> mybatis-plus-boot-starter</ artifactId>
< version> 3.3.2</ version>
</ dependency>
< dependency>
< groupId> mysql</ groupId>
< artifactId> mysql-connector-java</ artifactId>
< scope> runtime</ scope>
</ dependency>
< 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>
< 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- boot
datasource :
driver-class-name : com.mysql.cj.jdbc.Driver
url : jdbc: mysql: //192.168.164.128: 3306/edu_course? useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username : root
password : QiDian@666
redis :
host : 192.168.164.128
port : 6379
eureka :
client :
service-url :
defaultZone : http: //localhost: 7001/eureka
register-with-eureka : true
fetch-registry : true
instance :
prefer-ip-address : true
instance-id : ${ spring.cloud.client.ip- address} : ${ server.port}
mybatis-plus :
mapper-locations : classpath: mybatis/mapper/*.xml
在启动类所在的包下创建entity包,然后创建如下实体类(不使用VO的,因为一次执行,多次使用,虽然这一次比较久,但可以多次使用,具体在85章博客有说明,但并非一定按照他(这个)的方式,若有更好的方式(针对大数据量或者复杂连接的情况,这里就使用这个方式,来测试一下,顺便看看结果),那么可以使用更好的,比如这里使用类似VO的DTO,DTO主要是在不改变mp的情况下的镜像,当然也可以操作其他,而并非镜像,主要取决于你,即用来返回的总体的数据(如操作加上list),而不是单纯的操作vo的部分的数据,并且在前端移动数据不好的情况下,你可以选择使用DTO操作vo的方式(DTO和VO只是名称而已,并非固定操作(所以在实际开发中需要先看这个项目对应的这个的含义),所以名称可以随便变,就算是VO,也可以操作DTO,只是规定这样的含义而已,通常用DTO代表全能),建议以后使用DTO(因为我们一般偏向于逻辑),因为他的方式或多或少需要一些顺序,如果是多个连续的主键和外键的碰撞,那么可能会比较麻烦(比如对应的里面突然是作为对方的外键,那么这种方式就可能操作不了),虽然这种情况比较少,很明显,DTO是一个全能的操作,因为只看你的逻辑关联,若存在项目中有xml,并且有mp的情况下,为了防止xml影响mp,所以通常可以加上@TableField(exist = false),他使得在查询时进行忽略,且不影响xml,而对与mp的增删改,null,不操作的情况,那么他虽然只操作查询,但也并不会进行处理,这种情况下也可以选择不操作DTO的某些镜像,只是对实体类操作了改变,所以总体来说可以解决,但是DTO也并非完美,因为在service中可能存在多个mapper,即代码集合一起,感觉没有什么顺序和对应可言,这样来对标xml在服务器中也存在关联的处理,总体来说各有好处和坏处):
package com. lagou. entity ;
import lombok. AllArgsConstructor ;
import lombok. Data ;
import lombok. NoArgsConstructor ;
import lombok. ToString ;
import java. io. Serializable ;
import java. util. Date ;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class ActivityCourse implements Serializable {
private static final long serialVersionUID = - 89461375082427542L ;
private Integer id;
private Integer courseId;
private Date beginTime;
private Date endTime;
private Long amount;
private Integer stock;
private Integer status;
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 ;
@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 ;
private Object id;
private String courseName;
private String brief;
private Object price;
private String priceTag;
private Object discounts;
private String discountsTag;
private String courseDescriptionMarkDown;
private String courseDescription;
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;
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 ;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseLesson implements Serializable {
private CourseMedia courseMedia;
private static final long serialVersionUID = - 35857311228165600L ;
private Object id;
private Integer courseId;
private Integer sectionId;
private String theme;
private Integer duration;
private Integer isFree;
private Date createTime;
private Date updateTime;
private Integer isDel;
private Integer orderNum;
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 ;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseMedia implements Serializable {
private static final long serialVersionUID = 673974921818890080L ;
private Integer id;
private Integer courseId;
private Integer sectionId;
private Integer lessonId;
private String coverImageUrl;
private String duration;
private String fileEdk;
private Long fileSize;
private String fileName;
private String fileDk;
private Date createTime;
private Date updateTime;
private Integer isDel;
private Integer durationNum;
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 ;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseSection implements Serializable {
private List < CourseLesson > courseLessonList;
private static final long serialVersionUID = 698702451600763670L ;
private Object id;
private Integer courseId;
private String sectionName;
private String description;
private Date createTime;
private Date updateTime;
private Integer isDel;
private Integer orderNum;
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 ;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Teacher implements Serializable {
private static final long serialVersionUID = 738571768582945875L ;
private Object 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 {
List < Course > getAllCourse ( ) ;
List < Course > getCourseByUserId ( @Param ( "userId" ) String userId) ;
Course getCourseById ( @Param ( "courseid" ) Integer courseid) ;
}
创建service.CourseService接口及其实现类:
package com. lagou. service ;
import com. lagou. entity. Course ;
import java. util. List ;
public interface CourseService {
List < Course > getAllCourse ( ) ;
List < Course > getCourseByUserId ( String userId) ;
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 {
@Autowired
private CourseMapper courseMapper;
@Override
public List < Course > getAllCourse ( ) {
System . out. println ( "===查询mysql===" ) ;
return courseMapper. getAllCourse ( ) ;
}
@Override
public List < Course > getCourseByUserId ( String userId) {
return courseMapper. getCourseByUserId ( userId) ;
}
@Override
public 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 {
@Autowired
private 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" )
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 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" />
< 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" >
SELECT
c.`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`
FROM
activity_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里面
修改或添加对应实现类的部分代码:
@Autowired
private RedisTemplate < Object , Object > redisTemplate;
@Override
public List < Course > getAllCourse ( ) {
RedisSerializer rs = new StringRedisSerializer ( ) ;
redisTemplate. setKeySerializer ( rs) ;
System . out. println ( "查询redis" ) ;
List < Course > list = ( List < Course > ) redisTemplate. opsForValue ( ) . get ( "allCourses" ) ;
if ( list == null ) {
System . out. println ( "====MySql数据库====" ) ;
list = courseMapper. getAllCourse ( ) ;
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" )
public List < Course > getAllCoursee ( ) {
ExecutorService es = Executors . newFixedThreadPool ( 20 ) ;
for ( int i = 1 ; i <= 20 ; i++ ) {
es. submit ( new Runnable ( ) {
@Override
public void run ( ) {
courseService. getAllCourse ( ) ;
}
} ) ;
}
return courseService. getAllCourse ( ) ;
}
我们重启项目,访问localhost:8004/course/getAllCoursee后,查看后台,可以发现,打印了多个"mysql"的信息,即:
== == MySql 数据库== ==
所以我们可以发现,他的确发生了缓存穿透,且出现了21个对应的两个redis和mysql的打印信息
当我们再次的执行,可以发现,只有21个redis了
那么如何解决呢:
最简单粗暴的解决方案:同步方法锁
@Override
public synchronized List < Course > getAllCourse ( ) {
因为我们都是访问该一个服务器,所以直接的加锁就可以,而不是访问多个(在80章博客说明过,那么就需要分布式锁了)
所以这里直接的加锁就可以了
重启项目,再次的测试,会发现,只会出现一个对应的mysql的打印信息了,即:
== == MySql 数据库== ==
但是我们也知道,直接的加锁,必然导致效率低下,因为后面的线程需要等待,虽然这里并没有明显体现(因为用户还是太少了)
所以说,若在大量的用户下,这个方式是不可取的,因为效率非常低,可能导致某些用户需要等待许久
效率稍微高一些的方案:同步代码块(双层检测锁 DCL:double check lock)
我们现在修改getAllCourse方法:
@Override
public List < Course > getAllCourse ( ) {
RedisSerializer rs = new StringRedisSerializer ( ) ;
redisTemplate. setKeySerializer ( rs) ;
System . out. println ( "查询redis" ) ;
List < Course > list = ( List < Course > ) redisTemplate. opsForValue ( ) . get ( "allCourses" ) ;
if ( list == null ) {
synchronized ( this ) {
list = ( List < Course > ) redisTemplate. opsForValue ( ) . get ( "allCourses" ) ;
if ( list == null ) {
System . out. println ( "====MySql数据库====" ) ;
list = courseMapper. getAllCourse ( ) ;
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%的优化通常需要算法来完成,剩余的看机器或者其他特殊原因了(通常考虑mysql与服务器的关联算法是一样的)
我们使用基本的查询来分开前面的sql语句,只需要多执行几次即可,然后将结果进行组装
那么有个问题,关联查询比分开后的多个单表查询总体来说是慢了还是快了,答:并不绝对
因为关联需要考虑笛卡尔积(在这里操作条件),如果没有条件,那么自然,多个单表查询快
如果有条件,通常多表查询可能要快些(前提是有索引),但是总体来说,多个单表要快,虽然单表也可以设置条件以及索引
但是通过条件的话,对应的条数基本是一样的,所以由于mysql的计算或者程序的计算基本类似
所以在条件方面,他们可以说是相同的,但是没有条件,一般是单表查询快
所以总体来说,没有条件时,单表是相加的形式,而多表是相乘的形式,自然,单表通常快点,有条件时,基本都是相乘的模式
但是有时候单表可能有格外的操作,使得解决后面的多余(在没有后续操作的情况下)
这时候,真的总体是相同的,但且由于基本都有条件
即就在代码多(包括类的建立),访问多(即条数多)和解决多余(可以说是细度的维护,或者在没有后续操作的情况下)以及mysql资源利用(计算,因为代码之所以多,就是为了解决这个)之间徘徊了,当然单表可能有其他好处,具体可以百度(比如表的锁竞争等等)
可以浅看一下这个博客https://blog.csdn.net/wangxuelei036/article/details/107647034,但是还需要考虑一个问题,单表虽然也存在其他好处,但是确需要更多的网络连接(请求多出,这个连接会占用数据库的连接最大数,因为数据库接收连接是有限的,虽然对占用这个问题来说很难出现),但是多表也并非没有代码,xml的编写也需要时间(这里对比单表的数据关联,因为比单表的可能麻烦一点),只是单表比较麻烦,而不是像xml那样的稳定操作(因为封装好的,但是在某些情况可能比单表差,如大数据量或者复杂连接下),所以具体情况具体分析,并非单表一定是好的,并且需要开发人员编写很多代码,所以在现实生活中,关联查询比较多,除非在大数据量和复杂连接条件下,因为机器扛不住,且xml不好写(xml的写的感受可能是比服务器关联要差的,可以自己测试,比如多个相同的id,需要别名),那么这个时候单表是一个好的选择,来减低数据库服务器的操作
为什么说单表也能是相乘:因为执行多次,后面的代码会体现的
但是我们还是不会在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 ;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Course implements Serializable {
private static final long serialVersionUID = 464248821202087847L ;
private Object id;
private String courseName;
private String brief;
private Object price;
private String priceTag;
private Object discounts;
private String discountsTag;
private String courseDescriptionMarkDown;
private String courseDescription;
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;
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 ;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseLesson implements Serializable {
private static final long serialVersionUID = - 35857311228165600L ;
private Object id;
private Integer courseId;
private Integer sectionId;
private String theme;
private Integer duration;
private Integer isFree;
private Date createTime;
private Date updateTime;
private Integer isDel;
private Integer orderNum;
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 ;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseSection implements Serializable {
private static final long serialVersionUID = 698702451600763670L ;
private Object id;
private Integer courseId;
private String sectionName;
private String description;
private Date createTime;
private Date updateTime;
private Integer isDel;
private Integer orderNum;
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 {
List < Course > getAllCourse ( ) ;
List < Course > getCourseByUserId ( String userId) ;
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 {
@Autowired
private CourseMapper courseMapper;
@Autowired
private RedisTemplate < Object , Object > redisTemplate;
@Override
public List < Course > getAllCourse ( ) {
List < Course > initCourse = getInitCourse ( ) ;
return initCourse;
}
@Override
public List < Course > getCourseByUserId ( String userId) {
return null ;
}
@Override
public Course getCourseById ( Integer courseid) {
return null ;
}
private List < Course > getInitCourse ( ) {
QueryWrapper q = new QueryWrapper ( ) ;
q. eq ( "status" , 1 ) ;
q. eq ( "is_del" , Boolean . FALSE ) ;
q. orderByDesc ( "sort_num" ) ;
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 {
@Autowired
private 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
public class CourseDTO {
private Teacher teacher;
private static final long serialVersionUID = 464248821202087847L ;
private Object id;
private String courseName;
private String brief;
private Object price;
private String priceTag;
private Object discounts;
private String discountsTag;
private String courseDescriptionMarkDown;
private String courseDescription;
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;
private Integer status;
private Integer sortNum;
private String previewFirstField;
private String previewSecondField;
private Integer sales;
}
修改CourseService接口的部分方法:
List < CourseDTO > getAllCourse ( ) ;
在CourseServiceImpl类里(也就是CourseService接口的实现类)加上或者修改如下:
@Autowired
private TeacherMapper teacherMapper;
@Override
public List < CourseDTO > getAllCourse ( ) {
List < Course > initCourse = getInitCourse ( ) ;
List < CourseDTO > courseDTOS = new ArrayList < > ( ) ;
for ( Course course : initCourse) {
CourseDTO dto = new CourseDTO ( ) ;
BeanUtils . copyProperties ( course, dto) ;
courseDTOS. add ( dto) ;
setTeacher ( 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 {
@Autowired
private CourseService courseService;
@GetMapping ( "getAllCourse" )
public List < CourseDTO > getAllCourse ( ) {
List < CourseDTO > list = courseService. getAllCourse ( ) ;
return list;
}
}
现在我们重启启动,访问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 ;
private Object id;
private Integer courseId;
private Integer sectionId;
private String theme;
private Integer duration;
private Integer isFree;
private Date createTime;
private Date updateTime;
private Integer isDel;
private Integer orderNum;
private Integer status;
}
现在,我们在CourseDTO类里添加如下属性:
private List < CourseLesson > 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类里修改或则添加如下:
@Autowired
private LessonMapper lessonMapper;
@Override
public List < CourseDTO > getAllCourse ( ) {
List < Course > initCourse = getInitCourse ( ) ;
List < CourseDTO > courseDTOS = new ArrayList < > ( ) ;
for ( Course course : initCourse) {
CourseDTO dto = new CourseDTO ( ) ;
BeanUtils . copyProperties ( course, dto) ;
courseDTOS. add ( dto) ;
setTeacher ( 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 ) ;
List < CourseLesson > list = lessonMapper. selectList ( q) ;
courseDTO. setLessonsDTO2 ( list) ;
}
至此,我们重启,再次的访问localhost:8004/course/getAllCourse,看看有没有对应的课时,若有,则操作成功
我们也发现,对应的DTO主要存放具体需要的内容(一般DTO主要存放数据,并且在层中移动或者返回(所以DTO可以代替VO,只不过我们大多数会使用VO,DTO大多数在单表处理中进行使用),而vo单纯针对返回,PO主要来接收,DO主要代表数据库完整数据(如果需要考虑非常高的维护,那么建议使用这三个来处理项目,而不是用逻辑来处理,使得减少具体表连接),当然,其实这些只是一种规范,如就算你操作vo作为接收也行,只是不建议),所以整体来说,单表,虽然使用更多的代码,但是也解决了数据的多余
就如上面说的,在大量的数据下(多余),前端操作计算大于连接mysql的时间,且包括执行时间,多表就不划算了
但实际上就算没有大于,我们也会为了更加的好细度的维护,也会使用单表,虽然单表的代码多(自然也是有缺点的)
虽然在少量数据下,访问多,但是可以更好维护,但在在大量数据下,访问多,但是执行速度快,且也更好的维护
但我们发现,这里多次使用DTO,这是因为mp的原因导致的,即mp虽然简化了代码,但也多出了简化的限制
一般来说维护是和性能来互相考虑的,在性能变化不大的情况下,我们主要考虑维护,所以上面主要是操作维护,因为单表并不会大幅度的减低性能(考虑不会减低的情况,因为他可能还会提升(关联够好,mysql关联不够好,包括查询的,或者数据关联),只是连接需要时间而已,且时间也是性能的一部分,所以考虑减低的情况),当然,过分的追求维护和性能是比较繁琐的,大多数我们是怎么方便怎么来,但也只是考虑小项目而言的,了解还是需要了解,大型项目不建议,这与设计模式可不同,设计模式也是考虑维护,但是有时候过分的设计可能维护会减低,这时因为有太多没有必要的处理,所以存在减低的可能性,因为你既然使用了,自然要考虑他们(需要你考虑多出来的东西,且他没有必要,自然无意义,即不好维护了,因为浪费你的时间),要不然怎么是设计模式呢,那还不如叫类和接口的随便关联呢
现在我们在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;
private static final long serialVersionUID = 698702451600763670L ;
private Object id;
private Integer courseId;
private String sectionName;
private String description;
private Date createTime;
private Date updateTime;
private Integer isDel;
private Integer orderNum;
private Integer status;
}
在CourseDTO类下,添加如下属性:
private List < SectionDTO > courseSections;
在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类里添加如下:
@Autowired
private SectionMapper sectionMapper;
@Override
public CourseDTO getCourseById ( Integer courseid) {
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 ( ) ) ;
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;
再在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类里加上如下:
@Autowired
private 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类里修改如下:
@Override
public List < CourseDTO > getAllCourse ( ) {
RedisSerializer rs = new StringRedisSerializer ( ) ;
redisTemplate. setKeySerializer ( rs) ;
System . out. println ( "***查询redis***" ) ;
List < CourseDTO > courseDTOS = ( List < CourseDTO > ) redisTemplate. opsForValue ( ) . get ( "allCourses" ) ;
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 ( ) ;
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 {
我们再次的重启项目,访问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>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-web</ artifactId>
</ dependency>
< dependency>
< groupId> org.springframework.cloud</ groupId>
< artifactId> spring-cloud-starter-netflix-eureka-client</ artifactId>
</ dependency>
< dependency>
< groupId> com.baomidou</ groupId>
< artifactId> mybatis-plus-boot-starter</ artifactId>
< version> 3.3.2</ version>
</ dependency>
< dependency>
< groupId> mysql</ groupId>
< artifactId> mysql-connector-java</ artifactId>
< scope> runtime</ scope>
</ dependency>
< 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- boot
datasource :
driver-class-name : com.mysql.cj.jdbc.Driver
url : jdbc: mysql: //192.168.164.128: 3306/edu_comment? useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username : root
password : QiDian@666
eureka :
client :
service-url :
defaultZone : http: //localhost: 7001/eureka/
register-with-eureka : true
fetch-registry : true
instance :
prefer-ip-address : true
instance-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" )
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 ;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseComment implements Serializable {
private static final long serialVersionUID = - 11641570368573216L ;
private List < CourseCommentFavoriteRecord > favoriteRecords;
private Object id;
private Integer courseId;
private Integer sectionId;
private Integer lessonId;
private Integer userId;
private String userName;
private Integer parentId;
private Integer isTop;
private String comment;
private Integer likeCount;
private Integer isReply;
private Integer type;
private Integer status;
private Date createTime;
private Date updateTime;
private Integer isDel;
private Integer lastOperator;
private Integer isNotify;
private Integer markBelong;
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 ;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseCommentFavoriteRecord implements Serializable {
private static final long serialVersionUID = 159062001487532233L ;
private Integer id;
private Integer userId;
private Integer commentId;
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 {
Integer saveComment ( CourseComment courseComment) ;
List < CourseComment > getCommentsByCourseId ( @Param ( "courseId" ) Integer courseId, @Param ( "offset" ) Integer offset, @Param ( "pagesize" ) Integer pagesize) ;
Integer existsFavorite ( @Param ( "cid" ) Integer cid, @Param ( "uid" ) Integer uid) ;
Integer saveCommentFavorite ( @Param ( "comment_id" ) Integer comment_id, @Param ( "user_id" ) Integer user_id) ;
Integer updateFavoriteStatus ( @Param ( "is_del" ) Integer is_del, @Param ( "comment_id" ) Integer comment_id, @Param ( "user_id" ) Integer user_id) ;
Integer FavoriteStatus ( @Param ( "comment_id" ) Integer comment_id, @Param ( "user_id" ) Integer user_id) ;
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 {
Integer saveComment ( CourseComment courseComment) ;
List < CourseComment > getCommentsByCourseId ( @Param ( "courseId" ) Integer courseId, @Param ( "offset" ) Integer offset, @Param ( "pagesize" ) Integer pagesize) ;
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 {
@Autowired
private CourseCommentDao courseCommentDao;
@Override
public Integer saveComment ( CourseComment courseComment) {
Integer integer = courseCommentDao. saveComment ( courseComment) ;
return integer;
}
@Override
public List < CourseComment > getCommentsByCourseId ( Integer courseId, Integer offset, Integer pagesize) {
List < CourseComment > commentsByCourseId = courseCommentDao. getCommentsByCourseId ( courseId, offset, pagesize) ;
return commentsByCourseId;
}
@Override
@Transactional
public 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 ) {
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 {
@Autowired
private 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" ) ;
CourseComment courseComment = new CourseComment ( ) ;
courseComment. setCourseId ( courseid) ;
courseComment. setSectionId ( 0 ) ;
courseComment. setLessonId ( 0 ) ;
courseComment. setUserId ( userid) ;
courseComment. setUserName ( username) ;
courseComment. setParentId ( 0 ) ;
courseComment. setComment ( comment) ;
courseComment. setType ( 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 > {
再修改CommentServiceImpl类的部分方法:
@Override
public 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) ;
然后重启项目,访问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 ( ) {
}
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 ( "获取课程详情失败!" ) ;
} ) ;
} ,
现在我们点击id为7的课程,往下滑,可以发现,出现留言了
现在我们修改CourseComment的部分属性:
@TableId ( type = IdType . AUTO )
private Integer id;
以及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 ) => {
this . getComment ( ) ;
} ) . catch ( ( error ) => {
this . $message. error ( "发表留言失败!" ) ;
} ) ;
} ,
记得修改如下(基本上需要用到user的都需要修改,即操作token,前端以及操作好的):
getCookie ( 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
if ( ca. indexOf ( name) == 0 ) {
console. log ( 9 )
console. log ( ca. substring ( name. length, ca. length) )
return ca. substring ( name. length, ca. length) ;
}
}
} ,
来保证得到对应的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 ) => {
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 > {
@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) ;
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 {
Integer saveComment ( CourseComment comment) ;
List < CourseComment > getCommentsByCourseId ( @Param ( "courseid" ) Integer courseid, @Param ( "offset" ) Integer offset, @Param ( "pageSize" ) Integer pageSize) ;
Integer saveFavorite ( Integer comment_id, Integer userid) ;
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 {
@Autowired
private CourseCommentDao courseCommentDao;
@Autowired
private CourseCommentFavoriteRecordDao courseCommentFavoriteRecordDao;
@Override
public Integer saveComment ( CourseComment comment) {
return courseCommentDao. insert ( comment) ;
}
@Override
public List < CourseComment > getCommentsByCourseId ( Integer courseid, Integer offset, Integer pageSize) {
return courseCommentDao. getCommentsByCourseId ( courseid, offset, pageSize) ;
}
@Override
public Integer saveFavorite ( Integer comment_id, Integer userid) {
return null ;
}
@Override
public 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 {
@Autowired
private 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" ) ) ) ;
CourseComment courseComment = new CourseComment ( ) ;
courseComment. setCourseId ( courseid) ;
courseComment. setSectionId ( 0 ) ;
courseComment. setLessonId ( 0 ) ;
courseComment. setUserId ( userid) ;
courseComment. setUserName ( username) ;
courseComment. setParentId ( 0 ) ;
courseComment. setComment ( comment) ;
courseComment. setType ( 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方法,内容如下:
@Override
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) ;
}
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方法,内容如下:
@Override
public 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 ) ;
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 ) => {
this . getComment ( ) ;
} ) . catch ( ( error ) => {
this . $message. error ( "点赞失败!" ) ;
} ) ;
} ,
cancelzan ( comment ) {
return this . axios
. get ( "http://localhost:8005/comment/cancelFavorite/" + comment. id+ "/" + this . userid)
. then ( ( 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)
若操作成功,那么就没有问题,至此点赞操作完成,当然了,因为前端操作了判断,所以基本不会执行相同的,而这里可以
使得更新相同的,但点赞数量增加了
但是,我们可以发现一个问题,上面虽然手动操作了异常,但是他还是始终是多个操作的,也就是说,如果前面的执行成功
后面的执行失败,那么虽然报错了,但是还是进行了操作,比如修改如下:
@Override
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;
}
重启项目,然后将对应数据库的点赞信息都删除,然后访问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章博客(下一章博客)里了,去该博客继续查看吧