FXHao

个人站

欢迎来到我的小世界~哈哈哈~


Django项目笔记_4

购物车模块

添加到购物车

  • 确定前端是否传递数据,传递什么数据,什么格式
  • 确定前端访问的方式(get post)
  • 确定返回给前端的什么数据,什么格式

购物车页面

  • 详情页的数量和总价,用js完成 jQuery 选择器参考文档

  • 购物车记录后台

    • 请求方式,采用ajax

    • 传递参数:商品id(sku_id) 商品数量(count)

      • get传参:/cart/add?sku_id=1&count=3
      • post传参:{"sku_id":1, "count":3}
      • url传参:url 配置时捕获参数
      • 如果只涉及到数据的修改,采用POST
      • 如果只涉及到数据的获取,采用GET
    • 注意:ajax发起的请求都在后台,在浏览器中看不到,所以购物车添加view 中不使用LoginRequiredMixin来判断登录

    • 更新

      # 业务处理:更新购物车记录
      conn = get_redis_connection('default')
      cart_key = 'cart_%d'%user.id
          
      # 校验商品的库存
      if count > sku.stock:
      	return JsonResponse({'res':4, 'errmsg':'商品库存不足'})
          
      # 更新
      conn.hset(cart_key, sku_id, count)
          
      # 计算用户购物车中商品的总件数 {'1':5, '2':3}
      total_count = 0
      vals = conn.hvals(cart_key)
      for val in vals:
      	total_count += int(val)
      
    • 删除

      # 业务处理:删除购物车记录
      conn = get_redis_connection('default')
      cart_key = 'cart_%d'%user.id
          
      # 删除 hdel
      conn.hdel(cart_key, sku_id)
          
      # 计算用户购物车中商品的总件数 {'1':5, '2':3}
      total_count = 0
      vals = conn.hvals(cart_key)
      for val in vals:
      	total_count += int(val)
      

订单模块

  • 订单创建:用户点击提交订单时,需传递的参数:收货地址,支付方式,商品ID
  • 用户每下一个订单,就需要向订单信息表(df_order_info)表中加入一条数据记录,用户的订单有几个商品,就需要向订单商品表(fd_order_goods)表中加入几条记录

mysql事务

  • 一组MySQL语句,要么都执行,要么都不执行

  • django中有个模块可以更方便的帮助我们控制事务

    from django.db import transaction
    

    只需要在函数前使用@transaction.atomic来装饰一下,之后函数里只要涉及数据库的操作,都会被放进事务里

  • 但一个函数里并不是所有数据库操作都要放进一个事务里,这时我们可以设置保存点

    • 设置保存点:savepoint()——>有返回值
    • 回滚保存点:savepoint_rollback(),把保存点作为参数传递进去
    • 释放保存点:savepoint_commit(),把保存点作为参数传递进去
  • mysql事务隔离级别

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

    • mysql默认的隔离级别是Repeatable Read(可重读),我们需要手动的修改为Read Committed(读取提交内容)

    • 打开MySQL的配置文件(/etc/mysql/mysql.conf.d/mysqld.cnf),添加内容:

      transaction-isolation = READ-COMMITTED
      

    • 修改完后,重启MySQL

      sudo service mysql restart
      

订单并发

  • 悲观锁:在查询商品的时候就上一个锁,等到下完单,更新了数据库时,才解锁,别人才可以拿到锁

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

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

    • update df_goods_sku set stock=0, sales=1 where id=17 and stock=1;
      
    • 但是当库存充足时,第一个用户下单完成更改了数据库时,第二个同时下单的用户因数据更新而与之前检测的不一致导致无法完成下单,这时,可以在商品查询时加一个for循环,让用户下单失败时重新查询商品,检查库存,若库存不足时,回滚数据,下单失败,若充足时,下单成功,跳出循环

    • 因为的事务的隔离性,会导致在用户1下单完成更改数据库后,用户2无法读取用户1更改的数据,所以还是会导致用户2下单失败,这时,需要上面提到的修改事务的隔离级别

订单支付

