Python实战项目-10文件存储/支付宝支付/支付成功回调接口

文件存储

视频文件存储在某个位置,如果放在自己服务器上

  • 放在项目的media文件夹
  • 服务器上线后,用户既要访问接口,又需要看视频,都是使用一个域名和端口
  • 分开:问价你单独放在文件服务器上,文件服务器带宽比较高
# 文件服务器:专门存储文件的服务器 	-第三方:     	-阿里云:对象存储 oss         -腾讯对象存储         -七牛云存储 	-自己搭建:     	fastdfs:文件对象存储  https://zhuanlan.zhihu.com/p/372286804         minio: 

我们可以使用对应的sdk包将文件传输上去

在此项目中我们选用七牛云来存储视频文件资源

使用代码,上传视频

我们参考官方文档使用即可

1.创建七牛云对象存储仓库

Python实战项目-10文件存储/支付宝支付/支付成功回调接口

2.直接在桌面上传文件即可

Python实战项目-10文件存储/支付宝支付/支付成功回调接口

1.1代码控制文件上传

python安装七牛云
pip install qiniu
本地测试
我们scripts文件夹下新建qiniu_test.py文件

# -*- coding: utf-8 -*- # flake8: noqa from qiniu import Auth, put_file, etag import qiniu.config #需要填写你的 Access Key 和 Secret Key # 在这里查看密钥 > https://portal.qiniu.com/user/key access_key = 'Access_Key' secret_key = 'Secret_Key' #构建鉴权对象 q = Auth(access_key, secret_key) #要上传的空间 bucket_name = 'Bucket_Name' #上传后保存的文件名 key = 'my-python-logo.png' #生成上传 Token,可以指定过期时间等 token = q.upload_token(bucket_name, key, 3600) #要上传文件的本地路径 localfile = './sync/bbb.jpg' ret, info = put_file(token, key, localfile, version='v2')  print(info) assert ret['key'] == key assert ret['hash'] == etag(localfile)  

尝试上传本地文件:
Python实战项目-10文件存储/支付宝支付/支付成功回调接口

成功

Python实战项目-10文件存储/支付宝支付/支付成功回调接口

搜索导航栏

前端Header组件上有个搜索框>>>输入内容,即可搜索
在所有商城类的网站,app都会有搜索功能,其实搜索功能非常复杂,且功能非常复杂技术含量高

  • 咱们目前只是简单的搜索,输入课程名字/价格,就可以把实战课搜出来
  • 输入:课程名字,价格把所有类型课程都搜出来(查询多个表)
  • 后面会有专门的搜索引擎:分布式全文检索引擎 es 做专门的搜索

前端页面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> 

搜索页面

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

搜索接口

class CourseSearchView(GenericViewSet, CommonListModelMixin):     queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('orders')     serializer_class = CourseSerializer     pagination_class = CommonPageNumberPagination     filter_backends = [SearchFilter]     search_fields = ['name'] 

Python实战项目-10文件存储/支付宝支付/支付成功回调接口

支付宝支付介绍

前端点击立即购买功能,会生成订单并跳转到付款界面

# 支付宝支付 	-测试环境:大家都可以测试     	-https://openhome.alipay.com/develop/sandbox/app     -正式环境:需要申请,有营业执照 

咱们开发虽然用的沙箱环境,后期上线,公司会自己注册,
注册成功后有个商户id号,作为开发,只要有商户id号,其他步骤都是一样,
所有无论开发还是测试,代码都一样,只是商户号不一样

使用支付宝支付

  • API接口

  • SDK:优先使用,早期支付宝没有python的sdk,后期有了

      -使用了第三方sdk   	-第三方人通过api接口,使用python封装了sdk,开源出来了 

沙箱环境
-安卓的支付宝app,付款用的(买家用)
-扫码使用这个app,付款,这个app的钱都是假的,付款测试商户(卖家)

支付测试,生成支付链接

安装
pip install python-alipay-sdk
生成公钥私钥

Python实战项目-10文件存储/支付宝支付/支付成功回调接口
Python实战项目-10文件存储/支付宝支付/支付成功回调接口
我们可以将生成的公钥配置在支付宝的(沙箱环境)上,生成一个支付宝公钥
以后我们使用这个支付宝公钥即可
Python实战项目-10文件存储/支付宝支付/支付成功回调接口
我们需要将支付宝的公钥,以及项目的应用私钥放入项目中
-pub.pem
-pri.pem
注意:
我们的公钥密钥需要符合要求格式
Python实战项目-10文件存储/支付宝支付/支付成功回调接口

