路飞项目----day13(文件远程存储,搜索导航栏,搜索接口,搜索页面,支付宝支付相关接口,下单接口重要,前端支付页面,支付成功回调接口)

上节回顾

# 1 课程相关数据录入
	-simpleui中录入
    -使用sql录入,在media下图片copy过去
    
    
# 2 课程分类接口


# 3 查询所有课程接口
	-带排序:人气,价格   内置排序类
    -带过滤:course_category=1
    	-第三方
    -带分页:pagination_class
    
    -序列化类:
    	-序列化字段
        	-表自己的字段----》按照原型图分析
            -关联字段----》teacher:子序列化,当前要序列化的对象中,有该字段
            
            -自定制字段---》
            	section_list(表模型中):逻辑是,根据课程取出所有章节(反向查询的字段),
                循环所有章节再取出章节下所有的课时(反向查询的字段),拼接出前端要的格式,每次
                 append完,都判断长度是否大于4
                course_type_name 系列:choice,显示中文

                
# 4 课程详情接口
	-课程列表接口,继承一个类
    	-使用的序列化类还是CourseSerializer,section_list不全
        -就想在查询课程详情的序列化类中有所有章节和所有章节下的课时
        	-如果还用这个序列化类,加个章节字段
            -如果不用这个序列化类,自己需要再写一个序列化类
            重写一下get_serializer_class,通过 action判断,如果是retrieve,就用咱们新写的
                
    -咱们如何做的?
    	-就是用了查询所有的序列化类,缺所有章节和课时
        -又写了一个接口,查询所有章节---》可以通过课程过滤

# 5 查询所有章节接口,带按课程过滤功能
	-序列化类,子序列化,多条

# 6 补充:
	以后一个前端页面,可能访问了N多接口
    一般不要在一个接口,返回所有数据

# 7 课程等级用 level
	-好处:
    	1 存储的数字,有choice对应,可以拿出中文或数字来
        2 占用空间小
    -不好出:
    	1 前端可能不知道数字的对应关系
        
        
        
    -choice字段,写死了,后期项目如果要新增一个 等级,需要停止项目,修改源代码,再发布新版本
    -如果不想停止项目,不改源代码,也能增加一个 新 等级
    	-本质level 需要对应另一个表,是个一对多的关系,只不过使用choice,不需要建立表了
        
        
        
# 8 前端复制 列表页面  详情页面
	-watch 当某个查询参数发生变化,再发送请求查询所有数据

	-详情页面:视频播放器组件
    	vue-core-video-player
        mp4,flv。。。。
        m3u8视频类型

.
.
.
.
.
.

今日内容

1 文件存储

# 视频文件,存储到某个位置,如果放在自己服务器上
	放在项目的media文件夹
	服务器上线后,用户既要访问接口,又要看视频,都是一个域名和端口
	就会出一个问题,所有的带宽都被视频占了,导致正常接口的访问速度,直线下降

# 一般策略:
	分开:文件单独放在文件服务器上,文件服务器带宽很高,和项目跑的服务器分开!!!

---------------------------------------------------

# 文件服务器:专门存储文件的服务器
-第三方:
	-阿里云:对象存储 oss
	-腾讯对象存储
	-七牛云存储

-自己搭建:
	fastdfs:文件对象存储  https://zhuanlan.zhihu.com/p/372286804

---------------------------------------------------

.
.

七牛云


# python 如何把文件传到上面
	-对应sdk


 # 七牛云存储
	使用代码,上传视频

	两种方案

	前端---》传到咱们的服务器----》服务端再传入到七牛云,返回链接给服务器,存到数据库

	用户在前端用js代码将视频直接传到七牛云上------拿到视频链接地址,提交到后端接口
	后端再将该视频链接保存到数据库

image
.
image
.
点击上传文件就可以上传文件到七牛上去了
image
.
image
.
后端项目安装七牛云sdk 在script里面创一个qiniu_t文件
image
.
.
.
.
.
.

1.1 七牛云上传文件


from qiniu import Auth,put_file

q = Auth('HWgePvKrFL36IcBG3Nqa4vdD-sg9H6_FQXeJ_soX','V2XTkjAZFF6BWfR-yB60Age4xvYQRqcuQJ-t1fdT')
# 代码初始化   两个串就是七牛云的秘钥,账号与密码


