文章目录

  • 结算页面
  • 订单模型
  • 把当前子应用注册到xadmin中
  • 后端实现生成订单的api接口
  • 使用django提供的mysql事务操作保证下单过程中的数据一致性
  • 前端请求生成订单
  • 前端请求后端的订单信息
  • 优惠券
  • 在前端实现可以让用户选择对应的支付方式


结算页面

完成购物车功能以后,那么我们可以让用户点击“去结算”按钮时,在后端提供一个查询勾选商品的API接口给客户端,展示数据在结算页面中。

Python超市自助结算_redis

后端实现API接口,cart/views.py,代码:

@action(methods=["get"], detail=False)
    def selected(self,request):
        """获取勾选的商品课程列表"""
        user_id = 1 # request.user.id
        redis = get_redis_connection("cart")
        cart_list = redis.hgetall("cart_%s" % user_id)
        selected_set = redis.smembers("selected_%s" % user_id )

        data = []
        for course_id_byte in selected_set:
            course_id = course_id_byte.decode()
            try:
                course = Course.objects.get( is_delete=False, is_show=True, pk=course_id )
            except Course.DoesNotExist:
                return Response({"message":"对不起,指定商品不存在!"}, status=status.HTTP_400_BAD_REQUEST)

            try:
                course_expire = CourseExpire.objects.get(course=course, expire_time=cart_list.get(course_id_byte))
                expire_text = course_expire.expire_text
                price = course_expire.price
            except CourseExpire.DoesNotExist:
                expire_text = "永久有效"
                price = course.price

            data.append({
                "id": course_id,
                "name": course.name,
                "course_img": settings.DOMAIL_IMAGE_URL + course.course_img.url,
                "expire": expire_text,
                "real_price": course.real_price(price),
                "price": price,
                "discount_name": course.discount_name
            })

        return Response(data)

需要修改courses/models.py中的Course的类方法real_price返回真实价格:

def real_price(self,price=None):
        """计算课程的真实价格"""
        if price is None:
            """如果计算真实价格额时候,函数没有指定计算价格,则使用永久价格来进行计算"""
            price = float(self.price)
        price = float(price)
        course_price_discount_list = self.activeprices.filter(is_show=True,is_delete=False).first()
        if course_price_discount_list is None:
            """查找不到当前商品课程参与的活动,则表示没有参加活动,直接返回原价"""
            return "%.2f" % price
        ...

前端完成页面从购物车页面跳转到结算页面。

components/Cart.vue代码:

<span class="goto_pay"><router-link to="/order">去结算</router-link></span>

增加显示结算页面的路由和组件代码:

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

// 这里导入可以让让用户访问的组件
// vue 中提供了@符号,表示src路径
...
import Order from "@/components/Order"
Vue.use(Router);

export default new Router({
  // 设置路由模式为‘history’,去掉默认的#
  mode: "history",
  routes:[
    // 路由列表
    {
      ...
    {
      path:"/order",
      name:"Order",
      component: Order
    }
  ]
});

components/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">
          <el-row>
             <el-col :span="2" class="checkbox">  </el-col>
             <el-col :span="10" class="course-info">
               <img src="../../static/image/course-cover.jpeg" alt="">
                <span>python入门</span>
             </el-col>
             <el-col :span="8"><span>永久有效</span></el-col>
             <el-col :span="4" class="course-price">¥99.50</el-col>
           </el-row>
        </div>
        <div class="cart-item">
          <el-row>
             <el-col :span="2" class="checkbox">  </el-col>
             <el-col :span="10" class="course-info">
               <img src="../../static/image/course-cover.jpeg" alt="">
                <span>python入门</span>
             </el-col>
             <el-col :span="8"><span>永久有效</span></el-col>
             <el-col :span="4" class="course-price">¥99.50</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/image/alipay2.png" alt=""></span>
                <span class="alipay wechat"><img src="../../static/image/wechat.png" alt=""></span>
              </el-col>
              <el-col :span="8" class="count">实付款: <span>¥99.50</span></el-col>
              <el-col :span="4" class="cart-pay"><span>支付宝支付</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 {
      }
    },
    components:{
      Header,
      Footer,
    },
    created(){

    },
    methods: {

    }
  }
</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;
  margin-bottom: 30px;
}
.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>

在组件中获取购物车勾选商品的数据,

