支付宝支付

一、支付宝支付介绍

# 购买课程,付款--->支付宝支付
	- 支付宝支付(即便没有账号,也可以测试)
    - 微信支付(需要用营业执照申请商家帐号)
    - 银联支付
    
# 支付宝支付介绍地址
https://opendocs.alipay.com/open/270/105898?pathHash=b3b2b667

集成支付宝流程

	-我们自己的网站:点击购买按钮--->向我们后端发送请求--->携带购买商品信息--->生成订单,入库,订单是未支付状态----->生成支付宝支付链接---->返回给前端
    -前端拿到支付链接--->get请求打开---->咱们的前端就来到了支付宝的页面-->用户掏出手机扫描支付--->付款完成---->支付宝收到了钱---->get回调(咱们配置回调地址)---->跳回我们自己的网页--->支付宝还会发送post请求给我们后端---->我们要验证签名,通过后,把订单状态改为已支付状态

支付宝沙箱环境

# 要使用支付宝:需要营业执照注册,咱们没有,咱们使用沙箱环境测试, 测试通过,后期只要换成公司的商户号和支付宝公钥私钥即可
# 沙箱环境:https://openhome.alipay.com/platform/appDaily.htm?tab=info
# 需要生成公钥私钥:非对称加密--->公钥加密,私钥解密
	https://opendocs.alipay.com/common/02kipl
    
# 生成公钥和私钥
# 把你的公钥,配置在支付宝账号里,生成支付宝公钥---->写支付,需要用支付宝公钥和私钥
把生成的公钥保存到沙箱商家中,并拿到支付宝公钥

二、GitHub开源框架

安装

#  使用第三方sdk
	-基于官方的api封装的
    -地址:https://github.com/fzlee/alipay
        
# 依赖
>: pip install python-alipay-sdk --upgrade
# 如果抛ssl相关错误,代表缺失该包
>: pip install pyopenssl

alipay_public_key.pem

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

app_private_key.pem

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

测试dom

from alipay import AliPay, DCAliPay, ISVAliPay
from alipay.utils import AliPayConfig

# 私钥和支付宝公钥 需要去支付宝官网生成配置
# 只能使用测试环境:沙箱环境:https://openhome.alipay.com/platform/appDaily.htm?tab=info
# 应用私钥
app_private_key_string = open("./app_private_key.pem").read()
# 支付宝公钥
alipay_public_key_string = open("./alipay_public_key.pem").read()

alipay = AliPay(
    appid="9021000122697972",  # 支付宝页面上复制,沙箱环境--->公司有人会给你
    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)  # 可选,请求超时时间
)

subject = '兰博基尼'  # 订单的字符串

order_string = alipay.api_alipay_trade_page_pay(
    out_trade_no="201102102102102103",  # 订单号,唯一
    total_amount=100,
    subject=subject,
    return_url="https://example.com",
    notify_url="https://example.com/notify"  # 可选,不填则使用默认 notify url
)
# print(order_string)
# 拼接支付宝支付网关地址
print('https://openapi-sandbox.dl.alipaydev.com/gateway.do?' + order_string)  # 支付宝支付地址

三、支付宝二次封装

结构

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

settings

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 = '9021000122697972'

# 加密方式
SIGN = 'RSA2'

# 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
DEBUG = True

# 支付网关地址
# 三元表达式:条件成立之后的结果 if 条件 else 条件不成功之后的结果
GATEWAY = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'

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=False,  # 输出调试数据
    config=AliPayConfig(timeout=15)  # 可选,请求超时时间
)

# 支付网关地址
gateway = settings.GATEWAY

init.py

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

order/urls.py

from django.urls import path
from .views import AlipayView
urlpatterns = [
    path('test_alipay/', AlipayView.as_view()),
]

order/views.py

from rest_framework.views import APIView
from utils.common_response import APIResponse
# 导入支付宝的包
from libs.own_alipay import alipay, gateway


## 测试支付宝二次封装的包
class AlipayView(APIView):
    def get(self, request, *args, **kwargs):
        res = alipay.api_alipay_trade_page_pay(
            out_trade_no="201103232102102102102",  # 订单编号
            total_amount=9000,  # 转账钱数
            subject='狗狗课程',  # 主题
            return_url="https://example.com",
            notify_url="https://example.com/notify"
        )
        # 给前端支付宝转账链接
        pay_url = gateway + res
        return APIResponse(pay_url=pay_url)

四、下单应用

4.1 创建order应用

到apps路径下
python ../../manage.py startapp order

4.2 订单相关表设计

# Order  订单表
# OrderDetail  订单详情表
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)
    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="下单用户")
    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):
    """订单详情"""
    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__()

# 数据迁移命令

五、后台-下单接口

# 前端传入数据--->在订单表中插入记录,生成支付链接,返回给前端
	-前端传入数据:{'subject':'某个课程', 'total_amount':100, 'pay_type':1, 'courses':[1,2,3]}
    
    -后端做的事情--要保存,要校验--->序列化类来做
     # 1)订单总价校验
     # 2)生成订单号:唯一的
     # 3)获取支付用户:request.user
     # 4)支付链接生成:支付宝支付链接
     # 5)入库(两个表)的信息准备

