luffy_08

08支付和訂單

爲了方便開發,和之後項目的維護,咱們再次建立子應用orders來完成接下來的訂單和訂單支付功能。javascript

cd luffy/apps
python ../../manage.py startapp orders

註冊子應用,settings/dev.py,代碼:html

INSTALLED_APPS = [
    # 子應用
    。。。
    
    'orders',
]

訂單模型

from django.db import models

# Create your models here.
from luffy.utils.models import BaseModel
from users.models import User
from courses.models import Course
class Order(BaseModel):
    """訂單記錄"""
    status_choices = (
        (0, '未支付'),
        (1, '已支付'),
        (2, '已取消'),
        (3, '超時取消'),
    )
    pay_choices = (
        (0, '支付寶'),
        (1, '微信支付')
    )
    order_title = models.CharField(max_length=150,verbose_name="訂單標題")
    total_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="訂單總價", default=0)
    real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="實付金額", default=0)
    order_number = models.CharField(max_length=64,verbose_name="訂單號")
    order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="訂單狀態")
    pay_type = models.SmallIntegerField(choices=pay_choices, default=0, verbose_name="支付方式")
    use_coupon = models.BooleanField(default=False,verbose_name="是否使用優惠券")
    coupon = models.IntegerField(null=True, verbose_name="用戶優惠券ID")
    order_desc = models.TextField(max_length=500,null=True,blank=True, verbose_name="訂單描述")
    pay_time = models.DateTimeField(null=True, verbose_name="支付時間")
    user = models.ForeignKey(User, related_name='user_orders', on_delete=models.DO_NOTHING,verbose_name="下單用戶")

    class Meta:
        db_table="ly_order"
        verbose_name= "訂單記錄"
        verbose_name_plural= "訂單記錄"

    def __str__(self):
        return "%s,總價: %s,實付: %s" % (self.order_title, self.total_price, self.real_price)

from courses.models import CourseTime
class OrderDetail(BaseModel):
    """訂單詳情"""
    order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, verbose_name="訂單")
    course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, verbose_name="課程")
    expire = models.IntegerField(default='-1', 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="課程實價")
    discount_name = models.CharField(max_length=120,default="",verbose_name="優惠活動類型")
    class Meta:
        db_table="ly_order_detail"
        verbose_name= "訂單詳情"
        verbose_name_plural= "訂單詳情"

makemigrations && migrate前端

python manage.py makemigrations;
python manage.py migrate;

把當前子應用註冊到xadmin中

在當前子應用下建立adminx.py,代碼:vue

import xadmin

from .models import Order
class OrderModelAdmin(object):
    """訂單模型管理類"""
    pass

xadmin.site.register(Order, OrderModelAdmin)


from .models import OrderDetail
class OrderDetailModelAdmin(object):
    """訂單詳情模型管理類"""
    pass

xadmin.site.register(OrderDetail, OrderDetailModelAdmin)

後端實現生成訂單的api接口

from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from django_redis import get_redis_connection
from rest_framework.response import Response
from .models import Order,OrderDetail
from datetime import datetime
from courses.models import Course, CourseTime
import random
from django.db import transaction
from rest_framework import status

import logging
log = logging.getLogger("django")

class OrderAPIView(APIView):
    permission_classes = [IsAuthenticated]
    def post(self,request):
        """生成訂單"""
        # 獲取用戶ID
        # user_id = 1
        user_id = request.user.id

        # 訂單號,必須保證惟一
        order_number =  datetime.now().strftime("%Y%m%d%H%M%S") + "%08d" % user_id + "%04d" % random.randint(0,9999)

        with transaction.atomic():

            # 數據庫事務的回滾標記
            save_id = transaction.savepoint()

            # 生成空的訂單
            try:
                order = Order.objects.create(
                    order_title="路飛學城課程購買",
                    total_price=0,
                    real_price=0,
                    order_number= order_number,
                    user_id=user_id,
                )

                # 到redis獲取購物車信息
                redis = get_redis_connection("cart")
                # 勾選狀態
                course_selects_set = redis.smembers("cart_selected_%s" % user_id )
                print( course_selects_set )
                # 購物車中商品課程列表
                cart_course_list = redis.hgetall("cart_%s" % user_id )
                print( cart_course_list )
                # 經過購物車信息到數據中提取相關數據
                # 計算訂單總價
                total_price = 0

                # 開啓redis的管道操做[事務操做]
                pipeline = redis.pipeline()
                pipeline.multi()

                for course_id_byte,expire_byte in cart_course_list.items():
                    if course_id_byte in course_selects_set:
                        expire = expire_byte.decode()
                        course_id = course_id_byte.decode()
                        course = Course.objects.get(pk=course_id)
                        if expire == '-1':
                            """永久有效"""
                            course_price = course.get_course_price()

                        else:
                            """有購買週期"""
                            coursetime = CourseTime.objects.get(course=course_id,timer=expire)
                            course_price = coursetime.course.get_course_price(coursetime.price)

                        total_price += course_price

                        # 生成訂單詳情
                        OrderDetail.objects.create(
                            order_id=order.id,
                            course_id=course_id,
                            expire=expire,
                            price=course.price if expire=='-1' else coursetime.price,
                            real_price=course_price,
                            discount_name=course.get_course_discount_type()
                        )

                        # 從購物車中移除已經加入訂單的商品
                        pipeline.srem("cart_selected_%s" % user_id, course_id )
                        pipeline.hdel("cart_%s" % user_id, course_id )

                # 提交redis的事務操做
                pipeline.execute()
                # 補充訂單的總價格
                order.total_price=total_price
                order.real_price=total_price
                order.save()

            except Exception:
                # 記錄錯誤日誌
                log.error( "%s" % Exception )
                # 回滾事務
                transaction.savepoint_rollback(save_id)
                # 響應結果
                return Response({"message":"系統異常!"},status=status.HTTP_507_INSUFFICIENT_STORAGE)

        # 響應結果
        return Response({"message":"成功生成訂單!","order":order.order_number})

上面咱們使用了redis的事務操做保證數據的一致性。可是mysql裏面咱們也是在進行多表操做,因此也是須要使用事務來保證數據的一致性的。java

事務有四大特性:python

原子性(Atomicity)
一致性(Consistency)
隔離性(Isolation)[事務隔離級別->幻讀,髒讀]
持久性(Durability)

django框架自己就提供了2種事務操做的用法。mysql

django的事務操做方法主要經過 django.db.transation模塊完成的。ios

啓用事務用法1:git

from django.db import transaction
from rest_framework.views import APIView
class OrderAPIView(APIView):
    @transaction.atomic          # 開啓事務,當方法執行完成之後,自動提交事務
    def post(self,request):
        ....

啓用事務用法2:程序員

from django.db import transaction
from rest_framework.views import APIView
class OrderAPIView(APIView):
    def post(self,request):
        ....
        with transation.atomic(): # 開啓事務,當with語句執行完成之後,自動提交事務
            # 數據庫操做

在使用事務過程當中, 有時候會出現異常,當出現異常的時候,咱們須要讓程序中止下來,同時須要回滾事務。

from django.db import transaction
from rest_framework.views import APIView
class OrderAPIView(APIView):
    def post(self,request):
        ....
        with transation.atomic():
            # 設置事務回滾的標記點
            sid = transation.savepoint()

            ....

            try:
                ....
            except:
                transation.savepoint_rallback(sid)

使用django提供的mysql事務操做保證下單過程當中的數據一致性

視圖代碼:

from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from django_redis import get_redis_connection
from rest_framework.response import Response
from .models import Order,OrderDetail
from datetime import datetime
from courses.models import Course, CourseTime
import random
from django.db import transaction
from rest_framework import status

import logging
log = logging.getLogger("django")

class OrderAPIView(APIView):
    permission_classes = [IsAuthenticated]
    def post(self,request):
        """生成訂單"""
        # 獲取用戶ID
        # user_id = 1
        user_id = request.user.id

        # 訂單號,必須保證惟一
        order_number =  datetime.now().strftime("%Y%m%d%H%M%S") + "%08d" % user_id + "%04d" % random.randint(0,9999)

        with transaction.atomic():

            # 數據庫事務的回滾標記
            save_id = transaction.savepoint()

            # 生成空的訂單
            try:
                order = Order.objects.create(
                    order_title="路飛學城課程購買",
                    total_price=0,
                    real_price=0,
                    order_number= order_number,
                    user_id=user_id,
                )

                # 到redis獲取購物車信息
                redis = get_redis_connection("cart")
                # 勾選狀態
                course_selects_set = redis.smembers("cart_selected_%s" % user_id )
                print( course_selects_set )
                # 購物車中商品課程列表
                cart_course_list = redis.hgetall("cart_%s" % user_id )
                print( cart_course_list )
                # 經過購物車信息到數據中提取相關數據
                # 計算訂單總價
                total_price = 0

                # 開啓redis的管道操做[事務操做]
                pipeline = redis.pipeline()
                pipeline.multi()

                for course_id_byte,expire_byte in cart_course_list.items():
                    if course_id_byte in course_selects_set:
                        expire = expire_byte.decode()
                        course_id = course_id_byte.decode()
                        course = Course.objects.get(pk=course_id)
                        if expire == '-1':
                            """永久有效"""
                            course_price = course.get_course_price()

                        else:
                            """有購買週期"""
                            coursetime = CourseTime.objects.get(course=course_id,timer=expire)
                            course_price = coursetime.course.get_course_price(coursetime.price)

                        total_price += course_price

                        # 生成訂單詳情
                        OrderDetail.objects.create(
                            order_id=order.id,
                            course_id=course_id,
                            expire=expire,
                            price=course.price if expire=='-1' else coursetime.price,
                            real_price=course_price,
                            discount_name=course.get_course_discount_type()
                        )

                        # 從購物車中移除已經加入訂單的商品
                        pipeline.srem("cart_selected_%s" % user_id, course_id )
                        pipeline.hdel("cart_%s" % user_id, course_id )

                # 提交redis的事務操做
                pipeline.execute()
                # 補充訂單的總價格
                order.total_price=total_price
                order.real_price=total_price
                order.save()

            except Exception:
                # 記錄錯誤日誌
                log.error( "%s" % Exception )
                # 回滾事務
                transaction.savepoint_rollback(save_id)
                # 響應結果
                return Response({"message":"系統異常!"},status=status.HTTP_507_INSUFFICIENT_STORAGE)

        # 響應結果
        return Response({"message":"成功生成訂單!","order":order.order_number})

change the utils/models.py

from django.db import models


class BaseModel(models.Model):
    """公共字段模型"""
    orders = models.IntegerField(verbose_name='顯示順序', null=True, blank=True)
    is_show = models.BooleanField(verbose_name="是否上架", default=False)
    is_delete = models.BooleanField(verbose_name="邏輯刪除", default=False)
    create_time = models.DateTimeField(auto_now_add=True, verbose_name="添加時間")
    update_time = models.DateTimeField(auto_now=True, verbose_name="更新時間")

    class Meta:
        # 設置當前模型在數據遷移的時候不要爲它建立表
        abstract = True

the main urls.py

from django.contrib import admin
from django.urls import path, re_path, include
import xadmin
from xadmin.plugins import xversion
from django.views.static import serve
from django.conf import settings

xadmin.autodiscover()
xversion.register_models()  # version模塊自動註冊須要版本控制的 Model

urlpatterns = [
    ......
    path('orders/', include("orders.urls")),
]

orders/urls.py

from django.urls import path, re_path
from . import views


urlpatterns = [
    path("", views.OrderAPIView.as_view()),
]

前端請求生成訂單

