确认订单页面

当用户在购物车页面选中需要购买的商品或在商品的详情页面的时候点击直接购买的时候,会转到提交订单的页面。

购物车的页面

08 订单模块_json

商品的详情页面:

08 订单模块_django_02

通过上面用户的两种提交的请求,最终渲染出来的确认订单的页面如下:

08 订单模块_数据_03

要想渲染出来上面的页面,前端需要向后端传送的参数有:

  1 如果用户在商品的详情页面点击直接购买需要向后端传送的参数包括: 商品的sku_id和商品的数量count

  2 如果用户在购物车页面提交订单,那么需要向后端传送的数据只需要商品的sku_id就行。

后端的业务逻辑处理

  1 用post请求接受前端传送的参数

  2 用户必须是登陆的状态

  3 参数的校验,如果商品sku_id为空,则直接返回到购物车页面

  4 查询地址信息和商品信息 ,地址为空把值设为None,商品信息为空返回到购物车页面

  5 业务逻辑处理

    通过count是是否为None判断用户从商品的详情页面提交的数据还是从购物车页面提交过来的数据

      1 如果count的值为空则用户是从购物车中提交过来的数据,每件商品的数量count可以通过遍历传送过来的商品的sku_ids从购物车中获取,把商品添加到购物车中

      2 如果count的值不为空,数据就是从商品的详情页面提交而来,商品的数量就直接使用传送过来的count

  6 通过遍历传送过来sku_id的列表把商品的数量,每个商品对应的总价,商品的总数量和包含运费的总金额当成商品对象的属性添加进去

  7 把封装好数据返回给前端

后端的视图函数额代码如下:

 

from django.shortcuts import render,redirect
from utils.views import LoginRequiredMixin
from django.views.generic import View
from django.core.urlresolvers import reverse
from users.models import Address
from goods.models import GoodsSKU
from django_redis import get_redis_connection

class PlaceOrderView(LoginRequiredMixin, View):
def post(self,request):
# 接受前端传送过来额参数:sku_ids和count
sku_ids = request.POST.getlist('sku_ids')
count = request.POST.get('count')

# 参数的校验
if not sku_ids:
return redirect(reverse('cart:info'))
# 获取用户的地址信息
user = request.user
try:
address= Address.objects.filter(user=user).latest('create_time')
except Address.DoesNotExist:
address=None
# 业务逻辑处理
redis_con = get_redis_connection('default')

skus = [] # 用于传递给页面
total_skus_amount = 0 # 商品总金额
total_count = 0 # 商品总数量
total_amount = 0 # 包含运费的总金额
# 商品信息
if count is None:
cart=redis_con.hgetall('cart_%s' %user.id)
# 数据用户从购物车的页面提交来的
for sku_id in sku_ids:
try:
sku = GoodsSKU.objects.get(id=sku_id)
except GoodsSKU.DoesNotExist:
return redirect(reverse('cart:info'))
sku_id=str(sku_id)
sku_count = cart.get(sku_id.encode())
sku_count = int(sku_count)
# 计算商品的金额
amount = sku.price * sku_count
sku.amount = amount
sku.count = sku_count
skus.append(sku)

# 累计总金额和数量
total_skus_amount += amount
total_count += sku_count
else:
# 用户是从商品的详情页面跳转过来的
for sku_id in sku_ids: # 虽然只有一个商品 但是也是使用列表获取而来 [id]
try:
sku = GoodsSKU.objects.get(id=sku_id)
except GoodsSKU.DoesNotExist:
# 跳转到购物车页面
return redirect(reverse("cart:info"))

try:
count = int(count)
except Exception:
return redirect(reverse("goods:detail", args=(sku_id,)))

# 判断库存
if count >sku.stock:
return redirect(reverse("goods:detail", args=(sku_id,)))

amount = sku.price * count
sku.amount = amount
sku.count = count
skus.append(sku)

total_skus_amount += amount
total_count += count

# 将这个商品放到购物车中,方便用户下单时出现问题,还能从购物车中找到信息
redis_con.hset('cart_%s'%user.id,sku_id,count)

trans_cost = 10 # 运费
total_amount = total_skus_amount + trans_cost

context = {
"skus": skus,
"address": address,
"total_count": total_count,
"total_skus_amount": total_skus_amount,
"total_amount": total_amount,
"trans_cost": trans_cost,
# 下面生成订单信息的时候会用到
"sku_ids": ",".join(sku_ids),
}

return render(request, "place_order.html", context)

View Code

配置请求的路径,在根路径urls中配置

import orders.urls

url(r'^orders/', include(orders.urls, namespace="orders")),

 

在订单的urls

from django.conf.urls import url
from . import views

