Django项目之Web端电商网站的实战开发(完结)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_41782425/article/details/89059800

说明:该篇博客是博主一字一码编写的,实属不易,请尊重原创,谢谢大家!

接着上一篇博客继续往下写 :https://blog.csdn.net/qq_41782425/article/details/89015816

目录

一丶订单生成

二丶订单支付

三丶订单评论

四丶项目部署

五丶项目总结


一丶订单生成

1.显示订单提交页面

  • step1 在df_order/views中定义类视图post方法,显示订单提交页面
# /order/place
class OrderPlaceView(LoginRequiredMixin, View):
    """提交订单"""
    def post(self, request):
        """显示提交订单页面"""
        return render(request, 'place_order.html')
  • step2 在df_order/urls中定义路由规则
url(r"^place$", OrderPlaceView.as_view(), name='place'), # 显示订单页
  • step3 在place_order.html模板文件中,设置复选框checkbox的name为sku_ids以及value值为sku.id也就是商品id,因为当进行表单提交时,复选框的状态为未勾选状态时,则在headers from data表单数据中不会显示该商品的value值,即可以利用该点再后端获取提交订单页面中所有的数据
<form method="post" action="/order/place">
    <li class="col01"><input type="checkbox" name="sku_ids" value="{{ sku.id }}" checked></li>
    <li class="col04"><input type="submit" value="去结算"></li>
</form>

  • step4 在我的购物车页面点击去结算提交按钮,成功跳转到到提交订单页面

 2.获取提交订单页面中的数据

  • step1 判断用户是否登录状态,如果没有登录则跳转到登录页面
user = request.user
if not user.is_authenticated():
    return redirect(reverse('user:login'))
  • step2 获取表单中的sku_ids数据并进行校验
#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'))
  • step3 获取redis连接对象,通过遍历sku_ids获取商品的信息,并获取数据库中商品的数量,计算出商品的小计,总计和总件数
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
  • step4 设定运费金额,计算订单总金额组织模板上下文,返回给前端模板中
# 运费,一般在实际开发中需要单独创建一张表,当金额超过多少时免运费或者是多少钱,这里直接写死为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)
  • step5 在模板文件place_order.html中进行数据填坑
<div class="common_list_con clearfix">
    <dl>
        <dt>寄送到:</dt>
        {% for addr in addrs %}
        <dd><input type="radio" name="" checked="">{{ addr.addr }} ({{ addr.receiver }} 收) {{ addr.phone }}</dd>
        {% endfor %}
    </dl>
    <a href="{% url 'user:address' %}" class="edit_site">编辑收货地址</a>
</div>
{% for sku in skus %}
<ul class="goods_list_td clearfix">
    <li class="col01">{{ forloop.counter }}</li>
    <li class="col02"><img src="{{ sku.image.url }}"></li>
    <li class="col03">{{ sku.name }}</li>
    <li class="col04">{{ sku.unite }}</li>
    <li class="col05">{{ sku.price }}元</li>
    <li class="col06">{{ sku.count }}</li>
    <li class="col07">{{ sku.amount }}元</li>
</ul>
{% endfor %}
<div class="settle_con">
    <div class="total_goods_count">共<em>{{ total_count }}</em>件商品,总金额<b>{{ total_price }}元</b></div>
    <div class="transit">运费:<b>{{ transit_price }}元</b></div>
    <div class="total_pay">实付款:<b>{{ total_pay }}元</b></div>
</div>
  • step6 测试点击去结算按钮

  • step7 在提交订单页面上的收货地址,应该将数据库中默认的收货地址is_default字段为1的复选框checkbox状态为checked,而不应该全部地址都是勾选状态,所以在place_order.html中进行如下判断显示
<dd><input type="radio" name="addr_id" value="{{ addr.id }}" {% if addr.is_default %}checked{% endif %}>{{ addr.addr }} ({{ addr.receiver }} 收) {{ addr.phone }}</dd>
  • step8 刷新页面 

