上节回顾

# 1 课程相关数据录入
-simpleui中录入
-使用sql录入,在media下图片copy过去 # 2 课程分类接口 # 3 查询所有课程接口
-带排序:人气,价格 内置排序类
-带过滤:course_category=1
-第三方
-带分页:pagination_class -序列化类:
-序列化字段
-表自己的字段----》按照原型图分析
-关联字段----》teacher:子序列化,当前要序列化的对象中,有该字段 -自定制字段---》
section_list(表模型中):逻辑是,根据课程取出所有章节(反向查询的字段),
循环所有章节再取出章节下所有的课时(反向查询的字段),拼接出前端要的格式,每次
append完,都判断长度是否大于4
course_type_name 系列:models.py的表模型中必须写一个方法与之对应choice,显示中文 # 4 课程详情接口
# 方案一:
课程列表接口,继承一个类。
-使用的序列化类还是CourseSerializer,section_list不全
-就想在查询课程详情的序列化类中有所有章节和所有章节下的课时
-如果还用这个序列化类,加个章节字段
-如果不用这个序列化类,自己需要再写一个序列化类
重写一下get_serializer_class,通过 action判断,如果是retrieve,就用咱们新写的(查询课程详情时,使用别的序列化类) # 方案二(当前项目使用的方法):
-就是用了查询所有的序列化类,缺所有章节和课时
-又写了一个接口,查询所有章节---》可以通过课程过滤 # 5 查询所有章节接口,带按课程过滤功能
-序列化类,子序列化,多条 # 6 补充:
以后一个前端页面,可能访问了N多接口
一般不要在一个接口,返回所有数据 # 7 课程等级用 level
-好处:
1 存储的数字,有choice对应,可以拿出中文或数字来
2 占用空间小
-缺点:
1 前端可能不知道数字的对应关系 -choice字段,写死了,后期项目如果要新增一个 等级,需要停止项目,修改源代码,再发布新版本
-如果不想停止项目,不改源代码,也能增加一个 新 等级
-本质level 需要对应另一个表,是个一对多的关系,只不过使用choice,不需要建立表了 # 8 前端复制 列表页面 详情页面
-watch 当某个查询参数发生变化,再发送请求查询所有数据 -详情页面:视频播放器组件
vue-core-video-player
mp4,flv。。。。
m3u8视频类型 # 9 模型层补充
-ForeignKey:to to_field on_delete ...
-on_delete
-db_constraint:外键管理,默认为True,表明要建立外键关联,navicate的er图,看到一条线
-外键关联好处是:保证关联数据的准确性
-外键的坏处是:响应性能,公司里一般不加,有外键的关系,但是没有约束,就可以录入脏数据,使用程序层面控制 -related_name: 基于对象的跨表查,反向查询,替换的字段
select * from book where id = 1
book对象----》book.publish--->出版社对象 select * from publish where id=book.publish_id
select * from book inner join publish on book.publish=publish.id
-related_query_name: 基于连表的跨表查,反向查询,替换的表名

课程详情接口

课程详情接口方案一相关解释:

查询所有和查询单条接口序列化的字段不一样。

choice字段

choice -->  改源码 --> 运行效率高
添加新表 --> 改数据库--> 连表效率低

今日内容

1 文件存储

# 视频文件,存储到某个位置,如果放在自己服务器上
-放在项目的media文件夹
-服务器上线后,用户既要访问接口,又要看视频,都是一个域名和端口
-分开:文件单独放在文件服务器上,文件服务器带宽很高 # 文件服务器:专门存储文件的服务器
-第三方:
-阿里云:对象存储 oss
-腾讯对象存储
-七牛云存储
-上海公司
-技术驱动型公司:ceo 许式伟 -国内第一批使用go语言的,全线go语言
-go+ 语言
-自己搭建:
-ceph
-fastdfs:文件对象存储
-minio:自己搭建 # python 如何把文件传到上面
-对应sdk # 七牛云存储
-七牛云:
-注册账号
-创建空间(桶)
- 七牛云提供存储空间和测试域名
-上传文件:
-1 直接上传
-2 使用代码上传
-python:sdk
-前端上传:js代码案例 -两种方案
-前端---》传到咱们的服务器----》在服务端再传入到七牛云 # 公司内部存储:fastdfs
https://zhuanlan.zhihu.com/p/372286804 # MinIO
-部署完:http://docs.minio.org.cn/docs/master/minio-docker-quickstart-guide
-web管理页面:类似于七牛云
-python的sdk上传

