luffy_搜索功能前端__搜索结构页面__搜索功能接口__支付宝介绍__官方sdk,,第三方sdk,,支付宝公钥;应用私钥

前倾回顾

复制代码
# 1 课程板块---》表分析
    -多种类型课程---》设计成一个表还是多个表
    -表:
        - 实战课表
        - 课程分类表
        - 老师表
        - 章节表
        - 课时表
        
# 2 录入数据
# 3 课程板块的接口分析
    - 课程分类接口---》查询所有课程分析
    - 查询所有课程接口
        - 排序:内置排序
        - 过滤:第三方 django-filter---》按字段精准过滤
            -按分类精准过滤
        - 分页:基本分页
   - 查询课程详情
    -继续跟查询所有课程使用同一个视图类
    -serializer_class 是还继续用同一个
        -如果用同一个:详情缺数据---》章节及章节下课时数据
            -再配合一个接口:查询所有章节带课时---》带按课程过滤
        -不用同一个,重写一个
            -重写方法:定制使用哪个序列化类:get_serializer_class
                -根据action,不同的方法使用不同序列化类
            -补充:如果想数据源不一样,要重写:get_queryset
  -查询所有章节带课时的接口

# 4 查询所有课程分类

# 5 查询所有课程接口
## 5.1 视图类
class CourseView(GenericViewSet, CommonListModelMixin):
    queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
    serializer_class = CourseSerializer
    filter_backends = [OrderingFilter, DjangoFilterBackend]
    ordering_fields = ['-id', 'price', 'students']
    filterset_fields = ['course_category']
    pagination_class = PageNumberPagination
    
## 5.2 序列化类
class CourseSerializer(serializers.ModelSerializer):
    teacher = TeacherSerializer()  # 子序列化 如果有多条,就写成 many=True
    class Meta:
        model = Course
        fields = [
            'id',
            'name',
            'course_img',
            'brief',
            'attachment_path',
            'pub_sections',
            'price',
            'students',
            'period',
            'sections',

            'course_type_name',  # 定制字段,choice
            'level_name',
            'status_name',

            'teacher',  # 定制字段  1 表模型中写 2 序列化类中写 2 子序列化
            'section_list',  # 定制字段[表模型中],显示课时:课程直接跟章节关联--》章节跟课时管理--》做多显示4个课时,如果超过4个,只显示4个,如果不足4个,有几个显示几个

        ]
        
 ## 5.3 表模型--》定制section_list
    def section_list(self):
        l = []
        for course_chapter in self.coursechapters.all():  # 先拿出课程下所有章节  基于对象的反向查询
            for course_section in course_chapter.coursesections.all():  # 拿出当前章节下的所有课时
                # 拼接字典
                l.append(
                    {'id': course_section.pk,
                     'name': course_section.name,
                     'section_link': course_section.section_link,
                     'duration': course_section.duration,
                     'free_trail': course_section.free_trail, })
                if len(l) >= 4:  # 最多显示4条,不足4条,有几条显示几条
                    return l
        return l
    
    
    
# 6 课程详情
    -方式一:只需要在查询所有课程的视图类上加一个扩展类即可--》默认用同一个序列化类
        class CourseView(GenericViewSet, CommonListModelMixin, CommonRetrieveModelMixin):
         重写一个序列化类,重写get_serializer_class
    -方式二:
        配合一个新接口:查询所有章节:带按课程过滤功能
        class CourseChapterView(GenericViewSet, CommonListModelMixin):
            queryset = CourseChapter.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
            serializer_class = CourseChapterSerializer
            filter_backends = [DjangoFilterBackend]
            filterset_fields = ['course']
            
            
            
# 7 前端课程列表页
    -轻课.vue
    -免费课.vue
    -实战课.vue # 写它
# 8 实战课.vue
    -页面created成功:发了两个请求
        -查询所有课程分类
        -查询所有课程
        
# 9 课程详情页面
    -实战课列表跳转过来的---》携带id号
        -地址中  : /course/detail/:pk  ---》this.$route.params.pk
        -?后面  :/course/detail/?pk=1   --->this.$route.query.pk
    -视频播放器组件
        -多尝试几个开源的
        
        
# 10 视频托管
    -放到media
    -第三方托管:花钱
        -oss 阿里云
        -七牛云:使用流程
            -创建桶 bucket
            -代码上传:上传文件接口---》py---》七牛sdk--》传到七牛--》地址--》数据库中
            -前端上传:---》上传文件接口:视频地址---》数据库中
    -公司内部自己搭建
        -ceph
        -fastdfs:c写的,管理界面,命令或代码查看,不好
        -minio:管理页面
复制代码

今日内容

搜索功能前端

复制代码
# 搜索框---》Header.vue--》任意页面中都有

