搜索和支付模块

0 视频托管

# 1 文件:头像,文件,视频---》使用第三方托管
	-阿里oss
    -七牛云
    -腾讯
# 2 自己搭建 文件服务器
	-fastdfs:适合存储小文件
    -minIO:第三开源文件存储
    
    
# 3 桶:bucket

# 4 文件上传到七牛云
	-python
    -js
    
    
# 使用代码上传到 minio

#pip install minio

from minio import Minio

# 使用endpoint、access key和secret key来初始化minioClient对象。
minioClient = Minio('192.168.1.252:9000',
                    access_key='B1SKQVR6PRS1DT0NCSYM',
                    secret_key='Nqk3O0lHsbrv58OtyiMoCI41ZnTmSCMhsZZ2hptS',
                    secure=False)
# 调用make_bucket来创建一个存储桶。
# minioClient.make_bucket("maylogs", location="us-east-1")
# test01 为桶名字
res = minioClient.fput_object('lqz-test', 'lqz.jpg', './lqz.jpg')
print(res.object_name)
print('http://192.168.1.252:9090/lqz-test/lqz.jpg')
print('文件地址为【文件在浏览器打开会直接下载,放到index.html 中使用img引入查看】:\n', 'http://192.168.1.252:9000/test01/' + res.object_name)

# pip3 install py3Fdfs
from fdfs_client.client import get_tracker_conf, Fdfs_client

tracker_conf = get_tracker_conf('./client.conf')
client = Fdfs_client(tracker_conf)

#文件上传
# result = client.upload_by_filename('./lqz.jpg')
# print(result)
# {'Group name': b'group1', 'Remote file_id': b'group1/M00/00/00/rBMGZWCeGhqAR_vRAAIAABZebgw.sqlite', 'Status': 'Upload successed.', 'Local file name': './db.sqlite3', 'Uploaded size': '128.00KB', 'Storage IP': b'101.133.225.166'}
# 访问地址即可下载:http://192.168.1.252:8888/group1/M00/00/00/CgAAzmSihyKAUybqAAH8LXKkrrY060.jpg


#文件下载
# result = client.download_to_file('./xx.jpg', b'group1/M00/00/00/CgAAzmSihyKAUybqAAH8LXKkrrY060.jpg')
# print(result)


# #文件删除
result = client.delete_file(b'group1/M00/00/00/CgAAzmSihyKAUybqAAH8LXKkrrY060.jpg')
print(result)
# ('Delete file successed.', b'group1/M00/00/00/rBMGZWCeGhqAR_vRAAIAABZebgw.sqlite', b'192.168.1.252')

# #列出所有的group信息
# result = client.list_all_groups()
# print(result)

1 搜索功能前端

##### html####
<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" @click="search_action(search_word)"></el-button>
</form>


#### js  data:      
is_search_tip: true,
search_placeholder: '',
search_word: ''
###js :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;
    },
    
# css

 .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;
    }

2.1 搜索结果页面

