【Django 天天生鲜项目05】订单(Mysql事务、并发处理、支付宝支付、评论)

本部分涉及订单的生成、并发处理、支付、评论等

关键:MySQL事务、并发处理的悲观锁/乐观锁、支付宝SDK 的使用......

仅作为个人笔记

目录

2.创建订单

3.订单生成

3.1. MySQL事务

3.2. Django中使用事务

3.3. 订单并发问题

5.订单支付

5.1.支付宝开放平台

5.2.网站对接支付宝流程


 

订单

在购物车中点击去结算后,会将选中的商品id、数量、价格等传到订单页。实际上,只需要传用户要购买的商品的id即可,数量等会从redis数据库中获取。

订单页面大致如下:

【Django 天天生鲜项目05】订单(Mysql事务、并发处理、支付宝支付、评论)_第1张图片

 

1.订单页面显示的视图

表单中的checkbox只有被选中时值才会被提交。request.POST提交的类型为QueryDict,可以一个名字对应多个值;在视图中request.POST.getlist()来获。

 

 

前端注意对收货地址的默认选中:

        
寄送到:
{% for addr in addrs %}
{{addr.addr}} ({{addr.receviver}} 收) {{addr.phone}}
{% endfor %}

 

2.创建订单

用户点击提交订单时,创建订单。前端必须要传递的参数有:收货地址、支付方式、商品id等。

订单相关的MySQL表有:订单信息表:df_order_info、订单商品表:df_order_goods,二者为一对多的关系;

用户每下一个订单,就向df_order_info表中加入一条记录;用户的订单中有几个商品,就需要向df_order_goods表中加入几条记录。

 

前端js

提交订单使用Ajax提交,关键的js如下:

v
$('#order_btn').click(function() {
            // 获取用户选择的地址id, 支付方式, 要购买的商品id字符串
            addr_id = $('input[name="addr_id"]:checked').val()  // 选择器获取被选中的checkbox的值(收货地址
            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}
            // 发起ajax post请求,访问/order/commit, 传递的参数: addr_id pay_method, sku_ids
            $.post('/order/commit', params, function (data) {
                if (data.res==5){
                    alert('创建成功')
                }
                else {
                    alert(data.errmsg)
                }
            })
        });

注意,Ajax使用cdrf的方法:在html中加上:{% csrf_token %};在js中加上:csrf = $('input[name="csrfmiddlewaretoken"]').val()  ;

 

后台view

关键:用户每下一个订单,就向df_order_info表中加入一条记录;用户的订单中有几个商品,就需要向df_order_goods表中加入几条记录。

除了从前端和数据库中能直接获取的参数,还有些参数需要自己组织:订单id order_id(年月日时分秒+用户id)、总数total_count、总价total_price、运费transit_price等。

部分code如下,但下面的code还存在很多待完善的地方(库存判断等):

    def post(self, request):
        ...        
        # 组织参数
        # 订单id: 20171122181630+用户id (年月日时分秒+用户id)
        order_id = datetime.now().strftime('%Y%m%d%H%M%S') + str(user.id)
        transit_price = 10
        # 总数目和总金额(先假设为0存进去,添加完df_order_goods后再更新
        total_count = 0
        total_price = 0

        # todo: 向df_order_info表中添加一条记录
        order = OrderInfo.objects.create(order_id=order_id, ...)

        # todo: 用户的订单中有几个商品,需要向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:
                return JsonResponse({'res': 4, 'errmsg': '商品不存在'})

            # 从redis中获取用户所要购买的商品的数量
            count = conn.hget(cart_key, sku_id)

            # todo: 向df_order_goods表中添加一条记录
            OrderGoods.objects.create(order=order, ...)

            # 更新商品的库存和销量
            sku.stock -= int(count)
            sku.sales += int(count)
            sku.save()

            # 累加计算订单商品的总数量和总价格
            amount = sku.price * int(count)
            total_count += int(count)
            total_price += amount

        # 更新订单信息表中的商品的总数量和总价格
        order.total_count = total_count
        order.total_price = total_price
        order.save()

        # 从redis清除用户购物车中对应的记录(注意对列表拆包*sku_ids,如将[1,3]拆为 1,3 传递 )
        conn.hdel(cart_key, *sku_ids)

        # 返回应答
        return JsonResponse({'res': 5, 'message': '创建成功'})

 

