课程详情接口与前端、课程章节接口、搜索功能接口与前端、支付宝支付功能、视频文件等托管、支付接口与前端、支付成功回调页面
今日内容概要
- 课程详情接口
- 课程详情页前端
- 课程章节接口
- 搜索功能接口
- 搜索功能前端
- 支付宝支付介绍
- 支付宝支付二次封装
- 视频托管
- 支付接口
- 支付前端
- 支付成功回调接口
- 内网穿透
内容详细
1、课程详情接口
# 课程列表(所有课程:分页,排序...),再加一个课程详情
# 基于原来的课程列表接口,只需要再加入继承一个类:RetrieveModelMixin
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
class CourseView(GenericViewSet, ListModelMixin, RetrieveModelMixin):
# 访问:
http://127.0.0.1:8000/api/v1/course/actual/2/
可以按id号筛选
2、课程详情页前端
# 课程详情页前端
达到播放视频效果---》vue-video-player(很久没更新了)---》node版本高--》可能用不了
# vue-video-player 本身是封装了video.js库
# 换一个播放视频组件
vue-core-video-player -基于vue.js的视频播放器组件---》非常简单易用
2.1 vue-core-video-player的使用
# 第一步:安装
npm install --save vue-core-video-player
# 第二步:main.js 引入
// vue-core-video-player播放器
import VueCoreVideoPlayer from 'vue-core-video-player'
// Vue.use(VueCoreVideoPlayer)
Vue.use(VueCoreVideoPlayer, { // 国际化设置
lang: 'zh-CN'
})
# 第三步:在组件的html中使用
<div id="app">
<div class="player-container">
<vue-core-video-player src="http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4"></vue-core-video-player>
</div>
</div>
# https://www.cnblogs.com/liuqingzheng/p/16204851.html
# https://github.com/core-player/vue-core-video-player-examples
src/views/CourseDetail.vue内容:
<template>
<div class="detail">
<Header/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
<vue-core-video-player :src="mp4_url"
controls="auto"
autoplay
:muted="true"
title="致命诱惑"
@play="playFunc"
@pause="pauseFunc"
></vue-core-video-player>
</div>
<div class="wrap-right">
<h3 class="course-name">{{course_info.name}}</h3>
<p class="data">{{course_info.students}}人在学 课程总时长:{{course_info.sections}}课时/{{course_info.pub_sections}}小时 难度:{{course_info.level_name}}</p>
<div class="sale-time">
<p class="sale-type">价格 <span class="original_price">¥{{course_info.price}}</span></p>
<p class="expire"></p>
</div>
<div class="buy">
<div class="buy-btn">
<button class="buy-now">立即购买</button>
<button class="free">免费试学</button>
</div>
<div class="add-cart" @click="add_cart(course_info.id)">
<img src="@/assets/img/cart-yellow.svg" alt="">加入购物车
</div>
</div>
</div>
</div>
<div class="course-tab">
<ul class="tab-list">
<li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
<li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span>
</li>
<li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论</li>
<li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
</ul>
</div>
<div class="course-content">
<div class="course-tab-list">
<div class="tab-item" v-if="tabIndex==1">
<div class="course-brief" v-html="course_info.brief"></div>
</div>
<div class="tab-item" v-if="tabIndex==2">
<div class="tab-item-title">
<p class="chapter">课程章节</p>
<p class="chapter-length">共{{course_chapters.length}}章 {{course_info.sections}}个课时</p>
</div>
<div class="chapter-item" v-for="chapter in course_chapters" :key="chapter.name">
<p class="chapter-title"><img src="@/assets/img/enum.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}
</p>
<ul class="section-list">
<li class="section-item" v-for="section in chapter.coursesections" :key="section.name">
<p class="name"><span class="index">{{chapter.chapter}}-{{section.orders}}</span>
{{section.name}}<span class="free" v-if="section.free_trail">免费</span></p>
<p class="time">{{section.duration}} <img src="@/assets/img/chapter-player.svg"></p>
<button class="try" v-if="section.free_trail">立即试学</button>
<button class="try" v-else>立即购买</button>
</li>
</ul>
</div>
</div>
<div class="tab-item" v-if="tabIndex==3">
用户评论
</div>
<div class="tab-item" v-if="tabIndex==4">
常见问题
</div>
</div>
<div class="course-side">
<div class="teacher-info">
<h4 class="side-title"><span>授课老师</span></h4>
<div class="teacher-content">
<div class="cont1">
<img :src="course_info.teacher.image">
<div class="name">
<p class="teacher-name">{{course_info.teacher.name}}
{{course_info.teacher.title}}</p>
<p class="teacher-title">{{course_info.teacher.signature}}</p>
</div>
</div>
<p class="narrative">{{course_info.teacher.brief}}</p>
</div>
</div>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "@/components/Header"
import Footer from "@/components/Footer"
export default {
name: "Detail",
data() {
return {
tabIndex: 2, // 当前选项卡显示的下标
course_id: 0, // 当前课程信息的ID
course_info: {
teacher: {},
}, // 课程信息
course_chapters: [], // 课程的章节课时列表
// mp4_url:'http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4',
mp4_url: [
{
src: 'http://rb1x3v1fm.sabkt.gdipper.com/%E8%87%B4%E5%91%BD%E8%AF%B1%E6%83%91320p.mp4',
resolution: 360,
},
{
src: 'http://rb1x3v1fm.sabkt.gdipper.com/%E8%87%B4%E5%91%BD%E8%AF%B1%E6%83%91720p.mp4',
resolution: 720,
},
{
src: 'http://rb1x3v1fm.sabkt.gdipper.com/%E8%87%B4%E5%91%BD%E8%AF%B1%E6%83%914k.mp4',
resolution: '4k',
}],
}
},
created() {
this.get_course_id();
this.get_course_data();
this.get_chapter();
},
methods: {
playFunc(){
console.log('开始了')
},
pauseFunc(){
console.log('暂停了')
},
get_course_id() {
// 获取地址栏上面的课程ID
this.course_id = this.$route.params.pk
if (this.course_id < 1) {
let _this = this;
_this.$alert("对不起,当前视频不存在!", "警告", {
callback() {
_this.$router.go(-1);
}
});
}
},
get_course_data() {
// ajax请求课程信息
this.$axios.get(`${this.$settings.base_url}course/actual/${this.course_id}/`).then(response => {
// window.console.log(response.data);
this.course_info = response.data;
console.log(this.course_info)
}).catch(() => {
this.$message({
message: "对不起,访问页面出错!请联系客服工作人员!"
});
})
},
get_chapter() {
// 获取当前课程对应的章节课时信息
// http://127.0.0.1:8000/course/chapters/?course=(pk)
this.$axios.get(`${this.$settings.base_url}course/chapter/`, {
params: {
"course_id": this.course_id,
}
}).then(response => {
this.course_chapters = response.data;
}).catch(error => {
window.console.log(error.response);
})
},
},
components: {
Header,
Footer,
}
}
</script>
<style scoped>
.main {
background: #fff;
padding-top: 30px;
}
.course-info {
width: 1200px;
margin: 0 auto;
overflow: hidden;
}
.wrap-left {
float: left;
width: 690px;
height: 388px;
background-color: #000;
}
.wrap-right {
float: left;
position: relative;
height: 388px;
}
.course-name {
font-size: 20px;
color: #333;
padding: 10px 23px;
letter-spacing: .45px;
}
.data {
padding-left: 23px;
padding-right: 23px;
padding-bottom: 16px;
font-size: 14px;
color: #9b9b9b;
}
.sale-time {
width: 464px;
background: #fa6240;
font-size: 14px;
color: #4a4a4a;
padding: 10px 23px;
overflow: hidden;
}
.sale-type {
font-size: 16px;
color: #fff;
letter-spacing: .36px;
float: left;
}
.sale-time .expire {
font-size: 14px;
color: #fff;
float: right;
}
.sale-time .expire .second {
width: 24px;
display: inline-block;
background: #fafafa;
color: #5e5e5e;
padding: 6px 0;
text-align: center;
}
.course-price {
background: #fff;
font-size: 14px;
color: #4a4a4a;
padding: 5px 23px;
}
.discount {
font-size: 26px;
color: #fa6240;
margin-left: 10px;
display: inline-block;
margin-bottom: -5px;
}
.original {
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
text-decoration: line-through;
}
.buy {
width: 464px;
padding: 0px 23px;
position: absolute;
left: 0;
bottom: 20px;
overflow: hidden;
}
.buy .buy-btn {
float: left;
}
.buy .buy-now {
width: 125px;
height: 40px;
border: 0;
background: #ffc210;
border-radius: 4px;
color: #fff;
cursor: pointer;
margin-right: 15px;
outline: none;
}
.buy .free {
width: 125px;
height: 40px;
border-radius: 4px;
cursor: pointer;
margin-right: 15px;
background: #fff;
color: #ffc210;
border: 1px solid #ffc210;
}
.add-cart {
float: right;
font-size: 14px;
color: #ffc210;
text-align: center;
cursor: pointer;
margin-top: 10px;
}
.add-cart img {
width: 20px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.course-tab {
width: 100%;
background: #fff;
margin-bottom: 30px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course-tab .tab-list {
width: 1200px;
margin: auto;
color: #4a4a4a;
overflow: hidden;
}
.tab-list li {
float: left;
margin-right: 15px;
padding: 26px 20px 16px;
font-size: 17px;
cursor: pointer;
}
.tab-list .active {
color: #ffc210;
border-bottom: 2px solid #ffc210;
}
.tab-list .free {
color: #fb7c55;
}
.course-content {
width: 1200px;
margin: 0 auto;
background: #FAFAFA;
overflow: hidden;
padding-bottom: 40px;
}
.course-tab-list {
width: 880px;
height: auto;
padding: 20px;
background: #fff;
float: left;
box-sizing: border-box;
overflow: hidden;
position: relative;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item {
width: 880px;
background: #fff;
padding-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item-title {
justify-content: space-between;
padding: 25px 20px 11px;
border-radius: 4px;
margin-bottom: 20px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
overflow: hidden;
}
.chapter {
font-size: 17px;
color: #4a4a4a;
float: left;
}
.chapter-length {
float: right;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
}
.chapter-title {
font-size: 16px;
color: #4a4a4a;
letter-spacing: .26px;
padding: 12px;
background: #eee;
border-radius: 2px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
}
.chapter-title img {
width: 18px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.section-list {
padding: 0 20px;
}
.section-list .section-item {
padding: 15px 20px 15px 36px;
cursor: pointer;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.section-item .name {
font-size: 14px;
color: #666;
float: left;
}
.section-item .index {
margin-right: 5px;
}
.section-item .free {
font-size: 12px;
color: #fff;
letter-spacing: .19px;
background: #ffc210;
border-radius: 100px;
padding: 1px 9px;
margin-left: 10px;
}
.section-item .time {
font-size: 14px;
color: #666;
letter-spacing: .23px;
opacity: 1;
transition: all .15s ease-in-out;
float: right;
}
.section-item .time img {
width: 18px;
height: 18px;
margin-left: 15px;
vertical-align: text-bottom;
}
.section-item .try {
width: 86px;
height: 28px;
background: #ffc210;
border-radius: 4px;
font-size: 14px;
color: #fff;
position: absolute;
right: 20px;
top: 10px;
opacity: 0;
transition: all .2s ease-in-out;
cursor: pointer;
outline: none;
border: none;
}
.section-item:hover {
background: #fcf7ef;
box-shadow: 0 0 0 0 #f3f3f3;
}
.section-item:hover .name {
color: #333;
}
.section-item:hover .try {
opacity: 1;
}
.course-side {
width: 300px;
height: auto;
margin-left: 20px;
float: right;
}
.teacher-info {
background: #fff;
margin-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.side-title {
font-weight: normal;
font-size: 17px;
color: #4a4a4a;
padding: 18px 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.side-title span {
display: inline-block;
border-left: 2px solid #ffc210;
padding-left: 12px;
}
.teacher-content {
padding: 30px 20px;
box-sizing: border-box;
}
.teacher-content .cont1 {
margin-bottom: 12px;
overflow: hidden;
}
.teacher-content .cont1 img {
width: 54px;
height: 54px;
margin-right: 12px;
float: left;
}
.teacher-content .cont1 .name {
float: right;
}
.teacher-content .cont1 .teacher-name {
width: 188px;
font-size: 16px;
color: #4a4a4a;
padding-bottom: 4px;
}
.teacher-content .cont1 .teacher-title {
width: 188px;
font-size: 13px;
color: #9b9b9b;
white-space: nowrap;
}
.teacher-content .narrative {
font-size: 14px;
color: #666;
line-height: 24px;
}
</style>
3、课程章节接口
# 查询所有章节---》章节跟课程有关联,根据课程id号,过滤章节
# 比如:查询课程id为1的所有章节
# 本质就是查询所有章节,带过滤功能
添加路由 course/urls.py
# http://127.0.0.1:8000/api/v1/course/chapter/?course_id=2
router.register('chapter', CourseChapterView, 'chapter')
记得修改router/index.js:
{
path: '/actual/detail/:pk',
name: 'CourseDetail',
component: CourseDetail
},
在视图类course/views.py 添加:
class CourseChapterView(GenericViewSet, ListModelMixin):
queryset = CourseChapter.objects.all().filter(is_show=True, is_delete=False).order_by('orders')
serializer_class = CourseChapterSerializer
filter_backends = [DjangoFilterBackend]
# 加入过滤---》按照课程id过滤--》第三方django-filter
filter_fields = ['course_id']
在序列化类course/ serializer.py 添加:
class CourseSectionSerializer(serializers.ModelSerializer):
class Meta:
model = CourseSection
fields = ['name', 'orders', 'section_link', 'duration', 'free_trail']
class CourseChapterSerializer(serializers.ModelSerializer):
# 使用子序列化
coursesections = CourseSectionSerializer(many=True)
class Meta:
model = CourseChapter
# CourseChapter表中隐藏了一个coursesections字段,对象.coursesections
fields = ['id', 'name', 'chapter', 'summary', 'coursesections'] # 返回该章节下的课时
4、搜索功能接口
# 搜索功能--》配合搜索接口
# 类比:淘宝,京东,抖音,微博,百度---》这个接口本应该是最复杂,最有技术含量的接口
不同的人搜出来的结果都不一样,因为有个性化推荐
淘宝搜 [连体衣]--->【电饭煲】 199以下
# 咱们是最简单的实现,后期学完es:分布式全文检索引擎,基于es,写个百度
# 正常来讲:搜索可以搜索所有类型课程---》免费课,实战课,轻课--》咱们只写了实战课,只能搜出实战课
# 咱么如果只有实战课,实际上可以和查询所有实战课课程接口放一起---》为了方便后期的扩展,我们要单独写
# 咱么你的可以按照课程名字模糊匹配
# 速度慢的问题---》总共课程就不多--》mysql完全顶得住
# 课程名字和id 在redis中缓存
# 只要用搜索,对数量比较大,就上专业的搜索引擎---》es ---》倒排索引
# 访问:
http://127.0.0.1:8000/api/v1/course/search/?search=go 测试
添加路由 course/urls.py:
from .views import CourseCategoryView, CourseView, CourseChapterView, CourseSearchView
# http://127.0.0.1:8000/api/v1/course/search/?search=go
router.register('search', CourseSearchView, 'search')
在视图类course/views.py 添加:
# 只针对于实战课的搜索接口
class CourseSearchView(GenericViewSet, ListModelMixin):
queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
serializer_class = CourseSerializer
filter_backends = [SearchFilter]
search_fields = ['name', ]
pagination_class = CommonPageNumberPagination
# 方便后期扩展
# def list(self, request, *args, **kwargs):
# # 这个查的是实战课
# res=super().list(request, *args, **kwargs)
# # res2=查询免费课
# # res3=查询轻课
#
# # 上面全是取数据库查询
# # 后期改成取es中查询,
# return APIResponse(result={'free_course':'字典','actual_course':res.data})
5、搜索功能前端页面
# 主要写:
头部组件中的搜索框
搜索结果展示页面
router/index.js加入:
import SearchCourse from "@/views/SearchCourse";
{
path: '/course/search',
name: 'SearchCourse',
component: SearchCourse
},
创建 views/SearchCourse.vue 搜索结果展示页:
<template>
<div class="search-course course">
<Header/>
<!-- 课程列表 -->
<div class="main">
<div v-if="course_list.length > 0" class="course-list">
<div class="course-item" v-for="course in course_list" :key="course.name">
<div class="course-image">
<img :src="course.course_img" alt="">
</div>
<div class="course-info">
<h3>
<router-link :to="'/free/detail/'+course.id">{{ course.name }}</router-link>
<span><img src="@/assets/img/avatar1.svg" alt="">{{ course.students }}人已加入学习</span></h3>
<p class="teather-info">
{{ course.teacher.name }} {{ course.teacher.title }} {{ course.teacher.signature }}
<span
v-if="course.sections>course.pub_sections">共{{ course.sections }}课时/已更新{{ course.pub_sections }}课时</span>
<span v-else>共{{ course.sections }}课时/更新完成</span>
</p>
<ul class="section-list">
<li v-for="(section, key) in course.section_list" :key="section.name"><span
class="section-title">0{{ key + 1 }} | {{ section.name }}</span>
<span class="free" v-if="section.free_trail">免费</span></li>
</ul>
<div class="pay-box">
<div v-if="course.discount_type">
<span class="discount-type">{{ course.discount_type }}</span>
<span class="discount-price">¥{{ course.real_price }}元</span>
<span class="original-price">原价:{{ course.price }}元</span>
</div>
<span v-else class="discount-price">¥{{ course.price }}元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
</div>
<div v-else style="text-align: center; line-height: 60px">
没有搜索结果
</div>
<div class="course_pagination block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="filter.page"
:page-sizes="[2, 3, 5, 10]"
:page-size="filter.page_size"
layout="sizes, prev, pager, next"
:total="course_total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
import Header from '../components/Header'
export default {
name: "SearchCourse",
components: {
Header,
},
data() {
return {
course_list: [],
course_total: 0,
filter: {
page_size: 10,
page: 1,
search: '',
}
}
},
created() {
this.get_course()
},
watch: {
'$route.query'() {
this.get_course()
}
},
methods: {
handleSizeChange(val) {
// 每页数据量发生变化时执行的方法
this.filter.page = 1;
this.filter.page_size = val;
},
handleCurrentChange(val) {
// 页码发生变化时执行的方法
this.filter.page = val;
},
get_course() {
// 获取搜索的关键字
this.filter.search = this.$route.query.word;
// 获取课程列表信息
this.$axios.get(`${this.$settings.base_url}course/search/`, {
params: this.filter
}).then(response => {
// 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
this.course_list = response.data.results;
this.course_total = response.data.count;
}).catch(() => {
this.$message({
message: "获取课程信息有误,请联系客服工作人员"
})
})
}
}
}
</script>
<style scoped>
.course {
background: #f6f6f6;
}
.course .main {
width: 1100px;
margin: 35px auto 0;
}
.course .condition {
margin-bottom: 35px;
padding: 25px 30px 25px 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course .cate-list {
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
padding-bottom: 18px;
margin-bottom: 17px;
}
.course .cate-list::after {
content: "";
display: block;
clear: both;
}
.course .cate-list li {
float: left;
font-size: 16px;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
border: 1px solid transparent; /* transparent 透明 */
}
.course .cate-list .title {
color: #888;
margin-left: 0;
letter-spacing: .36px;
padding: 0;
line-height: 28px;
}
.course .cate-list .this {
color: #ffc210;
border: 1px solid #ffc210 !important;
border-radius: 30px;
}
.course .ordering::after {
content: "";
display: block;
clear: both;
}
.course .ordering ul {
float: left;
}
.course .ordering ul::after {
content: "";
display: block;
clear: both;
}
.course .ordering .condition-result {
float: right;
font-size: 14px;
color: #9b9b9b;
line-height: 28px;
}
.course .ordering ul li {
float: left;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
}
.course .ordering .title {
font-size: 16px;
color: #888;
letter-spacing: .36px;
margin-left: 0;
padding: 0;
line-height: 28px;
}
.course .ordering .this {
color: #ffc210;
}
.course .ordering .price {
position: relative;
}
.course .ordering .price::before,
.course .ordering .price::after {
cursor: pointer;
content: "";
display: block;
width: 0px;
height: 0px;
border: 5px solid transparent;
position: absolute;
right: 0;
}
.course .ordering .price::before {
border-bottom: 5px solid #aaa;
margin-bottom: 2px;
top: 2px;
}
.course .ordering .price::after {
border-top: 5px solid #aaa;
bottom: 2px;
}
.course .ordering .price_up::before {
border-bottom-color: #ffc210;
}
.course .ordering .price_down::after {
border-top-color: #ffc210;
}
.course .course-item:hover {
box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}
.course .course-item {
width: 1100px;
background: #fff;
padding: 20px 30px 20px 20px;
margin-bottom: 35px;
border-radius: 2px;
cursor: pointer;
box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
/* css3.0 过渡动画 hover 事件操作 */
transition: all .2s ease;
}
.course .course-item::after {
content: "";
display: block;
clear: both;
}
/* 顶级元素 父级元素 当前元素{} */
.course .course-item .course-image {
float: left;
width: 423px;
height: 210px;
margin-right: 30px;
}
.course .course-item .course-image img {
max-width: 100%;
max-height: 210px;
}
.course .course-item .course-info {
float: left;
width: 596px;
}
.course-item .course-info h3 a {
font-size: 26px;
color: #333;
font-weight: normal;
margin-bottom: 8px;
}
.course-item .course-info h3 span {
font-size: 14px;
color: #9b9b9b;
float: right;
margin-top: 14px;
}
.course-item .course-info h3 span img {
width: 11px;
height: auto;
margin-right: 7px;
}
.course-item .course-info .teather-info {
font-size: 14px;
color: #9b9b9b;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.course-item .course-info .teather-info span {
float: right;
}
.course-item .section-list::after {
content: "";
display: block;
clear: both;
}
.course-item .section-list li {
float: left;
width: 44%;
font-size: 14px;
color: #666;
padding-left: 22px;
/* background: url("路径") 是否平铺 x轴位置 y轴位置 */
background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
margin-bottom: 15px;
}
.course-item .section-list li .section-title {
/* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
max-width: 200px;
}
.course-item .section-list li:hover {
background-image: url("/src/assets/img/play-icon-yellow.svg");
color: #ffc210;
}
.course-item .section-list li .free {
width: 34px;
height: 20px;
color: #fd7b4d;
vertical-align: super;
margin-left: 10px;
border: 1px solid #fd7b4d;
border-radius: 2px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.course-item .section-list li:hover .free {
color: #ffc210;
border-color: #ffc210;
}
.course-item {
position: relative;
}
.course-item .pay-box {
position: absolute;
bottom: 20px;
width: 600px;
}
.course-item .pay-box::after {
content: "";
display: block;
clear: both;
}
.course-item .pay-box .discount-type {
padding: 6px 10px;
font-size: 16px;
color: #fff;
text-align: center;
margin-right: 8px;
background: #fa6240;
border: 1px solid #fa6240;
border-radius: 10px 0 10px 0;
float: left;
}
.course-item .pay-box .discount-price {
font-size: 24px;
color: #fa6240;
float: left;
}
.course-item .pay-box .original-price {
text-decoration: line-through;
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
float: left;
margin-top: 10px;
}
.course-item .pay-box .buy-now {
width: 120px;
height: 38px;
background: transparent;
color: #fa6240;
font-size: 16px;
border: 1px solid #fd7b4d;
border-radius: 3px;
transition: all .2s ease-in-out;
float: right;
text-align: center;
line-height: 38px;
position: absolute;
right: 0;
bottom: 5px;
}
.course-item .pay-box .buy-now:hover {
color: #fff;
background: #ffc210;
border: 1px solid #ffc210;
}
.course .course_pagination {
margin-bottom: 60px;
text-align: center;
}
</style>
components/Header.vue
写在:
<template>
<div class="header">
<div class="slogan">
<p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
</div>
<div class="nav">
......
<form class="search">
<div class="tips" v-if="is_search_tip">
<span @click="search_action('Python')">Python</span>
<span @click="search_action('Linux')">Linux</span>
</div>
<input type="text" :placeholder="search_placeholder" @focus="on_search" @blur="off_search"
v-model="search_word">
<button type="button" class="glyphicon glyphicon-search" @click="search_action(search_word)"></button>
</form>
</div>
</div>
</template>
/////////////////////////////////////////////////////////////////
写在:
<script>
export default {
methods: {
// 写下面内容
},
},
search_action(search_word) {
if (!search_word) {
this.$message('请输入要搜索的内容');
return
}
// this.$route当前路径,query ?name=lqz -->this.$route.query.name 拿到lqz
if (search_word !== this.$route.query.word) {
this.$router.push(`/course/search?word=${search_word}`);
}
this.search_word = '';
},
on_search() {
this.search_placeholder = '请输入想搜索的课程';
this.is_search_tip = false;
},
off_search() {
this.search_placeholder = '';
this.is_search_tip = true;
},
},
/////////////////////////////////////////////////////////////////
写在<script>标签中:
export default {
name: "Header",
data() {
return {
url_path: sessionStorage.url_path || '/',
is_login: false,
is_register: false,
username: '',
icon: '',
is_search_tip: true,
search_placeholder: '',
search_word: ''
}
},
/////////////////////////////////////////////////////////////////
写在<style scoped>
</style>标签最下面:
.search {
float: right;
position: relative;
margin-top: 22px;
margin-right: 10px;
}
.search input, .search button {
border: none;
outline: none;
background-color: white;
}
.search input {
border-bottom: 1px solid #eeeeee;
}
.search input:focus {
border-bottom-color: orange;
}
.search input:focus + button {
color: orange;
}
.search .tips {
position: absolute;
bottom: 3px;
left: 0;
}
.search .tips span {
border-radius: 11px;
background-color: #eee;
line-height: 22px;
display: inline-block;
padding: 0 7px;
margin-right: 3px;
cursor: pointer;
color: #aaa;
font-size: 14px;
}
.search .tips span:hover {
color: orange;
}
6、支付宝支付介绍
# 目前线上付款方式有多种:支付宝,微信,银联
# 以支付宝为例
# 官方提供了API接口,原来没有提供python的sdk,使用第三方
# 现在官方提供了python的sdk可以尝试使用官方sdk
# 使用支付宝支付,需要是企业,要有营业执照才能申请,支付宝商家
# 测试环境--沙箱环境,即便咱们不是商家,也可以测试,以后换成真正的appid,公钥私钥--》付款就到真正的地址
# 沙箱环境:https://openhome.alipay.com/platform/appDaily.htm?tab=info
# 沙箱版支付宝客户端:一个app,连的环境是测试环境,里面的钱可以一直冲,支付付到测试环境里
# 支付宝支付的场景
-https://opendocs.alipay.com/open/270/105898
# 流程
我们自己网站有个立即购买按钮---》用户点击---》向咱们后端发送一个请求---》咱们后端生成一个没有支付的订单和支付连接(支付宝的)--->返回给前端---》前端放在支付连接的地址---》显示出支付宝支付页面---》app扫码支付,账号密码支付---》支付成功---》支付宝收到了支付---》支付宝会回调回咱们项目支付成功的页面---》页面展示用户购买成功---》支付宝还有一个post回调---》咱们项目利用post回调,修改订单状态
7、支付宝支付功能二次封装
# 原来官方没有sdk,需要基于api去写
# 第三方有人封装了python版的sdk
# 第一步:
下载:pip install python-alipay-sdk --upgrade
# 第二步:生成公钥私钥 ---》非对称加密
对称加密 :加密和解密使用同一个秘钥
非对称加密:加密用公钥,解密用私钥
补充:支付宝为了更安全,需要使用咱们的公钥配置在网站上,生成一个支付宝公钥
https://opendocs.alipay.com/mini/02c7i5
下载软件,在软件中生成
在 scripts路径下创建目录:
t_alipay
创建三个文本:
pra # 我们自己的私钥
pub_ali # 用自己的公钥生成的阿里的公钥
pub_our # 我们自己的公钥
s1.py # 测试文件
# 如果测试遇到报错 cryptography不支持 可以下载老版本:
pip install cryptography==2.6
在libs 下创建:
libs
├── iPay # aliapy二次封装包
│ ├── __init__.py # 包文件
│ ├── pem # 公钥私钥文件夹
│ │ ├── alipay_public_key.pem # 支付宝公钥文件 pub_ali
│ │ ├── app_private_key.pem # 应用私钥文件 pra
│ ├── pay.py # 支付文件
└── └── settings.py # 应用配置
# settings.py:
import os
# 应用私钥
APP_PRIVATE_KEY_STRING = open(
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'app_private_key.pem')).read()
# 支付宝公钥
ALIPAY_PUBLIC_KEY_STRING = open(
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'alipay_public_key.pem')).read()
# 应用ID
APP_ID = '2016092000554611'
# 加密方式
SIGN = 'RSA2'
# 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
DEBUG = True
# 支付网关
GATEWAY = 'https://openapi.alipaydev.com/gateway.do' if DEBUG else 'https://openapi.alipay.com/gateway.do'
-----------------------------------------------------------------
# pay.py:
from alipay import AliPay
from . import settings
# 支付对象
alipay = AliPay(
appid=settings.APP_ID,
app_notify_url=None,
app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
sign_type=settings.SIGN,
debug=settings.DEBUG
)
# 支付网关
gateway = settings.GATEWAY
-----------------------------------------------------------------
# __init__.py :
from .pay import gateway, alipay
8、视频托管
# 图片,视频,一般不放在自己项目中--->只要访问一次图片,就向后端发一次请求--》消耗服务器资源
# 静态类型文件,托管到第三方平台---》花钱---》使用第三方平台的服务器资源和带宽
# 视频托管到云平台了
阿里 oss
七牛云存储
...
# 公司里多种方案---》托管到其他平台
云平台---》花钱---》七牛云
公司自己搭建文件服务器
fastdfs:搭建笔记,python上传:https://zhuanlan.zhihu.com/p/372286804
Minio:自己搭建Minio,阿里云上--》搭建笔记--》上传下载比较简单 --有单独的图形化界面后台,登陆上看到文件
ceph
...
放到项目中
# 七牛云---》国内go语言推崇者--》项目基本都是go开发的---》许式伟
go的国内镜像:goproxy.cn 就是七牛云的
豆瓣python---》python写的
# 七牛云上传
手动上传---》手动配置视频地址--(不采取)
前端上传,返回连接地址,提交到咱们自己的后端,存到数据库
前端把视频传到咱们后端,咱们后端通过python传到七牛云,生成连接,存到数据库
# 注册七牛云平台
本地使用 七牛文件上传:
# 安装:
pip install qiniu
# 演示代码:
from qiniu import Auth, put_file, etag
# q = Auth('-BoMxfk8dxgDq87mA-qLkgmI_AHYfF27tOFan_NJ', 'Wijh6Fg6gedtBqoqKElkxrrlySoiVWbFSfQ6I-qM')
q = Auth('quD6BDbCTC0H3c_g5ZEDnCxX31sqpd0ttYDjwQIP', 'JRSalGcID6Ah4-StDTN6_KA7WwP-OnhXrjv_hM-j')
# 要上传的空间
bucket_name = 'jgx'
# 上传后保存的文件名
key = '致命诱惑.png'
# 生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)
# 要上传文件的本地路径
localfile = './default.png'
ret, info = put_file(token, key, localfile, version='v2')
print(info)
9、支付接口
# 登陆后才能访问, 前端支付按钮--->点击支付按钮,向后端发送post请求{courses:[1,2,3],total_amount:99}-->生成订单(订单表)--》生成支付链接--》返回给前端--->前端打开
# 扩写auth的user表,使用drf-jwt,提供的认证(认证类,权限类)
9.1 订单相关表设计
# 订单表
# 订单详情表
"""
class Order(models.Model):
# 主键、总金额、订单名、订单号、订单状态、创建时间、支付时间、流水号、支付方式、支付人(外键) - 优惠劵(外键,可为空)
pass
class OrderDetail(models.Model):
# 订单号(外键)、商品(外键)、实价、成交价 - 商品数量
pass
"""
# cd到apps目录下:
python ../../manage.py startapp order
# 到配置文件 dev.py中注册:
INSTALLED_APPS = [
...
'order',
]
luffy_api/apps/order/models.py:
from django.db import models
from user.models import User
from course.models import Course
class Order(models.Model):
"""订单模型"""
status_choices = (
(0, '未支付'),
(1, '已支付'),
(2, '已取消'),
(3, '超时取消'),
)
pay_choices = (
(1, '支付宝'),
(2, '微信支付'),
)
# 订单标题
subject = models.CharField(max_length=150, verbose_name="订单标题")
# 总价格
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总价", default=0)
# 订单号--使用uuid生成
out_trade_no = models.CharField(max_length=64, verbose_name="订单号", unique=True)
# 流水号支付宝返回的
trade_no = models.CharField(max_length=64, null=True, verbose_name="流水号")
# 订单状态 待支付,已支付。。。
order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
# 微信,支付宝
pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
# 支付时间--》支付宝回调回来会有
pay_time = models.DateTimeField(null=True, verbose_name="支付时间")
# 用户表关联
user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False,
verbose_name="下单用户")
# 订单创建时间 auto_now_add:新增这个时间可以不传,用当前时间 auto_now:修改时间不传,自动存入当前时间
created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = "luffy_order"
verbose_name = "订单记录"
verbose_name_plural = "订单记录"
def __str__(self):
return "%s - ¥%s" % (self.subject, self.total_amount)
@property
def courses(self):
data_list = []
for item in self.order_courses.all():
data_list.append({
"id": item.id,
"course_name": item.course.name,
"real_price": item.real_price,
})
return data_list
class OrderDetail(models.Model):
"""订单详情"""
# 跟订单一对多
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False,
verbose_name="订单")
# 跟课程一对多
course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, db_constraint=False,
verbose_name="课程")
# 价格
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
# 真实价格
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")
class Meta:
db_table = "luffy_order_detail"
verbose_name = "订单详情"
verbose_name_plural = "订单详情"
def __str__(self):
try:
return "%s的订单:%s" % (self.course.name, self.order.out_trade_no)
except:
return super().__str__()
# luffy_api/apps/order/admin.py:
from django.contrib import admin
from .models import *
admin.site.register(OrderDetail)
admin.site.register(Order)
### 迁移数据库:
cd 到项目根目录下
PS E:\Django\luffy_api> python manage.py makemigrations
PS E:\Django\luffy_api> python manage.py migrate
9.2 支付接口
路由:
# 总路由中 luffy_api/urls.py 添加:
path('api/v1/order/', include('order.urls')), # http://127.0.0.1:8000/api/v1/order/
# luffy_api/apps/order/urls.py中 添加:
from django.urls import path, include
from rest_framework.routers import SimpleRouter
from .views import OrderView
router = SimpleRouter()
# 支付接口 127.0.0.1:8000/api/v1/order/pay/ 携带headers/Authorization+ jwt tonken。# body如图
router.register('pay', OrderView, 'pay')
urlpatterns = [
path('', include(router.urls)),
]
# postman访问测试
新建序列化类 luffy_api/apps/order/serializer.py:
from .models import Order, OrderDetail
from course.models import Course
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
# 只用来做反序列化--->数据校验-->重写create方法--》存两个表
class OrderSerializer(serializers.ModelSerializer):
# 前端传入的是课程列表[1,2,3,]---》转成[课程对象1,课程对象2,课程对象三] 方案一
courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True)
# 前端传入的是课程列表[1,2,3,] 方案二
# courses = serializers.ListField()
class Meta:
model = Order
# 订单标题,总价格,支付方式,courses所有购买的课程id号列表
fields = ('subject', 'total_amount', 'pay_type', 'courses')
def _check_total_amount(self, attrs):
courses = attrs.get('courses') # 列表[课程1,课程2,课程3]
total_amount = attrs.get('total_amount') # 前端传入的总价格
total_price = 0
for course in courses:
total_price += course.price
if total_price != total_amount: # 计算完的价格不等于传入的价格,抛异常
raise ValidationError('total_amount error')
return total_amount
def _get_out_trade_no(self):
import uuid
code = str(uuid.uuid4()) # 分布式id的生成方案:订单号全局唯一,如何生成全局唯一的订单号:uuid,使用当前时间时间戳(重复概率),雪花算法
return code.replace('-', '')
# 获取支付人
def _get_user(self):
return self.context.get('request').user
# 获取支付链接
def _get_pay_url(self, out_trade_no, total_amount, subject):
from libs import iPay
from django.conf import settings
order_string = iPay.alipay.api_alipay_trade_page_pay(
out_trade_no=out_trade_no,
total_amount=float(total_amount), # 只有生成支付宝链接时,不能用Decimal
subject=subject,
return_url=settings.RETURN_URL, # get回调地址,前台地址
notify_url=settings.NOTIFY_URL, # post回调地址,后台地址
)
pay_url = iPay.gateway + '?' + order_string
# 将支付链接存入,传递给views
self.context['pay_url'] = pay_url
# 入库(两个表)的信息准备
def _before_create(self, attrs, user, out_trade_no):
attrs['user'] = user
attrs['out_trade_no'] = out_trade_no
def validate(self, attrs):
# 1)订单总价校验--
total_amount = self._check_total_amount(attrs)
# 2)生成订单号--》唯一的
out_trade_no = self._get_out_trade_no()
# 3)支付用户:request.user
user = self._get_user()
# 4)支付链接生成
self._get_pay_url(out_trade_no, total_amount, attrs.get('subject'))
# 5)入库(两个表)的信息准备
self._before_create(attrs, user, out_trade_no)
# 代表该校验方法通过,进入入库操作
return attrs
def create(self, validated_data):
courses = validated_data.pop('courses')
# 订单表入库,不需要courses, 订单号,订单标题,订单价格,购买人,支付方式
order = Order.objects.create(**validated_data)
# 订单详情表入库:只需要订单对象,课程对象(courses要拆成一个个course)
for course in courses:
OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)
return order
luffy_api/apps/order/views.py:
from utils.response import APIResponse
from .models import Order
from .serializer import OrderSerializer
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class OrderView(GenericViewSet, CreateModelMixin):
# 登陆后才能访问
authentication_classes = [JSONWebTokenAuthentication, ]
permission_classes = [IsAuthenticated]
queryset = Order.objects.all()
serializer_class = OrderSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
pay_url = serializer.context.get('pay_url')
self.perform_create(serializer)
return APIResponse(pay_url=pay_url)
luffy_api/setting/user_settings.py 添加:
# 用户自己的配置
# 上线后必须换成公网地址
# 后台基URL
BASE_URL = 'http://127.0.0.1:8000'
# 前台基URL
LUFFY_URL = 'http://127.0.0.1:8080'
# 支付宝同步异步回调接口配置
# 后台post异步回调接口
NOTIFY_URL = BASE_URL + "/api/v1/order/success/"
# 前台同步回调接口,没有 / 结尾
RETURN_URL = LUFFY_URL + "/pay/success"
10、支付前端页面展示
src/views/CourseDetail.vue内容:
<template>
<div class="detail">
<Header/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
<vue-core-video-player :src="mp4_url"
controls="auto"
autoplay
:muted="true"
title="致命诱惑"
@play="playFunc"
@pause="pauseFunc"
></vue-core-video-player>
</div>
<div class="wrap-right">
<h3 class="course-name">{{course_info.name}}</h3>
<p class="data">{{course_info.students}}人在学 课程总时长:{{course_info.sections}}课时/{{course_info.pub_sections}}小时 难度:{{course_info.level_name}}</p>
<div class="sale-time">
<p class="sale-type">价格 <span class="original_price">¥{{course_info.price}}</span></p>
<p class="expire"></p>
</div>
<div class="buy">
<div class="buy-btn">
<button class="buy-now" @click="go_pay(course_info)">立即购买</button>
<button class="free">免费试学</button>
</div>
<div class="add-cart" @click="add_cart(course_info.id)">
<img src="@/assets/img/cart-yellow.svg" alt="">加入购物车
</div>
</div>
</div>
</div>
<div class="course-tab">
<ul class="tab-list">
<li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
<li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span>
</li>
<li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论</li>
<li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
</ul>
</div>
<div class="course-content">
<div class="course-tab-list">
<div class="tab-item" v-if="tabIndex==1">
<div class="course-brief" v-html="course_info.brief"></div>
</div>
<div class="tab-item" v-if="tabIndex==2">
<div class="tab-item-title">
<p class="chapter">课程章节</p>
<p class="chapter-length">共{{course_chapters.length}}章 {{course_info.sections}}个课时</p>
</div>
<div class="chapter-item" v-for="chapter in course_chapters" :key="chapter.name">
<p class="chapter-title"><img src="@/assets/img/enum.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}
</p>
<ul class="section-list">
<li class="section-item" v-for="section in chapter.coursesections" :key="section.name">
<p class="name"><span class="index">{{chapter.chapter}}-{{section.orders}}</span>
{{section.name}}<span class="free" v-if="section.free_trail">免费</span></p>
<p class="time">{{section.duration}} <img src="@/assets/img/chapter-player.svg"></p>
<button class="try" v-if="section.free_trail">立即试学</button>
<button class="try" v-else>立即购买</button>
</li>
</ul>
</div>
</div>
<div class="tab-item" v-if="tabIndex==3">
用户评论
</div>
<div class="tab-item" v-if="tabIndex==4">
常见问题
</div>
</div>
<div class="course-side">
<div class="teacher-info">
<h4 class="side-title"><span>授课老师</span></h4>
<div class="teacher-content">
<div class="cont1">
<img :src="course_info.teacher.image">
<div class="name">
<p class="teacher-name">{{course_info.teacher.name}}
{{course_info.teacher.title}}</p>
<p class="teacher-title">{{course_info.teacher.signature}}</p>
</div>
</div>
<p class="narrative">{{course_info.teacher.brief}}</p>
</div>
</div>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "@/components/Header"
import Footer from "@/components/Footer"
export default {
name: "Detail",
data() {
return {
tabIndex: 2, // 当前选项卡显示的下标
course_id: 0, // 当前课程信息的ID
course_info: {
teacher: {},
}, // 课程信息
course_chapters: [], // 课程的章节课时列表
// mp4_url:'http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4',
mp4_url: [
{
src: 'http://rb1x3v1fm.sabkt.gdipper.com/%E8%87%B4%E5%91%BD%E8%AF%B1%E6%83%91320p.mp4',
resolution: 360,
},
{
src: 'http://rb1x3v1fm.sabkt.gdipper.com/%E8%87%B4%E5%91%BD%E8%AF%B1%E6%83%91720p.mp4',
resolution: 720,
},
{
src: 'http://rb1x3v1fm.sabkt.gdipper.com/%E8%87%B4%E5%91%BD%E8%AF%B1%E6%83%914k.mp4',
resolution: '4k',
}],
}
},
created() {
this.get_course_id();
this.get_course_data();
this.get_chapter();
},
methods: {
go_pay(course_info) {
// 1 去cookie中取token,如果没有说明没登陆,不允许购买
let token = this.$cookies.get("token")
if (token) {
this.$axios.post(this.$settings.base_url + 'order/pay/', {
"subject": course_info.name,
"total_amount": course_info.price,
"pay_type": 1,
"courses": [course_info.id]
}, {
headers: {
authorization: 'jwt ' + token,
}
}).then(res => {
if (res.data.status = 100) {
let pay_url = res.data.pay_url
// 跳转,在当前窗口打开这个链接
open(pay_url, '_self');
} else {
this.$message({
message: "下单失败,请联系统管理员"
});
}
})
} else {
this.$message({
message: "对不起,您没有登录,请登陆后购买!"
});
}
},
playFunc() {
console.log('开始了')
},
pauseFunc() {
console.log('暂停了')
},
get_course_id() {
// 获取地址栏上面的课程ID
this.course_id = this.$route.params.pk
if (this.course_id < 1) {
let _this = this;
_this.$alert("对不起,当前视频不存在!", "警告", {
callback() {
_this.$router.go(-1);
}
});
}
},
get_course_data() {
// ajax请求课程信息
this.$axios.get(`${this.$settings.base_url}course/actual/${this.course_id}/`).then(response => {
// window.console.log(response.data);
this.course_info = response.data;
console.log(this.course_info)
}).catch(() => {
this.$message({
message: "对不起,访问页面出错!请联系客服工作人员!"
});
})
},
get_chapter() {
// 获取当前课程对应的章节课时信息
// http://127.0.0.1:8000/course/chapters/?course=(pk)
this.$axios.get(`${this.$settings.base_url}course/chapter/`, {
params: {
"course_id": this.course_id,
}
}).then(response => {
this.course_chapters = response.data;
}).catch(error => {
window.console.log(error.response);
})
},
},
components: {
Header,
Footer,
}
}
</script>
<style scoped>
.main {
background: #fff;
padding-top: 30px;
}
.course-info {
width: 1200px;
margin: 0 auto;
overflow: hidden;
}
.wrap-left {
float: left;
width: 690px;
height: 388px;
background-color: #000;
}
.wrap-right {
float: left;
position: relative;
height: 388px;
}
.course-name {
font-size: 20px;
color: #333;
padding: 10px 23px;
letter-spacing: .45px;
}
.data {
padding-left: 23px;
padding-right: 23px;
padding-bottom: 16px;
font-size: 14px;
color: #9b9b9b;
}
.sale-time {
width: 464px;
background: #fa6240;
font-size: 14px;
color: #4a4a4a;
padding: 10px 23px;
overflow: hidden;
}
.sale-type {
font-size: 16px;
color: #fff;
letter-spacing: .36px;
float: left;
}
.sale-time .expire {
font-size: 14px;
color: #fff;
float: right;
}
.sale-time .expire .second {
width: 24px;
display: inline-block;
background: #fafafa;
color: #5e5e5e;
padding: 6px 0;
text-align: center;
}
.course-price {
background: #fff;
font-size: 14px;
color: #4a4a4a;
padding: 5px 23px;
}
.discount {
font-size: 26px;
color: #fa6240;
margin-left: 10px;
display: inline-block;
margin-bottom: -5px;
}
.original {
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
text-decoration: line-through;
}
.buy {
width: 464px;
padding: 0px 23px;
position: absolute;
left: 0;
bottom: 20px;
overflow: hidden;
}
.buy .buy-btn {
float: left;
}
.buy .buy-now {
width: 125px;
height: 40px;
border: 0;
background: #ffc210;
border-radius: 4px;
color: #fff;
cursor: pointer;
margin-right: 15px;
outline: none;
}
.buy .free {
width: 125px;
height: 40px;
border-radius: 4px;
cursor: pointer;
margin-right: 15px;
background: #fff;
color: #ffc210;
border: 1px solid #ffc210;
}
.add-cart {
float: right;
font-size: 14px;
color: #ffc210;
text-align: center;
cursor: pointer;
margin-top: 10px;
}
.add-cart img {
width: 20px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.course-tab {
width: 100%;
background: #fff;
margin-bottom: 30px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course-tab .tab-list {
width: 1200px;
margin: auto;
color: #4a4a4a;
overflow: hidden;
}
.tab-list li {
float: left;
margin-right: 15px;
padding: 26px 20px 16px;
font-size: 17px;
cursor: pointer;
}
.tab-list .active {
color: #ffc210;
border-bottom: 2px solid #ffc210;
}
.tab-list .free {
color: #fb7c55;
}
.course-content {
width: 1200px;
margin: 0 auto;
background: #FAFAFA;
overflow: hidden;
padding-bottom: 40px;
}
.course-tab-list {
width: 880px;
height: auto;
padding: 20px;
background: #fff;
float: left;
box-sizing: border-box;
overflow: hidden;
position: relative;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item {
width: 880px;
background: #fff;
padding-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item-title {
justify-content: space-between;
padding: 25px 20px 11px;
border-radius: 4px;
margin-bottom: 20px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
overflow: hidden;
}
.chapter {
font-size: 17px;
color: #4a4a4a;
float: left;
}
.chapter-length {
float: right;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
}
.chapter-title {
font-size: 16px;
color: #4a4a4a;
letter-spacing: .26px;
padding: 12px;
background: #eee;
border-radius: 2px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
}
.chapter-title img {
width: 18px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.section-list {
padding: 0 20px;
}
.section-list .section-item {
padding: 15px 20px 15px 36px;
cursor: pointer;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.section-item .name {
font-size: 14px;
color: #666;
float: left;
}
.section-item .index {
margin-right: 5px;
}
.section-item .free {
font-size: 12px;
color: #fff;
letter-spacing: .19px;
background: #ffc210;
border-radius: 100px;
padding: 1px 9px;
margin-left: 10px;
}
.section-item .time {
font-size: 14px;
color: #666;
letter-spacing: .23px;
opacity: 1;
transition: all .15s ease-in-out;
float: right;
}
.section-item .time img {
width: 18px;
height: 18px;
margin-left: 15px;
vertical-align: text-bottom;
}
.section-item .try {
width: 86px;
height: 28px;
background: #ffc210;
border-radius: 4px;
font-size: 14px;
color: #fff;
position: absolute;
right: 20px;
top: 10px;
opacity: 0;
transition: all .2s ease-in-out;
cursor: pointer;
outline: none;
border: none;
}
.section-item:hover {
background: #fcf7ef;
box-shadow: 0 0 0 0 #f3f3f3;
}
.section-item:hover .name {
color: #333;
}
.section-item:hover .try {
opacity: 1;
}
.course-side {
width: 300px;
height: auto;
margin-left: 20px;
float: right;
}
.teacher-info {
background: #fff;
margin-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.side-title {
font-weight: normal;
font-size: 17px;
color: #4a4a4a;
padding: 18px 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.side-title span {
display: inline-block;
border-left: 2px solid #ffc210;
padding-left: 12px;
}
.teacher-content {
padding: 30px 20px;
box-sizing: border-box;
}
.teacher-content .cont1 {
margin-bottom: 12px;
overflow: hidden;
}
.teacher-content .cont1 img {
width: 54px;
height: 54px;
margin-right: 12px;
float: left;
}
.teacher-content .cont1 .name {
float: right;
}
.teacher-content .cont1 .teacher-name {
width: 188px;
font-size: 16px;
color: #4a4a4a;
padding-bottom: 4px;
}
.teacher-content .cont1 .teacher-title {
width: 188px;
font-size: 13px;
color: #9b9b9b;
white-space: nowrap;
}
.teacher-content .narrative {
font-size: 14px;
color: #666;
line-height: 24px;
}
</style>
11、支付成功回调接口
luffy_api/apps/order/views.py 添加:
from utils.my_logging import logger
from rest_framework.views import APIView
from rest_framework.response import Response
# 支付回调接口
class SuccessViewSet(APIView):
# 认证取消
authentication_classes = ()
permission_classes = ()
# 支付宝同步回调给前台,在同步通知给后台处理
# 写不写都行---》给咱们前端做二次验证的
def get(self, request, *args, **kwargs):
# return Response('后台已知晓,Over!!!')
# 不能在该接口完成订单修改操作
# 但是可以在该接口中校验订单状态(已经收到支付宝post异步通知,订单已修改),告诉前台
# print(type(request.query_params)) # django.http.request.QueryDict
# print(type(request.query_params.dict())) # dict
out_trade_no = request.query_params.get('out_trade_no')
try:
Order.objects.get(out_trade_no=out_trade_no, order_status=1)
return APIResponse(status=100, msg='订单支付成功')
except:
return APIResponse(status=101, msg='订单还未支付')
# 支付宝异步回调处理
def post(self, request, *args, **kwargs):
try:
# request.data前端(支付宝)post传给咱们的数据--》request.data--》 QueyDic对象,不允许pop,把它转成字典
result_data = request.data.dict()
# 支付宝给我的订单号---》数据库有个订单号
out_trade_no = result_data.get('out_trade_no')
# 前面-->验证签名才信任支付宝,防止伪造
signature = result_data.pop('sign')
from libs import iPay
result = iPay.alipay.verify(result_data, signature)
if result and result_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
# 完成订单修改:订单状态、流水号、支付时间
# 已支付
Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1)
# 完成日志记录
logger.warning('%s订单支付成功' % out_trade_no)
return Response('success')
else:
logger.error('%s订单支付失败' % out_trade_no)
except:
pass
return Response('failed')
luffy_api/apps/order/urls.py添加路由:
path('success/', SuccessViewSet.as_view()),
新建支付成功前端回调页面views/PaySuccess.vue:
<template>
<div class="pay-success">
<!--如果是单独的页面,就没必要展示导航栏(带有登录的用户)-->
<Header/>
<div class="main">
<div class="title">
<div class="success-tips">
<p class="tips">您已成功购买 1 门课程!</p>
</div>
</div>
<div class="order-info">
<p class="info"><b>订单号:</b><span>{{ result.out_trade_no }}</span></p>
<p class="info"><b>交易号:</b><span>{{ result.trade_no }}</span></p>
<p class="info"><b>付款时间:</b><span><span>{{ result.timestamp }}</span></span></p>
</div>
<div class="study">
<span>立即学习</span>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/Header"
export default {
name: "Success",
data() {
return {
result: {},
};
},
created() {
// url后拼接的参数:?及后面的所有参数 => ?a=1&b=2
// console.log(location.search);
// 解析支付宝回调的url参数
let params = location.search.substring(1); // 去除? => a=1&b=2
let items = params.length ? params.split('&') : []; // ['a=1', 'b=2']
//逐个将每一项添加到args对象中
for (let i = 0; i < items.length; i++) { // 第一次循环a=1,第二次b=2
let k_v = items[i].split('='); // ['a', '1']
//解码操作,因为查询字符串经过编码的
if (k_v.length >= 2) {
// url编码反解
let k = decodeURIComponent(k_v[0]);
this.result[k] = decodeURIComponent(k_v[1]);
// 没有url编码反解
// this.result[k_v[0]] = k_v[1];
}
}
// 解析后的结果
// console.log(this.result);
// 把地址栏上面的支付结果,再get请求转发给后端
this.$axios({
url: this.$settings.base_url + 'order/success/' + location.search,
method: 'get',
}).then(response => {
console.log(response.data)
if (response.data.status != 100) {
alert('暂时还没收到您的支付,请稍后刷新再试')
}
}).catch(() => {
console.log('支付结果同步失败');
})
},
components: {
Header,
}
}
</script>
<style scoped>
.main {
padding: 60px 0;
margin: 0 auto;
width: 1200px;
background: #fff;
}
.main .title {
display: flex;
-ms-flex-align: center;
align-items: center;
padding: 25px 40px;
border-bottom: 1px solid #f2f2f2;
}
.main .title .success-tips {
box-sizing: border-box;
}
.title img {
vertical-align: middle;
width: 60px;
height: 60px;
margin-right: 40px;
}
.title .success-tips {
box-sizing: border-box;
}
.title .tips {
font-size: 26px;
color: #000;
}
.info span {
color: #ec6730;
}
.order-info {
padding: 25px 48px;
padding-bottom: 15px;
border-bottom: 1px solid #f2f2f2;
}
.order-info p {
display: -ms-flexbox;
display: flex;
margin-bottom: 10px;
font-size: 16px;
}
.order-info p b {
font-weight: 400;
color: #9d9d9d;
white-space: nowrap;
}
.study {
padding: 25px 40px;
}
.study span {
display: block;
width: 140px;
height: 42px;
text-align: center;
line-height: 42px;
cursor: pointer;
background: #ffc210;
border-radius: 6px;
font-size: 16px;
color: #fff;
}
</style>
添加前端路由 router/index.js:
import PaySuccess from "@/views/PaySuccess";
{
path: '/pay/success',
name: 'pay-success',
component: PaySuccess
},
12、内网穿透
# 内网穿透基本都要花钱:花生壳,frp...
# 一般都要收费---》买个公网服务器上线后才能正常回调
https://zhuanlan.zhihu.com/p/370483324