<template>
  <div class="cart">
    <Header/>
    <div class="cart-info">
        <h3 class="cart-top">购物车结算 <span>共{{course_list.length}}门课程</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" :key="key" v-for="course,key in course_list">
          <el-row>
             <el-col :span="2" class="checkbox">  </el-col>
             <el-col :span="10" class="course-info">
               <img :src="course.course_img" alt="">
               <div class="course_name">
                 {{course.name}}
                 <span class="discount_name">{{course.discount_name}}</span>
               </div>
             </el-col>
             <el-col :span="8"><span>{{course.expire}}</span></el-col>
             <el-col :span="4" class="course-price">
               ¥{{course.real_price}}
               <span class="original-price">原价 ¥{{course.price}}</span>
             </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/image/alipay2.png" alt=""></span>
                <span class="alipay wechat"><img src="../../static/image/wechat.png" alt=""></span>
              </el-col>
              <el-col :span="8" class="count">实付款: <span>¥{{get_total()}}</span></el-col>
              <el-col :span="4" class="cart-pay"><span>支付宝支付</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 {
          course_list:[], // 勾选商品
      }
    },
    components:{
      Header,
      Footer,
    },
    created(){
      this.check_user_login();
      this.get_selected_course();
    },
    methods: {
      check_user_login(){
        // 检查用户是否登录了
        let user_token = localStorage.user_token || sessionStorage.user_token;
        if( !user_token ){
            // 判断用户是否登录了
            this.$confirm("对不起,您尚未登录!请登录后继续操作!","警告").then(()=>{
                this.$router.push("/user/login");
            });
        }
        return user_token;
      },
      get_selected_course(){
          // 获取购物车中的勾选商品
                  // 获取购物车的勾选商品信息
        this.$axios.get(`${this.$settings.Host}/cart/course/selected/`,{
          headers:{
            "Authorization":"jwt " + this.check_user_login(),
          }
        }).then(response=>{
          this.course_list = response.data;
        }).catch(error=>{
          console.log(error.response);
        });
      },
      get_total(){
          // 计算总价格
          let total = 0;

          for(let key in this.course_list){
              total += parseFloat(this.course_list[key].real_price);
          }

          return total.toFixed(2);
      }
    }
  }
</script>

<style>

...

.course-price{
    line-height: 28px;
    padding-top: 30px;
}
.course-price .original-price{
    display: block;
    text-decoration: line-through;
    color: #9b9b9b;
}
.course-info img{
    float: left;
}
.course-info .course_name{
    float: left;
    line-height: 28px;
    padding-top: 30px;
}
.course-info .course_name .discount_name{
  display: block;
  height: 14px;
  color: #ffc210;
}
</style>

完成了商品信息展示以后,我们把优惠券功能和积分功能延后处理,先完成订单的生成.

所以为了方便开发,和以后项目的维护,我们再次创建子应用orders来完成接下来的订单和订单支付功能。

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

注册子应用,settings/dev.py,代码:

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

订单模型

订单模型分析:

订单模型:  优惠券ID,积分兑换数量,订单总价格,订单标题,订单支付时间,用户ID,支付状态,订单有效时间,订单号,支付方式,
    订单详情模型: 商品ID,商品原价,商品实价,商品有效期,商品优惠方式
    用户购买商品记录: 
    优惠券模型:
    积分流水模型:

为什么有订单号?

原因是支付平台需要记录每一个商家的资金流水,所以需要我们这边提供一个足够复杂的流水号和支付平台保持一致。
所以订单号是支付平台那边强制要求在支付时提供给平台的。

订单模型的代码:
orders/models.py

from django.db import models
from luffyapi.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 = (
        (1, '支付宝'),
        (2, '微信支付'),
    )
    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=1, verbose_name="支付方式")
    credit = models.IntegerField(default=0, verbose_name="使用的积分数量")
    coupon = models.IntegerField(default=0, verbose_name="用户优惠券ID")
    order_desc = models.TextField(max_length=500, 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)


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='0', 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,null=True, default="",blank="", verbose_name="优惠类型")
    class Meta:
        db_table="ly_order_detail"
        verbose_name= "订单详情"
        verbose_name_plural= "订单详情"

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

数据迁移:

python manage.py makemigrations
python manage.py migrate
把当前子应用注册到xadmin中

在当前子应用下创建orders/adminx.py,代码:

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接口

orders/views.py,代码:

from rest_framework.generics import CreateAPIView
from .models import Order,OrderDetail
from .serializers import OrderModelSerializer
class OrderAPIView(CreateAPIView):
    queryset = Order.objects.filter(is_delete=False, is_show=True)
    serializer_class = OrderModelSerializer

orders/serializers.py,代码:

from rest_framework import serializers
from .models import Order,OrderDetail
from datetime import datetime
import random
from django_redis import get_redis_connection
from courses.models import Course,CourseExpire