3.创建订单

  • step1 在提交订单页面,点击提交订单按钮,需要向后端传递提交订单页面中的收获地址和支付方式以及商品的id,即在place_order.html模板文件中需要手动去添加支付方式的value值对应后端订单模型类中的df_order_info表,收获地址前面已经进行设置了,所以不需要设置
<input type="radio" name="pay_style" value="1" checked>
<label class="cash">货到付款</label>
<input type="radio" name="pay_style" value="2">
<label class="weixin">微信支付</label>
<input type="radio" name="pay_style" value="3">
<label class="zhifubao"></label>
<input type="radio" name="pay_style" value="4">
<label class="bank">银行卡支付</label>
  • step2 收货地址和支付方式已经在前端模板中设置好了,最后需要设置的就是商品的id,需要在后端中进行组织好传递到前端模板文件的数据格式,提交订单页面中的商品id已经保存到厚度那中skus列表中了,现在将该列表保存的商品id以逗号进行拼接,传递到前端模板中
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
}
  • step3 在前端模板中提交订单按钮中,定义标签属性sku_ids
<a href="javascript:;" sku_ids={{ sku_ids }} id="order_btn">提交订单</a>
  • step4 在place_order.html模板文件中进行提交订单js代码编写,获取用户选择的收货地址,支付方式以及购买的商品id,并通过ajax post方式向后端发送请求
$('#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) {
        
    });
  • step5 紧接着在后端中定义类视图post方法,配置url路由
# /order/commit
class OrderCommitView(View):
    """创建订单"""
    def post(self, request):
        pass
url(r"^commit$", OrderCommitView.as_view(), name='commit'), # 创建订单
  • step6 判断用户是否登录,获取参数以及校验参数完整性
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": "参数不完整"})
  • step7 判断传递的支付方式以及收货地址是否合法
# 判断前端传递过来的支付方式是否存在
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": "非法的收货地址"})
  • step8 向df_order_info表中插入记录,提交订单会在df_order_info以及df_order_goods这两张表中各产生一条记录,由于df_order_goods表外键为df_order_info表,所以需要先在df_order_info表中产生订单记录,在df_order_info表字段中,还需要去获取order_id,total_count,total_price,transit_price
#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)
  • step9 向df_order_goods表中插入记录,有多少种商品插入多少条记录
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
  • step10 更新订单信息表df_order_info中的总件数和总金额
order.total_count = total_count
order.total_price = total_price
order.save()
  • step11 清除用户购物车中的商品,最后返回给前端正确响应
conn.hdel(cart_key, *sku_ids)  # *sku_ids ---> [1,2,3]变成1,2,3
# 返回正确响应
return JsonResponse({'errno':"ok", 'error_message':'订单创建成功'})
  • step12 在place_order.html模板js中进行打印显示
$.post('/order/commit',params, function (data) {
    if(data.errno=="ok"){
        alert("创建订单成功")
    }
    else {
        alert(data.error_msg)
    }
});
  • step13 创建订单测试

  • step14 查看数据库df_order_info表数据

  • step15 查看数据库df_order_goods表数据

4.订单生成使用mysql事务

  • step1 当某个商品库存只有2件时,有不止一个用户购物车添加了2个该商品,假设第一个用户提交订单,成功后,则后面的用户提交订单时,则应该提示用户商品库存不足
# todo:判断用户购物车中商品的数量是否小于商品库存
if int(count) > sku.stock:
    return JsonResponse({"errno":"5", "error_msg":"商品库存不足"})
  • step2 经过上面的判断后,当商品库存不足顾客订单上的商品数量时,则会提示商品库存不足,即不会在df_order_goods表中插入数据,但是会在df_order_info表中生成订单记录,这样肯定不对,商品不足的情况下即使生成了订单,那么订单的数据肯定是错误的,那么就需要使用事务的特性来帮助我们完成在插入数据时,要么成功,要么撤回,这是mysql事务原子性的体现,在django中使用数据库事务参考---Django数据库事务
