这里写目录标题
一.昨日回顾
1 支付宝支付:
-商户号:营业执照(登录的沙箱环境的商家)
-客户:我们客户
2 生成公钥和私钥:公钥配置到支付宝网站上----》支付宝公钥
-担心私钥不安全(不要把公钥私钥直接放在代码中,而是放在服务器上)
-支付宝支付(官方没有sdk,只提供了api),我们用了第三方,担心有问题
3 支付流程
-用户在前端提交支付--->后端要生成订单(未支付)--->生成支付宝付款链接--->返回前端--->get请求支付链接--->支付宝付款页面(扫描付款)--->支付宝收到钱--->get回调到我们网站(展示用户付款成功)--->post回调(验签)修改订单状态
-用户刚打开支付链接,你服务挂了,用户付款成功了
-get回调也回不来了
-支付宝的post回调,你也收不到(24小时以后,支付宝会推送8次)
-25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h)
二.今日内容
1.订单表设计
1 两张表
-一个订单可能包含多门课程
-订单表:订单号,订单生成时间,订单总价格。。。(订单跟订单详情是一对多的关系)
-订单详情表:order,course,该课程的价格。。。
from django.db import models
from user.models import User
from course.models import Course
# 不同app之间的表,可以建立关联关系,导入使用
#user = models.ForeignKey(User) # 不能用引号引起来
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)
# 咱们生成的订单号(唯一:分布式id生成方案:雪花算法,snowflake)
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="支付方式")
# 支付时间(付款成功的时候,等支付宝post回调,回掉回来以后,返回数据中有支付时间)
pay_time = models.DateTimeField(null=True, verbose_name="支付时间")
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)
@property
def courses(self):
data_list = []
for item in self.order_courses.all():
data_list.append({
"id": item.id,
"course_name": item.course.name,
"real_price": item.real_price,
})
return data_list
class OrderDetail(models.Model):
"""订单详情"""
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.CASCADE, 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__() # 相当于没写
2.立即付款接口(一堆校验,登录后)
1 付款接口需要登录后才能使用
-自己写jwt的认证类
-djangorestframwork-jwt 内置有个认证类,+ djangorestframwork的权限类也可以完成认证
(目前使用这种)
2 记得更改lib/al_pay/pay.py
这个网址后面记得加'?'
# 支付网关
GATEWAY = 'https://openapi.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'
2.1视图类
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from .models import Order
from .serializer import PayOrderSerializer
from utils.response import APIResponse
class PayView(GenericViewSet,CreateModelMixin):
queryset = Order.objects.all()
serializer_class = PayOrderSerializer
# 认证类,需要搭配权限类--->保证必须登录才能用
# 只使用下面一个:(JSONWebTokenAuthentication)
# 用户可以登录,也可以不登录,
# 如果登录了后续能之间获取request.user
# 如果没有登录,就获取不到
authentication_classes = [JSONWebTokenAuthentication,]
permission_classes = [IsAuthenticated,]
# 重写create,控制返回的数据
def create(self, request, *args, **kwargs):
ser=self.get_serializer(data=request.data,context={'request':request})
ser.is_valid(raise_exception=True)
#保存
self.perform_create(ser)
# ser.save() 跟跟上面一样
pay_url=ser.context.get('pay_url')
return APIResponse(msg='订单创建成功',pay_url=pay_url)
2.2序列化类
from rest_framework import serializers
from course.models import Course
from rest_framework.exceptions import ValidationError
from utils.exception import MyException
from .models import Order,OrderDetail
import uuid
from django.conf import settings
from lib.al_pay import alipay,gateway
class PayOrderSerializer(serializers.ModelSerializer):
# 用户传入的是courses:[1,2,3]==转成课程对象==》[obj1,obj2,obj3]
# courses:[1,2,3]---->PrimaryKeyRelatedField--->指定queryset,会去queryset中过滤,主键为[1,2,3]的课程
courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True, write_only=True)
# user=serializers.PrimaryKeyRelatedField(queryset=User.objects.all(),many=False)
class Meta:
model = Order
# 从前端传过来(订单号后端生成)
'''
{'subject':'python从入门到精通',
total_amount:100,
pay_type:1 (支付类型,目前只有支付宝),
courses:[1,2,3] 课程id号
}
'''
fields = ['subject', 'total_amount', 'pay_type', 'courses']
extra_kwargs = {
'total_amount': {'required': True, }
}
def _check_price(self, attrs):
courses = attrs.get('courses')
total = 0
for course in courses:
total += course.price
if not total == attrs.get('total_amount'):
raise MyException('钱数不合法')
def _get_order_no(self):
order_no=str(uuid.uuid4())
return order_no.replace('-','')
def _get_user(self):
user=self.context.get('request').user
return user
def _gen_pay_url(self,attrs,order_no):
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=order_no,
total_amount=float(attrs.get('total_amount')), # 转成浮点型
subject=attrs.get('subject'),
# 写在配置文件中
return_url=settings.RETURN_URL,
notify_url=settings.NOTIFY_URL
)
return gateway+order_string
def _pre_create(self,attrs,user,order_no):
# 把用户,订单状态,订单号,写入attrs
attrs['order_status']=0
attrs['user']=user
attrs['out_trade_no']=order_no
def validate(self, attrs):
# 1)订单总价校验
self._check_price(attrs)
# 2)生成订单号
order_no=self._get_order_no()
# 3)支付用户:request.user
user=self._get_user()
# 4)支付链接生成
pay_url=self._gen_pay_url(attrs,order_no)
self.context['pay_url']=pay_url
# 5)入库(两个表)的信息准备(重写create方法)
self._pre_create(attrs,user,order_no)
return attrs
def create(self, validated_data):
# 存两个表
# course需要存detail表
courses=validated_data.pop('courses')
# 开启一个事务
order=Order.objects.create(**validated_data)
for course in courses:
OrderDetail.objects.create(order=order,course=course,price=course.price,real_price=course.price)
return order
2.3自定义异常类
'''=========>utils/exception.py'''
# 自定义异常类
from rest_framework.exceptions import APIException
class MyException(APIException):
status_code = 405
default_detail = '校验出错了'
default_code = '错误'
def __init__(self, detail=None, code=None):
super().__init__(detail=None, code=None)
2.4配置文件
'''========>settings/dev.py'''
# 支付宝回调地址
RETURN_URL = "http://127.0.0.1:8080/course/paysucess"
NOTIFY_URL = "http://127.0.0.1:8000/" # 必须要用公网
2.5路由
# 使用相对导入
from . import views
# 自动生成路径
from rest_framework.routers import SimpleRouter
from .views import PayView
router = SimpleRouter()
# 127.0.0.1:8000/order/pay/
router.register('pay',PayView,'PayView')
from .views import PayView
urlpatterns = [
# path('pay', PayView.as_view()),
]
urlpatterns+=router.urls # 方式二
3.立即付款前端
views/ActualCourse.vue
buy_now(course) {
//没有登录,提示请先登录
let token = this.$cookies.get('token')
if (token) {
//发送请求获取支付链接
this.$axios({
method: 'post',
url: this.$settings.base_url + '/order/pay/',
data: {
subject: course.name,
total_amount: course.price,
pay_type: 1,
courses: [course.id,]
},
headers: {
authorization: 'jwt ' + token
}
}
).then(item => {
console.log(item.data)
if (item.data.status == 0) {
open(item.data.pay_url, '_self')
} else {
this.$message({
message: item.data.msg
})
}
})
} else {
this.$message({
message: "请先登录"
})
}
}
4.支付成功get回调用户展示
4.1luffycity/ src / views / PaySucess.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后拼接的参数:?及后面的所有参数 => ?a=1&b=2
// console.log(location.search);
// 解析支付宝回调的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];
}
}
// 解析后的结果
// console.log(this.result);
// 把地址栏上面的支付结果,再get请求转发给后端
this.$axios({
url: this.$settings.base_url + '/order/success/' + location.search,
method: 'get',
}).then(response => {
console.log(response.data);
if(!response.data.status==0){
console.log('暂未收到您的付款,请3s钟后再刷新该页面')
}
}).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>
4.2luffycity/ src / router / index.js
{
path: '/course/paysucess',
name: 'PaySucess',
component: PaySucess,
}
4.3get回调参数
# get回调,携带一些参数过来,展示课程购买成功
charset=utf-8&
out_trade_no=fe2baf0893d1403a89773e1e0151b4b3&
method=alipay.trade.page.pay.return&total_amount=99.00&
sign=C%2BtM1IqF9QBB7N86m6sEkJDoe8nvGKEymOPst%2FhrOTqZvZdyRTbr37a%2BAkhjV6Co6ot64mwJDVvlraJqevYltjXvWNovcViYAXL3JNZ%2FUoOo91PIsgFMJsTgXSy2R%2FyQ7NAhRhIhxGhNs5JNzTLt2JINKcZ%2FiUxzM%2Bkz3Z1EbjgB0JaDoNgRs9Wpwqb1VT%2FwZnyOAuoxOBwhij2SeP2ZC5qZfjQ8gzglSiUzbrQplbT3ZCGu5NHE1h42Zs8r3PkyxhghoK2T8UC7suI2u7l94713L8vP5hnegxkB79fNd4DQEZ4hnTV5nWDXjXw5RP9ob%2FQthMCTGssPS9%2Flme0%2F7w%3D%3D&
trade_no=2021011522001453300501032429&
auth_app_id=2016092000554611&version=1.0&
app_id=2016092000554611&
sign_type=RSA2&seller_id=2088102176466324&
timestamp=2021-01-15%2012%3A23%3A44
4.4后端接口
'''=====>order/urls.py'''
from .views import PaySuccess
path('success', PaySuccess.as_view()),
'''=====>order/views.py'''
class PaySuccess(APIView):
def get(self,request,*args,**kwargs): # 咱们前端回调的
out_trade_no=request.GET.get('out_trade_no')
# 去数据库查询该订单是否已经支付完成
order=Order.objects.filter(out_trade_no=out_trade_no,order_status=1).first()
if order: # post回调回调完了,订单状态改了
return APIResponse(msg='支付成功')
else:
return APIResponse(status=1,msg='暂未支付成功')
5.支付成功post回调修改订单状态
# lib/al_pay/init.py
from .pay import alipay,gateway
# order/views.py
from rest_framework.views import APIView
from utils.logging import get_logger
logger=get_logger('pay')
class PaySuccess(APIView):
def post(self,request,*args,**kwargs): # 支付宝回调
# 验签,通过,修改订单状态,返回给支付宝success
try:
result_data = request.data.dict() # 一定不能忘
out_trade_no = result_data.get('out_trade_no')
signature = result_data.pop('sign') # 签名,必须验证签名
from lib import al_pay
result = al_pay.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')