视频放在自己的media文件夹下,只要来了一个请求,就会建立一个http链接,一直输送视频。会占用我们整个项目的并发量,导致项目性能变低。

所以我们使用第三方储存视频,看视频的请求,请求的是第三方的服务器,不占用我们自己项目的并发量。我们项目的数据库,就只需要存储第三方视频服务的链接即可。

为了安全性考虑,公司也可以自己搭建文件服务器。

手动上传文件:

python sdk 上传文件:可以使用celery实现异步上传文件。

# 上传文件的方式:
1. 前端 --> django后端 --> 七牛云 --> django后端存储七牛云地址
2. 前端 --> 七牛云 -- 七牛云返回链接 --> 前端传链接到后端接口 --> 后端存储到数据库(视频不经过我们的项目后端)

1.1 七牛云上传文件

from qiniu import Auth,put_file

q = Auth('','')
# 要上传的空间
bucket_name = 'lqz'
# 上传后保存的文件名
key = '致命诱惑.mp4'
# 生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)
# 要上传文件的本地路径
localfile = './致命诱惑.mp4'
ret, info = put_file(token, key, localfile, version='v2')
print(info)

2 搜索导航栏

# 前端  Header 组件上有个搜索框----》输入内容,搜索----》后端搜索接口

# 所有的商城类的网站,app,都会有搜索功能,其实搜索功能非常复杂且技术含量高
-咱们目前只是简单的搜索,输入课程名字,价格 就可以把实战课搜出来
-输入:课程名字,价格把所有类型课程都搜出来(查询多个表)
-后面会有专门的搜索引擎:分布式全文检索引擎 es 做专门的搜索 # 前端搜索结果呈现页面

查看搜索框:

点击搜索按钮,跳转到搜索页面:(也可以通过@keyup.enter.native监听回车键来实现)

所以我们需要新建一个搜索页面。并且将搜索信息search_word传入这个搜索页面。

在新页面,获取搜索信息:

搜索页面监听属性:

当搜索条件发生变化,会重新运行函数。

2.1 Header.vue