<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'
import api from '../assets/js/settings'
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(api.search, {
        params: this.filter
      }).then(response => {
        // 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
        this.course_list = response.data.results;
        this.course_total = response.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>

2 搜索功能接口

from rest_framework.filters import SearchFilter


class CourseSearchView(GenericViewSet, APIListModelMixin):
    queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
    serializer_class = CourseSerializer
    pagination_class = PageNumberPagination  # 分页
    filter_backends = [SearchFilter]
    search_fields = ['name']

    # 咱么目前搜索接口:只能搜索 实战课
    # 后期要能搜索:实战课,免费课,轻课
    # 甚至更强大:输入老师名字搜到老师
    # 搜索接口后期可以返回很多数据--》结合原型图
    # 有个性化推荐

    # def list(self, request, *args, **kwargs):
    #     # 实战课
    #     res=super().list(request, *args, **kwargs)
    #     search=request.query_params.get('search')
    #     # 根据search=高级 去老师表中,根据名字或老师介绍---》推荐几个老师
    #     # 搜名字或老师介绍中有 ---》mysql--》介绍--》文字很多 like =%高级%-->效率非常低
    #         # 可以使用专业的搜索引擎:es  全文检索
    #     # 个性化推荐:用户画像---》通过打标签:青年  本科  单身  宅
    #     return APIResponse(actual_course_list=res.results,teacher_list=[])

3 支付宝支付介绍

# 1 支付
	-1 支付宝支付
    -2 微信支付
    -3 银联支付
    -4 自己支付:支付牌照
    
    
# 2 支付宝支付
	-商户号:别人把钱付款--》付到商户里面
    -商户再提现
    ----需要营业执照----没有可以使用沙箱环境测试----
    
    
    -扫码登录:沙箱环境--》不需要申请条件--》可以测试
    	-https://open.alipay.com/develop/manage 
        
    -网站支付:https://opendocs.alipay.com/open/270/105899
    
    -手机网站支付:可以掉起支付宝app
        -咱们不会:输入账号密码支付
    	-https://opendocs.alipay.com/open/270/105898?pathHash=b3b2b667
    
    -网站支付:
    	-跳转到支付宝支付页面
        	-手机扫码付款
            -在网页上输入支付宝账号密码付款
            
            
# 3 申请支付宝商户号,限制条件
#申请条件
 支持的账号类型:支付宝企业账号、支付宝个人账号。
# 签约申请提交材料要求如下:
• 提供网站地址,网站能正常访问且页面显示完整,网站需要明确经营内容且有完整的商品信息。
• 网站必须通过 ICP 备案,且备案主体需与支付宝账号主体一致。若网站备案主体与当前账号主体不同时需上传授权函。
• 如以个人账号申请,需提供营业执照,且支付宝账号名称需与营业执照主体一致。
注意:需按照要求提交材料,若部分材料不合格,收款额度将受到限制(单笔收款 ≤ 2000 元,单日收款 ≤ 20000 元)。若签约时未能提供相关材料(如营业执照),请在合约生效后的 30 天内补全,否则会影响正常收款


# 4 沙箱环境--》测试环境
	-https://open.alipay.com/develop/sandbox/app
	-商户号:
    -买家号:

    -安卓沙箱app--》跟支付宝一样
    
    
# 5 web端,集成,支付流程
	-1 前端:购买按钮
    -2 点击支付按钮,触发后端下单接口:生成支付链接,生成订单[订单表生成一条记录]
    -3 用户扫码付款[登陆后输入密码付款]
    -4 支付宝收到付款成功---》get回调--》回调到前端--》前端支付成功页面
    -5 支付宝收到付款成功---》post回调--》回调后端---》修改订单状态--》已支付状态
image-20240521173327507

3.1 快速体验

# 1 API 接口和sdk
	-早期没有python的sdk---》只能使用api接口--》第三方基于api接口封装了非官方sdk
    	-https://github.com/fzlee/alipay
        -pip install python-alipay-sdk 
    -后期有了官方sdk:
    	-https://opendocs.alipay.com/common/02np8q?pathHash=7847ca4f
        
        
# 2 支付宝支付通信,验证签名,都是使用非对称加密--》支付宝提供的软件
	-软件:https://opendocs.alipay.com/common/02kipk?pathHash=0d20b438
	-公钥
    -私钥
    
# 3 在支付宝沙箱环境中[正式环境]
	-把刚刚生成的公钥填入
    -会生成一个支付宝公钥--》把这个东西复制出来
    
# 4 在代码中,使用【支付宝公钥】和刚刚生成的【私钥】,放到代码中
    GitHub开源框架
    https://github.com/fzlee/alipay
# 5、公钥私钥设置
    """
    # alipay_public_key.pem
    -----BEGIN PUBLIC KEY-----
    支付宝公钥
    -----END PUBLIC KEY-----

    # app_private_key.pem
    -----BEGIN RSA PRIVATE KEY-----
    用户私钥
    -----END RSA PRIVATE KEY-----
    """   
# 6测试代码
from alipay import AliPay, DCAliPay, ISVAliPay
from alipay.utils import AliPayConfig

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

alipay = AliPay(
    appid="9021000137627113",
    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)  # 可选,请求超时时间
)
#老版本
order_string = alipay.api_alipay_trade_page_pay(
    out_trade_no="20161112",
    total_amount=99,
    subject='python',
    return_url="https://example.com",
    notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url
)
# print(f'支付地址:https://openapi-sandbox.dl.alipaydev.com/gateway.do?{order_string}')
#新版本
res=alipay.client_api(
  "alipay.trade.page.pay",
  biz_content={
      "out_trade_no": "20161112",
      "total_amount": 0.01,
      "subject": "an order"
  },
  return_url="https://example.com", # this is optional
)
print(f'支付地址:https://openapi-sandbox.dl.alipaydev.com/gateway.do?{res}')

image-20240521182512894

3.2 官方sdk测试

pip install alipay-sdk-python
from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
from alipay.aop.api.domain.AlipayTradePagePayModel import AlipayTradePagePayModel
from alipay.aop.api.request.AlipayTradePagePayRequest import AlipayTradePagePayRequest
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 = '9021000129694319'
    # 应用私钥
    alipay_client_config.app_private_key = 'MIIEowIBAAKCAQEA2+5a8/zdCXHnzE3T0DrWgalKE2CLhRX0+HVhVxC7+OL1Aoxag5evU6CQW0F/aixnu6bBowy+tKOggqLuRzJsZhitCpt1PyJAsQO10STIa7GAhfmZMW+SEkmLvo5n/DIQmHmjcEjQj+dS1QQH/qnIsquT7D3BPxJanTUQurIiiL/TlxV5ur4qvuYu/1Pg4uixhi/UJa2sunwQK9u1+D8fYRf4zuYEltSA3ZfgFDFOyZK/mkVdEWL9snZqJRYVQIC6xPX3qeVX7Rx0768B5HzTlzT1zp9j5tMktxjRHJX98/lYluE6vy9loBYa6VF6UOVAvRLenQKyswSiUIU+WqyQ1wIDAQABAoIBAQDBwkqrLhlmWs3GtsJnb47QCN9UFviUNXXu9yrc08dnTDxjFFgiGx7B5HGJlDi2x4xUTGPITFAvQQEGVpjqbMgHYrIA6FjxWDH6QbSLH4bbKjR61B1c6licd+L/7OI707e/PVr6b4wfW8MkHDsW52oDzxmxRe7crETcv4WPlaTLJ+JPRILVP3rWcG/DZJTe+HwHHEwWZmBKrSRUqy+PF9CCQ3FYxRIKYcg7JxnA/JlGPjnzQMckrBfDoNi2XsK4w23ioh6n6mWKQ86ENDyUQMRoB32zcSOGj2If5uVYf6ydMNTNJeGT6CUn2o4jkTdKtljXWKSnDm8zRQDqif6TsSLxAoGBAPyOO5E9dmXtPBD9YyjUauEzLNG5B87iibOn2jLfQ7PuenZgPXXG2L+q6+iAUxJzQKFUkbvQXVwGHk2/pVCeogoZdCOUQebuSSuqSkBH+UGP0WiHcOFeoJckmT74ijZ1ESWVHek8foIBab2+8gSWNBMyWaTsmYI02zq8VF9r3IiTAoGBAN7uN9DpUD70Ev5yydVLkxwC6vI85ML/G9b6iHDvd3HAlG5GcRraSdThRQ3YCDkLJufGJ7bnAEYHARdaohgNZfRLWK1HkUYk3tQThMPYBxDXbJtfm7p2Kl+/WKm4+TtrKUdnDhZPod8bL2awVth4hfoKO9GzF80R8vBPo9W6yZUtAoGAA5CKXLFuY1/m0iKRbLkazRTo1Aj1iEEASo3a8Y7fKMH77oHLPEdTNdlWvRBam88OoXhNGkaFms/nS5eh4LJsfRIA5qOoDndchwY/SAr8BKXgAcavnC62u4tjslTVtpEObeZd5rXY30Lf2DLCvbfVAlRamY5RWFogogKYekROd4sCgYAFkmKmwA4XZLZM0cWlpRvqKVCB+W+mSAYEG4Lpf7K2jx+mmfAdwbLytSaqr+mUs2inhlZbxe5F0cr/MG64ty0DLBbtTcqdvDItjsdUtcOHcjrurzcPNADfH8MxisP/7i+77yF1AUyEbQOER4gEJQ8ELtlL5nQD1h0CUJtBrkd3iQKBgHab6GJDk5JGapKSwC32FP+LNDsm++0RhZGYlGzlsa8JnkOYuMoY4Y7D9IfJA1xbfxNECDFAg/FdyOQ+vzESx1i39HYZ6WXa0wmy2kCVugGVM/8Wdp3gjbZ378iMl3GQ+rDerkmyYpzm9t1L0/8GRrhiS+OkfMBhs7gcZbL74BsZ'
    # 阿里公钥
    alipay_client_config.alipay_public_key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhddgdkn8X2t+3gnPA9dkqUNe1+SIcPQ8Mmb/8Ynac0In/s9BC5rBNrtkKEtvejQN+jmOJh9x03yHGwObYYUckwAXEIKw9LhXQWu3LdTbczl3UWgH9IW3BsZYvOY9xJWZJN1cq3MskFbqoIgB+lDiq86JRYS/QpyvCk7t5ZtY58w5A/iSTOGAqINzIW9BZBmQM8euJd26u5JNEMuotXHWlJzPeERNnxzJRUi8MpltDXfSzlxmATI/Aw2u1HGY91OIv1h7A46lURaCdyc57aj7ot+rLFymTMvKyYhyyfA2FjyyXlwZQowBFE7rDBIe+uLttgBu6O+1sUl2kRw9IeOk0QIDAQAB'

    """
    得到客户端对象。
    注意,一个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 = 999
    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)

3.3 第三方sdk测试

from alipay import AliPay
from alipay.utils import AliPayConfig

# 支付宝网页下载的证书不能直接被使用,需要加上头尾
# 你可以在此处找到例子: tests/certs/ali/ali_private_key.pem
app_private_key_string = open("./private_key.pem").read()
alipay_public_key_string = open("./al_public_key.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)  # 可选,请求超时时间
)
# 老版本
order_string = alipay.api_alipay_trade_page_pay(
    out_trade_no="10001010",
    total_amount=99,
    subject='Go语言入门',
    return_url="https://example.com",
    notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url
)
print(f'支付地址:https://openapi-sandbox.dl.alipaydev.com/gateway.do?{order_string}')


# 新版本--暂时没通
# res=alipay.client_api(
#   "alipay.trade.page.pay",
#   biz_content={
#     "out_trade_no": "100212",
#     "total_amount": 8889,
#     "subject": "性感内衣"
#   },
#   return_url="https://example.com", # this is optional
#   notify_url='https://example.com'
# )

4 支付宝二次封装

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

4.2.1 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)  # 可选,请求超时时间
)

4.2.2 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'

4.2.3 init.py

from .pay import alipay
from .settings import GATEWAY

5 支付相关表

# 1 创建一个新的app,order
# 2 表模型有
# 用户在前端点击立即购买---》触发我们后端下单接口---》下单接口返回给前端支付地址---》前端跳转到支付链接---》用户去付款
# 表分析
	-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__()

from order.models import Order,OrderDetail

# Register your models here.
admin.site.register(Order)
admin.site.register(OrderDetail)

6 下单接口(登陆后才能使用)

# 1 前端携带数据什么格式?不需要携带用户id,携带token--》request.user 就是当前登陆用户
	post请求--》{'courses':[1,],'total_amount':0.1,'subject':课程名,'pay_type':1}
    
# 2 后端接口

6.1 路由

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

urlpatterns = [
    # 127.0.0.1:8000/api/v1/order/success/--->get
    path('success/', OrderSuccessView.as_view()),
]
urlpatterns += router.urls

6.2 视图类

pip install python-alipay-sdk --upgrade
class OrderPayView(GenericViewSet):
    # 必须登录才能访问:认证类  权限类
    authentication_classes = [JWTAuthentication]
    permission_classes = [IsAuthenticated]
    # 校验--》生成支付链接--》生成订单--》序列化类的validate中
    serializer_class = OrderPaySerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data, context={'request': request})
        serializer.is_valid(raise_exception=True)
        serializer.save()  # 下单-》生成订单,--》重写create方法
        pay_url = serializer.context.get('pay_url')
        return APIResponse(pay_url=pay_url)

6.3 序列化类

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


# 1 校验--》{'courses':[1,],'total_amount':0.1,'subject':课程名,'pay_type':1}
# 2 反序列化的保存
class OrderPaySerializer(serializers.ModelSerializer):
    # courses 重写
    # courses 本来是 [1,4,5]--->会去Course.objects.all() 数据集中映射---》变成 --》[course1对象,course4对象,course5对象,]
    courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(),many=True)

    # courses=serializers.ListField()

    class Meta:
        model = Order
        fields = [
            'courses',  # 不是表中字段,需要重写
            'total_amount',
            'subject',
            'pay_type'
        ]

    def _check_total_amount(self, attrs):
        # 1 取出用户传的 courses---》课程 对象 列表
        courses = attrs.get('courses')
        # 2 取出总价格
        real_total_amount = 0
        total_amount = attrs.get('total_amount')
        # 3 通过  课程对象 列表 获取价格--》累加到一起,跟传入的 总价格比较,如果一致,就什么都不做,如果不一致,抛异常
        for course in courses:
            real_total_amount += course.price
        if real_total_amount != total_amount:
            raise APIException('价格不合法')
        return total_amount

    def _get_out_trade_no(self):
        # 使用uuid生成--》后期会有别的生成id的方案:1 效率高 2 不重复 3 单调递增趋势 4 在分布式节点中不会重复
        out_trade_no = str(uuid.uuid4()).replace('-', '')
        return out_trade_no

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

    def _get_pay_url(self, out_trade_no, total_amount, subject):
        order_string = alipay.api_alipay_trade_page_pay(
            out_trade_no=out_trade_no,
            total_amount=float(total_amount),  # 只有生成支付宝链接时,不能用Decimal
            subject=subject,
            return_url=settings.RETURN_URL,  # get 回调 --》前端
            notify_url=settings.NOTIFY_URL,  # post 回调--》后端
        )
        pay_url = GATEWAY + '?' + order_string
        # 将支付链接存入,传递给views
        self.context['pay_url'] = pay_url

    def _before_create(self, attrs, user, out_trade_no):
        # attrs ={'courses':[对象,],'total_amount':0.1,'subject':课程名,'pay_type':1}
        attrs['user'] = user
        attrs['out_trade_no'] = out_trade_no

    def validate(self, attrs):
        # 1 校验数据是否正确[订单总价校验]--》total_amount 和 courses 比较价格是否正确
        total_amount = self._check_total_amount(attrs)
        # 2 生成订单号--》唯一的--》
        out_trade_no = self._get_out_trade_no()
        # 3 获取支付人 --》当前登录用户
        user = self._get_user()
        # 4 获取支付链接--》
        self._get_pay_url(out_trade_no, total_amount, attrs.get('subject'))
        # 5 入库(两个表)的信息准备
        self._before_create(attrs, user, out_trade_no)
        return attrs

    def create(self, validated_data):
        # validated_data = {'courses': [对象, ], 'total_amount': 0.1, 'subject': 课程名, 'pay_type': 1,user:对象,out_trade_no:3333}
        # 存两个表
        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

  • settings
BASE_URL = 'http://127.0.0.1:8000'
# 前台基URL
LUFFY_URL = 'http://127.0.0.1:8080'
# 支付宝同步异步回调接口配置
# 后台异步回调接口
NOTIFY_URL = BASE_URL + "/api/v1/order/success/"
# 前台同步回调接口,没有 / 结尾
RETURN_URL = LUFFY_URL + "/api/v1/pay/success"
  • simple_jwt
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(days=7),
    "AUTH_HEADER_TYPES": ("Bearer",), }  # Access Token的有效期
image-20240521215722604

7 支付前端

<template>
  <div class="pay-success">
    <!--如果是单独的页面,就没必要展示导航栏(带有登录的用户)-->
    <Header/>
    <div class="main">
      <div class="title">
        <div class="success-tips">
          <p class="tips">您已成功购买 1 门课程!</p>
        </div>
      </div>
      <div class="order-info">
        <p class="info"><b>订单号:</b><span>{{ result.out_trade_no }}</span></p>
        <p class="info"><b>交易号:</b><span>{{ result.trade_no }}</span></p>
        <p class="info"><b>付款时间:</b><span><span>{{ result.timestamp }}</span></span></p>
      </div>
      <div class="study">
        <span>立即学习</span>
      </div>
    </div>
  </div>
</template>

<script>
import Header from "@/components/Header"
import api from '../assets/js/settings'
export default {
  name: "Success",
  data() {
    return {
      result: {},
    };
  },
  created() {
    // url后拼接的参数:?及后面的所有参数 => ?a=1&b=2
    console.log(location.search);

    // 解析支付宝回调的url参数
    let params = location.search.substring(1);  // 去除? => a=1&b=2

    let items = params.length ? params.split('&') : [];  // ['a=1', 'b=2']
    //逐个将每一项添加到args对象中
    for (let i = 0; i < items.length; i++) {  // 第一次循环a=1,第二次b=2
      let k_v = items[i].split('=');  // ['a', '1']
      //解码操作,因为查询字符串经过编码的
      if (k_v.length >= 2) {
        // url编码反解
        let k = decodeURIComponent(k_v[0]);
        this.result[k] = decodeURIComponent(k_v[1]);
        // 没有url编码反解
        // this.result[k_v[0]] = k_v[1];
      }

    }
    // 解析后的结果
    console.log(this.result);


    // 把地址栏上面的支付结果,再get请求转发给后端
    this.$axios({
      url: api.success + location.search,
      method: 'get',
    }).then(response => {
      // console.log(response.data);
        this.$message({
          type:"success",
          message: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>

8 支付成功前端回调

8.1 内网穿透

https://zhuanlan.zhihu.com/p/370483324

8.2 支付宝回调数据格式

#  回调数据格式
data = {
     "subject": "测试订单",
     "gmt_payment": "2016-11-16 11:42:19",
     "charset": "utf-8",
     "seller_id": "xxxx",
     "trade_status": "TRADE_SUCCESS",
     "buyer_id": "xxxx",
     "auth_app_id": "xxxx",
     "buyer_pay_amount": "0.01",
     "version": "1.0",
     "gmt_create": "2016-11-16 11:42:18",
     "trade_no": "xxxx",
     "fund_bill_list": "[{\"amount\":\"0.01\",\"fundChannel\":\"ALIPAYACCOUNT\"}]",
     "app_id": "xxxx",
     "notify_time": "2016-11-16 11:42:19",
     "point_amount": "0.00",
     "total_amount": "0.01",
     "notify_type": "trade_status_sync",
     "out_trade_no": "订单号", # 咱们的uuid
     "buyer_logon_id": "xxxx",
     "notify_id": "xxxx",
     "seller_email": "xxxx",
     "receipt_amount": "0.01",
     "invoice_amount": "0.01",
     "sign": "签名" # 验证签名
}

8.3 支付宝回调

class OrderSuccessView(APIView):
    # http://127.0.0.1:8000/api/v1/order/success/--->get
    def get(self, request, *args, **kwargs):
        # 给咱们前端用
        # 1 取出订单号
        out_trade_no = request.query_params.get('out_trade_no')
        # 2 去数据库查询
        order = Order.objects.filter(out_trade_no=out_trade_no, order_status=1).first()
        if order:  # 说明支付宝的post回调回来了,修改了订单状态
            return APIResponse(msg='支付成功,请去学习把')
        else:
            return APIResponse(code=101, msg='暂未收到您的付款,请稍后刷新再试')

    # post 给支付宝回调用--修改订单状态--->
    # 支付宝异步回调--》如果不返回正常的响应,会多次回调
    # 这个接口需要加认证类吗?--》支付宝用,没有token的,所以一定不能加认证类
    # 支付宝,在公网---》目前咱们测试--》永远回调不进来--》写完没法测试
    # 内网穿透:花生壳,第三方内外穿透软件,原来免费,后来收费
    def post(self, request, *args, **kwargs):
        try:
            # json编码 -->是字典
            # urlencoded--》querydict---》dict()----》纯字典
            # 支付宝回调编码是:urlencoded
            result_data = request.data.dict()  # 把request.data  ---> 转成字典格式
            out_trade_no = result_data.get('out_trade_no')
            trade_no = result_data.get('trade_no')
            signature = result_data.pop('sign')
            pay_time = result_data.get('notify_time','2024-06-01')

            result = 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, trade_no=trade_no,pay_time=pay_time)
                # 完成日志记录
                logger.warning('%s订单支付成功' % out_trade_no)
                return Response('success')
            else:
                logger.error('%s订单支付失败' % out_trade_no)
        except:
            pass
posted @ 2024-05-27 12:07  -半城烟雨  阅读(4)  评论(0编辑  收藏  举报