3.订单生成

在上面创建订单的视图中,还可能存在库存不足,特别是当两个用户都将商品加入购物车,先后提交了订单,但后提交时库存不足的现象。这时,不应该再为后者创建订单相关的数据表。这是就可以使用MySQL的事务来处理。

3.1. MySQL事务

mysql事务: 一组sql操作,要么都成功,要么都失败。即一组mysql语句,要么执行,要么全部不执行。创建订单的一系列操作,要么都成功,要么都失败。MySQL 事务    https://www.mysqlzh.com/

 

事务的特点

  • 原子性:一组事务,要么成功;要么撤回。
  • 稳定性 :有非法数据(外键约束之类),事务撤回。
  • 隔离性事务独立运行。一个事务处理后的结果,影响了其他事务,那么其他事务会撤回。事务的100%隔离,需要牺牲速度。
  • 可靠性:软、硬件崩溃后,InnoDB数据表驱动会利用日志文件重构修改。可靠性和高速度不可兼得, innodb_flush_log_at_trx_commit 选项 决定什么时候吧事务保存到日志里。

 

事务控制语句

  • BEGIN 或 START TRANSACTION;显式地开启一个事务;
  • COMMITCOMMIT WORK 等价。COMMIT会提交事务,并使已对数据库进行的所有修改称为永久性的;
  • ROLLBACK:与ROLLBACK WORK 等价。回滚会结束用户的务,并撤销正在进行的所有未提交的修改;
  • SAVEPOINT identifier(SAVEPOINT 保存点名);SAVEPOINT允许在事务中创建一个保存点,一个事务中可以有多个SAVEPOINT
  • RELEASE SAVEPOINT identifier;删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;
  • ROLLBACK TO identifier;把事务回滚到标记点;

 

mysql事务隔离级别

SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

  • Read Uncommitted(读取未提交内容)
    在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

  • Read Committed(读取提交内容)
    这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
  • Repeatable Read(可重读)
    这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的幻影行。InnoDBFalcon存储引擎通过多版本并发控制(MVCCMultiversion Concurrency Control)机制解决了该问题。
  • Serializable(可串行化) 
    这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

 

这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:

  • 脏读(Drity Read)某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
  • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几行(Row)数据,而另一个事务却在此时插入了新的几行数据,先前的事务在接下来的查询中,就会发现有几行数据是它先前所没有的。
隔离级别 脏读 不可重复读 幻读
Read Uncommitted(读未提交) V V V
Read Committed(读已提交) X V V
Repeatable Read(可重读) X X V
Serializable(可串行化)  X X X

设置mysql事务的隔离级别

打开mysql配置文件: sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf  ,找到 skip-external-locking 可在其下面添加如下行,配置隔离级别:

transaction-isolation = READ-COMMITTED

然后保存配置文件,重启mysql服务:sudo service mysql restart
 

 

3.2. Django中使用事务

整个订单的创建对MySQL的操作应该放在一个事务中。

Django的相关文档: 数据库事务、显式控制事务、保存点

 

  • 导入:from django.db import transaction
  • 要把sql语句放在事务中,只需用 @transaction.atomic  装饰相关的函数,就会把函数中的数据库操作放在一个事务里。

 

所以上面的视图中,只要对post函数使用 @transaction.atomic 装饰器即可使用事务

    @transaction.atomic
    def post(self, request):
        ...

 

设置保存点

保存点由 django.db.transaction: 中的三个函数来控制:

  • savepoint(using=None):创建新的保存点。这标志着事务中已知处于“良好”状态的一个点。返回保存点ID (sid) 。
  • savepoint_commit(sid, using=None):释放保存点 sid 。自保存点被创建依赖执行的更改成为事务的一部分。
  • savepoint_rollback(sid, using=None):回滚事务来保存 sid 