class OrderModelSerializer(serializers.ModelSerializer):

    class Meta:
        model = Order
        fields = [
            "id", "order_title", "total_price",
            "real_price", "order_number", "order_status",
            "pay_type", "credit",
            "coupon", "pay_time",
        ]
        extra_kwargs = {
            "id": {"read_only": True, },
            "order_title": {"read_only": True, },
            "total_price": {"read_only": True, },
            "real_price": {"read_only": True, },
            "order_number": {"read_only": True, },
            "order_status": {"read_only": True, },
            "pay_time": {"read_only": True, },
            "pay_type": {"required": True, },
            "credit": {"required": True, "min_value": 0},
            "coupon": {"required": True, },
        }

    def create(self, validated_data):
        """生成订单"""
        """1. 先生成订单记录"""
        # 接受客户端提交的数据
        pay_type = validated_data.get("pay_type")
        credit = validated_data.get("credit", 0)
        coupon = validated_data.get("coupon", 0)
        # 生成必要参数
        user_id = 1 # todo 回头我们学习怎么在序列化器中获取视图中的数据
        order_title = "路飞学城课程购买"
        order_number = datetime.now().strftime("%Y%m%d%H%M%S")+("%06d" % user_id)+("%04d" % random.randint(0,9999))
        order_status = 0 # 未支付

        # 生成订单记录
        order = super().create({
            "order_title":order_title,
            "total_price":0,  # 等后面生成订单详情的时候,需要循环购物车中商品时,再计算总价格,再填进来
            "real_price":0,
            "order_number":order_number,
            "order_status":order_status,
            "pay_type": pay_type,
            "credit": credit,
            "coupon": coupon,
            "order_desc": "",
            "user_id": user_id,
            "orders": 0,  # 排序字段
        })

        """2. 再生成订单详情"""
        # 从redis中提取勾选商品
        redis = get_redis_connection("cart")
        # 从购物车中一区订单信息
        course_set = redis.smembers("selected_%s" % user_id )
        cart_list = redis.hgetall("cart_%s" % user_id )

        # 声明订单总价格和订单实价
        total_price = 0

        for course_id_bytes in course_set:
            """在循环中把每一件商品添加订单详情"""
            course_expire_bytes = cart_list[course_id_bytes]
            expire_id = int( course_expire_bytes.decode() )
            course_id = int( course_id_bytes.decode() )

            try:
                course = Course.objects.get(pk=course_id)
            except:
                raise serializers.ValidationError("对不起,商品课程不存在!")


            # 提取课程的有效期选项
            try:
                """有效期选项"""
                course_expire = CourseExpire.objects.get(pk=expire_id)
                price = course_expire.price
            except CourseExpire.DoesNotExist:
                """永久有效"""
                price = course.price


            # 生成订单详情记录
            order_detail = OrderDetail.objects.create(
                order=order,
                course=course,
                expire= expire_id,
                price = price,
                real_price = course.real_price(price),
                discount_name = course.discount_name,
                orders=0,  # 排序字段
            )

            total_price += float(order_detail.real_price)

        # 保存订单的总价格
        order.total_price = total_price
        order.real_price = total_price # todo 暂时先默认总价格为实付价格
        order.save()

        """3. 清除掉购物车中勾选的商品"""


        return order

注册路由,orders/urls.py,代码;

from django.urls import path, re_path
from . import views
urlpatterns = [
    path(r'', views.OrderAPIView.as_view() ),
]

总路由中注册子应用路由:

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

因为在项目运营中,如果在生成订单记录以后,而生成订单详情时发生错误,则会产生空订单的情况,所以我们需要使用数据库的事务来保证数据的一致性。

上面我们使用了redis的事务操作保证数据的一致性。但是mysql里面我们也是在进行多表操作,所以也是需要使用事务来保证数据的一致性的。

事务: 在完成一个整体功能时,操作到了多个表数据,或者同一个表的多条记录,如果要保证这些SQL语句操作作为一个整体保存到数据库中,那么可以使用事务(transation),
	事务具有4个特性,5个隔离等级
  
  四个特性:一致性,原子性,隔离性,持久性
  # 隔离性:两个事务的隔离性,隔离性的修改可以通过数据库的配置文件进行修改
  五个隔离级别: 串行隔离,可重复读,已提交读,未提交读,没有隔离级别
    原子性(Atomicity)
    一致性(Consistency)
    隔离性(Isolation)[事务隔离级别->幻读,脏读]
    持久性(Durability)

  在mysql中有专门的SQl语句来完成事务的操作,事务操作一般有3个步骤:
		设置事务开始  transation start
		事务的处理[增删改]
		设置事务的回滚或者提交 rollback / commit