<template>
  。。。
            <span class="go-pay" @click="gotopay">去結算</span>
  。。。
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
    name: "Cart",
    。。。。
    methods:{
      。。。。
      gotopay(){
        // 提交結算,生成訂單
        this.$axios.post(this.$settings.Host+"/orders/",{},{
            headers:{
              // 注意:jwt後面必須有且只有一個空格!!!!
              "Authorization":"jwt " + this.token
            }
        }).then(response=>{
          let _this = this;
          this.$alert(response.data.message,"提示",{
            callback(){
              _this.$router.push(`orders/${response.data.order}`);
            }
          })
        }).catch(error=>{
          console.log(error.response)
        })
      }
    },
    components:{Header,Footer}
}
</script>

create a Order.vue

<template>
    
</template>

<script>
    export default {
        name: "Order"
    }
</script>

<style scoped>

</style>

router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Home from '../components/Home'
import Login from "../components/Login";
import Register from "../components/Register";
import Course from "../components/Course";
import Detail from "../components/Detail";
import Player from "../components/Player";
import Cart from "../components/Cart";
import Order from "../components/Order";

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
      ...
    {
      name: "Order",
      path: "/orders/:order",  // 地址欄的命名綁定參數, 在組件中能夠經過 this.$router.param.order能夠獲取數據
      component: Order,
    }
  ]
})

顯示結算頁面

Order.vue

<template>
  <div class="cart">
    <Header/>
    <div class="cart-info">
        <h3 class="cart-top">購物車結算 <span>共1門課程</span></h3>
        <div class="cart-title">
           <el-row>
             <el-col :span="2">&nbsp;</el-col>
             <el-col :span="10">課程</el-col>
             <el-col :span="8">有效期</el-col>
             <el-col :span="4">價格</el-col>
           </el-row>
        </div>
          <div class="cart-item" v-for="item in course_list">
          <el-row>
             <el-col :span="2" class="checkbox">&nbsp;&nbsp;</el-col>
             <el-col :span="10" class="course-info">
               <img :src="item.course.course_http_img" alt="">
                <span>{{item.course.name}}</span>
             </el-col>
             <el-col :span="8"><span>永久有效</span></el-col>
             <el-col :span="4" class="course-price">¥{{item.unit_price}}</el-col>
           </el-row>
        </div>
        <div class="calc">
            <el-row class="pay-row">
              <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col>
              <el-col :span="8">
                <span class="alipay"><img src="../../static/img/alipay2.png" alt=""></span>
                <span class="alipay wechat"><img src="../../static/img/wechat.png" alt=""></span>
              </el-col>
              <el-col :span="8" class="count">實付款: <span>¥{{total}}</span></el-col>
              <el-col :span="4" class="cart-pay"><span @click="payhander">支付寶支付</span></el-col>
            </el-row>
        </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
  import Header from "./common/Header"
  import Footer from "./common/Footer"
  export default {
    name:"Order",
    data(){
      return {
        token: localStorage.token || sessionStorage.token,
        id: localStorage.id || sessionStorage.id,
        order_id:sessionStorage.order_id || null,
        course_list:[],
        total:0,
      }
    },
    components:{
      Header,
      Footer,
    },
    created(){

    },
    methods: {
      payhander(){

      }
    }
  }
</script>

<style scoped>
.cart{
  margin-top: 80px;
}
.cart-info{
  overflow: hidden;
  width: 1200px;
  margin: auto;
}
.cart-top{
  font-size: 18px;
  color: #666;
  margin: 25px 0;
  font-weight: normal;
}
.cart-top span{
    font-size: 12px;
    color: #d0d0d0;
    display: inline-block;
}
.cart-title{
    background: #F7F7F7;
    height: 70px;
}
.calc{
  margin-top: 25px;
  margin-bottom: 40px;
}

.calc .count{
  text-align: right;
  margin-right: 10px;
  vertical-align: middle;
}
.calc .count span{
    font-size: 36px;
    color: #333;
}
.calc .cart-pay{
    margin-top: 5px;
    width: 110px;
    height: 38px;
    outline: none;
    border: none;
    color: #fff;
    line-height: 38px;
    background: #ffc210;
    border-radius: 4px;
    font-size: 16px;
    text-align: center;
    cursor: pointer;
}
.cart-item{
  height: 120px;
  line-height: 120px;
}
.course-info img{
    width: 175px;
    height: 115px;
    margin-right: 35px;
    vertical-align: middle;
}
.alipay{
  display: inline-block;
  height: 48px;
}
.alipay img{
  height: 100%;
  width:auto;
}

.pay-text{
  display: block;
  text-align: right;
  height: 100%;
  line-height: 100%;
  vertical-align: middle;
  margin-top: 20px;
}
</style>

後端提供訂單數據的api

orders/serializers.py,序列化器,代碼:

from rest_framework import serializers
from .models import Order,OrderDetail

class OrderCourseSerializer(serializers.ModelSerializer):
    class Meta:
        model = OrderDetail
        fields = ("course_name","expire_text","price","real_price","discount_name","course_img")

class OrderDetailModelSerializer(serializers.ModelSerializer):
    order_courses = OrderCourseSerializer(many=True)
    class Meta:
        model = Order
        fields = ("id", "total_price", "real_price","pay_type","use_coupon","coupon","order_courses")

模型中新增返回課程圖片的字段,代碼:

from courses.models import CourseTime
class OrderDetail(BaseModel):
    """訂單詳情"""
    order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, verbose_name="訂單")
    course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, verbose_name="課程")
    expire = models.IntegerField(default='-1', 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="課程實價")
    discount_name = models.CharField(max_length=120,default="",verbose_name="優惠活動類型")
    class Meta:
        db_table="ly_order_detail"
        verbose_name= "訂單詳情"
        verbose_name_plural= "訂單詳情"

    def course_img(self):
        # 返回圖片的url地址
        return self.course.course_img.url

    def expire_text(self):
        if self.expire == -1:
            return "永久有效"
        else:
            print( self.expire )
            coursetime = CourseTime.objects.get(timer=self.expire,course=self.course)
            return coursetime.title

    def course_name(self):
        return self.course.name

視圖代碼:

from .serializers import OrderDetailModelSerializer
class OrderDetailAPIView(APIView):
    permission_classes = [IsAuthenticated]
    def get(self,request,pk):
        try:
            order = Order.objects.get(order_number=pk)
        except Order.DoesNotExist:
            return Response({"message":"訂單信息有誤!"},status=status.HTTP_400_BAD_REQUEST)

        serializer = OrderDetailModelSerializer(instance=order)

        return Response(serializer.data, status=status.HTTP_200_OK)

路由代碼:

re_path(r"(?P<pk>\d+)/",views.OrderDetailAPIView.as_view()),

前端請求後端的訂單信息

<template>
  <div class="cart">
    <Header/>
    <div class="cart-info">
        <h3 class="cart-top">購物車結算 <span>共1門課程</span></h3>
        <div class="cart-title">
           <el-row>
             <el-col :span="2">&nbsp;</el-col>
             <el-col :span="10">課程</el-col>
             <el-col :span="8">有效期</el-col>
             <el-col :span="4">價格</el-col>
           </el-row>
        </div>
        <div class="cart-item" v-for="course in order_info.order_courses">
          <el-row>
             <el-col :span="2" class="checkbox">&nbsp;&nbsp;</el-col>
             <el-col :span="10" class="course-info">
               <img style="float: left;" :src="$settings.Host+course.course_img" alt="">
               <span class="course_name">
                 <span>{{course.course_name}}</span><br>
                 <span class="discount_name">{{course.discount_name}}</span>
               </span>
             </el-col>
             <el-col :span="8" class="lh"><span>{{course.expire_text}}</span></el-col>
             <el-col :span="4">
               <div class="course-price">
                  <p class="real_price">¥{{course.real_price}}</p>
                  <span class="original_price">原價: ¥{{course.price}}</span>
               </div>
             </el-col>
           </el-row>
        </div>
        <div>
        <div class="coupon">
          <div id="accordion">
            <div class="coupon-box">
              <div class="coupon-title">
                <span class="select-coupon">使用優惠劵:</span>
                <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" style="width: 20px; height: 20px" class="collapsed" aria-expanded="false">
                  <img class="sign" src="../../static/img/12.png" width="20" height="20" alt=""></a>
                  <span class="coupon-num">有0張可用</span>
              </div>
              <p class="sum-price-wrap" style="margin-right: 45px">商品總金額:<span class="sum-price">{{order_info.total_price}}元</span></p>
            </div>
            <div style="text-align: left; height: 0px;" id="collapseOne" class="panel-collapse out collapse" aria-expanded="false">
              <ul class="coupon-list" style="display: none;">

              </ul>
              <div style="text-align: center; width: 100%; padding: 50px 0px; align-items: center; justify-content: center; border-bottom: 1px solid rgb(232, 232, 232);">
                <span style="font-size: 16px; color: #9b9b9b">暫無可用優惠券</span>
              </div>
            </div>
          </div>
          <div style="height: 30px; margin-top: 40px; display: flex; align-items: center; justify-content: flex-end">
            <input type="checkbox" class="ok" id="color-input-red">
            <label for="color-input-red"><img src=""alt=""></label>
            <p class="discount-num" style="color:#9B9B9B">使用個人貝里</p>
            <p class="discount-num" style="margin-right: 45px">
              <span style="display: none;">可用0個已抵扣 ¥0</span>
            </p>
          </div>
          <p class="sun-coupon-num" style="margin-right: 45px;margin-bottom:43px">優惠券抵扣:<span>0元</span></p>
        </div>
        </div>
        <div class="calc">
            <el-row class="pay-row">
              <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col>
              <el-col :span="8">
                <span class="alipay"><img src="../../static/img/alipay2.png" alt=""></span>
                <span class="alipay wechat"><img src="../../static/img/wechat.png" alt=""></span>
              </el-col>
              <el-col :span="8" class="count">實付款: <span>¥{{order_info.total_price}}</span></el-col>
              <el-col :span="4" class="cart-pay"><span @click="payhander">支付寶支付</span></el-col>
            </el-row>
        </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
  import Header from "./common/Header"
  import Footer from "./common/Footer"
  export default {
    name:"Order",
    data(){
      return {
        order_info:{},
      }
    },
    components:{
      Header,
      Footer,
    },
    created(){
      // 判斷用戶是否已經登陸
      let token = sessionStorage.token || localStorage.token;
      let _this = this;
      if(!token){
        this.$alert("對不起,您還沒有登陸!請登陸!","警告",{
          callback(){
            _this.$router.push("/login");
          }
        })
      }
      // 獲取地址欄上面的訂單號
      let order_number = this.$route.params.order;

      // 發送請求獲取數據
      this.$axios.get(this.$settings.Host+`/orders/${order_number}/`,{
          headers:{
            // 注意下方的空格!!!
            "Authorization":"jwt " + token,
          },
      }).then(response=>{
        this.order_info = response.data;
      }).catch(error=>{
        console.log(error.response);
      })
    },
    methods: {
      payhander(){

      }
    }
  }
</script>

<style scoped>
.cart{
  margin-top: 80px;
}
.cart-info{
  overflow: hidden;
  width: 1200px;
  margin: auto;
}
.cart-top{
  font-size: 18px;
  color: #666;
  margin: 25px 0;
  font-weight: normal;
}
.cart-top span{
    font-size: 12px;
    color: #d0d0d0;
    display: inline-block;
}
.cart-title{
    background: #F7F7F7;
    height: 70px;
}
.calc{
  margin-top: 25px;
  margin-bottom: 40px;
}