<template>
<div class="header">
<div class="slogan">
<p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
</div>
<div class="nav">
<ul class="left-part">
<li class="logo">
<router-link to="/">
<img src="../assets/img/head-logo.svg" alt="">
</router-link>
</li>
<li class="ele">
<span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
</li>
<li class="ele">
<span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
</li>
<li class="ele">
<span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
</li>
</ul> <div class="right-part">
<div v-if="!username">
<span @click="put_login">登录</span>
<span class="line">|</span>
<span @click="put_register">注册</span>
</div>
<div v-else>
<span>{{ username }}</span>
<span class="line">|</span>
<span>注销</span>
</div> </div>
</div> <Login v-if="is_login" @close="close_login" @go="put_register" @success="success_login"/>
<Register v-if="is_register" @close="close_register" @go="put_login" @success="success_register"/>
<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> </template> <script>
import Login from "@/components/Login";
import Register from "@/components/Register"; export default {
name: "Header",
data() {
return {
// 当前所在路径,去sessionStorage取的,如果取不到,就是 /
url_path: sessionStorage.url_path || '/',
is_login: false,
is_register: false,
username: this.$cookies.get('username'),
token: this.$cookies.get('token'),
is_search_tip: true,
search_placeholder: '',
search_word: ''
}
},
methods: {
search_action(search_word) {
console.log(search_word)
if (!search_word) {
this.$message('请输入要搜索的内容');
return
}
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;
},
goPage(url_path) {
// 已经是当前路由就没有必要重新跳转
if (this.url_path !== url_path) {
this.$router.push(url_path);
}
sessionStorage.url_path = url_path;
},
put_login() {
this.is_login = true;
this.is_register = false;
},
put_register() {
this.is_login = false;
this.is_register = true;
},
close_login() {
this.is_login = false;
},
close_register() {
this.is_register = false;
},
success_login() {
this.is_login = false;
this.username = this.$cookies.get('username')
this.token = this.$cookies.get('token')
},
success_register() {
this.is_login = true
this.is_register = false }
},
created() {
// 组件加载万成,就取出当前的路径,存到sessionStorage this.$route.path
sessionStorage.url_path = this.$route.path;
// 把url_path = 当前路径
this.url_path = this.$route.path;
},
components: {
Login,
Register
}
}
</script> <style scoped>
.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;
} .header {
background-color: white;
box-shadow: 0 0 5px 0 #aaa;
} .header:after {
content: "";
display: block;
clear: both;
} .slogan {
background-color: #eee;
height: 40px;
} .slogan p {
width: 1200px;
margin: 0 auto;
color: #aaa;
font-size: 13px;
line-height: 40px;
} .nav {
background-color: white;
user-select: none;
width: 1200px;
margin: 0 auto; } .nav ul {
padding: 15px 0;
float: left;
} .nav ul:after {
clear: both;
content: '';
display: block;
} .nav ul li {
float: left;
} .logo {
margin-right: 20px;
} .ele {
margin: 0 20px;
} .ele span {
display: block;
font: 15px/36px '微软雅黑';
border-bottom: 2px solid transparent;
cursor: pointer;
} .ele span:hover {
border-bottom-color: orange;
} .ele span.active {
color: orange;
border-bottom-color: orange;
} .right-part {
float: right;
} .right-part .line {
margin: 0 10px;
} .right-part span {
line-height: 68px;
cursor: pointer;
}
</style>

3 搜索接口

# 搜索接口:get请求---》/course/search/---》要搜索的条件通过问号拼接在后面
#### 路由
router.register('search', views.SearchCourseView, 'search') #### 视图类
class SearchCourseView(GenericViewSet, CommonListModelMixin):
# 只有实战课
queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders')
serializer_class = CourseSerializer
filter_backends = [SearchFilter, ]
search_fields = ['name', 'price']
pagination_class = PageNumberPagination

当前搜索页面只能搜索实战课,而真正的搜索应该是什么都能搜的。所以我们需要重写list方法,拿着搜索字段,去其他表搜。

也就是在list方法默认是查询当前视图类所对应的模型,而我们也可以查询别的表的数据。

4 搜索页面

路由配置:

{
path: '/course/search',
name: 'search',
component: SearchView
},

代码:

<template>

  <div>您要搜索的内容是:{{search_word}}</div>

</template>