# 要上传的七牛云空间
bucket_name = 'tengyifan'
# 上传后保存的文件名
key = '致命诱惑.mp4'
# 生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)


# 要上传文件的本地路径
localfile = './致命诱惑.mp4'

# 右键运行该脚本文件 就把token,key,localfile 参数用put_file函数发到七牛云上去
ret, info = put_file(token, key, localfile, version='v2')
print(info)

image
.
image
.
拿到外链地址,放到浏览器里打开就能,播放上传到七牛云里的视频了
image
.
.
.
.
.
.
.

2 搜索导航栏 重要


# 前端  Header 组件上有个搜索框----》输入内容,搜索----》后端搜索接口

# 所有的商城类的网站,app,都会有搜索功能,其实搜索功能非常复杂且技术含量高
	咱们目前只是简单的搜索,输入课程名字,价格 就可以把实战课搜出来
	输入:课程名字,价格把所有类型课程都搜出来(查询多个表)

	后面会有专门的搜索引擎:分布式全文检索引擎 es  做专门的搜索

# 前端搜索结果呈现页面

.
.
.

2.1 Header.vue

<template>
  <div class="header">
    <div class="slogan">
      <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
    </div>
    <div class="nav">
      <ul class="left-part">
        <li class="logo">
          <router-link to="/">
            <img src="../assets/img/head-logo.svg" alt="">
          </router-link>
        </li>
        <li class="ele">
          <span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
        </li>
        <li class="ele">
          <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
        </li>
        <li class="ele">
          <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
        </li>
      </ul>

      <div class="right-part">
        <div v-if="!username">
          <span @click="put_login">登录</span>
          <span class="line">|</span>
          <span @click="put_register">注册</span>
        </div>
        <div v-else>
          <span>{{ username }}</span>
          <span class="line">|</span>
          <span>注销</span>
        </div>

      </div>
    </div>

    <Login v-if="is_login" @close="close_login" @go="put_register" @success="success_login"/>
    <Register v-if="is_register" @close="close_register" @go="put_login" @success="success_register"/>
    <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">
      <button type="button" class="glyphicon glyphicon-search" @click="search_action(search_word)">搜索</button>
    </form>
  </div>

</template>

<script>
import Login from "@/components/Login";
import Register from "@/components/Register";

