购物车实现
一、切换有效期选项
1.改变课程有效期
要实现课程有效期的计算,则必须我们要清楚一个课程可以有1到多个有效期选项。默认保存在课程模型中的价格如果有值,则这个值是永久有效的购买价格。如果有别的购买期限,则我们需要另行创建一个模型来保存!
course/models.py
...
class CourseExpire(BaseModel):
"""课程有效期模型"""
# 后面可以在数据库把course和expire_time字段设置为联合索引
course = models.ForeignKey("Course", related_name='course_expire', on_delete=models.CASCADE, verbose_name="课程名称")
# 有效期限,天数
expire_time = models.IntegerField(verbose_name="有效期", null=True, blank=True, help_text="有效期按天数计算")
# 一个月有效等等
expire_text = models.CharField(max_length=150, verbose_name="提示文本", null=True, blank=True)
# 每个有效期的价格
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程价格", default=0)
class Meta:
db_table = "ly_course_expire"
verbose_name = "课程有效期"
verbose_name_plural = verbose_name
def __str__(self):
return "课程:%s,有效期:%s,价格:%s" % (self.course, self.expire_text, self.price)
执行迁移
python manage.py makemigrations
python manage.py migrate
添加测试数据
INSERT INTO `ly_course_expire`
(`id`,`orders`,`is_show`,`is_deleted`,`created_time`,`updated_time`,`expire_time`,`expire_text`,`course_id`,`price`)
VALUES
(1,1,1,0,'2019-08-19 02:05:22.368823','2019-08-19 02:05:22.368855',30,'一个月有效',1,398.00),
(2,2,1,0,'2019-08-19 02:05:37.397205','2019-08-19 02:05:37.397233',60,'2个月有效',1,588.00),
(3,3,1,0,'2019-08-19 02:05:57.029411','2019-08-19 02:05:57.029440',180,'半年内有效',1,1000.00),
(4,4,1,0,'2019-08-19 02:07:29.066617','2019-08-19 02:08:29.156730',3,'3天内有效',3,0.88),
(5,3,1,0,'2019-08-19 02:07:46.120827','2019-08-19 02:08:18.652452',30,'1个月有效',3,188.00),
(6,3,1,0,'2019-08-19 02:07:59.876421','2019-08-19 02:07:59.876454',60,'2个月有效',3,298.00);
注册到xadmin中,course/adminx.py,代码:
from .models import CourseExpire
class CourseExpireModelAdmin(object):
"""商品有效期模型"""
pass
xadmin.site.register(CourseExpire, CourseExpireModelAdmin)
在course表中的price字段加一个提示文本
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0,help_text='如果填写的价格为0,那么表示当前课程在购买的时候,没有永久有效的期限。')
course/models.py
...
# 获取课程有效期
def get_expire(self):
expire_list = self.course_expire.all()
data = []
for expire in expire_list:
data.append({
'id':expire.id,
'expire_text':expire.expire_text,
'price':expire.price,
})
# 当价格为0时,没有永久有效这一项,其他的都有
if self.price > 0:
data.append({
'id': 0,
'expire_text': '永久有效',
'price': self.price,
})
return data
...
cart/views.py,新增expire_list
,当前课程有效期的选项列表,代码:
...
cart_data_list.append({
'course_id':course_obj.id,
'name':course_obj.name,
'course_img':contains.SERVER_ADDR + course_obj.course_img.url ,
'price':course_obj.price,
'real_price':course_obj.real_price(),
'expire_id':expire_id,
'expire_list':course_obj.get_expire(),
'selected':False, # 默认没有勾选
})
...
postman测试:
![](https://i-blog.csdnimg.cn/blog_migrate/5fae5a1deae4de7b65def32a1cf877c0.png#pic_center)
客户端中获取到数据以后,展示有效期选项:
CartItem.vue
...
<div class="cart_column column_3">
<el-select v-model="cart.expire_id" size="mini" placeholder="请选择购买有效期" class="my_el_select">
<el-option :label="expire.expire_text" :value="expire.id" :key="expire.id" v-for="(expire,expire_index) in cart.expire_list"></el-option>
</el-select>
</div>
...
注意:在购物车的视图中需要修改一下
cart/views.py
...
cart_data_list.append({
'course_id':course_obj.id,
'name':course_obj.name,
'course_img':contains.SERVER_ADDR + course_obj.course_img.url ,
'price':course_obj.price,
'real_price':course_obj.real_price(),
'expire_id':int(expire_id), #不然它是个字符串,因为redis的问题,只能存为字符串,那么前端渲染下拉框的时候,会多一个选项0.
'expire_list':course_obj.get_expire(),
'selected':False, #默认没有勾选
})
...
有了有效期选项列表以后,我们就可以实现,当用户切换有效期选项以后,同步更新redis中购物车商品的有效期值。
2.根据课程有效期调整价格
路由代码:cart/urls.py
from django.urls import path,re_path
from . import views
urlpatterns = [
path('add_cart/', views.AddCartView.as_view(
{'post':'add','get':'cart_list',
'patch':'change_select','put':'cancel_select',
'delete':'delete_course'
})),
path('expires/', views.AddCartView.as_view({'put':'change_expire','get':'show_pay_info'}))
]
服务端提供切换有效期选项的接口,视图:cart/views.py
...
# 切换有效期
def change_expire(self,request):
user_id = request.user.id
course_id = request.data.get('course_id')
expire_id = request.data.get('expire_id')
try:
course_obj = models.Course.objects.get(id=course_id)
except:
return Response({'msg':'课程不存在'},status=status.HTTP_400_BAD_REQUEST)
try:
if expire_id > 0:
expire_object = models.CourseExpire.objects.get(id=expire_id)
except:
return Response({'msg':'课程有效期不存在'},status=status.HTTP_400_BAD_REQUEST)
real_price = course_obj.real_price(expire_id)
conn = get_redis_connection('cart')
conn.hset('cart_%s' % user_id, course_id, expire_id)
return Response({'msg':'切换成功!', 'real_price': real_price})
...
客户端中,实现axios请求,CartItem.vue,代码:
...
export default {
name: "CartItem",
data(){
return {
// checked:false,
}
},
props:['cart', ],
watch:{
'cart.selected':function (){
// 添加选中
let token = localStorage.token || sessionStorage.token;
if (this.cart.selected){
this.$axios.patch(`${this.$settings.Host}/cart/add_cart/`,{
course_id: this.cart.course_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.$message.success(res.data.msg);
this.$emit('cal_t_p')
}).catch((error)=>{
this.$message.error(res.data.msg);
})
}
else {
// 取消选中
this.$axios.put(`${this.$settings.Host}/cart/add_cart/`,{
course_id: this.cart.course_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.$message.success(res.data.msg);
this.$emit('cal_t_p')
}).catch((error)=>{
this.$message.error(res.data.msg);
})
}
},
'cart.expire_id':function (){
let token = localStorage.token || sessionStorage.token;
this.$axios.put(`${this.$settings.Host}/cart/expires/`,{
course_id: this.cart.course_id,
expire_id:this.cart.expire_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}
).then((res)=>{
this.$message.success(res.data.msg);
// console.log('>>>>', res.data.real_price)
this.cart.real_price = res.data.real_price;
this.$emit('change_expire_handler',)
}).catch((error)=>{
this.$message.error(error.response.data.msg)
})
}
},
...
接下来分2步进行:
- 在用户进入购物车页面时,根据不同的有效期选项,在获取购物车商品列表时,需要查询真实价格。
- 在用户点击切换不同有效期选项时,也要同步切换当前商品课程对应的真实价格
在用户进入页面时,根据不同有效期选项显示对应的真实价格
模型代码:course模型类models.py中的real_price方法需要改一下:
...
# 真实价格计算
def real_price(self,expire_id=0):
price = float(self.price)
if expire_id > 0:
expire_obj = self.course_expire.get(id=expire_id)
price = float(expire_obj.price)
r_price = price
a = self.activity()
if a:
sale = a[0].discount.sale
condition_price = a[0].discount.condition
# 限时免费
if not sale.strip():
r_price = 0
# 限时折扣 *0.5
elif '*' in sale.strip():
if price >= condition_price:
_, d = sale.split('*')
r_price = price * float(d)
# 限时减免 -100
elif sale.strip().startswith('-'):
if price >= condition_price:
_, d = sale.split('-')
r_price = price - float(d)
# 满减
# '''
# 满100-10
# 满300 - 50
# 满600 - 100
# 满200-25
#
# '''
elif '满' in sale:
if price >= condition_price:
l1 = sale.split('\r\n')
dis_list = [] #10 50 25
for i in l1:
a, b = i[1:].split('-')
#400
if price >= float(a):
dis_list.append(float(b))
max_dis = max(dis_list)
r_price = price - max_dis
return r_price
...
视图调用模型中的real_price时,通过传递expire_id,来计算真实价格到购物车商品列表中.
cart/views.py
...
def cart_list(self,request):
# requset.user
# print('>>>>>>>>>>>>>>>', request.user)
# user_id = 1
user_id = request.user.id
conn = get_redis_connection('cart')
conn.delete('selected_cart_%s' % user_id)
ret = conn.hgetall('cart_%s' % user_id) #dict {b'1': b'0', b'2': b'0'}
cart_data_list = []
# print(ret)
try:
for cid, eid in ret.items():
course_id = cid.decode('utf-8')
# expire_id = int(eid.decode('utf-8'))
conn.hset('cart_%s' % user_id, course_id, 0)
expire_id = 0
course_obj = models.Course.objects.get(id=course_id)
cart_data_list.append({
'course_id':course_obj.id,
'name':course_obj.name,
'course_img':contains.SERVER_ADDR + course_obj.course_img.url ,
'price':course_obj.price,
'real_price':course_obj.real_price(),
'expire_id':expire_id,
'expire_list':course_obj.get_expire(),
'selected':False, #默认没有勾选
})
except Exception:
logger.error('获取购物车数据失败')
return Response({'msg':'后台数据库出问题了,请联系管理员'},status=status.HTTP_507_INSUFFICIENT_STORAGE)
# try:
# models.Course.objects.filter()
# print(cart_data_list)
return Response({'msg':'xxx','cart_data_list':cart_data_list})
...
客户端点击切换不同有效期选项时,更新真实价格
在原来切换有效期选项时,直接增加获取真实价格的代码逻辑,cart/views.py,代码:
...
# 切换有效期
def change_expire(self,request):
user_id = request.user.id
course_id = request.data.get('course_id')
expire_id = request.data.get('expire_id')
try:
course_obj = models.Course.objects.get(id=course_id)
except:
return Response({'msg':'课程不存在'},status=status.HTTP_400_BAD_REQUEST)
try:
if expire_id > 0:
expire_object = models.CourseExpire.objects.get(id=expire_id)
except:
return Response({'msg':'课程有效期不存在'},status=status.HTTP_400_BAD_REQUEST)
real_price = course_obj.real_price(expire_id)
conn = get_redis_connection('cart')
conn.hset('cart_%s' % user_id, course_id, expire_id)
return Response({'msg':'切换成功!', 'real_price': real_price})
...
客户端在ajax请求中,补充修改价格,CartItem.vue,代码:
<template>
<div class="cart_item">
<div class="cart_column column_1">
<el-checkbox class="my_el_checkbox" v-model="cart.selected"></el-checkbox>
</div>
<div class="cart_column column_2">
<img :src="cart.course_img" alt="">
<span><router-link to="/course/detail/1">{{cart.name}}</router-link></span>
</div>
<div class="cart_column column_3">
<el-select v-model="cart.expire_id" size="mini" placeholder="请选择购买有效期" class="my_el_select">
<el-option :label="expire.expire_text" :value="expire.id" :key="expire.id" v-for="(expire,expire_index) in cart.expire_list"></el-option>
</el-select>
</div>
<div class="cart_column column_4">¥{{cart.real_price}}</div>
<div class="cart_column column_4" id="delete" @click="delete_course">删除</div>
</div>
</template>
<script>
export default {
name: "CartItem",
data(){
return {
// checked:false,
}
},
props:['cart', ],
watch:{
'cart.selected':function (){
// 添加选中
let token = localStorage.token || sessionStorage.token;
if (this.cart.selected){
this.$axios.patch(`${this.$settings.Host}/cart/add_cart/`,{
course_id: this.cart.course_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.$message.success(res.data.msg);
this.$emit('cal_t_p')
}).catch((error)=>{
this.$message.error(res.data.msg);
})
}
else {
// 取消选中
this.$axios.put(`${this.$settings.Host}/cart/add_cart/`,{
course_id: this.cart.course_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.$message.success(res.data.msg);
this.$emit('cal_t_p')
}).catch((error)=>{
this.$message.error(res.data.msg);
})
}
},
'cart.expire_id':function (){
let token = localStorage.token || sessionStorage.token;
this.$axios.put(`${this.$settings.Host}/cart/expires/`,{
course_id: this.cart.course_id,
expire_id:this.cart.expire_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}
).then((res)=>{
this.$message.success(res.data.msg);
// console.log('>>>>', res.data.real_price)
this.cart.real_price = res.data.real_price;
this.$emit('change_expire_handler',)
}).catch((error)=>{
this.$message.error(error.response.data.msg)
})
}
},
methods:{
delete_course(){
let token = localStorage.token || sessionStorage.token;
this.$axios.delete(`${this.$settings.Host}/cart/add_cart/`,{
params:{
course_id: this.cart.course_id, // request.query_params.get('course_id')
},
headers:{
'Authorization':'jwt ' + token
}
})
.then((res)=>{
this.$message.success(res.data.msg);
this.$emit('delete_course_handler')
})
.catch((error)=>{
this.$message.error(error.response.data.msg);
})
}
}
}
</script>
...
3.统计整个购物车中,勾选所有商品的总价格
有了购物车中各个商品课程的真实价格以后,那么我们就统计计算整个购物车中,勾选所有商品的总价格了。
先看一个foreach
的用法
let a = [11,22,33];
undefined
a.forEach(function(value,key){
console.log(value,key);
});
结果:
11 0
22 1
33 2
Cart.vue
<template>
<div class="cart">
<Vheader></Vheader>
<div class="cart_info">
<div class="cart_title">
<span class="text">我的购物车</span>
<span class="total">共4门课程</span>
</div>
<div class="cart_table">
<div class="cart_head_row">
<span class="doing_row"></span>
<span class="course_row">课程</span>
<span class="expire_row">有效期</span>
<span class="price_row">单价</span>
<span class="do_more">操作</span>
</div>
<div class="cart_course_list">
<CartItem v-for="(value,index) in cart_data_list" :key="index" :cart="value" @cal_t_p="cal_total_price" @change_expire_handler="cal_total_price" @delete_course_handler="delete_c(index)"></CartItem>
</div>
<div class="cart_footer_row">
<span class="cart_select"><label> <el-checkbox v-model="checked"></el-checkbox><span>全选</span></label></span>
<span class="cart_delete"><i class="el-icon-delete"></i> <span>删除</span></span>
<span class="goto_pay"><router-link to="/order/">去结算</router-link></span>
<span class="cart_total">总计:¥{{total_price}}</span>
</div>
</div>
</div>
<Footer></Footer>
</div>
</template>
<script>
import Vheader from "./common/Vheader"
import Footer from "./common/Footer"
import CartItem from "./common/CartItem"
export default {
name: "Cart",
data(){
return {
checked: false,
total_price:0,
cart_data_list:[],
}
},
methods:{
cal_total_price(){
// {
// "name": "flask框架",
// "course_img": "http://www.lyapi.com:8001/media/course/course-cover.jpeg",
// "price": 1110.0,
// "real_price": 0,
// "expire_id": "0",
// 'selected':true,
// }
// {
// "name": "flask框架",
// "course_img": "http://www.lyapi.com:8001/media/course/course-cover.jpeg",
// "price": 1110.0,
// "real_price": 0,
// "expire_id": "0",
// 'selected':false,
// }
let t_price = 0
this.cart_data_list.forEach((v,k)=>{
if (v.selected){
t_price += v.real_price
}
})
console.log('total_price>>>>',t_price)
this.total_price = t_price
},
delete_c(index){
this.cart_data_list.splice(index,1)
this.cal_total_price()
}
},
created() {
let token = sessionStorage.token || localStorage.token;
if (token){
this.$axios.get(`${this.$settings.Host}/cart/add_cart/`,{
headers:{
'Authorization':'jwt ' + token
}
})
.then((res)=>{
this.cart_data_list = res.data.cart_data_list
})
.catch((error)=>{
this.$message.error(error.response.data.msg);
})
}else {
this.$router.push('/user/login');
}
},
components:{
Vheader,
Footer,
CartItem,
}
}
</script>
<style scoped>
.cart_info{
width: 1200px;
margin: 0 auto 50px;
}
.cart_title{
margin: 25px 0;
}
.cart_title .text{
font-size: 18px;
color: #666;
}
.cart_title .total{
font-size: 12px;
color: #d0d0d0;
}
.cart_table{
width: 1170px;
}
.cart_table .cart_head_row{
background: #F7F7F7;
width: 100%;
height: 80px;
line-height: 80px;
padding-right: 30px;
}
.cart_table .cart_head_row::after{
content: "";
display: block;
clear: both;
}
.cart_table .cart_head_row .doing_row,
.cart_table .cart_head_row .course_row,
.cart_table .cart_head_row .expire_row,
.cart_table .cart_head_row .price_row,
.cart_table .cart_head_row .do_more{
padding-left: 10px;
height: 80px;
float: left;
}
.cart_table .cart_head_row .doing_row{
width: 78px;
}
.cart_table .cart_head_row .course_row{
width: 530px;
}
.cart_table .cart_head_row .expire_row{
width: 188px;
}
.cart_table .cart_head_row .price_row{
width: 162px;
}
.cart_table .cart_head_row .do_more{
width: 162px;
}
.cart_footer_row{
padding-left: 36px;
background: #F7F7F7;
width: 100%;
height: 80px;
line-height: 80px;
}
.cart_footer_row .cart_select span{
margin-left: 14px;
font-size: 18px;
color: #666;
}
.cart_footer_row .cart_delete{
margin-left: 58px;
}
.cart_delete .el-icon-delete{
font-size: 18px;
}
.cart_delete span{
margin-left: 15px;
cursor: pointer;
font-size: 18px;
color: #666;
}
.cart_total{
float: right;
margin-right: 62px;
font-size: 18px;
color: #666;
}
.goto_pay{
float: right;
width: 159px;
height: 80px;
outline: none;
border: none;
background: #ffc210;
font-size: 18px;
color: #fff;
text-align: center;
cursor: pointer;
}
</style>
4.切换课程勾选状态或有效期选项时,重新计算总价
在父组件中调用子组件时,声明调用统计总价的方法。
Cart.vue
...
<div class="cart_course_list">
<CartItem v-for="(value,index) in cart_data_list" :key="index" :cart="value" @cal_t_p="cal_total_price" @change_expire_handler="cal_total_price" @delete_course_handler="delete_c(index)"></CartItem>
</div>
...
在子组件中,当用户切换勾选状态和切换有效期时,通过this.$emit()
通知父组件调用cal_total_price
方法
CartItem.vue
...
<script>
export default {
name: "CartItem",
data(){
return {
// checked:false,
}
},
props:['cart', ],
watch:{
'cart.selected':function (){
// 添加选中
let token = localStorage.token || sessionStorage.token;
if (this.cart.selected){
this.$axios.patch(`${this.$settings.Host}/cart/add_cart/`,{
course_id: this.cart.course_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.$message.success(res.data.msg);
this.$emit('cal_t_p')
}).catch((error)=>{
this.$message.error(res.data.msg);
})
}
else {
// 取消选中
this.$axios.put(`${this.$settings.Host}/cart/add_cart/`,{
course_id: this.cart.course_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.$message.success(res.data.msg);
this.$emit('cal_t_p')
}).catch((error)=>{
this.$message.error(res.data.msg);
})
}
},
'cart.expire_id':function (){
let token = localStorage.token || sessionStorage.token;
this.$axios.put(`${this.$settings.Host}/cart/expires/`,{
course_id: this.cart.course_id,
expire_id:this.cart.expire_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}
).then((res)=>{
this.$message.success(res.data.msg);
// console.log('>>>>', res.data.real_price)
this.cart.real_price = res.data.real_price;
this.$emit('change_expire_handler',)
}).catch((error)=>{
this.$message.error(error.response.data.msg)
})
}
},
methods:{
delete_course(){
let token = localStorage.token || sessionStorage.token;
this.$axios.delete(`${this.$settings.Host}/cart/add_cart/`,{
params:{
course_id: this.cart.course_id, // request.query_params.get('course_id')
},
headers:{
'Authorization':'jwt ' + token
}
})
.then((res)=>{
this.$message.success(res.data.msg);
this.$emit('delete_course_handler')
})
.catch((error)=>{
this.$message.error(error.response.data.msg);
})
}
}
}
</script>
...
5.每次刷新有效期都为永久有效
cart/views.py。hset
def cart_list(self,request):
# requset.user
# print('>>>>>>>>>>>>>>>', request.user)
# user_id = 1
user_id = request.user.id
conn = get_redis_connection('cart')
conn.delete('selected_cart_%s' % user_id)
ret = conn.hgetall('cart_%s' % user_id) #dict {b'1': b'0', b'2': b'0'}
cart_data_list = []
# print(ret)
try:
for cid, eid in ret.items():
course_id = cid.decode('utf-8')
# expire_id = int(eid.decode('utf-8'))
conn.hset('cart_%s' % user_id, course_id, 0)
expire_id = 0
course_obj = models.Course.objects.get(id=course_id)
cart_data_list.append({
'course_id':course_obj.id,
'name':course_obj.name,
'course_img':contains.SERVER_ADDR + course_obj.course_img.url ,
'price':course_obj.price,
'real_price':course_obj.real_price(),
'expire_id':expire_id,
'expire_list':course_obj.get_expire(),
'selected':False, #默认没有勾选
})
except Exception:
logger.error('获取购物车数据失败')
return Response({'msg':'后台数据库出问题了,请联系管理员'},status=status.HTTP_507_INSUFFICIENT_STORAGE)
# try:
# models.Course.objects.filter()
# print(cart_data_list)
return Response({'msg':'xxx','cart_data_list':cart_data_list})
二、购物车商品的删除操作
CartItem.vue
<template>
<div class="cart_item">
<div class="cart_column column_1">
<el-checkbox class="my_el_checkbox" v-model="cart.selected"></el-checkbox>
</div>
<div class="cart_column column_2">
<img :src="cart.course_img" alt="">
<span><router-link to="/course/detail/1">{{cart.name}}</router-link></span>
</div>
<div class="cart_column column_3">
<el-select v-model="cart.expire_id" size="mini" placeholder="请选择购买有效期" class="my_el_select">
<el-option :label="expire.expire_text" :value="expire.id" :key="expire.id" v-for="(expire,expire_index) in cart.expire_list"></el-option>
</el-select>
</div>
<div class="cart_column column_4">¥{{cart.real_price}}</div>
<div class="cart_column column_4" id="delete" @click="delete_course">删除</div>
</div>
</template>
<script>
export default {
name: "CartItem",
data(){
return {
// checked:false,
}
},
props:['cart', ],
watch:{
'cart.selected':function (){
// 添加选中
let token = localStorage.token || sessionStorage.token;
if (this.cart.selected){
this.$axios.patch(`${this.$settings.Host}/cart/add_cart/`,{
course_id: this.cart.course_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.$message.success(res.data.msg);
this.$emit('cal_t_p')
}).catch((error)=>{
this.$message.error(res.data.msg);
})
}
else {
// 取消选中
this.$axios.put(`${this.$settings.Host}/cart/add_cart/`,{
course_id: this.cart.course_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.$message.success(res.data.msg);
this.$emit('cal_t_p')
}).catch((error)=>{
this.$message.error(res.data.msg);
})
}
},
'cart.expire_id':function (){
let token = localStorage.token || sessionStorage.token;
this.$axios.put(`${this.$settings.Host}/cart/expires/`,{
course_id: this.cart.course_id,
expire_id:this.cart.expire_id,
},{
headers:{
'Authorization':'jwt ' + token
}
}
).then((res)=>{
this.$message.success(res.data.msg);
// console.log('>>>>', res.data.real_price)
this.cart.real_price = res.data.real_price;
this.$emit('change_expire_handler',)
}).catch((error)=>{
this.$message.error(error.response.data.msg)
})
}
},
methods:{
delete_course(){
let token = localStorage.token || sessionStorage.token;
this.$axios.delete(`${this.$settings.Host}/cart/add_cart/`,{
params:{
course_id: this.cart.course_id, // request.query_params.get('course_id')
},
headers:{
'Authorization':'jwt ' + token
}
})
.then((res)=>{
this.$message.success(res.data.msg);
this.$emit('delete_course_handler')
})
.catch((error)=>{
this.$message.error(error.response.data.msg);
})
}
}
}
</script>
<style scoped>
#delete{
cursor: pointer;
}
/*.cart_item{*/
/* height: 100px;*/
/*}*/
.cart_item::after{
content: "";
display: block;
clear: both;
}
.cart_column{
float: left;
height: 150px;
display: flex;
align-items: center;
}
.cart_item .column_1{
width: 88px;
position: relative;
}
.my_el_checkbox{
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
margin: auto;
width: 16px;
height: 16px;
}
.cart_item .column_2 {
/*padding: 67px 10px;*/
width: 520px;
/*height: 116px;*/
}
.cart_item .column_2 img{
width: 175px;
/*height: 115px;*/
margin-right: 35px;
/*vertical-align: middle;*/
}
.cart_item .column_3{
width: 197px;
position: relative;
padding-left: 10px;
}
.my_el_select{
width: 117px;
height: 28px;
position: absolute;
top: 0;
bottom: 0;
margin: auto;
}
.cart_item .column_4{
/*padding: 67px 10px;*/
/*height: 116px;*/
width: 142px;
/*line-height: 116px;*/
}
</style>
cart/urls.py
from django.urls import path,re_path
from . import views
urlpatterns = [
path('add_cart/', views.AddCartView.as_view(
{'post':'add','get':'cart_list',
'patch':'change_select','put':'cancel_select',
'delete':'delete_course'
})),
path('expires/', views.AddCartView.as_view({'put':'change_expire','get':'show_pay_info'}))
]
cart/views.py
...
# 删除购物车数据
def delete_course(self,request):
user_id = request.user.id
course_id = request.query_params.get('course_id')
print('course_id>>>',course_id)
conn = get_redis_connection('cart')
pipe = conn.pipeline()
pipe.hdel('cart_%s' % user_id, course_id)
pipe.srem('selected_cart_%s' % user_id, course_id)
pipe.execute()
return Response({'msg':'删除成功'})
...
Cart.vue
<template>
<div class="cart">
<Vheader></Vheader>
<div class="cart_info">
<div class="cart_title">
<span class="text">我的购物车</span>
<span class="total">共4门课程</span>
</div>
<div class="cart_table">
<div class="cart_head_row">
<span class="doing_row"></span>
<span class="course_row">课程</span>
<span class="expire_row">有效期</span>
<span class="price_row">单价</span>
<span class="do_more">操作</span>
</div>
<div class="cart_course_list">
<CartItem v-for="(value,index) in cart_data_list" :key="index" :cart="value" @cal_t_p="cal_total_price" @change_expire_handler="cal_total_price" @delete_course_handler="delete_c(index)"></CartItem>
</div>
<div class="cart_footer_row">
<span class="cart_select"><label> <el-checkbox v-model="checked"></el-checkbox><span>全选</span></label></span>
<span class="cart_delete"><i class="el-icon-delete"></i> <span>删除</span></span>
<span class="goto_pay"><router-link to="/order/">去结算</router-link></span>
<span class="cart_total">总计:¥{{total_price}}</span>
</div>
</div>
</div>
<Footer></Footer>
</div>
</template>
<script>
import Vheader from "./common/Vheader"
import Footer from "./common/Footer"
import CartItem from "./common/CartItem"
export default {
name: "Cart",
data(){
return {
checked: false,
total_price:0,
cart_data_list:[],
}
},
methods:{
cal_total_price(){
let t_price = 0
this.cart_data_list.forEach((v,k)=>{
if (v.selected){
t_price += v.real_price
}
})
console.log('total_price>>>>',t_price)
this.total_price = t_price
},
delete_c(index){
this.cart_data_list.splice(index,1)
this.cal_total_price()
}
},
created() {
let token = sessionStorage.token || localStorage.token;
if (token){
this.$axios.get(`${this.$settings.Host}/cart/add_cart/`,{
headers:{
'Authorization':'jwt ' + token
}
})
.then((res)=>{
this.cart_data_list = res.data.cart_data_list
})
.catch((error)=>{
this.$message.error(error.response.data.msg);
})
}else {
this.$router.push('/user/login');
}
},
components:{
Vheader,
Footer,
CartItem,
}
}
</script>
<style scoped>
.cart_info{
width: 1200px;
margin: 0 auto 50px;
}
.cart_title{
margin: 25px 0;
}
.cart_title .text{
font-size: 18px;
color: #666;
}
.cart_title .total{
font-size: 12px;
color: #d0d0d0;
}
.cart_table{
width: 1170px;
}
.cart_table .cart_head_row{
background: #F7F7F7;
width: 100%;
height: 80px;
line-height: 80px;
padding-right: 30px;
}
.cart_table .cart_head_row::after{
content: "";
display: block;
clear: both;
}
.cart_table .cart_head_row .doing_row,
.cart_table .cart_head_row .course_row,
.cart_table .cart_head_row .expire_row,
.cart_table .cart_head_row .price_row,
.cart_table .cart_head_row .do_more{
padding-left: 10px;
height: 80px;
float: left;
}
.cart_table .cart_head_row .doing_row{
width: 78px;
}
.cart_table .cart_head_row .course_row{
width: 530px;
}
.cart_table .cart_head_row .expire_row{
width: 188px;
}
.cart_table .cart_head_row .price_row{
width: 162px;
}
.cart_table .cart_head_row .do_more{
width: 162px;
}
.cart_footer_row{
padding-left: 36px;
background: #F7F7F7;
width: 100%;
height: 80px;
line-height: 80px;
}
.cart_footer_row .cart_select span{
margin-left: 14px;
font-size: 18px;
color: #666;
}
.cart_footer_row .cart_delete{
margin-left: 58px;
}
.cart_delete .el-icon-delete{
font-size: 18px;
}
.cart_delete span{
margin-left: 15px;
cursor: pointer;
font-size: 18px;
color: #666;
}
.cart_total{
float: right;
margin-right: 62px;
font-size: 18px;
color: #666;
}
.goto_pay{
float: right;
width: 159px;
height: 80px;
outline: none;
border: none;
background: #ffc210;
font-size: 18px;
color: #fff;
text-align: center;
cursor: pointer;
}
</style>
结算页面
总Order.vue
<template>
<div class="cart">
<Vheader/>
<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,index) 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="">
<span>{{course.name}}</span>
</el-col>
<el-col :span="8"><span>{{course.expire_text}}</span></el-col>
<el-col :span="4" class="course-price">¥{{course.real_price}}</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/img/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" :class="select_coupon(index,coupon.id)" v-for="(coupon,index) in coupon_list" @click="change_coupon(index,coupon.id)">
<p class="coupon-name">{{coupon.coupon.name}}</p>
<p class="coupon-condition">满{{coupon.coupon.condition}}元可以使用</p>
<p class="coupon-time start_time">开始时间:{{coupon.start_time.replace('T',' ')}}</p>
<p class="coupon-time end_time">过期时间:{{coupon.end_time.replace('T',' ')}}</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>总积分:{{credit}},
<el-input-number v-model="num" @change="handleChange" :min="0" :max="max_credit()" label="描述文字"></el-input-number>,已抵扣 ¥{{num / credit_to_money}},本次花费{{num}}积分</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" v-if="pay_type===0"><img src="../../static/img/alipay2.png" alt=""></span>
<span class="alipay" @click="pay_type=0" v-else><img src="../../static/img/alipay.png" alt=""></span>
<span class="alipay wechat" v-if="pay_type===1"><img src="../../static/img/wechat2.png" alt="" ></span>
<span class="alipay wechat" @click="pay_type=1" v-else><img src="../../static/img/wechat.png" alt=""></span>
</el-col>
<el-col :span="8" class="count">实付款: <span>¥{{ (total_price - num/credit_to_money).toFixed(2)}}</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 Vheader from "./common/Vheader"
import Footer from "./common/Footer"
export default {
name:"Order",
data(){
return {
id: localStorage.id || sessionStorage.id,
order_id:sessionStorage.order_id || null,
course_list: [],
coupon_list: [],
total_price: 0, //加上优惠券和积分计算之后的真实总价
total_real_price: 0,
pay_type:0,
num:0,
current_coupon:0, // 当前选中的优惠券id
use_coupon:false,
use_credit:false,
coupon_obj:{}, // 当前coupon对象 {}
credit:0, // 用户剩余积分
credit_to_money:0, //积分兑换比例
}
},
components:{
Vheader,
Footer,
},
watch:{
use_coupon(){
if (this.use_coupon === false){
this.current_coupon = 0;
}
},
//当选中的优惠券发生变化时,重新计算总价
current_coupon(){
this.cal_total_price();
}
}
,
created(){
this.get_order_data();
this.get_user_coupon();
this.get_credit();
},
methods: {
max_credit(){
let a = parseFloat(this.total_price) * parseFloat(this.credit_to_money);
if (this.credit >= a){
return a
}else {
return parseFloat(this.credit)
}
},
get_credit(){
this.credit = localStorage.getItem('credit') || sessionStorage.getItem('credit')
this.credit_to_money = localStorage.getItem('credit_to_money') || sessionStorage.getItem('credit_to_money')
},
handleChange(value){
if (!value){
this.num = 0
}
console.log(value);
},
cal_total_price(){
if (this.current_coupon !== 0){
let tt = this.total_real_price;
let sales = this.coupon_obj.coupon.sale;
let d = parseFloat(this.coupon_obj.coupon.sale.substr(1));
if (sales[0] === '-'){
tt = this.total_real_price - d
// this.total_real_price -= d
}else if (sales[0] === '*'){
// this.total_real_price *= d
tt = this.total_real_price * d
}
this.total_price = tt;
}
},
// 记录切换的couponid
change_coupon(index,coupon_id){
let current_c = this.coupon_list[index]
if (this.total_real_price < current_c.coupon.condition){
return false
}
let current_time = new Date() / 1000;
let s_time = new Date(current_c.start_time) / 1000
let e_time = new Date(current_c.end_time) / 1000
if (current_time < s_time || current_time > e_time){
return false
}
this.current_coupon = coupon_id;
this.coupon_obj = current_c;
},
select_coupon(index,coupon_id){
// {
// "id": 2,
// "start_time": "2020-11-10T09:03:00",
// "coupon": {
// "name": "五十元优惠券",
// "coupon_type": 1,
// "timer": 30,
// "condition": 50,
// "sale": "-50"
// },
// "end_time": "2020-12-10T09:03:00"
// },
let current_c = this.coupon_list[index]
if (this.total_real_price < current_c.coupon.condition){
return 'disable'
}
let current_time = new Date() / 1000;
let s_time = new Date(current_c.start_time) / 1000
let e_time = new Date(current_c.end_time) / 1000
if (current_time < s_time || current_time > e_time){
return 'disable'
}
if (this.current_coupon === coupon_id){
return 'active'
}
return ''
},
get_order_data(){
let token = localStorage.token || sessionStorage.token;
this.$axios.get(`${this.$settings.Host}/cart/expires/`,{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.course_list = res.data.data;
this.total_real_price = res.data.total_real_price;
this.total_price = res.data.total_real_price;
})
},
get_user_coupon(){
let token = localStorage.token || sessionStorage.token;
this.$axios.get(`${this.$settings.Host}/coupon/list/`,{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.coupon_list = res.data;
}).catch((error)=>{
this.$message.error('优惠券获取错误')
})
},
// 支付
payhander(){
let token = localStorage.token || sessionStorage.token;
this.$axios.post(`${this.$settings.Host}/order/add_money/`,{
"pay_type":this.pay_type,
"coupon":this.current_coupon,
"credit":this.num,
},{
headers:{
'Authorization':'jwt ' + token
}
}).then((res)=>{
this.$message.success('订单已经生成,马上跳转支付页面')
let order_number = res.data.order_number
let token = localStorage.token || sessionStorage.token;
this.$axios.get(`${this.$settings.Host}/payment/alipay/?order_number=${order_number}`,{
headers:{
'Authorization':'jwt ' + token
}
})
.then((res)=>{
// res.data : alipay.trade...?a=1&b=2....
location.href = res.data.url;
})
}).catch((error)=>{
this.$message.error(error.response.data.msg);
})
}
}
}
</script>
<style scoped>
.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{
overflow: hidden;
}
.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;
float: right;
}
.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;
}
.coupon-item p{
margin: 0;
padding: 0;
}
.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>
分几部分来写:
结算页面商品展示
优惠券使用
积分使用
支付
index.js
...
import Order from '@/components/Order'
...
{
path: '/order/',
component: Order
},
...
Cart.vue
...
<span class="goto_pay"><router-link to="/order/">去结算</router-link></span>
...
一、订单的生成
cd luffyapi/apps
python ../../manage.py startapp order
注册子应用,settings/dev.py,代码:
INSTALLED_APPS = [
# 子应用
。。。
'order',
]
order/urls.py
from django.urls import path,re_path
from . import views
urlpatterns = [
path('add_money/',views.OrderView.as_view(),)
]
总urls.py
...
path(r'order/',include('order.urls')),
...
order/views.py
from django.shortcuts import render
from rest_framework.generics import CreateAPIView
# Create your views here.
from . import models
from .serializers import OrderModelSerializer
from rest_framework.permissions import IsAuthenticated
class OrderView(CreateAPIView):
queryset = models.Order.objects.filter(is_deleted=False,is_show=True)
serializer_class = OrderModelSerializer
permission_classes = [IsAuthenticated, ]
cart/views.py
...
# 结算页面数据
def show_pay_info(self,request):
user_id = request.user.id
conn = get_redis_connection('cart')
select_list = conn.smembers('selected_cart_%s' % user_id)
data = []
ret = conn.hgetall('cart_%s' % user_id) # dict {b'1': b'0', b'2': b'0'}
# print(ret)
total_price = 0
total_real_price = 0
for cid, eid in ret.items():
expire_id = int(eid.decode('utf-8'))
if cid in select_list:
course_id = int(cid.decode('utf-8'))
course_obj = models.Course.objects.get(id=course_id)
if expire_id > 0:
expire_obj = models.CourseExpire.objects.get(id=expire_id)
course_real_price = course_obj.real_price(expire_id)
total_real_price += course_real_price
data.append({
'course_id':course_obj.id,
'name':course_obj.name,
'course_img':contains.SERVER_ADDR + course_obj.course_img.url ,
'real_price':course_real_price,
'expire_text':expire_obj.expire_text,
})
else:
course_real_price = course_obj.real_price(expire_id)
total_real_price += course_real_price
data.append({
'course_id': course_obj.id,
'name': course_obj.name,
'course_img': contains.SERVER_ADDR + course_obj.course_img.url,
'real_price': course_real_price,
'expire_text': '永久有效',
})
return Response({'data':data,'total_real_price':total_real_price})
...
cart/urls.py
...
path('expires/', views.AddCartView.as_view({'put':'change_expire','get':'show_pay_info'}))
...
1.订单模型
订单模型分析:
订单模型: 优惠券ID,积分兑换数量,订单总价格,订单标题,订单支付时间,用户ID,订单状态,订单有效时间,订单号,支付方式,
订单详情模型: 商品ID,商品原价,商品实价,商品有效期,商品优惠方式
商品购买记录:
优惠券模型:
积分流水模型:
订单号
支付平台需要记录每一个商家的资金流水,所以需要我们这边提供一个足够复杂的流水号和支付平台保持一致。
所以订单号是支付平台那边强制要求在支付时提供给平台的。
订单号,因为第三方支付平台依靠这个订单号来识别当前订单是否已经支付过的,所以必须唯一。
订单模型的代码:
order/models.py
from django.db import models
# Create your models here.
from lyapi.settings import contains
from lyapi.utils.models import BaseModel
from users.models import User
from course.models import Course,CourseExpire
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=1, verbose_name="支付方式")
credit = models.IntegerField(default=0, verbose_name="使用的积分数量")
coupon = models.IntegerField(null=True, verbose_name="用户优惠券ID")
order_desc = models.TextField(max_length=500, verbose_name="订单描述",null=True,blank=True)
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)
def order_detail_data(self):
order_detail_objs = self.order_courses.all()
data_list = [
]
for order_detail in order_detail_objs:
expire_id = order_detail.expire
if expire_id > 0:
expire_obj = CourseExpire.objects.get(id=expire_id)
expire_text = expire_obj.expire_text
else:
expire_text = '永久有效'
order_dict = {
'course_img':contains.SERVER_ADDR + order_detail.course.course_img.url,
'course_name':order_detail.course.name,
'expire_text':expire_text,
'price':order_detail.price,
'real_price': self.real_price,
'discount_name':order_detail.discount_name,
}
data_list.append(order_dict)
return data_list
# '''
# order_dict:[
# {
# 订单id:'xxx',
# 订单号:'xxxx',
# course_list:[
# {
# 'course_id':1,
# 'course_name':'flask框架'
# },
# {
# 'course_id':1,
# 'course_name':'flask框架'
# },
# ]
# },
# {
# 订单id:'xxx',
# 订单号:'xxxx',
# course_list:[
# {
# 'course_id':1,
# 'course_name':'flask框架'
# },
# {
# 'course_id':1,
# 'course_name':'flask框架'
# },
# ]
# }
#
#
# ]
#
# '''
class OrderDetail(BaseModel):
"""
订单详情
"""
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, verbose_name="订单ID")
course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, verbose_name="课程ID")
expire = models.IntegerField(default='0', verbose_name="有效期周期",help_text="0表示永久有效")
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 __str__(self):
return "%s" % (self.course.name)
数据迁移:
python manage.py makemigrations
python manage.py migrate
把当前子应用注册到xadmin中,order/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)
order/__init__.py
,
default_app_config = "order.apps.OrderConfig"
order/apps.py
from django.apps import AppConfig
class OrderConfig(AppConfig):
name = 'order'
verbose_name = '订单管理'
2.后端实现生成订单的api接口
通过postman 访问成功以后,有出现空单的生成情况,原因一次性生成多条数据记录或者一次性操作多个模型,都有可能产生中途报错的情况,所以我们需要在生成订单时保证多个数据操作的原子性。
事务: 在完成一个整体功能时,操作到了多个表数据,或者同一个表的多条记录,如果要保证这些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语句执行完成以后,自动提交事务
# 数据库操作
在使用事务过程中, 有时候会出现异常,当出现异常的时候,我们需要让程序停止下来,同时需要回滚事务。
order/serializers.py
import datetime
from rest_framework import serializers
from . import models
from django_redis import get_redis_connection
from users.models import User
from course.models import Course
from course.models import CourseExpire
from coupon.models import UserCoupon, Coupon
from django.db import transaction
from lyapi.settings import contains
class OrderModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Order
fields = ['id', 'order_number', 'pay_type', 'coupon', 'credit']
extra_kwargs = {
'id':{'read_only':True},
'order_number':{'read_only':True},
'pay_type':{'write_only':True},
'coupon':{'write_only':True},
'credit':{'write_only':True},
}
def validate(self, attrs):
# 支付方式
pay_type = int(attrs.get('pay_type',0)) #
if pay_type not in [i[0] for i in models.Order.pay_choices]:
raise serializers.ValidationError('支付方式不对!')
coupon_id = attrs.get('coupon', 0)
if coupon_id > 0:
try:
user_conpon_obj = UserCoupon.objects.get(id=coupon_id)
except:
raise serializers.ValidationError('订单创建失败,优惠券id不对')
# condition = user_conpon_obj.coupon.condition
now = datetime.datetime.now().timestamp()
start_time = user_conpon_obj.start_time.timestamp()
end_time = user_conpon_obj.end_time.timestamp()
if now < start_time or now > end_time:
raise serializers.ValidationError('订单创建失败,优惠券不在使用范围内,滚犊子')
credit = attrs.get('credit')
user_credit = self.context['request'].user.credit
if credit > user_credit:
raise serializers.ValidationError('积分超上限了,别乱搞')
return attrs
def create(self, validated_data):
try:
# 生成订单号 [日期,用户id,自增数据]
current_time = datetime.datetime.now()
now = current_time.strftime('%Y%m%d%H%M%S')
user_id = self.context['request'].user.id
conn = get_redis_connection('cart')
num = conn.incr('num')
order_number = now + "%06d" % user_id + "%06d" % num
total_price = 0 #总原价
total_real_price = 0 # 总真实价格
with transaction.atomic(): # 添加事务
sid = transaction.savepoint() #创建事务保存点
# 生成订单
order_obj = models.Order.objects.create(**{
'order_title': '31期订单',
'total_price': 0,
'real_price': 0,
'order_number': order_number,
'order_status': 0,
'pay_type': validated_data.get('pay_type', 0),
'credit': 0,
'coupon': 0,
'order_desc': '女朋友',
'pay_time': current_time,
'user_id': user_id,
# 'user':user_obj,
})
select_list = conn.smembers('selected_cart_%s' % user_id)
ret = conn.hgetall('cart_%s' % user_id) # dict {b'1': b'0', b'2': b'0'}
for cid, eid in ret.items():
expire_id = int(eid.decode('utf-8'))
if cid in select_list:
course_id = int(cid.decode('utf-8'))
course_obj = Course.objects.get(id=course_id)
# expire_text = '永久有效'
if expire_id > 0:
expire_text = CourseExpire.objects.get(id=expire_id).expire_text
# 生成订单详情
models.OrderDetail.objects.create(**{
'order': order_obj,
'course': course_obj,
'expire': expire_id,
'price': course_obj.price,
'real_price': course_obj.real_price(expire_id),
'discount_name': course_obj.discount_name(),
})
total_price += course_obj.price
total_real_price += course_obj.real_price(expire_id)
coupon_id = validated_data.get('coupon')
# condition = 100
# if coupon_id > 0:
try:
user_conpon_obj = UserCoupon.objects.get(id=coupon_id)
except:
transaction.savepoint_rollback(sid)
raise serializers.ValidationError('订单创建失败,优惠券id不对')
condition = user_conpon_obj.coupon.condition
if total_real_price < condition:
transaction.savepoint_rollback(sid)
raise serializers.ValidationError('订单创建失败,优惠券不满足使用条件')
sale = user_conpon_obj.coupon.sale
if sale[0] == '-':
total_real_price -= float(sale[1:])
elif sale[0] == '*':
total_real_price *= float(sale[1:])
order_obj.coupon = coupon_id
# 积分判断
credit = float(validated_data.get('credit',0))
if credit > contains.CREDIT_MONEY * total_real_price:
transaction.savepoint_rollback(sid)
raise serializers.ValidationError('使用积分超过了上线,别高事情')
# 积分计算
total_real_price -= credit / contains.CREDIT_MONEY
order_obj.credit = credit
order_obj.total_price = total_price
order_obj.real_price = total_real_price
order_obj.save()
# 结算成功之后,再清除
# conn = get_redis_connection('cart')
# conn.delete('selected_cart_%s' % user_id)
# print('xxxxx')
except Exception:
raise models.Order.DoesNotExist
# order_obj.order_number = order_number
return order_obj