<script>
export default {
name: "SearchCourse",
data(){
return {
search_word:''
}
},
created() {
this.search_word=this.$route.query.word
// 向后端发送请求 }
}
</script> <style scoped> </style>
<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.$route.query.wd; // 获取课程列表信息
this.$axios.get(`${this.$settings.BASE_URL}/course/search/`, {
params: this.filter
}).then(response => {
console.log(response)
// 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
this.course_list = response.data.data.results;
this.course_total = response.data.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>

5 支付宝支付介绍

# 立即购买功能,点击立即购买按钮,使用支付宝支付

# 项目中需要有在线支付功能
-支付宝支付
-微信支付:备案过域名
-云闪付 # 支付宝支付
-测试环境:大家都可以测试
-https://openhome.alipay.com/develop/sandbox/app
-正式环境:申请,有营业执照
-咱们做不了 # 咱们开发虽然用的沙箱环境,后期上线,公司会自己注册,注册成功后有个商户id号,作为开发,只要有商户id好,其他步骤都是一样,所有无论开发还是测试,代码都一样,只是商户号不一样 # 支付流程 # 使用支付宝支付
-API接口
-SDK:优先使用,早期,支付宝没有python的sdk,后期有了
-使用了第三方sdk
-第三方人通过api接口,使用python封装了sdk,开源出来了 # 沙箱环境
-介绍:Sandbox。程序的虚拟执行环境,不需要申请各种认证,直接写程序,后期只需要换成真实的秘钥即可
-安卓的支付宝app,付款用的---》买家用的
-扫码使用这个app,付款,这个app的钱都是假的,付款进测试商户(卖家)

有两个回调 一个get回调、一个post回调。post回调是支付宝发生给我们后端的,包含了用户是否付款的信息。

沙箱环境:

还需要下个安卓app,用于沙箱环境测试。这个app可以模拟买家和卖家:

5.1 支付测试,生成支付链接

# https://github.com/fzlee/alipay
# 安装
pip install python-alipay-sdk # 生成公钥私钥---》非对称加密
-使用支付宝提供的工具:https://opendocs.alipay.com/common/02kipl
-就可以生成公钥私钥 -生成的公钥---》配置在支付宝的网站上(沙箱环境)---》生成一个支付宝公钥---》以后使用这个支付宝公钥
# 支付宝公钥,应用私钥---》放到项目中
-pub.pem
-pri.pem # 代码
from alipay import AliPay
from alipay.utils import AliPayConfig
app_private_key_string = open("./pri.pem").read()
alipay_public_key_string = open("./pub.pem").read()
alipay = AliPay(
appid="2016092000554611",
app_notify_url=None, # 默认回调 url
app_private_key_string=app_private_key_string,
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_string=alipay_public_key_string,
sign_type="RSA2", # RSA 或者 RSA2
debug=False, # 默认 False
verbose=False, # 输出调试数据
config=AliPayConfig(timeout=15) # 可选,请求超时时间
)
res=alipay.api_alipay_trade_page_pay(subject='性感内衣', out_trade_no='asdas23sddfsasf', total_amount='999')
print('https://openapi.alipaydev.com/gateway.do?'+res)

下载生成公钥私钥的软件:

生成公钥私钥:

配置公钥到沙箱环境:

支付宝又会生成一个公钥,以后我们使用支付宝生成的公钥,配置在脚本中。私钥还是使用我们自己生成的私钥。

公钥还要遵循一些格式:

私钥格式:

向里面插入公钥私钥。

会生成链接:

这个链接需要拼接上支付宝网关。

可以访问到支付宝的界面:

6 支付宝二次封装

6.1 目录结构

libs
├── iPay # aliapy二次封装包
│ ├── __init__.py # 包文件
│ ├── pem # 公钥私钥文件夹
│ │ ├── alipay_public_key.pem # 支付宝公钥文件
│ │ ├── app_private_key.pem # 应用私钥文件
│ ├── pay.py # 支付文件
└── └── settings.py # 应用配置 # 保证公钥私钥的安全:
1 环境变量
2 专门写一个公钥私钥管理的服务(项目),发送请求去获取,接口中带很多认证
- 加密解密,如果是明文数据,可能会被拦截获取

init.py

from .pay import alipay
from .settings import GATEWAY

pay.py

from alipay import AliPay
from alipay.utils import AliPayConfig
from . import settings
alipay = AliPay(
appid=settings.APP_ID,
app_notify_url=None, # 默认回调 url
app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
sign_type=settings.SIGN, # RSA 或者 RSA2
debug=settings.DEBUG, # 默认 False
verbose=settings.DEBUG, # 输出调试数据
config=AliPayConfig(timeout=15) # 可选,请求超时时间
) # res = alipay.api_alipay_trade_page_pay(subject='性感内衣', out_trade_no='asdas23sddfsasf', total_amount='999')
# print('https://openapi.alipaydev.com/gateway.do?' + res)

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?'

登录买家账号,进行沙箱测试。

7 订单表设计

# 订单板块需要写的接口
-下单接口---》没有支付是订单是待支付状态
-支付宝post回调接口---》修改订单状态成已支付
-前端get回调接口(暂时先不关注)
# 订单板块表设计
-订单表
-订单详情表 # 新建order 的app,在models.py中写入表
from django.db import models 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(可能重复,概率很多) 【分布式id的生成】 雪花算法
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="支付时间")
# 跟用户一对多 models.DO_NOTHING
user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False,
verbose_name="下单用户")
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) class OrderDetail(models.Model):
"""订单详情"""
# related_name 反向查询替换表名小写_set
# on_delete 级联删除
# db_constraint=False ----》默认是True,会在表中为Order何OrderDetail创建外键约束
# db_constraint=False 没有外键约束,插入数据 速度快, 可能会产生脏数据【不合理】,所以咱们要用程序控制,以后公司惯用的
# 对到数据库上,它是不建立外键,基于对象的跨表查,基于连表的查询,继续用,跟之前没有任何区别
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.DO_NOTHING, 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__()