.calc .count{
  text-align: right;
  margin-right: 10px;
  vertical-align: middle;
}
.calc .count span{
    font-size: 36px;
    color: #333;
}
.calc .cart-pay{
    margin-top: 5px;
    width: 110px;
    height: 38px;
    outline: none;
    border: none;
    color: #fff;
    line-height: 38px;
    background: #ffc210;
    border-radius: 4px;
    font-size: 16px;
    text-align: center;
    cursor: pointer;
}
.cart-item{
  height: 120px;
  /*line-height: 120px;*/
}
.cart-item .lh{
  line-height: 120px;
}
.course-info img{
    width: 175px;
    height: 115px;
    margin-right: 35px;
    vertical-align: middle;
}
.course-price{
  margin-top: 40px;
}
.alipay{
  display: inline-block;
  height: 48px;
}
.alipay img{
  height: 100%;
  width:auto;
}

.pay-text{
  display: block;
  text-align: right;
  height: 100%;
  line-height: 100%;
  vertical-align: middle;
  margin-top: 20px;
}
.real_price{
    color: #333;
    margin-bottom: 10px;
}
.original_price{
    color: #9b9b9b;
    letter-spacing: .36px;
    text-decoration: line-through;
}
.coupon{
  margin-top: 30px;
}
.coupon-box{
  text-align: left;
  display: flex;
  padding-bottom: 22px;
  padding-left:30px;
  border-bottom: 1px solid #e8e8e8;
}
.coupon-title{
  display: flex;
}
.sum-price-wrap{
    display: inline-block;
    margin-left: auto;
    font-size: 16px;
    color: #4a4a4a;
}
.discount_name{
    color: #ffc210;
    margin-top: 24px;
    font-size: 14px;
    letter-spacing: .32px;
}
.course_name{
  margin-top: 40px;
  display: block;
}
</style>

優惠券

建立一個coupon子應用.

cd luffy/apps
python ../../manage.py startapp coupon

註冊子應用

INSTALLED_APPS = [
 

    # 子應用
    。。。
    'coupon',
]

模型分析:

1558574562894

代碼:

from django.db import models
from luffy.utils.models import BaseModel
# Create your models here.
class Coupon(BaseModel):
    """優惠券"""
    coupon_choices = (
        (0, '折扣優惠'),
        (1, '減免優惠')
    )
    name = models.CharField(max_length=32, verbose_name="優惠券標題")
    coupon_type = models.SmallIntegerField(choices=coupon_choices, default=0, verbose_name="優惠券類型")
    timer = models.IntegerField(verbose_name="優惠券有效期", default=30, help_text="")
    condition = models.IntegerField(blank=True, default=0, verbose_name="知足使用優惠券的價格條件")
    sale = models.TextField(verbose_name="優惠公式", help_text="""
        *號開頭表示折扣價,例如*0.82表示八二折;<br>
        -號開頭表示減免價,例如-10表示在總價基礎上減免10元<br>    
        """)

    class Meta:
        db_table = "ly_coupon"
        verbose_name="優惠券"
        verbose_name_plural="優惠券"

    def __str__(self):
        return "%s" % (self.name)

from users.models import User
class UserCoupon(BaseModel):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="coupons", verbose_name="用戶")
    coupon = models.ForeignKey(Coupon, on_delete=models.CASCADE, related_name="users", verbose_name="優惠券")
    start_time = models.DateTimeField(verbose_name="優惠策略的開始時間")
    is_use = models.BooleanField(default=False,verbose_name="優惠券是否使用過")

    class Meta:
        db_table = "ly_user_coupon"
        verbose_name = "用戶的優惠券"
        verbose_name_plural = "用戶的優惠券"

    def __str__(self):
        return "優惠券:%s,用戶:%s" % (self.coupon.name, self.user.username)

數據遷移

cd ../../
python manage.py makemigrations
python manage.py migrate

註冊到xadmin,添加測試數據[1.添加優惠券,給用戶發放優惠券]

import xadmin
from .models import Coupon
class CouponModelAdmin(object):
    """優惠券模型管理類"""
    list_display = ["name","coupon_type","timer"]
xadmin.site.register(Coupon, CouponModelAdmin)



from .models import UserCoupon
class UserCouponModelAdmin(object):
    """個人優惠券模型管理類"""
    list_display = ["user","coupon","start_time","is_use"]

xadmin.site.register(UserCoupon, UserCouponModelAdmin)

1558574889770

在訂單頁面查詢當前用戶擁有的優惠券

前端編寫優惠券的樣式代碼:

<template>
  <div class="cart">
    <Header/>
    <div class="cart-info">
        <h3 class="cart-top">購物車結算 <span>共1門課程</span></h3>
        <div class="cart-title">
           <el-row>
             <el-col :span="2">&nbsp;</el-col>
             <el-col :span="10">課程</el-col>
             <el-col :span="8">有效期</el-col>
             <el-col :span="4">價格</el-col>
           </el-row>
        </div>
        <div class="cart-item" v-for="course in order_info.order_courses">
          <el-row>
             <el-col :span="2" class="checkbox">&nbsp;&nbsp;</el-col>
             <el-col :span="10" class="course-info">
               <img style="float: left;" :src="$settings.Host+course.course_img" alt="">
               <span class="course_name">
                 <span>{{course.course_name}}</span><br>
                 <span class="discount_name">{{course.discount_name}}</span>
               </span>
             </el-col>
             <el-col :span="8" class="lh"><span>{{course.expire_text}}</span></el-col>
             <el-col :span="4">
               <div class="course-price">
                  <p class="real_price">¥{{course.real_price}}</p>
                  <span class="original_price">原價: ¥{{course.price}}</span>
               </div>
             </el-col>
           </el-row>
        </div>
        <div>
        <div class="coupon">
          <div id="accordion">
            <div class="coupon-box">
              <div class="coupon-title">
                <span class="select-coupon">使用優惠劵:</span>
                <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" style="width: 20px; height: 20px" class="collapsed" aria-expanded="false">
                  <img class="sign" src="../../static/img/12.png" width="20" height="20" alt=""></a>
                  <span class="coupon-num">有0張可用</span>
              </div>
              <p class="sum-price-wrap" style="margin-right: 45px">商品總金額:<span class="sum-price">{{order_info.total_price}}元</span></p>
            </div>
            <div style="text-align: left;" id="collapseOne" class="panel-collapse out collapse" aria-expanded="false">
              <ul class="coupon-list" style="">
                <li class="coupon-item">
                  <span>優惠券</span>
                  <span>¥10元</span>
                  <span>開始使用時間: 2019-10-01</span>
                </li>
                <li class="coupon-item">
                  <span>優惠券</span>
                  <span>¥10元</span>
                  <span>開始使用時間: 2019-10-01</span>
                </li>
              </ul>
              <div style="text-align: center; width: 100%; padding: 50px 0px; align-items: center; justify-content: center; border-bottom: 1px solid rgb(232, 232, 232);">
                <span style="font-size: 16px; color: #9b9b9b">暫無可用優惠券</span>
              </div>
            </div>
          </div>
          <div style="height: 30px; margin-top: 40px; display: flex; align-items: center; justify-content: flex-end">
            <input type="checkbox" class="ok" id="color-input-red">
            <label for="color-input-red"><img src="" alt=""></label>
            <p class="discount-num" style="color:#9B9B9B">使用個人貝里</p>
            <p class="discount-num" style="margin-right: 45px">
              <span style="display: none;">可用0個已抵扣 ¥0</span>
            </p>
          </div>
          <p class="sun-coupon-num" style="margin-right: 45px;margin-bottom:43px">優惠券抵扣:<span>0元</span></p>
        </div>
        </div>
        <div class="calc">
            <el-row class="pay-row">
              <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col>
              <el-col :span="8">
                <span class="alipay"><img src="../../static/img/alipay2.png" alt=""></span>
                <span class="alipay wechat"><img src="../../static/img/wechat.png" alt=""></span>
              </el-col>
              <el-col :span="8" class="count">實付款: <span>¥{{order_info.total_price}}</span></el-col>
              <el-col :span="4" class="cart-pay"><span @click="payhander">支付寶支付</span></el-col>
            </el-row>
        </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
  。。。。
</script>

<style scoped>
。。。
    
.coupon-list{
  overflow: hidden;
}
.coupon-item{
  float: left;
  margin-left: 10px;
  margin-right: 10px;
  width: 200px;
  height: 120px;
  border: 1px solid #000;
  padding: 10px;
}
.coupon-item span{
  display: block;
  font-size: 14px;
}
</style>

後端提供查詢當前用戶擁有的優惠券api接口

序列化器,代碼:

from rest_framework import serializers
from .models import Coupon, UserCoupon
class CouponModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = Coupon
        fields = ("name","coupon_type","timer","condition","sale")


class UserCouponModelSerializer(serializers.ModelSerializer):
    coupon = CouponModelSerializer()
    class Meta:
        model = UserCoupon
        fields = ("id","start_time","coupon")

視圖,代碼:

from rest_framework.generics import ListAPIView
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from .models import UserCoupon
from .serializers import UserCouponModelSerializer
class UserCouponAPIVew(ListAPIView):
    """個人優惠券"""
    queryset = UserCoupon.objects.filter(is_show=True,is_delete=False,is_use=False)
    serializer_class = UserCouponModelSerializer
    permission_classes = [IsAuthenticated]
    filter_backends = [DjangoFilterBackend]
    ordering_fields = ('user_id',)

子應用路由,代碼:

from django.urls import path
from . import views
urlpatterns = [
    path(r"list/",views.UserCouponAPIVew.as_view()),
]

總路由,代碼:

path('coupon/', include("coupon.urls")),

前端展現當前用戶擁有的優惠券並勾選使用優惠券之後,會自動調整訂單實付價格

代碼:

<template>
  <div class="cart">
    <Header/>
    <div class="cart-info">
        <h3 class="cart-top">購物車結算 <span>共1門課程</span></h3>
        <div class="cart-title">
           <el-row>
             <el-col :span="2">&nbsp;</el-col>
             <el-col :span="10">課程</el-col>
             <el-col :span="8">有效期</el-col>
             <el-col :span="4">價格</el-col>
           </el-row>
        </div>
        <div class="cart-item" v-for="course in order_info.order_courses">
          <el-row>
             <el-col :span="2" class="checkbox">&nbsp;&nbsp;</el-col>
             <el-col :span="10" class="course-info">
               <img style="float: left;" :src="$settings.Host+course.course_img" alt="">
               <span class="course_name">
                 <span>{{course.course_name}}</span><br>
                 <span class="discount_name">{{course.discount_name}}</span>
               </span>
             </el-col>
             <el-col :span="8" class="lh"><span>{{course.expire_text}}</span></el-col>
             <el-col :span="4">
               <div class="course-price">
                  <p class="real_price">¥{{course.real_price}}</p>
                  <span class="original_price">原價: ¥{{course.price}}</span>
               </div>
             </el-col>
           </el-row>
        </div>
        <div>
        <div class="coupon">
          <div id="accordion">
            <div class="coupon-box">
              <div class="coupon-title">
                <span class="select-coupon">使用優惠劵:</span>
                <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" style="width: 20px; height: 20px" class="collapsed" aria-expanded="false">
                  <img class="sign" src="../../static/img/12.png" width="20" height="20" alt=""></a>
                  <span class="coupon-num">有0張可用</span>
              </div>
              <p class="sum-price-wrap" style="margin-right: 45px">商品總金額:<span class="sum-price">{{order_info.total_price}}元</span></p>
            </div>
            <div style="text-align: left;" id="collapseOne" class="panel-collapse out collapse" aria-expanded="false">
              <ul class="coupon-list" v-if="coupon_list.length>0">
                <li @click="use_coupon=item.id" v-for="item in coupon_list" class="coupon-item" :class="use_coupon==item.id?'coupon_selected':''">
                  <span>{{item.coupon.name}}</span>
                  <span v-if="item.coupon.type==1">¥{{item.coupon.sale}}元</span>
                  <span style="font-size: 12px;">開始使用時間: {{new Date(item.start_time).toLocaleString()}}</span>
                </li>
              </ul>
              <div v-else style="text-align: center; width: 100%; padding: 50px 0px; align-items: center; justify-content: center; border-bottom: 1px solid rgb(232, 232, 232);">
                <span style="font-size: 16px; color: #9b9b9b">暫無可用優惠券</span>
              </div>
            </div>
          </div>
          <div style="height: 30px; margin-top: 40px; display: flex; align-items: center; justify-content: flex-end">
            <input type="checkbox" class="ok" id="color-input-red">
            <label for="color-input-red"><img src="" alt=""></label>
            <p class="discount-num" style="color:#9B9B9B">使用個人貝里</p>
            <p class="discount-num" style="margin-right: 45px">
              <span style="display: none;">可用0個已抵扣 ¥0</span>
            </p>
          </div>
          <p class="sun-coupon-num" style="margin-right: 45px;margin-bottom:43px">優惠券抵扣:<span>0元</span></p>
        </div>
        </div>
        <div class="calc">
            <el-row class="pay-row">
              <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col>
              <el-col :span="8">
                <span class="alipay"><img src="../../static/img/alipay2.png" alt=""></span>
                <span class="alipay wechat"><img src="../../static/img/wechat.png" alt=""></span>
              </el-col>
              <el-col :span="8" class="count">實付款: <span>¥{{order_info.real_price}}</span></el-col>
              <el-col :span="4" class="cart-pay"><span @click="payhander">支付寶支付</span></el-col>
            </el-row>
        </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
  import Header from "./common/Header"
  import Footer from "./common/Footer"
  export default {
    name:"Order",
    data(){
      return {
        use_coupon: 0,
        order_info:{},
        coupon_list:[], // 用於展現優惠券列表
        coupon_list2:[],// 用於勾選優惠券功能
      }
    },
    components:{
      Header,
      Footer,
    },
    watch:{
      use_coupon(coupon_id){
        let data = this.coupon_list2[ coupon_id ];
        let sale = parseFloat( data.coupon.sale.slice(1) );
        if( data.coupon.coupon_type == 0 ){
          // 折扣優惠券
          this.order_info.real_price = this.order_info.total_price * sale;
        }else if(data.coupon.coupon_type == 1){
          // 減免優惠券
          this.order_info.real_price = this.order_info.total_price - sale;
        }
        this.order_info.real_price = this.order_info.real_price.toFixed(2);

        // 發送數據到後端,進行同步
      }
    },
    created(){
      // 判斷用戶是否已經登陸
      let token = sessionStorage.token || localStorage.token;
      let _this = this;
      if(!token){
        this.$alert("對不起,您還沒有登陸!請登陸!","警告",{
          callback(){
            _this.$router.push("/login");
          }
        })
      }
      // 獲取地址欄上面的訂單號
      let order_number = this.$route.params.order;

      // 發送請求獲取數據
      this.$axios.get(this.$settings.Host+`/orders/${order_number}/`,{
          headers:{
            // 注意下方的空格!!!
            "Authorization":"jwt " + token,
          },
      }).then(response=>{
        this.order_info = response.data;
      }).catch(error=>{
        console.log(error.response);
      });

      // 獲取用戶的優惠券
      this.get_coupon_list()
    },
    methods: {
      get_coupon_list(){
        let user_id = localStorage.user_id || sessionStorage.user_id;
        let token = localStorage.token || sessionStorage.token;
        // 獲取當前用戶的優惠券
        this.$axios.get(this.$settings.Host+"/coupon/list/",{
          params:{
            user_id,
          },
          headers:{
            // 注意下方的空格!!!
            "Authorization":"jwt " + token,
          },
        }).then(response=>{
          // 調整獲取到優惠券列表,以優惠券ID做爲下標
          let data_list = [];
          response.data.forEach(row=>{
            data_list[row.id] = row;
          });
          this.coupon_list2= data_list;
          this.coupon_list = response.data;
        }).catch(error=>{
          console.log(error.response);
        })
      },
      payhander(){

      },

    }
  }
</script>

<style scoped>
.cart{
  margin-top: 80px;
}
.cart-info{
  overflow: hidden;
  width: 1200px;
  margin: auto;
}
.cart-top{
  font-size: 18px;
  color: #666;
  margin: 25px 0;
  font-weight: normal;
}
.cart-top span{
    font-size: 12px;
    color: #d0d0d0;
    display: inline-block;
}
.cart-title{
    background: #F7F7F7;
    height: 70px;
}
.calc{
  margin-top: 25px;
  margin-bottom: 40px;
}

.calc .count{
  text-align: right;
  margin-right: 10px;
  vertical-align: middle;
}
.calc .count span{
    font-size: 36px;
    color: #333;
}
.calc .cart-pay{
    margin-top: 5px;
    width: 110px;
    height: 38px;
    outline: none;
    border: none;
    color: #fff;
    line-height: 38px;
    background: #ffc210;
    border-radius: 4px;
    font-size: 16px;
    text-align: center;
    cursor: pointer;
}
.cart-item{
  height: 120px;
  /*line-height: 120px;*/
}
.cart-item .lh{
  line-height: 120px;
}
.course-info img{
    width: 175px;
    height: 115px;
    margin-right: 35px;
    vertical-align: middle;
}
.course-price{
  margin-top: 40px;
}
.alipay{
  display: inline-block;
  height: 48px;
}
.alipay img{
  height: 100%;
  width:auto;
}

.pay-text{
  display: block;
  text-align: right;
  height: 100%;
  line-height: 100%;
  vertical-align: middle;
  margin-top: 20px;
}
.real_price{
    color: #333;
    margin-bottom: 10px;
}
.original_price{
    color: #9b9b9b;
    letter-spacing: .36px;
    text-decoration: line-through;
}
.coupon{
  margin-top: 30px;
}
.coupon-box{
  text-align: left;
  display: flex;
  padding-bottom: 22px;
  padding-left:30px;
  border-bottom: 1px solid #e8e8e8;
}
.coupon-title{
  display: flex;
}
.sum-price-wrap{
    display: inline-block;
    margin-left: auto;
    font-size: 16px;
    color: #4a4a4a;
}
.discount_name{
    color: #ffc210;
    margin-top: 24px;
    font-size: 14px;
    letter-spacing: .32px;
}
.course_name{
  margin-top: 40px;
  display: block;
}
.coupon-list{
  overflow: hidden;
}
.coupon-item{
  float: left;
  margin-left: 10px;
  margin-right: 10px;
  width: 200px;
  height: 60px;
  border: 1px solid #000;
  padding: 10px;
}
.coupon-item span{
  display: block;
  font-size: 14px;
}
.coupon_selected{
  border-color: red;
  color: indianred;
}
</style>

發起支付-支付寶

支付寶開發平臺登陸

https://open.alipay.com/platform/home.htm

1554189376336

1554189403316

1554189677144

沙箱環境

  • 是支付寶提供給開發者的模擬支付的環境

  • 沙箱環境跟真實環境是分開的,項目上線時必須切換對應的配置服務器地址和開發者ID和密鑰。

  • 沙箱應用開發文檔:<https://docs.open.alipay.com

  • 沙箱帳號https://openhome.alipay.com/platform/appDaily.htm?tab=account

    進入沙箱環境.png

真實的支付寶網關:   https://openapi.alipay.com/gateway.do
    
沙箱的支付寶網關:   https://openapi.alipaydev.com/gateway.do

支付寶開發者文檔

電腦網站支付流程

1558585884855

開發支付功能

cd luffy/apps
python ../../manage.py startapp payments

註冊子應用

INSTALLED_APPS = [
    。。。。
    'payments',
]

配置祕鑰

1. 生成應用的私鑰和公鑰

下載對應系統的祕鑰生成工具: https://doc.open.alipay.com/docs/doc.htm?treeId=291&articleId=105971&docType=1

1554192077808

應用公鑰複製粘貼到支付寶網站頁面中.

1554192111270

1554192122648

點擊修改之後,粘貼進去

1554192143494

2. 保存應用私鑰文件

在payments應用中新建keys目錄,用來保存祕鑰文件。

將應用私鑰文件app_private_key.pem複製到payment/keys目錄下。

-----BEGIN RSA PRIVATE KEY-----
私鑰
-----END RSA PRIVATE KEY-----

3. 保存支付寶公鑰

在payment/keys目錄下新建alipay_public_key.pem文件,用於保存支付寶的公鑰文件。

將支付寶的公鑰內容複製到alipay_public_key.pem文件中

支付寶公鑰

-----BEGIN PUBLIC KEY-----
公鑰
-----END PUBLIC KEY-----

1554200221417

4. 使用支付寶的sdk開發支付接口

SDK:https://docs.open.alipay.com/270/106291/

python版本的支付寶SDK文檔:https://github.com/fzlee/alipay/blob/master/README.zh-hans.md

安裝命令:

pip install python-alipay-sdk --upgrade

後端提供發起支付的接口url地址

from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from orders.models import Order
from coupon.models import UserCoupon
from alipay import AliPay
from django.conf import settings
import os
from django.db import transaction
from decimal import Decimal

class AlipayAPIView(APIView):
    # permission_classes = [IsAuthenticated]

    def get(self,request):
        """生成支付寶支付地址"""
        # 接受參數[優惠券,訂單號]
        coupon_id = request.query_params.get("coupon_id")
        order_number  = request.query_params.get("order_number")
        try:
            order = Order.objects.get(order_number=order_number)
        except Order.DoesNotExist:
            return Response({"message":"對不起,當前訂單信息不存在!沒法進行支付"},status=status.HTTP_400_BAD_REQUEST)

        if coupon_id != None and coupon_id != "0":
            with transaction.atomic():
                save_id = transaction.savepoint()
                # 從新計算訂單實際支付價格
                try:
                    user_coupon = UserCoupon.objects.get(pk=coupon_id)
                except UserCoupon.DoesNotExist:
                    return Response({"message": "對不起,當前訂單使用的優惠券不存在!沒法進行支付"}, status=status.HTTP_400_BAD_REQUEST)

                if user_coupon.coupon.coupon_type == 0:
                    """折扣優惠"""
                    order.real_price = order.total_price * Decimal(user_coupon.coupon.sale[1:])
                elif user_coupon.coupon.coupon_type == 1:
                    order.real_price = order.total_price - Decimal(user_coupon.coupon.sale[1:])
                else:
                    return Response({"message": "當前優惠券沒法使用!沒法進行支付"}, status=status.HTTP_400_BAD_REQUEST)

                try:
                    # 通過上面的計算,保存實付價格和使用的優惠券
                    order.use_coupon = True
                    order.coupon = user_coupon.id
                    order.save()

                    # 上面的優惠券已經被使用了,因此咱們須要修改優惠券的狀態
                    user_coupon.is_use = True
                    user_coupon.save()

                except:
                    transaction.savepoint_rollback(save_id)
                    return Response({"message": "系統異常,沒法進行支付"}, status=status.HTTP_400_BAD_REQUEST)

        # 構造支付寶支付連接地址
        alipay = AliPay(
            appid=settings.ALIPAY_APPID,
            app_notify_url=settings.APP_NOTIFY_URL,  # 默認回調url
            app_private_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/app_private_key.pem"),
            # 支付寶的公鑰,驗證支付寶回傳消息使用,不是你本身的公鑰,
            alipay_public_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/alipay_public_key.pem"),
            sign_type="RSA2",  # RSA 或者 RSA2
            debug=settings.ALIPAY_DEBUG
        )

        order_string = alipay.api_alipay_trade_page_pay(
            out_trade_no=order.order_number,
            total_amount= "%.2f" % order.real_price,
            subject=order.order_title,
            return_url=settings.ALIPAY_RETURN_URL,
            notify_url=settings.ALIPAY_NOTIFY_URL,
        )

        url = settings.APIPAY_GATEWAY + "?" + order_string

        return Response({"message":"發起支付成功","url":url})