from django.db import transaction

@transaction.atomic
def viewfunc(request):
    # This code executes inside a transaction.
    do_stuff()
  • step3 在df_order/views订单提交视图OrderCommitView中对post方法进行装饰,需要导入transaction模块
@transaction.atomic
def post(self, request):
  • step4 设置事务保存点,在进行数据库插入数据之前进行设置
# 设置事务保存点
save_id = transaction.savepoint()
  • step5 在出现异常的时候,进行事务回滚
transaction.savepoint_rollback(save_id)
  • step6 将post函数中关于数据库操作的代码放在try中,当在任何地方出现异常时,全部进行回滚操作
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 })
  • step7 在异常外面进行事务的提交操作
# 没有出现异常则进行事务的提交操作
transaction.savepoint_commit(save_id)

5.订单并发问题

悲观锁

说明:电商网站在做秒杀活动时,就会出现大量用户对同一商品进行购买,当该秒杀商品库存只有1个时,如果很多顾客同时进行点击购买,就会出现一个库存的商品卖了好几百个情况,导致并发生成订单问题

  • step1 使用悲观锁进行订单并发处理,悲观锁获取数据时对数据行了锁定,其他事务要想获取锁,必须等原事务结束,换句话说就是当用户1在做sql查询时,进行加锁,那么期间用户2使用同sql语句进行查询时,需要等待用户1事务提交或者回滚事务结束后解锁,才能进行操作;在数据库中对查询语句加锁就是在后面加上for update,如select * from df_goods_sku where id = 41 for update;在django中则在objects下有个select_for_update方法,等价于上面
sku = GoodsSKU.objects.select_for_update().get(id=sku_id)
  • step2 查看数据库中商品库存前五商品,库存最少的为商品id为8的盒装草莓

  • step3 进行测试,在查询商品信息代码后秒加上以下代码,打印出用户的id和商品库存信息,休眠10秒是为了演示效果,博主使用两个浏览器对商品id为8库存为1的盒装草莓进行购买
print("用户id:%d,商品库存:%d"%(user.id, sku.stock))
import time
time.sleep(3)

  • step4 查看数据库中该商品的库存与销量

乐观锁

说明:在查询数据的时候不加锁,在进行数据更新时判断更新时的库存和之前查出数据库的库存是否一致

  • step1 乐观锁,在更新商品库存时,条件为该商品的id以及商品的库存必须与更新之前的商品库存一致,才进行库存的更新,如update df_goods_sku set stock=0, sales=1 where id=8 and stock=1;当用户1购买商品成功则执行update语句那么就会修改stock的库存为0,那么用户2在购买商品时执行update语句时就不会成功,因为此时的where条件中的stock库存与购买前商品库存不一致导致条件不成立,就不会往下执行
# 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":"下单失败"})
  • step2 进行测试前将数据库中商品id为8的商品库存设置为1

  • step3 进行测试,其中某个账户完成创建订单成功时,则原始库存与更新后的库存肯定不一致,另一个用户在执行到update语句时,则会出现无法修改,因为条件中的库存不等于原始库存

  • step4 当库存不止为1时,也就是说足够两个用户同时下单时,如果以原有库存与现有库存进行sql修改条件判断时,那么也同样会出现一个客户下单成功,另一个客户下单失败

解决方法

在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.在订单提交页面点击提交订单按钮,提交订单成功后应该跳转到用户中心---订单页面中

  • step1 在place_order.html模板文件中,当后端发回来的data数据errno值为ok表示提交成功,那么在js中进行如下编写,在页面显示出订单提交成功的提示,并跳转到用户中心订单页
$.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)
}
});
  • step2 测试,当进行提交订单时,提示提交订单成功并跳转到用户中心订单页

2.获取并显示我的订单页面的数据 

  • step1 因为在我的订单页面中需要用到分页,所以需要修改df_user/urls中订单的正则