export default {
  name: "Header",
  data() {
    return {
      // 当前所在路径,去sessionStorage取的,如果取不到,就是 /
      url_path: sessionStorage.url_path || '/',
      is_login: false,
      is_register: false,
      username: this.$cookies.get('username'),
      token: this.$cookies.get('token'),
      is_search_tip: true,
      search_placeholder: '',
      search_word: ''
    }
  },
  methods: {
    search_action(search_word) {
      console.log(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;
    },
    goPage(url_path) {
      // 已经是当前路由就没有必要重新跳转
      if (this.url_path !== url_path) {
        this.$router.push(url_path);
      }
      sessionStorage.url_path = url_path;
    },
    put_login() {
      this.is_login = true;
      this.is_register = false;
    },
    put_register() {
      this.is_login = false;
      this.is_register = true;
    },
    close_login() {
      this.is_login = false;
    },
    close_register() {
      this.is_register = false;
    },
    success_login() {
      this.is_login = false;
      this.username = this.$cookies.get('username')
      this.token = this.$cookies.get('token')
    },
    success_register() {
      this.is_login = true
      this.is_register = false

    }
  },
  created() {
    // 组件加载万成,就取出当前的路径,存到sessionStorage  this.$route.path
    sessionStorage.url_path = this.$route.path;
    // 把url_path = 当前路径
    this.url_path = this.$route.path;
  },
  components: {
    Login,
    Register
  }
}
</script>

<style scoped>
.search {
  float: right;
  position: relative;
  margin-top: 22px;
  margin-right: 10px;
}

.search input, .search button {
  border: none;
  outline: none;
  background-color: white;
}

.search input {
  border-bottom: 1px solid #eeeeee;
}

.search input:focus {
  border-bottom-color: orange;
}

.search input:focus + button {
  color: orange;
}

.search .tips {
  position: absolute;
  bottom: 3px;
  left: 0;
}

.search .tips span {
  border-radius: 11px;
  background-color: #eee;
  line-height: 22px;
  display: inline-block;
  padding: 0 7px;
  margin-right: 3px;
  cursor: pointer;
  color: #aaa;
  font-size: 14px;

}

.search .tips span:hover {
  color: orange;
}

.header {
  background-color: white;
  box-shadow: 0 0 5px 0 #aaa;
}

.header:after {
  content: "";
  display: block;
  clear: both;
}

.slogan {
  background-color: #eee;
  height: 40px;
}

.slogan p {
  width: 1200px;
  margin: 0 auto;
  color: #aaa;
  font-size: 13px;
  line-height: 40px;
}

.nav {
  background-color: white;
  user-select: none;
  width: 1200px;
  margin: 0 auto;

}

.nav ul {
  padding: 15px 0;
  float: left;
}

.nav ul:after {
  clear: both;
  content: '';
  display: block;
}

.nav ul li {
  float: left;
}

.logo {
  margin-right: 20px;
}

.ele {
  margin: 0 20px;
}

.ele span {
  display: block;
  font: 15px/36px '微软雅黑';
  border-bottom: 2px solid transparent;
  cursor: pointer;
}

.ele span:hover {
  border-bottom-color: orange;
}

.ele span.active {
  color: orange;
  border-bottom-color: orange;
}

.right-part {
  float: right;
}

.right-part .line {
  margin: 0 10px;
}

.right-part span {
  line-height: 68px;
  cursor: pointer;
}
</style>

.
.
.
.
.
.

3 搜索接口

# 搜索接口:get请求---》/course/search/---》要搜索的条件通过问号拼接在后面


# 路由
router.register('search', views.SearchCourseView, 'search')


# 视图类
class SearchCourseView(GenericViewSet, CommonListModelMixin):
    # 只有实战课
    queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders')
    serializer_class = CourseSerializer
    filter_backends = [SearchFilter, ]
    search_fields = ['name', 'price']
    pagination_class = PageNumberPagination

.
.
.

4 搜索页面

# 简单代码演示
<template>

  <div>你要搜索的类容:{{ search_word }}</div>
</template>

<script>
export default {
  name: "CourseSearchView",
  data(){
    return{
      search_word:'',
    }
  },
  created() {
    // 拿路由问号后携带的数据
    this.search_word = this.$route.query.word
    // console.log(this.$route.params)    // 路由斜杠后 分出来的数据
    // console.log(this.$route.query)    // 路由问号后 分出来的数据

    // 向后端发送请求

  }

}

</script>

<style scoped>

</style>

.
.
.
.
CourseSearchView


<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="[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: "CourseSearchView",
        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.$route.query.wd;

                // 获取课程列表信息, 将搜索的关键字以?search=搜索关键字 的形式拼到发给后端的请求路径的后面
                // 这样后端就可以用DRF自带的搜索类SearchFilter对要过滤的字段,用关键字进行模糊匹配
                this.$axios.get(`${this.$settings.BASE_URL}/course/search/`, {
                    params: this.filter  // filter字典里面有个search的键

                }).then(response => {
                    // 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中

                    console.log(response)
                    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>


.
.
.
.
.
.
.
.
.
.

5 支付宝支付介绍

-----------------------------------------------

# 立即购买功能,点击立即购买按钮,使用支付宝支付


# 支付宝支付
-测试环境:大家都可以测试,就是沙箱环境
	-https://openhome.alipay.com/develop/sandbox/app
-正式环境:申请,有营业执照
	-咱们做不了

-----------------------------------------------
# 咱们开发虽然用的沙箱环境,后期上线,公司会自己注册,
# 注册成功后有个商户id号,作为开发,只要有商户id号,其他步骤都是一样,
# 所有无论开发还是测试,代码都一样,只是商户号不一样!!!

# 支付流程
	前台提交订单到后台,后台返回支付宝的订单连接给前端
	前端打开支付宝的订单链接,输入用户名密码等信息后,提交请求给支付宝
	支付宝返回给前端一个一个get请求,返回给后端一个post请求
	如果后端没有响应支付宝的post请求,2天内会总共发送8次post请求给后端
	只要后端有一次响应了success给支付宝,就确认付款成功了!!!

.
.
.
.

-----------------------------------------------

# 使用支付宝支付
	-API接口
	-SDK:优先使用,早期,支付宝没有python的sdk,后期有了
	-使用了第三方sdk
		-第三方通过api接口,使用python封装了sdk,开源出来了

-----------------------------------------------

 # 沙箱环境
	-安卓的支付宝app,付款用的---》买家用的
	-扫码使用这个app,付款,这个app的钱都是假的,付款进测试商户(卖家)


https://opendocs.alipay.com/mini   # 支付宝文档中心
https://opendocs.alipay.com/open/01bxlm   # 开发工具包sdk的地址
https://opendocs.alipay.com/open/02no41   # sdk的使用
-----------------------------------------------

沙箱环境
image
.
image
.
sdk
image
.
.
.

5.1 支付测试,生成支付链接

# github 网址
# https://github.com/fzlee/alipay

------------------------------------------------

# 安装
pip install python-alipay-sdk

------------------------------------------------

# 生成公钥私钥---》非对称加密
	-使用支付宝提供的工具:https://opendocs.alipay.com/common/02kipl
	-就可以生成公钥私钥

	-生成的公钥---》配置在支付宝的网站上(沙箱环境),会生成一个支付宝公钥
	以后使用这个支付宝公钥

------------------------------------------------

# 支付宝公钥,应用私钥---》放到项目中
	-pub.pem
	-pri.pem

下载安装 密钥工具
image
image
image
.
image
image
image
.
把ali的公钥放到pub.pem文件里
把自己应用的私钥放到priv.pem文件里
.
.

# alipay_t.py 代码

from alipay import AliPay
from alipay.utils import AliPayConfig

app_private_key_string = open("./pri.pem").read()  # 打开应用的私钥
alipay_public_key_string = open("./pub.pem").read()  # 打开支付宝的公钥

alipay = AliPay(
    appid="2021000122626531",  # 自己的支付宝沙箱环境的appid

    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)  # 可选,请求超时时间
)
res = alipay.api_alipay_trade_page_pay(subject='性感内衣', out_trade_no='asdas23sddfsasf', total_amount='999')
print('https://openapi.alipaydev.com/gateway.do?' + res)

沙箱环境的支付宝付款界面
image
.
.
.
.
.
.

6 支付宝二次封装

6.1 目录结构


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

init.py

from .pay import alipay
from .settings import GATEWAY

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

# res = alipay.api_alipay_trade_page_pay(subject='性感内衣', out_trade_no='asdas23sddfsasf', total_amount='999')
# print('https://openapi.alipaydev.com/gateway.do?' + res)

settings.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 = '2016092000554611'

# 加密方式
SIGN = 'RSA2'

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

# 支付网关
GATEWAY = 'https://openapi.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'

.
.
.
.
.
.

7 订单表设计


# 订单板块需要写的接口
	-下单接口---》没有支付是订单是待支付状态
	-支付宝post回调接口---》修改订单状态成已支付
	-前端get回调接口(暂时先不关注)


# 订单板块表设计
	-订单表
	-订单详情表

----------------------------------------------

# 新建order 的app,在models.py中写入表

from django.db import models

# Create your models here.

from django.db import models
from luffy_api.apps.user.models import User
from luffy_api.apps.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(可能重复,概率很多)    【分布式id的生成】  雪花算法
    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="支付时间")

    # 跟用户一对多    models.DO_NOTHING
    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):
    """订单详情"""
    # related_name 反向查询替换表名小写_set
    # on_delete 级联删除
    # db_constraint=False ----》默认是True,会在表中为Order何OrderDetail创建外键约束
    # db_constraint=False  没有外键约束,插入数据 速度快,  可能会产生脏数据【不合理】,所以咱们要用程序控制,以后公司惯用的
    # 映射到数据库上,它是不建立外键,基于对象的跨表查,基于连表的查询,继续用,跟之前没有任何区别
    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.DO_NOTHING, 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__()