urlpatterns = [
url(r"^place$", views.PlaceOrderView.as_view(), name="place"),
]

 当用户在购物车中提交:

08 订单模块_django_04

生成的订单页面如下:

08 订单模块_json_05

 

提交订单的页面 (生成订单信息表和订单商品表)

前端的页面如下所示

08 订单模块_json_06

前端任务

  根据后端的视图函数要生成的订单信息表和订单商品表,

  前端需要向后端传送的数据:  user 地址id、支付方式、商品id  "1,2,3,4,5"  与 数量(从购物车中获取)

后端在生成两张表的时候,牵扯到如下的知识点:

  1 事物

  2 高并发

  3 时间函数的使用

1 事务的使用:

 

数据库事务:​​http://python.usyiyi.cn/translate/django_182/topics/db/transactions.html​

from django.db import transaction

save_id = transaction.savepoint() # 创建保存点

transaction.savepoint_rollback(save_id) # 回退(回滚)到保存点

transaction.savepoint_commit(save_id) # 提交保存点

 

2 并发访问控制

 当多个用户同时去抢同一个商品的时候,就有可能会出现库存不足,把一些错误的数据保存到数据库中

 

 

08 订单模块_django_07

解决的方法: 采用悲观锁,采用乐观锁,采用队列,排队下单

悲观锁

SQL语句: select …where . for update

特点:在查询的时候立即上锁 

 

08 订单模块_json_08

乐观锁

查询时不锁数据,提交更改时进行判断

只有满足条件时才会修改数据,就是所有的信息都和最初自己查询的一致时,才会修改

and

 这里采用的是乐观锁

3 时间函数的使用

from django.utils import timezone
order_id=timezone.now() # 获取当前的时间
order_id.strftime("%Y%m%d%H%M%S") # 把时间转换成字符串


strftime 将时间类型转换为字符串
strptime 将字符串转换为时间



python提供的时间模块 datetime time

 

这里用户必须是登陆的,如果未登录,返回json的数据用户未登陆,这里我自定义的装饰器,用来检验登录状态,如果用户未登录,返回json数据

在utils/views中,添加以下代码

from django.http import JsonResponse
from functools import wraps

# 自定义的装饰器,用来检验登录状态,如果用户未登录,返回json数据
def login_required_json(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated():
# 如果用户未登录, 返回json错误信息
return JsonResponse({"code": 1, "message": "用户未登录"})
else:
# 如果用户已登录,则执行视图函数
return view_func(request, *args, **kwargs)
return wrapper


class LoginRequiredJsonMixin(object):
"""要求用户登录的功能补充逻辑, 使用自定义的login_required_json装饰器"""
@classmethod
def as_view(cls, **initkwargs):
view = super(LoginRequiredJsonMixin, cls).as_view(**initkwargs) # 实际上就是调用的django提供的类视图基类的as_view
return login_required_json(view)

 自定义一个装饰器让视图函数支持事物的操作

在utils/views中,添加以下代码

from django.db import transaction


class TransactionAtomicMixin(object):
"""支持事务的操作"""
@classmethod
def as_view(cls, **initkwargs):
view = super(TransactionAtomicMixin, cls).as_view(**initkwargs)
return transaction.atomic(view)

 

视图函数使用的时候通过导入继承上面的两个类就可以了

后端任务

 根据前端传的参数生成订单信息表和商品的订单表

主要的业务逻辑的处理

  1 接受前端传来的订单数据  ( user 地址(address_id)、支付方式(pay_method)、商品sku_ids----》  "1,2,3,4,5"  与 数量(从购物车中获取))

  2 进行参数校验

    1 判断地址是否存在,不存在返回  return JsonResponse({"code": 2, "message": "地址不存在"})

       2  判断支付方式,如果不支持的支付,return JsonResponse({"code": 3, "message": "不支持的支付方式"})

  3 把sku_ids的字符串(‘1,2,3,4,5’)转换成商品的ID列表[1,2,3,4,5]

  4 获取购物车数据

  5 自定义订单编号

  6  创建事务用到的保存点·

  7 生成订单信息表  

  8 遍历商品sku_ids,判断商品信息合理与否的同时保存到订单的商品表

     9 采用乐观锁的方式,更新商品的库存,销量

  10   在订单商品表中保存商品的信息  

    11 更新订单信息表数据,处理总金额总数量·  

 12 提交数据的事务操作

 13 将处理后的购物车数据cart保存到redis中

 14 返回给前端处理的结果, 返回json数据

 

08 订单模块_数据_09

08 订单模块_django_10

from django.shortcuts import render, redirect
from django.views.generic import View
from utils.views import LoginRequiredMixin, LoginRequiredJsonMixin, TransactionAtomicMixin
from django.core.urlresolvers import reverse
from goods.models import GoodsSKU
from django_redis import get_redis_connection
from users.models import Address
from django.http import JsonResponse, HttpResponse
from orders.models import OrderInfo, OrderGoods
from django_redis import get_redis_connection
from django.utils import timezone
from django.db import transaction



class CommitOrderView(LoginRequiredJsonMixin, TransactionAtomicMixin, View):
"""提交订单"""
def post(self, request):
"""接受订单数据, 保存订单"""
# 获取要保存的订单的数据
# user 地址id、支付方式、商品id与 数量(从购物车中获取)
user = request.user
address_id = request.POST.get("address_id")
pay_method = request.POST.get("pay_method") # 支付方式 "1"
sku_ids = request.POST.get("sku_ids") # "1,2,3,4,5"

# 进行校验
# 判断地址是否存在
try:
address = Address.objects.get(id=address_id, user=user)
except Address.DoesNotExist:
return JsonResponse({"code": 2, "message": "地址不存在"})

# 判断支付方式
pay_method = int(pay_method)
if pay_method not in OrderInfo.PAY_METHODS.keys():
return JsonResponse({"code": 3, "message": "不支持的支付方式"})

# 判断商品存在与否
sku_ids = sku_ids.split(",")

# 获取购物车数据
redis_conn = get_redis_connection("default")
cart = redis_conn.hgetall("cart_%s" % user.id)

# 创建一个订单的基本信息数据 OrderInfo 订单商品表的数据会用到这个

# 自定义的订单编号格式 "20171026111111用户id"
order_id = timezone.now().strftime("%Y%m%d%H%M%S") + str(user.id)

# 创建事务用到的保存点
save_id = transaction.savepoint()

try:
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_amount=0,
trans_cost=10, # 运费暂时写死
pay_method=pay_method
)