关于订单号:

订单号的生成:

使用uuid4,但是并发量高的时候,uuid4可能重复。

uuid3、1需要传入参数,重复概率更低。

企业级项目分布式id的生成:使用雪花算法(重复概率更低)

订单详情:

使用db_constraint=False可能会插入脏数据,需要用程序去控制数据输入。使用db_constraint=False,实际上是数据库层面不建立外键关系,而是用程序写逻辑建立外键关系。跨表查询还是可以使用,因为跨表查询是基于django orm。

7.1 双下str异常捕获

这里使用了跨表查询,如果发生报错就会抛出异常,导致程序崩了。

7.2 ForeignKey on_delete补充


# ForeignKey 中 on_delete
-CASCADE 级联删除
-DO_NOTHING 啥都不做,没有外键约束才能用它(db_constraint=False)
-SET_NULL 字段置为空,字段需要满足null=True
-SET_DEFAULT 设置为默认值,default='xx'
-PROTECT 受保护的,被设置的数据不能删除。很少用。
-models.SET(函数内存地址) 当外键删除时,会将字段设置成set内函数的返回值(可以在函数内做日志记录)

8 生成订单接口

# 登录后才能用---》前端点击立即购买----》post--》携带数据  {courses:[1,],total_amount:99.9,subject:'xx课程'}----》视图类中重写create方法---》主要逻辑写到序列化类中

# 主要逻辑:
1 取出所有课程id号,拿到课程
2 统计总价格,跟传入的total_amount做比较,如果一样,继续往后
3 获取购买人信息:登录后才能访问的接口 request.user
4 生成订单号 支付链接需要,存订单表需要
5 生成支付链接:支付宝支付生成, 6 生成订单记录,订单是待支付状态(order,order_detail)
7 返回前端支付链接 '''下单接口需要加认证类。'''

8.1 视图类

class PayView(GenericViewSet, CreateModelMixin):
queryset = Order.objects.all()
serializer_class = PaySerializer
# 配置认证类
authentication_classes = [JSONWebTokenAuthentication]
permission_classes = [IsAuthenticated] # 下单接口
def create(self, request, *args, **kwargs):
# 注意:把request对象,传入到序列化类中
serializer = self.get_serializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
pay_url = serializer.context.get('pay_url')
return APIResponse(pay_url=pay_url)

视图类:

序列化类中要使用request对象,所以可以将request传入context上下文,在序列化类使用。

8.2 序列化类