# 搜索结果页面
#### Header.vue
##template:
<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 type="button" @click="search_action(search_word)" icon="el-icon-search"></el-button>
</form>
## data:
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;
},


## style
复制代码

搜索结果页面

复制代码
<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="'/course/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="[2, 3, 5, 10]"
            :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('/course/search/', {
        params: this.filter
      }).then(response => {
        this.course_list = response.actual_course;
      }).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>
复制代码

搜索功能接口

复制代码
## 路由
router.register('search', SearchCourseView, 'search')
# 视图函数

from utils.common_response import APIResponse
from rest_framework.filters import SearchFilter
class SearchCourseView(GenericViewSet, CommonListModelMixin):
    '''
    目前我们只写了实战课:搜索只能搜索实战课
    实际上:要把所有类型课程搜索出来
        -实战课
        -轻课
        -免费课
    高级的搜索:非常牛
        -淘宝:为什么搜索出来是你想要的。不同人搜索不一样
        -mysql做不到---》分布式全文减少引擎 Elasticsearch

    一个公司项目的灵魂其实就是搜索功能[推荐]

    '''
    queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
    serializer_class = CourseSerializer
    filter_backends = [SearchFilter]
    search_fields = ['name','brief']

    def list(self, request, *args, **kwargs):
        # 正常应该返回各种类型的课程,但是我们只有实战课,只返回实战课
        res=super(SearchCourseView, self).list(request, *args, **kwargs)

        # free_course='查别的表,序列化'

        # light_course='查别的表,序列化'

        return APIResponse(actual_course=res.data.get('results'),free_course=[],light_course=[])
复制代码

支付宝介绍

复制代码
# 立即购买:使用支付宝支付
    -1 我们系统充值[支付宝,微信]---》余额购买
    -2 支付宝支付---》使用支付宝支付
    -3 微信支付
# 介绍:
    -https://open.alipay.com/develop/manage  扫码登录
    -网站支付:https://opendocs.alipay.com/open/270/105899
    
        -手机网站支付:可以掉起支付宝app
        -咱们不会:输入账号密码支付
    -https://opendocs.alipay.com/open/270/105898?pathHash=b3b2b667
    
    -网站支付:
        -跳转到支付宝支付页面
            -手机扫码付款
            -在网页上输入支付宝账号密码付款
            
# 接入条件
    -1 支付宝账号:个人和商户(需要用营业执照申请)
    -2 网站:备案过ICP    ICP证浙B2-20160559    ---》没有
        -国内:所有网站--》通过工信部审核--》上线运营
        -域名:购买,按年付费
        -云服务器:有公网ip--》所有人都能根据ip访问这个机器
            -阿里云买的服务器-->协助你备案--》两周
    -3 营业执照,且支付宝账号名称需与营业执照主体一致
    # 公司里:公司专门有财务
        -1 商户号
        -2 私钥
        -3 公钥
        -4 支付宝公钥
# 费率
    0.6%  
    
# 开发:
https://opendocs.alipay.com/open/270/01didh?ref=api
# 支付流程
    用户在我们网站前端---》点下单,立即购买---》向我们后端发起请求---》我们后端要请求支付宝--》支付宝返回给我们一个支付链接---》我们把链接返回给前端---》前端打开链接----》进入支付宝页面---》登录--》付款---》付款成功---》支付宝会两个回调:get post --->咱们后端接收到post回调后-->修改订单状态
    
    
# 关于回调
    -用户付款成功,支付宝会有个回调
        -get:调用前端---》给用户看
        -post:调用后端--》修改订单状态的--》异步通知说明
            -对于 PC 网站支付的交易,在用户支付完成之后,支付宝会根据 API 中商家传入的 notify_url,通过 POST 请求的形式将支付结果作为参数通知到商家系统。
            -在进行异步通知交互时,如果支付宝收到的应答不是 success ,支付宝会认为通知失败,会通过一定的策略定期重新发起通知。通知的间隔频率为:4m、10m、10m、1h、2h、6h、15h。

            
# 我们现在申请不了商户---》开发测试只能用沙箱环境--》后期公司里申请了商户号后
    -只需要修改:商户号,公钥,私钥即可
    
# 沙箱环境:测试环境---》数据都假的--》流程跟正常一样
    -https://open.alipay.com/develop/sandbox/app?is_switch_sandbox=true
复制代码

 支付宝二次封装

支付测试

# 支付宝提供了 API  也有SDK
    -SDK---》官方的:https://opendocs.alipay.com/common/02n6z6 --》官方文档不友好
        
    -第三方基于API接口封装的 sdk---》更方便

官方sdk

复制代码
# alipay.trade.page.pay  


# 1 安装:pip install alipay-sdk-python
# 2 写代码测试:
import logging
import traceback