'''
ForeignKey 中on_delete
    -CASCADE   级联删除,关联的数据删除,该数据也删除
    -DO_NOTHING   关联的数据删除,该数据啥都不做,没有外键约束才能用它
    -SET_NULL      关联的数据删除,该字段置为空,字段 null=True
    -SET_DEFAULT   关联的数据删除,该字段设置为默认值,default='xx'
    -PROTECT      受保护的,很少用
    -models.SET(函数内存地址)   会设置成set内的值

'''

---------------------------------------------------

.
.
.
.
.
.
.
.

7 下单接口 重要


# 分析  下单接口,最终就是要生成订单,那就是增的逻辑

# 登录后才能用该接口
# 前端点击立即购买----发送post请求
# 携带数据  {courses:[1,],total_amount:99.9,subject:'xx课程'} 到后端
#             课程id号        总价格            订单标题

# 接口要与数据库打交道,又是增的逻辑,所以视图类继承
# GenericViewSet, CreateModelMixin
# 最后要返回给前端支付链接
# 所以视图类中重写create方法-------主要逻辑写到序列化类中

---------------------------------------------
---------------------------------------------

# 主要逻辑:
	1 取出所有课程id号,拿到课程
	2 统计总价格,跟传入的total_amount做比较,如果一样,继续往后

	3 获取购买人信息:登录后才能访问的接口 request.user
	4 生成订单号 支付链接需要,存订单表需要
	5 生成支付链接:支付宝支付生成,

	6 生成订单记录(order,order_detail),订单是待支付状态
	7 返回前端支付链接