在 django等web框架中,只要ORM模型,一般都会实现了事务操作封装
所以在django中我们可以直接使用ORM模型提供的事务操作方法即可完成事务的操作

django框架本身就提供了2种事务操作的用法。

django的事务操作方法主要通过django.db.transation模块完成的。

  • 启用事务用法1:
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语句执行完成以后,自动提交事务
            # 数据库操作

在使用事务过程中, 有时候会出现异常,当出现异常的时候,我们需要让程序停止下来,同时需要回滚SQL语句,也就是回滚事务。

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事务操作保证下单过程中的数据一致性

视图代码:
orders/views.py

from rest_framework import serializers
from .models import Order,OrderDetail
from datetime import datetime
import random
from django_redis import get_redis_connection
from courses.models import Course,CourseExpire
from django.db import transaction
class OrderModelSerializer(serializers.ModelSerializer):

    class Meta:
        model = Order
        fields = [
            "id", "order_title", "total_price",
            "real_price", "order_number", "order_status",
            "pay_type", "credit",
            "coupon", "pay_time",
        ]
        extra_kwargs = {
            "id": {"read_only": True, },
            "order_title": {"read_only": True, },
            "total_price": {"read_only": True, },
            "real_price": {"read_only": True, },
            "order_number": {"read_only": True, },
            "order_status": {"read_only": True, },
            "pay_time": {"read_only": True, },
            "pay_type": {"required": True, },
            "credit": {"required": True, "min_value": 0},
            "coupon": {"required": True, },
        }

    def create(self, validated_data):
        """生成订单"""
        """1. 先生成订单记录"""
        # 接受客户端提交的数据
        pay_type = validated_data.get("pay_type")
        credit = validated_data.get("credit", 0)
        coupon = validated_data.get("coupon", 0)
        # 生成必要参数
        user_id = 1 # todo 回头我们学习怎么在序列化器中获取视图中的数据
        order_title = "路飞学城课程购买"
        order_number = datetime.now().strftime("%Y%m%d%H%M%S")+("%06d" % user_id)+("%04d" % random.randint(0,9999))
        order_status = 0 # 未支付

        # 生成订单记录
        with transaction.atomic():
            # 设置SQL语句的回滚位置
            save_id = transaction.savepoint()

            order = super().create({
                "order_title":order_title,
                "total_price":0,  # 等后面生成订单详情的时候,需要循环购物车中商品时,再计算总价格,再填进来
                "real_price":0,
                "order_number":order_number,
                "order_status":order_status,
                "pay_type": pay_type,
                "credit": credit,
                "coupon": coupon,
                "order_desc": "",
                "user_id": user_id,
                "orders": 0,  # 排序字段
            })

            """2. 再生成订单详情"""
            # 从redis中提取勾选商品
            redis = get_redis_connection("cart")
            # 从购物车中一区订单信息
            course_set = redis.smembers("selected_%s" % user_id )
            cart_list = redis.hgetall("cart_%s" % user_id )

            # 声明订单总价格和订单实价
            total_price = 0

            for course_id_bytes in course_set:
                """在循环中把每一件商品添加订单详情"""
                course_expire_bytes = cart_list[course_id_bytes]
                expire_id = int( course_expire_bytes.decode() )
                course_id = int( course_id_bytes.decode() )

                try:
                    course = Course.objects.get(pk=course_id)
                except:
                    transaction.savepoint_rollback(save_id)
                    raise serializers.ValidationError("对不起,商品课程不存在!")


                # 提取课程的有效期选项
                try:
                    """有效期选项"""
                    course_expire = CourseExpire.objects.get(pk=expire_id)
                    price = course_expire.price
                except CourseExpire.DoesNotExist:
                    """永久有效"""
                    price = course.price


                # 生成订单详情记录
                try:
                    order_detail = OrderDetail.objects.create(
                        order=order,
                        course=course,
                        expire= expire_id,
                        price = price,
                        real_price = course.real_price(price),
                        discount_name = course.discount_name,
                        orders=0,  # 排序字段
                    )
                except:
                    transaction.savepoint_rollback(save_id)
                    raise serializers.ValidationError("对不起,订单生成失败!请联系客服工作人员!")

                total_price += float(order_detail.real_price)

            # 保存订单的总价格
            order.total_price = total_price
            order.real_price = total_price # todo 暂时先默认总价格为实付价格
            order.save()

        """3. 清除掉购物车中勾选的商品"""


        return order

一旦购物车中选中的商品被转移到了购物车中,则购物车中原来被选中的商品是否要删除?

要,只需要保留购物车中没有勾选过的商品
orders/serializer.py

