day13
内容概要
- 文件存储
- 搜索导航栏
- 搜索接口
- 搜索页面
- 支付宝支付介绍
- 支付宝二次封装
- 订单表设计
- 下单接口
- 前端支付页面
- 支付成功回调
文件存储
视频文件,存储到某个位置,如果放在自己服务器上
- 放在项目的media文件夹
- 服务器上线以后,用户既要访问接口,又要看视频,都是访问一个域名和端口
- 分开:文件单独放在文件服务器上,文件服务器带宽很高
文件服务器:专门存储文件分服务器
第三方:
-
阿里云:对象存储 oss
-
腾讯对象存储
-
七牛云
-
自己搭建:
fastdfs:文件对象存储
https://zhuanlan.zhihu.com/p/372286804
minio:有空可以看看看





python如何把文件传到上面
对应的sdk






# -*- coding: utf-8 -*-
# flake8: noqa
from qiniu import Auth, put_file, etag
import qiniu.config
# 需要填写你的 Access Key 和 Secret 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)


# -*- coding: utf-8 -*-
# flake8: noqa
from qiniu import Auth, put_file, etag
import qiniu.config
# 需要填写你的 Access Key 和 Secret Key
access_key = ''
secret_key = ''
# 构建鉴权对象
q = Auth(access_key, secret_key)
# 要上传的空间
bucket_name = 'ieu'
# 上传后保存的文件名
key = '测试1.mp4'
# 生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)
# 要上传文件的本地路径
localfile = './测试1.mp4'
ret, info = put_file(token, key, localfile, version='v2')
print(info)
assert ret['key'] == key
assert ret['hash'] == etag(localfile)
搜索导航栏
前端 Header组件上有个所有框 ---》输入内容,搜索---->搜索后端接口
所有的商城类的网站,app,都会有搜索功能,起始搜索功能非常复杂且技术含量高
咱们目前只是简单的搜索,输入课程名字,价格 就可以把实战课出来
输入:课程名字,价格把所有类型课程都搜出来(查询多个表)
后面会有专门的搜索引擎:分布式全文检索引擎 es 做专门的搜索
前端搜索结果呈现页面
在Header里面加了些东西
<template>
<div class="header">
<div class="slogan">
<p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
</div>
<div class="nav">
<ul class="left-part">
<li class="logo">
<router-link to="/">
<img src="../assets/img/head-logo.svg" alt="">
</router-link>
</li>
<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 @click="logout">注销</span>
</div>
</div>
<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>
<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"/>
</div>
</template>
<script>
import Login from "@/components/Login";
import Register from "@/components/Register";
export default {
name: "Header",
components: {
Login,
Register
},
data() {
return {
// 当前所在路径,去sessionStorage取的,如果取不到,就是 /
url_path: sessionStorage.url_path || '/',
is_login: false,
is_register: false,
username: this.$cookies.get("username"),
is_search_tip: true,
search_placeholder: '',
search_word: ''
}
},
methods: {
search_action(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
},
logout() {
this.$cookies.remove("username")
this.$cookies.remove("token")
this.username = ''
this.token = ''
}
},
created() {
// 组件加载万成,就取出当前的路径,存到sessionStorage this.$route.path
sessionStorage.url_path = this.$route.path;
// 把url_path = 当前路径
this.url_path = this.$route.path;
},
}
</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="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 @click="logout">注销</span>
</div>
</div>
<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>
<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"/>
</div>
</template>
<script>
import Login from "@/components/Login";
import Register from "@/components/Register";
export default {
name: "Header",
components: {
Login,
Register
},
data() {
return {
// 当前所在路径,去sessionStorage取的,如果取不到,就是 /
url_path: sessionStorage.url_path || '/',
is_login: false,
is_register: false,
username: this.$cookies.get("username"),
is_search_tip: true,
search_placeholder: '',
search_word: ''
}
},
methods: {
search_action(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
},
logout() {
this.$cookies.remove("username")
this.$cookies.remove("token")
this.username = ''
this.token = ''
}
},
created() {
// 组件加载万成,就取出当前的路径,存到sessionStorage this.$route.path
sessionStorage.url_path = this.$route.path;
// 把url_path = 当前路径
this.url_path = this.$route.path;
},
}
</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>

后端
from rest_framework.filters import SearchFilter
class SearchView(GenericViewSet, CommonListModelMixin):
queryset = Course.objects.all()
serializer_class = CourseSerializer
filter_backends = [SearchFilter, ]
search_fields = ["name", "price"]
url层
route.register("search", views.SearchView, "search")
支付包介绍
立即购买功能,点击立即购买按钮,使用支付宝支付
支付宝支付
测试环境:
https://openhome.alipay.com/develop/sandbox/app





开始安装