支付宝的使用

  • 支付宝开放平台

  • 使用python工具包 参考文档

  • 安装:

    # 从 1.3.0升级上来的用户, 请先卸载pycrypto:
    pip uninstall pycrypto
    # 安装python-alipay-sdk
    pip install python-alipay-sdk --upgrade
    
  • 生成秘钥文件

    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 查看公钥的内容

  • 将—–BEGIN PUBLIC KEY—–和—–END PUBLIC KEY—–中间的内容保存在支付宝的用户配置中(沙箱或者正式)

  • 下载支付宝的公钥文件:将公钥的内容复制保存到一个文本文件中(alipay_pubilc_key.pem),注意需要在文本的首尾添加标记位(—–BEGIN PUBLIC KEY—–和—–END PUBLIC KEY—–)

  • 将刚刚生成的私钥app_private_key.pem和支付宝公钥alipay_public_key.pem放到我们的项目目录中

  • 使用支付宝 python包的初始化,然后调用支付接口

    class OrderPayView(View):
        '''订单支付'''
      
        def post(self, request):
            '''订单支付'''
            # 用户是否登陆
            user = request.user
            if not user.is_authenticated():
                return JsonResponse({'res': 0, 'errmsg': '用户未登录'})
      
            # 接受参数
            order_id = request.POST.get('order_id')
      
            # 校验参数
            if not order_id:
                return JsonResponse({'res': 1, 'errmsg': '无效订单ID'})
      
            try:
                order = OrderInfo.objects.get(order_id=order_id,
                                              user=user,  # 检验用户是否正确
                                              pay_method=3,  # 校验支付方式是否为支付宝
                                              order_status=1)  # 校验该订单是否是未支付
            except OrderInfo.DoesNotExist:
                return JsonResponse({'res': 2, 'errmsg': '无效订单'})
      
            # 业务处理:使用Python SDK调用支付宝的支付接口
            # 初始化
            alipay = AliPay(
                appid="2016100200644785",  # 应用id
                app_notify_url=None,  # 默认回调url
                app_private_key_path=os.path.join(settings.BASE_DIR, 'apps/order/app_private_key.pem'),
                # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
                alipay_public_key_path=os.path.join(settings.BASE_DIR, 'apps/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,  # 订单ID
                total_amount=str(total_pay),  # 总金额
                subject='天天生鲜{}'.format(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})
    

获取支付结果接口

  • 参考文档

  • 在源码里有个api_alipay_trade_query()封装好了调用支付宝查询的接口

    class CheckPayView(View):
        '''查看订单支付结果'''
      
        def post(self, request):
            '''查询订单支付结果'''
            user = request.user
            if not user.is_authenticated():
                return JsonResponse({'res': 0, 'errmsg': '用户未登录'})
      
            # 接受参数
            order_id = request.POST.get('order_id')
      
            # 校验参数
            if not order_id:
                return JsonResponse({'res': 1, 'errmsg': '无效订单ID'})
      
            try:
                order = OrderInfo.objects.get(order_id=order_id,
                                              user=user,  # 检验用户是否正确
                                              pay_method=3,  # 校验支付方式是否为支付宝
                                              order_status=1)  # 校验该订单是否是未支付
            except OrderInfo.DoesNotExist:
                return JsonResponse({'res': 2, 'errmsg': '无效订单'})
      
            # 业务处理:使用Python SDK调用支付宝的支付接口
            # 初始化
            alipay = AliPay(
                appid="2016100200644785",  # 应用id
                app_notify_url=None,  # 默认回调url
                app_private_key_path=os.path.join(settings.BASE_DIR, 'apps/order/app_private_key.pem'),
                # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
                alipay_public_key_path=os.path.join(settings.BASE_DIR, 'apps/order/alipay_public_key.pem'),
                sign_type="RSA2",  # RSA 或者 RSA2
                debug=True  # 默认False
            )
      
            # 调用查询接口
            while True:
                response = alipay.api_alipay_trade_query(order_id)
                # response 返回的是一个字典 格式如下
                # response = {
                #
                #         "trade_no": "2017032121001004070200176844",  # 支付宝交易号
                #         "code": "10000",   # 接口调用是否成功
                #         "invoice_amount": "20.00",
                #         "open_id": "20880072506750308812798160715407",
                #         "fund_bill_list": [
                #             {
                #                 "amount": "20.00",
                #                 "fund_channel": "ALIPAYACCOUNT"
                #             }
                #         ],
                #         "buyer_logon_id": "csq***@sandbox.com",
                #         "send_pay_date": "2017-03-21 13:29:17",
                #         "receipt_amount": "20.00",
                #         "out_trade_no": "out_trade_no15",
                #         "buyer_pay_amount": "20.00",
                #         "buyer_user_id": "2088102169481075",
                #         "msg": "Success",
                #         "point_amount": "0.00",
                #         "trade_status": "TRADE_SUCCESS",   # 支付结果
                #         "total_amount": "20.00"
                # }
      
                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.order_status = 4  # 待评价
                    order.save()
                    # 返回结果
                    return JsonResponse({'res': 3, 'message': '支付成功'})
                elif code == '40004' or (code == '10000' and response.get('trade_status') == 'WAIT_BUYER_PAY'):
                    # 等待买家付款
                    # 业务处理失败,可能一会就会成功
                    import time
                    time.sleep(2)
                    continue
                else:
                    # 支付出错
                    print(code)
                    return JsonResponse({'res': 4, 'errmsg': '支付失败'})
    

项目部署

流程

uwsgi

  • 使用uwsgi作为web服务器

  • 安装:pip install uwsgi

  • 配置:settings.py修改

    DEBUG=FALSE
    ALLOWED_HOSTS=[*] 
    
  • 在项目中新建文件uwsig.ini存放文件配置

    #使用nginx连接时使用
    socket=127.0.0.1:8080
    #直接做web服务器使用  ython manage.py runserver ip:port
    ;http=127.0.0.1:8080
    #项目目录
    cdir=/home/fxh/python/project/dailyfresh
    #项目中wsgi.py文件的目录,相对于项目目录
    wsgi-file=dailyfresh/wsgi.py
    # 指定启动工作的进程数
    processes=4
    # 指定工作进程中的线程数
    threads=2
    master=True
    # 保存启动后主进程的pid
    pidfile=uwsgi.pid
    # 设置uwsgi后台运行
    daemonize=uwsgi.log
    # 设置虚拟环境的路径
    virtualenv=/home/fxh/.virtualenvs/Project/django
    
  • 启动:uwsgi --ini 配置文件路径 --plugin python3

  • 停止:uwsgi --stop uwsgi_pid路径

nginx

  • nginx 配置转发请求给uwsgi:修改nginx配置文件

    • location / {
      	include uwsgi_params;
      	uwsgi_pass uwsgi服务器的ip:port;
      }
      
  • nginx配置处理静态文件

    • django settings.py中配置收集静态文件路径:

      STATIC_ROOT=收集的静态文件路径 例如:/var/www/dailyfresh/static;
      
    • django 收集静态文件的命令:

      python manage.py collectstatic
      

      执行上面的命令会把项目中所使用的静态文件收集到STATIC_ROOT指定的目录下

    • 收集完静态文件之后,让nginx提供静态文件,需要在nginx配置文件中增加如下配置:

      location /static {
      	alias /var/www/dailyfresh/static/;
      }
      
  • nginx转发请求给另外地址

    • location 对应的配置项中增加 proxy_pass 转发的服务器地址。

      如当用户访问127.0.0.1时,在nginx中配置把这个请求转发给172.16.179.131:80(nginx)服务器,让这台服务器提供静态首页,配置如下:

      location = /{
      	proxy_pass http://172.16.179.131;
      }
      
  • nginx配置upstream实现负载均衡

    • ngnix 配置负载均衡时,在server配置的前面增加upstream配置项

      upstream dailyfresh {
      	server 127.0.0.1:8080;
      	server 127.0.0.1:8081;
      }