# 校验字段,反序列化      不会序列化的
class PaySerializer(serializers.ModelSerializer):
# courses 不是表的字段,需要重写--->新东西
# courses=serializers.ListField() # 咱们不用这种 courses=[1,2,3] # 前端传入的 courses=[1,2,3]--->根据queryset对应的qs对象 做映射,映射成courses=[课程对象1,课程对象2,课程对象3]
courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True) class Meta:
model = Order
fields = ['courses', 'total_amount', 'subject'] # 前端传入的字段是什么,这里就写什么 def _check_total_amount(self, attrs):
courses = attrs.get('courses') # 课程对象列表 [课程对象1,课程对象2]
total_amount = attrs.get('total_amount')
new_total_amount = 0
for course in courses:
new_total_amount += course.price
if total_amount == new_total_amount:
return new_total_amount
raise APIException('价格有误!!') def _get_out_trade_no(self):
# uuid生成
return str(uuid.uuid4()) def _get_user(self):
user = self.context.get('request').user
return user def _get_pay_url(self, out_trade_no, total_amount, subject):
# 生成支付链接
res = alipay.api_alipay_trade_page_pay(
total_amount=float(total_amount),
subject=subject,
out_trade_no=out_trade_no,
return_url=settings.RETURN_URL, # 前端的
notify_url=settings.NOTIFY_URL # 后端接口,写这个接口该订单状态 )
# return GATEWAY + res
self.context['pay_url'] = GATEWAY + res def _before_create(self, attrs, user, out_trade_no):
# 剔除courses----》要不要剔除,要pop,但是不在这,在create方法中pop
# 订单号,加入到attrs中
attrs['out_trade_no'] = out_trade_no
# 把user加入到attrs中
attrs['user'] = user 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 # 生成订单,存订单表,一定要重写create,存俩表
def create(self, validated_data):
# validated_data:{subject,total_amount,user,out_trade_no,courses}
courses = validated_data.pop('courses')
order = Order.objects.create(**validated_data)
# 存订单详情表,存几条,取决于courses有几个
for course in courses:
OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price) return order

我们还是在全局钩子里写逻辑。

分析我们要使用序列化类做的事情:校验字段、反序列化。(不做序列化)

courses不是订单表的字段,需要在序列化类重写。courses是个列表,需要使用ListField。但是还有别的方法:

因为是反序列化多条数据,所以要加many=True

全局钩子:

支付链接需要生成订单号、存订单表也需要订单号。

生成订单号:

强转str:

通过request获取当前下单的用户:

获取支付链接:

return_url:后端回调地址

notify_url:前端回调地址

在settings配置回调地址:

将支付链接存到上下文。

入库前的准备:

需要剔除courses因为这不是订单表的字段。但是订单详情需要courses字段,所以不在这里剔除。

重写create方法:因为我们要存两个表(订单、订单详情)

测试:

匿名用户报错:

原因是没有配置认证类。

配置认证类:

设置过期时间:

JWT_AUTH = {
# 过期时间1天
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}

登录:

8.3 路由

# http://127.0.0.1:8000/api/v1/order/pay/pay/
router.register('pay',views.PayView,'pay')

9 前端支付页面

登录之后才能向后端发送请求:

需要携带token向后端发送请求。

数据库查看订单状态:

支付宝支付成功之后,会回调到前端地址:

所以要在前端再写一个支付成功页面:

数据是从地址栏解析出来的:

实际代码:

来到前端的支付成功页面之后,不一定真正的支付成功了。只有后端的订单状态修改了,才是真正的支付成功了。

9.1 CourseDetail.vue

go_pay() {
// 判断是否登录
let token = this.$cookies.get('token')
if (token) {
this.$axios.post(this.$settings.BASE_URL + '/order/pay/', {
subject: this.course_info.name,
total_amount: this.course_info.price,
courses: [this.course_id]
}, {
headers: {
Authorization: `jwt ${token}`
}
}).then(res => {
if (res.data.code == 100) {
// 打开支付连接地址
open(res.data.pay_url, '_self');
} else {
this.$message(res.data.msg)
}
})
} else {
this.$message('您没有登录,请先登录')
}
}

