抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

Hello world!

这里开始是前台部分

搭建项目前台环境

使用服务端渲染技术NuxtJS框架

![02 搭建项目前台环境(nuxt)](https://reria-images.oss-cn-hangzhou.aliyuncs.com/img01/images-master/img/02 搭建项目前台环境(nuxt).png)

搭建NuxtJS

  1. 先赋值template至工作区
  2. 修改.eslintrc.js(复制后台的即可)
  3. 修改package.json,必须修改其中的name,version,decription,author等
  4. 修改nuxt.config.js
  5. 安装依赖npm install
  6. 测试运行npm run dev

NUXT目录结构

  1. 资源目录 assets
    用于组织未编译的静态资源如 LESS、SASS 或 JavaScript。
  2. 组件目录 components
    用于组织应用的 Vue.js 组件。Nuxt.js 不会扩展增强该目录下 Vue.js 组件,即这些组件不会像页面组件那样有 asyncData 方法的特性。
  3. 布局目录 layouts
    用于组织应用的布局组件。
    1. default.vue:设置头尾信息
  4. 页面目录 pages
    用于组织应用的路由及视图。Nuxt.js 框架读取该目录下所有的 .vue 文件并自动生成对应的路由配置。
  5. 插件目录 plugins
    用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。
  6. nuxt.config.js 文件
    nuxt.config.js 文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。

引入基础资源和默认头尾视图

Nuxt本身基于vue,但是没有引入element-ui,所以需要引入

这里还引入了其他需要的资源,直接cv即可

  1. assets
  2. layout -> default.vue
  3. pages -> index.vue

default.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<template>
<div class="in-wrap">
<!-- 公共头引入 -->
<header id="header">
<section class="container">
<h1 id="logo">
<a href="#" title="谷粒学院">
<img src="~/assets/img/logo.png" width="100%" alt="谷粒学院" />
</a>
</h1>
<div class="h-r-nsl">
<ul class="nav">
<!-- 固定路由 -->
<router-link to="/" tag="li" active-class="current" exact>
<a>首页</a>
</router-link>
<router-link to="/course" tag="li" active-class="current">
<a>课程</a>
</router-link>
<router-link to="/teacher" tag="li" active-class="current">
<a>名师</a>
</router-link>
<router-link to="/article" tag="li" active-class="current">
<a>文章</a>
</router-link>
<router-link to="/qa" tag="li" active-class="current">
<a>问答</a>
</router-link>
</ul>
<!-- / nav -->
<ul class="h-r-login">
<li v-if="!loginInfo.id" id="no-login">
<a href="/login" title="登录">
<em class="icon18 login-icon">&nbsp;</em>
<span class="vam ml5">登录</span>
</a>
|
<a href="/register" title="注册">
<span class="vam ml5">注册</span>
</a>
</li>
<li v-if="loginInfo.id" id="is-login-one" class="mr10">
<a id="headerMsgCountId" href="#" title="消息">
<em class="icon18 news-icon">&nbsp;</em>
</a>
<q class="red-point" style="display: none">&nbsp;</q>
</li>
<li v-if="loginInfo.id" id="is-login-two" class="h-r-user">
<a href="/ucenter" title>
<img
:src="loginInfo.avatar"
width="30"
height="30"
class="vam picImg"
alt
/>
<span id="userName" class="vam disIb">{{
loginInfo.nickname
}}</span>
</a>
<a
href="javascript:void(0);"
title="退出"
@click="logout()"
class="ml5"
>退出</a
>
</li>
<!-- /未登录显示第1 li;登录后显示第2,3 li -->
</ul>
<aside class="h-r-search">
<form action="#" method="post">
<label class="h-r-s-box">
<input
type="text"
placeholder="输入你想学的课程"
name="queryCourse.courseName"
value
/>
<button type="submit" class="s-btn">
<em class="icon18">&nbsp;</em>
</button>
</label>
</form>
</aside>
</div>
<aside class="mw-nav-btn">
<div class="mw-nav-icon"></div>
</aside>
<div class="clear"></div>
</section>
</header>
<!-- /公共头引入 -->

<nuxt />

<!-- 公共底引入 -->
<footer id="footer">
<section class="container">
<div class>
<h4 class="hLh30">
<span class="fsize18 f-fM c-999">友情链接</span>
</h4>
<ul class="of flink-list">
<li>
<a href="http://www.atguigu.com/" title="尚硅谷" target="_blank"
>尚硅谷</a
>
</li>
</ul>
<div class="clear"></div>
</div>
<div class="b-foot">
<section class="fl col-7">
<section class="mr20">
<section class="b-f-link">
<a href="#" title="关于我们" target="_blank">关于我们</a>|
<a href="#" title="联系我们" target="_blank">联系我们</a>|
<a href="#" title="帮助中心" target="_blank">帮助中心</a>|
<a href="#" title="资源下载" target="_blank">资源下载</a>|
<span>服务热线:010-56253825(北京) 0755-85293825(深圳)</span>
<span>Email:info@atguigu.com</span>
</section>
<section class="b-f-link mt10">
<span>©2018课程版权均归谷粒学院所有 京ICP备17055252号</span>
</section>
</section>
</section>
<aside class="fl col-3 tac mt15">
<section class="gf-tx">
<span>
<img src="~/assets/img/wx-icon.png" alt />
</span>
</section>
<section class="gf-tx">
<span>
<img src="~/assets/img/wb-icon.png" alt />
</span>
</section>
</aside>
<div class="clear"></div>
</div>
</section>
</footer>
<!-- /公共底引入 -->
</div>
</template>
<script>
import '~/assets/css/reset.css'
import '~/assets/css/theme.css'
import '~/assets/css/global.css'
import '~/assets/css/web.css'
export default {
}
</script>

index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<template>
<div>
<!-- 幻灯片 开始 -->
<div v-swiper:mySwiper="swiperOption">
<div class="swiper-wrapper">
<div
v-for="banner in bannerList"
:key="banner.id"
class="swiper-slide"
style="background: #040b1b"
>
<a target="_blank" :href="banner.linkUrl">
<img :src="banner.imageUrl" :alt="banner.title" />
</a>
</div>
</div>
<div class="swiper-pagination swiper-pagination-white"></div>
<div
class="swiper-button-prev swiper-button-white"
slot="button-prev"
></div>
<div
class="swiper-button-next swiper-button-white"
slot="button-next"
></div>
</div>
<!-- 幻灯片 结束 -->

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

轮播图组件

本项目需要用到一个轮播图组件awesome-swiper(这里使用3.1.3版本)

安装

npm install vue-awesome-swiper@3.1.3

配置

在 plugins 文件夹下新建文件 nuxt-swiper-plugin.js

1
2
3
import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'
Vue.use(VueAwesomeSwiper)

在 nuxt.config.js 文件中配置插件

1
2
3
4
5
6
7
8
9
10
module.exports = {
// some nuxt config...
// awesome-swiper插件配置引入
plugins: [
{ src: '~/plugins/nuxt-swiper-plugin.js', ssr: false }
],
css: [
'swiper/dist/css/swiper.css'
]
}

axios依赖与封装

npm install axios

创建utils -> request.js

注意这里的baseURL要与nginx对应

1
2
3
4
5
6
7
import axios from 'axios'
// 创建axios实例
const service = axios.create({
baseURL: 'http://localhost:9001', // api的base_url
timeout: 20000 // 请求超时时间
})
export default service

element-ui

安装

npm install element-ui

配置

plugins -> nuxt-swiper-plugin.js

1
2
3
4
5
6
7
8
import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'
import VueQriously from 'vue-qriously'
import ElementUI from 'element-ui' //element-ui的全部组件
import 'element-ui/lib/theme-chalk/index.css'//element-ui的css
Vue.use(ElementUI) //使用elementUI
Vue.use(VueQriously)
Vue.use(VueAwesomeSwiper)

测试

必看注意,直接启动的话eslint语法检查十分严格,类似于空格是2个就不能是4个,所以要把严格检查关闭

解决vue/cli3.0 语法验证规则 ESLint: Expected indentation of 2 spaces but found 4. (indent) - 走看看 (zoukankan.com)

仔细参考上述文章内容,注意两个步骤都要执行才行(弄了我十几分钟有点烦躁,好在解决了)

首页基础搭建

讲师和课程视图模板搭建

固定路由

注意default.js中设置了头导航的固定路由,所以需要在pages文件夹下创建对应的视图

注意其中的router-link(即路由导航),其中的to对应的地址就是视图的地址,值为文件夹的路径,to会自动找到pages文件夹下对应名字的文件夹中的index.vue

按照这个思路创建对应视图即可,然后复制资料中已经有的视图模板

动态路由

动态路由编写有一定的规范

例如需要根据id查询一条记录,需要在对应pages的文件夹下创建_id.vue文件,(必须是以下划线开头,参数名为下划线后面的文件名)

创建视图

根据上述条件创建即可,然后内容模板cv即可

当前项目结构

image-20210928194240441

后端环境搭建

创建子模块

service_cms

创建并编写配置文件application.properties

注意这里的DataSource配置,如果不对可能会报springboot找不到bean的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 服务端口
server.port=8004
# 服务名
spring.application.name=service-cms
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456789
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/educms/mapper/xml/*.xml
spring.redis.host=192.168.44.132
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

这里记得去nginx.conf里配置一下

创建数据库表

导入资料的sql文件即可

编写启动类

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@ComponentScan(basePackages = {"com.atguigu"})
@MapperScan("com.atguigu.educms.mapper")
@EnableDiscoveryClient // nacos注册
public class CmsApplication {
public static void main(String[] args) {
SpringApplication.run(CmsApplication.class, args);
}
}

代码生成器

老样子改一下就行

生成之后跨域注释加一下、mapperscan加一下、配置文件中扫描mapper.xml路径加一下

一些实体类字段的注释也要加一下,比如说自动添加和逻辑删除

1
2
3
@MapperScan("com.atguigu.educms.mapper")
public class CmsApplication {
}
1
2
3
4
@CrossOrigin
public class CrmBannerController {

}
1
2
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/educms/mapper/xml/*.xml

当前项目结构

image-20210928200432351

首页轮播图(后端)

后台应该也有轮播图的管理模块,所以需要两个bannerController

编写BannerAdminController

用于后台的banner管理接口,TODO:前端页面自己写(我选择不写使用swagger测试嘿嘿😙)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RestController
@RequestMapping("/educms/banneradmin")
@CrossOrigin
public class BannerAdminController {
@Autowired
private CrmBannerService bannerService;
// 分页查询
@GetMapping("pageBanner/{pageNo}/{limit}")
public R pageBanner(@PathVariable Long limit, @PathVariable Long pageNo) {
Page<CrmBanner> pageBanner = new Page<>(pageNo, limit);
bannerService.page(pageBanner, null);
return R.ok().data("records", pageBanner.getRecords()).data("total", pageBanner.getTotal());
}
//2 添加banner
@PostMapping("addBanner")
public R addBanner(@RequestBody CrmBanner crmBanner) {
bannerService.save(crmBanner);
return R.ok();
}
@ApiOperation(value = "获取Banner")
@GetMapping("get/{id}")
public R get(@PathVariable String id) {
CrmBanner banner = bannerService.getById(id);
return R.ok().data("item", banner);
}
@ApiOperation(value = "修改Banner")
@PutMapping("update")
public R updateById(@RequestBody CrmBanner banner) {
bannerService.updateById(banner);
return R.ok();
}
@ApiOperation(value = "删除Banner")
@DeleteMapping("remove/{id}")
public R remove(@PathVariable String id) {
bannerService.removeById(id);
return R.ok();
}
}

编写BannerFrontController

用于前台的banner接口,主要用于显示banner

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/educms/bannerfront")
@CrossOrigin
public class BannerFrontController {
@Autowired
private CrmBannerService bannerService;
//查询banner信息
@GetMapping("getBannerList")
public R getBannerList() {
List<CrmBanner> list = bannerService.selectAllBanner();
return R.ok().data("list", list);
}
}

编写Service

这里自己写了个service,结果啥也没实现,纯属脱裤子放屁,我改了

我发现我错了,我应该看完整个视频才说话,这个是为了后面的Redis做准备的

写Mapper里最好(弹幕:感谢这段代码,昨天写了,今天老板让我走)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class CrmBannerServiceImpl extends ServiceImpl<CrmBannerMapper, CrmBanner> implements CrmBannerService {
// 查询所有banner
@Override
public List<CrmBanner> selectAllBanner() {
// 根据id降序排序,显示排序后的前两条记录
QueryWrapper<CrmBanner> wrapper = new QueryWrapper<>();
// 设置排序条件
wrapper.orderByDesc("id");
// 设置显示数量,last拼接sql语句
wrapper.last("limit 2");
List<CrmBanner> crmBanners = baseMapper.selectList(wrapper);
return crmBanners;
}
}

首页热门课程和名师(后端)

注意这里,controller这些又写到service_edu模块中了

编写Mapper

需求:根据id进行降序排序(或者根据表中专门的热度排序),显示排序之后的前8条数据

EduTeacherMapper

1
2
3
4
5
6
<select id="selectIndexList" resultType="com.atguigu.eduservice.entity.EduTeacher">
select *
from edu_teacher
order by id desc
limit 4
</select>

EduCourseMapper

1
2
3
4
5
6
<select id="selectIndexList" resultType="com.atguigu.eduservice.entity.EduCourse">
select *
from edu_course
order by id desc
limit 8
</select>

编写service

就直接调用baseMapper中的接口即可

编写controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/eduservice/index")
@CrossOrigin
public class IndexController {

@Autowired
private EduCourseService courseService;
@Autowired
private EduTeacherService teacherService;

// 查询热门课程和讲师
@GetMapping("index")
public R getIndex() {
List<EduCourse> courseList = courseService.getIndexList();
List<EduTeacher> teacherList = teacherService.getIndexList();
return R.ok().data("courseList", courseList).data("teacherList", teacherList);
}
}

首页轮播图和热门讲师课程(前端)

API

  1. api -> banner.js
  2. api -> index.js
1
2
3
4
5
6
7
8
9
10
11
import request from '@/utils/request'

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

index.js

1
2
3
4
5
6
7
8
9
10
11
import request from '@/utils/request'

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

调用接口

注意这里的response需要两个data,应为这个模板没有帮忙封装response的一些东西,之前的直接获得的是response.data

轮播图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<template>
<div>
<!-- 幻灯片 开始 -->
<div v-swiper:mySwiper="swiperOption">
<div class="swiper-wrapper">
<div
v-for="banner in bannerList"
:key="banner.id"
class="swiper-slide"
style="background: #040b1b"
>
<a target="_blank" :href="banner.linkUrl">
<img :src="banner.imageUrl" :alt="banner.title" />
</a>
</div>
</div>
<div class="swiper-pagination swiper-pagination-white"></div>
<div
class="swiper-button-prev swiper-button-white"
slot="button-prev"
></div>
<div
class="swiper-button-next swiper-button-white"
slot="button-next"
></div>
</div>
<!-- 幻灯片 结束 -->
</div>
</template>

<script>
import bannerApi from '@/api/banner'
export default {
data() {
return {
swiperOption: {
// 配置分页
pagination: {
el: '.swiper-pagination' // 分页的dom节点
},
// 配置导航
navigation: {
nextEl: '.swiper-button-next', // 下一页dom节点
prevEl: '.swiper-button-prev' // 前一页dom节点
}
},
// 数组
bannerList: [],
courseList: [],
teacherList: []
}
},
created() {
// 初始化数据
this.getBannerList()
},
methods: {
// 初始化首页轮播图课程讲师列表
getBannerList() {
bannerApi.getBannerList().then(result => {
this.bannerList = result.data.data.list
})
}
}
}
</script>

热门课程讲师

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
<template>
<div>
<div id="aCoursesList">
<!-- 网校课程 开始 -->
<div>
<section class="container">
<header class="comm-title">
<h2 class="tac">
<span class="c-333">热门课程</span>
</h2>
</header>
<div>
<article class="comm-course-list">
<ul class="of" id="bna">
<li
v-for="course in courseList"
:key="course.id"
>
<div class="cc-l-wrap">
<section class="course-img">
<img
:src="course.cover"
class="img-responsive"
:alt="course.title"
/>
<div class="cc-mask">
<a
href="#"
title="开始学习"
class="comm-btn c-btn-1"
>开始学习</a
>
</div>
</section>
<h3 class="hLh30 txtOf mt10">
<a
href="#"
:title="course.title"
class="
course-title
fsize18
c-333
"
>{{ course.title }}</a
>
</h3>
<section class="mt10 hLh20 of">
<span
class="fr jgTag bg-green"
v-if="
Number(course.price) === 0
"
>
<i class="c-fff fsize12 f-fA"
>免费</i
>
</span>
<span class="fl jgAttr c-ccc f-fA">
<i class="c-999 f-fA"
>9634人学习</i
>
|
<i class="c-999 f-fA"
>9634评论</i
>
</span>
</section>
</div>
</li>
</ul>
<div class="clear"></div>
</article>
<section class="tac pt20">
<a
href="#"
title="全部课程"
class="comm-btn c-btn-2"
>全部课程</a
>
</section>
</div>
</section>
</div>
<!-- /网校课程 结束 -->
<!-- 网校名师 开始 -->
<div>
<section class="container">
<header class="comm-title">
<h2 class="tac">
<span class="c-333">名师大咖</span>
</h2>
</header>
<div>
<article class="i-teacher-list">
<ul class="of">
<li
v-for="teacher in teacherList"
:key="teacher.id"
>
<section class="i-teach-wrap">
<div class="i-teach-pic">
<a
href="/teacher/1"
:title="teacher.name"
>
<img
:alt="teacher.name"
:src="teacher.avatar"
/>
</a>
</div>
<div class="mt10 hLh30 txtOf tac">
<a
href="/teacher/1"
:title="teacher.name"
class="fsize18 c-666"
>{{ teacher.name }}</a
>
</div>
<div class="hLh30 txtOf tac">
<span class="fsize14 c-999">{{
teacher.career
}}</span>
</div>
<div class="mt15 i-q-txt">
<p class="c-999 f-fA">
{{ teacher.intro }}
</p>
</div>
</section>
</li>
</ul>
<div class="clear"></div>
</article>
<section class="tac pt20">
<a
href="#"
title="全部讲师"
class="comm-btn c-btn-2"
>全部讲师</a
>
</section>
</div>
</section>
</div>
<!-- /网校名师 结束 -->
</div>
</div>
</template>

<script>
import bannerApi from '@/api/banner'
import indexApi from '@/api/index'
export default {
data() {
return {
swiperOption: {
// 配置分页
pagination: {
el: '.swiper-pagination' // 分页的dom节点
},
// 配置导航
navigation: {
nextEl: '.swiper-button-next', // 下一页dom节点
prevEl: '.swiper-button-prev' // 前一页dom节点
}
},
// 数组
bannerList: [],
courseList: [],
teacherList: []
}
},
created() {
// 初始化数据
this.getBannerList()
this.getCourseTeacher()
},
methods: {
// 初始化首页课程讲师列表
getCourseTeacher() {
indexApi.getIndexList().then(result => {
this.courseList = result.data.data.courseList
this.teacherList = result.data.data.teacherList
})
}
}
}
</script>

测试

这里我没有把url对应起来,所以报了Access to XMLHttpRequest at 'http://localhost:9001/educms/bannerfront/getBannerList' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.的错误

这个错误还有可能是nginx没有配置,或者跨域注解没有加

单点登录SSO

![02 单点登录三种方式介绍](https://reria-images.oss-cn-hangzhou.aliyuncs.com/img01/images-master/img/02 单点登录三种方式介绍.png)

Redis

介绍

image-20210929131659223

整合

该项目在common模块中配置

依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

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

创建配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Configuration  // 配置类
@EnableCaching // 开启cashe缓存
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}

springboot缓存注解

在相对应接口中添加Redis缓存使用注解即可使用Redis缓存

  1. @Cacheable:一般用在查询方法上,根据方法对其返回结果进行缓存,下次请求时如果缓存存在,name直接读取缓存数据返回
  2. @CachePut:一般用在新增方法上,使用该注解的方法每次都会执行,并将结果存入指定的缓存中
  3. @CacheEvict:一般用在更新或者删除方法上,会清空指定的缓存

使用Redis服务

安装Redis并启动

这里我去谷粒商城看了

配置文件

在需要使用redis的模块的配置文件application.properties中添加配置

1
2
3
4
5
6
7
8
#redis配置
#注意这里的host和port要和虚拟机的对应
spring.redis.host=192.168.128.129
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1

首页缓存

在相关的service方法上添加注释(注意这里为什么要在service中加,因为注释自动添加到redis的话,是从返回值获取的,controller中定义了统一返回格式,不适用)

1
2
3
4
5
6
7
8
@Service
public class CrmBannerServiceImpl extends ServiceImpl<CrmBannerMapper, CrmBanner> implements CrmBannerService {
// 查询所有banner
@Override
@Cacheable(key = "'selectIndexList'", value = "banner") // Redis缓存,注意这里key中要加单引号
public List<CrmBanner> selectAllBanner() {
}
}

redis相关命令

  1. 使用root权限:su root
  2. 查看VMwarehost:ifconfig
  3. 修改配置文件:vi /mydata/redis/conf/redis.conf
  4. 启动redis:docker start redis
  5. 进入redis:docker exec -it redis redis-cli
  6. 查看所有keyvalue:keys *
  7. 设置查看:set/get xxx

JWT(Json Web Token)

image-20210929144700746

依赖

先引入依赖,common_utils模块即可

1
2
3
4
5
6
7
<dependencies>
<!-- JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>

utils类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
public class JwtUtils {
public static final long EXPIRE = 1000 * 60 * 60 * 24;
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
public static String getJwtToken(String id, String nickname) {
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("guli-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();

return JwtToken;
}
/**
* 判断token是否存在与有效
*
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 判断token是否存在与有效
*
* @param request
* @return
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if (StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token获取会员id
*
* @param request
* @return
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if (StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String) claims.get("id");
}
}

短信微服务

阿里云短信服务

开启之后,申请签名管理和模板管理

???必须要备案网站好家伙mua的

TODO,没有域名下次再说吧

创建子模块

service_msm模块

配置文件application.properties

基本上都是一样的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 服务端口
server.port=8005
# 服务名
spring.application.name=service-msm
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456789
#redis配置
spring.redis.host=192.168.128.129
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#最小空闲
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/educms/mapper/xml/*.xml

启动类

1
2
3
4
5
6
7
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ComponentScan(basePackages = {"com.atguigu"})
public class MsmApplication {
public static void main(String[] args) {
SpringApplication.run(MsmApplication.class, args);
}
}

controller

TODO

service

TODO

登录注册(后端)

基础工作

创建模块

service_ucenter

数据库

ucenter_member

代码生成器

老样子

配置文件

老样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 服务端口
server.port=8006
# 服务名
spring.application.name=service-ucenter
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456789
#redis配置
spring.redis.host=192.168.128.129
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#最小空闲
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/educenter/mapper/xml/*.xml

启动类

1
2
3
4
5
6
7
8
@SpringBootApplication
@ComponentScan({"com.atguigu"})
@MapperScan("com.atguigu.educenter.mapper")
public class UcenterApplication {
public static void main(String[] args) {
SpringApplication.run(UcenterApplication.class, args);
}
}

还有nginx配置,跨域配置等

登录

MD5Utils

加密工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.atguigu.educenter.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class MD5 {
public static String encrypt(String strSrc) {
try {
char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f' };
byte[] bytes = strSrc.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("MD5加密出错!!+" + e);
}
}
public static void main(String[] args) {
System.out.println(MD5.encrypt("111111"));
}

}

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/educenter/member")
@CrossOrigin
public class UcenterMemberController {
@Autowired
private UcenterMemberService memberService;
// 登录
@PostMapping("login")
public R loginUser(@RequestBody UcenterMember member) {
// 调用service方法,并且返回token
String token = memberService.login(member);
return R.ok().data("token", token);
}
}

service

注意使用MD5工具加密再比较密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService {
// 登录
@Override
public String login(UcenterMember member) {
// 获得手机号和密码
String mobile = member.getMobile();
String password = member.getPassword();
// 非空判断
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
throw new GuliException(20001, "手机或密码为空");
}
// 查询数据库
QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
wrapper.eq("mobile", mobile);
UcenterMember mmember = baseMapper.selectOne(wrapper);
if (mmember == null) {
throw new GuliException(20001, "该账号不存在");
}
// 判断密码是否正确
// 数据库密码为加密后的密码,所以需要转换
// 使用加密方式MD5(只能加密)
if (!mmember.getPassword().equals(MD5.encrypt(password))) {
throw new GuliException(20001, "密码错误");
}
// 判断是否禁用
if (mmember.getIsDisabled()) {
throw new GuliException(20001, "账号禁用");
}
// 生成token字符串
String token = JwtUtils.getJwtToken(mmember.getId(), mmember.getNickname());
return token;
}
}

swagger测试

初始值随便复制一个mobile,密码统一为111111

注册

vo实体类

1
2
3
4
5
6
7
8
9
10
11
@Data
public class RegisterVo {
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "手机号")
private String mobile;
@ApiModelProperty(value = "密码")
private String password;
//@ApiModelProperty(value = "验证码")
//private String code;
}

controller

1
2
3
4
5
6
// 注册
@PostMapping("register")
public R registerUser(@RequestBody RegisterVo registerVo) {
memberService.register(registerVo);
return R.ok();
}

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 注册
@Override
public void register(RegisterVo registerVo) {
// TODO 验证码判断(暂时不写)
//获取注册的数据
String mobile = registerVo.getMobile(); //手机号
String nickname = registerVo.getNickname(); //昵称
String password = registerVo.getPassword(); //密码
//非空判断
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password) || StringUtils.isEmpty(nickname)) {
throw new GuliException(20001, "输入为空");
}
//判断手机号是否重复,表里面存在相同手机号不进行添加
QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
wrapper.eq("mobile", mobile);
Integer count = baseMapper.selectCount(wrapper);
if (count > 0) {
throw new GuliException(20001, "手机号重复");
}
//数据添加数据库中
UcenterMember member = new UcenterMember();
member.setMobile(mobile);
member.setNickname(nickname);
member.setPassword(MD5.encrypt(password));//密码需要加密的
member.setIsDisabled(false);//用户不禁用
member.setAvatar("http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132");
baseMapper.insert(member);
}

回显数据

根据token获得用户信息

controller

1
2
3
4
5
6
7
8
9
10
// 根据token获取用户信息
@GetMapping("getMemberInfo")
public R getMemberInfo(HttpServletRequest request) {
// 调用jwt工具类方法获取request对象的头信息,获得id
String memberId = JwtUtils.getMemberIdByJwtToken(request);
// 获得用户信息
UcenterMember member = memberService.getById(memberId);
// 这里最好额外使用vo实体类
return R.ok().data("member", member);
}

登录注册(前端)

注册

layout

创建sign.vue然后cv

这个是专门的注册登录页面的布局

api

api中创建对应js即可

1
2
3
4
5
6
7
8
9
10
11
import request from '@/utils/request'
export default {
// 注册
register(registerVo) {
return request({
url: '/educenter/member/register',
method: 'post',
data: registerVo
})
}
}

vue页面

先去layout -> default.vue中的地址该为登录注册页面的地址

然后在pages里创建vue视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<template>
<div class="main">
<div class="title">
<a href="/login">登录</a>
<span>·</span>
<a class="active" href="/register">注册</a>
</div>
<div class="sign-up-container">
<el-form ref="userForm" :model="params">
<el-form-item
class="input-prepend restyle"
prop="nickname"
:rules="[
{ required: true, message: '请输入你的昵称', trigger: 'blur' },
]"
>
<div>
<el-input
type="text"
placeholder="你的昵称"
v-model="params.nickname"
/>
<i class="iconfont icon-user" />
</div>
</el-form-item>
<el-form-item
class="input-prepend restyle no-radius"
prop="mobile"
:rules="[
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{ validator: checkPhone, trigger: 'blur' },
]"
>
<div>
<el-input
type="text"
placeholder="手机号"
v-model="params.mobile"
/>
<i class="iconfont icon-phone" />
</div>
</el-form-item>

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

<el-form-item
class="input-prepend"
prop="password"
:rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]"
>
<div>
<el-input
type="password"
placeholder="设置密码"
v-model="params.password"
/>
<i class="iconfont icon-password" />
</div>
</el-form-item>

<div class="btn">
<input
type="button"
class="sign-up-button"
value="注册"
@click="submitRegister()"
/>
</div>
<p class="sign-up-msg">
点击 “注册” 即表示您同意并愿意遵守简书
<br />
<a target="_blank" href="http://www.jianshu.com/p/c44d171298ce"
>用户协议</a
>

<a target="_blank" href="http://www.jianshu.com/p/2ov8x3">隐私政策</a>

</p>
</el-form>
<!-- 更多注册方式 -->
<div class="more-sign">
<h6>社交帐号直接注册</h6>
<ul>
<li>
<a
id="weixin"
class="weixin"
target="_blank"
href="http://huaan.free.idcfengye.com/api/ucenter/wx/login"
><i class="iconfont icon-weixin"
/></a>
</li>
<li>
<a id="qq" class="qq" target="_blank" href="#"
><i class="iconfont icon-qq"
/></a>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import '~/assets/css/sign.css'
import '~/assets/css/iconfont.css'
export default {
layout: 'sign',
data() {
return {
params: {
// 封装注册输入数据
mobile: '',
code: '', // 验证码
nickname: '',
password: ''
},
sending: true, // 是否发送验证码
second: 60, // 倒计时间
codeTest: '获取验证码'
}
},
methods: {}
}
</script>

vue方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 注册提交
submitRegister() {
registerApi.register(this.params).then(result => {
// 提示注册成功
this.$message({
type: 'success',
message: '注册成功'
})
// 跳转登录页面
this.$router.push({ path: '/login' })
})
},
// 发送验证码
getCodeFun() {
if ((this.sending = false)) {
this.$message({
type: 'error',
message: '请勿频繁点击'
})
return
}
// 由于阿里云短信服务不能用,所以这里写假验证
this.sending = false
// 调用倒计时的方法
this.timeDown()
},
// 验证手机号格式
checkPhone(rule, value, callback) {
// debugger
if (!/^1[34578]\d{9}$/.test(value)) {
return callback(new Error('手机号码格式不正确'))
}
return callback()
},
// 倒计时
timeDown() {
const result = setInterval(() => {
--this.second
this.codeTest = this.second
if (this.second < 1) {
clearInterval(result)
this.sending = true
this.second = 60
this.codeTest = '获取验证码'
}
}, 1000)
}

TODO:这里有很多地方需要完善:比如点击验证码之后再次点击数字需要弹窗警告,但是现在不会而且还会加速计时器

登录

cookie插件

这里需要用到cookie,需要下载

npm install js-cookie

api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import request from '@/utils/request'

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

vue视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<template>
<div class="main">
<div class="title">
<a class="active" href="/login">登录</a>
<span>·</span>
<a href="/register">注册</a>
</div>
<div class="sign-up-container">
<el-form ref="userForm" :model="user">
<el-form-item
class="input-prepend restyle"
prop="mobile"
:rules="[
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{ validator: checkPhone, trigger: 'blur' },
]"
>
<div>
<el-input type="text" placeholder="手机号" v-model="user.mobile" />
<i class="iconfont icon-phone" />
</div>
</el-form-item>

<el-form-item
class="input-prepend"
prop="password"
:rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]"
>
<div>
<el-input
type="password"
placeholder="密码"
v-model="user.password"
/>
<i class="iconfont icon-password" />
</div>
</el-form-item>

<div class="btn">
<input
type="button"
class="sign-in-button"
value="登录"
@click="submitLogin()"
/>
</div>
</el-form>
<!-- 更多登录方式 -->
<div class="more-sign">
<h6>社交帐号登录</h6>
<ul>
<li>
<a
id="weixin"
class="weixin"
target="_blank"
href="http://qy.free.idcfengye.com/api/ucenter/weixinLogin/login"
><i class="iconfont icon-weixin"
/></a>
</li>
<li>
<a id="qq" class="qq" target="_blank" href="#"
><i class="iconfont icon-qq"
/></a>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import '~/assets/css/sign.css'
import '~/assets/css/iconfont.css'
export default {
layout: 'sign',
data() {
return {
user: {
mobile: '',
password: ''
},
loginInfo: {}
}
},
methods: {}
}
</script>
<style>
.el-form-item__error {
z-index: 9999999;
}
</style>

拦截器

在下面的方法中有说明登录token的步骤,其中需要用到拦截器

拦截器写在utils文件夹下的request.js文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cookie from 'js-cookie'

// 每次请求中使用拦截器
service.interceptors.request.use(
config => {
if (cookie.get('guli_token')) {
// 从cookie中获得token并且放入header中
config.headers['token'] = cookie.get('guli_token');
}
return config
},
err => {
return Promise.reject(err)
}
)

vue方法

  1. 调用接口登录获取token
  2. 将token放入cookie
  3. 前端拦截器:判断cookie里是否有token,如果有则放到header中
  4. 根据header中的token调用接口获取用户信息
  5. 把获取的用户信息放入cookie

login.vue中

注意这里把对象放入cookie中是要使用JSON.stringify转换为字符串才行,要不然就是object,回报错(cookie获得的json对象为undefined)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 登录的方法
submitLogin() {
// 1、调用接口进行登录,返回token字符串
loginApi.login(this.user).then(result => {
// 2、获得token,并放入cookie
// 第一个参数cookie名称,第二个参数值,第三个参数作用范围
cookieApi.set('guli_token', result.data.data.token, {
domain: 'localhost'
})
// 4、调用接口获取header中的token
loginApi.getLoginMemberInfo().then(result => {
// 5、根据token获取用户信息放入cookie中
console.log(response.data.data.member)
cookieApi.set('guli_ucenter', JSON.stringify(result.data.data.member), {
domain: 'localhost'
})
})
// 跳转页面
this.$router.push({ path: '/' })
})
},
// 验证手机号格式
checkPhone(rule, value, callback) {
if (!/^1[34578]\d{9}$/.test(value)) {
return callback(new Error('手机号码格式不正确'))
}
return callback()
}

default.vue中编写根据token获得用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
created() {
this.showInfo()
},
methods: {
// 从cookie中获取用户信息
showInfo() {
// 注意这里需要转换
console.log('从cookie中获得用户信息:' + cookieApi.get('guli_ucenter'))
var loginInfoStr = cookieApi.get('guli_ucenter')
console.log(cookieApi.get('guli_ucenter'))
// 把字符串转换为JSON格式
if (loginInfoStr) {
this.loginInfo = JSON.parse(loginInfoStr)
}
},
// 退出登录
logout() {
// 清除cookie
cookieApi.set('guli_token', '', { domain: 'localhost' })
cookieApi.set('guli_ucenter', '', { domain: 'localhost' })
// 清除数据
this.token = ''
this.loginInfo = {}
// 跳转至首页
this.$router.push({ path: '/' })
}
}

TODO:这里需要完善的地方有很多,登录校验(JWT strings must contain exactly 2 period characters. Found: 0)如果输入错误页面竟然显示cookie undefined,而且重新登录也不行,还有前端的信息提示

微信扫码登录(后端+前端)

OAuth2

一种方案,解决两个问题:

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

![03 OAuth2介绍](https://reria-images.oss-cn-hangzhou.aliyuncs.com/img01/images-master/img/03 OAuth2介绍.png)

微信登录准备工作

没事了要付钱,鹅肠真有你的

没事了老师共享账号,好耶!

配置文件

service_ucenter模块的配置文件即可(只用于登录注册)

1
2
3
4
5
6
# 微信开放平台 appid
wx.open.app_id=wxed9954c01bb89b47
# 微信开放平台 appsecret
wx.open.app_secret=a7482517235173ddb4083788de60b90e
# 微信开放平台 重定向url
wx.open.redirect_url=http://localhost:8160/api/ucenter/wx/callback

常量类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.atguigu.educenter.utils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ConstantWxUtils implements InitializingBean {
@Value("${wx.open.app_id}")
private String appId;
@Value("${wx.open.app_secret}")
private String appSecret;
@Value("${wx.open.redirect_url}")
private String redirectUrl;
public static String WX_OPEN_APP_ID;
public static String WX_OPEN_APP_SECRET;
public static String WX_OPEN_REDIRECT_URL;
@Override
public void afterPropertiesSet() throws Exception {
WX_OPEN_APP_ID = appId;
WX_OPEN_APP_SECRET = appSecret;
WX_OPEN_REDIRECT_URL = redirectUrl;
}
}

生成二维码

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
@CrossOrigin
@RequestMapping("/api/ucenter/wx")
public class WxApiController {
// 生成扫描二维码
@GetMapping("login")
public String getWxCode() throws UnsupportedEncodingException {
// 参数位置都是用%s来代替,然后使用String.format
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=%s" +
"&redirect_uri=%s" +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=%s" +
"#wechat_redirect";
// 需要进行url编码
String redirectUrl = URLEncoder.encode(ConstantWxUtils.WX_OPEN_REDIRECT_URL, "UTF-8");
// 传入url中的参数获得最终的url
String url = String.format(baseUrl, ConstantWxUtils.WX_OPEN_APP_ID, redirectUrl, "atguigu");
// 重定向到微信地址
return "redirect:" + url;
}
}

注意这里需要修改端口号,因为用的是老师的url,所以要对应,nginx里也要改

这里我明白了一点nginx的作用,前端只需要访问nginx提供的9001端口号,而9001能够转发到8001等其他端口号,所以后端端口号改了只要该nginx就行,神奇!

获取扫描人信息

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <dependencies>
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>

HttpClientUtils

太长不复制了

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Autowired
private UcenterMemberService memberService;
// 获取扫描人的信息,添加数据
@GetMapping("callback")
public String callback(String code, String status) throws Exception {
// 1、获取code,类似于验证码
// 2、请求微信api得到access_token和openid
String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
// 拼接参数,发送请求
String accessTokenUrl = String.format(baseAccessTokenUrl, ConstantWxUtils.WX_OPEN_APP_ID, ConstantWxUtils.WX_OPEN_APP_SECRET, code);
String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);
// 转换json->map集合,获取其中的两个参数,使用json转换工具 Gson
Gson gson = new Gson();
HashMap accessTokenMap = gson.fromJson(accessTokenInfo, HashMap.class);
String access_token = (String) accessTokenMap.get("access_token");
String openid = (String) accessTokenMap.get("openid");
// 把扫描人信息添加数据库里面
// 判断数据表里面是否存在相同微信信息,根据openid判断
UcenterMember member = memberService.getOpenIdMember(openid);
if (member == null) {
// 3、拿着得到accsess_token 和 openid,再去请求微信提供固定的地址,获取到扫描人信息
// 访问微信的资源服务器,获取用户信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
// 拼接参数,发送请求
String userInfoUrl = String.format(baseUserInfoUrl, access_token, openid);
String userInfo = HttpClientUtils.get(userInfoUrl);
//转换并获取返回userinfo字符串扫描人信息
HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);
String nickname = (String) userInfoMap.get("nickname");//昵称
String headimgurl = (String) userInfoMap.get("headimgurl");//头像
// 创建扫描member对象
member = new UcenterMember();
member.setOpenid(openid);
member.setNickname(nickname);
member.setAvatar(headimgurl);
memberService.save(member);
}
// 4、使用jwt根据member对象生成token字符串
String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
// 5、最后:返回首页面,通过路径传递token字符串
return "redirect:http://localhost:3000?token=" + jwtToken;
}

service

1
2
3
4
5
6
7
8
// 根据openid查询
@Override
public UcenterMember getOpenIdMember(String openid) {
QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
wrapper.eq("openid", openid);
UcenterMember member = baseMapper.selectOne(wrapper);
return member;
}

前端

取地址栏中的带名称的参数值(而不是xx/xx/xx这种使用this.$route.params.xx)使用this.$route.query.xxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
created() {
// 获取url中的token
this.token = this.$route.query.token
if (this.token) {
this.wxLogin()
}
this.showInfo()
},
methods: {
// 微信登录后显示用户信息
wxLogin() {
// 把token放入cookie
cookieApi.set('guli_token', this.token, { domain: 'localhost' })
// 调用接口获取header中的token
loginApi.getLoginMemberInfo().then(result => {
// 5、根据token获取用户信息放入cookie中
cookieApi.set('guli_ucenter', JSON.stringify(result.data.data.member), {
domain: 'localhost'
})
})
// 跳转页面
this.$router.push({ path: '/' })
},
// 从cookie中获取用户信息
showInfo() {
var loginInfoStr = cookieApi.get('guli_ucenter')
// 把字符串转换为JSON格式
if (loginInfoStr) {
this.loginInfo = JSON.parse(loginInfoStr)
}
}
}

注意把进入微信扫码的href改为http://localhost:8160/api/ucenter/wx/login即可

评论




🧡💛💚💙💜🖤🤍