天天生鲜(Django4.0版本) + 开发遇到的问题及解决

目录

1、项目来源及开发介绍

1.1、天天生鲜介绍

1.2、Web开发流程介绍

1.2.1、需求分析

1.2.2、项目架构概览

1.2.3、SKU与SPU概念

2、项目架构

3、数据库表结构

4、用户认证模型

5、类视图

6、用户模块开发

6.1、Django4.0认证系统文档

6.2、用户注册

6.2.1、django发送邮件

 6.2.2、Celery异步任务队列

6.3、用户激活

6.3.1、加密用户身份信息

6.3.2、解密用户身份信息验证

6.4、用户登录

6.4.1、配置redis作为Django缓存和session后端

6.4.2、登录判断装饰器login_required

6.5、用户退出

6.6、用户地址

6.7、用户个人信息

6.7.1 redis存储历史浏览记录分析

6.7.2、django-redis获取redis链接

7、商品模块开发

7.1、了解FastDFS分布式文件系统

7.2、python对接fastdfs

7.3、项目上传图片和使用图片流程

7.4、Django二次开发对接FastDFS

7.5、商品首页

7.5.1、基本页面处理

7.5.2、保存购物车信息的数据设计

7.5.3 页面静态化

7.5.4、使用缓存

7.6、商品详情页

7.7、商品列表页

7.7.1、分页

7.7.2、页码控制

7.8、商品搜索

7.8.1、安装和配置

7.8.2、索引文件生成

7.8.3、全文检索的使用

7.8.4、改变分词方式

8、购物车模块开发

8.1、添加到购物车

8.2、购物车页面

8.3、购物车记录更新

8.4、购物车记录删除

9、订单模块开发

9.1、提交订单页面

9.2、订单生成

9.2.1、mysql事务

9.2.2、订单并发处理

9.3、用户中心-订单页

9.4、订单支付

10、项目部署

10.1、uwsgi

10.1.1、uwsgi的安装

10.1.2、uwsgi的配置

10.1.3、uwsgi的启动和停止

10.2、nginx

10.2.1、nginx 配置转发请求给uwsgi

10.2.2、nginx配置处理静态文件

10.2.3 nginx转发请求给另外地址

10.2.4、nginx配置upstream实现负载均衡

10.2.5、部署项目流程图

11、开发过程中遇到的bug个人总结

1、在模板中载入静态文件前需要在配置文件settings.py中加入配置

2、django新版本中查询数据库后对象不存在报错的异常类是core.exceptions的ObjectDoesNotExist

3、加密用户身份信息当做url的token值,用authlib包代替itsdangerous,可以指定签名算法HS256

4、celery不支持window10,安装eventlet包,使用celery -A celery_tasks.tasks worker -l info -P eventlet -E命令或者建议使用下面的命令就用安装eventlet包

5、django自带的login()报错:redis输入了一个空类型的值

6、Django的authenticate已经包含了is_active判断,即使用户名密码正确,is_active为0也会返回空,所以需要在setting.py中加配置

7、FastDFS只支持Linux,可以把fastDFS运行在云服务器上,Windows本地电脑上运行FastDFS客户端fdfs-client-py

8、window启动nginx,先到文件夹中启动nginx.exe,再到cmd中输入nginx.exe。停止nginx,在cmd中完整有序停止用nginx -s quit,再用taskkill /f /t /im nginx.exe

9、is_authenticated在Django4.0中是一个属性而不是一个方法

10、Django4.0已经移除ungettext,所以在引入haystack的时候会报错,修改haystack文件夹下的admin.py把ungettext三处地方修改为ngettext

11、Django4.0已经移除smart_text,修改haystack文件夹下的form.py俩处地方把smart_text修改为smart_str

12、配置为乐观锁时,需要设置MySQL默认的隔离级别(可重复读)为(读取提交内容),在MySQL配置文件skip-ex,,,下添加一行transaction-isolation = READ-COMMITTED

13、网站如果想让支付宝平台访问(获取支付后的结果),需要有公网IP

14、安装alipay-sdk-python 提示安装pycrypto 问题:

15、支付宝沙箱测试不需要重新设置密钥和公钥,直接使用默认。

16、使用支付宝交易查询接口要使用AlipayTradeQueryResponse来获取解析后响应的结果

17、win10上没法部署uwsgi,可以通过其它方式间接使用uwsgi,但没必要,最终都是要部署在Linux系统。

18、Django网站响应慢(修改数据库查询方式)

19、指定表名,不用一定和应用名相连,class Meta:  db_table = '表名'

20、发布网站时需要在settings.py中把debug改为false,allowed_host = ['*']

21、cookie:是由服务器生成,存储在浏览器端的一小段文本信息。

22、session

23、模板中的自定义过滤器参数只能是一个或俩个

24、csrf防护(跨站请求伪造攻击):

25、url反向解析:


1、项目来源及开发介绍

修改后的天天生鲜(Django4.0版本):

