路飞:文件存储、前端搜索导航栏、前端搜索页面、后端搜索接口、支付宝支付介绍、支付宝支付功能封装、订单表设计、后端下单接口、前端支付页面、支付成功回调接口
一、文件存储
当我们的视频文件存储在我们自己项目的服务器上时,存在以下情况:
- 服务器上限后,用户访问接口和获取视频都是在一个接口处理的,当视频很大的时候,效率很低
- 视频文件存储在media文件夹中,十分占用服务器空间
因此我们需要把文件存储在文件服务器:专门存储文件的服务器,带宽很高,传输效率高。
文件服务器通常分成两种
如何用python传文件到文件服务器?
- 我们需要在对应的官网的文档中查找对应的SDK,通过python的SDK来传输文件
七牛云上传文件
这里我们使用七牛云,因为他可以免费使用一个月
1.1七牛云存储空间使用
步骤一
打开七牛云网站,登陆后点击右上角的控制台
步骤二
如果没有认证,需要根据页面提示先进行认证
接着点击左侧的存储空间
步骤三
在新页面点击左上方的新建空间按钮
步骤四
在右侧出现的模态框中配置空间的信息
ps1:存储区域就代表服务器的位置
ps2:访问控制我们选择公开,这样我们的项目才能更容易的访问
步骤五
点击确定后我们会看到一个提示弹窗,告诉我们已经给我们配备了测试域名,试用期一个月,点击知道了按钮,关闭即可
步骤六
点击我们刚创建的存储空间,会进入下图界面,然后点击文件管理,然后点击上传文件按钮
步骤七
在新出现的模态框中选中本地文件添加,然后等待上传结束后刷新网页查看结果
步骤八
点击右侧的更多按钮,然后点击复制外链,就可以获取视频的路由
我的外链路由
http://rrgcbfc2w.hd-bkt.clouddn.com/Sdsdsdshuddddttle_1.mp4
1.2使用代码上传文件到七牛云
逻辑分析
我们我们使用文件服务器存储文件,基本上可以分成两种存储方式
- 在前端项目中,上传文件到文件服务器,返回给后端 文件的url,把url存储在数据库中
- 在前端项目中把文件传到后端,然后再上传服务器,把获取到的url存储在数据库中
官方文档:https://developer.qiniu.com/kodo/1242/python
操作流程
步骤一
安装七牛云的模块
pip install qiniu
步骤二
从文档中拷贝上传文件的代码
然后根据自己的情况配置参数
access_key, secret_key查看地址:
https://portal.qiniu.com/user/key
from qiniu import Auth, put_file, etag import qiniu.config #需要填写你的 Access Key 和 Secret Key access_key = 'Mh-VJHc0xihdf6MmbuwRxdkZP0QU6AygPdApQMcu' secret_key = 'k59-YsDTIF6wcXQHSoSMCfQpEns4gnrTywGwI79a' #构建鉴权对象 q = Auth(access_key, secret_key) #要上传的空间 bucket_name = 'zzh-luffy' #上传后保存的文件名 key = '雨景.mp4' #生成上传 Token,可以指定过期时间等 token = q.upload_token(bucket_name, key, 3600) #要上传文件的本地路径 这里为了方便,直接放在当前目录下了 localfile = './雨景.mp4' ret, info = put_file(token, key, localfile, version='v2') print(ret) print(info) # 这两个断言没什么用 # assert ret['key'] == key # assert ret['hash'] == etag(localfile)
查看结果
这里我们就可以跟在网页上一样操作了,复制外链然后使用即可
二、前端搜索导航栏
前端的 Header 组件上有个搜索框,输入内容后,点击搜索按钮,向后端接口发送请求获取过滤后的数据,在结果页面展示出来
ps:
- 所有的商城类的网站,app,都会有搜索功能,其实搜索功能非常复杂且技术含量高
- 咱们目前只是简单的搜索,输入课程名字,价格 就可以把实战课搜出来
- 输入:课程名字,价格把所有类型课程都搜出来(查询多个表)
- 后面会有专门的搜索引擎:分布式全文检索引擎 es 做专门的搜索
2.1 Header.vue
这里我们把搜索框放在Header组件中,添加了搜索框的样式和事件、变量、样式
<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> <!--这是图标边上的三个课程分类,实现的效果也是点击后跳转到对应的页面,但是这里动态绑定了class属性,当某个标签被点击,就会出现一个下划线的样式,他是通过当前的路由是否跟这个标签中用来对比的路由一致来判断的--> <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"> <!--这里根据注册后的结果获取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: { 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 }, 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; }, }, 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> .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; } .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; } </style>
ps:这里我们用this.
三、前端搜索页面
写完搜索框的代码,我们还需要创建一个搜索结果页面
在搜索框的事件中,我们可以看到接收到后端的数据后,跳转的路由是/course/search,因此我们创建一个SearchCourse.vue文件
然后再创建 这个搜索结果页面的时候,我们需要展示被搜索的内容是什么,这里我们在created方法中获取
从$route这个当前的路由对象中获取搜索时携带在路由中的搜索内容绑定给对应的变量
在获取到搜索的内容后,我们向后端发送请求,获取对应的数据,展示在页面中
3.1 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.$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>
3.2 路由中注册
import SearchCourse from "@/views/SearchCourse"; const routes = [ ... { path: '/course/search', name: 'search', component: SearchCourse }, ]
四、后端搜索接口
根据前端代码,我们可以分析出接口的需求
1、get请求 2、请求的路由是/course/search/ 3、请求中携带的搜索条件通过问号拼接在路由后面
4.1路由
# 搜索接口 # 访问 http://127.0.0.1:8000/api/v1/course/search/ router.register('search', views.SearchCourseView, 'search')
4.2视图类
from rest_framework.filters import SearchFilter # 查询接口,相当于是一个获取所有数据的接口,因此使用我们自定义的List类,然后通过过滤实现筛选功能 class SearchCourseView(GenericViewSet, CommonListModelMixin): queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders') serializer_class = CourseSerializer # 过滤 # 因为这里搜索功能应该使用模糊匹配获取数据,所以我们使用SearchFilter类来过滤 filter_backends = [SearchFilter, ] search_fields = ['name', 'price'] # 分页 pagination_class = CommonPageNumberPagination '分页类之前导入过了'
拓展
这里我们只编写了实战课程的数据,但是在实际搜索的时候,需要获取三种课程的所有数据进行过滤,因此这个时候我们就必须重写list方法,手动获取另外两种课程的过滤后的数据
伪代码
# 补充:正常如果有多个课程,这个接口都要搜索返回,需要重写list # def list(self, request, *args, **kwargs): # res=super(SearchCourseView, self).list(request, *args, **kwargs) # actual_course=res.data # # 免费课,自己写 # free_course=[{},{}] # # 轻课,自己写 # light_course=[{},{},{}] # return APIResponse(actual_course=actual_course,free_course=free_course,light_course=light_course) # # {code:100,msg:成功,actual_course:[],free_course:[],light_course:[]}
五、支付宝支付介绍
5.1简介
当我们点击立即购买按钮,触发购买功能的请求,使用支付宝支付
ps:腾讯的微信,支付需要有营业执照,支付宝有测试环境供大家使用
官网文档:https://opendocs.alipay.com/open/270/105898/
测试环境网址(沙箱环境):https://openhome.alipay.com/develop/sandbox/app
正式环境:需要申请,并要有营业执照,咋们目前用不了
ps:咱们开发虽然用的沙箱环境,后期上线,公司会自己注册,注册成功后有个商户id号,作为开发,只要有商户id好,其他步骤都是一样,所有无论开发还是测试,代码都一样,只是商户号不一样
5.2支付流程讲解
当我们前端发送了一个支付请求后,会给后端发送请求
后端接收到前端的请求后,通过支付宝的sdk,朝支付宝的服务器发送请求,获取到一个支付链接,返回给前端
用户在打开支付链接后,就可以进行扫码付款(在支付宝的网站付款)
后面的过程就可以自定义,通常来说,后端会向支付宝获取请求的回调结果,获取到了结果就是返回支付成功页面,否则就是展示正在查询中的页面,查询成功就把这条记录写进数据库中
ps:如果用户关闭页面十分快,在我们查到回调结果之前就关闭了页面,可能会出现服务器不能获取同步回调结果的情况,导致往数据库写入结果的时候失败
这时候如果支付宝服务器发现我们接受了结果后没有给他返回是否成功的信息,就会在48小时之内,总共发送8次post请求通知我们的服务器
支付宝8次异步通知机制文档:https://opendocs.alipay.com/open/270/105902/
5.3使用支付宝支付功能
跟之前的功能一样,可以使用一下三种方式
-API接口 -SDK:优先使用,早期,支付宝没有python的sdk,后期有了 -使用了第三方sdk -第三方人通过api接口,使用python封装了sdk,开源出来了
沙箱环境
-有安卓的支付宝app,付款用的---》买家用的 -扫码使用这个app,付款,这个app的钱都是假的,付款进测试商户(卖家)
接着我们登陆测试环境的网址,登陆后就可以看到我们自己的沙箱APPID以及上架账号PID
这里是沙箱版的支付宝app,测试付款的时候会用到
这里是沙箱环境的账号信息
5.1 支付测试,生成支付链接
在之前,支付宝并没有给出官方的SDK,因此那时候使用的都是第三方的SDK,这里我们使用第三方的SDK,毕竟官网的文档十分详细
第三方SDK网址:https://github.com/fzlee/alipay
操作流程
步骤一
安装第三方sdk的模块
python
pip install python-alipay-sdk
步骤二
下载支付宝官方的公钥私钥生成工具获取公钥私钥
文档地址:https://docs.open.alipay.com/291/105971
工具下载地址:https://opendocs.alipay.com/common/02kipk
步骤三
下载后一路下一步安装即可,打开软件后,我们点击左侧的第一个功能生成密钥,选好配置后点击生成
步骤四
这时候我们就能看到自己的公钥私钥,然后配置到支付宝的沙箱网站上去
步骤五
当我们在网站上上传了自己的公钥后,他会给我们一个新的公钥,双重保障数据安全
步骤六
把获取到的支付宝公钥和自己的私钥放到python中的文件内
pub.pem
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjcbQa1MO8KwRF/ut+Ox6y+r2xOx6dk7UQhNM5E/zi+bqEjZacJ93E3rDTeDOBIMwpfiJE328iVdz6szD0iRfODOXP976C2v/Uqapg5dr3FDaROapzeGqiZkze873frRZMMZhIFzIK8LpAUXo55OKPm1BJw+SSLmducL9AVJ33w8LwKLSqZhbudJmI161X/yJR/Yd5mvQW6AncABuycFwUu31mry8U2/cyglSB0SGASJlrHVqgGyghF/VKg0OXawpIYqhZJER2B3EDha0NIH4YdEF17uk4lcD0DYKYfxCrXT+GJzGRP1+mH928jnHMnOynNnpRpC3/wtPY48PYOX8gQIDAQAB -----END PUBLIC KEY-----
pri.pem
-----BEGIN RSA PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCz89po66Kf4PII1Diyk9W4YecHwGu3sspgzJ8WFfhkxqgkS+/I7bOM/HiPc1Xg9FlfqSQKg++s/+WssNdESJnnKYlCFQ2BQ/DEj0zTj2xGTK8Fepwb8X0yLaOm+TyjcbFGIumrEqEgnWgzFbHDlNl1djZLwmo8Q2NKDwRIqXAnr4lrXjpcxttHobQnVxRZjN3jzSEKuLr7rcGuMVyphgzftrD6V+I90TRL6OxLHPyUE2xkZI5t7mLod/pmqBQuCNd4AwVric6LohPTG5B4efCnOTcMhjmJQWeSP31UzRextTR1SAkkQl9jKLjYKXQzbvt1KRYCAY86r0YelrJRzvg9AgMBAAECggEAPT4gdtS64+6KcLbJFP17HuUy21CBWGNgvNDm031mq+VwE+pEEP3UvYavASMfdw+x2vyVZ36/cwEkCUGmAXcz6ZgZPoQrtHKK3lpmqxmEn+g5KIQx3NCy6wDdUVZBbN6klloLbE1KyO7JJ5mahhWCbJduoxnnTdshT3MM4i3IHbjtGnLyf+gTXlxEDiDtVLSZzMBIXdRA5LwF0uJ9r/ENvBQppfAxYyLBWXkmS8+BiBT8OhkBbzIcCpFTeUw+XmsafQKyvCd4KWZpjqclw/ZA9l+18z0zSTM6CIhziKVcxTDc7jDYQIuam+HGEvjIvZBVYnMa2q94vKGyE9K7iR3wAQKBgQDtFpOzsQwn8LJfbR2D+WpwppchWn5GnI0uvYk2nJVkgePs2IJiHgVLeVqRzfqzOqCWP0G/cizmCwpUm6rzHuud/cs2EogQB62hJeoO6FaPzndZfz7iRlY3b0hTKUvz7GEW/lXDC3a4NhruyyX1d51eV3B0foeCRdLTxvB8U7ecnQKBgQDCTovwO8TDbSj3ryYmWSJCnj40O3gsdqtJrbcEam3uACSbEEZbinI9V5PdjcdpxHA3TalJkEiFWckf+l9KcLRC7wxKCQEky5esd2Cm5iEeOPqnV81Tg9qfeT/u1xwSwSHFsY3uHYaojylQPYJCPesbP2oP6LrWdXnYM5mL/FRoIQKBgBfYAGy+7okJxPah46kFKXZA2swo7LAvSGed+jG6169u/LwyHhK+ECxB/SDSxVbHG6VgoT0ev3M3Qwe1TCD9SBbwkkJS5Mov639cb8imByaZThl7GZjqF/ulPnHWomParv0AASIHgh4OmvDPD3c7W2FEi/O7UeeBHC3eQQtP5BRlAoGBAKLXSRgQPxq5BThupT5GPRwvmloT7Ob2nR6mY1dkCrUHkaYSwaQ+JveZyeZHx/OwGYt/nnx0WxLG6HPQVMQCKFBgHqB9Y2P+7CIx+eJlwyOdBRPx1bZELpiv+RClXnWpPjB8WUJRpGTndew1YFE7qymuv6iNlufclDzJIcnbsrGBAoGBAJ97vfmPmO3Q2GoEqv3mim2mHiSYOn+lK1AHUYmVFOwTc+/QD6KuvvFvM/yY3AA7P8l3Z0457LjzOR0wBrwphUN7milPmMpnfH/lWNLzzfqo/7cqgcF6UkfowZ2gL0xwS+0F/28otRNixsJtXpp8/y5Zr8NTXcmG+kadpi3PX95i -----END RSA PRIVATE KEY-----
步骤七
从文档冲拷贝代码,修改配置后运行
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="2021000122628443", 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)
ps:out_trade_no相当于订单号,不能重复。total_amount是总价格
六、支付宝支付功能封装
这里跟短信接口一样,需要我们进行封装
6.1 目录结构
libs ├── iPay # aliapy二次封装包 │ ├── __init__.py # 包文件 │ ├── pem # 公钥私钥文件夹 │ │ ├── alipay_public_key.pem # 支付宝公钥文件 │ │ ├── app_private_key.pem # 应用私钥文件 │ ├── pay.py # 支付文件 └── └── settings.py # 应用配置
6.2 封装步骤
步骤一
在libs包中创建alipay_v1包,在内部封装支付宝支付的代码,创建相应的文件
步骤二
在包的内部创建一个pem文件夹放入公钥私钥文件,并对他们重命名
步骤三
编写对应的功能代码
*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 = '2021000122628443' # 加密方式 SIGN = 'RSA2' # 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False DEBUG = True # 支付网关 GATEWAY = 'https://openapi.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'
步骤四
创建order app,发送订单的请求,并在配置文件中注册
命令 python ../../manage.py startapp order 配置文件 INSTALLED_APPS = [ 'simpleui', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'corsheaders', 'home', 'user', 'course', 'order', ]
步骤五
创建路由文件,并在总路由中配置路由分发
总路由
urlpatterns = [ ... # 路由分发 path('api/v1/home/', include('home.urls')), path('api/v1/user/', include('user.urls')), path('api/v1/course/', include('course.urls')), path('api/v1/order/', include('order.urls')), ]
子路由
from rest_framework.routers import SimpleRouter from . import views router = SimpleRouter() urlpatterns = [ ] urlpatterns += router.urls
步骤六
编写对应的视图类
from django.shortcuts import render # Create your views here. from libs.alipay_v1 import alipay, GATEWAY from utils.common_response import APIResponse from rest_framework.viewsets import ViewSet, GenericViewSet from rest_framework.decorators import action class PayView(ViewSet): @action(methods=['POST'], detail=False) def pay(self, request): res = alipay.api_alipay_trade_page_pay(subject='男士内衣', out_trade_no='asdfasd33', total_amount='888') pay_url = GATEWAY + res return APIResponse(pay_url=pay_url)
测试结果
七、订单表设计
订单板块需要写的接口分析
- 下单接口---》没有支付是订单是待支付状态
- 支付宝post回调接口---》修改订单状态成已支付
- 前端get回调接口(暂时先不关注)
订单板块表设计
- 订单表
- 订单详情表
在order 的app的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(可能重复,概率很多) 【分布式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__() ''' ForeignKey 中on_delete -CASCADE 级联删除 -models.DO_NOTHING 啥都不做,没有外键约束才能用它 -SET_NULL 字段置为空,字段 null=True -SET_DEFAULT 设置为默认值,default='xx' -PROTECT 受保护的,很少用 -models.SET(函数内存地址) 会设置成set内的值 '''
ps:添加了db_constraint=False的字段,外键还是存在的,但是没有约束了
进行数据库迁移
(luffy) D:\pythonproject\luffy\luffy_api>python manage.py makemigrations (luffy) D:\pythonproject\luffy\luffy_api>python manage.py migrate
八、后端下单接口
接口分析
登录后才能用---》前端点击立即购买----》发送post请求--》携带数据 {courses:[1,],total_amount:99.9,subject:'xx课程'}----》视图类中重写create方法---》主要逻辑写到序列化类中
ps:这里的课程使用列表,是为了方便将来添加多个课程的时候使用
主要逻辑
1 取出所有课程id号,拿到课程 2 统计总价格,跟传入的total_amount做比较,如果一样,继续往后 3 获取购买人信息:登录后才能访问的接口 request.user 4 生成订单号 支付链接需要,存订单表需要 5 生成支付链接:支付宝支付生成, 6 生成订单记录,订单是待支付状态(order,order_detail) 7 返回前端支付链接
8.1 序列化类
from rest_framework import serializers from .models import Order, OrderDetail from course.models import Course from rest_framework.exceptions import APIException import uuid from libs.alipay_v1 import GATEWAY, alipay from django.conf import settings class PaySerializer(serializers.ModelSerializer): # 这里的courses因为我们传入的是一个列表,所以如果我们想用普通方式接受并获取课程的数据 # 需要用ListField # courses=serializers.ListField() # 咱们不用这种 courses=[1,2,3] # 这里的PrimaryKeyRelatedField的作用如下 # 根据course表的主键去查询数据,返回的数据会是对象,因为这里我们获取的是多个数据,所以要加many=True # 然后再参数中我们要在queryset中获取要查询的表的数据 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
8.2 视图类
from django.shortcuts import render # Create your views here. from libs.alipay_v1 import alipay, GATEWAY from utils.common_response import APIResponse from rest_framework.viewsets import ViewSet, GenericViewSet from rest_framework.mixins import CreateModelMixin from rest_framework.decorators import action from .models import Order from .serializer import PaySerializer from rest_framework_jwt.authentication import JSONWebTokenAuthentication from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView class PayView(GenericViewSet, CreateModelMixin): # 这些测试用的代码需要注释掉,因为后面需要对数据库进行操作,要使用GenericViewSet # @action(methods=['POST'], detail=False) # def pay(self, request): # res = alipay.api_alipay_trade_page_pay(subject='男士内衣', out_trade_no='asdfasd33', total_amount='888') # pay_url = GATEWAY + res # return APIResponse(pay_url=pay_url) queryset = Order.objects.all() serializer_class = PaySerializer # 配置认证类 authentication_classes = [JSONWebTokenAuthentication] permission_classes = [IsAuthenticated] # 下单接口 def create(self, request, *args, **kwargs): # 这里我们重写create方法,让他不序列化(因为序列化类中写的字段是不存在与表中的),并自定义返回的数据的格式 # 注意:把request对象,传入到序列化类中,还是用context属性传递 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)
8.3配置jwt的过期时间
这个配置要加载配置文件中
import datetime JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=3000), }
8.4 路由
order的路由
from rest_framework.routers import SimpleRouter from . import views from django.urls import path router = SimpleRouter() # http://127.0.0.1:8000/api/v1/order/pay/pay/ router.register('pay', views.PayView, 'pay') urlpatterns = [ # http://127.0.0.1:8000/api/v1/order/success/ path('success/', views.PaySuccess.as_view()), ] urlpatterns += router.urls
九、前端支付页面
9.1 给对应的支付按钮绑定事件
CourseDetail.vue
在按钮中绑定事件 <button class="buy-now" @click="go_pay">立即购买</button> <button class="try" v-if="section.free_trail">立即试学</button> 在js代码中编写对应事件的代码 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
import PaySuccess from "@/views/PaySuccess"; <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>
十、支付成功回调接口
接口分析
# get 给自己用,当我们的网页跳转到结果展示页面,会用这个接口去数据库查看订单状态 # post 给支付宝用,支付宝把订单装填通过这个 接口返回给我们 # 该接口不要加任何认证和权限,因为支付宝的服务器不会登陆我们的项目
视图层代码
from rest_framework.response import Response from utils.common_logger import logger from libs import alipay_v1 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') # 都是支付宝要求的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步