在配置文件中編輯支付寶的配置信息[實際的值根據本身的帳號而定]

# 支付寶
ALIPAY_APP_ID="2016091600523592" # 應用ID
APLIPAY_APP_NOTIFY_URL = None      # 應用回調地址[支付成功之後,支付寶返回結果到哪個地址下面]
ALIPAY_DEBUG = True
# APIPAY_GATEWAY="https://openapi.alipay.com/gateway.do"
APIPAY_GATEWAY="https://openapi.alipaydev.com/gateway.do"
ALIPAY_RETURN_URL = "http://127.0.0.1:8080/success"
ALIPAY_NOTIFY_URL = "http://127.0.0.1:8080/success"

前端點擊"支付寶支付",請求後端的發起支付api

<template>
  <div class="cart">
    <Header/>
    <div class="cart-info">
        <h3 class="cart-top">購物車結算 <span>共1門課程</span></h3>
        <div class="cart-title">
           <el-row>
             <el-col :span="2">&nbsp;</el-col>
             <el-col :span="10">課程</el-col>
             <el-col :span="8">有效期</el-col>
             <el-col :span="4">價格</el-col>
           </el-row>
        </div>
        <div class="cart-item" v-for="course in order_info.order_courses">
          <el-row>
             <el-col :span="2" class="checkbox">&nbsp;&nbsp;</el-col>
             <el-col :span="10" class="course-info">
               <img style="float: left;" :src="$settings.Host+course.course_img" alt="">
               <span class="course_name">
                 <span>{{course.course_name}}</span><br>
                 <span class="discount_name">{{course.discount_name}}</span>
               </span>
             </el-col>
             <el-col :span="8" class="lh"><span>{{course.expire_text}}</span></el-col>
             <el-col :span="4">
               <div class="course-price">
                  <p class="real_price">¥{{course.real_price}}</p>
                  <span class="original_price">原價: ¥{{course.price}}</span>
               </div>
             </el-col>
           </el-row>
        </div>
        <div>
        <div class="coupon">
          <div id="accordion">
            <div class="coupon-box">
              <div class="coupon-title">
                <span class="select-coupon">使用優惠劵:</span>
                <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" style="width: 20px; height: 20px" class="collapsed" aria-expanded="false">
                  <img class="sign" src="../../static/img/12.png" width="20" height="20" alt=""></a>
                  <span class="coupon-num">有0張可用</span>
              </div>
              <p class="sum-price-wrap" style="margin-right: 45px">商品總金額:<span class="sum-price">{{order_info.total_price}}元</span></p>
            </div>
            <div style="text-align: left;" id="collapseOne" class="panel-collapse out collapse" aria-expanded="false">
              <ul class="coupon-list" v-if="coupon_list.length>0">
                <li @click="use_coupon=item.id" v-for="item in coupon_list" class="coupon-item" :class="use_coupon==item.id?'coupon_selected':''">
                  <span>{{item.coupon.name}}</span>
                  <span v-if="item.coupon.type==1">¥{{item.coupon.sale}}元</span>
                  <span style="font-size: 12px;">開始使用時間: {{new Date(item.start_time).toLocaleString()}}</span>
                </li>
              </ul>
              <div v-else style="text-align: center; width: 100%; padding: 50px 0px; align-items: center; justify-content: center; border-bottom: 1px solid rgb(232, 232, 232);">
                <span style="font-size: 16px; color: #9b9b9b">暫無可用優惠券</span>
              </div>
            </div>
          </div>
          <div style="height: 30px; margin-top: 40px; display: flex; align-items: center; justify-content: flex-end">
            <input type="checkbox" class="ok" id="color-input-red">
            <label for="color-input-red"><img src="" alt=""></label>
            <p class="discount-num" style="color:#9B9B9B">使用個人貝里</p>
            <p class="discount-num" style="margin-right: 45px">
              <span style="display: none;">可用0個已抵扣 ¥0</span>
            </p>
          </div>
          <p class="sun-coupon-num" style="margin-right: 45px;margin-bottom:43px">優惠券抵扣:<span>0元</span></p>
        </div>
        </div>
        <div class="calc">
            <el-row class="pay-row">
              <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col>
              <el-col :span="8">
                <span class="alipay"><img src="../../static/img/alipay2.png" alt=""></span>
                <span class="alipay wechat"><img src="../../static/img/wechat.png" alt=""></span>
              </el-col>
              <el-col :span="8" class="count">實付款: <span>¥{{order_info.real_price}}</span></el-col>
              <el-col :span="4" class="cart-pay"><span @click="payhander">支付寶支付</span></el-col>
            </el-row>
        </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
  import Header from "./common/Header"
  import Footer from "./common/Footer"
  export default {
    name:"Order",
    data(){
      return {
        use_coupon: 0,  // 使用的優惠券ID
        order_info:{},
        coupon_list:[], // 用於展現優惠券列表
        coupon_list2:[],// 用於勾選優惠券功能
      }
    },
    components:{
      Header,
      Footer,
    },
    watch:{
      use_coupon(coupon_id){
        let data = this.coupon_list2[ coupon_id ];
        let sale = parseFloat( data.coupon.sale.slice(1) );
        if( data.coupon.coupon_type == 0 ){
          // 折扣優惠券
          this.order_info.real_price = this.order_info.total_price * sale;
        }else if(data.coupon.coupon_type == 1){
          // 減免優惠券
          this.order_info.real_price = this.order_info.total_price - sale;
        }
        this.order_info.real_price = this.order_info.real_price.toFixed(2);

        // 發送數據到後端,進行同步
      }
    },
    created(){
      // 判斷用戶是否已經登陸
      let token = sessionStorage.token || localStorage.token;
      let _this = this;
      if(!token){
        this.$alert("對不起,您還沒有登陸!請登陸!","警告",{
          callback(){
            _this.$router.push("/login");
          }
        })
      }
      // 獲取地址欄上面的訂單號
      let order_number = this.$route.params.order;

      // 發送請求獲取數據
      this.$axios.get(this.$settings.Host+`/orders/${order_number}/`,{
          headers:{
            // 注意下方的空格!!!
            "Authorization":"jwt " + token,
          },
      }).then(response=>{
        this.order_info = response.data;
      }).catch(error=>{
        console.log(error.response);
      });

      // 獲取用戶的優惠券
      this.get_coupon_list()
    },
    methods: {
      get_coupon_list(){
        let user_id = localStorage.user_id || sessionStorage.user_id;
        let token = localStorage.token || sessionStorage.token;
        // 獲取當前用戶的優惠券
        this.$axios.get(this.$settings.Host+"/coupon/list/",{
          params:{
            user_id,
          },
          headers:{
            // 注意下方的空格!!!
            "Authorization":"jwt " + token,
          },
        }).then(response=>{
          // 調整獲取到優惠券列表,以優惠券ID做爲下標
          let data_list = [];
          response.data.forEach(row=>{
            data_list[row.id] = row;
          });
          this.coupon_list2= data_list;
          this.coupon_list = response.data;
        }).catch(error=>{
          console.log(error.response);
        })
      },
      payhander(){

        this.$confirm("您即將跳轉到支付寶頁面進行訂單支付?","提示").then(()=>{
            let token = localStorage.token || sessionStorage.token;
            // 獲取訂單號
            let order_number = this.$route.params.order;
            // 發起支付請求
            this.$axios.get(this.$settings.Host+"/payments/alipay/url/",{
              params:{
                order_number: order_number,
                coupon_id: this.use_coupon,
              },
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + token,
              },
            }).then(response=>{
              let url = response.data.url;
              console.log(url);
              // 頁面跳轉
              location.assign( url );
            }).catch(error=>{
              console.log(error.response)
            })

        });


      },

    }
  }
</script>

<style scoped>
.cart{
  margin-top: 80px;
}
.cart-info{
  overflow: hidden;
  width: 1200px;
  margin: auto;
}
.cart-top{
  font-size: 18px;
  color: #666;
  margin: 25px 0;
  font-weight: normal;
}
.cart-top span{
    font-size: 12px;
    color: #d0d0d0;
    display: inline-block;
}
.cart-title{
    background: #F7F7F7;
    height: 70px;
}
.calc{
  margin-top: 25px;
  margin-bottom: 40px;
}

.calc .count{
  text-align: right;
  margin-right: 10px;
  vertical-align: middle;
}
.calc .count span{
    font-size: 36px;
    color: #333;
}
.calc .cart-pay{
    margin-top: 5px;
    width: 110px;
    height: 38px;
    outline: none;
    border: none;
    color: #fff;
    line-height: 38px;
    background: #ffc210;
    border-radius: 4px;
    font-size: 16px;
    text-align: center;
    cursor: pointer;
}
.cart-item{
  height: 120px;
  /*line-height: 120px;*/
}
.cart-item .lh{
  line-height: 120px;
}
.course-info img{
    width: 175px;
    height: 115px;
    margin-right: 35px;
    vertical-align: middle;
}
.course-price{
  margin-top: 40px;
}
.alipay{
  display: inline-block;
  height: 48px;
}
.alipay img{
  height: 100%;
  width:auto;
}

.pay-text{
  display: block;
  text-align: right;
  height: 100%;
  line-height: 100%;
  vertical-align: middle;
  margin-top: 20px;
}
.real_price{
    color: #333;
    margin-bottom: 10px;
}
.original_price{
    color: #9b9b9b;
    letter-spacing: .36px;
    text-decoration: line-through;
}
.coupon{
  margin-top: 30px;
}
.coupon-box{
  text-align: left;
  display: flex;
  padding-bottom: 22px;
  padding-left:30px;
  border-bottom: 1px solid #e8e8e8;
}
.coupon-title{
  display: flex;
}
.sum-price-wrap{
    display: inline-block;
    margin-left: auto;
    font-size: 16px;
    color: #4a4a4a;
}
.discount_name{
    color: #ffc210;
    margin-top: 24px;
    font-size: 14px;
    letter-spacing: .32px;
}
.course_name{
  margin-top: 40px;
  display: block;
}
.coupon-list{
  overflow: hidden;
}
.coupon-item{
  float: left;
  margin-left: 10px;
  margin-right: 10px;
  width: 200px;
  height: 60px;
  border: 1px solid #000;
  padding: 10px;
}
.coupon-item span{
  display: block;
  font-size: 14px;
}
.coupon_selected{
  border-color: red;
  color: indianred;
}
</style>

支付成功的模板

<template>
  <div class="success">
    <Header :current_page="current_page"/>
    <div class="main">
        <div class="title">
          <img src="../../static/images/right.svg" alt="">
          <div class="success-tips">
              <p class="tips1">您已成功購買 1 門課程!</p>
              <p class="tips2">你還能夠加入QQ羣 <span>747556033</span> 學習交流</p>
          </div>
        </div>
        <div class="order-info">
            <p class="info1"><b>付款時間:</b><span>2019/04/02 10:27</span></p>
            <p class="info2"><b>付款金額:</b><span >0</span></p>
            <p class="info3"><b>課程信息:</b><span><span>《Pycharm使用祕籍》</span></span></p>
        </div>
        <div class="wechat-code">
          <img src="../../static/images/server.cf99f78.png" alt="" class="er">
          <p><img src="../../static/images/tan.svg" alt="">重要!微信掃碼關注得到學習通知&amp;課程更新提醒!不然將嚴重影響學習進度和課程體驗!</p>
        </div>
        <div class="study">
          <span>當即學習</span>
        </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
  import Header from "./common/Header"
  import Footer from "./common/Footer"
  export default{
    name:"Success",
    data(){
      return {
        current_page:0,
      };
    },
    components:{
      Header,
      Footer,
    }
  }