GitHub - LiXZe/dailyfreshContribute to LiXZe/dailyfresh development by creating an account on GitHub.https://github.com/LiXZe/dailyfresh

1.1、天天生鲜介绍

    首先,此次天天生鲜项目来源是黑马程序员Python就业班的Web开发项目,虽是几年前的项目,但在Python商城项目里也算是功能比较齐全,什么异步任务处理、页面静态化、搜索引擎、高并发库存问题、邮件发送激活、购物车缓存、分布式文件存储、服务器部署、支付宝支付接口调用这些都有,适合拿来学习Python的Web开发练练手,此篇博客的内容也是我对原先项目笔记的修改,有些图片是原先黑马项目的图片。

    其次,原先天天生鲜项目使用的是Django1.8.2版本,我自己改成了新版本Django4.0版本的,使用的虚拟环境是anaconda,不论学习什么语言和框架,一定要会看框架对应的开发文档,从1.8.2到4.0版本的变动挺多的,就需要自己去查看Django4.0的开发文档,附上Django4.0中文的开发文档对应链接:

Django 文档 | Django 文档 | Djangohttps://docs.djangoproject.com/zh-hans/4.0/

    最后文末,是我在开发过程中遇到的各种问题,然后我就记了下来,附上了我的解决方法(由于我这个项目是在win10环境下写的,所以遇到的问题会更多。

先上网站运行后截图:

首先是主页:

用户中心的3个功能页面:

 

 

 

 购物车页面:

 

订单支付:

 

商品详情:

 

 

 

1.2、Web开发流程介绍

在正式学习商城项目开发前,先介绍一下常见Web商城项目的开发流程:

1.2.1、需求分析

1.2.1.1、用户模块

1.注册页

  • 注册时校验用户名是否已被注册。
  • 完成用户信息的注册。
  • 给用户的注册邮箱发送邮件,用户点击邮件中的激活链接完成用户账户的激活。

2.登录页

  • 实现用户的登录功能。

3.用户中心

  • 用户中心信息页:显示登录用户的信息,包括用户名、电话和地址,同时页面下方显示出用户最近浏览的商品信息。
  • 用户中心地址页:显示登录用户的默认收件地址,页面下方的表单可以新增用户的收货地址。
  • 用户中心订单页:显示登录用户的订单信息。

4.其他

  • 如果用户已经登录,页面顶部显示登录用户的信息。

1.2.1.2、商品相关

1.首页

  • 动态指定首页轮播商品信息。
  • 动态指定首页活动信息。
  • 动态获取商品的种类信息并显示。
  • 动态指定首页显示的每个种类的商品(包括图片商品和文字商品)。
  • 点击某一个商品时跳转到商品的详情页面。

2.商品详情页

  • 显示出某个商品的详情信息。
  • 页面的左下方显示出该种类商品的2个新品信息。

3.商品列表页

  • 显示出某一个种类商品的列表数据,分页显示并支持按照默认、价格、和人气进行排序。
  • 页面的左下方显示出该种类商品的2个新品信息。

4.其他

  • 通过页面搜索框搜索商品信息。

1.2.1.3、购物车相关

  • 列表页和详情页将商品添加到购物车。
  • 用户登录后,首页,详情页,列表页显示登录用户购物车中商品的数目。
  • 购物车页面:对用户购物车中商品的操作。如选择某件商品,增加或减少购物车中商品的数目。

1.2.1.4、订单相关

  • 提交订单页面:显示用户准备购买的商品信息。
  • 点击提交订单完成订单的创建。
  • 用户中心订单页显示用户的订单信息。
  • 点击支付完成订单的支付。

1.2.2、项目架构概览

1.2.2.1、页面图:

1.2.2.2、功能图:

1.2.2.3、部署图:

1.2.3、SKU与SPU概念

    SPU = Standard Product Unit (标准产品单位)

    SPU 是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述 了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个 SPU。 例如:iphone7 就是一个 SPU,与商家,与颜色、款式、套餐都无关。

    SKU=stock keeping unit(库存量单位)

    SKU 即库存进出计量的单位, 可以是以件、盒、托盘等为单位。 SKU 是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。 在服装、鞋类商品中使用最多最普遍。 例如:纺织品中一个 SKU 通常表示:规格、颜色、款式。

2、项目架构

3、数据库表结构

4、用户认证模型

5、类视图

将视图view以类的形式定义

通用类视图基类:

django.views.generic.View  ( 与django.views.generic.base.View 是同一个)

urls.py中配置路由使用类视图的as_view()方法

由dispatch()方法具体将请求request分发至对应请求方式的处理方法中(get、post等)

类视图资料:内置基于类的视图 API | Django 文档 | Django

6、用户模块开发

6.1、Django4.0认证系统文档

django-admin 和 manage.py | Django 文档 | Django

方法名

备注

create_user

创建用户

authenticate

登录验证

login

记录登录状态

logout

退出用户登录

is_authenticated

判断用户是否登录

login_required装饰器

进行登录判断

6.2、用户注册

6.2.1、django发送邮件

 6.2.2、Celery异步任务队列

6.2.2.1、使用

在项目根目录下新建一个celery_task文件夹,文件夹下新建一个task文件:

# 使用celery
from django.core.mail import send_mail
from django.conf import settings
from celery import Celery
import time
from django.template import loader

# 在任务处理者一端加这几句
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dailyfresh.settings")
django.setup()

from goods.models import GoodsType, IndexGoodsBanner, IndexPromotionBanner, IndexTypeGoodsBanner

# 创建一个Celery类的实例对象
app = Celery('celery_tasks.tasks', broker='redis://mast:yourpassword@127.0.0.1:6379/1')  # 使用我本地电脑的redis的1号数据库当做celery的broker,

此段代码在注册视图函数中,异步实现发送邮件功能:

# 发送激活邮件,包含激活链接: http://127.0.0.1:8000/user/active/token值
        # 激活链接中需要包含用户的身份信息, 并且要把身份信息进行加密

        """加密用户的身份信息,生成激活token,使用authlib代替itsdangerous"""
        data = {'confirm': user.id}
        header = {'alg': 'HS256'}  # 签名算法
        token = jwt.encode(header=header, payload=data, key=settings.SECRET_KEY)  # byte类型
        token = token.decode()

        # 发送邮箱:使用celery异步处理发送邮件,加入到任务队列(broker)中
        send_register_active_email.delay(email, username, token)

        # 返回应答, 跳转到首页
        return redirect(reverse('goods:index'))

6.2.2.2、发出任务

6.2.2.3、启动worker

重新打开一个终端,输入命令celery -A celery_tasks.tasks worker -l info --pool solo

6.3、用户激活

使用authlib加密用户的身份信息,而不是使用原先项目自带的itsdangerous。

authlib的使用:

参考资料:

JSON Web Token (JWT) — Authlib 1.0.1 documentation

6.3.1、加密用户身份信息

from authlib.jose import jwt, JoseError


"""加密用户的身份信息,生成激活token,使用authlib代替itsdangerous"""
    data = {'confirm': user.id}
    header = {'alg': 'HS256'}  # 签名算法
    token = jwt.encode(header=header, payload=data, key=settings.SECRET_KEY)  # byte类型
    token = token.decode()

6.3.2、解密用户身份信息验证

# 进行解密,获取要激活的用户信息
    try:
        info = jwt.decode(token, settings.SECRET_KEY)
        print(info)
        # 获取待激活用户的id
        user_id = info['confirm']

        # 根据id获取用户信息
        user = User.objects.get(id=user_id)
        user.is_active = 1
        user.save()

6.4、用户登录

6.4.1、配置redis作为Django缓存和session后端

django-redis文档:django-redis 中文文档 — Django-Redis 4.7.0 文档

配置:

# Django的缓存配置
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/2",  # 使用2号数据库
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "PASSWORD": "yourpassword"
        }
    }
}