视图类

from utils.common_response import APIResponse
from rest_framework.viewsets import GenericViewSet
from .serializer import PayOrderSerializer
from rest_framework_jwt.authentication import JSONWebTokenAuthentication  # 认证类
from rest_framework.permissions import IsAuthenticated  # 频率类


# 下单支付接口
# http://127.0.0.1:8000/api/v1/order/pay/  ---->post请求,需要先登录,请求头中Authorization:jwt 
class PayOrderView(GenericViewSet):
    serializer_class = PayOrderSerializer
    # 认证类和权限类
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated]

    def create(self, request, *args, **kwargs):
        # 新增数据,支付成功后,需要在订单表、订单详情表中都新增一条数据
        ser = self.get_serializer(data=request.data, context={'request': request})  # 用上下文保存request对象,context是个字典
        ser.is_valid(raise_exception=True)
        ser.save()
        pay_url = ser.context.get('pay_url')
        return APIResponse(msg='下单成功', pay_url=pay_url)

序列化类

from rest_framework import serializers
from .models import Order, OrderDetail
from course.models import Course
from rest_framework.exceptions import APIException
from libs.own_alipay import alipay, gateway
import uuid


# 下单的序列化,需要校验数据,保存
class PayOrderSerializer(serializers.ModelSerializer):
    # {'subject':'某个课程', 'total_amount':100, 'pay_type':1, 'courses':[1,2,3]}

    # 高级用法 传入的courses=[1,2,3]---->courses=[id为1的课程对象,id为2的课程对象,id为3的课程对象]
    courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True)  # 多条数据加上many=True

    class Meta:
        model = Order
        fields = ['subject', 'total_amount', 'pay_type', 'courses']

    def _check_price(self, attrs):
        # 取出前端传入的总价格
        total_amount = attrs.get('total_amount')
        # 根据传入的课程,计算出总价格,做比较
        courses = attrs.get('courses')  # 此时就已经是一个个对象了
        # 根据列表for循环其中的对象计算总价格
        real_total_amount = 0
        for one_course in courses:
            real_total_amount += one_course.price
        if not total_amount == real_total_amount:
            raise APIException('价格不一致')

    def _get_order_no(self):
        # 使用uuid生成订单号
        return str(uuid.uuid4())  # uuids是对象,需要str转一下

    def _get_user(self):
        # 返回当前登录用户,只有登录后,执行认证类后保存用户信息
        request = self.context.get('request')
        return request.user

    def _get_pay_url(self, out_trade_no, total_amount, subject):
        order_string = alipay.api_alipay_trade_page_pay(
            out_trade_no=out_trade_no,  # 订单号,唯一
            total_amount=float(total_amount),
            subject=subject,
            return_url="https://example.com",
            notify_url="https://example.com/notify"  # 可选,不填则使用默认 notify url
        )
        pay_url = gateway + order_string
        return pay_url

    def validate(self, attrs):
        # 1)订单总价校验
        self._check_price(attrs)
        # 2)生成订单号:唯一的
        out_trade_no = self._get_order_no()
        # 3)获取支付用户:request.user
        user = self._get_user()
        # 4)支付链接生成:支付宝支付链接
        pay_url = self._get_pay_url(out_trade_no, attrs.get('total_amount'), attrs.get('subject'))
        # 5)入库(两个表)的信息准备
        attrs['user'] = user
        attrs['out_trade_no'] = out_trade_no
        # attrs={subject, total_amount, pay_type, courses, user, out_trade_no}
        self.context['pay_url'] = pay_url
        return attrs

    def create(self, validated_data):
        # {subject, total_amount, pay_type, courses, user, out_trade_no}
        courses = validated_data.pop('courses')
        # 增加订单表数据
        order = Order.objects.create(**validated_data)
        # 增加订单详情表的数据,可能有多门课程,使用for循环
        for course in courses:
            OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)
        return order

路由

from .views import AlipayView, PayOrderView
from rest_framework.routers import SimpleRouter

router = SimpleRouter()
router.register('pay', PayOrderView, 'pay')

urlpatterns = [
]
urlpatterns += router.urls

六、下单接口前端

课程主页或是详情页或者搜索页

CourseDetail.vue
<template>
    ...
	<!-- 所有的立即购买都需要加上点击事件 -->
    <button class="buy-now" @click="handleGoPay">立即购买</button>
    ...
</template>

