Python實戰專案-10檔案儲存/支付寶支付/支付成功回撥介面

2023-03-14 21:01:09

檔案儲存

視訊檔儲存在某個位置,如果放在自己伺服器上

  • 放在專案的media資料夾
  • 伺服器上線後,使用者既要存取介面,又需要看視訊,都是使用一個域名和埠
  • 分開:問價你單獨放在檔案伺服器上,檔案伺服器頻寬比較高
# 檔案伺服器:專門儲存檔案的伺服器
	-第三方:
    	-阿里雲:物件儲存 oss
        -騰訊物件儲存
        -七牛雲端儲存
	-自己搭建:
    	fastdfs:檔案物件儲存  https://zhuanlan.zhihu.com/p/372286804
        minio:

我們可以使用對應的sdk包將檔案傳輸上去

在此專案中我們選用七牛雲來儲存視訊檔資源

使用程式碼,上傳視訊

我們參考官方檔案使用即可

1.建立七牛雲物件儲存倉庫

2.直接在桌面上傳檔案即可

1.1程式碼控制檔案上傳

python安裝七牛雲
pip install qiniu
本地測試
我們scripts資料夾下新建qiniu_test.py檔案

# -*- coding: utf-8 -*-
# flake8: noqa
from qiniu import Auth, put_file, etag
import qiniu.config
#需要填寫你的 Access Key 和 Secret Key
# 在這裡檢視金鑰 > https://portal.qiniu.com/user/key
access_key = 'Access_Key'
secret_key = 'Secret_Key'
#構建鑑權物件
q = Auth(access_key, secret_key)
#要上傳的空間
bucket_name = 'Bucket_Name'
#上傳後儲存的檔名
key = 'my-python-logo.png'
#生成上傳 Token,可以指定過期時間等
token = q.upload_token(bucket_name, key, 3600)
#要上傳檔案的本地路徑
localfile = './sync/bbb.jpg'
ret, info = put_file(token, key, localfile, version='v2') 
print(info)
assert ret['key'] == key
assert ret['hash'] == etag(localfile)

嘗試上傳本地檔案:

成功

搜尋導航欄

前端Header元件上有個搜尋方塊>>>輸入內容,即可搜尋
在所有商城類的網站,app都會有搜尋功能,其實搜尋功能非常複雜,且功能非常複雜技術含量高

  • 咱們目前只是簡單的搜尋,輸入課程名字/價格,就可以把實戰課搜出來
  • 輸入:課程名字,價格把所有型別課程都搜出來(查詢多個表)
  • 後面會有專門的搜尋引擎:分散式全文檢索引擎 es 做專門的搜尋

前端頁面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">

支付寶支付介紹

前端點選立即購買功能,會生成訂單並跳轉到付款介面

# 支付寶支付
	-測試環境:大家都可以測試
    	-https://openhome.alipay.com/develop/sandbox/app
    -正式環境:需要申請,有營業執照

咱們開發雖然用的沙箱環境,後期上線,公司會自己註冊,
註冊成功後有個商戶id號,作為開發,只要有商戶id號,其他步驟都是一樣,
所有無論開發還是測試,程式碼都一樣,只是商戶號不一樣

使用支付寶支付

  • API介面

  • SDK:優先使用,早期支付寶沒有python的sdk,後期有了

      -使用了第三方sdk
      	-第三方人通過api介面,使用python封裝了sdk,開源出來了
    

沙箱環境
-安卓的支付寶app,付款用的(買家用)
-掃碼使用這個app,付款,這個app的錢都是假的,付款測試商戶(賣家)

支付測試,生成支付連結

安裝
pip install python-alipay-sdk
生成公鑰私鑰



我們可以將生成的公鑰設定在支付寶的(沙箱環境)上,生成一個支付寶公鑰
以後我們使用這個支付寶公鑰即可

我們需要將支付寶的公鑰,以及專案的應用私鑰放入專案中
-pub.pem
-pri.pem
注意:
我們的公鑰金鑰需要符合要求格式

教學參考:https://github.com/fzlee/alipay/tree/master/tests/certs/ali

支付測試程式碼:

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="2021000122628354", # 沙盒支付寶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='asdbasbdjqweo', total_amount='2888')
print('https://openapi.alipaydev.com/gateway.do?'+res)

執行指令碼獲取連結,開啟

支付寶支付二次封裝

目錄結構

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 GETWAY

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)  # 可選,請求超時時間
)

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

# 加密方式
SIGN = 'RSA2'

# 是否是支付寶測試環境(沙箱環境),如果採用真是支付寶環境,設定False
DEBUG = True

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

訂單表設計

-訂單表
-訂單詳情表

下單介面-->沒有支付是訂單時待支付狀態
支付寶post回撥介面--> 修改訂單狀態 --已完成
前端get回撥介面

我們需要新建order app

models.py

# Create your models here.
# 訂單板塊需要寫的介面
# 新建order 的app,在models.py中寫入表
from django.db import models

from django.db import models
from course.models import Course

'''
ForeignKey 中on_delete 
    -CASCADE  級聯刪除
    -DO_NOTHING    啥都不做,沒有外來鍵約束才能用它
    -SET_NULL       欄位置為空,欄位 null=True
    -SET_DEFAULT   設定為預設值,default='xx'
    -PROTECT    受保護的,很少用
    -models.SET(函數記憶體地址)   會設定成set內的值

'''
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__()

