「路飞项目14」支付宝支付

10086upup / 2023-07-06 / 原文

1 搜索后台接口

# 咱们写的很简单,但是公司中这个接口是最牛逼的
-搜索接口中带个性化推荐
    
    
# 咱们只写了实战课,搜索要搜索所有跟用户输入相关的
-实战课
-轻课
-免费
-资料 文档
    
    
    
  

1.1 视图类

from utils.common_response import APIResponse
from rest_framework.filters import SearchFilter
# 搜索就是查询所有,带过滤   按课程名搜索
class SearchCourseView(GenericViewSet, ListModelMixin):
    serializer_class = CourseSerializer
    queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders')
    filter_backends = [SearchFilter]
    search_fields = ['name', 'brief']  
# 1 brief内容非常多,如果使用mysql的搜索,like 全文搜,随着数据量越来越大,搜索越来越慢---》分布式全文检索引擎elasticsearch

    # def list(self, request, *args, **kwargs):
    #     res=super().list(request, *args, **kwargs)
    #     actual_course=res.data.data   # [{},{}]
    #     free_course=[{},{},{}]
    #     light_course=[{},{},{}]
    #     others=[{},{},{}]
    #     return APIResponse(actual_course=actual_course,free_course=free_course)

# 2 有很多课程,免费课,轻课,文档。。。
    
# 3 存疑

1.2 路由

router.register('search', SearchCourseView, 'search')

2 搜索前台

2.1 搜索导航栏Header.vue 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">
        <el-button icon="el-icon-search" circle @click="search_action(search_word)"></el-button>
      </form>

2.2 搜索结果页面Search.vue--->配置路由

<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="[3, 5]"
                        :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.$axios.get(`${this.$settings.BASE_URL}course/search/`, {
                    params: this.filter
                }).then(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>

3 支付宝支付介绍

# 购买课程,付款---》支付宝支付
- 支付宝支付(即便没有账号,也可以测试)元
- 微信支付(需要用营业执照申请商家帐号)分
- 银联支付
    
# 支付宝支付介绍
https://opendocs.alipay.com/open/203/106493
    
# 集成支付宝流程
-我们自己的网站:点击购买按钮---》向我们后端发送请求---》携带购买商品信息---》生成订单,入库,订单是未支付状态-----》生成支付宝支付链接----》返回给前端

-前端拿到支付链接---》get请求打开----》咱们的前端就来到了支付宝的页面--》用户掏出手机扫描支付---》付款完成----》支付宝收到了钱----》get回调(咱们配置回调地址)----》跳回我们自己的网页---》支付宝还会发送post请求给我们后端----》我们要验签,通过后,把订单状态改为已支付状态
    
    
    
#  使用第三方sdk
-基于官方的api封装的
-pip install python-alipay-sdk --upgrade
    
  
# 要使用支付宝:需要营业执照注册,咱们没有,咱们使用沙箱环境测试, 测试通过,后期只要换成公司的商户号和支付宝公钥私钥即可


#沙箱环境:https://openhome.alipay.com/platform/appDaily.htm?tab=info


# 需要生成公钥私钥:非对称加密---》公钥加密,私钥解密
	https://opendocs.alipay.com/common/02kipl
    
# 生成公钥和私钥
# 把你的公钥,配置在支付宝账号里,生成支付宝公钥----》写支付,需要用支付宝公钥和私钥


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

# 私钥和支付宝公钥 需要去支付宝官网申请
# 只能使用测试环境:沙箱环境
app_private_key_string = open("./private.key").read()
alipay_public_key_string = open("./alipayPublic.key").read()

alipay = AliPay(
    appid="2016092000554611",   # 支付宝页面上复制,沙箱环境---》公司有人会给你
    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="201102102102102102",
    total_amount=644491.03,
    subject=subject,
    return_url="https://example.com",
    notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url
)

print('https://openapi-sandbox.dl.alipaydev.com/gateway.do?'+order_string)

4 支付宝二次封装

# GitHub开源框架
https://github.com/fzlee/alipay


# 依赖
pip install python-alipay-sdk --upgrade
# 如果抛ssl相关错误,代表缺失该包
pip install pyopenssl


# 结构
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-sandbox.dl.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi-sandbox.dl.alipaydev.com/gateway.do?'


# 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


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


# 补充:在自己项目的配置文件中配置支付宝回调接口:settings.py | dev.py
# 上线后必须换成公网地址
# 后台基URL
BASE_URL = 'http://127.0.0.1:8000/api/v1'
# 前台基URL
LUFFY_URL = 'http://127.0.0.1:8080'
# 支付宝同步异步回调接口配置
# 后台异步回调接口
NOTIFY_URL = BASE_URL + "/order/success/"
# 前台同步回调接口,没有 / 结尾
RETURN_URL = LUFFY_URL + "/pay/success"

5 下单接口

5.1 sql


5.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__()

5.3 下单接口

# 前端传入数据---》在订单表中插入记录,生成支付链接,返回给前端
-前端传入数据:{'subject':'某个课程', 'total_amount':100, 'pay_type':1, 'courses':[1,2,3]}
    
-后端做的事情--要保存,要校验---》序列化类来做
     # 1)订单总价校验
     # 2)生成订单号:唯一的
    使用uuid,数据库自增,当前时间+redis 自增,雪花算法:生成分布式唯一id,美团leaf算法
     # 3)获取支付用户:request.user
     # 4)支付链接生成:支付宝支付链接
     # 5)入库(两个表)的信息准备

视图类

from django.shortcuts import render

# Create your views here.

# 测试支付宝封装的包
from rest_framework.views import APIView
from libs.alipay_common import alipay, gateway
from utils.common_response import APIResponse


class AliPyaView(APIView):
    def get(self, request, *args, **kwargs):
        res = alipay.api_alipay_trade_page_pay(
            out_trade_no="201103232102102102102",
            total_amount=1,
            subject='性感内衣',
            return_url="https://example.com",
            notify_url="https://example.com/notify"
        )
        pay_url = gateway + res
        return APIResponse(pay_url=pay_url)


from .serialzier import PaySerializer
from rest_framework.viewsets import GenericViewSet
from .models import Order

# 127.0.0.1:8000/api/v1/order/pay/  ---->post
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class PayOrderView(GenericViewSet):
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated]
    serializer_class = PaySerializer

    def create(self, request, *args, **kwargs):
        ser = self.get_serializer(context={'request': request}, data=request.data)
        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