url(r"^order/(?P<page>\d+)$", UserOrderView.as_view(), name="order"),  # 用户中心-订单
  • step2 将所有的页面涉及链接到我的订单页面的地址进行修改
<a href="{% url 'user:order' 1 %}">我的订单</a>
<li><a href="{% url 'user:order' 1 %}"{% if page == 'order' %}class="active"{% endif %}>· 全部订单</a></li>
  • step3 在df_user/views中UserOrderView类视图get中接收参数page
# /user/order
class UserOrderView(LoginRequiredMixin, View):
    """用户中心-订单"""
    def get(self, request, page):
        """显示页面"""
        return render(request, "user_center_order.html", {"page":"order"})
  • step4 获取用户的所有订单信息
user = request.user
orders = OrderInfo.objects.filter(user=user)
  • step5 遍历用户订单集获取
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   
  • step6 对数据进行分页显示
# 对数据进行分页
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)
  • step7 组织模板上下文,返回给前端模板
# 组织模板上下文
context={
    "order_page":order_page,"pages":pages,
    "page": "order"}

return render(request, "user_center_order.html",context)
  • step8 在user_center_order.html中进行数据填坑
<div class="right_content clearfix">
        <h3 class="common_title2">全部订单</h3>
        {% for order in order_page %}
        <ul class="order_list_th w978 clearfix">
            <li class="col01">{{ order.create_time }}</li>
            <li class="col02">订单号:{{ order.order_id }}</li>
            <li class="col02 stress">{{ order.order_status }}</li>
        </ul>
        <table class="order_list_table w980">
            <tbody>
                <tr>
                    <td width="55%">
                        {% for order_sku in order.order_skus %}
                        <ul class="order_goods_list clearfix">                  
                            <li class="col01"><img src="{{ order_sku.sku.image.url }}"></li>
                            <li class="col02">{{ order_sku.sku.name }}<em>{{ order_sku.price }}元/{{ order_sku.sku.unite }}g</em></li>
                            <li class="col03">{{ order_sku.count }}</li>
                            <li class="col04">{{ order_sku.amount }}元</li>
                        </ul>
                        {% endfor %}
                    </td>
                    {% comment %}add过滤器作加法计算{% endcomment %}
                    <td width="15%">{{ order.total_price |add:order.transit_price}}(含运费:{{ order.transit_price }})元</td>
                    <td width="15%">{{ order.order_status }}</td>
                    <td width="15%"><a href="#" class="oper_btn">去付款</a></td>
                </tr>
            </tbody>
        </table>
        {% endfor %}
        <div class="pagenation">
            {% if order_page.has_previous%}
            <a href="{% url 'user:order' order_page.previous_page_number %}"><上一页</a>
            {% endif %}
            {% for pindex in pages %}
                {% if pindex == order_page.number %}
                    <a href="{% url 'user:order' pindex %}" class="active">{{ pindex }}</a>
                {% else %}
                    <a href="{% url 'user:order' pindex %}">{{ pindex }}</a>
                {% endif %}
            {% endfor %}
            {% if order_page.has_next %}
            <a href="{% url 'user:order' order_page.next_page_number %}">下一页></a>
            {% endif %}
        </div>
</div>
  • step9 刷新页面成功将数据库数据显示到页面上

  • step10 在上图中会发现订单状态一栏显示的并不是未支付已支付已完成等状态,而是显示的数字1,因为订单创建到数据库中状态栏保存的就是为数字,这里需要进行获取订单状态信息;在视图中向order对象中动态添加对应模型中的状态名
# 动态添加属性,保存订单状态
order.status_name = OrderInfo.ORDER_STATUS[order.order_status]
  • step11 然后紧接着在user_center_order.html模板中将{{ order.order_status }}替换成{{ order.status_name }}即可,刷新页面成功显示出状态名

  • step12 一般来说在我的订单页面中,第一页显示的肯定是最新创建的,所以需要按照创建时间进行排序