6.4.2、登录判断装饰器login_required

可以在用户模块的地址配置直接使用:

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),  # 使用视图类
    re_path(r'active/(?P<token>.*)$', ActiveView.as_view(), name='active'),  # 激活用户
    path('login/', LoginView.as_view(), name='login'),

    path('', UserInfoView.as_view(), name='user'),  # 用户信息-中心
    re_path(r'order/(?P<page>\d+)$', UserOrderView.as_view(), name='order'),  # 用户信息-订单
    path('address/', AddressView.as_view(), name='address'),  # 用户信息-地址
    path('logout/', LogoutView.as_view(), name='logout'),  # 用户注销
    path('testcelery/', views.testCelery)

    # path('', login_required(UserInfoView.as_view()), name='user'),  # 用户信息-中心
    # path('order/', login_required(UserOrderView.as_view()), name='order'),  # 用户信息-订单
    # path('address/', login_required(AddressView.as_view()), name='address'),  # 用户信息-地址
    # path('register', views.register, name='register'),
    # path('register_handle', views.register_handle, name='register_handle')
]

或者修改类LoginRequireMixin中的父类的as_view方法:

在setting.py中配置未登录时的跳

6.5、用户退出

logout函数清除登录用户的session信息。

6.6、用户地址

模型类和模型管理器类:

6.7、用户个人信息

6.7.1 redis存储历史浏览记录分析

 

参考资料:

Redis 命令参考 — Redis 命令参考

Welcome to redis-py’s documentation! — redis-py dev documentation

6.7.2、django-redis获取redis链接

from django_redis import get_redis_connection


on = get_redis_connection('default')  # 使用setting.py中默认的redis配置

7、商品模块开发

7.1、了解FastDFS分布式文件系统

(我个人是把FastDFS部署在自己的阿里云服务器上,安装到云服务器上去访问是也遇到了很多问题,文末总结问题)

