04、支付接口

1、支付宝支付介绍

# 目前线上付款方式有多种:支付宝,微信,银联 # 以支付宝为例 # 官方提供了API接口,原来没有提供python的sdk,使用第三方 # 现在官方提供了python的sdk可以尝试使用官方sdk  # 使用支付宝支付,需要是企业,要有营业执照才能申请,支付宝商家 # 测试环境,沙箱环境,即便咱们不是商家,也可以测试,以后换成真正的appid,公钥私钥--》付款就到真正的地址 # 沙箱环境:https://openhome.alipay.com/platform/appDaily.htm?tab=info # 沙箱版支付宝客户端:一个app,连得环境是测试环境,里面的钱可以一直冲,支付付到测试环境里    # 支付宝支付的场景     -https://opendocs.alipay.com/open/270/105898 # 流程 -我们自己网站有个立即购买按钮---》用户点击---》向咱们后端发送一个请求---》咱们后端生成一个没有支付的订单和支付连接(支付宝的)--->返回给前端
---》前端放在支付连接的地址---》显示出支付宝支付页面---》app扫码支付,账号密码支付---》支付成功---》支付宝收到了支付---》
支付宝会回调回咱们项目支付成功的页面---》页面展示用户购买成功---》支付宝还有一个post回调---》咱们项目利用post回调,修改订单状态

img

2、支付宝支付二次封装

结构

libs     ├── iPay                              # aliapy二次封装包     │   ├── __init__.py                 # 包文件     │   ├── pem                            # 公钥私钥文件夹     │   │   ├── alipay_public_key.pem    # 支付宝公钥文件     │   │   ├── app_private_key.pem        # 应用私钥文件     │   ├── pay.py                        # 支付文件     └── └── settings.py                  # 应用配置

alipay_public_key.pem

-----BEGIN PUBLIC KEY----- 拿应用公钥跟支付宝换来的支付宝公钥 -----END PUBLIC KEY-----

app_private_key.pem

-----BEGIN RSA PRIVATE KEY----- 通过支付宝公钥私钥签发软件签发的应用私钥 -----END RSA PRIVATE KEY-----

setting.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 = '2016093000631831'  # 加密方式 SIGN = 'RSA2'  # 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False DEBUG = True  # 支付网关 GATEWAY = 'https://openapi.alipaydev.com/gateway.do' if DEBUG else 'https://openapi.alipay.com/gateway.do'
View Code

pay.py

from alipay import AliPay from . import settings  # 支付对象 alipay = AliPay(     appid=settings.APP_ID,     app_notify_url=None,     app_private_key_string=settings.APP_PRIVATE_KEY_STRING,     alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,     sign_type=settings.SIGN,     debug=settings.DEBUG )  # 支付网关 gateway = settings.GATEWAY
View Code

init.py

# 包对外提供的变量 from .pay import gateway, alipay

3、支付接口表设计

思路

# 登陆后才能访问, 前端支付按钮--->点击支付按钮,向后端发送post请求{courses:[1,2,3],total_amount:99}-->生成订单(订单表)--》生成支付连接--》返回给前端--->前端打开  # 扩写auth的user表,使用drf-jwt,提供的认证(认证类,权限类)

订单相关表设计思路

# 订单表   # 订单详情表  class Order(models.Model):     # 主键、总金额、订单名、订单号、订单状态、创建时间、支付时间、流水号、支付方式、支付人(外键) - 优惠劵(外键,可为空)     pass  class OrderDetail(models.Model):     # 订单号(外键)、商品(外键)、实价、成交价 - 商品数量     pass 

订单相关表设计

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生成     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=支付时间)     # 用户表关联     user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False,                              verbose_name=下单用户)     # 订单创建时间  auto_now_add:新增这个时间可以不传,用当前时间   auto_now:修改时间不传,自动存入当前时间     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__()
View Code

4、支付接口

路由

router.register('pay', OrderView, 'pay')

视图类

class OrderView(GenericViewSet, CreateModelMixin):     # 登陆后才能访问     authentication_classes = [JSONWebTokenAuthentication,]     permission_classes = [IsAuthenticated]     queryset = Order.objects.all()     serializer_class = OrderSerializer      def create(self, request, *args, **kwargs):         serializer = self.get_serializer(data=request.data,context={'request':request})         serializer.is_valid(raise_exception=True)         pay_url = serializer.context.get('pay_url')         self.perform_create(serializer)         return APIResponse(pay_url=pay_url)

序列化类