執行遷移命令>>>

下單介面

介面分析:
	使用者登入後才能使用
	前端點選立即購買 ---> post請求攜帶資料 
    {courses:[1,],total_amount:99.9,subject:'xx課程'}
	檢視類中重寫create方法
	將主要邏輯寫到序列化類中
# 主要邏輯:
	1 取出所有課程id號,拿到課程
    2 統計總價格,跟傳入的total_amount做比較,如果一樣,繼續往後
    3 獲取購買人資訊:登入後才能存取的介面 request.user
    4 生成訂單號 支付連結需要,存訂單表需要
    5 生成支付連結:支付寶支付生成,
    6 生成訂單記錄,訂單是待支付狀態(order,order_detail)
    7 返回前端支付連結

路由

from rest_framework.routers import SimpleRouter
from . import views

router = SimpleRouter()
router.register('pay', views.PayView, 'pay')
urlpatterns = [
    # path('',include(router.urls))
]
urlpatterns += router.urls

檢視層

from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from .models import Order
from .serializer import PaySerializer
from utils.response import APIResponse
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
# Create your views here.
class PayView(GenericViewSet,CreateModelMixin):
    queryset = Order.objects.all()
    serializer_class = PaySerializer
    authentication_classes = [JSONWebTokenAuthentication] # 使用JWT許可權類設定必須配許可權類
    permission_classes = [IsAuthenticated]

    def create(self, request, *args, **kwargs):
        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)

序列化類

# 校驗欄位,反序列化      不會序列化的
class PaySerializer(serializers.ModelSerializer):
    # courses 不是表的欄位,需要重寫--->新東西
    # courses=serializers.ListField()  # 咱們不用這種  courses=[1,2,3]

    # 前端傳入的 courses=[1,2,3]--->根據queryset對應的qs物件 做對映,對映成courses=[課程物件1,課程物件2,課程物件3]
    courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True)

    class Meta:
        model = Order
        fields = ['courses', 'total_amount', 'subject']  # 前端傳入的欄位是什麼,這裡就寫什麼

    def _check_total_amount(self, attrs):
        courses = attrs.get('courses')  # 課程物件列表  [課程物件1,課程物件2]
        total_amount = attrs.get('total_amount')
        new_total_amount = 0
        for course in courses:
            new_total_amount += course.price
        if total_amount == new_total_amount:
            return new_total_amount
        raise APIException('價格有誤!!')

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

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

    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  # 後端介面,寫這個介面該訂單狀態

        )
        # return GATEWAY + res
        self.context['pay_url'] = GATEWAY + res

    def _before_create(self, attrs, user, out_trade_no):
        # 剔除courses----》要不要剔除,要pop,但是不在這,在create方法中pop
        # 訂單號,加入到attrs中
        attrs['out_trade_no'] = out_trade_no
        # 把user加入到attrs中
        attrs['user'] = user

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

    # 生成訂單,存訂單表,一定要重寫create,存倆表
    def create(self, validated_data):
        # validated_data:{subject,total_amount,user,out_trade_no,courses}
        courses = validated_data.pop('courses')
        order = Order.objects.create(**validated_data)
        # 存訂單詳情表,存幾條,取決於courses有幾個
        for course in courses:
            OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)

        return order

序列化類中要使用request物件,所以可以將request傳入context上下文,在序列化類使用。

我們還是在全域性勾點裡寫邏輯。
分析我們要使用序列化類做的事情:校驗欄位、反序列化。(不做序列化)
courses不是訂單表的欄位,需要在序列化類重寫。courses是個列表,需要使用ListField。但是還有別的方法:

因為是反序列化多條資料,所以要加many=True

注意我們必須要登入之後才能獲取訂單連結,

我們使用許可權類+認證類來限制登入使用者下單

前端支付頁面


需要攜帶token向後端傳送請求。
資料庫檢視訂單狀態

支付成功後會回撥到前端地址


所以要在前端再寫一個支付成功頁面:

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('您沒有登入,請先登入')
      }
    }

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>

支付成功回撥介面

# 支付成功,支付寶會有倆回撥
	-get 回撥,調前端
    	-為了保證準確性,支付寶回撥會前端後,我們自己向後端傳送一個請求,查詢一下這個訂單是否支付成功
    -post 回撥,調後端介面
    	-後端介面,接受支付寶的回撥,修改訂單狀態
        -這個介面需要登入嗎?不需要任何的認證和許可權
        -如果使用者點了支付----》跳轉到了支付寶頁面---》你的服務掛機了---》會出現什麼情況
        	-支付寶在24小時內,會有8次回撥,
            
            
# 兩個介面:
	-post回撥,給支付寶用
    -get回撥,給我們前端做二次校驗使用

由於我們現在處於內網,所以接收不到回撥資訊

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')  # 都是支付寶要求的

Response的格式需要符合支付寶要求。如果支付寶回撥回不去了(後端崩了),48小時之內支付寶會進行8次回撥,任意一次回撥成功就可以了(給支付寶返回success)。如果8次回撥都沒有收到,還有一個對賬單的功能。
這兩個介面是否需要新增認證?
不能加任何認證和許可權,會導致支付寶無法回撥。加個頻率沒關係。