集群

启动FastDFS的方法,需要的操作:

修改如下的配置文件  (在/etc/fdfs目录中)

 

tracker_server=己的ip:22122

启动tracker、storage、nginx服务:

sudo service fdfs_trackerd start

sudo service fdfs_storaged start

sudo /usr/local/nginx/sbin/nginx

执行如下命令测试是否成功

fdfs_upload_file /etc/fdfs/client.conf 要上片文件

如果返回类似group1/M00/00/00/rBIK6VcaP0aARXXvAAHrUgHEviQ394.jpg的文件id则说明文件上传成功

在浏览器中可以用   127.0.0.1:8888/返回的文件id  

访问图片

7.2、python对接fastdfs

(根据视频提供的方法这一步也有出现一些问题,但还是给出,文末总结解决问题)

1. workon django_py3

2. 进入fdfs_client-py-master.zip所在目录

3. pip install fdfs_client-py-master.zip

7.3、项目上传图片和使用图片流程

 

海量存储,存储容量扩展方便。

文件内容重复。

结合nginx提高网站访问图片的效率。

7.4、Django二次开发对接FastDFS

配置文件settings中加入如下配置:

# 设置Django的文件存储类
DEFAULT_FILE_STORAGE = 'utils.fdfs.storage.FDFSStorage'

# 设置fdfs使用的client.conf配置文件
FDFS_CLIENT_CONF = r'D:\django4.0\dailyfresh\utils\fdfs\client.conf'

# 设置我的阿里云服务器上的fdfs的nginx的IP和端口号
FDFS_URL = 'http://121.89.236.181:8888/'

创建utils/fdfs 目录:

storage.py文件中自定义文件存储类:

from django.core.files.storage import Storage
from fdfs_client.client import Fdfs_client
from dailyfresh import settings


class FDFSStorage(Storage):
    """自定义fastdfs文件存储类"""

    def __init__(self, client_conf=None, base_url=None):
        """动态地对storage类进行配置"""

        if client_conf is None:
            client_conf = settings.FDFS_CLIENT_CONF
        self.client_conf = client_conf
        if base_url is None:
            base_url = settings.FDFS_URL
        self.base_url = base_url

    def save(self, name, content, max_length=None):  # 一定需要添加参数max_length
        """name为上传文件的名字,content包含上传文件内容的File对象"""

        client = Fdfs_client(self.client_conf)
        # 上传文件到我的阿里云服务器fastdfs系统中
        result = client.upload_by_buffer(content.read())
        if result.get('Status') != 'Upload successed.':
            # 上传失败
            raise Exception('上传文件到云服务器的fastdfs系统失败!')
        filename = result.get('Remote file_id')

        # save方法最后返回的内容为保存在django系统中的图片文件名称
        return filename

    def url(self, name):
        """返回访问文件的url地址,name为保存在django数据表中的为文件名"""

        return self.base_url + name

7.5、商品首页

7.5.1、基本页面处理

动态给对象增加属性

7.5.2、保存购物车信息的数据设计

7.5.3 页面静态化

把原本动态的页面处理结果保存成html文件,让用户直接访问这个生成出来的静态的html页面

配置在管理员页面的方法中:

from django.contrib import admin
from goods.models import GoodsType, IndexTypeGoodsBanner, IndexPromotionBanner, IndexGoodsBanner, GoodsSKU
from django.core.cache import cache


class BaseModelAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        """新增或更新表中的数据时调用"""

        super().save_model(request, obj, form, change)

        # 发出任务,让celery worker重新生成首页静态页
        from celery_tasks.tasks import generate_static_index_html
        generate_static_index_html.delay()

        # 清除首页的缓存数据
        cache.delete('index_page_data')

    def delete_model(self, request, obj):
        """删除表中的数据时调用"""
        
        super().delete_model(request, obj)
        # 发出任务,让celery worker重新生成首页静态页
        from celery_tasks.tasks import generate_static_index_html
        generate_static_index_html.delay()

        # 清除首页的缓存数据
        cache.delete('index_page_data')

7.5.4、使用缓存

将处理计算的结果先临时保存起来,下次使用的时候可以先直接使用,如果没有这个备份的数据,重新进行计算处理

将缓存数据保存在内存中 (本项目中保存在redis中)

cache

修改了数据库的数据,直接删除缓存

缓存要设置有效期

7.6、商品详情页

添加历史浏览记录:

 # 获取用户购物车中商品的数目
        user = request.user
        cart_count = 0
        if user.is_authenticated:
            # 用户已登录
            conn = get_redis_connection('default')
            cart_key = 'cart_%d' % user.id
            cart_count = conn.hlen(cart_key)

            # 添加用户的历史记录
            conn = get_redis_connection('default')
            history_key = 'history_%d' % user.id
            # 移除列表中的goods_id
            conn.lrem(history_key, 0, goods_id)
            # 把goods_id插入到列表的左侧
            conn.lpush(history_key, goods_id)
            # 只保存用户最新浏览的5条信息
            conn.ltrim(history_key, 0, 4)