# 只用来做反序列化--->数据校验-->重写create方法--》存两个表 class OrderSerializer(serializers.ModelSerializer):     # 前端传入的是课程列表[1,2,3,]---》转成[课程对象1,课程对象2,课程对象三]     courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True)      # 前端传入的是课程列表[1,2,3,]     # courses = serializers.ListField()      class Meta:         model = Order         # 订单标题,总价格,支付方式,courses所有购买的课程id号列表         fields = ('subject', 'total_amount', 'pay_type', 'courses')      def _check_total_amount(self, attrs):         courses = attrs.get('courses')  # 列表[课程1,课程2,课程3]         total_amount = attrs.get('total_amount')  # 前端传入的总价格         total_price = 0         for course in courses:             total_price += course.price         if total_price != total_amount:  # 计算完的价格不等于传入的价格,抛异常             raise ValidationError('total_amount error')         return total_amount      def _get_out_trade_no(self):         import uuid         code = str(uuid.uuid4())  # 分布式id的生成方案:订单号全局唯一,如何生成全局唯一的订单号:uuid,使用当前时间时间戳(重复概率),雪花算法         return code.replace('-', '')      # 获取支付人     def _get_user(self):         return self.context.get('request').user      # 获取支付链接     def _get_pay_url(self, out_trade_no, total_amount, subject):         from libs import iPay         from django.conf import settings         order_string = iPay.alipay.api_alipay_trade_page_pay(             out_trade_no=out_trade_no,             total_amount=float(total_amount),  # 只有生成支付宝链接时,不能用Decimal             subject=subject,             return_url=settings.RETURN_URL,  # get回调地址,前台地址             notify_url=settings.NOTIFY_URL,  # post回调地址,后台地址         )         pay_url = iPay.gateway + '?' + order_string         # 将支付链接存入,传递给views         self.context['pay_url'] = pay_url      # 入库(两个表)的信息准备     def _before_create(self, attrs, user, out_trade_no):         attrs['user'] = user         attrs['out_trade_no'] = out_trade_no      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      def create(self, validated_data):         courses = validated_data.pop('courses')         # 订单表入库,不需要courses, 订单号,订单标题,订单价格,购买人,支付方式         order = Order.objects.create(**validated_data)         # 订单详情表入库:只需要订单对象,课程对象(courses要拆成一个个course)         for course in courses:             OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)         return order
View Code

5、支付前端

 go_pay(course_info) {                 // 1 去cookie中取token,如果没有说明没登陆,不允许购买                 let token = this.$cookies.get(token)                 if (token) {                     this.$axios.post(this.$settings.base_url + 'order/pay/', {                         subject: course_info.name,                         total_amount: course_info.price,                         pay_type: 1,                         courses: [course_info.id]                     }, {                         headers: {                             authorization: 'jwt ' + token,                         }                     }).then(res => {                         if (res.data.status = 100) {                             let pay_url = res.data.pay_url                             // 跳转,在当前窗口打开这个链接                             open(pay_url, '_self');                         } else {                             this.$message({                                 message: 下单失败,请联系统管理员                             });                         }                      })                 } else {                     this.$message({                         message: 对不起,您没有登录,请登陆后购买!                     });                 }             },
View Code

6、支付成功回调接口

from utils.log import logger # from rest_framework.viewsets import ViewSet from rest_framework.views import  APIView from rest_framework.response import Response # 支付回调接口 class SuccessViewSet(APIView):     # 认证取消     authentication_classes = ()     permission_classes = ()     # 支付宝同步回调给前台,在同步通知给后台处理     # 写不写都行---》给咱们前端做二次验证的     def get(self, request, *args, **kwargs):         # return Response('后台已知晓,Over!!!')         # 不能在该接口完成订单修改操作         # 但是可以在该接口中校验订单状态(已经收到支付宝post异步通知,订单已修改),告诉前台         # print(type(request.query_params))  # django.http.request.QueryDict         # print(type(request.query_params.dict()))  # dict          out_trade_no = request.query_params.get('out_trade_no')         try:             Order.objects.get(out_trade_no=out_trade_no, order_status=1)             return APIResponse(status=100,msg='订单支付成功')         except:             return APIResponse(status=101,msg='订单还未支付')       # 支付宝异步回调处理     def post(self, request, *args, **kwargs):         try:             # request.data前端(支付宝)post传给咱们的数据--》request.data--》 QueyDic对象,不允许pop,把它转成字典             result_data = request.data.dict()             # 支付宝给我的订单号---》数据库有个订单号             out_trade_no = result_data.get('out_trade_no')             # 前面-->验证签名才信任支付宝,防止伪造             signature = result_data.pop('sign')             from libs import iPay             result = iPay.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')
View Code

7、支付成功前端

<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!=100){                     alert('暂时还没收到您的支付,请稍后刷新再试')                 }             }).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>
View Code

8、内网穿透

# 内网穿透基本都要花钱:花生壳,frp。。。 # 一般都要收费---》买个公网服务器上线后才能正常回调 # https://zhuanlan.zhihu.com/p/370483324