说明:该篇博客是博主一字一码编写的,实属不易,请尊重原创,谢谢大家!
接着上一篇博客继续往下写 :https://blog.csdn.net/qq_41782425/article/details/89015816
目录
一丶订单生成
二丶订单支付
三丶订单评论
四丶项目部署
五丶项目总结
1.显示订单提交页面
# /order/place
class OrderPlaceView(LoginRequiredMixin, View):
"""提交订单"""
def post(self, request):
"""显示提交订单页面"""
return render(request, 'place_order.html')
url(r"^place$", OrderPlaceView.as_view(), name='place'), # 显示订单页
2.获取提交订单页面中的数据
user = request.user
if not user.is_authenticated():
return redirect(reverse('user:login'))
#1 获取表单中的sku_ids参数
sku_ids = request.POST.getlist("sku_ids") #[11,19,39,41]
#2 校验参数
if not sku_ids:
return redirect(reverse('cart:cart_show'))
conn = get_redis_connection("default")
cart_key = "cart_%d" % user.id
skus = []
total_price = 0 # 总金额
total_count = 0 # 总件数
# 遍历sku_ids获取每个商品的信息
for sku_id in sku_ids:
# 根据商品id获取商品的信息
sku = GoodsSKU.objects.get(id=sku_id)
# 根据商品id获取商品的数量
count = conn.hget(cart_key, sku_id)
# 计算商品的小计
amount = sku.price*int(count)
# 动态保存购买商品的数量和小计
sku.count = count
sku.amount = amount
# 商品信息对象保存到列表中
skus.append(sku)
# 累加金额与件数
total_count += int(count)
total_price += amount
# 运费,一般在实际开发中需要单独创建一张表,当金额超过多少时免运费或者是多少钱,这里直接写死为10块
transit_price = 10
# 实际付款金额
total_pay = total_price + transit_price
# 获取收件人地址
addrs = Address.objects.filter(user=user)
# 组织模板上下文
context = {
'skus':skus,'total_count':total_count,
'total_price':total_price, 'transit_price':transit_price,
'total_pay':total_pay, 'addrs':addrs
}
return render(request, 'place_order.html', context)
- 寄送到:
{% for addr in addrs %}
- {{ addr.addr }} ({{ addr.receiver }} 收) {{ addr.phone }}
{% endfor %}
编辑收货地址
{% for sku in skus %}
- {{ forloop.counter }}
- {{ sku.name }}
- {{ sku.unite }}
- {{ sku.price }}元
- {{ sku.count }}
- {{ sku.amount }}元
{% endfor %}
共{{ total_count }}件商品,总金额{{ total_price }}元
运费:{{ transit_price }}元
实付款:{{ total_pay }}元
{{ addr.addr }} ({{ addr.receiver }} 收) {{ addr.phone }}
3.创建订单
sku_ids = ','.join(sku_ids) # 11,19,39,41
# 组织模板上下文
context = {
'skus':skus,'total_count':total_count,
'total_price':total_price, 'transit_price':transit_price,
'total_pay':total_pay, 'addrs':addrs,
'sku_ids':sku_ids
}
提交订单
$('#order_btn').click(function() {
// 获取用户选择的收货地址和支付方式以及商品id
addr_id = $('input[name="addr_id"]:checked').val();
pay_method = $('input[name="pay_style"]:checked').val();
sku_ids = $(this).attr('sku_ids');
csrf = $('input[name="csrfmiddlewaretoken"]').val();
params = {"addr_id":addr_id, "pay_method":pay_method, "sku_ids":sku_ids, "csrfmiddlewaretoken":csrf};
// 向后端/order/commit地址发送ajax post请求
$.post('/order/commit',params, function (data) {
});
# /order/commit
class OrderCommitView(View):
"""创建订单"""
def post(self, request):
pass
url(r"^commit$", OrderCommitView.as_view(), name='commit'), # 创建订单
user = request.user
if not user.is_authenticated():
return JsonResponse({"errno": 0, "error_msg": "请先登录"})
# 获取参数
addr_id = request.POST.get("addr_id")
pay_method = request.POST.get("pay_method")
sku_ids = request.POST.get("sku_ids")
# 校验参数
if not all([addr_id, pay_method, sku_ids]):
return JsonResponse({"errno": 1, "error_msg": "参数不完整"})
# 判断前端传递过来的支付方式是否存在
if pay_method not in OrderInfo.pay_method.key():
return JsonResponse({"errno": 2, "error_msg": "非法的支付方式"})
# 判断前端传递过来的收货地址是否存在
try:
addr = Address.objects.get(id=addr_id)
except Address.DoesNotExist:
return JsonResponse({"errno": 3, "error_msg": "非法的收货地址"})
#1 自定义order_id 20190408174330+用户id(作为唯一标识)
order_id = datetime.now().strftime("%Y%m%d%H%M%S") + str(user.id)
#2 总件数和总金额初始设置为0
total_count = 0
total_price = 0
#3 运费
transit_price = 10
# todo: 向df_order_info表中插入记录
order = OrderInfo.objects.create(
order_id=order_id, user=user,
addr=addr, pay_method=pay_method,
total_count=total_count, total_price=total_price,
transit_price=transit_price)
conn = get_redis_connection("default")
cart_key = "cart_%d" % user.id
sku_ids = sku_ids.split(',')
for sku_id in sku_ids:
# 获取商品信息
try:
sku = GoodsSKU.objects.get(id=sku_id)
except GoodsSKU.DoesNotExist:
return JsonResponse({"errno": 4, "error_msg": "商品不存在"})
# 从redis数据库中获取用户购买商品的数量
count = conn.hget(cart_key, sku_id)
# todo:向df_order_goods表中添加记录
OrderGoods.objects.create(
order=order,sku=sku,
count=count,price=sku.price)
# todo: 更新商品的库存和销量
sku.stock -= int(count)
sku.sales += int(count)
sku.save()
# todo: 累加计算订单商品的总数量和总价格
amount = sku.price * int(count)
total_count += int(count)
total_price += amount
order.total_count = total_count
order.total_price = total_price
order.save()
conn.hdel(cart_key, *sku_ids) # *sku_ids ---> [1,2,3]变成1,2,3
# 返回正确响应
return JsonResponse({'errno':"ok", 'error_message':'订单创建成功'})
$.post('/order/commit',params, function (data) {
if(data.errno=="ok"){
alert("创建订单成功")
}
else {
alert(data.error_msg)
}
});
4.订单生成使用mysql事务
# todo:判断用户购物车中商品的数量是否小于商品库存
if int(count) > sku.stock:
return JsonResponse({"errno":"5", "error_msg":"商品库存不足"})
from django.db import transaction
@transaction.atomic
def viewfunc(request):
# This code executes inside a transaction.
do_stuff()
@transaction.atomic
def post(self, request):
# 设置事务保存点
save_id = transaction.savepoint()
transaction.savepoint_rollback(save_id)
try:
order = OrderInfo.objects.create(
order_id=order_id, user=user,
addr=addr, pay_method=pay_method,
total_count=total_count, total_price=total_price,
transit_price=transit_price)
# 中间代码省略
order.total_count = total_count
order.total_price = total_price
order.save()
except Exception as e:
# 将整个关于数据操作的代码,放在try里面,任何地方出现异常,立即做事务回滚
transaction.savepoint_rollback(save_id)
return JsonResponse({"errno":6, "error_msg":e })
# 没有出现异常则进行事务的提交操作
transaction.savepoint_commit(save_id)
5.订单并发问题
悲观锁
说明:电商网站在做秒杀活动时,就会出现大量用户对同一商品进行购买,当该秒杀商品库存只有1个时,如果很多顾客同时进行点击购买,就会出现一个库存的商品卖了好几百个情况,导致并发生成订单问题
sku = GoodsSKU.objects.select_for_update().get(id=sku_id)
print("用户id:%d,商品库存:%d"%(user.id, sku.stock))
import time
time.sleep(3)
乐观锁
说明:在查询数据的时候不加锁,在进行数据更新时判断更新时的库存和之前查出数据库的库存是否一致
# todo: 更新商品的库存和销量
orgin_stock = sku.stock
new_stock = orgin_stock - int(count)
new_sales = sku.sales + int(count)
# update df_goods_sku set stock=0, sales=1 where id=8 and stock=1;
res = GoodsSKU.objects.filter(id=sku_id, stock=orgin_stock).update(stock=new_stock, sales=new_sales)
if res == 0:
# 表示没有修改成功,进行事务的回滚操作
transaction.savepoint_rollback(save_id)
return JsonResponse({"errno":6, "error_msg":"下单失败"})
解决方法
①在mysql配置文件中(windows---my.ini linux----mysqld.cnf)添加如下配置读取提交内容
# 设置mysql数据库隔离级别
transaction-isolation = READ-COMMITTED
②在视图中对逻辑代码进行三次尝试判断,当用户1创建订单成功,更新了库存,从头开始去执行查询最新库存进行判断,这样就不会出现在商品库存充足的情况下,多个顾客在同一时间对同一商品进行下单操作,出现只有一个用户下单成功的情况
for sku_id in sku_ids:
for i in range(3):
# 代码省略
if res == 0:
if i == 2: # 第三次尝试如果不成功执行下面代码
# 表示没有修改成功,进行事务的回滚操作
transaction.savepoint_rollback(save_id)
return JsonResponse({"errno":6, "error_msg":"下单失败"})
continue
# 代码省略
# 一次成功则跳出循环
break
1.在订单提交页面点击提交订单按钮,提交订单成功后应该跳转到用户中心---订单页面中
$.post('/order/commit',params, function (data) {
if(data.errno=="ok"){
{#alert("创建订单成功")#}
localStorage.setItem('order_finish',2);
$('.popup_con').fadeIn('fast', function() {
setTimeout(function () {
$('.popup_con').fadeOut('fast', function () {
window.open(data.pay_url)
});
}, 3000)
});
}
else {
alert(data.error_msg)
}
});
2.获取并显示我的订单页面的数据
url(r"^order/(?P\d+)$", UserOrderView.as_view(), name="order"), # 用户中心-订单
我的订单
· 全部订单
# /user/order
class UserOrderView(LoginRequiredMixin, View):
"""用户中心-订单"""
def get(self, request, page):
"""显示页面"""
return render(request, "user_center_order.html", {"page":"order"})
user = request.user
orders = OrderInfo.objects.filter(user=user)
for order in orders:
order_skus = OrderGoods.objects.filter(order_id=order.order_id)
for order_sku in order_skus:
# 商品小计
amount = order_sku.count * order_sku.price
# 动态的添加属性
order_sku.amount = amount
# 动态添加属性
order.order_skus = order_skus
# 对数据进行分页
paginator = Paginator(orders, 2) # Show 25 contacts per page
# 获取页数
try:
page = int(page)
except Exception as e:
page = 1
# 判断用户传递过来的页数,是否小于总页数,大于总页数则设置第一页
if page > paginator.num_pages:
page = 1
order_page = paginator.page(page)
num_pages = paginator.num_pages
if num_pages < 5:
pages = range(1, num_pages + 1)
elif page <= 3:
pages = range(1, 6)
elif num_pages - page <= 2:
pages = range(num_pages - 4, num_pages + 1)
else:
pages = range(page - 2, page + 3)
# 组织模板上下文
context={
"order_page":order_page,"pages":pages,
"page": "order"}
return render(request, "user_center_order.html",context)
全部订单
{% for order in order_page %}
- {{ order.create_time }}
- 订单号:{{ order.order_id }}
- {{ order.order_status }}
{% for order_sku in order.order_skus %}
- {{ order_sku.sku.name }}{{ order_sku.price }}元/{{ order_sku.sku.unite }}g
- {{ order_sku.count }}
- {{ order_sku.amount }}元
{% endfor %}
{% comment %}add过滤器作加法计算{% endcomment %}
{{ order.total_price |add:order.transit_price}}(含运费:{{ order.transit_price }})元
{{ order.order_status }}
去付款
{% endfor %}
{% if order_page.has_previous%}
<上一页
{% endif %}
{% for pindex in pages %}
{% if pindex == order_page.number %}
{{ pindex }}
{% else %}
{{ pindex }}
{% endif %}
{% endfor %}
{% if order_page.has_next %}
下一页>
{% endif %}
# 动态添加属性,保存订单状态
order.status_name = OrderInfo.ORDER_STATUS[order.order_status]
orders = OrderInfo.objects.filter(user=user).order_by('-create_time')
window.location.href = '/user/order/1';
3.接入支付宝进行订单支付
# /order/pay
class OrderPayView(View):
"""订单支付"""
def post(self, request):
pass
user = request.user
if not user.is_authenticated():
return redirect(reverse('user:login'))
# 接收参数
order_id = request.POST.get("order_id")
if not order_id:
return JsonResponse({"errno":1, "error_msg":"参数不完整"})
try:
order = OrderInfo.objects.get(order_id=order_id, user=user, pay_method=3, order_status=1)
except OrderInfo.DoesNotExist:
return JsonResponse({"errno":2, "error_msg":"无效订单"})
# todo: 使用支付宝python SDK工具,调用支付宝支付接口
# 初始化
alipay = AliPay(
appid="", # 应用id
app_notify_url=None, # 默认回调url
app_private_key_path=os.path.join(settings.BASE_DIR, 'apps/df_order/app_private_key.pem'),
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_path=os.path.join(settings.BASE_DIR, 'apps/df_order/alipay_public_key.pem'),
sign_type="RSA2", # RSA 或者 RSA2
debug = True # 默认False
)
# 电脑网站支付,需要跳转到https://openapi.alipay.com/gateway.do? + order_string
total_pay = order.total_price + order.transit_price
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=order_id, #订单编号
total_amount=str(total_pay),
subject=u"天天生鲜<%s>" % order_id,
return_url=None,
notify_url=None # 可选, 不填则使用默认notify url
)
# 构造用户跳转的支付链接地址
pay_url = "https://openapi.alipaydev.com/gateway.do?" + order_string
return JsonResponse({"errno":"ok", "pay_url":pay_url})
url(r"^pay$", OrderPayView.as_view(), name='pay'), # 订单支付
去付款
4.获取支付宝支付结果
$.post('/order/check', params, function (data) {
// 判断用户是否支付成功
if(data.errno=="ok"){
alert("支付成功");
location.reload()
}
else {
alert(data.error_msg)
}
})
# /order/check
class OrderCheckView(View):
"""查看支付宝支付结果"""
def post(self, request):
pass
# 判断用户是否登录
user = request.user
if not user.is_authenticated():
return redirect(reverse('user:login'))
# 接收参数
order_id = request.POST.get("order_id")
# 校验参数
if not order_id:
return JsonResponse({"errno": 1, "error_msg": "参数不完整"})
try:
order = OrderInfo.objects.get(order_id=order_id, user=user, pay_method=3, order_status=1)
except OrderInfo.DoesNotExist:
return JsonResponse({"errno":2, "error_msg":"无效订单"})
# todo: 使用支付宝python SDK工具,调用支付宝支付接口
# 初始化
alipay = AliPay(
appid="", # 应用id
app_notify_url=None, # 默认回调url
app_private_key_path=os.path.join(settings.BASE_DIR, 'apps/df_order/app_private_key.pem'),
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_path=os.path.join(settings.BASE_DIR, 'apps/df_order/alipay_public_key.pem'),
sign_type="RSA2", # RSA 或者 RSA2
debug = True # 默认False
)
while True:
response = alipay.api_alipay_trade_query(order_id)
# 从支付宝返回的响应数据中获取code以及trade_status
code = response.get("code")
trade_status = response.get("trade_status")
if code == '10000' and trade_status == "TRADE_SUCCESS":
# 表示成功
# 获取支付宝交易号
trade_no = response.get("trade_no")
# 更新订单状态
order.trade_no = trade_no
order.order_status = 4 # 待评价
order.save()
# 返回正确响应
return JsonResponse({"errno":"ok", "error_msg":"交易成功"})
elif code == '40004' or (code == '10000' and trade_status == "WAIT_BUYER_PAY"):
# 业务处理失败以及等待买家付款
import time
time.sleep(5) # 休眠5秒再次调用支付宝交易查询接口,重新获取状态码以及支付状态信息
continue
else:
return JsonResponse({"errno":4, "error_msg":"交易失败"})
url(r"^check$", OrderCheckView.as_view(), name='check'), # 订单支付结果
1.显示订单评价页面
$('.oper_btn').each(function () {
// 获取订单状态
status = $(this).attr('status');
if(status == 1){
$(this).text('去支付')
}
else if(status == 4){
$(this).text('去评价')
}
else if(status == 5){
$(this).text('已完成')
}
});
$('.oper_btn').click(function () {
// 获取页面上订单状态
status = $(this).attr('status');
// 获取订单id
order_id = $(this).attr('order_id');
if(status == 1){
// 状态为1表示待支付状态
csrf = $('input[name="csrfmiddlewaretoken"]').val();
params = {"order_id":order_id, "csrfmiddlewaretoken":csrf};
// 向后端接口/order/pay 发送ajax post请求
$.post('/order/pay', params, function (data) {
if(data.errno=="ok"){
// 引导客户到支付界面
window.open(data.pay_url);
// 向/order/check 发起请求查询支付宝支付结果
$.post('/order/check', params, function (data) {
// 判断用户是否支付成功
if(data.errno=="ok"){
alert("支付成功");
location.reload()
}
else {
alert(data.error_msg)
}
})
}
else {
alert(data.error_msg)
}
})
}
else if(status == 4){
// 去评价
location.href = '/order/comment/' + order_id
return false;
}
})
# /order/comment
class OrderCommentView(LoginRequiredMixin, View):
"""订单评价"""
def get(self, request, order_id):
"""显示评论页面"""
pass
user = request.user
# 校验数据
if not order_id:
return redirect(reverse('user:order'))
try:
order = OrderInfo.objects.get(order_id=order_id, user=user)
except OrderInfo.DoesNotExist:
return redirect(reverse("user:order"))
# 获取订单商品信息
order_skus = OrderGoods.objects.filter(order_id=order_id)
for order_sku in order_skus:
# 计算商品的小计
amount = order_sku.count * order_sku.price
# 动态给order_sku增加属性amount,保存商品小计
order_sku.amount = amount
# 动态给order增加属性order_skus, 保存订单商品信息
order.order_skus = order_skus
# 渲染模板
return render(request, "order_comment.html", {"order": order})
url(r"^comment/(?P.+)$", OrderCommentView.as_view(), name='comment'), # 订单评价
订单评价
- {{order.create_time}}
- 订单号:{{order.order_id}}
- {{order.status_name}}
2.处理评论内容
user = request.user
# 校验数据
if not order_id:
return redirect(reverse('user:order'))
try:
order = OrderInfo.objects.get(order_id=order_id, user=user)
except OrderInfo.DoesNotExist:
return redirect(reverse("user:order"))
total_count = request.POST.get("total_count")
total_count = int(total_count)
for i in range(1, total_count + 1):
# 获取评论的商品的id
sku_id = request.POST.get("sku_%d" % i) # sku_1 sku_2
# 获取评论的商品的内容
content = request.POST.get('content_%d' % i, '') # cotent_1 content_2 content_3
try:
order_goods = OrderGoods.objects.get(order=order, sku_id=sku_id)
except OrderGoods.DoesNotExist:
continue
order_goods.comment = content
order_goods.save()
order.order_status = 5 # 已完成
order.save()
return redirect(reverse("user:order", kwargs={"page": 1}))
3.显示评论信息
- 商品介绍
- 评论
- 商品详情:
- {{ sku.goods.detail |safe }}
$('#tag_detail').click(function () {
$('#tag_comment').removeClass('active');
$(this).addClass('active');
$('#tab_detail').show();
$('#tab_comment').hide();
});
$('#tag_comment').click(function () {
$('#tag_detail').removeClass('active');
$(this).addClass('active');
$('#tab_detail').hide();
$('#tab_comment').show();
});
1.使用uwsgi作为项目运行的web服务器
说明:在开发环境中运行项目需要在终端上执行python2 manage.py runserver命令,其中runserver就是django给我们提供的web服务器;uwsgi服务器就是遵循WSGI协议的web服务器
grant all privileges on dailyfresh.* to "taogang"@"%" identified by "123456"
[uwsgi]
#使用nginx连接时使用
#socket=127.0.0.1:8080
#直接做web服务器使用
http=127.0.0.1:8080
#项目目录
chdir=/home/taogang/Desktop/dailyfresh
#项目中wsgi.py文件的目录,相对于项目目录
wsgi-file=dailyfresh/wsgi.py
# 工作进程数
processes=4
threads=2
master=True
pidfile=uwsgi.pid
daemonize=uwsgi.log
virtualenv=/home/taogang/.virtualenvs/django_py2
启动:uwsgi –-ini 配置文件路径 例如:uwsgi –-ini uwsgi.ini
停止:uwsgi --stop uwsgi.pid路径 例如:uwsgi –-stop uwsgi.pid
2.部署架构图
3.配置nginx将用户请求转交给uwsgi
[uwsgi]
#使用nginx连接时使用
socket=127.0.0.1:8080
#直接做web服务器使用
#http=127.0.0.1:8080
4.配置nginx处理静态文件
# 配置收集静态文件路径
STATIC_ROOT = '/usr/www/dailyfresh/static'
python manage.py collectstatic
5.通过nginx调度服务器将用户请求转发到其他地址上
6.nginx配置upstream实现负载均衡
upstream dailyfresh{
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location /static {
# 指定静态文件存放的目录
alias /usr/www/dailyfresh/static;
}
location / {
# 包含uwsgi的请求参数
include uwsgi_params;
# 转交请求给uwsgi
#uwsgi_pass 127.0.0.1:8080;
uwsgin_pass dailyfresh;
location = / {
# 转发到静态资源服务器
proxy_pass http://171.213.28.217;
}
1. 生鲜类产品 B2C PC电脑端网页
2. 功能模块:用户模块 商品模块(首页、 搜索、商品) 购物车模块 订单模块(下单、 支付)
3. 用户模块:注册、登录、激活、退出、个人中心、地址
4. 商品模块:首页、详情、列表、搜索(haystack+whoosh)
5. 购物车: 增加、删除、修改、查询
6. 订单模块:确认订单页面、提交订单(下单)、请求支付、查询支付结果、评论
7. django默认的认证系统 AbstractUser
8. itsdangerous 生成签名的token (序列化工具 dumps loads)
9. 邮件 (django提供邮件支持 配置参数 send_mail)
10. celery (重点 整体认识 异步任务)
11. 页面静态化 (缓解压力 celery nginx)
12. 缓存(缓解压力, 保存的位置、有效期、与数据库的一致性问题)
13. FastDFS (分布式的图片存储服务, 修改了django的默认文件存储系统)
14. 搜索( whoosh 索引 分词)
15. 购物车redis 哈希 历史记录redis list
16. ajax 前端用ajax请求后端接口
17. 事务
18. 高并发的库存问题 (悲观锁、乐观锁)
19. 支付的使用流程
20. nginx (负载均衡 提供静态文件)