7.7、商品列表页

7.7.1、分页

django有自带的分页器:分页 | Django 文档 | Django

 

7.7.2、页码控制

7.8、商品搜索

7.8.1、安装和配置

(在win10环境下安装haystack、whoosh和jieba会遇到bug)

安装python包。

pip install django-haystack

pip install whoosh

在settings.py文件中注册应用haystack并做如下配置:

# 全文检索框架的配置
HAYSTACK_CONNECTIONS = {
    'default': {
        # 使用whoosh引擎
        # 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
        'ENGINE': 'haystack.backends.whoosh_cn_backend.WhooshEngine',  # 使用自定义用jieba中文分词库修改过的引擎
        # 索引文件路径
        'PATH': Path.joinpath(BASE_DIR, 'whoosh_index'),
    }
}

7.8.2、索引文件生成

在goods应用目录下新建一个search_indexes.py文件,在其中定义一个商品索引类:

在templates下面新建目录search/indexes/goods:

在此目录下面新建一个文件goodssku_text.txt并编辑内容如下:

 

使用命令生成索引文件。

      python manage.py rebuild_index

7.8.3、全文检索的使用

1) 在项目目录下的url文件配置url:

urlpatterns = [
    path('admin/', admin.site.urls),
    # 做url反向解析,动态获取url地址。namespace是放在include里面,include还需要传一个元组,元组内还要有app_name。
    path('user/', include(('user.urls', 'user'), namespace='user')),  # 用户模块
    path('cart/', include(('cart.urls', 'cart'), namespace='cart')),  # 购物车模块
    path('order/', include(('order.urls', 'order'), namespace='order')),  # 订单模块
    path('tinymce/', include('tinymce.urls')),  # 富文本编辑器
    path('search/', include('haystack.urls')),  # 全文检索框架
    path('', include(('goods.urls', 'goods'), namespace='goods')),  # 商品模块,作为主页
]

2)表单搜索时设置表单内容如下:

点击标题进行提交时,会通过haystack搜索数据。

全文检索结果:

搜索出结果后,haystack会把搜索出的结果传递给templates/search目录下的search.html,传递的上下文包括:

query:搜索关键字

page:当前页的page对象 –>遍历page对象,获取到的是SearchResult类的实例对象,对象的属性object才是模型类的对象。

paginator:分页paginator对象

通过HAYSTACK_SEARCH_RESULTS_PER_PAGE 可以控制每页显示数量。

7.8.4、改变分词方式

1) 安装jieba分词模块。

pip install jieba

去到conda虚拟环境下python的下的haystack目录(因为我是用pip install安装的包):

/home/python/.virtualenvs/bj17_py3/lib/python3.5/site-packages/haystack/backends/

在上面的目录中创建ChineseAnalyzer.py文件:

import jiebafrom whoosh.analysis import Tokenizer, Token class ChineseTokenizer(Tokenizer):  def __call__(self, value, positions=False, chars=False,  keeporiginal=False, removestops=True,  start_pos=0, start_char=0, mode='', **kwargs):  t = Token(positions, chars, removestops=removestops, mode=mode, **kwargs)  seglist = jieba.cut(value, cut_all=True)  for w in seglist:  t.original = t.text = w  t.boost = 1.0  if positions:   t.pos = start_pos + value.find(w)  if chars:  t.startchar = start_char + value.find(w)  t.endchar = start_char + value.find(w) + len(w)  yield t def ChineseAnalyzer():  return ChineseTokenizer()

复制whoosh_backend.py文件,改为如下名称。

        whoosh_cn_backend.py

打开复制出来的新文件,引入中文分析类,内部采用jieba分词。

from .ChineseAnalyzer import ChineseAnalyzer

更改词语分析类:

查找 analyzer=StemmingAnalyzer() 改为 analyzer=ChineseAnalyzer()

修改settings.py文件中的配置项。

重新创建索引数据

python manage.py rebuild_index

8、购物车模块开发

8.1、添加到购物车

确定前端是否传递数据,传递什么数据,什么格式

确定前端访问的方式(get  post)

确定返回给前端的什么数据,什么格式

8.2、购物车页面

购物车页面js

Jquery选择器参考资料:

http://www.w3school.com.cn/jquery/jquery_ref_selectors.asp

8.3、购物车记录更新

采用ajax post请求

前端需要传递的参数:商品id(sku_id) 商品数量(count)

8.4、购物车记录删除

采用ajax post请求

前端需要传递的参数:商品id(sku_id)

9、订单模块开发

9.1、提交订单页面

9.2、订单生成

9.2.1、mysql事务

  • 事务概念

一组mysql语句,要么执行,要么全不不执行。

事务的特点

1原子性:一组事务,要么成功;要么撤回。

2稳定性 有非法数据(外键约束之类),事务撤回。