from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
from alipay.aop.api.FileItem import FileItem
from alipay.aop.api.domain.AlipayTradeAppPayModel import AlipayTradeAppPayModel
from alipay.aop.api.domain.AlipayTradePagePayModel import AlipayTradePagePayModel
from alipay.aop.api.domain.AlipayTradePayModel import AlipayTradePayModel
from alipay.aop.api.domain.GoodsDetail import GoodsDetail
from alipay.aop.api.domain.SettleDetailInfo import SettleDetailInfo
from alipay.aop.api.domain.SettleInfo import SettleInfo
from alipay.aop.api.domain.SubMerchant import SubMerchant
from alipay.aop.api.request.AlipayOfflineMaterialImageUploadRequest import AlipayOfflineMaterialImageUploadRequest
from alipay.aop.api.request.AlipayTradeAppPayRequest import AlipayTradeAppPayRequest
from alipay.aop.api.request.AlipayTradePagePayRequest import AlipayTradePagePayRequest
from alipay.aop.api.request.AlipayTradePayRequest import AlipayTradePayRequest
from alipay.aop.api.response.AlipayOfflineMaterialImageUploadResponse import AlipayOfflineMaterialImageUploadResponse
from alipay.aop.api.response.AlipayTradePayResponse import AlipayTradePayResponse

if __name__ == '__main__':
    """
    设置配置,包括支付宝网关地址、app_id、应用私钥、支付宝公钥等,其他配置值可以查看AlipayClientConfig的定义。
    """
    alipay_client_config = AlipayClientConfig()
    # alipay_client_config.server_url = 'https://openapi.alipay.com/gateway.do'  # 真实环境
    alipay_client_config.server_url = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do'  # 沙箱环境
    alipay_client_config.app_id = ''
    # 应用私钥
    alipay_client_config.app_private_key = ''
    # 阿里公钥
    alipay_client_config.alipay_public_key = ''

    """
    得到客户端对象。
    注意,一个alipay_client_config对象对应一个DefaultAlipayClient,定义DefaultAlipayClient对象后,alipay_client_config不得修改,如果想使用不同的配置,请定义不同的DefaultAlipayClient。
    logger参数用于打印日志,不传则不打印,建议传递。
    """
    client = DefaultAlipayClient(alipay_client_config=alipay_client_config)
    """
    页面接口示例:alipay.trade.page.pay
    """
    # 对照接口文档,构造请求对象
    model = AlipayTradePagePayModel()
    model.out_trade_no = "000010004"
    model.total_amount = 1
    model.subject = "重启娃娃-保密发货"
    model.body = "重启娃娃"
    model.product_code = "FAST_INSTANT_TRADE_PAY"

    request = AlipayTradePagePayRequest(biz_model=model)
    # 两个回调地址:get回调  post 回调
    request.return_url='http://www.baidu.com' # get回调
    request.notify_url='http://www.baidu.com/post' # post 回调  我们看不到
    # 得到构造的请求,如果http_method是GET,则是一个带完成请求参数的url,如果http_method是POST,则是一段HTML表单片段
    response = client.page_execute(request, http_method="GET")

    print("alipay.trade.page.pay response:" + response)
复制代码

第三方sdk

# https://github.com/fzlee/alipay
# pip install python-alipay-sdk
# 支付宝软件:生成公钥私钥---》验证签名
-生成应用公钥,应用私钥
-上传到支付宝平台:填入应用公钥---》生成一个支付宝公钥
- 应用公钥,应用私钥(不要给别人),支付宝公钥
-咱们自己生成的跟支付宝提供给咱们的没有区别,只能用一个


# 支付宝公钥 应用私钥

复制代码
from alipay import AliPay, DCAliPay, ISVAliPay
from alipay.utils import AliPayConfig

# 支付宝网页下载的证书不能直接被使用,需要加上头尾
# 你可以在此处找到例子: tests/certs/ali/ali_private_key.pem
app_private_key_string = open("./app_private.pem").read()
alipay_public_key_string = open("./public_key_ali.pem").read()

alipay = AliPay(
    appid="9021000129694319",
    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)  # 可选,请求超时时间
)

# 电脑网站支付,需要跳转到:https://openapi.alipay.com/gateway.do? + order_string
order_string = alipay.api_alipay_trade_page_pay(
    out_trade_no="010001101",
    total_amount=8,
    subject='充气娃娃',
    return_url="https://www.baidu.com",
    notify_url="https://www.baidu.com/post" # 可选,不填则使用默认 notify url
)
#
print('https://openapi-sandbox.dl.alipaydev.com/gateway.do?'+order_string)
复制代码

支付宝支付封装

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

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)  # 可选,请求超时时间
)
复制代码

settings.py

复制代码
import os

#### 替换应用私钥   支付宝公钥  和 应用ID即可

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

init.py

from .pay import alipay
from .settings import GATEWAY

支付相关表

