每天进步一点点,关注我哦,每天分享测试技术文章

文件存储

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

  • 放在项目的media文件夹
  • 服务器上线后,用户既要访问接口,又需要看视频,都是使用一个域名和端口
  • 分开:问价你单独放在文件服务器上,文件服务器带宽比较高

# 文件服务器:专门存储文件的服务器 -第三方: -阿里云:对象存储 oss -腾讯对象存储 -七牛云存储 -自己搭建: fastdfs:文件对象存储 https://zhuanlan.zhihu.com/p/372286804 minio:

免费领取码同学软件测试课程笔记+超多学习资料+完整视频+面试题,可加交流群:【1134725192 】

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

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

使用代码,上传视频

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

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

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

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)

尝试上传本地文件:

成功

搜索导航栏

前端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 = truethis.is_register = false}},created() {// 组件加载万成,就取出当前的路径,存到sessionStorage  this.$route.pathsessionStorage.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 }}<spanv-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"><spanclass="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 = CourseSerializerpagination_class = CommonPageNumberPaginationfilter_backends = [SearchFilter]search_fields = ['name']

支付宝支付介绍

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

# 支付宝支付-测试环境:大家都可以测试-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
生成公钥私钥

我们可以将生成的公钥配置在支付宝的(沙箱环境)上,生成一个支付宝公钥
以后我们使用这个支付宝公钥即可

我们需要将支付宝的公钥,以及项目的应用私钥放入项目中
-pub.pem
-pri.pem
注意:
我们的公钥密钥需要符合要求格式

教程参考:alipay/tests/certs/ali at master · fzlee/alipay · GitHub

支付测试代码:

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", # 沙盒支付宝appidapp_notify_url=None,  # 默认回调 urlapp_private_key_string=app_private_key_string,# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,alipay_public_key_string=alipay_public_key_string,sign_type="RSA2",  # RSA 或者 RSA2debug=False,  # 默认 Falseverbose=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)

运行脚本获取链接,打开

 支付宝支付二次封装

目录结构

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,  # 默认回调 urlapp_private_key_string=settings.APP_PRIVATE_KEY_STRING,# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,sign_type=settings.SIGN,  # RSA 或者 RSA2debug=settings.DEBUG,  # 默认 Falseverbose=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 modelsfrom 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_NOTHINGuser = 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.user4 生成订单号 支付链接需要,存订单表需要5 生成支付链接:支付宝支付生成,6 生成订单记录,订单是待支付状态(order,order_detail)7 返回前端支付链接

路由

from rest_framework.routers import SimpleRouter
from . import viewsrouter = 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 = PaySerializerauthentication_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 = Orderfields = ['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 = 0for course in courses:new_total_amount += course.priceif total_amount == new_total_amount:return new_total_amountraise APIException('价格有误!!')def _get_out_trade_no(self):# uuid生成return str(uuid.uuid4())def _get_user(self):user = self.context.get('request').userreturn userdef _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 + resself.context['pay_url'] = GATEWAY + resdef _before_create(self, attrs, user, out_trade_no):# 剔除courses----》要不要剔除,要pop,但是不在这,在create方法中pop# 订单号,加入到attrs中attrs['out_trade_no'] = out_trade_no# 把user加入到attrs中attrs['user'] = userdef validate(self, attrs):# 1)订单总价校验total_amount = self._check_total_amount(attrs)# 2)生成订单号out_trade_no = self._get_out_trade_no()# 3)支付用户:request.useruser = 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。但是还有别的方法:

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

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

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

前端支付页面

需要携带token向后端发送请求。
数据库查看订单状态

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

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

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=2let items = params.length ? params.split('&') : [];  // ['a=1', 'b=2']//逐个将每一项添加到args对象中for (let i = 0; i < items.length; i++) {  // 第一次循环a=1,第二次b=2let 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回调,给我们前端做二次校验使用

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

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:passreturn Response('failed')  # 都是支付宝要求的

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

最后:

这些资料,对于想转行做【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!凡事要趁早,特别是技术行业,一定要提升技术功底。希望对大家有所帮助……

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

  1. python有趣的小项目-有趣的十个Python实战项目,让你瞬间爱上Python!

    前言 Python 是一种极具可读性和通用性的编程语言.Python 这个名字的灵感来自于英国喜剧团体 Monty Python,它的开发团队有一个重要的基础目标,就是使语言使用起来很有趣.Pytho ...

  2. python项目开发实例-有趣的十个Python实战项目,让你瞬间爱上Python!

    前言 Python 是一种极具可读性和通用性的编程语言.Python 这个名字的灵感来自于英国喜剧团体 Monty Python,它的开发团队有一个重要的基础目标,就是使语言使用起来很有趣.Pytho ...

  3. python能做什么项目-这十个Python实战项目,让你瞬间读懂Python!

    前言 Python 是一种极具可读性和通用性的编程语言.Python 这个名字的灵感来自于英国喜剧团体 Monty Python,它的开发团队有一个重要的基础目标,就是使语言使用起来很有趣.Pytho ...

  4. 有趣的十个Python实战项目,让你瞬间爱上Python!

    前言 Python 是一种极具可读性和通用性的编程语言.Python 这个名字的灵感来自于英国喜剧团体 Monty Python,它的开发团队有一个重要的基础目标,就是使语言使用起来很有趣.Pytho ...

  5. python十个实例-有趣的十个Python实战项目,让你瞬间爱上Python!

    前言 Python 是一种极具可读性和通用性的编程语言.Python 这个名字的灵感来自于英国喜剧团体 Monty Python,它的开发团队有一个重要的基础目标,就是使语言使用起来很有趣.Pytho ...

  6. Python实战项目总结

    Python实战项目总结 项目:处理超市营业额. 题目:一:已知文件"超市营业额2.xlsx",将所有员工的工号前面增加一位数字,增加的数字和原工号最后一位相同,把修改好的数据写入 ...

  7. Python实战项目:高血压检测项目调查问卷接口的测试

    Python实战项目:高血压检测项目调查问卷接口的测试 在前面的Python实战项目中介绍过高血压检测项目和自动化综合测试的相关内容,那么如何结合自动化综合测试的内容进行高血压检测项目的测试呢,尤其是 ...

  8. python项目归纳总结-这4个Python实战项目,让你瞬间读懂Python!

    原标题:这4个Python实战项目,让你瞬间读懂Python! 来自:https://www.toutiao.com/a6593456515221291528/ Python 是一种极具可读性和通用性 ...

  9. python项目实战干货_干货 | 这4个Python实战项目,让你瞬间读懂Python!

    Python是一种极具可读性和通用性的编程语言.Python这个名字的灵感来自于英国喜剧团体Monty Python,它的开发团队有一个重要的基础目标,就是使语言使用起来很有趣.Python易于设置, ...

最新文章

  1. 数据结构线段树介绍与笔试算法题-LeetCode 307. Range Sum Query - Mutable--Java解法
  2. MICROSOFT SQL SERVER 2005 SEPTEMPTER CTP下载
  3. 数据分析系列:绘制散点图(matplotlib)
  4. PHP基础知识(一)
  5. 【面向工业界】京东NLP落地应用实战
  6. TeeChart中 Line的Clear方法
  7. SANTENDO的大脑训练计划
  8. 路径.git下的文件
  9. Dell做RAID配置图文全教程
  10. python爬虫案例——csdn数据采集
  11. 深度学习caffe:最优化方法
  12. 松下FPX通用通信编程实例
  13. kodi文件管理smb服务器,KODI win10,kodi不能发现smb共享
  14. Spring源码解析一 (IOC容器初始化深度解析)
  15. ctfshow 做题 MISC入门 模块 21~30
  16. Vue 编写(preventReClick)防暴点 +防抖(debounce)和节流(throttle)函数
  17. SpringMVC Controller接收普通类型参数 postman配置
  18. 使用 emeditor 删除空行
  19. Liunx中mysql服务
  20. 一、浙江专升本高等数学考点-函数

热门文章

  1. 可信AI年度观察 | 智能语音产业需求不断升级,评测重点由技术转向产品
  2. 巴啦啦能量 哈尔滨理工大学软件与微电子学院第八届程序设计竞赛同步赛(低年级)...
  3. VS2019使用QT框架搭建记录
  4. 网页中单一表格横纵表头锁定
  5. Visio对mysql怎么画er图_Microsoft Office Visio如何绘制ER图?Microsoft Office Visio绘制ER图的方法步骤...
  6. 【华为OD机试真题 JAVA】计算堆栈中的剩余数字
  7. word中通过宏对某章节下图片批量插入题注
  8. 【六行代码】Python逐行读取txt、换行输出到txt
  9. 如何判断Android应用运行在鸿蒙系统上
  10. 精臣b3s蓝牙打印服务