文件存储

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

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

我们可以使用对应的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 = 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']

支付宝支付介绍

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

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

注意:

我们的公钥密钥需要符合要求格式

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

支付测试代码:

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)

运行脚本获取链接,打开

支付宝支付二次封装

目录结构

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。但是还有别的方法:



因为是反序列化多条数据,所以要加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=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回调,给我们前端做二次校验使用

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

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

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

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

Python实战项目-10文件存储/支付宝支付/支付成功回调接口的更多相关文章

  1. 再一波Python实战项目列表

    前言: 近几年Python可谓是大热啊,很多人都纷纷投入Python的学习中,以前我们实验楼总结过多篇Python实战项目列表,不但有用还有趣,最主要的是咱们实验楼不但有详细的开发教程,更有在线开发环 ...

  2. Python实战项目网络爬虫 之 爬取小说吧小说正文

    本次实战项目适合,有一定Python语法知识的小白学员.本人也是根据一些网上的资料,自己摸索编写的内容.有不明白的童鞋,欢迎提问. 目的:爬取百度小说吧中的原创小说<猎奇师>部分小说内容 ...

  3. Python常用的数据文件存储的4种格式(txt/json/csv/excel)及操作Excel相关的第三方库(xlrd/xlwt/pandas/openpyxl)(2021最新版)

    序言:保存数据的方式各种各样,最简单的方式是直接保存为文本文件,如TXT.JSON.CSV等,除此之外Excel也是现在比较流行的存储格式,通过这篇文章你也将掌握通过一些第三方库(xlrd/xlwt/ ...

  4. python实战项目 — 使用bs4 爬取猫眼电影热榜(存入本地txt、以及存储数据库列表)

    案例一: 重点: 1. 使用bs4 爬取 2. 数据写入本地 txt from bs4 import BeautifulSoup import requests url = "http:// ...

  5. python实战项目

    没有一个完整的项目开发过程,是不会对整个开发流程以及理论知识有牢固的认知的,对于怎样将所学的理论知识应用到实际开发中更是不得而知了! 以上就是我们在学习过程中必须要有项目实战开发经验的原因,其实无论项 ...

  6. python实战项目 — 爬取中国票房网年度电影信息并保存在csv

    import pandas as pd import requests from bs4 import BeautifulSoup import time def spider(url, header ...

  7. python实战项目练习-Django商城项目之注册功能实现

    设计到的前端知识 项目的前端页面使用vue来实现局部刷新,通过数据的双向绑定实现与用户的交互,下面来看一下需求,在用户输入内容后,前端需要做一些简单的规则校验,我们希望在在用户输入后能够实时检测,如果 ...

  8. ansible 实战项目之文件操作(二)

    一,前言 如果没有安装好的话看我以前的贴子哦!! 上次安装已经确定通了,所以下面步骤应该是完全ok的 特点: (1).轻量级,无需在客户端安装agent,更新时,只需在操作机上进行一次更新即可: (2 ...

  9. python实战项目 — 爬取 妹子图网,保存图片到本地

    重点: 1. 用def函数 2. 使用 os.path.dirname("路径保存") , 实现每组图片保存在独立的文件夹中 方法1: import requests from l ...

  10. 7 个有趣的 Python 实战项目,超级适合练手

    关于Python,有一句名言:不要重复造轮子. 但是问题有三个: 1.你不知道已经有哪些轮子已经造好了,哪个适合你用.有名有姓的的著名轮子就400多个,更别说没名没姓自己在制造中的轮子. 2.确实没重 ...

随机推荐

  1. 045_List view button

    http://www.interactiveties.com/b_execute_javascript_button.php https://developer.salesforce.com/foru ...

  2. How to use lspci, lsscsi, lsusb, and lsblk to get Linux system devices information

    There are many utilities available to check Linux system hardware information. Some commands report ...

  3. PostScript语言教程(七、条件语句)

    POSTSCRIPT语言中有许多操作符用于制定程序内流的控制.我们在前一章使用了一个repeat运算.所有的控制操作符都使用了之前简要提到的对象类型,即函数,用于我们调用. 7.1.函数 函数(既过程 ...

  4. 请求接口类型blob,下载文件

    axiosGet () { var This = this var url = '/group1/M00/00/21/MejEvGOX_zOAL2kiAAAAUhB5Iqg138.txt?token= ...

  5. Python的入门学习之 Day 7——from“夜曲编程”

    Day 7 time: 2021.8.4. 今天主要将"if-else"再扩展, 得到"if-elif-else"模型.它与"if-else" ...

  6. Mysql学习:3、sql数据类型及命令

    1.sql功能分类: 2.常见数据类型: 3.sql命令: DDL命令: a.创建数据库: create database testdatabase(数据库名称) character set utf8 ...

  7. 解决href 不下载直接跳转到新的页面

    1.下载 不支持所有浏览器 2 var eleTextarea = document.querySelector('textarea'); var eleButton = document.query ...

  8. MeanShift 均值漂移算法

    MeanShift, 它常被用在图像识别中的目标跟踪,数据聚类.分类等场景

  9. ES7-ES12总结篇

    脑图模式       插入 ES7-ES12  ES7  Array.prototype.includes()   includes() 方法用来判断一个数组是否包含一个指定的值,如果包含则 ...

  10. C# Async / Await State Machine

    The async/await keywords in C# are very much syntactical sugar that the compiler will use to generat ...