<script>
 	export default {
    	methods: {
            // 给购买按钮增加点击事件
            handleGoPay(){
                  // 1 判断是否登录,必须登陆后才能购买
                  let token= this.$cookies.get('token')  // let,在这里使用let表示token是局部变量,只能在这方法中使用
                  if(token){
                  // 向后端发起订单请求
                    this.$axios.post(`${this.$settings.BASE_URL}order/pay/`,{
                      subject:this.course_info.name,// course_info课程的信息,是created中像后端发请求,拿取课程数据后赋值的变量
                      total_amount:this.course_info.price,
                      pay_type:1,
                      courses:[this.course_info.id],  // 前端的数据中是字典,只有id这个key,没有pk
                    },{
                      headers:{  // 请求头的数据
                        Authorization:'jwt '+token
                      }
                    }).then(res=>{
                      if(res.data.code==100){
                        let pay_url=res.data.pay_url
                        // 打开这个地址,在当前页面中打开
                        open(pay_url,'_self')

                      }else {
                        this.$message('下单失败')
                      }
                    })

                  }else {
                    // 还可以弹出登录组件
                    this.$message('您没有登录,请先登录')
                  }
            },
        }
    }
</script>

七、支付宝支付成功后的get回调和post回调

7.1 之前的代码修改

settings/dev.py

# 上线后必须换成公网地址
## 支付宝的设置
# 后台基URL
BACKEND_URL = 'http://127.0.0.1:8000'
# 前台基URL
LUFFY_URL = 'http://127.0.0.1:8080'
# 支付宝同步异步回调接口配置
# 后台异步回调接口
NOTIFY_URL = BACKEND_URL + "/api/v1/order/success/"  # post回调地址--->配合接口--->修改订单状态
# 前台同步回调接口,没有 / 结尾
RETURN_URL = LUFFY_URL + "/pay/success"  # 给用户看的,Vue的组件

order/serializer.py

    from django.conf import settings
    
    def _get_pay_url(self, out_trade_no, total_amount, subject):
        order_string = alipay.api_alipay_trade_page_pay(
            out_trade_no=out_trade_no,  # 订单号,唯一
            total_amount=float(total_amount),
            subject=subject,
            return_url=settings.RETURN_URL,  # get回调地址,支付宝前端回调的
            notify_url=settings.NOTIFY_URL  # post回调地址,支付宝会向这个接口发送post请求,告诉咱们支付成功了
        )
        pay_url = gateway + order_string
        return pay_url

7.2 前端回调页面--支付成功页面

<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 => {
            if (response.data.code !== 100) {
                // console.log(res.data);
                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>

路由

import PaySuccess from "@/views/PaySuccess.vue";

const routes = [
	// ...
    {
        path: '/pay/success',  // 支付页面组件
        name: 'pay_success',
        component: PaySuccess
    },
]

7.3 前端回调get请求

路由

urlpatterns = [
    path('success/', SuccessView.as_view({'get': 'get', 'post': 'post'})),
]

order/views.py

# 后端支付宝回调接口
class SuccessView(ViewSet):
    # 要不要加认证类,不要加认证类!!!,加上后post请求永远不会成功,支付宝没有登录的token
    def get(self, request, *args, **kwargs):
        # 前端支付成功页面的get请求,检查表中是不是真的更改数据
        try:
            out_trade_no = request.query_params.get('out_trade_no')
            # 直接查询订单存在,并且后端已经修改了支付状态的数据对象(支付宝已经调用了post修改状态了)
            Order.objects.get(out_trade_no=out_trade_no, order_status=1)
            return APIResponse()
        except Exception as e:
            return APIResponse(code=101, msg='订单未支付')

7.4 后端支付宝回调接口

# 因为咱们 地址是 127.0.0.1 ,支付宝回调,只能回调公网地址,调不回我们这,我们收不到支付宝的回调--->
解决方案:
	1 使用公网ip部署项目,买个服务器
    2 使用内网穿透:花生壳、...(就是个软件)
	内网穿透方案:https://zhuanlan.zhihu.com/p/370483324

# 后端支付宝回调接口
class SuccessView(ViewSet):
    # 要不要加认证类,不要加认证类!!!,加上后post请求永远不会成功,支付宝没有登录的token
    
	# ...
    
    def post(self, request, *args, **kwargs):
        try:
            # 支付宝回调的post请求给我们--->数据携带再请求体中--->多种格式:urlencoded(QueryDict格式)、formdata(QueryDict格式跟文件二进制格式)、json(字典格式)
            # 支付宝的post是urlencoded -->request.data是QueryDict格式,使用dict做成字典格式
            result_data = request.data.dict()
            out_trade_no = result_data.get('out_trade_no')
            signature = result_data.pop('sign')
            result = alipay.verify(result_data, signature)  # 验签通过表示是支付宝给的,不是别人伪造的
            print(result_data)
            # trade_status 确认订单付款成功了
            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)
                # Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1, trade_no=result_data.get('trade_no'), pay_time=result_data.get('timestamp'))
                # 完成日志记录
                logger.warning('%s订单支付成功' % out_trade_no)
                return Response('success')  # 给支付宝的返回必须这样,不能有别的东西
            else:
                logger.error('%s订单支付失败' % out_trade_no)
        except:
            pass
        return Response('failed')
posted @ 2023-07-14 08:25  星空看海  阅读(102)  评论(0编辑  收藏  举报