# 用户在前端点击立即购买---》触发我们后端下单接口---》下单接口返回给前端支付地址---》前端跳转到支付链接---》用户去付款
# 表分析
    -1 订单表
    -2 订单详情表 :一个订单有多个订单详情
复制代码
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__()
复制代码

下单接口

# 用户在前端点击立即购买---》触发我们后端下单接口---》下单接口返回给前端支付地址---》前端跳转到支付链接---》用户去付款

# 前端携带什么数据
    {'courses':[1,],'total_amount':0.1,'subject':课程名,'pay_type':1}

路由

复制代码
from django.urls import path

# from .views import TestLoggerView, TestExceptionView, TestResponseView, cors
from .views import OrderView

from rest_framework.routers import SimpleRouter

router = SimpleRouter()
# /api/v1/order/pay/--->post 请求---》OrderView的create方法
router.register('pay', OrderView, 'pay')
urlpatterns = [
]

urlpatterns += router.urls
复制代码

视图类

复制代码
class OrderView(GenericViewSet):
    # 必须登录了---》认证类,权限类
    authentication_classes = [JWTAuthentication]
    permission_classes = [IsAuthenticated]

    # 序列化类做保存【需要自己写--》存订单和订单详情表】---》数据校验
    # serializer_class = OrderSerializer

    def create(self, request, *args, **kwargs):
        # serializer = self.get_serializer(data=request.data)
        serializer = OrderSerializer(data=request.data, context={
            'request': request})  # {'courses':[1,],'total_amount':0.1,'subject':课程名,'pay_type':1}
        serializer.is_valid(raise_exception=True)
        serializer.save()
        pay_url = serializer.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.ipay import alipay, GATEWAY
from django.conf import settings


# 1 校验数据  2 保存
class OrderSerializer(serializers.ModelSerializer):
    # courses = serializers.ListField()  # 正常应该使用 ListField 接收列表-->[1,2,3]
    # 一下会把 [1,2,3] 按 queryset 映射成对象 [课程id为1的,课程id为2的,课程id为3的]
    courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True)
    class Meta:
        model = Order
        # 表中没有  courses ,这个字段要重写
        fields = ['total_amount', 'subject', 'pay_type', 'courses']  # 用来校验数据的
    def validate(self, attrs):  # {'courses':[课程id为1d的课程对象,],'total_amount':0.1,'subject':课程名,'pay_type':1}
        # 1 校验计算的总价格和传入的价格是否一致
        self._check_price(attrs)
        # 2 生成订单号--》uuid
        out_trade_no = self._get_out_trade_no()
        # 3 生成支付链接--放到 context中
        pay_url = self._get_pay_url(attrs, out_trade_no)
        # 4 获取 支付用户---》当前登录用户
        user = self._get_user()
        # 5 入库前的准备:需要订单号
        self._pre_save(pay_url, out_trade_no, user, attrs)
        return attrs

    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_out_trade_no(self):
        return str(uuid.uuid4())

    def _get_pay_url(self, attrs, out_trade_no):
        order_string = alipay.api_alipay_trade_page_pay(
            out_trade_no=out_trade_no,
            total_amount=float(attrs.get('total_amount')),  # 注意,把它转成小数
            subject=attrs.get('subject'),
            return_url=settings.RETURN_URL,
            notify_url=settings.NOTIFY_URL  # 可选,不填则使用默认 notify url
        )
        pay_url = GATEWAY + order_string
        return pay_url

    def _get_user(self):
        request = self.context.get('request')
        return request.user

    def _pre_save(self, pay_url, out_trade_no, user, attrs):
        self.context['pay_url'] = pay_url
        attrs['out_trade_no'] = out_trade_no
        attrs['user'] = user

    def create(self, validated_data):
        # {'courses':[课程id为1d的课程对象,],'total_amount':0.1,'subject':课程名,'pay_type':1,out_trade_no:dasfdasfd}
        # 先存 Order表
        courses = validated_data.pop('courses')
        order = Order.objects.create(**validated_data)
        # 再存OrderDetail表
        for course in courses:
            OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)
        return order
复制代码

前端支付

复制代码
    go_pay() {
      // 1 校验用户是否登录
      let token = this.$cookies.get('token')
      if (token) {
        this.$axios({
          url: '/order/pay/',
          method: 'POST',
          headers: {
            authorization: 'Bearer ' + token,
          },
          data: {
            'courses': [this.course_id],
            'total_amount': this.course_info.price,
            'subject': this.course_info.name,
            'pay_type': 1
          }

        }).then(res => {
          let pay_url = res.pay_url
          //打开链接,支付
          open(pay_url, '_self');
        }).catch(res => {
          this.$message.error('系统异常,请稍后再试')
        })
      } else {
        this.$message.error('您没有登录,请先登录再下单')
      }
    },
复制代码

 

posted @   拆尼斯、帕丁顿  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示