教程参考:https://github.com/fzlee/alipay/tree/master/tests/certs/ali

支付测试代码:
Python实战项目-10文件存储/支付宝支付/支付成功回调接口

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="2021000122628354", # 沙盒支付宝appid     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='asdbasbdjqweo', total_amount='2888') print('https://openapi.alipaydev.com/gateway.do?'+res) 

运行脚本获取链接,打开

Python实战项目-10文件存储/支付宝支付/支付成功回调接口

支付宝支付二次封装

目录结构

libs     ├── iPay  							# aliapy二次封装包     │   ├── __init__.py 				# 包文件     │   ├── pem							# 公钥私钥文件夹     │   │   ├── alipay_public_key.pem	# 支付宝公钥文件     │   │   ├── app_private_key.pem		# 应用私钥文件     │   ├── pay.py						# 支付文件     └── └── settings.py  				# 应用配置   

init.py

from .pay import alipay from .settings import GETWAY 

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)  # 可选,请求超时时间 ) 

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 = '22222222222'  # 加密方式 SIGN = 'RSA2'  # 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False DEBUG = True  # 支付网关 GATEWAY = 'https://openapi.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?' 

订单表设计

-订单表
-订单详情表

下单接口-->没有支付是订单时待支付状态 支付宝post回调接口--> 修改订单状态 --已完成 前端get回调接口 

我们需要新建order app

models.py

# Create your models here. # 订单板块需要写的接口 # 新建order 的app,在models.py中写入表 from django.db import models  from django.db import models from course.models import Course  ''' ForeignKey 中on_delete      -CASCADE  级联删除     -DO_NOTHING    啥都不做,没有外键约束才能用它     -SET_NULL       字段置为空,字段 null=True     -SET_DEFAULT   设置为默认值,default='xx'     -PROTECT    受保护的,很少用     -models.SET(函数内存地址)   会设置成set内的值  ''' 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__() 

执行迁移命令>>>

下单接口

接口分析: 	用户登录后才能使用 	前端点击立即购买 ---> 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 返回前端支付链接 

路由

from rest_framework.routers import SimpleRouter from . import views  router = SimpleRouter() router.register('pay', views.PayView, 'pay') urlpatterns = [     # path('',include(router.urls)) ] urlpatterns += router.urls 

视图层

from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import CreateModelMixin from .models import Order from .serializer import PaySerializer from utils.response import APIResponse from rest_framework_jwt.authentication import JSONWebTokenAuthentication from rest_framework.permissions import IsAuthenticated # Create your views here. class PayView(GenericViewSet,CreateModelMixin):     queryset = Order.objects.all()     serializer_class = PaySerializer     authentication_classes = [JSONWebTokenAuthentication] # 使用JWT权限类配置必须配权限类     permission_classes = [IsAuthenticated]      def create(self, request, *args, **kwargs):         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) 

序列化类

# 校验字段,反序列化      不会序列化的 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  

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

我们还是在全局钩子里写逻辑。
分析我们要使用序列化类做的事情:校验字段、反序列化。(不做序列化)
courses不是订单表的字段,需要在序列化类重写。courses是个列表,需要使用ListField。但是还有别的方法:
Python实战项目-10文件存储/支付宝支付/支付成功回调接口
因为是反序列化多条数据,所以要加many=True

注意我们必须要登录之后才能获取订单链接,

我们使用权限类+认证类来限制登录用户下单

前端支付页面

Python实战项目-10文件存储/支付宝支付/支付成功回调接口
需要携带token向后端发送请求。
数据库查看订单状态
Python实战项目-10文件存储/支付宝支付/支付成功回调接口

支付成功后会回调到前端地址

Python实战项目-10文件存储/支付宝支付/支付成功回调接口
所以要在前端再写一个支付成功页面:

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('您没有登录,请先登录')       }     } 

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> 

支付成功回调接口

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

Python实战项目-10文件存储/支付宝支付/支付成功回调接口

由于我们现在处于内网,所以接收不到回调信息

Python实战项目-10文件存储/支付宝支付/支付成功回调接口

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')  # 都是支付宝要求的  

Response的格式需要符合支付宝要求。如果支付宝回调回不去了(后端崩了),48小时之内支付宝会进行8次回调,任意一次回调成功就可以了(给支付宝返回success)。如果8次回调都没有收到,还有一个对账单的功能。
这两个接口是否需要添加认证?
不能加任何认证和权限,会导致支付宝无法回调。加个频率没关系。

发表评论

相关文章