1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > Python实战项目-10文件存储/支付宝支付/支付成功回调接口

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

时间:2023-01-20 19:15:05

相关推荐

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

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

文件存储

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

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

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

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

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

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

使用代码,上传视频

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

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

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

1.1代码控制文件上传

python安装七牛云

pip install qiniu

本地测试

我们scripts文件夹下新建qiniu_test.py文件

# -*- coding: utf-8 -*-# flake8: noqafrom qiniu import Auth, put_file, etagimport qiniu.config#需要填写你的 Access Key 和 Secret Key# 在这里查看密钥 > /user/keyaccess_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'] == keyassert 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']

支付宝支付介绍

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

# 支付宝支付-测试环境:大家都可以测试-/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 AliPayfrom alipay.utils import AliPayConfigapp_private_key_string = open("pri.pem").read()alipay_public_key_string = open("pub.pem").read()alipay = AliPay(appid="000122628354", # 沙盒支付宝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('/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 alipayfrom .settings import GETWAY

pay.py

from alipay import AliPayfrom alipay.utils import AliPayConfigfrom . import settingsalipay = 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()# 应用IDAPP_ID = '22222222222'# 加密方式SIGN = 'RSA2'# 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置FalseDEBUG = True# 支付网关GATEWAY = '/gateway.do?' if DEBUG else '/gateway.do?'

订单表设计

-订单表

-订单详情表

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

我们需要新建order app

models.py

# Create your models here.# 订单板块需要写的接口# 新建order 的app,在models.py中写入表from django.db import modelsfrom django.db import modelsfrom 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 SimpleRouterfrom . import viewsrouter = SimpleRouter()router.register('pay', views.PayView, 'pay')urlpatterns = [# path('',include(router.urls))]urlpatterns += router.urls

视图层

from rest_framework.viewsets import GenericViewSetfrom rest_framework.mixins import CreateModelMixinfrom .models import Orderfrom .serializer import PaySerializerfrom utils.response import APIResponsefrom rest_framework_jwt.authentication import JSONWebTokenAuthenticationfrom 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

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

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

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次回调都没有收到,还有一个对账单的功能。

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

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

最后:

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

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。