</script>

<style scoped>
.success{
  padding-top: 80px;
}
.main{
    height: 100%;
    padding-top: 25px;
    padding-bottom: 25px;
    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 .tips1{
    font-size: 22px;
    color: #000;
}
.title .tips2{
    font-size: 16px;
    color: #4a4a4a;
    letter-spacing: 0;
    text-align: center;
    margin-top: 10px;
}
.title .tips2 span{
    color: #ec6730;
}
.order-info{
    padding: 25px 48px;
    padding-bottom: 15px;
    border-bottom: 1px solid #f2f2f2;
}
.order-info p{
  font-family: PingFangSC-Regular;
    display: -ms-flexbox;
    display: flex;
    margin-bottom: 10px;
    font-size: 16px;
}
.order-info p b{
  font-weight: 400;
  color: #9d9d9d;
  white-space: nowrap;
}
.wechat-code{
    display: flex;
    -ms-flex-align: center;
    align-items: center;
    padding: 25px 40px;
    border-bottom: 1px solid #f2f2f2;
}
.wechat-code>img{
    width: 100px;
    height: 100px;
    margin-right: 15px;
}
.wechat-code p{
    font-family: PingFangSC-Regular;
    font-size: 14px;
    color: #d0021b;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-align: center;
    align-items: center;
}
.wechat-code p>img{
    width: 16px;
    height: 16px;
    margin-right: 10px;
}
.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-family: PingFangSC-Regular;
  font-size: 16px;
  color: #fff;
}
</style>

後端接受支付結果

支付寶會返回的參數以下列表:

http://www.luffycity.cn:8080?
charset=utf-8&
out_trade_no=2019040217080000000010976&
method=alipay.trade.page.pay.return&
total_amount=1206.44&
sign=XKJG5826fH%2F9%2B3jCWw2ODjlc%2FuGLfqmr5RnimSAqrh%2B5bFkWcbLDh5V6VYtMqCpwnYp3FuGPqEeUeRO6WK62Qz0Q5nQGOA394IdxPfTOzry7PXuwYf41PCbDq53yg7vCYrobz4Tt8uajeADJLJwIsL%2F%2B88vbDEISUDUujL4442kl3oLh3EDD8DxZc2LLsv1Z%2FEFGJMfcTA47A4T7qmjB%2BbLKJetZZBISdt9RDL0q8A%2BAfb8B3Ux1nq%2F0EiNGiwIlWC1pvUCHK2UXMJW3kmgU9P9Zoujrj4ER28oieQt6Rt4gQXeah5uYtAMkftWfZpiyu%2FjUkr6iRx%2B4mP5IFz4Uew%3D%3D&
trade_no=2019040222001439881000005802&
auth_app_id=2016091600523592&
version=1.0&
app_id=2016091600523592&
sign_type=RSA2&
seller_id=2088102175868026&
timestamp=2019-04-02%2017%3A13%3A15

後端完成 支付寶支付結果的處理並更新訂單和購買記錄

users/models.py,模型代碼:

from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    """用戶模型類"""
    mobile = models.CharField(max_length=11, unique=True, verbose_name='手機號')

    class Meta:
        db_table = 'ly_users'
        verbose_name = '用戶'
        verbose_name_plural = verbose_name


from luffy.utils.models import BaseModel
from courses.models import Course
class UserCourse(BaseModel):
    pay_choices = (
        (0, '支付寶'),
        (1, '微信支付'),
        (2, '免費活動'),
        (3, '活動贈品'),
        (4, '系統贈送'),
    )
    user = models.ForeignKey(User, related_name='user_courses', on_delete=models.DO_NOTHING,verbose_name="用戶")
    course = models.ForeignKey(Course, related_name='course_users', on_delete=models.DO_NOTHING, verbose_name="課程")
    buy_number = models.CharField(max_length=128, null=True, verbose_name="帳單號")
    buy_type = models.SmallIntegerField(choices=pay_choices, default=0, verbose_name="購買方式")
    pay_time = models.DateTimeField(null=True, verbose_name="購買時間")
    out_time = models.DateTimeField(null=True, verbose_name="過時時間")

    class Meta:
        db_table = 'ly_user_course'
        verbose_name = '課程購買記錄'
        verbose_name_plural = verbose_name

payments/views.py,視圖代碼:

from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from orders.models import Order
from coupon.models import UserCoupon
from alipay import AliPay
from django.conf import settings
import os
from django.db import transaction
from decimal import Decimal
import logging
log = logging.getLogger("django")
from datetime import datetime
from users.models import UserCourse

class AlipayAPIView(APIView):
    # permission_classes = [IsAuthenticated]

    def get(self,request):
        """生成支付寶支付地址"""
        # 接受參數[優惠券,訂單號]
        coupon_id = request.query_params.get("coupon_id")
        order_number  = request.query_params.get("order_number")
        try:
            order = Order.objects.get(order_number=order_number)
        except Order.DoesNotExist:
            return Response({"message":"對不起,當前訂單信息不存在!沒法進行支付"},status=status.HTTP_400_BAD_REQUEST)

        if coupon_id != None and coupon_id != "0":
            with transaction.atomic():
                save_id = transaction.savepoint()
                # 從新計算訂單實際支付價格
                try:
                    user_coupon = UserCoupon.objects.get(pk=coupon_id)
                except UserCoupon.DoesNotExist:
                    return Response({"message": "對不起,當前訂單使用的優惠券不存在!沒法進行支付"}, status=status.HTTP_400_BAD_REQUEST)

                if user_coupon.coupon.coupon_type == 0:
                    """折扣優惠"""
                    order.real_price = order.total_price * Decimal(user_coupon.coupon.sale[1:])
                elif user_coupon.coupon.coupon_type == 1:
                    order.real_price = order.total_price - Decimal(user_coupon.coupon.sale[1:])
                else:
                    return Response({"message": "當前優惠券沒法使用!沒法進行支付"}, status=status.HTTP_400_BAD_REQUEST)

                try:
                    # 通過上面的計算,保存實付價格和使用的優惠券
                    order.use_coupon = True
                    order.coupon = user_coupon.id
                    order.save()

                    # 上面的優惠券已經被使用了,因此咱們須要修改優惠券的狀態
                    user_coupon.is_use = True
                    user_coupon.save()

                except:
                    transaction.savepoint_rollback(save_id)
                    return Response({"message": "系統異常,沒法進行支付"}, status=status.HTTP_400_BAD_REQUEST)

        # 構造支付寶支付連接地址
        alipay = AliPay(
            appid=settings.ALIPAY_APPID,
            app_notify_url=settings.APP_NOTIFY_URL,  # 默認回調url
            app_private_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/app_private_key.pem"),
            # 支付寶的公鑰,驗證支付寶回傳消息使用,不是你本身的公鑰,
            alipay_public_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/alipay_public_key.pem"),
            sign_type="RSA2",  # RSA 或者 RSA2
            debug=settings.ALIPAY_DEBUG
        )

        order_string = alipay.api_alipay_trade_page_pay(
            out_trade_no=order.order_number,
            total_amount= "%.2f" % order.real_price,
            subject=order.order_title,
            return_url=settings.ALIPAY_RETURN_URL,
            notify_url=settings.ALIPAY_NOTIFY_URL,
        )

        url = settings.APIPAY_GATEWAY + "?" + order_string

        return Response({"message":"發起支付成功","url":url})


class AlipayResult(APIView):
    def get(self,request):
        # 獲取支付結果的全部參數,並轉換成字典
        data = request.query_params.dict()
        # 在字典中移除sign簽名
        signature = data.pop("sign")

        alipay = AliPay(
            appid=settings.ALIPAY_APPID,
            app_notify_url=settings.APP_NOTIFY_URL,  # 默認回調url
            app_private_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/app_private_key.pem"),
            # 支付寶的公鑰,驗證支付寶回傳消息使用,不是你本身的公鑰,
            alipay_public_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/alipay_public_key.pem"),
            sign_type="RSA2",  # RSA 或者 RSA2
            debug=settings.ALIPAY_DEBUG
        )

        success = alipay.verify(data, signature)
        if success:
            # 支付成功!
            # 更新訂單
            order_number = data.get("out_trade_no")
            try:
                order = Order.objects.get( order_number=order_number )
            except Order.DoesNotExist:
                log.error("訂單號:%s不存在!" % order_number )
                return Response({"message": "無效的訂單號"},status=status.HTTP_500_INTERNAL_SERVER_ERROR)

            with transaction.atomic():
                save_id = transaction.savepoint()
                try:
                    order.order_status = 1
                    order.pay_time = datetime.now()
                    # order.pay_time = data.get("timestamp")
                    order.save()

                    # 課程與用戶之間添加一條購買記錄
                    detail_list = order.order_courses.all()
                    course_list = []
                    for detail in detail_list:

                        if detail.expire=='-1':
                            out_time = "2099-01-01 00:00:00"
                        else:
                            out_time = datetime.now().timestamp() + detail.expire * 86400
                            # 日期時間對象 = fromtimestamp(數值時間戳)
                            out_time = datetime.fromtimestamp(out_time)
                            out_time = out_time.strftime("%Y-%m-%d %H:%M:%S")

                        UserCourse.objects.create(
                            user=order.user,
                            course=detail.course,
                            buy_number=data.get("trade_no"),
                            buy_type=0,
                            pay_time=data.get("timestamp"),
                            out_time=out_time
                        )

                        course_list.append(detail.course.name)

                    return Response({"message":{
                        "pay_time": order.pay_time.strftime("%Y-%m-%d %H:%M:%S"),
                        "real_price": order.real_price,
                        "course_list":course_list,
                    }})

                except:
                    log.error("修改訂單和購買記錄發生異常!")
                    transaction.savepoint_rollback(save_id)
                    return Response({"message":"系統異常!"},status=status.HTTP_500_INTERNAL_SERVER_ERROR)

payments/urls.py,路由代碼:

from django.urls import path,re_path
from . import views
urlpatterns = [
    path("alipay/url/",views.AlipayAPIView.as_view() ),
    path("alipay/result/",views.AlipayResult.as_view() ),
]

前端把地址欄上面返回的同步支付結果轉發給服務端

Success.vue

<template>
  <div class="success">
    <Header/>
    <div class="main">
        <div class="title">
<!--          <img src="../../static/images/right.svg" alt="">-->
          <div class="success-tips">
              <p class="tips1">您已成功購買 {{order_info.course_list.length}} 門課程!</p>
              <p class="tips2">你還能夠加入QQ羣 <span>747556033</span> 學習交流</p>
          </div>
        </div>
        <div class="order-info">
            <p class="info1"><b>付款時間:</b><span>{{order_info.pay_time}}</span></p>
            <p class="info2"><b>付款金額:</b><span >¥{{order_info.real_price}}元</span></p>
            <p class="info3"><b>課程信息:</b><span><span>{{order_info.course_list2}}</span></span></p>
        </div>
        <div class="wechat-code">