---------------------------------------------

.
.
.

7.1 序列化类

# 校验字段,反序列化      不会序列化的

from rest_framework import serializers
from luffy_api.apps.order.models import Order, OrderDetail
from luffy_api.apps.course.models import Course
from rest_framework.exceptions import APIException
import uuid
from luffy_api.libs.alipay_v1 import GATEWAY, alipay
from django.conf import settings


# 只会校验字段,反序列化    不会序列化,因为视图类里面没有用序列化对象去.data
# 前端传什么字段,fields里面就写什么字段即可
class PaySerializer(serializers.ModelSerializer):
    #  courses 不是表的字段,需要重写该字段
    # courses = serializers.ListField()  # 这样可以,但是拿到的还是前端传入的courses=[1,2,3]

    courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True)

    # 前端传入的courses=[1,2,3]  该方法会根据queryset对象,做映射,将courses映射成courses=[课程对象1,课程对象2,课程对象3]
    # 这样拿到的courses就列表套一个个的课程对象了

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

    def _check_total_amount(self, attrs):
        courses = attrs.get('courses')  # 拿到的就是 上面的课程对象列表
        total_amount = attrs.get('total_amount')
        new_total = 0
        for course in courses:
            new_total += course.price
        if new_total == total_amount:
            return total_amount
        else:
            raise APIException('价格有误')

    def _get_out_trade_no(self):
        # uuid 生成订单号
        return str(uuid.uuid4())

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

    def get_subject(self, attrs):
        return attrs.get('subject')

    def _get_pay_url(self, out_trade_no, total_amount, subject):
        res = alipay.api_alipay_trade_page_pay(
            total_amount=float(total_amount),
            subject=subject,
            out_trade_no=out_trade_no,
            return_url=settings.RETURN_URL,  # 前端的
            notify_url=settings.NOTIFY_URL  # 后端接口,写这个接口改订单状态,需要到配置文件里配置下,
        )
        pay_url = GATEWAY + res
        self.context['pay_url'] = pay_url

    def _before_create(self, attrs, user_obj, out_trade_no):
        # 订单号加入到attrs中
        attrs['out_trade_no'] = out_trade_no
        # 把user加入到attrs中
        attrs['user'] = user_obj
        # attrs里面暂时还不能剔除courses,需要在序列化类里面create方法里面用

    # 全局钩子
    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_obj = self._get_user()

        # 4)支付链接生成
        subject = attrs.get('subject')
        self._get_pay_url(out_trade_no, total_amount, subject)

        # 5)入库(两个表)的信息准备
        self._before_create(attrs, user_obj, out_trade_no)

        return attrs

    # 正常只是往关联表里面新增数据,不需要重写,
    # 但是现在是要往订单表里面写数据,还要往订单详情表里面存数据
    # 所以需要重写序列化类的create方法
    def create(self, validated_data):
        # validated_data {courses,subject,out_trade_no,user,total_amount}
        courses = validated_data.pop('courses')
        order_obj = Order.objects.create(**validated_data)  # 存入订单表
        # 存订单详情表

        for course in courses:
            OrderDetail.objects.create(order=order_obj,
                                       course=course,
                                       price=course.price,
                                       real_price=course.price
                                       )

        return order_obj