3隔离性事务独立运行。一个事务处理后的结果,影响了其他事务,那么其他事务会撤回。事务的100%隔离,需要牺牲速度。

4可靠性:软、硬件崩溃后,InnoDB数据表驱动会利用日志文件重构修改。可靠性和高速度不可兼得, innodb_flush_log_at_trx_commit 选项 决定什么时候吧事务保存到日志里。

  • 事务控制语句

BEGINSTART TRANSACTION;显式地开启一个事务;

COMMIT;也可以使用COMMIT WORK,不过二者是等价的。COMMIT会提交事务,并使已对数据库进行的所有修改称为永久性的;

ROLLBACK;有可以使用ROLLBACK WORK,不过二者是等价的。回滚会结束用户的务,并撤销正在进行的所有未提交的修改;

SAVEPOINT identifierSAVEPOINT允许在事务中创建一个保存点,一个事务中可以有多个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)数据,而另一个事务却在此时插入了新的几行数据,先前的事务在接下来的查询中,就会发现有几行数据是它先前所没有的。

  • 设置mysql事务的隔离级别

打开mysql配置文件: sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf, 添加如下行。

保存配置文件,重启mysql服务。

      sudo service mysql restart

9.2.2、订单并发处理

9.2.1.1、悲观锁

select * from df_goods_sku where id=17 for update;

 

订单创建(悲观锁):

# 前端传递的参数:地址id(addr_id) 支付方式(pay_method) 用户要购买的商品id字符串(sku_ids)
# mysql事务: 一组sql操作,要么都成功,要么都失败
# 高并发:秒杀
# 支付宝支付
class OrderCommitView1(View):
    """订单创建(悲观锁版本)"""

    @transaction.atomic
    def post(self, request):
        """订单创建"""

        # 判断用户是否登录
        user = request.user
        if not user.is_authenticated:
            # 用户未登录
            return JsonResponse({'res': 0, 'errmsg': '用户未登录'})

        # 接收参数
        addr_id = request.POST.get('addr_id')
        pay_method = request.POST.get('pay_method')
        sku_ids = request.POST.get('sku_ids')  # 1,3

        # 校验参数
        if not all([addr_id, pay_method, sku_ids]):
            return JsonResponse({'res': 1, 'errmsg': '参数不完整'})

        # 校验支付方式
        if pay_method not in OrderInfo.PAY_METHODS.keys():
            return JsonResponse({'res': 2, 'errmsg': '非法的支付方式'})

        # 校验地址
        try:
            addr = Address.objects.get(id=addr_id)
        except ObjectDoesNotExist:
            # 地址不存在
            return JsonResponse({'res': 3, 'errmsg': '地址非法'})

        # todo: 创建订单核心业务

        # 组织参数
        # 订单id: 20171122181630+用户id
        order_id = datetime.now().strftime('%Y%m%d%H%M%S') + str(user.id)

        # 运费
        transit_price = 10

        # 总数目和总金额
        total_count = 0
        total_price = 0

        # 设置事务保存点
        save_id = transaction.savepoint()
        try:
            # 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)

            # 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:
                    # select * from df_goods_sku where id=sku_id for update;(加锁)
                    sku = GoodsSKU.objects.select_for_update().get(id=sku_id)
                except:
                    # 商品不存在
                    transaction.savepoint_rollback(save_id)
                    return JsonResponse({'res': 4, 'errmsg': '商品不存在'})

                print('user:%d stock:%d' % (user.id, sku.stock))
                # import time
                # time.sleep(10)

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

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

                # 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

            # todo: 更新订单信息表中的商品的总数量和总价格
            order.total_count = total_count
            order.total_price = total_price
            order.save()
        except Exception as e:
            transaction.savepoint_rollback(save_id)
            return JsonResponse({'res': 7, 'errmsg': '下单失败'})

        # 提交事务
        transaction.savepoint_commit(save_id)

        # todo: 清除用户购物车中对应的记录
        conn.hdel(cart_key, *sku_ids)

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

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

  • 9.2.2.2、乐观锁

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

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

冲突比较少的时候,使用乐观锁。

冲突比较多的时候,使用悲观锁。

订单创建(乐观锁):