orders = OrderInfo.objects.filter(user=user).order_by('-create_time')
  • step13 细节就是在js中,当创建订单成功即跳转到我的订单页,因为之前显示订单页时,没有做分页,不需要传递page页数
window.location.href = '/user/order/1';
  • step14 刷新页面则按照创建订单倒序进行显示,也就是显示出最新创建的订购单

3.接入支付宝进行订单支付

  • step2 网站向支付宝平台发送支付请求,采用的是网络请求方式,下图为应用于支付宝平台秘钥验证导图

  • step3  进入电脑网站支付,查看开发说明文档

# /order/pay
class OrderPayView(View):
    """订单支付"""
    def post(self, request):
        pass
  • step5 判断用户是否登录并接收参数
user = request.user
if not user.is_authenticated():
    return redirect(reverse('user:login'))
# 接收参数
order_id = request.POST.get("order_id")
  • step6 校验参数
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":"无效订单"})
  • step7 调用支付宝接口,并传递必要参数,最后向模板返回响应数据
# 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})
  • step8 配置支付路由
url(r"^pay$", OrderPayView.as_view(), name='pay'), # 订单支付
  • step9 在user_center_order.html模板中编写js,需要在去支付按钮标签中获取订单编号以及订单的状态,当状态为1表示该订单为待支付状态,此时才会向后端接口发送ajax post请求,后端返回正确响应,则引导客户到支付宝支付界面,进行支付
<td width="15%"><a href="#" order_id="{{ order.order_id }}" status="{{ order.order_status }}" class="oper_btn">去付款</a></td>
<script>
    $('.oper_btn').click(function () {
        // 获取页面上订单状态
        status = $(this).attr('status')
        if(status == 1){
            // 状态为1表示待支付状态
            // 获取订单id
            order_id = $(this).attr('order_id');
            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)
                }
                else {
                    alert(data.error_msg)
                }
            })
        }
    })
</script>
  • step10 测试点击去付款跳转到支付宝支付页面

  • step11 支付测试利用沙箱买家账号进行支付测试

4.获取支付宝支付结果

  • step1 在项目中使用支付宝整个流程图

  • step2 在user_center_order.html模板中当打开支付界面后,即向/order/check后端接口发送ajax post请求,传递参数为订单号order_id,在后端接口中调用支付宝检查支付结果的接口,获取支付结果信息返回正确响应到前端模板页面中
$.post('/order/check', params, function (data) {
    // 判断用户是否支付成功
    if(data.errno=="ok"){
        alert("支付成功");
        location.reload()
    }
    else {
        alert(data.error_msg)
    }
})
  • step3 在df_order/views中定义类视图post方法
# /order/check
class OrderCheckView(View):
    """查看支付宝支付结果"""
    def post(self, request):
        pass
  • step4 判断用户是否登录获取参数校验参数以及对支付宝工具初始化,跟前面写的支付宝支付视图逻辑一样
# 判断用户是否登录
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
)
  • step5 调用sdk工具中支付宝交易查询接口,根据返回的response数据中的code接口状态码以及支付状态trade_status来判断支付是否成功
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":"交易失败"})
  • step6 配置urls路由
url(r"^check$", OrderCheckView.as_view(), name='check'), # 订单支付结果
  • step7 进行订单支付,查看返回的支付结果,以及订单状态

三丶订单评论

1.显示订单评价页面

  • step1 当订单支付成功后,订单状态变为待评价,但该订单却还显示去支付按钮

  • step2 将已经支付成功的订单对应显示出去评价按钮,在js中进行遍历判断status状态,设置对应状态对应的功能
$('.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('已完成')
    }
});
  • step3 刷新页面,显示正确订单状态对应的功能按钮

  • step4 当用户点击去评价时,在点击事件中,判断如果status状态为4,则跳转到评论地址,传递参数order_id,博主在测试时候发现location.href无法跳转,解决方法就是在下面添加return false即可
$('.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;
    }
})
  • step5 在df_order/views中定义类视图get方法,显示评论页面