import uuid
from libs.alipay_common import alipay, gateway


# 校验,保存
class PaySerializer(serializers.ModelSerializer):
    # 高级用法 传入的courses=[1,2,3]---->courses=[id为1的课程对象,id为2的课程对象,id为3的课程对象]
    courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), 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')  # 是一个个对象了
        real_total_amount = 0
        for course in courses:
            real_total_amount += course.price
        if not total_amount == real_total_amount:
            raise APIException('价格不一致')

    def _get_order_no(self):
        return str(uuid.uuid4())

    def _get_user(self):
        # 返回当前登录用户
        request = self.context.get('request')
        return request.user

    def _get_pay_url(self, order_no, subject, total_amount):
        res = alipay.api_alipay_trade_page_pay(
            out_trade_no=order_no,
            total_amount=float(total_amount),
            subject=subject,
            return_url="https://example.com",
            notify_url="https://example.com/notify"
        )
        pay_url = gateway + res
        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('subject'), attrs.get('total_amount'))
        # 5)入库(两个表)的信息准备
        attrs['user'] = user
        attrs['out_trade_no'] = out_trade_no
        # attrs={'subject', 'total_amount', 'pay_type', 'courses',user,order_no}
        self.context['pay_url'] = pay_url
        return attrs

    def create(self, validated_data):  # {'subject', 'total_amount', 'pay_type', 'courses',user,order_no}
        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

路由

from django.contrib import admin
from django.urls import path

from rest_framework.routers import SimpleRouter
from .views import PayOrderView

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

urlpatterns = [
    # AliPyaView
    # path('test/', AliPyaView.as_view()),

]
urlpatterns += router.urls