class OrderCommitView(View):
    """订单创建(乐观锁版本)"""

    @transaction.atomic
    def post(self, request):
        """订单创建"""

        # 判断用户是否登录
        user = request.user
        if not user.is_authenticated:
            # 用户未登录
            return JsonResponse({'res': 0, 'errmsg': '用户未登录'})

        # 接收参数
        addr_id = request.POST.get('addr_id')
        pay_method = request.POST.get('pay_method')
        sku_ids = request.POST.get('sku_ids')  # 1,3

        # 校验参数
        if not all([addr_id, pay_method, sku_ids]):
            return JsonResponse({'res': 1, 'errmsg': '参数不完整'})

        # 校验支付方式
        if pay_method not in OrderInfo.PAY_METHODS.keys():
            return JsonResponse({'res': 2, 'errmsg': '非法的支付方式'})

        # 校验地址
        try:
            addr = Address.objects.get(id=addr_id)
        except ObjectDoesNotExist:
            # 地址不存在
            return JsonResponse({'res': 3, 'errmsg': '地址非法'})

        # todo: 创建订单核心业务

        # 组织参数
        # 订单id: 20171122181630+用户id
        order_id = datetime.now().strftime('%Y%m%d%H%M%S') + str(user.id)

        # 运费
        transit_price = 10

        # 总数目和总金额
        total_count = 0
        total_price = 0

        # 设置事务保存点
        save_id = transaction.savepoint()
        try:
            # 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)

            # 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:
                for i in range(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)

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

                    # todo: 更新商品的库存和销量
                    orgin_stock = sku.stock
                    new_stock = orgin_stock - int(count)
                    new_sales = sku.sales + int(count)

                    # print('user:%d times:%d stock:%d' % (user.id, i, sku.stock))
                    # import time
                    # time.sleep(10)

                    # 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': '下单失败2'})
                        continue

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

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

                    # 跳出循环
                    break

            # todo: 更新订单信息表中的商品的总数量和总价格
            order.total_count = total_count
            order.total_price = total_price
            order.save()
        except Exception as e:
            transaction.savepoint_rollback(save_id)
            return JsonResponse({'res': 7, 'errmsg': '下单失败'})

        # 提交事务
        transaction.savepoint_commit(save_id)

        # todo: 清除用户购物车中对应的记录
        conn.hdel(cart_key, *sku_ids)

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

9.3、用户中心-订单页

9.4、订单支付

调用支付宝的统一收单下单并支付页面接口:
统一收单下单并支付页面接口 | 网页&移动应用

然后还需要调用支付宝的收单线下交易查询接口:

统一收单线下交易查询接口 | 网页&移动应用

其中需要用到的商家和买家账户信息都在沙箱系统中:

支付宝开放平台

 

 

10、项目部署

10.1、uwsgi

遵循wsgi协议的web服务器。

10.1.1、uwsgi的安装

       pip install uwsgi

10.1.2、uwsgi的配置

项目部署时,需要修改settings.py文件夹下的:

DEBUG=FALSE

ALLOWED_HOSTS=[‘*’]

uwsgi文件:

[uwsgi]
#使用nginx连接时使用
socket=127.0.0.1:8080
#直接做web服务器使用 python manage.py runserver ip:port
#http=127.0.0.1:8080
#项目目录
chdir=D:\django4.0\dailyfresh
#项目中wsgi.py文件的目录,相对于项目目录
wsgi-file=dailyfresh/wsgi.py
#指定启动的工作进程数
processes=4
#指定工作进程中的线程数
threads=2
master=True
#保存启动之后主进程的pid
pidfile=uwsgi.pid
#设置uwsgi后台运行,uwsgi.log保存日志信息
daemonize=uwsgi.log
#设置虚拟环境的路径
virtualenv=D:\Anaconda3\envs\dailyfresh

10.1.3、uwsgi的启动和停止

启动:uwsgi –-ini 配置文件路径 例如:uwsgi –-ini uwsgi.ini

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

10.2、nginx

10.2.1、nginx 配置转发请求给uwsgi

location / {

         include uwsgi_params;

         uwsgi_pass uwsgi服务器的ip:port;

}

10.2.2、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/;

}

10.2.3 nginx转发请求给另外地址

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

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

配置如下:

location = /{

         proxy_pass http://172.16.179.131;

}

10.2.4、nginx配置upstream实现负载均衡

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

upstream dailyfresh {

         server 127.0.0.1:8080;

         server 127.0.0.1:8081;

}

10.2.5、部署项目流程图

  1. 项目总结
  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 (负载均衡  提供静态文件)

11、开发过程中遇到的bug个人总结

1、在模板中载入静态文件前需要在配置文件settings.py中加入配置

2、django新版本中查询数据库后对象不存在报错的异常类是core.exceptions的ObjectDoesNotExist

3、加密用户身份信息当做url的token值,用authlib包代替itsdangerous,可以指定签名算法HS256

4、celery不支持window10,安装eventlet包,使用celery -A celery_tasks.tasks worker -l info -P eventlet -E命令或者建议使用下面的命令就用安装eventlet包

用celery生成静态页面报django.db.utils.DatabaseError错误,改为celery -A celery_tasks.tasks worker -l info --pool solo启动就不会报错

5、django自带的login()报错:redis输入了一个空类型的值

原因:在setting.py中配置redis(密码)时不能直接放在url中

6、Django的authenticate已经包含了is_active判断,即使用户名密码正确,is_active为0也会返回空,所以需要在setting.py中加配置

7、FastDFS只支持Linux,可以把fastDFS运行在云服务器上,Windows本地电脑上运行FastDFS客户端fdfs-client-py