比如,可以在下面这些位置设置保存点:

    @transaction.atomic
    def post(self, request):
        ......

        # 设置事务保存点
        save_id = transaction.savepoint()

        try:
            # todo: 向df_order_info表中添加一条记录
            order = OrderInfo.objects.create(order_id=order_id,
                                             ......)

            # todo: 用户的订单中有几个商品,需要向df_order_goods表中加入几条记录
            ...... 
            for sku_id in sku_ids:
                # 获取商品的信息
                try:
                    sku = GoodsSKU.objects.get(id=sku_id)
                except:
                    transaction.savepoint_rollback(save_id)  # 商品不存在,回滚
                    return JsonResponse({'res': 4, 'errmsg': '商品不存在'})

                # 从redis中获取用户所要购买的商品的数量
                count = conn.hget(cart_key, sku_id)

                # 判断商品的库存
                if int(count) > sku.stock:
                    transaction.savepoint_rollback(save_id)  # 回滚
                    return JsonResponse({'res': 6, 'errmsg': '商品库存不足'})

                # todo: 向df_order_goods表中添加一条记录
                OrderGoods.objects.create(order=order,
                                          ......)

                ......

        except Exception as e:
            transaction.savepoint_rollback(save_id)
            return JsonResponse({'res': 7, 'errmsg': '下单失败'})

        # 提交事务
        transaction.savepoint_commit(save_id)

 

3.3. 订单并发问题

在极短的时间内,有两个用户进行了下面的操作。因为进程间的调用时随机的,有可能在库存判断时,两个用户的判断都是有库存的,但接下来进程1先执行,向数据库添加记录并将库存更新为0。这时运行进程2,因为已经判断过库存了,所以也执行了相同的操作。这时,就会造成订单中的商品数已经超出了实际的库存数了。

进程1(用户1发起) 进程2(用户2发起)

向 df_order_info 添加一条记录;

查询sku_id=17的商品信息;

库存判断;

向 df_order_info 添加一条记录;

查询sku_id=17的商品信息;

库存判断;

向 df_order_goods 中添加记录;

下单后库存更新为0

...

向 df_order_goods 中添加记录;

下单后库存更新为0

...

 

悲观锁

悲观锁获取数据时对数据行了锁定,其他事务要想获取锁,必须等原事务结束。

进程1(用户1发起) 进程2(用户2发起)

向 df_order_info 添加一条记录;

加锁:select * from df_goods_sku where id=sku_id for update;

查询sku_id=17的商品信息;

库存判断;

向 df_order_info 添加一条记录;

加锁:select * from df_goods_sku where id=sku_id for update;

查询sku_id=17的商品信息;

库存判断;

向 df_order_goods 中添加记录;

下单后库存更新为0

事务结束,锁释放。

向 df_order_goods 中添加记录;

下单后库存更新为0

事务结束,锁释放。

在视图中,把获取商品信息的语句加上 select_for_update() 


# select * from df_goods_sku where id=sku_id for update;
sku = GoodsSKU.objects.select_for_update().get(id=sku_id)

 

乐观锁

查询数据时不加锁,提交更改时再次进行判断。判断更新时的库存和之前查出的库存是否一致 (:不一致不一定库存就为0,所以应该在加以判断。可以在外面套一个循环,使其尝试几次,几次都不一致则判断为无库存)。

update df_goods_sku set stock=0, sales=1 where id=17 and stock=1;

进程1(用户1发起) 进程2(用户2发起)

向 df_order_info 添加一条记录;

加锁:select * from df_goods_sku where id=sku_id for update;

查询sku_id=17的商品信息;

库存判断;

向 df_order_info 添加一条记录;

加锁:select * from df_goods_sku where id=sku_id for update;

查询sku_id=17的商品信息;

库存判断;

向 df_order_goods 中添加记录;

下单后库存更新为0

...

update df_goods_sku set stock=0, sales=1 where id=17 and stock=1;

向 df_order_goods 中添加记录;

下单后库存更新为0

...

update df_goods_sku set stock=0, sales=1 where id=17 and stock=1;

注意,应该先配置隔离级别为READ-COMMITTED。如果是Django2.x以后,这是默认的。

使用乐观锁:

class OrderCommitView(View):
    """订单创建"""
    @transaction.atomic
    def post(self, request):
            ......
            for sku_id in sku_ids:
                for i in range(3):  # 使用乐观锁做判断时最多做3次判断
                    # 获取商品的信息
                    try:
                        sku = GoodsSKU.objects.get(id=sku_id)  # 正常查,不加悲观锁
                    except:
                        transaction.savepoint_rollback(save_id)  # 商品不存在,回滚
                        return JsonResponse({'res': 4, 'errmsg': '商品不存在'})

                    # 从redis中获取用户所要购买的商品的数量
                    count = conn.hget(cart_key, sku_id)

                    # 判断商品的库存
                    if int(count) > sku.stock:
                        transaction.savepoint_rollback(save_id)
                        return JsonResponse({'res': 6, 'errmsg': '商品库存不足'})

                    # 更新商品的库存和销量(使用乐观锁时)
                    orgin_stock = sku.stock
                    new_stock = orgin_stock - int(count)
                    new_sales = sku.sales + int(count)
                    # update df_goods_sku set stock=new_stock, sales=new_sales where id=sku_id and stock = orgin_stock
                    # 执行并返回受影响的行数
                    res = GoodsSKU.objects.filter(id=sku_id, stock=orgin_stock).update(stock=new_stock, sales=new_sales)
                    if res == 0:
                        if i == 2:  # 第3次尝试,还是不一致则判为无库存下单失败
                            transaction.savepoint_rollback(save_id)
                            return JsonResponse({'res': 7, 'errmsg': '下单失败!!'})
                        continue

                    # todo: 向df_order_goods表中添加一条记录
                    OrderGoods.objects.create(order=order, ......)

                    # todo: 累加计算订单商品的总数量和总价格
                    ......
                    # 跳出循环
                    break
    ......

 

总之:

  • 冲突比较少的时候,使用乐观锁。
  • 冲突比较多的时候,使用悲观锁。乐观锁重复代价比较大时,也使用悲观锁。

 

4.订单页面显示

提交订单前端中,订单创建成功并返回后,Ajax的function会弹窗提醒,并在3秒后跳转用户中心的订单页面

place_order.html


......
        $('#order_btn').click(function() {
            // 获取用户选择的地址id, 支付方式, 要购买的商品id字符串
            addr_id = $('input[name="addr_id"]:checked').val()  // 选择器获取被选中的checkbox的值(收货地址
            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}

            // 发起ajax post请求,访问/order/commit, 传递的参数: addr_id pay_method, sku_ids
            $.post('/order/commit', params, function (data) {
                if (data.res==5){
                    // 创建成功
                    localStorage.setItem('order_finish',2);
                    $('.popup_con').fadeIn('fast', function() {
                        setTimeout(function(){
                            $('.popup_con').fadeOut('fast',function(){
                                window.location.href = '{% url 'user:order' 1 %}';
                            });
                        },3000)
                    });
                }
                else {
                    alert(data.errmsg)
                }
            })
        });

在用户中心的订单页面{% url 'user:order' %}  即 user_center_order.html 中。需要查出所有订单、分页等。

user/view.py的视图中:

class UserOrderView(LoginRequiredMixin, View):
    """用户中心-订单页"""
    def get(self, request, page):
        ......
        # 遍历获取订单商品的信息
        for order in orders:
            # 根据order_id查询订单商品信息
            order_skus = OrderGoods.objects.filter(order_if=order.order_id).order_by('-create_time')

            # 遍历order_skus计算商品的小计
            for order_sku in order_skus:
                amount = order_sku.price * order_sku.count
                # 动态给order_sku增加属性amount,保存订单商品的小计
                order_sku.amount = amount
                
            # 动态给order增加属性,保存订单状态标题
            order.status_name = OrderInfo.ORDER_STATUS[order.order_status]
            # 动态给order增加属性,保存订单商品的信息
            order.order_skus = order_skus

            # 分页
        ......
        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_name }}
{% for order_sku in order.order_skus %}
  • {{ order_sku.sku.name }} {{ order_sku.price }}元/{{ order_sku.sku.unite }}
  • {{ order_sku.amount }}
  • 11.80元
{% endfor %}
{{ order.total_price|add:order.transit_price }}(含运费:{{ order.transit_price }})元 {{ order.order_status_name }} 去付款
{% endfor %}

5.订单支付

订单支付以支付宝支付为例,使用支付宝开放平台。

 

