爲了方便開發,和之後項目的維護,咱們再次建立子應用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;
在當前子應用下建立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)
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)
視圖代碼:
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"> </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"> </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>
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"> </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"> </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', ]
模型分析:
代碼:
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)
前端編寫優惠券的樣式代碼:
<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"> </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"> </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"> </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"> </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
是支付寶提供給開發者的模擬支付的環境
沙箱環境跟真實環境是分開的,項目上線時必須切換對應的配置服務器地址和開發者ID和密鑰。
沙箱應用開發文檔:<https://docs.open.alipay.com
沙箱帳號:https://openhome.alipay.com/platform/appDaily.htm?tab=account
真實的支付寶網關: https://openapi.alipay.com/gateway.do 沙箱的支付寶網關: https://openapi.alipaydev.com/gateway.do
cd luffy/apps python ../../manage.py startapp payments
註冊子應用
INSTALLED_APPS = [ 。。。。 'payments', ]
下載對應系統的祕鑰生成工具: https://doc.open.alipay.com/docs/doc.htm?treeId=291&articleId=105971&docType=1
應用公鑰複製粘貼到支付寶網站頁面中.
點擊修改之後,粘貼進去
在payments應用中新建keys目錄,用來保存祕鑰文件。
將應用私鑰文件app_private_key.pem複製到payment/keys目錄下。
-----BEGIN RSA PRIVATE KEY----- 私鑰 -----END RSA PRIVATE KEY-----
在payment/keys目錄下新建alipay_public_key.pem文件,用於保存支付寶的公鑰文件。
將支付寶的公鑰內容複製到alipay_public_key.pem文件中
-----BEGIN PUBLIC KEY----- 公鑰 -----END PUBLIC KEY-----
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
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"
<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"> </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"> </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="">重要!微信掃碼關注得到學習通知&課程更新提醒!不然將嚴重影響學習進度和課程體驗!</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="">重要!微信掃碼關注得到學習通知&課程更新提醒!不然將嚴重影響學習進度和課程體驗!</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