这里我在自己的服务器上改了很久。我目的是为了把FastDFS安装在服务器上,然后自己本地跑代码测试的时候就就可以从云服务器上把图片拉下来。配置过程要按下面的来。

可以把云服务器上FastDFS的storage.conf, client.conf, mod_fastdfs.conf这3个文件的tracker_ip都设为外网IP。
每次更改后需要对tracker和storage这俩个服务器重启,已经重启nginx服务,然后fdfs_monitor /etc/fdfs/client.conf检查一下监控信息。
本地上传图片到云服务器的FastDFS时的自定义的storage.py代码文件中,参数为路径的方法不能是client.upload_by_file或client.upload_by_filename,否则就会报错。

然后django上传图片到服务器返回来的路径没有后缀,所以要用浏览器的前端代码去解析

8、window启动nginx,先到文件夹中启动nginx.exe,再到cmd中输入nginx.exe。停止nginx,在cmd中完整有序停止用nginx -s quit,再用taskkill /f /t /im nginx.exe

9、is_authenticated在Django4.0中是一个属性而不是一个方法

10、Django4.0已经移除ungettext,所以在引入haystack的时候会报错,修改haystack文件夹下的admin.py把ungettext三处地方修改为ngettext

11、Django4.0已经移除smart_text,修改haystack文件夹下的form.py俩处地方把smart_text修改为smart_str

12、配置为乐观锁时,需要设置MySQL默认的隔离级别(可重复读)为(读取提交内容),在MySQL配置文件skip-ex,,,下添加一行transaction-isolation = READ-COMMITTED

13、网站如果想让支付宝平台访问(获取支付后的结果),需要有公网IP

14、安装alipay-sdk-python 提示安装pycrypto 问题:

直接先pip install pycryptodome,下载alipay-sdk-python-3.3.398放到Lib\site-packages 的文件目录下,
进入到文件中修改 setup.py, 将requires = ["pycrypto","rsa"] 修改成 requires = ["crypto","rsa"]

15、支付宝沙箱测试不需要重新设置密钥和公钥,直接使用默认。

使用支付宝网页支付接口一直报错商户ID错误,原因settle_detail_info.trans_in才是设置商户ID(PID),而不是设置买家id的
sub_merchant.merchant_id是设置子商户商户id的而不是商户ID的

16、使用支付宝交易查询接口要使用AlipayTradeQueryResponse来获取解析后响应的结果

返回的数据就封装在这个类里,像使用交易支付接口一样的方法就会报Alipay SDK源代码出错

17、win10上没法部署uwsgi,可以通过其它方式间接使用uwsgi,但没必要,最终都是要部署在Linux系统。

18、Django网站响应慢(修改数据库查询方式)

CONN_MAX_AGE(定义连接的最大生存期的参数)=None设置为无限制的持久连接,避免了在每个请求中重新建立到数据库的连接的开销,但是
除了get方法外,all,filter,exclude,order_by方法都会返回一个查询集(QuerySet)
查询集特性:

1、惰性查询:只有在实际使用查询集中的数据的时候才会发生对数据库的真正查询。
2、缓存:当使用的是同一个查询集时,第一次使用的时候会发生实际数据库的查询,然后把结果缓存起来,之后再使用这个查询集时,使用的是缓存中的结果。

19、指定表名,不用一定和应用名相连,class Meta:  db_table = '表名'

20、发布网站时需要在settings.py中把debug改为false,allowed_host = ['*']

21、cookie:是由服务器生成,存储在浏览器端的一小段文本信息。

设置:HttpResponse('').set_cookie('key', value, max_age=计算秒数,expires=)
获取:reques.COOKIE

22、session

浏览器访问服务器,在django_session表中设置session信息,表中有个session_key和session_data,session_key这一列是唯一标识码,session_data这一列是设置的信息。
然后django拿着这个唯一标识码设置一个cookie名为sessionid,下次浏览器访问服务器就会根据sessionid的值取出对应session的信息(这一步django会自动帮我们做),放在request.session中。
设置:request.session['']=
获取:request.session['']

23、模板中的自定义过滤器参数只能是一个或俩个

模板继承:所有页面都相同的内容都放在父模板文件中{% extensds 'booktest/base.html' %}
父模板需要预留一个块,{% block 块名 %}  {% endblock 块名 %} 

24、csrf防护(跨站请求伪造攻击):

只针对post请求,在有post请求的模板下加一个模板标签{% csrf_token %}
    1)渲染模板文件时在页面生成一个名字叫做csrfmiddlewaretoken的隐藏域。
    2)服务器交给浏览器保存一个名字为csrftoken的cookie信息。
    3)提交表单时,两个值都会发给服务器,服务器进行比对,如果一样,则csrf验证通过,否则失败。

25、url反向解析:

某一个url配置的地址发生变化时,页面上使用反向解析生成地址的位置不需要发生变化。
      from django.urls import reverse

  • 11
    点赞
  • 94
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值