5.1.支付宝开放平台

支付宝开放平台:https://open.alipay.com/platform/home.htm ,可以使用自己的帐号登录。

实际开发中,在开发接入中创建自己需要的应用,接入支付宝开放平台

这里,仅使用沙箱来测试支付业务,在开发者中心控制台中,找到研发服务沙箱环境(根据下面的文档操作即可)。

沙箱相关文档:https://opendocs.alipay.com/open/200/105311

 

支付宝开发者各种文档:https://openhome.alipay.com/developmentDocument.htm

电脑网站支付:https://opendocs.alipay.com/open/270、快速接入

 

比较方便的是,支付宝为了帮助开发者调用开放接口,提供了开放平台服务端 SDK,包含 JAVA、PHP、NodeJS、Python 和 .NET 五种,封装了签名&验签、HTTP 接口请求等基础功能。请先下载对应语言版本的 SDK 并引入您的开发工程。

 

(1) 一个更方便的非官方SDK:https://github.com/fzlee/alipay/blob/master/README.zh-hans.md

# 命令安装python-alipay-sdk
pip install python-alipay-sdk --upgrade

(2) python版的官方 Alipay SDK:PyPI项目依赖,即https://pypi.org/project/alipay-sdk-python/3.3.398/

将whl文件下载下来安装。或在你的环境中,使用命令:pip install alipay-sdk-python==3.3.398 安装

 

 

5.2.网站对接支付宝流程

  1. 用户下单(去付款,访问/order/pay,post传递订单id参数 );
  2. 商户系统调用支付接口调用支付接口(Django调用支付接口alipay.trade.page.pay):
    传参:订单id、总金额、订单标题、return_url、notify_url 等。
  3. 支付宝返回支付页面的地址(支付页面)到Django,Django引导用户到支付页面;
  4. 用户登录支付宝......确认支付;
  5. 支付宝同步访问商户的 return_url ,传递参数告诉网站用户支付的结果;
  6. 支付宝异步访问商户的 notify_url ,传递参数告诉网站用户支付的结果;

    :结果以异步的为准。当我们在本地用127.0.0.1调试时,公网是访问不到的,我们可以主动通过 alipay.trade.query 查询获取交易的结果
     
  7. ...

【Django 天天生鲜项目05】订单(Mysql事务、并发处理、支付宝支付、评论)_第2张图片

 