from rest_framework import serializers
from .models import Order,OrderDetail
from datetime import datetime
import random
from django_redis import get_redis_connection
from courses.models import Course,CourseExpire
from django.db import transaction
class OrderModelSerializer(serializers.ModelSerializer):

    ...

    def create(self, validated_data):
        """生成订单"""
        ...

        """3. 清除掉购物车中勾选的商品"""
        pip = redis.pipeline()
        pip.multi()
        for course_id_bytes in cart_list:
            if course_id_bytes in course_set:
                pip.hdel("cart_%s" % user_id, course_id_bytes)
                pip.srem("selected_%s" % user_id, course_id_bytes)
        pip.execute()

        return order
前端请求生成订单

Order.vue

<template>
  <div class="cart">
    <Header/>
    <div class="cart-info">
        <h3 class="cart-top">购物车结算 <span>共{{course_list.length}}门课程</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" :key="key" v-for="course,key in course_list">
          <el-row>
             <el-col :span="2" class="checkbox">  </el-col>
             <el-col :span="10" class="course-info">
               <img :src="course.course_img" alt="">
               <div class="course_name">
                 {{course.name}}
                 <span class="discount_name">{{course.discount_name}}</span>
               </div>
             </el-col>
             <el-col :span="8"><span>{{course.expire}}</span></el-col>
             <el-col :span="4" class="course-price">
               ¥{{course.real_price}}
               <span class="original-price">原价 ¥{{course.price}}</span>
             </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" @click="pay_type=1">
                  <img v-if="pay_type==1" src="../../static/image/alipay2.png" alt="">
                  <img v-else src="../../static/image/alipay.png" alt="">
                </span>
                <span class="alipay wechat" @click="pay_type=2">
                  <img v-if="pay_type==2" src="../../static/image/wechat2.png" alt="">
                  <img v-else src="../../static/image/wechat.png" alt="">
                </span>
              </el-col>
              <el-col :span="8" class="count">实付款: <span>¥{{get_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 {
          course_list:[], // 勾选商品
          pay_type: 1,    // 支付方式
          credit: 0,      // 积分
          coupon: 0,      // 优惠券ID,0表示没有使用优惠券
      }
    },
    components:{
      Header,
      Footer,
    },
    created(){
      this.check_user_login();
      this.get_selected_course();
    },
    methods: {
      check_user_login(){
        // 检查用户是否登录了
        let user_token = localStorage.user_token || sessionStorage.user_token;
        if( !user_token ){
            // 判断用户是否登录了
            this.$confirm("对不起,您尚未登录!请登录后继续操作!","警告").then(()=>{
                this.$router.push("/user/login");
            });
        }
        return user_token;
      },
      get_selected_course(){
          // 获取购物车中的勾选商品
                  // 获取购物车的勾选商品信息
        this.$axios.get(`${this.$settings.Host}/cart/course/selected/`,{
          headers:{
            "Authorization":"jwt " + this.check_user_login(),
          }
        }).then(response=>{
          this.course_list = response.data;
        }).catch(error=>{
          console.log(error.response);
        });
      },
      get_total(){
          // 计算总价格
          let total = 0;

          for(let key in this.course_list){
              total += parseFloat(this.course_list[key].real_price);
          }

          return total.toFixed(2);
      },
      payHander(){
          // 生成订单
          this.$axios.post(`${this.$settings.Host}/orders/`,{
              pay_type: this.pay_type,
              credit: this.credit,
              coupon: this.coupon
          },{
              headers:{
                "Authorization":"jwt " + this.check_user_login(),
              }
          }).then(response=>{
              // 下单成功
              console.log(response);

              // 发起支付

          }).catch(error=>{
              console.log(error.response);
          })


      }
    }
  }
</script>

上面实现了订单的生成功能,但是还有2个问题,我们需要在整体前后端流程走通以后来完成的。

userSerializer(instance=模型对象,data=客户端的字典数据, context={request:request,view:view,format:format})
  1. 用户ID怎么在序列化器中接受到
    user = self.context["request"].user.id 原理:drf的序列化器中存储第三个参数 context
  2. 订单生成以后,用户未必会支付,所以我们要设置一个定时的功能来完成订单的超时取消!
    实现这个功能有2种方案:
  • 基于Celery的定时异步任务来完成
  • 在django有一个第三方模块 django-crontab可以提供给我们用于设置定时调用执行函数的功能!
前端请求后端的订单信息

Order.vue