9.2 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参数
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];
} } // 把地址栏上面的支付结果,再get请求转发给后端
this.$axios({
url: this.$settings.BASE_URL + '/order/success/' + location.search,
method: 'get',
}).then(response => {
if (response.data.code != 100) {
alert(response.data.msg)
}
}).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>

10 支付成功回调接口

# 支付成功,支付宝会有俩回调
-get 回调,调前端
-为了保证准确性,支付宝回调会前端后,我们自己向后端发送一个请求,查询一下这个订单是否支付成功
-post 回调,调后端接口
-后端接口,接受支付宝的回调,修改订单状态
-这个接口需要登录吗?不需要任何的认证和权限
-如果用户点了支付----》跳转到了支付宝页面---》你的服务挂机了---》会出现什么情况
-支付宝在24小时内,会有8次回调, # 两个接口:
-post回调,给支付宝用
-get回调,给我们前端做二次校验使用

两个回调接口:

支付成功后端接口:

支付宝后端回调接口(post):

在内网中无法回调成功。(可以使用内网穿透做测试)

request.data.dict():

验证签名:可能会有人模仿支付宝发送请求,修改订单状态。

Response的格式需要符合支付宝要求。如果支付宝回调回不去了(后端崩了),48小时之内支付宝会进行8次回调,任意一次回调成功就可以了(给支付宝返回success)。如果8次回调都没有收到,还有一个对账单的功能。

这两个接口是否需要添加认证?

不能加任何认证和权限,会导致支付宝无法回调。加个频率没关系。

class PaySuccess(APIView):
def get(self, request): # 咱们用的
out_trade_no = request.query_params.get('out_trade_no')
order = Order.objects.filter(out_trade_no=out_trade_no, order_status=1).first()
if order: # 支付宝回调完, 订单状态改了
return APIResponse()
else:
return APIResponse(code=101, msg='暂未收到您的付款,请稍后刷新再试') def post(self, request): # 给支付宝用的,项目需要上线后才能看到 内网中,无法回调成功【使用内网穿透】
try:
result_data = request.data.dict() # requset.data 是post提交的数据,如果是urlencoded格式,requset.data是QueryDict对象,方法dict()---》转成真正的字典
out_trade_no = result_data.get('out_trade_no')
signature = result_data.pop('sign')
# 验证签名的---》验签
result = alipay_v1.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') # 都是支付宝要求的