5.3.支付代码实现过程

  • 安装 Alipay SDK ,本处使用更方便的 python-alipay-sdk (pycryptodomex)。
    pip install python-alipay-sdk --upgrade

     
  • 然后参考 配置应用环境 将电脑的公钥和私钥生成出来。linux也可以使用OpenSSL来生成秘钥文件:
    openssl
    OpenSSL> genrsa -out app_private_key.pem   2048  # 私钥
    OpenSSL> rsa -in app_private_key.pem -pubout -out app_public_key.pem # 生成跟私钥对应的公钥
    OpenSSL> exit

    使用命令 cat app_publict_key.pem 查看公钥的内容:
    【Django 天天生鲜项目05】订单(Mysql事务、并发处理、支付宝支付、评论)_第3张图片
    -----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----中间的内容保存在支付宝的用户配置中,即秘钥设置中公钥加签模式处(沙箱或者正式应用)

  • 复制支付宝提供的支付宝公钥。将其保存在项目的文件中,比如 /order/alipay_public_key.pem ,保存的格式如下。顺便将刚刚生成的电脑的私钥文件也拷贝到这个目录。(请求时用自己的私钥加密,收到信息时用支付宝公钥解密

    -----BEGIN PUBLIC KEY-----
    这里粘贴上支付宝公钥
    -----END PUBLIC KEY-----
    

     

  • 在order应用的views.py中,支付宝支付的视图:
    # 前端ajax传递的参数:订单id(order_id)
    # /order/pay
    class OrderPayView(View):
        """支付宝订单创建"""
        def post(self, request):
            """订单创建"""
            ......
            with open(os.path.join(settings.BASE_DIR, 'apps/order/app_private_key.pem'), "r") as f:
                app_private_key = f.read()
            with open(os.path.join(settings.BASE_DIR, 'apps/order/alipay_public_key.pem'), "r") as f:
                alipay_public_key = f.read()
    
            # 业务处理:使用python sdk调用支付宝的支付接口
            # 初始化
            alipay = AliPay(
                appid="2016102400753000",  # 应用id
                app_notify_url=None,  # 默认回调url
                app_private_key_string=app_private_key,
                alipay_public_key_string=alipay_public_key,
                # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
                sign_type="RSA2",  # RSA 或者 RSA2
                debug=True  # 默认False,改为True访问沙箱的地址
            )
            # 调用支付接口。电脑网站支付,需要跳转到https://openapi.alipaydev.com/gateway.do? + order_string
            order_string = alipay.api_alipay_trade_page_pay(
                out_trade_no=order_id,  # 订单id
                total_amount=total_pay,  # 支付总金额
                subject='天天生鲜%s' % order_id,
                return_url=None,
                notify_url=None  # 可选, 不填则使用默认notify url
            )
    
            # 返回应答
            pay_url = 'https://openapi.alipaydev.com/gateway.do?' + order_string
            return JsonResponse({'res': 3, 'pay_url': pay_url})  # 要将用户引导到支付地址
    

 

  • 视图返回支付地址给页面,页面要将用户引导到支付地址。绑定“去付款”位置的按钮点击事件
    ...
    去付款
    ...
    
    

     
  • 用户支付成功后,Django网站由于没有配置使用notify_url,所以还需调用alipay.trade.query获取支付结果。

    视图函数如下,SDK与官方API对应的接口为 api_alipay_trade_query 。
    # 前端ajax传递的参数:订单id(order_id)
    # /order/check
    class CheckPayView(View):
        """查看订单支付的结果"""
        def post(self, request):
            ......            
            # 调用支付宝的交易查询接口
            while True:
                response = alipay.api_alipay_trade_query(order_id)
                
                code = response.get('code')
                if code == '10000' and response.get('trade_status') == 'TRADE_SUCCESS':
                    # 支付成功
                    # 获取支付宝交易号
                    trade_no = response.get('trade_no')
                    # 更新订单状态到数据库
                    order.trade_no = trade_no
                    order.trade_status = 4  # 待评价
                    order.save()
                    return JsonResponse({'res': 3, 'message': '支付成功'})
                
                elif code == '10000' and response.get('trade_status') == 'WAIT_BUYER_PAY':
                    # 等待买家付款
                    # 业务处理失败,可能一会就会成功
                    import time
                    time.sleep(5)
                    continue
                else:
                    # 支付出错
                    return JsonResponse({'res': 4, 'errmsg': '支付失败'})


     

5.4.评论

评论的后台视图在order/views.py,关键是从前端或数据库获取对应的订单id,对其评论字段进行读写:

class CommentView(LoginRequiredMixin, View):
    """订单评论"""
    def get(self, request, order_id):
        """提供评论页面"""
        ......
        # 根据订单的状态获取订单的状态标题
        order.status_name = OrderInfo.ORDER_STATUS[order.order_status]

        # 获取订单商品信息
        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})

    def post(self, request, order_id):
        """处理评论内容"""
        ......
        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):
            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}))

支付完成后,用户中心订单页的“去支付”按钮应该改为“去评价”等相关功能。在js中:

    

 

在detail.html中,以下js使评论内容显示在“评论”而不是“详情”:

            
  • 商品介绍
  • 评论
商品详情:
{# 使用save过滤器关闭富文本转义#}
{{ sku.goods.detail|safe }}
{% for order in sku_orders %}
评论时间:{{ order.update_time }}  用户名:{{ order.order.user.username }}
评论内容:{{ order.comment }}
{% endfor %}
......

 

01 框架、数据表设计、项目框架笔记

02 注册、登录、用户中心 (itsdangerous模块加密、celery异步、 Django 的验证系统、redis作为缓存等)

03 FastDFS文件存储-首页-详情页-列表页

04 搜索(搜索引擎、分词包的使用)、购物车

05 订单(Mysql事务、并发处理、支付宝支付、评论)

06 项目部署(uwsgi服务器、Nginx服务器)

 

-----end-----

你可能感兴趣的:(Django实战)