<template>
  <div class="cart">
    <Header/>
    <div class="cart-info">
        <h3 class="cart-top">购物车结算 <span>共{{course_list.length}}门课程</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" :key="key" v-for="course,key in course_list">
          <el-row>
             <el-col :span="2" class="checkbox">  </el-col>
             <el-col :span="10" class="course-info">
               <img :src="course.course_img" alt="">
               <div class="course_name">
                 {{course.name}}
                 <span class="discount_name">{{course.discount_name}}</span>
               </div>
             </el-col>
             <el-col :span="8"><span>{{course.expire}}</span></el-col>
             <el-col :span="4" class="course-price">
               ¥{{course.real_price}}
               <span class="original-price">原价 ¥{{course.price}}</span>
             </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" @click="pay_type=1">
                  <img v-if="pay_type==1" src="../../static/image/alipay2.png" alt="">
                  <img v-else src="../../static/image/alipay.png" alt="">
                </span>
                <span class="alipay wechat" @click="pay_type=2">
                  <img v-if="pay_type==2" src="../../static/image/wechat2.png" alt="">
                  <img v-else src="../../static/image/wechat.png" alt="">
                </span>
              </el-col>
              <el-col :span="8" class="count">实付款: <span>¥{{get_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 {
          course_list:[], // 勾选商品
          pay_type: 1,    // 支付方式
          credit: 0,      // 积分
          coupon: 0,      // 优惠券ID,0表示没有使用优惠券
      }
    },
    components:{
      Header,
      Footer,
    },
    created(){
      this.check_user_login();
      this.get_selected_course();
    },
    methods: {
      check_user_login(){
        // 检查用户是否登录了
        let user_token = localStorage.user_token || sessionStorage.user_token;
        if( !user_token ){
            // 判断用户是否登录了
            this.$confirm("对不起,您尚未登录!请登录后继续操作!","警告").then(()=>{
                this.$router.push("/user/login");
            });
        }
        return user_token;
      },
      get_selected_course(){
          // 获取购物车中的勾选商品
                  // 获取购物车的勾选商品信息
        this.$axios.get(`${this.$settings.Host}/cart/course/selected/`,{
          headers:{
            "Authorization":"jwt " + this.check_user_login(),
          }
        }).then(response=>{
          this.course_list = response.data;
        }).catch(error=>{
          console.log(error.response);
        });
      },
      get_total(){
          // 计算总价格
          let total = 0;

          for(let key in this.course_list){
              total += parseFloat(this.course_list[key].real_price);
          }

          return total.toFixed(2);
      },
      payHander(){
          // 生成订单
          this.$axios.post(`${this.$settings.Host}/orders/`,{
              pay_type: this.pay_type,
              credit: this.credit,
              coupon: this.coupon
          },{
              headers:{
                "Authorization":"jwt " + this.check_user_login(),
              }
          }).then(response=>{
              // 下单成功
              console.log(response);

              // 发起支付

          }).catch(error=>{
              console.log(error.response);
          })


      }
    }
  }
</script>

优惠券

创建一个coupon子应用.

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

注册子应用

