第八阶段模块二
前端门户系统
访问:http://edufront.lagou.com/
用户名:15510792995 密码:111111
页面不需要我们自己开发,使用提供的页面即可
运行项目 npm run serve
1、首页显示全部课程
Index.vue
<script>
import Header from "./Header/Header"; //顶部登录条
import Footer from "./Footer/index"; //顶部登录条
export default {
name: "Index",
components: {
Header,
Footer,
},
data() {
return {
activeName: "allLesson",
courseList:[] // 课程集合
};
},
created() {
this.getCourseList(); // 当组件创建完毕,就调用获取课程的方法
},
methods: {
changeCourseTab(tabName) {
this.classSelect = tabName;
sessionStorage && sessionStorage.setItem("courseTab", tabName);
},
gotoDetail() {
this.$router.push({ name: "Course", params: { courseid: 1 } });
},
getCourseList(){ // 去dubbo服务获取全部课程的数据
return this.axios
.get("http://localhost:8002/course/getAllCourse")
.then(result =>{
console.log(result);
this.courseList = result.data;
}).catch(error =>{
this.$message.error("获取课程信息失败!!!");
});
}
},
};
</script>
<template>
<div>
<Header></Header>
<div style="width: 850PX;display: inline-block; margin:0px auto;">
<el-tabs v-model="activeName">
<el-tab-pane label="选课" name="allLesson">
<ul class="course-ul-pc">
<!-- 课程信息展示开始 -->
<li class="course-li" v-for="(item,index) in courseList" :key="index">
<!-- 课程封面图 -->
<img
:src="item.courseImgUrl"
class="teacher-portrait hover-pointer"/>
<!-- 课程文字信息 -->
<div class="content-main">
<!-- 课程标题 -->
<div class="content-title hover-pointer">
<div
class="p-title"
style="text-align:left;"
@click="gotoDetail"
>
<span>
{{item.courseName}}
</span>
</div>
<!-- 作者和职称 -->
<p class="p-title-buy text-overflow">
<span class="p-author-span">
{{item.teacher.teacherName}}
</span>
<span class="p-author-line" />
<span class="p-author-span">
{{item.teacher.position}}
</span>
</p>
<p></p>
<!-- 课程简单描述 -->
<p class="p-describe" style="text-align:left;">
{{item.brief}}
</p>
</div>
<!-- 课程前两个章节信息 -->
<ul class="content-course" style="text-align:left;">
<!-- 章节1 免费试看,通常是第一章的前两节课-->
<li
class="content-course-lesson text-overflow"
style="width:300px"
v-for="(lesson,index) in item.courseSections[0].courseLessons.slice(0,2)" :key="index"
>
<!-- 免费试看图标 -->
<img
src="@/assets/course-list/free-course.png"
class="free-label hover-pointer"
/>
<span class="theme-span hover-pointer">
{{lesson.theme}}
</span>
</li>
</ul>
<!-- 价格信息 -->
<div class="content-price" style="text-align:left;">
<p class="content-price-p">
<span class="content-price-orange-sm">¥</span>
<span class="content-price-orange">{{item.discounts}}</span>
<span class="current-price">
<span class="current-price-unite">¥</span>
{{item.price}}
</span>
<span class="activity-name">成就自己</span>
<span class="content-price-buy">{{item.sales}}</span>
</p>
<div class="btn btn-green btn-offset">立即购买</div>
</div>
</div>
</li>
<!-- 课程信息结束 -->
</ul>
</el-tab-pane>
</el-tabs>
</div>
<Footer></Footer>
</div>
</template>
2、登录
<template>
<div class="header-pc-wrap">
<!-- 登录框 开始-->
<el-dialog
style="width:800px;margin:0px auto;"
title=""
:visible.sync="dialogFormVisible">
<el-form>
<el-form-item>
<h1 style="font-size:30px;color:#00B38A">拉勾</h1>
</el-form-item>
<el-form-item>
<el-input v-model="phone" placeholder="请输入常用手机号..."></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="password" placeholder="请输入密码..."></el-input>
</el-form-item>
</el-form>
<el-button
style="width:100%;margin:0px auto;background-color: #00B38A;font-size:20px"
type="primary"
@click="login">确 定</el-button>
<p></p>
<!-- 微信登录图标 -->
<img
@click="goToLoginWX"
src="http://www.lgstatic.com/lg-passport-fed/static/pc/modules/common/img/icon-wechat@2x_68c86d1.png"
alt=""
/>
</el-dialog>
<!-- 登录框 结束-->
<!-- 顶部登录条 -->
<div class="wrap-box">
<div @click="toToIndex" class="edu-icon"></div>
<div @click="toToIndex" class="text">拉勾教育</div>
<div class="right-var-wrap" v-if="!isLogin">
<div class="login-handler" @click="goToLogin">登录 | 注册</div>
</div>
<div class="right-var-wrap" v-if="isLogin">
<div
:class="{ 'tip-icon': true, 'has-new-message': isHasNewMessage }"
@click="toToNotic"
>
<i class="el-icon-bell"></i>
</div>
<img :src="userDTO.content.portrait" class="avatar-wrap" />
<div class="bar-wrap">
<ul class="account-bar" >
<li class="user_dropdown" data-lg-tj-track-code="index_user" >
<span class="unick">{{ userDTO.content.name }}</span>
<i />
<ul style="">
<li @click="goToSetting">
账号设置
</li>
<li @click="logout">
退出
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Header",
props: {},
data() {
return {
isLogin: false, // 登录状态,true:已登录,false:未登录
userDTO:null, // 用来保存登录的用户信息
isHasNewMessage: false, // 是否有新的推送消息
dialogFormVisible: false, // 是否显示登录框,true:显示,false:隐藏
phone: "", // 双向绑定表单 手机号
password: "", // 双向绑定表单 密码
};
},
computed: {
},
watch: {
},
mounted() {
},
created(){
// 当刷新页面,组件创建成功之后,立刻检测本地储存中是否存在用户对象
this.userDTO = JSON.parse(localStorage.getItem("user"));
if(this.userDTO != null){
this.isLogin = true; // 已登录
}
},
methods: {
goToSetting() {
this.$router.push("/setting"); // 跳转个人设置页面
},
goToLogin() {
this.dialogFormVisible = true; // 显示登录框
},
login(){ // 前去登录
return this.axios
.get("http://localhost:8002/user/login",{
params:{
phone:this.phone,
password:this.password
}
})
.then((result) =>{
console.log(result);
this.dialogFormVisible = false; // 关闭登录框
this.userDTO = result.data; // 保存返回数据中的用户对象信息
this.isLogin = true; // 更新登录状态
localStorage.setItem("user",JSON.stringify(this.userDTO)); // 将登录成功的对象信息保存到本地储存中
}).catch((error) =>{
this.$message.error("登录失败!!!");
});
},
goToLoginWX() {
alert("微信登录");
},
toToIndex() {
this.$router.push("/"); //回到首页
},
toToNotic(){
},
logout(){ // 登出
localStorage.setItem("user",null); // 将登录成功的对象信息保存到本地储存中
this.isLogin = false; // 更新登录状态
alert("谢谢使用,再见!!!");
}
},
};
</script>
3、已购课程
<script>
import Header from "./Header/Header"; //顶部登录条
import Footer from "./Footer/index"; //顶部登录条
export default {
name: "Index",
components: {
Header,
Footer,
},
data() {
return {
activeName: "allLesson",
courseList:[], // 课程集合
myCourseList:[], // 我购买过的课程列表
isLogin:false, // 登录状态
user:null, // 已登录的用户对象信息
};
},
created() {
this.user = JSON.parse(localStorage.getItem("user"));
if(this.user != null){
this.isLogin = true; // 已登录
this.getMyCourseList(); // 调用查询我购买的课程
}
this.getCourseList(); // 当组件创建完毕,就调用获取所有课程的方法
},
methods: {
changeCourseTab(tabName) {
this.classSelect = tabName;
sessionStorage && sessionStorage.setItem("courseTab", tabName);
},
gotoDetail() {
this.$router.push({ name: "Course", params: { courseid: 1 } });
},
getCourseList(){ // 去dubbo服务获取全部课程的数据
return this.axios
.get("http://localhost:8002/course/getAllCourse")
.then(result =>{
console.log(result);
this.courseList = result.data;
}).catch(error =>{
this.$message.error("获取课程信息失败!!!");
});
},
getMyCourseList(){ // 查询我购买的课程
return this.axios
.get("http://localhost:8002/course/getCourseByUserId/" + this.user.content.id)
.then(result =>{
console.log(result);
this.myCourseList = result.data;
}).catch(error =>{
this.$message.error("获取已购买课程信息失败!!!");
});
}
},
};
</script>
<el-tab-pane label="已购" name="hasPay">
<div v-if="!isLogin">
<img src="@/assets/course-list/no-login@2x.png" class="no-data-icon"/>
<div class="no-data-title">您还没有登录</div>
<div class="no-data-title">登录后即可查看已购课程</div>
<div class="btn btn-yellow btn-center">立即登录</div>
</div>
<div v-if="isLogin">
<ul class="course-ul-pc">
<!-- 课程信息展示开始 -->
<li class="course-li" v-for="(item,index) in myCourseList" :key="index">
<!-- 课程封面图 -->
<img
:src="item.courseImgUrl"
class="teacher-portrait hover-pointer"/>
<!-- 课程文字信息 -->
<div class="content-main">
<!-- 课程标题 -->
<div class="content-title hover-pointer">
<div
class="p-title"
style="text-align:left;"
@click="gotoDetail"
>
<span>
{{item.courseName}}
</span>
</div>
<!-- 作者和职称 -->
<p class="p-title-buy text-overflow">
<span class="p-author-span">
{{item.teacher.teacherName}}
</span>
<span class="p-author-line" />
<span class="p-author-span">
{{item.teacher.position}}
</span>
</p>
<p></p>
<!-- 课程简单描述 -->
<p class="p-describe" style="text-align:left;">
{{item.brief}}
</p>
</div>
<!-- 课程前两个章节信息 -->
<ul class="content-course" style="text-align:left;">
<!-- 章节1 免费试看,通常是第一章的前两节课-->
<li
class="content-course-lesson text-overflow"
style="width:300px"
v-for="(lesson,index) in item.courseSections[0].courseLessons.slice(0,2)" :key="index">
<!-- 免费试看图标 -->
<img src="@/assets/course-list/free-course.png" class="free-label hover-pointer"/>
<span class="theme-span hover-pointer">
{{lesson.theme}}
</span>
</li>
</ul>
<!-- 价格信息 -->
<div class="content-price" style="text-align:left;">
<p class="content-price-p">
<span class="content-price-orange-sm">¥</span>
<span class="content-price-orange">{{item.discounts}}</span>
<span class="current-price">
<span class="current-price-unite">¥</span>
{{item.price}}
</span>
<span class="activity-name">成就自己</span>
<span class="content-price-buy">{{item.sales}}</span>
</p>
<div class="btn btn-yellow btn-offset">好好学习</div>
</div>
</div>
</li>
<!-- 课程信息结束 -->
</ul>
</div>
</el-tab-pane>
4、课程详情-基本信息
Index.vue
<div
class="p-title"
style="text-align:left;"
@click="gotoDetail(item)"
>
<span>
{{item.courseName}}
</span>
</div>
<script>
methods: { // 将课程对象传递到课程详情组件
gotoDetail(item) {
this.$router.push({ name: "Course", params: { course: item } });
},
}
</script>
Course.vue
<script>
data() {
return {
activeName: "intro",
course:null,
};
},
created(){
this.course = this.$route.params.course; // 从路由中获得参数对象赋值给本组件的参数
},
</script>
<div class="intro-content">
<img class="course-img" :src="course.courseImgUrl" alt="课程图片"/>
<div class="conent-wrap">
<div class="name" style="text-align:left;">
{{course.courseName}}
</div>
<div class="des text-omit" style="text-align:left;">
{{course.brief}}
</div>
<div class="title">
<div class="teacher-name text-omit">
讲师:{{course.teacher.teacherName}}
<span class="line"></span>
{{course.teacher.position}}
</div>
</div>
<div class="lesson-info">
<div class="boook-icon backgroun-img-set"></div>
<div class="time">{{totalLessons}} 讲 / {{course.totalDuration}} 课时</div>
<div class="person-icon backgroun-img-set"></div>
<div class="person">{{course.sales}}人已购买</div>
</div>
</div>
</div>
5、课程详情-共计多少讲
<div class="time">{{totalLessons}} 讲 / {{course.totalDuration}} 课时</div>
<script>
data() {
return {
activeName: "intro",
course:null,
totalLessons:0, // 本门课程的总节数
};
},
created(){
this.course = this.$route.params.course; // 从路由中获得参数对象赋值给本组件的参数
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;
},
</script>
6、课程详情-实现课程信息描述
html不识别数据库的html标签,必须使用v-html才可以
<el-tab-pane label="课程信息" name="intro">
<div v-html="course.courseDescription" class="content-p pc-background"></div>
</el-tab-pane>
7、课程详情-显示章节目录
<div class="public-class-container is-pc">
<el-tabs v-model="activeName">
<el-tab-pane label="课程信息" name="intro">
<div v-html="course.courseDescription" class="content-p pc-background"></div>
</el-tab-pane>
<el-tab-pane label="目录" name="directory">
<div class="class-menu-contaniner list-page-container more-sections more-sections-padding">
<!-- 每章 开始 -->
<div v-for="(section , index) in course.courseSections" :key="index">
<div class="section-name single-line">
{{section.sectionName}}
</div>
<!-- 每节课 开始 -->
<div class="class-menu-block">
<div v-for="(lesson , index) in section.courseLessons" :key="index" class="class-level-one over-ellipsis" @click="watchCourse(1)">
<div class="text-wrap">
<div class="content">{{lesson.theme}}</div>
<div class="item-status-wrap item-status-wrap-list">
<div class="item-status test-watch">试看</div>
</div>
</div>
</div>
<!-- 每节课 结束 -->
</div>
</div>
<!-- 每章 结束 -->
</div>
</el-tab-pane>
</el-tabs>
</div>
8、课程详情-显示全部留言
<script>
data() {
return {
activeName: "intro",
course:null,
totalLessons:0, // 本门课程的总节数
commentList:null, // 所有留言
};
},
created(){
this.course = this.$route.params.course; // 从路由中获得参数对象赋值给本组件的参数
//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;
this.getComment();
},
methods: {
watchCourse(lessonid) {
alert("观看第【" + lessonid + "】节课程视频!");
this.$router.push({
name: "videoDetail",
params: { lessonid: lessonid },
});
},
buy(courseid) {
alert("购买第【" + courseid + "】门课程成功,加油!");
},
getComment(){
return this.axios
.get("http://localhost:8002/course/comment/getCourseCommentList/"+this.course.id+"/1/20")
.then(result =>{
console.log(result);
this.commentList = result.data;
}).catch(error =>{
this.$message.error("获取留言信息失败!!!");
});
},
},
</script>
<!-- 留言板 开始-->
<div class="message">
<div class="message-topic">
<div class="message-topic-title normal-font">精选留言</div>
</div>
<div>
<div class="message-edit">
<textarea rows="20" style="border:none;resize: none;"
contenteditable="true"
placeholder="分享学习心得、思考感悟或者给自己一个小鼓励吧!"
class="edit-div pcStyle"
></textarea>
<div class="message-edit-count">
<span class="message-edit-count-cur">0</span>
<span class="message-edit-count-max">/2000</span>
</div>
</div>
<div class="message-edit-footer flex">
<button class="message-edit-btn disableBg" >发表留言</button>
</div>
</div>
<!-- 留言 开始 -->
<div class="message-list" v-for="(comment , index) in commentList" :key="index">
<div class="message-list-title">
<div class="message-list-title-left">
<div class="message-list-title-left-name">{{comment.userName}}</div>
<div class="message-list-title-left-tag"></div>
</div>
<div class="message-list-title-right">
<img class="message-list-title-right-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4BAMAAABaqCYtAAAAJFBMVEVHcEwAuI4AtIsAtIsAtIoAtYwAtIsAtYwAtowAx5kAtIsAs4qd4c1kAAAAC3RSTlMAGMfz3VGnbTYIhXtDq8EAAAETSURBVDjLldWhb8JAFAbwg8CGhGSGzNQtULNkWUioWbJkpmYKAQaBITPLLKZysmLz+xfolUL6/XM72msh9N2X8ImaX17vrq+vVeqU39XTuK/kdEMA01jGDY4ZyYWFIRPxp0S8u+8KeBJ+WDxIGFrMJbQm7qhVYSJgr8JUwNsKtYDtCiHgPavcsDVDstsuyDk7NeYDk6G8pM3bWXNawQUijWq8QyPPpQy+50ET9bF09go5pus3cMV0feFEc2DfieZRBU7Up7fjSkwZZgz3DA8MHxl6DP8YRgxjgokimDHcMdwyfGG4ZPjJMCKoY4KJIpgz3DHcMvQYFuPpk/005t1mffHlOs/ETvxXAA0Ul3oQHsp/xD93wxfHcC4VkwAAAABJRU5ErkJggg==" alt="">
<div class="message-list-title-right-praise">{{comment.likeCount}}</div>
</div>
</div>
<div class="message-list-content">
{{comment.comment}}
</div>
</div>
<!-- 留言 结束 -->
</div>
<!-- 留言板 结束-->
9、课程详情-章节状态
<script>
import Header from "./Header/Header"; //顶部登录条
import Footer from "./Footer/index"; //顶部登录条
export default {
name: "Course",
components: {
Header,
Footer,
},
data() {
return {
activeName: "intro",
course:null,
totalLessons:0, // 本门课程的总节数
commentList:null, // 所有留言
isLogin:false, // 未登录
isBuy:false, // 未购买
user:null, // 当前用户
myCourseList:[], // 当前用户购买过的所有课程
};
},
created(){
this.course = this.$route.params.course; // 从路由中获得参数对象赋值给本组件的参数
// 检测是否登录
this.user = JSON.parse(localStorage.getItem("user"));
if(this.user != null){
this.isLogin = true; // 已登录
this.getMyCourseList(); // 查询登录用户购买的所有课程
}
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;
this.getComment();
},
methods: {
watchCourse(lessonid) {
alert("观看第【" + lessonid + "】节课程视频!");
this.$router.push({
name: "videoDetail",
params: { lessonid: lessonid },
});
},
buy(courseid) {
alert("购买第【" + courseid + "】门课程成功,加油!");
},
// 获取本课程的全部留言
getComment(){
return this.axios.get("http://localhost:8002/course/comment/getCourseCommentList/"+this.course.id+"/1/20")
.then(result =>{
console.log(result);
this.commentList = result.data;
}).catch(error =>{
this.$message.error("获取留言信息失败!!!");
});
},
// 查询当前用户购买的所有课程
getMyCourseList(){
return this.axios
.get("http://localhost:8002/course/getCourseByUserId/" + this.user.content.id)
.then((result) =>{
console.log(result);
this.myCourseList = result.data;
// 检测当前的课程是否购买过
for(let i = 0; i < this.myCourseList.length; i++){
if(this.myCourseList[i].id == this.course.id){
this.isBuy = true; // 标记购买过本课程
break;
}
}
}).catch(error =>{
this.$message.error("获取已购买课程信息失败!!!");
});
},
},
};
</script>
<el-tab-pane label="目录" name="directory">
<div class="class-menu-contaniner list-page-container more-sections more-sections-padding">
<!-- 第一章 开始 -->
<div v-for="section in course.courseSections.slice(0,1)">
<div class="section-name single-line">{{section.sectionName}}</div>
<!-- 每节课 开始 -->
<div class="class-menu-block">
<div
class="class-level-one over-ellipsis"
v-for="(lesson , index) in section.courseLessons" :key="index"
@click="watchCourse(1)">
<div class="text-wrap">
<div class="content">{{lesson.theme}}</div>
<div class="item-status-wrap item-status-wrap-list">
<!-- 第一章,前两节 -->
<div v-if="index<2">
<!-- 未登录 => 试看 -->
<div v-if="!isLogin" class="item-status test-watch">试看</div>
<!-- 已登录,未购买 => 试看 -->
<div v-else-if="isLogin && !isBuy" class="item-status test-watch">试看</div>
<!-- 已登录,已购买 => 播放 -->
<div v-else class="item-status test-watch">播放</div>
</div>
<!-- 第一章,除了前两节的 -->
<div v-if="index>1">
<!-- 未登录 => 锁 -->
<div v-if="!isLogin" class="item-status lock"></div>
<!-- 已登录,未购买 => 锁 -->
<div v-else-if="isLogin && !isBuy" class="item-status lock"></div>
<!-- 已登录,已购买 => 播放 -->
<div v-else class="item-status test-watch">播放</div>
</div>
</div>
</div>
</div>
<!-- 每节课 结束 -->
</div>
</div>
<!-- 第一章 结束 -->
<!-- 其余章 开始 -->
<div v-for="section in course.courseSections.slice(1,course.courseSections.length)">
<div class="section-name single-line">{{section.sectionName}}</div>
<!-- 每节课 开始 -->
<div class="class-menu-block">
<div
class="class-level-one over-ellipsis"
v-for="(lesson , index) in section.courseLessons" :key="index"
@click="watchCourse(1)">
<div class="text-wrap">
<div class="content">{{lesson.theme}}</div>
<div class="item-status-wrap item-status-wrap-list">
<!-- 未登录 => 锁 -->
<div v-if="!isLogin" class="item-status lock"></div>
<!-- 已登录,未购买 => 锁 -->
<div v-else-if="isLogin && !isBuy" class="item-status lock"></div>
<!-- 已登录,已购买 => 播放 -->
<div v-else class="item-status test-watch">播放</div>
</div>
</div>
</div>
<!-- 每节课 结束 -->
</div>
</div>
<!-- 其余章 结束 -->
</div>
</el-tab-pane>
10、在课程详情页点击视频播放
Course.vue
<el-tab-pane label="目录" name="directory">
<div class="class-menu-contaniner list-page-container more-sections more-sections-padding">
<!-- 第一章 开始 -->
<div v-for="section in course.courseSections.slice(0,1)">
<div class="section-name single-line">{{section.sectionName}}</div>
<!-- 每节课 开始 -->
<div class="class-menu-block">
<div
class="class-level-one over-ellipsis"
v-for="(lesson , index) in section.courseLessons" :key="index"
@click="watchCourse(1,lesson.id,index,lesson.courseMedia)">
<div class="text-wrap">
<div class="content">{{lesson.theme}}</div>
<div class="item-status-wrap item-status-wrap-list">
<!-- 第一章,前两节 -->
<div v-if="index<2">
<!-- 未登录 => 试看 -->
<div v-if="!isLogin" class="item-status test-watch">试看</div>
<!-- 已登录,未购买 => 试看 -->
<div v-else-if="isLogin && !isBuy" class="item-status test-watch">试看</div>
<!-- 已登录,已购买 => 播放 -->
<div v-else class="item-status test-watch">播放</div>
</div>
<!-- 第一章,除了前两节的 -->
<div v-if="index>1">
<!-- 未登录 => 锁 -->
<div v-if="!isLogin" class="item-status lock"></div>
<!-- 已登录,未购买 => 锁 -->
<div v-else-if="isLogin && !isBuy" class="item-status lock"></div>
<!-- 已登录,已购买 => 播放 -->
<div v-else class="item-status test-watch">播放</div>
</div>
</div>
</div>
</div>
<!-- 每节课 结束 -->
</div>
</div>
<!-- 第一章 结束 -->
<!-- 其余章 开始 -->
<div v-for="section in course.courseSections.slice(1,course.courseSections.length)">
<div class="section-name single-line">{{section.sectionName}}</div>
<!-- 每节课 开始 -->
<div class="class-menu-block">
<div
class="class-level-one over-ellipsis"
v-for="(lesson , index) in section.courseLessons" :key="index"
@click="watchCourse(2,lesson.id,index,lesson.courseMedia)">
<div class="text-wrap">
<div class="content">{{lesson.theme}}</div>
<div class="item-status-wrap item-status-wrap-list">
<!-- 未登录 => 锁 -->
<div v-if="!isLogin" class="item-status lock"></div>
<!-- 已登录,未购买 => 锁 -->
<div v-else-if="isLogin && !isBuy" class="item-status lock"></div>
<!-- 已登录,已购买 => 播放 -->
<div v-else class="item-status test-watch">播放</div>
</div>
</div>
</div>
<!-- 每节课 结束 -->
</div>
</div>
<!-- 其余章 结束 -->
</div>
</el-tab-pane>
<script>
methods: {
//播放视频 ( 章节,小节课编号,每节课的索引,每节课的视频对象)
watchCourse(status,lessonid,index,media) {
if(media == null || media == ""){
this.$message.error("播放失败,暂无视频!");
}else{
// 试看的可以跳转播放页面
if(status == 1 && index < 2){
this.$message.success("观看第【" + lessonid + "】节课程视频!");
this.$router.push({ name: "videoDetail", params: { course: this.course , lessonid:lessonid } });
}else{ // 锁上的,先验证是否登录
if(!this.isLogin){
this.$message.success("请先登录!");
}else{ // 登录后,再验证是否购买过
if(!this.isBuy){
this.$message.warning("请购买并解锁,才能观看本视频!");
}else{
this.$message.success("观看第【" + lessonid + "】节课程视频!");
this.$router.push({ name: "videoDetail", params: { course: this.course , lessonid:lessonid } });
}
}
}
}
},
}
</script>
videoDetail.vue
<script>
export default {
name: "videoDetail",
components: {},
data() {
return {
myvideo: null, // 播放器对象
isplay: false, //是否在播放
nowTime: "00:00", //当前播放时间
totalTime: "00:00", //总时长
course:null, // 课程
lessonid:0, // 当前播放视频的课节id
};
},
computed: {},
created() {
// 判断登录(暂无)
// 从上一级页面的请求中获得课程对象和小结编号
this.course = this.$route.params.course;
this.lessonid = this.$route.params.lessonid;
},
mounted() {
this.myvideo = document.getElementById("myvideo");
this.initplay(); // 初始化播放的视频
},
methods: {
// 初始化时播放的视频
initplay(){
// 1.在课程信息中查找即将播放的视频小节的编号
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++){
let lesson = section.courseLessons[j]; // 每节课
if(lesson.courseMedia != null){
if(this.lessonid == lesson.courseMedia.lessonId){
console.log("视频地址:" + lesson.courseMedia.fileEdk);
// 2.将小节视频的地址 赋值 给播放器,进行播放
this.myvideo.src = lesson.courseMedia.fileEdk;
return;
}
}
}
}
},
},
};
</script>
11、播放页的信息显示
<script>
export default {
name: "videoDetail",
components: {},
data() {
return {
myvideo: null, // 播放器对象
isplay: false, //是否在播放
nowTime: "00:00", //当前播放时间
totalTime: "00:00", //总时长
course:null, // 课程
lessonid:0, // 当前播放视频的课节id
lessonName:null, // 当前播放的视频名称
isLogin:false, // 未登录
isBuy:false, // 未购买
};
},
computed: {},
created() {
// 判断登录
this.user = JSON.parse(localStorage.getItem("user"));
if(this.user != null){
this.isLogin = true; // 已登录
}
// 从上一级页面的请求中获得课程对象和小结编号
this.course = this.$route.params.course;
this.lessonid = this.$route.params.lessonid;
this.isBuy = this.$route.params.isBuy;
},
mounted() {
this.myvideo = document.getElementById("myvideo");
this.initplay(); // 初始化播放的视频
},
methods: {
play() {
this.isplay = !this.isplay;
if (this.isplay) {
this.myvideo.play();
} else {
this.myvideo.pause();
}
},
// 获取视频的时间是秒为单位,格式化城00:00的格式
formatTime(time) {
let mm = Math.floor((time % 3600) / 60);
let ss = Math.floor(time % 60);
mm = mm < 10 ? "0" + mm : mm;
ss = ss < 10 ? "0" + ss : ss;
return `${mm}:${ss}`;
},
//获取初始化信息
getInit() {
if (!this.myvideo) {
//获取失败,显示0
this.totalTime = this.formatTime(0);
} else {
//获取视频总时长
this.totalTime = this.formatTime(this.myvideo.duration);
}
},
//播放时显示当前播放时间和总时长
handlerNowTime() {
if (!this.myvideo) {
this.nowTime = `${this.formatTime(0)}`;
}
this.nowTime = this.formatTime(this.myvideo.currentTime || 0);
},
//返回
goBack() {
this.$router.push({ name: "Course" });
},
//播放课程
playLesson() {
alert("播放视频!");
//this.myvideo.sec = mp4;
},
// 初始化时播放的视频
initplay(){
// 1.在课程信息中查找即将播放的视频小节的编号
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++){
let lesson = section.courseLessons[j]; // 每节课
if(lesson.courseMedia != null){
if(this.lessonid == lesson.courseMedia.lessonId){
console.log("视频地址:" + lesson.courseMedia.fileEdk);
this.lessonName = lesson.theme;
// 2.将小节视频的地址 赋值 给播放器,进行播放
this.myvideo.src = lesson.courseMedia.fileEdk;
return;
}
}
}
}
},
},
};
</script>
<div class="content-container">
<!-- 第一章 开始 -->
<div v-for="section in course.courseSections.slice(0,1)">
<div class="content-label">
<div class="content-label-title single-line">{{section.sectionName}}</div>
<img
class="arrow-icon"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkBAMAAAATLoWrAAAAGFBMVEVHcEz///8dS1W+AAAAB3RSTlMAgwWyMeGgBsFrrQAAAFVJREFUKM9jYBiugMUBQ0i8EF2EsbxcAF1ReXkhuqJiczRl4uVGyqjKgIoUmFCVARUxMKAoAyliYEBRBlaEqiwcpAikrBShKglCqyFUMcEYCgwjBAAAeaoQrHtg6QoAAAAASUVORK5CYII="
alt=""
/>
</div>
<div class="content-sections">
<!-- 未播放的视频 -->
<div class="content-section" v-for="(lesson,index) in section.courseLessons" :key="index">
<!-- 第一章,前两节 -->
<div v-if="index < 2">
<div class="section-item clearfix">
<span class="kw-icon-video section-type-icon fl"><i class="el-icon-video-play"></i></span>
<span class="section-dec need-update">{{lesson.theme}}</span>
<span class="section-status-icon pause-play">试看</span>
</div>
<div class="section-duration">
<span v-if="lesson.courseMedia !=null">时长:{{lesson.courseMedia.duration}}</span>
<span v-else="lesson.courseMedia ==null">时长:无媒体文件</span>
</div>
</div>
<div v-if="index > 1">
<div class="section-item clearfix">
<span class="kw-icon-video section-type-icon fl"><i class="el-icon-video-play"></i></span>
<span class="section-dec need-update">{{lesson.theme}}</span>
<!-- 未登录 => 锁 -->
<span v-if="!isLogin" class="section-status-icon pause-play">未解锁</span>
<!-- 已登录,未购买 => 锁 -->
<span v-else-if="isLogin && !isBuy" class="section-status-icon pause-play">未解锁</span>
<!-- 已登录,已购买 => 播放 -->
<span v-else class="section-status-icon pause-play">播放</span>
</div>
<div class="section-duration">
<span v-if="lesson.courseMedia !=null">时长:{{lesson.courseMedia.duration}}</span>
<span v-else="lesson.courseMedia ==null">时长:无媒体文件</span>
</div>
</div>
</div>
</div>
</div>
<!-- 第一章 结束 -->
<!-- 其余章 开始 -->
<div v-for="section in course.courseSections.slice(1,course.courseSections.length)">
<div class="content-label">
<div class="content-label-title single-line">{{section.sectionName}}</div>
<img
class="arrow-icon"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkBAMAAAATLoWrAAAAGFBMVEVHcEz///8dS1W+AAAAB3RSTlMAgwWyMeGgBsFrrQAAAFVJREFUKM9jYBiugMUBQ0i8EF2EsbxcAF1ReXkhuqJiczRl4uVGyqjKgIoUmFCVARUxMKAoAyliYEBRBlaEqiwcpAikrBShKglCqyFUMcEYCgwjBAAAeaoQrHtg6QoAAAAASUVORK5CYII="
alt=""
/>
</div>
<div class="content-sections">
<!-- 未播放的视频 -->
<div class="content-section" v-for="(lesson,index) in section.courseLessons" :key="index">
<div>
<div class="section-item clearfix">
<span class="kw-icon-video section-type-icon fl"><i class="el-icon-video-play"></i></span>
<span class="section-dec need-update">{{lesson.theme}}</span>
<!-- 未登录 => 锁 -->
<span v-if="!isLogin" class="section-status-icon pause-play">未解锁</span>
<!-- 已登录,未购买 => 锁 -->
<span v-else-if="isLogin && !isBuy" class="section-status-icon pause-play">未解锁</span>
<!-- 已登录,已购买 => 播放 -->
<span v-else class="section-status-icon pause-play">播放</span>
</div>
<div class="section-duration">
<span v-if="lesson.courseMedia !=null">时长:{{lesson.courseMedia.duration}}</span>
<span v-else="lesson.courseMedia ==null">时长:无媒体文件</span>
</div>
</div>
</div>
</div>
</div>
<!-- 其余章 结束 -->
</div>
12、当前播放的视频高亮突出
<div class="content-container">
<!-- 第一章 开始 -->
<div v-for="section in course.courseSections.slice(0,1)">
<div class="content-label">
<div class="content-label-title single-line">{{section.sectionName}}</div>
<img
class="arrow-icon"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkBAMAAAATLoWrAAAAGFBMVEVHcEz///8dS1W+AAAAB3RSTlMAgwWyMeGgBsFrrQAAAFVJREFUKM9jYBiugMUBQ0i8EF2EsbxcAF1ReXkhuqJiczRl4uVGyqjKgIoUmFCVARUxMKAoAyliYEBRBlaEqiwcpAikrBShKglCqyFUMcEYCgwjBAAAeaoQrHtg6QoAAAAASUVORK5CYII="
alt=""
/>
</div>
<!-- 第一章 开始 -->
<div class="content-sections">
<div :class="{
'content-section': lesson.id != lessonid,
'content-section content-section-choose': lesson.id == lessonid,
}"
v-for="(lesson,index) in section.courseLessons" :key="index">
<!-- 第一章,前两节 -->
<div v-if="index < 2">
<div class="section-item clearfix">
<span :class="{
'kw-icon-video section-type-icon': lesson.id != lessonid,
'kw-icon-video section-type-icon lv': lesson.id == lessonid,
}"><i class="el-icon-video-play"></i></span>
<span :class="{
'section-dec': lesson.id != lessonid,
'section-dec lv': lesson.id == lessonid,
}">{{lesson.theme}}</span>
<span v-if="lesson.id != lessonid" class="section-status-icon pause-play">试看</span>
<span v-else class="section-status-icon pause-play"></span>
</div>
<div class="section-duration">
<span v-if="lesson.courseMedia !=null">时长:{{lesson.courseMedia.duration}}</span>
<span v-else="lesson.courseMedia ==null">时长:无媒体文件</span>
</div>
</div>
<div v-if="index > 1">
<div class="section-item clearfix">
<span :class="{
'kw-icon-video section-type-icon':lesson.id != lessonid,
'kw-icon-video section-type-icon lv':lesson.id == lessonid,
}"><i class="el-icon-video-play"></i></span>
<span :class="{
'section-dec': lesson.id != lessonid,
'section-dec lv': lesson.id == lessonid,
}">{{lesson.theme}}</span>
<!-- 未登录 => 锁 -->
<span v-if="!isLogin" class="section-status-icon pause-play">未解锁</span>
<!-- 已登录,未购买 => 锁 -->
<span v-else-if="isLogin && !isBuy" class="section-status-icon pause-play">未解锁</span>
<!-- 已登录,已购买 => 播放 -->
<span v-else-if="lesson.id != lessonid" class="section-status-icon pause-play">播放</span>
<span v-else-if="lesson.id == lessonid" class="section-status-icon pause-play"></span>
</div>
<div class="section-duration">
<span v-if="lesson.courseMedia !=null">时长:{{lesson.courseMedia.duration}}</span>
<span v-else="lesson.courseMedia ==null">时长:无媒体文件</span>
</div>
</div>
</div>
</div>
</div>
<!-- 第一章 结束 -->
<!-- 其余章 开始 -->
<div v-for="section in course.courseSections.slice(1,course.courseSections.length)">
<div class="content-label">
<div class="content-label-title single-line">{{section.sectionName}}</div>
<img
class="arrow-icon"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkBAMAAAATLoWrAAAAGFBMVEVHcEz///8dS1W+AAAAB3RSTlMAgwWyMeGgBsFrrQAAAFVJREFUKM9jYBiugMUBQ0i8EF2EsbxcAF1ReXkhuqJiczRl4uVGyqjKgIoUmFCVARUxMKAoAyliYEBRBlaEqiwcpAikrBShKglCqyFUMcEYCgwjBAAAeaoQrHtg6QoAAAAASUVORK5CYII="
alt=""
/>
</div>
<div class="content-sections">
<!-- 未播放的视频 -->
<div :class="{
'content-section':lesson.id != lessonid,
'content-section content-section-choose':lesson.id == lessonid,
}" v-for="(lesson,index) in section.courseLessons" :key="index">
<div>
<div class="section-item clearfix">
<span :class="{
'kw-icon-video section-type-icon':lesson.id != lessonid,
'kw-icon-video section-type-icon lv':lesson.id == lessonid,
}"><i class="el-icon-video-play"></i></span>
<span :class="{
'section-dec': lesson.id != lessonid,
'section-dec lv': lesson.id == lessonid,
}">{{lesson.theme}}</span>
<!-- 未登录 => 锁 -->
<span v-if="!isLogin" class="section-status-icon pause-play">未解锁</span>
<!-- 已登录,未购买 => 锁 -->
<span v-else-if="isLogin && !isBuy" class="section-status-icon pause-play">未解锁</span>
<!-- 已登录,已购买 => 播放 -->
<span v-else-if="lesson.id != lessonid" class="section-status-icon pause-play">播放</span>
<span v-else-if="lesson.id == lessonid" class="section-status-icon pause-play"></span>
</div>
<div class="section-duration">
<span v-if="lesson.courseMedia !=null">时长:{{lesson.courseMedia.duration}}</span>
<span v-else="lesson.courseMedia ==null">时长:无媒体文件</span>
</div>
</div>
</div>
</div>
</div>
<!-- 其余章 结束 -->
</div>
13、点击课标题切换视频并播放
<div :class="{
'content-section': lesson.id != lessonid,
'content-section content-section-choose': lesson.id == lessonid,
}"
v-for="(lesson,index) in section.courseLessons"
:key="index"
@click="playLesson(lesson)"
>
<script>
methods: {
//播放课程
playLesson(status,index,lesson) {
if(lesson.courseMedia == null){
this.$message.error("播放失败,暂无视频!");
}else{
// 试看的可以跳转播放页面
if(status == 1 && index < 2){
this.playNow(lesson);
}else{ // 锁上的,先验证是否登录
if(!this.isLogin){
this.$message.success("请先登录!");
}else{ // 登录后,再验证是否购买过
if(!this.isBuy){
this.$message.warning("请购买并解锁,才能观看本视频!");
}else{
this.playNow(lesson);
}
}
}
}
},
// 立刻播放
playNow(lesson){
this.lessonid = lesson.id; // 当前播放的视频就是我点击的课
this.myvideo.src = lesson.courseMedia.fileEdk; // 切换播放器的播放地址
this.myvideo.play(); // 立刻播放
this.isplay = true; // 按钮呈现为播放状态
},
};
</script>
14、留言点赞
14.1 修改dao
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class CourseComment implements Serializable {
private static final long serialVersionUID = 922554392538715061L;
// 一条留言:N个点赞
private List<CourseCommentFavoriteRecord> favoriteRecords;
}
<?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="mapper.CourseCommentDao">
<resultMap type="com.lagou.entity.CourseComment" id="commentMap">
<result property="id" column="cc_id" jdbcType="VARCHAR"/>
<result property="courseId" column="course_id" jdbcType="INTEGER"/>
<result property="sectionId" column="section_id" jdbcType="INTEGER"/>
<result property="lessonId" column="lesson_id" jdbcType="INTEGER"/>
<result property="userId" column="cc_user_id" jdbcType="INTEGER"/>
<result property="userName" column="user_name" jdbcType="VARCHAR"/>
<result property="parentId" column="parent_id" jdbcType="INTEGER"/>
<result property="isTop" column="is_top" jdbcType="INTEGER"/>
<result property="comment" column="comment" jdbcType="VARCHAR"/>
<result property="likeCount" column="like_count" jdbcType="INTEGER"/>
<result property="isReply" column="is_reply" jdbcType="INTEGER"/>
<result property="type" column="type" jdbcType="INTEGER"/>
<result property="status" column="status" jdbcType="INTEGER"/>
<result property="createTime" column="cc_create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="cc_update_time" jdbcType="TIMESTAMP"/>
<result property="isDel" column="cc_is_del" jdbcType="INTEGER"/>
<result property="lastOperator" column="last_operator" jdbcType="INTEGER"/>
<result property="isNotify" column="is_notify" jdbcType="INTEGER"/>
<result property="markBelong" column="mark_belong" jdbcType="INTEGER"/>
<result property="replied" column="replied" jdbcType="INTEGER"/>
<!--N个点赞-->
<collection property="favoriteRecords" ofType="com.lagou.entity.CourseCommentFavoriteRecord">
<result property="id" column="ccfr_id" jdbcType="OTHER"/>
<result property="userId" column="ccfr_user_id" jdbcType="INTEGER"/>
<result property="commentId" column="comment_id" jdbcType="INTEGER"/>
<result property="isDel" column="ccfr_is_del" jdbcType="OTHER"/>
<result property="createTime" column="ccfr_create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="ccfr_update_time" jdbcType="TIMESTAMP"/>
</collection>
</resultMap>
<!--查询某门课程的全部留言(分页)-->
<select id="getCommentByCourseId" resultMap="commentMap">
select
cc.id cc_id,`course_id`,`section_id`,`lesson_id`,cc.user_id cc_user_id,`user_name`,`parent_id`,`is_top`,`comment`,`like_count`,`is_reply`,`type`,`status`,cc.create_time cc_create_time,cc.update_time cc_update_time,cc.is_del cc_id_del,`last_operator`,`is_notify`,`mark_belong`,`replied`,
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
from course_comment cc left join course_comment_favorite_record ccfr
on cc.id = ccfr.comment_id
where cc.is_del = 0
and course_id = #{courseId}
order by is_top desc , like_count desc , cc.create_time desc
limit #{offset}, #{pageSize}
</select>
</mapper>
@Test
public void getComment(){
List<CourseComment> list = courseCommentDao.getCommentByCourseId(7, 1, 20);
for (CourseComment comment : list){
System.out.println("【"+ comment.getUserName() + "】" + "留言:" + comment.getComment());
for (CourseCommentFavoriteRecord fr : comment.getFavoriteRecords()) {
System.out.println("---->" + fr.getId());
}
}
}
14.2 修改controller
@GetMapping("comment/getCourseCommentList/{courseId}/{pageIndex}/{pageSize}")
public List<CourseComment> getCommentByCourseId(@PathVariable("courseId") Integer courseId,@PathVariable("pageIndex") Integer pageIndex,@PathVariable("pageSize") Integer pageSize){
int offset = (pageIndex-1)*pageSize;
List<CourseComment> list = commentService.getCommentByCourseId(courseId, offset, pageSize);
System.out.println("获取第" + courseId +"门课程的留言:共计" + list.size() + "条");
return list;
}
14.3 页面点赞之后的显示样式
<!-- 留言板 开始-->
<div class="message">
<div class="message-topic">
<div class="message-topic-title normal-font">精选留言</div>
</div>
<div>
<div class="message-edit">
<textarea rows="20" style="border:none;resize: none;"
contenteditable="true"
placeholder="分享学习心得、思考感悟或者给自己一个小鼓励吧!"
class="edit-div pcStyle"
v-model="comment"
></textarea>
</div>
</div>
<div class="message-edit-footer flex">
<button class="message-edit-btn disableBg" >发表留言</button>
</div>
<!-- 留言 开始 -->
<div class="message-list" v-for="(comment , index) in commentList" :key="index">
<div class="message-list-title">
<div class="message-list-title-left">
<div class="message-list-title-left-name">{{comment.userName}}</div>
<div class="message-list-title-left-tag"></div>
</div>
<!-- {{JSON.stringify(comment.favoriteRecords).indexOf( "100030017" )}} -->
<!-- 已赞 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4BAMAAABaqCYtAAAAJFBMVEVHcEwAuI4AtIsAtIsAtIoAtYwAtIsAtYwAtowAx5kAtIsAs4qd4c1kAAAAC3RSTlMAGMfz3VGnbTYIhXtDq8EAAAETSURBVDjLldWhb8JAFAbwg8CGhGSGzNQtULNkWUioWbJkpmYKAQaBITPLLKZysmLz+xfolUL6/XM72msh9N2X8ImaX17vrq+vVeqU39XTuK/kdEMA01jGDY4ZyYWFIRPxp0S8u+8KeBJ+WDxIGFrMJbQm7qhVYSJgr8JUwNsKtYDtCiHgPavcsDVDstsuyDk7NeYDk6G8pM3bWXNawQUijWq8QyPPpQy+50ET9bF09go5pus3cMV0feFEc2DfieZRBU7Up7fjSkwZZgz3DA8MHxl6DP8YRgxjgokimDHcMdwyfGG4ZPjJMCKoY4KJIpgz3DHcMvQYFuPpk/005t1mffHlOs/ETvxXAA0Ul3oQHsp/xD93wxfHcC4VkwAAAABJRU5ErkJggg== -->
<div v-if="JSON.stringify(comment.favoriteRecords).indexOf( user.content.id ) >= 0" class="message-list-title-right">
<img class="message-list-title-right-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4BAMAAABaqCYtAAAAJFBMVEVHcEwAuI4AtIsAtIsAtIoAtYwAtIsAtYwAtowAx5kAtIsAs4qd4c1kAAAAC3RSTlMAGMfz3VGnbTYIhXtDq8EAAAETSURBVDjLldWhb8JAFAbwg8CGhGSGzNQtULNkWUioWbJkpmYKAQaBITPLLKZysmLz+xfolUL6/XM72msh9N2X8ImaX17vrq+vVeqU39XTuK/kdEMA01jGDY4ZyYWFIRPxp0S8u+8KeBJ+WDxIGFrMJbQm7qhVYSJgr8JUwNsKtYDtCiHgPavcsDVDstsuyDk7NeYDk6G8pM3bWXNawQUijWq8QyPPpQy+50ET9bF09go5pus3cMV0feFEc2DfieZRBU7Up7fjSkwZZgz3DA8MHxl6DP8YRgxjgokimDHcMdwyfGG4ZPjJMCKoY4KJIpgz3DHcMvQYFuPpk/005t1mffHlOs/ETvxXAA0Ul3oQHsp/xD93wxfHcC4VkwAAAABJRU5ErkJggg==" alt="">
<div class="message-list-title-right-praise">{{comment.likeCount}}</div>
</div>
<!-- 没点过赞 -->
<div v-else class="message-list-title-right">
<img class="message-list-title-right-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4BAMAAABaqCYtAAAAKlBMVEVHcExnZ2dzc3NmZmZqampmZmZmZmZnZ2dnZ2dnZ2dmZmZoaGhmZmZmZmZl+8SAAAAADXRSTlMA/AbsFtilbj5YwSqJPyESoQAAAZxJREFUOMt1lTtLA1EQha8xRhPTBEmhuKCCoNgoIlYLMcRKBG0sxIUgCDaBSDohEO0FEbQyIBZaBazERvAPWCwxPnP+i3tnrlGTmVPswn73NXNm7hrzq9m9kZ2ckTUUABifkOEBrK7liR7BMRFOA/uFc+BUgnV8mFisEW5IsIFi9FzBuwR91KJnAm8S9EIbxSBeBRZHk86MrBQJWjymJUC3nlugSyk+SQyhANfxos+s4krfM0DZvmbw2cuSCHNGi3PAfUygXYiU79ryyw1ibf0xZ9intBsz6SBadx24iiZXz8kPxCiTtYdLPzKTVFkkLQAZO/VikwYW/x/wHohcT/MiPQE8W9frxJrlbpiw4xvA0vbNmWyhj2Nrhmy+B7nEyTsN0rIaJAc0SDWqwX7rhAYfMa/Dui0bDZbwZAwUGNjWUWActnUUyN2hwDTaOkxRaSiwj6pRhjHKgTazSkWlwBK1jgIpBwrkHCgwyZ0oQ86BAjkHCjziG0KE8YBvCA/5KacOm6sgrHFAotouT6J23bkkLbsNDjM9yt7yP+IbQYga5De+eBMAAAAASUVORK5CYII=" alt="">
<div class="message-list-title-right-praise">{{comment.likeCount}}</div>
</div>
</div>
<div class="message-list-content">
{{comment.comment}}
</div>
<!--删除留言(必须登录才能删除自己的)-->
<!--
<div class="message-delete pointer">
<img class="message-delete-icon" src="https://img-blog.csdnimg.cn/2022010702583723148.png" alt="">删除
</div>
-->
</div>
<!-- 留言 结束 -->
</div>
<!-- 留言板 结束-->
14.4 页面调用点赞和取消赞
<!-- 留言板 开始-->
<div class="message">
<div class="message-topic">
<div class="message-topic-title normal-font">精选留言</div>
</div>
<div>
<div class="message-edit">
<textarea rows="20" style="border:none;resize: none;"
contenteditable="true"
placeholder="分享学习心得、思考感悟或者给自己一个小鼓励吧!"
class="edit-div pcStyle"
v-model="comment"
></textarea>
</div>
</div>
<div class="message-edit-footer flex">
<button class="message-edit-btn disableBg" >发表留言</button>
</div>
<!-- 留言 开始 -->
<div class="message-list" v-for="(comment , index) in commentList" :key="index">
<div class="message-list-title">
<div class="message-list-title-left">
<div class="message-list-title-left-name">{{comment.userName}}</div>
<div class="message-list-title-left-tag"></div>
</div>
<!-- {{JSON.stringify(comment.favoriteRecords).indexOf( "100030017" )}} -->
<!-- 已赞 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4BAMAAABaqCYtAAAAJFBMVEVHcEwAuI4AtIsAtIsAtIoAtYwAtIsAtYwAtowAx5kAtIsAs4qd4c1kAAAAC3RSTlMAGMfz3VGnbTYIhXtDq8EAAAETSURBVDjLldWhb8JAFAbwg8CGhGSGzNQtULNkWUioWbJkpmYKAQaBITPLLKZysmLz+xfolUL6/XM72msh9N2X8ImaX17vrq+vVeqU39XTuK/kdEMA01jGDY4ZyYWFIRPxp0S8u+8KeBJ+WDxIGFrMJbQm7qhVYSJgr8JUwNsKtYDtCiHgPavcsDVDstsuyDk7NeYDk6G8pM3bWXNawQUijWq8QyPPpQy+50ET9bF09go5pus3cMV0feFEc2DfieZRBU7Up7fjSkwZZgz3DA8MHxl6DP8YRgxjgokimDHcMdwyfGG4ZPjJMCKoY4KJIpgz3DHcMvQYFuPpk/005t1mffHlOs/ETvxXAA0Ul3oQHsp/xD93wxfHcC4VkwAAAABJRU5ErkJggg== -->
<div @click="cancelzan(comment)" v-if="JSON.stringify(comment.favoriteRecords).indexOf( user.content.id ) >= 0" class="message-list-title-right">
<img class="message-list-title-right-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4BAMAAABaqCYtAAAAJFBMVEVHcEwAuI4AtIsAtIsAtIoAtYwAtIsAtYwAtowAx5kAtIsAs4qd4c1kAAAAC3RSTlMAGMfz3VGnbTYIhXtDq8EAAAETSURBVDjLldWhb8JAFAbwg8CGhGSGzNQtULNkWUioWbJkpmYKAQaBITPLLKZysmLz+xfolUL6/XM72msh9N2X8ImaX17vrq+vVeqU39XTuK/kdEMA01jGDY4ZyYWFIRPxp0S8u+8KeBJ+WDxIGFrMJbQm7qhVYSJgr8JUwNsKtYDtCiHgPavcsDVDstsuyDk7NeYDk6G8pM3bWXNawQUijWq8QyPPpQy+50ET9bF09go5pus3cMV0feFEc2DfieZRBU7Up7fjSkwZZgz3DA8MHxl6DP8YRgxjgokimDHcMdwyfGG4ZPjJMCKoY4KJIpgz3DHcMvQYFuPpk/005t1mffHlOs/ETvxXAA0Ul3oQHsp/xD93wxfHcC4VkwAAAABJRU5ErkJggg==" alt="">
<div class="message-list-title-right-praise">{{comment.likeCount}}</div>
</div>
<!-- 没点过赞 -->
<div @click="zan(comment)" v-else class="message-list-title-right">
<img class="message-list-title-right-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4BAMAAABaqCYtAAAAKlBMVEVHcExnZ2dzc3NmZmZqampmZmZmZmZnZ2dnZ2dnZ2dmZmZoaGhmZmZmZmZl+8SAAAAADXRSTlMA/AbsFtilbj5YwSqJPyESoQAAAZxJREFUOMt1lTtLA1EQha8xRhPTBEmhuKCCoNgoIlYLMcRKBG0sxIUgCDaBSDohEO0FEbQyIBZaBazERvAPWCwxPnP+i3tnrlGTmVPswn73NXNm7hrzq9m9kZ2ckTUUABifkOEBrK7liR7BMRFOA/uFc+BUgnV8mFisEW5IsIFi9FzBuwR91KJnAm8S9EIbxSBeBRZHk86MrBQJWjymJUC3nlugSyk+SQyhANfxos+s4krfM0DZvmbw2cuSCHNGi3PAfUygXYiU79ryyw1ibf0xZ9intBsz6SBadx24iiZXz8kPxCiTtYdLPzKTVFkkLQAZO/VikwYW/x/wHohcT/MiPQE8W9frxJrlbpiw4xvA0vbNmWyhj2Nrhmy+B7nEyTsN0rIaJAc0SDWqwX7rhAYfMa/Dui0bDZbwZAwUGNjWUWActnUUyN2hwDTaOkxRaSiwj6pRhjHKgTazSkWlwBK1jgIpBwrkHCgwyZ0oQ86BAjkHCjziG0KE8YBvCA/5KacOm6sgrHFAotouT6J23bkkLbsNDjM9yt7yP+IbQYga5De+eBMAAAAASUVORK5CYII=" alt="">
<div class="message-list-title-right-praise">{{comment.likeCount}}</div>
</div>
</div>
<div class="message-list-content">
{{comment.comment}}
</div>
<!--删除留言(必须登录才能删除自己的)-->
<!--
<div class="message-delete pointer">
<img class="message-delete-icon" src="https://img-blog.csdnimg.cn/2022010702583723148.png" alt="">删除
</div>
-->
</div>
<!-- 留言 结束 -->
</div>
<!-- 留言板 结束-->
<script>
methods: {
// 点赞
zan(comment){
return this.axios
.get("http://localhost:8002/course/comment/saveFavorite/" + comment.id + "/" + this.user.content.id)
.then((result) =>{
// console.log(result);
// 重新获取本门课的全部留言信息
this.getComment();
}).catch(error =>{
this.$message.error("点赞失败!!!");
});
},
// 取消赞
cancelzan(comment){
return this.axios
.get("http://localhost:8002/course/comment/cancelFavorite/" + comment.id + "/" + this.user.content.id)
.then((result) =>{
// console.log(result);
// 重新获取本门课的全部留言信息
this.getComment();
}).catch(error =>{
this.$message.error("取消赞失败!!!");
});
},
},
};
</script>
<!--查询某门课程的全部留言(分页)-->
<select id="getCommentByCourseId" resultMap="commentMap">
select
cc.id cc_id,`course_id`,`section_id`,`lesson_id`,cc.user_id cc_user_id,`user_name`,`parent_id`,`is_top`,`comment`,`like_count`,`is_reply`,`type`,`status`,cc.create_time cc_create_time,cc.update_time cc_update_time,cc.is_del cc_id_del,`last_operator`,`is_notify`,`mark_belong`,`replied`,
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
from course_comment cc left join course_comment_favorite_record ccfr
on cc.id = ccfr.comment_id
where cc.is_del = 0
and ccfr.is_del = 0 <!-- 点赞表也要过滤掉被删除的数据 -->
and course_id = #{courseId}
order by is_top desc , like_count desc , cc.create_time desc
limit #{offset}, #{pageSize}
</select>
15、发表留言
Course.vue
<script>
import Header from "./Header/Header"; //顶部登录条
import Footer from "./Footer/index"; //顶部登录条
export default {
name: "Course",
components: {
Header,
Footer,
},
data() {
return {
activeName: "intro",
course:null,
totalLessons:0, // 本门课程的总节数
commentList:null, // 所有留言
isLogin:false, // 未登录
isBuy:false, // 未购买
user:null, // 当前用户
myCourseList:[], // 当前用户购买过的所有课程
comment:null, // 待发表的留言内容
};
},
methods: {
// 发表留言
saveComment(){
return this.axios
.get("http://localhost:8002/course/comment/saveCourseComment",{
params:{
courseid:this.courseid,
userid:this.user.content.id,
userName:this.user.content.name,
comment:this.comment,
}
})
.then((result) =>{
// console.log(result);
// 重新获取本门课的全部留言信息
this.getComment();
}).catch(error =>{
this.$message.error("发表留言失败!!!");
});
},
};
</script>
web消费方
public interface CommentService {
/**
* 保存留言
* @param comment 留言内容对象
* @return 受影响的行数
*/
Integer saveComment(CourseComment comment);
}
@RestController
@RequestMapping("course")
public class CommentController {
@Reference // 远程消费
private CommentService commentService;
@GetMapping("comment/saveCourseComment")
public Object saveCourseComment(Integer courseId,Integer userId,String userName,String comment) throws UnsupportedEncodingException {
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); // 章节编号 (预留字段,为项目的2.0版本保留)
courseComment.setLessonId(0); // 小节编号 (预留字段,为项目的2.0版本保留)
courseComment.setUserId(userId); // 用户编号
courseComment.setUserName(userName); // 用户昵称
courseComment.setParentId(0); // 没有父id (预留字段,为项目的2.0版本保留)
courseComment.setComment(comment); // 留言内容
courseComment.setType(0); // 0用户留言 (预留字段,为项目的2.0版本保留)
courseComment.setLastOperator(userId); // 最后操作的用户编号
Integer i = commentService.saveComment(courseComment);
System.out.println(i);
return i;
}
}
service提供方
<!--保存留言-->
<insert id="saveComment">
insert into `course_comment`(`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`) values
(#{courseId},#{sectionId},#{lessonId},#{userId},#{userName},#{parentId},0,#{comment},0,0,#{type},0,sysdate(),sysdate(),0,#{lastOperator},1,0,0)
</insert>
微信登录-前后端
微信开放平台(针对开发者和公司) https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
1、准备工作
网站应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统。
在进行微信OAuth2.0授权登录接入之前,在微信开放平台注册开发者帐号,并拥有一个已审核通过的网站应用,并获得相应的AppID和AppSecret,申请微信登录且通过审核后,可开始接入流程。
注册帐号和申请应用都是免费的,必须要有一个线上的网站,才能审核通过(过程还是挺麻烦的),就可以使用微信的登录了
但是如果想使用微信支付的功能,就必须认证开发者资质(认证一次300块人民币)
2、名词解释
2.1 OAuth2.0协议
OAuth(Open Authorization)协议就是为用户资源的授权提供了一个安全、开放、简易的标准。
OAuth在第三方应用与服务提供商之间设置了一个授权层,第三方应用通过授权层获取令牌,再通过令牌获取信息。
比如:皇宫大院,并不是每一块区域你都可以去溜达的。一个小奴才专门负责打扫后宫的寝室卫生,后宫门口有N多带刀侍卫,每次进门工作。都要问皇上,因为想进入到后宫内院,只有皇帝一个人说的算,皇帝让谁进,谁才能进。但是每次问皇上呢,又太累,所以“令牌”出现了,皇上命人制作了一些令牌,给打扫卫生的小太监每人一个,想进去,出示令牌给侍卫即可。这就是“宫廷版Oauth协议”。
玩抖音,发视频,抖音需要访问你相册的授权,话筒的授权,地理位置的授权等等
一句话,我不想帐号密码给第三方应用,但我还想用他们的功能,而他们的功能需要我的部分数据来协助。ok,咱玩令牌。
令牌与密码的作用都可以进入系统,但是有三点差异:
1、令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改, 就不会发生变化。
2、令牌可以被数据所有者撤销,会立即失效。以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销
3、令牌有权限范围,比如只能进小区的二号门。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
上面这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。
OAuth的四种授权模式:
1、授权码模式(功能最完整、流程最严密的授权模式)
说白了,授权码模式,不再client和user之间商量授权,而是client想要被授权,所以 client去找了一个和事佬大妈,大妈将client和user叫到了一起(认证服务器),给大妈个面子,这事就这么定了。就是这样的一个过程,全程中认证服务器会发布一个认证码贯穿始终。
2、密码模式 - 了解
3、简化模式 - 了解
4、客户端模式 - 了解
2.2 AppID
应用ID,唯一标识(身份证号)
2.3 AppSecret
应用的密钥(密码)
2.4 code
授权的临时凭证(例如:临时身份证)
2.5 access_token
接口调用凭证(例如:真正的身份证,虎符,令牌)
3、登录授权时序图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HTSNC55G-1634474311284)(E:\MarkDown\拉勾笔记\微信登录授权时序图)]
4、开发步骤
4.1 vue项目安装
微信官方提供的生成二维码的js
npm install vue-wxlogin
如果不是vue的项目,可以直接引用官方提供的js文件,来生成二维码
4.2 页面引入
<!-- 登录框 开始-->
<el-dialog
style="width:800px;margin:0px auto;"
title=""
:visible.sync="dialogFormVisible">
<div id="loginForm">
<el-form>
<el-form-item>
<h1 style="font-size:30px;color:#00B38A">拉勾</h1>
</el-form-item>
<el-form-item>
<el-input v-model="phone" placeholder="请输入常用手机号..."></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="password" placeholder="请输入密码..."></el-input>
</el-form-item>
</el-form>
<el-button
style="width:100%;margin:0px auto;background-color: #00B38A;font-size:20px"
type="primary"
@click="login">确 定</el-button>
<p></p>
<!-- 微信登录图标 -->
<img
@click="goToLoginWX"
src="http://www.lgstatic.com/lg-passport-fed/static/pc/modules/common/img/icon-wechat@2x_68c86d1.png"
alt=""
/>
</div>
<!-- 二维码 -->
<wxlogin id="wxLoginForm" style="display:none"
:appid="appid" :scope="scope" :redirect_uri="redirect_uri">
</wxlogin>
</el-dialog>
<!-- 登录框 结束-->
<script>
import wxlogin from 'vue-wxlogin'; // 引入
export default {
name: "Header",
components:{
wxlogin // 声明引用的组件
},
data() {
return {
isLogin: false, // 登录状态,true:已登录,false:未登录
userDTO:null, // 用来保存登录的用户信息
isHasNewMessage: false, // 是否有新的推送消息
dialogFormVisible: false, // 是否显示登录框,true:显示,false:隐藏
phone: "", // 双向绑定表单 手机号
password: "", // 双向绑定表单 密码
appid:"wxd99431bbff8305a0", // 应用唯一标识,在微信开放平台提交应用审核通过后获得
scope:"snsapi_login", // 应用授权作用域,网页应用目前仅填写snsapi_login即可
redirect_uri:"http://www.pinzhi365.com/wxlogin", //重定向地址,(回调地址)
};
},
methods: {
goToLoginWX() {
// 普通的登录表单隐藏
document.getElementById("loginForm").style.display = "none";
// 显示二维码
document.getElementById("wxLoginForm").style.display = "block";
},
},
</script>
4.3 修改hosts文件
文件位置:C:\Windows\System32\drivers\etc\hosts
回调默认指定的是80端口,别忘记将tomcat的8002端口修改成80
127.0.0.1 www.pinzhi365.com
4.4 依赖
<!-- 需要使用HttpServletRequest获得参数 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
<scope>provided</scope>
</dependency>
<!-- 需要使用HttpClient发出请求 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
4.5 封装HttpClient
/**
* @auther wei
* @date 2021/10/15 14:49
* @description HttpClient的封装工具类
*/
public class HttpClientUtil {
public static String doGet(String url){
return doGet(url,null);
}
/**
* get请求,支持request请求方式,不支持restfull方式
* @param url 请求地址
* @param param 参数
* @return 响应的字符串
*/
public static String doGet(String url, Map<String,String> param){
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
String resultString = null;
CloseableHttpResponse response = null;
try {
// 创建url
URIBuilder builder = new URIBuilder(url);
if (param != null){
// 在url后面拼接参数
for (String key : param.keySet()) {
builder.addParameter(key,param.get(key));
}
}
URI uri = builder.build();
// 创建http get请求
HttpGet httpGet = new HttpGet(uri);
// 执行请求
response = httpClient.execute(httpGet);
// 从响应对象中获取状态码(成功或失败的状态)
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应的状态 = " + statusCode);
// 200:表示响应成功
if (statusCode == 200){
// 响应的内容字符串
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
try{
if (response != null){
response.close();
}
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return resultString;
}
}
4.6 定义从微信返回的数据对象
/**
* @auther wei
* @date 2021/10/15 17:14
* @description 令牌实体类
*/
public class Token {
private String access_token;//接口调用凭证
private String expires_in; //access_token接口调用凭证超时时间,单位(秒)
private String refresh_token;//用户刷新access_token
private String openid; //授权用户唯一标识
private String scope; //用户授权的作用域,使用逗号(,)分隔
private String unionid; //当且仅当该网站应用已获得该用户的userinfo授权时,才会出现该字段。
// 略
}
/**
* @auther wei
* @date 2021/10/15 17:27
* @description 微信用户信息
*/
public class WxUser {
private String openid; // 普通用户的标识,对当前开发者帐号唯一
private String nickname; // 普通用户昵称
private String sex; // 普通用户性别,1为男性,2为女性
private String province; // 普通用户个人资料填写的省份
private String city; // 普通用户个人资料填写的城市
private String country; // 国家,如中国为CN
private String headimgurl; // 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
private String privilege; // 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
private String unionid; // 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。
// 略
}
4.7 回调函数controller
@RestController
public class WxLoginController {
@GetMapping("wxlogin")
public Object wxlogin(HttpServletRequest request){
// 1.微信官方发给我们一个临时凭证
String code = request.getParameter("code");
System.out.println("【临时凭证】code = " + code);
// 2.通过code,去微信官方申请一个正式的token(令牌)
String getTokenByCode_url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=wxd99431bbff8305a0&secret=60f78681d063590a469f1b297feff3c4&code="+code+"&grant_type=authorization_code";
String tokenString = HttpClientUtil.doGet(getTokenByCode_url);
System.out.println(tokenString);
// 3将json格式的token字符串转换成实体对象,方便存和取
Token token = JSON.parseObject(tokenString, Token.class);
// 4.通过token,去微信官方获取用户信息
String getUserByToken_url = "https://api.weixin.qq.com/sns/userinfo?access_token="+token.getAccess_token()+"&openid="+token.getOpenid();
String userInfoString = HttpClientUtil.doGet(getUserByToken_url);
System.out.println("userInfoString = " + userInfoString);
// 5.将json格式的user字符串转换成实体对象,方便存和取
WxUser wxUser = JSON.parseObject(userInfoString, WxUser.class);
System.out.println("微信昵称 = " +wxUser.getNickname());
System.out.println("微信头像 = " +wxUser.getHeadimgurl());
return wxUser;
}
}
如果下面的错误:是因为谷歌浏览器有bug,待修复!切换别的浏览器即可
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mJKnHRLd-1634474311289)(E:\MarkDown\拉勾笔记\微信登录bug.png)]
web服务的端口号必须是80!
4.8 后续
WxLoginController
@RestController
@RequestMapping("user")
public class WxLoginController {
@Reference
private UserService userService;
private UserDTO dto = null; // 是否用微信登录成功,dto为null,则尚未登录
@GetMapping("wxlogin")
public String wxlogin(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1.微信官方发给我们一个临时凭证
String code = request.getParameter("code");
System.out.println("【临时凭证】code = " + code);
// 2.通过code,去微信官方申请一个正式的token(令牌)
String getTokenByCode_url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=wxd99431bbff8305a0&secret=60f78681d063590a469f1b297feff3c4&code="+code+"&grant_type=authorization_code";
String tokenString = HttpClientUtil.doGet(getTokenByCode_url);
System.out.println(tokenString);
// 3将json格式的token字符串转换成实体对象,方便存和取
Token token = JSON.parseObject(tokenString, Token.class);
// 4.通过token,去微信官方获取用户信息
String getUserByToken_url = "https://api.weixin.qq.com/sns/userinfo?access_token="+token.getAccess_token()+"&openid="+token.getOpenid();
String userInfoString = HttpClientUtil.doGet(getUserByToken_url);
System.out.println("userInfoString = " + userInfoString);
// 5.将json格式的user字符串转换成实体对象,方便存和取
WxUser wxUser = JSON.parseObject(userInfoString, WxUser.class);
System.out.println("微信昵称 = " +wxUser.getNickname());
System.out.println("微信头像 = " +wxUser.getHeadimgurl());
// 拉勾的业务流程! 需要手机号(wxUser.getUnionid())和密码(wxUser.getUnionid()),头像和昵称
//return "user/login?phone="+wxUser.getUnionid()+"&password="+wxUser.getUnionid()+"&nickname="+wxUser.getNickname()+"&headimg="+wxUser.getHeadimgurl();
User user = null;
dto = new UserDTO();
// 检测手机号是否注册
Integer i = userService.checkPhone(wxUser.getUnionid());
if (i == 0){
// 未注册,自动注册并登录
userService.register(wxUser.getUnionid(),wxUser.getUnionid(),wxUser.getNickname(),wxUser.getHeadimgurl());
dto.setMessage("手机号尚未注册,系统已帮您自动注册,请牢记密码!");
user = userService.login(wxUser.getUnionid(), wxUser.getUnionid());
}else {
user = userService.login(wxUser.getUnionid(), wxUser.getUnionid());
if (user == null){
dto.setState(300); // 300表示失败
dto.setMessage("账号密码不匹配,登录失败!");
}else {
dto.setState(200); // 200表示成功
dto.setMessage("登录成功!");
}
}
dto.setContent(user);
// 重定向
response.sendRedirect("http://localhost:8080");
return null;
}
@GetMapping("checkWxStatus")
public UserDTO checkWxStatus(){
return this.dto;
}
@GetMapping("logout")
public Object logout(){
this.dto = null;
return null;
}
}
Header.vue
<script>
import wxlogin from 'vue-wxlogin'; // 引入
export default {
name: "Header",
components:{
wxlogin // 声明引用的组件
},
props: {},
data() {
return {
isLogin: false, // 登录状态,true:已登录,false:未登录
userDTO:null, // 用来保存登录的用户信息
isHasNewMessage: false, // 是否有新的推送消息
dialogFormVisible: false, // 是否显示登录框,true:显示,false:隐藏
phone: "", // 双向绑定表单 手机号
password: "", // 双向绑定表单 密码
appid:"wxd99431bbff8305a0", // 应用唯一标识,在微信开放平台提交应用审核通过后获得
scope:"snsapi_login", // 应用授权作用域,网页应用目前仅填写snsapi_login即可
redirect_uri:"http://www.pinzhi365.com/user/wxlogin", //重定向地址,(回调地址)
};
},
computed: {
},
watch: {
},
mounted() {
},
created(){
// 当刷新页面,组件创建成功之后,立刻检测本地储存中是否存在用户对象
this.userDTO = JSON.parse(localStorage.getItem("user"));
if(this.userDTO != null){
this.isLogin = true; // 已登录
}else{
// 去检测微信是否登录过
this.axios
.get("http://localhost:80/user/checkWxStatus")
.then((result) =>{
this.userDTO = (result.data);
this.phone = this.userDTO.content.phone;
this.password = this.userDTO.content.password;
this.login(); // 走普通登录
}).catch((error) =>{
//this.$message.error("登录失败!!!");
});
}
},
methods: {
logout(){ // 登出
localStorage.setItem("user",null); // 将登录成功的对象信息保存到本地储存中
this.isLogin = false; // 更新登录状态
alert("谢谢使用,再见!!!");
// 去检测微信是否登录过
this.axios
.get("http://localhost:80/user/logout")
.then((result) =>{
}).catch((error) =>{
//this.$message.error("登录失败!!!");
});
}
},
};
</script>
4.9 解决二维码在谷歌浏览器的bug
谷歌浏览器调试的时候,iframe标签跨域问题导致无法跳转的bug
如果iframe未添加sandbox属性,或者sandbox属性不赋值,就代表采用默认的安全策略
即:iframe的页面将会被当做一个独立的源,并且不能提交表单,不能执行javascript脚本,也不能让包含iframe的父页面导航到其他地方,所有的插件,如flash等也全部不能起作用
简单来说iframe就只剩下一个展示数据的功能,正如他的名字一样,所有的内容都被放进了一个 “单独的沙盒”
sandbox包含的属性及作用:
属性值 | 说明 |
---|---|
allow-scripts | 允许运行执行脚本 |
allow-top-navigation | 允许iframe能够主导window.top进行页面跳转 |
allow-same-origin | 允许同域请求,比如ajax,storage |
allow-forms | 允许进行提交表单 |
allow-popups | 允许iframe中弹出新窗口,比如,window.open,target=”_blank” |
allow-pointer-lock | 在iframe中可以锁定鼠标,主要和鼠标锁定有关 |
加上 sandbox=“allow-scripts allow-top-navigation allow-same-origin” 属性,即可解决
官方js:http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js
无法修改微信服务器上的js文件,所以我们将js代码放在本地并进行修改:
created(){
!(function(a, b, c) {
function d(a) {
var c = "default";
a.self_redirect === !0
? (c = "true")
: a.self_redirect === !1 && (c = "false");
var d = b.createElement("iframe"),
e =
"https://open.weixin.qq.com/connect/qrconnect?appid=" +
a.appid +
"&scope=" +
a.scope +
"&redirect_uri=" +
a.redirect_uri +
"&state=" +
a.state +
"&login_type=jssdk&self_redirect=" +
c +
"&styletype=" +
(a.styletype || "") +
"&sizetype=" +
(a.sizetype || "") +
"&bgcolor=" +
(a.bgcolor || "") +
"&rst=" +
(a.rst || "");
(e += a.style ? "&style=" + a.style : ""),
(e += a.href ? "&href=" + a.href : ""),
(d.src = e),
(d.frameBorder = "0"),
(d.allowTransparency = "true"),
(d.sandbox = "allow-scripts allow-top-navigation allow-same-origin"),
// 允许多种请求
(d.scrolling = "no"),
(d.width = "300px"),
(d.height = "400px");
var f = b.getElementById(a.id);
(f.innerHTML = ""), f.appendChild(d);
}
a.WxLogin = d;
})(window, document);
}
Course.vue
<div id="wxLoginForm"></div>
methods: {
// 微信登录
goToLoginWX() {
// 普通的登录表单隐藏
document.getElementById("loginForm").style.display = "none";
// 显示二维码的容器
document.getElementById("wxLoginForm").style.display = "block";
// 去生成二维码
// 待dom更新之后再用二维码渲染其内容
this.$nextTick(function(){
this.createCode(); // 直接调用会报错:TypeError: Cannot read property
'appendChild' of null
});
},
// 生成二维码
createCode(){
var obj = new WxLogin({
id:"wxLoginForm", // 显示二维码的容器
appid: "wxd99431bbff8305a0", // 应用唯一标识,在微信开放平台提交应用审核通过后获得
scope: "snsapi_login", // 应用授权作用域,网页应用目前仅填写snsapi_login即可
redirect_uri: "http://www.pinzhi365.com/user/wxlogin", //重定向地址,(回调地址)
href: "data:text/css;base64,base64加密后的样式"
});
},
}
.impowerBox .qrcode {width: 200px;}
.impowerBox .title {display: none;}
.impowerBox .info {width: 200px;}
.status_icon {display: none}cs
.impowerBox .status {text-align: center;}
我们用站长工具对样式代码进行base64加密:http://tool.chinaz.com/Tools/Base64.aspx
微信支付
1、创建二维码
1、安装 qrcodejs2 (注意:安装的是qrcodejs2,不要安装qrcode —> 会报错)
npm install qrcodejs2 --save
2、Course.vue页面中引入
<!-- 微信支付二维码 -->
<el-dialog :visible.sync="dialogFormVisible" style="width:800px;margin:0px auto;" >
<h1 style="font-size:30px;color:#00B38A">微信扫一扫支付</h1>
<div id="qrcode" style="width:210px;margin:20px auto;"></div>
</el-dialog>
<script>
import Header from "./Header/Header"; //顶部登录条
import Footer from "./Footer/index"; //顶部登录条
import QRCode from 'qrcodejs2'; // 引入qrcodejs
export default {
name: "Course",
components: {
Header,
Footer,
QRCode, // 声明组件
},
data() {
return {
dialogFormVisible:false, // 默认false:隐藏,true:显示
};
},
methods: {
// 购买课程
buy(courseid) {
//alert("购买第【" + courseid + "】门课程成功,加油!");
this.dialogFormVisible = true; // 显示提示框
// 待dom更新之后再用二维码渲染其内容
this.$nextTick(function(){
this.createCode(); // 直接调用会报错:TypeError: Cannot read properties of undefined (reading 'appendChild')
});
},
// 生成二维码
createCode(){
// QRCode(存放二维码的dom元素的id,二维码的属性参数)
let qrcode = new QRCode('qrcode',{
width:200,
height:200,
text:"我爱你中国" // 二维码中包含的信息
});
},
}
};
</script>
2、准备工作
2.1 名词介绍
参数 | 说明 |
---|---|
appid | 微信公众帐号或开放平台APP的唯一标识 |
partner | 商户号(配置文件中的partner:账户) |
partnerkey | 商户密钥(密码) |
sign | 数字签名,根据微信官方提供的密钥和一套算法生成的一个加密信息,就是为了保 证交易安全 |
如果获得这些信息?
需要注册认证公众号,费用300元/次
2.2 获取认证的流程
1) 注册公众号(类型:服务号)
根据营业执照类型选择以下主体注册:
个体工商户 | 企业/公司 | 政府 | 媒体 | 其他类型
2) 认证公众号
公众号认证后才申请微信支付:300元/次
3) 提交材料申请微信支付
登录公众平台,左侧菜单【微信支付】,开始填写资料等待审核,审核时间1~5工作日
这里需要提交的资料有营业执照!
4) 开户成功,登录商户平台进行验证
资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资 金数额,完成账户验证。
5) 在线签署协议
本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。
6) 查看自己的公众号的参数
public class PayConfig {
//企业公众号ID
public static String appid = "wx8397f8696b538317";
// 财付通平台的商户帐号
public static String partner = "1473426802";
// 财付通平台的商户密钥
public static String partnerKey = "8A627A4578ACE384017C997F12D68B23";
// 回调URL
public static String notifyurl ="http://a31ef7db.ngrok.io/WeChatPay/WeChatPayNotify";
}
3、支付流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WZtI6lWh-1634474311291)(E:\MarkDown\拉勾笔记\微信支付流程)]
4、工具介绍
4.1 SDK
https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=11_1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6HGFJvaB-1634474311297)(E:\MarkDown\拉勾笔记\微信支付开发文档)]
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
主要使用sdk中的三个功能:
1、获取随机字符串(生成订单编号)
WXPayUtil.generateNonceStr();
2、将map转换成xml字符串(自动添加签名)
WXPayUtil.generateSignedXml(map,partnerKey);
3、将xml字符串转换整map
WXPayUtil.xmlToMap(result);
4.2 JFinal 框架
JFinal 是基于Java 语言的极速 web 开发框架,其核心设计目标是开发迅速、代码量少、学习简单、功能强大、轻量级、易扩展
取代HttpClient
<dependency>
<groupId>com.jfinal</groupId>
<artifactId>jfinal</artifactId>
<version>3.5</version>
</dependency>
5、构建二维码
Course.vue
<script>
methods:{
// 生成二维码
createCode(){
// 去获取支付链接
this.axios
.get("http://localhost:80/order/createCode",{
params:{
courseid:this.course.id,
courseName:this.course.courseName,
price:this.course.discounts
}
})
.then((result) =>{
console.log(result);
// QRCode(存放二维码的dom元素的id,二维码的属性参数)
let qrcode = new QRCode('qrcode',{
width:200,
height:200,
text:result.data.code_url // 将返回的数据嵌入到二维码中
});
}).catch(error =>{
this.$message.error("生成二维码失败!!!");
});
},
},
</script>
支付配置
public class PayConfig {
// 企业公众号ID
public static String appid = "wx8397f8696b538317";
// 财付通平台的商户帐号
public static String partner = "1473426802";
// 财付通平台的商户密钥
public static String partnerKey = "8A627A4578ACE384017C997F12D68B23";
// 回调URL
public static String notifyurl = "http://a31ef7db.ngrok.io/WeChatPay/WeChatPayNotify";
}
createCodeController
/**
* @auther wei
* @date 2021/10/17 17:01
* @description 微信支付相关控制
*/
@RestController
@RequestMapping("order")
public class WxPayController {
@GetMapping("createCode")
public Object createCode(String courseid,String courseName,String price) throws Exception {
courseName = new String(courseName.getBytes("ISO-8859-1"),"UTF-8");
// 1.编写商户信息
HashMap<String,String> mm = new HashMap();
mm.put("appid", PayConfig.appid); // 公众号ID
mm.put("mch_id",PayConfig.partner); // 商户号
mm.put("nonce_str",WXPayUtil.generateNonceStr()); // 随机字符串
mm.put("body",courseName); // 商品描述
mm.put("out_trade_no",WXPayUtil.generateNonceStr()); // 商户订单号
mm.put("total_fee",price); // 订单金额,订单总金额,单位为分,只能为整数
mm.put("spbill_create_ip","127.0.0.1"); // 终端IP
mm.put("notify_url",PayConfig.notifyurl); // 通知地址
mm.put("trade_type","NATIVE"); // 交易类型
//System.out.println("商户信息:" + mm);
// 2.生成数字签名,并把商户信息转换成xml格式
String xml = WXPayUtil.generateSignedXml(mm, PayConfig.partnerKey);
//System.out.println("商户的xml信息:" +xml);
// 3.将xml数据发送给微信支付平台,从而生成订单
String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
// 发送请求并返回一个xml格式的字符串
String result = HttpKit.post(url, xml);
// 4.微信平台返回xml格式的数据,将其转换为map格式并返回给前端
Map<String, String> resultMap = WXPayUtil.xmlToMap(result);
//System.out.println("返回的xml,转换成map后:" +resultMap);
resultMap.put("orderId",mm.get("out_trade_no"));
return resultMap;
}
}
解决商品描述乱码
courseName = new String(courseName.getBytes("ISO-8859-1"),"UTF-8");
6、检查支付状态
<!-- 微信支付二维码 -->
<el-dialog :visible.sync="dialogFormVisible" style="width:800px;margin:0px auto;" >
<h1 style="font-size:30px;color:#00B38A">微信扫一扫支付</h1>
<div id="qrcode" style="width:210px;margin:20px auto;"></div>
<h2 id="statusText"></h2>
<p id="closeText"></p>
</el-dialog>
<script>
data() {
return {
activeName: "intro",
course:null,
totalLessons:0, // 本门课程的总节数
commentList:null, // 所有留言
isLogin:false, // 未登录
isBuy:false, // 未购买
user:null, // 当前用户
myCourseList:[], // 当前用户购买过的所有课程
comment:null, // 待发表的留言内容
dialogFormVisible:false, // 默认false:隐藏,true:显示
time:null, // 计时对象
};
},
methods: {
// 生成二维码
createCode(){
// 去获取支付链接
this.axios
.get("http://localhost:80/order/createCode",{
params:{
courseid:this.course.id,
courseName:this.course.courseName,
price:1 // 测试支付金额固定为1分钱,真实上线环境再改回此真实价格:this.course.discounts
}
})
.then((result) =>{
console.log(result);
// QRCode(存放二维码的dom元素的id,二维码的属性参数)
let qrcode = new QRCode('qrcode',{
width:200,
height:200,
text:result.data.code_url // 将返回的数据嵌入到二维码中
});
}).catch(error =>{
this.$message.error("生成二维码失败!!!");
});
// 检查支付状态
this.axios
.get("http://localhost:80/order/checkOrderStatus",{
params:{
orderId:result.data.orderId, // 传递 订单编号 进行查询
}
})
.then((result) =>{
console.log(result);
if(result.data.trade_state=="SUCCESS"){
document.getElementById("statusText").innerHTML = "<i style='color:#00B38A' class='el-icon-success'></i> 支付成功";
let s = 3; // 倒计时的秒数
this.closeQRForm(s); // 关闭二维码窗口
}
}).catch(error =>{
this.$message.error("查询订单失败!!!");
});
},
// 倒计时关闭二维码窗口
closeQRForm(s){
let that = this;
this.time = setInterval(function(){
document.getElementById("closeText").innerHTML = "( "+ s-- +" ) 秒后关闭本窗口";
if(s == 0){
clearInterval(that.time); // 停止计时器
that.dialogFormVisible = false; // 二维码窗口隐藏
that.isBuy = true; // 修改购买状态(已购买)
// 真实的修改数据库
}
},1000);
},
</script>
@GetMapping("createCode")
public Object createCode(String courseid,String courseName,String price) throws Exception {
courseName = new String(courseName.getBytes("ISO-8859-1"),"UTF-8");
// 1.编写商户信息
HashMap<String,String> mm = new HashMap();
mm.put("appid", PayConfig.appid); // 公众号ID
mm.put("mch_id",PayConfig.partner); // 商户号
mm.put("nonce_str",WXPayUtil.generateNonceStr()); // 随机字符串
mm.put("body",courseName); // 商品描述
mm.put("out_trade_no",WXPayUtil.generateNonceStr()); // 商户订单号
mm.put("total_fee",price); // 订单金额,订单总金额,单位为分,只能为整数
mm.put("spbill_create_ip","127.0.0.1"); // 终端IP
mm.put("notify_url",PayConfig.notifyurl); // 通知地址
mm.put("trade_type","NATIVE"); // 交易类型
//System.out.println("商户信息:" + mm);
// 2.生成数字签名,并把商户信息转换成xml格式
String xml = WXPayUtil.generateSignedXml(mm, PayConfig.partnerKey);
//System.out.println("商户的xml信息:" +xml);
// 3.将xml数据发送给微信支付平台,从而生成订单
String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
// 发送请求并返回一个xml格式的字符串
String result = HttpKit.post(url, xml);
// 4.微信平台返回xml格式的数据,将其转换为map格式并返回给前端
Map<String, String> resultMap = WXPayUtil.xmlToMap(result);
//System.out.println("返回的xml,转换成map后:" +resultMap);
resultMap.put("orderId",mm.get("out_trade_no"));
return resultMap;
}
@GetMapping("checkOrderStatus")
public Object checkOrderStatus(String orderId) throws Exception {
// 1.编写商户信息
HashMap<String,String> mm = new HashMap();
mm.put("appid", PayConfig.appid); // 公众号ID
mm.put("mch_id",PayConfig.partner); // 商户号
mm.put("out_trade_no",orderId); // 商户订单号
mm.put("nonce_str",WXPayUtil.generateNonceStr()); // 随机字符串
// 2.生成数字签名
String xml = WXPayUtil.generateSignedXml(mm, PayConfig.partnerKey);
// 3.发送查询请求给微信支付平台
String url = "https://api.mch.weixin.qq.com/pay/orderquery";
// 查询订单状态的开始时间
long beginTime = System.currentTimeMillis();
// 不停的去微信后台询问是否支付
while (true) {
// 4.对微信支付平台返回的查询结果进行处理
String result = HttpKit.post(url, xml);
Map<String, String> resultMap = WXPayUtil.xmlToMap(result);
// 已经支付成功
if (resultMap.get("trade_state").equalsIgnoreCase("SUCCESS")){
return resultMap;
}
// 超过30秒未支付,停止询问
if (System.currentTimeMillis() - beginTime > 30000){
return resultMap;
}
Thread.sleep(30000); // 每隔3秒,询问一次微信支付平台
}
}
7、保存订单并更新状态
<script>
data() {
return {
activeName: "intro",
course:null,
totalLessons:0, // 本门课程的总节数
commentList:null, // 所有留言
isLogin:false, // 未登录
isBuy:false, // 未购买
user:null, // 当前用户
myCourseList:[], // 当前用户购买过的所有课程
comment:null, // 待发表的留言内容
dialogFormVisible:false, // 默认false:隐藏,true:显示
time:null, // 计时对象
orderNo:"", // 订单编号
};
},
methods: {
// 生成二维码
createCode(){
// 去获取支付链接
this.axios
.get("http://localhost:80/order/createCode",{
params:{
courseid:this.course.id,
courseName:this.course.courseName,
price:1 // 测试支付金额固定为1分钱,真实上线环境再改回此真实价格:this.course.discounts
}
})
.then((result) =>{
console.log(result);
// QRCode(存放二维码的dom元素的id,二维码的属性参数)
let qrcode = new QRCode('qrcode',{
width:200,
height:200,
text:result.data.code_url // 将返回的数据嵌入到二维码中
});
}).catch(error =>{
this.$message.error("生成二维码失败!!!");
});
this.orderNo = result.data.orderId;
// 保存订单 状态为:已创建 0
this.saveOrder();
// 检查支付状态
this.axios
.get("http://localhost:80/order/checkOrderStatus",{
params:{
orderId:result.data.orderId, // 传递 订单编号 进行查询
}
})
.then((result) =>{
if(result.data.trade_state=="SUCCESS"){
document.getElementById("statusText").innerHTML = "<i style='color:#00B38A' class='el-icon-success'></i> 支付成功";
// 支付成功
this.updateOrder(20);
}
/*
else if(result.data.trade_state=="NOTPAY"){
document.getElementById("statusText").innerHTML = "<i style='color:#00B38A' class='el-icon-success'></i> 未支付";
this.updateOrder(10);
}
*/
// 3秒后关闭二维码窗口
let s = 3; // 倒计时的秒数
this.closeQRForm(s); // 关闭二维码窗口
}).catch(error =>{
this.$message.error("查询订单失败!!!");
});
},
// 倒计时关闭二维码窗口
closeQRForm(s){
let that = this;
this.time = setInterval(function(){
document.getElementById("closeText").innerHTML = "( "+ s-- +" ) 秒后关闭本窗口";
if(s == 0){
clearInterval(that.time); // 停止计时器
that.dialogFormVisible = false; // 二维码窗口隐藏
that.isBuy = true; // 修改购买状态(已购买)
}
},1000);
},
// 保存订单
saveOrder(){
this.axios
.get("http://localhost:80/order/saveOrder",{
params:{
orderNo:this.orderNo,
user_id:this.user.content.id,
course_id:this.course_id,
activity_course_id:this.course_id,
source_type:1,
}
})
.then((result) =>{
console.log(result);
}).catch(error =>{
this.$message.error("保存订单失败!!!");
});
},
// 更新订单状态
updateOrder(statusCode){
return this.axios
.get("http://localhost:80/order/updateOrder",{
params:{
orderNo:this.orderNo,
status:statusCode,
}
})
.then((result) =>{
console.log("更新订单【"+this.orderNo+"】状态为:" + statusCode);
}).catch(error =>{
this.$message.error("更新订单失败!!!");
});
},
};
</script>
web消费方
@GetMapping("updateOrder")
public Integer saveOrder(String orderNo, Integer status){
System.out.println("订单编号 = " + orderNo);
System.out.println("状态编码 = " + status);
Integer integer = orderService.updateOrder(orderNo, status);
System.out.println("订单额更新 = " + integer);
return integer;
}