# 遍历商品,判断商品信息合理与否的同时保存到订单的商品表
total_count = 0
total_amount = 0
for sku_id in sku_ids:
# 对一商品尝试下单三次
for i in range(3):
try:
sku = GoodsSKU.objects.get(id=sku_id)
except GoodsSKU.DoesNotExist:
# 回退到保存点
transaction.savepoint_rollback(save_id)
return JsonResponse({"code": 4, "message": "商品信息有误"})

# 从购物车中获取用户订购的商品的数量
count = cart.get(sku_id.encode())
count = int(count)

# 判断商品的库存够不够
if count > sku.stock:
transaction.savepoint_rollback(save_id)
return JsonResponse({"code": 5, "message": "库存不足"})

new_stock = sku.stock - count
new_sales = sku.sales + count

# 采用乐观锁的方式,更新商品的库存,销量
# update会返回更新成功的数据数目

result = GoodsSKU.objects.filter(id=sku_id, stock=sku.stock).update(stock=new_stock, sales=new_sales)
# update goods_sku set stock=new_stock, sales=new_sales where id=sku_id and stock=sku.stock
if result == 0 and i < 2:
# 表示库存更新失败,下单失败
continue
elif result == 0 and i == 2:
# 表示尝试了三次都失败
transaction.savepoint_rollback(save_id)
return JsonResponse({"code": 6, "message": "下单失败"})

# 在订单商品表中保存商品的信息
OrderGoods.objects.create(
order=order,
sku=sku,
count=count,
price=sku.price,
)

# 计算订单的总金额
total_amount += (sku.price * count)
# 计算订单商品的总数量
total_count += count

# 对这个商品下单成功
break

# 更新订单信息表数据,处理总金额总数量
order.total_amount = total_amount + 10
order.total_count = total_count
order.save()
except Exception as e:
print(e)
# 出现了任何异常信息,都要回滚事务
transaction.savepoint_rollback(save_id)
return JsonResponse({"code": 7, "message": "下单失败"})

# 提交数据的事务操作
transaction.savepoint_commit(save_id)

# 将处理后的购物车数据cart保存到redis中
# sku_ids =[1,2,3,4,5]
#
# redis_conn.hdel("cart_%s" % user.id, 1,2,3,4,5)

redis_conn.hdel("cart_%s" % user.id, *sku_ids)

# 返回给前端处理的结果, 返回json数据
return JsonResponse({"code": 0, "message": "下单成功"})

View Code

 

 

在订单应用的urls中增加url的请求路径:

url(r"^commit$", views.CommitOrderView.as_view(), name="commit"),

 

当用户提交订单的信息成功后,在数据库中查看订单信息表

08 订单模块_json_11

在数据库中查看订单商品表

08 订单模块_数据_12