文章目录
- 结算页面
- 订单模型
- 把当前子应用注册到xadmin中
- 后端实现生成订单的api接口
- 使用django提供的mysql事务操作保证下单过程中的数据一致性
- 前端请求生成订单
- 前端请求后端的订单信息
- 优惠券
- 在前端实现可以让用户选择对应的支付方式
结算页面
完成购物车功能以后,那么我们可以让用户点击“去结算”按钮时,在后端提供一个查询勾选商品的API接口给客户端,展示数据在结算页面中。
后端实现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})
- 用户ID怎么在序列化器中接受到
user = self.context["request"].user.id
原理:drf的序列化器中存储第三个参数context
, - 订单生成以后,用户未必会支付,所以我们要设置一个定时的功能来完成订单的超时取消!
实现这个功能有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)
前端在收银台页面中,展示当前优惠券效果页面和积分页面 ,代码:
<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>