<!--          <img src="../../static/images/server.cf99f78.png" alt="" class="er">-->
<!--          <p><img src="../../static/images/tan.svg" alt="">重要!微信掃碼關注得到學習通知&amp;課程更新提醒!不然將嚴重影響學習進度和課程體驗!</p>-->
        </div>
        <div class="study">
          <span>當即學習</span>
        </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
  import Header from "./common/Header"
  import Footer from "./common/Footer"
  export default{
    name:"Success",
    data(){
      return {
        order_info:{
          course_list:[]
        }
      };
    },
    created(){
        let token = localStorage.token || sessionStorage.token;

        if(!token){
          this.$alert("對不起,您還沒有登陸!請登陸!","警告",{
            callback(){
              _this.$router.push("/login");
            }
          })
        }

        // 轉發支付結果到後端
        this.$axios.get(this.$settings.Host+"/payments/alipay/result/"+location.search,{
          headers:{
            // 注意下方的空格!!!
            "Authorization":"jwt " + token,
          },
        }).then(response=>{
          this.order_info = response.data.message;
          this.order_info.course_list2 = "";
          this.order_info.course_list.forEach(course=>{
            this.order_info.course_list2+=`《${course}》`;
          })
        }).catch(error=>{

          console.log(error.response);
        })
    },
    components:{
      Header,
      Footer,
    }
  }
</script>

<style scoped>
.success{
  padding-top: 80px;
}
.main{
    height: 100%;
    padding-top: 25px;
    padding-bottom: 25px;
    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 .tips1{
    font-size: 22px;
    color: #000;
}
.title .tips2{
    font-size: 16px;
    color: #4a4a4a;
    letter-spacing: 0;
    text-align: center;
    margin-top: 10px;
}
.title .tips2 span{
    color: #ec6730;
}
.order-info{
    padding: 25px 48px;
    padding-bottom: 15px;
    border-bottom: 1px solid #f2f2f2;
}
.order-info p{
  font-family: PingFangSC-Regular;
    display: -ms-flexbox;
    display: flex;
    margin-bottom: 10px;
    font-size: 16px;
}
.order-info p b{
  font-weight: 400;
  color: #9d9d9d;
  white-space: nowrap;
}
.wechat-code{
    display: flex;
    -ms-flex-align: center;
    align-items: center;
    padding: 25px 40px;
    border-bottom: 1px solid #f2f2f2;
}
.wechat-code>img{
    width: 100px;
    height: 100px;
    margin-right: 15px;
}
.wechat-code p{
    font-family: PingFangSC-Regular;
    font-size: 14px;
    color: #d0021b;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-align: center;
    align-items: center;
}
.wechat-code p>img{
    width: 16px;
    height: 16px;
    margin-right: 10px;
}
.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-family: PingFangSC-Regular;
  font-size: 16px;
  color: #fff;
}
</style>

修改訂單結果[新增接口接受異步支付結果]

from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from orders.models import Order
from coupon.models import UserCoupon
from alipay import AliPay
from django.conf import settings
import os
from django.db import transaction
from decimal import Decimal
import logging
log = logging.getLogger("django")
from datetime import datetime
from users.models import UserCourse

class AlipayAPIView(APIView):
    ......


class AlipayResult(APIView):
    def get(self,request):
        ......

   def post(self,request):

        data = request.data.dict()
        # 在字典中移除sign簽名
        signature = data.pop("sign")

        alipay = AliPay(
            appid=settings.ALIPAY_APPID,
            app_notify_url=settings.APP_NOTIFY_URL,  # 默認回調url
            app_private_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/app_private_key.pem"),
            # 支付寶的公鑰,驗證支付寶回傳消息使用,不是你本身的公鑰,
            alipay_public_key_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys/alipay_public_key.pem"),
            sign_type="RSA2",  # RSA 或者 RSA2
            debug=settings.ALIPAY_DEBUG
        )

        success = alipay.verify(data, signature)
        if success:
            # 支付成功!
            # 更新訂單
            order_number = data.get("out_trade_no")
            try:
                order = Order.objects.get( order_number=order_number )
            except Order.DoesNotExist:
                log.error("訂單號:%s不存在!" % order_number )
                return Response({"message": "無效的訂單號"},status=status.HTTP_500_INTERNAL_SERVER_ERROR)

            with transaction.atomic():
                save_id = transaction.savepoint()
                try:
                    order.order_status = 1
                    order.pay_time = datetime.now()
                    # order.pay_time = data.get("timestamp")
                    order.save()

                    # 課程與用戶之間添加一條購買記錄
                    detail_list = order.order_courses.all()
                    course_list = []
                    for detail in detail_list:

                        if detail.expire=='-1':
                            out_time = "2099-01-01 00:00:00"
                        else:
                            out_time = datetime.now().timestamp() + detail.expire * 86400
                            # 日期時間對象 = fromtimestamp(數值時間戳)
                            out_time = datetime.fromtimestamp(out_time)
                            out_time = out_time.strftime("%Y-%m-%d %H:%M:%S")

                        UserCourse.objects.create(
                            user=order.user,
                            course=detail.course,
                            buy_number=data.get("trade_no"),
                            buy_type=0,
                            pay_time=data.get("timestamp"),
                            out_time=out_time
                        )

                        course_list.append(detail.course.name)

                    return Response("success", content_type="text/html")

                except:
                    log.error("修改訂單和購買記錄發生異常!")
                    transaction.savepoint_rollback(save_id)
                    return Response({"message":"系統異常!"},status=status.HTTP_500_INTERNAL_SERVER_ERROR)

個人訂單

<template>
  <div class="user-order">
    <Header/>
    <div class="main">
        <div class="banner"></div>
          <div class="profile">
              <div class="profile-info">
                  <div class="avatar"><img class="newImg" width="100%" alt="" src="/static/img/logo@2x.png"></div>
                  <span class="user-name">Mixtea</span>
                  <span class="user-job">深圳市 | 程序員</span>
              </div>
              <ul class="my-item">
                  <li>個人帳戶</li>
                  <li class="active">個人訂單</li>
                  <li>我的資料</li>
                  <li>帳號安全</li>
              </ul>
            </div>
          <div class="user-data">
            <ul class="nav">
              <li class="order-info">訂單</li>
              <li class="course-expire">有效期</li>
              <li class="course-price">課程價格</li>
              <li class="real-price">實付金額</li>
              <li class="order-status">交易狀態</li>
              <li class="order-do">交易操做</li>
            </ul>
            <div class="my-order-item">
                <div class="user-data-header">
                  <span class="order-time">2019-04-02 10:27:49</span>
                  <span class="order-num">訂單號:
                      <span class="my-older-number">20190402102749606</span>
                  </span>
                </div>
                <ul class="nav user-data-list">
              <li class="order-info">
                  <img src="../../static/course/1544059695.jpeg" alt="">
                  <div class="order-info-title">
                    <p class="course-title">Pycharm使用祕籍</p>
                    <p class="price-service">限時免費</p>
                  </div>
              </li>
              <li class="course-expire">永久有效</li>
              <li class="course-price">977.00</li>
              <li class="real-price">577.00</li>
              <li class="order-status">交易成功</li>
              <li class="order-do">
                <span class="btn btn2">去學習</span>
              </li>
            </ul>
            </div>
            <div class="my-order-item">
                <div class="user-data-header">
                  <span class="order-time">2019-04-02 10:27:49</span>
                  <span class="order-num">訂單號:
                      <span class="my-older-number">20190402102749606</span>
                  </span>
                </div>
                <ul class="nav user-data-list">
              <li class="order-info">
                  <img src="../../static/course/1544059695.jpeg" alt="">
                  <div class="order-info-title">
                    <p class="course-title">Pycharm使用祕籍</p>
                    <p class="price-service">限時免費</p>
                  </div>
              </li>
              <li class="course-expire">永久有效</li>
              <li class="course-price">977.00</li>
              <li class="real-price">577.00</li>
              <li class="order-status">交易成功</li>
              <li class="order-do">
                <span class="btn btn2">去學習</span>
              </li>
            </ul>
            </div>
            <div class="my-order-item">
                <div class="user-data-header">
                  <span class="order-time">2019-04-02 10:27:49</span>
                  <span class="order-num">訂單號:
                      <span class="my-older-number">20190402102749606</span>
                  </span>
                </div>
                <ul class="nav user-data-list">
              <li class="order-info">
                  <img src="../../static/course/1544059695.jpeg" alt="">
                  <div class="order-info-title">
                    <p class="course-title">Pycharm使用祕籍</p>
                    <p class="price-service">限時免費</p>
                  </div>
              </li>
              <li class="course-expire">永久有效</li>
              <li class="course-price">977.00</li>
              <li class="real-price">577.00</li>
              <li class="order-status">交易成功</li>
              <li class="order-do">
                <span class="btn btn2">去學習</span>
              </li>
            </ul>
            </div>
            <div class="my-order-item">
                <div class="user-data-header">
                  <span class="order-time">2019-04-02 10:27:49</span>
                  <span class="order-num">訂單號:
                      <span class="my-older-number">20190402102749606</span>
                  </span>
                </div>
                <ul class="nav user-data-list">
              <li class="order-info">
                  <img src="../../static/course/1544059695.jpeg" alt="">
                  <div class="order-info-title">
                    <p class="course-title">Pycharm使用祕籍</p>
                    <p class="price-service">限時免費</p>
                  </div>
              </li>
              <li class="course-expire">永久有效</li>
              <li class="course-price">977.00</li>
              <li class="real-price">577.00</li>
              <li class="order-status">交易成功</li>
              <li class="order-do">
                <span class="btn btn2">去學習</span>
              </li>
            </ul>
            </div>
            <div class="my-order-item">
                <div class="user-data-header">
                  <span class="order-time">2019-04-02 10:27:49</span>
                  <span class="order-num">訂單號:
                      <span class="my-older-number">20190402102749606</span>
                  </span>
                </div>
                <ul class="nav user-data-list">
              <li class="order-info">
                  <img src="../../static/course/1544059695.jpeg" alt="">
                  <div class="order-info-title">
                    <p class="course-title">Pycharm使用祕籍</p>
                    <p class="price-service">限時免費</p>
                  </div>
              </li>
              <li class="course-expire">永久有效</li>
              <li class="course-price">977.00</li>
              <li class="real-price">577.00</li>
              <li class="order-status">交易成功</li>
              <li class="order-do">
                <span class="btn btn2">去學習</span>
              </li>
            </ul>
            </div>
        </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
  import Header from "./common/Header"
  import Footer from "./common/Footer"
  export default{
    name:"UserOrder",
    data(){
      return {
      };
    },
    components:{
      Header,
      Footer,
    }
  }
</script>

<style scoped>
.user-order{
  padding-top: 80px;
}
.main .banner{
    width: 100%;
    height: 324px;
    background: url(/static/img/my_bkging.0648ebe.png) no-repeat;
    background-size: cover;
    z-index: 1;
}
.profile{
    width: 1200px;
    margin: 0 auto;
}
.profile-info{
    text-align: center;
    margin-top: -80px;
}
.avatar{
    width: 120px;
    height: 120px;
    border-radius: 60px;
    overflow: hidden;
    margin: 0 auto;
}
.user-name{
    display: block;
    font-size: 24px;
    color: #4a4a4a;
    margin-top: 14px;
}
.user-job{
    display: block;
    font-size: 11px;
    color: #9b9b9b;
 }