INSTALLED_APPS = [
 

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

coupon/models.py代码:

from django.db import models
from luffyapi.utils.models import BaseModel
# Create your models here.
class Coupon(BaseModel):
    """优惠券"""
    coupon_choices = (
        (1, '减免优惠'),
        (2, '折扣优惠'),
    )
    name = models.CharField(max_length=64, verbose_name="优惠券标题")
    coupon_type = models.SmallIntegerField(choices=coupon_choices, default=1, 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)

Python超市自助结算_python_02

前端在收银台页面中,展示当前优惠券效果页面和积分页面 ,代码:

<template>
  <div class="cart">
    <Header/>
    <div class="cart-info">
        <h3 class="cart-top">购物车结算 <span>共{{course_list.length}}门课程</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" :key="key" v-for="course,key in course_list">
          <el-row>
             <el-col :span="2" class="checkbox">  </el-col>
             <el-col :span="10" class="course-info">
               <img :src="course.course_img" alt="">
               <div class="course_name">
                 {{course.name}}
                 <span class="discount_name">{{course.discount_name}}</span>
               </div>
             </el-col>
             <el-col :span="8"><span>{{course.expire}}</span></el-col>
             <el-col :span="4" class="course-price">
               ¥{{course.real_price}}
               <span class="original-price">原价 ¥{{course.price}}</span>
             </el-col>
           </el-row>
        </div>

        <div class="discount">
          <div id="accordion">
            <div class="coupon-box">
              <div class="icon-box">
                <span class="select-coupon">使用优惠劵:</span>
                <a class="select-icon unselect" :class="use_coupon?'is_selected':''" @click="use_coupon=!use_coupon"><img class="sign is_show_select" src="../../static/image/12.png" alt=""></a>
                <span class="coupon-num">有{{coupon_list.length}}张可用</span>
              </div>
              <p class="sum-price-wrap">商品总金额:<span class="sum-price">0.00元</span></p>
            </div>
            <div id="collapseOne" v-if="use_coupon">
              <ul class="coupon-list"  v-if="coupon_list.length>0">
                <li class="coupon-item disable">
                  <p class="coupon-name">10元优惠券</p>
                  <p class="coupon-condition">满10元可以使用</p>
                  <p class="coupon-time start_time">开始时间:2019-10:01 00:00:00</p>
                  <p class="coupon-time end_time">过期时间:2019-11:01 00:00:00</p>
                </li>
                <li class="coupon-item active">
                  <p class="coupon-name">10元优惠券</p>
                  <p class="coupon-condition">满10元可以使用</p>
                  <p class="coupon-time start_time">开始时间:2019-10:01 00:00:00</p>
                  <p class="coupon-time end_time">过期时间:2019-11:01 00:00:00</p>
                </li>
                <li class="coupon-item">
                  <p class="coupon-name">10元优惠券</p>
                  <p class="coupon-condition">满10元可以使用</p>
                  <p class="coupon-time start_time">开始时间:2019-10:01 00:00:00</p>
                  <p class="coupon-time end_time">过期时间:2019-11:01 00:00:00</p>
                </li>
                <li class="coupon-item">
                  <p class="coupon-name">10元优惠券</p>
                  <p class="coupon-condition">满10元可以使用</p>
                  <p class="coupon-time start_time">开始时间:2019-10:01 00:00:00</p>
                  <p class="coupon-time end_time">过期时间:2019-11:01 00:00:00</p>
                </li>
              </ul>
              <div class="no-coupon" v-if="coupon_list.length<1">
                <span class="no-coupon-tips">暂无可用优惠券</span>
              </div>
            </div>
          </div>
          <div class="credit-box">
            <label class="my_el_check_box"><el-checkbox class="my_el_checkbox" v-model="use_credit"></el-checkbox></label>
            <p class="discount-num1" v-if="!use_credit">使用我的贝里</p>
            <p class="discount-num2" v-else><span>总积分:100,已抵扣 ¥0.00,本次花费0积分</span></p>
          </div>
          <p class="sun-coupon-num">优惠券抵扣:<span>0.00元</span></p>
        </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" @click="pay_type=1">
                  <img v-if="pay_type==1" src="../../static/image/alipay2.png" alt="">
                  <img v-else src="../../static/image/alipay.png" alt="">
                </span>
                <span class="alipay wechat" @click="pay_type=2">
                  <img v-if="pay_type==2" src="../../static/image/wechat2.png" alt="">
                  <img v-else src="../../static/image/wechat.png" alt="">
                </span>
              </el-col>
              <el-col :span="8" class="count">实付款: <span>¥{{get_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 {
          course_list:[],     // 勾选商品
          pay_type: 1,        // 支付方式
          use_credit: false,  // 是否使用了优惠券
          credit: 0,          // 积分
          use_coupon: false,  // 优惠券ID,0表示没有使用优惠券
          coupon: 0,          // 优惠券ID,0表示没有使用优惠券
          coupon_list:[1,2,3]      // 优惠券列表
      }
    },
    components:{
      Header,
      Footer,
    },
    created(){
      this.check_user_login();
      this.get_selected_course();
    },
    methods: {
      check_user_login(){
        // 检查用户是否登录了
        let user_token = localStorage.user_token || sessionStorage.user_token;
        if( !user_token ){
            // 判断用户是否登录了
            this.$confirm("对不起,您尚未登录!请登录后继续操作!","警告").then(()=>{
                this.$router.push("/user/login");
            });
        }
        return user_token;
      },
      get_selected_course(){
          // 获取购物车中的勾选商品
                  // 获取购物车的勾选商品信息
        this.$axios.get(`${this.$settings.Host}/cart/course/selected/`,{
          headers:{
            "Authorization":"jwt " + this.check_user_login(),
          }
        }).then(response=>{
          this.course_list = response.data;
        }).catch(error=>{
          console.log(error.response);
        });
      },
      get_total(){
          // 计算总价格
          let total = 0;

          for(let key in this.course_list){
              total += parseFloat(this.course_list[key].real_price);
          }

          return total.toFixed(2);
      },
      payHander(){
          // 生成订单
          this.$axios.post(`${this.$settings.Host}/orders/`,{
              pay_type: this.pay_type,
              credit: this.credit,
              coupon: this.coupon
          },{
              headers:{
                "Authorization":"jwt " + this.check_user_login(),
              }
          }).then(response=>{
              // 下单成功
              console.log(response);

              // 发起支付

          }).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;
  margin-bottom: 30px;
}
.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;
}
.course-price{
    line-height: 28px;
    padding-top: 30px;
}
.course-price .original-price{
    display: block;
    text-decoration: line-through;
    color: #9b9b9b;
}
.course-info img{
    float: left;
}
.course-info .course_name{
    float: left;
    line-height: 28px;
    padding-top: 30px;
}
.course-info .course_name .discount_name{
  display: block;
  height: 14px;
  color: #ffc210;
}

.coupon-box{
  text-align: left;
  padding-bottom: 22px;
  padding-left:30px;
  border-bottom: 1px solid #e8e8e8;
}
.coupon-box::after{
  content: "";
  display: block;
  clear: both;
}
.icon-box{
  float: left;
}
.icon-box .select-coupon{
  float: left;
  color: #666;
  font-size: 16px;
}
.icon-box::after{
  content:"";
  clear:both;
  display: block;
}
.select-icon{
  width: 20px;
  height: 20px;
  float: left;
}
.select-icon img{
  max-height:100%;
  max-width: 100%;
  margin-top: 2px;
  transform: rotate(-90deg);
  transition: transform .5s;
}
.is_show_select{
  transform: rotate(0deg)!important;
}
.coupon-num{
    height: 22px;
    line-height: 22px;
    padding: 0 5px;
    text-align: center;
    font-size: 12px;
    float: left;
    color: #fff;
    letter-spacing: .27px;
    background: #fa6240;
    border-radius: 2px;
    margin-left: 20px;
}
.sum-price-wrap{
    float: right;
    font-size: 16px;
    color: #4a4a4a;
    margin-right: 45px;
}
.sum-price-wrap .sum-price{
  font-size: 18px;
  color: #fa6240;
}

.no-coupon{
  text-align: center;
  width: 100%;
  padding: 50px 0px;
  align-items: center;
  justify-content: center; /* 文本两端对其 */
  border-bottom: 1px solid rgb(232, 232, 232);
}
.no-coupon-tips{
  font-size: 16px;
  color: #9b9b9b;
}
.credit-box{
  height: 30px;
  margin-top: 40px;
  display: flex;
  align-items: center;
  justify-content: flex-end
}
.my_el_check_box{
  position: relative;
}
.my_el_checkbox{
  margin-right: 10px;
  width: 16px;
  height: 16px;
}
.discount-num1{
  color: #9b9b9b;
  font-size: 16px;
  margin-right: 45px;
}
.discount-num2{
  margin-right: 45px;
  font-size: 16px;
  color: #4a4a4a;
}
.sun-coupon-num{
  margin-right: 45px;
  margin-bottom:43px;
  margin-top: 40px;
  font-size: 16px;
  color: #4a4a4a;
  display: inline-block;
}
.sun-coupon-num span{
  font-size: 18px;
  color: #fa6240;
}
.coupon-list{
  margin: 20px 0;
}
.coupon-list::after{
  display: block;
  content:"";
  clear: both;
}
.coupon-item{
  float: left;
  margin: 15px 8px;
  width: 180px;
  height: 100px;
  padding: 5px;
  background-color: #fa3030;
  cursor: pointer;
}
.coupon-list .active{
  background-color: #fa9000;
}
.coupon-list .disable{
  cursor: not-allowed;
  background-color: #fa6060;
}
.coupon-condition{
  font-size: 12px;
  text-align: center;
  color: #fff;
}
.coupon-name{
  color: #fff;
  font-size: 24px;
  text-align: center;
}
.coupon-time{
  text-align: left;
  color: #fff;
  font-size: 12px;
}
.unselect{
  margin-left: 0px;
  transform: rotate(-90deg);
}
.is_selected{
  transform: rotate(-1turn)!important;
}
</style>
在前端实现可以让用户选择对应的支付方式
<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" @click="pay_type=1" v-if="pay_type!=1"><img src="../../static/image/alipay.png" alt="支付宝"></span>
                <span class="alipay" v-if="pay_type==1"><img src="../../static/image/alipay2.png" alt="支付宝"></span>
                <span class="alipay wechat" @click="pay_type=2" v-if="pay_type!=2"><img src="../../static/image/wechat.png" alt="微信支付"></span>
                <span class="alipay wechat" v-if="pay_type==2"><img src="../../static/image/wechat2.png" alt="微信支付"></span>
              </el-col>
              <el-col :span="8" class="count">实付款: <span>¥{{total_real_price}}</span></el-col>
              <el-col :span="4" class="cart-pay"><span @click="payhander">立即支付</span></el-col>
            </el-row>
        </div>