安装完成后


然后复制 公钥 到 支付宝

然后去下载 sdk
有官方的,也有第三方的 包结构
libs
├── iPay # aliapy二次封装包
│ ├── __init__.py # 包文件
│ ├── pem # 公钥私钥文件夹
│ │ ├── alipay_public_key.pem # 支付宝公钥文件
│ │ ├── app_private_key.pem # 应用私钥文件
│ ├── pay.py # 支付文件
└── └── settings.py # 应用配置
我使用第三方的
https://github.com/fzlee/alipay






pay层
from alipay import AliPay, DCAliPay, ISVAliPay
from alipay.utils import AliPayConfig
from scripts.alipay_t import settings
app_private_key_string = open("./pem/app_private_key.pem").read()
alipay_public_key_string = open("./pem/alipay_public_key.pem").read()
alipay = AliPay(
appid=settings.APP_ID,
app_notify_url=None, # 默认回调 url
app_private_key_string=app_private_key_string,
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_string=alipay_public_key_string,
sign_type=settings.SIGN, # RSA 或者 RSA2
debug=settings.DEBUG, # 默认 False
verbose=settings.DEBUG, # 输出调试数据
config=AliPayConfig(timeout=15) # 可选,请求超时时间
)
# res=alipay.api_alipay_trade_page_pay(subject='性感内衣', out_trade_no='asdas23sddfsasf', total_amount='999')
# print('https://openapi.alipaydev.com/gateway.do?'+res)
# dc_alipay = DCAliPay(
# appid="appid",
# app_notify_url="http://example.com/app_notify_url",
# app_private_key_string=app_private_key_string,
# app_public_key_cert_string=app_public_key_cert_string,
# alipay_public_key_cert_string=alipay_public_key_cert_string,
setting
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 = '2021000122627346'
# 加密方式
SIGN = 'RSA2'
# 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
DEBUG = True
# 支付网关
GATEWAY = 'https://openapi.alipaydev.com/gateway.do' if DEBUG else 'https://openapi.alipay.com/gateway.do'
订单表设计
订单板块需要写的接口
- 下单接口---》没有支付是订单时待支付状态
- 支付宝post回调接口---》修改订单状态成已支付
- 前端get回调接口
订单板状表设计
-
订单表
-
订单详情表
# 订单板块需要写的接口 -下单接口---》没有支付是订单是待支付状态 -支付宝post回调接口---》修改订单状态成已支付 -前端get回调接口(暂时先不关注) # 订单板块表设计 -订单表 -订单详情表 # 新建order 的app,在models.py中写入表 from django.db import models from django.db import models from user.models import User from course.models import Course class Order(models.Model): """订单模型""" status_choices = ( (0, '未支付'), (1, '已支付'), (2, '已取消'), (3, '超时取消'), ) pay_choices = ( (1, '支付宝'), (2, '微信支付'), ) # 订单标题 subject = models.CharField(max_length=150, verbose_name="订单标题") # 订单总价格 total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总价", default=0) # 订单号,咱们后端生成的,唯一:后期支付宝回调回来的数据会带着这个订单号,根据这个订单号修改订单状态 # 使用什么生成? uuid(可能重复,概率很多) 【分布式id的生成】 雪花算法 out_trade_no = models.CharField(max_length=64, verbose_name="订单号", unique=True) # 流水号:支付宝生成的,回调回来,会带着 trade_no = models.CharField(max_length=64, null=True, verbose_name="流水号") # 订单状态 order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态") # 支付类型,目前只有支付宝 pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式") # 支付时间---》支付宝回调回来,会带着 pay_time = models.DateTimeField(null=True, verbose_name="支付时间") # 跟用户一对多 models.DO_NOTHING user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="下单用户") created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: db_table = "luffy_order" verbose_name = "订单记录" verbose_name_plural = "订单记录" def __str__(self): return "%s - ¥%s" % (self.subject, self.total_amount) class OrderDetail(models.Model): """订单详情""" # related_name 反向查询替换表名小写_set # on_delete 级联删除 # db_constraint=False ----》默认是True,会在表中为Order何OrderDetail创建外键约束 # db_constraint=False 没有外键约束,插入数据 速度快, 可能会产生脏数据【不合理】,所以咱们要用程序控制,以后公司惯用的 # 对到数据库上,它是不建立外键,基于对象的跨表查,基于连表的查询,继续用,跟之前没有任何区别 order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False, verbose_name="订单") course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="课程") price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价") real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价") class Meta: db_table = "luffy_order_detail" verbose_name = "订单详情" verbose_name_plural = "订单详情" def __str__(self): try: return "%s的订单:%s" % (self.course.name, self.order.out_trade_no) except: return super().__str__()
模型被删除
如果一个模型使用了外键,那么在对方那个模型被删除后,该进行什么样的操作。可以通过on_delete来指定,可以指定的类型如下:
- CASCADE:级联操作。如果外键对应的那条数据被删除了,那么这条数据也会被删除。
- PROTECT:受保护。及只要这条数据引用了外键的那条数据,那么就不能删除外键的那条数据。
- SET_NULL:设置为空。如果外键的那条数据被删除了,那么本条数据上就将这个这个字段设置为空。如果设置的这个选项的前提是指定这个字段可以为空
- SET_DEFAULT:设置默认值,如果外键的那条数据被删除的,则这条数据则设置为默认值。如果设置这个选项前提是指定这个字段有默认值
- SET():如果外键的那条数据被删除了,那么将会获取SET函数中的值(里面可以填写函数的内存地址),如果是可以调用的,则会把返回值
- DO_NOTHING:不采取任何行为。一切全看数据库级别的约束。
下单接口
# 登录后才能用---》前端点击立即购买----》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 返回前端支付链接
view层
from rest_framework.viewsets import GenericViewSet
from rest_framework.views import APIView
from rest_framework.response import Response
from utils import logger
from .models import Course, Order
from .serializer import PaySerializer
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from utils import APIResponse
from libs import aalipay
class PayView(GenericViewSet, ):
serializer_class = PaySerializer
authentication_classes = [JSONWebTokenAuthentication, ]
permission_classes = [IsAuthenticated]
def create(self, request):
# 订单里面 还需要 用户 id 用户id 去context 里面去取 如果没有则会报错
serializer = self.get_serializer(data=request.data, context={"request": request})
serializer.is_valid(raise_exception=True)
url = serializer.context.get("pay_url")
self.perform_create(serializer)
return APIResponse(pay_url=url)
def perform_create(self, res):
res.save()
serializer层
from rest_framework import serializers
from .models import Course, Order, OrderDetail
import uuid
from libs import aalipay, GATEWAY
from rest_framework.exceptions import APIException
from django.conf import settings
class PaySerializer(serializers.ModelSerializer):
# course = serializers.ListField() # 如果这样写的化 会自己去拿课程对象
# 这样写 他会 for 循环里面的id 然后去 course 表里面去查对应的课程对象 得到的 [课程对象,]
courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True)
class Meta:
model = Order
# 全部课程,但是这个字段 表中没有所以需要自己写 可能产品有多个 一般是列表
# total_amount需要自己校验 价格是否准确
# subject 订单的标题 这个我们表中有
fields = ["courses", "total_amount", "subject"]
def validate(self, attrs):
# 在这 校验 总价格 和 生成订单
course_obj_list = attrs.get("courses")
total_amount = 0
for course in course_obj_list:
total_amount += float(course.price)
if total_amount == attrs.get("total_amount"):
# 生成订单号
out_trade_no = self.get_uuid()
# 去完成添加前的数据整理
self._before_create(out_trade_no, attrs)
# 则去生成订单
self.get_pay_order(attrs.get("subject"), out_trade_no, total_amount)
return attrs
raise APIException("生成订单失败")
def get_uuid(self):
# 只有上亿的时候 uuid4才会出现重复
# 可以用uuid2或者其他的 需要传参数 所以重复概率小些
return str(uuid.uuid4()) # 如果是 uuid 对象不能被序列化
## res = aalipay.api_alipay_trade_page_pay(subject='xx课程', out_trade_no='8d0be464-f30c-486b-aa4a-57da887efb45',
# total_amount='99')
def get_pay_order(self, subject, out_trade_no, total_amount):
res = aalipay.api_alipay_trade_page_pay(subject=subject,
out_trade_no=out_trade_no,
total_amount=total_amount,
# 前端的回调地址
return_url=settings.RETURN_URL,
# 后端的回调地址
notify_url=settings.NOTIFY_URL)
self.context["pay_url"] = 'https://openapi.alipaydev.com/gateway.do?' + res
# print('https://openapi.alipaydev.com/gateway.do?' + res, 111)
# alipay.api_alipay_trade_page_pay(subject='性感内衣', out_trade_no='asdas23sddfsasf', total_amount='999')
def _before_create(self, out_trade_no, attrs):
attrs["out_trade_no"] = out_trade_no
attrs["user"] = self.context.get("request").user
def create(self, validated_data):
# 写如订单和 写入订单详情
# validated_data = {courses total_amount subject, out_trade_no
courses = validated_data.pop("courses")
order_obj = Order.objects.create(**validated_data)
# 批量生成订单详情
order_detail_list = []
for i in courses:
obj = OrderDetail(course=i, price=i.price, order=order_obj, real_price=i.price)
order_detail_list.append(obj)
OrderDetail.objects.bulk_create(order_detail_list)
return order_obj
路由
from django.urls import path, include
from .views import PayView, PaySuccess
from django.urls import path, include
from rest_framework.routers import SimpleRouter
route = SimpleRouter()
route.register('pay', PayView, 'pay')
urlpatterns = [
path("success/", PaySuccess.as_view())
]
urlpatterns += route.urls
下单的前端
在课程详情中加一个事件
go_pay() {
// 得先登录才能购买 先取校验token
let token = this.$cookies.get("token")
if (!token) {
this.$message.error("请先登录")
} else {
this.$axios.post(this.$settings.BASE_URL + '/order/pay/', {
courses: [this.course_id,],
total_amount: this.course_info.price,
subject: this.course_info.name
}, {
headers: {
Authorization: `jwt ${token}`
}
}).then(res => {
if (res.data.code === 100) {
// _self 是在当前跳转
open(res.data.pay_url, '_self')
} else {
this.$message.error(res.data.msg)
}
})
}
},
支付成功的回调
前端路由
{
path: '/pay/success',
name: 'paysuccess',
component: PaySuccess
},
前端的页面
<template>
<div class="pay-success">
<!--如果是单独的页面,就没必要展示导航栏(带有登录的用户)-->
<Header/>
<div class="main">
<div class="title">
<div class="success-tips">
<p class="tips">您已成功购买 1 门课程!</p>
</div>
</div>
<div class="order-info">
<p class="info"><b>订单号:</b><span>{{ result.out_trade_no }}</span></p>
<p class="info"><b>交易号:</b><span>{{ result.trade_no }}</span></p>
<p class="info"><b>付款时间:</b><span><span>{{ result.timestamp }}</span></span></p>
</div>
<div class="study">
<span>立即学习</span>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/Header"
export default {
name: "Success",
data() {
return {
result: {},
};
},
created() {
// 解析支付宝回调的url参数
let params = location.search.substring(1); // 去除? => a=1&b=2
let items = params.length ? params.split('&') : []; // ['a=1', 'b=2']
//逐个将每一项添加到args对象中
for (let i = 0; i < items.length; i++) { // 第一次循环a=1,第二次b=2
let k_v = items[i].split('='); // ['a', '1']
//解码操作,因为查询字符串经过编码的
if (k_v.length >= 2) {
// url编码反解
let k = decodeURIComponent(k_v[0]);
this.result[k] = decodeURIComponent(k_v[1]);
// 没有url编码反解
// this.result[k_v[0]] = k_v[1];
}
}
// 把地址栏上面的支付结果,再get请求转发给后端
this.$axios({
url: this.$settings.BASE_URL + '/order/success/' + location.search,
method: 'get',
}).then(response => {
if (response.data.code != 100) {
alert(response.data.msg)
}
}).catch(() => {
console.log('支付结果同步失败');
})
},
components: {
Header,
}
}
</script>
<style scoped>
.main {
padding: 60px 0;
margin: 0 auto;
width: 1200px;
background: #fff;
}
.main .title {
display: flex;
-ms-flex-align: center;
align-items: center;
padding: 25px 40px;
border-bottom: 1px solid #f2f2f2;
}
.main .title .success-tips {
box-sizing: border-box;
}
.title img {
vertical-align: middle;
width: 60px;
height: 60px;
margin-right: 40px;
}
.title .success-tips {
box-sizing: border-box;
}
.title .tips {
font-size: 26px;
color: #000;
}
.info span {
color: #ec6730;
}
.order-info {
padding: 25px 48px;
padding-bottom: 15px;
border-bottom: 1px solid #f2f2f2;
}
.order-info p {
display: -ms-flexbox;
display: flex;
margin-bottom: 10px;
font-size: 16px;
}
.order-info p b {
font-weight: 400;
color: #9d9d9d;
white-space: nowrap;
}
.study {
padding: 25px 40px;
}
.study span {
display: block;
width: 140px;
height: 42px;
text-align: center;
line-height: 42px;
cursor: pointer;
background: #ffc210;
border-radius: 6px;
font-size: 16px;
color: #fff;
}
</style>
后端路由
from django.urls import path, include
from .views import PayView, PaySuccess
from django.urls import path, include
from rest_framework.routers import SimpleRouter
route = SimpleRouter()
route.register('pay', PayView, 'pay')
urlpatterns = [
path("success/", PaySuccess.as_view())
]
urlpatterns += route.urls
后端的views
class PaySuccess(APIView):
def get(self, request):
out_trade_no = request.query_params.get("out_trade_no")
res = Order.objects.filter(out_trade_no=out_trade_no, order_status=1).first()
if not res:
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 = aalipay.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') # 都是支付宝要求的

浙公网安备 33010602011771号