.my-item{
    list-style: none;
    line-height: 1.42857143;
    color: #333;
    width: 474px;
    height: 31px;
    display: -ms-flexbox;
    display: flex;
    cursor: pointer;
    margin: 41px auto 0;
    -ms-flex-pack: justify;
    justify-content: space-between;
}
.my-item .active{
    border-bottom: 1px solid #000;
}
.user-data{
    width: 1200px;
    height: auto;
    margin: 0 auto;
    padding-top: 30px;
    border-top: 1px solid #e8e8e8;
    margin-bottom: 63px;
}
.nav{
    width: 100%;
    height: 60px;
    background: #e9e9e9;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-align: center;
    align-items: center;
}
.nav li{
    margin-left: 20px;
    margin-right: 28px;
    height: 60px;
    line-height: 60px;
    list-style: none;
    font-family: PingFangSC-Medium;
    font-size: 13px;
    color: #333;
    border-bottom: 1px solid #e9e9e9;
  width: 160px;
}
.nav .order-info{ width: 325px; }
.nav .course-expire{ width: 60px; }
.nav .course-price{ width: 130px; }
.user-data-header{
    display: flex;
    height: 44px;
    color: #4a4a4a;
    font-size: 14px;
    background: #f3f3f3;
    -ms-flex-align: center;
    align-items: center;
    font-family: PingFangSC-Regular;
}
.order-time{
    font-size: 12px;
    display: inline-block;
    margin-left: 20px;
}
.order-num{
    font-size: 12px;
    display: inline-block;
    margin-left: 29px;
}
.user-data-list{
    height: 100%;
    display: flex;
}
.user-data-list{
  background: none;
}
.user-data-list li{
    height: 60px;
    line-height: 60px;
}
.user-data-list .order-info{
    display: flex;
    align-items: center;
    margin-right: 28px;
}
.user-data-list .order-info img{
    max-width: 100px;
    max-height: 75px;
    margin-right: 22px;
}
.course-title{
    width: 203px;
    font-size: 13px;
    color: #333;
    line-height: 20px;
    font-family: PingFangSC-Medium;
    margin-top: -10px;
}
.order-info-title .price-service{
    line-height: 18px;
}
.price-service{
    font-size: 12px;
    color: #fa6240;
    padding: 0 5px;
    border: 1px solid #fa6240;
    border-radius: 4px;
    margin-top: 4px;
    position: absolute;
}
.order-info-title{
    margin-top: -10px;
}
.user-data-list .course-expire{
    font-size: 12px;
    color: #ff5502;
    font-family: PingFangSC-Medium;
    width: 60px;

    text-align: center;
}
.btn {
  width: 100px;
  height: 32px;
  font-size: 14px;
  color: #fff;
  background: #ffc210;
  border-radius: 4px;
  border: none;
  outline: none;
  font-family: PingFangSC-Medium;
  transition: all .25s ease;
  display: inline-block;
  line-height: 32px;
  text-align: center;
  cursor: pointer;
}
</style>

路由註冊:

import Vue from "vue"
import Router from "vue-router"

// 導入頁面組件
。。。
import User from "../components/User"
import UserOrder from "../components/UserOrder"


Vue.use(Router);

export default new Router({
  // 設置路由模式爲‘history’,去掉默認的#
  mode: "history",
  routes:[
   ....
    {
      name:"User",
      path:"/my",
      component: User,
      // children:[  // 設置子路由,在父級路由對應的組件中若是存在父子公用部分頁面,可使用router-view來實現子路由
      //   {
      //     name:"UserOrder",
      //     path:"/order",
      //     component: UserOrder,
      //   }
      // ]
    },
    {
      name:"UserOrder",
      path:"/my/order",
      component: UserOrder,
    },
  ]
})

後端提供查詢當前登陸用戶的訂單列表信息。

orders/models.py,模型新增返回訂單狀態的文本格式

class Order(BaseModel):
    """訂單記錄"""
    。。。。
    def order_status_text(self):
        return self.status_choices[self.order_status][1]

users/serializers.py,序列化器,代碼:

"""會員訂單"""
from orders.models import Order,OrderDetail
class OrderDetailListModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = OrderDetail
        fields = ("price","real_price","discount_name","expire_text","course_img","course_name","course")

class OrderListModelSerializer(serializers.ModelSerializer):
    order_courses = OrderDetailListModelSerializer(many=True)
    class Meta:
        model = Order
        fields = ("order_courses","id","create_time","pay_time","order_number","real_price","total_price","order_status","order_status_text","pay_type")

class UserOrderModelSerializer(serializers.ModelSerializer):
    user_orders = OrderListModelSerializer(many=True)
    class Meta:
        model = User
        # fields = ("username","身份信息..")
        fields = ("username","user_orders")

users/views.py,視圖代碼:

from .serializers import UserOrderModelSerializer
from rest_framework.generics import RetrieveAPIView
from rest_framework.permissions import IsAuthenticated
class UserOrderAPIView(RetrieveAPIView):
    permission_classes = [IsAuthenticated]
    serializer_class = UserOrderModelSerializer
    queryset = User.objects.all()

users/urls.py,路由代碼:

re_path(r'(?P<pk>\d+)/orders/',views.UserOrderAPIView.as_view()),

前端請求獲取當前登陸用戶的訂單信息

<template>
  <div class="user-order">
    <Header/>
    <div class="main">
        <div class="banner"></div>
          <div class="profile">
              <div class="profile-info">
                  <div class="avatar"><img class="newImg" width="100%" alt="" src="/static/img/logo@2x.png"></div>
                  <span class="user-name">{{user_info.username}}</span>
                  <span class="user-job">深圳市 | 程序員</span>
              </div>
              <ul class="my-item">
                  <li>個人帳戶</li>
                  <li class="active">個人訂單</li>
                  <li>我的資料</li>
                  <li>帳號安全</li>
              </ul>
            </div>
          <div class="user-data">
            <ul class="nav">
              <li class="order-info">訂單</li>
              <li class="course-expire">有效期</li>
              <li class="course-price">課程價格</li>
              <li class="real-price">實付金額</li>
              <li class="order-status">交易狀態</li>
              <li class="order-do">交易操做</li>
            </ul>
            <div class="my-order-item" v-for="order in user_info.user_orders">
                <div class="user-data-header">
                  <div class="order-time">
                    下單時間: {{new Date(order.create_time).toLocaleString()}}
                    <span style="padding-left: 20px;" v-if="order.pay_time">付款時間: {{new Date(order.pay_time).toLocaleString()}}</span>
                  </div>
                  <span class="order-num">訂單號:
                      <span class="my-older-number">{{order.order_number}}</span>
                  </span>
                </div>
              <ul class="nav user-data-list" v-for="course in order.order_courses">
                <li class="order-info">
                    <img :src="$settings.Host+course.course_img" alt="">
                    <div class="order-info-title">
                      <p class="course-title">{{course.course_name}}</p>
                      <p v-if="course.discount_name" class="price-service">{{course.discount_name}}</p>
                    </div>
                </li>
                <li class="course-expire">{{course.expire_text}}</li>
                <li class="course-price">{{course.price}}</li>
                <li class="real-price">{{course.real_price}}</li>
                <li class="order-status">{{order.order_status_text}}</li>
                <li class="order-do">
                  <router-link v-if="order.order_status==1" to="/my/course" class="btn btn2">去學習</router-link>
                  <router-link v-if="order.order_status==0" :to="'/orders/'+order.order_number" class="btn btn2">去付款</router-link>
                </li>
              </ul>

            </div>

        </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
  import Header from "./common/Header"
  import Footer from "./common/Footer"
  export default{
    name:"UserOrder",
    data(){
      return {
        user_info:{}
      };
    },
    created(){
        let token = localStorage.token || sessionStorage.token;
        let user_id = localStorage.user_id || sessionStorage.user_id;
        if(!token){
          this.$alert("對不起,您還沒有登陸!請登陸!","警告",{
            callback(){
              _this.$router.push("/login");
            }
          })
        }

        this.$axios.get(this.$settings.Host+`/users/${user_id}/orders/`,{
          headers:{
            // 注意下方的空格!!!
            "Authorization":"jwt " + token,
          },
        }).then(response=>{
          this.user_info = response.data;
          console.log(this.user_info);
        }).catch(error=>{
          console.log( error.response )
        })
    },
    components:{
      Header,
      Footer,
    }
  }
</script>

<style scoped>
.user-order{
  padding-top: 80px;
}
.main .banner{
    width: 100%;
    height: 324px;
    background: url(/static/img/my_bkging.0648ebe.png) no-repeat;
    background-size: cover;
    z-index: 1;
}
.profile{
    width: 1200px;
    margin: 0 auto;
}
.profile-info{
    text-align: center;
    margin-top: -80px;
}
.avatar{
    width: 120px;
    height: 120px;
    border-radius: 60px;
    overflow: hidden;
    margin: 0 auto;
}
.user-name{
    display: block;
    font-size: 24px;
    color: #4a4a4a;
    margin-top: 14px;
}
.user-job{
    display: block;
    font-size: 11px;
    color: #9b9b9b;
 }
.my-item{
    list-style: none;
    line-height: 1.42857143;
    color: #333;
    width: 474px;
    height: 31px;
    display: -ms-flexbox;
    display: flex;
    cursor: pointer;
    margin: 41px auto 0;
    -ms-flex-pack: justify;
    justify-content: space-between;
}
.my-item .active{
    border-bottom: 1px solid #000;
}
.user-data{
    width: 1200px;
    height: auto;
    margin: 0 auto;
    padding-top: 30px;
    border-top: 1px solid #e8e8e8;
    margin-bottom: 63px;
}
.nav{
    width: 100%;
    height: 60px;
    background: #e9e9e9;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-align: center;
    align-items: center;
}
.nav li{
    margin-left: 20px;
    margin-right: 28px;
    height: 60px;
    line-height: 60px;
    list-style: none;
    font-family: PingFangSC-Medium;
    font-size: 13px;
    color: #333;
    border-bottom: 1px solid #e9e9e9;
  width: 160px;
}
.nav .order-info{ width: 325px; }
.nav .course-expire{ width: 60px; }
.nav .course-price{ width: 130px; }
.user-data-header{
    display: flex;
    height: 44px;
    color: #4a4a4a;
    font-size: 14px;
    background: #f3f3f3;
    -ms-flex-align: center;
    align-items: center;
    font-family: PingFangSC-Regular;
}
.order-time{
    font-size: 12px;
    display: inline-block;
    margin-left: 20px;
}
.order-num{
    font-size: 12px;
    display: inline-block;
    margin-left: 29px;
}
.user-data-list{
    height: 100%;
    display: flex;
}
.user-data-list{
  background: none;
}
.user-data-list li{
    height: 60px;
    line-height: 60px;
}
.user-data-list .order-info{
    display: flex;
    align-items: center;
    margin-right: 28px;
}
.user-data-list .order-info img{
    max-width: 100px;
    max-height: 75px;
    margin-right: 22px;
}
.course-title{
    width: 203px;
    font-size: 13px;
    color: #333;
    line-height: 20px;
    font-family: PingFangSC-Medium;
    margin-top: -10px;
}
.order-info-title .price-service{
    line-height: 18px;
}
.price-service{
    font-size: 12px;
    color: #fa6240;
    padding: 0 5px;
    border: 1px solid #fa6240;
    border-radius: 4px;
    margin-top: 4px;
    position: absolute;
}
.order-info-title{
    margin-top: -10px;
}
.user-data-list .course-expire{
    font-size: 12px;
    color: #ff5502;
    font-family: PingFangSC-Medium;
    width: 60px;

    text-align: center;
}
.btn {
  width: 100px;
  height: 32px;
  font-size: 14px;
  color: #fff;
  background: #ffc210;
  border-radius: 4px;
  border: none;
  outline: none;
  font-family: PingFangSC-Medium;
  transition: all .25s ease;
  display: inline-block;
  line-height: 32px;
  text-align: center;
  cursor: pointer;
}
</style>

訂單狀態顯示分析

根據訂單狀態顯示:
1. 若是未支付[order.order_stauts=0],則顯示"去支付"按鈕
2. 若是已支付[order.order_stauts=1],則顯示"去學習"按鈕
3. 若是未支付,並超過指定時間[12個小時],則顯示"已取消" [celery+RabbitMQ / Django-crontab 定時任務 ]
   用戶下單在12小時之後自動判斷訂單狀態若是是0,則直接改爲3
本站公眾號
   歡迎關注本站公眾號,獲取更多信息