.
.
.
.
.

7.2 视图类


from luffy_api.libs.alipay_v1 import alipay, GATEWAY
from rest_framework.viewsets import ViewSet, GenericViewSet
from luffy_api.utils.common_response import APIResponse
from rest_framework.decorators import action
from rest_framework.mixins import CreateModelMixin
from luffy_api.apps.order.models import Order, OrderDetail
from luffy_api.apps.order.serializer import PaySerializer
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from luffy_api.libs import alipay_v1
from rest_framework.response import Response
from luffy_api.utils.common_logger import logger


class PayView(GenericViewSet, CreateModelMixin):
    # @action(methods=['POST'], detail=False)
    # def pay(self, request):
    #     res = alipay.api_alipay_trade_page_pay(subject='玩具卡车', out_trade_no='87889789', total_amount='888')
    #     pay_url = GATEWAY + res
    #     return APIResponse(pay_url=pay_url)

    queryset = Order.objects.all()
    serializer_class = PaySerializer
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated]

    # 下单接口
    def create(self, request, *args, **kwargs):
        # 怎么把request对象,传入序列化对象中,还是通过序列化类的context

        serializer = self.get_serializer(data=request.data, context={'request': request})

        serializer.is_valid(raise_exception=True)  # 走到里面出问题了

        self.perform_create(serializer)

        pay_url = serializer.context.get('pay_url')
        return APIResponse(pay_url=pay_url)

    # 下单接口全部写完了

.
.
.
.

8.3 路由

# http://127.0.0.1:8000/api/v1/order/pay/pay/
router.register('pay',views.PayView,'pay')

.
.
.
.
.

8 前端支付页面

8.1 CourseDetail.vue

go_pay() {
      // 判断是否登录
      let token = this.$cookies.get('token')
      if (token) {
        this.$axios.post(this.$settings.BASE_URL + '/order/pay/', {
          subject: this.course_info.name,
          total_amount: this.course_info.price,
          courses: [this.course_id]
        }, {
          headers: {
            Authorization: `jwt ${token}`
          }
        }).then(res => {
          if (res.data.code == 100) {
            // 打开支付连接地址
            open(res.data.pay_url, '_self');
          } else {
            this.$message(res.data.msg)
          }
        })
      } else {
        this.$message('您没有登录,请先登录')
      }
    }

.
.
.

8.2 PaySuccess.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参数
    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];
      }

    }

    // 把地址栏上面的支付结果,再get请求转发给后端
    this.$axios({
      url: this.$settings.BASE_URL + '/order/success/' + location.search,
      method: 'get',
    }).then(response => {
      if (response.data.code != 100) {
        alert(response.data.msg)
      }
    }).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>

.
.
.
.
.
.
.

9 支付成功回调接口

# get 给自己用
# post 给支付宝用

# 该接口不要加任何认证和权限

.
.
.


# 支付成功回调接口
class PaySuccess(APIView):

    def get(self, request):  # 咱们用的
        out_trade_no = request.query_params.get('out_trade_no')
        order = Order.objects.filter(out_trade_no=out_trade_no, order_status=1).first()
        if order:  # 支付宝回调完, 订单状态改了
            return APIResponse()
        else:
            return APIResponse(code=101, msg='暂未收到您的付款,请稍后刷新再试')

    def post(self, request):  # 给支付宝用的,项目需要上线后才能看到  内网中,无法回调成功【使用内网穿透】
        try:
            result_data = request.data.dict()
            # requset.data 是post提交的数据,如果是urlencoded格式,requset.data是QueryDict对象,方法dict()---》转成真正的字典

            out_trade_no = result_data.get('out_trade_no')
            signature = result_data.pop('sign')
            # 验证签名的---》验签
            result = alipay_v1.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')  # 都是支付宝要求的

.
.
.
.
.

作业

# 讲到哪写到哪

posted @   tengyifan  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示