【django-vue】七牛云上传视频 搜索接口 支付宝sdk二次封装 下单接口 前端支付页面 支付成功回调接口的更多相关文章

  1. 七牛云上传视频并截取第一帧为图片(js实现)

    本文出自APICloud官方论坛, 感谢论坛版主 东冥羽的分享. 七牛云上传视频并截取第一帧作为视频的封面图. 使用js上传,模块videoPlayer截取第一帧(有专门的截图模块,但是我使用的有点问 ...

  2. 七牛云上传视频(后端获取tolen)

    参照网址 https://developer.qiniu.com/kodo/sdk/1242/python #pip install qiniufrom qiniu import Auth #需要填写 ...

  3. Laravel-admin 七牛云上传文件到七牛云出现卡顿失败情况

    由于所做项目需要管理后台众多,所以选择了Laravel-admin后台框架进行开发.节省了权限控制以及页面处理等问题的时间 Laravel-admin文档地址 http://laravel-admin ...

  4. 在Windows Server 2008 R2 Server中,上传视频遇到的问题(二)

    上一篇  在Windows Server 2008 R2 Server中,上传视频遇到的问题(一)中遇到上传40M视频报404,然后修改配置文件节点: <httpRuntime targetFr ...

  5. iOS 七牛云上传并获取图片----【客户端】

           最近做了七牛云存储的有关内容,涉及到与后台交互获取验证的token,无奈,后台自命清高,不与理会,没办法呀,于是自己搞呗.首先呢在在七牛上注册一个账号,然后呢添加一个存储空间这时候空间名 ...

  6. thinkphp 中 使用七牛云上传

    利用七牛云私有空间存储文件 第一步,注册七牛云,创建空间,将空间设为私有 需要记下的东西: AK,SK,bucket 第二步配置ThinkPHP 在config.php添加 'UPLOAD_SITEI ...

  7. laravel中的文件上传到本地+七牛云上传

    首先在filesystems.php 配置好上传的文件的目录起名为upload 在Storage/目录下面 目录下面的app/upload 如果没有这个文件会自动创建 这里的名字upload名字是跟控 ...

  8. thinkphp3.2使用七牛云上传文件

    最近项目中用到了七牛云服务,来分享一下thinkphp使用七牛云来进行文件上传 1.首先在七牛云创建一个空间,例如空间名为test.获取secrectKey,accessKey 2.在thinkphp ...

  9. iUploader 2.0 七牛云上传工具

    iUploader 软件介绍: iUploader主要功能将文件上传至七牛云,返回 Markdown 格式的链接到剪贴板 功能介绍: 图片本地压缩 图片右键上传 图片截取上传 图片复制上传 图片拖拽上 ...

  10. 七牛云-上传、删除文件,工具类(Day49)

    要求: 1. java1.8以上 2. Maven: 这里的version指定了一个版本范围,每次更新pom.xml的时候会尝试去下载7.5.x版本中的最新版本,你可以手动指定一个固定的版本. < ...

随机推荐

  1. WebViewJavascriptBridge.js代码学习

    //notation: js file can only use this kind of comments //since comments will cause error when use in ...

  2. Sealos 云操作系统一键集成 runwasi,解锁 Wasm 的无限潜力

    WebAssembly (通常缩写为 Wasm) 是一种为网络浏览器设计的低级编程语言.它旨在提供一种比传统的 JavaScript 更快.更高效的方式来执行代码,以弥补 JavaScript 在性能 ...

  3. 【主流技术】详解 Spring Boot 2.7.x 集成 ElasticSearch7.x 全过程(二)

    目录 前言 一.添加依赖 二. yml 配置 三.注入依赖 四.CRUD 常用 API ES 实体类 documents 操作 常见条件查询(重点) 分页查询 排序 构造查询 测试调用 五.文章小结 ...

  4. python之字符串format()方法

    format()方法就是格式化指定的值,然后再将其插入字符串的占位符内 占位符用大括号{}来定义哈,可以使用命名索引{sws}.编号索引{0}.甚至空的占位符{}来标识占位符. 语法就是 string ...

  5. top命令和ps命令

    top 命令和 ps 命令 ps 命令 ps 命令查看系统的瞬时信息.通常使用ps -ef | grep 进程名, -e 代表显示所有进程,-f 表示做一个更为完整的输出.经常使用这个命令获得进程的 ...

  6. .NET Conf 2023 Chengdu - 成都会场即将到来!

    12月9日 天府之国 不见不散 今年的.NET Conf 2023,中国区首次有两个会场举办Local Event,北京会场12月16日,成都会场12月9日.这是所有中国.NET开发者的节日,成都会场 ...

  7. Activity发送信息给Fragment

    在MainActivity中设置发送的信息,在fragment中接收, @Override public void onClick(View view) { switch (view.getId()) ...

  8. 基于winform(C#)的飞鸟小游戏

    本项目是一款基于C# (winform)版本的飞鸟小游戏,是一款益智类游戏 其效果如下图所示 如上图所示为飞鸟游戏的初始化界面: 可以看到游戏包含了四个功能: 启动 注册 登陆 排行榜 启动:是用于开 ...

  9. Mybatis-Flex之基础查询

    1.selectOneById /** * selectOneById(id):根据主键查询数据. */ @Test public void testSelectOneById() { /** * S ...

  10. Flask-SQLAlchemy常用新旧查询语法对比

    https://docs.sqlalchemy.org/en/20/tutorial/data.html 新旧版语法的说明 在2.x的SQLALchemy中,查询语法为: db.session.exe ...