# /order/comment
class OrderCommentView(LoginRequiredMixin, View):
    """订单评价"""
    def get(self, request, order_id):
        """显示评论页面"""
        pass
  • step6 校验参数,参数不存在或者参数不正确则跳转到订单页面
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"))
  •  step7 获取订单商品信息,遍历商品信息,获取每个商品的小计,动态给订单对象添加属性
# 获取订单商品信息
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})
  • step8 在df_order/urls中配置视图url
url(r"^comment/(?P<order_id>.+)$", OrderCommentView.as_view(), name='comment'), # 订单评价
  • step9  在将user_center_order.html复制到templates目录下命名为order_comment.html,进行如下编写
<div class="right_content clearfix">
    <h3 class="common_title2">订单评价</h3>
        <ul class="order_list_th w978 clearfix">
            <li class="col01">{{order.create_time}}</li>
            <li class="col02">订单号:{{order.order_id}}</li>
            <li class="col02 stress">{{order.status_name}}</li>
        </ul>
    <form method="post">
        {% csrf_token %}
        {# 订单id #}
        <input type="hidden" name="order_id" value="{{order.order_id}}">
        {# 订单中有几个商品 #}
        <input type="hidden" name="total_count" value="{{order.order_skus|length}}">
        {% for order_sku in order.order_skus %}
        <table class="order_list_table w980">
            <tbody>
                <tr>
                    <td width="80%">
                        <ul class="order_goods_list clearfix">
                            <li class="col01"><img src="{{ order_sku.sku.image.url }}"></li>
                            <li class="col02">{{order_sku.sku.name}}<em>{{order_sku.price}}/{{order_sku.sku.unite}}</em></li>
                            <li class="col03">{{order_sku.count}}</li>
                        </ul>
                    </td>
                    <td width="20%">{{order_sku.amount}}元</td>
                </tr>
            </tbody>
        </table>
        <div class="site_con">
            <input type="hidden" name="sku_{{forloop.counter}}" value="{{order_sku.sku.id}}">
            <div class="form_group form_group2">
                <label>评价内容:</label>
                <textarea class="site_area" name="content_{{forloop.counter}}"></textarea>
            </div>
        </div>
        {% endfor %}
        <input type="submit" name="" value="提交" class="info_submit">
    </form>
</div>
  • step10 点击去评价功能按钮,测试显示订单评价页面

2.处理评论内容

  • step1 在OrderCommentView类视图中定义post方法,在方法中对参数进行校验
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"))
  • step2 获取表单提交中的评论条数
total_count = request.POST.get("total_count")
total_count = int(total_count)
  • step3 循环遍历获取订单中商品的评论内容,设置df_order_goods表中的comment字段的内容为客户填写的评论内容content
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()
  • step4 最后设置订单状态为5也就是已完成状态,重定向到订单页面第一页
order.order_status = 5  # 已完成
order.save()
return redirect(reverse("user:order", kwargs={"page": 1}))
  • step5 测试订单评论功能

  • step6 查看上一步中评论的商品详情信息,刚商品的评论信息显示到了商品介绍一栏,正确来说应该在评论里面

3.显示评论信息

  • step1 在商品详情页面(detail.html)模板文件中,li标签模块商品介绍以和评论,分别添加id属性;同时在div标签中分别添加id属性,需要主要的是评论内容默认是不显示的,所以在.tab_content标签中添加style="display: none"
<ul class="detail_tab clearfix">
    <li id="tag_detail" class="active">商品介绍</li>
    <li id="tag_comment">评论</li>
</ul>
<div class="tab_content" id="tab_detail">
    <dl>
        <dt>商品详情:</dt>
        <dd>{{ sku.goods.detail |safe }}</dd>
    </dl>
</div>
<div class="tab_content" id="tab_comment" style="display: none">
    <dl>
        {% for comment in sku_orders_comment %}
        <dt>评论时间:{{ comment.update_time }}&nbsp;&nbsp;{{ comment.order.user.username }}</dt>
        <dd>评论内容:{{ comment.comment |safe }}</dd>
        {% endfor %}
    </dl>
</div>
  • step2 刷新页面后,商品介绍li模块中不再显示出评论内容

  • step3 添加如下js控制商品介绍模块与评论模块点击事件显示激活与内容
$('#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();
});
  • step4 测试商品介绍与评论点击事件显示对应内容(商品详情当初在创建表数据时,没有填写,所以显示空)

四丶项目部署

1.使用uwsgi作为项目运行的web服务器

说明:在开发环境中运行项目需要在终端上执行python2 manage.py runserver命令,其中runserver就是django给我们提供的web服务器;uwsgi服务器就是遵循WSGI协议的web服务器

  • step1 安装uwsgi服务器

  • step2 博主将windows中的dailyfresh项目拷贝到ubuntu桌面,需要在ubuntu中配置项目配置文件settings的mysql数据库为windows中的mysql数据库,因为ubuntu中的mysql数据库表中没有任何数据,需要注意的是windows上的防火墙必须处于关闭状态 ,在windows mysql中需要创建一个用户并且该用户只能使用dailyfresh数据库,即将该用户配置到ubuntu项目settings配置文件中 
grant all privileges on dailyfresh.* to "taogang"@"%" identified by "123456"
  • step3 在ubuntu中进行远程连接windows中的mysql 

  • step4 ubuntu中配置setting数据库连接

 

  • step6 修改项目settings配置文件中的调试模式以及允许的主机访问为任何一台主机都可以进行访问

  • step7 在项目根目录下创建一个uwsgi.ini配置文件,编写以下内容
[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
  • step8 启动uwsgi
启动:uwsgi –-ini 配置文件路径 例如:uwsgi –-ini uwsgi.ini

  • step9 启动uwsgi后,在浏览器输入http://127.0.0.1:8080/index后,显示出主页页面,但不能加载静态文件,原因是在settings中配置DEBUG=False导致的,当DEBUG=True时,django服务器会帮我们去加载静态文件,但现在使用uwsgi服务器,该服务器不会帮我们加载静态文件

  • step10 停止uwsgi
停止:uwsgi --stop uwsgi.pid路径 例如:uwsgi –-stop uwsgi.pid

2.部署架构图 

3.配置nginx将用户请求转交给uwsgi 

  • step1 修改项目根目录下的uwsgi.ini文件,注释掉http请求(即在浏览器输入http://127.0.0.1:8080/index不能直接访问),取消socket注释,使用nginx来连接uwsgi
[uwsgi]
#使用nginx连接时使用
socket=127.0.0.1:8080
#直接做web服务器使用
#http=127.0.0.1:8080

  • step2 因为没有多余的电脑当做nginx服务器来将请求转发到uwsgi服务器上,所以博主这里打算使用ubuntu中的nginx,进入nginx配置文件,首先博主将之前nginx的配置进行了备份处理,再进行如下编写

  • step3 重启nginx服务以及启动uwsgi服务

  • step4 在浏览器输入http://127.0.0.1/index,因为不输入端口号默认为80端口即也就是在nginx监听端口,在80监听端口中,配置了location / 中包含uwsgi的请求参数,并将请求转给uwsgi服务器127.0.0.1:8080也就是在项目根目录下的uwsgi.ini配置文件中配置的socket地址,uwsgi服务器调用django项目中的application接口,将页面返回给nginx服务器,最后nginx服务器将页面返回给浏览器
  • 说明:因为博主在进行项目迁移到ubuntu时,使用python manage.py runserver启动项目后,访问主页并在ubuntu redis数据库中设置了主页的缓存,所以显示了带有静态资源的主页

4.配置nginx处理静态文件

  • step1 在ubuntu中创建static目录用户存放项目使用的静态文件

  • step2 修改static目录的权限

  • step3 django settings.py中配置收集静态文件路径
# 配置收集静态文件路径
STATIC_ROOT = '/usr/www/dailyfresh/static'
  • step4  收集项目所使用的静态文件 
python manage.py collectstatic

  • step4 编写nginx配置文件,指定静态文件的目录

  • step5 重启nginx服务

  • step6 在火狐浏览器中打开禁用缓存按钮,刷新页面成功加载静态文件资源

  • step7 浏览其他页面是否也能成功加载静态文件

5.通过nginx调度服务器将用户请求转发到其他地址上

  • step1 整个部署架构说明,当用户在浏览器中输入127.0.0.1也就是http://127.0.0.1:80/,因为在浏览器中默认端口为80,所以写不写无所谓,即访问了 / 地址,表示用户请求的是网站的静态主页面;当用户在浏览器中输入除了 / 以外的地址,比如127.0.0.1/index,即访问了 /index 地址,表示用户请求的是动态资源主页面,前面博客中写到主页面静态化时,是在虚拟机ubuntu2中使用celery异步生成主页静态页面,通过访问ubuntu2中的nginx服务器的ip+/,匹配nginx服务器中配置的location来获取指定celery异步生成的静态主页index.html文件,最终通过nginx服务器返回给浏览器,浏览器渲染到页面;而现在项目部署需要使用uwsgi服务器来运行项目,而不是用django提供的测试环境服务器,那么就需要另外一台电脑来作为动态资源服务器(ubuntu1),那么就需要使用调度服务器也就是另一台nginx服务器(ubuntu1)来根据用户请求地址的不同分为两种 / 和其他(访问其他就是除了/以外的请求,在项目中就是/index 和/static)来进行调度,用户请求的是 / 则调度器向静态页面服务器(ubuntu2)去获取静态资源数据,如果不是访问 / 则向动态资源服务器(ubuntu1)获取动态资源数据,页面中静态资源的路径为/static下的某个路径即在调度器nginx中则直接去本地/usr/www/dailyfresh/static目录中直接获取文件,需要注意的是在ubuntu1电脑中安装了nginx服务器和uwsgi服务器以及django项目,通过前面进行测试一样,通过nginx服务器来访问uwsgi服务器,然后uwsgi服务器再通过调用django项目的application接口来运行项目,无非就是多添加了一个 / 地址请求静态主页面的服务器,即需要两台ubuntu虚拟机,其中ubuntu1负责调度器以及动态资源服务器使用,而ubuntu2则只是提供静态页面,充当静态页面服务器,因为博主的电脑实在是运行不了两台虚拟机以及本机的windows,所以无法进行演示操作

  • step2 通过上一步的详细说明,那么就需要在ubuntu1中配置nginx服务器(动态页面服务器+调度器)
  • 说明:location = / 唯一的当满足请求地址只能是 / 的时候,才能进行匹配转发

  • step3 ubuntu2中配置nginx服务器(静态页面服务器),该配置也就是之前做主页静态化时的配置

6.nginx配置upstream实现负载均衡

  • step1 最终部署项目流程图

  • step2 对于动态资源服务器(应用服务器)可以有多台,即每台都是uwsgi+django,但每台应用服务器配置连接nginx服务器(调度器)的socket端口不同,如第一台配置8080,第二天配置8081,在启动uwsgi时各自执行自己的uwsgi -- ini uwsgi.ini;那么就需要在调度器(nginx)中进行如下配置,当用户请求 / 开头的任何地址即请求动态资源,则会upstream指定dailyfresh中均衡拿取地址,进行转发,比如说用户访问主页则是启动了127.0.0.1:8080此uwsgi服务器获取的资源,当用户在主页点击某个商品进行商品详情页时,则调度器(nginx)会将请求转发给127.0.0.1:8081端口的uwsgi服务器,由该服务器来获取商品详情页所需要的资源数据,这样做就能达到负载均衡
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 (负载均衡  提供静态文件)

猜你喜欢

转载自blog.csdn.net/qq_41782425/article/details/89059800
今日推荐